mslxdff 0.1.203 → 0.1.207

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 (330) hide show
  1. package/README.md +3 -3
  2. package/bin/mslxdff.js +1 -1
  3. package/docs/cli_help_mini.md +146 -0
  4. package/package.json +38 -38
  5. package/plugins/README.md +14 -14
  6. package/plugins/prefer-model.mjs.example +14 -14
  7. package/src/anthropic/canonical-json.js +55 -0
  8. package/src/anthropic/count-tokens.js +61 -61
  9. package/src/anthropic/request-body.js +183 -126
  10. package/src/anthropic/request-shape.js +60 -0
  11. package/src/anthropic/session-id.js +58 -0
  12. package/src/anthropic/shared.js +18 -18
  13. package/src/anthropic/sse-aggregate.js +56 -56
  14. package/src/anthropic/tool-media.js +75 -0
  15. package/src/anthropic/translate.js +296 -287
  16. package/src/anthropic/web-search.js +361 -361
  17. package/src/api-kind.js +31 -31
  18. package/src/autostart.js +288 -288
  19. package/src/bench/probe.js +1 -1
  20. package/src/bench/runner.js +1 -1
  21. package/src/bench/via-probe.js +1 -1
  22. package/src/bench/via.js +144 -144
  23. package/src/bench/workbuddy-bench.js +1 -1
  24. package/src/chat/config.js +11 -11
  25. package/src/chat/cooling.js +81 -81
  26. package/src/chat/direct.js +58 -58
  27. package/src/chat/index.js +6 -6
  28. package/src/chat/spinner.js +38 -38
  29. package/src/chat/sse.js +69 -69
  30. package/src/chat/stats.js +247 -247
  31. package/src/chat/store.js +48 -48
  32. package/src/chat/tool-handlers.js +67 -67
  33. package/src/chat/tools.js +1 -1
  34. package/src/chat/upstream.js +59 -59
  35. package/src/chat-pipeline/auto-race.js +158 -158
  36. package/src/chat-pipeline/empty-turn.js +147 -147
  37. package/src/chat-pipeline/engine.js +17 -17
  38. package/src/chat-pipeline/index.js +8 -40
  39. package/src/chat-pipeline/serial-trial.js +279 -283
  40. package/src/chat-pipeline/terminal-verdict.js +132 -147
  41. package/src/chooser.js +54 -54
  42. package/src/claude-desktop/apply.js +101 -101
  43. package/src/claude-desktop/context1m.js +38 -38
  44. package/src/claude-desktop/doctor.js +146 -146
  45. package/src/claude-desktop/slots.js +152 -152
  46. package/src/cli/bootstrap.js +1 -1
  47. package/src/cli/commands/claude-desktop.js +284 -284
  48. package/src/cli/commands/daemon.js +231 -231
  49. package/src/cli/commands/model/list-live.js +22 -22
  50. package/src/cli/commands/model/list-providers.js +76 -76
  51. package/src/cli/commands/model/list-render.js +77 -77
  52. package/src/cli/commands/model/list-sort.js +52 -52
  53. package/src/cli/commands/model/list-tty.js +76 -76
  54. package/src/cli/commands/model/list.js +166 -166
  55. package/src/cli/commands/model/live-models.js +24 -24
  56. package/src/cli/commands/model/stats.js +43 -43
  57. package/src/cli/commands/model/status.js +47 -47
  58. package/src/cli/commands/model.js +51 -51
  59. package/src/cli/commands/provider/add.js +108 -108
  60. package/src/cli/commands/provider/allowlist.js +99 -99
  61. package/src/cli/commands/provider/cline-free.js +99 -99
  62. package/src/cli/commands/provider/cline-login.js +218 -218
  63. package/src/cli/commands/provider/cline-quota.js +65 -65
  64. package/src/cli/commands/provider/codearts-login.js +45 -45
  65. package/src/cli/commands/provider/config.js +119 -119
  66. package/src/cli/commands/provider/globalqwenwork-login.js +126 -126
  67. package/src/cli/commands/provider/keys-guard.js +50 -50
  68. package/src/cli/commands/provider/keys.js +161 -161
  69. package/src/cli/commands/provider/qoder-checkin.js +81 -81
  70. package/src/cli/commands/provider/qoder-login.js +102 -102
  71. package/src/cli/commands/provider/raccoon-login.js +180 -180
  72. package/src/cli/commands/provider/raccoon-password.js +162 -162
  73. package/src/cli/commands/provider/raccoon-quota.js +109 -109
  74. package/src/cli/commands/provider/traework-checkin.js +144 -144
  75. package/src/cli/commands/provider/traework-login.js +164 -164
  76. package/src/cli/commands/provider/workbuddy-login.js +153 -153
  77. package/src/cli/commands/provider/zcode-login.js +77 -77
  78. package/src/cli/commands/provider/zcode-quota.js +55 -55
  79. package/src/cli/commands/provider.js +1 -1
  80. package/src/cli/commands/stats.js +158 -149
  81. package/src/cli/commands/system.js +261 -261
  82. package/src/cli/commands/timezone.js +47 -47
  83. package/src/cli/commands/use-group.js +60 -60
  84. package/src/cli/commands/workbuddy.js +189 -189
  85. package/src/cli/format.js +141 -141
  86. package/src/cli/group-helpers.js +70 -70
  87. package/src/cli/help.js +80 -80
  88. package/src/cli/index.js +70 -70
  89. package/src/cli/interactive.js +121 -121
  90. package/src/cli/join-core.js +99 -99
  91. package/src/cli/join-wizard.js +175 -175
  92. package/src/cli/policy.js +136 -136
  93. package/src/cli/provider-row.js +126 -126
  94. package/src/cli/util.js +24 -24
  95. package/src/compat.js +53 -53
  96. package/src/events.js +38 -38
  97. package/src/free-lane.js +131 -131
  98. package/src/free-watcher.js +63 -63
  99. package/src/groups.js +262 -262
  100. package/src/logs.js +195 -195
  101. package/src/metrics.js +71 -62
  102. package/src/model-capabilities/context-lookup.js +65 -65
  103. package/src/model-capabilities/merge.js +142 -142
  104. package/src/model-trace.js +187 -187
  105. package/src/models.js +1 -1
  106. package/src/opencode-identity.js +72 -72
  107. package/src/peers.js +254 -254
  108. package/src/plugins.js +107 -107
  109. package/src/providers/AGENTS.md +49 -0
  110. package/src/providers/base.js +164 -164
  111. package/src/providers/classify.js +22 -22
  112. package/src/providers/cline/allowlist-sync.js +93 -93
  113. package/src/providers/cline/free-catalog.js +71 -71
  114. package/src/providers/cline/headers.js +37 -37
  115. package/src/providers/cline/index.js +130 -130
  116. package/src/providers/cline/models.js +150 -150
  117. package/src/providers/cline/usage.js +274 -274
  118. package/src/providers/codearts/auth-pool.js +148 -148
  119. package/src/providers/codearts/concurrency.js +34 -34
  120. package/src/providers/codearts/const.js +65 -65
  121. package/src/providers/codearts/dpop.js +95 -95
  122. package/src/providers/codearts/index.js +175 -175
  123. package/src/providers/codearts/login.js +214 -214
  124. package/src/providers/codearts/models.js +182 -182
  125. package/src/providers/codearts/sign.js +96 -96
  126. package/src/providers/codearts/sse.js +204 -204
  127. package/src/providers/codearts/stream.js +209 -209
  128. package/src/providers/codearts/sts.js +124 -124
  129. package/src/providers/codearts.js +1 -1
  130. package/src/providers/dispatcher.js +146 -146
  131. package/src/providers/generic.js +156 -156
  132. package/src/providers/globalqwenwork/account-store.js +141 -141
  133. package/src/providers/globalqwenwork/constants.js +73 -73
  134. package/src/providers/globalqwenwork/crypto.js +2 -2
  135. package/src/providers/globalqwenwork/http.js +2 -2
  136. package/src/providers/globalqwenwork/index.js +331 -331
  137. package/src/providers/globalqwenwork/payload.js +147 -147
  138. package/src/providers/globalqwenwork/rsa.js +2 -2
  139. package/src/providers/globalqwenwork/sse.js +2 -2
  140. package/src/providers/globalqwenwork/stream.js +2 -2
  141. package/src/providers/globalqwenwork.js +1 -1
  142. package/src/providers/globalworkbuddy/account-store.js +198 -198
  143. package/src/providers/globalworkbuddy/auth.js +97 -97
  144. package/src/providers/globalworkbuddy/catalog.js +138 -138
  145. package/src/providers/globalworkbuddy/constants.js +73 -73
  146. package/src/providers/globalworkbuddy/device-auth.js +120 -120
  147. package/src/providers/globalworkbuddy/errors.js +98 -98
  148. package/src/providers/globalworkbuddy/headers.js +89 -89
  149. package/src/providers/globalworkbuddy/models.js +96 -96
  150. package/src/providers/globalworkbuddy/payload.js +80 -80
  151. package/src/providers/index.js +5 -5
  152. package/src/providers/model-id.js +172 -172
  153. package/src/providers/opencode.js +19 -19
  154. package/src/providers/openrouter.js +31 -31
  155. package/src/providers/qoder/account-store.js +106 -106
  156. package/src/providers/qoder/aggregate.js +60 -60
  157. package/src/providers/qoder/baseprompt.json +591 -591
  158. package/src/providers/qoder/checkin.js +160 -160
  159. package/src/providers/qoder/constants.js +44 -44
  160. package/src/providers/qoder/encode.js +41 -41
  161. package/src/providers/qoder/fingerprint.js +38 -38
  162. package/src/providers/qoder/models.js +77 -77
  163. package/src/providers/qoder/oauth.js +84 -84
  164. package/src/providers/qoder/payload.js +162 -162
  165. package/src/providers/qoder/request.js +27 -27
  166. package/src/providers/qoder/session.js +134 -134
  167. package/src/providers/qoder/sticky.js +38 -38
  168. package/src/providers/qoder/stream.js +116 -116
  169. package/src/providers/qoder.js +1 -1
  170. package/src/providers/raccoon/account-store.js +216 -216
  171. package/src/providers/raccoon/auth.js +67 -67
  172. package/src/providers/raccoon/chat.js +228 -228
  173. package/src/providers/raccoon/const.js +180 -180
  174. package/src/providers/raccoon/credits.js +185 -185
  175. package/src/providers/raccoon/crypto.js +43 -43
  176. package/src/providers/raccoon/envelope.js +31 -31
  177. package/src/providers/raccoon/headers.js +45 -45
  178. package/src/providers/raccoon/index.js +216 -216
  179. package/src/providers/raccoon/login.js +279 -279
  180. package/src/providers/raccoon/models.js +160 -160
  181. package/src/providers/raccoon/password-login.js +102 -102
  182. package/src/providers/raccoon/qr-codewords.js +115 -115
  183. package/src/providers/raccoon/qr.js +236 -236
  184. package/src/providers/raccoon/sse.js +135 -135
  185. package/src/providers/responses-channel.js +120 -120
  186. package/src/providers/share-keys.js +53 -53
  187. package/src/providers/traework/account-store.js +234 -234
  188. package/src/providers/traework/chat.js +217 -217
  189. package/src/providers/traework/checkin.js +77 -77
  190. package/src/providers/traework/constants.js +41 -41
  191. package/src/providers/traework/errors.js +52 -52
  192. package/src/providers/traework/headers.js +79 -79
  193. package/src/providers/traework/index.js +132 -132
  194. package/src/providers/traework/market-identity.js +116 -116
  195. package/src/providers/traework/models.js +98 -98
  196. package/src/providers/traework/payload.js +86 -86
  197. package/src/providers/traework/sse.js +184 -184
  198. package/src/providers/traework/token.js +52 -52
  199. package/src/providers/traework.js +2 -2
  200. package/src/providers/workbuddy/account-store.js +186 -186
  201. package/src/providers/workbuddy/auth.js +158 -158
  202. package/src/providers/workbuddy/balance.js +86 -86
  203. package/src/providers/workbuddy/cat-travel.js +125 -125
  204. package/src/providers/workbuddy/checkin.js +75 -75
  205. package/src/providers/workbuddy/desktop-info.js +78 -78
  206. package/src/providers/workbuddy/growth-api.js +67 -67
  207. package/src/providers/workbuddy/growth-plans.js +63 -63
  208. package/src/providers/workbuddy/growth.js +181 -181
  209. package/src/providers/workbuddy/index.js +217 -217
  210. package/src/providers/workbuddy/models.js +126 -126
  211. package/src/providers/workbuddy/payload.js +141 -141
  212. package/src/providers/workbuddy/rotation-log.js +83 -83
  213. package/src/providers/workbuddy/sanitize-tools.js +61 -61
  214. package/src/providers/workbuddy/sdk-chat.js +26 -26
  215. package/src/providers/workbuddy-balance.js +78 -78
  216. package/src/providers/zcode/account-store.js +129 -129
  217. package/src/providers/zcode/auth.js +28 -28
  218. package/src/providers/zcode/chat.js +179 -179
  219. package/src/providers/zcode/const.js +54 -54
  220. package/src/providers/zcode/context-shape.js +162 -162
  221. package/src/providers/zcode/headers.js +93 -93
  222. package/src/providers/zcode/index.js +124 -124
  223. package/src/providers/zcode/models.js +65 -65
  224. package/src/providers/zcode/oauth.js +120 -120
  225. package/src/providers/zcode/quota.js +176 -176
  226. package/src/providers/zcode/sse.js +196 -196
  227. package/src/readline-compat.js +25 -25
  228. package/src/responses/translate.js +311 -311
  229. package/src/routes/AGENTS.md +37 -0
  230. package/src/routes/chat/broadband-handler.js +72 -72
  231. package/src/routes/chat/empty-turn-report.js +31 -31
  232. package/src/routes/chat/exhausted-handler.js +87 -87
  233. package/src/routes/chat/hedge-handler.js +130 -130
  234. package/src/routes/chat/index.js +1 -1
  235. package/src/routes/chat/local-handler.js +63 -63
  236. package/src/routes/chat/peer-handler.js +81 -81
  237. package/src/routes/chat.js +3 -3
  238. package/src/routes/fallback.js +88 -88
  239. package/src/routes/groups-relay.js +214 -214
  240. package/src/routes/groups.js +122 -122
  241. package/src/routes/helpers.js +93 -83
  242. package/src/routes/index.js +147 -147
  243. package/src/routes/messages-route.js +375 -365
  244. package/src/routes/peers.js +10 -2
  245. package/src/routes/relay-queue.js +181 -181
  246. package/src/routes/relay.js +60 -60
  247. package/src/routes/responses-route.js +213 -213
  248. package/src/routes/stream-body.js +84 -84
  249. package/src/routes/stream-hold.js +337 -337
  250. package/src/routes/stream-scan.js +366 -366
  251. package/src/routes.js +3 -3
  252. package/src/runtime/auto-update.js +113 -113
  253. package/src/runtime/broadband-stream.js +79 -79
  254. package/src/runtime/broadband.js +103 -103
  255. package/src/runtime/group-sync.js +81 -81
  256. package/src/runtime/keep-warm.js +120 -120
  257. package/src/runtime/lifecycle-forensics.js +116 -116
  258. package/src/runtime/lifecycle-log.js +87 -87
  259. package/src/runtime/log-stamp.js +73 -73
  260. package/src/runtime/proc-info.js +137 -137
  261. package/src/runtime/qoder-checkin.js +93 -93
  262. package/src/runtime/raccoon-checkin.js +126 -126
  263. package/src/runtime/server-lifecycle.js +189 -189
  264. package/src/runtime/traework-checkin.js +118 -118
  265. package/src/runtime/workbuddy-checkin.js +120 -120
  266. package/src/runtime/workbuddy-growth.js +133 -133
  267. package/src/state/facade.js +58 -58
  268. package/src/state/memory.js +162 -162
  269. package/src/state/merge.js +25 -25
  270. package/src/state/migrations/cline-unify.js +94 -94
  271. package/src/state/migrations.js +33 -33
  272. package/src/state/persist.js +42 -42
  273. package/src/state/provider-config.js +142 -142
  274. package/src/state/schemas/group.js +31 -31
  275. package/src/state/schemas/model.js +94 -94
  276. package/src/state/schemas/peer.js +31 -31
  277. package/src/state/schemas/port.js +10 -10
  278. package/src/state/schemas/provider.js +202 -202
  279. package/src/state/schemas/timezone.js +70 -70
  280. package/src/state/schemas/token.js +48 -48
  281. package/src/state/schemas/use-group.js +102 -102
  282. package/src/state/store.js +176 -176
  283. package/src/state.js +1 -1
  284. package/src/sync-claude-desktop.js +234 -234
  285. package/src/sync-claude.js +152 -152
  286. package/src/sync-codex.js +116 -116
  287. package/src/sync-workbuddy.js +158 -158
  288. package/src/talk-full-redact.js +129 -129
  289. package/src/talk-full.js +131 -131
  290. package/src/talk-log.js +257 -257
  291. package/src/text-tokens.js +52 -52
  292. package/src/thinking-effort.js +67 -67
  293. package/src/time.js +106 -106
  294. package/src/timeline.js +26 -26
  295. package/src/transport/index.js +1 -1
  296. package/src/upstream-engine/index.js +74 -74
  297. package/src/upstream-engine/mode.js +15 -15
  298. package/src/upstream-engine/sdk/attempt.js +221 -221
  299. package/src/upstream-engine/sdk/chat.js +28 -28
  300. package/src/upstream-engine/sdk/convert.js +192 -192
  301. package/src/upstream-engine/sdk/diagnose.js +62 -62
  302. package/src/upstream-engine/sdk/dispatch.js +55 -55
  303. package/src/upstream-engine/sdk/responses.js +176 -176
  304. package/src/upstream-engine/sdk/sse.js +166 -166
  305. package/src/upstream-responses.js +305 -305
  306. package/src/usage/cache-fields.js +58 -0
  307. package/src/usage/record.js +109 -106
  308. package/src/usage/report.js +209 -195
  309. package/src/v6pool/claim.js +157 -157
  310. package/src/v6pool/config.js +65 -65
  311. package/src/v6pool/detect.js +130 -130
  312. package/src/v6pool/index.js +152 -152
  313. package/src/v6pool/prefix.js +140 -140
  314. package/src/v6pool/probe.js +40 -40
  315. package/src/v6pool/report.js +104 -104
  316. package/src/v6pool/rotate.js +263 -263
  317. package/src/warp-pool/config.js +74 -74
  318. package/src/warp-pool/health.js +106 -106
  319. package/src/warp-pool/identity.js +141 -141
  320. package/src/warp-pool/index.js +358 -358
  321. package/src/warp-pool/keys.js +29 -29
  322. package/src/warp-pool/nodes.js +95 -95
  323. package/src/warp-pool/ports.js +160 -160
  324. package/src/warp-pool/register.js +113 -113
  325. package/src/warp-pool/report.js +232 -232
  326. package/src/warp-pool/revive.js +168 -168
  327. package/src/warp-pool/rotate.js +214 -214
  328. package/src/warp-pool/strategy.js +43 -43
  329. package/src/warp-pool/verdict.js +69 -69
  330. package/src/warp-pool/wireproxy.js +162 -162
@@ -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
+ }