@szc-ft/mcp-szcd-client 0.28.2 → 0.30.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.
@@ -49,6 +49,28 @@ export class AntAdapter {
49
49
  this.componentMeta = options.componentMeta || {};
50
50
  }
51
51
 
52
+ /**
53
+ * 统一的表单项查找 helper(浏览器端执行)
54
+ * 弹窗/抽屉打开时优先在其中搜索,避免页面搜索栏的同名 label 被错误命中
55
+ * 用法:在 frame.evaluate 内调用 _findFormItem(label)
56
+ */
57
+ static get FORM_ITEM_HELPER() {
58
+ return `
59
+ function _findFormItem(label) {
60
+ var modalOrDrawer = document.querySelector('.ant-modal:not(.ant-modal-hidden), .ant-drawer:not(.ant-drawer-hidden), .ant-drawer-open .ant-drawer-content-wrapper');
61
+ var scope = modalOrDrawer || document;
62
+ var items = Array.from(scope.querySelectorAll('.ant-form-item'));
63
+ if (items.length === 0 && scope !== document) {
64
+ items = items.concat(Array.from(document.querySelectorAll('.ant-form-item')));
65
+ }
66
+ return items.find(function(el) {
67
+ var lbl = el.querySelector('.ant-form-item-label label, .ant-form-item-label');
68
+ return lbl && (lbl.innerText || lbl.textContent || '').trim().includes(label);
69
+ }) || null;
70
+ }
71
+ `;
72
+ }
73
+
52
74
  /**
53
75
  * 关闭所有打开的下拉浮层 / 抽屉 / 弹窗
54
76
  * 反馈第 2 条:每次 Select 操作后必须调用,否则下次操作会命中残留选项
@@ -61,9 +83,9 @@ export class AntAdapter {
61
83
  });
62
84
  // 2) 隐藏所有未隐藏的下拉浮层
63
85
  const dropdowns = document.querySelectorAll(
64
- "body > .ant-select-dropdown:not(.ant-select-dropdown-hidden), " +
65
- "body > .ant-cascader-dropdown:not(.ant-cascader-dropdown-hidden), " +
66
- "body > .ant-picker-dropdown:not(.ant-picker-dropdown-hidden)"
86
+ ".ant-select-dropdown:not(.ant-select-dropdown-hidden), " +
87
+ ".ant-cascader-dropdown:not(.ant-cascader-dropdown-hidden), " +
88
+ ".ant-picker-dropdown:not(.ant-picker-dropdown-hidden)"
67
89
  );
68
90
  const count = dropdowns.length;
69
91
  dropdowns.forEach((el) => {
@@ -79,39 +101,45 @@ export class AntAdapter {
79
101
  }).catch(() => ({ closed: 0 }));
80
102
  }
81
103
 
104
+ /**
105
+ * 在 frame 上执行 evaluate,自动注入 _findFormItem helper
106
+ * 后续所有新方法只需调用本方法,无需手动处理弹窗/抽屉优先逻辑
107
+ */
108
+ async _evalWithFormHelper(frame, fnBody) {
109
+ const wrapped = "(function() {" + AntAdapter.FORM_ITEM_HELPER + "return (" + fnBody + ")();\n})()";
110
+ return frame.evaluate(wrapped);
111
+ }
112
+
82
113
  /**
83
114
  * 定位包含指定 label 的 .ant-form-item,返回其内 control 元素的 selector 信息
84
115
  * 不直接返回 ElementHandle(跨 evaluate 边界丢失),而是返回一个临时标记 id
85
116
  */
