novel-system/scripts/pre-commit-check.py

209 lines
6.8 KiB
Python
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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()