mslxdff 0.1.135 → 0.1.138

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.
Files changed (45) hide show
  1. package/package.json +2 -2
  2. package/src/chat/gateway.js +1 -1
  3. package/src/chat/prompt.js +1 -1
  4. package/src/chat/terminal.js +1 -1
  5. package/src/chat-pipeline/serial-trial.js +11 -3
  6. package/src/cli/commands/model/list-providers.js +1 -1
  7. package/src/cli/commands/model/list-render.js +2 -2
  8. package/src/cli/commands/provider/add.js +2 -1
  9. package/src/cli/commands/provider/bench-via.js +1 -1
  10. package/src/cli/commands/provider/cline-free.js +94 -0
  11. package/src/cli/commands/provider/cline-login.js +20 -15
  12. package/src/cli/commands/provider/index.js +22 -0
  13. package/src/cli/commands/provider/models.js +4 -1
  14. package/src/cli/commands/provider/workbuddy-login.js +1 -1
  15. package/src/cli/commands/use-group.js +8 -4
  16. package/src/cli/commands/workbuddy.js +14 -21
  17. package/src/cli/index.js +3 -0
  18. package/src/compat.js +23 -5
  19. package/src/model-capabilities/index.js +16 -3
  20. package/src/model-capabilities/merge.js +127 -0
  21. package/src/providers/classify.js +2 -2
  22. package/src/providers/cline/auth.js +1 -1
  23. package/src/providers/cline/chat.js +14 -11
  24. package/src/providers/cline/free-catalog.js +71 -0
  25. package/src/providers/cline/index.js +2 -16
  26. package/src/providers/cline/models.js +28 -51
  27. package/src/providers/dispatcher.js +1 -1
  28. package/src/providers/generic.js +47 -9
  29. package/src/providers/model-id.js +4 -4
  30. package/src/providers/registry.js +1 -1
  31. package/src/providers/share-keys.js +7 -2
  32. package/src/providers/workbuddy/account-store.js +73 -7
  33. package/src/providers/workbuddy/chat.js +1 -1
  34. package/src/providers/workbuddy/index.js +10 -20
  35. package/src/readline-compat.js +9 -10
  36. package/src/routes/models-route.js +11 -1
  37. package/src/runtime/providers-setup.js +9 -0
  38. package/src/runtime/server-lifecycle.js +2 -2
  39. package/src/state/migrations/cline-unify.js +94 -0
  40. package/src/state/migrations.js +33 -0
  41. package/src/state/schemas/token.js +6 -2
  42. package/src/state/schemas/use-group.js +47 -6
  43. package/src/sync-workbuddy.js +1 -1
  44. package/src/upstream-engine/index.js +1 -1
  45. package/src/upstream-probe/rotate.js +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mslxdff",
3
- "version": "0.1.135",
3
+ "version": "0.1.138",
4
4
  "description": "测试项目,请勿使用。",
5
5
  "type": "module",
6
6
  "bin": {
@@ -12,7 +12,7 @@
12
12
  "docs:check": "node scripts/docs-check.js"
13
13
  },
14
14
  "engines": {
15
- "node": ">=16"
15
+ "node": ">=18"
16
16
  },