86
117
  async _markFormItemControl(frame, labelText, controlHint) {
87
- return frame.evaluate(({ label, hint }) => {
88
- const items = Array.from(document.querySelectorAll(".ant-form-item"));
89
- const item = items.find((el) => {
90
- const lbl = el.querySelector(".ant-form-item-label label, .ant-form-item-label");
91
- if (!lbl) return false;
92
- return (lbl.innerText || lbl.textContent || "").trim().includes(label);
93
- });
118
+ const label = labelText.replace(/'/g, "\\'");
119
+ const hint = controlHint;
120
+ return this._evalWithFormHelper(frame, `() => {
121
+ const item = _findFormItem('${label}');
94
122
  if (!item) return { found: false };
95
123
 
96
124
  // 找控件:根据 hint 决定优先匹配何种节点
97
125
  let control;
98
- if (hint === "select") {
126
+ if (${JSON.stringify(hint)} === "select") {
99
127
  control = item.querySelector(".ant-select:not(.ant-select-disabled)");
100
- } else if (hint === "treeSelect") {
128
+ } else if (${JSON.stringify(hint)} === "treeSelect") {
101
129
  control = item.querySelector(".ant-select.ant-tree-select, .ant-tree-select");
102
- } else if (hint === "cascader") {
130
+ } else if (${JSON.stringify(hint)} === "cascader") {
103
131
  control = item.querySelector(".ant-cascader, .ant-cascader-picker");
104
- } else if (hint === "datePicker") {
132
+ } else if (${JSON.stringify(hint)} === "datePicker") {
105
133
  control = item.querySelector(".ant-picker");
106
- } else if (hint === "input") {
134
+ } else if (${JSON.stringify(hint)} === "input") {
107
135
  control = item.querySelector("input.ant-input, textarea.ant-input, .ant-input-affix-wrapper input");
108
- } else if (hint === "radio") {
136
+ } else if (${JSON.stringify(hint)} === "radio") {
109
137
  control = item.querySelector(".ant-radio-group");
110
- } else if (hint === "checkbox") {
138
+ } else if (${JSON.stringify(hint)} === "checkbox") {
111
139
  control = item.querySelector(".ant-checkbox-group, .ant-checkbox-wrapper");
112
- } else if (hint === "switch") {
140
+ } else if (${JSON.stringify(hint)} === "switch") {
113
141
  control = item.querySelector(".ant-switch");
114
- } else if (hint === "upload") {
142
+ } else if (${JSON.stringify(hint)} === "upload") {
115
143
  control = item.querySelector(".ant-upload input[type='file'], input[type='file']");
116
144
  } else {
117
145
  control = item.querySelector("input, .ant-select, .ant-picker, .ant-cascader");
@@ -120,7 +148,7 @@ export class AntAdapter {
120
148
  if (!control) return { found: true, hasControl: false };
121
149
 
122
150
  // 用临时 data 属性标记,避免重复 traversal
123
- const marker = `__sz_marker_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;
151
+ const marker = '__sz_marker_' + Date.now() + '_' + Math.random().toString(36).slice(2, 6);
124
152
  control.setAttribute("data-sz-marker", marker);
125
153
  item.setAttribute("data-sz-form-item-marker", marker);
126
154
  const rect = control.getBoundingClientRect();
@@ -131,7 +159,7 @@ export class AntAdapter {
131
159
  tagName: control.tagName,
132
160
  visible: rect.width > 0 && rect.height > 0,
133
161
  };
134
- }, { label: labelText, hint: controlHint });
162
+ }`);
135
163
  }
136
164
 
137
165
  async _clearMarkers(frame) {
@@ -189,12 +217,12 @@ export class AntAdapter {
189
217
  }
190
218
 
191
219
  const selectedSummary = [];
220
+ let popupResolved = popupFrame;
192
221
  for (const opt of options) {
193
222
  // 3) 浮层探测:先在触发器 frame 找,没找到再在 popupFrame 找(兜底)
194
- let popupResolved = popupFrame;
195
223
  let optionFound = await pollEval(popupResolved, (text) => {
196
224
  const items = Array.from(document.querySelectorAll(
197
- "body > .ant-select-dropdown:not(.ant-select-dropdown-hidden) .ant-select-item-option"
225
+ ".ant-select-dropdown:not(.ant-select-dropdown-hidden) .ant-select-item-option"
198
226
  ));
199
227
  const visibleItems = items.filter((el) => {
200
228
  const rect = el.getBoundingClientRect();
@@ -220,7 +248,7 @@ export class AntAdapter {
220
248
  if (parentFrame && parentFrame !== frame) {
221
249
  const retryFound = await pollEval(parentFrame, (text) => {
222
250
  const items = Array.from(document.querySelectorAll(
223
- "body > .ant-select-dropdown:not(.ant-select-dropdown-hidden) .ant-select-item-option"
251
+ ".ant-select-dropdown:not(.ant-select-dropdown-hidden) .ant-select-item-option"
224
252
  ));
225
253
  const visibleItems = items.filter((el) => {
226
254
  const rect = el.getBoundingClientRect();
@@ -257,12 +285,13 @@ export class AntAdapter {
257
285
  };
258
286
  }
259
287
 
260
- // 4) mousedown 选项(在 popupResolved frame 内)
288
+ // 4) 点击选项(在 popupResolved frame 内),需同时触发 mousedown+click 才可驱动 React onChange
261
289
  await popupResolved.evaluate((tag) => {
262
290
  const el = document.querySelector(`[data-sz-option-marker="${tag}"]`);
263
291
  if (!el) return false;
264
292
  el.dispatchEvent(new MouseEvent("mousedown", { bubbles: true, cancelable: true, button: 0 }));
265
293
  el.dispatchEvent(new MouseEvent("mouseup", { bubbles: true, cancelable: true, button: 0 }));
294
+ el.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true, button: 0 }));
266
295
  el.removeAttribute("data-sz-option-marker");
267
296
  return true;
268
297
  }, optionFound.value.tag);
@@ -338,8 +367,8 @@ export class AntAdapter {
338
367
 
339
368
  const found = await pollEval(frame, ({ text, isLeaf }) => {
340
369
  const titles = Array.from(document.querySelectorAll(
341
- "body > .ant-select-dropdown:not(.ant-select-dropdown-hidden) .ant-select-tree-title, " +
342
- "body > .ant-tree-select-dropdown:not(.ant-select-dropdown-hidden) .ant-select-tree-title"
370
+ ".ant-select-dropdown:not(.ant-select-dropdown-hidden) .ant-select-tree-title, " +
371
+ ".ant-tree-select-dropdown:not(.ant-select-dropdown-hidden) .ant-select-tree-title"
343
372
  ));
344
373
  const visible = titles.filter((el) => {
345
374
  const rect = el.getBoundingClientRect();
@@ -408,29 +437,33 @@ export class AntAdapter {
408
437
  */
409
438
  async input(frame, target, value, opts = {}) {
410
439
  const clear = opts.clear !== false;
411
- const result = await frame.evaluate(({ target, value, clear }) => {
412
- let el = null;
413
-
414
- if (target.label) {
415
- const items = Array.from(document.querySelectorAll(".ant-form-item"));
416
- const item = items.find((x) => {
417
- const lbl = x.querySelector(".ant-form-item-label");
418
- return lbl && (lbl.innerText || lbl.textContent || "").trim().includes(target.label);
419
- });
420
- el = item?.querySelector("input.ant-input, textarea.ant-input, .ant-input-affix-wrapper input, input[type='text'], input[type='number'], input[type='password'], input:not([type])");
421
- } else if (target.placeholder) {
422
- el = document.querySelector(`input[placeholder*="${target.placeholder}"], textarea[placeholder*="${target.placeholder}"]`);
423
- } else if (target.selector) {
424
- try { el = document.querySelector(target.selector); } catch { el = null; }
440
+ const label = (target.label || '').replace(/'/g, "\\'");
441
+ const placeholder = (target.placeholder || '').replace(/'/g, "\\'");
442
+ const selector = (target.selector || '').replace(/'/g, "\\'");
443
+
444
+ const result = await this._evalWithFormHelper(frame, `() => {
445
+ var el = null;
446
+ var val = ${JSON.stringify(value)};
447
+ var shouldClear = ${clear};
448
+
449
+ var item = _findFormItem('${label}');
450
+ if (item) {
451
+ el = item.querySelector("input.ant-input, textarea.ant-input, .ant-input-affix-wrapper input, input[type='text'], input[type='number'], input[type='password'], input:not([type])");
452
+ }
453
+ if (!el && '${placeholder}') {
454
+ el = document.querySelector('input[placeholder*="${placeholder}"], textarea[placeholder*="${placeholder}"]');
455
+ }
456
+ if (!el && '${selector}') {
457
+ try { el = document.querySelector('${selector}'); } catch(e) { el = null; }
425
458
  }
426
459
 
427
460
  if (!el) return { found: false };
428
461
  el.scrollIntoView({ block: "center" });
429
- const proto = Object.getPrototypeOf(el);
430
- const valueSetter = Object.getOwnPropertyDescriptor(proto, "value")?.set;
462
+ var proto = Object.getPrototypeOf(el);
463
+ var valueSetter = Object.getOwnPropertyDescriptor(proto, "value") && Object.getOwnPropertyDescriptor(proto, "value").set;
431
464
 
432
465
  el.focus();
433
- const newValue = clear ? value : (el.value || "") + value;
466
+ var newValue = shouldClear ? val : (el.value || "") + val;
434
467
  if (valueSetter) {
435
468
  valueSetter.call(el, newValue);
436
469
  } else {
@@ -444,7 +477,7 @@ export class AntAdapter {
444
477
  placeholder: el.getAttribute("placeholder"),
445
478
  tagName: el.tagName,
446
479
  };
447
- }, { target, value, clear });
480
+ }`);
448
481
 
449
482
  return {
450
483
  acted: result.found,
@@ -460,27 +493,26 @@ export class AntAdapter {
460
493
  * Ant Radio 选择
461
494
  */
462
495
  async radio(frame, fieldLabel, optionLabel) {
463
- const result = await frame.evaluate(({ field, opt }) => {
464
- const items = Array.from(document.querySelectorAll(".ant-form-item"));
465
- const item = items.find((x) => {
466
- const lbl = x.querySelector(".ant-form-item-label");
467
- return lbl && (lbl.innerText || lbl.textContent || "").trim().includes(field);
468
- });
496
+ const field = fieldLabel.replace(/'/g, "\\'");
497
+ const opt = optionLabel.replace(/'/g, "\\'");
498
+
499
+ const result = await this._evalWithFormHelper(frame, `() => {
500
+ var item = _findFormItem('${field}');
469
501
  if (!item) return { found: false, reason: "form item not found" };
470
502
 
471
- const wrappers = Array.from(item.querySelectorAll(".ant-radio-wrapper"));
472
- const target = wrappers.find((w) => (w.innerText || "").trim().includes(opt));
473
- if (!target) return { found: false, reason: `radio option "${opt}" not found`, available: wrappers.map((w) => (w.innerText || "").trim()) };
503
+ var wrappers = Array.from(item.querySelectorAll(".ant-radio-wrapper"));
504
+ var target = wrappers.find(function(w) { return (w.innerText || "").trim().includes('${opt}'); });
505
+ if (!target) return { found: false, reason: 'radio option not found', available: wrappers.map(function(w) { return (w.innerText || "").trim(); }) };
474
506
 
475
507
  target.scrollIntoView({ block: "center" });
476
508
  target.click();
477
- const input = target.querySelector("input[type='radio']");
509
+ var input = target.querySelector("input[type='radio']");
478
510
  return {
479
511
  found: true,
480
- checked: input?.checked || false,
481
- value: input?.value || "",
512
+ checked: input && input.checked || false,
513
+ value: input && input.value || "",
482
514
  };
483
- }, { field: fieldLabel, opt: optionLabel });
515
+ }`);
484
516
  return {
485
517
  acted: result.found,
486
518
  field: fieldLabel,
@@ -495,24 +527,20 @@ export class AntAdapter {
495
527
  * Ant Checkbox 切换(按 label 文本)
496
528
  */
497
529
  async checkbox(frame, fieldLabel, optionLabel, checked = true) {
498
- const result = await frame.evaluate(({ field, opt, target }) => {
499
- const items = Array.from(document.querySelectorAll(".ant-form-item"));
500
- const item = items.find((x) => {
501
- const lbl = x.querySelector(".ant-form-item-label");
502
- return lbl && (lbl.innerText || lbl.textContent || "").trim().includes(field);
503
- });
530
+ const field = fieldLabel.replace(/'/g, "\\'");
531
+ const opt = (optionLabel || '').replace(/'/g, "\\'");
532
+
533
+ const result = await this._evalWithFormHelper(frame, `() => {
534
+ var item = _findFormItem('${field}');
504
535
  if (!item) return { found: false };
505
- // 支持 group 和单独 checkbox
506
- const wrappers = Array.from(item.querySelectorAll(".ant-checkbox-wrapper"));
507
- const wrapper = opt
508
- ? wrappers.find((w) => (w.innerText || "").includes(opt))
509
- : wrappers[0];
536
+ var wrappers = Array.from(item.querySelectorAll(".ant-checkbox-wrapper"));
537
+ var wrapper = '${opt}' ? wrappers.find(function(w) { return (w.innerText || "").includes('${opt}'); }) : wrappers[0];
510
538
  if (!wrapper) return { found: false, reason: "checkbox not found" };
511
- const input = wrapper.querySelector("input[type='checkbox']");
539
+ var input = wrapper.querySelector("input[type='checkbox']");
512
540
  if (!input) return { found: false };
513
- if (input.checked !== target) wrapper.click();
541
+ if (input.checked !== ${checked}) wrapper.click();
514
542
  return { found: true, checked: input.checked };
515
- }, { field: fieldLabel, opt: optionLabel, target: checked });
543
+ }`);
516
544
  return { acted: result.found, field: fieldLabel, option: optionLabel, verified: result.checked === checked };
517
545
  }
518
546
 
@@ -520,19 +548,17 @@ export class AntAdapter {
520
548
  * Ant Switch 切换
521
549
  */
522
550
  async switch(frame, fieldLabel, on = true) {
523
- const result = await frame.evaluate(({ field, target }) => {
524
- const items = Array.from(document.querySelectorAll(".ant-form-item"));
525
- const item = items.find((x) => {
526
- const lbl = x.querySelector(".ant-form-item-label");
527
- return lbl && (lbl.innerText || lbl.textContent || "").trim().includes(field);
528
- });
551
+ const field = fieldLabel.replace(/'/g, "\\'");
552
+
553
+ const result = await this._evalWithFormHelper(frame, `() => {
554
+ var item = _findFormItem('${field}');
529
555
  if (!item) return { found: false };
530
- const sw = item.querySelector(".ant-switch");
556
+ var sw = item.querySelector(".ant-switch");
531
557
  if (!sw) return { found: false };
532
- const isOn = sw.getAttribute("aria-checked") === "true" || sw.classList.contains("ant-switch-checked");
533
- if (isOn !== target) sw.click();
534
- return { found: true, state: target };
535
- }, { field: fieldLabel, target: on });
558
+ var isOn = sw.getAttribute("aria-checked") === "true" || sw.classList.contains("ant-switch-checked");
559
+ if (isOn !== ${on}) sw.click();
560
+ return { found: true, state: ${on} };
561
+ }`);
536
562
  return { acted: result.found, field: fieldLabel, state: result.state, verified: result.state === on };
537
563
  }
538
564
 
@@ -541,29 +567,27 @@ export class AntAdapter {
541
567
  */
542
568
  async datePicker(frame, fieldLabel, dateString) {
543
569
  await this.closeAllDropdowns(frame);
544
- const result = await frame.evaluate(({ field, value }) => {
545
- const items = Array.from(document.querySelectorAll(".ant-form-item"));
546
- const item = items.find((x) => {
547
- const lbl = x.querySelector(".ant-form-item-label");
548
- return lbl && (lbl.innerText || lbl.textContent || "").trim().includes(field);
549
- });
570
+ const field = fieldLabel.replace(/'/g, "\\'");
571
+ const val = (dateString || '').replace(/'/g, "\\'");
572
+
573
+ const result = await this._evalWithFormHelper(frame, `() => {
574
+ var item = _findFormItem('${field}');
550
575
  if (!item) return { found: false };
551
- const picker = item.querySelector(".ant-picker");
552
- const input = picker?.querySelector("input");
576
+ var picker = item.querySelector(".ant-picker");
577
+ var input = picker && picker.querySelector("input");
553
578
  if (!input) return { found: false };
554
579
 
555
580
  input.scrollIntoView({ block: "center" });
556
581
  input.focus();
557
- const setter = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(input), "value")?.set;
558
- if (setter) setter.call(input, value);
559
- else input.value = value;
582
+ var setter = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(input), "value") && Object.getOwnPropertyDescriptor(Object.getPrototypeOf(input), "value").set;
583
+ if (setter) setter.call(input, '${val}');
584
+ else input.value = '${val}';
560
585
  input.dispatchEvent(new Event("input", { bubbles: true }));
