@vfvrpq/llm-cli 1.0.0 → 1.1.0
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.md +27 -0
- package/deepseek_cli.js +1 -1
- package/glm_cli.js +1 -1
- package/llm_cli.js +776 -0
- package/package.json +11 -3
package/README.md
CHANGED
|
@@ -24,6 +24,33 @@ node glm_cli.js ask "你好"
|
|
|
24
24
|
|
|
25
25
|
下文示例中的 `node glm_cli.js` / `node deepseek_cli.js` 在全局安装后可分别换成 `glm-cli` / `deepseek-cli`。
|
|
26
26
|
|
|
27
|
+
## 多供应商 llm-cli(v1.1 新增)
|
|
28
|
+
|
|
29
|
+
`llm_cli.js`(bin `llm-cli`)把多家供应商收进一个脚本,配置目录 `~/.llm-cli`:
|
|
30
|
+
|
|
31
|
+
| 供应商 | 默认端点 | 默认模型 | Key 环境变量 |
|
|
32
|
+
|---|---|---|---|
|
|
33
|
+
| `glm` 智谱 | `open.bigmodel.cn/api/paas/v4` | `glm-5.3-flash` | `GLM_API_KEY` |
|
|
34
|
+
| `deepseek` | `api.deepseek.com/v1` | `deepseek-flash` | `DEEPSEEK_API_KEY` |
|
|
35
|
+
| `mimo` 小米 | `api.xiaomimimo.com/v1` | `mimo-v2.6-flash` | `MIMO_API_KEY` |
|
|
36
|
+
| `kimi` 月之暗面 | `api.moonshot.cn/v1` | `kimi-latest` | `MOONSHOT_API_KEY` |
|
|
37
|
+
| `siliconflow` 硅基流动 | `api.siliconflow.cn/v1` | 需 `-m` 指定 | `SILICONFLOW_API_KEY` |
|
|
38
|
+
| `ark` 火山方舟(豆包) | `ark.cn-beijing.volces.com/api/v3` | 需 `-m` 指定 | `ARK_API_KEY` |
|
|
39
|
+
| `openai` | `api.openai.com/v1` | 需 `-m` 指定 | `OPENAI_API_KEY` |
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
node llm_cli.js config set api-key -p mimo # 按供应商保存 Key
|
|
43
|
+
node llm_cli.js config list # 查看全部配置状态
|
|
44
|
+
node llm_cli.js ask "你好" # 默认走 glm
|
|
45
|
+
node llm_cli.js ask -m deepseek-v4-pro "推理题" # 按模型名前缀自动选供应商
|
|
46
|
+
node llm_cli.js chat -p kimi # 指定供应商
|
|
47
|
+
node llm_cli.js models -p mimo # 查看该账号可用模型
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
- 供应商选择优先级:`-p` > 模型名前缀推断 > `config set provider` > 默认 `glm`
|
|
51
|
+
- 智谱 coding 订阅(本机 MiMo 同款配置):`--base-url https://open.bigmodel.cn/api/coding/paas/v4`
|
|
52
|
+
- 端点与模型清单均已实测;cc-switch(farion1231/cc-switch)的 OpenAI 兼容端点清单是本表的参考来源
|
|
53
|
+
|
|
27
54
|
## 准备 API Key
|
|
28
55
|
|
|
29
56
|
- GLM(智谱):<https://open.bigmodel.cn> 控制台 → API Key(海外版 <https://z.ai>)
|
package/deepseek_cli.js
CHANGED
|
@@ -19,7 +19,7 @@ const path = require("node:path");
|
|
|
19
19
|
const readline = require("node:readline");
|
|
20
20
|
const { Writable } = require("node:stream");
|
|
21
21
|
|
|
22
|
-
const VERSION = "1.
|
|
22
|
+
const VERSION = "1.1.0";
|
|
23
23
|
|
|
24
24
|
const DEFAULT_BASE_URL = "https://api.deepseek.com";
|
|
25
25
|
const DEFAULT_MODEL = "deepseek-chat";
|
package/glm_cli.js
CHANGED
|
@@ -19,7 +19,7 @@ const path = require("node:path");
|
|
|
19
19
|
const readline = require("node:readline");
|
|
20
20
|
const { Writable } = require("node:stream");
|
|
21
21
|
|
|
22
|
-
const VERSION = "1.
|
|
22
|
+
const VERSION = "1.1.0";
|
|
23
23
|
|
|
24
24
|
// 默认智谱国内站;使用海外 z.ai 时换成 https://api.z.ai/api/paas/v4
|
|
25
25
|
const DEFAULT_BASE_URL = "https://open.bigmodel.cn/api/paas/v4";
|
package/llm_cli.js
ADDED
|
@@ -0,0 +1,776 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* llm-cli —— 多供应商终端客户端:智谱 GLM / DeepSeek / 小米 MiMo / Kimi / 硅基流动 / 火山方舟 / OpenAI。
|
|
4
|
+
* 零依赖,需要 Node.js >= 18(内置 fetch)。
|
|
5
|
+
*
|
|
6
|
+
* 快速开始:
|
|
7
|
+
* node llm_cli.js config set api-key --provider mimo # 按供应商保存 Key(隐藏输入)
|
|
8
|
+
* node llm_cli.js ask "一句话介绍你自己"
|
|
9
|
+
* node llm_cli.js chat -p kimi
|
|
10
|
+
* node llm_cli.js models -p deepseek
|
|
11
|
+
*
|
|
12
|
+
* 供应商与 Key 获取地址见 --help;配置文件 ~/.llm-cli/config.json。
|
|
13
|
+
*/
|
|
14
|
+
"use strict";
|
|
15
|
+
|
|
16
|
+
const fs = require("node:fs");
|
|
17
|
+
const os = require("node:os");
|
|
18
|
+
const path = require("node:path");
|
|
19
|
+
const readline = require("node:readline");
|
|
20
|
+
const { Writable } = require("node:stream");
|
|
21
|
+
|
|
22
|
+
const VERSION = "1.1.0";
|
|
23
|
+
|
|
24
|
+
// defaultModel 为 null 表示该供应商没有内置默认模型,调用时必须用 -m 指定
|
|
25
|
+
const PROVIDERS = {
|
|
26
|
+
glm: {
|
|
27
|
+
label: "智谱 GLM",
|
|
28
|
+
defaultBase: "https://open.bigmodel.cn/api/paas/v4",
|
|
29
|
+
defaultModel: "glm-5.3-flash",
|
|
30
|
+
envKeys: ["GLM_API_KEY", "ZHIPUAI_API_KEY", "ZHIPU_API_KEY"],
|
|
31
|
+
keyUrl: "https://open.bigmodel.cn 控制台 -> API Key",
|
|
32
|
+
hint: "可用模型:glm-5.3-flash / glm-5.3 / glm-4.6 / glm-4.7 等(coding 订阅端点见 README)",
|
|
33
|
+
supportsThinking: true,
|
|
34
|
+
prefixes: ["glm"],
|
|
35
|
+
},
|
|
36
|
+
deepseek: {
|
|
37
|
+
label: "DeepSeek",
|
|
38
|
+
defaultBase: "https://api.deepseek.com/v1",
|
|
39
|
+
defaultModel: "deepseek-flash",
|
|
40
|
+
envKeys: ["DEEPSEEK_API_KEY"],
|
|
41
|
+
keyUrl: "https://platform.deepseek.com -> API Keys",
|
|
42
|
+
hint: "当前账号可用:deepseek-flash / deepseek-v4-pro",
|
|
43
|
+
supportsThinking: false,
|
|
44
|
+
prefixes: ["deepseek"],
|
|
45
|
+
},
|
|
46
|
+
mimo: {
|
|
47
|
+
label: "小米 MiMo",
|
|
48
|
+
defaultBase: "https://api.xiaomimimo.com/v1",
|
|
49
|
+
defaultModel: "mimo-v2.6-flash",
|
|
50
|
+
envKeys: ["MIMO_API_KEY", "XIAOMI_API_KEY"],
|
|
51
|
+
keyUrl: "https://platform.xiaomimimo.com",
|
|
52
|
+
hint: "可用模型:mimo-v2.6-flash / mimo-v2.6-pro / mimo-v2.5 等",
|
|
53
|
+
supportsThinking: false,
|
|
54
|
+
prefixes: ["mimo"],
|
|
55
|
+
},
|
|
56
|
+
kimi: {
|
|
57
|
+
label: "Kimi(月之暗面)",
|
|
58
|
+
defaultBase: "https://api.moonshot.cn/v1",
|
|
59
|
+
defaultModel: "kimi-latest",
|
|
60
|
+
envKeys: ["MOONSHOT_API_KEY", "KIMI_API_KEY"],
|
|
61
|
+
keyUrl: "https://platform.moonshot.cn -> API Key",
|
|
62
|
+
hint: "默认 kimi-latest(自动指向最新模型)",
|
|
63
|
+
supportsThinking: false,
|
|
64
|
+
prefixes: ["kimi", "moonshot"],
|
|
65
|
+
},
|
|
66
|
+
siliconflow: {
|
|
67
|
+
label: "硅基流动 SiliconFlow",
|
|
68
|
+
defaultBase: "https://api.siliconflow.cn/v1",
|
|
69
|
+
defaultModel: null,
|
|
70
|
+
envKeys: ["SILICONFLOW_API_KEY"],
|
|
71
|
+
keyUrl: "https://cloud.siliconflow.cn -> API 密钥",
|
|
72
|
+
hint: "模型名形如 deepseek-ai/DeepSeek-V3.1,需用 -m 指定",
|
|
73
|
+
supportsThinking: false,
|
|
74
|
+
prefixes: ["siliconflow"],
|
|
75
|
+
},
|
|
76
|
+
ark: {
|
|
77
|
+
label: "火山方舟 Ark(豆包)",
|
|
78
|
+
defaultBase: "https://ark.cn-beijing.volces.com/api/v3",
|
|
79
|
+
defaultModel: null,
|
|
80
|
+
envKeys: ["ARK_API_KEY"],
|
|
81
|
+
keyUrl: "https://console.volcengine.com/ark -> API Key",
|
|
82
|
+
hint: "模型为接入点 ID 或 doubao-* 名称,需用 -m 指定",
|
|
83
|
+
supportsThinking: false,
|
|
84
|
+
prefixes: ["ark", "doubao"],
|
|
85
|
+
},
|
|
86
|
+
openai: {
|
|
87
|
+
label: "OpenAI",
|
|
88
|
+
defaultBase: "https://api.openai.com/v1",
|
|
89
|
+
defaultModel: null,
|
|
90
|
+
envKeys: ["OPENAI_API_KEY"],
|
|
91
|
+
keyUrl: "https://platform.openai.com -> API keys",
|
|
92
|
+
hint: "需用 -m 指定模型(如 gpt-*)",
|
|
93
|
+
supportsThinking: false,
|
|
94
|
+
prefixes: ["gpt"],
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
// 环境变量 LLM_CLI_HOME 可把配置目录改到别处(便携 / 多账号场景)
|
|
99
|
+
const CONFIG_DIR = process.env.LLM_CLI_HOME || path.join(os.homedir(), ".llm-cli");
|
|
100
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
|
|
101
|
+
const HISTORY_FILE = path.join(CONFIG_DIR, "chat-latest.json");
|
|
102
|
+
|
|
103
|
+
const STREAM_IDLE_MS = 120000; // 流式模式下两次数据块之间的最大等待(毫秒)
|
|
104
|
+
const PLAIN_MS = 300000; // 非流式模式的整体等待(毫秒)
|
|
105
|
+
|
|
106
|
+
const HINTS = {
|
|
107
|
+
401: "API Key 缺失、无效或未生效。请执行: node llm_cli.js config set api-key --provider <名称>",
|
|
108
|
+
403: "当前 Key 无权访问该模型,或账户余额不足。",
|
|
109
|
+
404: "模型名或接口地址可能有误:用 -m/--model 指定模型,--base-url 指定接口地址。",
|
|
110
|
+
402: "账户余额不足,请前往对应平台充值。",
|
|
111
|
+
429: "请求过于频繁,或额度/资源包不足,请稍后再试。",
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const TIMEOUT_REASON = "llm-cli-idle-timeout";
|
|
115
|
+
|
|
116
|
+
class ApiError extends Error {}
|
|
117
|
+
|
|
118
|
+
const colors = { dim: "", bold: "", red: "", green: "", cyan: "", reset: "" };
|
|
119
|
+
|
|
120
|
+
function enableColors(noColor) {
|
|
121
|
+
if (noColor || process.env.NO_COLOR || !process.stdout.isTTY) return;
|
|
122
|
+
colors.dim = "\x1b[2m";
|
|
123
|
+
colors.bold = "\x1b[1m";
|
|
124
|
+
colors.red = "\x1b[31m";
|
|
125
|
+
colors.green = "\x1b[32m";
|
|
126
|
+
colors.cyan = "\x1b[36m";
|
|
127
|
+
colors.reset = "\x1b[0m";
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const state = { currentAbort: null };
|
|
131
|
+
|
|
132
|
+
// ---------------------------------------------------------------- 配置读写
|
|
133
|
+
|
|
134
|
+
function loadConfig() {
|
|
135
|
+
try {
|
|
136
|
+
return JSON.parse(fs.readFileSync(CONFIG_FILE, "utf8"));
|
|
137
|
+
} catch (e) {
|
|
138
|
+
if (e.code === "ENOENT") return {};
|
|
139
|
+
console.error(`警告:配置文件 ${CONFIG_FILE} 读取失败(${e.message}),将忽略已有配置。`);
|
|
140
|
+
return {};
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function saveConfig(cfg) {
|
|
145
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
146
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(cfg, null, 2) + "\n", "utf8");
|
|
147
|
+
if (process.platform !== "win32") {
|
|
148
|
+
try { fs.chmodSync(CONFIG_FILE, 0o600); } catch {}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function normalizeProvider(p) {
|
|
153
|
+
const key = String(p || "").trim().toLowerCase();
|
|
154
|
+
const alias = { zhipu: "glm", moonshot: "kimi", xiaomi: "mimo", volc: "ark" };
|
|
155
|
+
const name = alias[key] || key;
|
|
156
|
+
if (!PROVIDERS[name]) {
|
|
157
|
+
throw new ApiError(`未知供应商 "${p}",可选:${Object.keys(PROVIDERS).join(" / ")}`);
|
|
158
|
+
}
|
|
159
|
+
return name;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// 没显式指定 --provider 时,尝试从模型名前缀推断(如 glm-* / deepseek-* / mimo-*)
|
|
163
|
+
function inferProvider(model) {
|
|
164
|
+
const m = String(model || "").toLowerCase();
|
|
165
|
+
for (const [id, pc] of Object.entries(PROVIDERS)) {
|
|
166
|
+
if (pc.prefixes.some((p) => m.startsWith(p))) return id;
|
|
167
|
+
}
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function providerOf(args, cfg) {
|
|
172
|
+
return normalizeProvider(args.provider || inferProvider(args.model) || cfg.default_provider || "glm");
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function resolveRuntime(args, cfg) {
|
|
176
|
+
const provider = providerOf(args, cfg);
|
|
177
|
+
const pc = PROVIDERS[provider];
|
|
178
|
+
const conf = (cfg.providers && cfg.providers[provider]) || {};
|
|
179
|
+
let apiKey = args.api_key;
|
|
180
|
+
if (!apiKey) {
|
|
181
|
+
for (const k of pc.envKeys) {
|
|
182
|
+
const v = process.env[k];
|
|
183
|
+
if (v) { apiKey = v; break; }
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
if (!apiKey) apiKey = conf.api_key;
|
|
187
|
+
const model = args.model || conf.model || pc.defaultModel;
|
|
188
|
+
if (!model) {
|
|
189
|
+
throw new ApiError(`[${provider}] 没有内置默认模型,必须用 -m 指定。${pc.hint ? "\n" + pc.hint : ""}`);
|
|
190
|
+
}
|
|
191
|
+
return {
|
|
192
|
+
provider,
|
|
193
|
+
label: pc.label,
|
|
194
|
+
apiKey,
|
|
195
|
+
model,
|
|
196
|
+
baseUrl: args.base_url || conf.base_url || pc.defaultBase,
|
|
197
|
+
pc,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function requireKey(apiKey, rt) {
|
|
202
|
+
if (apiKey) return apiKey;
|
|
203
|
+
const pc = rt.pc;
|
|
204
|
+
throw new ApiError(
|
|
205
|
+
`尚未配置 [${rt.provider}] 的 API Key,请任选其一:\n` +
|
|
206
|
+
` 1. node llm_cli.js config set api-key --provider ${rt.provider} (推荐)\n` +
|
|
207
|
+
` 2. 设置环境变量 ${pc.envKeys[0]}\n` +
|
|
208
|
+
` 3. 临时使用:--api-key <你的Key>\n` +
|
|
209
|
+
`Key 获取:${pc.keyUrl}`
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function mask(key) {
|
|
214
|
+
if (!key) return "(未设置)";
|
|
215
|
+
if (key.length <= 8) return key.slice(0, 2) + "****";
|
|
216
|
+
return key.slice(0, 6) + "..." + key.slice(-4);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// ---------------------------------------------------------------- HTTP / SSE
|
|
220
|
+
|
|
221
|
+
function isAbort(err) {
|
|
222
|
+
return err && (err.name === "AbortError" || err.code === "ABORT_ERR");
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async function friendlyHTTPError(resp) {
|
|
226
|
+
let detail = "";
|
|
227
|
+
try {
|
|
228
|
+
const text = await resp.text();
|
|
229
|
+
try {
|
|
230
|
+
const obj = JSON.parse(text);
|
|
231
|
+
detail = (obj.error && (obj.error.message || obj.error.msg)) || text.trim();
|
|
232
|
+
} catch {
|
|
233
|
+
detail = text.trim();
|
|
234
|
+
}
|
|
235
|
+
} catch {}
|
|
236
|
+
let msg = `请求失败 [HTTP ${resp.status}]`;
|
|
237
|
+
if (detail) msg += `:${detail}`;
|
|
238
|
+
if (HINTS[resp.status]) msg += `\n提示:${HINTS[resp.status]}`;
|
|
239
|
+
return msg;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
async function apiFetch(url, apiKey, payload, method, signal) {
|
|
243
|
+
const headers = { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" };
|
|
244
|
+
let body;
|
|
245
|
+
if (payload !== null && payload !== undefined) {
|
|
246
|
+
body = JSON.stringify(payload);
|
|
247
|
+
if (payload.stream) headers.Accept = "text/event-stream";
|
|
248
|
+
}
|
|
249
|
+
let resp;
|
|
250
|
+
try {
|
|
251
|
+
resp = await fetch(url, { method, headers, body, signal });
|
|
252
|
+
} catch (err) {
|
|
253
|
+
if (isAbort(err)) throw err;
|
|
254
|
+
const cause = err.cause ? (err.cause.message || String(err.cause)) : err.message;
|
|
255
|
+
throw new ApiError(`无法连接服务器:${cause}(请检查网络或 --base-url)`);
|
|
256
|
+
}
|
|
257
|
+
if (!resp.ok) throw new ApiError(await friendlyHTTPError(resp));
|
|
258
|
+
return resp;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
async function* iterSSE(body) {
|
|
262
|
+
const decoder = new TextDecoder();
|
|
263
|
+
let buf = "";
|
|
264
|
+
for await (const chunk of body) {
|
|
265
|
+
buf += decoder.decode(chunk, { stream: true });
|
|
266
|
+
let idx;
|
|
267
|
+
while ((idx = buf.indexOf("\n")) >= 0) {
|
|
268
|
+
const line = buf.slice(0, idx).trim();
|
|
269
|
+
buf = buf.slice(idx + 1);
|
|
270
|
+
if (!line.startsWith("data:")) continue;
|
|
271
|
+
const data = line.slice(5).trim();
|
|
272
|
+
if (data === "[DONE]") return;
|
|
273
|
+
if (data) yield data;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function buildPayload(rt, messages, args) {
|
|
279
|
+
const payload = { model: rt.model, messages, stream: !args.no_stream };
|
|
280
|
+
if (args.temperature != null) payload.temperature = Number(args.temperature);
|
|
281
|
+
if (args.max_tokens) payload.max_tokens = Number(args.max_tokens);
|
|
282
|
+
if (args.thinking != null) {
|
|
283
|
+
if (rt.pc.supportsThinking) {
|
|
284
|
+
payload.thinking = { type: args.thinking === "on" ? "enabled" : "disabled" };
|
|
285
|
+
} else {
|
|
286
|
+
console.error(`${colors.dim}(提示: --thinking 仅对智谱 GLM 生效,已忽略)${colors.reset}`);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
if (args.debug) {
|
|
290
|
+
console.error(`[debug] ${rt.baseUrl.replace(/\/+$/, "")}/chat/completions`);
|
|
291
|
+
console.error(`[debug] ${JSON.stringify(payload)}`);
|
|
292
|
+
}
|
|
293
|
+
return payload;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// ---------------------------------------------------------------- 对话调用
|
|
297
|
+
|
|
298
|
+
async function chatCompletion(rt, payload) {
|
|
299
|
+
const url = rt.baseUrl.replace(/\/+$/, "") + "/chat/completions";
|
|
300
|
+
const stream = !!payload.stream;
|
|
301
|
+
const controller = new AbortController();
|
|
302
|
+
let timer = null;
|
|
303
|
+
const arm = () => {
|
|
304
|
+
clearTimeout(timer);
|
|
305
|
+
timer = setTimeout(() => controller.abort(TIMEOUT_REASON), stream ? STREAM_IDLE_MS : PLAIN_MS);
|
|
306
|
+
};
|
|
307
|
+
arm();
|
|
308
|
+
state.currentAbort = controller;
|
|
309
|
+
const contentParts = [];
|
|
310
|
+
const reasoningParts = [];
|
|
311
|
+
let usage = null;
|
|
312
|
+
try {
|
|
313
|
+
const resp = await apiFetch(url, rt.apiKey, payload, "POST", controller.signal);
|
|
314
|
+
if (stream) {
|
|
315
|
+
let inThinking = false;
|
|
316
|
+
for await (const data of iterSSE(resp.body)) {
|
|
317
|
+
arm();
|
|
318
|
+
let obj;
|
|
319
|
+
try { obj = JSON.parse(data); } catch { continue; }
|
|
320
|
+
if (obj.error) throw new ApiError("服务端返回错误:" + JSON.stringify(obj.error));
|
|
321
|
+
if (obj.usage) usage = obj.usage;
|
|
322
|
+
const choice = (obj.choices || [])[0];
|
|
323
|
+
if (!choice) continue;
|
|
324
|
+
const delta = choice.delta || {};
|
|
325
|
+
const rc = delta.reasoning_content;
|
|
326
|
+
if (rc) {
|
|
327
|
+
if (!inThinking) {
|
|
328
|
+
inThinking = true;
|
|
329
|
+
console.log(`${colors.dim}—— 思考 ——${colors.reset}`);
|
|
330
|
+
}
|
|
331
|
+
process.stdout.write(colors.dim + rc + colors.reset);
|
|
332
|
+
reasoningParts.push(rc);
|
|
333
|
+
}
|
|
334
|
+
const text = delta.content;
|
|
335
|
+
if (text) {
|
|
336
|
+
if (inThinking) {
|
|
337
|
+
inThinking = false;
|
|
338
|
+
console.log(`\n${colors.bold}—— 回答 ——${colors.reset}`);
|
|
339
|
+
}
|
|
340
|
+
process.stdout.write(text);
|
|
341
|
+
contentParts.push(text);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
console.log();
|
|
345
|
+
} else {
|
|
346
|
+
const obj = await resp.json();
|
|
347
|
+
if (obj.error) throw new ApiError("服务端返回错误:" + JSON.stringify(obj.error));
|
|
348
|
+
const message = ((obj.choices || [])[0] || {}).message || {};
|
|
349
|
+
if (message.reasoning_content) {
|
|
350
|
+
console.log(`${colors.dim}—— 思考 ——\n${message.reasoning_content}${colors.reset}`);
|
|
351
|
+
reasoningParts.push(message.reasoning_content);
|
|
352
|
+
}
|
|
353
|
+
const content = message.content || "";
|
|
354
|
+
console.log(content);
|
|
355
|
+
contentParts.push(content);
|
|
356
|
+
usage = obj.usage || null;
|
|
357
|
+
}
|
|
358
|
+
} catch (err) {
|
|
359
|
+
if (isAbort(err)) {
|
|
360
|
+
if (controller.signal.reason === TIMEOUT_REASON) {
|
|
361
|
+
console.log(`\n${colors.dim}(等待数据超时,已中断)${colors.reset}`);
|
|
362
|
+
} else {
|
|
363
|
+
console.log(`\n${colors.dim}(已中断本次回复)${colors.reset}`);
|
|
364
|
+
}
|
|
365
|
+
} else {
|
|
366
|
+
throw err;
|
|
367
|
+
}
|
|
368
|
+
} finally {
|
|
369
|
+
clearTimeout(timer);
|
|
370
|
+
state.currentAbort = null;
|
|
371
|
+
}
|
|
372
|
+
return { content: contentParts.join(""), reasoning: reasoningParts.join(""), usage };
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function printUsageLine(usage) {
|
|
376
|
+
if (usage && usage.prompt_tokens != null && usage.completion_tokens != null) {
|
|
377
|
+
console.log(`${colors.dim}[tokens] 输入 ${usage.prompt_tokens} · 输出 ${usage.completion_tokens}${colors.reset}`);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// ---------------------------------------------------------------- 子命令
|
|
382
|
+
|
|
383
|
+
function readQuestionFromFiles(files) {
|
|
384
|
+
return files.map((fp) => {
|
|
385
|
+
let text;
|
|
386
|
+
try {
|
|
387
|
+
text = fs.readFileSync(fp, "utf8");
|
|
388
|
+
} catch (e) {
|
|
389
|
+
throw new ApiError(`无法读取文件 ${fp}:${e.message}`);
|
|
390
|
+
}
|
|
391
|
+
return `文件 \`${path.basename(fp)}\` 内容:\n\`\`\`\n${text}\n\`\`\``;
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
async function cmdAsk(args, cfg) {
|
|
396
|
+
const rt = resolveRuntime(args, cfg);
|
|
397
|
+
requireKey(rt.apiKey, rt);
|
|
398
|
+
|
|
399
|
+
let question = args._.join(" ").trim();
|
|
400
|
+
if (!question && !process.stdin.isTTY) {
|
|
401
|
+
try { question = fs.readFileSync(0, "utf8").trim(); } catch {}
|
|
402
|
+
}
|
|
403
|
+
if (args.file && args.file.length) {
|
|
404
|
+
question = (question ? question + "\n\n" : "") + readQuestionFromFiles(args.file).join("\n\n");
|
|
405
|
+
}
|
|
406
|
+
if (!question) {
|
|
407
|
+
throw new ApiError('请提供问题内容,例如:node llm_cli.js ask "你好"(或用管道传入;交互式多轮请用 chat 子命令)');
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const messages = [];
|
|
411
|
+
if (args.system) messages.push({ role: "system", content: args.system });
|
|
412
|
+
messages.push({ role: "user", content: question });
|
|
413
|
+
|
|
414
|
+
const { content, usage } = await chatCompletion(rt, buildPayload(rt, messages, args));
|
|
415
|
+
if (args.output) {
|
|
416
|
+
fs.mkdirSync(path.dirname(path.resolve(args.output)), { recursive: true });
|
|
417
|
+
fs.writeFileSync(args.output, content, "utf8");
|
|
418
|
+
console.log(`${colors.dim}回答已保存到 ${args.output}${colors.reset}`);
|
|
419
|
+
}
|
|
420
|
+
printUsageLine(usage);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const SLASH_HELP = [
|
|
424
|
+
"命令:",
|
|
425
|
+
" /help 显示本帮助",
|
|
426
|
+
" /new 清空当前对话,重新开始",
|
|
427
|
+
" /provider <名称> 临时切换供应商",
|
|
428
|
+
" /model <名称> 临时切换模型",
|
|
429
|
+
" /system <文本> 设置/更新 system 提示词(不带文本则清除)",
|
|
430
|
+
" /save [路径] 保存当前对话记录为 JSON",
|
|
431
|
+
" /exit 退出(或 Ctrl+C / Ctrl+D)",
|
|
432
|
+
].join("\n");
|
|
433
|
+
|
|
434
|
+
async function cmdChat(args, cfg) {
|
|
435
|
+
const rt = resolveRuntime(args, cfg);
|
|
436
|
+
requireKey(rt.apiKey, rt);
|
|
437
|
+
|
|
438
|
+
let messages = args.system ? [{ role: "system", content: args.system }] : [];
|
|
439
|
+
if (args.resume) {
|
|
440
|
+
let data;
|
|
441
|
+
try {
|
|
442
|
+
data = JSON.parse(fs.readFileSync(args.resume, "utf8"));
|
|
443
|
+
} catch (e) {
|
|
444
|
+
throw new ApiError(`无法读取对话记录 ${args.resume}:${e.message}`);
|
|
445
|
+
}
|
|
446
|
+
messages = data.messages || messages;
|
|
447
|
+
if (data.model) rt.model = data.model;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
console.log(`${colors.cyan}llm-cli ${VERSION} · ${rt.provider}(${rt.label}) · 模型 ${rt.model} · ${rt.baseUrl}${colors.reset}`);
|
|
451
|
+
console.log(`${colors.dim}输入消息开始对话,/help 查看命令,/exit 退出。${colors.reset}`);
|
|
452
|
+
|
|
453
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
454
|
+
rl.setPrompt(`${colors.green}你${colors.reset} > `);
|
|
455
|
+
rl.prompt();
|
|
456
|
+
|
|
457
|
+
// 输入流结束(EOF)会自动关闭 readline,此时再 prompt 会抛 ERR_USE_AFTER_CLOSE
|
|
458
|
+
let rlClosed = false;
|
|
459
|
+
rl.on("close", () => { rlClosed = true; });
|
|
460
|
+
const safePrompt = () => { if (!rlClosed) rl.prompt(); };
|
|
461
|
+
|
|
462
|
+
// Ctrl+C:正在生成时中断本次回复,空闲时退出
|
|
463
|
+
rl.on("SIGINT", () => {
|
|
464
|
+
if (state.currentAbort) state.currentAbort.abort();
|
|
465
|
+
else rl.close();
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
try {
|
|
469
|
+
for await (const line of rl) {
|
|
470
|
+
const text = line.trim();
|
|
471
|
+
if (!text) { safePrompt(); continue; }
|
|
472
|
+
|
|
473
|
+
if (text.startsWith("/")) {
|
|
474
|
+
const cmd = text.split(/\s+/)[0].toLowerCase();
|
|
475
|
+
const rest = text.slice(cmd.length).trim();
|
|
476
|
+
if (["/exit", "/quit", "/q"].includes(cmd)) break;
|
|
477
|
+
else if (cmd === "/help") console.log(SLASH_HELP);
|
|
478
|
+
else if (cmd === "/new") {
|
|
479
|
+
messages = messages.filter((m) => m.role === "system");
|
|
480
|
+
console.log(`${colors.dim}已清空对话。${colors.reset}`);
|
|
481
|
+
} else if (cmd === "/provider") {
|
|
482
|
+
if (rest) {
|
|
483
|
+
const p = normalizeProvider(rest);
|
|
484
|
+
const pc = PROVIDERS[p];
|
|
485
|
+
const conf = (cfg.providers && cfg.providers[p]) || {};
|
|
486
|
+
let k = process.env[pc.envKeys[0]] || conf.api_key;
|
|
487
|
+
if (!k) {
|
|
488
|
+
console.log(`${colors.red}[${p}] 未配置 Key:config set api-key --provider ${p}${colors.reset}`);
|
|
489
|
+
} else {
|
|
490
|
+
rt.provider = p;
|
|
491
|
+
rt.pc = pc;
|
|
492
|
+
rt.apiKey = k;
|
|
493
|
+
rt.baseUrl = conf.base_url || pc.defaultBase;
|
|
494
|
+
rt.model = conf.model || pc.defaultModel;
|
|
495
|
+
if (!rt.model) console.log(`${colors.dim}注意:[${p}] 无默认模型,请用 /model 指定。${colors.reset}`);
|
|
496
|
+
console.log(`${colors.dim}已切换供应商: ${p} (${pc.label})${colors.reset}`);
|
|
497
|
+
}
|
|
498
|
+
} else {
|
|
499
|
+
console.log(`当前供应商: ${rt.provider} (${rt.label})`);
|
|
500
|
+
}
|
|
501
|
+
} else if (cmd === "/model") {
|
|
502
|
+
if (rest) { rt.model = rest; console.log(`${colors.dim}已切换模型: ${rt.model}${colors.reset}`); }
|
|
503
|
+
else console.log(`当前模型: ${rt.model}`);
|
|
504
|
+
} else if (cmd === "/system") {
|
|
505
|
+
messages = messages.filter((m) => m.role !== "system");
|
|
506
|
+
if (rest) {
|
|
507
|
+
messages.unshift({ role: "system", content: rest });
|
|
508
|
+
console.log(`${colors.dim}system 已设置。${colors.reset}`);
|
|
509
|
+
} else {
|
|
510
|
+
console.log(`${colors.dim}system 已清除。${colors.reset}`);
|
|
511
|
+
}
|
|
512
|
+
} else if (cmd === "/save") {
|
|
513
|
+
const file = rest || HISTORY_FILE;
|
|
514
|
+
fs.mkdirSync(path.dirname(path.resolve(file)), { recursive: true });
|
|
515
|
+
fs.writeFileSync(file, JSON.stringify({ provider: rt.provider, model: rt.model, messages }, null, 2) + "\n", "utf8");
|
|
516
|
+
console.log(`${colors.dim}对话已保存到 ${file}${colors.reset}`);
|
|
517
|
+
} else {
|
|
518
|
+
console.log(`${colors.dim}未知命令 ${cmd},输入 /help 查看帮助。${colors.reset}`);
|
|
519
|
+
}
|
|
520
|
+
safePrompt();
|
|
521
|
+
continue;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
if (!rt.model) {
|
|
525
|
+
console.log(`${colors.red}当前供应商没有默认模型,请先 /model <名称>。${colors.reset}`);
|
|
526
|
+
safePrompt();
|
|
527
|
+
continue;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
messages.push({ role: "user", content: text });
|
|
531
|
+
try {
|
|
532
|
+
const { content, usage } = await chatCompletion(rt, buildPayload(rt, messages, args));
|
|
533
|
+
if (content.trim()) messages.push({ role: "assistant", content });
|
|
534
|
+
else messages.pop(); // 回复为空(如被中断),移除未完成回合的用户消息
|
|
535
|
+
printUsageLine(usage);
|
|
536
|
+
} catch (e) {
|
|
537
|
+
if (e instanceof ApiError) {
|
|
538
|
+
console.error(`${colors.red}${e.message}${colors.reset}`);
|
|
539
|
+
messages.pop(); // 回合失败,移除未送达的用户消息
|
|
540
|
+
} else {
|
|
541
|
+
throw e;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
safePrompt();
|
|
545
|
+
}
|
|
546
|
+
} finally {
|
|
547
|
+
rl.close();
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
async function cmdModels(args, cfg) {
|
|
552
|
+
const rt = resolveRuntime(args, cfg);
|
|
553
|
+
requireKey(rt.apiKey, rt);
|
|
554
|
+
const url = rt.baseUrl.replace(/\/+$/, "") + "/models";
|
|
555
|
+
const controller = new AbortController();
|
|
556
|
+
const timer = setTimeout(() => controller.abort(TIMEOUT_REASON), 60000);
|
|
557
|
+
try {
|
|
558
|
+
const resp = await apiFetch(url, rt.apiKey, null, "GET", controller.signal);
|
|
559
|
+
const obj = await resp.json();
|
|
560
|
+
const items = obj.data || [];
|
|
561
|
+
if (!items.length) {
|
|
562
|
+
console.log("服务端未返回模型列表。请直接用 -m 指定模型名,或查阅对应平台文档。");
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
for (const it of items) console.log(typeof it === "string" ? it : it.id);
|
|
566
|
+
} catch (err) {
|
|
567
|
+
if (isAbort(err)) throw new ApiError("请求超时(>60s)。");
|
|
568
|
+
throw err;
|
|
569
|
+
} finally {
|
|
570
|
+
clearTimeout(timer);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
function promptHidden(question) {
|
|
575
|
+
return new Promise((resolve) => {
|
|
576
|
+
process.stderr.write(question);
|
|
577
|
+
const sink = new Writable({ write(_chunk, _enc, cb) { cb(); } });
|
|
578
|
+
const rl = readline.createInterface({ input: process.stdin, output: sink, terminal: true });
|
|
579
|
+
let done = false;
|
|
580
|
+
const finish = (answer) => {
|
|
581
|
+
if (done) return;
|
|
582
|
+
done = true;
|
|
583
|
+
rl.close();
|
|
584
|
+
process.stderr.write("\n");
|
|
585
|
+
resolve(String(answer || "").trim());
|
|
586
|
+
};
|
|
587
|
+
rl.on("close", () => finish(""));
|
|
588
|
+
rl.question("", finish);
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
async function cmdConfig(args, cfg) {
|
|
593
|
+
const [action, item, value] = args._;
|
|
594
|
+
if (action === "path") {
|
|
595
|
+
console.log(CONFIG_FILE);
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
if (action === "list" || action === "providers") {
|
|
600
|
+
for (const [id, pc] of Object.entries(PROVIDERS)) {
|
|
601
|
+
const conf = (cfg.providers && cfg.providers[id]) || {};
|
|
602
|
+
const model = conf.model || pc.defaultModel || "(需 -m 指定)";
|
|
603
|
+
console.log(`${id.padEnd(13)} ${pc.label.padEnd(18)} key:${conf.api_key || process.env[pc.envKeys[0]] ? "已配" : "未配"} 默认模型: ${model}`);
|
|
604
|
+
}
|
|
605
|
+
console.log(`默认供应商: ${cfg.default_provider || "glm"}`);
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
if (action === "get") {
|
|
610
|
+
console.log(`配置文件 : ${CONFIG_FILE}`);
|
|
611
|
+
console.log(`默认供应商 : ${cfg.default_provider || "glm"}`);
|
|
612
|
+
for (const [id, pc] of Object.entries(PROVIDERS)) {
|
|
613
|
+
const conf = (cfg.providers && cfg.providers[id]) || {};
|
|
614
|
+
if (!conf.api_key && !conf.model && !conf.base_url) continue;
|
|
615
|
+
console.log(`[${id}]`);
|
|
616
|
+
console.log(` api_key : ${mask(conf.api_key)}`);
|
|
617
|
+
console.log(` model : ${conf.model || `(默认 ${pc.defaultModel || "需 -m 指定"})`}`);
|
|
618
|
+
console.log(` base_url : ${conf.base_url || `(默认 ${pc.defaultBase})`}`);
|
|
619
|
+
}
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
const items = { "api-key": "api_key", model: "model", "base-url": "base_url" };
|
|
624
|
+
|
|
625
|
+
if (action === "set") {
|
|
626
|
+
if (item === "provider") {
|
|
627
|
+
const key = normalizeProvider(value);
|
|
628
|
+
cfg.default_provider = key;
|
|
629
|
+
saveConfig(cfg);
|
|
630
|
+
console.log(`已设置默认供应商: ${key}(${PROVIDERS[key].label})`);
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
if (!item || !(item in items)) throw new ApiError("支持设置: api-key / model / base-url / provider");
|
|
634
|
+
const provider = providerOf(args, cfg);
|
|
635
|
+
let v = value;
|
|
636
|
+
if (item === "api-key" && !v) v = await promptHidden(`请输入 [${provider}] 的 API Key(输入不会回显): `);
|
|
637
|
+
v = (v || "").trim();
|
|
638
|
+
if (!v) throw new ApiError(`缺少 ${item} 的值,例如: node llm_cli.js config set ${item} <值> --provider ${provider}`);
|
|
639
|
+
cfg.providers = cfg.providers || {};
|
|
640
|
+
cfg.providers[provider] = cfg.providers[provider] || {};
|
|
641
|
+
cfg.providers[provider][items[item]] = v;
|
|
642
|
+
saveConfig(cfg);
|
|
643
|
+
console.log(`已保存到 [${provider}]。配置文件: ${CONFIG_FILE}`);
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
if (action === "del") {
|
|
648
|
+
if (!item || !(item in items)) throw new ApiError("支持删除: api-key / model / base-url");
|
|
649
|
+
const provider = providerOf(args, cfg);
|
|
650
|
+
if (cfg.providers && cfg.providers[provider]) delete cfg.providers[provider][items[item]];
|
|
651
|
+
saveConfig(cfg);
|
|
652
|
+
console.log(`已删除 [${provider}] 的 ${item}。`);
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
throw new ApiError("用法: config set|get|del|list|path");
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// ---------------------------------------------------------------- 命令行入口
|
|
660
|
+
|
|
661
|
+
const ALIASES = {
|
|
662
|
+
"-m": "model", "--model": "model",
|
|
663
|
+
"-p": "provider", "--provider": "provider",
|
|
664
|
+
"--api-key": "api_key",
|
|
665
|
+
"--base-url": "base_url",
|
|
666
|
+
"--system": "system",
|
|
667
|
+
"-t": "temperature", "--temperature": "temperature",
|
|
668
|
+
"--max-tokens": "max_tokens",
|
|
669
|
+
"--thinking": "thinking",
|
|
670
|
+
"-f": "file", "--file": "file",
|
|
671
|
+
"-o": "output", "--output": "output",
|
|
672
|
+
"--resume": "resume",
|
|
673
|
+
};
|
|
674
|
+
|
|
675
|
+
function printHelp() {
|
|
676
|
+
const rows = Object.entries(PROVIDERS)
|
|
677
|
+
.map(([id, pc]) => ` ${id.padEnd(13)} ${pc.label.padEnd(18)} ${pc.defaultModel || "(需 -m)"}`)
|
|
678
|
+
.join("\n");
|
|
679
|
+
console.log(`llm-cli ${VERSION} —— 多供应商终端客户端(零依赖,Node.js >= 18)
|
|
680
|
+
|
|
681
|
+
用法: node llm_cli.js <command> [参数] [选项]
|
|
682
|
+
|
|
683
|
+
命令:
|
|
684
|
+
ask 单次提问: node llm_cli.js ask "问题"
|
|
685
|
+
chat 多轮交互对话(支持 /provider /model /system /save)
|
|
686
|
+
models 列出当前账号可用的模型
|
|
687
|
+
config 管理本地配置: set/get/del/list/path
|
|
688
|
+
|
|
689
|
+
供应商 (-p/--provider,默认 glm,也可按模型名前缀自动推断):
|
|
690
|
+
${rows}
|
|
691
|
+
|
|
692
|
+
选项:
|
|
693
|
+
-m, --model <名称> 模型名(无默认模型的供应商必须指定)
|
|
694
|
+
-p, --provider <名称> 供应商
|
|
695
|
+
--api-key <Key> 本次使用的 API Key(优先级最高)
|
|
696
|
+
--base-url <地址> 接口地址
|
|
697
|
+
--system <文本> system 提示词
|
|
698
|
+
-t, --temperature <值> 采样温度
|
|
699
|
+
--max-tokens <数量> 最大输出 token 数
|
|
700
|
+
--thinking <on|off> 深度思考开关(仅智谱 GLM 生效)
|
|
701
|
+
--no-stream 关闭流式输出
|
|
702
|
+
--no-color / --debug 关闭彩色 / 调试输出
|
|
703
|
+
|
|
704
|
+
示例:
|
|
705
|
+
node llm_cli.js config set api-key -p mimo
|
|
706
|
+
node llm_cli.js config list # 查看所有供应商配置状态
|
|
707
|
+
node llm_cli.js ask "用一句话解释量子纠缠"
|
|
708
|
+
node llm_cli.js ask -m deepseek-v4-pro "推理题" # 模型名前缀自动选 DeepSeek
|
|
709
|
+
node llm_cli.js chat -p kimi
|
|
710
|
+
node llm_cli.js models -p glm
|
|
711
|
+
type report.txt | node llm_cli.js ask "总结这份文档"`);
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
function parseArgs(argv) {
|
|
715
|
+
const opts = { _: [] };
|
|
716
|
+
for (let i = 0; i < argv.length; i++) {
|
|
717
|
+
const a = argv[i];
|
|
718
|
+
if (a === "-h" || a === "--help") { opts.help = true; continue; }
|
|
719
|
+
if (a === "-V" || a === "--version") { console.log(`llm-cli ${VERSION}`); process.exit(0); }
|
|
720
|
+
if (a === "--no-stream") { opts.no_stream = true; continue; }
|
|
721
|
+
if (a === "--no-color") { opts.no_color = true; continue; }
|
|
722
|
+
if (a === "--debug") { opts.debug = true; continue; }
|
|
723
|
+
const key = ALIASES[a];
|
|
724
|
+
if (!key) {
|
|
725
|
+
if (a.startsWith("-")) throw new ApiError(`未知参数: ${a}`);
|
|
726
|
+
opts._.push(a);
|
|
727
|
+
continue;
|
|
728
|
+
}
|
|
729
|
+
const val = argv[++i];
|
|
730
|
+
if (val === undefined) throw new ApiError(`参数 ${a} 缺少值`);
|
|
731
|
+
if (key === "file") (opts.file = opts.file || []).push(val);
|
|
732
|
+
else opts[key] = val;
|
|
733
|
+
}
|
|
734
|
+
return opts;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
async function main() {
|
|
738
|
+
if (typeof fetch === "undefined") {
|
|
739
|
+
console.error("需要 Node.js 18+(内置 fetch)。当前 Node 版本过旧,请升级后使用。");
|
|
740
|
+
process.exitCode = 1;
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
743
|
+
let args;
|
|
744
|
+
try {
|
|
745
|
+
args = parseArgs(process.argv.slice(2));
|
|
746
|
+
} catch (e) {
|
|
747
|
+
console.error(`${colors.red}${e.message}${colors.reset}`);
|
|
748
|
+
printHelp();
|
|
749
|
+
process.exitCode = 1;
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
enableColors(args.no_color);
|
|
753
|
+
if (args.help || args._.length === 0) {
|
|
754
|
+
printHelp();
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
757
|
+
const command = args._.shift();
|
|
758
|
+
const cfg = loadConfig();
|
|
759
|
+
try {
|
|
760
|
+
if (command === "ask") await cmdAsk(args, cfg);
|
|
761
|
+
else if (command === "chat") await cmdChat(args, cfg);
|
|
762
|
+
else if (command === "models") await cmdModels(args, cfg);
|
|
763
|
+
else if (command === "config") await cmdConfig(args, cfg);
|
|
764
|
+
else throw new ApiError(`未知命令 "${command}",可用:ask / chat / models / config`);
|
|
765
|
+
} catch (e) {
|
|
766
|
+
if (e instanceof ApiError) {
|
|
767
|
+
console.error(`${colors.red}${e.message}${colors.reset}`);
|
|
768
|
+
process.exitCode = 1;
|
|
769
|
+
} else {
|
|
770
|
+
console.error(e);
|
|
771
|
+
process.exitCode = 1;
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
main();
|
package/package.json
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vfvrpq/llm-cli",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "终端里直接使用智谱 GLM
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "终端里直接使用智谱 GLM、DeepSeek、小米 MiMo、Kimi 等主流模型的零依赖命令行客户端(含 llm-cli / glm-cli / deepseek-cli 三个命令)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "vfvrpq",
|
|
7
7
|
"type": "commonjs",
|
|
8
8
|
"bin": {
|
|
9
|
+
"llm-cli": "llm_cli.js",
|
|
9
10
|
"glm-cli": "glm_cli.js",
|
|
10
11
|
"deepseek-cli": "deepseek_cli.js"
|
|
11
12
|
},
|
|
12
13
|
"files": [
|
|
14
|
+
"llm_cli.js",
|
|
13
15
|
"glm_cli.js",
|
|
14
16
|
"deepseek_cli.js",
|
|
15
17
|
"README.md",
|
|
@@ -21,8 +23,14 @@
|
|
|
21
23
|
"keywords": [
|
|
22
24
|
"glm",
|
|
23
25
|
"deepseek",
|
|
26
|
+
"mimo",
|
|
27
|
+
"xiaomi",
|
|
28
|
+
"kimi",
|
|
29
|
+
"moonshot",
|
|
30
|
+
"siliconflow",
|
|
31
|
+
"ark",
|
|
32
|
+
"doubao",
|
|
24
33
|
"zhipu",
|
|
25
|
-
"bigmodel",
|
|
26
34
|
"llm",
|
|
27
35
|
"cli",
|
|
28
36
|
"chat"
|