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,353 @@
/**
* GUI Agent Server 管理器Electron 集成)
*
* 提供非 Windows 平台下 agent-gui-server (Node.js MCP server) 的启动、停止和状态管理。
* agent-gui-server 提供 GUI 桌面自动化能力(截图、鼠标、键盘等 MCP tools
*
* 端口通过 settings 中的 guiMcpPort 配置(默认 60008与 agent 会话注入的 MCP URL 对齐。
* API Key 通过 GUI_AGENT_API_KEY 环境变量传入(复用 anthropic_api_key
*/
import * as fs from "fs";
import * as path from "path";
import log from "electron-log";
import { t } from "../i18n";
import { ManagedProcess } from "../../processManager";
import {
getResourcesPath,
getNodeBinPathWithFallback,
getAppEnv,
} from "../system/dependencies";
import { isWindows } from "../system/shellEnv";
import { getDb, readSetting } from "../../db";
import { DEFAULT_GUI_MCP_PORT } from "@shared/constants";
import { killProcessTreesListeningOnTcpPort } from "../utils/processTree";
/** GUI Agent Server 进程名称 */
const PROCESS_NAME = "gui-agent-server";
/**
* 从 DB 读取 GUI MCP 端口配置
*/
export function getGuiMcpPort(): number {
try {
const config = readSetting("step1_config") as {
guiMcpPort?: number;
} | null;
return config?.guiMcpPort ?? DEFAULT_GUI_MCP_PORT;
} catch {
return DEFAULT_GUI_MCP_PORT;
}
}
/**
* 获取 agent-gui-server 的入口 JS 文件路径
*
* 打包后: process.resourcesPath/agent-gui-server/dist/index.js
* 开发时: {cwd}/resources/agent-gui-server/dist/index.js
*/
function getGuiAgentServerEntryPath(): string | null {
const resourcesPath = getResourcesPath();
const entryPath = path.join(
resourcesPath,
"agent-gui-server",
"dist",
"index.js",
);
if (fs.existsSync(entryPath)) {
return entryPath;
}
log.warn(`[GuiAgentServer] Entry file not found: ${entryPath}`);
return null;
}
/**
* 从 SQLite 获取 API Key优先 gui_agent_vision_model 中的 apiKey
*/
function getApiKeyFromDb(): string | null {
try {
const db = getDb();
if (!db) return null;
// 优先读取 gui_agent_vision_model 配置中的 apiKey
const visionConfigRow = db
.prepare("SELECT value FROM settings WHERE key = ?")
.get("gui_agent_vision_model") as { value: string } | undefined;
if (visionConfigRow) {
const config = JSON.parse(visionConfigRow.value);
if (config?.apiKey) return config.apiKey;
}
// 回退到全局 anthropic_api_key
const apiKeyRow = db
.prepare("SELECT value FROM settings WHERE key = ?")
.get("anthropic_api_key") as { value: string } | undefined;
return apiKeyRow?.value ?? null;
} catch (e) {
log.warn("[GuiAgentServer] Failed to read API Key:", e);
return null;
}
}
/**
* 检查视觉模型是否已配置
*/
function checkVisionModelConfigured(): boolean {
try {
const db = getDb();
if (!db) return false;
const visionConfigRow = db
.prepare("SELECT value FROM settings WHERE key = ?")
.get("gui_agent_vision_model") as { value: string } | undefined;
if (visionConfigRow) {
const config = JSON.parse(visionConfigRow.value);
if (config?.apiKey && config?.model) return true;
}
return false;
} catch (e) {
return false;
}
}
/**
* 获取视觉模型名称
*/
function getVisionModelName(): string | null {
try {
const db = getDb();
if (!db) return null;
const visionConfigRow = db
.prepare("SELECT value FROM settings WHERE key = ?")
.get("gui_agent_vision_model") as { value: string } | undefined;
if (visionConfigRow) {
const config = JSON.parse(visionConfigRow.value);
if (config?.model) return config.model;
}
return null;
} catch (e) {
return null;
}
}
// ========== 状态管理 ==========
let processInstance: ManagedProcess | null = null;
let running = false;
let runningPort: number | null = null;
let lastError: string | null = null;
/**
* 启动 GUI Agent Server
*
* 仅在非 Windows 平台有效。Windows 平台使用 windows-mcp见 windowsMcp.ts
*/
export async function startGuiAgentServer(): Promise<{
success: boolean;
port?: number;
error?: string;
}> {
// Windows 平台跳过(使用 windows-mcp
if (isWindows()) {
log.info("[GuiAgentServer] Skipped: Windows platform uses windows-mcp");
return { success: true };
}
// 已运行则直接返回
if (running && processInstance && runningPort !== null) {
return { success: true, port: runningPort };
}
// 读取配置的端口
const port = getGuiMcpPort();
// 启动前清理占用该端口的残留进程(跨平台实现)
try {
log.info(`[GuiAgentServer] Pre-start port sweep for ${port}...`);
await killProcessTreesListeningOnTcpPort(port);
await new Promise((r) => setTimeout(r, 450));
} catch (e) {
log.warn("[GuiAgentServer] Pre-start port sweep:", e);
}
// 解析入口文件
const entryPath = getGuiAgentServerEntryPath();
if (!entryPath) {
const error = t("Claw.GUIAgent.entryNotFound");
log.error("[GuiAgentServer] GUI Agent entry file not found");
return { success: false, error };
}
// 检查 Node.js 可用性
const nodeBinPath = getNodeBinPathWithFallback();
if (!nodeBinPath) {
const error = t("Claw.GUIAgent.nodeNotFound");
log.error("[GuiAgentServer] Node.js binary not found");
return { success: false, error };
}
// 获取 API Key
const apiKey = getApiKeyFromDb();
if (!apiKey) {
log.warn("[GuiAgentServer] API key missing (GUI Agent may be limited)");
}
// 检查视觉模型是否配置
const hasVisionModel = checkVisionModelConfigured();
const visionModelName = getVisionModelName();
// 构建环境变量
const env: Record<string, string> = {
...getAppEnv(),
GUI_AGENT_API_KEY: apiKey ?? "",
// 明确指定传输协议和端口
GUI_AGENT_TRANSPORT: "http",
GUI_AGENT_PORT: port.toString(),
// 传递视觉模型名称,无配置则为空(用于在 MCP 工具列表中禁用 gui_analyze_screen
GUI_AGENT_VISION_MODEL: hasVisionModel ? (visionModelName ?? "") : "",
// 禁用遥测
ANONYMIZED_TELEMETRY: "false",
};
// 构建启动参数
const args = [entryPath, "--port", port.toString(), "--transport", "http"];
// 创建并启动进程
processInstance = new ManagedProcess(PROCESS_NAME);
log.info(`[GuiAgentServer] Starting on port ${port}...`);
log.info(`[GuiAgentServer] Entry: ${entryPath}`);
try {
const result = await processInstance.start({
command: nodeBinPath,
args,
env,
startupDelayMs: 5000, // GUI Agent Server 需要较长的启动时间
onStdoutLine: (line) => {
// 转发到 electron-log
if (line.trim()) {
log.info(`[GuiAgentServer] ${line}`);
}
},
});
if (!result.success) {
lastError = result.error ?? "Failed to start";
running = false;
runningPort = null;
processInstance = null;
log.error(`[GuiAgentServer] Failed to start: ${lastError}`);
return { success: false, error: lastError };
}
running = true;
runningPort = port;
lastError = null;
log.info(`[GuiAgentServer] Started successfully on port ${port}`);
return { success: true, port };
} catch (e) {
const errorMsg = e instanceof Error ? e.message : String(e);
lastError = errorMsg;
running = false;
runningPort = null;
processInstance = null;
log.error(`[GuiAgentServer] Start exception: ${errorMsg}`);
return { success: false, error: errorMsg };
}
}
/**
* 停止 GUI Agent Server
*/
export async function stopGuiAgentServer(): Promise<{
success: boolean;
error?: string;
}> {
if (!running || !processInstance) {
running = false;
runningPort = null;
return { success: true };
}
log.info("[GuiAgentServer] Stopping...");
// 兜底ManagedProcess 进程可能已退出但端口仍被占用(如子进程残留)
try {
await killProcessTreesListeningOnTcpPort(getGuiMcpPort());
} catch (e) {
log.warn("[GuiAgentServer] TCP port sweep after stop:", e);
}
try {
const result = await processInstance.stopAsync();
running = false;
runningPort = null;
processInstance = null;
if (result.success) {
log.info("[GuiAgentServer] Stopped successfully");
return { success: true };
} else {
log.error(`[GuiAgentServer] Stop failed: ${result.message}`);
return { success: false, error: result.message };
}
} catch (e) {
const errorMsg = e instanceof Error ? e.message : String(e);
running = false;
runningPort = null;
processInstance = null;
log.error(`[GuiAgentServer] Stop exception: ${errorMsg}`);
return { success: false, error: errorMsg };
}
}
/**
* 获取 GUI Agent Server 运行状态
*/
export function getGuiAgentServerStatus(): {
running: boolean;
port?: number;
error?: string | null;
} {
return {
running,
port: running ? (runningPort ?? undefined) : undefined,
error: lastError,
};
}
/**
* 获取 GUI Agent Server MCP URL供 Agent 引擎注入使用)
*
* 返回 http://127.0.0.1:{runningPort}/mcp使用实际运行的端口。
* 仅在非 Windows 平台且内嵌 gui 服务已启动时返回非 null。
* Windows 桌面自动化由 windows-mcp 提供,请使用 getWindowsMcpUrl()(见 acpEngine
*/
export function getGuiAgentServerUrl(): string | null {
if (isWindows()) {
return null;
}
if (!running || runningPort === null) {
return null;
}
return `http://127.0.0.1:${runningPort}/mcp`;
}
/**
* 检查 GUI Agent Server 是否可用(入口文件存在且非 Windows
*/
export function isGuiAgentServerAvailable(): boolean {
if (isWindows()) {
return false;
}
const entryPath = getGuiAgentServerEntryPath();
return entryPath !== null;
}