import { app } from 'electron/main' import { normalizeAccountPlatformUid } from '../shared/account-identity' import { getProjectedAccountHealth } from './account-health' import { getCircuitBreakerState } from './circuit-breaker' import { collectRecoverySnapshot } from './crash-recovery' import { getKeepAlivePlan } from './keep-alive' import { getLeaseManagerSnapshot } from './lease-manager' import { getMonitorSchedulerSnapshot } from './monitor-scheduler' import { getObservedRequestSnapshot } from './network-observer' import { getHiddenPlaywrightSnapshot } from './playwright-cdp' import { getProcessMetricsSnapshot } from './process-metrics' import { getPublishSchedulerSnapshot } from './publish-scheduler' import { getRateLimiterSnapshot } from './rate-limiter' import { getRuntimeControllerSnapshot } from './runtime-controller' import { getSchedulerState } from './scheduler' import { getSessionHandle, listSessionHandleSnapshots } from './session-registry' import { getTransportSnapshot } from './transport/api-client' import { describeVaultBackend } from './vault' import { getHotViewPoolPolicy, listHotViews } from './view-pool' function minutesAhead(now: number, minutes: number): number { return now + minutes * 60_000 } function parseTimestamp(value: string | null | undefined): number | null { if (!value) { return null } const timestamp = Date.parse(value) return Number.isNaN(timestamp) ? null : timestamp } type RuntimeControllerSnapshot = ReturnType type RuntimeControllerAccount = RuntimeControllerSnapshot['accounts'][number] function createRuntimeAccountView( controller: RuntimeControllerSnapshot, account: RuntimeControllerAccount, context: { hotViews: ReturnType primaryClientId: string clientOnline: boolean lastAccountsSyncAt: number now: number }, ) { const sessionHandle = getSessionHandle(account.id) const isHot = context.hotViews.some((item) => item.accountId === account.id) const accountQueueDepth = controller.tasks.filter( (task) => task.accountId === account.id && ['queued', 'in_progress'].includes(task.status), ).length const projected = getProjectedAccountHealth({ accountId: account.id, platform: account.platform, health: account.health, verifiedAt: account.verified_at, }) return { id: account.id, platform: account.platform, displayName: projected.profile?.displayName ?? account.display_name, platformUid: normalizeAccountPlatformUid(account.platform_uid) || account.platform_uid, avatarUrl: projected.profile?.avatarUrl ?? account.avatar_url ?? null, health: projected.health, authState: projected.authState, probeState: projected.probeState, authReason: projected.authReason, tags: account.tags, syncVersion: account.sync_version, clientId: account.client_id ?? context.primaryClientId, partition: projected.partition || sessionHandle?.partition || `persist:acc-${account.id}`, sessionState: isHot ? 'hot' : sessionHandle ? 'warm' : 'cold', lastSyncAt: context.lastAccountsSyncAt || parseTimestamp(account.verified_at) || context.now, lastVerifiedAt: projected.lastVerifiedAt, lastProbeAt: projected.lastProbeAt, nextProbeAt: projected.nextProbeAt, queueDepth: accountQueueDepth, online: context.clientOnline, } } export function createRuntimeSnapshot() { return createLiveRuntimeSnapshot(getRuntimeControllerSnapshot()) } export function createRuntimeAccountSnapshot(accountId: string) { const controller = getRuntimeControllerSnapshot() const now = Date.now() const primaryClientId = controller.client?.id ?? controller.session?.desktop_client?.id ?? 'desktop-self' const clientOnline = controller.running && controller.lastHeartbeatStatus !== 'failed' const account = controller.accounts.find((item) => item.id === accountId) if (!account) { return null } return createRuntimeAccountView(controller, account, { hotViews: listHotViews(), primaryClientId, clientOnline, lastAccountsSyncAt: controller.lastAccountsSyncAt, now, }) } function createLiveRuntimeSnapshot(controller: RuntimeControllerSnapshot) { const now = Date.now() // Electron Session instances are not safe to ship across IPC to the renderer. const sessions = listSessionHandleSnapshots() const hotViews = listHotViews() const leaseManager = getLeaseManagerSnapshot() const rateLimiter = getRateLimiterSnapshot() const scheduler = getSchedulerState() const transport = getTransportSnapshot() const primaryClientId = controller.client?.id ?? controller.session?.desktop_client?.id ?? 'desktop-self' const lastSeenAt = controller.lastHeartbeatAt || parseTimestamp(controller.client?.last_seen_at) || now const clientOnline = controller.running && controller.lastHeartbeatStatus !== 'failed' const queueDepth = controller.tasks.filter((task) => ['queued', 'in_progress'].includes(task.status), ).length const clients = [ { id: primaryClientId, label: 'This device', deviceName: controller.client?.device_name ?? controller.session?.desktop_client?.device_name ?? 'Current Desktop', os: controller.client?.os ?? process.platform, status: clientOnline ? 'online' : 'offline', lastSeenAt, queueDepth, heartbeat: clientOnline ? 'healthy' : 'stale', channel: controller.client?.channel ?? 'dev', }, ] as const const accounts = controller.accounts.map((account) => createRuntimeAccountView(controller, account, { hotViews, primaryClientId, clientOnline, lastAccountsSyncAt: controller.lastAccountsSyncAt, now, }), ) const tasks = controller.tasks.map((task) => ({ ...task })) const healthCounts = accounts.reduce>((acc, item) => { acc[item.health] = (acc[item.health] ?? 0) + 1 return acc }, {}) const blockingAccountCount = accounts.filter((item) => ['expired', 'revoked', 'challenge_required'].includes(item.authState), ).length return { generatedAt: now, app: { name: '省心推', version: app.getVersion(), channel: process.env.NODE_ENV === 'development' ? 'dev' : 'release', platform: process.platform, sessionCount: sessions.length, hotViewCount: hotViews.length, }, workspace: { id: `ws-${controller.client?.workspace_id ?? controller.session?.desktop_client?.workspace_id ?? 'current'}`, name: `Workspace #${controller.client?.workspace_id ?? controller.session?.desktop_client?.workspace_id ?? 'current'}`, strategy: controller.dispatchConnected ? 'websocket-first / db-fallback' : 'db-fallback / heartbeat recovery', nextSweepAt: scheduler.nextPullAt ?? minutesAhead(now, 1), lastFullSyncAt: controller.lastAccountsSyncAt || now, }, summary: { onlineClients: clients.filter((item) => item.status === 'online').length, accountsBound: accounts.length, queuedTasks: tasks.filter((item) => item.status === 'queued').length, issuesOpen: tasks.filter((item) => ['unknown', 'failed'].includes(item.status)).length + blockingAccountCount, healthCounts, }, clients, accounts, tasks, activity: controller.activity, subsystems: { transport, circuitBreaker: getCircuitBreakerState(), keepAlive: getKeepAlivePlan(), leaseManager, rateLimiter, recovery: collectRecoverySnapshot(), runtimeController: { running: controller.running, dispatchConnected: controller.dispatchConnected, queueDepth: controller.queueDepth, lastHeartbeatAt: controller.lastHeartbeatAt, lastHeartbeatStatus: controller.lastHeartbeatStatus, lastPullAt: controller.lastPullAt, lastPullStatus: controller.lastPullStatus, lastAccountsSyncAt: controller.lastAccountsSyncAt, lastServerTime: controller.lastServerTime, lastError: controller.lastError, }, scheduler, publishScheduler: getPublishSchedulerSnapshot(), monitorScheduler: getMonitorSchedulerSnapshot(), sessions, hotViews, hotViewPool: getHotViewPoolPolicy(), networkDebug: getObservedRequestSnapshot(), playwright: getHiddenPlaywrightSnapshot(), processMetrics: getProcessMetricsSnapshot(), vault: describeVaultBackend(), }, } }