@vfvrpq/llm-cli 1.0.0 → 1.2.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 +48 -0
- package/deepseek_cli.js +26 -592
- package/glm_cli.js +27 -594
- package/lib/args.js +93 -0
- package/lib/chat.js +110 -0
- package/lib/colors.js +15 -0
- package/lib/commands/ask.js +50 -0
- package/lib/commands/chat.js +142 -0
- package/lib/commands/config-cmd.js +92 -0
- package/lib/commands/models.js +30 -0
- package/lib/config.js +50 -0
- package/lib/errors.js +32 -0
- package/lib/hidden.js +24 -0
- package/lib/http.js +50 -0
- package/lib/registry.js +77 -0
- package/lib/run.js +73 -0
- package/llm_cli.js +38 -0
- package/package.json +13 -3
- package/providers/ark/index.js +12 -0
- package/providers/deepseek/index.js +12 -0
- package/providers/glm/index.js +12 -0
- package/providers/index.js +14 -0
- package/providers/kimi/index.js +12 -0
- package/providers/mimo/index.js +12 -0
- package/providers/openai/index.js +12 -0
- package/providers/siliconflow/index.js +12 -0
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 };
|
package/lib/registry.js
ADDED
|
@@ -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 };
|
package/llm_cli.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* llm-cli —— 多供应商终端客户端(薄入口)。
|
|
4
|
+
* 实现为原子化模块:命令/协议在 lib/,每家供应商的定义独立在 providers/<名称>/index.js。
|
|
5
|
+
*
|
|
6
|
+
* 快速开始:
|
|
7
|
+
* node llm_cli.js config set api-key -p 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
|
+
"use strict";
|
|
13
|
+
|
|
14
|
+
const { run } = require("./lib/run");
|
|
15
|
+
const { ALL } = require("./providers");
|
|
16
|
+
|
|
17
|
+
const VERSION = "1.2.0";
|
|
18
|
+
|
|
19
|
+
run({
|
|
20
|
+
name: "llm-cli",
|
|
21
|
+
file: "llm_cli.js",
|
|
22
|
+
version: VERSION,
|
|
23
|
+
tagline: "多供应商终端客户端(零依赖,Node.js >= 18)",
|
|
24
|
+
registry: ALL,
|
|
25
|
+
configEnv: "LLM_CLI_HOME",
|
|
26
|
+
configDirName: ".llm-cli",
|
|
27
|
+
fixedProvider: null,
|
|
28
|
+
features: { provider: true, thinking: true, list: true },
|
|
29
|
+
examples: [
|
|
30
|
+
"node llm_cli.js config set api-key -p mimo",
|
|
31
|
+
"node llm_cli.js config list # 查看所有供应商配置状态",
|
|
32
|
+
'node llm_cli.js ask "用一句话解释量子纠缠"',
|
|
33
|
+
'node llm_cli.js ask -m deepseek-v4-pro "推理题" # 模型名前缀自动选 DeepSeek',
|
|
34
|
+
"node llm_cli.js chat -p kimi",
|
|
35
|
+
"node llm_cli.js models -p glm",
|
|
36
|
+
'type report.txt | node llm_cli.js ask "总结这份文档"',
|
|
37
|
+
],
|
|
38
|
+
});
|
package/package.json
CHANGED
|
@@ -1,17 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vfvrpq/llm-cli",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "终端里直接使用智谱 GLM
|
|
3
|
+
"version": "1.2.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",
|
|
17
|
+
"lib",
|
|
18
|
+
"providers",
|
|
15
19
|
"README.md",
|
|
16
20
|
"LICENSE"
|
|
17
21
|
],
|
|
@@ -21,8 +25,14 @@
|
|
|
21
25
|
"keywords": [
|
|
22
26
|
"glm",
|
|
23
27
|
"deepseek",
|
|
28
|
+
"mimo",
|
|
29
|
+
"xiaomi",
|
|
30
|
+
"kimi",
|
|
31
|
+
"moonshot",
|
|
32
|
+
"siliconflow",
|
|
33
|
+
"ark",
|
|
34
|
+
"doubao",
|
|
24
35
|
"zhipu",
|
|
25
|
-
"bigmodel",
|
|
26
36
|
"llm",
|
|
27
37
|
"cli",
|
|
28
38
|
"chat"
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// 火山方舟 Ark(豆包)供应商定义(模型为接入点 ID 或 doubao-* 名称)。
|
|
3
|
+
module.exports = {
|
|
4
|
+
label: "火山方舟 Ark(豆包)",
|
|
5
|
+
defaultBase: "https://ark.cn-beijing.volces.com/api/v3",
|
|
6
|
+
defaultModel: null,
|
|
7
|
+
envKeys: ["ARK_API_KEY"],
|
|
8
|
+
keyUrl: "https://console.volcengine.com/ark -> API Key",
|
|
9
|
+
hint: "模型为接入点 ID 或 doubao-* 名称,需用 -m 指定",
|
|
10
|
+
supportsThinking: false,
|
|
11
|
+
prefixes: ["ark", "doubao"],
|
|
12
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// DeepSeek 供应商定义。
|
|
3
|
+
module.exports = {
|
|
4
|
+
label: "DeepSeek",
|
|
5
|
+
defaultBase: "https://api.deepseek.com/v1",
|
|
6
|
+
defaultModel: "deepseek-flash",
|
|
7
|
+
envKeys: ["DEEPSEEK_API_KEY"],
|
|
8
|
+
keyUrl: "https://platform.deepseek.com -> API Keys",
|
|
9
|
+
hint: "当前账号可用:deepseek-flash / deepseek-v4-pro",
|
|
10
|
+
supportsThinking: false,
|
|
11
|
+
prefixes: ["deepseek"],
|
|
12
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// 智谱 GLM 供应商定义。修改供应商(端点/默认模型/Key 环境变量)只动本文件。
|
|
3
|
+
module.exports = {
|
|
4
|
+
label: "智谱 GLM",
|
|
5
|
+
defaultBase: "https://open.bigmodel.cn/api/paas/v4",
|
|
6
|
+
defaultModel: "glm-5.3-flash",
|
|
7
|
+
envKeys: ["GLM_API_KEY", "ZHIPUAI_API_KEY", "ZHIPU_API_KEY"],
|
|
8
|
+
keyUrl: "https://open.bigmodel.cn 控制台 -> API Key",
|
|
9
|
+
hint: "可用模型:glm-5.3-flash / glm-5.3 / glm-4.6 / glm-4.7 等(coding 订阅端点见 README)",
|
|
10
|
+
supportsThinking: true,
|
|
11
|
+
prefixes: ["glm"],
|
|
12
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// 供应商聚合注册表:新增供应商 = 在 providers/ 下建文件夹 + 在此登记一行。
|
|
3
|
+
// 顺序即默认优先级(第一个是兜底默认供应商)。
|
|
4
|
+
const ALL = {
|
|
5
|
+
glm: require("./glm"),
|
|
6
|
+
deepseek: require("./deepseek"),
|
|
7
|
+
mimo: require("./mimo"),
|
|
8
|
+
kimi: require("./kimi"),
|
|
9
|
+
siliconflow: require("./siliconflow"),
|
|
10
|
+
ark: require("./ark"),
|
|
11
|
+
openai: require("./openai"),
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
module.exports = { ALL };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Kimi(月之暗面)供应商定义。端点参考 cc-switch codexProviderPresets。
|
|
3
|
+
module.exports = {
|
|
4
|
+
label: "Kimi(月之暗面)",
|
|
5
|
+
defaultBase: "https://api.moonshot.cn/v1",
|
|
6
|
+
defaultModel: "kimi-latest",
|
|
7
|
+
envKeys: ["MOONSHOT_API_KEY", "KIMI_API_KEY"],
|
|
8
|
+
keyUrl: "https://platform.moonshot.cn -> API Key",
|
|
9
|
+
hint: "默认 kimi-latest(自动指向最新模型)",
|
|
10
|
+
supportsThinking: false,
|
|
11
|
+
prefixes: ["kimi", "moonshot"],
|
|
12
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// 小米 MiMo 供应商定义(端点与模型清单 2026-09 实测)。
|
|
3
|
+
module.exports = {
|
|
4
|
+
label: "小米 MiMo",
|
|
5
|
+
defaultBase: "https://api.xiaomimimo.com/v1",
|
|
6
|
+
defaultModel: "mimo-v2.6-flash",
|
|
7
|
+
envKeys: ["MIMO_API_KEY", "XIAOMI_API_KEY"],
|
|
8
|
+
keyUrl: "https://platform.xiaomimimo.com",
|
|
9
|
+
hint: "可用模型:mimo-v2.6-flash / mimo-v2.6-pro / mimo-v2.5 等",
|
|
10
|
+
supportsThinking: false,
|
|
11
|
+
prefixes: ["mimo"],
|
|
12
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// OpenAI 供应商定义。
|
|
3
|
+
module.exports = {
|
|
4
|
+
label: "OpenAI",
|
|
5
|
+
defaultBase: "https://api.openai.com/v1",
|
|
6
|
+
defaultModel: null,
|
|
7
|
+
envKeys: ["OPENAI_API_KEY"],
|
|
8
|
+
keyUrl: "https://platform.openai.com -> API keys",
|
|
9
|
+
hint: "需用 -m 指定模型(如 gpt-*)",
|
|
10
|
+
supportsThinking: false,
|
|
11
|
+
prefixes: ["gpt"],
|
|
12
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// 硅基流动 SiliconFlow 供应商定义(模型库丰富,无统一默认模型,需 -m 指定)。
|
|
3
|
+
module.exports = {
|
|
4
|
+
label: "硅基流动 SiliconFlow",
|
|
5
|
+
defaultBase: "https://api.siliconflow.cn/v1",
|
|
6
|
+
defaultModel: null,
|
|
7
|
+
envKeys: ["SILICONFLOW_API_KEY"],
|
|
8
|
+
keyUrl: "https://cloud.siliconflow.cn -> API 密钥",
|
|
9
|
+
hint: "模型名形如 deepseek-ai/DeepSeek-V3.1,需用 -m 指定",
|
|
10
|
+
supportsThinking: false,
|
|
11
|
+
prefixes: ["siliconflow"],
|
|
12
|
+
};
|