import path from "node:path"; const repoRoot = path.resolve(path.dirname(new URL(import.meta.url).pathname), "..", ".."); function scriptPath(relativePath) { return path.join(repoRoot, relativePath); } function field({ name, label, type = "text", defaultValue = "", required = false, placeholder = "", description = "", options = [], }) { return { name, label, type, defaultValue, required, placeholder, description, options, }; } export const TOOL_DEFINITIONS = [ { id: "pool.manage", tabTitle: "池管理", description: "查看并编辑 Codex 账号池与 API 池的 pool.json,支持新增、修改、删除并手动保存。", riskNotes: [ "这是本地文件写操作,会改 acc_pool 或 api_pool 下的 pool.json", "保存后不会自动热重载运行中的代理,通常需要手动刷新或重启对应代理", ], scriptPath: "", command: "", argsSchema: [], dangerLevel: "medium", confirmRequired: false, longRunning: false, virtual: true, }, { id: "api-pool.start", tabTitle: "API 池代理", description: "启动 Claude Code 或 Codex 的 apiUrl/apiKey 轮询代理。这个功能是长驻进程,会持续运行直到你手动停止。", riskNotes: [ "会占用新的本地端口并持续输出运行日志", "会把请求转发到 API 池中的上游地址,并在失败后自动切换节点", ], scriptPath: scriptPath("src/scripts/api-pool-proxy.mjs"), command: "node", argsSchema: [ field({ name: "provider", label: "Provider", type: "select", defaultValue: "codex", options: [ { label: "Codex", value: "codex" }, { label: "Claude Code", value: "claude-code" }, ], }), field({ name: "host", label: "监听地址", defaultValue: "127.0.0.1" }), field({ name: "port", label: "监听端口", type: "number", defaultValue: 8789 }), field({ name: "poolDir", label: "池目录", defaultValue: "api_pool/codex", description: "建议分别使用 api_pool/codex 和 api_pool/claude-code。", }), field({ name: "localApiKey", label: "本地 API Key", type: "password", defaultValue: "local-api-pool-proxy-key", }), field({ name: "maxSwitchAttempts", label: "最大切换次数", type: "number", defaultValue: 5, }), field({ name: "requestTimeoutMs", label: "请求超时毫秒", type: "number", defaultValue: 60000, }), field({ name: "enableScheduledSwitch", label: "启用定时切换", type: "checkbox", defaultValue: true, }), field({ name: "scheduledSwitchIntervalMs", label: "定时切换间隔毫秒", type: "number", defaultValue: 900000, }), field({ name: "proxyUrl", label: "上游 HTTP 代理", defaultValue: "http://127.0.0.1:8118", placeholder: "http://127.0.0.1:8118", }), ], dangerLevel: "low", confirmRequired: false, longRunning: true, }, { id: "proxy.start", tabTitle: "Codex 账号池代理", description: "启动 Codex 本地代理,负责账号池加载、探活、刷新和请求转发。这个功能是长驻进程,会持续运行直到你手动停止。", riskNotes: [ "会占用本地端口并持续输出运行日志", "不会直接改 ~/.codex 配置,但一旦客户端指向它,请求就会走账号池逻辑", ], scriptPath: scriptPath("src/scripts/codex-local-proxy.mjs"), command: "node", argsSchema: [ field({ name: "host", label: "监听地址", defaultValue: "127.0.0.1" }), field({ name: "port", label: "监听端口", type: "number", defaultValue: 8787 }), field({ name: "tokensDir", label: "账号目录", defaultValue: "acc_pool" }), field({ name: "upstreamBase", label: "上游地址", defaultValue: "https://chatgpt.com/backend-api/codex", }), field({ name: "refreshEndpoint", label: "刷新地址", defaultValue: "https://auth.openai.com/oauth/token", }), field({ name: "probeUrl", label: "探活地址", defaultValue: "https://chatgpt.com/backend-api/codex/models?client_version=0.117.0", }), field({ name: "localApiKey", label: "本地 API Key", type: "password", defaultValue: "local-acc-pool-proxy-key", }), field({ name: "maxSwitchAttempts", label: "最大切换次数", type: "number", defaultValue: 5, }), field({ name: "requestTimeoutMs", label: "请求超时毫秒", type: "number", defaultValue: 60000, }), field({ name: "proxyUrl", label: "上游 HTTP 代理", defaultValue: "http://127.0.0.1:8118", placeholder: "http://127.0.0.1:8118", }), ], dangerLevel: "low", confirmRequired: false, longRunning: true, }, { id: "llm.probe", tabTitle: "LLM 探测", description: "探测某个地址对 OpenAI 和 Anthropic 协议的兼容性,并直接输出终端结果。", riskNotes: [ "会主动请求目标接口并输出兼容性结果", "不会改 ~/.codex 配置", ], scriptPath: scriptPath("src/scripts/probe-llm-endpoint.mjs"), command: "node", argsSchema: [ field({ name: "baseUrl", label: "目标 Base URL", required: true, placeholder: "https://example.com", }), field({ name: "key", label: "API Key / Token", type: "password", required: true, }), field({ name: "skipAnthropic", label: "跳过 Anthropic 探测", type: "checkbox", defaultValue: false, }), field({ name: "skipOpenAI", label: "跳过 OpenAI 探测", type: "checkbox", defaultValue: false, }), field({ name: "skipPublic", label: "跳过公开信息探测", type: "checkbox", defaultValue: false, }), ], dangerLevel: "low", confirmRequired: false, longRunning: false, }, { id: "config.switch", tabTitle: "配置切换", description: "管理 Codex 和 Claude Code 的本机配置预设,支持新增、编辑、复制、删除和启用。", riskNotes: [ "启用时会直接写入 ~/.codex 或 ~/.claude 下的本机配置文件", "写入前会自动备份旧文件,但不会自动回滚当前正在使用的配置", ], scriptPath: "", command: "", argsSchema: [], dangerLevel: "medium", confirmRequired: false, longRunning: false, virtual: true, }, ]; const FIELD_TO_ARG = { "proxy.start": { host: "host", port: "port", tokensDir: "tokens-dir", upstreamBase: "upstream-base", refreshEndpoint: "refresh-endpoint", probeUrl: "probe-url", localApiKey: "local-api-key", maxSwitchAttempts: "max-switch-attempts", requestTimeoutMs: "request-timeout-ms", proxyUrl: "proxy-url", }, "api-pool.start": { provider: "provider", host: "host", port: "port", poolDir: "pool-dir", localApiKey: "local-api-key", maxSwitchAttempts: "max-switch-attempts", requestTimeoutMs: "request-timeout-ms", enableScheduledSwitch: "enable-scheduled-switch", scheduledSwitchIntervalMs: "scheduled-switch-interval-ms", proxyUrl: "proxy-url", }, "llm.probe": { baseUrl: "baseUrl", key: "key", skipAnthropic: "skipAnthropic", skipOpenAI: "skipOpenAI", skipPublic: "skipPublic", }, }; function normalizeValue(fieldDef, value) { if (fieldDef.type === "checkbox") { return value === true || value === "true"; } if (fieldDef.type === "number") { if (value === "" || value == null) return ""; return Number(value); } return value == null ? "" : String(value); } export function getToolDefinition(toolId) { return TOOL_DEFINITIONS.find((tool) => tool.id === toolId) || null; } export function getDefaultParams(tool) { return Object.fromEntries( tool.argsSchema.map((fieldDef) => [fieldDef.name, fieldDef.defaultValue]), ); } export function sanitizeParams(tool, rawParams = {}) { const defaults = getDefaultParams(tool); const params = {}; for (const fieldDef of tool.argsSchema) { const rawValue = rawParams[fieldDef.name] ?? defaults[fieldDef.name]; params[fieldDef.name] = normalizeValue(fieldDef, rawValue); } return params; } export function validateRequiredFields(tool, params) { return tool.argsSchema .filter((fieldDef) => fieldDef.required) .filter((fieldDef) => { const value = params[fieldDef.name]; return value === "" || value == null; }) .map((fieldDef) => fieldDef.label); } export function requiresConfirmation(tool, params) { return tool.confirmRequired; } export function buildCliArgs(tool, params) { if (tool.virtual) return []; const argMap = FIELD_TO_ARG[tool.id] || {}; const args = [tool.scriptPath]; for (const fieldDef of tool.argsSchema) { const cliKey = argMap[fieldDef.name]; const value = params[fieldDef.name]; if (!cliKey) continue; if (fieldDef.type === "checkbox") { if (value === true || value === false) { args.push(`--${cliKey}=${value ? "true" : "false"}`); } continue; } if (value === "" || value == null) continue; args.push(`--${cliKey}=${value}`); } return args; } export function buildCommandPreview(tool, params, options = {}) { if (tool.virtual) { return "内置页面"; } const argMap = FIELD_TO_ARG[tool.id] || {}; const hiddenFields = new Set(options.hiddenFields || []); const parts = [tool.command, tool.scriptPath]; for (const fieldDef of tool.argsSchema) { const cliKey = argMap[fieldDef.name]; const value = params[fieldDef.name]; if (!cliKey) continue; if (fieldDef.type === "checkbox") { if (value === true || value === false) { parts.push(`--${cliKey}=${value ? "true" : "false"}`); } continue; } if (value === "" || value == null) continue; const displayValue = hiddenFields.has(fieldDef.name) ? "***" : value; parts.push(`--${cliKey}=${displayValue}`); } return parts.join(" "); } export function serializeTool(tool) { const defaults = getDefaultParams(tool); return { id: tool.id, tabTitle: tool.tabTitle, description: tool.description, riskNotes: tool.riskNotes, argsSchema: tool.argsSchema, defaults, dangerLevel: tool.dangerLevel, confirmRequired: tool.confirmRequired, longRunning: tool.longRunning, virtual: Boolean(tool.virtual), defaultCommandPreview: buildCommandPreview(tool, defaults, { hiddenFields: ["apiKey", "key", "localApiKey"], }), }; } export function createToolPayload() { return TOOL_DEFINITIONS.map(serializeTool); }