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",
|
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", () => ({
|
vi.mock("../packages/mcp", () => ({
|
||||||
mcpProxyManager: {
|
mcpProxyManager: {
|
||||||
getStatus: () => mockMcp.status,
|
getStatus: () => mockMcp.status,
|
||||||
@@ -55,6 +87,14 @@ vi.mock("./stateService", () => ({
|
|||||||
readDigitalEmployeeRuntimeRecords: () => mockRuntime.records,
|
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", () => {
|
describe("digital employee skill catalog service", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.resetModules();
|
vi.resetModules();
|
||||||
@@ -65,7 +105,7 @@ describe("digital employee skill catalog service", () => {
|
|||||||
|
|
||||||
const result = listDigitalEmployeeSkillCapabilities();
|
const result = listDigitalEmployeeSkillCapabilities();
|
||||||
|
|
||||||
expect(result.skills).toEqual([
|
expect(result.skills).toEqual(expect.arrayContaining([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
skill_id: "qimingclaw-mcp-server-crm",
|
skill_id: "qimingclaw-mcp-server-crm",
|
||||||
server_id: "crm",
|
server_id: "crm",
|
||||||
@@ -92,6 +132,37 @@ describe("digital employee skill catalog service", () => {
|
|||||||
tools: ["crm.export"],
|
tools: ["crm.export"],
|
||||||
recent_calls: [],
|
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 { mcpProxyManager } from "../packages/mcp";
|
||||||
import type { McpServerEntry } from "../packages/mcp";
|
import type { McpServerEntry } from "../packages/mcp";
|
||||||
|
import { agentService } from "../engines/unifiedAgent";
|
||||||
import { readDigitalEmployeeRuntimeRecords } from "./stateService";
|
import { readDigitalEmployeeRuntimeRecords } from "./stateService";
|
||||||
|
|
||||||
export interface DigitalEmployeeSkillRecentCall {
|
export interface DigitalEmployeeSkillRecentCall {
|
||||||
@@ -30,9 +31,9 @@ export interface DigitalEmployeeSkillCapability {
|
|||||||
description: string;
|
description: string;
|
||||||
category: string;
|
category: string;
|
||||||
capability_level: "core" | "standard" | "specialized" | "experimental";
|
capability_level: "core" | "standard" | "specialized" | "experimental";
|
||||||
source: "qimingclaw-mcp";
|
source: "qimingclaw-mcp" | "qimingclaw-acp";
|
||||||
server_id: string;
|
server_id: string;
|
||||||
transport: "stdio" | "remote";
|
transport: "stdio" | "remote" | "local";
|
||||||
running: boolean;
|
running: boolean;
|
||||||
governance: DigitalEmployeeSkillGovernance;
|
governance: DigitalEmployeeSkillGovernance;
|
||||||
recent_calls: DigitalEmployeeSkillRecentCall[];
|
recent_calls: DigitalEmployeeSkillRecentCall[];
|
||||||
@@ -50,6 +51,63 @@ type McpServerEntryWithRuntimeFields = McpServerEntry & {
|
|||||||
discoveredTools?: string[];
|
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(): {
|
export function listDigitalEmployeeSkillCapabilities(): {
|
||||||
generatedAt: string;
|
generatedAt: string;
|
||||||
skills: DigitalEmployeeSkillCapability[];
|
skills: DigitalEmployeeSkillCapability[];
|
||||||
@@ -139,12 +197,114 @@ export function listDigitalEmployeeSkillCapabilities(): {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
skills.push(...buildAcpSkills(runtime.events));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
generatedAt: new Date().toISOString(),
|
generatedAt: new Date().toISOString(),
|
||||||
skills,
|
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(
|
function buildGovernance(
|
||||||
enabled: boolean,
|
enabled: boolean,
|
||||||
globalAllowTools: string[],
|
globalAllowTools: string[],
|
||||||
|
|||||||
@@ -871,8 +871,8 @@ file server tools
|
|||||||
|
|
||||||
当前客户端实现由 `digitalEmployee:getSkillCapabilities` IPC 输出本地 Skill Catalog,
|
当前客户端实现由 `digitalEmployee:getSkillCapabilities` IPC 输出本地 Skill Catalog,
|
||||||
embedded digital 页面通过 `window.QimingClawBridge.digital.getSkillCapabilities()`
|
embedded digital 页面通过 `window.QimingClawBridge.digital.getSkillCapabilities()`
|
||||||
合并到 `/api/skill`。目录先覆盖 MCP server / tool,后续再继续扩展 ACP command、
|
合并到 `/api/skill`。目录已覆盖 MCP server / tool,并把 qimingclaw ACP 内建能力
|
||||||
GUI agent tool 和 file server tool。
|
投射为 Skill;后续再继续扩展 GUI agent tool 和 file server tool。
|
||||||
|
|
||||||
MCP 技能字段已包含:
|
MCP 技能字段已包含:
|
||||||
|
|
||||||
@@ -887,6 +887,18 @@ recent_calls / call_count / last_called_at
|
|||||||
project_ids
|
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 server
|
||||||
- 查看 MCP allow / deny 策略
|
- 查看 MCP allow / deny 策略
|
||||||
- 查看 MCP proxy 运行状态
|
- 查看 MCP proxy 运行状态
|
||||||
|
- 查看 ACP 内建能力与最近调用
|
||||||
- 查看输入输出说明
|
- 查看输入输出说明
|
||||||
- 查看最近调用记录
|
- 查看最近调用记录
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user