feat(doc): 新增 DOCX 转 Markdown Skill

This commit is contained in:
zhiye.sun
2026-08-25 11:22:43 +08:00
parent f35b6a99d2
commit fa2ae00d5e
8 changed files with 541 additions and 21 deletions
+34 -18
View File
@@ -36,6 +36,8 @@
## 4. 迁移批次
迁移分为“特殊样本”和“同质批量”两个阶段。前者用于覆盖迁移机制的不同风险类型,后者在规则稳定后提高吞吐量。批次数量不固定:特殊样本原则上逐个迁移,批量阶段每批建议 6~12 个同质 Skill。
### 第 0 批:迁移基础设施
第 0 批属于仓库内部维护工具,不发布为市场 Skill:
@@ -47,29 +49,41 @@
完成门槛:能够对全部来源 Skill 生成稳定清单,并在不复制来源内容的情况下更新状态。
### 第 1 批:低风险样板
### 第 1 阶段:特殊样本
先完成唯一标准样板,再按插件推进,避免一次跨模块迁移导致集中返工:
特殊样本不追求模块连续性,而是覆盖不同迁移机制:
1. `doc/format-md`
2. `doc/report`、`doc/requirements`
3. `git/commit-msg`
4. `knowledge/handoff`
1. `doc/format-md`:纯指令型样本,已完成。
2. `doc/docx-to-md`:多来源合并、脚本、依赖和生成物样本,已完成。
3. `git/commit-msg`:只读 Git 状态分析样本。
4. `git/branch`:修改仓库状态和二次授权边界样本。
5. `knowledge/handoff`:模板化文档与项目上下文样本。
6. `skill/guidance`:大型参考资料、索引和渐进式加载样本。
7. `skill/migrate`:脚本、检查、同步和状态追踪组成的复合工作流样本。
完成门槛:`doc/format-md` 固化目录模板、验证命令、行为用例和迁移记录格式后,才能继续本批其余 Skill。
完成门槛:每种样本均通过对应验证,并形成可复用的命名、目录、独立实现、测试、扫描和状态同步规则。出现未覆盖的新结构或权限类型时,应补充样本,不直接扩批。
### 第 2 批:文档转换
### 第 2 阶段:同质批量迁移
特殊样本完成后,按同一插件、相近能力和相同风险类型组织批量迁移:
- 每批建议 6~12 个 Skill,高度同质时可以整组处理。
- 不把只读能力与有副作用能力、纯指令与复杂脚本、普通迁移与公开资料重建强行合为一批。
- 每批仍需迁移前确认和提交前确认,并统一更新 README、迁移计划和来源状态。
- 任一验收门禁失败时暂停该批,不继续扩大范围。
### 文档转换批次
所属插件:`doc`
1. `docx-to-markdown`
1. `docx-to-md`
2. `markdown-to-docx`
3. `excel-to-markdown`
4. `archive-docs`
重点验证图片、表格、合并单元格、编码、覆盖策略和路径安全。脚本及测试数据必须独立创建。
### 第 3 批:Git 工作流
### Git 工作流批次
所属插件:`git`
@@ -78,9 +92,9 @@
3. `export-changes`
4. `integrate-branch`
`summarize-commit` 已在样板批次完成。涉及提交、合并和远端操作的 Skill 必须保留明确授权边界,并保护脏工作区。
`commit-msg` 将作为只读 Git 特殊样本先行完成。涉及提交、合并和远端操作的 Skill 必须保留明确授权边界,并保护脏工作区。
### 第 4 批:知识管理
### 知识管理批次
所属插件:`knowledge`
@@ -92,7 +106,7 @@
来源中与经验初始化、提升和回扫相关的多个能力统一合并为 `maintain-lessons`,通过模式区分具体工作。
### 第 5 批:开发主流程
### 开发主流程批次
所属插件:`dev`
@@ -119,7 +133,7 @@ plan-change
多个来源中的代码检查、代码审查能力合并为 `review-code`,通过工作区、提交和分支三种模式覆盖。
### 第 6 批:项目规范与前端辅助
### 项目规范与前端辅助批次
1. `dev/select-component`
2. `dev/design-style`
@@ -129,7 +143,7 @@ plan-change
这里只实现读取和维护“当前项目自身规范”的机制,不随 CraftKit 提供任何来源项目规范。
### 第 7 批:基于公开资料重建
### 基于公开资料重建批次
以下能力不从来源文本改写,而是根据官方资料重新设计:
@@ -142,7 +156,7 @@ plan-change
中性规格必须记录所采用的公开标准、官方文档和许可证信息。
### 第 8 批:暂缓或排除
### 暂缓或排除
默认排除:
@@ -170,7 +184,7 @@ plan-change
| 多套前端设计 | `design-frontend` |
| 多套后端实现 | `implement-backend` |
| 多套前端实现 | `implement-frontend` |
| 多套 Word 转 Markdown | `docx-to-markdown` |
| 多套 Word 转 Markdown | `docx-to-md` |
| 经验初始化、提升、回扫 | `maintain-lessons` |
| 规范检索和索引维护 | `retrieve-guidance`、`maintain-guidance` |
@@ -211,4 +225,6 @@ plan-change
- [x] 实现 Skill 与敏感内容检查脚本。
- [x] 实现迁移台账预览和更新脚本。
- [x] 迁移并验证首个样板 `doc/format-md`。
- [ ] 样板通过后推进第 1 批其余 Skill。
- [x] 迁移并验证脚本型特殊样本 `doc/docx-to-md`。
- [ ] 完成其余特殊样本并总结批量迁移规则。
- [ ] 按插件和风险类型推进同质批量迁移。
+8 -2
View File
@@ -55,7 +55,10 @@
"source-a:fdde0da43c1ec4c7": {
"sourcePathHash": "fdde0da43c1ec4c7fddad96b9172435af6be96ceafa2cc824f70e04a3cf11085",
"sourceSha256": "46a02dbff9c3fff57a0802853e2424983927f53c7f1cc3d8a42542403e62a161",
"status": "pending"
"status": "migrated",
"target": "plugins/doc/skills/docx-to-md",
"targetVersion": "0.1.0",
"reviewedAt": "2026-08-25"
},
"source-a:4efb6f9dd0f39dff": {
"sourcePathHash": "4efb6f9dd0f39dff13339e62ff24b672552efcbcedb007e9ea86fe41950adbb0",
@@ -140,7 +143,10 @@
"source-b:d6d4166800584a61": {
"sourcePathHash": "d6d4166800584a61ad6cbf96af57bb8ee269a16babba753f267b48efd66f6d2d",
"sourceSha256": "60f8708db24f368ecec55bd3f326683fac60bd6776ac69e0fafaf4c77333e703",
"status": "pending"
"status": "migrated",
"target": "plugins/doc/skills/docx-to-md",
"targetVersion": "0.1.0",
"reviewedAt": "2026-08-25"
},
"source-b:023767d9c7be6295": {
"sourcePathHash": "023767d9c7be62956d12fd01c94ac676b9fe6719741acd6f412c8b446efa2760",
+117
View File
@@ -0,0 +1,117 @@
import importlib.util
import sys
import tempfile
import unittest
import zipfile
from pathlib import Path
SCRIPT = (
Path(__file__).parents[2]
/ "plugins"
/ "doc"
/ "skills"
/ "docx-to-md"
/ "scripts"
/ "convert.py"
)
SPEC = importlib.util.spec_from_file_location("docx_to_md_convert", SCRIPT)
MODULE = importlib.util.module_from_spec(SPEC)
assert SPEC.loader is not None
sys.modules[SPEC.name] = MODULE
SPEC.loader.exec_module(MODULE)
class DocxToMarkdownTest(unittest.TestCase):
"""验证转换器的核心内容、覆盖保护和输入边界。"""
def make_docx(self, path: Path) -> None:
"""创建只包含公开 OOXML 结构的最小测试文档。"""
document = """<?xml version="1.0" encoding="UTF-8"?>
<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
<w:body>
<w:p><w:pPr><w:pStyle w:val="Heading1"/></w:pPr><w:r><w:t>测试标题</w:t></w:r></w:p>
<w:p><w:r><w:rPr><w:b/></w:rPr><w:t>加粗正文</w:t></w:r>
<w:hyperlink r:id="rLink"><w:r><w:t>示例链接</w:t></w:r></w:hyperlink></w:p>
<w:p><w:pPr><w:numPr><w:ilvl w:val="0"/><w:numId w:val="1"/></w:numPr></w:pPr>
<w:r><w:t>列表项目</w:t></w:r></w:p>
<w:p><w:r><w:drawing><a:blip r:embed="rImage"/></w:drawing></w:r></w:p>
<w:tbl>
<w:tr><w:tc><w:p><w:r><w:t>名称</w:t></w:r></w:p></w:tc><w:tc><w:p><w:r><w:t>值</w:t></w:r></w:p></w:tc></w:tr>
<w:tr><w:tc><w:p><w:r><w:t>A</w:t></w:r></w:p></w:tc><w:tc><w:p><w:r><w:t>1</w:t></w:r></w:p></w:tc></w:tr>
</w:tbl>
</w:body>
</w:document>"""
styles = """<?xml version="1.0" encoding="UTF-8"?>
<w:styles xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:style w:type="paragraph" w:styleId="Heading1"><w:name w:val="heading 1"/></w:style>
</w:styles>"""
numbering = """<?xml version="1.0" encoding="UTF-8"?>
<w:numbering xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:abstractNum w:abstractNumId="0"><w:lvl w:ilvl="0"><w:numFmt w:val="bullet"/></w:lvl></w:abstractNum>
<w:num w:numId="1"><w:abstractNumId w:val="0"/></w:num>
</w:numbering>"""
relationships = """<?xml version="1.0" encoding="UTF-8"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
<Relationship Id="rLink" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink" Target="https://example.com" TargetMode="External"/>
<Relationship Id="rImage" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Target="media/test.png"/>
</Relationships>"""
with zipfile.ZipFile(path, "w") as archive:
archive.writestr("word/document.xml", document)
archive.writestr("word/styles.xml", styles)
archive.writestr("word/numbering.xml", numbering)
archive.writestr("word/_rels/document.xml.rels", relationships)
archive.writestr("word/media/test.png", b"\x89PNG\r\n\x1a\n")
archive.writestr("word/comments.xml", "<comments/>")
def test_convert_common_content_and_report_warning(self) -> None:
"""常见结构应转换,无法处理的批注应明确告警。"""
with tempfile.TemporaryDirectory() as temp:
root = Path(temp)
source = root / "sample.docx"
output = root / "result"
self.make_docx(source)
result = MODULE.DocxConverter(source, output).convert()
markdown = result.output_file.read_text(encoding="utf-8")
self.assertIn("# 测试标题", markdown)
self.assertIn("**加粗正文**", markdown)
self.assertIn("[示例链接](https://example.com)", markdown)
self.assertIn("- 列表项目", markdown)
self.assertIn("| 名称 | 值 |", markdown)
self.assertIn("![图片](images/image-", markdown)
self.assertEqual(result.images, 1)
self.assertTrue(any("批注" in warning for warning in result.warnings))
def test_existing_output_requires_force(self) -> None:
"""默认不得写入已有输出目录,显式覆盖后才可继续。"""
with tempfile.TemporaryDirectory() as temp:
root = Path(temp)
source = root / "sample.docx"
output = root / "result"
self.make_docx(source)
output.mkdir()
with self.assertRaises(FileExistsError):
MODULE.DocxConverter(source, output).convert()
result = MODULE.DocxConverter(source, output, force=True).convert()
self.assertTrue(result.output_file.exists())
def test_cli_rejects_non_docx(self) -> None:
"""命令行入口应拒绝扩展名不正确的文件。"""
with tempfile.TemporaryDirectory() as temp:
source = Path(temp) / "sample.txt"
source.write_text("not docx", encoding="utf-8")
self.assertEqual(MODULE.main([str(source)]), 2)
if __name__ == "__main__":
unittest.main()