import { ipcMain } from "electron"; 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, getDigitalEmployeeSyncStatus, } from "../services/digitalEmployee/syncService"; export function registerDigitalEmployeeHandlers(ctx: HandlerContext): void { ipcMain.handle("digitalEmployee:getSnapshot", async () => { const snapshot = buildSnapshot(ctx); const state = recordDigitalEmployeeSnapshot(snapshot); return { ...snapshot, state: { updatedAt: state.updatedAt, events: state.events, }, }; }); ipcMain.handle("digitalEmployee:getState", async () => { 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(); }); ipcMain.handle("digitalEmployee:flushSync", async () => { return flushDigitalEmployeeSyncOutbox({ force: true }); }); } 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 { generatedAt: new Date().toISOString(), services: { agent: { running: agentService.isReady, engineType: agentService.getEngineType(), }, mcp: normalizeServiceStatus(mcpProxyManager.getStatus()), computerServer: normalizeServiceStatus(getComputerServerStatus()), lanproxy: normalizeServiceStatus(ctx.lanproxy.status()), fileServer: normalizeServiceStatus(ctx.fileServer.status()), guiServer: normalizeServiceStatus(getGuiStatus()), }, sessions: { total: sessions.length, active: sessions.filter((session) => session.status === "active" || session.status === "pending", ).length, items: sessions.slice(0, 10), }, }; } function normalizeServiceStatus( value: object | null | undefined, ): DigitalEmployeeServiceStatus { if (!value) return { running: false }; const record = value as Record; return { running: record.running === true, error: typeof record.error === "string" ? record.error : undefined, engineType: typeof record.engineType === "string" ? record.engineType : undefined, port: typeof record.port === "number" ? record.port : undefined, serverCount: typeof record.serverCount === "number" ? record.serverCount : undefined, serverNames: Array.isArray(record.serverNames) ? record.serverNames.filter((item): item is string => typeof item === "string") : undefined, pid: typeof record.pid === "number" ? record.pid : undefined, }; } function getGuiStatus(): object { if (!FEATURES.ENABLE_GUI_AGENT_SERVER) { return { running: false, error: "GUI Agent Server is not available" }; } return isWindows() ? getWindowsMcpStatus() : getGuiAgentServerStatus(); }