@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.
@@ -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
+