browser-opt 1.0.23 → 1.0.25

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.
Files changed (57) hide show
  1. package/dist/browser-core/agent.d.ts +4 -0
  2. package/dist/browser-core/agent.d.ts.map +1 -1
  3. package/dist/browser-core/agent.js +13 -0
  4. package/dist/browser-core/agent.js.map +1 -1
  5. package/dist/browser-opt/index.d.ts +1 -1
  6. package/dist/browser-opt/index.d.ts.map +1 -1
  7. package/dist/browser-opt/runner/actions/index.d.ts +1 -1
  8. package/dist/browser-opt/runner/actions/index.d.ts.map +1 -1
  9. package/dist/browser-opt/runner/actions/index.js +135 -3
  10. package/dist/browser-opt/runner/actions/index.js.map +1 -1
  11. package/dist/browser-opt/runner/actions/utils/click-action.d.ts +1 -1
  12. package/dist/browser-opt/runner/actions/utils/click-action.d.ts.map +1 -1
  13. package/dist/browser-opt/runner/actions/utils/click-action.js +10 -4
  14. package/dist/browser-opt/runner/actions/utils/click-action.js.map +1 -1
  15. package/dist/browser-opt/runner/actions/utils/date-action.d.ts +1 -1
  16. package/dist/browser-opt/runner/actions/utils/date-action.d.ts.map +1 -1
  17. package/dist/browser-opt/runner/actions/utils/date-action.js +5 -1
  18. package/dist/browser-opt/runner/actions/utils/date-action.js.map +1 -1
  19. package/dist/browser-opt/runner/actions/utils/dom-action.d.ts +14 -3
  20. package/dist/browser-opt/runner/actions/utils/dom-action.d.ts.map +1 -1
  21. package/dist/browser-opt/runner/actions/utils/dom-action.js +301 -20
  22. package/dist/browser-opt/runner/actions/utils/dom-action.js.map +1 -1
  23. package/dist/browser-opt/runner/actions/utils/fill-action.d.ts +1 -1
  24. package/dist/browser-opt/runner/actions/utils/fill-action.d.ts.map +1 -1
  25. package/dist/browser-opt/runner/actions/utils/fill-action.js +27 -7
  26. package/dist/browser-opt/runner/actions/utils/fill-action.js.map +1 -1
  27. package/dist/browser-opt/runner/actions/utils/select-option-action.d.ts +1 -1
  28. package/dist/browser-opt/runner/actions/utils/select-option-action.d.ts.map +1 -1
  29. package/dist/browser-opt/runner/actions/utils/select-option-action.js +467 -82
  30. package/dist/browser-opt/runner/actions/utils/select-option-action.js.map +1 -1
  31. package/dist/browser-opt/runner/actions/utils/table-row-checkbox-action.d.ts +2 -2
  32. package/dist/browser-opt/runner/actions/utils/table-row-checkbox-action.d.ts.map +1 -1
  33. package/dist/browser-opt/runner/actions/utils/table-row-checkbox-action.js +79 -19
  34. package/dist/browser-opt/runner/actions/utils/table-row-checkbox-action.js.map +1 -1
  35. package/dist/browser-opt/runner/actions/utils/upload-action.d.ts +8 -1
  36. package/dist/browser-opt/runner/actions/utils/upload-action.d.ts.map +1 -1
  37. package/dist/browser-opt/runner/actions/utils/upload-action.js +517 -64
  38. package/dist/browser-opt/runner/actions/utils/upload-action.js.map +1 -1
  39. package/dist/browser-opt/runner/evidence.d.ts.map +1 -1
  40. package/dist/browser-opt/runner/evidence.js +2 -1
  41. package/dist/browser-opt/runner/evidence.js.map +1 -1
  42. package/dist/browser-opt/runner/index.d.ts.map +1 -1
  43. package/dist/browser-opt/runner/index.js +7 -0
  44. package/dist/browser-opt/runner/index.js.map +1 -1
  45. package/dist/browser-opt/runner/step-executor.d.ts.map +1 -1
  46. package/dist/browser-opt/runner/step-executor.js +39 -8
  47. package/dist/browser-opt/runner/step-executor.js.map +1 -1
  48. package/dist/browser-opt/type.d.ts +29 -0
  49. package/dist/browser-opt/type.d.ts.map +1 -1
  50. package/dist/browser-opt/utils.d.ts.map +1 -1
  51. package/dist/browser-opt/utils.js +204 -19
  52. package/dist/browser-opt/utils.js.map +1 -1
  53. package/dist/cli/utils/output.d.ts.map +1 -1
  54. package/dist/cli/utils/output.js +11 -0
  55. package/dist/cli/utils/output.js.map +1 -1
  56. package/package.json +1 -1
  57. package/skills/browser-opt/SKILL.md +10 -2
