@qing3a/flow-rpa-engine 0.3.0 → 0.4.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/behavior.d.ts +215 -0
- package/dist/behavior.js +296 -0
- package/dist/behavior.js.map +1 -0
- package/dist/cdp/page.d.ts +28 -2
- package/dist/cdp/page.js +400 -8
- package/dist/cdp/page.js.map +1 -1
- package/dist/executor.d.ts +25 -0
- package/dist/executor.js +97 -3
- package/dist/executor.js.map +1 -1
- package/dist/export.js +6 -1
- package/dist/export.js.map +1 -1
- package/dist/gate.d.ts +9 -3
- package/dist/gate.js +19 -7
- package/dist/gate.js.map +1 -1
- package/dist/human.js +89 -24
- package/dist/human.js.map +1 -1
- package/dist/learning.d.ts +47 -3
- package/dist/learning.js +121 -8
- package/dist/learning.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 行为档案(Behavior Archive)—— 记忆层(docs/BEHAVIOR-SPACE.md)。
|
|
3
|
+
*
|
|
4
|
+
* 定位:引擎对页面交互行为规律的语义化知识库。在流程执行时旁路观察
|
|
5
|
+
* (点击/输入前后页面功能区出现/消失、延迟、文本变化),积累成语义化档案,
|
|
6
|
+
* 供 L2(learning.ts)消费,产出自动修正建议(机器安全门替代人工确认)。
|
|
7
|
+
*
|
|
8
|
+
* 数据形态(纯语义,无坐标、无原文):
|
|
9
|
+
* 每次动作一条记录,包含:动作信息(type/selectorHit)、触发前后页面状态
|
|
10
|
+
* (功能区文本哈希、URL 路由、文本变化)、结果(成败/耗时)。
|
|
11
|
+
* selector/target 文本落盘时掩码(掩码前移原则,不存敏感原文)。
|
|
12
|
+
*/
|
|
13
|
+
/** 一次动作的行为观察记录(纯语义,无坐标) */
|
|
14
|
+
export interface BehaviorEntry {
|
|
15
|
+
/** 记录类型(与 events.jsonl 的 action_footprint 同构;行为档案用独立文件) */
|
|
16
|
+
type: 'behavior_observation';
|
|
17
|
+
/** 来源流程 id(建议归属依据:行为建议挂到数据来源的 flow 下,而非当前 run 的 flow) */
|
|
18
|
+
flowId: string;
|
|
19
|
+
runId: string;
|
|
20
|
+
stepId: string;
|
|
21
|
+
/** 动作类型:click / input / navigate / extract */
|
|
22
|
+
actionType: string;
|
|
23
|
+
/** 选择器原文(掩码后;含数组链中实际命中的下标) */
|
|
24
|
+
selector: string;
|
|
25
|
+
selectorHit: string;
|
|
26
|
+
/** 动作前页面状态(语义摘要) */
|
|
27
|
+
before: {
|
|
28
|
+
/** URL 路由键(host+path,无 query——SPA query 变化不算路由跳转) */
|
|
29
|
+
route: string;
|
|
30
|
+
/** 页面文本哈希(变化检测用,不存原文) */
|
|
31
|
+
textHash: string;
|
|
32
|
+
/** 页面文本长度 */
|
|
33
|
+
textLen: number;
|
|
34
|
+
};
|
|
35
|
+
/** 动作后页面状态(语义摘要) */
|
|
36
|
+
after: {
|
|
37
|
+
route: string;
|
|
38
|
+
textHash: string;
|
|
39
|
+
textLen: number;
|
|
40
|
+
};
|
|
41
|
+
/** 前后差异(转移层,浅层 diff) */
|
|
42
|
+
delta: {
|
|
43
|
+
routeChanged: boolean;
|
|
44
|
+
textChanged: boolean;
|
|
45
|
+
textDeltaLen: number;
|
|
46
|
+
};
|
|
47
|
+
/** 动作结果 */
|
|
48
|
+
result: {
|
|
49
|
+
ok: boolean;
|
|
50
|
+
error: string;
|
|
51
|
+
durationMs: number;
|
|
52
|
+
};
|
|
53
|
+
/** 区域快照(P2:动作目标所在区域;由采集时区域识别填充) */
|
|
54
|
+
region?: RegionSnapshot;
|
|
55
|
+
at: string;
|
|
56
|
+
}
|
|
57
|
+
/** 简易文本哈希(FNV-1a 32 位;仅用于变化检测,非加密) */
|
|
58
|
+
export declare function textHash(s: string): string;
|
|
59
|
+
/** 路由键(host+path,忽略 query/hash) */
|
|
60
|
+
export declare function routeKey(u: string): string;
|
|
61
|
+
/**
|
|
62
|
+
* 区域识别规则(behavior.rules.json,随流程包分发——引擎零业务逻辑铁律)。
|
|
63
|
+
* 引擎只提供规则执行框架:加载 rules → 匹配目标元素祖先 → 打区域标签。
|
|
64
|
+
*/
|
|
65
|
+
export interface RegionRule {
|
|
66
|
+
/** 规则 id(唯一) */
|
|
67
|
+
id: string;
|
|
68
|
+
/** 匹配条件(全部满足才算命中;缺省字段不参与匹配) */
|
|
69
|
+
match: {
|
|
70
|
+
/** 区域文本包含关键词(如「职位描述」→ 详情区) */
|
|
71
|
+
textContains?: string;
|
|
72
|
+
/** 区域文本包含全部关键词(多条件精确匹配,如「职位描述」+「任职要求」才认详情区) */
|
|
73
|
+
textContainsAll?: string[];
|
|
74
|
+
/** 区域内含输入框(input/textarea)→ 搜索区等 */
|
|
75
|
+
hasInput?: boolean;
|
|
76
|
+
/** 区域内含按钮(button/a[role=button]) */
|
|
77
|
+
hasButton?: boolean;
|
|
78
|
+
/** 祖先标签(header/nav/main/aside/footer) */
|
|
79
|
+
ancestorTag?: string;
|
|
80
|
+
/** 祖先 class 包含(如卡片在 rec-job-list 内 → 列表区) */
|
|
81
|
+
ancestorClass?: string;
|
|
82
|
+
};
|
|
83
|
+
/** 命中后打的区域标签(如「详情区」/「搜索区」) */
|
|
84
|
+
label: string;
|
|
85
|
+
}
|
|
86
|
+
/** 行为规则文件(flows/<flowId>/behavior.rules.json) */
|
|
87
|
+
export interface BehaviorRules {
|
|
88
|
+
rulesVersion: number;
|
|
89
|
+
regionRules: RegionRule[];
|
|
90
|
+
}
|
|
91
|
+
/** 区域特征(P2 规则匹配输入:DOM 侧收集的祖先层特征) */
|
|
92
|
+
export interface RegionFeatures {
|
|
93
|
+
/** 元素文本(祖先 innerText) */
|
|
94
|
+
text: string;
|
|
95
|
+
/** 是否含输入框 */
|
|
96
|
+
hasInput: boolean;
|
|
97
|
+
/** 是否含按钮 */
|
|
98
|
+
hasButton: boolean;
|
|
99
|
+
/** 标签名(小写) */
|
|
100
|
+
tag: string;
|
|
101
|
+
/** class 字符串 */
|
|
102
|
+
cls: string;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* P2 规则匹配(纯函数,可单测):规则的全部 match 条件满足才命中。
|
|
106
|
+
* 引擎零业务逻辑——规则来自 behavior.rules.json,这里只执行匹配框架。
|
|
107
|
+
*/
|
|
108
|
+
export declare function matchRegionRule(rule: RegionRule, f: RegionFeatures): boolean;
|
|
109
|
+
/**
|
|
110
|
+
* P2 区域识别(纯函数,可单测):对每层祖先特征依次用规则匹配,命中即返回。
|
|
111
|
+
* 返回 { label, ruleId };无命中 → null。
|
|
112
|
+
*/
|
|
113
|
+
export declare function identifyRegionFromFeatures(rules: RegionRule[], features: RegionFeatures[]): {
|
|
114
|
+
label: string;
|
|
115
|
+
ruleId: string;
|
|
116
|
+
} | null;
|
|
117
|
+
/** 行为档案:区域快照(P2:动作目标所在区域的语义描述) */
|
|
118
|
+
export interface RegionSnapshot {
|
|
119
|
+
/** 区域标签(如「详情区」/「搜索区」;无规则命中 → undefined) */
|
|
120
|
+
label?: string;
|
|
121
|
+
/** 命中规则的 id */
|
|
122
|
+
ruleId?: string;
|
|
123
|
+
/** 目标元素的祖先链摘要(top 3,tag.cls)——抗改版重定位用 */
|
|
124
|
+
ancestry?: string[];
|
|
125
|
+
}
|
|
126
|
+
/** 每步骤/动作的耗时统计(预测性超时数据源;p50/p95 防极端值) */
|
|
127
|
+
export interface TimingStat {
|
|
128
|
+
samples: number;
|
|
129
|
+
avgDurationMs: number;
|
|
130
|
+
p50Ms: number;
|
|
131
|
+
p95Ms: number;
|
|
132
|
+
}
|
|
133
|
+
/** 状态档案(增量合并,原子写):按 步骤|动作 聚合 timing + 计数 */
|
|
134
|
+
export interface RegionProfile {
|
|
135
|
+
/** schema 版本(未来升级迁移用;当前 v1) */
|
|
136
|
+
schemaVersion: 1;
|
|
137
|
+
flowId: string;
|
|
138
|
+
updatedAt: string;
|
|
139
|
+
/** stepId → actionType → 统计(含 seen 置信度与 timing) */
|
|
140
|
+
steps: Record<string, Record<string, {
|
|
141
|
+
seen: number;
|
|
142
|
+
timing?: TimingStat;
|
|
143
|
+
}>>;
|
|
144
|
+
/** 区域出现统计(P2):label → seen(区域功能模型的置信度) */
|
|
145
|
+
regions?: Record<string, {
|
|
146
|
+
seen: number;
|
|
147
|
+
ruleId?: string;
|
|
148
|
+
}>;
|
|
149
|
+
}
|
|
150
|
+
/** 当前 profile schema 版本(升级时 bump + 迁移) */
|
|
151
|
+
export declare const PROFILE_SCHEMA_VERSION: 1;
|
|
152
|
+
/** 状态档案:每步骤保留的最大耗时样本数(防无限增长;超了丢最旧) */
|
|
153
|
+
export declare const PROFILE_MAX_SAMPLES = 50;
|
|
154
|
+
/** 简易互斥锁(单进程内读-合并-写原子化;--http 多请求并发安全) */
|
|
155
|
+
export declare class Mutex {
|
|
156
|
+
private tail;
|
|
157
|
+
run<T>(fn: () => T | Promise<T>): Promise<T>;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* 行为档案库:内存积累 + 落盘 JSONL(data/behavior/archive.jsonl,幂等追加)+ 状态档案
|
|
161
|
+
* (data/behavior/profiles/<flowId>.json,增量合并+原子写+互斥锁)。
|
|
162
|
+
* 单例由 app 层持有(与 RunStore 同级),run 期间通过 onBehavior 回调喂入。
|
|
163
|
+
*/
|
|
164
|
+
export declare class BehaviorArchive {
|
|
165
|
+
private entries;
|
|
166
|
+
private file;
|
|
167
|
+
private profilesDir;
|
|
168
|
+
/** 状态档案内存缓存(flowId → profile);读-合并-写经互斥锁原子化 */
|
|
169
|
+
private profileCache;
|
|
170
|
+
/** 每步骤耗时样本(内存:stepId|actionType → sorted 数组),用于 p50/p95 */
|
|
171
|
+
private samplesCache;
|
|
172
|
+
/** 读-合并-写互斥锁(并发安全) */
|
|
173
|
+
private mutex;
|
|
174
|
+
constructor(dataRoot: string);
|
|
175
|
+
/** 追加一条观察记录(内存 + JSONL 落盘 + 增量合并进状态档案) */
|
|
176
|
+
record(entry: BehaviorEntry): void;
|
|
177
|
+
/** 增量合并:entry → 内存 profile + 样本缓存(写盘由 flushProfile 完成) */
|
|
178
|
+
private mergeEntry;
|
|
179
|
+
/** 读状态档案(内存缓存优先;互斥锁内读文件) */
|
|
180
|
+
private loadProfile;
|
|
181
|
+
/** 读状态档案(公开;互斥锁内——预测查询与合并不并发冲突) */
|
|
182
|
+
readProfile(flowId: string): Promise<RegionProfile | undefined>;
|
|
183
|
+
/** 全部内存记录(供 L2 分析) */
|
|
184
|
+
all(): BehaviorEntry[];
|
|
185
|
+
/** 读取落盘的最近 N 条(跨 run 积累;供 L2 模式分析) */
|
|
186
|
+
readRecent(n: number): BehaviorEntry[];
|
|
187
|
+
/** 归档路径(供诊断/审计) */
|
|
188
|
+
path(): string;
|
|
189
|
+
/** 统计:按 区域/目标 分组的行为规律(L2 消费的聚合形态) */
|
|
190
|
+
summarize(): Array<{
|
|
191
|
+
stepId: string;
|
|
192
|
+
actionType: string;
|
|
193
|
+
selector: string;
|
|
194
|
+
count: number;
|
|
195
|
+
failRate: number;
|
|
196
|
+
avgDurationMs: number;
|
|
197
|
+
}>;
|
|
198
|
+
/**
|
|
199
|
+
* 增强层·预测性超时(docs/BEHAVIOR-SPACE-V2.md P0/P1):
|
|
200
|
+
* 用【状态档案】的历史耗时建议该步骤 verify 超时 = max(默认, avgDurationMs × 1.5)。
|
|
201
|
+
* 读 profile(增量维护,O(1) 读文件缓存,不再每次扫描 archive.jsonl)。
|
|
202
|
+
* 建议级:样本 < PREDICT_MIN_SAMPLES → 返回 0(档案不足,不增强,用默认);
|
|
203
|
+
* 无该步骤记录 → 返回 undefined(完全不增强)。
|
|
204
|
+
* 需传入 flowId(profile 按流程组织;v1 签名兼容由调用方补 flowId)。
|
|
205
|
+
*/
|
|
206
|
+
predictTimeout(flowId: string, stepId: string, actionType: string, defaultMs: number): Promise<number | undefined>;
|
|
207
|
+
}
|
|
208
|
+
/** 预测性超时:最少样本数(机器安全门·门1 复用:≥3 才可信) */
|
|
209
|
+
export declare const PREDICT_MIN_SAMPLES = 3;
|
|
210
|
+
/** 预测性超时:历史均值安全系数(留余量,防波动) */
|
|
211
|
+
export declare const PREDICT_FACTOR = 1.5;
|
|
212
|
+
/** 行为档案的存档文件数(用于测试/诊断) */
|
|
213
|
+
export declare function archiveFileCount(dataRoot: string): number;
|
|
214
|
+
/** 行为档案目录总字节数(用于测试/诊断) */
|
|
215
|
+
export declare function archiveBytes(dataRoot: string): number;
|
package/dist/behavior.js
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import { mkdirSync, appendFileSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { nowIso } from './storage.js';
|
|
4
|
+
import { writeFileAtomic, writeJsonAtomic } from './atomic.js';
|
|
5
|
+
/** 简易文本哈希(FNV-1a 32 位;仅用于变化检测,非加密) */
|
|
6
|
+
export function textHash(s) {
|
|
7
|
+
let h = 0x811c9dc5;
|
|
8
|
+
for (let i = 0; i < s.length; i++) {
|
|
9
|
+
h ^= s.charCodeAt(i);
|
|
10
|
+
h = Math.imul(h, 0x01000193);
|
|
11
|
+
}
|
|
12
|
+
return (h >>> 0).toString(16).padStart(8, '0');
|
|
13
|
+
}
|
|
14
|
+
/** 路由键(host+path,忽略 query/hash) */
|
|
15
|
+
export function routeKey(u) {
|
|
16
|
+
try {
|
|
17
|
+
const x = new URL(u);
|
|
18
|
+
return x.hostname + x.pathname;
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return u;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* P2 规则匹配(纯函数,可单测):规则的全部 match 条件满足才命中。
|
|
26
|
+
* 引擎零业务逻辑——规则来自 behavior.rules.json,这里只执行匹配框架。
|
|
27
|
+
*/
|
|
28
|
+
export function matchRegionRule(rule, f) {
|
|
29
|
+
const m = rule.match;
|
|
30
|
+
if (m.textContains !== undefined && !f.text.includes(m.textContains))
|
|
31
|
+
return false;
|
|
32
|
+
if (m.textContainsAll !== undefined) {
|
|
33
|
+
for (const kw of m.textContainsAll) {
|
|
34
|
+
if (!f.text.includes(kw))
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (m.hasInput === true && !f.hasInput)
|
|
39
|
+
return false;
|
|
40
|
+
if (m.hasButton === true && !f.hasButton)
|
|
41
|
+
return false;
|
|
42
|
+
if (m.ancestorTag !== undefined && !f.tag.includes(m.ancestorTag))
|
|
43
|
+
return false;
|
|
44
|
+
if (m.ancestorClass !== undefined && !f.cls.includes(m.ancestorClass))
|
|
45
|
+
return false;
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* P2 区域识别(纯函数,可单测):对每层祖先特征依次用规则匹配,命中即返回。
|
|
50
|
+
* 返回 { label, ruleId };无命中 → null。
|
|
51
|
+
*/
|
|
52
|
+
export function identifyRegionFromFeatures(rules, features) {
|
|
53
|
+
for (const f of features) {
|
|
54
|
+
for (const r of rules) {
|
|
55
|
+
if (matchRegionRule(r, f))
|
|
56
|
+
return { label: r.label, ruleId: r.id };
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
/** 当前 profile schema 版本(升级时 bump + 迁移) */
|
|
62
|
+
export const PROFILE_SCHEMA_VERSION = 1;
|
|
63
|
+
/** 状态档案:每步骤保留的最大耗时样本数(防无限增长;超了丢最旧) */
|
|
64
|
+
export const PROFILE_MAX_SAMPLES = 50;
|
|
65
|
+
/** 百分位计算(sorted 数组) */
|
|
66
|
+
function percentile(sorted, p) {
|
|
67
|
+
if (sorted.length === 0)
|
|
68
|
+
return 0;
|
|
69
|
+
const idx = Math.min(sorted.length - 1, Math.max(0, Math.round((p / 100) * (sorted.length - 1))));
|
|
70
|
+
return sorted[idx];
|
|
71
|
+
}
|
|
72
|
+
/** 简易互斥锁(单进程内读-合并-写原子化;--http 多请求并发安全) */
|
|
73
|
+
export class Mutex {
|
|
74
|
+
tail = Promise.resolve();
|
|
75
|
+
async run(fn) {
|
|
76
|
+
const prev = this.tail;
|
|
77
|
+
let release;
|
|
78
|
+
this.tail = new Promise((r) => (release = r));
|
|
79
|
+
await prev;
|
|
80
|
+
try {
|
|
81
|
+
return await fn();
|
|
82
|
+
}
|
|
83
|
+
finally {
|
|
84
|
+
release();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* 行为档案库:内存积累 + 落盘 JSONL(data/behavior/archive.jsonl,幂等追加)+ 状态档案
|
|
90
|
+
* (data/behavior/profiles/<flowId>.json,增量合并+原子写+互斥锁)。
|
|
91
|
+
* 单例由 app 层持有(与 RunStore 同级),run 期间通过 onBehavior 回调喂入。
|
|
92
|
+
*/
|
|
93
|
+
export class BehaviorArchive {
|
|
94
|
+
entries = [];
|
|
95
|
+
file;
|
|
96
|
+
profilesDir;
|
|
97
|
+
/** 状态档案内存缓存(flowId → profile);读-合并-写经互斥锁原子化 */
|
|
98
|
+
profileCache = new Map();
|
|
99
|
+
/** 每步骤耗时样本(内存:stepId|actionType → sorted 数组),用于 p50/p95 */
|
|
100
|
+
samplesCache = new Map();
|
|
101
|
+
/** 读-合并-写互斥锁(并发安全) */
|
|
102
|
+
mutex = new Mutex();
|
|
103
|
+
constructor(dataRoot) {
|
|
104
|
+
this.file = join(dataRoot, 'behavior', 'archive.jsonl');
|
|
105
|
+
this.profilesDir = join(dataRoot, 'behavior', 'profiles');
|
|
106
|
+
try {
|
|
107
|
+
mkdirSync(join(dataRoot, 'behavior'), { recursive: true });
|
|
108
|
+
mkdirSync(this.profilesDir, { recursive: true });
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
// 目录不可写:旁路原则——构造不抛错,record 落盘时再静默失败
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/** 追加一条观察记录(内存 + JSONL 落盘 + 增量合并进状态档案) */
|
|
115
|
+
record(entry) {
|
|
116
|
+
this.entries.push(entry);
|
|
117
|
+
try {
|
|
118
|
+
appendFileSync(this.file, JSON.stringify(entry) + '\n', 'utf8');
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
// 落盘失败静默(观察层不阻断流程;旁路采集原则)
|
|
122
|
+
}
|
|
123
|
+
// 增量合并进状态档案(互斥锁内读-合并-写;失败静默)
|
|
124
|
+
void this.mutex.run(() => {
|
|
125
|
+
try {
|
|
126
|
+
this.mergeEntry(entry);
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
// profile 合并失败静默(旁路原则)
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
/** 增量合并:entry → 内存 profile + 样本缓存(写盘由 flushProfile 完成) */
|
|
134
|
+
mergeEntry(entry) {
|
|
135
|
+
const { flowId, stepId, actionType } = entry;
|
|
136
|
+
// 样本缓存(p50/p95):只记 ok 的耗时
|
|
137
|
+
if (entry.result.ok) {
|
|
138
|
+
const key = `${stepId}|${actionType}`;
|
|
139
|
+
const arr = this.samplesCache.get(key) ?? [];
|
|
140
|
+
arr.push(entry.result.durationMs);
|
|
141
|
+
arr.sort((a, b) => a - b);
|
|
142
|
+
if (arr.length > PROFILE_MAX_SAMPLES)
|
|
143
|
+
arr.splice(0, arr.length - PROFILE_MAX_SAMPLES);
|
|
144
|
+
this.samplesCache.set(key, arr);
|
|
145
|
+
}
|
|
146
|
+
// profile 状态
|
|
147
|
+
let prof = this.profileCache.get(flowId);
|
|
148
|
+
if (!prof) {
|
|
149
|
+
prof = this.loadProfile(flowId) ?? { schemaVersion: PROFILE_SCHEMA_VERSION, flowId, updatedAt: nowIso(), steps: {} };
|
|
150
|
+
this.profileCache.set(flowId, prof);
|
|
151
|
+
}
|
|
152
|
+
const stepMap = prof.steps[stepId] ?? {};
|
|
153
|
+
const act = stepMap[actionType] ?? { seen: 0 };
|
|
154
|
+
act.seen += 1;
|
|
155
|
+
if (entry.result.ok) {
|
|
156
|
+
const key = `${stepId}|${actionType}`;
|
|
157
|
+
const samples = this.samplesCache.get(key) ?? [];
|
|
158
|
+
act.timing = {
|
|
159
|
+
samples: samples.length,
|
|
160
|
+
avgDurationMs: Math.round(samples.reduce((a, b) => a + b, 0) / samples.length),
|
|
161
|
+
p50Ms: percentile(samples, 50),
|
|
162
|
+
p95Ms: percentile(samples, 95),
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
stepMap[actionType] = act;
|
|
166
|
+
prof.steps[stepId] = stepMap;
|
|
167
|
+
// P2 区域聚合:entry.region.label 出现计数(区域功能模型的置信度)
|
|
168
|
+
if (entry.region?.label) {
|
|
169
|
+
const regions = prof.regions ?? {};
|
|
170
|
+
const reg = regions[entry.region.label] ?? { seen: 0 };
|
|
171
|
+
reg.seen += 1;
|
|
172
|
+
if (entry.region.ruleId)
|
|
173
|
+
reg.ruleId = entry.region.ruleId;
|
|
174
|
+
regions[entry.region.label] = reg;
|
|
175
|
+
prof.regions = regions;
|
|
176
|
+
}
|
|
177
|
+
prof.updatedAt = nowIso();
|
|
178
|
+
// 原子写盘(互斥锁内,并发安全)
|
|
179
|
+
try {
|
|
180
|
+
writeJsonAtomic(join(this.profilesDir, `${flowId}.json`), prof);
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
// 写盘失败静默
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
/** 读状态档案(内存缓存优先;互斥锁内读文件) */
|
|
187
|
+
loadProfile(flowId) {
|
|
188
|
+
try {
|
|
189
|
+
const raw = readFileSync(join(this.profilesDir, `${flowId}.json`), 'utf8');
|
|
190
|
+
return JSON.parse(raw);
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
return undefined;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
/** 读状态档案(公开;互斥锁内——预测查询与合并不并发冲突) */
|
|
197
|
+
async readProfile(flowId) {
|
|
198
|
+
return this.mutex.run(() => this.profileCache.get(flowId) ?? this.loadProfile(flowId));
|
|
199
|
+
}
|
|
200
|
+
/** 全部内存记录(供 L2 分析) */
|
|
201
|
+
all() {
|
|
202
|
+
return [...this.entries];
|
|
203
|
+
}
|
|
204
|
+
/** 读取落盘的最近 N 条(跨 run 积累;供 L2 模式分析) */
|
|
205
|
+
readRecent(n) {
|
|
206
|
+
try {
|
|
207
|
+
const raw = readFileSync(this.file, 'utf8');
|
|
208
|
+
const lines = raw.split('\n').filter(Boolean);
|
|
209
|
+
const out = [];
|
|
210
|
+
for (const l of lines.slice(-n)) {
|
|
211
|
+
try {
|
|
212
|
+
out.push(JSON.parse(l));
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
// 跳过坏行
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return out;
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
return [];
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
/** 归档路径(供诊断/审计) */
|
|
225
|
+
path() {
|
|
226
|
+
return this.file;
|
|
227
|
+
}
|
|
228
|
+
/** 统计:按 区域/目标 分组的行为规律(L2 消费的聚合形态) */
|
|
229
|
+
summarize() {
|
|
230
|
+
const byKey = new Map();
|
|
231
|
+
for (const e of this.entries) {
|
|
232
|
+
const k = `${e.stepId}|${e.actionType}|${e.selector}`;
|
|
233
|
+
const arr = byKey.get(k) ?? [];
|
|
234
|
+
arr.push(e);
|
|
235
|
+
byKey.set(k, arr);
|
|
236
|
+
}
|
|
237
|
+
const out = [];
|
|
238
|
+
for (const [k, arr] of byKey) {
|
|
239
|
+
const [stepId, actionType, selector] = k.split('|');
|
|
240
|
+
const fails = arr.filter((e) => !e.result.ok).length;
|
|
241
|
+
out.push({
|
|
242
|
+
stepId,
|
|
243
|
+
actionType,
|
|
244
|
+
selector,
|
|
245
|
+
count: arr.length,
|
|
246
|
+
failRate: fails / arr.length,
|
|
247
|
+
avgDurationMs: Math.round(arr.reduce((a, e) => a + e.result.durationMs, 0) / arr.length),
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
return out;
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* 增强层·预测性超时(docs/BEHAVIOR-SPACE-V2.md P0/P1):
|
|
254
|
+
* 用【状态档案】的历史耗时建议该步骤 verify 超时 = max(默认, avgDurationMs × 1.5)。
|
|
255
|
+
* 读 profile(增量维护,O(1) 读文件缓存,不再每次扫描 archive.jsonl)。
|
|
256
|
+
* 建议级:样本 < PREDICT_MIN_SAMPLES → 返回 0(档案不足,不增强,用默认);
|
|
257
|
+
* 无该步骤记录 → 返回 undefined(完全不增强)。
|
|
258
|
+
* 需传入 flowId(profile 按流程组织;v1 签名兼容由调用方补 flowId)。
|
|
259
|
+
*/
|
|
260
|
+
async predictTimeout(flowId, stepId, actionType, defaultMs) {
|
|
261
|
+
const prof = await this.mutex.run(() => this.profileCache.get(flowId) ?? this.loadProfile(flowId));
|
|
262
|
+
const act = prof?.steps[stepId]?.[actionType];
|
|
263
|
+
if (!act || !act.timing)
|
|
264
|
+
return undefined;
|
|
265
|
+
if (act.timing.samples < PREDICT_MIN_SAMPLES)
|
|
266
|
+
return 0;
|
|
267
|
+
const suggested = Math.max(defaultMs, Math.round(act.timing.avgDurationMs * PREDICT_FACTOR));
|
|
268
|
+
return suggested > defaultMs ? suggested : 0;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
/** 预测性超时:最少样本数(机器安全门·门1 复用:≥3 才可信) */
|
|
272
|
+
export const PREDICT_MIN_SAMPLES = 3;
|
|
273
|
+
/** 预测性超时:历史均值安全系数(留余量,防波动) */
|
|
274
|
+
export const PREDICT_FACTOR = 1.5;
|
|
275
|
+
/** 行为档案的存档文件数(用于测试/诊断) */
|
|
276
|
+
export function archiveFileCount(dataRoot) {
|
|
277
|
+
const dir = join(dataRoot, 'behavior');
|
|
278
|
+
try {
|
|
279
|
+
return readdirSync(dir).filter((f) => f.endsWith('.jsonl')).length;
|
|
280
|
+
}
|
|
281
|
+
catch {
|
|
282
|
+
return 0;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
/** 行为档案目录总字节数(用于测试/诊断) */
|
|
286
|
+
export function archiveBytes(dataRoot) {
|
|
287
|
+
const dir = join(dataRoot, 'behavior');
|
|
288
|
+
try {
|
|
289
|
+
const f = join(dir, 'archive.jsonl');
|
|
290
|
+
return statSync(f).size;
|
|
291
|
+
}
|
|
292
|
+
catch {
|
|
293
|
+
return 0;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
//# sourceMappingURL=behavior.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"behavior.js","sourceRoot":"","sources":["../src/behavior.ts"],"names":[],"mappings":"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"}
|
package/dist/cdp/page.d.ts
CHANGED
|
@@ -16,7 +16,7 @@ import type { DomSnapshot } from '../export.js';
|
|
|
16
16
|
* T1 simcheck:模拟质量评分(验收工具,不进生产链路)。
|
|
17
17
|
*/
|
|
18
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";
|
|
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 safeQuery(sel) {\n try {\n return Array.from(document.querySelectorAll(sel)).filter(isVisible);\n } catch (e) {\n return [];\n }\n }\n function getBySels(selector) {\n // A3 \u6570\u7EC4\u94FE\uFF08#17 \u4FEE\u590D\uFF09\uFF1A\u4F9D\u6B21\u5C1D\u8BD5\uFF0C\u7B2C\u4E00\u4E2A\u547D\u4E2D\u7684\u8FD4\u56DE\n if (Array.isArray(selector)) {\n for (const s of selector) {\n const nodes = getBySels(s);\n if (nodes.length > 0) return nodes;\n }\n return [];\n }\n const sel = String(selector);\n if (sel.startsWith('text:')) return byText(sel.slice(5));\n // #16-B \u7F16\u53F7\u5BFB\u5740\uFF08\u865A\u62DF\u7A7A\u95F4\u89C6\u89C9\uFF09\uFF1A#N = numberedElements \u5217\u8868\u7B2C N \u4E2A\u53EF\u89C1\u4EA4\u4E92\u5143\u7D20\uFF081 \u8D77\uFF09\u3002\n // \u5FEB\u7167\u7F16\u53F7\u4E0E\u5BFB\u5740\u5171\u7528\u540C\u4E00\u679A\u4E3E \u2192 Agent/\u65E5\u5FD7\u53EF\u300C\u770B\u7F16\u53F7\u76F4\u63A5\u70B9\u300D\uFF1B\u7F16\u53F7\u968F DOM \u53D8\u5316\uFF0C\u64CD\u4F5C\u65F6\u73B0\u53D6\u3002\n const numMatch = sel.match(/^#(d+)$/);\n if (numMatch) {\n const idx = Number(numMatch[1]) - 1;\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 return idx >= 0 && idx < nodes.length ? [nodes[idx]] : [];\n }\n // \u6DF7\u5408\u4E32\uFF08CSS \u4E0E text: \u9017\u53F7\u5206\u9694\uFF0C\u5982 \"a.search-btn, text:\u641C\u7D22\"\uFF09\uFF1A\u9010\u6BB5\u5C1D\u8BD5\u5408\u5E76\u53BB\u91CD\n // \uFF08#17 \u4FEE\u590D\uFF1Atext: \u6BB5\u4E0D\u80FD\u518D\u6DF7\u8FDB querySelectorAll\u2014\u2014\u6574\u4F53\u4F1A SyntaxError \u5931\u6548\uFF09\n const parts = sel.split(',').map((p) => p.trim()).filter(Boolean);\n if (parts.length > 1 || parts.some((p) => p.startsWith('text:'))) {\n const out = [];\n for (const p of parts) {\n const nodes = p.startsWith('text:') ? byText(p.slice(5)) : safeQuery(p);\n for (const n of nodes) {\n if (isVisible(n) && !out.includes(n)) out.push(n);\n }\n }\n return out;\n }\n return safeQuery(sel);\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 // #16-A \u865A\u62DF\u7A7A\u95F4\u89C6\u89C9\uFF1A\u5143\u7D20\u8BED\u4E49\u63CF\u8FF0\uFF08\u906E\u6321\u68C0\u6D4B/\u65E5\u5FD7\u7528\uFF09\n function describeEl(el) {\n if (!el || el.nodeType !== 1) return null;\n const t = (el.innerText || el.value || el.textContent || '').trim();\n return {\n tag: el.tagName.toLowerCase(),\n cls: (el.className || '').toString().slice(0, 60),\n text: t.slice(0, 30),\n role: el.getAttribute && el.getAttribute('role'),\n };\n }\n // #16-B/P2 \u533A\u57DF\u8BC6\u522B\uFF1A\u76EE\u6807\u5143\u7D20\u5411\u4E0A\u904D\u5386\u7956\u5148\uFF08\u22645 \u5C42\uFF09\uFF0C\u7528\u884C\u4E3A\u89C4\u5219\u5339\u914D\u51FA\u533A\u57DF\u6807\u7B7E\u3002\n // \u89C4\u5219\uFF08behavior.rules.json \u968F\u6D41\u7A0B\u5305\u5206\u53D1\uFF0C\u5F15\u64CE\u96F6\u4E1A\u52A1\u903B\u8F91\uFF09\uFF1A\u5168\u90E8 match \u6761\u4EF6\u6EE1\u8DB3\u624D\u547D\u4E2D\u3002\n // \u8FD4\u56DE { label, ruleId, ancestry }\u2014\u2014ancestry \u4E3A\u7956\u5148\u94FE\u6458\u8981\uFF08\u6297\u6539\u7248\u91CD\u5B9A\u4F4D\u7528\uFF09\u3002\n function identifyRegion(selector, index, rulesJson) {\n const target = pick(selector, index);\n if (!target) return null;\n let rules = [];\n try { rules = JSON.parse(rulesJson || '[]'); } catch (e) { rules = []; }\n const ancestry = [];\n let el = target;\n for (let d = 0; d < 5 && el; d++) {\n if (el !== target && el.nodeType === 1) {\n const cls = (el.className || '').toString().slice(0, 40);\n ancestry.push(el.tagName.toLowerCase() + (cls ? '.' + cls : ''));\n }\n // \u7528\u89C4\u5219\u5339\u914D\u5F53\u524D\u7956\u5148\n for (const r of rules) {\n const m = r.match || {};\n let hit = true;\n if (m.textContains && !((el.innerText || '').includes(m.textContains))) hit = false;\n if (m.textContainsAll && Array.isArray(m.textContainsAll)) {\n const txt = el.innerText || '';\n for (const kw of m.textContainsAll) {\n if (!txt.includes(kw)) { hit = false; break; }\n }\n }\n if (m.hasInput && !el.querySelector('input, textarea')) hit = false;\n if (m.hasButton && !el.querySelector('button, a[role=\"button\"], [role=\"button\"]')) hit = false;\n if (m.ancestorTag && !(el.tagName || '').toLowerCase().includes(m.ancestorTag)) hit = false;\n if (m.ancestorClass && !((el.className || '').toString().includes(m.ancestorClass))) hit = false;\n if (hit) {\n return { label: r.label, ruleId: r.id, ancestry: ancestry.slice(0, 3) };\n }\n }\n el = el.parentElement;\n }\n return { label: undefined, ruleId: undefined, ancestry: ancestry.slice(0, 3) };\n }\n // #16-A \u70B9\u51FB\u524D\u906E\u6321\u68C0\u6D4B\uFF1A\u8FD4\u56DE (x,y) \u5904\u6700\u9876\u5C42\u5143\u7D20 + \u8BE5\u5143\u7D20\u662F\u5426\u5C5E\u4E8E\u76EE\u6807\uFF08\u76EE\u6807\u6216\u5176\u540E\u4EE3\uFF09\n function hitTestAt(selector, index, x, y) {\n const target = pick(selector, index);\n if (!target) return { found: false };\n const top = document.elementFromPoint(x, y);\n if (!top) return { found: true, hit: false, top: null };\n const hit = top === target || target.contains(top);\n return {\n found: true,\n hit,\n top: describeEl(top),\n target: describeEl(target),\n };\n }\n // #16-A \u5BF9\u8BDD\u6846\u5173\u95ED\uFF1A\u906E\u6321\u8005\uFF08dialog-layer \u7B49\u906E\u7F69\uFF09\u5185\u7684\u5173\u95ED\u6309\u94AE\u5750\u6807\uFF08\u4F9B\u5F15\u64CE\u70B9\u51FB\u5173\u95ED\u540E\u91CD\u8BD5\uFF09\n function findDialogClose(selector, index) {\n const target = pick(selector, index);\n if (!target) return null;\n const cx = Math.round(target.getBoundingClientRect().left + target.getBoundingClientRect().width / 2);\n const cy = Math.round(target.getBoundingClientRect().top + target.getBoundingClientRect().height / 2);\n const top = document.elementFromPoint(cx, cy);\n if (!top) return null;\n // \u627E\u6700\u9876\u5C42\u5143\u7D20\u4F5C\u4E3A\u906E\u6321\u8005\uFF08dialog-layer/\u906E\u7F69\uFF09\uFF0C\u5728\u5176\u5185\u627E\u5173\u95ED\u6309\u94AE\n let blocker = top;\n while (blocker && blocker !== document.body) {\n const s = getComputedStyle(blocker);\n if ((s.position === 'fixed' || s.position === 'absolute') && parseFloat(s.zIndex) >= 100) break;\n blocker = blocker.parentElement;\n }\n if (!blocker || blocker === document.body) return null;\n // \u5173\u95ED\u6309\u94AE\u5019\u9009\uFF1Aclass \u542B close / \u56FE\u6807\u6309\u94AE / \u6587\u672C \u00D7\n const closeBtns = [...blocker.querySelectorAll('a,button,[role=\"button\"],i,span,div')].filter((el) => {\n const cls = (el.className || '').toString();\n const txt = (el.innerText || '').trim();\n const r = el.getBoundingClientRect();\n return r.width > 8 && r.height > 8 &&\n (/close|close-btn|icon-close|dialog-close|del|btn-close/i.test(cls) || txt === '\u00D7' || txt === '\u2715' || txt === 'X' || txt === '\u5173\u95ED');\n });\n const btn = closeBtns[0];\n if (!btn) return null;\n const r = btn.getBoundingClientRect();\n return { x: Math.round(r.left + r.width / 2), y: Math.round(r.top + r.height / 2), cls: (btn.className || '').toString().slice(0, 40) };\n }\n // #16-A \u539F\u5B50\u5316\u906E\u6321\u68C0\u6D4B\uFF1A\u4E0E rect \u91C7\u96C6\u540C\u4E00\u6B21\u6C42\u503C\uFF08\u6D88\u9664\u300C\u91C7\u96C6-\u68C0\u6D4B\u300D\u4E24\u6B21\u6C42\u503C\u95F4\u7684 DOM \u7ADE\u6001\uFF09\u3002\n // \u8FD4\u56DE\u6BCF\u4E2A\u53EF\u89C1\u76EE\u6807\u7684 rect + \u4E2D\u5FC3\u70B9\u547D\u4E2D\u4FE1\u606F + \u5B89\u5168\u70B9\u51FB\u70B9\uFF1A\n // hit=true \u4E2D\u5FC3\u70B9\u547D\u4E2D\u76EE\u6807\u6216\u5176\u975E\u4EA4\u4E92\u540E\u4EE3\uFF08\u5B89\u5168\u70B9\u51FB\u70B9\uFF09\n // hit=false \u4E2D\u5FC3\u70B9\u88AB\u771F\u5B9E\u906E\u6321\uFF08\u6D6E\u52A8\u5C42/\u5F39\u5C42/\u5176\u5B83\u5143\u7D20\u8986\u76D6\uFF09\n // danger=true \u4E2D\u5FC3\u70B9\u843D\u5728\u76EE\u6807\u5185\u7684\u3010\u53EF\u4EA4\u4E92\u5B50\u5143\u7D20\u3011\uFF08\u94FE\u63A5/\u6309\u94AE\u7B49\uFF09\u4E0A\u2014\u2014\u70B9\u51FB\u4F1A\u89E6\u53D1\u5B50\u5143\u7D20\u800C\u975E\u76EE\u6807\n // safe \u77E9\u5F62\u5185\u626B\u63CF\u5230\u7684\u7B2C\u4E00\u4E2A\u300C\u5C5E\u4E8E\u76EE\u6807\u4E14\u975E\u4EA4\u4E92\u5B50\u5143\u7D20\u300D\u7684\u70B9\uFF08\u4E2D\u5FC3\u88AB\u76D6/\u5371\u9669\u65F6\u6362\u70B9\u7528\uFF09\uFF1B\n // \u65E0\u5B89\u5168\u70B9\u5219 null\uFF08\u6574\u4E2A\u76EE\u6807\u88AB\u76D6 \u2192 \u5F15\u64CE\u4FA7 ESC \u5173\u95ED\u6D6E\u52A8\u5C42\u91CD\u8BD5\uFF09\n // top=null\uFF08elementFromPoint \u65E0\u7ED3\u679C\uFF0C\u5982\u5143\u7D20\u90E8\u5206\u5728\u89C6\u53E3\u5916/\u6E32\u67D3\u8FB9\u7F18\uFF09\u2192 \u4E0D\u89C6\u4E3A\u906E\u6321\uFF0Chit=true\u3002\n function clickableRects(selector) {\n const d = window.__rpaDom;\n const nodes = d && d.getBySels ? d.getBySels(selector) : [];\n const vw = innerWidth || document.documentElement.clientWidth;\n const vh = innerHeight || document.documentElement.clientHeight;\n const out = [];\n const INTERACTIVE = 'a,button,[role=\"button\"],[role=\"link\"],input,select,textarea,[onclick],label';\n const isSafeHit = (el, top) => {\n if (!top) return false;\n if (top === el) return true;\n if (el.contains(top)) {\n return !(top.closest && top.closest(INTERACTIVE) === top);\n }\n return false;\n };\n for (let i = 0; i < nodes.length; i++) {\n const el = nodes[i];\n const t = el.getBoundingClientRect();\n const fullW = Math.max(1, t.width);\n const fullH = Math.max(1, t.height);\n const x = Math.max(t.left, 0);\n const y = Math.max(t.top, 0);\n const w = Math.max(0, Math.min(t.right, vw) - x);\n const h = Math.max(0, Math.min(t.bottom, vh) - y);\n if (w <= 0 || h <= 0) continue;\n // #16-A \u53EF\u89C1\u6BD4\u4F8B\uFF1A\u89C6\u53E3\u5185\u9762\u79EF / \u603B\u9762\u79EF\u3002\u7A84\u89C6\u53E3\u4E0B\u5927\u90E8\u5206\u5728\u89C6\u53E3\u5916\u7684\u5143\u7D20\n // \uFF08\u5982\u9876\u90E8\u5BFC\u822A\u53F3\u4FA7\u7684\u641C\u7D22\u6309\u94AE\uFF09\u70B9\u51FB\u5750\u6807\u4F1A\u843D\u5728\u89C6\u53E3\u5916/\u8FB9\u7F18 \u2192 CDP input \u6302\u8D77\u6216\u70B9\u7A7A\u3002\n // \u53EF\u89C1\u6BD4\u4F8B < 50% \u2192 \u6807\u8BB0 partiallyVisible\uFF0C\u5F15\u64CE\u4FA7\u62A5\u300C\u76EE\u6807\u5927\u90E8\u5206\u5728\u89C6\u53E3\u5916\u300D\u800C\u975E\u5C1D\u8BD5\u70B9\u51FB\u3002\n const visibleRatio = (w * h) / (fullW * fullH);\n const partiallyVisible = visibleRatio < 0.5;\n if (partiallyVisible) {\n out.push({\n x: Math.round(x), y: Math.round(y), w: Math.round(w), h: Math.round(h),\n hit: false, danger: false, safe: null, partiallyVisible: true, top: null, target: describeEl(el),\n });\n continue;\n }\n const cx = Math.round(x + w / 2);\n const cy = Math.round(y + h / 2);\n const top = document.elementFromPoint(cx, cy);\n if (!top) {\n // elementFromPoint \u65E0\u7ED3\u679C\uFF1A\u53EF\u89C1\u6BD4\u4F8B\u8DB3\u591F\uFF08\u226550%\uFF09\u4F46\u53D6\u4E0D\u5230\u9876\u5C42\u5143\u7D20 \u2192 \u6E32\u67D3\u8FB9\u754C\u5BB9\u5FCD\uFF0C\u4E0D\u963B\u585E\n out.push({\n x: Math.round(x), y: Math.round(y), w: Math.round(w), h: Math.round(h),\n hit: true, danger: false, safe: null, partiallyVisible: false, top: null, target: describeEl(el),\n });\n continue;\n }\n const belongs = top === el || el.contains(top);\n const isInteractiveChild = top !== el && el.contains(top) && top.closest && top.closest(INTERACTIVE) === top;\n // \u4E2D\u5FC3\u88AB\u76D6/\u5371\u9669 \u2192 \u626B\u63CF\u77E9\u5F62\u5185\u90E8\u627E\u5B89\u5168\u70B9\uFF085\u00D75 \u7F51\u683C\uFF0C\u907F\u5F00\u4EA4\u4E92\u5B50\u5143\u7D20\u4E0E\u906E\u6321\uFF09\n let safe = null;\n if (!(belongs && !isInteractiveChild)) {\n for (let gy = 1; gy <= 4 && !safe; gy++) {\n for (let gx = 1; gx <= 4 && !safe; gx++) {\n const sx = Math.round(x + (w * gx) / 5);\n const sy = Math.round(y + (h * gy) / 5);\n const st = document.elementFromPoint(sx, sy);\n if (st && isSafeHit(el, st)) safe = { x: sx, y: sy };\n }\n }\n }\n out.push({\n x: Math.round(x), y: Math.round(y), w: Math.round(w), h: Math.round(h),\n hit: belongs && !isInteractiveChild,\n danger: isInteractiveChild,\n safe,\n partiallyVisible: false,\n top: describeEl(top),\n target: describeEl(el),\n });\n }\n return out;\n }\n // #16-A \u76EE\u6807\u77E9\u5F62\u5185\u627E\u5B89\u5168\u70B9\u51FB\u70B9\uFF1A\u7F51\u683C\u91C7\u6837\uFF085\u00D75\uFF09\uFF0C\u8FD4\u56DE\u7B2C\u4E00\u4E2A\u300C\u5C5E\u4E8E\u76EE\u6807\u4E14\u4E0D\u5728\u53EF\u4EA4\u4E92\u5B50\u5143\u7D20\u4E0A\u300D\u7684\u70B9\u3002\n // \u5927\u5BB9\u5668\u5361\u7247\uFF08\u5982 job-card-wrap\uFF09\u4E2D\u5FC3\u5E38\u843D\u5728\u516C\u53F8\u94FE\u63A5/\u804C\u4F4D\u8BE6\u60C5\u94FE\u63A5\u4E0A\uFF0C\u76F4\u63A5\u70B9\u4F1A\u8BEF\u8DF3\u8F6C\uFF1B\n // \u5B89\u5168\u70B9\u907F\u5F00\u4EA4\u4E92\u5B50\u5143\u7D20\uFF08a/button/[onclick]\uFF09\uFF0C\u53EA\u843D\u5728\u6587\u672C/\u7A7A\u767D\u533A\u57DF\uFF08\u70B9\u51FB\u4ECD\u7531\u7236\u7EA7\u4E8B\u4EF6\u5192\u6CE1\u5904\u7406\uFF09\u3002\n function safePointIn(selector, index) {\n const el = pick(selector, index);\n if (!el) return null;\n const t = el.getBoundingClientRect();\n const INTERACTIVE = 'a,button,[role=\"button\"],[role=\"link\"],input,select,textarea,[onclick],label';\n for (let gy = 2; gy <= 3; gy++) {\n for (let gx = 2; gx <= 3; gx++) {\n const cx = Math.round(t.left + (t.width * gx) / 5);\n const cy = Math.round(t.top + (t.height * gy) / 5);\n const top = document.elementFromPoint(cx, cy);\n if (!top) continue;\n if (top === el || el.contains(top)) {\n const bad = top !== el && top.closest && top.closest(INTERACTIVE) === top;\n if (!bad) return { x: cx, y: cy };\n }\n }\n }\n // \u515C\u5E95\uFF1A\u4E2D\u5FC3\u70B9\uFF08\u53EF\u80FD\u5371\u9669\uFF0C\u7531\u5F15\u64CE\u4FA7\u518D\u5224\u65AD\uFF09\n return { x: Math.round(t.left + t.width / 2), y: Math.round(t.top + t.height / 2) };\n }\n return { getBySels, scrollIntoView, textOf, focusInput, pageText, numberedElements, describeEl, hitTestAt, clickableRects, safePointIn, identifyRegion, findDialogClose };\n})();\ntrue\n";
|
|
20
20
|
/** ENV_CHECK 检测注入脚本(A1;页面端一次 evaluate 返回全部信号) */
|
|
21
21
|
export declare function envCheckSource(envCheck: EnvCheckConfig): string;
|
|
22
22
|
/** ENV_CHECK 配置(flow.json 顶层可选字段;平台可配置,引擎不硬编码) */
|
|
@@ -48,8 +48,24 @@ export declare class CdpPage implements PageOps {
|
|
|
48
48
|
setEnvCheck(config: EnvCheckConfig | undefined): void;
|
|
49
49
|
/** 确保浏览器调试实例在跑(端口不可连则拉起 Edge;edgePath 空则直接报错) */
|
|
50
50
|
ensureBrowser(edgePath: string, userDataDir: string): Promise<void>;
|
|
51
|
-
/**
|
|
51
|
+
/**
|
|
52
|
+
* 打开/复用标签页并导航到 url(绑定拟人层;等待页面就绪)。
|
|
53
|
+
* 风控友好改进(2026-08-26,Hunter 思路对照):优先复用 9222 上已加载的同站标签
|
|
54
|
+
* (避免每次 run 新开标签的冷导航挂起 + 建销模式);finalize 只关本次新建的标签,
|
|
55
|
+
* 复用标签保留 → 持久标签模式(对齐扩展/自带 Chrome 方案的行为)。
|
|
56
|
+
*/
|
|
52
57
|
private open;
|
|
58
|
+
/** 视口最小宽度阈值:保证顶部导航元素(搜索按钮等)完整可见(zhipin 搜索按钮右缘 ~1132px) */
|
|
59
|
+
private static readonly MIN_VIEWPORT_WIDTH;
|
|
60
|
+
/**
|
|
61
|
+
* 窄视口自动最大化:视口宽度 < 1280 → 用 CDP 最大化窗口(真实窗口操作)。
|
|
62
|
+
* 最大化是异步的(窗口管理器动画),等待视口变宽;失败静默(partiallyVisible 兜底报错)。
|
|
63
|
+
*/
|
|
64
|
+
private ensureViewportWidth;
|
|
65
|
+
/** 本次 run 是否新建标签(finalize 只关新建的;复用标签保留 → 持久标签) */
|
|
66
|
+
private openedThisRun;
|
|
67
|
+
/** 提取 URL 的 host(不含端口;空 = 解析失败) */
|
|
68
|
+
private hostOfUrl;
|
|
53
69
|
/** 当前标签页的 CDP 客户端(human 持有的 sink 中的 client) */
|
|
54
70
|
private currentClient;
|
|
55
71
|
/** 注入 DOM 辅助(幂等:页面已有 __rpaDom 则跳过) */
|
|
@@ -62,6 +78,16 @@ export declare class CdpPage implements PageOps {
|
|
|
62
78
|
snapshotDom(): Promise<DomSnapshot>;
|
|
63
79
|
navigate(url: string): Promise<void>;
|
|
64
80
|
click(selector: string, index: number): Promise<void>;
|
|
81
|
+
/** P2 区域识别:目标元素所在区域的语义描述(DOM_SOURCE.identifyRegion;旁路,失败返回 null) */
|
|
82
|
+
identifyRegion(selector: string, index: number, rulesJson: string): Promise<{
|
|
83
|
+
label?: string;
|
|
84
|
+
ruleId?: string;
|
|
85
|
+
ancestry?: string[];
|
|
86
|
+
} | null>;
|
|
87
|
+
/** 路由键:hostname + pathname(忽略 query/hash——SPA query 变化不算跳转) */
|
|
88
|
+
private routeKey;
|
|
89
|
+
/** URL 显示标签(host+path,截断) */
|
|
90
|
+
private hostLabel;
|
|
65
91
|
input(selector: string, value: string): Promise<void>;
|
|
66
92
|
/** A4 canvas 提取:注入 canvas 文本提取脚本(fillText 劫持 + 重组) */
|
|
67
93
|
private extractCanvas;
|