magicrew-auth 0.3.23 → 0.3.25

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.
package/AGENTS.md CHANGED
@@ -67,6 +67,7 @@ curl -s "https://usemagpie.ai/api/latest?channel=stable" | grep -o '"version":"[
67
67
  | 14 | **卡片上额度正常,路由页里这个号却是「剩余额度未知」** | 窗口**不能标 `aside`**:magpie 的 `allowanceOf()` 把 `Aside`/`Text` 的窗口整条跳过,一条不剩 ⇒ 该号 `Known=false`(官网文档 issue #3650 明写)。判据:`curl -s http://127.0.0.1:3425/v1/magpie/trace`,看这个 provider 的 `known` 是 `true` 还是**字段缺失** | HOOKS 三 · `src/hooks/usage.mjs` 头 |
68
68
  | 15 | 接 ZCode / Claude Code 系客户端,带工具的请求回 `400 {"error":{"message":"模型响应失败","code":4017}}` | **上游的 `additionalProperties` 只认布尔值,任何对象形态都拒** —— 不只是 `{}`,`{type:"string"}` 这种有约束的 schema 一样 400(两个模型逐行一致)。判据:同一请求只把该字段换成 `true` → 200。改写已在 `rewriteRequestBody` 里做(非布尔一律换 true;**这是降级放宽、会丢约束**,取舍见 internals 十七) | internals 十七 · `tests/body-rewrite.mjs` |
69
69
  | 16 | 网关路径(agent 从 magpie 的 `/v1` 进来)报 `502 did not stream: token无效`,而 provider 显示已登录 | auth 里没带 org → `accountOf()` 落 `"default"` → 读不到真实账号的 key 槽 → 只能用 `auth.access`,它过了 2 小时就 403。`accountFor()` 已在**恰好一个真实账号**时兜到它;**多账号不猜**(猜错就是拿别人的 key 扣别人的积分) | internals 十四 · 十五 · `tests/account-fallback.mjs` |
70
+ | 17 | 单轮回 `400 ... tools.N.function.parameters/properties/functions/items: invalid JSON Schema` | 上游不接受**空 schema 对象**当 `items`:`items:{}` 必 400,`items:{description:"x"}` 却 200。判据:同一请求只删 `items:{}` → 200。改写已做(删 `items:{}` / `true`,**严格等价**;`false` 不动) | internals 十七 · `tests/body-rewrite.mjs` |
70
71
 
71
72
  **坑 14 的两个附带判据**(都实测过):
72
73
 
package/CHANGELOG.md CHANGED
@@ -1,5 +1,77 @@
1
1
  # 更新记录
2
2
 
3
+ ## 0.3.25 — 2026-10-28
4
+
5
+ **0.3.24 的 schema 扫描范围收窄:只走 schema 位置**
6
+
7
+ 0.3.24 的 `loosenEmptyItems()` 与 `loosenAdditionalProperties()` 递归了**所有**对象。
8
+ 那会把 `properties` 里恰好叫 `items` / `additionalProperties` 的工具参数当成 schema
9
+ 关键字:`properties.items = {}` 的参数会被删掉,`properties.additionalProperties =
10
+ {type:"string"}` 会被改成 `true`;`enum` / `default` 值里的同名键也会被误改。
11
+
12
+ 两者现在共用 `walkSchema()`,只进入真正的 schema 位置:
13
+
14
+ - `properties` / `patternProperties` / `$defs` / `definitions` / `dependentSchemas` 的**值**
15
+ - `allOf` / `anyOf` / `oneOf` / `prefixItems`
16
+ - `items` / `additionalProperties` / `unevaluatedProperties` / `propertyNames` /
17
+ `contains` / `not` / `if` / `then` / `else` / `contentSchema` / `dependencies`
18
+ - 不扫键名,不扫 `enum` / `default` / `const` / `examples`(它们不是 schema)
19
+
20
+ 0.3.24 的 `items:{}` 等价替换规则不变,本版只修扫描范围。
21
+
22
+ ### 验证
23
+
24
+ - `tests/body-rewrite.mjs`:36 个断言(新增 5 个同名属性 / `enum` 值反例)。
25
+ - 端到端复跑:41 个工具、`properties.functions.items:{}` → 200(国内 / 国际)。
26
+
27
+ ## 0.3.24 — 2026-10-28
28
+
29
+ **修 `items: {}` 导致的整轮 400**
30
+
31
+ ### 症状
32
+
33
+ 用户的一次普通对话直接失败,界面只显示:
34
+
35
+ ```
36
+ 本轮运行失败 400: {"code":null,"message":"超级麦吉: HTTP 400 请求错误:
37
+ {\"error\":{\"code\":\"InvalidParameter\",\"message\":\"tools.40.function.parameters/
38
+ properties/functions/items: invalid JSON Schema; check the schema keywords
39
+ and references Request id: ...\",\"param\":\"\",\"type\":\"BadRequest\"}}",
40
+ "param":null,"type":"invalid_request_error"}
41
+ ```
42
+
43
+ ### 根因
44
+
45
+ 某个工具的参数 schema 里,数组的 `items` 是**空对象 `{}`**。上游的 JSON Schema
46
+ 校验器只拒这一种空 schema 形态;`items:{description:"x"}`、`items:{type:"string"}`、
47
+ 缺省 `items` 都是 200。国内站 `magicrew` 与国际站 `magicrew-intl` 的报错逐行一致。
48
+
49
+ 按 JSON Schema 规范 `items:{}` 是合法的("任意元素"),客户端没写错;这是上游
50
+ 兼容性不足。但插件是唯一能改请求体的地方。
51
+
52
+ ### 修法
53
+
54
+ `src/hooks/loader.mjs` 新增 `loosenEmptyItems()`,在 `rewriteRequestBody` 里递归扫
55
+ `tools[].function.parameters`:
56
+
57
+ ```
58
+ items: {} → 删除 // JSON Schema 缺省值就是 {},严格等价
59
+ items: true → 删除 // 同上;上游对它回的是另一种 400
60
+ items: false → 不动 // 「数组必须为空」,删掉就是放开
61
+ items: {...} → 不动 // 有任何一个键,上游都收
62
+ ```
63
+
64
+ 与 `additionalProperties`、`max_tokens`、`stream_options` 共用一次 `JSON.parse`;
65
+ 没改到时仍返回原始字符串。
66
+
67
+ ### 验证
68
+
69
+ - `tests/body-rewrite.mjs`:31 个断言(新增 11 个),`items:{}` / `true` 删除、
70
+ `{description}` / `{type}` / `false` 不动、`tools[40]` 命中、combined 改写、不误伤。
71
+ - 端到端(2026-10-28):41 个工具、第 41 个带 `properties.functions.items:{}`,
72
+ 原样请求 → `400 tools.40.../items`;过 `rewriteRequestBody` → `200`。
73
+ 国内站、国际站都跑过。
74
+
3
75
  ## 0.3.23 — 2026-10-28
4
76
 
5
77
  **修工具 schema 兼容 + 网关路径拿错 key,顺手把「签一把顶一把」测清楚了**
package/docs/internals.md CHANGED
@@ -1055,7 +1055,12 @@ magicrew 既不返回 `maxInputTokens`(`flows/models` 的 `configs` 只有 tem
1055
1055
 
1056
1056
  ---
1057
1057
 
1058
- ## 十七、上游不认 `additionalProperties: {}` —— 工具 schema 要在请求里兜住
1058
+ ## 十七、上游不认空 schema —— `additionalProperties: {}` 与 `items: {}`
1059
+
1060
+ > 两处是同一类兼容性问题的两个面:客户端按 JSON Schema 规范写出**空对象**,
1061
+ > 上游的校验器/适配层拒收。都只能在请求改写里兜。
1062
+
1063
+ ### 一、`additionalProperties: {}` → `400 code 4017`
1059
1064
 
1060
1065
  **症状**:接 ZCode / Claude Code 系客户端时,一次带 54 个工具定义的请求必定失败:
1061
1066
 
@@ -1111,19 +1116,66 @@ POST /v1/chat/completions
1111
1116
  保留原值就是整个请求失败。两害相权,让工具调用能跑(可能多出未声明的字段)
1112
1117
  比让 agent 完全不可用要好;方向也安全 —— 是放宽不是收紧,不会让原本合法的调用被拒。
1113
1118
 
1114
- 它与 `max_tokens`、`stream_options` 三处改写共用**一次** `JSON.parse`;
1119
+ 它与 `max_tokens`、`stream_options`、`items: {}` 四处改写共用**一次** `JSON.parse`;
1115
1120
  没改到时返回原始字符串(省一次序列化 —— 54 工具的请求体可达上百 KB)。
1116
1121
 
1117
- 边界由 `tests/body-rewrite.mjs` 钉住(20 个断言):非布尔一律换、布尔一律不动、
1122
+ 边界由 `tests/body-rewrite.mjs` 钉住(36 个断言):非布尔一律换、布尔一律不动、
1118
1123
  其余字段一字不动、没改到就不重新序列化。
1119
1124
 
1125
+ ### 二、`items: {}` → `400 invalid JSON Schema`
1126
+
1127
+ **症状**(2026-10-28,用户截图原文):
1128
+
1129
+ ```
1130
+ HTTP 400
1131
+ {"code":null,"message":"超级麦吉: HTTP 400 请求错误: {\"error\":{\"code\":\"InvalidParameter\",
1132
+ \"message\":\"tools.40.function.parameters/properties/functions/items: invalid JSON Schema;
1133
+ check the schema keywords and references Request id: ...\",\"param\":\"\",\"type\":\"BadRequest\"}}",
1134
+ "param":null,"type":"invalid_request_error"}
1135
+ ```
1136
+
1137
+ 与 `additionalProperties` 那条不同:这次上游**给了完整路径**(`tools.40.../items`),
1138
+ 而且国内站与 `magicrew-intl` 的报错逐行一致。
1139
+
1140
+ **最小复现**:41 个工具,第 41 个的参数是
1141
+
1142
+ ```json
1143
+ {"type":"object","properties":{"functions":{"type":"array","items":{}}},"required":["functions"]}
1144
+ ```
1145
+
1146
+ | `items` | 结果 |
1147
+ |---|---|
1148
+ | 缺省 | 200 |
1149
+ | `{}` | **400 invalid JSON Schema**(路径 `.../properties/functions/items`) |
1150
+ | `{description:"x"}` | 200 —— 空对象才炸,不是"缺 type" |
1151
+ | `{type:"string"}` | 200 |
1152
+ | `true` | 400 `模型响应失败`(另一条 400) |
1153
+ | `false` | 400 `模型响应失败` |
1154
+
1155
+ **根因**:客户端用 `z.array(z.unknown())` / 动态函数列表生成 schema 时,数组元素序列化成
1156
+ `{}`。JSON Schema 里 `items` 的缺省值本来就是 `{}` —— "任意元素",所以
1157
+ `items: {}` 删掉是**严格等价**,不是降级放宽;`items: true` 同理。
1158
+ `items: false`("数组必须为空")语义不同,不能删,原样留着。
1159
+
1160
+ **修法**:`loosenEmptyItems()` 删 `items: {}` 与 `items: true`,与
1161
+ `loosenAdditionalProperties()` 在同一个 `rewriteRequestBody` 里各跑一遍。
1162
+
1163
+ 两者共用 `walkSchema()`,**只进入真正的 schema 位置**(`properties` 等映射的**值**、
1164
+ `allOf` / `anyOf` / `oneOf`、`items` / `additionalProperties` / `not` / `if` 等),
1165
+ 不扫键名,也不扫 `enum` / `default` / `const`。否则一个恰好叫 `items` 或
1166
+ `additionalProperties` 的工具参数会被当成关键字误删/误改(0.3.24 的教训,0.3.25 修)。
1167
+
1168
+ **端到端验证**(2026-10-28):同一份 41 工具请求
1169
+ `raw → 400 tools.40.../items`;过 `rewriteRequestBody` 后 `→ 200`。
1170
+ 国内站、国际站都验过。
1171
+
1120
1172
  ### 为什么算插件的事,不算客户端的事
1121
1173
 
1122
1174
  按规范 `{}` 与 `true` 等价,客户端没写错;**上游的兼容性不足**。
1123
1175
  但插件是唯一能改请求体的地方(见 `src/hooks/loader.mjs` 顶部),
1124
1176
  在这里兜住比让每个用户去改 ZCode 的源码现实。
1125
1177
 
1126
- ### 同一处的第三个改写,以及它的教训
1178
+ ### 同一处这些改写的两个教训
1127
1179
 
1128
1180
  同一次排查里还确认了两件事,都值得记住:
1129
1181
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "magicrew-auth",
3
- "version": "0.3.23",
3
+ "version": "0.3.25",
4
4
  "description": "超级麦吉 / MagiCrew for magpie and OpenCode — one package, two providers: letsmagic.cn (国内) and magicrew.ai (international). Password or web-JWT sign-in, the account's own live model list, points balance, newcomer rewards, invite codes.",
5
5
  "type": "module",
6
6
  "main": "./index.mjs",
@@ -42,34 +42,130 @@ import { ensureKey, accountOf } from "../keys.mjs"
42
42
  * 一次真实请求里 54 个工具中的 `ExitPlanMode` 与 `CreateWorkflow` 就这么写。
43
43
  * 客户端没写错(按规范 `{}` 与 `true` 等价),是上游兼容性不足,插件这边兜住。
44
44
  *
45
+ * 只走 schema 位置(见 `walkSchema`):一个恰好叫 `additionalProperties` 的工具参数
46
+ * 不会被误改。
47
+ *
45
48
  * @returns {boolean} 有没有改到
46
49
  */
47
- export function loosenAdditionalProperties(node) {
48
- if (!node || typeof node !== "object") return false
49
- if (Array.isArray(node)) {
50
- let any = false
51
- for (const n of node) if (loosenAdditionalProperties(n)) any = true
52
- return any
53
- }
54
- let changed = false
50
+ // JSON Schema 里"值是 schema"的关键字。只走这些位置,**不扫 properties 的键名**,
51
+ // 也不扫 enum / default / const / examples —— 那些不是 schema。
52
+ // 旧写法递归所有对象,会把一个叫 `additionalProperties` 的工具参数(在
53
+ // properties 的键上)当成关键字改掉,也会把 enum 值里的 `items` 键删掉。
54
+ const SCHEMA_MAPS = new Set([
55
+ "properties",
56
+ "patternProperties",
57
+ "$defs",
58
+ "definitions",
59
+ "dependentSchemas",
60
+ ])
61
+ const SCHEMA_ONE = new Set([
62
+ "additionalProperties",
63
+ "unevaluatedProperties",
64
+ "propertyNames",
65
+ "contains",
66
+ "not",
67
+ "if",
68
+ "then",
69
+ "else",
70
+ "contentSchema",
71
+ "items",
72
+ "additionalItems",
73
+ "unevaluatedItems",
74
+ ])
75
+ const SCHEMA_LIST = new Set(["allOf", "anyOf", "oneOf", "prefixItems"])
76
+
77
+ /**
78
+ * 在 schema 对象上原地跑 `visit`,并只递归进真正的 schema 位置。
79
+ * @returns {boolean} `visit` 或任一子 schema 改到过
80
+ */
81
+ function walkSchema(node, visit) {
82
+ if (!node || typeof node !== "object" || Array.isArray(node)) return false
83
+ let changed = visit(node)
55
84
  for (const [k, v] of Object.entries(node)) {
56
- if (k === "additionalProperties") {
57
- // 布尔值上游认,原样留着(false 是"不许额外属性",语义相反,绝对不能动)
58
- if (typeof v !== "boolean") {
59
- node[k] = true
60
- changed = true
61
- }
85
+ if (!v || typeof v !== "object") continue
86
+ if (SCHEMA_MAPS.has(k) && !Array.isArray(v)) {
87
+ for (const child of Object.values(v)) if (walkSchema(child, visit)) changed = true
62
88
  continue
63
89
  }
64
- if (v && typeof v === "object" && loosenAdditionalProperties(v)) changed = true
90
+ if (SCHEMA_LIST.has(k) && Array.isArray(v)) {
91
+ for (const child of v) if (walkSchema(child, visit)) changed = true
92
+ continue
93
+ }
94
+ if (SCHEMA_ONE.has(k)) {
95
+ if (Array.isArray(v)) {
96
+ for (const child of v) if (walkSchema(child, visit)) changed = true
97
+ } else if (walkSchema(v, visit)) changed = true
98
+ continue
99
+ }
100
+ if (k === "dependencies" && !Array.isArray(v)) {
101
+ for (const child of Object.values(v)) {
102
+ if (child && typeof child === "object" && !Array.isArray(child) && walkSchema(child, visit)) {
103
+ changed = true
104
+ }
105
+ }
106
+ }
65
107
  }
66
108
  return changed
67
109
  }
68
110
 
111
+ export function loosenAdditionalProperties(node) {
112
+ return walkSchema(node, (n) => {
113
+ // 布尔值上游认,原样留着(false 是"不许额外属性",语义相反,绝对不能动)
114
+ if (!Object.hasOwn(n, "additionalProperties")) return false
115
+ if (typeof n.additionalProperties === "boolean") return false
116
+ n.additionalProperties = true
117
+ return true
118
+ })
119
+ }
120
+
69
121
  /**
70
- * 改写发往上游的请求体。**一次 parse,三处改写都在这**。
122
+ * 上游的 JSON Schema 校验器不接受 `items: {}`,整轮请求回 HTTP 400:
123
+ *
124
+ * {"error":{"code":null,"message":"超级麦吉: HTTP 400 请求错误: {\"error\":
125
+ * {\"code\":\"InvalidParameter\",\"message\":\"tools.40.function.parameters/
126
+ * properties/functions/items: invalid JSON Schema; check the schema
127
+ * keywords and references Request id: ...\",\"param\":\"\",\"type\":
128
+ * \"BadRequest\"}}","param":null,"type":"invalid_request_error"}}
129
+ *
130
+ * 报错只给路径,不说是哪个客户端。实测(2026-10-28,同一把 key、同一份请求、
131
+ * 只改 tools[0] 的这一处;国内站 `magicrew` 与国际站 `magicrew-intl` 逐行一致):
71
132
  *
72
- * 三处都是实测出来的,不是猜的:
133
+ * items: 缺省 → 200
134
+ * items: {} → 400 invalid JSON Schema
135
+ * items: {description:"x"} → 200 // 空对象才炸,不是"缺 type"
136
+ * items: {type:"string"} → 200
137
+ * items: true → 400 模型响应失败(另一种 400)
138
+ * items: false → 400 模型响应失败
139
+ *
140
+ * 为什么可以删:JSON Schema 里 `items` 的缺省值就是 `{}` —— "任意元素"。
141
+ * 所以 `items: {}` 删掉是**严格等价**(`true` 同理),不是降级放宽。
142
+ * `items: false` 表示"数组必须为空",语义不同,不能删,原样留着。
143
+ *
144
+ * 命中来源:客户端用 `z.array(z.unknown())` / 动态函数列表生成工具 schema 时,
145
+ * 数组元素会序列化成 `{}`。一个真实请求里第 41 个工具的参数是
146
+ * `properties.functions.items: {}`,于是整轮失败。
147
+ *
148
+ * 只走 schema 位置:`properties` 里一个恰好叫 `items` 的工具参数(哪怕 schema 就是
149
+ * `{}`)不会被误删;`enum` / `default` 值里的 `items` 键同样不碰。
150
+ *
151
+ * @returns {boolean} 有没有改到
152
+ */
153
+ export function loosenEmptyItems(node) {
154
+ return walkSchema(node, (n) => {
155
+ if (!Object.hasOwn(n, "items")) return false
156
+ const v = n.items
157
+ const emptyObject =
158
+ v && typeof v === "object" && !Array.isArray(v) && Object.keys(v).length === 0
159
+ if (!emptyObject && v !== true) return false
160
+ delete n.items
161
+ return true
162
+ })
163
+ }
164
+
165
+ /**
166
+ * 改写发往上游的请求体。**一次 parse,四处改写都在这**。
167
+ *
168
+ * 四处都是实测出来的,不是猜的:
73
169
  *
74
170
  * ① max_tokens 抬到 512
75
171
  * 实测 glm-5.3-flash:64 → finish_reason:"length" 且 content:""
@@ -83,6 +179,8 @@ export function loosenAdditionalProperties(node) {
83
179
  *
84
180
  * ③ tools 里的 additionalProperties: {} → true(见 looser 的注释)
85
181
  *
182
+ * ④ tools 里的 items: {} → 删除(见 loosenEmptyItems 的注释)
183
+ *
86
184
  * 没改到时返回原始字符串(省一次序列化 —— 54 个工具的请求体不小)。
87
185
  *
88
186
  * @param {unknown} raw
@@ -116,7 +214,9 @@ export function rewriteRequestBody(raw) {
116
214
  if (Array.isArray(j.tools)) {
117
215
  for (const t of j.tools) {
118
216
  const p = t?.function?.parameters
119
- if (p && typeof p === "object" && loosenAdditionalProperties(p)) changed = true
217
+ if (!p || typeof p !== "object") continue
218
+ if (loosenAdditionalProperties(p)) changed = true
219
+ if (loosenEmptyItems(p)) changed = true
120
220
  }
121
221
  }
122
222
 
@@ -163,7 +263,7 @@ export function makeLoader(c) {
163
263
  let body = init?.body
164
264
  if (body === undefined && req) body = await req.text()
165
265
 
166
- // 三处请求体改写,全在 rewriteRequestBody 里(那里有实测出处)
266
+ // 四处请求体改写,全在 rewriteRequestBody 里(那里有实测出处)
167
267
  body = rewriteRequestBody(body)
168
268
 
169
269
  return fetch(url, {