state_manager.py 55 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331
  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. # 查找实体
  649. if entity_type:
  650. if entity_id not in self._state.get("entities_v3", {}).get(entity_type, {}):
  651. return False
  652. entity = self._state["entities_v3"][entity_type][entity_id]
  653. else:
  654. entity_type = self.get_entity_type(entity_id)
  655. if not entity_type:
  656. return False
  657. entity = self._state["entities_v3"][entity_type][entity_id]
  658. for key, value in updates.items():
  659. if key == "attributes" and isinstance(value, dict):
  660. # v5.0 引入: attributes 存在 current 字段
  661. if "current" not in entity:
  662. entity["current"] = {}
  663. entity["current"].update(value)
  664. # 记录补丁(current 增量)
  665. patch = self._pending_entity_patches.get((entity_type, entity_id))
  666. if patch is None:
  667. patch = _EntityPatch(entity_type=entity_type, entity_id=entity_id)
  668. self._pending_entity_patches[(entity_type, entity_id)] = patch
  669. patch.current_updates.update(value)
  670. elif key == "current" and isinstance(value, dict):
  671. if "current" not in entity:
  672. entity["current"] = {}
  673. entity["current"].update(value)
  674. patch = self._pending_entity_patches.get((entity_type, entity_id))
  675. if patch is None:
  676. patch = _EntityPatch(entity_type=entity_type, entity_id=entity_id)
  677. self._pending_entity_patches[(entity_type, entity_id)] = patch
  678. patch.current_updates.update(value)
  679. else:
  680. entity[key] = value
  681. patch = self._pending_entity_patches.get((entity_type, entity_id))
  682. if patch is None:
  683. patch = _EntityPatch(entity_type=entity_type, entity_id=entity_id)
  684. self._pending_entity_patches[(entity_type, entity_id)] = patch
  685. patch.top_updates[key] = value
  686. return True
  687. def update_entity_appearance(self, entity_id: str, chapter: int, entity_type: str = None):
  688. """更新实体出场章节"""
  689. if not entity_type:
  690. entity_type = self.get_entity_type(entity_id)
  691. if not entity_type:
  692. return
  693. entities_v3 = self._state.get("entities_v3")
  694. if not isinstance(entities_v3, dict):
  695. entities_v3 = {t: {} for t in self.ENTITY_TYPES}
  696. self._state["entities_v3"] = entities_v3
  697. entities_v3.setdefault(entity_type, {})
  698. entity = entities_v3[entity_type].get(entity_id)
  699. if entity:
  700. if entity.get("first_appearance", 0) == 0:
  701. entity["first_appearance"] = chapter
  702. entity["last_appearance"] = chapter
  703. # 记录补丁:锁内应用 first=min(non-zero), last=max
  704. patch = self._pending_entity_patches.get((entity_type, entity_id))
  705. if patch is None:
  706. patch = _EntityPatch(entity_type=entity_type, entity_id=entity_id)
  707. self._pending_entity_patches[(entity_type, entity_id)] = patch
  708. if patch.appearance_chapter is None:
  709. patch.appearance_chapter = chapter
  710. else:
  711. patch.appearance_chapter = max(int(patch.appearance_chapter), int(chapter))
  712. # ==================== 状态变化记录 ====================
  713. def record_state_change(
  714. self,
  715. entity_id: str,
  716. field: str,
  717. old_value: Any,
  718. new_value: Any,
  719. reason: str,
  720. chapter: int
  721. ):
  722. """记录状态变化"""
  723. if "state_changes" not in self._state:
  724. self._state["state_changes"] = []
  725. change = StateChange(
  726. entity_id=entity_id,
  727. field=field,
  728. old_value=old_value,
  729. new_value=new_value,
  730. reason=reason,
  731. chapter=chapter
  732. )
  733. change_dict = asdict(change)
  734. self._state["state_changes"].append(change_dict)
  735. self._pending_state_changes.append(change_dict)
  736. # 同时更新实体属性
  737. self.update_entity(entity_id, {"attributes": {field: new_value}})
  738. def get_state_changes(self, entity_id: Optional[str] = None) -> List[Dict]:
  739. """获取状态变化历史"""
  740. changes = self._state.get("state_changes", [])
  741. if entity_id:
  742. changes = [c for c in changes if c.get("entity_id") == entity_id]
  743. return changes
  744. # ==================== 关系管理 ====================
  745. def add_relationship(
  746. self,
  747. from_entity: str,
  748. to_entity: str,
  749. rel_type: str,
  750. description: str,
  751. chapter: int
  752. ):
  753. """添加关系"""
  754. rel = Relationship(
  755. from_entity=from_entity,
  756. to_entity=to_entity,
  757. type=rel_type,
  758. description=description,
  759. chapter=chapter
  760. )
  761. # v5.0 引入: 实体关系存入 structured_relationships,避免与 relationships(人物关系字典) 冲突
  762. if "structured_relationships" not in self._state:
  763. self._state["structured_relationships"] = []
  764. rel_dict = asdict(rel)
  765. self._state["structured_relationships"].append(rel_dict)
  766. self._pending_structured_relationships.append(rel_dict)
  767. def get_relationships(self, entity_id: Optional[str] = None) -> List[Dict]:
  768. """获取关系列表"""
  769. rels = self._state.get("structured_relationships", [])
  770. if entity_id:
  771. rels = [
  772. r for r in rels
  773. if r.get("from_entity") == entity_id or r.get("to_entity") == entity_id
  774. ]
  775. return rels
  776. # ==================== 批量操作 ====================
  777. def _record_disambiguation(self, chapter: int, uncertain_items: Any) -> List[str]:
  778. """
  779. 记录消歧反馈到 state.json,便于 Writer/Context Agent 感知风险。
  780. 约定:
  781. - >= extraction_confidence_medium:写入 disambiguation_warnings(采用但警告)
  782. - < extraction_confidence_medium:写入 disambiguation_pending(需人工确认)
  783. """
  784. if not isinstance(uncertain_items, list) or not uncertain_items:
  785. return []
  786. warnings: List[str] = []
  787. now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
  788. for item in uncertain_items:
  789. if not isinstance(item, dict):
  790. continue
  791. mention = str(item.get("mention", "") or "").strip()
  792. if not mention:
  793. continue
  794. raw_conf = item.get("confidence", 0.0)
  795. try:
  796. confidence = float(raw_conf)
  797. except (TypeError, ValueError):
  798. confidence = 0.0
  799. # 候选:支持 [{"type","id"}...] 或 ["id1","id2"] 两种形式
  800. candidates_raw = item.get("candidates", [])
  801. candidates: List[Dict[str, str]] = []
  802. if isinstance(candidates_raw, list):
  803. for c in candidates_raw:
  804. if isinstance(c, dict):
  805. cid = str(c.get("id", "") or "").strip()
  806. ctype = str(c.get("type", "") or "").strip()
  807. entry: Dict[str, str] = {}
  808. if ctype:
  809. entry["type"] = ctype
  810. if cid:
  811. entry["id"] = cid
  812. if entry:
  813. candidates.append(entry)
  814. else:
  815. cid = str(c).strip()
  816. if cid:
  817. candidates.append({"id": cid})
  818. entity_type = str(item.get("type", "") or "").strip()
  819. suggested_id = str(item.get("suggested", "") or "").strip()
  820. adopted_raw = item.get("adopted", None)
  821. chosen_id = ""
  822. if isinstance(adopted_raw, str):
  823. chosen_id = adopted_raw.strip()
  824. elif adopted_raw is True:
  825. chosen_id = suggested_id
  826. else:
  827. # 兼容字段名:entity_id / chosen_id
  828. chosen_id = str(item.get("entity_id") or item.get("chosen_id") or "").strip() or suggested_id
  829. context = str(item.get("context", "") or "").strip()
  830. note = str(item.get("warning", "") or "").strip()
  831. record: Dict[str, Any] = {
  832. "chapter": int(chapter),
  833. "mention": mention,
  834. "type": entity_type,
  835. "suggested_id": suggested_id,
  836. "chosen_id": chosen_id,
  837. "confidence": confidence,
  838. "candidates": candidates,
  839. "context": context,
  840. "note": note,
  841. "created_at": now,
  842. }
  843. if confidence >= float(self.config.extraction_confidence_medium):
  844. self._state.setdefault("disambiguation_warnings", []).append(record)
  845. self._pending_disambiguation_warnings.append(record)
  846. chosen_part = f" → {chosen_id}" if chosen_id else ""
  847. warnings.append(f"消歧警告: {mention}{chosen_part} (confidence: {confidence:.2f})")
  848. else:
  849. self._state.setdefault("disambiguation_pending", []).append(record)
  850. self._pending_disambiguation_pending.append(record)
  851. warnings.append(f"消歧需人工确认: {mention} (confidence: {confidence:.2f})")
  852. return warnings
  853. def process_chapter_result(self, chapter: int, result: Dict) -> List[str]:
  854. """
  855. 处理 Data Agent 的章节处理结果(v5.1 引入,v5.4 沿用)
  856. 输入格式:
  857. - entities_appeared: 出场实体列表
  858. - entities_new: 新实体列表
  859. - state_changes: 状态变化列表
  860. - relationships_new: 新关系列表
  861. 返回警告列表
  862. """
  863. warnings = []
  864. # v5.1 引入: 记录章节号用于 SQLite 同步
  865. self._pending_sqlite_data["chapter"] = chapter
  866. # 处理出场实体
  867. for entity in result.get("entities_appeared", []):
  868. entity_id = entity.get("id")
  869. entity_type = entity.get("type")
  870. if entity_id:
  871. self.update_entity_appearance(entity_id, chapter, entity_type)
  872. # v5.1 引入: 缓存用于 SQLite 同步
  873. self._pending_sqlite_data["entities_appeared"].append(entity)
  874. # 处理新实体
  875. for entity in result.get("entities_new", []):
  876. entity_id = entity.get("suggested_id") or entity.get("id")
  877. if entity_id and entity_id != "NEW":
  878. new_entity = EntityState(
  879. id=entity_id,
  880. name=entity.get("name", ""),
  881. type=entity.get("type", "角色"),
  882. tier=entity.get("tier", "装饰"),
  883. aliases=entity.get("mentions", []),
  884. first_appearance=chapter,
  885. last_appearance=chapter
  886. )
  887. if not self.add_entity(new_entity):
  888. warnings.append(f"实体已存在: {entity_id}")
  889. # v5.1 引入: 缓存用于 SQLite 同步
  890. self._pending_sqlite_data["entities_new"].append(entity)
  891. # 处理状态变化
  892. for change in result.get("state_changes", []):
  893. self.record_state_change(
  894. entity_id=change.get("entity_id", ""),
  895. field=change.get("field", ""),
  896. old_value=change.get("old"),
  897. new_value=change.get("new"),
  898. reason=change.get("reason", ""),
  899. chapter=chapter
  900. )
  901. # v5.1 引入: 缓存用于 SQLite 同步
  902. self._pending_sqlite_data["state_changes"].append(change)
  903. # 处理关系
  904. for rel in result.get("relationships_new", []):
  905. self.add_relationship(
  906. from_entity=rel.get("from", ""),
  907. to_entity=rel.get("to", ""),
  908. rel_type=rel.get("type", ""),
  909. description=rel.get("description", ""),
  910. chapter=chapter
  911. )
  912. # v5.1 引入: 缓存用于 SQLite 同步
  913. self._pending_sqlite_data["relationships_new"].append(rel)
  914. # 处理消歧不确定项(不影响实体写入,但必须对 Writer 可见)
  915. warnings.extend(self._record_disambiguation(chapter, result.get("uncertain", [])))
  916. # 写入 chapter_meta(钩子/模式/结束状态)
  917. chapter_meta = result.get("chapter_meta")
  918. if isinstance(chapter_meta, dict):
  919. meta_key = f"{int(chapter):04d}"
  920. self._state.setdefault("chapter_meta", {})
  921. self._state["chapter_meta"][meta_key] = chapter_meta
  922. self._pending_chapter_meta[meta_key] = chapter_meta
  923. # 更新进度
  924. self.update_progress(chapter)
  925. # 同步主角状态(entities_v3 → protagonist_state)
  926. self.sync_protagonist_from_entity()
  927. return warnings
  928. # ==================== 导出 ====================
  929. def export_for_context(self) -> Dict:
  930. """导出用于上下文的精简版状态(v5.0 引入,v5.4 沿用)"""
  931. # 从 entities_v3 构建精简视图
  932. entities_flat = {}
  933. for type_name, entities in self._state.get("entities_v3", {}).items():
  934. for eid, e in entities.items():
  935. entities_flat[eid] = {
  936. "name": e.get("canonical_name", eid),
  937. "type": type_name,
  938. "tier": e.get("tier", "装饰"),
  939. "current": e.get("current", {})
  940. }
  941. return {
  942. "progress": self._state.get("progress", {}),
  943. "entities": entities_flat,
  944. # v5.1 引入: alias_index 已迁移到 index.db,这里返回空(兼容性)
  945. "alias_index": {},
  946. "recent_changes": [], # v5.1 引入: 从 index.db 查询
  947. "disambiguation": {
  948. "warnings": self._state.get("disambiguation_warnings", [])[-self.config.export_disambiguation_slice:],
  949. "pending": self._state.get("disambiguation_pending", [])[-self.config.export_disambiguation_slice:],
  950. },
  951. }
  952. # ==================== 主角同步 ====================
  953. def get_protagonist_entity_id(self) -> Optional[str]:
  954. """获取主角实体 ID(通过 is_protagonist 标记或 SQLite 查询)"""
  955. # 方式1: 通过 SQLStateManager 查询 (v5.1)
  956. if self._sql_state_manager:
  957. protagonist = self._sql_state_manager.get_protagonist()
  958. if protagonist:
  959. return protagonist.get("id")
  960. # 方式2: 通过 protagonist_state.name 查找别名
  961. protag_name = self._state.get("protagonist_state", {}).get("name")
  962. if protag_name and self._sql_state_manager:
  963. entities = self._sql_state_manager._index_manager.get_entities_by_alias(protag_name)
  964. for entry in entities:
  965. if entry.get("type") == "角色":
  966. return entry.get("id")
  967. return None
  968. def sync_protagonist_from_entity(self, entity_id: str = None):
  969. """
  970. 将主角实体的状态同步到 protagonist_state (v5.1: 从 SQLite 读取)
  971. 用于确保 consistency-checker 等依赖 protagonist_state 的组件获取最新数据
  972. """
  973. if entity_id is None:
  974. entity_id = self.get_protagonist_entity_id()
  975. if entity_id is None:
  976. return
  977. entity = self.get_entity(entity_id, "角色")
  978. if not entity:
  979. return
  980. current = entity.get("current")
  981. if not isinstance(current, dict):
  982. current = entity.get("current_json", {})
  983. if isinstance(current, str):
  984. try:
  985. current = json.loads(current) if current else {}
  986. except (json.JSONDecodeError, TypeError):
  987. current = {}
  988. if not isinstance(current, dict):
  989. current = {}
  990. protag = self._state.setdefault("protagonist_state", {})
  991. # 同步境界
  992. if "realm" in current:
  993. power = protag.setdefault("power", {})
  994. power["realm"] = current["realm"]
  995. if "layer" in current:
  996. power["layer"] = current["layer"]
  997. # 同步位置
  998. if "location" in current:
  999. loc = protag.setdefault("location", {})
  1000. loc["current"] = current["location"]
  1001. if "last_chapter" in current:
  1002. loc["last_chapter"] = current["last_chapter"]
  1003. def sync_protagonist_to_entity(self, entity_id: str = None):
  1004. """
  1005. 将 protagonist_state 同步到 entities_v3 中的主角实体
  1006. 用于初始化或手动编辑 protagonist_state 后保持一致性
  1007. """
  1008. if entity_id is None:
  1009. entity_id = self.get_protagonist_entity_id()
  1010. if entity_id is None:
  1011. return
  1012. protag = self._state.get("protagonist_state", {})
  1013. if not protag:
  1014. return
  1015. updates = {}
  1016. # 同步境界
  1017. power = protag.get("power", {})
  1018. if power.get("realm"):
  1019. updates["realm"] = power["realm"]
  1020. if power.get("layer"):
  1021. updates["layer"] = power["layer"]
  1022. # 同步位置
  1023. loc = protag.get("location", {})
  1024. if loc.get("current"):
  1025. updates["location"] = loc["current"]
  1026. if updates:
  1027. self.update_entity(entity_id, updates, "角色")
  1028. # ==================== CLI 接口 ====================
  1029. def main():
  1030. import argparse
  1031. from pydantic import ValidationError
  1032. from .cli_output import print_success, print_error
  1033. from .schemas import validate_data_agent_output, format_validation_error, normalize_data_agent_output
  1034. from .index_manager import IndexManager
  1035. parser = argparse.ArgumentParser(description="State Manager CLI (v5.4)")
  1036. parser.add_argument("--project-root", type=str, help="项目根目录")
  1037. subparsers = parser.add_subparsers(dest="command")
  1038. # 读取进度
  1039. subparsers.add_parser("get-progress")
  1040. # 获取实体
  1041. get_entity_parser = subparsers.add_parser("get-entity")
  1042. get_entity_parser.add_argument("--id", required=True)
  1043. # 列出实体
  1044. list_parser = subparsers.add_parser("list-entities")
  1045. list_parser.add_argument("--type", help="按类型过滤")
  1046. list_parser.add_argument("--tier", help="按层级过滤")
  1047. # 处理章节结果
  1048. process_parser = subparsers.add_parser("process-chapter")
  1049. process_parser.add_argument("--chapter", type=int, required=True, help="章节号")
  1050. process_parser.add_argument("--data", required=True, help="JSON 格式的处理结果")
  1051. args = parser.parse_args()
  1052. command_started_at = time.perf_counter()
  1053. # 初始化
  1054. config = None
  1055. if args.project_root:
  1056. from .config import DataModulesConfig
  1057. config = DataModulesConfig.from_project_root(args.project_root)
  1058. manager = StateManager(config)
  1059. logger = IndexManager(config)
  1060. tool_name = f"state_manager:{args.command or 'unknown'}"
  1061. def _append_timing(success: bool, *, error_code: str | None = None, error_message: str | None = None, chapter: int | None = None):
  1062. elapsed_ms = int((time.perf_counter() - command_started_at) * 1000)
  1063. safe_append_perf_timing(
  1064. manager.config.project_root,
  1065. tool_name=tool_name,
  1066. success=success,
  1067. elapsed_ms=elapsed_ms,
  1068. chapter=chapter,
  1069. error_code=error_code,
  1070. error_message=error_message,
  1071. )
  1072. def emit_success(data=None, message: str = "ok", chapter: int | None = None):
  1073. print_success(data, message=message)
  1074. safe_log_tool_call(logger, tool_name=tool_name, success=True)
  1075. _append_timing(True, chapter=chapter)
  1076. def emit_error(code: str, message: str, suggestion: str | None = None, chapter: int | None = None):
  1077. print_error(code, message, suggestion=suggestion)
  1078. safe_log_tool_call(
  1079. logger,
  1080. tool_name=tool_name,
  1081. success=False,
  1082. error_code=code,
  1083. error_message=message,
  1084. )
  1085. _append_timing(False, error_code=code, error_message=message, chapter=chapter)
  1086. if args.command == "get-progress":
  1087. emit_success(manager._state.get("progress", {}), message="progress")
  1088. elif args.command == "get-entity":
  1089. entity = manager.get_entity(args.id)
  1090. if entity:
  1091. emit_success(entity, message="entity")
  1092. else:
  1093. emit_error("NOT_FOUND", f"未找到实体: {args.id}")
  1094. elif args.command == "list-entities":
  1095. if args.type:
  1096. entities = manager.get_entities_by_type(args.type)
  1097. elif args.tier:
  1098. entities = manager.get_entities_by_tier(args.tier)
  1099. else:
  1100. entities = manager.get_all_entities()
  1101. payload = [{"id": eid, **e} for eid, e in entities.items()]
  1102. emit_success(payload, message="entities")
  1103. elif args.command == "process-chapter":
  1104. data = json.loads(args.data)
  1105. validated = None
  1106. last_exc = None
  1107. for _ in range(3):
  1108. try:
  1109. validated = validate_data_agent_output(data)
  1110. break
  1111. except ValidationError as exc:
  1112. last_exc = exc
  1113. data = normalize_data_agent_output(data)
  1114. if validated is None:
  1115. err = format_validation_error(last_exc) if last_exc else {
  1116. "code": "SCHEMA_VALIDATION_FAILED",
  1117. "message": "数据结构校验失败",
  1118. "details": {"errors": []},
  1119. "suggestion": "请检查 data-agent 输出字段是否完整且类型正确",
  1120. }
  1121. emit_error(err["code"], err["message"], suggestion=err.get("suggestion"))
  1122. return
  1123. warnings = manager.process_chapter_result(args.chapter, validated.model_dump(by_alias=True))
  1124. manager.save_state()
  1125. emit_success({"chapter": args.chapter, "warnings": warnings}, message="chapter_processed", chapter=args.chapter)
  1126. else:
  1127. emit_error("UNKNOWN_COMMAND", "未指定有效命令", suggestion="请查看 --help")
  1128. if __name__ == "__main__":
  1129. if sys.platform == "win32":
  1130. enable_windows_utf8_stdio()
  1131. main()