383 lines
14 KiB
Python
383 lines
14 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""
|
||
IndexReadingMixin extracted from IndexManager.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
import sys
|
||
from datetime import datetime
|
||
from typing import Any, Dict, List, Optional
|
||
|
||
|
||
class IndexReadingMixin:
|
||
def save_chapter_reading_power(self, meta: ChapterReadingPowerMeta):
|
||
"""保存章节追读力元数据"""
|
||
with self._get_conn() as conn:
|
||
cursor = conn.cursor()
|
||
cursor.execute(
|
||
"""
|
||
INSERT OR REPLACE INTO chapter_reading_power
|
||
(chapter, hook_type, hook_strength, coolpoint_patterns,
|
||
micropayoffs, hard_violations, soft_suggestions,
|
||
is_transition, override_count, debt_balance)
|
||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||
""",
|
||
(
|
||
meta.chapter,
|
||
meta.hook_type,
|
||
meta.hook_strength,
|
||
json.dumps(meta.coolpoint_patterns, ensure_ascii=False),
|
||
json.dumps(meta.micropayoffs, ensure_ascii=False),
|
||
json.dumps(meta.hard_violations, ensure_ascii=False),
|
||
json.dumps(meta.soft_suggestions, ensure_ascii=False),
|
||
1 if meta.is_transition else 0,
|
||
meta.override_count,
|
||
meta.debt_balance,
|
||
),
|
||
)
|
||
conn.commit()
|
||
|
||
def get_chapter_reading_power(self, chapter: int) -> Optional[Dict]:
|
||
"""获取章节追读力元数据"""
|
||
with self._get_conn() as conn:
|
||
cursor = conn.cursor()
|
||
cursor.execute(
|
||
"SELECT * FROM chapter_reading_power WHERE chapter = ?", (chapter,)
|
||
)
|
||
row = cursor.fetchone()
|
||
if row:
|
||
return self._row_to_dict(
|
||
row,
|
||
parse_json=[
|
||
"coolpoint_patterns",
|
||
"micropayoffs",
|
||
"hard_violations",
|
||
"soft_suggestions",
|
||
],
|
||
)
|
||
return None
|
||
|
||
def get_recent_reading_power(self, limit: int = 10) -> List[Dict]:
|
||
"""获取最近章节的追读力元数据"""
|
||
with self._get_conn() as conn:
|
||
cursor = conn.cursor()
|
||
cursor.execute(
|
||
"""
|
||
SELECT * FROM chapter_reading_power
|
||
ORDER BY chapter DESC
|
||
LIMIT ?
|
||
""",
|
||
(limit,),
|
||
)
|
||
return [
|
||
self._row_to_dict(
|
||
row,
|
||
parse_json=[
|
||
"coolpoint_patterns",
|
||
"micropayoffs",
|
||
"hard_violations",
|
||
"soft_suggestions",
|
||
],
|
||
)
|
||
for row in cursor.fetchall()
|
||
]
|
||
|
||
def get_pattern_usage_stats(self, last_n_chapters: int = 20) -> Dict[str, int]:
|
||
"""获取最近N章的爽点模式使用统计"""
|
||
with self._get_conn() as conn:
|
||
cursor = conn.cursor()
|
||
cursor.execute(
|
||
"""
|
||
SELECT coolpoint_patterns FROM chapter_reading_power
|
||
ORDER BY chapter DESC
|
||
LIMIT ?
|
||
""",
|
||
(last_n_chapters,),
|
||
)
|
||
|
||
stats = {}
|
||
for row in cursor.fetchall():
|
||
if row["coolpoint_patterns"]:
|
||
try:
|
||
patterns = json.loads(row["coolpoint_patterns"])
|
||
for p in patterns:
|
||
stats[p] = stats.get(p, 0) + 1
|
||
except json.JSONDecodeError as exc:
|
||
print(
|
||
f"[index_manager] failed to parse JSON in chapter_reading_power.coolpoint_patterns: {exc}",
|
||
file=sys.stderr,
|
||
)
|
||
return stats
|
||
|
||
def get_hook_type_stats(self, last_n_chapters: int = 20) -> Dict[str, int]:
|
||
"""获取最近N章的钩子类型使用统计"""
|
||
with self._get_conn() as conn:
|
||
cursor = conn.cursor()
|
||
cursor.execute(
|
||
"""
|
||
SELECT hook_type FROM chapter_reading_power
|
||
WHERE hook_type IS NOT NULL AND hook_type != ''
|
||
ORDER BY chapter DESC
|
||
LIMIT ?
|
||
""",
|
||
(last_n_chapters,),
|
||
)
|
||
|
||
stats = {}
|
||
for row in cursor.fetchall():
|
||
hook = row["hook_type"]
|
||
stats[hook] = stats.get(hook, 0) + 1
|
||
return stats
|
||
|
||
# ==================== v5.4 审查指标 ====================
|
||
|
||
def save_review_metrics(self, metrics: ReviewMetrics) -> None:
|
||
"""保存审查指标记录"""
|
||
with self._get_conn() as conn:
|
||
cursor = conn.cursor()
|
||
cursor.execute(
|
||
"""
|
||
INSERT INTO review_metrics
|
||
(start_chapter, end_chapter, overall_score, dimension_scores,
|
||
severity_counts, critical_issues, report_file, notes, created_at, updated_at)
|
||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
||
ON CONFLICT(start_chapter, end_chapter)
|
||
DO UPDATE SET
|
||
overall_score = excluded.overall_score,
|
||
dimension_scores = excluded.dimension_scores,
|
||
severity_counts = excluded.severity_counts,
|
||
critical_issues = excluded.critical_issues,
|
||
report_file = excluded.report_file,
|
||
notes = excluded.notes,
|
||
updated_at = CURRENT_TIMESTAMP
|
||
""",
|
||
(
|
||
metrics.start_chapter,
|
||
metrics.end_chapter,
|
||
metrics.overall_score,
|
||
json.dumps(metrics.dimension_scores, ensure_ascii=False),
|
||
json.dumps(metrics.severity_counts, ensure_ascii=False),
|
||
json.dumps(metrics.critical_issues, ensure_ascii=False),
|
||
metrics.report_file,
|
||
metrics.notes,
|
||
),
|
||
)
|
||
conn.commit()
|
||
|
||
def get_recent_review_metrics(self, limit: int = 5) -> List[Dict]:
|
||
"""获取最近审查记录"""
|
||
with self._get_conn() as conn:
|
||
cursor = conn.cursor()
|
||
cursor.execute(
|
||
"""
|
||
SELECT * FROM review_metrics
|
||
ORDER BY end_chapter DESC, start_chapter DESC
|
||
LIMIT ?
|
||
""",
|
||
(limit,),
|
||
)
|
||
return [
|
||
self._row_to_dict(
|
||
row,
|
||
parse_json=["dimension_scores", "severity_counts", "critical_issues"],
|
||
)
|
||
for row in cursor.fetchall()
|
||
]
|
||
|
||
def get_review_trend_stats(self, last_n: int = 5) -> Dict[str, Any]:
|
||
"""获取审查趋势统计"""
|
||
records = self.get_recent_review_metrics(last_n)
|
||
if not records:
|
||
return {
|
||
"count": 0,
|
||
"overall_avg": 0.0,
|
||
"dimension_avg": {},
|
||
"severity_totals": {},
|
||
"recent_ranges": [],
|
||
}
|
||
|
||
overall_scores: List[float] = []
|
||
dimension_totals: Dict[str, float] = {}
|
||
dimension_counts: Dict[str, int] = {}
|
||
severity_totals: Dict[str, int] = {}
|
||
|
||
for record in records:
|
||
score = record.get("overall_score")
|
||
if score is not None:
|
||
try:
|
||
overall_scores.append(float(score))
|
||
except (TypeError, ValueError):
|
||
pass
|
||
|
||
dimensions = record.get("dimension_scores") or {}
|
||
if isinstance(dimensions, dict):
|
||
for key, value in dimensions.items():
|
||
try:
|
||
val = float(value)
|
||
except (TypeError, ValueError):
|
||
continue
|
||
dimension_totals[key] = dimension_totals.get(key, 0.0) + val
|
||
dimension_counts[key] = dimension_counts.get(key, 0) + 1
|
||
|
||
severities = record.get("severity_counts") or {}
|
||
if isinstance(severities, dict):
|
||
for key, value in severities.items():
|
||
try:
|
||
count = int(value)
|
||
except (TypeError, ValueError):
|
||
continue
|
||
severity_totals[key] = severity_totals.get(key, 0) + count
|
||
|
||
overall_avg = round(sum(overall_scores) / len(overall_scores), 2) if overall_scores else 0.0
|
||
dimension_avg = {
|
||
key: round(dimension_totals[key] / dimension_counts[key], 2)
|
||
for key in dimension_totals
|
||
if dimension_counts.get(key, 0) > 0
|
||
}
|
||
recent_ranges = [
|
||
{
|
||
"start_chapter": record.get("start_chapter"),
|
||
"end_chapter": record.get("end_chapter"),
|
||
"overall_score": record.get("overall_score", 0),
|
||
}
|
||
for record in records
|
||
]
|
||
|
||
return {
|
||
"count": len(records),
|
||
"overall_avg": overall_avg,
|
||
"dimension_avg": dimension_avg,
|
||
"severity_totals": severity_totals,
|
||
"recent_ranges": recent_ranges,
|
||
}
|
||
|
||
# ==================== 写作清单评分(Phase F) ====================
|
||
|
||
def save_writing_checklist_score(self, meta: WritingChecklistScoreMeta) -> None:
|
||
"""保存章节写作清单评分。"""
|
||
with self._get_conn() as conn:
|
||
cursor = conn.cursor()
|
||
cursor.execute(
|
||
"""
|
||
INSERT INTO writing_checklist_scores (
|
||
chapter, template, total_items, required_items,
|
||
completed_items, completed_required,
|
||
total_weight, completed_weight, completion_rate, score,
|
||
score_breakdown, pending_items, source, notes
|
||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||
ON CONFLICT(chapter) DO UPDATE SET
|
||
template=excluded.template,
|
||
total_items=excluded.total_items,
|
||
required_items=excluded.required_items,
|
||
completed_items=excluded.completed_items,
|
||
completed_required=excluded.completed_required,
|
||
total_weight=excluded.total_weight,
|
||
completed_weight=excluded.completed_weight,
|
||
completion_rate=excluded.completion_rate,
|
||
score=excluded.score,
|
||
score_breakdown=excluded.score_breakdown,
|
||
pending_items=excluded.pending_items,
|
||
source=excluded.source,
|
||
notes=excluded.notes,
|
||
updated_at=CURRENT_TIMESTAMP
|
||
""",
|
||
(
|
||
meta.chapter,
|
||
meta.template,
|
||
meta.total_items,
|
||
meta.required_items,
|
||
meta.completed_items,
|
||
meta.completed_required,
|
||
meta.total_weight,
|
||
meta.completed_weight,
|
||
meta.completion_rate,
|
||
meta.score,
|
||
json.dumps(meta.score_breakdown, ensure_ascii=False),
|
||
json.dumps(meta.pending_items, ensure_ascii=False),
|
||
meta.source,
|
||
meta.notes,
|
||
),
|
||
)
|
||
conn.commit()
|
||
|
||
def get_writing_checklist_score(self, chapter: int) -> Optional[Dict[str, Any]]:
|
||
"""获取指定章节的写作清单评分。"""
|
||
with self._get_conn() as conn:
|
||
cursor = conn.cursor()
|
||
cursor.execute(
|
||
"SELECT * FROM writing_checklist_scores WHERE chapter = ?",
|
||
(chapter,),
|
||
)
|
||
row = cursor.fetchone()
|
||
if not row:
|
||
return None
|
||
return self._row_to_dict(row, parse_json=["score_breakdown", "pending_items"])
|
||
|
||
def get_recent_writing_checklist_scores(self, limit: int = 10) -> List[Dict[str, Any]]:
|
||
"""获取最近章节写作清单评分。"""
|
||
with self._get_conn() as conn:
|
||
cursor = conn.cursor()
|
||
cursor.execute(
|
||
"""
|
||
SELECT * FROM writing_checklist_scores
|
||
ORDER BY chapter DESC
|
||
LIMIT ?
|
||
""",
|
||
(limit,),
|
||
)
|
||
return [
|
||
self._row_to_dict(row, parse_json=["score_breakdown", "pending_items"])
|
||
for row in cursor.fetchall()
|
||
]
|
||
|
||
def get_writing_checklist_score_trend(self, last_n: int = 10) -> Dict[str, Any]:
|
||
"""获取写作清单评分趋势统计。"""
|
||
records = self.get_recent_writing_checklist_scores(limit=max(1, int(last_n)))
|
||
if not records:
|
||
return {
|
||
"count": 0,
|
||
"score_avg": 0.0,
|
||
"completion_avg": 0.0,
|
||
"required_completion_avg": 0.0,
|
||
"recent": [],
|
||
}
|
||
|
||
scores: List[float] = []
|
||
completion_rates: List[float] = []
|
||
required_rates: List[float] = []
|
||
for row in records:
|
||
try:
|
||
scores.append(float(row.get("score", 0.0)))
|
||
except (TypeError, ValueError):
|
||
pass
|
||
try:
|
||
completion_rates.append(float(row.get("completion_rate", 0.0)))
|
||
except (TypeError, ValueError):
|
||
pass
|
||
|
||
required_items = int(row.get("required_items") or 0)
|
||
completed_required = int(row.get("completed_required") or 0)
|
||
if required_items > 0:
|
||
required_rates.append(completed_required / required_items)
|
||
else:
|
||
required_rates.append(1.0)
|
||
|
||
return {
|
||
"count": len(records),
|
||
"score_avg": round(sum(scores) / len(scores), 2) if scores else 0.0,
|
||
"completion_avg": round(sum(completion_rates) / len(completion_rates), 4) if completion_rates else 0.0,
|
||
"required_completion_avg": round(sum(required_rates) / len(required_rates), 4) if required_rates else 0.0,
|
||
"recent": [
|
||
{
|
||
"chapter": row.get("chapter"),
|
||
"score": row.get("score"),
|
||
"completion_rate": row.get("completion_rate"),
|
||
}
|
||
for row in records
|
||
],
|
||
}
|
||
|