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
@@ -0,0 +1,86 @@
1
+ // OpenAI → SOLO llm_utils_chat 请求体改写(照抄 traework2api internal/upstream/payload.go)。
2
+ import { DEFAULT_MODEL, FUNCTION } from "./constants.js";
3
+
4
+ // tool_choice 归一化(上游 Go struct 是 string 类型)。
5
+ export function normalizeToolChoice(obj) {
6
+ const suppress = () => { delete obj.tools; delete obj.functions; };
7
+ if (!("tool_choice" in obj)) return;
8
+ const tc = obj.tool_choice;
9
+ if (typeof tc === "string") {
10
+ if (String(tc).trim().toLowerCase() === "none") { delete obj.tool_choice; suppress(); }
11
+ return;
12
+ }
13
+ if (tc && typeof tc === "object" && !Array.isArray(tc)) {
14
+ const typ = String(tc.type || "").trim().toLowerCase();
15
+ if (typ === "none") { delete obj.tool_choice; suppress(); }
16
+ else if (typ === "auto" || typ === "required") obj.tool_choice = typ;
17
+ else if (typ === "function") {
18
+ const name = String(tc.function?.name || tc.name || "").trim();
19
+ obj.tool_choice = name || "auto";
20
+ } else delete obj.tool_choice;
21
+ return;
22
+ }
23
+ delete obj.tool_choice;
24
+ }
25
+
26
+ // tools:缺 function 剔除;function.parameters 对象 → JSON 字符串。
27
+ export function normalizeTools(obj) {
28
+ const raw = obj.tools;
29
+ if (raw === undefined) return;
30
+ if (!Array.isArray(raw) || !raw.length) return;
31
+ const out = [];
32
+ for (const item of raw) {
33
+ if (!item || typeof item !== "object") continue;
34
+ const fn = item.function;
35
+ if (!fn || typeof fn !== "object") continue;
36
+ if (fn.parameters && typeof fn.parameters === "object" && !Array.isArray(fn.parameters)) {
37
+ try { fn.parameters = JSON.stringify(fn.parameters); } catch {}
38
+ }
39
+ out.push(item);
40
+ }
41
+ if (!out.length) { delete obj.tools; return; }
42
+ obj.tools = out;
43
+ }
44
+
45
+ // 单 pass 改写;无法解析时原样返回。
46
+ export function prepareBody(src) {
47
+ let obj;
48
+ if (typeof src === "string") {
49
+ if (!src) return src;
50
+ try { obj = JSON.parse(src); } catch { return src; }
51
+ } else if (src && typeof src === "object") {
52
+ obj = { ...src };
53
+ } else return src;
54
+ if (!obj || typeof obj !== "object" || Array.isArray(obj)) return src;
55
+ obj.stream = true;
56
+ obj.function = FUNCTION;
57
+ if (Array.isArray(obj.messages)) {
58
+ for (const mi of obj.messages) {
59
+ if (!mi || typeof mi !== "object") continue;
60
+ if (mi.role === "assistant" && Array.isArray(mi.tool_calls)) {
61
+ const kept = [];
62
+ for (const tci of mi.tool_calls) {
63
+ if (!tci || typeof tci !== "object") continue;
64
+ const tc = { ...tci };
65
+ if (tc.function && typeof tc.function === "object") {
66
+ tc.function_call = tc.function;
67
+ delete tc.function;
68
+ }
69
+ const fc = tc.function_call;
70
+ if (fc && typeof fc === "object" && !String(fc.name || "").trim()) continue;
71
+ kept.push(tc);
72
+ }
73
+ if (!kept.length) delete mi.tool_calls;
74
+ else mi.tool_calls = kept;
75
+ }
76
+ if (!("content" in mi) || mi.content == null) continue;
77
+ if (typeof mi.content === "string") mi.content = [{ type: "text", text: mi.content }];
78
+ }
79
+ }
80
+ let model = String(obj.model || "").trim() || DEFAULT_MODEL;
81
+ obj.config_name = model;
82
+ obj.model = model;
83
+ normalizeToolChoice(obj);
84
+ normalizeTools(obj);
85
+ return obj;
86
+ }
@@ -0,0 +1,184 @@
1
+ // SOLO 自定义 SSE 解析 → OpenAI SSE(流式转换 + 非流式聚合)。
2
+ // 照抄 traework2api internal/upstream/solosse.go。纯函数,可测。
3
+ import { SOLOStreamError } from "./errors.js";
4
+
5
+ // 解析一条事件(eventName 为 event 行值,dataLine 为 data 行值)。
6
+ export function parseSOLOLine(eventName, dataLine) {
7
+ const ev = { event: String(eventName || "").trim(), response: "", reasoning: "", toolCalls: null, usage: null, finishReason: "", errorCode: 0, errorMessage: "" };
8
+ if (!dataLine) return ev;
9
+ let raw;
10
+ try { raw = JSON.parse(dataLine); } catch { throw new Error("solo sse data parse error"); }
11
+ if (!raw || typeof raw !== "object") return ev;
12
+ if (ev.event === "output") {
13
+ if (typeof raw.response === "string") ev.response = raw.response;
14
+ if (typeof raw.reasoning_content === "string") ev.reasoning = raw.reasoning_content;
15
+ if ("tool_calls" in raw && raw.tool_calls != null) ev.toolCalls = raw.tool_calls;
16
+ } else if (ev.event === "token_usage") {
17
+ ev.usage = raw;
18
+ } else if (ev.event === "done") {
19
+ if (typeof raw.finish_reason === "string") ev.finishReason = raw.finish_reason;
20
+ } else if (ev.event === "error") {
21
+ if (typeof raw.code === "number") ev.errorCode = raw.code;
22
+ else if (typeof raw.code === "string" && raw.code.trim() !== "" && Number.isFinite(Number(raw.code))) ev.errorCode = Number(raw.code);
23
+ if (typeof raw.message === "string") ev.errorMessage = raw.message;
24
+ }
25
+ return ev;
26
+ }
27
+
28
+ // sseState 维护一行 SSE 的 event/data 跨行累积。
29
+ export function createSseState() { return { event: "", data: "" }; }
30
+ export function resetSseState(st) { st.event = ""; st.data = ""; }
31
+
32
+ // scanLine 处理一行;返回该行触发的事件(事件边界时解析并返回)。
33
+ export function scanLine(st, line) {
34
+ const s = String(line ?? "");
35
+ if (s === "") {
36
+ if (!st.event) { resetSseState(st); return null; }
37
+ let ev = null;
38
+ try { ev = parseSOLOLine(st.event, st.data); } catch { ev = null; }
39
+ resetSseState(st);
40
+ return ev;
41
+ }
42
+ if (s.startsWith("event:")) st.event = s.slice("event:".length).trim();
43
+ else if (s.startsWith("data:")) st.data += s.slice("data:".length);
44
+ else if (s.startsWith(":")) { /* 注释行忽略 */ }
45
+ return null;
46
+ }
47
+
48
+ function mergeToolCallDelta(merged, delta) {
49
+ if (typeof delta?.id === "string" && delta.id) merged.id = delta.id;
50
+ if (typeof delta?.type === "string" && delta.type) merged.type = delta.type;
51
+ let df = delta?.function && typeof delta.function === "object" ? { ...delta.function } : null;
52
+ if (!df && delta?.function_call && typeof delta.function_call === "object") df = { ...delta.function_call };
53
+ if (!df) return;
54
+ delete df.namespace;
55
+ delete df.partial_arguments;
56
+ if (!merged.function || typeof merged.function !== "object") merged.function = {};
57
+ if (typeof df.name === "string" && df.name) merged.function.name = df.name;
58
+ if (typeof df.arguments === "string" && df.arguments) {
59
+ merged.function.arguments = (typeof merged.function.arguments === "string" ? merged.function.arguments : "") + df.arguments;
60
+ }
61
+ }
62
+
63
+ function mergeToolCallJSON(toolCalls, toolOrder, raw) {
64
+ if (raw == null) return;
65
+ let arr;
66
+ if (Array.isArray(raw)) arr = raw;
67
+ else if (raw && typeof raw === "object") arr = [raw];
68
+ else return;
69
+ for (const call of arr) {
70
+ if (!call || typeof call !== "object") continue;
71
+ const idx = Number.isFinite(Number(call.index)) ? Number(call.index) : 0;
72
+ if (!Object.prototype.hasOwnProperty.call(toolCalls, idx)) {
73
+ toolCalls[idx] = { index: idx };
74
+ toolOrder.push(idx);
75
+ }
76
+ mergeToolCallDelta(toolCalls[idx], call);
77
+ }
78
+ }
79
+
80
+ // 把 SOLO tool_call 条目 function_call → function(流式 delta 用)。
81
+ export function normalizeStreamToolCalls(raw) {
82
+ const arr = Array.isArray(raw) ? raw : [raw];
83
+ const out = [];
84
+ for (const item of arr) {
85
+ if (!item || typeof item !== "object") continue;
86
+ const call = { ...item };
87
+ if (call.function_call && typeof call.function_call === "object") {
88
+ call.function = call.function_call;
89
+ delete call.function_call;
90
+ }
91
+ if (call.function && typeof call.function === "object") {
92
+ call.function = { ...call.function };
93
+ delete call.function.namespace;
94
+ delete call.function.partial_arguments;
95
+ }
96
+ out.push(call);
97
+ }
98
+ return out;
99
+ }
100
+
101
+ function openAIChunk(id, created, model, delta, finish, usage) {
102
+ const chunk = { id, object: "chat.completion.chunk", created, model: model || "", choices: [{ index: 0, delta }] };
103
+ if (finish) chunk.choices[0].finish_reason = finish;
104
+ if (usage) chunk.usage = usage;
105
+ return chunk;
106
+ }
107
+
108
+ // convertToOpenAIChunks:SOLO SSE 文本 → OpenAI SSE 文本(每 chunk flush 语义由调用方保证至少一个 [DONE])。
109
+ export function convertToOpenAIChunks(soloText, { model = "", chatId = "", created = 0 } = {}) {
110
+ const id = chatId || `chatcmpl-${Date.now()}`;
111
+ const ts = created || Math.floor(Date.now() / 1000);
112
+ const st = createSseState();
113
+ const frames = [];
114
+ let pendingUsage = null;
115
+ let sawDone = false;
116
+ const push = (delta, finish) => frames.push(`data: ${JSON.stringify(openAIChunk(id, ts, model, delta, finish, pendingUsage || undefined))}\n\n`) || (pendingUsage = null);
117
+ for (const line of String(soloText || "").split("\n")) {
118
+ const ev = scanLine(st, line.replace(/\r$/, ""));
119
+ if (!ev) continue;
120
+ if (ev.event === "output") {
121
+ const delta = {};
122
+ if (ev.response) delta.content = ev.response;
123
+ if (ev.reasoning) delta.reasoning_content = ev.reasoning;
124
+ if (ev.toolCalls != null) delta.tool_calls = normalizeStreamToolCalls(ev.toolCalls);
125
+ if (Object.keys(delta).length) push(delta, "");
126
+ } else if (ev.event === "token_usage") pendingUsage = ev.usage;
127
+ else if (ev.event === "done") { push({}, ev.finishReason || "stop"); frames.push("data: [DONE]\n\n"); sawDone = true; }
128
+ else if (ev.event === "error") {
129
+ const se = new SOLOStreamError(ev.errorCode, ev.errorMessage);
130
+ frames.push(`event: error\ndata: ${JSON.stringify(se.message)}\n\n`);
131
+ frames.push("data: [DONE]\n\n");
132
+ sawDone = true;
133
+ }
134
+ }
135
+ if (!sawDone) frames.push("data: [DONE]\n\n");
136
+ return frames.join("");
137
+ }
138
+
139
+ // aggregateToCompletion:完整 SOLO SSE 文本 → 单个 OpenAI chat.completion(非流式)。
140
+ export function aggregateToCompletion(soloText, { model = "", chatId = "", created = 0 } = {}) {
141
+ const st = createSseState();
142
+ let content = "";
143
+ let reasoning = "";
144
+ let finishReason = "stop";
145
+ let usage = null;
146
+ const toolCalls = {};
147
+ const toolOrder = [];
148
+ for (const line of String(soloText || "").split("\n")) {
149
+ const ev = scanLine(st, line.replace(/\r$/, ""));
150
+ if (!ev) continue;
151
+ if (ev.event === "output") {
152
+ content += ev.response || "";
153
+ reasoning += ev.reasoning || "";
154
+ mergeToolCallJSON(toolCalls, toolOrder, ev.toolCalls);
155
+ } else if (ev.event === "token_usage") usage = ev.usage;
156
+ else if (ev.event === "done") { if (ev.finishReason) finishReason = ev.finishReason; }
157
+ else if (ev.event === "error") throw new SOLOStreamError(ev.errorCode, ev.errorMessage);
158
+ }
159
+ const message = { role: "assistant", content };
160
+ if (reasoning) message.reasoning_content = reasoning;
161
+ if (toolOrder.length) message.tool_calls = [...toolOrder].sort((a, b) => a - b).map((i) => toolCalls[i]);
162
+ const resp = {
163
+ id: chatId || `chatcmpl-${Date.now()}`,
164
+ object: "chat.completion",
165
+ created: created || Math.floor(Date.now() / 1000),
166
+ model: model || "",
167
+ choices: [{ index: 0, message, finish_reason: finishReason }],
168
+ };
169
+ if (usage) resp.usage = usage;
170
+ return resp;
171
+ }
172
+
173
+ // aggregateStreamReader:ReadableStream(fetch Response.body)→ chat.completion(流式 Response 转非流式用)。
174
+ export async function aggregateStreamReader(stream, opts = {}) {
175
+ const reader = stream.getReader();
176
+ const dec = new TextDecoder();
177
+ let text = "";
178
+ for (;;) {
179
+ const { done, value } = await reader.read();
180
+ if (value) text += dec.decode(value, { stream: !done });
181
+ if (done) break;
182
+ }
183
+ return aggregateToCompletion(text, opts);
184
+ }
@@ -0,0 +1,52 @@
1
+ // ExchangeToken 刷新(照抄 traework2api internal/upstream/client.go refreshLocked)。
2
+ import { CLIENT_ID, EP_EXCHANGE, IDE_VERSION, OAUTH_HOST } from "./constants.js";
3
+ import { oauthHeaders } from "./headers.js";
4
+
5
+ // TokenExpireAt 毫秒 → Unix 秒归一(毫秒 ~1.7e12,秒 ~1.7e9,用 1e12 区分)。
6
+ export function normalizeExpiresAt(v) {
7
+ const n = Number(v) || 0;
8
+ return n > 1e12 ? Math.floor(n / 1000) : n;
9
+ }
10
+
11
+ export async function exchangeRefresh(fetchImpl, apiHost, refreshToken, { timeoutMs = 30_000 } = {}) {
12
+ const rt = String(refreshToken || "").trim();
13
+ if (!rt) throw new Error("no refreshToken");
14
+ const host = String(apiHost || "").trim().replace(/\/+$/, "") || OAUTH_HOST;
15
+ const ctl = new AbortController();
16
+ const timer = setTimeout(() => ctl.abort(new Error("traework ExchangeToken timed out")), timeoutMs);
17
+ if (typeof timer.unref === "function") timer.unref();
18
+ let res;
19
+ try {
20
+ res = await fetchImpl(`${host}${EP_EXCHANGE}`, {
21
+ method: "POST",
22
+ headers: oauthHeaders(),
23
+ body: JSON.stringify({ ClientID: CLIENT_ID, RefreshToken: rt, ClientSecret: "-", UserID: "" }),
24
+ signal: ctl.signal,
25
+ });
26
+ } finally { clearTimeout(timer); }
27
+ const txt = await res.text().catch(() => "");
28
+ if (res.status >= 400) throw Object.assign(new Error(`exchange failed: http ${res.status} ${txt.slice(0, 200)}`), { status: res.status, body: txt });
29
+ let j;
30
+ try { j = JSON.parse(txt); } catch { throw new Error(`exchange parse: non-JSON ${txt.slice(0, 200)}`); }
31
+ const result = j?.Result || {};
32
+ if (!result.Token) throw new Error("refresh_failed: no token in response — re-login required");
33
+ let expiresAt = 0;
34
+ if (Number(result.TokenExpireAt) > 0) expiresAt = normalizeExpiresAt(result.TokenExpireAt);
35
+ else if (Number(result.TokenExpireDuration) > 0) expiresAt = Math.floor(Date.now() / 1000) + Number(result.TokenExpireDuration);
36
+ return { token: result.Token, refreshToken: result.RefreshToken || rt, expiresAt };
37
+ }
38
+
39
+ export async function fetchUserInfo(fetchImpl, apiHost, accessToken, { timeoutMs = 30_000 } = {}) {
40
+ const host = String(apiHost || "").trim().replace(/\/+$/, "") || OAUTH_HOST;
41
+ const res = await fetchImpl(`${host}/cloudide/api/v3/trae/GetUserInfo`, {
42
+ method: "POST",
43
+ headers: { ...oauthHeaders(), "X-Cloudide-Token": accessToken || "" },
44
+ body: JSON.stringify({ ReqSource: "IDE", IDEVersion: IDE_VERSION }),
45
+ });
46
+ const txt = await res.text().catch(() => "");
47
+ if (res.status >= 400) throw Object.assign(new Error(`userinfo failed: http ${res.status}`), { status: res.status });
48
+ let j;
49
+ try { j = JSON.parse(txt); } catch { throw new Error("userinfo parse: non-JSON"); }
50
+ const r = j?.Result || j || {};
51
+ return { uid: String(r.UserID || ""), nickname: String(r.ScreenName || ""), enterpriseId: String(r.EnterpriseID || "") };
52
+ }
@@ -0,0 +1,2 @@
1
+ // Shim — 保持历史导入路径兼容,真实实现下沉至 ./traework/ 深模块
2
+ export * from "./traework/index.js";
@@ -1,17 +1,83 @@
1
1
  // WorkBuddy 账号落盘单一源:auths/workbuddy-<uid>.json(0600 tmp+rename)+ state.json keys/auths 按 uid 去重。
