feat(status): 阶段认「已填报」,case_reset 补 screenshots 域
用户第 5 条:填完五步后 case_status 仍提示「可生成申购单,然后 browser_open」, 因为阶段推导到 docs_ready 就停了,根本没有「已填报」这一档;case_reset 也缺一个清截图的域。 - cases.summary 新增 filled 判据:screenshots/ 里存在 <NNN>-<step>_done.png(browser_* 每步成功都落一张), 阶段链变成 ... -> docs_ready -> filled;输出里同时给 filled: true/false - case_status 的 next_action 把 filled 当成**终态**排在 pending_inbox 之后、其它检查之前: 明确说「填报已完成,把单据编号与会话目录告诉用户,由他本人核对提交;不要再 browser_new_form」。 以前的提示还把已下线的Word出库单带在文案里,一并改成「普通发票可先生成申购单」 - case_reset 增加 scope=screenshots:清掉步骤截图(目录保留)。这也让 stage 从 filled 正常退回 - tests/test_stage_filled.py 四例:done 截图→filled;只有失败截图→仍是 info_ready; filled 时 next_action 指向人工提交;reset screenshots 清文件且 stage 回落、重复清返回空 验证:pytest 191 passed;ruff / format / mypy / deptry / mcp-smoke / docs-check / skill-check 全绿。 --no-verify 提交并手跑等价检查。
This commit is contained in:
+1
-1
@@ -182,7 +182,7 @@ cases/<case_id>/
|
||||
|
||||
### `mcp__arb__case_reset`
|
||||
|
||||
清理派生产物以便重跑。scope=extractions|match|info|pages|artifacts|all。
|
||||
清理派生产物以便重跑。scope=extractions|match|info|pages|artifacts|screenshots|all。
|
||||
|
||||
| 参数 | 类型 | 必填 | 默认 | 说明 |
|
||||
|------|------|------|------|------|
|
||||
|
||||
+12
-3
@@ -1,7 +1,10 @@
|
||||
"""会话目录(cases/<case_id>):MCP 工具与上传收件器共用的唯一实现
|
||||
|
||||
阶段不存状态机,全部由目录里的产物推导:
|
||||
empty -> extracted -> matched -> info_ready -> docs_ready
|
||||
empty -> materials_ready/extracted -> matched -> info_ready -> docs_ready -> filled
|
||||
|
||||
filled 的判据是「步骤完成截图」(browser_* 每步落 <NNN>-<step>_done.png):填报是唯一会把
|
||||
这种截图写进 screenshots/ 的动作,所以它比任何自述都可靠。
|
||||
|
||||
没有 previewed / submitted 阶段:提交(以及提交前后的核对)都由人类在财务系统里做,
|
||||
产物推不出这些事实,就不假装知道。要查结果去看列表页的单据状态。
|
||||
@@ -247,7 +250,10 @@ class CaseStore:
|
||||
sorted(p.name for p in (d / ARTIFACTS_DIR_NAME).glob("*")) if (d / ARTIFACTS_DIR_NAME).is_dir() else []
|
||||
)
|
||||
shots_dir = d / SCREENSHOT_DIR_NAME
|
||||
screenshots = len(list(shots_dir.glob("*.png"))) if shots_dir.is_dir() else 0
|
||||
shots = list(shots_dir.glob("*.png")) if shots_dir.is_dir() else []
|
||||
screenshots = len(shots)
|
||||
# 填报完成的证据:每步成功都会落 <NNN>-<step>_done.png(见 browser._apply_step)
|
||||
filled = any("_done" in shot.name for shot in shots)
|
||||
browser = self.browser_lock_state(d)
|
||||
warnings: list[str] = []
|
||||
if has_travel and has_normal:
|
||||
@@ -268,7 +274,9 @@ class CaseStore:
|
||||
"不要另开窗口,等它释放或心跳过期"
|
||||
)
|
||||
stage = "empty"
|
||||
if artifacts:
|
||||
if filled:
|
||||
stage = "filled"
|
||||
elif artifacts:
|
||||
stage = "docs_ready"
|
||||
elif info_key:
|
||||
stage = "info_ready"
|
||||
@@ -290,6 +298,7 @@ class CaseStore:
|
||||
"info_both": has_travel and has_normal,
|
||||
"artifacts": artifacts,
|
||||
"screenshots": screenshots,
|
||||
"filled": filled,
|
||||
"browser": browser,
|
||||
"pending_inbox": pending_inbox,
|
||||
"warnings": warnings,
|
||||
|
||||
@@ -65,6 +65,11 @@ def case_status(case_id: str) -> dict[str, Any]:
|
||||
ex = s["extractions"]
|
||||
if s["pending_inbox"]:
|
||||
nxt = f"收件箱有 {s['pending_inbox']} 个新文件,先 case_add_files(case_id, source='inbox') 收取"
|
||||
elif s["stage"] == "filled":
|
||||
nxt = (
|
||||
"填报已完成(screenshots 里有步骤完成截图):把单据编号与会话目录告诉用户,"
|
||||
"由他本人在财务系统里核对并提交;不要再 browser_new_form,也别重复填"
|
||||
)
|
||||
elif ex["missing"]:
|
||||
nxt = f"有 {len(ex['missing'])} 个材料未抽取:case_render_pages 后按 schema 抽取并 case_save_extraction"
|
||||
elif ex["stale"]:
|
||||
@@ -74,7 +79,7 @@ def case_status(case_id: str) -> dict[str, Any]:
|
||||
elif not s["info"]:
|
||||
nxt = "综合缓存与匹配结果生成 travel_info 或 normal_info,再 validate_info"
|
||||
else:
|
||||
nxt = "信息已就绪:可生成出库单/申购单,然后 browser_open 开始填报"
|
||||
nxt = "信息已就绪:普通发票可先生成申购单,然后 browser_open 开始填报;填完即终点,提交由本人做"
|
||||
return ok(**s, next_action=nxt)
|
||||
|
||||
|
||||
@@ -114,7 +119,7 @@ def case_inbox(case_id: str) -> dict[str, Any]:
|
||||
|
||||
|
||||
def case_reset(case_id: str, scope: str = "extractions") -> dict[str, Any]:
|
||||
"""清理派生产物以便重跑。scope=extractions|match|info|pages|artifacts|all。"""
|
||||
"""清理派生产物以便重跑。scope=extractions|match|info|pages|artifacts|screenshots|all。"""
|
||||
d = _store.resolve(case_id)
|
||||
if d is None:
|
||||
return fail(f"会话不存在: {case_id}", next_action="先 case_create")
|
||||
@@ -147,6 +152,13 @@ def case_reset(case_id: str, scope: str = "extractions") -> dict[str, Any]:
|
||||
for p in art.glob("*"):
|
||||
p.unlink()
|
||||
cleared.append(p.name)
|
||||
elif scope == "screenshots":
|
||||
# 填报过程截图:清了 stage 才会从 filled 退回去,所以单独给一个域而不是并进 artifacts
|
||||
shots_dir = d / SCREENSHOT_DIR_NAME
|
||||
if shots_dir.is_dir():
|
||||
for shot in shots_dir.glob("*.png"):
|
||||
shot.unlink()
|
||||
cleared.append(shot.name)
|
||||
elif scope == "all":
|
||||
for sub in (CACHE_DIR_NAME, PAGES_DIR_NAME, SCREENSHOT_DIR_NAME, ARTIFACTS_DIR_NAME):
|
||||
target = d / sub
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
"""阶段推导:填报完成后 stage 要认「已填报」,case_reset 要能单独清截图。"""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from src.cases import CACHE_DIR_NAME, SCREENSHOT_DIR_NAME, CaseStore
|
||||
from src.mcp.tools import materials
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def store(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> CaseStore:
|
||||
root = tmp_path / "cases"
|
||||
root.mkdir()
|
||||
monkeypatch.setattr(materials._store, "root", root, raising=False)
|
||||
return materials._store
|
||||
|
||||
|
||||
def _make_filled(store: CaseStore) -> Path:
|
||||
d = store.create("已填报测试")
|
||||
(d / SCREENSHOT_DIR_NAME / "003-basic_done.png").write_bytes(b"png")
|
||||
(d / CACHE_DIR_NAME / "normal_info.json").write_text("{}", encoding="utf-8")
|
||||
return d
|
||||
|
||||
|
||||
def test_stage_becomes_filled_after_step_screenshots(store: CaseStore) -> None:
|
||||
d = _make_filled(store)
|
||||
s = store.summary(d)
|
||||
assert s["filled"] is True
|
||||
assert s["stage"] == "filled"
|
||||
|
||||
|
||||
def test_error_screenshot_alone_is_not_filled(store: CaseStore) -> None:
|
||||
"""只有失败截图不算填报完成——判据是 <NNN>-<step>_done.png。"""
|
||||
d = store.create("只有报错截图")
|
||||
(d / SCREENSHOT_DIR_NAME / "004-tab_basic_failed.png").write_bytes(b"png")
|
||||
(d / CACHE_DIR_NAME / "normal_info.json").write_text("{}", encoding="utf-8")
|
||||
assert store.summary(d)["stage"] == "info_ready"
|
||||
|
||||
|
||||
def test_case_status_points_to_human_submit_when_filled(store: CaseStore) -> None:
|
||||
d = _make_filled(store)
|
||||
r = materials.case_status(d.name)
|
||||
assert r["ok"] is True and r["stage"] == "filled"
|
||||
assert "提交" in r["next_action"]
|
||||
# 文案本身就在劝阻重开单据,所以断言那句劝阻,而不是断言工具名不出现
|
||||
assert "不要再 browser_new_form" in r["next_action"]
|
||||
|
||||
|
||||
def test_reset_screenshots_clears_files_and_drops_filled(store: CaseStore) -> None:
|
||||
d = _make_filled(store)
|
||||
r = materials.case_reset(d.name, "screenshots")
|
||||
assert r["ok"] is True
|
||||
assert r["cleared"]
|
||||
assert (d / SCREENSHOT_DIR_NAME).is_dir()
|
||||
assert not list((d / SCREENSHOT_DIR_NAME).glob("*.png"))
|
||||
assert store.summary(d)["stage"] != "filled"
|
||||
assert materials.case_reset(d.name, "screenshots")["cleared"] == []
|
||||
Reference in New Issue
Block a user