diff --git a/qimingclaw/crates/agent-electron-client/src/main/ipc/computerHandlers.ts b/qimingclaw/crates/agent-electron-client/src/main/ipc/computerHandlers.ts index 5b295dc0..ea9d3f2f 100644 --- a/qimingclaw/crates/agent-electron-client/src/main/ipc/computerHandlers.ts +++ b/qimingclaw/crates/agent-electron-client/src/main/ipc/computerHandlers.ts @@ -1,5 +1,9 @@ import { ipcMain } from "electron"; import { agentService } from "../services/engines/unifiedAgent"; +import { + recordDigitalEmployeeChatReceived, + recordDigitalEmployeeChatResult, +} from "../services/digitalEmployee/stateService"; import type { ComputerChatRequest, ComputerAgentStatusResponse, @@ -10,11 +14,17 @@ import type { export function registerComputerHandlers(): void { ipcMain.handle("computer:chat", async (_, request: ComputerChatRequest) => { + recordDigitalEmployeeChatReceived(request); // 与 HTTP 路径一致:按 project_id 路由到对应 AcpEngine let acpEngine; try { acpEngine = await agentService.ensureEngineForRequest(request); } catch (err: any) { + recordDigitalEmployeeChatResult(request, { + success: false, + code: "5000", + message: err.message || "Engine switch failed", + }); return { code: "5000", message: err.message || "Engine switch failed", @@ -24,6 +34,11 @@ export function registerComputerHandlers(): void { } as HttpResult; } if (!acpEngine) { + recordDigitalEmployeeChatResult(request, { + success: false, + code: "5000", + message: "Agent not initialized", + }); return { code: "5000", message: "Agent not initialized", @@ -32,7 +47,17 @@ export function registerComputerHandlers(): void { success: false, } as HttpResult; } - return acpEngine.chat(request); + const result = await acpEngine.chat(request); + recordDigitalEmployeeChatResult(request, { + success: result.success, + code: result.code, + message: result.message, + project_id: result.data?.project_id, + session_id: result.data?.session_id, + request_id: result.data?.request_id || request.request_id, + engine: acpEngine.engineName, + }); + return result; }); ipcMain.handle( diff --git a/qimingclaw/crates/agent-electron-client/src/main/services/computerServer.ts b/qimingclaw/crates/agent-electron-client/src/main/services/computerServer.ts index b50aa478..f455cb30 100644 --- a/qimingclaw/crates/agent-electron-client/src/main/services/computerServer.ts +++ b/qimingclaw/crates/agent-electron-client/src/main/services/computerServer.ts @@ -31,6 +31,10 @@ import { firstTokenTrace } from "./engines/perf/firstTokenTrace"; import { checkFileServerHealth } from "./packages/fileServerHealth"; import { LOCALHOST_HOSTNAME } from "./constants"; import { getConfiguredPorts } from "./startupPorts"; +import { + recordDigitalEmployeeChatReceived, + recordDigitalEmployeeChatResult, +} from "./digitalEmployee/stateService"; import type { ComputerChatRequest, HttpResult, @@ -406,6 +410,7 @@ async function handleRequest( let t1: number, t2: number, t3: number, t4: number; const body = (await parseBody(req)) as ComputerChatRequest; + recordDigitalEmployeeChatReceived(body); t1 = Date.now(); firstTokenTrace.trace( "chat.received", @@ -515,6 +520,11 @@ async function handleRequest( try { acpEngine = await agentService.ensureEngineForRequest(body); } catch (err: any) { + recordDigitalEmployeeChatResult(body, { + success: false, + code: "5000", + message: err.message || "Engine switch failed", + }); log.error("❌ [HTTP] Engine switch failed:", err); firstTokenTrace.trace( "chat.failed", @@ -549,6 +559,11 @@ async function handleRequest( getPerfLogger().info(`[PERF] /chat.ensureEngine: ${t3 - t2_5}ms`); if (!acpEngine) { + recordDigitalEmployeeChatResult(body, { + success: false, + code: "5000", + message: "Agent not initialized", + }); log.error("❌ [HTTP] Agent not initialized"); sendJson(res, 200, httpError("5000", "Agent not initialized")); return; @@ -556,6 +571,15 @@ async function handleRequest( // chat() 已返回 HttpResult 格式 const result = await acpEngine.chat(body); + recordDigitalEmployeeChatResult(body, { + success: result.success, + code: result.code, + message: result.message, + project_id: result.data?.project_id, + session_id: result.data?.session_id, + request_id: result.data?.request_id || body.request_id, + engine: acpEngine.engineName, + }); t4 = Date.now(); firstTokenTrace.trace( result.success ? "chat.response.sent" : "chat.failed", diff --git a/qimingclaw/crates/agent-electron-client/src/main/services/digitalEmployee/stateService.ts b/qimingclaw/crates/agent-electron-client/src/main/services/digitalEmployee/stateService.ts index 72bee95a..264b037c 100644 --- a/qimingclaw/crates/agent-electron-client/src/main/services/digitalEmployee/stateService.ts +++ b/qimingclaw/crates/agent-electron-client/src/main/services/digitalEmployee/stateService.ts @@ -48,6 +48,25 @@ export interface DigitalEmployeeState { 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; +} + function defaultState(): DigitalEmployeeState { return { version: 1, @@ -96,6 +115,121 @@ export function recordDigitalEmployeeSnapshot( 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, + }); + 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); + 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(); +} + function persistSnapshotTables( snapshot: DigitalEmployeeSnapshot, event: DigitalEmployeeStateEvent | null, @@ -315,6 +449,16 @@ function upsertRun(input: { 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 insertEvent( event: DigitalEmployeeStateEvent, runId: string, @@ -340,6 +484,29 @@ function insertEvent( markOutbox("event", event.event_id, "insert", event, event.occurred_at); } +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, diff --git a/qimingclaw/docs/digital-employee-frontend-integration-plan.md b/qimingclaw/docs/digital-employee-frontend-integration-plan.md index ebafc686..eec07fe1 100644 --- a/qimingclaw/docs/digital-employee-frontend-integration-plan.md +++ b/qimingclaw/docs/digital-employee-frontend-integration-plan.md @@ -350,6 +350,35 @@ digitalEmployee:getState - 在 computer server 的 `/computer/chat` 入口创建 Task / Run。 - 在 ACP event forwarder 中写入 Run/Event。 +## Phase 5:管理端任务入口落地(已开始) + +目标:管理端或后端通过 `/computer/chat` 下发任务时,qimingclaw 本地数字员工状态可以生成真实任务记录。 + +当前 qimingclaw 已接入: + +```text +crates/agent-electron-client/src/main/services/computerServer.ts +crates/agent-electron-client/src/main/ipc/computerHandlers.ts +crates/agent-electron-client/src/main/services/digitalEmployee/stateService.ts +``` + +记录时点: + +- 收到 chat 请求:创建/更新 Plan、Task、Run,写入 `computer_chat_received`。 +- chat 成功返回:Run 标记 completed,写入 `computer_chat_completed`。 +- chat 失败返回:Run 标记 failed,写入 `computer_chat_failed`。 + +当前联动意义: + +- 管理端任务进入客户端后,本地 SQLite 会立即有可追溯记录。 +- 对应 Plan / Task / Run / Event 均进入 `digital_sync_outbox`。 +- 后续管理端 sync API 确定后,可将 outbox 推送到管理端并回填 `remote_id`。 + +下一步: + +- 接 ACP event forwarder,把 token、tool、step、artifact 写入 Run/Event/Artifact。 +- 增加 sync worker,根据 `step1_config.serverHost` 和登录 token 推送 outbox。 + ## 管理端联动原则 数字员工状态是本地优先,但不能成为本地孤岛。