@pyrokine/mcp-chrome 2.3.2 → 2.4.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/README.md +19 -8
- package/README_zh.md +16 -5
- package/dist/anti-detection/behavior.d.ts.map +1 -1
- package/dist/anti-detection/behavior.js.map +1 -1
- package/dist/anti-detection/injection.js.map +1 -1
- package/dist/cdp/client.d.ts.map +1 -1
- package/dist/cdp/client.js.map +1 -1
- package/dist/cdp/launcher.d.ts.map +1 -1
- package/dist/cdp/launcher.js.map +1 -1
- package/dist/core/auto-wait.d.ts.map +1 -1
- package/dist/core/auto-wait.js.map +1 -1
- package/dist/core/browser-driver.d.ts.map +1 -1
- package/dist/core/errors.d.ts.map +1 -1
- package/dist/core/errors.js.map +1 -1
- package/dist/core/extension-errors.d.ts.map +1 -1
- package/dist/core/locator.d.ts.map +1 -1
- package/dist/core/locator.js.map +1 -1
- package/dist/core/retry.js.map +1 -1
- package/dist/core/session.d.ts.map +1 -1
- package/dist/core/session.js.map +1 -1
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/core/unified-session.d.ts +7 -0
- package/dist/core/unified-session.d.ts.map +1 -1
- package/dist/core/unified-session.js +14 -0
- package/dist/core/unified-session.js.map +1 -1
- package/dist/extension/bridge.d.ts.map +1 -1
- package/dist/extension/bridge.js.map +1 -1
- package/dist/extension/http-server.d.ts.map +1 -1
- package/dist/extension/http-server.js.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/tools/browse.d.ts.map +1 -1
- package/dist/tools/browse.js +12 -37
- package/dist/tools/browse.js.map +1 -1
- package/dist/tools/cookies.js.map +1 -1
- package/dist/tools/diagnostics.d.ts +31 -0
- package/dist/tools/diagnostics.d.ts.map +1 -0
- package/dist/tools/diagnostics.js +92 -0
- package/dist/tools/diagnostics.js.map +1 -0
- package/dist/tools/evaluate.d.ts +7 -0
- package/dist/tools/evaluate.d.ts.map +1 -1
- package/dist/tools/evaluate.js +111 -85
- package/dist/tools/evaluate.js.map +1 -1
- package/dist/tools/extract.d.ts.map +1 -1
- package/dist/tools/extract.js +26 -8
- package/dist/tools/extract.js.map +1 -1
- package/dist/tools/input.d.ts +2 -0
- package/dist/tools/input.d.ts.map +1 -1
- package/dist/tools/input.js +301 -183
- package/dist/tools/input.js.map +1 -1
- package/dist/tools/logs.d.ts +14 -0
- package/dist/tools/logs.d.ts.map +1 -1
- package/dist/tools/logs.js +37 -9
- package/dist/tools/logs.js.map +1 -1
- package/dist/tools/post-condition.d.ts +18 -1
- package/dist/tools/post-condition.d.ts.map +1 -1
- package/dist/tools/post-condition.js +48 -94
- package/dist/tools/post-condition.js.map +1 -1
- package/dist/tools/target-diagnostics.d.ts +31 -0
- package/dist/tools/target-diagnostics.d.ts.map +1 -0
- package/dist/tools/target-diagnostics.js +85 -0
- package/dist/tools/target-diagnostics.js.map +1 -0
- package/dist/tools/wait.d.ts.map +1 -1
- package/dist/tools/wait.js +182 -208
- package/dist/tools/wait.js.map +1 -1
- package/extension/dist/assets/{index.ts-CJXGbOcp.js → index.ts-BMmrcSQ6.js} +235 -66
- package/extension/dist/manifest.json +1 -1
- package/extension/dist/service-worker-loader.js +1 -1
- package/package.json +14 -10
package/dist/tools/input.js
CHANGED
|
@@ -11,9 +11,25 @@
|
|
|
11
11
|
import { z } from 'zod';
|
|
12
12
|
import { generateBezierPath, getMouseMoveDelay, getTypingDelay, randomDelay } from '../anti-detection/index.js';
|
|
13
13
|
import { formatErrorResponse, formatResponse, getSession, getUnifiedSession } from '../core/index.js';
|
|
14
|
+
import { appendDiagnostics, finishDiagnostics, startDiagnostics } from './diagnostics.js';
|
|
14
15
|
import { postConditionSchema, waitForPostCondition } from './post-condition.js';
|
|
15
16
|
import { targetToFindParams, targetZodSchema } from './schema.js';
|
|
17
|
+
import { buildTargetDiagnostics } from './target-diagnostics.js';
|
|
16
18
|
const INPUT_WAIT_MAX_MS = 60_000;
|
|
19
|
+
const TEXT_SELECTION_INPUT_TYPES = new Set(['text', 'search', 'tel', 'url', 'password']);
|
|
20
|
+
export function supportsTextSelection(tag, inputType) {
|
|
21
|
+
return (tag.toLowerCase() === 'textarea' ||
|
|
22
|
+
(tag.toLowerCase() === 'input' && TEXT_SELECTION_INPUT_TYPES.has((inputType ?? 'text').toLowerCase())));
|
|
23
|
+
}
|
|
24
|
+
export function replaceNthOccurrence(value, find, replacement, nth = 0) {
|
|
25
|
+
let index = -1;
|
|
26
|
+
for (let occurrence = 0; occurrence <= nth; occurrence++) {
|
|
27
|
+
index = value.indexOf(find, index + (occurrence > 0 ? 1 : 0));
|
|
28
|
+
if (index === -1)
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
return value.slice(0, index) + replacement + value.slice(index + find.length);
|
|
32
|
+
}
|
|
17
33
|
/**
|
|
18
34
|
* InputEvent schema
|
|
19
35
|
*/
|
|
@@ -138,30 +154,65 @@ async function handleInput(args) {
|
|
|
138
154
|
const humanize = args.humanize ?? false;
|
|
139
155
|
return await unifiedSession.withTabId(args.tabId, async () => {
|
|
140
156
|
return await unifiedSession.withFrame(args.frame, async () => {
|
|
141
|
-
const
|
|
157
|
+
const diagnostics = await startDiagnostics(unifiedSession, args.diagnostics);
|
|
142
158
|
const warnings = [];
|
|
143
159
|
const eventResults = [];
|
|
144
160
|
const session = mode === 'extension' ? undefined : getSession();
|
|
145
161
|
const inputStartedAt = Date.now();
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
162
|
+
let eventsExecuted = 0;
|
|
163
|
+
try {
|
|
164
|
+
for (const event of args.events) {
|
|
165
|
+
const eventTimeout = args.timeout === undefined
|
|
166
|
+
? undefined
|
|
167
|
+
: Math.max(0, args.timeout - (Date.now() - inputStartedAt));
|
|
168
|
+
if (eventTimeout !== undefined && eventTimeout <= 0) {
|
|
169
|
+
throw new Error(`input timeout before event dispatch (${args.timeout}ms)`);
|
|
170
|
+
}
|
|
171
|
+
const result = await executeInputEvent({ unifiedSession, session, mode, humanize, timeout: eventTimeout, frame: args.frame }, event);
|
|
172
|
+
++eventsExecuted;
|
|
173
|
+
if (typeof result === 'string') {
|
|
174
|
+
warnings.push(result);
|
|
175
|
+
}
|
|
176
|
+
else if (result) {
|
|
177
|
+
eventResults.push(result);
|
|
178
|
+
}
|
|
152
179
|
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
const response = formatErrorResponse(error);
|
|
183
|
+
const text = response.content[0]?.text;
|
|
184
|
+
if (text) {
|
|
185
|
+
try {
|
|
186
|
+
const payload = JSON.parse(text);
|
|
187
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
188
|
+
const timedOut = /timeout|超时/i.test(message);
|
|
189
|
+
const preActionTimeout = /timeout before event dispatch|超过 input 剩余 timeout/i.test(message);
|
|
190
|
+
const actionMayHaveExecuted = timedOut && !preActionTimeout;
|
|
191
|
+
payload.eventsExecuted = eventsExecuted;
|
|
192
|
+
payload.actionExecuted = eventsExecuted > 0 || actionMayHaveExecuted;
|
|
193
|
+
payload.actionStatus = actionMayHaveExecuted ? 'unknown' : 'failed';
|
|
194
|
+
payload.verificationRequested = Boolean(args.postCondition);
|
|
195
|
+
payload.verificationStatus = 'unavailable';
|
|
196
|
+
payload.failureStage = 'action';
|
|
197
|
+
payload.retryable = /timeout|超时|disconnect|未连接|context|debugger/i.test(message);
|
|
198
|
+
appendDiagnostics(payload, await finishDiagnostics(unifiedSession, diagnostics));
|
|
199
|
+
response.content[0].text = JSON.stringify(payload, null, 2);
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
// 保留原始错误响应
|
|
203
|
+
}
|
|
159
204
|
}
|
|
205
|
+
return response;
|
|
160
206
|
}
|
|
161
207
|
const result = {
|
|
162
208
|
success: true,
|
|
163
|
-
eventsExecuted
|
|
209
|
+
eventsExecuted,
|
|
164
210
|
mode,
|
|
211
|
+
actionExecuted: true,
|
|
212
|
+
actionStatus: 'completed',
|
|
213
|
+
verificationRequested: Boolean(args.postCondition),
|
|
214
|
+
verificationStatus: 'unavailable',
|
|
215
|
+
retryable: false,
|
|
165
216
|
};
|
|
166
217
|
if (warnings.length > 0) {
|
|
167
218
|
result.warnings = warnings;
|
|
@@ -170,17 +221,46 @@ async function handleInput(args) {
|
|
|
170
221
|
result.eventResults = eventResults;
|
|
171
222
|
}
|
|
172
223
|
if (args.postCondition) {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
result.
|
|
224
|
+
const postCondition = await waitForPostCondition(unifiedSession, args.postCondition, 'input');
|
|
225
|
+
result.postCondition = postCondition;
|
|
226
|
+
result.verificationStatus = postCondition.verificationStatus;
|
|
227
|
+
result.retryable = postCondition.retryable;
|
|
228
|
+
if (postCondition.verificationStatus !== 'matched') {
|
|
229
|
+
result.success = false;
|
|
230
|
+
result.failureStage = 'verification';
|
|
231
|
+
}
|
|
177
232
|
}
|
|
233
|
+
appendDiagnostics(result, await finishDiagnostics(unifiedSession, diagnostics));
|
|
178
234
|
return formatResponse(result);
|
|
179
235
|
}); // withFrame
|
|
180
236
|
}); // withTabId
|
|
181
237
|
}
|
|
182
238
|
catch (error) {
|
|
183
|
-
|
|
239
|
+
const response = formatErrorResponse(error);
|
|
240
|
+
const text = response.content[0]?.text;
|
|
241
|
+
if (text) {
|
|
242
|
+
try {
|
|
243
|
+
const payload = JSON.parse(text);
|
|
244
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
245
|
+
Object.assign(payload, {
|
|
246
|
+
actionExecuted: false,
|
|
247
|
+
actionStatus: 'failed',
|
|
248
|
+
verificationRequested: Boolean(args.postCondition),
|
|
249
|
+
verificationStatus: 'unavailable',
|
|
250
|
+
failureStage: 'action',
|
|
251
|
+
retryable: /timeout|超时|disconnect|未连接|context|debugger|attach/i.test(message),
|
|
252
|
+
diagnosticsStatus: args.diagnostics ? 'unavailable' : 'disabled',
|
|
253
|
+
});
|
|
254
|
+
if (args.diagnostics) {
|
|
255
|
+
payload.diagnosticsError = 'diagnostics 未能在 tab/frame 初始化失败前启动';
|
|
256
|
+
}
|
|
257
|
+
response.content[0].text = JSON.stringify(payload, null, 2);
|
|
258
|
+
}
|
|
259
|
+
catch {
|
|
260
|
+
// 保留原始错误响应
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return response;
|
|
184
264
|
}
|
|
185
265
|
}
|
|
186
266
|
const unifiedOnlyEventTypes = new Set([
|
|
@@ -192,7 +272,7 @@ const unifiedOnlyEventTypes = new Set([
|
|
|
192
272
|
]);
|
|
193
273
|
async function executeInputEvent(context, event) {
|
|
194
274
|
if (context.mode === 'extension' || unifiedOnlyEventTypes.has(event.type)) {
|
|
195
|
-
return executeUnifiedEvent(context.unifiedSession, event, context.humanize, context.timeout);
|
|
275
|
+
return executeUnifiedEvent(context.unifiedSession, event, context.humanize, context.timeout, context.frame);
|
|
196
276
|
}
|
|
197
277
|
if (!context.session) {
|
|
198
278
|
throw new Error('CDP 输入事件缺少 session');
|
|
@@ -200,27 +280,6 @@ async function executeInputEvent(context, event) {
|
|
|
200
280
|
await executeCdpEvent(context.session, event, context.humanize, context.timeout);
|
|
201
281
|
return undefined;
|
|
202
282
|
}
|
|
203
|
-
async function captureDiagnosticsStart(unifiedSession) {
|
|
204
|
-
await unifiedSession.enableConsole();
|
|
205
|
-
await unifiedSession.enableNetwork();
|
|
206
|
-
const consoleLogs = await unifiedSession.getConsoleLogs();
|
|
207
|
-
const network = await unifiedSession.getNetworkRequests();
|
|
208
|
-
return { consoleCount: consoleLogs.length, networkCount: network.length };
|
|
209
|
-
}
|
|
210
|
-
async function captureDiagnosticsDelta(unifiedSession, start) {
|
|
211
|
-
const consoleLogs = await unifiedSession.getConsoleLogs();
|
|
212
|
-
const network = await unifiedSession.getNetworkRequests();
|
|
213
|
-
return {
|
|
214
|
-
console: consoleLogs
|
|
215
|
-
.slice(start.consoleCount)
|
|
216
|
-
.filter((item) => ['error', 'warning', 'warn'].includes(item.level))
|
|
217
|
-
.slice(-20),
|
|
218
|
-
failedRequests: network
|
|
219
|
-
.slice(start.networkCount)
|
|
220
|
-
.filter((item) => item.errorText || (item.status !== undefined && item.status >= 400))
|
|
221
|
-
.slice(-20),
|
|
222
|
-
};
|
|
223
|
-
}
|
|
224
283
|
/**
|
|
225
284
|
* 通过真实鼠标事件选中页面文本
|
|
226
285
|
*
|
|
@@ -234,15 +293,20 @@ async function captureDiagnosticsDelta(unifiedSession, start) {
|
|
|
234
293
|
* 如果 target 是选择器类型,先通过 actionableClick 聚焦
|
|
235
294
|
* select/replace 事件用,保证选区建立前 activeElement 就是目标
|
|
236
295
|
*/
|
|
237
|
-
async function focusTargetForCommands(unifiedSession, target, timeout) {
|
|
296
|
+
async function focusTargetForCommands(unifiedSession, target, timeout, frame) {
|
|
238
297
|
if ('x' in target || 'y' in target) {
|
|
239
|
-
|
|
298
|
+
const point = await getTargetPointExtension(unifiedSession, target, timeout, frame);
|
|
299
|
+
await unifiedSession.mouseMove(point.x, point.y);
|
|
300
|
+
await unifiedSession.mouseClick('left');
|
|
301
|
+
return;
|
|
240
302
|
}
|
|
241
303
|
const params = targetToFindParams(target);
|
|
242
304
|
const els = await unifiedSession.find(params.selector, params.text, params.xpath, timeout);
|
|
243
305
|
const nth0 = params.nth ?? 0;
|
|
244
306
|
if (els.length <= nth0) {
|
|
245
|
-
|
|
307
|
+
throw new StructuredToolError(els.length === 0 ? 'TARGET_NOT_FOUND' : 'TARGET_INDEX_OUT_OF_RANGE', els.length === 0
|
|
308
|
+
? `未找到命令目标: ${JSON.stringify(target)}`
|
|
309
|
+
: `第 ${nth0} 个命令目标不存在(共 ${els.length} 个)`, '请检查 target、frame 和 target.nth,或先用 extract type=state 查看当前候选元素', await buildTargetDiagnosticContext(unifiedSession, target, els.length, nth0, timeout, '命令目标未找到', frame));
|
|
246
310
|
}
|
|
247
311
|
const focused = await unifiedSession.evaluate(`(() => {
|
|
248
312
|
const ref = window.__mcpElementMap?.[${JSON.stringify(els[nth0].refId)}]
|
|
@@ -253,27 +317,38 @@ async function focusTargetForCommands(unifiedSession, target, timeout) {
|
|
|
253
317
|
el.focus()
|
|
254
318
|
return document.activeElement === el
|
|
255
319
|
})()`, undefined, timeout);
|
|
256
|
-
|
|
320
|
+
if (focused !== true) {
|
|
321
|
+
throw new StructuredToolError('TARGET_FOCUS_FAILED', '命令目标未成功聚焦', '请检查目标是否可编辑、未 disabled 并处于正确 frame', await buildTargetDiagnosticContext(unifiedSession, target, els.length, nth0, timeout, '命令目标聚焦失败', frame));
|
|
322
|
+
}
|
|
257
323
|
}
|
|
258
|
-
async function focusTargetIfNeeded(unifiedSession, target,
|
|
259
|
-
if (!target
|
|
324
|
+
async function focusTargetIfNeeded(unifiedSession, target, timeout, frame) {
|
|
325
|
+
if (!target) {
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
if ('x' in target || 'y' in target) {
|
|
329
|
+
const point = await getTargetPointExtension(unifiedSession, target, timeout, frame);
|
|
330
|
+
await unifiedSession.mouseMove(point.x, point.y);
|
|
331
|
+
await unifiedSession.mouseClick('left');
|
|
260
332
|
return;
|
|
261
333
|
}
|
|
262
334
|
const params = targetToFindParams(target);
|
|
263
335
|
const els = await unifiedSession.find(params.selector, params.text, params.xpath, timeout);
|
|
264
|
-
const nth0 = params.nth ??
|
|
265
|
-
if (els.length
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
336
|
+
const nth0 = params.nth ?? 0;
|
|
337
|
+
if (els.length <= nth0) {
|
|
338
|
+
throw new StructuredToolError(els.length === 0 ? 'TARGET_NOT_FOUND' : 'TARGET_INDEX_OUT_OF_RANGE', els.length === 0
|
|
339
|
+
? `未找到目标元素: ${JSON.stringify(target)}`
|
|
340
|
+
: `第 ${nth0} 个匹配元素不存在(共 ${els.length} 个)`, '请检查 target、frame 和 target.nth,或先用 extract type=state 查看当前候选元素', await buildTargetDiagnosticContext(unifiedSession, target, els.length, nth0, timeout, '聚焦目标未找到', frame));
|
|
341
|
+
}
|
|
342
|
+
const result = await unifiedSession.actionableClick(els[nth0].refId);
|
|
343
|
+
if (!result.success) {
|
|
344
|
+
throw new StructuredToolError('ACTIONABILITY_FAILED', result.error || '目标元素无法聚焦', result.suggestions?.[0] ?? '请检查元素是否可见、未被遮挡并处于正确 frame', {
|
|
345
|
+
...result,
|
|
346
|
+
...(await buildTargetDiagnosticContext(unifiedSession, target, els.length, nth0, timeout, result.error || '目标元素无法聚焦', frame)),
|
|
347
|
+
});
|
|
273
348
|
}
|
|
274
349
|
}
|
|
275
|
-
async function collectTextSelectionDiagnostics(unifiedSession, findText, scopeTarget,
|
|
276
|
-
return await unifiedSession.evaluate(`function(findText,
|
|
350
|
+
async function collectTextSelectionDiagnostics(unifiedSession, findText, scopeTarget, occurrenceNth, scopeNth, scopeSelector, scopeText, scopeXpath, timeout) {
|
|
351
|
+
return await unifiedSession.evaluate(`function(findText, occurrenceNth, scopeNth, scopeTarget, scopeSelector, scopeText, scopeXpath) {
|
|
277
352
|
function selectorFor(el) {
|
|
278
353
|
if (!(el instanceof Element)) return null;
|
|
279
354
|
if (el.id) return '#' + CSS.escape(el.id);
|
|
@@ -314,22 +389,21 @@ async function collectTextSelectionDiagnostics(unifiedSession, findText, scopeTa
|
|
|
314
389
|
}
|
|
315
390
|
var root = document.body;
|
|
316
391
|
if (scopeXpath) {
|
|
317
|
-
var xr = document.evaluate(scopeXpath, document, null, XPathResult.
|
|
318
|
-
root = xr.
|
|
392
|
+
var xr = document.evaluate(scopeXpath, document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
|
|
393
|
+
root = xr.snapshotItem(scopeNth);
|
|
319
394
|
} else if (scopeSelector) {
|
|
320
|
-
var candidates = document.querySelectorAll(scopeSelector);
|
|
395
|
+
var candidates = Array.from(document.querySelectorAll(scopeSelector));
|
|
321
396
|
if (scopeText) {
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
}
|
|
325
|
-
} else {
|
|
326
|
-
root = candidates[0];
|
|
397
|
+
candidates = candidates.filter(function(candidate) {
|
|
398
|
+
return (candidate.textContent || '').includes(scopeText);
|
|
399
|
+
});
|
|
327
400
|
}
|
|
401
|
+
root = candidates[scopeNth];
|
|
328
402
|
} else if (scopeText) {
|
|
329
|
-
var all = document.querySelectorAll('*')
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
403
|
+
var all = Array.from(document.querySelectorAll('*')).filter(function(candidate) {
|
|
404
|
+
return (candidate.textContent || '').includes(scopeText);
|
|
405
|
+
});
|
|
406
|
+
root = all[scopeNth];
|
|
333
407
|
}
|
|
334
408
|
var candidateElements = [];
|
|
335
409
|
if (root instanceof Element) {
|
|
@@ -341,7 +415,8 @@ async function collectTextSelectionDiagnostics(unifiedSession, findText, scopeTa
|
|
|
341
415
|
return {
|
|
342
416
|
scope: {
|
|
343
417
|
target: scopeTarget,
|
|
344
|
-
|
|
418
|
+
targetNth: scopeNth,
|
|
419
|
+
occurrenceNth: occurrenceNth,
|
|
345
420
|
findText: findText,
|
|
346
421
|
selector: scopeSelector,
|
|
347
422
|
text: scopeText,
|
|
@@ -362,40 +437,41 @@ async function collectTextSelectionDiagnostics(unifiedSession, findText, scopeTa
|
|
|
362
437
|
},
|
|
363
438
|
candidates: candidateElements.map(summarizeElement).filter(Boolean)
|
|
364
439
|
};
|
|
365
|
-
}`, undefined, timeout, [findText,
|
|
440
|
+
}`, undefined, timeout, [findText, occurrenceNth, scopeNth, scopeTarget ?? null, scopeSelector, scopeText, scopeXpath]);
|
|
366
441
|
}
|
|
367
442
|
async function selectText(unifiedSession, findText, scopeTarget, nth = 0, timeout) {
|
|
368
443
|
// 将 target 转为查询参数,传入注入脚本进行 DOM 查询
|
|
369
444
|
let scopeSelector = null;
|
|
370
445
|
let scopeText = null;
|
|
371
446
|
let scopeXpath = null;
|
|
447
|
+
let scopeNth = 0;
|
|
372
448
|
if (scopeTarget && !('x' in scopeTarget) && !('y' in scopeTarget)) {
|
|
373
449
|
const params = targetToFindParams(scopeTarget);
|
|
374
450
|
scopeSelector = params.selector ?? null;
|
|
375
451
|
scopeText = params.text ?? null;
|
|
376
452
|
scopeXpath = params.xpath ?? null;
|
|
453
|
+
scopeNth = params.nth ?? 0;
|
|
377
454
|
}
|
|
378
455
|
// Step 1: 注入脚本定位文本
|
|
379
|
-
const result = await unifiedSession.evaluate(`function(findText, nth, scopeSelector, scopeText, scopeXpath) {
|
|
456
|
+
const result = await unifiedSession.evaluate(`function(findText, nth, scopeNth, scopeSelector, scopeText, scopeXpath) {
|
|
380
457
|
// 确定搜索根节点
|
|
381
458
|
var root = document.body;
|
|
382
459
|
if (scopeXpath) {
|
|
383
|
-
var xr = document.evaluate(scopeXpath, document, null, XPathResult.
|
|
384
|
-
root = xr.
|
|
460
|
+
var xr = document.evaluate(scopeXpath, document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
|
|
461
|
+
root = xr.snapshotItem(scopeNth);
|
|
385
462
|
} else if (scopeSelector) {
|
|
386
|
-
var candidates = document.querySelectorAll(scopeSelector);
|
|
463
|
+
var candidates = Array.from(document.querySelectorAll(scopeSelector));
|
|
387
464
|
if (scopeText) {
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
}
|
|
391
|
-
} else {
|
|
392
|
-
root = candidates[0];
|
|
465
|
+
candidates = candidates.filter(function(candidate) {
|
|
466
|
+
return (candidate.textContent || '').includes(scopeText);
|
|
467
|
+
});
|
|
393
468
|
}
|
|
469
|
+
root = candidates[scopeNth];
|
|
394
470
|
} else if (scopeText) {
|
|
395
|
-
var all = document.querySelectorAll('*')
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
471
|
+
var all = Array.from(document.querySelectorAll('*')).filter(function(candidate) {
|
|
472
|
+
return (candidate.textContent || '').includes(scopeText);
|
|
473
|
+
});
|
|
474
|
+
root = all[scopeNth];
|
|
399
475
|
}
|
|
400
476
|
if (!root) return {type: 'noscope'};
|
|
401
477
|
|
|
@@ -403,6 +479,11 @@ async function selectText(unifiedSession, findText, scopeTarget, nth = 0, timeou
|
|
|
403
479
|
var tag = root.tagName;
|
|
404
480
|
if (tag === 'INPUT' || tag === 'TEXTAREA') {
|
|
405
481
|
var val = root.value || '';
|
|
482
|
+
var inputType = tag === 'INPUT' ? (root.type || 'text').toLowerCase() : 'textarea';
|
|
483
|
+
var selectionCapable = tag === 'TEXTAREA' || ['text', 'search', 'tel', 'url', 'password'].indexOf(inputType) !== -1;
|
|
484
|
+
if (!selectionCapable) {
|
|
485
|
+
return {type: 'unsupported', tag: tag.toLowerCase(), inputType: inputType, currentValue: String(val).slice(0, 160)};
|
|
486
|
+
}
|
|
406
487
|
var pos = -1;
|
|
407
488
|
for (var n = 0; n <= nth; n++) {
|
|
408
489
|
pos = val.indexOf(findText, pos + (n > 0 ? 1 : 0));
|
|
@@ -410,9 +491,7 @@ async function selectText(unifiedSession, findText, scopeTarget, nth = 0, timeou
|
|
|
410
491
|
}
|
|
411
492
|
// 原子化:定位到 input 同时完成 focus + setSelectionRange,避免外层 mouseClick 聚焦不可靠
|
|
412
493
|
root.focus();
|
|
413
|
-
|
|
414
|
-
root.setSelectionRange(pos, pos + findText.length);
|
|
415
|
-
}
|
|
494
|
+
root.setSelectionRange(pos, pos + findText.length);
|
|
416
495
|
return {type: 'input', selectionStart: pos, selectionEnd: pos + findText.length};
|
|
417
496
|
}
|
|
418
497
|
|
|
@@ -421,16 +500,19 @@ async function selectText(unifiedSession, findText, scopeTarget, nth = 0, timeou
|
|
|
421
500
|
for (var k = 0; k < inputs.length; k++) {
|
|
422
501
|
var inp = inputs[k];
|
|
423
502
|
var v = inp.value || '';
|
|
503
|
+
var childType = inp.tagName === 'INPUT' ? (inp.type || 'text').toLowerCase() : 'textarea';
|
|
504
|
+
var childSelectionCapable = inp.tagName === 'TEXTAREA' || ['text', 'search', 'tel', 'url', 'password'].indexOf(childType) !== -1;
|
|
424
505
|
var ip = -1;
|
|
425
506
|
for (var n2 = 0; n2 <= nth; n2++) {
|
|
426
507
|
ip = v.indexOf(findText, ip + (n2 > 0 ? 1 : 0));
|
|
427
508
|
if (ip === -1) break;
|
|
428
509
|
}
|
|
510
|
+
if (ip !== -1 && !childSelectionCapable) {
|
|
511
|
+
return {type: 'unsupported', tag: inp.tagName.toLowerCase(), inputType: childType, currentValue: String(v).slice(0, 160)};
|
|
512
|
+
}
|
|
429
513
|
if (ip !== -1) {
|
|
430
514
|
inp.focus();
|
|
431
|
-
|
|
432
|
-
inp.setSelectionRange(ip, ip + findText.length);
|
|
433
|
-
}
|
|
515
|
+
inp.setSelectionRange(ip, ip + findText.length);
|
|
434
516
|
return {type: 'input', selectionStart: ip, selectionEnd: ip + findText.length};
|
|
435
517
|
}
|
|
436
518
|
}
|
|
@@ -495,13 +577,22 @@ async function selectText(unifiedSession, findText, scopeTarget, nth = 0, timeou
|
|
|
495
577
|
endY: er.y + er.height / 2,
|
|
496
578
|
formatted: formatted || undefined
|
|
497
579
|
};
|
|
498
|
-
}`, undefined, timeout, [findText, nth, scopeSelector, scopeText, scopeXpath]);
|
|
580
|
+
}`, undefined, timeout, [findText, nth, scopeNth, scopeSelector, scopeText, scopeXpath]);
|
|
499
581
|
if (!result || result.type === 'noscope') {
|
|
500
|
-
const diagnostics = await collectTextSelectionDiagnostics(unifiedSession, findText, scopeTarget, nth, scopeSelector, scopeText, scopeXpath, timeout);
|
|
582
|
+
const diagnostics = await collectTextSelectionDiagnostics(unifiedSession, findText, scopeTarget, nth, scopeNth, scopeSelector, scopeText, scopeXpath, timeout);
|
|
501
583
|
throw new StructuredToolError('TEXT_SCOPE_NOT_FOUND', `未找到目标元素: ${JSON.stringify(scopeTarget)}`, '请检查 target 是否能定位到包含目标文本的元素,或先用 extract(state) 查看可交互元素', diagnostics);
|
|
502
584
|
}
|
|
585
|
+
if (result.type === 'unsupported') {
|
|
586
|
+
throw new StructuredToolError('UNSUPPORTED_SELECTION', `${result.tag}[type=${result.inputType}] 不支持文本 selection`, 'select 只支持 textarea 和 text/search/tel/url/password input;请使用 type mode="controlled" 设置完整 value', {
|
|
587
|
+
elementTag: result.tag,
|
|
588
|
+
inputType: result.inputType,
|
|
589
|
+
currentValue: result.currentValue,
|
|
590
|
+
requestedFind: findText,
|
|
591
|
+
recommendedMode: 'controlled',
|
|
592
|
+
});
|
|
593
|
+
}
|
|
503
594
|
if (result.type === 'notfound') {
|
|
504
|
-
const diagnostics = await collectTextSelectionDiagnostics(unifiedSession, findText, scopeTarget, nth, scopeSelector, scopeText, scopeXpath, timeout);
|
|
595
|
+
const diagnostics = await collectTextSelectionDiagnostics(unifiedSession, findText, scopeTarget, nth, scopeNth, scopeSelector, scopeText, scopeXpath, timeout);
|
|
505
596
|
throw new StructuredToolError('TEXT_NOT_FOUND', scopeTarget
|
|
506
597
|
? `目标元素内未找到文本 "${findText}"${nth > 0 ? `(第 ${nth} 个匹配)` : ''}`
|
|
507
598
|
: `未找到文本: "${findText}"${nth > 0 ? `(第 ${nth} 个匹配)` : ''}`, '请检查 find 文本、nth 序号和 target 范围;context.candidates 提供了当前范围内的候选文本和 selector', diagnostics);
|
|
@@ -652,13 +743,13 @@ function validateEvent(event) {
|
|
|
652
743
|
break;
|
|
653
744
|
}
|
|
654
745
|
}
|
|
655
|
-
async function executeUnifiedEvent(unifiedSession, event, humanize, timeout) {
|
|
746
|
+
async function executeUnifiedEvent(unifiedSession, event, humanize, timeout, frame) {
|
|
656
747
|
validateEvent(event);
|
|
657
748
|
const handler = unifiedInputHandlers[event.type];
|
|
658
749
|
if (!handler) {
|
|
659
750
|
throw new Error(`未知事件类型: ${event.type}`);
|
|
660
751
|
}
|
|
661
|
-
return handler({ unifiedSession, humanize, timeout }, event);
|
|
752
|
+
return handler({ unifiedSession, humanize, timeout, frame }, event);
|
|
662
753
|
}
|
|
663
754
|
const unifiedInputHandlers = {
|
|
664
755
|
keydown: handleUnifiedKeyDown,
|
|
@@ -680,15 +771,12 @@ const unifiedInputHandlers = {
|
|
|
680
771
|
editorCommand: handleUnifiedEditorCommand,
|
|
681
772
|
drag: handleUnifiedDrag,
|
|
682
773
|
};
|
|
683
|
-
async function handleUnifiedKeyDown({ unifiedSession, timeout }, event) {
|
|
774
|
+
async function handleUnifiedKeyDown({ unifiedSession, timeout, frame }, event) {
|
|
684
775
|
if (unifiedSession.getInputMode() === 'stealth' && event.commands && event.commands.length > 0) {
|
|
685
776
|
throw new Error('commands 参数不支持 stealth 输入模式,请先调用 manage action=inputMode inputMode=precise 切换后重试');
|
|
686
777
|
}
|
|
687
778
|
if (event.commands && event.commands.length > 0 && event.target) {
|
|
688
|
-
|
|
689
|
-
if (!focused) {
|
|
690
|
-
throw new Error('commands 目标未找到或未成功聚焦');
|
|
691
|
-
}
|
|
779
|
+
await focusTargetForCommands(unifiedSession, event.target, timeout, frame);
|
|
692
780
|
}
|
|
693
781
|
await unifiedSession.keyDown(event.key, event.commands);
|
|
694
782
|
if (event.commands && event.commands.length > 0) {
|
|
@@ -700,7 +788,7 @@ async function handleUnifiedKeyUp({ unifiedSession }, event) {
|
|
|
700
788
|
await unifiedSession.keyUp(event.key);
|
|
701
789
|
return undefined;
|
|
702
790
|
}
|
|
703
|
-
async function handleUnifiedClick({ unifiedSession, timeout }, event) {
|
|
791
|
+
async function handleUnifiedClick({ unifiedSession, timeout, frame }, event) {
|
|
704
792
|
const button = event.button ?? 'left';
|
|
705
793
|
const clickCount = event.clickCount ?? 1;
|
|
706
794
|
if (!event.target) {
|
|
@@ -708,7 +796,7 @@ async function handleUnifiedClick({ unifiedSession, timeout }, event) {
|
|
|
708
796
|
return undefined;
|
|
709
797
|
}
|
|
710
798
|
if ('x' in event.target && 'y' in event.target) {
|
|
711
|
-
const point = await getTargetPointExtension(unifiedSession, event.target, timeout);
|
|
799
|
+
const point = await getTargetPointExtension(unifiedSession, event.target, timeout, frame);
|
|
712
800
|
await unifiedSession.mouseMove(point.x, point.y);
|
|
713
801
|
await unifiedSession.mouseClick(button, clickCount);
|
|
714
802
|
return undefined;
|
|
@@ -726,14 +814,14 @@ async function handleUnifiedClick({ unifiedSession, timeout }, event) {
|
|
|
726
814
|
return undefined;
|
|
727
815
|
}
|
|
728
816
|
}
|
|
729
|
-
const point = await getTargetPointExtension(unifiedSession, event.target, timeout);
|
|
817
|
+
const point = await getTargetPointExtension(unifiedSession, event.target, timeout, frame);
|
|
730
818
|
await unifiedSession.mouseMove(point.x, point.y);
|
|
731
819
|
await unifiedSession.mouseClick(button, clickCount, typeof point.refId === 'string' ? point.refId : undefined);
|
|
732
820
|
return undefined;
|
|
733
821
|
}
|
|
734
|
-
async function handleUnifiedMouseDown({ unifiedSession, timeout }, event) {
|
|
822
|
+
async function handleUnifiedMouseDown({ unifiedSession, timeout, frame }, event) {
|
|
735
823
|
if (event.target) {
|
|
736
|
-
const point = await getTargetPointExtension(unifiedSession, event.target, timeout);
|
|
824
|
+
const point = await getTargetPointExtension(unifiedSession, event.target, timeout, frame);
|
|
737
825
|
await unifiedSession.mouseMove(point.x, point.y);
|
|
738
826
|
}
|
|
739
827
|
await unifiedSession.mouseDown(event.button ?? 'left');
|
|
@@ -743,8 +831,8 @@ async function handleUnifiedMouseUp({ unifiedSession }, event) {
|
|
|
743
831
|
await unifiedSession.mouseUp(event.button ?? 'left');
|
|
744
832
|
return undefined;
|
|
745
833
|
}
|
|
746
|
-
async function handleUnifiedMouseMove({ unifiedSession, humanize, timeout }, event) {
|
|
747
|
-
const point = await getTargetPointExtension(unifiedSession, event.target, timeout);
|
|
834
|
+
async function handleUnifiedMouseMove({ unifiedSession, humanize, timeout, frame }, event) {
|
|
835
|
+
const point = await getTargetPointExtension(unifiedSession, event.target, timeout, frame);
|
|
748
836
|
if (humanize && event.steps && event.steps > 1) {
|
|
749
837
|
const path = generateBezierPath(unifiedSession.getMousePosition(), point, event.steps);
|
|
750
838
|
for (const p of path) {
|
|
@@ -757,7 +845,7 @@ async function handleUnifiedMouseMove({ unifiedSession, humanize, timeout }, eve
|
|
|
757
845
|
}
|
|
758
846
|
return undefined;
|
|
759
847
|
}
|
|
760
|
-
async function handleUnifiedWheel({ unifiedSession, timeout }, event) {
|
|
848
|
+
async function handleUnifiedWheel({ unifiedSession, timeout, frame }, event) {
|
|
761
849
|
if (event.target) {
|
|
762
850
|
const { selector, text, xpath, nth: nthParam } = targetToFindParams(event.target);
|
|
763
851
|
const elements = await unifiedSession.find(selector, text, xpath, timeout);
|
|
@@ -766,19 +854,19 @@ async function handleUnifiedWheel({ unifiedSession, timeout }, event) {
|
|
|
766
854
|
await unifiedSession.scroll(event.deltaX ?? 0, event.deltaY ?? 0, elements[nth].refId);
|
|
767
855
|
return undefined;
|
|
768
856
|
}
|
|
769
|
-
const point = await getTargetPointExtension(unifiedSession, event.target, timeout);
|
|
857
|
+
const point = await getTargetPointExtension(unifiedSession, event.target, timeout, frame);
|
|
770
858
|
await unifiedSession.mouseMove(point.x, point.y);
|
|
771
859
|
}
|
|
772
860
|
await unifiedSession.mouseWheel(event.deltaX ?? 0, event.deltaY ?? 0);
|
|
773
861
|
return undefined;
|
|
774
862
|
}
|
|
775
|
-
async function handleUnifiedTouchStart({ unifiedSession, timeout }, event) {
|
|
776
|
-
const point = await getTargetPointExtension(unifiedSession, event.target, timeout);
|
|
863
|
+
async function handleUnifiedTouchStart({ unifiedSession, timeout, frame }, event) {
|
|
864
|
+
const point = await getTargetPointExtension(unifiedSession, event.target, timeout, frame);
|
|
777
865
|
await unifiedSession.touchStart(point.x, point.y);
|
|
778
866
|
return undefined;
|
|
779
867
|
}
|
|
780
|
-
async function handleUnifiedTouchMove({ unifiedSession, timeout }, event) {
|
|
781
|
-
const point = await getTargetPointExtension(unifiedSession, event.target, timeout);
|
|
868
|
+
async function handleUnifiedTouchMove({ unifiedSession, timeout, frame }, event) {
|
|
869
|
+
const point = await getTargetPointExtension(unifiedSession, event.target, timeout, frame);
|
|
782
870
|
await unifiedSession.touchMove(point.x, point.y);
|
|
783
871
|
return undefined;
|
|
784
872
|
}
|
|
@@ -786,13 +874,13 @@ async function handleUnifiedTouchEnd({ unifiedSession }) {
|
|
|
786
874
|
await unifiedSession.touchEnd();
|
|
787
875
|
return undefined;
|
|
788
876
|
}
|
|
789
|
-
async function handleUnifiedType({ unifiedSession, humanize, timeout }, event) {
|
|
877
|
+
async function handleUnifiedType({ unifiedSession, humanize, timeout, frame }, event) {
|
|
790
878
|
if (event.mode === 'controlled' || event.dispatch) {
|
|
791
|
-
await inputControlled(unifiedSession, event, timeout);
|
|
879
|
+
await inputControlled(unifiedSession, event, timeout, frame);
|
|
792
880
|
return undefined;
|
|
793
881
|
}
|
|
794
882
|
if (event.target) {
|
|
795
|
-
const point = await getTargetPointExtension(unifiedSession, event.target, timeout);
|
|
883
|
+
const point = await getTargetPointExtension(unifiedSession, event.target, timeout, frame);
|
|
796
884
|
await unifiedSession.mouseMove(point.x, point.y);
|
|
797
885
|
await unifiedSession.mouseClick('left');
|
|
798
886
|
}
|
|
@@ -826,13 +914,80 @@ async function handleUnifiedWait({ timeout }, event) {
|
|
|
826
914
|
await new Promise((resolve) => setTimeout(resolve, event.ms));
|
|
827
915
|
return undefined;
|
|
828
916
|
}
|
|
829
|
-
async function handleUnifiedSelect({ unifiedSession, timeout }, event) {
|
|
830
|
-
await focusTargetIfNeeded(unifiedSession, event.target,
|
|
917
|
+
async function handleUnifiedSelect({ unifiedSession, timeout, frame }, event) {
|
|
918
|
+
await focusTargetIfNeeded(unifiedSession, event.target, timeout, frame);
|
|
919
|
+
const selectionTarget = await unifiedSession.evaluate(`(() => {
|
|
920
|
+
const el = document.activeElement;
|
|
921
|
+
if (!(el instanceof HTMLInputElement)) return null;
|
|
922
|
+
return {tag: 'input', inputType: (el.type || 'text').toLowerCase(), value: String(el.value || '').slice(0, 160)};
|
|
923
|
+
})()`, undefined, timeout);
|
|
924
|
+
if (selectionTarget && !supportsTextSelection(selectionTarget.tag, selectionTarget.inputType)) {
|
|
925
|
+
throw new StructuredToolError('UNSUPPORTED_SELECTION', `input[type=${selectionTarget.inputType}] 不支持文本 selection`, '请改用 type mode="controlled" 设置完整 value,replace 事件会对这类 input 使用完整 value 更新路径', {
|
|
926
|
+
elementTag: selectionTarget.tag,
|
|
927
|
+
inputType: selectionTarget.inputType,
|
|
928
|
+
currentValue: selectionTarget.value,
|
|
929
|
+
requestedFind: event.find,
|
|
930
|
+
recommendedMode: 'controlled',
|
|
931
|
+
});
|
|
932
|
+
}
|
|
831
933
|
await selectText(unifiedSession, event.find, event.target, event.nth, timeout);
|
|
832
934
|
return undefined;
|
|
833
935
|
}
|
|
834
|
-
async function handleUnifiedReplace({ unifiedSession, timeout }, event) {
|
|
835
|
-
await focusTargetIfNeeded(unifiedSession, event.target,
|
|
936
|
+
async function handleUnifiedReplace({ unifiedSession, timeout, frame }, event) {
|
|
937
|
+
await focusTargetIfNeeded(unifiedSession, event.target, timeout, frame);
|
|
938
|
+
const directResult = await unifiedSession.evaluate(`function(findText, replacementText, nth) {
|
|
939
|
+
var el = document.activeElement;
|
|
940
|
+
if (!(el instanceof HTMLInputElement)) return {handled: false};
|
|
941
|
+
var type = (el.type || 'text').toLowerCase();
|
|
942
|
+
if (['text', 'search', 'tel', 'url', 'password'].indexOf(type) !== -1) return {handled: false};
|
|
943
|
+
var current = String(el.value || '');
|
|
944
|
+
var pos = -1;
|
|
945
|
+
for (var i = 0; i <= nth; i++) {
|
|
946
|
+
pos = current.indexOf(findText, pos + (i > 0 ? 1 : 0));
|
|
947
|
+
if (pos === -1) return {handled: true, success: false, tag: 'input', inputType: type, currentValue: current.slice(0, 160), reason: 'text_not_found'};
|
|
948
|
+
}
|
|
949
|
+
if (el.disabled || el.readOnly) return {handled: true, success: false, tag: 'input', inputType: type, currentValue: current.slice(0, 160), reason: 'not_editable'};
|
|
950
|
+
var requested = current.slice(0, pos) + replacementText + current.slice(pos + findText.length);
|
|
951
|
+
var setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')?.set;
|
|
952
|
+
if (!setter) return {handled: true, success: false, tag: 'input', inputType: type, currentValue: current.slice(0, 160), requestedValue: requested.slice(0, 160), reason: 'no_native_setter'};
|
|
953
|
+
setter.call(el, requested);
|
|
954
|
+
el.dispatchEvent(new Event('input', {bubbles: true}));
|
|
955
|
+
el.dispatchEvent(new Event('change', {bubbles: true}));
|
|
956
|
+
var actual = String(el.value || '');
|
|
957
|
+
return {handled: true, success: actual === requested, tag: 'input', inputType: type, currentValue: current.slice(0, 160), requestedValue: requested.slice(0, 160), actualValue: actual.slice(0, 160), reason: actual === requested ? undefined : 'value_not_applied'};
|
|
958
|
+
}`, undefined, timeout, [event.find, event.text, event.nth ?? 0]);
|
|
959
|
+
if (directResult.handled) {
|
|
960
|
+
if (!directResult.success) {
|
|
961
|
+
const valueNotApplied = directResult.reason === 'value_not_applied';
|
|
962
|
+
throw new StructuredToolError(directResult.reason === 'text_not_found'
|
|
963
|
+
? 'TEXT_NOT_FOUND'
|
|
964
|
+
: valueNotApplied
|
|
965
|
+
? 'VALUE_NOT_APPLIED'
|
|
966
|
+
: 'UNSUPPORTED_SELECTION', directResult.reason === 'text_not_found'
|
|
967
|
+
? `目标元素内未找到文本 "${event.find}"`
|
|
968
|
+
: valueNotApplied
|
|
969
|
+
? '浏览器没有接受 replace 计算出的完整 value'
|
|
970
|
+
: '目标 input 不支持文本 selection,且无法安全更新完整 value', valueNotApplied
|
|
971
|
+
? '请根据 actualValue 提供该 input type 接受的完整值,或使用 controlled type 设置合法值'
|
|
972
|
+
: '请使用 mode="controlled" 的 type 事件设置完整 value,或改用支持文本 selection 的 input 类型', {
|
|
973
|
+
elementTag: directResult.tag,
|
|
974
|
+
inputType: directResult.inputType,
|
|
975
|
+
currentValue: directResult.currentValue,
|
|
976
|
+
requestedFind: event.find,
|
|
977
|
+
requestedValue: directResult.requestedValue,
|
|
978
|
+
actualValue: directResult.actualValue,
|
|
979
|
+
recommendedMode: 'controlled',
|
|
980
|
+
reason: directResult.reason,
|
|
981
|
+
});
|
|
982
|
+
}
|
|
983
|
+
return {
|
|
984
|
+
elementTag: directResult.tag,
|
|
985
|
+
inputType: directResult.inputType,
|
|
986
|
+
requestedValue: directResult.requestedValue,
|
|
987
|
+
actualValue: directResult.actualValue,
|
|
988
|
+
normalized: directResult.actualValue !== directResult.requestedValue,
|
|
989
|
+
};
|
|
990
|
+
}
|
|
836
991
|
const formatted = await selectText(unifiedSession, event.find, event.target, event.nth, timeout);
|
|
837
992
|
let selectionConfirmed = false;
|
|
838
993
|
for (let i = 0; i < 25; i++) {
|
|
@@ -883,18 +1038,18 @@ async function handleUnifiedReplace({ unifiedSession, timeout }, event) {
|
|
|
883
1038
|
}
|
|
884
1039
|
return undefined;
|
|
885
1040
|
}
|
|
886
|
-
async function handleUnifiedEditorContext({ unifiedSession, timeout }, event) {
|
|
887
|
-
return (await editorAction(unifiedSession, 'context', event, timeout));
|
|
1041
|
+
async function handleUnifiedEditorContext({ unifiedSession, timeout, frame }, event) {
|
|
1042
|
+
return (await editorAction(unifiedSession, 'context', event, timeout, frame));
|
|
888
1043
|
}
|
|
889
|
-
async function handleUnifiedEditorInsert({ unifiedSession, timeout }, event) {
|
|
890
|
-
await editorAction(unifiedSession, 'insert', event, timeout);
|
|
1044
|
+
async function handleUnifiedEditorInsert({ unifiedSession, timeout, frame }, event) {
|
|
1045
|
+
await editorAction(unifiedSession, 'insert', event, timeout, frame);
|
|
891
1046
|
return undefined;
|
|
892
1047
|
}
|
|
893
|
-
async function handleUnifiedEditorCommand({ unifiedSession, timeout }, event) {
|
|
894
|
-
await editorAction(unifiedSession, 'command', event, timeout);
|
|
1048
|
+
async function handleUnifiedEditorCommand({ unifiedSession, timeout, frame }, event) {
|
|
1049
|
+
await editorAction(unifiedSession, 'command', event, timeout, frame);
|
|
895
1050
|
return undefined;
|
|
896
1051
|
}
|
|
897
|
-
async function handleUnifiedDrag({ unifiedSession, timeout }, event) {
|
|
1052
|
+
async function handleUnifiedDrag({ unifiedSession, timeout, frame }, event) {
|
|
898
1053
|
if (!event.target) {
|
|
899
1054
|
throw new Error('drag 事件需要 target 参数');
|
|
900
1055
|
}
|
|
@@ -915,10 +1070,10 @@ async function handleUnifiedDrag({ unifiedSession, timeout }, event) {
|
|
|
915
1070
|
const srcEls = await unifiedSession.find(srcParams.selector, srcParams.text, srcParams.xpath, timeout);
|
|
916
1071
|
const dstEls = await unifiedSession.find(dstParams.selector, dstParams.text, dstParams.xpath, timeout);
|
|
917
1072
|
if (srcEls.length <= srcNth) {
|
|
918
|
-
throw new
|
|
1073
|
+
throw new StructuredToolError(srcEls.length === 0 ? 'TARGET_NOT_FOUND' : 'TARGET_INDEX_OUT_OF_RANGE', `drag 源元素未找到: ${JSON.stringify(event.target)}`, '请检查 drag 源 target、frame 和 nth', await buildTargetDiagnosticContext(unifiedSession, event.target, srcEls.length, srcNth, timeout, 'drag 源元素未找到', frame));
|
|
919
1074
|
}
|
|
920
1075
|
if (dstEls.length <= dstNth) {
|
|
921
|
-
throw new
|
|
1076
|
+
throw new StructuredToolError(dstEls.length === 0 ? 'TARGET_NOT_FOUND' : 'TARGET_INDEX_OUT_OF_RANGE', `drag 目标元素未找到: ${JSON.stringify(event.to)}`, '请检查 drag 目标 to、frame 和 nth', await buildTargetDiagnosticContext(unifiedSession, event.to, dstEls.length, dstNth, timeout, 'drag 目标元素未找到', frame));
|
|
922
1077
|
}
|
|
923
1078
|
return unifiedSession.dragAndDrop(srcEls[srcNth].refId, dstEls[dstNth].refId);
|
|
924
1079
|
};
|
|
@@ -945,7 +1100,7 @@ async function handleUnifiedDrag({ unifiedSession, timeout }, event) {
|
|
|
945
1100
|
* - precise 模式直接使用
|
|
946
1101
|
* - stealth 模式需减 offset 转为 iframe 相对坐标
|
|
947
1102
|
*/
|
|
948
|
-
async function inputControlled(unifiedSession, event, timeout) {
|
|
1103
|
+
async function inputControlled(unifiedSession, event, timeout, frame) {
|
|
949
1104
|
if (!event.target) {
|
|
950
1105
|
throw new Error('controlled 输入需要 target 参数定位输入元素');
|
|
951
1106
|
}
|
|
@@ -956,62 +1111,25 @@ async function inputControlled(unifiedSession, event, timeout) {
|
|
|
956
1111
|
const elements = await unifiedSession.find(selector, searchText, xpath, timeout);
|
|
957
1112
|
const nth = nthParam ?? 0;
|
|
958
1113
|
if (elements.length === 0 || nth >= elements.length) {
|
|
959
|
-
throw new StructuredToolError('TARGET_NOT_FOUND', `controlled 输入目标未找到: ${JSON.stringify(event.target)}`, '请检查 target 是否能定位到 input、textarea、select 或 contenteditable 元素;context.candidates 提供当前页面候选元素', await buildTargetDiagnosticContext(unifiedSession, event.target, elements.length, nth, timeout));
|
|
1114
|
+
throw new StructuredToolError('TARGET_NOT_FOUND', `controlled 输入目标未找到: ${JSON.stringify(event.target)}`, '请检查 target 是否能定位到 input、textarea、select 或 contenteditable 元素;context.candidates 提供当前页面候选元素', await buildTargetDiagnosticContext(unifiedSession, event.target, elements.length, nth, timeout, 'controlled 输入目标未找到', frame));
|
|
960
1115
|
}
|
|
961
1116
|
const result = await unifiedSession.dispatchInput(elements[nth].refId, event.text ?? '');
|
|
962
1117
|
if (!result.success) {
|
|
963
|
-
throw new StructuredToolError('CONTROLLED_INPUT_FAILED', result.error || 'controlled 输入失败', '请检查目标元素是否可编辑、未 disabled,并确认当前 frame 与 target 匹配', await buildTargetDiagnosticContext(unifiedSession, event.target, elements.length, nth, timeout, result.error || 'controlled 输入失败'));
|
|
1118
|
+
throw new StructuredToolError('CONTROLLED_INPUT_FAILED', result.error || 'controlled 输入失败', '请检查目标元素是否可编辑、未 disabled,并确认当前 frame 与 target 匹配', await buildTargetDiagnosticContext(unifiedSession, event.target, elements.length, nth, timeout, result.error || 'controlled 输入失败', frame));
|
|
964
1119
|
}
|
|
965
1120
|
}
|
|
966
|
-
async function buildTargetDiagnosticContext(unifiedSession, target, matchCount, nth, timeout, reason = '目标元素未找到') {
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
activeElement: active ? {
|
|
975
|
-
tag: active.tagName.toLowerCase(),
|
|
976
|
-
id: active.id || undefined,
|
|
977
|
-
className: typeof active.className === 'string' ? active.className : undefined,
|
|
978
|
-
text: (active.textContent || '').trim().slice(0, 80),
|
|
979
|
-
value: 'value' in active ? active.value : undefined,
|
|
980
|
-
selectionStart: 'selectionStart' in active ? active.selectionStart : undefined,
|
|
981
|
-
selectionEnd: 'selectionEnd' in active ? active.selectionEnd : undefined
|
|
982
|
-
} : null,
|
|
983
|
-
selection: {
|
|
984
|
-
text: selection ? selection.toString().slice(0, 160) : '',
|
|
985
|
-
collapsed: selection ? selection.isCollapsed : true,
|
|
986
|
-
anchorNode: selection && selection.anchorNode ? selection.anchorNode.nodeName : null,
|
|
987
|
-
focusNode: selection && selection.focusNode ? selection.focusNode.nodeName : null
|
|
988
|
-
},
|
|
989
|
-
candidates: candidates.map(function(el) {
|
|
990
|
-
const rect = el.getBoundingClientRect();
|
|
991
|
-
return {
|
|
992
|
-
tag: el.tagName.toLowerCase(),
|
|
993
|
-
id: el.id || undefined,
|
|
994
|
-
role: el.getAttribute('role') || undefined,
|
|
995
|
-
text: (el.textContent || el.getAttribute('aria-label') || el.getAttribute('placeholder') || '').trim().slice(0, 80),
|
|
996
|
-
visible: rect.width > 0 && rect.height > 0,
|
|
997
|
-
disabled: !!el.disabled || el.getAttribute('aria-disabled') === 'true',
|
|
998
|
-
bounds: {x: rect.x, y: rect.y, width: rect.width, height: rect.height}
|
|
999
|
-
};
|
|
1000
|
-
})
|
|
1001
|
-
};
|
|
1002
|
-
})()`, undefined, timeout);
|
|
1003
|
-
}
|
|
1004
|
-
catch (err) {
|
|
1005
|
-
page = { diagnosticError: err instanceof Error ? err.message : String(err) };
|
|
1006
|
-
}
|
|
1007
|
-
return { reason, target, matchCount, nth, page };
|
|
1121
|
+
async function buildTargetDiagnosticContext(unifiedSession, target, matchCount, nth, timeout, reason = '目标元素未找到', frame) {
|
|
1122
|
+
return buildTargetDiagnostics(unifiedSession, target, {
|
|
1123
|
+
nth,
|
|
1124
|
+
timeout,
|
|
1125
|
+
frame,
|
|
1126
|
+
matchCount,
|
|
1127
|
+
lastState: reason,
|
|
1128
|
+
});
|
|
1008
1129
|
}
|
|
1009
|
-
async function editorAction(unifiedSession, action, event, timeout) {
|
|
1130
|
+
async function editorAction(unifiedSession, action, event, timeout, frame) {
|
|
1010
1131
|
if (event.target) {
|
|
1011
|
-
|
|
1012
|
-
if (!focused) {
|
|
1013
|
-
throw new Error('editor 目标未找到或未成功聚焦');
|
|
1014
|
-
}
|
|
1132
|
+
await focusTargetForCommands(unifiedSession, event.target, timeout, frame);
|
|
1015
1133
|
}
|
|
1016
1134
|
return unifiedSession.evaluate(`function(action, text, command) {
|
|
1017
1135
|
var active = document.activeElement;
|
|
@@ -1074,7 +1192,7 @@ async function editorAction(unifiedSession, action, event, timeout) {
|
|
|
1074
1192
|
return {success: true, command: command};
|
|
1075
1193
|
}`, undefined, timeout, [action, event.text ?? '', event.command ?? '']);
|
|
1076
1194
|
}
|
|
1077
|
-
async function getTargetPointExtension(unifiedSession, target, timeout) {
|
|
1195
|
+
async function getTargetPointExtension(unifiedSession, target, timeout, frame) {
|
|
1078
1196
|
const frameOffset = unifiedSession.getFrameOffset();
|
|
1079
1197
|
const isStealth = unifiedSession.getInputMode() === 'stealth';
|
|
1080
1198
|
// 原始坐标:用户意图为 iframe 相对坐标
|
|
@@ -1089,10 +1207,10 @@ async function getTargetPointExtension(unifiedSession, target, timeout) {
|
|
|
1089
1207
|
const elements = await unifiedSession.find(selector, text, xpath, timeout);
|
|
1090
1208
|
const nth = nthParam ?? 0;
|
|
1091
1209
|
if (elements.length === 0) {
|
|
1092
|
-
throw new StructuredToolError('TARGET_NOT_FOUND', `未找到目标元素: ${JSON.stringify(target)}`, '请检查 target 是否正确,或先用 extract type=state 查看 interactiveElements 候选', await buildTargetDiagnosticContext(unifiedSession, target, elements.length, nth, timeout));
|
|
1210
|
+
throw new StructuredToolError('TARGET_NOT_FOUND', `未找到目标元素: ${JSON.stringify(target)}`, '请检查 target 是否正确,或先用 extract type=state 查看 interactiveElements 候选', await buildTargetDiagnosticContext(unifiedSession, target, elements.length, nth, timeout, '目标元素未找到', frame));
|
|
1093
1211
|
}
|
|
1094
1212
|
if (nth >= elements.length) {
|
|
1095
|
-
throw new StructuredToolError('TARGET_INDEX_OUT_OF_RANGE', `第 ${nth} 个匹配元素不存在(共 ${elements.length} 个)`, '请降低 nth,或先用 extract type=state 查看当前匹配到的候选元素', await buildTargetDiagnosticContext(unifiedSession, target, elements.length, nth, timeout, '目标元素序号越界'));
|
|
1213
|
+
throw new StructuredToolError('TARGET_INDEX_OUT_OF_RANGE', `第 ${nth} 个匹配元素不存在(共 ${elements.length} 个)`, '请降低 nth,或先用 extract type=state 查看当前匹配到的候选元素', await buildTargetDiagnosticContext(unifiedSession, target, elements.length, nth, timeout, '目标元素序号越界', frame));
|
|
1096
1214
|
}
|
|
1097
1215
|
// 视口外时滚动后重新取 rect:与 actionableClick (left+single) 行为对齐,
|
|
1098
1216
|
// 否则非左键 / 多击的坐标路径在视口外坐标 dispatch,浏览器找不到元素,事件丢失
|