Commit Graph

23 Commits

Author SHA1 Message Date
wandering d2fcedc6cd fix(requisition): 申购单不再收录会议费/会务费/培训费发票
用户按 2026-10-21 定案提的第 1 条:会议费这类因出差发生的普通发票并入差旅单的「会务费培训费」栏,
但 doc_requisition 之前照收所有 invoice_type=general 的发票——一旦点了申购单,1500 的会议费会被列成
"采购物品"。

- load_requisition_items 加 skipped_conference 出参:命 is_conference_like(src/core/expense_kind.py,
  唯一口径,认 item_name 与 remark 两个通道)的发票直接跳过,并把名字回传给调用方
- doc_requisition 返回里带 skipped_conference,用户能看出"少的那几张去哪了";不算错误、不影响生成
- CLI 入口同步打印被跳过的条目
- tests/test_requisition.py 补 3 例:会议费被剔/remark 里写培训费也被剔/不传出参照样能工作
- 文档:工具 docstring 与 src/infra/documents/README.md 同步,mcp-docs 重生成工具表

验证:pytest 全绿(test_requisition 28 例),ruff / format / mypy / deptry / mcp-smoke / docs-check /
skill-check 全绿。--no-verify 提交并手跑等价检查。
2026-10-21 13:50:05 +08:00
wandering 0d9cc7b4ca feat(core): 报销口径与文本宽度两道闸——会议费并入差旅单,超宽文本不再静默 500
这一批是 2026-10-21 正式填报踩出来的两类问题,一起收进来。

**一、报销口径只在一处判定(新增 src/core/expense_kind.py)**

用户定案:因出差发生的会议费/会务费/培训费这类普通发票**并入差旅单**(明细的会务费培训费栏、
会议通知当附件),不拆成两张单、也不因此放弃差旅补助;只有货物类(易耗品/材料/配件)才走
普通报销 + 申购单。以前 case_status、validate_info、browser_new_form 三处都写"混合发票只能走
普通报销",等于把这条合法路径说成错的,Agent 照做就会白拆一张单还漏补助。现在 is_conference_like /
goods_invoices 是唯一判定,工具提示与 case_status 的 warnings 都由它驱动。

**二、文本宽度闸:自由文本超宽 = 无提示 500(新增宽度口径 + 两道预检)**

联机实测:差旅明细备注写到 101 字,POST 回 500 且页面无任何提示、弹窗停在原地,现场看着像"脚本没点着
确定";同一张单 97 字(宽度 130)却存进去了,上限夹在两者之间且后端不肯说。于是取汉字/全角记 2、
字母数字记 1 的"宽度",validator.TEXT_WIDTH_LIMIT=100(≈50 汉字,同时满足"100 字符"与"131 字节"
两种可能口径),普通报销说明另用 DESCRIPTION_WIDTH_LIMIT=40。拦截点两处:规则里的 max_len_bytes
(validate_extracted_info 的 missing_fields 会写明"过长:宽度 N > 上限 M")与 browser_fill 在 POST 前的
_text_limit_problems(失败返回 too_long 列出超长字段)。

**三、交通工具代码表挪进 core(新增 src/core/vehicles.py)**

财务系统 #jtgj 的 value 表与中文别名(train→火车 等)从填报层挪到 core,一处三用:填报取 value、
体检判合法、报错给候选。老写法 if vehicle in vehicle_map 遇英文枚举是**静默不选**且仍返回成功,
直到提交被必填挡回来——现在认不出的值直接报错。

其余:cases.goods_general_items 给 case_status 补"有 N 张货物类普通发票"的 warning;
travel.py 改用 vehicles 并显式报错;config/validation_rules.json 加 max_len_bytes;
INSTRUCTIONS/SKILL/判读参考/操作指南同步口径与宽度提示。新增 tests/test_text_limits.py 与
tests/test_vehicles_and_scope.py。

验证:ruff / format / mypy src / deptry / pytest / mcp-smoke / docs-check / skill-check 全绿。
--no-verify 提交并手跑等价检查。
2026-10-21 13:49:12 +08:00
wandering 44f6466e8b fix(upload): 第二个按钮改名为「选择已有文件」,不再承诺相册
用户实测:去掉 accept 后能选到文件了,但那个入口开的是系统文件选择器,不是相册网格——
「从相册或文件选择」是我按规范许的愿,设备不给。用户接受"只有文件也行,照片在里面能找到",要求把名字改实。

- 按钮改为「选择已有文件」;顶部说明改为「拍一张,或从手机里选已有的文件」
- 按钮下补一句:照片在文件选择器里能找到(多在 图片/DCIM/Download 目录)
- 两个测试文件都加锁:断言按钮文案是「选择已有文件」且 HTML 里不出现「从相册」「相册或文件」——
  防的是以后有人把承诺写回文案,不是格式检查
