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
@@ -5,30 +5,54 @@ import * as fs from 'node:fs';
5
5
  import * as path from 'node:path';
6
6
  import { findUploadRef } from '../../../utils.js';
7
7
  import { captureTransientSnapshot } from '../../evidence.js';
8
- /** 执行上传动作,优先使用快照 ref,再用隐藏 input 和长表单滚动搜索兜底。 */
8
+ /** 执行上传动作,先展示可见上传区域,再使用快照 ref、隐藏 input 或长表单搜索结果上传。 */
9
9
  export async function executeUploadAction(agent, action, snapshot, outputDir, options) {
10
- let ref = findUploadRef(snapshot, action.field);
10
+ const sources = action.sources ?? [action.source];
11
+ if (sources.length > 1) {
12
+ return executeBatchUploadAction(agent, action.field, sources, outputDir, action.rowNumber);
13
+ }
11
14
  const searchOutput = [];
15
+ let ref = action.rowNumber ? null : findUploadRef(snapshot, action.field);
16
+ let scrollTarget = ref;
12
17
  if (!ref) {
13
- const domTarget = findHiddenUploadInputSelector(agent, action.field);
18
+ const domTarget = findHiddenUploadInputSelector(agent, action.field, action.rowNumber);
14
19
  if (domTarget) {
15
20
  ref = domTarget.selector;
21
+ scrollTarget = domTarget.scrollSelector ?? null;
16
22
  searchOutput.push(`upload dom selector ${domTarget.selector}`);
17
23
  }
18
24
  }
19
- if (!ref && options.allowViewportSearch) {
25
+ if (!ref && revealTableUploadInput(agent, action.field, action.rowNumber)) {
26
+ const rowLabel = action.rowNumber ? `第 ${action.rowNumber} 行 ` : '';
27
+ searchOutput.push(`upload reveal table input ${rowLabel}${action.field}`);
28
+ for (let attempt = 1; attempt <= 10 && !ref; attempt += 1) {
29
+ agent.waitMs(100);
30
+ const domTarget = findHiddenUploadInputSelector(agent, action.field, action.rowNumber);
31
+ if (domTarget) {
32
+ ref = domTarget.selector;
33
+ scrollTarget = domTarget.scrollSelector ?? null;
34
+ searchOutput.push(`upload dynamic input ${attempt} ${domTarget.selector}`);
35
+ }
36
+ }
37
+ }
38
+ if (!ref && !action.rowNumber && options.allowViewportSearch) {
20
39
  const scrolled = searchUploadInLongForm(agent, action.field);
21
40
  if (scrolled) {
22
41
  ref = scrolled.ref;
42
+ scrollTarget = scrolled.ref;
23
43
  searchOutput.push(...scrolled.logs);
24
44
  }
25
45
  }
26
46
  if (!ref) {
27
- throw new Error(`无法找到上传控件:${action.field}`);
47
+ const rowLabel = action.rowNumber ? `第 ${action.rowNumber}` : '';
48
+ throw new Error(`无法找到上传控件:${rowLabel}${action.field};DOM诊断:${diagnoseUploadTarget(agent, action.field, action.rowNumber)}`);
28
49
  }
29
- const filePath = await prepareUploadFile(action.source, outputDir);
50
+ if (scrollTarget) {
51
+ agent.scrollIntoView(scrollTarget);
52
+ }
53
+ const filePath = await prepareUploadFile(sources[0], outputDir);
30
54
  const output = agent.upload(ref, [filePath]);
31
- const completionOutput = waitForUploadCompletion(agent, action.field);
55
+ const completionOutput = waitForUploadCompletion(agent, action.field, action.rowNumber, ref);
32
56
  return [
33
57
  ...searchOutput,
34
58
  `upload @${ref} ${filePath}`,
@@ -36,43 +60,164 @@ export async function executeUploadAction(agent, action, snapshot, outputDir, op
36
60
  completionOutput,
37
61
  ].filter(Boolean).join('\n').trim();
38
62
  }
39
- /** 上传命令返回后等待字段作用域内的加载态消失,避免后续步骤操作尚未就绪的表单。 */
40
- function waitForUploadCompletion(agent, field) {
63
+ /** 上传动作必须在目标字段内形成文件、预览或组件成功态,不能只依赖 upload 命令返回。 */
64
+ export function verifyUploadActionEffect(agent, action, afterSnapshot) {
65
+ const expectedCount = action.sources?.length ?? 1;
66
+ const state = getUploadState(agent, action.field, action.rowNumber, expectedCount);
67
+ const rowLabel = action.rowNumber ? `第 ${action.rowNumber} 行` : '';
68
+ const target = `${rowLabel}${action.field}`;
69
+ if (state.failed) {
70
+ return { passed: false, message: `目标字段显示上传失败:${target}` };
71
+ }
72
+ if (state.completed && (state.completedCount ?? expectedCount) >= expectedCount) {
73
+ return { passed: true, message: `已确认上传成功:${target}` };
74
+ }
75
+ const normalizedSnapshot = afterSnapshot.text.replace(/\s+/g, '');
76
+ const escapedField = action.field.replace(/\s+/g, '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
77
+ const hasExplicitSuccess = new RegExp(`(?:${escapedField}.{0,80}(?:上传成功|上传完成|预览)|(?:上传成功|上传完成|预览).{0,80}${escapedField})`).test(normalizedSnapshot);
78
+ if (hasExplicitSuccess) {
79
+ return { passed: true, message: `已通过页面状态确认上传成功:${target}` };
80
+ }
81
+ return { passed: false, message: `未在目标字段检测到上传成功状态:${target}` };
82
+ }
83
+ /** 批量上传前滚动字段容器一次,再按 input 的 DOM 顺序逐槽位绑定图片。 */
84
+ async function executeBatchUploadAction(agent, field, sources, outputDir, rowNumber) {
85
+ const { selectors, scrollSelector } = findBatchUploadInputSelectors(agent, field, sources.length, rowNumber);
86
+ if (selectors.length < sources.length) {
87
+ throw new Error(`上传槽位不足:${field}需要 ${sources.length} 个,找到 ${selectors.length} 个`);
88
+ }
89
+ if (scrollSelector) {
90
+ agent.scrollIntoView(scrollSelector);
91
+ }
92
+ const filePaths = await Promise.all(sources.map((source) => prepareUploadFile(source, outputDir)));
93
+ const output = [`upload batch slots ${selectors.join(', ')}`];
94
+ for (const [index, filePath] of filePaths.entries()) {
95
+ const selector = selectors[index];
96
+ output.push(`upload slot ${index + 1} @${selector} ${filePath}`, agent.upload(selector, [filePath]), waitForUploadCompletion(agent, field, rowNumber, selector));
97
+ }
98
+ return output.filter(Boolean).join('\n').trim();
99
+ }
100
+ /** 上传命令返回后等待目标字段出现明确成功态,并兼容只写入文件但未触发组件事件的页面。 */
101
+ function waitForUploadCompletion(agent, field, rowNumber, ref) {
41
102
  const waitLogs = [];
103
+ let inputChangeAttempted = false;
42
104
  agent.waitMs(300);
43
105
  for (let attempt = 1; attempt <= 20; attempt += 1) {
44
- const script = `(() => {
45
- const uploadHelper = ${uploadDomHelperSource()};
46
- return JSON.stringify(uploadHelper.getUploadStateByField(${JSON.stringify(field)}));
47
- })()`;
48
- const state = parseEvalJson(agent.evaluate(script));
106
+ const state = getUploadState(agent, field, rowNumber, 1);
49
107
  if (state.failed) {
50
108
  throw new Error(`上传失败:${field}`);
51
109
  }
52
- if (state.pending !== true) {
110
+ if (state.completed === true) {
53
111
  return [...waitLogs, `upload settled ${field}`].join('\n');
54
112
  }
113
+ if (!inputChangeAttempted && state.pending !== true) {
114
+ inputChangeAttempted = true;
115
+ const notification = notifyUploadInputChange(agent, ref);
116
+ if (notification.dispatched === true) {
117
+ waitLogs.push(`upload notify input change ${field} files=${notification.files ?? 0}`);
118
+ agent.waitMs(300);
119
+ continue;
120
+ }
121
+ }
55
122
  waitLogs.push(`upload wait ${attempt} ${field}`);
56
123
  agent.waitMs(500);
57
124
  }
58
125
  throw new Error(`等待上传完成超时:${field}`);
59
126
  }
60
- /** Ant Upload 等组件会隐藏真实 file input,snapshot 缺失时改用 DOM 字段邻近关系定位。 */
61
- function findHiddenUploadInputSelector(agent, field) {
127
+ /** 仅对已注入文件的隐藏 input 补发原生事件,解决部分 Ant Upload 未响应 CDP 文件写入的问题。 */
128
+ function notifyUploadInputChange(agent, ref) {
129
+ if (!ref.startsWith('[') && !ref.startsWith('#') && !ref.startsWith('.')) {
130
+ return { dispatched: false };
131
+ }
132
+ const script = `(() => {
133
+ const input = document.querySelector(${JSON.stringify(ref)});
134
+ if (!(input instanceof HTMLInputElement) || input.type !== 'file' || !input.files?.length) {
135
+ return JSON.stringify({ dispatched: false, files: input instanceof HTMLInputElement ? input.files?.length || 0 : 0 });
136
+ }
137
+ input.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
138
+ input.dispatchEvent(new Event('change', { bubbles: true, composed: true }));
139
+ return JSON.stringify({ dispatched: true, files: input.files.length });
140
+ })()`;
141
+ try {
142
+ return parseEvalJson(agent.evaluate(script));
143
+ }
144
+ catch {
145
+ return { dispatched: false };
146
+ }
147
+ }
148
+ /** 统一读取字段级上传状态,供等待逻辑和最终效果校验复用。 */
149
+ function getUploadState(agent, field, rowNumber, expectedCount) {
150
+ const rowArgument = rowNumber ? `, ${rowNumber}` : ', null';
62
151
  const script = `(() => {
63
152
  const uploadHelper = ${uploadDomHelperSource()};
64
- const result = uploadHelper.findUploadInputByField(${JSON.stringify(field)});
153
+ return JSON.stringify(uploadHelper.getUploadStateByField(${JSON.stringify(field)}${rowArgument}, ${expectedCount}));
154
+ })()`;
155
+ return parseEvalJson(agent.evaluate(script));
156
+ }
157
+ /** Ant Upload 等组件会隐藏真实 file input,同时返回用于滚动的可见字段容器。 */
158
+ function findHiddenUploadInputSelector(agent, field, rowNumber) {
159
+ const rowArgument = rowNumber ? `, ${rowNumber}` : '';
160
+ const script = `(() => {
161
+ const uploadHelper = ${uploadDomHelperSource()};
162
+ const result = uploadHelper.findUploadInputByField(${JSON.stringify(field)}${rowArgument});
65
163
  return JSON.stringify(result);
66
164
  })()
67
165
  `;
68
166
  try {
69
167
  const parsed = parseEvalJson(agent.evaluate(script));
70
- return parsed.found && parsed.selector ? { selector: parsed.selector } : null;
168
+ return parsed.found && parsed.selector
169
+ ? { selector: parsed.selector, scrollSelector: parsed.scrollSelector }
170
+ : null;
71
171
  }
72
172
  catch {
73
173
  return null;
74
174
  }
75
175
  }
176
+ /** 表格上传组件可能在点击单元格入口后才创建 file input,按列头定位首行入口并触发一次。 */
177
+ function revealTableUploadInput(agent, field, rowNumber) {
178
+ const rowArgument = rowNumber ? `, ${rowNumber}` : '';
179
+ const script = `(() => {
180
+ const uploadHelper = ${uploadDomHelperSource()};
181
+ return JSON.stringify(uploadHelper.revealTableUploadInputByField(${JSON.stringify(field)}${rowArgument}));
182
+ })()`;
183
+ try {
184
+ return parseEvalJson(agent.evaluate(script)).revealed === true;
185
+ }
186
+ catch {
187
+ return false;
188
+ }
189
+ }
190
+ /** 上传定位最终失败时记录表头、数据行和 file input 的真实结构,避免仅凭快照反复猜测。 */
191
+ function diagnoseUploadTarget(agent, field, rowNumber) {
192
+ const rowArgument = rowNumber ? `, ${rowNumber}` : '';
193
+ const script = `(() => {
194
+ const uploadHelper = ${uploadDomHelperSource()};
195
+ return JSON.stringify(uploadHelper.diagnoseUploadByField(${JSON.stringify(field)}${rowArgument}));
196
+ })()`;
197
+ try {
198
+ return JSON.stringify(parseEvalJson(agent.evaluate(script)).diagnostic ?? {});
199
+ }
200
+ catch (error) {
201
+ return JSON.stringify({ error: error instanceof Error ? error.message : String(error) });
202
+ }
203
+ }
204
+ /** 批量上传前一次性标记字段内的全部目标 input,确保素材与槽位按页面顺序一一对应。 */
205
+ function findBatchUploadInputSelectors(agent, field, count, rowNumber) {
206
+ const rowArgument = rowNumber ? `, ${rowNumber}` : '';
207
+ const script = `(() => {
208
+ const uploadHelper = ${uploadDomHelperSource()};
209
+ const result = uploadHelper.findUploadInputsByField(${JSON.stringify(field)}, ${count}${rowArgument});
210
+ return JSON.stringify(result);
211
+ })()
212
+ `;
213
+ try {
214
+ const parsed = parseEvalJson(agent.evaluate(script));
215
+ return { selectors: parsed.selectors ?? [], scrollSelector: parsed.scrollSelector };
216
+ }
217
+ catch {
218
+ return { selectors: [] };
219
+ }
220
+ }
76
221
  /** 返回在页面上下文执行的上传控件定位工具源码,兼容隐藏 input[type=file]。 */
77
222
  function uploadDomHelperSource() {
78
223
  return `
@@ -83,6 +228,19 @@ const browserOptUploadVisible = (element) => {
83
228
  const rect = element.getBoundingClientRect();
84
229
  return style.visibility !== 'hidden' && style.display !== 'none' && rect.width > 0 && rect.height > 0;
85
230
  };
231
+ const browserOptUploadStableSelector = (element, attribute, prefix) => {
232
+ const existingId = element.getAttribute(attribute);
233
+ const existingSelector = existingId ? '[' + attribute + '="' + existingId + '"]' : '';
234
+ if (existingSelector && document.querySelectorAll(existingSelector).length === 1) {
235
+ return existingSelector;
236
+ }
237
+ const root = document.documentElement;
238
+ const sequence = Number(root.getAttribute('data-browser-opt-upload-sequence') || '0') + 1;
239
+ root.setAttribute('data-browser-opt-upload-sequence', String(sequence));
240
+ const id = prefix + sequence;
241
+ element.setAttribute(attribute, id);
242
+ return '[' + attribute + '="' + id + '"]';
243
+ };
86
244
  const browserOptUploadAncestorChain = (element) => {
87
245
  const chain = [];
88
246
  let current = element;
@@ -92,8 +250,146 @@ const browserOptUploadAncestorChain = (element) => {
92
250
  }
93
251
  return chain;
94
252
  };
253
+ const browserOptUploadTableRoot = (element) => element.closest('.ant-table, .el-table, [role="table"], [role="grid"]')
254
+ || element.closest('table')?.parentElement;
255
+ const browserOptUploadTableRows = (root) => root
256
+ ? [...root.querySelectorAll('tbody tr, [role="row"], .ant-table-tbody-virtual-holder-inner .ant-table-row')]
257
+ .filter((row) => !row.closest('thead') && !row.querySelector(':scope > [role="columnheader"]'))
258
+ : [];
259
+ const browserOptUploadRowCells = (row) => [...row.querySelectorAll(':scope > td, :scope > th, :scope > [role="cell"], :scope > [role="gridcell"], :scope > .ant-table-cell')];
260
+ const browserOptUploadVisibleDescendants = (root, selector) => [...root.querySelectorAll(selector)].filter(browserOptUploadVisible);
261
+ const browserOptUploadRowGroups = (rows) => {
262
+ const groups = [];
263
+ for (const row of [...rows].sort((left, right) => left.getBoundingClientRect().top - right.getBoundingClientRect().top)) {
264
+ const top = row.getBoundingClientRect().top;
265
+ const group = groups.find((item) => Math.abs(item.top - top) <= 2);
266
+ if (group) group.rows.push(row);
267
+ else groups.push({ top, rows: [row] });
268
+ }
269
+ return groups.map((group) => group.rows);
270
+ };
271
+ const browserOptUploadMatchesRow = (element, rowNumber) => {
272
+ if (!rowNumber) return true;
273
+ const row = element.closest('tbody tr, [role="row"], .ant-table-row');
274
+ const root = row ? browserOptUploadTableRoot(row) : null;
275
+ const targetRows = root ? browserOptUploadRowGroups(browserOptUploadTableRows(root))[rowNumber - 1] || [] : [];
276
+ return Boolean(row && targetRows.includes(row));
277
+ };
278
+ const browserOptUploadHorizontalOverlap = (left, right) => Math.max(0, Math.min(left.right, right.right) - Math.max(left.left, right.left));
279
+ const browserOptUploadSyncVirtualHeader = (element) => {
280
+ const virtualTable = element.closest('.ant-table-virtual');
281
+ const virtualBody = element.closest('.ant-table-tbody-virtual-holder');
282
+ const virtualHeader = virtualTable?.querySelector('.ant-table-header');
283
+ if (virtualBody && virtualHeader) {
284
+ virtualHeader.scrollLeft = virtualBody.scrollLeft;
285
+ }
286
+ };
287
+ const browserOptUploadCellIdentities = (cell) => {
288
+ const identities = [];
289
+ for (const name of ['data-column-key', 'data-field', 'data-prop', 'data-index', 'aria-colindex']) {
290
+ const value = cell.getAttribute(name);
291
+ if (value) identities.push(name + '=' + value);
292
+ }
293
+ for (const token of cell.classList || []) {
294
+ if (/column|(^|[-_])col([-_]|\\d)|cell[-_]\\d/i.test(token) && !/^(ant-table-cell|el-table__cell)$/.test(token)) {
295
+ identities.push('class=' + token);
296
+ }
297
+ }
298
+ const table = cell.closest('table');
299
+ const index = cell.cellIndex;
300
+ const col = table && index >= 0 ? table.querySelectorAll('colgroup col')[index] : null;
301
+ if (col) {
302
+ for (const name of ['name', 'class', 'data-column-key', 'data-index']) {
303
+ const value = col.getAttribute(name);
304
+ if (value) identities.push('col-' + name + '=' + value);
305
+ }
306
+ }
307
+ return [...new Set(identities)];
308
+ };
309
+ const browserOptUploadInputsInside = (cell) => {
310
+ if (!cell) return [];
311
+ const inputs = [
312
+ ...(cell.matches('input[type="file"]') ? [cell] : []),
313
+ ...cell.querySelectorAll('input[type="file"]'),
314
+ ];
315
+ return [...new Set(inputs)];
316
+ };
317
+ const browserOptUploadTableColumnCells = (header, rowNumber) => {
318
+ const component = browserOptUploadTableRoot(header);
319
+ if (!component) return [];
320
+ browserOptUploadSyncVirtualHeader(component);
321
+ const headerRect = header.getBoundingClientRect();
322
+ const rows = browserOptUploadTableRows(component);
323
+ const targetRows = rowNumber ? browserOptUploadRowGroups(rows)[rowNumber - 1] || [] : rows;
324
+ const headerCells = [...(header.parentElement?.children || [])];
325
+ const columnIndex = header.cellIndex >= 0 ? header.cellIndex : headerCells.indexOf(header);
326
+
327
+ if (component.matches('.ant-table-virtual') && columnIndex >= 0) {
328
+ const exactCells = targetRows.map((row) => browserOptUploadRowCells(row)[columnIndex]).filter(Boolean);
329
+ if (exactCells.length > 0) return exactCells;
330
+ }
331
+
332
+ const headerIdentities = browserOptUploadCellIdentities(header);
333
+ const identityCells = headerIdentities.length === 0 ? [] : targetRows
334
+ .flatMap((row) => browserOptUploadRowCells(row)
335
+ .filter((cell) => browserOptUploadCellIdentities(cell).some((identity) => headerIdentities.includes(identity))));
336
+ if (identityCells.length > 0) return identityCells;
337
+
338
+ const sameTextHeaders = [...component.querySelectorAll('thead th, thead td, [role="columnheader"]')]
339
+ .filter((item) => normalizeBrowserOptUploadText(item.textContent || '') === normalizeBrowserOptUploadText(header.textContent || ''));
340
+ const geometricCells = sameTextHeaders.length > 1 ? [] : targetRows
341
+ .flatMap((row) => browserOptUploadRowCells(row)
342
+ .map((cell) => ({ cell, rowTop: row.getBoundingClientRect().top, overlap: browserOptUploadHorizontalOverlap(headerRect, cell.getBoundingClientRect()) })))
343
+ .filter((item) => item.overlap > 0)
344
+ .sort((a, b) => b.overlap - a.overlap || a.rowTop - b.rowTop)
345
+ .map((item) => item.cell);
346
+ if (geometricCells.length > 0) return geometricCells;
347
+
348
+ if (columnIndex < 0) return [];
349
+ const ownTable = header.closest('table');
350
+ const ownRows = ownTable?.querySelector('tbody tr') ? [...ownTable.querySelectorAll('tbody tr')] : [];
351
+ const indexedRows = rowNumber
352
+ ? (ownRows.length > 0 ? [ownRows[rowNumber - 1]].filter(Boolean) : targetRows)
353
+ : (ownRows.length > 0 ? ownRows : rows);
354
+ return indexedRows.map((row) => browserOptUploadRowCells(row)[columnIndex]).filter(Boolean);
355
+ };
356
+ const browserOptUploadFieldHeaders = (field) => [...document.querySelectorAll('thead th, thead td, [role="columnheader"]')]
357
+ .filter((element) => browserOptUploadVisible(element))
358
+ .filter((element) => normalizeBrowserOptUploadText(element.textContent || '').includes(normalizeBrowserOptUploadText(field)));
359
+ const browserOptUploadHeadersForCell = (cell) => {
360
+ const root = browserOptUploadTableRoot(cell);
361
+ if (!root) return [];
362
+ const cellRect = cell.getBoundingClientRect();
363
+ const identities = browserOptUploadCellIdentities(cell);
364
+ const headers = [...root.querySelectorAll('thead th, thead td, [role="columnheader"]')]
365
+ .filter((header) => browserOptUploadVisible(header));
366
+ if (root.matches('.ant-table-virtual')) {
367
+ const row = cell.closest('.ant-table-row');
368
+ const columnIndex = row ? browserOptUploadRowCells(row).indexOf(cell) : -1;
369
+ const header = columnIndex >= 0 ? headers[columnIndex] : null;
370
+ if (header) return [header];
371
+ }
372
+ const identityHeaders = identities.length === 0 ? [] : headers
373
+ .filter((header) => browserOptUploadCellIdentities(header).some((identity) => identities.includes(identity)));
374
+ if (identityHeaders.length > 0) return identityHeaders;
375
+ return headers
376
+ .map((header) => ({ header, overlap: browserOptUploadHorizontalOverlap(header.getBoundingClientRect(), cellRect) }))
377
+ .filter((item) => item.overlap > 0)
378
+ .sort((a, b) => b.overlap - a.overlap)
379
+ .map((item) => item.header);
380
+ };
95
381
  const browserOptUploadContext = (input, fieldText) => {
96
382
  const chain = browserOptUploadAncestorChain(input);
383
+ const cell = input.closest('td, th, [role="cell"], [role="gridcell"], .ant-table-cell');
384
+ const table = cell?.closest('table');
385
+ if (cell) {
386
+ const geometricHeader = browserOptUploadHeadersForCell(cell)
387
+ .find((header) => normalizeBrowserOptUploadText(header.textContent || '').includes(fieldText));
388
+ if (geometricHeader) {
389
+ return { text: normalizeBrowserOptUploadText(geometricHeader.textContent || ''), depth: 0.125, source: 'table-geometry' };
390
+ }
391
+ }
392
+
97
393
  const directMatch = chain
98
394
  .map((element, depth) => ({ element, depth, text: normalizeBrowserOptUploadText(element.textContent || '') }))
99
395
  .find((item) => item.text.includes(fieldText));
@@ -101,6 +397,18 @@ const browserOptUploadContext = (input, fieldText) => {
101
397
  return { text: directMatch.text, depth: directMatch.depth, source: 'ancestor' };
102
398
  }
103
399
 
400
+ if (cell && table) {
401
+ const headerRows = [...table.querySelectorAll('thead tr')];
402
+ const headerCells = headerRows.length > 0
403
+ ? [...headerRows[headerRows.length - 1].querySelectorAll('th, td')]
404
+ : [];
405
+ const header = headerCells[cell.cellIndex];
406
+ const headerText = normalizeBrowserOptUploadText(header?.textContent || '');
407
+ if (headerText.includes(fieldText)) {
408
+ return { text: headerText, depth: 0.25, source: 'table-header' };
409
+ }
410
+ }
411
+
104
412
  for (const item of chain.map((element, depth) => ({ element, depth }))) {
105
413
  const siblings = [item.element.previousElementSibling, item.element.nextElementSibling]
106
414
  .filter(Boolean)
@@ -120,9 +428,44 @@ const browserOptUploadContext = (input, fieldText) => {
120
428
 
121
429
  return { text: normalizeBrowserOptUploadText(chain.map((element) => element.textContent || '').join(' ')), depth: 99, source: 'none' };
122
430
  };
123
- function findUploadInputByField(field) {
431
+ function findUploadInputsByField(field, limit, rowNumber = null) {
124
432
  const fieldText = normalizeBrowserOptUploadText(field);
125
- const inputs = [...document.querySelectorAll('input[type="file"]')];
433
+ const tableCandidates = [];
434
+ for (const header of browserOptUploadFieldHeaders(field)) {
435
+ for (const cell of browserOptUploadTableColumnCells(header, rowNumber)) {
436
+ for (const input of browserOptUploadInputsInside(cell).filter((candidate) => browserOptUploadMatchesRow(candidate, rowNumber))) {
437
+ tableCandidates.push({ input, cell });
438
+ }
439
+ }
440
+ }
441
+ const revealedCells = [...document.querySelectorAll('[data-browser-opt-upload-target-field]')]
442
+ .filter((cell) => cell.getAttribute('data-browser-opt-upload-target-field') === fieldText)
443
+ .filter((cell) => !rowNumber || cell.getAttribute('data-browser-opt-upload-target-row') === String(rowNumber));
444
+ const revealedInputs = [...document.querySelectorAll('input[type="file"]')]
445
+ .filter((input) => !input.hasAttribute('data-browser-opt-upload-before-reveal'));
446
+ if (revealedCells.length === 1 && revealedInputs.length === 1) {
447
+ tableCandidates.push({ input: revealedInputs[0], cell: revealedCells[0] });
448
+ }
449
+ if (tableCandidates.length > 0) {
450
+ const uniqueTableCandidates = [...new Map(tableCandidates.map((item) => [item.input, item])).values()];
451
+ const selectors = uniqueTableCandidates.slice(0, limit).map((candidate) => browserOptUploadStableSelector(
452
+ candidate.input,
453
+ 'data-browser-opt-upload-id',
454
+ 'browser-opt-upload-',
455
+ ));
456
+ const scrollCell = uniqueTableCandidates[0]?.cell;
457
+ let scrollSelector;
458
+ if (scrollCell) {
459
+ scrollSelector = browserOptUploadStableSelector(
460
+ scrollCell,
461
+ 'data-browser-opt-upload-scroll-target',
462
+ 'browser-opt-upload-scroll-',
463
+ );
464
+ }
465
+ return { found: selectors.length > 0, selectors, count: uniqueTableCandidates.length, scrollSelector };
466
+ }
467
+ const inputs = [...document.querySelectorAll('input[type="file"]')]
468
+ .filter((input) => browserOptUploadMatchesRow(input, rowNumber));
126
469
  const candidates = inputs.map((input, index) => {
127
470
  const chain = browserOptUploadAncestorChain(input);
128
471
  const context = browserOptUploadContext(input, fieldText);
@@ -131,56 +474,166 @@ function findUploadInputByField(field) {
131
474
  const score = context.text.includes(fieldText) ? 3 : fieldText.includes(context.text) && context.text ? 2 : 0;
132
475
  return { input, index, text: context.text, depth: context.depth, score, visible: Boolean(rect && rect.width > 0 && rect.height > 0) };
133
476
  }).sort((a, b) => b.score - a.score || a.depth - b.depth || Number(b.visible) - Number(a.visible) || a.index - b.index);
134
- const best = candidates[0];
135
- if (!best || best.score <= 0) {
477
+ let selected = candidates.filter((candidate) => candidate.score > 0);
478
+ if (selected.length === 0) {
136
479
  if (inputs.length === 1) {
137
- inputs[0].setAttribute('data-browser-opt-upload-id', 'browser-opt-upload-0');
138
- return { found: true, selector: '[data-browser-opt-upload-id="browser-opt-upload-0"]', fallbackSingleInput: true };
480
+ selected = candidates.slice(0, 1);
481
+ } else {
482
+ return { found: false, selectors: [], count: inputs.length };
139
483
  }
140
- return { found: false, count: inputs.length };
141
484
  }
142
- const id = 'browser-opt-upload-' + best.index;
143
- best.input.setAttribute('data-browser-opt-upload-id', id);
144
- return { found: true, selector: '[data-browser-opt-upload-id="' + id + '"]', matchedText: best.text };
485
+ const selectors = selected.slice(0, limit).map((candidate) => browserOptUploadStableSelector(
486
+ candidate.input,
487
+ 'data-browser-opt-upload-id',
488
+ 'browser-opt-upload-',
489
+ ));
490
+ const firstSelected = selected[0];
491
+ const scrollElement = firstSelected
492
+ ? browserOptUploadAncestorChain(firstSelected.input)
493
+ .find((element) => browserOptUploadVisible(element) && normalizeBrowserOptUploadText(element.textContent || '').includes(fieldText))
494
+ || browserOptUploadAncestorChain(firstSelected.input).find((element) => browserOptUploadVisible(element))
495
+ : null;
496
+ let scrollSelector;
497
+ if (scrollElement) {
498
+ scrollSelector = browserOptUploadStableSelector(
499
+ scrollElement,
500
+ 'data-browser-opt-upload-scroll-target',
501
+ 'browser-opt-upload-scroll-',
502
+ );
503
+ }
504
+ return { found: selectors.length > 0, selectors, count: selected.length, scrollSelector };
505
+ }
506
+ function findUploadInputByField(field, rowNumber = null) {
507
+ const result = findUploadInputsByField(field, 1, rowNumber);
508
+ return result.found
509
+ ? { found: true, selector: result.selectors[0], count: result.count, scrollSelector: result.scrollSelector }
510
+ : result;
145
511
  }
146
- function getUploadStateByField(field) {
147
- const target = findUploadInputByField(field);
148
- if (!target.found || !target.selector) {
149
- return { found: false, pending: false, failed: false };
512
+ function getUploadStateByField(field, rowNumber = null, expectedCount = 1) {
513
+ const targets = findUploadInputsByField(field, expectedCount, rowNumber);
514
+ const tableScopes = browserOptUploadFieldHeaders(field)
515
+ .flatMap((header) => browserOptUploadTableColumnCells(header, rowNumber));
516
+ const uniqueTableScopes = [...new Set(tableScopes)].slice(0, expectedCount);
517
+ if ((!targets.found || targets.selectors.length === 0) && uniqueTableScopes.length === 0) {
518
+ return { found: false, pending: false, failed: false, inputFilesCount: 0 };
150
519
  }
151
- const input = document.querySelector(target.selector);
152
- if (!input) {
153
- return { found: false, pending: false, failed: false };
520
+ const fieldText = normalizeBrowserOptUploadText(field);
521
+ const inputs = (targets.selectors || []).map((selector) => document.querySelector(selector)).filter(Boolean);
522
+ const scopes = uniqueTableScopes.length > 0 ? uniqueTableScopes : inputs.map((input) => {
523
+ const chain = browserOptUploadAncestorChain(input);
524
+ return input.closest('td, th, [role="cell"], [role="gridcell"], .ant-table-cell')
525
+ || chain.find((element) => /ant-form-item|form-item|field/i.test(String(element.className || ''))
526
+ && normalizeBrowserOptUploadText(element.textContent || '').includes(fieldText))
527
+ || chain.find((element) => normalizeBrowserOptUploadText(element.textContent || '').includes(fieldText))
528
+ || input.parentElement;
529
+ }).filter(Boolean);
530
+ const states = scopes.map((scope, index) => {
531
+ const input = inputs[index] || inputs.find((candidate) => scope.contains(candidate));
532
+ const visibleMatches = (selectors) => [...scope.querySelectorAll(selectors)].filter(browserOptUploadVisible);
533
+ const failed = visibleMatches([
534
+ '.ant-upload-list-item-error',
535
+ '.ant-progress-status-exception',
536
+ '.el-upload-list__item.is-fail',
537
+ '[class*="upload-error"]',
538
+ '[class*="upload-fail"]'
539
+ ].join(',')).length > 0 || /上传失败|上传错误/.test(scope.textContent || '');
540
+ const pending = visibleMatches([
541
+ '.ant-upload-list-item-uploading',
542
+ '.ant-upload-list-item-progress',
543
+ '.ant-progress-status-active',
544
+ '.ant-spin-spinning',
545
+ '.anticon-loading',
546
+ '.el-loading-mask',
547
+ '.el-icon-loading',
548
+ '[aria-busy="true"]',
549
+ '[class*="uploading"]'
550
+ ].join(',')).length > 0;
551
+ const success = visibleMatches([
552
+ '.ant-upload-list-item-done',
553
+ '.ant-upload-list-item-success',
554
+ '.ant-upload-list-picture-card',
555
+ '.el-upload-list__item.is-success',
556
+ '[class*="upload-success"]',
557
+ '[class*="upload-done"]',
558
+ 'img[src]'
559
+ ].join(',')).length > 0;
560
+ // file input 持有文件只表示命令写入成功;必须等上传组件渲染出预览或成功态,才能确认业务上传完成。
561
+ const completed = !pending && !failed && success;
562
+ return { pending, failed, completed, inputFilesCount: input?.files?.length || 0 };
563
+ });
564
+ const completedCount = states.filter((state) => state.completed).length;
565
+ return {
566
+ found: true,
567
+ pending: states.some((state) => state.pending),
568
+ failed: states.some((state) => state.failed),
569
+ completed: completedCount >= expectedCount,
570
+ completedCount,
571
+ inputFilesCount: states.reduce((sum, state) => sum + (state.inputFilesCount || 0), 0),
572
+ };
573
+ }
574
+ function revealTableUploadInputByField(field, rowNumber = null) {
575
+ for (const header of browserOptUploadFieldHeaders(field)) {
576
+ for (const cell of browserOptUploadTableColumnCells(header, rowNumber)) {
577
+ const candidates = browserOptUploadVisibleDescendants(cell, 'button, label, [role="button"], [class*="upload"], [onclick]')
578
+ .sort((a, b) => {
579
+ const aRect = a.getBoundingClientRect();
580
+ const bRect = b.getBoundingClientRect();
581
+ return aRect.width * aRect.height - bRect.width * bRect.height;
582
+ });
583
+ const trigger = candidates[0] || (browserOptUploadVisible(cell) ? cell : null);
584
+ if (!trigger) continue;
585
+ document.querySelectorAll('[data-browser-opt-upload-before-reveal]').forEach((input) => {
586
+ input.removeAttribute('data-browser-opt-upload-before-reveal');
587
+ });
588
+ document.querySelectorAll('[data-browser-opt-upload-target-field]').forEach((target) => {
589
+ target.removeAttribute('data-browser-opt-upload-target-field');
590
+ target.removeAttribute('data-browser-opt-upload-target-row');
591
+ });
592
+ document.querySelectorAll('input[type="file"]').forEach((input) => {
593
+ input.setAttribute('data-browser-opt-upload-before-reveal', 'true');
594
+ });
595
+ cell.setAttribute('data-browser-opt-upload-target-field', normalizeBrowserOptUploadText(field));
596
+ cell.setAttribute('data-browser-opt-upload-target-row', rowNumber ? String(rowNumber) : '');
597
+ trigger.scrollIntoView({ block: 'center', inline: 'nearest' });
598
+ trigger.click();
599
+ return { found: true, revealed: true };
600
+ }
154
601
  }
602
+ return { found: false, revealed: false };
603
+ }
604
+ function diagnoseUploadByField(field, rowNumber = null) {
155
605
  const fieldText = normalizeBrowserOptUploadText(field);
156
- const scope = browserOptUploadAncestorChain(input)
157
- .find((element) => normalizeBrowserOptUploadText(element.textContent || '').includes(fieldText))
158
- || input.parentElement;
159
- if (!scope) {
160
- return { found: true, pending: false, failed: false };
161
- }
162
- const visibleMatches = (selectors) => [...scope.querySelectorAll(selectors)].filter(browserOptUploadVisible);
163
- const failed = visibleMatches([
164
- '.ant-upload-list-item-error',
165
- '.ant-progress-status-exception',
166
- '.el-upload-list__item.is-fail',
167
- '[class*="upload-error"]',
168
- '[class*="upload-fail"]'
169
- ].join(',')).length > 0;
170
- const pending = visibleMatches([
171
- '.ant-upload-list-item-uploading',
172
- '.ant-upload-list-item-progress',
173
- '.ant-progress-status-active',
174
- '.ant-spin-spinning',
175
- '.anticon-loading',
176
- '.el-loading-mask',
177
- '.el-icon-loading',
178
- '[aria-busy="true"]',
179
- '[class*="uploading"]'
180
- ].join(',')).length > 0;
181
- return { found: true, pending, failed };
182
- }
183
- return { findUploadInputByField, getUploadStateByField };
606
+ const headers = [...document.querySelectorAll('thead th, thead td, [role="columnheader"]')]
607
+ .filter((header) => normalizeBrowserOptUploadText(header.textContent || '').includes(fieldText))
608
+ .map((header) => {
609
+ const component = browserOptUploadTableRoot(header);
610
+ const rect = header.getBoundingClientRect();
611
+ return {
612
+ tag: header.tagName,
613
+ className: String(header.className || ''),
614
+ identities: browserOptUploadCellIdentities(header),
615
+ rect: { left: rect.left, right: rect.right, top: rect.top, width: rect.width },
616
+ component: component ? { tag: component.tagName, className: String(component.className || '') } : null,
617
+ rows: browserOptUploadTableRows(component).length,
618
+ ownRows: header.closest('table')?.querySelectorAll('tbody tr').length || 0,
619
+ };
620
+ });
621
+ const inputs = [...document.querySelectorAll('input[type="file"]')]
622
+ .filter((input) => browserOptUploadMatchesRow(input, rowNumber))
623
+ .map((input) => {
624
+ const cell = input.closest('td, th, [role="cell"], [role="gridcell"], .ant-table-cell');
625
+ const context = browserOptUploadContext(input, fieldText);
626
+ return {
627
+ className: String(input.className || ''),
628
+ cellTag: cell?.tagName || null,
629
+ cellClassName: String(cell?.className || ''),
630
+ identities: cell ? browserOptUploadCellIdentities(cell) : [],
631
+ context: { source: context.source, depth: context.depth, matches: context.text.includes(fieldText) },
632
+ };
633
+ });
634
+ return { diagnostic: { headers, inputs, inputCount: inputs.length } };
635
+ }
636
+ return { findUploadInputByField, findUploadInputsByField, getUploadStateByField, revealTableUploadInputByField, diagnoseUploadByField };
184
637
  })()
185
638
  `;
186
639
  }