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

468 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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) };
}
});
}