feat(client): project acp skills to digital catalog

This commit is contained in:
baiyanyun
2026-06-08 18:31:03 +08:00
parent e4652ace1c
commit 38e5bdbc04
3 changed files with 250 additions and 6 deletions

View File

@@ -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,
}),
]));
});
});

View File

@@ -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[],

View File

@@ -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 内建能力与最近调用
- 查看输入输出说明
- 查看最近调用记录