@vfvrpq/llm-cli 1.1.0 → 1.2.1

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/lib/http.js ADDED
@@ -0,0 +1,50 @@
1
+ "use strict";
2
+ // HTTP 请求(fetch 封装 + 错误映射)与 SSE 流解析。
3
+ const { ApiError, friendlyHTTPError } = require("./errors");
4
+
5
+ const STREAM_IDLE_MS = 120000; // 流式模式下两次数据块之间的最大等待(毫秒)
6
+ const PLAIN_MS = 300000; // 非流式模式的整体等待(毫秒)
7
+ const TIMEOUT_REASON = "llm-cli-idle-timeout";
8
+
9
+ function isAbort(err) {
10
+ return err && (err.name === "AbortError" || err.code === "ABORT_ERR");
11
+ }
12
+
13
+ async function apiFetch(url, apiKey, payload, method, signal, hints) {
14
+ const headers = { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" };
15
+ let body;
16
+ if (payload !== null && payload !== undefined) {
17
+ body = JSON.stringify(payload);
18
+ if (payload.stream) headers.Accept = "text/event-stream";
19
+ }
20
+ let resp;
21
+ try {
22
+ resp = await fetch(url, { method, headers, body, signal });
23
+ } catch (err) {
24
+ if (isAbort(err)) throw err;
25
+ const cause = err.cause ? (err.cause.message || String(err.cause)) : err.message;
26
+ throw new ApiError(`无法连接服务器:${cause}(请检查网络或 --base-url)`);
27
+ }
28
+ if (!resp.ok) throw new ApiError(await friendlyHTTPError(resp, hints));
29
+ return resp;
30
+ }
31
+
32
+ // 解析 OpenAI 风格 SSE:逐行产出 data: 负载,遇到 [DONE] 结束
33
+ async function* iterSSE(body) {
34
+ const decoder = new TextDecoder();
35
+ let buf = "";
36
+ for await (const chunk of body) {
37
+ buf += decoder.decode(chunk, { stream: true });
38
+ let idx;
39
+ while ((idx = buf.indexOf("\n")) >= 0) {
40
+ const line = buf.slice(0, idx).trim();
41
+ buf = buf.slice(idx + 1);
42
+ if (!line.startsWith("data:")) continue;
43
+ const data = line.slice(5).trim();
44
+ if (data === "[DONE]") return;
45
+ if (data) yield data;
46
+ }
47
+ }
48
+ }
49
+
50
+ module.exports = { STREAM_IDLE_MS, PLAIN_MS, TIMEOUT_REASON, isAbort, apiFetch, iterSSE };
@@ -0,0 +1,77 @@
1
+ "use strict";
2
+ // 供应商注册表操作:选择、模型名推断、运行时解析、Key 解析与掩码。
3
+ const { ApiError, DEFAULT_HINTS } = require("./errors");
4
+
5
+ function normalizeProvider(p, registry) {
6
+ const key = String(p || "").trim().toLowerCase();
7
+ const alias = { zhipu: "glm", moonshot: "kimi", xiaomi: "mimo", volc: "ark" };
8
+ const name = alias[key] || key;
9
+ if (!registry[name]) {
10
+ throw new ApiError(`未知供应商 "${p}",可选:${Object.keys(registry).join(" / ")}`);
11
+ }
12
+ return name;
13
+ }
14
+
15
+ // 没显式指定 --provider 时,按模型名前缀推断(如 glm-* / deepseek-* / mimo-*)
16
+ function inferProvider(model, registry) {
17
+ const m = String(model || "").toLowerCase();
18
+ for (const [id, pc] of Object.entries(registry)) {
19
+ if ((pc.prefixes || []).some((p) => m.startsWith(p))) return id;
20
+ }
21
+ return null;
22
+ }
23
+
24
+ function providerOf(args, cfg, ctx) {
25
+ if (ctx.fixedProvider) return ctx.fixedProvider;
26
+ if (args.provider) return normalizeProvider(args.provider, ctx.registry);
27
+ const inferred = ctx.features.provider ? inferProvider(args.model, ctx.registry) : null;
28
+ const fallback = cfg.default_provider || Object.keys(ctx.registry)[0];
29
+ return normalizeProvider(inferred || fallback, ctx.registry);
30
+ }
31
+
32
+ function resolveRuntime(args, cfg, ctx) {
33
+ const provider = providerOf(args, cfg, ctx);
34
+ const pc = ctx.registry[provider];
35
+ const conf = (cfg.providers && cfg.providers[provider]) || {};
36
+ let apiKey = args.api_key;
37
+ if (!apiKey) {
38
+ for (const k of pc.envKeys) {
39
+ const v = process.env[k];
40
+ if (v) { apiKey = v; break; }
41
+ }
42
+ }
43
+ if (!apiKey) apiKey = conf.api_key;
44
+ const model = args.model || conf.model || pc.defaultModel;
45
+ if (!model) {
46
+ throw new ApiError(`[${provider}] 没有内置默认模型,必须用 -m 指定。${pc.hint ? "\n" + pc.hint : ""}`);
47
+ }
48
+ return {
49
+ provider,
50
+ label: pc.label,
51
+ apiKey,
52
+ model,
53
+ baseUrl: args.base_url || conf.base_url || pc.defaultBase,
54
+ pc,
55
+ hints: { ...DEFAULT_HINTS, ...(pc.hints || {}) },
56
+ };
57
+ }
58
+
59
+ function requireKey(apiKey, rt) {
60
+ if (apiKey) return apiKey;
61
+ const pc = rt.pc;
62
+ throw new ApiError(
63
+ `尚未配置 [${rt.provider}] 的 API Key,请任选其一:\n` +
64
+ ` 1. config set api-key${` --provider ${rt.provider}`} (推荐)\n` +
65
+ ` 2. 设置环境变量 ${pc.envKeys[0]}\n` +
66
+ ` 3. 临时使用:--api-key <你的Key>\n` +
67
+ `Key 获取:${pc.keyUrl}`
68
+ );
69
+ }
70
+
71
+ function mask(key) {
72
+ if (!key) return "(未设置)";
73
+ if (key.length <= 8) return key.slice(0, 2) + "****";
74
+ return key.slice(0, 6) + "..." + key.slice(-4);
75
+ }
76
+
77
+ module.exports = { normalizeProvider, inferProvider, providerOf, resolveRuntime, requireKey, mask };
package/lib/run.js ADDED
@@ -0,0 +1,73 @@
1
+ "use strict";
2
+ // 入口引导:解析参数 → 加载配置 → 分发子命令。三个入口脚本都通过 run() 启动。
3
+ const { parseArgs, printHelp } = require("./args");
4
+ const { enableColors, colors } = require("./colors");
5
+ const { ApiError } = require("./errors");
6
+ const { loadUnified, saveUnified, resolveConfigPath, historyPathFor } = require("./config");
7
+ const ask = require("./commands/ask");
8
+ const chat = require("./commands/chat");
9
+ const models = require("./commands/models");
10
+ const configCmd = require("./commands/config-cmd");
11
+
12
+ async function run(entry) {
13
+ if (typeof fetch === "undefined") {
14
+ console.error("需要 Node.js 18+(内置 fetch)。当前 Node 版本过旧,请升级后使用。");
15
+ process.exitCode = 1;
16
+ return;
17
+ }
18
+ if (process.platform === "win32") {
19
+ for (const s of [process.stdout, process.stderr]) {
20
+ try { s.reconfigure({ errors: "replace" }); } catch {}
21
+ }
22
+ }
23
+
24
+ let args;
25
+ try {
26
+ args = parseArgs(process.argv.slice(2), entry.features);
27
+ } catch (e) {
28
+ console.error(`${colors.red}${e.message}${colors.reset}`);
29
+ printHelp(entry);
30
+ process.exitCode = 1;
31
+ return;
32
+ }
33
+ enableColors(args.no_color);
34
+ if (args.version) {
35
+ console.log(`${entry.name} ${entry.version}`);
36
+ return;
37
+ }
38
+ if (args.help || args._.length === 0) {
39
+ printHelp(entry);
40
+ return;
41
+ }
42
+
43
+ const command = args._.shift();
44
+ const configFile = resolveConfigPath(entry);
45
+ const cfg = loadUnified(configFile, { singleProvider: entry.fixedProvider });
46
+ const ctx = {
47
+ entry,
48
+ registry: entry.registry,
49
+ features: entry.features,
50
+ fixedProvider: entry.fixedProvider,
51
+ configFile,
52
+ historyFile: historyPathFor(configFile),
53
+ saveCfg: (c) => saveUnified(configFile, c, { singleProvider: entry.fixedProvider }),
54
+ };
55
+
56
+ try {
57
+ if (command === "ask") await ask(args, cfg, ctx);
58
+ else if (command === "chat") await chat(args, cfg, ctx);
59
+ else if (command === "models") await models(args, cfg, ctx);
60
+ else if (command === "config") await configCmd(args, cfg, ctx);
61
+ else throw new ApiError(`未知命令 "${command}",可用:ask / chat / models / config`);
62
+ } catch (e) {
63
+ if (e instanceof ApiError) {
64
+ console.error(`${colors.red}${e.message}${colors.reset}`);
65
+ process.exitCode = 1;
66
+ } else {
67
+ console.error(e);
68
+ process.exitCode = 1;
69
+ }
70
+ }
71
+ }
72
+
73
+ module.exports = { run };