chore: initialize qiming workspace repository

This commit is contained in:
Codex
2026-05-29 14:22:48 +08:00
commit bfd67a0f2c
10750 changed files with 1885711 additions and 0 deletions

View File

@@ -0,0 +1,467 @@
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) };
}
});
}

View File

@@ -0,0 +1,931 @@
import {
ipcMain,
app,
dialog,
shell,
session as electronSession,
systemPreferences,
BrowserWindow,
} from "electron";
import * as path from "path";
import * as fs from "fs";
import { execSync } from "child_process";
import log from "electron-log";
import type { HandlerContext } from "@shared/types/ipc";
import { LATEST_LOG_BASENAME } from "../bootstrap/logConfig";
import {
checkForUpdates,
downloadUpdate,
installUpdate,
getUpdateState,
openReleasesPage,
} from "../services/autoUpdater";
import { getDeviceId } from "../services/system/deviceId";
import { getTrayManager } from "../window/trayManager";
import { getAutoLaunchManager } from "../window/autoLaunchManager";
import { APP_DISPLAY_NAME } from "@shared/constants";
import { readSetting, writeSetting } from "../db";
import { getDomainTokenKey } from "@shared/utils/domain";
import { t } from "../services/i18n";
// WebView 窗口缓存
let webviewWindow: BrowserWindow | null = null;
const isIpv4Host = (host: string): boolean =>
/^(25[0-5]|2[0-4]\d|1?\d?\d)(\.(25[0-5]|2[0-4]\d|1?\d?\d)){3}$/.test(host);
const isIpv6Host = (host: string): boolean => /^[0-9a-f:]+$/i.test(host);
const useHostOnlyCookie = (host: string): boolean =>
host === "localhost" || isIpv4Host(host) || isIpv6Host(host);
function resolveCookieDomain(url: string): string | undefined {
try {
const host = new URL(url).hostname;
return useHostOnlyCookie(host) ? undefined : host;
} catch {
return undefined;
}
}
/**
* 解析 JWT token 的过期时间
* 注意:仅解析过期时间,不验证签名。过期时间来自不可信来源(外部 token
* 这里的用途是设置 cookie 的过期时间,即使被伪造也只是影响本地 cookie 生命周期。
*/
function parseJwtExp(token: string): number | null {
try {
const parts = token.split(".");
if (parts.length < 2) return null;
const payload = Buffer.from(parts[1], "base64url").toString("utf8");
const parsed = JSON.parse(payload) as { exp?: unknown };
if (typeof parsed.exp !== "number" || !Number.isFinite(parsed.exp)) {
return null;
}
return parsed.exp;
} catch {
return null;
}
}
// JWT 无 exp 或 exp 即将到期时的兜底 cookie TTL7 天)
const TICKET_COOKIE_FALLBACK_TTL_SECONDS = 7 * 24 * 60 * 60;
function resolveTicketExpirationDate(token: string): number | undefined {
const exp = parseJwtExp(token);
const fallback =
Math.floor(Date.now() / 1000) + TICKET_COOKIE_FALLBACK_TTL_SECONDS;
if (!exp) return fallback;
// 过期时间太近(<60s时使用兜底 TTL避免创建 session cookie
if (exp <= Math.floor(Date.now() / 1000) + 60) return fallback;
return exp;
}
function getHostname(url: string): string | null {
try {
return new URL(url).hostname.toLowerCase();
} catch {
return null;
}
}
async function removeSameNameCookies(params: {
url: string;
name: string;
domain?: string;
}): Promise<void> {
const host = getHostname(params.url);
if (!host) return;
const targetDomain = params.domain?.toLowerCase();
const all = await electronSession.defaultSession.cookies.get({
name: params.name,
});
for (const c of all) {
const cookieDomain = (c.domain || "").toLowerCase().replace(/^\./, "");
const domainMatched = targetDomain
? cookieDomain === targetDomain
: cookieDomain === host || host.endsWith(`.${cookieDomain}`);
if (!domainMatched) continue;
const removeHost = cookieDomain || host;
const removePath = c.path?.startsWith("/") ? c.path : `/${c.path || ""}`;
const removeUrl = `${c.secure ? "https" : "http"}://${removeHost}${removePath}`;
try {
await electronSession.defaultSession.cookies.remove(removeUrl, c.name);
} catch (error) {
log.warn("[IPC] session:setCookie remove old cookie failed:", error);
}
}
}
export function registerAppHandlers(ctx: HandlerContext): void {
// Autolaunch — 统一通过 AutoLaunchManager 操作,确保 args 一致Windows 注册表 entry 一致)
ipcMain.handle("autolaunch:get", async () => {
try {
const mgr = getAutoLaunchManager();
if (mgr) return mgr.isEnabled();
// fallback: AutoLaunchManager 未初始化时直接读
const settings = app.getLoginItemSettings({ args: ["--hidden"] });
return settings.openAtLogin;
} catch (error) {
log.error("[IPC] autolaunch:get failed:", error);
return false;
}
});
ipcMain.handle("autolaunch:set", async (_, enabled: boolean) => {
try {
const mgr = getAutoLaunchManager();
let success = false;
if (mgr) {
success = await mgr.setEnabled(enabled);
} else {
// fallback
app.setLoginItemSettings({
openAtLogin: enabled,
openAsHidden: true,
args: ["--hidden"],
});
success = true;
}
if (!success)
return {
success: false,
error: t("Claw.Settings.messages.settingFailed"),
};
// 同步托盘缓存状态
getTrayManager()?.refreshAutoLaunchState();
// 通知所有渲染进程
for (const win of BrowserWindow.getAllWindows()) {
if (!win.isDestroyed()) {
win.webContents.send("autolaunch:changed", enabled);
}
}
return { success: true };
} catch (error) {
log.error("[IPC] autolaunch:set failed:", error);
return { success: false, error: String(error) };
}
});
// Log handlers
ipcMain.handle("log:getDir", () => {
return log.transports.file.getFile().path
? path.dirname(log.transports.file.getFile().path)
: app.getPath("logs");
});
// Renderer 进程写入日志到主进程日志文件
ipcMain.handle(
"log:write",
async (
_,
level: "info" | "warn" | "error",
message: string,
...args: unknown[]
) => {
switch (level) {
case "error":
log.error(message, ...args);
break;
case "warn":
log.warn(message, ...args);
break;
default:
log.info(message, ...args);
}
},
);
ipcMain.handle("log:openDir", async () => {
try {
const currentPath = log.transports.file.getFile().path;
const logDir = currentPath
? path.dirname(currentPath)
: app.getPath("logs");
const latestPath = path.join(logDir, LATEST_LOG_BASENAME);
const fileToSelect = fs.existsSync(latestPath)
? latestPath
: currentPath || path.join(logDir, "main.log");
// 打开日志目录并尽量在资源管理器中选中 latest.log或 main.log便于用户直接看到当前日志入口
try {
if (process.platform === "darwin") {
execSync(`open -R "${fileToSelect}"`, { encoding: "utf-8" });
} else if (process.platform === "win32") {
const winPath = fileToSelect.replace(/\//g, "\\");
execSync(`explorer /select,"${winPath}"`, { encoding: "utf-8" });
} else {
await shell.openPath(logDir);
}
} catch (_) {
await shell.openPath(logDir);
}
return { success: true };
} catch (error) {
log.error("[IPC] log:openDir failed:", error);
return { success: false, error: String(error) };
}
});
// 应用日志列表:默认 2000 条,单次最多 10000 条offset 为“从最新往前跳过条数”,用于向上滚动加载更多
const DEFAULT_LOG_LIST = 2000;
const MAX_LOG_LIST = 10000;
ipcMain.handle(
"log:list",
async (_, count: number = DEFAULT_LOG_LIST, offset: number = 0) => {
try {
const currentPath = log.transports.file.getFile().path;
const logDir = currentPath
? path.dirname(currentPath)
: app.getPath("logs");
const latestPath = path.join(logDir, LATEST_LOG_BASENAME);
const logPath =
(fs.existsSync(latestPath) ? latestPath : currentPath) || currentPath;
if (!logPath || !fs.existsSync(logPath)) {
return [];
}
const content = fs.readFileSync(logPath, "utf-8");
const lines = content.split("\n").filter(Boolean);
const limit = Math.min(
Math.max(1, count ?? DEFAULT_LOG_LIST),
MAX_LOG_LIST,
);
const safeOffset = Math.max(0, offset);
// 取“从文件末尾往前 offset+limit 到 offset”的一段时间顺序旧→新
const slice =
safeOffset === 0
? lines.slice(-limit)
: lines.slice(-(safeOffset + limit), -safeOffset);
return slice.map((line) => {
const match = line.match(
/^\[(\d{4}-\d{2}-\d{2}\s\d{2}:\d{2}:\d{2}\.\d{3})\]\s\[(\w+)\]\s(.*)$/,
);
if (match) {
return {
timestamp: match[1],
level: match[2].toLowerCase(),
message: match[3],
};
}
return { timestamp: "", level: "info", message: line };
});
} catch (error) {
log.error("[IPC] log:list failed:", error);
return [];
}
},
);
// App handlers
ipcMain.handle("app:getVersion", () => {
return app.getVersion();
});
ipcMain.handle("app:getDeviceId", () => {
return getDeviceId();
});
ipcMain.handle("app:checkUpdate", async () => {
try {
return await checkForUpdates();
} catch (error) {
log.error("[IPC] app:checkUpdate failed:", error);
return { hasUpdate: false, error: String(error) };
}
});
ipcMain.handle("app:downloadUpdate", async () => {
try {
const result = await downloadUpdate();
log.info(
`[IPC] app:downloadUpdate → success=${result.success}, error="${result.error}"`,
);
return result;
} catch (error) {
log.error("[IPC] app:downloadUpdate failed:", error);
return { success: false, error: String(error) };
}
});
ipcMain.handle("app:installUpdate", () => {
try {
return installUpdate();
} catch (error) {
log.error("[IPC] app:installUpdate failed:", error);
return { success: false, error: String(error) };
}
});
ipcMain.handle("app:getUpdateState", () => {
return getUpdateState();
});
ipcMain.handle("app:openReleasesPage", async () => {
await openReleasesPage();
return { success: true };
});
// 调试:获取升级检测详细信息
ipcMain.handle("app:getUpdateDebugInfo", async () => {
try {
const { getInstallerType, canAutoUpdate } =
await import("../services/autoUpdater");
const installerType = getInstallerType();
const canUpdate = canAutoUpdate();
// 获取应用目录和文件列表(仅用于调试)
let appFiles: string[] = [];
if (process.platform === "win32") {
try {
const appDir = path.dirname(app.getPath("exe"));
appFiles = require("fs").readdirSync(appDir);
} catch (e) {
log.error("[IPC] Failed to read app directory for debug:", e);
}
}
// 查找可能的卸载程序
const uninstallers = appFiles.filter((f) => {
const lower = f.toLowerCase();
return lower.startsWith("uninstall") || lower.startsWith("unins");
});
return {
success: true,
platform: process.platform,
arch: process.arch,
isPackaged: app.isPackaged,
appVersion: app.getVersion(),
appName: app.getName(),
appPath: app.getAppPath(),
exePath: app.getPath("exe"),
installerType,
canAutoUpdate: canUpdate,
appDir:
process.platform === "win32"
? path.dirname(app.getPath("exe"))
: null,
uninstallerFiles: uninstallers,
totalAppFiles: appFiles.length,
};
} catch (error) {
log.error("[IPC] app:getUpdateDebugInfo failed:", error);
return { success: false, error: String(error) };
}
});
// Permissions (macOS)
ipcMain.handle("permissions:check", async () => {
if (process.platform !== "darwin") {
return [];
}
try {
const items = [
{
key: "accessibility",
name: t("Claw.PermissionsPage.macosAccessibility"),
description: t("Claw.PermissionsPage.macosAccessibilityDesc"),
status: systemPreferences.isTrustedAccessibilityClient(false)
? "granted"
: "denied",
},
{
key: "screen_recording",
name: t("Claw.PermissionsPage.macosScreenRecording"),
description: t("Claw.PermissionsPage.macosScreenRecordingDesc"),
status:
systemPreferences.getMediaAccessStatus("screen") === "granted"
? "granted"
: "denied",
},
{
key: "file_access",
name: t("Claw.PermissionsPage.macosFullDiskAccess"),
description: t("Claw.PermissionsPage.macosFullDiskAccessDesc"),
status: "unknown" as const,
},
];
return items;
} catch (error) {
log.error("[IPC] permissions:check failed:", error);
return [];
}
});
ipcMain.handle(
"permissions:openSettings",
async (_, permissionKey: string) => {
try {
if (process.platform !== "darwin") {
return { success: false, error: "Not macOS" };
}
const urlMap: Record<string, string> = {
accessibility:
"x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility",
screen_recording:
"x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture",
file_access:
"x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles",
};
const url = urlMap[permissionKey];
if (url) {
await shell.openExternal(url);
return { success: true };
}
return { success: false, error: "Unknown permission" };
} catch (error) {
log.error("[IPC] permissions:openSettings failed:", error);
return { success: false, error: String(error) };
}
},
);
// Shell
ipcMain.handle("shell:openExternal", async (_, url: string) => {
try {
await shell.openExternal(url);
return { success: true };
} catch (error) {
log.error("[IPC] shell:openExternal failed:", error);
return { success: false, error: String(error) };
}
});
// 打开本地路径(目录或文件),用于设置页等“在系统文件管理器中打开”场景。
// 说明Electron 的 shell.openPath 成功时返回空字符串,失败时返回错误文本。
ipcMain.handle("shell:openPath", async (_, targetPath: string) => {
try {
if (!targetPath || typeof targetPath !== "string") {
return { success: false, error: "Path is required" };
}
const openResult = await shell.openPath(targetPath);
if (openResult) {
return { success: false, error: openResult };
}
return { success: true };
} catch (error) {
log.error("[IPC] shell:openPath failed:", error);
return { success: false, error: String(error) };
}
});
// Dialog
ipcMain.handle("dialog:openDirectory", async (_, title?: string) => {
const mainWindow = ctx.getMainWindow();
if (!mainWindow) return { success: false, error: "No window" };
const result = await dialog.showOpenDialog(mainWindow, {
title: title || t("Claw.Settings.workspace.selectDir"),
properties: ["openDirectory", "createDirectory"],
});
if (result.canceled || result.filePaths.length === 0) {
return { success: false, canceled: true };
}
return { success: true, path: result.filePaths[0] };
});
// ========== Session / Cookie ==========
ipcMain.handle(
"session:setCookie",
async (
_,
params: {
url: string;
name: string;
value: string;
domain?: string;
expirationDate?: number;
httpOnly?: boolean;
secure?: boolean;
},
) => {
try {
await removeSameNameCookies({
url: params.url,
name: params.name,
domain: params.domain,
});
const cookieDetails: Electron.CookiesSetDetails = {
url: params.url,
name: params.name,
value: params.value,
path: "/",
httpOnly: params.httpOnly ?? true,
// secure 跟随 URL schemeHTTPS → trueHTTP → false
// 渲染进程不传时根据 URL 自动判断
secure: params.secure ?? params.url.startsWith("https://"),
};
const expirationDate =
typeof params.expirationDate === "number"
? params.expirationDate
: params.name === "ticket"
? resolveTicketExpirationDate(params.value)
: undefined;
if (typeof expirationDate === "number") {
cookieDetails.expirationDate = expirationDate;
}
if (params.domain) {
cookieDetails.domain = params.domain;
}
// Chromium rejects SameSite=None without Secure.
// For non-HTTPS domains, omit sameSite to keep cookie write compatible.
if (cookieDetails.secure) {
cookieDetails.sameSite = "no_restriction";
}
await electronSession.defaultSession.cookies.set(cookieDetails);
await electronSession.defaultSession.cookies.flushStore();
log.info("[IPC] session:setCookie success for domain:", params.domain);
return { success: true };
} catch (error) {
log.error("[IPC] session:setCookie failed:", error);
return { success: false, error: String(error) };
}
},
);
ipcMain.handle(
"session:getCookie",
async (
_,
params: {
url: string;
name: string;
},
) => {
try {
const cookies = await electronSession.defaultSession.cookies.get({
url: params.url,
name: params.name,
});
const hit = cookies[0];
if (!hit) return { success: true, found: false };
return {
success: true,
found: true,
count: cookies.length,
cookies: cookies.map((c) => ({
name: c.name,
domain: c.domain,
path: c.path,
httpOnly: c.httpOnly,
secure: c.secure,
sameSite: c.sameSite,
session: c.session,
expirationDate: c.expirationDate,
})),
cookie: {
name: hit.name,
domain: hit.domain,
path: hit.path,
httpOnly: hit.httpOnly,
secure: hit.secure,
sameSite: hit.sameSite,
session: hit.session,
expirationDate: hit.expirationDate,
},
};
} catch (error) {
log.error("[IPC] session:getCookie failed:", error);
return { success: false, error: String(error) };
}
},
);
ipcMain.handle(
"session:removeCookie",
async (_, params: { url: string; name: string }) => {
try {
await removeSameNameCookies({
url: params.url,
name: params.name,
});
await electronSession.defaultSession.cookies.flushStore();
log.info("[IPC] session:removeCookie success", {
url: params.url,
name: params.name,
});
return { success: true };
} catch (error) {
log.error("[IPC] session:removeCookie failed:", error);
return { success: false, error: String(error) };
}
},
);
ipcMain.handle("session:flushStore", async () => {
try {
await electronSession.defaultSession.cookies.flushStore();
return { success: true };
} catch (error) {
log.error("[IPC] session:flushStore failed:", error);
return { success: false, error: String(error) };
}
});
// ========== WebView Window ==========
const WEBVIEW_BOUNDS_KEY = "webview_window_bounds";
/**
* 打开独立的 WebView 窗口
* 使用 defaultSession与主窗口共享 Cookie
* 首次最大化,记住用户调整后的尺寸
*/
ipcMain.handle(
"webview:openWindow",
async (
_,
params: {
url: string;
title?: string;
},
) => {
try {
const { url, title } = params;
const syncTicketCookie = async (retries = 3): Promise<void> => {
if (!/^https?:\/\//i.test(url)) return;
const oneShotToken = readSetting("auth.token");
const domainTokenKey = getDomainTokenKey(url);
const domainToken = readSetting(domainTokenKey);
const hasOneShotToken =
typeof oneShotToken === "string" && oneShotToken.length > 0;
const token = hasOneShotToken ? oneShotToken : domainToken;
const tokenSource = hasOneShotToken ? "one_shot" : "domain_cache";
if (typeof token !== "string" || !token) {
log.debug("[IPC] webview:openWindow no token available for sync", {
url,
domainTokenKey,
oneShotTokenPresent: hasOneShotToken,
domainTokenPresent:
typeof domainToken === "string" && domainToken.length > 0,
});
return;
}
log.debug("[IPC] webview:openWindow token selected", {
url,
tokenSource,
domainTokenKey,
});
if (!hasOneShotToken) {
const current = await electronSession.defaultSession.cookies.get({
url,
name: "ticket",
});
// 检查已有 cookie 是否有效(未过期)
const hasValidCookie =
current.length > 0 &&
current.some((c) => {
// session cookie无 expirationDate视为有效
if (!c.expirationDate) return true;
// 持久 cookie仅在未过期时有效
return c.expirationDate * 1000 > Date.now();
});
if (hasValidCookie) {
// 域名缓存 token 仅做兜底;目标站点已有有效 ticket 时不覆盖
writeSetting("auth.token", null);
log.info(
"[IPC] webview:openWindow detected valid ticket, skip domain-token sync",
{
url,
count: current.length,
},
);
return;
}
// cookie 已过期或不存在,继续走同步流程
if (current.length > 0) {
log.info(
"[IPC] webview:openWindow detected expired ticket, re-syncing",
{
url,
count: current.length,
},
);
}
}
await removeSameNameCookies({
url,
name: "ticket",
domain: resolveCookieDomain(url),
});
const secure = url.startsWith("https://");
const cookieDetails: Electron.CookiesSetDetails = {
url,
name: "ticket",
value: token,
path: "/",
httpOnly: true,
secure,
};
const cookieDomain = resolveCookieDomain(url);
if (cookieDomain) {
cookieDetails.domain = cookieDomain;
}
const expirationDate = resolveTicketExpirationDate(token);
if (typeof expirationDate === "number") {
cookieDetails.expirationDate = expirationDate;
}
if (secure) {
cookieDetails.sameSite = "no_restriction";
}
await electronSession.defaultSession.cookies.set(cookieDetails);
await electronSession.defaultSession.cookies.flushStore();
writeSetting("auth.token", null);
log.debug("[IPC] webview:openWindow synced ticket cookie", {
url,
tokenSource,
secure,
domain: cookieDetails.domain || "(host-only)",
});
};
// 带重试的 Cookie 同步
const syncWithRetry = async (maxRetries = 3): Promise<void> => {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
await syncTicketCookie();
return;
} catch (error) {
if (attempt === maxRetries) {
throw error;
}
log.warn(
`[IPC] webview:openWindow ticket cookie sync attempt ${attempt}/${maxRetries} failed, retrying...`,
error,
);
await new Promise((resolve) =>
setTimeout(resolve, 100 * attempt),
);
}
}
};
try {
await syncWithRetry(3);
} catch (error) {
// 不阻塞页面打开;保留 token 供下次重试。
log.debug(
"[IPC] webview:openWindow ticket cookie sync failed after retries:",
error,
);
}
// 如果窗口已存在,聚焦并导航
if (webviewWindow && !webviewWindow.isDestroyed()) {
webviewWindow.loadURL(url);
webviewWindow.focus();
if (title) webviewWindow.setTitle(title);
log.debug(
"[IPC] webview:openWindow - reused existing window for:",
url,
);
return { success: true, reused: true };
}
// 获取图标路径
const getIconPath = () => {
if (app.isPackaged) {
if (process.platform === "darwin") {
return path.join(process.resourcesPath, "icon.icns");
}
return path.join(process.resourcesPath, "icon.png");
}
if (process.platform === "darwin") {
return path.join(process.cwd(), "public", "icon.icns");
}
return path.join(process.cwd(), "public", "icon.png");
};
// 读取上次保存的窗口尺寸
let savedBounds: {
width: number;
height: number;
x?: number;
y?: number;
maximized?: boolean;
} | null = null;
try {
const saved = await ctx
.getMainWindow()
?.webContents?.executeJavaScript(
`localStorage.getItem('${WEBVIEW_BOUNDS_KEY}')`,
);
if (saved && typeof saved === "string") {
savedBounds = JSON.parse(saved);
}
} catch {
// 忽略读取错误
}
// 创建新窗口
webviewWindow = new BrowserWindow({
width: savedBounds?.width || 1200,
height: savedBounds?.height || 800,
x: savedBounds?.x,
y: savedBounds?.y,
minWidth: 600,
minHeight: 400,
title: title || `${APP_DISPLAY_NAME} - ${t("Claw.Sessions.title")}`,
icon: getIconPath(),
webPreferences: {
// 使用 defaultSession与主窗口共享 Cookie
contextIsolation: true,
nodeIntegration: false,
sandbox: true,
webviewTag: false, // 直接加载 URL不需要 webview tag
spellcheck: false, // 禁用拼写检查
},
show: false,
backgroundColor: "#ffffff",
});
// 首次打开时最大化(没有保存的尺寸时)
if (!savedBounds) {
webviewWindow.maximize();
} else if (savedBounds.maximized) {
webviewWindow.maximize();
}
// 加载 URL
await webviewWindow.loadURL(url);
// 窗口显示
webviewWindow.once("ready-to-show", () => {
webviewWindow?.show();
log.debug("[IPC] webview:openWindow - window shown for:", url);
});
// 保存窗口尺寸(防抖)
let saveBoundsTimeout: NodeJS.Timeout | null = null;
const saveBounds = () => {
if (saveBoundsTimeout) clearTimeout(saveBoundsTimeout);
saveBoundsTimeout = setTimeout(() => {
if (webviewWindow && !webviewWindow.isDestroyed()) {
const bounds = webviewWindow.getBounds();
const isMaximized = webviewWindow.isMaximized();
const data = JSON.stringify({
...bounds,
maximized: isMaximized,
});
// 写入 localStorage通过主窗口
ctx
.getMainWindow()
?.webContents?.executeJavaScript(
`localStorage.setItem('${WEBVIEW_BOUNDS_KEY}', '${data}')`,
)
.catch(() => {});
}
}, 500);
};
// 监听窗口调整和移动
webviewWindow.on("resize", saveBounds);
webviewWindow.on("move", saveBounds);
webviewWindow.on("maximize", saveBounds);
webviewWindow.on("unmaximize", saveBounds);
// 窗口关闭时清理引用
webviewWindow.on("closed", () => {
webviewWindow = null;
if (saveBoundsTimeout) clearTimeout(saveBoundsTimeout);
log.debug("[IPC] webview:openWindow - window closed");
});
return { success: true, reused: false };
} catch (error) {
log.error("[IPC] webview:openWindow failed:", error);
return { success: false, error: String(error) };
}
},
);
/**
* 关闭 WebView 窗口
*/
ipcMain.handle("webview:closeWindow", async () => {
try {
if (webviewWindow && !webviewWindow.isDestroyed()) {
webviewWindow.close();
webviewWindow = null;
}
return { success: true };
} catch (error) {
log.error("[IPC] webview:closeWindow failed:", error);
return { success: false, error: String(error) };
}
});
/**
* 检查 WebView 窗口是否打开
*/
ipcMain.handle("webview:isWindowOpen", async () => {
return webviewWindow !== null && !webviewWindow.isDestroyed();
});
}

