209 lines
6.8 KiB
Python
209 lines
6.8 KiB
Python
#!/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()
|