Files
geo/apps/desktop-client/src/main/runtime-snapshot.ts
T
root 162abdc97c
Backend CI / Backend (push) Has been cancelled
Frontend CI / Frontend (push) Failing after 1m39s
chore(frontend): introduce prettier + eslint and prune unused code
- Add Prettier 3 with prettier-plugin-organize-imports (sorts/removes unused imports)
- Add ESLint 10 flat config with typescript-eslint + eslint-plugin-vue + eslint-config-prettier
- Add root scripts: format, format:check, lint, lint:fix
- Reformat 257 files across admin-web, ops-web, desktop-client, packages
- Remove unused locals/exports flagged by --noUnusedLocals/--noUnusedParameters
- Fix duplicate localTabLabel key, surrogate-pair regex u-flag, NBSP literal in regex
- Skip server/ (Go) and apps/browser-extension/ (deprecated per ADR)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 20:39:09 +08:00

229 lines
8.3 KiB
TypeScript

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<typeof getRuntimeControllerSnapshot>
type RuntimeControllerAccount = RuntimeControllerSnapshot['accounts'][number]
function createRuntimeAccountView(
controller: RuntimeControllerSnapshot,
account: RuntimeControllerAccount,
context: {
hotViews: ReturnType<typeof listHotViews>
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<Record<string, number>>((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(),
},
}
}