mslxdff 0.1.199 → 0.1.203

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 (327) hide show
  1. package/README.md +3 -3
  2. package/bin/mslxdff.js +1 -1
  3. package/package.json +38 -38
  4. package/plugins/README.md +14 -14
  5. package/plugins/prefer-model.mjs.example +14 -14
  6. package/src/anthropic/count-tokens.js +61 -61
  7. package/src/anthropic/request-body.js +126 -126
  8. package/src/anthropic/shared.js +18 -18
  9. package/src/anthropic/sse-aggregate.js +56 -56
  10. package/src/anthropic/translate.js +287 -287
  11. package/src/anthropic/web-search.js +361 -361
  12. package/src/api-kind.js +31 -31
  13. package/src/autostart.js +288 -288
  14. package/src/bench/probe.js +1 -1
  15. package/src/bench/runner.js +1 -1
  16. package/src/bench/via-probe.js +1 -1
  17. package/src/bench/via.js +144 -144
  18. package/src/bench/workbuddy-bench.js +1 -1
  19. package/src/chat/config.js +11 -11
  20. package/src/chat/cooling.js +81 -81
  21. package/src/chat/direct.js +58 -58
  22. package/src/chat/index.js +6 -6
  23. package/src/chat/spinner.js +38 -38
  24. package/src/chat/sse.js +69 -69
  25. package/src/chat/stats.js +247 -247
  26. package/src/chat/store.js +48 -48
  27. package/src/chat/tool-handlers.js +67 -67
  28. package/src/chat/tools.js +1 -1
  29. package/src/chat/upstream.js +59 -59
  30. package/src/chat-pipeline/auto-race.js +158 -158
  31. package/src/chat-pipeline/empty-turn.js +147 -147
  32. package/src/chat-pipeline/engine.js +17 -17
  33. package/src/chat-pipeline/serial-trial.js +283 -283
  34. package/src/chat-pipeline/terminal-verdict.js +147 -147
  35. package/src/chooser.js +54 -54
  36. package/src/claude-desktop/apply.js +101 -101
  37. package/src/claude-desktop/context1m.js +38 -38
  38. package/src/claude-desktop/doctor.js +146 -146
  39. package/src/claude-desktop/slots.js +152 -152
  40. package/src/cli/bootstrap.js +1 -1
  41. package/src/cli/commands/claude-desktop.js +284 -284
  42. package/src/cli/commands/daemon.js +231 -231
  43. package/src/cli/commands/model/list-live.js +22 -22
  44. package/src/cli/commands/model/list-providers.js +76 -76
  45. package/src/cli/commands/model/list-render.js +77 -77
  46. package/src/cli/commands/model/list-sort.js +52 -52
  47. package/src/cli/commands/model/list-tty.js +76 -76
  48. package/src/cli/commands/model/list.js +166 -166
  49. package/src/cli/commands/model/live-models.js +24 -24
  50. package/src/cli/commands/model/stats.js +43 -43
  51. package/src/cli/commands/model/status.js +47 -47
  52. package/src/cli/commands/model.js +51 -51
  53. package/src/cli/commands/provider/add.js +108 -108
  54. package/src/cli/commands/provider/allowlist.js +99 -99
  55. package/src/cli/commands/provider/cline-free.js +99 -99
  56. package/src/cli/commands/provider/cline-login.js +218 -218
  57. package/src/cli/commands/provider/cline-quota.js +65 -65
  58. package/src/cli/commands/provider/codearts-login.js +45 -45
  59. package/src/cli/commands/provider/config.js +119 -119
  60. package/src/cli/commands/provider/globalqwenwork-login.js +126 -126
  61. package/src/cli/commands/provider/keys-guard.js +50 -50
  62. package/src/cli/commands/provider/keys.js +161 -161
  63. package/src/cli/commands/provider/qoder-checkin.js +81 -81
  64. package/src/cli/commands/provider/qoder-login.js +102 -102
  65. package/src/cli/commands/provider/raccoon-login.js +180 -180
  66. package/src/cli/commands/provider/raccoon-password.js +162 -162
  67. package/src/cli/commands/provider/raccoon-quota.js +109 -109
  68. package/src/cli/commands/provider/traework-checkin.js +144 -144
  69. package/src/cli/commands/provider/traework-login.js +164 -164
  70. package/src/cli/commands/provider/workbuddy-login.js +153 -153
  71. package/src/cli/commands/provider/zcode-login.js +77 -77
  72. package/src/cli/commands/provider/zcode-quota.js +55 -55
  73. package/src/cli/commands/provider.js +1 -1
  74. package/src/cli/commands/stats.js +149 -149
  75. package/src/cli/commands/system.js +261 -261
  76. package/src/cli/commands/timezone.js +47 -47
  77. package/src/cli/commands/use-group.js +60 -60
  78. package/src/cli/commands/workbuddy.js +189 -189
  79. package/src/cli/format.js +141 -141
  80. package/src/cli/group-helpers.js +70 -70
  81. package/src/cli/help.js +80 -80
  82. package/src/cli/index.js +70 -70
  83. package/src/cli/interactive.js +121 -121
  84. package/src/cli/join-core.js +99 -99
  85. package/src/cli/join-wizard.js +175 -175
  86. package/src/cli/policy.js +136 -136
  87. package/src/cli/provider-row.js +126 -126
  88. package/src/cli/util.js +24 -24
  89. package/src/compat.js +53 -53
  90. package/src/events.js +38 -38
  91. package/src/free-lane.js +131 -131
  92. package/src/free-watcher.js +63 -63
  93. package/src/groups.js +262 -262
  94. package/src/logs.js +195 -195
  95. package/src/metrics.js +62 -62
  96. package/src/model-capabilities/context-lookup.js +65 -65
  97. package/src/model-capabilities/merge.js +142 -142
  98. package/src/model-trace.js +187 -187
  99. package/src/models.js +1 -1
  100. package/src/opencode-identity.js +72 -72
  101. package/src/peers.js +254 -251
  102. package/src/plugins.js +107 -107
  103. package/src/providers/base.js +164 -164
  104. package/src/providers/classify.js +22 -22
  105. package/src/providers/cline/allowlist-sync.js +93 -93
  106. package/src/providers/cline/free-catalog.js +71 -71
  107. package/src/providers/cline/headers.js +37 -37
  108. package/src/providers/cline/index.js +130 -130
  109. package/src/providers/cline/models.js +150 -150
  110. package/src/providers/cline/usage.js +274 -274
  111. package/src/providers/codearts/auth-pool.js +148 -148
  112. package/src/providers/codearts/concurrency.js +34 -34
  113. package/src/providers/codearts/const.js +65 -65
  114. package/src/providers/codearts/dpop.js +95 -95
  115. package/src/providers/codearts/index.js +175 -175
  116. package/src/providers/codearts/login.js +214 -214
  117. package/src/providers/codearts/models.js +182 -182
  118. package/src/providers/codearts/sign.js +96 -96
  119. package/src/providers/codearts/sse.js +204 -204
  120. package/src/providers/codearts/stream.js +209 -209
  121. package/src/providers/codearts/sts.js +124 -124
  122. package/src/providers/codearts.js +1 -1
  123. package/src/providers/dispatcher.js +146 -146
  124. package/src/providers/generic.js +156 -156
  125. package/src/providers/globalqwenwork/account-store.js +141 -141
  126. package/src/providers/globalqwenwork/constants.js +73 -73
  127. package/src/providers/globalqwenwork/crypto.js +2 -2
  128. package/src/providers/globalqwenwork/http.js +2 -2
  129. package/src/providers/globalqwenwork/index.js +331 -331
  130. package/src/providers/globalqwenwork/payload.js +147 -147
  131. package/src/providers/globalqwenwork/rsa.js +2 -2
  132. package/src/providers/globalqwenwork/sse.js +2 -2
  133. package/src/providers/globalqwenwork/stream.js +2 -2
  134. package/src/providers/globalqwenwork.js +1 -1
  135. package/src/providers/globalworkbuddy/account-store.js +198 -198
  136. package/src/providers/globalworkbuddy/auth.js +97 -97
  137. package/src/providers/globalworkbuddy/catalog.js +138 -138
  138. package/src/providers/globalworkbuddy/constants.js +73 -73
  139. package/src/providers/globalworkbuddy/device-auth.js +120 -120
  140. package/src/providers/globalworkbuddy/errors.js +98 -98
  141. package/src/providers/globalworkbuddy/headers.js +89 -89
  142. package/src/providers/globalworkbuddy/models.js +96 -96
  143. package/src/providers/globalworkbuddy/payload.js +80 -80
  144. package/src/providers/index.js +5 -5
  145. package/src/providers/model-id.js +172 -172
  146. package/src/providers/opencode.js +19 -19
  147. package/src/providers/openrouter.js +31 -31
  148. package/src/providers/qoder/account-store.js +106 -106
  149. package/src/providers/qoder/aggregate.js +60 -60
  150. package/src/providers/qoder/baseprompt.json +591 -591
  151. package/src/providers/qoder/checkin.js +160 -160
  152. package/src/providers/qoder/constants.js +44 -44
  153. package/src/providers/qoder/encode.js +41 -41
  154. package/src/providers/qoder/fingerprint.js +38 -38
  155. package/src/providers/qoder/models.js +77 -77
  156. package/src/providers/qoder/oauth.js +84 -84
  157. package/src/providers/qoder/payload.js +162 -162
  158. package/src/providers/qoder/request.js +27 -27
  159. package/src/providers/qoder/session.js +134 -134
  160. package/src/providers/qoder/sticky.js +38 -38
  161. package/src/providers/qoder/stream.js +116 -116
  162. package/src/providers/qoder.js +1 -1
  163. package/src/providers/raccoon/account-store.js +216 -216
  164. package/src/providers/raccoon/auth.js +67 -67
  165. package/src/providers/raccoon/chat.js +228 -228
  166. package/src/providers/raccoon/const.js +180 -165
  167. package/src/providers/raccoon/credits.js +185 -175
  168. package/src/providers/raccoon/crypto.js +43 -0
  169. package/src/providers/raccoon/envelope.js +31 -31
  170. package/src/providers/raccoon/headers.js +45 -45
  171. package/src/providers/raccoon/index.js +216 -216
  172. package/src/providers/raccoon/login.js +279 -279
  173. package/src/providers/raccoon/models.js +160 -160
  174. package/src/providers/raccoon/password-login.js +102 -87
  175. package/src/providers/raccoon/qr-codewords.js +115 -115
  176. package/src/providers/raccoon/qr.js +236 -236
  177. package/src/providers/raccoon/sse.js +135 -135
  178. package/src/providers/responses-channel.js +120 -120
  179. package/src/providers/share-keys.js +53 -53
  180. package/src/providers/traework/account-store.js +234 -234
  181. package/src/providers/traework/chat.js +217 -217
  182. package/src/providers/traework/checkin.js +77 -77
  183. package/src/providers/traework/constants.js +41 -41
  184. package/src/providers/traework/errors.js +52 -52
  185. package/src/providers/traework/headers.js +79 -79
  186. package/src/providers/traework/index.js +132 -132
  187. package/src/providers/traework/market-identity.js +116 -116
  188. package/src/providers/traework/models.js +98 -98
  189. package/src/providers/traework/payload.js +86 -86
  190. package/src/providers/traework/sse.js +184 -184
  191. package/src/providers/traework/token.js +52 -52
  192. package/src/providers/traework.js +2 -2
  193. package/src/providers/workbuddy/account-store.js +186 -186
  194. package/src/providers/workbuddy/auth.js +158 -158
  195. package/src/providers/workbuddy/balance.js +86 -86
  196. package/src/providers/workbuddy/cat-travel.js +125 -125
  197. package/src/providers/workbuddy/checkin.js +75 -75
  198. package/src/providers/workbuddy/desktop-info.js +78 -78
  199. package/src/providers/workbuddy/growth-api.js +67 -67
  200. package/src/providers/workbuddy/growth-plans.js +63 -63
  201. package/src/providers/workbuddy/growth.js +181 -181
  202. package/src/providers/workbuddy/index.js +217 -217
  203. package/src/providers/workbuddy/models.js +126 -126
  204. package/src/providers/workbuddy/payload.js +141 -141
  205. package/src/providers/workbuddy/rotation-log.js +83 -83
  206. package/src/providers/workbuddy/sanitize-tools.js +61 -61
  207. package/src/providers/workbuddy/sdk-chat.js +26 -26
  208. package/src/providers/workbuddy-balance.js +78 -78
  209. package/src/providers/zcode/account-store.js +129 -129
  210. package/src/providers/zcode/auth.js +28 -28
  211. package/src/providers/zcode/chat.js +179 -179
  212. package/src/providers/zcode/const.js +54 -54
  213. package/src/providers/zcode/context-shape.js +162 -162
  214. package/src/providers/zcode/headers.js +93 -93
  215. package/src/providers/zcode/index.js +124 -124
  216. package/src/providers/zcode/models.js +65 -65
  217. package/src/providers/zcode/oauth.js +120 -120
  218. package/src/providers/zcode/quota.js +176 -176
  219. package/src/providers/zcode/sse.js +196 -196
  220. package/src/readline-compat.js +25 -25
  221. package/src/responses/translate.js +311 -311
  222. package/src/routes/chat/broadband-handler.js +72 -72
  223. package/src/routes/chat/empty-turn-report.js +31 -31
  224. package/src/routes/chat/exhausted-handler.js +87 -87
  225. package/src/routes/chat/hedge-handler.js +130 -130
  226. package/src/routes/chat/index.js +1 -1
  227. package/src/routes/chat/local-handler.js +63 -63
  228. package/src/routes/chat/peer-handler.js +81 -81
  229. package/src/routes/chat.js +3 -3
  230. package/src/routes/fallback.js +88 -88
  231. package/src/routes/groups-relay.js +214 -214
  232. package/src/routes/groups.js +122 -122
  233. package/src/routes/helpers.js +83 -83
  234. package/src/routes/index.js +147 -147
  235. package/src/routes/messages-route.js +365 -365
  236. package/src/routes/peers.js +23 -1
  237. package/src/routes/relay-queue.js +181 -181
  238. package/src/routes/relay.js +60 -60
  239. package/src/routes/responses-route.js +213 -213
  240. package/src/routes/stream-body.js +84 -84
  241. package/src/routes/stream-hold.js +337 -337
  242. package/src/routes/stream-scan.js +366 -366
  243. package/src/routes.js +3 -3
  244. package/src/runtime/auto-update.js +113 -113
  245. package/src/runtime/bootstrap.js +8 -0
  246. package/src/runtime/broadband-stream.js +79 -79
  247. package/src/runtime/broadband.js +103 -103
  248. package/src/runtime/group-sync.js +81 -81
  249. package/src/runtime/keep-warm.js +120 -0
  250. package/src/runtime/lifecycle-forensics.js +116 -116
  251. package/src/runtime/lifecycle-log.js +87 -87
  252. package/src/runtime/log-stamp.js +73 -73
  253. package/src/runtime/proc-info.js +137 -137
  254. package/src/runtime/qoder-checkin.js +93 -93
  255. package/src/runtime/raccoon-checkin.js +126 -126
  256. package/src/runtime/server-lifecycle.js +189 -189
  257. package/src/runtime/traework-checkin.js +118 -118
  258. package/src/runtime/workbuddy-checkin.js +120 -120
  259. package/src/runtime/workbuddy-growth.js +133 -133
  260. package/src/server.js +7 -0
  261. package/src/state/facade.js +58 -58
  262. package/src/state/memory.js +162 -162
  263. package/src/state/merge.js +25 -25
  264. package/src/state/migrations/cline-unify.js +94 -94
  265. package/src/state/migrations.js +33 -33
  266. package/src/state/persist.js +42 -42
  267. package/src/state/provider-config.js +142 -142
  268. package/src/state/schemas/group.js +31 -31
  269. package/src/state/schemas/model.js +94 -94
  270. package/src/state/schemas/peer.js +31 -31
  271. package/src/state/schemas/port.js +10 -10
  272. package/src/state/schemas/provider.js +202 -202
  273. package/src/state/schemas/timezone.js +70 -70
  274. package/src/state/schemas/token.js +48 -48
  275. package/src/state/schemas/use-group.js +102 -102
  276. package/src/state/store.js +176 -176
  277. package/src/state.js +1 -1
  278. package/src/sync-claude-desktop.js +234 -234
  279. package/src/sync-claude.js +152 -152
  280. package/src/sync-codex.js +116 -116
  281. package/src/sync-workbuddy.js +158 -158
  282. package/src/talk-full-redact.js +129 -129
  283. package/src/talk-full.js +131 -131
  284. package/src/talk-log.js +257 -257
  285. package/src/text-tokens.js +52 -52
  286. package/src/thinking-effort.js +67 -67
  287. package/src/time.js +106 -106
  288. package/src/timeline.js +26 -26
  289. package/src/transport/index.js +1 -1
  290. package/src/upstream-engine/index.js +74 -72
  291. package/src/upstream-engine/mode.js +15 -15
  292. package/src/upstream-engine/sdk/attempt.js +221 -221
  293. package/src/upstream-engine/sdk/chat.js +28 -28
  294. package/src/upstream-engine/sdk/convert.js +192 -192
  295. package/src/upstream-engine/sdk/diagnose.js +62 -62
  296. package/src/upstream-engine/sdk/dispatch.js +55 -55
  297. package/src/upstream-engine/sdk/responses.js +176 -176
  298. package/src/upstream-engine/sdk/sse.js +166 -166
  299. package/src/upstream-responses.js +305 -305
  300. package/src/upstream.js +53 -1
  301. package/src/usage/record.js +106 -106
  302. package/src/usage/report.js +195 -195
  303. package/src/v6pool/claim.js +157 -157
  304. package/src/v6pool/config.js +65 -65
  305. package/src/v6pool/detect.js +130 -130
  306. package/src/v6pool/index.js +152 -152
  307. package/src/v6pool/prefix.js +140 -140
  308. package/src/v6pool/probe.js +40 -40
  309. package/src/v6pool/report.js +104 -104
  310. package/src/v6pool/rotate.js +263 -239
  311. package/src/warp-pool/config.js +74 -74
  312. package/src/warp-pool/health.js +106 -106
  313. package/src/warp-pool/identity.js +141 -141
  314. package/src/warp-pool/index.js +358 -358
  315. package/src/warp-pool/keys.js +29 -29
  316. package/src/warp-pool/nodes.js +95 -95
  317. package/src/warp-pool/ports.js +160 -160
  318. package/src/warp-pool/register.js +113 -113
  319. package/src/warp-pool/report.js +232 -232
  320. package/src/warp-pool/revive.js +168 -168
  321. package/src/warp-pool/rotate.js +214 -214
  322. package/src/warp-pool/strategy.js +43 -43
  323. package/src/warp-pool/verdict.js +69 -69
  324. package/src/warp-pool/wireproxy.js +162 -162
  325. package/docs/cli_help_mini.md +0 -146
  326. package/src/providers/AGENTS.md +0 -49
  327. package/src/routes/AGENTS.md +0 -37
