mslxdff 0.1.231 → 0.1.233

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 (117) hide show
  1. package/bin/mslxdff.js +3 -3
  2. package/package.json +1 -1
  3. package/src/auto.js +264 -254
  4. package/src/bench/cline-bench.js +42 -42
  5. package/src/bench/probe.js +70 -70
  6. package/src/bench/report.js +162 -162
  7. package/src/bench/runner.js +77 -77
  8. package/src/bench/via-probe.js +124 -124
  9. package/src/bench/via-routes.js +87 -87
  10. package/src/bench/workbuddy-bench.js +54 -54
  11. package/src/chat/engine.js +160 -160
  12. package/src/chat/gateway.js +163 -163
  13. package/src/chat/orchestrator.js +234 -234
  14. package/src/chat/prompt.js +70 -70
  15. package/src/chat/repl.js +88 -88
  16. package/src/chat/terminal.js +135 -135
  17. package/src/chat/tools.js +306 -306
  18. package/src/chat-pipeline/index.js +155 -145
  19. package/src/chat-pipeline/policy.js +84 -84
  20. package/src/cli/commands/group.js +266 -266
  21. package/src/cli/commands/model/picks.js +50 -50
  22. package/src/cli/commands/provider/bench-via.js +247 -247
  23. package/src/cli/commands/provider/bench.js +141 -141
  24. package/src/cli/commands/provider/index.js +139 -139
  25. package/src/cli/commands/provider/models.js +149 -149
  26. package/src/cli/commands/provider/qwenwork-login.js +119 -119
  27. package/src/cli/commands/provider/raccoon-quota.js +156 -156
  28. package/src/cli/commands/sync.js +310 -310
  29. package/src/cli/status.js +279 -279
  30. package/src/daemon.js +246 -246
  31. package/src/model-capabilities/enrich.js +86 -86
  32. package/src/model-capabilities/index.js +183 -183
  33. package/src/model-capabilities/parse.js +70 -70
  34. package/src/model-trace.js +10 -2
  35. package/src/models.js +292 -225
  36. package/src/peers.js +345 -345
  37. package/src/providers/agentrouter.js +238 -238
  38. package/src/providers/base.js +164 -164
  39. package/src/providers/cline/auth.js +228 -228
  40. package/src/providers/cline/chat.js +307 -307
  41. package/src/providers/cline.js +2 -2
  42. package/src/providers/codearts/chat.js +207 -207
  43. package/src/providers/dispatcher.js +147 -147
  44. package/src/providers/globalqwenwork/cosy.js +123 -123
  45. package/src/providers/globalqwenwork/crypto.js +218 -218
  46. package/src/providers/globalqwenwork/http.js +20 -20
  47. package/src/providers/globalqwenwork/rsa.js +54 -54
  48. package/src/providers/globalqwenwork/sse.js +268 -268
  49. package/src/providers/globalqwenwork/stream.js +130 -130
  50. package/src/providers/globalqwenwork/upstream.js +122 -122
  51. package/src/providers/keyring.js +60 -60
  52. package/src/providers/qoder/account-store.js +18 -0
  53. package/src/providers/qoder/chat.js +292 -203
  54. package/src/providers/qoder/checkin.js +25 -12
  55. package/src/providers/qoder/index.js +311 -249
  56. package/src/providers/qoder/models.js +64 -2
  57. package/src/providers/qoder/oauth.js +42 -1
  58. package/src/providers/qoder/payload.js +116 -80
  59. package/src/providers/qoder/request.js +7 -2
  60. package/src/providers/qoder/session.js +26 -10
  61. package/src/providers/qoder/sse.js +233 -145
  62. package/src/providers/qoder/sticky.js +5 -2
  63. package/src/providers/qoder/wire-messages.js +242 -0
  64. package/src/providers/qwenwork/account-store.js +133 -133
  65. package/src/providers/qwenwork/constants.js +67 -67
  66. package/src/providers/qwenwork/cosy.js +120 -120
  67. package/src/providers/qwenwork/crypto.js +218 -218
  68. package/src/providers/qwenwork/http.js +20 -20
  69. package/src/providers/qwenwork/index.js +337 -337
  70. package/src/providers/qwenwork/payload.js +144 -144
  71. package/src/providers/qwenwork/rsa.js +54 -54
  72. package/src/providers/qwenwork/sse.js +268 -268
  73. package/src/providers/qwenwork/stream.js +130 -130
  74. package/src/providers/qwenwork/upstream.js +120 -120
  75. package/src/providers/qwenwork.js +1 -1
  76. package/src/providers/raccoon/chat.js +1 -1
  77. package/src/providers/raccoon/const.js +219 -202
  78. package/src/providers/raccoon/credits.js +285 -285
  79. package/src/providers/raccoon/index.js +15 -2
  80. package/src/providers/raccoon/models.js +68 -17
  81. package/src/providers/registry.js +89 -89
  82. package/src/providers/workbuddy/auth.js +16 -0
  83. package/src/providers/workbuddy/chat.js +298 -264
  84. package/src/providers/workbuddy/client-headers.js +148 -0
  85. package/src/providers/workbuddy/desktop-info.js +17 -1
  86. package/src/providers/workbuddy/models.js +8 -20
  87. package/src/providers/workbuddy/reshape.js +152 -152
  88. package/src/providers/workbuddy.js +2 -2
  89. package/src/reasoning.js +32 -32
  90. package/src/routes/chat/broadband-handler.js +80 -80
  91. package/src/routes/chat/gateway.js +46 -46
  92. package/src/routes/chat/relay-pipeline.js +282 -282
  93. package/src/routes/chat/via-route-handler.js +156 -156
  94. package/src/routes/hedge.js +260 -260
  95. package/src/routes/models-route.js +167 -167
  96. package/src/routes/peers.js +373 -373
  97. package/src/routes/stream.js +364 -347
  98. package/src/runtime/bootstrap.js +75 -75
  99. package/src/runtime/keep-warm.js +135 -135
  100. package/src/runtime/provider-gate.js +75 -75
  101. package/src/runtime/providers-setup.js +187 -187
  102. package/src/runtime/raccoon-checkin.js +175 -175
  103. package/src/server.js +85 -85
  104. package/src/state/schemas/allowlist.js +92 -92
  105. package/src/sync-opencode.js +280 -280
  106. package/src/transport/index.js +244 -244
  107. package/src/transport/pool.js +56 -56
  108. package/src/transport/retry.js +24 -24
  109. package/src/transport/sse.js +93 -93
  110. package/src/upstream-probe/display.js +52 -52
  111. package/src/upstream-probe/probe.js +49 -49
  112. package/src/upstream-probe/rotate.js +110 -110
  113. package/src/upstream-probe/start.js +45 -45
  114. package/src/upstream.js +355 -355
  115. package/docs/cli_help_mini.md +0 -148
  116. package/src/providers/AGENTS.md +0 -49
  117. package/src/routes/AGENTS.md +0 -37
