1
0

index_manager.py 43 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """
  4. Index Manager - 索引管理模块 (v5.1)
  5. 管理 index.db (SQLite) 的读写操作:
  6. - 章节元数据索引
  7. - 实体出场记录
  8. - 场景索引
  9. - 实体存储 (从 state.json 迁移)
  10. - 别名索引 (一对多)
  11. - 状态变化记录
  12. - 关系存储
  13. - 快速查询接口
  14. v5.1 变更:
  15. - 新增 entities 表替代 state.json 中的 entities_v3
  16. - 新增 aliases 表替代 state.json 中的 alias_index (支持一对多)
  17. - 新增 state_changes 表替代 state.json 中的 state_changes
  18. - 新增 relationships 表替代 state.json 中的 structured_relationships
  19. """
  20. import sqlite3
  21. import json
  22. from pathlib import Path
  23. from typing import Dict, List, Optional, Any, Tuple
  24. from dataclasses import dataclass, field
  25. from contextlib import contextmanager
  26. from datetime import datetime
  27. from .config import get_config
  28. @dataclass
  29. class ChapterMeta:
  30. """章节元数据"""
  31. chapter: int
  32. title: str
  33. location: str
  34. word_count: int
  35. characters: List[str]
  36. summary: str = ""
  37. @dataclass
  38. class SceneMeta:
  39. """场景元数据"""
  40. chapter: int
  41. scene_index: int
  42. start_line: int
  43. end_line: int
  44. location: str
  45. summary: str
  46. characters: List[str]
  47. @dataclass
  48. class EntityMeta:
  49. """实体元数据 (v5.1 新增)"""
  50. id: str
  51. type: str # 角色/地点/物品/势力/招式
  52. canonical_name: str
  53. tier: str = "装饰" # 核心/重要/次要/装饰
  54. desc: str = ""
  55. current: Dict = field(default_factory=dict) # 当前状态 (realm/location/items等)
  56. first_appearance: int = 0
  57. last_appearance: int = 0
  58. is_protagonist: bool = False
  59. is_archived: bool = False
  60. @dataclass
  61. class StateChangeMeta:
  62. """状态变化记录 (v5.1 新增)"""
  63. entity_id: str
  64. field: str
  65. old_value: str
  66. new_value: str
  67. reason: str
  68. chapter: int
  69. @dataclass
  70. class RelationshipMeta:
  71. """关系记录 (v5.1 新增)"""
  72. from_entity: str
  73. to_entity: str
  74. type: str
  75. description: str
  76. chapter: int
  77. class IndexManager:
  78. """索引管理器"""
  79. def __init__(self, config=None):
  80. self.config = config or get_config()
  81. self._init_db()
  82. def _init_db(self):
  83. """初始化数据库表"""
  84. self.config.ensure_dirs()
  85. with self._get_conn() as conn:
  86. cursor = conn.cursor()
  87. # 章节表
  88. cursor.execute("""
  89. CREATE TABLE IF NOT EXISTS chapters (
  90. chapter INTEGER PRIMARY KEY,
  91. title TEXT,
  92. location TEXT,
  93. word_count INTEGER,
  94. characters TEXT,
  95. summary TEXT,
  96. created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
  97. )
  98. """)
  99. # 场景表
  100. cursor.execute("""
  101. CREATE TABLE IF NOT EXISTS scenes (
  102. id INTEGER PRIMARY KEY AUTOINCREMENT,
  103. chapter INTEGER,
  104. scene_index INTEGER,
  105. start_line INTEGER,
  106. end_line INTEGER,
  107. location TEXT,
  108. summary TEXT,
  109. characters TEXT,
  110. UNIQUE(chapter, scene_index)
  111. )
  112. """)
  113. # 实体出场表
  114. cursor.execute("""
  115. CREATE TABLE IF NOT EXISTS appearances (
  116. id INTEGER PRIMARY KEY AUTOINCREMENT,
  117. entity_id TEXT,
  118. chapter INTEGER,
  119. mentions TEXT,
  120. confidence REAL,
  121. UNIQUE(entity_id, chapter)
  122. )
  123. """)
  124. # 创建索引
  125. cursor.execute("CREATE INDEX IF NOT EXISTS idx_scenes_chapter ON scenes(chapter)")
  126. cursor.execute("CREATE INDEX IF NOT EXISTS idx_appearances_entity ON appearances(entity_id)")
  127. cursor.execute("CREATE INDEX IF NOT EXISTS idx_appearances_chapter ON appearances(chapter)")
  128. # ==================== v5.1 新增表 ====================
  129. # 实体表 (替代 state.json 中的 entities_v3)
  130. cursor.execute("""
  131. CREATE TABLE IF NOT EXISTS entities (
  132. id TEXT PRIMARY KEY,
  133. type TEXT NOT NULL,
  134. canonical_name TEXT NOT NULL,
  135. tier TEXT DEFAULT '装饰',
  136. desc TEXT,
  137. current_json TEXT,
  138. first_appearance INTEGER DEFAULT 0,
  139. last_appearance INTEGER DEFAULT 0,
  140. is_protagonist INTEGER DEFAULT 0,
  141. is_archived INTEGER DEFAULT 0,
  142. created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  143. updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
  144. )
  145. """)
  146. # 别名表 (替代 state.json 中的 alias_index,支持一对多)
  147. cursor.execute("""
  148. CREATE TABLE IF NOT EXISTS aliases (
  149. alias TEXT NOT NULL,
  150. entity_id TEXT NOT NULL,
  151. entity_type TEXT NOT NULL,
  152. created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  153. PRIMARY KEY (alias, entity_id, entity_type)
  154. )
  155. """)
  156. # 状态变化表 (替代 state.json 中的 state_changes)
  157. cursor.execute("""
  158. CREATE TABLE IF NOT EXISTS state_changes (
  159. id INTEGER PRIMARY KEY AUTOINCREMENT,
  160. entity_id TEXT NOT NULL,
  161. field TEXT NOT NULL,
  162. old_value TEXT,
  163. new_value TEXT,
  164. reason TEXT,
  165. chapter INTEGER NOT NULL,
  166. created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
  167. )
  168. """)
  169. # 关系表 (替代 state.json 中的 structured_relationships)
  170. cursor.execute("""
  171. CREATE TABLE IF NOT EXISTS relationships (
  172. id INTEGER PRIMARY KEY AUTOINCREMENT,
  173. from_entity TEXT NOT NULL,
  174. to_entity TEXT NOT NULL,
  175. type TEXT NOT NULL,
  176. description TEXT,
  177. chapter INTEGER NOT NULL,
  178. created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  179. UNIQUE(from_entity, to_entity, type)
  180. )
  181. """)
  182. # v5.1 新索引
  183. cursor.execute("CREATE INDEX IF NOT EXISTS idx_entities_type ON entities(type)")
  184. cursor.execute("CREATE INDEX IF NOT EXISTS idx_entities_tier ON entities(tier)")
  185. cursor.execute("CREATE INDEX IF NOT EXISTS idx_entities_protagonist ON entities(is_protagonist)")
  186. cursor.execute("CREATE INDEX IF NOT EXISTS idx_aliases_entity ON aliases(entity_id)")
  187. cursor.execute("CREATE INDEX IF NOT EXISTS idx_aliases_alias ON aliases(alias)")
  188. cursor.execute("CREATE INDEX IF NOT EXISTS idx_state_changes_entity ON state_changes(entity_id)")
  189. cursor.execute("CREATE INDEX IF NOT EXISTS idx_state_changes_chapter ON state_changes(chapter)")
  190. cursor.execute("CREATE INDEX IF NOT EXISTS idx_relationships_from ON relationships(from_entity)")
  191. cursor.execute("CREATE INDEX IF NOT EXISTS idx_relationships_to ON relationships(to_entity)")
  192. cursor.execute("CREATE INDEX IF NOT EXISTS idx_relationships_chapter ON relationships(chapter)")
  193. conn.commit()
  194. @contextmanager
  195. def _get_conn(self):
  196. """获取数据库连接"""
  197. conn = sqlite3.connect(str(self.config.index_db))
  198. conn.row_factory = sqlite3.Row
  199. try:
  200. yield conn
  201. finally:
  202. conn.close()
  203. # ==================== 章节操作 ====================
  204. def add_chapter(self, meta: ChapterMeta):
  205. """添加/更新章节元数据"""
  206. with self._get_conn() as conn:
  207. cursor = conn.cursor()
  208. cursor.execute("""
  209. INSERT OR REPLACE INTO chapters
  210. (chapter, title, location, word_count, characters, summary)
  211. VALUES (?, ?, ?, ?, ?, ?)
  212. """, (
  213. meta.chapter,
  214. meta.title,
  215. meta.location,
  216. meta.word_count,
  217. json.dumps(meta.characters, ensure_ascii=False),
  218. meta.summary
  219. ))
  220. conn.commit()
  221. def get_chapter(self, chapter: int) -> Optional[Dict]:
  222. """获取章节元数据"""
  223. with self._get_conn() as conn:
  224. cursor = conn.cursor()
  225. cursor.execute("SELECT * FROM chapters WHERE chapter = ?", (chapter,))
  226. row = cursor.fetchone()
  227. if row:
  228. return self._row_to_dict(row, parse_json=["characters"])
  229. return None
  230. def get_recent_chapters(self, limit: int = None) -> List[Dict]:
  231. """获取最近章节"""
  232. if limit is None:
  233. limit = self.config.query_recent_chapters_limit
  234. with self._get_conn() as conn:
  235. cursor = conn.cursor()
  236. cursor.execute("""
  237. SELECT * FROM chapters
  238. ORDER BY chapter DESC
  239. LIMIT ?
  240. """, (limit,))
  241. return [self._row_to_dict(row, parse_json=["characters"]) for row in cursor.fetchall()]
  242. # ==================== 场景操作 ====================
  243. def add_scenes(self, chapter: int, scenes: List[SceneMeta]):
  244. """添加章节场景"""
  245. with self._get_conn() as conn:
  246. cursor = conn.cursor()
  247. # 先删除该章节旧场景
  248. cursor.execute("DELETE FROM scenes WHERE chapter = ?", (chapter,))
  249. # 插入新场景
  250. for scene in scenes:
  251. cursor.execute("""
  252. INSERT INTO scenes
  253. (chapter, scene_index, start_line, end_line, location, summary, characters)
  254. VALUES (?, ?, ?, ?, ?, ?, ?)
  255. """, (
  256. scene.chapter,
  257. scene.scene_index,
  258. scene.start_line,
  259. scene.end_line,
  260. scene.location,
  261. scene.summary,
  262. json.dumps(scene.characters, ensure_ascii=False)
  263. ))
  264. conn.commit()
  265. def get_scenes(self, chapter: int) -> List[Dict]:
  266. """获取章节场景"""
  267. with self._get_conn() as conn:
  268. cursor = conn.cursor()
  269. cursor.execute("""
  270. SELECT * FROM scenes
  271. WHERE chapter = ?
  272. ORDER BY scene_index
  273. """, (chapter,))
  274. return [self._row_to_dict(row, parse_json=["characters"]) for row in cursor.fetchall()]
  275. def search_scenes_by_location(self, location: str, limit: int = None) -> List[Dict]:
  276. """按地点搜索场景"""
  277. if limit is None:
  278. limit = self.config.query_scenes_by_location_limit
  279. with self._get_conn() as conn:
  280. cursor = conn.cursor()
  281. cursor.execute("""
  282. SELECT * FROM scenes
  283. WHERE location LIKE ?
  284. ORDER BY chapter DESC
  285. LIMIT ?
  286. """, (f"%{location}%", limit))
  287. return [self._row_to_dict(row, parse_json=["characters"]) for row in cursor.fetchall()]
  288. # ==================== 出场记录操作 ====================
  289. def record_appearance(
  290. self,
  291. entity_id: str,
  292. chapter: int,
  293. mentions: List[str],
  294. confidence: float = 1.0,
  295. skip_if_exists: bool = False
  296. ):
  297. """记录实体出场
  298. Args:
  299. entity_id: 实体ID
  300. chapter: 章节号
  301. mentions: 提及列表
  302. confidence: 置信度
  303. skip_if_exists: 如果为True,当记录已存在时跳过(避免覆盖已有mentions)
  304. """
  305. with self._get_conn() as conn:
  306. cursor = conn.cursor()
  307. if skip_if_exists:
  308. # 先检查是否已存在
  309. cursor.execute(
  310. "SELECT 1 FROM appearances WHERE entity_id = ? AND chapter = ?",
  311. (entity_id, chapter)
  312. )
  313. if cursor.fetchone():
  314. return # 已存在,跳过
  315. cursor.execute("""
  316. INSERT OR REPLACE INTO appearances
  317. (entity_id, chapter, mentions, confidence)
  318. VALUES (?, ?, ?, ?)
  319. """, (
  320. entity_id,
  321. chapter,
  322. json.dumps(mentions, ensure_ascii=False),
  323. confidence
  324. ))
  325. conn.commit()
  326. def get_entity_appearances(self, entity_id: str, limit: int = None) -> List[Dict]:
  327. """获取实体出场记录"""
  328. if limit is None:
  329. limit = self.config.query_entity_appearances_limit
  330. with self._get_conn() as conn:
  331. cursor = conn.cursor()
  332. cursor.execute("""
  333. SELECT * FROM appearances
  334. WHERE entity_id = ?
  335. ORDER BY chapter DESC
  336. LIMIT ?
  337. """, (entity_id, limit))
  338. return [self._row_to_dict(row, parse_json=["mentions"]) for row in cursor.fetchall()]
  339. def get_recent_appearances(self, limit: int = None) -> List[Dict]:
  340. """获取最近出场的实体"""
  341. if limit is None:
  342. limit = self.config.query_recent_appearances_limit
  343. with self._get_conn() as conn:
  344. cursor = conn.cursor()
  345. cursor.execute("""
  346. SELECT entity_id, MAX(chapter) as last_chapter, COUNT(*) as total
  347. FROM appearances
  348. GROUP BY entity_id
  349. ORDER BY last_chapter DESC
  350. LIMIT ?
  351. """, (limit,))
  352. return [dict(row) for row in cursor.fetchall()]
  353. def get_chapter_appearances(self, chapter: int) -> List[Dict]:
  354. """获取某章所有出场实体"""
  355. with self._get_conn() as conn:
  356. cursor = conn.cursor()
  357. cursor.execute("""
  358. SELECT * FROM appearances
  359. WHERE chapter = ?
  360. ORDER BY confidence DESC
  361. """, (chapter,))
  362. return [self._row_to_dict(row, parse_json=["mentions"]) for row in cursor.fetchall()]
  363. # ==================== v5.1 实体操作 ====================
  364. def upsert_entity(self, entity: EntityMeta, update_metadata: bool = False) -> bool:
  365. """
  366. 插入或更新实体 (智能合并)
  367. - 新实体: 直接插入
  368. - 已存在: 更新 current_json, last_appearance, updated_at
  369. - update_metadata=True: 同时更新 canonical_name/tier/desc/is_protagonist/is_archived
  370. 返回是否为新实体
  371. """
  372. with self._get_conn() as conn:
  373. cursor = conn.cursor()
  374. # 检查是否存在
  375. cursor.execute("SELECT id, current_json FROM entities WHERE id = ?", (entity.id,))
  376. existing = cursor.fetchone()
  377. if existing:
  378. # 已存在: 智能合并 current_json
  379. old_current = {}
  380. if existing["current_json"]:
  381. try:
  382. old_current = json.loads(existing["current_json"])
  383. except json.JSONDecodeError:
  384. pass
  385. # 合并 current (新值覆盖旧值)
  386. merged_current = {**old_current, **entity.current}
  387. if update_metadata:
  388. # 完整更新(包括元数据)
  389. cursor.execute("""
  390. UPDATE entities SET
  391. canonical_name = ?,
  392. tier = ?,
  393. desc = ?,
  394. current_json = ?,
  395. last_appearance = ?,
  396. is_protagonist = ?,
  397. is_archived = ?,
  398. updated_at = CURRENT_TIMESTAMP
  399. WHERE id = ?
  400. """, (
  401. entity.canonical_name,
  402. entity.tier,
  403. entity.desc,
  404. json.dumps(merged_current, ensure_ascii=False),
  405. entity.last_appearance,
  406. 1 if entity.is_protagonist else 0,
  407. 1 if entity.is_archived else 0,
  408. entity.id
  409. ))
  410. else:
  411. # 只更新 current 和 last_appearance
  412. cursor.execute("""
  413. UPDATE entities SET
  414. current_json = ?,
  415. last_appearance = ?,
  416. updated_at = CURRENT_TIMESTAMP
  417. WHERE id = ?
  418. """, (
  419. json.dumps(merged_current, ensure_ascii=False),
  420. entity.last_appearance,
  421. entity.id
  422. ))
  423. conn.commit()
  424. return False
  425. else:
  426. # 新实体: 插入
  427. cursor.execute("""
  428. INSERT INTO entities
  429. (id, type, canonical_name, tier, desc, current_json,
  430. first_appearance, last_appearance, is_protagonist, is_archived)
  431. VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
  432. """, (
  433. entity.id,
  434. entity.type,
  435. entity.canonical_name,
  436. entity.tier,
  437. entity.desc,
  438. json.dumps(entity.current, ensure_ascii=False),
  439. entity.first_appearance,
  440. entity.last_appearance,
  441. 1 if entity.is_protagonist else 0,
  442. 1 if entity.is_archived else 0
  443. ))
  444. conn.commit()
  445. return True
  446. def get_entity(self, entity_id: str) -> Optional[Dict]:
  447. """获取单个实体"""
  448. with self._get_conn() as conn:
  449. cursor = conn.cursor()
  450. cursor.execute("SELECT * FROM entities WHERE id = ?", (entity_id,))
  451. row = cursor.fetchone()
  452. if row:
  453. return self._row_to_dict(row, parse_json=["current_json"])
  454. return None
  455. def get_entities_by_type(self, entity_type: str, include_archived: bool = False) -> List[Dict]:
  456. """按类型获取实体"""
  457. with self._get_conn() as conn:
  458. cursor = conn.cursor()
  459. if include_archived:
  460. cursor.execute("""
  461. SELECT * FROM entities WHERE type = ?
  462. ORDER BY last_appearance DESC
  463. """, (entity_type,))
  464. else:
  465. cursor.execute("""
  466. SELECT * FROM entities WHERE type = ? AND is_archived = 0
  467. ORDER BY last_appearance DESC
  468. """, (entity_type,))
  469. return [self._row_to_dict(row, parse_json=["current_json"]) for row in cursor.fetchall()]
  470. def get_entities_by_tier(self, tier: str) -> List[Dict]:
  471. """按重要度获取实体 (核心/重要/次要/装饰)"""
  472. with self._get_conn() as conn:
  473. cursor = conn.cursor()
  474. cursor.execute("""
  475. SELECT * FROM entities WHERE tier = ? AND is_archived = 0
  476. ORDER BY last_appearance DESC
  477. """, (tier,))
  478. return [self._row_to_dict(row, parse_json=["current_json"]) for row in cursor.fetchall()]
  479. def get_core_entities(self) -> List[Dict]:
  480. """获取所有核心实体 (用于 Context Agent 全量加载)"""
  481. with self._get_conn() as conn:
  482. cursor = conn.cursor()
  483. cursor.execute("""
  484. SELECT * FROM entities
  485. WHERE (tier IN ('核心', '重要') OR is_protagonist = 1) AND is_archived = 0
  486. ORDER BY is_protagonist DESC, tier, last_appearance DESC
  487. """)
  488. return [self._row_to_dict(row, parse_json=["current_json"]) for row in cursor.fetchall()]
  489. def get_protagonist(self) -> Optional[Dict]:
  490. """获取主角实体"""
  491. with self._get_conn() as conn:
  492. cursor = conn.cursor()
  493. cursor.execute("SELECT * FROM entities WHERE is_protagonist = 1 LIMIT 1")
  494. row = cursor.fetchone()
  495. if row:
  496. return self._row_to_dict(row, parse_json=["current_json"])
  497. return None
  498. def update_entity_current(self, entity_id: str, updates: Dict) -> bool:
  499. """
  500. 增量更新实体的 current 字段 (不覆盖其他字段)
  501. 例如: update_entity_current("xiaoyan", {"realm": "斗师"})
  502. """
  503. with self._get_conn() as conn:
  504. cursor = conn.cursor()
  505. cursor.execute("SELECT current_json FROM entities WHERE id = ?", (entity_id,))
  506. row = cursor.fetchone()
  507. if not row:
  508. return False
  509. current = {}
  510. if row["current_json"]:
  511. try:
  512. current = json.loads(row["current_json"])
  513. except json.JSONDecodeError:
  514. pass
  515. current.update(updates)
  516. cursor.execute("""
  517. UPDATE entities SET
  518. current_json = ?,
  519. updated_at = CURRENT_TIMESTAMP
  520. WHERE id = ?
  521. """, (json.dumps(current, ensure_ascii=False), entity_id))
  522. conn.commit()
  523. return True
  524. def archive_entity(self, entity_id: str) -> bool:
  525. """归档实体 (不删除,只是标记)"""
  526. with self._get_conn() as conn:
  527. cursor = conn.cursor()
  528. cursor.execute("""
  529. UPDATE entities SET is_archived = 1, updated_at = CURRENT_TIMESTAMP
  530. WHERE id = ?
  531. """, (entity_id,))
  532. conn.commit()
  533. return cursor.rowcount > 0
  534. # ==================== v5.1 别名操作 ====================
  535. def register_alias(self, alias: str, entity_id: str, entity_type: str) -> bool:
  536. """
  537. 注册别名 (支持一对多)
  538. 同一别名可映射多个实体 (如 "天云宗" → 地点 + 势力)
  539. """
  540. with self._get_conn() as conn:
  541. cursor = conn.cursor()
  542. try:
  543. cursor.execute("""
  544. INSERT OR IGNORE INTO aliases (alias, entity_id, entity_type)
  545. VALUES (?, ?, ?)
  546. """, (alias, entity_id, entity_type))
  547. conn.commit()
  548. return cursor.rowcount > 0
  549. except sqlite3.IntegrityError:
  550. return False
  551. def get_entities_by_alias(self, alias: str) -> List[Dict]:
  552. """
  553. 根据别名查找实体 (一对多)
  554. 返回所有匹配的实体 (可能有多个不同类型)
  555. """
  556. with self._get_conn() as conn:
  557. cursor = conn.cursor()
  558. cursor.execute("""
  559. SELECT e.*, a.entity_type as alias_type
  560. FROM entities e
  561. JOIN aliases a ON e.id = a.entity_id
  562. WHERE a.alias = ?
  563. """, (alias,))
  564. return [self._row_to_dict(row, parse_json=["current_json"]) for row in cursor.fetchall()]
  565. def get_entity_aliases(self, entity_id: str) -> List[str]:
  566. """获取实体的所有别名"""
  567. with self._get_conn() as conn:
  568. cursor = conn.cursor()
  569. cursor.execute("SELECT alias FROM aliases WHERE entity_id = ?", (entity_id,))
  570. return [row["alias"] for row in cursor.fetchall()]
  571. def remove_alias(self, alias: str, entity_id: str) -> bool:
  572. """移除别名"""
  573. with self._get_conn() as conn:
  574. cursor = conn.cursor()
  575. cursor.execute("DELETE FROM aliases WHERE alias = ? AND entity_id = ?", (alias, entity_id))
  576. conn.commit()
  577. return cursor.rowcount > 0
  578. # ==================== v5.1 状态变化操作 ====================
  579. def record_state_change(self, change: StateChangeMeta) -> int:
  580. """
  581. 记录状态变化
  582. 返回记录 ID
  583. """
  584. with self._get_conn() as conn:
  585. cursor = conn.cursor()
  586. cursor.execute("""
  587. INSERT INTO state_changes
  588. (entity_id, field, old_value, new_value, reason, chapter)
  589. VALUES (?, ?, ?, ?, ?, ?)
  590. """, (
  591. change.entity_id,
  592. change.field,
  593. change.old_value,
  594. change.new_value,
  595. change.reason,
  596. change.chapter
  597. ))
  598. conn.commit()
  599. return cursor.lastrowid
  600. def get_entity_state_changes(self, entity_id: str, limit: int = 20) -> List[Dict]:
  601. """获取实体的状态变化历史"""
  602. with self._get_conn() as conn:
  603. cursor = conn.cursor()
  604. cursor.execute("""
  605. SELECT * FROM state_changes
  606. WHERE entity_id = ?
  607. ORDER BY chapter DESC, id DESC
  608. LIMIT ?
  609. """, (entity_id, limit))
  610. return [dict(row) for row in cursor.fetchall()]
  611. def get_recent_state_changes(self, limit: int = 50) -> List[Dict]:
  612. """获取最近的状态变化"""
  613. with self._get_conn() as conn:
  614. cursor = conn.cursor()
  615. cursor.execute("""
  616. SELECT * FROM state_changes
  617. ORDER BY chapter DESC, id DESC
  618. LIMIT ?
  619. """, (limit,))
  620. return [dict(row) for row in cursor.fetchall()]
  621. def get_chapter_state_changes(self, chapter: int) -> List[Dict]:
  622. """获取某章的所有状态变化"""
  623. with self._get_conn() as conn:
  624. cursor = conn.cursor()
  625. cursor.execute("""
  626. SELECT * FROM state_changes
  627. WHERE chapter = ?
  628. ORDER BY id
  629. """, (chapter,))
  630. return [dict(row) for row in cursor.fetchall()]
  631. # ==================== v5.1 关系操作 ====================
  632. def upsert_relationship(self, rel: RelationshipMeta) -> bool:
  633. """
  634. 插入或更新关系
  635. 相同 (from, to, type) 会更新 description 和 chapter
  636. 返回是否为新关系
  637. """
  638. with self._get_conn() as conn:
  639. cursor = conn.cursor()
  640. # 检查是否存在
  641. cursor.execute("""
  642. SELECT id FROM relationships
  643. WHERE from_entity = ? AND to_entity = ? AND type = ?
  644. """, (rel.from_entity, rel.to_entity, rel.type))
  645. existing = cursor.fetchone()
  646. if existing:
  647. cursor.execute("""
  648. UPDATE relationships SET
  649. description = ?,
  650. chapter = ?
  651. WHERE id = ?
  652. """, (rel.description, rel.chapter, existing["id"]))
  653. conn.commit()
  654. return False
  655. else:
  656. cursor.execute("""
  657. INSERT INTO relationships
  658. (from_entity, to_entity, type, description, chapter)
  659. VALUES (?, ?, ?, ?, ?)
  660. """, (
  661. rel.from_entity,
  662. rel.to_entity,
  663. rel.type,
  664. rel.description,
  665. rel.chapter
  666. ))
  667. conn.commit()
  668. return True
  669. def get_entity_relationships(self, entity_id: str, direction: str = "both") -> List[Dict]:
  670. """
  671. 获取实体的关系
  672. direction: "from" | "to" | "both"
  673. """
  674. with self._get_conn() as conn:
  675. cursor = conn.cursor()
  676. if direction == "from":
  677. cursor.execute("""
  678. SELECT * FROM relationships WHERE from_entity = ?
  679. ORDER BY chapter DESC
  680. """, (entity_id,))
  681. elif direction == "to":
  682. cursor.execute("""
  683. SELECT * FROM relationships WHERE to_entity = ?
  684. ORDER BY chapter DESC
  685. """, (entity_id,))
  686. else: # both
  687. cursor.execute("""
  688. SELECT * FROM relationships
  689. WHERE from_entity = ? OR to_entity = ?
  690. ORDER BY chapter DESC
  691. """, (entity_id, entity_id))
  692. return [dict(row) for row in cursor.fetchall()]
  693. def get_relationship_between(self, entity1: str, entity2: str) -> List[Dict]:
  694. """获取两个实体之间的所有关系"""
  695. with self._get_conn() as conn:
  696. cursor = conn.cursor()
  697. cursor.execute("""
  698. SELECT * FROM relationships
  699. WHERE (from_entity = ? AND to_entity = ?)
  700. OR (from_entity = ? AND to_entity = ?)
  701. ORDER BY chapter DESC
  702. """, (entity1, entity2, entity2, entity1))
  703. return [dict(row) for row in cursor.fetchall()]
  704. def get_recent_relationships(self, limit: int = 30) -> List[Dict]:
  705. """获取最近建立的关系"""
  706. with self._get_conn() as conn:
  707. cursor = conn.cursor()
  708. cursor.execute("""
  709. SELECT * FROM relationships
  710. ORDER BY chapter DESC, id DESC
  711. LIMIT ?
  712. """, (limit,))
  713. return [dict(row) for row in cursor.fetchall()]
  714. # ==================== 批量操作 ====================
  715. def process_chapter_data(
  716. self,
  717. chapter: int,
  718. title: str,
  719. location: str,
  720. word_count: int,
  721. entities: List[Dict],
  722. scenes: List[Dict]
  723. ) -> Dict[str, int]:
  724. """
  725. 处理章节数据,批量写入索引
  726. 返回写入统计
  727. """
  728. stats = {"chapters": 0, "scenes": 0, "appearances": 0}
  729. # 提取出场角色
  730. characters = [e.get("id") for e in entities if e.get("type") == "角色"]
  731. # 写入章节元数据
  732. self.add_chapter(ChapterMeta(
  733. chapter=chapter,
  734. title=title,
  735. location=location,
  736. word_count=word_count,
  737. characters=characters,
  738. summary="" # 可后续由 Data Agent 生成
  739. ))
  740. stats["chapters"] = 1
  741. # 写入场景
  742. scene_metas = []
  743. for s in scenes:
  744. scene_metas.append(SceneMeta(
  745. chapter=chapter,
  746. scene_index=s.get("index", 0),
  747. start_line=s.get("start_line", 0),
  748. end_line=s.get("end_line", 0),
  749. location=s.get("location", ""),
  750. summary=s.get("summary", ""),
  751. characters=s.get("characters", [])
  752. ))
  753. self.add_scenes(chapter, scene_metas)
  754. stats["scenes"] = len(scene_metas)
  755. # 写入出场记录
  756. for entity in entities:
  757. entity_id = entity.get("id")
  758. if entity_id and entity_id != "NEW":
  759. self.record_appearance(
  760. entity_id=entity_id,
  761. chapter=chapter,
  762. mentions=entity.get("mentions", []),
  763. confidence=entity.get("confidence", 1.0)
  764. )
  765. stats["appearances"] += 1
  766. return stats
  767. # ==================== 辅助方法 ====================
  768. def _row_to_dict(self, row: sqlite3.Row, parse_json: List[str] = None) -> Dict:
  769. """将 Row 转换为字典"""
  770. d = dict(row)
  771. if parse_json:
  772. for key in parse_json:
  773. if key in d and d[key]:
  774. try:
  775. d[key] = json.loads(d[key])
  776. except json.JSONDecodeError:
  777. pass
  778. return d
  779. def get_stats(self) -> Dict[str, int]:
  780. """获取索引统计"""
  781. with self._get_conn() as conn:
  782. cursor = conn.cursor()
  783. cursor.execute("SELECT COUNT(*) FROM chapters")
  784. chapters = cursor.fetchone()[0]
  785. cursor.execute("SELECT COUNT(*) FROM scenes")
  786. scenes = cursor.fetchone()[0]
  787. cursor.execute("SELECT COUNT(DISTINCT entity_id) FROM appearances")
  788. appearances = cursor.fetchone()[0]
  789. cursor.execute("SELECT MAX(chapter) FROM chapters")
  790. max_chapter = cursor.fetchone()[0] or 0
  791. # v5.1 新增统计
  792. cursor.execute("SELECT COUNT(*) FROM entities")
  793. entities = cursor.fetchone()[0]
  794. cursor.execute("SELECT COUNT(*) FROM entities WHERE is_archived = 0")
  795. active_entities = cursor.fetchone()[0]
  796. cursor.execute("SELECT COUNT(*) FROM aliases")
  797. aliases = cursor.fetchone()[0]
  798. cursor.execute("SELECT COUNT(*) FROM state_changes")
  799. state_changes = cursor.fetchone()[0]
  800. cursor.execute("SELECT COUNT(*) FROM relationships")
  801. relationships = cursor.fetchone()[0]
  802. return {
  803. "chapters": chapters,
  804. "scenes": scenes,
  805. "appearances": appearances,
  806. "max_chapter": max_chapter,
  807. # v5.1 新增
  808. "entities": entities,
  809. "active_entities": active_entities,
  810. "aliases": aliases,
  811. "state_changes": state_changes,
  812. "relationships": relationships
  813. }
  814. # ==================== CLI 接口 ====================
  815. def main():
  816. import argparse
  817. parser = argparse.ArgumentParser(description="Index Manager CLI (v5.1)")
  818. parser.add_argument("--project-root", type=str, help="项目根目录")
  819. subparsers = parser.add_subparsers(dest="command")
  820. # 获取统计
  821. subparsers.add_parser("stats")
  822. # 查询章节
  823. chapter_parser = subparsers.add_parser("get-chapter")
  824. chapter_parser.add_argument("--chapter", type=int, required=True)
  825. # 查询最近出场
  826. recent_parser = subparsers.add_parser("recent-appearances")
  827. recent_parser.add_argument("--limit", type=int, default=None)
  828. # 查询实体出场
  829. entity_parser = subparsers.add_parser("entity-appearances")
  830. entity_parser.add_argument("--entity", required=True)
  831. entity_parser.add_argument("--limit", type=int, default=None)
  832. # 搜索场景
  833. search_parser = subparsers.add_parser("search-scenes")
  834. search_parser.add_argument("--location", required=True)
  835. search_parser.add_argument("--limit", type=int, default=None)
  836. # 处理章节数据 (写入)
  837. process_parser = subparsers.add_parser("process-chapter")
  838. process_parser.add_argument("--chapter", type=int, required=True)
  839. process_parser.add_argument("--title", required=True)
  840. process_parser.add_argument("--location", required=True)
  841. process_parser.add_argument("--word-count", type=int, required=True)
  842. process_parser.add_argument("--entities", required=True, help="JSON 格式的实体列表")
  843. process_parser.add_argument("--scenes", required=True, help="JSON 格式的场景列表")
  844. # ==================== v5.1 新增命令 ====================
  845. # 获取实体
  846. get_entity_parser = subparsers.add_parser("get-entity")
  847. get_entity_parser.add_argument("--id", required=True, help="实体 ID")
  848. # 获取核心实体
  849. subparsers.add_parser("get-core-entities")
  850. # 获取主角
  851. subparsers.add_parser("get-protagonist")
  852. # 按类型获取实体
  853. type_parser = subparsers.add_parser("get-entities-by-type")
  854. type_parser.add_argument("--type", required=True, help="实体类型 (角色/地点/物品/势力/招式)")
  855. type_parser.add_argument("--include-archived", action="store_true")
  856. # 按别名查找实体
  857. alias_parser = subparsers.add_parser("get-by-alias")
  858. alias_parser.add_argument("--alias", required=True, help="别名")
  859. # 获取实体别名
  860. aliases_parser = subparsers.add_parser("get-aliases")
  861. aliases_parser.add_argument("--entity", required=True, help="实体 ID")
  862. # 注册别名
  863. reg_alias_parser = subparsers.add_parser("register-alias")
  864. reg_alias_parser.add_argument("--alias", required=True)
  865. reg_alias_parser.add_argument("--entity", required=True)
  866. reg_alias_parser.add_argument("--type", required=True, help="实体类型")
  867. # 获取实体关系
  868. rel_parser = subparsers.add_parser("get-relationships")
  869. rel_parser.add_argument("--entity", required=True)
  870. rel_parser.add_argument("--direction", choices=["from", "to", "both"], default="both")
  871. # 获取状态变化
  872. changes_parser = subparsers.add_parser("get-state-changes")
  873. changes_parser.add_argument("--entity", required=True)
  874. changes_parser.add_argument("--limit", type=int, default=20)
  875. # 写入实体
  876. upsert_entity_parser = subparsers.add_parser("upsert-entity")
  877. upsert_entity_parser.add_argument("--data", required=True, help="JSON 格式的实体数据")
  878. # 写入关系
  879. upsert_rel_parser = subparsers.add_parser("upsert-relationship")
  880. upsert_rel_parser.add_argument("--data", required=True, help="JSON 格式的关系数据")
  881. # 写入状态变化
  882. state_change_parser = subparsers.add_parser("record-state-change")
  883. state_change_parser.add_argument("--data", required=True, help="JSON 格式的状态变化数据")
  884. args = parser.parse_args()
  885. # 初始化
  886. config = None
  887. if args.project_root:
  888. from .config import DataModulesConfig
  889. config = DataModulesConfig.from_project_root(args.project_root)
  890. manager = IndexManager(config)
  891. if args.command == "stats":
  892. stats = manager.get_stats()
  893. print(json.dumps(stats, ensure_ascii=False, indent=2))
  894. elif args.command == "get-chapter":
  895. chapter = manager.get_chapter(args.chapter)
  896. if chapter:
  897. print(json.dumps(chapter, ensure_ascii=False, indent=2))
  898. else:
  899. print(f"未找到章节: {args.chapter}")
  900. elif args.command == "recent-appearances":
  901. appearances = manager.get_recent_appearances(args.limit)
  902. for a in appearances:
  903. print(f"{a['entity_id']}: 最后出场第 {a['last_chapter']} 章, 共 {a['total']} 次")
  904. elif args.command == "entity-appearances":
  905. appearances = manager.get_entity_appearances(args.entity, args.limit)
  906. print(f"{args.entity} 出场记录:")
  907. for a in appearances:
  908. print(f" 第 {a['chapter']} 章: {a['mentions']}")
  909. elif args.command == "search-scenes":
  910. scenes = manager.search_scenes_by_location(args.location, args.limit)
  911. for s in scenes:
  912. print(f"第 {s['chapter']} 章 场景 {s['scene_index']}: {s['location']}")
  913. print(f" {s['summary'][:50]}...")
  914. elif args.command == "process-chapter":
  915. entities = json.loads(args.entities)
  916. scenes = json.loads(args.scenes)
  917. stats = manager.process_chapter_data(
  918. chapter=args.chapter,
  919. title=args.title,
  920. location=args.location,
  921. word_count=args.word_count,
  922. entities=entities,
  923. scenes=scenes
  924. )
  925. print(f"✓ 已处理第 {args.chapter} 章")
  926. print(f" 章节: {stats['chapters']}, 场景: {stats['scenes']}, 出场记录: {stats['appearances']}")
  927. # ==================== v5.1 新增命令处理 ====================
  928. elif args.command == "get-entity":
  929. entity = manager.get_entity(args.id)
  930. if entity:
  931. print(json.dumps(entity, ensure_ascii=False, indent=2))
  932. else:
  933. print(f"未找到实体: {args.id}")
  934. elif args.command == "get-core-entities":
  935. entities = manager.get_core_entities()
  936. print(json.dumps(entities, ensure_ascii=False, indent=2))
  937. elif args.command == "get-protagonist":
  938. protagonist = manager.get_protagonist()
  939. if protagonist:
  940. print(json.dumps(protagonist, ensure_ascii=False, indent=2))
  941. else:
  942. print("未设置主角")
  943. elif args.command == "get-entities-by-type":
  944. entities = manager.get_entities_by_type(args.type, args.include_archived)
  945. print(json.dumps(entities, ensure_ascii=False, indent=2))
  946. elif args.command == "get-by-alias":
  947. entities = manager.get_entities_by_alias(args.alias)
  948. if entities:
  949. print(json.dumps(entities, ensure_ascii=False, indent=2))
  950. else:
  951. print(f"未找到别名: {args.alias}")
  952. elif args.command == "get-aliases":
  953. aliases = manager.get_entity_aliases(args.entity)
  954. if aliases:
  955. print(f"{args.entity} 的别名: {', '.join(aliases)}")
  956. else:
  957. print(f"{args.entity} 没有别名")
  958. elif args.command == "register-alias":
  959. success = manager.register_alias(args.alias, args.entity, args.type)
  960. if success:
  961. print(f"✓ 已注册别名: {args.alias} → {args.entity} ({args.type})")
  962. else:
  963. print(f"别名已存在或注册失败: {args.alias}")
  964. elif args.command == "get-relationships":
  965. rels = manager.get_entity_relationships(args.entity, args.direction)
  966. print(json.dumps(rels, ensure_ascii=False, indent=2))
  967. elif args.command == "get-state-changes":
  968. changes = manager.get_entity_state_changes(args.entity, args.limit)
  969. print(json.dumps(changes, ensure_ascii=False, indent=2))
  970. elif args.command == "upsert-entity":
  971. data = json.loads(args.data)
  972. entity = EntityMeta(
  973. id=data["id"],
  974. type=data["type"],
  975. canonical_name=data["canonical_name"],
  976. tier=data.get("tier", "装饰"),
  977. desc=data.get("desc", ""),
  978. current=data.get("current", {}),
  979. first_appearance=data.get("first_appearance", 0),
  980. last_appearance=data.get("last_appearance", 0),
  981. is_protagonist=data.get("is_protagonist", False),
  982. is_archived=data.get("is_archived", False)
  983. )
  984. is_new = manager.upsert_entity(entity)
  985. print(f"✓ {'新建' if is_new else '更新'}实体: {entity.id}")
  986. elif args.command == "upsert-relationship":
  987. data = json.loads(args.data)
  988. rel = RelationshipMeta(
  989. from_entity=data["from_entity"],
  990. to_entity=data["to_entity"],
  991. type=data["type"],
  992. description=data.get("description", ""),
  993. chapter=data["chapter"]
  994. )
  995. is_new = manager.upsert_relationship(rel)
  996. print(f"✓ {'新建' if is_new else '更新'}关系: {rel.from_entity} → {rel.to_entity} ({rel.type})")
  997. elif args.command == "record-state-change":
  998. data = json.loads(args.data)
  999. change = StateChangeMeta(
  1000. entity_id=data["entity_id"],
  1001. field=data["field"],
  1002. old_value=data.get("old_value", ""),
  1003. new_value=data["new_value"],
  1004. reason=data.get("reason", ""),
  1005. chapter=data["chapter"]
  1006. )
  1007. record_id = manager.record_state_change(change)
  1008. print(f"✓ 已记录状态变化 #{record_id}: {change.entity_id}.{change.field}")
  1009. if __name__ == "__main__":
  1010. main()