> **状态(2026-10)**:本仓库现在是 **TypeScript 单一技术栈**。 > 下文描述的 Python 实现已整体移出并备份(见 [BACKUP.md](BACKUP.md)); > 文中的 `python xxx.py` / `pytest` 命令指向备份中的参考实现,对应的 TS 入口见 [README.md](README.md)。 > 设计规范、实测结论与用例记录照旧有效——TS 代码的注释大量引用它们。 # AmberFlow 总体架构 > 本文描述当前代码的设计意图与边界。历史迭代(每轮的失败模式、实测数字、修复依据)见 > `bench/OPTIMIZATION_REPORT.md`;给 agent 的硬约束清单见 `CLAUDE.md`。 ## 一、定位与核心契约 把浏览器自动化拆成 **System 2 编排 + System 1 执行** 两半: - **大模型负责编排,并把它固化成"冻结流程"**:有序子目标,每个子目标自带**可验证的完成信号**, 能点名目标时再给一行 `target_hint`。 - **laya(pip 依赖,421M 非自回归、单次前向出决策)负责按流程逐步精确执行**:每步只回答 "现在点哪个元素 / 这步危险吗"。 执行器的三条"永不"(整个设计的地基): 1. **永不重新规划** —— 计划是输入,不是建议; 2. **永不编造值** —— 填入表单的值只来自计划的 `slots` 数据袋(缺值就 `needs_value` 停下,如实报错); 3. **永不把模型的判断当完成** —— 只有 DOM 信号算完成(`is_done` 的 noul 仅作参考,且有信号时干脆不调用)。 ## 二、分层(单向依赖) ``` ┌── LLMPlanner (System 2:一次性编排 / 失败后重规划) 用户目标 / Excel 行 ─┤ └── PlanningRules (同一套规则:既渲染进 LLM prompt,又校验人工编辑) │ ▼ task_planner.py PlanningRules(一套规则: 进 prompt + 校验计划) · snapshot_text(给 planner 看的 页面渲染, 复用 criteria_view) · TaskPlan(JSON checkpoint/resume + slots 数据袋 + target_hint) · LLMPlanner(build_messages/make_plan/replan) · run_plan(整计划共享 ONE browser_session + 真 replan 接线) │ ▼ browser_agent.py BrowserMiddleware.run —— 唯一执行器 每步: extract_elements(无站点专属选择器) → 规则层 _nav_rule / _affordance_rule / target_hint(零模型调用) → 否则 laya predict_shortlist(k=8, 语义选择) → 置信度 floor → 风险闸门 is_risky → 可点性探针 → 执行 → inert 快照比对 → DOM done-signal │ ▼ auditor.py 事后审计(Mock / LLM):读整条 trace 判 APPROVE / REJECT │ ▼ laya (pip) Agent.predict / predict_shortlist · Router.route/predict(选 checkpoint) ``` `bench/run_bench.py` 的 `BenchMiddleware` **继承**(不复制)这个执行循环,只做两件事: 选 checkpoint、拨消融开关(rules on/off、anti-repeat、conf floor)。单测钉死 `BenchMiddleware.run is BrowserMiddleware.run`。 ### 2.1 包结构:`layaapp/` 就是应用层 扁平模块已收进一个显式分层的包,驱动脚本只依赖应用层入口: ``` layaapp/__init__.py 应用层公共 API(驱动只 import 这一个) ├── planner.py 规划:PlanningRules / TaskPlan(+slots) / LLMPlanner / run_plan ├── executor.py 编排:BrowserMiddleware(唯一执行循环)/ browser_session / 路由 ├── extraction.py 看:一次 JS pass(frames+shadow)/ criteria_view / hint 回捞 / esig ├── rules.py 决:字面映射(nav / affordance / hint_match)+ 词表 ├── audit.py 审:trace 审计(Mock / LLM) └── picker.py 动作:固定动作集拾取器 + 三闸门(Path A/B) ``` 依赖方向严格单向:`__init__ → executor → {extraction, rules, audit}`、`planner → {executor, extraction}`。 顶层同名模块(`browser_agent.py` 等)只剩**兼容 shim**,保证旧 import 与 `python browser_agent.py` 不炸。 ## 三、三条并行入口,共用一套核心 | 入口 | 决策方式 | 适用 | |---|---|---| | **自由元素发现栈** `browser_agent` / `bench` / `plan_cli` / `long_task` | 每步现场扫描 DOM(≤40 元素),规则/hint → laya | 任意页面、长任务 | | **固定动作栈** `browser_picker` + `browser_picker_playwright`(Path A) / `picker_server`+`extension`(Path B) | 9 个硬编码动作 + 三闸门(注入 noul / 不可逆 noul / 置信度) | 已知动作集、浏览器扩展场景 | | **数据驱动批处理** `camera_entry` | **按类型列命中 `TYPE_TO_FORM` 即定页**(零模型调用),类型空白/未知回落模型;填值 verbatim | Excel → 多页表单录入 | | **表格→设备配置** `ip_config` | 每行一份冻结计划(`TaskPlan.slots` 即数据袋),门户按序列号 hint 进设备页,点击前预填 ip/掩码/网关,保存走风险闸门,完成只看页面回显 | IP 规划表 → 摄像头网络设置页 | | **场景用例** `ctrip` | 一句中文需求 → 19 个子目标的冻结计划(每步一次点击 + 一条 DOM 信号 + 一个 hint),筛选/排序状态走 URL,站点自己的账本只由点击写入,再用**独立口径**判分 | 携程式「搜票 + 搜酒店 + 记录」用例(线上被封锁,跑本地复刻站) | | **流程即配置** `transfer` | 流程写进 `transfer/specs/.json`:**阶段列表(stage/repeat/steps)本身就是骨架** + 每步的 hint(s)/signal/`when`/`retries`/URL 模板 + 数据袋;加载器只做校验/展开,`--emit-plan` 出 `TaskPlan` JSON | 迁移到布局不同的同类站点:#2 英文/下拉+单选/网格/分页,#3 表格/滑块,#4 表单搜索,#5 **两页向导**,#6 **条件子目标**,#7 **重试(冷启动)** | ## 四、关键设计决策(每条都有实测依据,不是风格偏好) ### 1. 分工边界:字面映射交给规则,语义交给 laya - `_nav_rule`:href 匹配,**必须出现导航动词**(否则 "add to the shopping cart" 会命中 cart.html); - `_affordance_rule`:`data-test` 前缀匹配 / label 全词匹配 / CJK 词边界; - `target_hint`:计划点名的目标,**规则直接命中,零模型调用**,且命中"不可区分"的多个候选时取扫描序第一个; - 实测根据:zero-shot 元素选择在歧义页面不可用(quotes 登录页正确目标 0.04 vs logo 0.59;关掉规则 3/15 且超时)。 ### 2. 完成判定:DOM 信号是唯一真相 信号语言五种:`url_contains` / `css_exists` / `css_text_equals` / `css_text_contains` / `css_value_equals`。 真站点优先 `css_text_contains`(zh.wikipedia 的 `

` 是"艾伦·图灵编辑")。 ### 3. 安全:分层风险 + 独立审计 风险 = **确定性关键词规则 OR 模型投票 ≥0.6**(基座风险头随措辞翻转),命中**只要求确认不阻断**; 审计是独立的 System 2 作业(`MockAuditor` 离线 / `LLMAuditor` 走 OpenAI 兼容端点)。 ### 4. 上下文工程(把页面塞进 512–1024 token) - **只喂当前子目标**,绝不喂整条长目标(实测置信度 0.42 → 0.16); - `state_text` 关键词重叠压缩(budget 320),不用 embedding; - `aria_snapshot` 提供 role 归一化,但**保留自家 label 与 `[test=..] -> href` 后缀**(纯 aria 视图实测会把 logo 顶到第一); - `MAX_ELEMENTS=40`:**主内容优先**分层(`main/article` 且不在 `nav/aside/footer/header`),并且 **预算永远不能吃掉计划已点名的目标** —— `hint_lookup()` 按 hint 语法回捞。 - **抽取是"一次 JS pass"**:tag/label(含 `