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.
Files changed (99) hide show
  1. package/.dsh/skills/gongwen-skill/SKILL.md +3 -4
  2. package/.dsh/skills/gongwen-skill.md +3 -4
  3. package/CHANGELOG.md +41 -0
  4. package/README.md +7 -6
  5. package/SKILL.md +3 -4
  6. package/cordis.patch.yml +4 -0
  7. package/dsh/client.js +3 -1
  8. package/dsh/index.js +4 -2
  9. package/engine/__init__.py +1 -0
  10. package/engine/auto_optimizer.py +5 -0
  11. package/engine/chat_review.py +5 -0
  12. package/engine/config.py +1 -0
  13. package/engine/core/__init__.py +1 -0
  14. package/engine/core/document/__init__.py +1 -0
  15. package/engine/core/document/_generator_helpers.py +5 -0
  16. package/engine/core/document/ai_structure_analyzer.py +1 -0
  17. package/engine/core/document/annotator.py +5 -0
  18. package/engine/core/document/editor.py +6 -0
  19. package/engine/core/document/font_utils.py +1 -0
  20. package/engine/core/document/generator.py +1 -0
  21. package/engine/core/document/markdown_converter.py +61 -2
  22. package/engine/core/document/models.py +1 -0
  23. package/engine/core/document/modifier.py +20 -5
  24. package/engine/core/document/ooxml_parser.py +5 -0
  25. package/engine/core/document/ooxml_workflow.py +5 -0
  26. package/engine/core/document/parser.py +30 -5
  27. package/engine/core/document/parser_format.py +6 -0
  28. package/engine/core/document/reviewer_comments.py +5 -0
  29. package/engine/core/document/structure_analyzer.py +1 -0
  30. package/engine/core/document/tracked_annotator.py +5 -0
  31. package/engine/core/document/tracked_changes.py +5 -0
  32. package/engine/core/document/tracked_common.py +5 -0
  33. package/engine/core/rules/__init__.py +1 -0
  34. package/engine/core/rules/checker.py +230 -13
  35. package/engine/core/rules/engine.py +1 -0
  36. package/engine/core/rules/fixer.py +1 -0
  37. package/engine/core/rules/loader.py +1 -0
  38. package/engine/core/rules/manager.py +1 -0
  39. package/engine/docx_to_image.py +5 -0
  40. package/engine/fact_check.py +5 -0
  41. package/engine/focus_checker.py +5 -0
  42. package/engine/handoff.py +1 -0
  43. package/engine/inject.py +27 -9
  44. package/engine/live_edit.py +5 -0
  45. package/engine/optimizer.py +1 -1
  46. package/engine/review_generator.py +5 -0
  47. package/engine/structure_checker.py +5 -0
  48. package/engine/style_profile.py +5 -0
  49. package/engine/table_sign_generator.py +5 -0
  50. package/engine/table_sign_template.py +6 -0
  51. package/engine/template_builder.py +1 -0
  52. package/engine/utils/__init__.py +6 -0
  53. package/engine/utils/errors.py +5 -0
  54. package/engine/utils/logger.py +1 -0
  55. package/engine/utils/parse.py +5 -0
  56. package/engine/utils/zip_utils.py +5 -0
  57. package/gongwen/__init__.py +2 -1
  58. package/gongwen/__main__.py +1 -0
  59. package/gongwen/_bootstrap.py +6 -0
  60. package/gongwen/_legacy.py +32 -13
  61. package/gongwen/cli/__init__.py +1 -0
  62. package/gongwen/cli/content_cmds.py +6 -0
  63. package/gongwen/cli/doctor_cmds.py +6 -0
  64. package/gongwen/cli/font_cmds.py +7 -0
  65. package/gongwen/cli/helpers.py +7 -0
  66. package/gongwen/cli/misc_cmds.py +6 -0
  67. package/gongwen/cli/review_cmds.py +6 -0
  68. package/gongwen/cli/style_helpers.py +6 -0
  69. package/gongwen/cli/update_cmds.py +6 -0
  70. package/gongwen/md2docx_render.py +509 -0
  71. package/package.json +1 -1
  72. package/prompts/style-prompts.md +5 -4
  73. package/prompts/usage-prompts.md +7 -1
  74. package/pyproject.toml +1 -1
  75. package/rules/official/_common.yaml +7 -3
  76. package/rules/official/announcement.yaml +4 -0
  77. package/rules/official/bill.yaml +13 -0
  78. package/rules/official/bulletin.yaml +13 -0
  79. package/rules/official/command.yaml +4 -0
  80. package/rules/official/communique.yaml +21 -0
  81. package/rules/official/decision.yaml +4 -0
  82. package/rules/official/instruction.yaml +13 -0
  83. package/rules/official/letter.yaml +4 -0
  84. package/rules/official/meeting.yaml +13 -0
  85. package/rules/official/minutes.yaml +13 -0
  86. package/rules/official/news.yaml +50 -0
  87. package/rules/official/notice.yaml +27 -1
  88. package/rules/official/notice_public.yaml +4 -0
  89. package/rules/official/opinion.yaml +13 -0
  90. package/rules/official/regulation.yaml +13 -0
  91. package/rules/official/reply.yaml +13 -0
  92. package/rules/official/report.yaml +13 -0
  93. package/rules/official/request.yaml +4 -0
  94. package/rules/official/resolution.yaml +13 -0
  95. package/rules/official/speech.yaml +4 -0
  96. package/rules/official/summary.yaml +13 -0
  97. package/rules/official/table_sign.yaml +4 -0
  98. package/rules/official/technical_proposal.yaml +4 -0
  99. 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
