gongwen-skill 2.13.0 → 2.15.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/AGENTS.md +0 -2
- package/CHANGELOG.md +33 -0
- package/README.md +2 -2
- package/SKILL.md +2 -2
- package/dsh/index.js +1 -1
- package/engine/core/document/models.py +30 -0
- package/engine/core/document/ooxml_parser.py +39 -0
- package/engine/core/document/parser.py +25 -5
- package/engine/core/rules/checker.py +14 -1
- package/gongwen/__init__.py +1 -1
- package/gongwen/_legacy.py +55 -2
- package/gongwen/cli/content_cmds.py +3 -0
- package/gongwen/cli/doctor_cmds.py +34 -7
- package/package.json +9 -3
- package/prompts/usage-prompts.md +1 -1
- package/pyproject.toml +1 -1
- package/rules/official/_common.yaml +10 -0
package/AGENTS.md
CHANGED
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,39 @@
|
|
|
4
4
|
Licensed under the MIT License. See the LICENSE file for details.
|
|
5
5
|
-->
|
|
6
6
|
|
|
7
|
+
## v2.15.0 (2026-10-01)
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
- **npm peer 依赖在 DSH 0.2.0-rc.2 宿主上安装失败(P1,GitHub issue #2「0.2.0 rc-2装上就报错」)**:DSH 宿主升级到 0.2.0-rc.2 线后,其捆绑的 `@deepseek-ai/dsh-tools@0.2.0-rc.2` 与 `@deepseek-ai/dsh-client-ui-settings-plugins@0.2.0-rc.2` 均不满足 v2.14.0 声明的 peer 范围(`>=0.1.5-rc.3` / `>=0.1.2-rc.1`)。根因是 node-semver 的预发布逐线放行规则:范围里带预发布版本(如 `0.1.5-rc.3`)的比较器,只允许**相同 [major,minor,patch] 元组**的预发布版本参与匹配,0.2.0 线因此整体不可达 → pnpm 解析 peer 报 `ERR_PNPM_NO_MATCHING_VERSION`(与 issue #1 同根,属宿主升线后的复发性问题)
|
|
11
|
+
- `@deepseek-ai/dsh-tools` peer 改为多线锚定:`>=0.1.2-alpha.1 || >=0.1.5-alpha.1 || >=0.1.7-alpha.1 || >=0.2.0-0`(实测覆盖 0.1.2/0.1.5/0.1.7/0.2.0 全部已发布预发布线,与 npm 全部 30 个已发布版本匹配 16 个关键线)
|
|
12
|
+
- `@deepseek-ai/dsh-client-ui-settings-plugins` peer 改为:`>=0.1.2-rc.1 || >=0.1.5-rc.1 || >=0.2.0-0`
|
|
13
|
+
- **新增 `peerDependenciesMeta` 将 4 个 `@deepseek-ai/*` peer 全部标为 `optional: true`**:DSH 桌面端市场安装后这些宿主单例原就会被移除、运行时统一走宿主共享副本(issue #1 已核实的安装模型),optional 化后安装期不再因 peer 版本线漂移失败,未来宿主再升线也不受影响;运行时 import 仍解析到宿主提供的副本(版本由宿主保证)
|
|
14
|
+
- `doctor`(O2 peer 检查)版本解析升级:`_parse_version_tuple` 支持 `-alpha.N` / `-0` 预发布标记,新增 `_peer_branch_minima` 按 `||` OR 分支保守判定(安装版本 ≥ 任一分支下限即满足),避免对多线范围误报
|
|
15
|
+
- **doctor O1 宿主版本检查兼容 0.2.0 线**:`_parse_version_tuple` 预发布段由仅识别 `-rc.N` 扩展为通用 `-<tag>N`(rc/alpha/裸数字),0.2.0-rc.2 正确判定 ≥ 0.1.2-rc.1,不再误报
|
|
16
|
+
|
|
17
|
+
### Notes
|
|
18
|
+
- 本版为 DSH 插件安装兼容性修复,CLI 功能无变化;纯 CLI 用户不受影响
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## v2.14.0 (2026-09-26)
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
- **解析读取可见性(V2.14)**:文本框内容(`w:txbxContent`,公文红头/发文字号常承载于此)与修订追踪摘要(`w:ins`/`w:del`)正式纳入 DocumentModel(`textboxes` / `revisions` 字段),`parse --json` 结构化输出——此前两者在解析层静默丢失(文本框仅挂非 Pydantic 属性、修订因 python-docx 高层 API 不读 `w:ins` 包裹 run 而整体丢失)
|
|
26
|
+
- **文本框内容检查规则 CHK-TBX001**:检测到文本框但内容为空时提示(红头/发文字号可能缺失);无文本框的文档不触发,避免误报
|
|
27
|
+
- **check/optimize 读取可见性提示**:文档含修订(含作者与增删数)或存在空文本框时,非 JSON 路径显式打印提示,不再静默忽略;`optimize --json` 新增 `textboxes` / `revisions` 字段供 Agent 感知解析降级
|
|
28
|
+
- **未知公文类型显式警告**:check/optimize 对 `-t` 拼错类型不再静默回退 common 规则,打印警告并提示 list-types(发布前全功能测试发现)
|
|
29
|
+
- **新增测试**:`tests/test_read_visibility.py`(文本框入模型、修订摘要、JSON 序列化、普通文档默认值、提示输出、CHK-TBX001、reviewers_count 回归等 7 用例,本地)
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
- **npm peer 依赖解析失败(P1,GitHub issue #1)**:`@deepseek-ai/dsh-tools` peer 下限由 `>=0.1.0` 修正为 `>=0.1.5-rc.3`。根因:该包在 npm 上仅发布 rc/alpha 预发布版本(latest tag = `0.0.1-rc.1`),而 npm semver 的预发布 tuple 规则使 `>=0.1.0`(无预发布比较器)无法匹配任何版本,导致 pnpm 安装报 `ERR_PNPM_NO_MATCHING_VERSION`;现对齐官方宿主配套版本(`@deepseek-ai/dsh-base ^0.1.5-rc.3` 传递提供的 dsh-tools)
|
|
33
|
+
- **optimize-content tracked 模式 NameError(P1,发布前全功能测试发现)**:`_run_tracked_mode` 打印「审稿角色」时引用从未赋值的 `reviewers_count`(O10 重构遗留),文档已生成但崩溃 rc=1;现从 `args.reviewers` 取值(full 默认 6 / quick 3),rc 恢复 0
|
|
34
|
+
- **SKILL.md 标题与对外展示名统一**:H1 由「公文文档格式化 Skill(GB/T 9704)」改为「公文全流程处理专家(GB/T 9704)」,三副本(根 / `.dsh/skills/gongwen-skill/SKILL.md` / `.dsh/skills/gongwen-skill.md`)已同步
|
|
35
|
+
- **AGENTS.md 尾部重复行清理**:删除重复的「内容由AI生成,仅供参考」一行
|
|
36
|
+
|
|
37
|
+
### Changed
|
|
38
|
+
- **README 下载量徽章动态化**:`Downloads-0` 硬编码徽章改为 PyPI 动态下载量徽章(`img.shields.io/pypi/dm/gongwen-skill`)
|
|
39
|
+
|
|
7
40
|
## v2.13.0 (2026-09-11)
|
|
8
41
|
|
|
9
42
|
### Added
|
package/README.md
CHANGED
|
@@ -18,7 +18,7 @@ Licensed under the MIT License. See the LICENSE file for details.
|
|
|
18
18
|

|
|
19
19
|

|
|
20
20
|

|
|
21
|
-

|
|
22
22
|
|
|
23
23
|
本 Skill 源自开源桌面项目 [AI 公文智能优化助手](https://github.com/linhut/document-ai-assistant),将其核心格式引擎抽取、剥离桌面端/数据库依赖后独立发行,支持公文的**模板建立、解析、规则检查、自动修复、内容优化、Markdown 转公文**全流程能力。同时原生集成 **DeepSeek Harness (DSH)** 技能系统,支持 DSH Agent 自动发现与加载。
|
|
24
24
|
|
|
@@ -30,7 +30,7 @@ Licensed under the MIT License. See the LICENSE file for details.
|
|
|
30
30
|
|------|------|------|
|
|
31
31
|
| 📋 列类型 | `list-types` | 列出 25 种支持的公文类型(新增主持词 host_speech;含新闻稿/讲话稿) |
|
|
32
32
|
| 🏗️ 模板生成 | `template` | 按类型生成 GB/T 9704 标准空白模板 |
|
|
33
|
-
| 🔍 解析 | `parse` | `.docx` → 结构化 DocumentModel |
|
|
33
|
+
| 🔍 解析 | `parse` | `.docx` → 结构化 DocumentModel(含文本框/修订摘要) |
|
|
34
34
|
| ✅ 格式检查 | `check` | 按国标检查,分级 P0/P1/P2(只读) |
|
|
35
35
|
| 🔧 格式修复 | `optimize` | 自动修复字体/字号/行距/页边距,输出合规文档;`--verify` 单命令闭环自动复查、P0 存在时退出码非 0;`--json` 结构化输出 |
|
|
36
36
|
| ✍️ **内容优化** | **`optimize-content`** | 内容润色:默认 **Word 原生修订+批注**(审阅面板逐条接受/拒绝),可选行内差异对比版;`--precheck` 预检 changes 与原文一致性、`--preset quick/full/review` 参数收敛 |
|
package/SKILL.md
CHANGED
|
@@ -19,7 +19,7 @@ metadata:
|
|
|
19
19
|
Licensed under the MIT License. See the LICENSE file for details.
|
|
20
20
|
-->
|
|
21
21
|
|
|
22
|
-
#
|
|
22
|
+
# 公文全流程处理专家(GB/T 9704)
|
|
23
23
|
|
|
24
24
|
---
|
|
25
25
|
|
|
@@ -900,7 +900,7 @@ python -m gongwen fix-common 文件.docx -o 成品.docx
|
|
|
900
900
|
|
|
901
901
|
| 命令 | 用途 | 最小用法 |
|
|
902
902
|
|------|------|---------|
|
|
903
|
-
| `parse` | .docx → 结构化 DocumentModel JSON | `python -m gongwen parse 公文.docx` |
|
|
903
|
+
| `parse` | .docx → 结构化 DocumentModel JSON(含文本框/修订摘要) | `python -m gongwen parse 公文.docx` |
|
|
904
904
|
| `check` | 按国标检查格式(只读,分级 P0/P1/P2) | `python -m gongwen check 公文.docx -t notice` |
|
|
905
905
|
| `audit` | 审计文档:检查删除线/加粗/AI 声明等痕迹 | `python -m gongwen audit 公文.docx` |
|
|
906
906
|
|
package/dsh/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// 公文全流程处理专家 - DSH plugin bridge (gongwen-skill, v2.
|
|
1
|
+
// 公文全流程处理专家 - DSH plugin bridge (gongwen-skill, v2.15.0+)
|
|
2
2
|
// (c) 2026 Jose AI (https://www.linhut.cn)
|
|
3
3
|
// https://github.com/linhut/gongwen-skill
|
|
4
4
|
// Licensed under the MIT License. See the LICENSE file for details.
|
|
@@ -122,6 +122,32 @@ class HeaderFooter(BaseModel):
|
|
|
122
122
|
has_page_number: bool = False # 是否包含页码域
|
|
123
123
|
|
|
124
124
|
|
|
125
|
+
# ---------------------------------------------------------------------------
|
|
126
|
+
# Read-visibility structures(V2.14:读取可见性)
|
|
127
|
+
# 公文红头/发文字号常承载于文本框(<w:txbxContent>),修订(<w:ins>/<w:del>)
|
|
128
|
+
# 是流转中的草稿核心信息——两者此前在解析层静默丢失,现正式纳入模型。
|
|
129
|
+
# ---------------------------------------------------------------------------
|
|
130
|
+
|
|
131
|
+
class TextBox(BaseModel):
|
|
132
|
+
"""文本框内容(红头、发文字号等常承载于文本框)。"""
|
|
133
|
+
index: int = 0
|
|
134
|
+
text: str = ""
|
|
135
|
+
paragraphs: list[str] = Field(default_factory=list)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class RevisionInfo(BaseModel):
|
|
139
|
+
"""文档修订追踪摘要(w:ins 新增 / w:del 删除)。"""
|
|
140
|
+
insert_count: int = 0
|
|
141
|
+
delete_count: int = 0
|
|
142
|
+
authors: list[str] = Field(default_factory=list)
|
|
143
|
+
inserted_snippets: list[str] = Field(default_factory=list) # 新增内容片段(前 5 条)
|
|
144
|
+
deleted_snippets: list[str] = Field(default_factory=list) # 删除内容片段(前 5 条)
|
|
145
|
+
|
|
146
|
+
@property
|
|
147
|
+
def has_revisions(self) -> bool:
|
|
148
|
+
return self.insert_count > 0 or self.delete_count > 0
|
|
149
|
+
|
|
150
|
+
|
|
125
151
|
# ---------------------------------------------------------------------------
|
|
126
152
|
# Page setup
|
|
127
153
|
# ---------------------------------------------------------------------------
|
|
@@ -170,6 +196,10 @@ class DocumentModel(BaseModel):
|
|
|
170
196
|
headers: list[HeaderFooter] = Field(default_factory=list)
|
|
171
197
|
footers: list[HeaderFooter] = Field(default_factory=list)
|
|
172
198
|
|
|
199
|
+
# V2.14:读取可见性——文本框内容与修订摘要(此前静默丢失)
|
|
200
|
+
textboxes: list[TextBox] = Field(default_factory=list)
|
|
201
|
+
revisions: RevisionInfo = Field(default_factory=RevisionInfo)
|
|
202
|
+
|
|
173
203
|
# Source file info
|
|
174
204
|
filename: Optional[str] = Field(default=None, exclude=True)
|
|
175
205
|
source_path: Optional[str] = Field(default=None, exclude=True)
|
|
@@ -156,3 +156,42 @@ class OOXMLParser:
|
|
|
156
156
|
def para_text_from_element(p_elem) -> str:
|
|
157
157
|
"""从 <w:p> 节点提取文本(NS4 修复:用 iter 遍历后代,与文本框解析一致)。"""
|
|
158
158
|
return ''.join(t.text or '' for t in p_elem.iter(f'{{{W}}}t'))
|
|
159
|
+
|
|
160
|
+
def parse_revisions(self, docx_path: str | Path) -> Dict[str, Any]:
|
|
161
|
+
"""
|
|
162
|
+
解析文档修订追踪(<w:ins> 新增 / <w:del> 删除)摘要。
|
|
163
|
+
|
|
164
|
+
V2.14:python-docx 高层 API 不读 w:ins 包裹的 run,导致修订内容在
|
|
165
|
+
解析层静默丢失——此处用 OOXML 树直接统计,让流转中的草稿修订可见。
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
dict: {insert_count, delete_count, authors[], inserted_snippets[], deleted_snippets[]}
|
|
169
|
+
"""
|
|
170
|
+
root = self._load_document_xml(docx_path)
|
|
171
|
+
authors: set[str] = set()
|
|
172
|
+
inserted: list[str] = []
|
|
173
|
+
deleted: list[str] = []
|
|
174
|
+
|
|
175
|
+
for ins in root.iter(f'{{{W}}}ins'):
|
|
176
|
+
a = ins.get(f'{{{W}}}author')
|
|
177
|
+
if a:
|
|
178
|
+
authors.add(a)
|
|
179
|
+
txt = ''.join(t.text or '' for t in ins.iter(f'{{{W}}}t'))
|
|
180
|
+
if txt.strip():
|
|
181
|
+
inserted.append(txt.strip())
|
|
182
|
+
|
|
183
|
+
for dl in root.iter(f'{{{W}}}del'):
|
|
184
|
+
a = dl.get(f'{{{W}}}author')
|
|
185
|
+
if a:
|
|
186
|
+
authors.add(a)
|
|
187
|
+
txt = ''.join(t.text or '' for t in dl.iter(f'{{{W}}}delText'))
|
|
188
|
+
if txt.strip():
|
|
189
|
+
deleted.append(txt.strip())
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
'insert_count': len(inserted),
|
|
193
|
+
'delete_count': len(deleted),
|
|
194
|
+
'authors': sorted(authors),
|
|
195
|
+
'inserted_snippets': inserted[:5],
|
|
196
|
+
'deleted_snippets': deleted[:5],
|
|
197
|
+
}
|
|
@@ -582,17 +582,37 @@ def parse_docx(file_path: Path | str) -> DocumentModel:
|
|
|
582
582
|
logger.info(f"Parsed: {len(paragraphs)} paragraphs, {len(tables)} tables, "
|
|
583
583
|
f"{len(headers)} headers, {len(footers)} footers")
|
|
584
584
|
|
|
585
|
-
# I7:
|
|
585
|
+
# I7/V2.14: 文本框(红头区域)内容正式入模型——此前仅挂非 Pydantic 属性,
|
|
586
|
+
# JSON 输出与 check/optimize 均不可见(静默丢失);现纳入 DocumentModel.textboxes
|
|
587
|
+
from engine.core.document.models import TextBox
|
|
586
588
|
try:
|
|
587
589
|
textboxes = ooxml.parse_textboxes(str(file_path))
|
|
590
|
+
model.textboxes = [
|
|
591
|
+
TextBox(index=tb.index, text=tb.text, paragraphs=tb.paragraphs)
|
|
592
|
+
for tb in textboxes
|
|
593
|
+
]
|
|
594
|
+
# 向后兼容 alias:仍挂到非标准属性(防外部模块引用)
|
|
595
|
+
object.__setattr__(model, '_textboxes', [tb.text for tb in textboxes])
|
|
596
|
+
object.__setattr__(model, '_textbox_paragraphs', [tb.paragraphs for tb in textboxes])
|
|
588
597
|
if textboxes:
|
|
589
|
-
object.__setattr__(model, '_textboxes', [tb.text for tb in textboxes])
|
|
590
|
-
object.__setattr__(model, '_textbox_paragraphs', [tb.paragraphs for tb in textboxes])
|
|
591
598
|
logger.info(f"Textboxes extracted: {len(textboxes)} (红头/文本框区域)")
|
|
592
|
-
else:
|
|
593
|
-
object.__setattr__(model, '_textboxes', [])
|
|
594
599
|
except Exception as e:
|
|
595
600
|
logger.debug(f"Textbox extraction skipped: {e}")
|
|
601
|
+
object.__setattr__(model, '_textboxes', [])
|
|
602
|
+
|
|
603
|
+
# V2.14: 修订追踪摘要(w:ins/w:del)——此前 python-docx 层静默丢弃
|
|
604
|
+
try:
|
|
605
|
+
rev = ooxml.parse_revisions(str(file_path))
|
|
606
|
+
model.revisions = type(model.revisions)(
|
|
607
|
+
insert_count=rev['insert_count'], delete_count=rev['delete_count'],
|
|
608
|
+
authors=rev['authors'],
|
|
609
|
+
inserted_snippets=rev['inserted_snippets'],
|
|
610
|
+
deleted_snippets=rev['deleted_snippets'],
|
|
611
|
+
)
|
|
612
|
+
if model.revisions.has_revisions:
|
|
613
|
+
logger.info(f"Revisions detected: {rev['insert_count']} ins / {rev['delete_count']} del")
|
|
614
|
+
except Exception as e:
|
|
615
|
+
logger.debug(f"Revision parse skipped: {e}")
|
|
596
616
|
|
|
597
617
|
# I7: 段落索引映射挂载(供批注锚定等下游使用)
|
|
598
618
|
object.__setattr__(model, '_para_index_map', para_index_map)
|
|
@@ -861,7 +861,20 @@ def _check_header_field(model, rule_id: str, severity: str, name: str,
|
|
|
861
861
|
field_name = field_path.split(".", 1)[1] if "." in field_path else ""
|
|
862
862
|
|
|
863
863
|
try:
|
|
864
|
-
if field_name == "
|
|
864
|
+
if field_name == "textbox":
|
|
865
|
+
# V2.14:文本框内容可见性(CHK-TBX001)——红头/发文字号常承载于文本框;
|
|
866
|
+
# 文本框存在但为空 → 提示缺失;无文本框 → 不报(避免误报)
|
|
867
|
+
tb_list = getattr(model, 'textboxes', None) or []
|
|
868
|
+
empty = [tb for tb in tb_list if not (tb.text or '').strip()]
|
|
869
|
+
if tb_list and empty:
|
|
870
|
+
issues.append(CheckIssue(
|
|
871
|
+
rule_id=rule_id, check_type="content", severity=severity,
|
|
872
|
+
name=name, location=f"textbox:{empty[0].index}",
|
|
873
|
+
original_text="",
|
|
874
|
+
suggested_fix=expected,
|
|
875
|
+
reason=message or f"检测到 {len(empty)} 个空文本框(红头/发文字号可能缺失)",
|
|
876
|
+
))
|
|
877
|
+
elif field_name == "doc_number":
|
|
865
878
|
# 令号模式:〔2026〕1号 / 第1号 / (2026)1号 / 2026年1号 等
|
|
866
879
|
all_text = ''.join((getattr(p, 'text', '') or '') for p in model.paragraphs)
|
|
867
880
|
has_doc_number = re.search(
|
package/gongwen/__init__.py
CHANGED
package/gongwen/_legacy.py
CHANGED
|
@@ -16,7 +16,7 @@ from gongwen.cli.helpers import (
|
|
|
16
16
|
parse_config_overrides as _parse_config_overrides,
|
|
17
17
|
load_rules_with_overrides as _load_rules_with_overrides,
|
|
18
18
|
)
|
|
19
|
-
__version__ = "2.
|
|
19
|
+
__version__ = "2.15.0"
|
|
20
20
|
# 版本号应与 gongwen/__init__.py 保持一致,每次发版同步更新
|
|
21
21
|
"""
|
|
22
22
|
中文公文全流程处理专家 —— 基于 GB/T 9704《党政机关公文格式》国家标准。
|
|
@@ -137,7 +137,12 @@ def cmd_parse(args):
|
|
|
137
137
|
# --json:强制完整 JSON 到 stdout(即使指定了 -o),Agent 可统一解析
|
|
138
138
|
print(text)
|
|
139
139
|
elif args.output:
|
|
140
|
-
|
|
140
|
+
parts = [f"{len(model.paragraphs)} 段落", f"{len(model.tables)} 表格"]
|
|
141
|
+
if model.textboxes:
|
|
142
|
+
parts.append(f"{len(model.textboxes)} 文本框")
|
|
143
|
+
if model.revisions.has_revisions:
|
|
144
|
+
parts.append(f"修订 {model.revisions.insert_count}+/{model.revisions.delete_count}-")
|
|
145
|
+
print(f"已解析: {args.output} ({', '.join(parts)})")
|
|
141
146
|
else:
|
|
142
147
|
print(text)
|
|
143
148
|
|
|
@@ -157,6 +162,9 @@ def cmd_check(args):
|
|
|
157
162
|
if args.severity:
|
|
158
163
|
issues = [i for i in issues if i.severity == args.severity]
|
|
159
164
|
|
|
165
|
+
# V2.14:未知公文类型显式警告(拼错类型不再静默回退 common)
|
|
166
|
+
_warn_unknown_doc_type(args.doc_type)
|
|
167
|
+
|
|
160
168
|
if args.json:
|
|
161
169
|
results = [{
|
|
162
170
|
"severity": i.severity, "rule_id": i.rule_id, "name": i.name,
|
|
@@ -173,6 +181,38 @@ def cmd_check(args):
|
|
|
173
181
|
for i in issues:
|
|
174
182
|
print(f" [{i.severity}] {i.rule_id}: {i.name} @ {i.location}")
|
|
175
183
|
print(f" 实际: {i.original_text} → 期望: {i.suggested_fix}")
|
|
184
|
+
# V2.14:读取可见性提示——文本框/修订不静默忽略
|
|
185
|
+
_print_read_visibility_hints(model)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _print_read_visibility_hints(model):
|
|
189
|
+
"""V2.14:解析层读取可见性提示。
|
|
190
|
+
|
|
191
|
+
文本框(红头/发文字号)与修订(w:ins/w:del)是公文流转的高频信息载体,
|
|
192
|
+
此前在解析层静默丢失;现在解析可见,这里向用户显式告知降级/注意事项。
|
|
193
|
+
"""
|
|
194
|
+
if model.textboxes:
|
|
195
|
+
empty = [tb for tb in model.textboxes if not (tb.text or '').strip()]
|
|
196
|
+
if empty:
|
|
197
|
+
print(f" [提示] 检测到 {len(empty)} 个空文本框(红头/发文字号可能缺失)")
|
|
198
|
+
if model.revisions.has_revisions:
|
|
199
|
+
authors = '、'.join(model.revisions.authors) or '未知'
|
|
200
|
+
print(f" [提示] 文档含修订 {model.revisions.insert_count} 处新增 / "
|
|
201
|
+
f"{model.revisions.delete_count} 处删除(作者: {authors});"
|
|
202
|
+
f"检查基于当前可见内容,建议先接受/拒绝修订后再处理")
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _warn_unknown_doc_type(doc_type):
|
|
206
|
+
"""V2.14:未知公文类型显式警告(此前静默回退 common 规则,拼错类型难发现)。"""
|
|
207
|
+
if not doc_type:
|
|
208
|
+
return None
|
|
209
|
+
from engine.core.rules.loader import list_available_types
|
|
210
|
+
known = list_available_types()
|
|
211
|
+
if doc_type not in known:
|
|
212
|
+
print(f" ⚠️ 警告: 未知公文类型「{doc_type}」,已回退使用通用规则"
|
|
213
|
+
f"(python -m gongwen list-types 查看 {len(known)} 类)")
|
|
214
|
+
return doc_type
|
|
215
|
+
return None
|
|
176
216
|
|
|
177
217
|
|
|
178
218
|
def cmd_optimize(args):
|
|
@@ -199,6 +239,9 @@ def cmd_optimize(args):
|
|
|
199
239
|
# 确定文档类型(共享辅助函数,优先 -t 参数,其次文件名推断)
|
|
200
240
|
doc_type, type_source = _detect_doc_type(input_path, args.doc_type)
|
|
201
241
|
|
|
242
|
+
# V2.14:未知公文类型显式警告(拼错类型不再静默回退 common)
|
|
243
|
+
_warn_unknown_doc_type(doc_type)
|
|
244
|
+
|
|
202
245
|
# 解析文档并检查
|
|
203
246
|
model = parse_docx(str(input_path))
|
|
204
247
|
issues = engine.check(model, doc_type)
|
|
@@ -232,6 +275,14 @@ def cmd_optimize(args):
|
|
|
232
275
|
"verify_executed": False, "verify_passed": False,
|
|
233
276
|
"verify_p0": None, "verify_p1": None, "verify_p2": None,
|
|
234
277
|
"verify_error": None,
|
|
278
|
+
# V2.14:读取可见性(文本框/修订)——供 Agent 感知解析降级
|
|
279
|
+
"textboxes": len(model.textboxes),
|
|
280
|
+
"revisions": ({
|
|
281
|
+
"has": True,
|
|
282
|
+
"insert": model.revisions.insert_count,
|
|
283
|
+
"delete": model.revisions.delete_count,
|
|
284
|
+
"authors": model.revisions.authors,
|
|
285
|
+
} if model.revisions.has_revisions else None),
|
|
235
286
|
}
|
|
236
287
|
|
|
237
288
|
# === 预览信息(--json 时不打印人类文本)===
|
|
@@ -249,6 +300,8 @@ def cmd_optimize(args):
|
|
|
249
300
|
print(f" - {i.name} @ {i.location}")
|
|
250
301
|
if layout_parts:
|
|
251
302
|
print(f"🎨 版式注入: {', '.join(layout_parts)}")
|
|
303
|
+
# V2.14:读取可见性提示(文本框/修订不静默忽略)
|
|
304
|
+
_print_read_visibility_hints(model)
|
|
252
305
|
|
|
253
306
|
if not args.apply:
|
|
254
307
|
if not is_json:
|
|
@@ -577,6 +577,9 @@ def _run_tracked_mode(args, changes, style_name, style_prompt, content_rules, do
|
|
|
577
577
|
authors = sorted({s.author for s in suggestions})
|
|
578
578
|
print(f" 审阅者: {', '.join(authors)}(可按审阅者筛选)")
|
|
579
579
|
# S2 修复:审稿角色显示映射(默认 6 角色不再误判为 3 角色)
|
|
580
|
+
# V2.14 修复:reviewers_count 此前从未赋值(O10 重构遗留 NameError,
|
|
581
|
+
# 导致 tracked 模式生成文档后 rc=1)——从 args.reviewers 取(full 默认 6、quick 3)
|
|
582
|
+
reviewers_count = int(getattr(args, 'reviewers', 6) or 6)
|
|
580
583
|
_role_display = {
|
|
581
584
|
3: "精简版(3角色)",
|
|
582
585
|
5: "完整版(5角色)",
|
|
@@ -717,17 +717,42 @@ def _check_display_name_consistency() -> dict:
|
|
|
717
717
|
|
|
718
718
|
|
|
719
719
|
def _parse_version_tuple(v: str):
|
|
720
|
-
"""解析 1.2.3 / 4 / 3.18.0 / 0.1.2-rc.1
|
|
720
|
+
"""解析 1.2.3 / 4 / 3.18.0 / 0.1.2-rc.1 / 0.1.2-alpha.1 / 0.1.2-0 → 可比较元组。
|
|
721
721
|
|
|
722
|
-
rc.N 预发布 <
|
|
722
|
+
rc.N / alpha.N / 预发布 0 < 同版本正式版(预发布取末尾数字段,正式版取 inf)。
|
|
723
723
|
无法解析返回 None(调用方按不满足处理,保守)。
|
|
724
724
|
"""
|
|
725
725
|
import re as _re
|
|
726
|
-
m = _re.match(r"^(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:-
|
|
726
|
+
m = _re.match(r"^(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:-([0-9A-Za-z.]+))?$", v.strip())
|
|
727
727
|
if not m:
|
|
728
728
|
return None
|
|
729
|
-
|
|
730
|
-
|
|
729
|
+
major, minor, patch = int(m.group(1)), int(m.group(2) or 0), int(m.group(3) or 0)
|
|
730
|
+
pre = m.group(4)
|
|
731
|
+
if pre is None:
|
|
732
|
+
return (major, minor, patch, float("inf"))
|
|
733
|
+
# 预发布取末尾数字段近似(rc.1→1、alpha.1→1、0→0、裸 rc→0),
|
|
734
|
+
# 仅用于 doctor 下限比较,不追求与 node-semver 逐线规则完全一致
|
|
735
|
+
seg = pre.split(".")[-1]
|
|
736
|
+
n = int(seg) if seg.isdigit() else 0
|
|
737
|
+
return (major, minor, patch, n)
|
|
738
|
+
|
|
739
|
+
|
|
740
|
+
def _peer_branch_minima(range_str: str) -> list:
|
|
741
|
+
"""把 peer 范围(可能含 `||` OR 多线)拆成各分支版本下限元组列表。
|
|
742
|
+
|
|
743
|
+
背景:DSH 宿主按 0.1.2 / 0.1.5 / 0.1.7 / 0.2.0 等预发布线迭代,
|
|
744
|
+
peer 范围需多线锚定(npm semver 预发布逐线放行),doctor 据此做保守检查:
|
|
745
|
+
安装版本 ≥ 任一分支下限即视为满足(比 node-semver 略宽松,宁可少报)。
|
|
746
|
+
"""
|
|
747
|
+
out = []
|
|
748
|
+
for branch in (range_str or "").split("||"):
|
|
749
|
+
b = branch.strip().lstrip(">=").strip()
|
|
750
|
+
if not b:
|
|
751
|
+
continue
|
|
752
|
+
t = _parse_version_tuple(b)
|
|
753
|
+
if t is not None:
|
|
754
|
+
out.append(t)
|
|
755
|
+
return out
|
|
731
756
|
|
|
732
757
|
|
|
733
758
|
def _dsh_profiles() -> list:
|
|
@@ -828,7 +853,7 @@ def _check_dsh_peer_deps() -> dict:
|
|
|
828
853
|
problems = []
|
|
829
854
|
checked = 0
|
|
830
855
|
for peer, minimum in peers.items():
|
|
831
|
-
|
|
856
|
+
branch_mins = _peer_branch_minima(minimum)
|
|
832
857
|
installed = []
|
|
833
858
|
for nm in nm_dirs:
|
|
834
859
|
pkg = nm / peer / "package.json"
|
|
@@ -844,7 +869,9 @@ def _check_dsh_peer_deps() -> dict:
|
|
|
844
869
|
checked += 1
|
|
845
870
|
for profile, ver in installed:
|
|
846
871
|
key = _parse_version_tuple(ver)
|
|
847
|
-
|
|
872
|
+
# 满足 = 不低于任一分支下限(多线 OR 保守判定)
|
|
873
|
+
ok = key is not None and any(key >= mn for mn in branch_mins)
|
|
874
|
+
if not ok:
|
|
848
875
|
problems.append(f"{peer}: {profile}={ver or '未知'}(要求 {minimum})")
|
|
849
876
|
if problems:
|
|
850
877
|
return {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gongwen-skill",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.15.0",
|
|
4
4
|
"description": "公文全流程处理专家 - GB/T 9704 格式检查/修复/内容优化/模板生成/版式注入",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dsh/index.js",
|
|
@@ -46,9 +46,15 @@
|
|
|
46
46
|
},
|
|
47
47
|
"peerDependencies": {
|
|
48
48
|
"@deepseek-ai/cordis": ">=4",
|
|
49
|
-
"@deepseek-ai/dsh-tools": ">=0.1.0",
|
|
49
|
+
"@deepseek-ai/dsh-tools": ">=0.1.2-alpha.1 || >=0.1.5-alpha.1 || >=0.1.7-alpha.1 || >=0.2.0-0",
|
|
50
50
|
"@deepseek-ai/schemastery": ">=3.18.0",
|
|
51
|
-
"@deepseek-ai/dsh-client-ui-settings-plugins": ">=0.1.2-rc.1"
|
|
51
|
+
"@deepseek-ai/dsh-client-ui-settings-plugins": ">=0.1.2-rc.1 || >=0.1.5-rc.1 || >=0.2.0-0"
|
|
52
|
+
},
|
|
53
|
+
"peerDependenciesMeta": {
|
|
54
|
+
"@deepseek-ai/cordis": { "optional": true },
|
|
55
|
+
"@deepseek-ai/dsh-tools": { "optional": true },
|
|
56
|
+
"@deepseek-ai/schemastery": { "optional": true },
|
|
57
|
+
"@deepseek-ai/dsh-client-ui-settings-plugins": { "optional": true }
|
|
52
58
|
},
|
|
53
59
|
"scripts": {
|
|
54
60
|
"prepack": "node scripts/clean-cache.js",
|
package/prompts/usage-prompts.md
CHANGED
package/pyproject.toml
CHANGED
|
@@ -501,6 +501,16 @@ check_rules:
|
|
|
501
501
|
expected: true
|
|
502
502
|
message: "整段加粗不符合公文规范,通常仅首句或点题词需要加粗"
|
|
503
503
|
|
|
504
|
+
# --- 文本框内容检查 (CHK-TBX001, V2.14) ---
|
|
505
|
+
# 公文红头/发文字号常承载于文本框(w:txbxContent);文本框存在但内容为空
|
|
506
|
+
# 说明红头区域有缺失。仅"检测到文本框"时触发,无文本框的文档不报。
|
|
507
|
+
- id: CHK-TBX001
|
|
508
|
+
name: "文本框内容检查"
|
|
509
|
+
severity: P2
|
|
510
|
+
field: header.textbox
|
|
511
|
+
expected: "文本框内容非空"
|
|
512
|
+
message: "检测到文本框但内容为空(红头/发文字号可能在文本框中缺失)"
|
|
513
|
+
|
|
504
514
|
# --- 主送机关格式检查 (CHK-C026) ---
|
|
505
515
|
- id: CHK-C026
|
|
506
516
|
name: "主送机关格式检查"
|