561
586
  input.dispatchEvent(new Event("change", { bubbles: true }));
562
- // 回车确认
563
587
  input.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", code: "Enter", keyCode: 13, which: 13, bubbles: true }));
564
588
  input.blur();
565
589
  return { found: true, value: input.value };
566
- }, { field: fieldLabel, value: dateString });
590
+ }`);
567
591
  await this.closeAllDropdowns(frame);
568
592
  return { acted: result.found, field: fieldLabel, value: result.value, verified: result.value === dateString };
569
593
  }
@@ -596,7 +620,7 @@ export class AntAdapter {
596
620
  const seg = segments[i];
597
621
  const result = await pollEval(frame, (text) => {
598
622
  const all = Array.from(document.querySelectorAll(
599
- "body > .ant-cascader-dropdown:not(.ant-cascader-dropdown-hidden) .ant-cascader-menu-item"
623
+ ".ant-cascader-dropdown:not(.ant-cascader-dropdown-hidden) .ant-cascader-menu-item"
600
624
  ));
601
625
  const visible = all.filter((el) => {
602
626
  const r = el.getBoundingClientRect();
@@ -625,21 +649,16 @@ export class AntAdapter {
625
649
  * Upload 文件上传
626
650
  */
627
651
  async upload(frame, fieldLabel, filePath) {
628
- // 注意:puppeteer 上传文件需要通过 ElementHandle.uploadFile,evaluate 内做不到
629
- // 这里只能返回需要上层处理的标志
630
- const result = await frame.evaluate((field) => {
631
- const items = Array.from(document.querySelectorAll(".ant-form-item"));
632
- const item = items.find((x) => {
633
- const lbl = x.querySelector(".ant-form-item-label");
634
- return lbl && (lbl.innerText || lbl.textContent || "").trim().includes(field);
635
- });
652
+ const field = fieldLabel.replace(/'/g, "\\'");
653
+ const result = await this._evalWithFormHelper(frame, `() => {
654
+ var item = _findFormItem('${field}');
636
655
  if (!item) return { found: false };
637
- const input = item.querySelector("input[type='file']");
656
+ var input = item.querySelector("input[type='file']");
638
657
  if (!input) return { found: false };
639
- const marker = `__sz_upload_${Date.now()}`;
658
+ var marker = '__sz_upload_' + Date.now();
640
659
  input.setAttribute("data-sz-marker", marker);
641
- return { found: true, marker };
642
- }, fieldLabel);
660
+ return { found: true, marker: marker };
661
+ }`);
643
662
 
644
663
  if (!result.found) return { acted: false, reason: "upload input not found" };
645
664