AI_PROJECT / src /ui-server /tool-registry.mjs
chenchenaoyang's picture
Deploy from Codex
f395b70 verified
Raw
History Blame Contribute Delete
11.2 kB
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);
}