@pyrokine/mcp-chrome 2.3.1 → 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 +38 -11
- package/README_zh.md +35 -8
- 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 +21 -6
- 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 +320 -181
- 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 +49 -83
- 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 +203 -217
- package/dist/tools/wait.js.map +1 -1
- package/extension/dist/assets/content.ts-pEASbjEL.js +6 -0
- package/extension/dist/assets/index.ts-BMmrcSQ6.js +8713 -0
- package/extension/dist/assets/popup-BwuBB9Cz.css +142 -0
- package/extension/dist/assets/popup.html-B81xccfq.js +127 -0
- package/extension/dist/icons/icon128.png +0 -0
- package/extension/dist/icons/icon16.png +0 -0
- package/extension/dist/icons/icon48.png +0 -0
- package/extension/dist/manifest.json +63 -0
- package/extension/dist/public/icons/icon128.png +0 -0
- package/extension/dist/public/icons/icon16.png +0 -0
- package/extension/dist/public/icons/icon48.png +0 -0
- package/extension/dist/service-worker-loader.js +1 -0
- package/extension/dist/src/popup/popup.html +50 -0
- package/package.json +16 -11
package/dist/tools/input.js
CHANGED
|
@@ -11,8 +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';
|
|
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
|
+
}
|
|
16
33
|
/**
|
|
17
34
|
* InputEvent schema
|
|
18
35
|
*/
|
|
@@ -61,7 +78,13 @@ const inputEventSchema = z.object({
|
|
|
61
78
|
deltaY: z.number().optional().describe('垂直滚动量'),
|
|
62
79
|
text: z.string().max(10000).optional().describe('输入文本(type,最大 10000 字符)或替换文本(replace)'),
|
|
63
80
|
delay: z.number().min(0).max(100).optional().describe('按键间隔毫秒(type 事件最大 100ms,避免长时延 DoS)'),
|
|
64
|
-
ms: z
|
|
81
|
+
ms: z
|
|
82
|
+
.number()
|
|
83
|
+
.int()
|
|
84
|
+
.min(0)
|
|
85
|
+
.max(INPUT_WAIT_MAX_MS)
|
|
86
|
+
.optional()
|
|
87
|
+
.describe('等待毫秒(wait 事件最大 60000ms,且不能超过 input 剩余 timeout)'),
|
|
65
88
|
find: z.string().optional().describe('要查找并选中的文本(select/replace)'),
|
|
66
89
|
nth: z.number().optional().describe('第 N 个匹配(select/replace,从 0 开始,默认 0 即第一个)'),
|
|
67
90
|
command: z.string().optional().describe('浏览器编辑命令(editorCommand),如 bold、italic、insertOrderedList'),
|
|
@@ -131,23 +154,65 @@ async function handleInput(args) {
|
|
|
131
154
|
const humanize = args.humanize ?? false;
|
|
132
155
|
return await unifiedSession.withTabId(args.tabId, async () => {
|
|
133
156
|
return await unifiedSession.withFrame(args.frame, async () => {
|
|
134
|
-
const
|
|
157
|
+
const diagnostics = await startDiagnostics(unifiedSession, args.diagnostics);
|
|
135
158
|
const warnings = [];
|
|
136
159
|
const eventResults = [];
|
|
137
160
|
const session = mode === 'extension' ? undefined : getSession();
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
161
|
+
const inputStartedAt = Date.now();
|
|
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
|
+
}
|
|
142
179
|
}
|
|
143
|
-
|
|
144
|
-
|
|
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
|
+
}
|
|
145
204
|
}
|
|
205
|
+
return response;
|
|
146
206
|
}
|
|
147
207
|
const result = {
|
|
148
208
|
success: true,
|
|
149
|
-
eventsExecuted
|
|
209
|
+
eventsExecuted,
|
|
150
210
|
mode,
|
|
211
|
+
actionExecuted: true,
|
|
212
|
+
actionStatus: 'completed',
|
|
213
|
+
verificationRequested: Boolean(args.postCondition),
|
|
214
|
+
verificationStatus: 'unavailable',
|
|
215
|
+
retryable: false,
|
|
151
216
|
};
|
|
152
217
|
if (warnings.length > 0) {
|
|
153
218
|
result.warnings = warnings;
|
|
@@ -156,17 +221,46 @@ async function handleInput(args) {
|
|
|
156
221
|
result.eventResults = eventResults;
|
|
157
222
|
}
|
|
158
223
|
if (args.postCondition) {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
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
|
+
}
|
|
163
232
|
}
|
|
233
|
+
appendDiagnostics(result, await finishDiagnostics(unifiedSession, diagnostics));
|
|
164
234
|
return formatResponse(result);
|
|
165
235
|
}); // withFrame
|
|
166
236
|
}); // withTabId
|
|
167
237
|
}
|
|
168
238
|
catch (error) {
|
|
169
|
-
|
|
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;
|
|
170
264
|
}
|
|
171
265
|
}
|
|
172
266
|
const unifiedOnlyEventTypes = new Set([
|
|
@@ -178,7 +272,7 @@ const unifiedOnlyEventTypes = new Set([
|
|
|
178
272
|
]);
|
|
179
273
|
async function executeInputEvent(context, event) {
|
|
180
274
|
if (context.mode === 'extension' || unifiedOnlyEventTypes.has(event.type)) {
|
|
181
|
-
return executeUnifiedEvent(context.unifiedSession, event, context.humanize, context.timeout);
|
|
275
|
+
return executeUnifiedEvent(context.unifiedSession, event, context.humanize, context.timeout, context.frame);
|
|
182
276
|
}
|
|
183
277
|
if (!context.session) {
|
|
184
278
|
throw new Error('CDP 输入事件缺少 session');
|
|
@@ -186,27 +280,6 @@ async function executeInputEvent(context, event) {
|
|
|
186
280
|
await executeCdpEvent(context.session, event, context.humanize, context.timeout);
|
|
187
281
|
return undefined;
|
|
188
282
|
}
|
|
189
|
-
async function captureDiagnosticsStart(unifiedSession) {
|
|
190
|
-
await unifiedSession.enableConsole();
|
|
191
|
-
await unifiedSession.enableNetwork();
|
|
192
|
-
const consoleLogs = await unifiedSession.getConsoleLogs();
|
|
193
|
-
const network = await unifiedSession.getNetworkRequests();
|
|
194
|
-
return { consoleCount: consoleLogs.length, networkCount: network.length };
|
|
195
|
-
}
|
|
196
|
-
async function captureDiagnosticsDelta(unifiedSession, start) {
|
|
197
|
-
const consoleLogs = await unifiedSession.getConsoleLogs();
|
|
198
|
-
const network = await unifiedSession.getNetworkRequests();
|
|
199
|
-
return {
|
|
200
|
-
console: consoleLogs
|
|
201
|
-
.slice(start.consoleCount)
|
|
202
|
-
.filter((item) => ['error', 'warning', 'warn'].includes(item.level))
|
|
203
|
-
.slice(-20),
|
|
204
|
-
failedRequests: network
|
|
205
|
-
.slice(start.networkCount)
|
|
206
|
-
.filter((item) => item.errorText || (item.status !== undefined && item.status >= 400))
|
|
207
|
-
.slice(-20),
|
|
208
|
-
};
|
|
209
|
-
}
|
|
210
283
|
/**
|
|
211
284
|
* 通过真实鼠标事件选中页面文本
|
|
212
285
|
*
|
|
@@ -220,15 +293,20 @@ async function captureDiagnosticsDelta(unifiedSession, start) {
|
|
|
220
293
|
* 如果 target 是选择器类型,先通过 actionableClick 聚焦
|
|
221
294
|
* select/replace 事件用,保证选区建立前 activeElement 就是目标
|
|
222
295
|
*/
|
|
223
|
-
async function focusTargetForCommands(unifiedSession, target, timeout) {
|
|
296
|
+
async function focusTargetForCommands(unifiedSession, target, timeout, frame) {
|
|
224
297
|
if ('x' in target || 'y' in target) {
|
|
225
|
-
|
|
298
|
+
const point = await getTargetPointExtension(unifiedSession, target, timeout, frame);
|
|
299
|
+
await unifiedSession.mouseMove(point.x, point.y);
|
|
300
|
+
await unifiedSession.mouseClick('left');
|
|
301
|
+
return;
|
|
226
302
|
}
|
|
227
303
|
const params = targetToFindParams(target);
|
|
228
304
|
const els = await unifiedSession.find(params.selector, params.text, params.xpath, timeout);
|
|
229
305
|
const nth0 = params.nth ?? 0;
|
|
230
306
|
if (els.length <= nth0) {
|
|
231
|
-
|
|
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));
|
|
232
310
|
}
|
|
233
311
|
const focused = await unifiedSession.evaluate(`(() => {
|
|
234
312
|
const ref = window.__mcpElementMap?.[${JSON.stringify(els[nth0].refId)}]
|
|
@@ -239,27 +317,38 @@ async function focusTargetForCommands(unifiedSession, target, timeout) {
|
|
|
239
317
|
el.focus()
|
|
240
318
|
return document.activeElement === el
|
|
241
319
|
})()`, undefined, timeout);
|
|
242
|
-
|
|
320
|
+
if (focused !== true) {
|
|
321
|
+
throw new StructuredToolError('TARGET_FOCUS_FAILED', '命令目标未成功聚焦', '请检查目标是否可编辑、未 disabled 并处于正确 frame', await buildTargetDiagnosticContext(unifiedSession, target, els.length, nth0, timeout, '命令目标聚焦失败', frame));
|
|
322
|
+
}
|
|
243
323
|
}
|
|
244
|
-
async function focusTargetIfNeeded(unifiedSession, target,
|
|
245
|
-
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');
|
|
246
332
|
return;
|
|
247
333
|
}
|
|
248
334
|
const params = targetToFindParams(target);
|
|
249
335
|
const els = await unifiedSession.find(params.selector, params.text, params.xpath, timeout);
|
|
250
|
-
const nth0 = params.nth ??
|
|
251
|
-
if (els.length
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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
|
+
});
|
|
259
348
|
}
|
|
260
349
|
}
|
|
261
|
-
async function collectTextSelectionDiagnostics(unifiedSession, findText, scopeTarget,
|
|
262
|
-
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) {
|
|
263
352
|
function selectorFor(el) {
|
|
264
353
|
if (!(el instanceof Element)) return null;
|
|
265
354
|
if (el.id) return '#' + CSS.escape(el.id);
|
|
@@ -300,22 +389,21 @@ async function collectTextSelectionDiagnostics(unifiedSession, findText, scopeTa
|
|
|
300
389
|
}
|
|
301
390
|
var root = document.body;
|
|
302
391
|
if (scopeXpath) {
|
|
303
|
-
var xr = document.evaluate(scopeXpath, document, null, XPathResult.
|
|
304
|
-
root = xr.
|
|
392
|
+
var xr = document.evaluate(scopeXpath, document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
|
|
393
|
+
root = xr.snapshotItem(scopeNth);
|
|
305
394
|
} else if (scopeSelector) {
|
|
306
|
-
var candidates = document.querySelectorAll(scopeSelector);
|
|
395
|
+
var candidates = Array.from(document.querySelectorAll(scopeSelector));
|
|
307
396
|
if (scopeText) {
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
}
|
|
311
|
-
} else {
|
|
312
|
-
root = candidates[0];
|
|
397
|
+
candidates = candidates.filter(function(candidate) {
|
|
398
|
+
return (candidate.textContent || '').includes(scopeText);
|
|
399
|
+
});
|
|
313
400
|
}
|
|
401
|
+
root = candidates[scopeNth];
|
|
314
402
|
} else if (scopeText) {
|
|
315
|
-
var all = document.querySelectorAll('*')
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
403
|
+
var all = Array.from(document.querySelectorAll('*')).filter(function(candidate) {
|
|
404
|
+
return (candidate.textContent || '').includes(scopeText);
|
|
405
|
+
});
|
|
406
|
+
root = all[scopeNth];
|
|
319
407
|
}
|
|
320
408
|
var candidateElements = [];
|
|
321
409
|
if (root instanceof Element) {
|
|
@@ -327,7 +415,8 @@ async function collectTextSelectionDiagnostics(unifiedSession, findText, scopeTa
|
|
|
327
415
|
return {
|
|
328
416
|
scope: {
|
|
329
417
|
target: scopeTarget,
|
|
330
|
-
|
|
418
|
+
targetNth: scopeNth,
|
|
419
|
+
occurrenceNth: occurrenceNth,
|
|
331
420
|
findText: findText,
|
|
332
421
|
selector: scopeSelector,
|
|
333
422
|
text: scopeText,
|
|
@@ -348,40 +437,41 @@ async function collectTextSelectionDiagnostics(unifiedSession, findText, scopeTa
|
|
|
348
437
|
},
|
|
349
438
|
candidates: candidateElements.map(summarizeElement).filter(Boolean)
|
|
350
439
|
};
|
|
351
|
-
}`, undefined, timeout, [findText,
|
|
440
|
+
}`, undefined, timeout, [findText, occurrenceNth, scopeNth, scopeTarget ?? null, scopeSelector, scopeText, scopeXpath]);
|
|
352
441
|
}
|
|
353
442
|
async function selectText(unifiedSession, findText, scopeTarget, nth = 0, timeout) {
|
|
354
443
|
// 将 target 转为查询参数,传入注入脚本进行 DOM 查询
|
|
355
444
|
let scopeSelector = null;
|
|
356
445
|
let scopeText = null;
|
|
357
446
|
let scopeXpath = null;
|
|
447
|
+
let scopeNth = 0;
|
|
358
448
|
if (scopeTarget && !('x' in scopeTarget) && !('y' in scopeTarget)) {
|
|
359
449
|
const params = targetToFindParams(scopeTarget);
|
|
360
450
|
scopeSelector = params.selector ?? null;
|
|
361
451
|
scopeText = params.text ?? null;
|
|
362
452
|
scopeXpath = params.xpath ?? null;
|
|
453
|
+
scopeNth = params.nth ?? 0;
|
|
363
454
|
}
|
|
364
455
|
// Step 1: 注入脚本定位文本
|
|
365
|
-
const result = await unifiedSession.evaluate(`function(findText, nth, scopeSelector, scopeText, scopeXpath) {
|
|
456
|
+
const result = await unifiedSession.evaluate(`function(findText, nth, scopeNth, scopeSelector, scopeText, scopeXpath) {
|
|
366
457
|
// 确定搜索根节点
|
|
367
458
|
var root = document.body;
|
|
368
459
|
if (scopeXpath) {
|
|
369
|
-
var xr = document.evaluate(scopeXpath, document, null, XPathResult.
|
|
370
|
-
root = xr.
|
|
460
|
+
var xr = document.evaluate(scopeXpath, document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
|
|
461
|
+
root = xr.snapshotItem(scopeNth);
|
|
371
462
|
} else if (scopeSelector) {
|
|
372
|
-
var candidates = document.querySelectorAll(scopeSelector);
|
|
463
|
+
var candidates = Array.from(document.querySelectorAll(scopeSelector));
|
|
373
464
|
if (scopeText) {
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
}
|
|
377
|
-
} else {
|
|
378
|
-
root = candidates[0];
|
|
465
|
+
candidates = candidates.filter(function(candidate) {
|
|
466
|
+
return (candidate.textContent || '').includes(scopeText);
|
|
467
|
+
});
|
|
379
468
|
}
|
|
469
|
+
root = candidates[scopeNth];
|
|
380
470
|
} else if (scopeText) {
|
|
381
|
-
var all = document.querySelectorAll('*')
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
471
|
+
var all = Array.from(document.querySelectorAll('*')).filter(function(candidate) {
|
|
472
|
+
return (candidate.textContent || '').includes(scopeText);
|
|
473
|
+
});
|
|
474
|
+
root = all[scopeNth];
|
|
385
475
|
}
|
|
386
476
|
if (!root) return {type: 'noscope'};
|
|
387
477
|
|
|
@@ -389,6 +479,11 @@ async function selectText(unifiedSession, findText, scopeTarget, nth = 0, timeou
|
|
|
389
479
|
var tag = root.tagName;
|
|
390
480
|
if (tag === 'INPUT' || tag === 'TEXTAREA') {
|
|
391
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
|
+
}
|
|
392
487
|
var pos = -1;
|
|
393
488
|
for (var n = 0; n <= nth; n++) {
|
|
394
489
|
pos = val.indexOf(findText, pos + (n > 0 ? 1 : 0));
|
|
@@ -396,9 +491,7 @@ async function selectText(unifiedSession, findText, scopeTarget, nth = 0, timeou
|
|
|
396
491
|
}
|
|
397
492
|
// 原子化:定位到 input 同时完成 focus + setSelectionRange,避免外层 mouseClick 聚焦不可靠
|
|
398
493
|
root.focus();
|
|
399
|
-
|
|
400
|
-
root.setSelectionRange(pos, pos + findText.length);
|
|
401
|
-
}
|
|
494
|
+
root.setSelectionRange(pos, pos + findText.length);
|
|
402
495
|
return {type: 'input', selectionStart: pos, selectionEnd: pos + findText.length};
|
|
403
496
|
}
|
|
404
497
|
|
|
@@ -407,16 +500,19 @@ async function selectText(unifiedSession, findText, scopeTarget, nth = 0, timeou
|
|
|
407
500
|
for (var k = 0; k < inputs.length; k++) {
|
|
408
501
|
var inp = inputs[k];
|
|
409
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;
|
|
410
505
|
var ip = -1;
|
|
411
506
|
for (var n2 = 0; n2 <= nth; n2++) {
|
|
412
507
|
ip = v.indexOf(findText, ip + (n2 > 0 ? 1 : 0));
|
|
413
508
|
if (ip === -1) break;
|
|
414
509
|
}
|
|
510
|
+
if (ip !== -1 && !childSelectionCapable) {
|
|
511
|
+
return {type: 'unsupported', tag: inp.tagName.toLowerCase(), inputType: childType, currentValue: String(v).slice(0, 160)};
|
|
512
|
+
}
|
|
415
513
|
if (ip !== -1) {
|
|
416
514
|
inp.focus();
|
|
417
|
-
|
|
418
|
-
inp.setSelectionRange(ip, ip + findText.length);
|
|
419
|
-
}
|
|
515
|
+
inp.setSelectionRange(ip, ip + findText.length);
|
|
420
516
|
return {type: 'input', selectionStart: ip, selectionEnd: ip + findText.length};
|
|
421
517
|
}
|
|
422
518
|
}
|
|
@@ -481,13 +577,22 @@ async function selectText(unifiedSession, findText, scopeTarget, nth = 0, timeou
|
|
|
481
577
|
endY: er.y + er.height / 2,
|
|
482
578
|
formatted: formatted || undefined
|
|
483
579
|
};
|
|
484
|
-
}`, undefined, timeout, [findText, nth, scopeSelector, scopeText, scopeXpath]);
|
|
580
|
+
}`, undefined, timeout, [findText, nth, scopeNth, scopeSelector, scopeText, scopeXpath]);
|
|
485
581
|
if (!result || result.type === 'noscope') {
|
|
486
|
-
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);
|
|
487
583
|
throw new StructuredToolError('TEXT_SCOPE_NOT_FOUND', `未找到目标元素: ${JSON.stringify(scopeTarget)}`, '请检查 target 是否能定位到包含目标文本的元素,或先用 extract(state) 查看可交互元素', diagnostics);
|
|
488
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
|
+
}
|
|
489
594
|
if (result.type === 'notfound') {
|
|
490
|
-
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);
|
|
491
596
|
throw new StructuredToolError('TEXT_NOT_FOUND', scopeTarget
|
|
492
597
|
? `目标元素内未找到文本 "${findText}"${nth > 0 ? `(第 ${nth} 个匹配)` : ''}`
|
|
493
598
|
: `未找到文本: "${findText}"${nth > 0 ? `(第 ${nth} 个匹配)` : ''}`, '请检查 find 文本、nth 序号和 target 范围;context.candidates 提供了当前范围内的候选文本和 selector', diagnostics);
|
|
@@ -596,7 +701,7 @@ function validateEvent(event) {
|
|
|
596
701
|
}
|
|
597
702
|
break;
|
|
598
703
|
case 'type':
|
|
599
|
-
if (
|
|
704
|
+
if (event.text === undefined) {
|
|
600
705
|
requiredEventParam(event, 'text');
|
|
601
706
|
}
|
|
602
707
|
break;
|
|
@@ -638,13 +743,13 @@ function validateEvent(event) {
|
|
|
638
743
|
break;
|
|
639
744
|
}
|
|
640
745
|
}
|
|
641
|
-
async function executeUnifiedEvent(unifiedSession, event, humanize, timeout) {
|
|
746
|
+
async function executeUnifiedEvent(unifiedSession, event, humanize, timeout, frame) {
|
|
642
747
|
validateEvent(event);
|
|
643
748
|
const handler = unifiedInputHandlers[event.type];
|
|
644
749
|
if (!handler) {
|
|
645
750
|
throw new Error(`未知事件类型: ${event.type}`);
|
|
646
751
|
}
|
|
647
|
-
return handler({ unifiedSession, humanize, timeout }, event);
|
|
752
|
+
return handler({ unifiedSession, humanize, timeout, frame }, event);
|
|
648
753
|
}
|
|
649
754
|
const unifiedInputHandlers = {
|
|
650
755
|
keydown: handleUnifiedKeyDown,
|
|
@@ -666,15 +771,12 @@ const unifiedInputHandlers = {
|
|
|
666
771
|
editorCommand: handleUnifiedEditorCommand,
|
|
667
772
|
drag: handleUnifiedDrag,
|
|
668
773
|
};
|
|
669
|
-
async function handleUnifiedKeyDown({ unifiedSession, timeout }, event) {
|
|
774
|
+
async function handleUnifiedKeyDown({ unifiedSession, timeout, frame }, event) {
|
|
670
775
|
if (unifiedSession.getInputMode() === 'stealth' && event.commands && event.commands.length > 0) {
|
|
671
776
|
throw new Error('commands 参数不支持 stealth 输入模式,请先调用 manage action=inputMode inputMode=precise 切换后重试');
|
|
672
777
|
}
|
|
673
778
|
if (event.commands && event.commands.length > 0 && event.target) {
|
|
674
|
-
|
|
675
|
-
if (!focused) {
|
|
676
|
-
throw new Error('commands 目标未找到或未成功聚焦');
|
|
677
|
-
}
|
|
779
|
+
await focusTargetForCommands(unifiedSession, event.target, timeout, frame);
|
|
678
780
|
}
|
|
679
781
|
await unifiedSession.keyDown(event.key, event.commands);
|
|
680
782
|
if (event.commands && event.commands.length > 0) {
|
|
@@ -686,7 +788,7 @@ async function handleUnifiedKeyUp({ unifiedSession }, event) {
|
|
|
686
788
|
await unifiedSession.keyUp(event.key);
|
|
687
789
|
return undefined;
|
|
688
790
|
}
|
|
689
|
-
async function handleUnifiedClick({ unifiedSession, timeout }, event) {
|
|
791
|
+
async function handleUnifiedClick({ unifiedSession, timeout, frame }, event) {
|
|
690
792
|
const button = event.button ?? 'left';
|
|
691
793
|
const clickCount = event.clickCount ?? 1;
|
|
692
794
|
if (!event.target) {
|
|
@@ -694,7 +796,7 @@ async function handleUnifiedClick({ unifiedSession, timeout }, event) {
|
|
|
694
796
|
return undefined;
|
|
695
797
|
}
|
|
696
798
|
if ('x' in event.target && 'y' in event.target) {
|
|
697
|
-
const point = await getTargetPointExtension(unifiedSession, event.target, timeout);
|
|
799
|
+
const point = await getTargetPointExtension(unifiedSession, event.target, timeout, frame);
|
|
698
800
|
await unifiedSession.mouseMove(point.x, point.y);
|
|
699
801
|
await unifiedSession.mouseClick(button, clickCount);
|
|
700
802
|
return undefined;
|
|
@@ -712,14 +814,14 @@ async function handleUnifiedClick({ unifiedSession, timeout }, event) {
|
|
|
712
814
|
return undefined;
|
|
713
815
|
}
|
|
714
816
|
}
|
|
715
|
-
const point = await getTargetPointExtension(unifiedSession, event.target, timeout);
|
|
817
|
+
const point = await getTargetPointExtension(unifiedSession, event.target, timeout, frame);
|
|
716
818
|
await unifiedSession.mouseMove(point.x, point.y);
|
|
717
819
|
await unifiedSession.mouseClick(button, clickCount, typeof point.refId === 'string' ? point.refId : undefined);
|
|
718
820
|
return undefined;
|
|
719
821
|
}
|
|
720
|
-
async function handleUnifiedMouseDown({ unifiedSession, timeout }, event) {
|
|
822
|
+
async function handleUnifiedMouseDown({ unifiedSession, timeout, frame }, event) {
|
|
721
823
|
if (event.target) {
|
|
722
|
-
const point = await getTargetPointExtension(unifiedSession, event.target, timeout);
|
|
824
|
+
const point = await getTargetPointExtension(unifiedSession, event.target, timeout, frame);
|
|
723
825
|
await unifiedSession.mouseMove(point.x, point.y);
|
|
724
826
|
}
|
|
725
827
|
await unifiedSession.mouseDown(event.button ?? 'left');
|
|
@@ -729,8 +831,8 @@ async function handleUnifiedMouseUp({ unifiedSession }, event) {
|
|
|
729
831
|
await unifiedSession.mouseUp(event.button ?? 'left');
|
|
730
832
|
return undefined;
|
|
731
833
|
}
|
|
732
|
-
async function handleUnifiedMouseMove({ unifiedSession, humanize, timeout }, event) {
|
|
733
|
-
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);
|
|
734
836
|
if (humanize && event.steps && event.steps > 1) {
|
|
735
837
|
const path = generateBezierPath(unifiedSession.getMousePosition(), point, event.steps);
|
|
736
838
|
for (const p of path) {
|
|
@@ -743,7 +845,7 @@ async function handleUnifiedMouseMove({ unifiedSession, humanize, timeout }, eve
|
|
|
743
845
|
}
|
|
744
846
|
return undefined;
|
|
745
847
|
}
|
|
746
|
-
async function handleUnifiedWheel({ unifiedSession, timeout }, event) {
|
|
848
|
+
async function handleUnifiedWheel({ unifiedSession, timeout, frame }, event) {
|
|
747
849
|
if (event.target) {
|
|
748
850
|
const { selector, text, xpath, nth: nthParam } = targetToFindParams(event.target);
|
|
749
851
|
const elements = await unifiedSession.find(selector, text, xpath, timeout);
|
|
@@ -752,19 +854,19 @@ async function handleUnifiedWheel({ unifiedSession, timeout }, event) {
|
|
|
752
854
|
await unifiedSession.scroll(event.deltaX ?? 0, event.deltaY ?? 0, elements[nth].refId);
|
|
753
855
|
return undefined;
|
|
754
856
|
}
|
|
755
|
-
const point = await getTargetPointExtension(unifiedSession, event.target, timeout);
|
|
857
|
+
const point = await getTargetPointExtension(unifiedSession, event.target, timeout, frame);
|
|
756
858
|
await unifiedSession.mouseMove(point.x, point.y);
|
|
757
859
|
}
|
|
758
860
|
await unifiedSession.mouseWheel(event.deltaX ?? 0, event.deltaY ?? 0);
|
|
759
861
|
return undefined;
|
|
760
862
|
}
|
|
761
|
-
async function handleUnifiedTouchStart({ unifiedSession, timeout }, event) {
|
|
762
|
-
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);
|
|
763
865
|
await unifiedSession.touchStart(point.x, point.y);
|
|
764
866
|
return undefined;
|
|
765
867
|
}
|
|
766
|
-
async function handleUnifiedTouchMove({ unifiedSession, timeout }, event) {
|
|
767
|
-
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);
|
|
768
870
|
await unifiedSession.touchMove(point.x, point.y);
|
|
769
871
|
return undefined;
|
|
770
872
|
}
|
|
@@ -772,13 +874,13 @@ async function handleUnifiedTouchEnd({ unifiedSession }) {
|
|
|
772
874
|
await unifiedSession.touchEnd();
|
|
773
875
|
return undefined;
|
|
774
876
|
}
|
|
775
|
-
async function handleUnifiedType({ unifiedSession, humanize, timeout }, event) {
|
|
877
|
+
async function handleUnifiedType({ unifiedSession, humanize, timeout, frame }, event) {
|
|
776
878
|
if (event.mode === 'controlled' || event.dispatch) {
|
|
777
|
-
await inputControlled(unifiedSession, event, timeout);
|
|
879
|
+
await inputControlled(unifiedSession, event, timeout, frame);
|
|
778
880
|
return undefined;
|
|
779
881
|
}
|
|
780
882
|
if (event.target) {
|
|
781
|
-
const point = await getTargetPointExtension(unifiedSession, event.target, timeout);
|
|
883
|
+
const point = await getTargetPointExtension(unifiedSession, event.target, timeout, frame);
|
|
782
884
|
await unifiedSession.mouseMove(point.x, point.y);
|
|
783
885
|
await unifiedSession.mouseClick('left');
|
|
784
886
|
}
|
|
@@ -802,17 +904,90 @@ async function handleUnifiedType({ unifiedSession, humanize, timeout }, event) {
|
|
|
802
904
|
}
|
|
803
905
|
return undefined;
|
|
804
906
|
}
|
|
805
|
-
|
|
907
|
+
function ensureWaitFitsTimeout(ms, timeout) {
|
|
908
|
+
if (timeout !== undefined && ms > timeout) {
|
|
909
|
+
throw new Error(`events[].ms (${ms}ms) 超过 input 剩余 timeout (${timeout}ms)`);
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
async function handleUnifiedWait({ timeout }, event) {
|
|
913
|
+
ensureWaitFitsTimeout(event.ms, timeout);
|
|
806
914
|
await new Promise((resolve) => setTimeout(resolve, event.ms));
|
|
807
915
|
return undefined;
|
|
808
916
|
}
|
|
809
|
-
async function handleUnifiedSelect({ unifiedSession, timeout }, event) {
|
|
810
|
-
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
|
+
}
|
|
811
933
|
await selectText(unifiedSession, event.find, event.target, event.nth, timeout);
|
|
812
934
|
return undefined;
|
|
813
935
|
}
|
|
814
|
-
async function handleUnifiedReplace({ unifiedSession, timeout }, event) {
|
|
815
|
-
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
|
+
}
|
|
816
991
|
const formatted = await selectText(unifiedSession, event.find, event.target, event.nth, timeout);
|
|
817
992
|
let selectionConfirmed = false;
|
|
818
993
|
for (let i = 0; i < 25; i++) {
|
|
@@ -863,18 +1038,18 @@ async function handleUnifiedReplace({ unifiedSession, timeout }, event) {
|
|
|
863
1038
|
}
|
|
864
1039
|
return undefined;
|
|
865
1040
|
}
|
|
866
|
-
async function handleUnifiedEditorContext({ unifiedSession, timeout }, event) {
|
|
867
|
-
return (await editorAction(unifiedSession, 'context', event, timeout));
|
|
1041
|
+
async function handleUnifiedEditorContext({ unifiedSession, timeout, frame }, event) {
|
|
1042
|
+
return (await editorAction(unifiedSession, 'context', event, timeout, frame));
|
|
868
1043
|
}
|
|
869
|
-
async function handleUnifiedEditorInsert({ unifiedSession, timeout }, event) {
|
|
870
|
-
await editorAction(unifiedSession, 'insert', event, timeout);
|
|
1044
|
+
async function handleUnifiedEditorInsert({ unifiedSession, timeout, frame }, event) {
|
|
1045
|
+
await editorAction(unifiedSession, 'insert', event, timeout, frame);
|
|
871
1046
|
return undefined;
|
|
872
1047
|
}
|
|
873
|
-
async function handleUnifiedEditorCommand({ unifiedSession, timeout }, event) {
|
|
874
|
-
await editorAction(unifiedSession, 'command', event, timeout);
|
|
1048
|
+
async function handleUnifiedEditorCommand({ unifiedSession, timeout, frame }, event) {
|
|
1049
|
+
await editorAction(unifiedSession, 'command', event, timeout, frame);
|
|
875
1050
|
return undefined;
|
|
876
1051
|
}
|
|
877
|
-
async function handleUnifiedDrag({ unifiedSession, timeout }, event) {
|
|
1052
|
+
async function handleUnifiedDrag({ unifiedSession, timeout, frame }, event) {
|
|
878
1053
|
if (!event.target) {
|
|
879
1054
|
throw new Error('drag 事件需要 target 参数');
|
|
880
1055
|
}
|
|
@@ -895,10 +1070,10 @@ async function handleUnifiedDrag({ unifiedSession, timeout }, event) {
|
|
|
895
1070
|
const srcEls = await unifiedSession.find(srcParams.selector, srcParams.text, srcParams.xpath, timeout);
|
|
896
1071
|
const dstEls = await unifiedSession.find(dstParams.selector, dstParams.text, dstParams.xpath, timeout);
|
|
897
1072
|
if (srcEls.length <= srcNth) {
|
|
898
|
-
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));
|
|
899
1074
|
}
|
|
900
1075
|
if (dstEls.length <= dstNth) {
|
|
901
|
-
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));
|
|
902
1077
|
}
|
|
903
1078
|
return unifiedSession.dragAndDrop(srcEls[srcNth].refId, dstEls[dstNth].refId);
|
|
904
1079
|
};
|
|
@@ -925,7 +1100,7 @@ async function handleUnifiedDrag({ unifiedSession, timeout }, event) {
|
|
|
925
1100
|
* - precise 模式直接使用
|
|
926
1101
|
* - stealth 模式需减 offset 转为 iframe 相对坐标
|
|
927
1102
|
*/
|
|
928
|
-
async function inputControlled(unifiedSession, event, timeout) {
|
|
1103
|
+
async function inputControlled(unifiedSession, event, timeout, frame) {
|
|
929
1104
|
if (!event.target) {
|
|
930
1105
|
throw new Error('controlled 输入需要 target 参数定位输入元素');
|
|
931
1106
|
}
|
|
@@ -936,62 +1111,25 @@ async function inputControlled(unifiedSession, event, timeout) {
|
|
|
936
1111
|
const elements = await unifiedSession.find(selector, searchText, xpath, timeout);
|
|
937
1112
|
const nth = nthParam ?? 0;
|
|
938
1113
|
if (elements.length === 0 || nth >= elements.length) {
|
|
939
|
-
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));
|
|
940
1115
|
}
|
|
941
1116
|
const result = await unifiedSession.dispatchInput(elements[nth].refId, event.text ?? '');
|
|
942
1117
|
if (!result.success) {
|
|
943
|
-
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));
|
|
944
1119
|
}
|
|
945
1120
|
}
|
|
946
|
-
async function buildTargetDiagnosticContext(unifiedSession, target, matchCount, nth, timeout, reason = '目标元素未找到') {
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
activeElement: active ? {
|
|
955
|
-
tag: active.tagName.toLowerCase(),
|
|
956
|
-
id: active.id || undefined,
|
|
957
|
-
className: typeof active.className === 'string' ? active.className : undefined,
|
|
958
|
-
text: (active.textContent || '').trim().slice(0, 80),
|
|
959
|
-
value: 'value' in active ? active.value : undefined,
|
|
960
|
-
selectionStart: 'selectionStart' in active ? active.selectionStart : undefined,
|
|
961
|
-
selectionEnd: 'selectionEnd' in active ? active.selectionEnd : undefined
|
|
962
|
-
} : null,
|
|
963
|
-
selection: {
|
|
964
|
-
text: selection ? selection.toString().slice(0, 160) : '',
|
|
965
|
-
collapsed: selection ? selection.isCollapsed : true,
|
|
966
|
-
anchorNode: selection && selection.anchorNode ? selection.anchorNode.nodeName : null,
|
|
967
|
-
focusNode: selection && selection.focusNode ? selection.focusNode.nodeName : null
|
|
968
|
-
},
|
|
969
|
-
candidates: candidates.map(function(el) {
|
|
970
|
-
const rect = el.getBoundingClientRect();
|
|
971
|
-
return {
|
|
972
|
-
tag: el.tagName.toLowerCase(),
|
|
973
|
-
id: el.id || undefined,
|
|
974
|
-
role: el.getAttribute('role') || undefined,
|
|
975
|
-
text: (el.textContent || el.getAttribute('aria-label') || el.getAttribute('placeholder') || '').trim().slice(0, 80),
|
|
976
|
-
visible: rect.width > 0 && rect.height > 0,
|
|
977
|
-
disabled: !!el.disabled || el.getAttribute('aria-disabled') === 'true',
|
|
978
|
-
bounds: {x: rect.x, y: rect.y, width: rect.width, height: rect.height}
|
|
979
|
-
};
|
|
980
|
-
})
|
|
981
|
-
};
|
|
982
|
-
})()`, undefined, timeout);
|
|
983
|
-
}
|
|
984
|
-
catch (err) {
|
|
985
|
-
page = { diagnosticError: err instanceof Error ? err.message : String(err) };
|
|
986
|
-
}
|
|
987
|
-
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
|
+
});
|
|
988
1129
|
}
|
|
989
|
-
async function editorAction(unifiedSession, action, event, timeout) {
|
|
1130
|
+
async function editorAction(unifiedSession, action, event, timeout, frame) {
|
|
990
1131
|
if (event.target) {
|
|
991
|
-
|
|
992
|
-
if (!focused) {
|
|
993
|
-
throw new Error('editor 目标未找到或未成功聚焦');
|
|
994
|
-
}
|
|
1132
|
+
await focusTargetForCommands(unifiedSession, event.target, timeout, frame);
|
|
995
1133
|
}
|
|
996
1134
|
return unifiedSession.evaluate(`function(action, text, command) {
|
|
997
1135
|
var active = document.activeElement;
|
|
@@ -1054,7 +1192,7 @@ async function editorAction(unifiedSession, action, event, timeout) {
|
|
|
1054
1192
|
return {success: true, command: command};
|
|
1055
1193
|
}`, undefined, timeout, [action, event.text ?? '', event.command ?? '']);
|
|
1056
1194
|
}
|
|
1057
|
-
async function getTargetPointExtension(unifiedSession, target, timeout) {
|
|
1195
|
+
async function getTargetPointExtension(unifiedSession, target, timeout, frame) {
|
|
1058
1196
|
const frameOffset = unifiedSession.getFrameOffset();
|
|
1059
1197
|
const isStealth = unifiedSession.getInputMode() === 'stealth';
|
|
1060
1198
|
// 原始坐标:用户意图为 iframe 相对坐标
|
|
@@ -1069,10 +1207,10 @@ async function getTargetPointExtension(unifiedSession, target, timeout) {
|
|
|
1069
1207
|
const elements = await unifiedSession.find(selector, text, xpath, timeout);
|
|
1070
1208
|
const nth = nthParam ?? 0;
|
|
1071
1209
|
if (elements.length === 0) {
|
|
1072
|
-
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));
|
|
1073
1211
|
}
|
|
1074
1212
|
if (nth >= elements.length) {
|
|
1075
|
-
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));
|
|
1076
1214
|
}
|
|
1077
1215
|
// 视口外时滚动后重新取 rect:与 actionableClick (left+single) 行为对齐,
|
|
1078
1216
|
// 否则非左键 / 多击的坐标路径在视口外坐标 dispatch,浏览器找不到元素,事件丢失
|
|
@@ -1227,7 +1365,8 @@ async function handleCdpType({ session, humanize, timeout }, event) {
|
|
|
1227
1365
|
await session.type(event.text, delay);
|
|
1228
1366
|
}
|
|
1229
1367
|
}
|
|
1230
|
-
async function handleCdpWait(
|
|
1368
|
+
async function handleCdpWait({ timeout }, event) {
|
|
1369
|
+
ensureWaitFitsTimeout(event.ms, timeout);
|
|
1231
1370
|
await new Promise((resolve) => setTimeout(resolve, event.ms));
|
|
1232
1371
|
}
|
|
1233
1372
|
async function handleCdpDrag() {
|