feat(client): project digital employee runtime into UI
This commit is contained in:
@@ -3,15 +3,20 @@ import type { HandlerContext } from "@shared/types/ipc";
|
||||
import { FEATURES } from "@shared/featureFlags";
|
||||
import { agentService } from "../services/engines/unifiedAgent";
|
||||
import { mcpProxyManager } from "../services/packages/mcp";
|
||||
import type { McpServerEntry } from "../services/packages/mcp";
|
||||
import { getComputerServerStatus } from "../services/computerServer";
|
||||
import { getGuiAgentServerStatus } from "../services/packages/guiAgentServer";
|
||||
import { getWindowsMcpStatus } from "../services/packages/windowsMcp";
|
||||
import { isWindows } from "../services/system/shellEnv";
|
||||
import {
|
||||
recordDigitalEmployeeSnapshot,
|
||||
readDigitalEmployeeRuntimeRecords,
|
||||
readDigitalEmployeeState,
|
||||
readDigitalEmployeeUiState,
|
||||
saveDigitalEmployeeUiState,
|
||||
type DigitalEmployeeServiceStatus,
|
||||
type DigitalEmployeeSnapshot,
|
||||
type DigitalEmployeeUiStateUpdate,
|
||||
} from "../services/digitalEmployee/stateService";
|
||||
import {
|
||||
flushDigitalEmployeeSyncOutbox,
|
||||
@@ -35,6 +40,25 @@ export function registerDigitalEmployeeHandlers(ctx: HandlerContext): void {
|
||||
return readDigitalEmployeeState();
|
||||
});
|
||||
|
||||
ipcMain.handle("digitalEmployee:getRuntimeRecords", async () => {
|
||||
return readDigitalEmployeeRuntimeRecords();
|
||||
});
|
||||
|
||||
ipcMain.handle("digitalEmployee:getUiState", async () => {
|
||||
return readDigitalEmployeeUiState();
|
||||
});
|
||||
|
||||
ipcMain.handle("digitalEmployee:getSkillCapabilities", async () => {
|
||||
return buildSkillCapabilities();
|
||||
});
|
||||
|
||||
ipcMain.handle(
|
||||
"digitalEmployee:saveUiState",
|
||||
async (_, update: DigitalEmployeeUiStateUpdate) => {
|
||||
return saveDigitalEmployeeUiState(update);
|
||||
},
|
||||
);
|
||||
|
||||
ipcMain.handle("digitalEmployee:getSyncStatus", async () => {
|
||||
return getDigitalEmployeeSyncStatus();
|
||||
});
|
||||
@@ -44,6 +68,135 @@ export function registerDigitalEmployeeHandlers(ctx: HandlerContext): void {
|
||||
});
|
||||
}
|
||||
|
||||
interface DigitalEmployeeSkillCapability {
|
||||
skill_id: string;
|
||||
name: string;
|
||||
version: string;
|
||||
enabled: boolean;
|
||||
description: string;
|
||||
category: string;
|
||||
capability_level: "core" | "standard" | "specialized" | "experimental";
|
||||
source: "qimingclaw-mcp";
|
||||
server_id: string;
|
||||
transport: "stdio" | "remote";
|
||||
running: boolean;
|
||||
tools?: string[];
|
||||
tool_count?: number;
|
||||
allow_tools?: string[];
|
||||
deny_tools?: string[];
|
||||
}
|
||||
|
||||
type McpServerEntryWithRuntimeFields = McpServerEntry & {
|
||||
enabled?: boolean;
|
||||
discoveredTools?: string[];
|
||||
};
|
||||
|
||||
function buildSkillCapabilities(): {
|
||||
generatedAt: string;
|
||||
skills: DigitalEmployeeSkillCapability[];
|
||||
} {
|
||||
const status = mcpProxyManager.getStatus();
|
||||
const config = mcpProxyManager.getConfig();
|
||||
const skills: DigitalEmployeeSkillCapability[] = [];
|
||||
|
||||
for (const [serverId, rawEntry] of Object.entries(config.mcpServers || {})) {
|
||||
const entry = rawEntry as McpServerEntryWithRuntimeFields;
|
||||
const enabled = entry.enabled !== false;
|
||||
const transport = "url" in entry ? "remote" : "stdio";
|
||||
const discoveredTools = normalizeToolList(entry.discoveredTools);
|
||||
const allowedTools = normalizeToolList(entry.allowTools);
|
||||
const deniedTools = normalizeToolList(entry.denyTools);
|
||||
const effectiveTools = chooseEffectiveTools(
|
||||
discoveredTools,
|
||||
allowedTools,
|
||||
deniedTools,
|
||||
);
|
||||
|
||||
skills.push({
|
||||
skill_id: `qimingclaw-mcp-server-${slugifySkillPart(serverId)}`,
|
||||
name: `MCP 服务:${serverId}`,
|
||||
version: "1.0.0",
|
||||
enabled,
|
||||
description: effectiveTools.length > 0
|
||||
? `来自 qimingclaw MCP 配置,当前可用于数字员工编排的工具数:${effectiveTools.length}。`
|
||||
: "来自 qimingclaw MCP 配置,可作为数字员工的外部工具服务。",
|
||||
category: "MCP",
|
||||
capability_level: "specialized",
|
||||
source: "qimingclaw-mcp",
|
||||
server_id: serverId,
|
||||
transport,
|
||||
running: status.running,
|
||||
tools: effectiveTools,
|
||||
tool_count: effectiveTools.length,
|
||||
...(allowedTools.length > 0 ? { allow_tools: allowedTools } : {}),
|
||||
...(deniedTools.length > 0 ? { deny_tools: deniedTools } : {}),
|
||||
});
|
||||
|
||||
for (const toolName of effectiveTools.slice(0, 120)) {
|
||||
skills.push({
|
||||
skill_id: `qimingclaw-mcp-tool-${slugifySkillPart(serverId)}-${slugifySkillPart(toolName)}`,
|
||||
name: `MCP 工具:${toolName}`,
|
||||
version: "1.0.0",
|
||||
enabled,
|
||||
description: `由 ${serverId} 提供,可被 qimingclaw Agent 调用并纳入数字员工任务编排。`,
|
||||
category: "MCP 工具",
|
||||
capability_level: "standard",
|
||||
source: "qimingclaw-mcp",
|
||||
server_id: serverId,
|
||||
transport,
|
||||
running: status.running,
|
||||
tools: [toolName],
|
||||
tool_count: 1,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
generatedAt: new Date().toISOString(),
|
||||
skills,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeToolList(value: unknown): string[] {
|
||||
if (!Array.isArray(value)) return [];
|
||||
return [...new Set(value.filter((item): item is string => (
|
||||
typeof item === "string" && item.trim().length > 0
|
||||
)).map((item) => item.trim()))];
|
||||
}
|
||||
|
||||
function chooseEffectiveTools(
|
||||
discoveredTools: string[],
|
||||
allowedTools: string[],
|
||||
deniedTools: string[],
|
||||
): string[] {
|
||||
const baseTools = discoveredTools.length > 0 ? discoveredTools : allowedTools;
|
||||
if (baseTools.length === 0) return [];
|
||||
if (allowedTools.length > 0) {
|
||||
const allowed = new Set(allowedTools);
|
||||
return baseTools.filter((tool) => allowed.has(tool));
|
||||
}
|
||||
if (deniedTools.length > 0) {
|
||||
const denied = new Set(deniedTools);
|
||||
return baseTools.filter((tool) => !denied.has(tool));
|
||||
}
|
||||
return baseTools;
|
||||
}
|
||||
|
||||
function slugifySkillPart(value: string): string {
|
||||
const slug = value
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, "-")
|
||||
.replace(/^-+|-+$/g, "");
|
||||
if (slug) return slug;
|
||||
|
||||
let hash = 0;
|
||||
for (let index = 0; index < value.length; index += 1) {
|
||||
hash = (hash * 31 + value.charCodeAt(index)) >>> 0;
|
||||
}
|
||||
return `capability-${hash.toString(36)}`;
|
||||
}
|
||||
|
||||
function buildSnapshot(ctx: HandlerContext): DigitalEmployeeSnapshot {
|
||||
const sessions = agentService.listAllSessionsDetailed();
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user