@@ -1,14 +1,16 @@
1
- export function clickFieldScopedDomTarget(agent, field, target) {
1
+ export function clickFieldScopedDomTarget(agent, field, target, rowNumber) {
2
+ const rowArgument = rowNumber ? `, ${rowNumber}` : '';
2
3
  const script = `(() => {
3
4
  const helper = ${fieldScopedDomHelperSource()};
4
- return JSON.stringify(helper.clickFieldScopedTarget(${JSON.stringify(field)}, ${JSON.stringify(target)}));
5
+ return JSON.stringify(helper.clickFieldScopedTarget(${JSON.stringify(field)}, ${JSON.stringify(target)}${rowArgument}));
5
6
  })()`;
6
7
  try {
7
8
  const parsed = parseEvalJson(agent.evaluate(script));
8
9
  if (parsed.found && parsed.clicked) {
9
10
  agent.waitMs(300);
10
11
  const clickedText = parsed.targetText ? ` (${parsed.targetText.trim()})` : '';
11
- return `click dom ${field} -> ${target}${clickedText}`;
12
+ const rowLabel = rowNumber ? `第 ${rowNumber} ` : '';
13
+ return `click dom ${rowLabel}${field} -> ${target}${clickedText}`;
12
14
  }
13
15
  return null;
14
16
  }
@@ -16,16 +18,36 @@ export function clickFieldScopedDomTarget(agent, field, target) {
16
18
  return null;
17
19
  }
18
20
  }
19
- export function fillFieldScopedDomTarget(agent, field, value) {
21
+ /** 未提供列名时在指定数据行内查找唯一目标,适用于“点击第 N 行删除按钮”这类表达。 */
22
+ export function clickTableRowDomTarget(agent, target, rowNumber) {
20
23
  const script = `(() => {
21
24
  const helper = ${fieldScopedDomHelperSource()};
22
- return JSON.stringify(helper.fillFieldScopedTarget(${JSON.stringify(field)}, ${JSON.stringify(value)}));
25
+ return JSON.stringify(helper.clickTableRowTarget(${JSON.stringify(target)}, ${rowNumber}));
26
+ })()`;
27
+ try {
28
+ const parsed = parseEvalJson(agent.evaluate(script));
29
+ if (parsed.found && parsed.clicked) {
30
+ agent.waitMs(300);
31
+ return `click dom 第 ${rowNumber} 行 -> ${target}`;
32
+ }
33
+ return null;
34
+ }
35
+ catch {
36
+ return null;
37
+ }
38
+ }
39
+ export function fillFieldScopedDomTarget(agent, field, value, preserveFocus = false, rowNumber) {
40
+ const rowArgument = rowNumber ? `, ${rowNumber}` : '';
41
+ const script = `(() => {
42
+ const helper = ${fieldScopedDomHelperSource()};
43
+ return JSON.stringify(helper.fillFieldScopedTarget(${JSON.stringify(field)}, ${JSON.stringify(value)}, ${preserveFocus}${rowArgument}));
23
44
  })()`;
24
45
  try {
25
46
  const parsed = parseEvalJson(agent.evaluate(script));
26
47
  if (parsed.found && parsed.filled) {
27
48
  agent.waitMs(300);
28
- return `fill dom ${field} ${JSON.stringify(value)}`;
49
+ const rowLabel = rowNumber ? `第 ${rowNumber} 行 ` : '';
50
+ return `fill dom ${rowLabel}${field} ${JSON.stringify(value)}`;
29
51
  }
30
52
  return null;
31
53
  }
@@ -34,10 +56,11 @@ export function fillFieldScopedDomTarget(agent, field, value) {
34
56
  }
35
57
  }
36
58
  /** 读取字段对应输入控件的真实 DOM 值,供 snapshot 未暴露值时做后置校验。 */
37
- export function readFieldScopedDomValue(agent, field) {
59
+ export function readFieldScopedDomValue(agent, field, rowNumber) {
60
+ const rowArgument = rowNumber ? `, ${rowNumber}` : '';
38
61
  const script = `(() => {
39
62
  const helper = ${fieldScopedDomHelperSource()};
40
- return JSON.stringify(helper.readFieldScopedValue(${JSON.stringify(field)}));
63
+ return JSON.stringify(helper.readFieldScopedValue(${JSON.stringify(field)}${rowArgument}));
41
64
  })()`;
42
65
  try {
43
66
  const parsed = parseEvalJson(agent.evaluate(script));
@@ -47,6 +70,43 @@ export function readFieldScopedDomValue(agent, field) {
47
70
  return null;
48
71
  }
49
72
  }
73
+ /** 在指定表格行列内处理开关、单复选框或打开下拉框,避免同列多行控件串位。 */
74
+ export function selectFieldScopedDomTarget(agent, field, option, rowNumber, mode = 'select') {
75
+ const script = `(() => {
76
+ const helper = ${fieldScopedDomHelperSource()};
77
+ return JSON.stringify(helper.selectFieldScopedTarget(${JSON.stringify(field)}, ${JSON.stringify(option)}, ${rowNumber}, ${JSON.stringify(mode)}, false));
78
+ })()`;
79
+ try {
80
+ const parsed = parseEvalJson(agent.evaluate(script));
81
+ if (parsed.opened) {
82
+ agent.waitMs(300);
83
+ return { dropdownOpened: true, output: null };
84
+ }
85
+ if (parsed.reached) {
86
+ agent.waitMs(parsed.changed ? 300 : 0);
87
+ const verb = parsed.changed ? 'select dom' : 'selection skipped';
88
+ return { dropdownOpened: false, output: `${verb} 第 ${rowNumber} 行 ${field}=${option}` };
89
+ }
90
+ return null;
91
+ }
92
+ catch {
93
+ return null;
94
+ }
95
+ }
96
+ /** 从指定表格单元格读取选择类控件状态,确保后置校验不引用其他数据行。 */
97
+ export function verifyFieldScopedDomSelection(agent, field, option, rowNumber, mode = 'select') {
98
+ const script = `(() => {
99
+ const helper = ${fieldScopedDomHelperSource()};
100
+ return JSON.stringify(helper.selectFieldScopedTarget(${JSON.stringify(field)}, ${JSON.stringify(option)}, ${rowNumber}, ${JSON.stringify(mode)}, true));
101
+ })()`;
102
+ try {
103
+ const parsed = parseEvalJson(agent.evaluate(script));
104
+ return typeof parsed.reached === 'boolean' ? parsed.reached : null;
105
+ }
106
+ catch {
107
+ return null;
108
+ }
109
+ }
50
110
  function parseEvalJson(raw) {
51
111
  const decoded = JSON.parse(raw.trim());
52
112
  return typeof decoded === 'string'
@@ -92,6 +152,32 @@ const inputLike = (element) => {
92
152
  if (element instanceof HTMLInputElement) return !['hidden', 'file', 'checkbox', 'radio', 'button', 'submit', 'reset'].includes(element.type);
93
153
  return element.isContentEditable === true;
94
154
  };
155
+ const switchLike = (element) => element.matches('[role="switch"], .ant-switch, [class*="switch"]') && !disabled(element);
156
+ const selectLike = (element) => element.matches('select:not(:disabled), .ant-select:not(.ant-select-disabled), .el-select:not(.is-disabled), [role="combobox"]');
157
+ const choiceInput = (element, optionText) => {
158
+ const input = element.matches('input[type="radio"], input[type="checkbox"]')
159
+ ? element
160
+ : element.querySelector?.('input[type="radio"], input[type="checkbox"]');
161
+ if (!input || disabled(input)) return null;
162
+ const label = element.closest('label') || input.closest('label') || (input.id ? document.querySelector('label[for="' + CSS.escape(input.id) + '"]') : null);
163
+ const text = normalize(textOf(label || element));
164
+ return text && (text.includes(optionText) || optionText.includes(text)) ? { input, label } : null;
165
+ };
166
+ const switchState = (element) => {
167
+ const aria = element.getAttribute('aria-checked');
168
+ if (aria === 'true' || aria === '1') return true;
169
+ if (aria === 'false' || aria === '0') return false;
170
+ if (typeof element.checked === 'boolean') return element.checked;
171
+ const className = String(element.className || '');
172
+ if (element.classList.contains('ant-switch-checked') || element.classList.contains('is-checked') || className.includes('switch-checked')) return true;
173
+ return false;
174
+ };
175
+ const desiredSwitchState = (option) => {
176
+ const text = normalize(option);
177
+ if (/^(是|开|开启|打开|启用|展示|true|yes|on)$/.test(text)) return true;
178
+ if (/^(否|关|关闭|停用|禁用|不展示|false|no|off)$/.test(text)) return false;
179
+ return null;
180
+ };
95
181
  const dispatchMouse = (element) => {
96
182
  element.scrollIntoView({ block: 'center', inline: 'nearest' });
97
183
  const rect = element.getBoundingClientRect();
@@ -107,8 +193,15 @@ const dispatchMouse = (element) => {
107
193
  element.dispatchEvent(new MouseEvent('mouseup', init));
108
194
  element.dispatchEvent(new MouseEvent('click', init));
109
195
  };
110
- const dispatchValue = (element, value) => {
196
+ const dispatchValue = (element, value, keepFocus = false) => {
111
197
  element.scrollIntoView({ block: 'center', inline: 'nearest' });
198
+ // Ant 虚拟表格的表头与表体使用独立滚动容器,scrollIntoView 后显式同步表头避免视觉与列映射错位。
199
+ const virtualTable = element.closest('.ant-table-virtual');
200
+ const virtualBody = element.closest('.ant-table-tbody-virtual-holder');
201
+ const virtualHeader = virtualTable?.querySelector('.ant-table-header');
202
+ if (virtualBody && virtualHeader) {
203
+ virtualHeader.scrollLeft = virtualBody.scrollLeft;
204
+ }
112
205
  element.focus?.();
113
206
  const preserveFocus = Boolean(element.closest('[role="combobox"], .ant-select, .el-select'));
114
207
  if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement) {
@@ -120,7 +213,7 @@ const dispatchValue = (element, value) => {
120
213
  }
121
214
  element.dispatchEvent(new Event('input', { bubbles: true }));
122
215
  element.dispatchEvent(new Event('change', { bubbles: true }));
123
- if (!preserveFocus) {
216
+ if (!preserveFocus && !keepFocus) {
124
217
  element.blur?.();
125
218
  }
126
219
  };
@@ -134,6 +227,107 @@ const fieldElements = (field) => {
134
227
  };
135
228
  const closestFieldContainer = (element) => element.closest('.ant-form-item, .el-form-item, .form-item, [class*="form-item"], [class*="FormItem"], [class*="field"], [class*="Field"]');
136
229
  const actionableDescendants = (root, predicate) => [...root.querySelectorAll('*')].filter((element) => visible(element) && predicate(element));
230
+ const tableComponentRoot = (header) => header.closest('.ant-table, .el-table, [role="table"], [role="grid"]')
231
+ || header.closest('table')?.parentElement;
232
+ const tableRows = (root) => root
233
+ ? [...root.querySelectorAll('tbody tr, [role="row"], .ant-table-tbody-virtual-holder-inner .ant-table-row')]
234
+ .filter((row) => !row.closest('thead') && !row.querySelector(':scope > [role="columnheader"]'))
235
+ : [];
236
+ const tableRowCells = (row) => [...row.querySelectorAll(':scope > td, :scope > th, :scope > [role="cell"], :scope > [role="gridcell"], :scope > .ant-table-cell')];
237
+ const tableRowGroups = (rows) => {
238
+ const groups = [];
239
+ for (const row of [...rows].sort((left, right) => left.getBoundingClientRect().top - right.getBoundingClientRect().top)) {
240
+ const top = row.getBoundingClientRect().top;
241
+ const group = groups.find((item) => Math.abs(item.top - top) <= 2);
242
+ if (group) {
243
+ group.rows.push(row);
244
+ } else {
245
+ groups.push({ top, rows: [row] });
246
+ }
247
+ }
248
+ return groups.map((group) => group.rows);
249
+ };
250
+ const horizontalOverlap = (left, right) => Math.max(0, Math.min(left.right, right.right) - Math.max(left.left, right.left));
251
+ const tableCellIdentities = (cell) => {
252
+ const identities = [];
253
+ for (const name of ['data-column-key', 'data-field', 'data-prop', 'data-index', 'aria-colindex']) {
254
+ const value = cell.getAttribute(name);
255
+ if (value) identities.push(name + '=' + value);
256
+ }
257
+ for (const token of cell.classList || []) {
258
+ if (/column|(^|[-_])col([-_]|\\d)|cell[-_]\\d/i.test(token) && !/^(ant-table-cell|el-table__cell)$/.test(token)) {
259
+ identities.push('class=' + token);
260
+ }
261
+ }
262
+ const table = cell.closest('table');
263
+ const index = cell.cellIndex;
264
+ const col = table && index >= 0 ? table.querySelectorAll('colgroup col')[index] : null;
265
+ if (col) {
266
+ for (const name of ['name', 'class', 'data-column-key', 'data-index']) {
267
+ const value = col.getAttribute(name);
268
+ if (value) identities.push('col-' + name + '=' + value);
269
+ }
270
+ }
271
+ return [...new Set(identities)];
272
+ };
273
+ const targetInside = (cell, predicate) => {
274
+ if (!cell) return null;
275
+ if (visible(cell) && predicate(cell)) return cell;
276
+ return actionableDescendants(cell, predicate)[0] || null;
277
+ };
278
+ const tableColumnTarget = (fieldElement, predicate, rowNumber) => {
279
+ const header = fieldElement.closest('th, td')
280
+ || (fieldElement.getAttribute('role') === 'columnheader' ? fieldElement : null);
281
+ if (!header) return null;
282
+ const component = tableComponentRoot(header);
283
+ const headerRect = header.getBoundingClientRect();
284
+ const rows = tableRows(component);
285
+ const targetRows = rowNumber ? tableRowGroups(rows)[rowNumber - 1] || [] : rows;
286
+ const headerCells = [...(header.parentElement?.children || [])];
287
+ const columnIndex = header.cellIndex >= 0 ? header.cellIndex : headerCells.indexOf(header);
288
+
289
+ // Ant 虚拟表格保留完整列顺序但表头、表体滚动位置可能短暂不同,必须优先按稳定列序号定位。
290
+ if (component?.matches('.ant-table-virtual') && columnIndex >= 0) {
291
+ for (const row of targetRows) {
292
+ const target = targetInside(tableRowCells(row)[columnIndex], predicate);
293
+ if (target) return target;
294
+ }
295
+ }
296
+ const headerIdentities = tableCellIdentities(header);
297
+ const identityTargets = headerIdentities.length === 0 ? [] : targetRows
298
+ .flatMap((row) => tableRowCells(row)
299
+ .filter((cell) => tableCellIdentities(cell).some((identity) => headerIdentities.includes(identity)))
300
+ .map((cell) => ({ target: targetInside(cell, predicate), rowTop: row.getBoundingClientRect().top })))
301
+ .filter((item) => item.target)
302
+ .sort((a, b) => a.rowTop - b.rowTop);
303
+ if (identityTargets.length > 0) return identityTargets[0].target;
304
+
305
+ // 滚动表格常把表头和表体拆成不同 table,并额外渲染固定列副本;按屏幕横坐标映射才能避免列序号漂移。
306
+ const sameTextHeaders = component ? [...component.querySelectorAll('thead th, thead td, [role="columnheader"]')]
307
+ .filter((item) => normalize(textOf(item)) === normalize(textOf(header))) : [];
308
+ const geometricTargets = sameTextHeaders.length > 1 ? [] : targetRows
309
+ .flatMap((row) => tableRowCells(row)
310
+ .map((cell) => ({ cell, rowTop: row.getBoundingClientRect().top, overlap: horizontalOverlap(headerRect, cell.getBoundingClientRect()) })))
311
+ .filter((item) => item.overlap > 0)
312
+ .map((item) => ({ ...item, target: targetInside(item.cell, predicate) }))
313
+ .filter((item) => item.target)
314
+ .sort((a, b) => b.overlap - a.overlap || a.rowTop - b.rowTop);
315
+ if (geometricTargets.length > 0) return geometricTargets[0].target;
316
+
317
+ if (columnIndex < 0) return null;
318
+ const ownTable = header.closest('table');
319
+ const ownRows = ownTable?.querySelector('tbody tr') ? [...ownTable.querySelectorAll('tbody tr')] : [];
320
+ const indexedRows = rowNumber
321
+ ? (ownRows.length > 0 ? [ownRows[rowNumber - 1]].filter(Boolean) : targetRows)
322
+ : (ownRows.length > 0 ? ownRows : rows);
323
+ for (const row of indexedRows) {
324
+ const cell = tableRowCells(row)[columnIndex];
325
+ if (!cell) continue;
326
+ const target = targetInside(cell, predicate);
327
+ if (target) return target;
328
+ }
329
+ return null;
330
+ };
137
331
  const collectSiblingScope = (element) => {
138
332
  const scope = [];
139
333
  let current = element;
@@ -169,9 +363,19 @@ const structurallyScopedTargets = (fieldElement, predicate) => {
169
363
  }
170
364
  return null;
171
365
  };
172
- const scopedTarget = (field, predicate) => {
366
+ const scopedTarget = (field, predicate, rowNumber = null) => {
173
367
  const fields = fieldElements(field);
174
- for (const fieldItem of fields) {
368
+ const tableFields = fields.filter((fieldItem) => fieldItem.element.closest('th, [role="columnheader"]'));
369
+ for (const fieldItem of tableFields.length > 0 ? tableFields : fields) {
370
+ const tableTarget = tableColumnTarget(fieldItem.element, predicate, rowNumber);
371
+ if (tableTarget) {
372
+ return { element: tableTarget, fields };
373
+ }
374
+ // 列头定位失败时不能退化为普通近邻搜索,否则会把值写入相邻列并造成同源校验假阳性。
375
+ if (fieldItem.element.closest('th, [role="columnheader"]')) {
376
+ continue;
377
+ }
378
+
175
379
  const container = closestFieldContainer(fieldItem.element);
176
380
  const scoped = container ? [...container.querySelectorAll('*')].filter((element) => visible(element) && predicate(element)) : [];
177
381
  if (scoped.length > 0) {
@@ -185,25 +389,41 @@ const scopedTarget = (field, predicate) => {
185
389
  }
186
390
  return { element: null, fields };
187
391
  };
188
- function clickFieldScopedTarget(field, target) {
392
+ function clickFieldScopedTarget(field, target, rowNumber = null) {
189
393
  const targetText = normalize(target);
190
- const result = scopedTarget(field, (element) => clickable(element) && textMatches(element, targetText));
394
+ const result = scopedTarget(field, (element) => clickable(element) && textMatches(element, targetText), rowNumber);
191
395
  if (result.element) {
192
396
  dispatchMouse(result.element);
193
397
  return { found: true, clicked: true, targetText: textOf(result.element) };
194
398
  }
195
399
  return { found: result.fields.length > 0, clicked: false };
196
400
  }
197
- function fillFieldScopedTarget(field, value) {
198
- const result = scopedTarget(field, inputLike);
401
+ function clickTableRowTarget(target, rowNumber) {
402
+ const targetText = normalize(target);
403
+ const rootSelector = '.ant-table, .el-table, [role="table"], [role="grid"], table';
404
+ const roots = [...document.querySelectorAll(rootSelector)]
405
+ .filter((root) => !root.parentElement?.closest(rootSelector));
406
+ const targets = roots.flatMap((root) => {
407
+ const rows = tableRowGroups(tableRows(root))[rowNumber - 1] || [];
408
+ return rows.flatMap((row) => [row, ...row.querySelectorAll('*')])
409
+ .filter((element) => visible(element) && clickable(element) && textMatches(element, targetText));
410
+ });
411
+ const targetElement = targets
412
+ .sort((left, right) => normalize(textOf(left)).length - normalize(textOf(right)).length)[0];
413
+ if (!targetElement) return { found: false, clicked: false };
414
+ dispatchMouse(targetElement);
415
+ return { found: true, clicked: true, targetText: textOf(targetElement) };
416
+ }
417
+ function fillFieldScopedTarget(field, value, preserveFocus, rowNumber = null) {
418
+ const result = scopedTarget(field, inputLike, rowNumber);
199
419
  if (result.element) {
200
- dispatchValue(result.element, value);
420
+ dispatchValue(result.element, value, preserveFocus);
201
421
  return { found: true, filled: true, targetText: textOf(result.element) };
202
422
  }
203
423
  return { found: result.fields.length > 0, filled: false };
204
424
  }
205
- function readFieldScopedValue(field) {
206
- const result = scopedTarget(field, inputLike);
425
+ function readFieldScopedValue(field, rowNumber = null) {
426
+ const result = scopedTarget(field, inputLike, rowNumber);
207
427
  if (!result.element) {
208
428
  return { found: false };
209
429
  }
@@ -212,7 +432,68 @@ function readFieldScopedValue(field) {
212
432
  : result.element.textContent || '';
213
433
  return { found: true, value };
214
434
  }
215
- return { clickFieldScopedTarget, fillFieldScopedTarget, readFieldScopedValue };
435
+ function selectFieldScopedTarget(field, option, rowNumber, mode = 'select', verifyOnly = false) {
436
+ const optionText = normalize(option);
437
+ const result = scopedTarget(field, (element) => switchLike(element) || selectLike(element) || Boolean(choiceInput(element, optionText)), rowNumber);
438
+ if (!result.element) return { found: false };
439
+
440
+ const switchElement = result.element.closest('[role="switch"], .ant-switch, [class*="switch"]');
441
+ if (switchElement && switchLike(switchElement)) {
442
+ const desired = desiredSwitchState(option);
443
+ if (desired === null) return { found: true, kind: 'switch' };
444
+ const before = switchState(switchElement);
445
+ if (!verifyOnly && before !== desired) dispatchMouse(switchElement);
446
+ const reached = verifyOnly ? before === desired : switchState(switchElement) === desired;
447
+ return { found: true, kind: 'switch', reached, changed: !verifyOnly && before !== desired };
448
+ }
449
+
450
+ const choice = choiceInput(result.element, optionText);
451
+ if (choice) {
452
+ const desired = mode !== 'deselect';
453
+ const cell = choice.input.closest('td, th, [role="cell"], [role="gridcell"], .ant-table-cell');
454
+ const group = cell ? [...cell.querySelectorAll('input[type="checkbox"]')].filter((input) => !disabled(input)) : [choice.input];
455
+ let changed = false;
456
+ if (!verifyOnly && mode === 'exclusive') {
457
+ for (const sibling of group) {
458
+ if (sibling !== choice.input && sibling.checked) {
459
+ (sibling.closest('label') || sibling).click();
460
+ changed = true;
461
+ }
462
+ }
463
+ }
464
+ if (!verifyOnly && choice.input.checked !== desired) {
465
+ (choice.label || choice.input).click();
466
+ changed = true;
467
+ }
468
+ const reached = choice.input.checked === desired
469
+ && (mode !== 'exclusive' || group.filter((input) => input !== choice.input && input.checked).length === 0);
470
+ return { found: true, kind: choice.input.type, reached, changed };
471
+ }
472
+
473
+ const selectElement = result.element.closest('select, .ant-select, .el-select, [role="combobox"]');
474
+ if (!selectElement) return { found: true };
475
+ const values = [
476
+ selectElement instanceof HTMLSelectElement ? selectElement.selectedOptions?.[0]?.textContent || selectElement.value : '',
477
+ selectElement.querySelector('input:not([type="hidden"])')?.value,
478
+ selectElement.querySelector('.ant-select-selection-item, .el-input__inner, [class*="singleValue"]')?.textContent,
479
+ ].map(normalize).filter(Boolean);
480
+ if (verifyOnly) {
481
+ return { found: true, kind: 'dropdown', reached: values.some((value) => value === optionText) };
482
+ }
483
+ if (selectElement instanceof HTMLSelectElement) {
484
+ const nativeOption = [...selectElement.options].find((item) => normalize(item.textContent || item.value) === optionText);
485
+ if (!nativeOption) return { found: true, kind: 'dropdown', reached: false };
486
+ selectElement.value = nativeOption.value;
487
+ selectElement.dispatchEvent(new Event('input', { bubbles: true }));
488
+ selectElement.dispatchEvent(new Event('change', { bubbles: true }));
489
+ return { found: true, kind: 'dropdown', reached: true, changed: true };
490
+ }
491
+ document.querySelectorAll('[data-browser-opt-active-select="true"]').forEach((element) => element.removeAttribute('data-browser-opt-active-select'));
492
+ selectElement.setAttribute('data-browser-opt-active-select', 'true');
493
+ dispatchMouse(selectElement.querySelector('.ant-select-selector, .el-input, [role="combobox"]') || selectElement);
494
+ return { found: true, kind: 'dropdown', opened: true };
495
+ }
496
+ return { clickFieldScopedTarget, clickTableRowTarget, fillFieldScopedTarget, readFieldScopedValue, selectFieldScopedTarget };
216
497
  })()
217
498
  `;
218
499
  }
@@ -1 +1 @@
1
- {"version":3,"file":"dom-action.js","sourceRoot":"","sources":["../../../../../src/browser-opt/runner/actions/utils/dom-action.ts"],"names":[],"mappings":"AAcA,MAAM,UAAU,yBAAyB,CAAC,KAAmB,EAAE,KAAa,EAAE,MAAc;IAC1F,MAAM,MAAM,GAAG;mBACE,0BAA0B,EAAE;wDACS,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,KAAK,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC;KACnG,CAAC;IAEJ,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,aAAa,CAAC,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC;QACrD,IAAI,MAAM,CAAC,KAAK,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;YACnC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YAClB,MAAM,WAAW,GAAG,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,KAAK,MAAM,CAAC,UAAU,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;YAC9E,OAAO,aAAa,KAAK,OAAO,MAAM,GAAG,WAAW,EAAE,CAAC;QACzD,CAAC;QACD,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,MAAM,UAAU,wBAAwB,CAAC,KAAmB,EAAE,KAAa,EAAE,KAAa;IACxF,MAAM,MAAM,GAAG;mBACE,0BAA0B,EAAE;uDACQ,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,KAAK,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC;KACjG,CAAC;IAEJ,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,aAAa,CAAC,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC;QACrD,IAAI,MAAM,CAAC,KAAK,IAAI,MAAM,CAAC,MAAM,EAAE,CAAC;YAClC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YAClB,OAAO,YAAY,KAAK,IAAI,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE,CAAC;QACtD,CAAC;QACD,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,iDAAiD;AACjD,MAAM,UAAU,uBAAuB,CAAC,KAAmB,EAAE,KAAa;IACxE,MAAM,MAAM,GAAG;mBACE,0BAA0B,EAAE;sDACO,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC;KACtE,CAAC;IAEJ,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,aAAa,CAAC,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC;QACrD,OAAO,MAAM,CAAC,KAAK,IAAI,OAAO,MAAM,CAAC,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC;IAChF,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,SAAS,aAAa,CAAC,GAAW;IAChC,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,EAAE,CAAY,CAAC;IAClD,OAAO,OAAO,OAAO,KAAK,QAAQ;QAChC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAyB;QAC7C,CAAC,CAAC,OAA+B,CAAC;AACtC,CAAC;AAED,0CAA0C;AAC1C,SAAS,0BAA0B;IACjC,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA+JR,CAAC;AACF,CAAC"}
1
+ {"version":3,"file":"dom-action.js","sourceRoot":"","sources":["../../../../../src/browser-opt/runner/actions/utils/dom-action.ts"],"names":[],"mappings":"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"}
@@ -3,7 +3,7 @@
3
3
  */
4
4
  import type { BrowserAgent } from '#browser-core/agent';
5
5
  import type { DeterministicAction, SnapshotEvidence } from '../../../type.js';
6
- /** 执行输入动作,登录页阻塞时转为 handoff,避免误报找不到业务字段。 */
6
+ /** 输入前按需滚动目标,登录页阻塞时转为 handoff,避免误报找不到业务字段。 */
7
7
  export declare function executeFillAction(agent: BrowserAgent, action: Extract<DeterministicAction, {
8
8
  type: 'fill';
9
9
  }>, snapshot: SnapshotEvidence): string;
@@ -1 +1 @@
1
- {"version":3,"file":"fill-action.d.ts","sourceRoot":"","sources":["../../../../../src/browser-opt/runner/actions/utils/fill-action.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACxD,OAAO,KAAK,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAM9E,2CAA2C;AAC3C,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,YAAY,EACnB,MAAM,EAAE,OAAO,CAAC,mBAAmB,EAAE;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,EACtD,QAAQ,EAAE,gBAAgB,GACzB,MAAM,CA4BR;AAED,8CAA8C;AAC9C,wBAAgB,sBAAsB,CACpC,KAAK,EAAE,YAAY,EACnB,MAAM,EAAE,OAAO,CAAC,mBAAmB,EAAE;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,EACtD,aAAa,EAAE,gBAAgB,GAC9B;IAAE,MAAM,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAuBtC"}
1
+ {"version":3,"file":"fill-action.d.ts","sourceRoot":"","sources":["../../../../../src/browser-opt/runner/actions/utils/fill-action.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACxD,OAAO,KAAK,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAM9E,8CAA8C;AAC9C,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,YAAY,EACnB,MAAM,EAAE,OAAO,CAAC,mBAAmB,EAAE;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,EACtD,QAAQ,EAAE,gBAAgB,GACzB,MAAM,CAuCR;AAED,8CAA8C;AAC9C,wBAAgB,sBAAsB,CACpC,KAAK,EAAE,YAAY,EACnB,MAAM,EAAE,OAAO,CAAC,mBAAmB,EAAE;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,EACtD,aAAa,EAAE,gBAAgB,GAC9B;IAAE,MAAM,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAyCtC"}
@@ -2,32 +2,52 @@ import { findTextboxRef, readTextboxValue } from '../../../utils.js';
2
2
  import { captureTransientSnapshot } from '../../evidence.js';
3
3
  import { buildLoginHandoffActionOutput, isLoginLikeSnapshot } from '../../handoff.js';
4
4
  import { fillFieldScopedDomTarget, readFieldScopedDomValue } from './dom-action.js';
5
- /** 执行输入动作,登录页阻塞时转为 handoff,避免误报找不到业务字段。 */
5
+ /** 输入前按需滚动目标,登录页阻塞时转为 handoff,避免误报找不到业务字段。 */
6
6
  export function executeFillAction(agent, action, snapshot) {
7
- let ref = findTextboxRef(snapshot, action.field);
7
+ let ref = action.rowNumber ? null : findTextboxRef(snapshot, action.field);
8
8
  if (!ref) {
9
9
  if (isLoginLikeSnapshot(snapshot)) {
10
10
  return buildLoginHandoffActionOutput(agent, `当前页面仍在登录页,无法继续填写“${action.field}”,请先完成登录后再继续自动化。`);
11
11
  }
12
12
  agent.waitMs(500);
13
13
  const settledSnapshot = captureTransientSnapshot(agent);
14
- ref = findTextboxRef(settledSnapshot, action.field);
14
+ ref = action.rowNumber ? null : findTextboxRef(settledSnapshot, action.field);
15
15
  if (ref) {
16
+ agent.scrollIntoView(ref);
16
17
  const output = agent.fill(ref, action.value);
17
- return `fill delayed @${ref} ${JSON.stringify(action.value)}\n${output}`.trim();
18
+ const pressOutput = action.pressKey ? agent.press(action.pressKey) : '';
19
+ return `fill delayed @${ref} ${JSON.stringify(action.value)}\n${output}\n${pressOutput}`.trim();
18
20
  }
19
- const domOutput = fillFieldScopedDomTarget(agent, action.field, action.value);
21
+ const domOutput = fillFieldScopedDomTarget(agent, action.field, action.value, Boolean(action.pressKey), action.rowNumber);
20
22
  if (domOutput) {
21
- return domOutput;
23
+ const pressOutput = action.pressKey ? agent.press(action.pressKey) : '';
24
+ return `${domOutput}\n${pressOutput}`.trim();
22
25
  }
23
26
  throw new Error(`无法找到输入框:${action.field}`);
24
27
  }
28
+ agent.scrollIntoView(ref);
25
29
  const output = agent.fill(ref, action.value);
26
- return `fill @${ref} ${JSON.stringify(action.value)}\n${output}`.trim();
30
+ const pressOutput = action.pressKey ? agent.press(action.pressKey) : '';
31
+ return `fill @${ref} ${JSON.stringify(action.value)}\n${output}\n${pressOutput}`.trim();
27
32
  }
28
33
  /** 确认目标输入框已写入预期值,避免命令成功返回但受控表单没有接收输入时误报通过。 */
29
34
  export function verifyFillActionEffect(agent, action, afterSnapshot) {
35
+ if (action.rowNumber) {
36
+ const domValue = readFieldScopedDomValue(agent, action.field, action.rowNumber);
37
+ const target = `第 ${action.rowNumber} 行${action.field}`;
38
+ if (domValue === action.value) {
39
+ return { passed: true, message: `已通过 DOM 确认输入值:${target}=${action.value}` };
40
+ }
41
+ const domDescription = domValue === null ? 'DOM 未定位到该行输入框' : `DOM 实际值为${JSON.stringify(domValue)}`;
42
+ return {
43
+ passed: false,
44
+ message: `动作后未确认输入值:${target}=${action.value}(${domDescription})`,
45
+ };
46
+ }
30
47
  const actualValue = readTextboxValue(afterSnapshot, action.field);
48
+ if (action.pressKey === 'Enter' && afterSnapshot.text.includes(action.value)) {
49
+ return { passed: true, message: `已确认输入并按 Enter 提交:${action.field}=${action.value}` };
50
+ }
31
51
  if (actualValue === action.value) {
32
52
  return { passed: true, message: `已确认输入值:${action.field}=${action.value}` };
33
53
  }
@@ -1 +1 @@
1
- {"version":3,"file":"fill-action.js","sourceRoot":"","sources":["../../../../../src/browser-opt/runner/actions/utils/fill-action.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"fill-action.js","sourceRoot":"","sources":["../../../../../src/browser-opt/runner/actions/utils/fill-action.ts"],"names":[],"mappings":"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"}
@@ -3,7 +3,7 @@
3
3
  */
4
4
  import type { BrowserAgent } from '#browser-core/agent';
5
5
  import type { DeterministicAction, SnapshotEvidence, DeterministicExecutionOptions } from '../../../type.js';
6
- /** 执行选项选择动作,包含 switch、原生单选复选框和长表单滚动搜索兜底。 */
6
+ /** 选择前按需滚动目标,包含 switch、原生单选复选框和长表单滚动搜索兜底。 */
7
7
  export declare function executeSelectOptionAction(agent: BrowserAgent, action: Extract<DeterministicAction, {
8
8
  type: 'select-option';
9
9
  }>, snapshot: SnapshotEvidence, options: DeterministicExecutionOptions): string;
@@ -1 +1 @@
1
- {"version":3,"file":"select-option-action.d.ts","sourceRoot":"","sources":["../../../../../src/browser-opt/runner/actions/utils/select-option-action.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACxD,OAAO,KAAK,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,6BAA6B,EAAE,MAAM,kBAAkB,CAAC;AAY7G,4CAA4C;AAC5C,wBAAgB,yBAAyB,CACvC,KAAK,EAAE,YAAY,EACnB,MAAM,EAAE,OAAO,CAAC,mBAAmB,EAAE;IAAE,IAAI,EAAE,eAAe,CAAA;CAAE,CAAC,EAC/D,QAAQ,EAAE,gBAAgB,EAC1B,OAAO,EAAE,6BAA6B,GACrC,MAAM,CA+ER;AAED,2CAA2C;AAC3C,wBAAgB,8BAA8B,CAC5C,KAAK,EAAE,YAAY,EACnB,MAAM,EAAE,OAAO,CAAC,mBAAmB,EAAE;IAAE,IAAI,EAAE,eAAe,CAAA;CAAE,CAAC,EAC/D,aAAa,EAAE,gBAAgB,GAC9B;IAAE,MAAM,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CA+BtC"}
1
+ {"version":3,"file":"select-option-action.d.ts","sourceRoot":"","sources":["../../../../../src/browser-opt/runner/actions/utils/select-option-action.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACxD,OAAO,KAAK,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,6BAA6B,EAAE,MAAM,kBAAkB,CAAC;AAa7G,6CAA6C;AAC7C,wBAAgB,yBAAyB,CACvC,KAAK,EAAE,YAAY,EACnB,MAAM,EAAE,OAAO,CAAC,mBAAmB,EAAE;IAAE,IAAI,EAAE,eAAe,CAAA;CAAE,CAAC,EAC/D,QAAQ,EAAE,gBAAgB,EAC1B,OAAO,EAAE,6BAA6B,GACrC,MAAM,CAyHR;AAED,2CAA2C;AAC3C,wBAAgB,8BAA8B,CAC5C,KAAK,EAAE,YAAY,EACnB,MAAM,EAAE,OAAO,CAAC,mBAAmB,EAAE;IAAE,IAAI,EAAE,eAAe,CAAA;CAAE,CAAC,EAC/D,aAAa,EAAE,gBAAgB,GAC9B;IAAE,MAAM,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CA4DtC"}