@qing3a/flow-rpa-app 0.6.0 → 0.7.1

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.
@@ -0,0 +1,27 @@
1
+ /**
2
+ * T035-1(S2):同站闸门 key 的 **① 运行时实际 host** 观测源。
3
+ *
4
+ * 问题:闸门的 `acquire`(run 前)发生在页面存在之前——
5
+ * - mock 模式 / resume:页面驱动由执行层临时构建,入口层拿不到;
6
+ * - 真实模式:每次 run 前会**会话轮换**(重启浏览器到 about:blank),此刻读到的
7
+ * 「当前标签 URL」是上一次会话的残留页,不是本次 run 真正会访问的站点。
8
+ * 在 run 前硬探一次浏览器只会把上一次的残留站点记成这次的闸门维度(错锚),所以入口层**不探测**。
9
+ *
10
+ * 做法:执行层在 run 起点把**运行时页面 URL**按 flowId 记成只读观测;
11
+ * 入口层(server run_flow / CLI precheckRun)下次解析闸门 key 时把它当优先级 ① 的来源。
12
+ *
13
+ * 边界:
14
+ * - 优先级 ②(流程声明的 navigate host)始终压得住它——有声明就用声明,观测只在**无声明**时生效;
15
+ * - 因此这条观测只影响「没有 navigate 步骤的流程」(正是 S2 的漏洞面),不会改写已有流程的闸门维度;
16
+ * - 按 flowId 分桶:A 流程观测到的站点不会被 B 流程借用(跨站不互相污染)。
17
+ */
18
+ /** 记录一次运行时 URL 观测(测试注入缝:绕开页面驱动,直接设定「本流程上次所在站点」) */
19
+ export declare function setObservedRuntimeUrl(flowId: string, url: string): void;
20
+ /**
21
+ * 记录一次运行时页面 URL 观测(执行层 run 起点调用;探针抛错 → 转为「无观测」,绝不阻断 run)。
22
+ * `page.currentUrl()` 由驱动提供:mock 页面如实回 URL,真实 CdpPage 在会话轮换后通常回 about:blank
23
+ * (此时记录空串 = 本流程暂时没有可用观测,入口层退回优先级 ② / ③)。
24
+ */
25
+ export declare function observeRuntimeUrl(flowId: string, probe: () => Promise<string> | string, record?: (url: string) => void): Promise<string>;
26
+ /** 该流程最近一次 run 起点观测到的运行时页面 URL(无观测 → 空串) */
27
+ export declare function observedRuntimeUrl(flowId: string): string;
@@ -0,0 +1,44 @@
1
+ /**
2
+ * T035-1(S2):同站闸门 key 的 **① 运行时实际 host** 观测源。
3
+ *
4
+ * 问题:闸门的 `acquire`(run 前)发生在页面存在之前——
5
+ * - mock 模式 / resume:页面驱动由执行层临时构建,入口层拿不到;
6
+ * - 真实模式:每次 run 前会**会话轮换**(重启浏览器到 about:blank),此刻读到的
7
+ * 「当前标签 URL」是上一次会话的残留页,不是本次 run 真正会访问的站点。
8
+ * 在 run 前硬探一次浏览器只会把上一次的残留站点记成这次的闸门维度(错锚),所以入口层**不探测**。
9
+ *
10
+ * 做法:执行层在 run 起点把**运行时页面 URL**按 flowId 记成只读观测;
11
+ * 入口层(server run_flow / CLI precheckRun)下次解析闸门 key 时把它当优先级 ① 的来源。
12
+ *
13
+ * 边界:
14
+ * - 优先级 ②(流程声明的 navigate host)始终压得住它——有声明就用声明,观测只在**无声明**时生效;
15
+ * - 因此这条观测只影响「没有 navigate 步骤的流程」(正是 S2 的漏洞面),不会改写已有流程的闸门维度;
16
+ * - 按 flowId 分桶:A 流程观测到的站点不会被 B 流程借用(跨站不互相污染)。
17
+ */
18
+ /** flowId → 最近一次 run 起点观测到的运行时页面 URL */
19
+ const observed = new Map();
20
+ /** 记录一次运行时 URL 观测(测试注入缝:绕开页面驱动,直接设定「本流程上次所在站点」) */
21
+ export function setObservedRuntimeUrl(flowId, url) {
22
+ observed.set(flowId, url);
23
+ }
24
+ /**
25
+ * 记录一次运行时页面 URL 观测(执行层 run 起点调用;探针抛错 → 转为「无观测」,绝不阻断 run)。
26
+ * `page.currentUrl()` 由驱动提供:mock 页面如实回 URL,真实 CdpPage 在会话轮换后通常回 about:blank
27
+ * (此时记录空串 = 本流程暂时没有可用观测,入口层退回优先级 ② / ③)。
28
+ */
29
+ export async function observeRuntimeUrl(flowId, probe, record = (url) => observed.set(flowId, url)) {
30
+ let url = '';
31
+ try {
32
+ url = String(await probe());
33
+ }
34
+ catch {
35
+ url = ''; // 探针失败 = 探不到(降级到流程声明 / flow 维度),不是错误
36
+ }
37
+ record(url);
38
+ return url;
39
+ }
40
+ /** 该流程最近一次 run 起点观测到的运行时页面 URL(无观测 → 空串) */
41
+ export function observedRuntimeUrl(flowId) {
42
+ return observed.get(flowId) ?? '';
43
+ }
44
+ //# sourceMappingURL=gate-runtime.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gate-runtime.js","sourceRoot":"","sources":["../src/gate-runtime.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,wCAAwC;AACxC,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAkB,CAAA;AAE1C,mDAAmD;AACnD,MAAM,UAAU,qBAAqB,CAAC,MAAc,EAAE,GAAW;IAC/D,QAAQ,CAAC,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;AAC3B,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,MAAc,EACd,KAAqC,EACrC,SAAgC,CAAC,GAAG,EAAE,EAAE,CAAC,QAAQ,CAAC,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC;IAElE,IAAI,GAAG,GAAG,EAAE,CAAA;IACZ,IAAI,CAAC;QACH,GAAG,GAAG,MAAM,CAAC,MAAM,KAAK,EAAE,CAAC,CAAA;IAC7B,CAAC;IAAC,MAAM,CAAC;QACP,GAAG,GAAG,EAAE,CAAA,CAAC,qCAAqC;IAChD,CAAC;IACD,MAAM,CAAC,GAAG,CAAC,CAAA;IACX,OAAO,GAAG,CAAA;AACZ,CAAC;AAED,4CAA4C;AAC5C,MAAM,UAAU,kBAAkB,CAAC,MAAc;IAC/C,OAAO,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE,CAAA;AACnC,CAAC"}
@@ -5,6 +5,8 @@ import { type FlowPatch } from '@qing3a/flow-rpa-engine/src/schema.js';
5
5
  * 扫描 flows/ 目录下各子目录的 flow.json,缓存解析结果。
