389 lines
12 KiB
TypeScript
389 lines
12 KiB
TypeScript
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 };
|
||
});
|
||
}
|