2
2
  // token-auto / device-login / 刷新(服务端 saveFn 与 CLI 定时任务)共用同一实现,安全细节只维护一份。
3
3
  // Note: 刷新落盘只走 applyTokenRefresh,别再各自内联一套 — 见 .agents/notes/implemented/architecture/2026-09-17-workbuddy-persist-single-seam.md
4
- import { writeFileSync, mkdirSync, existsSync, renameSync, unlinkSync } from "node:fs";
4
+ // Note: 凭据目录别再用 cwd 兜底 — 见 .agents/notes/implemented/architecture/2026-09-20-workbuddy-authdir-follows-state.md
5
+ import { writeFileSync, mkdirSync, existsSync, renameSync, unlinkSync, readdirSync, readFileSync } from "node:fs";
5
6
  import { join, dirname } from "node:path";
6
7
  import { tmpdir } from "node:os";
7
- import { isTestEnv } from "../../state/store.js";
8
+ import { isTestEnv, defaultStateFile } from "../../state/store.js";
9
+
10
+ // 凭据目录单一真相:跟着「账本」(state 文件)走 —— 默认 ~/.config/mslxdff/auths。
11
+ // 旧实现末尾兜底 `join(process.cwd(), "auths")`:在任意目录里起服务就把企业长效
12
+ // refreshToken 写进那个目录,而 .gitignore 只管自己所在的仓库 → 凭据落到不受保护的目录
13
+ // (本机实测:两个真实账号因此分居 `~/.config/mslxdff/auths` 与 `项目根/auths` 两处)。已修。
14
+ // 纯策略:写入目录的唯一目标(explicit > 测试隔离 > 「跟账本走」= state 文件同目录/auths)。
15
+ // 抽成纯函数是为了可测 —— `node --test` 下 isTestEnv() 恒真,环境态没法在测试里翻面。
16
+ export function authDirFor({ explicit = "", testEnv = false, stateFile } = {}) {
17
+ if (explicit) return explicit;
18
+ if (testEnv) return join(tmpdir(), "mslxdff-test-auths");
19
+ return join(dirname(stateFile), "auths");
20
+ }
8
21
 
