feat(client): project acp skills to digital catalog
This commit is contained in:
@@ -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,
|
||||
}),
|
||||
]));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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[],
|
||||
|
||||
@@ -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 内建能力与最近调用
|
||||
- 查看输入输出说明
|
||||
- 查看最近调用记录
|
||||
|
||||
|
||||
Reference in New Issue
Block a user