feat(desktop-client/wangyihao): persist session cookies and recover bound partitions

Snapshot 163.com/126.net cookies on bind/refresh to a userData vault and
restore them when reattaching a session, recover orphan partitions by
matching the detected mediaInfo, verify the bind window via the console
state instead of a generic profile probe, and route token/draft fetches
through the https API origin while opening console pages over http to
match the live editor.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-30 19:18:03 +08:00
parent c87842347e
commit b89892be6e
2 changed files with 530 additions and 40 deletions
@@ -69,9 +69,11 @@ type WangyiPublishDetail = {
newProminentFlag: string;
};
const WANGYI_ORIGIN = "https://mp.163.com";
const WANGYI_HOME_URL = `${WANGYI_ORIGIN}/subscribe_v4/index.html`;
const WANGYI_MANAGE_URL = `${WANGYI_ORIGIN}/subscribe_v4/index.html#/article-manage`;
const WANGYI_API_ORIGIN = "https://mp.163.com";
const WANGYI_CONSOLE_ORIGIN = "http://mp.163.com";
const WANGYI_API_REFERER = `${WANGYI_API_ORIGIN}/subscribe_v4/index.html`;
const WANGYI_HOME_URL = `${WANGYI_CONSOLE_ORIGIN}/subscribe_v4/index.html`;
const WANGYI_MANAGE_URL = `${WANGYI_CONSOLE_ORIGIN}/subscribe_v4/index.html#/article-manage`;
const WANGYI_CREATIVE_STATEMENT = "个人原创,仅供参考";
const WANGYI_PUBLISH_DELAY_MS = 2_000;
const WANGYI_TOKEN_WAIT_MS = 12_000;
@@ -155,8 +157,8 @@ async function wangyiHeaders(
(await sessionCookieHeader(context.session, "163.com").catch(() => ""));
return {
accept: "application/json, text/plain, */*",
origin: WANGYI_ORIGIN,
referer: WANGYI_HOME_URL,
origin: WANGYI_API_ORIGIN,
referer: WANGYI_API_REFERER,
...(cookie ? { cookie } : {}),
...extra,
};
@@ -165,7 +167,7 @@ async function wangyiHeaders(
async function fetchAccount(context: PublishAdapterContext): Promise<WangyiAccount | null> {
const response = await sessionFetchJson<WangyiInfoResponse>(
context.session,
`${WANGYI_ORIGIN}/wemedia/info.do`,
`${WANGYI_API_ORIGIN}/wemedia/info.do`,
{
credentials: "include",
headers: await wangyiHeaders(context),
@@ -191,7 +193,7 @@ async function fetchPublishDetail(
newProminentFlag: "",
};
const url = new URL(`${WANGYI_ORIGIN}/wemedia/article/postpage.do`);
const url = new URL(`${WANGYI_API_ORIGIN}/wemedia/article/postpage.do`);
url.searchParams.set("_", String(Date.now()));
url.searchParams.set("wemediaId", account.wemediaId);
url.searchParams.set("mediaId", account.wemediaId);
@@ -224,13 +226,24 @@ async function getPublishToken(context: PublishAdapterContext): Promise<string>
throw new Error("wangyihao_playwright_missing");
}
await page.goto(WANGYI_HOME_URL, {
waitUntil: "domcontentloaded",
});
for (let attempt = 0; attempt < 2; attempt += 1) {
await page.goto(WANGYI_HOME_URL, {
waitUntil: attempt === 0 ? "domcontentloaded" : "load",
timeout: 20_000,
}).catch(() => null);
await page.waitForLoadState("networkidle", { timeout: 3_000 }).catch(() => null);
if (attempt > 0) {
await sleep(800, context.signal);
}
// 网易号的 neg 有时是顶层绑定,不一定挂在 window 上;这里保持和旧版插件一致的读取方式。
const token = await readPublishTokenFromPage(page);
return token.trim();
// 网易号的 neg 有时是顶层绑定,不一定挂在 window 上;这里保持和旧版插件一致的读取方式。
const token = (await readPublishTokenFromPage(page)).trim();
if (token) {
return token;
}
}
return "";
}
async function readPublishTokenFromPage(page: Page): Promise<string> {
@@ -415,7 +428,7 @@ async function uploadImage(
form.append("from", "neteasecode_mp");
form.append("logotext", account.name);
const url = new URL(`${WANGYI_ORIGIN}/api/v3/upload/picupload`);
const url = new URL(`${WANGYI_API_ORIGIN}/api/v3/upload/picupload`);
url.searchParams.set("_", String(Date.now()));
url.searchParams.set("wemediaId", account.wemediaId);
url.searchParams.set("realUserId", account.realUserId);
@@ -444,7 +457,7 @@ async function saveDraft(
html: string,
token: string,
): Promise<string> {
const url = new URL(`${WANGYI_ORIGIN}/wemedia/article/status/api/publishV2.do`);
const url = new URL(`${WANGYI_API_ORIGIN}/wemedia/article/status/api/publishV2.do`);
url.searchParams.set("_", String(Date.now()));
const body = buildDraftBody(context, account, detail, html, token);
@@ -772,7 +785,7 @@ async function publishDraft(context: PublishAdapterContext, draftId: string): Pr
throw new Error("wangyihao_playwright_missing");
}
const draftUrl = `https://mp.163.com/subscribe_v4/index.html#/article-publish/${encodeURIComponent(draftId)}?option=editDraft`;
const draftUrl = `${WANGYI_CONSOLE_ORIGIN}/subscribe_v4/index.html#/article-publish/${encodeURIComponent(draftId)}?option=editDraft`;
await page.goto(draftUrl, {
waitUntil: "domcontentloaded",
});