Spaces:
Sleeping
Sleeping
| 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); | |
| } | |