View File

@@ -0,0 +1,135 @@
import { ipcMain } from "electron";
import { agentService } from "../services/engines/unifiedAgent";
import type {
ComputerChatRequest,
ComputerAgentStatusResponse,
ComputerAgentStopResponse,
ComputerAgentCancelResponse,
HttpResult,
} from "../services/engines/unifiedAgent";
export function registerComputerHandlers(): void {
ipcMain.handle("computer:chat", async (_, request: ComputerChatRequest) => {
// 与 HTTP 路径一致:按 project_id 路由到对应 AcpEngine
let acpEngine;
try {
acpEngine = await agentService.ensureEngineForRequest(request);
} catch (err: any) {
return {
code: "5000",
message: err.message || "Engine switch failed",
data: null,
tid: null,
success: false,
} as HttpResult;
}
if (!acpEngine) {
return {
code: "5000",
message: "Agent not initialized",
data: null,
tid: null,
success: false,
} as HttpResult;
}
return acpEngine.chat(request);
});
ipcMain.handle(
"computer:agentStatus",
async (_, request: { user_id: string; project_id?: string }) => {
const projectId = request.project_id || "";
const projectEngine = agentService.getEngineForProject(projectId);
const acpEngine = projectEngine || agentService.getAcpEngine();
const session = acpEngine?.findSessionByProjectId(projectId) ?? null;
const response: ComputerAgentStatusResponse = {
user_id: request.user_id,
project_id: projectId,
is_alive: !!projectEngine,
session_id: session?.id ?? null,
status: session
? session.status === "active"
? "Busy"
: "Idle"
: null,
last_activity: session?.lastActivity
? new Date(session.lastActivity).toISOString()
: null,
created_at: session ? new Date(session.createdAt).toISOString() : null,
};
return {
code: "0000",
message: "success",
data: response,
tid: null,
success: true,
} as HttpResult<ComputerAgentStatusResponse>;
},
);
ipcMain.handle(
"computer:agentStop",
async (_, request: { user_id: string; project_id?: string }) => {
const projectId = request.project_id || "";
const acpEngine = projectId
? agentService.getEngineForProject(projectId)
: agentService.getAcpEngine();
if (acpEngine) {
await agentService.stopEngine(projectId || undefined);
}
const response: ComputerAgentStopResponse = {
success: true,
message: acpEngine
? "Agent stopped successfully"
: "Agent not found (already stopped)",
user_id: request.user_id,
project_id: projectId,
};
return {
code: "0000",
message: "success",
data: response,
tid: null,
success: true,
} as HttpResult<ComputerAgentStopResponse>;
},
);
ipcMain.handle(
"computer:cancelSession",
async (
_,
request: { user_id: string; project_id?: string; session_id?: string },
) => {
const projectId = request.project_id || "";
const acpEngine =
agentService.getEngineForProject(projectId) ||
agentService.getAcpEngine();
if (acpEngine && request.session_id) {
await acpEngine.abortSession(request.session_id);
} else if (acpEngine && projectId) {
const session = acpEngine.findSessionByProjectId(projectId);
if (session) await acpEngine.abortSession(session.id);
}
const response: ComputerAgentCancelResponse = {
success: true,
session_id: request.session_id || "",
};
return {
code: "0000",
message: "success",
data: response,
tid: null,
success: true,
} as HttpResult<ComputerAgentCancelResponse>;
},
);
ipcMain.handle("computer:health", async () => {
return {
status: agentService.isReady ? "healthy" : "offline",
engineType: agentService.getEngineType(),
timestamp: new Date().toISOString(),
};
});
}