17
17
  "files": [
18
18
  "bin/",
@@ -138,7 +138,7 @@ export function createGatewayClient({
138
138
  }
139
139
  if (provider === "opencode" && rawModel.includes("/")) {
140
140
  const maybe = rawModel.split("/")[0];
141
- if (["workbuddy", "clinebot", "sensenova", "openrouter", "generic"].includes(maybe)) provider = maybe;
141
+ if (["workbuddy", "cline", "sensenova", "openrouter", "generic"].includes(maybe)) provider = maybe;
142
142
  }
143
143
  } catch {}
144
144
  if (TRACE) {
@@ -58,7 +58,7 @@ ${mini}
58
58
  - 需要执行命令时调用 run_command,需要看文件时调用 read_file,需要检查网络/服务可用性时调用 curl。
59
59
  - curl 简写:upstream(=上游 https://opencode.ai/zen/v1/models)、local/health(=本机 /health)、local/models(=本机 /v1/models),也支持完整 http(s) URL;会自动补上游头、本机 token 与已配置供应商 key(直连 https://api.b.ai/v1/models 会自动带 bai 的 key,无需手动加头)。
60
60
  - 查“某供应商有哪些模型”**优先用 CLI 直查**:若上方“可用模型”已能回答,直接前缀过滤回答(如 workbuddy/ 即 workbuddy);需实时拉取时调用 run_command: "-provider <id> models"(如 -provider workbuddy models)或 "-model list --provider <id>",按 allowlist 过滤,--json 供脚本。**禁止**调 -provider <id> list(这是查配置,不是查模型!)。**错误示例**:workbuddy有哪些模型 → 调 -provider workbuddy list → 错。**正确**:-provider workbuddy models。禁止为此调用 -showtoken。
61
- - 严禁幻觉命令:mslxdff "hi" --model X / mslxdff --model X "hi" / mslxdff -chat --model X 都不存在,输出只会是 status 页。探活任意模型(含 clinebot/*、workbuddy/*、bai/*)必须用 curl POST http://localhost:8989/v1/chat/completions,body 为 {"model":"<前缀/模型>","messages":[{"role":"user","content":"hi"}],"stream":false},成功 200 + x-mslxdff-via:local 即通;401 代表本机 token 陈旧需提示 mslxdff -stop && mslxdff;403 + x-mslxdff-allowlist:1 代表白名单未放行需 allowlist add。
61
+ - 严禁幻觉命令:mslxdff "hi" --model X / mslxdff --model X "hi" / mslxdff -chat --model X 都不存在,输出只会是 status 页。探活任意模型(含 cline/*、workbuddy/*、bai/*)必须用 curl POST http://localhost:8989/v1/chat/completions,body 为 {"model":"<前缀/模型>","messages":[{"role":"user","content":"hi"}],"stream":false},成功 200 + x-mslxdff-via:local 即通;401 代表本机 token 陈旧需提示 mslxdff -stop && mslxdff;403 + x-mslxdff-allowlist:1 代表白名单未放行需 allowlist add。
62
62
  - **禁止重复调用(最高优先级)**:同一 run_command/curl/read_file 在本轮只执行一次,重复会被工具侧 SKIPPED_DUP 拦截;查询类(-showtoken/-status/-provider list/-providers list/-model list/-group list/-log 等)**调用一次即答案**,拿到 OK 结果后必须**立即用中文直接回答用户**,禁止再发起任何工具调用。收到 SKIPPED_DUP 或“请直接回答/禁止再调用”提示时,必须 0 工具直接回答。
63
63
  - 禁止调用 -uninstall,包含即拒绝;-showtoken 仅在用户明确要求查看/调试本机 token 时才用,查模型/查供应商严禁调用。
64
64
  - 回复风格:简洁友好,执行前后用中文说明你在做什么。
@@ -96,7 +96,7 @@ export function handleSlash(line, ctx) {
96
96
  const id = arg;
97
97
  if (id.includes("/")) {
98
98
  console.log(`\x1b[31m[拒绝] ${id} 带供应商前缀 — -chat 只支持 opencode 上游模型(裸 id,直连 opencode.ai 免费池)\x1b[0m`);
99
- console.log(`\x1b[90m其他供应商(deepseek/ workbuddy/ clinebot/ 等)请走网关:mslxdff -model set <id> 或 curl 本机 /v1/chat/completions\x1b[0m`);
99
+ console.log(`\x1b[90m其他供应商(deepseek/ workbuddy/ cline/ 等)请走网关:mslxdff -model set <id> 或 curl 本机 /v1/chat/completions\x1b[0m`);
100
100
  return { handled: true };
101
101
  }
102
102
  if (!isFreeModel(id) || !freePoolNow().includes(id)) {
@@ -9,7 +9,13 @@ import { handlePeerRelay } from "../routes/chat/peer-handler.js";
9
9
  import { handleBroadbandRelay } from "../routes/chat/broadband-handler.js";
10
10
  import { handleViaRoute } from "../routes/chat/via-route-handler.js";
11
11
  import { handleExhaustedLocal, handleExhaustedAll } from "../routes/chat/exhausted-handler.js";
12
- import { shouldUseGroupForModel, isHardLocalOnly } from "../state/schemas/use-group.js";
12
+ import { shouldUseGroupForModel, isHardLocalOnly, isKeyProviderDirectOnly } from "../state/schemas/use-group.js";
13
+
14
+ function groupSkipReason(model) {
15
+ if (isHardLocalOnly(model)) return "provider local-only(禁组员,仅本机直连)";
16
+ if (isKeyProviderDirectOnly(model)) return "key provider default direct(仅本机直连,MSLXDFF_USE_GROUP_KEYS=1 可开组员)";
17
+ return "useGroup=off";
18
+ }
13
19
 
14
20
  /**
15
21
  * 串行 trial — 从 engine.js 抽出的第二段:via-route 单路径 → 串行 trial →
@@ -43,6 +49,8 @@ export async function runSerialTrial(ctx, deps = {}) {
43
49
  } catch (e) {
44
50
  evt("via-route-exception", { reqId, model: requested, error: errMsg(e) });
45
51
  }
52
+ } else if (!useAuto && requested && requested.includes("/") && canForwardPeers && !lockModel && peers) {
53
+ evt("group-skip", { reqId, model: requested, reason: `${groupSkipReason(requested)} (via-route)` });
46
54
  }
47
55
 
48
56
  let lastErr = viaRouteLastErr;
@@ -143,7 +151,7 @@ export async function runSerialTrial(ctx, deps = {}) {
143
151
  }
144
152
  if (canForwardPeers) {
145
153
  if (!shouldUseGroupForModel(model)) {
146
- evt("group-skip", { reqId, model, reason: isHardLocalOnly(model) ? "provider local-only(禁组员,仅本机直连)" : "useGroup=off (peer)" });
154
+ evt("group-skip", { reqId, model, reason: `${groupSkipReason(model)} (peer)` });
147
155
  } else {
148
156
  const pr = await peerRelay({ model, body, lastErr, requested, useAuto, lockModel, auto, peers, handlerCtx, evt, logCall, mark, perf0, stages, startedAt, plugins, res });
149
157
  if (pr.handled) return { done: true };
@@ -152,7 +160,7 @@ export async function runSerialTrial(ctx, deps = {}) {
152
160
  }
153
161
  if (groups) {
154
162
  if (!shouldUseGroupForModel(model)) {
155
- evt("group-skip", { reqId, model, reason: isHardLocalOnly(model) ? "provider local-only(禁组员,仅本机直连)" : "useGroup=off (broadband)" });
163
+ evt("group-skip", { reqId, model, reason: `${groupSkipReason(model)} (broadband)` });
156
164
  } else {
157
165
  const br = await broadbandRelay({ model, body, hops, lastErr, requested, useAuto, lockModel, auto, groups, token, bus, logs, handlerCtx, evt, mark, perf0, stages, res, startedAt, plugins });
158
166
  if (br.handled) return { done: true };
@@ -8,7 +8,7 @@ export async function renderOtherProviders({ pickedIds, ids, fullAliases }) {
8
8
  try { _la2(); } catch {}
9
9
  const configs = loadProviderConfigs();
10
10
  const otherIds = Object.keys(configs).filter((k) => String(k).toLowerCase() !== "opencode");
11
- const order2 = ["workbuddy", "clinebot", "openrouter", "bai"];
11
+ const order2 = ["workbuddy", "cline", "openrouter", "bai"];
12
12
  otherIds.sort((a, b) => {
13
13
  const ia = order2.indexOf(a), ib = order2.indexOf(b);
14
14
  if (ia !== -1 || ib !== -1) {
@@ -57,7 +57,7 @@ export function renderFreeList({ ids, at, pickedIds, sortedProvs, groups, aliasM
57
57
  }
58
58
  }
59
59
 
60
- /** 按供应商分组 + 排序(opencode/workbuddy/clinebot/openrouter 优先) */
60
+ /** 按供应商分组 + 排序(opencode/workbuddy/cline/openrouter 优先) */
61
61
  export function groupByProvider(ids) {
62
62
  const groups = {};
63
63
  for (const id of ids) {
@@ -65,7 +65,7 @@ export function groupByProvider(ids) {
65
65
  if (!groups[prov]) groups[prov] = [];
66
66
  groups[prov].push(id);
67
67
  }
68
- const order = ["opencode", "workbuddy", "clinebot", "openrouter"];
68
+ const order = ["opencode", "workbuddy", "cline", "openrouter"];
69
69
  const sortedProvs = Object.keys(groups).sort((a, b) => {
70
70
  const ia = order.indexOf(a), ib = order.indexOf(b);
71
71
  if (ia !== -1 || ib !== -1) {
@@ -69,7 +69,8 @@ export async function handleProviderAdd(id, sub, rest) {
69
69
  try {
70
70
  const { writeFileSync, mkdirSync } = await import("node:fs");
71
71
  const { join } = await import("node:path");
72
- const dir = process.env.WORKBUDDY_AUTH_DIR || join(process.cwd(), "auths");
72
+ const { resolveAuthDir } = await import("../../providers/workbuddy/account-store.js");
73
+ const dir = resolveAuthDir();
73
74
  mkdirSync(dir, { recursive: true });
74
75
  const fp = join(dir, `workbuddy-${uid}.json`);
75
76
  const doc = { account: { uid, enterpriseId: "", nickname: "" }, auth: { accessToken: token, refreshToken: "", expiresAt: Math.floor(Date.now()/1000)+3600, domain: "www.codebuddy.cn" } };
@@ -120,7 +120,7 @@ export async function handleVia({ providerId, opts, fetchImpl, loadConfigs, load
120
120
  for (const k of Object.keys(raw.providerConfigs || {})) if (!targetIds.includes(k) && (loadAllowed(k) || []).length) targetIds.push(k);
121
121
  for (const k of Object.keys(raw.providerKeys || {})) if (!targetIds.includes(k) && (loadAllowed(k) || []).length) targetIds.push(k);
122
122
  } catch {}
123
- if (!targetIds.length) targetIds = ["openrouter", "workbuddy", "clinebot"].filter((p) => (loadAllowed(p) || []).length);
123
+ if (!targetIds.length) targetIds = ["openrouter", "workbuddy", "cline"].filter((p) => (loadAllowed(p) || []).length);
124
124
  // opencode 默认 allowAny 但 allowlist 常空:有裸 picks 且确认包含 opencode 时纳入
125
125
  if (includeOpencode && !targetIds.includes("opencode") && picks.some((p) => !String(p).includes("/") || String(p).toLowerCase().startsWith("opencode/"))) {
126
126
  targetIds.push("opencode");
@@ -0,0 +1,94 @@
1
+ /**
2
+ * mslxdff -provider cline free [sync] [--yes] [--json] [--keep-extra]
3
+ * 只读:直查上游免费目录(公开接口,无凭据)并与当前 allowlist 求差;只有 `free sync --yes` 才写 allowlist。
4
+ * allowlist 存裸 id(不带 cline/ 前缀),与 normalizeAllowedModel 语义一致;落盘走 saveProviderAllowedModels 保留 keys/baseUrl。
5
+ */
6
+ import { normalizeProviderId } from "../../../providers/model-id.js";
7
+ import { fetchFreeCatalog, FALLBACK_FREE, FREE_CATALOG_PATH } from "../../../providers/cline/free-catalog.js";
8
+
9
+ /** 上游不可达时退回内置兜底目录(标注 source=fallback,供人判断是否该落盘) */
10
+ async function loadCatalog() {
11
+ const cat = await fetchFreeCatalog({});
12
+ if (cat.ok) return { ...cat, source: "upstream" };
13
+ return {
14
+ ...cat,
15
+ source: "fallback",
16
+ models: FALLBACK_FREE.map((m) => ({ ...m })),
17
+ ids: FALLBACK_FREE.map((m) => m.id),
18
+ };
19
+ }
20
+
21
+ function computeNext(current, freeIds, keepExtra) {
22
+ const next = keepExtra ? [...new Set([...current, ...freeIds])] : [...freeIds];
23
+ return {
24
+ next,
25
+ added: next.filter((m) => !current.includes(m)),
26
+ removed: current.filter((m) => !next.includes(m)),
27
+ kept: next.filter((m) => current.includes(m)),
28
+ };
29
+ }
30
+
31
+ function renderList(cat, current, next, plan) {
32
+ console.log(`cline free 目录(GET ${FREE_CATALOG_PATH} → free):${cat.ids.length} 个${cat.source === "fallback" ? "(内置兜底,上游不可达)" : ""}`);
33
+ const w = Math.max(20, ...cat.ids.map((s) => s.length)) + 2;
34
+ for (const m of cat.models) console.log(` + ${String(m.id).padEnd(w)}${m.name && m.name !== m.id ? m.name : ""}`);
35
+ console.log(`当前 allowlist:${current.length} 个`);
36
+ for (const id of current) console.log(` ${next.includes(id) ? "✓" : "-"} ${id}${next.includes(id) ? "" : " (不在 free 目录)"}`);
37
+ console.log(`结果:allowlist ${current.length} → ${next.length}(+${plan.added.length} 新增 / -${plan.removed.length} 移除 / ${plan.kept.length} 保留)`);
38
+ }
39
+
40
+ export async function handleClineFree(id, sub, rest = [], args = []) {
41
+ if (normalizeProviderId(id) !== "cline") return false;
42
+ if (String(sub || "").toLowerCase() !== "free") return false;
43
+ const isSync = String(rest[1] || "").toLowerCase() === "sync";
44
+ const wantsJson = args.includes("--json") || args.includes("-json");
45
+ const yes = args.includes("--yes") || args.includes("-y");
46
+ const keepExtra = args.includes("--keep-extra");
47
+
48
+ const { loadProviderAllowedModels, saveProviderAllowedModels } = await import("../../../state.js");
49
+ const cat = await loadCatalog();
50
+ const current = loadProviderAllowedModels("cline");
51
+ const plan = computeNext(current, cat.ids, keepExtra);
52
+ const dryRun = !(isSync && yes);
53
+ let written = [];
54
+ if (isSync && !dryRun) {
55
+ try {
56
+ written = saveProviderAllowedModels("cline", plan.next);
57
+ } catch (e) {
58
+ console.error(`❌ 写入 allowlist 失败: ${String(e?.message || e)}`);
59
+ process.exit(1);
60
+ }
61
+ }
62
+ const warning = cat.source === "fallback" ? `上游目录不可达(${cat.error || cat.status}),使用内置兜底目录,可能过期` : null;
63
+
64
+ if (wantsJson) {
65
+ console.log(JSON.stringify({
66
+ provider: "cline",
67
+ mode: isSync ? "sync" : "list",
68
+ source: cat.source,
69
+ url: cat.url,
70
+ free: cat.ids,
71
+ current,
72
+ next: plan.next,
73
+ added: plan.added,
74
+ removed: plan.removed,
75
+ kept: plan.kept,
76
+ dryRun,
77
+ written,
78
+ ...(warning ? { warning } : {}),
79
+ }, null, 2));
80
+ process.exit(0);
81
+ }
82
+
83
+ if (warning) console.error(`⚠️ ${warning}`);
84
+ renderList(cat, current, plan.next, plan);
85
+ if (!isSync) {
86
+ console.log(`\n预览(只读)。一键同步:mslxdff -provider cline free sync --yes · 只增不删:加 --keep-extra`);
87
+ } else if (dryRun) {
88
+ console.log(`\n预览模式,未写入。执行:mslxdff -provider cline free sync --yes${keepExtra ? " --keep-extra" : ""}`);
89
+ } else {
90
+ console.log(`\n✅ 已写入 allowlist:${written.length} 个(providerConfigs.cline.allowedModels)`);
91
+ console.log(` 生效:mslxdff -restart(daemon 的 /v1/models 聚合有 10 分钟缓存,重启即刷新)`);
92
+ }
93
+ process.exit(0);
94
+ }
@@ -1,7 +1,8 @@
1
1
  import { compatFetch, getUndici, timeoutSignal } from "../../../compat.js";
2
+ import { normalizeProviderId } from "../../../providers/model-id.js";
2
3
  export async function handleClineLogin(id, sub) {
3
4
  if (sub !== "login" && sub !== "auth" && sub !== "oauth") return false;
4
- if (id !== "cline" && id !== "clinebot" && id !== "cline-bot") return false;
5
+ if (normalizeProviderId(id) !== "cline") return false;
5
6
 
6
7
  const CLIENT_ID = "client_01K3A541FN8TA3EPPHTD2325AR";
7
8
  const WORKOS_DEVICE = "https://api.workos.com/user_management/authorize/device";
@@ -97,7 +98,7 @@ export async function handleClineLogin(id, sub) {
97
98
  }
98
99
  }
99
100
  if (!workos) {
100
- console.error("\n❌ 授权超时,请重新运行 mslxdff -provider clinebot login");
101
+ console.error("\n❌ 授权超时,请重新运行 mslxdff -provider cline login");
101
102
  process.exit(1);
102
103
  }
103
104
  console.log("\n✅ WorkOS 授权成功!\n🔗 用 WorkOS token 在 Cline 注册...");
@@ -117,28 +118,32 @@ export async function handleClineLogin(id, sub) {
117
118
  console.log("=".repeat(60));
118
119
  console.log(`✅ 登录成功! 账号: ${email}`);
119
120
  console.log(`🔑 refreshToken: ${rt.slice(0, 8)}…${rt.slice(-8)} (${rt.length} 字符)`);
120
- // 落盘到 state(同时写 cline 与 clinebot 两个 id,兼容)
121
- const { loadProviderKeys, saveProviderConfig, loadProviderConfig } = await import("../../../state.js");
122
- for (const pid of ["cline", "clinebot"]) {
123
- try {
124
- const cur = loadProviderKeys(pid);
125
- if (cur.includes(rt)) {
126
- console.log(` ℹ️ ${pid} 已存在相同 token,跳过`);
127
- continue;
128
- }
121
+ // 落盘到 state(供应商 id 恒为 cline;写盘前先跑一次性迁移,保证老用户不会再写出 clinebot)
122
+ const { defaultStateFile, loadProviderKeys, saveProviderConfig, loadProviderConfig } = await import("../../../state.js");
123
+ try {
124
+ const { runStateMigrations } = await import("../../../state/migrations.js");
125
+ const mig = await runStateMigrations({ file: defaultStateFile() });
126
+ if (mig.applied.length) console.log(` ℹ️ 已合并遗留供应商配置: ${mig.applied.join(", ")}`);
127
+ } catch {}
128
+ try {
129
+ const pid = "cline";
130
+ const cur = loadProviderKeys(pid);
131
+ if (cur.includes(rt)) {
132
+ console.log(` ℹ️ ${pid} 已存在相同 token,跳过`);
133
+ } else {
129
134
  const cfg = loadProviderConfig(pid) || { baseUrl: "", keys: [] };
130
135
  const nextKeys = [...new Set([...(cfg.keys || cur), rt].filter(Boolean))];
131
136
  saveProviderConfig(pid, { baseUrl: cfg.baseUrl || "https://api.cline.bot", keys: nextKeys });
132
137
  console.log(` ✅ 已写入 ${pid}(现 ${nextKeys.length} 个账号)`);
133
- } catch (e) {
134
- console.log(` ⚠️ 写入 ${pid} 失败: ${e.message}`);
135
138
  }
139
+ } catch (e) {
140
+ console.log(` ⚠️ 写入 cline 失败: ${e.message}`);
136
141
  }
137
142
  console.log("=".repeat(60));
138
143
  console.log("\n下一步:");
139
144
  console.log(" mslxdff -restart 重启网关使新账号生效");
140
- console.log(" mslxdff -provider clinebot bench --json 测速 deepseek 是否 200");
145
+ console.log(" mslxdff -provider cline bench --json 测速 deepseek 是否 200");
141
146
  console.log(" mslxdff -chat 直接对话,模型选 deepseek/deepseek-v4-flash");
142
- console.log("\n多账号:重复 `mslxdff -provider clinebot login` 追加,二号自动做后备");
147
+ console.log("\n多账号:重复 `mslxdff -provider cline login` 追加,二号自动做后备");
143
148
  process.exit(0);
144
149
  }
@@ -1,4 +1,5 @@
1
1
  import { readFileSync } from "node:fs";
2
+ import { normalizeProviderId } from "../../../providers/model-id.js";
2
3
  import { defaultStateFile } from "../../../state.js";
3
4
 
4
5
  export async function handleProviders(args) {
@@ -74,6 +75,27 @@ export async function handleProvider(args) {
74
75
  if (await handleProviderModels(id, sub, args, rest)) return true;
75
76
  const { handleProviderBench } = await import("./bench.js");
76
77
  if (await handleProviderBench(id, sub, rest, args)) return true;
78
+ const { handleClineFree } = await import("./cline-free.js");
79
+ if (await handleClineFree(id, sub, rest, args)) return true;
80
+ if (normalizeProviderId(id) === "cline" && sub === "migrate") {
81
+ const { runStateMigrations } = await import("../../../state/migrations.js");
82
+ const dryRun = args.includes("--dry-run") || args.includes("--dryrun");
83
+ const r = await runStateMigrations({ dryRun });
84
+ const d = r.details["cline-unify"] || {};
85
+ if (r.errors.length) console.error(`❌ 迁移失败: ${r.errors.map((e) => `${e.id}: ${e.error}`).join("; ")}`);
86
+ if (dryRun) {
87
+ console.log(d.changed
88
+ ? `cline migrate 干跑:将合并遗留 cline id 配置(keys ${d.before.keys} → ${d.after.keys} / allowlist ${d.before.allowedModels} → ${d.after.allowedModels}),未写入`
89
+ : "cline migrate 干跑:无需迁移(未发现 providerConfigs.clinebot / cline-bot)");
90
+ } else if (d.applied) {
91
+ console.log(`✅ cline migrate 完成:keys ${d.before.keys} → ${d.after.keys} / allowlist ${d.before.allowedModels} → ${d.after.allowedModels}`);
92
+ if (d.backup) console.log(` 备份:${d.backup}`);
93
+ console.log(" 生效:mslxdff -restart");
94
+ } else {
95
+ console.log("cline migrate:无需迁移(未发现 providerConfigs.clinebot / cline-bot)");
96
+ }
97
+ process.exit(r.errors.length ? 1 : 0);
98
+ }
77
99
  const { handleProviderKeys } = await import("./keys.js");
78
100
  await handleProviderKeys(id, sub, rest, args);
79
101
  return true;
@@ -1,9 +1,12 @@
1
1
  import { join } from "node:path";
2
2
  import { defaultStateFile } from "../../../state.js";
3
3
  import { logDir } from "../../../logs.js";
4
+ import { normalizeProviderId } from "../../../providers/model-id.js";
4
5
 
5
6
  export async function handleProviderModels(id, sub, args, rest) {
6
7
  if (!(sub === "models" || sub === "show-models" || sub === "list-models" || sub === "ls")) return false;
8
+ // 供应商 id 归一(clinebot/cline-bot → cline):老脚本 `-provider clinebot models` 仍走 cline 直查
9
+ id = normalizeProviderId(id);
7
10
  const { loadProviderConfig, loadProviderKeys, isModelAllowed } = await import("../../../state.js");
8
11
  const wantsJson = args.includes("--json") || args.includes("-json");
9
12
  const cfg = loadProviderConfig(id);
@@ -34,7 +37,7 @@ export async function handleProviderModels(id, sub, args, rest) {
34
37
  let provider;
35
38
  if (id === "workbuddy") {
36
39
  provider = createWorkbuddyProvider({ baseUrl, apiKeys: keys, auths, file: defaultStateFile() });
37
- } else if (id === "cline" || id === "clinebot") {
40
+ } else if (id === "cline") {
38
41
  // 与聚合目录同源:recommended-models 的 free(generic 的 /api/v1/models 是全量目录,含大量不可用条目)
39
42
  const { createClineProvider } = await import("../../../providers/cline.js");
40
43
  provider = createClineProvider({ id, baseUrl: baseUrl || undefined, apiKeys: keys, file: defaultStateFile() });
@@ -1,5 +1,5 @@
1
1
  import { compatFetch, timeoutSignal } from "../../../compat.js";
2
- // mslxdff -provider workbuddy login — WorkBuddy 官方设备授权流(对标 clinebot login)。
2
+ // mslxdff -provider workbuddy login — WorkBuddy 官方设备授权流(对标 cline login)。
3
3
  // 学自 Sliverkiss/workbuddy2api(cmd/login):POST /v2/plugin/auth/state 拿 state+authUrl →
4
4
  // 浏览器登录 → GET /v2/plugin/auth/token?state= 轮询 → GET /v2/plugin/login/account?state= 拿 uid。
5
5
  // 不走 whistle/MITM,不依赖 codebuddy CLI,自然支持多账号追加。
@@ -1,4 +1,4 @@
1
- import { loadUseGroup, saveUseGroup, getEffectiveUseGroup, getUseGroupEnv } from "../../state/schemas/use-group.js";
1
+ import { loadUseGroup, saveUseGroup, getEffectiveUseGroup, getUseGroupEnv, getUseGroupKeysEnv } from "../../state/schemas/use-group.js";
2
2
  import { argValue } from "../policy.js";
3
3
 
4
4
  function parseInput(v) {
@@ -26,15 +26,18 @@ export async function handleUseGroup(args) {
26
26
  const envVal = getUseGroupEnv();
27
27
  const effective = getEffectiveUseGroup();
28
28
  const stored = loadUseGroup();
29
+ const keysEnv = getUseGroupKeysEnv();
29
30
 
30
31
  if (raw === null || raw === undefined || raw === "") {
31
32
  // 查询模式
32
- console.log(`use-group: ${effective ? "on" : "off"} (effective)`);
33
+ console.log(`use-group: ${effective ? "on" : "off"} (effective, 仅 opencode 免费池)`);
33
34
  console.log(` stored: ${stored ? "on" : "off"} (state.json useGroup)`);
34
35
  if (envVal !== null) console.log(` env MSLXDFF_USE_GROUP=${envVal ? "on" : "off"} (overrides stored)`);
36
+ console.log(` keys: ${keysEnv ? "on" : "off"} (env MSLXDFF_USE_GROUP_KEYS, 默认 off: 带 key 上游恒直连)`);
35
37
  console.log(` default: on`);
36
- console.log(` usage: mslxdff -use-group on|off (本机失败时是否走组员网络,默认 on)`);
38
+ console.log(` usage: mslxdff -use-group on|off (opencode 失败时是否走组员网络,默认 on)`);
37
39
  console.log(` env: MSLXDFF_USE_GROUP=0|1 (优先级高于 state)`);
40
+ console.log(` env: MSLXDFF_USE_GROUP_KEYS=1 (把 key 供应商开回组员,cline/workbuddy 仍硬禁)`);
38
41
  process.exit(0);
39
42
  }
40
43
 
@@ -50,7 +53,8 @@ export async function handleUseGroup(args) {
50
53
 
51
54
  saveUseGroup(parsed);
52
55
  console.log(`use-group set to ${parsed ? "on" : "off"} (stored in state.json)`);
53
- console.log(` ${parsed ? "允许" : "不再允许"}走组员网络(via-route/hedge/peer/broadband,全部供应商)`);
56
+ console.log(` ${parsed ? "允许 opencode" : "opencode 也不再"}走组员网络(via-route/hedge/peer/broadband)`);
57
+ console.log(` 带 key 上游默认恒直连(不受本开关影响,MSLXDFF_USE_GROUP_KEYS=1 可开回,cline/workbuddy 仍硬禁)`);
54
58
  if (!parsed) console.log(` 提示:所有请求将仅在本机重试,不再走组员中继`);
55
59
  process.exit(0);
56
60
  }
@@ -126,12 +126,15 @@ export async function handleWorkbuddy(args) {
126
126
  auths.splice(rmIdx,1); keys.splice(rmIdx,1);
127
127
  saveProviderConfig("workbuddy", { baseUrl: cfg.baseUrl||"https://copilot.tencent.com", keys, auths });
128
128
  if (!keep) {
129
+ // 旧实现只删 cwd/auths 那一份,留下的旧副本会被读取兜底"复活"账号 → 扫所有候选目录
129
130
  try {
130
131
  const { existsSync, unlinkSync } = await import("node:fs");
131
132
  const { join } = await import("node:path");
132
- const dir = process.env.WORKBUDDY_AUTH_DIR || join(process.cwd(), "auths");
133
- const fp = join(dir, `workbuddy-${uid}.json`);
134
- if (existsSync(fp)) { unlinkSync(fp); console.log(`removed file ${fp}`); }
133
+ const { resolveAuthDirs } = await import("../../providers/workbuddy/account-store.js");
134
+ for (const dir of resolveAuthDirs()) {
135
+ const fp = join(dir, `workbuddy-${uid}.json`);
136
+ if (existsSync(fp)) { unlinkSync(fp); console.log(`removed file ${fp}`); }
137
+ }
135
138
  } catch {}
136
139
  }
137
140
  try { const { getBalanceCache } = await import("../../providers/workbuddy-balance.js"); getBalanceCache().delete(uid); } catch {}
@@ -155,24 +158,14 @@ async function loadWorkbuddyAccounts() {
155
158
  })).filter((it) => it.uid && it.key);
156
159
  if (items.length) return items;
157
160
  try {
158
- const { readdirSync, readFileSync, existsSync } = await import("node:fs");
159
- const { join } = await import("node:path");
160
- const { resolveAuthDir } = await import("../../providers/workbuddy/account-store.js");
161
- const dir = resolveAuthDir();
162
- if (existsSync(dir)) {
163
- for (const f of readdirSync(dir).filter((x) => x.startsWith("workbuddy-") && x.endsWith(".json"))) {
164
- try {
165
- const j = JSON.parse(readFileSync(join(dir, f), "utf8"));
166
- if (j?.account?.uid && j?.auth?.accessToken) {
167
- const domain = j.auth.domain || "www.codebuddy.cn";
168
- items.push({
169
- uid: j.account.uid, domain, enterpriseId: j.account.enterpriseId || "",
170
- key: j.auth.accessToken,
171
- auth: { uid: j.account.uid, domain, enterpriseId: j.account.enterpriseId || "", refreshToken: j.auth.refreshToken || "" },
172
- });
173
- }
174
- } catch {}
175
- }
161
+ const { listAccountDocs } = await import("../../providers/workbuddy/account-store.js");
162
+ for (const { uid, doc } of listAccountDocs()) {
163
+ const domain = doc.auth.domain || "www.codebuddy.cn";
164
+ items.push({
165
+ uid, domain, enterpriseId: doc.account.enterpriseId || "",
166
+ key: doc.auth.accessToken,
167
+ auth: { uid, domain, enterpriseId: doc.account.enterpriseId || "", refreshToken: doc.auth.refreshToken || "" },
168
+ });
176
169
  }
177
170
  } catch {}
178
171
  return items;
package/src/cli/index.js CHANGED
@@ -1,12 +1,15 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import { join, dirname } from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
+ import { assertMinNode } from "../compat.js";
4
5
 
5
6
  const __dirname = dirname(fileURLToPath(import.meta.url));
6
7
  const VERSION = JSON.parse(readFileSync(join(__dirname, "..", "..", "package.json"), "utf8")).version;
7
8
 
8
9
  // deep CLI entry — single inlet: run(args) — hides all command branching inside
9
10
  export async function run(args = process.argv.slice(2)) {
11
+ // 运行环境门(强制):项目要求 Node >=18(ADR-0024),不满足则人话退出
12
+ if (!assertMinNode()) process.exit(1);
10
13
  const { handleHelp, handleUpdate, handleRefreshToken, handleShowToken, handleUninstall, handleLog, handlePlugins, handleChat, handleStatus, handleFree, handleAutostart } = await import("./commands/system.js");
11
14
  if (await handleHelp(args, VERSION)) return;
12
15
  if (await handleUpdate(args, VERSION)) return;
package/src/compat.js CHANGED
@@ -1,8 +1,8 @@
1
- // 老 Node 兼容层(engines >=16):Node 16 缺 globalThis.fetch(18+)、
2
- // AbortSignal.timeout(17.3+)、structuredClone(17+);裸全局 crypto 是
3
- // WebCrypto,其 randomUUID 要 19+,只有 node:crypto 的 14.17+ 可用。
4
- // undici 8 需 Node 22+,依赖已锁 5.x(老 Node 可跑,API 兼容我们用到的面)。
5
- // 统一从这里取,源码里勿直接用这些全局。
1
+ // 运行时兼容层(项目要求 Node >=18,见 ADR-0024;源码里勿直接用裸 fetch/globalThis.fetch)。
2
+ // 仍保留本层的原因:① undici 是显式依赖(Agent 连接池/keep-alive 要用),
3
+ // 各家 fetch 实现统一从这里取,避免 globalThis.fetch 与 undici.fetch 混用;
4
+ // ② AbortSignal.timeout/structuredClone 在 18+ 已原生,这里的兜底只为可读报错,
5
+ // 不再承诺 <18 可用(<18 请走 assertMinNode 的人话提示)。
6
6
  import { randomUUID } from "node:crypto";
7
7
 
8
8
  let _undici = null;
@@ -33,3 +33,21 @@ export function clone(v) {
33
33
  }
34
34
 
35
35
  export const uuid = () => randomUUID();
36
+
37
+ // ---- 运行环境门(强制):项目要求 Node >=18(ADR-0024)----
38
+ // Node <18 缺 Response/Headers/ReadableStream/TransformStream 等 Web 全局,
39
+ // 上游适配层有 29 处裸用,跑起来必 ReferenceError;与其半途炸掉,不如入口给人话。
40
+ export const MIN_NODE_MAJOR = 18;
41
+
42
+ export function nodeMajor() {
43
+ return Number(String(process.versions?.node || "0").split(".")[0]) || 0;
44
+ }
45
+
46
+ export function assertMinNode({ min = MIN_NODE_MAJOR, label = "mslxdff" } = {}) {
47
+ const major = nodeMajor();
48
+ if (major >= min) return true;
49
+ console.error(`[运行环境不满足] ${label} 需要 Node ${min}+,当前 v${process.versions.node}。`);
50
+ console.error("原因:Node <18 没有 Response/Headers/ReadableStream 等全局对象,上游转发必然失败。");
51
+ console.error("升级方式:nvm install 20 && nvm use 20,或到 https://nodejs.org/ 装 LTS(推荐 20/22)。");
52
+ return false;
53
+ }
@@ -117,8 +117,22 @@ export function createCapabilitiesService({
117
117
  function providers() {
118
118
  return [...capsIndex.keys()].sort();
119
119
  }
120
-
121
- return { ready, get, list, providers, npmIndex: () => new Map(npmIndex) };
120
+ // readyWarm:仅用内存/磁盘缓存热身,绝不网络请求。/models 富化用它避免冷启动阻塞;
121
+ // 冷缓存时后台拉新(不 await),下次请求即富化。
122
+ async function readyWarm() {
123
+ const t = now();
124
+ if (raw && t - loadedAt < ttl) return true;
125
+ const disk = readCache();
126
+ if (disk) {
127
+ raw = disk;
128
+ capsIndex = buildIndex(disk);
129
+ loadedAt = t;
130
+ return true;
131
+ }
132
+ ready().catch(() => {}); // 后台拉新,失败静默(/models 原样降级)
133
+ return false;
134
+ }
135
+ return { ready, readyWarm, get, list, providers, npmIndex: () => new Map(npmIndex) };
122
136
  }
123
137
 
124
138
  // 模块级单例:HTTP handler 懒加载,测试 _reset 后注入
@@ -128,7 +142,6 @@ export function globalCapabilities() {
128
142
  return _global;
129
143
  }
130
144
  export function _resetGlobalCapabilities() { _global = null; }
131
-
132
145
  // 缓存落盘位置:MSLXDFF_MODELS_DEV_CACHE 覆盖 > ~/.config/mslxdff/models-dev.json(与 state 同目录)
133
146
  function defaultCacheFile() {
134
147
  const override = process.env.MSLXDFF_MODELS_DEV_CACHE;