magpie-marvis 0.2.3 → 0.2.5
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 +18 -1
- package/package.json +1 -1
- package/src/hooks/methods.mjs +4 -1
- package/src/marvis/browser.mjs +38 -32
- package/src/marvis/desktop.mjs +1 -1
- package/src/marvis/login.mjs +6 -2
package/CHANGELOG.md
CHANGED
|
@@ -3,7 +3,24 @@
|
|
|
3
3
|
> 每个版本一节,**一条一行**,说用户能感知的变化。
|
|
4
4
|
> 格式与规矩见 [UPDATING.md](./UPDATING.md)。
|
|
5
5
|
|
|
6
|
-
## 0.2.
|
|
6
|
+
## 0.2.5 — 2026-11-06
|
|
7
|
+
|
|
8
|
+
**修复**
|
|
9
|
+
|
|
10
|
+
- **`state` 格式与客户端不一致**:app 是 `state=${Math.random()*100}`(**小数**,如 `31.1177412`),
|
|
11
|
+
我写成了 `Math.floor(Math.random()*1e9)`(整数)。抓包确认服务端收到的是 `845583851` 这种
|
|
12
|
+
- **`loginType` 映射错了**:渲染层 `yct={QC:"1",WX:"2",WXAPP:"2"}`,
|
|
13
|
+
`Ual-Access-Login-Type` 应该是 `1`/`2`,我写的是 `6`/`5`
|
|
14
|
+
|
|
15
|
+
**修复(`-109` 的最终根因)**
|
|
16
|
+
|
|
17
|
+
- **`marvis_oauth` 会被请求两次**:QQ 的 `self_redirect` + 页面自身重载,
|
|
18
|
+
让同一个 code 打两遍 —— 第一次成功返回 token,第二次因为 code 已消耗而回
|
|
19
|
+
`-109 RC_PARAMS_INVALID`。我之前读的是**页面最终状态**,拿到的正是第二次。
|
|
20
|
+
客户端主进程用 `[oauth-dedup]`("reuse first request")处理这件事;
|
|
21
|
+
插件改成监听 `Network.responseReceived`,**只认第一个响应体**,抓到立刻返回
|
|
22
|
+
- **`authorize()` 阻塞 1.4 秒**:里面调了 `findBrowser()`(15 次注册表查询),
|
|
23
|
+
违反 HOOKS 四·陷阱 1。挪进 `callback()`
|
|
7
24
|
|
|
8
25
|
**修复**
|
|
9
26
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "magpie-marvis",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.5",
|
|
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/desktop.mjs
CHANGED
|
@@ -226,7 +226,7 @@ export function parseLoginState(text) {
|
|
|
226
226
|
openid: m[1],
|
|
227
227
|
accessToken: m[2],
|
|
228
228
|
refreshToken: m[3] || undefined,
|
|
229
|
-
loginType: /^WX
|
|
229
|
+
loginType: /^WX/i.test(m[4] ?? "") ? "2" : "1",
|
|
230
230
|
guid: memGuid ? memGuid[1].toLowerCase() : guid,
|
|
231
231
|
expiresAt: expire ? Number(expire[1]) * 1000 : Date.now() + 30 * 24 * 3600_000,
|
|
232
232
|
}
|
package/src/marvis/login.mjs
CHANGED
|
@@ -56,7 +56,9 @@ export function authorizeUrl(loginType, guid) {
|
|
|
56
56
|
client_id: QQ_APP_ID,
|
|
57
57
|
// ⚠ 必须带 ?login_type=QC,见 REDIRECT 上方说明
|
|
58
58
|
redirect_uri: `${REDIRECT}?login_type=${loginType}`,
|
|
59
|
-
state
|
|
59
|
+
// ⚠ 必须与 Marvis 渲染层逐字一致:`state=${Math.random()*100}`
|
|
60
|
+
// 是**小数**(如 31.1177412),不是整数。服务端会拿它做校验,格式不符会被判参数无效。
|
|
61
|
+
state: String(Math.random() * 100),
|
|
60
62
|
})
|
|
61
63
|
return `https://graph.qq.com/oauth2.0/authorize?${p}`
|
|
62
64
|
}
|
|
@@ -228,7 +230,9 @@ function normalize(info, accessToken, openid) {
|
|
|
228
230
|
refreshToken: info?.refresh_token ? String(info.refresh_token) : undefined,
|
|
229
231
|
nickname: info?.nick_name ? safeDecode(info.nick_name) : undefined,
|
|
230
232
|
avatar: info?.head_img_url ?? undefined,
|
|
231
|
-
|
|
233
|
+
// ⚠ `Ual-Access-Login-Type` 的真实取值:QC→"1"、WX/WXAPP→"2"
|
|
234
|
+
// (渲染层 `yct={QC:"1",WX:"2",WXAPP:"2"}`)。不是 6。
|
|
235
|
+
loginType: info?.login_type === "WX" || info?.login_type === "WXAPP" ? "2" : "1",
|
|
232
236
|
expiresAt: expire > 1e9 ? expire * 1000 : Date.now() + 30 * 24 * 3600_000,
|
|
233
237
|
}
|
|
234
238
|
}
|