diff --git a/apps/desktop-client/src/main/account-health.ts b/apps/desktop-client/src/main/account-health.ts index 02decd8..470850e 100644 --- a/apps/desktop-client/src/main/account-health.ts +++ b/apps/desktop-client/src/main/account-health.ts @@ -19,6 +19,8 @@ import { getPersistedPartition, getSessionHandle } from './session-registry' const PROBE_TICK_MS = 10_000 const STALE_AFTER_MS = 45 * 60_000 +const INITIAL_PROBE_MIN_MS = 5_000 +const INITIAL_PROBE_MAX_MS = 3 * 60_000 const REGULAR_PROBE_MIN_MS = 30 * 60_000 const REGULAR_PROBE_MAX_MS = STALE_AFTER_MS const CONFIRM_BACKOFF_STEPS_MS = [30_000, 120_000] as const @@ -103,7 +105,7 @@ function resolveKnownPartition(accountId: string): string | null { } function nextInitialProbeAt(now = Date.now()): number { - return now + return now + randomInt(INITIAL_PROBE_MIN_MS, INITIAL_PROBE_MAX_MS) } function nextRegularProbeAt(now = Date.now()): number { @@ -311,7 +313,7 @@ function applyExpiredResult( record.probeState = 'idle' record.authReason = result.reason ?? 'login_redirect' record.lastProbeAt = now - record.nextProbeAt = nextRegularProbeAt(now) + record.nextProbeAt = null record.lastAuthFailureAt = now record.suspectedExpiredAt = null record.confirmFailureCount = 0 @@ -795,58 +797,6 @@ export function markTrackedAccountMissingPartition( return snapshot } -export function reconcileTrackedAccountRemoteState( - account: PublishAccountIdentity, - input: { - health?: DesktopAccountInfo['health'] | null - verifiedAt?: string | null - profile?: AccountHealthProfile | null - }, -): AccountHealthSnapshot | null { - if (input.health !== 'live') { - return getAccountHealthSnapshot(account.id) - } - - const remoteVerifiedAt = parseVerifiedAt(input.verifiedAt) - if (!remoteVerifiedAt) { - return getAccountHealthSnapshot(account.id) - } - - const record = ensureRecord(account) - const newestLocalStateAt = Math.max( - record.lastVerifiedAt ?? 0, - record.lastAuthFailureAt ?? 0, - record.lastProbeAt ?? 0, - ) - if (remoteVerifiedAt <= newestLocalStateAt) { - return toPublicSnapshot(record) - } - - const previousSignature = snapshotSignature(record) - record.authRevision += 1 - record.platform = account.platform - record.authState = 'active' - record.probeState = 'idle' - record.authReason = 'bind_success' - record.lastVerifiedAt = remoteVerifiedAt - record.lastProbeAt = remoteVerifiedAt - record.nextProbeAt = nextRegularProbeAt() - record.lastAuthFailureAt = null - record.profile = input.profile ?? record.profile - record.sessionFingerprint = input.profile?.subjectUid ?? record.sessionFingerprint - record.suspectedExpiredAt = null - record.confirmFailureCount = 0 - - const snapshot = commitRecord(record, previousSignature) - const queuedJob = probeJobs.get(account.id) - if (queuedJob?.status === 'queued') { - removeQueuedProbeJob(account.id) - queuedJob.resolve(snapshot) - } - - return snapshot -} - export function getAccountHealthSnapshot(accountId: string): AccountHealthSnapshot | null { ensureInitialized() const record = records.get(accountId) diff --git a/apps/desktop-client/src/main/runtime-controller.ts b/apps/desktop-client/src/main/runtime-controller.ts index 2f6890c..cecebf5 100644 --- a/apps/desktop-client/src/main/runtime-controller.ts +++ b/apps/desktop-client/src/main/runtime-controller.ts @@ -32,7 +32,6 @@ import { markTrackedAccountMissingPartition, markTrackedAccountBound, probeTrackedAccount, - reconcileTrackedAccountRemoteState, reportAccountFailure, syncTrackedAccounts, } from './account-health' @@ -326,21 +325,67 @@ function isAccountOwnedByCurrentClient(account: DesktopAccountInfo): boolean { } function hasLocalAccountAuthorization(account: DesktopAccountInfo): boolean { - return isAccountOwnedByCurrentClient(account) + return isAccountOwnedByCurrentClient(account) && hasLocalAccountSession(account.id) } function accountRequiresLocalAuthorization(account: DesktopAccountInfo): boolean { return Boolean(account.client_id) } -function accountRequiresStrictProbe(account: DesktopAccountInfo): boolean { - return ['bilibili', 'juejin', 'smzdm'].includes(account.platform) -} - function shouldReportAccountPresence(account: DesktopAccountInfo): boolean { return hasLocalAccountSession(account.id) && isAccountOwnedByCurrentClient(account) } +function buildRuntimeAccountDedupeKey(account: DesktopAccountInfo): string { + if (isAIPlatformId(account.platform)) { + return `ai-platform:${account.platform}` + } + + return buildAccountIdentityKey({ + platform: account.platform, + platformUid: account.platform_uid, + displayName: account.display_name, + }) +} + +function accountHealthRank(health: DesktopAccountInfo['health']): number { + switch (health) { + case 'live': + return 4 + case 'risk': + return 3 + case 'captcha': + return 2 + case 'expired': + return 1 + default: + return 0 + } +} + +function accountTimestamp(value: string | null | undefined): number { + return parseTimestamp(value) ?? 0 +} + +function shouldPreferRuntimeAccount( + candidate: DesktopAccountInfo, + current: DesktopAccountInfo, +): boolean { + const candidateHealthRank = accountHealthRank(candidate.health) + const currentHealthRank = accountHealthRank(current.health) + if (candidateHealthRank !== currentHealthRank) { + return candidateHealthRank > currentHealthRank + } + + const candidateVerifiedAt = accountTimestamp(candidate.verified_at) + const currentVerifiedAt = accountTimestamp(current.verified_at) + if (candidateVerifiedAt !== currentVerifiedAt) { + return candidateVerifiedAt > currentVerifiedAt + } + + return candidate.sync_version > current.sync_version +} + function runtimePlatformLabel(platform: string): string { return getAIPlatformCatalogItem(platform)?.label ?? platform } @@ -988,17 +1033,7 @@ async function syncAccounts(source: 'startup' | 'manual'): Promise { ...account, platform_uid: normalizedPlatformUid, }) - if (hasLocalAccountAuthorization(account) && !accountRequiresStrictProbe(account)) { - reconcileTrackedAccountRemoteState(accountIdentity, { - health: account.health, - verifiedAt: account.verified_at, - profile: { - subjectUid: normalizedPlatformUid, - displayName: account.display_name, - avatarUrl: account.avatar_url, - }, - }) - } else if (accountRequiresLocalAuthorization(account)) { + if (accountRequiresLocalAuthorization(account) && !hasLocalAccountAuthorization(account)) { markTrackedAccountMissingPartition(accountIdentity) } const projected = getProjectedAccountHealth({ @@ -1045,24 +1080,27 @@ async function syncAccounts(source: 'startup' | 'manual'): Promise { }) const duplicateAccounts: DesktopAccountInfo[] = [] const dedupedAccounts: DesktopAccountInfo[] = [] - const seenAccountKeys = new Set() + const seenAccountIndexes = new Map() for (const account of syncedAccounts) { if (!account) { continue } - const identityKey = buildAccountIdentityKey({ - platform: account.platform, - platformUid: account.platform_uid, - displayName: account.display_name, - }) - if (seenAccountKeys.has(identityKey)) { - duplicateAccounts.push(account) + const identityKey = buildRuntimeAccountDedupeKey(account) + const existingIndex = seenAccountIndexes.get(identityKey) + if (existingIndex !== undefined) { + const existingAccount = dedupedAccounts[existingIndex] + if (shouldPreferRuntimeAccount(account, existingAccount)) { + duplicateAccounts.push(existingAccount) + dedupedAccounts[existingIndex] = account + } else { + duplicateAccounts.push(account) + } continue } - seenAccountKeys.add(identityKey) + seenAccountIndexes.set(identityKey, dedupedAccounts.length) dedupedAccounts.push(account) } @@ -1097,6 +1135,8 @@ async function cleanupDuplicateDesktopAccounts(accounts: DesktopAccountInfo[]): for (const account of accounts) { try { await deleteDesktopAccount(account.id, account.sync_version) + await clearSessionHandle(account.id) + forgetTrackedAccountHealth(account.id) } catch (error) { console.warn('[desktop-runtime] duplicate desktop account cleanup failed', { accountId: account.id, @@ -1331,9 +1371,14 @@ export function noteRuntimeAccountBound(account: DesktopAccountInfo): void { const existingIndex = state.accounts.findIndex( (item) => item.id === normalizedAccount.id || + (isAIPlatformId(normalizedAccount.platform) && item.platform === normalizedAccount.platform) || (item.platform === normalizedAccount.platform && sameAccountPlatformUid(item.platform_uid, normalizedAccount.platform_uid)), ) + const replacedAccount = + existingIndex >= 0 && state.accounts[existingIndex].id !== normalizedAccount.id + ? state.accounts[existingIndex] + : null if (existingIndex >= 0) { state.accounts = state.accounts.map((item, index) => @@ -1348,6 +1393,19 @@ export function noteRuntimeAccountBound(account: DesktopAccountInfo): void { displayName: normalizedAccount.display_name, avatarUrl: normalizedAccount.avatar_url, }) + if (replacedAccount) { + state.accountProfiles.delete(replacedAccount.id) + forgetTrackedAccountHealth(replacedAccount.id) + void clearSessionHandle(replacedAccount.id) + void deleteDesktopAccount(replacedAccount.id, replacedAccount.sync_version).catch((error) => { + console.warn('[desktop-runtime] replaced desktop account cleanup failed', { + accountId: replacedAccount.id, + platform: replacedAccount.platform, + platformUid: replacedAccount.platform_uid, + message: errorMessage(error), + }) + }) + } state.lastAccountsSyncAt = Date.now() syncTrackedAccounts( state.accounts diff --git a/apps/desktop-client/src/renderer/views/AiPlatformsView.vue b/apps/desktop-client/src/renderer/views/AiPlatformsView.vue index 89521a8..67c939a 100644 --- a/apps/desktop-client/src/renderer/views/AiPlatformsView.vue +++ b/apps/desktop-client/src/renderer/views/AiPlatformsView.vue @@ -45,9 +45,11 @@ function isLocallyAuthorized(account: AccountRow | null): boolean { } const overview = computed(() => ({ - configuredPlatforms: desktopMonitoringMediaCatalog.length, authorized: platformCards.value.filter((platform) => isLocallyAuthorized(platform.account)) .length, + pending: platformCards.value.filter( + (platform) => platform.account && !isLocallyAuthorized(platform.account), + ).length, onlineClients: snapshot.value?.summary.onlineClients ?? 0, })) @@ -259,12 +261,12 @@ async function unbindPlatform(account: AccountRow) {
-
支持平台
-
{{ overview.configuredPlatforms }}
+
已授权
+
{{ overview.authorized }}
-
授权正常
-
{{ overview.authorized }}
+
待处理
+
{{ overview.pending }}
在线客户端