@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.
- package/README.md +16 -13
- package/dist/cli.d.ts +4 -2
- package/dist/cli.js +83 -22
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +5 -0
- package/dist/config.js +22 -2
- package/dist/config.js.map +1 -1
- package/dist/gate-key.d.ts +65 -0
- package/dist/gate-key.js +93 -0
- package/dist/gate-key.js.map +1 -0
- package/dist/gate-runtime.d.ts +27 -0
- package/dist/gate-runtime.js +44 -0
- package/dist/gate-runtime.js.map +1 -0
- package/dist/registry.d.ts +33 -2
- package/dist/registry.js +94 -10
- package/dist/registry.js.map +1 -1
- package/dist/runner.d.ts +30 -3
- package/dist/runner.js +89 -14
- package/dist/runner.js.map +1 -1
- package/dist/server.js +29 -11
- package/dist/server.js.map +1 -1
- package/dist/session-rotation.d.ts +58 -0
- package/dist/session-rotation.js +61 -0
- package/dist/session-rotation.js.map +1 -0
- package/dist/status.d.ts +51 -0
- package/dist/status.js +117 -41
- package/dist/status.js.map +1 -1
- package/package.json +2 -2
- package/skill/flow-rpa-engine.md +154 -37
- package/starter/baseline.json +17 -0
- package/starter/liepin_search_resumes/flow.json +78 -54
|
@@ -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"}
|
package/dist/registry.d.ts
CHANGED
|
@@ -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
|
|
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 {
|
|
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
|
-
/**
|
|
6
|
-
|
|
7
|
-
|
|
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(
|
|
12
|
+
raw = readFileSync(join(flowsDir, BASELINE_FILE), 'utf8');
|
|
11
13
|
}
|
|
12
14
|
catch {
|
|
13
|
-
return undefined; //
|
|
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
|
|
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
|
|
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
|
}
|
package/dist/registry.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"registry.js","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;
|
|
1
|
+
{"version":3,"file":"registry.js","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"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"}
|
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
|
|
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
|
-
/**
|
|
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
|
|
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]
|
|
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, {
|
|
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
|
-
/**
|
|
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
|
-
|
|
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;只读) */
|