View File

@@ -0,0 +1,153 @@
import { ipcMain } from "electron";
import log from "electron-log";
import { isDepsSyncInProgress } from "../bootstrap/startup";
export function registerDependencyHandlers(): void {
ipcMain.handle(
"dependencies:checkAll",
async (_, options?: { checkLatest?: boolean }) => {
const { checkAllDependencies } =
await import("../services/system/dependencies");
log.info("[IPC] Checking all dependencies...");
try {
const results = await checkAllDependencies(options);
return {
success: true,
results,
syncInProgress: isDepsSyncInProgress(),
};
} catch (error) {
log.error("[IPC] Dependency check failed:", error);
return { success: false, error: String(error) };
}
},
);
ipcMain.handle("dependencies:checkNode", async () => {
const { checkNodeVersion } =
await import("../services/system/dependencies");
try {
const result = await checkNodeVersion();
return { success: true, ...result };
} catch (error) {
return { success: false, error: String(error) };
}
});
ipcMain.handle("dependencies:checkUv", async () => {
const { checkUvVersion } = await import("../services/system/dependencies");
try {
const result = await checkUvVersion();
return { success: true, ...result };
} catch (error) {
return { success: false, error: String(error) };
}
});
/** 检测应用包内集成的 qiming-mcp-stdio-proxy与 Node/uv 一起在系统环境中展示 */
ipcMain.handle("dependencies:checkMcpProxyBundled", async () => {
const { checkMcpProxyBundled } =
await import("../services/system/dependencies");
try {
const result = await checkMcpProxyBundled();
return { success: true, ...result };
} catch (error) {
return { success: false, available: false, error: String(error) };
}
});
/** 检测应用包内集成的 qimingcode 引擎二进制 */
ipcMain.handle("dependencies:checkQimingCodeBundled", async () => {
const { checkQimingCodeBundled } =
await import("../services/system/dependencies");
try {
const result = await checkQimingCodeBundled();
return { success: true, ...result };
} catch (error) {
return { success: false, available: false, error: String(error) };
}
});
/** 检测应用包内集成的 claude-code-acp-ts */
ipcMain.handle("dependencies:checkClaudeCodeAcpBundled", async () => {
const { checkClaudeCodeAcpBundled } =
await import("../services/system/dependencies");
try {
const result = await checkClaudeCodeAcpBundled();
return { success: true, ...result };
} catch (error) {
return { success: false, available: false, error: String(error) };
}
});
/** 检测应用包内集成的 qiming-file-server */
ipcMain.handle("dependencies:checkQimingFileServerBundled", async () => {
const { checkQimingFileServerBundled } =
await import("../services/system/dependencies");
try {
const result = await checkQimingFileServerBundled();
return { success: true, ...result };
} catch (error) {
return { success: false, available: false, error: String(error) };
}
});
ipcMain.handle(
"dependencies:detectPackage",
async (_, packageName: string, binName?: string) => {
const { detectNpmPackage } =
await import("../services/system/dependencies");
try {
const result = await detectNpmPackage(packageName, binName);
return { success: true, ...result };
} catch (error) {
return { success: false, error: String(error) };
}
},
);
ipcMain.handle(
"dependencies:installPackage",
async (
_,
packageName: string,
options?: { registry?: string; version?: string },
) => {
const { installNpmPackage } =
await import("../services/system/dependencies");
log.info(`[IPC] Installing package: ${packageName}`);
try {
const result = await installNpmPackage(packageName, options);
return result;
} catch (error) {
log.error("[IPC] Install failed:", error);
return { success: false, error: String(error) };
}
},
);
ipcMain.handle("dependencies:installMissing", async () => {
const { installMissingDependencies } =
await import("../services/system/dependencies");
log.info("[IPC] Installing missing dependencies...");
try {
const result = await installMissingDependencies();
return result;
} catch (error) {
log.error("[IPC] Install missing failed:", error);
return { success: false, error: String(error) };
}
});
ipcMain.handle("dependencies:getAppDataDir", async () => {
const { getAppDataDir } =
await import("../services/system/workspaceManager");
return getAppDataDir();
});
ipcMain.handle("dependencies:getRequiredList", async () => {
const { getSetupRequiredDependencies } =
await import("../services/system/dependencies");
return getSetupRequiredDependencies();
});
}

