@qing3a/flow-rpa-engine 0.1.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.
- package/dist/atomic.d.ts +9 -0
- package/dist/atomic.js +32 -0
- package/dist/atomic.js.map +1 -0
- package/dist/bench.d.ts +57 -0
- package/dist/bench.js +112 -0
- package/dist/bench.js.map +1 -0
- package/dist/cdp/client.d.ts +70 -0
- package/dist/cdp/client.js +272 -0
- package/dist/cdp/client.js.map +1 -0
- package/dist/cdp/page.d.ts +93 -0
- package/dist/cdp/page.js +411 -0
- package/dist/cdp/page.js.map +1 -0
- package/dist/executor.d.ts +122 -0
- package/dist/executor.js +399 -0
- package/dist/executor.js.map +1 -0
- package/dist/export.d.ts +128 -0
- package/dist/export.js +234 -0
- package/dist/export.js.map +1 -0
- package/dist/gate.d.ts +56 -0
- package/dist/gate.js +144 -0
- package/dist/gate.js.map +1 -0
- package/dist/hello.d.ts +5 -0
- package/dist/hello.js +13 -0
- package/dist/hello.js.map +1 -0
- package/dist/human.d.ts +162 -0
- package/dist/human.js +506 -0
- package/dist/human.js.map +1 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -0
- package/dist/learning.d.ts +82 -0
- package/dist/learning.js +240 -0
- package/dist/learning.js.map +1 -0
- package/dist/mask.d.ts +32 -0
- package/dist/mask.js +70 -0
- package/dist/mask.js.map +1 -0
- package/dist/platform.d.ts +66 -0
- package/dist/platform.js +215 -0
- package/dist/platform.js.map +1 -0
- package/dist/record.d.ts +48 -0
- package/dist/record.js +42 -0
- package/dist/record.js.map +1 -0
- package/dist/schema.d.ts +130 -0
- package/dist/schema.js +93 -0
- package/dist/schema.js.map +1 -0
- package/dist/simprofile.d.ts +94 -0
- package/dist/simprofile.js +247 -0
- package/dist/simprofile.js.map +1 -0
- package/dist/storage.d.ts +63 -0
- package/dist/storage.js +138 -0
- package/dist/storage.js.map +1 -0
- package/dist/validate.d.ts +37 -0
- package/dist/validate.js +108 -0
- package/dist/validate.js.map +1 -0
- package/package.json +32 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { type ExtractResult, type PageOps } from '../executor.js';
|
|
2
|
+
import type { SimProfile } from '../simprofile.js';
|
|
3
|
+
import type { DomSnapshot } from '../export.js';
|
|
4
|
+
/**
|
|
5
|
+
* CDP PageOps 适配层 —— executor 的 PageOps 接真实浏览器(移植自 Rust `cdp_page.rs`)。
|
|
6
|
+
*
|
|
7
|
+
* 组合:
|
|
8
|
+
* - CdpClient:CDP 命令(导航 / 页面求值)
|
|
9
|
+
* - Human(拟人层):点击 / 输入走拟人事件序列(轨迹 + 节奏)
|
|
10
|
+
* - window.__rpaDom DOM 辅助:选择器定位(CSS 或 text:xxx)、滚动、取文本、聚焦、
|
|
11
|
+
* 页面文本、编号元素 —— 页面侧只暴露「文本 + 编号元素」口径
|
|
12
|
+
*
|
|
13
|
+
* A1 ENV_CHECK(EXECUTION-SPEC 七):flow.json 顶层 envCheck 配置(verifyUrls/dialogs),
|
|
14
|
+
* 每步执行前注入检测,命中 → 执行树级中止(ExecError verify 类别 + category='env_check')。
|
|
15
|
+
* A4 canvas 提取:extract 可选 params.mode='canvas' → 注入 canvas 文本提取脚本。
|
|
16
|
+
* T1 simcheck:模拟质量评分(验收工具,不进生产链路)。
|
|
17
|
+
*/
|
|
18
|
+
/** 页面内 DOM 辅助(window.__rpaDom) */
|
|
19
|
+
export declare const DOM_SOURCE = "\nwindow.__rpaDom = (function() {\n function isVisible(el) {\n if (!el || el.nodeType !== 1) return false;\n const s = window.getComputedStyle(el);\n if (s.display === 'none' || s.visibility === 'hidden' || parseFloat(s.opacity) === 0) return false;\n const r = el.getBoundingClientRect();\n return r.width > 0 && r.height > 0;\n }\n function byText(text) {\n const needle = String(text).trim();\n const visible = Array.from(document.querySelectorAll('body *')).filter(isVisible);\n const leaf = visible.filter(el => el.children.length === 0);\n const exact = leaf.filter(el => (el.textContent || '').trim() === needle);\n if (exact.length > 0) return exact;\n return leaf.filter(el => (el.textContent || '').includes(needle));\n }\n function getBySels(selector) {\n const sel = String(selector);\n if (sel.startsWith('text:')) return byText(sel.slice(5));\n try {\n return Array.from(document.querySelectorAll(sel)).filter(isVisible);\n } catch (e) {\n return [];\n }\n }\n function pick(selector, index) {\n const nodes = getBySels(selector);\n return nodes[index] || nodes[0] || null;\n }\n function scrollIntoView(selector, index) {\n const el = pick(selector, index);\n if (!el) return false;\n el.scrollIntoView({ block: 'center', inline: 'center' });\n return true;\n }\n function textOf(selector, index) {\n const el = pick(selector, index);\n return el ? (el.innerText || el.textContent || '') : '';\n }\n function focusInput(selector, index) {\n const el = pick(selector, index);\n if (!el) return false;\n if (typeof el.select === 'function') el.select();\n el.focus();\n return true;\n }\n function pageText() {\n let t = document.body ? document.body.innerText : '';\n const inputs = Array.from(document.querySelectorAll('input, textarea, [contenteditable=\"true\"]'));\n for (const el of inputs) {\n const v = el.value || el.textContent || '';\n if (v) t += '\\n' + v;\n }\n return t;\n }\n function numberedElements(maxN) {\n const tags = 'a,button,input,select,textarea,[role=\"button\"],[role=\"link\"],[contenteditable=\"true\"]';\n const nodes = Array.from(document.querySelectorAll(tags)).filter(isVisible);\n const cap = maxN || 50;\n return nodes.slice(0, cap).map(el => ({\n tag: el.tagName.toLowerCase(),\n text: (el.innerText || el.value || el.textContent || '').trim().slice(0, 40)\n }));\n }\n return { getBySels, scrollIntoView, textOf, focusInput, pageText, numberedElements };\n})();\ntrue\n";
|
|
20
|
+
/** ENV_CHECK 检测注入脚本(A1;页面端一次 evaluate 返回全部信号) */
|
|
21
|
+
export declare function envCheckSource(envCheck: EnvCheckConfig): string;
|
|
22
|
+
/** ENV_CHECK 配置(flow.json 顶层可选字段;平台可配置,引擎不硬编码) */
|
|
23
|
+
export interface EnvCheckConfig {
|
|
24
|
+
verifyUrls?: string[];
|
|
25
|
+
dialogs?: Array<{
|
|
26
|
+
selector: string;
|
|
27
|
+
closeSelectors?: string[];
|
|
28
|
+
}>;
|
|
29
|
+
}
|
|
30
|
+
/** ENV_CHECK 检测结果 */
|
|
31
|
+
export interface EnvCheckSignal {
|
|
32
|
+
urlHit: boolean;
|
|
33
|
+
dialog: {
|
|
34
|
+
selector: string;
|
|
35
|
+
hasClose: boolean;
|
|
36
|
+
} | null;
|
|
37
|
+
}
|
|
38
|
+
/** 真实浏览器页面驱动:实现 executor 的 PageOps */
|
|
39
|
+
export declare class CdpPage implements PageOps {
|
|
40
|
+
private readonly port;
|
|
41
|
+
private readonly sim;
|
|
42
|
+
private targetId?;
|
|
43
|
+
private human?;
|
|
44
|
+
/** ENV_CHECK 配置(flow.json 顶层 envCheck;缺省不检测) */
|
|
45
|
+
private envCheck?;
|
|
46
|
+
constructor(port: number, sim: SimProfile);
|
|
47
|
+
/** 设置 ENV_CHECK 配置(A1;流程加载时注入) */
|
|
48
|
+
setEnvCheck(config: EnvCheckConfig | undefined): void;
|
|
49
|
+
/** 确保浏览器调试实例在跑(端口不可连则拉起 Edge;edgePath 空则直接报错) */
|
|
50
|
+
ensureBrowser(edgePath: string, userDataDir: string): Promise<void>;
|
|
51
|
+
/** 打开新标签页并导航到 url(绑定拟人层;等待页面就绪) */
|
|
52
|
+
private open;
|
|
53
|
+
/** 当前标签页的 CDP 客户端(human 持有的 sink 中的 client) */
|
|
54
|
+
private currentClient;
|
|
55
|
+
/** 注入 DOM 辅助(幂等:页面已有 __rpaDom 则跳过) */
|
|
56
|
+
private ensureDom;
|
|
57
|
+
/** JS 字符串字面量(selector 插值安全) */
|
|
58
|
+
private jsStr;
|
|
59
|
+
/** ENV_CHECK 检测(A1 D-V2:每步执行前;命中 → 抛 verify 类别错误 + category=env_check) */
|
|
60
|
+
private checkEnv;
|
|
61
|
+
/** 观察通道 DOM 快照(契约口径:文本快照摘要 + 编号元素) */
|
|
62
|
+
snapshotDom(): Promise<DomSnapshot>;
|
|
63
|
+
navigate(url: string): Promise<void>;
|
|
64
|
+
click(selector: string, index: number): Promise<void>;
|
|
65
|
+
input(selector: string, value: string): Promise<void>;
|
|
66
|
+
/** A4 canvas 提取:注入 canvas 文本提取脚本(fillText 劫持 + 重组) */
|
|
67
|
+
private extractCanvas;
|
|
68
|
+
extract(selector: string): Promise<ExtractResult>;
|
|
69
|
+
/** canvas 提取入口(executor extract 的 mode='canvas' 分支调用) */
|
|
70
|
+
extractMode(selector: string, mode: string | undefined): Promise<ExtractResult>;
|
|
71
|
+
nextPage(): Promise<boolean>;
|
|
72
|
+
currentUrl(): Promise<string>;
|
|
73
|
+
pageText(): Promise<string>;
|
|
74
|
+
waitSelector(selector: string, timeoutMs: number): Promise<void>;
|
|
75
|
+
waitMs(ms: number): Promise<void>;
|
|
76
|
+
finalize(): Promise<void>;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* T1 simcheck:模拟质量评分(事件真实性 45 + 轨迹自然度 30 + 时序 15 + 行为丰富度 10)。
|
|
80
|
+
* 只作开发期验收工具,不进生产链路、不暴露 MCP 工具。
|
|
81
|
+
*/
|
|
82
|
+
export interface SimCheckResult {
|
|
83
|
+
total: number;
|
|
84
|
+
eventReality: number;
|
|
85
|
+
trajectoryNaturalness: number;
|
|
86
|
+
timing: number;
|
|
87
|
+
behaviorRichness: number;
|
|
88
|
+
passed: boolean;
|
|
89
|
+
}
|
|
90
|
+
export declare function simcheckScore(events: Array<{
|
|
91
|
+
type: string;
|
|
92
|
+
button?: string;
|
|
93
|
+
}>): SimCheckResult;
|
package/dist/cdp/page.js
ADDED
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
import { ExecError as ExecErr } from '../executor.js';
|
|
2
|
+
import { Human } from '../human.js';
|
|
3
|
+
import { snapshotFromWeb } from '../export.js';
|
|
4
|
+
import { CdpClient, CdpInputSink, closeTarget, listTargets, openTarget } from './client.js';
|
|
5
|
+
/**
|
|
6
|
+
* CDP PageOps 适配层 —— executor 的 PageOps 接真实浏览器(移植自 Rust `cdp_page.rs`)。
|
|
7
|
+
*
|
|
8
|
+
* 组合:
|
|
9
|
+
* - CdpClient:CDP 命令(导航 / 页面求值)
|
|
10
|
+
* - Human(拟人层):点击 / 输入走拟人事件序列(轨迹 + 节奏)
|
|
11
|
+
* - window.__rpaDom DOM 辅助:选择器定位(CSS 或 text:xxx)、滚动、取文本、聚焦、
|
|
12
|
+
* 页面文本、编号元素 —— 页面侧只暴露「文本 + 编号元素」口径
|
|
13
|
+
*
|
|
14
|
+
* A1 ENV_CHECK(EXECUTION-SPEC 七):flow.json 顶层 envCheck 配置(verifyUrls/dialogs),
|
|
15
|
+
* 每步执行前注入检测,命中 → 执行树级中止(ExecError verify 类别 + category='env_check')。
|
|
16
|
+
* A4 canvas 提取:extract 可选 params.mode='canvas' → 注入 canvas 文本提取脚本。
|
|
17
|
+
* T1 simcheck:模拟质量评分(验收工具,不进生产链路)。
|
|
18
|
+
*/
|
|
19
|
+
/** 页面内 DOM 辅助(window.__rpaDom) */
|
|
20
|
+
export const DOM_SOURCE = `
|
|
21
|
+
window.__rpaDom = (function() {
|
|
22
|
+
function isVisible(el) {
|
|
23
|
+
if (!el || el.nodeType !== 1) return false;
|
|
24
|
+
const s = window.getComputedStyle(el);
|
|
25
|
+
if (s.display === 'none' || s.visibility === 'hidden' || parseFloat(s.opacity) === 0) return false;
|
|
26
|
+
const r = el.getBoundingClientRect();
|
|
27
|
+
return r.width > 0 && r.height > 0;
|
|
28
|
+
}
|
|
29
|
+
function byText(text) {
|
|
30
|
+
const needle = String(text).trim();
|
|
31
|
+
const visible = Array.from(document.querySelectorAll('body *')).filter(isVisible);
|
|
32
|
+
const leaf = visible.filter(el => el.children.length === 0);
|
|
33
|
+
const exact = leaf.filter(el => (el.textContent || '').trim() === needle);
|
|
34
|
+
if (exact.length > 0) return exact;
|
|
35
|
+
return leaf.filter(el => (el.textContent || '').includes(needle));
|
|
36
|
+
}
|
|
37
|
+
function getBySels(selector) {
|
|
38
|
+
const sel = String(selector);
|
|
39
|
+
if (sel.startsWith('text:')) return byText(sel.slice(5));
|
|
40
|
+
try {
|
|
41
|
+
return Array.from(document.querySelectorAll(sel)).filter(isVisible);
|
|
42
|
+
} catch (e) {
|
|
43
|
+
return [];
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function pick(selector, index) {
|
|
47
|
+
const nodes = getBySels(selector);
|
|
48
|
+
return nodes[index] || nodes[0] || null;
|
|
49
|
+
}
|
|
50
|
+
function scrollIntoView(selector, index) {
|
|
51
|
+
const el = pick(selector, index);
|
|
52
|
+
if (!el) return false;
|
|
53
|
+
el.scrollIntoView({ block: 'center', inline: 'center' });
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
function textOf(selector, index) {
|
|
57
|
+
const el = pick(selector, index);
|
|
58
|
+
return el ? (el.innerText || el.textContent || '') : '';
|
|
59
|
+
}
|
|
60
|
+
function focusInput(selector, index) {
|
|
61
|
+
const el = pick(selector, index);
|
|
62
|
+
if (!el) return false;
|
|
63
|
+
if (typeof el.select === 'function') el.select();
|
|
64
|
+
el.focus();
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
function pageText() {
|
|
68
|
+
let t = document.body ? document.body.innerText : '';
|
|
69
|
+
const inputs = Array.from(document.querySelectorAll('input, textarea, [contenteditable="true"]'));
|
|
70
|
+
for (const el of inputs) {
|
|
71
|
+
const v = el.value || el.textContent || '';
|
|
72
|
+
if (v) t += '\\n' + v;
|
|
73
|
+
}
|
|
74
|
+
return t;
|
|
75
|
+
}
|
|
76
|
+
function numberedElements(maxN) {
|
|
77
|
+
const tags = 'a,button,input,select,textarea,[role="button"],[role="link"],[contenteditable="true"]';
|
|
78
|
+
const nodes = Array.from(document.querySelectorAll(tags)).filter(isVisible);
|
|
79
|
+
const cap = maxN || 50;
|
|
80
|
+
return nodes.slice(0, cap).map(el => ({
|
|
81
|
+
tag: el.tagName.toLowerCase(),
|
|
82
|
+
text: (el.innerText || el.value || el.textContent || '').trim().slice(0, 40)
|
|
83
|
+
}));
|
|
84
|
+
}
|
|
85
|
+
return { getBySels, scrollIntoView, textOf, focusInput, pageText, numberedElements };
|
|
86
|
+
})();
|
|
87
|
+
true
|
|
88
|
+
`;
|
|
89
|
+
/** ENV_CHECK 检测注入脚本(A1;页面端一次 evaluate 返回全部信号) */
|
|
90
|
+
export function envCheckSource(envCheck) {
|
|
91
|
+
const verifyUrls = (envCheck.verifyUrls ?? []).map((u) => JSON.stringify(u));
|
|
92
|
+
const dialogs = (envCheck.dialogs ?? []).map((d) => ({
|
|
93
|
+
selector: d.selector,
|
|
94
|
+
closeSelectors: d.closeSelectors ?? [],
|
|
95
|
+
}));
|
|
96
|
+
const dialogsJson = JSON.stringify(dialogs);
|
|
97
|
+
return `(() => {
|
|
98
|
+
const url = location.href;
|
|
99
|
+
const verifyUrls = [${verifyUrls.join(',')}];
|
|
100
|
+
const dialogs = ${dialogsJson};
|
|
101
|
+
const signal = { urlHit: false, dialog: null };
|
|
102
|
+
if (verifyUrls.some(u => url.includes(u))) signal.urlHit = true;
|
|
103
|
+
for (const d of dialogs) {
|
|
104
|
+
const el = document.querySelector(d.selector);
|
|
105
|
+
if (el) {
|
|
106
|
+
const closeSel = d.closeSelectors && d.closeSelectors.length > 0 ? d.closeSelectors[0] : null;
|
|
107
|
+
signal.dialog = { selector: d.selector, hasClose: !!closeSel };
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return signal;
|
|
112
|
+
})()`;
|
|
113
|
+
}
|
|
114
|
+
/** 标签页初始化等页面就绪的超时(open 时等待 readyState=complete) */
|
|
115
|
+
const OPEN_READY_TIMEOUT_MS = 30_000;
|
|
116
|
+
function pageErr(e) {
|
|
117
|
+
return ExecErr.page(e instanceof Error ? e.message : String(e));
|
|
118
|
+
}
|
|
119
|
+
/** 真实浏览器页面驱动:实现 executor 的 PageOps */
|
|
120
|
+
export class CdpPage {
|
|
121
|
+
port;
|
|
122
|
+
sim;
|
|
123
|
+
targetId;
|
|
124
|
+
human;
|
|
125
|
+
/** ENV_CHECK 配置(flow.json 顶层 envCheck;缺省不检测) */
|
|
126
|
+
envCheck;
|
|
127
|
+
constructor(port, sim) {
|
|
128
|
+
this.port = port;
|
|
129
|
+
this.sim = sim;
|
|
130
|
+
}
|
|
131
|
+
/** 设置 ENV_CHECK 配置(A1;流程加载时注入) */
|
|
132
|
+
setEnvCheck(config) {
|
|
133
|
+
this.envCheck = config;
|
|
134
|
+
}
|
|
135
|
+
/** 确保浏览器调试实例在跑(端口不可连则拉起 Edge;edgePath 空则直接报错) */
|
|
136
|
+
async ensureBrowser(edgePath, userDataDir) {
|
|
137
|
+
// 检查调试端口是否可连(/json/list);不可连且给了 edgePath → 拉起 Edge
|
|
138
|
+
try {
|
|
139
|
+
await listTargets(this.port);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
// 端口不可连
|
|
144
|
+
}
|
|
145
|
+
if (!edgePath) {
|
|
146
|
+
throw ExecErr.page('浏览器未就绪:调试端口不可连且未配置 Edge 路径');
|
|
147
|
+
}
|
|
148
|
+
const { spawn } = await import('node:child_process');
|
|
149
|
+
spawn(edgePath, [
|
|
150
|
+
`--remote-debugging-port=${this.port}`,
|
|
151
|
+
`--user-data-dir=${userDataDir}`,
|
|
152
|
+
'--no-first-run',
|
|
153
|
+
'--no-default-browser-check',
|
|
154
|
+
'about:blank',
|
|
155
|
+
], { detached: true, stdio: 'ignore' }).unref();
|
|
156
|
+
// 轮询等调试端口就绪(Edge 冷启动 ~3-5s)
|
|
157
|
+
for (let i = 0; i < 16; i++) {
|
|
158
|
+
await sleep(500);
|
|
159
|
+
try {
|
|
160
|
+
await listTargets(this.port);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
// 继续等
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
throw ExecErr.page('浏览器拉起后调试端口未就绪');
|
|
168
|
+
}
|
|
169
|
+
/** 打开新标签页并导航到 url(绑定拟人层;等待页面就绪) */
|
|
170
|
+
async open(url) {
|
|
171
|
+
const t = await openTarget(url, this.port);
|
|
172
|
+
const client = await CdpClient.connect(t.wsUrl);
|
|
173
|
+
// 等待页面 readyState=complete(不强制成功,verify/wait_selector 兜底)
|
|
174
|
+
const deadline = Date.now() + OPEN_READY_TIMEOUT_MS;
|
|
175
|
+
for (;;) {
|
|
176
|
+
const state = await client.evaluate('document.readyState').catch(() => '');
|
|
177
|
+
if (state === 'complete' || Date.now() >= deadline)
|
|
178
|
+
break;
|
|
179
|
+
await sleep(200);
|
|
180
|
+
}
|
|
181
|
+
this.targetId = t.id;
|
|
182
|
+
this.human = new Human(new CdpInputSink(client), this.sim);
|
|
183
|
+
}
|
|
184
|
+
/** 当前标签页的 CDP 客户端(human 持有的 sink 中的 client) */
|
|
185
|
+
currentClient() {
|
|
186
|
+
const h = this.human;
|
|
187
|
+
if (!h)
|
|
188
|
+
throw ExecErr.page('无活动标签页');
|
|
189
|
+
return h.sink.client;
|
|
190
|
+
}
|
|
191
|
+
/** 注入 DOM 辅助(幂等:页面已有 __rpaDom 则跳过) */
|
|
192
|
+
async ensureDom() {
|
|
193
|
+
const client = this.currentClient();
|
|
194
|
+
const has = await client.evaluate('!!window.__rpaDom').catch(() => false);
|
|
195
|
+
if (has)
|
|
196
|
+
return;
|
|
197
|
+
await client.evaluate(DOM_SOURCE);
|
|
198
|
+
}
|
|
199
|
+
/** JS 字符串字面量(selector 插值安全) */
|
|
200
|
+
jsStr(s) {
|
|
201
|
+
return JSON.stringify(s);
|
|
202
|
+
}
|
|
203
|
+
/** ENV_CHECK 检测(A1 D-V2:每步执行前;命中 → 抛 verify 类别错误 + category=env_check) */
|
|
204
|
+
async checkEnv() {
|
|
205
|
+
if (!this.envCheck)
|
|
206
|
+
return;
|
|
207
|
+
const client = this.currentClient();
|
|
208
|
+
let signal;
|
|
209
|
+
try {
|
|
210
|
+
signal = (await client.evaluate(envCheckSource(this.envCheck)));
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
return; // 注入失败不阻断(无 envCheck 时行为不变)
|
|
214
|
+
}
|
|
215
|
+
if (signal?.urlHit || signal?.dialog) {
|
|
216
|
+
const detail = signal.urlHit ? '环境风险: 命中验证 URL' : `环境风险: 检测到风险弹窗 (${signal.dialog?.selector})`;
|
|
217
|
+
const err = ExecErr.verify('env_check', detail, 'env_check');
|
|
218
|
+
throw err;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
/** 观察通道 DOM 快照(契约口径:文本快照摘要 + 编号元素) */
|
|
222
|
+
async snapshotDom() {
|
|
223
|
+
await this.ensureDom();
|
|
224
|
+
const client = this.currentClient();
|
|
225
|
+
const text = String(await client.evaluate('window.__rpaDom.pageText()').catch(() => ''));
|
|
226
|
+
const elsRaw = (await client.evaluate('window.__rpaDom.numberedElements(50)').catch(() => []));
|
|
227
|
+
const els = Array.isArray(elsRaw)
|
|
228
|
+
? elsRaw
|
|
229
|
+
.filter((e) => !!e && typeof e === 'object')
|
|
230
|
+
.map((e) => [String(e.tag), String(e.text)])
|
|
231
|
+
: [];
|
|
232
|
+
return snapshotFromWeb(text, els);
|
|
233
|
+
}
|
|
234
|
+
// ---- PageOps ----
|
|
235
|
+
async navigate(url) {
|
|
236
|
+
if (!this.targetId) {
|
|
237
|
+
await this.open(url);
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
const client = this.currentClient();
|
|
241
|
+
await client.send('Page.navigate', { url });
|
|
242
|
+
}
|
|
243
|
+
async click(selector, index) {
|
|
244
|
+
const human = this.human;
|
|
245
|
+
if (!human)
|
|
246
|
+
throw ExecErr.page('无活动标签页');
|
|
247
|
+
await this.ensureDom();
|
|
248
|
+
await this.checkEnv();
|
|
249
|
+
const client = this.currentClient();
|
|
250
|
+
// 滚动到目标可见
|
|
251
|
+
await client.evaluate(`window.__rpaDom.scrollIntoView(${this.jsStr(selector)}, ${index})`);
|
|
252
|
+
// 拟人点击:randomClickIn 在元素可见矩形内随机取点,1 次
|
|
253
|
+
const [clicked] = await human.randomClickIn(selector, 1, 500);
|
|
254
|
+
if (clicked === 0) {
|
|
255
|
+
throw ExecErr.page(`点击目标不存在: ${selector}`);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
async input(selector, value) {
|
|
259
|
+
const human = this.human;
|
|
260
|
+
if (!human)
|
|
261
|
+
throw ExecErr.page('无活动标签页');
|
|
262
|
+
await this.ensureDom();
|
|
263
|
+
await this.checkEnv();
|
|
264
|
+
const client = this.currentClient();
|
|
265
|
+
// 滚动到可见 + 聚焦 + 全选(全选使键盘输入替换已有内容)
|
|
266
|
+
await client.evaluate(`window.__rpaDom.scrollIntoView(${this.jsStr(selector)}, 0) && window.__rpaDom.focusInput(${this.jsStr(selector)}, 0)`);
|
|
267
|
+
await human.keyboardInput(value, {});
|
|
268
|
+
}
|
|
269
|
+
/** A4 canvas 提取:注入 canvas 文本提取脚本(fillText 劫持 + 重组) */
|
|
270
|
+
async extractCanvas(selector) {
|
|
271
|
+
const client = this.currentClient();
|
|
272
|
+
const CANVAS_EXTRACT = `(() => {
|
|
273
|
+
// Proxy 沙箱 + fillText 劫持:捕获 canvas 渲染文本
|
|
274
|
+
const texts = [];
|
|
275
|
+
const origFillText = CanvasRenderingContext2D.prototype.fillText;
|
|
276
|
+
CanvasRenderingContext2D.prototype.fillText = function(text, x, y) {
|
|
277
|
+
texts.push({ text: String(text), x: x, y: y });
|
|
278
|
+
return origFillText.apply(this, arguments);
|
|
279
|
+
};
|
|
280
|
+
// 触发目标 canvas 重绘(如已渲染则直接读取)
|
|
281
|
+
const canvas = document.querySelector(${this.jsStr(selector)});
|
|
282
|
+
if (canvas && canvas.getContext) {
|
|
283
|
+
try { canvas.getContext('2d'); } catch (e) {}
|
|
284
|
+
}
|
|
285
|
+
// 重组:按 y 分行、x 排序(简化:按捕获顺序拼接)
|
|
286
|
+
texts.sort((a, b) => a.y - b.y || a.x - b.x);
|
|
287
|
+
const lines = [];
|
|
288
|
+
let curY = null, cur = [];
|
|
289
|
+
for (const t of texts) {
|
|
290
|
+
if (curY === null || Math.abs(t.y - curY) <= 2) { cur.push(t.text); curY = t.y; }
|
|
291
|
+
else { lines.push(cur.join(' ')); cur = [t.text]; curY = t.y; }
|
|
292
|
+
}
|
|
293
|
+
if (cur.length) lines.push(cur.join(' '));
|
|
294
|
+
return lines.join('\\n');
|
|
295
|
+
})()`;
|
|
296
|
+
const content = String(await client.evaluate(CANVAS_EXTRACT).catch(() => ''));
|
|
297
|
+
return { content, count: content.trim() === '' ? 0 : 1 };
|
|
298
|
+
}
|
|
299
|
+
async extract(selector) {
|
|
300
|
+
await this.ensureDom();
|
|
301
|
+
await this.checkEnv();
|
|
302
|
+
const client = this.currentClient();
|
|
303
|
+
const content = String(await client.evaluate(`window.__rpaDom.textOf(${this.jsStr(selector)}, 0) || ''`));
|
|
304
|
+
return { content, count: content.trim() === '' ? 0 : 1 };
|
|
305
|
+
}
|
|
306
|
+
/** canvas 提取入口(executor extract 的 mode='canvas' 分支调用) */
|
|
307
|
+
async extractMode(selector, mode) {
|
|
308
|
+
// 待真实 canvas 场景接入 PageOps 接口:executor 暂不读 params.mode,extractMode 为
|
|
309
|
+
// 独立入口(A4 canvas 提取后端已就绪,接入点留到真实 canvas 页面流程出现时)
|
|
310
|
+
if (mode === 'canvas')
|
|
311
|
+
return this.extractCanvas(selector);
|
|
312
|
+
return this.extract(selector);
|
|
313
|
+
}
|
|
314
|
+
async nextPage() {
|
|
315
|
+
// 依次尝试常见「下一页」定位:文本优先,CSS 兜底
|
|
316
|
+
const candidates = [
|
|
317
|
+
'text:下一页',
|
|
318
|
+
'text:下一页 >',
|
|
319
|
+
'.next-page, .btn-next, .pagination-next, .page-next',
|
|
320
|
+
"a[aria-label*='下一页']",
|
|
321
|
+
];
|
|
322
|
+
const human = this.human;
|
|
323
|
+
if (!human)
|
|
324
|
+
return false;
|
|
325
|
+
await this.ensureDom();
|
|
326
|
+
const client = this.currentClient();
|
|
327
|
+
for (const sel of candidates) {
|
|
328
|
+
const ok = await client
|
|
329
|
+
.evaluate(`window.__rpaDom.getBySels(${this.jsStr(sel)}).length > 0`)
|
|
330
|
+
.catch(() => false);
|
|
331
|
+
if (!ok)
|
|
332
|
+
continue;
|
|
333
|
+
const [clicked] = await human.randomClickIn(sel, 1, 500);
|
|
334
|
+
if (clicked > 0)
|
|
335
|
+
return true;
|
|
336
|
+
}
|
|
337
|
+
return false;
|
|
338
|
+
}
|
|
339
|
+
async currentUrl() {
|
|
340
|
+
if (!this.human)
|
|
341
|
+
return '';
|
|
342
|
+
return String(await this.currentClient().evaluate('location.href').catch(() => ''));
|
|
343
|
+
}
|
|
344
|
+
async pageText() {
|
|
345
|
+
if (!this.human)
|
|
346
|
+
return '';
|
|
347
|
+
await this.ensureDom().catch(() => undefined);
|
|
348
|
+
return String(await this.currentClient().evaluate("window.__rpaDom ? window.__rpaDom.pageText() : (document.body ? document.body.innerText : '')").catch(() => ''));
|
|
349
|
+
}
|
|
350
|
+
async waitSelector(selector, timeoutMs) {
|
|
351
|
+
const client = this.currentClient();
|
|
352
|
+
await this.ensureDom();
|
|
353
|
+
const expr = `window.__rpaDom.getBySels(${this.jsStr(selector)}).length > 0`;
|
|
354
|
+
const deadline = Date.now() + Math.max(1, timeoutMs);
|
|
355
|
+
for (;;) {
|
|
356
|
+
const found = await client.evaluate(expr).catch(() => false);
|
|
357
|
+
if (found)
|
|
358
|
+
return;
|
|
359
|
+
if (Date.now() >= deadline) {
|
|
360
|
+
throw ExecErr.page(`等待选择器 ${selector} 超时`);
|
|
361
|
+
}
|
|
362
|
+
await sleep(200);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
async waitMs(ms) {
|
|
366
|
+
await sleep(ms);
|
|
367
|
+
}
|
|
368
|
+
async finalize() {
|
|
369
|
+
const tid = this.targetId;
|
|
370
|
+
if (!tid)
|
|
371
|
+
return;
|
|
372
|
+
this.targetId = undefined;
|
|
373
|
+
this.human = undefined;
|
|
374
|
+
await closeTarget(tid, this.port).catch(() => undefined);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
378
|
+
export function simcheckScore(events) {
|
|
379
|
+
// 事件真实性(45):鼠标事件应含 pressed/released(真实按下有释放);有 char 输入事件加分
|
|
380
|
+
let eventReality = 0;
|
|
381
|
+
const types = events.map((e) => e.type);
|
|
382
|
+
if (types.includes('mousePressed') && types.includes('mouseReleased'))
|
|
383
|
+
eventReality += 30;
|
|
384
|
+
if (types.includes('mouseMoved'))
|
|
385
|
+
eventReality += 15;
|
|
386
|
+
// 轨迹自然度(30):移动事件数 > 5(多步轨迹)加分;过多(>100)减分
|
|
387
|
+
const moves = types.filter((t) => t === 'mouseMoved').length;
|
|
388
|
+
let trajectoryNaturalness = 0;
|
|
389
|
+
if (moves > 5 && moves <= 100)
|
|
390
|
+
trajectoryNaturalness += 30;
|
|
391
|
+
else if (moves > 0)
|
|
392
|
+
trajectoryNaturalness += 15;
|
|
393
|
+
// 时序(15):有按下-释放间隔(无法精确测,用事件序列含 pressed+released)
|
|
394
|
+
let timing = 0;
|
|
395
|
+
if (types.includes('mousePressed') && types.includes('mouseReleased'))
|
|
396
|
+
timing += 15;
|
|
397
|
+
// 行为丰富度(10):事件类型 ≥3 种
|
|
398
|
+
let behaviorRichness = 0;
|
|
399
|
+
if (new Set(types).size >= 3)
|
|
400
|
+
behaviorRichness += 10;
|
|
401
|
+
const total = eventReality + trajectoryNaturalness + timing + behaviorRichness;
|
|
402
|
+
return {
|
|
403
|
+
total,
|
|
404
|
+
eventReality,
|
|
405
|
+
trajectoryNaturalness,
|
|
406
|
+
timing,
|
|
407
|
+
behaviorRichness,
|
|
408
|
+
passed: total >= 70,
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
//# sourceMappingURL=page.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"page.js","sourceRoot":"","sources":["../../src/cdp/page.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { type Flow } from './schema.js';
|
|
2
|
+
export type { Flow } from './schema.js';
|
|
3
|
+
/**
|
|
4
|
+
* 执行器 —— 解释执行流程定义(移植自 Rust `executor.rs`,按 EXECUTION-SPEC M4 扩展)。
|
|
5
|
+
*
|
|
6
|
+
* 设计核心:
|
|
7
|
+
* - 引擎唯一代码层,流程是数据:执行器只按 `Flow.steps` 解释执行,不含任何平台逻辑。
|
|
8
|
+
* - **页面抽象(`PageOps`,全 async)**:执行器不直接碰 CDP/DOM,通过接口驱动页面。
|
|
9
|
+
* 真实浏览器用 CDP 实现(M8),测试用 mock 实现 —— 执行逻辑可单测,引擎与浏览器控制解耦。
|
|
10
|
+
* - 验证点(verify)是成败判定的唯一依据:每步执行后按 verify 检查,失败即中止流程。
|
|
11
|
+
*
|
|
12
|
+
* EXECUTION-SPEC M4 扩展(相对 Rust 版):
|
|
13
|
+
* - **loop 循环语义(D-L1~D-L5)**:`{i}` 注入循环上下文;click 索引越界 = 列表耗尽
|
|
14
|
+
* 自然终止(非错误);maxIterations 默认 50;禁止嵌套 loop(validate 层拒绝)。
|
|
15
|
+
* - **extract field/save(D-E1~D-E3)**:field = 结构化记录标签;save:true 经 ExtractSink
|
|
16
|
+
* 落盘 `data/extracted/<runId>/<stepId>.json`(原始数据不进 run.json/观察包);
|
|
17
|
+
* extract 返回 {content, count}。
|
|
18
|
+
* - **A3 选择器链(D-S1~D-S2)**:selector 支持 string | string[],依次尝试、第一个命中生效,
|
|
19
|
+
* 记录 `selector_hit`。
|
|
20
|
+
* - **extract 有 verify 时按标准流程检查**(Rust 版提前 return 不查 verify;EXECUTION-SPEC
|
|
21
|
+
* 验收断言 3 要求 verify 生效)。
|
|
22
|
+
*/
|
|
23
|
+
export type ExecErrorKind = 'page' | 'step_action' | 'verify' | 'unknown_step';
|
|
24
|
+
/**
|
|
25
|
+
* 执行错误。`message` 为完整文案(与 Rust `ExecError::to_string()` 一致,测试断言依赖)。
|
|
26
|
+
* `category` 为失败类别(D-L2 用 `index_out_of_range`;A2 失败类型学 M6 扩展)。
|
|
27
|
+
*/
|
|
28
|
+
export declare class ExecError extends Error {
|
|
29
|
+
readonly kind: ExecErrorKind;
|
|
30
|
+
readonly detail: string;
|
|
31
|
+
readonly category: string | undefined;
|
|
32
|
+
constructor(kind: ExecErrorKind, detail: string, opts?: {
|
|
33
|
+
stepId?: string;
|
|
34
|
+
category?: string;
|
|
35
|
+
});
|
|
36
|
+
static page(msg: string, opts?: {
|
|
37
|
+
category?: string;
|
|
38
|
+
}): ExecError;
|
|
39
|
+
static stepAction(stepId: string, msg: string): ExecError;
|
|
40
|
+
static verify(stepId: string, msg: string, category?: string): ExecError;
|
|
41
|
+
static unknownStep(type: string): ExecError;
|
|
42
|
+
}
|
|
43
|
+
/** 提取结果:content=文本,count=命中元素数(D-E3;count 供循环/统计用) */
|
|
44
|
+
export interface ExtractResult {
|
|
45
|
+
content: string;
|
|
46
|
+
count: number;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* 页面抽象:执行器通过它操作页面。真实浏览器 = CDP 实现(M8);测试 = mock 实现。
|
|
50
|
+
* 全部 async(MIGRATION-NODE 四:PageOps 接口全 async)。
|
|
51
|
+
*/
|
|
52
|
+
export interface PageOps {
|
|
53
|
+
navigate(url: string): Promise<void>;
|
|
54
|
+
/**
|
|
55
|
+
* 点击元素。selector 形如 `.class` / `#id` / `text:xxx`;index 为循环内第 i 个。
|
|
56
|
+
* **越界约定(D-L2)**:index >= 列表长度时抛 ExecError,message 以「索引越界」开头,
|
|
57
|
+
* category 为 `index_out_of_range`(executor 的 loop 分支据此判定列表耗尽)。
|
|
58
|
+
*/
|
|
59
|
+
click(selector: string, index: number): Promise<void>;
|
|
60
|
+
input(selector: string, value: string): Promise<void>;
|
|
61
|
+
extract(selector: string): Promise<ExtractResult>;
|
|
62
|
+
nextPage(): Promise<boolean>;
|
|
63
|
+
currentUrl(): Promise<string>;
|
|
64
|
+
pageText(): Promise<string>;
|
|
65
|
+
waitSelector(selector: string, timeoutMs: number): Promise<void>;
|
|
66
|
+
waitMs(ms: number): Promise<void>;
|
|
67
|
+
finalize(): Promise<void>;
|
|
68
|
+
}
|
|
69
|
+
/** 提取明细落盘(D-E2):save:true 的 extract 内容写入本地,由 storage 层实现(M5) */
|
|
70
|
+
export interface ExtractSink {
|
|
71
|
+
saveExtracted(runId: string, stepId: string, field: string | undefined, content: string): void;
|
|
72
|
+
}
|
|
73
|
+
/** 验证点执行结果(run.json 的 verify 字段原料) */
|
|
74
|
+
export interface VerifyOutcome {
|
|
75
|
+
kind?: string;
|
|
76
|
+
detail?: string;
|
|
77
|
+
passed: boolean;
|
|
78
|
+
durationMs: number;
|
|
79
|
+
}
|
|
80
|
+
/** 单步执行结果(结构化:run.json 每步记录的原料;M4 扩展 field/saved/count/selectorHit) */
|
|
81
|
+
export interface StepResult {
|
|
82
|
+
stepId: string;
|
|
83
|
+
/** 步骤类型(navigate/click/...) */
|
|
84
|
+
kind?: string;
|
|
85
|
+
/** done=成功(verify 通过或无 verify);failed=失败(动作或 verify 失败) */
|
|
86
|
+
status: 'done' | 'failed';
|
|
87
|
+
error?: string;
|
|
88
|
+
/** 提取的内容(extract 步骤;save:true 时仅摘要进 run.json,原文由 sink 落盘) */
|
|
89
|
+
extracted?: string;
|
|
90
|
+
/** 提取的字段标签(D-E1:field=命名标签,candidateList/resumeContent 等) */
|
|
91
|
+
field?: string;
|
|
92
|
+
/** 是否已落盘 extracted 明细(D-E2:save:true 且 sink 存在) */
|
|
93
|
+
saved?: boolean;
|
|
94
|
+
/** 提取命中元素数(D-E3) */
|
|
95
|
+
count?: number;
|
|
96
|
+
/** 实际生效的选择器(A3 D-S2;未命中失败时记最后尝试项) */
|
|
97
|
+
selectorHit?: string;
|
|
98
|
+
verify?: VerifyOutcome;
|
|
99
|
+
durationMs: number;
|
|
100
|
+
/** input 步骤的输入值(落盘时刻已掩码,绝不含明文) */
|
|
101
|
+
inputValueMasked?: string;
|
|
102
|
+
/** 冷却锚点(D-C1:wait 步骤 coolKey;server 层据此记账 key=flow#step#coolKey) */
|
|
103
|
+
cooldownKey?: string;
|
|
104
|
+
}
|
|
105
|
+
/** 流程执行结果 */
|
|
106
|
+
export interface FlowResult {
|
|
107
|
+
flowId: string;
|
|
108
|
+
status: 'done' | 'failed';
|
|
109
|
+
error?: string;
|
|
110
|
+
steps: StepResult[];
|
|
111
|
+
}
|
|
112
|
+
export interface RunFlowOptions {
|
|
113
|
+
/** extract 明细落盘(D-E2);缺省则 save:true 不落盘但记录 saved=false */
|
|
114
|
+
sink?: ExtractSink;
|
|
115
|
+
/** 本次运行 id(供 sink 落盘路径用) */
|
|
116
|
+
runId?: string;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* 执行整个流程(失败即中止)。等价 Rust `run_flow`(全 async)。
|
|
120
|
+
* vars 接受 Map 或 Record({keyword} / {i} 等插值来源)。
|
|
121
|
+
*/
|
|
122
|
+
export declare function runFlow(flow: Flow, page: PageOps, vars: Map<string, string> | Record<string, string>, opts?: RunFlowOptions): Promise<FlowResult>;
|