From 14ac1cd768c321e2c13ddd96169a193db529ac3f Mon Sep 17 00:00:00 2001 From: snowgitea Date: Fri, 11 Sep 2026 23:31:48 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20doc-writing-standard=20v1.7.0=EF=BC=88?= =?UTF-8?q?=E9=A6=96=E6=AC=A1=E7=BA=B3=E5=85=A5=E7=89=88=E6=9C=AC=E6=8E=A7?= =?UTF-8?q?=E5=88=B6=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 全流程(六阶段):0-A 八类资料登记表 → 0-B 三张清单(涉及/不涉及/待确认) → 0-C 模板骨架与逐小节饱满度分档 → 0-D 骨架核对 + 公开渠道补全 → 0-E 来源三元组 → 分批成文 → 人工审校 → 交付前一致性门禁 正文写作铁律 7.1–7.5: - 7.1 来源分级 A/B/C(事实 / 编制口径 / 合理推演) - 7.2 用语正反对照表(禁用「详见第 X 章」「按行业通行做法」「与附件 X 口径一致」等) - 7.3 扩写两方向(向前找证据 / 向后找影响),不横向注水 - 7.4 饱满度四档:D 一句话 ≤50 字 / C 交代 ≥100 字 / B 论证 ≥200 字 / A 清单(不看字数) - 7.4.1 三问定档流程、7.4.2 模板小节标题 → 档位速查表、7.4.3《饱满度分档表》为阶段 0-C 强制产出物 - 7.5 成文自检清单(按档位判字数达标) 配套文件: - references/review-failure-cases.md:梓潼初设 6 例真实评审失败 + docx 落地 6 条技术红线 + 交付前自查清单 - scripts/md2docx.py:md → 宋体(四处字体全设)/ 小四 / 首行缩进 2 字符 / 1.5 倍行距 docx 其他:第十三节阶段产物落盘规范(目录树 + 命名 + 留痕纪律)、第十四节评审高频扣分点库。 --- .gitignore | 16 ++ SKILL.md | 299 +++++++++++++++++++++++++++++ references/review-failure-cases.md | 74 +++++++ scripts/md2docx.py | 270 ++++++++++++++++++++++++++ 4 files changed, 659 insertions(+) create mode 100644 .gitignore create mode 100644 SKILL.md create mode 100644 references/review-failure-cases.md create mode 100644 scripts/md2docx.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7fa990d --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +# Python +__pycache__/ +*.pyc +*.pyo + +# 编辑器 / 系统 +.vscode/ +.idea/ +*.swp + +# Windows +Thumbs.db +Desktop.ini + +# 转换产物(md2docx 的临时输出) +*.docx diff --git a/SKILL.md b/SKILL.md new file mode 100644 index 0000000..cad2cc5 --- /dev/null +++ b/SKILL.md @@ -0,0 +1,299 @@ +--- +name: doc-writing-standard +description: 方案类文档通用写作规范(全流程、跨项目复用)。资料盘点 → 意图识别 → 模板化骨架 → 资料核对与公开渠道补全 → 血肉来源标注 → 成文(正文写作铁律:来源分级、零交叉引用)→ 人工审校 → 一致性门禁。黄线:无依据不推断。适用于建设/服务/运维类政务可研、初设、需求论证、项目建议书、投标方案等。含模板小节饱满度分档识别(一句话档 / ≥100 字 / ≥200 字 / 清单档)。 +version: 1.7.0 +author: Jony +updated: 2026-09-11 +usage: 编写或修订任何"多章节、有数据口径、需过评审"的正式文档时加载;用户要求扩写/丰富/写饱满某章节时;写作前必须完成阶段0;按文档类型加载骨架模板;成文按排版规范与成文铁律输出 +--- + +# 方案类文档写作规范(通用版) + +## 文件索引 + +| 文件 | 职责 | 何时读 | +|---|---|---| +| `SKILL.md` | 本文件:全流程、黄线、成文铁律(主体,必读) | 每次加载技能时 | +| `references/review-failure-cases.md` | 评审高频扣分点与真实失败案例库 | 交付前自查、被评审抓住后回填 | +| `scripts/md2docx.py` | md → 宋体/小四/首行缩进 2 字符/1.5 倍行距 docx 转换脚本 | 需要出打印稿时 | + +--- + +## 〇、设计原则与黄线 + +**方法与项目分离**:本技能只存可复用的通用方法;项目专属信息(单位名、文号、数字、案例)一律存放在项目资料中,写作时通过"来源三元组"挂接(见第五节),不固化在技能与正文中。 + +**黄线条款(红线,优先级最高)**: + +1. 无来源依据的内容,禁止推断写入;找不到出处 → 待确认清单,确认前只写占位。 +2. 数字必须有出处,无法溯源的数字不得出现在正文。 +3. 口径矛盾必须问,不得擅自取舍。 +4. "不涉及"必须有依据(模板规则或资料/需求方确认)。 +5. 待确认项合并成一次结构化问答(问题+选项+建议默认值),未决项以【待确认:编号】占位。 +6. 引用外部资料必须留痕(URL/文号/访问日期),缺一不得引用。 + +## 一、流程总则(六阶段,顺序不可跳) + +阶段 0(写作前强制): + +- 0-A 基础资料盘点(八类登记表 + 动态项清单) +- 0-B 意图识别(三张清单:涉及/不涉及/待确认) +- 0-C 模板选择与骨架确定(含**逐小节饱满度分档**,产出《饱满度分档表》) +- 0-D 骨架与资料核对 + 公开渠道补全 +- 0-E "血肉"来源标注完善 + +随后:v0.1 骨架 → v0.2 细化提纲 → 分批成文(按第七节成文铁律)→ 人工审校 → 一致性门禁。 + +> 各阶段产出物的落盘位置与命名见【十三、阶段产物落盘规范】。 + +## 二、阶段 0-A / 0-B + +### 0-A 八类资料登记表 + +逐项登记:有无 / 来源文件 / 关键数据;缺失 → 【待补】。 + +项目名称 → 背景(单位概况/定位/投资资金/工期)→ 政策(国家/省/市/县四级:名称+文号+关键条款)→ 现状(设施底数/系统能力/网络/安全/运维,含存量数字)→ 痛点与需求 → 建设/服务内容 → 目标(总体/分层/量化绩效)→ 备注(模板/提纲/同类参考/预算清单/会议纪要/批复)。 + +### 动态项清单(逐项目必须重新确认,禁止沿用上一项目) + +编制单位与人员(模板封面页及正文 1.3):一般由建设单位(委托单位)+ 咨询设计单位(编制单位,盖章)构成,并落实负责人/审核/审定/核校/编写人员;有的项目由建设单位自行编制,按实际判断。 + +同类动态项:项目名称是否含标段/分期、资金来源构成、工期起算方式、主管单位名称(以当地机构为准)。 + +### 0-B 三张清单 + +1. 涉及清单(核心 / 需写 / 简写 + 素材出处 + 篇幅预算)。 +2. 不涉及清单:带(*)的条目 → 不列、序号顺延;未带(*)的条目 → 保留并写"本项目不涉及此项";每条能回答"凭什么不涉及"。 +3. 待确认清单:资料缺失 / 口径冲突 / 敏感表述,每条含问题、选项、建议默认值、影响章节。 + +三张清单经需求方确认后才进骨架。 + +## 三、阶段 0-C:模板选择与骨架确定 + +### 适用范围 + +政务信息化项目(建设类/服务类/运维类按内容形态区分)的可研、初设、需求论证、项目建议书、投标方案;主管单位以当地机构为准(常见为数据局类部门),审批、承诺函、验收对接均面向主管单位。 + +### 内容形态分类 + +| 内容形态 | 典型文档 | 骨架重心 | 意图识别侧重 | 数据纪律侧重 | +|---|---|---|---|---| +| 建设类(工程/购置) | 可研/初设/需求论证 | 按甲方模板全章节 | 逐节核对取舍((*)规则) | 概算明细可复算 | +| 服务类(购买服务) | 可研/服务方案/投标 | 服务内容/SLA/人员/工具/考核 | 服务颗粒度与频次时效 | 人月法或混合计量 | +| 运维类 | 运维方案/投标 | 运维内容/SLA/流程/考核/应急 | 服务项与考核表一一对应 | 人月法或系统年 | + +### 模板优先级 + +甲方给定模板 > 同类已批复范本 > 内置典型骨架。 + +## 四、阶段 0-D / 0-E + +### 0-D 骨架与资料核对 + 公开渠道补全 + +骨架逐节点与资料核对,标"有料/缺料";缺料且属公开事实的(政策原文、建设单位概况、标准规范),经建设单位官网或搜索引擎补全,先把骨架准备为 md 工作稿;留痕(来源+日期+置信度:官方文件 > 官方网站 > 权威媒体);项目内部数据不得用网络资料替代;检索不到 → 待确认清单。 + +### 0-E 来源三元组 + +(素材:项目资料:文件名/章节 | 公开来源:URL+日期 | 待确认:编号) + +无一元支撑的节点不得进入 v0.2。 + +## 五、结构规范 + +1. 结论先行(金字塔原理):每节第一句=核心结论。 +2. 三段式正文:结论段 → 依据段(数据/标准/表格)→ 图表位;描述公式=对象+动作+数量/频次+交付物。 +3. 标题一般到三级;图表位显式标注;锚点图先行。 + +## 六、排版规范(成文输出统一执行) + +正文字体:宋体,小四(12pt),中西文统一宋体(w:ascii / w:hAnsi / w:eastAsia / w:cs 四处全设)。标题:宋体加粗,字号按甲方模板。正文段落:首行缩进 2 字符。行间距:1.5 倍。页面:A4,页边距按甲方模板。禁止混用等线/Calibri/Consolas 等西文字体。 + +python-docx 统一设置函数(四属性全设:w:ascii/w:hAnsi/w:eastAsia/w:cs → 宋体;首行缩进 Pt(24);行距 1.5)配 "md → 宋体 docx" 转换脚本用于打印成稿:**脚本实体见 `scripts/md2docx.py`**,调用方式: + +``` +set PYTHONUTF8=1 +python scripts/md2docx.py 输入.md 输出.docx +``` + +docx 技术红线:Windows 下 python 脚本写 .py 文件执行;XML 插入锚点不能以 `` 开头结尾夹住插入点,应以 `` 结尾;设 PYTHONUTF8=1。 + +## 七、成文铁律(正文写作操作规范) + +骨架【结论】只是论点。成文 = 将每个结论节点转换为"论点+论据+论证链"的可交付正文;本章为通用操作规范,逐条执行、逐条可判定。 + +### 7.1 来源分级标注(任何论断落笔前先定级) + +| 来源级 | 判定标准 | 正文写法 | 禁止 | +|---|---|---|---| +| A 事实 | 有勘察记录/政策原文/已确认数据支撑 | 直接陈述,可含数字 | 无 | +| B 编制口径 | 骨架阶段或会议确认的约定 | 直接陈述 | 包装成"经勘察发现" | +| C 合理推演 | 由 A/B 按业务逻辑衍生 | 只做定性描述,不出现数字 | 编造数量与百分比 | + +配套动作:文末附《编写依据对照自检表》(正式排版时不列入正文),逐条记录:论断 → 来源级 → 出处 → 有效性核查结论。引用的标准必须为现行有效版本,被代替/废止的必须更正。 + +### 7.2 用语正反对照表(出现左列写法立即替换) + +| 禁用写法 | 原因 | 替代写法 | +|---|---|---| +| "详见第 X 章 / 对应 X.X 节" | 交叉引用易错且扰乱排版 | 删除;对应关系写入自检表 | +| "按行业通行做法 / 对照要求" | 无真实现行出处即为推断 | 锚定项目已确认口径,或注明真实文号 | +| "与附件 X 口径一致" | 指向句冗余 | 删除;口径一致性在自检表维护 | +| 推演内容出现数字 | C 级不得量化 | 改定性描述,或补充 A 级数据来源 | +| 连续形容词无细节 | 论证链断裂 | 回骨架补论据(按 7.3) | + +### 7.3 扩写操作流程(收到"写饱满/丰富"指令时按序执行) + +1. 向前找证据:将资料中与该结论相关的细节、条款、已确认数据装入论证,颗粒度到具体对象/位置/环节; +2. 向后找影响:写明"不解决的后果"与"解决后的改变"; +3. 按 7.4 确定本节饱满度档位; +4. 完成后逐项过 7.5 自检清单。 + +### 7.4 饱满度分档:先识别,再落笔 + +**四档定义**(字数下限为硬约束,括号内为典型区间) + +| 档位 | 字数下限 | 判定含义 | 写作形态 | +|---|---|---|---| +| **D 一句话档** | ≤50 字 | 只需声明一个事实或结论 | 一句话 + 依据(括注) | +| **C 交代档** | **≥100 字**(典型 100–200) | 需把"是什么/现在怎样"交代清楚 | 一段陈述:对象 + 关键事实 + 边界 | +| **B 论证档** | **≥200 字**(典型 300–600) | 需证明"凭什么" | 结论段 + 多层论据(A 级数据/条款)+ 影响分析 | +| **A 清单档** | 不看字数 | 需把明细列全 | 表格/清单,考核条目完备性与每行依据 | + +档位与章节职能的对应关系:论证型 → B,交代型 → C,声明/占位型 → D,清单/表格型 → A。 + +#### 7.4.1 对模板逐小节识别的流程(阶段 0-C 执行,四步) + +**第 0 步 · 锁定判定单元**:以甲方模板的**最小小节**(一般三级标题)为单元,**不是章**。同一章内不同小节可属不同档。 + +**第 1 步 · 三问定档(核心判据,按序问)** + +| # | 问题 | 答"是" | 答"否" | +|---|---|---|---| +| Q1 | 评审会不会追问"凭什么"? | → **B 论证档** | 继续 Q2 | +| Q2 | 有需要摆开的事实/数字/条款/明细吗? | 明细成列 → **A 清单档**;仅几个关键事实 → **C 交代档** | 继续 Q3 | +| Q3 | 删掉这一节,全文逻辑会断吗? | → **C 交代档** | → **D 一句话档** | + +**第 2 步 · 关键词兜底**:Q1–Q3 拿不准时,对照 7.4.2 表定档。 + +**第 3 步 · 落表留痕**:逐个登记 `小节号 | 模板原文标题 | 档位 | 字数下限 | 判定依据(Q几 / 关键词表)`。 + +**第 4 步 · 抽检校验**:随机抽 3 个小节各试写 50 字——若"一句话就写完了"却定了 B 档,或"50 字明显不够"却定了 D 档,说明判据用错,回 Q1 重判。 + +#### 7.4.2 模板小节标题 → 档位速查表 + +适用于政务可研/初设/需求论证/项目建议书/运维方案/投标方案的常见小节。 + +| 模板小节标题特征 | 档位 | 说明 | +|---|---|---| +| 项目名称、建设地点、建设周期/工期、招标方式、资金来源构成(单值)、主管单位、联系人 | **D** | 单值事实,一句话即完整 | +| 本项目不涉及项、XX 不适用/暂不建设说明 | **D** | 一句话 + 依据(模板规则或需求方确认) | +| 建设单位概况、项目背景、现状概述、建设目标(总述)、组织机构、培训与管理概述、运维范围概述 | **C** | 交代清楚"是什么/现在怎样" | +| 必要性、需求分析、可行性论证、技术路线比选、方案论证、效益分析、风险与对策、性能指标论证、投资估算编制依据、运维服务必要性、SLA 设定依据、考核机制设计依据 | **B** | 必须回答"凭什么",≥200 字 | +| 编制依据、政策依据 | **A** | 列文件名称 + 文号 + 关键条款 | +| 概算/预算明细、设备清单、服务一览表、人员配置表、考核表、指标表、进度计划、培训计划表 | **A** | 考核条目完整与每行计价依据 | +| 技术实现方案、系统架构、功能设计 | **B + A** | 文字论证 ≥200 字,配置项另出清单/表 | + +**标题含糊时的处理规则(按序适用)** + +1. 模板带括号提示或子标题 → 按子标题定档; +2. 该节结论会被下游章节引用 → 至少 C 档; +3. 该节是本项目核心卖点(必要性 / 方案 / 效益 / 运维内容 / SLA)→ B 档; +4. 仍判不准 → 先按 C 档写,交付前按第十节门禁复核调整。 + +#### 7.4.3 《饱满度分档表》是阶段 0 的强制产出物 + +- 阶段 **0-C**(模板选择与骨架确定)时产出 `0-C-饱满度分档表.md`,与骨架一并交需求方确认; +- **未出分档表不得进入 v0.2 细化提纲**; +- 表结构(模板): + +| 小节号 | 模板原文标题 | 档位 | 字数下限 | 判定依据 | 实际字数 | 是否达标 | +|---|---|---|---|---|---|---| +| 3.4 | 项目必要性 | B | ≥200 | Q1 是 | | | +| 3.4.1 | 建设地点 | D | ≤50 | 关键词表:单值事实 | | | +| 5.2.3 | 设备清单 | A | — | Q2 明细成列 | | | + +- 成文时按表执行;交付前门禁复核"实际字数是否达标、实际档位是否与分档表一致"。 + +### 7.5 成文自检清单(每节完成即逐项打勾) + +- [ ] 回答了"是什么 → 凭什么 → 有什么影响"三问 +- [ ] 每个论断已定来源级(A/B/C),与自检表一致 +- [ ] 无 7.2 表中禁用写法 +- [ ] 无交叉引用 +- [ ] 无未经出处的数字 +- [ ] 本节档位与《饱满度分档表》一致,且字数达标(D ≤50 / C ≥100 / B ≥200 / A 条目完整) +- [ ] 引用标准均为现行有效版本 + +## 八、数据与口径纪律 + +1. 数据台账(单一真源),正文只引用台账。 +2. 口径同步:改一处同步全部引用处,跨文件一致。 +3. 动词口径:"提升/新增" → "维持/保持":现状已达标的只能写"维持"。 +4. 禁用词门禁:废弃口径列禁用清单,交付前全文检索归零。 +5. 金额纪律:单位全文统一、合计写大写、每行写计价依据、分项表与服务内容条目一一对应。 + +## 九、人工审校(成文后必经,AI 不替代) + +AI 产出后交人工审校:数据与台账逐项核对;口径与敏感表述确认;格式与签章;逻辑通读。通过才定稿。 + +## 十、交付前一致性门禁 + +三问(钱可复算?数可溯源?重不重复?)+ 禁用词归零 + 待确认清单状态更新 + 序号图表连续无空号 + 产出后主动验证(数字核对/引用清零/口径一致性检索,不等需求方指出)。 + +## 十一、执行纪律 + +长内容分批写入并写后验证;读取最小化;文档版本化(v0.1 → v0.2 → v1.0);技能迭代 +0.0.1/+0.1 并登记;md 格式自检——标题统一 `##`/`###`、不用加粗伪标题、标题与表格前后保留空行、交付前渲染预览确认。 + +## 十二、版本线 + +v1.3.x 政务口径/排版/动态项 → v1.4.0 并入正文写作铁律 → v1.5.0 方法与项目分离 → v1.5.1 修正 Markdown 结构 → v1.5.2 第七节重构为表格式操作规范 → v1.6.0 修复符号丢失(→ / 复选框),新增第十三节落盘规范、第十四节扣分点库,补 `scripts/md2docx.py` 脚本实体 → **v1.7.0 解决"模板小节无法定档"缺口:7.4 重构为四档(D 一句话 / C ≥100 / B ≥200 / A 清单)+ 7.4.1 三问定档流程 + 7.4.2 标题关键词速查表 + 7.4.3《饱满度分档表》升为阶段 0-C 强制产出物,7.5 自检项同步改为按档位判字数**。 + +## 十三、阶段产物落盘规范 + +**总则**:每阶段产物必须落盘,口头一致不算完成。`{项目}` 为项目根目录,结构固定如下: + +``` +{项目}/ +├─ 00-source/ 阶段0 输入资料(甲方模板/提纲/会议纪要/批复/预算清单) +├─ 01-inventory/ +│ ├─ 0-A-资料登记表.md 八类登记表 + 动态项清单 +│ ├─ 0-B-三张清单.md 涉及/不涉及/待确认 +│ ├─ 0-C-骨架-v0.1.md 模板选择结果与骨架 +│ └─ 0-C-饱满度分档表.md 逐小节档位/字数下限/判定依据(见 7.4.3) +├─ 02-draft/ +│ ├─ 0-D-骨架核对与补全.md 有料/缺料标注 + 公开渠道留痕 +│ ├─ 0-E-来源三元组.md 节点 → 三元组 +│ ├─ v0.2-细化提纲.md +│ └─ v1.0-正文.md 分批成文的工作稿 +├─ 03-docx/ md → 宋体 docx 打印稿(scripts/md2docx.py 产出) +├─ 04-review/ +│ ├─ 编写依据对照自检表.md 论断 → 来源级 → 出处 → 有效性核查 +│ └─ 一致性门禁-检查记录.md 三问 + 禁用词归零 + 待确认状态 +└─ 05-final/ 定稿与签章版 +``` + +**命名规则** + +- 阶段编号前缀(`0-A-`/`0-B-`)与技能小节号一一对应,便于回溯; +- 版本号写在文件名尾部(`v0.1`/`v0.2`/`v1.0`),**不覆盖写**,保留全部历史版本; +- 待确认项以 `【待确认:编号】` 内联在正文,同时登记进 `0-B-三张清单.md`;闭环后更新状态(未决/已决/作废)。 + +**留痕纪律** + +- 公开来源必须记 URL + 访问日期 + 置信度(官方文件 > 官方网站 > 权威媒体); +- 项目内部数据不得用网络资料替代,缺失即进待确认清单。 + +## 十四、评审高频扣分点库 + +完整失败案例与原文出处见 `references/review-failure-cases.md`。**每次评审被抓后,先更新该文件,再回填本表。** + +| # | 扣分点 | 触发写法 | 纠正动作 | +|---|---|---|---| +| 1 | 来源级标注缺失 | 把编制口径写成"经勘察发现" | 回 7.1 定级,改写法 | +| 2 | 无出处修饰语 | "对照等保要求""按行业通行做法" | 找真实现行出处,找不到就删 | +| 3 | 交叉引用重复/错误 | "对应 5.2.3、5.2.2、5.3.2、5.2.2" | 全文清零,对应关系改入自检表 | +| 4 | 冗余指向句 | "与附件三口径一致" | 删除 | +| 5 | 引用标准已废止 | GB/T 28181-2016 | 联网核查,改现行版(2022) | +| 6 | 推演内容带数字 | C 级论断出现百分比 | 改定性描述,或补 A 级来源 | +| 7 | 章节注水 | 声明型章节硬撑篇幅 | 按 7.4 表降档 | +| 8 | 口径不统一 | 同一数字在不同章节取值不同 | 回数据台账,全文同步 | diff --git a/references/review-failure-cases.md b/references/review-failure-cases.md new file mode 100644 index 0000000..182a385 --- /dev/null +++ b/references/review-failure-cases.md @@ -0,0 +1,74 @@ +# 评审高频扣分点与真实失败案例库 + +> 本文件是 `doc-writing-standard` 的配套参考。**每次评审被抓后先更新本文件,再回填 SKILL.md 第十四节的汇总表。** +> 更新时间:2026-09-11 · 维护:Jony + +--- + +## 一、真实失败案例(全部在项目中被评审/需求方当场抓住) + +### 案例 1:来源级标注缺失(梓潼法院安防初设 · 3.4(3)) + +- **原文写法**:「无主动监测告警」 +- **问题**:未标性质。看似勘察发现,实为编制口径。需求方追问"这话从哪来的",无法当场回答。 +- **定性**:B 级(编制口径)被写成了 A 级(事实)。 +- **纠正**:回到 7.1 定级,正文改为可溯源的表述或标注编制口径;自检表登记出处。 + +### 案例 2:无出处修饰语(梓潼初设 · 3.6) + +- **原文写法**:「对照等级保护基本要求」 +- **问题**:引用"等级保护基本要求"却给不出具体标准号与版本,属于无真实出处的推断;评审一问即破。 +- **纠正**:找到真实现行标准出处(写明标准号+年份)再引用;找不到就删除,改为锚定项目自身已确认口径。 + +### 案例 3:交叉引用重复错误(梓潼初设 · 全文) + +- **原文写法**:「对应 5.2.3、5.2.2、5.3.2、5.2.2」 +- **问题**:5.2.2 重复出现;章节号还会随排版变动而失效,交叉引用必然出错。 +- **纠正**:全文检索「详见第」「对应」并清零;章节间对应关系只在《编写依据对照自检表》维护,正文自成一体。 + +### 案例 4:冗余指向句(梓潼初设 · 结论段) + +- **原文写法**:「与附件三口径一致」 +- **问题**:需求方判为"多余"。口径对应关系属于编写层信息,不该进正文。 +- **纠正**:删除。口径一致性在自检表维护。 + +### 案例 5:引用标准已废止(梓潼初设 · 引用清单) + +- **原文写法**:`GB/T 28181-2016` +- **问题**:该标准已被 2022 版代替,编制时未察觉。 +- **纠正**:引用标准一律联网核查现行有效性;被代替/废止的必须更正为现行版本并记录核查日期。 + +### 案例 6:章节注水(梓潼初设 · 声明型章节) + +- **表现**:声明型/占位型章节(如"本项目不涉及此项")被硬撑成整段篇幅。 +- **问题**:饱满度与章节职能不匹配,读起来"虚"。 +- **纠正**:按 7.4 表降档——声明型一句话+依据即可。 + +--- + +## 二、docx 落地技术红线(卫健委运维方案项目实战) + +转 Word 阶段踩过的坑,与内容无关但会直接导致交付失败: + +| # | 红线 | 说明 | +|---|---|---| +| 1 | 非 ASCII 符号经有损编码会**直接丢失** | 曾出现全文 `→`、`☐` 被吞掉只剩双空格(本文档 v1.5.2 即受害),落地前必须逐字符核验 | +| 2 | Windows 下 python 脚本一律写 `.py` 文件执行 | 内联 `-c` 易踩引号/转义地狱 | +| 3 | XML 插入锚点不能以 `` 开头结尾夹住插入点 | 会把内容插进段落内部产生非法嵌套 ``;锚点应以 `` 结尾 | +| 4 | `PYTHONUTF8=1` | 不设会出现编码相关的诡异报错 | +| 5 | 字体四处全设 | `w:ascii` / `w:hAnsi` / `w:eastAsia` / `w:cs`,只设 `font.name` 会回落成等线/Calibri | +| 6 | 首行缩进用 `Pt(24)` | 小四 12pt × 2 字符;不要用字符数 API,跨版本不稳 | + +--- + +## 三、扣分点自查速查(交付前逐项过) + +- [ ] 每个论断都能回答"这话从哪来的"(A/B/C 已定级) +- [ ] 无「详见第 X 章」「对应 X.X 节」「与附件 X 口径一致」 +- [ ] 无「按行业通行做法」「对照 XX 要求」这类无出处修饰语 +- [ ] 引用标准均为现行有效版本(已联网核查,记核查日期) +- [ ] C 级推演内容不含数字 +- [ ] 同一数字在不同章节取值一致(回数据台账核对) +- [ ] 声明型章节没有注水 +- [ ] 正文文件无符号丢失(`→` / `☐` 等非 ASCII 符号在位) +- [ ] 待确认清单状态已更新(未决 / 已决 / 作废) diff --git a/scripts/md2docx.py b/scripts/md2docx.py new file mode 100644 index 0000000..ba944ca --- /dev/null +++ b/scripts/md2docx.py @@ -0,0 +1,270 @@ +# -*- coding: utf-8 -*- +"""md2docx.py — 方案类文档 Markdown → Word(宋体 / 小四 / 首行缩进 2 字符 / 1.5 倍行距) + +用法(Windows,务必先设 UTF-8): + set PYTHONUTF8=1 + python scripts/md2docx.py 输入.md 输出.docx + +依赖: + pip install python-docx + +说明: + 只负责排版落地,不改动内容。 + 支持: 标题(#~####)、正文段落、管道表格、无序/有序列表、- [ ] 复选框、**加粗**、代码块。 + 复杂合并单元格、图片、公式请在 Word 中人工微调。 +""" + +import os +import re +import sys + +from docx import Document +from docx.enum.table import WD_TABLE_ALIGNMENT +from docx.enum.text import WD_ALIGN_PARAGRAPH +from docx.oxml.ns import qn +from docx.shared import Cm, Pt + +# ==================== 可按甲方模板调整的参数 ==================== +FONT_NAME = '宋体' # 中西文统一宋体 +BODY_SIZE = Pt(12) # 小四 = 12pt +FIRST_LINE_INDENT = Pt(24) # 首行缩进 2 字符(2 x 12pt) +LINE_SPACING = 1.5 # 行间距 1.5 倍 +HEADING_SIZES = {1: Pt(16), 2: Pt(15), 3: Pt(14), 4: Pt(12)} +HEADING_ALIGN_CENTER = {1} # 一级标题居中 +TABLE_FONT_SIZE = Pt(12) +TABLE_HEADER_BOLD = True +CODE_FONT_SIZE = Pt(10.5) +PAGE_WIDTH, PAGE_HEIGHT = Cm(21), Cm(29.7) # A4 +MARGIN_TB, MARGIN_LR = Cm(2.54), Cm(3.17) +# ================================================================ + +CHECKBOX = {False: '\u2610', True: '\u2611'} # ☐ / ☑ + + +def set_run_font(run, size=None, bold=None): + """四处字体全设,避免 Word 回落到等线 / Calibri 造成字体不统一。""" + run.font.name = FONT_NAME + rpr = run._element.get_or_add_rPr() + rfonts = rpr.find(qn('w:rFonts')) + if rfonts is None: + rfonts = rpr.makeelement(qn('w:rFonts'), {}) + rpr.append(rfonts) + rfonts.set(qn('w:ascii'), FONT_NAME) + rfonts.set(qn('w:hAnsi'), FONT_NAME) + rfonts.set(qn('w:eastAsia'), FONT_NAME) + rfonts.set(qn('w:cs'), FONT_NAME) + if size is not None: + run.font.size = size + if bold is not None: + run.font.bold = bold + + +def add_runs(paragraph, text, size, bold=False): + """把 **加粗** 解析成多个 run。""" + for piece in re.split(r'(\*\*.+?\*\*)', text): + if not piece: + continue + if piece.startswith('**') and piece.endswith('**') and len(piece) > 4: + set_run_font(paragraph.add_run(piece[2:-2]), size, True) + else: + set_run_font(paragraph.add_run(piece), size, bold) + + +def add_body(doc, text): + p = doc.add_paragraph() + pf = p.paragraph_format + pf.first_line_indent = FIRST_LINE_INDENT + pf.line_spacing = LINE_SPACING + add_runs(p, text, BODY_SIZE) + return p + + +def add_heading(doc, text, level): + p = doc.add_paragraph() + pf = p.paragraph_format + pf.line_spacing = LINE_SPACING + pf.space_before = Pt(6) + pf.space_after = Pt(6) + if level in HEADING_ALIGN_CENTER: + pf.alignment = WD_ALIGN_PARAGRAPH.CENTER + add_runs(p, text, HEADING_SIZES.get(level, BODY_SIZE), True) + return p + + +def add_list_item(doc, text, prefix='\u2022 '): + p = doc.add_paragraph() + pf = p.paragraph_format + pf.left_indent = FIRST_LINE_INDENT + pf.line_spacing = LINE_SPACING + add_runs(p, prefix + text, BODY_SIZE) + return p + + +def is_table_sep(line): + s = line.strip() + return bool(s) and set(s) <= set('|-: ') and '-' in s + + +def split_row(line): + s = line.strip() + if s.startswith('|'): + s = s[1:] + if s.endswith('|'): + s = s[:-1] + return [c.strip() for c in s.split('|')] + + +def add_table(doc, rows): + cols = max(len(r) for r in rows) + table = doc.add_table(rows=0, cols=cols) + table.style = 'Table Grid' + table.alignment = WD_TABLE_ALIGNMENT.CENTER + for i, row in enumerate(rows): + cells = table.add_row().cells + for j in range(cols): + text = row[j] if j < len(row) else '' + para = cells[j].paragraphs[0] + para.paragraph_format.line_spacing = LINE_SPACING + add_runs(para, text, TABLE_FONT_SIZE, bold=(i == 0 and TABLE_HEADER_BOLD)) + return table + + +def normalize_links(text): + text = re.sub(r'!\[([^\]]*)\]\([^)]*\)', r'\1', text) # 图片 -> alt 文本 + text = re.sub(r'\[([^\]]+)\]\(([^)]+)\)', r'\1(\2)', text) # 链接 -> 文本(URL) + return text + + +def setup_document(): + doc = Document() + style = doc.styles['Normal'] + style.font.name = FONT_NAME + style.font.size = BODY_SIZE + try: + style.element.get_or_add_rPr().get_or_add_rFonts().set(qn('w:eastAsia'), FONT_NAME) + except Exception: + pass + + sec = doc.sections[0] + sec.page_width, sec.page_height = PAGE_WIDTH, PAGE_HEIGHT + sec.top_margin = sec.bottom_margin = MARGIN_TB + sec.left_margin = sec.right_margin = MARGIN_LR + return doc + + +def convert(md_path, docx_path): + with open(md_path, encoding='utf-8') as f: + lines = f.read().split('\n') + + # 跳过 YAML frontmatter(技能元数据不进正文) + start = 0 + if lines and lines[0].strip() == '---': + for k in range(1, len(lines)): + if lines[k].strip() == '---': + start = k + 1 + break + + doc = setup_document() + i, in_code = start, False + + while i < len(lines): + line = lines[i].rstrip() + stripped = line.strip() + + # 代码块 + if stripped.startswith('```'): + in_code = not in_code + i += 1 + continue + if in_code: + p = doc.add_paragraph() + p.paragraph_format.line_spacing = 1.0 + p.paragraph_format.left_indent = FIRST_LINE_INDENT + add_runs(p, line, CODE_FONT_SIZE) + i += 1 + continue + + if not stripped: + i += 1 + continue + + # 标题 + m = re.match(r'^(#{1,6})\s+(.*)$', stripped) + if m: + add_heading(doc, m.group(2).strip(), len(m.group(1))) + i += 1 + continue + + # 表格 + if '|' in stripped and i + 1 < len(lines) and is_table_sep(lines[i + 1]): + rows = [] + while i < len(lines) and '|' in lines[i]: + if not is_table_sep(lines[i]): + rows.append(split_row(lines[i])) + i += 1 + if rows: + add_table(doc, rows) + continue + + # 分隔线 + if re.match(r'^([-*_]\s*){3,}$', stripped): + i += 1 + continue + + # 复选框(必须早于普通列表判断) + m = re.match(r'^[-*+]\s+\[([ xX])\]\s*(.*)$', stripped) + if m: + add_list_item(doc, normalize_links(m.group(2)), + prefix=CHECKBOX[m.group(1).lower() == 'x'] + ' ') + i += 1 + continue + + # 无序列表 + m = re.match(r'^[-*+]\s+(.*)$', stripped) + if m: + add_list_item(doc, normalize_links(m.group(1))) + i += 1 + continue + + # 有序列表 + m = re.match(r'^(\d+)[.)]\s+(.*)$', stripped) + if m: + add_list_item(doc, normalize_links(m.group(2)), prefix=m.group(1) + '. ') + i += 1 + continue + + # 引用 + if stripped.startswith('>'): + add_body(doc, normalize_links(stripped.lstrip('>').strip())) + i += 1 + continue + + # 普通正文 + add_body(doc, normalize_links(stripped)) + i += 1 + + doc.save(docx_path) + + +def main(): + if len(sys.argv) < 3: + print(__doc__) + sys.exit(1) + + md_path, docx_path = sys.argv[1], sys.argv[2] + if not os.path.isfile(md_path): + print('输入文件不存在: %s' % md_path) + sys.exit(1) + + out_dir = os.path.dirname(os.path.abspath(docx_path)) + if out_dir and not os.path.isdir(out_dir): + os.makedirs(out_dir) + + convert(md_path, docx_path) + print('已生成: %s' % docx_path) + print('字体=%s 正文=%s 首行缩进=%s 行距=%s' % + (FONT_NAME, BODY_SIZE.pt, FIRST_LINE_INDENT.pt, LINE_SPACING)) + + +if __name__ == '__main__': + main()