mslxdff 0.1.136 → 0.1.140

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 (95) 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/chooser.js +31 -12
  6. package/src/cli/commands/model/list-live.js +22 -0
  7. package/src/cli/commands/model/list-providers.js +1 -1
  8. package/src/cli/commands/model/list-render.js +2 -2
  9. package/src/cli/commands/model/list.js +2 -0
  10. package/src/cli/commands/model/live-models.js +24 -0
  11. package/src/cli/commands/provider/add.js +2 -1
  12. package/src/cli/commands/provider/bench-via.js +1 -1
  13. package/src/cli/commands/provider/cline-free.js +94 -0
  14. package/src/cli/commands/provider/cline-login.js +20 -15
  15. package/src/cli/commands/provider/codearts-login.js +45 -0
  16. package/src/cli/commands/provider/index.js +34 -0
  17. package/src/cli/commands/provider/keys.js +13 -2
  18. package/src/cli/commands/provider/models.js +13 -1
  19. package/src/cli/commands/provider/qoder-checkin.js +81 -0
  20. package/src/cli/commands/provider/qoder-login.js +103 -0
  21. package/src/cli/commands/provider/traework-login.js +202 -0
  22. package/src/cli/commands/provider/workbuddy-login.js +1 -1
  23. package/src/cli/commands/workbuddy.js +14 -21
  24. package/src/cli/index.js +3 -0
  25. package/src/cli/interactive.js +41 -4
  26. package/src/cli/provider-row.js +1 -1
  27. package/src/compat.js +23 -5
  28. package/src/model-capabilities/merge.js +2 -2
  29. package/src/providers/classify.js +1 -1
  30. package/src/providers/cline/auth.js +1 -1
  31. package/src/providers/cline/chat.js +14 -11
  32. package/src/providers/cline/free-catalog.js +71 -0
  33. package/src/providers/cline/index.js +2 -16
  34. package/src/providers/cline/models.js +28 -51
  35. package/src/providers/codearts/auth-pool.js +148 -0
  36. package/src/providers/codearts/chat.js +176 -0
  37. package/src/providers/codearts/const.js +60 -0
  38. package/src/providers/codearts/dpop.js +95 -0
  39. package/src/providers/codearts/index.js +160 -0
  40. package/src/providers/codearts/login.js +214 -0
  41. package/src/providers/codearts/models.js +182 -0
  42. package/src/providers/codearts/sign.js +96 -0
  43. package/src/providers/codearts/sse.js +204 -0
  44. package/src/providers/codearts/stream.js +209 -0
  45. package/src/providers/codearts/sts.js +124 -0
  46. package/src/providers/codearts.js +1 -0
  47. package/src/providers/model-id.js +4 -4
  48. package/src/providers/qoder/account-store.js +107 -0
  49. package/src/providers/qoder/aggregate.js +51 -0
  50. package/src/providers/qoder/baseprompt.json +592 -0
  51. package/src/providers/qoder/chat.js +72 -0
  52. package/src/providers/qoder/checkin.js +160 -0
  53. package/src/providers/qoder/constants.js +45 -0
  54. package/src/providers/qoder/encode.js +42 -0
  55. package/src/providers/qoder/fingerprint.js +39 -0
  56. package/src/providers/qoder/index.js +106 -0
  57. package/src/providers/qoder/models.js +62 -0
  58. package/src/providers/qoder/oauth.js +85 -0
  59. package/src/providers/qoder/payload.js +156 -0
  60. package/src/providers/qoder/request.js +26 -0
  61. package/src/providers/qoder/session.js +135 -0
  62. package/src/providers/qoder/sse.js +46 -0
  63. package/src/providers/qoder/stream.js +81 -0
  64. package/src/providers/qoder.js +2 -0
  65. package/src/providers/registry.js +16 -1
  66. package/src/providers/share-keys.js +7 -2
  67. package/src/providers/traework/account-store.js +149 -0
  68. package/src/providers/traework/chat.js +211 -0
  69. package/src/providers/traework/checkin.js +35 -0
  70. package/src/providers/traework/constants.js +21 -0
  71. package/src/providers/traework/errors.js +52 -0
  72. package/src/providers/traework/headers.js +47 -0
  73. package/src/providers/traework/index.js +122 -0
  74. package/src/providers/traework/models.js +97 -0
  75. package/src/providers/traework/payload.js +86 -0
  76. package/src/providers/traework/sse.js +184 -0
  77. package/src/providers/traework/token.js +52 -0
  78. package/src/providers/traework.js +2 -0
  79. package/src/providers/workbuddy/account-store.js +73 -7
  80. package/src/providers/workbuddy/chat.js +1 -1
  81. package/src/providers/workbuddy/index.js +10 -20
  82. package/src/readline-compat.js +9 -10
  83. package/src/routes/models-route.js +19 -1
  84. package/src/runtime/bootstrap.js +10 -0
  85. package/src/runtime/provider-gate.js +30 -0
  86. package/src/runtime/providers-setup.js +18 -3
  87. package/src/runtime/qoder-checkin.js +93 -0
  88. package/src/runtime/server-lifecycle.js +2 -2
  89. package/src/runtime/traework-checkin.js +104 -0
  90. package/src/state/migrations/cline-unify.js +94 -0
  91. package/src/state/migrations.js +33 -0
  92. package/src/state/schemas/token.js +6 -2
  93. package/src/state/schemas/use-group.js +4 -4
  94. package/src/sync-workbuddy.js +1 -1
  95. package/src/upstream-engine/index.js +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mslxdff",