6
6
  * D5(2026-08-26):加载 flow.json 后合并用户覆盖层 flow.patch.json
7
7
  * (按 stepId 定向整行替换 verify/params;patch 语法错误 → 警告不阻断,D1 fail-loud 复用)。
8
+ * T035-2(2026-09-10,ANTI-BAN-PRINCIPLES §五 S6):加载后比对**内置基线哈希**,
9
+ * 标注流程来源(`builtin` / `builtin-modified` / `local`)——本地改动**可见但不阻断**。
8
10
  */
9
11
  /** 流程参数声明(D-R6;list_flows 返回,供 Agent 组装 vars) */
10
12
  export interface FlowInputDecl {
@@ -12,7 +14,18 @@ export interface FlowInputDecl {
12
14
  /** 必填(D-R6:缺省 false;缺 required 参数时 run_flow 报「缺少必填参数」) */
13
15
  required: boolean;
14
16
  description?: string;
17
+ /** T031C-1:缺省值(String 化后透传给 Agent)。有 default ⇒ 调用方可省略(required 也不例外) */
18
+ default?: string;
15
19
  }
20
+ /**
21
+ * T035-2(S6):流程来源标注。
22
+ * - `builtin`:目录文件名与内容都与**内置基线**逐一致(原样分发的内置流程包)
23
+ * - `builtin-modified`:命中基线(是内置流程),但至少一个文件内容与基线不符(用户本地改过)
24
+ * - `local`:不在基线清单里(用户自建流程,或内置目录被改名)
25
+ * 无基线数据(用户目录里没有 `baseline.json`,或包内副本缺失)时统一标 `local`——
26
+ * **不可校验不等于有罪**:降级为无校验 + 一行日志,不告警、不阻断。
27
+ */
28
+ export type FlowOrigin = 'builtin' | 'builtin-modified' | 'local';
16
29
  /** 流程摘要(list_flows 返回) */
17
30
  export interface FlowSummary {
18
31
  id: string;
@@ -20,24 +33,42 @@ export interface FlowSummary {
20
33
  version: number;
21
34
  path: string;
22
35
  inputs: FlowInputDecl[];
36
+ /** T035-2(S6):来源标注(工具面可见;不落 run.json) */
37
+ origin: FlowOrigin;
38
+ }
39
+ /** 基线清单(`flows/baseline.json`;由脚本生成,禁止手写) */
40
+ export interface FlowBaseline {
41
+ /** 清单格式版本(当前 1);未来格式变更靠它做兼容分支 */
42
+ version: number;
43
+ /** 生成时刻(ISO;仅供人看) */
44
+ generatedAt: string;
45
+ /** flowId → { 文件名 → sha256 } */
46
+ flows: Record<string, {
47
+ files: Record<string, string>;
48
+ }>;
23
49
  }
50
+ /** 基线文件名(与流程目录同级,即 flows/ 下) */
51
+ export declare const BASELINE_FILE = "baseline.json";
52
+ /** 解析基线清单(缺失/损坏/格式不符 → undefined = 无校验;**绝不抛错**) */
53
+ export declare function loadBaseline(flowsDir: string): FlowBaseline | undefined;
24
54
  /** D5 整行替换合并:patch.steps[id].verify / params 整体替换原值(dsh 整行语义) */
25
55
  export declare function applyPatch(flow: Flow, patch: FlowPatch | undefined): Flow;
26
56
  export declare class FlowRegistry {
27
57
  private readonly flowsDir;
28
58
  /** flow_id → 已解析的流程定义(缓存;flow 为 patch 合并后的最终形态) */
29
59
  private cache;
30
- /** 最近一次扫描的流程加载警告(坏 flow.json / 坏 patch 等;fail-loud 警告态,get_status 呈现) */
60
+ /** 最近一次扫描的流程加载警告(坏 flow.json / 坏 patch / 偏离内置基线等;fail-loud 警告态,get_status 呈现) */
31
61
  private _warnings;
32
62
  constructor(flowsDir: string);
33
63
  /** 最近一次扫描的警告(D1 fail-loud:坏流程启动即呈现,不静默跳过) */
34
64
  warnings(): string[];
35
- /** 扫描 flows/ 下各子目录的 flow.json(含 D5 patch 合并) */
65
+ /** 扫描 flows/ 下各子目录的 flow.json(含 D5 patch 合并 + T035-2 基线比对) */
36
66
  list(): FlowSummary[];
37
67
  get(flowId: string): {
38
68
  version: number;
39
69
  raw: string;
40
70
  flow: Flow;
71
+ origin: FlowOrigin;
41
72
  } | undefined;
42
73
  /** 流程目录(pkg_update 替换用) */
43
74
  flowsDirOf(): string;
package/dist/registry.js CHANGED
@@ -1,17 +1,66 @@
1
- import { readdirSync, readFileSync } from 'node:fs';
1
+ import { createHash } from 'node:crypto';
2
+ import { readdirSync, readFileSync, statSync } from 'node:fs';
2
3
  import { join } from 'node:path';
3
4
  import { parseFlow } from '@qing3a/flow-rpa-engine/src/validate.js';
4
5
  import { FlowPatchSchema } from '@qing3a/flow-rpa-engine/src/schema.js';
5
- /** 读取用户覆盖层(flows/<name>/flow.patch.json;不存在 undefined;解析失败 → 警告信息 + undefined) */
6
- function loadPatch(flowDir, name, warnings) {
7
- const patchPath = join(flowDir, 'flow.patch.json');
6
+ /** 基线文件名(与流程目录同级,即 flows/ 下) */
7
+ export const BASELINE_FILE = 'baseline.json';
8
+ /** 解析基线清单(缺失/损坏/格式不符 → undefined = 无校验;**绝不抛错**) */
9
+ export function loadBaseline(flowsDir) {
8
10
  let raw;
9
11
  try {
10
- raw = readFileSync(patchPath, 'utf8');
12
+ raw = readFileSync(join(flowsDir, BASELINE_FILE), 'utf8');
11
13
  }
12
14
  catch {
13
- return undefined; // patch(正常)
15
+ return undefined; // 无基线文件(老版本流程目录 / 用户自建目录)
14
16
  }
17
+ try {
18
+ const parsed = JSON.parse(raw);
19
+ if (!parsed || typeof parsed !== 'object' || typeof parsed.flows !== 'object' || parsed.flows === null)
20
+ return undefined;
21
+ return {
22
+ version: typeof parsed.version === 'number' ? parsed.version : 0,
23
+ generatedAt: typeof parsed.generatedAt === 'string' ? parsed.generatedAt : '',
24
+ flows: parsed.flows,
25
+ };
26
+ }
27
+ catch {
28
+ return undefined; // 基线损坏 → 退化为无校验(fail-soft:它只是「来源可见性」,不是执行前置)
29
+ }
30
+ }
31
+ /** sha256(十六进制小写) */
32
+ function sha256(bytes) {
33
+ return createHash('sha256').update(bytes).digest('hex');
34
+ }
35
+ /** 流程目录下的文件名(小写排序;目录/子目录不进清单——starter 包只有平铺文件) */
36
+ function listFlowFiles(flowDir) {
37
+ try {
38
+ return readdirSync(flowDir)
39
+ .filter((f) => {
40
+ try {
41
+ return statSync(join(flowDir, f)).isFile();
42
+ }
43
+ catch {
44
+ return false;
45
+ }
46
+ })
47
+ .sort();
48
+ }
49
+ catch {
50
+ return [];
51
+ }
52
+ }
53
+ /** 读取用户覆盖层(`flow.patch.json` 的**原始字节**;不存在 → undefined) */
54
+ function readPatchRaw(flowDir) {
55
+ try {
56
+ return readFileSync(join(flowDir, 'flow.patch.json'), 'utf8');
57
+ }
58
+ catch {
59
+ return undefined;
60
+ }
61
+ }
62
+ /** 解析覆盖层原始字节(语法/结构错误 → 警告 + undefined;原流程生效) */
63
+ function parsePatch(raw, name, warnings) {
15
64
  try {
16
65
  return FlowPatchSchema.parse(JSON.parse(raw));
17
66
  }
@@ -41,7 +90,7 @@ export class FlowRegistry {
41
90
  flowsDir;
42
91
  /** flow_id → 已解析的流程定义(缓存;flow 为 patch 合并后的最终形态) */
43
92
  cache = new Map();
44
- /** 最近一次扫描的流程加载警告(坏 flow.json / 坏 patch 等;fail-loud 警告态,get_status 呈现) */
93
+ /** 最近一次扫描的流程加载警告(坏 flow.json / 坏 patch / 偏离内置基线等;fail-loud 警告态,get_status 呈现) */
45
94
  _warnings = [];
46
95
  constructor(flowsDir) {
47
96
  this.flowsDir = flowsDir;
@@ -50,9 +99,10 @@ export class FlowRegistry {
50
99
  warnings() {
51
100
  return this._warnings;
52
101
  }
53
- /** 扫描 flows/ 下各子目录的 flow.json(含 D5 patch 合并) */
102
+ /** 扫描 flows/ 下各子目录的 flow.json(含 D5 patch 合并 + T035-2 基线比对) */
54
103
  list() {
55
104
  this._warnings = [];
105
+ this.cache.clear(); // 每次扫描重建缓存:来源标注不做陈旧缓存(文件可能刚被改)
56
106
  const out = [];
57
107
  let entries;
58
108
  try {
@@ -63,6 +113,9 @@ export class FlowRegistry {
63
113
  catch {
64
114
  return [];
65
115
  }
116
+ // T035-2:基线只读一次(每目录都读会浪费;缺失 → 无校验降级)
117
+ const baseline = loadBaseline(this.flowsDir);
118
+ let modifiedCount = 0;
66
119
  for (const name of entries) {
67
120
  const flowDir = join(this.flowsDir, name);
68
121
  const flowPath = join(flowDir, 'flow.json');
@@ -74,7 +127,27 @@ export class FlowRegistry {
74
127
  continue;
75
128
  }
76
129
  try {
77
- const flow = applyPatch(parseFlow(raw), loadPatch(flowDir, name, this._warnings));
130
+ const patchRaw = readPatchRaw(flowDir);
131
+ const flow = applyPatch(parseFlow(raw), patchRaw !== undefined ? parsePatch(patchRaw, name, this._warnings) : undefined);
132
+ // T035-2(S6):来源判定——基线声称认得这个 id 吗?逐文件哈希一致吗?
133
+ // 目录名与 flow.id 都可作为基线键(starter 同步后两者一致;用户改名后仍能认出来)
134
+ const base = baseline?.flows?.[name] ?? baseline?.flows?.[flow.id];
135
+ let origin = 'local';
136
+ if (base && base.files && typeof base.files === 'object') {
137
+ const actual = new Map();
138
+ for (const f of listFlowFiles(flowDir))
139
+ actual.set(f, sha256(readFileSync(join(flowDir, f))));
140
+ const expected = base.files;
141
+ const expectedNames = Object.keys(expected);
142
+ const sameFileSet = expectedNames.length === actual.size && expectedNames.every((f) => actual.has(f));
143
+ const sameContent = sameFileSet && expectedNames.every((f) => actual.get(f) === expected[f]);
144
+ origin = sameContent ? 'builtin' : 'builtin-modified';
145
+ if (origin === 'builtin-modified') {
146
+ modifiedCount++;
147
+ // 不阻断执行(用户有权改自己的流程);只在 doctor / get_status / 启动日志呈现
148
+ this._warnings.push(`流程 ${flow.id} 与内置基线不一致(本地已改动)`);
149
+ }
150
+ }
78
151
  out.push({
79
152
  id: flow.id,
80
153
  name: flow.name,
@@ -84,9 +157,12 @@ export class FlowRegistry {
84
157
  name: i.name,
85
158
  required: i.required === true,
86
159
  ...(i.description ? { description: i.description } : {}),
160
+ // T031C-1:default 一并透传(Agent 不必靠猜缺省值;executor 侧同源注入)
161
+ ...(i.default !== undefined ? { default: String(i.default) } : {}),
87
162
  })),
163
+ origin,
88
164
  });
89
- this.cache.set(flow.id, { version: flow.version, raw, flow });
165
+ this.cache.set(flow.id, { version: flow.version, raw, flow, origin });
90
166
  }
91
167
  catch (e) {
92
168
  // D1:解析失败不静默跳过——收集警告(启动/状态页呈现),等 run_flow 才暴露太晚
@@ -94,6 +170,14 @@ export class FlowRegistry {
94
170
  continue;
95
171
  }
96
172
  }
173
+ // T035-2:无基线(或基线里一个流程都不认得)→ **一行降级日志**(可校验性本身要可见,
174
+ // 但「包内 baseline.json 缺失」不是用户的问题,故用日志而不用警告——见 brief「不报错」)
175
+ if (baseline === undefined && out.length > 0) {
176
+ console.log(`[flow-rpa] 无内置流程基线(${BASELINE_FILE} 缺失或损坏),跳过来源校验(不阻断执行)`);
177
+ }
178
+ else if (baseline !== undefined && modifiedCount > 0) {
179
+ console.log(`[flow-rpa] ${modifiedCount} 个内置流程与基线不一致(本地已改动,仅告警不阻断)`);
180
+ }
97
181
  out.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
98
182
  return out;
99
183
  }
@@ -1 +1 @@
1
- {"version":3,"file":"registry.js","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"registry.js","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AACxC,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAA;AAC7D,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAChC,OAAO,EAAE,SAAS,EAAa,MAAM,yCAAyC,CAAA;AAC9E,OAAO,EAAE,eAAe,EAAkB,MAAM,uCAAuC,CAAA;AAoDvF,gCAAgC;AAChC,MAAM,CAAC,MAAM,aAAa,GAAG,eAAe,CAAA;AAE5C,oDAAoD;AACpD,MAAM,UAAU,YAAY,CAAC,QAAgB;IAC3C,IAAI,GAAW,CAAA;IACf,IAAI,CAAC;QACH,GAAG,GAAG,YAAY,CAAC,IAAI,CAAC,QAAQ,EAAE,aAAa,CAAC,EAAE,MAAM,CAAC,CAAA;IAC3D,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAA,CAAC,0BAA0B;IAC7C,CAAC;IACD,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAA0B,CAAA;QACvD,IAAI,CAAC,MAAM,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,OAAO,MAAM,CAAC,KAAK,KAAK,QAAQ,IAAI,MAAM,CAAC,KAAK,KAAK,IAAI;YAAE,OAAO,SAAS,CAAA;QACxH,OAAO;YACL,OAAO,EAAE,OAAO,MAAM,CAAC,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;YAChE,WAAW,EAAE,OAAO,MAAM,CAAC,WAAW,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE;YAC7E,KAAK,EAAE,MAAM,CAAC,KAA8B;SAC7C,CAAA;IACH,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAA,CAAC,6CAA6C;IAChE,CAAC;AACH,CAAC;AAED,qBAAqB;AACrB,SAAS,MAAM,CAAC,KAAsB;IACpC,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;AACzD,CAAC;AAED,kDAAkD;AAClD,SAAS,aAAa,CAAC,OAAe;IACpC,IAAI,CAAC;QACH,OAAO,WAAW,CAAC,OAAO,CAAC;aACxB,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE;YACZ,IAAI,CAAC;gBACH,OAAO,QAAQ,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC,MAAM,EAAE,CAAA;YAC5C,CAAC;YAAC,MAAM,CAAC;gBACP,OAAO,KAAK,CAAA;YACd,CAAC;QACH,CAAC,CAAC;aACD,IAAI,EAAE,CAAA;IACX,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAA;IACX,CAAC;AACH,CAAC;AAED,2DAA2D;AAC3D,SAAS,YAAY,CAAC,OAAe;IACnC,IAAI,CAAC;QACH,OAAO,YAAY,CAAC,IAAI,CAAC,OAAO,EAAE,iBAAiB,CAAC,EAAE,MAAM,CAAC,CAAA;IAC/D,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAA;IAClB,CAAC;AACH,CAAC;AAED,gDAAgD;AAChD,SAAS,UAAU,CAAC,GAAW,EAAE,IAAY,EAAE,QAAkB;IAC/D,IAAI,CAAC;QACH,OAAO,eAAe,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAc,CAAA;IAC5D,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,uCAAuC;QACvC,QAAQ,CAAC,IAAI,CAAC,GAAG,IAAI,oCAAoC,CAAC,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAA;QACtG,OAAO,SAAS,CAAA;IAClB,CAAC;AACH,CAAC;AAED,iEAAiE;AACjE,MAAM,UAAU,UAAU,CAAC,IAAU,EAAE,KAA4B;IACjE,IAAI,CAAC,KAAK;QAAE,OAAO,IAAI,CAAA;IACvB,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE;QACjC,MAAM,CAAC,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,CAAA;QAC3B,IAAI,CAAC,CAAC;YAAE,OAAO,CAAC,CAAA;QAChB,OAAO;YACL,GAAG,CAAC;YACJ,GAAG,CAAC,CAAC,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YACvD,GAAG,CAAC,CAAC,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SACxD,CAAA;IACH,CAAC,CAAC,CAAA;IACF,OAAO,EAAE,GAAG,IAAI,EAAE,KAAK,EAAE,CAAA;AAC3B,CAAC;AAED,MAAM,OAAO,YAAY;IACN,QAAQ,CAAQ;IACjC,mDAAmD;IAC3C,KAAK,GAAG,IAAI,GAAG,EAA4E,CAAA;IACnG,iFAAiF;IACzE,SAAS,GAAa,EAAE,CAAA;IAEhC,YAAY,QAAgB;QAC1B,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAA;IAC1B,CAAC;IAED,6CAA6C;IAC7C,QAAQ;QACN,OAAO,IAAI,CAAC,SAAS,CAAA;IACvB,CAAC;IAED,8DAA8D;IAC9D,IAAI;QACF,IAAI,CAAC,SAAS,GAAG,EAAE,CAAA;QACnB,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAA,CAAC,+BAA+B;QAClD,MAAM,GAAG,GAAkB,EAAE,CAAA;QAC7B,IAAI,OAAiB,CAAA;QACrB,IAAI,CAAC;YACH,OAAO,GAAG,WAAW,CAAC,IAAI,CAAC,QAAQ,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC;iBAC1D,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC;iBAC9B,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAA;QACvB,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,EAAE,CAAA;QACX,CAAC;QACD,qCAAqC;QACrC,MAAM,QAAQ,GAAG,YAAY,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAA;QAC5C,IAAI,aAAa,GAAG,CAAC,CAAA;QACrB,KAAK,MAAM,IAAI,IAAI,OAAO,EAAE,CAAC;YAC3B,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAA;YACzC,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,EAAE,WAAW,CAAC,CAAA;YAC3C,IAAI,GAAW,CAAA;YACf,IAAI,CAAC;gBACH,GAAG,GAAG,YAAY,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAA;YACtC,CAAC;YAAC,MAAM,CAAC;gBACP,SAAQ;YACV,CAAC;YACD,IAAI,CAAC;gBACH,MAAM,QAAQ,GAAG,YAAY,CAAC,OAAO,CAAC,CAAA;gBACtC,MAAM,IAAI,GAAG,UAAU,CAAC,SAAS,CAAC,GAAG,CAAC,EAAE,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,UAAU,CAAC,QAAQ,EAAE,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAA;gBACxH,2CAA2C;gBAC3C,mDAAmD;gBACnD,MAAM,IAAI,GAAG,QAAQ,EAAE,KAAK,EAAE,CAAC,IAAI,CAAC,IAAI,QAAQ,EAAE,KAAK,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;gBAClE,IAAI,MAAM,GAAe,OAAO,CAAA;gBAChC,IAAI,IAAI,IAAI,IAAI,CAAC,KAAK,IAAI,OAAO,IAAI,CAAC,KAAK,KAAK,QAAQ,EAAE,CAAC;oBACzD,MAAM,MAAM,GAAG,IAAI,GAAG,EAAkB,CAAA;oBACxC,KAAK,MAAM,CAAC,IAAI,aAAa,CAAC,OAAO,CAAC;wBAAE,MAAM,CAAC,GAAG,CAAC,CAAC,EAAE,MAAM,CAAC,YAAY,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;oBAC7F,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAA;oBAC3B,MAAM,aAAa,GAAG,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAA;oBAC3C,MAAM,WAAW,GAAG,aAAa,CAAC,MAAM,KAAK,MAAM,CAAC,IAAI,IAAI,aAAa,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAA;oBACrG,MAAM,WAAW,GAAG,WAAW,IAAI,aAAa,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAA;oBAC5F,MAAM,GAAG,WAAW,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,kBAAkB,CAAA;oBACrD,IAAI,MAAM,KAAK,kBAAkB,EAAE,CAAC;wBAClC,aAAa,EAAE,CAAA;wBACf,oDAAoD;wBACpD,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,MAAM,IAAI,CAAC,EAAE,kBAAkB,CAAC,CAAA;oBACtD,CAAC;gBACH,CAAC;gBACD,GAAG,CAAC,IAAI,CAAC;oBACP,EAAE,EAAE,IAAI,CAAC,EAAE;oBACX,IAAI,EAAE,IAAI,CAAC,IAAI;oBACf,OAAO,EAAE,IAAI,CAAC,OAAO;oBACrB,IAAI,EAAE,QAAQ;oBACd,MAAM,EAAE,CAAC,IAAI,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;wBACtC,IAAI,EAAE,CAAC,CAAC,IAAI;wBACZ,QAAQ,EAAE,CAAC,CAAC,QAAQ,KAAK,IAAI;wBAC7B,GAAG,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;wBACxD,qDAAqD;wBACrD,GAAG,CAAC,CAAC,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;qBACnE,CAAC,CAAC;oBACH,MAAM;iBACP,CAAC,CAAA;gBACF,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,CAAA;YACvE,CAAC;YAAC,OAAO,CAAC,EAAE,CAAC;gBACX,gDAAgD;gBAChD,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,GAAG,IAAI,oBAAoB,CAAC,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAA;gBAC5F,SAAQ;YACV,CAAC;QACH,CAAC;QACD,kDAAkD;QAClD,yDAAyD;QACzD,IAAI,QAAQ,KAAK,SAAS,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC7C,OAAO,CAAC,GAAG,CAAC,sBAAsB,aAAa,uBAAuB,CAAC,CAAA;QACzE,CAAC;aAAM,IAAI,QAAQ,KAAK,SAAS,IAAI,aAAa,GAAG,CAAC,EAAE,CAAC;YACvD,OAAO,CAAC,GAAG,CAAC,cAAc,aAAa,4BAA4B,CAAC,CAAA;QACtE,CAAC;QACD,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;QAC5D,OAAO,GAAG,CAAA;IACZ,CAAC;IAED,GAAG,CAAC,MAAc;QAChB,IAAI,IAAI,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC;YAAE,IAAI,CAAC,IAAI,EAAE,CAAA;QACtC,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,CAAA;IAC/B,CAAC;IAED,2BAA2B;IAC3B,UAAU;QACR,OAAO,IAAI,CAAC,QAAQ,CAAA;IACtB,CAAC;CACF"}
package/dist/runner.d.ts CHANGED
@@ -7,8 +7,9 @@ import { ObservationExporter } from '@qing3a/flow-rpa-engine/src/export.js';
7
7
  import { PerfGate } from '@qing3a/flow-rpa-engine/src/bench.js';
8
8
  import { CooldownGate, SiteGate } from '@qing3a/flow-rpa-engine/src/gate.js';
9
9
  import { BehaviorArchive } from '@qing3a/flow-rpa-engine/src/behavior.js';
10
- import { FlowRegistry } from './registry.js';
10
+ import { FlowRegistry, type FlowOrigin } from './registry.js';
11
11
  import type { RunConfig } from './config.js';
12
+ import { type GateKeyResolution, type GateKeySource } from './gate-key.js';
12
13
  /**
13
14
  * run_flow 异步执行层(移植 Rust `server.rs` 的 execute_run/spawn_run + M9 D-R1/D-R2)。
14
15
  *
@@ -34,6 +35,11 @@ export interface RunFlowResult {
34
35
  pauseGuidance?: string;
35
36
  /** T027/B2:登录态预检拒绝(run 保持 paused 未执行;CLI 据此退出码 1,与「resume 后再次 paused」区分) */
36
37
  precheckRejected?: boolean;
38
+ /** T035-2(S6):流程来源(builtin / builtin-modified / local)——**只进工具面/CLI 输出,不落 run.json** */
39
+ origin?: FlowOrigin;
40
+ /** T035-1(S2):本次 run 实际使用的同站闸门 key(`site:<host>` / `flow:<id>`)与维度——只进工具面 */
41
+ gateKey?: string;
42
+ gateKeySource?: GateKeySource;
37
43
  }
38
44
  /**
39
45
  * T027/B2:pauseReason → 结构化下一步指引(人工接管交接,bsk human-in-the-loop 借鉴)。
@@ -54,6 +60,12 @@ export interface RunFlowArgs {
54
60
  loopIter?: number;
55
61
  loopSub?: number;
56
62
  };
63
+ /**
64
+ * T035-1(S2):调用方**已解析过**的闸门 key(server/CLI 的 run 前 acquire 用过的那一个)。
65
+ * 传入 = 同一 run 的 acquire 与 penalize 记同一个 key(不重复探测页面、不产生二次口径漂移);
66
+ * 不传(如 resume_run)= run 内按「运行时 host → 流程声明 host → flow 维度」现解析。
67
+ */
68
+ gateKeyResolved?: GateKeyResolution;
57
69
  }
58
70
  /** 执行上下文(runner 持有全部共享状态) */
59
71
  export interface ExecCtx {
@@ -115,7 +127,17 @@ export declare class MockPage implements PageOps {
115
127
  private extracted;
116
128
  /** 测试用:waitMs 真实阻塞时长(默认 0 = no-op;设 >0 可让 run 慢下来验证队列忙) */
117
129
  slowWaitMs: number;
130
+ /** 测试用:注入的非拟人兜底事件(takeFallbacks 取走并清空;默认空 = 纯拟人路径) */
131
+ fallbackEvents: Array<{
132
+ kind: string;
133
+ selector?: string;
134
+ }>;
118
135
  setEnvCheck(config: EnvCheck | undefined): void;
136
+ /** 兜底计量(PageOps.takeFallbacks):取走并清空 */
137
+ takeFallbacks(): Array<{
138
+ kind: string;
139
+ selector?: string;
140
+ }>;
119
141
  navigate(url: string): Promise<void>;
120
142
  click(sel: string, index: number): Promise<void>;
121
143
  input(_sel: string, v: string): Promise<void>;
@@ -139,7 +161,12 @@ export declare class MockPage implements PageOps {
139
161
  */
140
162
  export declare function housekeepingAfterRun(ctx: ExecCtx): Promise<void>;
141
163
  /** 同步执行一个 run(构造驱动 → 解释执行 → 落盘 → 自动上报 → 更新运行状态) */
142
- export declare function executeRun(ctx: ExecCtx, args: RunFlowArgs, runId: string): Promise<RunFlowResult>;
164
+ export declare function executeRun(ctx: ExecCtx, args: RunFlowArgs, runId: string,
165
+ /**
166
+ * 测试注入的页面驱动(**仅 mock 模式生效**;缺省 new MockPage())。
167
+ * 真实模式恒用 CdpPage —— 注入值被忽略,测试缝隙不会漏进真实执行路径。
168
+ */
169
+ pageOverride?: PageOps): Promise<RunFlowResult>;
143
170
  /**
144
171
  * T027/B2:登录态探测器(真实模式缺省实现)——连 9222 现有标签页,
145
172
  * 用 engine region.ts 登录区双重特征(isLoginSignal:路由特征 + 页面文本强标记,与 executor 每步复查同源)判定是否已登录。
@@ -173,7 +200,7 @@ export declare function resumeRun(ctx: ExecCtx, runId: string, precheck?: Resume
173
200
  * 不破坏 open-browser/懒加载登录:登录态在 profile(重启保留);下次 open-browser/run 重新拉起。
174
201
  */
175
202
  export declare function rotateBrowserSession(port: number, edgePath: string, userDataDir: string): Promise<void>;
176
- /** 流程的目标站点 host(取首个 navigate 步骤的 url;无 navigate 返回 undefined —— 不节流 mock) */
203
+ /** 流程声明的目标站点 host(T035-1 = **全部** navigate 步骤里最后一个;无 navigate undefined) */
177
204
  export declare function flowHost(ctx: ExecCtx, flowId: string): string | undefined;
178
205
  /** 队列当前状态(自描述聚合用 D-S1;只读) */
179
206
  export declare function queueState(): {
package/dist/runner.js CHANGED
@@ -9,9 +9,13 @@ import { nowIso, RunStore, extractSink } from '@qing3a/flow-rpa-engine/src/stora
9
9
  import { cleanTmpFiles, retireRuns } from '@qing3a/flow-rpa-engine/src/housekeeping.js';
10
10
  import { ObservationExporter } from '@qing3a/flow-rpa-engine/src/export.js';
11
11
  import { PerfGate } from '@qing3a/flow-rpa-engine/src/bench.js';
12
- import { CooldownGate, hostOf, SiteGate } from '@qing3a/flow-rpa-engine/src/gate.js';
12
+ import { CooldownGate, DEEP_CRAWL_MIN_PAGES, deepCrawlPenaltySecs, hostOf, SiteGate } from '@qing3a/flow-rpa-engine/src/gate.js';
13
13
  import { BehaviorArchive } from '@qing3a/flow-rpa-engine/src/behavior.js';
14
14
  import { FlowRegistry } from './registry.js';
15
+ import { defaultEdgePath } from './config.js';
16
+ import { rotationFromConfig, rotationLogLine, setRotationObservation } from './session-rotation.js';
17
+ import { declaredNavigateHost, gateKeyLabel, reportGateFallback, resolveGateKeyInRun, } from './gate-key.js';
18
+ import { observeRuntimeUrl } from './gate-runtime.js';
15
19
  /**
16
20
  * T027/B2:pauseReason → 结构化下一步指引(人工接管交接,bsk human-in-the-loop 借鉴)。
17
21
  * 只进工具返回 message / CLI 输出,绝不落 run.json(data/ 格式冻结);绝不自动 resume(无人值守边界)。
@@ -22,7 +26,7 @@ export function pauseGuidance(reason, runId) {
22
26
  return `请在引擎浏览器(open-browser/run 弹出的专用浏览器)完成登录后执行 ${resumeHint}`;
23
27
  }
24
28
  if (reason === 'env_check') {
25
- return `引擎检测到环境风险(验证码/风险弹窗,流程 envCheck 配置命中)——请人工查看页面并处理后执行 ${resumeHint};绝不自动重试(防封铁律)`;
29
+ return `引擎检测到环境风险(命中风险页基线 / 验证码 / 风险弹窗)——请人工查看页面并处理后执行 ${resumeHint};绝不自动重试(防封铁律)`;
26
30
  }
27
31
  return `请人工处理页面状态后执行 ${resumeHint}`;
28
32
  }
@@ -137,10 +141,18 @@ export class MockPage {
137
141
  extracted = [];
138
142
  /** 测试用:waitMs 真实阻塞时长(默认 0 = no-op;设 >0 可让 run 慢下来验证队列忙) */
139
143
  slowWaitMs = MockPage.defaultSlowWaitMs;
144
+ /** 测试用:注入的非拟人兜底事件(takeFallbacks 取走并清空;默认空 = 纯拟人路径) */
145
+ fallbackEvents = [];
140
146
  setEnvCheck(config) {
141
147
  this.envCheck = config;
142
148
  MockPage.lastEnvCheck = config;
143
149
  }
150
+ /** 兜底计量(PageOps.takeFallbacks):取走并清空 */
151
+ takeFallbacks() {
152
+ const out = this.fallbackEvents;
153
+ this.fallbackEvents = [];
154
+ return out;
155
+ }
144
156
  async navigate(url) {
145
157
  this.url = url;
146
158
  }
@@ -233,18 +245,33 @@ function loadBehaviorRules(flowsDir, flowId) {
233
245
  }
234
246
  }
235
247
  /** 同步执行一个 run(构造驱动 → 解释执行 → 落盘 → 自动上报 → 更新运行状态) */
236
- export async function executeRun(ctx, args, runId) {
248
+ export async function executeRun(ctx, args, runId,
249
+ /**
250
+ * 测试注入的页面驱动(**仅 mock 模式生效**;缺省 new MockPage())。
251
+ * 真实模式恒用 CdpPage —— 注入值被忽略,测试缝隙不会漏进真实执行路径。
252
+ */
253
+ pageOverride) {
237
254
  const cached = ctx.registry.get(args.flowId);
238
255
  if (!cached) {
239
256
  const r = { runId, status: 'failed', error: `流程不存在: ${args.flowId}` };
240
257
  ctx.runs.set(runId, r);
241
258
  return r;
242
259
  }
243
- const { flow, version } = cached;
260
+ const { flow, version, origin } = cached;
261
+ // T033-2(S5):会话轮换状态显式化——与 doctor/get_status 同一判据(session-rotation.ts)。
262
+ // 「有没有轮换、依据哪个 edgePath」不再只藏在代码分支里:mock 驱动如实呈现「未轮换」。
263
+ const rotation = setRotationObservation({
264
+ ...rotationFromConfig(ctx.config.edgePath, defaultEdgePath()),
265
+ ...(ctx.config.mock ? { enabled: false, reason: 'disabled', edgePath: '' } : {}),
266
+ });
267
+ if (!ctx.config.mock) {
268
+ // 真实 run 每次打一行:本次 run 是否轮换 + 依据的 edgePath
269
+ console.log(`[flow-rpa] ${rotationLogLine(rotation)}`);
270
+ }
244
271
  // 驱动选择:mock(调试/无浏览器)或真实浏览器(M8 CdpPage)
245
272
  let page;
246
273
  if (ctx.config.mock) {
247
- page = new MockPage();
274
+ page = pageOverride ?? new MockPage();
248
275
  }
249
276
  else {
250
277
  // T010 P2-6(修订):run 前轮换——每次 run 用全新浏览器会话(同 profile 登录态保留)。
@@ -252,7 +279,7 @@ export async function executeRun(ctx, args, runId) {
252
279
  // 失败仅日志(ensureBrowser 会按需拉起,不阻断)。
253
280
  if (ctx.config.edgePath.length > 0) {
254
281
  await rotateBrowserSession(ctx.config.cdpPort, ctx.config.edgePath, ctx.config.userDataDir).catch((e) => {
255
- console.log(`[flow-rpa] 会话轮换失败(浏览器保持,run 前重试): ${e instanceof Error ? e.message : String(e)}`);
282
+ console.log(`[flow-rpa] 会话轮换失败(浏览器保持,运行前重试): ${e instanceof Error ? e.message : String(e)}`);
256
283
  });
257
284
  }
258
285
  const { CdpPage } = await import('@qing3a/flow-rpa-engine/src/cdp/page.js');
@@ -260,6 +287,15 @@ export async function executeRun(ctx, args, runId) {
260
287
  await cdpPage.ensureBrowser(ctx.config.edgePath, ctx.config.userDataDir);
261
288
  page = cdpPage;
262
289
  }
290
+ // T035-1(S2):闸门 key 只在 run 起点解析**一次**,acquire 与 penalize 共用同一个 key。
291
+ // 优先级:① 调用方已解析(server/CLI 的 run 前 acquire 用过的那一个,零重复探测、零口径漂移)
292
+ // → ② 运行时页面 host(`page.currentUrl()`;mock 页面如实回 URL,真实 CdpPage 在会话轮换后
293
+ // 通常是 about:blank → 探不到就降级,闸门不会因此消失)→ ③ 流程声明的 navigate host
294
+ // → ④ `flow:<id>` 维度兜底 + 一行告警。任何情况下 key 都存在——闸门不再有「静默无 key」态。
295
+ const runtimeUrl = await observeRuntimeUrl(flow.id, () => page.currentUrl());
296
+ const gate = resolveGateKeyInRun(args.gateKeyResolved, runtimeUrl, flow.id, flow.steps);
297
+ if (gate.source === 'flow')
298
+ reportGateFallback(flow.id);
263
299
  // A1 ENV_CHECK 注入(D-V1):flow.json 顶层 envCheck → 页面检测配置;缺省不检测(D-V4)
264
300
  if (flow.envCheck) {
265
301
  const setEnvCheck = page.setEnvCheck;
@@ -291,6 +327,9 @@ export async function executeRun(ctx, args, runId) {
291
327
  onBehavior: (entry) => ctx.behavior.record(entry),
292
328
  // P2 区域识别规则(behavior.rules.json 随流程包分发;引擎零业务逻辑——缺失则不识别区域)
293
329
  behaviorRules: loadBehaviorRules(ctx.flowsDir, flow.id),
330
+ // T037-3(0.7.0)节奏层人格:本安装/本账号的翻页节拍区间(引擎护栏内取段;
331
+ // 流程显式 paceMs 优先,人格不得突破下限)。缺省节奏 → 引擎缺省 15~25s(行为不变)。
332
+ nextPagePaceMs: ctx.config.simProfile.rhythm?.nextPagePaceMs,
294
333
  // T015 P0-2:resume 起点(暂停点后继续,不重跑已完成步骤)
295
334
  ...(args.resume
296
335
  ? {
@@ -329,7 +368,15 @@ export async function executeRun(ctx, args, runId) {
329
368
  keywords: [],
330
369
  });
331
370
  ctx.store.saveRun(rec);
332
- ctx.runs.set(runId, { runId, status: 'failed', error: `流程中断(已落盘 ${events.length} 条步骤记录): ${e instanceof Error ? e.message : String(e)}`, owner: args.owner });
371
+ ctx.runs.set(runId, {
372
+ runId,
373
+ status: 'failed',
374
+ error: `流程中断(已落盘 ${events.length} 条步骤记录): ${e instanceof Error ? e.message : String(e)}`,
375
+ owner: args.owner,
376
+ origin,
377
+ gateKey: gate.key,
378
+ gateKeySource: gate.source,
379
+ });
333
380
  throw e;
334
381
  }
335
382
  // D-S1:DOM 快照采集(真实浏览器分支;finalize 可能已关 tab → 捕获异常,失败不阻断 run 落盘)
@@ -396,6 +443,9 @@ export async function executeRun(ctx, args, runId) {
396
443
  pauseReason: result.pauseReason,
397
444
  owner: args.owner,
398
445
  pauseGuidance: pauseGuidance(result.pauseReason, runId), // T027/B2:结构化指引(message 层)
446
+ origin,
447
+ gateKey: gate.key,
448
+ gateKeySource: gate.source,
399
449
  };
400
450
  ctx.runs.set(runId, pr);
401
451
  return pr;
@@ -419,6 +469,9 @@ export async function executeRun(ctx, args, runId) {
419
469
  error: runError,
420
470
  perfNote: perfCheck.message,
421
471
  owner: args.owner,
472
+ origin,
473
+ gateKey: gate.key,
474
+ gateKeySource: gate.source,
422
475
  };
423
476
  ctx.runs.set(runId, r);
424
477
  // D-C2(M5 半成品 #3 接通):run 结束后对带 cooldownKey 的步骤记账(key=flow#step#coolKey)
@@ -427,6 +480,33 @@ export async function executeRun(ctx, args, runId) {
427
480
  ctx.coolGate.record(`${flow.id}#${step.stepId}#${step.cooldownKey}`);
428
481
  }
429
482
  }
483
+ // 翻页防封护栏(2026-09-06,远程用户猎聘风控实证):翻过页的 run 记同站惩罚窗口——
484
+ // 下一个关键词的 run 至少再等 DEEP_CRAWL_PENALTY_SECS 秒(run 间常规节流 15~70s 不足以
485
+ // 拉开「连翻 N 页」的节奏)。计入失败 run(翻到一半失败同样已产生深翻页行为)。
486
+ // T035-1(S2):惩罚记在**本 run 的闸门 key** 上(与 acquire 同一个 key,见函数头 gate 解析)——
487
+ // 没有 navigate 步骤的自建流程同样进惩罚窗口,不再因「无 host」而整块跳过。
488
+ const flipped = result.steps.reduce((sum, s) => sum + (s.pagesFlipped ?? 0), 0);
489
+ if (flipped >= DEEP_CRAWL_MIN_PAGES) {
490
+ // 时长区间随机(150~300s):可观测停顿不得固定值(值域纪律 ①,ANTI-BAN-PRINCIPLES)
491
+ const penaltySecs = deepCrawlPenaltySecs();
492
+ ctx.siteGate.penalize(gate.key, penaltySecs);
493
+ console.log(`[flow-rpa] 深度翻页冷却: 本次 run 翻页 ${flipped} 页 → ${gateKeyLabel(gate)} 下次执行至少再等 ${penaltySecs}s`);
494
+ }
495
+ // 兜底计量(B 类捷径,ANTI-BAN-PRINCIPLES ②):记录本次 run 实际发生的非拟人兜底事件,
496
+ // 落 data/behavior/fallback-events.jsonl(追加式旁路台账;不写 run.json——data/ 记录格式冻结)。
497
+ // 目的:让「兜底率」可度量(0 是目标,>0 必须能解释),是 reactFallback/合成事件逐条消除的前提。
498
+ const fallbackHits = result.steps.flatMap((s) => (s.fallbacks ?? []).map((f) => ({ stepId: s.stepId, kind: f.kind, selector: f.selector })));
499
+ if (fallbackHits.length > 0) {
500
+ try {
501
+ const fbPath = join(ctx.store.root, 'behavior', 'fallback-events.jsonl');
502
+ mkdirSync(join(ctx.store.root, 'behavior'), { recursive: true });
503
+ appendFileSync(fbPath, JSON.stringify({ at: nowIso(), runId, flowId: flow.id, owner: args.owner, hits: fallbackHits }) + '\n', 'utf8');
504
+ }
505
+ catch {
506
+ // 旁路计量:写失败不影响 run
507
+ }
508
+ console.log(`[flow-rpa] 兜底计量: 本次 run 发生 ${fallbackHits.length} 次非拟人兜底(${[...new Set(fallbackHits.map((h) => h.kind))].join(', ')})——目标 0,见 docs/ANTI-BAN-PRINCIPLES.md`);
509
+ }
430
510
  // T020 P0-2:run 完成后顺带清理(超窗 runs 归档/删除 + 残留 tmp 清扫;失败不阻断收尾)
431
511
  await housekeepingAfterRun(ctx);
432
512
  // T010 P2-6:会话轮换已前移至 run 开始前(每次 run 新会话)——run 尾部不再轮换
@@ -591,17 +671,12 @@ export async function rotateBrowserSession(port, edgePath, userDataDir) {
591
671
  throw new Error(`浏览器新会话 ${port} 未就绪(${edgePath})`);
592
672
  }
593
673
  }
594
- /** 流程的目标站点 host(取首个 navigate 步骤的 url;无 navigate 返回 undefined —— 不节流 mock) */
674
+ /** 流程声明的目标站点 host(T035-1 = **全部** navigate 步骤里最后一个;无 navigate undefined) */
595
675
  export function flowHost(ctx, flowId) {
596
676
  const cached = ctx.registry.get(flowId);
597
677
  if (!cached)
598
678
  return undefined;
599
- const nav = cached.flow.steps.find((s) => s.type === 'navigate');
600
- const url = typeof nav?.params.url === 'string' ? nav.params.url : undefined;
601
- if (!url)
602
- return undefined;
603
- const h = hostOf(url);
604
- return h.length > 0 ? h : undefined;
679
+ return declaredNavigateHost(cached.flow.steps);
605
680
  }
606
681
  const queue = { running: false, pending: [] };
607
682
  /** 队列当前状态(自描述聚合用 D-S1;只读) */