Kaynağa Gözat

fix: 保存审查报告并修复审查指标落库

lingfengQAQ 1 ay önce
ebeveyn
işleme
fd85cac7da

+ 19 - 0
webnovel-writer/scripts/data_modules/tests/test_webnovel_unified_cli.py

@@ -408,6 +408,7 @@ def test_review_pipeline_forwards_with_resolved_project_root(monkeypatch, tmp_pa
             str(tmp_path / "metrics.json"),
             "--report-file",
             "审查报告/第18章.md",
+            "--save-metrics",
         ],
     )
 
@@ -427,6 +428,7 @@ def test_review_pipeline_forwards_with_resolved_project_root(monkeypatch, tmp_pa
         str(tmp_path / "metrics.json"),
         "--report-file",
         "审查报告/第18章.md",
+        "--save-metrics",
     ]
 
 
@@ -458,6 +460,7 @@ def test_review_pipeline_main_creates_output_directories(tmp_path):
     )
 
     metrics_out = project_root / ".webnovel" / "tmp" / "review" / "metrics.json"
+    report_file = project_root / "审查报告" / "第9章审查报告.md"
 
     old_argv = sys.argv
     sys.argv = [
@@ -470,6 +473,9 @@ def test_review_pipeline_main_creates_output_directories(tmp_path):
         str(review_results_path),
         "--metrics-out",
         str(metrics_out),
+        "--report-file",
+        "审查报告/第9章审查报告.md",
+        "--save-metrics",
     ]
     try:
         review_pipeline_module.main()
@@ -477,6 +483,19 @@ def test_review_pipeline_main_creates_output_directories(tmp_path):
         sys.argv = old_argv
 
     assert metrics_out.is_file()
