feat(doc): 新增文档转换与规则化归档 Skill

This commit is contained in:
zhiye.sun
2026-08-25 15:32:12 +08:00
parent dbc2f65154
commit d255bdd524
20 changed files with 735 additions and 20 deletions
+9 -8
View File
@@ -4,7 +4,7 @@
将两个本地来源中的通用能力整理为 CraftKit Skill。迁移结果必须适用于 Codex 插件市场,并与来源项目的品牌、内部框架、业务知识和运行环境解耦。
迁移不追求来源 Skill 与目标 Skill 一一对应。重复能力应合并,公司专属能力应排除,目标数量以职责清晰和实际复用价值为准。
迁移不追求来源 Skill 与目标 Skill 一一对应。重复能力应合并;公司专属实现不得迁移,但应优先将其解决的通用问题重建为中性平替,确无独立价值或无法安全替代时才排除。目标数量以职责清晰和实际复用价值为准。
## 2. 迁移路径
@@ -60,9 +60,9 @@
5. `knowledge/handoff`:模板化文档与项目上下文样本,已完成。
6. `skill/guidance`:大型参考资料、索引和渐进式加载样本,已完成;只检索项目资料和基于公开一级资料独立重建的中性公共基线。
7. `knowledge/init`:成熟项目与空项目双模式初始化、参考项目提炼和共享/本地信息边界样本,已完成。
8. `skill/migrate`:脚本、检查、同步和状态追踪组成的复合工作流样本。
8. `skill/migrate`:不迁移。来源能力用于把 Claude Code Skill 转换为 Codex Skill;CraftKit 自始按 Codex 规范开发,不存在平台转换需求。来源扫描、目标检查和状态追踪继续由 `migration/scripts/` 作为仓库维护设施承担。
完成门槛:每种样本均通过对应验证,并形成可复用的命名、目录、独立实现、测试、扫描和状态同步规则。出现未覆盖的新结构或权限类型时,应补充样本,不直接扩批。
完成门槛:适用样本均通过对应验证,不适用样本记录排除依据,并形成可复用的命名、目录、独立实现、测试、扫描和状态同步规则。出现未覆盖的新结构或权限类型时,应补充样本,不直接扩批。当前特殊样本阶段已完成,可以进入同质批量迁移。
### 第 2 阶段:同质批量迁移
@@ -78,9 +78,9 @@
所属插件:`doc`
1. `docx-to-md`
2. `markdown-to-docx`
3. `excel-to-markdown`
4. `archive-docs`
2. `md-to-docx`(已完成)
3. `xlsx-to-md`(已完成)
4. `archive`(已完成,以可配置规则平替固定目录、业务文件名、专有章节拆分和内部系统校验)
重点验证图片、表格、合并单元格、编码、覆盖策略和路径安全。脚本及测试数据必须独立创建。
@@ -230,5 +230,6 @@ plan-change
- [x] 迁移并验证只读 Git 特殊样本 `git/commit-msg`。
- [x] 迁移并验证有副作用 Git 特殊样本 `git/branch`。
- [x] 迁移并验证模板化交接特殊样本 `knowledge/handoff`。
- [ ] 完成其余特殊样本并总结批量迁移规则。
- [ ] 按插件和风险类型推进同质批量迁移。
- [x] 完成其余特殊样本并总结批量迁移规则。
- [ ] 按插件和风险类型继续推进同质批量迁移。
- [x] 完成首个同质批量:`md-to-docx`、`xlsx-to-md`、`archive`。
+18 -5
View File
@@ -63,12 +63,17 @@
"source-a:4efb6f9dd0f39dff": {
"sourcePathHash": "4efb6f9dd0f39dff13339e62ff24b672552efcbcedb007e9ea86fe41950adbb0",
"sourceSha256": "a914823bdf50d9e5c8ee70d6363c5c0e7aea341ed390587a9285c70a7047558e",
"status": "pending"
"status": "migrated",
"target": "plugins/doc/skills/xlsx-to-md",
"targetVersion": "0.1.0",
"reviewedAt": "2026-08-25"
},
"source-a:32e7cfc21d252985": {
"sourcePathHash": "32e7cfc21d25298559bffbe7f0918f0f6e7d2aac29e3eca39d1f43d8226cf934",
"sourceSha256": "0f0486e3f25279a1a1cb8cecfef4dc01830471b5d31b52a9d17a425919997bc2",
"status": "pending"
"status": "excluded",
"reason": "CraftKit 原生面向 Codex,不需要 Claude Code 到 Codex 的平台转换能力",
"reviewedAt": "2026-08-25"
},
"source-a:bc91b57fa2a50049": {
"sourcePathHash": "bc91b57fa2a500498b31b0f1a87dfa8ab83c32e4b361b4ea1614ea6721234b7c",
@@ -162,7 +167,10 @@
"source-b:b127c47139270a7f": {
"sourcePathHash": "b127c47139270a7f9157e18ad6a0c975b99caf07b9a0874daeac86d9a99ce48c",
"sourceSha256": "0214b80071f07beaff4f5837b983546ff71dbaa65bc29e34a5a23c9d946b683b",
"status": "pending"
"status": "migrated",
"target": "plugins/doc/skills/md-to-docx",
"targetVersion": "0.1.0",
"reviewedAt": "2026-08-25"
},
"source-b:34ab291ce3cfed2e": {
"sourcePathHash": "34ab291ce3cfed2e69cc6f08309b1a41142be91235b9a573daf98639b4a001cc",
@@ -319,7 +327,10 @@
"source-b:3763e17cff825df6": {
"sourcePathHash": "3763e17cff825df6920f57d84736093082014f2a0316d9e84ee18ac8078ff6e5",
"sourceSha256": "bd2e4ae7f8a9bded1f7a983289fb49556f30cd5d6c6382500dd8c464fdd66e02",
"status": "pending"
"status": "migrated",
"target": "plugins/doc/skills/archive",
"targetVersion": "0.1.0",
"reviewedAt": "2026-08-25"
},
"source-b:9e3e75840a46af96": {
"sourcePathHash": "9e3e75840a46af96c33a1885f192813239b511eb972a4962b30b76de7392c88c",
@@ -340,7 +351,9 @@
"source-b:988b200efd0b8be0": {
"sourcePathHash": "988b200efd0b8be024060e0785da2f864bf7ee30d7c7b434fde7ab1e4cdcc9d8",
"sourceSha256": "e7fa073d592167c07b06698d664192baafb126afa1a6bfdc50a056e1ea13e56f",
"status": "pending"
"status": "excluded",
"reason": "CraftKit 原生面向 Codex,不需要 Claude Code 到 Codex 的平台转换能力",
"reviewedAt": "2026-08-25"
},
"source-b:8a7cdf719d8f8a3e": {
"sourcePathHash": "8a7cdf719d8f8a3e0200781a82062cc5a839e4d2c2fd400982771968245d62dd",
+78
View File
@@ -0,0 +1,78 @@
"""验证文档批次三个独立实现的核心行为和安全边界。"""
from __future__ import annotations
import json
import subprocess
import tempfile
import unittest
from pathlib import Path
from docx import Document
from openpyxl import Workbook
ROOT = Path(__file__).resolve().parents[2]
MD_SCRIPT = ROOT / "plugins/doc/skills/md-to-docx/scripts/convert.py"
XLSX_SCRIPT = ROOT / "plugins/doc/skills/xlsx-to-md/scripts/convert.py"
ARCHIVE_SCRIPT = ROOT / "plugins/doc/skills/archive/scripts/archive.py"
class DocumentBatchTests(unittest.TestCase):
"""使用临时目录执行真实转换,不依赖仓库外测试材料。"""
def run_script(self, script: Path, *args: object) -> subprocess.CompletedProcess[str]:
"""使用当前受控 Python 运行时执行目标脚本。"""
return subprocess.run(
[str(Path(__import__("sys").executable)), str(script), *(str(value) for value in args)],
capture_output=True, text=True, encoding="utf-8", check=False,
)
def test_markdown_to_docx_preserves_common_blocks(self) -> None:
"""标题、列表、表格和代码块应生成可重新打开的 DOCX。"""
with tempfile.TemporaryDirectory() as temp:
folder = Path(temp); source = folder / "sample.md"; output = folder / "sample.docx"
source.write_text("# 标题\n\n- 项目\n\n| 名称 | 值 |\n| --- | --- |\n| A | 1 |\n\n```py\nprint('ok')\n```\n", encoding="utf-8")
result = self.run_script(MD_SCRIPT, source)
self.assertEqual(0, result.returncode, result.stderr)
document = Document(output)
self.assertEqual("标题", document.paragraphs[0].text)
self.assertEqual(1, len(document.tables))
self.assertNotEqual(0, self.run_script(MD_SCRIPT, source).returncode)
def test_xlsx_to_markdown_handles_merges_and_escaping(self) -> None:
"""合并区域应填充值,表格特殊字符应安全转义。"""
with tempfile.TemporaryDirectory() as temp:
folder = Path(temp); source = folder / "sample.xlsx"
workbook = Workbook(); sheet = workbook.active; sheet.title = "数据"
sheet.append(["名称", "值"]); sheet.append(["A|B", "=1+1"])
sheet.merge_cells("A3:B3"); sheet["A3"] = "合并"
workbook.create_sheet("空表"); workbook.save(source)
result = self.run_script(XLSX_SCRIPT, source)
self.assertEqual(0, result.returncode, result.stderr)
markdown = source.with_suffix(".md").read_text(encoding="utf-8")
self.assertIn(r"A\|B", markdown)
self.assertIn("| 合并 | 合并 |", markdown)
self.assertIn("_空工作表_", markdown)
def test_archive_is_dry_run_first_and_keeps_source(self) -> None:
"""预演不得写目标,执行后仍须保留来源并只抽取指定章节。"""
with tempfile.TemporaryDirectory() as temp:
root = Path(temp); source = root / "specs/api.md"; source.parent.mkdir()
source.write_text("---\nowner: team\n---\n# 概述\n正文\n## API\n接口\n## 其他\n忽略\n", encoding="utf-8")
config = root / "rules.json"
config.write_text(json.dumps({"version": 1, "archiveRoot": "archive", "rules": [{"match": "specs/*.md", "target": "{stem}.md", "section": "API", "stripFrontmatter": True, "requiredText": ["接口"]}]}, ensure_ascii=False), encoding="utf-8")
target = root / "archive/api.md"
preview = self.run_script(ARCHIVE_SCRIPT, "--root", root, "--config", config)
self.assertEqual(0, preview.returncode, preview.stderr); self.assertFalse(target.exists())
applied = self.run_script(ARCHIVE_SCRIPT, "--root", root, "--config", config, "--apply")
self.assertEqual(0, applied.returncode, applied.stderr)
self.assertTrue(source.exists()); self.assertEqual("## API\n接口\n", target.read_text(encoding="utf-8"))
if __name__ == "__main__":
unittest.main()