- 文档:upload README 入口表改名;操作指南 FAQ 加一行「没有相册页签怎么办」;方案文档 §22 记教训

连着三条同类教训(capture 挂错、accept 被路由到相机、按钮名过度承诺)指向同一件事:
移动端网页的能力边界由设备与 WebView 决定,文案只能写实测到的行为,不能写规范里的应然。

pytest 164 passed;ruff / format / mypy / deptry / docs-check / skill-check 全绿。--no-verify 提交。
2026-10-21 11:18:47 +08:00
wandering bcf35b2803 fix(upload): 相册入口去掉 accept 并改用 label 原生激活
用户二次实测:拆了两个 input 后「从相册或文件选择」仍进相机。先排除自己写错——pick-gallery 绑的确实是
#gallery、capture 只有一处且在 camera 段。真因是相册入口带 accept="image/*,.pdf,...":
**微信内置浏览器与部分国产 ROM 会把这种 accept 直接路由到相机**,不给相册/文件列表。

- 相册入口去掉 accept,只留 multiple:让系统给完整选择器
- 类型限制交给服务端白名单:不支持的扩展名 415 带原因,页面状态行如实显示
- 两个入口改为 <label for> 原生转发,删掉 JS .click():少一类 WebView 手势问题
- 回归锁补进 tests/test_upload_page.py:camera 段有 capture、gallery 段既无 capture 也无 accept、
  HTML 不出现 setAttribute 与 .click()、两个 input 共用 uploadFiles
- 文档:upload README 用表格写清两个入口的属性与理由;操作指南 FAQ 两行;方案文档 §21

教训:这次不是代码错,是按规范写的属性被某个浏览器解释成另一种意思。移动端表单以最保守写法为准。
用户需重启自己的收件器加载新模板,手机侧重开上传页。
pytest 全绿;ruff / format / mypy / deptry / docs-check / skill-check 通过。--no-verify 提交。
2026-10-21 11:08:02 +08:00
wandering 787f259e38 fix(upload): 手机页拆成拍照与选文件两个入口,并堵住重复启动导致的孤儿进程
用户实测报回:手机上传只能拍照,选不了相册与已有文件,历史版本可以。根因在 src/upload/page.py——
只有一个 input,点击回调里 setAttribute("capture","environment") 无条件加在它身上,按规范这就是
强制调起相机。它是收件器诞生那次(14b6b4c)带进来的,旧 Web 页不受影响,所以用户的记忆没错。

- 拆两个 input 两个按钮:「拍照上传」带 capture;「从相册或文件选择」不带 capture、multiple、
  accept 含 .pdf。上传逻辑抽成 uploadFiles(ev) 两处共用,字段名仍是 file(对齐后端)
- 顺手删掉我上一轮写在页面上的开发者说明(用户不该在手机上读 capture 属性)
- tests/test_upload_page.py 6 例回归锁:两个 input、capture 只属于 camera 段、脚本里不许再有
  setAttribute、两个 input 共用同一处理函数、PC 页未受影响

更值得记的是第二件事:改完我重启收件器并验真实 HTML,返回的竟还是旧模板。查下来 5055 上有两个
LISTENING 进程——uv run 是外壳/venv 跳板/真解释器三层,杀外层留下持有 socket 的孙进程,而 Windows
允许重复绑同一端口,手机命中谁纯看运气。这就是 AGENTS「多进程共存」那条的现场版,只不过这次是收件器。

- port_busy() 启动前探测:端口已被听则退出码 6 拒绝二次启动并提示处置方式
- GET /api/ping 回 {ok, pid, started_at, root},一眼看出命中的是哪个进程;不含文件或会话信息
- tests/test_upload_server.py 4 例:ping 不泄露、port_busy 对监听与空闲端口的判定、真实渲染的两个入口
- 实测清理:批量停掉全部 upload.server → 端口释放 → 起唯一一个(pid 3600)→ ping 正常、两个入口在、
  capture 只剩 1 处、setAttribute 消失;再起第二个被退出码 6 拒掉
- 文档:upload README(路由表、两个入口、端口闸门)、操作指南 FAQ 两行、AGENTS 多进程共存补收件器、
  SKILL 补「行为像旧的就怀疑旧进程」,已 make skill-install 同步;方案文档 §19-§20

教训:**验证要打到真实服务上,别只跑单测**——单测 6 条全绿时线上还是旧页面。
pytest 164 passed;ruff / format / mypy / deptry / mcp-smoke 26 工具 / docs-check / skill-check 全绿。
--no-verify 提交并手跑等价检查。
2026-10-21 10:43:08 +08:00
wandering 5dda126e21 feat(upload): 建会话即给这一单的手机直链,收件器地址改为显式配置
用户选了 B 方案。新建一单报销时不该在首页一堆二维码里猜扫哪个:首页把全部会话平铺、按字典序最旧在前、
且没有归档概念只增不减,入口本身就是坏的。现在 case_create 直接返回 upload_url。

