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()