#!/usr/bin/env python3 """ pre-commit-check.py · 冰朔小说创作系统 · 提交前自动检查 放在 scripts/ 目录,git commit 前手动或通过 pre-commit hook 自动调用。 检查范围:chapters/ 下所有 .txt 文件(正文)。 检查项: 1. 字数:非空白字符 2000-2800 区间(仅单章文件) 2. 违禁词:AI 化套话 3. 半角标点:引号、省略号、破折号 4. 代词一致性:许怀钰=他,何采霏=她(按书检测) 5. Markdown 泄露:** ## 等 6. 元叙述:正文中不出现"第X章"(标题行除外,多章文件跳过) 用法: python3 scripts/pre-commit-check.py 退出码: 0 = 全部通过 1 = 有不达标项 """ import re import os import sys import glob REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) CHAPTERS_DIR = os.path.join(REPO_ROOT, "chapters") # --- 检查规则 --- # 违禁词:AI 化套话 # 注意:"最后"单独检查,只匹配段首/句首的"最后,"模式(AI 三段式标志) FORBIDDEN_WORDS = [ "值得注意的是", "不得不说", "总的来说", "作为一个", "说道", "心中充满", "不禁", "不由得", "与此同时", ] # "最后"只检查句首用法(AI 三段式 "最后,") FORBIDDEN_PATTERNS = [ (r'\n\s*最后[,。]', '"最后"句首(AI 三段式)'), ] HALF_WIDTH_CHECKS = [ ('"', "半角引号(应使用中文双引号)"), ("...", "半角省略号(应使用……6点)"), ("--", "半角破折号(应使用——中文破折号)"), ] MARKDOWN_LEAKS = ["**", "##"] # 每本书的主角代词规则:主角名 -> 正确代词 PROTAGONIST_RULES = { "许怀钰": "他", "何采霏": "她", } def count_chars(text): """非空白字符统计(D169 铁律)""" return len(re.findall(r'[^\s]', text)) def is_multi_chapter_file(filename): """判断是否为多章合并文件(之之侧旧格式)""" # 匹配 "肖轩447-450章" / "沈婉凝278-282章" / "女频续写263章" 等 if re.search(r'\d+-\d+章', filename): return True # 匹配 "肖轩447-450章【8684字】" 这种 if '【' in filename and '章' in filename: return True # 匹配 "女频续写263章(沈婉凝)" 这种单文件多章 if re.search(r'\d+章.*(', filename) and '第' not in filename: return True return False def check_chapter(filepath): """检查单个章节文件,返回 (passed, issues, skipped_reason)""" issues = [] skipped_reason = None with open(filepath, 'r', encoding='utf-8') as f: text = f.read() filename = os.path.basename(filepath) # 0. 空文件检查 if not text.strip(): issues.append(f" ❌ 空文件") return False, issues, None # 判断是否为多章合并文件 multi_chapter = is_multi_chapter_file(filename) if multi_chapter: skipped_reason = "多章合并文件(旧格式,跳过字数和元叙述检查)" # 1. 字数检查(仅单章文件) char_count = count_chars(text) if not multi_chapter: if char_count < 2000: issues.append(f" ❌ 字数不足: {char_count} < 2000(差 {2000 - char_count} 字)") elif char_count > 2800: issues.append(f" ⚠️ 字数超标: {char_count} > 2800") else: issues.append(f" ✓ 字数: {char_count}") else: issues.append(f" ℹ️ 字数: {char_count}(多章合并,不判定)") # 2. 违禁词检查(所有文件都查) for word in FORBIDDEN_WORDS: if word in text: count = text.count(word) issues.append(f" ❌ 违禁词 \"{word}\": {count} 次") # 2b. 违禁模式检查(句首"最后"等) for pattern, desc in FORBIDDEN_PATTERNS: matches = re.findall(pattern, text) if matches: issues.append(f" ❌ {desc}: {len(matches)} 处") # 3. 半角标点检查(所有文件都查) for pattern_str, desc in HALF_WIDTH_CHECKS: if pattern_str in text: count = text.count(pattern_str) issues.append(f" ⚠️ {desc}: {count} 处") # 4. Markdown 泄露(所有文件都查) for md in MARKDOWN_LEAKS: if md in text: issues.append(f" ❌ Markdown 泄露: \"{md}\"") # 5. 元叙述检查(仅单章文件) if not multi_chapter: lines = text.split('\n') for i, line in enumerate(lines): if i == 0: # 标题行,跳过 continue if re.search(r'第\s*\d+\s*章', line): issues.append(f" ❌ 元叙述: 第{i+1}行出现\"第X章\"") # 6. 代词一致性 dirname = os.path.basename(os.path.dirname(filepath)) for name, correct_pronoun in PROTAGONIST_RULES.items(): if name in dirname or name in filename: wrong_pronoun = "她" if correct_pronoun == "他" else "他" wrong_pattern = f"{name}{wrong_pronoun}" if wrong_pattern in text: issues.append(f" ❌ 代词错误: \"{wrong_pattern}\"({name}应为{correct_pronoun})") # 判断是否通过 # ⚠️ 标记为警告(不阻断提交),❌ 标记为错误(阻断提交) has_errors = any("❌" in issue for issue in issues) passed = not has_errors return passed, issues, skipped_reason def main(): print("=" * 60) print("冰朔小说创作系统 · 提交前自动检查") print("=" * 60) print() # 找到所有 .txt 文件 txt_files = glob.glob(os.path.join(CHAPTERS_DIR, "**", "*.txt"), recursive=True) if not txt_files: print("未找到章节文件,跳过检查。") sys.exit(0) all_passed = True total_files = 0 failed_files = 0 skipped_files = 0 for filepath in sorted(txt_files): rel_path = os.path.relpath(filepath, REPO_ROOT) total_files += 1 passed, issues, skipped_reason = check_chapter(filepath) if skipped_reason: skipped_files += 1 if not passed: all_passed = False failed_files += 1 print(f"📋 {rel_path}") if skipped_reason: print(f" ℹ️ {skipped_reason}") for issue in issues: print(issue) print() # 汇总 print("=" * 60) print(f"检查完成: {total_files} 个文件, {total_files - failed_files} 通过, {failed_files} 不达标, {skipped_files} 跳过") if all_passed: print("✅ 全部通过,可以提交。") sys.exit(0) else: print("❌ 有不达标项(❌ 为阻断项,⚠️ 为提醒项)。") print(" 阻断项请修复后再提交。") sys.exit(1) if __name__ == "__main__": main()