import { getDb, readSetting, writeSetting } from "../../db"; const STATE_KEY = "digital_employee_state_v1"; const UI_STATE_KEY = "digital_employee_ui_state_v1"; const MAX_EVENTS = 200; export interface DigitalEmployeeServiceStatus { running?: boolean; error?: string; engineType?: string | null; port?: number; serverCount?: number; serverNames?: string[]; pid?: number; } export interface DigitalEmployeeSessionSummary { total: number; active: number; items: Array<{ id: string; title?: string; status?: string; engineType?: string; projectId?: string; }>; } export interface DigitalEmployeeSnapshot { generatedAt: string; services: Record; sessions: DigitalEmployeeSessionSummary; } export interface DigitalEmployeePlanRecord { 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; } export interface DigitalEmployeeTaskRecord { 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; } export interface DigitalEmployeeRunRecord { 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; } export interface DigitalEmployeeEventRecord { 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; } export interface DigitalEmployeeArtifactRecord { 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; } export interface DigitalEmployeeApprovalRecord { 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; } export interface DigitalEmployeeRuntimeRecords { generatedAt: string; plans: DigitalEmployeePlanRecord[]; tasks: DigitalEmployeeTaskRecord[]; runs: DigitalEmployeeRunRecord[]; events: DigitalEmployeeEventRecord[]; artifacts: DigitalEmployeeArtifactRecord[]; approvals: DigitalEmployeeApprovalRecord[]; } export interface DigitalEmployeeUiState { selectedDutyIdsByDate: Record; confirmedDates: Record; reportScheduleTime: string; reportGeneratedAtByDate: Record; decisionResultsByDate: Record>; profile: { name: string; company: string; position: string; currentStatus: string; }; } export interface DigitalEmployeeUiStateUpdate { state: DigitalEmployeeUiState; action?: string; date?: string; metadata?: Record; } export interface DigitalEmployeeStateEvent { event_id: string; kind: string; occurred_at: string; message: string; plan_id: string | null; task_id: string | null; } export interface DigitalEmployeeState { version: 1; updatedAt: string | null; lastSnapshot: DigitalEmployeeSnapshot | null; events: DigitalEmployeeStateEvent[]; pendingSyncCount?: number; } export interface DigitalEmployeeChatRequestRecord { user_id?: string; project_id?: string; prompt?: string; session_id?: string; request_id?: string; model_provider?: { provider?: string; model?: string; default_model?: string }; } export interface DigitalEmployeeChatResultRecord { success: boolean; code?: string; message?: string; project_id?: string; session_id?: string; request_id?: string; engine?: string; } export interface DigitalEmployeeRuntimeEventRecord { kind: string; message?: string; request_id?: string; session_id?: string; project_id?: string; status?: "running" | "completed" | "failed" | "pending"; payload?: unknown; } function defaultState(): DigitalEmployeeState { return { version: 1, updatedAt: null, lastSnapshot: null, events: [], }; } function defaultUiState(): DigitalEmployeeUiState { return { selectedDutyIdsByDate: {}, confirmedDates: {}, reportScheduleTime: "18:00", reportGeneratedAtByDate: {}, decisionResultsByDate: {}, profile: { name: "飞天数字员工", company: "qimingclaw 客户端", position: "客户端任务执行员", currentStatus: "working", }, }; } function normalizeStringMap(value: unknown): Record { if (!value || typeof value !== "object" || Array.isArray(value)) return {}; return Object.fromEntries( Object.entries(value as Record) .filter((entry): entry is [string, string] => typeof entry[0] === "string" && typeof entry[1] === "string", ), ); } function normalizeStringArrayMap(value: unknown): Record { if (!value || typeof value !== "object" || Array.isArray(value)) return {}; return Object.fromEntries( Object.entries(value as Record).map(([key, list]) => [ key, Array.isArray(list) ? list.filter((item): item is string => typeof item === "string") : [], ]), ); } function normalizeNestedStringMap(value: unknown): Record> { if (!value || typeof value !== "object" || Array.isArray(value)) return {}; return Object.fromEntries( Object.entries(value as Record).map(([key, map]) => [ key, normalizeStringMap(map), ]), ); } function normalizeUiState( value: Partial, ): DigitalEmployeeUiState { const fallback = defaultUiState(); const profile: Partial = value.profile || {}; return { selectedDutyIdsByDate: normalizeStringArrayMap(value.selectedDutyIdsByDate), confirmedDates: normalizeStringMap(value.confirmedDates), reportScheduleTime: typeof value.reportScheduleTime === "string" && value.reportScheduleTime.trim() ? value.reportScheduleTime : fallback.reportScheduleTime, reportGeneratedAtByDate: normalizeStringMap(value.reportGeneratedAtByDate), decisionResultsByDate: normalizeNestedStringMap(value.decisionResultsByDate), profile: { name: typeof profile.name === "string" && profile.name.trim() ? profile.name : fallback.profile.name, company: typeof profile.company === "string" && profile.company.trim() ? profile.company : fallback.profile.company, position: typeof profile.position === "string" && profile.position.trim() ? profile.position : fallback.profile.position, currentStatus: typeof profile.currentStatus === "string" && profile.currentStatus.trim() ? profile.currentStatus : fallback.profile.currentStatus, }, }; } function digitalEmployeeUiActionMessage(action: string, date?: string): string { const suffix = date ? `(${date})` : ""; switch (action) { case "confirm_workday": return `数字员工今日任务已确认${suffix}`; case "update_profile": return "数字员工资料已更新"; case "update_report_schedule": return `数字员工日报时间已更新${suffix}`; case "generate_daily_report": return `数字员工日报已生成${suffix}`; case "decide_approval": return `数字员工待决策事项已处理${suffix}`; default: return `数字员工页面状态已更新${suffix}`; } } export function readDigitalEmployeeState(): DigitalEmployeeState { const state = readSetting(STATE_KEY); const pendingSyncCount = countPendingSyncItems(); if (!state || typeof state !== "object") { return { ...defaultState(), pendingSyncCount }; } return { ...defaultState(), ...(state as Partial), version: 1, pendingSyncCount, }; } export function writeDigitalEmployeeState(state: DigitalEmployeeState): void { writeSetting(STATE_KEY, state); } export function readDigitalEmployeeUiState(): DigitalEmployeeUiState { const state = readSetting(UI_STATE_KEY); if (!state || typeof state !== "object") return defaultUiState(); return normalizeUiState(state as Partial); } export function saveDigitalEmployeeUiState( update: DigitalEmployeeUiStateUpdate, ): DigitalEmployeeUiState { const state = normalizeUiState(update.state); writeSetting(UI_STATE_KEY, state); recordUiApprovalDecision(update); if (update.action) { recordDigitalEmployeeRuntimeEvent({ kind: `digital_workday_${update.action}`, message: digitalEmployeeUiActionMessage(update.action, update.date), status: "completed", payload: { action: update.action, date: update.date, metadata: update.metadata, state, }, }); } return state; } function recordUiApprovalDecision(update: DigitalEmployeeUiStateUpdate): void { if (update.action !== "decide_approval") return; const metadata = objectRecord(update.metadata); const decisionId = stringValue(metadata?.decision_id); const decision = stringValue(metadata?.decision); if (!decisionId || !decision) return; const db = getDb(); if (!db) return; const existing = db.prepare(` SELECT id, plan_id, task_id, run_id, title, status, payload, created_at FROM digital_approvals WHERE id = ? `).get(decisionId) as Record | undefined; if (!existing) return; const now = new Date().toISOString(); const previousStatus = stringField(existing, "status") || "pending"; const storedPayload = parseStoredPayload(existing["payload"]); const storedPayloadRecord = objectRecord(storedPayload); const payload = storedPayloadRecord ? { ...storedPayloadRecord, decision, decidedAt: now, previousStatus, } : { value: storedPayload, decision, decidedAt: now, previousStatus, }; upsertApproval({ id: decisionId, planId: stringField(existing, "plan_id"), taskId: stringField(existing, "task_id"), runId: stringField(existing, "run_id"), title: stringField(existing, "title") || "待确认事项", status: approvalDecisionStatus(decision), payload, now, }); } function approvalDecisionStatus(decision: string): string { const normalized = decision.toLowerCase(); if (["approve", "approved", "accept", "accepted"].includes(normalized)) return "approved"; if (["reject", "rejected", "deny", "denied"].includes(normalized)) return "rejected"; if (["dismiss", "dismissed", "skip", "skipped"].includes(normalized)) return "dismissed"; return "completed"; } export function readDigitalEmployeeRuntimeRecords( limit = 120, ): DigitalEmployeeRuntimeRecords { const db = getDb(); if (!db) { return { generatedAt: new Date().toISOString(), plans: [], tasks: [], runs: [], events: [], artifacts: [], approvals: [], }; } const safeLimit = Math.max(1, Math.min(Math.floor(limit), 300)); const plans = db.prepare(` SELECT id, remote_id, title, objective, status, source, payload, sync_status, last_synced_at, sync_error, created_at, updated_at FROM digital_plans ORDER BY updated_at DESC, created_at DESC LIMIT ? `).all(safeLimit) as Array>; const tasks = db.prepare(` SELECT id, remote_id, plan_id, title, status, assigned_skill_id, payload, sync_status, last_synced_at, sync_error, created_at, updated_at FROM digital_tasks ORDER BY updated_at DESC, created_at DESC LIMIT ? `).all(safeLimit) as Array>; const runs = db.prepare(` SELECT id, remote_id, plan_id, task_id, status, started_at, finished_at, payload, sync_status, last_synced_at, sync_error, created_at, updated_at FROM digital_runs ORDER BY updated_at DESC, created_at DESC LIMIT ? `).all(safeLimit) as Array>; const events = db.prepare(` SELECT id, remote_id, plan_id, task_id, run_id, kind, message, payload, sync_status, last_synced_at, sync_error, occurred_at, created_at FROM digital_events ORDER BY occurred_at DESC, created_at DESC LIMIT ? `).all(safeLimit) as Array>; const artifacts = db.prepare(` SELECT id, remote_id, plan_id, task_id, run_id, kind, name, uri, payload, sync_status, last_synced_at, sync_error, created_at FROM digital_artifacts ORDER BY created_at DESC LIMIT ? `).all(safeLimit) as Array>; const approvals = db.prepare(` SELECT id, remote_id, plan_id, task_id, run_id, title, status, payload, sync_status, last_synced_at, sync_error, created_at, updated_at FROM digital_approvals ORDER BY updated_at DESC, created_at DESC LIMIT ? `).all(safeLimit) as Array>; return { generatedAt: new Date().toISOString(), plans: plans.map(toPlanRecord), tasks: tasks.map(toTaskRecord), runs: runs.map(toRunRecord), events: events.map(toEventRecord), artifacts: artifacts.map(toArtifactRecord), approvals: approvals.map(toApprovalRecord), }; } export function recordDigitalEmployeeSnapshot( snapshot: DigitalEmployeeSnapshot, ): DigitalEmployeeState { const previous = readDigitalEmployeeState(); const events = [...previous.events]; const event = eventFromSnapshot(snapshot, previous.lastSnapshot); if (event) { events.unshift(event); } const next: DigitalEmployeeState = { version: 1, updatedAt: snapshot.generatedAt, lastSnapshot: snapshot, events: events.slice(0, MAX_EVENTS), }; writeDigitalEmployeeState(next); persistSnapshotTables(snapshot, event); return next; } export function recordDigitalEmployeeChatReceived( request: DigitalEmployeeChatRequestRecord, ): { planId: string; taskId: string; runId: string } { const now = new Date().toISOString(); const ids = chatIds(request); const title = chatTitle(request); const db = getDb(); if (!db) return ids; const tx = db.transaction(() => { upsertPlan({ id: ids.planId, title: `远程任务:${title}`, objective: request.prompt || request.project_id || "管理端下发任务", status: "running", payload: request, now, }); upsertTask({ id: ids.taskId, planId: ids.planId, title, status: "running", assignedSkillId: "qimingclaw-computer-control", payload: request, now, }); upsertRun({ id: ids.runId, planId: ids.planId, taskId: ids.taskId, status: "running", payload: { request }, now, }); persistPayloadArtifactsAndApprovals(ids, request, now); insertEvent( { event_id: `${ids.runId}:received`, kind: "computer_chat_received", occurred_at: now, message: `收到管理端任务:${title}`, plan_id: ids.planId, task_id: ids.taskId, }, ids.runId, { request }, ); }); tx(); return ids; } export function recordDigitalEmployeeChatResult( request: DigitalEmployeeChatRequestRecord, result: DigitalEmployeeChatResultRecord, ): void { const now = new Date().toISOString(); const ids = chatIds({ ...request, request_id: request.request_id || result.request_id, session_id: request.session_id || result.session_id, project_id: request.project_id || result.project_id, }); const status = result.success ? "completed" : "failed"; const title = chatTitle(request); const db = getDb(); if (!db) return; const tx = db.transaction(() => { upsertPlan({ id: ids.planId, title: `远程任务:${title}`, objective: request.prompt || request.project_id || "管理端下发任务", status, payload: { request, result }, now, }); upsertTask({ id: ids.taskId, planId: ids.planId, title, status, assignedSkillId: "qimingclaw-computer-control", payload: { request, result }, now, }); upsertRun({ id: ids.runId, planId: ids.planId, taskId: ids.taskId, status, payload: { request, result }, now, }); finishRun(ids.runId, status, now); persistPayloadArtifactsAndApprovals(ids, { request, result }, now); insertEvent( { event_id: `${ids.runId}:${status}`, kind: result.success ? "computer_chat_completed" : "computer_chat_failed", occurred_at: now, message: result.success ? `管理端任务完成:session=${result.session_id || request.session_id || "unknown"}` : `管理端任务失败:${result.message || result.code || "unknown error"}`, plan_id: ids.planId, task_id: ids.taskId, }, ids.runId, { request, result }, ); }); tx(); } export function recordDigitalEmployeeRuntimeEvent( event: DigitalEmployeeRuntimeEventRecord, ): void { const now = new Date().toISOString(); const ids = chatIds({ request_id: event.request_id, session_id: event.session_id, project_id: event.project_id, prompt: event.message, }); const status = event.status ?? "running"; const title = event.message || event.kind; const db = getDb(); if (!db) return; const tx = db.transaction(() => { upsertPlan({ id: ids.planId, title: `运行事件:${title}`, objective: title, status, payload: event.payload ?? event, now, }); upsertTask({ id: ids.taskId, planId: ids.planId, title, status, assignedSkillId: "qimingclaw-acp-session", payload: event.payload ?? event, now, }); upsertRun({ id: ids.runId, planId: ids.planId, taskId: ids.taskId, status, payload: event.payload ?? event, now, }); if (status === "completed" || status === "failed") { finishRun(ids.runId, status, now); } persistPayloadArtifactsAndApprovals(ids, event.payload ?? event, now); insertEvent( { event_id: `${ids.runId}:${event.kind}:${Date.now()}`, kind: event.kind, occurred_at: now, message: title, plan_id: ids.planId, task_id: ids.taskId, }, ids.runId, event.payload ?? event, ); }); tx(); } function persistSnapshotTables( snapshot: DigitalEmployeeSnapshot, event: DigitalEmployeeStateEvent | null, ): void { const db = getDb(); if (!db) return; const now = snapshot.generatedAt; const planStatus = snapshot.services.agent?.running || snapshot.services.mcp?.running ? "running" : "pending"; const taskStatus = snapshot.services.agent?.running ? "running" : "pending"; const runStatus = planStatus === "running" ? "running" : "pending"; const planPayload = { services: snapshot.services, sessions: snapshot.sessions, }; const taskPayload = { service: "agent", agent: snapshot.services.agent, mcp: snapshot.services.mcp, }; const runPayload = { snapshot, }; const tx = db.transaction(() => { upsertPlan({ id: "qimingclaw-client-readiness", title: "客户端运行状态巡检", objective: "确认 qimingclaw 客户端、Agent、MCP 和远程接入能力是否处于可执行状态。", status: planStatus, payload: planPayload, now, }); upsertTask({ id: "task-agent-status", planId: "qimingclaw-client-readiness", title: "读取 Agent 服务状态", status: taskStatus, assignedSkillId: "qimingclaw-acp-session", payload: taskPayload, now, }); upsertRun({ id: "run-service-snapshot", planId: "qimingclaw-client-readiness", taskId: "task-agent-status", status: runStatus, payload: runPayload, now, }); if (event) { insertEvent(event, "run-service-snapshot", { snapshot }); } }); tx(); } function stringify(value: unknown): string { return JSON.stringify(value ?? {}); } function stringField(row: Record, key: string): string { const value = row[key]; return typeof value === "string" ? value : ""; } function optionalStringField( row: Record, key: string, ): string | null { const value = row[key]; return typeof value === "string" && value.trim() ? value : null; } function parseStoredPayload(value: unknown): unknown { if (typeof value !== "string" || !value.trim()) return {}; try { return JSON.parse(value); } catch { return value; } } function toPlanRecord(row: Record): DigitalEmployeePlanRecord { return { id: stringField(row, "id"), remoteId: optionalStringField(row, "remote_id"), title: stringField(row, "title"), objective: optionalStringField(row, "objective"), status: stringField(row, "status"), source: stringField(row, "source"), payload: parseStoredPayload(row.payload), syncStatus: stringField(row, "sync_status"), lastSyncedAt: optionalStringField(row, "last_synced_at"), syncError: optionalStringField(row, "sync_error"), createdAt: stringField(row, "created_at"), updatedAt: stringField(row, "updated_at"), }; } function toTaskRecord(row: Record): DigitalEmployeeTaskRecord { return { id: stringField(row, "id"), remoteId: optionalStringField(row, "remote_id"), planId: optionalStringField(row, "plan_id"), title: stringField(row, "title"), status: stringField(row, "status"), assignedSkillId: optionalStringField(row, "assigned_skill_id"), payload: parseStoredPayload(row.payload), syncStatus: stringField(row, "sync_status"), lastSyncedAt: optionalStringField(row, "last_synced_at"), syncError: optionalStringField(row, "sync_error"), createdAt: stringField(row, "created_at"), updatedAt: stringField(row, "updated_at"), }; } function toRunRecord(row: Record): DigitalEmployeeRunRecord { return { id: stringField(row, "id"), remoteId: optionalStringField(row, "remote_id"), planId: optionalStringField(row, "plan_id"), taskId: optionalStringField(row, "task_id"), status: stringField(row, "status"), startedAt: optionalStringField(row, "started_at"), finishedAt: optionalStringField(row, "finished_at"), payload: parseStoredPayload(row.payload), syncStatus: stringField(row, "sync_status"), lastSyncedAt: optionalStringField(row, "last_synced_at"), syncError: optionalStringField(row, "sync_error"), createdAt: stringField(row, "created_at"), updatedAt: stringField(row, "updated_at"), }; } function toEventRecord(row: Record): DigitalEmployeeEventRecord { return { id: stringField(row, "id"), remoteId: optionalStringField(row, "remote_id"), planId: optionalStringField(row, "plan_id"), taskId: optionalStringField(row, "task_id"), runId: optionalStringField(row, "run_id"), kind: stringField(row, "kind"), message: stringField(row, "message"), payload: parseStoredPayload(row.payload), syncStatus: stringField(row, "sync_status"), lastSyncedAt: optionalStringField(row, "last_synced_at"), syncError: optionalStringField(row, "sync_error"), occurredAt: stringField(row, "occurred_at"), createdAt: stringField(row, "created_at"), }; } function toArtifactRecord( row: Record, ): DigitalEmployeeArtifactRecord { return { id: stringField(row, "id"), remoteId: optionalStringField(row, "remote_id"), planId: optionalStringField(row, "plan_id"), taskId: optionalStringField(row, "task_id"), runId: optionalStringField(row, "run_id"), kind: stringField(row, "kind"), name: optionalStringField(row, "name"), uri: optionalStringField(row, "uri"), payload: parseStoredPayload(row.payload), syncStatus: stringField(row, "sync_status"), lastSyncedAt: optionalStringField(row, "last_synced_at"), syncError: optionalStringField(row, "sync_error"), createdAt: stringField(row, "created_at"), }; } function toApprovalRecord( row: Record, ): DigitalEmployeeApprovalRecord { return { id: stringField(row, "id"), remoteId: optionalStringField(row, "remote_id"), planId: optionalStringField(row, "plan_id"), taskId: optionalStringField(row, "task_id"), runId: optionalStringField(row, "run_id"), title: stringField(row, "title"), status: stringField(row, "status"), payload: parseStoredPayload(row.payload), syncStatus: stringField(row, "sync_status"), lastSyncedAt: optionalStringField(row, "last_synced_at"), syncError: optionalStringField(row, "sync_error"), createdAt: stringField(row, "created_at"), updatedAt: stringField(row, "updated_at"), }; } function countPendingSyncItems(): number { const db = getDb(); if (!db) return 0; const row = db.prepare(` SELECT COUNT(*) AS count FROM digital_sync_outbox WHERE status = 'pending' `).get() as { count: number } | undefined; return row?.count ?? 0; } function syncOutboxId(entityType: string, entityId: string, operation = "upsert"): string { return `${entityType}:${operation}:${entityId}`; } function markOutbox( entityType: string, entityId: string, operation: string, payload: unknown, now: string, ): void { const db = getDb(); if (!db) return; db.prepare(` INSERT INTO digital_sync_outbox ( id, entity_type, entity_id, operation, payload, status, attempts, created_at, updated_at ) VALUES (?, ?, ?, ?, ?, 'pending', 0, ?, ?) ON CONFLICT(id) DO UPDATE SET payload = excluded.payload, status = 'pending', updated_at = excluded.updated_at, error = NULL `).run( syncOutboxId(entityType, entityId, operation), entityType, entityId, operation, stringify(payload), now, now, ); } function upsertPlan(input: { id: string; title: string; objective: string; status: string; payload: unknown; now: string; }): void { const db = getDb(); if (!db) return; db.prepare(` INSERT INTO digital_plans ( id, title, objective, status, source, payload, sync_status, created_at, updated_at ) VALUES (?, ?, ?, ?, 'qimingclaw', ?, 'pending', ?, ?) ON CONFLICT(id) DO UPDATE SET title = excluded.title, objective = excluded.objective, status = excluded.status, payload = excluded.payload, sync_status = CASE WHEN digital_plans.sync_status = 'synced' THEN 'pending' ELSE digital_plans.sync_status END, updated_at = excluded.updated_at `).run( input.id, input.title, input.objective, input.status, stringify(input.payload), input.now, input.now, ); markOutbox("plan", input.id, "upsert", input, input.now); } function upsertTask(input: { id: string; planId: string; title: string; status: string; assignedSkillId: string; payload: unknown; now: string; }): void { const db = getDb(); if (!db) return; db.prepare(` INSERT INTO digital_tasks ( id, plan_id, title, status, assigned_skill_id, payload, sync_status, created_at, updated_at ) VALUES (?, ?, ?, ?, ?, ?, 'pending', ?, ?) ON CONFLICT(id) DO UPDATE SET plan_id = excluded.plan_id, title = excluded.title, status = excluded.status, assigned_skill_id = excluded.assigned_skill_id, payload = excluded.payload, sync_status = CASE WHEN digital_tasks.sync_status = 'synced' THEN 'pending' ELSE digital_tasks.sync_status END, updated_at = excluded.updated_at `).run( input.id, input.planId, input.title, input.status, input.assignedSkillId, stringify(input.payload), input.now, input.now, ); markOutbox("task", input.id, "upsert", input, input.now); } function upsertRun(input: { id: string; planId: string; taskId: string; status: string; payload: unknown; now: string; }): void { const db = getDb(); if (!db) return; db.prepare(` INSERT INTO digital_runs ( id, plan_id, task_id, status, started_at, payload, sync_status, created_at, updated_at ) VALUES (?, ?, ?, ?, ?, ?, 'pending', ?, ?) ON CONFLICT(id) DO UPDATE SET plan_id = excluded.plan_id, task_id = excluded.task_id, status = excluded.status, payload = excluded.payload, sync_status = CASE WHEN digital_runs.sync_status = 'synced' THEN 'pending' ELSE digital_runs.sync_status END, updated_at = excluded.updated_at `).run( input.id, input.planId, input.taskId, input.status, input.now, stringify(input.payload), input.now, input.now, ); markOutbox("run", input.id, "upsert", input, input.now); } function finishRun(runId: string, status: string, now: string): void { const db = getDb(); if (!db) return; db.prepare(` UPDATE digital_runs SET status = ?, finished_at = ?, updated_at = ?, sync_status = 'pending' WHERE id = ? `).run(status, now, now, runId); } function upsertArtifact(input: { id: string; planId: string; taskId: string; runId: string; kind: string; name: string | null; uri: string | null; payload: unknown; now: string; }): void { const db = getDb(); if (!db) return; db.prepare(` INSERT INTO digital_artifacts ( id, plan_id, task_id, run_id, kind, name, uri, payload, sync_status, created_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'pending', ?) ON CONFLICT(id) DO UPDATE SET plan_id = excluded.plan_id, task_id = excluded.task_id, run_id = excluded.run_id, kind = excluded.kind, name = excluded.name, uri = excluded.uri, payload = excluded.payload, sync_status = CASE WHEN digital_artifacts.sync_status = 'synced' THEN 'pending' ELSE digital_artifacts.sync_status END `).run( input.id, input.planId, input.taskId, input.runId, input.kind, input.name, input.uri, stringify(input.payload), input.now, ); markOutbox("artifact", input.id, "upsert", input, input.now); } function upsertApproval(input: { id: string; planId: string; taskId: string; runId: string; title: string; status: string; payload: unknown; now: string; }): void { const db = getDb(); if (!db) return; db.prepare(` INSERT INTO digital_approvals ( id, plan_id, task_id, run_id, title, status, payload, sync_status, created_at, updated_at ) VALUES (?, ?, ?, ?, ?, ?, ?, 'pending', ?, ?) ON CONFLICT(id) DO UPDATE SET plan_id = excluded.plan_id, task_id = excluded.task_id, run_id = excluded.run_id, title = excluded.title, status = excluded.status, payload = excluded.payload, sync_status = CASE WHEN digital_approvals.sync_status = 'synced' THEN 'pending' ELSE digital_approvals.sync_status END, updated_at = excluded.updated_at `).run( input.id, input.planId, input.taskId, input.runId, input.title, input.status, stringify(input.payload), input.now, input.now, ); markOutbox("approval", input.id, "upsert", input, input.now); } function insertEvent( event: DigitalEmployeeStateEvent, runId: string, payload: unknown, ): void { const db = getDb(); if (!db) return; db.prepare(` INSERT OR IGNORE INTO digital_events ( id, plan_id, task_id, run_id, kind, message, payload, sync_status, occurred_at, created_at ) VALUES (?, ?, ?, ?, ?, ?, ?, 'pending', ?, ?) `).run( event.event_id, event.plan_id, event.task_id, runId, event.kind, event.message, stringify(payload), event.occurred_at, event.occurred_at, ); markOutbox("event", event.event_id, "insert", event, event.occurred_at); } function persistPayloadArtifactsAndApprovals( ids: { planId: string; taskId: string; runId: string }, payload: unknown, now: string, ): void { const sources = payloadSources(payload); const artifacts = sources.flatMap((source) => extractArtifacts(source)); const approvals = sources.flatMap((source) => extractApprovals(source)); dedupeByKey(artifacts, (artifact) => artifact.key).slice(0, 20).forEach((artifact, index) => { upsertArtifact({ id: `${ids.runId}:artifact:${safeId(artifact.key || String(index + 1))}`, planId: ids.planId, taskId: ids.taskId, runId: ids.runId, kind: artifact.kind, name: artifact.name, uri: artifact.uri, payload: artifact.payload, now, }); }); dedupeByKey(approvals, (approval) => approval.key).slice(0, 20).forEach((approval, index) => { upsertApproval({ id: `${ids.runId}:approval:${safeId(approval.key || String(index + 1))}`, planId: ids.planId, taskId: ids.taskId, runId: ids.runId, title: approval.title, status: approval.status, payload: approval.payload, now, }); }); } function payloadSources(payload: unknown): unknown[] { const record = objectRecord(payload); if (!record) return [payload]; return [ payload, record.result, record.response, record.output, record.payload, ].filter((item) => item !== undefined && item !== null); } interface ExtractedArtifact { key: string; kind: string; name: string | null; uri: string | null; payload: unknown; } function extractArtifacts(payload: unknown): ExtractedArtifact[] { const record = objectRecord(payload); if (!record) return typeof payload === "string" && looksLikeArtifactUri(payload) ? [artifactFromValue(payload, 0)] : []; const values = [ ...unknownArray(record.artifacts), ...unknownArray(record.outputs), ...unknownArray(record.files), ...unknownArray(record.attachments), ]; for (const key of ["artifact", "outputPath", "output_path", "filePath", "file_path", "path", "uri", "url"]) { if (record[key]) values.push(record[key]); } return values .map((value, index) => artifactFromValue(value, index)) .filter((artifact) => artifact.name || artifact.uri); } function artifactFromValue(value: unknown, index: number): ExtractedArtifact { const record = objectRecord(value); if (record) { 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) || artifactName(uri) || `产物 ${index + 1}`; const kind = stringValue(record.kind ?? record.type) || "artifact"; return { key: stringValue(record.id ?? record.artifact_id) || uri || name, kind, name, uri: uri || null, payload: value, }; } const text = String(value ?? "").trim(); return { key: text, kind: "artifact", name: artifactName(text) || `产物 ${index + 1}`, uri: looksLikeArtifactUri(text) ? text : null, payload: value, }; } interface ExtractedApproval { key: string; title: string; status: string; payload: unknown; } function extractApprovals(payload: unknown): ExtractedApproval[] { const record = objectRecord(payload); if (!record) return []; const values = [ ...unknownArray(record.approvals), ...unknownArray(record.approval_requests), ...unknownArray(record.approvalRequests), ...unknownArray(record.decisions), ]; if (record.needApproval === true || record.need_approval === true || record.requiresApproval === true) { values.push(record); } return values.map((value, index) => approvalFromValue(value, index)); } function approvalFromValue(value: unknown, index: number): ExtractedApproval { const record = objectRecord(value); if (!record) { return { key: String(index + 1), title: `待确认事项 ${index + 1}`, status: "pending", payload: value, }; } const title = stringValue(record.title ?? record.message ?? record.reason ?? record.summary) || `待确认事项 ${index + 1}`; return { key: stringValue(record.id ?? record.approval_id ?? record.key) || title, title, status: stringValue(record.status) || "pending", payload: value, }; } function objectRecord(value: unknown): Record | null { return value && typeof value === "object" && !Array.isArray(value) ? value as Record : null; } function unknownArray(value: unknown): unknown[] { return Array.isArray(value) ? value : []; } function stringValue(value: unknown): string { return typeof value === "string" ? value.trim() : ""; } function looksLikeArtifactUri(value: string): boolean { return /^(file|https?):\/\//i.test(value) || value.includes("/") || value.includes("\\"); } function artifactName(value: string): string { if (!value) return ""; const normalized = value.split(/[?#]/)[0] || value; return normalized.split(/[\\/]/).filter(Boolean).pop() ?? ""; } function dedupeByKey(items: T[], keyOf: (item: T) => string): T[] { const seen = new Set(); return items.filter((item) => { const key = keyOf(item); if (!key || seen.has(key)) return false; seen.add(key); return true; }); } function chatIds(request: DigitalEmployeeChatRequestRecord): { planId: string; taskId: string; runId: string; } { const base = safeId(request.request_id || request.session_id || request.project_id || `${Date.now()}`); return { planId: `computer-chat-plan-${base}`, taskId: `computer-chat-task-${base}`, runId: `computer-chat-run-${base}`, }; } function chatTitle(request: DigitalEmployeeChatRequestRecord): string { const prompt = (request.prompt || "").replace(/\s+/g, " ").trim(); if (prompt) return prompt.length > 48 ? `${prompt.slice(0, 48)}...` : prompt; return request.project_id ? `项目 ${request.project_id}` : "管理端下发任务"; } function safeId(value: string): string { return value.replace(/[^a-zA-Z0-9_-]+/g, "-").replace(/^-+|-+$/g, "") || "unknown"; } function eventFromSnapshot( snapshot: DigitalEmployeeSnapshot, previous: DigitalEmployeeSnapshot | null, ): DigitalEmployeeStateEvent | null { const currentSummary = summarizeSnapshot(snapshot); const previousSummary = previous ? summarizeSnapshot(previous) : ""; if (currentSummary === previousSummary) { return null; } return { event_id: `qimingclaw-snapshot-${Date.now()}`, kind: "service_snapshot", occurred_at: snapshot.generatedAt, message: currentSummary, plan_id: "qimingclaw-client-readiness", task_id: "task-agent-status", }; } function summarizeSnapshot(snapshot: DigitalEmployeeSnapshot): string { const serviceNames = [ ["agent", "Agent"], ["mcp", "MCP"], ["computerServer", "Computer Server"], ["lanproxy", "Lanproxy"], ["fileServer", "File Server"], ] as const; const services = serviceNames.map(([key, label]) => { const status = snapshot.services[key]; return `${label}:${status?.running ? "running" : "stopped"}`; }); services.push(`sessions:${snapshot.sessions.active}/${snapshot.sessions.total}`); return services.join(";"); }