@@ -0,0 +1,242 @@
1
+ // qoder 线格式消息转换层:OpenAI/Anthropic messages → Qoder 原生 wire messages。
2
+ // 从 payload.js 拆出(体积门 + 职责边界:这里只管"消息形状/配对/缓存标记/会话指纹",不管请求体装配)。
3
+ // 关键事实(2026-10-22 取证,对齐官方 qodercli + dsh-qoder-connect + magpie qoder 插件):
4
+ // - messages 必须发 Qoder 原生形状(user 带 contents[]、assistant 回传 reasoning_content、tool 带 tool_call_id);
5
+ // - 注入官方 Qoder agent 人格(缓存稳定种子);末个可缓存块打 cache_control:ephemeral(Anthropic 系只缓存标记段);
6
+ // - session_id 按对话稳定(同对话跨请求命中上游 prompt 缓存);
7
+ // - tool 配对修复:上游对「孤儿 tool result」整体拒收,须补派生 id / 防抢占认领 / 降级不丢内容。
8
+ import { createHash } from "node:crypto";
9
+
10
+ export function blankResponseMeta() {
11
+ return {
12
+ id: "",
13
+ usage: {
14
+ prompt_tokens: 0, completion_tokens: 0, total_tokens: 0,
15
+ completion_tokens_details: { reasoning_tokens: 0 },
16
+ prompt_tokens_details: { cached_tokens: 0 },
17
+ },
18
+ };
19
+ }
20
+
21
+ // OpenAI user 消息 → Qoder contents 形状
22
+ export function buildUserMessage(text) {
23
+ return {
24
+ role: "user", content: "",
25
+ contents: [{ type: "text", text: String(text || "") }],
26
+ response_meta: blankResponseMeta(),
27
+ reasoning_content_signature: "",
28
+ };
29
+ }
30
+
31
+ export function buildStructuredMessage(role, text) {
32
+ return { role, content: String(text || ""), response_meta: blankResponseMeta(), reasoning_content_signature: "" };
33
+ }
34
+
35
+ export function normalizeContent(content) {
36
+ if (typeof content === "string") return content;
37
+ if (content == null) return "";
38
+ if (Array.isArray(content)) {
39
+ let out = "";
40
+ for (const b of content) {
41
+ const t = typeof b === "object" && b ? b.text : b;
42
+ if (typeof t === "string") out += (out ? "\n\n" : "") + t;
43
+ }
44
+ return out;
45
+ }
46
+ return JSON.stringify(content);
47
+ }
48
+
49
+ // 单段 image → Qoder image_url part;拿不到可用 url 返回 null
50
+ function imagePartToUrl(b) {
51
+ // OpenAI 形状 {type:'image_url', image_url:{url}}
52
+ const direct = typeof b.image_url === "string" ? b.image_url : b.image_url?.url;
53
+ if (typeof direct === "string" && direct) return { type: "image_url", image_url: { url: direct } };
54
+ // Anthropic 形状 {type:'image', source:{type:'base64'|'url', media_type, data|url}}
55
+ const src = b.source;
56
+ if (src && typeof src === "object") {
57
+ if (src.type === "url" && typeof src.url === "string" && src.url)
58
+ return { type: "image_url", image_url: { url: src.url } };
59
+ if (src.type === "base64" && src.data && src.media_type)
60
+ return { type: "image_url", image_url: { url: `data:${src.media_type};base64,${src.data}` } };
61
+ }
62
+ return null;
63
+ }
64
+
65
+ // 官方会话人格(对齐 qoder-agent-sdk:magpie 插件同款):人格行前置、客户端 system 拼后。
66
+ // 固定前缀让上游 prompt 缓存跨轮命中,也让模型身份口径稳定(不再自称别名)。
67
+ const QODER_SYS = "You are a Qoder agent. Use the instructions below and the tools available to you to assist the user.";
68
+ export function withQoderPersona(systemText) {
69
+ const s = String(systemText || "").trim();
70
+ return s ? `${QODER_SYS}\n\n${s}` : QODER_SYS;
71
+ }
72
+
73
+ // tool 配对修复(对齐 magpie #277/#290/#359,实测上游行为):
74
+ // - 上游对「tool 行没有应答它前面 assistant 的某个 call」的请求整体拒绝
75
+ // ("Messages with role 'tool' must be a response to a preceding message with 'tool_calls'"),
76
+ // 客户端丢 id / 上下文压缩截断都会造成孤儿 result → 整个 turn 被枪毙(表现为莫名的 400/空 turn)。
77
+ // - 修法:call 缺 id 按内容派生稳定 id(同 turn 内跨请求一致,不破坏上游缓存);
78
+ // 孤儿 result 认领「其后没有任何 result 点名的那个未应答 call」(防孤儿抢占合法点名),认领不到降级 user 文本。
79
+ function callIdFor(at, k, c) {
80
+ const said = typeof c.function?.arguments === "string" ? c.function.arguments : JSON.stringify(c.function?.arguments ?? {});
81
+ return "call_" + createHash("sha256").update(`${at}\0${k}\0${c.function?.name ?? ""}\0${said}`).digest("hex").slice(0, 32);
82
+ }
83
+
84
+ // 从 from 起收集其后所有 result 行点名的 id(user 的 tool_result 数组形态与 tool 行都算),
85
+ // 直到下一个 assistant/user 文本行为止——这是"合法点名预约表",孤儿认领时须避开。
86
+ function namedAhead(incoming, from) {
87
+ const names = new Set();
88
+ for (let j = from; j < incoming.length; j++) {
89
+ const m = incoming[j];
90
+ if (!m) continue;
91
+ if (m.role === "tool") { if (m.tool_call_id) names.add(String(m.tool_call_id)); continue; }
92
+ if (m.role === "user" && Array.isArray(m.content) && m.content[0]?.type === "tool_result") {
93
+ for (const b of m.content) if (b?.type === "tool_result" && b.tool_use_id) names.add(String(b.tool_use_id));
94
+ continue;
95
+ }
96
+ break; // 碰到别的角色(assistant 新 turn / user 真话)即停
97
+ }
98
+ return names;
99
+ }
100
+
101
+ // OpenAI messages → Qoder 原生 wire messages(官方形状:user=contents[];assistant=content+tool_calls+reasoning_content)
102
+ export function toQoderWireMessages(incoming) {
103
+ const msgs = incoming || [];
104
+ const out = [];
105
+ let pendingCallIds = []; // 最近一个 assistant 轮里尚未被 result 应答的 call ids(按发出顺序)
106
+ let systemText = "";
107
+ const claimOrDemote = (id, at) => {
108
+ const sid = String(id || "");
109
+ if (sid && pendingCallIds.includes(sid)) {
110
+ pendingCallIds = pendingCallIds.filter((p) => p !== sid);
111
+ return { ok: true, id: sid };
112
+ }
113
+ // 客户端丢了 id / 点名了对不上的 id:认领「后面没人点名」的第一个未应答 call
114
+ const reserved = namedAhead(msgs, at);
115
+ const freeAt = pendingCallIds.findIndex((p) => !reserved.has(p));
116
+ if (freeAt >= 0) return { ok: true, id: pendingCallIds.splice(freeAt, 1)[0] };
117
+ return { ok: false };
118
+ };
119
+ for (let i = 0; i < msgs.length; i++) {
120
+ const m = msgs[i];
121
+ if (!m || typeof m !== "object") continue;
122
+ const text = normalizeContent(m.content);
123
+ if (m.role === "system") {
124
+ if (text) systemText = systemText ? systemText + "\n\n" + text : text;
125
+ continue;
126
+ }
127
+ if (m.role === "user") {
128
+ pendingCallIds = [];
129
+ // Anthropic tool_result 数组形态 → tool 行(翻译层产物)
130
+ if (Array.isArray(m.content) && m.content[0]?.type === "tool_result") {
131
+ for (const b of m.content) {
132
+ if (b?.type !== "tool_result") continue;
133
+ const rt = normalizeContent(b.content);
134
+ const claimed = claimOrDemote(b.tool_use_id || "", i);
135
+ if (claimed.ok) out.push({ role: "tool", tool_call_id: claimed.id, content: rt });
136
+ else out.push(buildUserMessage(rt)); // 无路可认领 → 降级 user 文本,内容不丢
137
+ }
138
+ continue;
139
+ }
140
+ if (Array.isArray(m.content)) {
141
+ const parts = [];
142
+ for (const b of m.content) {
143
+ if (!b || typeof b !== "object") continue;
144
+ if (b.type === "text" && typeof b.text === "string" && b.text) { parts.push({ type: "text", text: b.text }); continue; }
145
+ if (b.type === "image" || b.type === "image_url") {
146
+ const p = imagePartToUrl(b);
147
+ if (p) { parts.push(p); continue; }
148
+ }
149
+ const t = typeof b.text === "string" ? b.text : null;
150
+ if (t) parts.push({ type: "text", text: t });
151
+ }
152
+ if (!parts.length && text) parts.push({ type: "text", text });
153
+ out.push({
154
+ role: "user", content: "",
155
+ contents: parts.length ? parts : [{ type: "text", text: "" }],
156
+ response_meta: blankResponseMeta(),
157
+ reasoning_content_signature: "",
158
+ });
159
+ continue;
160
+ }
161
+ out.push(buildUserMessage(text));
162
+ continue;
163
+ }
164
+ if (m.role === "assistant") {
165
+ const hasToolCalls = Array.isArray(m.tool_calls) && m.tool_calls.length > 0;
166
+ if (!text && !hasToolCalls && !m.reasoning_content) continue; // 空气泡不发
167
+ pendingCallIds = [];
168
+ const msg = buildStructuredMessage("assistant", text || " ");
169
+ if (hasToolCalls) {
170
+ msg.tool_calls = m.tool_calls.map((c, k) => (c?.id ? c : { ...c, id: callIdFor(i, k, c || {}) }));
171
+ pendingCallIds = msg.tool_calls.map((c) => c.id);
172
+ }
173
+ if (m.reasoning_content) msg.reasoning_content = String(m.reasoning_content);
174
+ out.push(msg);
175
+ continue;
176
+ }
177
+ if (m.role === "tool") {
178
+ const claimed = claimOrDemote(m.tool_call_id || "", i);
179
+ if (claimed.ok) {
180
+ const msg = buildStructuredMessage("tool", text);
181
+ msg.tool_call_id = claimed.id;
182
+ if (m.name) msg.name = m.name;
183
+ out.push(msg);
184
+ } else {
185
+ out.push(buildUserMessage(text)); // 孤儿 result → user 文本(上游会整体拒绝 tool 行)
186
+ }
187
+ continue;
188
+ }
189
+ if (text) out.push(buildStructuredMessage(m.role || "user", text));
190
+ }
191
+ // system 人格前置一条(客户端无 system 时也给官方人格,对齐上游客户端行为)
192
+ out.unshift(buildStructuredMessage("system", withQoderPersona(systemText)));
193
+ return out;
194
+ }
195
+
196
+ // 会话指纹(prompt 缓存亲和的关键,对齐 magpie #290):同一对话(system + 首个 user 相同)
197
+ // 恒定同名 → 上游命中会话缓存,首包显著变快;不同对话各不相同。客户端的 key 原文绝不出门。
198
+ export function conversationSessionId(userId, modelKey, wireMessages) {
199
+ const sys = [];
200
+ let firstUser = "";
201
+ for (const m of wireMessages) {
202
+ if (m?.role === "system" && m.content) sys.push(String(m.content));
203
+ if (m?.role === "user") {
204
+ firstUser = JSON.stringify(m.contents || m.content || "");
205
+ break;
206
+ }
207
+ }
208
+ const h = createHash("sha256");
209
+ h.update("qoder-conv");
210
+ h.update("\0");
211
+ h.update(String(userId || ""));
212
+ h.update("\0");
213
+ h.update(String(modelKey || ""));
214
+ h.update("\0");
215
+ h.update(JSON.stringify([sys, firstUser]));
216
+ const hex = h.digest("hex");
217
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-4${hex.slice(13, 16)}-a${hex.slice(17, 20)}-${hex.slice(20, 32)}`;
218
+ }
219
+
220
+ // cache_control 标记(对齐 qoder-agent-sdk 真值,magpie #290):最后一个 user 轮里
221
+ // 最后一个非工具块打 ephemeral;末轮是 tool 结果时不标。Anthropic 系模型只缓存标记段,
222
+ // 不标 = 整段对话每次全价重算(慢且贵)。输入块原样不动,只加标记。
223
+ const UNCACHED_TYPES = new Set(["thinking", "redacted_thinking", "tool_use", "tool_result"]);
224
+ function findLastCacheable(blocks) {
225
+ for (let i = blocks.length - 1; i >= 0; i--) {
226
+ const t = blocks[i]?.type;
227
+ if (t && !UNCACHED_TYPES.has(t)) return i;
228
+ }
229
+ return -1;
230
+ }
231
+ export function cacheMark(wireMessages) {
232
+ const last = wireMessages[wireMessages.length - 1];
233
+ if (!last || last.role !== "user") return wireMessages;
234
+ const blocks = Array.isArray(last.contents) ? last.contents : null;
235
+ if (!blocks || !blocks.length) return wireMessages;
236
+ const idx = findLastCacheable(blocks);
237
+ if (idx < 0) return wireMessages;
238
+ const marked = blocks.slice();
239
+ marked[idx] = { ...marked[idx], cache_control: { type: "ephemeral" } };
240
+ wireMessages[wireMessages.length - 1] = { ...last, contents: marked };
241
+ return wireMessages;
242
+ }
@@ -1,133 +1,133 @@
1
- // qwenwork 账号落盘单一源:auths/qwenwork-<uid>.json(0600 tmp+rename)+ state.json 双写。
2
- // 照 src/providers/qwenwork/providers/qoder/account-store.js 的写法,命名空间独立(不得与 qoder 混)。
3
- import { writeFileSync, mkdirSync, existsSync, renameSync, unlinkSync, readdirSync, readFileSync, chmodSync } from "node:fs";
4
- import { join, dirname } from "node:path";
5
- import { tmpdir } from "node:os";
6
- import { isTestEnv, defaultStateFile } from "../../state/store.js";
7
-
8
- export function authDirFor({ explicit = "", testEnv = false, stateFile } = {}) {
9
- if (explicit) return explicit;
10
- if (testEnv) return join(tmpdir(), "mslxdff-test-auths");
11
- return join(dirname(stateFile), "auths");
12
- }
13
-
14
- export function resolveAuthDir() {
15
- return authDirFor({
16
- explicit: process.env.QWENWORK_AUTH_DIR || "",
17
- testEnv: isTestEnv(),
18
- stateFile: defaultStateFile(),
19
- });
20
- }
21
-
22
- export function resolveAuthDirs() {
23
- const primary = resolveAuthDir();
24
- const cwdDir = join(process.cwd(), "auths");
25
- return primary === cwdDir ? [primary] : [primary, cwdDir];
26
- }
27
-
28
- export function listAccountDocs({ dirs } = {}) {
29
- const out = [];
30
- const seen = new Set();
31
- for (const dir of dirs || resolveAuthDirs()) {
32
- let files = [];
33
- try {
34
- if (!existsSync(dir)) continue;
35
- files = readdirSync(dir).filter((f) => f.startsWith("qwenwork-") && f.endsWith(".json"));
36
- } catch { continue; }
37
- for (const f of files) {
38
- try {
39
- const doc = JSON.parse(readFileSync(join(dir, f), "utf8"));
40
- const uid = doc?.account?.uid;
41
- if (!uid || !doc?.auth?.accessToken) continue;
42
- if (seen.has(String(uid))) continue;
43
- seen.add(String(uid));
44
- out.push({ uid: String(uid), file: join(dir, f), dir, doc });
45
- } catch {}
46
- }
47
- }
48
- return out;
49
- }
50
-
51
- function writeAccountFile({ uid, accessToken, refreshToken = "", expiresAt = 0, name = "", email = "" }) {
52
- const authDir = resolveAuthDir();
53
- mkdirSync(authDir, { recursive: true });
54
- const doc = {
55
- account: { uid, name, email },
56
- auth: { accessToken, refreshToken, expiresAt },
57
- };
58
- const fp = join(authDir, `qwenwork-${uid}.json`);
59
- const tmp = `${fp}.tmp`;
60
- writeFileSync(tmp, JSON.stringify(doc, null, 2), { mode: 0o600 });
61
- try {
62
- if (existsSync(fp)) { try { chmodSync(fp, 0o600); } catch {} unlinkSync(fp); }
63
- renameSync(tmp, fp);
64
- } catch {
65
- writeFileSync(fp, JSON.stringify(doc, null, 2), { mode: 0o600 });
66
- try { unlinkSync(tmp); } catch {}
67
- }
68
- try { chmodSync(fp, 0o600); } catch {}
69
- return fp;
70
- }
71
-
72
- // state 落盘:keys[i] 存 JSON blob({device_token, refresh_token},对齐家族形状),auths 存 uid 索引行。
73
- async function persistAccounts(accounts, file) {
74
- const { loadProviderConfigs } = await import("../../state.js");
75
- const { saveProviderConfig } = await import("../../state.js");
76
- const cfg = (loadProviderConfigs(file ? { file } : {})?.qwenwork) || {};
77
- const keys = [...(Array.isArray(cfg.keys) ? cfg.keys : [])];
78
- const auths = [...(Array.isArray(cfg.auths) ? cfg.auths : [])];
79
- let updated = 0;
80
- for (const acc of accounts) {
81
- if (!acc?.uid || !acc?.accessToken) continue;
82
- const blob = JSON.stringify({ device_token: acc.accessToken, refresh_token: acc.refreshToken || "" });
83
- const row = { uid: acc.uid, name: acc.name || "", refreshToken: acc.refreshToken || "" };
84
- const idx = auths.findIndex((a) => String(a?.uid) === String(acc.uid));
85
- if (idx >= 0) { keys[idx] = blob; auths[idx] = row; updated += 1; }
86
- else { keys.push(blob); auths.push(row); }
87
- }
88
- // saveProviderConfig 起手保留 allowAnyModels(已在 state 层修复,见 ADR-0033 字段保留修复)
89
- saveProviderConfig("qwenwork", { baseUrl: cfg.baseUrl || "", keys, auths }, file ? { file } : {});
90
- return { keys, auths, updated };
91
- }
92
-
93
- export async function saveQwenworkAccount({ uid, accessToken, refreshToken = "", expiresAt = 0, name = "", email = "", file } = {}) {
94
- if (!uid || !accessToken) throw new Error("saveQwenworkAccount: 缺少 uid/accessToken");
95
- const fp = writeAccountFile({ uid, accessToken, refreshToken, expiresAt, name, email });
96
- const st = await persistAccounts([{ uid, accessToken, refreshToken, name }], file);
97
- return { file: fp, accounts: st.keys.length, updated: st.updated > 0 };
98
- }
99
-
100
- /** 回写刷新后的 token(上游每次 refresh 会轮换 refreshToken,必须落盘)。 */
101
- export async function updateQwenworkTokens({ uid, accessToken, refreshToken = "", expiresAt = 0, file } = {}) {
102
- if (!uid || !accessToken) throw new Error("updateQwenworkTokens: 缺少 uid/accessToken");
103
- const docs = listAccountDocs();
104
- const hit = docs.find((d) => String(d.uid) === String(uid));
105
- const prev = hit?.doc || {};
106
- const fp = writeAccountFile({
107
- uid,
108
- accessToken,
109
- refreshToken: refreshToken || prev?.auth?.refreshToken || "",
110
- expiresAt: expiresAt || 0,
111
- name: prev?.account?.name || "",
112
- email: prev?.account?.email || "",
113
- });
114
- await persistAccounts([{ uid, accessToken, refreshToken: refreshToken || prev?.auth?.refreshToken || "", name: prev?.account?.name || "" }], file);
115
- try {
116
- const { invalidateSession } = await import("./cosy.js");
117
- invalidateSession({ uid, accessToken: prev?.auth?.accessToken });
118
- } catch {}
119
- return { file: fp };
120
- }
121
-
122
- // 从 provider keys 的 blob 还原凭据(device_token/deviceToken 双形状兼容)
123
- export function accountFromBlob(blob) {
124
- try {
125
- const j = JSON.parse(String(blob || "{}"));
126
- const accessToken = String(j.device_token || j.accessToken || "");
127
- const refreshToken = String(j.refresh_token || j.refreshToken || "");
128
- if (!accessToken) return null;
129
- return { accessToken, refreshToken };
130
- } catch {
131
- return null;
132
- }
133
- }
1
+ // qwenwork 账号落盘单一源:auths/qwenwork-<uid>.json(0600 tmp+rename)+ state.json 双写。
2
+ // 照 src/providers/qwenwork/providers/qoder/account-store.js 的写法,命名空间独立(不得与 qoder 混)。
3
+ import { writeFileSync, mkdirSync, existsSync, renameSync, unlinkSync, readdirSync, readFileSync, chmodSync } from "node:fs";
4
+ import { join, dirname } from "node:path";
5
+ import { tmpdir } from "node:os";
6
+ import { isTestEnv, defaultStateFile } from "../../state/store.js";
7
+
8
+ export function authDirFor({ explicit = "", testEnv = false, stateFile } = {}) {
9
+ if (explicit) return explicit;
10
+ if (testEnv) return join(tmpdir(), "mslxdff-test-auths");
11
+ return join(dirname(stateFile), "auths");
12
+ }
13
+
14
+ export function resolveAuthDir() {
15
+ return authDirFor({
16
+ explicit: process.env.QWENWORK_AUTH_DIR || "",
17
+ testEnv: isTestEnv(),
18
+ stateFile: defaultStateFile(),
19
+ });
20
+ }
21
+
22
+ export function resolveAuthDirs() {
23
+ const primary = resolveAuthDir();
24
+ const cwdDir = join(process.cwd(), "auths");
25
+ return primary === cwdDir ? [primary] : [primary, cwdDir];
26
+ }
27
+
28
+ export function listAccountDocs({ dirs } = {}) {
29
+ const out = [];
30
+ const seen = new Set();
31
+ for (const dir of dirs || resolveAuthDirs()) {
32
+ let files = [];
33
+ try {
34
+ if (!existsSync(dir)) continue;
35
+ files = readdirSync(dir).filter((f) => f.startsWith("qwenwork-") && f.endsWith(".json"));
36
+ } catch { continue; }
37
+ for (const f of files) {
38
+ try {
39
+ const doc = JSON.parse(readFileSync(join(dir, f), "utf8"));
40
+ const uid = doc?.account?.uid;
41
+ if (!uid || !doc?.auth?.accessToken) continue;
42
+ if (seen.has(String(uid))) continue;
43
+ seen.add(String(uid));
44
+ out.push({ uid: String(uid), file: join(dir, f), dir, doc });
45
+ } catch {}
46
+ }
47
+ }
48
+ return out;
49
+ }
50
+
51
+ function writeAccountFile({ uid, accessToken, refreshToken = "", expiresAt = 0, name = "", email = "" }) {
52
+ const authDir = resolveAuthDir();
53
+ mkdirSync(authDir, { recursive: true });
54
+ const doc = {
55
+ account: { uid, name, email },
56
+ auth: { accessToken, refreshToken, expiresAt },
57
+ };
58
+ const fp = join(authDir, `qwenwork-${uid}.json`);
59
+ const tmp = `${fp}.tmp`;
60
+ writeFileSync(tmp, JSON.stringify(doc, null, 2), { mode: 0o600 });
61
+ try {
62
+ if (existsSync(fp)) { try { chmodSync(fp, 0o600); } catch {} unlinkSync(fp); }
63
+ renameSync(tmp, fp);
64
+ } catch {
65
+ writeFileSync(fp, JSON.stringify(doc, null, 2), { mode: 0o600 });
66
+ try { unlinkSync(tmp); } catch {}
67
+ }
68
+ try { chmodSync(fp, 0o600); } catch {}
69
+ return fp;
70
+ }
71
+
72
+ // state 落盘:keys[i] 存 JSON blob({device_token, refresh_token},对齐家族形状),auths 存 uid 索引行。
73
+ async function persistAccounts(accounts, file) {
74
+ const { loadProviderConfigs } = await import("../../state.js");
75
+ const { saveProviderConfig } = await import("../../state.js");
76
+ const cfg = (loadProviderConfigs(file ? { file } : {})?.qwenwork) || {};
77
+ const keys = [...(Array.isArray(cfg.keys) ? cfg.keys : [])];
78
+ const auths = [...(Array.isArray(cfg.auths) ? cfg.auths : [])];
79
+ let updated = 0;
80
+ for (const acc of accounts) {
81
+ if (!acc?.uid || !acc?.accessToken) continue;
82
+ const blob = JSON.stringify({ device_token: acc.accessToken, refresh_token: acc.refreshToken || "" });
83
+ const row = { uid: acc.uid, name: acc.name || "", refreshToken: acc.refreshToken || "" };
84
+ const idx = auths.findIndex((a) => String(a?.uid) === String(acc.uid));
85
+ if (idx >= 0) { keys[idx] = blob; auths[idx] = row; updated += 1; }
86
+ else { keys.push(blob); auths.push(row); }
87
+ }
88
+ // saveProviderConfig 起手保留 allowAnyModels(已在 state 层修复,见 ADR-0033 字段保留修复)
89
+ saveProviderConfig("qwenwork", { baseUrl: cfg.baseUrl || "", keys, auths }, file ? { file } : {});
90
+ return { keys, auths, updated };
91
+ }
92
+
93
+ export async function saveQwenworkAccount({ uid, accessToken, refreshToken = "", expiresAt = 0, name = "", email = "", file } = {}) {
94
+ if (!uid || !accessToken) throw new Error("saveQwenworkAccount: 缺少 uid/accessToken");
95
+ const fp = writeAccountFile({ uid, accessToken, refreshToken, expiresAt, name, email });
96
+ const st = await persistAccounts([{ uid, accessToken, refreshToken, name }], file);
97
+ return { file: fp, accounts: st.keys.length, updated: st.updated > 0 };
98
+ }
99
+
100
+ /** 回写刷新后的 token(上游每次 refresh 会轮换 refreshToken,必须落盘)。 */
101
+ export async function updateQwenworkTokens({ uid, accessToken, refreshToken = "", expiresAt = 0, file } = {}) {
102
+ if (!uid || !accessToken) throw new Error("updateQwenworkTokens: 缺少 uid/accessToken");
103
+ const docs = listAccountDocs();
104
+ const hit = docs.find((d) => String(d.uid) === String(uid));
105
+ const prev = hit?.doc || {};
106
+ const fp = writeAccountFile({
107
+ uid,
108
+ accessToken,
109
+ refreshToken: refreshToken || prev?.auth?.refreshToken || "",
110
+ expiresAt: expiresAt || 0,
111
+ name: prev?.account?.name || "",
112
+ email: prev?.account?.email || "",
113
+ });
114
+ await persistAccounts([{ uid, accessToken, refreshToken: refreshToken || prev?.auth?.refreshToken || "", name: prev?.account?.name || "" }], file);
115
+ try {
116
+ const { invalidateSession } = await import("./cosy.js");
117
+ invalidateSession({ uid, accessToken: prev?.auth?.accessToken });
118
+ } catch {}
119
+ return { file: fp };
120
+ }
121
+
122
+ // 从 provider keys 的 blob 还原凭据(device_token/deviceToken 双形状兼容)
123
+ export function accountFromBlob(blob) {
124
+ try {
125
+ const j = JSON.parse(String(blob || "{}"));
126
+ const accessToken = String(j.device_token || j.accessToken || "");
127
+ const refreshToken = String(j.refresh_token || j.refreshToken || "");
128
+ if (!accessToken) return null;
129
+ return { accessToken, refreshToken };
130
+ } catch {
131
+ return null;
132
+ }
133
+ }
@@ -1,67 +1,67 @@
1
- // qwenwork(千问办公 gateway.qwenwork.cn)上游技术常量。
2
- // 转译自 qwenwork2api-makers 的 edge-functions/_shared/config.js —— **禁止凭记忆改值**;
3
- // 要改先现网复测(客户端一升级这些快照就可能 403)。
4
- // Note: 为何独立 provider 而非 qoder 的新 region — 见 docs/adr/0037-qwenwork-independent-provider.md
5
- export const BASE = "https://gateway.qwenwork.cn";
6
-
7
- // OAuth 设备授权。client_id 与 qoder 恰好同值,但**不得 import qoder 常量**:
8
- // 同 client_id 不代表同租户(RSA 模数/模型池/额度池均不同,实测见 ADR-0037)。
9
- export const CLIENT_ID = "e883ade2-e6e3-4d6d-adf7-f92ceff5fdcb";
10
- export const REDIRECT_URI = "qwenwork-cn://";
11
-
12
- export const DEVICE_AUTH_URL = `${BASE}/device/selectAccounts`;
13
- export const POLL_URL = `${BASE}/api/v1/deviceToken/poll`;
14
- export const REFRESH_URL = `${BASE}/api/v1/deviceToken/refresh`;
15
- export const USERINFO_URL = `${BASE}/api/v1/userinfo`;
16
- // 额度/套餐:data.{user,plan,quota},quota.remaining 即积分余量(实测新免费号 2099.277)
17
- export const ACCOUNT_CONTEXT_URL = `${BASE}/api/v1/adapter/user/account-context?include=user,plan,quota`;
18
- // 无 `Encode=1`:请求体走**纯 JSON**,不像 qoder 那样过自定义 base64 字母表
19
- export const CHAT_URL = `${BASE}/algo/api/v2/service/pro/sse/agent_chat_generation?FetchKeys=llm_model_result&AgentId=agent_common`;
20
- export const MODELS_URL = `${BASE}/algo/api/v2/model/list`;
21
-
22
- export function envStr(name, fallback) {
23
- const v = process.env[name];
24
- return typeof v === "string" && v.trim() ? v.trim() : fallback;
25
- }
26
-
27
- /** env 可覆盖,排障不必改代码(改后必须现网复测)。 */
28
- export const UA = envStr("MSLXDFF_QWENWORK_UA", "qoderwork/1.0.5");
29
- export const COSY_VERSION = envStr("MSLXDFF_QWENWORK_COSY_VERSION", "1.1.18");
30
- export const IDE_VERSION = envStr("MSLXDFF_QWENWORK_IDE_VERSION", "1.0.5");
31
- export const RELEASE_VERSION = envStr("MSLXDFF_QWENWORK_RELEASE_VERSION", "1.0.5-26090901");
32
- export const BUILD = envStr("MSLXDFF_QWENWORK_BUILD", "26090901");
33
- export const CLIENT_TYPE = "6";
34
- export const BUSINESS_PRODUCT = "qoder_work";
35
- export const BUSINESS_TYPE = "agent";
36
- export const SCENE = "qwork";
37
- export const MACHINE_OS = "x86_64_win32";
38
-
39
- // 上游 model/list 返回 11 个根切片,实测**只有 `qwork` 非空**(其余 n=0):
40
- // chat/developer/assistant/inline/quest/nap/experts/qwake 全 0 —— 与 qoder 的 `chat` 切片零交集。
41
- export const MODEL_SLICE = "qwork";
42
- export const DEFAULT_MODEL = "flash";
43
- export const CONTEXT_LENGTH = 180000;
44
- export const MAX_TOKENS_DEFAULT = 32000;
45
-
46
- // 实测模型池(2026-09-29 真号取证):flash price_factor=0.1 / pro=1 / qwen3.8-max-preview=1.8,
47
- // 三者均 is_reasoning=true、is_vl=true、context_config 1M(默认)/200K/400K。
48
- // 仅作取数失败时的兜底展示,真实目录以 /model/list 为准。
49
- export const KNOWN_MODELS = [
50
- { key: "flash", name: "标准", priceFactor: 0.1 },
51
- { key: "pro", name: "高级", priceFactor: 1 },
52
- { key: "qwen3.8-max-preview", name: "Qwen3.8-Max", priceFactor: 1.8 },
53
- ];
54
-
55
- // RSA-1024 模数(逐字符转译 _shared/crypto.js 的 RSA_N,勿改)
56
- export const RSA_MODULUS_HEX =
57
- "c0f22307e5cd362e296bb04470f6de8fbf935ce24e8fcf511a0e2701329769c4" +
58
- "a76e499bb938036a52af1eaf818cf79a2600620e3ce87e371d2ca6d85803606a" +
59
- "1b3fa5e874643c9ed2db7e85673ef7227fca56e2e7c08f0927609bb896a9f24b" +
60
- "e1782099a66016a5bfdc3f1ff756bfc9e88d7b5dc5be30bf45a0223a00ebcecf";
61
- export const RSA_EXPONENT = 65537;
62
-
63
- export const LOGIN_TTL_MS = 10 * 60 * 1000;
64
- export const COOLDOWN_MS = 30_000;
65
- export const QUOTA_COOLDOWN_MS = 60 * 60 * 1000;
66
- export const REFRESH_MARGIN_MS = 30 * 60 * 1000;
67
- export const MODELS_CACHE_MS = 10 * 60 * 1000;
1
+ // qwenwork(千问办公 gateway.qwenwork.cn)上游技术常量。
2
+ // 转译自 qwenwork2api-makers 的 edge-functions/_shared/config.js —— **禁止凭记忆改值**;
3
+ // 要改先现网复测(客户端一升级这些快照就可能 403)。
4
+ // Note: 为何独立 provider 而非 qoder 的新 region — 见 docs/adr/0037-qwenwork-independent-provider.md
5
+ export const BASE = "https://gateway.qwenwork.cn";
6
+
7
+ // OAuth 设备授权。client_id 与 qoder 恰好同值,但**不得 import qoder 常量**:
8
+ // 同 client_id 不代表同租户(RSA 模数/模型池/额度池均不同,实测见 ADR-0037)。
9
+ export const CLIENT_ID = "e883ade2-e6e3-4d6d-adf7-f92ceff5fdcb";
10
+ export const REDIRECT_URI = "qwenwork-cn://";
11
+
12
+ export const DEVICE_AUTH_URL = `${BASE}/device/selectAccounts`;
13
+ export const POLL_URL = `${BASE}/api/v1/deviceToken/poll`;
14
+ export const REFRESH_URL = `${BASE}/api/v1/deviceToken/refresh`;
15
+ export const USERINFO_URL = `${BASE}/api/v1/userinfo`;
16
+ // 额度/套餐:data.{user,plan,quota},quota.remaining 即积分余量(实测新免费号 2099.277)
17
+ export const ACCOUNT_CONTEXT_URL = `${BASE}/api/v1/adapter/user/account-context?include=user,plan,quota`;
18
+ // 无 `Encode=1`:请求体走**纯 JSON**,不像 qoder 那样过自定义 base64 字母表
19
+ export const CHAT_URL = `${BASE}/algo/api/v2/service/pro/sse/agent_chat_generation?FetchKeys=llm_model_result&AgentId=agent_common`;
20
+ export const MODELS_URL = `${BASE}/algo/api/v2/model/list`;
21
+
22
+ export function envStr(name, fallback) {
23
+ const v = process.env[name];
24
+ return typeof v === "string" && v.trim() ? v.trim() : fallback;
25
+ }
26
+
27
+ /** env 可覆盖,排障不必改代码(改后必须现网复测)。 */
28
+ export const UA = envStr("MSLXDFF_QWENWORK_UA", "qoderwork/1.0.5");
29
+ export const COSY_VERSION = envStr("MSLXDFF_QWENWORK_COSY_VERSION", "1.1.18");
30
+ export const IDE_VERSION = envStr("MSLXDFF_QWENWORK_IDE_VERSION", "1.0.5");
31
+ export const RELEASE_VERSION = envStr("MSLXDFF_QWENWORK_RELEASE_VERSION", "1.0.5-26090901");
32
+ export const BUILD = envStr("MSLXDFF_QWENWORK_BUILD", "26090901");
33
+ export const CLIENT_TYPE = "6";
34
+ export const BUSINESS_PRODUCT = "qoder_work";
35
+ export const BUSINESS_TYPE = "agent";
36
+ export const SCENE = "qwork";
37
+ export const MACHINE_OS = "x86_64_win32";
38
+
39
+ // 上游 model/list 返回 11 个根切片,实测**只有 `qwork` 非空**(其余 n=0):
40
+ // chat/developer/assistant/inline/quest/nap/experts/qwake 全 0 —— 与 qoder 的 `chat` 切片零交集。
41
+ export const MODEL_SLICE = "qwork";
42
+ export const DEFAULT_MODEL = "flash";
43
+ export const CONTEXT_LENGTH = 180000;
44
+ export const MAX_TOKENS_DEFAULT = 32000;
45
+
46
+ // 实测模型池(2026-09-29 真号取证):flash price_factor=0.1 / pro=1 / qwen3.8-max-preview=1.8,
47
+ // 三者均 is_reasoning=true、is_vl=true、context_config 1M(默认)/200K/400K。
48
+ // 仅作取数失败时的兜底展示,真实目录以 /model/list 为准。
49
+ export const KNOWN_MODELS = [
50
+ { key: "flash", name: "标准", priceFactor: 0.1 },
51
+ { key: "pro", name: "高级", priceFactor: 1 },
52
+ { key: "qwen3.8-max-preview", name: "Qwen3.8-Max", priceFactor: 1.8 },
53
+ ];
54
+
55
+ // RSA-1024 模数(逐字符转译 _shared/crypto.js 的 RSA_N,勿改)
56
+ export const RSA_MODULUS_HEX =
57
+ "c0f22307e5cd362e296bb04470f6de8fbf935ce24e8fcf511a0e2701329769c4" +
58
+ "a76e499bb938036a52af1eaf818cf79a2600620e3ce87e371d2ca6d85803606a" +
59
+ "1b3fa5e874643c9ed2db7e85673ef7227fca56e2e7c08f0927609bb896a9f24b" +
60
+ "e1782099a66016a5bfdc3f1ff756bfc9e88d7b5dc5be30bf45a0223a00ebcecf";
61
+ export const RSA_EXPONENT = 65537;
62
+
63
+ export const LOGIN_TTL_MS = 10 * 60 * 1000;
64
+ export const COOLDOWN_MS = 30_000;
65
+ export const QUOTA_COOLDOWN_MS = 60 * 60 * 1000;
66
+ export const REFRESH_MARGIN_MS = 30 * 60 * 1000;
67
+ export const MODELS_CACHE_MS = 10 * 60 * 1000;