@glittering0717/ai-browser 2.1.2 → 2.1.3
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/package.json +1 -1
- package/scripts/release.cjs +45 -9
- package/src/main/page_manager.js +50 -3
- package/src/renderer/loading.html +75 -0
- package/src/shared/version.js +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glittering0717/ai-browser",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.3",
|
|
4
4
|
"description": "AI-native browser — renders pixels for humans, structured semantic-tree API for agents. No screenshots, no OCR, no coordinate guessing.",
|
|
5
5
|
"main": "src/main/index.js",
|
|
6
6
|
"bin": {
|
package/scripts/release.cjs
CHANGED
|
@@ -48,6 +48,14 @@ function run(cmd, argv, opts = {}) {
|
|
|
48
48
|
function git(...argv) { return run('git', argv); }
|
|
49
49
|
function gitOut(...argv) { return run('git', argv).stdout.trim(); }
|
|
50
50
|
|
|
51
|
+
// registry 的 github 登录可以吃环境变量,从而绕开 device flow ——
|
|
52
|
+
// device flow 在无 TTY / 后台执行时会静默卡在 "Logging in..." 不打印 URL,
|
|
53
|
+
// 看起来像卡死,实际是在等一个永远不会被打开的浏览器。
|
|
54
|
+
function ghToken() {
|
|
55
|
+
try { return execFileSync('gh', ['auth', 'token'], { encoding: 'utf8' }).trim(); }
|
|
56
|
+
catch { return ''; }
|
|
57
|
+
}
|
|
58
|
+
|
|
51
59
|
const done = [];
|
|
52
60
|
function mark(m) { done.push(m); }
|
|
53
61
|
|
|
@@ -160,8 +168,15 @@ if (npm.status === 0) {
|
|
|
160
168
|
mark('npm publish');
|
|
161
169
|
} else {
|
|
162
170
|
const out = `${npm.stdout || ''}${npm.stderr || ''}`;
|
|
163
|
-
//
|
|
164
|
-
|
|
171
|
+
// 三层认证失败的表现各不相同,逐条给可执行的下一步(2026-10-23 一天踩完):
|
|
172
|
+
// 401 / E404 on PUT → token 过期或被撤销(npm 用 404 掩盖"没权限")
|
|
173
|
+
// 403 + "bypass 2fa" → token 有效但建的时候没勾 Bypass 2FA
|
|
174
|
+
// EPUBLISHCONFLICT → 版本号已被占用
|
|
175
|
+
if (/bypass 2fa enabled is required/i.test(out)) {
|
|
176
|
+
fail('npm token 没有开 Bypass 2FA —— 账号开了 2FA 时,granular token 必须勾这个开关才能发布');
|
|
177
|
+
console.log(' https://www.npmjs.com/settings/glittering0717/tokens/new');
|
|
178
|
+
console.log(' Permissions: Read and write | Scope: @glittering0717 | 勾 Bypass 2FA');
|
|
179
|
+
} else if (/E401|E404|Unauthorized|Not found/i.test(out)) {
|
|
165
180
|
fail('npm 认证失效(401/404)。需要重新登录一次,之后本脚本可全自动:');
|
|
166
181
|
console.log(' npm login # 浏览器登录');
|
|
167
182
|
console.log(' 或:把新的 granular access token 写进 ~/.npmrc 的 //registry.npmjs.org/:_authToken');
|
|
@@ -181,17 +196,38 @@ if (!publisher) {
|
|
|
181
196
|
warn('未找到 mcp-publisher,跳过。安装:gh release download --repo modelcontextprotocol/registry \\');
|
|
182
197
|
console.log(' --pattern "mcp-publisher_darwin_arm64.tar.gz" && tar xzf ...');
|
|
183
198
|
} else {
|
|
184
|
-
|
|
185
|
-
|
|
199
|
+
// registry 的登录态是一枚短期 JWT(几小时就过期)。过期表现是 401
|
|
200
|
+
// "token has invalid claims: token is expired" —— 不是"没登录",
|
|
201
|
+
// 所以 `login` 会直接成功但不会有人去重跑它。这里自动续一次:
|
|
202
|
+
// MCP_GITHUB_TOKEN 让 login 走非交互路径(device flow 在无 TTY 时会静默卡住)。
|
|
203
|
+
const doPublish = () => spawnSync(publisher, ['publish'], { cwd: ROOT, encoding: 'utf8' });
|
|
204
|
+
let pub = doPublish();
|
|
205
|
+
let pubOut = `${pub.stdout || ''}${pub.stderr || ''}`;
|
|
206
|
+
if (pub.status !== 0 && /expired|invalid or expired|status 401/i.test(pubOut)) {
|
|
207
|
+
warn('registry 登录已过期,自动续期一次…');
|
|
208
|
+
const relogin = spawnSync(publisher, ['login', 'github'], {
|
|
209
|
+
cwd: ROOT,
|
|
210
|
+
encoding: 'utf8',
|
|
211
|
+
env: { ...process.env, MCP_GITHUB_TOKEN: ghToken() },
|
|
212
|
+
});
|
|
213
|
+
const reloginOut = `${relogin.stdout || ''}${relogin.stderr || ''}`;
|
|
214
|
+
if (relogin.status === 0 && /logged in/i.test(reloginOut)) {
|
|
215
|
+
ok('已重新登录');
|
|
216
|
+
pub = doPublish();
|
|
217
|
+
pubOut = `${pub.stdout || ''}${pub.stderr || ''}`;
|
|
218
|
+
} else {
|
|
219
|
+
warn('自动续期失败(需要 gh CLI 已登录):' + reloginOut.trim().split('\n').pop());
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
if (pub.status === 0 && /successfully published/i.test(pubOut)) {
|
|
186
223
|
ok('MCP registry 已发布');
|
|
187
224
|
mark('mcp registry');
|
|
188
225
|
} else {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
warn('需要先登录一次:mcp-publisher login github(device flow,点一下浏览器)');
|
|
226
|
+
fail('MCP registry 发布失败:' + pubOut.split('\n').filter(Boolean).slice(-6).join(' | '));
|
|
227
|
+
if (/was not found \(status: 404\)/i.test(pubOut)) {
|
|
228
|
+
warn('registry 要求 npm 上先有该版本 —— 先把 npm 那步修好,再重跑本脚本');
|
|
193
229
|
}
|
|
194
|
-
warn('
|
|
230
|
+
warn('补救:mcp-publisher publish');
|
|
195
231
|
}
|
|
196
232
|
}
|
|
197
233
|
|
package/src/main/page_manager.js
CHANGED
|
@@ -191,6 +191,11 @@ class PageManager {
|
|
|
191
191
|
// 垫片登记完再导航,否则真实文档先到、垫片只对"下一次文档"生效,那个 tab
|
|
192
192
|
// 就成了没有垫片的窗中窗(见 newTab 末尾长注释)。
|
|
193
193
|
this._windowOpenShimReady = new Map();
|
|
194
|
+
// tabId -> 'pending' | 'loading' | 'ready' | 'blank':这个 tab 现在处于导航的
|
|
195
|
+
// 哪一步。存在的理由:新 tab 在占位页(原先是纯白 about:blank)停留时,
|
|
196
|
+
// 光看 URL 分不清"正在加载"和"卡住了/开错了"。listTabs 把它报出去,
|
|
197
|
+
// agent 才知道该等而不是该重试。
|
|
198
|
+
this._tabNavState = new Map();
|
|
194
199
|
this._axDiffCache = new Map(); // tabId -> lite interactive snapshot (get_tree {mode:'diff'})
|
|
195
200
|
this._repaintTimers = new Map();// tabId -> 正在排队/已调度的重绘(合并同一批导航事件)
|
|
196
201
|
this._snapshots = new Map(); // snapshotId -> { tabId, js, value, at }(ui.snapshot 存基线)
|
|
@@ -3271,6 +3276,8 @@ return {ok:true,name:file.name,size:file.size,via:via,host_sel:hostSel,clicked:c
|
|
|
3271
3276
|
const wc0 = view.webContents;
|
|
3272
3277
|
const targetUrl = url || null;
|
|
3273
3278
|
if (wc0 && !wc0.isDestroyed()) {
|
|
3279
|
+
// 还没发起真实导航:停在占位页等垫片注入。
|
|
3280
|
+
this._tabNavState.set(tabId, 'pending');
|
|
3274
3281
|
// 垫片就绪信号:先挂上去,再去加载 about:blank。顺序不能反 ——
|
|
3275
3282
|
// ai:window_open_nav 可能在这一行之后立刻到达(IPC 保序,但它不等我们)。
|
|
3276
3283
|
let signalReady = () => {};
|
|
@@ -3281,19 +3288,38 @@ return {ok:true,name:file.name,size:file.size,via:via,host_sel:hostSel,clicked:c
|
|
|
3281
3288
|
if (proceeded) return;
|
|
3282
3289
|
proceeded = true;
|
|
3283
3290
|
Promise.resolve(this._ensureWindowOpenShim(tabId, view)).finally(() => {
|
|
3284
|
-
// 失败也放行:宁可这个 tab
|
|
3291
|
+
// 失败也放行:宁可这个 tab 没有垫片,也不能把它卡在占位页。
|
|
3285
3292
|
signalReady();
|
|
3286
3293
|
if (targetUrl && wc0 && !wc0.isDestroyed()) {
|
|
3294
|
+
// 让"正在加载"可被观测:白屏最贵的地方不是慢,是慢得无法和
|
|
3295
|
+
// "开错了"区分(见 listTabs 的 state 字段)。
|
|
3296
|
+
this._tabNavState.set(tabId, 'loading');
|
|
3297
|
+
wc0.once('did-finish-load', () => {
|
|
3298
|
+
if (this._tabNavState.get(tabId) === 'loading') this._tabNavState.set(tabId, 'ready');
|
|
3299
|
+
});
|
|
3287
3300
|
wc0.loadURL(targetUrl).catch(() => {});
|
|
3301
|
+
} else {
|
|
3302
|
+
// 无目标 URL 的占位 tab(window.open("") 的老站模式):它本来就
|
|
3303
|
+
// 要停在这里等站点赋 location,不算"卡住",但也不是就绪。
|
|
3304
|
+
this._tabNavState.set(tabId, 'blank');
|
|
3288
3305
|
}
|
|
3289
3306
|
});
|
|
3290
3307
|
};
|
|
3291
|
-
//
|
|
3308
|
+
// 兜底:占位页的 did-finish-load 万一来不了(renderer 异常、
|
|
3292
3309
|
// 视图尚未 attach 等),也不能把真实导航永久挂住——1.2s 后照常放行,
|
|
3293
3310
|
// 最坏退化成"首屏没有垫片"(与修复前一致),而不是打不开页面。
|
|
3294
3311
|
const guard = setTimeout(proceed, 1200);
|
|
3295
3312
|
if (guard.unref) guard.unref();
|
|
3296
|
-
|
|
3313
|
+
// ⚠️ 占位页用 loading.html 而不是 about:blank(2026-10-24 修)。
|
|
3314
|
+
//
|
|
3315
|
+
// 这段等待原本是纯白的 about:blank。快站约 4s,慢站实测 70s(GitHub
|
|
3316
|
+
// OAuth 直到 ERR_CONNECTION_TIMED_OUT 才切到错误页)—— 用户和 agent
|
|
3317
|
+
// 看到的都是"弹出来一个空白页面",和"垫片没把导航发出去"那个老 bug
|
|
3318
|
+
// (超星"进入学习")在现象上完全一样,无法区分。
|
|
3319
|
+
// loading.html 只做展示(目标地址 + 转圈),不发请求不跳转,真实导航
|
|
3320
|
+
// 仍由上面的 proceed 发起。无目标 URL 的占位 tab(window.open("") 的
|
|
3321
|
+
// 老站模式)保持 about:blank:它本来就要停在这里等 location 赋值。
|
|
3322
|
+
wc0.loadURL(this._loadingPlaceholderUrl(targetUrl)).catch(() => {});
|
|
3297
3323
|
wc0.once('did-finish-load', () => { clearTimeout(guard); proceed(); });
|
|
3298
3324
|
}
|
|
3299
3325
|
|
|
@@ -3524,6 +3550,7 @@ return {ok:true,name:file.name,size:file.size,via:via,host_sel:hostSel,clicked:c
|
|
|
3524
3550
|
}
|
|
3525
3551
|
this._windowOpenShimTabs.delete(tabId);
|
|
3526
3552
|
this._windowOpenShimReady.delete(tabId);
|
|
3553
|
+
this._tabNavState.delete(tabId);
|
|
3527
3554
|
// Release per-tab network resources so a closed tab can never leak its
|
|
3528
3555
|
// debugger attachment or cached request entries / stale monitors.
|
|
3529
3556
|
this._cleanupTabResources(tabId);
|
|
@@ -3579,6 +3606,12 @@ return {ok:true,name:file.name,size:file.size,via:via,host_sel:hostSel,clicked:c
|
|
|
3579
3606
|
url: view.webContents.getURL(),
|
|
3580
3607
|
title: view.webContents.getTitle(),
|
|
3581
3608
|
active: id === this.activeTab,
|
|
3609
|
+
// 'pending' = 停在占位页等脚本注入(真实导航还没发起)
|
|
3610
|
+
// 'loading' = 真实导航已发起、还没完成 —— 这时候页面多半还是空的,
|
|
3611
|
+
// 是"正在打开"而不是"开错了",等它,不要重试
|
|
3612
|
+
// 'blank' = window.open("") 的占位 tab,在等站点赋 location
|
|
3613
|
+
// 'ready' = 已就绪(没有记录时也按就绪处理)
|
|
3614
|
+
state: this._tabNavState.get(id) || 'ready',
|
|
3582
3615
|
});
|
|
3583
3616
|
}
|
|
3584
3617
|
return list;
|
|
@@ -3968,6 +4001,20 @@ return {ok:true,name:file.name,size:file.size,via:via,host_sel:hostSel,clicked:c
|
|
|
3968
4001
|
// load about:blank,did-finish-load 后再调这里。
|
|
3969
4002
|
// 幂等:每个 tab 只注册一次(_windowOpenShimTabs 记账);跨导航自动保留
|
|
3970
4003
|
// 正是 addScriptToEvaluateOnNewDocument 的语义,不需要重复注册。
|
|
4004
|
+
// 新 tab 的占位页 URL:有目标地址时用 loading.html(把白屏变成可诊断的
|
|
4005
|
+
// "正在打开 <url>"),无目标地址时保持 about:blank(window.open("") 的
|
|
4006
|
+
// 占位 tab 要停在这里等站点赋 location,不能显示成"正在打开")。
|
|
4007
|
+
_loadingPlaceholderUrl(targetUrl) {
|
|
4008
|
+
if (!targetUrl) return 'about:blank';
|
|
4009
|
+
try {
|
|
4010
|
+
const pagePath = path.join(__dirname, '../renderer/loading.html');
|
|
4011
|
+
const q = new URLSearchParams({ url: String(targetUrl) });
|
|
4012
|
+
return 'file://' + pagePath + '?' + q.toString();
|
|
4013
|
+
} catch (_e) {
|
|
4014
|
+
return 'about:blank';
|
|
4015
|
+
}
|
|
4016
|
+
}
|
|
4017
|
+
|
|
3971
4018
|
_ensureWindowOpenShim(tabId, view) {
|
|
3972
4019
|
if (this._windowOpenShimTabs.has(tabId)) return Promise.resolve(true);
|
|
3973
4020
|
this._windowOpenShimTabs.add(tabId);
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="zh-CN">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<title>正在打开…</title>
|
|
6
|
+
<!--
|
|
7
|
+
loading.html — 新标签页的加载占位页。
|
|
8
|
+
|
|
9
|
+
为什么需要它:新建 tab 时必须先 load 一个真实文档才能注入 window.open 垫片
|
|
10
|
+
(CDP 命令在未导航的空 webContents 上无人处理会挂住),注入完成后才导航到
|
|
11
|
+
真实 URL。这段间隔原本是 `about:blank` —— 也就是一片纯白。网络慢时它能持续
|
|
12
|
+
几十秒(实测 GitHub OAuth 70s 才 ERR_CONNECTION_TIMED_OUT),用户和 agent
|
|
13
|
+
看到的都是"弹出来一个空白页面",无法区分"正在加载"和"开错了"。
|
|
14
|
+
|
|
15
|
+
这个页面本身不发请求、不跑脚本,只把"正在打开哪个地址"显示出来,把白屏
|
|
16
|
+
变成一个可诊断的状态。它是纯展示:真实导航由主进程在垫片注入完成后发起。
|
|
17
|
+
-->
|
|
18
|
+
<style>
|
|
19
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
20
|
+
html, body { height: 100%; }
|
|
21
|
+
body {
|
|
22
|
+
background: #1e1e1e;
|
|
23
|
+
color: #d4d4d4;
|
|
24
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', sans-serif;
|
|
25
|
+
font-size: 13px;
|
|
26
|
+
display: flex;
|
|
27
|
+
align-items: center;
|
|
28
|
+
justify-content: center;
|
|
29
|
+
padding: 24px;
|
|
30
|
+
}
|
|
31
|
+
.box { max-width: 560px; width: 100%; }
|
|
32
|
+
.row { display: flex; align-items: center; gap: 10px; margin-bottom: 14px; }
|
|
33
|
+
.spinner {
|
|
34
|
+
width: 14px; height: 14px; flex: none;
|
|
35
|
+
border: 2px solid #3c3c3c;
|
|
36
|
+
border-top-color: #569cd6;
|
|
37
|
+
border-radius: 50%;
|
|
38
|
+
animation: spin 0.8s linear infinite;
|
|
39
|
+
}
|
|
40
|
+
@keyframes spin { to { transform: rotate(360deg); } }
|
|
41
|
+
h1 { font-size: 14px; font-weight: 600; color: #d4d4d4; }
|
|
42
|
+
.url {
|
|
43
|
+
background: #252526; border: 1px solid #3c3c3c; border-radius: 4px;
|
|
44
|
+
padding: 8px 10px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
45
|
+
font-size: 12px; color: #9cdcfd; word-break: break-all; margin-bottom: 14px;
|
|
46
|
+
min-height: 34px;
|
|
47
|
+
}
|
|
48
|
+
.hint { color: #858585; font-size: 12px; line-height: 1.7; }
|
|
49
|
+
</style>
|
|
50
|
+
</head>
|
|
51
|
+
<body>
|
|
52
|
+
<div class="box">
|
|
53
|
+
<div class="row">
|
|
54
|
+
<div class="spinner"></div>
|
|
55
|
+
<h1>正在打开…</h1>
|
|
56
|
+
</div>
|
|
57
|
+
<div class="url" id="u">about:blank</div>
|
|
58
|
+
<div class="hint">
|
|
59
|
+
新标签页需要先准备好脚本注入环境,再跳转目标地址。<br>
|
|
60
|
+
长时间停在这里通常意味着目标地址连接超时(常见于被墙或缓慢的第三方登录)。
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
<script>
|
|
64
|
+
// 只做展示:把目标地址从 query 里取出来显示。
|
|
65
|
+
// 不发起任何请求、不做跳转 —— 真实导航由主进程发起,避免和它抢。
|
|
66
|
+
(function () {
|
|
67
|
+
try {
|
|
68
|
+
var q = new URLSearchParams(location.search);
|
|
69
|
+
var u = q.get('url') || '';
|
|
70
|
+
if (u) document.getElementById('u').textContent = u;
|
|
71
|
+
} catch (e) {}
|
|
72
|
+
})();
|
|
73
|
+
</script>
|
|
74
|
+
</body>
|
|
75
|
+
</html>
|
package/src/shared/version.js
CHANGED
|
@@ -2,4 +2,4 @@
|
|
|
2
2
|
// Kept in JS (not imported from package.json) to avoid Node import-attribute
|
|
3
3
|
// (JSON module) compatibility issues inside Electron. package.json "version"
|
|
4
4
|
// is manually synced to this value; tests assert they match.
|
|
5
|
-
export const VERSION = '2.1.
|
|
5
|
+
export const VERSION = '2.1.3';
|