magpie-marvis 0.2.2 → 0.2.4
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/CHANGELOG.md +16 -1
- package/package.json +1 -1
- package/src/hooks/methods.mjs +4 -1
- package/src/marvis/browser.mjs +38 -32
- package/src/marvis/login.mjs +10 -3
package/CHANGELOG.md
CHANGED
|
@@ -3,7 +3,22 @@
|
|
|
3
3
|
> 每个版本一节,**一条一行**,说用户能感知的变化。
|
|
4
4
|
> 格式与规矩见 [UPDATING.md](./UPDATING.md)。
|
|
5
5
|
|
|
6
|
-
## 0.2.
|
|
6
|
+
## 0.2.4 — 2026-11-06
|
|
7
|
+
|
|
8
|
+
**修复(`-109` 的最终根因)**
|
|
9
|
+
|
|
10
|
+
- **`marvis_oauth` 会被请求两次**:QQ 的 `self_redirect` + 页面自身重载,
|
|
11
|
+
让同一个 code 打两遍 —— 第一次成功返回 token,第二次因为 code 已消耗而回
|
|
12
|
+
`-109 RC_PARAMS_INVALID`。我之前读的是**页面最终状态**,拿到的正是第二次。
|
|
13
|
+
客户端主进程用 `[oauth-dedup]`("reuse first request")处理这件事;
|
|
14
|
+
插件改成监听 `Network.responseReceived`,**只认第一个响应体**,抓到立刻返回
|
|
15
|
+
- **`authorize()` 阻塞 1.4 秒**:里面调了 `findBrowser()`(15 次注册表查询),
|
|
16
|
+
违反 HOOKS 四·陷阱 1。挪进 `callback()`
|
|
17
|
+
|
|
18
|
+
**修复**
|
|
19
|
+
|
|
20
|
+
- **响应解析对格式不敏感**:浏览器渲染 `application/json` 会加缩进/包装,
|
|
21
|
+
原来只看 `startsWith("{")` 会漏判。改成「抠第一个 `{` 到最后一个 `}`」再解析
|
|
7
22
|
|
|
8
23
|
**修复**
|
|
9
24
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "magpie-marvis",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
4
4
|
"description": "magpie / OpenCode provider plugin for Tencent Marvis: its DeepSeek V4 Pro, Kimi K2.5, Claude Opus/Sonnet 4.6, GPT-5.4 and Hunyuan models through the Marvis model gateway — no API key to paste, sign in with QQ or WeChat.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.mjs",
|
package/src/hooks/methods.mjs
CHANGED
|
@@ -60,7 +60,9 @@ function oauthLogin(loginType, label, instructions) {
|
|
|
60
60
|
label,
|
|
61
61
|
async authorize() {
|
|
62
62
|
const guid = deviceGuid()
|
|
63
|
-
|
|
63
|
+
// ⚠ 不要在这里 `findBrowser()` —— 它要查 15 次注册表(实测 1.4 秒),
|
|
64
|
+
// 而 `authorize()` 必须**立刻**返回(HOOKS 四·陷阱 1)。
|
|
65
|
+
// 浏览器查找挪进 `callback`,那时才真正需要它。
|
|
64
66
|
const url = authorizeUrl(loginType, guid)
|
|
65
67
|
|
|
66
68
|
return {
|
|
@@ -70,6 +72,7 @@ function oauthLogin(loginType, label, instructions) {
|
|
|
70
72
|
method: "auto",
|
|
71
73
|
instructions,
|
|
72
74
|
async callback(_url, _input, signal) {
|
|
75
|
+
const browser = findBrowser()
|
|
73
76
|
if (!browser) {
|
|
74
77
|
throw new Error(
|
|
75
78
|
"没有找到可用的 Chromium 内核浏览器(Chrome / Edge / 360 / QQ 浏览器都行)。" +
|
package/src/marvis/browser.mjs
CHANGED
|
@@ -315,52 +315,58 @@ export async function openIsolatedLogin({ browser, url, match, extraHeaders, tim
|
|
|
315
315
|
// 导航到授权页(钩子已装好:UA / client hints / navigator.webdriver)
|
|
316
316
|
await send(6, "Page.navigate", { url }).catch(() => {})
|
|
317
317
|
|
|
318
|
-
//
|
|
319
|
-
//
|
|
320
|
-
//
|
|
321
|
-
//
|
|
322
|
-
//
|
|
318
|
+
// ⚠⚠ 核心:`marvis_oauth` 会被**请求两次** —— QQ 的 `self_redirect` + 页面自身
|
|
319
|
+
// 的重载会让同一个 code 打两遍。第一次成功并返回 token,第二次因为 code
|
|
320
|
+
// 已被消耗而回 `-109 RC_PARAMS_INVALID`。
|
|
321
|
+
// 客户端主进程用 `[oauth-dedup]` 处理这件事("reuse first request");
|
|
322
|
+
// 我们这边等价的做法是:**只认第一个响应体**,绝不去读页面最终状态。
|
|
323
323
|
//
|
|
324
|
-
//
|
|
325
|
-
//
|
|
324
|
+
// 所以:监听 `Network.responseReceived` → 命中 `marvis_oauth` →
|
|
325
|
+
// 立刻 `Network.getResponseBody` 把正文抓下来,然后马上返回。
|
|
326
326
|
let nextId = 10
|
|
327
|
+
/** @type {string | null} */
|
|
328
|
+
let firstRequestId = null
|
|
329
|
+
/** @type {string} */
|
|
327
330
|
let hitUrl = ""
|
|
328
|
-
let body = ""
|
|
329
331
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
let href = ""
|
|
332
|
+
ws.addEventListener("message", (event) => {
|
|
333
|
+
let msg
|
|
334
334
|
try {
|
|
335
|
-
|
|
336
|
-
expression: "location.href",
|
|
337
|
-
returnByValue: true,
|
|
338
|
-
})
|
|
339
|
-
href = String(r?.result?.value ?? "")
|
|
335
|
+
msg = JSON.parse(String(event.data))
|
|
340
336
|
} catch {
|
|
341
|
-
|
|
337
|
+
return
|
|
338
|
+
}
|
|
339
|
+
if (msg.method === "Network.responseReceived" && !firstRequestId) {
|
|
340
|
+
const { requestId, response } = msg.params ?? {}
|
|
341
|
+
const u = String(response?.url ?? "")
|
|
342
|
+
// 只认 2xx/4xx 的**文档**响应;图片/脚本不算
|
|
343
|
+
const type = String(response?.type ?? "")
|
|
344
|
+
if (u && match.test(u) && (type === "Document" || type === "Fetch" || type === "XHR")) {
|
|
345
|
+
firstRequestId = requestId
|
|
346
|
+
hitUrl = u
|
|
347
|
+
}
|
|
342
348
|
}
|
|
349
|
+
})
|
|
350
|
+
|
|
351
|
+
while (Date.now() < deadline) {
|
|
352
|
+
if (signal?.aborted) throw new Error("登录已取消")
|
|
343
353
|
|
|
344
|
-
if (
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
for (let i = 0; i < 20; i++) {
|
|
354
|
+
if (firstRequestId) {
|
|
355
|
+
// 多试几次:responseReceived 之后 body 可能还没落盘
|
|
356
|
+
for (let i = 0; i < 15; i++) {
|
|
348
357
|
try {
|
|
349
|
-
const r = await send(nextId++, "
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
}
|
|
353
|
-
body = String(r?.result?.value ?? "")
|
|
354
|
-
if (body.trim()) break
|
|
358
|
+
const r = await send(nextId++, "Network.getResponseBody", { requestId: firstRequestId })
|
|
359
|
+
const raw = String(r?.body ?? "")
|
|
360
|
+
const body = r?.base64Encoded ? Buffer.from(raw, "base64").toString("utf8") : raw
|
|
361
|
+
if (body.trim()) return { url: hitUrl, body, browser: browser.name }
|
|
355
362
|
} catch {
|
|
356
|
-
/*
|
|
363
|
+
/* body 还没就绪 */
|
|
357
364
|
}
|
|
358
|
-
await new Promise((r) => setTimeout(r,
|
|
365
|
+
await new Promise((r) => setTimeout(r, 300))
|
|
359
366
|
}
|
|
360
|
-
return { url: hitUrl, body, browser: browser.name }
|
|
361
367
|
}
|
|
362
368
|
|
|
363
|
-
await new Promise((r) => setTimeout(r,
|
|
369
|
+
await new Promise((r) => setTimeout(r, 500))
|
|
364
370
|
}
|
|
365
371
|
|
|
366
372
|
} finally {
|
package/src/marvis/login.mjs
CHANGED
|
@@ -176,12 +176,19 @@ export async function exchangeCode(code, loginType, _guid, state) {
|
|
|
176
176
|
export async function parseCallback(callbackUrl, body, opts = {}) {
|
|
177
177
|
const text = String(body ?? "").trim()
|
|
178
178
|
|
|
179
|
-
//
|
|
180
|
-
|
|
179
|
+
// 主路径:正文就是 JSON。
|
|
180
|
+
// ⚠ 不能只看 `startsWith("{")` —— 浏览器渲染 `application/json` 时会加缩进/包装,
|
|
181
|
+
// 也可能前后带空白。改成「抠第一个 `{` 到最后一个 `}`」再解析。
|
|
182
|
+
const jsonText = (() => {
|
|
183
|
+
const a = text.indexOf("{")
|
|
184
|
+
const b = text.lastIndexOf("}")
|
|
185
|
+
return a >= 0 && b > a ? text.slice(a, b + 1) : ""
|
|
186
|
+
})()
|
|
187
|
+
if (jsonText) {
|
|
181
188
|
/** @type {any} */
|
|
182
189
|
let body0
|
|
183
190
|
try {
|
|
184
|
-
body0 = JSON.parse(
|
|
191
|
+
body0 = JSON.parse(jsonText)
|
|
185
192
|
} catch {
|
|
186
193
|
body0 = null
|
|
187
194
|
}
|