View File

@@ -0,0 +1,64 @@
import { ipcMain } from 'electron';
import log from 'electron-log';
import type { AgentEngine } from '../services/engines/engineManager';
export function registerEngineHandlers(): void {
ipcMain.handle('engine:checkLocal', async (_, engine: string) => {
const { isEngineInstalledLocally } = await import('../services/engines/engineManager');
return isEngineInstalledLocally(engine as AgentEngine);
});
ipcMain.handle('engine:checkGlobal', async (_, engine: string) => {
const { isEngineInstalledGlobally } = await import('../services/engines/engineManager');
return isEngineInstalledGlobally(engine as AgentEngine);
});
ipcMain.handle('engine:getVersion', async (_, engine: string) => {
const { getEngineVersion } = await import('../services/engines/engineManager');
return getEngineVersion(engine as AgentEngine);
});
ipcMain.handle('engine:findBinary', async (_, engine: string) => {
const { findEngineBinary } = await import('../services/engines/engineManager');
return findEngineBinary(engine as AgentEngine);
});
ipcMain.handle('engine:install', async (_, engine: string, options?: { registry?: string }) => {
const { installEngine } = await import('../services/engines/engineManager');
log.info(`[IPC] Installing engine: ${engine}`);
return installEngine(engine as AgentEngine, options);
});
ipcMain.handle('engine:start', async (_, config: {
engine: string;
apiKey?: string;
baseUrl?: string;
model?: string;
workspaceDir?: string;
}) => {
const { startEngine } = await import('../services/engines/engineManager');
log.info(`[IPC] Starting engine: ${config.engine}`);
return startEngine({ ...config, engine: config.engine as AgentEngine });
});
ipcMain.handle('engine:stop', async (_, engineId: string) => {
const { stopEngine } = await import('../services/engines/engineManager');
return stopEngine(engineId);
});
ipcMain.handle('engine:status', async (_, engineId?: string) => {
const { getEngineStatus } = await import('../services/engines/engineManager');
return getEngineStatus(engineId);
});
ipcMain.handle('engine:send', async (_, engineId: string, message: string) => {
const { sendToEngine } = await import('../services/engines/engineManager');
return sendToEngine(engineId, message);
});
ipcMain.handle('engine:stopAll', async () => {
const { stopAllEngines } = await import('../services/engines/engineManager');
await stopAllEngines();
return { success: true };
});
}

View File

