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,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
  }