llm-api-gateway-cli 1.0.5 → 1.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +116 -2
- package/README.md +101 -2
- package/cli-agent.js +186 -49
- package/cli-anthropic.js +57 -13
- package/cli-claude-code.js +59 -12
- package/cli-openai.js +48 -11
- package/lib/commands.js +397 -70
- package/lib/config.js +22 -2
- package/lib/configcmd.js +146 -45
- package/lib/hub.js +435 -43
- package/lib/i18n.js +80 -0
- package/lib/launcher.js +78 -24
- package/lib/mcp.js +16 -6
- package/lib/mcpadmin.js +331 -0
- package/lib/setup.js +101 -36
- package/package.json +1 -1
- package/public/manual.html +107 -3
- package/public/mcp.css +155 -0
- package/public/mcp.html +135 -0
- package/public/mcp.js +327 -0
- package/public/sessions.css +198 -0
- package/public/sessions.html +89 -0
- package/public/sessions.js +281 -0
- package/public/shell.html +8 -0
- package/public/shell.js +20 -4
- package/public/task.css +5 -0
- package/public/task.html +14 -1
package/cli-claude-code.js
CHANGED
|
@@ -24,7 +24,9 @@ import { fileURLToPath } from 'node:url';
|
|
|
24
24
|
import path from 'node:path';
|
|
25
25
|
|
|
26
26
|
import { loadDotEnv, DEFAULT_BASE_URL, sessionBaggage } from './lib/common.js';
|
|
27
|
-
import { resolveConfig,
|
|
27
|
+
import { resolveConfig, keyHint } from './lib/config.js';
|
|
28
|
+
// CLI 侧语言(只覆盖命令行输出;Web 界面与手册页仍是中文)。默认 zh,见 lib/i18n.js
|
|
29
|
+
import { pick, setLang, resolveLang, LANG_FLAG } from './lib/i18n.js';
|
|
28
30
|
|
|
29
31
|
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
30
32
|
loadDotEnv(SCRIPT_DIR); // 先加载 .env,让 GATEWAY_MODEL 等环境变量参与默认值计算
|
|
@@ -32,7 +34,7 @@ loadDotEnv(SCRIPT_DIR); // 先加载 .env,让 GATEWAY_MODEL 等环境变量参
|
|
|
32
34
|
const DEFAULT_MODEL = process.env.GATEWAY_MODEL || 'qwen3:8b';
|
|
33
35
|
const DEFAULT_HAIKU = 'qwen3:8b-nothink';
|
|
34
36
|
|
|
35
|
-
const
|
|
37
|
+
const HELP_ZH = `用法:node cli-claude-code.js [选项] [-- claude 参数...]
|
|
36
38
|
|
|
37
39
|
选项(本脚本自身):
|
|
38
40
|
-k, --key <sk-密钥> 网关发放的密钥(可用环境变量 SK/GATEWAY_KEY/ANTHROPIC_AUTH_TOKEN 或 .env)
|
|
@@ -45,12 +47,38 @@ const HELP = `用法:node cli-claude-code.js [选项] [-- claude 参数...]
|
|
|
45
47
|
--smoke 仅做一次 /v1/messages 连通性自检后退出,不拉起 claude
|
|
46
48
|
--print-env 仅打印要注入的环境变量,不拉起 claude
|
|
47
49
|
--config 打印 CC Switch / CC GUI 可直接粘贴的 JSON 配置
|
|
50
|
+
--lang <zh|en> 输出语言(默认 zh,也可用环境变量 LLM_GATEWAY_LANG)
|
|
48
51
|
-h, --help 显示帮助
|
|
49
52
|
|
|
50
53
|
透传:\`--\` 之后的参数原样交给 claude,例如:
|
|
51
54
|
node cli-claude-code.js -- -p "你好" --model deepseek-v4-pro
|
|
52
55
|
`;
|
|
53
56
|
|
|
57
|
+
const HELP_EN = `Usage: node cli-claude-code.js [options] [-- claude args...]
|
|
58
|
+
|
|
59
|
+
Options (this script's own):
|
|
60
|
+
-k, --key <sk-key> gateway API key (or env SK/GATEWAY_KEY/ANTHROPIC_AUTH_TOKEN, or .env)
|
|
61
|
+
--base-url <url> gateway base URL (default ${DEFAULT_BASE_URL})
|
|
62
|
+
-m, --model <model> ANTHROPIC_MODEL, the main model (default ${DEFAULT_MODEL})
|
|
63
|
+
--opus-model <model> ANTHROPIC_DEFAULT_OPUS_MODEL (default: same as --model)
|
|
64
|
+
--sonnet-model <model> ANTHROPIC_DEFAULT_SONNET_MODEL (default: same as --model)
|
|
65
|
+
--haiku-model <model> ANTHROPIC_DEFAULT_HAIKU_MODEL (default ${DEFAULT_HAIKU})
|
|
66
|
+
--small-fast-model <model> ANTHROPIC_SMALL_FAST_MODEL, background fast model (default: same as haiku)
|
|
67
|
+
--smoke run one /v1/messages connectivity check, then exit; do not launch claude
|
|
68
|
+
--print-env only print the environment variables to inject; do not launch claude
|
|
69
|
+
--config print JSON config ready to paste into CC Switch / CC GUI
|
|
70
|
+
--lang <zh|en> output language (default zh; env LLM_GATEWAY_LANG)
|
|
71
|
+
-h, --help show this help
|
|
72
|
+
|
|
73
|
+
Passthrough: everything after \`--\` is handed to claude unchanged, e.g.:
|
|
74
|
+
node cli-claude-code.js -- -p "hello" --model deepseek-v4-pro
|
|
75
|
+
`;
|
|
76
|
+
|
|
77
|
+
/** 大段帮助取词必须在**调用时**(模块顶层调 pick 会早于 setLang,永远拿到中文) */
|
|
78
|
+
function help() {
|
|
79
|
+
return pick(HELP_ZH, HELP_EN);
|
|
80
|
+
}
|
|
81
|
+
|
|
54
82
|
// ---------- 参数解析:本脚本自己的参数 vs 透传给 claude 的参数 ----------
|
|
55
83
|
// 注意这里不能直接用 lib/common.js 的 parseArgs:本脚本要把「不认识的参数」
|
|
56
84
|
// 原样透传给 claude,而通用解析器会把它们吃掉。
|
|
@@ -65,6 +93,7 @@ const WITH_VALUE = {
|
|
|
65
93
|
'--small-fast-model': 'smallFastModel',
|
|
66
94
|
'-k': 'key',
|
|
67
95
|
'-m': 'model',
|
|
96
|
+
...LANG_FLAG, // --lang <zh|en>:本脚本自己消费,不透传给 claude
|
|
68
97
|
};
|
|
69
98
|
const FLAG = {
|
|
70
99
|
'--smoke': 'smoke',
|
|
@@ -154,6 +183,7 @@ async function smoke(baseUrl, key, model) {
|
|
|
154
183
|
const url = `${baseUrl.replace(/\/+$/, '')}/v1/messages`;
|
|
155
184
|
// 用流式请求自检:Claude Code 实际走的就是 /v1/messages 的 SSE 流,最能反映真实可用性
|
|
156
185
|
// max_tokens 给足:本地 thinking 模型(如 qwen3:8b)会先把额度花在 reasoning 上,太小则看不到正文
|
|
186
|
+
// 正文这句是**发给网关的请求内容**(不是给人看的输出),一律保持中文,不随 --lang 变
|
|
157
187
|
const body = { model, max_tokens: 256, messages: [{ role: 'user', content: '你好,请用一句话回复' }], stream: true };
|
|
158
188
|
let res;
|
|
159
189
|
try {
|
|
@@ -167,12 +197,12 @@ async function smoke(baseUrl, key, model) {
|
|
|
167
197
|
body: JSON.stringify(body),
|
|
168
198
|
});
|
|
169
199
|
} catch (e) {
|
|
170
|
-
console.error(`[smoke] 连接异常: ${e?.message || e}`);
|
|
200
|
+
console.error(pick(`[smoke] 连接异常: ${e?.message || e}`, `[smoke] connection error: ${e?.message || e}`));
|
|
171
201
|
return false;
|
|
172
202
|
}
|
|
173
203
|
if (!res.ok) {
|
|
174
204
|
const text = await res.text();
|
|
175
|
-
console.error(`[smoke] 失败 HTTP ${res.status}: ${text.slice(0, 300)}`);
|
|
205
|
+
console.error(pick(`[smoke] 失败 HTTP ${res.status}: ${text.slice(0, 300)}`, `[smoke] failed HTTP ${res.status}: ${text.slice(0, 300)}`));
|
|
176
206
|
return false;
|
|
177
207
|
}
|
|
178
208
|
|
|
@@ -210,10 +240,13 @@ async function smoke(baseUrl, key, model) {
|
|
|
210
240
|
}
|
|
211
241
|
}
|
|
212
242
|
if (!started) {
|
|
213
|
-
console.error('[smoke] 失败:未收到 message_start 事件');
|
|
243
|
+
console.error(pick('[smoke] 失败:未收到 message_start 事件', '[smoke] failed: no message_start event received'));
|
|
214
244
|
return false;
|
|
215
245
|
}
|
|
216
|
-
console.error(
|
|
246
|
+
console.error(pick(
|
|
247
|
+
`[smoke] 成功 model=${model} 回复="${reply.slice(0, 40)}"`,
|
|
248
|
+
`[smoke] ok model=${model} reply="${reply.slice(0, 40)}"`,
|
|
249
|
+
));
|
|
217
250
|
return true;
|
|
218
251
|
}
|
|
219
252
|
|
|
@@ -225,13 +258,16 @@ function runClaude(passthrough, env) {
|
|
|
225
258
|
if (!r.error) return r.status ?? 0;
|
|
226
259
|
// Windows 下 npm 安装的 claude 是 .cmd shim,直接 spawn 会报 ENOENT/EINVAL → 回退 shell 解析
|
|
227
260
|
if (r.error.code !== 'ENOENT' && r.error.code !== 'EINVAL') {
|
|
228
|
-
console.error(`[错误] 启动 claude 失败: ${r.error.message}`);
|
|
261
|
+
console.error(pick(`[错误] 启动 claude 失败: ${r.error.message}`, `[error] failed to launch claude: ${r.error.message}`));
|
|
229
262
|
return 1;
|
|
230
263
|
}
|
|
231
264
|
const cmd = ['claude', ...passthrough].map((s) => (/[\s"&|<>]/.test(s) ? JSON.stringify(s) : s)).join(' ');
|
|
232
265
|
const r2 = spawnSync(cmd, { env, stdio: 'inherit', shell: true });
|
|
233
266
|
if (r2.error) {
|
|
234
|
-
console.error(
|
|
267
|
+
console.error(pick(
|
|
268
|
+
'[错误] 未找到 claude 命令:请先安装 Claude Code(npm install -g @anthropic-ai/claude-code)',
|
|
269
|
+
'[error] claude command not found: install Claude Code first (npm install -g @anthropic-ai/claude-code)',
|
|
270
|
+
));
|
|
235
271
|
return 1;
|
|
236
272
|
}
|
|
237
273
|
return r2.status ?? 0;
|
|
@@ -241,9 +277,12 @@ function runClaude(passthrough, env) {
|
|
|
241
277
|
|
|
242
278
|
async function main() {
|
|
243
279
|
const { opts, passthrough } = parseArgs(process.argv.slice(2));
|
|
280
|
+
// 语言要在**任何输出之前**定下来:--help / 报错 / 横幅都按它取词。
|
|
281
|
+
// 注意 --lang 已在 WITH_VALUE 里,不会被当成 claude 的参数透传出去
|
|
282
|
+
setLang(resolveLang(opts, process.env));
|
|
244
283
|
|
|
245
284
|
if (opts.help) {
|
|
246
|
-
console.log(
|
|
285
|
+
console.log(help());
|
|
247
286
|
return;
|
|
248
287
|
}
|
|
249
288
|
|
|
@@ -254,7 +293,7 @@ async function main() {
|
|
|
254
293
|
);
|
|
255
294
|
|
|
256
295
|
if (!key) {
|
|
257
|
-
console.error(
|
|
296
|
+
console.error(`${pick('[错误]', '[error]')} ${keyHint()}`);
|
|
258
297
|
process.exit(1);
|
|
259
298
|
}
|
|
260
299
|
|
|
@@ -306,10 +345,18 @@ async function main() {
|
|
|
306
345
|
return;
|
|
307
346
|
}
|
|
308
347
|
|
|
309
|
-
process.stderr.write(
|
|
348
|
+
process.stderr.write(pick(
|
|
349
|
+
`[gateway] 启动 Claude Code:base=${baseUrl} model=${env.ANTHROPIC_MODEL}\n`,
|
|
350
|
+
`[gateway] launching Claude Code: base=${baseUrl} model=${env.ANTHROPIC_MODEL}\n`,
|
|
351
|
+
));
|
|
310
352
|
// 把会话 id 显式打出来(与另外三个入口的提示一致),否则任务树里对不上是哪一次启动
|
|
311
353
|
const sid = /session\.id=([A-Za-z0-9._-]+)/.exec(String(env.ANTHROPIC_CUSTOM_HEADERS || ''));
|
|
312
|
-
if (sid)
|
|
354
|
+
if (sid) {
|
|
355
|
+
process.stderr.write(pick(
|
|
356
|
+
`[gateway] 会话 ${sid[1]}(W3C baggage: session.id)\n`,
|
|
357
|
+
`[gateway] session ${sid[1]} (W3C baggage: session.id)\n`,
|
|
358
|
+
));
|
|
359
|
+
}
|
|
313
360
|
const code = runClaude(passthrough, env);
|
|
314
361
|
process.exit(code);
|
|
315
362
|
}
|
package/cli-openai.js
CHANGED
|
@@ -28,14 +28,16 @@ import readline from 'node:readline';
|
|
|
28
28
|
import { randomUUID } from 'node:crypto';
|
|
29
29
|
|
|
30
30
|
import { loadDotEnv, parseArgs, sessionBaggage, DEFAULT_BASE_URL } from './lib/common.js';
|
|
31
|
-
import { resolveConfig, fail,
|
|
31
|
+
import { resolveConfig, fail, keyHint } from './lib/config.js';
|
|
32
|
+
// CLI 侧语言(只覆盖命令行输出;Web 界面与手册页仍是中文)。默认 zh,见 lib/i18n.js
|
|
33
|
+
import { pick, setLang, resolveLang, LANG_FLAG } from './lib/i18n.js';
|
|
32
34
|
|
|
33
35
|
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
34
36
|
loadDotEnv(SCRIPT_DIR); // 先加载 .env,让 GATEWAY_MODEL 等环境变量参与默认值计算
|
|
35
37
|
|
|
36
38
|
const DEFAULT_MODEL = process.env.GATEWAY_MODEL || 'qwen3:8b';
|
|
37
39
|
|
|
38
|
-
const
|
|
40
|
+
const HELP_ZH = `用法:node cli-openai.js [选项] [提示词]
|
|
39
41
|
|
|
40
42
|
选项:
|
|
41
43
|
-p, --prompt <文本> 单轮提问内容(省略则读位置参数 / 标准输入)
|
|
@@ -49,9 +51,33 @@ const HELP = `用法:node cli-openai.js [选项] [提示词]
|
|
|
49
51
|
--json 输出完整原始响应(非流式)
|
|
50
52
|
--list-models 列出上游可用模型
|
|
51
53
|
-i, --interactive 进入多轮交互(REPL)
|
|
54
|
+
--lang <zh|en> 输出语言(默认 zh,也可用环境变量 LLM_GATEWAY_LANG)
|
|
52
55
|
-h, --help 显示帮助
|
|
53
56
|
`;
|
|
54
57
|
|
|
58
|
+
const HELP_EN = `Usage: node cli-openai.js [options] [prompt]
|
|
59
|
+
|
|
60
|
+
Options:
|
|
61
|
+
-p, --prompt <text> one-shot prompt (falls back to the positional argument / stdin)
|
|
62
|
+
-s, --system <text> system prompt
|
|
63
|
+
-m, --model <model> model name (default ${DEFAULT_MODEL})
|
|
64
|
+
-k, --key <sk-key> gateway API key (or env SK/GATEWAY_KEY/OPENAI_API_KEY, or .env)
|
|
65
|
+
--base-url <url> gateway base URL (default ${DEFAULT_BASE_URL})
|
|
66
|
+
--temperature <v> sampling temperature
|
|
67
|
+
--max-tokens <v> max generated tokens
|
|
68
|
+
--no-stream disable streaming output (on by default)
|
|
69
|
+
--json print the full raw response (non-streaming)
|
|
70
|
+
--list-models list available upstream models
|
|
71
|
+
-i, --interactive multi-turn interactive mode (REPL)
|
|
72
|
+
--lang <zh|en> output language (default zh; env LLM_GATEWAY_LANG)
|
|
73
|
+
-h, --help show this help
|
|
74
|
+
`;
|
|
75
|
+
|
|
76
|
+
/** 大段帮助取词必须在**调用时**(模块顶层调 pick 会早于 setLang,永远拿到中文) */
|
|
77
|
+
function help() {
|
|
78
|
+
return pick(HELP_ZH, HELP_EN);
|
|
79
|
+
}
|
|
80
|
+
|
|
55
81
|
// 值型选项:会消费下一个参数作为值;布尔型选项:不消费后续参数
|
|
56
82
|
const VALUE_FLAGS = {
|
|
57
83
|
'--prompt': 'prompt', '-p': 'prompt',
|
|
@@ -61,6 +87,7 @@ const VALUE_FLAGS = {
|
|
|
61
87
|
'--base-url': 'base-url',
|
|
62
88
|
'--temperature': 'temperature',
|
|
63
89
|
'--max-tokens': 'max-tokens',
|
|
90
|
+
...LANG_FLAG, // --lang <zh|en>:与另外三个入口同一份定义(lib/i18n.js)
|
|
64
91
|
};
|
|
65
92
|
const BOOL_FLAGS = {
|
|
66
93
|
'--no-stream': 'no-stream',
|
|
@@ -86,23 +113,25 @@ function fmtUsage(u) {
|
|
|
86
113
|
const p = u.prompt_tokens ?? '?';
|
|
87
114
|
const c = u.completion_tokens ?? '?';
|
|
88
115
|
const t = u.total_tokens ?? (Number.isFinite(p) && Number.isFinite(c) ? p + c : '?');
|
|
89
|
-
return `输入 ${p} / 输出 ${c} / 合计 ${t} tokens
|
|
116
|
+
return pick(`输入 ${p} / 输出 ${c} / 合计 ${t} tokens`, `input ${p} / output ${c} / total ${t} tokens`);
|
|
90
117
|
}
|
|
91
118
|
|
|
92
119
|
// ---------- 主流程 ----------
|
|
93
120
|
|
|
94
121
|
async function main() {
|
|
95
122
|
const args = parseArgs(process.argv.slice(2), VALUE_FLAGS, BOOL_FLAGS);
|
|
123
|
+
// 语言要在**任何输出之前**定下来:--help / 报错 / 横幅都按它取词
|
|
124
|
+
setLang(resolveLang(args, process.env));
|
|
96
125
|
|
|
97
126
|
if (args.help) {
|
|
98
|
-
console.log(
|
|
127
|
+
console.log(help());
|
|
99
128
|
return;
|
|
100
129
|
}
|
|
101
130
|
|
|
102
131
|
const { key, baseUrl, model, temperature, maxTokens } = resolveConfig(args, { defaultModel: DEFAULT_MODEL });
|
|
103
132
|
|
|
104
133
|
if (!key) {
|
|
105
|
-
console.error(
|
|
134
|
+
console.error(`${pick('[错误]', '[error]')} ${keyHint()}`);
|
|
106
135
|
process.exit(1);
|
|
107
136
|
}
|
|
108
137
|
|
|
@@ -111,7 +140,12 @@ async function main() {
|
|
|
111
140
|
const sessionId = randomUUID();
|
|
112
141
|
const baggage = sessionBaggage(sessionId);
|
|
113
142
|
// 打到 stderr,便于在后台「任务树」里按这个 id 对号入座
|
|
114
|
-
if (baggage)
|
|
143
|
+
if (baggage) {
|
|
144
|
+
process.stderr.write(pick(
|
|
145
|
+
`[gateway] 会话 ${sessionId}(W3C baggage: session.id)\n`,
|
|
146
|
+
`[gateway] session ${sessionId} (W3C baggage: session.id)\n`,
|
|
147
|
+
));
|
|
148
|
+
}
|
|
115
149
|
|
|
116
150
|
const client = new OpenAI({
|
|
117
151
|
baseURL: `${baseUrl}/v1`,
|
|
@@ -123,7 +157,7 @@ async function main() {
|
|
|
123
157
|
try {
|
|
124
158
|
const res = await client.models.list();
|
|
125
159
|
const ids = (res.data || []).map((m) => m.id);
|
|
126
|
-
console.log(ids.length ? ids.join('\n') : '(上游未返回模型列表)');
|
|
160
|
+
console.log(ids.length ? ids.join('\n') : pick('(上游未返回模型列表)', '(upstream returned no model list)'));
|
|
127
161
|
} catch (e) {
|
|
128
162
|
fail(e);
|
|
129
163
|
}
|
|
@@ -141,7 +175,7 @@ async function main() {
|
|
|
141
175
|
if (!prompt && args._.length) prompt = args._.join(' ');
|
|
142
176
|
if (!prompt) prompt = await readStdin();
|
|
143
177
|
if (!prompt) {
|
|
144
|
-
console.log(
|
|
178
|
+
console.log(help());
|
|
145
179
|
return;
|
|
146
180
|
}
|
|
147
181
|
|
|
@@ -189,10 +223,13 @@ async function chat(client, model, messages, { stream, temperature, maxTokens, j
|
|
|
189
223
|
}
|
|
190
224
|
|
|
191
225
|
async function repl(client, model, opts) {
|
|
192
|
-
console.error(
|
|
226
|
+
console.error(pick(
|
|
227
|
+
`[gateway] 交互模式 model=${model},输入文本提问,\`exit\` 退出`,
|
|
228
|
+
`[gateway] interactive mode model=${model}; type a prompt, \`exit\` to quit`,
|
|
229
|
+
));
|
|
193
230
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
194
231
|
const messages = [];
|
|
195
|
-
const ask = () => new Promise((resolve) => rl.question('你> ', resolve));
|
|
232
|
+
const ask = () => new Promise((resolve) => rl.question(pick('你> ', 'you> '), resolve));
|
|
196
233
|
for (;;) {
|
|
197
234
|
let line;
|
|
198
235
|
try {
|
|
@@ -203,7 +240,7 @@ async function repl(client, model, opts) {
|
|
|
203
240
|
if (!line) continue;
|
|
204
241
|
if (/^(exit|quit)$/i.test(line)) break;
|
|
205
242
|
messages.push({ role: 'user', content: line });
|
|
206
|
-
process.stdout.write('助手> ');
|
|
243
|
+
process.stdout.write(pick('助手> ', 'assistant> '));
|
|
207
244
|
await chat(client, model, messages, { ...opts, json: false, exitOnError: false });
|
|
208
245
|
}
|
|
209
246
|
rl.close();
|