state_manager.py 56 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """
  4. State Manager - 状态管理模块 (v5.4)
  5. 管理 state.json 的读写操作:
  6. - 实体状态管理
  7. - 进度追踪
  8. - 关系记录
  9. v5.1 变更(v5.4 沿用):
  10. - 集成 SQLStateManager,同步写入 SQLite (index.db)
  11. - state.json 保留精简数据,大数据自动迁移到 SQLite
  12. """
  13. import json
  14. import logging
  15. import sys
  16. import time
  17. from copy import deepcopy
  18. from pathlib import Path
  19. from runtime_compat import enable_windows_utf8_stdio
  20. from typing import Dict, List, Optional, Any
  21. from dataclasses import dataclass, field, asdict
  22. from datetime import datetime
  23. import filelock
  24. from .config import get_config
  25. from .observability import safe_append_perf_timing, safe_log_tool_call
  26. logger = logging.getLogger(__name__)
  27. try:
  28. # 当 scripts 目录在 sys.path 中(常见:从 scripts/ 运行)
  29. from security_utils import atomic_write_json, read_json_safe
  30. except ImportError: # pragma: no cover
  31. # 当以 `python -m scripts.data_modules...` 从仓库根目录运行
  32. from scripts.security_utils import atomic_write_json, read_json_safe
  33. @dataclass
  34. class EntityState:
  35. """实体状态"""
  36. id: str
  37. name: str
  38. type: str # 角色/地点/物品/势力
  39. tier: str = "装饰" # 核心/重要/次要/装饰
  40. aliases: List[str] = field(default_factory=list)
  41. attributes: Dict[str, Any] = field(default_factory=dict)
  42. first_appearance: int = 0
  43. last_appearance: int = 0
  44. @dataclass
  45. class Relationship:
  46. """实体关系"""
  47. from_entity: str
  48. to_entity: str
  49. type: str
  50. description: str
  51. chapter: int
  52. @dataclass
  53. class StateChange:
  54. """状态变化记录"""
  55. entity_id: str
  56. field: str
  57. old_value: Any
  58. new_value: Any
  59. reason: str
  60. chapter: int
  61. timestamp: str = field(default_factory=lambda: datetime.now().isoformat())
  62. @dataclass
  63. class _EntityPatch:
  64. """待写入的实体增量补丁(用于锁内合并)"""
  65. entity_type: str
  66. entity_id: str
  67. replace: bool = False
  68. base_entity: Optional[Dict[str, Any]] = None # 新建实体时的完整快照(用于填充缺失字段)
  69. top_updates: Dict[str, Any] = field(default_factory=dict)
  70. current_updates: Dict[str, Any] = field(default_factory=dict)
  71. appearance_chapter: Optional[int] = None
  72. class StateManager:
  73. """状态管理器(v5.1 entities_v3 格式 + SQLite 同步,v5.4 沿用)"""
  74. # v5.0 引入的实体类型
  75. ENTITY_TYPES = ["角色", "地点", "物品", "势力", "招式"]
  76. def __init__(self, config=None, enable_sqlite_sync: bool = True):
  77. """
  78. 初始化状态管理器
  79. 参数:
  80. - config: 配置对象
  81. - enable_sqlite_sync: 是否启用 SQLite 同步 (默认 True)
  82. """
  83. self.config = config or get_config()
  84. self._state: Dict[str, Any] = {}
  85. # 与 security_utils.atomic_write_json 保持一致:state.json.lock
  86. self._lock_path = self.config.state_file.with_suffix(self.config.state_file.suffix + ".lock")
  87. # v5.1 引入: SQLite 同步
  88. self._enable_sqlite_sync = enable_sqlite_sync
  89. self._sql_state_manager = None
  90. if enable_sqlite_sync:
  91. try:
  92. from .sql_state_manager import SQLStateManager
  93. self._sql_state_manager = SQLStateManager(self.config)
  94. except ImportError:
  95. pass # SQLStateManager 不可用时静默降级
  96. # 待写入的增量(锁内重读 + 合并 + 写入)
  97. self._pending_entity_patches: Dict[tuple[str, str], _EntityPatch] = {}
  98. self._pending_alias_entries: Dict[str, List[Dict[str, str]]] = {}
  99. self._pending_state_changes: List[Dict[str, Any]] = []
  100. self._pending_structured_relationships: List[Dict[str, Any]] = []
  101. self._pending_disambiguation_warnings: List[Dict[str, Any]] = []
  102. self._pending_disambiguation_pending: List[Dict[str, Any]] = []
  103. self._pending_progress_chapter: Optional[int] = None
  104. self._pending_progress_words_delta: int = 0
  105. self._pending_chapter_meta: Dict[str, Any] = {}
  106. # v5.1 引入: 缓存待同步到 SQLite 的数据
  107. self._pending_sqlite_data: Dict[str, Any] = {
  108. "entities_appeared": [],
  109. "entities_new": [],
  110. "state_changes": [],
  111. "relationships_new": [],
  112. "chapter": None
  113. }
  114. self._load_state()
  115. def _now_progress_timestamp(self) -> str:
  116. return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
  117. def _ensure_state_schema(self, state: Dict[str, Any]) -> Dict[str, Any]:
  118. """确保 state.json 具备运行所需的关键字段(尽量不破坏既有数据)。"""
  119. if not isinstance(state, dict):
  120. state = {}
  121. state.setdefault("project_info", {})
  122. state.setdefault("progress", {})
  123. state.setdefault("protagonist_state", {})
  124. # relationships: 旧版本可能是 list(实体关系),v5.0 运行态用 dict(人物关系/重要关系)
  125. relationships = state.get("relationships")
  126. if isinstance(relationships, list):
  127. state.setdefault("structured_relationships", [])
  128. if isinstance(state.get("structured_relationships"), list):
  129. state["structured_relationships"].extend(relationships)
  130. state["relationships"] = {}
  131. elif not isinstance(relationships, dict):
  132. state["relationships"] = {}
  133. state.setdefault("world_settings", {"power_system": [], "factions": [], "locations": []})
  134. state.setdefault("plot_threads", {"active_threads": [], "foreshadowing": []})
  135. state.setdefault("review_checkpoints", [])
  136. state.setdefault("chapter_meta", {})
  137. state.setdefault(
  138. "strand_tracker",
  139. {
  140. "last_quest_chapter": 0,
  141. "last_fire_chapter": 0,
  142. "last_constellation_chapter": 0,
  143. "current_dominant": "quest",
  144. "chapters_since_switch": 0,
  145. "history": [],
  146. },
  147. )
  148. entities_v3 = state.get("entities_v3")
  149. # v5.1 引入: entities_v3, alias_index, state_changes, structured_relationships 已迁移到 index.db
  150. # 不再在 state.json 中初始化或维护这些字段
  151. if not isinstance(state.get("disambiguation_warnings"), list):
  152. state["disambiguation_warnings"] = []
  153. if not isinstance(state.get("disambiguation_pending"), list):
  154. state["disambiguation_pending"] = []
  155. # progress 基础字段
  156. progress = state["progress"]
  157. if not isinstance(progress, dict):
  158. progress = {}
  159. state["progress"] = progress
  160. progress.setdefault("current_chapter", 0)
  161. progress.setdefault("total_words", 0)
  162. progress.setdefault("last_updated", self._now_progress_timestamp())
  163. return state
  164. def _load_state(self):
  165. """加载状态文件"""
  166. if self.config.state_file.exists():
  167. self._state = read_json_safe(self.config.state_file, default={})
  168. self._state = self._ensure_state_schema(self._state)
  169. else:
  170. self._state = self._ensure_state_schema({})
  171. def save_state(self):
  172. """
  173. 保存状态文件(锁内重读 + 合并 + 原子写入)。
  174. 解决多 Agent 并行下的“读-改-写覆盖”风险:
  175. - 获取锁
  176. - 重新读取磁盘最新 state.json
  177. - 仅合并本实例产生的增量(pending_*)
  178. - 原子化写入
  179. """
  180. # 无增量时不写入,避免无意义覆盖
  181. has_pending = any(
  182. [
  183. self._pending_entity_patches,
  184. self._pending_alias_entries,
  185. self._pending_state_changes,
  186. self._pending_structured_relationships,
  187. self._pending_disambiguation_warnings,
  188. self._pending_disambiguation_pending,
  189. self._pending_chapter_meta,
  190. self._pending_progress_chapter is not None,
  191. self._pending_progress_words_delta != 0,
  192. ]
  193. )
  194. if not has_pending:
  195. return
  196. self.config.ensure_dirs()
  197. lock = filelock.FileLock(str(self._lock_path), timeout=10)
  198. try:
  199. with lock:
  200. disk_state = read_json_safe(self.config.state_file, default={})
  201. disk_state = self._ensure_state_schema(disk_state)
  202. # progress(合并为 max(chapter) + words_delta 累加)
  203. if self._pending_progress_chapter is not None or self._pending_progress_words_delta != 0:
  204. progress = disk_state.get("progress", {})
  205. if not isinstance(progress, dict):
  206. progress = {}
  207. disk_state["progress"] = progress
  208. try:
  209. current_chapter = int(progress.get("current_chapter", 0) or 0)
  210. except (TypeError, ValueError):
  211. current_chapter = 0
  212. if self._pending_progress_chapter is not None:
  213. progress["current_chapter"] = max(current_chapter, int(self._pending_progress_chapter))
  214. if self._pending_progress_words_delta:
  215. try:
  216. total_words = int(progress.get("total_words", 0) or 0)
  217. except (TypeError, ValueError):
  218. total_words = 0
  219. progress["total_words"] = total_words + int(self._pending_progress_words_delta)
  220. progress["last_updated"] = self._now_progress_timestamp()
  221. # v5.1 引入: 强制使用 SQLite 模式,移除大数据字段
  222. # 确保 state.json 中不存在这些膨胀字段
  223. for field in ["entities_v3", "alias_index", "state_changes", "structured_relationships"]:
  224. disk_state.pop(field, None)
  225. # 标记已迁移
  226. disk_state["_migrated_to_sqlite"] = True
  227. # disambiguation_warnings(追加去重 + 截断)
  228. if self._pending_disambiguation_warnings:
  229. warnings_list = disk_state.get("disambiguation_warnings")
  230. if not isinstance(warnings_list, list):
  231. warnings_list = []
  232. disk_state["disambiguation_warnings"] = warnings_list
  233. def _warn_key(w: Dict[str, Any]) -> tuple:
  234. return (
  235. w.get("chapter"),
  236. w.get("mention"),
  237. w.get("chosen_id"),
  238. w.get("confidence"),
  239. )
  240. existing_keys = {_warn_key(w) for w in warnings_list if isinstance(w, dict)}
  241. for w in self._pending_disambiguation_warnings:
  242. if not isinstance(w, dict):
  243. continue
  244. k = _warn_key(w)
  245. if k in existing_keys:
  246. continue
  247. warnings_list.append(w)
  248. existing_keys.add(k)
  249. # 只保留最近 N 条,避免文件无限增长
  250. max_keep = self.config.max_disambiguation_warnings
  251. if len(warnings_list) > max_keep:
  252. disk_state["disambiguation_warnings"] = warnings_list[-max_keep:]
  253. # disambiguation_pending(追加去重 + 截断)
  254. if self._pending_disambiguation_pending:
  255. pending_list = disk_state.get("disambiguation_pending")
  256. if not isinstance(pending_list, list):
  257. pending_list = []
  258. disk_state["disambiguation_pending"] = pending_list
  259. def _pending_key(w: Dict[str, Any]) -> tuple:
  260. return (
  261. w.get("chapter"),
  262. w.get("mention"),
  263. w.get("suggested_id"),
  264. w.get("confidence"),
  265. )
  266. existing_keys = {_pending_key(w) for w in pending_list if isinstance(w, dict)}
  267. for w in self._pending_disambiguation_pending:
  268. if not isinstance(w, dict):
  269. continue
  270. k = _pending_key(w)
  271. if k in existing_keys:
  272. continue
  273. pending_list.append(w)
  274. existing_keys.add(k)
  275. max_keep = self.config.max_disambiguation_pending
  276. if len(pending_list) > max_keep:
  277. disk_state["disambiguation_pending"] = pending_list[-max_keep:]
  278. # chapter_meta(新增:按章节号覆盖写入)
  279. if self._pending_chapter_meta:
  280. chapter_meta = disk_state.get("chapter_meta")
  281. if not isinstance(chapter_meta, dict):
  282. chapter_meta = {}
  283. disk_state["chapter_meta"] = chapter_meta
  284. chapter_meta.update(self._pending_chapter_meta)
  285. # 原子写入(锁已持有,不再二次加锁)
  286. atomic_write_json(self.config.state_file, disk_state, use_lock=False, backup=True)
  287. # v5.1 引入: 同步到 SQLite(失败时保留 pending 以便重试)
  288. sqlite_pending_snapshot = self._snapshot_sqlite_pending()
  289. sqlite_sync_ok = self._sync_to_sqlite()
  290. # 同步内存为磁盘最新快照
  291. self._state = disk_state
  292. # state.json 侧 pending 已写盘,直接清空
  293. self._pending_disambiguation_warnings.clear()
  294. self._pending_disambiguation_pending.clear()
  295. self._pending_chapter_meta.clear()
  296. self._pending_progress_chapter = None
  297. self._pending_progress_words_delta = 0
  298. # SQLite 侧 pending:成功后清空,失败则恢复快照(避免静默丢数据)
  299. if sqlite_sync_ok:
  300. self._pending_entity_patches.clear()
  301. self._pending_alias_entries.clear()
  302. self._pending_state_changes.clear()
  303. self._pending_structured_relationships.clear()
  304. self._clear_pending_sqlite_data()
  305. else:
  306. self._restore_sqlite_pending(sqlite_pending_snapshot)
  307. except filelock.Timeout:
  308. raise RuntimeError("无法获取 state.json 文件锁,请稍后重试")
  309. def _sync_to_sqlite(self) -> bool:
  310. """同步待处理数据到 SQLite(v5.1 引入,v5.4 沿用)"""
  311. if not self._sql_state_manager:
  312. return True
  313. # 方式1: 通过 process_chapter_result 收集的数据
  314. sqlite_data = self._pending_sqlite_data
  315. chapter = sqlite_data.get("chapter")
  316. # 记录已处理的 (entity_id, chapter) 组合,避免重复写入 appearances
  317. processed_appearances = set()
  318. if chapter is not None:
  319. try:
  320. self._sql_state_manager.process_chapter_entities(
  321. chapter=chapter,
  322. entities_appeared=sqlite_data.get("entities_appeared", []),
  323. entities_new=sqlite_data.get("entities_new", []),
  324. state_changes=sqlite_data.get("state_changes", []),
  325. relationships_new=sqlite_data.get("relationships_new", [])
  326. )
  327. # 标记已处理的出场记录
  328. for entity in sqlite_data.get("entities_appeared", []):
  329. if entity.get("id"):
  330. processed_appearances.add((entity.get("id"), chapter))
  331. for entity in sqlite_data.get("entities_new", []):
  332. eid = entity.get("suggested_id") or entity.get("id")
  333. if eid:
  334. processed_appearances.add((eid, chapter))
  335. except Exception as exc:
  336. logger.warning("SQLite sync failed (process_chapter_entities): %s", exc)
  337. return False
  338. # 方式2: 使用 add_entity/update_entity 收集的增量数据。
  339. # 数据缓存在 _pending_entity_patches 等变量中。
  340. return self._sync_pending_patches_to_sqlite(processed_appearances)
  341. def _sync_pending_patches_to_sqlite(self, processed_appearances: set = None) -> bool:
  342. """同步 _pending_entity_patches 等到 SQLite(v5.1 引入,v5.4 沿用)
  343. Args:
  344. processed_appearances: 已通过 process_chapter_entities 处理的 (entity_id, chapter) 集合,
  345. 用于避免重复写入 appearances 表(防止覆盖 mentions)
  346. """
  347. if not self._sql_state_manager:
  348. return True
  349. if processed_appearances is None:
  350. processed_appearances = set()
  351. # 元数据字段(不应写入 current_json)
  352. METADATA_FIELDS = {"canonical_name", "tier", "desc", "is_protagonist", "is_archived"}
  353. try:
  354. from .sql_state_manager import EntityData
  355. from .index_manager import EntityMeta
  356. # 同步实体补丁
  357. for (entity_type, entity_id), patch in self._pending_entity_patches.items():
  358. if patch.base_entity:
  359. # 新实体
  360. entity_data = EntityData(
  361. id=entity_id,
  362. type=entity_type,
  363. name=patch.base_entity.get("canonical_name", entity_id),
  364. tier=patch.base_entity.get("tier", "装饰"),
  365. desc=patch.base_entity.get("desc", ""),
  366. current=patch.base_entity.get("current", {}),
  367. aliases=[],
  368. first_appearance=patch.base_entity.get("first_appearance", 0),
  369. last_appearance=patch.base_entity.get("last_appearance", 0),
  370. is_protagonist=patch.base_entity.get("is_protagonist", False)
  371. )
  372. self._sql_state_manager.upsert_entity(entity_data)
  373. # 记录首次出场(跳过已处理的,避免覆盖 mentions)
  374. if patch.appearance_chapter is not None:
  375. if (entity_id, patch.appearance_chapter) not in processed_appearances:
  376. self._sql_state_manager._index_manager.record_appearance(
  377. entity_id=entity_id,
  378. chapter=patch.appearance_chapter,
  379. mentions=[entity_data.name],
  380. confidence=1.0,
  381. skip_if_exists=True # 关键:不覆盖已有记录
  382. )
  383. else:
  384. # 更新现有实体
  385. has_metadata_updates = bool(patch.top_updates and
  386. any(k in METADATA_FIELDS for k in patch.top_updates))
  387. # 非元数据的 top_updates 应该当作 current 更新
  388. # 例如:realm, layer, location 等状态字段
  389. non_metadata_top_updates = {
  390. k: v for k, v in patch.top_updates.items()
  391. if k not in METADATA_FIELDS
  392. } if patch.top_updates else {}
  393. # 合并 current_updates 和非元数据的 top_updates
  394. effective_current_updates = {**non_metadata_top_updates}
  395. if patch.current_updates:
  396. effective_current_updates.update(patch.current_updates)
  397. if has_metadata_updates:
  398. # 有元数据更新:使用 upsert_entity(update_metadata=True)
  399. existing = self._sql_state_manager.get_entity(entity_id)
  400. if existing:
  401. # 合并 current
  402. current = existing.get("current_json", {})
  403. if isinstance(current, str):
  404. import json
  405. current = json.loads(current) if current else {}
  406. if effective_current_updates:
  407. current.update(effective_current_updates)
  408. new_canonical_name = patch.top_updates.get("canonical_name")
  409. old_canonical_name = existing.get("canonical_name", "")
  410. entity_meta = EntityMeta(
  411. id=entity_id,
  412. type=existing.get("type", entity_type),
  413. canonical_name=new_canonical_name or old_canonical_name,
  414. tier=patch.top_updates.get("tier", existing.get("tier", "装饰")),
  415. desc=patch.top_updates.get("desc", existing.get("desc", "")),
  416. current=current,
  417. first_appearance=existing.get("first_appearance", 0),
  418. last_appearance=patch.appearance_chapter or existing.get("last_appearance", 0),
  419. is_protagonist=patch.top_updates.get("is_protagonist", existing.get("is_protagonist", False)),
  420. is_archived=patch.top_updates.get("is_archived", existing.get("is_archived", False))
  421. )
  422. self._sql_state_manager._index_manager.upsert_entity(entity_meta, update_metadata=True)
  423. # 如果 canonical_name 改名,自动注册新名字为 alias
  424. if new_canonical_name and new_canonical_name != old_canonical_name:
  425. self._sql_state_manager.register_alias(
  426. new_canonical_name, entity_id, existing.get("type", entity_type)
  427. )
  428. elif effective_current_updates:
  429. # 只有 current 更新(包括非元数据的 top_updates)
  430. self._sql_state_manager.update_entity_current(entity_id, effective_current_updates)
  431. # 更新 last_appearance 并记录出场
  432. if patch.appearance_chapter is not None:
  433. self._sql_state_manager._update_last_appearance(entity_id, patch.appearance_chapter)
  434. # 补充 appearances 记录
  435. # 使用 skip_if_exists=True 避免覆盖已有记录的 mentions
  436. if (entity_id, patch.appearance_chapter) not in processed_appearances:
  437. self._sql_state_manager._index_manager.record_appearance(
  438. entity_id=entity_id,
  439. chapter=patch.appearance_chapter,
  440. mentions=[],
  441. confidence=1.0,
  442. skip_if_exists=True # 关键:不覆盖已有记录
  443. )
  444. # 同步别名
  445. for alias, entries in self._pending_alias_entries.items():
  446. for entry in entries:
  447. entity_type = entry.get("type")
  448. entity_id = entry.get("id")
  449. if entity_type and entity_id:
  450. self._sql_state_manager.register_alias(alias, entity_id, entity_type)
  451. # 同步状态变化
  452. for change in self._pending_state_changes:
  453. self._sql_state_manager.record_state_change(
  454. entity_id=change.get("entity_id", ""),
  455. field=change.get("field", ""),
  456. old_value=change.get("old", change.get("old_value", "")),
  457. new_value=change.get("new", change.get("new_value", "")),
  458. reason=change.get("reason", ""),
  459. chapter=change.get("chapter", 0)
  460. )
  461. # 同步关系
  462. for rel in self._pending_structured_relationships:
  463. self._sql_state_manager.upsert_relationship(
  464. from_entity=rel.get("from_entity", ""),
  465. to_entity=rel.get("to_entity", ""),
  466. type=rel.get("type", "相识"),
  467. description=rel.get("description", ""),
  468. chapter=rel.get("chapter", 0)
  469. )
  470. return True
  471. except Exception as e:
  472. # SQLite 同步失败时记录警告(不中断主流程)
  473. logger.warning("SQLite sync failed: %s", e)
  474. return False
  475. def _snapshot_sqlite_pending(self) -> Dict[str, Any]:
  476. """抓取 SQLite 侧 pending 快照,用于同步失败回滚内存队列。"""
  477. return {
  478. "entity_patches": deepcopy(self._pending_entity_patches),
  479. "alias_entries": deepcopy(self._pending_alias_entries),
  480. "state_changes": deepcopy(self._pending_state_changes),
  481. "structured_relationships": deepcopy(self._pending_structured_relationships),
  482. "sqlite_data": deepcopy(self._pending_sqlite_data),
  483. }
  484. def _restore_sqlite_pending(self, snapshot: Dict[str, Any]) -> None:
  485. """恢复 SQLite 侧 pending 快照,避免同步失败后数据静默丢失。"""
  486. self._pending_entity_patches = snapshot.get("entity_patches", {})
  487. self._pending_alias_entries = snapshot.get("alias_entries", {})
  488. self._pending_state_changes = snapshot.get("state_changes", [])
  489. self._pending_structured_relationships = snapshot.get("structured_relationships", [])
  490. self._pending_sqlite_data = snapshot.get("sqlite_data", {
  491. "entities_appeared": [],
  492. "entities_new": [],
  493. "state_changes": [],
  494. "relationships_new": [],
  495. "chapter": None,
  496. })
  497. def _clear_pending_sqlite_data(self):
  498. """清空待同步的 SQLite 数据"""
  499. self._pending_sqlite_data = {
  500. "entities_appeared": [],
  501. "entities_new": [],
  502. "state_changes": [],
  503. "relationships_new": [],
  504. "chapter": None
  505. }
  506. # ==================== 进度管理 ====================
  507. def get_current_chapter(self) -> int:
  508. """获取当前章节号"""
  509. return self._state.get("progress", {}).get("current_chapter", 0)
  510. def update_progress(self, chapter: int, words: int = 0):
  511. """更新进度"""
  512. if "progress" not in self._state:
  513. self._state["progress"] = {}
  514. self._state["progress"]["current_chapter"] = chapter
  515. if words > 0:
  516. total = self._state["progress"].get("total_words", 0)
  517. self._state["progress"]["total_words"] = total + words
  518. # 记录增量:锁内合并时用 max(chapter) + words_delta 累加
  519. if self._pending_progress_chapter is None:
  520. self._pending_progress_chapter = chapter
  521. else:
  522. self._pending_progress_chapter = max(self._pending_progress_chapter, chapter)
  523. if words > 0:
  524. self._pending_progress_words_delta += int(words)
  525. # ==================== 实体管理 (v5.1 SQLite-first) ====================
  526. def get_entity(self, entity_id: str, entity_type: str = None) -> Optional[Dict]:
  527. """获取实体(v5.1 引入:优先从 SQLite 读取)"""
  528. # v5.1 引入: 优先从 SQLite 读取
  529. if self._sql_state_manager:
  530. entity = self._sql_state_manager._index_manager.get_entity(entity_id)
  531. if entity:
  532. return entity
  533. # 回退到内存 state (兼容未迁移场景)
  534. entities_v3 = self._state.get("entities_v3", {})
  535. if entity_type:
  536. return entities_v3.get(entity_type, {}).get(entity_id)
  537. # 遍历所有类型查找
  538. for type_name, entities in entities_v3.items():
  539. if entity_id in entities:
  540. return entities[entity_id]
  541. return None
  542. def get_entity_type(self, entity_id: str) -> Optional[str]:
  543. """获取实体所属类型"""
  544. # v5.1 引入: 优先从 SQLite 读取
  545. if self._sql_state_manager:
  546. entity = self._sql_state_manager._index_manager.get_entity(entity_id)
  547. if entity:
  548. return entity.get("type")
  549. # 回退到内存 state
  550. for type_name, entities in self._state.get("entities_v3", {}).items():
  551. if entity_id in entities:
  552. return type_name
  553. return None
  554. def get_all_entities(self) -> Dict[str, Dict]:
  555. """获取所有实体(扁平化视图)"""
  556. # v5.1 引入: 优先从 SQLite 读取
  557. if self._sql_state_manager:
  558. result = {}
  559. for entity_type in self.ENTITY_TYPES:
  560. entities = self._sql_state_manager._index_manager.get_entities_by_type(entity_type)
  561. for e in entities:
  562. eid = e.get("id")
  563. if eid:
  564. result[eid] = {**e, "type": entity_type}
  565. if result:
  566. return result
  567. # 回退到内存 state
  568. result = {}
  569. for type_name, entities in self._state.get("entities_v3", {}).items():
  570. for eid, e in entities.items():
  571. result[eid] = {**e, "type": type_name}
  572. return result
  573. def get_entities_by_type(self, entity_type: str) -> Dict[str, Dict]:
  574. """按类型获取实体"""
  575. # v5.1 引入: 优先从 SQLite 读取
  576. if self._sql_state_manager:
  577. entities = self._sql_state_manager._index_manager.get_entities_by_type(entity_type)
  578. if entities:
  579. return {e.get("id"): e for e in entities if e.get("id")}
  580. # 回退到内存 state
  581. return self._state.get("entities_v3", {}).get(entity_type, {})
  582. def get_entities_by_tier(self, tier: str) -> Dict[str, Dict]:
  583. """按层级获取实体"""
  584. # v5.1 引入: 优先从 SQLite 读取
  585. if self._sql_state_manager:
  586. result = {}
  587. for entity_type in self.ENTITY_TYPES:
  588. entities = self._sql_state_manager._index_manager.get_entities_by_tier(tier)
  589. for e in entities:
  590. eid = e.get("id")
  591. if eid and e.get("type") == entity_type:
  592. result[eid] = {**e, "type": entity_type}
  593. if result:
  594. return result
  595. # 回退到内存 state
  596. result = {}
  597. for type_name, entities in self._state.get("entities_v3", {}).items():
  598. for eid, e in entities.items():
  599. if e.get("tier") == tier:
  600. result[eid] = {**e, "type": type_name}
  601. return result
  602. def add_entity(self, entity: EntityState) -> bool:
  603. """添加新实体(v5.0 entities_v3 格式,v5.4 沿用)"""
  604. entity_type = entity.type
  605. if entity_type not in self.ENTITY_TYPES:
  606. entity_type = "角色"
  607. if "entities_v3" not in self._state:
  608. self._state["entities_v3"] = {t: {} for t in self.ENTITY_TYPES}
  609. if entity_type not in self._state["entities_v3"]:
  610. self._state["entities_v3"][entity_type] = {}
  611. # 检查是否已存在
  612. if entity.id in self._state["entities_v3"][entity_type]:
  613. return False
  614. # 转换为 v3 格式
  615. v3_entity = {
  616. "canonical_name": entity.name,
  617. "tier": entity.tier,
  618. "desc": "",
  619. "current": entity.attributes,
  620. "first_appearance": entity.first_appearance,
  621. "last_appearance": entity.last_appearance,
  622. "history": []
  623. }
  624. self._state["entities_v3"][entity_type][entity.id] = v3_entity
  625. # 记录实体补丁(新建:仅填充缺失字段,避免覆盖并发写入)
  626. patch = self._pending_entity_patches.get((entity_type, entity.id))
  627. if patch is None:
  628. patch = _EntityPatch(entity_type=entity_type, entity_id=entity.id)
  629. self._pending_entity_patches[(entity_type, entity.id)] = patch
  630. patch.replace = True
  631. patch.base_entity = v3_entity
  632. # v5.1 引入: 注册别名到 index.db (通过 SQLStateManager)
  633. if self._sql_state_manager:
  634. self._sql_state_manager._index_manager.register_alias(entity.name, entity.id, entity_type)
  635. for alias in entity.aliases:
  636. if alias:
  637. self._sql_state_manager._index_manager.register_alias(alias, entity.id, entity_type)
  638. return True
  639. def _register_alias_internal(self, entity_id: str, entity_type: str, alias: str):
  640. """内部方法:注册别名到 index.db(v5.1 引入)"""
  641. if not alias:
  642. return
  643. # v5.1 引入: 直接写入 SQLite
  644. if self._sql_state_manager:
  645. self._sql_state_manager._index_manager.register_alias(alias, entity_id, entity_type)
  646. def update_entity(self, entity_id: str, updates: Dict[str, Any], entity_type: str = None) -> bool:
  647. """更新实体属性(v5.0 引入,v5.4 沿用)"""
  648. # v5.1+ SQLite-first:
  649. # - entity_type 可能来自 SQLite(entities 表),但 state.json 不再持久化 entities_v3。
  650. # - 因此不能假设 self._state["entities_v3"][type][id] 一定存在(issues7 日志曾 KeyError)。
  651. resolved_type = entity_type or self.get_entity_type(entity_id)
  652. if not resolved_type:
  653. return False
  654. if resolved_type not in self.ENTITY_TYPES:
  655. resolved_type = "角色"
  656. # 仅在内存存在 v3 实体时才更新内存快照(不强行创建,避免 state.json 再膨胀)
  657. entities_v3 = self._state.get("entities_v3")
  658. entity = None
  659. if isinstance(entities_v3, dict):
  660. bucket = entities_v3.get(resolved_type)
  661. if isinstance(bucket, dict):
  662. entity = bucket.get(entity_id)
  663. # SQLite 启用时,即使内存实体缺失,也要记录 patch,确保 current 能增量写回 index.db
  664. patch = None
  665. if self._sql_state_manager:
  666. patch = self._pending_entity_patches.get((resolved_type, entity_id))
  667. if patch is None:
  668. patch = _EntityPatch(entity_type=resolved_type, entity_id=entity_id)
  669. self._pending_entity_patches[(resolved_type, entity_id)] = patch
  670. if entity is None and patch is None:
  671. return False
  672. did_any = False
  673. for key, value in updates.items():
  674. if key == "attributes" and isinstance(value, dict):
  675. if entity is not None:
  676. if "current" not in entity:
  677. entity["current"] = {}
  678. entity["current"].update(value)
  679. if patch is not None:
  680. patch.current_updates.update(value)
  681. did_any = True
  682. elif key == "current" and isinstance(value, dict):
  683. if entity is not None:
  684. if "current" not in entity:
  685. entity["current"] = {}
  686. entity["current"].update(value)
  687. if patch is not None:
  688. patch.current_updates.update(value)
  689. did_any = True
  690. else:
  691. if entity is not None:
  692. entity[key] = value
  693. if patch is not None:
  694. patch.top_updates[key] = value
  695. did_any = True
  696. return did_any
  697. def update_entity_appearance(self, entity_id: str, chapter: int, entity_type: str = None):
  698. """更新实体出场章节"""
  699. if not entity_type:
  700. entity_type = self.get_entity_type(entity_id)
  701. if not entity_type:
  702. return
  703. entities_v3 = self._state.get("entities_v3")
  704. if not isinstance(entities_v3, dict):
  705. entities_v3 = {t: {} for t in self.ENTITY_TYPES}
  706. self._state["entities_v3"] = entities_v3
  707. entities_v3.setdefault(entity_type, {})
  708. entity = entities_v3[entity_type].get(entity_id)
  709. if entity:
  710. if entity.get("first_appearance", 0) == 0:
  711. entity["first_appearance"] = chapter
  712. entity["last_appearance"] = chapter
  713. # 记录补丁:锁内应用 first=min(non-zero), last=max
  714. patch = self._pending_entity_patches.get((entity_type, entity_id))
  715. if patch is None:
  716. patch = _EntityPatch(entity_type=entity_type, entity_id=entity_id)
  717. self._pending_entity_patches[(entity_type, entity_id)] = patch
  718. if patch.appearance_chapter is None:
  719. patch.appearance_chapter = chapter
  720. else:
  721. patch.appearance_chapter = max(int(patch.appearance_chapter), int(chapter))
  722. # ==================== 状态变化记录 ====================
  723. def record_state_change(
  724. self,
  725. entity_id: str,
  726. field: str,
  727. old_value: Any,
  728. new_value: Any,
  729. reason: str,
  730. chapter: int
  731. ):
  732. """记录状态变化"""
  733. if "state_changes" not in self._state:
  734. self._state["state_changes"] = []
  735. change = StateChange(
  736. entity_id=entity_id,
  737. field=field,
  738. old_value=old_value,
  739. new_value=new_value,
  740. reason=reason,
  741. chapter=chapter
  742. )
  743. change_dict = asdict(change)
  744. self._state["state_changes"].append(change_dict)
  745. self._pending_state_changes.append(change_dict)
  746. # 同时更新实体属性
  747. self.update_entity(entity_id, {"attributes": {field: new_value}})
  748. def get_state_changes(self, entity_id: Optional[str] = None) -> List[Dict]:
  749. """获取状态变化历史"""
  750. changes = self._state.get("state_changes", [])
  751. if entity_id:
  752. changes = [c for c in changes if c.get("entity_id") == entity_id]
  753. return changes
  754. # ==================== 关系管理 ====================
  755. def add_relationship(
  756. self,
  757. from_entity: str,
  758. to_entity: str,
  759. rel_type: str,
  760. description: str,
  761. chapter: int
  762. ):
  763. """添加关系"""
  764. rel = Relationship(
  765. from_entity=from_entity,
  766. to_entity=to_entity,
  767. type=rel_type,
  768. description=description,
  769. chapter=chapter
  770. )
  771. # v5.0 引入: 实体关系存入 structured_relationships,避免与 relationships(人物关系字典) 冲突
  772. if "structured_relationships" not in self._state:
  773. self._state["structured_relationships"] = []
  774. rel_dict = asdict(rel)
  775. self._state["structured_relationships"].append(rel_dict)
  776. self._pending_structured_relationships.append(rel_dict)
  777. def get_relationships(self, entity_id: Optional[str] = None) -> List[Dict]:
  778. """获取关系列表"""
  779. rels = self._state.get("structured_relationships", [])
  780. if entity_id:
  781. rels = [
  782. r for r in rels
  783. if r.get("from_entity") == entity_id or r.get("to_entity") == entity_id
  784. ]
  785. return rels
  786. # ==================== 批量操作 ====================
  787. def _record_disambiguation(self, chapter: int, uncertain_items: Any) -> List[str]:
  788. """
  789. 记录消歧反馈到 state.json,便于 Writer/Context Agent 感知风险。
  790. 约定:
  791. - >= extraction_confidence_medium:写入 disambiguation_warnings(采用但警告)
  792. - < extraction_confidence_medium:写入 disambiguation_pending(需人工确认)
  793. """
  794. if not isinstance(uncertain_items, list) or not uncertain_items:
  795. return []
  796. warnings: List[str] = []
  797. now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
  798. for item in uncertain_items:
  799. if not isinstance(item, dict):
  800. continue
  801. mention = str(item.get("mention", "") or "").strip()
  802. if not mention:
  803. continue
  804. raw_conf = item.get("confidence", 0.0)
  805. try:
  806. confidence = float(raw_conf)
  807. except (TypeError, ValueError):
  808. confidence = 0.0
  809. # 候选:支持 [{"type","id"}...] 或 ["id1","id2"] 两种形式
  810. candidates_raw = item.get("candidates", [])
  811. candidates: List[Dict[str, str]] = []
  812. if isinstance(candidates_raw, list):
  813. for c in candidates_raw:
  814. if isinstance(c, dict):
  815. cid = str(c.get("id", "") or "").strip()
  816. ctype = str(c.get("type", "") or "").strip()
  817. entry: Dict[str, str] = {}
  818. if ctype:
  819. entry["type"] = ctype
  820. if cid:
  821. entry["id"] = cid
  822. if entry:
  823. candidates.append(entry)
  824. else:
  825. cid = str(c).strip()
  826. if cid:
  827. candidates.append({"id": cid})
  828. entity_type = str(item.get("type", "") or "").strip()
  829. suggested_id = str(item.get("suggested", "") or "").strip()
  830. adopted_raw = item.get("adopted", None)
  831. chosen_id = ""
  832. if isinstance(adopted_raw, str):
  833. chosen_id = adopted_raw.strip()
  834. elif adopted_raw is True:
  835. chosen_id = suggested_id
  836. else:
  837. # 兼容字段名:entity_id / chosen_id
  838. chosen_id = str(item.get("entity_id") or item.get("chosen_id") or "").strip() or suggested_id
  839. context = str(item.get("context", "") or "").strip()
  840. note = str(item.get("warning", "") or "").strip()
  841. record: Dict[str, Any] = {
  842. "chapter": int(chapter),
  843. "mention": mention,
  844. "type": entity_type,
  845. "suggested_id": suggested_id,
  846. "chosen_id": chosen_id,
  847. "confidence": confidence,
  848. "candidates": candidates,
  849. "context": context,
  850. "note": note,
  851. "created_at": now,
  852. }
  853. if confidence >= float(self.config.extraction_confidence_medium):
  854. self._state.setdefault("disambiguation_warnings", []).append(record)
  855. self._pending_disambiguation_warnings.append(record)
  856. chosen_part = f" → {chosen_id}" if chosen_id else ""
  857. warnings.append(f"消歧警告: {mention}{chosen_part} (confidence: {confidence:.2f})")
  858. else:
  859. self._state.setdefault("disambiguation_pending", []).append(record)
  860. self._pending_disambiguation_pending.append(record)
  861. warnings.append(f"消歧需人工确认: {mention} (confidence: {confidence:.2f})")
  862. return warnings
  863. def process_chapter_result(self, chapter: int, result: Dict) -> List[str]:
  864. """
  865. 处理 Data Agent 的章节处理结果(v5.1 引入,v5.4 沿用)
  866. 输入格式:
  867. - entities_appeared: 出场实体列表
  868. - entities_new: 新实体列表
  869. - state_changes: 状态变化列表
  870. - relationships_new: 新关系列表
  871. 返回警告列表
  872. """
  873. warnings = []
  874. # v5.1 引入: 记录章节号用于 SQLite 同步
  875. self._pending_sqlite_data["chapter"] = chapter
  876. # 处理出场实体
  877. for entity in result.get("entities_appeared", []):
  878. entity_id = entity.get("id")
  879. entity_type = entity.get("type")
  880. if entity_id:
  881. self.update_entity_appearance(entity_id, chapter, entity_type)
  882. # v5.1 引入: 缓存用于 SQLite 同步
  883. self._pending_sqlite_data["entities_appeared"].append(entity)
  884. # 处理新实体
  885. for entity in result.get("entities_new", []):
  886. entity_id = entity.get("suggested_id") or entity.get("id")
  887. if entity_id and entity_id != "NEW":
  888. new_entity = EntityState(
  889. id=entity_id,
  890. name=entity.get("name", ""),
  891. type=entity.get("type", "角色"),
  892. tier=entity.get("tier", "装饰"),
  893. aliases=entity.get("mentions", []),
  894. first_appearance=chapter,
  895. last_appearance=chapter
  896. )
  897. if not self.add_entity(new_entity):
  898. warnings.append(f"实体已存在: {entity_id}")
  899. # v5.1 引入: 缓存用于 SQLite 同步
  900. self._pending_sqlite_data["entities_new"].append(entity)
  901. # 处理状态变化
  902. for change in result.get("state_changes", []):
  903. self.record_state_change(
  904. entity_id=change.get("entity_id", ""),
  905. field=change.get("field", ""),
  906. old_value=change.get("old"),
  907. new_value=change.get("new"),
  908. reason=change.get("reason", ""),
  909. chapter=chapter
  910. )
  911. # v5.1 引入: 缓存用于 SQLite 同步
  912. self._pending_sqlite_data["state_changes"].append(change)
  913. # 处理关系
  914. for rel in result.get("relationships_new", []):
  915. self.add_relationship(
  916. from_entity=rel.get("from", ""),
  917. to_entity=rel.get("to", ""),
  918. rel_type=rel.get("type", ""),
  919. description=rel.get("description", ""),
  920. chapter=chapter
  921. )
  922. # v5.1 引入: 缓存用于 SQLite 同步
  923. self._pending_sqlite_data["relationships_new"].append(rel)
  924. # 处理消歧不确定项(不影响实体写入,但必须对 Writer 可见)
  925. warnings.extend(self._record_disambiguation(chapter, result.get("uncertain", [])))
  926. # 写入 chapter_meta(钩子/模式/结束状态)
  927. chapter_meta = result.get("chapter_meta")
  928. if isinstance(chapter_meta, dict):
  929. meta_key = f"{int(chapter):04d}"
  930. self._state.setdefault("chapter_meta", {})
  931. self._state["chapter_meta"][meta_key] = chapter_meta
  932. self._pending_chapter_meta[meta_key] = chapter_meta
  933. # 更新进度
  934. self.update_progress(chapter)
  935. # 同步主角状态(entities_v3 → protagonist_state)
  936. self.sync_protagonist_from_entity()
  937. # 长期记忆写入(best-effort,不阻断主流程)
  938. try:
  939. from .memory.writer import MemoryWriter
  940. writer = MemoryWriter(self.config)
  941. mem_result = writer.update_from_chapter_result(chapter, result)
  942. logger.info("memory_write: %s", mem_result)
  943. except Exception as exc:
  944. logger.warning("memory_write_failed: %s", exc)
  945. return warnings
  946. # ==================== 导出 ====================
  947. def export_for_context(self) -> Dict:
  948. """导出用于上下文的精简版状态(v5.0 引入,v5.4 沿用)"""
  949. # 从 entities_v3 构建精简视图
  950. entities_flat = {}
  951. for type_name, entities in self._state.get("entities_v3", {}).items():
  952. for eid, e in entities.items():
  953. entities_flat[eid] = {
  954. "name": e.get("canonical_name", eid),
  955. "type": type_name,
  956. "tier": e.get("tier", "装饰"),
  957. "current": e.get("current", {})
  958. }
  959. return {
  960. "progress": self._state.get("progress", {}),
  961. "entities": entities_flat,
  962. # v5.1 引入: alias_index 已迁移到 index.db,这里返回空(兼容性)
  963. "alias_index": {},
  964. "recent_changes": [], # v5.1 引入: 从 index.db 查询
  965. "disambiguation": {
  966. "warnings": self._state.get("disambiguation_warnings", [])[-self.config.export_disambiguation_slice:],
  967. "pending": self._state.get("disambiguation_pending", [])[-self.config.export_disambiguation_slice:],
  968. },
  969. }
  970. # ==================== 主角同步 ====================
  971. def get_protagonist_entity_id(self) -> Optional[str]:
  972. """获取主角实体 ID(通过 is_protagonist 标记或 SQLite 查询)"""
  973. # 方式1: 通过 SQLStateManager 查询 (v5.1)
  974. if self._sql_state_manager:
  975. protagonist = self._sql_state_manager.get_protagonist()
  976. if protagonist:
  977. return protagonist.get("id")
  978. # 方式2: 通过 protagonist_state.name 查找别名
  979. protag_name = self._state.get("protagonist_state", {}).get("name")
  980. if protag_name and self._sql_state_manager:
  981. entities = self._sql_state_manager._index_manager.get_entities_by_alias(protag_name)
  982. for entry in entities:
  983. if entry.get("type") == "角色":
  984. return entry.get("id")
  985. return None
  986. def sync_protagonist_from_entity(self, entity_id: str = None):
  987. """
  988. 将主角实体的状态同步到 protagonist_state (v5.1: 从 SQLite 读取)
  989. 用于确保 consistency-checker 等依赖 protagonist_state 的组件获取最新数据
  990. """
  991. if entity_id is None:
  992. entity_id = self.get_protagonist_entity_id()
  993. if entity_id is None:
  994. return
  995. entity = self.get_entity(entity_id, "角色")
  996. if not entity:
  997. return
  998. current = entity.get("current")
  999. if not isinstance(current, dict):
  1000. current = entity.get("current_json", {})
  1001. if isinstance(current, str):
  1002. try:
  1003. current = json.loads(current) if current else {}
  1004. except (json.JSONDecodeError, TypeError):
  1005. current = {}
  1006. if not isinstance(current, dict):
  1007. current = {}
  1008. protag = self._state.setdefault("protagonist_state", {})
  1009. # 同步境界
  1010. if "realm" in current:
  1011. power = protag.setdefault("power", {})
  1012. power["realm"] = current["realm"]
  1013. if "layer" in current:
  1014. power["layer"] = current["layer"]
  1015. # 同步位置
  1016. if "location" in current:
  1017. loc = protag.setdefault("location", {})
  1018. loc["current"] = current["location"]
  1019. if "last_chapter" in current:
  1020. loc["last_chapter"] = current["last_chapter"]
  1021. def sync_protagonist_to_entity(self, entity_id: str = None):
  1022. """
  1023. 将 protagonist_state 同步到 entities_v3 中的主角实体
  1024. 用于初始化或手动编辑 protagonist_state 后保持一致性
  1025. """
  1026. if entity_id is None:
  1027. entity_id = self.get_protagonist_entity_id()
  1028. if entity_id is None:
  1029. return
  1030. protag = self._state.get("protagonist_state", {})
  1031. if not protag:
  1032. return
  1033. updates = {}
  1034. # 同步境界
  1035. power = protag.get("power", {})
  1036. if power.get("realm"):
  1037. updates["realm"] = power["realm"]
  1038. if power.get("layer"):
  1039. updates["layer"] = power["layer"]
  1040. # 同步位置
  1041. loc = protag.get("location", {})
  1042. if loc.get("current"):
  1043. updates["location"] = loc["current"]
  1044. if updates:
  1045. self.update_entity(entity_id, updates, "角色")
  1046. # ==================== CLI 接口 ====================
  1047. def main():
  1048. import argparse
  1049. import sys
  1050. from pydantic import ValidationError
  1051. from .cli_output import print_success, print_error
  1052. from .cli_args import normalize_global_project_root, load_json_arg
  1053. from .schemas import validate_data_agent_output, format_validation_error, normalize_data_agent_output
  1054. from .index_manager import IndexManager
  1055. parser = argparse.ArgumentParser(description="State Manager CLI (v5.4)")
  1056. parser.add_argument("--project-root", type=str, help="项目根目录")
  1057. subparsers = parser.add_subparsers(dest="command")
  1058. # 读取进度
  1059. subparsers.add_parser("get-progress")
  1060. # 获取实体
  1061. get_entity_parser = subparsers.add_parser("get-entity")
  1062. get_entity_parser.add_argument("--id", required=True)
  1063. # 列出实体
  1064. list_parser = subparsers.add_parser("list-entities")
  1065. list_parser.add_argument("--type", help="按类型过滤")
  1066. list_parser.add_argument("--tier", help="按层级过滤")
  1067. # 处理章节结果
  1068. process_parser = subparsers.add_parser("process-chapter")
  1069. process_parser.add_argument("--chapter", type=int, required=True, help="章节号")
  1070. process_parser.add_argument("--data", required=True, help="JSON 格式的处理结果")
  1071. argv = normalize_global_project_root(sys.argv[1:])
  1072. args = parser.parse_args(argv)
  1073. command_started_at = time.perf_counter()
  1074. # 初始化
  1075. config = None
  1076. if args.project_root:
  1077. # 允许传入“工作区根目录”,统一解析到真正的 book project_root(必须包含 .webnovel/state.json)
  1078. from project_locator import resolve_project_root
  1079. from .config import DataModulesConfig
  1080. try:
  1081. resolved_root = resolve_project_root(args.project_root)
  1082. except FileNotFoundError:
  1083. # 兼容旧行为:显式目录无法被 locator 识别时,直接按传入路径初始化。
  1084. resolved_root = Path(args.project_root).expanduser().resolve()
  1085. config = DataModulesConfig.from_project_root(resolved_root)
  1086. manager = StateManager(config)
  1087. logger = IndexManager(config)
  1088. tool_name = f"state_manager:{args.command or 'unknown'}"
  1089. def _append_timing(success: bool, *, error_code: str | None = None, error_message: str | None = None, chapter: int | None = None):
  1090. elapsed_ms = int((time.perf_counter() - command_started_at) * 1000)
  1091. safe_append_perf_timing(
  1092. manager.config.project_root,
  1093. tool_name=tool_name,
  1094. success=success,
  1095. elapsed_ms=elapsed_ms,
  1096. chapter=chapter,
  1097. error_code=error_code,
  1098. error_message=error_message,
  1099. )
  1100. def emit_success(data=None, message: str = "ok", chapter: int | None = None):
  1101. print_success(data, message=message)
  1102. safe_log_tool_call(logger, tool_name=tool_name, success=True)
  1103. _append_timing(True, chapter=chapter)
  1104. def emit_error(code: str, message: str, suggestion: str | None = None, chapter: int | None = None):
  1105. print_error(code, message, suggestion=suggestion)
  1106. safe_log_tool_call(
  1107. logger,
  1108. tool_name=tool_name,
  1109. success=False,
  1110. error_code=code,
  1111. error_message=message,
  1112. )
  1113. _append_timing(False, error_code=code, error_message=message, chapter=chapter)
  1114. if args.command == "get-progress":
  1115. emit_success(manager._state.get("progress", {}), message="progress")
  1116. elif args.command == "get-entity":
  1117. entity = manager.get_entity(args.id)
  1118. if entity:
  1119. emit_success(entity, message="entity")
  1120. else:
  1121. emit_error("NOT_FOUND", f"未找到实体: {args.id}")
  1122. elif args.command == "list-entities":
  1123. if args.type:
  1124. entities = manager.get_entities_by_type(args.type)
  1125. elif args.tier:
  1126. entities = manager.get_entities_by_tier(args.tier)
  1127. else:
  1128. entities = manager.get_all_entities()
  1129. payload = [{"id": eid, **e} for eid, e in entities.items()]
  1130. emit_success(payload, message="entities")
  1131. elif args.command == "process-chapter":
  1132. data = load_json_arg(args.data)
  1133. validated = None
  1134. last_exc = None
  1135. for _ in range(3):
  1136. try:
  1137. validated = validate_data_agent_output(data)
  1138. break
  1139. except ValidationError as exc:
  1140. last_exc = exc
  1141. data = normalize_data_agent_output(data)
  1142. if validated is None:
  1143. err = format_validation_error(last_exc) if last_exc else {
  1144. "code": "SCHEMA_VALIDATION_FAILED",
  1145. "message": "数据结构校验失败",
  1146. "details": {"errors": []},
  1147. "suggestion": "请检查 data-agent 输出字段是否完整且类型正确",
  1148. }
  1149. emit_error(err["code"], err["message"], suggestion=err.get("suggestion"))
  1150. return
  1151. warnings = manager.process_chapter_result(args.chapter, validated.model_dump(by_alias=True))
  1152. manager.save_state()
  1153. emit_success({"chapter": args.chapter, "warnings": warnings}, message="chapter_processed", chapter=args.chapter)
  1154. else:
  1155. emit_error("UNKNOWN_COMMAND", "未指定有效命令", suggestion="请查看 --help")
  1156. if __name__ == "__main__":
  1157. if sys.platform == "win32":
  1158. enable_windows_utf8_stdio()
  1159. main()