index_entity_mixin.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """
  4. IndexEntityMixin extracted from IndexManager.
  5. """
  6. from __future__ import annotations
  7. import json
  8. import logging
  9. from datetime import datetime
  10. from typing import Any, Dict, List, Optional
  11. logger = logging.getLogger(__name__)
  12. class IndexEntityMixin:
  13. def upsert_entity(self, entity: EntityMeta, update_metadata: bool = False) -> bool:
  14. """
  15. 插入或更新实体 (智能合并)
  16. - 新实体: 直接插入
  17. - 已存在: 更新 current_json, last_appearance, updated_at
  18. - update_metadata=True: 同时更新 canonical_name/tier/desc/is_protagonist/is_archived
  19. 返回是否为新实体
  20. """
  21. with self._get_conn() as conn:
  22. cursor = conn.cursor()
  23. # 检查是否存在
  24. cursor.execute(
  25. "SELECT id, current_json FROM entities WHERE id = ?", (entity.id,)
  26. )
  27. existing = cursor.fetchone()
  28. if existing:
  29. # 已存在: 智能合并 current_json
  30. old_current = {}
  31. if existing["current_json"]:
  32. try:
  33. old_current = json.loads(existing["current_json"])
  34. except json.JSONDecodeError as exc:
  35. logger.warning(
  36. "failed to parse JSON in entities.current_json: %s",
  37. exc,
  38. )
  39. # 合并 current (新值覆盖旧值)
  40. merged_current = {**old_current, **entity.current}
  41. if update_metadata:
  42. # 完整更新(包括元数据)
  43. cursor.execute(
  44. """
  45. UPDATE entities SET
  46. canonical_name = ?,
  47. tier = ?,
  48. desc = ?,
  49. current_json = ?,
  50. last_appearance = ?,
  51. is_protagonist = ?,
  52. is_archived = ?,
  53. updated_at = CURRENT_TIMESTAMP
  54. WHERE id = ?
  55. """,
  56. (
  57. entity.canonical_name,
  58. entity.tier,
  59. entity.desc,
  60. json.dumps(merged_current, ensure_ascii=False),
  61. entity.last_appearance,
  62. 1 if entity.is_protagonist else 0,
  63. 1 if entity.is_archived else 0,
  64. entity.id,
  65. ),
  66. )
  67. else:
  68. # 只更新 current 和 last_appearance
  69. cursor.execute(
  70. """
  71. UPDATE entities SET
  72. current_json = ?,
  73. last_appearance = ?,
  74. updated_at = CURRENT_TIMESTAMP
  75. WHERE id = ?
  76. """,
  77. (
  78. json.dumps(merged_current, ensure_ascii=False),
  79. entity.last_appearance,
  80. entity.id,
  81. ),
  82. )
  83. conn.commit()
  84. return False
  85. else:
  86. # 新实体: 插入
  87. cursor.execute(
  88. """
  89. INSERT INTO entities
  90. (id, type, canonical_name, tier, desc, current_json,
  91. first_appearance, last_appearance, is_protagonist, is_archived)
  92. VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
  93. """,
  94. (
  95. entity.id,
  96. entity.type,
  97. entity.canonical_name,
  98. entity.tier,
  99. entity.desc,
  100. json.dumps(entity.current, ensure_ascii=False),
  101. entity.first_appearance,
  102. entity.last_appearance,
  103. 1 if entity.is_protagonist else 0,
  104. 1 if entity.is_archived else 0,
  105. ),
  106. )
  107. conn.commit()
  108. return True
  109. def get_entity(self, entity_id: str) -> Optional[Dict]:
  110. """获取单个实体"""
  111. with self._get_conn() as conn:
  112. cursor = conn.cursor()
  113. cursor.execute("SELECT * FROM entities WHERE id = ?", (entity_id,))
  114. row = cursor.fetchone()
  115. if row:
  116. return self._row_to_dict(row, parse_json=["current_json"])
  117. return None
  118. def get_entities_by_type(
  119. self, entity_type: str, include_archived: bool = False
  120. ) -> List[Dict]:
  121. """按类型获取实体"""
  122. with self._get_conn() as conn:
  123. cursor = conn.cursor()
  124. if include_archived:
  125. cursor.execute(
  126. """
  127. SELECT * FROM entities WHERE type = ?
  128. ORDER BY last_appearance DESC
  129. """,
  130. (entity_type,),
  131. )
  132. else:
  133. cursor.execute(
  134. """
  135. SELECT * FROM entities WHERE type = ? AND is_archived = 0
  136. ORDER BY last_appearance DESC
  137. """,
  138. (entity_type,),
  139. )
  140. return [
  141. self._row_to_dict(row, parse_json=["current_json"])
  142. for row in cursor.fetchall()
  143. ]
  144. def get_entities_by_tier(self, tier: str) -> List[Dict]:
  145. """按重要度获取实体 (核心/重要/次要/装饰)"""
  146. with self._get_conn() as conn:
  147. cursor = conn.cursor()
  148. cursor.execute(
  149. """
  150. SELECT * FROM entities WHERE tier = ? AND is_archived = 0
  151. ORDER BY last_appearance DESC
  152. """,
  153. (tier,),
  154. )
  155. return [
  156. self._row_to_dict(row, parse_json=["current_json"])
  157. for row in cursor.fetchall()
  158. ]
  159. def get_core_entities(self) -> List[Dict]:
  160. """获取所有核心实体 (用于 Context Agent 全量加载)"""
  161. with self._get_conn() as conn:
  162. cursor = conn.cursor()
  163. cursor.execute("""
  164. SELECT * FROM entities
  165. WHERE (tier IN ('核心', '重要') OR is_protagonist = 1) AND is_archived = 0
  166. ORDER BY is_protagonist DESC, tier, last_appearance DESC
  167. """)
  168. return [
  169. self._row_to_dict(row, parse_json=["current_json"])
  170. for row in cursor.fetchall()
  171. ]
  172. def get_protagonist(self) -> Optional[Dict]:
  173. """获取主角实体"""
  174. with self._get_conn() as conn:
  175. cursor = conn.cursor()
  176. cursor.execute("SELECT * FROM entities WHERE is_protagonist = 1 LIMIT 1")
  177. row = cursor.fetchone()
  178. if row:
  179. return self._row_to_dict(row, parse_json=["current_json"])
  180. return None
  181. def update_entity_current(self, entity_id: str, updates: Dict) -> bool:
  182. """
  183. 增量更新实体的 current 字段 (不覆盖其他字段)
  184. 例如: update_entity_current("xiaoyan", {"realm": "斗师"})
  185. """
  186. with self._get_conn() as conn:
  187. cursor = conn.cursor()
  188. cursor.execute(
  189. "SELECT current_json FROM entities WHERE id = ?", (entity_id,)
  190. )
  191. row = cursor.fetchone()
  192. if not row:
  193. return False
  194. current = {}
  195. if row["current_json"]:
  196. try:
  197. current = json.loads(row["current_json"])
  198. except json.JSONDecodeError as exc:
  199. logger.warning(
  200. "failed to parse JSON in update_entity_current current_json: %s",
  201. exc,
  202. )
  203. current.update(updates)
  204. cursor.execute(
  205. """
  206. UPDATE entities SET
  207. current_json = ?,
  208. updated_at = CURRENT_TIMESTAMP
  209. WHERE id = ?
  210. """,
  211. (json.dumps(current, ensure_ascii=False), entity_id),
  212. )
  213. conn.commit()
  214. return True
  215. def archive_entity(self, entity_id: str) -> bool:
  216. """归档实体 (不删除,只是标记)"""
  217. with self._get_conn() as conn:
  218. cursor = conn.cursor()
  219. cursor.execute(
  220. """
  221. UPDATE entities SET is_archived = 1, updated_at = CURRENT_TIMESTAMP
  222. WHERE id = ?
  223. """,
  224. (entity_id,),
  225. )
  226. conn.commit()
  227. return cursor.rowcount > 0
  228. # ==================== v5.1 别名操作 ====================
  229. def register_alias(self, alias: str, entity_id: str, entity_type: str) -> bool:
  230. """
  231. 注册别名 (支持一对多)
  232. 同一别名可映射多个实体 (如 "天云宗" → 地点 + 势力)
  233. """
  234. with self._get_conn() as conn:
  235. cursor = conn.cursor()
  236. try:
  237. cursor.execute(
  238. """
  239. INSERT OR IGNORE INTO aliases (alias, entity_id, entity_type)
  240. VALUES (?, ?, ?)
  241. """,
  242. (alias, entity_id, entity_type),
  243. )
  244. conn.commit()
  245. return cursor.rowcount > 0
  246. except sqlite3.IntegrityError:
  247. return False
  248. def get_entities_by_alias(self, alias: str) -> List[Dict]:
  249. """
  250. 根据别名查找实体 (一对多)
  251. 返回所有匹配的实体 (可能有多个不同类型)
  252. """
  253. with self._get_conn() as conn:
  254. cursor = conn.cursor()
  255. cursor.execute(
  256. """
  257. SELECT e.*, a.entity_type as alias_type
  258. FROM entities e
  259. JOIN aliases a ON e.id = a.entity_id
  260. WHERE a.alias = ?
  261. """,
  262. (alias,),
  263. )
  264. return [
  265. self._row_to_dict(row, parse_json=["current_json"])
  266. for row in cursor.fetchall()
  267. ]
  268. def get_entity_aliases(self, entity_id: str) -> List[str]:
  269. """获取实体的所有别名"""
  270. with self._get_conn() as conn:
  271. cursor = conn.cursor()
  272. cursor.execute(
  273. "SELECT alias FROM aliases WHERE entity_id = ?", (entity_id,)
  274. )
  275. return [row["alias"] for row in cursor.fetchall()]
  276. def remove_alias(self, alias: str, entity_id: str) -> bool:
  277. """移除别名"""
  278. with self._get_conn() as conn:
  279. cursor = conn.cursor()
  280. cursor.execute(
  281. "DELETE FROM aliases WHERE alias = ? AND entity_id = ?",
  282. (alias, entity_id),
  283. )
  284. conn.commit()
  285. return cursor.rowcount > 0
  286. # ==================== v5.1 状态变化操作 ====================
  287. def record_state_change(self, change: StateChangeMeta) -> int:
  288. """
  289. 记录状态变化
  290. 返回记录 ID
  291. """
  292. with self._get_conn() as conn:
  293. cursor = conn.cursor()
  294. cursor.execute(
  295. """
  296. INSERT INTO state_changes
  297. (entity_id, field, old_value, new_value, reason, chapter)
  298. VALUES (?, ?, ?, ?, ?, ?)
  299. """,
  300. (
  301. change.entity_id,
  302. change.field,
  303. change.old_value,
  304. change.new_value,
  305. change.reason,
  306. change.chapter,
  307. ),
  308. )
  309. conn.commit()
  310. return cursor.lastrowid
  311. def get_entity_state_changes(self, entity_id: str, limit: int = 20) -> List[Dict]:
  312. """获取实体的状态变化历史"""
  313. with self._get_conn() as conn:
  314. cursor = conn.cursor()
  315. cursor.execute(
  316. """
  317. SELECT * FROM state_changes
  318. WHERE entity_id = ?
  319. ORDER BY chapter DESC, id DESC
  320. LIMIT ?
  321. """,
  322. (entity_id, limit),
  323. )
  324. return [dict(row) for row in cursor.fetchall()]
  325. def get_recent_state_changes(self, limit: int = 50) -> List[Dict]:
  326. """获取最近的状态变化"""
  327. with self._get_conn() as conn:
  328. cursor = conn.cursor()
  329. cursor.execute(
  330. """
  331. SELECT * FROM state_changes
  332. ORDER BY chapter DESC, id DESC
  333. LIMIT ?
  334. """,
  335. (limit,),
  336. )
  337. return [dict(row) for row in cursor.fetchall()]
  338. def get_chapter_state_changes(self, chapter: int) -> List[Dict]:
  339. """获取某章的所有状态变化"""
  340. with self._get_conn() as conn:
  341. cursor = conn.cursor()
  342. cursor.execute(
  343. """
  344. SELECT * FROM state_changes
  345. WHERE chapter = ?
  346. ORDER BY id
  347. """,
  348. (chapter,),
  349. )
  350. return [dict(row) for row in cursor.fetchall()]
  351. # ==================== v5.1 关系操作 ====================
  352. def upsert_relationship(self, rel: RelationshipMeta) -> bool:
  353. """
  354. 插入或更新关系
  355. 相同 (from, to, type) 会更新 description 和 chapter
  356. 返回是否为新关系
  357. """
  358. with self._get_conn() as conn:
  359. cursor = conn.cursor()
  360. # 检查是否存在
  361. cursor.execute(
  362. """
  363. SELECT id FROM relationships
  364. WHERE from_entity = ? AND to_entity = ? AND type = ?
  365. """,
  366. (rel.from_entity, rel.to_entity, rel.type),
  367. )
  368. existing = cursor.fetchone()
  369. if existing:
  370. cursor.execute(
  371. """
  372. UPDATE relationships SET
  373. description = ?,
  374. chapter = ?
  375. WHERE id = ?
  376. """,
  377. (rel.description, rel.chapter, existing["id"]),
  378. )
  379. conn.commit()
  380. return False
  381. else:
  382. cursor.execute(
  383. """
  384. INSERT INTO relationships
  385. (from_entity, to_entity, type, description, chapter)
  386. VALUES (?, ?, ?, ?, ?)
  387. """,
  388. (
  389. rel.from_entity,
  390. rel.to_entity,
  391. rel.type,
  392. rel.description,
  393. rel.chapter,
  394. ),
  395. )
  396. conn.commit()
  397. return True
  398. def get_entity_relationships(
  399. self, entity_id: str, direction: str = "both"
  400. ) -> List[Dict]:
  401. """
  402. 获取实体的关系
  403. direction: "from" | "to" | "both"
  404. """
  405. with self._get_conn() as conn:
  406. cursor = conn.cursor()
  407. if direction == "from":
  408. cursor.execute(
  409. """
  410. SELECT * FROM relationships WHERE from_entity = ?
  411. ORDER BY chapter DESC
  412. """,
  413. (entity_id,),
  414. )
  415. elif direction == "to":
  416. cursor.execute(
  417. """
  418. SELECT * FROM relationships WHERE to_entity = ?
  419. ORDER BY chapter DESC
  420. """,
  421. (entity_id,),
  422. )
  423. else: # both
  424. cursor.execute(
  425. """
  426. SELECT * FROM relationships
  427. WHERE from_entity = ? OR to_entity = ?
  428. ORDER BY chapter DESC
  429. """,
  430. (entity_id, entity_id),
  431. )
  432. return [dict(row) for row in cursor.fetchall()]
  433. def get_relationship_between(self, entity1: str, entity2: str) -> List[Dict]:
  434. """获取两个实体之间的所有关系"""
  435. with self._get_conn() as conn:
  436. cursor = conn.cursor()
  437. cursor.execute(
  438. """
  439. SELECT * FROM relationships
  440. WHERE (from_entity = ? AND to_entity = ?)
  441. OR (from_entity = ? AND to_entity = ?)
  442. ORDER BY chapter DESC
  443. """,
  444. (entity1, entity2, entity2, entity1),
  445. )
  446. return [dict(row) for row in cursor.fetchall()]
  447. def get_recent_relationships(self, limit: int = 30) -> List[Dict]:
  448. """获取最近建立的关系"""
  449. with self._get_conn() as conn:
  450. cursor = conn.cursor()
  451. cursor.execute(
  452. """
  453. SELECT * FROM relationships
  454. ORDER BY chapter DESC, id DESC
  455. LIMIT ?
  456. """,
  457. (limit,),
  458. )
  459. return [dict(row) for row in cursor.fetchall()]
  460. # ==================== v5.3 Override Contract 操作 ====================
  461. def update_entity_field(self, entity_id: str, field: str, value: Any) -> bool:
  462. """Compatibility helper to update a single entity field in current_json."""
  463. return self.update_entity_current(entity_id, {field: value})