- src/config.py 新增可选字段 upload_base_url(环境变量 UPLOAD_BASE_URL;留空才退回探测)
- src/cases.py:upload_page_url() 优先用配置;新增 CaseStore.upload_url() 拼会话直链,会话 id 含中文
  (如 20261021-毕业采集)必须百分号转义,否则部分客户端打不开;删掉替换探测函数时留下的不可达旧函数体
- 新工具 case_upload_link(case_id):链接丢了重取,只在用户要时调;INSTRUCTIONS 第 1 步与硬约束更新,
  令牌只允许出现在这两个工具的返回里,case_status 与 case_list 刻意不带链接
- 收件器横幅优先用配置地址,没配时明确提示探测地址可能手机打不开
- 文档:README、AGENTS、src/upload/README、src/mcp/README、docs/MCP.md 标准流程、操作指南 7.5、
  scripts/data/README、config.example.json;SKILL 第 1-2 步改为发直链并提示收件器是独立进程;
  工具表由 make mcp-docs 重生成(26 个工具),闸门演练过:从 GROUPS 摘掉新工具会退出码 2 报错
- 纠正我自己写错的事实:install_skill 原先把 ~/.agents/skills 写死成本机启用,而来源开关会变
  (本机今早 08:11 就翻回 ~/.dsh/skills)。改为按 skills-manager 日志选最近启用的来源,
  一个都没启用则退出码 5 拒绝安装,新增 --list-sources;AGENTS 与技能 README 的表述同步改掉

踩过的坑记一笔:upload_base_url 少写端口会把链接打到 80(收件器听 5055),
已在配置注释、AGENTS、字段表三处写明必须带端口。

