修复: 同窗统一消费实体出场章

This commit is contained in:
zizi 2026-07-22 16:44:25 +08:00
parent d015f1d321
commit 7f675922b2
2 changed files with 70 additions and 51 deletions

View File

@ -2398,6 +2398,13 @@ def _run(work_id, max_windows, max_calls, model, redo_window, semantic_on):
did = name_map[nm][0] did = name_map[nm][0]
to_update.setdefault(did, []).append(it["观察点"]) to_update.setdefault(did, []).append(it["观察点"])
pres_add.setdefault(did, set()).update(_int_chaps(it.get("出场章"))) pres_add.setdefault(did, set()).update(_int_chaps(it.get("出场章")))
# 在任何实体更新写入前汇总纯出场,确保同卡全部章节由一次 merge 或机械收尾消费。
for it in obs.get("纯出场", []):
nm = (it.get("名称") or "").strip()
if nm in name_map:
pres_add.setdefault(name_map[nm][0], set()).update(
_int_chaps(it.get("出场章"))
)
# ④ 卡更新(分批≤6) # ④ 卡更新(分批≤6)
items = sorted(to_update.items()) items = sorted(to_update.items())
for i in range(0, len(items), UPDATE_BATCH): for i in range(0, len(items), UPDATE_BATCH):
@ -2501,13 +2508,7 @@ def _run(work_id, max_windows, max_calls, model, redo_window, semantic_on):
relation_touched.add( relation_touched.add(
_insert_relation_card(conn, work_id, win_no, rp) _insert_relation_card(conn, work_id, win_no, rp)
) )
# ⑥ 机械收尾:出场章统一并入(纯出场 + 被更新卡,抽检#2)+ 窗置 done # ⑥ 机械收尾:仅处理未被更新模型消费的出场章,然后窗置 done。
for it in obs.get("纯出场", []):
nm = (it.get("名称") or "").strip()
if nm in name_map:
pres_add.setdefault(name_map[nm][0], set()).update(
_int_chaps(it.get("出场章"))
)
presence_touched = set() presence_touched = set()
for did, chs in pres_add.items(): for did, chs in pres_add.items():
if not chs: if not chs:

View File

@ -2438,53 +2438,71 @@ def test_run_pure_presence_rejects_external_state_and_revision_conflicts():
def test_run_presence_paths_accept_numeric_strings(): def test_run_presence_paths_accept_numeric_strings():
"""run 的已知更新归并卡与纯出场都把数字字符串章号落成整数。""" """同卡横跨新信息与纯出场时,有无模型更新都只走一次实体写入并落全章节。"""
state = { def run_case(updates):
"window_status": "pending", state = {
"payload": { "window_status": "pending",
"type": "character", "revisions": {701: 0},
"名称": "安若雪", "payload": {
"别名": [], "type": "character",
"字段": {}, "名称": "安若雪",
"出场章": [], "别名": [],
"_work_id": 8, "字段": {},
}, "出场章": [],
} "_work_id": 8,
},
}
def fake_m3_json(prompt, model, need_keys, system=None): def fake_m3_json(prompt, model, need_keys, system=None):
if need_keys == ("新名字", "已知实体新信息", "纯出场"): if need_keys == ("新名字", "已知实体新信息", "纯出场"):
return ({ return ({
"新名字": [], "新名字": [],
"已知实体新信息": [ "已知实体新信息": [
{"名称": "安若雪", "观察点": "出现新变化", "出场章": ["70"]} {"名称": "安若雪", "观察点": "出现新变化", "出场章": ["70"]}
], ],
"纯出场": [{"名称": "安若雪", "出场章": ["71"]}], "纯出场": [{"名称": "安若雪", "出场章": ["71"]}],
}, {}) }, {})
return ({"更新": []}, {}) return ({"更新": deepcopy(updates)}, {})
with patch.object(pu.psycopg, "connect", side_effect=lambda *_: _RunConn(state)), \ with patch.object(pu.psycopg, "connect", side_effect=lambda *_: _RunConn(state)), \
patch.object(pu, "load_entity_contracts", return_value=_fake_contracts()), \ patch.object(pu, "load_entity_contracts", return_value=_fake_contracts()), \
patch.object( patch.object(
pu, pu,
"load_window_material", "load_window_material",
return_value=("安若雪连续出现", {70: "安若雪出现新变化。", 71: "安若雪继续前进。"}), return_value=("安若雪连续出现", {70: "安若雪出现新变化。", 71: "安若雪继续前进。"}),
), \ ), \
patch.object( patch.object(
pu, pu,
"load_known", "load_known",
return_value=({"安若雪": (701, "character", "旧摘要")}, {}, {}, {701: 0}), return_value=({"安若雪": (701, "character", "旧摘要")}, {}, {}, {701: 0}),
), \ ), \
patch.object(pu, "m3_json", side_effect=fake_m3_json): patch.object(pu, "m3_json", side_effect=fake_m3_json), \
pu._run( patch.object(pu, "undo_window"), \
work_id=8, patch.object(pu, "merge_card", wraps=pu.merge_card) as merge_mock, \
max_windows=0, patch.object(
max_calls=0, pu, "_write_locked_upgrade_draft", wraps=pu._write_locked_upgrade_draft
model="MiniMax-M3", ) as write_mock:
redo_window=0, pu._run(8, 0, 0, "MiniMax-M3", 0, False)
semantic_on=False, return state, merge_mock.call_count, write_mock.call_count
)
check("run-presence-归并与纯出场数字字符串均落库", state["payload"]["出场章"] == [70, 71]) updated, merge_calls, write_calls = run_case([
{"draft_id": 701, "变更字段": {"一句话摘要": "新摘要"}, "别名新增": []},
])
check("run-presence-有更新时只写一次",
merge_calls == 1 and write_calls == 1 and updated["revisions"][701] == 1,
detail=f"merge={merge_calls}, write={write_calls}, state={updated}")
check("run-presence-有更新时一次消费两数组章节",
updated["window_status"] == "done" and updated["payload"]["出场章"] == [70, 71],
detail=str(updated))
unchanged, merge_calls, write_calls = run_case([])
check("run-presence-无更新时由机械收尾写一次",
merge_calls == 0 and write_calls == 1 and unchanged["revisions"][701] == 1,
detail=f"merge={merge_calls}, write={write_calls}, state={unchanged}")
check("run-presence-无更新时两数组章节仍落库",
unchanged["window_status"] == "done" and unchanged["payload"]["出场章"] == [70, 71],
detail=str(unchanged))
def test_load_known_projection(): def test_load_known_projection():