87b4080b1b
入口只剩 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 提交说明)。
87 lines
4.1 KiB
Python
87 lines
4.1 KiB
Python
"""MCP 服务器入口(本项目的唯一业务入口)
|
||
|
||
启动:python -m src.mcp.server (stdio 传输)
|
||
|
||
导入本模块必须零副作用:DSH 的 stdio 协商会先拉起一个探测进程再杀掉,
|
||
因此这里不启动浏览器、不开端口、不写状态文件,一切有状态动作发生在工具调用里。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from collections.abc import Callable
|
||
from typing import Any
|
||
|
||
from mcp.server import MCPServer
|
||
|
||
from .tools import analysis, documents, extraction, materials
|
||
|
||
SERVER_NAME = "auto-reimbursement"
|
||
|
||
INSTRUCTIONS = """财务报销自动化工具集(发票抽取交接 → 匹配 → 校验 → 文档 → 浏览器填报 → 提交)。
|
||
|
||
当前已实现(可直接调用):
|
||
1. mcp__arb__case_create 建会话;mcp__arb__case_add_files 拷入发票/支付截图/申请单
|
||
(路径一律用正斜杠,如 D:/Code/...;手机扫码上传的文件先 mcp__arb__case_add_files(source="inbox") 收取)。
|
||
2. mcp__arb__case_render_pages 渲染页面并取 PDF 文本层;text_chars>0 时优先 mcp__arb__case_read_text 读文本层,
|
||
否则读 pages 里的 PNG。文本层与读图结论冲突时(尤其编号、卡号、金额位数)必须复核,不要默认信任一路。
|
||
3. 按 mcp__arb__extraction_spec 的字段定义逐张产出 JSON,用 mcp__arb__case_save_extraction 落盘
|
||
(日期 YYYY-MM-DD、金额两位小数字符串;invoice_type 写错会被直接拒绝)。
|
||
4. mcp__arb__match_payments 做金额匹配;返回的 review_required 必须逐条处理(未匹配、发票总额小于支付
|
||
总额、一对多凑额),处理完再进下一步。贪心结果"成功"不等于"已核对"。
|
||
5. 依据 totals 与 suggested_invoice_type 综合缓存和匹配结果,产出 travel_info 或 normal_info,
|
||
用 mcp__arb__save_info 落盘(会自动跑规则校验);字段拿不准先 mcp__arb__rules_get 对照规则,
|
||
或用 mcp__arb__validate_info(draft=...) 不落盘反复试。不完整就向用户要补充材料,禁止自己宣布可提交。
|
||
拼 JSON 前先 mcp__arb__info_skeleton(info_type) 取骨架:required 为 true 的键必须存在(含值为空串的备注类
|
||
字段),骨架与 validation_rules.json 同源,别凭记忆造结构。
|
||
6. 普通发票用 mcp__arb__doc_requisition 生成 Excel 申购单(COM 自动化,需本机装有 Excel;
|
||
exclude 可剔除不参与申购的物品;纯差旅没有申购明细会返回失败提示,不是错误)。
|
||
人工核对后要改单张票据的某个字段,用 mcp__arb__case_patch_extraction(只传要改的键,会回显 from/to)。
|
||
7. 每步之后用 mcp__arb__case_status 复查态势;阶段由产物推导,没有内部状态机。
|
||
mcp__arb__case_reset(scope) 可清派生产物重跑;mcp__arb__case_list 看全部会话。
|
||
|
||
尚未实现(**现在不要调用,也不要在回答里承诺**):Word 出库单与 Excel 申购单、浏览器填报、
|
||
提交预览与执行。需要这些步骤时告诉用户该环节还在改造中,可暂用旧的 Web 流程,不要自己拼 HTML 或猜接口。
|
||
|
||
硬约束:不得回显密码;不得臆造财务数据,拿不准的字段标出来问用户;产物只写 cases/<case_id>/。
|
||
"""
|
||
|
||
TOOLS: list[Callable[..., dict[str, Any]]] = [
|
||
materials.case_create,
|
||
materials.case_list,
|
||
materials.case_status,
|
||
materials.case_add_files,
|
||
materials.case_inbox,
|
||
materials.case_reset,
|
||
extraction.case_render_pages,
|
||
extraction.case_read_text,
|
||
extraction.case_save_extraction,
|
||
extraction.extraction_spec,
|
||
extraction.case_patch_extraction,
|
||
analysis.match_payments,
|
||
analysis.save_info,
|
||
analysis.validate_info,
|
||
analysis.rules_get,
|
||
analysis.info_skeleton,
|
||
documents.doc_requisition,
|
||
]
|
||
|
||
|
||
def build_server() -> MCPServer:
|
||
"""构造并注册全部工具(无副作用,可被探测进程安全导入)。"""
|
||
server = MCPServer(name=SERVER_NAME, instructions=INSTRUCTIONS)
|
||
for fn in TOOLS:
|
||
server.tool()(fn)
|
||
return server
|
||
|
||
|
||
mcp = build_server()
|
||
|
||
|
||
def main() -> None:
|
||
"""以 stdio 传输运行。"""
|
||
mcp.run("stdio")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|