index_manager.py 109 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972
  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 typing import Dict, List, Optional, Any, Tuple
  34. from dataclasses import dataclass, field
  35. from contextlib import contextmanager
  36. from datetime import datetime
  37. from .config import get_config
  38. @dataclass
  39. class ChapterMeta:
  40. """章节元数据"""
  41. chapter: int
  42. title: str
  43. location: str
  44. word_count: int
  45. characters: List[str]
  46. summary: str = ""
  47. @dataclass
  48. class SceneMeta:
  49. """场景元数据"""
  50. chapter: int
  51. scene_index: int
  52. start_line: int
  53. end_line: int
  54. location: str
  55. summary: str
  56. characters: List[str]
  57. @dataclass
  58. class EntityMeta:
  59. """实体元数据 (v5.1 引入)"""
  60. id: str
  61. type: str # 角色/地点/物品/势力/招式
  62. canonical_name: str
  63. tier: str = "装饰" # 核心/重要/次要/装饰
  64. desc: str = ""
  65. current: Dict = field(default_factory=dict) # 当前状态 (realm/location/items等)
  66. first_appearance: int = 0
  67. last_appearance: int = 0
  68. is_protagonist: bool = False
  69. is_archived: bool = False
  70. @dataclass
  71. class StateChangeMeta:
  72. """状态变化记录 (v5.1 引入)"""
  73. entity_id: str
  74. field: str
  75. old_value: str
  76. new_value: str
  77. reason: str
  78. chapter: int
  79. @dataclass
  80. class RelationshipMeta:
  81. """关系记录 (v5.1 引入)"""
  82. from_entity: str
  83. to_entity: str
  84. type: str
  85. description: str
  86. chapter: int
  87. @dataclass
  88. class OverrideContractMeta:
  89. """Override Contract (v5.3 引入)"""
  90. chapter: int
  91. constraint_type: str # SOFT_HOOK_STRENGTH / SOFT_MICROPAYOFF / etc.
  92. constraint_id: str # 具体约束标识
  93. rationale_type: str # TRANSITIONAL_SETUP / LOGIC_INTEGRITY / etc.
  94. rationale_text: str # 具体理由说明
  95. payback_plan: str # 偿还计划描述
  96. due_chapter: int # 偿还截止章节
  97. status: str = "pending" # pending / fulfilled / overdue / cancelled
  98. @dataclass
  99. class ChaseDebtMeta:
  100. """追读力债务 (v5.3 引入)"""
  101. id: int = 0
  102. debt_type: str = "" # hook_strength / micropayoff / coolpoint / etc.
  103. original_amount: float = 1.0 # 初始债务量
  104. current_amount: float = 1.0 # 当前债务量(含利息)
  105. interest_rate: float = 0.1 # 利息率(每章)
  106. source_chapter: int = 0 # 产生债务的章节
  107. due_chapter: int = 0 # 截止章节
  108. override_contract_id: int = 0 # 关联的Override Contract
  109. status: str = "active" # active / paid / overdue / written_off
  110. @dataclass
  111. class DebtEventMeta:
  112. """债务事件日志 (v5.3 引入)"""
  113. debt_id: int
  114. event_type: (
  115. str # created / interest_accrued / partial_payment / full_payment / overdue
  116. )
  117. amount: float
  118. chapter: int
  119. note: str = ""
  120. @dataclass
  121. class ChapterReadingPowerMeta:
  122. """章节追读力元数据 (v5.3 引入)"""
  123. chapter: int
  124. hook_type: str = "" # 章末钩子类型
  125. hook_strength: str = "medium" # strong / medium / weak
  126. coolpoint_patterns: List[str] = field(default_factory=list) # 使用的爽点模式
  127. micropayoffs: List[str] = field(default_factory=list) # 微兑现列表
  128. hard_violations: List[str] = field(default_factory=list) # 硬约束违规
  129. soft_suggestions: List[str] = field(default_factory=list) # 软建议
  130. is_transition: bool = False # 是否为过渡章
  131. override_count: int = 0 # Override Contract数量
  132. debt_balance: float = 0.0 # 当前债务余额
  133. @dataclass
  134. class ReviewMetrics:
  135. """审查指标记录 (v5.4 引入)"""
  136. start_chapter: int
  137. end_chapter: int
  138. overall_score: float = 0.0
  139. dimension_scores: Dict[str, float] = field(default_factory=dict)
  140. severity_counts: Dict[str, int] = field(default_factory=dict)
  141. critical_issues: List[str] = field(default_factory=list)
  142. report_file: str = ""
  143. notes: str = ""
  144. @dataclass
  145. class WritingChecklistScoreMeta:
  146. """写作清单评分记录(Context Contract v2 Phase F)"""
  147. chapter: int
  148. template: str = "plot"
  149. total_items: int = 0
  150. required_items: int = 0
  151. completed_items: int = 0
  152. completed_required: int = 0
  153. total_weight: float = 0.0
  154. completed_weight: float = 0.0
  155. completion_rate: float = 0.0
  156. score: float = 0.0
  157. score_breakdown: Dict[str, Any] = field(default_factory=dict)
  158. pending_items: List[str] = field(default_factory=list)
  159. source: str = "context_manager"
  160. notes: str = ""
  161. class IndexManager:
  162. """索引管理器"""
  163. def __init__(self, config=None):
  164. self.config = config or get_config()
  165. self._init_db()
  166. def _init_db(self):
  167. """初始化数据库表"""
  168. self.config.ensure_dirs()
  169. with self._get_conn() as conn:
  170. cursor = conn.cursor()
  171. # 章节表
  172. cursor.execute("""
  173. CREATE TABLE IF NOT EXISTS chapters (
  174. chapter INTEGER PRIMARY KEY,
  175. title TEXT,
  176. location TEXT,
  177. word_count INTEGER,
  178. characters TEXT,
  179. summary TEXT,
  180. created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
  181. )
  182. """)
  183. # 场景表
  184. cursor.execute("""
  185. CREATE TABLE IF NOT EXISTS scenes (
  186. id INTEGER PRIMARY KEY AUTOINCREMENT,
  187. chapter INTEGER,
  188. scene_index INTEGER,
  189. start_line INTEGER,
  190. end_line INTEGER,
  191. location TEXT,
  192. summary TEXT,
  193. characters TEXT,
  194. UNIQUE(chapter, scene_index)
  195. )
  196. """)
  197. # 实体出场表
  198. cursor.execute("""
  199. CREATE TABLE IF NOT EXISTS appearances (
  200. id INTEGER PRIMARY KEY AUTOINCREMENT,
  201. entity_id TEXT,
  202. chapter INTEGER,
  203. mentions TEXT,
  204. confidence REAL,
  205. UNIQUE(entity_id, chapter)
  206. )
  207. """)
  208. # 创建索引
  209. cursor.execute(
  210. "CREATE INDEX IF NOT EXISTS idx_scenes_chapter ON scenes(chapter)"
  211. )
  212. cursor.execute(
  213. "CREATE INDEX IF NOT EXISTS idx_appearances_entity ON appearances(entity_id)"
  214. )
  215. cursor.execute(
  216. "CREATE INDEX IF NOT EXISTS idx_appearances_chapter ON appearances(chapter)"
  217. )
  218. # ==================== v5.1 引入表 ====================
  219. # 实体表 (替代 state.json 中的 entities_v3)
  220. cursor.execute("""
  221. CREATE TABLE IF NOT EXISTS entities (
  222. id TEXT PRIMARY KEY,
  223. type TEXT NOT NULL,
  224. canonical_name TEXT NOT NULL,
  225. tier TEXT DEFAULT '装饰',
  226. desc TEXT,
  227. current_json TEXT,
  228. first_appearance INTEGER DEFAULT 0,
  229. last_appearance INTEGER DEFAULT 0,
  230. is_protagonist INTEGER DEFAULT 0,
  231. is_archived INTEGER DEFAULT 0,
  232. created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  233. updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
  234. )
  235. """)
  236. # 别名表 (替代 state.json 中的 alias_index,支持一对多)
  237. cursor.execute("""
  238. CREATE TABLE IF NOT EXISTS aliases (
  239. alias TEXT NOT NULL,
  240. entity_id TEXT NOT NULL,
  241. entity_type TEXT NOT NULL,
  242. created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  243. PRIMARY KEY (alias, entity_id, entity_type)
  244. )
  245. """)
  246. # 状态变化表 (替代 state.json 中的 state_changes)
  247. cursor.execute("""
  248. CREATE TABLE IF NOT EXISTS state_changes (
  249. id INTEGER PRIMARY KEY AUTOINCREMENT,
  250. entity_id TEXT NOT NULL,
  251. field TEXT NOT NULL,
  252. old_value TEXT,
  253. new_value TEXT,
  254. reason TEXT,
  255. chapter INTEGER NOT NULL,
  256. created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
  257. )
  258. """)
  259. # 关系表 (替代 state.json 中的 structured_relationships)
  260. cursor.execute("""
  261. CREATE TABLE IF NOT EXISTS relationships (
  262. id INTEGER PRIMARY KEY AUTOINCREMENT,
  263. from_entity TEXT NOT NULL,
  264. to_entity TEXT NOT NULL,
  265. type TEXT NOT NULL,
  266. description TEXT,
  267. chapter INTEGER NOT NULL,
  268. created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  269. UNIQUE(from_entity, to_entity, type)
  270. )
  271. """)
  272. # v5.1 引入索引
  273. cursor.execute(
  274. "CREATE INDEX IF NOT EXISTS idx_entities_type ON entities(type)"
  275. )
  276. cursor.execute(
  277. "CREATE INDEX IF NOT EXISTS idx_entities_tier ON entities(tier)"
  278. )
  279. cursor.execute(
  280. "CREATE INDEX IF NOT EXISTS idx_entities_protagonist ON entities(is_protagonist)"
  281. )
  282. cursor.execute(
  283. "CREATE INDEX IF NOT EXISTS idx_aliases_entity ON aliases(entity_id)"
  284. )
  285. cursor.execute(
  286. "CREATE INDEX IF NOT EXISTS idx_aliases_alias ON aliases(alias)"
  287. )
  288. cursor.execute(
  289. "CREATE INDEX IF NOT EXISTS idx_state_changes_entity ON state_changes(entity_id)"
  290. )
  291. cursor.execute(
  292. "CREATE INDEX IF NOT EXISTS idx_state_changes_chapter ON state_changes(chapter)"
  293. )
  294. cursor.execute(
  295. "CREATE INDEX IF NOT EXISTS idx_relationships_from ON relationships(from_entity)"
  296. )
  297. cursor.execute(
  298. "CREATE INDEX IF NOT EXISTS idx_relationships_to ON relationships(to_entity)"
  299. )
  300. cursor.execute(
  301. "CREATE INDEX IF NOT EXISTS idx_relationships_chapter ON relationships(chapter)"
  302. )
  303. # ==================== v5.3 引入表:追读力债务管理 ====================
  304. # Override Contract 表
  305. cursor.execute("""
  306. CREATE TABLE IF NOT EXISTS override_contracts (
  307. id INTEGER PRIMARY KEY AUTOINCREMENT,
  308. chapter INTEGER NOT NULL,
  309. constraint_type TEXT NOT NULL,
  310. constraint_id TEXT NOT NULL,
  311. rationale_type TEXT NOT NULL,
  312. rationale_text TEXT,
  313. payback_plan TEXT,
  314. due_chapter INTEGER NOT NULL,
  315. status TEXT DEFAULT 'pending',
  316. created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  317. fulfilled_at TIMESTAMP,
  318. UNIQUE(chapter, constraint_type, constraint_id)
  319. )
  320. """)
  321. # 追读力债务表
  322. cursor.execute("""
  323. CREATE TABLE IF NOT EXISTS chase_debt (
  324. id INTEGER PRIMARY KEY AUTOINCREMENT,
  325. debt_type TEXT NOT NULL,
  326. original_amount REAL DEFAULT 1.0,
  327. current_amount REAL DEFAULT 1.0,
  328. interest_rate REAL DEFAULT 0.1,
  329. source_chapter INTEGER NOT NULL,
  330. due_chapter INTEGER NOT NULL,
  331. override_contract_id INTEGER,
  332. status TEXT DEFAULT 'active',
  333. created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  334. updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  335. FOREIGN KEY (override_contract_id) REFERENCES override_contracts(id)
  336. )
  337. """)
  338. # 债务事件日志表
  339. cursor.execute("""
  340. CREATE TABLE IF NOT EXISTS debt_events (
  341. id INTEGER PRIMARY KEY AUTOINCREMENT,
  342. debt_id INTEGER NOT NULL,
  343. event_type TEXT NOT NULL,
  344. amount REAL NOT NULL,
  345. chapter INTEGER NOT NULL,
  346. note TEXT,
  347. created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  348. FOREIGN KEY (debt_id) REFERENCES chase_debt(id)
  349. )
  350. """)
  351. # 章节追读力元数据表
  352. cursor.execute("""
  353. CREATE TABLE IF NOT EXISTS chapter_reading_power (
  354. chapter INTEGER PRIMARY KEY,
  355. hook_type TEXT,
  356. hook_strength TEXT DEFAULT 'medium',
  357. coolpoint_patterns TEXT,
  358. micropayoffs TEXT,
  359. hard_violations TEXT,
  360. soft_suggestions TEXT,
  361. is_transition INTEGER DEFAULT 0,
  362. override_count INTEGER DEFAULT 0,
  363. debt_balance REAL DEFAULT 0.0,
  364. created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  365. updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
  366. )
  367. """)
  368. # v5.3 引入索引
  369. cursor.execute(
  370. "CREATE INDEX IF NOT EXISTS idx_override_contracts_chapter ON override_contracts(chapter)"
  371. )
  372. cursor.execute(
  373. "CREATE INDEX IF NOT EXISTS idx_override_contracts_status ON override_contracts(status)"
  374. )
  375. cursor.execute(
  376. "CREATE INDEX IF NOT EXISTS idx_override_contracts_due ON override_contracts(due_chapter)"
  377. )
  378. cursor.execute(
  379. "CREATE INDEX IF NOT EXISTS idx_chase_debt_status ON chase_debt(status)"
  380. )
  381. cursor.execute(
  382. "CREATE INDEX IF NOT EXISTS idx_chase_debt_source ON chase_debt(source_chapter)"
  383. )
  384. cursor.execute(
  385. "CREATE INDEX IF NOT EXISTS idx_chase_debt_due ON chase_debt(due_chapter)"
  386. )
  387. cursor.execute(
  388. "CREATE INDEX IF NOT EXISTS idx_debt_events_debt ON debt_events(debt_id)"
  389. )
  390. cursor.execute(
  391. "CREATE INDEX IF NOT EXISTS idx_debt_events_chapter ON debt_events(chapter)"
  392. )
  393. # ==================== v5.4 新增表:无效事实与日志 ====================
  394. # 无效事实表
  395. cursor.execute("""
  396. CREATE TABLE IF NOT EXISTS invalid_facts (
  397. id INTEGER PRIMARY KEY,
  398. source_type TEXT NOT NULL,
  399. source_id TEXT NOT NULL,
  400. reason TEXT NOT NULL,
  401. status TEXT DEFAULT 'pending',
  402. marked_by TEXT NOT NULL,
  403. marked_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  404. confirmed_at TIMESTAMP,
  405. chapter_discovered INTEGER
  406. )
  407. """)
  408. cursor.execute(
  409. "CREATE INDEX IF NOT EXISTS idx_invalid_status ON invalid_facts(status)"
  410. )
  411. cursor.execute(
  412. "CREATE INDEX IF NOT EXISTS idx_invalid_source ON invalid_facts(source_type, source_id)"
  413. )
  414. # 审查指标表
  415. cursor.execute("""
  416. CREATE TABLE IF NOT EXISTS review_metrics (
  417. start_chapter INTEGER NOT NULL,
  418. end_chapter INTEGER NOT NULL,
  419. overall_score REAL DEFAULT 0,
  420. dimension_scores TEXT,
  421. severity_counts TEXT,
  422. critical_issues TEXT,
  423. report_file TEXT,
  424. notes TEXT,
  425. created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  426. updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  427. PRIMARY KEY (start_chapter, end_chapter)
  428. )
  429. """)
  430. cursor.execute(
  431. "CREATE INDEX IF NOT EXISTS idx_review_metrics_end ON review_metrics(end_chapter)"
  432. )
  433. # RAG 查询日志
  434. cursor.execute("""
  435. CREATE TABLE IF NOT EXISTS rag_query_log (
  436. id INTEGER PRIMARY KEY,
  437. query TEXT,
  438. query_type TEXT,
  439. results_count INTEGER,
  440. hit_sources TEXT,
  441. latency_ms INTEGER,
  442. chapter INTEGER,
  443. created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
  444. )
  445. """)
  446. cursor.execute(
  447. "CREATE INDEX IF NOT EXISTS idx_rag_query_type ON rag_query_log(query_type)"
  448. )
  449. cursor.execute(
  450. "CREATE INDEX IF NOT EXISTS idx_rag_query_chapter ON rag_query_log(chapter)"
  451. )
  452. # 工具调用统计
  453. cursor.execute("""
  454. CREATE TABLE IF NOT EXISTS tool_call_stats (
  455. id INTEGER PRIMARY KEY,
  456. tool_name TEXT,
  457. success BOOLEAN,
  458. retry_count INTEGER DEFAULT 0,
  459. error_code TEXT,
  460. error_message TEXT,
  461. chapter INTEGER,
  462. created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
  463. )
  464. """)
  465. cursor.execute(
  466. "CREATE INDEX IF NOT EXISTS idx_tool_stats_name ON tool_call_stats(tool_name)"
  467. )
  468. cursor.execute(
  469. "CREATE INDEX IF NOT EXISTS idx_tool_stats_chapter ON tool_call_stats(chapter)"
  470. )
  471. # 写作清单评分记录(Phase F)
  472. cursor.execute("""
  473. CREATE TABLE IF NOT EXISTS writing_checklist_scores (
  474. chapter INTEGER PRIMARY KEY,
  475. template TEXT DEFAULT 'plot',
  476. total_items INTEGER DEFAULT 0,
  477. required_items INTEGER DEFAULT 0,
  478. completed_items INTEGER DEFAULT 0,
  479. completed_required INTEGER DEFAULT 0,
  480. total_weight REAL DEFAULT 0,
  481. completed_weight REAL DEFAULT 0,
  482. completion_rate REAL DEFAULT 0,
  483. score REAL DEFAULT 0,
  484. score_breakdown TEXT,
  485. pending_items TEXT,
  486. source TEXT,
  487. notes TEXT,
  488. created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  489. updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
  490. )
  491. """)
  492. cursor.execute(
  493. "CREATE INDEX IF NOT EXISTS idx_checklist_score_value ON writing_checklist_scores(score)"
  494. )
  495. conn.commit()
  496. @contextmanager
  497. def _get_conn(self):
  498. """获取数据库连接"""
  499. conn = sqlite3.connect(str(self.config.index_db))
  500. conn.row_factory = sqlite3.Row
  501. try:
  502. yield conn
  503. finally:
  504. conn.close()
  505. # ==================== 章节操作 ====================
  506. def add_chapter(self, meta: ChapterMeta):
  507. """添加/更新章节元数据"""
  508. with self._get_conn() as conn:
  509. cursor = conn.cursor()
  510. cursor.execute(
  511. """
  512. INSERT OR REPLACE INTO chapters
  513. (chapter, title, location, word_count, characters, summary)
  514. VALUES (?, ?, ?, ?, ?, ?)
  515. """,
  516. (
  517. meta.chapter,
  518. meta.title,
  519. meta.location,
  520. meta.word_count,
  521. json.dumps(meta.characters, ensure_ascii=False),
  522. meta.summary,
  523. ),
  524. )
  525. conn.commit()
  526. def get_chapter(self, chapter: int) -> Optional[Dict]:
  527. """获取章节元数据"""
  528. with self._get_conn() as conn:
  529. cursor = conn.cursor()
  530. cursor.execute("SELECT * FROM chapters WHERE chapter = ?", (chapter,))
  531. row = cursor.fetchone()
  532. if row:
  533. return self._row_to_dict(row, parse_json=["characters"])
  534. return None
  535. def get_recent_chapters(self, limit: int = None) -> List[Dict]:
  536. """获取最近章节"""
  537. if limit is None:
  538. limit = self.config.query_recent_chapters_limit
  539. with self._get_conn() as conn:
  540. cursor = conn.cursor()
  541. cursor.execute(
  542. """
  543. SELECT * FROM chapters
  544. ORDER BY chapter DESC
  545. LIMIT ?
  546. """,
  547. (limit,),
  548. )
  549. return [
  550. self._row_to_dict(row, parse_json=["characters"])
  551. for row in cursor.fetchall()
  552. ]
  553. # ==================== 场景操作 ====================
  554. def add_scenes(self, chapter: int, scenes: List[SceneMeta]):
  555. """添加章节场景"""
  556. with self._get_conn() as conn:
  557. cursor = conn.cursor()
  558. # 先删除该章节旧场景
  559. cursor.execute("DELETE FROM scenes WHERE chapter = ?", (chapter,))
  560. # 插入新场景
  561. for scene in scenes:
  562. cursor.execute(
  563. """
  564. INSERT INTO scenes
  565. (chapter, scene_index, start_line, end_line, location, summary, characters)
  566. VALUES (?, ?, ?, ?, ?, ?, ?)
  567. """,
  568. (
  569. scene.chapter,
  570. scene.scene_index,
  571. scene.start_line,
  572. scene.end_line,
  573. scene.location,
  574. scene.summary,
  575. json.dumps(scene.characters, ensure_ascii=False),
  576. ),
  577. )
  578. conn.commit()
  579. def get_scenes(self, chapter: int) -> List[Dict]:
  580. """获取章节场景"""
  581. with self._get_conn() as conn:
  582. cursor = conn.cursor()
  583. cursor.execute(
  584. """
  585. SELECT * FROM scenes
  586. WHERE chapter = ?
  587. ORDER BY scene_index
  588. """,
  589. (chapter,),
  590. )
  591. return [
  592. self._row_to_dict(row, parse_json=["characters"])
  593. for row in cursor.fetchall()
  594. ]
  595. def search_scenes_by_location(self, location: str, limit: int = None) -> List[Dict]:
  596. """按地点搜索场景"""
  597. if limit is None:
  598. limit = self.config.query_scenes_by_location_limit
  599. with self._get_conn() as conn:
  600. cursor = conn.cursor()
  601. cursor.execute(
  602. """
  603. SELECT * FROM scenes
  604. WHERE location LIKE ?
  605. ORDER BY chapter DESC
  606. LIMIT ?
  607. """,
  608. (f"%{location}%", limit),
  609. )
  610. return [
  611. self._row_to_dict(row, parse_json=["characters"])
  612. for row in cursor.fetchall()
  613. ]
  614. # ==================== 出场记录操作 ====================
  615. def record_appearance(
  616. self,
  617. entity_id: str,
  618. chapter: int,
  619. mentions: List[str],
  620. confidence: float = 1.0,
  621. skip_if_exists: bool = False,
  622. ):
  623. """记录实体出场
  624. Args:
  625. entity_id: 实体ID
  626. chapter: 章节号
  627. mentions: 提及列表
  628. confidence: 置信度
  629. skip_if_exists: 如果为True,当记录已存在时跳过(避免覆盖已有mentions)
  630. """
  631. with self._get_conn() as conn:
  632. cursor = conn.cursor()
  633. if skip_if_exists:
  634. # 先检查是否已存在
  635. cursor.execute(
  636. "SELECT 1 FROM appearances WHERE entity_id = ? AND chapter = ?",
  637. (entity_id, chapter),
  638. )
  639. if cursor.fetchone():
  640. return # 已存在,跳过
  641. cursor.execute(
  642. """
  643. INSERT OR REPLACE INTO appearances
  644. (entity_id, chapter, mentions, confidence)
  645. VALUES (?, ?, ?, ?)
  646. """,
  647. (
  648. entity_id,
  649. chapter,
  650. json.dumps(mentions, ensure_ascii=False),
  651. confidence,
  652. ),
  653. )
  654. conn.commit()
  655. def get_entity_appearances(self, entity_id: str, limit: int = None) -> List[Dict]:
  656. """获取实体出场记录"""
  657. if limit is None:
  658. limit = self.config.query_entity_appearances_limit
  659. with self._get_conn() as conn:
  660. cursor = conn.cursor()
  661. cursor.execute(
  662. """
  663. SELECT * FROM appearances
  664. WHERE entity_id = ?
  665. ORDER BY chapter DESC
  666. LIMIT ?
  667. """,
  668. (entity_id, limit),
  669. )
  670. return [
  671. self._row_to_dict(row, parse_json=["mentions"])
  672. for row in cursor.fetchall()
  673. ]
  674. def get_recent_appearances(self, limit: int = None) -> List[Dict]:
  675. """获取最近出场的实体"""
  676. if limit is None:
  677. limit = self.config.query_recent_appearances_limit
  678. with self._get_conn() as conn:
  679. cursor = conn.cursor()
  680. cursor.execute(
  681. """
  682. SELECT entity_id, MAX(chapter) as last_chapter, COUNT(*) as total
  683. FROM appearances
  684. GROUP BY entity_id
  685. ORDER BY last_chapter DESC
  686. LIMIT ?
  687. """,
  688. (limit,),
  689. )
  690. return [dict(row) for row in cursor.fetchall()]
  691. def get_chapter_appearances(self, chapter: int) -> List[Dict]:
  692. """获取某章所有出场实体"""
  693. with self._get_conn() as conn:
  694. cursor = conn.cursor()
  695. cursor.execute(
  696. """
  697. SELECT * FROM appearances
  698. WHERE chapter = ?
  699. ORDER BY confidence DESC
  700. """,
  701. (chapter,),
  702. )
  703. return [
  704. self._row_to_dict(row, parse_json=["mentions"])
  705. for row in cursor.fetchall()
  706. ]
  707. # ==================== v5.1 实体操作 ====================
  708. def upsert_entity(self, entity: EntityMeta, update_metadata: bool = False) -> bool:
  709. """
  710. 插入或更新实体 (智能合并)
  711. - 新实体: 直接插入
  712. - 已存在: 更新 current_json, last_appearance, updated_at
  713. - update_metadata=True: 同时更新 canonical_name/tier/desc/is_protagonist/is_archived
  714. 返回是否为新实体
  715. """
  716. with self._get_conn() as conn:
  717. cursor = conn.cursor()
  718. # 检查是否存在
  719. cursor.execute(
  720. "SELECT id, current_json FROM entities WHERE id = ?", (entity.id,)
  721. )
  722. existing = cursor.fetchone()
  723. if existing:
  724. # 已存在: 智能合并 current_json
  725. old_current = {}
  726. if existing["current_json"]:
  727. try:
  728. old_current = json.loads(existing["current_json"])
  729. except json.JSONDecodeError:
  730. pass
  731. # 合并 current (新值覆盖旧值)
  732. merged_current = {**old_current, **entity.current}
  733. if update_metadata:
  734. # 完整更新(包括元数据)
  735. cursor.execute(
  736. """
  737. UPDATE entities SET
  738. canonical_name = ?,
  739. tier = ?,
  740. desc = ?,
  741. current_json = ?,
  742. last_appearance = ?,
  743. is_protagonist = ?,
  744. is_archived = ?,
  745. updated_at = CURRENT_TIMESTAMP
  746. WHERE id = ?
  747. """,
  748. (
  749. entity.canonical_name,
  750. entity.tier,
  751. entity.desc,
  752. json.dumps(merged_current, ensure_ascii=False),
  753. entity.last_appearance,
  754. 1 if entity.is_protagonist else 0,
  755. 1 if entity.is_archived else 0,
  756. entity.id,
  757. ),
  758. )
  759. else:
  760. # 只更新 current 和 last_appearance
  761. cursor.execute(
  762. """
  763. UPDATE entities SET
  764. current_json = ?,
  765. last_appearance = ?,
  766. updated_at = CURRENT_TIMESTAMP
  767. WHERE id = ?
  768. """,
  769. (
  770. json.dumps(merged_current, ensure_ascii=False),
  771. entity.last_appearance,
  772. entity.id,
  773. ),
  774. )
  775. conn.commit()
  776. return False
  777. else:
  778. # 新实体: 插入
  779. cursor.execute(
  780. """
  781. INSERT INTO entities
  782. (id, type, canonical_name, tier, desc, current_json,
  783. first_appearance, last_appearance, is_protagonist, is_archived)
  784. VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
  785. """,
  786. (
  787. entity.id,
  788. entity.type,
  789. entity.canonical_name,
  790. entity.tier,
  791. entity.desc,
  792. json.dumps(entity.current, ensure_ascii=False),
  793. entity.first_appearance,
  794. entity.last_appearance,
  795. 1 if entity.is_protagonist else 0,
  796. 1 if entity.is_archived else 0,
  797. ),
  798. )
  799. conn.commit()
  800. return True
  801. def get_entity(self, entity_id: str) -> Optional[Dict]:
  802. """获取单个实体"""
  803. with self._get_conn() as conn:
  804. cursor = conn.cursor()
  805. cursor.execute("SELECT * FROM entities WHERE id = ?", (entity_id,))
  806. row = cursor.fetchone()
  807. if row:
  808. return self._row_to_dict(row, parse_json=["current_json"])
  809. return None
  810. def get_entities_by_type(
  811. self, entity_type: str, include_archived: bool = False
  812. ) -> List[Dict]:
  813. """按类型获取实体"""
  814. with self._get_conn() as conn:
  815. cursor = conn.cursor()
  816. if include_archived:
  817. cursor.execute(
  818. """
  819. SELECT * FROM entities WHERE type = ?
  820. ORDER BY last_appearance DESC
  821. """,
  822. (entity_type,),
  823. )
  824. else:
  825. cursor.execute(
  826. """
  827. SELECT * FROM entities WHERE type = ? AND is_archived = 0
  828. ORDER BY last_appearance DESC
  829. """,
  830. (entity_type,),
  831. )
  832. return [
  833. self._row_to_dict(row, parse_json=["current_json"])
  834. for row in cursor.fetchall()
  835. ]
  836. def get_entities_by_tier(self, tier: str) -> List[Dict]:
  837. """按重要度获取实体 (核心/重要/次要/装饰)"""
  838. with self._get_conn() as conn:
  839. cursor = conn.cursor()
  840. cursor.execute(
  841. """
  842. SELECT * FROM entities WHERE tier = ? AND is_archived = 0
  843. ORDER BY last_appearance DESC
  844. """,
  845. (tier,),
  846. )
  847. return [
  848. self._row_to_dict(row, parse_json=["current_json"])
  849. for row in cursor.fetchall()
  850. ]
  851. def get_core_entities(self) -> List[Dict]:
  852. """获取所有核心实体 (用于 Context Agent 全量加载)"""
  853. with self._get_conn() as conn:
  854. cursor = conn.cursor()
  855. cursor.execute("""
  856. SELECT * FROM entities
  857. WHERE (tier IN ('核心', '重要') OR is_protagonist = 1) AND is_archived = 0
  858. ORDER BY is_protagonist DESC, tier, last_appearance DESC
  859. """)
  860. return [
  861. self._row_to_dict(row, parse_json=["current_json"])
  862. for row in cursor.fetchall()
  863. ]
  864. def get_protagonist(self) -> Optional[Dict]:
  865. """获取主角实体"""
  866. with self._get_conn() as conn:
  867. cursor = conn.cursor()
  868. cursor.execute("SELECT * FROM entities WHERE is_protagonist = 1 LIMIT 1")
  869. row = cursor.fetchone()
  870. if row:
  871. return self._row_to_dict(row, parse_json=["current_json"])
  872. return None
  873. def update_entity_current(self, entity_id: str, updates: Dict) -> bool:
  874. """
  875. 增量更新实体的 current 字段 (不覆盖其他字段)
  876. 例如: update_entity_current("xiaoyan", {"realm": "斗师"})
  877. """
  878. with self._get_conn() as conn:
  879. cursor = conn.cursor()
  880. cursor.execute(
  881. "SELECT current_json FROM entities WHERE id = ?", (entity_id,)
  882. )
  883. row = cursor.fetchone()
  884. if not row:
  885. return False
  886. current = {}
  887. if row["current_json"]:
  888. try:
  889. current = json.loads(row["current_json"])
  890. except json.JSONDecodeError:
  891. pass
  892. current.update(updates)
  893. cursor.execute(
  894. """
  895. UPDATE entities SET
  896. current_json = ?,
  897. updated_at = CURRENT_TIMESTAMP
  898. WHERE id = ?
  899. """,
  900. (json.dumps(current, ensure_ascii=False), entity_id),
  901. )
  902. conn.commit()
  903. return True
  904. def archive_entity(self, entity_id: str) -> bool:
  905. """归档实体 (不删除,只是标记)"""
  906. with self._get_conn() as conn:
  907. cursor = conn.cursor()
  908. cursor.execute(
  909. """
  910. UPDATE entities SET is_archived = 1, updated_at = CURRENT_TIMESTAMP
  911. WHERE id = ?
  912. """,
  913. (entity_id,),
  914. )
  915. conn.commit()
  916. return cursor.rowcount > 0
  917. # ==================== v5.1 别名操作 ====================
  918. def register_alias(self, alias: str, entity_id: str, entity_type: str) -> bool:
  919. """
  920. 注册别名 (支持一对多)
  921. 同一别名可映射多个实体 (如 "天云宗" → 地点 + 势力)
  922. """
  923. with self._get_conn() as conn:
  924. cursor = conn.cursor()
  925. try:
  926. cursor.execute(
  927. """
  928. INSERT OR IGNORE INTO aliases (alias, entity_id, entity_type)
  929. VALUES (?, ?, ?)
  930. """,
  931. (alias, entity_id, entity_type),
  932. )
  933. conn.commit()
  934. return cursor.rowcount > 0
  935. except sqlite3.IntegrityError:
  936. return False
  937. def get_entities_by_alias(self, alias: str) -> List[Dict]:
  938. """
  939. 根据别名查找实体 (一对多)
  940. 返回所有匹配的实体 (可能有多个不同类型)
  941. """
  942. with self._get_conn() as conn:
  943. cursor = conn.cursor()
  944. cursor.execute(
  945. """
  946. SELECT e.*, a.entity_type as alias_type
  947. FROM entities e
  948. JOIN aliases a ON e.id = a.entity_id
  949. WHERE a.alias = ?
  950. """,
  951. (alias,),
  952. )
  953. return [
  954. self._row_to_dict(row, parse_json=["current_json"])
  955. for row in cursor.fetchall()
  956. ]
  957. def get_entity_aliases(self, entity_id: str) -> List[str]:
  958. """获取实体的所有别名"""
  959. with self._get_conn() as conn:
  960. cursor = conn.cursor()
  961. cursor.execute(
  962. "SELECT alias FROM aliases WHERE entity_id = ?", (entity_id,)
  963. )
  964. return [row["alias"] for row in cursor.fetchall()]
  965. def remove_alias(self, alias: str, entity_id: str) -> bool:
  966. """移除别名"""
  967. with self._get_conn() as conn:
  968. cursor = conn.cursor()
  969. cursor.execute(
  970. "DELETE FROM aliases WHERE alias = ? AND entity_id = ?",
  971. (alias, entity_id),
  972. )
  973. conn.commit()
  974. return cursor.rowcount > 0
  975. # ==================== v5.1 状态变化操作 ====================
  976. def record_state_change(self, change: StateChangeMeta) -> int:
  977. """
  978. 记录状态变化
  979. 返回记录 ID
  980. """
  981. with self._get_conn() as conn:
  982. cursor = conn.cursor()
  983. cursor.execute(
  984. """
  985. INSERT INTO state_changes
  986. (entity_id, field, old_value, new_value, reason, chapter)
  987. VALUES (?, ?, ?, ?, ?, ?)
  988. """,
  989. (
  990. change.entity_id,
  991. change.field,
  992. change.old_value,
  993. change.new_value,
  994. change.reason,
  995. change.chapter,
  996. ),
  997. )
  998. conn.commit()
  999. return cursor.lastrowid
  1000. def get_entity_state_changes(self, entity_id: str, limit: int = 20) -> List[Dict]:
  1001. """获取实体的状态变化历史"""
  1002. with self._get_conn() as conn:
  1003. cursor = conn.cursor()
  1004. cursor.execute(
  1005. """
  1006. SELECT * FROM state_changes
  1007. WHERE entity_id = ?
  1008. ORDER BY chapter DESC, id DESC
  1009. LIMIT ?
  1010. """,
  1011. (entity_id, limit),
  1012. )
  1013. return [dict(row) for row in cursor.fetchall()]
  1014. def get_recent_state_changes(self, limit: int = 50) -> List[Dict]:
  1015. """获取最近的状态变化"""
  1016. with self._get_conn() as conn:
  1017. cursor = conn.cursor()
  1018. cursor.execute(
  1019. """
  1020. SELECT * FROM state_changes
  1021. ORDER BY chapter DESC, id DESC
  1022. LIMIT ?
  1023. """,
  1024. (limit,),
  1025. )
  1026. return [dict(row) for row in cursor.fetchall()]
  1027. def get_chapter_state_changes(self, chapter: int) -> List[Dict]:
  1028. """获取某章的所有状态变化"""
  1029. with self._get_conn() as conn:
  1030. cursor = conn.cursor()
  1031. cursor.execute(
  1032. """
  1033. SELECT * FROM state_changes
  1034. WHERE chapter = ?
  1035. ORDER BY id
  1036. """,
  1037. (chapter,),
  1038. )
  1039. return [dict(row) for row in cursor.fetchall()]
  1040. # ==================== v5.1 关系操作 ====================
  1041. def upsert_relationship(self, rel: RelationshipMeta) -> bool:
  1042. """
  1043. 插入或更新关系
  1044. 相同 (from, to, type) 会更新 description 和 chapter
  1045. 返回是否为新关系
  1046. """
  1047. with self._get_conn() as conn:
  1048. cursor = conn.cursor()
  1049. # 检查是否存在
  1050. cursor.execute(
  1051. """
  1052. SELECT id FROM relationships
  1053. WHERE from_entity = ? AND to_entity = ? AND type = ?
  1054. """,
  1055. (rel.from_entity, rel.to_entity, rel.type),
  1056. )
  1057. existing = cursor.fetchone()
  1058. if existing:
  1059. cursor.execute(
  1060. """
  1061. UPDATE relationships SET
  1062. description = ?,
  1063. chapter = ?
  1064. WHERE id = ?
  1065. """,
  1066. (rel.description, rel.chapter, existing["id"]),
  1067. )
  1068. conn.commit()
  1069. return False
  1070. else:
  1071. cursor.execute(
  1072. """
  1073. INSERT INTO relationships
  1074. (from_entity, to_entity, type, description, chapter)
  1075. VALUES (?, ?, ?, ?, ?)
  1076. """,
  1077. (
  1078. rel.from_entity,
  1079. rel.to_entity,
  1080. rel.type,
  1081. rel.description,
  1082. rel.chapter,
  1083. ),
  1084. )
  1085. conn.commit()
  1086. return True
  1087. def get_entity_relationships(
  1088. self, entity_id: str, direction: str = "both"
  1089. ) -> List[Dict]:
  1090. """
  1091. 获取实体的关系
  1092. direction: "from" | "to" | "both"
  1093. """
  1094. with self._get_conn() as conn:
  1095. cursor = conn.cursor()
  1096. if direction == "from":
  1097. cursor.execute(
  1098. """
  1099. SELECT * FROM relationships WHERE from_entity = ?
  1100. ORDER BY chapter DESC
  1101. """,
  1102. (entity_id,),
  1103. )
  1104. elif direction == "to":
  1105. cursor.execute(
  1106. """
  1107. SELECT * FROM relationships WHERE to_entity = ?
  1108. ORDER BY chapter DESC
  1109. """,
  1110. (entity_id,),
  1111. )
  1112. else: # both
  1113. cursor.execute(
  1114. """
  1115. SELECT * FROM relationships
  1116. WHERE from_entity = ? OR to_entity = ?
  1117. ORDER BY chapter DESC
  1118. """,
  1119. (entity_id, entity_id),
  1120. )
  1121. return [dict(row) for row in cursor.fetchall()]
  1122. def get_relationship_between(self, entity1: str, entity2: str) -> List[Dict]:
  1123. """获取两个实体之间的所有关系"""
  1124. with self._get_conn() as conn:
  1125. cursor = conn.cursor()
  1126. cursor.execute(
  1127. """
  1128. SELECT * FROM relationships
  1129. WHERE (from_entity = ? AND to_entity = ?)
  1130. OR (from_entity = ? AND to_entity = ?)
  1131. ORDER BY chapter DESC
  1132. """,
  1133. (entity1, entity2, entity2, entity1),
  1134. )
  1135. return [dict(row) for row in cursor.fetchall()]
  1136. def get_recent_relationships(self, limit: int = 30) -> List[Dict]:
  1137. """获取最近建立的关系"""
  1138. with self._get_conn() as conn:
  1139. cursor = conn.cursor()
  1140. cursor.execute(
  1141. """
  1142. SELECT * FROM relationships
  1143. ORDER BY chapter DESC, id DESC
  1144. LIMIT ?
  1145. """,
  1146. (limit,),
  1147. )
  1148. return [dict(row) for row in cursor.fetchall()]
  1149. # ==================== v5.3 Override Contract 操作 ====================
  1150. def create_override_contract(self, contract: OverrideContractMeta) -> int:
  1151. """
  1152. 创建或更新 Override Contract
  1153. 使用 SQLite 的 INSERT ... ON CONFLICT ... DO UPDATE 实现原子 UPSERT:
  1154. - 并发安全,无需显式锁
  1155. - 保持 id 不变,避免 chase_debt.override_contract_id 悬挂
  1156. - 完全冻结终态:已 fulfilled/cancelled 的合约所有字段都不会被修改
  1157. 兼容性:支持 SQLite 3.24+(ON CONFLICT 语法),不依赖 RETURNING(3.35+)
  1158. 返回合约 ID
  1159. """
  1160. with self._get_conn() as conn:
  1161. cursor = conn.cursor()
  1162. # 使用 ON CONFLICT 实现原子 UPSERT(SQLite 3.24+)
  1163. # 终态完全冻结:fulfilled/cancelled 状态下所有字段都保持不变
  1164. cursor.execute(
  1165. """
  1166. INSERT INTO override_contracts
  1167. (chapter, constraint_type, constraint_id, rationale_type,
  1168. rationale_text, payback_plan, due_chapter, status)
  1169. VALUES (?, ?, ?, ?, ?, ?, ?, ?)
  1170. ON CONFLICT(chapter, constraint_type, constraint_id) DO UPDATE SET
  1171. rationale_type = CASE
  1172. WHEN override_contracts.status IN ('fulfilled', 'cancelled')
  1173. THEN override_contracts.rationale_type
  1174. ELSE excluded.rationale_type
  1175. END,
  1176. rationale_text = CASE
  1177. WHEN override_contracts.status IN ('fulfilled', 'cancelled')
  1178. THEN override_contracts.rationale_text
  1179. ELSE excluded.rationale_text
  1180. END,
  1181. payback_plan = CASE
  1182. WHEN override_contracts.status IN ('fulfilled', 'cancelled')
  1183. THEN override_contracts.payback_plan
  1184. ELSE excluded.payback_plan
  1185. END,
  1186. due_chapter = CASE
  1187. WHEN override_contracts.status IN ('fulfilled', 'cancelled')
  1188. THEN override_contracts.due_chapter
  1189. ELSE excluded.due_chapter
  1190. END,
  1191. status = CASE
  1192. WHEN override_contracts.status IN ('fulfilled', 'cancelled')
  1193. THEN override_contracts.status
  1194. ELSE excluded.status
  1195. END
  1196. """,
  1197. (
  1198. contract.chapter,
  1199. contract.constraint_type,
  1200. contract.constraint_id,
  1201. contract.rationale_type,
  1202. contract.rationale_text,
  1203. contract.payback_plan,
  1204. contract.due_chapter,
  1205. contract.status,
  1206. ),
  1207. )
  1208. # 不使用 RETURNING(需要 SQLite 3.35+),改用查询获取 id
  1209. cursor.execute(
  1210. """
  1211. SELECT id FROM override_contracts
  1212. WHERE chapter = ? AND constraint_type = ? AND constraint_id = ?
  1213. """,
  1214. (contract.chapter, contract.constraint_type, contract.constraint_id),
  1215. )
  1216. row = cursor.fetchone()
  1217. if not row:
  1218. # UPSERT 后查不到记录是异常情况,不应发生
  1219. raise RuntimeError(
  1220. f"Override Contract UPSERT 后无法获取 id: "
  1221. f"chapter={contract.chapter}, type={contract.constraint_type}, "
  1222. f"id={contract.constraint_id}"
  1223. )
  1224. contract_id = row[0]
  1225. conn.commit()
  1226. return contract_id
  1227. def get_pending_overrides(self, before_chapter: int = None) -> List[Dict]:
  1228. """获取待偿还的Override Contracts"""
  1229. with self._get_conn() as conn:
  1230. cursor = conn.cursor()
  1231. if before_chapter:
  1232. cursor.execute(
  1233. """
  1234. SELECT * FROM override_contracts
  1235. WHERE status = 'pending' AND due_chapter <= ?
  1236. ORDER BY due_chapter ASC
  1237. """,
  1238. (before_chapter,),
  1239. )
  1240. else:
  1241. cursor.execute("""
  1242. SELECT * FROM override_contracts
  1243. WHERE status = 'pending'
  1244. ORDER BY due_chapter ASC
  1245. """)
  1246. return [dict(row) for row in cursor.fetchall()]
  1247. def get_overdue_overrides(self, current_chapter: int) -> List[Dict]:
  1248. """获取已逾期的Override Contracts"""
  1249. with self._get_conn() as conn:
  1250. cursor = conn.cursor()
  1251. cursor.execute(
  1252. """
  1253. SELECT * FROM override_contracts
  1254. WHERE status = 'pending' AND due_chapter < ?
  1255. ORDER BY due_chapter ASC
  1256. """,
  1257. (current_chapter,),
  1258. )
  1259. return [dict(row) for row in cursor.fetchall()]
  1260. def fulfill_override(self, contract_id: int) -> bool:
  1261. """标记Override Contract为已偿还"""
  1262. with self._get_conn() as conn:
  1263. cursor = conn.cursor()
  1264. cursor.execute(
  1265. """
  1266. UPDATE override_contracts SET
  1267. status = 'fulfilled',
  1268. fulfilled_at = CURRENT_TIMESTAMP
  1269. WHERE id = ?
  1270. """,
  1271. (contract_id,),
  1272. )
  1273. conn.commit()
  1274. return cursor.rowcount > 0
  1275. def get_chapter_overrides(self, chapter: int) -> List[Dict]:
  1276. """获取某章创建的Override Contracts"""
  1277. with self._get_conn() as conn:
  1278. cursor = conn.cursor()
  1279. cursor.execute(
  1280. """
  1281. SELECT * FROM override_contracts WHERE chapter = ?
  1282. """,
  1283. (chapter,),
  1284. )
  1285. return [dict(row) for row in cursor.fetchall()]
  1286. # ==================== v5.3 追读力债务操作 ====================
  1287. def create_debt(self, debt: ChaseDebtMeta) -> int:
  1288. """
  1289. 创建追读力债务
  1290. 返回债务 ID
  1291. """
  1292. with self._get_conn() as conn:
  1293. cursor = conn.cursor()
  1294. cursor.execute(
  1295. """
  1296. INSERT INTO chase_debt
  1297. (debt_type, original_amount, current_amount, interest_rate,
  1298. source_chapter, due_chapter, override_contract_id, status)
  1299. VALUES (?, ?, ?, ?, ?, ?, ?, ?)
  1300. """,
  1301. (
  1302. debt.debt_type,
  1303. debt.original_amount,
  1304. debt.current_amount,
  1305. debt.interest_rate,
  1306. debt.source_chapter,
  1307. debt.due_chapter,
  1308. debt.override_contract_id if debt.override_contract_id else None,
  1309. debt.status,
  1310. ),
  1311. )
  1312. conn.commit()
  1313. debt_id = cursor.lastrowid
  1314. # 记录创建事件
  1315. self._record_debt_event(
  1316. cursor,
  1317. debt_id,
  1318. "created",
  1319. debt.original_amount,
  1320. debt.source_chapter,
  1321. f"创建债务: {debt.debt_type}",
  1322. )
  1323. conn.commit()
  1324. return debt_id
  1325. def get_active_debts(self) -> List[Dict]:
  1326. """获取所有活跃债务"""
  1327. with self._get_conn() as conn:
  1328. cursor = conn.cursor()
  1329. cursor.execute("""
  1330. SELECT * FROM chase_debt
  1331. WHERE status = 'active'
  1332. ORDER BY due_chapter ASC
  1333. """)
  1334. return [dict(row) for row in cursor.fetchall()]
  1335. def get_overdue_debts(self, current_chapter: int) -> List[Dict]:
  1336. """获取已逾期的债务(包括 active 但已过期的,以及已标记为 overdue 的)"""
  1337. with self._get_conn() as conn:
  1338. cursor = conn.cursor()
  1339. cursor.execute(
  1340. """
  1341. SELECT * FROM chase_debt
  1342. WHERE (status = 'overdue')
  1343. OR (status = 'active' AND due_chapter < ?)
  1344. ORDER BY due_chapter ASC
  1345. """,
  1346. (current_chapter,),
  1347. )
  1348. return [dict(row) for row in cursor.fetchall()]
  1349. def get_total_debt_balance(self) -> float:
  1350. """获取总债务余额(包括 active 和 overdue)"""
  1351. with self._get_conn() as conn:
  1352. cursor = conn.cursor()
  1353. cursor.execute("""
  1354. SELECT COALESCE(SUM(current_amount), 0) FROM chase_debt
  1355. WHERE status IN ('active', 'overdue')
  1356. """)
  1357. return cursor.fetchone()[0]
  1358. def accrue_interest(self, current_chapter: int) -> Dict[str, Any]:
  1359. """
  1360. 计算利息(每章调用一次)
  1361. - 对 active 和 overdue 债务都计息(逾期债务继续累积利息)
  1362. - 使用 debt_events 表防止同一章重复计息
  1363. - 检查逾期并更新状态
  1364. 返回: {debts_processed, total_interest, new_overdues, skipped_already_processed}
  1365. """
  1366. result = {
  1367. "debts_processed": 0,
  1368. "total_interest": 0.0,
  1369. "new_overdues": 0,
  1370. "skipped_already_processed": 0,
  1371. }
  1372. with self._get_conn() as conn:
  1373. cursor = conn.cursor()
  1374. # 获取所有未偿还债务(active + overdue 都继续计息)
  1375. cursor.execute("""
  1376. SELECT * FROM chase_debt WHERE status IN ('active', 'overdue')
  1377. """)
  1378. debts = cursor.fetchall()
  1379. for debt in debts:
  1380. debt_id = debt["id"]
  1381. current_amount = debt["current_amount"]
  1382. interest_rate = debt["interest_rate"]
  1383. due_chapter = debt["due_chapter"]
  1384. debt_status = debt["status"]
  1385. # 检查本章是否已计息(防止重复调用)
  1386. cursor.execute(
  1387. """
  1388. SELECT 1 FROM debt_events
  1389. WHERE debt_id = ? AND chapter = ? AND event_type = 'interest_accrued'
  1390. """,
  1391. (debt_id, current_chapter),
  1392. )
  1393. if cursor.fetchone():
  1394. result["skipped_already_processed"] += 1
  1395. continue
  1396. # 计算利息
  1397. interest = current_amount * interest_rate
  1398. new_amount = current_amount + interest
  1399. # 更新债务
  1400. cursor.execute(
  1401. """
  1402. UPDATE chase_debt SET
  1403. current_amount = ?,
  1404. updated_at = CURRENT_TIMESTAMP
  1405. WHERE id = ?
  1406. """,
  1407. (new_amount, debt_id),
  1408. )
  1409. # 记录利息事件
  1410. self._record_debt_event(
  1411. cursor,
  1412. debt_id,
  1413. "interest_accrued",
  1414. interest,
  1415. current_chapter,
  1416. f"利息: {interest:.2f} (利率: {interest_rate * 100:.0f}%)",
  1417. )
  1418. result["debts_processed"] += 1
  1419. result["total_interest"] += interest
  1420. # 检查是否逾期(仅对 active 状态的债务)
  1421. if debt_status == "active" and current_chapter > due_chapter:
  1422. cursor.execute(
  1423. """
  1424. UPDATE chase_debt SET status = 'overdue'
  1425. WHERE id = ? AND status = 'active'
  1426. """,
  1427. (debt_id,),
  1428. )
  1429. if cursor.rowcount > 0:
  1430. result["new_overdues"] += 1
  1431. self._record_debt_event(
  1432. cursor,
  1433. debt_id,
  1434. "overdue",
  1435. new_amount,
  1436. current_chapter,
  1437. f"债务逾期 (截止: 第{due_chapter}章)",
  1438. )
  1439. conn.commit()
  1440. return result
  1441. def pay_debt(self, debt_id: int, amount: float, chapter: int) -> Dict[str, Any]:
  1442. """
  1443. 偿还债务
  1444. - 校验 amount > 0
  1445. - 完全偿还时,使用原子 UPDATE 检查并标记关联 Override 为 fulfilled
  1446. (并发安全:用 NOT EXISTS 子查询确保所有债务都已清零)
  1447. 返回: {remaining, fully_paid, override_fulfilled}
  1448. """
  1449. # 校验偿还金额
  1450. if amount <= 0:
  1451. return {
  1452. "remaining": 0,
  1453. "fully_paid": False,
  1454. "error": "偿还金额必须大于0",
  1455. }
  1456. with self._get_conn() as conn:
  1457. cursor = conn.cursor()
  1458. cursor.execute(
  1459. "SELECT current_amount, override_contract_id FROM chase_debt WHERE id = ?",
  1460. (debt_id,),
  1461. )
  1462. row = cursor.fetchone()
  1463. if not row:
  1464. return {"remaining": 0, "fully_paid": False, "error": "债务不存在"}
  1465. current = row["current_amount"]
  1466. override_contract_id = row["override_contract_id"]
  1467. remaining = max(0, current - amount)
  1468. override_fulfilled = False
  1469. if remaining == 0:
  1470. # 完全偿还
  1471. cursor.execute(
  1472. """
  1473. UPDATE chase_debt SET
  1474. current_amount = 0,
  1475. status = 'paid',
  1476. updated_at = CURRENT_TIMESTAMP
  1477. WHERE id = ?
  1478. """,
  1479. (debt_id,),
  1480. )
  1481. self._record_debt_event(
  1482. cursor, debt_id, "full_payment", amount, chapter, "债务已完全偿还"
  1483. )
  1484. # 原子检查并标记 Override 为 fulfilled
  1485. # 使用 NOT EXISTS 子查询确保并发安全:只有当确实没有未清债务时才更新
  1486. if override_contract_id:
  1487. cursor.execute(
  1488. """
  1489. UPDATE override_contracts SET
  1490. status = 'fulfilled',
  1491. fulfilled_at = CURRENT_TIMESTAMP
  1492. WHERE id = ?
  1493. AND status = 'pending'
  1494. AND NOT EXISTS (
  1495. SELECT 1 FROM chase_debt
  1496. WHERE override_contract_id = ?
  1497. AND status IN ('active', 'overdue')
  1498. )
  1499. """,
  1500. (override_contract_id, override_contract_id),
  1501. )
  1502. if cursor.rowcount > 0:
  1503. override_fulfilled = True
  1504. else:
  1505. # 部分偿还
  1506. cursor.execute(
  1507. """
  1508. UPDATE chase_debt SET
  1509. current_amount = ?,
  1510. updated_at = CURRENT_TIMESTAMP
  1511. WHERE id = ?
  1512. """,
  1513. (remaining, debt_id),
  1514. )
  1515. self._record_debt_event(
  1516. cursor,
  1517. debt_id,
  1518. "partial_payment",
  1519. amount,
  1520. chapter,
  1521. f"部分偿还,剩余: {remaining:.2f}",
  1522. )
  1523. conn.commit()
  1524. return {
  1525. "remaining": remaining,
  1526. "fully_paid": remaining == 0,
  1527. "override_fulfilled": override_fulfilled,
  1528. }
  1529. def _record_debt_event(
  1530. self,
  1531. cursor,
  1532. debt_id: int,
  1533. event_type: str,
  1534. amount: float,
  1535. chapter: int,
  1536. note: str = "",
  1537. ):
  1538. """记录债务事件(内部方法)"""
  1539. cursor.execute(
  1540. """
  1541. INSERT INTO debt_events (debt_id, event_type, amount, chapter, note)
  1542. VALUES (?, ?, ?, ?, ?)
  1543. """,
  1544. (debt_id, event_type, amount, chapter, note),
  1545. )
  1546. def get_debt_history(self, debt_id: int) -> List[Dict]:
  1547. """获取债务的事件历史"""
  1548. with self._get_conn() as conn:
  1549. cursor = conn.cursor()
  1550. cursor.execute(
  1551. """
  1552. SELECT * FROM debt_events
  1553. WHERE debt_id = ?
  1554. ORDER BY created_at ASC
  1555. """,
  1556. (debt_id,),
  1557. )
  1558. return [dict(row) for row in cursor.fetchall()]
  1559. # ==================== v5.3 章节追读力元数据操作 ====================
  1560. def save_chapter_reading_power(self, meta: ChapterReadingPowerMeta):
  1561. """保存章节追读力元数据"""
  1562. with self._get_conn() as conn:
  1563. cursor = conn.cursor()
  1564. cursor.execute(
  1565. """
  1566. INSERT OR REPLACE INTO chapter_reading_power
  1567. (chapter, hook_type, hook_strength, coolpoint_patterns,
  1568. micropayoffs, hard_violations, soft_suggestions,
  1569. is_transition, override_count, debt_balance)
  1570. VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
  1571. """,
  1572. (
  1573. meta.chapter,
  1574. meta.hook_type,
  1575. meta.hook_strength,
  1576. json.dumps(meta.coolpoint_patterns, ensure_ascii=False),
  1577. json.dumps(meta.micropayoffs, ensure_ascii=False),
  1578. json.dumps(meta.hard_violations, ensure_ascii=False),
  1579. json.dumps(meta.soft_suggestions, ensure_ascii=False),
  1580. 1 if meta.is_transition else 0,
  1581. meta.override_count,
  1582. meta.debt_balance,
  1583. ),
  1584. )
  1585. conn.commit()
  1586. def get_chapter_reading_power(self, chapter: int) -> Optional[Dict]:
  1587. """获取章节追读力元数据"""
  1588. with self._get_conn() as conn:
  1589. cursor = conn.cursor()
  1590. cursor.execute(
  1591. "SELECT * FROM chapter_reading_power WHERE chapter = ?", (chapter,)
  1592. )
  1593. row = cursor.fetchone()
  1594. if row:
  1595. return self._row_to_dict(
  1596. row,
  1597. parse_json=[
  1598. "coolpoint_patterns",
  1599. "micropayoffs",
  1600. "hard_violations",
  1601. "soft_suggestions",
  1602. ],
  1603. )
  1604. return None
  1605. def get_recent_reading_power(self, limit: int = 10) -> List[Dict]:
  1606. """获取最近章节的追读力元数据"""
  1607. with self._get_conn() as conn:
  1608. cursor = conn.cursor()
  1609. cursor.execute(
  1610. """
  1611. SELECT * FROM chapter_reading_power
  1612. ORDER BY chapter DESC
  1613. LIMIT ?
  1614. """,
  1615. (limit,),
  1616. )
  1617. return [
  1618. self._row_to_dict(
  1619. row,
  1620. parse_json=[
  1621. "coolpoint_patterns",
  1622. "micropayoffs",
  1623. "hard_violations",
  1624. "soft_suggestions",
  1625. ],
  1626. )
  1627. for row in cursor.fetchall()
  1628. ]
  1629. def get_pattern_usage_stats(self, last_n_chapters: int = 20) -> Dict[str, int]:
  1630. """获取最近N章的爽点模式使用统计"""
  1631. with self._get_conn() as conn:
  1632. cursor = conn.cursor()
  1633. cursor.execute(
  1634. """
  1635. SELECT coolpoint_patterns FROM chapter_reading_power
  1636. ORDER BY chapter DESC
  1637. LIMIT ?
  1638. """,
  1639. (last_n_chapters,),
  1640. )
  1641. stats = {}
  1642. for row in cursor.fetchall():
  1643. if row["coolpoint_patterns"]:
  1644. try:
  1645. patterns = json.loads(row["coolpoint_patterns"])
  1646. for p in patterns:
  1647. stats[p] = stats.get(p, 0) + 1
  1648. except json.JSONDecodeError:
  1649. pass
  1650. return stats
  1651. def get_hook_type_stats(self, last_n_chapters: int = 20) -> Dict[str, int]:
  1652. """获取最近N章的钩子类型使用统计"""
  1653. with self._get_conn() as conn:
  1654. cursor = conn.cursor()
  1655. cursor.execute(
  1656. """
  1657. SELECT hook_type FROM chapter_reading_power
  1658. WHERE hook_type IS NOT NULL AND hook_type != ''
  1659. ORDER BY chapter DESC
  1660. LIMIT ?
  1661. """,
  1662. (last_n_chapters,),
  1663. )
  1664. stats = {}
  1665. for row in cursor.fetchall():
  1666. hook = row["hook_type"]
  1667. stats[hook] = stats.get(hook, 0) + 1
  1668. return stats
  1669. # ==================== v5.4 审查指标 ====================
  1670. def save_review_metrics(self, metrics: ReviewMetrics) -> None:
  1671. """保存审查指标记录"""
  1672. with self._get_conn() as conn:
  1673. cursor = conn.cursor()
  1674. cursor.execute(
  1675. """
  1676. INSERT INTO review_metrics
  1677. (start_chapter, end_chapter, overall_score, dimension_scores,
  1678. severity_counts, critical_issues, report_file, notes, created_at, updated_at)
  1679. VALUES (?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
  1680. ON CONFLICT(start_chapter, end_chapter)
  1681. DO UPDATE SET
  1682. overall_score = excluded.overall_score,
  1683. dimension_scores = excluded.dimension_scores,
  1684. severity_counts = excluded.severity_counts,
  1685. critical_issues = excluded.critical_issues,
  1686. report_file = excluded.report_file,
  1687. notes = excluded.notes,
  1688. updated_at = CURRENT_TIMESTAMP
  1689. """,
  1690. (
  1691. metrics.start_chapter,
  1692. metrics.end_chapter,
  1693. metrics.overall_score,
  1694. json.dumps(metrics.dimension_scores, ensure_ascii=False),
  1695. json.dumps(metrics.severity_counts, ensure_ascii=False),
  1696. json.dumps(metrics.critical_issues, ensure_ascii=False),
  1697. metrics.report_file,
  1698. metrics.notes,
  1699. ),
  1700. )
  1701. conn.commit()
  1702. def get_recent_review_metrics(self, limit: int = 5) -> List[Dict]:
  1703. """获取最近审查记录"""
  1704. with self._get_conn() as conn:
  1705. cursor = conn.cursor()
  1706. cursor.execute(
  1707. """
  1708. SELECT * FROM review_metrics
  1709. ORDER BY end_chapter DESC, start_chapter DESC
  1710. LIMIT ?
  1711. """,
  1712. (limit,),
  1713. )
  1714. return [
  1715. self._row_to_dict(
  1716. row,
  1717. parse_json=["dimension_scores", "severity_counts", "critical_issues"],
  1718. )
  1719. for row in cursor.fetchall()
  1720. ]
  1721. def get_review_trend_stats(self, last_n: int = 5) -> Dict[str, Any]:
  1722. """获取审查趋势统计"""
  1723. records = self.get_recent_review_metrics(last_n)
  1724. if not records:
  1725. return {
  1726. "count": 0,
  1727. "overall_avg": 0.0,
  1728. "dimension_avg": {},
  1729. "severity_totals": {},
  1730. "recent_ranges": [],
  1731. }
  1732. overall_scores: List[float] = []
  1733. dimension_totals: Dict[str, float] = {}
  1734. dimension_counts: Dict[str, int] = {}
  1735. severity_totals: Dict[str, int] = {}
  1736. for record in records:
  1737. score = record.get("overall_score")
  1738. if score is not None:
  1739. try:
  1740. overall_scores.append(float(score))
  1741. except (TypeError, ValueError):
  1742. pass
  1743. dimensions = record.get("dimension_scores") or {}
  1744. if isinstance(dimensions, dict):
  1745. for key, value in dimensions.items():
  1746. try:
  1747. val = float(value)
  1748. except (TypeError, ValueError):
  1749. continue
  1750. dimension_totals[key] = dimension_totals.get(key, 0.0) + val
  1751. dimension_counts[key] = dimension_counts.get(key, 0) + 1
  1752. severities = record.get("severity_counts") or {}
  1753. if isinstance(severities, dict):
  1754. for key, value in severities.items():
  1755. try:
  1756. count = int(value)
  1757. except (TypeError, ValueError):
  1758. continue
  1759. severity_totals[key] = severity_totals.get(key, 0) + count
  1760. overall_avg = round(sum(overall_scores) / len(overall_scores), 2) if overall_scores else 0.0
  1761. dimension_avg = {
  1762. key: round(dimension_totals[key] / dimension_counts[key], 2)
  1763. for key in dimension_totals
  1764. if dimension_counts.get(key, 0) > 0
  1765. }
  1766. recent_ranges = [
  1767. {
  1768. "start_chapter": record.get("start_chapter"),
  1769. "end_chapter": record.get("end_chapter"),
  1770. "overall_score": record.get("overall_score", 0),
  1771. }
  1772. for record in records
  1773. ]
  1774. return {
  1775. "count": len(records),
  1776. "overall_avg": overall_avg,
  1777. "dimension_avg": dimension_avg,
  1778. "severity_totals": severity_totals,
  1779. "recent_ranges": recent_ranges,
  1780. }
  1781. # ==================== 写作清单评分(Phase F) ====================
  1782. def save_writing_checklist_score(self, meta: WritingChecklistScoreMeta) -> None:
  1783. """保存章节写作清单评分。"""
  1784. with self._get_conn() as conn:
  1785. cursor = conn.cursor()
  1786. cursor.execute(
  1787. """
  1788. INSERT INTO writing_checklist_scores (
  1789. chapter, template, total_items, required_items,
  1790. completed_items, completed_required,
  1791. total_weight, completed_weight, completion_rate, score,
  1792. score_breakdown, pending_items, source, notes
  1793. ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
  1794. ON CONFLICT(chapter) DO UPDATE SET
  1795. template=excluded.template,
  1796. total_items=excluded.total_items,
  1797. required_items=excluded.required_items,
  1798. completed_items=excluded.completed_items,
  1799. completed_required=excluded.completed_required,
  1800. total_weight=excluded.total_weight,
  1801. completed_weight=excluded.completed_weight,
  1802. completion_rate=excluded.completion_rate,
  1803. score=excluded.score,
  1804. score_breakdown=excluded.score_breakdown,
  1805. pending_items=excluded.pending_items,
  1806. source=excluded.source,
  1807. notes=excluded.notes,
  1808. updated_at=CURRENT_TIMESTAMP
  1809. """,
  1810. (
  1811. meta.chapter,
  1812. meta.template,
  1813. meta.total_items,
  1814. meta.required_items,
  1815. meta.completed_items,
  1816. meta.completed_required,
  1817. meta.total_weight,
  1818. meta.completed_weight,
  1819. meta.completion_rate,
  1820. meta.score,
  1821. json.dumps(meta.score_breakdown, ensure_ascii=False),
  1822. json.dumps(meta.pending_items, ensure_ascii=False),
  1823. meta.source,
  1824. meta.notes,
  1825. ),
  1826. )
  1827. conn.commit()
  1828. def get_writing_checklist_score(self, chapter: int) -> Optional[Dict[str, Any]]:
  1829. """获取指定章节的写作清单评分。"""
  1830. with self._get_conn() as conn:
  1831. cursor = conn.cursor()
  1832. cursor.execute(
  1833. "SELECT * FROM writing_checklist_scores WHERE chapter = ?",
  1834. (chapter,),
  1835. )
  1836. row = cursor.fetchone()
  1837. if not row:
  1838. return None
  1839. return self._row_to_dict(row, parse_json=["score_breakdown", "pending_items"])
  1840. def get_recent_writing_checklist_scores(self, limit: int = 10) -> List[Dict[str, Any]]:
  1841. """获取最近章节写作清单评分。"""
  1842. with self._get_conn() as conn:
  1843. cursor = conn.cursor()
  1844. cursor.execute(
  1845. """
  1846. SELECT * FROM writing_checklist_scores
  1847. ORDER BY chapter DESC
  1848. LIMIT ?
  1849. """,
  1850. (limit,),
  1851. )
  1852. return [
  1853. self._row_to_dict(row, parse_json=["score_breakdown", "pending_items"])
  1854. for row in cursor.fetchall()
  1855. ]
  1856. def get_writing_checklist_score_trend(self, last_n: int = 10) -> Dict[str, Any]:
  1857. """获取写作清单评分趋势统计。"""
  1858. records = self.get_recent_writing_checklist_scores(limit=max(1, int(last_n)))
  1859. if not records:
  1860. return {
  1861. "count": 0,
  1862. "score_avg": 0.0,
  1863. "completion_avg": 0.0,
  1864. "required_completion_avg": 0.0,
  1865. "recent": [],
  1866. }
  1867. scores: List[float] = []
  1868. completion_rates: List[float] = []
  1869. required_rates: List[float] = []
  1870. for row in records:
  1871. try:
  1872. scores.append(float(row.get("score", 0.0)))
  1873. except (TypeError, ValueError):
  1874. pass
  1875. try:
  1876. completion_rates.append(float(row.get("completion_rate", 0.0)))
  1877. except (TypeError, ValueError):
  1878. pass
  1879. required_items = int(row.get("required_items") or 0)
  1880. completed_required = int(row.get("completed_required") or 0)
  1881. if required_items > 0:
  1882. required_rates.append(completed_required / required_items)
  1883. else:
  1884. required_rates.append(1.0)
  1885. return {
  1886. "count": len(records),
  1887. "score_avg": round(sum(scores) / len(scores), 2) if scores else 0.0,
  1888. "completion_avg": round(sum(completion_rates) / len(completion_rates), 4) if completion_rates else 0.0,
  1889. "required_completion_avg": round(sum(required_rates) / len(required_rates), 4) if required_rates else 0.0,
  1890. "recent": [
  1891. {
  1892. "chapter": row.get("chapter"),
  1893. "score": row.get("score"),
  1894. "completion_rate": row.get("completion_rate"),
  1895. }
  1896. for row in records
  1897. ],
  1898. }
  1899. def get_debt_summary(self) -> Dict[str, Any]:
  1900. """获取债务汇总信息"""
  1901. with self._get_conn() as conn:
  1902. cursor = conn.cursor()
  1903. # 活跃债务
  1904. cursor.execute("""
  1905. SELECT COUNT(*) as count, COALESCE(SUM(current_amount), 0) as total
  1906. FROM chase_debt WHERE status = 'active'
  1907. """)
  1908. active = cursor.fetchone()
  1909. # 逾期债务
  1910. cursor.execute("""
  1911. SELECT COUNT(*) as count, COALESCE(SUM(current_amount), 0) as total
  1912. FROM chase_debt WHERE status = 'overdue'
  1913. """)
  1914. overdue = cursor.fetchone()
  1915. # 待偿还Override
  1916. cursor.execute("""
  1917. SELECT COUNT(*) FROM override_contracts WHERE status = 'pending'
  1918. """)
  1919. pending_overrides = cursor.fetchone()[0]
  1920. return {
  1921. "active_debts": active["count"],
  1922. "active_total": active["total"],
  1923. "overdue_debts": overdue["count"],
  1924. "overdue_total": overdue["total"],
  1925. "pending_overrides": pending_overrides,
  1926. "total_balance": active["total"] + overdue["total"],
  1927. }
  1928. # ==================== 批量操作 ====================
  1929. def process_chapter_data(
  1930. self,
  1931. chapter: int,
  1932. title: str,
  1933. location: str,
  1934. word_count: int,
  1935. entities: List[Dict],
  1936. scenes: List[Dict],
  1937. ) -> Dict[str, int]:
  1938. """
  1939. 处理章节数据,批量写入索引
  1940. 返回写入统计
  1941. """
  1942. stats = {"chapters": 0, "scenes": 0, "appearances": 0}
  1943. # 提取出场角色
  1944. characters = [e.get("id") for e in entities if e.get("type") == "角色"]
  1945. # 写入章节元数据
  1946. self.add_chapter(
  1947. ChapterMeta(
  1948. chapter=chapter,
  1949. title=title,
  1950. location=location,
  1951. word_count=word_count,
  1952. characters=characters,
  1953. summary="", # 可后续由 Data Agent 生成
  1954. )
  1955. )
  1956. stats["chapters"] = 1
  1957. # 写入场景
  1958. scene_metas = []
  1959. for s in scenes:
  1960. scene_metas.append(
  1961. SceneMeta(
  1962. chapter=chapter,
  1963. scene_index=s.get("index", 0),
  1964. start_line=s.get("start_line", 0),
  1965. end_line=s.get("end_line", 0),
  1966. location=s.get("location", ""),
  1967. summary=s.get("summary", ""),
  1968. characters=s.get("characters", []),
  1969. )
  1970. )
  1971. self.add_scenes(chapter, scene_metas)
  1972. stats["scenes"] = len(scene_metas)
  1973. # 写入出场记录
  1974. for entity in entities:
  1975. entity_id = entity.get("id")
  1976. if entity_id and entity_id != "NEW":
  1977. self.record_appearance(
  1978. entity_id=entity_id,
  1979. chapter=chapter,
  1980. mentions=entity.get("mentions", []),
  1981. confidence=entity.get("confidence", 1.0),
  1982. )
  1983. stats["appearances"] += 1
  1984. return stats
  1985. # ==================== 辅助方法 ====================
  1986. def _row_to_dict(self, row: sqlite3.Row, parse_json: List[str] = None) -> Dict:
  1987. """将 Row 转换为字典"""
  1988. d = dict(row)
  1989. if parse_json:
  1990. for key in parse_json:
  1991. if key in d and d[key]:
  1992. try:
  1993. d[key] = json.loads(d[key])
  1994. except json.JSONDecodeError:
  1995. pass
  1996. return d
  1997. # ==================== 无效事实管理 ====================
  1998. def mark_invalid_fact(
  1999. self,
  2000. source_type: str,
  2001. source_id: str,
  2002. reason: str,
  2003. marked_by: str = "user",
  2004. chapter_discovered: Optional[int] = None,
  2005. ) -> int:
  2006. """标记无效事实(pending)"""
  2007. with self._get_conn() as conn:
  2008. cursor = conn.cursor()
  2009. cursor.execute(
  2010. """
  2011. INSERT INTO invalid_facts
  2012. (source_type, source_id, reason, status, marked_by, chapter_discovered)
  2013. VALUES (?, ?, ?, 'pending', ?, ?)
  2014. """,
  2015. (source_type, str(source_id), reason, marked_by, chapter_discovered),
  2016. )
  2017. conn.commit()
  2018. return int(cursor.lastrowid)
  2019. def resolve_invalid_fact(self, invalid_id: int, action: str) -> bool:
  2020. """确认或撤销无效标记"""
  2021. action = action.lower()
  2022. with self._get_conn() as conn:
  2023. cursor = conn.cursor()
  2024. if action == "confirm":
  2025. cursor.execute(
  2026. """
  2027. UPDATE invalid_facts
  2028. SET status = 'confirmed', confirmed_at = CURRENT_TIMESTAMP
  2029. WHERE id = ?
  2030. """,
  2031. (invalid_id,),
  2032. )
  2033. elif action == "dismiss":
  2034. cursor.execute("DELETE FROM invalid_facts WHERE id = ?", (invalid_id,))
  2035. else:
  2036. return False
  2037. conn.commit()
  2038. return cursor.rowcount > 0
  2039. def list_invalid_facts(self, status: Optional[str] = None) -> List[Dict]:
  2040. """列出无效事实"""
  2041. with self._get_conn() as conn:
  2042. cursor = conn.cursor()
  2043. if status:
  2044. cursor.execute(
  2045. "SELECT * FROM invalid_facts WHERE status = ? ORDER BY id DESC",
  2046. (status,),
  2047. )
  2048. else:
  2049. cursor.execute("SELECT * FROM invalid_facts ORDER BY id DESC")
  2050. return [dict(r) for r in cursor.fetchall()]
  2051. def get_invalid_ids(self, source_type: str, status: str = "confirmed") -> set[str]:
  2052. """获取无效事实 ID 集合"""
  2053. with self._get_conn() as conn:
  2054. cursor = conn.cursor()
  2055. cursor.execute(
  2056. "SELECT source_id FROM invalid_facts WHERE source_type = ? AND status = ?",
  2057. (source_type, status),
  2058. )
  2059. return {str(r[0]) for r in cursor.fetchall() if r and r[0] is not None}
  2060. # ==================== 日志记录 ====================
  2061. def log_rag_query(
  2062. self,
  2063. query: str,
  2064. query_type: str,
  2065. results_count: int,
  2066. hit_sources: Optional[str] = None,
  2067. latency_ms: Optional[int] = None,
  2068. chapter: Optional[int] = None,
  2069. ) -> None:
  2070. with self._get_conn() as conn:
  2071. cursor = conn.cursor()
  2072. cursor.execute(
  2073. """
  2074. INSERT INTO rag_query_log
  2075. (query, query_type, results_count, hit_sources, latency_ms, chapter)
  2076. VALUES (?, ?, ?, ?, ?, ?)
  2077. """,
  2078. (query, query_type, results_count, hit_sources, latency_ms, chapter),
  2079. )
  2080. conn.commit()
  2081. def log_tool_call(
  2082. self,
  2083. tool_name: str,
  2084. success: bool,
  2085. retry_count: int = 0,
  2086. error_code: Optional[str] = None,
  2087. error_message: Optional[str] = None,
  2088. chapter: Optional[int] = None,
  2089. ) -> None:
  2090. with self._get_conn() as conn:
  2091. cursor = conn.cursor()
  2092. cursor.execute(
  2093. """
  2094. INSERT INTO tool_call_stats
  2095. (tool_name, success, retry_count, error_code, error_message, chapter)
  2096. VALUES (?, ?, ?, ?, ?, ?)
  2097. """,
  2098. (tool_name, int(bool(success)), retry_count, error_code, error_message, chapter),
  2099. )
  2100. conn.commit()
  2101. def get_stats(self) -> Dict[str, int]:
  2102. """获取索引统计"""
  2103. with self._get_conn() as conn:
  2104. cursor = conn.cursor()
  2105. cursor.execute("SELECT COUNT(*) FROM chapters")
  2106. chapters = cursor.fetchone()[0]
  2107. cursor.execute("SELECT COUNT(*) FROM scenes")
  2108. scenes = cursor.fetchone()[0]
  2109. cursor.execute("SELECT COUNT(DISTINCT entity_id) FROM appearances")
  2110. appearances = cursor.fetchone()[0]
  2111. cursor.execute("SELECT MAX(chapter) FROM chapters")
  2112. max_chapter = cursor.fetchone()[0] or 0
  2113. # v5.1 引入统计
  2114. cursor.execute("SELECT COUNT(*) FROM entities")
  2115. entities = cursor.fetchone()[0]
  2116. cursor.execute("SELECT COUNT(*) FROM entities WHERE is_archived = 0")
  2117. active_entities = cursor.fetchone()[0]
  2118. cursor.execute("SELECT COUNT(*) FROM aliases")
  2119. aliases = cursor.fetchone()[0]
  2120. cursor.execute("SELECT COUNT(*) FROM state_changes")
  2121. state_changes = cursor.fetchone()[0]
  2122. cursor.execute("SELECT COUNT(*) FROM relationships")
  2123. relationships = cursor.fetchone()[0]
  2124. # v5.3 引入统计
  2125. cursor.execute("SELECT COUNT(*) FROM override_contracts")
  2126. override_contracts = cursor.fetchone()[0]
  2127. cursor.execute(
  2128. "SELECT COUNT(*) FROM override_contracts WHERE status = 'pending'"
  2129. )
  2130. pending_overrides = cursor.fetchone()[0]
  2131. cursor.execute("SELECT COUNT(*) FROM chase_debt WHERE status = 'active'")
  2132. active_debts = cursor.fetchone()[0]
  2133. cursor.execute(
  2134. "SELECT COALESCE(SUM(current_amount), 0) FROM chase_debt WHERE status IN ('active', 'overdue')"
  2135. )
  2136. total_debt = cursor.fetchone()[0]
  2137. cursor.execute("SELECT COUNT(*) FROM chapter_reading_power")
  2138. reading_power_records = cursor.fetchone()[0]
  2139. cursor.execute("SELECT COUNT(*) FROM review_metrics")
  2140. review_metrics = cursor.fetchone()[0]
  2141. return {
  2142. "chapters": chapters,
  2143. "scenes": scenes,
  2144. "appearances": appearances,
  2145. "max_chapter": max_chapter,
  2146. # v5.1 引入
  2147. "entities": entities,
  2148. "active_entities": active_entities,
  2149. "aliases": aliases,
  2150. "state_changes": state_changes,
  2151. "relationships": relationships,
  2152. # v5.3 引入
  2153. "override_contracts": override_contracts,
  2154. "pending_overrides": pending_overrides,
  2155. "active_debts": active_debts,
  2156. "total_debt": total_debt,
  2157. "reading_power_records": reading_power_records,
  2158. "review_metrics": review_metrics,
  2159. }
  2160. # ==================== CLI 接口 ====================
  2161. def main():
  2162. import argparse
  2163. from .cli_output import print_success, print_error
  2164. parser = argparse.ArgumentParser(description="Index Manager CLI (v5.4)")
  2165. parser.add_argument("--project-root", type=str, help="项目根目录")
  2166. subparsers = parser.add_subparsers(dest="command")
  2167. # 获取统计
  2168. subparsers.add_parser("stats")
  2169. # 查询章节
  2170. chapter_parser = subparsers.add_parser("get-chapter")
  2171. chapter_parser.add_argument("--chapter", type=int, required=True)
  2172. # 查询最近出场
  2173. recent_parser = subparsers.add_parser("recent-appearances")
  2174. recent_parser.add_argument("--limit", type=int, default=None)
  2175. # 查询实体出场
  2176. entity_parser = subparsers.add_parser("entity-appearances")
  2177. entity_parser.add_argument("--entity", required=True)
  2178. entity_parser.add_argument("--limit", type=int, default=None)
  2179. # 搜索场景
  2180. search_parser = subparsers.add_parser("search-scenes")
  2181. search_parser.add_argument("--location", required=True)
  2182. search_parser.add_argument("--limit", type=int, default=None)
  2183. # 处理章节数据 (写入)
  2184. process_parser = subparsers.add_parser("process-chapter")
  2185. process_parser.add_argument("--chapter", type=int, required=True)
  2186. process_parser.add_argument("--title", required=True)
  2187. process_parser.add_argument("--location", required=True)
  2188. process_parser.add_argument("--word-count", type=int, required=True)
  2189. process_parser.add_argument("--entities", required=True, help="JSON 格式的实体列表")
  2190. process_parser.add_argument("--scenes", required=True, help="JSON 格式的场景列表")
  2191. # ==================== v5.1 引入命令 ====================
  2192. # 获取实体
  2193. get_entity_parser = subparsers.add_parser("get-entity")
  2194. get_entity_parser.add_argument("--id", required=True, help="实体 ID")
  2195. # 获取核心实体
  2196. subparsers.add_parser("get-core-entities")
  2197. # 获取主角
  2198. subparsers.add_parser("get-protagonist")
  2199. # 按类型获取实体
  2200. type_parser = subparsers.add_parser("get-entities-by-type")
  2201. type_parser.add_argument(
  2202. "--type", required=True, help="实体类型 (角色/地点/物品/势力/招式)"
  2203. )
  2204. type_parser.add_argument("--include-archived", action="store_true")
  2205. # 按别名查找实体
  2206. alias_parser = subparsers.add_parser("get-by-alias")
  2207. alias_parser.add_argument("--alias", required=True, help="别名")
  2208. # 获取实体别名
  2209. aliases_parser = subparsers.add_parser("get-aliases")
  2210. aliases_parser.add_argument("--entity", required=True, help="实体 ID")
  2211. # 注册别名
  2212. reg_alias_parser = subparsers.add_parser("register-alias")
  2213. reg_alias_parser.add_argument("--alias", required=True)
  2214. reg_alias_parser.add_argument("--entity", required=True)
  2215. reg_alias_parser.add_argument("--type", required=True, help="实体类型")
  2216. # 获取实体关系
  2217. rel_parser = subparsers.add_parser("get-relationships")
  2218. rel_parser.add_argument("--entity", required=True)
  2219. rel_parser.add_argument(
  2220. "--direction", choices=["from", "to", "both"], default="both"
  2221. )
  2222. # 获取状态变化
  2223. changes_parser = subparsers.add_parser("get-state-changes")
  2224. changes_parser.add_argument("--entity", required=True)
  2225. changes_parser.add_argument("--limit", type=int, default=20)
  2226. # 写入实体
  2227. upsert_entity_parser = subparsers.add_parser("upsert-entity")
  2228. upsert_entity_parser.add_argument(
  2229. "--data", required=True, help="JSON 格式的实体数据"
  2230. )
  2231. # 写入关系
  2232. upsert_rel_parser = subparsers.add_parser("upsert-relationship")
  2233. upsert_rel_parser.add_argument("--data", required=True, help="JSON 格式的关系数据")
  2234. # 写入状态变化
  2235. state_change_parser = subparsers.add_parser("record-state-change")
  2236. state_change_parser.add_argument(
  2237. "--data", required=True, help="JSON 格式的状态变化数据"
  2238. )
  2239. # ==================== v5.4 新增命令 ====================
  2240. invalid_parser = subparsers.add_parser("mark-invalid")
  2241. invalid_parser.add_argument("--source-type", required=True)
  2242. invalid_parser.add_argument("--source-id", required=True)
  2243. invalid_parser.add_argument("--reason", required=True)
  2244. invalid_parser.add_argument("--marked-by", default="user")
  2245. invalid_parser.add_argument("--chapter", type=int, default=None)
  2246. resolve_parser = subparsers.add_parser("resolve-invalid")
  2247. resolve_parser.add_argument("--id", type=int, required=True)
  2248. resolve_parser.add_argument("--action", choices=["confirm", "dismiss"], required=True)
  2249. list_invalid_parser = subparsers.add_parser("list-invalid")
  2250. list_invalid_parser.add_argument("--status", choices=["pending", "confirmed"], default=None)
  2251. review_save_parser = subparsers.add_parser("save-review-metrics")
  2252. review_save_parser.add_argument("--data", required=True, help="JSON 格式的审查指标数据")
  2253. review_recent_parser = subparsers.add_parser("get-recent-review-metrics")
  2254. review_recent_parser.add_argument("--limit", type=int, default=5)
  2255. review_trend_parser = subparsers.add_parser("get-review-trend-stats")
  2256. review_trend_parser.add_argument("--last-n", type=int, default=5)
  2257. checklist_score_save_parser = subparsers.add_parser("save-writing-checklist-score")
  2258. checklist_score_save_parser.add_argument("--data", required=True, help="JSON 格式的写作清单评分数据")
  2259. checklist_score_get_parser = subparsers.add_parser("get-writing-checklist-score")
  2260. checklist_score_get_parser.add_argument("--chapter", type=int, required=True)
  2261. checklist_score_recent_parser = subparsers.add_parser("get-recent-writing-checklist-scores")
  2262. checklist_score_recent_parser.add_argument("--limit", type=int, default=10)
  2263. checklist_score_trend_parser = subparsers.add_parser("get-writing-checklist-score-trend")
  2264. checklist_score_trend_parser.add_argument("--last-n", type=int, default=10)
  2265. # ==================== v5.3 引入命令 ====================
  2266. # 获取债务汇总
  2267. subparsers.add_parser("get-debt-summary")
  2268. # 获取最近章节追读力元数据
  2269. reading_power_parser = subparsers.add_parser("get-recent-reading-power")
  2270. reading_power_parser.add_argument("--limit", type=int, default=10)
  2271. # 获取章节追读力元数据
  2272. chapter_rp_parser = subparsers.add_parser("get-chapter-reading-power")
  2273. chapter_rp_parser.add_argument("--chapter", type=int, required=True)
  2274. # 获取爽点模式使用统计
  2275. pattern_stats_parser = subparsers.add_parser("get-pattern-usage-stats")
  2276. pattern_stats_parser.add_argument("--last-n", type=int, default=20)
  2277. # 获取钩子类型使用统计
  2278. hook_stats_parser = subparsers.add_parser("get-hook-type-stats")
  2279. hook_stats_parser.add_argument("--last-n", type=int, default=20)
  2280. # 获取待偿还Override
  2281. pending_override_parser = subparsers.add_parser("get-pending-overrides")
  2282. pending_override_parser.add_argument("--before-chapter", type=int, default=None)
  2283. # 获取逾期Override
  2284. overdue_override_parser = subparsers.add_parser("get-overdue-overrides")
  2285. overdue_override_parser.add_argument("--current-chapter", type=int, required=True)
  2286. # 获取活跃债务
  2287. subparsers.add_parser("get-active-debts")
  2288. # 获取逾期债务
  2289. overdue_debt_parser = subparsers.add_parser("get-overdue-debts")
  2290. overdue_debt_parser.add_argument("--current-chapter", type=int, required=True)
  2291. # 计算利息
  2292. accrue_parser = subparsers.add_parser("accrue-interest")
  2293. accrue_parser.add_argument("--current-chapter", type=int, required=True)
  2294. # 偿还债务
  2295. pay_debt_parser = subparsers.add_parser("pay-debt")
  2296. pay_debt_parser.add_argument("--debt-id", type=int, required=True)
  2297. pay_debt_parser.add_argument("--amount", type=float, required=True)
  2298. pay_debt_parser.add_argument("--chapter", type=int, required=True)
  2299. # 创建Override Contract
  2300. create_override_parser = subparsers.add_parser("create-override-contract")
  2301. create_override_parser.add_argument(
  2302. "--data", required=True, help="JSON 格式的Override Contract数据"
  2303. )
  2304. # 创建债务
  2305. create_debt_parser = subparsers.add_parser("create-debt")
  2306. create_debt_parser.add_argument("--data", required=True, help="JSON 格式的债务数据")
  2307. # 标记Override已偿还
  2308. fulfill_override_parser = subparsers.add_parser("fulfill-override")
  2309. fulfill_override_parser.add_argument("--contract-id", type=int, required=True)
  2310. # 保存章节追读力元数据
  2311. save_rp_parser = subparsers.add_parser("save-chapter-reading-power")
  2312. save_rp_parser.add_argument(
  2313. "--data", required=True, help="JSON 格式的章节追读力元数据"
  2314. )
  2315. args = parser.parse_args()
  2316. # 初始化
  2317. config = None
  2318. if args.project_root:
  2319. from .config import DataModulesConfig
  2320. config = DataModulesConfig.from_project_root(args.project_root)
  2321. manager = IndexManager(config)
  2322. tool_name = f"index_manager:{args.command or 'unknown'}"
  2323. def emit_success(data=None, message: str = "ok", chapter: Optional[int] = None):
  2324. print_success(data, message=message)
  2325. try:
  2326. manager.log_tool_call(tool_name, True, chapter=chapter)
  2327. except Exception:
  2328. pass
  2329. def emit_error(code: str, message: str, suggestion: Optional[str] = None, chapter: Optional[int] = None):
  2330. print_error(code, message, suggestion=suggestion)
  2331. try:
  2332. manager.log_tool_call(tool_name, False, error_code=code, error_message=message, chapter=chapter)
  2333. except Exception:
  2334. pass
  2335. if args.command == "stats":
  2336. emit_success(manager.get_stats(), message="stats")
  2337. elif args.command == "get-chapter":
  2338. chapter = manager.get_chapter(args.chapter)
  2339. if chapter:
  2340. emit_success(chapter, message="chapter")
  2341. else:
  2342. emit_error("NOT_FOUND", f"未找到章节: {args.chapter}")
  2343. elif args.command == "recent-appearances":
  2344. appearances = manager.get_recent_appearances(args.limit)
  2345. emit_success(appearances, message="recent_appearances")
  2346. elif args.command == "entity-appearances":
  2347. appearances = manager.get_entity_appearances(args.entity, args.limit)
  2348. emit_success({"entity": args.entity, "appearances": appearances}, message="entity_appearances")
  2349. elif args.command == "search-scenes":
  2350. scenes = manager.search_scenes_by_location(args.location, args.limit)
  2351. emit_success(scenes, message="scenes")
  2352. elif args.command == "process-chapter":
  2353. entities = json.loads(args.entities)
  2354. scenes = json.loads(args.scenes)
  2355. stats = manager.process_chapter_data(
  2356. chapter=args.chapter,
  2357. title=args.title,
  2358. location=args.location,
  2359. word_count=args.word_count,
  2360. entities=entities,
  2361. scenes=scenes,
  2362. )
  2363. emit_success(stats, message="chapter_processed", chapter=args.chapter)
  2364. # ==================== v5.1 引入命令处理 ====================
  2365. elif args.command == "get-entity":
  2366. entity = manager.get_entity(args.id)
  2367. if entity:
  2368. emit_success(entity, message="entity")
  2369. else:
  2370. emit_error("NOT_FOUND", f"未找到实体: {args.id}")
  2371. elif args.command == "get-core-entities":
  2372. entities = manager.get_core_entities()
  2373. emit_success(entities, message="core_entities")
  2374. elif args.command == "get-protagonist":
  2375. protagonist = manager.get_protagonist()
  2376. if protagonist:
  2377. emit_success(protagonist, message="protagonist")
  2378. else:
  2379. emit_error("NOT_FOUND", "未设置主角")
  2380. elif args.command == "get-entities-by-type":
  2381. entities = manager.get_entities_by_type(args.type, args.include_archived)
  2382. emit_success(entities, message="entities_by_type")
  2383. elif args.command == "get-by-alias":
  2384. entities = manager.get_entities_by_alias(args.alias)
  2385. if entities:
  2386. emit_success(entities, message="entities_by_alias")
  2387. else:
  2388. emit_error("NOT_FOUND", f"未找到别名: {args.alias}")
  2389. elif args.command == "get-aliases":
  2390. aliases = manager.get_entity_aliases(args.entity)
  2391. if aliases:
  2392. emit_success({"entity": args.entity, "aliases": aliases}, message="aliases")
  2393. else:
  2394. emit_error("NOT_FOUND", f"{args.entity} 没有别名")
  2395. elif args.command == "register-alias":
  2396. success = manager.register_alias(args.alias, args.entity, args.type)
  2397. if success:
  2398. emit_success(
  2399. {"alias": args.alias, "entity": args.entity, "type": args.type},
  2400. message="alias_registered",
  2401. )
  2402. else:
  2403. emit_error("ALIAS_EXISTS", f"别名已存在或注册失败: {args.alias}")
  2404. elif args.command == "get-relationships":
  2405. rels = manager.get_entity_relationships(args.entity, args.direction)
  2406. emit_success(rels, message="relationships")
  2407. elif args.command == "get-state-changes":
  2408. changes = manager.get_entity_state_changes(args.entity, args.limit)
  2409. emit_success(changes, message="state_changes")
  2410. elif args.command == "upsert-entity":
  2411. data = json.loads(args.data)
  2412. entity = EntityMeta(
  2413. id=data["id"],
  2414. type=data["type"],
  2415. canonical_name=data["canonical_name"],
  2416. tier=data.get("tier", "装饰"),
  2417. desc=data.get("desc", ""),
  2418. current=data.get("current", {}),
  2419. first_appearance=data.get("first_appearance", 0),
  2420. last_appearance=data.get("last_appearance", 0),
  2421. is_protagonist=data.get("is_protagonist", False),
  2422. is_archived=data.get("is_archived", False),
  2423. )
  2424. is_new = manager.upsert_entity(entity)
  2425. emit_success({"id": entity.id, "created": is_new}, message="entity_upserted")
  2426. elif args.command == "upsert-relationship":
  2427. data = json.loads(args.data)
  2428. rel = RelationshipMeta(
  2429. from_entity=data["from_entity"],
  2430. to_entity=data["to_entity"],
  2431. type=data["type"],
  2432. description=data.get("description", ""),
  2433. chapter=data["chapter"],
  2434. )
  2435. is_new = manager.upsert_relationship(rel)
  2436. emit_success(
  2437. {"from": rel.from_entity, "to": rel.to_entity, "type": rel.type, "created": is_new},
  2438. message="relationship_upserted",
  2439. )
  2440. elif args.command == "record-state-change":
  2441. data = json.loads(args.data)
  2442. change = StateChangeMeta(
  2443. entity_id=data["entity_id"],
  2444. field=data["field"],
  2445. old_value=data.get("old_value", ""),
  2446. new_value=data["new_value"],
  2447. reason=data.get("reason", ""),
  2448. chapter=data["chapter"],
  2449. )
  2450. record_id = manager.record_state_change(change)
  2451. emit_success({"id": record_id, "entity": change.entity_id, "field": change.field}, message="state_change_recorded")
  2452. # ==================== v5.4 无效事实命令处理 ====================
  2453. elif args.command == "mark-invalid":
  2454. invalid_id = manager.mark_invalid_fact(
  2455. args.source_type,
  2456. args.source_id,
  2457. args.reason,
  2458. marked_by=args.marked_by,
  2459. chapter_discovered=args.chapter,
  2460. )
  2461. emit_success({"id": invalid_id}, message="invalid_marked")
  2462. elif args.command == "resolve-invalid":
  2463. ok = manager.resolve_invalid_fact(args.id, args.action)
  2464. if ok:
  2465. emit_success({"id": args.id, "action": args.action}, message="invalid_resolved")
  2466. else:
  2467. emit_error("INVALID_ACTION", f"无法处理 action: {args.action}")
  2468. elif args.command == "list-invalid":
  2469. rows = manager.list_invalid_facts(args.status)
  2470. emit_success(rows, message="invalid_list")
  2471. elif args.command == "save-review-metrics":
  2472. data = json.loads(args.data)
  2473. metrics = ReviewMetrics(
  2474. start_chapter=data["start_chapter"],
  2475. end_chapter=data["end_chapter"],
  2476. overall_score=data.get("overall_score", 0.0),
  2477. dimension_scores=data.get("dimension_scores", {}),
  2478. severity_counts=data.get("severity_counts", {}),
  2479. critical_issues=data.get("critical_issues", []),
  2480. report_file=data.get("report_file", ""),
  2481. notes=data.get("notes", ""),
  2482. )
  2483. manager.save_review_metrics(metrics)
  2484. emit_success(
  2485. {"start_chapter": metrics.start_chapter, "end_chapter": metrics.end_chapter},
  2486. message="review_metrics_saved",
  2487. )
  2488. elif args.command == "get-recent-review-metrics":
  2489. records = manager.get_recent_review_metrics(args.limit)
  2490. emit_success(records, message="recent_review_metrics")
  2491. elif args.command == "get-review-trend-stats":
  2492. stats = manager.get_review_trend_stats(args.last_n)
  2493. emit_success(stats, message="review_trend_stats")
  2494. elif args.command == "save-writing-checklist-score":
  2495. data = json.loads(args.data)
  2496. metrics = WritingChecklistScoreMeta(
  2497. chapter=data["chapter"],
  2498. template=data.get("template", "plot"),
  2499. total_items=data.get("total_items", 0),
  2500. required_items=data.get("required_items", 0),
  2501. completed_items=data.get("completed_items", 0),
  2502. completed_required=data.get("completed_required", 0),
  2503. total_weight=data.get("total_weight", 0.0),
  2504. completed_weight=data.get("completed_weight", 0.0),
  2505. completion_rate=data.get("completion_rate", 0.0),
  2506. score=data.get("score", 0.0),
  2507. score_breakdown=data.get("score_breakdown", {}),
  2508. pending_items=data.get("pending_items", []),
  2509. source=data.get("source", "context_manager"),
  2510. notes=data.get("notes", ""),
  2511. )
  2512. manager.save_writing_checklist_score(metrics)
  2513. emit_success({"chapter": metrics.chapter, "score": metrics.score}, message="writing_checklist_score_saved")
  2514. elif args.command == "get-writing-checklist-score":
  2515. score = manager.get_writing_checklist_score(args.chapter)
  2516. if score:
  2517. emit_success(score, message="writing_checklist_score")
  2518. else:
  2519. emit_error("NOT_FOUND", f"未找到第 {args.chapter} 章的写作清单评分")
  2520. elif args.command == "get-recent-writing-checklist-scores":
  2521. scores = manager.get_recent_writing_checklist_scores(args.limit)
  2522. emit_success(scores, message="recent_writing_checklist_scores")
  2523. elif args.command == "get-writing-checklist-score-trend":
  2524. trend = manager.get_writing_checklist_score_trend(args.last_n)
  2525. emit_success(trend, message="writing_checklist_score_trend")
  2526. # ==================== v5.3 引入命令处理 ====================
  2527. elif args.command == "get-debt-summary":
  2528. summary = manager.get_debt_summary()
  2529. emit_success(summary, message="debt_summary")
  2530. elif args.command == "get-recent-reading-power":
  2531. records = manager.get_recent_reading_power(args.limit)
  2532. emit_success(records, message="recent_reading_power")
  2533. elif args.command == "get-chapter-reading-power":
  2534. record = manager.get_chapter_reading_power(args.chapter)
  2535. if record:
  2536. emit_success(record, message="chapter_reading_power")
  2537. else:
  2538. emit_error("NOT_FOUND", f"未找到第 {args.chapter} 章的追读力元数据")
  2539. elif args.command == "get-pattern-usage-stats":
  2540. stats = manager.get_pattern_usage_stats(args.last_n)
  2541. emit_success(stats, message="pattern_usage_stats")
  2542. elif args.command == "get-hook-type-stats":
  2543. stats = manager.get_hook_type_stats(args.last_n)
  2544. emit_success(stats, message="hook_type_stats")
  2545. elif args.command == "get-pending-overrides":
  2546. overrides = manager.get_pending_overrides(args.before_chapter)
  2547. emit_success(overrides, message="pending_overrides")
  2548. elif args.command == "get-overdue-overrides":
  2549. overrides = manager.get_overdue_overrides(args.current_chapter)
  2550. emit_success(overrides, message="overdue_overrides")
  2551. elif args.command == "get-active-debts":
  2552. debts = manager.get_active_debts()
  2553. emit_success(debts, message="active_debts")
  2554. elif args.command == "get-overdue-debts":
  2555. debts = manager.get_overdue_debts(args.current_chapter)
  2556. emit_success(debts, message="overdue_debts")
  2557. elif args.command == "accrue-interest":
  2558. result = manager.accrue_interest(args.current_chapter)
  2559. emit_success(result, message="interest_accrued", chapter=args.current_chapter)
  2560. elif args.command == "pay-debt":
  2561. result = manager.pay_debt(args.debt_id, args.amount, args.chapter)
  2562. if "error" in result:
  2563. emit_error("PAY_DEBT_FAILED", result["error"], chapter=args.chapter)
  2564. else:
  2565. emit_success(result, message="debt_payment", chapter=args.chapter)
  2566. elif args.command == "create-override-contract":
  2567. data = json.loads(args.data)
  2568. contract = OverrideContractMeta(
  2569. chapter=data["chapter"],
  2570. constraint_type=data["constraint_type"],
  2571. constraint_id=data["constraint_id"],
  2572. rationale_type=data["rationale_type"],
  2573. rationale_text=data.get("rationale_text", ""),
  2574. payback_plan=data.get("payback_plan", ""),
  2575. due_chapter=data["due_chapter"],
  2576. status=data.get("status", "pending"),
  2577. )
  2578. contract_id = manager.create_override_contract(contract)
  2579. emit_success({"id": contract_id}, message="override_contract_created")
  2580. elif args.command == "create-debt":
  2581. data = json.loads(args.data)
  2582. debt = ChaseDebtMeta(
  2583. debt_type=data["debt_type"],
  2584. original_amount=data.get("original_amount", 1.0),
  2585. current_amount=data.get("current_amount", data.get("original_amount", 1.0)),
  2586. interest_rate=data.get("interest_rate", 0.1),
  2587. source_chapter=data["source_chapter"],
  2588. due_chapter=data["due_chapter"],
  2589. override_contract_id=data.get("override_contract_id", 0),
  2590. status=data.get("status", "active"),
  2591. )
  2592. debt_id = manager.create_debt(debt)
  2593. emit_success({"id": debt_id, "debt_type": debt.debt_type}, message="debt_created")
  2594. elif args.command == "fulfill-override":
  2595. success = manager.fulfill_override(args.contract_id)
  2596. if success:
  2597. emit_success({"id": args.contract_id}, message="override_fulfilled")
  2598. else:
  2599. emit_error("NOT_FOUND", f"未找到 Override Contract #{args.contract_id}")
  2600. elif args.command == "save-chapter-reading-power":
  2601. data = json.loads(args.data)
  2602. meta = ChapterReadingPowerMeta(
  2603. chapter=data["chapter"],
  2604. hook_type=data.get("hook_type", ""),
  2605. hook_strength=data.get("hook_strength", "medium"),
  2606. coolpoint_patterns=data.get("coolpoint_patterns", []),
  2607. micropayoffs=data.get("micropayoffs", []),
  2608. hard_violations=data.get("hard_violations", []),
  2609. soft_suggestions=data.get("soft_suggestions", []),
  2610. is_transition=data.get("is_transition", False),
  2611. override_count=data.get("override_count", 0),
  2612. debt_balance=data.get("debt_balance", 0.0),
  2613. )
  2614. manager.save_chapter_reading_power(meta)
  2615. emit_success({"chapter": meta.chapter}, message="reading_power_saved")
  2616. else:
  2617. emit_error("UNKNOWN_COMMAND", "未指定有效命令", suggestion="请查看 --help")
  2618. if __name__ == "__main__":
  2619. import sys
  2620. if sys.platform == "win32":
  2621. import io
  2622. sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8")
  2623. sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8")
  2624. main()