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

389 lines
12 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 * as fs from "fs";
import log from "electron-log";
import { z } from "zod";
import type { HandlerContext } from "@shared/types/ipc";
import { createServiceManager } from "../window/serviceManager";
import { checkLanproxyHealth } from "../services/packages/lanproxyHealth";
export const lanproxyConfigSchema = z.object({
serverIp: z.string().min(1),
serverPort: z.number().int().positive(),
clientKey: z.string().min(1),
ssl: z.boolean().optional(),
});
export const agentRunnerConfigSchema = z.object({
binPath: z.string().min(1),
backendPort: z.number().int().positive(),
proxyPort: z.number().int().positive(),
apiKey: z.string().min(1),
apiBaseUrl: z.string().min(1),
defaultModel: z.string().min(1),
});
export const portSchema = z.number().int().positive();
function invalidArgs(channel: string, issues: unknown) {
log.warn(`[IPC] ${channel} invalid args:`, issues);
return { success: false, error: `Invalid arguments for ${channel}` };
}
/** 模块级 _serviceManager供其他模块如 computerServer访问 */
let _serviceManager: ReturnType<typeof createServiceManager> | null = null;
/** 获取 _serviceManager 实例(需先调用 registerProcessHandlers */
export function getServiceManager(): ReturnType<
typeof createServiceManager
> | null {
return _serviceManager;
}
export function registerProcessHandlers(ctx: HandlerContext): void {
_serviceManager = createServiceManager({
lanproxy: ctx.lanproxy,
fileServer: ctx.fileServer,
agentRunner: ctx.agentRunner,
});
// 本地别名,确保 TypeScript 知道它已被赋值
const sm = _serviceManager;
// ==================== Helper: Start File Server ====================
const startFileServerProcess = async (
port: number,
): Promise<{ success: boolean; error?: string }> => {
return sm.startFileServer(port);
};
// ==================== Helper: Start Lanproxy ====================
const startLanproxyProcess = async (config: {
serverIp: string;
serverPort: number;
clientKey: string;
ssl?: boolean;
}): Promise<{
success: boolean;
error?: string;
healthCheck?: { healthy: boolean; error?: string };
}> => {
if (ctx.lanproxy.running) {
// 切换账号后 clientKey 会变化,必须用新配置重启,不能跳过。
// 否则旧进程继续使用旧 clientKey 导致「本地显示已联通、会话显示离线」。
log.info(
"[Lanproxy] Already running, stopping first before restarting with new config",
);
await ctx.lanproxy.stopAsync();
}
const useSsl = config.ssl !== false;
const maskedKey =
config.clientKey.length > 8
? `${config.clientKey.slice(0, 4)}****${config.clientKey.slice(-4)}`
: "****";
log.info("[Lanproxy] Starting", {
server: config.serverIp,
port: config.serverPort,
keyMasked: maskedKey,
ssl: useSsl,
});
const result = await sm.startLanproxy(config);
if (result.success) {
log.info("[Lanproxy] Started", {
server: config.serverIp,
port: config.serverPort,
});
// 远端 health 接口可选(私有化部署可能未提供);异步探测仅打日志,不阻塞 IPC、不影响启动结果
void checkLanproxyHealth(config.clientKey)
.then((health) => {
result.healthCheck = health;
if (!health.healthy) {
log.warn(
"[Lanproxy] Post-start health probe failed (non-fatal; private backends may omit /api/sandbox/config/health):",
health.error,
);
} else {
log.info("[Lanproxy] Post-start health probe OK");
}
})
.catch((e) => {
log.warn("[Lanproxy] Post-start health probe error (non-fatal):", e);
});
} else {
log.error("[Lanproxy] Start failed", {
error: result.error,
server: config.serverIp,
port: config.serverPort,
});
}
return result;
};
// Lanproxy handlers
ipcMain.handle(
"lanproxy:start",
async (
_,
config: {
serverIp: string;
serverPort: number;
clientKey: string;
ssl?: boolean;
},
) => {
const parsed = lanproxyConfigSchema.safeParse(config);
if (!parsed.success) {
return invalidArgs("lanproxy:start", parsed.error.issues);
}
return startLanproxyProcess(parsed.data);
},
);
ipcMain.handle("lanproxy:stop", async () => {
return ctx.lanproxy.stop();
});
ipcMain.handle("lanproxy:status", () => {
return ctx.lanproxy.status();
});
/** 供设置页判断是否可显示「启动」并提示不可用原因 */
ipcMain.handle("lanproxy:isAvailable", async () => {
const { getLanproxyBinPath } =
await import("../services/system/dependencies");
const binPath = getLanproxyBinPath();
return { available: fs.existsSync(binPath) };
});
// Agent Runner handlers
ipcMain.handle(
"agentRunner:start",
async (
_,
config: {
binPath: string;
backendPort: number;
proxyPort: number;
apiKey: string;
apiBaseUrl: string;
defaultModel: string;
},
) => {
const parsed = agentRunnerConfigSchema.safeParse(config);
if (!parsed.success) {
return invalidArgs("agentRunner:start", parsed.error.issues);
}
const cfg = parsed.data;
const { getAppEnv } = await import("../services/system/dependencies");
if (ctx.agentRunner.running) {
return { success: true, message: "Already running" };
}
const args = [
"--backend-port",
String(cfg.backendPort),
"--proxy-port",
String(cfg.proxyPort),
"--api-key",
cfg.apiKey,
"--api-base-url",
cfg.apiBaseUrl,
"--default-model",
cfg.defaultModel,
];
// 仅记录端口与 URL不记录 apiKey避免敏感信息写入日志
log.info(
"Starting agent runner:",
cfg.binPath,
"--backend-port",
cfg.backendPort,
"--proxy-port",
cfg.proxyPort,
"--api-base-url",
cfg.apiBaseUrl,
);
const result = await ctx.agentRunner.start({
command: cfg.binPath,
args,
shell: true,
env: getAppEnv(),
startupDelayMs: 2000,
});
if (result.success) {
ctx.setAgentRunnerPorts({
backendPort: cfg.backendPort,
proxyPort: cfg.proxyPort,
});
}
return result;
},
);
ipcMain.handle("agentRunner:stop", async () => {
const result = ctx.agentRunner.stop();
ctx.setAgentRunnerPorts(null);
return result;
});
ipcMain.handle("agentRunner:status", () => {
const st = ctx.agentRunner.status();
return {
...st,
backendUrl: ctx.agentRunnerPorts
? `http://127.0.0.1:${ctx.agentRunnerPorts.backendPort}`
: undefined,
proxyUrl: ctx.agentRunnerPorts
? `http://127.0.0.1:${ctx.agentRunnerPorts.proxyPort}`
: undefined,
};
});
// File Server handlers
ipcMain.handle("fileServer:start", async (_, port: number = 60000) => {
const parsed = portSchema.safeParse(port);
if (!parsed.success) {
return invalidArgs("fileServer:start", parsed.error.issues);
}
return startFileServerProcess(parsed.data);
});
ipcMain.handle("fileServer:stop", async () => {
return ctx.fileServer.stop();
});
ipcMain.handle("fileServer:status", () => {
return ctx.fileServer.status();
});
// Computer Server handlers (Agent HTTP 接口服务,对齐 rcoder /computer/* API)
ipcMain.handle("computerServer:status", async () => {
const { getComputerServerStatus } =
await import("../services/computerServer");
return getComputerServerStatus();
});
ipcMain.handle("computerServer:start", async (_, port?: number) => {
const { startComputerServer } = await import("../services/computerServer");
const { getConfiguredPorts } = await import("../services/startupPorts");
const resolvedPortRaw = port ?? getConfiguredPorts().agent;
const parsed = portSchema.safeParse(resolvedPortRaw);
if (!parsed.success) {
return invalidArgs("computerServer:start", parsed.error.issues);
}
const resolvedPort = parsed.data;
return startComputerServer(resolvedPort);
});
ipcMain.handle("computerServer:stop", async () => {
const { stopComputerServer } = await import("../services/computerServer");
await stopComputerServer();
return { success: true };
});
// Admin Server handlers (管理接口服务)
// Admin Server 已合并到 Computer Server (60006),不再有独立的 60007 端口
// start/stop 为空操作Computer Server 通过 services:restartAll 管理)
ipcMain.handle("adminServer:start", async () => {
return { success: true };
});
ipcMain.handle("adminServer:stop", async () => {
return { success: true };
});
// adminServer:status 现在返回 Computer Server 状态
ipcMain.handle("adminServer:status", async () => {
const { getComputerServerStatus } =
await import("../services/computerServer");
return getComputerServerStatus();
});
// ==================== services:restartAll ====================
ipcMain.handle("services:restartAll", async () => {
log.info("[Services] Restarting all services...");
try {
const { stopComputerServer } = await import("../services/computerServer");
await stopComputerServer();
} catch (e) {
log.warn("[Services] ComputerServer stop error (ignored):", e);
}
const base = await sm.restartAllServices();
const results: Record<string, { success: boolean; error?: string }> = {
...base.results,
};
// 补充 processHandlers 特有步骤Computer Server
try {
const { startComputerServer } =
await import("../services/computerServer");
const { getConfiguredPorts } = await import("../services/startupPorts");
const { agent: agentPort } = getConfiguredPorts();
results.computerServer = await startComputerServer(agentPort);
log.info("[Services] ComputerServer started:", results.computerServer);
} catch (e) {
results.computerServer = { success: false, error: String(e) };
log.error("[Services] ComputerServer start failed:", e);
}
log.info("[Services] All services restart complete:", results);
return { success: true, results };
});
// ==================== services:stopAll ====================
ipcMain.handle("services:stopAll", async () => {
log.info("[Services] Stopping all services...");
const base = await sm.stopAllServices();
const results: Record<string, { success: boolean; error?: string }> = {
...base.results,
};
// 补充 processHandlers 特有步骤Computer Server
try {
const { stopComputerServer } = await import("../services/computerServer");
await stopComputerServer();
results.computerServer = { success: true };
log.info("[Services] ComputerServer stopped");
} catch (e) {
results.computerServer = { success: false, error: String(e) };
log.error("[Services] ComputerServer stop failed:", e);
}
log.info("[Services] All services stopped:", results);
return { success: true, results };
});
// ==================== services:restartAllExceptLanproxy ====================
ipcMain.handle("services:restartAllExceptLanproxy", async () => {
log.info("[Services] Restarting all services except lanproxy...");
const base = await sm.restartAllServicesExceptLanproxy();
const results: Record<string, { success: boolean; error?: string }> = {
...base.results,
};
// 补充 processHandlers 特有步骤Computer Server
try {
const { startComputerServer } =
await import("../services/computerServer");
const { getConfiguredPorts } = await import("../services/startupPorts");
const { agent: agentPort } = getConfiguredPorts();
results.computerServer = await startComputerServer(agentPort);
log.info("[Services] ComputerServer started:", results.computerServer);
} catch (e) {
results.computerServer = { success: false, error: String(e) };
log.error("[Services] ComputerServer start failed:", e);
}
log.info(
"[Services] All services (except lanproxy) restart complete:",
results,
);
return { success: true, results };
});
}