paperfit-cli 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/.claude/commands/adjust-length.md +21 -0
  2. package/.claude/commands/check-visual.md +27 -0
  3. package/.claude/commands/fix-layout.md +31 -0
  4. package/.claude/commands/migrate-template.md +23 -0
  5. package/.claude/commands/repair-table.md +21 -0
  6. package/.claude/commands/show-status.md +32 -0
  7. package/.claude-plugin/README.md +77 -0
  8. package/.claude-plugin/marketplace.json +41 -0
  9. package/.claude-plugin/plugin.json +39 -0
  10. package/CLAUDE.md +266 -0
  11. package/CONTRIBUTING.md +131 -0
  12. package/LICENSE +21 -0
  13. package/README.md +164 -0
  14. package/agents/code-surgeon-agent.md +214 -0
  15. package/agents/layout-detective-agent.md +229 -0
  16. package/agents/orchestrator-agent.md +254 -0
  17. package/agents/quality-gatekeeper-agent.md +270 -0
  18. package/agents/rule-engine-agent.md +224 -0
  19. package/agents/semantic-polish-agent.md +250 -0
  20. package/bin/paperfit.js +176 -0
  21. package/config/agent_roles.yaml +56 -0
  22. package/config/layout_rules.yaml +54 -0
  23. package/config/templates.yaml +241 -0
  24. package/config/vto_taxonomy.yaml +489 -0
  25. package/config/writing_rules.yaml +64 -0
  26. package/install.sh +30 -0
  27. package/package.json +52 -0
  28. package/requirements.txt +5 -0
  29. package/scripts/benchmark_runner.py +629 -0
  30. package/scripts/compile.sh +244 -0
  31. package/scripts/config_validator.py +339 -0
  32. package/scripts/cv_detector.py +600 -0
  33. package/scripts/evidence_collector.py +167 -0
  34. package/scripts/float_fixers.py +861 -0
  35. package/scripts/inject_defects.py +549 -0
  36. package/scripts/install-claude-global.js +148 -0
  37. package/scripts/install.js +66 -0
  38. package/scripts/install.sh +106 -0
  39. package/scripts/overflow_fixers.py +656 -0
  40. package/scripts/package-for-opensource.sh +138 -0
  41. package/scripts/parse_log.py +260 -0
  42. package/scripts/postinstall.js +38 -0
  43. package/scripts/pre_tool_use.py +265 -0
  44. package/scripts/render_pages.py +244 -0
  45. package/scripts/session_logger.py +329 -0
  46. package/scripts/space_util_fixers.py +773 -0
  47. package/scripts/state_manager.py +352 -0
  48. package/scripts/test_commands.py +187 -0
  49. package/scripts/test_cv_detector.py +214 -0
  50. package/scripts/test_integration.py +290 -0
  51. package/skills/consistency-polisher/SKILL.md +337 -0
  52. package/skills/float-optimizer/SKILL.md +284 -0
  53. package/skills/latex_fixers/__init__.py +82 -0
  54. package/skills/latex_fixers/float_fixers.py +392 -0
  55. package/skills/latex_fixers/fullwidth_fixers.py +375 -0
  56. package/skills/latex_fixers/overflow_fixers.py +250 -0
  57. package/skills/latex_fixers/semantic_micro_tuning.py +362 -0
  58. package/skills/latex_fixers/space_util_fixers.py +389 -0
  59. package/skills/latex_fixers/utils.py +55 -0
  60. package/skills/overflow-repair/SKILL.md +304 -0
  61. package/skills/space-util-fixer/SKILL.md +307 -0
  62. package/skills/taxonomy-vto/SKILL.md +486 -0
  63. package/skills/template-migrator/SKILL.md +251 -0
  64. package/skills/visual-inspector/SKILL.md +217 -0
  65. package/skills/writing-polish/SKILL.md +289 -0
