index_reading_mixin.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """
  4. IndexReadingMixin extracted from IndexManager.
  5. """
  6. from __future__ import annotations
  7. import json
  8. import sys
  9. from datetime import datetime
  10. from typing import Any, Dict, List, Optional
  11. class IndexReadingMixin:
  12. def save_chapter_reading_power(self, meta: ChapterReadingPowerMeta):
  13. """保存章节追读力元数据"""
  14. with self._get_conn() as conn:
  15. cursor = conn.cursor()
  16. cursor.execute(
  17. """
  18. INSERT OR REPLACE INTO chapter_reading_power
  19. (chapter, hook_type, hook_strength, coolpoint_patterns,
  20. micropayoffs, hard_violations, soft_suggestions,
  21. is_transition, override_count, debt_balance)
  22. VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
  23. """,
  24. (
  25. meta.chapter,
  26. meta.hook_type,
  27. meta.hook_strength,
  28. json.dumps(meta.coolpoint_patterns, ensure_ascii=False),
  29. json.dumps(meta.micropayoffs, ensure_ascii=False),
  30. json.dumps(meta.hard_violations, ensure_ascii=False),
  31. json.dumps(meta.soft_suggestions, ensure_ascii=False),
  32. 1 if meta.is_transition else 0,
  33. meta.override_count,
  34. meta.debt_balance,
  35. ),
  36. )
  37. conn.commit()
  38. def get_chapter_reading_power(self, chapter: int) -> Optional[Dict]:
  39. """获取章节追读力元数据"""
  40. with self._get_conn() as conn:
  41. cursor = conn.cursor()
  42. cursor.execute(
  43. "SELECT * FROM chapter_reading_power WHERE chapter = ?", (chapter,)
  44. )
  45. row = cursor.fetchone()
  46. if row:
  47. return self._row_to_dict(
  48. row,
  49. parse_json=[
  50. "coolpoint_patterns",
  51. "micropayoffs",
  52. "hard_violations",
  53. "soft_suggestions",
  54. ],
  55. )
  56. return None
  57. def get_recent_reading_power(self, limit: int = 10) -> List[Dict]:
  58. """获取最近章节的追读力元数据"""
  59. with self._get_conn() as conn:
  60. cursor = conn.cursor()
  61. cursor.execute(
  62. """
  63. SELECT * FROM chapter_reading_power
  64. ORDER BY chapter DESC
  65. LIMIT ?
  66. """,
  67. (limit,),
  68. )
  69. return [
  70. self._row_to_dict(
  71. row,
  72. parse_json=[
  73. "coolpoint_patterns",
  74. "micropayoffs",
  75. "hard_violations",
  76. "soft_suggestions",
  77. ],
  78. )
  79. for row in cursor.fetchall()
  80. ]
  81. def get_pattern_usage_stats(self, last_n_chapters: int = 20) -> Dict[str, int]:
  82. """获取最近N章的爽点模式使用统计"""
  83. with self._get_conn() as conn:
  84. cursor = conn.cursor()
  85. cursor.execute(
  86. """
  87. SELECT coolpoint_patterns FROM chapter_reading_power
  88. ORDER BY chapter DESC
  89. LIMIT ?
  90. """,
  91. (last_n_chapters,),
  92. )
  93. stats = {}
  94. for row in cursor.fetchall():
  95. if row["coolpoint_patterns"]:
  96. try:
  97. patterns = json.loads(row["coolpoint_patterns"])
  98. for p in patterns:
  99. stats[p] = stats.get(p, 0) + 1
  100. except json.JSONDecodeError as exc:
  101. print(
  102. f"[index_manager] failed to parse JSON in chapter_reading_power.coolpoint_patterns: {exc}",
  103. file=sys.stderr,
  104. )
  105. return stats
  106. def get_hook_type_stats(self, last_n_chapters: int = 20) -> Dict[str, int]:
  107. """获取最近N章的钩子类型使用统计"""
  108. with self._get_conn() as conn:
  109. cursor = conn.cursor()
  110. cursor.execute(
  111. """
  112. SELECT hook_type FROM chapter_reading_power
  113. WHERE hook_type IS NOT NULL AND hook_type != ''
  114. ORDER BY chapter DESC
  115. LIMIT ?
  116. """,
  117. (last_n_chapters,),
  118. )
  119. stats = {}
  120. for row in cursor.fetchall():
  121. hook = row["hook_type"]
  122. stats[hook] = stats.get(hook, 0) + 1
  123. return stats
  124. # ==================== v5.4 审查指标 ====================
  125. def save_review_metrics(self, metrics: ReviewMetrics) -> None:
  126. """保存审查指标记录"""
  127. with self._get_conn() as conn:
  128. cursor = conn.cursor()
  129. cursor.execute(
  130. """
  131. INSERT INTO review_metrics
  132. (start_chapter, end_chapter, overall_score, dimension_scores,
  133. severity_counts, critical_issues, report_file, notes, created_at, updated_at)
  134. VALUES (?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
  135. ON CONFLICT(start_chapter, end_chapter)
  136. DO UPDATE SET
  137. overall_score = excluded.overall_score,
  138. dimension_scores = excluded.dimension_scores,
  139. severity_counts = excluded.severity_counts,
  140. critical_issues = excluded.critical_issues,
  141. report_file = excluded.report_file,
  142. notes = excluded.notes,
  143. updated_at = CURRENT_TIMESTAMP
  144. """,
  145. (
  146. metrics.start_chapter,
  147. metrics.end_chapter,
  148. metrics.overall_score,
  149. json.dumps(metrics.dimension_scores, ensure_ascii=False),
  150. json.dumps(metrics.severity_counts, ensure_ascii=False),
  151. json.dumps(metrics.critical_issues, ensure_ascii=False),
  152. metrics.report_file,
  153. metrics.notes,
  154. ),
  155. )
  156. conn.commit()
  157. def get_recent_review_metrics(self, limit: int = 5) -> List[Dict]:
  158. """获取最近审查记录"""
  159. with self._get_conn() as conn:
  160. cursor = conn.cursor()
  161. cursor.execute(
  162. """
  163. SELECT * FROM review_metrics
  164. ORDER BY end_chapter DESC, start_chapter DESC
  165. LIMIT ?
  166. """,
  167. (limit,),
  168. )
  169. return [
  170. self._row_to_dict(
  171. row,
  172. parse_json=["dimension_scores", "severity_counts", "critical_issues"],
  173. )
  174. for row in cursor.fetchall()
  175. ]
  176. def get_review_trend_stats(self, last_n: int = 5) -> Dict[str, Any]:
  177. """获取审查趋势统计"""
  178. records = self.get_recent_review_metrics(last_n)
  179. if not records:
  180. return {
  181. "count": 0,
  182. "overall_avg": 0.0,
  183. "dimension_avg": {},
  184. "severity_totals": {},
  185. "recent_ranges": [],
  186. }
  187. overall_scores: List[float] = []
  188. dimension_totals: Dict[str, float] = {}
  189. dimension_counts: Dict[str, int] = {}
  190. severity_totals: Dict[str, int] = {}
  191. for record in records:
  192. score = record.get("overall_score")
  193. if score is not None:
  194. try:
  195. overall_scores.append(float(score))
  196. except (TypeError, ValueError):
  197. pass
  198. dimensions = record.get("dimension_scores") or {}
  199. if isinstance(dimensions, dict):
  200. for key, value in dimensions.items():
  201. try:
  202. val = float(value)
  203. except (TypeError, ValueError):
  204. continue
  205. dimension_totals[key] = dimension_totals.get(key, 0.0) + val
  206. dimension_counts[key] = dimension_counts.get(key, 0) + 1
  207. severities = record.get("severity_counts") or {}
  208. if isinstance(severities, dict):
  209. for key, value in severities.items():
  210. try:
  211. count = int(value)
  212. except (TypeError, ValueError):
  213. continue
  214. severity_totals[key] = severity_totals.get(key, 0) + count
  215. overall_avg = round(sum(overall_scores) / len(overall_scores), 2) if overall_scores else 0.0
  216. dimension_avg = {
  217. key: round(dimension_totals[key] / dimension_counts[key], 2)
  218. for key in dimension_totals
  219. if dimension_counts.get(key, 0) > 0
  220. }
  221. recent_ranges = [
  222. {
  223. "start_chapter": record.get("start_chapter"),
  224. "end_chapter": record.get("end_chapter"),
  225. "overall_score": record.get("overall_score", 0),
  226. }
  227. for record in records
  228. ]
  229. return {
  230. "count": len(records),
  231. "overall_avg": overall_avg,
  232. "dimension_avg": dimension_avg,
  233. "severity_totals": severity_totals,
  234. "recent_ranges": recent_ranges,
  235. }
  236. # ==================== 写作清单评分(Phase F) ====================
  237. def save_writing_checklist_score(self, meta: WritingChecklistScoreMeta) -> None:
  238. """保存章节写作清单评分。"""
  239. with self._get_conn() as conn:
  240. cursor = conn.cursor()
  241. cursor.execute(
  242. """
  243. INSERT INTO writing_checklist_scores (
  244. chapter, template, total_items, required_items,
  245. completed_items, completed_required,
  246. total_weight, completed_weight, completion_rate, score,
  247. score_breakdown, pending_items, source, notes
  248. ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
  249. ON CONFLICT(chapter) DO UPDATE SET
  250. template=excluded.template,
  251. total_items=excluded.total_items,
  252. required_items=excluded.required_items,
  253. completed_items=excluded.completed_items,
  254. completed_required=excluded.completed_required,
  255. total_weight=excluded.total_weight,
  256. completed_weight=excluded.completed_weight,
  257. completion_rate=excluded.completion_rate,
  258. score=excluded.score,
  259. score_breakdown=excluded.score_breakdown,
  260. pending_items=excluded.pending_items,
  261. source=excluded.source,
  262. notes=excluded.notes,
  263. updated_at=CURRENT_TIMESTAMP
  264. """,
  265. (
  266. meta.chapter,
  267. meta.template,
  268. meta.total_items,
  269. meta.required_items,
  270. meta.completed_items,
  271. meta.completed_required,
  272. meta.total_weight,
  273. meta.completed_weight,
  274. meta.completion_rate,
  275. meta.score,
  276. json.dumps(meta.score_breakdown, ensure_ascii=False),
  277. json.dumps(meta.pending_items, ensure_ascii=False),
  278. meta.source,
  279. meta.notes,
  280. ),
  281. )
  282. conn.commit()
  283. def get_writing_checklist_score(self, chapter: int) -> Optional[Dict[str, Any]]:
  284. """获取指定章节的写作清单评分。"""
  285. with self._get_conn() as conn:
  286. cursor = conn.cursor()
  287. cursor.execute(
  288. "SELECT * FROM writing_checklist_scores WHERE chapter = ?",
  289. (chapter,),
  290. )
  291. row = cursor.fetchone()
  292. if not row:
  293. return None
  294. return self._row_to_dict(row, parse_json=["score_breakdown", "pending_items"])
  295. def get_recent_writing_checklist_scores(self, limit: int = 10) -> List[Dict[str, Any]]:
  296. """获取最近章节写作清单评分。"""
  297. with self._get_conn() as conn:
  298. cursor = conn.cursor()
  299. cursor.execute(
  300. """
  301. SELECT * FROM writing_checklist_scores
  302. ORDER BY chapter DESC
  303. LIMIT ?
  304. """,
  305. (limit,),
  306. )
  307. return [
  308. self._row_to_dict(row, parse_json=["score_breakdown", "pending_items"])
  309. for row in cursor.fetchall()
  310. ]
  311. def get_writing_checklist_score_trend(self, last_n: int = 10) -> Dict[str, Any]:
  312. """获取写作清单评分趋势统计。"""
  313. records = self.get_recent_writing_checklist_scores(limit=max(1, int(last_n)))
  314. if not records:
  315. return {
  316. "count": 0,
  317. "score_avg": 0.0,
  318. "completion_avg": 0.0,
  319. "required_completion_avg": 0.0,
  320. "recent": [],
  321. }
  322. scores: List[float] = []
  323. completion_rates: List[float] = []
  324. required_rates: List[float] = []
  325. for row in records:
  326. try:
  327. scores.append(float(row.get("score", 0.0)))
  328. except (TypeError, ValueError):
  329. pass
  330. try:
  331. completion_rates.append(float(row.get("completion_rate", 0.0)))
  332. except (TypeError, ValueError):
  333. pass
  334. required_items = int(row.get("required_items") or 0)
  335. completed_required = int(row.get("completed_required") or 0)
  336. if required_items > 0:
  337. required_rates.append(completed_required / required_items)
  338. else:
  339. required_rates.append(1.0)
  340. return {
  341. "count": len(records),
  342. "score_avg": round(sum(scores) / len(scores), 2) if scores else 0.0,
  343. "completion_avg": round(sum(completion_rates) / len(completion_rates), 4) if completion_rates else 0.0,
  344. "required_completion_avg": round(sum(required_rates) / len(required_rates), 4) if required_rates else 0.0,
  345. "recent": [
  346. {
  347. "chapter": row.get("chapter"),
  348. "score": row.get("score"),
  349. "completion_rate": row.get("completion_rate"),
  350. }
  351. for row in records
  352. ],
  353. }