magpie-marvis 0.2.5 → 0.2.7

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
@@ -1,78 +1,85 @@
1
- # 更新记录
2
-
3
- > 每个版本一节,**一条一行**,说用户能感知的变化。
4
- > 格式与规矩见 [UPDATING.md](./UPDATING.md)。
5
-
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()`
24
-
25
- **修复**
26
-
27
- - **响应解析对格式不敏感**:浏览器渲染 `application/json` 会加缩进/包装,
28
- 原来只看 `startsWith("{")` 会漏判。改成「抠第一个 `{` 到最后一个 `}`」再解析
29
-
30
- **修复**
31
-
32
- - **授权页卡在「正在完成授权…」,magpie 加不进号**:上一版用 `Fetch.fulfillRequest`
33
- 把回调换成静态页再在页面里 `fetch` 换取 —— 但执行上下文会随导航销毁,`Runtime.evaluate`
34
- 拿不到结果。改成**完全不拦截**:让浏览器自己走完换取(TLS 指纹 / Cookie / Referer 天然齐全),
35
- 只轮询 `location.href` 等它跳到回调地址,再读 `document.body.innerText`
36
-
37
- **变更**
38
-
39
- - **上下文窗口声明改为 1,000,000**(除 `hunyuan-turbos-vision` 仍 32k)。
40
- 依据:实测 `main-auto` 发 **400,000 字符 = 190,511 tokens** 返回 200,
41
- 远超 `config_defaults.yaml` 声明的 128k —— 那个 128k 是客户端自己的保守值
42
- - **用户设置优先**:`config()` 不再整块覆盖 provider。改成**逐模型、逐字段合并** ——
43
- 用户在 magpie 设置里改过的 `limit.context` / `limit.output` / `name` / `api` 一律保留,
44
- 插件只补缺失字段和新增模型
45
-
46
- **修复(逆向客户端主进程后对齐)**
47
-
48
- - **换取请求改在页面上下文里发**:`yybadaccess` 的 WAF 做 TLS 指纹校验,
49
- Node 的 undici(OpenSSL)和 Chromium(BoringSSL)指纹不同 ——
50
- 带头的请求从 Node 发出去一律 **400 空响应**。
51
- - **补 `Referer`**:客户端重放时会把原始请求头整份复制(`reqHeaders` / `cookies` / `referer`),
52
- 缺 referer 服务端判来源不合法。
53
- - **去掉 `Ual-Access-Businessid`**:实测它会让 WAF 直接 400(这是我这轮踩的最大坑——
54
- 加头反而更糟,不加头时至少还能拿到 `-109` JSON)。
55
- - 浏览器先开 `about:blank`、装好钩子再 `Page.navigate` —— 否则首次请求漏过去,拦截永远不触发。
56
-
57
- **修复(这是 `-109` 的真正根因)**
58
-
59
- - **`redirect_uri` 少了 `?login_type=QC`**。Marvis 渲染层的 `Bpe(e)` 返回的是
60
- `.../marvis_oauth?login_type=${e}`,我写成了 `.../marvis_oauth`。
61
- 服务端靠这个参数知道「用哪个 app 的 `client_secret` 去换 code」——
62
- 少了它,**哪怕 code 是真的也一律 `-109 RC_PARAMS_INVALID`**(是参数无效,不是 code 无效)。
63
- - 浏览器登录改为**正常走完回调请求**(带 Cookie、带正确 `redirect_uri`),
64
- 只读第一个响应体;不再拦截后用 Node 重放(重放丢 Cookie,同样 `-109`)
65
- - 微信登录同步修正
66
-
67
- - **浏览器登录「授权码已被使用过」(`-109`)**:根因是 **QQ 会把 `redirect_uri` 的查询串整个丢掉** ——
68
- 落地页收到的是 `?code=...&state=...`,**没有 `login_type`**,服务端因此判参数无效。
69
- 改成用 CDP `Fetch.requestPaused` 在**跳转前**拦下回调,自己从 URL 取 `code` 再带 `login_type` 换。
70
- 页面同时显示「授权完成」,用户不用对着 `-109` 的 JSON 发愣
71
- - **换 token 带了鉴权头**:`marvis_oauth` 是**免鉴权**端点,带 `Ual-Access-*` 签名头会直接 400 空响应
72
- - **`deviceGuid()` 永远取不到注册表**:JS 里 `\S` `\T` 是无效转义,反斜杠被吃掉,
73
- `reg` 收到 `HKCUSOFTWARETencentMarvisEnv` → 「无效参数」。改用 `String.raw`
74
- - **本机导入的 guid 与 token 不同源**:服务端按 `(openid, guid, token)` 三元组校验,
75
- 现在优先从**同一段内存**里取 `guid=`
76
- - 去掉 `--disable-blink-features=AutomationControlled`(Edge 会弹「不受支持的命令行标记」警告条),
77
- 改用 CDP `Page.addScriptToEvaluateOnNewDocument` 抹掉 `navigator.webdriver`
78
-
1
+ # 更新记录
2
+
3
+ > 每个版本一节,**一条一行**,说用户能感知的变化。
4
+ > 格式与规矩见 [UPDATING.md](./UPDATING.md)。
5
+
6
+ ## 0.2.7 — 2026-11-06
7
+
8
+ **修复**
9
+
10
+ - **UA 换回客户端自己的 Electron UA**(`Marvis/1.60.3300.408 ... Electron/44.7.0`)。
11
+ 之前为了「伪装成普通浏览器」用的是纯 Chrome UA —— 服务端很可能就是按这个区分调用方
12
+ - **去掉 `--disable-blink-features=AutomationControlled`**:它会在地址栏下面挂一条
13
+ 「你使用的是不受支持的命令行标记」黄色警告条。`navigator.webdriver` 改用 CDP 抹掉
14
+
15
+ **修复**
16
+
17
+ - **`state` 格式与客户端不一致**:app 是 `state=${Math.random()*100}`(**小数**,如 `31.1177412`),
18
+ 我写成了 `Math.floor(Math.random()*1e9)`(整数)。抓包确认服务端收到的是 `845583851` 这种
19
+ - **`loginType` 映射错了**:渲染层 `yct={QC:"1",WX:"2",WXAPP:"2"}`,
20
+ `Ual-Access-Login-Type` 应该是 `1`/`2`,我写的是 `6`/`5`
21
+
22
+ **修复(`-109` 的最终根因)**
23
+
24
+ - **`marvis_oauth` 会被请求两次**:QQ 的 `self_redirect` + 页面自身重载,
25
+ 让同一个 code 打两遍 —— 第一次成功返回 token,第二次因为 code 已消耗而回
26
+ `-109 RC_PARAMS_INVALID`。我之前读的是**页面最终状态**,拿到的正是第二次。
27
+ 客户端主进程用 `[oauth-dedup]`("reuse first request")处理这件事;
28
+ 插件改成监听 `Network.responseReceived`,**只认第一个响应体**,抓到立刻返回
29
+ - **`authorize()` 阻塞 1.4 秒**:里面调了 `findBrowser()`(15 次注册表查询),
30
+ 违反 HOOKS 四·陷阱 1。挪进 `callback()`
31
+
32
+ **修复**
33
+
34
+ - **响应解析对格式不敏感**:浏览器渲染 `application/json` 会加缩进/包装,
35
+ 原来只看 `startsWith("{")` 会漏判。改成「抠第一个 `{` 到最后一个 `}`」再解析
36
+
37
+ **修复**
38
+
39
+ - **授权页卡在「正在完成授权…」,magpie 加不进号**:上一版用 `Fetch.fulfillRequest`
40
+ 把回调换成静态页再在页面里 `fetch` 换取 —— 但执行上下文会随导航销毁,`Runtime.evaluate`
41
+ 拿不到结果。改成**完全不拦截**:让浏览器自己走完换取(TLS 指纹 / Cookie / Referer 天然齐全),
42
+ 只轮询 `location.href` 等它跳到回调地址,再读 `document.body.innerText`
43
+
44
+ **变更**
45
+
46
+ - **上下文窗口声明改为 1,000,000**(除 `hunyuan-turbos-vision` 仍 32k)。
47
+ 依据:实测 `main-auto` 发 **400,000 字符 = 190,511 tokens** 返回 200,
48
+ 远超 `config_defaults.yaml` 声明的 128k —— 那个 128k 是客户端自己的保守值
49
+ - **用户设置优先**:`config()` 不再整块覆盖 provider。改成**逐模型、逐字段合并** ——
50
+ 用户在 magpie 设置里改过的 `limit.context` / `limit.output` / `name` / `api` 一律保留,
51
+ 插件只补缺失字段和新增模型
52
+
53
+ **修复(逆向客户端主进程后对齐)**
54
+
55
+ - **换取请求改在页面上下文里发**:`yybadaccess` 的 WAF 做 TLS 指纹校验,
56
+ Node 的 undici(OpenSSL)和 Chromium(BoringSSL)指纹不同 ——
57
+ 带头的请求从 Node 发出去一律 **400 空响应**。
58
+ - **补 `Referer`**:客户端重放时会把原始请求头整份复制(`reqHeaders` / `cookies` / `referer`),
59
+ 缺 referer 服务端判来源不合法。
60
+ - **去掉 `Ual-Access-Businessid`**:实测它会让 WAF 直接 400(这是我这轮踩的最大坑——
61
+ 加头反而更糟,不加头时至少还能拿到 `-109` JSON)。
62
+ - 浏览器先开 `about:blank`、装好钩子再 `Page.navigate` —— 否则首次请求漏过去,拦截永远不触发。
63
+
64
+ **修复(这是 `-109` 的真正根因)**
65
+
66
+ - **`redirect_uri` 少了 `?login_type=QC`**。Marvis 渲染层的 `Bpe(e)` 返回的是
67
+ `.../marvis_oauth?login_type=${e}`,我写成了 `.../marvis_oauth`。
68
+ 服务端靠这个参数知道「用哪个 app 的 `client_secret` 去换 code」——
69
+ 少了它,**哪怕 code 是真的也一律 `-109 RC_PARAMS_INVALID`**(是参数无效,不是 code 无效)。
70
+ - 浏览器登录改为**正常走完回调请求**(带 Cookie、带正确 `redirect_uri`),
71
+ 只读第一个响应体;不再拦截后用 Node 重放(重放丢 Cookie,同样 `-109`)
72
+ - 微信登录同步修正
73
+
74
+ - **浏览器登录「授权码已被使用过」(`-109`)**:根因是 **QQ 会把 `redirect_uri` 的查询串整个丢掉** ——
75
+ 落地页收到的是 `?code=...&state=...`,**没有 `login_type`**,服务端因此判参数无效。
76
+ 改成用 CDP `Fetch.requestPaused` 在**跳转前**拦下回调,自己从 URL 取 `code` 再带 `login_type` 换。
77
+ 页面同时显示「授权完成」,用户不用对着 `-109` 的 JSON 发愣
78
+ - **换 token 带了鉴权头**:`marvis_oauth` 是**免鉴权**端点,带 `Ual-Access-*` 签名头会直接 400 空响应
79
+ - **`deviceGuid()` 永远取不到注册表**:JS 里 `\S` `\T` 是无效转义,反斜杠被吃掉,
80
+ `reg` 收到 `HKCUSOFTWARETencentMarvisEnv` → 「无效参数」。改用 `String.raw`
81
+ - **本机导入的 guid 与 token 不同源**:服务端按 `(openid, guid, token)` 三元组校验,
82
+ 现在优先从**同一段内存**里取 `guid=`
83
+ - 去掉 `--disable-blink-features=AutomationControlled`(Edge 会弹「不受支持的命令行标记」警告条),
84
+ 改用 CDP `Page.addScriptToEvaluateOnNewDocument` 抹掉 `navigator.webdriver`
85
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "magpie-marvis",
3
- "version": "0.2.5",
3
+ "version": "0.2.7",
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",
@@ -1,142 +1,154 @@
1
- // ② auth.methods —— 两条登录路
2
- //
3
- // A. QQ / 微信 授权(默认):自己起一个**隔离浏览器**(一次性 profile、伪装正常 Chrome、
4
- // 去掉自动化痕迹),用户在窗口里授权,插件从 CDP 抓回调 URL 里的 `code`。
5
- // → `{ url, opened: true, method: "auto", callback }`
6
- // `opened: true` 让 magpie **不要**再开系统默认浏览器(否则弹两个窗口)。
7
- //
8
- // B. 从本机 Marvis 导入:装了客户端且登录过的人,直接读进程内存里的登录态。
9
- // → 同样 `opened: true` + `method: "auto"`,整个登录就是「点一下 → 读内存 → 成功」。
10
- //
11
- // ⚠ `authorize()` 必须**立刻**返回 —— 真正等用户的是 `callback()`。
12
- // 在这里 await 登录会和 magpie 双向死锁(HOOKS §四·陷阱1)。
13
- //
14
- // ⚠ 为什么不能像别的插件那样起本地 loopback 收回调:redirect_uri 是 QQ 互联里
15
- // 预注册死的,改不了(详见 src/marvis/login.mjs 顶部)。所以只能「跳过去 → 读地址栏」。
16
-
17
- import { PROVIDER, LOGIN_TYPE } from "../context.mjs"
18
- import { authorizeUrl, parseCallback, deviceGuid } from "../marvis/login.mjs"
19
- import { openIsolatedLogin, findBrowser } from "../marvis/browser.mjs"
20
- import { resolveAccount } from "../account-name.mjs"
21
- import { importFromDesktop, desktopAvailable } from "../marvis/desktop.mjs"
22
-
23
- /** 回调地址判据:跳到 marvis_oauth 且带 code= */
24
- const CALLBACK = /marvis_client_login\/marvis_oauth\?[^#]*\bcode=/i
25
-
26
- /**
27
- * 登录成功时给宿主的字段。
28
- * `accountId` 是**名字**(卡片上显示),`uid` 是身份(HOOKS §八)。
29
- * ⚠ `refresh` 必须有 —— 缺了它 magpie 不会把账号写进 plugin-auth.json,
30
- * 表现是「登录显示成功,但界面说无法列出账号」。
31
- */
32
- export function authOf(acct) {
33
- const { accountId, uid } = resolveAccount({
34
- name: acct.nickname ?? "",
35
- phone: "",
36
- urn: acct.openid,
37
- })
38
- return {
39
- type: "success",
40
- access: acct.accessToken,
41
- // token 30 天有效,refresh 会成对轮换 —— 这里存自己,refresh 钩子负责换新
42
- refresh: acct.refreshToken ?? acct.accessToken,
43
- expires: acct.expiresAt ?? 0,
44
- ...(accountId ? { accountId } : {}),
45
- ...(uid ? { uid } : {}),
46
- // 落盘的额外字段:模型请求随时要用
47
- metadata: {
48
- openid: acct.openid,
49
- guid: acct.guid ?? deviceGuid(),
50
- loginType: acct.loginType ?? "6",
51
- nickname: acct.nickname ?? "",
52
- },
53
- }
54
- }
55
-
56
- /** 授权页登录(QQ / 微信),走隔离浏览器 */
57
- function oauthLogin(loginType, label, instructions) {
58
- return {
59
- type: "oauth",
60
- label,
61
- async authorize() {
62
- const guid = deviceGuid()
63
- // ⚠ 不要在这里 `findBrowser()` —— 它要查 15 次注册表(实测 1.4 秒),
64
- // 而 `authorize()` 必须**立刻**返回(HOOKS 四·陷阱 1)。
65
- // 浏览器查找挪进 `callback`,那时才真正需要它。
66
- const url = authorizeUrl(loginType, guid)
67
-
68
- return {
69
- url,
70
- // 我们自己开隔离浏览器,magpie 不要再开系统默认浏览器
71
- opened: true,
72
- method: "auto",
73
- instructions,
74
- async callback(_url, _input, signal) {
75
- const browser = findBrowser()
76
- if (!browser) {
77
- throw new Error(
78
- "没有找到可用的 Chromium 内核浏览器(Chrome / Edge / 360 / QQ 浏览器都行)。" +
79
- "请装一个,或改用「从本机 Marvis 导入」。",
80
- )
81
- }
82
- // 起隔离浏览器 → 等它跳到回调地址
83
- // ⚠ 回调页**本身就是换 token 的响应**(服务端直接吐 JSON),
84
- // 所以这里读页面正文,**不要再发一次请求** —— 再发就是第二个 code,必然 -109。
85
- const { url: callbackUrl, body } = await openIsolatedLogin({
86
- browser,
87
- url,
88
- match: CALLBACK,
89
- timeoutMs: 5 * 60_000,
90
- signal,
91
- })
92
-
93
- // 拦截时 body 为空 -> parseCallback 自己拿 code + login_type 去换
94
- const acct = await parseCallback(callbackUrl, body, { loginType })
95
- return authOf({ ...acct, guid, loginType: acct.loginType ?? LOGIN_TYPE[loginType] ?? "6" })
96
- },
97
- }
98
- },
99
- }
100
- }
101
-
102
- export function methods() {
103
- return [
104
- oauthLogin(
105
- "QC",
106
- "QQ 登录",
107
- "1) 会打开一个**独立的**浏览器窗口(不碰你日常浏览器)\n" +
108
- "2) 用你的 QQ 登录并确认授权\n" +
109
- "3) 授权完成后窗口会自己关掉,回到 magpie 即可",
110
- ),
111
- oauthLogin(
112
- "WX",
113
- "微信登录",
114
- "1) 会打开一个**独立的**浏览器窗口(不碰你日常浏览器)\n" +
115
- "2) 用微信扫码并确认\n" +
116
- "3) 授权完成后窗口会自己关掉,回到 magpie 即可",
117
- ),
118
- {
119
- type: "oauth",
120
- label: "从本机 Marvis 导入(需已安装并登录)",
121
- async authorize() {
122
- const ok = await desktopAvailable()
123
- return {
124
- // ⚠ 必须 opened: true + method: "auto",否则 magpie 会去开这个 url
125
- // (之前给 about:blank 弹了一次「没有可打开此链接的应用」)
126
- url: "",
127
- opened: true,
128
- method: "auto",
129
- instructions: ok
130
- ? "已检测到本机 Marvis 客户端,正在读取登录态…"
131
- : "本机没找到运行中的 Marvis 客户端。请先打开 Marvis 并登录,或改用 QQ / 微信 登录。",
132
- async callback() {
133
- const acct = await importFromDesktop()
134
- return authOf(acct)
135
- },
136
- }
137
- },
138
- },
139
- ]
140
- }
141
-
142
- export { PROVIDER, CALLBACK }
1
+ // ② auth.methods —— 两条登录路
2
+ //
3
+ // A. QQ / 微信 授权:自己起一个**隔离浏览器**(一次性 profile、伪装客户端 UA、
4
+ // 抹掉 navigator.webdriver),用户在窗口里授权,插件从 CDP 抓回调响应。
5
+ // → `{ url, opened: true, method: "auto", callback }`
6
+ // `opened: true` 让 magpie **不要**再开系统默认浏览器(否则弹两个窗口)。
7
+ //
8
+ // B. 从本机 Marvis 导入:装了客户端且登录过的人,直接读进程内存里的登录态。
9
+ // → 同样 `opened: true` + `method: "auto"`,整个登录就是「点一下 → 读内存 → 成功」。
10
+ //
11
+ // ⚠ `authorize()` 必须**立刻**返回 —— 真正等用户的是 `callback()`。
12
+ // 在这里 await 登录会和 magpie 双向死锁(HOOKS §四·陷阱1)。
13
+ // `findBrowser()` 要查十几次注册表(实测 1.4 秒),必须放在 callback 里。
14
+ //
15
+ // ⚠ 已知问题:浏览器扫码这条路的换取服务端一直回 `-109 RC_PARAMS_INVALID`,
16
+ // 参数已逐字对齐官方客户端(redirect_uri / login_type / state / UA / Cookie / Referer)
17
+ // 仍然被拒 —— 判断是服务端对该入口有额外校验,插件侧无法绕过。
18
+ // 失败时给出可执行的替代路径,不让用户对着 109 干瞪眼。
19
+
20
+ import { PROVIDER, LOGIN_TYPE } from "../context.mjs"
21
+ import { authorizeUrl, parseCallback, deviceGuid } from "../marvis/login.mjs"
22
+ import { openIsolatedLogin, findBrowser } from "../marvis/browser.mjs"
23
+ import { resolveAccount } from "../account-name.mjs"
24
+ import { importFromDesktop, desktopAvailable } from "../marvis/desktop.mjs"
25
+
26
+ /** 回调地址判据:跳到 marvis_oauth 且带 code= */
27
+ const CALLBACK = /marvis_client_login\/marvis_oauth\?[^#]*\bcode=/i
28
+
29
+ /** 服务端拒绝浏览器登录时的提示 */
30
+ const BROWSER_LOGIN_HINT =
31
+ " [提示] 浏览器登录路服务端目前不接受(换取参数已逐字对齐官方客户端仍被拒)。" +
32
+ "请改用「从本机 Marvis 导入」——装了 Marvis 客户端并在里面登录过就能一键导入。"
33
+
34
+ /**
35
+ * 登录成功时给宿主的字段。
36
+ * `accountId` 是**名字**(卡片上显示),`uid` 是身份(HOOKS §八)。
37
+ * ⚠ `refresh` 必须有 —— 缺了它 magpie 不会把账号写进 plugin-auth.json,
38
+ * 表现是「登录显示成功,但界面说无法列出账号」。
39
+ */
40
+ export function authOf(acct) {
41
+ const { accountId, uid } = resolveAccount({
42
+ name: acct.nickname ?? "",
43
+ phone: "",
44
+ urn: acct.openid,
45
+ })
46
+ return {
47
+ type: "success",
48
+ access: acct.accessToken,
49
+ // token 30 天有效,refresh 会成对轮换 —— 这里存自己,refresh 钩子负责换新
50
+ refresh: acct.refreshToken ?? acct.accessToken,
51
+ expires: acct.expiresAt ?? 0,
52
+ ...(accountId ? { accountId } : {}),
53
+ ...(uid ? { uid } : {}),
54
+ // 落盘的额外字段:模型请求随时要用
55
+ metadata: {
56
+ openid: acct.openid,
57
+ guid: acct.guid ?? deviceGuid(),
58
+ loginType: acct.loginType ?? "1",
59
+ nickname: acct.nickname ?? "",
60
+ },
61
+ }
62
+ }
63
+
64
+ /** 授权页登录(QQ / 微信),走隔离浏览器 */
65
+ function oauthLogin(loginType, label, instructions) {
66
+ return {
67
+ type: "oauth",
68
+ label,
69
+ async authorize() {
70
+ const guid = deviceGuid()
71
+ // ⚠ 不要在这里 findBrowser() —— 它要查 15 次注册表(实测 1.4 秒),
72
+ // 而 authorize() 必须**立刻**返回(HOOKS 四·陷阱 1)。
73
+ const url = authorizeUrl(loginType, guid)
74
+
75
+ return {
76
+ url,
77
+ // 我们自己开隔离浏览器,magpie 不要再开系统默认浏览器
78
+ opened: true,
79
+ method: "auto",
80
+ instructions,
81
+ async callback(_url, _input, signal) {
82
+ const browser = findBrowser()
83
+ if (!browser) {
84
+ throw new Error(
85
+ "没有找到可用的 Chromium 内核浏览器(Chrome / Edge / 360 / QQ 浏览器都行)。" +
86
+ "请装一个,或改用「从本机 Marvis 导入」。",
87
+ )
88
+ }
89
+
90
+ // 起隔离浏览器 → 等它跳到回调地址,读第一个响应体
91
+ const { url: callbackUrl, body } = await openIsolatedLogin({
92
+ browser,
93
+ url,
94
+ match: CALLBACK,
95
+ timeoutMs: 5 * 60_000,
96
+ signal,
97
+ })
98
+
99
+ /** @type {any} */
100
+ let acct
101
+ try {
102
+ acct = await parseCallback(callbackUrl, body, { loginType })
103
+ } catch (err) {
104
+ const msg = String(err?.message ?? err)
105
+ throw new Error(/-109|RC_PARAMS_INVALID/.test(msg) ? msg + BROWSER_LOGIN_HINT : msg)
106
+ }
107
+ return authOf({ ...acct, guid, loginType: acct.loginType ?? LOGIN_TYPE[loginType] ?? "1" })
108
+ },
109
+ }
110
+ },
111
+ }
112
+ }
113
+
114
+ export function methods() {
115
+ return [
116
+ oauthLogin(
117
+ "QC",
118
+ "QQ 登录",
119
+ "1) 会打开一个**独立的**浏览器窗口(不碰你日常浏览器)\n" +
120
+ "2) 用你的 QQ 登录并确认授权\n" +
121
+ "3) 授权完成后窗口会自己关掉,回到 magpie 即可",
122
+ ),
123
+ oauthLogin(
124
+ "WX",
125
+ "微信登录",
126
+ "1) 会打开一个**独立的**浏览器窗口(不碰你日常浏览器)\n" +
127
+ "2) 用微信扫码并确认\n" +
128
+ "3) 授权完成后窗口会自己关掉,回到 magpie 即可",
129
+ ),
130
+ {
131
+ type: "oauth",
132
+ label: "从本机 Marvis 导入(需已安装并登录)",
133
+ async authorize() {
134
+ const ok = await desktopAvailable()
135
+ return {
136
+ // ⚠ 必须 opened: true + method: "auto",否则 magpie 会去开这个 url
137
+ // (之前给 about:blank 弹了一次「没有可打开此链接的应用」)
138
+ url: "",
139
+ opened: true,
140
+ method: "auto",
141
+ instructions: ok
142
+ ? "已检测到本机 Marvis 客户端,正在读取登录态…"
143
+ : "本机没找到运行中的 Marvis 客户端。请先打开 Marvis 并登录,或改用 QQ / 微信 登录。",
144
+ async callback() {
145
+ const acct = await importFromDesktop()
146
+ return authOf(acct)
147
+ },
148
+ }
149
+ },
150
+ },
151
+ ]
152
+ }
153
+
154
+ export { PROVIDER, CALLBACK }
@@ -45,10 +45,16 @@ const WIN_EXES = [
45
45
  "msedge.exe",
46
46
  ]
47
47
 
48
- /** 正常 Chrome 的 UA —— 不是 headless、不是 Electron、不带 Marvis 字样 */
48
+ /**
49
+ * 浏览器 UA —— **必须是 Marvis 客户端自己的 Electron UA**。
50
+ *
51
+ * 一开始我用普通 Chrome UA「伪装身份」,结果服务端一律 `-109 RC_PARAMS_INVALID`。
52
+ * 客户端 BrowserWindow 的真实 UA 是带 `Marvis/<version>` 和 `Electron/<ver>` 的 ——
53
+ * 服务端按它识别调用方,缺了就判参数无效。
54
+ */
49
55
  const CHROME_UA =
50
56
  "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) " +
51
- "Chrome/152.0.0.0 Safari/537.36"
57
+ "Marvis/1.60.3300.408 Chrome/152.0.7977.130 Electron/44.7.0 Safari/537.36"
52
58
 
53
59
  /** Windows 注册表 App Paths 查浏览器;查不到返回空串。 */
54
60
  export function registryAppPath(exe) {
@@ -219,8 +225,9 @@ export async function openIsolatedLogin({ browser, url, match, extraHeaders, tim
219
225
  "--disable-extensions",
220
226
  "--disable-background-networking",
221
227
  "--disable-sync",
222
- // 去掉自动化痕迹(这是防风控的关键,不是可选项)
223
- "--disable-blink-features=AutomationControlled",
228
+ // 去掉自动化痕迹。⚠ 不用 `--disable-blink-features=AutomationControlled` ——
229
+ // 它会在地址栏下面挂一条「你使用的是不受支持的命令行标记」警告条,很扎眼。
230
+ // `navigator.webdriver` 改用 CDP 的 `Page.addScriptToEvaluateOnNewDocument` 抹掉。
224
231
  "--exclude-switches=enable-automation",
225
232
  `--user-agent=${CHROME_UA}`,
226
233
  "--lang=zh-CN",