@@ -0,0 +1,162 @@
/**
* 单元测试: eventForwarders — 监听器生命周期管理
*
* 覆盖内容:
* - registerEventForwarders 注册监听器到 agentService
* - unregisterEventForwarders 移除所有已注册监听器
* - 重复调用 registerEventForwarders 不会叠加监听器(幂等性)
* - 事件转发到 renderer webContents
*/
import { describe, it, expect, vi, beforeEach } from "vitest";
vi.mock("electron-log", () => ({
default: {
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
debug: vi.fn(),
},
}));
vi.mock("../services/computerServer", () => ({
pushSseEvent: vi.fn(),
}));
// Use dynamic import to get the actual agentService (an EventEmitter) after mocking
// The unifiedAgent module exports agentService as a singleton EventEmitter
vi.mock("../services/engines/unifiedAgent", async () => {
const { EventEmitter } = await import("events");
return {
agentService: new EventEmitter(),
};
});
import {
registerEventForwarders,
unregisterEventForwarders,
} from "./eventForwarders";
import { agentService } from "../services/engines/unifiedAgent";
import type { HandlerContext } from "@shared/types/ipc";
function createMockCtx() {
const send = vi.fn();
return {
ctx: {
getMainWindow: () => ({ webContents: { send } }) as any,
lanproxy: {} as any,
fileServer: {} as any,
agentRunner: {} as any,
guiServer: {} as any,
agentRunnerPorts: null,
setAgentRunnerPorts: vi.fn(),
} as HandlerContext,
send,
};
}
describe("eventForwarders", () => {
beforeEach(() => {
(agentService as any).removeAllListeners();
unregisterEventForwarders();
vi.clearAllMocks();
});
it("registerEventForwarders registers listeners on agentService", () => {
const { ctx } = createMockCtx();
registerEventForwarders(ctx);
expect(agentService.listenerCount("message.updated")).toBe(1);
expect(agentService.listenerCount("session.created")).toBe(1);
expect(agentService.listenerCount("error")).toBe(1);
expect(agentService.listenerCount("ready")).toBe(1);
expect(agentService.listenerCount("destroyed")).toBe(1);
expect(agentService.listenerCount("computer:progress")).toBe(1);
expect(agentService.listenerCount("computer:promptStart")).toBe(1);
expect(agentService.listenerCount("computer:promptEnd")).toBe(1);
});
it("unregisterEventForwarders removes all registered listeners", () => {
const { ctx } = createMockCtx();
registerEventForwarders(ctx);
unregisterEventForwarders();
expect(agentService.listenerCount("message.updated")).toBe(0);
expect(agentService.listenerCount("error")).toBe(0);
expect(agentService.listenerCount("ready")).toBe(0);
expect(agentService.listenerCount("destroyed")).toBe(0);
expect(agentService.listenerCount("computer:progress")).toBe(0);
expect(agentService.listenerCount("computer:promptStart")).toBe(0);
expect(agentService.listenerCount("computer:promptEnd")).toBe(0);
});
it("registerEventForwarders is idempotent — no listener stacking", () => {
const { ctx } = createMockCtx();
registerEventForwarders(ctx);
registerEventForwarders(ctx);
registerEventForwarders(ctx);
expect(agentService.listenerCount("message.updated")).toBe(1);
expect(agentService.listenerCount("error")).toBe(1);
expect(agentService.listenerCount("computer:progress")).toBe(1);
});
it("unregisterEventForwarders is safe to call when nothing is registered", () => {
expect(() => unregisterEventForwarders()).not.toThrow();
});
it("SSE event forwards to renderer via webContents.send", () => {
const { ctx, send } = createMockCtx();
registerEventForwarders(ctx);
const testData = { sessionId: "test-123", status: "active" };
agentService.emit("session.created", testData);
expect(send).toHaveBeenCalledWith("agent:event", {
type: "session.created",
data: testData,
});
});
it("error event forwards error message to renderer", () => {
const { ctx, send } = createMockCtx();
registerEventForwarders(ctx);
agentService.emit("error", new Error("test error"));
expect(send).toHaveBeenCalledWith("agent:event", {
type: "error",
data: { message: "test error" },
});
});
it("ready and destroyed events forward to renderer", () => {
const { ctx, send } = createMockCtx();
registerEventForwarders(ctx);
agentService.emit("ready");
expect(send).toHaveBeenCalledWith("agent:event", {
type: "ready",
data: {},
});
agentService.emit("destroyed");
expect(send).toHaveBeenCalledWith("agent:event", {
type: "destroyed",
data: {},
});
});
it("after unregister, events no longer forward", () => {
const { ctx, send } = createMockCtx();
registerEventForwarders(ctx);
unregisterEventForwarders();
agentService.emit("session.created", { test: true });
// Note: not emitting 'error' here — EventEmitter throws unhandled errors with no listeners
agentService.emit("ready");
expect(send).not.toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,176 @@
import { agentService } from "../services/engines/unifiedAgent";
import type { UnifiedSessionMessage } from "../services/engines/unifiedAgent";
import { pushSseEvent } from "../services/computerServer";
import { firstTokenTrace } from "../services/engines/perf/firstTokenTrace";
import type { HandlerContext } from "@shared/types/ipc";
import log from "electron-log";
/** Stored handlers for cleanup */
const registeredHandlers: Array<{
event: string;
handler: (...args: any[]) => void;
}> = [];
/**
* Unregister all event forwarders from agentService.
* Call before re-registering or during cleanup.
*/
export function unregisterEventForwarders(): void {
if (registeredHandlers.length === 0) return;
for (const { event, handler } of registeredHandlers) {
agentService.off(event, handler);
}
registeredHandlers.length = 0;
log.info("[EventForwarders] Unregistered all event forwarders");
}
export function registerEventForwarders(ctx: HandlerContext): void {
// Ensure idempotency: unregister any previous handlers first
unregisterEventForwarders();
// Forward all agent SSE events to the renderer
const sseEventTypes = [
"message.updated",
"message.removed",
"message.part.updated",
"message.part.removed",
"permission.updated",
"permission.replied",
"session.created",
"session.updated",
"session.deleted",
"session.status",
"session.idle",
"session.error",
"session.diff",
"file.edited",
"server.connected",
];
log.info(
"[EventForwarders] Registering event forwarders for:",
sseEventTypes.join(", "),
);
for (const eventType of sseEventTypes) {
const handler = (data: unknown) => {
// Debug: log message events
if (eventType.startsWith("message")) {
log.debug(
`[EventForwarders] 📨 Received ${eventType}:`,
JSON.stringify(data).substring(0, 200),
);
}
ctx.getMainWindow()?.webContents.send("agent:event", {
type: eventType,
data,
});
};
agentService.on(eventType, handler);
registeredHandlers.push({ event: eventType, handler });
}
const errorHandler = (error: Error) => {
ctx.getMainWindow()?.webContents.send("agent:event", {
type: "error",
data: { message: error.message },
});
};
agentService.on("error", errorHandler);
registeredHandlers.push({ event: "error", handler: errorHandler });
const readyHandler = () => {
ctx.getMainWindow()?.webContents.send("agent:event", {
type: "ready",
data: {},
});
};
agentService.on("ready", readyHandler);
registeredHandlers.push({ event: "ready", handler: readyHandler });
const destroyedHandler = () => {
ctx.getMainWindow()?.webContents.send("agent:event", {
type: "destroyed",
data: {},
});
};
agentService.on("destroyed", destroyedHandler);
registeredHandlers.push({ event: "destroyed", handler: destroyedHandler });
// ==================== computer:* Event Forwarding (rcoder camelCase format) ====================
const progressHandler = (data: unknown) => {
log.debug(
"[EventForwarders] 📨 Received computer:progress:",
JSON.stringify(data).substring(0, 200),
);
ctx.getMainWindow()?.webContents.send("computer:progress", data);
const d = data as UnifiedSessionMessage;
if (d?.sessionId) {
// sessionId is now the ACP protocol UUID — same as acpSessionId
log.debug(
`[EventForwarders] 📤 Pushing SSE event: sessionId=${d.sessionId}, subType=${d.subType}`,
);
pushSseEvent(d.sessionId, d.subType || "message", d);
}
};
agentService.on("computer:progress", progressHandler);
registeredHandlers.push({
event: "computer:progress",
handler: progressHandler,
});
const promptStartHandler = (data: {
sessionId: string;
acpSessionId?: string;
requestId?: string;
}) => {
firstTokenTrace.trace("event.prompt_start.forward", {
requestId: data.requestId,
sessionId: data.sessionId,
});
// sessionId is now the ACP protocol UUID
const event: UnifiedSessionMessage = {
sessionId: data.sessionId,
acpSessionId: data.acpSessionId,
messageType: "sessionPromptStart",
subType: "prompt_start",
data: { request_id: data.requestId },
timestamp: new Date().toISOString(),
};
ctx.getMainWindow()?.webContents.send("computer:progress", event);
pushSseEvent(data.sessionId, "prompt_start", event);
};
agentService.on("computer:promptStart", promptStartHandler);
registeredHandlers.push({
event: "computer:promptStart",
handler: promptStartHandler,
});
const promptEndHandler = (data: {
sessionId: string;
acpSessionId?: string;
reason?: string;
description?: string;
}) => {
firstTokenTrace.trace("event.prompt_end.forward", {
sessionId: data.sessionId,
});
// sessionId is now the ACP protocol UUID
const event: UnifiedSessionMessage = {
sessionId: data.sessionId,
acpSessionId: data.acpSessionId,
messageType: "sessionPromptEnd",
subType: data.reason || "end_turn",
data: { reason: data.reason, description: data.description },
timestamp: new Date().toISOString(),
};
ctx.getMainWindow()?.webContents.send("computer:progress", event);
pushSseEvent(data.sessionId, data.reason || "end_turn", event);
};
agentService.on("computer:promptEnd", promptEndHandler);
registeredHandlers.push({
event: "computer:promptEnd",
handler: promptEndHandler,
});
}

View File

@@ -0,0 +1,154 @@
/**
* GUI Server IPC Handlers
*
* 注册 guiServer:start / guiServer:stop / guiServer:status / guiServer:isEnabled / guiServer:setEnabled IPC handlers
* 委托给 guiAgentServer.ts非 Windows和 windowsMcp.tsWindows实现。
*/
import { ipcMain } from "electron";
import log from "electron-log";
import {
startGuiAgentServer,
stopGuiAgentServer,
getGuiAgentServerStatus,
} from "../services/packages/guiAgentServer";
import {
startWindowsMcp,
stopWindowsMcp,
getWindowsMcpStatus,
} from "../services/packages/windowsMcp";
import { isWindows } from "../services/system/shellEnv";
import { FEATURES } from "@shared/featureFlags";
import { readSetting, writeSetting } from "../db";
/** 从 step1_config 读取 guiMcpEnabled 运行时开关 */
function getGuiMcpEnabled(): boolean {
try {
const config = readSetting("step1_config") as {
guiMcpEnabled?: boolean;
} | null;
// 兼容老配置:历史 step1_config 可能没有 guiMcpEnabled 字段。
// 缺省按“关闭”处理,设置页初始开关默认关闭。
return config?.guiMcpEnabled ?? false;
} catch {
return false;
}
}
/** 写入 guiMcpEnabled 到 step1_config */
function setGuiMcpEnabled(enabled: boolean): void {
const existing = readSetting("step1_config") as Record<
string,
unknown
> | null;
writeSetting("step1_config", { ...(existing || {}), guiMcpEnabled: enabled });
}
export function registerGuiServerHandlers(): void {
// ===== guiServer:isEnabled =====
ipcMain.handle("guiServer:isEnabled", async () => {
if (!FEATURES.ENABLE_GUI_AGENT_SERVER) {
return { enabled: false, reason: "not_available" };
}
return { enabled: getGuiMcpEnabled(), reason: "ok" };
});
// ===== guiServer:setEnabled =====
ipcMain.handle("guiServer:setEnabled", async (_, enabled: boolean) => {
if (!FEATURES.ENABLE_GUI_AGENT_SERVER) {
return { success: false, error: "GUI Agent Server is not available" };
}
try {
// 先停服务
if (isWindows()) {
await stopWindowsMcp();
} else {
await stopGuiAgentServer();
}
// 保存开关状态
setGuiMcpEnabled(enabled);
// 如果是启用,则重新启动
if (enabled) {
if (isWindows()) {
return await startWindowsMcp();
} else {
return await startGuiAgentServer();
}
}
return { success: true };
} catch (e) {
const msg = e instanceof Error ? e.message : String(e);
log.error("[IPC] guiServer:setEnabled error:", msg);
return { success: false, error: msg };
}
});
// ===== guiServer:start =====
ipcMain.handle("guiServer:start", async () => {
if (!FEATURES.ENABLE_GUI_AGENT_SERVER) {
return { success: false, error: "GUI Agent Server is not available" };
}
if (!getGuiMcpEnabled()) {
return { success: false, error: "GUI MCP is disabled in settings" };
}
try {
// 手动点击启动:先 stop 再 start尽量清掉残留 uv/python 进程并释放 GUI MCP 端口
if (isWindows()) {
try {
await stopWindowsMcp();
} catch (preStopErr) {
log.warn("[IPC] guiServer:start pre-stop Windows MCP:", preStopErr);
}
return await startWindowsMcp();
}
try {
await stopGuiAgentServer();
} catch (preStopErr) {
log.warn("[IPC] guiServer:start pre-stop GUI Agent:", preStopErr);
}
return await startGuiAgentServer();
} catch (e) {
const msg = e instanceof Error ? e.message : String(e);
log.error("[IPC] guiServer:start error:", msg);
return { success: false, error: msg };
}
});
// ===== guiServer:stop =====
ipcMain.handle("guiServer:stop", async () => {
if (!FEATURES.ENABLE_GUI_AGENT_SERVER) {
return { success: false, error: "GUI Agent Server is not available" };
}
try {
if (isWindows()) {
return await stopWindowsMcp();
} else {
return await stopGuiAgentServer();
}
} catch (e) {
const msg = e instanceof Error ? e.message : String(e);
log.error("[IPC] guiServer:stop error:", msg);
return { success: false, error: msg };
}
});
// ===== guiServer:status =====
ipcMain.handle("guiServer:status", async () => {
if (!FEATURES.ENABLE_GUI_AGENT_SERVER) {
return { running: false, error: "GUI Agent Server is not available" };
}
try {
if (isWindows()) {
return getWindowsMcpStatus();
} else {
return getGuiAgentServerStatus();
}
} catch (e) {
const msg = e instanceof Error ? e.message : String(e);
log.error("[IPC] guiServer:status error:", msg);
return { running: false, error: msg };
}
});
log.info("[IPC] guiServer handlers registered");
}

View File

@@ -0,0 +1,46 @@
/**
* i18n IPC 通道
*
* 提供主进程语言同步的 IPC 接口
*
* @version 1.0.0
* @updated 2026-04-07
*/
import { ipcMain } from "electron";
import log from "electron-log";
import { setMainLang, getMainLang } from "../services/i18n";
import { getTrayManager } from "../window/trayManager";
/**
* 注册 i18n IPC 通道
*/
export function registerI18nHandlers(): void {
/**
* 获取当前主进程语言
* @channel i18n:getLang
*/
ipcMain.handle("i18n:getLang", () => {
return getMainLang();
});
/**
* 切换主进程语言
* @channel i18n:setLang
* @param lang 语言代码
*/
ipcMain.handle("i18n:setLang", async (_event, lang: string) => {
try {
setMainLang(lang);
// 通知托盘刷新菜单和 tooltip
getTrayManager()?.refresh();
log.info(`[IPC i18n] Language changed to: ${lang}`);
return { success: true };
} catch (error) {
log.error("[IPC i18n] Failed to change language:", error);
return { success: false, error: String(error) };
}
});
log.info("[IPC] i18n handlers registered");
}

View File

@@ -0,0 +1,37 @@
import type { HandlerContext } from "@shared/types/ipc";
import { registerWindowHandlers } from "./windowHandlers";
import { registerSettingsHandlers } from "./settingsHandlers";
import { registerMcpHandlers } from "./mcpHandlers";
import { registerAgentHandlers } from "./agentHandlers";
import { registerComputerHandlers } from "./computerHandlers";
import { registerProcessHandlers } from "./processHandlers";
import { registerDependencyHandlers } from "./dependencyHandlers";
import { registerEngineHandlers } from "./engineHandlers";
import { registerAppHandlers } from "./appHandlers";
import { registerEventForwarders } from "./eventForwarders";
import { registerMemoryHandlers } from "./memoryHandlers";
import { registerSandboxHandlers } from "./sandboxHandlers";
import { registerPerfHandlers } from "./perfHandlers";
import { registerGuiServerHandlers } from "./guiServerHandlers";
import { registerI18nHandlers } from "./i18nHandlers";
import log from "electron-log";
export function registerAllHandlers(ctx: HandlerContext): void {
registerWindowHandlers(ctx);
registerSettingsHandlers();
registerMcpHandlers();
registerAgentHandlers();
registerComputerHandlers();
registerProcessHandlers(ctx);
registerDependencyHandlers();
registerEngineHandlers();
registerAppHandlers(ctx);
registerEventForwarders(ctx);
registerMemoryHandlers();
registerSandboxHandlers();
registerPerfHandlers();
registerGuiServerHandlers();
registerI18nHandlers();
log.info("IPC handlers registered");
}

View File

@@ -0,0 +1,137 @@
import { ipcMain, dialog } from "electron";
import { getDb } from "../db";
import {
mcpProxyManager,
DEFAULT_MCP_PROXY_CONFIG,
discoverMcpTools,
} from "../services/packages/mcp";
import type { McpServersConfig } from "../services/packages/mcp";
import log from "electron-log";
import * as fs from "fs";
import * as path from "path";
export function registerMcpHandlers(): void {
// 启动 MCP Proxy仅验证 binary 可用性)
ipcMain.handle("mcp:start", async () => {
return mcpProxyManager.start();
});
// 停止 MCP Proxyno-op
ipcMain.handle("mcp:stop", async () => {
return mcpProxyManager.stop();
});
// 重启 MCP Proxy仅验证 binary 可用性)
ipcMain.handle("mcp:restart", async () => {
return mcpProxyManager.restart();
});
// 获取运行状态
ipcMain.handle("mcp:status", async () => {
return mcpProxyManager.getStatus();
});
// 获取本地配置(仅用户配置的 MCP不包括 ACP 动态下发的)
ipcMain.handle("mcp:getConfig", async () => {
const db = getDb();
const saved = db
?.prepare("SELECT value FROM settings WHERE key = ?")
.get("mcp_local_config") as { value: string } | undefined;
if (saved) {
try {
return JSON.parse(saved.value);
} catch (e) {
log.warn("[McpProxy] Config JSON parse failed, using default:", e);
}
}
// 返回空配置(不包括默认的 chrome-devtools那是系统级的
return { mcpServers: {} };
});
// 保存本地配置
ipcMain.handle("mcp:setConfig", async (_, config: McpServersConfig) => {
try {
const db = getDb();
const configJson = JSON.stringify(config);
db?.prepare(
"INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)",
).run("mcp_local_config", configJson);
// 注意:不再调用 mcpProxyManager.setConfig()
// 因为 mcpProxyManager 的配置应该由 syncMcpConfigToProxyAndReload() 统一管理
log.info("[McpProxy] Local config saved");
return { success: true };
} catch (error) {
return { success: false, error: String(error) };
}
});
// 发现 MCP 工具
ipcMain.handle("mcp:discoverTools", async (_, serverId: string) => {
try {
const tools = await discoverMcpTools(serverId);
return { success: true, tools };
} catch (error) {
log.error("[McpProxy] Tool discovery failed:", error);
return {
success: false,
error: error instanceof Error ? error.message : String(error),
};
}
});
// 导出配置到文件
ipcMain.handle("mcp:exportConfig", async () => {
try {
const db = getDb();
const saved = db
?.prepare("SELECT value FROM settings WHERE key = ?")
.get("mcp_local_config") as { value: string } | undefined;
const config = saved ? JSON.parse(saved.value) : { mcpServers: {} };
// 打开保存对话框
const result = await dialog.showSaveDialog({
title: "导出 MCP 配置",
defaultPath: `mcp-config-${Date.now()}.json`,
filters: [{ name: "JSON", extensions: ["json"] }],
});
if (result.canceled || !result.filePath) {
return { success: false, error: "User cancelled" };
}
// 写入文件
fs.writeFileSync(
result.filePath,
JSON.stringify(config, null, 2),
"utf-8",
);
// 设置文件权限为 0600仅所有者可读写
try {
fs.chmodSync(result.filePath, 0o600);
} catch (chmodError) {
log.warn("[McpProxy] Failed to set file permissions:", chmodError);
}
log.info("[McpProxy] Config exported to:", result.filePath);
return { success: true, filePath: result.filePath };
} catch (error) {
log.error("[McpProxy] Export config failed:", error);
return { success: false, error: String(error) };
}
});
// 获取端口deprecated no-op
ipcMain.handle("mcp:getPort", async () => {
return 0;
});
// 保存端口deprecated no-op
ipcMain.handle("mcp:setPort", async () => {
return { success: true };
});
}

View File

@@ -0,0 +1,265 @@
/**
* Memory IPC Handlers
*
* IPC handlers for memory service operations
* Based on specs/long-memory/long-memory.md
*/
import { ipcMain } from 'electron';
import log from 'electron-log';
import { memoryService } from '../services/memory';
import type {
MemoryConfig,
MemoryEntry,
HybridSearchOptions,
InjectionOptions,
ModelConfig,
} from '../services/memory/types';
export function registerMemoryHandlers(): void {
// === Lifecycle ===
ipcMain.handle('memory:init', async (_, workspaceDir: string, config?: Partial<MemoryConfig>) => {
try {
const success = await memoryService.init(workspaceDir, config);
return { success };
} catch (error) {
log.error('[IPC] memory:init failed:', error);
return { success: false, error: String(error) };
}
});
ipcMain.handle('memory:destroy', async () => {
try {
await memoryService.destroy();
return { success: true };
} catch (error) {
log.error('[IPC] memory:destroy failed:', error);
return { success: false };
}
});
ipcMain.handle('memory:status', () => {
return memoryService.getStatus();
});
ipcMain.handle('memory:ensureReady', async () => {
try {
return await memoryService.ensureMemoryReadyForSession();
} catch (error) {
log.error('[IPC] memory:ensureReady failed:', error);
return { ready: false, synced: false };
}
});
// === Configuration ===
ipcMain.handle('memory:getConfig', () => {
return memoryService.getConfig();
});
ipcMain.handle('memory:updateConfig', (_, config: Partial<MemoryConfig>) => {
try {
memoryService.updateConfig(config);
return { success: true };
} catch (error) {
log.error('[IPC] memory:updateConfig failed:', error);
return { success: false };
}
});
// === Extraction ===
ipcMain.handle('memory:extract', (_, sessionId: string, messageId: string, messages: Array<{ role: 'user' | 'assistant'; content: string }>, modelConfig: ModelConfig) => {
try {
const taskId = memoryService.extractFromConversation(sessionId, messageId, messages, modelConfig);
return { success: true, taskId };
} catch (error) {
log.error('[IPC] memory:extract failed:', error);
return { success: false, error: String(error) };
}
});
ipcMain.handle('memory:append', (_, content: string, title?: string) => {
try {
memoryService.appendMemory(content, title);
return { success: true };
} catch (error) {
log.error('[IPC] memory:append failed:', error);
return { success: false, error: String(error) };
}
});
ipcMain.handle('memory:handleMessage', (_, message: { role: 'user' | 'assistant'; content: string }, sessionId: string, modelConfig: ModelConfig) => {
try {
memoryService.handleMessage(sessionId, message, modelConfig);
return { success: true };
} catch (error) {
log.error('[IPC] memory:handleMessage failed:', error);
return { success: false, error: String(error) };
}
});
ipcMain.handle('memory:onSessionEnd', async (_, sessionId: string, modelConfig: ModelConfig) => {
try {
const taskId = await memoryService.onSessionEnd(sessionId, modelConfig);
return { success: true, taskId };
} catch (error) {
log.error('[IPC] memory:onSessionEnd failed:', error);
return { success: false, error: String(error) };
}
});
ipcMain.handle('memory:getExtractionProgress', (_, sessionId: string) => {
try {
const progress = memoryService.getExtractionProgress(sessionId);
return { success: true, progress };
} catch (error) {
log.error('[IPC] memory:getExtractionProgress failed:', error);
return { success: false, error: String(error) };
}
});
// === Retrieval ===
ipcMain.handle('memory:search', async (_, query: string, options?: HybridSearchOptions) => {
try {
const results = await memoryService.search(query, options);
return { success: true, results };
} catch (error) {
log.error('[IPC] memory:search failed:', error);
return { success: false, error: String(error) };
}
});
ipcMain.handle('memory:getContext', async (_, query: string, options?: InjectionOptions) => {
try {
const context = await memoryService.getInjectionContext(query, options);
return { success: true, context };
} catch (error) {
log.error('[IPC] memory:getContext failed:', error);
return { success: false, error: String(error) };
}
});
// === File Operations ===
ipcMain.handle('memory:sync', async () => {
try {
const result = await memoryService.syncWorkspace();
return { success: true, result };
} catch (error) {
log.error('[IPC] memory:sync failed:', error);
return { success: false, error: String(error) };
}
});
ipcMain.handle('memory:rebuildIndex', async () => {
try {
const result = await memoryService.rebuildIndex();
return { success: true, result };
} catch (error) {
log.error('[IPC] memory:rebuildIndex failed:', error);
return { success: false, error: String(error) };
}
});
ipcMain.handle('memory:getFiles', () => {
try {
const files = memoryService.getMemoryFiles();
return { success: true, files };
} catch (error) {
log.error('[IPC] memory:getFiles failed:', error);
return { success: false, error: String(error) };
}
});
// === Management ===
ipcMain.handle('memory:add', (_, entry: Partial<MemoryEntry>) => {
try {
const id = memoryService.addMemory(entry);
return { success: true, id };
} catch (error) {
log.error('[IPC] memory:add failed:', error);
return { success: false, error: String(error) };
}
});
ipcMain.handle('memory:update', (_, id: string, updates: Partial<MemoryEntry>) => {
try {
memoryService.updateMemory(id, updates);
return { success: true };
} catch (error) {
log.error('[IPC] memory:update failed:', error);
return { success: false, error: String(error) };
}
});
ipcMain.handle('memory:delete', (_, id: string) => {
try {
memoryService.deleteMemory(id);
return { success: true };
} catch (error) {
log.error('[IPC] memory:delete failed:', error);
return { success: false, error: String(error) };
}
});
ipcMain.handle('memory:list', (_, options?: { status?: string; source?: string; limit?: number }) => {
try {
const memories = memoryService.listMemories(options);
return { success: true, memories };
} catch (error) {
log.error('[IPC] memory:list failed:', error);
return { success: false, error: String(error) };
}
});
// === Scheduled Tasks ===
ipcMain.handle('memory:runConsolidation', async (_, modelConfig?: { provider: string; model: string; apiKey: string; baseUrl?: string }) => {
try {
const result = await memoryService.runConsolidation(modelConfig);
return { success: true, result };
} catch (error) {
log.error('[IPC] memory:runConsolidation failed:', error);
return { success: false, error: String(error) };
}
});
ipcMain.handle('memory:runCleanup', async () => {
try {
const result = await memoryService.runCleanup();
return { success: true, result };
} catch (error) {
log.error('[IPC] memory:runCleanup failed:', error);
return { success: false, error: String(error) };
}
});
// === Vector ===
ipcMain.handle('memory:checkVectorSupport', async () => {
try {
const result = await memoryService.checkVectorSupport();
return { success: true, ...result };
} catch (error) {
log.error('[IPC] memory:checkVectorSupport failed:', error);
return { success: false, error: String(error) };
}
});
ipcMain.handle('memory:setEmbeddingConfig', (_, config: { enabled: boolean; provider?: string; model?: string; dimensions?: number; apiKey?: string }) => {
try {
memoryService.setEmbeddingConfig(config);
return { success: true };
} catch (error) {
log.error('[IPC] memory:setEmbeddingConfig failed:', error);
return { success: false, error: String(error) };
}
});
// === Queue Status ===
ipcMain.handle('memory:getQueueStatus', () => {
return memoryService.getQueueStatus();
});
ipcMain.handle('memory:getSchedulerStatus', () => {
return memoryService.getSchedulerStatus();
});
log.info('[IPC] Memory handlers registered');
}

View File

@@ -0,0 +1,48 @@
import { ipcMain } from "electron";
import { getPerfLogger } from "../bootstrap/logConfig";
export function registerPerfHandlers(): void {
// 渲染进程 fire-and-forgetipcMain.on无返回值
ipcMain.on("perf:log", (_event, msg: unknown) => {
if (typeof msg !== "string") return;
getPerfLogger().info(msg);
});
}
/**
* 创建 fileServer stdout 解析器,追踪 create-workspace 请求耗时并写入 perf 日志。
*
* stdout 日志格式(来自 qiming-file-server
* POST-[requestId] -请求 /api/computer/create-workspace
* POST-[requestId] -响应(200) /api/computer/create-workspace
*
* 返回的回调传入 ManagedProcess.start({ onStdoutLine }) 即可。
* 注Map 条目在正常请求/响应配对后自动删除;若 fileServer 异常重启导致响应缺失,
* 少量孤立 key 会随 onStdoutLine 闭包一起被 GC不存在全局泄漏。
*/
export function createFileServerPerfHandler(): (chunk: string) => void {
const startMap = new Map<string, number>();
return (chunk: string): void => {
for (const line of chunk.split("\n")) {
const reqMatch = line.match(
/POST-(\[\S+\])\s+-请求\s+\/api\/computer\/create-workspace/,
);
if (reqMatch) {
startMap.set(reqMatch[1], Date.now());
continue;
}
const resMatch = line.match(
/POST-(\[\S+\])\s+-响应\(\d+\)\s+\/api\/computer\/create-workspace/,
);
if (resMatch) {
const start = startMap.get(resMatch[1]);
if (start !== undefined) {
startMap.delete(resMatch[1]);
getPerfLogger().info(
`[PERF] create-workspace: ${Date.now() - start}ms rid=${resMatch[1]}`,
);
}
}
}
};
}

View File

@@ -0,0 +1,183 @@
import { describe, it, expect } from "vitest";
import { z } from "zod";
/**
* Schema 验证测试
*
* 注意:这些 schema 定义与 processHandlers.ts 中保持同步。
* 不能直接从 processHandlers.ts 导入,因为该模块依赖 Electron API (app.getPath)
* 在单元测试环境中无法初始化。
*
* 如果 schema 定义更新,请同步修改此文件。
*/
// 与 processHandlers.ts 中的定义保持同步
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();
describe("lanproxyConfigSchema", () => {
it("should accept valid config", () => {
const result = lanproxyConfigSchema.safeParse({
serverIp: "192.168.1.1",
serverPort: 8080,
clientKey: "test-client-key",
});
expect(result.success).toBe(true);
});
it("should accept valid config with ssl", () => {
const result = lanproxyConfigSchema.safeParse({
serverIp: "example.com",
serverPort: 443,
clientKey: "key-123",
ssl: true,
});
expect(result.success).toBe(true);
});
it("should reject empty serverIp", () => {
const result = lanproxyConfigSchema.safeParse({
serverIp: "",
serverPort: 8080,
clientKey: "test-key",
});
expect(result.success).toBe(false);
if (!result.success) {
expect(result.error.issues[0].path).toContain("serverIp");
}
});
it("should reject zero serverPort", () => {
const result = lanproxyConfigSchema.safeParse({
serverIp: "192.168.1.1",
serverPort: 0,
clientKey: "test-key",
});
expect(result.success).toBe(false);
});
it("should reject negative serverPort", () => {
const result = lanproxyConfigSchema.safeParse({
serverIp: "192.168.1.1",
serverPort: -1,
clientKey: "test-key",
});
expect(result.success).toBe(false);
});
it("should reject non-integer serverPort", () => {
const result = lanproxyConfigSchema.safeParse({
serverIp: "192.168.1.1",
serverPort: 8080.5,
clientKey: "test-key",
});
expect(result.success).toBe(false);
});
it("should reject empty clientKey", () => {
const result = lanproxyConfigSchema.safeParse({
serverIp: "192.168.1.1",
serverPort: 8080,
clientKey: "",
});
expect(result.success).toBe(false);
});
it("should reject missing required fields", () => {
const result = lanproxyConfigSchema.safeParse({});
expect(result.success).toBe(false);
if (!result.success) {
const paths = result.error.issues.map((i) => i.path.join("."));
expect(paths).toContain("serverIp");
expect(paths).toContain("serverPort");
expect(paths).toContain("clientKey");
}
});
});
describe("agentRunnerConfigSchema", () => {
const validConfig = {
binPath: "/path/to/agent",
backendPort: 3000,
proxyPort: 3001,
apiKey: "test-api-key",
apiBaseUrl: "https://api.example.com",
defaultModel: "claude-3",
};
it("should accept valid config", () => {
const result = agentRunnerConfigSchema.safeParse(validConfig);
expect(result.success).toBe(true);
});
it("should reject empty binPath", () => {
const result = agentRunnerConfigSchema.safeParse({
...validConfig,
binPath: "",
});
expect(result.success).toBe(false);
});
it("should reject invalid apiBaseUrl", () => {
const result = agentRunnerConfigSchema.safeParse({
...validConfig,
apiBaseUrl: "",
});
expect(result.success).toBe(false);
});
it("should reject zero ports", () => {
const result = agentRunnerConfigSchema.safeParse({
...validConfig,
backendPort: 0,
});
expect(result.success).toBe(false);
});
it("should reject missing all required fields", () => {
const result = agentRunnerConfigSchema.safeParse({});
expect(result.success).toBe(false);
if (!result.success) {
expect(result.error.issues.length).toBeGreaterThanOrEqual(6);
}
});
});
describe("portSchema", () => {
it("should accept valid port", () => {
expect(portSchema.safeParse(8080).success).toBe(true);
expect(portSchema.safeParse(1).success).toBe(true);
expect(portSchema.safeParse(65535).success).toBe(true);
});
it("should reject zero", () => {
expect(portSchema.safeParse(0).success).toBe(false);
});
it("should reject negative", () => {
expect(portSchema.safeParse(-1).success).toBe(false);
});
it("should reject non-integer", () => {
expect(portSchema.safeParse(8080.5).success).toBe(false);
});
it("should reject string", () => {
expect(portSchema.safeParse("8080").success).toBe(false);
});
});

View File

@@ -0,0 +1,388 @@
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 };
});
}

