mirror of
https://github.com/kuaifan/dootask.git
synced 2026-07-28 17:06:00 +00:00
页面操作(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>
76 lines
3.3 KiB
JavaScript
Vendored
76 lines
3.3 KiB
JavaScript
Vendored
/**
|
||
* 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;
|