@@ -0,0 +1,244 @@
1
+ #!/bin/bash
2
+ #
3
+ # PaperFit LaTeX 编译脚本
4
+ #
5
+ # 封装 LaTeX 编译流程,支持 latexmk 和 pdflatex 两种方式,
6
+ # 自动处理参考文献、交叉引用的多次编译,并输出结构化日志。
7
+ #
8
+ # 用法:
9
+ # ./compile.sh <main_tex> [--clean] [--engine pdflatex|xelatex|lualatex]
10
+ #
11
+ # 示例:
12
+ # ./compile.sh main.tex
13
+ # ./compile.sh main.tex --clean
14
+ # ./compile.sh main.tex --engine xelatex
15
+
16
+ set -e # 遇到错误立即退出
17
+
18
+ # 颜色输出
19
+ RED='\033[0;31m'
20
+ GREEN='\033[0;32m'
21
+ YELLOW='\033[1;33m'
22
+ NC='\033[0m' # No Color
23
+
24
+ # 默认参数
25
+ MAIN_TEX=""
26
+ CLEAN_MODE=false
27
+ ENGINE="pdflatex"
28
+ USE_LATEXMK=true
29
+ BIBER=false
30
+
31
+ # 帮助信息
32
+ usage() {
33
+ cat << EOF
34
+ 用法: $0 <main_tex> [选项]
35
+
36
+ 选项:
37
+ --clean 清理临时文件后重新编译
38
+ --engine ENGINE 指定编译引擎 (pdflatex, xelatex, lualatex) [默认: pdflatex]
39
+ --no-latexmk 禁用 latexmk,使用手动多次编译
40
+ --biber 使用 biber 处理参考文献(默认使用 bibtex)
41
+ -h, --help 显示此帮助信息
42
+
43
+ 示例:
44
+ $0 main.tex
45
+ $0 main.tex --clean --engine xelatex
46
+ $0 main.tex --no-latexmk --biber
47
+ EOF
48
+ exit 0
49
+ }
50
+
51
+ # 解析参数
52
+ while [[ $# -gt 0 ]]; do
53
+ case $1 in
54
+ --clean)
55
+ CLEAN_MODE=true
56
+ shift
57
+ ;;
58
+ --engine)
59
+ ENGINE="$2"
60
+ shift 2
61
+ ;;
62
+ --no-latexmk)
63
+ USE_LATEXMK=false
64
+ shift
65
+ ;;
66
+ --biber)
67
+ BIBER=true
68
+ shift
69
+ ;;
70
+ -h|--help)
71
+ usage
72
+ ;;
73
+ *.tex)
74
+ MAIN_TEX="$1"
75
+ shift
76
+ ;;
77
+ *)
78
+ echo -e "${RED}未知参数: $1${NC}"
79
+ usage
80
+ ;;
81
+ esac
82
+ done
83
+
84
+ # 检查主文件是否提供
85
+ if [[ -z "$MAIN_TEX" ]]; then
86
+ echo -e "${RED}错误: 未指定主 .tex 文件${NC}"
87
+ usage
88
+ fi
89
+
90
+ # 检查主文件是否存在
91
+ if [[ ! -f "$MAIN_TEX" ]]; then
92
+ echo -e "${RED}错误: 文件不存在: $MAIN_TEX${NC}"
93
+ exit 1
94
+ fi
95
+
96
+ # 获取文件名(不含扩展名)
97
+ BASENAME=$(basename "$MAIN_TEX" .tex)
98
+ DIRNAME=$(dirname "$MAIN_TEX")
99
+ if [[ "$DIRNAME" == "." ]]; then
100
+ DIRNAME=""
101
+ fi
102
+ MAIN_NAME="${DIRNAME:+$DIRNAME/}$BASENAME"
103
+
104
+ # 进入主文件所在目录
105
+ cd "$(dirname "$MAIN_TEX")" || exit 1
106
+ MAIN_TEX_FILE=$(basename "$MAIN_TEX")
107
+
108
+ echo -e "${GREEN}========================================${NC}"
109
+ echo -e "${GREEN}PaperFit LaTeX 编译${NC}"
110
+ echo -e "${GREEN}========================================${NC}"
111
+ echo "主文件: $MAIN_TEX"
112
+ echo "编译引擎: $ENGINE"
113
+ echo "使用 latexmk: $USE_LATEXMK"
114
+ echo "参考文献工具: $([ "$BIBER" = true ] && echo "biber" || echo "bibtex")"
115
+ echo ""
116
+
117
+ # 清理模式
118
+ if [[ "$CLEAN_MODE" == true ]]; then
119
+ echo -e "${YELLOW}清理临时文件...${NC}"
120
+ if [[ "$USE_LATEXMK" == true ]]; then
121
+ latexmk -C "$MAIN_TEX_FILE"
122
+ else
123
+ rm -f "$BASENAME".{aux,log,out,toc,lof,lot,blg,bbl,bcf,run.xml,fls,fdb_latexmk,synctex.gz,nav,snm,vrb}
124
+ fi
125
+ echo -e "${GREEN}清理完成${NC}\n"
126
+ fi
127
+
128
+ # 编译函数
129
+ compile_with_latexmk() {
130
+ echo -e "${YELLOW}使用 latexmk 编译...${NC}"
131
+
132
+ local latexmk_opts="-pdf -interaction=nonstopmode"
133
+ if [[ "$ENGINE" != "pdflatex" ]]; then
134
+ latexmk_opts="$latexmk_opts -$ENGINE"
135
+ fi
136
+
137
+ if latexmk $latexmk_opts "$MAIN_TEX_FILE"; then
138
+ echo -e "${GREEN}latexmk 编译成功${NC}"
139
+ return 0
140
+ else
141
+ echo -e "${RED}latexmk 编译失败${NC}"
142
+ return 1
143
+ fi
144
+ }
145
+
146
+ compile_manual() {
147
+ echo -e "${YELLOW}使用手动多次编译...${NC}"
148
+
149
+ local tex_cmd="$ENGINE -interaction=nonstopmode"
150
+
151
+ # 第一遍编译
152
+ echo "第一遍 $ENGINE..."
153
+ if ! $tex_cmd "$MAIN_TEX_FILE" > /dev/null 2>&1; then
154
+ echo -e "${RED}第一遍编译失败,请查看 ${BASENAME}.log${NC}"
155
+ return 1
156
+ fi
157
+
158
+ # 处理参考文献
159
+ if grep -q "\\bibliography{" "$MAIN_TEX_FILE" || grep -q "\\bibdata{" "${BASENAME}.aux" 2>/dev/null; then
160
+ echo "处理参考文献..."
161
+ if [[ "$BIBER" == true ]]; then
162
+ if biber "$BASENAME" > /dev/null 2>&1; then
163
+ echo "biber 成功"
164
+ else
165
+ echo -e "${YELLOW}biber 失败,尝试 bibtex...${NC}"
166
+ bibtex "$BASENAME" > /dev/null 2>&1 || echo -e "${YELLOW}警告: 参考文献处理可能有问题${NC}"
167
+ fi
168
+ else
169
+ bibtex "$BASENAME" > /dev/null 2>&1 || echo -e "${YELLOW}警告: bibtex 失败或无需参考文献${NC}"
170
+ fi
171
+ fi
172
+
173
+ # 第二遍编译(更新交叉引用)
174
+ echo "第二遍 $ENGINE..."
175
+ if ! $tex_cmd "$MAIN_TEX_FILE" > /dev/null 2>&1; then
176
+ echo -e "${RED}第二遍编译失败${NC}"
177
+ return 1
178
+ fi
179
+
180
+ # 检查是否需要第三次编译(交叉引用未稳定)
181
+ if grep -q "Rerun to get" "${BASENAME}.log"; then
182
+ echo "第三次 $ENGINE (稳定交叉引用)..."
183
+ if ! $tex_cmd "$MAIN_TEX_FILE" > /dev/null 2>&1; then
184
+ echo -e "${YELLOW}第三次编译有警告,但 PDF 已生成${NC}"
185
+ fi
186
+ fi
187
+
188
+ echo -e "${GREEN}手动编译完成${NC}"
189
+ return 0
190
+ }
191
+
192
+ # 执行编译
193
+ START_TIME=$(date +%s)
194
+
195
+ if [[ "$USE_LATEXMK" == true ]]; then
196
+ compile_with_latexmk
197
+ COMPILE_STATUS=$?
198
+ else
199
+ compile_manual
200
+ COMPILE_STATUS=$?
201
+ fi
202
+
203
+ END_TIME=$(date +%s)
204
+ DURATION=$((END_TIME - START_TIME))
205
+
206
+ # 检查 PDF 是否生成
207
+ PDF_FILE="${BASENAME}.pdf"
208
+ if [[ -f "$PDF_FILE" ]]; then
209
+ PDF_SIZE=$(du -h "$PDF_FILE" | cut -f1)
210
+ PDF_PAGES=$(pdfinfo "$PDF_FILE" 2>/dev/null | grep "Pages:" | awk '{print $2}' || echo "?")
211
+ echo ""
212
+ echo -e "${GREEN}========================================${NC}"
213
+ echo -e "${GREEN}编译结果${NC}"
214
+ echo -e "${GREEN}========================================${NC}"
215
+ echo "PDF 文件: $PDF_FILE"
216
+ echo "文件大小: $PDF_SIZE"
217
+ echo "页数: $PDF_PAGES"
218
+ echo "编译耗时: ${DURATION}s"
219
+
220
+ # 输出日志摘要
221
+ LOG_FILE="${BASENAME}.log"
222
+ if [[ -f "$LOG_FILE" ]]; then
223
+ ERROR_COUNT=$(grep -c "^!" "$LOG_FILE" 2>/dev/null || echo 0)
224
+ WARNING_COUNT=$(grep -c "Warning:" "$LOG_FILE" 2>/dev/null || echo 0)
225
+ OVERFULL_COUNT=$(grep -c "Overfull" "$LOG_FILE" 2>/dev/null || echo 0)
226
+ echo "错误数: $ERROR_COUNT"
227
+ echo "警告数: $WARNING_COUNT"
228
+ echo "Overfull hbox: $OVERFULL_COUNT"
229
+ fi
230
+ echo -e "${GREEN}========================================${NC}"
231
+ else
232
+ echo -e "${RED}========================================${NC}"
233
+ echo -e "${RED}编译失败: PDF 未生成${NC}"
234
+ echo -e "${RED}========================================${NC}"
235
+ echo "请查看日志文件: ${BASENAME}.log"
236
+ exit 1
237
+ fi
238
+
239
+ # 根据编译状态返回退出码
240
+ if [[ $COMPILE_STATUS -eq 0 ]] && [[ -f "$PDF_FILE" ]]; then
241
+ exit 0
242
+ else
243
+ exit 1
244
+ fi
@@ -0,0 +1,339 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ PaperFit 配置验证器
4
+
5
+ 验证所有 YAML 配置文件的完整性和一致性。
6
+ 防止配置错误导致检测失效或 Agent 行为异常。
7
+
8
+ 用法:
9
+ python config_validator.py [--verbose]
10
+ """
11
+
12
+ import os
13
+ import sys
14
+ import yaml
15
+ from pathlib import Path
16
+ from typing import Any, Dict, List, Optional, Tuple
17
+ from dataclasses import dataclass
18
+ from typing import TypedDict
19
+
20
+
21
+ # ============================================================
22
+ # 配置 Schema 定义
23
+ # ============================================================
24
+
25
+ CONFIG_DIR = Path(__file__).parent.parent / "config"
26
+
27
+ CONFIG_FILES = {
28
+ "vto_taxonomy.yaml": "VTO 缺陷分类体系",
29
+ "layout_rules.yaml": "版式硬规则与阈值",
30
+ "writing_rules.yaml": "写作硬规则",
31
+ "templates.yaml": "模板参数",
32
+ "agent_roles.yaml": "Agent 职责描述",
33
+ }
34
+
35
+
36
+ @dataclass
37
+ class ValidationError:
38
+ """验证错误"""
39
+ file: str
40
+ field: str
41
+ message: str
42
+ severity: str # "error" | "warning"
43
+
44
+
45
+ class LayoutRulesSchema(TypedDict, total=False):
46
+ """layout_rules.yaml 的 schema"""
47
+ whitespace: Dict[str, float]
48
+ table: Dict[str, Any]
49
+ float: Dict[str, Any]
50
+ equation: Dict[str, float]
51
+ paragraph: Dict[str, Any]
52
+ font: Dict[str, Any]
53
+ consistency: Dict[str, bool]
54
+
55
+
56
+ class VtoTaxonomySchema(TypedDict, total=False):
57
+ """vto_taxonomy.yaml 的 schema"""
58
+ version: str
59
+ description: str
60
+ categories: List[Dict[str, Any]]
61
+ defects: List[Dict[str, Any]]
62
+ severity_levels: Dict[str, Any]
63
+ skill_routing: Dict[str, List[str]]
64
+
65
+
66
+ # ============================================================
67
+ # 验证器类
68
+ # ============================================================
69
+
70
+ class ConfigValidator:
71
+ """配置验证器"""
72
+
73
+ def __init__(self, config_dir: Path = CONFIG_DIR):
74
+ self.config_dir = config_dir
75
+ self.errors: List[ValidationError] = []
76
+ self.warnings: List[ValidationError] = []
77
+ self.loaded_configs: Dict[str, Any] = {}
78
+
79
+ def validate_all(self) -> Tuple[bool, List[ValidationError], List[ValidationError]]:
80
+ """验证所有配置文件"""
81
+ print(f"验证配置目录:{self.config_dir}")
82
+ print("-" * 50)
83
+
84
+ for config_file, description in CONFIG_FILES.items():
85
+ config_path = self.config_dir / config_file
86
+ if not config_path.exists():
87
+ self.errors.append(ValidationError(
88
+ file=config_file,
89
+ field="",
90
+ message=f"配置文件不存在:{config_file}",
91
+ severity="error"
92
+ ))
93
+ print(f"[缺失] {config_file}: {description}")
94
+ continue
95
+
96
+ print(f"[检查] {config_file}: {description}")
97
+ self._load_and_validate(config_file)
98
+
99
+ print("-" * 50)
100
+
101
+ # 输出错误和警告
102
+ if self.errors:
103
+ print(f"\n发现 {len(self.errors)} 个错误:")
104
+ for err in self.errors:
105
+ print(f" [ERROR] {err.file}: {err.field} - {err.message}")
106
+
107
+ if self.warnings:
108
+ print(f"\n发现 {len(self.warnings)} 个警告:")
109
+ for warn in self.warnings:
110
+ print(f" [WARN] {warn.file}: {warn.field} - {warn.message}")
111
+
112
+ success = len(self.errors) == 0
113
+ print(f"\n验证结果:{'通过' if success else '失败'} ({len(self.warnings)} 个警告)")
114
+
115
+ return success, self.errors, self.warnings
116
+
117
+ def _load_and_validate(self, config_file: str) -> None:
118
+ """加载并验证单个配置文件"""
119
+ config_path = self.config_dir / config_file
120
+
121
+ try:
122
+ with open(config_path, 'r', encoding='utf-8') as f:
123
+ config = yaml.safe_load(f)
124
+ self.loaded_configs[config_file] = config
125
+ except yaml.YAMLError as e:
126
+ self.errors.append(ValidationError(
127
+ file=config_file,
128
+ field="",
129
+ message=f"YAML 解析错误:{e}",
130
+ severity="error"
131
+ ))
132
+ return
133
+ except Exception as e:
134
+ self.errors.append(ValidationError(
135
+ file=config_file,
136
+ field="",
137
+ message=f"读取失败:{e}",
138
+ severity="error"
139
+ ))
140
+ return
141
+
142
+ # 根据文件类型进行特定验证
143
+ if config_file == "layout_rules.yaml":
144
+ self._validate_layout_rules(config_file, config)
145
+ elif config_file == "vto_taxonomy.yaml":
146
+ self._validate_vto_taxonomy(config_file, config)
147
+ elif config_file == "templates.yaml":
148
+ self._validate_templates(config_file, config)
149
+ elif config_file == "writing_rules.yaml":
150
+ self._validate_writing_rules(config_file, config)
151
+ elif config_file == "agent_roles.yaml":
152
+ self._validate_agent_roles(config_file, config)
153
+
154
+ def _validate_layout_rules(self, file: str, config: Dict) -> None:
155
+ """验证 layout_rules.yaml"""
156
+ # 验证 whitespace 部分
157
+ whitespace = config.get('whitespace', {})
158
+ if 'trailing_whitespace_max_ratio' in whitespace:
159
+ val = whitespace['trailing_whitespace_max_ratio']
160
+ if not (0 < val < 1):
161
+ self.errors.append(ValidationError(
162
+ file=file, field="whitespace.trailing_whitespace_max_ratio",
163
+ message=f"阈值必须在 0-1 之间,当前值:{val}", severity="error"
164
+ ))
165
+
166
+ # 验证 table 部分
167
+ table = config.get('table', {})
168
+ if 'min_width_utilization' in table and 'max_width_utilization' in table:
169
+ if table['min_width_utilization'] >= table['max_width_utilization']:
170
+ self.errors.append(ValidationError(
171
+ file=file, field="table.width_utilization",
172
+ message="min_width_utilization 必须小于 max_width_utilization",
173
+ severity="error"
174
+ ))
175
+
176
+ # 验证 float 部分
177
+ float_cfg = config.get('float', {})
178
+ if 'max_reference_distance_pages' in float_cfg:
179
+ val = float_cfg['max_reference_distance_pages']
180
+ if val < 0:
181
+ self.errors.append(ValidationError(
182
+ file=file, field="float.max_reference_distance_pages",
183
+ message=f"距离不能为负数,当前值:{val}", severity="error"
184
+ ))
185
+
186
+ def _validate_vto_taxonomy(self, file: str, config: Dict) -> None:
187
+ """验证 vto_taxonomy.yaml"""
188
+ # 验证 categories
189
+ categories = config.get('categories', [])
190
+ category_ids = set()
191
+ for cat in categories:
192
+ cat_id = cat.get('id')
193
+ if not cat_id:
194
+ self.errors.append(ValidationError(
195
+ file=file, field="categories[].id",
196
+ message="分类缺少 id 字段", severity="error"
197
+ ))
198
+ continue
199
+ if cat_id in category_ids:
200
+ self.errors.append(ValidationError(
201
+ file=file, field="categories[].id",
202
+ message=f"分类 ID 重复:{cat_id}", severity="error"
203
+ ))
204
+ category_ids.add(cat_id)
205
+
206
+ # 验证 defects
207
+ defects = config.get('defects', [])
208
+ defect_ids = set()
209
+ for defect in defects:
210
+ defect_id = defect.get('id')
211
+ if not defect_id:
212
+ self.errors.append(ValidationError(
213
+ file=file, field="defects[].id",
214
+ message="缺陷缺少 id 字段", severity="error"
215
+ ))
216
+ continue
217
+ if defect_id in defect_ids:
218
+ self.errors.append(ValidationError(
219
+ file=file, field="defects[].id",
220
+ message=f"缺陷 ID 重复:{defect_id}", severity="error"
221
+ ))
222
+ defect_ids.add(defect_id)
223
+
224
+ # 验证 category 引用
225
+ category = defect.get('category')
226
+ if category and category not in category_ids:
227
+ self.errors.append(ValidationError(
228
+ file=file, field=f"defects[{defect_id}].category",
229
+ message=f"引用不存在的分类:{category}", severity="error"
230
+ ))
231
+
232
+ # 验证 skill_routing
233
+ skill_routing = defect.get('fix_strategy', {}).get('primary_skill')
234
+ if skill_routing:
235
+ valid_skills = [
236
+ 'space-util-fixer', 'float-optimizer', 'consistency-polisher',
237
+ 'overflow-repair', 'template-migrator', 'visual-inspector', 'writing-polish'
238
+ ]
239
+ if skill_routing not in valid_skills:
240
+ self.warnings.append(ValidationError(
241
+ file=file, field=f"defects[{defect_id}].fix_strategy.primary_skill",
242
+ message=f"未知的技能:{skill_routing}", severity="warning"
243
+ ))
244
+
245
+ # 验证 skill_routing 一致性
246
+ skill_routing_cfg = config.get('skill_routing', {})
247
+ for skill, defect_list in skill_routing_cfg.items():
248
+ for defect_ref in defect_list:
249
+ if defect_ref not in defect_ids:
250
+ self.errors.append(ValidationError(
251
+ file=file, field=f"skill_routing.{skill}",
252
+ message=f"引用不存在的缺陷 ID: {defect_ref}", severity="error"
253
+ ))
254
+
255
+ def _validate_templates(self, file: str, config: Dict) -> None:
256
+ """验证 templates.yaml"""
257
+ templates = config.get('templates', {})
258
+ for name, template in templates.items():
259
+ if 'columns' in template:
260
+ cols = template['columns']
261
+ if cols not in [1, 2]:
262
+ self.errors.append(ValidationError(
263
+ file=file, field=f"templates.{name}.columns",
264
+ message=f"栏数必须是 1 或 2,当前值:{cols}", severity="error"
265
+ ))
266
+ if 'target_pages' in template:
267
+ pages = template['target_pages']
268
+ if pages < 1:
269
+ self.errors.append(ValidationError(
270
+ file=file, field=f"templates.{name}.target_pages",
271
+ message=f"页数必须大于 0,当前值:{pages}", severity="error"
272
+ ))
273
+
274
+ def _validate_writing_rules(self, file: str, config: Dict) -> None:
275
+ """验证 writing_rules.yaml"""
276
+ # 验证写作规则的基本结构
277
+ if 'forbidden_words' in config:
278
+ if not isinstance(config['forbidden_words'], list):
279
+ self.errors.append(ValidationError(
280
+ file=file, field="forbidden_words",
281
+ message="必须是列表类型", severity="error"
282
+ ))
283
+
284
+ if 'required_tense' in config:
285
+ tense = config['required_tense']
286
+ valid_tenses = ['present', 'past', 'present_perfect']
287
+ if tense not in valid_tenses:
288
+ self.errors.append(ValidationError(
289
+ file=file, field="required_tense",
290
+ message=f"无效的时态:{tense},有效值:{valid_tenses}",
291
+ severity="error"
292
+ ))
293
+
294
+ def _validate_agent_roles(self, file: str, config: Dict) -> None:
295
+ """验证 agent_roles.yaml"""
296
+ agents = config.get('agents', {})
297
+ for name, role in agents.items():
298
+ if 'description' not in role:
299
+ self.warnings.append(ValidationError(
300
+ file=file, field=f"agents.{name}",
301
+ message="缺少 description 字段", severity="warning"
302
+ ))
303
+ if 'tools' not in role:
304
+ self.warnings.append(ValidationError(
305
+ file=file, field=f"agents.{name}",
306
+ message="缺少 tools 字段", severity="warning"
307
+ ))
308
+
309
+
310
+ # ============================================================
311
+ # 主函数
312
+ # ============================================================
313
+
314
+ def main():
315
+ """主函数"""
316
+ import argparse
317
+
318
+ parser = argparse.ArgumentParser(description="PaperFit 配置验证器")
319
+ parser.add_argument('--verbose', '-v', action='store_true',
320
+ help='显示详细输出')
321
+ parser.add_argument('--config-dir', type=str, default=None,
322
+ help='配置文件目录')
323
+ args = parser.parse_args()
324
+
325
+ config_dir = Path(args.config_dir) if args.config_dir else CONFIG_DIR
326
+
327
+ validator = ConfigValidator(config_dir)
328
+ success, errors, warnings = validator.validate_all()
329
+
330
+ if args.verbose:
331
+ print("\n已加载的配置:")
332
+ for file, config in validator.loaded_configs.items():
333
+ print(f" - {file}: {len(str(config))} 字符")
334
+
335
+ sys.exit(0 if success else 1)
336
+
337
+
338
+ if __name__ == "__main__":
339
+ main()