@glittering0717/ai-browser 2.1.6 → 2.1.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/package.json +3 -2
- package/src/main/page_manager.js +175 -1
- package/src/main/ws_server.js +10 -1
- package/src/shared/dialog_policy.cjs +111 -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.7",
|
|
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": {
|
|
@@ -79,7 +79,8 @@
|
|
|
79
79
|
"coverage": "node tools/coverage_probe.cjs",
|
|
80
80
|
"build": "node scripts/build_dmg.cjs",
|
|
81
81
|
"verify:build": "node scripts/verify_build.cjs",
|
|
82
|
-
"install:app": "node scripts/install_app.cjs"
|
|
82
|
+
"install:app": "node scripts/install_app.cjs",
|
|
83
|
+
"dialog": "node e2e/dialog_ws.cjs"
|
|
83
84
|
},
|
|
84
85
|
"dependencies": {
|
|
85
86
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
package/src/main/page_manager.js
CHANGED
|
@@ -18,6 +18,10 @@ import { withEffectState, isTargetMiss as isTargetMissError } from '../shared/ef
|
|
|
18
18
|
// 前后快照探针:动作返回 success 只证明"命令发出去了",不证明"页面接住了"。
|
|
19
19
|
// 后者必须由掌握渲染进程的一侧给出(见 shared/page_probe.js 顶部事故说明)。
|
|
20
20
|
import pageProbe from '../shared/page_probe.js';
|
|
21
|
+
// 浏览器级对话框(alert / confirm / prompt / beforeunload)的策略。它们是
|
|
22
|
+
// Electron 画的原生 sheet,不在 DOM、不在 AX 树,而且**打开期间渲染器被阻塞**,
|
|
23
|
+
// 所以必须由主进程通过 CDP Page.handleJavaScriptDialog 应答 —— 见该模块头部说明。
|
|
24
|
+
import dialogPolicy from '../shared/dialog_policy.cjs';
|
|
21
25
|
// 默认读法(DOM extractor)的"哪个 editable 才是正文"索引。AX 路径在
|
|
22
26
|
// normalizeAxTree 里已经自带,这条补的是默认路径 —— 见文件内注释。
|
|
23
27
|
import editorsProbe from '../shared/editors_probe.js';
|
|
@@ -196,6 +200,14 @@ class PageManager {
|
|
|
196
200
|
// 光看 URL 分不清"正在加载"和"卡住了/开错了"。listTabs 把它报出去,
|
|
197
201
|
// agent 才知道该等而不是该重试。
|
|
198
202
|
this._tabNavState = new Map();
|
|
203
|
+
// tabId -> 最近一次被处理掉的 JS 对话框:{type, message, accept, promptText,
|
|
204
|
+
// auto, at}。回执里带上它,agent 才知道"刚才那个动作中间弹了个确认框,
|
|
205
|
+
// 我替你按了确定"—— 否则它会以为点击之后什么都没发生。
|
|
206
|
+
this._lastDialog = new Map();
|
|
207
|
+
// 已挂上"对话框监听"的 tab(每个 tab 一次;Page.enable 是幂等的)
|
|
208
|
+
this._dialogWatched = new Set();
|
|
209
|
+
// tabId -> 当前**仍然打开**的对话框(manual 策略下会长期存在)
|
|
210
|
+
this._openDialogs = new Map();
|
|
199
211
|
this._axDiffCache = new Map(); // tabId -> lite interactive snapshot (get_tree {mode:'diff'})
|
|
200
212
|
this._repaintTimers = new Map();// tabId -> 正在排队/已调度的重绘(合并同一批导航事件)
|
|
201
213
|
this._snapshots = new Map(); // snapshotId -> { tabId, js, value, at }(ui.snapshot 存基线)
|
|
@@ -1449,6 +1461,9 @@ class PageManager {
|
|
|
1449
1461
|
async executeAction(action, target, params = {}, tabId) {
|
|
1450
1462
|
const ms = actionDeadlineMs(action, params);
|
|
1451
1463
|
const tid = tabId !== undefined ? tabId : this.activeTab;
|
|
1464
|
+
// 本次动作的起始时刻:用来判断"有没有对话框是在这一次动作里被应答的",
|
|
1465
|
+
// 并把结果写进回执(否则 agent 不知道点击中间弹过确认框)。
|
|
1466
|
+
const startedAt = Date.now();
|
|
1452
1467
|
// 动作前先拍一张。为什么拍在 withDeadline 外面:deadline 只应该盖住动作
|
|
1453
1468
|
// 本身,探针有它自己的 1.5s 上限,混进同一个预算会让"动作超时"和
|
|
1454
1469
|
// "探针慢"两种完全不同的故障共用一个错误码。
|
|
@@ -1468,18 +1483,28 @@ class PageManager {
|
|
|
1468
1483
|
const probeAfter = await this._pageProbe(tid, action);
|
|
1469
1484
|
out = pageProbe.applyProbe(out, probeBefore, probeAfter, action);
|
|
1470
1485
|
}
|
|
1486
|
+
const dlg = this._dialogSince(tid, startedAt);
|
|
1487
|
+
if (dlg) out = { ...out, dialog: dlg };
|
|
1471
1488
|
return out;
|
|
1472
1489
|
} catch (e) {
|
|
1473
1490
|
if (e && e.__deadline) {
|
|
1491
|
+
// 超时的第一种解释是"主线程忙",但更常见也更致命的是**有对话框把渲染器
|
|
1492
|
+
// 冻住了**(alert/confirm 是原生 sheet,agent 看不见、也点不到)。
|
|
1493
|
+
// 这里先探测并按策略解开,再回报;不解开的话这条 tab 之后每条命令都会
|
|
1494
|
+
// 以同样的方式超时,agent 只会一路重试到放弃。
|
|
1495
|
+
const dlg = await this.recoverBlockedTab(tid).catch(() => null);
|
|
1474
1496
|
// 明确告诉 agent 两件事:① 它该重试/换策略;② 这个动作**可能已经部分生效**
|
|
1475
1497
|
// (超时时指针可能已经按下、字符可能已经输入了一半)—— 后者必须说,否则
|
|
1476
1498
|
// agent 会以为页面没被动过而直接重试,造成双倍输入。
|
|
1477
1499
|
return this._annotateActResult({
|
|
1478
1500
|
success: false,
|
|
1479
|
-
error:
|
|
1501
|
+
error: dlg
|
|
1502
|
+
? `blocked by a browser-level JS dialog (${dlg.type}) — it is a native sheet, not page DOM, and the page is frozen while it is open. It has been ${dlg.accept ? 'accepted' : 'dismissed'} automatically; re-read the tree before retrying.`
|
|
1503
|
+
: `${e.message} — the page's main thread is likely blocked; the action may have partially applied`,
|
|
1480
1504
|
timed_out: true,
|
|
1481
1505
|
deadline_ms: ms,
|
|
1482
1506
|
action,
|
|
1507
|
+
...(dlg ? { dialog: dlg, retry: 'verify_first' } : {}),
|
|
1483
1508
|
}, action, target, tid);
|
|
1484
1509
|
}
|
|
1485
1510
|
throw e;
|
|
@@ -3239,6 +3264,14 @@ return {ok:true,name:file.name,size:file.size,via:via,host_sel:hostSel,clicked:c
|
|
|
3239
3264
|
// Mask automation fingerprint: remove Electron from UA
|
|
3240
3265
|
view.webContents.setUserAgent(config.userAgent);
|
|
3241
3266
|
|
|
3267
|
+
// beforeunload:Electron 侧的等价物。**不处理它会静默拦住导航** —— 既没有
|
|
3268
|
+
// 对话框、也没有报错,表现成"点了链接/调了 navigate,页面就是不动"。
|
|
3269
|
+
// 策略和 CDP 那条路共用一套(shared/dialog_policy.cjs):默认放行。
|
|
3270
|
+
view.webContents.on('will-prevent-unload', (e) => {
|
|
3271
|
+
const policy = this.dialogPolicy || dialogPolicy.policyFromEnv();
|
|
3272
|
+
if (dialogPolicy.allowUnload({ policy })) e.preventDefault();
|
|
3273
|
+
});
|
|
3274
|
+
|
|
3242
3275
|
// Intercept window.open / new-window → create new tab instead
|
|
3243
3276
|
view.webContents.setWindowOpenHandler(({ url: urlToOpen }) => {
|
|
3244
3277
|
const childId = self.newTab(urlToOpen);
|
|
@@ -3551,6 +3584,10 @@ return {ok:true,name:file.name,size:file.size,via:via,host_sel:hostSel,clicked:c
|
|
|
3551
3584
|
this._windowOpenShimTabs.delete(tabId);
|
|
3552
3585
|
this._windowOpenShimReady.delete(tabId);
|
|
3553
3586
|
this._tabNavState.delete(tabId);
|
|
3587
|
+
// 对话框状态跟着 tab 走:留着会让下次同 id 的 tab 继承一个不存在的对话框。
|
|
3588
|
+
this._lastDialog.delete(tabId);
|
|
3589
|
+
this._openDialogs.delete(tabId);
|
|
3590
|
+
this._dialogWatched.delete(tabId);
|
|
3554
3591
|
// Release per-tab network resources so a closed tab can never leak its
|
|
3555
3592
|
// debugger attachment or cached request entries / stale monitors.
|
|
3556
3593
|
this._cleanupTabResources(tabId);
|
|
@@ -3612,6 +3649,9 @@ return {ok:true,name:file.name,size:file.size,via:via,host_sel:hostSel,clicked:c
|
|
|
3612
3649
|
// 'blank' = window.open("") 的占位 tab,在等站点赋 location
|
|
3613
3650
|
// 'ready' = 已就绪(没有记录时也按就绪处理)
|
|
3614
3651
|
state: this._tabNavState.get(id) || 'ready',
|
|
3652
|
+
// 只有 manual 策略下才会长期存在。auto 模式下对话框在打开的瞬间就被
|
|
3653
|
+
// 应答了,所以正常情况这个字段不出现 —— 出现即意味着"有人需要点它"。
|
|
3654
|
+
...(this._openDialogs.has(id) ? { dialog: this._openDialogs.get(id) } : {}),
|
|
3615
3655
|
});
|
|
3616
3656
|
}
|
|
3617
3657
|
return list;
|
|
@@ -3973,6 +4013,127 @@ return {ok:true,name:file.name,size:file.size,via:via,host_sel:hostSel,clicked:c
|
|
|
3973
4013
|
}
|
|
3974
4014
|
}
|
|
3975
4015
|
|
|
4016
|
+
// === 浏览器级对话框(alert / confirm / prompt / beforeunload)===
|
|
4017
|
+
//
|
|
4018
|
+
// 这类对话框由 Chromium 交给 embedder 渲染,Electron 画成原生 sheet:不在
|
|
4019
|
+
// DOM、不在 AX 树、点不到;**而且打开期间渲染器被阻塞**(Runtime.evaluate
|
|
4020
|
+
// 都不返回)。所以它必须由主进程应答 —— 页面侧的任何办法(JS 覆盖
|
|
4021
|
+
// window.confirm、语义树里找按钮)都治不了"渲染器已经停住"这件事。
|
|
4022
|
+
//
|
|
4023
|
+
// 应答走 CDP `Page.handleJavaScriptDialog`:该命令由浏览器进程处理,不需要
|
|
4024
|
+
// 渲染器,所以对话框阻塞页面时它照样生效。
|
|
4025
|
+
_ensureDialogWatch(tabId, view) {
|
|
4026
|
+
if (this._dialogWatched.has(tabId)) return;
|
|
4027
|
+
this._dialogWatched.add(tabId);
|
|
4028
|
+
const wc = view && view.webContents;
|
|
4029
|
+
if (!wc || wc.isDestroyed()) { this._dialogWatched.delete(tabId); return; }
|
|
4030
|
+
try {
|
|
4031
|
+
// 只有 Page 域启用后才会收到 javascriptDialogOpening。空文档上这条命令
|
|
4032
|
+
// 可能挂住,所以不 await(挂住也无害:只是这个 tab 收不到事件,动作超时
|
|
4033
|
+
// 时还有 recoverBlockedTab 的兜底探测)。
|
|
4034
|
+
wc.debugger.sendCommand('Page.enable').catch(() => {});
|
|
4035
|
+
} catch (_e) {
|
|
4036
|
+
this._dialogWatched.delete(tabId);
|
|
4037
|
+
}
|
|
4038
|
+
}
|
|
4039
|
+
|
|
4040
|
+
/** 当前策略(实例字段优先,便于测试/诊断;否则读环境变量)。 */
|
|
4041
|
+
_dialogPolicyNow() {
|
|
4042
|
+
return this.dialogPolicy || dialogPolicy.policyFromEnv();
|
|
4043
|
+
}
|
|
4044
|
+
|
|
4045
|
+
async _handleDialogOpening(tabId, params) {
|
|
4046
|
+
const dialog = {
|
|
4047
|
+
type: String((params && params.type) || 'alert'),
|
|
4048
|
+
message: String((params && params.message) || ''),
|
|
4049
|
+
defaultPrompt: params && params.defaultPrompt,
|
|
4050
|
+
at: Date.now(),
|
|
4051
|
+
};
|
|
4052
|
+
const decision = dialogPolicy.decideDialog(dialog, { policy: this._dialogPolicyNow() });
|
|
4053
|
+
if (!decision.auto) {
|
|
4054
|
+
// manual:不替调用方决定,把状态挂出去(listTabs 可见),由人来点。
|
|
4055
|
+
this._openDialogs.set(tabId, { type: dialog.type, message: dialog.message });
|
|
4056
|
+
return;
|
|
4057
|
+
}
|
|
4058
|
+
const handled = await this._answerDialog(tabId, decision.accept, decision.promptText);
|
|
4059
|
+
this._openDialogs.delete(tabId);
|
|
4060
|
+
if (!handled) return;
|
|
4061
|
+
this._lastDialog.set(tabId, {
|
|
4062
|
+
type: dialog.type,
|
|
4063
|
+
message: dialog.message,
|
|
4064
|
+
accept: !!decision.accept,
|
|
4065
|
+
...(decision.accept && decision.promptText ? { prompt_text: decision.promptText } : {}),
|
|
4066
|
+
auto: true,
|
|
4067
|
+
at: dialog.at,
|
|
4068
|
+
});
|
|
4069
|
+
}
|
|
4070
|
+
|
|
4071
|
+
async _answerDialog(tabId, accept, promptText) {
|
|
4072
|
+
const view = this._getView(tabId);
|
|
4073
|
+
const wc = view && view.webContents;
|
|
4074
|
+
if (!wc || wc.isDestroyed()) return false;
|
|
4075
|
+
try {
|
|
4076
|
+
const params = { accept: !!accept };
|
|
4077
|
+
if (accept && typeof promptText === 'string' && promptText) params.promptText = promptText;
|
|
4078
|
+
await withDeadline(
|
|
4079
|
+
wc.debugger.sendCommand('Page.handleJavaScriptDialog', params),
|
|
4080
|
+
2000,
|
|
4081
|
+
'dialog_handle'
|
|
4082
|
+
);
|
|
4083
|
+
return true;
|
|
4084
|
+
} catch (_e) {
|
|
4085
|
+
return false; // "No dialog is showing" / debugger 未 attach:都不是错误
|
|
4086
|
+
}
|
|
4087
|
+
}
|
|
4088
|
+
|
|
4089
|
+
/**
|
|
4090
|
+
* 兜底:动作超时时**探测**是否有对话框把渲染器冻结了,并按策略应答。
|
|
4091
|
+
*
|
|
4092
|
+
* 为什么需要它:事件监听依赖 Page 域已启用。若某个 tab 从未启用(或启用被
|
|
4093
|
+
* 1500ms deadline 掐掉),我们就收不到 javascriptDialogOpening,但仍然必须
|
|
4094
|
+
* 有能力把它解开 —— 否则那条 tab 会永久卡死,只能重启浏览器。
|
|
4095
|
+
* CDP 对"没有对话框"会返回 "No dialog is showing",正好当探测用。
|
|
4096
|
+
*
|
|
4097
|
+
* @returns {null|{type,message,accept,auto,recovered:true}} 解开了就返回信息
|
|
4098
|
+
*/
|
|
4099
|
+
async recoverBlockedTab(tabId) {
|
|
4100
|
+
const tid = tabId !== undefined ? tabId : this.activeTab;
|
|
4101
|
+
const decision = dialogPolicy.decideDialog({ type: 'confirm' }, { policy: this._dialogPolicyNow() });
|
|
4102
|
+
if (!decision.auto) return null; // manual 策略:不擅自替人点
|
|
4103
|
+
const view = this._getView(tid);
|
|
4104
|
+
const wc = view && view.webContents;
|
|
4105
|
+
if (!wc || wc.isDestroyed()) return null;
|
|
4106
|
+
try {
|
|
4107
|
+
await withDeadline(
|
|
4108
|
+
wc.debugger.sendCommand('Page.handleJavaScriptDialog', { accept: decision.accept }),
|
|
4109
|
+
2000,
|
|
4110
|
+
'dialog_probe'
|
|
4111
|
+
);
|
|
4112
|
+
} catch (_e) {
|
|
4113
|
+
return null; // 没有对话框(或 CDP 不可用)
|
|
4114
|
+
}
|
|
4115
|
+
const known = this._openDialogs.get(tid) || {};
|
|
4116
|
+
this._openDialogs.delete(tid);
|
|
4117
|
+
const info = {
|
|
4118
|
+
type: known.type || 'unknown',
|
|
4119
|
+
message: known.message || '',
|
|
4120
|
+
accept: !!decision.accept,
|
|
4121
|
+
auto: true,
|
|
4122
|
+
recovered: true,
|
|
4123
|
+
at: Date.now(),
|
|
4124
|
+
};
|
|
4125
|
+
this._lastDialog.set(tid, info);
|
|
4126
|
+
return info;
|
|
4127
|
+
}
|
|
4128
|
+
|
|
4129
|
+
/** 取"这次动作期间"处理过的对话框(回执用)。 */
|
|
4130
|
+
_dialogSince(tabId, since) {
|
|
4131
|
+
const d = this._lastDialog.get(tabId);
|
|
4132
|
+
if (!d || d.at < since) return null;
|
|
4133
|
+
const { at, ...rest } = d;
|
|
4134
|
+
return rest;
|
|
4135
|
+
}
|
|
4136
|
+
|
|
3976
4137
|
_ensureCdp(tabId, view, domain) {
|
|
3977
4138
|
try {
|
|
3978
4139
|
const wc = view.webContents;
|
|
@@ -3983,6 +4144,9 @@ return {ok:true,name:file.name,size:file.size,via:via,host_sel:hostSel,clicked:c
|
|
|
3983
4144
|
// Single shared listener per tab -> no duplicate broadcasts even when
|
|
3984
4145
|
// many sessions subscribe to network/runtime events on the same tab.
|
|
3985
4146
|
wc.debugger.on('message', (_event, method, params) => this._onCdpMessage(tabId, method, params));
|
|
4147
|
+
// 任何 tab 一旦挂上 CDP,就顺手盯住对话框:漏掉一个 alert 会让整条 tab
|
|
4148
|
+
// 永久卡死(渲染器被阻塞,后续每条命令都超时)。
|
|
4149
|
+
this._ensureDialogWatch(tabId, view);
|
|
3986
4150
|
}
|
|
3987
4151
|
const entry = this._cdpTabs.get(tabId);
|
|
3988
4152
|
if (!entry.domains.has(domain)) {
|
|
@@ -4132,6 +4296,16 @@ return {ok:true,name:file.name,size:file.size,via:via,host_sel:hostSel,clicked:c
|
|
|
4132
4296
|
}
|
|
4133
4297
|
|
|
4134
4298
|
_onCdpMessage(tabId, method, params) {
|
|
4299
|
+
// 浏览器级对话框:**必须在最前面处理**,且不能受任何订阅条件限制 ——
|
|
4300
|
+
// 它决定这条 tab 会不会被永久冻结(见 _ensureDialogWatch 注释)。
|
|
4301
|
+
if (method === 'Page.javascriptDialogOpening') {
|
|
4302
|
+
this._handleDialogOpening(tabId, params).catch(() => {});
|
|
4303
|
+
return;
|
|
4304
|
+
}
|
|
4305
|
+
if (method === 'Page.javascriptDialogClosed') {
|
|
4306
|
+
this._openDialogs.delete(tabId);
|
|
4307
|
+
return;
|
|
4308
|
+
}
|
|
4135
4309
|
// 抓包:请求侧(method / headers / POST body / initiator)与响应侧都落进
|
|
4136
4310
|
// network_monitor;旧的网络事件推送行为保持不变。
|
|
4137
4311
|
if (typeof method === 'string' && method.indexOf('Network.') === 0) {
|
package/src/main/ws_server.js
CHANGED
|
@@ -142,14 +142,23 @@ export function startWSServer(pageManager, port = config.wsPort, onQuit = null)
|
|
|
142
142
|
send({ jsonrpc: '2.0', id, result });
|
|
143
143
|
} catch (e) {
|
|
144
144
|
if (e && e.__deadline) {
|
|
145
|
+
// 外层预算可能先于 page_manager 内层到期(内层还要等动作前探针)。
|
|
146
|
+
// 所以这条兜底路径也要能识别"是对话框把页面冻住了"—— 否则用户
|
|
147
|
+
// 看到的就是"点了删除,超时",而屏幕上那个原生确认框没人应答。
|
|
148
|
+
const dlg = await Promise.resolve()
|
|
149
|
+
.then(() => pageManager.recoverBlockedTab?.(tabId))
|
|
150
|
+
.catch(() => null);
|
|
145
151
|
send({
|
|
146
152
|
jsonrpc: '2.0', id,
|
|
147
153
|
result: {
|
|
148
154
|
success: false,
|
|
149
|
-
error:
|
|
155
|
+
error: dlg
|
|
156
|
+
? `blocked by a browser-level JS dialog (${dlg.type}) — native sheet, not page DOM; the page is frozen while it is open. It has been ${dlg.accept ? 'accepted' : 'dismissed'} automatically; re-read the tree before retrying.`
|
|
157
|
+
: `${e.message} — the page's main thread is likely blocked; the action may have partially applied`,
|
|
150
158
|
timed_out: true,
|
|
151
159
|
deadline_ms: budget,
|
|
152
160
|
action: params.action,
|
|
161
|
+
...(dlg ? { dialog: dlg, retry: 'verify_first' } : {}),
|
|
153
162
|
},
|
|
154
163
|
});
|
|
155
164
|
} else {
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// shared/dialog_policy.cjs — JavaScript 对话框(alert / confirm / prompt /
|
|
2
|
+
// beforeunload)的处理策略。
|
|
3
|
+
//
|
|
4
|
+
// # 为什么需要这一层(这是产品缺陷,不是站点适配)
|
|
5
|
+
//
|
|
6
|
+
// `alert()/confirm()` 不是页面 DOM 的一部分:Chromium 把它们交给 embedder
|
|
7
|
+
// 渲染,Electron 就画成**原生 sheet**(屏幕中间那个 Cancel / OK)。由此产生
|
|
8
|
+
// 三个后果,全都指向同一个事实——**agent 永远够不到它**:
|
|
9
|
+
//
|
|
10
|
+
// 1. 语义树里没有它(不在 DOM,也不在 AX 树),get_tree 看不见;
|
|
11
|
+
// 2. 截图里它是一层系统 UI,点击坐标不归页面渲染器管;
|
|
12
|
+
// 3. **对话框打开期间整个渲染器被阻塞**:Runtime.evaluate / Input.dispatch
|
|
13
|
+
// 全部挂住不返回,于是任何 act 都以"超时、主线程可能被阻塞"结束。
|
|
14
|
+
//
|
|
15
|
+
// 实测(2026-10-24,本地 fixture):点一个调用 confirm() 的按钮 → act 15s
|
|
16
|
+
// 超时,随后 evaluate 8s 超时;屏幕上是一个原生 "确定删除这条评论? Cancel/OK"。
|
|
17
|
+
// 用户侧表现就是"删不掉,还卡住"。
|
|
18
|
+
//
|
|
19
|
+
// 既然对话框属于**浏览器进程**,处理它的责任也必须在浏览器进程这边:
|
|
20
|
+
// 这一类对话框由本模块给出策略、由 page_manager 通过 CDP
|
|
21
|
+
// `Page.handleJavaScriptDialog` 直接应答(这条命令走浏览器进程,不需要
|
|
22
|
+
// 渲染器,所以对话框阻塞页面时它照样有效)。
|
|
23
|
+
//
|
|
24
|
+
// # 默认策略(auto)为什么是"接受"
|
|
25
|
+
//
|
|
26
|
+
// 对话框是对**上一个用户动作**的确认。agent 点"删除评论"时,点击本身就是
|
|
27
|
+
// 意图,再让它去点"确定"没有任何信息增量 —— 拒绝反而会让删除永远失败
|
|
28
|
+
// (这正是用户报的现象)。所以 auto 接受 alert/confirm/beforeunload;
|
|
29
|
+
// prompt 用页面自带的默认值提交(等于人直接按 OK),并把提交的值回报给
|
|
30
|
+
// 调用方。策略可整体覆盖:AI_BROWSER_DIALOG_POLICY=auto|accept|dismiss|manual。
|
|
31
|
+
//
|
|
32
|
+
// manual 只用于诊断:不自动应答,对话框保持打开,由人来点。
|
|
33
|
+
|
|
34
|
+
'use strict';
|
|
35
|
+
|
|
36
|
+
const POLICIES = ['auto', 'accept', 'dismiss', 'manual'];
|
|
37
|
+
|
|
38
|
+
/** 环境变量/参数里的策略值归一化;非法值回落到 auto(不因为配置写错就卡死页面)。 */
|
|
39
|
+
function normalizePolicy(raw) {
|
|
40
|
+
const s = String(raw == null ? '' : raw).trim().toLowerCase();
|
|
41
|
+
return POLICIES.includes(s) ? s : 'auto';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** 从环境变量读策略。每次读而不是缓存:便于诊断时改环境重启即可。 */
|
|
45
|
+
function policyFromEnv(env = process.env) {
|
|
46
|
+
return normalizePolicy(env && env.AI_BROWSER_DIALOG_POLICY);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* 决定一个对话框怎么应答。
|
|
51
|
+
*
|
|
52
|
+
* @param {{type?:string, message?:string, defaultPrompt?:string}} dialog
|
|
53
|
+
* CDP `Page.javascriptDialogOpening` 的载荷。
|
|
54
|
+
* @param {{policy?:string}} [opts]
|
|
55
|
+
* @returns {{accept:boolean, promptText?:string, auto:boolean, reason:string}}
|
|
56
|
+
* auto=false 表示"没有人替它做决定"(manual 策略),调用方需要把它暴露出去。
|
|
57
|
+
*/
|
|
58
|
+
function decideDialog(dialog, opts = {}) {
|
|
59
|
+
const policy = normalizePolicy(opts.policy);
|
|
60
|
+
const type = String((dialog && dialog.type) || 'alert');
|
|
61
|
+
|
|
62
|
+
if (policy === 'manual') {
|
|
63
|
+
return { accept: false, auto: false, reason: 'manual: left open for a human/caller' };
|
|
64
|
+
}
|
|
65
|
+
if (policy === 'accept') {
|
|
66
|
+
return { accept: true, auto: true, promptText: promptAnswer(dialog), reason: 'policy=accept' };
|
|
67
|
+
}
|
|
68
|
+
if (policy === 'dismiss') {
|
|
69
|
+
return { accept: false, auto: true, reason: 'policy=dismiss' };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// auto:按"人看到这个对话框会怎么做"来判断
|
|
73
|
+
switch (type) {
|
|
74
|
+
case 'beforeunload':
|
|
75
|
+
// 触发它的动作(点链接 / navigate / 关标签)本身就是"我要走",人也是点
|
|
76
|
+
// "离开"。拒绝会让 unload 卡死,页面再也导航不了。
|
|
77
|
+
return { accept: true, auto: true, reason: 'auto: beforeunload → leave (the action asked for it)' };
|
|
78
|
+
case 'alert':
|
|
79
|
+
// 只有一个按钮,没有选择空间。
|
|
80
|
+
return { accept: true, auto: true, reason: 'auto: alert → ok' };
|
|
81
|
+
case 'confirm':
|
|
82
|
+
return { accept: true, auto: true, reason: 'auto: confirm → accepted (the action that raised it was the intent)' };
|
|
83
|
+
case 'prompt':
|
|
84
|
+
// 文本无法猜,用页面给的默认值提交(等价人直接按 OK),并把值回报。
|
|
85
|
+
return { accept: true, auto: true, promptText: promptAnswer(dialog), reason: 'auto: prompt → accepted with default text' };
|
|
86
|
+
default:
|
|
87
|
+
return { accept: true, auto: true, reason: `auto: ${type} → accepted` };
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function promptAnswer(dialog) {
|
|
92
|
+
const d = dialog && dialog.defaultPrompt;
|
|
93
|
+
return typeof d === 'string' ? d : '';
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* 导航时是否放行 beforeunload(Electron 的 will-prevent-unload)。
|
|
98
|
+
* 只有明确要求"别自动通过"的策略才拦:manual / dismiss。
|
|
99
|
+
*/
|
|
100
|
+
function allowUnload(opts = {}) {
|
|
101
|
+
const policy = normalizePolicy(opts.policy);
|
|
102
|
+
return policy === 'auto' || policy === 'accept';
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
module.exports = {
|
|
106
|
+
POLICIES,
|
|
107
|
+
normalizePolicy,
|
|
108
|
+
policyFromEnv,
|
|
109
|
+
decideDialog,
|
|
110
|
+
allowUnload,
|
|
111
|
+
};
|
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.7';
|