9
22
  export function resolveAuthDir() {
10
- if (process.env.WORKBUDDY_AUTH_DIR) return process.env.WORKBUDDY_AUTH_DIR;
11
- const sf = process.env.MSLXDFF_STATE_FILE || "";
12
- if (sf.includes("mslxdff-test") || isTestEnv()) return join(tmpdir(), "mslxdff-test-auths");
13
- if (sf && sf.includes("mslxdff-")) return join(dirname(sf), "auths");
14
- return join(process.cwd(), "auths");
23
+ return authDirFor({
24
+ explicit: process.env.WORKBUDDY_AUTH_DIR || "",
25
+ testEnv: isTestEnv(),
26
+ stateFile: defaultStateFile(),
27
+ });
28
+ }
29
+
30
+ // 纯策略:算出读取候选目录(主位置优先,历史 cwd/auths 只读兜底,相同则去重)。
31
+ // 抽成纯函数是为了可测 —— `node --test` 下 isTestEnv() 恒真,环境态没法在测试里翻面。
32
+ export function authDirCandidates({ primary, explicit = "", testEnv = false, cwdDir = "" } = {}) {
33
+ const dirs = [primary];
34
+ if (!explicit && !testEnv && cwdDir && cwdDir !== primary) dirs.push(cwdDir);
35
+ return dirs;
36
+ }
37
+
38
+ // 读取候选(迁移期兼容):主位置优先,历史 `cwd()/auths` 只读兜底。
39
+ // 显式 WORKBUDDY_AUTH_DIR 或测试环境不兜底:既避免测试扫到真实目录,也避免重新引入 cwd 依赖。
40
+ export function resolveAuthDirs() {
41
+ return authDirCandidates({
42
+ primary: resolveAuthDir(),
43
+ explicit: process.env.WORKBUDDY_AUTH_DIR || "",
44
+ testEnv: isTestEnv(),
45
+ cwdDir: join(process.cwd(), "auths"),
46
+ });
47
+ }
48
+
49
+ // 按 uid 定位账号文件(主位置优先,旧位置兜底);找不到返回 null。
50
+ export function findAccountFile(uid) {
51
+ for (const dir of resolveAuthDirs()) {
52
+ const fp = join(dir, `workbuddy-${uid}.json`);
53
+ if (existsSync(fp)) return fp;
54
+ }
55
+ return null;
56
+ }
57
+
58
+ // 扫描账号文件(主位置先扫,同 uid 以主位置为准),返回已校验的凭证行。
59
+ // 读取侧单一真相:provider 构造、CLI 账号加载、token-auto 都走这里,别再各自 readdir 一套。
60
+ export function listAccountDocs({ dirs } = {}) {
61
+ const out = [];
62
+ const seen = new Set();
63
+ for (const dir of dirs || resolveAuthDirs()) {
64
+ let files = [];
65
+ try {
66
+ if (!existsSync(dir)) continue;
67
+ files = readdirSync(dir).filter((f) => f.startsWith("workbuddy-") && f.endsWith(".json"));
68
+ } catch { continue; }
69
+ for (const f of files) {
70
+ try {
71
+ const doc = JSON.parse(readFileSync(join(dir, f), "utf8"));
72
+ const uid = doc?.account?.uid;
73
+ if (!uid || !doc?.auth?.accessToken) continue;
74
+ if (seen.has(String(uid))) continue;
75
+ seen.add(String(uid));
76
+ out.push({ uid: String(uid), file: join(dir, f), dir, doc });
77
+ } catch {}
78
+ }
79
+ }
80
+ return out;
15
81
  }
