@web-auto/camo 0.1.18 → 0.1.20
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/README.md +18 -19
- package/bin/browser-service.mjs +11 -0
- package/package.json +7 -2
- package/scripts/install.mjs +3 -3
- package/src/cli.mjs +8 -5
- package/src/commands/attach.mjs +141 -0
- package/src/commands/browser.mjs +5 -16
- package/src/commands/mouse.mjs +2 -12
- package/src/container/runtime-core/operations/index.mjs +6 -15
- package/src/container/subscription-registry.mjs +6 -6
- package/src/core/actions.mjs +0 -12
- package/src/core/index.mjs +0 -1
- package/src/lifecycle/lock.mjs +7 -3
- package/src/services/browser-service/index.js +651 -0
- package/src/services/browser-service/index.js.map +1 -0
- package/src/services/browser-service/internal/BrowserSession.input.test.js +322 -0
- package/src/services/browser-service/internal/BrowserSession.input.test.js.map +1 -0
- package/src/services/browser-service/internal/BrowserSession.js +304 -0
- package/src/services/browser-service/internal/BrowserSession.js.map +1 -0
- package/src/services/browser-service/internal/ElementRegistry.js +61 -0
- package/src/services/browser-service/internal/ElementRegistry.js.map +1 -0
- package/src/services/browser-service/internal/ProfileLock.js +85 -0
- package/src/services/browser-service/internal/ProfileLock.js.map +1 -0
- package/src/services/browser-service/internal/SessionManager.js +184 -0
- package/src/services/browser-service/internal/SessionManager.js.map +1 -0
- package/src/services/browser-service/internal/SessionManager.test.js +40 -0
- package/src/services/browser-service/internal/SessionManager.test.js.map +1 -0
- package/src/services/browser-service/internal/browser-session/cookies.js +145 -0
- package/src/services/browser-service/internal/browser-session/cookies.js.map +1 -0
- package/src/services/browser-service/internal/browser-session/input-ops.js +127 -0
- package/src/services/browser-service/internal/browser-session/input-ops.js.map +1 -0
- package/src/services/browser-service/internal/browser-session/input-pipeline.js +133 -0
- package/src/services/browser-service/internal/browser-session/input-pipeline.js.map +1 -0
- package/src/services/browser-service/internal/browser-session/logging.js +46 -0
- package/src/services/browser-service/internal/browser-session/navigation.js +39 -0
- package/src/services/browser-service/internal/browser-session/navigation.js.map +1 -0
- package/src/services/browser-service/internal/browser-session/page-hooks.js +443 -0
- package/src/services/browser-service/internal/browser-session/page-hooks.js.map +1 -0
- package/src/services/browser-service/internal/browser-session/page-management.js +212 -0
- package/src/services/browser-service/internal/browser-session/page-management.js.map +1 -0
- package/src/services/browser-service/internal/browser-session/recording.js +199 -0
- package/src/services/browser-service/internal/browser-session/recording.js.map +1 -0
- package/src/services/browser-service/internal/browser-session/runtime-events.js +62 -0
- package/src/services/browser-service/internal/browser-session/runtime-events.js.map +1 -0
- package/src/services/browser-service/internal/browser-session/session-core.js +85 -0
- package/src/services/browser-service/internal/browser-session/session-core.js.map +1 -0
- package/src/services/browser-service/internal/browser-session/session-state.js +39 -0
- package/src/services/browser-service/internal/browser-session/session-state.js.map +1 -0
- package/src/services/browser-service/internal/browser-session/types.js +15 -0
- package/src/services/browser-service/internal/browser-session/types.js.map +1 -0
- package/src/services/browser-service/internal/browser-session/utils.js +69 -0
- package/src/services/browser-service/internal/browser-session/utils.js.map +1 -0
- package/src/services/browser-service/internal/browser-session/viewport-manager.js +47 -0
- package/src/services/browser-service/internal/browser-session/viewport-manager.js.map +1 -0
- package/src/services/browser-service/internal/browser-session/viewport.js +216 -0
- package/src/services/browser-service/internal/browser-session/viewport.js.map +1 -0
- package/src/services/browser-service/internal/container-matcher.js +852 -0
- package/src/services/browser-service/internal/container-matcher.js.map +1 -0
- package/src/services/browser-service/internal/container-registry.js +182 -0
- package/src/services/browser-service/internal/engine-manager.js +259 -0
- package/src/services/browser-service/internal/engine-manager.js.map +1 -0
- package/src/services/browser-service/internal/fingerprint.js +203 -0
- package/src/services/browser-service/internal/fingerprint.js.map +1 -0
- package/src/services/browser-service/internal/heartbeat.js +137 -0
- package/src/services/browser-service/internal/logging.js +46 -0
- package/src/services/browser-service/internal/page-runtime/runtime.js +1317 -0
- package/src/services/browser-service/internal/pageRuntime.js +29 -0
- package/src/services/browser-service/internal/pageRuntime.js.map +1 -0
- package/src/services/browser-service/internal/runtimeInjector.js +31 -0
- package/src/services/browser-service/internal/runtimeInjector.js.map +1 -0
- package/src/services/browser-service/internal/service-process-logger.js +140 -0
- package/src/services/browser-service/internal/state-bus.js +46 -0
- package/src/services/browser-service/internal/state-bus.js.map +1 -0
- package/src/services/browser-service/internal/storage-paths.js +42 -0
- package/src/services/browser-service/internal/storage-paths.js.map +1 -0
- package/src/services/browser-service/internal/ws-server.js +1194 -0
- package/src/services/browser-service/internal/ws-server.js.map +1 -0
- package/src/services/browser-service/internal/ws-server.test.js +59 -0
- package/src/services/browser-service/internal/ws-server.test.js.map +1 -0
- package/src/services/browser-service/server.mjs +6 -0
- package/src/services/controller/cli-bridge.js +93 -0
- package/src/services/controller/container-index.js +50 -0
- package/src/services/controller/container-storage.js +36 -0
- package/src/services/controller/controller-actions.js +207 -0
- package/src/services/controller/controller.js +1138 -0
- package/src/services/controller/selectors.js +54 -0
- package/src/services/controller/transport.js +118 -0
- package/src/utils/browser-service.mjs +100 -125
- package/src/utils/config.mjs +22 -21
- package/src/utils/help.mjs +11 -9
- package/src/utils/ws-client.mjs +30 -0
|
@@ -0,0 +1,1138 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import { loadContainerIndex } from './container-index.js';
|
|
4
|
+
import { createContainerActionHandlers } from './controller-actions.js';
|
|
5
|
+
import { createTransport } from './transport.js';
|
|
6
|
+
import { createCliBridge } from './cli-bridge.js';
|
|
7
|
+
|
|
8
|
+
function normalizeInputMode(mode) {
|
|
9
|
+
const raw = String(mode || '').trim().toLowerCase();
|
|
10
|
+
return raw === 'protocol' ? 'protocol' : 'system';
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export class UiController {
|
|
14
|
+
constructor(options = {}) {
|
|
15
|
+
this.repoRoot = options.repoRoot || process.cwd();
|
|
16
|
+
this.messageBus = options.messageBus;
|
|
17
|
+
// 外置容器树:统一使用 ~/.camo/container-lib
|
|
18
|
+
this.userContainerRoot =
|
|
19
|
+
options.userContainerRoot || path.join(os.homedir(), '.camo', 'container-lib');
|
|
20
|
+
this.containerIndexPath = options.containerIndexPath || process.env.CAMO_CONTAINER_INDEX || '';
|
|
21
|
+
this.cliTargets = options.cliTargets || {};
|
|
22
|
+
this.defaultWsHost = options.defaultWsHost || '127.0.0.1';
|
|
23
|
+
this.defaultWsPort = Number(options.defaultWsPort || 8765);
|
|
24
|
+
this.defaultHttpHost = options.defaultHttpHost || '127.0.0.1';
|
|
25
|
+
this.defaultHttpPort = Number(options.defaultHttpPort || 7704);
|
|
26
|
+
this.defaultHttpProtocol = options.defaultHttpProtocol || 'http';
|
|
27
|
+
this.inputMode = normalizeInputMode(process.env.CAMO_INPUT_MODE);
|
|
28
|
+
this.errorHandler = null;
|
|
29
|
+
this.containerIndex = null;
|
|
30
|
+
this.containerActions = this.buildContainerActions();
|
|
31
|
+
this.transport = createTransport({
|
|
32
|
+
env: process.env,
|
|
33
|
+
defaults: {
|
|
34
|
+
wsHost: this.defaultWsHost,
|
|
35
|
+
wsPort: this.defaultWsPort,
|
|
36
|
+
httpHost: this.defaultHttpHost,
|
|
37
|
+
httpPort: this.defaultHttpPort,
|
|
38
|
+
httpProtocol: this.defaultHttpProtocol,
|
|
39
|
+
},
|
|
40
|
+
debugLog: (label, data) => this.debugLog(label, data),
|
|
41
|
+
});
|
|
42
|
+
this.cliBridge = createCliBridge({
|
|
43
|
+
cliTargets: this.cliTargets,
|
|
44
|
+
repoRoot: this.repoRoot,
|
|
45
|
+
env: process.env,
|
|
46
|
+
logger: {
|
|
47
|
+
info: (label, data) => this.debugLog(label, data),
|
|
48
|
+
warn: (label, data) => this.debugLog(label, data),
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
buildContainerActions() {
|
|
54
|
+
return createContainerActionHandlers({
|
|
55
|
+
userContainerRoot: this.userContainerRoot,
|
|
56
|
+
errorHandler: this.errorHandler,
|
|
57
|
+
getContainerIndex: () => this.getContainerIndex(),
|
|
58
|
+
fetchInspectorSnapshot: (opts) => this.captureInspectorSnapshot(opts),
|
|
59
|
+
fetchInspectorBranch: (opts) => this.captureInspectorBranch(opts),
|
|
60
|
+
fetchContainerMatch: (payload) => this.handleContainerMatchCore(payload),
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
getContainerIndex() {
|
|
65
|
+
if (!this.containerIndexPath) {
|
|
66
|
+
throw new Error('CAMO_CONTAINER_INDEX is required for container actions.');
|
|
67
|
+
}
|
|
68
|
+
if (!this.containerIndex) {
|
|
69
|
+
this.containerIndex = loadContainerIndex(this.containerIndexPath, this.errorHandler);
|
|
70
|
+
}
|
|
71
|
+
return this.containerIndex || {};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
debugLog(label, data) {
|
|
75
|
+
if (process.env.DEBUG !== '1' && process.env.CAMO_DEBUG !== '1') return;
|
|
76
|
+
try {
|
|
77
|
+
const safe = JSON.stringify(data);
|
|
78
|
+
// Keep logs single-line JSON for grepability.
|
|
79
|
+
console.log(`[ui-controller:${label}] ${safe}`);
|
|
80
|
+
} catch {
|
|
81
|
+
console.log(`[ui-controller:${label}]`, data);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async handleAction(action, payload = {}) {
|
|
86
|
+
const startedAt = Date.now();
|
|
87
|
+
const profileId = (payload.profileId || payload.profile || payload.sessionId || '').toString();
|
|
88
|
+
this.debugLog('action:start', { action, profileId });
|
|
89
|
+
switch (action) {
|
|
90
|
+
case 'browser:status':
|
|
91
|
+
return this.fetchBrowserStatus();
|
|
92
|
+
case 'session:list':
|
|
93
|
+
return this.runCliCommand('session-manager', ['list']);
|
|
94
|
+
case 'session:create':
|
|
95
|
+
return this.handleSessionCreate(payload);
|
|
96
|
+
case 'session:delete':
|
|
97
|
+
return this.handleSessionDelete(payload);
|
|
98
|
+
case 'logs:stream':
|
|
99
|
+
return this.handleLogsStream(payload);
|
|
100
|
+
case 'operations:list':
|
|
101
|
+
return this.runCliCommand('operations', ['list']);
|
|
102
|
+
case 'operations:run':
|
|
103
|
+
return this.handleOperationRun(payload);
|
|
104
|
+
case 'containers:inspect':
|
|
105
|
+
return this.handleContainerInspect(payload);
|
|
106
|
+
case 'containers:inspect-container':
|
|
107
|
+
return this.handleContainerInspectContainer(payload);
|
|
108
|
+
case 'containers:inspect-branch':
|
|
109
|
+
return this.handleContainerInspectBranch(payload);
|
|
110
|
+
case 'containers:remap':
|
|
111
|
+
return this.handleContainerRemap(payload);
|
|
112
|
+
case 'containers:create-child':
|
|
113
|
+
return this.handleContainerCreateChild(payload);
|
|
114
|
+
case 'containers:update-alias':
|
|
115
|
+
return this.handleContainerUpdateAlias(payload);
|
|
116
|
+
case 'containers:update-operations':
|
|
117
|
+
return this.handleContainerUpdateOperations(payload);
|
|
118
|
+
case 'browser:highlight':
|
|
119
|
+
return this.handleBrowserHighlight(payload);
|
|
120
|
+
case 'browser:clear-highlight':
|
|
121
|
+
return this.handleBrowserClearHighlight(payload);
|
|
122
|
+
case 'browser:execute':
|
|
123
|
+
return this.handleBrowserExecute(payload);
|
|
124
|
+
case 'browser:screenshot':
|
|
125
|
+
return this.handleBrowserScreenshot(payload);
|
|
126
|
+
case 'browser:page:list':
|
|
127
|
+
return this.handleBrowserPageList(payload);
|
|
128
|
+
case 'browser:page:new':
|
|
129
|
+
return this.handleBrowserPageNew(payload);
|
|
130
|
+
case 'browser:page:switch':
|
|
131
|
+
return this.handleBrowserPageSwitch(payload);
|
|
132
|
+
case 'browser:page:close':
|
|
133
|
+
return this.handleBrowserPageClose(payload);
|
|
134
|
+
case 'browser:goto':
|
|
135
|
+
return this.handleBrowserGoto(payload);
|
|
136
|
+
case 'browser:highlight-dom-path':
|
|
137
|
+
return this.handleBrowserHighlightDomPath(payload);
|
|
138
|
+
case 'browser:cancel-pick':
|
|
139
|
+
return this.handleBrowserCancelDomPick(payload);
|
|
140
|
+
case 'browser:pick-dom':
|
|
141
|
+
return this.handleBrowserPickDom(payload);
|
|
142
|
+
case 'keyboard:press':
|
|
143
|
+
return this.handleKeyboardPress(payload);
|
|
144
|
+
case 'keyboard:type':
|
|
145
|
+
return this.handleKeyboardType(payload);
|
|
146
|
+
case 'system:shortcut':
|
|
147
|
+
return this.handleSystemShortcut(payload);
|
|
148
|
+
case 'system:input-mode:set':
|
|
149
|
+
return this.handleSystemInputModeSet(payload);
|
|
150
|
+
case 'system:input-mode:get':
|
|
151
|
+
return this.handleSystemInputModeGet();
|
|
152
|
+
case 'mouse:wheel':
|
|
153
|
+
return this.handleMouseWheel(payload);
|
|
154
|
+
case 'mouse:click':
|
|
155
|
+
return this.handleMouseClick(payload);
|
|
156
|
+
case 'dom:branch:2':
|
|
157
|
+
return this.handleDomBranch2(payload);
|
|
158
|
+
case 'dom:pick:2':
|
|
159
|
+
return this.handleDomPick2(payload);
|
|
160
|
+
case 'browser:inspect_tree':
|
|
161
|
+
return this.fetchInspectTree(payload);
|
|
162
|
+
case 'containers:match':
|
|
163
|
+
return this.handleContainerMatch(payload);
|
|
164
|
+
case 'container:operation':
|
|
165
|
+
return this.handleContainerOperation(payload);
|
|
166
|
+
default:
|
|
167
|
+
return { success: false, error: `Unknown action: ${action}` };
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async runWithTrace(label, fn) {
|
|
172
|
+
const startedAt = Date.now();
|
|
173
|
+
try {
|
|
174
|
+
const res = await fn();
|
|
175
|
+
this.debugLog(`${label}:ok`, { ms: Date.now() - startedAt });
|
|
176
|
+
return res;
|
|
177
|
+
} catch (err) {
|
|
178
|
+
this.debugLog(`${label}:err`, { ms: Date.now() - startedAt, error: err?.message || String(err) });
|
|
179
|
+
throw err;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async fetchBrowserStatus() {
|
|
184
|
+
try {
|
|
185
|
+
const url = `${this.getBrowserHttpBase()}/health`;
|
|
186
|
+
const res = await fetch(url);
|
|
187
|
+
if (!res.ok) return { success: false, error: `HTTP ${res.status}` };
|
|
188
|
+
return { success: true, data: await res.json() };
|
|
189
|
+
} catch (err) {
|
|
190
|
+
return { success: false, error: err.message };
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async fetchInspectTree(payload) {
|
|
195
|
+
try {
|
|
196
|
+
const wsUrl = this.getBrowserWsUrl();
|
|
197
|
+
const command = {
|
|
198
|
+
type: 'command',
|
|
199
|
+
session_id: payload?.profile || 'default',
|
|
200
|
+
data: {
|
|
201
|
+
command_type: 'container_operation',
|
|
202
|
+
action: 'inspect_tree',
|
|
203
|
+
page_context: { url: payload?.url || 'https://example.com' },
|
|
204
|
+
parameters: {
|
|
205
|
+
...(payload?.rootSelector ? { root_selector: payload.rootSelector } : {}),
|
|
206
|
+
...(typeof payload?.maxDepth === 'number' ? { max_depth: payload.maxDepth } : {}),
|
|
207
|
+
...(typeof payload?.maxChildren === 'number' ? { max_children: payload.maxChildren } : {}),
|
|
208
|
+
},
|
|
209
|
+
},
|
|
210
|
+
};
|
|
211
|
+
const wsResult = await this.sendWsCommand(wsUrl, command, 15000);
|
|
212
|
+
if (wsResult?.data?.success !== true) {
|
|
213
|
+
return { success: false, error: wsResult?.data?.error || 'inspect_tree failed' };
|
|
214
|
+
}
|
|
215
|
+
return { success: true, data: wsResult.data.data };
|
|
216
|
+
} catch (err) {
|
|
217
|
+
return { success: false, error: err.message };
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async handleSessionCreate(payload = {}) {
|
|
222
|
+
if (!payload.profile) {
|
|
223
|
+
throw new Error('缺少 profile');
|
|
224
|
+
}
|
|
225
|
+
const args = ['create', '--profile', payload.profile];
|
|
226
|
+
if (payload.url) args.push('--url', payload.url);
|
|
227
|
+
if (payload.headless !== undefined) args.push('--headless', String(payload.headless));
|
|
228
|
+
if (payload.keepOpen !== undefined) args.push('--keep-open', String(payload.keepOpen));
|
|
229
|
+
return this.runCliCommand('session-manager', args);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async handleSessionDelete(payload = {}) {
|
|
233
|
+
if (!payload.profile) {
|
|
234
|
+
throw new Error('缺少 profile');
|
|
235
|
+
}
|
|
236
|
+
return this.runCliCommand('session-manager', ['delete', '--profile', payload.profile]);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async handleLogsStream(payload = {}) {
|
|
240
|
+
const args = ['stream'];
|
|
241
|
+
if (payload.source) args.push('--source', payload.source);
|
|
242
|
+
if (payload.session) args.push('--session', payload.session);
|
|
243
|
+
if (payload.lines) args.push('--lines', String(payload.lines));
|
|
244
|
+
return this.runCliCommand('logging', args);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
async handleOperationRun(payload = {}) {
|
|
248
|
+
const op = payload.op || payload.operation || payload.id;
|
|
249
|
+
if (!op) throw new Error('缺少操作 ID');
|
|
250
|
+
const args = ['run', '--op', op];
|
|
251
|
+
if (payload.config) {
|
|
252
|
+
args.push('--config', JSON.stringify(payload.config));
|
|
253
|
+
}
|
|
254
|
+
return this.runCliCommand('operations', args);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
async handleContainerInspect(payload = {}) {
|
|
258
|
+
return this.containerActions.handleContainerInspect(payload);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
async handleContainerInspectContainer(payload = {}) {
|
|
262
|
+
return this.containerActions.handleContainerInspectContainer(payload);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
async handleContainerInspectBranch(payload = {}) {
|
|
266
|
+
return this.containerActions.handleContainerInspectBranch(payload);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
async handleContainerRemap(payload = {}) {
|
|
270
|
+
return this.containerActions.handleContainerRemap(payload);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
async handleContainerCreateChild(payload = {}) {
|
|
274
|
+
return this.containerActions.handleContainerCreateChild(payload);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
async handleContainerUpdateAlias(payload = {}) {
|
|
278
|
+
return this.containerActions.handleContainerUpdateAlias(payload);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
async handleContainerUpdateOperations(payload = {}) {
|
|
282
|
+
return this.containerActions.handleContainerUpdateOperations(payload);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
async handleContainerMatch(payload = {}) {
|
|
286
|
+
return this.containerActions.handleContainerMatch(payload);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
async handleContainerMatchCore(payload = {}) {
|
|
290
|
+
const profile = payload.profileId || payload.profile;
|
|
291
|
+
const url = payload.url;
|
|
292
|
+
if (!profile) throw new Error('缺少 profile');
|
|
293
|
+
if (!url) throw new Error('缺少 URL');
|
|
294
|
+
try {
|
|
295
|
+
const context = await this.captureInspectorSnapshot({
|
|
296
|
+
profile,
|
|
297
|
+
url,
|
|
298
|
+
maxDepth: payload.maxDepth || 2,
|
|
299
|
+
maxChildren: payload.maxChildren || 5,
|
|
300
|
+
rootSelector: payload.rootSelector,
|
|
301
|
+
});
|
|
302
|
+
const snapshot = context.snapshot;
|
|
303
|
+
const rootContainer = snapshot?.root_match?.container || snapshot?.container_tree?.container || snapshot?.container_tree?.containers?.[0];
|
|
304
|
+
const matchPayload = {
|
|
305
|
+
sessionId: context.sessionId,
|
|
306
|
+
profileId: context.profileId,
|
|
307
|
+
url: context.targetUrl,
|
|
308
|
+
matched: !!rootContainer,
|
|
309
|
+
container: rootContainer || null,
|
|
310
|
+
snapshot,
|
|
311
|
+
};
|
|
312
|
+
this.messageBus?.publish?.('containers.matched', matchPayload);
|
|
313
|
+
this.messageBus?.publish?.('handshake.status', {
|
|
314
|
+
status: matchPayload.matched ? 'ready' : 'pending',
|
|
315
|
+
profileId: matchPayload.profileId,
|
|
316
|
+
sessionId: matchPayload.sessionId,
|
|
317
|
+
url: matchPayload.url,
|
|
318
|
+
matched: matchPayload.matched,
|
|
319
|
+
containerId: matchPayload.container?.id || null,
|
|
320
|
+
source: 'containers:match',
|
|
321
|
+
ts: Date.now(),
|
|
322
|
+
});
|
|
323
|
+
return { success: true, data: matchPayload };
|
|
324
|
+
} catch (err) {
|
|
325
|
+
throw new Error(`容器匹配失败: ${err?.message || String(err)}`);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
async handleContainerOperation(payload = {}) {
|
|
330
|
+
const containerId = payload.containerId || payload.id;
|
|
331
|
+
const operationId = payload.operationId;
|
|
332
|
+
const sessionId =
|
|
333
|
+
payload.profile ||
|
|
334
|
+
payload.profileId ||
|
|
335
|
+
payload.profile_id ||
|
|
336
|
+
payload.sessionId ||
|
|
337
|
+
payload.session_id;
|
|
338
|
+
|
|
339
|
+
if (!containerId) {
|
|
340
|
+
return { success: false, error: 'Missing containerId' };
|
|
341
|
+
}
|
|
342
|
+
if (!operationId) {
|
|
343
|
+
return { success: false, error: 'Missing operationId' };
|
|
344
|
+
}
|
|
345
|
+
if (!sessionId) {
|
|
346
|
+
return { success: false, error: 'Missing sessionId/profile' };
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const port = process.env.CAMO_UNIFIED_PORT || 7701;
|
|
350
|
+
const host = '127.0.0.1';
|
|
351
|
+
|
|
352
|
+
try {
|
|
353
|
+
const mergedConfig = { ...(payload.config || {}) };
|
|
354
|
+
if (['click', 'type', 'key', 'scroll'].includes(String(operationId))) {
|
|
355
|
+
if (this.inputMode === 'protocol') {
|
|
356
|
+
mergedConfig.useSystemMouse = false;
|
|
357
|
+
} else if (typeof mergedConfig.useSystemMouse !== 'boolean') {
|
|
358
|
+
mergedConfig.useSystemMouse = true;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const response = await fetch(`http://${host}:${port}/v1/container/${containerId}/execute`, {
|
|
363
|
+
method: 'POST',
|
|
364
|
+
headers: { 'Content-Type': 'application/json' },
|
|
365
|
+
body: JSON.stringify({
|
|
366
|
+
operationId,
|
|
367
|
+
config: mergedConfig,
|
|
368
|
+
sessionId,
|
|
369
|
+
}),
|
|
370
|
+
});
|
|
371
|
+
if (!response.ok) {
|
|
372
|
+
return { success: false, error: await response.text() };
|
|
373
|
+
}
|
|
374
|
+
return await response.json();
|
|
375
|
+
} catch (err) {
|
|
376
|
+
return { success: false, error: err?.message || String(err) };
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
async handleBrowserHighlight(payload = {}) {
|
|
381
|
+
const profile = payload.profile || payload.sessionId;
|
|
382
|
+
const selector = (payload.selector || '').trim();
|
|
383
|
+
if (!profile) {
|
|
384
|
+
throw new Error('缺少会话/ profile 信息');
|
|
385
|
+
}
|
|
386
|
+
if (!selector) {
|
|
387
|
+
throw new Error('缺少 selector');
|
|
388
|
+
}
|
|
389
|
+
const options = payload.options || {};
|
|
390
|
+
|
|
391
|
+
// 处理颜色
|
|
392
|
+
let style = options.style;
|
|
393
|
+
const color = payload.color;
|
|
394
|
+
if (!style) {
|
|
395
|
+
if (color === 'green') style = '2px solid rgba(76, 175, 80, 0.95)';
|
|
396
|
+
else if (color === 'blue') style = '2px solid rgba(33, 150, 243, 0.95)';
|
|
397
|
+
else if (color === 'red') style = '2px solid rgba(244, 67, 54, 0.95)';
|
|
398
|
+
else if (color && /^[a-z]+$/i.test(color)) style = `2px solid ${color}`;
|
|
399
|
+
else style = '2px solid rgba(255, 0, 0, 0.8)';
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const highlightOpts = {
|
|
403
|
+
style,
|
|
404
|
+
duration: options.duration,
|
|
405
|
+
channel: options.channel || payload.channel,
|
|
406
|
+
sticky: options.sticky,
|
|
407
|
+
maxMatches: options.maxMatches,
|
|
408
|
+
};
|
|
409
|
+
try {
|
|
410
|
+
const result = await this.sendHighlightViaWs(profile, selector, highlightOpts);
|
|
411
|
+
this.messageBus?.publish?.('ui.highlight.result', {
|
|
412
|
+
success: true,
|
|
413
|
+
selector,
|
|
414
|
+
source: result?.source || 'unknown',
|
|
415
|
+
details: result?.details || null,
|
|
416
|
+
});
|
|
417
|
+
return { success: true, data: result };
|
|
418
|
+
} catch (err) {
|
|
419
|
+
const errorMessage = err?.message || '高亮请求失败';
|
|
420
|
+
this.messageBus?.publish?.('ui.highlight.result', {
|
|
421
|
+
success: false,
|
|
422
|
+
selector,
|
|
423
|
+
error: errorMessage,
|
|
424
|
+
});
|
|
425
|
+
return { success: false, error: errorMessage };
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
async handleBrowserClearHighlight(payload = {}) {
|
|
431
|
+
const profile = payload.profile || payload.sessionId;
|
|
432
|
+
if (!profile) {
|
|
433
|
+
throw new Error('缺少会话/ profile 信息');
|
|
434
|
+
}
|
|
435
|
+
try {
|
|
436
|
+
const result = await this.sendClearHighlightViaWs(profile, payload.channel || payload.options?.channel || null);
|
|
437
|
+
this.messageBus?.publish?.('ui.highlight.result', {
|
|
438
|
+
success: true,
|
|
439
|
+
selector: null,
|
|
440
|
+
details: result,
|
|
441
|
+
});
|
|
442
|
+
return { success: true, data: result };
|
|
443
|
+
} catch (err) {
|
|
444
|
+
const message = err?.message || '清除高亮失败';
|
|
445
|
+
this.messageBus?.publish?.('ui.highlight.result', {
|
|
446
|
+
success: false,
|
|
447
|
+
selector: null,
|
|
448
|
+
error: message,
|
|
449
|
+
});
|
|
450
|
+
throw err;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
async handleBrowserHighlightDomPath(payload = {}) {
|
|
455
|
+
const profile = payload.profile || payload.sessionId;
|
|
456
|
+
const domPath = (payload.path || payload.domPath || payload.dom_path || '').trim();
|
|
457
|
+
if (!profile) {
|
|
458
|
+
throw new Error('缺少会话/ profile 信息');
|
|
459
|
+
}
|
|
460
|
+
if (!domPath) {
|
|
461
|
+
throw new Error('缺少 DOM 路径');
|
|
462
|
+
}
|
|
463
|
+
const options = payload.options || {};
|
|
464
|
+
const channel = options.channel || payload.channel || 'hover-dom';
|
|
465
|
+
const rootSelector = options.rootSelector || payload.rootSelector || payload.root_selector || null;
|
|
466
|
+
|
|
467
|
+
let style = options.style;
|
|
468
|
+
if (!style && payload.color) {
|
|
469
|
+
const color = payload.color;
|
|
470
|
+
if (color === 'green') style = '2px solid rgba(76, 175, 80, 0.95)';
|
|
471
|
+
else if (color === 'blue') style = '2px solid rgba(33, 150, 243, 0.95)';
|
|
472
|
+
else if (color === 'red') style = '2px solid rgba(244, 67, 54, 0.95)';
|
|
473
|
+
else if (color && /^[a-z]+$/i.test(color)) style = `2px solid ${color}`;
|
|
474
|
+
}
|
|
475
|
+
if (!style) {
|
|
476
|
+
style = '2px solid rgba(96, 165, 250, 0.95)';
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
const sticky = typeof options.sticky === 'boolean' ? options.sticky : true;
|
|
480
|
+
try {
|
|
481
|
+
const result = await this.sendHighlightDomPathViaWs(profile, domPath, {
|
|
482
|
+
channel,
|
|
483
|
+
style,
|
|
484
|
+
sticky,
|
|
485
|
+
duration: options.duration,
|
|
486
|
+
rootSelector,
|
|
487
|
+
});
|
|
488
|
+
this.messageBus?.publish?.('ui.highlight.result', {
|
|
489
|
+
success: true,
|
|
490
|
+
selector: null,
|
|
491
|
+
details: result?.details || null,
|
|
492
|
+
});
|
|
493
|
+
return { success: true, data: result };
|
|
494
|
+
} catch (err) {
|
|
495
|
+
const errorMessage = err?.message || 'DOM 路径高亮失败';
|
|
496
|
+
this.messageBus?.publish?.('ui.highlight.result', {
|
|
497
|
+
success: false,
|
|
498
|
+
selector: null,
|
|
499
|
+
error: errorMessage,
|
|
500
|
+
});
|
|
501
|
+
throw err;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
async handleBrowserExecute(payload = {}) {
|
|
506
|
+
const profile = payload.profile || payload.sessionId;
|
|
507
|
+
const script = payload.script || payload.code || '';
|
|
508
|
+
if (!profile) {
|
|
509
|
+
throw new Error('缺少会话/ profile 信息');
|
|
510
|
+
}
|
|
511
|
+
if (!script) {
|
|
512
|
+
throw new Error('缺少 script 参数');
|
|
513
|
+
}
|
|
514
|
+
try {
|
|
515
|
+
const result = await this.sendExecuteViaWs(profile, script);
|
|
516
|
+
return { success: true, data: result };
|
|
517
|
+
} catch (err) {
|
|
518
|
+
const errorMessage = err?.message || '执行脚本失败';
|
|
519
|
+
throw new Error(errorMessage);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
async handleBrowserScreenshot(payload = {}) {
|
|
524
|
+
const profileId = (payload.profileId || payload.profile || payload.sessionId || 'default').toString();
|
|
525
|
+
const fullPage = typeof payload.fullPage === 'boolean' ? payload.fullPage : Boolean(payload.fullPage);
|
|
526
|
+
const result = await this.browserServiceCommand('screenshot', { profileId, fullPage }, { timeoutMs: 60000 });
|
|
527
|
+
return { success: true, data: result };
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
async handleBrowserPageList(payload = {}) {
|
|
531
|
+
const profileId = (payload.profileId || payload.profile || payload.sessionId || 'default').toString();
|
|
532
|
+
const result = await this.browserServiceCommand('page:list', { profileId }, { timeoutMs: 30000 });
|
|
533
|
+
return { success: true, data: result };
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
async handleBrowserPageNew(payload = {}) {
|
|
537
|
+
const profileId = (payload.profileId || payload.profile || payload.sessionId || 'default').toString();
|
|
538
|
+
const url = payload.url ? String(payload.url) : undefined;
|
|
539
|
+
const result = await this.browserServiceCommand('page:new', { profileId, ...(url ? { url } : {}) }, { timeoutMs: 30000 });
|
|
540
|
+
const index = Number(result?.index ?? result?.data?.index);
|
|
541
|
+
if (Number.isFinite(index)) {
|
|
542
|
+
return { success: true, data: result };
|
|
543
|
+
}
|
|
544
|
+
const list = await this.browserServiceCommand('page:list', { profileId }, { timeoutMs: 30000 });
|
|
545
|
+
const activeIndexRaw = list?.activeIndex ?? list?.data?.activeIndex;
|
|
546
|
+
const activeIndex = Number(activeIndexRaw);
|
|
547
|
+
if (Number.isFinite(activeIndex)) {
|
|
548
|
+
return { success: true, data: { ...(result || {}), index: activeIndex, fallback: 'activeIndex' } };
|
|
549
|
+
}
|
|
550
|
+
return { success: true, data: result };
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
async handleBrowserPageSwitch(payload = {}) {
|
|
554
|
+
const profileId = (payload.profileId || payload.profile || payload.sessionId || 'default').toString();
|
|
555
|
+
const index = Number(payload.index);
|
|
556
|
+
if (!Number.isFinite(index)) throw new Error('index required');
|
|
557
|
+
const result = await this.browserServiceCommand('page:switch', { profileId, index }, { timeoutMs: 30000 });
|
|
558
|
+
return { success: true, data: result };
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
async handleBrowserPageClose(payload = {}) {
|
|
562
|
+
const profileId = (payload.profileId || payload.profile || payload.sessionId || 'default').toString();
|
|
563
|
+
const hasIndex = typeof payload.index !== 'undefined' && payload.index !== null;
|
|
564
|
+
const index = hasIndex ? Number(payload.index) : undefined;
|
|
565
|
+
const result = await this.browserServiceCommand(
|
|
566
|
+
'page:close',
|
|
567
|
+
{ profileId, ...(Number.isFinite(index) ? { index } : {}) },
|
|
568
|
+
{ timeoutMs: 30000 },
|
|
569
|
+
);
|
|
570
|
+
return { success: true, data: result };
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
async handleBrowserGoto(payload = {}) {
|
|
574
|
+
const profileId = (payload.profileId || payload.profile || payload.sessionId || 'default').toString();
|
|
575
|
+
const url = (payload.url || '').toString();
|
|
576
|
+
if (!url) throw new Error('url required');
|
|
577
|
+
const result = await this.browserServiceCommand('goto', { profileId, url });
|
|
578
|
+
return { success: true, data: result };
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
async handleKeyboardPress(payload = {}) {
|
|
582
|
+
const profileId = (payload.profileId || payload.profile || payload.sessionId || 'default').toString();
|
|
583
|
+
const key = (payload.key || 'Enter').toString();
|
|
584
|
+
const delay = typeof payload.delay === 'number' ? payload.delay : undefined;
|
|
585
|
+
const result = await this.browserServiceCommand('keyboard:press', { profileId, key, ...(delay ? { delay } : {}) });
|
|
586
|
+
return { success: true, data: result };
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
async handleKeyboardType(payload = {}) {
|
|
590
|
+
const profileId = (payload.profileId || payload.profile || payload.sessionId || 'default').toString();
|
|
591
|
+
const text = (payload.text ?? '').toString();
|
|
592
|
+
const delay = typeof payload.delay === 'number' ? payload.delay : undefined;
|
|
593
|
+
const submit = typeof payload.submit === 'boolean' ? payload.submit : Boolean(payload.submit);
|
|
594
|
+
const result = await this.browserServiceCommand(
|
|
595
|
+
'keyboard:type',
|
|
596
|
+
{ profileId, text, ...(delay ? { delay } : {}), ...(submit ? { submit } : {}) },
|
|
597
|
+
);
|
|
598
|
+
return { success: true, data: result };
|
|
599
|
+
}
|
|
600
|
+
async handleSystemShortcut(payload = {}) {
|
|
601
|
+
const shortcut = String(payload.shortcut || '').trim();
|
|
602
|
+
const app = String(payload.app || 'camoufox').trim();
|
|
603
|
+
if (!shortcut) throw new Error('shortcut required');
|
|
604
|
+
|
|
605
|
+
if (process.platform === 'darwin') {
|
|
606
|
+
const { spawnSync } = await import('node:child_process');
|
|
607
|
+
spawnSync('osascript', ['-e', `tell application "${app}" to activate`]);
|
|
608
|
+
if (shortcut === 'new-tab') {
|
|
609
|
+
const res = spawnSync('osascript', [
|
|
610
|
+
'-e',
|
|
611
|
+
'tell application "System Events" to keystroke "t" using command down'
|
|
612
|
+
]);
|
|
613
|
+
if (res.status != 0) throw new Error('osascript new-tab failed');
|
|
614
|
+
return { success: true, data: { ok: true, shortcut } };
|
|
615
|
+
}
|
|
616
|
+
throw new Error(`unsupported shortcut: ${shortcut}`);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
if (process.platform === 'win32') {
|
|
620
|
+
const { spawnSync } = await import('node:child_process');
|
|
621
|
+
if (shortcut === 'new-tab') {
|
|
622
|
+
const script =
|
|
623
|
+
'Add-Type -AssemblyName System.Windows.Forms; $ws = New-Object -ComObject WScript.Shell; $ws.SendKeys("^t");';
|
|
624
|
+
const res = spawnSync('powershell', ['-NoProfile', '-Command', script], { windowsHide: true });
|
|
625
|
+
if (res.status != 0) throw new Error('powershell new-tab failed');
|
|
626
|
+
return { success: true, data: { ok: true, shortcut } };
|
|
627
|
+
}
|
|
628
|
+
throw new Error(`unsupported shortcut: ${shortcut}`);
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
throw new Error('unsupported platform');
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
async handleSystemInputModeSet(payload = {}) {
|
|
635
|
+
const mode = normalizeInputMode(payload.mode);
|
|
636
|
+
this.inputMode = mode;
|
|
637
|
+
process.env.CAMO_INPUT_MODE = mode;
|
|
638
|
+
return { success: true, data: { mode } };
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
async handleSystemInputModeGet() {
|
|
642
|
+
return { success: true, data: { mode: this.inputMode } };
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
|
|
646
|
+
async handleMouseWheel(payload = {}) {
|
|
647
|
+
const profileId = (payload.profileId || payload.profile || payload.sessionId || 'default').toString();
|
|
648
|
+
const deltaY = Number(payload.deltaY);
|
|
649
|
+
const deltaX = Number(payload.deltaX);
|
|
650
|
+
const result = await this.browserServiceCommand('mouse:wheel', { profileId, deltaY, deltaX });
|
|
651
|
+
return { success: true, data: result };
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
async handleMouseClick(payload = {}) {
|
|
655
|
+
const profileId = (payload.profileId || payload.profile || payload.sessionId || 'default').toString();
|
|
656
|
+
const x = Number(payload.x);
|
|
657
|
+
const y = Number(payload.y);
|
|
658
|
+
const button = payload.button || 'left';
|
|
659
|
+
const clicks = typeof payload.clicks === 'number' ? payload.clicks : 1;
|
|
660
|
+
const delay = typeof payload.delay === 'number' ? payload.delay : undefined;
|
|
661
|
+
if (!Number.isFinite(x) || !Number.isFinite(y)) {
|
|
662
|
+
throw new Error('x/y required for mouse:click');
|
|
663
|
+
}
|
|
664
|
+
const result = await this.browserServiceCommand('mouse:click', { profileId, x, y, button, clicks, ...(delay ? { delay } : {}) });
|
|
665
|
+
return { success: true, data: result };
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
async handleBrowserCancelDomPick(payload = {}) {
|
|
669
|
+
const profile = payload.profile || payload.sessionId;
|
|
670
|
+
if (!profile) {
|
|
671
|
+
throw new Error('缺少会话/ profile 信息');
|
|
672
|
+
}
|
|
673
|
+
try {
|
|
674
|
+
const data = await this.sendCancelDomPickViaWs(profile);
|
|
675
|
+
this.messageBus?.publish?.('ui.domPicker.result', {
|
|
676
|
+
success: false,
|
|
677
|
+
cancelled: true,
|
|
678
|
+
source: 'cancel-action',
|
|
679
|
+
details: data,
|
|
680
|
+
});
|
|
681
|
+
return { success: true, data };
|
|
682
|
+
} catch (err) {
|
|
683
|
+
const message = err?.message || '取消捕获失败';
|
|
684
|
+
this.messageBus?.publish?.('ui.domPicker.result', {
|
|
685
|
+
success: false,
|
|
686
|
+
cancelled: true,
|
|
687
|
+
error: message,
|
|
688
|
+
});
|
|
689
|
+
throw err;
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
async handleBrowserPickDom(payload = {}) {
|
|
694
|
+
const profile = payload.profile || payload.sessionId;
|
|
695
|
+
if (!profile) {
|
|
696
|
+
throw new Error('缺少会话/ profile 信息');
|
|
697
|
+
}
|
|
698
|
+
const timeout = Math.min(Math.max(Number(payload.timeout) || 25000, 3000), 60000);
|
|
699
|
+
const rootSelector = payload.rootSelector || payload.root_selector || null;
|
|
700
|
+
const startedAt = Date.now();
|
|
701
|
+
try {
|
|
702
|
+
const result = await this.sendDomPickerViaWs(profile, {
|
|
703
|
+
timeout,
|
|
704
|
+
rootSelector,
|
|
705
|
+
});
|
|
706
|
+
this.messageBus?.publish?.('ui.domPicker.result', {
|
|
707
|
+
success: true,
|
|
708
|
+
selector: result?.selector || null,
|
|
709
|
+
domPath: result?.dom_path || null,
|
|
710
|
+
durationMs: Date.now() - startedAt,
|
|
711
|
+
});
|
|
712
|
+
return { success: true, data: result };
|
|
713
|
+
} catch (err) {
|
|
714
|
+
const message = err?.message || '元素拾取失败';
|
|
715
|
+
this.messageBus?.publish?.('ui.domPicker.result', { success: false, error: message });
|
|
716
|
+
throw err;
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// v2 DOM pick:直接暴露 dom_path + selector 给 UI
|
|
721
|
+
async handleDomPick2(payload = {}) {
|
|
722
|
+
const profile = payload.profile || payload.sessionId;
|
|
723
|
+
if (!profile) {
|
|
724
|
+
throw new Error('缺少会话/ profile 信息');
|
|
725
|
+
}
|
|
726
|
+
const timeout = Math.min(Math.max(Number(payload.timeout) || 25000, 3000), 60000);
|
|
727
|
+
const rootSelector = payload.rootSelector || payload.root_selector || null;
|
|
728
|
+
const result = await this.sendDomPickerViaWs(profile, { timeout, rootSelector });
|
|
729
|
+
// 统一输出结构:domPath + selector
|
|
730
|
+
return {
|
|
731
|
+
success: true,
|
|
732
|
+
data: {
|
|
733
|
+
domPath: result?.dom_path || null,
|
|
734
|
+
selector: result?.selector || null,
|
|
735
|
+
raw: result,
|
|
736
|
+
},
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
async sendHighlightViaWs(sessionId, selector, options = {}) {
|
|
741
|
+
const payload = {
|
|
742
|
+
type: 'command',
|
|
743
|
+
session_id: sessionId,
|
|
744
|
+
data: {
|
|
745
|
+
command_type: 'dev_command',
|
|
746
|
+
action: 'highlight_element',
|
|
747
|
+
parameters: {
|
|
748
|
+
selector,
|
|
749
|
+
...(options.style ? { style: options.style } : {}),
|
|
750
|
+
...(typeof options.duration === 'number' ? { duration: options.duration } : {}),
|
|
751
|
+
...(options.channel ? { channel: options.channel } : {}),
|
|
752
|
+
...(typeof options.sticky === 'boolean' ? { sticky: options.sticky } : {}),
|
|
753
|
+
...(typeof options.maxMatches === 'number' ? { max_matches: options.maxMatches } : {}),
|
|
754
|
+
},
|
|
755
|
+
},
|
|
756
|
+
};
|
|
757
|
+
const response = await this.sendWsCommand(this.getBrowserWsUrl(), payload, 20000);
|
|
758
|
+
const data = response?.data || response;
|
|
759
|
+
const success = data?.success !== false;
|
|
760
|
+
if (!success) {
|
|
761
|
+
const err = data?.error || response?.error;
|
|
762
|
+
throw new Error(err || 'highlight_element failed');
|
|
763
|
+
}
|
|
764
|
+
return {
|
|
765
|
+
success: true,
|
|
766
|
+
source: 'ws',
|
|
767
|
+
details: data?.data || data,
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
async sendHighlightDomPathViaWs(sessionId, domPath, options = {}) {
|
|
772
|
+
const payload = {
|
|
773
|
+
type: 'command',
|
|
774
|
+
session_id: sessionId,
|
|
775
|
+
data: {
|
|
776
|
+
command_type: 'dev_command',
|
|
777
|
+
action: 'highlight_dom_path',
|
|
778
|
+
parameters: {
|
|
779
|
+
path: domPath,
|
|
780
|
+
...(options.style ? { style: options.style } : {}),
|
|
781
|
+
...(typeof options.duration === 'number' ? { duration: options.duration } : {}),
|
|
782
|
+
...(options.channel ? { channel: options.channel } : {}),
|
|
783
|
+
...(typeof options.sticky === 'boolean' ? { sticky: options.sticky } : {}),
|
|
784
|
+
...(options.rootSelector ? { root_selector: options.rootSelector } : {}),
|
|
785
|
+
},
|
|
786
|
+
},
|
|
787
|
+
};
|
|
788
|
+
const response = await this.sendWsCommand(this.getBrowserWsUrl(), payload, 20000);
|
|
789
|
+
const data = response?.data || response;
|
|
790
|
+
const success = data?.success !== false;
|
|
791
|
+
if (!success) {
|
|
792
|
+
const err = data?.error || response?.error;
|
|
793
|
+
throw new Error(err || 'highlight_dom_path failed');
|
|
794
|
+
}
|
|
795
|
+
return {
|
|
796
|
+
success: true,
|
|
797
|
+
source: 'ws',
|
|
798
|
+
details: data?.data || data,
|
|
799
|
+
};
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
async sendClearHighlightViaWs(sessionId, channel = null) {
|
|
803
|
+
const payload = {
|
|
804
|
+
type: 'command',
|
|
805
|
+
session_id: sessionId,
|
|
806
|
+
data: {
|
|
807
|
+
command_type: 'dev_command',
|
|
808
|
+
action: 'clear_highlight',
|
|
809
|
+
parameters: channel ? { channel } : {},
|
|
810
|
+
},
|
|
811
|
+
};
|
|
812
|
+
const response = await this.sendWsCommand(this.getBrowserWsUrl(), payload, 10000);
|
|
813
|
+
const data = response?.data || response;
|
|
814
|
+
if (data?.success === false) {
|
|
815
|
+
throw new Error(data?.error || 'clear_highlight failed');
|
|
816
|
+
}
|
|
817
|
+
return data?.data || data || { removed: 0 };
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
async sendCancelDomPickViaWs(sessionId) {
|
|
821
|
+
const payload = {
|
|
822
|
+
type: 'command',
|
|
823
|
+
session_id: sessionId,
|
|
824
|
+
data: {
|
|
825
|
+
command_type: 'dev_command',
|
|
826
|
+
action: 'cancel_dom_pick',
|
|
827
|
+
parameters: {},
|
|
828
|
+
},
|
|
829
|
+
};
|
|
830
|
+
const response = await this.sendWsCommand(this.getBrowserWsUrl(), payload, 10000);
|
|
831
|
+
const data = response?.data || response;
|
|
832
|
+
if (data?.success === false) {
|
|
833
|
+
throw new Error(data?.error || 'cancel_dom_pick failed');
|
|
834
|
+
}
|
|
835
|
+
return data?.data || data || { cancelled: false };
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
async sendExecuteViaWs(sessionId, script) {
|
|
839
|
+
const payload = {
|
|
840
|
+
type: 'command',
|
|
841
|
+
session_id: sessionId,
|
|
842
|
+
data: {
|
|
843
|
+
command_type: 'node_execute',
|
|
844
|
+
node_type: 'evaluate',
|
|
845
|
+
parameters: {
|
|
846
|
+
script,
|
|
847
|
+
},
|
|
848
|
+
},
|
|
849
|
+
};
|
|
850
|
+
const response = await this.sendWsCommand(this.getBrowserWsUrl(), payload, 10000);
|
|
851
|
+
const data = response?.data || response;
|
|
852
|
+
if (data?.success === false) {
|
|
853
|
+
throw new Error(data?.error || 'execute failed');
|
|
854
|
+
}
|
|
855
|
+
return data?.data || data || { result: null };
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
async sendDomPickerViaWs(sessionId, options = {}) {
|
|
859
|
+
const timeout = Math.min(Math.max(Number(options.timeout) || 25000, 3000), 60000);
|
|
860
|
+
const payload = {
|
|
861
|
+
type: 'command',
|
|
862
|
+
session_id: sessionId,
|
|
863
|
+
data: {
|
|
864
|
+
command_type: 'node_execute',
|
|
865
|
+
node_type: 'pick_dom',
|
|
866
|
+
parameters: {
|
|
867
|
+
timeout,
|
|
868
|
+
...(options.rootSelector ? { root_selector: options.rootSelector } : {}),
|
|
869
|
+
},
|
|
870
|
+
},
|
|
871
|
+
};
|
|
872
|
+
const response = await this.sendWsCommand(this.getBrowserWsUrl(), payload, timeout + 5000);
|
|
873
|
+
const data = response?.data;
|
|
874
|
+
if (data?.success === false) {
|
|
875
|
+
throw new Error(data?.error || 'pick_dom failed');
|
|
876
|
+
}
|
|
877
|
+
const result = data?.data || data;
|
|
878
|
+
if (!result) {
|
|
879
|
+
throw new Error('picker result missing');
|
|
880
|
+
}
|
|
881
|
+
return result;
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
async fetchContainerSnapshotFromService({ sessionId, url, maxDepth, maxChildren, rootContainerId, rootSelector }) {
|
|
885
|
+
if (!sessionId || !url) {
|
|
886
|
+
throw new Error('缺少 sessionId 或 URL');
|
|
887
|
+
}
|
|
888
|
+
const payload = {
|
|
889
|
+
type: 'command',
|
|
890
|
+
session_id: sessionId,
|
|
891
|
+
data: {
|
|
892
|
+
command_type: 'container_operation',
|
|
893
|
+
action: 'inspect_tree',
|
|
894
|
+
page_context: { url },
|
|
895
|
+
parameters: {
|
|
896
|
+
...(typeof maxDepth === 'number' ? { max_depth: maxDepth } : {}),
|
|
897
|
+
...(typeof maxChildren === 'number' ? { max_children: maxChildren } : {}),
|
|
898
|
+
...(rootContainerId ? { root_container_id: rootContainerId } : {}),
|
|
899
|
+
...(rootSelector ? { root_selector: rootSelector } : {}),
|
|
900
|
+
},
|
|
901
|
+
},
|
|
902
|
+
};
|
|
903
|
+
const response = await this.sendWsCommand(this.getBrowserWsUrl(), payload, 20000);
|
|
904
|
+
if (response?.data?.success) {
|
|
905
|
+
return response.data.data || response.data.snapshot || response.data;
|
|
906
|
+
}
|
|
907
|
+
throw new Error(response?.data?.error || response?.error || 'inspect_tree failed');
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
async fetchDomBranchFromService({ sessionId, url, path, rootSelector, maxDepth, maxChildren }) {
|
|
911
|
+
if (!sessionId || !url || !path) {
|
|
912
|
+
throw new Error('缺少 sessionId / URL / DOM 路径');
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
// 使用 WebSocket 而不是 CLI(避免 fixture 依赖)
|
|
916
|
+
const payload = {
|
|
917
|
+
type: 'command',
|
|
918
|
+
session_id: sessionId,
|
|
919
|
+
data: {
|
|
920
|
+
command_type: 'container_operation',
|
|
921
|
+
action: 'inspect_dom_branch',
|
|
922
|
+
page_context: { url },
|
|
923
|
+
parameters: {
|
|
924
|
+
path,
|
|
925
|
+
...(rootSelector ? { root_selector: rootSelector } : {}),
|
|
926
|
+
...(typeof maxDepth === 'number' ? { max_depth: maxDepth } : {}),
|
|
927
|
+
...(typeof maxChildren === 'number' ? { max_children: maxChildren } : {}),
|
|
928
|
+
},
|
|
929
|
+
},
|
|
930
|
+
};
|
|
931
|
+
const response = await this.sendWsCommand(this.getBrowserWsUrl(), payload, 20000);
|
|
932
|
+
if (response?.data?.success) {
|
|
933
|
+
const data = response.data.data || response.data.branch || response.data;
|
|
934
|
+
// 适配:确保返回结构包含 path 和 node 字段
|
|
935
|
+
if (data && !data.node && data.path) {
|
|
936
|
+
const nodeData = {
|
|
937
|
+
path: data.path,
|
|
938
|
+
children: data.children || [],
|
|
939
|
+
childCount: data.node_count || (data.children?.length || 0)
|
|
940
|
+
};
|
|
941
|
+
if (data.tag) nodeData.tag = data.tag;
|
|
942
|
+
if (data.id) nodeData.id = data.id;
|
|
943
|
+
if (data.classes) nodeData.classes = data.classes;
|
|
944
|
+
return { path: data.path, node: nodeData };
|
|
945
|
+
}
|
|
946
|
+
return data;
|
|
947
|
+
}
|
|
948
|
+
throw new Error(response?.data?.error || response?.error || 'inspect_dom_branch failed');
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
// v2 DOM branch:按 domPath + depth 获取局部树
|
|
952
|
+
async handleDomBranch2(payload = {}) {
|
|
953
|
+
const profile = payload.profile || payload.sessionId;
|
|
954
|
+
const url = payload.url;
|
|
955
|
+
const path = payload.path || payload.domPath;
|
|
956
|
+
if (!profile) throw new Error('缺少会话/ profile 信息');
|
|
957
|
+
if (!url) throw new Error('缺少 URL');
|
|
958
|
+
if (!path) throw new Error('缺少 DOM 路径');
|
|
959
|
+
const maxDepth = typeof payload.maxDepth === 'number' ? payload.maxDepth : payload.depth;
|
|
960
|
+
const maxChildren = typeof payload.maxChildren === 'number' ? payload.maxChildren : (payload.maxChildren || 12);
|
|
961
|
+
const rootSelector = payload.rootSelector || payload.root_selector || null;
|
|
962
|
+
const sessionId = profile;
|
|
963
|
+
const branch = await this.fetchDomBranchFromService({
|
|
964
|
+
sessionId,
|
|
965
|
+
url,
|
|
966
|
+
path,
|
|
967
|
+
rootSelector,
|
|
968
|
+
maxDepth: typeof maxDepth === 'number' ? maxDepth : undefined,
|
|
969
|
+
maxChildren: typeof maxChildren === 'number' ? maxChildren : undefined,
|
|
970
|
+
});
|
|
971
|
+
return { success: true, data: branch };
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
async captureSnapshotFromFixture({ profileId, url, maxDepth, maxChildren, containerId, rootSelector }) {
|
|
975
|
+
throw new Error('fixture snapshot fallback has been removed; use active browser session snapshot');
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
async captureBranchFromFixture({ profileId, url, path: domPath, rootSelector, maxDepth, maxChildren }) {
|
|
979
|
+
throw new Error('fixture branch fallback has been removed; use active browser session branch');
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
async captureInspectorSnapshot(options = {}) {
|
|
983
|
+
const profile = options.profile;
|
|
984
|
+
const sessions = await this.fetchSessions();
|
|
985
|
+
const targetSession = profile ? this.findSessionByProfile(sessions, profile) : sessions[0] || null;
|
|
986
|
+
const sessionId = targetSession?.session_id || targetSession?.sessionId || profile || null;
|
|
987
|
+
const profileId = profile || targetSession?.profileId || targetSession?.profile_id || sessionId || null;
|
|
988
|
+
const targetUrl = options.url || targetSession?.current_url || targetSession?.currentUrl;
|
|
989
|
+
const requestedContainerId = options.containerId || options.rootContainerId;
|
|
990
|
+
if (!targetUrl) {
|
|
991
|
+
throw new Error('无法确定会话 URL,请先在浏览器中打开目标页面');
|
|
992
|
+
}
|
|
993
|
+
let liveError = null;
|
|
994
|
+
let snapshot = null;
|
|
995
|
+
if (sessionId) {
|
|
996
|
+
try {
|
|
997
|
+
snapshot = await this.fetchContainerSnapshotFromService({
|
|
998
|
+
sessionId,
|
|
999
|
+
url: targetUrl,
|
|
1000
|
+
maxDepth: options.maxDepth,
|
|
1001
|
+
maxChildren: options.maxChildren,
|
|
1002
|
+
rootContainerId: requestedContainerId,
|
|
1003
|
+
rootSelector: options.rootSelector,
|
|
1004
|
+
});
|
|
1005
|
+
} catch (err) {
|
|
1006
|
+
liveError = err;
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
if (!snapshot || !snapshot.container_tree) {
|
|
1010
|
+
const rootError = liveError || new Error('容器树为空,检查容器定义或选择器是否正确');
|
|
1011
|
+
throw rootError;
|
|
1012
|
+
}
|
|
1013
|
+
if (requestedContainerId) {
|
|
1014
|
+
snapshot = this.focusSnapshotOnContainer(snapshot, requestedContainerId);
|
|
1015
|
+
}
|
|
1016
|
+
return {
|
|
1017
|
+
sessionId: sessionId || profileId || 'unknown-session',
|
|
1018
|
+
profileId: profileId || 'default',
|
|
1019
|
+
targetUrl,
|
|
1020
|
+
snapshot,
|
|
1021
|
+
};
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
async captureInspectorBranch(options = {}) {
|
|
1025
|
+
const profile = options.profile;
|
|
1026
|
+
const domPath = options.path;
|
|
1027
|
+
if (!profile) throw new Error('缺少 profile');
|
|
1028
|
+
if (!domPath) throw new Error('缺少 DOM 路径');
|
|
1029
|
+
const sessions = await this.fetchSessions();
|
|
1030
|
+
const targetSession = profile ? this.findSessionByProfile(sessions, profile) : sessions[0] || null;
|
|
1031
|
+
const sessionId = targetSession?.session_id || targetSession?.sessionId || profile || null;
|
|
1032
|
+
const profileId = profile || targetSession?.profileId || targetSession?.profile_id || sessionId || null;
|
|
1033
|
+
const targetUrl = options.url || targetSession?.current_url || targetSession?.currentUrl;
|
|
1034
|
+
if (!targetUrl) {
|
|
1035
|
+
throw new Error('无法确定会话 URL');
|
|
1036
|
+
}
|
|
1037
|
+
let branch = null;
|
|
1038
|
+
let liveError = null;
|
|
1039
|
+
if (sessionId) {
|
|
1040
|
+
try {
|
|
1041
|
+
branch = await this.fetchDomBranchFromService({
|
|
1042
|
+
sessionId,
|
|
1043
|
+
url: targetUrl,
|
|
1044
|
+
path: domPath,
|
|
1045
|
+
rootSelector: options.rootSelector,
|
|
1046
|
+
maxDepth: options.maxDepth,
|
|
1047
|
+
maxChildren: options.maxChildren,
|
|
1048
|
+
});
|
|
1049
|
+
} catch (err) {
|
|
1050
|
+
liveError = err;
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
if (!branch?.node) {
|
|
1054
|
+
throw liveError || new Error('无法获取 DOM 分支');
|
|
1055
|
+
}
|
|
1056
|
+
return {
|
|
1057
|
+
sessionId: sessionId || profileId || 'unknown-session',
|
|
1058
|
+
profileId: profileId || 'default',
|
|
1059
|
+
targetUrl,
|
|
1060
|
+
branch,
|
|
1061
|
+
};
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
async runCliCommand(target, args = []) {
|
|
1065
|
+
return this.cliBridge.runCliCommand(target, args);
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
async fetchSessions() {
|
|
1069
|
+
return this.cliBridge.fetchSessions();
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
findSessionByProfile(sessions, profile) {
|
|
1073
|
+
return this.cliBridge.findSessionByProfile(sessions, profile);
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
getBrowserWsUrl() {
|
|
1077
|
+
return this.transport.getBrowserWsUrl();
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
getBrowserHttpBase() {
|
|
1081
|
+
return this.transport.getBrowserHttpBase();
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
async browserServiceCommand(action, args, options = {}) {
|
|
1085
|
+
return this.transport.browserServiceCommand(action, args, options);
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
sendWsCommand(wsUrl, payload, timeoutMs = 15000) {
|
|
1089
|
+
return this.transport.sendWsCommand(wsUrl, payload, timeoutMs);
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
focusSnapshotOnContainer(snapshot, containerId) {
|
|
1093
|
+
if (!containerId || !snapshot?.container_tree) {
|
|
1094
|
+
return snapshot;
|
|
1095
|
+
}
|
|
1096
|
+
const target = this.cloneContainerSubtree(snapshot.container_tree, containerId);
|
|
1097
|
+
if (!target) {
|
|
1098
|
+
return snapshot;
|
|
1099
|
+
}
|
|
1100
|
+
const nextSnapshot = {
|
|
1101
|
+
...snapshot,
|
|
1102
|
+
container_tree: target,
|
|
1103
|
+
metadata: {
|
|
1104
|
+
...(snapshot.metadata || {}),
|
|
1105
|
+
root_container_id: containerId,
|
|
1106
|
+
},
|
|
1107
|
+
};
|
|
1108
|
+
if (!nextSnapshot.root_match || nextSnapshot.root_match?.container?.id !== containerId) {
|
|
1109
|
+
nextSnapshot.root_match = {
|
|
1110
|
+
container: {
|
|
1111
|
+
id: containerId,
|
|
1112
|
+
...(target.name ? { name: target.name } : {}),
|
|
1113
|
+
},
|
|
1114
|
+
matched_selector: target.match?.matched_selector,
|
|
1115
|
+
};
|
|
1116
|
+
}
|
|
1117
|
+
return nextSnapshot;
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
cloneContainerSubtree(node, targetId) {
|
|
1121
|
+
if (!node) return null;
|
|
1122
|
+
if (node.id === targetId || node.container_id === targetId) {
|
|
1123
|
+
return this.deepClone(node);
|
|
1124
|
+
}
|
|
1125
|
+
if (Array.isArray(node.children)) {
|
|
1126
|
+
for (const child of node.children) {
|
|
1127
|
+
const match = this.cloneContainerSubtree(child, targetId);
|
|
1128
|
+
if (match) return match;
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
return null;
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
deepClone(payload) {
|
|
1135
|
+
return JSON.parse(JSON.stringify(payload));
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
}
|