# -*- coding: utf-8 -*-
"""
黄金三章检查工具 v2.0 (LLM-Driven)
功能:检测小说前三章是否符合"黄金三章"标准
v2.0 重大升级:
- 保留关键词预检作为快速模式
- 新增 LLM 深度评估模式(AI Native)
- 生成结构化评估 Prompt,解析 XML 评估结果
核心检查点:
- 第 1 章:300 字内主角出场 + 金手指线索 + 强冲突开局
- 第 2 章:金手指展示 + 初次小胜 + 即时爽点
- 第 3 章:悬念钩子 + 下一阶段预告 + 爽点密度 >= 1
使用方法:
python golden_three_checker.py --auto # 快速关键词模式
python golden_three_checker.py --auto --mode llm # LLM 深度评估(推荐)
python golden_three_checker.py --auto --generate-prompt # 仅生成评估 Prompt
"""
import sys
import os
import re
import json
import argparse
from pathlib import Path
from runtime_compat import enable_windows_utf8_stdio
from typing import Dict, List, Optional, Any
# 导入项目定位和章节路径模块
from project_locator import resolve_project_root
from chapter_paths import find_chapter_file
# Windows UTF-8 输出修复
if sys.platform == "win32":
enable_windows_utf8_stdio()
# ============================================================================
# LLM 评估 Prompt 模板
# ============================================================================
LLM_EVALUATION_PROMPT = """你是一位网文编辑,专门负责评估小说开篇的"黄金三章"质量。
请根据以下标准,对这三章内容进行专业评估:
## 黄金三章标准
### 第 1 章核心检查点:
1. **主角 300 字内出场**:主角是否在前 300 字内登场?身份是否清晰?
2. **金手指线索**:是否有金手指/外挂的暗示或线索?
3. **强冲突开局**:开篇是否有足够强的冲突/危机/矛盾?
### 第 2 章核心检查点:
1. **金手指展示**:金手指是否有明确展示?读者能否理解其能力?
2. **初次小胜**:主角是否获得了第一次小规模胜利/成功?
3. **即时爽点**:是否有让读者感到爽快/满足的场景?
### 第 3 章核心检查点:
1. **悬念钩子**:章节结尾是否有悬念?能否驱动读者继续阅读?
2. **下一阶段预告**:是否暗示了接下来的剧情走向/新挑战?
3. **爽点密度**:本章是否至少有 1 个明显的爽点场景?
---
## 待评估内容
### 第 1 章
```
{chapter1_content}
```
### 第 2 章
```
{chapter2_content}
```
### 第 3 章
```
{chapter3_content}
```
---
## 输出要求
请以如下 XML 格式输出你的评估结果(务必严格遵循格式):
```xml
具体证据/引用原文
如未通过,给出改进建议
具体证据
改进建议
具体证据
改进建议
具体证据
改进建议
具体证据
改进建议
具体证据
改进建议
具体证据
改进建议
具体证据
改进建议
具体证据
改进建议
0-100
优秀|良好|需改进|严重不足
最需要改进的问题
次要问题
```
现在开始评估:
"""
class GoldenThreeChecker:
"""黄金三章检查器 v2.0"""
def __init__(self, chapter_files: List[str], mode: str = "keyword"):
"""
初始化检查器
Args:
chapter_files: 章节文件路径列表(必须是前3章)
mode: 检查模式 ("keyword" 快速模式, "llm" LLM评估模式)
"""
if len(chapter_files) != 3:
raise ValueError("必须提供前 3 章的文件路径")
self.chapter_files = chapter_files
self.mode = mode
self.chapters: List[Dict[str, Any]] = []
self.results: Dict[str, Any] = {
"mode": mode,
"ch1": {"主角300字内出场": False, "金手指线索": False, "强冲突开局": False, "详细": {}},
"ch2": {"金手指展示": False, "初次小胜": False, "即时爽点": False, "详细": {}},
"ch3": {"悬念钩子": False, "下一阶段预告": False, "爽点密度>=1": False, "详细": {}},
}
def load_chapters(self) -> None:
"""加载章节内容"""
for i, file_path in enumerate(self.chapter_files):
if not os.path.exists(file_path):
raise FileNotFoundError(f"文件不存在: {file_path}")
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
self.chapters.append({
"number": i + 1,
"path": file_path,
"content": content,
"word_count": len(re.sub(r'\s+', '', content))
})
# ============================================================================
# 快速关键词模式(保留原有逻辑)
# ============================================================================
def check_chapter1_keywords(self) -> None:
"""检查第1章(关键词模式)"""
content = self.chapters[0]["content"]
first_300_chars = content[:300]
# 检查1: 主角 300 字内出场
protagonist_keywords = ["林天", "我", "主角", "少年", "他", "叶凡", "萧炎", "楚枫"]
for keyword in protagonist_keywords:
if keyword in first_300_chars:
self.results["ch1"]["主角300字内出场"] = True
self.results["ch1"]["详细"]["主角出场关键词"] = keyword
break
# 检查2: 金手指线索
golden_finger_keywords = [
"系统", "空间", "重生", "穿越", "戒指", "老爷爷",
"器灵", "传承", "血脉", "觉醒", "签到", "任务", "面板", "属性"
]
found = [kw for kw in golden_finger_keywords if kw in content]
self.results["ch1"]["金手指线索"] = len(found) > 0
self.results["ch1"]["详细"]["金手指关键词"] = found
# 检查3: 强冲突开局
conflict_keywords = [
"退婚", "羞辱", "嘲讽", "废物", "落魄", "危机",
"追杀", "绝境", "被困", "重伤", "濒死", "灭族"
]
found = [kw for kw in conflict_keywords if kw in content]
self.results["ch1"]["强冲突开局"] = len(found) > 0
self.results["ch1"]["详细"]["冲突关键词"] = found
def check_chapter2_keywords(self) -> None:
"""检查第2章(关键词模式)"""
content = self.chapters[1]["content"]
system_display_keywords = ["【", "╔", "姓名", "境界", "力量", "属性", "获得", "奖励", "升级"]
found = [kw for kw in system_display_keywords if kw in content]
self.results["ch2"]["金手指展示"] = len(found) >= 2
self.results["ch2"]["详细"]["展示关键词"] = found
victory_keywords = ["击败", "胜利", "获胜", "成功", "通过", "突破", "秒杀", "碾压"]
found = [kw for kw in victory_keywords if kw in content]
self.results["ch2"]["初次小胜"] = len(found) > 0
self.results["ch2"]["详细"]["胜利关键词"] = found
cool_keywords = ["震惊", "不可能", "怎么会", "全场哗然", "目瞪口呆", "难以置信"]
found = [kw for kw in cool_keywords if kw in content]
self.results["ch2"]["即时爽点"] = len(found) >= 2
self.results["ch2"]["详细"]["爽点关键词"] = found
def check_chapter3_keywords(self) -> None:
"""检查第3章(关键词模式)"""
content = self.chapters[2]["content"]
last_300_chars = content[-300:]
suspense_keywords = ["?", "!", "危机", "即将", "突然", "就在这时", "阴影", "杀机"]
found = [kw for kw in suspense_keywords if kw in last_300_chars]
self.results["ch3"]["悬念钩子"] = len(found) >= 2
self.results["ch3"]["详细"]["悬念关键词"] = found
preview_keywords = ["秘境", "大比", "选拔", "试炼", "任务", "挑战", "前往", "即将"]
found = [kw for kw in preview_keywords if kw in content]
self.results["ch3"]["下一阶段预告"] = len(found) > 0
self.results["ch3"]["详细"]["预告关键词"] = found
cool_count = sum(content.count(kw) for kw in ["震惊", "不可能", "全场哗然", "天才", "击败", "获得"])
self.results["ch3"]["爽点密度>=1"] = cool_count >= 1
self.results["ch3"]["详细"]["爽点统计"] = cool_count
# ============================================================================
# LLM 评估模式
# ============================================================================
def generate_llm_prompt(self) -> str:
"""生成 LLM 评估 Prompt"""
# 截取每章内容(避免过长)
max_chars_per_chapter = 6000
ch1 = self.chapters[0]["content"][:max_chars_per_chapter]
ch2 = self.chapters[1]["content"][:max_chars_per_chapter]
ch3 = self.chapters[2]["content"][:max_chars_per_chapter]
prompt = LLM_EVALUATION_PROMPT.format(
chapter1_content=ch1,
chapter2_content=ch2,
chapter3_content=ch3
)
return prompt
def parse_llm_response(self, xml_response: str) -> Dict[str, Any]:
"""解析 LLM 返回的 XML 评估结果"""
results: Dict[str, Any] = {
"mode": "llm",
"ch1": {"详细": {}},
"ch2": {"详细": {}},
"ch3": {"详细": {}},
"overall_score": 0,
"verdict": "",
"top_issues": []
}
# 提取 overall_score
score_match = re.search(r'(\d+)', xml_response)
if score_match:
results["overall_score"] = int(score_match.group(1))
# 提取 verdict
verdict_match = re.search(r'([^<]+)', xml_response)
if verdict_match:
results["verdict"] = verdict_match.group(1).strip()
# 提取每章的检查点
chapter_pattern = re.compile(
r'(.*?)',
re.DOTALL
)
check_pattern = re.compile(
r'\s*'
r'([^<]*)\s*'
r'([^<]*)\s*'
r'',
re.DOTALL
)
for chapter_match in chapter_pattern.finditer(xml_response):
chapter_num = chapter_match.group(1)
chapter_content = chapter_match.group(2)
chapter_key = f"ch{chapter_num}"
for check_match in check_pattern.finditer(chapter_content):
check_name = check_match.group(1)
passed = check_match.group(2) == "true"
score = int(check_match.group(3))
evidence = check_match.group(4).strip()
suggestion = check_match.group(5).strip()
results[chapter_key][check_name] = passed
results[chapter_key]["详细"][check_name] = {
"score": score,
"evidence": evidence,
"suggestion": suggestion
}
# 提取 top_issues
issue_pattern = re.compile(r'([^<]+)')
for issue_match in issue_pattern.finditer(xml_response):
priority = int(issue_match.group(1))
issue_text = issue_match.group(2).strip()
results["top_issues"].append({"priority": priority, "issue": issue_text})
return results
# ============================================================================
# 报告生成
# ============================================================================
def calculate_score(self) -> tuple:
"""计算总体得分"""
total_checks = 0
passed_checks = 0
for chapter_key in ["ch1", "ch2", "ch3"]:
for check_key, check_value in self.results[chapter_key].items():
if check_key != "详细" and isinstance(check_value, bool):
total_checks += 1
if check_value:
passed_checks += 1
score = (passed_checks / total_checks) * 100 if total_checks > 0 else 0
return score, passed_checks, total_checks
def generate_report(self) -> str:
"""生成检查报告"""
score, passed, total = self.calculate_score()
report = []
report.append("=" * 60)
report.append(f"黄金三章诊断报告 (模式: {self.mode})")
report.append("=" * 60)
report.append(f"\n总体得分: {score:.1f}% ({passed}/{total} 项通过)\n")
# 第 1 章
report.append("-" * 60)
report.append("【第 1 章】检查结果")
report.append("-" * 60)
for check_name in ["主角300字内出场", "金手指线索", "强冲突开局"]:
passed = self.results["ch1"].get(check_name, False)
icon = "✅" if passed else "❌"
report.append(f"{icon} {check_name}: {'通过' if passed else '未通过'}")
# 显示详细信息
detail = self.results["ch1"]["详细"].get(check_name)
if isinstance(detail, dict):
if detail.get("evidence"):
report.append(f" └─ 证据: {detail['evidence'][:100]}...")
if not passed and detail.get("suggestion"):
report.append(f" └─ 建议: {detail['suggestion']}")
elif isinstance(detail, list) and detail:
report.append(f" └─ 关键词: {', '.join(detail[:5])}")
# 第 2 章
report.append("\n" + "-" * 60)
report.append("【第 2 章】检查结果")
report.append("-" * 60)
for check_name in ["金手指展示", "初次小胜", "即时爽点"]:
passed = self.results["ch2"].get(check_name, False)
icon = "✅" if passed else "❌"
report.append(f"{icon} {check_name}: {'通过' if passed else '未通过'}")
detail = self.results["ch2"]["详细"].get(check_name)
if isinstance(detail, dict) and detail.get("evidence"):
report.append(f" └─ 证据: {detail['evidence'][:100]}...")
elif isinstance(detail, list) and detail:
report.append(f" └─ 关键词: {', '.join(detail[:5])}")
# 第 3 章
report.append("\n" + "-" * 60)
report.append("【第 3 章】检查结果")
report.append("-" * 60)
for check_name in ["悬念钩子", "下一阶段预告", "爽点密度>=1"]:
passed = self.results["ch3"].get(check_name, False)
icon = "✅" if passed else "❌"
report.append(f"{icon} {check_name}: {'通过' if passed else '未通过'}")
detail = self.results["ch3"]["详细"].get(check_name)
if isinstance(detail, dict) and detail.get("evidence"):
report.append(f" └─ 证据: {detail['evidence'][:100]}...")
# 改进建议
report.append("\n" + "=" * 60)
report.append("【改进建议】")
report.append("=" * 60)
if score < 60:
report.append("\n🔴 警告: 开篇吸引力不足,严重影响读者留存率!")
elif score < 80:
report.append("\n🟡 注意: 开篇有改进空间")
else:
report.append("\n✅ 很好!开篇符合黄金三章标准")
# LLM 模式的额外信息
if self.mode == "llm" and self.results.get("top_issues"):
report.append("\n优先修复:")
for issue in self.results["top_issues"]:
report.append(f" {issue['priority']}. {issue['issue']}")
report.append("\n" + "=" * 60)
return "\n".join(report)
def run(self) -> None:
"""执行检查"""
print("正在加载章节...")
self.load_chapters()
print(f"✅ 已加载 {len(self.chapters)} 章")
for ch in self.chapters:
print(f" - 第 {ch['number']} 章: {ch['word_count']} 字")
print(f"\n正在执行检查 (模式: {self.mode})...\n")
if self.mode == "keyword":
self.check_chapter1_keywords()
self.check_chapter2_keywords()
self.check_chapter3_keywords()
report = self.generate_report()
print(report)
elif self.mode == "llm":
prompt = self.generate_llm_prompt()
print("=" * 60)
print("LLM 评估模式:请将以下 Prompt 发送给 Claude/GPT")
print("=" * 60)
print("\n--- PROMPT START ---\n")
print(prompt[:2000] + "\n...[内容已截断,完整版见输出文件]...")
print("\n--- PROMPT END ---\n")
# 保存完整 prompt
output_dir = Path(".webnovel")
output_dir.mkdir(exist_ok=True)
prompt_file = output_dir / "golden_three_prompt.md"
with open(prompt_file, 'w', encoding='utf-8') as f:
f.write(prompt)
print(f"📄 完整 Prompt 已保存至: {prompt_file}")
print("\n💡 使用方法:")
print(" 1. 将 Prompt 发送给 Claude/GPT")
print(" 2. 获取 XML 格式的评估结果")
print(" 3. 运行: python golden_three_checker.py --parse-response ")
# 保存结果
output_dir = Path(".webnovel")
output_dir.mkdir(exist_ok=True)
output_file = output_dir / "golden_three_report.json"
with open(output_file, 'w', encoding='utf-8') as f:
json.dump(self.results, f, ensure_ascii=False, indent=2)
print(f"\n📄 详细结果已保存至: {output_file}")
def main():
parser = argparse.ArgumentParser(
description="黄金三章检查工具 v2.0 (LLM-Driven)",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
示例:
# 快速关键词模式(默认)
python golden_three_checker.py --auto
# LLM 深度评估模式(推荐)
python golden_three_checker.py --auto --mode llm
# 解析 LLM 返回的评估结果
python golden_three_checker.py --parse-response response.xml
""".strip(),
)
parser.add_argument("chapter_files", nargs="*", help="前三章文件路径")
parser.add_argument("--auto", action="store_true", help="自动定位前三章文件")
parser.add_argument("--mode", choices=["keyword", "llm"], default="keyword",
help="检查模式: keyword(快速) / llm(深度)")
parser.add_argument("--project-root", default=None, help="项目根目录")
parser.add_argument("--parse-response", metavar="FILE", help="解析 LLM 返回的 XML 文件")
args = parser.parse_args()
# 解析 LLM 响应模式
if args.parse_response:
if not os.path.exists(args.parse_response):
print(f"❌ 文件不存在: {args.parse_response}")
sys.exit(1)
with open(args.parse_response, 'r', encoding='utf-8') as f:
xml_content = f.read()
checker = GoldenThreeChecker(["dummy"] * 3, mode="llm")
checker.results = checker.parse_llm_response(xml_content)
print("=" * 60)
print("LLM 评估结果解析")
print("=" * 60)
print(json.dumps(checker.results, ensure_ascii=False, indent=2))
sys.exit(0)
# 正常检查模式
chapter_files = []
if args.auto or not args.chapter_files:
try:
project_root = resolve_project_root(args.project_root)
except FileNotFoundError as e:
print(f"❌ {e}")
sys.exit(1)
for i in range(1, 4):
chapter_path = find_chapter_file(project_root, i)
if chapter_path:
chapter_files.append(str(chapter_path))
else:
print(f"❌ 找不到第 {i} 章文件")
sys.exit(1)
print(f"📂 项目根目录: {project_root}")
print(f"📄 检测到前三章: {', '.join(Path(f).name for f in chapter_files)}\n")
else:
if len(args.chapter_files) < 3:
print("用法: python golden_three_checker.py <第1章路径> <第2章路径> <第3章路径>")
sys.exit(1)
chapter_files = args.chapter_files[:3]
try:
checker = GoldenThreeChecker(chapter_files, mode=args.mode)
checker.run()
except Exception as e:
print(f"❌ 错误: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
if __name__ == "__main__":
main()