View File

@@ -0,0 +1,559 @@
/**
* 沙箱 IPC 通道
*
* 提供沙箱工作区管理的 IPC 接口
*
* @version 1.0.0
* @updated 2026-03-22
*/
import { ipcMain } from "electron";
import log from "electron-log";
import type {
Workspace,
CreateWorkspaceOptions,
ExecuteOptions,
ExecuteResult,
FileInfo,
PermissionType,
PermissionResult,
PermissionRequest,
CleanupResult,
SandboxStatus,
SandboxPolicy,
SandboxCapabilities,
} from "@shared/types/sandbox";
import {
SandboxError,
SandboxErrorCode,
isSandboxError,
} from "@shared/errors/sandbox";
import { t } from "../services/i18n";
// 服务单例(在服务实现完成后注入)
let sandboxService: {
createWorkspace(
sessionId: string,
options?: CreateWorkspaceOptions,
): Promise<Workspace>;
destroyWorkspace(sessionId: string): Promise<void>;
listWorkspaces(): Workspace[];
getWorkspace(sessionId: string): Workspace | undefined;
execute(
sessionId: string,
command: string,
args: string[],
options?: ExecuteOptions,
): Promise<ExecuteResult>;
readFile(sessionId: string, path: string): Promise<string>;
writeFile(sessionId: string, path: string, content: string): Promise<void>;
isAvailable(): Promise<boolean>;
getStatus(): Promise<SandboxStatus>;
cleanup(): Promise<CleanupResult>;
} | null = null;
let permissionService: {
checkPermission(
sessionId: string,
type: PermissionType,
target: string,
): Promise<PermissionResult>;
requestPermission(
sessionId: string,
type: PermissionType,
target: string,
reason?: string,
): Promise<unknown>;
getPendingRequests(sessionId?: string): PermissionRequest[];
approve(
requestId: string,
approvedBy?: "user" | "system",
reason?: string,
): Promise<void>;
deny(requestId: string, reason?: string): Promise<void>;
} | null = null;
let sandboxControlService: {
getPolicy(): Promise<SandboxPolicy>;
setPolicy(patch: Partial<SandboxPolicy>): Promise<SandboxPolicy>;
getCapabilities(): Promise<SandboxCapabilities>;
setup(params?: {
windows?: { sandbox?: { mode?: "read-only" | "workspace-write" } };
}): Promise<{ success: boolean; message?: string }>;
} | null = null;
/**
* 设置沙箱服务实例
*/
export function setSandboxService(service: typeof sandboxService): void {
sandboxService = service;
log.info("[IPC] Sandbox service injected");
}
/**
* 设置权限服务实例
*/
export function setPermissionService(service: typeof permissionService): void {
permissionService = service;
log.info("[IPC] Permission service injected");
}
/**
* 设置沙箱控制服务(策略/能力/setup
*/
export function setSandboxControlService(
service: typeof sandboxControlService,
): void {
sandboxControlService = service;
log.info("[IPC] Sandbox control service injected");
}
/**
* 获取沙箱服务(带检查)
*/
function getSandboxService(): NonNullable<typeof sandboxService> {
if (!sandboxService) {
throw new SandboxError(
"Sandbox service not initialized",
SandboxErrorCode.SANDBOX_UNAVAILABLE,
);
}
return sandboxService;
}
/**
* 获取权限服务(带检查)
*/
function getPermissionService(): NonNullable<typeof permissionService> {
if (!permissionService) {
throw new SandboxError(
"Permission service not initialized",
SandboxErrorCode.INTERNAL_ERROR,
);
}
return permissionService;
}
function getSandboxControlService(): NonNullable<typeof sandboxControlService> {
if (!sandboxControlService) {
throw new SandboxError(
"Sandbox control service not initialized",
SandboxErrorCode.SANDBOX_UNAVAILABLE,
);
}
return sandboxControlService;
}
/**
* 错误处理包装器
*/
function handleError(
error: unknown,
operation: string,
): { success: false; error: string; code?: string } {
log.error(`[IPC] sandbox:${operation} failed:`, error);
if (isSandboxError(error)) {
return {
success: false,
error: error.getUserMessage(),
code: error.code,
};
}
return {
success: false,
error: error instanceof Error ? error.message : String(error),
};
}
/**
* 注册沙箱 IPC 通道
*/
export function registerSandboxHandlers(): void {
// ============================================================================
// 工作区管理
// ============================================================================
/**
* 创建工作区
* @channel sandbox:create
* @param sessionId 会话 ID
* @param options 创建选项
*/
ipcMain.handle(
"sandbox:create",
async (_, sessionId: string, options?: CreateWorkspaceOptions) => {
log.info("[IPC] sandbox:create:", { sessionId, options });
try {
const service = getSandboxService();
const workspace = await service.createWorkspace(sessionId, options);
return { success: true, data: workspace };
} catch (error) {
return handleError(error, "create");
}
},
);
/**
* 销毁工作区
* @channel sandbox:destroy
* @param sessionId 会话 ID
*/
ipcMain.handle("sandbox:destroy", async (_, sessionId: string) => {
log.info("[IPC] sandbox:destroy:", { sessionId });
try {
const service = getSandboxService();
await service.destroyWorkspace(sessionId);
return { success: true };
} catch (error) {
return handleError(error, "destroy");
}
});
/**
* 列出所有工作区
* @channel sandbox:list
*/
ipcMain.handle("sandbox:list", async () => {
log.info("[IPC] sandbox:list");
try {
const service = getSandboxService();
const workspaces = service.listWorkspaces();
return { success: true, data: workspaces };
} catch (error) {
return handleError(error, "list");
}
});
/**
* 获取工作区信息
* @channel sandbox:info
* @param sessionId 会话 ID
*/
ipcMain.handle("sandbox:info", async (_, sessionId: string) => {
log.info("[IPC] sandbox:info:", { sessionId });
try {
const service = getSandboxService();
const workspace = service.getWorkspace(sessionId);
if (!workspace) {
return {
success: false,
error: t("Claw.Sandbox.workspaceNotFound", sessionId),
code: SandboxErrorCode.WORKSPACE_NOT_FOUND,
};
}
return { success: true, data: workspace };
} catch (error) {
return handleError(error, "info");
}
});
// ============================================================================
// 命令执行
// ============================================================================
/**
* 执行命令
* @channel sandbox:execute
* @param sessionId 会话 ID
* @param command 命令
* @param args 参数
* @param options 执行选项
*/
ipcMain.handle(
"sandbox:execute",
async (
_,
sessionId: string,
command: string,
args: string[],
options?: ExecuteOptions,
) => {
log.info("[IPC] sandbox:execute:", { sessionId, command, args, options });
try {
const service = getSandboxService();
const result = await service.execute(sessionId, command, args, options);
return { success: true, data: result };
} catch (error) {
return handleError(error, "execute");
}
},
);
// ============================================================================
// 文件操作
// ============================================================================
/**
* 读取文件
* @channel sandbox:readFile
* @param sessionId 会话 ID
* @param path 文件路径
*/
ipcMain.handle(
"sandbox:readFile",
async (_, sessionId: string, path: string) => {
log.info("[IPC] sandbox:readFile:", { sessionId, path });
try {
const service = getSandboxService();
const content = await service.readFile(sessionId, path);
return { success: true, data: content };
} catch (error) {
return handleError(error, "readFile");
}
},
);
/**
* 写入文件
* @channel sandbox:writeFile
* @param sessionId 会话 ID
* @param path 文件路径
* @param content 文件内容
*/
ipcMain.handle(
"sandbox:writeFile",
async (_, sessionId: string, path: string, content: string) => {
log.info("[IPC] sandbox:writeFile:", { sessionId, path });
try {
const service = getSandboxService();
await service.writeFile(sessionId, path, content);
return { success: true };
} catch (error) {
return handleError(error, "writeFile");
}
},
);
// ============================================================================
// 权限管理
// ============================================================================
/**
* 检查权限
* @channel sandbox:checkPermission
* @param sessionId 会话 ID
* @param type 权限类型
* @param target 目标资源
*/
ipcMain.handle(
"sandbox:checkPermission",
async (_, sessionId: string, type: PermissionType, target: string) => {
log.info("[IPC] sandbox:checkPermission:", { sessionId, type, target });
try {
const service = getPermissionService();
const result = await service.checkPermission(sessionId, type, target);
return { success: true, data: result };
} catch (error) {
return handleError(error, "checkPermission");
}
},
);
/**
* 请求权限
* @channel sandbox:requestPermission
* @param sessionId 会话 ID
* @param type 权限类型
* @param target 目标资源
* @param reason 请求原因
*/
ipcMain.handle(
"sandbox:requestPermission",
async (
_,
sessionId: string,
type: PermissionType,
target: string,
reason?: string,
) => {
log.info("[IPC] sandbox:requestPermission:", {
sessionId,
type,
target,
reason,
});
try {
const service = getPermissionService();
const permission = await service.requestPermission(
sessionId,
type,
target,
reason,
);
return { success: true, data: permission };
} catch (error) {
return handleError(error, "requestPermission");
}
},
);
/**
* 获取待审批权限列表
* @channel sandbox:getPendingPermissions
* @param sessionId 可选的会话 ID 过滤
*/
ipcMain.handle(
"sandbox:getPendingPermissions",
async (_, sessionId?: string) => {
log.info("[IPC] sandbox:getPendingPermissions:", { sessionId });
try {
const service = getPermissionService();
const requests = service.getPendingRequests(sessionId);
return { success: true, data: requests };
} catch (error) {
return handleError(error, "getPendingPermissions");
}
},
);
/**
* 批准权限请求
* @channel sandbox:approvePermission
* @param requestId 请求 ID
* @param reason 原因
*/
ipcMain.handle(
"sandbox:approvePermission",
async (_, requestId: string, reason?: string) => {
log.info("[IPC] sandbox:approvePermission:", { requestId, reason });
try {
const service = getPermissionService();
await service.approve(requestId, "user", reason);
return { success: true };
} catch (error) {
return handleError(error, "approvePermission");
}
},
);
/**
* 拒绝权限请求
* @channel sandbox:denyPermission
* @param requestId 请求 ID
* @param reason 原因
*/
ipcMain.handle(
"sandbox:denyPermission",
async (_, requestId: string, reason?: string) => {
log.info("[IPC] sandbox:denyPermission:", { requestId, reason });
try {
const service = getPermissionService();
await service.deny(requestId, reason);
return { success: true };
} catch (error) {
return handleError(error, "denyPermission");
}
},
);
// ============================================================================
// 清理与状态
// ============================================================================
/**
* 清理资源
* @channel sandbox:cleanup
*/
ipcMain.handle("sandbox:cleanup", async () => {
log.info("[IPC] sandbox:cleanup");
try {
const service = getSandboxService();
const result = await service.cleanup();
return { success: true, data: result };
} catch (error) {
return handleError(error, "cleanup");
}
});
/**
* 获取沙箱状态
* @channel sandbox:status
*/
ipcMain.handle("sandbox:status", async () => {
log.info("[IPC] sandbox:status");
try {
const service = getSandboxService();
const status = await service.getStatus();
return { success: true, data: status };
} catch (error) {
return handleError(error, "status");
}
});
/**
* 获取沙箱策略
* @channel sandbox:policy:get
*/
ipcMain.handle("sandbox:policy:get", async () => {
log.info("[IPC] sandbox:policy:get");
try {
const control = getSandboxControlService();
const policy = await control.getPolicy();
return { success: true, data: policy };
} catch (error) {
return handleError(error, "policy:get");
}
});
/**
* 更新沙箱策略
* @channel sandbox:policy:set
*/
ipcMain.handle(
"sandbox:policy:set",
async (_, patch: Partial<SandboxPolicy>) => {
log.info("[IPC] sandbox:policy:set:", { patch });
try {
const control = getSandboxControlService();
const policy = await control.setPolicy(patch);
return { success: true, data: policy };
} catch (error) {
return handleError(error, "policy:set");
}
},
);
/**
* 获取后端能力
* @channel sandbox:capabilities
*/
ipcMain.handle("sandbox:capabilities", async () => {
log.info("[IPC] sandbox:capabilities");
try {
const control = getSandboxControlService();
const capabilities = await control.getCapabilities();
return { success: true, data: capabilities };
} catch (error) {
return handleError(error, "capabilities");
}
});
/**
* 执行后端 setup当前主要用于 Windows Sandbox
* @channel sandbox:setup
*/
ipcMain.handle(
"sandbox:setup",
async (
_,
params?: {
windows?: { sandbox?: { mode?: "read-only" | "workspace-write" } };
},
) => {
log.info("[IPC] sandbox:setup:", { params });
try {
const control = getSandboxControlService();
const result = await control.setup(params);
return { success: true, data: result };
} catch (error) {
return handleError(error, "setup");
}
},
);
log.info("[IPC] Sandbox handlers registered");
}