16
82
 
17
83
  function jwtExp(token) {
@@ -102,7 +102,7 @@ export function createChatService({
102
102
 
103
103
  // SDK 通道(缺省启用):缺省底层走 @ai-sdk/openai-compatible,`legacy`/关闭词
104
104
  // (局部 MSLXDFF_WORKBUDDY_SDK,未设则继承全局 MSLXDFF_UPSTREAM_ENGINE)回退原生 transport;
105
- // 不可用(Node16/未安装)自动回退并告警一次。上层轮换/刷新/reshape 全链复用。
105
+ // 不可用(未安装)自动回退并告警一次。上层轮换/刷新/reshape 全链复用。
106
106
  // Note: 为什么默认 SDK、翻译层代价与回退语义 — 见 .agents/notes/implemented/feature/2026-09-12-workbuddy-sdk-channel.md
107
107
  // 上游非标字段(thinking / reasoning_effort / reasoning_content / tool_choice 字符串)不依赖 SDK 白名单:
108
108
  // payload.js 前置语义改写 + AI SDK providerOptions 透传(非白名单 key 原样 spread 进 body,2.0.41 实测)。
@@ -1,11 +1,9 @@
1
1
  import { createKeyRing } from "../keyring.js";
2
2
  import { loadProviderKeys, loadProviderAuths, loadProviderBaseUrl, WORKBUDDY_DEFAULT_BASE_URL, loadProviderModelsPath, loadProviderChatPath } from "../../state.js";
3
- import { existsSync, readdirSync, readFileSync } from "node:fs";
4
3
  import { compatFetch } from "../../compat.js";
5
- import { join } from "node:path";
6
4
  import { envInt, joinUrl, getUndici, createAgent } from "../base.js";
7
5
  import { createAuthService, isAuthError, isInsufficientStatus, decodeJwtExp } from "./auth.js";
8
- import { applyTokenRefresh, resolveAuthDir } from "./account-store.js";
6
+ import { applyTokenRefresh, listAccountDocs } from "./account-store.js";
9
7
  import { createChatService } from "./chat.js";
10
8
  import { createModelsService } from "./models.js";
11
9
  import { createBalanceCache, getCachedBalance as defaultGetCached, setCachedBalance as defaultSetCached } from "./balance.js";
@@ -61,24 +59,16 @@ export function createWorkbuddyProvider({
61
59
  let authList = Array.isArray(auths) && auths.length ? auths : authsFromState;
62
60
 
63
61
  if (!authList.length && !keys.length) {
62
+ // 账号目录读取统一走 listAccountDocs:主位置(跟 state 走)优先,旧 cwd/auths 只读兜底
64
63
  try {
65
- const authDir = resolveAuthDir();
66
- if (existsSync(authDir)) {
67
- const files = readdirSync(authDir).filter((f) => f.startsWith("workbuddy-") && f.endsWith(".json"));
68
- for (const f of files) {
69
- try {
70
- const j = JSON.parse(readFileSync(join(authDir, f), "utf8"));
71
- if (j?.auth?.accessToken && j?.account?.uid) {
72
- if (!keys.includes(j.auth.accessToken)) keys.push(j.auth.accessToken);
73
- authList.push({
74
- uid: j.account.uid,
75
- domain: j.auth.domain || "www.codebuddy.cn",
76
- enterpriseId: j.account.enterpriseId || "",
77
- refreshToken: j.auth.refreshToken || "",
78
- });
79
- }
80
- } catch {}
81
- }
64
+ for (const { uid, doc } of listAccountDocs()) {
65
+ if (!keys.includes(doc.auth.accessToken)) keys.push(doc.auth.accessToken);
66
+ authList.push({
67
+ uid,
68
+ domain: doc.auth.domain || "www.codebuddy.cn",
69
+ enterpriseId: doc.account.enterpriseId || "",
70
+ refreshToken: doc.auth.refreshToken || "",
71
+ });
82
72
  }
83
73
  } catch {}
84
74
  }
@@ -1,8 +1,9 @@
1
- // readline 兼容层:`node:readline/promises` 需要 Node 17+,老 Node(如 VPS 的 16)
2
- // 会 `ERR_UNKNOWN_BUILTIN_MODULE` 直接崩在 import 期,连人话报错都来不及打。
3
- // 这里只用回调版 `node:readline`(Node 12+ 全有),把 `question` 包成 Promise;
4
- // `prompt()/close()/for await` 回调版原生即有,对我们用到的部分行为一致。
1
+ // readline 统一走回调版 `node:readline`(question 包成 Promise),
2
+ // 顺带给 -chat 一个人话版本门(项目要求 Node >=18,ADR-0024)。
3
+ // 保留理由:回调版在 18+ 行为一致且无 import 期风险(`node:readline/promises`
4
+ // 若被误用于旧运行时会直接 ERR_UNKNOWN_BUILTIN_MODULE,连报错都来不及打)。
5
5
  import readline from "node:readline";
6
+ import { MIN_NODE_MAJOR, nodeMajor } from "./compat.js";
6
7
 
7
8
  export function createInterface(opts) {
8
9
  const rl = readline.createInterface(opts);
@@ -11,13 +12,11 @@ export function createInterface(opts) {
11
12
  return rl;
12
13
  }
13
14
 
14
- export function nodeMajor() {
15
- return Number(String(process.versions?.node || "0").split(".")[0]) || 0;
16
- }
15
+ export { nodeMajor };
17
16
 
18
- // -chat 依赖 fetch(<18 由 src/compat.js 用 undici polyfill),低于 engines 下限
19
- // 直接给一句人话。返回 true=通过,false=已打印升级指引。
20
- export function assertChatNode({ min = 16 } = {}) {
17
+ // -chat 版本门:项目要求 Node >=18(ADR-0024),不满足直接给人话 + 升级指引。
18
+ // 返回 true=通过,false=已打印升级指引。
19
+ export function assertChatNode({ min = MIN_NODE_MAJOR } = {}) {
21
20
  const major = nodeMajor();
22
21
  if (major >= min) return true;
23
22
  console.error(`Node 版本过旧(当前 v${process.versions.node}),-chat 需要 Node ${min}+(推荐 20+)。`);
@@ -15,7 +15,17 @@ export function isCodexModelsCaller(req) {
15
15
  return /(^|&)client_version=/.test(q);
16
16
  }
17
17
 
18
- export async function modelsHandler({ req, res, models, plugins, capabilities, wbSource }) {
18
+ // 勾选即对外目录:modelPicks 非空时 /v1/models 只暴露勾选的模型(与 -models 交互勾选语义闭环);
19
+ // 空勾选 = 不过滤(保留旧默认全量目录,防呆:新装/清空勾选不至于对外空目录)。?all=1 逃生门。
20
+ // Note: 空勾选不裁剪 + ?all=1 逃生门是硬边界(缺一即成单向棘轮/下游零模型)— 见 .agents/notes/implemented/architecture/2026-09-21-models-list-scoped-by-picks.md
21
+ export function filterByPicks(data, picks) {
22
+ if (!picks?.length) return data;
23
+ const set = new Set(picks.map(String));
24
+ const list = (Array.isArray(data?.data) ? data.data : []).filter((m) => set.has(String(m?.id || "")));
25
+ return { ...(data || { object: "list" }), data: list };
26
+ }
27
+
28
+ export async function modelsHandler({ req, res, models, plugins, capabilities, wbSource, loadPicks } = {}) {
19
29
  if (!models) return json(res, 501, { error: "Models service not configured" });
20
30
  const codex = isCodexModelsCaller(req);
21
31
  const withCodex = (out) => (codex && out && typeof out === "object" ? { ...out, models: [] } : out);
@@ -30,6 +40,14 @@ export async function modelsHandler({ req, res, models, plugins, capabilities, w
30
40
  data = await mergeModelsList(data, { capsSvc: capabilities, wbSource });
31
41
  } catch { /* 富化失败不阻塞 /models */ }
32
42
  }
43
+ // 勾选即对外目录(?all=1 逃生门):picks 非空只暴露勾选项;空 picks 不过滤
44
+ if (!/(^|&)all=1(&|$)/.test(qs)) {
45
+ let picks = [];
46
+ try {
47
+ picks = (loadPicks ? await loadPicks() : (await import("../state.js")).loadModelPicks()) || [];
48
+ } catch { picks = []; }
49
+ data = filterByPicks(data, picks);
50
+ }
33
51
  // 插件 hook:models:list — 返回数组可替换对外模型列表({object:"list",data:[...]} 或纯 id 数组)
34
52
  if (plugins?.length) {
35
53
  const ml = await runHook(plugins, "models:list", { data });
@@ -27,6 +27,16 @@ export async function startDaemonMain(VERSION) {
27
27
  try { logs.appendEvent({ ts: Date.now(), type: "workbuddy-checkin-setup-failed", error: String(e?.message || e).slice(0, 200) }); } catch {}
28
28
  });
29
29
 
30
+ const { setupTraeworkCheckin } = await import("./traework-checkin.js");
31
+ void setupTraeworkCheckin({ bus, logs }).catch((e) => {
32
+ try { logs.appendEvent({ ts: Date.now(), type: "traework-checkin-setup-failed", error: String(e?.message || e).slice(0, 200) }); } catch {}
33
+ });
34
+
35
+ const { setupQoderCheckin } = await import("./qoder-checkin.js");
36
+ void setupQoderCheckin({ bus, logs }).catch((e) => {
37
+ try { logs.appendEvent({ ts: Date.now(), type: "qoder-checkin-setup-failed", error: String(e?.message || e).slice(0, 200) }); } catch {}
38
+ });
39
+
30
40
  const { setupWorkbuddyGrowth } = await import("./workbuddy-growth.js");
31
41
  void setupWorkbuddyGrowth({ bus, logs }).catch((e) => {
32
42
  try { logs.appendEvent({ ts: Date.now(), type: "workbuddy-growth-setup-failed", error: String(e?.message || e).slice(0, 200) }); } catch {}
@@ -0,0 +1,30 @@
1
+ // 定制 provider 启用门禁(纯函数 + auth 号型名单),从 providers-setup.js 拆出以守住 ≤10KB 体积门。
2
+ // 背景:门禁曾要求 `baseUrl && keys` 两者皆有,导致 qoder(keys 有、baseUrl 空)被静默跳过 ——
3
+ // 表现为 /v1/models 无该供应商模型、`-models` 挑不到、网关报 `Model qoder/x is not supported`。
4
+ // Note: auth 号型不要求 baseUrl 是硬约束(旧门禁 baseUrl&&keys 曾静默跳过整家 qoder)— 见 .agents/notes/implemented/bug-fix/2026-09-21-auth-doc-provider-gate.md
5
+
6
+ /** 凭证在 auth 目录、无需 baseUrl 的「auth 号型」定制供应商(端点由各自 constants 决定)。 */
7
+ export const AUTH_DOC_PROVIDER_IDS = ["workbuddy", "traework", "qoder"];
8
+
9
+ /**
10
+ * - auth 号型:keys 或 auths 或 auth 目录有号 → 启用(baseUrl 可空)
11
+ * - 其他定制(cline/codearts/插件注册):有 keys → 启用(baseUrl 可空,端点由工厂决定)
12
+ */
13
+ export function shouldEnableCustomProvider(gid, { keys = [], auths = [], hasAuthDocs = false } = {}) {
14
+ if (AUTH_DOC_PROVIDER_IDS.includes(gid)) return keys.length > 0 || auths.length > 0 || hasAuthDocs;
15
+ return keys.length > 0;
16
+ }
17
+
18
+ /** 按 gid 载入各自 account-store 并列出落盘账号(失败/无目录 → 空数组,不抛)。 */
19
+ export async function loadAuthDocs(gid) {
20
+ try {
21
+ const store = gid === "workbuddy"
22
+ ? await import("../providers/workbuddy/account-store.js")
23
+ : gid === "traework"
24
+ ? await import("../providers/traework/account-store.js")
25
+ : await import("../providers/qoder/account-store.js");
26
+ return store.listAccountDocs();
27
+ } catch {
28
+ return [];
29
+ }
30
+ }
@@ -14,11 +14,22 @@ import { loadProviderKeys, loadProviderAuths, loadProviderConfigs } from "../sta
14
14
  import { refreshIntervalMs, modelCooldownMs, slowCooldownMs, peerCooldownMs, peerLimitCooldownMs, peerHeatMs, banWindowMs, banThreshold } from "../cli/policy.js";
15
15
  import { errMsg } from "../cli/util.js";
16
16
 
17
+
18
+ import { shouldEnableCustomProvider, loadAuthDocs } from "./provider-gate.js";
17
19
  /**
18
20
  * 世界组装 — 插件加载 → providers → models/auto/peers/groups/bans。
19
21
  * 无参(读 env/state 自身),返回 ctx 由门面接力 server-lifecycle。
20
22
  */
21
23
  export async function setupProviders() {
24
+ // 供应商 id 统一(旧 cline id → cline):必须在实例化任何 provider 之前跑;幂等,失败不阻断启动
25
+ try {
26
+ const { runStateMigrations } = await import("../state/migrations.js");
27
+ const mig = await runStateMigrations({ file: defaultStateFile() });
28
+ if (mig.applied.length) console.log(`state migrations applied: ${mig.applied.join(", ")}`);
29
+ for (const e of mig.errors) console.log(`state migration failed: ${e.id} — ${e.error}`);
30
+ } catch (err) {
31
+ console.log(`state migrations failed: ${errMsg(err)}`);
32
+ }
22
33
  const { token, created } = await loadToken();
23
34
  const pkgRoot2 = join(dirname(fileURLToPath(import.meta.url)), "..", "..");
24
35
  const pluginDirs = resolvePluginDirs({ pkgRoot: pkgRoot2 });
@@ -76,12 +87,16 @@ export async function setupProviders() {
76
87
  const { getCustomProviderFactory } = await import("../providers/registry.js");
77
88
  const customFactory = await getCustomProviderFactory(gid, base);
78
89
  if (customFactory) {
79
- if (gid === "workbuddy" && !keys.length) continue;
80
- if (gid !== "workbuddy" && (!base || !keys.length)) continue;
90
+ // auth 号型(workbuddy/traework/qoder):无 keys 但 auth 目录有号也启用;baseUrl 可空
91
+ // (端点由各自 constants 决定,如 qoder 的 qoder://native)。
92
+ const hasAuthDocs = !keys.length && !auths.length ? (await loadAuthDocs(gid)).length > 0 : false;
93
+ if (!shouldEnableCustomProvider(gid, { keys, auths, hasAuthDocs })) continue;
81
94
  try {
82
95
  const provider = gid === "workbuddy"
83
96
  ? await customFactory({ baseUrl: base || "https://copilot.tencent.com", apiKeys: keys, auths })
84
- : await customFactory({ id: gid, baseUrl: base, apiKeys: keys });
97
+ : gid === "traework"
98
+ ? await customFactory({ baseUrl: base || "https://trae-api-cn.mchost.guru", apiKeys: keys, auths })
99
+ : await customFactory({ id: gid, baseUrl: base, apiKeys: keys });
85
100
  providers.push(provider);
86
101
  console.log(`provider enabled: ${gid} (${keys.length} key${keys.length > 1 ? "s" : ""}) baseUrl=${base || provider.baseUrl} [custom]`);
87
102
  appendEvent({ ts: Date.now(), type: "provider-enabled", provider: gid, keys: keys.length, baseUrl: base || provider.baseUrl });