diff --git a/.gitignore b/.gitignore index 6f6c3aa..9732ee2 100644 --- a/.gitignore +++ b/.gitignore @@ -22,4 +22,5 @@ apps/*/firefox-mv*/ *.log *.tmp apps/*/stats.html -ops-api \ No newline at end of file +ops-api +tmp/* \ No newline at end of file diff --git a/apps/desktop-client/src/main/account-binder.ts b/apps/desktop-client/src/main/account-binder.ts index 94d8e32..7fa0df3 100644 --- a/apps/desktop-client/src/main/account-binder.ts +++ b/apps/desktop-client/src/main/account-binder.ts @@ -1,5 +1,12 @@ import { createHash } from 'node:crypto' -import { mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs' +import { + appendFileSync, + mkdirSync, + readFileSync, + readdirSync, + statSync, + writeFileSync, +} from 'node:fs' import { dirname, join } from 'node:path' import type { DesktopAccountInfo } from '@geo/shared-types' @@ -21,6 +28,7 @@ import { buildDoubaoSessionCredentialEvidence, DOUBAO_AUTH_PROBE_URL, doubaoHasBoundAccountPageSignal, + isDoubaoLoggedInCredentialCookieName, probeDoubaoAccountSession, readDoubaoAuthPageState, } from './adapters/doubao/auth-rules' @@ -291,6 +299,7 @@ type DongchediAccountInfoResponse = { const activeBindOperations = new Map() const MAX_CONCURRENT_BIND_WINDOWS = 2 +const ACCOUNT_BIND_DIAGNOSTIC_FILE = 'desktop-account-bind-events.jsonl' const TOUTIAO_LOGIN_URL = 'https://mp.toutiao.com/auth/page/login' const TOUTIAO_CONSOLE_HOME_URL = 'https://mp.toutiao.com/profile_v4/index' @@ -398,6 +407,75 @@ function hashText(value: string): string { return createHash('sha1').update(value).digest('hex').slice(0, 20) } +function diagnosticHash(value: string | null | undefined): string | null { + const normalized = value?.trim() + if (!normalized) { + return null + } + return createHash('sha256').update(normalized).digest('hex').slice(0, 16) +} + +function diagnosticUrl(value: string | null | undefined): string | null { + const normalized = value?.trim() + if (!normalized) { + return null + } + + try { + const parsed = new URL(normalized) + return `${parsed.origin}${parsed.pathname}` + } catch { + return normalized.slice(0, 160) + } +} + +function appendAccountBindDiagnostic(event: Record): void { + const target = join(app.getPath('userData'), ACCOUNT_BIND_DIAGNOSTIC_FILE) + try { + mkdirSync(dirname(target), { recursive: true }) + appendFileSync(target, `${JSON.stringify({ at: new Date().toISOString(), ...event })}\n`, 'utf8') + } catch { + // Best-effort diagnostics. Never block binding on local log writes. + } +} + +async function summarizeDoubaoAuthCookies( + session: Session, + urls: Iterable, +): Promise> { + const summaries: Array<{ + url: string | null + cookieCount: number + authCookieCount: number + authCookies: string[] + }> = [] + + for (const url of urls) { + try { + const cookies = await session.cookies.get({ url }) + const authCookies = cookies + .filter((cookie) => isDoubaoLoggedInCredentialCookieName(cookie.name)) + .map((cookie) => `${cookie.name}:${cookie.value.length}`) + .sort() + summaries.push({ + url: diagnosticUrl(url), + cookieCount: cookies.length, + authCookieCount: authCookies.length, + authCookies, + }) + } catch (error) { + summaries.push({ + url: diagnosticUrl(url), + cookieCount: 0, + authCookieCount: 0, + authCookies: [`error:${error instanceof Error ? error.message : String(error)}`], + }) + } + } + + return summaries +} + function buildBilibiliMixinKey(imgKey: string, subKey: string): string { const raw = imgKey + subKey return BILIBILI_WBI_MIXIN_KEY_ENC_TAB.map((index) => raw.charAt(index)) @@ -1463,45 +1541,107 @@ async function detectDoubaoPlatform( context: DetectContext, ): Promise { if (!context.webContents || context.webContents.isDestroyed()) { + appendAccountBindDiagnostic({ + action: 'doubao_detect_failed', + platform: platformId, + reason: 'web_contents_destroyed', + }) return null } const currentURL = context.webContents.getURL() const platformMeta = aiPlatformCatalog.find((item) => item.id === platformId) ?? null if (!platformMeta) { - return null - } - - if (currentURL && looksLikeLoginRedirect(currentURL, platformMeta.loginUrl)) { + appendAccountBindDiagnostic({ + action: 'doubao_detect_failed', + platform: platformId, + reason: 'platform_meta_missing', + url: diagnosticUrl(currentURL), + }) return null } const pageState = await readDoubaoAuthPageState(context.webContents).catch(() => null) + const urls = new Set([platformMeta.loginUrl, platformMeta.consoleUrl]) + const cookieSummary = await summarizeDoubaoAuthCookies(context.session, urls) + const pageStateDiagnostic = { + displayName: pageState?.displayName ?? null, + hasAvatar: Boolean(pageState?.avatarUrl), + currentPath: pageState?.currentPath ?? null, + strongAuthSignalCount: pageState?.strongAuthSignalCount ?? 0, + loginSignalCount: pageState?.loginSignalCount ?? 0, + loggedOutSignalCount: pageState?.loggedOutSignalCount ?? 0, + challengeSignalCount: pageState?.challengeSignalCount ?? 0, + } + if ((pageState?.challengeSignalCount ?? 0) > 0) { + appendAccountBindDiagnostic({ + action: 'doubao_detect_failed', + platform: platformId, + reason: 'challenge_required', + url: diagnosticUrl(currentURL), + pageState: pageStateDiagnostic, + cookies: cookieSummary, + }) return null } if ((pageState?.loggedOutSignalCount ?? 0) > 0) { + appendAccountBindDiagnostic({ + action: 'doubao_detect_failed', + platform: platformId, + reason: 'logged_out_signal', + url: diagnosticUrl(currentURL), + pageState: pageStateDiagnostic, + cookies: cookieSummary, + }) return null } // Doubao is intentionally platform-specific. It allows anonymous chat, but // our desktop binding requires the real sidebar account menu plus login cookies. if (!doubaoHasBoundAccountPageSignal(pageState)) { + appendAccountBindDiagnostic({ + action: 'doubao_detect_failed', + platform: platformId, + reason: 'doubao_account_menu_missing', + url: diagnosticUrl(currentURL), + pageState: pageStateDiagnostic, + cookies: cookieSummary, + }) return null } - const urls = new Set([platformMeta.loginUrl, platformMeta.consoleUrl]) const credentialEvidence = await buildDoubaoSessionCredentialEvidence( context.session, urls, pageState, ) if (!credentialEvidence) { + appendAccountBindDiagnostic({ + action: 'doubao_detect_failed', + platform: platformId, + reason: 'doubao_login_cookie_missing', + url: diagnosticUrl(currentURL), + pageState: pageStateDiagnostic, + cookies: cookieSummary, + }) return null } + const platformUid = boundedIdentity('doubao-session', credentialEvidence) + appendAccountBindDiagnostic({ + action: 'doubao_detect_success', + platform: platformId, + reason: 'detected', + url: diagnosticUrl(currentURL), + pageState: pageStateDiagnostic, + cookies: cookieSummary, + displayName: pageState?.displayName ?? `${label} 账号`, + platformUidHash: diagnosticHash(platformUid), + }) + return sanitizeDetectedAccount({ - platformUid: boundedIdentity('doubao-session', credentialEvidence), + platformUid, displayName: pageState?.displayName ?? `${label} 账号`, avatarUrl: pageState?.avatarUrl ?? null, }) @@ -3995,6 +4135,13 @@ export async function bindPublishAccount(platformId: string): Promise { + if (error instanceof ApiClientError) { + return { + name: error.name, + message: error.message, + code: error.code, + status: error.status ?? null, + detail: error.detail ?? null, + requestId: error.requestId ?? null, + handled: error.handled === true, + } + } + + if (error instanceof Error) { + return { + name: error.name, + message: error.message, + } + } + + return { + name: typeof error, + message: String(error), + } +} + +function appendAccountUpsertDiagnostic( + payload: UpsertDesktopAccountRequest, + error: unknown, +): void { + const target = join(app.getPath('userData'), ACCOUNT_UPSERT_DIAGNOSTIC_FILE) + const event = { + at: new Date().toISOString(), + action: 'desktop_account_upsert_failed', + transport: { + baseURL: transportState.baseURL, + auth: transportState.auth, + requestDebugMode: transportState.requestDebugMode, + hasClient: desktopApiClient !== null, + hasClientToken: Boolean(transportSession.clientToken), + }, + request: { + platform: payload.platform, + platformUidHash: diagnosticHash(payload.platform_uid), + accountFingerprintHash: diagnosticHash(payload.account_fingerprint ?? null), + displayName: payload.display_name, + health: payload.health, + verifiedAt: payload.verified_at ?? null, + tagCount: payload.tags?.length ?? 0, + hasIfSyncVersion: typeof payload.if_sync_version === 'number', + }, + error: normalizeErrorForDiagnostics(error), + } + + try { + mkdirSync(dirname(target), { recursive: true }) + appendFileSync(target, `${JSON.stringify(event)}\n`, 'utf8') + } catch { + // Best-effort diagnostics. Never block account binding on local log writes. + } +} + function currentRequestDebugMode(): 'main-process' | 'renderer-devtools' { return canUseRendererDevtoolsProxy() ? 'renderer-devtools' : 'main-process' } @@ -521,6 +597,7 @@ export async function upsertDesktopAccount( if (accountUpsertCache.get(cacheKey)?.inFlight === request) { accountUpsertCache.delete(cacheKey) } + appendAccountUpsertDiagnostic(payload, error) throw error } }