diff --git a/qimingclaw/crates/agent-electron-client/src/main/services/digitalEmployee/skillCatalogService.test.ts b/qimingclaw/crates/agent-electron-client/src/main/services/digitalEmployee/skillCatalogService.test.ts index b0193f91..26568c06 100644 --- a/qimingclaw/crates/agent-electron-client/src/main/services/digitalEmployee/skillCatalogService.test.ts +++ b/qimingclaw/crates/agent-electron-client/src/main/services/digitalEmployee/skillCatalogService.test.ts @@ -40,10 +40,42 @@ const mockRuntime = vi.hoisted(() => ({ }, occurredAt: "2026-06-07T07:59:00.000Z", }, + { + id: "event-2", + kind: "computer_chat_completed", + message: "管理端任务完成:session=session-b", + payload: { + source: "computer_chat", + projectId: "project-b", + sessionId: "session-b", + status: "completed", + }, + occurredAt: "2026-06-07T08:01:00.000Z", + }, + { + id: "event-3", + kind: "permission.updated", + message: "权限请求:写入文件", + payload: { + source: "acp-permission", + sessionId: "session-a", + status: "pending", + }, + occurredAt: "2026-06-07T08:02:00.000Z", + }, ], }, })); +const mockAgent = vi.hoisted(() => ({ + isReady: true, + engineType: "claude-code-acp", + sessions: [ + { id: "session-a", status: "active", projectId: "project-a" }, + { id: "session-b", status: "idle", projectId: "project-b" }, + ], +})); + vi.mock("../packages/mcp", () => ({ mcpProxyManager: { getStatus: () => mockMcp.status, @@ -55,6 +87,14 @@ vi.mock("./stateService", () => ({ readDigitalEmployeeRuntimeRecords: () => mockRuntime.records, })); +vi.mock("../engines/unifiedAgent", () => ({ + agentService: { + get isReady() { return mockAgent.isReady; }, + getEngineType: () => mockAgent.engineType, + listAllSessionsDetailed: () => mockAgent.sessions, + }, +})); + describe("digital employee skill catalog service", () => { beforeEach(() => { vi.resetModules(); @@ -65,7 +105,7 @@ describe("digital employee skill catalog service", () => { const result = listDigitalEmployeeSkillCapabilities(); - expect(result.skills).toEqual([ + expect(result.skills).toEqual(expect.arrayContaining([ expect.objectContaining({ skill_id: "qimingclaw-mcp-server-crm", server_id: "crm", @@ -92,6 +132,37 @@ describe("digital employee skill catalog service", () => { tools: ["crm.export"], recent_calls: [], }), - ]); + ])); + }); + + it("projects built-in ACP capabilities as digital skills", async () => { + const { listDigitalEmployeeSkillCapabilities } = await import("./skillCatalogService"); + + const result = listDigitalEmployeeSkillCapabilities(); + + expect(result.skills).toEqual(expect.arrayContaining([ + expect.objectContaining({ + skill_id: "qimingclaw-acp-session", + source: "qimingclaw-acp", + server_id: "qimingclaw-acp", + transport: "local", + running: true, + description: expect.stringContaining("活跃会话 1 个"), + tools: ["create_session", "send_prompt", "cancel_session", "list_sessions"], + }), + expect.objectContaining({ + skill_id: "qimingclaw-computer-control", + recent_calls: [expect.objectContaining({ event_id: "event-2" })], + project_ids: ["project-b"], + }), + expect.objectContaining({ + skill_id: "qimingclaw-acp-permission", + recent_calls: [expect.objectContaining({ event_id: "event-3" })], + }), + expect.objectContaining({ + skill_id: "qimingclaw-artifact-reporting", + tool_count: 3, + }), + ])); }); }); diff --git a/qimingclaw/crates/agent-electron-client/src/main/services/digitalEmployee/skillCatalogService.ts b/qimingclaw/crates/agent-electron-client/src/main/services/digitalEmployee/skillCatalogService.ts index 8acf7d69..f0f2f395 100644 --- a/qimingclaw/crates/agent-electron-client/src/main/services/digitalEmployee/skillCatalogService.ts +++ b/qimingclaw/crates/agent-electron-client/src/main/services/digitalEmployee/skillCatalogService.ts @@ -1,5 +1,6 @@ import { mcpProxyManager } from "../packages/mcp"; import type { McpServerEntry } from "../packages/mcp"; +import { agentService } from "../engines/unifiedAgent"; import { readDigitalEmployeeRuntimeRecords } from "./stateService"; export interface DigitalEmployeeSkillRecentCall { @@ -30,9 +31,9 @@ export interface DigitalEmployeeSkillCapability { description: string; category: string; capability_level: "core" | "standard" | "specialized" | "experimental"; - source: "qimingclaw-mcp"; + source: "qimingclaw-mcp" | "qimingclaw-acp"; server_id: string; - transport: "stdio" | "remote"; + transport: "stdio" | "remote" | "local"; running: boolean; governance: DigitalEmployeeSkillGovernance; recent_calls: DigitalEmployeeSkillRecentCall[]; @@ -50,6 +51,63 @@ type McpServerEntryWithRuntimeFields = McpServerEntry & { discoveredTools?: string[]; }; +interface AcpSkillDefinition { + skill_id: string; + name: string; + description: string; + category: string; + capability_level: DigitalEmployeeSkillCapability["capability_level"]; + tools: string[]; + match: (event: DigitalEmployeeRuntimeEventLike) => boolean; +} + +interface DigitalEmployeeRuntimeEventLike { + id: string; + kind: string; + message: string; + payload: unknown; + occurredAt: string; +} + +const ACP_SKILLS: AcpSkillDefinition[] = [ + { + skill_id: "qimingclaw-acp-session", + name: "ACP 会话执行", + description: "创建和驱动 qimingclaw ACP 会话,把用户目标转为本地 Agent 执行过程。", + category: "ACP", + capability_level: "core", + tools: ["create_session", "send_prompt", "cancel_session", "list_sessions"], + match: (event) => includesAny(event, ["acp_session", "agent_session", "session_update", "session_created", "prompt"]), + }, + { + skill_id: "qimingclaw-computer-control", + name: "客户端电脑控制", + description: "承接 /computer/chat 远程任务,在客户端执行浏览器、桌面和文件相关操作。", + category: "ACP", + capability_level: "specialized", + tools: ["computer_chat", "browser_control", "desktop_action", "file_delivery"], + match: (event) => includesAny(event, ["computer_chat", "/computer/chat", "desktop", "browser"]), + }, + { + skill_id: "qimingclaw-acp-permission", + name: "权限与人工确认", + description: "把 ACP permission 请求投射为数字员工审批,并把同意、驳回回写到本地会话。", + category: "治理", + capability_level: "specialized", + tools: ["permission_request", "permission_reply", "approval_sync"], + match: (event) => includesAny(event, ["permission", "approval", "decide_approval"]), + }, + { + skill_id: "qimingclaw-artifact-reporting", + name: "产物与日报整理", + description: "整理 ACP/任务运行产物、来源记录和日报材料,供管理端同步和本地下载。", + category: "交付", + capability_level: "standard", + tools: ["artifact_collect", "daily_report", "outbox_sync"], + match: (event) => includesAny(event, ["artifact", "report", "sync", "outbox"]), + }, +]; + export function listDigitalEmployeeSkillCapabilities(): { generatedAt: string; skills: DigitalEmployeeSkillCapability[]; @@ -139,12 +197,114 @@ export function listDigitalEmployeeSkillCapabilities(): { } } + skills.push(...buildAcpSkills(runtime.events)); + return { generatedAt: new Date().toISOString(), skills, }; } +function buildAcpSkills(events: DigitalEmployeeRuntimeEventLike[]): DigitalEmployeeSkillCapability[] { + const sessions = safeAgentSessions(); + const running = safeAgentReady(); + const engineType = safeAgentEngineType(); + const activeSessionCount = sessions.filter((session) => ( + session.status === "active" || session.status === "pending" + )).length; + const governance = buildGovernance(true, [], [], [], []); + + return ACP_SKILLS.map((definition) => { + const recentCalls = events + .filter(definition.match) + .map((event) => toAcpRecentCall(event, definition.tools[0])) + .sort((left, right) => right.occurred_at.localeCompare(left.occurred_at)); + return { + skill_id: definition.skill_id, + name: definition.name, + version: "1.0.0", + enabled: true, + description: [ + definition.description, + engineType ? `当前 Agent 引擎:${engineType}。` : null, + activeSessionCount > 0 ? `活跃会话 ${activeSessionCount} 个。` : null, + ].filter(Boolean).join(" "), + category: definition.category, + capability_level: definition.capability_level, + source: "qimingclaw-acp", + server_id: "qimingclaw-acp", + transport: "local", + running, + governance, + recent_calls: recentCalls.slice(0, 5), + call_count: recentCalls.length, + last_called_at: recentCalls[0]?.occurred_at ?? null, + tools: definition.tools, + tool_count: definition.tools.length, + project_ids: projectIds(recentCalls), + }; + }); +} + +function toAcpRecentCall( + event: DigitalEmployeeRuntimeEventLike, + fallbackToolName: string, +): DigitalEmployeeSkillRecentCall { + const payload = objectRecord(event.payload); + const toolName = stringValue(payload?.toolName) + || stringValue(payload?.tool_name) + || fallbackToolName; + return { + event_id: event.id, + kind: event.kind, + message: event.message, + status: stringValue(payload?.status) || null, + occurred_at: event.occurredAt, + tool_name: toolName, + project_id: stringValue(payload?.projectId ?? payload?.project_id) || null, + session_id: stringValue(payload?.sessionId ?? payload?.session_id) || null, + }; +} + +function includesAny(event: DigitalEmployeeRuntimeEventLike, needles: string[]): boolean { + const payload = objectRecord(event.payload); + const text = [ + event.kind, + event.message, + stringValue(payload?.source), + stringValue(payload?.commandKind), + stringValue(payload?.toolName ?? payload?.tool_name), + ].join(" ").toLowerCase(); + return needles.some((needle) => text.includes(needle.toLowerCase())); +} + +function safeAgentReady(): boolean { + try { + return agentService.isReady; + } catch { + return false; + } +} + +function safeAgentEngineType(): string | null { + try { + return agentService.getEngineType() ?? null; + } catch { + return null; + } +} + +function safeAgentSessions(): Array<{ status?: string; projectId?: string | null }> { + try { + return agentService.listAllSessionsDetailed().map((session) => ({ + status: session.status, + projectId: session.projectId ?? null, + })); + } catch { + return []; + } +} + function buildGovernance( enabled: boolean, globalAllowTools: string[], diff --git a/qimingclaw/docs/digital-employee-frontend-integration-plan.md b/qimingclaw/docs/digital-employee-frontend-integration-plan.md index 0a26f781..406fd813 100644 --- a/qimingclaw/docs/digital-employee-frontend-integration-plan.md +++ b/qimingclaw/docs/digital-employee-frontend-integration-plan.md @@ -871,8 +871,8 @@ file server tools 当前客户端实现由 `digitalEmployee:getSkillCapabilities` IPC 输出本地 Skill Catalog, embedded digital 页面通过 `window.QimingClawBridge.digital.getSkillCapabilities()` -合并到 `/api/skill`。目录先覆盖 MCP server / tool,后续再继续扩展 ACP command、 -GUI agent tool 和 file server tool。 +合并到 `/api/skill`。目录已覆盖 MCP server / tool,并把 qimingclaw ACP 内建能力 +投射为 Skill;后续再继续扩展 GUI agent tool 和 file server tool。 MCP 技能字段已包含: @@ -887,6 +887,18 @@ recent_calls / call_count / last_called_at project_ids ``` +ACP 技能目录已包含: + +```text +qimingclaw-acp-session +qimingclaw-computer-control +qimingclaw-acp-permission +qimingclaw-artifact-reporting +``` + +这些技能会展示 Agent 引擎、运行状态、活跃会话数、内建 command/tool 列表, +并从本地 digital runtime events 归集最近调用、项目 ID 和会话 ID。 + 用户可以: - 查看技能 @@ -894,6 +906,7 @@ project_ids - 查看所属 MCP server - 查看 MCP allow / deny 策略 - 查看 MCP proxy 运行状态 +- 查看 ACP 内建能力与最近调用 - 查看输入输出说明 - 查看最近调用记录