+    assert report_file.is_file()
+    report_text = report_file.read_text(encoding="utf-8")
+    assert "# 第9章审查报告" in report_text
+    assert "小问题" in report_text
+    assert "## 其他问题" in report_text
+
+    import sqlite3
+
+    with sqlite3.connect(project_root / ".webnovel" / "index.db") as conn:
+        row = conn.execute(
+            "SELECT start_chapter, end_chapter, report_file FROM review_metrics"
+        ).fetchone()
+    assert row == (9, 9, "审查报告/第9章审查报告.md")
 
 
 def test_webnovel_skill_flow_runs_story_contract_context_and_review_pipeline_with_stubbed_vector_model(

+ 3 - 0
webnovel-writer/scripts/data_modules/webnovel.py

@@ -289,6 +289,7 @@ def main() -> None:
     p_review_pipeline.add_argument("--review-results", required=True, help="reviewer 原始结果 JSON 文件")
     p_review_pipeline.add_argument("--metrics-out", default="", help="metrics 输出文件")
     p_review_pipeline.add_argument("--report-file", default="", help="审查报告路径")
+    p_review_pipeline.add_argument("--save-metrics", action="store_true", help="直接写入 index.db")
 
     knowledge_parser = sub.add_parser("knowledge", help="时序知识查询")
     knowledge_sub = knowledge_parser.add_subparsers(dest="knowledge_action")
@@ -387,6 +388,8 @@ def main() -> None:
             return_args.extend(["--metrics-out", str(args.metrics_out)])
         if args.report_file:
             return_args.extend(["--report-file", str(args.report_file)])
+        if args.save_metrics:
+            return_args.append("--save-metrics")
         raise SystemExit(_run_script("review_pipeline.py", return_args))
 
     if tool == "knowledge":

+ 122 - 2
webnovel-writer/scripts/review_pipeline.py

@@ -12,7 +12,7 @@ import argparse
 import json
 import sys
 from pathlib import Path
-from typing import Any, Dict
+from typing import Any, Dict, List
 
 from runtime_compat import enable_windows_utf8_stdio
 
@@ -28,6 +28,119 @@ _ensure_scripts_path()
 from data_modules.review_schema import parse_review_output
 
 
+def _resolve_report_path(project_root: Path, report_file: str) -> Path:
+    root = project_root.expanduser().resolve()
+    report_path = Path(report_file).expanduser()
+    if not report_path.is_absolute():
+        report_path = root / report_path
+    report_path = report_path.resolve()
+    try:
+        report_path.relative_to(root)
+    except ValueError as exc:
+        raise ValueError("report-file 必须位于 project_root 目录内") from exc
+    return report_path
+
+
+def _format_issue(issue: Dict[str, Any], index: int) -> List[str]:
+    description = str(issue.get("description") or "未填写问题描述")
+    severity = str(issue.get("severity") or "medium")
+    category = str(issue.get("category") or "other")
+    location = str(issue.get("location") or "未标注位置")
+    evidence = str(issue.get("evidence") or "未提供证据")
+    fix_hint = str(issue.get("fix_hint") or "未提供修复方向")
+    blocking = "是" if issue.get("blocking") else "否"
+
+    return [
+        f"{index}. **{description}**",
+        f"   - 严重级别:{severity}",
+        f"   - 分类:{category}",
+        f"   - 位置:{location}",
+        f"   - 阻断:{blocking}",
+        f"   - 证据:{evidence}",
+        f"   - 修复方向:{fix_hint}",
+    ]
+
+
+def render_review_report(payload: Dict[str, Any]) -> str:
+    result = payload["review_result"]
+    metrics = payload["metrics"]
+    issues = list(result.get("issues", []))
+    blocking_issues = [issue for issue in issues if issue.get("blocking")]
+    non_blocking_issues = [issue for issue in issues if not issue.get("blocking")]
+    severity_counts = metrics.get("severity_counts", {})
+
+    lines: List[str] = [
+        f"# 第{payload['chapter']}章审查报告",
+        "",
+        "## 总览",
+        "",
+        f"- 问题数:{result.get('issues_count', 0)}",
+        f"- 阻断数:{result.get('blocking_count', 0)}",
+        f"- 结论:{'需修复后重审' if result.get('has_blocking') else '无阻断问题'}",
+    ]
+    summary = str(result.get("summary") or "").strip()
+    if summary:
+        lines.append(f"- 摘要:{summary}")
+    if severity_counts:
+        ordered = [
+            f"{level}={severity_counts.get(level, 0)}"
+            for level in ("critical", "high", "medium", "low")
+        ]
+        lines.append(f"- 严重级别统计:{', '.join(ordered)}")
+
+    lines.extend(["", "## 阻断问题", ""])
+    if blocking_issues:
+        for index, issue in enumerate(blocking_issues, start=1):
+            lines.extend(_format_issue(issue, index))
+            lines.append("")
+    else:
+        lines.append("无。")
+        lines.append("")
+
+    lines.extend(["## 其他问题", ""])
+    if non_blocking_issues:
+        for index, issue in enumerate(non_blocking_issues, start=1):
+            lines.extend(_format_issue(issue, index))
+            lines.append("")
+    else:
+        lines.append("无。")
+        lines.append("")
+
+    lines.extend(["## 修复方向", ""])
+    if issues:
+        ordered_issues = [*blocking_issues, *non_blocking_issues]
+        for index, issue in enumerate(ordered_issues, start=1):
+            description = str(issue.get("description") or "未填写问题描述")
+            fix_hint = str(issue.get("fix_hint") or "未提供修复方向")
+            lines.append(f"{index}. {description}:{fix_hint}")
+    else:
+        lines.append("暂无需要修复的问题。")
+
+    return "\n".join(lines).rstrip() + "\n"
+
+
+def write_review_report(project_root: Path, report_file: str, payload: Dict[str, Any]) -> Path:
+    report_path = _resolve_report_path(project_root, report_file)
+    report_path.parent.mkdir(parents=True, exist_ok=True)
+    report_path.write_text(render_review_report(payload), encoding="utf-8")
+    return report_path
+
+
+def _build_review_metrics_record(metrics: Dict[str, Any]):
+    from data_modules.index_manager import ReviewMetrics
+
+    return ReviewMetrics(
+        start_chapter=int(metrics["start_chapter"]),
+        end_chapter=int(metrics["end_chapter"]),
+        overall_score=float(metrics.get("overall_score", 0.0)),
+        dimension_scores=dict(metrics.get("dimension_scores", {})),
+        severity_counts=dict(metrics.get("severity_counts", {})),
+        critical_issues=list(metrics.get("critical_issues", [])),
+        report_file=str(metrics.get("report_file", "")),
+        notes=str(metrics.get("notes", "")),
+    )
+
+
 def build_review_artifacts(
     project_root: Path,
     chapter: int,
@@ -74,12 +187,19 @@ def main() -> None:
             encoding="utf-8",
         )
 
+    if args.report_file:
+        write_review_report(
+            project_root=project_root,
+            report_file=args.report_file,
+            payload=payload,
+        )
+
     if args.save_metrics:
         from data_modules.config import DataModulesConfig
         from data_modules.index_manager import IndexManager
         config = DataModulesConfig.from_project_root(project_root)
         manager = IndexManager(config)
-        manager.save_review_metrics(payload["metrics"])
+        manager.save_review_metrics(_build_review_metrics_record(payload["metrics"]))
 
     print(json.dumps(payload, ensure_ascii=False, indent=2))