Files
qiming/qimingclaw/crates/agent-electron-client/src/main/ipc/digitalEmployeeHandlers.ts

249 lines
8.1 KiB
TypeScript

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<string, unknown>;
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();
}