index_manager.py 44 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """
  4. Index Manager - 索引管理模块 (v5.4)
  5. 管理 index.db (SQLite) 的读写操作:
  6. - 章节元数据索引
  7. - 实体出场记录
  8. - 场景索引
  9. - 实体存储 (从 state.json 迁移)
  10. - 别名索引 (一对多)
  11. - 状态变化记录
  12. - 关系存储
  13. - 快速查询接口
  14. - 追读力债务管理 (v5.3 引入,v5.4 沿用)
  15. v5.4 变更:
  16. - 新增 invalid_facts 表:追踪无效事实 (pending/confirmed)
  17. - 新增 tool_call_stats 表:记录工具调用成功率与错误信息
  18. - 新增 review_metrics 表:记录审查指标与趋势数据
  19. v5.3 变更:
  20. - 新增 override_contracts 表:记录违背软建议时的Override Contract
  21. - 新增 chase_debt 表:追读力债务追踪
  22. - 新增 debt_events 表:债务事件日志(产生/偿还/利息)
  23. - 新增 chapter_reading_power 表:章节追读力元数据
  24. v5.1 变更:
  25. - 新增 entities 表替代 state.json 中的 entities_v3
  26. - 新增 aliases 表替代 state.json 中的 alias_index (支持一对多)
  27. - 新增 state_changes 表替代 state.json 中的 state_changes
  28. - 新增 relationships 表替代 state.json 中的 structured_relationships
  29. """
  30. import sqlite3
  31. import json
  32. from pathlib import Path
  33. from runtime_compat import enable_windows_utf8_stdio
  34. from typing import Dict, List, Optional, Any, Tuple
  35. from dataclasses import dataclass, field
  36. from contextlib import contextmanager
  37. from datetime import datetime
  38. from .config import get_config
  39. from .index_chapter_mixin import IndexChapterMixin
  40. from .index_entity_mixin import IndexEntityMixin
  41. from .index_debt_mixin import IndexDebtMixin
  42. from .index_reading_mixin import IndexReadingMixin
  43. from .index_observability_mixin import IndexObservabilityMixin
  44. from .observability import safe_log_tool_call
  45. @dataclass
  46. class ChapterMeta:
  47. """章节元数据"""
  48. chapter: int
  49. title: str
  50. location: str
  51. word_count: int
  52. characters: List[str]
  53. summary: str = ""
  54. @dataclass
  55. class SceneMeta:
  56. """场景元数据"""
  57. chapter: int
  58. scene_index: int
  59. start_line: int
  60. end_line: int
  61. location: str
  62. summary: str
  63. characters: List[str]
  64. @dataclass
  65. class EntityMeta:
  66. """实体元数据 (v5.1 引入)"""
  67. id: str
  68. type: str # 角色/地点/物品/势力/招式
  69. canonical_name: str
  70. tier: str = "装饰" # 核心/重要/次要/装饰
  71. desc: str = ""
  72. current: Dict = field(default_factory=dict) # 当前状态 (realm/location/items等)
  73. first_appearance: int = 0
  74. last_appearance: int = 0
  75. is_protagonist: bool = False
  76. is_archived: bool = False
  77. @dataclass
  78. class StateChangeMeta:
  79. """状态变化记录 (v5.1 引入)"""
  80. entity_id: str
  81. field: str
  82. old_value: str
  83. new_value: str
  84. reason: str
  85. chapter: int
  86. @dataclass
  87. class RelationshipMeta:
  88. """关系记录 (v5.1 引入)"""
  89. from_entity: str
  90. to_entity: str
  91. type: str
  92. description: str
  93. chapter: int
  94. @dataclass
  95. class OverrideContractMeta:
  96. """Override Contract (v5.3 引入)"""
  97. chapter: int
  98. constraint_type: str # SOFT_HOOK_STRENGTH / SOFT_MICROPAYOFF / etc.
  99. constraint_id: str # 具体约束标识
  100. rationale_type: str # TRANSITIONAL_SETUP / LOGIC_INTEGRITY / etc.
  101. rationale_text: str # 具体理由说明
  102. payback_plan: str # 偿还计划描述
  103. due_chapter: int # 偿还截止章节
  104. status: str = "pending" # pending / fulfilled / overdue / cancelled
  105. @dataclass
  106. class ChaseDebtMeta:
  107. """追读力债务 (v5.3 引入)"""
  108. id: int = 0
  109. debt_type: str = "" # hook_strength / micropayoff / coolpoint / etc.
  110. original_amount: float = 1.0 # 初始债务量
  111. current_amount: float = 1.0 # 当前债务量(含利息)
  112. interest_rate: float = 0.1 # 利息率(每章)
  113. source_chapter: int = 0 # 产生债务的章节
  114. due_chapter: int = 0 # 截止章节
  115. override_contract_id: int = 0 # 关联的Override Contract
  116. status: str = "active" # active / paid / overdue / written_off
  117. @dataclass
  118. class DebtEventMeta:
  119. """债务事件日志 (v5.3 引入)"""
  120. debt_id: int
  121. event_type: (
  122. str # created / interest_accrued / partial_payment / full_payment / overdue
  123. )
  124. amount: float
  125. chapter: int
  126. note: str = ""
  127. @dataclass
  128. class ChapterReadingPowerMeta:
  129. """章节追读力元数据 (v5.3 引入)"""
  130. chapter: int
  131. hook_type: str = "" # 章末钩子类型
  132. hook_strength: str = "medium" # strong / medium / weak
  133. coolpoint_patterns: List[str] = field(default_factory=list) # 使用的爽点模式
  134. micropayoffs: List[str] = field(default_factory=list) # 微兑现列表
  135. hard_violations: List[str] = field(default_factory=list) # 硬约束违规
  136. soft_suggestions: List[str] = field(default_factory=list) # 软建议
  137. is_transition: bool = False # 是否为过渡章
  138. override_count: int = 0 # Override Contract数量
  139. debt_balance: float = 0.0 # 当前债务余额
  140. @dataclass
  141. class ReviewMetrics:
  142. """审查指标记录 (v5.4 引入)"""
  143. start_chapter: int
  144. end_chapter: int
  145. overall_score: float = 0.0
  146. dimension_scores: Dict[str, float] = field(default_factory=dict)
  147. severity_counts: Dict[str, int] = field(default_factory=dict)
  148. critical_issues: List[str] = field(default_factory=list)
  149. report_file: str = ""
  150. notes: str = ""
  151. @dataclass
  152. class WritingChecklistScoreMeta:
  153. """写作清单评分记录(Context Contract v2 Phase F)"""
  154. chapter: int
  155. template: str = "plot"
  156. total_items: int = 0
  157. required_items: int = 0
  158. completed_items: int = 0
  159. completed_required: int = 0
  160. total_weight: float = 0.0
  161. completed_weight: float = 0.0
  162. completion_rate: float = 0.0
  163. score: float = 0.0
  164. score_breakdown: Dict[str, Any] = field(default_factory=dict)
  165. pending_items: List[str] = field(default_factory=list)
  166. source: str = "context_manager"
  167. notes: str = ""
  168. class IndexManager(IndexChapterMixin, IndexEntityMixin, IndexDebtMixin, IndexReadingMixin, IndexObservabilityMixin):
  169. """索引管理器"""
  170. def __init__(self, config=None):
  171. self.config = config or get_config()
  172. self._init_db()
  173. def _init_db(self):
  174. """初始化数据库表"""
  175. self.config.ensure_dirs()
  176. with self._get_conn() as conn:
  177. cursor = conn.cursor()
  178. # 章节表
  179. cursor.execute("""
  180. CREATE TABLE IF NOT EXISTS chapters (
  181. chapter INTEGER PRIMARY KEY,
  182. title TEXT,
  183. location TEXT,
  184. word_count INTEGER,
  185. characters TEXT,
  186. summary TEXT,
  187. created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
  188. )
  189. """)
  190. # 场景表
  191. cursor.execute("""
  192. CREATE TABLE IF NOT EXISTS scenes (
  193. id INTEGER PRIMARY KEY AUTOINCREMENT,
  194. chapter INTEGER,
  195. scene_index INTEGER,
  196. start_line INTEGER,
  197. end_line INTEGER,
  198. location TEXT,
  199. summary TEXT,
  200. characters TEXT,
  201. UNIQUE(chapter, scene_index)
  202. )
  203. """)
  204. # 实体出场表
  205. cursor.execute("""
  206. CREATE TABLE IF NOT EXISTS appearances (
  207. id INTEGER PRIMARY KEY AUTOINCREMENT,
  208. entity_id TEXT,
  209. chapter INTEGER,
  210. mentions TEXT,
  211. confidence REAL,
  212. UNIQUE(entity_id, chapter)
  213. )
  214. """)
  215. # 创建索引
  216. cursor.execute(
  217. "CREATE INDEX IF NOT EXISTS idx_scenes_chapter ON scenes(chapter)"
  218. )
  219. cursor.execute(
  220. "CREATE INDEX IF NOT EXISTS idx_appearances_entity ON appearances(entity_id)"
  221. )
  222. cursor.execute(
  223. "CREATE INDEX IF NOT EXISTS idx_appearances_chapter ON appearances(chapter)"
  224. )
  225. # ==================== v5.1 引入表 ====================
  226. # 实体表 (替代 state.json 中的 entities_v3)
  227. cursor.execute("""
  228. CREATE TABLE IF NOT EXISTS entities (
  229. id TEXT PRIMARY KEY,
  230. type TEXT NOT NULL,
  231. canonical_name TEXT NOT NULL,
  232. tier TEXT DEFAULT '装饰',
  233. desc TEXT,
  234. current_json TEXT,
  235. first_appearance INTEGER DEFAULT 0,
  236. last_appearance INTEGER DEFAULT 0,
  237. is_protagonist INTEGER DEFAULT 0,
  238. is_archived INTEGER DEFAULT 0,
  239. created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  240. updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
  241. )
  242. """)
  243. # 别名表 (替代 state.json 中的 alias_index,支持一对多)
  244. cursor.execute("""
  245. CREATE TABLE IF NOT EXISTS aliases (
  246. alias TEXT NOT NULL,
  247. entity_id TEXT NOT NULL,
  248. entity_type TEXT NOT NULL,
  249. created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  250. PRIMARY KEY (alias, entity_id, entity_type)
  251. )
  252. """)
  253. # 状态变化表 (替代 state.json 中的 state_changes)
  254. cursor.execute("""
  255. CREATE TABLE IF NOT EXISTS state_changes (
  256. id INTEGER PRIMARY KEY AUTOINCREMENT,
  257. entity_id TEXT NOT NULL,
  258. field TEXT NOT NULL,
  259. old_value TEXT,
  260. new_value TEXT,
  261. reason TEXT,
  262. chapter INTEGER NOT NULL,
  263. created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
  264. )
  265. """)
  266. # 关系表 (替代 state.json 中的 structured_relationships)
  267. cursor.execute("""
  268. CREATE TABLE IF NOT EXISTS relationships (
  269. id INTEGER PRIMARY KEY AUTOINCREMENT,
  270. from_entity TEXT NOT NULL,
  271. to_entity TEXT NOT NULL,
  272. type TEXT NOT NULL,
  273. description TEXT,
  274. chapter INTEGER NOT NULL,
  275. created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  276. UNIQUE(from_entity, to_entity, type)
  277. )
  278. """)
  279. # v5.1 引入索引
  280. cursor.execute(
  281. "CREATE INDEX IF NOT EXISTS idx_entities_type ON entities(type)"
  282. )
  283. cursor.execute(
  284. "CREATE INDEX IF NOT EXISTS idx_entities_tier ON entities(tier)"
  285. )
  286. cursor.execute(
  287. "CREATE INDEX IF NOT EXISTS idx_entities_protagonist ON entities(is_protagonist)"
  288. )
  289. cursor.execute(
  290. "CREATE INDEX IF NOT EXISTS idx_aliases_entity ON aliases(entity_id)"
  291. )
  292. cursor.execute(
  293. "CREATE INDEX IF NOT EXISTS idx_aliases_alias ON aliases(alias)"
  294. )
  295. cursor.execute(
  296. "CREATE INDEX IF NOT EXISTS idx_state_changes_entity ON state_changes(entity_id)"
  297. )
  298. cursor.execute(
  299. "CREATE INDEX IF NOT EXISTS idx_state_changes_chapter ON state_changes(chapter)"
  300. )
  301. cursor.execute(
  302. "CREATE INDEX IF NOT EXISTS idx_relationships_from ON relationships(from_entity)"
  303. )
  304. cursor.execute(
  305. "CREATE INDEX IF NOT EXISTS idx_relationships_to ON relationships(to_entity)"
  306. )
  307. cursor.execute(
  308. "CREATE INDEX IF NOT EXISTS idx_relationships_chapter ON relationships(chapter)"
  309. )
  310. # ==================== v5.3 引入表:追读力债务管理 ====================
  311. # Override Contract 表
  312. cursor.execute("""
  313. CREATE TABLE IF NOT EXISTS override_contracts (
  314. id INTEGER PRIMARY KEY AUTOINCREMENT,
  315. chapter INTEGER NOT NULL,
  316. constraint_type TEXT NOT NULL,
  317. constraint_id TEXT NOT NULL,
  318. rationale_type TEXT NOT NULL,
  319. rationale_text TEXT,
  320. payback_plan TEXT,
  321. due_chapter INTEGER NOT NULL,
  322. status TEXT DEFAULT 'pending',
  323. created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  324. fulfilled_at TIMESTAMP,
  325. UNIQUE(chapter, constraint_type, constraint_id)
  326. )
  327. """)
  328. # 追读力债务表
  329. cursor.execute("""
  330. CREATE TABLE IF NOT EXISTS chase_debt (
  331. id INTEGER PRIMARY KEY AUTOINCREMENT,
  332. debt_type TEXT NOT NULL,
  333. original_amount REAL DEFAULT 1.0,
  334. current_amount REAL DEFAULT 1.0,
  335. interest_rate REAL DEFAULT 0.1,
  336. source_chapter INTEGER NOT NULL,
  337. due_chapter INTEGER NOT NULL,
  338. override_contract_id INTEGER,
  339. status TEXT DEFAULT 'active',
  340. created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  341. updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  342. FOREIGN KEY (override_contract_id) REFERENCES override_contracts(id)
  343. )
  344. """)
  345. # 债务事件日志表
  346. cursor.execute("""
  347. CREATE TABLE IF NOT EXISTS debt_events (
  348. id INTEGER PRIMARY KEY AUTOINCREMENT,
  349. debt_id INTEGER NOT NULL,
  350. event_type TEXT NOT NULL,
  351. amount REAL NOT NULL,
  352. chapter INTEGER NOT NULL,
  353. note TEXT,
  354. created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  355. FOREIGN KEY (debt_id) REFERENCES chase_debt(id)
  356. )
  357. """)
  358. # 章节追读力元数据表
  359. cursor.execute("""
  360. CREATE TABLE IF NOT EXISTS chapter_reading_power (
  361. chapter INTEGER PRIMARY KEY,
  362. hook_type TEXT,
  363. hook_strength TEXT DEFAULT 'medium',
  364. coolpoint_patterns TEXT,
  365. micropayoffs TEXT,
  366. hard_violations TEXT,
  367. soft_suggestions TEXT,
  368. is_transition INTEGER DEFAULT 0,
  369. override_count INTEGER DEFAULT 0,
  370. debt_balance REAL DEFAULT 0.0,
  371. created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  372. updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
  373. )
  374. """)
  375. # v5.3 引入索引
  376. cursor.execute(
  377. "CREATE INDEX IF NOT EXISTS idx_override_contracts_chapter ON override_contracts(chapter)"
  378. )
  379. cursor.execute(
  380. "CREATE INDEX IF NOT EXISTS idx_override_contracts_status ON override_contracts(status)"
  381. )
  382. cursor.execute(
  383. "CREATE INDEX IF NOT EXISTS idx_override_contracts_due ON override_contracts(due_chapter)"
  384. )
  385. cursor.execute(
  386. "CREATE INDEX IF NOT EXISTS idx_chase_debt_status ON chase_debt(status)"
  387. )
  388. cursor.execute(
  389. "CREATE INDEX IF NOT EXISTS idx_chase_debt_source ON chase_debt(source_chapter)"
  390. )
  391. cursor.execute(
  392. "CREATE INDEX IF NOT EXISTS idx_chase_debt_due ON chase_debt(due_chapter)"
  393. )
  394. cursor.execute(
  395. "CREATE INDEX IF NOT EXISTS idx_debt_events_debt ON debt_events(debt_id)"
  396. )
  397. cursor.execute(
  398. "CREATE INDEX IF NOT EXISTS idx_debt_events_chapter ON debt_events(chapter)"
  399. )
  400. # ==================== v5.4 新增表:无效事实与日志 ====================
  401. # 无效事实表
  402. cursor.execute("""
  403. CREATE TABLE IF NOT EXISTS invalid_facts (
  404. id INTEGER PRIMARY KEY,
  405. source_type TEXT NOT NULL,
  406. source_id TEXT NOT NULL,
  407. reason TEXT NOT NULL,
  408. status TEXT DEFAULT 'pending',
  409. marked_by TEXT NOT NULL,
  410. marked_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  411. confirmed_at TIMESTAMP,
  412. chapter_discovered INTEGER
  413. )
  414. """)
  415. cursor.execute(
  416. "CREATE INDEX IF NOT EXISTS idx_invalid_status ON invalid_facts(status)"
  417. )
  418. cursor.execute(
  419. "CREATE INDEX IF NOT EXISTS idx_invalid_source ON invalid_facts(source_type, source_id)"
  420. )
  421. # 审查指标表
  422. cursor.execute("""
  423. CREATE TABLE IF NOT EXISTS review_metrics (
  424. start_chapter INTEGER NOT NULL,
  425. end_chapter INTEGER NOT NULL,
  426. overall_score REAL DEFAULT 0,
  427. dimension_scores TEXT,
  428. severity_counts TEXT,
  429. critical_issues TEXT,
  430. report_file TEXT,
  431. notes TEXT,
  432. created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  433. updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  434. PRIMARY KEY (start_chapter, end_chapter)
  435. )
  436. """)
  437. cursor.execute(
  438. "CREATE INDEX IF NOT EXISTS idx_review_metrics_end ON review_metrics(end_chapter)"
  439. )
  440. # RAG 查询日志
  441. cursor.execute("""
  442. CREATE TABLE IF NOT EXISTS rag_query_log (
  443. id INTEGER PRIMARY KEY,
  444. query TEXT,
  445. query_type TEXT,
  446. results_count INTEGER,
  447. hit_sources TEXT,
  448. latency_ms INTEGER,
  449. chapter INTEGER,
  450. created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
  451. )
  452. """)
  453. cursor.execute(
  454. "CREATE INDEX IF NOT EXISTS idx_rag_query_type ON rag_query_log(query_type)"
  455. )
  456. cursor.execute(
  457. "CREATE INDEX IF NOT EXISTS idx_rag_query_chapter ON rag_query_log(chapter)"
  458. )
  459. # 工具调用统计
  460. cursor.execute("""
  461. CREATE TABLE IF NOT EXISTS tool_call_stats (
  462. id INTEGER PRIMARY KEY,
  463. tool_name TEXT,
  464. success BOOLEAN,
  465. retry_count INTEGER DEFAULT 0,
  466. error_code TEXT,
  467. error_message TEXT,
  468. chapter INTEGER,
  469. created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
  470. )
  471. """)
  472. cursor.execute(
  473. "CREATE INDEX IF NOT EXISTS idx_tool_stats_name ON tool_call_stats(tool_name)"
  474. )
  475. cursor.execute(
  476. "CREATE INDEX IF NOT EXISTS idx_tool_stats_chapter ON tool_call_stats(chapter)"
  477. )
  478. # 写作清单评分记录(Phase F)
  479. cursor.execute("""
  480. CREATE TABLE IF NOT EXISTS writing_checklist_scores (
  481. chapter INTEGER PRIMARY KEY,
  482. template TEXT DEFAULT 'plot',
  483. total_items INTEGER DEFAULT 0,
  484. required_items INTEGER DEFAULT 0,
  485. completed_items INTEGER DEFAULT 0,
  486. completed_required INTEGER DEFAULT 0,
  487. total_weight REAL DEFAULT 0,
  488. completed_weight REAL DEFAULT 0,
  489. completion_rate REAL DEFAULT 0,
  490. score REAL DEFAULT 0,
  491. score_breakdown TEXT,
  492. pending_items TEXT,
  493. source TEXT,
  494. notes TEXT,
  495. created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  496. updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
  497. )
  498. """)
  499. cursor.execute(
  500. "CREATE INDEX IF NOT EXISTS idx_checklist_score_value ON writing_checklist_scores(score)"
  501. )
  502. conn.commit()
  503. @contextmanager
  504. def _get_conn(self):
  505. """获取数据库连接"""
  506. conn = sqlite3.connect(str(self.config.index_db))
  507. conn.row_factory = sqlite3.Row
  508. try:
  509. yield conn
  510. finally:
  511. conn.close()
  512. # ==================== 章节操作 ====================
  513. # ==================== CLI 接口 ====================
  514. def main():
  515. import argparse
  516. from .cli_output import print_success, print_error
  517. parser = argparse.ArgumentParser(description="Index Manager CLI (v5.4)")
  518. parser.add_argument("--project-root", type=str, help="项目根目录")
  519. subparsers = parser.add_subparsers(dest="command")
  520. # 获取统计
  521. subparsers.add_parser("stats")
  522. # 查询章节
  523. chapter_parser = subparsers.add_parser("get-chapter")
  524. chapter_parser.add_argument("--chapter", type=int, required=True)
  525. # 查询最近出场
  526. recent_parser = subparsers.add_parser("recent-appearances")
  527. recent_parser.add_argument("--limit", type=int, default=None)
  528. # 查询实体出场
  529. entity_parser = subparsers.add_parser("entity-appearances")
  530. entity_parser.add_argument("--entity", required=True)
  531. entity_parser.add_argument("--limit", type=int, default=None)
  532. # 搜索场景
  533. search_parser = subparsers.add_parser("search-scenes")
  534. search_parser.add_argument("--location", required=True)
  535. search_parser.add_argument("--limit", type=int, default=None)
  536. # 处理章节数据 (写入)
  537. process_parser = subparsers.add_parser("process-chapter")
  538. process_parser.add_argument("--chapter", type=int, required=True)
  539. process_parser.add_argument("--title", required=True)
  540. process_parser.add_argument("--location", required=True)
  541. process_parser.add_argument("--word-count", type=int, required=True)
  542. process_parser.add_argument("--entities", required=True, help="JSON 格式的实体列表")
  543. process_parser.add_argument("--scenes", required=True, help="JSON 格式的场景列表")
  544. # ==================== v5.1 引入命令 ====================
  545. # 获取实体
  546. get_entity_parser = subparsers.add_parser("get-entity")
  547. get_entity_parser.add_argument("--id", required=True, help="实体 ID")
  548. # 获取核心实体
  549. subparsers.add_parser("get-core-entities")
  550. # 获取主角
  551. subparsers.add_parser("get-protagonist")
  552. # 按类型获取实体
  553. type_parser = subparsers.add_parser("get-entities-by-type")
  554. type_parser.add_argument(
  555. "--type", required=True, help="实体类型 (角色/地点/物品/势力/招式)"
  556. )
  557. type_parser.add_argument("--include-archived", action="store_true")
  558. # 按别名查找实体
  559. alias_parser = subparsers.add_parser("get-by-alias")
  560. alias_parser.add_argument("--alias", required=True, help="别名")
  561. # 获取实体别名
  562. aliases_parser = subparsers.add_parser("get-aliases")
  563. aliases_parser.add_argument("--entity", required=True, help="实体 ID")
  564. # 注册别名
  565. reg_alias_parser = subparsers.add_parser("register-alias")
  566. reg_alias_parser.add_argument("--alias", required=True)
  567. reg_alias_parser.add_argument("--entity", required=True)
  568. reg_alias_parser.add_argument("--type", required=True, help="实体类型")
  569. # 获取实体关系
  570. rel_parser = subparsers.add_parser("get-relationships")
  571. rel_parser.add_argument("--entity", required=True)
  572. rel_parser.add_argument(
  573. "--direction", choices=["from", "to", "both"], default="both"
  574. )
  575. # 获取状态变化
  576. changes_parser = subparsers.add_parser("get-state-changes")
  577. changes_parser.add_argument("--entity", required=True)
  578. changes_parser.add_argument("--limit", type=int, default=20)
  579. # 写入实体
  580. upsert_entity_parser = subparsers.add_parser("upsert-entity")
  581. upsert_entity_parser.add_argument(
  582. "--data", required=True, help="JSON 格式的实体数据"
  583. )
  584. # 写入关系
  585. upsert_rel_parser = subparsers.add_parser("upsert-relationship")
  586. upsert_rel_parser.add_argument("--data", required=True, help="JSON 格式的关系数据")
  587. # 写入状态变化
  588. state_change_parser = subparsers.add_parser("record-state-change")
  589. state_change_parser.add_argument(
  590. "--data", required=True, help="JSON 格式的状态变化数据"
  591. )
  592. # ==================== v5.4 新增命令 ====================
  593. invalid_parser = subparsers.add_parser("mark-invalid")
  594. invalid_parser.add_argument("--source-type", required=True)
  595. invalid_parser.add_argument("--source-id", required=True)
  596. invalid_parser.add_argument("--reason", required=True)
  597. invalid_parser.add_argument("--marked-by", default="user")
  598. invalid_parser.add_argument("--chapter", type=int, default=None)
  599. resolve_parser = subparsers.add_parser("resolve-invalid")
  600. resolve_parser.add_argument("--id", type=int, required=True)
  601. resolve_parser.add_argument("--action", choices=["confirm", "dismiss"], required=True)
  602. list_invalid_parser = subparsers.add_parser("list-invalid")
  603. list_invalid_parser.add_argument("--status", choices=["pending", "confirmed"], default=None)
  604. review_save_parser = subparsers.add_parser("save-review-metrics")
  605. review_save_parser.add_argument("--data", required=True, help="JSON 格式的审查指标数据")
  606. review_recent_parser = subparsers.add_parser("get-recent-review-metrics")
  607. review_recent_parser.add_argument("--limit", type=int, default=5)
  608. review_trend_parser = subparsers.add_parser("get-review-trend-stats")
  609. review_trend_parser.add_argument("--last-n", type=int, default=5)
  610. checklist_score_save_parser = subparsers.add_parser("save-writing-checklist-score")
  611. checklist_score_save_parser.add_argument("--data", required=True, help="JSON 格式的写作清单评分数据")
  612. checklist_score_get_parser = subparsers.add_parser("get-writing-checklist-score")
  613. checklist_score_get_parser.add_argument("--chapter", type=int, required=True)
  614. checklist_score_recent_parser = subparsers.add_parser("get-recent-writing-checklist-scores")
  615. checklist_score_recent_parser.add_argument("--limit", type=int, default=10)
  616. checklist_score_trend_parser = subparsers.add_parser("get-writing-checklist-score-trend")
  617. checklist_score_trend_parser.add_argument("--last-n", type=int, default=10)
  618. # ==================== v5.3 引入命令 ====================
  619. # 获取债务汇总
  620. subparsers.add_parser("get-debt-summary")
  621. # 获取最近章节追读力元数据
  622. reading_power_parser = subparsers.add_parser("get-recent-reading-power")
  623. reading_power_parser.add_argument("--limit", type=int, default=10)
  624. # 获取章节追读力元数据
  625. chapter_rp_parser = subparsers.add_parser("get-chapter-reading-power")
  626. chapter_rp_parser.add_argument("--chapter", type=int, required=True)
  627. # 获取爽点模式使用统计
  628. pattern_stats_parser = subparsers.add_parser("get-pattern-usage-stats")
  629. pattern_stats_parser.add_argument("--last-n", type=int, default=20)
  630. # 获取钩子类型使用统计
  631. hook_stats_parser = subparsers.add_parser("get-hook-type-stats")
  632. hook_stats_parser.add_argument("--last-n", type=int, default=20)
  633. # 获取待偿还Override
  634. pending_override_parser = subparsers.add_parser("get-pending-overrides")
  635. pending_override_parser.add_argument("--before-chapter", type=int, default=None)
  636. # 获取逾期Override
  637. overdue_override_parser = subparsers.add_parser("get-overdue-overrides")
  638. overdue_override_parser.add_argument("--current-chapter", type=int, required=True)
  639. # 获取活跃债务
  640. subparsers.add_parser("get-active-debts")
  641. # 获取逾期债务
  642. overdue_debt_parser = subparsers.add_parser("get-overdue-debts")
  643. overdue_debt_parser.add_argument("--current-chapter", type=int, required=True)
  644. # 计算利息
  645. accrue_parser = subparsers.add_parser("accrue-interest")
  646. accrue_parser.add_argument("--current-chapter", type=int, required=True)
  647. # 偿还债务
  648. pay_debt_parser = subparsers.add_parser("pay-debt")
  649. pay_debt_parser.add_argument("--debt-id", type=int, required=True)
  650. pay_debt_parser.add_argument("--amount", type=float, required=True)
  651. pay_debt_parser.add_argument("--chapter", type=int, required=True)
  652. # 创建Override Contract
  653. create_override_parser = subparsers.add_parser("create-override-contract")
  654. create_override_parser.add_argument(
  655. "--data", required=True, help="JSON 格式的Override Contract数据"
  656. )
  657. # 创建债务
  658. create_debt_parser = subparsers.add_parser("create-debt")
  659. create_debt_parser.add_argument("--data", required=True, help="JSON 格式的债务数据")
  660. # 标记Override已偿还
  661. fulfill_override_parser = subparsers.add_parser("fulfill-override")
  662. fulfill_override_parser.add_argument("--contract-id", type=int, required=True)
  663. # 保存章节追读力元数据
  664. save_rp_parser = subparsers.add_parser("save-chapter-reading-power")
  665. save_rp_parser.add_argument(
  666. "--data", required=True, help="JSON 格式的章节追读力元数据"
  667. )
  668. args = parser.parse_args()
  669. # 初始化
  670. config = None
  671. if args.project_root:
  672. from .config import DataModulesConfig
  673. config = DataModulesConfig.from_project_root(args.project_root)
  674. manager = IndexManager(config)
  675. tool_name = f"index_manager:{args.command or 'unknown'}"
  676. def emit_success(data=None, message: str = "ok", chapter: Optional[int] = None):
  677. print_success(data, message=message)
  678. safe_log_tool_call(manager, tool_name=tool_name, success=True, chapter=chapter)
  679. def emit_error(code: str, message: str, suggestion: Optional[str] = None, chapter: Optional[int] = None):
  680. print_error(code, message, suggestion=suggestion)
  681. safe_log_tool_call(
  682. manager,
  683. tool_name=tool_name,
  684. success=False,
  685. error_code=code,
  686. error_message=message,
  687. chapter=chapter,
  688. )
  689. if args.command == "stats":
  690. emit_success(manager.get_stats(), message="stats")
  691. elif args.command == "get-chapter":
  692. chapter = manager.get_chapter(args.chapter)
  693. if chapter:
  694. emit_success(chapter, message="chapter")
  695. else:
  696. emit_error("NOT_FOUND", f"未找到章节: {args.chapter}")
  697. elif args.command == "recent-appearances":
  698. appearances = manager.get_recent_appearances(args.limit)
  699. emit_success(appearances, message="recent_appearances")
  700. elif args.command == "entity-appearances":
  701. appearances = manager.get_entity_appearances(args.entity, args.limit)
  702. emit_success({"entity": args.entity, "appearances": appearances}, message="entity_appearances")
  703. elif args.command == "search-scenes":
  704. scenes = manager.search_scenes_by_location(args.location, args.limit)
  705. emit_success(scenes, message="scenes")
  706. elif args.command == "process-chapter":
  707. entities = json.loads(args.entities)
  708. scenes = json.loads(args.scenes)
  709. stats = manager.process_chapter_data(
  710. chapter=args.chapter,
  711. title=args.title,
  712. location=args.location,
  713. word_count=args.word_count,
  714. entities=entities,
  715. scenes=scenes,
  716. )
  717. emit_success(stats, message="chapter_processed", chapter=args.chapter)
  718. # ==================== v5.1 引入命令处理 ====================
  719. elif args.command == "get-entity":
  720. entity = manager.get_entity(args.id)
  721. if entity:
  722. emit_success(entity, message="entity")
  723. else:
  724. emit_error("NOT_FOUND", f"未找到实体: {args.id}")
  725. elif args.command == "get-core-entities":
  726. entities = manager.get_core_entities()
  727. emit_success(entities, message="core_entities")
  728. elif args.command == "get-protagonist":
  729. protagonist = manager.get_protagonist()
  730. if protagonist:
  731. emit_success(protagonist, message="protagonist")
  732. else:
  733. emit_error("NOT_FOUND", "未设置主角")
  734. elif args.command == "get-entities-by-type":
  735. entities = manager.get_entities_by_type(args.type, args.include_archived)
  736. emit_success(entities, message="entities_by_type")
  737. elif args.command == "get-by-alias":
  738. entities = manager.get_entities_by_alias(args.alias)
  739. if entities:
  740. emit_success(entities, message="entities_by_alias")
  741. else:
  742. emit_error("NOT_FOUND", f"未找到别名: {args.alias}")
  743. elif args.command == "get-aliases":
  744. aliases = manager.get_entity_aliases(args.entity)
  745. if aliases:
  746. emit_success({"entity": args.entity, "aliases": aliases}, message="aliases")
  747. else:
  748. emit_error("NOT_FOUND", f"{args.entity} 没有别名")
  749. elif args.command == "register-alias":
  750. success = manager.register_alias(args.alias, args.entity, args.type)
  751. if success:
  752. emit_success(
  753. {"alias": args.alias, "entity": args.entity, "type": args.type},
  754. message="alias_registered",
  755. )
  756. else:
  757. emit_error("ALIAS_EXISTS", f"别名已存在或注册失败: {args.alias}")
  758. elif args.command == "get-relationships":
  759. rels = manager.get_entity_relationships(args.entity, args.direction)
  760. emit_success(rels, message="relationships")
  761. elif args.command == "get-state-changes":
  762. changes = manager.get_entity_state_changes(args.entity, args.limit)
  763. emit_success(changes, message="state_changes")
  764. elif args.command == "upsert-entity":
  765. data = json.loads(args.data)
  766. entity = EntityMeta(
  767. id=data["id"],
  768. type=data["type"],
  769. canonical_name=data["canonical_name"],
  770. tier=data.get("tier", "装饰"),
  771. desc=data.get("desc", ""),
  772. current=data.get("current", {}),
  773. first_appearance=data.get("first_appearance", 0),
  774. last_appearance=data.get("last_appearance", 0),
  775. is_protagonist=data.get("is_protagonist", False),
  776. is_archived=data.get("is_archived", False),
  777. )
  778. is_new = manager.upsert_entity(entity)
  779. emit_success({"id": entity.id, "created": is_new}, message="entity_upserted")
  780. elif args.command == "upsert-relationship":
  781. data = json.loads(args.data)
  782. rel = RelationshipMeta(
  783. from_entity=data["from_entity"],
  784. to_entity=data["to_entity"],
  785. type=data["type"],
  786. description=data.get("description", ""),
  787. chapter=data["chapter"],
  788. )
  789. is_new = manager.upsert_relationship(rel)
  790. emit_success(
  791. {"from": rel.from_entity, "to": rel.to_entity, "type": rel.type, "created": is_new},
  792. message="relationship_upserted",
  793. )
  794. elif args.command == "record-state-change":
  795. data = json.loads(args.data)
  796. change = StateChangeMeta(
  797. entity_id=data["entity_id"],
  798. field=data["field"],
  799. old_value=data.get("old_value", ""),
  800. new_value=data["new_value"],
  801. reason=data.get("reason", ""),
  802. chapter=data["chapter"],
  803. )
  804. record_id = manager.record_state_change(change)
  805. emit_success({"id": record_id, "entity": change.entity_id, "field": change.field}, message="state_change_recorded")
  806. # ==================== v5.4 无效事实命令处理 ====================
  807. elif args.command == "mark-invalid":
  808. invalid_id = manager.mark_invalid_fact(
  809. args.source_type,
  810. args.source_id,
  811. args.reason,
  812. marked_by=args.marked_by,
  813. chapter_discovered=args.chapter,
  814. )
  815. emit_success({"id": invalid_id}, message="invalid_marked")
  816. elif args.command == "resolve-invalid":
  817. ok = manager.resolve_invalid_fact(args.id, args.action)
  818. if ok:
  819. emit_success({"id": args.id, "action": args.action}, message="invalid_resolved")
  820. else:
  821. emit_error("INVALID_ACTION", f"无法处理 action: {args.action}")
  822. elif args.command == "list-invalid":
  823. rows = manager.list_invalid_facts(args.status)
  824. emit_success(rows, message="invalid_list")
  825. elif args.command == "save-review-metrics":
  826. data = json.loads(args.data)
  827. metrics = ReviewMetrics(
  828. start_chapter=data["start_chapter"],
  829. end_chapter=data["end_chapter"],
  830. overall_score=data.get("overall_score", 0.0),
  831. dimension_scores=data.get("dimension_scores", {}),
  832. severity_counts=data.get("severity_counts", {}),
  833. critical_issues=data.get("critical_issues", []),
  834. report_file=data.get("report_file", ""),
  835. notes=data.get("notes", ""),
  836. )
  837. manager.save_review_metrics(metrics)
  838. emit_success(
  839. {"start_chapter": metrics.start_chapter, "end_chapter": metrics.end_chapter},
  840. message="review_metrics_saved",
  841. )
  842. elif args.command == "get-recent-review-metrics":
  843. records = manager.get_recent_review_metrics(args.limit)
  844. emit_success(records, message="recent_review_metrics")
  845. elif args.command == "get-review-trend-stats":
  846. stats = manager.get_review_trend_stats(args.last_n)
  847. emit_success(stats, message="review_trend_stats")
  848. elif args.command == "save-writing-checklist-score":
  849. data = json.loads(args.data)
  850. metrics = WritingChecklistScoreMeta(
  851. chapter=data["chapter"],
  852. template=data.get("template", "plot"),
  853. total_items=data.get("total_items", 0),
  854. required_items=data.get("required_items", 0),
  855. completed_items=data.get("completed_items", 0),
  856. completed_required=data.get("completed_required", 0),
  857. total_weight=data.get("total_weight", 0.0),
  858. completed_weight=data.get("completed_weight", 0.0),
  859. completion_rate=data.get("completion_rate", 0.0),
  860. score=data.get("score", 0.0),
  861. score_breakdown=data.get("score_breakdown", {}),
  862. pending_items=data.get("pending_items", []),
  863. source=data.get("source", "context_manager"),
  864. notes=data.get("notes", ""),
  865. )
  866. manager.save_writing_checklist_score(metrics)
  867. emit_success({"chapter": metrics.chapter, "score": metrics.score}, message="writing_checklist_score_saved")
  868. elif args.command == "get-writing-checklist-score":
  869. score = manager.get_writing_checklist_score(args.chapter)
  870. if score:
  871. emit_success(score, message="writing_checklist_score")
  872. else:
  873. emit_error("NOT_FOUND", f"未找到第 {args.chapter} 章的写作清单评分")
  874. elif args.command == "get-recent-writing-checklist-scores":
  875. scores = manager.get_recent_writing_checklist_scores(args.limit)
  876. emit_success(scores, message="recent_writing_checklist_scores")
  877. elif args.command == "get-writing-checklist-score-trend":
  878. trend = manager.get_writing_checklist_score_trend(args.last_n)
  879. emit_success(trend, message="writing_checklist_score_trend")
  880. # ==================== v5.3 引入命令处理 ====================
  881. elif args.command == "get-debt-summary":
  882. summary = manager.get_debt_summary()
  883. emit_success(summary, message="debt_summary")
  884. elif args.command == "get-recent-reading-power":
  885. records = manager.get_recent_reading_power(args.limit)
  886. emit_success(records, message="recent_reading_power")
  887. elif args.command == "get-chapter-reading-power":
  888. record = manager.get_chapter_reading_power(args.chapter)
  889. if record:
  890. emit_success(record, message="chapter_reading_power")
  891. else:
  892. emit_error("NOT_FOUND", f"未找到第 {args.chapter} 章的追读力元数据")
  893. elif args.command == "get-pattern-usage-stats":
  894. stats = manager.get_pattern_usage_stats(args.last_n)
  895. emit_success(stats, message="pattern_usage_stats")
  896. elif args.command == "get-hook-type-stats":
  897. stats = manager.get_hook_type_stats(args.last_n)
  898. emit_success(stats, message="hook_type_stats")
  899. elif args.command == "get-pending-overrides":
  900. overrides = manager.get_pending_overrides(args.before_chapter)
  901. emit_success(overrides, message="pending_overrides")
  902. elif args.command == "get-overdue-overrides":
  903. overrides = manager.get_overdue_overrides(args.current_chapter)
  904. emit_success(overrides, message="overdue_overrides")
  905. elif args.command == "get-active-debts":
  906. debts = manager.get_active_debts()
  907. emit_success(debts, message="active_debts")
  908. elif args.command == "get-overdue-debts":
  909. debts = manager.get_overdue_debts(args.current_chapter)
  910. emit_success(debts, message="overdue_debts")
  911. elif args.command == "accrue-interest":
  912. result = manager.accrue_interest(args.current_chapter)
  913. emit_success(result, message="interest_accrued", chapter=args.current_chapter)
  914. elif args.command == "pay-debt":
  915. result = manager.pay_debt(args.debt_id, args.amount, args.chapter)
  916. if "error" in result:
  917. emit_error("PAY_DEBT_FAILED", result["error"], chapter=args.chapter)
  918. else:
  919. emit_success(result, message="debt_payment", chapter=args.chapter)
  920. elif args.command == "create-override-contract":
  921. data = json.loads(args.data)
  922. contract = OverrideContractMeta(
  923. chapter=data["chapter"],
  924. constraint_type=data["constraint_type"],
  925. constraint_id=data["constraint_id"],
  926. rationale_type=data["rationale_type"],
  927. rationale_text=data.get("rationale_text", ""),
  928. payback_plan=data.get("payback_plan", ""),
  929. due_chapter=data["due_chapter"],
  930. status=data.get("status", "pending"),
  931. )
  932. contract_id = manager.create_override_contract(contract)
  933. emit_success({"id": contract_id}, message="override_contract_created")
  934. elif args.command == "create-debt":
  935. data = json.loads(args.data)
  936. debt = ChaseDebtMeta(
  937. debt_type=data["debt_type"],
  938. original_amount=data.get("original_amount", 1.0),
  939. current_amount=data.get("current_amount", data.get("original_amount", 1.0)),
  940. interest_rate=data.get("interest_rate", 0.1),
  941. source_chapter=data["source_chapter"],
  942. due_chapter=data["due_chapter"],
  943. override_contract_id=data.get("override_contract_id", 0),
  944. status=data.get("status", "active"),
  945. )
  946. debt_id = manager.create_debt(debt)
  947. emit_success({"id": debt_id, "debt_type": debt.debt_type}, message="debt_created")
  948. elif args.command == "fulfill-override":
  949. success = manager.fulfill_override(args.contract_id)
  950. if success:
  951. emit_success({"id": args.contract_id}, message="override_fulfilled")
  952. else:
  953. emit_error("NOT_FOUND", f"未找到 Override Contract #{args.contract_id}")
  954. elif args.command == "save-chapter-reading-power":
  955. data = json.loads(args.data)
  956. meta = ChapterReadingPowerMeta(
  957. chapter=data["chapter"],
  958. hook_type=data.get("hook_type", ""),
  959. hook_strength=data.get("hook_strength", "medium"),
  960. coolpoint_patterns=data.get("coolpoint_patterns", []),
  961. micropayoffs=data.get("micropayoffs", []),
  962. hard_violations=data.get("hard_violations", []),
  963. soft_suggestions=data.get("soft_suggestions", []),
  964. is_transition=data.get("is_transition", False),
  965. override_count=data.get("override_count", 0),
  966. debt_balance=data.get("debt_balance", 0.0),
  967. )
  968. manager.save_chapter_reading_power(meta)
  969. emit_success({"chapter": meta.chapter}, message="reading_power_saved")
  970. else:
  971. emit_error("UNKNOWN_COMMAND", "未指定有效命令", suggestion="请查看 --help")
  972. if __name__ == "__main__":
  973. import sys
  974. if sys.platform == "win32":
  975. enable_windows_utf8_stdio()
  976. main()