View File

@@ -0,0 +1,56 @@
import { ipcMain } from 'electron';
import { getDb, readSetting } from '../db';
import log from 'electron-log';
import { readQuickInitConfig } from '../bootstrap/quickInit';
export function registerSettingsHandlers(): void {
// Settings
ipcMain.handle('settings:get', (_, key: string) => {
const db = getDb();
if (!db) return null;
const stmt = db.prepare('SELECT value FROM settings WHERE key = ?');
const row = stmt.get(key) as { value: string } | undefined;
return row ? JSON.parse(row.value) : null;
});
ipcMain.handle('settings:set', (_, key: string, value: unknown) => {
const db = getDb();
if (!db) return false;
if (value === null || value === undefined) {
db.prepare('DELETE FROM settings WHERE key = ?').run(key);
} else {
const stmt = db.prepare(
'INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)'
);
stmt.run(key, JSON.stringify(value));
}
return true;
});
// Mirror / Registry — 动态切换 npm、uv 镜像源
ipcMain.handle('mirror:get', async () => {
const { getMirrorConfig, MIRROR_PRESETS } = await import('../services/system/dependencies');
return { success: true, ...getMirrorConfig(), presets: MIRROR_PRESETS };
});
ipcMain.handle('mirror:set', async (_, config: { npmRegistry?: string; uvIndexUrl?: string }) => {
const { setMirrorConfig, getMirrorConfig } = await import('../services/system/dependencies');
try {
setMirrorConfig(config);
// 持久化到 SQLite
const db = getDb();
if (db) {
const stmt = db.prepare('INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)');
stmt.run('mirror_config', JSON.stringify(getMirrorConfig()));
}
return { success: true };
} catch (error) {
return { success: false, error: String(error) };
}
});
// Quick Init — 读取 ~/.qimingclaw/qimingclaw.json 快捷配置
ipcMain.handle('quickInit:getConfig', () => {
return readQuickInitConfig();
});
}

View File

@@ -0,0 +1,21 @@
import { ipcMain } from 'electron';
import type { HandlerContext } from '@shared/types/ipc';
export function registerWindowHandlers(ctx: HandlerContext): void {
ipcMain.handle('window:minimize', () => {
ctx.getMainWindow()?.minimize();
});
ipcMain.handle('window:maximize', () => {
const win = ctx.getMainWindow();
if (win?.isMaximized()) {
win.unmaximize();
} else {
win?.maximize();
}
});
ipcMain.handle('window:close', () => {
ctx.getMainWindow()?.close();
});
}