测试:tests/test_upload_link.py 新增 6 例(配置与环境变量优先级、探测退回、直链含令牌且 ASCII 转义、
重取稳定、未知会话拒绝、令牌不泄漏进 case_status 与 case_list)。pytest 154 passed;
ruff / format / mypy / deptry / mcp-smoke 26 工具 / docs-check / skill-check 全绿。
端到端实测:拿生成的直链访问真实运行中的收件器返回 200 且页面是上传页。
仍用 --no-verify 提交并手跑等价检查。
2026-10-21 10:05:33 +08:00
wandering 821822a408 docs: 全面体检并更新文档,工具参考改为从运行时 schema 生成
先体检再动手:44 个 Markdown,相对链接 0 处失效,逐份分辨「现状文档」与「历史记录」。
历史记录(error-experience/*、架构分析、已过时的全景图)保留原貌只加指路,不重写。

- 新增 scripts/gen_mcp_docs.py:起 stdio 子进程 list_tools 后整段重生成 docs/MCP.md 的「工具参考」
  (291 行 / 25 个工具,占这份文档四分之三,手工维护必漂)。--check 漂移退出码 1,--diff 打印差异;
  GROUPS 是刻意闸门——加了工具没登记就报错,防止「有工具没文档」。接进 make mcp-docs / docs-check
  与 tasks.py 同名任务
- 生成时抓到两处真实漂移:invoice_type 线上是 string/null 且默认 null,文档手写成 null/string 无默认;
  case_list 无参数在旧文档里写成一句散文。现已与线上一字不差
- 过期说法清理:src/README 的「browser_* 与 submit_* 尚未实现」(browser 早已联机验通、submit 定案不做);
  tests/README 的「当前 107 例」(现 148)与缺 test_mcp_browser.py 一行、「Playwright 填报不进单测」
  (现在选择器分支/状态判定/错误路径有单测,只有真实页面需要联机);.agents/README 的「Cursor Agent」;
  plans/README 里还写着「提交加确认 token」;scripts/data/README 表格有一行跑到正文后面
- 工程实践指南里把已删除的 CLI 入口当例子:mypy src/main.py、make run、python src/main.py 全部改为现状
  (mypy src / 唯一入口 python -m src.mcp.server),并补上「任务必须透传退出码」这条本轮踩到的坑
- README:「还没做完」重写为「边界与未完事项」——把刻意不做(提交、submit_* 整类、删除、撤回)与
  真的未完(科目/项目/人员固定取第 N 行,待业务口径)分开写;开发命令补 mcp-docs / docs-check / skill-install
- 操作指南:7.3 经济科目与项目从「要改就改索引」改成「已知限制,等口径,填报后务必人工核对」;
  相关命令补齐新目标;MCP.md 标准流程补上填报与「到此为止,提交由人」两步,并改掉与新纪律矛盾的
  「把截图报给用户」表述
- AGENTS:命令表加两行;测试段给出改工具后的固定顺序(mcp-smoke → mcp-docs → 重连宿主);提交纪律
  补「make check 之外还有 docs-check / skill-check 两道专项门禁」;截图产物改标注为排查证据
- install_skill.py:拿不到主目录时退出码改 4,别和「漂移」的 1 混成一类
- 已核对:submit_execute / src/main.py 在文档里的残留都是禁止性表述或「已删除」清单项,不是现状描述

验证:ruff / format --check / mypy src / deptry / pytest 148 passed / mcp-smoke 25 工具 /
docs-check 与 skill-check 均退出码 0。--no-verify 提交并手跑等价检查。
2026-10-20 23:12:46 +08:00
wandering 6aec1bc837 fix(browser): 全流程联机验证通过,修掉附件静默假成功等 5 个真问题
两条真实草稿走完全流程并逐张截图核对:普通报销 4 步(明细 3010109/3张/464、支付 231 工行公务卡、
附件入库,单据 202610020884);差旅报销 5 步(明细 交通费/2张/233 火车 阜阳→合肥、补助 3×80+3×100=540、
附件入库,单据 202610030245 总额 773.00)。验完两条都按守卫删除并复查 0 命中。

修的问题(都是单元测试看不见的):
1) 附件步骤是静默假成功:「附件说明」在财务系统里是 required,info 没给 attachment_desc 时填了空串,
   点确定被浏览器 HTML5 校验挡住——不报错、不发请求、弹窗不关,代码却打印「上传完成」回 ok:true。
   新增 base.upload_attachment()/confirm_attachment():缺说明兜底成「发票:文件名」、提交后确认弹窗已关、
   把 validationMessage 带进异常;附件必须落在会话目录内,文件不存在直接报错(原来是静默跳过)
2) 步骤函数依赖上一步留下的页面状态:续填时停在基本信息页,#next3/#next4 存在但不可见,点它必超时。
   所有步骤改成显式 open_tab(...),basic 标记给两个候选(日常 #EXPENEXPLAIN / 差旅 #CAUSE)
3) 导航快速通道只判断「在不在报销系统」,不比对 page_key:停在日常页却以为进了差旅页,差旅单据搜不到
   差点误判成流程坏了。抽成 on_target_page(page_key)
4) _on_reimburse 用子串比主机(http://elsewhere 会命中主机 x)→ 改 urlsplit().netloc 精确比,
   收紧后立刻联机回归,两类页面的判定各自正确
5) 空状态行「没有找到匹配的记录」既被 wait_rows_loaded 当成还在加载(每次搜索白等 20 秒),
   又被 search_forms 算成 1 条命中;两处显式判定,等待超时也从 20 秒收到 10 秒

- browser_fill:续填时前置步骤在财务系统里已完成、内存无记录,降级为 warnings 而不是拒绝(否则合法
  操作被挡),next_action 也不再瞎猜下一步
- browser_open_form 文档与未命中提示:命中判据是列表页可见文字,长说明会被截断,最稳的 match 是单据编号
- tests/test_mcp_browser.py +6 例(basic 多候选、两列表页互不认错、弹窗不关必须报错并带浏览器提示、
  附件说明兜底、路径越界与缺文件拒绝、附件清单按「附件文件名」定位表);FakePage 加 click/validations/
  tables,让 base.py 真实逻辑不依赖 Playwright 也能测。148 passed / 1.0s
- 文档:AGENTS 增「多标签共用一页」「两个独立列表页」「附件两处硬约束」三条实测约束;浏览器包 README
  重写(含 threading._register_atexit 的正确姿势与截图含个人信息的提醒);SKILL 补「每步自洽可单独重填」
  与附件说明兜底;方案文档 §15 记录全流程结果、5 个问题、我自己的两次误判与剩余项

验证:ruff / format / mypy src / deptry / pytest 148 passed / mcp_smoke 25 工具全绿;联机回归 16:14-16:16。
删除与提交类动作仍不进工具层。--no-verify 提交并手跑等价检查。
2026-10-20 16:32:56 +08:00
wandering 64bbe53e71 fix(config): 让 config.json 的 URL 字段真正生效,并守住占位假值
文档一直写着「地址来自 scripts/data/config.json」,但 config.py 读 URL 只看环境变量与内置默认,
根本不读这个文件;本地那份还是改造前的旧文件,三个 URL 键是示例占位假值,还带着早已删除的
consumable_storage / attachment_dir。修法是让文档变成真的:

- 优先级:环境变量 > config.json > src/config.py 内置默认;值统一 strip;坏 JSON 或缺文件不抛,退回默认并告警
- 含 your- 的占位值按未配置处理并 WARNING:优先级一改这些假地址就会立刻生效并把登录打挂,这条守卫
  是本改动自身的安全带(老文件实测:三条 WARNING 后回落到内置默认,登录不受影响)
- 凭据只从文件读(宿主清洗子进程的敏感环境变量,靠 env 传密码不可靠);新增 safe_config() 作为唯一
  可进日志的视图,load_config() 含明文密码
- 清死代码:SAFE_CONFIG_KEYS、attachment_dir(Web 时代遗留,全项目零引用)
- config.example.json 补五个可选 URL 键(留空=用默认);.env.example 的示例赋值全部注释掉——
  env 优先级最高,整份复制出去会真拿 your-sso-url 去登录
- 本地 scripts/data/config.json 去掉 7 个陈旧键,只留凭据;改前备份为 config.json.bak-20261020
- 测试 tests/test_config.py 3 → 9 例:文件覆盖默认 / env 覆盖文件 / 占位忽略 / strip / 凭据不读 env /
  safe_config 无密码 / 坏文件 / 缺文件 / 返回副本 / 示例文件不得再含 your- 假值;全部指向 tmp 不碰真凭据
- 文档:操作指南写清三级优先级与占位语义,scripts/data/README 补字段表,AGENTS 增加配置优先级约束,
  方案文档 §14 记录并划掉 §13.4 对应未完成项

验证:ruff / format --check / mypy src / deptry 全绿,pytest 141 passed,mcp_smoke 25 工具;
load_config() 实测无 WARNING 且 URL 与凭据取值正确。--no-verify 提交并手跑等价检查。
2026-10-20 11:41:34 +08:00
wandering ceefe3c4eb feat(mcp): P3 联机验证通过,新增 browser_open_form 断点续填,修 4 个联机才暴露的真 bug
联机跑通 open→login→new_form→fill(basic)→page_state→close:财务系统真的落库(单据编号 202610020850,
basic 页点「下一步」即保存)。据此补上续填能力并把测试草稿清理干净(删除前后截图 + 复查 0 命中)。

- 新增第 25 个工具 browser_open_form:列表页 #seacher 定位 → 唯一命中 → 行内单选 → #update 修改,
  读回 #EXPENEXPLAIN 证明数据还在;preview_only 只定位不点开并回显 bill_no/state/amount;
  命中多张直接拒绝并回 candidates,不替用户猜单据
- 修 4 个真 bug(都是单元测试看不见的):
  1) navigate_to_reimburse 单点硬等 5 秒偶发超时 → 全程轮询 + 失败带页面文字与截图;
     并查明直达报销 URL 不建立会话(会被踢「长时间未操作,请重新登录!」),门户点击链是必经
  2) actor.close_all 用 atexit 注册无效:concurrent.futures.thread 的钩子走 threading._register_atexit,
     比 atexit 早跑,executor 已 shutdown → close 提交不进去。改为注册到同一列表,实测 close
     在 playwright_0 线程执行,退出不再漏浏览器
  3) wait_rows_loaded 只认 <a>,真实「查看」是 <button>,每次白等满 20 秒(也拖慢了测试套件)
  4) 空结果「没有找到匹配的记录」被当成加载中,搜不到时再白等 20 秒
- browser_page_state 语义修正:logged_in 判门户 URL,进报销系统后必然 false 会误导 → 拆成
  at_portal / in_reimburse_system / on_form_page
- case_status 补齐 info_both / screenshots / browser(锁持有者与心跳年龄)/ warnings(混合 info、
  陈旧抽取、他人持锁);.browser.lock 文件名由 cases.py 单一定义,actor 反向引用
- 列表页结构实测写进 AGENTS:btSelectItem 单选、#insert/#update/#delete、行内「查看」是 gzlc 流程图、
  可见搜索框是 #seacher 而 #expenexplain 在收起面板(fill 会卡 30 秒)
- 测试:FakeBot 绑 BaseBot 真实行逻辑,新增 5 例续填用例(唯一命中/歧义拒绝/未命中/preview/空关键词),
  134 passed 0.36s;文档 docs/MCP.md 工具参考按 wire schema 重生成(25 工具),SKILL 增「断了不要重来,
  用 browser_open_form 续填」与破坏性操作纪律;方案文档 §13.5 记录联机结果、4 个 bug 与未完成项

验证:mcp_smoke 25 工具 / ruff / format / mypy / deptry / pytest 134 passed 全绿;
联机时间线见方案文档 §13.5。仍用 --no-verify 提交并手跑等价检查(原因见 AGENTS 提交纪律)。
2026-10-20 10:44:57 +08:00
wandering 1725244faf feat(mcp): 浏览器分步填报 P3——7 个 browser 工具 + 单线程 actor + 磁盘锁
填报从「一把梭」变成分步:Agent 可以开浏览器、登录、建单、按步填、看页面、关,每步有截图,
中途失败不必从头重来。提交不在本步——提交不可逆,留给 P4 的 submit_preview/submit_execute。

- src/infra/browser/actor.py(新):Playwright 单线程 actor(sync API 的 greenlet 线程亲和要求所有
  调用回到创建线程)+ 会话注册表 + cases/<id>/.browser.lock(PID/started_at/heartbeat/url,
  心跳 180 秒过期才允许接管;锁文件损坏按无锁处理;atexit 尽力关浏览器)。与 COM actor 分开。
- base.py:login_portal 拆成 submit_login + wait_for_portal(返回布尔不抛异常),新增 portal_reached/
  human_required;_screenshot 支持 screenshot_dir 并按 001-步骤名.png 编号、返回路径、失败只告警
- src/mcp/tools/browser.py(新):browser_open/login/new_form/fill/steps/page_state/close,工具 17 → 24
  · need_human 用 ok:true + logged_in:false 返回(扫码不是失败,避免宿主当错误重试或另开窗口)
  · 步骤顺序硬约束,跳步拒绝并用 next_action 指回该补的一步;步骤报错回传 detail.screenshot
  · browser_close 明确不提交不存草稿;travel 五步、normal 四步(无 subsidy)
- tests/test_mcp_browser.py(新,17 例,FakeBot/FakePage 不起浏览器):锁四种情形、need_human 分支与
  截图落点、重复登录幂等、未打开时各工具拒绝、类型推断与两 info 并存时拒猜、未登录拒建单、跳步拦截、
  步骤推进与 remaining、未知步骤、报错截图、close 释放会话与锁、open 复用、steps 两种流程
- 文档:docs/MCP.md 工具段按 wire schema 重生成(24 工具);AGENTS/SKILL/操作指南/README/浏览器 README
  同步分步流程、锁与心跳、有头默认、提交必须交人工;方案文档 §13 记录实现、取舍与未完成项

验证:
- 联机跑通 open(3.1s 起有头 Chromium)→ 锁写入(pid/heartbeat)→ page_state(about:blank,
  001-state.png 4721 字节)→ 重复 open 复用(already_open=true,不多开窗)→ close(锁释放、注册表清空)
- 登录与填报的联机验证需要用户在场(SSO 可能扫码),本轮未做
- 门禁:ruff / format --check / mypy src / deptry / pytest 129 passed / mcp_smoke 24 工具
  instructions 2109 字节。仍用 --no-verify 提交并手跑等价检查(原因见 AGENTS 提交纪律)。
2026-10-20 00:46:23 +08:00
wandering 7e900ebdea fix(requisition): 模板三级解析,空白模板入库,产物只写会话 artifacts
历史清洗删掉了根目录两份 低值易耗品申购*.xls——它们其实是**已填写的真实申购单**
(半导体型号、单价、项目名都在),不是模板;同时旧实现「项目根 glob 取名字最大者」会把
上一次生成的产物当模板,所以一并修掉:

- 新增 resolve_requisition_template():REQUISITION_TEMPLATE 环境变量 → templates/ → 项目根(兼容旧位置);
  环境变量指向不存在的文件时告警并继续回退,不静默失败
- 入库 templates/低值易耗品申购.xls 空白件(由旧文件删掉第 3 行起的全部内容得到,UsedRange 只剩 2 行:
  标题 + 表头;真实采购明细不进仓库)
- generate_requisition 默认输出改为 <会话>/artifacts/低值易耗品申购<ts>.xls,不再写模板旁边
- mcp/tools/documents.py 的 _find_template 改为复用同一解析函数,避免两处规则漂移
- 模块 docstring 与 CLI help 里 src/web/uploads 的过时示例改为 cases/
- tests:5 个纯单测覆盖解析优先级(env 优先 / templates 胜过根目录 / 根目录取最新 /
  env 指向不存在的文件时回退 / 全都没有时返回 None),不依赖 COM

验证:真实 Excel COM 端到端 2 项 → 产物 UsedRange 5 行(标题/表头/2 明细/合计),第 6 行起为空,
无旧明细残留;fill_requisition_excel 原本就会先删第 3 行起内容,这条也实测确认过。
门禁:ruff / format --check / mypy src / deptry 全绿,pytest 112 passed,mcp_smoke 17 工具。
2026-10-19 23:57:44 +08:00
wandering e1acf2de84 docs: 文档全面对齐 MCP 架构,并脱敏公开操作指南
接口与入门:
- 新增 docs/MCP.md:注册方法(DSH patch + 通用 client)、返回信封、会话目录布局、标准流程、
  硬约束与未实现清单;工具参考段由**运行时 input_schema 自动生成**,与代码同源不会漂移
- 删除 docs/API.md(856 行 Flask 路由/SSE 文档,描述的端点已不存在)
- 重写根 README.md:为什么放弃自带前端、职责分工表、快速开始、注册配置、流程链、结构与风险
- 重写 AGENTS.md:命令表换成 mcp/mcp-smoke/upload;mypy 目标改 src;新增「提交纪律」
  (pre-commit 的 stash 窗口会回滚未暂存改动)、COM 与 Playwright 线程约束、多进程共存提醒

包文档:src/README.md 与各包 README 按现状重写;新增 src/mcp/README.md(写工具的规矩、
宿主侧四个坑)与 src/upload/README.md(路由表与安全边界);tests/scripts/config 三处索引更新。

提示词迁移:src/infra/llm/ 整个删除,三份系统提示词迁到 .agents/skills/reimbursement/references/
(invoice-fields / travel-info-fields / normal-info-fields),删掉 can_submit 与 suggestion
(save_info 也会主动剥离),「严格只输出 JSON」改为「交给对应工具落盘」;新增 SKILL.md 作为
宿主侧操作纪律(已被 DSH 热加载为 reimbursement 技能)。

作废处理:删除 .agents/docs/guides/系统事件流全景图.md(SSE 与 file_events/agent_events 通道已整体
移除,现在没有任何事件流);给项目架构全景图与架构分析-2026-06-15 加「已过时」横幅保留历史;
config.example.json 去 consumable_storage 加 upload_pin;.env.example 去 LLM 段并说明已不再自动加载 .env。

安全:docs/报销操作指南.md 的示例配置里原来写着**真实姓名、16 位公务卡号、内网 LLM 地址与工号**
(自 9fe5d70/2026-05-25 起在版本历史里),本次全部换成占位符。历史清洗与令牌轮换另议。

门禁(改动含 src/infra/llm 删除,故重跑全部门禁):
  ruff check PASS | ruff format --check PASS(42) | mypy src PASS(34) | deptry PASS(36) |
  pytest 107 passed | scripts/mcp_smoke.py PASS(17 工具 / instructions 1449 字节 / case_list ok)
申购单在依赖清理后另已实机复测:2.4s 生成,产物单元格读回正确。
2026-10-19 22:40:06 +08:00
wandering 87b4080b1b refactor!: 删除 Web/CLI 应用层与内置 LLM 抽取,出库单功能下线(2b)
入口只剩 MCP 服务器;编排由宿主 Agent 负责,本项目只留工具与确定性内核。

删除:
- src/web/(Flask 路由、SSE 轮询、自研聊天前端、模板与静态资源,约 3400 行)
- src/main.py、src/pipeline.py、src/pipeline_core.py(CLI 全流程入口与两套重复管道)
- core/extraction/{extractor,llm_extractor}.py(内置 LLM 抽取、llm_stream.log、file_events.log、并行抽取)
- infra/documents/consumable.py 与 infra/documents/pdf.py:出库单功能下线(Word 模板 4fbb4c3 起已不在仓库,
  该路径此前一直静默失败),PDF 渲染由 core/extraction/render.py 取代
- mcp__arb__doc_consumable 工具;exceptions.ExtractionError;config 的 consumable_storage / get_llm_config /
  load_session_config / SESSION_CONFIG_KEYS(Web 专属,已无引用)
- 针对被删代码的 tests/test_extractor.py、tests/test_llm_extractor.py 与 scripts/ 下的 LLM 调试脚本
- 依赖:llama-index、llama-index-llms-openai-like、python-dotenv(连同其 50+ 传递依赖)
- 提交进来的调试截图 src/images/、根目录 images/ 与 .coverage(并补 .gitignore)

保留与新增:
- 材料入口改为独立进程 src/upload(手机扫码/PC 上传 → cases/<id>/inbox/)
- runner.py 自带 save_invoice_groups,不再依赖 pipeline_core
- scripts/mcp_smoke.py:stdio 自检(拉起自身、列工具、调 case_list、验证 instructions 下发)
- make mcp / mcp-smoke / upload 取代 make run;check 的 mypy 目标从 src/main.py 扩到 src

验证:mcp_smoke 通过(17 个工具、instructions 1449 字节、case_list 正常);申购单实机生成此前已验证。
门禁:ruff check / format --check(43) / mypy src(36 文件) / deptry / pytest 107 passed。
注:仍用 --no-verify 提交并手动跑等价检查(原因见 2f08bbd 提交说明)。
2026-10-19 22:04:25 +08:00
wandering 2f08bbd6e8 refactor: 删除自研 Agent 状态机与 SSE 事件通道
编排(提取-校验-修正循环、轮次上限、补充材料、终态保护)由宿主 Agent 承担,本项目不再自建运行时。

- 删除 src/agent/:AgentSession/AgentState 状态机、校验-修正循环、agent_events.log 事件通道(共 878 行)
- src/web/routes.py:移除 5 个 Agent 端点与 _emit_ready_and_submit(READY 即自动提交财务系统的逻辑一并消失),
  SSE 不再轮询 agent_events.log;_run_agent_task 更名 _run_session_task;824→490 行,18→13 端点
- src/infra/documents/requisition.py:项目名来源改为 .invoice_cache/normal_info.json → travel_info.json →
  旧 agent_state.json(兼容历史会话目录)→ 默认值
- AGENTS.md:架构章节改写为 MCP 模式,CLI 与 Web 标注为遗留待删;补两条集成约束
  (required 键必须存在、宿主侧路径用正斜杠)
- tests:requisition 回退链 +3 用例;Web 冒烟确认 GET / 与保留端点正常、/api/agent/* 返回 404

门禁:ruff check / format --check / mypy src/main.py / deptry 全绿,pytest 132 passed。
注:用 --no-verify 提交——上一轮 pre-commit 在暂存-回滚窗口内被中断,把工作区里几个未暂存
文件回滚成了 HEAD 版本(已重放并逐项核对)。本仓库 pre-commit 只跑 ruff,等价检查已手动执行。
2026-10-19 19:55:26 +08:00
wandering 14b6b4c2d7 feat: MCP 服务器与上传收件器,确立 cases/ 会话模型
- src/mcp: 15 个工具(材料 / 抽取交接 / 匹配 / 校验 / 骨架),stdio 入口 python -m src.mcp.server
- src/upload: 独立进程的扫码上传收件器(服务端 segno 二维码、零 CDN、令牌强制、只写不读)
- src/cases.py: 会话目录唯一实现,阶段由产物推导,不再存在状态机文件
- core/extraction: 拆出 cache.py(纯 IO)与 render.py(PDF 渲染 + 文本层 + 自动转正)
- core/matching: runner.py 从缓存匹配,输出 review_required 而非只给贪心结果
- schemas.py: 抽取字段定义单一真源;payment 补 merchant(validator 早已要求却从未采集)
- config: 修 _CONFIG_PATH 多一层 parent 导致 username/password 恒为空;新增 upload_pin
- 类型: validator.py 10 处与 browser/__init__ 2 处既存 mypy 错误一并修掉(行为以快照 diff 验证不变)
- 依赖: +mcp 2.3.0 +segno;cases/ 与 .mnemon/ 进 .gitignore
- 实测记录: .agents/docs/plans/MCP化改造方案-2026-10-19.md(含 DSH 注册与热重连结论)

注: 本提交用 --no-verify 跳过 pre-commit,等价的 ruff/format 检查已手动跑过(见下条提交说明)。
2026-10-19 19:54:48 +08:00
wandering 4fbb4c36ed 增加生成学校易耗品填写的转换程序 2026-08-31 18:25:35 +08:00
wandering 44496a54e6 fix: 补充 __init__.py 导出符号,修复测试导入错误
- src/core/extraction/__init__.py: 导出 _image_to_base64
- src/core/matching/__init__.py: 导出所有内部函数(_build_invoice_summary, _build_payment_records, _invoices_to_records, _match, _match_by_filename, _match_one_to_many, _match_one_to_one, _relative_tolerance, _safe_float)
- src/infra/documents/__init__.py: 导出 INVOICE_LEVEL_COLUMNS, PAYMENT_RECORD_COLUMNS
- tests/test_extractor.py: 修正 monkeypatch 路径为模块级引用
2026-07-02 18:51:59 +08:00
wandering 9043886b6f refactor: 架构重组 — doc/bot → core/infra,新增 Agent 调度模块
- src/doc/ 拆分为 src/core/extraction/, matching/, validation/(核心业务逻辑)
- src/bot/ 重命名为 src/infra/browser/(浏览器自动化基础设施)
- fill_consumable_doc.py → src/infra/documents/consumable.py
- 新增 Agent 调度模块:coordinator.py, events.py, session.py,重构 orchestrator.py
- 更新 AGENTS.md、README.md 及所有子目录 README
2026-07-02 18:36:19 +08:00
wandering e9e0252638 agent 模式补充文件功能已经实现 2026-06-15 10:39:01 +08:00
wandering 01d4686ceb 实现Agent对话,合格自动提交,不合格补充材料的能力 2026-06-14 12:56:33 +08:00
wandering 3bd325e827 完成差旅发票录入流程 2026-06-11 19:22:34 +08:00
wandering 07c0126741 重构项目为LLM 驱动 2026-06-09 16:25:20 +08:00