mslxdff 0.1.203 → 0.1.208

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 (332) hide show
  1. package/README.md +3 -3
  2. package/bin/mslxdff.js +1 -1
  3. package/docs/cli_help_mini.md +147 -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/v6-pool.js +123 -0
  85. package/src/cli/commands/workbuddy.js +189 -189
  86. package/src/cli/format.js +141 -141
  87. package/src/cli/group-helpers.js +70 -70
  88. package/src/cli/help.js +80 -80
  89. package/src/cli/index.js +73 -70
  90. package/src/cli/interactive.js +121 -121
  91. package/src/cli/join-core.js +99 -99
  92. package/src/cli/join-wizard.js +175 -175
  93. package/src/cli/policy.js +136 -136
  94. package/src/cli/provider-row.js +126 -126
  95. package/src/cli/util.js +24 -24
  96. package/src/compat.js +53 -53
  97. package/src/events.js +38 -38
  98. package/src/free-lane.js +131 -131
  99. package/src/free-watcher.js +63 -63
  100. package/src/groups.js +262 -262
  101. package/src/logs.js +195 -195
  102. package/src/metrics.js +71 -62
  103. package/src/model-capabilities/context-lookup.js +65 -65
  104. package/src/model-capabilities/merge.js +142 -142
  105. package/src/model-trace.js +187 -187
  106. package/src/models.js +1 -1
  107. package/src/opencode-identity.js +72 -72
  108. package/src/peers.js +254 -254
  109. package/src/plugins.js +107 -107
  110. package/src/providers/AGENTS.md +49 -0
  111. package/src/providers/base.js +164 -164
  112. package/src/providers/classify.js +22 -22
  113. package/src/providers/cline/allowlist-sync.js +93 -93
  114. package/src/providers/cline/free-catalog.js +71 -71
  115. package/src/providers/cline/headers.js +37 -37
  116. package/src/providers/cline/index.js +130 -130
  117. package/src/providers/cline/models.js +150 -150
  118. package/src/providers/cline/usage.js +274 -274
  119. package/src/providers/codearts/auth-pool.js +148 -148
  120. package/src/providers/codearts/concurrency.js +34 -34
  121. package/src/providers/codearts/const.js +65 -65
  122. package/src/providers/codearts/dpop.js +95 -95
  123. package/src/providers/codearts/index.js +175 -175
  124. package/src/providers/codearts/login.js +214 -214
  125. package/src/providers/codearts/models.js +182 -182
  126. package/src/providers/codearts/sign.js +96 -96
  127. package/src/providers/codearts/sse.js +204 -204
  128. package/src/providers/codearts/stream.js +209 -209
  129. package/src/providers/codearts/sts.js +124 -124
  130. package/src/providers/codearts.js +1 -1
  131. package/src/providers/dispatcher.js +146 -146
  132. package/src/providers/generic.js +156 -156
  133. package/src/providers/globalqwenwork/account-store.js +141 -141
  134. package/src/providers/globalqwenwork/constants.js +73 -73
  135. package/src/providers/globalqwenwork/crypto.js +2 -2
  136. package/src/providers/globalqwenwork/http.js +2 -2
  137. package/src/providers/globalqwenwork/index.js +331 -331
  138. package/src/providers/globalqwenwork/payload.js +147 -147
  139. package/src/providers/globalqwenwork/rsa.js +2 -2
  140. package/src/providers/globalqwenwork/sse.js +2 -2
  141. package/src/providers/globalqwenwork/stream.js +2 -2
  142. package/src/providers/globalqwenwork.js +1 -1
  143. package/src/providers/globalworkbuddy/account-store.js +198 -198
  144. package/src/providers/globalworkbuddy/auth.js +97 -97
  145. package/src/providers/globalworkbuddy/catalog.js +138 -138
  146. package/src/providers/globalworkbuddy/constants.js +73 -73
  147. package/src/providers/globalworkbuddy/device-auth.js +120 -120
  148. package/src/providers/globalworkbuddy/errors.js +98 -98
  149. package/src/providers/globalworkbuddy/headers.js +89 -89
  150. package/src/providers/globalworkbuddy/models.js +96 -96
  151. package/src/providers/globalworkbuddy/payload.js +80 -80
  152. package/src/providers/index.js +5 -5
  153. package/src/providers/model-id.js +172 -172
  154. package/src/providers/opencode.js +19 -19
  155. package/src/providers/openrouter.js +31 -31
  156. package/src/providers/qoder/account-store.js +106 -106
  157. package/src/providers/qoder/aggregate.js +60 -60
  158. package/src/providers/qoder/baseprompt.json +591 -591
  159. package/src/providers/qoder/checkin.js +160 -160
  160. package/src/providers/qoder/constants.js +44 -44
  161. package/src/providers/qoder/encode.js +41 -41
  162. package/src/providers/qoder/fingerprint.js +38 -38
  163. package/src/providers/qoder/models.js +77 -77
  164. package/src/providers/qoder/oauth.js +84 -84
  165. package/src/providers/qoder/payload.js +162 -162
  166. package/src/providers/qoder/request.js +27 -27
  167. package/src/providers/qoder/session.js +134 -134
  168. package/src/providers/qoder/sticky.js +38 -38
  169. package/src/providers/qoder/stream.js +116 -116
  170. package/src/providers/qoder.js +1 -1
  171. package/src/providers/raccoon/account-store.js +216 -216
  172. package/src/providers/raccoon/auth.js +67 -67
  173. package/src/providers/raccoon/chat.js +228 -228
  174. package/src/providers/raccoon/const.js +180 -180
  175. package/src/providers/raccoon/credits.js +185 -185
  176. package/src/providers/raccoon/crypto.js +43 -43
  177. package/src/providers/raccoon/envelope.js +31 -31
  178. package/src/providers/raccoon/headers.js +45 -45
  179. package/src/providers/raccoon/index.js +216 -216
  180. package/src/providers/raccoon/login.js +279 -279
  181. package/src/providers/raccoon/models.js +160 -160
  182. package/src/providers/raccoon/password-login.js +102 -102
  183. package/src/providers/raccoon/qr-codewords.js +115 -115
  184. package/src/providers/raccoon/qr.js +236 -236
  185. package/src/providers/raccoon/sse.js +135 -135
  186. package/src/providers/responses-channel.js +120 -120
  187. package/src/providers/share-keys.js +53 -53
  188. package/src/providers/traework/account-store.js +234 -234
  189. package/src/providers/traework/chat.js +217 -217
  190. package/src/providers/traework/checkin.js +77 -77
  191. package/src/providers/traework/constants.js +41 -41
  192. package/src/providers/traework/errors.js +52 -52
  193. package/src/providers/traework/headers.js +79 -79
  194. package/src/providers/traework/index.js +132 -132
  195. package/src/providers/traework/market-identity.js +116 -116
  196. package/src/providers/traework/models.js +98 -98
  197. package/src/providers/traework/payload.js +86 -86
  198. package/src/providers/traework/sse.js +184 -184
  199. package/src/providers/traework/token.js +52 -52
  200. package/src/providers/traework.js +2 -2
  201. package/src/providers/workbuddy/account-store.js +186 -186
  202. package/src/providers/workbuddy/auth.js +158 -158
  203. package/src/providers/workbuddy/balance.js +86 -86
  204. package/src/providers/workbuddy/cat-travel.js +125 -125
  205. package/src/providers/workbuddy/checkin.js +75 -75
  206. package/src/providers/workbuddy/desktop-info.js +78 -78
  207. package/src/providers/workbuddy/growth-api.js +67 -67
  208. package/src/providers/workbuddy/growth-plans.js +63 -63
  209. package/src/providers/workbuddy/growth.js +181 -181
  210. package/src/providers/workbuddy/index.js +217 -217
  211. package/src/providers/workbuddy/models.js +126 -126
  212. package/src/providers/workbuddy/payload.js +141 -141
  213. package/src/providers/workbuddy/rotation-log.js +83 -83
  214. package/src/providers/workbuddy/sanitize-tools.js +61 -61
  215. package/src/providers/workbuddy/sdk-chat.js +26 -26
  216. package/src/providers/workbuddy-balance.js +78 -78
  217. package/src/providers/zcode/account-store.js +129 -129
  218. package/src/providers/zcode/auth.js +28 -28
  219. package/src/providers/zcode/chat.js +179 -179
  220. package/src/providers/zcode/const.js +54 -54
  221. package/src/providers/zcode/context-shape.js +162 -162
  222. package/src/providers/zcode/headers.js +93 -93
  223. package/src/providers/zcode/index.js +124 -124
  224. package/src/providers/zcode/models.js +65 -65
  225. package/src/providers/zcode/oauth.js +120 -120
  226. package/src/providers/zcode/quota.js +176 -176
  227. package/src/providers/zcode/sse.js +196 -196
  228. package/src/readline-compat.js +25 -25
  229. package/src/responses/translate.js +311 -311
  230. package/src/routes/AGENTS.md +37 -0
  231. package/src/routes/chat/broadband-handler.js +72 -72
  232. package/src/routes/chat/empty-turn-report.js +31 -31
  233. package/src/routes/chat/exhausted-handler.js +87 -87
  234. package/src/routes/chat/hedge-handler.js +130 -130
  235. package/src/routes/chat/index.js +1 -1
  236. package/src/routes/chat/local-handler.js +63 -63
  237. package/src/routes/chat/peer-handler.js +81 -81
  238. package/src/routes/chat.js +3 -3
  239. package/src/routes/fallback.js +88 -88
  240. package/src/routes/groups-relay.js +214 -214
  241. package/src/routes/groups.js +122 -122
  242. package/src/routes/helpers.js +93 -83
  243. package/src/routes/index.js +147 -147
  244. package/src/routes/messages-route.js +375 -365
  245. package/src/routes/peers.js +10 -2
  246. package/src/routes/relay-queue.js +181 -181
  247. package/src/routes/relay.js +60 -60
  248. package/src/routes/responses-route.js +213 -213
  249. package/src/routes/stream-body.js +84 -84
  250. package/src/routes/stream-hold.js +337 -337
  251. package/src/routes/stream-scan.js +366 -366
  252. package/src/routes.js +3 -3
  253. package/src/runtime/auto-update.js +113 -113
  254. package/src/runtime/broadband-stream.js +79 -79
  255. package/src/runtime/broadband.js +103 -103
  256. package/src/runtime/group-sync.js +81 -81
  257. package/src/runtime/keep-warm.js +120 -120
  258. package/src/runtime/lifecycle-forensics.js +116 -116
  259. package/src/runtime/lifecycle-log.js +87 -87
  260. package/src/runtime/log-stamp.js +73 -73
  261. package/src/runtime/proc-info.js +137 -137
  262. package/src/runtime/qoder-checkin.js +93 -93
  263. package/src/runtime/raccoon-checkin.js +126 -126
  264. package/src/runtime/server-lifecycle.js +189 -189
  265. package/src/runtime/traework-checkin.js +118 -118
  266. package/src/runtime/workbuddy-checkin.js +120 -120
  267. package/src/runtime/workbuddy-growth.js +133 -133
  268. package/src/state/facade.js +58 -58
  269. package/src/state/memory.js +162 -162
  270. package/src/state/merge.js +25 -25
  271. package/src/state/migrations/cline-unify.js +94 -94
  272. package/src/state/migrations.js +33 -33
  273. package/src/state/persist.js +42 -42
  274. package/src/state/provider-config.js +142 -142
  275. package/src/state/schemas/group.js +31 -31
  276. package/src/state/schemas/model.js +94 -94
  277. package/src/state/schemas/peer.js +31 -31
  278. package/src/state/schemas/port.js +10 -10
  279. package/src/state/schemas/provider.js +202 -202
  280. package/src/state/schemas/timezone.js +70 -70
  281. package/src/state/schemas/token.js +48 -48
  282. package/src/state/schemas/use-group.js +102 -102
  283. package/src/state/store.js +176 -176
  284. package/src/state.js +1 -1
  285. package/src/sync-claude-desktop.js +234 -234
  286. package/src/sync-claude.js +152 -152
  287. package/src/sync-codex.js +116 -116
  288. package/src/sync-workbuddy.js +158 -158
  289. package/src/talk-full-redact.js +129 -129
  290. package/src/talk-full.js +131 -131
  291. package/src/talk-log.js +257 -257
  292. package/src/text-tokens.js +52 -52
  293. package/src/thinking-effort.js +67 -67
  294. package/src/time.js +106 -106
  295. package/src/timeline.js +26 -26
  296. package/src/transport/index.js +1 -1
  297. package/src/upstream-engine/index.js +74 -74
  298. package/src/upstream-engine/mode.js +15 -15
  299. package/src/upstream-engine/sdk/attempt.js +221 -221
  300. package/src/upstream-engine/sdk/chat.js +28 -28
  301. package/src/upstream-engine/sdk/convert.js +192 -192
  302. package/src/upstream-engine/sdk/diagnose.js +62 -62
  303. package/src/upstream-engine/sdk/dispatch.js +55 -55
  304. package/src/upstream-engine/sdk/responses.js +176 -176
  305. package/src/upstream-engine/sdk/sse.js +166 -166
  306. package/src/upstream-responses.js +305 -305
  307. package/src/usage/cache-fields.js +58 -0
  308. package/src/usage/record.js +109 -106
  309. package/src/usage/report.js +209 -195
  310. package/src/v6pool/claim.js +157 -157
  311. package/src/v6pool/config.js +65 -65
  312. package/src/v6pool/detect.js +130 -130
  313. package/src/v6pool/index.js +152 -152
  314. package/src/v6pool/prefix.js +140 -140
  315. package/src/v6pool/probe.js +40 -40
  316. package/src/v6pool/report.js +104 -104
  317. package/src/v6pool/rotate.js +263 -263
  318. package/src/v6pool/selftest.js +154 -0
  319. package/src/warp-pool/config.js +74 -74
  320. package/src/warp-pool/health.js +106 -106
  321. package/src/warp-pool/identity.js +141 -141
  322. package/src/warp-pool/index.js +358 -358
  323. package/src/warp-pool/keys.js +29 -29
  324. package/src/warp-pool/nodes.js +95 -95
  325. package/src/warp-pool/ports.js +160 -160
  326. package/src/warp-pool/register.js +113 -113
  327. package/src/warp-pool/report.js +232 -232
  328. package/src/warp-pool/revive.js +168 -168
  329. package/src/warp-pool/rotate.js +214 -214
  330. package/src/warp-pool/strategy.js +43 -43
  331. package/src/warp-pool/verdict.js +69 -69
  332. package/src/warp-pool/wireproxy.js +162 -162
