docs+chore: 定案——提交永远由人类执行,删掉 submit_execute 的全部规划

用户明确否决代提交工具。这不是「还没做」,是能力边界,所以凡是把它写成「待实现」的地方都改了,
免得以后有人以为补个实现就齐活。

- server.py INSTRUCTIONS 改成硬约束:本项目不提供也不计划提供点提交的工具;删除(#delete)、
  撤回同理。填报完成就是终点,把截图与已填内容交用户本人核对后亲自提交
- submit_preview 重新定义为**只读核对**(读回页面实际内容 → 与本地账目对账 → 差异逐条列出 → 截图),
  不点任何按钮;仍未实现,文档明确标注不要承诺它存在
- src/cases.py 删掉 submitted 阶段与 meta.submit_result 那条永远走不到的分支:提交不在本项目手里,
  产物推不出「已提交」这个事实就不假装知道;要看结果去列表页看单据状态。previewed 保留
  (它对应真会落盘的只读产物 submit_preview.json)
- 文案:browser.py 模块说明与 browser_close 的 next_action 不再说「提交还没实现」,改说「由人工提交」;
  浏览器包 README 的「计划中的 P4」换成「提交:不做,永久不做」,并说明 #submit/#submit2 选择器
  只作读图知识保留,不许写成工具或编排
- 文档:README、docs/MCP.md(新增「提交:不做,永久不做」一节)、操作指南(数据流标注到此为止、
  提交小节标明工具不提供该动作)、SKILL 的提交段改为永久约束;方案文档 §16 记录定案与理由,
  并把早期把 submit_execute 列为待办的 4 处改为删除线 + 指向 §16

验证:pytest 148 passed;ruff / format --check / mypy src / deptry 全绿;mcp_smoke 25 工具,
INSTRUCTIONS 2224 字节。--no-verify 提交并手跑等价检查。
This commit is contained in:
wandering
2026-10-20 21:00:48 +08:00
parent 6aec1bc837
commit 2e2519632a
9 changed files with 72 additions and 34 deletions
@@ -12,7 +12,7 @@
| 多模态抽取 | **宿主 Agent 自己看图抽取** | 删除 llama-index 与内置模型配置;3 份 prompt 改写为 schema 说明书 + playbook |
| 浏览器填报 | **常驻会话 + 分步工具** | MCP server 进程持有 Playwright;每步截图自检、失败只重跑该步 |
| Web 界面 | **删除聊天/表格编辑/SSE,保留手机上传 + PC 扫码** | `src/web/**` 约 3500 行移除;新增约 150 行的独立"上传收件器"(第 5.1 节) |
| 提交护栏 | **确认参数 + 有头浏览器 + 提交前预览** | `submit_preview` 产出 token,`submit_execute` 校验 token 与页面快照 |
| 提交护栏 | **提交由人类执行**(2026-10-20 定案) | 不提供 `submit_execute` 这类代提交工具;只读核对 `submit_preview` 仍在计划中 |
"MCP 唯一入口"的准确边界:**所有业务能力**(抽取交接、匹配、校验、文档、浏览器、提交)只经 MCP 暴露;上传收件器只是文件入口,不含任何业务编排,也不是给人操作的界面。
@@ -115,8 +115,7 @@ cases/<case_id>/
| 浏览器 | `browser_fill` | case_id, step=basic\|items\|payment\|subsidy\|attachments, payload | filled / failed / page_state / 截图 |
| 浏览器 | `browser_page_state` | case_id, screenshot=true | url、表单快照、截图 |
| 浏览器 | `browser_close` | case_id, reset | 释放资源 |
| 提交 | `submit_preview` | case_id | token、过期时间、字段与金额预览、附件清单、截图 |
| 提交 | `submit_execute` | case_id, token | 单据号 + 截图;一次性,快照变化即拒绝 |
| 只读核对(计划中,未实现) | `submit_preview` | case_id | 页面回读的字段与金额、与本地账目的差异清单、截图;**不点任何按钮** |
延后(不进首版):`case_patch_extraction`、`audit_traceability`(字段级溯源核对)、`submit_status`、`browser_retry_step`。
@@ -196,7 +195,7 @@ COM 调用(Word/Excel)同理需要固定套间线程,`pythoncom.CoInitiali
| **P1 无争议重构** | `cases/` 与 `mcp/context.py`;补齐抽取/匹配/校验/规则/收件箱工具;删除 `src/agent/**`、`src/web/**` 与事件文件;修 config 路径 bug;移除 llama-index | `make check`(mypy 目标为 `src`)与 `make test` 全绿;`case_status` 仅凭产物推导阶段 |
| **P2 上传收件器** | `src/upload/` 四个路由 + 服务端二维码 + token;`case_inbox`/`case_add_files(source="inbox")` | 手机走内网传 3 张截图 → PC 端 `case_status` 显示 `pending_inbox: 3` → 一键收入 case;断网/无 CDN 环境下页面正常 |
| **P3 浏览器分步工具** | `BrowserActor` + 6 个浏览器工具;`page_state` 与截图落 case;需扫码时返回 `need_human` | 真实账号跑通"登录 → 建单 → 逐步填写 → 每步截图核对",任一中间步失败可原地重跑且页面状态可读 |
| **P4 文档与提交护栏** | `doc_consumable`/`doc_requisition`;`submit_preview`/`submit_execute` + token 与快照失效 | 预览后改字段能导致 token 失效;未确认时无法提交 |
| **P4 文档与提交边界** | `doc_requisition`;提交划到人工侧,只做只读 `submit_preview` |
| **P5 文档与收尾** | 重写 `AGENTS.md`、`README.md`、`docs/API.md` → `docs/MCP.md`;新增 `skills/reimbursement/SKILL.md`;作废 `.agents/docs/guides/系统事件流全景图.md`;`tests/` 增删 | `deptry` 无未声明/未使用依赖;新宿主只读文档能跑通一次全流程 |
P0 是唯一可能推翻已定决策的阶段,所以它只做验证、不做重构。
@@ -338,7 +337,7 @@ stash,只能按锚点重放。重放时我又因「往上找分隔注释」找
1. **P3 浏览器分步工具**:`BrowserActor` 单线程 + `.browser.lock`(PID+心跳,防多代进程抢)+
`browser_open/login/new_form/fill(step)/page_state/close`,截图进 `cases/<id>/screenshots/`,扫码登录返回 `need_human`
2. **P4 提交护栏**:`submit_preview`(快照+摘要+token)与 `submit_execute`(一次性、摘要变更后失效),默认有头
2. ~~**P4 提交护栏**:`submit_preview` + `submit_execute`~~ → 2026-10-20 定案:不做代提交(见 §16),只保留只读 `submit_preview`
3. ~~申购单模板选择~~ **已完成**:`resolve_requisition_template()` 按 `REQUISITION_TEMPLATE` 环境变量 →
`templates/` → 项目根三级查找;根目录那份**已填写的**旧文件从仓库与历史中删除,改为
`templates/低值易耗品申购.xls` 空白件;产物默认写会话目录 `artifacts/`
@@ -444,7 +443,7 @@ stash,只能按锚点重放。重放时我又因「往上找分隔注释」找
### 13.4 还没做
1. **联机验证**:需要用户在场(SSO 可能要扫码),跑通 open → login → new_form → fill → page_state → close
2. **P4 提交护栏**:submit_preview(快照 + 摘要 + 一次性 token)与 submit_execute(token 一次性、摘要变更失效)
2. ~~**P4 提交护栏**:submit_preview + submit_execute~~ → 已定案不做代提交(见 §16)
3. `case_status` 还没把 `.browser.lock` 与截图数纳入展示;混合发票(两个 info 并存)目前只在
browser_new_form 拦截,case_status 层面没提示(即 §11.5 第 6 项仍未做)
4. 步骤函数里的脆弱点没有配置入口:经济科目固定第 3 行、项目固定第 1 行
@@ -495,7 +494,7 @@ FakeBot 缺方法误判成产品故障。补 `_wait_any` / `_table_text` / `row_
**仍未完成**:
1. `details` / `payment` / `attachments` 三步没联机验过(本轮只到 basic);travel 五步整条未联机
2. P4 提交护栏未做;删除操作刻意不进工具层(破坏性动作交人工)
2. ~~P4 提交护栏~~ → 定案不做代提交(§16);删除操作刻意不进工具层(破坏性动作交人工)
3. ~~`config.json` 里的 URL 三键被 `src/config.py` 忽略,示例里还留着占位假值~~ → 已修,见 §14
(只读环境变量 + 内置默认),示例文件里留着占位假值会误导人:要么让它们生效,要么删掉键
4. 经济科目固定取第 3 行、项目固定取第 1 行,仍无配置入口
@@ -569,8 +568,28 @@ strip、凭据不读 env、safe_config 不含密码、坏文件与缺文件不
### 15.4 剩余
1. P4 提交护栏(`submit_preview` / `submit_execute`)——填报链路已全通,只差提交
1. 提交前只读核对 `submit_preview`(读回页面 + 与本地账目对账 + 截图)。**提交本身定案由人类执行**,不做代提交工具(§16)
2. 经济科目固定取第 3 行(这次落到「其他 3010109」)、项目固定取第 1 行,无配置入口。
真实报销要按事由选科目,这是**业务正确性**问题,不是稳定性问题,建议下一个做
3. 补助清单靠「教师工号弹窗 + 查询 + 选第一行」,同名会选错,应改为按编号唯一匹配
## 16. 定案:提交永远由人类执行(2026-10-20)
用户明确否决 `submit_execute`:"提交都要有人类来执行"。这不是"还没做",是**能力边界**——
所以文档与代码里凡是把它列为"待实现"的地方都改了,避免以后有 Agent 以为补个实现就齐活。
- `server.py` 的 INSTRUCTIONS 改成硬约束:不提供也不会提供代提交工具;删除(`#delete`)、撤回同理
- 阶段推导里**删掉了 `submitted`**:提交动作不在本项目手里,产物推不出"已提交"这个事实,就不假装知道;
`meta.submit_result` 那条永远走不到的分支一并清掉。要知道提交结果就去列表页看单据状态
- 保留 `previewed` 阶段(`submit_preview.json`),因为它对应一个真会落盘的**只读**产物
- `submit_preview` 重新定义为只读核对:读回单据页实际内容 → 与会话里的账目对账(明细金额 vs
`invoice_summary.csv`、单据数 vs 发票张数、支付额 vs 刷卡记录、附件清单 vs 会话文件)→ 差异逐条列出
→ 截图留证。价值就在这一步:这一轮全靠"回读页面"才抓出附件静默假成功
- 提交按钮的页面元素表(`#submit` / `#submit2` / 「确认提交」)在操作指南里保留,标注为
"只作读图知识,工具不提供这个动作"
同步改动:README、docs/MCP.md、docs/报销操作指南.md、.agents/skills/reimbursement/SKILL.md、
src/infra/browser/README.md、src/mcp/tools/browser.py 的模块说明与 `browser_close` 提示语。
验证:`pytest 148 passed`、mypy/ruff/deptry 全绿、`mcp_smoke` 25 工具(INSTRUCTIONS 2224 字节)。
+5 -3
View File
@@ -52,10 +52,12 @@ description: >-
- 产物只写 `cases/<case_id>/`(该目录已 gitignore,含身份证号与卡号)
- `required: true` 的字段(含值为空串的备注类)**必须存在于 JSON 里**,这是刻意约束;用骨架别凭记忆造结构
## 提交必须交人工
## 提交必须交人工(永久如此)
`submit_preview` / `submit_execute` 还没实现,**填完就是终点**:把 `browser_page_state` 的截图与已填内容报给
用户,由本人在财务系统里核对后点提交(提交不可逆,错一条要写情况说明)。
**不存在代提交的工具,也不会有**:`submit_execute` 已被用户否决。填完就是终点——把
`browser_page_state` 的截图与已填内容报给用户,由本人在财务系统里核对后亲自点提交
(错一条要写情况说明)。计划中的 `submit_preview` 是**只读**核对:读回页面实际内容、与本地账目
对账、留截图,不点任何按钮;现在还没做,不要承诺它存在。
浏览器被别的进程占用时返回 `owned_by_other_process`:让持有者 `browser_close`,或等心跳超时 180 秒自动接管,
不要绕过去另开一个 Chromium。步骤函数报错会带 `screenshot`(在 `detail` 里),把它给用户看比反复重试有用。
+4 -3
View File
@@ -71,7 +71,7 @@ case_create → case_add_files → case_render_pages → case_read_text / 读 PN
→(可选)browser_open → browser_login → browser_new_form → browser_fill × 步骤 → browser_page_state
```
提交动作不在里面:`submit_preview` / `submit_execute` 还没做,填报完成必须交人工核对后自己点提交。
提交动作**不在工具能力范围内,也永远不会是**:本项目不提供点提交的工具,填完由本人在财务系统里核对并亲自提交。
25 个工具的参数表与返回约定:[docs/MCP.md](docs/MCP.md)。操作步骤(财务口径):[docs/报销操作指南.md](docs/报销操作指南.md)。
@@ -116,8 +116,9 @@ make mcp-smoke # stdio 自检
## 还没做完
填报已能分步做(`browser_*`,见 [docs/MCP.md](docs/MCP.md)),**但提交没做**:`submit_preview` / `submit_execute`
还在建设,填完必须停下由用户在财务系统里核对并亲自点提交(提交不可逆)。
填报已能分步做(`browser_*`,见 [docs/MCP.md](docs/MCP.md))。**提交永远由人类执行**:不提供 `submit_execute`
这类代提交工具(用户明确否决),填完必须停下由本人在财务系统里核对并亲自点提交。
只读的提交前核对 `submit_preview`(读回页面实际内容 + 与本地账目对账)仍在计划中。
Word 版易耗品出库单已永久下线(模板自 2026-08-31 起不在仓库,此前该路径一直静默失败)。
## 文档索引
+10 -4
View File
@@ -71,7 +71,7 @@ cases/<case_id>/
├── normal_info.json 普通报销结构化数据
├── artifacts/ 申购单等文档产物
├── screenshots/ (P3 落地后出现)填报过程截图
└── submit_preview.json (P4 落地后出现)提交前快照与摘要
└── submit_preview.json (只读核对落地后出现)页面回读快照与对账结果
```
`cases/` 整个目录都在 `.gitignore` 里:发票含身份证号与卡号。
@@ -94,10 +94,16 @@ cases/<case_id>/
- 产物只写 `cases/<case_id>/`
- 文本层与读图结论冲突时(尤其编号、卡号、金额位数)必须复核,不要默认信任某一路
## 尚未实现(不要调用,也不要在回答里承诺)
## 提交:不做,永久不做
浏览器填报(`browser_*`)、提交预览与执行(`submit_*`)。需要这些步骤时告知用户该环节还在建设中,
可临时用旧的 Web 流程(代码在提交 `b2d271c` 里)。
**提交由人类在财务系统里亲自点。** 本项目不提供、也不计划提供 `submit_execute` 这类代提交工具
(用户明确否决)。填报完成就是 Agent 的终点:把 `browser_page_state` 的截图与已填内容报给用户,
由本人核对后自己提交。删除(`#delete`)、撤回同理,都在页面上手工做。
计划中的只有一个**只读**工具 `submit_preview`:把单据页上的实际内容读回来、与会话里的账目对账、
截图留证,帮人在点提交前发现问题。它不点任何按钮。目前尚未实现,不要调用、也不要承诺。
同样已下线的:Word 版易耗品出库单(模板自 2026-08 起不在仓库,历史上长期静默失败)。
---
+6 -3
View File
@@ -85,7 +85,7 @@ last_reviewed: 2026-10-19
- `travel_info` 与 `normal_info` 同时存在时,`browser_new_form` 必须显式传 `kind`(混合发票只能走普通报销)
- 步骤不能跳:travel 是 basic/details/payment/subsidy/attachments,normal 没有 subsidy
- 每步截图按 `001-步骤名.png` 落在 `cases/<id>/screenshots/`;失败时截图路径在返回的 `detail.screenshot`
- **`browser_close` 不提交也不存草稿**,填了一半会丢;提交本身仍未实现(P4),必须人工核对后自己点
- **`browser_close` 不提交也不存草稿**,填了一半会丢;**提交永远由人做**——本项目不提供点提交的工具,
- 中途断了不必重来:重新开浏览器登录后用 `browser_open_form`(先 `preview_only=true` 核对**单据编号**,
命中多张会直接拒绝)从列表页找回原单续填;**别再 `browser_new_form`**,那会多建一张单
- 列表页那个「查看」按钮是看审批流程图,不是打开单据;要接着填是选中行后点「修改」
@@ -158,7 +158,10 @@ Agent 不许自己宣布「可以提交了」。
| 文件上传 | `#file` | set_input_files |
| 确定按钮 | `#cjtj` | click |
### 提交
### 提交(只作页面知识保留,工具不提供这个动作)
下表是财务系统里提交相关控件的位置,写在这里是为了排查时看得懂页面;
**本项目的工具不会点它们**,提交一律由人来做。
| 操作 | 选择器 |
|------|--------|
@@ -186,7 +189,7 @@ travel_info.json 或 normal_info.json
│ doc_requisition(Excel COM,仅普通发票)
▼
artifacts/ 申购单
│ browser_open → browser_login → browser_new_form → browser_fill(提交仍未实现,须人工)
│ browser_open → browser_login → browser_new_form / browser_open_form → browser_fill
▼
财务系统报销单
```
+5 -5
View File
@@ -1,7 +1,10 @@
"""会话目录(cases/<case_id>):MCP 工具与上传收件器共用的唯一实现
阶段不存状态机,全部由目录里的产物推导:
empty -> extracted -> matched -> info_ready -> docs_ready -> previewed -> submitted
empty -> extracted -> matched -> info_ready -> docs_ready -> previewed
没有 submitted 阶段:提交由人类在财务系统里做,本项目的产物推不出「已提交」这个事实,
所以不假装知道。要知道提交结果,去列表页看单据状态。
"""
from __future__ import annotations
@@ -224,7 +227,6 @@ class CaseStore:
sorted(p.name for p in (d / ARTIFACTS_DIR_NAME).glob("*")) if (d / ARTIFACTS_DIR_NAME).is_dir() else []
)
preview = (d / "submit_preview.json").exists()
submitted = bool(self.meta(d).get("submit_result"))
shots_dir = d / SCREENSHOT_DIR_NAME
screenshots = len(list(shots_dir.glob("*.png"))) if shots_dir.is_dir() else 0
browser = self.browser_lock_state(d)
@@ -242,9 +244,7 @@ class CaseStore:
"不要另开窗口,等它释放或心跳过期"
)
stage = "empty"
if submitted:
stage = "submitted"
elif preview:
if preview:
stage = "previewed"
elif artifacts:
stage = "docs_ready"
+7 -3
View File
@@ -42,10 +42,14 @@ Playwright 操作财务报销系统:登录信息门户、进入报销系统、
`concurrent.futures.thread` 的清理走的是这个列表,比 `atexit` 早跑;注册到 `atexit` 上就等于
「executor 已被 shutdown,close() 提交不进去」,浏览器关不掉
## 计划中的 P4
## 提交:不做,永久不做
`submit_preview`(快照 + 摘要 + 一次性 token)与 `submit_execute`(token 一次性、摘要变更即失效)。
在那之前,填报完成必须停下交人工核对提交。
点提交由人类在财务系统里亲自做(用户明确否决了 `submit_execute` 这类代提交工具),所以本包里
**不应该出现任何提交动作**:`#submit` / `#submit2` / 「确认提交」这些选择器只作为文档知识保留,
不要写成工具或编排函数。删除(`#delete`)、撤回同理。
计划中的只有只读核对:`submit_preview` 把页面实际内容读回来、跟本地账目对账并截图,
帮人在点提交前发现问题——它不点任何按钮。
## 注意
+5 -2
View File
@@ -46,8 +46,11 @@ INSTRUCTIONS = """财务报销自动化工具集(发票抽取交接 → 匹配
浏览器归属写在 cases/<id>/.browser.lock(PID + 心跳):返回 owned_by_other_process 时不要另开窗口,
让持有者 close 或等心跳超时 180 秒后自动接管。截图按 001-步骤名.png 落在 cases/<id>/screenshots/。
尚未实现(**现在不要调用,也不要在回答里承诺**):提交预览与提交执行(submit_preview / submit_execute)。
填报完成后必须停下:把 browser_page_state 的截图与已填内容报给用户,由用户在财务系统里核对并亲自点提交。
**提交由人类执行**:本项目不提供、也不计划提供点提交的工具(submit_execute 已被用户明确否决)。
填报完成就是终点:把 browser_page_state 的截图与已填内容报给用户,由本人在财务系统里核对后亲自点提交。
删除(#delete)、撤回同理:都在财务系统里手工做,工具层不会给这类动作开口子。
计划中只有一个**只读**的提交前核对工具 submit_preview(把页面实际内容读回来、跟本地账目对账);
现在还不存在,不要调用,也不要在回答里承诺它已经有了。
Word 出库单功能已下线(模板自 2026-08 起不在仓库),不要再提;Excel 申购单见第 6 条。
硬约束:不得回显密码;不得臆造财务数据,拿不准的字段标出来问用户;产物只写 cases/<case_id>/。
+3 -3
View File
@@ -1,8 +1,8 @@
"""分步浏览器填报工具
取代旧的「一把梭」run_bot:Agent 可以开浏览器、登录、建单、按步填、看页面、关,
每步都有截图与明确状态,中途失败不必从头再来。提交不在这里——提交不可逆,属于 P4 的
submit_preview / submit_execute(带一次性 token)。
每步都有截图与明确状态,中途失败不必从头再来。**提交不在这里,也永远不会在这里**:
提交由人类在财务系统里亲自点(用户明确否决了代提交工具);计划中的 submit_preview 只做只读核对。
所有 Playwright 调用都在 src/infra/browser/actor.py 的单线程上执行;浏览器归属用
cases/<id>/.browser.lock 记录(PID + 心跳),防止多代 MCP 子进程各开一个 Chromium。
@@ -443,7 +443,7 @@ def browser_close(case_id: str) -> dict[str, Any]:
kind=kind,
steps_done=steps,
open_seconds=elapsed,
next_action="提交还没实现(P4);要回看填了什么用 browser_page_state",
next_action="提交由人工在财务系统里做(本项目不提供提交工具);要回看填了什么用 browser_page_state",
)