#!/usr/bin/env node /** * thinking.mjs —— ZCode 思考档位(双写:热 + 持久)。 * * node thinking.mjs status [--json] [--model ] * node thinking.mjs set [] [--model ] [--json] [--hot-only | --persist-only] * node thinking.mjs rollback [--json] [--steps N] * node thinking.mjs history [--json] [--limit N] * node thinking.mjs variants [--model ] [--json] * node thinking.mjs defaults [--json] | defaults set [--variant X] [--model Y] | defaults clear * node thinking.mjs doctor [--json] * * 环境变量: * ZCODE_V2_CONFIG v2/config.json 路径(默认 ~/.zcode/v2/config.json) * ZCODE_SESSION_DB session 库路径(默认 ~/.zcode/cli/db/db.sqlite) * ZCODE_THINKING_CONFIG 本插件的默认值文件(默认 ~/.zcode/zcode-thinking-mode.json) * ZCODE_THINKING_STATE 切换历史 state 文件(默认与默认值文件同目录 zcode-thinking-mode-state.json) * ZCODE_THINKING_VARIANT / ZCODE_THINKING_MODEL 临时覆盖默认值(优先于文件) * * 双写说明: * 1) session 库 local_setting(user/default/model/reasoningLevel)—— 全局热档位, * 新会话/新 turn 模型解析直接读它;/effort 切的也是这一格。 * 2) v2/config.json 各模型 reasoning.defaultVariant —— 模型配置里的持久默认。 * v2/config 写前自动备份: config.json.bak-(同秒撞名时补 .1/.2)。 * sqlite 单格 upsert,旧值会打印出来(回滚就是再 set 回去)。 * * 默认值文件说明: * plugin.json 里的 userConfig(default_variant / target_model)在设置界面能填, * 但宿主只把 ${user_config.*} 插值到 MCP server 字段与 hook,不会插到命令正文里, * 所以脚本必须自己读一份落地文件才有意义 —— 就是这个 defaults 文件。 * 优先级:命令行参数 > 环境变量 > 默认值文件 > 内置兜底。 * * 生效: * 新会话/重启后按新档位;**当前会话要立刻生效,再敲一行内置命令 /effort <档位>**(热切,不重启)。 */ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; const V2_CONFIG = process.env.ZCODE_V2_CONFIG || path.join(os.homedir(), ".zcode", "v2", "config.json"); const SESSION_DB = process.env.ZCODE_SESSION_DB || path.join(os.homedir(), ".zcode", "cli", "db", "db.sqlite"); const DEFAULTS_FILE = process.env.ZCODE_THINKING_CONFIG || path.join(os.homedir(), ".zcode", "zcode-thinking-mode.json"); const STATE_FILE = process.env.ZCODE_THINKING_STATE || path.join(path.dirname(DEFAULTS_FILE), "zcode-thinking-mode-state.json"); /** 没有默认值文件、也没有环境变量时的兜底 */ const BUILTIN_DEFAULTS = { variant: null, model: null }; // ---------------------------------------------------------------- 小工具 function fail(msg, json, extra) { if (json) console.log(JSON.stringify({ error: msg, v2config: V2_CONFIG, sessionDb: SESSION_DB, defaultsFile: DEFAULTS_FILE, stateFile: STATE_FILE, ...(extra ?? {}) }, null, 2)); else { console.log(`思考模式切换失败:${msg}`); console.log(`模型配置:${V2_CONFIG}`); console.log(`热档位库:${SESSION_DB}(可用 ZCODE_V2_CONFIG / ZCODE_SESSION_DB 指定)`); } process.exitCode = 1; } function loadConfig() { return JSON.parse(fs.readFileSync(V2_CONFIG, "utf8")); } /* ---------------------------------------------------------------- 默认值 * plugin.json 的 userConfig 在设置界面能填,但宿主只把 ${user_config.*} 插值进 * MCP server 字段与 hook,命令正文拿不到 —— 所以脚本自己读这份落地文件, * 否则那两个配置项就是摆设(填了也不生效)。 */ function readDefaults() { let file = {}; try { file = JSON.parse(fs.readFileSync(DEFAULTS_FILE, "utf8")) ?? {}; } catch { /* 无文件/坏文件都当空 */ } const pick = (...vs) => vs.find((v) => typeof v === "string" && v.trim() !== "") ?? null; return { file, variant: pick(process.env.ZCODE_THINKING_VARIANT, file.variant, BUILTIN_DEFAULTS.variant), model: pick(process.env.ZCODE_THINKING_MODEL, file.model, BUILTIN_DEFAULTS.model), source: { variant: pick(process.env.ZCODE_THINKING_VARIANT) ? "env" : (pick(file.variant) ? "file" : "none"), model: pick(process.env.ZCODE_THINKING_MODEL) ? "env" : (pick(file.model) ? "file" : "none"), }, }; } function writeDefaults(patch) { const cur = readDefaults().file ?? {}; for (const [k, v] of Object.entries(patch)) { if (v === null || v === undefined || v === "") delete cur[k]; else cur[k] = v; } fs.mkdirSync(path.dirname(DEFAULTS_FILE), { recursive: true }); fs.writeFileSync(DEFAULTS_FILE, JSON.stringify(cur, null, 2) + "\n", "utf8"); return cur; } const DEFAULTS_HINT = "默认值文件:~/.zcode/zcode-thinking-mode.json(可用 `defaults set` 写、`defaults clear` 清)"; function defaultsReport(asJson) { const d = readDefaults(); if (asJson) { console.log(JSON.stringify({ file: DEFAULTS_FILE, exists: fs.existsSync(DEFAULTS_FILE), content: d.file, effective: { variant: d.variant, model: d.model }, source: d.source, builtin: BUILTIN_DEFAULTS, }, null, 2)); return 0; } console.log("## 思考模式默认值"); console.log(""); console.log(`- 默认档位:\`${d.variant ?? "(未设)"}\`${d.variant ? ` (来源:${d.source.variant === "env" ? "环境变量" : "默认值文件"})` : ""}`); console.log(`- 默认模型:\`${d.model ?? "(全部)"}\`${d.model ? ` (来源:${d.source.model === "env" ? "环境变量" : "默认值文件"})` : ""}`); console.log(`- 文件:${DEFAULTS_FILE}${fs.existsSync(DEFAULTS_FILE) ? "" : "(尚未创建)"}`); if (fs.existsSync(DEFAULTS_FILE)) console.log(`- 内容:${JSON.stringify(d.file)}`); console.log(""); console.log("优先级:命令行参数 > 环境变量(ZCODE_THINKING_VARIANT / ZCODE_THINKING_MODEL) > 默认值文件 > 内置兜底"); console.log(""); console.log("set 不带档位参数时会用这里的默认档位;status/variants 不带 --model 时用这里的默认模型。"); return 0; } /* ---------------------------------------------------------------- doctor * 自检:安装是否完整、路径能否定位、两个写入目标是否可写、node:sqlite 是否可用。 * 只读检查,不改任何东西;有 fail 时退出码 1。 */ async function doctor(asJson) { const checks = []; const add = (level, name, detail, fix) => checks.push({ level, name, detail, fix }); const exists = (p) => { try { return fs.existsSync(p); } catch { return false; } }; const major = Number(process.versions.node.split(".")[0]); add(major >= 22 ? "ok" : "warn", "Node 版本", `v${process.versions.node}`, major >= 22 ? null : "热档位读写需要 node:sqlite(Node ≥22.5),否则只能改模型配置"); let sqlite = null; try { ({ DatabaseSync: sqlite } = await import("node:sqlite")); } catch { /* 下面报 */ } add(sqlite ? "ok" : "fail", "node:sqlite", sqlite ? "可用" : "导入失败", sqlite ? null : "升级 Node 到 ≥22.5,否则热档位那一半写不了"); // 模型配置 if (!exists(V2_CONFIG)) add("fail", "模型配置", `读不到 ${V2_CONFIG}`, "确认已配置过模型;或设 ZCODE_V2_CONFIG 指向实际路径"); else { let doc = null; try { doc = loadConfig(); } catch (e) { add("fail", "模型配置", `JSON 解析失败(${e.message})`, "修好 JSON 或从 .bak-* 恢复"); } if (doc) { const withReasoning = collectModels(doc).filter((m) => m.reasoning); add(withReasoning.length ? "ok" : "warn", "可切换模型", `${withReasoning.length} 个带 reasoning`, withReasoning.length ? null : "没有任何模型声明 reasoning.variants,档位切不了"); add("ok", "模型配置解析", V2_CONFIG, null); } } // 热档位库 if (!exists(SESSION_DB)) add("fail", "热档位库", `读不到 ${SESSION_DB}`, "确认 ZCode 跑过至少一次;或设 ZCODE_SESSION_DB"); else if (sqlite) { try { const db = new sqlite(SESSION_DB, { readOnly: true }); const tbl = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='local_setting'").get(); db.close(); add(tbl ? "ok" : "fail", "热档位库结构", tbl ? "有 local_setting 表" : "缺 local_setting 表", tbl ? null : "这不是有效的 session 库,热档位写不进去"); } catch (e) { add("fail", "热档位库", `打开失败(${e.message})`, "检查文件权限/是否被独占"); } } // 热档位当前值与库是否一致 const hot = await readGlobalLevel(); add(hot.error ? "warn" : "ok", "当前热档位", hot.error ? hot.error : (hot.level ?? `(未设:${hot.note ?? "无此行"})`), null); // 默认值文件 const d = readDefaults(); add("ok", "默认值", `档位=${d.variant ?? "(未设)"} 模型=${d.model ?? "(全部)"}`, `来源:${d.source.variant}/${d.source.model};${DEFAULTS_HINT}`); // 写入能力 const dir = path.dirname(V2_CONFIG); add(exists(dir) ? "ok" : "warn", "配置目录", dir, exists(dir) ? null : "目录不存在,切换时会创建失败"); if (exists(V2_CONFIG)) { try { fs.accessSync(V2_CONFIG, fs.constants.W_OK); add("ok", "配置可写", "有写权限", null); } catch { add("fail", "配置可写", "无写权限", "检查文件是否为只读或属主不对"); } } add(exists(path.dirname(DEFAULTS_FILE)) ? "ok" : "warn", "默认值目录", path.dirname(DEFAULTS_FILE), exists(path.dirname(DEFAULTS_FILE)) ? null : "目录不存在,`defaults set` 时会自动创建"); // 备份可写 const bakProbe = `${V2_CONFIG}.bak-probe`; let backupWritable = false; try { fs.writeFileSync(bakProbe, ""); fs.unlinkSync(bakProbe); backupWritable = true; } catch { /* 下面报 */ } add(backupWritable ? "ok" : "fail", "备份可写", backupWritable ? "能在配置目录写备份" : "写不了备份文件", backupWritable ? null : "配置目录不可写,切换时会因无法备份而中止"); const fails = checks.filter((c) => c.level === "fail"); const warns = checks.filter((c) => c.level === "warn"); if (asJson) { console.log(JSON.stringify({ v2config: V2_CONFIG, sessionDb: SESSION_DB, defaultsFile: DEFAULTS_FILE, checks, fails: fails.length, warns: warns.length }, null, 2)); } else { const icon = { ok: "✓", warn: "⚠", fail: "✗" }; console.log("# zcode-thinking-mode 自检"); console.log(`- ${checks.length} 项:${checks.length - fails.length - warns.length} ok / ${warns.length} warn / ${fails.length} fail`); console.log(""); for (const c of checks) { console.log(`${icon[c.level]} ${c.name}:${c.detail}`); if (c.fix) console.log(` → ${c.fix}`); } console.log(""); console.log(fails.length ? `结论:有 ${fails.length} 项必须先修,否则切换会失败。` : warns.length ? `结论:可正常工作,${warns.length} 项警告建议处理。` : "结论:一切就绪。"); } return fails.length ? 1 : 0; } /** * 备份名精确到秒,同一秒内连切两次会撞名 —— 后一份会覆盖前一份, * 于是"回滚到改动前"实际退回的是倒数第二次的中间态。撞名时补序号。 */ function saveConfig(doc) { const ts = new Date(); const p = (x) => String(x).padStart(2, "0"); const stamp = `${ts.getFullYear()}${p(ts.getMonth() + 1)}${p(ts.getDate())}-${p(ts.getHours())}${p(ts.getMinutes())}${p(ts.getSeconds())}`; let bak = `${V2_CONFIG}.bak-${stamp}`; for (let i = 1; fs.existsSync(bak); i++) bak = `${V2_CONFIG}.bak-${stamp}.${i}`; fs.copyFileSync(V2_CONFIG, bak); fs.writeFileSync(V2_CONFIG, JSON.stringify(doc, null, 2), "utf8"); return bak; } function collectModels(doc) { const out = []; const providers = doc?.provider ?? {}; for (const [pid, p] of Object.entries(providers)) { const models = p?.models ?? {}; for (const [mname, m] of Object.entries(models)) { out.push({ providerId: pid, providerName: p?.name ?? pid, model: mname, enabled: p?.enabled !== false, reasoning: m?.reasoning ?? null, }); } } return out; } function filterByModel(list, name) { if (!name) return list; const n = name.toLowerCase(); return list.filter((m) => m.model.toLowerCase() === n); } // ---------------------------------------------------------------- 热档位(sqlite) async function readGlobalLevel() { let DatabaseSync; try { ({ DatabaseSync } = await import("node:sqlite")); } catch { return { error: "当前 node 不支持 node:sqlite(需 Node 22.5+),热档位读不到" }; } let db; try { db = new DatabaseSync(SESSION_DB, { readOnly: true }); const row = db.prepare( "SELECT value FROM local_setting WHERE scope='user' AND scope_id='default' AND namespace='model' AND key='reasoningLevel'" ).get(); if (!row) return { level: null, note: "库里没有 reasoningLevel 行(从未用 /effort 切过)" }; const v = JSON.parse(row.value); return { level: typeof v?.level === "string" ? v.level : null }; } catch (e) { return { error: `读热档位库失败(${e?.message ?? e})` }; } finally { try { db?.close(); } catch { /* ignore */ } } } async function writeGlobalLevel(level) { let DatabaseSync; try { ({ DatabaseSync } = await import("node:sqlite")); } catch { throw new Error("当前 node 不支持 node:sqlite(需 Node 22.5+),热档位写不了(模型配置仍可写)"); } const db = new DatabaseSync(SESSION_DB); try { db.exec("PRAGMA busy_timeout = 5000"); const tbl = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='local_setting'").get(); if (!tbl) throw new Error("库里没有 local_setting 表,不是有效的 session 库"); const prev = db.prepare( "SELECT value FROM local_setting WHERE scope='user' AND scope_id='default' AND namespace='model' AND key='reasoningLevel'" ).get(); const now = Date.now(); db.prepare(` INSERT INTO local_setting (scope, scope_id, namespace, key, value, schema_version, time_created, time_updated) VALUES ('user', 'default', 'model', 'reasoningLevel', ?, 1, ?, ?) ON CONFLICT(scope, scope_id, namespace, key) DO UPDATE SET value = excluded.value, schema_version = excluded.schema_version, time_updated = excluded.time_updated `).run(JSON.stringify({ level }), now, now); let previous = null; try { previous = prev ? JSON.parse(prev.value)?.level ?? null : null; } catch { /* ignore */ } return { previous }; } finally { try { db?.close(); } catch { /* ignore */ } } } // ---------------------------------------------------------------- state 历史 // 每次 set 成功后追加一条:{ts, variant, model, hotOnly, persistOnly, // hotPrevious, hotLevel, persistPrevious:[{provider,model,from,to}], persistCurrent, // backup}。rollback 按最新一条把热档位与持久默认写回去,不删历史(可多次回滚)。 function readState() { try { const raw = fs.readFileSync(STATE_FILE, "utf8"); const j = JSON.parse(raw); return Array.isArray(j?.entries) ? j.entries : []; } catch { return []; } } function appendState(entry) { const entries = readState(); entries.push({ ts: new Date().toISOString(), ...entry }); // 只保留最近 50 条,防止无限增长 const kept = entries.slice(-50); fs.mkdirSync(path.dirname(STATE_FILE), { recursive: true }); fs.writeFileSync(STATE_FILE, JSON.stringify({ entries: kept }, null, 2) + "\n", "utf8"); return kept; } function historyReport(asJson, limit) { const entries = readState().slice(-(limit ?? 10)); if (asJson) { console.log(JSON.stringify({ stateFile: STATE_FILE, count: entries.length, entries }, null, 2)); return 0; } console.log("## 思考模式切换历史"); console.log(""); console.log(`- 文件:${STATE_FILE}${fs.existsSync(STATE_FILE) ? "" : "(尚无记录)"}`); if (!entries.length) { console.log("- 暂无切换记录"); return 0; } for (const e of entries) { const scope = e.hotOnly ? "仅热" : e.persistOnly ? "仅持久" : "热+持久"; console.log(`- ${e.ts} ${e.hotPrevious ?? "—"} → \`${e.hotLevel}\` [${scope}]${e.model ? ` (模型:${e.model})` : ""}${e.backup ? ` 备份:${e.backup}` : ""}`); } console.log(""); console.log(`回滚:node scripts/thinking.mjs rollback(把最近一次的改动写回去)`); return 0; } async function doRollback(asJson, steps) { const entries = readState(); if (!entries.length) { fail("没有切换历史可回滚", asJson, { stateFile: STATE_FILE }); process.exit(1); } const n = Math.max(1, Math.min(steps ?? 1, entries.length)); const target = entries[entries.length - n]; // 1) 热档位写回 if (target.hotPrevious !== undefined && target.hotPrevious !== null) { try { await writeGlobalLevel(target.hotPrevious); } catch (e) { fail(`回滚热档位失败(${e?.message ?? e})——模型配置未动`, asJson); process.exit(1); } } // 2) 持久默认逐个写回(只动历史里记录过的模型) let doc; try { doc = loadConfig(); } catch (e) { fail(`读模型配置失败(${e?.message ?? e})`, asJson); process.exit(1); } const restored = []; for (const p of target.persistPrevious ?? []) { const m = doc?.provider?.[p.provider]?.models?.[p.model]; if (m?.reasoning && m.reasoning.defaultVariant !== p.from) { m.reasoning.defaultVariant = p.from; restored.push({ provider: p.provider, model: p.model, defaultVariant: p.from }); } } let bak = null; if (restored.length) { try { bak = saveConfig(doc); } catch (e) { fail(`热档位已回滚到 ${target.hotPrevious},但写模型配置失败(${e?.message ?? e})`, asJson); process.exit(1); } } if (asJson) { console.log(JSON.stringify({ stateFile: STATE_FILE, rolledBack: target.ts, hotLevel: target.hotPrevious, restored, backup: bak, }, null, 2)); } else { console.log("## 已回滚到上次切换之前"); console.log(""); console.log(`- 回滚目标:${target.ts} (${target.hotPrevious ?? "—"} → \`${target.hotLevel}\`)`); console.log(`- 全局热档位 → \`${target.hotPrevious ?? "(历史未记录,不动)"}\``); if (restored.length) { for (const r of restored) console.log(`- **${r.model}** (provider \`${r.provider}\`)持久默认 → \`${r.defaultVariant}\``); console.log(`备份:${bak}`); } else console.log("- 模型配置持久默认:与历史一致,未改写"); console.log(""); console.log(`当前会话要立刻生效,再敲一行内置命令:\`/effort ${target.hotPrevious}\`(热切,不重启)`); } process.exit(0); } // ---------------------------------------------------------------- CLI const argv = process.argv.slice(2); const asJson = argv.includes("--json"); const ACTIONS = ["status", "set", "variants", "defaults", "doctor", "rollback", "history"]; const action = argv.find((a) => ACTIONS.includes(a)) ?? "status"; function opt(name) { const i = argv.indexOf(name); return i >= 0 ? argv[i + 1] : undefined; } /** --model 优先;没给就用默认值文件/环境变量里的,再没有就是 null(全部) */ const defaults = readDefaults(); const modelOpt = opt("--model") ?? defaults.model ?? undefined; if (action === "doctor") { process.exit(await doctor(asJson)); } if (action === "history") { const lim = opt("--limit") ?? opt("--steps"); process.exit(historyReport(asJson, lim ? Number(lim) : 10)); } if (action === "rollback") { const steps = opt("--steps"); await doRollback(asJson, steps ? Number(steps) : 1); } if (action === "defaults") { // defaults / defaults set / defaults clear const sub = argv[argv.indexOf("defaults") + 1]; if (sub === "set") { const v = opt("--variant"); const m = opt("--model"); if (v === undefined && m === undefined) { fail("用法: node thinking.mjs defaults set [--variant ] [--model ] [--json]", asJson); process.exit(1); } const content = writeDefaults({ variant: v ?? undefined, model: m ?? undefined }); if (asJson) console.log(JSON.stringify({ file: DEFAULTS_FILE, content }, null, 2)); else { console.log("## 默认值已写入"); console.log(""); console.log(`- 文件:\`${DEFAULTS_FILE}\``); console.log(`- 内容:${JSON.stringify(content)}`); console.log(""); console.log("之后 `set` 不带档位就用这个默认档位;`status`/`variants` 不带 --model 就用这个默认模型。"); } process.exit(0); } if (sub === "clear") { try { fs.rmSync(DEFAULTS_FILE, { force: true }); } catch (e) { fail(`删除默认值文件失败(${e?.message ?? e})`, asJson); process.exit(1); } if (asJson) console.log(JSON.stringify({ file: DEFAULTS_FILE, removed: true }, null, 2)); else console.log(`## 默认值已清空\n\n- 已删除 \`${DEFAULTS_FILE}\`\n- 之后回到内置兜底(档位不预设、模型=全部)`); process.exit(0); } process.exit(defaultsReport(asJson)); } const hot = await readGlobalLevel(); if (action === "variants") { let doc; try { doc = loadConfig(); } catch (e) { fail(`读模型配置失败(${e?.message ?? e})`, asJson); process.exit(1); } const list = filterByModel(collectModels(doc).filter((m) => m.reasoning), modelOpt); if (asJson) { console.log(JSON.stringify({ v2config: V2_CONFIG, sessionDb: SESSION_DB, hotLevel: hot.level ?? null, models: list.map((m) => ({ provider: m.providerId, model: m.model, variants: m.reasoning.variants ?? [], defaultVariant: m.reasoning.defaultVariant ?? null })), }, null, 2)); } else if (!list.length) { console.log("没有带 reasoning 的模型(或 --model 名称不对)。"); } else { console.log(`全局热档位:${hot.level ?? "(读失败:" + (hot.error ?? hot.note) + ")"}`); console.log(""); console.log("## 可选思考档位"); console.log(""); for (const m of list) { console.log(`- **${m.model}** (provider \`${m.providerId}\`):可选 \`${(m.reasoning.variants ?? []).join(" / ") || "—"}\`,持久默认 \`${m.reasoning.defaultVariant ?? "—"}\``); } } process.exit(0); } if (action === "status") { let doc; try { doc = loadConfig(); } catch (e) { fail(`读模型配置失败(${e?.message ?? e})`, asJson); process.exit(1); } const list = filterByModel(collectModels(doc), modelOpt); if (asJson) { console.log(JSON.stringify({ v2config: V2_CONFIG, sessionDb: SESSION_DB, hotLevel: hot.level ?? null, hotError: hot.error ?? hot.note ?? null, models: list.map((m) => ({ provider: m.providerId, model: m.model, enabled: m.enabled, reasoning: m.reasoning ? { enabled: m.reasoning.enabled ?? null, variants: m.reasoning.variants ?? [], defaultVariant: m.reasoning.defaultVariant ?? null } : null, })), }, null, 2)); } else if (!list.length) { console.log("模型配置里没找到模型(或 --model 名称不对)。"); } else { console.log("## 思考模式现状"); console.log(""); if (hot.level) console.log(`全局热档位:\`${hot.level}\`(新会话直接按它;当前会话要立刻生效再敲 \`/effort ${hot.level}\`)`); else console.log(`全局热档位:读失败(${(hot.error ?? hot.note ?? "未知")})`); console.log(`模型配置:\`${V2_CONFIG}\``); console.log(""); for (const m of list) { if (!m.reasoning) console.log(`- **${m.model}** (provider \`${m.providerId}\`):无 reasoning 配置(不支持切换档位)`); else console.log(`- **${m.model}** (provider \`${m.providerId}\`):持久默认 \`${m.reasoning.defaultVariant ?? "—"}\`,可选 \`${(m.reasoning.variants ?? []).join(" / ") || "—"}\``); } console.log(""); console.log("切换示例:`/thinking:set high` 或 `/thinking:set low --model GLM-5.3-Flash`"); } process.exit(0); } // --- set --- const setIdx = argv.indexOf("set"); const variantArg = setIdx >= 0 ? argv[setIdx + 1] : undefined; // 不带档位时用默认值文件里的档位;两处都没有才报用法错(不再硬性要求手打) const variant = (variantArg && !variantArg.startsWith("--")) ? variantArg : defaults.variant; const variantFromDefault = !(variantArg && !variantArg.startsWith("--")); const hotOnly = argv.includes("--hot-only"); const persistOnly = argv.includes("--persist-only"); if (hotOnly && persistOnly) { fail("用法:--hot-only 与 --persist-only 不能同时用(只写热档位,或只写模型配置)", asJson); process.exit(1); } if (!variant) { fail("没给档位,默认值文件里也没设。用法: node thinking.mjs set [--model ] [--json] [--hot-only | --persist-only]\n" + " 或先定个默认:`node thinking.mjs defaults set --variant high`", asJson); process.exit(1); } let doc; try { doc = loadConfig(); } catch (e) { fail(`读模型配置失败(${e?.message ?? e})`, asJson); process.exit(1); } const targets = filterByModel(collectModels(doc).filter((m) => m.reasoning), modelOpt); if (!targets.length) { fail(modelOpt ? `没找到带 reasoning 的模型:${modelOpt}` : "模型配置里没有带 reasoning 的模型", asJson); process.exit(1); } // 校验收紧:① 档位必须在任一目标的 variants 并集里(防 ultra 之类非法档位两边都不写); // ② 之后仍做逐模型整批校验,有一个不支持就整批中止。 const unionVariants = [...new Set(targets.flatMap((t) => (t.reasoning.variants ?? []).map((v) => String(v).toLowerCase())))]; if (unionVariants.length && !unionVariants.includes(String(variant).toLowerCase())) { fail(`非法档位 ${variant}(全部目标的可选:${unionVariants.join("/")})——两边都没写`, asJson); process.exit(1); } for (const t of targets) { const vs = t.reasoning.variants ?? []; if (vs.length && !vs.map((v) => String(v).toLowerCase()).includes(variant.toLowerCase())) { fail(`模型 ${t.model} 不支持档位 ${variant}(可选:${vs.join("/")})——整批中止,两边都没写`, asJson); process.exit(1); } } // 规范大小写(跟模型自带的 variants 一致) const canonicalOf = (t) => (t.reasoning.variants ?? []).find((v) => String(v).toLowerCase() === variant.toLowerCase()) ?? variant; const canonical = canonicalOf(targets[0]); const persistPrevious = targets.map((t) => ({ provider: t.providerId, model: t.model, from: t.reasoning.defaultVariant ?? null, })); // 1) 热档位(默认写;--persist-only 时跳过) let hotPrev = null; if (!persistOnly) { try { ({ previous: hotPrev } = await writeGlobalLevel(canonical)); } catch (e) { fail(`写热档位库失败(${e?.message ?? e})——模型配置未动`, asJson); process.exit(1); } } else { try { hotPrev = (await readGlobalLevel()).level ?? null; } catch { hotPrev = null; } } // 2) 模型配置持久默认(默认写;--hot-only 时跳过) const changed = []; if (!hotOnly) { for (const t of targets) { const m = doc.provider[t.providerId].models[t.model]; const c = canonicalOf(t); if (m?.reasoning && m.reasoning.defaultVariant !== c) { m.reasoning.defaultVariant = c; changed.push({ provider: t.providerId, model: t.model, defaultVariant: c }); } } } let bak = null; if (changed.length) { try { bak = saveConfig(doc); } catch (e) { fail(`热档位已切到 ${canonical},但写模型配置失败(${e?.message ?? e})`, asJson, { hotLevel: canonical }); process.exit(1); } } // set 成功才记历史(含分写标记与持久旧值,供 rollback 用) appendState({ variant: canonical, model: modelOpt ?? null, hotOnly, persistOnly, hotPrevious: hotOnly ? hotPrev : hotPrev, hotLevel: canonical, persistPrevious, persistCurrent: canonical, backup: bak, }); const scopeLabel = hotOnly ? "仅热" : persistOnly ? "仅持久" : "热 + 持久"; if (asJson) { console.log(JSON.stringify({ v2config: V2_CONFIG, sessionDb: SESSION_DB, stateFile: STATE_FILE, backup: bak, hotPrevious: hotPrev, hotLevel: canonical, hotOnly, persistOnly, variantFromDefault, modelFromDefault: !opt("--model") && !!defaults.model, changed, }, null, 2)); } else { console.log(`## 思考模式已切换(${scopeLabel})`); console.log(""); if (variantFromDefault) console.log(`- 档位来自默认值(${defaults.source.variant === "env" ? "环境变量" : "默认值文件"}):\`${canonical}\``); if (!persistOnly) console.log(`- 全局热档位:${hotPrev ?? "—"} → \`${canonical}\`(新会话直接生效)`); else console.log(`- 全局热档位:本次仅持久,未动(当前 \`${hotPrev ?? "—"}\`)`); if (!opt("--model") && defaults.model) console.log(`- 作用模型来自默认值(未给 --model):\`${defaults.model}\``); if (hotOnly) console.log("- 模型配置持久默认:本次仅热,未改写"); else if (changed.length) { for (const c of changed) console.log(`- **${c.model}** (provider \`${c.provider}\`)持久默认 → \`${c.defaultVariant}\``); console.log(""); console.log(`备份:${bak}(回滚:node scripts/thinking.mjs rollback,或把它拷回 ${V2_CONFIG})`); } else { console.log("- 模型配置持久默认:本来就是目标档位,未改写"); } console.log(""); console.log(`历史已记:${STATE_FILE}`); if (!persistOnly) console.log(`当前这轮会话要立刻生效,再敲一行内置命令:\`/effort ${canonical}\`(热切,不重启)`); }