kuaifan a3d5bdf93c feat(ai-assistant): 重构页面操作为统一执行器(Web DOM floor + Electron CDP 双后端)
页面操作(doo page → /ws → 浮窗 operation-module → action-executor)原用手写
iView-only 选择器 + 朴素 value= 赋值,在 React+Radix 同源 iframe 业务表单上几乎
填不进。重构为「复用 Playwright 注入脚本的描述层 + 按环境选后端的执行层」:

描述层(共用):
- 新增 aria/aria-bundle.js,vendoring Playwright ariaSnapshot/roleUtils/domUtils
  子集预构建为 ESM(Apache-2.0 NOTICE + 固定上游 commit),产出 YAML 快照与活的
  ref→Element Map。
- page-context-collector 改用 buildSnapshot 采集,operation-module 经 setRefMap
  以 Map 直接持有元素取代 selector 反查。

执行层(按环境选后端,失败回退):
- web-backend:原生 prototype value setter + beforeinput/input/change/blur 序列
  + 回读校验(不符报 value_not_applied,绝不假报成功);自定义下拉/日期 open→click。
- electron-backend + electron/lib/page-input.js:经 webContents.debugger 走 CDP
  Input 域产生 isTrusted=true 可信输入(insertText/dispatchMouseEvent/KeyEvent),
  任一步失败回退 Web 后端。
- 复杂控件(动态明细表/成员选择器/文件上传)如实返回 unsupported_widget。

协议与链路(doo page、active-context 失效守卫、导航类 action)保持不变。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 02:03:59 +00:00

76 lines
3.3 KiB
JavaScript
Vendored
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Electron 输入后端CDP 可信输入,渲染侧)
*
* 桌面端经 preload 暴露的 window.electron.sendAsync('pageInput', ...) 把输入意图
* 转给主进程,由 webContents.debugger 走 CDP Input 域产生 isTrusted=true 的可信输入
* (等价 Playwright 真正可靠的那一半,但第一方、在用户会话内,且能到达同源 iframe
*
* 仅覆盖最受益于可信输入的 type/click其余操作select 的 open→click、check、scroll
* 等)页面内合成事件已足够,交回 Web 后端。任一 CDP 调用失败 → 回退 Web 后端。
*
* 注意:主进程 ipcMain.handle('pageInput') 见 electron/lib/page-input.js。
*/
export function isElectronInputAvailable() {
return !!(typeof window !== 'undefined'
&& window.$A && window.$A.isElectron
&& window.electron && typeof window.electron.sendAsync === 'function');
}
function centerOf(el) {
const r = el.getBoundingClientRect();
return { x: Math.round(r.left + r.width / 2), y: Math.round(r.top + r.height / 2) };
}
// 选中元素现有内容,便于 insertText 覆盖
function selectAll(el, win) {
try {
if (typeof el.select === 'function') { el.select(); return; }
if (el.isContentEditable) {
const sel = win.getSelection && win.getSelection();
if (sel) {
const range = el.ownerDocument.createRange();
range.selectNodeContents(el);
sel.removeAllRanges();
sel.addRange(range);
}
}
} catch (e) { /* ignore */ }
}
export function createElectronBackend(getFallback) {
const cdp = (op, args) => window.electron.sendAsync('pageInput', Object.assign({ op }, args));
return {
name: 'electron',
async perform(element, action, value, ctx) {
const win = (ctx && ctx.win) || element.ownerDocument.defaultView || window;
try {
if (action === 'type' || action === 'fill' || action === 'clear') {
const v = action === 'clear' ? '' : (value == null ? '' : String(value));
if (typeof element.focus === 'function') element.focus();
selectAll(element, win);
await cdp('insertText', { text: v }); // 覆盖当前选区
const got = element.value != null ? element.value : (element.textContent || '');
if (v && got !== v && !(got || '').includes(v)) {
throw new Error('value_not_applied: 可信输入回读不一致');
}
return { success: true, action: 'type', value: v, backend: 'electron' };
}
if (action === 'click') {
const { x, y } = centerOf(element);
await cdp('click', { x, y });
return { success: true, action: 'click', backend: 'electron' };
}
} catch (e) {
// 可信输入失败 → 回退 Web 后端
return getFallback().perform(element, action, value, ctx);
}
// 其余操作页面内合成已足够,交回 Web 后端
return getFallback().perform(element, action, value, ctx);
},
};
}
export default createElectronBackend;