468 lines
14 KiB
TypeScript
468 lines
14 KiB
TypeScript
import { ipcMain } from "electron";
|
||
import log from "electron-log";
|
||
import { z } from "zod";
|
||
import { agentService } from "../services/engines/unifiedAgent";
|
||
import type { AgentConfig } from "../services/engines/unifiedAgent";
|
||
import {
|
||
mcpProxyManager,
|
||
syncMcpConfigToProxyAndReload,
|
||
} from "../services/packages/mcp";
|
||
|
||
const agentConfigSchema = z
|
||
.object({
|
||
engine: z.enum(["qimingcode", "claude-code"]),
|
||
workspaceDir: z.string(),
|
||
apiKey: z.string().optional(),
|
||
baseUrl: z.string().optional(),
|
||
model: z.string().optional(),
|
||
apiProtocol: z.string().optional(),
|
||
hostname: z.string().optional(),
|
||
port: z.number().int().positive().optional(),
|
||
timeout: z.number().int().positive().optional(),
|
||
engineBinaryPath: z.string().optional(),
|
||
env: z.record(z.string()).optional(),
|
||
mcpServers: z.record(z.unknown()).optional(),
|
||
permissionMode: z
|
||
.enum(["default", "acceptEdits", "bypassPermissions"])
|
||
.optional(),
|
||
systemPrompt: z.string().optional(),
|
||
purpose: z.enum(["engine"]).optional(),
|
||
})
|
||
.passthrough();
|
||
|
||
const sessionIdSchema = z.string().min(1);
|
||
const partsSchema = z.array(z.any());
|
||
const promptOptionsSchema = z.any().optional();
|
||
const permissionResponseSchema = z.enum(["once", "always", "reject"]);
|
||
|
||
function invalidArgs(channel: string, issues: unknown) {
|
||
const details =
|
||
Array.isArray(issues) && issues.length > 0
|
||
? issues
|
||
.map((i: any) =>
|
||
i.path ? `${i.path.join(".")}: ${i.message}` : i.message,
|
||
)
|
||
.join("; ")
|
||
: String(issues);
|
||
log.warn(`[IPC] ${channel} invalid args:`, issues);
|
||
return {
|
||
success: false,
|
||
error: `Invalid arguments for ${channel}: ${details}`,
|
||
};
|
||
}
|
||
|
||
export function registerAgentHandlers(): void {
|
||
// Initialize unified agent service
|
||
ipcMain.handle("agent:init", async (_, config: AgentConfig) => {
|
||
const parsedConfig = agentConfigSchema.safeParse(config);
|
||
if (!parsedConfig.success) {
|
||
return invalidArgs("agent:init", parsedConfig.error.issues);
|
||
}
|
||
const typedConfig = parsedConfig.data as AgentConfig;
|
||
|
||
log.info("[IPC] Initializing unified agent:", config.engine);
|
||
try {
|
||
// Auto-inject MCP config if MCP proxy is running and no mcpServers provided
|
||
let finalConfig: AgentConfig = typedConfig;
|
||
if (!typedConfig.mcpServers) {
|
||
const mcpConfig = mcpProxyManager.getAgentMcpConfig();
|
||
if (mcpConfig) {
|
||
finalConfig = { ...typedConfig, mcpServers: mcpConfig };
|
||
log.info(
|
||
"[IPC] Auto-injected MCP config into agent:",
|
||
Object.keys(mcpConfig),
|
||
);
|
||
}
|
||
}
|
||
const ok = await agentService.init(finalConfig);
|
||
|
||
// 仅当调用方显式传入 mcpServers(原始服务器列表)时,同步到 MCP Proxy 并动态加载
|
||
// auto-inject 时 finalConfig.mcpServers 是桥接配置,不能写回 proxy
|
||
if (
|
||
typedConfig.mcpServers &&
|
||
Object.keys(typedConfig.mcpServers).length > 0
|
||
) {
|
||
await syncMcpConfigToProxyAndReload(typedConfig.mcpServers);
|
||
}
|
||
|
||
return {
|
||
success: ok,
|
||
engineType: agentService.getEngineType(),
|
||
};
|
||
} catch (error) {
|
||
log.error("[IPC] agent:init failed:", error);
|
||
return { success: false, error: String(error) };
|
||
}
|
||
});
|
||
|
||
// Get agent service status
|
||
ipcMain.handle("agent:serviceStatus", () => {
|
||
return {
|
||
running: agentService.isReady,
|
||
engineType: agentService.getEngineType(),
|
||
};
|
||
});
|
||
|
||
// Destroy unified agent service
|
||
ipcMain.handle("agent:destroy", async () => {
|
||
try {
|
||
await agentService.destroy();
|
||
return { success: true };
|
||
} catch (error) {
|
||
return { success: false, error: String(error) };
|
||
}
|
||
});
|
||
|
||
// Get engine type
|
||
ipcMain.handle("agent:getEngineType", () => {
|
||
return agentService.getEngineType();
|
||
});
|
||
|
||
// Check if ready
|
||
ipcMain.handle("agent:isReady", () => {
|
||
return agentService.isReady;
|
||
});
|
||
|
||
// List sessions
|
||
ipcMain.handle("agent:listSessions", async () => {
|
||
try {
|
||
const sessions = await agentService.listSessions();
|
||
return { success: true, data: sessions };
|
||
} catch (error) {
|
||
return { success: false, error: String(error) };
|
||
}
|
||
});
|
||
|
||
// Create session
|
||
ipcMain.handle(
|
||
"agent:createSession",
|
||
async (_, opts?: { parentID?: string; title?: string }) => {
|
||
try {
|
||
const session = await agentService.createSession(opts);
|
||
return { success: true, data: session };
|
||
} catch (error) {
|
||
return { success: false, error: String(error) };
|
||
}
|
||
},
|
||
);
|
||
|
||
// Get session
|
||
ipcMain.handle("agent:getSession", async (_, id: string) => {
|
||
try {
|
||
const session = await agentService.getSession(id);
|
||
return { success: true, data: session };
|
||
} catch (error) {
|
||
return { success: false, error: String(error) };
|
||
}
|
||
});
|
||
|
||
// Delete session
|
||
ipcMain.handle("agent:deleteSession", async (_, id: string) => {
|
||
try {
|
||
await agentService.deleteSession(id);
|
||
return { success: true };
|
||
} catch (error) {
|
||
return { success: false, error: String(error) };
|
||
}
|
||
});
|
||
|
||
// Update session title (ACP doesn't support this, but keep for compatibility)
|
||
ipcMain.handle(
|
||
"agent:updateSession",
|
||
async (_, id: string, title?: string) => {
|
||
try {
|
||
// ACP doesn't have a separate update method, title is set via session info updates
|
||
// Return success for compatibility
|
||
return { success: true, data: { id, title } };
|
||
} catch (error) {
|
||
return { success: false, error: String(error) };
|
||
}
|
||
},
|
||
);
|
||
|
||
// Get session status (ACP doesn't have this, return empty for compatibility)
|
||
ipcMain.handle("agent:getSessionStatus", async () => {
|
||
try {
|
||
return { success: true, data: {} };
|
||
} catch (error) {
|
||
return { success: false, error: String(error) };
|
||
}
|
||
});
|
||
|
||
// Get messages (ACP doesn't store messages, return empty for compatibility)
|
||
ipcMain.handle("agent:getMessages", async () => {
|
||
try {
|
||
return { success: true, data: [] };
|
||
} catch (error) {
|
||
return { success: false, error: String(error) };
|
||
}
|
||
});
|
||
|
||
// Get single message (ACP doesn't store messages, return error for compatibility)
|
||
ipcMain.handle(
|
||
"agent:getMessage",
|
||
async (_, sessionId: string, messageId: string) => {
|
||
try {
|
||
return { success: false, error: "ACP engine does not store messages" };
|
||
} catch (error) {
|
||
return { success: false, error: String(error) };
|
||
}
|
||
},
|
||
);
|
||
|
||
// Prompt (blocking)
|
||
ipcMain.handle(
|
||
"agent:prompt",
|
||
async (_, sessionId: string, parts: any[], opts?: any) => {
|
||
const sid = sessionIdSchema.safeParse(sessionId);
|
||
const ps = partsSchema.safeParse(parts);
|
||
const op = promptOptionsSchema.safeParse(opts);
|
||
if (!sid.success || !ps.success || !op.success) {
|
||
return invalidArgs("agent:prompt", {
|
||
sessionId: sid.success ? null : sid.error.issues,
|
||
parts: ps.success ? null : ps.error.issues,
|
||
opts: op.success ? null : op.error.issues,
|
||
});
|
||
}
|
||
try {
|
||
const result = await agentService.prompt(sid.data, ps.data, op.data);
|
||
return { success: true, data: result };
|
||
} catch (error) {
|
||
return { success: false, error: String(error) };
|
||
}
|
||
},
|
||
);
|
||
|
||
// Prompt (async, non-blocking - results via SSE events)
|
||
ipcMain.handle(
|
||
"agent:promptAsync",
|
||
async (_, sessionId: string, parts: any[], opts?: any) => {
|
||
const sid = sessionIdSchema.safeParse(sessionId);
|
||
const ps = partsSchema.safeParse(parts);
|
||
const op = promptOptionsSchema.safeParse(opts);
|
||
if (!sid.success || !ps.success || !op.success) {
|
||
return invalidArgs("agent:promptAsync", {
|
||
sessionId: sid.success ? null : sid.error.issues,
|
||
parts: ps.success ? null : ps.error.issues,
|
||
opts: op.success ? null : op.error.issues,
|
||
});
|
||
}
|
||
try {
|
||
await agentService.promptAsync(sid.data, ps.data, op.data);
|
||
return { success: true };
|
||
} catch (error) {
|
||
return { success: false, error: String(error) };
|
||
}
|
||
},
|
||
);
|
||
|
||
// Abort session
|
||
ipcMain.handle("agent:abort", async (_, sessionId?: string) => {
|
||
if (sessionId !== undefined) {
|
||
const sid = sessionIdSchema.safeParse(sessionId);
|
||
if (!sid.success) {
|
||
return invalidArgs("agent:abort", sid.error.issues);
|
||
}
|
||
}
|
||
try {
|
||
await agentService.abortSession(sessionId || "");
|
||
return { success: true };
|
||
} catch (error) {
|
||
return { success: false, error: String(error) };
|
||
}
|
||
});
|
||
|
||
// Respond to permission request
|
||
ipcMain.handle(
|
||
"agent:respondPermission",
|
||
async (
|
||
_,
|
||
_sessionId: string,
|
||
permissionId: string,
|
||
response: "once" | "always" | "reject",
|
||
) => {
|
||
const pid = z.string().min(1).safeParse(permissionId);
|
||
const rsp = permissionResponseSchema.safeParse(response);
|
||
if (!pid.success || !rsp.success) {
|
||
return invalidArgs("agent:respondPermission", {
|
||
permissionId: pid.success ? null : pid.error.issues,
|
||
response: rsp.success ? null : rsp.error.issues,
|
||
});
|
||
}
|
||
try {
|
||
agentService.respondPermission(pid.data, rsp.data);
|
||
return { success: true };
|
||
} catch (error) {
|
||
return { success: false, error: String(error) };
|
||
}
|
||
},
|
||
);
|
||
|
||
// List tools (ACP doesn't support this, return empty for compatibility)
|
||
ipcMain.handle("agent:listTools", async () => {
|
||
try {
|
||
return { success: true, data: [] };
|
||
} catch (error) {
|
||
return { success: false, error: String(error) };
|
||
}
|
||
});
|
||
|
||
// List providers (ACP doesn't support this, return empty for compatibility)
|
||
ipcMain.handle("agent:listProviders", async () => {
|
||
try {
|
||
return { success: true, data: [] };
|
||
} catch (error) {
|
||
return { success: false, error: String(error) };
|
||
}
|
||
});
|
||
|
||
// Get session diff (ACP doesn't support this, return empty for compatibility)
|
||
ipcMain.handle("agent:getSessionDiff", async () => {
|
||
try {
|
||
return { success: true, data: [] };
|
||
} catch (error) {
|
||
return { success: false, error: String(error) };
|
||
}
|
||
});
|
||
|
||
// Revert session (ACP doesn't support this, return error for compatibility)
|
||
ipcMain.handle("agent:revert", async () => {
|
||
try {
|
||
return { success: false, error: "ACP engine does not support revert" };
|
||
} catch (error) {
|
||
return { success: false, error: String(error) };
|
||
}
|
||
});
|
||
|
||
// Unrevert session (ACP doesn't support this, return error for compatibility)
|
||
ipcMain.handle("agent:unrevert", async () => {
|
||
try {
|
||
return { success: false, error: "ACP engine does not support unrevert" };
|
||
} catch (error) {
|
||
return { success: false, error: String(error) };
|
||
}
|
||
});
|
||
|
||
// Share session (ACP doesn't support this, return error for compatibility)
|
||
ipcMain.handle("agent:shareSession", async () => {
|
||
try {
|
||
return { success: false, error: "ACP engine does not support share" };
|
||
} catch (error) {
|
||
return { success: false, error: String(error) };
|
||
}
|
||
});
|
||
|
||
// Fork session (ACP doesn't support this, return error for compatibility)
|
||
ipcMain.handle("agent:forkSession", async () => {
|
||
try {
|
||
return { success: false, error: "ACP engine does not support fork" };
|
||
} catch (error) {
|
||
return { success: false, error: String(error) };
|
||
}
|
||
});
|
||
|
||
// Get config (ACP doesn't support this, return empty for compatibility)
|
||
ipcMain.handle("agent:getConfig", async () => {
|
||
try {
|
||
return { success: true, data: {} };
|
||
} catch (error) {
|
||
return { success: false, error: String(error) };
|
||
}
|
||
});
|
||
|
||
// Find text (ACP doesn't support this, return empty for compatibility)
|
||
ipcMain.handle("agent:findText", async () => {
|
||
try {
|
||
return { success: true, data: [] };
|
||
} catch (error) {
|
||
return { success: false, error: String(error) };
|
||
}
|
||
});
|
||
|
||
// Find files (ACP doesn't support this, return empty for compatibility)
|
||
ipcMain.handle("agent:findFiles", async () => {
|
||
try {
|
||
return { success: true, data: [] };
|
||
} catch (error) {
|
||
return { success: false, error: String(error) };
|
||
}
|
||
});
|
||
|
||
// List files (ACP doesn't support this, return empty for compatibility)
|
||
ipcMain.handle("agent:listFiles", async () => {
|
||
try {
|
||
return { success: true, data: [] };
|
||
} catch (error) {
|
||
return { success: false, error: String(error) };
|
||
}
|
||
});
|
||
|
||
// Read file (ACP doesn't support this, return error for compatibility)
|
||
ipcMain.handle("agent:readFile", async () => {
|
||
try {
|
||
return { success: false, error: "ACP engine does not support readFile" };
|
||
} catch (error) {
|
||
return { success: false, error: String(error) };
|
||
}
|
||
});
|
||
|
||
// Claude Code prompt (ACP engine)
|
||
ipcMain.handle("agent:claudePrompt", async (_, message: string) => {
|
||
try {
|
||
const result = await agentService.claudePrompt(message);
|
||
return { success: true, data: result };
|
||
} catch (error) {
|
||
return { success: false, error: String(error) };
|
||
}
|
||
});
|
||
|
||
// MCP status (ACP doesn't support this, return empty for compatibility)
|
||
ipcMain.handle("agent:mcpStatus", async () => {
|
||
try {
|
||
return { success: true, data: {} };
|
||
} catch (error) {
|
||
return { success: false, error: String(error) };
|
||
}
|
||
});
|
||
|
||
// List agents (ACP doesn't support this, return empty for compatibility)
|
||
ipcMain.handle("agent:listAgents", async () => {
|
||
try {
|
||
return { success: true, data: [] };
|
||
} catch (error) {
|
||
return { success: false, error: String(error) };
|
||
}
|
||
});
|
||
|
||
// List commands (ACP doesn't support this, return empty for compatibility)
|
||
ipcMain.handle("agent:listCommands", async () => {
|
||
try {
|
||
return { success: true, data: [] };
|
||
} catch (error) {
|
||
return { success: false, error: String(error) };
|
||
}
|
||
});
|
||
|
||
// List all sessions with detailed status (for Sessions tab)
|
||
ipcMain.handle("agent:listSessionsDetailed", async () => {
|
||
try {
|
||
const sessions = agentService.listAllSessionsDetailed();
|
||
return { success: true, data: sessions };
|
||
} catch (error) {
|
||
log.error("[IPC] agent:listSessionsDetailed failed:", error);
|
||
return { success: false, error: String(error), data: [] };
|
||
}
|
||
});
|
||
|
||
// Stop a specific session (abort + delete from engine)
|
||
ipcMain.handle("agent:stopSession", async (_, sessionId: string) => {
|
||
try {
|
||
const stopped = await agentService.stopSession(sessionId);
|
||
return { success: stopped };
|
||
} catch (error) {
|
||
log.error("[IPC] agent:stopSession failed:", error);
|
||
return { success: false, error: String(error) };
|
||
}
|
||
});
|
||
}
|