# 移植与等价性记录(PORTING) 本仓库的前身是 Python 实现,现已收敛为 TypeScript 单一技术栈;Python 版整体移出、经审查后删除(历史归档见 [BACKUP.md](BACKUP.md))。这份文档保留移植期的工程记录:模块对照、等价性对拍、保真细节、 已知偏差,以及真实站点实测与由此产生的修正。**面向使用者的目标/功能/用法在 [README.md](README.md)。** ## 模块现状(Python 参考 → TS) | 模块 | Python | TS | 验证 | |---|---|---|---| | `extraction.ts`(元素模型/esig/firstIfSame/extractSlots/frame 钩子) | `layaapp/extraction.py` 纯逻辑 | ✅ | 单测 + 对拍 | | `extraction.ts`(浏览器层:一次 JS pass / shadow / frames / CDP 探测 / hint 回捞 / criteria 视图 / state_text) | `extraction.py` 其余 | ✅ | **6/6 夹具逐元素对拍** + 10 集成测试 | | `signals.ts`(5 算子信号语言) | `layaapp/signals.py` | ✅ | 8 单测 | | `rules.ts`(nav / affordance / hint 语法 + 词表) | `layaapp/rules.py` | ✅ | **57/57 对拍** + 17 单测 | | `plan.ts`(`PlanningRules` + `TaskPlan` 往返) | `layaapp/planner.py` | ✅(含 M2 修正) | 11 单测 | | `audit.ts`(Mock/LLM 审计) | `layaapp/audit.py` | ✅ | 7 单测 | | `picker.ts`(固定动作拾取器 + 三闸门) | `layaapp/picker.py` | ✅ | 8 单测 | | `model.ts`(模型运行时接缝) | pip `laya` 调用面 | ✅ | 脚本化替身 | | `laya-format.ts`(序列布局/温度分桶/置信度/ECE) | `laya/common.py` 推理侧 | ✅ | **9 序列逐 id 对拍** + 校准对拍(14 单测) | | `executor.ts`(执行循环全量) | `layaapp/executor.py`(825 行) | ✅(1117 行) | **4/4 场景对拍** + 16 真浏览器集成测试 | | `browser-env.ts`(跨平台 chromium 解析 + 持久化上下文) | `browser_env.py` | ✅ | 集成测试走真实 chromium | | `planner.ts`(`runPlan` / `runSubgoalWithRetries` / `LLMPlanner` / `ScriptedPlanner`) | `planner.py` 其余 | ✅(含 H1 修正) | 18 单测 | | `flow.ts`(spec 加载/校验/展开 + 迁移度量) | `transfer/flow.py` | ✅(含重复键修正) | 27 单测 | | `plan-cli.ts`(计划 CRUD + 运行) | `plan_cli.py` | ✅(含 M3 修正) | 11 单测 + `npm run plan-cli` | | `laya-onnx.ts`(ONNX + tokenizer.json 推理) | pip `laya` | ✅ | 12 单测(含与 Python 4 位小数对拍、单候选短路) | | `datasource.ts`(取数引擎) | `datasource.py` | ✅ | 单测 + 163 项对拍 | | `bench.ts`(回归矩阵运行器 + 消融开关 + 线上包) | `bench/run_bench.py` | ✅ | 8 单测(含 `BenchMiddleware.run === BrowserMiddleware.run` 不变量) | | `ip-config.ts`(IP 规划表 → 网络设置页批量配置) | `ip_config.py` | ✅ | 6 单测(含 4 行端到端:3 OK + 1 被设备页校验拒绝) | | `ctrip-data.ts` / `ctrip-task.ts`(确定性复刻站数据 + 独立判分 + 任务) | `ctrip/` | ✅ | 10 项对拍(dumpJs 与报告逐字节相等) | | `camera-entry.ts`(Excel 类型列路由 → 两套表单) | `camera_entry.py` | ✅ | 10 单测(含 4 行端到端) | | `transfer-data2..7.ts` + `transfer-task.ts`(六个复刻站库存 + 迁移运行器 + 度量) | `transfer/` 其余 | ✅ | 45 单测 + **67 项对拍(6 站 dumpJs 逐字节相等)** | | `path-a.ts` / `long-task.ts` / `tune-guard.ts` | `browser_picker_playwright.py` / `long_task.py` / `tune_guard.py` | ✅ | 7 单测 | | `picker-server.ts`(Path B:本地模型服务,`node:http` 零依赖) | `picker_server.py` | ✅ | 30 测试;与 MV3 扩展字段逐条对齐 | | `h5-flights.ts` / `live-probe.ts` / `live-capture.ts` / `ota-probe.ts` / `profiles.ts` | ctrip·transfer 的网络与探针工具 | ✅ | 26 离线测试 | | `tests/extension.test.ts`(MV3 端到端) | `test_extension_load.py` | ✅ | 3 测试实跑 + 跳过路径实测 | | `tests/laya-internals.test.ts`(`test_laya.py` 推理侧:ECE / `_to_internal` / render_options / 温度桶 / qtype 反查) | `test_laya.py`(推理侧) | ✅ | 16 测试 | | `qunar-probe.ts` / `qunar-task.ts` | `qunar/probe.py` / `qunar/qunar_task.py` | ✅ | 13 离线测试(实测边界见 `qunar/results/QUNAR_RESULT.md`) | 顶层 `auditor.py` / `browser_agent.py` / `browser_picker.py` / `task_planner.py` 在 Python 里是 2 行兼容 shim, 实现分别落在 `audit.ts` / `executor.ts` / `picker.ts` / `planner.ts`。 `tests/repo-integrity.test.ts` 把上面这张表变成**可检查的断言**:仓库里不得存在 `.py`, 六个对拍工具必须读入已入库的冻结语料、不得在运行时调用 Python,测试读取的实测记录必须在库里。 ## 等价性怎么证明 对拍语料由 **Python 参考实现**生成并入库(`tools/*-corpus.json`、`*-python.json`),TS 侧重放逐项比对; 生成器随参考实现一并移除——用被测实现自己生成期望就不是独立参考。 | 层 | 规模 | 结果 | |---|---|---| | 规则(hint 语法/nav/affordance/slots) | 57 例 | **逐例一致** | | 提取(`extract_elements` vs `extractElements`) | 6 夹具 | **逐元素一致**(ref/tag/label/data_test/href/frame) | | 执行器(`BrowserMiddleware.run`) | 4 场景 | **一致**(done / stop_reason 文案 / 步数 / 逐步 how+method) | | laya 序列与校准 | 9 序列 + 校准 | **逐 token id 一致** | | datasource 引擎 | 163 项 | **逐项一致**(含 report_markdown 全文) | | ctrip 用例 | 10 项 | **一致**(dumpJs 与报告逐字节相等) | | transfer 迁移层 | 67 项 | **一致**(6 站 dumpJs 逐字节相等) | ```bash npm run verify # typecheck + 全部测试 + 六层对拍 npm run test:parity # 只跑六层对拍 ``` ### 重新生成语料 语料已冻结、不能再生成(生成器随参考实现移除,见 [BACKUP.md](BACKUP.md));改动对拍覆盖范围需要自行重建期望值。 ## 移植中刻意保留的语义细节 - **CJK 词边界**:`affordanceRule` 要求 label 的 CJK 串是指令的"独立 token"。 - **`extractSlots` 的 Unicode 续字符**:用 `[\p{L}\p{N}_-]` 复刻 Python 的 Unicode `\w`。 - **属性值大小写折叠**(`net-CAM-1001` 事故);`firstIfSame` 的扫描序裁决。 - **`serializeState` / `renderCriterion` 必须复刻 Python `json.dumps` 的 `", "`/`": "`**:分隔符直接改变 token 数。 - **温度 clamp 的 `float()` 语义**:`null`/对象/空串回落到 1.0(JS `Number(null)=0` 会掉到下限)。 - `checkSignal`、`audit()`、构造函数在 TS 侧是 **async**(Node 的浏览器/HTTP I/O)。 ## Node 与 Python Playwright 的 API 差异(踩过并解决) 1. `page.url` / `frame.url`:Node 是**方法** → `urlOf()`; 2. `locator.first`:Node 是**方法**;`.all()`/`count()` 异步 → `firstOf()` / await; 3. `page.evaluate("(arg) => {...}")`:Node 只把**孤立函数表达式**字符串当函数调用;带前置语句的字符串会被 当表达式求值并返回 `undefined`(静默 0 元素);最简单的箭头字符串同样不可靠(camera-entry 里它把 `