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:
@@ -1,9 +1,9 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { readdirSync, statSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
|
||||
import { BrowserWindow, app, nativeTheme, session as electronSession } from "electron/main";
|
||||
import type { Session, WebContents } from "electron/main";
|
||||
import type { Cookie, Session, WebContents } from "electron/main";
|
||||
import { aiPlatformCatalog } from "@geo/shared-types";
|
||||
import type { DesktopAccountInfo } from "@geo/shared-types";
|
||||
|
||||
@@ -160,14 +160,31 @@ type ZhihuMeResponse = {
|
||||
};
|
||||
|
||||
type WangyiInfoResponse = {
|
||||
code?: number | string;
|
||||
message?: string;
|
||||
msg?: string;
|
||||
data?: {
|
||||
mediaInfo?: {
|
||||
userId?: string | number;
|
||||
wemediaId?: string | number;
|
||||
tname?: string;
|
||||
icon?: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
type WangyiInfoFetchOptions = {
|
||||
allowCookieWrite?: boolean;
|
||||
};
|
||||
type StoredWangyihaoCookie = Pick<
|
||||
Cookie,
|
||||
"expirationDate" | "hostOnly" | "httpOnly" | "name" | "path" | "sameSite" | "secure" | "session" | "value"
|
||||
> & {
|
||||
domain: string;
|
||||
};
|
||||
type WangyihaoCookieVault = Record<string, {
|
||||
updatedAt: number;
|
||||
cookies: StoredWangyihaoCookie[];
|
||||
}>;
|
||||
|
||||
type JianshuUser = {
|
||||
id?: string | number;
|
||||
@@ -259,6 +276,23 @@ const ZOL_LOGIN_REDIRECT_URLS = [
|
||||
"https://passport.zol.com.cn/",
|
||||
"https://service.zol.com.cn/user/login",
|
||||
];
|
||||
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_INFO_URL = `${WANGYI_API_ORIGIN}/wemedia/info.do`;
|
||||
const WANGYI_CONSOLE_URL = `${WANGYI_CONSOLE_ORIGIN}/subscribe_v4/index.html`;
|
||||
const WANGYI_LOGIN_REDIRECT_URLS = [
|
||||
`${WANGYI_API_ORIGIN}/login.html`,
|
||||
`${WANGYI_CONSOLE_ORIGIN}/login.html`,
|
||||
"https://reg.163.com/",
|
||||
"https://dl.reg.163.com/",
|
||||
];
|
||||
const WANGYI_COOKIE_URLS = [
|
||||
WANGYI_API_ORIGIN,
|
||||
WANGYI_CONSOLE_ORIGIN,
|
||||
"https://reg.163.com/",
|
||||
"https://dl.reg.163.com/",
|
||||
];
|
||||
const BILIBILI_WBI_MIXIN_KEY_ENC_TAB = [
|
||||
46, 47, 18, 2, 53, 8, 23, 32,
|
||||
15, 50, 10, 31, 58, 3, 45, 35,
|
||||
@@ -1586,6 +1620,281 @@ async function hasToutiaoAuthCookies(session: Session): Promise<boolean> {
|
||||
return false;
|
||||
}
|
||||
|
||||
function extractWangyihaoAccount(response: WangyiInfoResponse | null | undefined): DetectedAccount | null {
|
||||
const media = response?.data?.mediaInfo;
|
||||
const uid = stringId(media?.userId) || stringId(media?.wemediaId);
|
||||
const displayName = normalizeText(media?.tname);
|
||||
if (!uid || !displayName) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
platformUid: uid,
|
||||
displayName,
|
||||
avatarUrl: normalizeRemoteUrl(media?.icon),
|
||||
};
|
||||
}
|
||||
|
||||
async function fetchWangyihaoInfo(
|
||||
session: Session,
|
||||
options: WangyiInfoFetchOptions = {},
|
||||
): Promise<WangyiInfoResponse | null> {
|
||||
const headers = {
|
||||
accept: "application/json, text/plain, */*",
|
||||
referer: WANGYI_API_REFERER,
|
||||
origin: WANGYI_API_ORIGIN,
|
||||
};
|
||||
|
||||
if (options.allowCookieWrite) {
|
||||
return await sessionFetchJson<WangyiInfoResponse>(
|
||||
session,
|
||||
WANGYI_INFO_URL,
|
||||
{
|
||||
credentials: "include",
|
||||
headers,
|
||||
},
|
||||
).catch(() => null);
|
||||
}
|
||||
|
||||
return await sessionCookieFetchJson<WangyiInfoResponse>(
|
||||
session,
|
||||
WANGYI_INFO_URL,
|
||||
{ headers },
|
||||
).catch(() => null);
|
||||
}
|
||||
|
||||
async function detectWangyihaoFromSession(session: Session): Promise<DetectedAccount | null> {
|
||||
return extractWangyihaoAccount(await fetchWangyihaoInfo(session));
|
||||
}
|
||||
|
||||
function wangyihaoCookieVaultPath(): string {
|
||||
return join(app.getPath("userData"), "desktop-wangyihao-cookies.json");
|
||||
}
|
||||
|
||||
function readWangyihaoCookieVault(): WangyihaoCookieVault {
|
||||
try {
|
||||
return JSON.parse(readFileSync(wangyihaoCookieVaultPath(), "utf8")) as WangyihaoCookieVault;
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
function writeWangyihaoCookieVault(vault: WangyihaoCookieVault): void {
|
||||
const target = wangyihaoCookieVaultPath();
|
||||
mkdirSync(dirname(target), { recursive: true });
|
||||
writeFileSync(target, JSON.stringify(vault, null, 2), "utf8");
|
||||
}
|
||||
|
||||
function isWangyihaoCookie(cookie: Cookie): boolean {
|
||||
const domain = cookie.domain?.toLowerCase() ?? "";
|
||||
return domain.endsWith("163.com") || domain.endsWith("126.net");
|
||||
}
|
||||
|
||||
function dedupeWangyihaoCookies(cookies: Cookie[]): StoredWangyihaoCookie[] {
|
||||
const seen = new Set<string>();
|
||||
const result: StoredWangyihaoCookie[] = [];
|
||||
for (const cookie of cookies) {
|
||||
const domain = cookie.domain;
|
||||
if (!domain || !isWangyihaoCookie(cookie) || !cookie.value) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const key = [
|
||||
domain,
|
||||
cookie.path,
|
||||
cookie.name,
|
||||
cookie.secure ? "secure" : "plain",
|
||||
].join("\n");
|
||||
if (seen.has(key)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(key);
|
||||
result.push({
|
||||
domain,
|
||||
expirationDate: cookie.expirationDate,
|
||||
hostOnly: cookie.hostOnly,
|
||||
httpOnly: cookie.httpOnly,
|
||||
name: cookie.name,
|
||||
path: cookie.path,
|
||||
sameSite: cookie.sameSite,
|
||||
secure: cookie.secure,
|
||||
session: cookie.session,
|
||||
value: cookie.value,
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function urlForStoredWangyihaoCookie(cookie: StoredWangyihaoCookie): string {
|
||||
const domain = cookie.domain.replace(/^\./, "");
|
||||
const protocol = cookie.secure ? "https" : "http";
|
||||
return `${protocol}://${domain}${cookie.path || "/"}`;
|
||||
}
|
||||
|
||||
async function saveWangyihaoSessionCookies(accountId: string, session: Session): Promise<void> {
|
||||
const collected: Cookie[] = [];
|
||||
for (const url of WANGYI_COOKIE_URLS) {
|
||||
collected.push(...await session.cookies.get({ url }).catch(() => []));
|
||||
}
|
||||
|
||||
const cookies = dedupeWangyihaoCookies(collected);
|
||||
if (!cookies.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const vault = readWangyihaoCookieVault();
|
||||
vault[accountId] = {
|
||||
updatedAt: Date.now(),
|
||||
cookies,
|
||||
};
|
||||
writeWangyihaoCookieVault(vault);
|
||||
}
|
||||
|
||||
async function restoreWangyihaoSessionCookies(accountId: string, session: Session): Promise<void> {
|
||||
const snapshot = readWangyihaoCookieVault()[accountId];
|
||||
if (!snapshot?.cookies?.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const cookie of snapshot.cookies) {
|
||||
await session.cookies.set({
|
||||
url: urlForStoredWangyihaoCookie(cookie),
|
||||
name: cookie.name,
|
||||
value: cookie.value,
|
||||
domain: cookie.domain,
|
||||
path: cookie.path || "/",
|
||||
secure: cookie.secure,
|
||||
httpOnly: cookie.httpOnly,
|
||||
sameSite: cookie.sameSite,
|
||||
...(!cookie.session && cookie.expirationDate ? { expirationDate: cookie.expirationDate } : {}),
|
||||
}).catch((error) => {
|
||||
console.warn("[desktop-session] restore wangyihao cookie failed", {
|
||||
accountId,
|
||||
domain: cookie.domain,
|
||||
name: cookie.name,
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function probeWangyihaoSession(
|
||||
account: PublishAccountIdentity,
|
||||
session: Session,
|
||||
options: WangyiInfoFetchOptions = {},
|
||||
): Promise<AuthProbeResult> {
|
||||
const response = await fetchWangyihaoInfo(session, options).catch(() => null);
|
||||
const detected = extractWangyihaoAccount(response);
|
||||
if (detected) {
|
||||
if (!detectedAccountMatchesIdentity(detected, account)) {
|
||||
return {
|
||||
verdict: "expired",
|
||||
reason: "login_redirect",
|
||||
profile: null,
|
||||
sessionFingerprint: sanitizeDetectedAccount(detected).platformUid,
|
||||
evidence: {
|
||||
detectedPlatformUid: detected.platformUid,
|
||||
expectedPlatformUid: account.platformUid,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
verdict: "active",
|
||||
reason: "probe_success",
|
||||
profile: toAccountHealthProfile(toPublishAccountProfile(detected)),
|
||||
sessionFingerprint: sanitizeDetectedAccount(detected).platformUid,
|
||||
};
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return {
|
||||
verdict: "network_error",
|
||||
reason: "network_error",
|
||||
profile: null,
|
||||
sessionFingerprint: null,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
verdict: "expired",
|
||||
reason: "login_redirect",
|
||||
profile: null,
|
||||
sessionFingerprint: null,
|
||||
evidence: {
|
||||
code: response.code ?? null,
|
||||
message: response.message ?? response.msg ?? null,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function hasWangyihaoAuthCookies(session: Session): Promise<boolean> {
|
||||
const authCookieNames = new Set([
|
||||
"P_INFO",
|
||||
"S_INFO",
|
||||
"NTES_SESS",
|
||||
"l_yd_sign",
|
||||
]);
|
||||
const isAuthCookie = (name: string) => authCookieNames.has(name) || name.startsWith("l_s_subscribe");
|
||||
const urls = [
|
||||
WANGYI_API_ORIGIN,
|
||||
WANGYI_CONSOLE_ORIGIN,
|
||||
"https://reg.163.com/",
|
||||
"https://dl.reg.163.com/",
|
||||
];
|
||||
|
||||
for (const url of urls) {
|
||||
const cookies = await session.cookies.get({ url }).catch(() => []);
|
||||
if (cookies.some((cookie) => isAuthCookie(cookie.name) && normalizeText(cookie.value))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
async function recoverWangyihaoSessionHandle(account: PublishAccountIdentity): Promise<SessionHandle | null> {
|
||||
const pendingPartitions = listPendingPartitions("wangyihao");
|
||||
if (!pendingPartitions.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const expectedUid = normalizeText(account.platformUid) ?? "";
|
||||
const expectedName = normalizeComparableText(account.displayName);
|
||||
for (const partition of pendingPartitions) {
|
||||
const candidateSession = electronSession.fromPartition(partition);
|
||||
if (!(await hasWangyihaoAuthCookies(candidateSession))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const detected = await detectWangyihaoFromSession(candidateSession).catch(() => null);
|
||||
if (!detected) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (expectedUid && sameAccountPlatformUid(detected.platformUid, expectedUid)) {
|
||||
console.info("[desktop-session] recovered wangyihao partition by uid", {
|
||||
accountId: account.id,
|
||||
partition,
|
||||
platformUid: detected.platformUid,
|
||||
});
|
||||
return createSessionHandleForPartition(account.id, partition);
|
||||
}
|
||||
|
||||
if (expectedName && normalizeComparableText(detected.displayName) === expectedName) {
|
||||
console.info("[desktop-session] recovered wangyihao partition by name", {
|
||||
accountId: account.id,
|
||||
partition,
|
||||
displayName: detected.displayName,
|
||||
});
|
||||
return createSessionHandleForPartition(account.id, partition);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async function recoverToutiaoSessionHandle(account: PublishAccountIdentity): Promise<SessionHandle | null> {
|
||||
const pendingPartitions = listPendingPartitions("toutiaohao");
|
||||
if (!pendingPartitions.length) {
|
||||
@@ -1651,12 +1960,19 @@ async function findLocalPublishAccountSessionHandle(
|
||||
): Promise<SessionHandle | null> {
|
||||
const existing = getSessionHandle(account.id);
|
||||
if (existing) {
|
||||
if (account.platform === "wangyihao") {
|
||||
await restoreWangyihaoSessionCookies(account.id, existing.session);
|
||||
}
|
||||
return existing;
|
||||
}
|
||||
|
||||
const persisted = getPersistedPartition(account.id);
|
||||
if (persisted) {
|
||||
return createSessionHandle(account.id);
|
||||
const handle = createSessionHandle(account.id);
|
||||
if (account.platform === "wangyihao") {
|
||||
await restoreWangyihaoSessionCookies(account.id, handle.session);
|
||||
}
|
||||
return handle;
|
||||
}
|
||||
|
||||
if (account.platform === "toutiaohao") {
|
||||
@@ -1666,6 +1982,13 @@ async function findLocalPublishAccountSessionHandle(
|
||||
}
|
||||
}
|
||||
|
||||
if (account.platform === "wangyihao") {
|
||||
const recovered = await recoverWangyihaoSessionHandle(account);
|
||||
if (recovered) {
|
||||
return recovered;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1783,6 +2106,10 @@ export async function probePublishAccountSession(
|
||||
};
|
||||
}
|
||||
|
||||
if (account.platform === "wangyihao") {
|
||||
return await probeWangyihaoSession(account, handle.session);
|
||||
}
|
||||
|
||||
const window = createBoundWindow(
|
||||
`${definition.label} 静默探针`,
|
||||
await resolvePublishAccountConsoleURL(definition, handle.session),
|
||||
@@ -1849,7 +2176,9 @@ export async function probePublishAccountSession(
|
||||
};
|
||||
}
|
||||
|
||||
if (definitionLooksLikeLoginRedirect(currentURL, definition)) {
|
||||
const isLoginRedirect = definitionLooksLikeLoginRedirect(currentURL, definition);
|
||||
|
||||
if (isLoginRedirect) {
|
||||
return {
|
||||
verdict: "expired",
|
||||
reason: "login_redirect",
|
||||
@@ -2136,6 +2465,15 @@ export async function silentRefreshPublishAccountSession(
|
||||
return false;
|
||||
}
|
||||
|
||||
if (account.platform === "wangyihao") {
|
||||
const result = await probeWangyihaoSession(account, handle.session, { allowCookieWrite: true })
|
||||
.catch(() => null);
|
||||
if (result?.verdict === "active") {
|
||||
await saveWangyihaoSessionCookies(account.id, handle.session);
|
||||
}
|
||||
return result?.verdict === "active";
|
||||
}
|
||||
|
||||
const window = createBoundWindow(
|
||||
`${definition.label} 静默续期`,
|
||||
await resolvePublishAccountConsoleURL(definition, handle.session),
|
||||
@@ -2199,6 +2537,13 @@ export async function inspectPublishAccountLocalSession(
|
||||
|
||||
const profile = await resolvePublishAccountProfileFromHandle(account, handle);
|
||||
if (!profile) {
|
||||
if (account.platform === "wangyihao") {
|
||||
return {
|
||||
availability: "expired",
|
||||
profile: null,
|
||||
};
|
||||
}
|
||||
|
||||
const consoleReady = await verifyPublishAccountConsoleAccess(account, handle).catch(() => false);
|
||||
if (consoleReady) {
|
||||
return {
|
||||
@@ -2344,6 +2689,121 @@ async function verifyToutiaoConsoleAccess(window: BrowserWindow, session: Sessio
|
||||
return extractToutiaoAccount(sessionResponse) !== null;
|
||||
}
|
||||
|
||||
function isWangyihaoLoggedOutText(text: string): boolean {
|
||||
return /(登录超时|登录态失效|登录状态失效|登录已过期|会话已过期|请重新登录|请先登录|未登录|网易邮箱账号登录|网易邮箱帐号登录|手机号登录|扫码登录)/i
|
||||
.test(text);
|
||||
}
|
||||
|
||||
async function readWangyihaoConsoleState(
|
||||
webContents: WebContents | null | undefined,
|
||||
): Promise<{ loggedOut: boolean; title: string; textSnippet: string } | null> {
|
||||
if (!webContents || webContents.isDestroyed()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const state = await webContents.executeJavaScript(
|
||||
`(() => {
|
||||
try {
|
||||
const text = String(document.body?.innerText || "");
|
||||
const title = String(document.title || "");
|
||||
const source = title + "\\n" + text.slice(0, 1200);
|
||||
return {
|
||||
title,
|
||||
textSnippet: text.slice(0, 500),
|
||||
loggedOut: /(登录超时|登录态失效|登录状态失效|登录已过期|会话已过期|请重新登录|请先登录|未登录|网易邮箱账号登录|网易邮箱帐号登录|手机号登录|扫码登录)/i.test(source),
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
})();`,
|
||||
true,
|
||||
).catch(() => null);
|
||||
|
||||
if (!state || typeof state !== "object") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const typed = state as { loggedOut?: unknown; title?: unknown; textSnippet?: unknown };
|
||||
const title = typeof typed.title === "string" ? typed.title : "";
|
||||
const textSnippet = typeof typed.textSnippet === "string" ? typed.textSnippet : "";
|
||||
return {
|
||||
loggedOut: typed.loggedOut === true || isWangyihaoLoggedOutText(`${title}\n${textSnippet}`),
|
||||
title,
|
||||
textSnippet,
|
||||
};
|
||||
}
|
||||
|
||||
async function verifyWangyihaoConsoleAccess(
|
||||
window: BrowserWindow,
|
||||
session: Session,
|
||||
account: PublishAccountIdentity,
|
||||
): Promise<boolean> {
|
||||
if (window.isDestroyed()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const currentURL = window.webContents.getURL();
|
||||
if (!currentURL || currentURL.startsWith("data:text/html")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const definition = platformBindingDefinitions.wangyihao;
|
||||
if (definition && definitionLooksLikeLoginRedirect(currentURL, definition)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const pageState = await readWangyihaoConsoleState(window.webContents);
|
||||
if (pageState?.loggedOut) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const detected = await detectWangyihaoFromSession(session).catch(() => null);
|
||||
if (detected) {
|
||||
return detectedAccountMatchesIdentity(detected, account);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function detectedAccountsLikelyMatch(left: DetectedAccount, right: DetectedAccount): boolean {
|
||||
const leftUid = normalizeAccountPlatformUid(left.platformUid) || left.platformUid;
|
||||
const rightUid = normalizeAccountPlatformUid(right.platformUid) || right.platformUid;
|
||||
if (leftUid && rightUid) {
|
||||
return sameAccountPlatformUid(leftUid, rightUid);
|
||||
}
|
||||
|
||||
const leftName = normalizeComparableText(left.displayName);
|
||||
const rightName = normalizeComparableText(right.displayName);
|
||||
return Boolean(leftName && rightName && leftName === rightName);
|
||||
}
|
||||
|
||||
async function verifyWangyihaoBindWindowReady(
|
||||
window: BrowserWindow,
|
||||
session: Session,
|
||||
expected: DetectedAccount,
|
||||
): Promise<boolean> {
|
||||
if (window.isDestroyed()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const currentURL = window.webContents.getURL();
|
||||
const definition = platformBindingDefinitions.wangyihao;
|
||||
if (!currentURL || currentURL.startsWith("data:text/html")) {
|
||||
return false;
|
||||
}
|
||||
if (definition && definitionLooksLikeLoginRedirect(currentURL, definition)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const pageState = await readWangyihaoConsoleState(window.webContents);
|
||||
if (pageState?.loggedOut) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const detected = await detectWangyihaoFromSession(session).catch(() => null);
|
||||
return detected ? detectedAccountsLikelyMatch(detected, expected) : false;
|
||||
}
|
||||
|
||||
function detectedAccountMatchesIdentity(
|
||||
detected: DetectedAccount,
|
||||
account: PublishAccountIdentity,
|
||||
@@ -2360,7 +2820,7 @@ function platformRequiresDetectedAccountForConsoleAccess(platformId: string): bo
|
||||
}
|
||||
|
||||
function platformRequiresDetectedAccountIdentityMatch(platformId: string): boolean {
|
||||
return platformId === "qiehao" || platformId === "zol";
|
||||
return platformId === "qiehao" || platformId === "wangyihao" || platformId === "zol";
|
||||
}
|
||||
|
||||
async function verifyZolConsoleAccess(
|
||||
@@ -2381,7 +2841,7 @@ async function verifyZolConsoleAccess(
|
||||
|
||||
const LOGIN_REDIRECT_TOKEN_RE =
|
||||
/(^|[/?#=&._-])(login|signin|sign-in|sign_in|signup|sign-up|register|auth|oauth|passport|scan|qrcode|qrlogin|wxlogin|sso)(?=$|[/?#=&._-])/i;
|
||||
const LOGIN_REDIRECT_HOST_RE = /(^|[.-])(login|passport|auth|sso)(?=$|[.-])/i;
|
||||
const LOGIN_REDIRECT_HOST_RE = /(^|[.-])(login|passport|auth|sso|reg)(?=$|[.-])/i;
|
||||
|
||||
function looksLikeLoginRedirect(currentURL: string, loginURL: string): boolean {
|
||||
if (!currentURL || !loginURL) {
|
||||
@@ -2468,6 +2928,10 @@ async function verifyPublishAccountConsoleAccess(
|
||||
return detected ? detectedAccountMatchesIdentity(detected, account) : false;
|
||||
}
|
||||
|
||||
if (account.platform === "wangyihao") {
|
||||
return await verifyWangyihaoConsoleAccess(window, handle.session, account);
|
||||
}
|
||||
|
||||
if (account.platform === "zol") {
|
||||
return await verifyZolConsoleAccess(window, handle.session, account);
|
||||
}
|
||||
@@ -2582,22 +3046,7 @@ async function detectZhihu({ session }: DetectContext): Promise<DetectedAccount
|
||||
}
|
||||
|
||||
async function detectWangyihao({ session }: DetectContext): Promise<DetectedAccount | null> {
|
||||
const response = await sessionFetchJson<WangyiInfoResponse>(
|
||||
session,
|
||||
"https://mp.163.com/wemedia/info.do",
|
||||
).catch(() => null);
|
||||
|
||||
const media = response?.data?.mediaInfo;
|
||||
const uid = stringId(media?.userId);
|
||||
if (!uid || !media?.tname) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
platformUid: uid,
|
||||
displayName: media.tname,
|
||||
avatarUrl: normalizeRemoteUrl(media.icon),
|
||||
};
|
||||
return await detectWangyihaoFromSession(session);
|
||||
}
|
||||
|
||||
async function detectJianshu({ session }: DetectContext): Promise<DetectedAccount | null> {
|
||||
@@ -3094,7 +3543,8 @@ const publishBindingDefinitions: Record<string, PublishPlatformBindingDefinition
|
||||
id: "wangyihao",
|
||||
label: "网易号",
|
||||
loginUrl: "https://mp.163.com/login.html",
|
||||
consoleUrl: "https://mp.163.com/index.html#/article/create",
|
||||
consoleUrl: WANGYI_CONSOLE_URL,
|
||||
loginRedirectUrls: WANGYI_LOGIN_REDIRECT_URLS,
|
||||
detect: detectWangyihao,
|
||||
},
|
||||
jianshu: {
|
||||
@@ -3414,6 +3864,29 @@ export async function bindPublishAccount(platformId: string): Promise<DesktopAcc
|
||||
}
|
||||
}
|
||||
|
||||
if (definition.id === "wangyihao") {
|
||||
const consoleReady = await verifyWangyihaoBindWindowReady(
|
||||
window,
|
||||
handle.session,
|
||||
normalizedDetected,
|
||||
);
|
||||
console.info("[desktop-bind] wangyihao console verification", {
|
||||
platform: definition.id,
|
||||
ready: consoleReady,
|
||||
url: window.isDestroyed() ? "" : window.webContents.getURL(),
|
||||
});
|
||||
if (!consoleReady) {
|
||||
return;
|
||||
}
|
||||
|
||||
const detectedFromWorkbench = await definition
|
||||
.detect({ session: handle.session, webContents: window.webContents })
|
||||
.catch(() => null);
|
||||
if (detectedFromWorkbench) {
|
||||
normalizedDetected = sanitizeDetectedAccount(detectedFromWorkbench);
|
||||
}
|
||||
}
|
||||
|
||||
normalizedDetected = await finalizeDetectedAccountForBind(
|
||||
definition,
|
||||
{ session: handle.session, webContents: window.webContents },
|
||||
@@ -3434,6 +3907,10 @@ export async function bindPublishAccount(platformId: string): Promise<DesktopAcc
|
||||
tags: [],
|
||||
});
|
||||
|
||||
if (definition.id === "wangyihao") {
|
||||
await saveWangyihaoSessionCookies(account.id, handle.session);
|
||||
}
|
||||
|
||||
settleSuccess(account);
|
||||
} catch (error) {
|
||||
console.error("[desktop-bind] upsert failed", {
|
||||
|
||||
@@ -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",
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user