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
+32
View File
@@ -0,0 +1,32 @@
---
name: xlsx-to-md
description: 将 Excel .xlsx 工作簿转换为 Markdown,按工作表提取表格、公式或缓存值,并处理合并单元格和空工作表。适用于需要可读文本版本的现代 Excel 文件;旧版 .xls、宏、图表或公式重算不应触发本 Skill。
---
# Excel 转 Markdown
使用 `scripts/convert.py` 将 `.xlsx` 工作簿转换为单个 Markdown 文件,每个工作表形成独立章节。
## 执行边界
- 只接受 `.xlsx`,不把 `.xls` 伪装成受支持格式。
- 默认保留公式文本;只有用户希望读取工作簿内已有缓存值时才使用 `--values`。转换器不会计算公式。
- 合并单元格默认将锚点值填充到合并区域,可用 `--merged anchor` 仅保留左上角值。
- 默认在输入文件旁生成同名 `.md`,已存在时停止;覆盖必须获得用户确认并传入 `--force`。
- 不提取宏、图表、批注、数据验证、条件格式或图片,也不自动安装依赖。
## 工作流
1. 确认公式模式、合并单元格策略和输出路径。
2. 使用工作区依赖运行时执行:
```text
python scripts/convert.py <input.xlsx> [--output <file.md>] [--values] [--merged fill|anchor] [--force]
```
3. 抽查工作表数量、标题、边界行列、公式和转义字符。
4. 报告空工作表、公式缓存为空及未迁移对象等警告。
## 语义说明
Markdown 只能表达二维文本表格。日期以 ISO 可读格式输出,单元格换行转换为 `<br>`,竖线会转义。数字显示可能与 Excel 自定义格式不同;需要报表级显示保真时应使用电子表格工具直接查看源文件。
@@ -0,0 +1,4 @@
interface:
display_name: "XLSX to Markdown"
short_description: "将 Excel 工作簿按工作表转换为 Markdown"
default_prompt: "使用 $xlsx-to-md 将这个 Excel 工作簿转换为 Markdown,并说明公式和合并单元格策略。"
@@ -0,0 +1,6 @@
# 支持范围
- 支持:工作表、二维单元格、公式文本、缓存值、日期、布尔值、错误值、合并区域。
- 降级:自定义数字格式、隐藏行列、筛选状态、富文本、超链接样式。
- 不支持:`.xls`、VBA、图表、图片、批注、数据透视表、公式计算。
- 默认公式模式比缓存值模式更稳定,因为缓存值可能缺失或过期。
@@ -0,0 +1,109 @@
#!/usr/bin/env python3
"""将 XLSX 工作簿的二维数据转换为 Markdown。"""
from __future__ import annotations
import argparse
import datetime as dt
import sys
from pathlib import Path
from openpyxl import load_workbook
def display(value: object) -> str:
"""将单元格值转换为稳定、可读且适合表格的文本。"""
if value is None:
return ""
if isinstance(value, dt.datetime):
value = value.isoformat(sep=" ", timespec="seconds")
elif isinstance(value, (dt.date, dt.time)):
value = value.isoformat()
elif isinstance(value, bool):
value = "TRUE" if value else "FALSE"
text = str(value).replace("\\", "\\\\").replace("|", "\\|")
return text.replace("\r\n", "<br>").replace("\n", "<br>").replace("\r", "<br>")
def matrix_for(sheet, merged_mode: str) -> tuple[list[list[str]], int]:
"""读取有效区域,并按选择的策略表达合并单元格。"""
values = [[cell.value for cell in row] for row in sheet.iter_rows()]
merged_count = len(sheet.merged_cells.ranges)
if merged_mode == "fill":
for merged in sheet.merged_cells.ranges:
anchor = values[merged.min_row - 1][merged.min_col - 1]
for row in range(merged.min_row - 1, merged.max_row):
for column in range(merged.min_col - 1, merged.max_col):
values[row][column] = anchor
while values and all(value is None for value in values[-1]):
values.pop()
width = max((max((i + 1 for i, value in enumerate(row) if value is not None), default=0) for row in values), default=0)
return [[display(value) for value in row[:width]] for row in values], merged_count
def render_sheet(title: str, rows: list[list[str]]) -> str:
"""将单个工作表渲染为 Markdown 章节。"""
lines = [f"## {title}", ""]
if not rows or not rows[0]:
return "\n".join(lines + ["_空工作表_"])
width = max(map(len, rows))
normalized = [row + [""] * (width - len(row)) for row in rows]
lines.extend((f"| {' | '.join(normalized[0])} |", f"| {' | '.join(['---'] * width)} |"))
lines.extend(f"| {' | '.join(row)} |" for row in normalized[1:])
return "\n".join(lines)
def convert(source: Path, output: Path, values_only: bool, merged_mode: str) -> tuple[int, int, int, list[str]]:
"""打开工作簿、转换全部工作表并写入 UTF-8 Markdown。"""
workbook = load_workbook(source, read_only=False, data_only=values_only)
sections = [f"# {source.stem}"]
row_count = merged_count = 0
warnings: list[str] = []
for sheet in workbook.worksheets:
rows, merged = matrix_for(sheet, merged_mode)
sections.append(render_sheet(sheet.title, rows))
row_count += len(rows)
merged_count += merged
if not rows or not rows[0]:
warnings.append(f"空工作表:{sheet.title}")
sheet_count = len(workbook.worksheets)
workbook.close()
output.write_text("\n\n".join(sections).rstrip() + "\n", encoding="utf-8")
return sheet_count, row_count, merged_count, warnings
def main(argv: list[str] | None = None) -> int:
"""校验文件类型、覆盖授权和输出路径后执行转换。"""
parser = argparse.ArgumentParser(description="将 XLSX 工作簿转换为 Markdown")
parser.add_argument("input", type=Path)
parser.add_argument("--output", type=Path)
parser.add_argument("--values", action="store_true", help="读取缓存值而不是公式文本")
parser.add_argument("--merged", choices=("fill", "anchor"), default="fill")
parser.add_argument("--force", action="store_true")
args = parser.parse_args(argv)
source = args.input.resolve()
output = (args.output or source.with_suffix(".md")).resolve()
if not source.is_file() or source.suffix.lower() != ".xlsx":
print("错误:只支持存在的 .xlsx 文件;旧版 .xls 不受支持", file=sys.stderr); return 2
if output.suffix.lower() not in {".md", ".markdown"}:
print("错误:输出文件必须是 Markdown", file=sys.stderr); return 2
if output.exists() and not args.force:
print(f"错误:输出文件已存在:{output}", file=sys.stderr); return 1
output.parent.mkdir(parents=True, exist_ok=True)
try:
sheets, rows, merged, warnings = convert(source, output, args.values, args.merged)
except (OSError, ValueError) as error:
print(f"错误:{error}", file=sys.stderr); return 1
print(f"Markdown:{output}")
print(f"工作表:{sheets},数据行:{rows},合并区域:{merged}")
for warning in warnings: print(f"警告:{warning}")
return 0
if __name__ == "__main__":
raise SystemExit(main())