- _EXCLUDE_ROLES = {'signature', 'date'}
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
- all_bold = all(r.format.bold for r in para.runs if r.text.strip())
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. Only check last 2 non-empty paragraphs (落款+日期)."""
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
- for para in sig_paras:
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
- for run in para.runs:
564
- if bool(run.format.bold) != bool(expected):
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
- if text and not getattr(p, 'is_title', False) \
649
- and role not in ('signature', 'date', 'annotation'):
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
- if actual and actual != str(expected).lower():
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.
@@ -1,4 +1,9 @@
1
1
  # -*- coding: utf-8 -*-
2
+ #
3
+ # (c) 2026 Jose AI (https://www.linhut.cn)
4
+ # https://github.com/linhut/gongwen-skill
5
+ # Licensed under the MIT License. See the LICENSE file for details.
6
+ #
2
7
  """
3
8
  docx 转图片可视化验证(借鉴 docx-skill 的文档转图片思路)。
4
9
 
@@ -1,4 +1,9 @@
1
1
  # -*- coding: utf-8 -*-
2
+ #
3
+ # (c) 2026 Jose AI (https://www.linhut.cn)
4
+ # https://github.com/linhut/gongwen-skill
5
+ # Licensed under the MIT License. See the LICENSE file for details.
6
+ #
2
7
  """
3
8
  事实核验模块 —— 对用户文档中的关键人事信息做主动交叉核验。
4
9
 
@@ -1,4 +1,9 @@
1
1
  # -*- coding: utf-8 -*-
2
+ #
3
+ # (c) 2026 Jose AI (https://www.linhut.cn)
4
+ # https://github.com/linhut/gongwen-skill
5
+ # Licensed under the MIT License. See the LICENSE file for details.
6
+ #
2
7
  """
3
8
  focus_checks 自动检查(v1.12.30 内容优化架构改进方案 改进 C)。
4
9
 
