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 { createHash } from "node:crypto";
|
||||||
import { readdirSync, statSync } from "node:fs";
|
import { mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
||||||
import { join } from "node:path";
|
import { dirname, join } from "node:path";
|
||||||
|
|
||||||
import { BrowserWindow, app, nativeTheme, session as electronSession } from "electron/main";
|
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 { aiPlatformCatalog } from "@geo/shared-types";
|
||||||
import type { DesktopAccountInfo } from "@geo/shared-types";
|
import type { DesktopAccountInfo } from "@geo/shared-types";
|
||||||
|
|
||||||
@@ -160,14 +160,31 @@ type ZhihuMeResponse = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type WangyiInfoResponse = {
|
type WangyiInfoResponse = {
|
||||||
|
code?: number | string;
|
||||||
|
message?: string;
|
||||||
|
msg?: string;
|
||||||
data?: {
|
data?: {
|
||||||
mediaInfo?: {
|
mediaInfo?: {
|
||||||
userId?: string | number;
|
userId?: string | number;
|
||||||
|
wemediaId?: string | number;
|
||||||
tname?: string;
|
tname?: string;
|
||||||
icon?: 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 = {
|
type JianshuUser = {
|
||||||
id?: string | number;
|
id?: string | number;
|
||||||
@@ -259,6 +276,23 @@ const ZOL_LOGIN_REDIRECT_URLS = [
|
|||||||
"https://passport.zol.com.cn/",
|
"https://passport.zol.com.cn/",
|
||||||
"https://service.zol.com.cn/user/login",
|
"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 = [
|
const BILIBILI_WBI_MIXIN_KEY_ENC_TAB = [
|
||||||
46, 47, 18, 2, 53, 8, 23, 32,
|
46, 47, 18, 2, 53, 8, 23, 32,
|
||||||
15, 50, 10, 31, 58, 3, 45, 35,
|
15, 50, 10, 31, 58, 3, 45, 35,
|
||||||
@@ -1586,6 +1620,281 @@ async function hasToutiaoAuthCookies(session: Session): Promise<boolean> {
|
|||||||
return false;
|
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> {
|
async function recoverToutiaoSessionHandle(account: PublishAccountIdentity): Promise<SessionHandle | null> {
|
||||||
const pendingPartitions = listPendingPartitions("toutiaohao");
|
const pendingPartitions = listPendingPartitions("toutiaohao");
|
||||||
if (!pendingPartitions.length) {
|
if (!pendingPartitions.length) {
|
||||||
@@ -1651,12 +1960,19 @@ async function findLocalPublishAccountSessionHandle(
|
|||||||
): Promise<SessionHandle | null> {
|
): Promise<SessionHandle | null> {
|
||||||
const existing = getSessionHandle(account.id);
|
const existing = getSessionHandle(account.id);
|
||||||
if (existing) {
|
if (existing) {
|
||||||
|
if (account.platform === "wangyihao") {
|
||||||
|
await restoreWangyihaoSessionCookies(account.id, existing.session);
|
||||||
|
}
|
||||||
return existing;
|
return existing;
|
||||||
}
|
}
|
||||||
|
|
||||||
const persisted = getPersistedPartition(account.id);
|
const persisted = getPersistedPartition(account.id);
|
||||||
if (persisted) {
|
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") {
|
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;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1783,6 +2106,10 @@ export async function probePublishAccountSession(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (account.platform === "wangyihao") {
|
||||||
|
return await probeWangyihaoSession(account, handle.session);
|
||||||
|
}
|
||||||
|
|
||||||
const window = createBoundWindow(
|
const window = createBoundWindow(
|
||||||
`${definition.label} 静默探针`,
|
`${definition.label} 静默探针`,
|
||||||
await resolvePublishAccountConsoleURL(definition, handle.session),
|
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 {
|
return {
|
||||||
verdict: "expired",
|
verdict: "expired",
|
||||||
reason: "login_redirect",
|
reason: "login_redirect",
|
||||||
@@ -2136,6 +2465,15 @@ export async function silentRefreshPublishAccountSession(
|
|||||||
return false;
|
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(
|
const window = createBoundWindow(
|
||||||
`${definition.label} 静默续期`,
|
`${definition.label} 静默续期`,
|
||||||
await resolvePublishAccountConsoleURL(definition, handle.session),
|
await resolvePublishAccountConsoleURL(definition, handle.session),
|
||||||
@@ -2199,6 +2537,13 @@ export async function inspectPublishAccountLocalSession(
|
|||||||
|
|
||||||
const profile = await resolvePublishAccountProfileFromHandle(account, handle);
|
const profile = await resolvePublishAccountProfileFromHandle(account, handle);
|
||||||
if (!profile) {
|
if (!profile) {
|
||||||
|
if (account.platform === "wangyihao") {
|
||||||
|
return {
|
||||||
|
availability: "expired",
|
||||||
|
profile: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const consoleReady = await verifyPublishAccountConsoleAccess(account, handle).catch(() => false);
|
const consoleReady = await verifyPublishAccountConsoleAccess(account, handle).catch(() => false);
|
||||||
if (consoleReady) {
|
if (consoleReady) {
|
||||||
return {
|
return {
|
||||||
@@ -2344,6 +2689,121 @@ async function verifyToutiaoConsoleAccess(window: BrowserWindow, session: Sessio
|
|||||||
return extractToutiaoAccount(sessionResponse) !== null;
|
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(
|
function detectedAccountMatchesIdentity(
|
||||||
detected: DetectedAccount,
|
detected: DetectedAccount,
|
||||||
account: PublishAccountIdentity,
|
account: PublishAccountIdentity,
|
||||||
@@ -2360,7 +2820,7 @@ function platformRequiresDetectedAccountForConsoleAccess(platformId: string): bo
|
|||||||
}
|
}
|
||||||
|
|
||||||
function platformRequiresDetectedAccountIdentityMatch(platformId: string): boolean {
|
function platformRequiresDetectedAccountIdentityMatch(platformId: string): boolean {
|
||||||
return platformId === "qiehao" || platformId === "zol";
|
return platformId === "qiehao" || platformId === "wangyihao" || platformId === "zol";
|
||||||
}
|
}
|
||||||
|
|
||||||
async function verifyZolConsoleAccess(
|
async function verifyZolConsoleAccess(
|
||||||
@@ -2381,7 +2841,7 @@ async function verifyZolConsoleAccess(
|
|||||||
|
|
||||||
const LOGIN_REDIRECT_TOKEN_RE =
|
const LOGIN_REDIRECT_TOKEN_RE =
|
||||||
/(^|[/?#=&._-])(login|signin|sign-in|sign_in|signup|sign-up|register|auth|oauth|passport|scan|qrcode|qrlogin|wxlogin|sso)(?=$|[/?#=&._-])/i;
|
/(^|[/?#=&._-])(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 {
|
function looksLikeLoginRedirect(currentURL: string, loginURL: string): boolean {
|
||||||
if (!currentURL || !loginURL) {
|
if (!currentURL || !loginURL) {
|
||||||
@@ -2468,6 +2928,10 @@ async function verifyPublishAccountConsoleAccess(
|
|||||||
return detected ? detectedAccountMatchesIdentity(detected, account) : false;
|
return detected ? detectedAccountMatchesIdentity(detected, account) : false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (account.platform === "wangyihao") {
|
||||||
|
return await verifyWangyihaoConsoleAccess(window, handle.session, account);
|
||||||
|
}
|
||||||
|
|
||||||
if (account.platform === "zol") {
|
if (account.platform === "zol") {
|
||||||
return await verifyZolConsoleAccess(window, handle.session, account);
|
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> {
|
async function detectWangyihao({ session }: DetectContext): Promise<DetectedAccount | null> {
|
||||||
const response = await sessionFetchJson<WangyiInfoResponse>(
|
return await detectWangyihaoFromSession(session);
|
||||||
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),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function detectJianshu({ session }: DetectContext): Promise<DetectedAccount | null> {
|
async function detectJianshu({ session }: DetectContext): Promise<DetectedAccount | null> {
|
||||||
@@ -3094,7 +3543,8 @@ const publishBindingDefinitions: Record<string, PublishPlatformBindingDefinition
|
|||||||
id: "wangyihao",
|
id: "wangyihao",
|
||||||
label: "网易号",
|
label: "网易号",
|
||||||
loginUrl: "https://mp.163.com/login.html",
|
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,
|
detect: detectWangyihao,
|
||||||
},
|
},
|
||||||
jianshu: {
|
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(
|
normalizedDetected = await finalizeDetectedAccountForBind(
|
||||||
definition,
|
definition,
|
||||||
{ session: handle.session, webContents: window.webContents },
|
{ session: handle.session, webContents: window.webContents },
|
||||||
@@ -3434,6 +3907,10 @@ export async function bindPublishAccount(platformId: string): Promise<DesktopAcc
|
|||||||
tags: [],
|
tags: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (definition.id === "wangyihao") {
|
||||||
|
await saveWangyihaoSessionCookies(account.id, handle.session);
|
||||||
|
}
|
||||||
|
|
||||||
settleSuccess(account);
|
settleSuccess(account);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[desktop-bind] upsert failed", {
|
console.error("[desktop-bind] upsert failed", {
|
||||||
|
|||||||
@@ -69,9 +69,11 @@ type WangyiPublishDetail = {
|
|||||||
newProminentFlag: string;
|
newProminentFlag: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const WANGYI_ORIGIN = "https://mp.163.com";
|
const WANGYI_API_ORIGIN = "https://mp.163.com";
|
||||||
const WANGYI_HOME_URL = `${WANGYI_ORIGIN}/subscribe_v4/index.html`;
|
const WANGYI_CONSOLE_ORIGIN = "http://mp.163.com";
|
||||||
const WANGYI_MANAGE_URL = `${WANGYI_ORIGIN}/subscribe_v4/index.html#/article-manage`;
|
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_CREATIVE_STATEMENT = "个人原创,仅供参考";
|
||||||
const WANGYI_PUBLISH_DELAY_MS = 2_000;
|
const WANGYI_PUBLISH_DELAY_MS = 2_000;
|
||||||
const WANGYI_TOKEN_WAIT_MS = 12_000;
|
const WANGYI_TOKEN_WAIT_MS = 12_000;
|
||||||
@@ -155,8 +157,8 @@ async function wangyiHeaders(
|
|||||||
(await sessionCookieHeader(context.session, "163.com").catch(() => ""));
|
(await sessionCookieHeader(context.session, "163.com").catch(() => ""));
|
||||||
return {
|
return {
|
||||||
accept: "application/json, text/plain, */*",
|
accept: "application/json, text/plain, */*",
|
||||||
origin: WANGYI_ORIGIN,
|
origin: WANGYI_API_ORIGIN,
|
||||||
referer: WANGYI_HOME_URL,
|
referer: WANGYI_API_REFERER,
|
||||||
...(cookie ? { cookie } : {}),
|
...(cookie ? { cookie } : {}),
|
||||||
...extra,
|
...extra,
|
||||||
};
|
};
|
||||||
@@ -165,7 +167,7 @@ async function wangyiHeaders(
|
|||||||
async function fetchAccount(context: PublishAdapterContext): Promise<WangyiAccount | null> {
|
async function fetchAccount(context: PublishAdapterContext): Promise<WangyiAccount | null> {
|
||||||
const response = await sessionFetchJson<WangyiInfoResponse>(
|
const response = await sessionFetchJson<WangyiInfoResponse>(
|
||||||
context.session,
|
context.session,
|
||||||
`${WANGYI_ORIGIN}/wemedia/info.do`,
|
`${WANGYI_API_ORIGIN}/wemedia/info.do`,
|
||||||
{
|
{
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
headers: await wangyiHeaders(context),
|
headers: await wangyiHeaders(context),
|
||||||
@@ -191,7 +193,7 @@ async function fetchPublishDetail(
|
|||||||
newProminentFlag: "",
|
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("_", String(Date.now()));
|
||||||
url.searchParams.set("wemediaId", account.wemediaId);
|
url.searchParams.set("wemediaId", account.wemediaId);
|
||||||
url.searchParams.set("mediaId", 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");
|
throw new Error("wangyihao_playwright_missing");
|
||||||
}
|
}
|
||||||
|
|
||||||
await page.goto(WANGYI_HOME_URL, {
|
for (let attempt = 0; attempt < 2; attempt += 1) {
|
||||||
waitUntil: "domcontentloaded",
|
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 上;这里保持和旧版插件一致的读取方式。
|
// 网易号的 neg 有时是顶层绑定,不一定挂在 window 上;这里保持和旧版插件一致的读取方式。
|
||||||
const token = await readPublishTokenFromPage(page);
|
const token = (await readPublishTokenFromPage(page)).trim();
|
||||||
return token.trim();
|
if (token) {
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
async function readPublishTokenFromPage(page: Page): Promise<string> {
|
async function readPublishTokenFromPage(page: Page): Promise<string> {
|
||||||
@@ -415,7 +428,7 @@ async function uploadImage(
|
|||||||
form.append("from", "neteasecode_mp");
|
form.append("from", "neteasecode_mp");
|
||||||
form.append("logotext", account.name);
|
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("_", String(Date.now()));
|
||||||
url.searchParams.set("wemediaId", account.wemediaId);
|
url.searchParams.set("wemediaId", account.wemediaId);
|
||||||
url.searchParams.set("realUserId", account.realUserId);
|
url.searchParams.set("realUserId", account.realUserId);
|
||||||
@@ -444,7 +457,7 @@ async function saveDraft(
|
|||||||
html: string,
|
html: string,
|
||||||
token: string,
|
token: string,
|
||||||
): Promise<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()));
|
url.searchParams.set("_", String(Date.now()));
|
||||||
|
|
||||||
const body = buildDraftBody(context, account, detail, html, token);
|
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");
|
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, {
|
await page.goto(draftUrl, {
|
||||||
waitUntil: "domcontentloaded",
|
waitUntil: "domcontentloaded",
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user