3
- "version": "0.1.136",
3
+ "version": "0.1.140",
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)) {
package/src/chooser.js CHANGED
@@ -1,33 +1,52 @@
1
- // 交互式模型选择器的纯渲染逻辑(便于测试);键盘循环在 bin/mslxdff.js
1
+ // 交互式模型选择器的纯渲染逻辑(便于测试);键盘循环在 interactive.js
2
2
 
3
- // items: [{ id, status?, ms?, fail?, current?, picked? }];cursor 当前高亮行
3
+ // items: [{ id, status?, ms?, fail?, current?, picked? }];cursor 当前高亮行(全局下标)
4
4
  // multi=true 时多选勾选(picked 显示 [x]/[ ]),否则单选默认模型(当前显示 ✓)
5
- // 返回行数组(无 ANSI 颜色,纯文本标记,Windows 终端友好)
6
- export function renderChooser(items, cursor = 0, { multi = false } = {}) {
7
- return items.map((it, i) => {
5
+ // index/size 可选——窗口分页渲染(只画当页 + 页头),不传 size 则全量渲染(行为不变)
6
+ // Note: 分页是本模块职责(键盘循环在 interactive.js),不传 size 必全量渲染 — 见 .agents/notes/implemented/feature/2026-09-21-models-paginated-chooser.md
7
+ export function renderChooser(items, cursor = 0, { multi = false, index = 0, size } = {}) {
8
+ if (size == null) return renderRows(items, cursor, 0, multi);
9
+ const total = items.length;
10
+ const sz = Math.max(1, Number(size) || 1);
11
+ const pages = Math.max(1, Math.ceil(total / sz));
12
+ const idx = Math.min(Math.max(Number(index) || 0, 0), pages - 1);
13
+ const start = idx * sz;
14
+ const rows = [`── 第 ${idx + 1}/${pages} 页 · 共 ${total} 项(←/→ 翻页) ──`];
15
+ rows.push(...renderRows(items.slice(start, start + sz), cursor, start, multi));
16
+ return rows;
17
+ }
18
+
19
+ function renderRows(slice, cursor, offset, multi) {
20
+ return slice.map((it, li) => {
21
+ const i = offset + li;
8
22
  const arrow = i === cursor ? "❯" : " ";
9
- const check = multi
10
- ? (it.picked ? " [✓]" : " [ ]")
11
- : (it.current ? " ✓ (current)" : "");
12
23
  let state = "";
13
24
  if (it.fail) state = ` [fail: ${it.fail}]`;
14
25
  else if (it.ms != null) state = ` [${it.ms}ms]`;
15
26
  else if (it.status && it.status !== "normal") state = ` [${it.status}]`;
27
+ if (multi) {
28
+ const box = it.picked ? "[✓]" : "[ ]";
29
+ return `${arrow} ${box} ${it.id}${state}`;
30
+ }
31
+ const check = it.current ? " ✓ (current)" : "";
16
32
  return `${arrow} ${it.id}${check}${state}`;
17
33
  });
18
34
  }
19
35
 
20
- export function renderChooserHelp(multi = false) {
36
+ export function renderChooserHelp(multi = false, hasPages = false) {
37
+ const paging = hasPages ? " · ←/→ page" : "";
21
38
  return multi
22
- ? ["", "↑/↓ move · Space toggle pick · Enter save picks · q/Esc cancel"]
23
- : ["", "↑/↓ move · Enter select as default · q/Esc cancel"];
39
+ ? ["", `↑/↓ move · Space toggle pick${paging} · Enter save picks · q/Esc cancel`]
40
+ : ["", `↑/↓ move${paging} · Enter select as default · q/Esc cancel`];
24
41
  }
25
42
 
26
- // 解析按键:返回 "up" | "down" | "enter" | "cancel" | "space" | null(忽略)
43
+ // 解析按键:返回 "up" | "down" | "pageup" | "pagedown" | "enter" | "cancel" | "space" | null(忽略)
27
44
  export function parseKey(str) {
28
45
  if (!str) return null;
29
46
  if (str === "\x1b[A" || str === "k") return "up";
30
47
  if (str === "\x1b[B" || str === "j") return "down";
48
+ if (str === "\x1b[D" || str === "h" || str === "\x1b[5~") return "pageup";
49
+ if (str === "\x1b[C" || str === "l" || str === "\x1b[6~") return "pagedown";
31
50
  if (str === "\r" || str === "\n") return "enter";
32
51
  if (str === " " || str === "\x1b[32") return "space"; // 空格(32=0x20 的 ASC 表示)或某些终端 terminator
33
52
  if (str === "\x1b" || str === "q" || str === "\x03") return "cancel";
@@ -0,0 +1,22 @@
1
+ // 交互式 `-models` 候选合并(从 list.js 拆出以守住 ≤10KB 体积门)。
2
+ // allowAny 且空白名单的供应商(qoder/codearts 类)不写 allowlist,其模型只存在于
3
+ // daemon 聚合的 /v1/models —— 不并入这些 id,该供应商在交互列表里「一个都挑不到」。
4
+ import { loadProviderAllowedModels } from "../../../state.js";
5
+
6
+ /** 把「空白名单且已启用」供应商的网关 live 模型并入候选(按前缀归属,去重)。 */
7
+ export async function mergeLiveIds({ combinedIds, seen, knownProviders, liveIds }) {
8
+ const needsLive = knownProviders.filter(
9
+ (k) => String(k).toLowerCase() !== "opencode" && loadProviderAllowedModels(k).length === 0,
10
+ );
11
+ if (!needsLive.length) return;
12
+ const { fetchDaemonModelIds } = await import("./live-models.js");
13
+ const live = await fetchDaemonModelIds();
14
+ for (const id of live) {
15
+ if (liveIds.includes(id)) continue; // 单供应商缓存已含 → 不重复
16
+ const pid = String(id).includes("/") ? String(id).split("/")[0].toLowerCase() : "opencode";
17
+ if (!needsLive.some((k) => String(k).toLowerCase() === pid)) continue;
18
+ if (seen.has(id)) continue;
19
+ seen.add(id);
20
+ combinedIds.push(id);
21
+ }
22
+ }
@@ -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) {
@@ -10,6 +10,7 @@ import { pickInteractiveMulti } from "../../interactive.js";
10
10
  import { buildAliasMap, readFullAliases, renderProviderList, renderFreeList, groupByProvider } from "./list-render.js";
11
11
  import { renderOtherProviders } from "./list-providers.js";
12
12
  import { filterStalePicks } from "../../../providers/model-id.js";
13
+ import { mergeLiveIds } from "./list-live.js";
13
14
 
14
15
  /**
15
16
  * `-model list` 全流程:参数解析 → 刷新/回退 → provider 分支 → TTY 交互 → 分组渲染。
@@ -159,6 +160,7 @@ export async function handleModelList(args, idx, sub) {
159
160
  }
160
161
  }
161
162
  }
163
+ await mergeLiveIds({ combinedIds, seen, knownProviders, liveIds: ids });
162
164
  staleFilter = { knownProviders, liveIds: ids, allowedIds };
163
165
  } catch {}
164
166
  // provider 已不存在的 picks 孤儿不进交互列表(数据不动,status --all 仍可审计)。
@@ -0,0 +1,24 @@
1
+ // 网关 live 模型列表:allowAny 且空白名单的供应商(qoder/codearts 类)不写 allowlist,
2
+ // 其模型只存在于 daemon 聚合的 /v1/models 里;交互式 `-models` 必须并入这些 id,
3
+ // 否则该供应商「一个模型都挑不到」(用户视角:-models 什么也没有)。
4
+ import { compatFetch, timeoutSignal } from "../../../compat.js";
5
+ import { effectivePort, effectiveHost } from "../../policy.js";
6
+ import { loadToken } from "../../../state.js";
7
+
8
+ export async function fetchDaemonModelIds({ fetchImpl = compatFetch, port, host, token, timeoutMs = 6000 } = {}) {
9
+ try {
10
+ const p = port ?? effectivePort([]);
11
+ const h = host ?? effectiveHost() ?? "127.0.0.1";
12
+ const tk = (token ?? (await loadToken())?.token) || "";
13
+ const res = await fetchImpl(`http://${h}:${p}/v1/models?all=1`, {
14
+ headers: tk ? { Authorization: `Bearer ${tk}` } : {},
15
+ signal: timeoutSignal(timeoutMs),
16
+ });
17
+ if (!res?.ok) return [];
18
+ const json = await res.json().catch(() => ({}));
19
+ return (json?.data || []).map((m) => m?.id).filter(Boolean);
20
+ } catch {
21
+ // 网关没跑 / 超时 / 鉴权失败:静默降级(候选列表退回 allowlist + picks)
22
+ return [];
23
+ }
24
+ }
@@ -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
  }
@@ -0,0 +1,45 @@
1
+ // `-provider codearts login`:浏览器 PKCE 登录华为云 → 凭证 blob 落盘 providerConfigs.codearts。
2
+ // 约定(对齐 cline-login/workbuddy-login):不匹配返 false 交给下一个 handler;匹配即处理并退出。
3
+ import { normalizeProviderId } from "../../../providers/model-id.js";
4
+ import { defaultStateFile, loadProviderKeys, saveProviderConfig, saveProviderAllowAnyModels, loadProviderBaseUrl } from "../../../state.js";
5
+ import { SNAP_BASE } from "../../../providers/codearts/const.js";
6
+ import { runCodeartsLogin, accountToBlob } from "../../../providers/codearts/login.js";
7
+ import { accountFromBlob } from "../../../providers/codearts/auth-pool.js";
8
+
9
+ export async function handleCodeartsLogin(id, sub, rest = [], deps = {}) {
10
+ if (normalizeProviderId(id) !== "codearts") return false;
11
+ if (!(sub === "login" || sub === "auth" || sub === "oauth")) return false;
12
+ const noAllowAny = (rest || []).includes("--no-allow-any");
13
+ const file = deps.file || defaultStateFile();
14
+ const fetchImpl = deps.fetchImpl;
15
+ const log = deps.log || ((m) => console.log(m));
16
+
17
+ try {
18
+ const { account, state } = await runCodeartsLogin({ fetchImpl, log });
19
+ const blob = accountToBlob(account, { codeVerifier: state.codeVerifier, dpopJwk: state.dpopJwk, clientId: state.clientId });
20
+ // 同一账号重复登录:替换旧 blob;新账号:追加(多账号 keyring 轮转)。
21
+ // 去重键:userId 非空才可信;双方任一为空时退化为 refreshToken 比较
22
+ // (空 userId==空 userId 的误判会把第二个不同账号当同一账号替换,真机实测恒剩 1 key)
23
+ const cur = loadProviderKeys("codearts", { file }).filter((k) => {
24
+ const parsed = accountFromBlob(k);
25
+ if (!parsed) return false;
26
+ if (parsed.userId && account.userId) return parsed.userId !== account.userId;
27
+ return parsed.refreshToken !== account.refreshToken;
28
+ });
29
+ const baseUrl = loadProviderBaseUrl("codearts", { file }) || SNAP_BASE;
30
+ saveProviderConfig("codearts", { baseUrl, keys: [...cur, blob] }, { file });
31
+ if (!noAllowAny) saveProviderAllowAnyModels("codearts", true, { file });
32
+
33
+ log("");
34
+ log(`✅ codearts 登录成功:${account.userName || account.userId || "(user)"}(剩余账号数:${cur.length + 1})`);
35
+ log(` baseUrl: ${baseUrl}`);
36
+ log(` 凭证已写入 providerConfigs.codearts.keys(state.json,0600 权限目录,绝不入库)`);
37
+ if (!noAllowAny) log(" allowAnyModels=on(免费福利供应商默认放行全部模型;收紧:mslxdff -provider codearts allowAny off)");
38
+ log(" 生效:mslxdff -restart(用户终端跑),然后 mslxdff -provider codearts models 验证");
39
+ process.exit(0);
40
+ } catch (err) {
41
+ console.error(`❌ codearts 登录失败:${String(err?.message || err)}`);
42
+ process.exit(1);
43
+ }
44
+ return true;
45
+ }
@@ -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) {
@@ -40,6 +41,9 @@ export async function handleProvider(args) {
40
41
  console.error(" mslxdff -provider add myapi https://api.example.com/v1 sk-xxx gpt-4 add with allowlist");
41
42
  console.error(" mslxdff -provider add myapi https://api.example.com/v1 sk-xxx --models-path /v1/models --chat-path /v1/chat/completions");
42
43
  console.error(" mslxdff -provider openrouter interactive hidden input (append)");
44
+ console.error(" mslxdff -provider codearts login 华为云 CodeArts PKCE 授权(浏览器登录 → 凭证 blob 落盘,ADR-0027)");
45
+ console.error(" mslxdff -provider qoder login [--region cn|global] Qoder 设备授权(国际站默认,国内站 --region cn)");
46
+ console.error(" mslxdff -provider qoder checkin [--json] [--region cn|global] [--any] [--dry] 每日签到领积分(cn=daily-check-in,global=campaigns)");
43
47
  console.error(" mslxdff -provider openrouter clear remove all keys");
44
48
  process.exit(1);
45
49
  }
@@ -66,6 +70,15 @@ export async function handleProvider(args) {
66
70
  if (await handleClineLogin(id, sub)) return true;
67
71
  const { handleWorkbuddyLogin } = await import("./workbuddy-login.js");
68
72
  if (await handleWorkbuddyLogin(id, sub, rest)) return true;
73
+ const { handleCodeartsLogin } = await import("./codearts-login.js");
74
+ if (await handleCodeartsLogin(id, sub, rest)) return true;
75
+ const { handleTraeworkLogin, handleTraeworkCheckin } = await import("./traework-login.js");
76
+ if (await handleTraeworkLogin(id, sub, rest)) return true;
77
+ if (await handleTraeworkCheckin(id, sub, rest)) return true;
78
+ const { handleQoderLogin } = await import("./qoder-login.js");
79
+ if (await handleQoderLogin(id, sub, rest)) return true;
80
+ const { handleQoderCheckin } = await import("./qoder-checkin.js");
81
+ if (await handleQoderCheckin(id, sub, rest)) return true;
69
82
  const { handleProviderConfig } = await import("./config.js");
70
83
  if (await handleProviderConfig(id, sub, rest)) return true;
71
84
  const { handleProviderAllowlist } = await import("./allowlist.js");
@@ -74,6 +87,27 @@ export async function handleProvider(args) {
74
87
  if (await handleProviderModels(id, sub, args, rest)) return true;
75
88
  const { handleProviderBench } = await import("./bench.js");
76
89
  if (await handleProviderBench(id, sub, rest, args)) return true;
90
+ const { handleClineFree } = await import("./cline-free.js");
91
+ if (await handleClineFree(id, sub, rest, args)) return true;
92
+ if (normalizeProviderId(id) === "cline" && sub === "migrate") {
93
+ const { runStateMigrations } = await import("../../../state/migrations.js");
94
+ const dryRun = args.includes("--dry-run") || args.includes("--dryrun");
95
+ const r = await runStateMigrations({ dryRun });
96
+ const d = r.details["cline-unify"] || {};
97
+ if (r.errors.length) console.error(`❌ 迁移失败: ${r.errors.map((e) => `${e.id}: ${e.error}`).join("; ")}`);
98
+ if (dryRun) {
99
+ console.log(d.changed
100
+ ? `cline migrate 干跑:将合并遗留 cline id 配置(keys ${d.before.keys} → ${d.after.keys} / allowlist ${d.before.allowedModels} → ${d.after.allowedModels}),未写入`
101
+ : "cline migrate 干跑:无需迁移(未发现 providerConfigs.clinebot / cline-bot)");
102
+ } else if (d.applied) {
103
+ console.log(`✅ cline migrate 完成:keys ${d.before.keys} → ${d.after.keys} / allowlist ${d.before.allowedModels} → ${d.after.allowedModels}`);
104
+ if (d.backup) console.log(` 备份:${d.backup}`);
105
+ console.log(" 生效:mslxdff -restart");
106
+ } else {
107
+ console.log("cline migrate:无需迁移(未发现 providerConfigs.clinebot / cline-bot)");
108
+ }
109
+ process.exit(r.errors.length ? 1 : 0);
110
+ }
77
111
  const { handleProviderKeys } = await import("./keys.js");
78
112
  await handleProviderKeys(id, sub, rest, args);
79
113
  return true;
@@ -10,12 +10,23 @@ export async function handleProviderKeys(id, sub, rest, args) {
10
10
  else console.log(`provider: ${id}${id === "openrouter" ? " (built-in baseUrl: https://openrouter.ai/api/v1)" : ""}`);
11
11
  if (keys.length) {
12
12
  console.log(` keys: ${keys.length} key${keys.length > 1 ? "s" : ""}`);
13
- keys.forEach((k, i) => console.log(` [${i + 1}] ${k.slice(0, 4)}…${k.slice(-4)} (${k.length} chars)`));
13
+ if (id === "codearts") {
14
+ const { accountFromBlob } = await import("../../../providers/codearts/auth-pool.js");
15
+ for (const [i, k] of keys.entries()) {
16
+ const a = accountFromBlob(k);
17
+ if (a) console.log(` [${i + 1}] user=${a.userName || "(未识别)"} uid=${a.userId ? `${a.userId.slice(0, 8)}…` : "(空,请删了重登回填)"} domain=${a.domainId ? a.domainId.slice(0, 8) + "…" : "(空)"} (${k.length} chars)`);
18
+ else console.log(` [${i + 1}] ${k.slice(0, 4)}…${k.slice(-4)} (${k.length} chars)`);
19
+ }
20
+ } else keys.forEach((k, i) => console.log(` [${i + 1}] ${k.slice(0, 4)}…${k.slice(-4)} (${k.length} chars)`));
14
21
  console.log(` remove by: mslxdff -provider ${id} remove <seq> [seq...] | <key-value>`);
15
22
  } else {
16
23
  console.log(` keys: (no keys configured)`);
17
24
  }
18
- console.log(` share keys to peers: 借出(默认,key 随转发自动附带,ADR-0019)`);
25
+ try {
26
+ const { shareableProviderIds } = await import("../../../providers/share-keys.js");
27
+ const shared = shareableProviderIds().includes(id);
28
+ console.log(` share keys to peers: ${shared ? "借出(随转发自动附带,ADR-0019)" : "不借出(local-only / 硬排除,codearts/cline 恒不借,ADR-0019/0015)"}`);
29
+ } catch { console.log(` share keys to peers: 借出(默认,key 随转发自动附带,ADR-0019)`); }
19
30
  console.log(` allowAnyModels: ${allowAny ? "ON (empty allowlist = allow all)" : "OFF (empty allowlist = BLOCK ALL)"} (mslxdff -provider ${id} allowAny on|off)`);
20
31
  if (allowed.length) console.log(` allowedModels: ${allowed.length} (${allowed.join(", ")}) — only these can be used`);
21
32
  else console.log(` allowedModels: (none — ${allowAny ? "allow all" : "BLOCK ALL"}) (mslxdff -provider ${id} allowlist set <model...> or allowAny on)`);
@@ -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,10 +37,19 @@ 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() });
44
+ } else if (id === "codearts") {
45
+ const { createCodeartsProvider } = await import("../../../providers/codearts.js");
46
+ provider = createCodeartsProvider({ baseUrl: baseUrl || undefined, apiKeys: keys, file: defaultStateFile() });
47
+ } else if (id === "traework") {
48
+ const { createTraeworkProvider } = await import("../../../providers/traework.js");
49
+ provider = createTraeworkProvider({ baseUrl: baseUrl || undefined, apiKeys: keys, auths, file: defaultStateFile() });
50
+ } else if (id === "qoder") {
51
+ const { createQoderProvider } = await import("../../../providers/qoder/index.js");
52
+ provider = createQoderProvider({ id, apiKeys: keys, file: defaultStateFile() });
41
53
  } else {
42
54
  if (!baseUrl) {
43
55
  console.error(`provider ${id}: missing baseUrl — set via: mslxdff -provider ${id} set-url <baseUrl>`);