package/engine/handoff.py CHANGED
@@ -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
  会话交接文档管理(Handoff)。
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
- # 项目出处:AI 公文智能优化助手 (https://www.linhut.cn)
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
- existing_ids = [r.get('Id') for r in rel_root]
553
- if 'rIdFtrEven' not in existing_ids:
554
- rel = etree.SubElement(rel_root, '{%s}Relationship' % rel_ns)
555
- rel.set('Id', 'rIdFtrEven')
556
- rel.set('Type', 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/footer')
557
- rel.set('Target', 'footer2.xml')
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',
@@ -1,4 +1,9 @@
1
1
  # -*- coding: utf-8 -*-
2
+ #
3
+ # (c) 2026 Jose AI (https://www.linhut.cn)
4
+ # https://github.com/linhut/gongwen-skill
5
+ # Licensed under the MIT License. See the LICENSE file for details.
6
+ #
2
7
  """
3
8
  LiveEdit —— 内存级 DocumentModel 编辑会话。
4
9
 
@@ -3,7 +3,7 @@
3
3
  # 公文文档格式化 Skill —— 内容优化差异对比引擎
4
4
  #
5
5
  # (c) 2026 Jose AI (https://www.linhut.cn)
6
- # 项目出处:AI 公文智能优化助手 (https://www.linhut.cn)
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 文档。
@@ -1,4 +1,9 @@
1
1
  # -*- coding: utf-8 -*-
2
+ #
3
+ # (c) 2026 Jose AI (https://www.linhut.cn)
4
+ # https://github.com/linhut/gongwen-skill
5
+ # Licensed under the MIT License. See the LICENSE file for details.
6
+ #
2
7
  """
3
8
  审稿流转文书生成器 —— 为五角色审稿机制提供模板文档。
4
9
 
@@ -1,4 +1,9 @@
1
1
  # -*- coding: utf-8 -*-
2
+ #
3
+ # (c) 2026 Jose AI (https://www.linhut.cn)
4
+ # https://github.com/linhut/gongwen-skill
5
+ # Licensed under the MIT License. See the LICENSE file for details.
6
+ #
2
7
  """
3
8
  结构完整性检查(v1.12.30 内容优化架构改进方案 改进 B)。
4
9
 
@@ -1,4 +1,9 @@
1
1
  # -*- coding: utf-8 -*-
2
+ #
3
+ # (c) 2026 Jose AI (https://www.linhut.cn)
4
+ # https://github.com/linhut/gongwen-skill
5
+ # Licensed under the MIT License. See the LICENSE file for details.
6
+ #
2
7
  """
3
8
  样式学习引擎 —— 从标准 .docx 文档提取完整排版样式,生成自定义命名模板。
4
9
 
@@ -1,4 +1,9 @@
1
1
  # -*- coding: utf-8 -*-
2
+ #
3
+ # (c) 2026 Jose AI (https://www.linhut.cn)
4
+ # https://github.com/linhut/gongwen-skill
5
+ # Licensed under the MIT License. See the LICENSE file for details.
6
+ #
2
7
  """
3
8
  桌签批量生成器 —— 基于模板文件的桌签生成。
4
9
 
@@ -1,4 +1,10 @@
1
1
  # -*- coding: utf-8 -*-
2
+ #
3
+ # (c) 2026 Jose AI (https://www.linhut.cn)
4
+ # https://github.com/linhut/gongwen-skill
5
+ # Licensed under the MIT License. See the LICENSE file for details.
6
+ #
7
+ # -*- coding: utf-8 -*-
2
8
  """
3
9
  座签默认模板生成器(方案六 P2-2:内置默认模板)。
4
10
 
@@ -3,6 +3,7 @@
3
3
  # 公文文档格式化 Skill —— 模板生成器(独立发行版)
4
4
  #
5
5
  # (c) 2026 Jose AI (https://www.linhut.cn)
6
+ # https://github.com/linhut/gongwen-skill
6
7
  # Licensed under the MIT License. See the LICENSE file for details.
7
8
  #
8
9
  # 本模块从原项目 backend/api/routes/template_download.py 中抽取纯逻辑,
@@ -0,0 +1,6 @@
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # (c) 2026 Jose AI (https://www.linhut.cn)
4
+ # https://github.com/linhut/gongwen-skill
5
+ # Licensed under the MIT License. See the LICENSE file for details.
6
+ #
@@ -1,4 +1,9 @@
1
1
  # -*- coding: utf-8 -*-
2
+ #
3
+ # (c) 2026 Jose AI (https://www.linhut.cn)
4
+ # https://github.com/linhut/gongwen-skill
5
+ # Licensed under the MIT License. See the LICENSE file for details.
6
+ #
2
7
  """
3
8
  统一错误处理策略(跨模块#4 修复)。
4
9
 
@@ -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
  Logging configuration for the application.
@@ -1,4 +1,9 @@
1
1
  # -*- coding: utf-8 -*-
2
+ #
3
+ # (c) 2026 Jose AI (https://www.linhut.cn)
4
+ # https://github.com/linhut/gongwen-skill
5
+ # Licensed under the MIT License. See the LICENSE file for details.
6
+ #
2
7
  """
3
8
  统一值解析工具(跨模块#3 修复)。
4
9
 
@@ -1,4 +1,9 @@
1
1
  # -*- coding: utf-8 -*-
2
+ #
3
+ # (c) 2026 Jose AI (https://www.linhut.cn)
4
+ # https://github.com/linhut/gongwen-skill
5
+ # Licensed under the MIT License. See the LICENSE file for details.
6
+ #
2
7
  """
3
8
  公共 ZIP / OOXML 工具(优化方案 v1.12.25 E3)。
4
9
 
@@ -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.0.0"
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.
@@ -2,6 +2,7 @@
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
  # python -m gongwen entry point
@@ -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