@@ -1,135 +1,135 @@
1
- // raccoon 流处理:上游就是 OpenAI 形(`/api/web/llm/v2/chat/completions`),
2
- // 因此**不做协议转换**——本文件只做三件事:形状判定、错误信封分类、非流式聚合。
3
- // 流式路径直接把上游 body 透传(逐字节),避免任何整形引入的失真。
4
- import { createSseParser } from "../../transport/sse.js";
5
- import { isRaccoonAuthFailure, parseRaccoonEnvelope, raccoonEnvelopeText } from "./envelope.js";
6
-
7
- /**
8
- * 额度类措辞(业务码未取证,见 ADR-0050 决策 8):**宁可漏判也不误报**。
9
- * 刻意不收裸 `insufficient`/`balance`/`points` —— 那些词会命中「权限不足」这类非额度错,
10
- * 一旦误判成 quota 就会长冷 1h 并回 429 `quota_exhausted`,违反 ADR-0044 的「非额度原因不得谎报」。
11
- */
12
- const QUOTA_HINTS = /(积分|额度|quota|欠费)/i;
13
-
14
- export function isRaccoonSseContentType(value) {
15
- return String(value || "").includes("text/event-stream");
16
- }
17
-
18
- function isQuotaLike(envelope) {
19
- const text = `${envelope?.message ?? ""} ${envelope?.details ?? ""}`;
20
- if (QUOTA_HINTS.test(text)) return true;
21
- return envelope?.status === 402;
22
- }
23
-
24
- /**
25
- * 失败分类 → `{ kind, code, status, message }`。
26
- * kind 取值:auth(重登)/ quota(长冷却)/ rate_limit(短冷却)/ server(上游 5xx,短冷却)/ unknown(4xx 如实透出)。
27
- */
28
- export function classifyRaccoonFailure(status, payload) {
29
- const envelope = parseRaccoonEnvelope(payload, status);
30
- let kind = "unknown";
31
- if (isRaccoonAuthFailure(envelope, status)) kind = "auth";
32
- else if (isQuotaLike(envelope)) kind = "quota";
33
- else if (status === 429) kind = "rate_limit";
34
- else if (status >= 500) kind = "server";
35
- return { kind, code: envelope.code, status, message: raccoonEnvelopeText(envelope, `HTTP ${status}`) };
36
- }
37
-
38
- const ERROR_MAP = {
39
- auth: { status: 401, type: "auth_error", text: (f) => `raccoon: 登录态已失效${f.code ? `(code ${f.code})` : ""}。请运行 mslxdff -provider raccoon login 重新登录` },
40
- quota: { status: 429, type: "quota_exhausted", text: (f) => `raccoon: 积分不足${f.message ? `(${f.message})` : ""}。请运行 mslxdff -provider raccoon checkin 领取每日积分,或更换账号` },
41
- rate_limit: { status: 429, type: "rate_limited", text: () => "raccoon: 上游限流(账号冷却中),请稍后重试" },
42
- server: { status: 502, type: "upstream_error", text: (f) => `raccoon: 上游服务异常${f.code ? `(code ${f.code})` : ""}${f.message ? `:${f.message}` : ""}` },
43
- // unknown 只覆盖 4xx(5xx 已被上面分流成 server):按请求缺陷如实回 400,不谎报成网关故障
44
- unknown: { status: 400, type: "invalid_request", text: (f) => `raccoon: 上游拒绝请求(HTTP ${f.status || "?"}${f.code ? ` code ${f.code}` : ""})${f.message ? `:${f.message}` : ""}` },
45
- };
46
-
47
- /** 失败 → Response(带 `x-mslxdff-raccoon-kind`,供工厂决定冷却档位)。 */
48
- export function raccoonErrorResponse(fail = {}) {
49
- const kind = ERROR_MAP[fail.kind] ? fail.kind : "unknown";
50
- const entry = ERROR_MAP[kind];
51
- const headers = { "Content-Type": "application/json" };
52
- if (fail.kind) headers["x-mslxdff-raccoon-kind"] = fail.kind;
53
- return new Response(JSON.stringify({ error: { message: entry.text(fail), type: entry.type, code: fail.code || 0 } }), {
54
- status: entry.status,
55
- headers,
56
- });
57
- }
58
-
59
- /** 工具调用增量合并(对齐 `qwenwork/sse.js` 的 mergeToolCallDelta:id/type 取一次,name/arguments 追加)。 */
60
- function mergeRaccoonToolCallDelta(merged, delta) {
61
- for (const key of ["id", "type"]) {
62
- if (merged[key] === undefined && typeof delta[key] === "string" && delta[key] !== "") merged[key] = delta[key];
63
- }
64
- const dfn = delta.function;
65
- if (!dfn || typeof dfn !== "object") return;
66
- if (!merged.function || typeof merged.function !== "object") merged.function = {};
67
- if (typeof dfn.name === "string" && dfn.name !== "") merged.function.name = (merged.function.name || "") + dfn.name;
68
- if (typeof dfn.arguments === "string" && dfn.arguments !== "") {
69
- merged.function.arguments = (merged.function.arguments || "") + dfn.arguments;
70
- }
71
- }
72
-
73
- /** 把 SSE 文本聚合成一条 OpenAI chat completion(非流式请求、以及「header 说是 JSON 但体是 SSE」时走这里)。 */
74
- export async function aggregateRaccoonSse(text, { model } = {}) {
75
- const parser = createSseParser();
76
- // 聚合拿到的是**完整 body**(不是分片):补一个定界符,否则末尾缺 `\n\n` 的最后一帧会被当半包丢掉
77
- const events = parser.push(`${String(text || "")}\n\n`);
78
- const content = [];
79
- const reasoning = [];
80
- const toolCalls = new Map();
81
- const toolOrder = [];
82
- let finishReason = "stop";
83
- let usage;
84
- let id = "";
85
- let created = 0;
86
- for (const raw of events) {
87
- if (raw === "[DONE]" || !raw) continue;
88
- let payload;
89
- try {
90
- payload = JSON.parse(raw);
91
- } catch {
92
- continue;
93
- }
94
- // 流内错误信封:优先按失败分类返回,不要静默当成空回复
95
- if (payload?.code !== undefined && Number(payload.code) !== 0) {
96
- return { error: classifyRaccoonFailure(200, payload) };
97
- }
98
- if (payload?.error) {
99
- return { error: classifyRaccoonFailure(200, payload) };
100
- }
101
- if (!id && typeof payload?.id === "string") id = payload.id;
102
- if (!created && Number.isFinite(payload?.created)) created = Number(payload.created);
103
- if (payload?.usage) usage = payload.usage;
104
- const choice = Array.isArray(payload?.choices) ? payload.choices[0] : undefined;
105
- const delta = choice?.delta ?? choice?.message;
106
- if (typeof delta?.content === "string" && delta.content.length > 0) content.push(delta.content);
107
- if (typeof delta?.reasoning_content === "string" && delta.reasoning_content.length > 0) reasoning.push(delta.reasoning_content);
108
- for (const call of Array.isArray(delta?.tool_calls) ? delta.tool_calls : []) {
109
- if (!call || typeof call !== "object") continue;
110
- const idx = typeof call.index === "number" ? call.index : 0;
111
- if (!toolCalls.has(idx)) {
112
- toolCalls.set(idx, { index: idx });
113
- toolOrder.push(idx);
114
- }
115
- mergeRaccoonToolCallDelta(toolCalls.get(idx), call);
116
- }
117
- if (typeof choice?.finish_reason === "string" && choice.finish_reason.length > 0) finishReason = choice.finish_reason;
118
- }
119
- const message = { role: "assistant", content: content.join("") || null };
120
- if (reasoning.length > 0) message.reasoning_content = reasoning.join("");
121
- if (toolOrder.length > 0) {
122
- toolOrder.sort((a, b) => a - b);
123
- message.tool_calls = toolOrder.map((idx) => toolCalls.get(idx));
124
- }
125
- return {
126
- openAi: {
127
- id: id || `chatcmpl-raccoon-${Date.now()}`,
128
- object: "chat.completion",
129
- created: created || Math.floor(Date.now() / 1000),
130
- model: model || "",
131
- choices: [{ index: 0, message, finish_reason: finishReason }],
132
- ...(usage ? { usage } : {}),
133
- },
134
- };
135
- }
1
+ // raccoon 流处理:上游就是 OpenAI 形(`/api/web/llm/v2/chat/completions`),
2
+ // 因此**不做协议转换**——本文件只做三件事:形状判定、错误信封分类、非流式聚合。
3
+ // 流式路径直接把上游 body 透传(逐字节),避免任何整形引入的失真。
4
+ import { createSseParser } from "../../transport/sse.js";
5
+ import { isRaccoonAuthFailure, parseRaccoonEnvelope, raccoonEnvelopeText } from "./envelope.js";
6
+
7
+ /**
8
+ * 额度类措辞(业务码未取证,见 ADR-0050 决策 8):**宁可漏判也不误报**。
9
+ * 刻意不收裸 `insufficient`/`balance`/`points` —— 那些词会命中「权限不足」这类非额度错,
10
+ * 一旦误判成 quota 就会长冷 1h 并回 429 `quota_exhausted`,违反 ADR-0044 的「非额度原因不得谎报」。
11
+ */
12
+ const QUOTA_HINTS = /(积分|额度|quota|欠费)/i;
13
+
14
+ export function isRaccoonSseContentType(value) {
15
+ return String(value || "").includes("text/event-stream");
16
+ }
17
+
18
+ function isQuotaLike(envelope) {
19
+ const text = `${envelope?.message ?? ""} ${envelope?.details ?? ""}`;
20
+ if (QUOTA_HINTS.test(text)) return true;
21
+ return envelope?.status === 402;
22
+ }
23
+
24
+ /**
25
+ * 失败分类 → `{ kind, code, status, message }`。
26
+ * kind 取值:auth(重登)/ quota(长冷却)/ rate_limit(短冷却)/ server(上游 5xx,短冷却)/ unknown(4xx 如实透出)。
27
+ */
28
+ export function classifyRaccoonFailure(status, payload) {
29
+ const envelope = parseRaccoonEnvelope(payload, status);
30
+ let kind = "unknown";
31
+ if (isRaccoonAuthFailure(envelope, status)) kind = "auth";
32
+ else if (isQuotaLike(envelope)) kind = "quota";
33
+ else if (status === 429) kind = "rate_limit";
34
+ else if (status >= 500) kind = "server";
35
+ return { kind, code: envelope.code, status, message: raccoonEnvelopeText(envelope, `HTTP ${status}`) };
36
+ }
37
+
38
+ const ERROR_MAP = {
39
+ auth: { status: 401, type: "auth_error", text: (f) => `raccoon: 登录态已失效${f.code ? `(code ${f.code})` : ""}。请运行 mslxdff -provider raccoon login 重新登录` },
40
+ quota: { status: 429, type: "quota_exhausted", text: (f) => `raccoon: 积分不足${f.message ? `(${f.message})` : ""}。请运行 mslxdff -provider raccoon checkin 领取每日积分,或更换账号` },
41
+ rate_limit: { status: 429, type: "rate_limited", text: () => "raccoon: 上游限流(账号冷却中),请稍后重试" },
42
+ server: { status: 502, type: "upstream_error", text: (f) => `raccoon: 上游服务异常${f.code ? `(code ${f.code})` : ""}${f.message ? `:${f.message}` : ""}` },
43
+ // unknown 只覆盖 4xx(5xx 已被上面分流成 server):按请求缺陷如实回 400,不谎报成网关故障
44
+ unknown: { status: 400, type: "invalid_request", text: (f) => `raccoon: 上游拒绝请求(HTTP ${f.status || "?"}${f.code ? ` code ${f.code}` : ""})${f.message ? `:${f.message}` : ""}` },
45
+ };
46
+
47
+ /** 失败 → Response(带 `x-mslxdff-raccoon-kind`,供工厂决定冷却档位)。 */
48
+ export function raccoonErrorResponse(fail = {}) {
49
+ const kind = ERROR_MAP[fail.kind] ? fail.kind : "unknown";
50
+ const entry = ERROR_MAP[kind];
51
+ const headers = { "Content-Type": "application/json" };
52
+ if (fail.kind) headers["x-mslxdff-raccoon-kind"] = fail.kind;
53
+ return new Response(JSON.stringify({ error: { message: entry.text(fail), type: entry.type, code: fail.code || 0 } }), {
54
+ status: entry.status,
55
+ headers,
56
+ });
57
+ }
58
+
59
+ /** 工具调用增量合并(对齐 `qwenwork/sse.js` 的 mergeToolCallDelta:id/type 取一次,name/arguments 追加)。 */
60
+ function mergeRaccoonToolCallDelta(merged, delta) {
61
+ for (const key of ["id", "type"]) {
62
+ if (merged[key] === undefined && typeof delta[key] === "string" && delta[key] !== "") merged[key] = delta[key];
63
+ }
64
+ const dfn = delta.function;
65
+ if (!dfn || typeof dfn !== "object") return;
66
+ if (!merged.function || typeof merged.function !== "object") merged.function = {};
67
+ if (typeof dfn.name === "string" && dfn.name !== "") merged.function.name = (merged.function.name || "") + dfn.name;
68
+ if (typeof dfn.arguments === "string" && dfn.arguments !== "") {
69
+ merged.function.arguments = (merged.function.arguments || "") + dfn.arguments;
70
+ }
71
+ }
72
+
73
+ /** 把 SSE 文本聚合成一条 OpenAI chat completion(非流式请求、以及「header 说是 JSON 但体是 SSE」时走这里)。 */
74
+ export async function aggregateRaccoonSse(text, { model } = {}) {
75
+ const parser = createSseParser();
76
+ // 聚合拿到的是**完整 body**(不是分片):补一个定界符,否则末尾缺 `\n\n` 的最后一帧会被当半包丢掉
77
+ const events = parser.push(`${String(text || "")}\n\n`);
78
+ const content = [];
79
+ const reasoning = [];
80
+ const toolCalls = new Map();
81
+ const toolOrder = [];
82
+ let finishReason = "stop";
83
+ let usage;
84
+ let id = "";
85
+ let created = 0;
86
+ for (const raw of events) {
87
+ if (raw === "[DONE]" || !raw) continue;
88
+ let payload;
89
+ try {
90
+ payload = JSON.parse(raw);
91
+ } catch {
92
+ continue;
93
+ }
94
+ // 流内错误信封:优先按失败分类返回,不要静默当成空回复
95
+ if (payload?.code !== undefined && Number(payload.code) !== 0) {
96
+ return { error: classifyRaccoonFailure(200, payload) };
97
+ }
98
+ if (payload?.error) {
99
+ return { error: classifyRaccoonFailure(200, payload) };
100
+ }
101
+ if (!id && typeof payload?.id === "string") id = payload.id;
102
+ if (!created && Number.isFinite(payload?.created)) created = Number(payload.created);
103
+ if (payload?.usage) usage = payload.usage;
104
+ const choice = Array.isArray(payload?.choices) ? payload.choices[0] : undefined;
105
+ const delta = choice?.delta ?? choice?.message;
106
+ if (typeof delta?.content === "string" && delta.content.length > 0) content.push(delta.content);
107
+ if (typeof delta?.reasoning_content === "string" && delta.reasoning_content.length > 0) reasoning.push(delta.reasoning_content);
108
+ for (const call of Array.isArray(delta?.tool_calls) ? delta.tool_calls : []) {
109
+ if (!call || typeof call !== "object") continue;
110
+ const idx = typeof call.index === "number" ? call.index : 0;
111
+ if (!toolCalls.has(idx)) {
112
+ toolCalls.set(idx, { index: idx });
113
+ toolOrder.push(idx);
114
+ }
115
+ mergeRaccoonToolCallDelta(toolCalls.get(idx), call);
116
+ }
117
+ if (typeof choice?.finish_reason === "string" && choice.finish_reason.length > 0) finishReason = choice.finish_reason;
118
+ }
119
+ const message = { role: "assistant", content: content.join("") || null };
120
+ if (reasoning.length > 0) message.reasoning_content = reasoning.join("");
121
+ if (toolOrder.length > 0) {
122
+ toolOrder.sort((a, b) => a - b);
123
+ message.tool_calls = toolOrder.map((idx) => toolCalls.get(idx));
124
+ }
125
+ return {
126
+ openAi: {
127
+ id: id || `chatcmpl-raccoon-${Date.now()}`,
128
+ object: "chat.completion",
129
+ created: created || Math.floor(Date.now() / 1000),
130
+ model: model || "",
131
+ choices: [{ index: 0, message, finish_reason: finishReason }],
132
+ ...(usage ? { usage } : {}),
133
+ },
134
+ };
135
+ }
@@ -1,120 +1,120 @@
1
- // 通用(带 key)供应商的 responses 通道:muse-spark* 这类模型上游只挂 /responses,
2
- // 打同 host 的 /chat/completions 会 503 "Endpoint is unavailable"(ocgo 实测 2026-09-22)。
3
- // 判定单一来源 = isResponsesModel(models.dev 模型级 provider.npm + muse-spark 前缀兜底),
4
- // 与 GET /v1/models 的 capabilities.upstreamApi 同源,新模型无需改码。
5
- //
6
- // 契约:run() 的出参恒为 **chat 形状**(SSE 或 JSON),调用方无需再判分支。
7
- // · 流式优先 @ai-sdk/openai 的 responses 适配器(含加密思考往返,ADR-0017 复用);
8
- // · SDK 不可用 / 非流式 → 原生 fetch + chatToResponsesBody 正转换 + 反向整形。
9
- // key 轮换、退避重试、_t 计时与 createChatRunner(base.js)同语义,避免两条通道行为漂移。
10
- import { chatToResponsesBody, toChatResponse, reshapeResponsesSse } from "../upstream-responses.js";
11
- import { attemptOnceResponsesSdk } from "../upstream-engine/sdk/responses.js";
12
- import { ENGINE_MARKER } from "../upstream-engine/sdk/chat.js";
13
- import { sleep } from "./base.js";
14
-
15
- /** 供应商 id → env 片段(`my-api` → `MY_API`),与 providerKeyEnv 同规则 */
16
- export function envSlug(id) {
17
- return String(id || "").toUpperCase().replace(/[^A-Z0-9]/g, "_");
18
- }
19
-
20
- /**
21
- * responses 端点路径:显式入参 > env `MSLXDFF_<ID>_RESPONSES_PATH` > 缺省 `/responses`。
22
- * 归一为以 `/` 开头(joinUrl 会再拼 baseUrl)。
23
- */
24
- export function resolveResponsesPath(id, responsesPath) {
25
- const raw = responsesPath || process.env[`MSLXDFF_${envSlug(id)}_RESPONSES_PATH`] || "";
26
- const s = String(raw).trim();
27
- if (!s) return "/responses";
28
- return s.startsWith("/") ? s : `/${s}`;
29
- }
30
-
31
- /**
32
- * 建 responses 通道。
33
- * @param {object} o
34
- * @param {string} o.id 供应商 id(用于报错文案与 providerName)
35
- * @param {string} o.url 已拼好的 responses 绝对 URL
36
- * @param {number} o.connectTimeoutMs 单次尝试超时
37
- * @param {object} o.retry 重试表(与 createChatRunner 同形状:network/429/50x → {attempts, delayMs})
38
- * @param {number} o.cooldownMs 全 key 冷却时的人话报错文案用
39
- * @param {boolean} o.sdkEnabled 是否允许走 AI SDK responses 适配器(流式路径)
40
- * @param {Function} o.buildHeaders (body, key, opts) → headers
41
- * @param {Function} o.fetchImpl 注入的 fetch(连接池/测试接缝)
42
- * @param {object} [o.dispatcher] undici Agent
43
- */
44
- export function createResponsesChannel({
45
- id,
46
- url,
47
- connectTimeoutMs = 30_000,
48
- retry = {},
49
- cooldownMs = 30_000,
50
- sdkEnabled = true,
51
- buildHeaders,
52
- fetchImpl,
53
- dispatcher = null,
54
- } = {}) {
55
- async function attemptOnce(body, key, opts) {
56
- const headers = buildHeaders ? buildHeaders(body, key, opts) : {};
57
- const wantsStream = body?.stream !== false;
58
- if (sdkEnabled && wantsStream) {
59
- try {
60
- const r = await attemptOnceResponsesSdk({
61
- url, body, headers, providerName: id, marker: ENGINE_MARKER, fetchImpl,
62
- });
63
- if (r) return r; // 适配器已产出 chat SSE
64
- } catch (e) {
65
- if (!e || (!e._sdkLoadFailed && !e._sdkUnsupported)) throw e;
66
- // SDK 装载失败 / URL 异形 → 落原生兜底(保底不变差)
67
- }
68
- }
69
- const controller = new AbortController();
70
- const timer = setTimeout(() => controller.abort(new Error(`${id} timed out after ${connectTimeoutMs}ms`)), connectTimeoutMs);
71
- try {
72
- const fetchOpts = { method: "POST", headers, body: JSON.stringify(chatToResponsesBody(body)), signal: controller.signal };
73
- if (dispatcher) fetchOpts.dispatcher = dispatcher;
74
- const res = await fetchImpl(url, fetchOpts);
75
- if (!res.ok) return res;
76
- if (wantsStream) return reshapeResponsesSse(res, body?.model);
77
- const json = await res.json().catch(() => null);
78
- return json ? toChatResponse(res, json) : res;
79
- } finally {
80
- clearTimeout(timer);
81
- }
82
- }
83
-
84
- /** key 轮换 + 退避重试 + _t 计时;语义对齐 base.js 的 createChatRunner.runChat */
85
- async function run(body, activeRing, sourceKey, opts) {
86
- const t0 = performance.now();
87
- const attempts = [];
88
- let waitMs = 0;
89
- const key = activeRing.next();
90
- if (!key && activeRing.size > 0) {
91
- const err = new Error(`${id}: all API keys are in cooldown (last error < ${cooldownMs}ms ago) — provider temporarily unavailable`);
92
- err._t = { attempts: [], waitMs: 0, totalMs: Math.round(performance.now() - t0), cooldownMs };
93
- throw err;
94
- }
95
- for (let attempt = 0; ; attempt++) {
96
- const t = performance.now();
97
- let result;
98
- try {
99
- result = await attemptOnce(body, key, opts);
100
- } catch (err) {
101
- result = err;
102
- }
103
- attempts.push({ attempt, type: result instanceof Error ? "network" : `http${result?.status}`, ms: Math.round(performance.now() - t) });
104
- if (result instanceof Error) {
105
- const entry = retry?.network;
106
- if (entry && attempt < entry.attempts) { await sleep(entry.delayMs); waitMs += entry.delayMs; continue; }
107
- activeRing.onError(key);
108
- result._t = { attempts, waitMs, totalMs: Math.round(performance.now() - t0) };
109
- throw result;
110
- }
111
- const entry = retry?.[result.status];
112
- if (entry && attempt < entry.attempts) { await sleep(entry.delayMs); waitMs += entry.delayMs; continue; }
113
- if (result.status === 401 || result.status === 403 || result.status === 429 || result.status >= 500) activeRing.onError(key);
114
- try { result._t = { attempts, waitMs, totalMs: Math.round(performance.now() - t0) }; } catch {}
115
- return result;
116
- }
117
- }
118
-
119
- return { url, attemptOnce, run };
120
- }
1
+ // 通用(带 key)供应商的 responses 通道:muse-spark* 这类模型上游只挂 /responses,
2
+ // 打同 host 的 /chat/completions 会 503 "Endpoint is unavailable"(ocgo 实测 2026-09-22)。
3
+ // 判定单一来源 = isResponsesModel(models.dev 模型级 provider.npm + muse-spark 前缀兜底),
4
+ // 与 GET /v1/models 的 capabilities.upstreamApi 同源,新模型无需改码。
5
+ //
6
+ // 契约:run() 的出参恒为 **chat 形状**(SSE 或 JSON),调用方无需再判分支。
7
+ // · 流式优先 @ai-sdk/openai 的 responses 适配器(含加密思考往返,ADR-0017 复用);
8
+ // · SDK 不可用 / 非流式 → 原生 fetch + chatToResponsesBody 正转换 + 反向整形。
9
+ // key 轮换、退避重试、_t 计时与 createChatRunner(base.js)同语义,避免两条通道行为漂移。
10
+ import { chatToResponsesBody, toChatResponse, reshapeResponsesSse } from "../upstream-responses.js";
11
+ import { attemptOnceResponsesSdk } from "../upstream-engine/sdk/responses.js";
12
+ import { ENGINE_MARKER } from "../upstream-engine/sdk/chat.js";
13
+ import { sleep } from "./base.js";
14
+
15
+ /** 供应商 id → env 片段(`my-api` → `MY_API`),与 providerKeyEnv 同规则 */
16
+ export function envSlug(id) {
17
+ return String(id || "").toUpperCase().replace(/[^A-Z0-9]/g, "_");
18
+ }
19
+
20
+ /**
21
+ * responses 端点路径:显式入参 > env `MSLXDFF_<ID>_RESPONSES_PATH` > 缺省 `/responses`。
22
+ * 归一为以 `/` 开头(joinUrl 会再拼 baseUrl)。
23
+ */
24
+ export function resolveResponsesPath(id, responsesPath) {
25
+ const raw = responsesPath || process.env[`MSLXDFF_${envSlug(id)}_RESPONSES_PATH`] || "";
26
+ const s = String(raw).trim();
27
+ if (!s) return "/responses";
28
+ return s.startsWith("/") ? s : `/${s}`;
29
+ }
30
+
31
+ /**
32
+ * 建 responses 通道。
33
+ * @param {object} o
34
+ * @param {string} o.id 供应商 id(用于报错文案与 providerName)
35
+ * @param {string} o.url 已拼好的 responses 绝对 URL
36
+ * @param {number} o.connectTimeoutMs 单次尝试超时
37
+ * @param {object} o.retry 重试表(与 createChatRunner 同形状:network/429/50x → {attempts, delayMs})
38
+ * @param {number} o.cooldownMs 全 key 冷却时的人话报错文案用
39
+ * @param {boolean} o.sdkEnabled 是否允许走 AI SDK responses 适配器(流式路径)
40
+ * @param {Function} o.buildHeaders (body, key, opts) → headers
41
+ * @param {Function} o.fetchImpl 注入的 fetch(连接池/测试接缝)
42
+ * @param {object} [o.dispatcher] undici Agent
43
+ */
44
+ export function createResponsesChannel({
45
+ id,
46
+ url,
47
+ connectTimeoutMs = 30_000,
48
+ retry = {},
49
+ cooldownMs = 30_000,
50
+ sdkEnabled = true,
51
+ buildHeaders,
52
+ fetchImpl,
53
+ dispatcher = null,
54
+ } = {}) {
55
+ async function attemptOnce(body, key, opts) {
56
+ const headers = buildHeaders ? buildHeaders(body, key, opts) : {};
57
+ const wantsStream = body?.stream !== false;
58
+ if (sdkEnabled && wantsStream) {
59
+ try {
60
+ const r = await attemptOnceResponsesSdk({
61
+ url, body, headers, providerName: id, marker: ENGINE_MARKER, fetchImpl,
62
+ });
63
+ if (r) return r; // 适配器已产出 chat SSE
64
+ } catch (e) {
65
+ if (!e || (!e._sdkLoadFailed && !e._sdkUnsupported)) throw e;
66
+ // SDK 装载失败 / URL 异形 → 落原生兜底(保底不变差)
67
+ }
68
+ }
69
+ const controller = new AbortController();
70
+ const timer = setTimeout(() => controller.abort(new Error(`${id} timed out after ${connectTimeoutMs}ms`)), connectTimeoutMs);
71
+ try {
72
+ const fetchOpts = { method: "POST", headers, body: JSON.stringify(chatToResponsesBody(body)), signal: controller.signal };
73
+ if (dispatcher) fetchOpts.dispatcher = dispatcher;
74
+ const res = await fetchImpl(url, fetchOpts);
75
+ if (!res.ok) return res;
76
+ if (wantsStream) return reshapeResponsesSse(res, body?.model);
77
+ const json = await res.json().catch(() => null);
78
+ return json ? toChatResponse(res, json) : res;
79
+ } finally {
80
+ clearTimeout(timer);
81
+ }
82
+ }
83
+
84
+ /** key 轮换 + 退避重试 + _t 计时;语义对齐 base.js 的 createChatRunner.runChat */
85
+ async function run(body, activeRing, sourceKey, opts) {
86
+ const t0 = performance.now();
87
+ const attempts = [];
88
+ let waitMs = 0;
89
+ const key = activeRing.next();
90
+ if (!key && activeRing.size > 0) {
91
+ const err = new Error(`${id}: all API keys are in cooldown (last error < ${cooldownMs}ms ago) — provider temporarily unavailable`);
92
+ err._t = { attempts: [], waitMs: 0, totalMs: Math.round(performance.now() - t0), cooldownMs };
93
+ throw err;
94
+ }
95
+ for (let attempt = 0; ; attempt++) {
96
+ const t = performance.now();
97
+ let result;
98
+ try {
99
+ result = await attemptOnce(body, key, opts);
100
+ } catch (err) {
101
+ result = err;
102
+ }
103
+ attempts.push({ attempt, type: result instanceof Error ? "network" : `http${result?.status}`, ms: Math.round(performance.now() - t) });
104
+ if (result instanceof Error) {
105
+ const entry = retry?.network;
106
+ if (entry && attempt < entry.attempts) { await sleep(entry.delayMs); waitMs += entry.delayMs; continue; }
107
+ activeRing.onError(key);
108
+ result._t = { attempts, waitMs, totalMs: Math.round(performance.now() - t0) };
109
+ throw result;
110
+ }
111
+ const entry = retry?.[result.status];
112
+ if (entry && attempt < entry.attempts) { await sleep(entry.delayMs); waitMs += entry.delayMs; continue; }
113
+ if (result.status === 401 || result.status === 403 || result.status === 429 || result.status >= 500) activeRing.onError(key);
114
+ try { result._t = { attempts, waitMs, totalMs: Math.round(performance.now() - t0) }; } catch {}
115
+ return result;
116
+ }
117
+ }
118
+
119
+ return { url, attemptOnce, run };
120
+ }
@@ -1,54 +1,54 @@
1
- // ADR-0019:借道 = 用你的 key —— 转发时自动借出,无开关、无白名单(组内互信是前提)。
2
- // header: `x-mslxdff-share-keys: <providerId>=<key1>,<key2>;<providerId2>=<key3>`
3
- import { splitModelId, DEFAULT_PROVIDER } from "./model-id.js";
4
- import { classifyProvider } from "./classify.js";
5
- import { loadProviderKeys, listProviderIdsWithKeys } from "../state.js";
6
- import { isRefreshToken } from "./cline/headers.js";
7
-
8
- export const SHARE_KEYS_HEADER = "x-mslxdff-share-keys";
9
-
10
- // 硬排除:refresh-token 型凭据(对端刷新会轮换 token,与本机互相踢下线)——正确性问题。
11
- // 二层保护:① 按 id 硬排除(cline 恒 local-only);② 按凭据形状兜底(见 shareableProviderIds)。
12
- // traework/workbuddy/zcode 同为本机账号绑定型(auths/*.json + uid;zcode 还带 per-account deviceMid),恒不借出。
13
- const NEVER_SHARE_IDS = new Set(["cline", "codearts", "traework", "qoder", "zcode", "raccoon"]);
14
- // 本节点应 cast key 到出站转发的供应商 id 集合 = 所有「本机有 key」的供应商,
15
- // 减去:opencode(无 key 且恒排除)、local-only(workbuddy/cline系,本就不走组员)、刷新型凭据。
16
- export function shareableProviderIds({ file } = {}) {
17
- const ids = [];
18
- for (const id of listProviderIdsWithKeys({ file })) {
19
- if (id === DEFAULT_PROVIDER) continue;
20
- if (NEVER_SHARE_IDS.has(id)) continue;
21
- try { if (classifyProvider(id) === "local-only") continue; } catch {}
22
- // 形状兜底:id 白名单可能有残留(迁移未跑/回滚/手改 config),只要该供应商现有 key 里
23
- // 有一个是 refreshToken 形态,就一律不外借——refreshToken 借出去会被对端刷新轮换,双向踢下线。
24
- try { if (loadProviderKeys(id, { file }).some((k) => isRefreshToken(k, id))) continue; } catch {}
25
- ids.push(id);
26
- }
27
- return ids;
28
- }
29
-
30
- // 组装 share header 值(只含 shareable 且当前请求命中前缀的供应商)。
31
- // splitModelId 用 shareableProviderIds() 作为已知集合:命中 → 该供应商在此节点 shareable;
32
- // 未命中或裸 id(默认供应商 opencode)→ 不附带。返回 null 表示本请求不需要附带。
33
- export function buildShareKeysHeader(model, { file } = {}) {
34
- const split = splitModelId(model, shareableProviderIds({ file }));
35
- if (!split.provider || split.provider === DEFAULT_PROVIDER) return null;
36
- const keys = loadProviderKeys(split.provider, { file });
37
- if (!keys.length) return null;
38
- return `${split.provider}=${keys.join(",")}`;
39
- }
40
-
41
- // 组员侧:解析 share header 为 { providerId -> keys[] }
42
- // 防御:默认供应商(opencode)即使出现在 header 一律忽略——它不该有共享 key。
43
- export function parseShareKeysHeader(value) {
44
- const out = {};
45
- if (!value) return out;
46
- for (const seg of String(value).split(";")) {
47
- const [provider, keysPart] = seg.split("=");
48
- const id = String(provider || "").trim();
49
- if (!id || id === DEFAULT_PROVIDER || !keysPart) continue;
50
- const keys = String(keysPart).split(",").map((k) => k.trim()).filter(Boolean);
51
- if (keys.length) out[id] = keys;
52
- }
53
- return out;
1
+ // ADR-0019:借道 = 用你的 key —— 转发时自动借出,无开关、无白名单(组内互信是前提)。
2
+ // header: `x-mslxdff-share-keys: <providerId>=<key1>,<key2>;<providerId2>=<key3>`
3
+ import { splitModelId, DEFAULT_PROVIDER } from "./model-id.js";
4
+ import { classifyProvider } from "./classify.js";
5
+ import { loadProviderKeys, listProviderIdsWithKeys } from "../state.js";
6
+ import { isRefreshToken } from "./cline/headers.js";
7
+
8
+ export const SHARE_KEYS_HEADER = "x-mslxdff-share-keys";
9
+
10
+ // 硬排除:refresh-token 型凭据(对端刷新会轮换 token,与本机互相踢下线)——正确性问题。
11
+ // 二层保护:① 按 id 硬排除(cline 恒 local-only);② 按凭据形状兜底(见 shareableProviderIds)。
12
+ // traework/workbuddy/zcode 同为本机账号绑定型(auths/*.json + uid;zcode 还带 per-account deviceMid),恒不借出。
13
+ const NEVER_SHARE_IDS = new Set(["cline", "codearts", "traework", "qoder", "zcode", "raccoon"]);
14
+ // 本节点应 cast key 到出站转发的供应商 id 集合 = 所有「本机有 key」的供应商,
15
+ // 减去:opencode(无 key 且恒排除)、local-only(workbuddy/cline系,本就不走组员)、刷新型凭据。
16
+ export function shareableProviderIds({ file } = {}) {
17
+ const ids = [];
18
+ for (const id of listProviderIdsWithKeys({ file })) {
19
+ if (id === DEFAULT_PROVIDER) continue;
20
+ if (NEVER_SHARE_IDS.has(id)) continue;
21
+ try { if (classifyProvider(id) === "local-only") continue; } catch {}
22
+ // 形状兜底:id 白名单可能有残留(迁移未跑/回滚/手改 config),只要该供应商现有 key 里
23
+ // 有一个是 refreshToken 形态,就一律不外借——refreshToken 借出去会被对端刷新轮换,双向踢下线。
24
+ try { if (loadProviderKeys(id, { file }).some((k) => isRefreshToken(k, id))) continue; } catch {}
25
+ ids.push(id);
26
+ }
27
+ return ids;
28
+ }
29
+
30
+ // 组装 share header 值(只含 shareable 且当前请求命中前缀的供应商)。
31
+ // splitModelId 用 shareableProviderIds() 作为已知集合:命中 → 该供应商在此节点 shareable;
32
+ // 未命中或裸 id(默认供应商 opencode)→ 不附带。返回 null 表示本请求不需要附带。
33
+ export function buildShareKeysHeader(model, { file } = {}) {
34
+ const split = splitModelId(model, shareableProviderIds({ file }));
35
+ if (!split.provider || split.provider === DEFAULT_PROVIDER) return null;
36
+ const keys = loadProviderKeys(split.provider, { file });
37
+ if (!keys.length) return null;
38
+ return `${split.provider}=${keys.join(",")}`;
39
+ }
40
+
41
+ // 组员侧:解析 share header 为 { providerId -> keys[] }
42
+ // 防御:默认供应商(opencode)即使出现在 header 一律忽略——它不该有共享 key。
43
+ export function parseShareKeysHeader(value) {
44
+ const out = {};
45
+ if (!value) return out;
46
+ for (const seg of String(value).split(";")) {
47
+ const [provider, keysPart] = seg.split("=");
48
+ const id = String(provider || "").trim();
49
+ if (!id || id === DEFAULT_PROVIDER || !keysPart) continue;
50
+ const keys = String(keysPart).split(",").map((k) => k.trim()).filter(Boolean);
51
+ if (keys.length) out[id] = keys;
52
+ }
53
+ return out;
54
54
  }