magicrew-auth 0.3.26 → 0.3.28
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 +4 -1
- package/CHANGELOG.md +280 -0
- package/README.md +133 -59
- package/docs/internals.md +122 -0
- package/package.json +1 -1
- package/src/captcha.mjs +146 -43
- package/src/headless.mjs +79 -29
- package/src/hooks/loader.mjs +163 -0
- package/src/hooks/methods.mjs +238 -147
- package/src/vision.mjs +83 -25
package/src/vision.mjs
CHANGED
|
@@ -128,49 +128,107 @@ function lastClock(text) {
|
|
|
128
128
|
}
|
|
129
129
|
|
|
130
130
|
async function askClock(base, model, imageDataUrl, variant) {
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
|
|
131
|
+
const body = {
|
|
132
|
+
model,
|
|
133
|
+
// ⚠ 这个上限是**给推理 token 留的**,不是给答案留的。
|
|
134
|
+
//
|
|
135
|
+
// 实测(2026-10-28,`workbuddy/deepseek-v4.1-flash`):max_tokens=400 时
|
|
136
|
+
// `finish_reason:"length"`、`reasoning_tokens:400`、**`content` 是 null** ——
|
|
137
|
+
// 推理过程把额度吃干净,答案一个字都没输出。判官拿到空串 ⇒ 判成
|
|
138
|
+
// 「不可信」⇒ 整条无头登录挂掉。给到 6000 时 reasoning 用 899 就收尾,
|
|
139
|
+
// `content` 正常回 "10"。
|
|
140
|
+
// ⇒ 别按「答案就一个数字」去估这个值;它买的是模型的思考空间。
|
|
141
|
+
max_tokens: 6000,
|
|
142
|
+
// ⚠ `reasoning_effort: "low"` —— 实测有据的省法(2026-10-29,同一模型同一批图,
|
|
143
|
+
// 每个配置各跑 6 轮):
|
|
144
|
+
//
|
|
145
|
+
// | | 出数字 | 平均耗时 | 平均 tokens | 平均 reasoning |
|
|
146
|
+
// | 不传 | 5/6 | 20.6 s | 2343 | 3581 字 |
|
|
147
|
+
// | low | 6/6 | 8.4 s | 1007 | 1646 字 |
|
|
148
|
+
//
|
|
149
|
+
// 更快、更省、而且**更稳**(不传的那一组有一次把 6000 token 全烧在思考上、
|
|
150
|
+
// `content` 交白卷)。判出来的角度两者一致。
|
|
151
|
+
//
|
|
152
|
+
// ⚠ 别指望「彻底不思考」——这个模型在网关侧关不掉,实测 12 组参数:
|
|
153
|
+
// `enable_thinking:false` / `thinking_budget:0` → 思考**更长**且 content 空;
|
|
154
|
+
// `reasoning_effort:"none"/"minimal"` → 比不传还长;
|
|
155
|
+
// `thinking:{type:"disabled"}` / `chat_template_kwargs` / `extra_body` /
|
|
156
|
+
// `reasoning:{enabled:false}` → 都只是小幅缩短,没有一个真的关掉。
|
|
157
|
+
// 要真不思考,只能换一个**非推理**的视觉模型(那由用户在 magpie 设置里选)。
|
|
158
|
+
//
|
|
159
|
+
// ⚠ 它**不是** OpenAI 规范字段:下面 send() 里有一层兜底,被 400 就退一步重发。
|
|
160
|
+
reasoning_effort: "low",
|
|
134
161
|
// temperature 0 是为了同一张图两次问法之外的噪声尽量小;
|
|
135
162
|
// 两次用不同措辞(variant)而不是靠随机温度,好复现
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
|
|
163
|
+
temperature: 0,
|
|
164
|
+
messages: [
|
|
165
|
+
{
|
|
166
|
+
role: "user",
|
|
167
|
+
content: [
|
|
168
|
+
{ type: "text", text: CLOCK_PROMPT(variant) },
|
|
169
|
+
{ type: "image_url", image_url: { url: imageDataUrl } },
|
|
170
|
+
],
|
|
171
|
+
},
|
|
172
|
+
],
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const send = (b) =>
|
|
176
|
+
fetch(`${base}/chat/completions`, {
|
|
177
|
+
method: "POST",
|
|
178
|
+
headers: { "content-type": "application/json" },
|
|
179
|
+
body: JSON.stringify(b),
|
|
180
|
+
signal: AbortSignal.timeout(90_000),
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
let res = await send(body)
|
|
184
|
+
if (res.status === 400 && Object.hasOwn(body, "reasoning_effort")) {
|
|
185
|
+
// 兼容兜底:识图模型是**用户自己选的**,不是我们能假设的(插件声明的模型目录里
|
|
186
|
+
// 什么都可能有,magpie 也允许自定义 provider)。有的 OpenAI 兼容端认出这个字段、
|
|
187
|
+
// 有的直接为未知字段回 400。宁可少省那几秒,也不要因为一个可选的优化把整条
|
|
188
|
+
// 无头登录打死 —— 去掉它重发一次,行为退回「不传」的那一档。
|
|
189
|
+
delete body.reasoning_effort
|
|
190
|
+
res = await send(body)
|
|
191
|
+
}
|
|
152
192
|
if (!res.ok) throw new Error(`识图模型回 HTTP ${res.status}`)
|
|
153
193
|
const j = await res.json()
|
|
154
|
-
const
|
|
155
|
-
|
|
194
|
+
const m = j?.choices?.[0]?.message ?? {}
|
|
195
|
+
const content = String(m.content ?? "").trim()
|
|
196
|
+
const reasoning = String(m.reasoning_content ?? "")
|
|
197
|
+
// ① 正常情况:答案在 content 里
|
|
198
|
+
if (content) return { clock: lastClock(content), text: content.slice(-200), from: "content" }
|
|
199
|
+
// ② 兜底:推理模型被截断、content 为空 —— 答案在 reasoning 的最后一段里
|
|
200
|
+
// (实测尾巴就是「最终决定:10。」这种形状)。只看尾巴 300 字,
|
|
201
|
+
// 别把前面推理中间提到的钟点当答案。
|
|
202
|
+
if (reasoning) {
|
|
203
|
+
const tail = reasoning.slice(-300)
|
|
204
|
+
return { clock: lastClock(tail), text: tail.slice(-200), from: "reasoning" }
|
|
205
|
+
}
|
|
206
|
+
return { clock: null, text: "", from: "empty" }
|
|
156
207
|
}
|
|
157
208
|
|
|
158
209
|
/**
|
|
159
210
|
* 让识图模型判断「该顺时针转多少度」。
|
|
160
211
|
*
|
|
212
|
+
* @param {string} imageDataUrl 验证码图的 data URL
|
|
213
|
+
* @param {{model?:string}} [opts] `model` 可覆盖设置里那个(给对比脚本 / 测试用;
|
|
214
|
+
* 正常运行不传,读用户选的)
|
|
161
215
|
* @returns {Promise<{ok:true, deg:number, clock:number, spread:number, model:string}
|
|
162
216
|
* |{ok:false, reason:string, detail?:string}>}
|
|
163
217
|
*/
|
|
164
|
-
export async function decideDegFromImage(imageDataUrl) {
|
|
165
|
-
const model = readVisionModel()
|
|
218
|
+
export async function decideDegFromImage(imageDataUrl, { model: overrideModel } = {}) {
|
|
219
|
+
const model = overrideModel || readVisionModel()
|
|
166
220
|
if (!model) return { ok: false, reason: "no-vision-model" }
|
|
167
221
|
|
|
168
222
|
const base = await resolveGatewayBase()
|
|
169
223
|
if (!base) return { ok: false, reason: "no-gateway", detail: "连不上 magpie 自己的网关(试过 3425/3426/8080)" }
|
|
170
224
|
|
|
171
225
|
const img = await toPngDataUrl(imageDataUrl)
|
|
172
|
-
|
|
173
|
-
|
|
226
|
+
// ⚠ **两个判官并行**:串行等两次就把延迟叠成两倍(实测单次 5~20 秒),
|
|
227
|
+
// 而它们互不依赖 —— 一起发,耗时等于较慢的那一次。
|
|
228
|
+
const [a, b] = await Promise.all([
|
|
229
|
+
askClock(base, model, img, 0).catch((e) => ({ clock: null, text: String(e?.message ?? e) })),
|
|
230
|
+
askClock(base, model, img, 1).catch(() => ({ clock: null, text: "" })),
|
|
231
|
+
])
|
|
174
232
|
|
|
175
233
|
if (a.clock == null && b.clock == null) {
|
|
176
234
|
return { ok: false, reason: "unparsable", detail: `${a.text} / ${b.text}` }
|