@@ -1,331 +1,331 @@
1
- // globalqwenwork provider 门面:多号 keyring 轮转 + 401/403 refresh 重试 + 额度长冷却换号。
2
- // 协议细节全部下沉到 cosy/upstream/payload/sse/stream;这里只做编排(对齐 qwenwork/index.js 形状)。
3
- // vendor 自 src/providers/qwenwork/index.js,改动面 = 命名空间(id/env/响应头/提示语)+ 默认模型名,
4
- // 中继逻辑逐字同构:实测国际站接受 cn 同款 COSY 签名与 chat body(2026-10-01 现网 200 出词)。
5
- // Note: 为何独立 provider 而非 qwenwork 的新 region — 见 docs/adr/0041-globalqwenwork-international-provider.md
6
- import { loadProviderKeys, loadProviderConfig } from "../../state.js";
7
- import { listAccountDocs, accountFromBlob, updateGlobalQwenworkTokens } from "./account-store.js";
8
- import { createKeyRing } from "../keyring.js";
9
- import { envInt } from "../base.js";
10
- import { compatFetch, timeoutSignal } from "../../compat.js";
11
- import { joinModelId } from "../model-id.js";
12
- import { buildBody, mapModel } from "./payload.js";
13
- import { chatRequest, modelList, refreshDeviceToken, expiryUnix, userInfo } from "./upstream.js";
14
- import { invalidateSession } from "./cosy.js";
15
- import { collectSync, isCreditsExhausted, creditsExhaustedText } from "./sse.js";
16
- import { transformStream } from "./stream.js";
17
- import { KNOWN_MODELS, MODELS_CACHE_MS, CHAT_URL, CONTEXT_LENGTH, DEFAULT_MODEL } from "./constants.js";
18
-
19
- const ID_PREFIX = "globalqwenwork/";
20
-
21
- function hostOf(url) {
22
- try { return new URL(String(url)).host; } catch { return "-"; }
23
- }
24
-
25
- function errRes(status, msg, type = "upstream_error", extra = {}) {
26
- return new Response(JSON.stringify({ error: { message: msg, type } }), {
27
- status,
28
- headers: { "Content-Type": "application/json", ...extra },
29
- });
30
- }
31
-
32
- export function createGlobalQwenworkProvider({
33
- id = "globalqwenwork",
34
- apiKeys,
35
- file,
36
- fetchImpl,
37
- cooldownMs = envInt("MSLXDFF_GLOBALQWENWORK_COOLDOWN_MS", 30_000),
38
- quotaCooldownMs = envInt("MSLXDFF_GLOBALQWENWORK_QUOTA_COOLDOWN_MS", 3600_000),
39
- connectTimeoutMs = Number(process.env.MSLXDFF_GLOBALQWENWORK_TIMEOUT_MS) || 120_000,
40
- } = {}) {
41
- // 单测隔离(MSLXDFF_TEST=1 / NODE_ENV=test / --test 进程):state 与 auth 目录走测试替身,
42
- // 与你个人真实账号完全隔离(防止单测把真号读进来、把真 refreshToken 写回去)。
43
- // 教训继承自 qwenwork:构造传了 `apiKeys: []`,门面却从 state.json 读出真号 → 无号测试打了现网。
44
- const testIsolated = process.env.MSLXDFF_TEST === "1" || process.env.NODE_ENV === "test" || String(process.execArgv || []).includes("--test") || process.argv.slice(1).some((a) => String(a).includes("--test"));
45
- const keys = (() => {
46
- if (testIsolated && file === undefined && apiKeys !== undefined) {
47
- return [...new Set((Array.isArray(apiKeys) ? apiKeys : []).map((k) => String(k).trim()).filter(Boolean))];
48
- }
49
- if (Array.isArray(apiKeys) && apiKeys.length) return [...new Set(apiKeys.map((k) => String(k).trim()).filter(Boolean))];
50
- try { return loadProviderKeys(id, file ? { file } : {}); } catch { return []; }
51
- })();
52
- let authList = [];
53
- if (!testIsolated) {
54
- try { authList = loadProviderConfig(id, file ? { file } : {})?.auths || []; } catch {}
55
- try {
56
- const docs = listAccountDocs();
57
- for (const d of docs) {
58
- const row = authList.find((a) => String(a?.uid) === String(d.uid));
59
- if (!row) authList.push({ uid: d.uid, refreshToken: d.doc?.auth?.refreshToken || "", name: d.doc?.account?.name || "" });
60
- }
61
- } catch {}
62
- }
63
- const ring = createKeyRing(keys, { cooldownMs });
64
- const exhaustedByQuota = new Set();
65
- if (!fetchImpl) fetchImpl = compatFetch;
66
- const cdbg = (...a) => { if (process.env.GLOBALQWENWORK_DEBUG === "1") console.log("[globalqwenwork-chat]", ...a); };
67
-
68
- // blob → 上游 account(含 uid/nickname/email,供 COSY 身份 payload 用)
69
- function accountFor(key) {
70
- const blob = accountFromBlob(key);
71
- if (!blob?.accessToken) return null;
72
- const auth = authList.find((a) => String(a?.refreshToken || "") === String(blob.refreshToken || "")) || {};
73
- let doc = null;
74
- try {
75
- const docs = listAccountDocs();
76
- doc = docs.find((d) => String(d.uid) === String(auth.uid)) || docs.find((d) => d.doc?.auth?.refreshToken === blob.refreshToken) || null;
77
- } catch {}
78
- return {
79
- id: auth.uid || doc?.uid || "",
80
- uid: auth.uid || doc?.uid || "",
81
- nickname: auth.name || doc?.doc?.account?.name || "",
82
- email: doc?.doc?.account?.email || "",
83
- accessToken: blob.accessToken,
84
- refreshToken: blob.refreshToken || auth.refreshToken || doc?.doc?.auth?.refreshToken || "",
85
- };
86
- }
87
-
88
- function withEcho(res, account, upstreamStatus) {
89
- try {
90
- res.headers.set("x-mslxdff-upstream", hostOf(CHAT_URL));
91
- if (account?.uid) res.headers.set("x-mslxdff-globalqwenwork-account", String(account.uid).slice(-8));
92
- if (upstreamStatus) res.headers.set("x-mslxdff-globalqwenwork-upstream-status", String(upstreamStatus));
93
- } catch {}
94
- return res;
95
- }
96
- function withQuota(res) {
97
- try { res.headers.set("x-mslxdff-globalqwenwork-quota", "1"); } catch {}
98
- return res;
99
- }
100
-
101
- async function tryRefresh(account) {
102
- if (!account?.refreshToken) return false;
103
- try {
104
- // 实测 .ai 的 /api/v1/deviceToken/refresh 接受 cn 原形状 {refresh_token}(200,且轮换 refresh_token)
105
- const data = await refreshDeviceToken(account.refreshToken, fetchImpl);
106
- const access = data.token || data.device_token;
107
- const refresh = data.refresh_token || account.refreshToken;
108
- const expiresAt = expiryUnix(data);
109
- account.accessToken = access;
110
- account.refreshToken = refresh;
111
- try { await updateGlobalQwenworkTokens({ uid: account.uid, accessToken: access, refreshToken: refresh, expiresAt, file }); }
112
- catch { invalidateSession(account); }
113
- // 同步 ring 里的 blob(token 轮换后旧 blob 即失效)
114
- try {
115
- const oldKey = ring.keys.find((k) => { const b = accountFromBlob(k); return b && b.refreshToken === data.refresh_token; }) || null;
116
- const newBlob = JSON.stringify({ device_token: access, refresh_token: refresh });
117
- const cur = ring.keys.find((k) => accountFromBlob(k)?.accessToken === account.accessToken);
118
- if (cur) ring.replace(cur, newBlob);
119
- else if (oldKey) ring.replace(oldKey, newBlob);
120
- } catch {}
121
- return true;
122
- } catch (e) {
123
- cdbg(`[refresh-fail] uid=${String(account.uid).slice(-8)} ${String(e?.message || e).slice(0, 160)}`);
124
- return false;
125
- }
126
- }
127
-
128
- async function chat(body, opts) {
129
- const stream = body?.stream !== false;
130
- const wantModel = typeof body?.model === "string" && body.model.startsWith(ID_PREFIX)
131
- ? body.model.slice(ID_PREFIX.length)
132
- : (body?.model || DEFAULT_MODEL);
133
- const modelKey = mapModel(wantModel);
134
- const stripped = { ...body, model: wantModel };
135
- const total = ring.keys.length;
136
- // 构造参数无号但 auth 目录有号:不视为"无号",直接按 401 引导 login——调用方先 login 落盘再 chat
137
- if (!total) {
138
- return errRes(401, "globalqwenwork: 无可用账号 — 先跑 mslxdff -provider globalqwenwork login", "auth_error");
139
- }
140
- let lastRes = null;
141
- const refreshed = new Set();
142
- for (let hop = 0; hop <= total; hop++) {
143
- const key = ring.next();
144
- if (!key) break;
145
- if (exhaustedByQuota.has(key)) {
146
- if (ring.isCooling(key)) {
147
- if (ring.available() > 0) continue;
148
- break;
149
- }
150
- exhaustedByQuota.delete(key);
151
- }
152
- let account = accountFor(key);
153
- if (!account) { try { ring.onError(key); } catch {} continue; }
154
- // 身份缺失(手动配 key、未走 login)时补一次 userinfo:COSY 身份 payload 的 uid/name/email 为空
155
- // 会被上游判 `code 101 Signature invalid`(cn 实测同 token 空身份 403、有身份 200;.ai 同构)。
156
- if (!account.uid) {
157
- try {
158
- const info = await userInfo(account.accessToken, fetchImpl);
159
- if (info?.id) account.uid = String(info.id);
160
- if (info?.name) account.nickname = String(info.name);
161
- if (info?.email) account.email = String(info.email);
162
- if (account.uid) {
163
- try {
164
- const { saveGlobalQwenworkAccount } = await import("./account-store.js");
165
- await saveGlobalQwenworkAccount({ uid: account.uid, accessToken: account.accessToken, refreshToken: account.refreshToken || "", name: account.nickname || "", email: account.email || "", file });
166
- } catch {}
167
- }
168
- } catch (e) {
169
- cdbg(`[identity-enrich-fail] ${String(e?.message || e).slice(0, 160)}`);
170
- }
171
- if (!account.uid) { try { ring.onError(key); } catch {} lastRes = withEcho(errRes(401, "globalqwenwork: 账号身份缺失且 userinfo 失败,该 key 不可用", "auth_error"), account, 401); continue; }
172
- }
173
- const bodyStr = buildBody(stripped, modelKey);
174
- let upRes;
175
- try {
176
- upRes = await chatRequest(account, modelKey, bodyStr, timeoutSignal(connectTimeoutMs), fetchImpl);
177
- } catch (e) {
178
- try { ring.onError(key); } catch {}
179
- lastRes = withEcho(errRes(502, `globalqwenwork: 上游连接失败 ${String(e?.message || e).slice(0, 200)}`), account, 502);
180
- continue;
181
- }
182
- if (!upRes.ok) {
183
- const text = await upRes.text().catch(() => "");
184
- const quota = isCreditsExhausted(upRes.status, text);
185
- if (quota) {
186
- try { ring.onError(key, quotaCooldownMs); } catch {}
187
- exhaustedByQuota.add(key);
188
- lastRes = withQuota(withEcho(errRes(429, `globalqwenwork: 账号积分耗尽 ${text.slice(0, 200)}`, "quota_exhausted"), account, upRes.status));
189
- if (ring.available() > 0) continue;
190
- break;
191
- }
192
- if ((upRes.status === 401 || upRes.status === 403) && account.refreshToken && !refreshed.has(key)) {
193
- refreshed.add(key);
194
- const ok = await tryRefresh(account);
195
- if (ok) { hop -= 1; continue; }
196
- try { ring.onError(key); } catch {}
197
- lastRes = withEcho(errRes(upRes.status, `globalqwenwork: 授权失效 ${text.slice(0, 200)}`, "auth_error"), account, upRes.status);
198
- continue;
199
- }
200
- try { ring.onError(key); } catch {}
201
- lastRes = withEcho(errRes(upRes.status >= 500 ? 502 : upRes.status, `globalqwenwork: 上游 ${upRes.status} ${text.slice(0, 200)}`), account, upRes.status);
202
- if (ring.available() > 0 && (upRes.status === 429 || upRes.status >= 500)) continue;
203
- return lastRes;
204
- }
205
- // 200:流内仍可能裹着业务错(信封 statusCodeValue / code),非流式聚合时由 collectSync 判决;
206
- // 流式路径照 transform 透传(错误帧由 sse.handleLine 转成流内 error 事件)。
207
- if (stream) {
208
- const out = new Response(transformStream(upRes.body, wantModel, () => {}), {
209
- status: 200,
210
- headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", Connection: "keep-alive" },
211
- });
212
- return withEcho(out, account, 0);
213
- }
214
- const result = await collectSync(upRes.body, wantModel);
215
- if (result.error) {
216
- if (creditsExhaustedText(result.error)) {
217
- try { ring.onError(key, quotaCooldownMs); } catch {}
218
- exhaustedByQuota.add(key);
219
- lastRes = withQuota(withEcho(errRes(429, `globalqwenwork: 账号积分耗尽 ${result.error}`, "quota_exhausted"), account, 429));
220
- if (ring.available() > 0) continue;
221
- break;
222
- }
223
- try { ring.onError(key); } catch {}
224
- lastRes = withEcho(errRes(502, `globalqwenwork: 上游流内错误 ${result.error}`), account, 502);
225
- continue;
226
- }
227
- const completion = result.completion;
228
- completion.model = wantModel;
229
- return withEcho(new Response(JSON.stringify(completion), { status: 200, headers: { "Content-Type": "application/json" } }), account, 0);
230
- }
231
- if (total > 0 && ring.keys.every((k) => exhaustedByQuota.has(k))) {
232
- return errRes(429, "globalqwenwork: 所有账号积分已用完,请更换账号或等待额度恢复", "quota_exhausted", { "x-mslxdff-globalqwenwork-quota-exhausted": "1" });
233
- }
234
- if (lastRes) return lastRes;
235
- if (!ring.available()) {
236
- return errRes(429, "globalqwenwork: 账号暂不可用(上游限流/排队冷却中),请稍后重试", "all_cooling", { "x-mslxdff-globalqwenwork-all-cooling": "1" });
237
- }
238
- return errRes(401, "globalqwenwork: 无可用账号 — 先跑 mslxdff -provider globalqwenwork login", "auth_error");
239
- }
240
-
241
- // 全号聚合:逐号拉 qwork 切片求并集(国际站实测该切片 2 个模型,多号互相印证)
242
- let cache = null;
243
- let fetchedAt = 0;
244
- function eachAccount() {
245
- const out = [];
246
- const seen = new Set();
247
- const push = (key, auth) => {
248
- const account = accountFor(key);
249
- if (!account?.accessToken || seen.has(account.accessToken)) return;
250
- seen.add(account.accessToken);
251
- out.push(account);
252
- };
253
- for (const k of keys) {
254
- const blob = accountFromBlob(k);
255
- if (!blob?.accessToken) continue;
256
- const auth = authList.find((a) => String(a?.refreshToken || "") === String(blob.refreshToken || "")) || {};
257
- push(k, auth);
258
- }
259
- if (!out.length) {
260
- try {
261
- for (const { uid, doc } of listAccountDocs()) {
262
- const a = doc?.auth || {};
263
- if (!a.accessToken) continue;
264
- const account = { id: uid, uid, nickname: doc?.account?.name || "", email: doc?.account?.email || "", accessToken: a.accessToken, refreshToken: a.refreshToken || "" };
265
- if (seen.has(account.accessToken)) continue;
266
- seen.add(account.accessToken);
267
- out.push(account);
268
- }
269
- } catch {}
270
- }
271
- return out;
272
- }
273
-
274
- async function listModels() {
275
- const now = Date.now();
276
- if (cache && now - fetchedAt < MODELS_CACHE_MS) return cache;
277
- const accounts = eachAccount();
278
- if (!accounts.length) return [];
279
- const seen = new Set();
280
- const out = [];
281
- for (const account of accounts) {
282
- try {
283
- const list = await modelList(account, fetchImpl);
284
- for (const m of list) {
285
- if (!m?.key || seen.has(m.key)) continue;
286
- seen.add(m.key);
287
- out.push({
288
- id: joinModelId(id, m.key),
289
- object: "model",
290
- created: 1790000000,
291
- owned_by: "globalqwenwork",
292
- name: m.name || m.key,
293
- context_length: m.maxInputTokens || CONTEXT_LENGTH,
294
- price_factor: m.priceFactor || 0,
295
- is_reasoning: !!m.isReasoning,
296
- });
297
- }
298
- } catch {}
299
- }
300
- if (out.length) { cache = out; fetchedAt = now; }
301
- // 取数失败时用实测快照兜底(国际站 qwork 切片恒为 2 个,不会空)
302
- if (!out.length && !cache) {
303
- return KNOWN_MODELS.map((m) => ({
304
- id: joinModelId(id, m.key),
305
- object: "model",
306
- created: 1790000000,
307
- owned_by: "globalqwenwork",
308
- name: m.name,
309
- context_length: CONTEXT_LENGTH,
310
- price_factor: m.priceFactor,
311
- is_reasoning: false,
312
- }));
313
- }
314
- return out.length ? out : (cache || []);
315
- }
316
-
317
- async function preheat() {
318
- try {
319
- const list = await listModels();
320
- return list.length ? { ok: true } : { ok: false, error: "no account" };
321
- } catch (e) { return { ok: false, error: String(e?.message || e).slice(0, 120) }; }
322
- }
323
-
324
- async function close() {}
325
- async function chatWithKeys(body, keysOverride, opts) {
326
- const tmp = createGlobalQwenworkProvider({ id, apiKeys: keysOverride, file, fetchImpl, cooldownMs, quotaCooldownMs, connectTimeoutMs });
327
- return tmp.chat(body, opts);
328
- }
329
-
330
- return { id, chat, chatWithKeys, listModels, preheat, close, keyRing: ring, baseUrl: "globalqwenwork://native" };
331
- }
1
+ // globalqwenwork provider 门面:多号 keyring 轮转 + 401/403 refresh 重试 + 额度长冷却换号。
2
+ // 协议细节全部下沉到 cosy/upstream/payload/sse/stream;这里只做编排(对齐 qwenwork/index.js 形状)。
3
+ // vendor 自 src/providers/qwenwork/index.js,改动面 = 命名空间(id/env/响应头/提示语)+ 默认模型名,
4
+ // 中继逻辑逐字同构:实测国际站接受 cn 同款 COSY 签名与 chat body(2026-10-01 现网 200 出词)。
5
+ // Note: 为何独立 provider 而非 qwenwork 的新 region — 见 docs/adr/0041-globalqwenwork-international-provider.md
6
+ import { loadProviderKeys, loadProviderConfig } from "../../state.js";
7
+ import { listAccountDocs, accountFromBlob, updateGlobalQwenworkTokens } from "./account-store.js";
8
+ import { createKeyRing } from "../keyring.js";
9
+ import { envInt } from "../base.js";
10
+ import { compatFetch, timeoutSignal } from "../../compat.js";
11
+ import { joinModelId } from "../model-id.js";
12
+ import { buildBody, mapModel } from "./payload.js";
13
+ import { chatRequest, modelList, refreshDeviceToken, expiryUnix, userInfo } from "./upstream.js";
14
+ import { invalidateSession } from "./cosy.js";
15
+ import { collectSync, isCreditsExhausted, creditsExhaustedText } from "./sse.js";
16
+ import { transformStream } from "./stream.js";
17
+ import { KNOWN_MODELS, MODELS_CACHE_MS, CHAT_URL, CONTEXT_LENGTH, DEFAULT_MODEL } from "./constants.js";
18
+
19
+ const ID_PREFIX = "globalqwenwork/";
20
+
21
+ function hostOf(url) {
22
+ try { return new URL(String(url)).host; } catch { return "-"; }
23
+ }
24
+
25
+ function errRes(status, msg, type = "upstream_error", extra = {}) {
26
+ return new Response(JSON.stringify({ error: { message: msg, type } }), {
27
+ status,
28
+ headers: { "Content-Type": "application/json", ...extra },
29
+ });
30
+ }
31
+
32
+ export function createGlobalQwenworkProvider({
33
+ id = "globalqwenwork",
34
+ apiKeys,
35
+ file,
36
+ fetchImpl,
37
+ cooldownMs = envInt("MSLXDFF_GLOBALQWENWORK_COOLDOWN_MS", 30_000),
38
+ quotaCooldownMs = envInt("MSLXDFF_GLOBALQWENWORK_QUOTA_COOLDOWN_MS", 3600_000),
39
+ connectTimeoutMs = Number(process.env.MSLXDFF_GLOBALQWENWORK_TIMEOUT_MS) || 120_000,
40
+ } = {}) {
41
+ // 单测隔离(MSLXDFF_TEST=1 / NODE_ENV=test / --test 进程):state 与 auth 目录走测试替身,
42
+ // 与你个人真实账号完全隔离(防止单测把真号读进来、把真 refreshToken 写回去)。
43
+ // 教训继承自 qwenwork:构造传了 `apiKeys: []`,门面却从 state.json 读出真号 → 无号测试打了现网。
44
+ const testIsolated = process.env.MSLXDFF_TEST === "1" || process.env.NODE_ENV === "test" || String(process.execArgv || []).includes("--test") || process.argv.slice(1).some((a) => String(a).includes("--test"));
45
+ const keys = (() => {
46
+ if (testIsolated && file === undefined && apiKeys !== undefined) {
47
+ return [...new Set((Array.isArray(apiKeys) ? apiKeys : []).map((k) => String(k).trim()).filter(Boolean))];
48
+ }
49
+ if (Array.isArray(apiKeys) && apiKeys.length) return [...new Set(apiKeys.map((k) => String(k).trim()).filter(Boolean))];
50
+ try { return loadProviderKeys(id, file ? { file } : {}); } catch { return []; }
51
+ })();
52
+ let authList = [];
53
+ if (!testIsolated) {
54
+ try { authList = loadProviderConfig(id, file ? { file } : {})?.auths || []; } catch {}
55
+ try {
56
+ const docs = listAccountDocs();
57
+ for (const d of docs) {
58
+ const row = authList.find((a) => String(a?.uid) === String(d.uid));
59
+ if (!row) authList.push({ uid: d.uid, refreshToken: d.doc?.auth?.refreshToken || "", name: d.doc?.account?.name || "" });
60
+ }
61
+ } catch {}
62
+ }
63
+ const ring = createKeyRing(keys, { cooldownMs });
64
+ const exhaustedByQuota = new Set();
65
+ if (!fetchImpl) fetchImpl = compatFetch;
66
+ const cdbg = (...a) => { if (process.env.GLOBALQWENWORK_DEBUG === "1") console.log("[globalqwenwork-chat]", ...a); };
67
+
68
+ // blob → 上游 account(含 uid/nickname/email,供 COSY 身份 payload 用)
69
+ function accountFor(key) {
70
+ const blob = accountFromBlob(key);
71
+ if (!blob?.accessToken) return null;
72
+ const auth = authList.find((a) => String(a?.refreshToken || "") === String(blob.refreshToken || "")) || {};
73
+ let doc = null;
74
+ try {
75
+ const docs = listAccountDocs();
76
+ doc = docs.find((d) => String(d.uid) === String(auth.uid)) || docs.find((d) => d.doc?.auth?.refreshToken === blob.refreshToken) || null;
77
+ } catch {}
78
+ return {
79
+ id: auth.uid || doc?.uid || "",
80
+ uid: auth.uid || doc?.uid || "",
81
+ nickname: auth.name || doc?.doc?.account?.name || "",
82
+ email: doc?.doc?.account?.email || "",
83
+ accessToken: blob.accessToken,
84
+ refreshToken: blob.refreshToken || auth.refreshToken || doc?.doc?.auth?.refreshToken || "",
85
+ };
86
+ }
87
+
88
+ function withEcho(res, account, upstreamStatus) {
89
+ try {
90
+ res.headers.set("x-mslxdff-upstream", hostOf(CHAT_URL));
91
+ if (account?.uid) res.headers.set("x-mslxdff-globalqwenwork-account", String(account.uid).slice(-8));
92
+ if (upstreamStatus) res.headers.set("x-mslxdff-globalqwenwork-upstream-status", String(upstreamStatus));
93
+ } catch {}
94
+ return res;
95
+ }
96
+ function withQuota(res) {
97
+ try { res.headers.set("x-mslxdff-globalqwenwork-quota", "1"); } catch {}
98
+ return res;
99
+ }
100
+
101
+ async function tryRefresh(account) {
102
+ if (!account?.refreshToken) return false;
103
+ try {
104
+ // 实测 .ai 的 /api/v1/deviceToken/refresh 接受 cn 原形状 {refresh_token}(200,且轮换 refresh_token)
105
+ const data = await refreshDeviceToken(account.refreshToken, fetchImpl);
106
+ const access = data.token || data.device_token;
107
+ const refresh = data.refresh_token || account.refreshToken;
108
+ const expiresAt = expiryUnix(data);
109
+ account.accessToken = access;
110
+ account.refreshToken = refresh;
111
+ try { await updateGlobalQwenworkTokens({ uid: account.uid, accessToken: access, refreshToken: refresh, expiresAt, file }); }
112
+ catch { invalidateSession(account); }
113
+ // 同步 ring 里的 blob(token 轮换后旧 blob 即失效)
114
+ try {
115
+ const oldKey = ring.keys.find((k) => { const b = accountFromBlob(k); return b && b.refreshToken === data.refresh_token; }) || null;
116
+ const newBlob = JSON.stringify({ device_token: access, refresh_token: refresh });
117
+ const cur = ring.keys.find((k) => accountFromBlob(k)?.accessToken === account.accessToken);
118
+ if (cur) ring.replace(cur, newBlob);
119
+ else if (oldKey) ring.replace(oldKey, newBlob);
120
+ } catch {}
121
+ return true;
122
+ } catch (e) {
123
+ cdbg(`[refresh-fail] uid=${String(account.uid).slice(-8)} ${String(e?.message || e).slice(0, 160)}`);
124
+ return false;
125
+ }
126
+ }
127
+
128
+ async function chat(body, opts) {
129
+ const stream = body?.stream !== false;
130
+ const wantModel = typeof body?.model === "string" && body.model.startsWith(ID_PREFIX)
131
+ ? body.model.slice(ID_PREFIX.length)
132
+ : (body?.model || DEFAULT_MODEL);
133
+ const modelKey = mapModel(wantModel);
134
+ const stripped = { ...body, model: wantModel };
135
+ const total = ring.keys.length;
136
+ // 构造参数无号但 auth 目录有号:不视为"无号",直接按 401 引导 login——调用方先 login 落盘再 chat
137
+ if (!total) {
138
+ return errRes(401, "globalqwenwork: 无可用账号 — 先跑 mslxdff -provider globalqwenwork login", "auth_error");
139
+ }
140
+ let lastRes = null;
141
+ const refreshed = new Set();
142
+ for (let hop = 0; hop <= total; hop++) {
143
+ const key = ring.next();
144
+ if (!key) break;
145
+ if (exhaustedByQuota.has(key)) {
146
+ if (ring.isCooling(key)) {
147
+ if (ring.available() > 0) continue;
148
+ break;
149
+ }
150
+ exhaustedByQuota.delete(key);
151
+ }
152
+ let account = accountFor(key);
153
+ if (!account) { try { ring.onError(key); } catch {} continue; }
154
+ // 身份缺失(手动配 key、未走 login)时补一次 userinfo:COSY 身份 payload 的 uid/name/email 为空
155
+ // 会被上游判 `code 101 Signature invalid`(cn 实测同 token 空身份 403、有身份 200;.ai 同构)。
156
+ if (!account.uid) {
157
+ try {
158
+ const info = await userInfo(account.accessToken, fetchImpl);
159
+ if (info?.id) account.uid = String(info.id);
160
+ if (info?.name) account.nickname = String(info.name);
161
+ if (info?.email) account.email = String(info.email);
162
+ if (account.uid) {
163
+ try {
164
+ const { saveGlobalQwenworkAccount } = await import("./account-store.js");
165
+ await saveGlobalQwenworkAccount({ uid: account.uid, accessToken: account.accessToken, refreshToken: account.refreshToken || "", name: account.nickname || "", email: account.email || "", file });
166
+ } catch {}
167
+ }
168
+ } catch (e) {
169
+ cdbg(`[identity-enrich-fail] ${String(e?.message || e).slice(0, 160)}`);
170
+ }
171
+ if (!account.uid) { try { ring.onError(key); } catch {} lastRes = withEcho(errRes(401, "globalqwenwork: 账号身份缺失且 userinfo 失败,该 key 不可用", "auth_error"), account, 401); continue; }
172
+ }
173
+ const bodyStr = buildBody(stripped, modelKey);
174
+ let upRes;
175
+ try {
176
+ upRes = await chatRequest(account, modelKey, bodyStr, timeoutSignal(connectTimeoutMs), fetchImpl);
177
+ } catch (e) {
178
+ try { ring.onError(key); } catch {}
179
+ lastRes = withEcho(errRes(502, `globalqwenwork: 上游连接失败 ${String(e?.message || e).slice(0, 200)}`), account, 502);
180
+ continue;
181
+ }
182
+ if (!upRes.ok) {
183
+ const text = await upRes.text().catch(() => "");
184
+ const quota = isCreditsExhausted(upRes.status, text);
185
+ if (quota) {
186
+ try { ring.onError(key, quotaCooldownMs); } catch {}
187
+ exhaustedByQuota.add(key);
188
+ lastRes = withQuota(withEcho(errRes(429, `globalqwenwork: 账号积分耗尽 ${text.slice(0, 200)}`, "quota_exhausted"), account, upRes.status));
189
+ if (ring.available() > 0) continue;
190
+ break;
191
+ }
192
+ if ((upRes.status === 401 || upRes.status === 403) && account.refreshToken && !refreshed.has(key)) {
193
+ refreshed.add(key);
194
+ const ok = await tryRefresh(account);
195
+ if (ok) { hop -= 1; continue; }
196
+ try { ring.onError(key); } catch {}
197
+ lastRes = withEcho(errRes(upRes.status, `globalqwenwork: 授权失效 ${text.slice(0, 200)}`, "auth_error"), account, upRes.status);
198
+ continue;
199
+ }
200
+ try { ring.onError(key); } catch {}
201
+ lastRes = withEcho(errRes(upRes.status >= 500 ? 502 : upRes.status, `globalqwenwork: 上游 ${upRes.status} ${text.slice(0, 200)}`), account, upRes.status);
202
+ if (ring.available() > 0 && (upRes.status === 429 || upRes.status >= 500)) continue;
203
+ return lastRes;
204
+ }
205
+ // 200:流内仍可能裹着业务错(信封 statusCodeValue / code),非流式聚合时由 collectSync 判决;
206
+ // 流式路径照 transform 透传(错误帧由 sse.handleLine 转成流内 error 事件)。
207
+ if (stream) {
208
+ const out = new Response(transformStream(upRes.body, wantModel, () => {}), {
209
+ status: 200,
210
+ headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", Connection: "keep-alive" },
211
+ });
212
+ return withEcho(out, account, 0);
213
+ }
214
+ const result = await collectSync(upRes.body, wantModel);
215
+ if (result.error) {
216
+ if (creditsExhaustedText(result.error)) {
217
+ try { ring.onError(key, quotaCooldownMs); } catch {}
218
+ exhaustedByQuota.add(key);
219
+ lastRes = withQuota(withEcho(errRes(429, `globalqwenwork: 账号积分耗尽 ${result.error}`, "quota_exhausted"), account, 429));
220
+ if (ring.available() > 0) continue;
221
+ break;
222
+ }
223
+ try { ring.onError(key); } catch {}
224
+ lastRes = withEcho(errRes(502, `globalqwenwork: 上游流内错误 ${result.error}`), account, 502);
225
+ continue;
226
+ }
227
+ const completion = result.completion;
228
+ completion.model = wantModel;
229
+ return withEcho(new Response(JSON.stringify(completion), { status: 200, headers: { "Content-Type": "application/json" } }), account, 0);
230
+ }
231
+ if (total > 0 && ring.keys.every((k) => exhaustedByQuota.has(k))) {
232
+ return errRes(429, "globalqwenwork: 所有账号积分已用完,请更换账号或等待额度恢复", "quota_exhausted", { "x-mslxdff-globalqwenwork-quota-exhausted": "1" });
233
+ }
234
+ if (lastRes) return lastRes;
235
+ if (!ring.available()) {
236
+ return errRes(429, "globalqwenwork: 账号暂不可用(上游限流/排队冷却中),请稍后重试", "all_cooling", { "x-mslxdff-globalqwenwork-all-cooling": "1" });
237
+ }
238
+ return errRes(401, "globalqwenwork: 无可用账号 — 先跑 mslxdff -provider globalqwenwork login", "auth_error");
239
+ }
240
+
241
+ // 全号聚合:逐号拉 qwork 切片求并集(国际站实测该切片 2 个模型,多号互相印证)
242
+ let cache = null;
243
+ let fetchedAt = 0;
244
+ function eachAccount() {
245
+ const out = [];
246
+ const seen = new Set();
247
+ const push = (key, auth) => {
248
+ const account = accountFor(key);
249
+ if (!account?.accessToken || seen.has(account.accessToken)) return;
250
+ seen.add(account.accessToken);
251
+ out.push(account);
252
+ };
253
+ for (const k of keys) {
254
+ const blob = accountFromBlob(k);
255
+ if (!blob?.accessToken) continue;
256
+ const auth = authList.find((a) => String(a?.refreshToken || "") === String(blob.refreshToken || "")) || {};
257
+ push(k, auth);
258
+ }
259
+ if (!out.length) {
260
+ try {
261
+ for (const { uid, doc } of listAccountDocs()) {
262
+ const a = doc?.auth || {};
263
+ if (!a.accessToken) continue;
264
+ const account = { id: uid, uid, nickname: doc?.account?.name || "", email: doc?.account?.email || "", accessToken: a.accessToken, refreshToken: a.refreshToken || "" };
265
+ if (seen.has(account.accessToken)) continue;
266
+ seen.add(account.accessToken);
267
+ out.push(account);
268
+ }
269
+ } catch {}
270
+ }
271
+ return out;
272
+ }
273
+
274
+ async function listModels() {
275
+ const now = Date.now();
276
+ if (cache && now - fetchedAt < MODELS_CACHE_MS) return cache;
277
+ const accounts = eachAccount();
278
+ if (!accounts.length) return [];
279
+ const seen = new Set();
280
+ const out = [];
281
+ for (const account of accounts) {
282
+ try {
283
+ const list = await modelList(account, fetchImpl);
284
+ for (const m of list) {
285
+ if (!m?.key || seen.has(m.key)) continue;
286
+ seen.add(m.key);
287
+ out.push({
288
+ id: joinModelId(id, m.key),
289
+ object: "model",
290
+ created: 1790000000,
291
+ owned_by: "globalqwenwork",
292
+ name: m.name || m.key,
293
+ context_length: m.maxInputTokens || CONTEXT_LENGTH,
294
+ price_factor: m.priceFactor || 0,
295
+ is_reasoning: !!m.isReasoning,
296
+ });
297
+ }
298
+ } catch {}
299
+ }
300
+ if (out.length) { cache = out; fetchedAt = now; }
301
+ // 取数失败时用实测快照兜底(国际站 qwork 切片恒为 2 个,不会空)
302
+ if (!out.length && !cache) {
303
+ return KNOWN_MODELS.map((m) => ({
304
+ id: joinModelId(id, m.key),
305
+ object: "model",
306
+ created: 1790000000,
307
+ owned_by: "globalqwenwork",
308
+ name: m.name,
309
+ context_length: CONTEXT_LENGTH,
310
+ price_factor: m.priceFactor,
311
+ is_reasoning: false,
312
+ }));
313
+ }
314
+ return out.length ? out : (cache || []);
315
+ }
316
+
317
+ async function preheat() {
318
+ try {
319
+ const list = await listModels();
320
+ return list.length ? { ok: true } : { ok: false, error: "no account" };
321
+ } catch (e) { return { ok: false, error: String(e?.message || e).slice(0, 120) }; }
322
+ }
323
+
324
+ async function close() {}
325
+ async function chatWithKeys(body, keysOverride, opts) {
326
+ const tmp = createGlobalQwenworkProvider({ id, apiKeys: keysOverride, file, fetchImpl, cooldownMs, quotaCooldownMs, connectTimeoutMs });
327
+ return tmp.chat(body, opts);
328
+ }
329
+
330
+ return { id, chat, chatWithKeys, listModels, preheat, close, keyRing: ring, baseUrl: "globalqwenwork://native" };
331
+ }