@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,241 @@
1
+ // preload/watcher.cjs — Mutation/state/captcha/message/JS-error watchers (CommonJS)
2
+ // Split out of bridge.js v6.9. No Electron dependency — side effects are
3
+ // emitted through an injected `onEvent(eventType, data)` callback so the
4
+ // module can be unit-tested under jsdom (bridge.js passes ipcRenderer.send).
5
+ // Exports: startTracking(callbacks), stopTracking()
6
+
7
+ // ============================================================
8
+ // 4. CAPTCHA + MESSAGE WATCHERS
9
+ // ============================================================
10
+
11
+ var observer = null;
12
+ var lastCaptchaSig = "";
13
+ var lastMessageSig = "";
14
+ var mutationQueue = [];
15
+ var mutationFlushTimer = null;
16
+ var scanTimer = null;
17
+ var MUTATION_FLUSH_DELAY = 500;
18
+ var SCAN_INTERVAL = 5000;
19
+
20
+ function scanCaptcha() {
21
+ var byCSS = document.querySelectorAll("[class*=captcha],[class*=verify],[class*=slider],[class*=slide],[class*=drag],[id*=captcha],[id*=verify],[class*=JDJRV],[class*=geetest],[class*=yidun],[class*=small-jd]");
22
+ var byAlt = document.querySelectorAll("img[alt*=验证码],img[alt*=captcha],img[alt*=verify],img[alt*=滑块],img[alt*=slide]");
23
+ var all = [];
24
+ for (var bi = 0; bi < byCSS.length; bi++) all.push(byCSS[bi]);
25
+ for (var ai = 0; ai < byAlt.length; ai++) { if (all.indexOf(byAlt[ai]) < 0) all.push(byAlt[ai]); }
26
+
27
+ var visible = [];
28
+ for (var i = 0; i < all.length; i++) {
29
+ var r = all[i].getBoundingClientRect();
30
+ if (r.width >= 0 || r.height >= 0) {
31
+ visible.push({
32
+ tag: all[i].tagName,
33
+ cls: String(all[i].className || "").slice(0, 80),
34
+ id: all[i].id || "",
35
+ text: String(all[i].textContent || "").trim().slice(0, 50),
36
+ bounds: { x: Math.round(r.x), y: Math.round(r.y), width: Math.round(r.width), height: Math.round(r.height) }
37
+ });
38
+ }
39
+ }
40
+
41
+ // Also scan for canvases in captcha areas
42
+ var canvases = document.querySelectorAll("canvas");
43
+ for (var ci = 0; ci < canvases.length; ci++) {
44
+ var cr = canvases[ci].getBoundingClientRect();
45
+ if (cr.width > 10 && cr.height > 10) {
46
+ // Check if canvas is inside a captcha-like container
47
+ var parent = canvases[ci].parentElement;
48
+ var pCls = (parent && parent.className || "") + (parent && parent.id || "");
49
+ if (/captcha|verify|slider|slide|drag|yidun|geetest/i.test(pCls)) {
50
+ visible.push({
51
+ tag: "CANVAS",
52
+ cls: (canvases[ci].className || "").slice(0, 40),
53
+ id: canvases[ci].id || "",
54
+ text: "[canvas captcha]",
55
+ bounds: { x: Math.round(cr.x), y: Math.round(cr.y), width: Math.round(cr.width), height: Math.round(cr.height) },
56
+ parent_cls: pCls.slice(0, 60)
57
+ });
58
+ }
59
+ }
60
+ }
61
+
62
+ return visible;
63
+ }
64
+
65
+ function scanMessages() {
66
+ var selectors = [
67
+ "[class*=toast]", "[class*=message]", "[class*=notification]", "[class*=alert]",
68
+ "[class*=snackbar]", "[class*=notice]", "[class*=tip]",
69
+ "[role=alert]", "[role=status]", "[class*=banner]"
70
+ ];
71
+ var msgs = [];
72
+ for (var si = 0; si < selectors.length; si++) {
73
+ var els = document.querySelectorAll(selectors[si]);
74
+ for (var ei = 0; ei < els.length; ei++) {
75
+ if (els[ei].offsetParent === null && window.getComputedStyle(els[ei]).display === "none") continue;
76
+ var r = els[ei].getBoundingClientRect();
77
+ var t = (els[ei].textContent || "").trim();
78
+ if (t && t.length < 200) {
79
+ msgs.push({ text: t.slice(0, 100), bounds: { x: Math.round(r.x), y: Math.round(r.y), width: Math.round(r.width), height: Math.round(r.height) } });
80
+ }
81
+ }
82
+ }
83
+ return msgs;
84
+ }
85
+
86
+ function runScan(emit) {
87
+ scanTimer = null;
88
+ var cap = scanCaptcha();
89
+ if (cap.length > 0) {
90
+ var sig = JSON.stringify(cap.map(function(c) { return c.cls + "|" + c.bounds.width + "x" + c.bounds.height; }));
91
+ if (sig !== lastCaptchaSig) {
92
+ lastCaptchaSig = sig;
93
+ emit("captcha_appeared", { elements: cap });
94
+ }
95
+ }
96
+ var msg = scanMessages();
97
+ if (msg.length > 0) {
98
+ var msig = JSON.stringify(msg.map(function(m) { return m.text; }));
99
+ if (msig !== lastMessageSig) {
100
+ lastMessageSig = msig;
101
+ emit("message_appeared", { messages: msg });
102
+ }
103
+ }
104
+ }
105
+
106
+ function flushMutations(emit) {
107
+ mutationFlushTimer = null;
108
+ var mutations = mutationQueue;
109
+ mutationQueue = [];
110
+ if (mutations.length === 0) return;
111
+
112
+ var stateChanges = [];
113
+
114
+ for (var mi = 0; mi < mutations.length; mi++) {
115
+ var m = mutations[mi];
116
+
117
+ if (m.type === "attributes") {
118
+ var attrName = (m.attributeName || "").toLowerCase();
119
+ var trackedAttrs = ["disabled", "class", "open", "checked", "aria-expanded", "aria-selected", "hidden", "value", "readonly"];
120
+ if (trackedAttrs.indexOf(attrName) >= 0) {
121
+ var el = m.target;
122
+ if (el instanceof HTMLElement) {
123
+ var aiId = el.getAttribute("data-ai-id");
124
+ if (aiId) {
125
+ stateChanges.push({
126
+ type: "state_changed",
127
+ targetId: aiId,
128
+ attribute: attrName,
129
+ value: el.getAttribute(attrName)
130
+ });
131
+ }
132
+ }
133
+ }
134
+ }
135
+
136
+ if (m.type === "childList") {
137
+ var parentId = m.target instanceof HTMLElement ? m.target.getAttribute("data-ai-id") : null;
138
+ if (m.addedNodes.length > 0 || m.removedNodes.length > 0) {
139
+ stateChanges.push({
140
+ type: "dom_changed",
141
+ parentId: parentId,
142
+ added: m.addedNodes.length,
143
+ removed: m.removedNodes.length
144
+ });
145
+ }
146
+ }
147
+ }
148
+
149
+ if (stateChanges.length > 0) {
150
+ var batch = [];
151
+ for (var si = 0; si < Math.min(stateChanges.length, 20); si++) batch.push(stateChanges[si]);
152
+ emit("state_changed", { changes: batch });
153
+ }
154
+ }
155
+
156
+ function bindObserver(emit) {
157
+ if (observer) observer.disconnect();
158
+ mutationQueue = [];
159
+ if (mutationFlushTimer) {
160
+ clearTimeout(mutationFlushTimer);
161
+ mutationFlushTimer = null;
162
+ }
163
+ if (scanTimer) {
164
+ clearInterval(scanTimer);
165
+ scanTimer = null;
166
+ }
167
+
168
+ observer = new MutationObserver(function(mutations) {
169
+ mutationQueue.push.apply(mutationQueue, mutations);
170
+ if (!mutationFlushTimer) {
171
+ mutationFlushTimer = setTimeout(function() { flushMutations(emit); }, MUTATION_FLUSH_DELAY);
172
+ }
173
+ });
174
+
175
+ observer.observe(document.body, {
176
+ childList: true,
177
+ subtree: true,
178
+ attributes: true,
179
+ attributeFilter: ["disabled", "class", "open", "checked", "aria-expanded", "aria-selected", "hidden", "value", "readonly"]
180
+ });
181
+
182
+ // Captcha/message scan runs on its own low-frequency timer to avoid
183
+ // blocking the renderer with querySelectorAll during page load.
184
+ scanTimer = setInterval(function() { runScan(emit); }, SCAN_INTERVAL);
185
+ setTimeout(function() { runScan(emit); }, 1000);
186
+ }
187
+
188
+ // ============================================================
189
+ // JS error capture
190
+ // ============================================================
191
+
192
+ var oldOnError = null;
193
+
194
+ function hookJsErrors(emit) {
195
+ oldOnError = window.onerror;
196
+ window.onerror = function(msg, url, line, col, err) {
197
+ emit("js_error", { message: String(msg), url: String(url), line: line, col: col, stack: (err && err.stack ? String(err.stack).slice(0, 300) : "") });
198
+ if (oldOnError) return oldOnError.apply(this, arguments);
199
+ return false;
200
+ };
201
+
202
+ window.addEventListener("unhandledrejection", function(ev) {
203
+ var reason = ev.reason;
204
+ var msg = reason instanceof Error ? reason.message : String(reason);
205
+ emit("js_error", { message: msg, type: "unhandledrejection", stack: (reason instanceof Error && reason.stack ? String(reason.stack).slice(0, 300) : "") });
206
+ });
207
+ }
208
+
209
+ // ============================================================
210
+ // Public API
211
+ // ============================================================
212
+
213
+ function startTracking(callbacks) {
214
+ callbacks = callbacks || {};
215
+ function nullEmit() {}
216
+ var emit = typeof callbacks.onEvent === "function" ? callbacks.onEvent : nullEmit;
217
+ bindObserver(emit);
218
+ hookJsErrors(emit);
219
+ }
220
+
221
+ function stopTracking() {
222
+ if (observer) {
223
+ observer.disconnect();
224
+ observer = null;
225
+ }
226
+ mutationQueue = [];
227
+ if (mutationFlushTimer) {
228
+ clearTimeout(mutationFlushTimer);
229
+ mutationFlushTimer = null;
230
+ }
231
+ if (scanTimer) {
232
+ clearInterval(scanTimer);
233
+ scanTimer = null;
234
+ }
235
+ if (oldOnError !== null) {
236
+ window.onerror = oldOnError;
237
+ oldOnError = null;
238
+ }
239
+ }
240
+
241
+ module.exports = { startTracking, stopTracking };
@@ -0,0 +1,115 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <style>
6
+ * { margin: 0; padding: 0; box-sizing: border-box; }
7
+ body {
8
+ height: 36px;
9
+ background: #1a1a1a;
10
+ display: flex;
11
+ align-items: center;
12
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
13
+ font-size: 12px;
14
+ color: #ccc;
15
+ user-select: none;
16
+ -webkit-app-region: drag;
17
+ }
18
+ #tabs {
19
+ display: flex;
20
+ flex: 1;
21
+ overflow: hidden;
22
+ -webkit-app-region: no-drag;
23
+ }
24
+ .tab {
25
+ display: flex;
26
+ align-items: center;
27
+ max-width: 200px;
28
+ min-width: 80px;
29
+ padding: 0 12px;
30
+ height: 36px;
31
+ cursor: pointer;
32
+ border-right: 1px solid #2a2a2a;
33
+ transition: background 0.15s;
34
+ }
35
+ .tab:hover { background: #2a2a2a; }
36
+ .tab.active { background: #2d2d30; color: #fff; border-bottom: 2px solid #007acc; }
37
+ .tab-title {
38
+ flex: 1;
39
+ overflow: hidden;
40
+ text-overflow: ellipsis;
41
+ white-space: nowrap;
42
+ margin-right: 8px;
43
+ }
44
+ .tab-close {
45
+ width: 16px;
46
+ height: 16px;
47
+ display: flex;
48
+ align-items: center;
49
+ justify-content: center;
50
+ border-radius: 3px;
51
+ font-size: 14px;
52
+ color: #666;
53
+ cursor: pointer;
54
+ }
55
+ .tab-close:hover { background: #3a3a3a; color: #fff; }
56
+ #new-tab {
57
+ width: 36px;
58
+ height: 36px;
59
+ display: flex;
60
+ align-items: center;
61
+ justify-content: center;
62
+ font-size: 20px;
63
+ cursor: pointer;
64
+ color: #888;
65
+ -webkit-app-region: no-drag;
66
+ }
67
+ #new-tab:hover { background: #2a2a2a; color: #fff; }
68
+ .tab-id { color: #555; font-size: 10px; margin-right: 4px; }
69
+ </style>
70
+ </head>
71
+ <body>
72
+ <div id="tabs"></div>
73
+ <div id="new-tab">+</div>
74
+ <script>
75
+ const { ipcRenderer } = require('electron');
76
+
77
+ async function renderTabs() {
78
+ try {
79
+ const tabs = await ipcRenderer.invoke('tab:list');
80
+ const container = document.getElementById('tabs');
81
+ container.innerHTML = '';
82
+ for (const tab of tabs) {
83
+ const el = document.createElement('div');
84
+ el.className = 'tab' + (tab.active ? ' active' : '');
85
+ const title = (tab.title || tab.url || 'New Tab').slice(0, 30);
86
+ el.innerHTML = '<span class="tab-id">[' + tab.id + ']</span>' +
87
+ '<span class="tab-title">' + title + '</span>' +
88
+ '<span class="tab-close" data-id="' + tab.id + '">&times;</span>';
89
+ el.addEventListener('click', function(e) {
90
+ if (e.target.classList.contains('tab-close')) {
91
+ ipcRenderer.send('tab:close', parseInt(e.target.dataset.id));
92
+ } else {
93
+ ipcRenderer.send('tab:activate', tab.id);
94
+ }
95
+ });
96
+ container.appendChild(el);
97
+ }
98
+ } catch(e) {
99
+ // ipcRenderer not ready yet
100
+ }
101
+ }
102
+
103
+ // expose for main process to call
104
+ window.renderTabs = renderTabs;
105
+
106
+ document.getElementById('new-tab').addEventListener('click', function() {
107
+ ipcRenderer.send('tab:new');
108
+ });
109
+
110
+ // Initial render + periodic refresh
111
+ renderTabs();
112
+ setInterval(renderTabs, 1500);
113
+ </script>
114
+ </body>
115
+ </html>
@@ -0,0 +1,32 @@
1
+ // shared/config.js — Centralized runtime configuration, single source of truth.
2
+ // Replaces hardcoded port 9223 / User-Agent / userData dir scattered across
3
+ // main/. Each value respects an AI_BROWSER_* env override, with defaults that
4
+ // are byte-identical to the previous hardcoded values (no behavior change by
5
+ // default).
6
+ import path from 'node:path';
7
+ import os from 'node:os';
8
+
9
+ const DEFAULT_UA =
10
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36';
11
+
12
+ export const config = {
13
+ // WebSocket JSON-RPC server + MCP auto-launch probe port
14
+ wsPort: Number(process.env.AI_BROWSER_PORT) || 9223,
15
+ // Electron userData dir — session/cookie persistence survives restart
16
+ userDataDir: process.env.AI_BROWSER_USER_DATA || path.join(os.homedir(), '.ai-browser'),
17
+ // Spoofed UA to mask the automation fingerprint
18
+ userAgent: process.env.AI_BROWSER_UA || DEFAULT_UA,
19
+ // Height of the in-app tab strip below the window title bar
20
+ tabBarHeight: 36,
21
+ // The session every tab's BrowserView uses. **Must stay a single source of
22
+ // truth**: session-level hooks (will-download, permission requests) are
23
+ // installed on `session.fromPartition(viewPartition)`. Hooking
24
+ // `session.defaultSession` instead silently does nothing — the views are not
25
+ // in that session. Measured 2026-10-12: a download was requested by the page,
26
+ // no will-download ever fired, and no file landed.
27
+ viewPartition: process.env.AI_BROWSER_VIEW_PARTITION || 'persist:ai-browser',
28
+ };
29
+
30
+ export function wsUrl(port = config.wsPort) {
31
+ return `ws://localhost:${port}`;
32
+ }
@@ -0,0 +1,89 @@
1
+ // shared/deadline.js — 给"会进页面"的调用加超时(纯函数,可无 Electron 单测)
2
+ //
3
+ // # 为什么需要这个文件
4
+ // ws_server 的每个 case 都是 `await pageManager.xxx(...)`,顶层只有 try/catch ——
5
+ // 抓得住"抛错",抓不住"不返回"。而 Chromium 的 CDP 命令**真的会不返回**:
6
+ // `Input.dispatchMouseEvent` 要等渲染进程 ack,渲染进程主线程被占住时 ack 永远不来。
7
+ //
8
+ // 实测(tools/_oneoff/blocked_renderer_probe.cjs,页面加载后 while 循环占住主线程
9
+ // 120s,单项上限 45s):
10
+ //
11
+ // ui.get_tree 5006ms ERR getTree timeout ← 本来就有超时
12
+ // ui.evaluate 10012ms ERR evaluate timeout ← CDP 5s + preload 回落 5s
13
+ // ui.get_focused 5006ms ERR getTree timeout
14
+ // ui.capabilities 15014ms ok
15
+ // ui.act 45001ms **仍无回包** ← 唯一真挂的一条
16
+ //
17
+ // 也就是说这不是"慢",是 agent 的工具调用永久悬挂、整个会话死掉,而且**没有任何错误
18
+ // 可看**。比抛错严重得多:抛错至少能让 agent 换策略。
19
+ //
20
+ // # 为什么超时必须按动作算,而不是一个固定值
21
+ // 固定小值会误杀合法长操作。实测这些路径自己就会睡很久:
22
+ // type/setContent 每字 interval_ms(默认 60ms)→ 500 字就是 30s
23
+ // drag duration_ms(默认 420ms)+ steps×~40ms,上限 5s
24
+ // clear 要走编辑器状态提交,多处 80~150ms 等待 + 回读校验
25
+ // 所以预算 = max(基线, 该动作自报的预期耗时 × 2 + 余量),并留 env / 单次调用的逃生门。
26
+
27
+ export const DEFAULT_ACTION_DEADLINE_MS = 15000;
28
+ export const MAX_ACTION_DEADLINE_MS = 300000;
29
+
30
+ /**
31
+ * 把一个 promise 套上超时。超时抛出的 error 带 `__deadline` 标记,调用方可以
32
+ * 据此把它翻译成"成功=false + 明确原因",而不是当成未知异常往上抛。
33
+ *
34
+ * 原 promise 迟到拒绝会被接住(不会变成 unhandled rejection)—— 超时之后它仍可能
35
+ * 完成或失败,那是后台的事,不该把进程吵醒。
36
+ */
37
+ export function withDeadline(promise, ms, label) {
38
+ const budget = Math.max(1, Number(ms) || DEFAULT_ACTION_DEADLINE_MS);
39
+ let timer = null;
40
+ const timeout = new Promise((_, reject) => {
41
+ timer = setTimeout(() => {
42
+ const err = new Error(`${label} timeout after ${budget}ms`);
43
+ err.__deadline = true;
44
+ err.deadline_ms = budget;
45
+ reject(err);
46
+ }, budget);
47
+ });
48
+ // 防止"超时已报错,原 promise 之后才拒绝"变成 unhandled rejection。
49
+ Promise.resolve(promise).catch(() => {});
50
+ return Promise.race([promise, timeout]).finally(() => {
51
+ if (timer) clearTimeout(timer);
52
+ });
53
+ }
54
+
55
+ /**
56
+ * 算一个动作的合理超时预算(毫秒)。
57
+ *
58
+ * 优先级:单次调用 `params.timeout_ms` > env `AI_BROWSER_ACTION_TIMEOUT_MS` >
59
+ * 按动作自报耗时推算。三者都是显式逃生门:agent 知道自己要 type 3000 字时,
60
+ * 不该被一个猜出来的默认值卡死。
61
+ */
62
+ export function actionDeadlineMs(action, params = {}) {
63
+ const p = params || {};
64
+ let need = DEFAULT_ACTION_DEADLINE_MS;
65
+
66
+ const text = String(p.text || '');
67
+ if (action === 'type' || action === 'setContent') {
68
+ const iv = Number(p.interval_ms) > 0 ? Number(p.interval_ms) : 60;
69
+ need = Math.max(need, text.length * iv + 5000);
70
+ } else if (action === 'drag') {
71
+ const dur = Number(p.duration_ms) > 0 ? Number(p.duration_ms) : 420;
72
+ const steps = Number(p.steps) > 0 ? Number(p.steps) : 14;
73
+ need = Math.max(need, dur + steps * 40 + 5000);
74
+ } else if (action === 'press') {
75
+ const rep = Number(p.repeat) > 0 ? Number(p.repeat) : 1;
76
+ const iv = Number(p.interval_ms) > 0 ? Number(p.interval_ms) : 60;
77
+ need = Math.max(need, rep * iv + 5000);
78
+ } else if (action === 'clear') {
79
+ // clear 要走编辑器状态提交与回读校验(多处 80~150ms 等待)。
80
+ need = Math.max(need, 20000);
81
+ }
82
+
83
+ const env = Number(process.env.AI_BROWSER_ACTION_TIMEOUT_MS);
84
+ if (Number.isFinite(env) && env > 0) need = env;
85
+ const perCall = Number(p.timeout_ms);
86
+ if (Number.isFinite(perCall) && perCall > 0) need = perCall;
87
+
88
+ return Math.min(Math.max(need, 1000), MAX_ACTION_DEADLINE_MS);
89
+ }
@@ -0,0 +1,17 @@
1
+ // shared/guards.js — Centralized eval-safety guard, single source of truth.
2
+ // Used by BOTH the raw WebSocket path (ws_server.js `ui.evaluate`) and the MCP
3
+ // path (mcp_server.js `browse_evaluate`). Keeping the limits + blocklist in one
4
+ // place prevents one entry being hardened and the other silently left open.
5
+
6
+ export const EVAL_MAX_LENGTH = 5000;
7
+ const NODE_IDENTIFIER_RE = /\bprocess\.\b|\brequire\s*\(|\bchild_process\b|\bglobalThis\.process\b/;
8
+
9
+ // Returns null when the script is safe to run, or the error message that
10
+ // should be returned to the caller. Message text is part of the contract —
11
+ // external ws/mcp clients may match on it, so keep it stable.
12
+ export function evaluateGuardError(js) {
13
+ if (typeof js !== 'string') return 'script must be a string';
14
+ if (js.length > EVAL_MAX_LENGTH) return `script exceeds ${EVAL_MAX_LENGTH} char limit`;
15
+ if (NODE_IDENTIFIER_RE.test(js)) return 'script contains disallowed Node-specific identifier';
16
+ return null;
17
+ }
@@ -0,0 +1,116 @@
1
+ // shared/protocol.js — JSON-RPC 2.0 subset for AI Browser
2
+ // Used by both main process (WS server) and preload (IPC bridge)
3
+
4
+ /**
5
+ * Create a JSON-RPC request string
6
+ */
7
+ export function createRequest(id, method, params = {}) {
8
+ return JSON.stringify({ jsonrpc: '2.0', id, method, params });
9
+ }
10
+
11
+ /**
12
+ * Create a JSON-RPC success response string
13
+ */
14
+ export function createResponse(id, result) {
15
+ return JSON.stringify({ jsonrpc: '2.0', id, result });
16
+ }
17
+
18
+ /**
19
+ * Create a JSON-RPC error response string
20
+ */
21
+ export function createError(id, code, message, data = null) {
22
+ const error = { code, message };
23
+ if (data) error.data = data;
24
+ return JSON.stringify({ jsonrpc: '2.0', id, error });
25
+ }
26
+
27
+ /**
28
+ * Create a server-pushed event (no id — notification per JSON-RPC)
29
+ */
30
+ export function createEvent(event, data) {
31
+ return JSON.stringify({ jsonrpc: '2.0', method: event, params: data });
32
+ }
33
+
34
+ /**
35
+ * Parse a JSON-RPC message string into object
36
+ */
37
+ export function parseMessage(msg) {
38
+ try {
39
+ const obj = JSON.parse(msg);
40
+ if (obj.jsonrpc !== '2.0') {
41
+ throw new Error('Invalid JSON-RPC version');
42
+ }
43
+ return obj;
44
+ } catch (e) {
45
+ throw new Error(`Failed to parse JSON-RPC message: ${e.message}`);
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Every ui.* method the WebSocket server actually dispatches.
51
+ *
52
+ * This list had drifted badly: it named 7 methods while the server served 28,
53
+ * so anyone reading it would conclude most of the API did not exist. A
54
+ * machine-readable contract that lies is worse than no contract — so
55
+ * `tests/test_protocol.js` now derives the real set from the `case 'ui.x':`
56
+ * labels in `src/main/ws_server.js` and fails if the two diverge. Add a method
57
+ * to the server and this list must follow, or the suite breaks.
58
+ */
59
+ export const VALID_METHODS = [
60
+ 'ui.get_tree',
61
+ 'ui.act',
62
+ 'ui.navigate',
63
+ 'ui.repaint',
64
+ 'ui.snapshot',
65
+ 'ui.diff',
66
+ 'ui.capabilities',
67
+ 'ui.evaluate',
68
+ 'ui.subscribe',
69
+ 'ui.unsubscribe',
70
+ 'ui.downloads',
71
+ 'ui.get_focused',
72
+ 'ui.peek',
73
+ 'ui.wait',
74
+ 'ui.scroll',
75
+ 'ui.new_tab',
76
+ 'ui.close_tab',
77
+ 'ui.list_tabs',
78
+ 'ui.set_active_tab',
79
+ 'ui.canvas_configure',
80
+ 'ui.canvas_list',
81
+ 'ui.canvas_read',
82
+ 'ui.canvas_capture',
83
+ 'ui.network_list',
84
+ 'ui.network_get',
85
+ 'ui.network_clear',
86
+ 'ui.network_configure',
87
+ 'ui.network_body',
88
+ 'ui.quit',
89
+ ];
90
+
91
+ /**
92
+ * Standard JSON-RPC error codes
93
+ */
94
+ export const ERROR_CODES = {
95
+ PARSE_ERROR: -32700,
96
+ INVALID_REQUEST: -32600,
97
+ METHOD_NOT_FOUND: -32601,
98
+ INVALID_PARAMS: -32602,
99
+ INTERNAL_ERROR: -32603,
100
+ ACTION_FAILED: -32001,
101
+ TARGET_NOT_FOUND: -32002,
102
+ INVALID_ACTION: -32003,
103
+ };
104
+
105
+ // For CJS compatibility in Electron preload
106
+ if (typeof module !== 'undefined' && module.exports) {
107
+ module.exports = {
108
+ createRequest,
109
+ createResponse,
110
+ createError,
111
+ createEvent,
112
+ parseMessage,
113
+ VALID_METHODS,
114
+ ERROR_CODES,
115
+ };
116
+ }