import type { ApprovalEntry, ApprovalSubscriptionPreferences, ApprovalTimelineResponse, ArtifactEntry, CanonicalEvent, CronJob, CronRun, CostSummary, DashboardResponse, DebugPlan, DebugRun, DebugStoreResponse, DebugTask, DiagResult, DigitalEmployeeDailyReport, DigitalEmployeeDecision, DigitalEmployeeDiagnosticSummary, DigitalEmployeeDuty, DigitalEmployeeManagedService, DigitalEmployeePlanAgentState, DigitalEmployeeRouteSummary, DigitalEmployeeRunDetail, DigitalEmployeeTaskAgentState, DigitalEmployeeTaskExecutionSummary, DigitalEmployeeWorkEvent, DigitalEmployeeWorkdayActionRequest, DigitalEmployeeWorkdayProjection, FlowResponse, GovernanceCommandRequest, GovernanceCommandResponse, IngressRouteRequest, IngressRouteResponse, MemoryEntry, NotificationPreferences, PlanDispatchResponse, ReadyStepDispatchResult, PlanStepDispatchPolicy, PlanStepSummary, RouteDecisionRecord, RouteDecisionTimelineResponse, SkillSpec, UserNotification, } from '@/types/api'; declare global { interface Window { __QIMINGCLAW_EMBEDDED_DIGITAL__?: boolean; QimingClawBridge?: { digital?: { getSnapshot?: () => Promise; getRuntimeRecords?: () => Promise; getUiState?: () => Promise; getSkillCapabilities?: () => Promise; setSkillEnabled?: (skillId: string, enabled: boolean) => Promise<{ ok?: boolean; skill_id?: string; enabled?: boolean; error?: string }>; getPlanTemplates?: () => Promise; listMemories?: (options?: { query?: string; category?: string; limit?: number }) => Promise; addMemory?: (input: QimingclawMemoryUpsertInput) => Promise; deleteMemory?: (keyOrId: string) => Promise; saveUiState?: (update: { state: AdapterState; action?: string; date?: string; metadata?: Record }) => Promise; getSyncStatus?: () => Promise; flushSync?: () => Promise; getCronSettings?: () => Promise; saveCronSettings?: (patch: Partial) => Promise; runSchedulerCheck?: () => Promise<{ activated: number; due?: number; skipped?: number }>; runScheduleNow?: (scheduleId: string) => Promise<{ ok?: boolean; scheduleRunId?: string; error?: string }>; dispatchPlanReadySteps?: (planId: string) => Promise; getScheduleRuns?: (scheduleId: string, limit?: number) => Promise; listRouteDecisions?: (options?: { limit?: number }) => Promise; recordRouteDecision?: (input: QimingclawRouteDecisionInput) => Promise; recordGovernanceCommand?: (command: QimingclawGovernanceCommandRecord) => Promise; recordDailyReport?: (input: QimingclawDailyReportRecordInput) => Promise; accessArtifact?: (artifactId: string, action: 'preview' | 'download' | 'open_location', options?: { actor?: string | null; source?: string | null }) => Promise; recordArtifactLifecycle?: (input: QimingclawArtifactLifecycleInput) => Promise; recordApprovalAction?: (input: QimingclawApprovalActionInput) => Promise; restartManagedService?: (serviceId: string, options?: { reason?: string | null; actor?: string | null }) => Promise; respondPermission?: (sessionId: string, permissionId: string, response: 'once' | 'always' | 'reject') => Promise<{ success?: boolean; error?: string }>; }; }; } } const STATE_KEY = 'qimingclaw:digital-employee-adapter-state'; const DEFAULT_REPORT_TIME = '18:00'; interface AdapterState { selectedDutyIdsByDate: Record; confirmedDates: Record; reportScheduleTime: string; reportGeneratedAtByDate: Record; decisionResultsByDate: Record>; notificationStateById?: Record; notificationPreferences?: NotificationPreferences; approvalSubscriptionPreferences?: ApprovalSubscriptionPreferences; planStepDispatchPolicy?: PlanStepDispatchPolicy; consoleRole?: string; rolePreferences?: Record; profile: { name: string; company: string; position: string; currentStatus: string; }; } interface NotificationState { status?: UserNotification['status']; read_at?: string | null; dismissed_at?: string | null; reply?: string | null; replied_at?: string | null; } const NOTIFICATION_LEVELS: UserNotification['level'][] = ['info', 'warn', 'error', 'action']; const NOTIFICATION_KINDS: UserNotification['kind'][] = [ 'user_message', 'need_input', 'need_approval', 'task_progress', 'task_failed', 'task_finished', 'confirmation_pending', 'confirmation_resolved', ]; const APPROVAL_SUBSCRIPTION_ACTIONS: ApprovalSubscriptionPreferences['subscribed_actions'] = ['comment', 'delegate', 'mark_timeout', 'mark_risk', 'clear_risk', 'decision']; const APPROVAL_SUBSCRIPTION_RISK_LEVELS: ApprovalSubscriptionPreferences['subscribed_risk_levels'] = ['normal', 'medium', 'high', 'critical']; interface QimingclawServiceStatus { running?: boolean; error?: string; engineType?: string; port?: number; serverCount?: number; serverNames?: string[]; } interface QimingclawSyncStatus { running: boolean; syncing: boolean; enabled: boolean; endpoint: string | null; missingCredentials?: string[]; pending: number; failed: number; lastSyncAt: string | null; lastSyncError: string | null; failureSummary?: QimingclawSyncFailureSummary; recentFailures?: QimingclawSyncFailure[]; } interface QimingclawSyncFailureSummary { total: number; dueForRetry: number; backoff: number; byEntityType: Array<{ entityType: string; count: number }>; } interface QimingclawSyncAttempt { attemptNo: number; status: string; error: string | null; endpoint?: string | null; startedAt: string; finishedAt: string; } interface QimingclawSyncFailure { id: string; entityType: string; entityId: string; operation: string; attempts: number; lastAttemptAt: string | null; nextRetryAt?: string | null; dueForRetry?: boolean; managementRecordUrl?: string | null; attemptHistory?: QimingclawSyncAttempt[]; error: string | null; createdAt: string; updatedAt: string; } interface QimingclawSnapshot { generatedAt: string; services: Record; managedServices?: QimingclawManagedService[] | null; sessions: { total: number; active: number; items: Array<{ id: string; title?: string; status?: string; engineType?: string; projectId?: string }>; }; sync?: QimingclawSyncStatus | null; runtime?: QimingclawRuntimeRecords | null; uiState?: AdapterState | null; planTemplates?: QimingclawPlanTemplateRecord[] | null; } interface QimingclawManagedService { serviceId: string; name: string; kind: string; status: string; statusLabel: string; running: boolean; error?: string | null; requiresHumanAction: boolean; dependentTasks: string[]; restartCount: number; lastSeenAt: string; instance?: Record; lease?: Record; } interface QimingclawPlanTemplateList { generatedAt: string; templateDir: string; templates: QimingclawPlanTemplateRecord[]; } interface QimingclawPlanTemplateRecord extends DebugPlan { source: 'qimingclaw-plan-template'; template_path: string; updated_at: string; } interface QimingclawGovernanceCommandRecord { commandKind: string; commandId: string; correlationId?: string; planId?: string | null; taskId?: string | null; runId?: string | null; stepId?: string | null; scheduleId?: string | null; accepted?: boolean; message?: string; error?: string | null; payload?: Record | null; result?: Record | null; } interface QimingclawDailyReportRecordInput { reportId?: string | null; date: string; title: string; summary: string; totals: Record; detailLines: string[]; artifactSources?: unknown[]; filename: string; textContent: string; generatedAt?: string | null; reportScheduleTime?: string | null; source?: string | null; model?: string | null; } interface QimingclawDailyReportRecordResult { reportId: string; planId: string; taskId: string; runId: string; artifactId: string; eventId: string; } interface QimingclawArtifactAccessResponse { ok: boolean; artifact_id: string; action: 'preview' | 'download' | 'open_location'; status: 'allowed' | 'rejected'; error?: string; reason_codes?: string[]; preview?: Record; event_id?: string | null; } interface QimingclawArtifactLifecycleInput { artifactId?: string | null; action: string; reason?: string | null; retentionUntil?: string | null; versionSummary?: Record | null; retentionSummary?: Record | null; deliverySummary?: Record | null; actor?: string | null; source?: string | null; planId?: string | null; taskId?: string | null; runId?: string | null; } interface QimingclawArtifactLifecycleResult { eventId: string; kind: string; payload: Record; } interface QimingclawApprovalActionInput { approvalId?: string | null; action: string; comment?: string | null; delegateTo?: string | null; delegateLabel?: string | null; dueAt?: string | null; riskLevel?: string | null; reason?: string | null; actor?: string | null; source?: string | null; planId?: string | null; taskId?: string | null; runId?: string | null; } interface QimingclawApprovalActionResult { eventId: string; kind: string; payload: Record; } interface QimingclawManagedServiceActionResponse { ok: boolean; service_id: string; action: 'restart'; status: 'success' | 'failed' | 'rejected'; message?: string; error?: string; managed_service?: DigitalEmployeeManagedService | null; event_id?: string | null; } interface QimingclawRouteDecisionInput { id?: string | null; idempotencyKey?: string | null; correlationId?: string | null; routeKind: string; intentKind: string; reasonCodes?: string[]; candidateSkills?: string[]; contextRefs?: Record; createdCommandId?: string | null; createsPlan?: boolean; createsTaskRun?: boolean; approvalMode?: string | null; policyResult?: string | null; entrypoint?: string | null; actor?: string | null; message?: string | null; envelope?: Record | null; recordedAt?: string | null; } interface QimingclawRuntimeRecords { generatedAt: string; plans: QimingclawPlanRecord[]; planSteps?: QimingclawPlanStepRecord[]; schedules?: QimingclawScheduleRecord[]; scheduleRuns?: QimingclawScheduleRunRecord[]; tasks: QimingclawTaskRecord[]; runs: QimingclawRunRecord[]; events: QimingclawEventRecord[]; artifacts?: QimingclawArtifactRecord[]; approvals?: QimingclawApprovalRecord[]; memories?: QimingclawMemoryRecord[]; } interface QimingclawMemoryRecord { id: string; remoteId?: string | null; key: string; content: string; category: string; source: string; sourcePath?: string | null; sessionId?: string | null; score?: number | null; status: string; payload: unknown; syncStatus: string; lastSyncedAt?: string | null; syncError?: string | null; createdAt: string; updatedAt: string; deletedAt?: string | null; } interface QimingclawMemoryUpsertInput { id?: string; key?: string; content: string; category?: string; source?: string; sourcePath?: string | null; sessionId?: string | null; score?: number | null; status?: string; payload?: Record; } interface QimingclawCronSettings { enabled: boolean; catch_up_on_startup: boolean; max_run_history: number; max_catch_up_runs: number; max_concurrent_schedule_runs: number; } interface QimingclawScheduleRecord { id: string; remoteId?: string | null; planId?: string | null; name: string; description?: string | null; cronExpr: string; timezone: string; status: string; enabled: boolean; prompt?: string | null; command?: string | null; payload: unknown; nextRunAt?: string | null; lastRunAt?: string | null; catchUpOnStartup: boolean; maxCatchUpRuns: number; maxRunHistory: number; failureCount: number; lastError?: string | null; syncStatus: string; createdAt: string; updatedAt: string; deletedAt?: string | null; } interface QimingclawScheduleRunRecord { id: string; scheduleId: string; planId?: string | null; runId?: string | null; dueAt: string; startedAt?: string | null; finishedAt?: string | null; status: string; attemptNo: number; triggerKind: string; error?: string | null; payload: unknown; createdAt: string; updatedAt: string; } interface QimingclawPlanRecord { id: string; remoteId?: string | null; title: string; objective?: string | null; status: string; source: string; payload: unknown; syncStatus: string; lastSyncedAt?: string | null; syncError?: string | null; createdAt: string; updatedAt: string; } interface QimingclawPlanStepRecord { id: string; remoteId?: string | null; planId: string; title: string; status: string; seq: number; dependsOn: string[]; preferredSkillIds: string[]; payload: unknown; syncStatus: string; lastSyncedAt?: string | null; syncError?: string | null; createdAt: string; updatedAt: string; } interface QimingclawTaskRecord { id: string; remoteId?: string | null; planId?: string | null; title: string; status: string; assignedSkillId?: string | null; payload: unknown; syncStatus: string; lastSyncedAt?: string | null; syncError?: string | null; createdAt: string; updatedAt: string; } interface QimingclawRunRecord { id: string; remoteId?: string | null; planId?: string | null; taskId?: string | null; status: string; startedAt?: string | null; finishedAt?: string | null; payload: unknown; syncStatus: string; lastSyncedAt?: string | null; syncError?: string | null; createdAt: string; updatedAt: string; } interface QimingclawEventRecord { id: string; remoteId?: string | null; planId?: string | null; taskId?: string | null; runId?: string | null; kind: string; message: string; payload: unknown; syncStatus: string; lastSyncedAt?: string | null; syncError?: string | null; occurredAt: string; createdAt: string; } interface QimingclawArtifactRecord { id: string; remoteId?: string | null; planId?: string | null; taskId?: string | null; runId?: string | null; kind: string; name?: string | null; uri?: string | null; payload: unknown; syncStatus: string; lastSyncedAt?: string | null; syncError?: string | null; createdAt: string; } interface QimingclawApprovalRecord { id: string; remoteId?: string | null; planId?: string | null; taskId?: string | null; runId?: string | null; title: string; status: string; payload: unknown; syncStatus: string; lastSyncedAt?: string | null; syncError?: string | null; createdAt: string; updatedAt: string; } interface QimingclawSkillCapabilities { generatedAt: string; skills: SkillSpec[]; } const SKILLS: SkillSpec[] = [ { skill_id: 'qimingclaw-acp-session', name: 'ACP 会话执行', version: '1.0.0', enabled: true, description: '通过 qimingclaw 的 ACP 引擎创建会话、执行任务并跟踪结果。', category: '执行', capability_level: 'core', }, { skill_id: 'qimingclaw-mcp-tools', name: 'MCP 工具编排', version: '1.0.0', enabled: true, description: '读取已配置 MCP 服务与工具,作为数字员工的工具能力池。', category: '工具', capability_level: 'core', }, { skill_id: 'qimingclaw-computer-control', name: '客户端电脑控制', version: '1.0.0', enabled: true, description: '承接 /computer/chat 执行入口,面向远程任务和本地自动化操作。', category: '桌面', capability_level: 'specialized', }, { skill_id: 'qimingclaw-artifact-report', name: '产物与日报整理', version: '1.0.0', enabled: true, description: '把执行记录、关键产物和异常汇总成数字员工日报。', category: '汇报', capability_level: 'standard', }, ]; const PLANS: DebugPlan[] = [ { plan_id: 'qimingclaw-client-readiness', title: '客户端运行状态巡检', objective: '确认 qimingclaw 客户端、Agent、MCP 和远程接入能力是否处于可执行状态。', status: 'running', created_at: todayAt(9, 0), steps: [ { step_id: 'agent-status', title: '检查 Agent 引擎状态', depends_on: [], preferred_skills: ['qimingclaw-acp-session'], tasks: [{ task_id: 'task-agent-status', title: '读取 Agent 服务状态', status: 'running' }], }, { step_id: 'mcp-status', title: '检查 MCP 工具能力', depends_on: ['agent-status'], preferred_skills: ['qimingclaw-mcp-tools'], tasks: [{ task_id: 'task-mcp-status', title: '同步 MCP 工具列表', status: 'pending' }], }, ], }, { plan_id: 'qimingclaw-remote-task-ingress', title: '远程任务接入准备', objective: '检查 lanproxy、computer server 和文件服务,为后端下发数字员工任务做准备。', status: 'pending', created_at: todayAt(9, 30), steps: [ { step_id: 'computer-server', title: '确认 /computer/chat 执行入口', depends_on: [], preferred_skills: ['qimingclaw-computer-control'], tasks: [{ task_id: 'task-computer-server', title: '检查电脑控制服务', status: 'pending' }], }, { step_id: 'file-artifact', title: '准备产物回传目录', depends_on: ['computer-server'], preferred_skills: ['qimingclaw-artifact-report'], tasks: [{ task_id: 'task-file-artifact', title: '整理执行产物通道', status: 'pending' }], }, ], }, { plan_id: 'qimingclaw-daily-report', title: '数字员工日报生成', objective: '汇总今日执行记录、服务状态和待介入事项,生成面向业务的日报。', status: 'pending', created_at: todayAt(17, 30), steps: [ { step_id: 'collect-events', title: '汇总运行事件', depends_on: [], preferred_skills: ['qimingclaw-artifact-report'], tasks: [{ task_id: 'task-collect-events', title: '整理今日运行明细', status: 'pending' }], }, { step_id: 'write-report', title: '生成日报摘要', depends_on: ['collect-events'], preferred_skills: ['qimingclaw-artifact-report'], tasks: [{ task_id: 'task-write-report', title: '生成数字员工日报', status: 'pending' }], }, ], }, ]; const PLAN_SCHEDULES: Record = { 'qimingclaw-client-readiness': { expression: '0 9 * * *', nextRun: todayAt(9, 0), scheduleText: '已定时 09:00 执行', }, 'qimingclaw-remote-task-ingress': { expression: '30 9 * * *', nextRun: todayAt(9, 30), scheduleText: '已定时 09:30 执行', }, 'qimingclaw-daily-report': { expression: '0 18 * * *', nextRun: todayAt(18, 0), scheduleText: '已定时 18:00 执行', }, }; const BUSINESS_VIEW_SOURCE = 'qimingclaw-runtime-projection'; type BusinessViewKind = 'plans' | 'plan-steps' | 'plan-step-graph' | 'tasks' | 'runs' | 'events' | 'artifacts' | 'approvals' | 'approval-history' | 'route-decisions' | 'notifications' | 'audits'; type BusinessViewRow = Record; export function isQimingclawDigitalApiEnabled(): boolean { return window.__QIMINGCLAW_EMBEDDED_DIGITAL__ === true; } export async function handleQimingclawDigitalApi( path: string, options: RequestInit = {}, ): Promise { if (!isQimingclawDigitalApiEnabled()) return undefined; const url = new URL(path, window.location.origin); const method = (options.method || 'GET').toUpperCase(); const pathname = url.pathname; if (method === 'GET' && pathname === '/api/digital-employee/workday') { return buildWorkday(url.searchParams.get('date') || todayInputDate(), await readQimingclawSnapshot()) as T; } if (method === 'POST' && pathname === '/api/digital-employee/workday/actions') { return await handleWorkdayAction(parseJsonBody(options.body), await readQimingclawSnapshot()) as T; } const managedServiceRestartMatch = pathname.match(/^\/api\/(?:digital-employee\/)?managed-services\/([^/]+)\/restart$/); if (method === 'POST' && managedServiceRestartMatch) { return await handleManagedServiceRestart( decodeURIComponent(managedServiceRestartMatch[1] || ''), parseOptionalJsonBody(options.body), ) as T; } const dailyReportDetailMatch = pathname.match(/^\/api\/digital-employee\/daily-reports\/([^/]+)$/); if (method === 'GET' && dailyReportDetailMatch) { const reportId = decodeURIComponent(dailyReportDetailMatch[1] || ''); const item = dailyReportRows(await readQimingclawSnapshot()) .find((row) => stringValue(row.report_id) === reportId || stringValue(row.artifact_id) === reportId) ?? null; return { item, source: BUSINESS_VIEW_SOURCE } as T; } if (method === 'GET' && pathname === '/api/digital-employee/daily-reports') { const rows = filterDailyReportRows(dailyReportRows(await readQimingclawSnapshot()), url.searchParams); return { ...paginateBusinessRows(rows, url.searchParams), source: BUSINESS_VIEW_SOURCE } as T; } if (method === 'GET' && pathname === '/api/digital-employee/skill-call-audits') { const rows = filterSkillCallAuditRows(skillCallAuditRows(await readQimingclawSnapshot()), url.searchParams); return { ...paginateBusinessRows(rows, url.searchParams), source: BUSINESS_VIEW_SOURCE } as T; } if (method === 'GET' && pathname === '/api/digital-employee/artifact-access-events') { const rows = filterArtifactAccessRows(artifactAccessRows(await readQimingclawSnapshot()), url.searchParams); return { ...paginateBusinessRows(rows, url.searchParams), source: BUSINESS_VIEW_SOURCE } as T; } const artifactAccessMatch = pathname.match(/^\/api\/(?:digital-employee\/artifacts|artifact)\/([^/]+)\/access$/); if (method === 'POST' && artifactAccessMatch) { const artifactId = decodeURIComponent(artifactAccessMatch[1] || ''); return await handleArtifactAccess(artifactId, parseOptionalJsonBody(options.body)) as T; } const artifactRetentionMatch = pathname.match(/^\/api\/digital-employee\/artifacts\/([^/]+)\/retention$/); if (method === 'POST' && artifactRetentionMatch) { const artifactId = decodeURIComponent(artifactRetentionMatch[1] || ''); return await handleArtifactRetention(artifactId, parseOptionalJsonBody(options.body), await readQimingclawSnapshot()) as T; } const artifactVersionsMatch = pathname.match(/^\/api\/digital-employee\/artifacts\/([^/]+)\/versions$/); if (method === 'GET' && artifactVersionsMatch) { const artifactId = decodeURIComponent(artifactVersionsMatch[1] || ''); const snapshot = await readQimingclawSnapshot(); return { items: artifactVersionRows(snapshot, artifactId), source: BUSINESS_VIEW_SOURCE } as T; } const artifactDetailMatch = pathname.match(/^\/api\/digital-employee\/artifacts\/([^/]+)$/); if (method === 'GET' && artifactDetailMatch) { const artifactId = decodeURIComponent(artifactDetailMatch[1] || ''); const snapshot = await readQimingclawSnapshot(); const item = artifactDetailRow(snapshot, artifactId); return { item, source: BUSINESS_VIEW_SOURCE } as T; } const businessPlanDetailMatch = pathname.match(/^\/api\/digital-employee\/plans\/([^/]+)$/); if (method === 'GET' && businessPlanDetailMatch) { const planId = decodeURIComponent(businessPlanDetailMatch[1] || ''); const item = businessPlanRows(await readQimingclawSnapshot()) .find((row) => stringValue(row.plan_id) === planId || stringValue(row.remote_id) === planId) ?? null; return { item, source: BUSINESS_VIEW_SOURCE } as T; } const approvalActionMatch = pathname.match(/^\/api\/digital-employee\/approvals\/([^/]+)\/actions$/); if (method === 'POST' && approvalActionMatch) { const approvalId = decodeURIComponent(approvalActionMatch[1] || ''); return await handleApprovalAction(approvalId, parseOptionalJsonBody(options.body), await readQimingclawSnapshot()) as T; } if (method === 'GET' && pathname === '/api/approval/subscriptions') { return approvalSubscriptionPreferences(notificationAdapterState(await readQimingclawSnapshot())) as T; } if (method === 'PATCH' && pathname === '/api/approval/subscriptions') { return await handleApprovalSubscriptionPreferencesPatch( parseJsonBody>(options.body), await readQimingclawSnapshot(), ) as T; } if (method === 'GET' && pathname === '/api/plan-step/dispatch-policy') { return planStepDispatchPolicy(notificationAdapterState(await readQimingclawSnapshot())) as T; } if (method === 'PATCH' && pathname === '/api/plan-step/dispatch-policy') { return await handlePlanStepDispatchPolicyPatch( parseJsonBody>(options.body), await readQimingclawSnapshot(), ) as T; } if (method === 'GET' && pathname === '/api/digital-employee/plan-steps') { const rows = filterPlanStepRows(planStepRows(await readQimingclawSnapshot()), url.searchParams); return { ...paginateBusinessRows(rows, url.searchParams), source: BUSINESS_VIEW_SOURCE } as T; } if (method === 'GET' && pathname === '/api/digital-employee/plan-step-graph') { const rows = filterPlanStepRows(planStepGraphRows(await readQimingclawSnapshot()), url.searchParams); return { ...paginateBusinessRows(rows, url.searchParams), source: BUSINESS_VIEW_SOURCE } as T; } if (method === 'GET' && pathname === '/api/digital-employee/approval-history') { const rows = filterApprovalHistoryRows(approvalHistoryRows(await readQimingclawSnapshot()), url.searchParams); return { ...paginateBusinessRows(rows, url.searchParams), source: BUSINESS_VIEW_SOURCE } as T; } const businessViewMatch = pathname.match(/^\/api\/digital-employee\/(plans|plan-steps|plan-step-graph|tasks|runs|events|artifacts|approvals|approval-history|route-decisions|notifications|audits)$/); if (method === 'GET' && businessViewMatch) { return buildBusinessViewResponse(businessViewMatch[1] as BusinessViewKind, await readQimingclawSnapshot(), url.searchParams) as T; } if (method === 'GET' && pathname === '/api/route-decisions') { return await readRouteDecisions(Number(url.searchParams.get('limit')) || 80) as T; } if (method === 'POST' && pathname === '/api/ingress/route') { return await handleIngressRoute(parseJsonBody(options.body), await readQimingclawSnapshot()) as T; } if (method === 'POST' && pathname === '/api/governance/command') { return await handleGovernanceCommand(parseJsonBody(options.body), await readQimingclawSnapshot()) as T; } if (method === 'GET' && pathname === '/api/scheduler/jobs') { return { jobs: buildSchedulerJobs(await readQimingclawSnapshot()) } as T; } if (method === 'GET' && pathname === '/api/cron') { return { jobs: buildSchedulerJobs(await readQimingclawSnapshot()) } as T; } if (method === 'POST' && pathname === '/api/cron') { return await handleCronCreate(parseJsonBody>(options.body), await readQimingclawSnapshot()) as T; } if (method === 'GET' && pathname === '/api/cron/settings') { return await readCronSettings() as T; } if (method === 'PATCH' && pathname === '/api/cron/settings') { const patch = parseJsonBody>(options.body); return await saveCronSettings(patch) as T; } if (method === 'POST' && pathname === '/api/cron/check') { const result = await window.QimingClawBridge?.digital?.runSchedulerCheck?.(); return { ok: true, activated: result?.activated ?? 0, ...result } as T; } const cronRunMatch = pathname.match(/^\/api\/cron\/([^/]+)\/runs$/); if (method === 'GET' && cronRunMatch) { const runs = await readCronRuns(decodeURIComponent(cronRunMatch[1] || ''), Number(url.searchParams.get('limit')) || 20); return { runs } as T; } const cronJobMatch = pathname.match(/^\/api\/cron\/([^/]+)$/); if ((method === 'PATCH' || method === 'DELETE') && cronJobMatch) { const scheduleId = decodeURIComponent(cronJobMatch[1] || ''); if (method === 'DELETE') { await handleCronCommand('delete_schedule', scheduleId, {}, await readQimingclawSnapshot()); return undefined as T; } return await handleCronPatch(scheduleId, parseJsonBody>(options.body), await readQimingclawSnapshot()) as T; } if (method === 'GET' && pathname === '/api/debug/plans') { return { plans: filterPlans(url.searchParams.get('q'), await readQimingclawSnapshot()) } as T; } if (method === 'GET' && pathname === '/api/debug/tasks') { const planId = url.searchParams.get('plan_id'); return buildTasksResponse(planId, await readQimingclawSnapshot()) as T; } if (method === 'GET' && pathname === '/api/debug/store') { return buildDebugStore(await readQimingclawSnapshot()) as T; } if (method === 'GET' && pathname === '/api/dashboard') { return buildDashboard(await readQimingclawSnapshot()) as T; } if ((method === 'GET' || method === 'POST') && pathname === '/api/doctor') { return buildDoctor(await readQimingclawSnapshot()) as T; } if (method === 'GET' && pathname === '/api/cost') { return { cost: buildCostSummary(await readQimingclawSnapshot()) } as T; } if (method === 'GET' && pathname === '/api/audit') { return { events: buildAuditEvents(await readQimingclawSnapshot(), url.searchParams) } as T; } if (method === 'GET' && pathname === '/api/artifact') { return { artifacts: buildArtifacts(await readQimingclawSnapshot()) } as T; } const approvalTimelineMatch = pathname.match(/^\/api\/approval\/([^/]+)\/timeline$/); if (method === 'GET' && approvalTimelineMatch) { return buildApprovalTimeline(decodeURIComponent(approvalTimelineMatch[1] || ''), await readQimingclawSnapshot()) as T; } if (method === 'GET' && pathname === '/api/notifications') { return buildNotificationsResponse(url.searchParams.get('status'), await readQimingclawSnapshot()) as T; } if (method === 'GET' && pathname === '/api/notifications/preferences') { return notificationPreferences(notificationAdapterState(await readQimingclawSnapshot())) as T; } if (method === 'PATCH' && pathname === '/api/notifications/preferences') { return await handleNotificationPreferencesPatch( parseJsonBody>(options.body), await readQimingclawSnapshot(), ) as T; } if (method === 'GET' && pathname === '/api/notifications/unread-count') { const snapshot = await readQimingclawSnapshot(); return { count: buildNotifications(snapshot, notificationAdapterState(snapshot)).filter((notification) => notification.status === 'unread').length } as T; } const notificationActionMatch = pathname.match(/^\/api\/notifications\/([^/]+)\/(read|dismiss|reply)$/); if (method === 'POST' && notificationActionMatch) { return await handleNotificationAction( decodeURIComponent(notificationActionMatch[1] || ''), notificationActionMatch[2] as 'read' | 'dismiss' | 'reply', parseOptionalJsonBody(options.body), await readQimingclawSnapshot(), ) as T; } if (method === 'GET' && pathname === '/api/memory') { return { entries: await readMemoryEntries(url.searchParams.get('query'), url.searchParams.get('category')) } as T; } if (method === 'POST' && pathname === '/api/memory') { return await handleMemoryCreate(parseJsonBody>(options.body)) as T; } const memoryDeleteMatch = pathname.match(/^\/api\/memory\/([^/]+)$/); if (method === 'DELETE' && memoryDeleteMatch) { await handleMemoryDelete(decodeURIComponent(memoryDeleteMatch[1] || '')); return undefined as T; } if (method === 'GET' && pathname.startsWith('/api/debug/plan/') && pathname.endsWith('/flow')) { return buildPlanFlow(decodeURIComponent(pathname.split('/')[4] || ''), await readQimingclawSnapshot()) as T; } if (method === 'GET' && pathname.startsWith('/api/debug/task/') && pathname.endsWith('/flow')) { return buildTaskFlow(decodeURIComponent(pathname.split('/')[4] || ''), await readQimingclawSnapshot()) as T; } const planReadyDispatchMatch = pathname.match(/^\/api\/digital-employee\/plans\/([^/]+)\/dispatch-ready-steps$/); if (method === 'POST' && planReadyDispatchMatch) { return await dispatchPlanReadySteps(decodeURIComponent(planReadyDispatchMatch[1] || ''), await readQimingclawSnapshot()) as T; } if (method === 'POST' && pathname.startsWith('/api/debug/plan/') && pathname.endsWith('/dispatch')) { return await dispatchPlan(decodeURIComponent(pathname.split('/')[4] || ''), await readQimingclawSnapshot()) as T; } const debugPlanActionMatch = pathname.match(/^\/api\/debug\/plan\/([^/]+)\/([^/]+)$/); if (method === 'POST' && debugPlanActionMatch) { return await handlePlanAction( decodeURIComponent(debugPlanActionMatch[1] || ''), decodeURIComponent(debugPlanActionMatch[2] || ''), await readQimingclawSnapshot(), parseOptionalJsonBody(options.body), ) as T; } const planSubmitMatch = pathname.match(/^\/api\/plan\/([^/]+)\/submit$/); if (method === 'POST' && planSubmitMatch) { return await handlePlanAction( decodeURIComponent(planSubmitMatch[1] || ''), 'submit', await readQimingclawSnapshot(), parseOptionalJsonBody(options.body), ) as T; } if (method === 'GET' && pathname.startsWith('/api/plan/') && pathname.endsWith('/revisions')) { return buildPlanRevisions(decodeURIComponent(pathname.split('/')[3] || ''), await readQimingclawSnapshot()) as T; } const taskActionMatch = pathname.match(/^\/api\/task\/([^/]+)\/([^/]+)$/); if (method === 'POST' && taskActionMatch) { return await handleTaskAction( decodeURIComponent(taskActionMatch[1] || ''), decodeURIComponent(taskActionMatch[2] || ''), await readQimingclawSnapshot(), parseOptionalJsonBody(options.body), ) as T; } if (method === 'GET' && pathname.startsWith('/api/plan/') && pathname.endsWith('/messages')) { return { messages: buildPlanMessages(decodeURIComponent(pathname.split('/')[3] || ''), await readQimingclawSnapshot()) } as T; } if (method === 'GET' && pathname === '/api/skill') { return { skills: await buildSkills() } as T; } if (method === 'POST' && pathname.startsWith('/api/skill/') && pathname.endsWith('/enabled')) { const skillId = decodeURIComponent(pathname.split('/')[3] || ''); const body = parseJsonBody<{ enabled?: boolean }>(options.body); return await setBridgeSkillEnabled(skillId, body.enabled ?? true) as T; } return undefined; } function defaultState(): AdapterState { return { selectedDutyIdsByDate: {}, confirmedDates: {}, reportScheduleTime: DEFAULT_REPORT_TIME, reportGeneratedAtByDate: {}, decisionResultsByDate: {}, notificationStateById: {}, notificationPreferences: defaultNotificationPreferences(), approvalSubscriptionPreferences: defaultApprovalSubscriptionPreferences(), planStepDispatchPolicy: defaultPlanStepDispatchPolicy(), consoleRole: 'sales', rolePreferences: {}, profile: { name: '飞天数字员工', company: 'qimingclaw 客户端', position: '客户端任务执行员', currentStatus: 'working', }, }; } function readState(): AdapterState { try { const raw = window.localStorage.getItem(STATE_KEY); return normalizeAdapterState(raw ? JSON.parse(raw) as Partial : {}); } catch { return defaultState(); } } function normalizeAdapterState(value: Partial): AdapterState { return { ...defaultState(), ...value, notificationPreferences: notificationPreferences(value), approvalSubscriptionPreferences: approvalSubscriptionPreferences(value), planStepDispatchPolicy: planStepDispatchPolicy(value), }; } function writeState(state: AdapterState): void { window.localStorage.setItem(STATE_KEY, JSON.stringify(state)); } async function readQimingclawSnapshot(): Promise { try { const snapshot = await window.QimingClawBridge?.digital?.getSnapshot?.() ?? null; if (!snapshot) return null; const [sync, runtime, uiState, planTemplates] = await Promise.all([ window.QimingClawBridge?.digital?.getSyncStatus?.().catch(() => null) ?? null, window.QimingClawBridge?.digital?.getRuntimeRecords?.().catch(() => null) ?? null, readBridgeUiState(), window.QimingClawBridge?.digital?.getPlanTemplates?.().catch(() => null) ?? null, ]); return { ...snapshot, sync, runtime, uiState, planTemplates: planTemplates?.templates ?? [] }; } catch { return null; } } async function readBridgeUiState(): Promise { try { return await window.QimingClawBridge?.digital?.getUiState?.() ?? null; } catch { return null; } } async function readBridgeSkillCapabilities(): Promise { try { const result = await window.QimingClawBridge?.digital?.getSkillCapabilities?.() ?? null; return Array.isArray(result?.skills) ? result.skills : []; } catch { return []; } } async function buildSkills(): Promise { const bridgeSkills = await readBridgeSkillCapabilities(); return dedupeSkills([...bridgeSkills, ...SKILLS]); } async function setBridgeSkillEnabled( skillId: string, enabled: boolean, ): Promise<{ ok: boolean; skill_id: string; enabled: boolean; error?: string }> { const bridge = window.QimingClawBridge?.digital; if (!bridge?.setSkillEnabled) { return { ok: false, skill_id: skillId, enabled, error: 'qimingclaw_bridge_unavailable' }; } try { const result = await bridge.setSkillEnabled(skillId, enabled); return { ok: result?.ok !== false, skill_id: result?.skill_id ?? skillId, enabled: result?.enabled ?? enabled, ...(result?.error ? { error: result.error } : {}), }; } catch (error) { return { ok: false, skill_id: skillId, enabled, error: error instanceof Error ? error.message : String(error), }; } } function dedupeSkills(skills: SkillSpec[]): SkillSpec[] { const seen = new Set(); const result: SkillSpec[] = []; for (const skill of skills) { const skillId = typeof skill.skill_id === 'string' ? skill.skill_id.trim() : ''; if (!skillId || seen.has(skillId)) continue; seen.add(skillId); result.push({ ...skill, skill_id: skillId }); } return result; } async function saveState( state: AdapterState, action?: string, date?: string, metadata?: Record, ): Promise { writeState(state); try { return await window.QimingClawBridge?.digital?.saveUiState?.({ state, action, date, metadata }) ?? state; } catch { return state; } } async function recordGovernanceCommand( body: GovernanceCommandRequest, response: GovernanceCommandResponse, ): Promise { try { await window.QimingClawBridge?.digital?.recordGovernanceCommand?.({ commandKind: body.command_kind, commandId: response.command_id, correlationId: response.correlation_id, planId: stringFromUnknown(response.result?.plan_id) || stringFromUnknown(body.context_refs?.plan_id) || stringFromUnknown(body.payload?.plan_id) || null, taskId: stringFromUnknown(response.result?.task_id) || stringFromUnknown(body.context_refs?.task_id) || stringFromUnknown(body.payload?.task_id) || null, runId: stringFromUnknown(response.result?.run_id) || stringFromUnknown(body.context_refs?.run_id) || stringFromUnknown(body.payload?.run_id) || stringFromUnknown(body.payload?.latest_run_id) || null, stepId: stringFromUnknown(response.result?.step_id) || stringFromUnknown(body.context_refs?.step_id) || stringFromUnknown(body.payload?.step_id) || stringFromUnknown(body.payload?.stepId) || null, scheduleId: stringFromUnknown(response.result?.schedule_id) || stringFromUnknown(body.context_refs?.schedule_id) || stringFromUnknown(body.payload?.schedule_id) || null, accepted: response.accepted, message: response.message, error: response.error ?? null, payload: body.payload ?? null, result: response.result ?? null, }); } catch { // Bridge persistence is best-effort; the adapter response should not block UI flow. } } async function readCronSettings(): Promise { try { return await window.QimingClawBridge?.digital?.getCronSettings?.() ?? defaultCronSettings(); } catch { return defaultCronSettings(); } } async function saveCronSettings(patch: Partial): Promise { try { return await window.QimingClawBridge?.digital?.saveCronSettings?.(patch) ?? { ...defaultCronSettings(), ...patch }; } catch { return { ...defaultCronSettings(), ...patch }; } } async function readMemoryEntries(query?: string | null, category?: string | null): Promise { try { const records = await window.QimingClawBridge?.digital?.listMemories?.({ query: query || undefined, category: category || undefined, limit: 300, }) ?? []; return records.map(memoryEntryFromRecord); } catch { return []; } } async function handleMemoryCreate(body: Record): Promise<{ ok: true; entry: MemoryEntry | null }> { const key = stringFromUnknown(body.key); const content = stringFromUnknown(body.content ?? body.text); const category = stringFromUnknown(body.category) || 'fact'; if (!content) return { ok: true, entry: null }; const record = await window.QimingClawBridge?.digital?.addMemory?.({ key: key || undefined, content, category, source: 'qimingclaw-digital-page', payload: { source: 'embedded-memory-api', key: key || null, }, }) ?? null; return { ok: true, entry: record ? memoryEntryFromRecord(record) : null }; } async function handleMemoryDelete(keyOrId: string): Promise { try { await window.QimingClawBridge?.digital?.deleteMemory?.(keyOrId); } catch { // Keep the DELETE endpoint best-effort so the UI can refresh into the true state. } } function memoryEntryFromRecord(record: QimingclawMemoryRecord): MemoryEntry { return { id: record.id, key: record.key || record.id, content: record.content, category: record.category || 'fact', timestamp: record.updatedAt || record.createdAt || new Date().toISOString(), session_id: record.sessionId ?? null, score: typeof record.score === 'number' ? record.score : null, }; } function defaultCronSettings(): QimingclawCronSettings { return { enabled: true, catch_up_on_startup: true, max_run_history: 100, max_catch_up_runs: 5, max_concurrent_schedule_runs: 1, }; } async function handleCronCreate( body: Record, snapshot: QimingclawSnapshot | null, ): Promise<{ status: string; job: CronJob }> { const stamp = Date.now(); const scheduleId = stringFromUnknown(body.id) || stringFromUnknown(body.schedule_id) || `cron-${slugifyIdPart(stringFromUnknown(body.name) || 'job')}-${stamp}`; await handleGovernanceCommand({ command_kind: 'create_schedule', context_refs: { schedule_id: scheduleId, plan_id: stringFromUnknown(body.plan_id) || stringFromUnknown(body.template_id) || undefined }, payload: { ...body, schedule_id: scheduleId, cron_expr: stringFromUnknown(body.schedule) || stringFromUnknown(body.cron_expr), }, correlation_id: `qimingclaw-cron-create-${stamp}`, idempotency_key: `qimingclaw-cron-create-${scheduleId}-${stamp}`, }, snapshot); const refreshed = await readQimingclawSnapshot(); return { status: 'created', job: buildSchedulerJobs(refreshed).find((job) => job.id === scheduleId) ?? cronJobFromBody(scheduleId, body) }; } async function handleCronPatch( scheduleId: string, patch: Record, snapshot: QimingclawSnapshot | null, ): Promise<{ status: string; job: CronJob }> { await handleCronCommand('update_schedule', scheduleId, patch, snapshot); const refreshed = await readQimingclawSnapshot(); return { status: 'updated', job: buildSchedulerJobs(refreshed).find((job) => job.id === scheduleId) ?? cronJobFromBody(scheduleId, patch) }; } async function handleCronCommand( commandKind: GovernanceCommandRequest['command_kind'], scheduleId: string, payload: Record, snapshot: QimingclawSnapshot | null, ): Promise { const stamp = Date.now(); return handleGovernanceCommand({ command_kind: commandKind, context_refs: { schedule_id: scheduleId, plan_id: stringFromUnknown(payload.plan_id) || stringFromUnknown(payload.template_id) || undefined }, payload: { ...payload, schedule_id: scheduleId, cron_expr: stringFromUnknown(payload.schedule) || stringFromUnknown(payload.cron_expr), }, correlation_id: `qimingclaw-cron-${commandKind}-${stamp}`, idempotency_key: `qimingclaw-cron-${commandKind}-${scheduleId}-${stamp}`, }, snapshot); } function cronJobFromBody(id: string, body: Record): CronJob { const now = new Date().toISOString(); const expression = stringFromUnknown(body.schedule) || stringFromUnknown(body.cron_expr) || '0 9 * * *'; return { id, name: stringFromUnknown(body.name) || id, expression, command: stringFromUnknown(body.command) || `digital-employee:${id}`, prompt: stringFromUnknown(body.prompt) || null, job_type: stringFromUnknown(body.job_type) || 'qimingclaw_schedule', schedule: { expression, schedule_id: id }, enabled: body.enabled !== false, delivery: { mode: 'qimingclaw_schedule', schedule_id: id }, template_id: stringFromUnknown(body.template_id) || stringFromUnknown(body.plan_id) || id, trigger_kind: 'scheduled', delete_after_run: body.delete_after_run === true, created_at: now, next_run: now, last_run: null, last_status: null, last_output: null, }; } async function readCronRuns(scheduleId: string, limit: number): Promise { const runs = await window.QimingClawBridge?.digital?.getScheduleRuns?.(scheduleId, limit).catch(() => []) ?? []; return runs.map((run, index) => ({ id: Number.isFinite(Date.parse(run.createdAt)) ? Date.parse(run.createdAt) + index : index + 1, job_id: run.scheduleId, started_at: run.startedAt || run.createdAt, finished_at: run.finishedAt || '', status: run.status, output: run.error || null, duration_ms: run.startedAt && run.finishedAt ? Math.max(0, Date.parse(run.finishedAt) - Date.parse(run.startedAt)) : null, })); } function parseJsonBody(body: BodyInit | null | undefined): T { if (typeof body !== 'string' || !body.trim()) return {} as T; return JSON.parse(body) as T; } function parseOptionalJsonBody(body: BodyInit | null | undefined): Record { return parseJsonBody>(body); } function todayInputDate(): string { const date = new Date(); return [ date.getFullYear(), String(date.getMonth() + 1).padStart(2, '0'), String(date.getDate()).padStart(2, '0'), ].join('-'); } function todayAt(hour: number, minute: number): string { const date = new Date(); date.setHours(hour, minute, 0, 0); return date.toISOString(); } function displayDate(date: string): string { return date.split('-').join('/'); } function formatTime(value?: string | null): string { if (!value) return '未知'; const date = new Date(value); if (Number.isNaN(date.getTime())) return value; return new Intl.DateTimeFormat('zh-CN', { hour: '2-digit', minute: '2-digit', }).format(date); } function selectedSetForDate(state: AdapterState, date: string): Set { return new Set(state.selectedDutyIdsByDate[date] ?? []); } function planStatusTone(status: string): string { if (['completed', 'succeeded', 'success'].includes(status)) return 'success'; if (['running', 'active'].includes(status)) return 'running'; if (['failed', 'error'].includes(status)) return 'danger'; return 'waiting'; } function isRunning(snapshot: QimingclawSnapshot | null, key: string): boolean { return snapshot?.services[key]?.running === true; } function serviceError(snapshot: QimingclawSnapshot | null, key: string): string | undefined { return snapshot?.services[key]?.error; } function runtimeStatusForPlan(plan: DebugPlan, snapshot: QimingclawSnapshot | null): string { if (!snapshot) return plan.status; if (plan.plan_id === 'qimingclaw-client-readiness') { if (isRunning(snapshot, 'agent') && isRunning(snapshot, 'mcp')) return 'running'; if (serviceError(snapshot, 'agent') || serviceError(snapshot, 'mcp')) return 'failed'; return 'pending'; } if (plan.plan_id === 'qimingclaw-remote-task-ingress') { if (isRunning(snapshot, 'computerServer') || isRunning(snapshot, 'lanproxy') || isRunning(snapshot, 'fileServer')) return 'running'; if (serviceError(snapshot, 'computerServer') || serviceError(snapshot, 'lanproxy') || serviceError(snapshot, 'fileServer')) return 'failed'; return 'pending'; } return plan.status; } function serviceLine(snapshot: QimingclawSnapshot | null, key: string, label: string): string { const service = snapshot?.services[key]; if (!service) return `${label}:未连接`; if (service.running) { const suffix = service.port ? `(端口 ${service.port})` : ''; return `${label}:运行中${suffix}`; } return `${label}:未运行${service.error ? `,${service.error}` : ''}`; } function buildManagedServices(snapshot: QimingclawSnapshot | null): DigitalEmployeeManagedService[] { const fromSnapshot = snapshot?.managedServices ?? []; if (fromSnapshot.length > 0) { return fromSnapshot.map((service) => ({ service_id: service.serviceId, name: service.name, kind: service.kind, status: service.status, status_label: service.statusLabel, running: service.running, error: service.error ?? null, requires_human_action: service.requiresHumanAction, dependent_tasks: service.dependentTasks ?? [], restart_count: service.restartCount ?? 0, last_seen_at: service.lastSeenAt, instance: service.instance, lease: service.lease, })); } return [ fallbackManagedService(snapshot, 'agent', 'Agent', 'agent', ['ACP 会话执行', 'Plan Agent']), fallbackManagedService(snapshot, 'mcp', 'MCP Proxy', 'mcp', ['Skill Catalog', 'MCP 工具调用']), fallbackManagedService(snapshot, 'computerServer', 'Computer Server', 'desktop', ['/computer/chat', '远程任务接入']), fallbackManagedService(snapshot, 'fileServer', 'File Server', 'file', ['Artifact 上传', '文件交付']), fallbackManagedService(snapshot, 'lanproxy', 'Lanproxy', 'network', ['远程访问', '管理端回连']), fallbackManagedService(snapshot, 'guiServer', 'GUI Agent Server', 'gui', ['GUI 自动化']), ]; } function fallbackManagedService( snapshot: QimingclawSnapshot | null, key: string, name: string, kind: string, dependentTasks: string[], ): DigitalEmployeeManagedService { const service = snapshot?.services[key]; const running = service?.running === true; const status = service?.error ? 'degraded' : running ? 'running' : 'stopped'; return { service_id: key, name, kind, status, status_label: status === 'running' ? '运行中' : status === 'degraded' ? '异常' : '未运行', running, error: service?.error ?? null, requires_human_action: Boolean(service?.error), dependent_tasks: dependentTasks, restart_count: 0, last_seen_at: snapshot?.generatedAt ?? new Date().toISOString(), instance: service ? { port: service.port, engineType: service.engineType, serverCount: service.serverCount, serverNames: service.serverNames, } : undefined, lease: { state: status === 'running' ? 'active' : status, updatedAt: snapshot?.generatedAt ?? new Date().toISOString(), }, }; } function managedServiceLine(service: DigitalEmployeeManagedService): string { const deps = service.dependent_tasks.length > 0 ? `;依赖任务:${service.dependent_tasks.slice(0, 2).join('、')}` : ''; return `${service.name}:${service.status_label}${service.error ? `,${service.error}` : ''}${deps}`; } function managedServiceSummary(services: DigitalEmployeeManagedService[]): string { const runningCount = services.filter((service) => service.running).length; const issueCount = services.filter((service) => service.requires_human_action || service.status === 'degraded').length; return `ManagedService:${runningCount}/${services.length} 运行,${issueCount} 项需处理`; } async function handleManagedServiceRestart( serviceId: string, body: Record, ): Promise { const bridge = window.QimingClawBridge?.digital; if (!bridge?.restartManagedService) { const snapshot = await readQimingclawSnapshot(); return { ok: false, service_id: serviceId, action: 'restart', status: 'failed', error: 'bridge_unavailable', managed_service: buildManagedServices(snapshot).find((service) => service.service_id === serviceId) ?? null, }; } try { const result = await bridge.restartManagedService(serviceId, { reason: stringValue(body.reason) || 'digital_employee_managed_service_restart', actor: stringValue(body.actor) || 'digital_employee_ui', }); const snapshot = await readQimingclawSnapshot(); return { ...result, managed_service: result.managed_service ?? buildManagedServices(snapshot).find((service) => service.service_id === serviceId) ?? null, }; } catch (error) { const snapshot = await readQimingclawSnapshot(); return { ok: false, service_id: serviceId, action: 'restart', status: 'failed', error: error instanceof Error ? error.message : String(error), managed_service: buildManagedServices(snapshot).find((service) => service.service_id === serviceId) ?? null, }; } } async function handleArtifactAccess( artifactId: string, body: Record, ): Promise { const bridge = window.QimingClawBridge?.digital; const action = normalizeArtifactAccessAction(stringValue(body.action)); if (!bridge?.accessArtifact) { return { ok: false, artifact_id: artifactId, action, status: 'rejected', error: 'bridge_unavailable', reason_codes: ['bridge_unavailable'], }; } try { return await bridge.accessArtifact(artifactId, action, { actor: stringValue(body.actor) || 'digital_employee_ui', source: stringValue(body.source) || 'sgrobot-digital-adapter', }); } catch (error) { return { ok: false, artifact_id: artifactId, action, status: 'rejected', error: error instanceof Error ? error.message : String(error), reason_codes: ['artifact_access_failed'], }; } } function normalizeArtifactAccessAction(value: string): 'preview' | 'download' | 'open_location' { if (value === 'download' || value === 'open_location') return value; return 'preview'; } async function handleArtifactRetention( artifactId: string, body: Record, snapshot: QimingclawSnapshot | null, ): Promise> { const bridge = window.QimingClawBridge?.digital; const action = stringValue(body.action) || 'mark_review'; const artifact = artifactDetailRow(snapshot, artifactId); if (!bridge?.recordArtifactLifecycle) { return { ok: false, artifact_id: artifactId, action, status: 'rejected', error: 'bridge_unavailable' }; } const result = await bridge.recordArtifactLifecycle({ artifactId, action, reason: stringValue(body.reason) || null, retentionUntil: stringValue(body.retention_until ?? body.retentionUntil) || null, versionSummary: artifact ? artifactVersionSummary(artifact) : null, retentionSummary: artifact ? artifactRetentionSummaryForAction(artifact, action, body) : null, deliverySummary: artifact ? artifactDeliverySummary(artifact) : null, actor: stringValue(body.actor) || 'digital_employee_ui', source: stringValue(body.source) || 'sgrobot-digital-adapter', planId: artifact ? stringValue(artifact.plan_id) || null : null, taskId: artifact ? stringValue(artifact.task_id) || null : null, runId: artifact ? stringValue(artifact.run_id) || null : null, }); return { ok: Boolean(result && result.kind !== 'artifact_retention_rejected'), artifact_id: artifactId, action, status: result?.kind === 'artifact_retention_rejected' ? 'rejected' : 'recorded', event_id: result?.eventId ?? null, item: artifact, }; } async function handleApprovalAction( approvalId: string, body: Record, snapshot: QimingclawSnapshot | null, ): Promise> { const bridge = window.QimingClawBridge?.digital; const action = stringValue(body.action) || 'comment'; const approval = approvalDetailRow(snapshot, approvalId); if (!bridge?.recordApprovalAction) { return { ok: false, approval_id: approvalId, action, status: 'rejected', error: 'bridge_unavailable' }; } const result = await bridge.recordApprovalAction({ approvalId, action, comment: stringValue(body.comment) || null, delegateTo: stringValue(body.delegate_to ?? body.delegateTo) || null, delegateLabel: stringValue(body.delegate_label ?? body.delegateLabel) || null, dueAt: stringValue(body.due_at ?? body.dueAt) || null, riskLevel: stringValue(body.risk_level ?? body.riskLevel) || null, reason: stringValue(body.reason) || null, actor: stringValue(body.actor) || 'digital_employee_ui', source: stringValue(body.source) || 'sgrobot-digital-adapter', planId: approval ? stringValue(approval.plan_id) || null : null, taskId: approval ? stringValue(approval.task_id) || null : null, runId: approval ? stringValue(approval.run_id) || null : null, }); return { ok: Boolean(result && result.kind !== 'approval_action_rejected'), approval_id: approvalId, action, status: result?.kind === 'approval_action_rejected' ? 'rejected' : 'recorded', event_id: result?.eventId ?? null, item: approval, }; } function syncLine(snapshot: QimingclawSnapshot | null): string { const sync = snapshot?.sync; if (!sync) return '管理端同步:未连接'; if (!sync.enabled) return '管理端同步:已停用'; if (!sync.endpoint) return '管理端同步:未配置地址'; if (sync.syncing) return `管理端同步:同步中,待同步 ${sync.pending} 条`; if (sync.failed > 0) return `管理端同步:${sync.failed} 条失败,待同步 ${sync.pending} 条`; if (sync.pending > 0) return `管理端同步:待同步 ${sync.pending} 条`; return sync.lastSyncAt ? `管理端同步:已同步,最近 ${sync.lastSyncAt}` : '管理端同步:等待本地数据'; } function compactSyncText(value?: string | null): string { return String(value || '').replace(/\s+/g, ' ').trim(); } function runtimePlans(snapshot: QimingclawSnapshot | null): QimingclawPlanRecord[] { return snapshot?.runtime?.plans ?? []; } function runtimePlanSteps(snapshot: QimingclawSnapshot | null): QimingclawPlanStepRecord[] { return snapshot?.runtime?.planSteps ?? []; } function runtimeTasks(snapshot: QimingclawSnapshot | null): QimingclawTaskRecord[] { return snapshot?.runtime?.tasks ?? []; } function runtimeRuns(snapshot: QimingclawSnapshot | null): QimingclawRunRecord[] { return snapshot?.runtime?.runs ?? []; } function runtimeEvents(snapshot: QimingclawSnapshot | null): QimingclawEventRecord[] { return snapshot?.runtime?.events ?? []; } function runtimeArtifacts(snapshot: QimingclawSnapshot | null): QimingclawArtifactRecord[] { return snapshot?.runtime?.artifacts ?? []; } function runtimeApprovals(snapshot: QimingclawSnapshot | null): QimingclawApprovalRecord[] { return snapshot?.runtime?.approvals ?? []; } function hasRuntimeRecords(snapshot: QimingclawSnapshot | null): boolean { return runtimePlans(snapshot).length > 0 || runtimePlanSteps(snapshot).length > 0 || runtimeTasks(snapshot).length > 0 || runtimeArtifacts(snapshot).length > 0 || runtimeApprovals(snapshot).length > 0; } function latestRuntimeTime(plan: QimingclawPlanRecord, tasks: QimingclawTaskRecord[]): string { const sorted = [plan.updatedAt, ...tasks.map((task) => task.updatedAt)] .filter(Boolean) .sort(); return sorted[sorted.length - 1] ?? plan.updatedAt ?? plan.createdAt; } function taskStatusTone(status: string): 'running' | 'success' | 'danger' | 'waiting' { const normalized = status.toLowerCase(); if (['completed', 'succeeded', 'success', 'done'].includes(normalized)) return 'success'; if (['failed', 'error', 'rejected', 'cancelled'].includes(normalized)) return 'danger'; if (['running', 'active', 'leased', 'syncing'].includes(normalized)) return 'running'; return 'waiting'; } function taskStatusLabel(status: string): string { const normalized = status.toLowerCase(); if (normalized === 'ready') return '待启动'; if (normalized === 'blocked') return '依赖阻塞'; if (normalized === 'skipped') return '已跳过'; if (normalized === 'cancelled') return '已取消'; if (normalized === 'paused') return '已暂停'; const tone = taskStatusTone(status); if (tone === 'success') return '已完成'; if (tone === 'danger') return '异常'; if (tone === 'running') return '执行中'; return '待执行'; } function runtimePlanTasks(snapshot: QimingclawSnapshot | null, planId: string): QimingclawTaskRecord[] { return runtimeTasks(snapshot).filter((task) => task.planId === planId); } function runtimeStepsForPlan(snapshot: QimingclawSnapshot | null, planId: string): QimingclawPlanStepRecord[] { return runtimePlanSteps(snapshot) .filter((step) => step.planId === planId) .sort((left, right) => left.seq - right.seq || left.createdAt.localeCompare(right.createdAt)); } function taskStepId(task: QimingclawTaskRecord): string { const record = asRecord(task.payload); return stringValue(record?.stepId ?? record?.step_id) || (task.planId ? `${task.planId}:runtime` : 'qimingclaw-runtime'); } function planRecordStatus(plan: QimingclawPlanRecord, tasks: QimingclawTaskRecord[]): string { if (tasks.some((task) => taskStatusTone(task.status) === 'danger')) return 'failed'; if (tasks.some((task) => taskStatusTone(task.status) === 'running')) return 'running'; if (tasks.length > 0 && tasks.every((task) => taskStatusTone(task.status) === 'success')) return 'completed'; return plan.status || 'pending'; } function buildPlanAgents(snapshot: QimingclawSnapshot | null): DigitalEmployeePlanAgentState[] { return runtimePlans(snapshot).map((plan) => { const tasks = runtimePlanTasks(snapshot, plan.id); const status = planRecordStatus(plan, tasks); const tone = planStatusTone(status); const failedTaskCount = tasks.filter((task) => taskStatusTone(task.status) === 'danger').length; const runningTaskCount = tasks.filter((task) => taskStatusTone(task.status) === 'running').length; return { agent_id: `plan-agent-${slugifyIdPart(plan.id)}`, plan_id: plan.id, title: plan.title, lifecycle: status, status_label: planStatusLabel(status), status_tone: tone, managed_task_count: tasks.length, running_task_count: runningTaskCount, failed_task_count: failedTaskCount, next_action: failedTaskCount > 0 ? 'review_failures' : runningTaskCount > 0 ? 'monitor_runs' : tone === 'success' ? 'archive_results' : 'activate_plan', can_replan: failedTaskCount > 0, can_pause: runningTaskCount > 0, can_resume: status === 'paused', updated_at: latestRuntimeTime(plan, tasks), }; }); } function buildTaskAgents(snapshot: QimingclawSnapshot | null): DigitalEmployeeTaskAgentState[] { const runs = runtimeRuns(snapshot); return runtimeTasks(snapshot).map((task) => { const latestRun = latestRunForTask(runs, task.id); const tone = taskStatusTone(task.status); const normalizedStatus = task.status.toLowerCase(); const stepId = taskStepId(task); return { agent_id: `task-agent-${slugifyIdPart(task.id)}`, task_id: task.id, plan_id: task.planId ?? null, step_id: stepId, title: task.title, execution_state: task.status, status_label: taskStatusLabel(task.status), status_tone: tone, assigned_skill_id: task.assignedSkillId ?? null, latest_run_id: latestRun?.id ?? null, can_retry: ['failed', 'cancelled', 'skipped'].includes(normalizedStatus), can_skip: ['pending', 'running'].includes(normalizedStatus), can_cancel: ['pending', 'running', 'paused'].includes(normalizedStatus), next_action: taskAgentNextAction(normalizedStatus, tone), updated_at: latestRun?.updatedAt ?? task.updatedAt, }; }); } function taskAgentNextAction( status: string, tone: ReturnType, ): string { if (['failed', 'cancelled', 'skipped'].includes(status)) return 'retry_or_review'; if (status === 'paused') return 'resume_or_cancel'; if (tone === 'running') return 'watch_progress'; if (tone === 'success') return 'collect_artifacts'; return 'start_run'; } function latestRunForTask(runs: QimingclawRunRecord[], taskId: string): QimingclawRunRecord | null { const sorted = runs .filter((run) => run.taskId === taskId) .sort((left, right) => (right.updatedAt || right.createdAt).localeCompare(left.updatedAt || left.createdAt)); return sorted[0] ?? null; } function runtimePlanToDebugPlan(plan: QimingclawPlanRecord, snapshot: QimingclawSnapshot | null): DebugPlan { const tasks = runtimePlanTasks(snapshot, plan.id); const planSteps = runtimeStepsForPlan(snapshot, plan.id); const status = planRecordStatus(plan, tasks); const knownStepIds = new Set(planSteps.map((step) => step.id)); const orphanTasks = tasks.filter((task) => !knownStepIds.has(taskStepId(task))); const steps = planSteps.length > 0 ? [ ...planSteps.map((step) => runtimePlanStepToSummary(step, tasks)), ...(orphanTasks.length > 0 ? [runtimeFallbackStepToSummary(plan.id, orphanTasks)] : []), ] : [runtimeFallbackStepToSummary(plan.id, tasks)]; return { plan_id: plan.id, title: plan.title, objective: plan.objective ?? undefined, status, created_at: plan.createdAt, updated_at: latestRuntimeTime(plan, tasks), source: plan.source, sync_status: plan.syncStatus, steps, }; } function runtimePlanStepToSummary( step: QimingclawPlanStepRecord, tasks: QimingclawTaskRecord[], ): PlanStepSummary { return { step_id: step.id, title: step.title, depends_on: step.dependsOn, preferred_skills: step.preferredSkillIds, tasks: tasks .filter((task) => taskStepId(task) === step.id) .map((task) => ({ task_id: task.id, title: task.title, status: task.status, })), }; } function runtimeFallbackStepToSummary( planId: string, tasks: QimingclawTaskRecord[], ): PlanStepSummary { return { step_id: `${planId}:runtime`, title: 'qimingclaw 执行记录', depends_on: [], preferred_skills: [...new Set(tasks.map((task) => task.assignedSkillId).filter((value): value is string => Boolean(value)))], tasks: tasks.map((task) => ({ task_id: task.id, title: task.title, status: task.status, })), }; } function runtimeTaskToDebugTask(task: QimingclawTaskRecord): DebugTask { return { task_id: task.id, title: task.title, status: task.status, plan_id: task.planId ?? null, step_id: taskStepId(task), assigned_skill_id: task.assignedSkillId ?? '', description: task.syncError ?? '', created_at: task.createdAt, updated_at: task.updatedAt, started_at: task.createdAt, finished_at: taskStatusTone(task.status) === 'success' || taskStatusTone(task.status) === 'danger' ? task.updatedAt : null, error_message: task.syncError ?? null, sync_status: task.syncStatus, }; } function runtimeRunToDebugRun(run: QimingclawRunRecord, events: QimingclawEventRecord[]): DebugRun { return { run_id: run.id, task_id: run.taskId ?? '', plan_id: run.planId ?? null, skill_id: undefined, attempt_no: 1, lifecycle: taskStatusTone(run.status) === 'success' ? 'Succeeded' : taskStatusTone(run.status) === 'danger' ? 'Failed' : taskStatusTone(run.status) === 'running' ? 'Running' : run.status, created_at: run.startedAt ?? run.createdAt, completed_at: run.finishedAt ?? null, events: events.filter((event) => event.runId === run.id).map((event) => ({ event_id: event.id, kind: event.kind, occurred_at: event.occurredAt, message: event.message, })), artifacts: artifactsForRun(run, events), sync_status: run.syncStatus, }; } function syncFailureLine(failure: QimingclawSyncFailure): string { const error = compactSyncText(failure.error); const detail = error.length > 120 ? `${error.slice(0, 120)}...` : error; const retryText = failure.dueForRetry ? '等待补偿' : failure.nextRetryAt ? `下次重试 ${failure.nextRetryAt}` : '等待重试'; return `${failure.entityType}/${failure.operation} ${failure.id} 重试 ${failure.attempts} 次,${retryText}${detail ? `,${detail}` : ''}`; } function withRuntimePlan(plan: DebugPlan, snapshot: QimingclawSnapshot | null): DebugPlan { return { ...plan, status: runtimeStatusForPlan(plan, snapshot) }; } function planStatusLabel(status: string): string { if (status === 'running') return '执行中'; if (status === 'completed' || status === 'succeeded') return '已完成'; if (status === 'failed') return '异常'; return '待执行'; } function runtimeDebugPlans(snapshot: QimingclawSnapshot | null): DebugPlan[] { const plans = runtimePlans(snapshot); if (plans.length > 0) return plans.map((plan) => runtimePlanToDebugPlan(plan, snapshot)); const tasks = runtimeTasks(snapshot); if (tasks.length === 0) return []; return tasks.map((task) => ({ plan_id: task.id, title: task.title, objective: '未绑定 Plan 的 qimingclaw 本地任务记录。', status: task.status, created_at: task.createdAt, updated_at: task.updatedAt, source: 'qimingclaw', sync_status: task.syncStatus, steps: [ { step_id: `${task.id}:runtime`, title: 'qimingclaw 执行记录', depends_on: [], preferred_skills: task.assignedSkillId ? [task.assignedSkillId] : [], tasks: [{ task_id: task.id, title: task.title, status: task.status }], }, ], })); } function templatePlans(snapshot: QimingclawSnapshot | null): DebugPlan[] { const customTemplates = snapshot?.planTemplates ?? []; const byId = new Map(); for (const plan of [...customTemplates, ...PLANS]) { if (!byId.has(plan.plan_id)) byId.set(plan.plan_id, plan); } return Array.from(byId.values()); } function mergedDebugPlans(snapshot: QimingclawSnapshot | null): DebugPlan[] { const byId = new Map(); for (const plan of [...runtimeDebugPlans(snapshot), ...templatePlans(snapshot).map((plan) => withRuntimePlan(plan, snapshot))]) { if (!byId.has(plan.plan_id)) byId.set(plan.plan_id, plan); } return Array.from(byId.values()); } function planTemplateJob(plan: DebugPlan, snapshot: QimingclawSnapshot | null): CronJob { const normalizedPlan = withRuntimePlan(plan, snapshot); const schedule = PLAN_SCHEDULES[plan.plan_id] ?? PLAN_SCHEDULES['qimingclaw-client-readiness']!; return { id: `schedule-${plan.plan_id}`, name: plan.title, expression: schedule.expression, command: `digital-employee:${plan.plan_id}`, prompt: plan.objective ?? null, job_type: 'plan_template', schedule: { expression: schedule.expression, template_id: plan.plan_id }, enabled: true, delivery: { mode: 'plan_template', template_id: plan.plan_id }, template_id: plan.plan_id, trigger_kind: 'scheduled', step_count: plan.steps?.length ?? 0, skill_ids: [...new Set((plan.steps ?? []).flatMap((step) => step.preferred_skills ?? []))], delete_after_run: false, created_at: plan.created_at, next_run: schedule.nextRun, last_run: normalizedPlan.status === 'running' ? todayAt(9, 5) : null, last_status: normalizedPlan.status === 'running' ? 'running' : null, last_output: null, }; } function runtimeScheduleJob(schedule: QimingclawScheduleRecord, snapshot: QimingclawSnapshot | null): CronJob { const plan = schedule.planId ? mergedDebugPlans(snapshot).find((item) => item.plan_id === schedule.planId) : null; const runs = snapshot?.runtime?.scheduleRuns ?? []; const latestRun = runs.find((run) => run.scheduleId === schedule.id); return { id: schedule.id, name: schedule.name, expression: schedule.cronExpr, command: schedule.command || `digital-employee:${schedule.planId || schedule.id}`, prompt: schedule.prompt ?? schedule.description ?? null, job_type: 'qimingclaw_schedule', schedule: { mode: 'qimingclaw_schedule', schedule_id: schedule.id, plan_id: schedule.planId, expression: schedule.cronExpr, timezone: schedule.timezone, status: schedule.status, failure_count: schedule.failureCount, last_error: schedule.lastError, }, enabled: schedule.enabled && schedule.status === 'enabled' && !schedule.deletedAt, delivery: { mode: 'qimingclaw_schedule', schedule_id: schedule.id, plan_id: schedule.planId }, template_id: schedule.planId ?? schedule.id, trigger_kind: 'scheduled', step_count: plan?.steps?.length ?? 0, skill_ids: [...new Set((plan?.steps ?? []).flatMap((step) => step.preferred_skills ?? []))], delete_after_run: false, created_at: schedule.createdAt, next_run: schedule.nextRunAt || schedule.updatedAt, last_run: schedule.lastRunAt ?? latestRun?.finishedAt ?? latestRun?.startedAt ?? null, last_status: latestRun?.status ?? (schedule.lastError ? 'failed' : null), last_output: latestRun?.error ?? schedule.lastError ?? null, }; } function buildSchedulerJobs(snapshot: QimingclawSnapshot | null = null): CronJob[] { const realScheduleJobs = (snapshot?.runtime?.schedules ?? []) .filter((schedule) => schedule.status !== 'deleted') .map((schedule) => runtimeScheduleJob(schedule, snapshot)); const templateJobs = templatePlans(snapshot).map((plan) => planTemplateJob(plan, snapshot)); if (realScheduleJobs.length > 0) { const realTemplateIds = new Set(realScheduleJobs.map((job) => job.template_id)); return [ ...realScheduleJobs, ...templateJobs.filter((job) => !realTemplateIds.has(job.template_id)), ]; } if (hasRuntimeRecords(snapshot)) { const runtimeJobs = runtimeDebugPlans(snapshot).map((plan) => ({ id: `runtime-${plan.plan_id}`, name: plan.title, expression: '', command: `qimingclaw-runtime:${plan.plan_id}`, prompt: plan.objective ?? null, job_type: 'qimingclaw_runtime', schedule: { mode: 'runtime_record', plan_id: plan.plan_id }, enabled: true, delivery: { mode: 'qimingclaw_runtime', plan_id: plan.plan_id }, template_id: plan.plan_id, trigger_kind: 'runtime_record', step_count: plan.steps?.length ?? 0, skill_ids: [...new Set((plan.steps ?? []).flatMap((step) => step.preferred_skills ?? []))], delete_after_run: false, created_at: plan.created_at, next_run: plan.updated_at ?? plan.created_at, last_run: plan.updated_at ?? plan.created_at, last_status: plan.status, last_output: null, })); const runtimeIds = new Set(runtimeJobs.map((job) => job.template_id)); return [ ...runtimeJobs, ...templateJobs.filter((job) => !runtimeIds.has(job.template_id)), ]; } return templateJobs; } function buildDuties(state: AdapterState, date: string, snapshot: QimingclawSnapshot | null): DigitalEmployeeDuty[] { const selected = selectedSetForDate(state, date); if (hasRuntimeRecords(snapshot)) { const plans = runtimePlans(snapshot); const tasksWithoutPlan = runtimeTasks(snapshot).filter((task) => !task.planId); const planDuties = plans.map((plan, index) => { const tasks = runtimePlanTasks(snapshot, plan.id); const status = planRecordStatus(plan, tasks); const successCount = tasks.filter((task) => taskStatusTone(task.status) === 'success').length; const failureCount = tasks.filter((task) => taskStatusTone(task.status) === 'danger').length; const selectedByDefault = selected.size === 0 ? index < 3 : selected.has(plan.id); return { id: plan.id, title: plan.title, summary: plan.objective || 'qimingclaw 本地执行记录已同步到数字员工。', implemented: true, selectable: true, selected: selectedByDefault, status, status_label: taskStatusLabel(status), status_tone: taskStatusTone(status), schedule_text: tasks.length > 0 ? `最近更新 ${formatTime(latestRuntimeTime(plan, tasks))}` : '等待任务记录', result_text: tasks.length > 0 ? `${tasks.length} 项任务,${successCount} 项完成,${failureCount} 项异常` : `同步状态:${plan.syncStatus}`, conversation_id: plan.id, }; }); const standaloneDuties = tasksWithoutPlan.map((task, index) => ({ id: task.id, title: task.title, summary: '未绑定 Plan 的 qimingclaw 本地任务记录。', implemented: true, selectable: true, selected: selected.size === 0 ? planDuties.length + index < 3 : selected.has(task.id), status: task.status, status_label: taskStatusLabel(task.status), status_tone: taskStatusTone(task.status), schedule_text: `最近更新 ${formatTime(task.updatedAt)}`, result_text: task.syncError || `同步状态:${task.syncStatus}`, conversation_id: task.id, })); const runtimeDutyIds = new Set([...planDuties, ...standaloneDuties].map((duty) => duty.id)); return [ ...planDuties, ...standaloneDuties, ...templateDuties(state, date, snapshot).filter((duty) => !runtimeDutyIds.has(duty.id)), ]; } return templateDuties(state, date, snapshot); } function templateDuties( state: AdapterState, date: string, snapshot: QimingclawSnapshot | null, ): DigitalEmployeeDuty[] { const selected = selectedSetForDate(state, date); return templatePlans(snapshot).map((rawPlan) => { const plan = withRuntimePlan(rawPlan, snapshot); const schedule = PLAN_SCHEDULES[plan.plan_id] ?? PLAN_SCHEDULES['qimingclaw-client-readiness']!; return { id: plan.plan_id, title: plan.title, summary: plan.objective ?? '等待 qimingclaw 执行链路补充任务说明。', implemented: true, selectable: true, selected: selected.has(plan.plan_id), status: plan.status, status_label: planStatusLabel(plan.status), status_tone: planStatusTone(plan.status), schedule_text: schedule.scheduleText, result_text: planResultText(plan, snapshot), conversation_id: plan.plan_id, }; }); } function planResultText(plan: DebugPlan, snapshot: QimingclawSnapshot | null): string { if (plan.plan_id === 'qimingclaw-client-readiness') { return [ serviceLine(snapshot, 'agent', 'Agent'), serviceLine(snapshot, 'mcp', 'MCP'), `活跃会话:${snapshot?.sessions.active ?? 0}`, ].join(';'); } if (plan.plan_id === 'qimingclaw-remote-task-ingress') { return [ serviceLine(snapshot, 'computerServer', 'Computer Server'), serviceLine(snapshot, 'lanproxy', 'Lanproxy'), serviceLine(snapshot, 'fileServer', 'File Server'), ].join(';'); } return plan.status === 'running' ? '正在同步客户端运行状态' : '等待任务执行'; } function buildTasks(planId?: string | null, snapshot: QimingclawSnapshot | null = null): DebugTask[] { const templateTasks = templatePlans(snapshot) .filter((plan) => !planId || plan.plan_id === planId) .map((plan) => withRuntimePlan(plan, snapshot)) .flatMap(templatePlanTasks); if (hasRuntimeRecords(snapshot)) { const runtimeResult = runtimeTasks(snapshot) .filter((task) => !planId || task.planId === planId || task.id === planId) .map(runtimeTaskToDebugTask); const runtimeTaskIds = new Set(runtimeResult.map((task) => task.task_id)); return [ ...runtimeResult, ...templateTasks.filter((task) => !runtimeTaskIds.has(task.task_id)), ]; } return templateTasks; } function templatePlanTasks(plan: DebugPlan): DebugTask[] { return (plan.steps ?? []).flatMap((step) => (step.tasks ?? []).map((task) => ({ task_id: task.task_id, title: task.title, status: plan.status === 'running' && task.status === 'pending' ? 'running' : task.status, plan_id: plan.plan_id, step_id: step.step_id, assigned_skill_id: step.preferred_skills?.[0] ?? '', description: `${plan.title} · ${step.title}`, created_at: plan.created_at, updated_at: plan.status === 'running' ? new Date().toISOString() : plan.created_at, started_at: task.status === 'running' ? todayAt(9, 5) : null, finished_at: task.status === 'completed' ? todayAt(9, 10) : null, }))); } function buildRuns(tasks: DebugTask[]): DebugRun[] { return tasks .filter((task) => task.status === 'running' || task.status === 'completed') .map((task, index) => ({ run_id: `qimingclaw-run-${index + 1}`, task_id: task.task_id, plan_id: task.plan_id, skill_id: task.assigned_skill_id, attempt_no: 1, lifecycle: task.status === 'running' ? 'Running' : 'Succeeded', created_at: task.started_at ?? task.created_at, completed_at: task.finished_at, events: [ { event_id: `${task.task_id}-event-start`, kind: 'RunStarted', occurred_at: task.started_at ?? task.created_at, message: `${task.title} 已进入 qimingclaw 执行链路`, }, ], })); } function buildTasksResponse(planId?: string | null, snapshot: QimingclawSnapshot | null = null): { tasks: DebugTask[]; runs: DebugRun[]; total: number; truncated: boolean } { const tasks = buildTasks(planId, snapshot); if (hasRuntimeRecords(snapshot)) { const taskIds = new Set(tasks.map((task) => task.task_id)); const runs = runtimeRuns(snapshot) .filter((run) => !run.taskId || taskIds.has(run.taskId)) .map((run) => runtimeRunToDebugRun(run, runtimeEvents(snapshot))); return { tasks, runs, total: tasks.length, truncated: false }; } return { tasks, runs: buildRuns(tasks), total: tasks.length, truncated: false }; } function buildDebugStore(snapshot: QimingclawSnapshot | null): DebugStoreResponse { const plans = filterPlans(null, snapshot); const tasks = buildTasks(null, snapshot); const runs = buildTasksResponse(null, snapshot).runs; const realPlanSteps = runtimePlanSteps(snapshot); return { tables: { canonical_events: buildCanonicalEvents(snapshot, tasks, runs), tasks, runs, plans, plan_steps: realPlanSteps.length > 0 ? realPlanSteps.map((step) => runtimePlanStepToSummary(step, runtimeTasks(snapshot))) : plans.flatMap((plan) => plan.steps ?? []), approvals: buildApprovals(snapshot), artifacts: buildArtifacts(snapshot), }, }; } function buildDashboard(snapshot: QimingclawSnapshot | null): DashboardResponse { const tasks = buildTasks(null, snapshot); const queued = tasks.filter((task) => taskStatusTone(task.status) === 'waiting').length; const running = tasks.filter((task) => taskStatusTone(task.status) === 'running').length; const succeeded = tasks.filter((task) => taskStatusTone(task.status) === 'success').length; const failed = tasks.filter((task) => taskStatusTone(task.status) === 'danger').length; const approvals = buildApprovals(snapshot); return { runtime: { active_tasks: running, failed_tasks: failed, pending_confirmations: approvals.filter((approval) => approval.status === 'pending').length, }, tasks: { total: tasks.length, queued, running, succeeded, failed, }, approvals: { total: approvals.length, pending: approvals.filter((approval) => approval.status === 'pending').length, }, }; } function buildDoctor(snapshot: QimingclawSnapshot | null): { results: DiagResult[]; summary: Record } { const now = new Date().toISOString(); const managedServices = buildManagedServices(snapshot); const tasks = buildTasks(null, snapshot); const approvals = buildApprovals(snapshot); const runtimeErrors = runtimeEvents(snapshot).filter((event) => /fail|error|reject|cancel/i.test(`${event.kind} ${event.message}`)); const results: DiagResult[] = managedServices.map((service) => ({ id: `service:${service.service_id}`, severity: service.requires_human_action || service.status === 'stopped' ? 'error' : service.status === 'degraded' ? 'warn' : 'ok', category: 'service', message: `${service.name}:${service.status_label || service.status}`, details: compactRecord({ error: service.error, kind: service.kind, running: service.running, dependent_tasks: service.dependent_tasks }), source: 'qimingclaw-managed-service', created_at: service.last_seen_at || now, })); const sync = snapshot?.sync; results.push({ id: 'sync:management', severity: doctorSeverityForSync(sync ?? null), category: 'sync', message: syncLine(snapshot), details: sync ? compactRecord({ pending: sync.pending, failed: sync.failed, endpoint: sync.endpoint, missing_credentials: sync.missingCredentials, last_error: sync.lastSyncError }) : null, source: 'qimingclaw-sync-status', created_at: sync?.lastSyncAt ?? now, }); for (const failure of sync?.recentFailures ?? []) { results.push({ id: `sync_failure:${failure.id}`, severity: failure.dueForRetry === false ? 'error' : 'warn', category: 'sync', message: syncFailureLine(failure), details: compactRecord({ entity_type: failure.entityType, entity_id: failure.entityId, operation: failure.operation, attempts: failure.attempts, next_retry_at: failure.nextRetryAt }), source: 'qimingclaw-sync-failure', created_at: failure.lastAttemptAt ?? failure.updatedAt ?? failure.createdAt, }); } const failedTasks = tasks.filter((task) => taskStatusTone(task.status) === 'danger'); results.push({ id: 'runtime:tasks', severity: failedTasks.length > 0 ? 'error' : tasks.length === 0 ? 'warn' : 'ok', category: 'runtime', message: tasks.length === 0 ? '尚未读取到 qimingclaw runtime task。' : `任务 ${tasks.length} 个,异常 ${failedTasks.length} 个。`, details: compactRecord({ task_count: tasks.length, failed_task_count: failedTasks.length }), source: 'qimingclaw-runtime-records', created_at: now, }); results.push({ id: 'runtime:approvals', severity: approvals.some((approval) => approval.status === 'pending') ? 'warn' : 'ok', category: 'approval', message: `审批 ${approvals.length} 个,待处理 ${approvals.filter((approval) => approval.status === 'pending').length} 个。`, details: compactRecord({ approval_count: approvals.length, pending_count: approvals.filter((approval) => approval.status === 'pending').length }), source: 'qimingclaw-approval-records', created_at: now, }); if (runtimeErrors.length > 0) { results.push({ id: 'runtime:events:error', severity: 'error', category: 'runtime', message: `最近运行事件中发现 ${runtimeErrors.length} 条异常事实。`, details: { event_ids: runtimeErrors.slice(0, 8).map((event) => event.id) }, source: 'qimingclaw-runtime-events', created_at: runtimeErrors[0]?.occurredAt ?? now, }); } if (!hasRuntimeRecords(snapshot)) { results.push({ id: 'runtime:data:empty', severity: 'warn', category: 'data', message: '本地数字员工 runtime 记录为空,页面将只显示兼容投影。', source: 'qimingclaw-runtime-records', created_at: now, }); } return { results, summary: { ok: results.filter((result) => result.severity === 'ok').length, warn: results.filter((result) => result.severity === 'warn').length, error: results.filter((result) => result.severity === 'error').length, generated_at: now, source: 'qimingclaw-runtime-projection', }, }; } function doctorSeverityForSync(sync: QimingclawSyncStatus | null): DiagResult['severity'] { if (!sync) return 'warn'; if (sync.failed > 0 || sync.lastSyncError || (sync.missingCredentials?.length ?? 0) > 0) return 'error'; if (!sync.enabled || !sync.endpoint || sync.pending > 0) return 'warn'; return 'ok'; } function buildCostSummary(snapshot: QimingclawSnapshot | null): CostSummary { const requestCount = runtimeEvents(snapshot).length + runtimeRuns(snapshot).length + runtimeTasks(snapshot).length; return { session_cost_usd: 0, daily_cost_usd: 0, monthly_cost_usd: 0, total_tokens: 0, request_count: requestCount, by_model: { 'qimingclaw-local': { model: 'qimingclaw-local', cost_usd: 0, total_tokens: 0, request_count: requestCount, }, }, estimated: true, source: 'qimingclaw-runtime-projection', updated_at: snapshot?.generatedAt ?? new Date().toISOString(), }; } function buildAuditEvents(snapshot: QimingclawSnapshot | null, searchParams: URLSearchParams): Array> { const limit = Math.max(1, Math.min(Math.floor(numberValue(searchParams.get('limit')) ?? 100), 300)); const category = stringValue(searchParams.get('category')) || stringValue(searchParams.get('kind')); const level = stringValue(searchParams.get('level')); const entityId = stringValue(searchParams.get('id')); const events = [ ...runtimeEvents(snapshot).map(auditEventFromRuntimeEvent), ...(snapshot?.sync?.recentFailures ?? []).map((failure) => ({ audit_id: `sync_failure:${failure.id}`, category: 'sync', action: 'sync_failed', level: failure.dueForRetry === false ? 'error' : 'warn', message: syncFailureLine(failure), actor: 'qimingclaw-sync', entity_type: failure.entityType, entity_id: failure.entityId, occurred_at: failure.lastAttemptAt ?? failure.updatedAt ?? failure.createdAt, payload: failure, })), ...buildArtifacts(snapshot).flatMap((artifact) => auditEventsFromArtifact(artifact)), ]; return events .filter((event) => !category || stringValue(event.category) === category || stringValue(event.entity_type) === category) .filter((event) => !level || stringValue(event.level) === level) .filter((event) => !entityId || stringValue(event.entity_id) === entityId || stringValue(event.audit_id).includes(entityId)) .sort((left, right) => stringValue(right.occurred_at).localeCompare(stringValue(left.occurred_at))) .slice(0, limit); } function buildDiagnosticSummary(snapshot: QimingclawSnapshot | null): DigitalEmployeeDiagnosticSummary { const doctorResults = buildDoctor(snapshot).results; const auditEvents = buildAuditEvents(snapshot, new URLSearchParams({ limit: '300' })); const cost = buildCostSummary(snapshot); const latestIssue = latestDiagnosticIssue(doctorResults, auditEvents); return { doctor: { ok: doctorResults.filter((result) => result.severity === 'ok').length, warn: doctorResults.filter((result) => result.severity === 'warn').length, error: doctorResults.filter((result) => result.severity === 'error').length, }, audit: { info: auditEvents.filter((event) => stringValue(event.level) === 'info').length, warn: auditEvents.filter((event) => stringValue(event.level) === 'warn').length, error: auditEvents.filter((event) => stringValue(event.level) === 'error').length, }, cost: { request_count: cost.request_count, estimated: Boolean(cost.estimated), updated_at: cost.updated_at ?? null, }, latest_issue: latestIssue, }; } function latestDiagnosticIssue( doctorResults: DiagResult[], auditEvents: Array>, ): DigitalEmployeeDiagnosticSummary['latest_issue'] { const doctorError = latestDoctorIssue(doctorResults, 'error'); if (doctorError) return doctorError; const auditError = latestAuditIssue(auditEvents, 'error'); if (auditError) return auditError; const doctorWarn = latestDoctorIssue(doctorResults, 'warn'); if (doctorWarn) return doctorWarn; return latestAuditIssue(auditEvents, 'warn'); } function latestDoctorIssue( doctorResults: DiagResult[], level: 'warn' | 'error', ): DigitalEmployeeDiagnosticSummary['latest_issue'] { const result = doctorResults .filter((item) => item.severity === level) .sort((left, right) => stringValue(right.created_at).localeCompare(stringValue(left.created_at)))[0]; if (!result) return null; return { id: stringValue(result.id) || `${result.category}:${level}`, source: 'doctor', level, category: stringValue(result.category) || 'diagnostic', message: result.message, occurred_at: result.created_at ?? null, }; } function latestAuditIssue( auditEvents: Array>, level: 'warn' | 'error', ): DigitalEmployeeDiagnosticSummary['latest_issue'] { const event = auditEvents.find((item) => stringValue(item.level) === level); if (!event) return null; return { id: stringValue(event.audit_id) || `${stringValue(event.category) || 'audit'}:${level}`, source: 'audit', level, category: stringValue(event.category) || stringValue(event.entity_type) || 'audit', message: stringValue(event.message) || stringValue(event.action) || '审计事件需要关注', occurred_at: stringValue(event.occurred_at) || null, }; } function auditRows(snapshot: QimingclawSnapshot | null): BusinessViewRow[] { return buildAuditEvents(snapshot, new URLSearchParams({ limit: '300' })).map((event) => ({ ...event, audit_id: stringValue(event.audit_id), status: stringValue(event.level) || 'info', status_label: auditLevelLabel(stringValue(event.level)), entity_id: stringValue(event.entity_id) || stringValue(event.audit_id), updated_at: stringValue(event.occurred_at), source: BUSINESS_VIEW_SOURCE, })); } function filterAuditRows(rows: BusinessViewRow[], searchParams: URLSearchParams): BusinessViewRow[] { const category = stringValue(searchParams.get('category')) || stringValue(searchParams.get('kind')); const level = stringValue(searchParams.get('level')); const id = stringValue(searchParams.get('id')); return filterBusinessRows(rows, searchParams) .filter((row) => !category || stringValue(row.category) === category || stringValue(row.entity_type) === category) .filter((row) => !level || stringValue(row.level) === level) .filter((row) => !id || stringValue(row.entity_id) === id || stringValue(row.audit_id).includes(id)); } function auditLevelLabel(level: string): string { if (level === 'error') return '异常'; if (level === 'warn') return '警告'; return '信息'; } function buildBusinessViewResponse(kind: BusinessViewKind, snapshot: QimingclawSnapshot | null, searchParams: URLSearchParams): Record { const rowsByKind: Record = { plans: businessPlanRows(snapshot), 'plan-steps': planStepRows(snapshot), 'plan-step-graph': planStepGraphRows(snapshot), tasks: businessTaskRows(snapshot), runs: businessRunRows(snapshot), events: businessEventRows(snapshot), artifacts: businessArtifactRows(snapshot), approvals: businessApprovalRows(snapshot), 'approval-history': approvalHistoryRows(snapshot), 'route-decisions': routeDecisionRows(snapshot), notifications: notificationRows(snapshot, notificationAdapterState(snapshot)), audits: auditRows(snapshot), }; const rows = kind === 'route-decisions' ? filterRouteDecisionRows(rowsByKind[kind], searchParams) : kind === 'plan-steps' || kind === 'plan-step-graph' ? filterPlanStepRows(rowsByKind[kind], searchParams) : kind === 'approval-history' ? filterApprovalHistoryRows(rowsByKind[kind], searchParams) : kind === 'notifications' ? filterNotificationRows(rowsByKind[kind], searchParams) : kind === 'audits' ? filterAuditRows(rowsByKind[kind], searchParams) : filterBusinessRows(rowsByKind[kind], searchParams); return { ...paginateBusinessRows(rows, searchParams), source: BUSINESS_VIEW_SOURCE, }; } function planStepRows(snapshot: QimingclawSnapshot | null): BusinessViewRow[] { return runtimePlanSteps(snapshot) .map((step) => { const payload = asRecord(step.payload) ?? {}; const dependencyGraph = asRecord(payload.dependencyGraph ?? payload.dependency_graph) ?? {}; const lastDispatch = asRecord(payload.lastDispatch ?? payload.last_dispatch) ?? {}; const lease = activePlanStepDispatchLease(payload); const readiness = planStepDispatchReadiness(step, snapshot, payload); const blockedDependencies = uniqueStrings(arrayValue(dependencyGraph.blockedDependencies ?? dependencyGraph.blocked_dependencies)); const waitingDependencies = uniqueStrings(arrayValue(dependencyGraph.waitingDependencies ?? dependencyGraph.waiting_dependencies)); const satisfiedDependencies = uniqueStrings(arrayValue(dependencyGraph.satisfiedDependencies ?? dependencyGraph.satisfied_dependencies)); return { step_id: step.id, node_id: step.id, remote_id: step.remoteId ?? null, plan_id: step.planId, title: step.title, status: step.status, seq: step.seq, depends_on: step.dependsOn, dependency_count: step.dependsOn.length, dependency_status: stringValue(dependencyGraph.status) || stepDependencyStatus(step, blockedDependencies, waitingDependencies), blocked_dependencies: blockedDependencies, blocked_dependency_count: blockedDependencies.length, blocked_dependency_details: planStepDependencyDetails(blockedDependencies, snapshot), waiting_dependencies: waitingDependencies, waiting_dependency_count: waitingDependencies.length, satisfied_dependencies: satisfiedDependencies, preferred_skill_ids: step.preferredSkillIds, task_id: readiness.taskId, latest_run_id: readiness.latestRunId, dispatchable: readiness.dispatchable, skip_reason: readiness.skipReason, last_dispatch_status: stringValue(lastDispatch.status) || null, last_dispatch_at: stringValue(lastDispatch.occurredAt ?? lastDispatch.occurred_at) || null, lease, lease_id: stringValue(lease?.lease_id ?? lease?.leaseId) || null, lease_state: stringValue(lease?.state) || null, lease_owner_device_id: stringValue(lease?.owner_device_id ?? lease?.ownerDeviceId) || null, lease_acquired_at: stringValue(lease?.acquired_at ?? lease?.acquiredAt) || null, lease_expires_at: stringValue(lease?.expires_at ?? lease?.expiresAt) || null, lease_released_at: stringValue(lease?.released_at ?? lease?.releasedAt) || null, lease_release_reason: stringValue(lease?.release_reason ?? lease?.releaseReason) || null, lease_active: Boolean(lease), sync_status: step.syncStatus, last_synced_at: step.lastSyncedAt ?? null, sync_error: step.syncError ?? null, created_at: step.createdAt, updated_at: step.updatedAt, entity_type: 'plan_step', entity_id: step.id, source: BUSINESS_VIEW_SOURCE, }; }) .sort((left, right) => stringValue(left.plan_id).localeCompare(stringValue(right.plan_id)) || Number(left.seq ?? 0) - Number(right.seq ?? 0) || stringValue(left.step_id).localeCompare(stringValue(right.step_id))); } function planStepGraphRows(snapshot: QimingclawSnapshot | null): BusinessViewRow[] { return planStepRows(snapshot).map((row) => ({ node_id: stringValue(row.node_id) || stringValue(row.step_id), step_id: stringValue(row.step_id), plan_id: stringValue(row.plan_id) || null, title: stringValue(row.title), status: stringValue(row.status), seq: row.seq, depends_on: arrayValue(row.depends_on), dependency_status: stringValue(row.dependency_status) || null, blocked_dependencies: arrayValue(row.blocked_dependencies), blocked_dependency_details: arrayValue(row.blocked_dependency_details), waiting_dependencies: arrayValue(row.waiting_dependencies), satisfied_dependencies: arrayValue(row.satisfied_dependencies), task_id: stringValue(row.task_id) || null, latest_run_id: stringValue(row.latest_run_id) || null, dispatchable: row.dispatchable === true, skip_reason: stringValue(row.skip_reason) || null, lease: asRecord(row.lease) ?? null, lease_id: stringValue(row.lease_id) || null, lease_state: stringValue(row.lease_state) || null, lease_owner_device_id: stringValue(row.lease_owner_device_id) || null, lease_acquired_at: stringValue(row.lease_acquired_at) || null, lease_expires_at: stringValue(row.lease_expires_at) || null, lease_released_at: stringValue(row.lease_released_at) || null, lease_release_reason: stringValue(row.lease_release_reason) || null, lease_active: row.lease_active === true, sync_status: stringValue(row.sync_status) || null, updated_at: stringValue(row.updated_at) || null, entity_type: 'plan_step_graph_node', entity_id: stringValue(row.step_id), source: BUSINESS_VIEW_SOURCE, })); } function filterPlanStepRows(rows: BusinessViewRow[], searchParams: URLSearchParams): BusinessViewRow[] { const planId = stringValue(searchParams.get('plan_id')); const stepId = stringValue(searchParams.get('step_id')) || stringValue(searchParams.get('id')); const status = stringValue(searchParams.get('status')); const syncStatus = stringValue(searchParams.get('sync_status')); return filterBusinessRows(rows, searchParams) .filter((row) => !planId || stringValue(row.plan_id) === planId) .filter((row) => !stepId || stringValue(row.step_id) === stepId || stringValue(row.node_id) === stepId || stringValue(row.entity_id) === stepId) .filter((row) => !status || stringValue(row.status) === status) .filter((row) => !syncStatus || stringValue(row.sync_status) === syncStatus); } function stepDependencyStatus(step: QimingclawPlanStepRecord, blockedDependencies: string[], waitingDependencies: string[]): string { if (blockedDependencies.length > 0) return 'blocked'; if (waitingDependencies.length > 0) return 'waiting'; if (step.dependsOn.length > 0) return 'ready'; return 'independent'; } function planStepDispatchReadiness( step: QimingclawPlanStepRecord, snapshot: QimingclawSnapshot | null, payload: Record, ): { taskId: string | null; latestRunId: string | null; dispatchable: boolean; skipReason: string | null } { const tasks = runtimePlanTasks(snapshot, step.planId); const task = tasks.find((candidate) => taskStepId(candidate) === step.id) ?? tasks.find((candidate) => !isTerminalRuntimeTaskStatus(candidate.status)) ?? null; const taskId = task?.id ?? null; const latestRunId = task ? latestRunForTask(runtimeRuns(snapshot), task.id)?.id ?? null : null; const lease = activePlanStepDispatchLease(payload); let skipReason: string | null = null; if (lease) skipReason = 'lease_active'; else if (step.status.toLowerCase() !== 'ready') skipReason = 'not_ready'; else if (!task) skipReason = 'missing_task'; else if (isTerminalRuntimeTaskStatus(task.status)) skipReason = 'task_terminal'; else if (runtimeRuns(snapshot).some((run) => run.taskId === task.id && isActiveRuntimeRunStatus(run.status))) skipReason = 'task_already_running'; else if (runtimeApprovals(snapshot).some((approval) => isOpenRuntimeApprovalStatus(approval.status) && (approval.planId === step.planId || approval.taskId === task.id || stringValue(asRecord(approval.payload)?.stepId ?? asRecord(approval.payload)?.step_id) === step.id))) skipReason = 'approval_pending'; else if (payload.auto_dispatch === false || payload.autoDispatch === false) skipReason = 'auto_dispatch_disabled'; return { taskId, latestRunId, dispatchable: !skipReason, skipReason }; } function planStepDependencyDetails(dependencyIds: string[], snapshot: QimingclawSnapshot | null): BusinessViewRow[] { const stepsById = new Map(runtimePlanSteps(snapshot).map((step) => [step.id, step])); return dependencyIds.map((dependencyId) => { const dependency = stepsById.get(dependencyId); if (!dependency) { return { step_id: dependencyId, task_id: null, latest_run_id: null, title: dependencyId, status: 'missing', dependency_status: 'missing', dispatchable: false, skip_reason: 'missing_step', }; } const payload = asRecord(dependency.payload) ?? {}; const graph = asRecord(payload.dependencyGraph ?? payload.dependency_graph) ?? {}; const lease = activePlanStepDispatchLease(payload); const readiness = planStepDispatchReadiness(dependency, snapshot, payload); return { step_id: dependency.id, task_id: readiness.taskId, latest_run_id: readiness.latestRunId, title: dependency.title, status: dependency.status, dependency_status: stringValue(graph.status) || dependency.status, dispatchable: readiness.dispatchable, skip_reason: readiness.skipReason, lease_state: stringValue(lease?.state) || null, lease_active: Boolean(lease), }; }); } function activePlanStepDispatchLease(payload: Record): Record | null { const lease = asRecord(payload.dispatchLease ?? payload.dispatch_lease); if (!lease) return null; const state = stringValue(lease.state) || 'active'; const expiresAt = stringValue(lease.expires_at ?? lease.expiresAt); const leaseId = stringValue(lease.lease_id ?? lease.leaseId); const ownerDeviceId = stringValue(lease.owner_device_id ?? lease.ownerDeviceId); if (state !== 'active' || !expiresAt || expiresAt <= new Date().toISOString() || !leaseId || !ownerDeviceId) return null; return lease; } function isTerminalRuntimeTaskStatus(status: string): boolean { return ['completed', 'succeeded', 'success', 'done', 'skipped', 'cancelled', 'canceled'].includes(status.toLowerCase()); } function isActiveRuntimeRunStatus(status: string): boolean { return ['running', 'pending', 'queued', 'started'].includes(status.toLowerCase()); } function isOpenRuntimeApprovalStatus(status: string): boolean { return ['pending', 'reviewing', 'open', 'waiting'].includes(status.toLowerCase() || 'pending'); } function routeDecisionRows(snapshot: QimingclawSnapshot | null): BusinessViewRow[] { return runtimeEvents(snapshot) .filter((event) => event.kind === 'route_decision') .map((event) => { const payload = asRecord(event.payload) ?? {}; const decision = routeDecisionFromRuntimePayload(payload, event); const attentionLevel = routeDecisionAttentionLevel(decision); return { ...decision, route_decision_id: decision.id, event_id: event.id, status: decision.policy_result || 'accepted', status_label: routeDecisionStatusLabel(decision), attention_level: attentionLevel, entity_type: 'route_decision', entity_id: decision.id, occurred_at: event.occurredAt, created_at: event.createdAt, updated_at: decision.recorded_at || event.occurredAt, payload, }; }) .sort((left, right) => stringValue(right.recorded_at).localeCompare(stringValue(left.recorded_at)) || stringValue(right.event_id).localeCompare(stringValue(left.event_id))); } function filterRouteDecisionRows(rows: BusinessViewRow[], searchParams: URLSearchParams): BusinessViewRow[] { const routeKind = stringValue(searchParams.get('route_kind')); const intentKind = stringValue(searchParams.get('intent_kind')); const policyResult = stringValue(searchParams.get('policy_result')); return filterBusinessRows(rows, searchParams) .filter((row) => !routeKind || stringValue(row.route_kind) === routeKind) .filter((row) => !intentKind || stringValue(row.intent_kind) === intentKind) .filter((row) => !policyResult || stringValue(row.policy_result) === policyResult); } function notificationRows(snapshot: QimingclawSnapshot | null, state: AdapterState): BusinessViewRow[] { const preferences = notificationPreferences(state); return buildNotificationCandidates(snapshot, state) .map((notification) => ({ notification_id: notification.notification_id, kind: notification.kind, level: notification.level, status: notification.status, title: notification.title, body: notification.body, plan_id: notification.plan_id, task_id: notification.task_id, correlation_id: notification.correlation_id, created_at: notification.created_at, updated_at: notification.replied_at || notification.dismissed_at || notification.read_at || notification.created_at, read_at: notification.read_at ?? null, dismissed_at: notification.dismissed_at ?? null, replied_at: notification.replied_at ?? null, muted_by_preferences: !notificationMatchesPreferences(notification, preferences), entity_type: 'notification', entity_id: notification.notification_id, source: BUSINESS_VIEW_SOURCE, })) .sort((left, right) => stringValue(right.created_at).localeCompare(stringValue(left.created_at)) || stringValue(right.notification_id).localeCompare(stringValue(left.notification_id))); } function filterNotificationRows(rows: BusinessViewRow[], searchParams: URLSearchParams): BusinessViewRow[] { const id = stringValue(searchParams.get('id')); const kind = stringValue(searchParams.get('kind')); const level = stringValue(searchParams.get('level')); const muted = stringValue(searchParams.get('muted')).toLowerCase(); return filterBusinessRows(rows, searchParams) .filter((row) => !id || stringValue(row.notification_id) === id || stringValue(row.correlation_id) === id || stringValue(row.entity_id) === id) .filter((row) => !kind || stringValue(row.kind) === kind) .filter((row) => !level || stringValue(row.level) === level) .filter((row) => { if (!muted) return true; const mutedValue = row.muted_by_preferences === true; if (['1', 'true', 'yes'].includes(muted)) return mutedValue; if (['0', 'false', 'no'].includes(muted)) return !mutedValue; return true; }); } function routeDecisionFromRuntimePayload(payload: Record, event: QimingclawEventRecord): RouteDecisionRecord { const decision = asRecord(payload.routeDecision ?? payload.route_decision ?? payload) ?? {}; return { id: stringValue(decision.id) || event.id, route_kind: stringValue(decision.route_kind ?? decision.routeKind) || 'ChatOnly', intent_kind: stringValue(decision.intent_kind ?? decision.intentKind) || 'chat', reason_codes: uniqueStrings(arrayValue(decision.reason_codes ?? decision.reasonCodes)), candidate_skills: uniqueStrings(arrayValue(decision.candidate_skills ?? decision.candidateSkills)), context_refs: asRecord(decision.context_refs ?? decision.contextRefs) ?? {}, created_command_id: stringValue(decision.created_command_id ?? decision.createdCommandId) || null, correlation_id: stringValue(decision.correlation_id ?? decision.correlationId) || event.id, creates_plan: Boolean(decision.creates_plan ?? decision.createsPlan), creates_task_run: Boolean(decision.creates_task_run ?? decision.createsTaskRun), approval_mode: stringValue(decision.approval_mode ?? decision.approvalMode) || null, policy_result: stringValue(decision.policy_result ?? decision.policyResult) || 'accepted', recorded_at: stringValue(decision.recorded_at ?? decision.recordedAt) || event.occurredAt, entrypoint: stringValue(decision.entrypoint) || null, actor: stringValue(decision.actor) || null, }; } function routeDecisionAttentionLevel(decision: RouteDecisionRecord): 'info' | 'action' | 'warn' | 'error' { if (decision.policy_result && decision.policy_result !== 'accepted') return 'error'; if (decision.reason_codes.includes('candidate_skills_disabled') || decision.reason_codes.includes('route_action_missing_context')) return 'warn'; if (decision.created_command_id) return 'action'; return 'info'; } function routeDecisionStatusLabel(decision: RouteDecisionRecord): string { const attentionLevel = routeDecisionAttentionLevel(decision); if (attentionLevel === 'error') return '策略拒绝'; if (attentionLevel === 'warn') return '需要关注'; if (decision.created_command_id) return '已映射命令'; return '已记录'; } function buildRouteDecisionSummary(snapshot: QimingclawSnapshot | null): DigitalEmployeeRouteSummary { const rows = routeDecisionRows(snapshot); const latest = rows[0] ?? null; return { total: rows.length, mapped_count: rows.filter((row) => stringValue(row.created_command_id)).length, attention_count: rows.filter((row) => ['warn', 'error'].includes(stringValue(row.attention_level))).length, latest: latest ? { id: stringValue(latest.route_decision_id), route_kind: stringValue(latest.route_kind), intent_kind: stringValue(latest.intent_kind), status_label: stringValue(latest.status_label), attention_level: stringValue(latest.attention_level), created_command_id: stringValue(latest.created_command_id) || null, recorded_at: stringValue(latest.recorded_at), } : null, }; } function businessPlanRows(snapshot: QimingclawSnapshot | null): BusinessViewRow[] { const runtimeById = new Map(runtimePlans(snapshot).map((plan) => [plan.id, plan])); const tasks = buildTasks(null, snapshot); const runs = buildTasksResponse(null, snapshot).runs; const events = buildCanonicalEvents(snapshot, tasks, runs); return filterPlans(null, snapshot).map((plan) => { const runtime = runtimeById.get(plan.plan_id); const relatedTasks = tasks.filter((task) => task.plan_id === plan.plan_id); const relatedRuns = runs.filter((run) => run.plan_id === plan.plan_id); const latestEventAt = events .filter((event) => event.plan_id === plan.plan_id) .map((event) => stringValue(event.occurred_at ?? event.timestamp)) .filter(Boolean) .sort() .pop() || null; return { plan_id: plan.plan_id, remote_id: runtime?.remoteId ?? null, title: plan.title, objective: plan.objective ?? null, status: plan.status, task_count: relatedTasks.length, run_count: relatedRuns.length, latest_event_at: latestEventAt, sync_status: (runtime?.syncStatus ?? stringValue(plan.sync_status)) || null, created_at: plan.created_at, updated_at: stringValue(plan.updated_at) || runtime?.updatedAt || plan.created_at, payload: runtime?.payload ?? compactRecord({ source: plan.source, steps: plan.steps }), }; }); } function businessTaskRows(snapshot: QimingclawSnapshot | null): BusinessViewRow[] { const runtimeById = new Map(runtimeTasks(snapshot).map((task) => [task.id, task])); return buildTasks(null, snapshot).map((task) => { const runtime = runtimeById.get(task.task_id); return { task_id: task.task_id, remote_id: runtime?.remoteId ?? null, plan_id: task.plan_id ?? null, title: task.title, status: task.status, assigned_skill_id: task.assigned_skill_id || runtime?.assignedSkillId || null, sync_status: (runtime?.syncStatus ?? stringValue(task.sync_status)) || null, created_at: task.created_at, updated_at: task.updated_at, payload: runtime?.payload ?? compactRecord({ step_id: task.step_id, description: task.description }), }; }); } function businessRunRows(snapshot: QimingclawSnapshot | null): BusinessViewRow[] { const runtimeById = new Map(runtimeRuns(snapshot).map((run) => [run.id, run])); return buildTasksResponse(null, snapshot).runs.map((run) => { const runtime = runtimeById.get(run.run_id); const startedAt = runtime?.startedAt ?? run.created_at; const finishedAt = runtime?.finishedAt ?? run.completed_at ?? null; const latestRunEvent = (run.events ?? []) .slice() .sort((left, right) => stringValue(right.occurred_at).localeCompare(stringValue(left.occurred_at)))[0]; return { run_id: run.run_id, remote_id: runtime?.remoteId ?? null, plan_id: run.plan_id ?? null, task_id: run.task_id || null, status: runtime?.status ?? run.lifecycle, started_at: startedAt ?? null, finished_at: finishedAt, duration_ms: durationMs(startedAt, finishedAt), last_event_message: latestRunEvent?.message ?? null, sync_status: (runtime?.syncStatus ?? stringValue(run.sync_status)) || null, created_at: runtime?.createdAt ?? run.created_at, payload: runtime?.payload ?? compactRecord({ lifecycle: run.lifecycle, attempt_no: run.attempt_no }), }; }); } function businessEventRows(snapshot: QimingclawSnapshot | null): BusinessViewRow[] { const tasks = buildTasks(null, snapshot); const runs = buildTasksResponse(null, snapshot).runs; const runtimeById = new Map(runtimeEvents(snapshot).map((event) => [event.id, event])); return buildCanonicalEvents(snapshot, tasks, runs).map((event) => { const runtime = runtimeById.get(event.event_id); const planId = stringValue(event.plan_id) || runtime?.planId || null; const taskId = stringValue(event.task_id) || runtime?.taskId || null; return { event_id: event.event_id, remote_id: runtime?.remoteId ?? null, plan_id: planId, task_id: taskId, run_id: runtime?.runId ?? null, kind: event.kind ?? event.event_type ?? runtime?.kind ?? null, message: event.message ?? runtime?.message ?? null, occurred_at: stringValue(event.occurred_at ?? event.timestamp) || runtime?.occurredAt, sync_status: runtime?.syncStatus ?? null, payload: runtime?.payload ?? compactRecord({ aggregate_type: event.aggregate_type, aggregate_id: event.aggregate_id }), }; }); } function businessArtifactRows(snapshot: QimingclawSnapshot | null): BusinessViewRow[] { const runtimeById = new Map(runtimeArtifacts(snapshot).map((artifact) => [artifact.id, artifact])); const artifacts = buildArtifacts(snapshot); return artifacts.map((artifact) => { const artifactId = stringValue(artifact.artifact_id) || stringValue(artifact.subject_id); const runtime = runtimeById.get(artifactId); const entity = businessEntityFromRefs({ plan_id: artifact.plan_id, task_id: artifact.task_id, run_id: artifact.run_id, fallback_type: 'artifact', fallback_id: artifact.subject_id ?? artifactId, }); const lifecycle = artifactLifecycleSummary(snapshot, artifactId); const version = artifactVersionMeta(artifacts, artifact); const delivery = artifactDeliverySummary(artifact); return { ...artifact, artifact_id: artifactId, remote_id: (runtime?.remoteId ?? stringValue(artifact.remote_id)) || null, entity_type: entity.entity_type, entity_id: entity.entity_id, sync_status: (runtime?.syncStatus ?? stringValue(artifact.sync_status)) || null, version: version.version, version_key: version.version_key, version_rank: version.version_rank, retention_status: lifecycle.retention_status, retention_until: lifecycle.retention_until, last_retention_action: lifecycle.last_retention_action, last_access_status: stringValue(asRecord(artifact.access)?.last_access_status) || null, delivery_summary: delivery, }; }); } function artifactDetailRow(snapshot: QimingclawSnapshot | null, artifactId: string): BusinessViewRow | null { const row = businessArtifactRows(snapshot) .find((artifact) => stringValue(artifact.artifact_id) === artifactId || stringValue(artifact.remote_id) === artifactId) ?? null; if (!row) return null; return { ...row, versions: artifactVersionRows(snapshot, stringValue(row.artifact_id)), lifecycle_events: artifactLifecycleRows(snapshot).filter((event) => stringValue(event.artifact_id) === stringValue(row.artifact_id)).slice(0, 20), }; } function artifactVersionRows(snapshot: QimingclawSnapshot | null, artifactId: string): BusinessViewRow[] { const artifacts = buildArtifacts(snapshot); const target = artifacts.find((artifact) => stringValue(artifact.artifact_id) === artifactId || stringValue(artifact.subject_id) === artifactId); if (!target) return []; const groupKey = artifactVersionGroupKey(target); return artifacts .filter((artifact) => artifactVersionGroupKey(artifact) === groupKey) .sort((left, right) => stringValue(left.created_at).localeCompare(stringValue(right.created_at))) .map((artifact, index) => { const meta = artifactVersionMeta(artifacts, artifact); return { artifact_id: stringValue(artifact.artifact_id) || stringValue(artifact.subject_id), name: stringValue(artifact.name) || null, version: meta.version || `local-v${index + 1}`, version_key: meta.version_key, version_rank: index + 1, delivery_summary: artifactDeliverySummary(artifact), retention_status: artifactLifecycleSummary(snapshot, stringValue(artifact.artifact_id)).retention_status, created_at: stringValue(artifact.created_at) || null, source_label: stringValue(artifact.source_label) || null, }; }); } function businessApprovalRows(snapshot: QimingclawSnapshot | null): BusinessViewRow[] { const runtimeById = new Map(runtimeApprovals(snapshot).map((approval) => [approval.id, approval])); return buildApprovals(snapshot).map((approval) => { const approvalId = stringValue(approval.approval_id); const runtime = runtimeById.get(approvalId); const summary = approvalActionSummary(snapshot, approvalId, approval); const entity = businessEntityFromRefs({ plan_id: approval.plan_id, task_id: approval.task_id, run_id: approval.run_id, fallback_type: stringValue(approval.source) === 'qimingclaw-sync' ? 'sync_outbox' : 'approval', fallback_id: approval.task_id || approvalId, }); return { ...approval, approval_id: approvalId, remote_id: (runtime?.remoteId ?? stringValue(approval.remote_id)) || null, entity_type: entity.entity_type, entity_id: entity.entity_id, sync_status: (runtime?.syncStatus ?? stringValue(approval.sync_status)) || null, risk_level: summary.risk_level, sla_status: summary.sla_status, due_at: summary.due_at, last_action: summary.last_action, last_actor: summary.last_actor, comment_count: summary.comment_count, delegate_to: summary.delegate_to, }; }); } function approvalDetailRow(snapshot: QimingclawSnapshot | null, approvalId: string): BusinessViewRow | null { const row = businessApprovalRows(snapshot) .find((approval) => stringValue(approval.approval_id) === approvalId || stringValue(approval.remote_id) === approvalId) ?? null; if (!row) return null; return { ...row, timeline: buildApprovalTimeline(stringValue(row.approval_id), snapshot).events, }; } function approvalActionRows(snapshot: QimingclawSnapshot | null): BusinessViewRow[] { return runtimeEvents(snapshot) .filter((event) => ['approval_action_recorded', 'approval_action_rejected'].includes(event.kind)) .map((event) => { const payload = asRecord(event.payload) ?? {}; const delegate = asRecord(payload.delegate_summary); const comment = asRecord(payload.comment_summary); const sla = asRecord(payload.sla_summary); const risk = asRecord(payload.risk_summary); return { action_id: event.id, event_id: event.id, kind: event.kind, approval_id: stringValue(payload.approval_id) || null, action: stringValue(payload.action) || null, status: stringValue(payload.status) || null, comment_preview: stringValue(comment?.preview) || null, comment_length: numberValue(comment?.length), delegate_to: stringValue(delegate?.label ?? delegate?.target) || null, sla_status: stringValue(sla?.sla_status) || null, due_at: stringValue(sla?.due_at) || null, risk_level: stringValue(risk?.risk_level) || null, actor: stringValue(payload.actor) || null, source: stringValue(payload.source) || null, plan_id: (event.planId ?? stringValue(payload.plan_id)) || null, task_id: (event.taskId ?? stringValue(payload.task_id)) || null, run_id: (event.runId ?? stringValue(payload.run_id)) || null, message: event.message, occurred_at: event.occurredAt, sync_status: event.syncStatus ?? null, payload, }; }) .sort((left, right) => stringValue(right.occurred_at).localeCompare(stringValue(left.occurred_at))); } function approvalHistoryRows(snapshot: QimingclawSnapshot | null): BusinessViewRow[] { const approvalRows = new Map(businessApprovalRows(snapshot).map((approval) => [stringValue(approval.approval_id), approval])); const actionRows = approvalActionRows(snapshot).map((action) => { const approvalId = stringValue(action.approval_id); const approval = approvalRows.get(approvalId); return { history_id: stringValue(action.action_id), event_id: stringValue(action.event_id), approval_id: approvalId || null, action: stringValue(action.action) || null, status: stringValue(action.status) || null, actor: stringValue(action.actor) || null, risk_level: stringValue(action.risk_level) || null, due_at: stringValue(action.due_at) || null, delegate_to: stringValue(action.delegate_to) || null, comment_length: numberValue(action.comment_length), message: stringValue(action.message) || null, plan_id: stringValue(action.plan_id) || stringValue(approval?.plan_id) || null, task_id: stringValue(action.task_id) || stringValue(approval?.task_id) || null, run_id: stringValue(action.run_id) || stringValue(approval?.run_id) || null, occurred_at: stringValue(action.occurred_at) || null, updated_at: stringValue(action.occurred_at) || null, sync_status: stringValue(action.sync_status) || null, entity_type: 'approval_history', entity_id: stringValue(action.action_id), source: BUSINESS_VIEW_SOURCE, }; }); const decisionRows = runtimeEvents(snapshot) .filter((event) => event.kind === 'approval_decision') .map((event) => { const payload = asRecord(event.payload) ?? {}; const approvalId = stringValue(payload.approvalId ?? payload.approval_id) || event.id.split(':decision:')[0] || ''; const approval = approvalRows.get(approvalId); return { history_id: event.id, event_id: event.id, approval_id: approvalId || null, action: 'decision', status: stringValue(payload.nextStatus ?? payload.next_status ?? payload.decision) || null, actor: stringValue(payload.actor) || stringValue(payload.source) || 'qimingclaw-approval-decision', risk_level: stringValue(approval?.risk_level) || null, due_at: stringValue(approval?.due_at) || null, delegate_to: stringValue(approval?.delegate_to) || null, comment_length: null, message: event.message || null, plan_id: event.planId || stringValue(payload.planId ?? payload.plan_id) || stringValue(approval?.plan_id) || null, task_id: event.taskId || stringValue(payload.taskId ?? payload.task_id) || stringValue(approval?.task_id) || null, run_id: event.runId || stringValue(payload.runId ?? payload.run_id) || stringValue(approval?.run_id) || null, occurred_at: event.occurredAt, updated_at: event.occurredAt, sync_status: event.syncStatus ?? null, entity_type: 'approval_history', entity_id: event.id, source: BUSINESS_VIEW_SOURCE, }; }); return [...actionRows, ...decisionRows] .sort((left, right) => stringValue(right.occurred_at).localeCompare(stringValue(left.occurred_at)) || stringValue(right.history_id).localeCompare(stringValue(left.history_id))); } function filterApprovalHistoryRows(rows: BusinessViewRow[], searchParams: URLSearchParams): BusinessViewRow[] { const approvalId = stringValue(searchParams.get('approval_id')) || stringValue(searchParams.get('id')); const action = stringValue(searchParams.get('action')); const riskLevel = stringValue(searchParams.get('risk_level')); return filterBusinessRows(rows, searchParams) .filter((row) => !approvalId || stringValue(row.approval_id) === approvalId || stringValue(row.history_id) === approvalId || stringValue(row.event_id) === approvalId) .filter((row) => !action || stringValue(row.action) === action) .filter((row) => !riskLevel || stringValue(row.risk_level) === riskLevel); } function approvalActionSummary( snapshot: QimingclawSnapshot | null, approvalId: string, approval: Record, ): Record { const actions = approvalActionRows(snapshot).filter((event) => stringValue(event.approval_id) === approvalId); const latest = actions[0]; const latestRisk = actions.find((event) => stringValue(event.risk_level)); const latestSla = actions.find((event) => { const status = stringValue(event.sla_status); return Boolean(status && status !== 'untracked'); }); const latestDelegate = actions.find((event) => stringValue(event.delegate_to)); const payload = asRecord(approval.payload); const dueAt = stringValue(latestSla?.due_at) || stringValue(payload?.due_at ?? payload?.dueAt ?? payload?.timeout_at ?? payload?.timeoutAt); const slaStatus = stringValue(latestSla?.sla_status) || approvalSlaStatus(dueAt); return { risk_level: stringValue(latestRisk?.risk_level) || 'normal', sla_status: slaStatus, due_at: dueAt || null, last_action: stringValue(latest?.action) || null, last_actor: stringValue(latest?.actor) || null, comment_count: actions.filter((event) => stringValue(event.action) === 'comment').length, delegate_to: stringValue(latestDelegate?.delegate_to) || null, }; } function approvalSlaStatus(dueAt: string): string { if (!dueAt) return 'untracked'; const dueMs = Date.parse(dueAt); if (!Number.isFinite(dueMs)) return 'untracked'; return dueMs < Date.now() ? 'timeout' : 'tracked'; } function skillCallAuditRows(snapshot: QimingclawSnapshot | null): BusinessViewRow[] { return runtimeEvents(snapshot) .filter((event) => ['skill_call_allowed', 'skill_call_rejected', 'skill_call_observed'].includes(event.kind)) .map((event) => { const payload = asRecord(event.payload) ?? {}; return { audit_id: event.id, event_id: event.id, remote_id: event.remoteId ?? null, decision: stringValue(payload.decision) || event.kind.replace('skill_call_', ''), source: stringValue(payload.source) || null, server_id: stringValue(payload.server_id) || null, tool_name: stringValue(payload.tool_name) || null, skill_id: stringValue(payload.skill_id) || null, call_id: stringValue(payload.call_id) || null, session_id: stringValue(payload.session_id) || null, task_id: (event.taskId ?? stringValue(payload.task_id)) || null, run_id: (event.runId ?? stringValue(payload.run_id)) || null, reason_codes: Array.isArray(payload.reason_codes) ? payload.reason_codes : [], message: event.message, occurred_at: event.occurredAt, sync_status: event.syncStatus ?? null, payload, }; }) .sort((left, right) => stringValue(right.occurred_at).localeCompare(stringValue(left.occurred_at))); } function filterSkillCallAuditRows(rows: BusinessViewRow[], searchParams: URLSearchParams): BusinessViewRow[] { const skillId = stringValue(searchParams.get('skill_id')); const serverId = stringValue(searchParams.get('server_id')); const toolName = stringValue(searchParams.get('tool_name')); const decision = stringValue(searchParams.get('decision')); return rows .filter((row) => !skillId || stringValue(row.skill_id) === skillId) .filter((row) => !serverId || stringValue(row.server_id) === serverId) .filter((row) => !toolName || stringValue(row.tool_name) === toolName) .filter((row) => !decision || stringValue(row.decision) === decision); } function artifactAccessRows(snapshot: QimingclawSnapshot | null): BusinessViewRow[] { return runtimeEvents(snapshot) .filter((event) => ['artifact_access_allowed', 'artifact_access_rejected'].includes(event.kind)) .map((event) => { const payload = asRecord(event.payload) ?? {}; return { access_id: event.id, event_id: event.id, remote_id: event.remoteId ?? null, artifact_id: stringValue(payload.artifact_id) || null, action: stringValue(payload.action) || null, status: stringValue(payload.status) || event.kind.replace('artifact_access_', ''), reason_codes: Array.isArray(payload.reason_codes) ? payload.reason_codes : [], actor: stringValue(payload.actor) || null, source: stringValue(payload.source) || null, plan_id: (event.planId ?? stringValue(payload.plan_id)) || null, task_id: (event.taskId ?? stringValue(payload.task_id)) || null, run_id: (event.runId ?? stringValue(payload.run_id)) || null, message: event.message, occurred_at: event.occurredAt, sync_status: event.syncStatus ?? null, payload, }; }) .sort((left, right) => stringValue(right.occurred_at).localeCompare(stringValue(left.occurred_at))); } function filterArtifactAccessRows(rows: BusinessViewRow[], searchParams: URLSearchParams): BusinessViewRow[] { const artifactId = stringValue(searchParams.get('artifact_id')); const action = stringValue(searchParams.get('action')); const status = stringValue(searchParams.get('status')); return rows .filter((row) => !artifactId || stringValue(row.artifact_id) === artifactId) .filter((row) => !action || stringValue(row.action) === action) .filter((row) => !status || stringValue(row.status) === status); } function artifactLifecycleRows(snapshot: QimingclawSnapshot | null): BusinessViewRow[] { return runtimeEvents(snapshot) .filter((event) => ['artifact_retention_marked', 'artifact_retention_rejected', 'artifact_version_observed'].includes(event.kind)) .map((event) => { const payload = asRecord(event.payload) ?? {}; return { lifecycle_id: event.id, event_id: event.id, kind: event.kind, artifact_id: stringValue(payload.artifact_id) || null, action: stringValue(payload.action) || null, status: stringValue(payload.status) || null, retention_status: stringValue(payload.retention_status) || null, retention_until: stringValue(payload.retention_until) || null, reason: stringValue(payload.reason) || null, actor: stringValue(payload.actor) || null, source: stringValue(payload.source) || null, plan_id: (event.planId ?? stringValue(payload.plan_id)) || null, task_id: (event.taskId ?? stringValue(payload.task_id)) || null, run_id: (event.runId ?? stringValue(payload.run_id)) || null, message: event.message, occurred_at: event.occurredAt, sync_status: event.syncStatus ?? null, payload, }; }) .sort((left, right) => stringValue(right.occurred_at).localeCompare(stringValue(left.occurred_at))); } function artifactLifecycleSummary(snapshot: QimingclawSnapshot | null, artifactId: string): Record { const latest = artifactLifecycleRows(snapshot) .find((event) => stringValue(event.artifact_id) === artifactId && stringValue(event.kind) === 'artifact_retention_marked'); return { retention_status: stringValue(latest?.retention_status) || 'unmanaged', retention_until: stringValue(latest?.retention_until) || null, last_retention_action: stringValue(latest?.action) || null, last_retention_at: stringValue(latest?.occurred_at) || null, }; } function artifactRetentionSummaryForAction( artifact: BusinessViewRow, action: string, body: Record, ): Record { return { previous_status: stringValue(artifact.retention_status) || 'unmanaged', retention_status: retentionStatusForAction(action), retention_until: stringValue(body.retention_until ?? body.retentionUntil) || null, reason: stringValue(body.reason) || null, }; } function retentionStatusForAction(action: string): string | null { if (action === 'mark_keep') return 'keep'; if (action === 'mark_expire') return 'expire_candidate'; if (action === 'mark_review') return 'review_required'; if (action === 'clear_retention') return 'unmanaged'; return null; } function artifactVersionSummary(artifact: BusinessViewRow): Record { return { version: stringValue(artifact.version) || null, version_key: stringValue(artifact.version_key) || null, version_rank: numberValue(artifact.version_rank), file_id: stringValue(artifact.file_id) || null, workspace_id: stringValue(artifact.workspace_id) || null, project_id: stringValue(artifact.project_id) || null, created_at: stringValue(artifact.created_at) || null, name: stringValue(artifact.name) || null, }; } function artifactDeliverySummary(artifact: Record): Record { return { delivery_status: stringValue(artifact.delivery_status) || null, delivery_stage: stringValue(artifact.delivery_stage) || null, workspace_id: stringValue(artifact.workspace_id) || null, project_id: stringValue(artifact.project_id) || null, file_id: stringValue(artifact.file_id) || null, version: stringValue(artifact.version) || null, file_size: numberValue(artifact.file_size), source_tool: stringValue(artifact.source_tool) || null, }; } function artifactVersionMeta(artifacts: ArtifactEntry[], artifact: Record): Record { const groupKey = artifactVersionGroupKey(artifact); const group = artifacts .filter((item) => artifactVersionGroupKey(item) === groupKey) .sort((left, right) => stringValue(left.created_at).localeCompare(stringValue(right.created_at))); const artifactId = stringValue(artifact.artifact_id) || stringValue(artifact.subject_id); const index = Math.max(0, group.findIndex((item) => stringValue(item.artifact_id) === artifactId || stringValue(item.subject_id) === artifactId)); return { version: stringValue(artifact.version) || `local-v${index + 1}`, version_key: `${groupKey}:v${stringValue(artifact.version) || index + 1}`, version_rank: index + 1, }; } function artifactVersionGroupKey(artifact: Record): string { const fileId = stringValue(artifact.file_id); const workspaceId = stringValue(artifact.workspace_id); const name = stringValue(artifact.name); if (fileId) return `file:${workspaceId}:${fileId}:${name}`; const artifactId = stringValue(artifact.artifact_id) || stringValue(artifact.subject_id); if (artifactId) return `artifact:${artifactId}`; return `fallback:${workspaceId}:${stringValue(artifact.project_id)}:${name || stringValue(artifact.uri)}`; } function dailyReportRows(snapshot: QimingclawSnapshot | null): BusinessViewRow[] { return buildArtifacts(snapshot) .filter(isDailyReportArtifact) .map((artifact) => { const payload = asRecord(artifact.payload) ?? {}; const artifactId = stringValue(artifact.artifact_id) || stringValue(payload.artifact_id) || stringValue(artifact.subject_id); const reportId = stringValue(payload.report_id) || stringValue(artifact.report_id) || artifactId.replace(/:artifact:text$/, ''); const generatedAt = stringValue(payload.generated_at) || stringValue(artifact.created_at) || new Date().toISOString(); const date = stringValue(payload.date) || generatedAt.slice(0, 10); return { report_id: reportId, artifact_id: artifactId, event_id: stringValue(payload.event_id) || `${reportId}:event:generated`, date, title: stringValue(payload.title) || stringValue(artifact.name) || `${date} 数字员工日报`, status: stringValue(payload.status) || 'ready', summary: stringValue(payload.summary) || stringValue(artifact.value) || '', totals: asRecord(payload.totals) ?? {}, detail_lines: arrayValue(payload.detail_lines), artifact_sources: arrayValue(payload.artifact_sources), filename: stringValue(payload.filename) || stringValue(artifact.name) || dailyReportFilename(date), format: stringValue(payload.format) || 'text', text_content: stringValue(payload.text_content) || stringValue(artifact.value), generated_at: generatedAt, sync_status: stringValue(artifact.sync_status) || null, entity_type: 'artifact', entity_id: artifactId, plan_id: stringValue(artifact.plan_id) || null, task_id: stringValue(artifact.task_id) || null, run_id: stringValue(artifact.run_id) || null, created_at: stringValue(artifact.created_at) || generatedAt, updated_at: generatedAt, payload, }; }) .sort((left, right) => stringValue(right.generated_at).localeCompare(stringValue(left.generated_at))); } function isDailyReportArtifact(artifact: ArtifactEntry): boolean { const payload = asRecord(artifact.payload) ?? {}; return stringValue(artifact.kind) === 'daily_report' || stringValue(payload.kind) === 'daily_report' || Boolean(stringValue(payload.report_id) && stringValue(payload.text_content)); } function filterDailyReportRows(rows: BusinessViewRow[], searchParams: URLSearchParams): BusinessViewRow[] { const date = stringValue(searchParams.get('date')); return filterBusinessRows(rows, searchParams) .filter((row) => !date || stringValue(row.date) === date); } function filterBusinessRows(rows: BusinessViewRow[], searchParams: URLSearchParams): BusinessViewRow[] { const entityId = stringValue(searchParams.get('entity_id')); const remoteId = stringValue(searchParams.get('remote_id')); const status = stringValue(searchParams.get('status')); const syncStatus = stringValue(searchParams.get('sync_status')); const updatedFrom = stringValue(searchParams.get('updated_from')); const updatedTo = stringValue(searchParams.get('updated_to')); return rows .filter((row) => !entityId || businessRowEntityIds(row).includes(entityId)) .filter((row) => !remoteId || stringValue(row.remote_id) === remoteId) .filter((row) => !status || stringValue(row.status) === status) .filter((row) => !syncStatus || stringValue(row.sync_status) === syncStatus) .filter((row) => { const timestamp = businessRowUpdatedAt(row); if (updatedFrom && (!timestamp || timestamp < updatedFrom)) return false; if (updatedTo && (!timestamp || timestamp > updatedTo)) return false; return true; }); } function paginateBusinessRows(rows: BusinessViewRow[], searchParams: URLSearchParams): { items: BusinessViewRow[]; page_no: number; page_size: number; total: number } { const pageNo = Math.max(1, Math.floor(numberValue(searchParams.get('page_no')) ?? 1)); const pageSize = Math.max(1, Math.min(Math.floor(numberValue(searchParams.get('page_size')) ?? 20), 100)); const start = (pageNo - 1) * pageSize; return { items: rows.slice(start, start + pageSize), page_no: pageNo, page_size: pageSize, total: rows.length, }; } function businessEntityFromRefs(refs: Record): { entity_type: string; entity_id: string | null } { const runId = stringValue(refs.run_id); if (runId) return { entity_type: 'run', entity_id: runId }; const taskId = stringValue(refs.task_id); if (taskId) return { entity_type: 'task', entity_id: taskId }; const planId = stringValue(refs.plan_id); if (planId) return { entity_type: 'plan', entity_id: planId }; return { entity_type: stringValue(refs.fallback_type) || 'record', entity_id: stringValue(refs.fallback_id) || null, }; } function businessRowEntityIds(row: BusinessViewRow): string[] { return uniqueStrings([ row.entity_id, row.plan_id, row.task_id, row.run_id, row.event_id, row.history_id, row.step_id, row.node_id, row.artifact_id, row.approval_id, row.notification_id, row.report_id, ]); } function businessRowUpdatedAt(row: BusinessViewRow): string { return stringValue(row.updated_at) || stringValue(row.finished_at) || stringValue(row.started_at) || stringValue(row.occurred_at) || stringValue(row.latest_event_at) || stringValue(row.created_at); } function durationMs(startedAt?: string | null, finishedAt?: string | null): number | null { if (!startedAt || !finishedAt) return null; const started = Date.parse(startedAt); const finished = Date.parse(finishedAt); if (!Number.isFinite(started) || !Number.isFinite(finished) || finished < started) return null; return finished - started; } function auditEventFromRuntimeEvent(event: QimingclawEventRecord): Record { const payload = asRecord(event.payload) ?? {}; const category = event.kind.startsWith('governance_') ? 'governance' : event.kind === 'route_decision' ? 'route' : event.kind === 'approval_decision' ? 'approval' : 'runtime'; const entityType = event.runId ? 'run' : event.taskId ? 'task' : event.planId ? 'plan' : 'event'; return { audit_id: event.id, category, action: event.kind, level: /fail|error|reject|cancel/i.test(`${event.kind} ${event.message}`) ? 'error' : 'info', message: event.message || event.kind, actor: stringValue(payload.actor) || stringValue(payload.source) || 'qimingclaw-runtime', entity_type: entityType, entity_id: event.runId ?? event.taskId ?? event.planId ?? event.id, occurred_at: event.occurredAt, payload, }; } function auditEventsFromArtifact(artifact: ArtifactEntry): Array> { const deliveryStatus = stringValue(artifact.delivery_status); if (!deliveryStatus) return []; return [{ audit_id: `artifact_delivery:${artifact.artifact_id ?? artifact.subject_id}`, category: 'artifact', action: stringValue(artifact.delivery_stage) || 'artifact_delivery', level: deliveryStatus === 'failed' || deliveryStatus === 'error' ? 'error' : 'info', message: `${artifact.name || artifact.kind || 'Artifact'}:${deliveryStatus}`, actor: stringValue(artifact.source_tool) || 'qimingclaw-file-server', entity_type: 'artifact', entity_id: artifact.artifact_id ?? artifact.subject_id, occurred_at: stringValue(artifact.created_at) || new Date().toISOString(), payload: artifact, }]; } function buildCanonicalEvents( snapshot: QimingclawSnapshot | null, tasks: DebugTask[], runs: DebugRun[], ): CanonicalEvent[] { const runtimeCanonicalEvents = runtimeEvents(snapshot).map((event) => ({ event_id: event.id, aggregate_type: event.runId ? 'run' : event.taskId ? 'task' : 'plan', aggregate_id: event.runId ?? event.taskId ?? event.planId ?? event.id, event_type: event.kind, kind: event.kind, message: event.message, occurred_at: event.occurredAt, timestamp: event.occurredAt, plan_id: event.planId ?? null, task_id: event.taskId ?? null, })); const runEvents = runs.flatMap((run) => (run.events ?? []).map((event) => ({ event_id: event.event_id ?? `${run.run_id}:${event.kind ?? 'event'}:${event.occurred_at ?? run.created_at}`, aggregate_type: 'run', aggregate_id: run.run_id, event_type: event.kind ?? run.lifecycle, kind: event.kind ?? run.lifecycle, message: event.message ?? run.lifecycle, occurred_at: event.occurred_at ?? run.completed_at ?? run.created_at, timestamp: event.occurred_at ?? run.completed_at ?? run.created_at, plan_id: run.plan_id ?? null, task_id: run.task_id || null, }))); const taskEvents = tasks.map((task) => ({ event_id: `task-status-${task.task_id}`, aggregate_type: 'task', aggregate_id: task.task_id, event_type: 'task_status', kind: task.status, message: `${task.title}:${taskStatusLabel(task.status)}`, occurred_at: task.updated_at ?? task.created_at, timestamp: task.updated_at ?? task.created_at, plan_id: task.plan_id ?? null, task_id: task.task_id, })); const syncEvents = (snapshot?.sync?.recentFailures ?? []).map((failure) => ({ event_id: `digital-sync-failure-${failure.id}`, aggregate_type: 'sync_outbox', aggregate_id: failure.id, event_type: 'digital_sync_failed', kind: 'digital_sync_failed', message: syncFailureLine(failure), occurred_at: failure.lastAttemptAt ?? failure.updatedAt ?? failure.createdAt, timestamp: failure.lastAttemptAt ?? failure.updatedAt ?? failure.createdAt, plan_id: null, task_id: null, })); return dedupeCanonicalEvents([ ...runtimeCanonicalEvents, ...runEvents, ...taskEvents, ...syncEvents, ]).sort((left, right) => (right.occurred_at ?? '').localeCompare(left.occurred_at ?? '')); } function dedupeCanonicalEvents(events: CanonicalEvent[]): CanonicalEvent[] { const seen = new Set(); return events.filter((event) => { if (!event.event_id || seen.has(event.event_id)) return false; seen.add(event.event_id); return true; }); } function buildApprovals(snapshot: QimingclawSnapshot | null): ApprovalEntry[] { const runtimeApprovalEntries = runtimeApprovals(snapshot).map((approval) => ({ approval_id: approval.id, task_id: approval.taskId ?? '', status: approval.status, created_at: approval.createdAt, plan_id: approval.planId ?? null, run_id: approval.runId ?? null, title: approval.title, reversible: approval.status === 'pending', source: 'qimingclaw-runtime', sync_status: approval.syncStatus, sync_error: approval.syncError, payload: approval.payload, })); const failures = snapshot?.sync?.recentFailures ?? []; const syncReviewEntries = failures .filter((failure) => failure.dueForRetry === false || /credential|auth|token|unauthor/i.test(failure.error ?? '')) .slice(0, 20) .map((failure) => ({ approval_id: `sync-review-${failure.id}`, task_id: failure.entityId, status: 'pending', created_at: failure.createdAt, title: `同步异常待处理:${failure.entityType}/${failure.entityId}`, reversible: false, source: 'qimingclaw-sync', error: failure.error, })); return [...runtimeApprovalEntries, ...syncReviewEntries]; } function buildApprovalTimeline(approvalId: string, snapshot: QimingclawSnapshot | null): ApprovalTimelineResponse { const approval = buildApprovals(snapshot).find((candidate) => stringValue(candidate.approval_id) === approvalId) ?? null; const runtimeApproval = runtimeApprovals(snapshot).find((candidate) => candidate.id === approvalId) ?? null; const approvalPayload = asRecord(approval?.payload ?? runtimeApproval?.payload); const approvalAcpPermission = acpPermissionFromPayload(approvalPayload); const refs = { approvalId, planId: stringValue(approval?.plan_id) || runtimeApproval?.planId || '', taskId: stringValue(approval?.task_id) || runtimeApproval?.taskId || '', runId: stringValue(approval?.run_id) || runtimeApproval?.runId || '', permissionId: approvalAcpPermission.permissionId, sessionId: approvalAcpPermission.sessionId, }; const approvalCreatedAt = stringValue(approval?.created_at) || runtimeApproval?.createdAt || runtimeApproval?.updatedAt || ''; const entries: ApprovalTimelineResponse['events'] = []; if (approval || runtimeApproval) { const title = stringValue(approval?.title) || runtimeApproval?.title || '待确认事项'; const status = stringValue(approval?.status) || runtimeApproval?.status || 'pending'; entries.push({ event_id: `${approvalId}:approval_record`, kind: 'approval_record', status, message: `审批记录:${title} -> ${status}`, occurred_at: approvalCreatedAt || new Date().toISOString(), payload: approvalPayload, source_type: 'approval', }); } for (const event of runtimeEvents(snapshot)) { if (!isApprovalTimelineEvent(event, refs)) continue; const payload = asRecord(event.payload); entries.push({ event_id: event.id, kind: event.kind, status: approvalTimelineEventStatus(event, payload), message: event.message || event.kind, occurred_at: event.occurredAt, payload, source_type: stringValue(payload?.source) || 'runtime_event', }); } return { approval_id: approvalId, approval, events: dedupeApprovalTimelineEvents(entries) .sort((left, right) => left.occurred_at.localeCompare(right.occurred_at) || left.event_id.localeCompare(right.event_id)), }; } function acpPermissionFromPayload(payload: Record | null): { permissionId: string; sessionId: string } { const nested = asRecord(payload?.acpPermission) ?? asRecord(payload?.acp_permission) ?? asRecord(payload?.permission); return { permissionId: stringValue(nested?.permission_id) || stringValue(nested?.permissionId) || stringValue(payload?.permission_id) || stringValue(payload?.permissionId), sessionId: stringValue(nested?.session_id) || stringValue(nested?.sessionId) || stringValue(payload?.session_id) || stringValue(payload?.sessionId), }; } function isApprovalTimelineEvent( event: QimingclawEventRecord, refs: { approvalId: string; planId: string; taskId: string; runId: string; permissionId: string; sessionId: string }, ): boolean { const payload = asRecord(event.payload); const payloadApprovalId = stringValue(payload?.approvalId) || stringValue(payload?.approval_id) || stringValue(payload?.decision_id) || stringValue(payload?.decisionId); if (payloadApprovalId === refs.approvalId) return true; if (event.id.startsWith(`${refs.approvalId}:`)) return true; const acpPermission = acpPermissionFromPayload(payload); if (refs.permissionId && acpPermission.permissionId === refs.permissionId) return true; if (refs.sessionId && acpPermission.sessionId === refs.sessionId) return true; const approvalLikeKind = event.kind.includes('approval') || event.kind.includes('permission'); if (!approvalLikeKind) return false; if (refs.runId && event.runId === refs.runId) return true; if (refs.taskId && event.taskId === refs.taskId) return true; if (refs.planId && event.planId === refs.planId) return true; return false; } function approvalTimelineEventStatus(event: QimingclawEventRecord, payload: Record | null): string | null { return stringValue(payload?.nextStatus) || stringValue(payload?.next_status) || stringValue(payload?.status) || stringValue(payload?.decision) || (event.kind === 'approval_decision' ? 'completed' : null); } function dedupeApprovalTimelineEvents(events: ApprovalTimelineResponse['events']): ApprovalTimelineResponse['events'] { const seen = new Set(); return events.filter((event) => { if (seen.has(event.event_id)) return false; seen.add(event.event_id); return true; }); } function notificationAdapterState(snapshot: QimingclawSnapshot | null): AdapterState { return snapshot?.uiState ? normalizeAdapterState(snapshot.uiState) : readState(); } function buildNotificationsResponse(status: string | null, snapshot: QimingclawSnapshot | null): { notifications: UserNotification[] } { const notifications = buildNotifications(snapshot, notificationAdapterState(snapshot)); const normalizedStatus = (status || 'all').toLowerCase(); if (normalizedStatus === 'all') { return { notifications: notifications.filter((notification) => notification.status !== 'dismissed') }; } return { notifications: notifications.filter((notification) => notification.status === normalizedStatus) }; } function buildNotifications(snapshot: QimingclawSnapshot | null, state: AdapterState): UserNotification[] { return buildNotificationCandidates(snapshot, state) .filter((notification) => notificationMatchesPreferences(notification, state.notificationPreferences)); } function buildNotificationCandidates(snapshot: QimingclawSnapshot | null, state: AdapterState): UserNotification[] { const notifications = dedupeNotifications([ ...approvalNotifications(snapshot), ...approvalActionNotifications(snapshot), ...syncFailureNotifications(snapshot), ...managedServiceNotifications(snapshot), ...planStepDispatchNotifications(snapshot), ...routeDecisionNotifications(snapshot), ...diagnosticNotifications(snapshot), ...auditNotifications(snapshot), ...taskNotifications(snapshot), ]); const overlays = state.notificationStateById ?? {}; return notifications .map((notification) => applyNotificationOverlay(notification, overlays[notification.notification_id])) .sort((left, right) => right.created_at.localeCompare(left.created_at) || right.notification_id.localeCompare(left.notification_id)); } function defaultNotificationPreferences(): NotificationPreferences { return { enabled: true, muted_kinds: [], muted_levels: [], updated_at: null, }; } function defaultApprovalSubscriptionPreferences(): ApprovalSubscriptionPreferences { return { enabled: true, subscribed_actions: [...APPROVAL_SUBSCRIPTION_ACTIONS], subscribed_risk_levels: ['high', 'critical'], notify_on_timeout: true, updated_at: null, }; } function defaultPlanStepDispatchPolicy(): PlanStepDispatchPolicy { return { enabled: true, max_per_sweep: 3, auto_dispatch_ready_steps: true, updated_at: null, }; } function approvalSubscriptionPreferences(state: Partial | null | undefined): ApprovalSubscriptionPreferences { const stored = state?.approvalSubscriptionPreferences; const fallback = defaultApprovalSubscriptionPreferences(); if (!stored || typeof stored !== 'object') return fallback; const actions = Array.isArray(stored.subscribed_actions) ? stored.subscribed_actions.filter((action): action is ApprovalSubscriptionPreferences['subscribed_actions'][number] => APPROVAL_SUBSCRIPTION_ACTIONS.includes(action as ApprovalSubscriptionPreferences['subscribed_actions'][number])) : fallback.subscribed_actions; const riskLevels = Array.isArray(stored.subscribed_risk_levels) ? stored.subscribed_risk_levels.filter((level): level is ApprovalSubscriptionPreferences['subscribed_risk_levels'][number] => APPROVAL_SUBSCRIPTION_RISK_LEVELS.includes(level as ApprovalSubscriptionPreferences['subscribed_risk_levels'][number])) : fallback.subscribed_risk_levels; return { enabled: stored.enabled !== false, subscribed_actions: [...new Set(actions)], subscribed_risk_levels: [...new Set(riskLevels)], notify_on_timeout: stored.notify_on_timeout !== false, updated_at: typeof stored.updated_at === 'string' ? stored.updated_at : fallback.updated_at, }; } function planStepDispatchPolicy(state: Partial | null | undefined): PlanStepDispatchPolicy { const stored = state?.planStepDispatchPolicy; const fallback = defaultPlanStepDispatchPolicy(); if (!stored || typeof stored !== 'object') return fallback; const maxPerSweep = Number(stored.max_per_sweep); return { enabled: stored.enabled !== false, max_per_sweep: Number.isFinite(maxPerSweep) ? Math.max(1, Math.min(Math.floor(maxPerSweep), 10)) : fallback.max_per_sweep, auto_dispatch_ready_steps: stored.auto_dispatch_ready_steps !== false, updated_at: typeof stored.updated_at === 'string' ? stored.updated_at : fallback.updated_at, }; } function notificationPreferences(state: Partial | null | undefined): NotificationPreferences { const stored = state?.notificationPreferences; const fallback = defaultNotificationPreferences(); if (!stored || typeof stored !== 'object') return fallback; const mutedKinds = Array.isArray(stored.muted_kinds) ? stored.muted_kinds.filter((kind): kind is UserNotification['kind'] => NOTIFICATION_KINDS.includes(kind as UserNotification['kind'])) : []; const mutedLevels = Array.isArray(stored.muted_levels) ? stored.muted_levels.filter((level): level is UserNotification['level'] => NOTIFICATION_LEVELS.includes(level as UserNotification['level'])) : []; return { enabled: stored.enabled !== false, muted_kinds: [...new Set(mutedKinds)], muted_levels: [...new Set(mutedLevels)], updated_at: typeof stored.updated_at === 'string' ? stored.updated_at : fallback.updated_at, }; } function notificationMatchesPreferences( notification: UserNotification, preferences: NotificationPreferences | undefined, ): boolean { const normalized = notificationPreferences({ notificationPreferences: preferences }); if (!normalized.enabled) return false; if (normalized.muted_levels.includes(notification.level)) return false; return !normalized.muted_kinds.includes(notification.kind); } async function handleNotificationPreferencesPatch( patch: Partial, snapshot: QimingclawSnapshot | null, ): Promise { const state = snapshot?.uiState ?? await readBridgeUiState() ?? readState(); const current = notificationPreferences(state); const next = notificationPreferences({ notificationPreferences: { ...current, ...patch, updated_at: new Date().toISOString(), }, }); await saveState( { ...normalizeAdapterState(state), notificationPreferences: next, }, 'update_notification_preferences', undefined, { notification_preferences: next }, ); return next; } async function handleApprovalSubscriptionPreferencesPatch( patch: Partial, snapshot: QimingclawSnapshot | null, ): Promise { const state = snapshot?.uiState ?? await readBridgeUiState() ?? readState(); const current = approvalSubscriptionPreferences(state); const next = approvalSubscriptionPreferences({ approvalSubscriptionPreferences: { ...current, ...patch, updated_at: new Date().toISOString(), }, }); await saveState( { ...normalizeAdapterState(state), approvalSubscriptionPreferences: next, }, 'update_approval_subscriptions', undefined, { approval_subscriptions: next }, ); return next; } async function handlePlanStepDispatchPolicyPatch( patch: Partial, snapshot: QimingclawSnapshot | null, ): Promise { const state = snapshot?.uiState ?? await readBridgeUiState() ?? readState(); const current = planStepDispatchPolicy(state); const next = planStepDispatchPolicy({ planStepDispatchPolicy: { ...current, ...patch, updated_at: new Date().toISOString(), }, }); await saveState( { ...normalizeAdapterState(state), planStepDispatchPolicy: next, }, 'update_plan_step_dispatch_policy', undefined, { plan_step_dispatch_policy: next }, ); return next; } function approvalNotifications(snapshot: QimingclawSnapshot | null): UserNotification[] { const summaries = new Map(businessApprovalRows(snapshot).map((approval) => [stringValue(approval.approval_id), approval])); return buildApprovals(snapshot) .filter((approval) => stringValue(approval.source) === 'qimingclaw-runtime') .map((approval) => { const status = stringValue(approval.status) || 'pending'; const open = isDecisionOpenStatus(status); const summary = summaries.get(stringValue(approval.approval_id)); const slaStatus = stringValue(summary?.sla_status); const riskLevel = stringValue(summary?.risk_level); const warn = open && (slaStatus === 'timeout' || riskLevel === 'high'); return { notification_id: `approval:${stringValue(approval.approval_id)}`, plan_id: stringValue(approval.plan_id), task_id: stringValue(approval.task_id), kind: open ? 'need_approval' : 'confirmation_resolved', title: warn ? '审批需要关注' : open ? '审批待处理' : '审批已处理', body: stringValue(approval.title) || '待确认事项', level: warn ? 'warn' : open ? 'action' : 'info', status: open ? 'unread' : 'resolved', correlation_id: stringValue(approval.approval_id), created_at: stringValue(approval.created_at) || new Date().toISOString(), } satisfies UserNotification; }); } function approvalActionNotifications(snapshot: QimingclawSnapshot | null): UserNotification[] { return approvalActionRows(snapshot) .filter((action) => stringValue(action.kind) === 'approval_action_recorded') .filter((action) => ['delegate', 'mark_timeout', 'mark_risk'].includes(stringValue(action.action))) .map((action) => { const actionKind = stringValue(action.action); const approvalId = stringValue(action.approval_id); const isTimeout = actionKind === 'mark_timeout'; const isRisk = actionKind === 'mark_risk'; return { notification_id: `approval-action:${approvalId}:${actionKind}:${slugifyIdPart(stringValue(action.occurred_at))}`, plan_id: stringValue(action.plan_id), task_id: stringValue(action.task_id), kind: 'need_approval', title: isTimeout ? '审批已超时' : isRisk ? '审批已标记风险' : '审批已转交', body: stringValue(action.message) || approvalId, level: isTimeout || isRisk ? 'warn' : 'action', status: 'unread', correlation_id: approvalId, created_at: stringValue(action.occurred_at) || new Date().toISOString(), } satisfies UserNotification; }); } function syncFailureNotifications(snapshot: QimingclawSnapshot | null): UserNotification[] { return (snapshot?.sync?.recentFailures ?? []).map((failure) => ({ notification_id: `sync_failure:${failure.id}`, plan_id: '', task_id: failure.entityId, kind: 'task_failed', title: `同步失败:${failure.entityType}`, body: syncFailureLine(failure), level: failure.dueForRetry === false ? 'error' : 'warn', status: 'unread', correlation_id: failure.id, created_at: failure.lastAttemptAt ?? failure.updatedAt ?? failure.createdAt, })); } function managedServiceNotifications(snapshot: QimingclawSnapshot | null): UserNotification[] { return buildManagedServices(snapshot) .filter((service) => service.requires_human_action || service.status === 'degraded' || service.status === 'stopped') .map((service) => ({ notification_id: `service:${service.service_id}`, plan_id: '', task_id: service.dependent_tasks?.[0] ?? '', kind: 'need_input', title: `服务需要处理:${service.name}`, body: service.error || `${service.name} 当前状态:${service.status_label || service.status}`, level: service.status === 'stopped' ? 'error' : 'warn', status: 'unread', correlation_id: service.service_id, created_at: service.last_seen_at || new Date().toISOString(), })); } function taskNotifications(snapshot: QimingclawSnapshot | null): UserNotification[] { return runtimeTasks(snapshot).flatMap((task) => { const normalized = task.status.toLowerCase(); const success = ['completed', 'succeeded', 'success', 'done'].includes(normalized); const failed = ['failed', 'error', 'rejected', 'cancelled', 'skipped'].includes(normalized); if (!success && !failed) return []; const timestamp = task.updatedAt || task.createdAt || new Date().toISOString(); return [{ notification_id: `task:${task.id}:${slugifyIdPart(timestamp)}`, plan_id: task.planId ?? '', task_id: task.id, kind: success ? 'task_finished' : 'task_failed', title: success ? '任务已完成' : '任务需要关注', body: `${task.title}:${taskStatusLabel(task.status)}`, level: success ? 'info' : 'error', status: 'unread', correlation_id: task.id, created_at: timestamp, } satisfies UserNotification]; }); } function planStepDispatchNotifications(snapshot: QimingclawSnapshot | null): UserNotification[] { return runtimeEvents(snapshot) .filter((event) => event.kind === 'plan_step_dispatch_failed') .map((event) => { const payload = asRecord(event.payload) ?? {}; const stepId = stringValue(payload.step_id) || event.id; const reason = stringValue(payload.reason); return { notification_id: `plan-step-dispatch:${stepId}:${slugifyIdPart(event.occurredAt)}`, plan_id: event.planId ?? stringValue(payload.plan_id), task_id: event.taskId ?? stringValue(payload.task_id), kind: 'task_failed', title: 'Ready 步骤派发失败', body: reason ? `${stepId}:${reason}` : (event.message || stepId), level: 'error', status: 'unread', correlation_id: stepId, created_at: event.occurredAt, } satisfies UserNotification; }); } function routeDecisionNotifications(snapshot: QimingclawSnapshot | null): UserNotification[] { return routeDecisionRows(snapshot) .filter((row) => ['warn', 'error', 'action'].includes(stringValue(row.attention_level))) .map((row) => { const attentionLevel = stringValue(row.attention_level); const routeId = stringValue(row.route_decision_id) || stringValue(row.event_id); const commandId = stringValue(row.created_command_id); const reasonCodes = arrayValue(row.reason_codes).map(stringValue).filter(Boolean).join(' / '); return { notification_id: `route-decision:${routeId}:${slugifyIdPart(stringValue(row.recorded_at))}`, plan_id: stringValue(asRecord(row.context_refs)?.plan_id), task_id: stringValue(asRecord(row.context_refs)?.task_id), kind: attentionLevel === 'action' ? 'task_progress' : 'task_failed', title: attentionLevel === 'action' ? '入口路由已映射' : '入口路由需要关注', body: commandId ? `${row.route_kind} / ${row.intent_kind} -> ${commandId}` : `${row.route_kind} / ${row.intent_kind}${reasonCodes ? `:${reasonCodes}` : ''}`, level: attentionLevel === 'error' ? 'error' : attentionLevel === 'warn' ? 'warn' : 'action', status: 'unread', correlation_id: routeId, created_at: stringValue(row.recorded_at) || new Date().toISOString(), } satisfies UserNotification; }); } function diagnosticNotifications(snapshot: QimingclawSnapshot | null): UserNotification[] { return buildDoctor(snapshot).results .filter((result) => result.severity === 'error' || result.severity === 'warn') .map((result) => { const level = result.severity === 'error' ? 'error' : 'warn'; const diagnosticId = stringValue(result.id) || `${stringValue(result.category) || 'diagnostic'}:${level}`; return { notification_id: `diagnostic:${diagnosticId}`, plan_id: '', task_id: '', kind: level === 'error' ? 'task_failed' : 'need_input', title: level === 'error' ? '诊断发现异常' : '诊断需要关注', body: result.message, level, status: 'unread', correlation_id: diagnosticId, created_at: result.created_at ?? new Date().toISOString(), } satisfies UserNotification; }); } function auditNotifications(snapshot: QimingclawSnapshot | null): UserNotification[] { return auditRows(snapshot) .filter((event) => ['error', 'warn'].includes(stringValue(event.level))) .map((event) => { const level = stringValue(event.level) === 'error' ? 'error' : 'warn'; const auditId = stringValue(event.audit_id) || stringValue(event.entity_id) || 'audit'; return { notification_id: `audit:${auditId}`, plan_id: stringValue(event.plan_id), task_id: stringValue(event.task_id) || (stringValue(event.entity_type) === 'task' ? stringValue(event.entity_id) : ''), kind: level === 'error' ? 'task_failed' : 'need_input', title: level === 'error' ? '审计发现异常' : '审计需要关注', body: stringValue(event.message) || stringValue(event.action) || auditId, level, status: 'unread', correlation_id: auditId, created_at: stringValue(event.occurred_at) || new Date().toISOString(), } satisfies UserNotification; }); } function applyNotificationOverlay(notification: UserNotification, overlay?: NotificationState): UserNotification { if (!overlay) return notification; return { ...notification, status: overlay.status ?? notification.status, read_at: overlay.read_at ?? notification.read_at ?? null, dismissed_at: overlay.dismissed_at ?? notification.dismissed_at ?? null, reply: overlay.reply ?? notification.reply ?? null, replied_at: overlay.replied_at ?? notification.replied_at ?? null, }; } function dedupeNotifications(notifications: UserNotification[]): UserNotification[] { const seen = new Set(); return notifications.filter((notification) => { if (!notification.notification_id || seen.has(notification.notification_id)) return false; seen.add(notification.notification_id); return true; }); } async function handleNotificationAction( notificationId: string, action: 'read' | 'dismiss' | 'reply', body: Record, snapshot: QimingclawSnapshot | null, ): Promise<{ ok: boolean; task_id?: string; status?: string; task_input_recorded?: boolean; governance_command_id?: string; governance_event_id?: string }> { const state = snapshot?.uiState ?? await readBridgeUiState() ?? readState(); const current = buildNotifications(snapshot, state).find((notification) => notification.notification_id === notificationId); const now = new Date().toISOString(); const previousOverlay = state.notificationStateById?.[notificationId]; const alreadyReplied = Boolean(previousOverlay?.replied_at); const overlay: NotificationState = { ...(previousOverlay ?? {}) }; let replyContent = ''; if (action === 'read') { overlay.status = current?.status === 'resolved' ? 'resolved' : 'read'; overlay.read_at = overlay.read_at ?? now; } else if (action === 'dismiss') { overlay.status = 'dismissed'; overlay.dismissed_at = now; } else { overlay.status = current?.status === 'resolved' ? 'resolved' : 'read'; overlay.read_at = overlay.read_at ?? now; replyContent = stringValue(body.content ?? body.reply ?? body.message); overlay.reply = replyContent; overlay.replied_at = now; } const nextState: AdapterState = { ...state, notificationStateById: { ...(state.notificationStateById ?? {}), [notificationId]: overlay, }, }; await saveState(nextState, `notification_${action}`, undefined, { notification_id: notificationId, action }); const taskInput = action === 'reply' && current?.kind === 'need_input' && current.task_id && replyContent && !alreadyReplied ? await recordNotificationTaskInput(current, replyContent, snapshot) : null; return { ok: true, task_id: current?.task_id, status: overlay.status, task_input_recorded: Boolean(taskInput?.accepted), governance_command_id: taskInput?.command_id, governance_event_id: governanceEventIdFromCommand(taskInput), }; } async function recordNotificationTaskInput( notification: UserNotification, replyContent: string, snapshot: QimingclawSnapshot | null, ): Promise { const stamp = Date.now(); return handleGovernanceCommand({ command_kind: 'provide_task_input', context_refs: { plan_id: notification.plan_id || undefined, task_id: notification.task_id, }, payload: { input: replyContent, source: 'notification_reply', notification_id: notification.notification_id, plan_id: notification.plan_id || undefined, task_id: notification.task_id, }, correlation_id: `qimingclaw-notification-reply-${slugifyIdPart(notification.notification_id)}-${stamp}`, idempotency_key: `qimingclaw-notification-reply-${notification.notification_id}`, }, snapshot); } function governanceEventIdFromCommand(response: GovernanceCommandResponse | null): string | undefined { const runId = stringFromUnknown(response?.result?.run_id); if (!response?.accepted || !runId || !response.command_id) return undefined; return `${runId}:provide_task_input:${safeEventIdPart(response.command_id)}`; } function safeEventIdPart(value: string): string { return value.replace(/[^a-zA-Z0-9_-]+/g, '-').replace(/^-+|-+$/g, '') || 'unknown'; } function buildWorkdayDecisions( state: AdapterState, date: string, snapshot: QimingclawSnapshot | null, ): DigitalEmployeeDecision[] { const plansById = new Map(runtimePlans(snapshot).map((plan) => [plan.id, plan])); const tasksById = new Map(runtimeTasks(snapshot).map((task) => [task.id, task])); const approvalSummaries = new Map(businessApprovalRows(snapshot).map((approval) => [stringValue(approval.approval_id), approval])); const resultByDecisionId = state.decisionResultsByDate?.[date] ?? {}; return buildApprovals(snapshot).filter((approval) => stringValue(approval.source) === 'qimingclaw-runtime').map((approval) => { const payload = asRecord(approval.payload); const decisionId = stringValue(approval.approval_id) || `approval-${slugifyIdPart(stringValue(approval.task_id) || stringValue(approval.title) || 'item')}`; const result = resultByDecisionId[decisionId]; const status = decisionStatus(result, stringValue(approval.status) || 'pending'); const planId = stringValue(approval.plan_id); const taskId = stringValue(approval.task_id); const plan = planId ? plansById.get(planId) : null; const task = taskId ? tasksById.get(taskId) : null; const summary = approvalSummaries.get(decisionId); return { id: decisionId, title: stringValue(approval.title) || stringValue(payload?.title) || '待确认事项', scenario: decisionScenario(approval, payload), status, status_label: decisionStatusLabel(status, result), plan_title: plan?.title || stringValue(payload?.plan_title) || stringValue(approval.plan_id) || 'qimingclaw 本地记录', task_title: task?.title || stringValue(payload?.task_title) || stringValue(approval.task_id) || '待确认任务', risk_level: stringValue(summary?.risk_level) || 'normal', sla_status: stringValue(summary?.sla_status) || 'untracked', due_at: stringValue(summary?.due_at) || null, last_action: stringValue(summary?.last_action) || null, last_actor: stringValue(summary?.last_actor) || null, comment_count: numberValue(summary?.comment_count) ?? 0, delegate_to: stringValue(summary?.delegate_to) || null, actions: isDecisionOpenStatus(status) ? [ { id: 'approved', label: '同意', tone: 'primary' }, { id: 'rejected', label: '驳回', tone: 'secondary' }, ] : [], }; }); } function decisionScenario(approval: ApprovalEntry, payload: Record | null): string { return stringValue(payload?.scenario) || stringValue(payload?.summary) || stringValue(payload?.reason) || stringValue(payload?.message) || stringValue(approval.error) || '来自 qimingclaw 本地运行记录的人工确认项。'; } function decisionStatus(result: string | undefined, originalStatus: string): string { const normalizedResult = String(result || '').toLowerCase(); if (['approve', 'approved', 'accept', 'accepted'].includes(normalizedResult)) return 'approved'; if (['reject', 'rejected', 'deny', 'denied'].includes(normalizedResult)) return 'rejected'; if (['dismiss', 'dismissed', 'skip', 'skipped'].includes(normalizedResult)) return 'dismissed'; return originalStatus || 'pending'; } function decisionStatusLabel(status: string, result?: string): string { if (result) { if (status === 'approved') return '已同意'; if (status === 'rejected') return '已驳回'; if (status === 'dismissed') return '已忽略'; return '已处理'; } if (status === 'approved') return '已同意'; if (status === 'rejected') return '已驳回'; if (status === 'completed') return '已完成'; if (status === 'failed') return '异常'; return '待确认'; } function isDecisionOpenStatus(status: string): boolean { return ['pending', 'requested'].includes(String(status || '').toLowerCase()); } function buildArtifacts(snapshot: QimingclawSnapshot | null): ArtifactEntry[] { const artifacts = [ ...runtimeArtifacts(snapshot).map(artifactFromRuntimeRecord), ...runtimeRuns(snapshot).flatMap((run) => artifactsForRun(run, runtimeEvents(snapshot))), ...runtimeEvents(snapshot).flatMap((event) => artifactsFromPayload(event.id, event.payload, event.occurredAt, { sourceType: 'event', sourceEventId: event.id, sourceLabel: `Event ${event.kind}`, planId: event.planId, taskId: event.taskId, runId: event.runId, })), ]; const deduped = dedupeArtifacts(artifacts); return deduped.map((artifact) => withArtifactAccessSummary(artifact, snapshot, deduped)); } function withArtifactAccessSummary(artifact: ArtifactEntry, snapshot: QimingclawSnapshot | null, artifacts: ArtifactEntry[]): ArtifactEntry { const artifactId = stringValue(artifact.artifact_id) || stringValue(artifact.subject_id); const lastAccess = artifactAccessRows(snapshot).find((row) => stringValue(row.artifact_id) === artifactId) ?? null; const hasLocalUri = isLocalArtifactUri(stringValue(artifact.uri)); const hasDailyText = isDailyReportArtifact(artifact) && Boolean(stringValue(asRecord(artifact.payload)?.text_content)); const formalRuntimeArtifact = runtimeArtifacts(snapshot).some((runtime) => runtime.id === artifactId || runtime.remoteId === artifactId); const previewable = formalRuntimeArtifact && (hasDailyText || hasLocalUri); const lifecycle = artifactLifecycleSummary(snapshot, artifactId); const version = artifactVersionMeta(artifacts, artifact); return { ...artifact, version: version.version, version_key: version.version_key, version_rank: version.version_rank, retention_status: lifecycle.retention_status, retention_until: lifecycle.retention_until, last_retention_action: lifecycle.last_retention_action, delivery_summary: artifactDeliverySummary(artifact), access: { previewable, downloadable: formalRuntimeArtifact && hasLocalUri, openable: formalRuntimeArtifact && hasLocalUri, access_policy: formalRuntimeArtifact ? 'qimingclaw-local-artifact-policy' : 'runtime-projection-readonly', last_access_status: stringValue(lastAccess?.status) || null, }, }; } function artifactFromRuntimeRecord(artifact: QimingclawArtifactRecord): ArtifactEntry { const payload = asRecord(artifact.payload); const delivery = asRecord(payload?.delivery); const sourceTool = stringValue(delivery?.source_tool); const deliveryUri = stringValue(delivery?.uri); const deliveryFilename = stringValue(delivery?.filename); return { artifact_id: artifact.id, subject_id: artifact.runId ?? artifact.taskId ?? artifact.planId ?? artifact.id, kind: artifact.kind, name: artifact.name || deliveryFilename || stringValue(payload?.name ?? payload?.title) || artifactName(artifact.uri ?? deliveryUri) || artifact.kind, uri: artifact.uri || deliveryUri || stringValue(payload?.uri ?? payload?.url ?? payload?.path) || undefined, value: stringValue(payload?.value ?? payload?.summary ?? payload?.text) || artifact.uri || deliveryUri || artifact.name || deliveryFilename || artifact.kind, plan_id: artifact.planId ?? null, task_id: artifact.taskId ?? null, run_id: artifact.runId ?? null, created_at: artifact.createdAt, source: 'qimingclaw-runtime', source_type: 'artifact', source_label: sourceTool ? `File Server: ${sourceTool}` : artifactSourceLabel('artifact', artifact.id), sync_status: artifact.syncStatus, sync_error: artifact.syncError, payload: artifact.payload, ...deliveryProjection(delivery), }; } function artifactsForRun(run: QimingclawRunRecord, events: QimingclawEventRecord[]): ArtifactEntry[] { return dedupeArtifacts([ ...artifactsFromPayload(run.id, run.payload, run.finishedAt ?? run.startedAt ?? run.createdAt, { sourceType: 'run', sourceLabel: artifactSourceLabel('run', run.id), planId: run.planId, taskId: run.taskId, runId: run.id, }), ...events .filter((event) => event.runId === run.id) .flatMap((event) => artifactsFromPayload(run.id, event.payload, event.occurredAt, { sourceType: 'event', sourceEventId: event.id, sourceLabel: `Event ${event.kind}`, planId: event.planId, taskId: event.taskId, runId: event.runId, })), ]); } interface ArtifactSourceContext { sourceType: string; sourceLabel: string; sourceEventId?: string | null; planId?: string | null; taskId?: string | null; runId?: string | null; } function artifactsFromPayload( subjectId: string, payload: unknown, createdAt?: string | null, sourceContext?: ArtifactSourceContext, ): ArtifactEntry[] { const record = asRecord(payload); if (!record) return typeof payload === 'string' ? [artifactFromValue(subjectId, payload, 0, createdAt, sourceContext)] : []; const explicitArtifacts = [ ...arrayValue(record.artifacts), ...arrayValue(record.outputs), ...arrayValue(record.files), ...arrayValue(record.attachments), ]; const artifacts = explicitArtifacts.map((item, index) => artifactFromValue(subjectId, item, index, createdAt, sourceContext)); for (const key of ['artifact', 'outputPath', 'output_path', 'filePath', 'file_path', 'path', 'uri', 'url']) { if (record[key]) artifacts.push(artifactFromValue(subjectId, record[key], artifacts.length, createdAt, sourceContext)); } return artifacts.filter((artifact) => artifact.name || artifact.uri || artifact.value); } function artifactFromValue( subjectId: string, value: unknown, index: number, createdAt?: string | null, sourceContext?: ArtifactSourceContext, ): ArtifactEntry { const record = asRecord(value); if (record) { const delivery = asRecord(record.delivery); const sourceTool = stringValue(delivery?.source_tool); const deliveryUri = stringValue(delivery?.uri); const uri = stringValue(record.uri ?? record.url ?? record.path ?? record.filePath ?? record.file_path ?? record.outputPath ?? record.output_path); const name = stringValue(record.name ?? record.title ?? record.filename) || stringValue(delivery?.filename) || artifactName(uri || deliveryUri) || `产物 ${index + 1}`; return { artifact_id: stringValue(record.artifact_id ?? record.id) || `artifact-${slugifyIdPart(subjectId)}-${index + 1}`, subject_id: stringValue(record.subject_id) || subjectId, kind: stringValue(record.kind ?? record.type) || 'artifact', name, uri: uri || deliveryUri, value: stringValue(record.value ?? record.summary ?? record.text) || uri || deliveryUri, created_at: (stringValue(record.created_at ?? record.createdAt) || createdAt) ?? undefined, source: 'qimingclaw-runtime', source_type: stringValue(record.source_type) || sourceContext?.sourceType || 'payload', source_event_id: stringValue(record.source_event_id) || sourceContext?.sourceEventId || null, source_label: sourceTool ? `File Server: ${sourceTool}` : stringValue(record.source_label) || sourceContext?.sourceLabel || artifactSourceLabel('payload', subjectId), plan_id: stringValue(record.plan_id) || sourceContext?.planId || null, task_id: stringValue(record.task_id) || sourceContext?.taskId || null, run_id: stringValue(record.run_id) || sourceContext?.runId || null, ...deliveryProjection(delivery), }; } const text = String(value ?? '').trim(); return { artifact_id: `artifact-${slugifyIdPart(subjectId)}-${index + 1}`, subject_id: subjectId, kind: 'artifact', name: artifactName(text) || `产物 ${index + 1}`, uri: looksLikeUri(text) ? text : undefined, value: text, created_at: createdAt ?? undefined, source: 'qimingclaw-runtime', source_type: sourceContext?.sourceType || 'payload', source_event_id: sourceContext?.sourceEventId || null, source_label: sourceContext?.sourceLabel || artifactSourceLabel('payload', subjectId), plan_id: sourceContext?.planId || null, task_id: sourceContext?.taskId || null, run_id: sourceContext?.runId || null, }; } function deliveryProjection(delivery: Record | null): Record { if (!delivery) return {}; return { delivery_status: stringValue(delivery.status) || null, delivery_stage: stringValue(delivery.stage) || null, workspace_id: stringValue(delivery.workspace_id) || null, project_id: stringValue(delivery.project_id) || null, file_id: stringValue(delivery.file_id) || null, version: stringValue(delivery.version) || null, file_size: numberValue(delivery.size), source_tool: stringValue(delivery.source_tool) || null, }; } function artifactSourceLabel(sourceType: string, sourceId?: string | null): string { const suffix = sourceId ? ` ${sourceId}` : ''; switch (sourceType) { case 'artifact': return `Artifact${suffix}`; case 'event': return `Event${suffix}`; case 'run': return `Run${suffix}`; case 'task': return `Task${suffix}`; case 'plan': return `Plan${suffix}`; default: return `Payload${suffix}`; } } function dedupeArtifacts(artifacts: ArtifactEntry[]): ArtifactEntry[] { const seen = new Set(); return artifacts.filter((artifact) => { const key = artifact.artifact_id || `${artifact.subject_id}:${artifact.uri || artifact.value || artifact.name}`; if (!key || seen.has(key)) return false; seen.add(key); return Boolean(artifact.name || artifact.uri || artifact.value); }); } function asRecord(value: unknown): Record | null { return value && typeof value === 'object' && !Array.isArray(value) ? value as Record : null; } function arrayValue(value: unknown): unknown[] { return Array.isArray(value) ? value : []; } function stringValue(value: unknown): string { return typeof value === 'string' ? value.trim() : ''; } function numberValue(value: unknown): number | null { if (typeof value === 'number' && Number.isFinite(value)) return value; if (typeof value === 'string' && value.trim()) { const parsed = Number(value); return Number.isFinite(parsed) ? parsed : null; } return null; } function looksLikeUri(value: string): boolean { return /^(file|https?):\/\//i.test(value) || value.includes('/') || value.includes('\\'); } function isLocalArtifactUri(value: string): boolean { if (!value) return false; if (/^file:\/\//i.test(value)) return true; if (/^[a-z][a-z0-9+.-]*:/i.test(value)) return false; return value.startsWith('/') || /^[a-z]:[\\/]/i.test(value); } function artifactName(value: string): string { if (!value) return ''; const normalized = value.split(/[?#]/)[0] || value; return normalized.split(/[\\/]/).filter(Boolean).pop() ?? ''; } function slugifyIdPart(value: string): string { const slug = value .trim() .toLowerCase() .replace(/[^a-z0-9]+/g, '-') .replace(/^-+|-+$/g, ''); if (slug) return slug; let hash = 0; for (let index = 0; index < value.length; index += 1) { hash = (hash * 31 + value.charCodeAt(index)) >>> 0; } return `item-${hash.toString(36)}`; } function filterPlans(query?: string | null, snapshot: QimingclawSnapshot | null = null): DebugPlan[] { const normalized = (query ?? '').trim().toLowerCase(); const plans = mergedDebugPlans(snapshot); if (!normalized) return plans; return plans.filter((plan) => ( plan.title.toLowerCase().includes(normalized) || plan.plan_id.toLowerCase().includes(normalized) || (plan.objective ?? '').toLowerCase().includes(normalized) )); } function buildRuntimeWorkEvents(snapshot: QimingclawSnapshot | null): DigitalEmployeeWorkEvent[] { const plansById = new Map(runtimePlans(snapshot).map((plan) => [plan.id, plan])); const tasksById = new Map(runtimeTasks(snapshot).map((task) => [task.id, task])); const events = runtimeEvents(snapshot).map((event) => { const task = event.taskId ? tasksById.get(event.taskId) : undefined; const planId = event.planId ?? task?.planId ?? null; const plan = planId ? plansById.get(planId) : undefined; const status = task?.status ?? plan?.status ?? (event.kind.includes('failed') || event.kind.includes('error') ? 'failed' : 'running'); const tone = taskStatusTone(status); return { id: event.id, time: formatTime(event.occurredAt), plan_title: plan?.title ?? 'qimingclaw 本地执行', task_title: task?.title ?? event.message, step_title: event.kind, detail: event.message || event.kind, status, status_label: taskStatusLabel(status), status_tone: tone, updated_at: event.occurredAt, conversation_id: planId ?? task?.id ?? event.id, }; }); if (events.length > 0) return events; return runtimeTasks(snapshot).map((task) => { const plan = task.planId ? plansById.get(task.planId) : undefined; return { id: `task-event-${task.id}`, time: formatTime(task.updatedAt), plan_title: plan?.title ?? 'qimingclaw 本地任务', task_title: task.title, step_title: task.assignedSkillId || 'qimingclaw-runtime', detail: task.syncError || `${task.title}:${taskStatusLabel(task.status)},同步状态 ${task.syncStatus}`, status: task.status, status_label: taskStatusLabel(task.status), status_tone: taskStatusTone(task.status), updated_at: task.updatedAt, conversation_id: task.planId ?? task.id, }; }); } function buildEvents(duties: DigitalEmployeeDuty[], confirmed: boolean, snapshot: QimingclawSnapshot | null): DigitalEmployeeWorkEvent[] { if (hasRuntimeRecords(snapshot)) { const runtimeWorkEvents = buildRuntimeWorkEvents(snapshot); return appendSyncEvent(runtimeWorkEvents, snapshot); } if (!confirmed) { return [{ id: 'event-await-confirmation', time: '待确认', plan_title: '任务设置', task_title: '等待确认今日任务', step_title: '选择职责', detail: '请先确认今天要执行的任务,确认后数字员工会开始执行。', status: 'pending', status_label: '待确认', status_tone: 'waiting', updated_at: new Date().toISOString(), conversation_id: null, }]; } const selectedDuties = duties.filter((duty) => duty.selected); const events = selectedDuties.map((duty) => ({ id: `event-${duty.id}`, time: duty.status === 'running' ? '实时' : '待执行', plan_title: duty.title, task_title: duty.title, step_title: duty.status === 'running' ? '同步 qimingclaw 服务状态' : '等待调度', detail: duty.status === 'running' ? `${duty.result_text}。快照时间:${snapshot?.generatedAt ?? '未连接 bridge'}` : '任务已确认,将在后续阶段接入 ACP/MCP 真实执行事件。', status: duty.status === 'running' ? 'running' : 'pending', status_label: duty.status === 'running' ? '执行中' : '待执行', status_tone: duty.status === 'running' ? 'running' : 'waiting', updated_at: new Date().toISOString(), conversation_id: duty.conversation_id, })); return appendSyncEvent(events, snapshot); } function appendSyncEvent(events: DigitalEmployeeWorkEvent[], snapshot: QimingclawSnapshot | null): DigitalEmployeeWorkEvent[] { const sync = snapshot?.sync; if (sync && (sync.pending > 0 || sync.failed > 0 || sync.lastSyncAt)) { const recentFailureText = (sync.recentFailures || []) .slice(0, 3) .map(syncFailureLine) .join(';'); return [...events, { id: 'event-management-sync', time: sync.syncing ? '实时' : '同步', plan_title: '管理端联动', task_title: '同步数字员工本地状态', step_title: sync.failed > 0 ? '同步补偿' : 'outbox 推送', detail: [ syncLine(snapshot), sync.lastSyncError ? `错误:${sync.lastSyncError}` : '', recentFailureText ? `最近失败:${recentFailureText}` : '', ].filter(Boolean).join('。'), status: sync.failed > 0 ? 'failed' : sync.syncing || sync.pending > 0 ? 'running' : 'completed', status_label: sync.failed > 0 ? '异常' : sync.syncing || sync.pending > 0 ? '同步中' : '已同步', status_tone: sync.failed > 0 ? 'danger' : sync.syncing || sync.pending > 0 ? 'running' : 'success', updated_at: sync.lastSyncAt ?? new Date().toISOString(), conversation_id: 'qimingclaw-management-sync', }]; } return events; } function buildExecutionSummaries(duties: DigitalEmployeeDuty[], events: DigitalEmployeeWorkEvent[]): DigitalEmployeeTaskExecutionSummary[] { const selectedDuties = duties.some((duty) => duty.selected) ? duties.filter((duty) => duty.selected) : duties; return selectedDuties.map((duty) => { const related = events.filter((event) => event.conversation_id === duty.conversation_id); const running = related.filter((event) => event.status === 'running').length; const success = related.filter((event) => event.status === 'completed').length; const failure = related.filter((event) => taskStatusTone(event.status) === 'danger').length; const waiting = Math.max(0, related.length - running - success - failure); return { id: duty.id, plan_id: duty.id, title: duty.title, description: duty.summary, selected: duty.selected, total_count: Math.max(related.length, 1), success_count: success, failure_count: failure, running_count: running, waiting_count: related.length === 0 ? 1 : waiting, latest_status_label: failure > 0 ? '异常' : running > 0 ? '执行中' : success > 0 ? '已完成' : '待执行', latest_status_tone: failure > 0 ? 'danger' : running > 0 ? 'running' : success > 0 ? 'success' : 'waiting', business_summary: duty.result_text, ai_summary: '当前由 qimingclaw 本地运行记录生成。', }; }); } function buildRunDetails(events: DigitalEmployeeWorkEvent[]): DigitalEmployeeRunDetail[] { return events.map((event) => ({ id: `run-detail-${event.id}`, time: event.time, task_name: event.task_title, step_name: event.step_title, operation_name: event.plan_title, status_label: event.status_label, status_tone: event.status_tone, business_detail: event.detail, raw_detail: event.detail, updated_at: event.updated_at, conversation_id: event.conversation_id, })); } function buildWorkday(date: string, snapshot: QimingclawSnapshot | null): DigitalEmployeeWorkdayProjection { const state = snapshot?.uiState ?? readState(); const confirmedAt = state.confirmedDates[date] ?? null; const confirmed = Boolean(confirmedAt); const duties = buildDuties(state, date, snapshot); const events = buildEvents(duties, confirmed, snapshot); const executionSummaries = buildExecutionSummaries(duties, events); const generatedAt = state.reportGeneratedAtByDate[date] ?? null; const selectedCount = duties.filter((duty) => duty.selected).length; const runningCount = events.filter((event) => event.status === 'running').length; const successCount = events.filter((event) => taskStatusTone(event.status) === 'success').length; const failureCount = events.filter((event) => taskStatusTone(event.status) === 'danger').length; const liveRecordCount = runtimePlans(snapshot).length + runtimeTasks(snapshot).length; const hasLiveRuntime = hasRuntimeRecords(snapshot); const sync = snapshot?.sync; const artifactSources = reportArtifactSources(buildArtifacts(snapshot)); const runDetails = buildRunDetails(events); const managedServices = buildManagedServices(snapshot); const managedServiceIssues = managedServices.filter((service) => service.requires_human_action || service.status === 'degraded'); const runningManagedServiceCount = managedServices.filter((service) => service.running).length; const planAgents = buildPlanAgents(snapshot); const taskAgents = buildTaskAgents(snapshot); const actionableTaskAgents = taskAgents.filter((agent) => agent.can_retry || agent.can_skip || agent.can_cancel); return { source: 'live', date, employee: { name: state.profile.name, title: '数字员工', major: 'qimingclaw 客户端任务执行', company: state.profile.company, position: state.profile.position, current_status: state.profile.currentStatus, current_status_label: state.profile.currentStatus === 'resting' ? '休息中' : '工作中', motto: '承接客户端任务,调度 ACP 与 MCP 能力完成执行。', }, workday: { confirmed, confirmed_at: confirmedAt, current_status: state.profile.currentStatus, current_status_label: state.profile.currentStatus === 'resting' ? '休息中' : '工作中', status_label: hasLiveRuntime ? (failureCount > 0 || managedServiceIssues.length > 0 ? '有异常' : runningCount > 0 ? '执行中' : '已同步') : confirmed ? '运行中' : '待确认', status_tone: hasLiveRuntime ? (failureCount > 0 || managedServiceIssues.length > 0 ? 'danger' : runningCount > 0 ? 'running' : 'success') : confirmed ? 'running' : 'waiting', summary: hasLiveRuntime ? `当前日期 ${displayDate(date)} 已读取 ${liveRecordCount} 条 qimingclaw 本地运行记录,${managedServiceSummary(managedServices)}。` : confirmed ? `当前日期 ${displayDate(date)} 已确认 ${selectedCount} 项任务。` : `当前日期 ${displayDate(date)} 待确认今日任务。`, bubble_text: hasLiveRuntime ? managedServiceIssues.length > 0 ? `我发现 ${managedServiceIssues.length} 项常驻服务需要处理,会把影响到的任务标出来。` : '我已接入 qimingclaw 本地运行记录,会持续展示真实任务、事件和同步状态。' : confirmed ? '我已接入 qimingclaw 客户端执行链路,正在等待真实 Run/Event 数据接入。' : '请先确认今天要执行的任务,我会把它们映射到 qimingclaw 执行链路。', }, overview: { title: '今日执行', status_label: hasLiveRuntime ? '本地真实记录' : confirmed ? '真实数据' : '待确认', status_tone: failureCount > 0 ? 'danger' : hasLiveRuntime || confirmed ? 'success' : 'waiting', metrics: [ { label: '已选任务', value: String(selectedCount) }, { label: '运行记录', value: String(events.length) }, { label: '执行中', value: String(runningCount) }, { label: '已完成', value: String(successCount) }, { label: '异常', value: String(failureCount) }, { label: '待同步', value: String(sync?.pending ?? 0) }, { label: '同步异常', value: String(sync?.failed ?? 0) }, { label: '服务运行', value: `${runningManagedServiceCount}/${managedServices.length}` }, { label: '需处理服务', value: String(managedServiceIssues.length) }, { label: 'Plan Agent', value: String(planAgents.length) }, { label: 'Task Agent', value: String(taskAgents.length) }, { label: '可治理任务', value: String(actionableTaskAgents.length) }, ], }, managed_services: managedServices, plan_agents: planAgents, task_agents: taskAgents, duties, events, execution_summaries: executionSummaries, run_details: runDetails, decisions: buildWorkdayDecisions(state, date, snapshot), delivery: { headline: hasLiveRuntime ? 'qimingclaw 本地记录已接入' : confirmed ? 'qimingclaw 执行链路已接入' : '等待确认任务设置', summary: hasLiveRuntime ? '数字员工页面正在读取本地 Plan / Task / Run / Event,并同步展示管理端 outbox 状态。' : '当前阶段已将数字员工页面切换到 qimingclaw adapter,并开始读取客户端服务状态。', lines: [ '已吸收 sgRobot 数字员工 UI 和核心展示模型。', hasLiveRuntime ? `已读取本地记录:Plan ${runtimePlans(snapshot).length} / Task ${runtimeTasks(snapshot).length} / Run ${runtimeRuns(snapshot).length} / Event ${runtimeEvents(snapshot).length}。` : '已建立 qimingclaw workday/plans/tasks/skills API adapter。', serviceLine(snapshot, 'agent', 'Agent'), serviceLine(snapshot, 'mcp', 'MCP'), serviceLine(snapshot, 'computerServer', 'Computer Server'), managedServiceSummary(managedServices), ...managedServiceIssues.slice(0, 3).map(managedServiceLine), `Plan Agent:${planAgents.length} 个计划代理,Task Agent:${taskAgents.length} 个任务代理。`, actionableTaskAgents[0] ? `待治理任务:${actionableTaskAgents[0].title},下一步 ${actionableTaskAgents[0].next_action}。` : 'Task Agent:暂无需要重试、跳过或取消的任务。', syncLine(snapshot), `活跃会话:${snapshot?.sessions.active ?? 0} / ${snapshot?.sessions.total ?? 0}`, ], artifacts: [ { name: '数字员工运行摘要', status: generatedAt ? 'ready' : 'pending' }, ...artifactSources.slice(0, 4).map((artifact) => ({ name: artifact.name, status: generatedAt ? 'ready' : 'available', uri: artifact.uri ?? null, source_label: artifact.source_label ?? null, source_type: artifact.source_type ?? null, })), ], }, route_summary: buildRouteDecisionSummary(snapshot), diagnostic_summary: buildDiagnosticSummary(snapshot), daily_report: buildDailyReportProjection({ date, state, generatedAt, selectedCount, events, executionSummaries, runDetails, artifactSources, successCount, failureCount, runningCount, snapshot }), }; } function buildDailyReportProjection(input: { date: string; state: AdapterState; generatedAt: string | null; selectedCount: number; events: DigitalEmployeeWorkEvent[]; executionSummaries: DigitalEmployeeTaskExecutionSummary[]; runDetails: DigitalEmployeeRunDetail[]; artifactSources: ReturnType; successCount: number; failureCount: number; runningCount: number; snapshot: QimingclawSnapshot | null; }): DigitalEmployeeDailyReport { const title = `${displayDate(input.date)} 飞天数字员工日报`; const summary = input.generatedAt ? `今日已完成数字员工页面到 qimingclaw adapter 的接入,并读取了 qimingclaw 服务状态。${syncLine(input.snapshot)}。` : '日报将在生成后汇总任务选择、运行明细和后续接入建议。'; const totals = { task_count: input.selectedCount, execution_count: input.events.length, success_count: input.successCount, failure_count: input.failureCount, running_count: input.runningCount, }; const detailLines = input.events.map((event) => `${event.time} ${event.task_title}:${event.detail}`); const reportId = input.generatedAt ? dailyReportId(input.date, input.generatedAt) : null; const downloadFilename = dailyReportFilename(input.date); const baseReport: DigitalEmployeeDailyReport = { report_id: reportId, artifact_id: reportId ? `${reportId}:artifact:text` : null, event_id: reportId ? `${reportId}:event:generated` : null, date: input.date, title, report_schedule_time: input.state.reportScheduleTime, status: input.generatedAt ? 'ready' : 'not_generated', generated_at: input.generatedAt, generated_by: input.generatedAt ? 'ai' : 'fallback', model: 'qimingclaw-adapter', source: 'qimingclaw', summary, totals, task_sections: input.executionSummaries, detail_lines: detailLines, artifact_sources: input.artifactSources, download_filename: downloadFilename, text_content: null, pdf_available: Boolean(input.generatedAt), pdf_url: null, }; return { ...baseReport, text_content: input.generatedAt ? buildDailyReportText(baseReport, input.runDetails, input.artifactSources) : null, }; } function buildDailyReportText( report: DigitalEmployeeDailyReport, details: DigitalEmployeeRunDetail[], artifacts: ReturnType, ): string { return [ report.title || `${report.date} 数字员工日报`, `日期:${report.date}`, `生成时间:${report.generated_at || '暂未生成'}`, `生成来源:${report.generated_by === 'ai' ? 'AI' : 'qimingclaw adapter'}`, '', '## 摘要', report.summary || '暂无摘要。', '', '## 指标', `任务总数:${report.totals.task_count}`, `执行次数:${report.totals.execution_count}`, `成功次数:${report.totals.success_count}`, `失败次数:${report.totals.failure_count}`, `运行中:${report.totals.running_count ?? 0}`, '', '## 任务汇总', ...(report.task_sections.length > 0 ? report.task_sections.map((section) => `- ${section.title}:${section.latest_status_label},执行 ${section.total_count} 次,成功 ${section.success_count},失败 ${section.failure_count}。${section.ai_summary || section.business_summary || ''}`) : ['暂无任务汇总。']), '', '## 运行明细', ...(details.length > 0 ? details.map((detail) => `- ${detail.time || '--:--'} ${detail.task_name} / ${detail.step_name || detail.operation_name || '任务执行'}:${detail.status_label},${detail.business_detail}`) : ['暂无运行明细。']), '', '## 产物来源', ...(artifacts.length > 0 ? artifacts.map((artifact) => `- ${artifact.name}:${artifact.source_label || artifact.source_type || '未知来源'}${artifact.uri ? `,${artifact.uri}` : ''}`) : ['暂无产物来源。']), '', '## 详情', ...((report.detail_lines ?? []).length > 0 ? report.detail_lines : ['暂无详情。']), ].join('\n'); } function dailyReportFilename(date: string): string { return `qimingclaw-digital-report-${date}.txt`; } function dailyReportId(date: string, generatedAt: string): string { return `daily-report:${date}:${slugifyIdPart(generatedAt)}`; } function reportArtifactSources(artifacts: ArtifactEntry[]) { return artifacts.slice(0, 20).map((artifact) => ({ id: stringValue(artifact.artifact_id) || `${stringValue(artifact.subject_id)}:${stringValue(artifact.uri ?? artifact.name)}`, name: stringValue(artifact.name) || stringValue(artifact.uri) || stringValue(artifact.value) || '未命名产物', kind: stringValue(artifact.kind) || null, uri: stringValue(artifact.uri) || null, delivery_status: stringValue(artifact.delivery_status) || null, delivery_stage: stringValue(artifact.delivery_stage) || null, workspace_id: stringValue(artifact.workspace_id) || null, project_id: stringValue(artifact.project_id) || null, file_id: stringValue(artifact.file_id) || null, version: stringValue(artifact.version) || null, version_key: stringValue(artifact.version_key) || null, version_rank: numberValue(artifact.version_rank), retention_status: stringValue(artifact.retention_status) || null, retention_until: stringValue(artifact.retention_until) || null, last_retention_action: stringValue(artifact.last_retention_action) || null, file_size: typeof artifact.file_size === 'number' ? artifact.file_size : null, source_tool: stringValue(artifact.source_tool) || null, source_label: stringValue(artifact.source_label) || null, source_type: stringValue(artifact.source_type) || null, source_event_id: stringValue(artifact.source_event_id) || null, plan_id: stringValue(artifact.plan_id) || null, task_id: stringValue(artifact.task_id) || null, run_id: stringValue(artifact.run_id) || null, created_at: stringValue(artifact.created_at) || null, access: artifact.access, delivery_summary: artifact.delivery_summary ?? null, })); } async function handleWorkdayAction(body: DigitalEmployeeWorkdayActionRequest, snapshot: QimingclawSnapshot | null): Promise { const state = snapshot?.uiState ?? await readBridgeUiState() ?? readState(); const date = body.date || todayInputDate(); if (body.action === 'confirm_workday') { state.selectedDutyIdsByDate[date] = body.duty_ids ?? []; state.confirmedDates[date] = new Date().toISOString(); } if (body.action === 'update_profile') { state.profile = { name: body.name || state.profile.name, company: body.company || state.profile.company, position: body.position || state.profile.position, currentStatus: body.current_status || state.profile.currentStatus, }; } if (body.action === 'update_report_schedule') { state.reportScheduleTime = body.report_schedule_time || state.reportScheduleTime; } if (body.action === 'generate_daily_report') { if (body.duty_ids) state.selectedDutyIdsByDate[date] = body.duty_ids; if (body.report_schedule_time) state.reportScheduleTime = body.report_schedule_time; state.reportGeneratedAtByDate[date] = new Date().toISOString(); } if (body.action === 'decide_approval' && body.decision_id) { state.decisionResultsByDate = state.decisionResultsByDate ?? {}; state.decisionResultsByDate[date] = { ...(state.decisionResultsByDate[date] ?? {}), [body.decision_id]: body.decision || 'resolved', }; } const acpPermissionResult = body.action === 'decide_approval' ? await respondAcpPermissionForDecision(body, snapshot) : null; const savedState = await saveState( state, body.action, date, body.action === 'decide_approval' ? { decision_id: body.decision_id, decision: body.decision, acp_permission: acpPermissionResult, } : undefined, ); const nextSnapshot = { ...(snapshot ?? { generatedAt: new Date().toISOString(), services: {}, sessions: { total: 0, active: 0, items: [] } }), uiState: savedState }; const projection = buildWorkday(date, nextSnapshot); if (body.action === 'generate_daily_report' && projection.daily_report) { await recordDailyReportArtifact(projection.daily_report); } return projection; } async function recordDailyReportArtifact(report: DigitalEmployeeDailyReport): Promise { if (!report.report_id || !report.generated_at || !report.text_content) return; try { await window.QimingClawBridge?.digital?.recordDailyReport?.({ reportId: report.report_id, date: report.date, title: report.title, summary: report.summary, totals: report.totals, detailLines: report.detail_lines, artifactSources: report.artifact_sources ?? [], filename: report.download_filename || dailyReportFilename(report.date), textContent: report.text_content, generatedAt: report.generated_at, reportScheduleTime: report.report_schedule_time, source: report.source ?? 'qimingclaw', model: report.model ?? 'qimingclaw-adapter', }); } catch (error) { console.warn('[qimingclawAdapter] record daily report failed:', error); } } async function respondAcpPermissionForDecision( body: DigitalEmployeeWorkdayActionRequest, snapshot: QimingclawSnapshot | null, ): Promise | null> { if (!body.decision_id || !body.decision) return null; const approval = findApprovalForDecision(body.decision_id, snapshot); if (!approval) return null; const refs = acpPermissionRefs(approval); if (!refs) return null; const response = acpResponseForDecision(body.decision); const respondPermission = window.QimingClawBridge?.digital?.respondPermission; if (!respondPermission) return null; try { const result = await respondPermission(refs.sessionId, refs.permissionId, response); return { permission_id: refs.permissionId, session_id: refs.sessionId, response, success: result?.success !== false, error: result?.error, }; } catch (error) { return { permission_id: refs.permissionId, session_id: refs.sessionId, response, success: false, error: error instanceof Error ? error.message : String(error), }; } } function findApprovalForDecision(decisionId: string, snapshot: QimingclawSnapshot | null): ApprovalEntry | null { return buildApprovals(snapshot).find((approval) => stringValue(approval.approval_id) === decisionId) ?? null; } function acpPermissionRefs(approval: ApprovalEntry): { permissionId: string; sessionId: string } | null { const payload = asRecord(approval.payload); const nestedPayload = asRecord(payload?.payload); const permissionId = stringValue(payload?.permissionId) || stringValue(payload?.permission_id) || stringValue(payload?.id) || stringValue(nestedPayload?.permissionId) || stringValue(nestedPayload?.permission_id) || stringValue(nestedPayload?.id); const sessionId = stringValue(payload?.sessionId) || stringValue(payload?.session_id) || stringValue(nestedPayload?.sessionId) || stringValue(nestedPayload?.session_id); const source = stringValue(payload?.source); if (!permissionId || !sessionId || (source && source !== 'acp-permission')) return null; return { permissionId, sessionId }; } function acpResponseForDecision(decision: string): 'once' | 'always' | 'reject' { const normalized = decision.toLowerCase(); if (normalized.includes('always')) return 'always'; if (normalized.includes('reject') || normalized.includes('deny')) return 'reject'; return 'once'; } async function handlePlanAction( planId: string, action: string, snapshot: QimingclawSnapshot | null, payload: Record = {}, ): Promise { const commandKind = planActionCommandKind(action); const correlationId = `plan-action-${slugifyIdPart(planId)}-${slugifyIdPart(action)}-${Date.now()}`; if (!commandKind) { return { command_id: `qimingclaw-digital-plan-action-${Date.now()}`, accepted: false, correlation_id: correlationId, message: `暂不支持计划动作:${action}`, error: 'unsupported_command', }; } return handleGovernanceCommand({ command_kind: commandKind, context_refs: { plan_id: planId }, payload: { ...payload, plan_id: planId, requested_action: `plan_${action}`, action_endpoint: `/api/debug/plan/${planId}/${action}`, }, correlation_id: correlationId, }, snapshot); } async function handleTaskAction( taskId: string, action: string, snapshot: QimingclawSnapshot | null, payload: Record = {}, ): Promise { const commandKind = taskActionCommandKind(action); const task = runtimeTasks(snapshot).find((candidate) => candidate.id === taskId); const latestRun = latestRunForTask(runtimeRuns(snapshot), taskId); const planId = task?.planId ?? (stringValue(payload.plan_id) || undefined); const runId = latestRun?.id ?? (stringValue(payload.run_id) || undefined); const stepId = task ? taskStepId(task) : (stringValue(payload.step_id) || undefined); const correlationId = `task-action-${slugifyIdPart(taskId)}-${slugifyIdPart(action)}-${Date.now()}`; if (!commandKind) { return { command_id: `qimingclaw-digital-task-action-${Date.now()}`, accepted: false, correlation_id: correlationId, message: `暂不支持任务动作:${action}`, error: 'unsupported_command', }; } return handleGovernanceCommand({ command_kind: commandKind, context_refs: { plan_id: planId, task_id: taskId, run_id: runId, step_id: stepId, }, payload: { ...payload, task_id: taskId, plan_id: planId ?? payload.plan_id, run_id: runId ?? payload.run_id, step_id: stepId ?? payload.step_id, requested_action: `task_${action}`, action_endpoint: `/api/task/${taskId}/${action}`, }, correlation_id: correlationId, }, snapshot); } function planActionCommandKind(action: string): GovernanceCommandRequest['command_kind'] | null { switch (action.toLowerCase()) { case 'submit': return 'submit_plan'; case 'approve': return 'approve_plan'; case 'activate': return 'activate_plan'; case 'pause': return 'pause_plan'; case 'resume': return 'resume_plan'; case 'cancel': return 'cancel_plan'; case 'amend': return 'amend_plan'; default: return null; } } function taskActionCommandKind(action: string): GovernanceCommandRequest['command_kind'] | null { switch (action.toLowerCase()) { case 'retry': return 'retry_task'; case 'skip': return 'skip_task'; case 'pause': return 'pause_task'; case 'resume': return 'resume_task'; case 'cancel': return 'cancel_task'; case 'start': return 'start_run'; default: return null; } } function buildPlanRevisions(planId: string, snapshot: QimingclawSnapshot | null): { plan_id: string; revisions: Array> } { const revisions = runtimeEvents(snapshot) .filter((event) => event.planId === planId && event.kind.startsWith('governance_')) .sort((left, right) => right.occurredAt.localeCompare(left.occurredAt) || right.createdAt.localeCompare(left.createdAt)) .map((event) => { const payload = asRecord(event.payload) ?? {}; const result = asRecord(payload.result) ?? {}; const commandKind = stringValue(payload.commandKind) || event.kind.replace(/^governance_/, ''); return { revision_id: event.id, plan_id: planId, event_id: event.id, command_kind: commandKind, action: commandKind.replace(/_plan$/, ''), status: stringValue(result.status) || stringValue(payload.status) || 'recorded', message: event.message, created_at: event.occurredAt, payload, }; }); return { plan_id: planId, revisions }; } async function readRouteDecisions(limit = 80): Promise { const bridge = window.QimingClawBridge?.digital; if (!bridge?.listRouteDecisions) { return { has_route_decision_state: false, decisions: [] }; } try { const decisions = await bridge.listRouteDecisions({ limit }); return { has_route_decision_state: true, decisions: decisions ?? [] }; } catch (error) { console.warn('[qimingclawAdapter] list route decisions failed:', error); return { has_route_decision_state: false, decisions: [] }; } } async function handleIngressRoute( body: IngressRouteRequest, snapshot: QimingclawSnapshot | null, ): Promise { const recordedAt = body.timestamp || new Date().toISOString(); const correlationId = body.correlation_id || `route-${Date.now()}`; const idSource = body.idempotency_key || correlationId; const contextRefs = buildIngressContextRefs(body); const classification = classifyIngressRoute(body, contextRefs, snapshot); const filteredCandidates = await filterRouteCandidateSkills(classification.candidateSkills); const reasonCodes = filteredCandidates.allCandidatesDisabled ? [...classification.reasonCodes, 'candidate_skills_disabled'] : classification.reasonCodes; const decision: RouteDecisionRecord = { id: `route-decision-${slugifyIdPart(idSource)}`, route_kind: classification.routeKind, intent_kind: classification.intentKind, reason_codes: reasonCodes, candidate_skills: filteredCandidates.candidateSkills, context_refs: contextRefs, created_command_id: null, correlation_id: correlationId, creates_plan: classification.createsPlan, creates_task_run: classification.createsTaskRun, approval_mode: null, policy_result: 'accepted', recorded_at: recordedAt, entrypoint: body.entrypoint ?? null, actor: body.actor ?? null, }; const routeCommand = buildGovernanceCommandForRouteDecision(decision, body, snapshot); const command = routeCommand.command ? await handleGovernanceCommand(routeCommand.command, snapshot) : null; if (command?.accepted && command.command_id) { decision.created_command_id = command.command_id; decision.reason_codes = uniqueStrings([...decision.reason_codes, 'route_action_mapped']); } else if (routeCommand.reasonCode) { decision.reason_codes = uniqueStrings([...decision.reason_codes, routeCommand.reasonCode]); } const bridge = window.QimingClawBridge?.digital; const recorded = bridge?.recordRouteDecision ? await bridge.recordRouteDecision({ id: decision.id, idempotencyKey: body.idempotency_key, correlationId, routeKind: decision.route_kind, intentKind: decision.intent_kind, reasonCodes: decision.reason_codes, candidateSkills: decision.candidate_skills, contextRefs: decision.context_refs, createdCommandId: decision.created_command_id, createsPlan: decision.creates_plan, createsTaskRun: decision.creates_task_run, approvalMode: decision.approval_mode, policyResult: decision.policy_result, entrypoint: decision.entrypoint, actor: decision.actor, message: `入口路由决策:${decision.route_kind} / ${decision.intent_kind}`, envelope: { text: body.text ?? null, payload: body.payload ?? null, attachments: body.attachments ?? [], }, recordedAt, }) : null; const routeDecision = recorded ?? decision; const resultIds = routeCommandResultIds(command); return { accepted: true, envelope: { entrypoint: body.entrypoint ?? 'chat', text: body.text ?? null, payload: body.payload ?? null, attachments: body.attachments ?? [], context_refs: contextRefs, }, response_kind: 'route_decision', route_decision: routeDecision, command, created_plan_id: resultIds.createdPlanId, created_schedule_id: resultIds.createdScheduleId, created_task_ids: resultIds.createdTaskIds, created_run_ids: resultIds.createdRunIds, projection_name: null, projection_payload: null, message: command?.command_id ? `已记录入口路由并映射治理动作:${routeDecision.route_kind} / ${routeDecision.intent_kind}` : `已记录入口路由:${routeDecision.route_kind} / ${routeDecision.intent_kind}`, }; } function buildGovernanceCommandForRouteDecision( decision: RouteDecisionRecord, body: IngressRouteRequest, snapshot: QimingclawSnapshot | null, ): { command: GovernanceCommandRequest | null; reasonCode: string } { if (decision.reason_codes.includes('candidate_skills_disabled')) { return { command: null, reasonCode: 'route_action_not_mapped' }; } const contextRefs = decision.context_refs ?? {}; const payload = asRecord(body.payload) ?? {}; const basePayload = compactRecord({ ...payload, source: 'qimingclaw-ingress-route', route_decision_id: decision.id, route_kind: decision.route_kind, intent_kind: decision.intent_kind, reason_codes: decision.reason_codes, ingress_text: body.text ?? null, }); const baseCommand = (commandKind: GovernanceCommandRequest['command_kind'], refs: Record): GovernanceCommandRequest => ({ command_kind: commandKind, context_refs: compactRecord({ ...contextRefs, ...refs }), payload: compactRecord({ ...basePayload, ...refs }), correlation_id: decision.correlation_id, idempotency_key: `route-command:${decision.id}`, }); if (decision.route_kind === 'PlanExecution' && decision.intent_kind === 'schedule') { const scheduleId = stringValue(contextRefs.schedule_id) || stringValue(payload.schedule_id) || stringValue(payload.scheduleId); if (!scheduleId) return { command: null, reasonCode: 'route_action_missing_context' }; return { command: baseCommand('run_schedule_now', { schedule_id: scheduleId }), reasonCode: 'route_action_mapped' }; } if (decision.route_kind === 'PlanExecution' && decision.intent_kind === 'execute') { const stepId = stringValue(contextRefs.step_id) || stringValue(payload.step_id) || stringValue(payload.stepId); const taskId = stringValue(contextRefs.task_id) || stringValue(payload.task_id) || stringValue(payload.taskId) || taskIdForRouteStep(snapshot, stepId); if (!taskId) return { command: null, reasonCode: 'route_action_missing_context' }; const task = runtimeTasks(snapshot).find((candidate) => candidate.id === taskId) ?? null; return { command: baseCommand('start_run', { plan_id: stringValue(contextRefs.plan_id) || task?.planId || stringValue(payload.plan_id) || stringValue(payload.planId), task_id: taskId, step_id: stepId || (task ? taskStepId(task) : ''), run_id: stringValue(contextRefs.run_id) || stringValue(payload.run_id) || stringValue(payload.runId), }), reasonCode: 'route_action_mapped', }; } if (decision.route_kind === 'PlanControl' && decision.intent_kind === 'control') { const actionText = routeActionText(body); const taskId = stringValue(contextRefs.task_id) || stringValue(payload.task_id) || stringValue(payload.taskId); const planId = stringValue(contextRefs.plan_id) || stringValue(payload.plan_id) || stringValue(payload.planId); const commandKind = routeControlCommandKind(actionText, Boolean(taskId)); if (!commandKind || (!taskId && !planId)) return { command: null, reasonCode: 'route_action_missing_context' }; return { command: baseCommand(commandKind, { plan_id: planId, task_id: taskId, step_id: stringValue(contextRefs.step_id) || stringValue(payload.step_id) || stringValue(payload.stepId), run_id: stringValue(contextRefs.run_id) || stringValue(payload.run_id) || stringValue(payload.runId), }), reasonCode: 'route_action_mapped', }; } return { command: null, reasonCode: 'route_action_not_mapped' }; } function taskIdForRouteStep(snapshot: QimingclawSnapshot | null, stepId: string): string { if (!stepId) return ''; const ready = runtimePlanSteps(snapshot).some((step) => step.id === stepId && step.status.toLowerCase() === 'ready'); if (!ready) return ''; return runtimeTasks(snapshot).find((task) => taskStepId(task) === stepId)?.id ?? ''; } function routeActionText(body: IngressRouteRequest): string { const payload = asRecord(body.payload) ?? {}; return [body.text, payload.action, payload.command, payload.intent, payload.requested_action] .map(stringValue) .filter(Boolean) .join(' ') .toLowerCase(); } function routeControlCommandKind(actionText: string, hasTaskTarget: boolean): GovernanceCommandRequest['command_kind'] | null { if (hasTaskTarget) { if (matchesRouteText(actionText, ['retry', '重试'])) return 'retry_task'; if (matchesRouteText(actionText, ['skip', '跳过'])) return 'skip_task'; if (matchesRouteText(actionText, ['pause', '暂停'])) return 'pause_task'; if (matchesRouteText(actionText, ['resume', '恢复'])) return 'resume_task'; if (matchesRouteText(actionText, ['cancel', '取消'])) return 'cancel_task'; if (matchesRouteText(actionText, ['start', 'run', 'execute', 'begin', '启动', '开始', '执行'])) return 'start_run'; return null; } if (matchesRouteText(actionText, ['submit', '提交'])) return 'submit_plan'; if (matchesRouteText(actionText, ['approve', '批准', '同意'])) return 'approve_plan'; if (matchesRouteText(actionText, ['pause', '暂停'])) return 'pause_plan'; if (matchesRouteText(actionText, ['resume', '恢复'])) return 'resume_plan'; if (matchesRouteText(actionText, ['cancel', '取消'])) return 'cancel_plan'; if (matchesRouteText(actionText, ['amend', 'edit', 'revise', '修改', '调整'])) return 'amend_plan'; if (matchesRouteText(actionText, ['activate', 'start', 'run', 'execute', '启动', '开始', '执行'])) return 'activate_plan'; return null; } function routeCommandResultIds(command: GovernanceCommandResponse | null): { createdPlanId: string | null; createdScheduleId: string | null; createdTaskIds: string[]; createdRunIds: string[]; } { const result = asRecord(command?.result) ?? {}; const createdTaskIds = uniqueStrings([ result.task_id, result.taskId, ...arrayValue(result.dispatched_tasks).map((task) => asRecord(task)?.task_id ?? asRecord(task)?.taskId), ]); return { createdPlanId: stringValue(result.plan_id) || stringValue(result.planId) || null, createdScheduleId: stringValue(result.schedule_id) || stringValue(result.scheduleId) || null, createdTaskIds, createdRunIds: uniqueStrings([result.run_id, result.runId]), }; } function buildIngressContextRefs(body: IngressRouteRequest): Record { const payload = asRecord(body.payload) ?? {}; const context = asRecord(body.context_refs) ?? {}; return compactRecord({ ...context, entrypoint: body.entrypoint ?? context.entrypoint, plan_id: stringValue(context.plan_id) || stringValue(payload.plan_id) || stringValue(payload.planId), task_id: stringValue(context.task_id) || stringValue(payload.task_id) || stringValue(payload.taskId), run_id: stringValue(context.run_id) || stringValue(payload.run_id) || stringValue(payload.runId), step_id: stringValue(context.step_id) || stringValue(payload.step_id) || stringValue(payload.stepId), schedule_id: stringValue(context.schedule_id) || stringValue(payload.schedule_id) || stringValue(payload.scheduleId), }); } function classifyIngressRoute( body: IngressRouteRequest, contextRefs: Record, snapshot: QimingclawSnapshot | null, ): { routeKind: string; intentKind: string; reasonCodes: string[]; candidateSkills: string[]; createsPlan: boolean; createsTaskRun: boolean } { const payload = asRecord(body.payload) ?? {}; const text = `${body.text ?? ''} ${stringValue(payload.action)} ${stringValue(payload.command)} ${stringValue(payload.intent)}`.toLowerCase(); const hasPayload = Object.keys(payload).length > 0; const hasContext = ['plan_id', 'task_id', 'step_id', 'schedule_id'].some((key) => Boolean(stringValue(contextRefs[key]))); const stepId = stringValue(contextRefs.step_id); const readyStep = stepId && runtimePlanSteps(snapshot).some((step) => step.id === stepId && step.status.toLowerCase() === 'ready'); if (hasContext && matchesRouteText(text, ['start', 'run', 'retry', 'skip', 'cancel', 'pause', 'resume', 'approve', '启动', '开始', '执行', '重试', '跳过', '取消', '暂停', '恢复', '批准'])) { return routeClassification('PlanControl', 'control', ['context_control_action'], contextRefs, payload, false, false); } if (body.entrypoint === 'cron' || matchesRouteText(text, ['cron', 'schedule', 'scheduled', '定时', '调度', '周期'])) { return routeClassification('PlanExecution', 'schedule', ['scheduled_entrypoint'], contextRefs, payload, false, true); } if (matchesRouteText(text, ['status', 'progress', 'result', 'report', 'artifact', 'memory', '状态', '进度', '结果', '日报', '产物', '记忆'])) { return routeClassification('ProjectionQuery', 'query', ['projection_query_text'], contextRefs, payload, false, false); } if (readyStep || matchesRouteText(text, ['execute', 'dispatch', 'delegate', 'run task', '执行', '开始', '委托', '派发'])) { return routeClassification('PlanExecution', 'execute', readyStep ? ['ready_step_context'] : ['execution_text'], contextRefs, payload, false, true); } if (!body.text && !hasPayload && !hasContext) { return routeClassification('AskClarification', 'chat', ['empty_ingress'], contextRefs, payload, false, false); } return routeClassification('ChatOnly', 'chat', ['default_chat'], contextRefs, payload, false, false); } function routeClassification( routeKind: string, intentKind: string, reasonCodes: string[], contextRefs: Record, payload: Record, createsPlan: boolean, createsTaskRun: boolean, ): { routeKind: string; intentKind: string; reasonCodes: string[]; candidateSkills: string[]; createsPlan: boolean; createsTaskRun: boolean } { return { routeKind, intentKind, reasonCodes, candidateSkills: routeCandidateSkills(routeKind, contextRefs, payload), createsPlan, createsTaskRun, }; } function routeCandidateSkills(routeKind: string, contextRefs: Record, payload: Record): string[] { const explicit = uniqueStrings([ ...arrayValue(contextRefs.skill_ids), ...arrayValue(contextRefs.skills), ...arrayValue(contextRefs.candidate_skills), ...arrayValue(payload.skill_ids), ...arrayValue(payload.skills), ...arrayValue(payload.candidate_skills), stringValue(contextRefs.skill_id), stringValue(payload.skill_id), ]); if (explicit.length > 0) return explicit; if (routeKind === 'PlanControl') return ['qimingclaw-acp-session']; if (routeKind === 'ProjectionQuery') return ['qimingclaw-artifact-reporting']; if (routeKind === 'PlanExecution') return ['qimingclaw-computer-control']; return []; } async function filterRouteCandidateSkills( candidateSkills: string[], ): Promise<{ candidateSkills: string[]; allCandidatesDisabled: boolean }> { if (candidateSkills.length === 0) { return { candidateSkills, allCandidatesDisabled: false }; } const bridge = window.QimingClawBridge?.digital; if (!bridge?.getSkillCapabilities) { return { candidateSkills, allCandidatesDisabled: false }; } try { const result = await bridge.getSkillCapabilities(); if (!Array.isArray(result?.skills)) { return { candidateSkills, allCandidatesDisabled: false }; } const enabledBySkillId = new Map(); for (const skill of result.skills) { const skillId = stringValue(skill.skill_id); if (skillId) enabledBySkillId.set(skillId, skill.enabled !== false); } const enabledCandidates = candidateSkills.filter((skillId) => enabledBySkillId.get(skillId) !== false); return { candidateSkills: enabledCandidates, allCandidatesDisabled: candidateSkills.length > 0 && enabledCandidates.length === 0, }; } catch (error) { console.warn('[qimingclawAdapter] filter route candidate skills failed:', error); return { candidateSkills, allCandidatesDisabled: false }; } } function uniqueStrings(values: unknown[]): string[] { return [...new Set(values.map(stringValue).filter(Boolean))]; } function matchesRouteText(text: string, keywords: string[]): boolean { return keywords.some((keyword) => text.includes(keyword.toLowerCase())); } function compactRecord(record: Record): Record { return Object.fromEntries(Object.entries(record).filter(([, value]) => value !== undefined && value !== null && value !== '')); } async function handleGovernanceCommand( body: GovernanceCommandRequest, snapshot: QimingclawSnapshot | null, ): Promise { const commandId = `qimingclaw-digital-${body.command_kind}-${Date.now()}`; const correlationId = body.correlation_id || commandId; const planId = stringFromUnknown(body.context_refs?.plan_id) || stringFromUnknown(body.payload?.plan_id) || stringFromUnknown(body.payload?.original_plan_id); const taskId = stringFromUnknown(body.context_refs?.task_id) || stringFromUnknown(body.payload?.task_id) || stringFromUnknown(body.payload?.original_task_id); const runId = stringFromUnknown(body.context_refs?.run_id) || stringFromUnknown(body.payload?.run_id) || stringFromUnknown(body.payload?.latest_run_id); const stepId = stringFromUnknown(body.context_refs?.step_id) || stringFromUnknown(body.payload?.step_id) || stringFromUnknown(body.payload?.stepId); const scheduleId = stringFromUnknown(body.context_refs?.schedule_id) || stringFromUnknown(body.payload?.schedule_id); if (body.command_kind === 'create_plan') { const createdPlanId = `qimingclaw-local-${slugifyIdPart(stringFromUnknown(body.payload?.title) || planId || 'plan')}-${Date.now()}`; const response = governanceAccepted(commandId, correlationId, '已在 qimingclaw 兼容层创建本地计划草稿。', { plan_id: createdPlanId, status: 'draft', source: 'qimingclaw-adapter', requested_action: body.payload?.requested_action, original_plan_id: body.payload?.original_plan_id ?? planId, }); await recordGovernanceCommand(body, response); return response; } if (body.command_kind === 'activate_plan' || body.command_kind === 'run_schedule_now') { const targetPlanId = planId || runtimeDebugPlans(snapshot)[0]?.plan_id || templatePlans(snapshot)[0]?.plan_id || 'qimingclaw-client-readiness'; const dispatch = await dispatchPlan(targetPlanId, snapshot, { recordCommand: false }); const response = governanceAccepted(commandId, correlationId, '已映射为 qimingclaw 本地任务激活。', { plan_id: targetPlanId, schedule_id: body.command_kind === 'run_schedule_now' ? scheduleId : null, status: 'active', dispatched_tasks: dispatch.dispatched_tasks, source: 'qimingclaw-adapter', }); await recordGovernanceCommand(body, response); return response; } if (body.command_kind === 'create_schedule') { const response = governanceAccepted(commandId, correlationId, '已记录 qimingclaw 兼容定时任务请求。', { schedule_id: scheduleId || `qimingclaw-schedule-${Date.now()}`, plan_id: planId, status: 'enabled', source: 'qimingclaw-adapter', }); await recordGovernanceCommand(body, response); return response; } if (['update_schedule', 'pause_schedule', 'resume_schedule', 'delete_schedule'].includes(body.command_kind)) { const status = body.command_kind === 'pause_schedule' ? 'paused' : body.command_kind === 'delete_schedule' ? 'deleted' : 'enabled'; const response = governanceAccepted(commandId, correlationId, `已接受 ${body.command_kind},并更新 qimingclaw 本地定时任务状态。`, { schedule_id: scheduleId || `qimingclaw-schedule-${Date.now()}`, plan_id: planId, status, source: 'qimingclaw-adapter', }); await recordGovernanceCommand(body, response); return response; } if (['submit_plan', 'approve_plan', 'amend_plan', 'resume_plan', 'pause_plan', 'cancel_plan'].includes(body.command_kind)) { const response = governanceAccepted(commandId, correlationId, `已接受 ${body.command_kind},当前由 qimingclaw 本地 adapter 记录兼容状态。`, { plan_id: planId, status: body.command_kind === 'approve_plan' ? 'approved' : body.command_kind === 'submit_plan' ? 'reviewing' : 'accepted', source: 'qimingclaw-adapter', }); await recordGovernanceCommand(body, response); return response; } if (['retry_task', 'skip_task', 'pause_task', 'resume_task', 'provide_task_input', 'cancel_task', 'start_run', 'cancel_run'].includes(body.command_kind)) { const targetTask = taskId || runtimeTasks(snapshot)[0]?.id || 'qimingclaw-task-agent'; const runtimeTask = runtimeTasks(snapshot).find((task) => task.id === targetTask); const targetRunId = runId || latestRunForTask(runtimeRuns(snapshot), targetTask)?.id || null; const targetStepId = stepId || (runtimeTask ? taskStepId(runtimeTask) : null); const response = governanceAccepted(commandId, correlationId, `已接受 ${body.command_kind},并记录到 qimingclaw Task Agent 控制平面。`, { plan_id: planId || runtimeTask?.planId || 'qimingclaw-task-agent-plan', task_id: targetTask, run_id: targetRunId, step_id: targetStepId, task_agent_id: `task-agent-${slugifyIdPart(targetTask)}`, status: taskGovernanceStatus(body.command_kind), next_action: taskGovernanceNextAction(body.command_kind), source: 'qimingclaw-task-agent', }); await recordGovernanceCommand(body, response); return response; } const response = { command_id: commandId, accepted: false, correlation_id: correlationId, error: 'missing_capability', message: `qimingclaw 数字员工兼容层尚未支持 ${body.command_kind}。`, }; await recordGovernanceCommand(body, response); return response; } function governanceAccepted( commandId: string, correlationId: string, message: string, result: Record, ): GovernanceCommandResponse { return { command_id: commandId, accepted: true, correlation_id: correlationId, message, result, }; } function taskGovernanceStatus(commandKind: string): string { switch (commandKind) { case 'retry_task': case 'start_run': case 'resume_task': return 'running'; case 'skip_task': return 'skipped'; case 'cancel_task': case 'cancel_run': return 'cancelled'; case 'pause_task': return 'paused'; default: return 'accepted'; } } function taskGovernanceNextAction(commandKind: string): string { switch (commandKind) { case 'retry_task': return 'start_retry_run'; case 'skip_task': return 'continue_next_task'; case 'pause_task': return 'await_resume'; case 'resume_task': return 'watch_progress'; case 'provide_task_input': return 'resume_with_input'; case 'cancel_task': case 'cancel_run': return 'close_execution'; case 'start_run': return 'watch_progress'; default: return 'record_control_event'; } } function stringFromUnknown(value: unknown): string { return typeof value === 'string' && value.trim() ? value.trim() : ''; } function buildPlanFlow(planId: string, snapshot: QimingclawSnapshot | null = null): FlowResponse { if (hasRuntimeRecords(snapshot)) { const plan = runtimePlans(snapshot).find((candidate) => candidate.id === planId); const tasks = buildTasks(planId, snapshot); const planSteps = runtimeStepsForPlan(snapshot, planId); const runs = buildTasksResponse(planId, snapshot).runs; const events = runtimeEvents(snapshot).filter((event) => event.planId === planId || tasks.some((task) => task.task_id === event.taskId)); const title = plan?.title ?? tasks[0]?.title ?? planId; const objective = plan?.objective ?? 'qimingclaw 本地运行记录'; const qimingclawFlowSteps: FlowResponse['steps'] = []; let nextSeq = 2; if (planSteps.length > 0) { const knownStepIds = new Set(planSteps.map((step) => step.id)); for (const step of planSteps) { qimingclawFlowSteps.push({ seq: nextSeq++, lane: 'qimingclaw', label: step.title, sub_label: `${taskStatusLabel(step.status)} · 依赖 ${step.dependsOn.length} · 技能 ${step.preferredSkillIds.length}`, timestamp: step.updatedAt, color: '#22c55e', source_type: 'plan_step', }); for (const task of tasks.filter((candidate) => candidate.step_id === step.id)) { qimingclawFlowSteps.push(runtimeTaskFlowStep(task, nextSeq++)); } } const orphanTasks = tasks.filter((task) => !knownStepIds.has(task.step_id || '')); if (orphanTasks.length > 0) { qimingclawFlowSteps.push({ seq: nextSeq++, lane: 'qimingclaw', label: '未归属步骤', sub_label: `包含 ${orphanTasks.length} 个任务`, timestamp: orphanTasks[0]?.updated_at ?? new Date().toISOString(), color: '#22c55e', source_type: 'plan_step', }); for (const task of orphanTasks) qimingclawFlowSteps.push(runtimeTaskFlowStep(task, nextSeq++)); } } else { for (const task of tasks) qimingclawFlowSteps.push(runtimeTaskFlowStep(task, nextSeq++)); } return { plan_id: planId, plan_title: title, plan_status: plan ? planRecordStatus(plan, runtimePlanTasks(snapshot, plan.id)) : tasks[0]?.status, plan_objective: objective, lanes: [ { key: 'intent', label: '目标', color: '#38bdf8' }, { key: 'qimingclaw', label: 'qimingclaw', color: '#22c55e' }, { key: 'execution', label: '执行', color: '#f59e0b' }, ], steps: [ { seq: 1, lane: 'intent', label: title, sub_label: objective, timestamp: plan?.createdAt ?? tasks[0]?.created_at ?? new Date().toISOString(), color: '#38bdf8', source_type: 'plan', }, ...qimingclawFlowSteps, ...runs.map((run, index) => ({ seq: nextSeq + index, lane: 'execution', label: run.lifecycle || 'Run', sub_label: run.run_id, timestamp: run.completed_at ?? run.created_at, color: '#f59e0b', source_type: 'run', })), ], events: events.map((event) => ({ event_id: event.id, kind: event.kind, occurred_at: event.occurredAt, message: event.message, })), }; } const plan = withRuntimePlan(templatePlans(snapshot).find((candidate) => candidate.plan_id === planId) ?? templatePlans(snapshot)[0]!, snapshot); const timestamp = new Date().toISOString(); return { plan_id: plan.plan_id, plan_title: plan.title, plan_status: plan.status, plan_objective: plan.objective, lanes: [ { key: 'intent', label: '目标', color: '#38bdf8' }, { key: 'qimingclaw', label: 'qimingclaw', color: '#22c55e' }, { key: 'execution', label: '执行', color: '#f59e0b' }, ], steps: [ { seq: 1, lane: 'intent', label: plan.title, sub_label: plan.objective, timestamp: plan.created_at, color: '#38bdf8', source_type: 'plan', }, { seq: 2, lane: 'qimingclaw', label: '接入客户端能力', sub_label: 'ACP / MCP / computer server adapter', timestamp, color: '#22c55e', source_type: 'adapter', }, { seq: 3, lane: 'execution', label: plan.status === 'running' ? '已读取 qimingclaw 服务状态' : '等待调度', sub_label: planResultText(plan, snapshot), timestamp, color: '#f59e0b', source_type: 'run', }, ], events: [ { event_id: `${plan.plan_id}-adapter-event`, kind: 'qimingclaw_adapter', occurred_at: timestamp, message: '数字员工页面已通过 qimingclaw adapter 获取数据。', }, ], }; } function runtimeTaskFlowStep(task: DebugTask, seq: number): FlowResponse['steps'][number] { return { seq, lane: 'qimingclaw', label: task.title, sub_label: `${taskStatusLabel(task.status)} · ${task.assigned_skill_id || 'qimingclaw-runtime'}`, timestamp: task.updated_at, color: '#22c55e', source_type: 'task', }; } function buildTaskFlow(taskId: string, snapshot: QimingclawSnapshot | null = null): FlowResponse { const task = runtimeTasks(snapshot).find((candidate) => candidate.id === taskId); if (!task) { return buildPlanFlow(taskId, snapshot); } const plan = task.planId ? runtimePlans(snapshot).find((candidate) => candidate.id === task.planId) : null; const runs = runtimeRuns(snapshot).filter((run) => run.taskId === task.id); const events = runtimeEvents(snapshot).filter((event) => event.taskId === task.id || runs.some((run) => run.id === event.runId)); return { plan_id: task.planId ?? task.id, plan_title: plan?.title ?? task.title, plan_status: task.status, plan_objective: plan?.objective ?? 'qimingclaw 本地任务记录', lanes: [ { key: 'task', label: '任务', color: '#38bdf8' }, { key: 'execution', label: '执行', color: '#22c55e' }, { key: 'event', label: '事件', color: '#f59e0b' }, ], steps: [ { seq: 1, lane: 'task', label: task.title, sub_label: `${taskStatusLabel(task.status)} · ${task.assignedSkillId || 'qimingclaw-runtime'}`, timestamp: task.updatedAt, color: '#38bdf8', source_type: 'task', }, ...runs.map((run, index) => ({ seq: index + 2, lane: 'execution', label: run.status || 'Run', sub_label: run.id, timestamp: run.finishedAt ?? run.startedAt ?? run.createdAt, color: '#22c55e', source_type: 'run', })), ...events.map((event, index) => ({ seq: runs.length + index + 2, lane: 'event', label: event.kind, sub_label: event.message, timestamp: event.occurredAt, color: '#f59e0b', source_type: 'event', })), ], events: events.map((event) => ({ event_id: event.id, kind: event.kind, occurred_at: event.occurredAt, message: event.message, })), }; } async function dispatchPlan( planId: string, snapshot: QimingclawSnapshot | null = null, options: { recordCommand?: boolean } = {}, ): Promise { const state = snapshot?.uiState ?? await readBridgeUiState() ?? readState(); const date = todayInputDate(); const plan = mergedDebugPlans(snapshot).find((candidate) => candidate.plan_id === planId); const activatedAt = Date.now(); const correlationId = `dispatch-${planId}-${activatedAt}`; const selected = new Set(state.selectedDutyIdsByDate[date] ?? []); selected.add(planId); state.selectedDutyIdsByDate[date] = [...selected]; state.confirmedDates[date] = state.confirmedDates[date] ?? new Date().toISOString(); await saveState(state, 'confirm_workday', date); const dispatchedTasks = buildTasks(planId, snapshot).map((task) => ({ task_id: task.task_id, title: task.title, status: task.status === 'pending' ? 'running' : task.status, })); if (options.recordCommand !== false) { await recordGovernanceCommand( { command_kind: 'activate_plan', context_refs: { plan_id: planId }, payload: { plan_id: planId, title: plan?.title, objective: plan?.objective, requested_action: 'dispatch_plan', }, correlation_id: correlationId, }, governanceAccepted( `qimingclaw-digital-dispatch-${slugifyIdPart(planId)}-${activatedAt}`, correlationId, '已将数字员工模板执行写入 qimingclaw 本地运行记录。', { plan_id: planId, status: 'active', dispatched_tasks: dispatchedTasks, source: 'qimingclaw-adapter', }, ), ); } return { ok: true, plan_id: planId, source_plan_id: planId, dispatched_tasks: dispatchedTasks, }; } async function dispatchPlanReadySteps( planId: string, snapshot: QimingclawSnapshot | null = null, ): Promise { const targetPlanId = stringValue(planId); const bridgeResult = targetPlanId ? await window.QimingClawBridge?.digital?.dispatchPlanReadySteps?.(targetPlanId).catch(() => null) : null; if (bridgeResult) return bridgeResult; const now = new Date().toISOString(); const orchestrationId = `manual-ready-steps-${slugifyIdPart(targetPlanId || 'unknown')}-${slugifyIdPart(now)}`; const rows = planStepRows(snapshot).filter((row) => stringValue(row.plan_id) === targetPlanId && stringValue(row.status).toLowerCase() === 'ready'); const state = notificationAdapterState(snapshot); const policy = planStepDispatchPolicy(state); const result: ReadyStepDispatchResult = { ok: true, plan_id: targetPlanId || null, trigger_source: 'manual', orchestration_id: orchestrationId, candidates: rows.length, dispatched: 0, skipped: 0, failed: 0, dispatched_step_ids: [], skipped_step_ids: [], failed_step_ids: [], errors: [], }; if (!targetPlanId || !policy.enabled) { result.skipped = rows.length; result.skipped_step_ids = rows.map((row) => stringValue(row.step_id)).filter(Boolean); return result; } const maxPerSweep = Math.max(1, Math.min(Math.floor(policy.max_per_sweep), 10)); let attempts = 0; for (const row of rows) { const stepId = stringValue(row.step_id); const taskId = stringValue(row.task_id); if (attempts >= maxPerSweep || row.dispatchable !== true || !taskId) { result.skipped += 1; if (stepId) result.skipped_step_ids?.push(stepId); continue; } attempts += 1; try { const runId = stringValue(row.latest_run_id) || `${taskId}:run:start_run:${slugifyIdPart(orchestrationId)}`; const commandId = `manual-ready-step-${slugifyIdPart(stepId)}-${Date.now()}`; await recordGovernanceCommand( { command_kind: 'start_run', context_refs: { plan_id: targetPlanId, task_id: taskId, run_id: runId, step_id: stepId }, payload: { source: 'digital-home-plan-orchestration', plan_id: targetPlanId, task_id: taskId, run_id: runId, step_id: stepId, trigger_source: 'manual', orchestration_id: orchestrationId, candidate_count: rows.length, }, correlation_id: commandId, }, governanceAccepted(commandId, commandId, '已手动推进 Ready PlanStep。', { plan_id: targetPlanId, task_id: taskId, run_id: runId, step_id: stepId, status: 'running', trigger_source: 'manual', orchestration_id: orchestrationId, source: 'qimingclaw-plan-orchestration', }), ); result.dispatched += 1; if (stepId) result.dispatched_step_ids?.push(stepId); } catch (error) { result.failed += 1; if (stepId) result.failed_step_ids?.push(stepId); result.errors?.push({ stepId, error: error instanceof Error ? error.message : 'dispatch failed' }); } } return result; } function buildPlanMessages(planId: string, snapshot: QimingclawSnapshot | null = null): Array<{ role: string; content: string; created_at?: string | null }> { const runtimePlan = runtimePlans(snapshot).find((candidate) => candidate.id === planId); if (runtimePlan) { const events = runtimeEvents(snapshot).filter((event) => event.planId === planId).slice(0, 12); return [ { role: 'user', content: `查看 qimingclaw 本地任务:${runtimePlan.title}`, created_at: runtimePlan.createdAt, }, ...events.map((event) => ({ role: 'assistant', content: `${event.kind}:${event.message}`, created_at: event.occurredAt, })), ]; } const plan = templatePlans(snapshot).find((candidate) => candidate.plan_id === planId) ?? templatePlans(snapshot)[0]!; return [ { role: 'user', content: `执行数字员工任务:${plan.title}`, created_at: plan.created_at, }, { role: 'assistant', content: `已接入 qimingclaw adapter。目标:${plan.objective ?? plan.title}`, created_at: new Date().toISOString(), }, ]; }