state_manager.py 54 KB

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