magpie-marvis 0.1.7 → 0.1.9
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 +10 -2
- package/package.json +1 -1
- package/src/marvis/browser.mjs +27 -27
- package/src/marvis/login.mjs +15 -3
package/CHANGELOG.md
CHANGED
|
@@ -3,9 +3,17 @@
|
|
|
3
3
|
> 每个版本一节,**一条一行**,说用户能感知的变化。
|
|
4
4
|
> 格式与规矩见 [UPDATING.md](./UPDATING.md)。
|
|
5
5
|
|
|
6
|
-
## 0.1.
|
|
6
|
+
## 0.1.9 — 2026-11-06
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
**修复(这是 `-109` 的真正根因)**
|
|
9
|
+
|
|
10
|
+
- **`redirect_uri` 少了 `?login_type=QC`**。Marvis 渲染层的 `Bpe(e)` 返回的是
|
|
11
|
+
`.../marvis_oauth?login_type=${e}`,我写成了 `.../marvis_oauth`。
|
|
12
|
+
服务端靠这个参数知道「用哪个 app 的 `client_secret` 去换 code」——
|
|
13
|
+
少了它,**哪怕 code 是真的也一律 `-109 RC_PARAMS_INVALID`**(是参数无效,不是 code 无效)。
|
|
14
|
+
- 浏览器登录改为**正常走完回调请求**(带 Cookie、带正确 `redirect_uri`),
|
|
15
|
+
只读第一个响应体;不再拦截后用 Node 重放(重放丢 Cookie,同样 `-109`)
|
|
16
|
+
- 微信登录同步修正
|
|
9
17
|
|
|
10
18
|
- **浏览器登录「授权码已被使用过」(`-109`)**:根因是 **QQ 会把 `redirect_uri` 的查询串整个丢掉** ——
|
|
11
19
|
落地页收到的是 `?code=...&state=...`,**没有 `login_type`**,服务端因此判参数无效。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "magpie-marvis",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.9",
|
|
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/marvis/browser.mjs
CHANGED
|
@@ -300,16 +300,14 @@ export async function openIsolatedLogin({ browser, url, match, timeoutMs = 5 * 6
|
|
|
300
300
|
source: "Object.defineProperty(navigator,'webdriver',{get:()=>undefined})",
|
|
301
301
|
}).catch(() => {})
|
|
302
302
|
|
|
303
|
-
// ⚠
|
|
304
|
-
//
|
|
305
|
-
//
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
let nextId = 10
|
|
312
|
-
/** @type {{url:string} | null} */
|
|
303
|
+
// ⚠ 让浏览器**正常走完**回调请求(带 Cookie、带正确的 redirect_uri),
|
|
304
|
+
// 我们只读**第一个**响应的正文。不拦截、不重发 ——
|
|
305
|
+
// 拦截后用 Node 的 fetch 重放会丢掉 Cookie,服务端就回 `-109`。
|
|
306
|
+
/** @type {string | null} */
|
|
307
|
+
let hitRequestId = null
|
|
308
|
+
/** @type {string} */
|
|
309
|
+
let hitUrl = ""
|
|
310
|
+
/** @type {{url:string, body:string} | null} */
|
|
313
311
|
let hit = null
|
|
314
312
|
|
|
315
313
|
ws.addEventListener("message", (event) => {
|
|
@@ -319,30 +317,32 @@ export async function openIsolatedLogin({ browser, url, match, timeoutMs = 5 * 6
|
|
|
319
317
|
} catch {
|
|
320
318
|
return
|
|
321
319
|
}
|
|
322
|
-
if (msg.method === "
|
|
323
|
-
const { requestId,
|
|
324
|
-
const u = String(
|
|
320
|
+
if (msg.method === "Network.responseReceived" && !hitRequestId) {
|
|
321
|
+
const { requestId, response } = msg.params ?? {}
|
|
322
|
+
const u = String(response?.url ?? "")
|
|
325
323
|
if (u && match.test(u)) {
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
send(nextId++, "Fetch.fulfillRequest", {
|
|
329
|
-
requestId,
|
|
330
|
-
responseCode: 200,
|
|
331
|
-
responseHeaders: [{ name: "content-type", value: "text/html; charset=utf-8" }],
|
|
332
|
-
body: Buffer.from(
|
|
333
|
-
"<!doctype html><meta charset=utf-8><title>Marvis 授权完成</title>" +
|
|
334
|
-
"<body style=\"font:16px/1.6 system-ui;padding:48px;color:#111\">" +
|
|
335
|
-
"<h2>授权完成</h2><p>回到 magpie 即可,窗口会自动关闭。</p></body>",
|
|
336
|
-
"utf8",
|
|
337
|
-
).toString("base64"),
|
|
338
|
-
}).catch(() => {})
|
|
324
|
+
hitRequestId = requestId
|
|
325
|
+
hitUrl = u
|
|
339
326
|
}
|
|
340
327
|
}
|
|
341
328
|
})
|
|
342
329
|
|
|
330
|
+
let nextId = 10
|
|
343
331
|
while (Date.now() < deadline) {
|
|
344
332
|
if (signal?.aborted) throw new Error("登录已取消")
|
|
345
|
-
|
|
333
|
+
|
|
334
|
+
if (hitRequestId && !hit) {
|
|
335
|
+
try {
|
|
336
|
+
const r = await send(nextId++, "Network.getResponseBody", { requestId: hitRequestId })
|
|
337
|
+
const raw = String(r?.body ?? "")
|
|
338
|
+
const body = r?.base64Encoded ? Buffer.from(raw, "base64").toString("utf8") : raw
|
|
339
|
+
if (body.trim()) hit = { url: hitUrl, body }
|
|
340
|
+
} catch {
|
|
341
|
+
/* body 还没就绪,下一轮再取 */
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
if (hit) return { url: hit.url, body: hit.body, browser: browser.name }
|
|
345
|
+
|
|
346
346
|
await new Promise((r) => setTimeout(r, 400))
|
|
347
347
|
}
|
|
348
348
|
|
package/src/marvis/login.mjs
CHANGED
|
@@ -17,7 +17,17 @@ import { createRequire } from "node:module"
|
|
|
17
17
|
|
|
18
18
|
const require = createRequire(import.meta.url)
|
|
19
19
|
|
|
20
|
-
/**
|
|
20
|
+
/**
|
|
21
|
+
* redirect_uri 的**基址**(不带查询串)。
|
|
22
|
+
*
|
|
23
|
+
* ⚠⚠ 授权时必须拼上 `?login_type=<QC|WX>` —— 这是整个登录的关键:
|
|
24
|
+
* 服务端要靠它知道「用哪个 app 的 client_secret 去换 code」。
|
|
25
|
+
* 少了它,哪怕 code 是真的,也会一律回 `-109 RC_PARAMS_INVALID`
|
|
26
|
+
* (`RC_PARAMS_INVALID` = 参数无效,不是 code 无效)。
|
|
27
|
+
*
|
|
28
|
+
* 对照 Marvis 渲染层的 `Bpe(e)`:
|
|
29
|
+
* `${base}/${n}/marvis_oauth?login_type=${e}` // e = QC | WX
|
|
30
|
+
*/
|
|
21
31
|
const REDIRECT = `${BASE}/marvis_client_login/marvis_oauth`
|
|
22
32
|
|
|
23
33
|
/**
|
|
@@ -30,7 +40,8 @@ export function authorizeUrl(loginType, guid) {
|
|
|
30
40
|
if (loginType === "WX") {
|
|
31
41
|
const p = new URLSearchParams({
|
|
32
42
|
appid: WX_APP_ID,
|
|
33
|
-
|
|
43
|
+
// ⚠ 同样必须带 ?login_type=WX
|
|
44
|
+
redirect_uri: `${REDIRECT}?login_type=${loginType}`,
|
|
34
45
|
response_type: "code",
|
|
35
46
|
scope: "snsapi_login",
|
|
36
47
|
state: guid,
|
|
@@ -43,7 +54,8 @@ export function authorizeUrl(loginType, guid) {
|
|
|
43
54
|
display: "pc",
|
|
44
55
|
response_type: "code",
|
|
45
56
|
client_id: QQ_APP_ID,
|
|
46
|
-
|
|
57
|
+
// ⚠ 必须带 ?login_type=QC,见 REDIRECT 上方说明
|
|
58
|
+
redirect_uri: `${REDIRECT}?login_type=${loginType}`,
|
|
47
59
|
state: String(Math.floor(Math.random() * 1e9)),
|
|
48
60
|
})
|
|
49
61
|
return `https://graph.qq.com/oauth2.0/authorize?${p}`
|