@glittering0717/ai-browser 0.0.0-stage → 1.2.0
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/LICENSE +21 -0
- package/README.md +784 -2
- package/bin/ai-browser.cjs +78 -0
- package/examples/AGENT_INSTRUCTIONS.md +166 -0
- package/examples/mcp-config.json +12 -0
- package/llms-install.md +158 -0
- package/package.json +78 -4
- package/scripts/electron_resolve.cjs +241 -0
- package/scripts/start.cjs +60 -0
- package/src/main/axExtractor.js +436 -0
- package/src/main/canvas_hook_source.js +654 -0
- package/src/main/canvas_monitor.js +269 -0
- package/src/main/capability_probe.js +415 -0
- package/src/main/download_monitor.js +137 -0
- package/src/main/index.js +359 -0
- package/src/main/mcp_server.js +408 -0
- package/src/main/mcp_tools.js +289 -0
- package/src/main/mcp_ws.js +178 -0
- package/src/main/network_monitor.js +805 -0
- package/src/main/page_manager.js +2743 -0
- package/src/main/snapshot_diff.js +371 -0
- package/src/main/ws_server.js +440 -0
- package/src/preload/actions.cjs +190 -0
- package/src/preload/bridge.cjs +75 -0
- package/src/preload/extractor.cjs +936 -0
- package/src/preload/watcher.cjs +241 -0
- package/src/renderer/tab_bar.html +115 -0
- package/src/shared/config.js +32 -0
- package/src/shared/deadline.js +89 -0
- package/src/shared/guards.js +17 -0
- package/src/shared/protocol.js +116 -0
- package/src/shared/value_contract.cjs +132 -0
- package/src/shared/version.js +5 -0
- package/src/shared/watchdog.js +57 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// shared/value_contract.cjs — node.value 截断契约(单一实现,两条读层共用)
|
|
2
|
+
//
|
|
3
|
+
// 背景:node.value 的 200 限制是本项目自己的行为,不是 CDP/无障碍树的限制。
|
|
4
|
+
// 旧实现在两处各自 `String(v).slice(0, 200)`:
|
|
5
|
+
// - src/preload/extractor.cjs(legacy DOM 读层)
|
|
6
|
+
// - src/main/axExtractor.js(AX 读层)
|
|
7
|
+
// 结果是"原值恰好 200"与"更长但被截断"不可区分,agent 无从判断要不要补读。
|
|
8
|
+
//
|
|
9
|
+
// 契约:
|
|
10
|
+
// - 未截断:只返回 { value },不给普通节点增加多余字段(省 token)。
|
|
11
|
+
// - 截断:额外返回 value_truncated / value_full_length / value_length_unit /
|
|
12
|
+
// value_fetch_ref。value 永远是原值前缀,不追加 "..."。
|
|
13
|
+
// - 长度单位统一为 Unicode code point(不是 UTF-16 code unit,也不是字节),
|
|
14
|
+
// 保证不会把 emoji 等 surrogate pair 切半。
|
|
15
|
+
//
|
|
16
|
+
// 同时被 ui.act{action:'get_value'} 复用做 offset/limit 分页,因此这里的
|
|
17
|
+
// slice 语义与截断语义必须完全一致。
|
|
18
|
+
//
|
|
19
|
+
// CJS:preload(require)与 main(ESM default import)都要能用。
|
|
20
|
+
|
|
21
|
+
const VALUE_UNIT = 'unicode_code_point';
|
|
22
|
+
const DEFAULT_VALUE_LIMIT = 200;
|
|
23
|
+
|
|
24
|
+
// get_value 的分页上限:默认一次拿 20000 code point,硬上限 100 万。
|
|
25
|
+
const GET_VALUE_DEFAULT_LIMIT = 20000;
|
|
26
|
+
const GET_VALUE_HARD_LIMIT = 1000000;
|
|
27
|
+
|
|
28
|
+
// code point 长度。Array.from 按迭代器拆分,等价于 code point 计数。
|
|
29
|
+
function codePointLength(raw) {
|
|
30
|
+
return Array.from(raw == null ? '' : String(raw)).length;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// 按 code point 取子串 [offset, offset+limit)。越界自动收敛,绝不产生孤立 surrogate。
|
|
34
|
+
function sliceByCodePoints(raw, offset, limit) {
|
|
35
|
+
const arr = Array.from(raw == null ? '' : String(raw));
|
|
36
|
+
const start = Math.max(0, Math.min(arr.length, Number(offset) || 0));
|
|
37
|
+
const end = (limit == null || !isFinite(Number(limit)))
|
|
38
|
+
? arr.length
|
|
39
|
+
: Math.min(arr.length, start + Math.max(0, Math.floor(Number(limit))));
|
|
40
|
+
return arr.slice(start, end).join('');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 截断节点 value。
|
|
45
|
+
* @param {*} raw 原始值
|
|
46
|
+
* @param {string} target 该节点的 data-ai-id(get_value 的定位主键)
|
|
47
|
+
* @param {number} limit code point 上限,默认 200
|
|
48
|
+
*/
|
|
49
|
+
function truncateNodeValue(raw, target, limit = DEFAULT_VALUE_LIMIT) {
|
|
50
|
+
const str = raw == null ? '' : String(raw);
|
|
51
|
+
const cap = Number.isFinite(Number(limit)) && Number(limit) > 0 ? Math.floor(Number(limit)) : DEFAULT_VALUE_LIMIT;
|
|
52
|
+
const arr = Array.from(str);
|
|
53
|
+
if (arr.length <= cap) return { value: str };
|
|
54
|
+
return {
|
|
55
|
+
value: arr.slice(0, cap).join(''),
|
|
56
|
+
value_truncated: true,
|
|
57
|
+
value_full_length: arr.length,
|
|
58
|
+
value_length_unit: VALUE_UNIT,
|
|
59
|
+
value_fetch_ref: { action: 'get_value', target },
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// 把内部字段 value_fetch_ref 展开成带实际 tab 的可直接执行提示,并删掉内部字段。
|
|
64
|
+
// 主进程组装最终树响应时调用(tab 只有主进程知道)。
|
|
65
|
+
function expandValueFetch(node, tabId) {
|
|
66
|
+
if (!node || !node.value_fetch_ref) return node;
|
|
67
|
+
const target = node.value_fetch_ref.target;
|
|
68
|
+
node.value_fetch = {
|
|
69
|
+
mcp: { tool: 'browse_act', args: { action: 'get_value', target, tab: tabId } },
|
|
70
|
+
ws: { method: 'ui.act', params: { action: 'get_value', target, tab: tabId } },
|
|
71
|
+
};
|
|
72
|
+
delete node.value_fetch_ref;
|
|
73
|
+
return node;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// 走一棵树,展开所有 value_fetch_ref。
|
|
77
|
+
function expandValueFetchTree(tree, tabId) {
|
|
78
|
+
if (!tree) return tree;
|
|
79
|
+
const walk = (n) => {
|
|
80
|
+
if (!n) return;
|
|
81
|
+
expandValueFetch(n, tabId);
|
|
82
|
+
for (const c of n.children || []) walk(c);
|
|
83
|
+
};
|
|
84
|
+
walk(tree);
|
|
85
|
+
return tree;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// 敏感控件:password/file 的"值"不进入语义树,也不给长度或取全量提示。
|
|
89
|
+
function sensitiveInputKind(el) {
|
|
90
|
+
if (!el || !el.tagName) return null;
|
|
91
|
+
const tag = String(el.tagName).toLowerCase();
|
|
92
|
+
if (tag !== 'input') return null;
|
|
93
|
+
const type = String((el.type || (el.getAttribute && el.getAttribute('type')) || 'text')).toLowerCase();
|
|
94
|
+
if (type === 'password') return 'password';
|
|
95
|
+
if (type === 'file') return 'file';
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ui.act{action:'get_value'} 的响应构造(与截断契约同一长度单位)。
|
|
100
|
+
function getValueResult(rawText, offset, limit) {
|
|
101
|
+
const full = codePointLength(rawText);
|
|
102
|
+
const start = Math.max(0, Math.min(full, Number(offset) || 0));
|
|
103
|
+
const cap = (limit == null || !isFinite(Number(limit)))
|
|
104
|
+
? GET_VALUE_DEFAULT_LIMIT
|
|
105
|
+
: Math.max(1, Math.min(GET_VALUE_HARD_LIMIT, Math.floor(Number(limit))));
|
|
106
|
+
const value = sliceByCodePoints(rawText, start, cap);
|
|
107
|
+
const returned = codePointLength(value);
|
|
108
|
+
const hasMore = start + returned < full;
|
|
109
|
+
return {
|
|
110
|
+
value,
|
|
111
|
+
offset: start,
|
|
112
|
+
returned_length: returned,
|
|
113
|
+
value_full_length: full,
|
|
114
|
+
value_length_unit: VALUE_UNIT,
|
|
115
|
+
value_truncated: hasMore,
|
|
116
|
+
next_offset: hasMore ? start + returned : null,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
module.exports = {
|
|
121
|
+
VALUE_UNIT,
|
|
122
|
+
DEFAULT_VALUE_LIMIT,
|
|
123
|
+
GET_VALUE_DEFAULT_LIMIT,
|
|
124
|
+
GET_VALUE_HARD_LIMIT,
|
|
125
|
+
codePointLength,
|
|
126
|
+
sliceByCodePoints,
|
|
127
|
+
truncateNodeValue,
|
|
128
|
+
expandValueFetch,
|
|
129
|
+
expandValueFetchTree,
|
|
130
|
+
sensitiveInputKind,
|
|
131
|
+
getValueResult,
|
|
132
|
+
};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// shared/version.js — Single source of truth for the app runtime version.
|
|
2
|
+
// Kept in JS (not imported from package.json) to avoid Node import-attribute
|
|
3
|
+
// (JSON module) compatibility issues inside Electron. package.json "version"
|
|
4
|
+
// is manually synced to this value; tests assert they match.
|
|
5
|
+
export const VERSION = '1.2.0';
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// shared/watchdog.js — Pure decision logic for the NetworkService watchdog.
|
|
2
|
+
// No Electron / Node-side-effect imports: everything here is a pure function so
|
|
3
|
+
// it can be unit tested without booting Electron. The main process owns the
|
|
4
|
+
// actual probing (net.request), the interval and the relaunch — this module only
|
|
5
|
+
// answers "what targets?" and "is it on?".
|
|
6
|
+
//
|
|
7
|
+
// Deliberately NOT included: any "is a human probably at the wheel?" heuristic.
|
|
8
|
+
// `mainWindow.isFocused()` cannot distinguish the human from the agent — the
|
|
9
|
+
// agent steals focus itself (page_manager's `_inputViaCdp` / `peek` call
|
|
10
|
+
// `window.show()/moveTop()/focus()` on every action). Under the most common
|
|
11
|
+
// scenario (a human sitting and watching the agent work) such a heuristic would
|
|
12
|
+
// defer forever and the watchdog would never self-heal at all. Not interrupting
|
|
13
|
+
// people is achieved by lowering false positives + making it configurable +
|
|
14
|
+
// making it switchable off — not by guessing who is at the keyboard.
|
|
15
|
+
|
|
16
|
+
// Two hosts by default, not one. The watchdog only needs the NetworkService to
|
|
17
|
+
// round-trip; if either host answers, the service is alive. example.com alone is
|
|
18
|
+
// unreliable from mainland China and produced false "wedged" verdicts (→ reboot
|
|
19
|
+
// loops), so baidu.com is a second, region-friendly probe.
|
|
20
|
+
export const DEFAULT_WATCHDOG_TARGETS = ['https://example.com/', 'https://www.baidu.com/'];
|
|
21
|
+
|
|
22
|
+
// Values that explicitly turn the watchdog off (case-insensitive).
|
|
23
|
+
const WATCHDOG_OFF_VALUES = ['0', 'off', 'false', 'no'];
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Parse the AI_BROWSER_WATCHDOG_URLS env var into a de-duplicated target list.
|
|
27
|
+
* Empty / unset → DEFAULT_WATCHDOG_TARGETS.
|
|
28
|
+
* @param {string|undefined} envStr raw comma-separated env value
|
|
29
|
+
* @returns {string[]} non-empty, trimmed, de-duplicated URLs
|
|
30
|
+
*/
|
|
31
|
+
export function parseWatchdogTargets(envStr) {
|
|
32
|
+
if (typeof envStr !== 'string') return [...DEFAULT_WATCHDOG_TARGETS];
|
|
33
|
+
|
|
34
|
+
const parsed = envStr
|
|
35
|
+
.split(',')
|
|
36
|
+
.map((s) => (typeof s === 'string' ? s.trim() : ''))
|
|
37
|
+
.filter((s) => s.length > 0);
|
|
38
|
+
|
|
39
|
+
const unique = [];
|
|
40
|
+
for (const url of parsed) {
|
|
41
|
+
if (!unique.includes(url)) unique.push(url);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return unique.length > 0 ? unique : [...DEFAULT_WATCHDOG_TARGETS];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Whether the watchdog should run at all. Defaults to on; AI_BROWSER_WATCHDOG
|
|
49
|
+
* set to 0/off/false/no (any case) disables it.
|
|
50
|
+
* @param {string|undefined} envValue
|
|
51
|
+
* @returns {boolean}
|
|
52
|
+
*/
|
|
53
|
+
export function isWatchdogEnabled(envValue) {
|
|
54
|
+
if (typeof envValue !== 'string') return true;
|
|
55
|
+
return !WATCHDOG_OFF_VALUES.includes(envValue.trim().toLowerCase());
|
|
56
|
+
}
|
|
57
|
+
|