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 CHANGED
@@ -3,9 +3,17 @@
3
3
  > 每个版本一节,**一条一行**,说用户能感知的变化。
4
4
  > 格式与规矩见 [UPDATING.md](./UPDATING.md)。
5
5
 
6
- ## 0.1.7 — 2026-11-06
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.7",
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",
@@ -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
- // 实测 QQ 会把 redirect_uri 的查询串整个丢掉 —— 落地页收到的
305
- // `?code=...&state=...` 里**没有 login_type**,服务端直接 `-109 RC_PARAMS_INVALID`。
306
- // 所以不能靠那个页面的响应,只能自己从 URL 里取 code,再带上 login_type 换一次。
307
- await send(4, "Fetch.enable", {
308
- patterns: [{ urlPattern: "*marvis_client_login/marvis_oauth*", requestStage: "Request" }],
309
- }).catch(() => {})
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 === "Fetch.requestPaused" && !hit) {
323
- const { requestId, request } = msg.params ?? {}
324
- const u = String(request?.url ?? "")
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
- hit = { url: u }
327
- // 让页面显示一句「授权完成」,避免用户对着 -109 的 JSON 发愣
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
- if (hit) return { url: hit.url, body: "", browser: browser.name }
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
 
@@ -17,7 +17,17 @@ import { createRequire } from "node:module"
17
17
 
18
18
  const require = createRequire(import.meta.url)
19
19
 
20
- /** redirect_uri —— 与 Marvis 客户端逐字一致 */
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
- redirect_uri: REDIRECT,
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
- redirect_uri: REDIRECT,
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}`