gongwen-skill 2.0.0 → 2.2.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.
- package/.dsh/skills/gongwen-skill/SKILL.md +3 -4
- package/.dsh/skills/gongwen-skill.md +3 -4
- package/CHANGELOG.md +41 -0
- package/README.md +7 -6
- package/SKILL.md +3 -4
- package/cordis.patch.yml +4 -0
- package/dsh/client.js +3 -1
- package/dsh/index.js +4 -2
- package/engine/__init__.py +1 -0
- package/engine/auto_optimizer.py +5 -0
- package/engine/chat_review.py +5 -0
- package/engine/config.py +1 -0
- package/engine/core/__init__.py +1 -0
- package/engine/core/document/__init__.py +1 -0
- package/engine/core/document/_generator_helpers.py +5 -0
- package/engine/core/document/ai_structure_analyzer.py +1 -0
- package/engine/core/document/annotator.py +5 -0
- package/engine/core/document/editor.py +6 -0
- package/engine/core/document/font_utils.py +1 -0
- package/engine/core/document/generator.py +1 -0
- package/engine/core/document/markdown_converter.py +61 -2
- package/engine/core/document/models.py +1 -0
- package/engine/core/document/modifier.py +20 -5
- package/engine/core/document/ooxml_parser.py +5 -0
- package/engine/core/document/ooxml_workflow.py +5 -0
- package/engine/core/document/parser.py +30 -5
- package/engine/core/document/parser_format.py +6 -0
- package/engine/core/document/reviewer_comments.py +5 -0
- package/engine/core/document/structure_analyzer.py +1 -0
- package/engine/core/document/tracked_annotator.py +5 -0
- package/engine/core/document/tracked_changes.py +5 -0
- package/engine/core/document/tracked_common.py +5 -0
- package/engine/core/rules/__init__.py +1 -0
- package/engine/core/rules/checker.py +230 -13
- package/engine/core/rules/engine.py +1 -0
- package/engine/core/rules/fixer.py +1 -0
- package/engine/core/rules/loader.py +1 -0
- package/engine/core/rules/manager.py +1 -0
- package/engine/docx_to_image.py +5 -0
- package/engine/fact_check.py +5 -0
- package/engine/focus_checker.py +5 -0
- package/engine/handoff.py +1 -0
- package/engine/inject.py +27 -9
- package/engine/live_edit.py +5 -0
- package/engine/optimizer.py +1 -1
- package/engine/review_generator.py +5 -0
- package/engine/structure_checker.py +5 -0
- package/engine/style_profile.py +5 -0
- package/engine/table_sign_generator.py +5 -0
- package/engine/table_sign_template.py +6 -0
- package/engine/template_builder.py +1 -0
- package/engine/utils/__init__.py +6 -0
- package/engine/utils/errors.py +5 -0
- package/engine/utils/logger.py +1 -0
- package/engine/utils/parse.py +5 -0
- package/engine/utils/zip_utils.py +5 -0
- package/gongwen/__init__.py +2 -1
- package/gongwen/__main__.py +1 -0
- package/gongwen/_bootstrap.py +6 -0
- package/gongwen/_legacy.py +32 -13
- package/gongwen/cli/__init__.py +1 -0
- package/gongwen/cli/content_cmds.py +6 -0
- package/gongwen/cli/doctor_cmds.py +6 -0
- package/gongwen/cli/font_cmds.py +7 -0
- package/gongwen/cli/helpers.py +7 -0
- package/gongwen/cli/misc_cmds.py +6 -0
- package/gongwen/cli/review_cmds.py +6 -0
- package/gongwen/cli/style_helpers.py +6 -0
- package/gongwen/cli/update_cmds.py +6 -0
- package/gongwen/md2docx_render.py +509 -0
- package/package.json +1 -1
- package/prompts/style-prompts.md +5 -4
- package/prompts/usage-prompts.md +7 -1
- package/pyproject.toml +1 -1
- package/rules/official/_common.yaml +7 -3
- package/rules/official/announcement.yaml +4 -0
- package/rules/official/bill.yaml +13 -0
- package/rules/official/bulletin.yaml +13 -0
- package/rules/official/command.yaml +4 -0
- package/rules/official/communique.yaml +21 -0
- package/rules/official/decision.yaml +4 -0
- package/rules/official/instruction.yaml +13 -0
- package/rules/official/letter.yaml +4 -0
- package/rules/official/meeting.yaml +13 -0
- package/rules/official/minutes.yaml +13 -0
- package/rules/official/news.yaml +50 -0
- package/rules/official/notice.yaml +27 -1
- package/rules/official/notice_public.yaml +4 -0
- package/rules/official/opinion.yaml +13 -0
- package/rules/official/regulation.yaml +13 -0
- package/rules/official/reply.yaml +13 -0
- package/rules/official/report.yaml +13 -0
- package/rules/official/request.yaml +4 -0
- package/rules/official/resolution.yaml +13 -0
- package/rules/official/speech.yaml +4 -0
- package/rules/official/summary.yaml +13 -0
- package/rules/official/table_sign.yaml +4 -0
- package/rules/official/technical_proposal.yaml +4 -0
- package/rules/official/work_plan.yaml +13 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# This file is part of the Official Document AI Assistant.
|
|
2
2
|
# (c) 2026 Jose AI (https://www.linhut.cn)
|
|
3
|
+
# https://github.com/linhut/gongwen-skill
|
|
3
4
|
# Licensed under the MIT License. See the LICENSE file for details.
|
|
4
5
|
"""
|
|
5
6
|
Format checker: validates a DocumentModel against loaded rules.
|
|
@@ -91,6 +92,16 @@ def check_document(model: DocumentModel, rules: dict[str, Any]) -> list[CheckIss
|
|
|
91
92
|
elif field_path.startswith("ending."):
|
|
92
93
|
# FIX-V153-02:ending.check 结语检查(CHK-N001/R001/RPT001/RP002/L002 等)
|
|
93
94
|
issues.extend(_check_ending(model, rule_id, severity, name, expected, message))
|
|
95
|
+
elif field_path.startswith("content."):
|
|
96
|
+
# P2-22 修复:content.* 内容要素检查(CHK-N003 等 37 条规则此前无分发分支,
|
|
97
|
+
# 全部"定义但从不执行"且每次 check 刷"未支持字段"告警)
|
|
98
|
+
issues.extend(_check_content_field(model, rule_id, severity, name, field_path,
|
|
99
|
+
expected, message))
|
|
100
|
+
elif field_path.startswith("header."):
|
|
101
|
+
# P2-23 修复:header.* 版头检查(CHK-CM002 令号、CHK-R003 主送机关)
|
|
102
|
+
# 此前无分发分支,规则定义但从不执行
|
|
103
|
+
issues.extend(_check_header_field(model, rule_id, severity, name, field_path,
|
|
104
|
+
expected, message))
|
|
94
105
|
else:
|
|
95
106
|
# P1-6 修复:删除 generic else 中的硬编码索引逻辑(model.paragraphs[0]/[1]
|
|
96
107
|
# 不一定是标题/正文,检查结果会指向错误段落),未识别的 field 直接 skip + warning
|
|
@@ -312,10 +323,32 @@ def _check_heading_level(model, rule_id, severity, name, field_path, expected, m
|
|
|
312
323
|
return issues
|
|
313
324
|
|
|
314
325
|
|
|
326
|
+
# P2-17:CHK-C030 辅助——纯标点 run 判定(不含任何正文内容的 run)
|
|
327
|
+
# 用字符集判断替代正则,避免 r'...\[...]' 无效转义 SyntaxWarning,且匹配更可靠
|
|
328
|
+
_PUNCT_ONLY_CHARS = set('。!?:;,、.…!?:;,()()""''《》〈〉【】[]')
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def _is_punct_only(text: str) -> bool:
|
|
332
|
+
"""判断 run 是否只含标点/空白(用于整段加粗判定时忽略标点 run)。"""
|
|
333
|
+
t = (text or "").strip()
|
|
334
|
+
if not t:
|
|
335
|
+
return True
|
|
336
|
+
return all(ch in _PUNCT_ONLY_CHARS or ch.isspace() for ch in t)
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def _count_sentence_end(text: str) -> int:
|
|
340
|
+
"""统计句末标点(。!? 以及英文 . ! ?)的数量,用于判断单句/多句段落。"""
|
|
341
|
+
if not text:
|
|
342
|
+
return 0
|
|
343
|
+
return sum(1 for ch in text if ch in '。!?.!?')
|
|
344
|
+
|
|
345
|
+
|
|
315
346
|
def _check_body(model, rule_id, severity, name, field_path, expected, message) -> list[CheckIssue]:
|
|
316
|
-
"""Check body paragraph formatting (excluding signature/date)."""
|
|
347
|
+
"""Check body paragraph formatting (excluding signature/date/annotation/recipient)."""
|
|
317
348
|
issues = []
|
|
318
|
-
|
|
349
|
+
# 顶格左对齐的段落(主送机关/称呼段、署名、日期、AI 声明批注)不属于正文,
|
|
350
|
+
# 不应套用正文的缩进/对齐/字体检查
|
|
351
|
+
_EXCLUDE_ROLES = {'signature', 'date', 'annotation', 'notes', 'recipient', 'salutation'}
|
|
319
352
|
body_paras = [p for p in model.paragraphs
|
|
320
353
|
if not p.is_heading and p.text.strip() and p.role not in _EXCLUDE_ROLES]
|
|
321
354
|
if not body_paras:
|
|
@@ -402,13 +435,23 @@ def _check_body(model, rule_id, severity, name, field_path, expected, message) -
|
|
|
402
435
|
elif sub_field == "bold_range":
|
|
403
436
|
# 检查正文段落是否整段加粗(通常只有首句/点题词应加粗)
|
|
404
437
|
if para.runs and para.text.strip():
|
|
405
|
-
|
|
438
|
+
# P2-17 修复:忽略纯标点 run(句号/逗号等不含正文的 run),
|
|
439
|
+
# 避免"首句加粗含句号"时标点 run 加粗触发整段加粗误报
|
|
440
|
+
_CONTENT_RUNS = [r for r in para.runs
|
|
441
|
+
if r.text.strip() and not _is_punct_only(r.text)]
|
|
442
|
+
if not _CONTENT_RUNS:
|
|
443
|
+
continue
|
|
444
|
+
all_bold = all(r.format.bold for r in _CONTENT_RUNS)
|
|
406
445
|
if all_bold:
|
|
407
446
|
# B-09(方案三):排除不应加粗的段落类型(称呼/导语/过渡/署名/会议日期等),
|
|
408
447
|
# 避免这些段落被误标为 body 后报告"整段加粗"问题造成噪音
|
|
409
448
|
from engine.core.document.modifier import should_bold_first_sentence
|
|
410
449
|
if not should_bold_first_sentence(para.text, para.role):
|
|
411
450
|
continue
|
|
451
|
+
# P2-17 修复:单句段落(仅 1 个句末标点)的首句加粗=整段加粗,
|
|
452
|
+
# 属于合理排版(首句即整段),不报;多句段落整段加粗才报
|
|
453
|
+
if _count_sentence_end(para.text) <= 1:
|
|
454
|
+
continue
|
|
412
455
|
issues.append(CheckIssue(
|
|
413
456
|
rule_id=rule_id, check_type="content", severity=severity,
|
|
414
457
|
name=name, location=f"paragraph:{para.index}",
|
|
@@ -462,17 +505,32 @@ def _check_page_setup(model, rule_id, severity, name, field_path, expected, mess
|
|
|
462
505
|
|
|
463
506
|
|
|
464
507
|
def _check_signature_area(model, rule_id, severity, name, field_path, expected, message, rules) -> list[CheckIssue]:
|
|
465
|
-
"""Check signature/date area formatting.
|
|
508
|
+
"""Check signature/date area formatting.
|
|
509
|
+
|
|
510
|
+
仅检查落款/日期段落(默认取最后 2 个非空段落):
|
|
511
|
+
- signature.* 字段只检查署名段(角色 signature,通常是倒数第 2 段)
|
|
512
|
+
- date.* 字段只检查日期段(角色 date,通常是最后 1 段)
|
|
513
|
+
避免把署名/日期规则同时套在两个段落上造成误报。
|
|
514
|
+
"""
|
|
466
515
|
issues = []
|
|
467
|
-
paras = [p for p in model.paragraphs if not p.is_heading and p.text.strip()
|
|
516
|
+
paras = [p for p in model.paragraphs if not p.is_heading and p.text.strip()
|
|
517
|
+
and p.role not in ('annotation', 'notes')]
|
|
468
518
|
if not paras:
|
|
469
519
|
return issues
|
|
470
520
|
|
|
471
|
-
# Signature area: only last 2 paragraphs (落款单位 + 日期)
|
|
472
|
-
sig_paras = paras[-2:] if len(paras) >= 2 else paras
|
|
473
521
|
sub_field = field_path.split(".", 1)[1] if "." in field_path else ""
|
|
522
|
+
is_date = field_path.startswith("date.")
|
|
474
523
|
|
|
475
|
-
|
|
524
|
+
# 按角色取签名段/日期段。仅当文档确实存在落款/日期时才检查,
|
|
525
|
+
# 避免把无落款的正文末段误判为签名/日期造成误报(位置回退不再使用)。
|
|
526
|
+
if is_date:
|
|
527
|
+
target = [p for p in paras if p.role == 'date']
|
|
528
|
+
else:
|
|
529
|
+
target = [p for p in paras if p.role == 'signature']
|
|
530
|
+
if not target:
|
|
531
|
+
return issues
|
|
532
|
+
|
|
533
|
+
for para in target:
|
|
476
534
|
if sub_field == "align":
|
|
477
535
|
if para.format.alignment and para.format.alignment != str(expected).lower():
|
|
478
536
|
issues.append(CheckIssue(
|
|
@@ -560,8 +618,11 @@ def _check_paragraph_type_field(model, rule_id, severity, name, field_path, expe
|
|
|
560
618
|
))
|
|
561
619
|
break
|
|
562
620
|
elif sub_field == "bold":
|
|
563
|
-
|
|
564
|
-
|
|
621
|
+
# 编号正文(一是/二是…):仅要求首句(首 run)加粗,其余正文不应加粗。
|
|
622
|
+
# 其余段落类型:按首 run 判断即可(段落级加粗风格由 bold_range 规则另行检查)。
|
|
623
|
+
_runs_to_check = para.runs[:1] if target == 'numbered_body' else para.runs[:1]
|
|
624
|
+
for run in _runs_to_check:
|
|
625
|
+
if run.format.bold is not None and bool(run.format.bold) != bool(expected):
|
|
565
626
|
issues.append(CheckIssue(
|
|
566
627
|
rule_id=rule_id, check_type="format", severity=severity,
|
|
567
628
|
name=name, location=f"paragraph:{para.index}",
|
|
@@ -645,8 +706,9 @@ def _check_ending(model, rule_id: str, severity: str, name: str,
|
|
|
645
706
|
text = text.strip()
|
|
646
707
|
role = getattr(p, 'role', '') or ''
|
|
647
708
|
# 排除标题、落款(signature/date)、批注(annotation)段——这些不是正文结语
|
|
648
|
-
|
|
649
|
-
|
|
709
|
+
# P2-22 修复:模型属性是 is_heading 而非 is_title,标题段不应纳入结语统计
|
|
710
|
+
if text and not getattr(p, 'is_heading', False) and role not in (
|
|
711
|
+
'signature', 'date', 'annotation'):
|
|
650
712
|
body_paras.append(text)
|
|
651
713
|
|
|
652
714
|
if not body_paras:
|
|
@@ -683,6 +745,159 @@ def _check_ending(model, rule_id: str, severity: str, name: str,
|
|
|
683
745
|
return issues
|
|
684
746
|
|
|
685
747
|
|
|
748
|
+
# P2-22:content.* 内容要素检查——字段名 → 要素关键词组(宽松匹配,高召回低误报)
|
|
749
|
+
_CONTENT_FIELD_KEYWORDS = {
|
|
750
|
+
"notice_items": ["时间", "地点", "要求", "请", "须", "要", "参加", "召开", "举办",
|
|
751
|
+
"组织", "开展", "落实", "遵守", "上报", "报送", "日期", "人员", "对象"],
|
|
752
|
+
"scope": ["范围", "适用于", "各", "单位", "部门", "地区", "辖区"],
|
|
753
|
+
"effective_date": ["自", "起施行", "施行", "生效", "即日", "之日起", "起执行"],
|
|
754
|
+
"validity": ["有效", "期限", "至", "自", "起"],
|
|
755
|
+
"meeting_elements": ["会议", "时间", "地点", "参加", "人员", "议题", "议程"],
|
|
756
|
+
"meeting_info": ["会议", "时间", "地点", "参加", "纪要", "议题"],
|
|
757
|
+
"reason": ["因", "由于", "为了", "鉴于", "依据", "根据"],
|
|
758
|
+
"basis": ["依据", "根据", "按照", "遵照"],
|
|
759
|
+
"purpose": ["为了", "为", "目的", "促进", "推动"],
|
|
760
|
+
"measures": ["措施", "办法", "方案", "要求", "应当", "应", "须"],
|
|
761
|
+
"proposer": ["提出", "建议", "提议", "呈报", "申报"],
|
|
762
|
+
"facts": ["事实", "情况", "经查", "查明", "核实"],
|
|
763
|
+
"items": ["事项", "内容", "如下", "包括", "如下"],
|
|
764
|
+
"legal_basis": ["依据", "根据", "依照", "按照", "法规", "条例"],
|
|
765
|
+
"decision_items": ["决定", "如下", "事项", "内容"],
|
|
766
|
+
"clauses": ["条", "款", "项", "规定", "如下"],
|
|
767
|
+
"structure": ["结构", "如下", "部分", "章节"],
|
|
768
|
+
"data": ["数据", "统计", "指标", "数字", "情况"],
|
|
769
|
+
"suggestions": ["建议", "意见", "应", "应当", "建议如下"],
|
|
770
|
+
"report_items": ["情况", "报告", "如下", "内容"],
|
|
771
|
+
"reply_to": ["关于", "收悉", "来函", "贵", "你"],
|
|
772
|
+
"attitude": ["同意", "不同意", "原则同意", "批准"],
|
|
773
|
+
"single_topic": ["一", "单一", "专项"],
|
|
774
|
+
"resolution_items": ["决定", "如下", "事项"],
|
|
775
|
+
"procedure": ["程序", "步骤", "按照", "流程"],
|
|
776
|
+
"background": ["背景", "概述", "现状", "问题"],
|
|
777
|
+
"alternatives": ["方案", "备选", "比较", "选项"],
|
|
778
|
+
"implementation_plan": ["实施", "计划", "进度", "安排", "时间表"],
|
|
779
|
+
"objectives": ["目标", "目的", "要求"],
|
|
780
|
+
"timeline": ["时间", "阶段", "进度", "月", "年", "日"],
|
|
781
|
+
"responsibilities": ["责任", "负责", "分工", "单位"],
|
|
782
|
+
"lead": ["导语", "开头", "首先"],
|
|
783
|
+
"source": ["来源", "据", "报道"],
|
|
784
|
+
"report_section": ["部分", "章节", "如下"],
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
|
|
788
|
+
def _check_content_field(model, rule_id: str, severity: str, name: str,
|
|
789
|
+
field_path: str, expected: str, message: str) -> list[CheckIssue]:
|
|
790
|
+
"""P2-22 修复:content.* 内容要素检查(此前所有该前缀规则被跳过并告警)。
|
|
791
|
+
|
|
792
|
+
策略:按字段名尾部映射要素关键词组,检查正文(body 段落)是否包含任一组关键词。
|
|
793
|
+
- 宽松匹配:命中任一关键词即通过(避免误报)
|
|
794
|
+
- 无法映射的字段名保持跳过(返回空列表,不产生告警噪音)
|
|
795
|
+
"""
|
|
796
|
+
issues = []
|
|
797
|
+
field_name = field_path.split(".", 1)[1] if "." in field_path else ""
|
|
798
|
+
keywords = _CONTENT_FIELD_KEYWORDS.get(field_name)
|
|
799
|
+
if not keywords:
|
|
800
|
+
return issues
|
|
801
|
+
|
|
802
|
+
try:
|
|
803
|
+
# 收集正文文本(排除标题/落款/日期/批注/称呼段)
|
|
804
|
+
body_texts = []
|
|
805
|
+
for p in model.paragraphs:
|
|
806
|
+
text = (getattr(p, 'text', '') or '').strip()
|
|
807
|
+
role = getattr(p, 'role', '') or ''
|
|
808
|
+
# P2-22 修复:模型属性是 is_heading 而非 is_title——原 is_title 永远为
|
|
809
|
+
# False,导致标题段被误纳入正文统计(标题中的"召开/组织"等词会使
|
|
810
|
+
# 空壳通知误判为"有要素"而漏报 CHK-N003)
|
|
811
|
+
if text and not getattr(p, 'is_heading', False) and role not in (
|
|
812
|
+
'signature', 'date', 'annotation', 'recipient', 'salutation'):
|
|
813
|
+
body_texts.append(text)
|
|
814
|
+
joined = ''.join(body_texts)
|
|
815
|
+
if not joined:
|
|
816
|
+
return issues
|
|
817
|
+
|
|
818
|
+
found = any(kw in joined for kw in keywords)
|
|
819
|
+
if not found:
|
|
820
|
+
issues.append(CheckIssue(
|
|
821
|
+
rule_id=rule_id,
|
|
822
|
+
check_type="content",
|
|
823
|
+
severity=severity,
|
|
824
|
+
name=name,
|
|
825
|
+
location="正文",
|
|
826
|
+
original_text=joined[-80:] if joined else "",
|
|
827
|
+
suggested_fix=expected,
|
|
828
|
+
reason=message or f"文档正文缺少相关要素(期望: {expected})",
|
|
829
|
+
))
|
|
830
|
+
except Exception as e:
|
|
831
|
+
logger.warning(f"_check_content_field 检查失败: {e}")
|
|
832
|
+
|
|
833
|
+
return issues
|
|
834
|
+
|
|
835
|
+
|
|
836
|
+
def _check_header_field(model, rule_id: str, severity: str, name: str,
|
|
837
|
+
field_path: str, expected: str, message: str) -> list[CheckIssue]:
|
|
838
|
+
"""P2-23 修复:header.* 版头检查(此前规则定义但从不执行)。
|
|
839
|
+
|
|
840
|
+
支持字段:
|
|
841
|
+
- header.doc_number(CHK-CM002 命令):检查文档是否标注令号(如"〔2026〕1号"、"第1号")
|
|
842
|
+
- header.recipient(CHK-R003 请示):检查是否含主送机关段,且只写一个主送机关
|
|
843
|
+
"""
|
|
844
|
+
import re
|
|
845
|
+
issues = []
|
|
846
|
+
field_name = field_path.split(".", 1)[1] if "." in field_path else ""
|
|
847
|
+
|
|
848
|
+
try:
|
|
849
|
+
if field_name == "doc_number":
|
|
850
|
+
# 令号模式:〔2026〕1号 / 第1号 / (2026)1号 / 2026年1号 等
|
|
851
|
+
all_text = ''.join((getattr(p, 'text', '') or '') for p in model.paragraphs)
|
|
852
|
+
has_doc_number = re.search(
|
|
853
|
+
r'[〔((]?\d{4}[〕))]?\s*\d+号|[第]\d+号|令\d+号|\d+号令',
|
|
854
|
+
all_text,
|
|
855
|
+
) is not None
|
|
856
|
+
if not has_doc_number:
|
|
857
|
+
issues.append(CheckIssue(
|
|
858
|
+
rule_id=rule_id, check_type="content", severity=severity,
|
|
859
|
+
name=name, location="文档开头",
|
|
860
|
+
original_text=all_text[:60] if all_text else "",
|
|
861
|
+
suggested_fix=expected,
|
|
862
|
+
reason=message or "命令(令)应标注令号",
|
|
863
|
+
))
|
|
864
|
+
elif field_name == "recipient":
|
|
865
|
+
# 主送机关:存在 recipient 角色的段,且应只有一个主送机关
|
|
866
|
+
recips = [p for p in model.paragraphs
|
|
867
|
+
if getattr(p, 'role', '') == 'recipient' and (p.text or '').strip()]
|
|
868
|
+
if not recips:
|
|
869
|
+
issues.append(CheckIssue(
|
|
870
|
+
rule_id=rule_id, check_type="content", severity=severity,
|
|
871
|
+
name=name, location="文档开头",
|
|
872
|
+
original_text="",
|
|
873
|
+
suggested_fix=expected,
|
|
874
|
+
reason=message or "请示应写明主送机关",
|
|
875
|
+
))
|
|
876
|
+
return issues
|
|
877
|
+
# 只写一个主送机关:一个 recipient 段 + 段内无多个机关分隔符
|
|
878
|
+
first = recips[0].text
|
|
879
|
+
if len(recips) > 1:
|
|
880
|
+
issues.append(CheckIssue(
|
|
881
|
+
rule_id=rule_id, check_type="content", severity=severity,
|
|
882
|
+
name=name, location=f"paragraph:{recips[0].index}",
|
|
883
|
+
original_text=first[:60],
|
|
884
|
+
suggested_fix="仅保留一个主送机关",
|
|
885
|
+
reason="请示一般只写一个主送机关(发现多个主送机关段)",
|
|
886
|
+
))
|
|
887
|
+
elif re.search(r'[、,,s]{2,}', first.strip().rstrip('::')) and len(first.strip().rstrip('::')) > 5:
|
|
888
|
+
issues.append(CheckIssue(
|
|
889
|
+
rule_id=rule_id, check_type="content", severity=severity,
|
|
890
|
+
name=name, location=f"paragraph:{recips[0].index}",
|
|
891
|
+
original_text=first[:60],
|
|
892
|
+
suggested_fix="仅保留一个主送机关",
|
|
893
|
+
reason="请示一般只写一个主送机关(段内含多个机关)",
|
|
894
|
+
))
|
|
895
|
+
except Exception as e:
|
|
896
|
+
logger.warning(f"_check_header_field 检查失败: {e}")
|
|
897
|
+
|
|
898
|
+
return issues
|
|
899
|
+
|
|
900
|
+
|
|
686
901
|
def _check_page_number(model, rule_id, severity, name, field_path, expected, message) -> list[CheckIssue]:
|
|
687
902
|
"""检查页脚页码域格式(P3-10:CHK-C023/024/029)。
|
|
688
903
|
|
|
@@ -710,7 +925,9 @@ def _check_page_number(model, rule_id, severity, name, field_path, expected, mes
|
|
|
710
925
|
break
|
|
711
926
|
elif sub_field == "alignment" or sub_field == "align":
|
|
712
927
|
actual = para.format.alignment
|
|
713
|
-
|
|
928
|
+
# GB/T 9704 允许"居中"或"翻页模式(单右双左,默认 footer 为 right/left)"。
|
|
929
|
+
# 翻页模式下默认 footer 对齐为 right(单页右空一字),不再强制 center。
|
|
930
|
+
if actual and actual not in ('center', 'left', 'right'):
|
|
714
931
|
issues.append(CheckIssue(
|
|
715
932
|
rule_id=rule_id, check_type="format", severity=severity,
|
|
716
933
|
name=name, location=f"footer:{hf.section_index}:paragraph:{para.index}",
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# This file is part of the Official Document AI Assistant.
|
|
2
2
|
# (c) 2026 Jose AI (https://www.linhut.cn)
|
|
3
|
+
# https://github.com/linhut/gongwen-skill
|
|
3
4
|
# Licensed under the MIT License. See the LICENSE file for details.
|
|
4
5
|
"""
|
|
5
6
|
Rule engine: orchestrates loading, checking, and fixing using the rule system.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# This file is part of the Official Document AI Assistant.
|
|
2
2
|
# (c) 2026 Jose AI (https://www.linhut.cn)
|
|
3
|
+
# https://github.com/linhut/gongwen-skill
|
|
3
4
|
# Licensed under the MIT License. See the LICENSE file for details.
|
|
4
5
|
"""
|
|
5
6
|
Rule-based fixer: interprets YAML fix_rules and delegates to DocumentModifier.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# This file is part of the Official Document AI Assistant.
|
|
2
2
|
# (c) 2026 Jose AI (https://www.linhut.cn)
|
|
3
|
+
# https://github.com/linhut/gongwen-skill
|
|
3
4
|
# Licensed under the MIT License. See the LICENSE file for details.
|
|
4
5
|
"""
|
|
5
6
|
YAML rule file loader. Reads rule definitions from the rules/ directory.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# This file is part of the Official Document AI Assistant.
|
|
2
2
|
# (c) 2026 Jose AI (https://www.linhut.cn)
|
|
3
|
+
# https://github.com/linhut/gongwen-skill
|
|
3
4
|
# Licensed under the MIT License. See the LICENSE file for details.
|
|
4
5
|
"""
|
|
5
6
|
Rule Manager: unified rule loading with priority layering.
|
package/engine/docx_to_image.py
CHANGED
package/engine/fact_check.py
CHANGED
package/engine/focus_checker.py
CHANGED
package/engine/handoff.py
CHANGED
package/engine/inject.py
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
# 公文文档格式化 Skill —— 版头 / 版记 / 页码注入(独立发行版)
|
|
4
4
|
#
|
|
5
5
|
# (c) 2026 Jose AI (https://www.linhut.cn)
|
|
6
|
-
#
|
|
6
|
+
# https://github.com/linhut/gongwen-skill
|
|
7
7
|
# Licensed under the MIT License. See the LICENSE file for details.
|
|
8
8
|
#
|
|
9
9
|
# 本模块从原项目 backend/api/routes/optimize.py 中抽取版头/版记/页码
|
|
@@ -485,7 +485,13 @@ def _apply_page_number_elements(para_elem, elements: list) -> None:
|
|
|
485
485
|
|
|
486
486
|
|
|
487
487
|
def _inject_even_page_footer_direct(output_path: str, fmt: str, font_name: str, size_pt: int) -> None:
|
|
488
|
-
"""直接操作 ZIP,添加偶数页页脚和 evenAndOddHeaders(单双页奇偶排版)。
|
|
488
|
+
"""直接操作 ZIP,添加偶数页页脚和 evenAndOddHeaders(单双页奇偶排版)。
|
|
489
|
+
|
|
490
|
+
幂等性(I19 修复):重复调用(如 md2docx 后再跑 pagenum/optimize --layout)
|
|
491
|
+
不会累积 even footerReference / 关系条目 / footer2.xml 重复条目——
|
|
492
|
+
此前重复调用会在 document.xml 留下多个 even footerReference、rels 关系缺失、
|
|
493
|
+
ZIP 出现重复 word/footer2.xml,导致 Word 报"文档损坏"。
|
|
494
|
+
"""
|
|
489
495
|
import zipfile
|
|
490
496
|
import io
|
|
491
497
|
from lxml import etree
|
|
@@ -502,6 +508,7 @@ def _inject_even_page_footer_direct(output_path: str, fmt: str, font_name: str,
|
|
|
502
508
|
elems = _build_page_number_xml(fmt, font_name, size_pt)
|
|
503
509
|
_apply_page_number_elements(even_p, elems)
|
|
504
510
|
|
|
511
|
+
even_rid = 'rIdFtrEven'
|
|
505
512
|
buf = io.BytesIO()
|
|
506
513
|
with zipfile.ZipFile(output_path, 'r') as zin:
|
|
507
514
|
with zipfile.ZipFile(buf, 'w', zipfile.ZIP_DEFLATED) as zout:
|
|
@@ -516,8 +523,12 @@ def _inject_even_page_footer_direct(output_path: str, fmt: str, font_name: str,
|
|
|
516
523
|
root = etree.fromstring(data)
|
|
517
524
|
ns_w = 'http://schemas.openxmlformats.org/wordprocessingml/2006/main'
|
|
518
525
|
ns_r = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships'
|
|
526
|
+
# I19 幂等:先移除所有已有的 even footerReference,避免重复累积
|
|
527
|
+
for sp in root.findall('.//{%s}sectPr' % ns_w):
|
|
528
|
+
for ref in list(sp.findall('{%s}footerReference' % ns_w)):
|
|
529
|
+
if ref.get('{%s}type' % ns_w) == 'even':
|
|
530
|
+
sp.remove(ref)
|
|
519
531
|
# NS11 修复:动态分配偶数页页脚关系 ID,避免与已有 ID 冲突
|
|
520
|
-
even_rid = 'rIdFtrEven'
|
|
521
532
|
used_ids = {ref.get('{%s}id' % ns_r) for sp in root.findall('.//{%s}sectPr' % ns_w)
|
|
522
533
|
for ref in sp.findall('{%s}footerReference' % ns_w)}
|
|
523
534
|
n = 0
|
|
@@ -549,14 +560,21 @@ def _inject_even_page_footer_direct(output_path: str, fmt: str, font_name: str,
|
|
|
549
560
|
# I15 修复:结构化 XML 编辑(替代纯文本替换,避免破坏结构)
|
|
550
561
|
rel_root = etree.fromstring(data)
|
|
551
562
|
rel_ns = 'http://schemas.openxmlformats.org/package/2006/relationships'
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
rel.
|
|
556
|
-
|
|
557
|
-
|
|
563
|
+
# I19 幂等:清理本工具遗留的偶数页页脚关系(rIdFtrEven / rIdFtrEvenN),
|
|
564
|
+
# 避免残留关系堆积;再按实际 even_rid 写入唯一关系
|
|
565
|
+
for rel in list(rel_root):
|
|
566
|
+
rid = rel.get('Id') or ''
|
|
567
|
+
if rid == 'rIdFtrEven' or rid.startswith('rIdFtrEven'):
|
|
568
|
+
rel_root.remove(rel)
|
|
569
|
+
rel = etree.SubElement(rel_root, '{%s}Relationship' % rel_ns)
|
|
570
|
+
rel.set('Id', even_rid)
|
|
571
|
+
rel.set('Type', 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/footer')
|
|
572
|
+
rel.set('Target', 'footer2.xml')
|
|
558
573
|
zout.writestr(item.filename,
|
|
559
574
|
etree.tostring(rel_root, xml_declaration=True, encoding='UTF-8', standalone=True))
|
|
575
|
+
elif item.filename == 'word/footer2.xml':
|
|
576
|
+
# I19 幂等:跳过旧 footer2.xml,避免 ZIP 出现重复条目
|
|
577
|
+
continue
|
|
560
578
|
else:
|
|
561
579
|
zout.writestr(item.filename, data)
|
|
562
580
|
zout.writestr('word/footer2.xml',
|
package/engine/live_edit.py
CHANGED
package/engine/optimizer.py
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
# 公文文档格式化 Skill —— 内容优化差异对比引擎
|
|
4
4
|
#
|
|
5
5
|
# (c) 2026 Jose AI (https://www.linhut.cn)
|
|
6
|
-
#
|
|
6
|
+
# https://github.com/linhut/gongwen-skill
|
|
7
7
|
# Licensed under the MIT License. See the LICENSE file for details.
|
|
8
8
|
#
|
|
9
9
|
# 功能:将原文段落与优化后段落进行句子级 diff,生成带标注的 .docx 文档。
|
package/engine/style_profile.py
CHANGED
package/engine/utils/__init__.py
CHANGED
package/engine/utils/errors.py
CHANGED
package/engine/utils/logger.py
CHANGED
package/engine/utils/parse.py
CHANGED
package/gongwen/__init__.py
CHANGED
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
# -*- coding: utf-8 -*-
|
|
3
3
|
#
|
|
4
4
|
# (c) 2026 Jose AI (https://www.linhut.cn)
|
|
5
|
+
# https://github.com/linhut/gongwen-skill
|
|
5
6
|
# Licensed under the MIT License. See the LICENSE file for details.
|
|
6
7
|
#
|
|
7
8
|
# 公文全流程处理工具 - gongwen-skill Python package
|
|
8
9
|
|
|
9
|
-
__version__ = "2.
|
|
10
|
+
__version__ = "2.2.0"
|
|
10
11
|
|
|
11
12
|
# Re-export everything from the legacy module for backward compatibility
|
|
12
13
|
# This allows: from gongwen import main, cmd_check, etc.
|
package/gongwen/__main__.py
CHANGED
package/gongwen/_bootstrap.py
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
+
|
|
2
3
|
# -*- coding: utf-8 -*-
|
|
4
|
+
#
|
|
5
|
+
# (c) 2026 Jose AI (https://www.linhut.cn)
|
|
6
|
+
# https://github.com/linhut/gongwen-skill
|
|
7
|
+
# Licensed under the MIT License. See the LICENSE file for details.
|
|
8
|
+
#
|
|
3
9
|
"""
|
|
4
10
|
gongwen._bootstrap —— 统一的引擎路径引导和编码设置。
|
|
5
11
|
|