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,614 @@
import {
app,
BrowserWindow,
Menu,
dialog,
ipcMain,
Tray,
nativeImage,
session,
} from "electron";
import * as path from "path";
import log from "electron-log";
import { initDatabase, closeDb, readSetting } from "./db";
import { ManagedProcess } from "./processManager";
import { registerAllHandlers } from "./ipc/index";
import { unregisterEventForwarders } from "./ipc/eventForwarders";
import { runStartupTasks } from "./bootstrap/startup";
import { agentService } from "./services/engines/unifiedAgent";
import { stopComputerServer } from "./services/computerServer";
import { mcpProxyManager } from "./services/packages/mcp";
import { stopGuiAgentServer } from "./services/packages/guiAgentServer";
import { FEATURES } from "@shared/featureFlags";
import { stopWindowsMcp } from "./services/packages/windowsMcp";
import type { HandlerContext } from "@shared/types/ipc";
import { DEFAULT_DEV_SERVER_PORT } from "./services/constants";
import {
APP_DISPLAY_NAME,
CLEANUP_TIMEOUT,
DEFAULT_WINDOW_HEIGHT,
DEFAULT_WINDOW_MIN_HEIGHT,
DEFAULT_WINDOW_MIN_WIDTH,
DEFAULT_WINDOW_WIDTH,
} from "@shared/constants";
import { initLogging } from "./bootstrap/logConfig";
import { initI18n, setMainLang } from "./services/i18n";
import { createTrayManager, TrayStatus } from "./window/trayManager";
import { createServiceManager } from "./window/serviceManager";
import { initAutoUpdater } from "./services/autoUpdater";
import { migrateDataDir, migrateSettingsPaths } from "./bootstrap/migrate";
import { getDeviceId, logSystemInfo } from "./services/system/deviceId";
import { initWebviewPolicy } from "./services/system/webviewPolicy";
// macOS 26 Tahoe 兼容性:禁用 Fontations 字体后端
// 参考: https://github.com/electron/electron/issues/49522
if (process.platform === "darwin") {
app.commandLine.appendSwitch("disable-features", "FontationsFontBackend");
}
// Linux 沙箱处理
// 参考: https://github.com/electron/electron/issues/17972
// 参考: https://github.com/electron-userland/electron-builder/issues/8951
//
// 沙箱启用策略:
// 1. deb/rpm 包:通过 postinst 脚本设置 chrome-sandbox 的 SUID 权限
// 2. AppImage依赖 unprivileged user namespaces内核需要支持
// 3. 开发模式:禁用沙箱以方便调试
// 4. 用户可通过环境变量 ELECTRON_DISABLE_SANDBOX=1 强制禁用
//
// 注意: 此代码在 initLogging() 之前执行,所以使用 console 而不是 log
if (process.platform === "linux") {
const isRoot = typeof process.getuid === "function" && process.getuid() === 0;
const isAppImage = process.env.APPIMAGE !== undefined;
const disableSandbox = process.env.ELECTRON_DISABLE_SANDBOX === "1";
const isDev = !app.isPackaged;
// 警告: 以 root 身份运行存在安全风险
if (isRoot) {
console.warn("[Security] Running as root is not recommended.");
console.warn("[Security] This poses significant security risks.");
}
// AppImage 使用 namespace-based sandbox
if (isAppImage) {
console.info(
"[AppImage] Using namespace-based sandbox (requires kernel unprivileged user namespaces)",
);
}
if (disableSandbox) {
// 用户显式禁用沙箱
console.warn("[Security] Sandbox disabled by ELECTRON_DISABLE_SANDBOX=1");
app.commandLine.appendSwitch("no-sandbox");
app.commandLine.appendSwitch("disable-setuid-sandbox");
} else if (isDev) {
// 开发模式:禁用沙箱
console.info("[Dev] Sandbox disabled in development mode");
app.commandLine.appendSwitch("no-sandbox");
app.commandLine.appendSwitch("disable-setuid-sandbox");
} else {
// 生产模式:默认启用沙箱
console.info(
"[Production] Sandbox enabled (SUID for deb/rpm, namespace for AppImage)",
);
}
}
// 日志:轮转 + TTL 清理 + 开发/正式差异化(见 logConfig.ts
initLogging();
initI18n();
log.info("Application starting...");
log.info("[FeatureFlags][main]", FEATURES);
// Global references
let mainWindow: BrowserWindow | null = null;
let trayManager: ReturnType<typeof createTrayManager> | null = null;
let isQuitting = false; // 标志:是否正在真正退出应用
let isInstallingUpdate = false; // 标志:是否正在执行 quitAndInstall 安装更新
let pendingSecondInstanceFocus = false; // 标志:窗口未创建前收到 second-instance 事件
// 单实例保护Windows 托盘常驻场景下再次启动时,复用当前实例而不是创建新实例
const hasSingleInstanceLock = app.requestSingleInstanceLock();
if (!hasSingleInstanceLock) {
log.warn("[App] Another instance is already running, quitting current one");
app.quit();
}
app.on("second-instance", () => {
log.info("[App] second-instance event received");
if (!mainWindow) {
pendingSecondInstanceFocus = true;
if (app.isReady()) {
createWindow();
}
return;
}
if (mainWindow.isMinimized()) {
mainWindow.restore();
}
if (!mainWindow.isVisible()) {
mainWindow.show();
}
mainWindow.focus();
});
// Get icon path (works in both dev and production)
function getIconPath() {
if (app.isPackaged) {
// Production: icons in app.asar (Resources)
if (process.platform === "darwin") {
return path.join(process.resourcesPath, "icon.icns");
}
return path.join(process.resourcesPath, "icon.png");
}
// Development: icons in project root
if (process.platform === "darwin") {
return path.join(process.cwd(), "public", "icon.icns");
}
return path.join(process.cwd(), "public", "icon.png");
}
// Get icon path for Dock (must be PNG - nativeImage cannot load .icns)
function getDockIconPath() {
if (app.isPackaged) {
return path.join(process.resourcesPath, "icon-dock.png");
}
return path.join(process.cwd(), "public", "icon-dock.png");
}
const isDev = process.env.NODE_ENV === "development" || !app.isPackaged;
const WEBVIEW_PERF_BRIDGE_PRELOAD = path.join(
__dirname,
"..",
"preload",
"webviewPerfBridge.js",
);
function shouldInjectWebviewPerfBridge(url: string): boolean {
try {
const parsed = new URL(url);
return /^https?:$/.test(parsed.protocol);
} catch {
return false;
}
}
// Managed child processes
const lanproxy = new ManagedProcess("lanproxy");
const fileServer = new ManagedProcess("fileServer");
const agentRunner = new ManagedProcess("agentRunner");
const guiServer = new ManagedProcess("gui-agent-server");
let agentRunnerPorts: { backendPort: number; proxyPort: number } | null = null;
function createWindow() {
mainWindow = new BrowserWindow({
width: DEFAULT_WINDOW_WIDTH,
height: DEFAULT_WINDOW_HEIGHT,
minWidth: DEFAULT_WINDOW_MIN_WIDTH,
minHeight: DEFAULT_WINDOW_MIN_HEIGHT,
title: APP_DISPLAY_NAME,
icon: getIconPath(),
webPreferences: {
preload: path.join(__dirname, "..", "preload", "index.js"),
contextIsolation: true,
nodeIntegration: false,
sandbox: false, // Need to access node for MCP
webviewTag: true,
spellcheck: false, // 禁用拼写检查
},
show: false,
});
// 为 webview guest 注入轻量 BridgeQimingClawBridge
// 当前策略:对所有 http/https 页面注入;真正是否生效由 guest 侧路由+容器二次判断。
mainWindow.webContents.on(
"will-attach-webview",
(_event, webPreferences, params) => {
const targetUrl = String(params.src || "");
if (!shouldInjectWebviewPerfBridge(targetUrl)) {
return;
}
webPreferences.preload = WEBVIEW_PERF_BRIDGE_PRELOAD;
log.info("[WebviewBridge] Injected guest preload for:", targetUrl);
},
);
// Load the app
if (isDev) {
mainWindow.loadURL(`http://localhost:${DEFAULT_DEV_SERVER_PORT}`);
mainWindow.webContents.openDevTools();
} else {
// 生产环境dist 目录被打包到 app.asar 中
// 使用 file:// 协议直接加载 asar 内的文件
const indexUrl = `file://${process.resourcesPath}/app.asar/dist/index.html`;
log.info("Loading app from:", indexUrl);
mainWindow.loadURL(indexUrl);
}
// Handle load failures
mainWindow.webContents.on(
"did-fail-load",
(_event, errorCode, errorDescription, validatedURL) => {
log.error("Failed to load:", validatedURL, errorCode, errorDescription);
dialog.showErrorBox(
"Load Error",
`Failed to load application: ${errorDescription}\n\nURL: ${validatedURL}`,
);
},
);
mainWindow.once("ready-to-show", () => {
mainWindow?.show();
log.info("Main window shown");
// macOS 开发模式:窗口显示后再创建托盘,提高菜单栏图标出现概率
if (process.platform === "darwin" && !app.isPackaged && !trayManager) {
setTimeout(
() =>
initTrayManager().catch((e) =>
log.warn("[Tray] Delayed init failed:", e),
),
300,
);
}
});
mainWindow.on("closed", () => {
mainWindow = null;
});
// 所有平台:点击关闭按钮时隐藏到托盘,而不是退出应用
// 只有从托盘菜单点击"退出"时才真正退出
mainWindow.on("close", (e) => {
if (isQuitting) {
// 正在退出,允许关闭
return;
}
// 阻止关闭,改为隐藏
e.preventDefault();
mainWindow?.hide();
log.info("[App] Window hidden to tray (close intercepted)");
});
// Create application menu
createMenu();
}
function createMenu() {
if (process.platform === "darwin") {
// macOS: 保留最小菜单,确保 Cmd+C/V/Q 等快捷键正常
const template: Electron.MenuItemConstructorOptions[] = [
{ role: "appMenu" },
{ role: "editMenu" },
{ role: "windowMenu" },
];
Menu.setApplicationMenu(Menu.buildFromTemplate(template));
} else {
// Windows/Linux: 去掉菜单栏,功能由界面和系统托盘提供
Menu.setApplicationMenu(null);
}
}
async function initTrayManager() {
// 创建服务管理器
const serviceManager = createServiceManager({
lanproxy,
fileServer,
agentRunner,
});
trayManager = createTrayManager({
onShowWindow: () => {
if (mainWindow) {
mainWindow.show();
mainWindow.focus();
} else {
// 窗口不存在时重新创建
createWindow();
}
},
onRestartServices: async () => {
log.info("[Tray] Restarting all services...");
await serviceManager.restartAllServices();
trayManager?.updateServicesStatus(true);
},
onStopServices: async () => {
log.info("[Tray] Stopping all services...");
await serviceManager.stopAllServices();
trayManager?.updateServicesStatus(false);
log.info("[Tray] All services stopped");
},
});
await trayManager.create();
log.info("[Tray] TrayManager initialized");
}
// IPC handler for tray status updates from renderer
ipcMain.handle("tray:updateStatus", (_, status: TrayStatus) => {
if (trayManager) {
trayManager.setStatus(status);
}
});
ipcMain.handle("tray:updateServicesStatus", (_, running: boolean) => {
if (trayManager) {
trayManager.updateServicesStatus(running);
}
});
async function cleanupAllProcesses(): Promise<void> {
log.info("[Cleanup] Stopping all processes...");
const stepTimeoutMs = Math.max(1500, Math.floor(CLEANUP_TIMEOUT / 6));
const runCleanupStep = async (
label: string,
fn: () => Promise<void> | void,
): Promise<void> => {
let completed = false;
let timeoutHandle: ReturnType<typeof setTimeout> | null = null;
const task = (async () => {
try {
await fn();
} catch (e) {
log.error(`[Cleanup] ${label} error:`, e);
} finally {
completed = true;
if (timeoutHandle) {
clearTimeout(timeoutHandle);
timeoutHandle = null;
}
}
})();
await Promise.race([
task,
new Promise<void>((resolve) => {
timeoutHandle = setTimeout(() => {
if (!completed) {
log.warn(`[Cleanup] ${label} timed out after ${stepTimeoutMs}ms`);
}
timeoutHandle = null;
resolve();
}, stepTimeoutMs);
}),
]);
};
await runCleanupStep("Computer server stop", async () => {
await stopComputerServer();
});
await runCleanupStep("Event forwarders unregister", () => {
unregisterEventForwarders();
});
await runCleanupStep("Agent service destroy", async () => {
await agentService.destroy();
});
await runCleanupStep("MCP proxy cleanup", async () => {
await mcpProxyManager.cleanup();
});
// Windowswindows-mcpuv/python由独立 ManagedProcess 管理
await runCleanupStep("Windows MCP stop", async () => {
await stopWindowsMcp();
});
// 非 Windowsagent-gui-server 进程
if (FEATURES.ENABLE_GUI_AGENT_SERVER) {
await runCleanupStep("GUI Agent server stop", async () => {
await stopGuiAgentServer();
});
}
await runCleanupStep("Engine processes stop", () => {
const { stopAllEngines } = require("./services/engines/engineManager");
stopAllEngines();
log.info("[Cleanup] Engine processes stopped");
});
await runCleanupStep("Process registry killAll", async () => {
const { processRegistry } = require("./services/system/processRegistry");
await processRegistry.killAll();
log.info("[Cleanup] Process registry cleared");
});
// Last-resort force kill for legacy managed processes.
// NOTE: guiServer is a legacy placeholder and typically not started directly.
agentRunner.kill();
lanproxy.kill();
fileServer.kill();
guiServer.kill();
log.info("[Cleanup] All processes stopped");
}
// App lifecycle
app.whenReady().then(async () => {
if (!hasSingleInstanceLock) {
return;
}
log.info("App ready");
logSystemInfo();
// Dev mode: fix CORS duplicate header issue
// Server returns both specific origin and '*', causing browser to reject.
// Strip duplicate Access-Control-Allow-Origin values.
if (isDev) {
session.defaultSession.webRequest.onHeadersReceived((details, callback) => {
const headers = details.responseHeaders;
if (headers) {
const acoKey = Object.keys(headers).find(
(k) => k.toLowerCase() === "access-control-allow-origin",
);
if (acoKey && headers[acoKey] && headers[acoKey].length > 1) {
// Keep only the specific origin (not '*')
const specific = headers[acoKey].find((v) => v !== "*");
headers[acoKey] = [specific || "*"];
// 仅在确实修改了 ACO 时才回传 responseHeaders
// 避免无条件替换导致 Set-Cookie 被 Chromium 网络服务丢弃
callback({ responseHeaders: headers });
return;
}
}
// 未修改任何 header → 不传 responseHeadersChromium 原样传递
callback({});
});
log.info("Dev CORS fix enabled");
}
// Set Dock icon on macOS (development mode needs this)
if (process.platform === "darwin" && app.dock) {
const iconPath = getDockIconPath();
log.info("Setting Dock icon from:", iconPath);
try {
const iconImage = nativeImage.createFromPath(iconPath);
log.info(
"Icon image size:",
iconImage.getSize(),
"isEmpty:",
iconImage.isEmpty(),
);
if (!iconImage.isEmpty()) {
app.dock.setIcon(iconImage);
log.info("Dock icon set successfully");
} else {
log.warn("Icon image is empty");
}
} catch (e) {
log.warn("Failed to set Dock icon:", e);
}
}
migrateDataDir();
initDatabase();
migrateSettingsPaths();
getDeviceId();
// 数据库就绪后,同步语言到主进程 i18n
// 优先级:本地保存 > Electron 系统语言 > 英文兜底
const savedLang = readSetting("i18n.active_lang") as string | undefined;
if (savedLang) {
setMainLang(savedLang);
} else {
// 无本地偏好:用 Electron 系统语言app.ready 后可靠)
setMainLang(app.getLocale() || "en");
}
const ctx: HandlerContext = {
getMainWindow: () => mainWindow,
lanproxy,
fileServer,
agentRunner,
guiServer,
get agentRunnerPorts() {
return agentRunnerPorts;
},
setAgentRunnerPorts: (ports) => {
agentRunnerPorts = ports;
},
};
registerAllHandlers(ctx);
await runStartupTasks();
createWindow();
if (pendingSecondInstanceFocus && mainWindow) {
pendingSecondInstanceFocus = false;
mainWindow.show();
mainWindow.focus();
}
initWebviewPolicy(() => mainWindow);
// 非 macOS 或已打包立即创建托盘。macOS 开发模式改为在 ready-to-show 后创建
if (!(process.platform === "darwin" && !app.isPackaged)) {
if (process.platform === "darwin" && app.dock) app.dock.show();
await initTrayManager();
}
initAutoUpdater(
() => mainWindow,
cleanupAllProcesses,
() => {
// 在 quitAndInstall 前被调用:
// - isQuitting=true 防止窗口 close 事件被拦截到托盘
// - isInstallingUpdate=true 让 before-quit 跳过 e.preventDefault()
// 保留 Squirrel.Mac 的正常退出流程
isQuitting = true;
isInstallingUpdate = true;
log.info(
"[App] Update install flagged: isQuitting=true, isInstallingUpdate=true",
);
},
);
});
app.on("window-all-closed", () => {
// 窗口已隐藏到托盘,此事件不应触发
// 如果触发,说明窗口被意外关闭,不退出应用
log.info(
"[App] window-all-closed event fired (should not happen with tray mode)",
);
});
app.on("activate", () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
let isCleaningUp = false;
app.on("before-quit", (e) => {
if (isCleaningUp) return;
isCleaningUp = true;
isQuitting = true; // 通知窗口 close 事件允许关闭
if (isInstallingUpdate) {
// quitAndInstall 场景macOS Squirrel.Mac / Windows NSIS / Linux AppImage 均走此路径):
// - cleanup 已在 installUpdate() 中先行触发,无需重复执行
// - 不能调用 e.preventDefault():各平台安装器依赖 app.quit() 的正常退出流程;
// 若阻止退出再 app.exit(0),安装器可能已经失去对退出时机的感知,导致安装失败
// 只关闭数据库后直接 return让 Electron 正常完成退出,安装器接管
log.info(
"[App] Before quit - update install in progress, skipping preventDefault to allow installer",
);
closeDb();
return;
}
// 普通退出流程:阻止立即退出,异步清理完成后再调用 app.exit(0)
e.preventDefault();
log.info("[App] Before quit - starting cleanup");
void (async () => {
const start = Date.now();
try {
await cleanupAllProcesses();
} finally {
const elapsed = Date.now() - start;
if (elapsed > CLEANUP_TIMEOUT) {
log.warn(
`[App] Cleanup exceeded budget (${elapsed}ms > ${CLEANUP_TIMEOUT}ms), forcing exit`,
);
}
closeDb();
log.info("[App] Cleanup complete, exiting");
app.exit(0);
}
})();
});
app.on("will-quit", () => {
log.info("[App] Will quit");
});
// Handle uncaught exceptions
process.on("uncaughtException", (error) => {
log.error("Uncaught exception:", error);
});
process.on("unhandledRejection", (reason) => {
log.error("Unhandled rejection:", reason);
});