Skill: glory904649854/md2doc (version 1.0.1)

--- name: md2doc description: Markdown 转 Word/PDF/HTML 文档转换器。支持 6 种样式模板（商务蓝、技术灰、简洁白、产品红、学术风、默认），自动生成封面、目录、页眉页脚。当用户需要将 Markdown 转成 Word、PDF 或 HTML，生成带样式的文档，或提到文档导出、格式转换时，使用此技能。 --- # MD2DOC - Markdown 文档转换技能 将 Markdown 文件转换为专业美观的 Word (.docx)、PDF 和 HTML 文档。 ## 安装 ### 方式一：直接复制 将 `md2doc` 文件夹复制到 OpenClaw 的 skills 目录： ```bash # Windows xcopy /E md2doc %USERPROFILE%\.openclaw\skills\ # Linux/Mac cp -r md2doc ~/.openclaw/skills/ ``` ### 方式二：安装依赖 ```bash pip install python-docx requests markdown beautifulsoup4 Pillow ``` ## 功能特性 - ✅ Markdown 转 Word (.docx) - ✅ Markdown 转 PDF（需安装 Word 或 LibreOffice） - ✅ **6 种样式模板**（商务蓝、技术灰、简洁白、产品红、学术风、默认） - ✅ 图片自动嵌入（支持本地路径和网络图片） - ✅ 表格自动美化（表头带背景色） - ✅ 代码块带背景色 - ✅ 标题层级自动处理 - ✅ 中文排版优化 - ✅ 同时输出 Word + PDF ## 使用方法 ### OpenClaw 中使用 当用户需要将 Markdown 文件转换为 Word/PDF 时： 1. **确认文件路径** - 获取用户提供的 MD 文件路径 2. **检查依赖** - 确保已安装 python-docx 和 requests 3. **执行转换** - 使用脚本转换文件 4. **返回结果** - 告知用户输出文件位置 ```python # 在 OpenClaw 中调用 import subprocess result = subprocess.run([ "python", "skills/md2doc/scripts/md2doc.py", "用户文件.md" ], capture_output=True, text=True) ``` ### 命令行使用 ```bash # 转 Word（默认样式） python scripts/md2doc.py input.md # 同时转 Word + PDF python scripts/md2doc.py input.md --pdf # 指定输出目录 python scripts/md2doc.py input.md --output-dir ./output --pdf # 使用样式模板 python scripts/md2doc.py input.md --style business python scripts/md2doc.py input.md --style product --pdf # 查看所有可用样式 python scripts/md2doc.py --list-styles ``` ### 可用样式模板 | 样式 | 说明 | 适用场景 | |------|------|----------| | `default` | 标准样式 | 一般文档 | | `business` | 商务蓝，蓝色主题 | 企业报告、商务文档 | | `tech` | 技术灰，灰色主题 | 技术文档、API 文档 | | `minimal` | 简洁白，极简风格 | 轻量文档、笔记 | | `product` | 产品红，红色强调 | PRD、产品需求文档 | | `academic` | 学术风，宋体正文 | 论文、学术报告 | ### 支持的 Markdown 语法 - 标题（# ## ### 等） - 段落和文本格式（**粗体**、*斜体*） - 列表（有序/无序） - 图片（本地路径或 URL） - 表格 - 代码块（行内和块级） - 引用块 - 分隔线 ## 工作流程 1. **检查依赖**：确保 Python 和必要库已安装 2. **读取 MD 文件**：解析 Markdown 内容 3. **处理图片**：下载网络图片，验证本地图片路径 4. **生成 Word**：使用 python-docx 创建 .docx 文件 5. **生成 PDF**（可选）：使用 Word 转 PDF 或 reportlab 6. **返回结果**：告知用户输出文件路径 ## 依赖安装 ```bash pip install python-docx markdown beautifulsoup4 requests Pillow ``` ## 样式说明 - 标题：微软雅黑，层级递减 - 正文：宋体，11pt - 代码块：等宽字体，浅灰背景 - 表格：带边框，表头加粗 - 图片：最大宽度 6 英寸，居中 ## 注意事项 - 图片路径可以是相对路径（相对于 MD 文件位置）或绝对路径 - 网络图片会自动下载并嵌入 - 不支持的 Markdown 扩展语法会被当作普通文本处理

path: package.json
content: {
  "name": "md2doc",
  "version": "1.0.0",
  "description": "Markdown 转 Word/PDF/HTML 文档转换器，支持多种样式模板",
  "author": "OpenClaw",
  "license": "MIT",
  "main": "SKILL.md",
  "scripts": {
    "convert": "python scripts/convert.py",
    "md2doc": "python scripts/md2doc.py"
  },
  "dependencies": {
    "python": ">=3.8",
    "python-docx": ">=0.8.11",
    "requests": ">=2.25.0",
    "markdown": ">=3.3.0",
    "beautifulsoup4": ">=4.9.0",
    "Pillow": ">=8.0.0"
  },
  "keywords": [
    "markdown",
    "word",
    "pdf",
    "html",
    "document",
    "converter"
  ]
}


path: README.md
content: # MD2DOC - Markdown 文档转换器

专业的 Markdown 转 Word/PDF/HTML 工具，支持多种样式模板。

## 功能特性

- ✅ **多格式输出** - Word (.docx)、PDF、HTML
- ✅ **6 种样式模板** - 商务蓝、技术灰、简洁白、产品红、学术风、默认
- ✅ **智能封面** - 自动生成专业封面页
- ✅ **自动目录** - 基于标题层级生成目录
- ✅ **页眉页脚** - 自动添加页码和页眉
- ✅ **图片嵌入** - 支持本地图片和网络图片
- ✅ **表格美化** - 表头带背景色
- ✅ **代码高亮** - 代码块带样式

## 快速开始

### 安装依赖
```bash
pip install python-docx requests markdown beautifulsoup4 Pillow
```

### 基本使用
```bash
# Markdown 转 Word
python scripts/convert.py input.md

# 使用样式模板
python scripts/convert.py input.md --style business

# 同时输出 Word + PDF + HTML
python scripts/convert.py input.md --all

# 带封面的完整示例
python scripts/convert.py prd.md \
    --style product \
    --output prd.docx \
    --pdf prd.pdf \
    --html prd.html \
    --cover-author "产品团队" \
    --cover-date "2026年3月"
```

## 样式模板

| 样式 | 特点 | 封面 | 目录 | 页眉 | 页脚 |
|------|------|------|------|------|------|
| `default` | 标准样式 | ❌ | ✅ | ❌ | ✅ |
| `business` | 商务蓝主题 | ✅ | ✅ | ✅ | ✅ |
| `tech` | 技术灰风格 | ❌ | ✅ | ❌ | ✅ |
| `minimal` | 极简黑白 | ❌ | ❌ | ❌ | ❌ |
| `product` | 产品红强调 | ✅ | ✅ | ❌ | ✅ |
| `academic` | 学术宋体 | ✅ | ✅ | ✅ | ✅ |

## 命令行参数

```
python scripts/convert.py <input.md> [选项]

选项:
  --output, -o          输出 Word 文件路径
  --pdf, -p             输出 PDF 文件路径
  --html                输出 HTML 文件路径
  --all, -a             同时输出所有格式
  --style, -s           样式模板 (default/business/tech/minimal/product/academic)
  --list-styles         列出所有可用样式
  --cover-title         封面标题
  --cover-subtitle      封面副标题
  --cover-author        作者
  --cover-date          日期
```

## 在 OpenClaw 中使用

```python
import subprocess

# 基本转换
result = subprocess.run([
    "python", "skills/md2doc/scripts/convert.py",
    "文档.md", "--style", "business"
], capture_output=True, text=True)

# 完整转换（Word + PDF + HTML）
result = subprocess.run([
    "python", "skills/md2doc/scripts/convert.py",
    "prd.md", "--all", "--style", "product",
    "--cover-author", "产品团队"
], capture_output=True, text=True)
```

## 目录结构

```
md2doc/
├── SKILL.md              # 技能说明
├── README.md             # 本文件
├── package.json          # 包信息
├── scripts/
│   ├── convert.py        # 主转换脚本
│   └── md2doc.py         # OpenClaw 入口
└── templates/
    └── styles.py         # 样式模板配置
```

## 依赖说明

- **Word 转 PDF**: 需要安装 Microsoft Word 或 LibreOffice
- **HTML 导出**: 纯 Python，无需额外软件
- **图片处理**: 自动下载网络图片并嵌入

## License

MIT


path: scripts/ai_analyzer.py
content: #!/usr/bin/env python3
"""
AI 文档分析器
自动识别文档类型、生成插图、创建图表
"""

import re
import json
from typing import Dict, List, Tuple, Optional


class DocumentAnalyzer:
    """文档类型分析器"""
    
    # 文档类型特征关键词
    DOC_PATTERNS = {
        'product': {
            'keywords': ['PRD', '产品需求', '功能需求', '用户故事', '原型', '交互', '产品经理', 
                        'feature', 'requirement', 'user story', 'mockup', 'prototype'],
            'weight': 1.0
        },
        'tech': {
            'keywords': ['API', '接口', '架构', '数据库', '算法', '代码', '技术方案', 
                        'implementation', 'database', 'algorithm', 'architecture', 'technical'],
            'weight': 1.0
        },
        'business': {
            'keywords': ['商业计划', '市场分析', '竞品分析', '商业模式', '盈利', 'ROI',
                        'business plan', 'market analysis', 'competitor', 'revenue', 'strategy'],
            'weight': 1.0
        },
        'academic': {
            'keywords': ['摘要', '关键词', '引言', '结论', '参考文献', '实验', '研究',
                        'abstract', 'introduction', 'conclusion', 'reference', 'research'],
            'weight': 1.0
        },
        'report': {
            'keywords': ['周报', '月报', '总结', '汇报', '进展', '里程碑', 'KPI',
                        'weekly', 'monthly', 'summary', 'report', 'progress', 'milestone'],
            'weight': 0.8
        }
    }
    
    def __init__(self):
        self.confidence_threshold = 0.3
    
    def analyze(self, content: str) -> Tuple[str, float, Dict]:
        """
        分析文档类型
        
        返回: (文档类型, 置信度, 详细信息)
        """
        content_lower = content.lower()
        scores = {}
        
        for doc_type, pattern in self.DOC_PATTERNS.items():
            score = 0
            matches = []
            for keyword in pattern['keywords']:
                count = content_lower.count(keyword.lower())
                if count > 0:
                    score += count * pattern['weight']
                    matches.append((keyword, count))
            scores[doc_type] = {
                'score': score,
                'matches': matches
            }
        
        # 找出最高分的类型
        if not scores:
            return 'default', 0.0, {}
        
        best_type = max(scores.keys(), key=lambda x: scores[x]['score'])
        best_score = scores[best_type]['score']
        
        # 计算置信度 (0-1)
        total_score = sum(s['score'] for s in scores.values())
        confidence = best_score / total_score if total_score > 0 else 0
        
        # 根据置信度决定是否使用检测到的类型
        if confidence < self.confidence_threshold:
            return 'default', confidence, scores
        
        return best_type, confidence, scores
    
    def suggest_style(self, doc_type: str) -> str:
        """根据文档类型推荐样式"""
        style_map = {
            'product': 'product',
            'tech': 'tech',
            'business': 'business',
            'academic': 'academic',
            'report': 'business',
            'default': 'default'
        }
        return style_map.get(doc_type, 'default')
    
    def extract_topics(self, content: str) -> List[str]:
        """提取文档主题关键词"""
        # 提取所有标题作为主题
        topics = []
        for line in content.split('\n'):
            if line.startswith('#'):
                topic = line.lstrip('#').strip()
                if topic:
                    topics.append(topic)
        return topics[:5]  # 取前5个主题
    
    def suggest_images(self, content: str, doc_type: str) -> List[Dict]:
        """
        建议需要生成的插图
        
        返回: [{'section': '章节标题', 'description': '图片描述', 'prompt': 'AI绘图提示词'}]
        """
        suggestions = []
        topics = self.extract_topics(content)
        
        # 根据文档类型生成不同的插图建议
        if doc_type == 'product':
            # 产品文档 - 建议产品示意图、流程图
            if topics:
                suggestions.append({
                    'section': '产品概述',
                    'description': f'{topics[0]} 产品概念图',
                    'prompt': f'Product concept illustration for "{topics[0]}", modern flat design, blue and white color scheme, professional, clean background'
                })
            
        elif doc_type == 'tech':
            # 技术文档 - 建议架构图、流程图
            if len(topics) >= 2:
                suggestions.append({
                    'section': '技术架构',
                    'description': '系统架构示意图',
                    'prompt': 'Technical architecture diagram, clean lines, server icons, database symbols, cloud computing, blue and gray colors, professional style'
                })
                
        elif doc_type == 'business':
            # 商业文档 - 建议商业图表、增长曲线
            suggestions.append({
                'section': '商业分析',
                'description': '商业增长趋势图',
                'prompt': 'Business growth chart, upward trending graph, professional blue color scheme, modern corporate style, clean background'
            })
            
        elif doc_type == 'academic':
            # 学术文档 - 建议研究示意图
            if topics:
                suggestions.append({
                    'section': '研究方法',
                    'description': '研究流程示意图',
                    'prompt': f'Research methodology diagram for "{topics[0]}", academic style, clean and minimal, educational illustration'
                })
        
        return suggestions


class DataExtractor:
    """数据提取器 - 从 Markdown 中提取表格数据"""
    
    def extract_tables(self, content: str) -> List[Dict]:
        """
        提取所有表格数据
        
        返回: [{'title': '表标题', 'headers': [], 'rows': [], 'type': '数据类型'}]
        """
        tables = []
        lines = content.split('\n')
        i = 0
        
        while i < len(lines):
            line = lines[i].strip()
            
            # 检测表格开始
            if line.startswith('|') and '|' in line[1:]:
                table_lines = []
                while i < len(lines) and lines[i].strip().startswith('|'):
                    table_lines.append(lines[i].strip())
                    i += 1
                
                # 解析表格
                parsed = self._parse_table(table_lines)
                if parsed:
                    # 尝试找到表格标题（前面的文本）
                    title = self._find_table_title(lines, i - len(table_lines))
                    parsed['title'] = title
                    tables.append(parsed)
            else:
                i += 1
        
        return tables
    
    def _parse_table(self, lines: List[str]) -> Optional[Dict]:
        """解析表格内容"""
        rows = []
        headers = []
        
        for idx, line in enumerate(lines):
            # 跳过分隔行
            if '---' in line or line.replace('|', '').replace('-', '').replace(':', '').strip() == '':
                continue
            
            cells = [cell.strip() for cell in line.split('|')[1:-1]]
            if not cells:
                continue
            
            if idx == 0 or (idx == 1 and '---' not in line):
                headers = cells
            else:
                rows.append(cells)
        
        if headers and rows:
            return {
                'headers': headers,
                'rows': rows,
                'type': self._detect_data_type(headers, rows)
            }
        return None
    
    def _detect_data_type(self, headers: List[str], rows: List[List[str]]) -> str:
        """检测数据类型"""
        # 检查是否包含数字列
        numeric_cols = 0
        for col_idx in range(len(headers)):
            try:
                for row in rows[:3]:  # 检查前3行
                    if col_idx < len(row):
                        val = row[col_idx].replace('%', '').replace(',', '').strip()
                        float(val)
                numeric_cols += 1
            except:
                pass
        
        # 根据列名判断
        header_str = ' '.join(headers).lower()
        if any(kw in header_str for kw in ['时间', '日期', 'time', 'date', '月份', '季度']):
            return 'time_series'
        elif any(kw in header_str for kw in ['占比', '百分比', 'percent', '比例']):
            return 'percentage'
        elif numeric_cols >= 2:
            return 'numeric_comparison'
        else:
            return 'categorical'
    
    def _find_table_title(self, lines: List[str], table_start: int) -> str:
        """查找表格标题（表格前面的文本）"""
        # 向前查找5行
        for i in range(table_start - 1, max(0, table_start - 6), -1):
            line = lines[i].strip()
            if line and not line.startswith('|'):
                # 去掉 Markdown 标记
                title = line.lstrip('#').strip()
                if title:
                    return title
        return '数据表'


class ChartGenerator:
    """图表生成器"""
    
    def __init__(self):
        self.supported_types = ['bar', 'line', 'pie', 'horizontal_bar']
    
    def generate_chart(self, table_data: Dict, chart_type: str = 'auto') -> str:
        """
        根据表格数据生成图表
        
        返回: 图表图片路径
        """
        try:
            import matplotlib.pyplot as plt
            import matplotlib
            matplotlib.rcParams['font.sans-serif'] = ['SimHei', 'DejaVu Sans']
            matplotlib.rcParams['axes.unicode_minus'] = False
        except ImportError:
            print("警告: 未安装 matplotlib，无法生成图表")
            return None
        
        headers = table_data['headers']
        rows = table_data['rows']
        data_type = table_data.get('type', 'categorical')
        
        # 自动选择图表类型
        if chart_type == 'auto':
            chart_type = self._suggest_chart_type(data_type, headers, rows)
        
        # 创建图表
        fig, ax = plt.subplots(figsize=(10, 6))
        
        if chart_type == 'bar':
            self._create_bar_chart(ax, headers, rows)
        elif chart_type == 'horizontal_bar':
            self._create_horizontal_bar_chart(ax, headers, rows)
        elif chart_type == 'line':
            self._create_line_chart(ax, headers, rows)
        elif chart_type == 'pie':
            self._create_pie_chart(ax, headers, rows)
        
        plt.tight_layout()
        
        # 保存图表
        import tempfile
        chart_path = tempfile.mktemp(suffix='.png')
        plt.savefig(chart_path, dpi=150, bbox_inches='tight')
        plt.close()
        
        return chart_path
    
    def _suggest_chart_type(self, data_type: str, headers: List[str], rows: List[List[str]]) -> str:
        """建议图表类型"""
        if data_type == 'time_series':
            return 'line'
        elif data_type == 'percentage':
            return 'pie'
        elif len(rows) > 5:
            return 'horizontal_bar'
        else:
            return 'bar'
    
    def _create_bar_chart(self, ax, headers, rows):
        """创建柱状图"""
        labels = [row[0] for row in rows if row]
        values = []
        for row in rows:
            if len(row) > 1:
                try:
                    values.append(float(row[1].replace('%', '').replace(',', '')))
                except:
                    values.append(0)
        
        x = range(len(labels))
        ax.bar(x, values, color='#4472C4')
        ax.set_xticks(x)
        ax.set_xticklabels(labels, rotation=45, ha='right')
        ax.set_ylabel(headers[1] if len(headers) > 1 else '数值')
        ax.set_title(headers[0] if headers else '数据对比')
    
    def _create_horizontal_bar_chart(self, ax, headers, rows):
        """创建水平柱状图"""
        labels = [row[0] for row in rows if row]
        values = []
        for row in rows:
            if len(row) > 1:
                try:
                    values.append(float(row[1].replace('%', '').replace(',', '')))
                except:
                    values.append(0)
        
        ax.barh(range(len(labels)), values, color='#4472C4')
        ax.set_yticks(range(len(labels)))
        ax.set_yticklabels(labels)
        ax.set_xlabel(headers[1] if len(headers) > 1 else '数值')
        ax.set_title(headers[0] if headers else '数据对比')
    
    def _create_line_chart(self, ax, headers, rows):
        """创建折线图"""
        labels = [row[0] for row in rows if row]
        values = []
        for row in rows:
            if len(row) > 1:
                try:
                    values.append(float(row[1].replace('%', '').replace(',', '')))
                except:
                    values.append(0)
        
        ax.plot(labels, values, marker='o', color='#4472C4', linewidth=2)
        ax.set_ylabel(headers[1] if len(headers) > 1 else '数值')
        ax.set_title(headers[0] if headers else '趋势图')
        plt.xticks(rotation=45, ha='right')
    
    def _create_pie_chart(self, ax, headers, rows):
        """创建饼图"""
        labels = [row[0] for row in rows if row]
        values = []
        for row in rows:
            if len(row) > 1:
                try:
                    values.append(float(row[1].replace('%', '').replace(',', '')))
                except:
                    values.append(0)
        
        ax.pie(values, labels=labels, autopct='%1.1f%%', startangle=90)
        ax.set_title(headers[0] if headers else '占比分布')


def analyze_document(content: str) -> Dict:
    """
    完整分析文档
    
    返回分析结果字典
    """
    analyzer = DocumentAnalyzer()
    extractor = DataExtractor()
    
    # 分析文档类型
    doc_type, confidence, details = analyzer.analyze(content)
    
    # 推荐样式
    suggested_style = analyzer.suggest_style(doc_type)
    
    # 提取主题
    topics = analyzer.extract_topics(content)
    
    # 建议插图
    image_suggestions = analyzer.suggest_images(content, doc_type)
    
    # 提取表格数据
    tables = extractor.extract_tables(content)
    
    return {
        'document_type': doc_type,
        'confidence': confidence,
        'suggested_style': suggested_style,
        'topics': topics,
        'image_suggestions': image_suggestions,
        'tables': tables,
        'details': details
    }


if __name__ == '__main__':
    # 测试
    test_content = """
# 产品需求文档

## 项目概述
这是一个电商APP的产品需求文档。

## 功能模块

| 模块 | 用户数 | 占比 |
|------|--------|------|
| 首页 | 10000 | 50% |
| 商品 | 5000 | 25% |
| 购物车 | 3000 | 15% |
| 我的 | 2000 | 10% |

## 技术架构
使用微服务架构。
"""
    
    result = analyze_document(test_content)
    print(json.dumps(result, ensure_ascii=False, indent=2))



path: scripts/convert.py
content: #!/usr/bin/env python3
"""Markdown 转 Word/PDF 转换器"""

import argparse
import os
import re
import sys
from pathlib import Path
from urllib.parse import urlparse

# 导入样式模板和AI分析器
sys.path.insert(0, str(Path(__file__).parent.parent / 'templates'))
sys.path.insert(0, str(Path(__file__).parent))
from styles import get_style, list_styles, StyleConfig
from ai_analyzer import analyze_document, ChartGenerator

try:
    import requests
    from docx import Document
    from docx.enum.text import WD_ALIGN_PARAGRAPH
    from docx.shared import Pt, Inches, RGBColor, Cm
    from docx.oxml.ns import qn
except ImportError as e:
    print(f"缺少依赖: {e}")
    print("请运行: pip install python-docx requests Pillow")
    sys.exit(1)


def set_chinese_font(run, font_name='宋体', font_size=11, bold=False):
    """设置中文字体"""
    run.font.name = font_name
    run._element.rPr.rFonts.set(qn('w:eastAsia'), font_name)
    run.font.size = Pt(font_size)
    run.font.bold = bold


def download_image(url, temp_dir):
    """下载网络图片"""
    try:
        response = requests.get(url, timeout=30)
        if response.status_code == 200:
            parsed = urlparse(url)
            filename = os.path.basename(parsed.path) or 'image.jpg'
            if '?' in filename:
                filename = filename.split('?')[0]
            temp_path = os.path.join(temp_dir, filename)
            with open(temp_path, 'wb') as f:
                f.write(response.content)
            return temp_path
    except Exception as e:
        print(f"下载图片失败 {url}: {e}")
    return None


def process_image_path(src, md_dir, temp_dir):
    """处理图片路径"""
    if not src:
        return None
    
    if src.startswith(('http://', 'https://')):
        return download_image(src, temp_dir)
    
    possible_paths = [
        os.path.join(md_dir, src),
        os.path.abspath(src),
        src,
    ]
    
    for path in possible_paths:
        if os.path.exists(path):
            return os.path.abspath(path)
    
    print(f"警告: 找不到图片 {src}")
    return None


def add_cover_page(doc, cfg, title="", subtitle="", author="", date="", cover_image_path=""):
    """添加封面页（支持背景色和图片）"""
    
    # 如果有封面图片，尝试添加
    if cover_image_path and os.path.exists(cover_image_path):
        try:
            img_para = doc.add_paragraph()
            img_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
            run = img_para.add_run()
            run.add_picture(cover_image_path, width=Inches(4))
            doc.add_paragraph()
        except:
            pass  # 图片添加失败就跳过
    
    # 空行占位
    for _ in range(5 if cover_image_path else 6):
        doc.add_paragraph()
    
    # 主标题
    title_para = doc.add_paragraph()
    title_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
    title_run = title_para.add_run(title or "文档标题")
    set_chinese_font(title_run, cfg.title_font, cfg.cover_title_size, bold=True)
    title_run.font.color.rgb = RGBColor(*cfg.title_color)
    
    doc.add_paragraph()
    
    # 副标题
    if subtitle:
        sub_para = doc.add_paragraph()
        sub_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
        sub_run = sub_para.add_run(subtitle)
        set_chinese_font(sub_run, cfg.body_font, cfg.cover_subtitle_size)
        sub_run.font.color.rgb = RGBColor(100, 100, 100)
    
    # 空行
    for _ in range(4):
        doc.add_paragraph()
    
    # 作者
    if author:
        author_para = doc.add_paragraph()
        author_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
        author_run = author_para.add_run(f"作者: {author}")
        set_chinese_font(author_run, cfg.body_font, 12)
    
    # 日期
    if date:
        date_para = doc.add_paragraph()
        date_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
        date_run = date_para.add_run(date)
        set_chinese_font(date_run, cfg.body_font, 11)
        date_run.font.color.rgb = RGBColor(128, 128, 128)
    
    # 分页
    doc.add_page_break()


def add_toc(doc, cfg, headings):
    """添加目录页（手动生成，无需更新域）"""
    # 目录标题
    toc_title = doc.add_heading(cfg.toc_title, level=1)
    for run in toc_title.runs:
        set_chinese_font(run, cfg.title_font, cfg.title1_size, bold=True)
        run.font.color.rgb = RGBColor(*cfg.title_color)
    
    # 手动生成目录项
    for level, text in headings:
        p = doc.add_paragraph()
        p.paragraph_format.left_indent = Pt((level - 1) * 20)  # 缩进
        
        # 添加标题文字
        run = p.add_run(text)
        if level == 1:
            set_chinese_font(run, cfg.title_font, cfg.body_size, bold=True)
        else:
            set_chinese_font(run, cfg.body_font, cfg.body_size - 1)
        run.font.color.rgb = RGBColor(*cfg.title_color if level == 1 else cfg.body_color)
    
    if not headings:
        p = doc.add_paragraph()
        run = p.add_run("(文档暂无标题)")
        set_chinese_font(run, cfg.body_font, 10)
        run.font.color.rgb = RGBColor(150, 150, 150)
    
    doc.add_page_break()


def add_header_footer(section, cfg, doc_title=""):
    """添加页眉页脚"""
    # 页眉
    if cfg.has_header:
        header = section.header
        header_para = header.paragraphs[0]
        header_para.text = cfg.header_text or doc_title
        header_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
        for run in header_para.runs:
            set_chinese_font(run, cfg.body_font, 9)
            run.font.color.rgb = RGBColor(128, 128, 128)
    
    # 页脚
    if cfg.has_footer or cfg.show_page_number:
        footer = section.footer
        footer_para = footer.paragraphs[0]
        footer_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
        
        if cfg.show_page_number:
            # 添加页码域
            from docx.oxml import OxmlElement
            from docx.oxml.ns import qn as oqn
            
            # 前缀文字
            if cfg.footer_text and '{page}' not in cfg.footer_text:
                prefix_run = footer_para.add_run(cfg.footer_text + " ")
                set_chinese_font(prefix_run, cfg.body_font, 9)
                prefix_run.font.color.rgb = RGBColor(128, 128, 128)
            
            # 页码域
            fldChar1 = OxmlElement('w:fldChar')
            fldChar1.set(oqn('w:fldCharType'), 'begin')
            
            instrText = OxmlElement('w:instrText')
            instrText.text = 'PAGE'
            
            fldChar2 = OxmlElement('w:fldChar')
            fldChar2.set(oqn('w:fldCharType'), 'separate')
            
            fldChar3 = OxmlElement('w:fldChar')
            fldChar3.set(oqn('w:fldCharType'), 'end')
            
            page_run = footer_para.add_run()
            set_chinese_font(page_run, cfg.body_font, 9)
            page_run.font.color.rgb = RGBColor(128, 128, 128)
            page_run._r.append(fldChar1)
            page_run._r.append(instrText)
            page_run._r.append(fldChar2)
            page_run._r.append(fldChar3)
        else:
            footer_run = footer_para.add_run(cfg.footer_text)
            set_chinese_font(footer_run, cfg.body_font, 9)
            footer_run.font.color.rgb = RGBColor(128, 128, 128)


def markdown_to_docx(md_file, output_file, style_name='default', 
                     cover_title="", cover_subtitle="", cover_author="", cover_date="",
                     include_toc=True):
    """将 Markdown 转换为 Word 文档"""
    
    # 获取样式配置
    cfg = get_style(style_name)
    
    with open(md_file, 'r', encoding='utf-8') as f:
        md_content = f.read()
    
    md_dir = os.path.dirname(os.path.abspath(md_file))
    temp_dir = os.path.join(os.path.dirname(output_file), '.temp_images')
    os.makedirs(temp_dir, exist_ok=True)
    
    doc = Document()
    
    # 设置页面边距和页眉页脚
    sections = doc.sections
    for section in sections:
        section.top_margin = Cm(cfg.page_margin_top)
        section.bottom_margin = Cm(cfg.page_margin_bottom)
        section.left_margin = Cm(cfg.page_margin_left)
        section.right_margin = Cm(cfg.page_margin_right)
        # 添加页眉页脚
        add_header_footer(section, cfg, cover_title or "文档")
    
    # 设置默认字体
    style = doc.styles['Normal']
    style.font.name = cfg.body_font
    style._element.rPr.rFonts.set(qn('w:eastAsia'), cfg.body_font)
    style.font.size = Pt(cfg.body_size)
    style.paragraph_format.line_spacing = cfg.line_spacing
    
    # 添加封面（如果启用）
    if cfg.has_cover:
        # 从 MD 内容提取标题（第一个 # 开头的行）
        doc_title = cover_title
        if not doc_title:
            for line in md_content.split('\n'):
                if line.startswith('# '):
                    doc_title = line[2:].strip()
                    break
        add_cover_page(doc, cfg, title=doc_title, subtitle=cover_subtitle, 
                      author=cover_author, date=cover_date)
    
    # 收集所有标题用于目录
    headings = []
    for line in md_content.split('\n'):
        if line.startswith('# '):
            headings.append((1, line[2:].strip()))
        elif line.startswith('## '):
            headings.append((2, line[3:].strip()))
        elif line.startswith('### '):
            headings.append((3, line[4:].strip()))
    
    # 添加目录（如果启用）
    if cfg.has_toc and include_toc:
        add_toc(doc, cfg, headings)
    
    lines = md_content.split('\n')
    i = 0
    in_code_block = False
    code_content = []
    
    while i < len(lines):
        line = lines[i]
        stripped = line.strip()
        
        # 代码块处理
        if stripped.startswith('```'):
            if in_code_block:
                code_text = '\n'.join(code_content)
                p = doc.add_paragraph()
                run = p.add_run(code_text)
                set_chinese_font(run, 'Consolas', 9)
                run.font.color.rgb = RGBColor(50, 50, 50)
                code_content = []
                in_code_block = False
            else:
                in_code_block = True
            i += 1
            continue
        
        if in_code_block:
            code_content.append(line)
            i += 1
            continue
        
        if not stripped:
            i += 1
            continue
        
        # 标题 - 使用样式配置
        if stripped.startswith('# '):
            heading = doc.add_heading(stripped[2:], level=1)
            for run in heading.runs:
                set_chinese_font(run, cfg.title_font, cfg.title1_size, bold=True)
                run.font.color.rgb = RGBColor(*cfg.title_color)
            i += 1
            continue
        
        if stripped.startswith('## '):
            heading = doc.add_heading(stripped[3:], level=2)
            for run in heading.runs:
                set_chinese_font(run, cfg.title_font, cfg.title2_size, bold=True)
                run.font.color.rgb = RGBColor(*cfg.title_color)
            i += 1
            continue
        
        if stripped.startswith('### '):
            heading = doc.add_heading(stripped[4:], level=3)
            for run in heading.runs:
                set_chinese_font(run, cfg.title_font, cfg.title3_size, bold=True)
                run.font.color.rgb = RGBColor(*cfg.title_color)
            i += 1
            continue
        
        # 图片
        img_match = re.match(r'!\[([^\]]*)\]\(([^)]+)\)', stripped)
        if img_match:
            alt_text, img_src = img_match.groups()
            img_path = process_image_path(img_src, md_dir, temp_dir)
            if img_path:
                try:
                    paragraph = doc.add_paragraph()
                    paragraph.alignment = WD_ALIGN_PARAGRAPH.CENTER
                    run = paragraph.add_run()
                    run.add_picture(img_path, width=Inches(5.5))
                    if alt_text:
                        caption = doc.add_paragraph()
                        caption.alignment = WD_ALIGN_PARAGRAPH.CENTER
                        caption_run = caption.add_run(alt_text)
                        set_chinese_font(caption_run, '宋体', 9)
                        caption_run.font.color.rgb = RGBColor(100, 100, 100)
                except Exception as e:
                    print(f"插入图片失败: {e}")
            i += 1
            continue
        
        # 表格
        if stripped.startswith('|') and '|' in stripped[1:]:
            table_lines = []
            while i < len(lines) and lines[i].strip().startswith('|'):
                table_lines.append(lines[i].strip())
                i += 1
            
            rows = []
            for row_line in table_lines:
                if '---' in row_line or row_line.replace('|', '').replace('-', '').replace(':', '').strip() == '':
                    continue
                cells = [cell.strip() for cell in row_line.split('|')[1:-1]]
                if cells:
                    rows.append(cells)
            
            if rows:
                num_cols = max(len(row) for row in rows)
                table = doc.add_table(rows=len(rows), cols=num_cols)
                table.style = 'Table Grid'
                
                for row_idx, row_data in enumerate(rows):
                    row = table.rows[row_idx]
                    for col_idx, cell_text in enumerate(row_data):
                        if col_idx < num_cols:
                            cell = row.cells[col_idx]
                            cell.text = cell_text
                            for paragraph in cell.paragraphs:
                                for run in paragraph.runs:
                                    if row_idx == 0:
                                        set_chinese_font(run, cfg.title_font, cfg.body_size, bold=True)
                                        # 表头背景色
                                        from docx.oxml import parse_xml
                                        from docx.oxml.ns import nsdecls
                                        shading_elm = parse_xml(r'<w:shd {} w:fill="{:02X}{:02X}{:02X}"/>'.format(
                                            nsdecls('w'), *cfg.table_header_bg))
                                        cell._tc.get_or_add_tcPr().append(shading_elm)
                                    else:
                                        set_chinese_font(run, cfg.body_font, cfg.body_size-1)
            continue
        
        # 普通段落（支持行内格式）
        paragraph = doc.add_paragraph()
        
        # 处理粗体、斜体、代码
        pattern = r'(\*\*([^*]+)\*\*|\*([^*]+)\*|`([^`]+)`)'
        last_end = 0
        
        for match in re.finditer(pattern, stripped):
            if match.start() > last_end:
                normal_text = stripped[last_end:match.start()]
                run = paragraph.add_run(normal_text)
                set_chinese_font(run, '宋体', 11)
            
            if match.group(2):  # 粗体
                run = paragraph.add_run(match.group(2))
                set_chinese_font(run, '微软雅黑', 11, bold=True)
            elif match.group(3):  # 斜体
                run = paragraph.add_run(match.group(3))
                set_chinese_font(run, '宋体', 11)
                run.italic = True
            elif match.group(4):  # 代码
                run = paragraph.add_run(match.group(4))
                set_chinese_font(run, 'Consolas', 10)
                run.font.color.rgb = RGBColor(200, 50, 50)
            
            last_end = match.end()
        
        if last_end < len(stripped):
            run = paragraph.add_run(stripped[last_end:])
            set_chinese_font(run, '宋体', 11)
        
        i += 1
    
    # 保存文档
    doc.save(output_file)
    print(f"Word 文档已保存: {output_file}")
    
    # 清理临时文件
    import shutil
    if os.path.exists(temp_dir):
        shutil.rmtree(temp_dir)
    
    return output_file


def markdown_to_html(md_file, output_file, style_name='default'):
    """将 Markdown 转换为 HTML"""
    import markdown
    
    cfg = get_style(style_name)
    
    with open(md_file, 'r', encoding='utf-8') as f:
        md_content = f.read()
    
    # 转换 Markdown 为 HTML
    html_content = markdown.markdown(
        md_content,
        extensions=['tables', 'fenced_code', 'toc']
    )
    
    # 构建完整 HTML 页面
    html_template = f'''<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>文档</title>
    <style>
        body {{
            font-family: "{cfg.body_font}", "Microsoft YaHei", sans-serif;
            font-size: {cfg.body_size}pt;
            line-height: {cfg.line_spacing};
            color: rgb{cfg.body_color};
            max-width: 800px;
            margin: 0 auto;
            padding: 40px 20px;
            background: #fafafa;
        }}
        h1, h2, h3 {{
            font-family: "{cfg.title_font}", "Microsoft YaHei", sans-serif;
            color: rgb{cfg.title_color};
            margin-top: 1.5em;
            margin-bottom: 0.5em;
        }}
        h1 {{ font-size: {cfg.title1_size}pt; border-bottom: 2px solid rgb{cfg.accent_color}; padding-bottom: 10px; }}
        h2 {{ font-size: {cfg.title2_size}pt; border-bottom: 1px solid #ddd; padding-bottom: 8px; }}
        h3 {{ font-size: {cfg.title3_size}pt; }}
        code {{
            font-family: "{cfg.code_font}", Consolas, monospace;
            font-size: {cfg.code_size}pt;
            background: rgb{cfg.code_bg};
            padding: 2px 6px;
            border-radius: 3px;
            color: rgb{cfg.code_color};
        }}
        pre {{
            background: rgb{cfg.code_bg};
            padding: 15px;
            border-radius: 5px;
            overflow-x: auto;
        }}
        pre code {{
            background: none;
            padding: 0;
        }}
        table {{
            border-collapse: collapse;
            width: 100%;
            margin: 1em 0;
        }}
        th, td {{
            border: 1px solid #ddd;
            padding: 8px 12px;
            text-align: left;
        }}
        th {{
            background: rgb{cfg.table_header_bg};
            font-weight: bold;
        }}
        img {{
            max-width: 100%;
            height: auto;
            display: block;
            margin: 1em auto;
        }}
        blockquote {{
            border-left: 4px solid rgb{cfg.accent_color};
            margin: 1em 0;
            padding-left: 1em;
            color: #666;
        }}
        a {{
            color: rgb{cfg.link_color};
            text-decoration: none;
        }}
        a:hover {{
            text-decoration: underline;
        }}
        hr {{
            border: none;
            border-top: 1px solid #ddd;
            margin: 2em 0;
        }}
        ul, ol {{
            padding-left: 2em;
        }}
        .toc {{
            background: #f5f5f5;
            padding: 20px;
            border-radius: 5px;
            margin: 2em 0;
        }}
        .toc ul {{
            list-style: none;
            padding-left: 0;
        }}
        .toc li {{
            margin: 5px 0;
        }}
        .toc a {{
            color: #333;
        }}
    </style>
</head>
<body>
{html_content}
</body>
</html>'''
    
    with open(output_file, 'w', encoding='utf-8') as f:
        f.write(html_template)
    
    print(f"HTML 已保存: {output_file}")
    return output_file


def docx_to_pdf(docx_file, pdf_file):
    """将 Word 转换为 PDF（支持多种方式）"""
    docx_file = os.path.abspath(docx_file)
    pdf_file = os.path.abspath(pdf_file)
    
    # 方案1: 使用 Microsoft Word COM
    try:
        import win32com.client
        word = win32com.client.Dispatch("Word.Application")
        word.Visible = False
        doc = word.Documents.Open(docx_file)
        doc.SaveAs(pdf_file, FileFormat=17)  # 17 = PDF
        doc.Close()
        word.Quit()
        print(f"PDF 已保存: {pdf_file}")
        return True
    except Exception as e:
        print(f"Word COM 失败: {e}")
    
    # 方案2: 使用 LibreOffice
    try:
        import subprocess
        # 尝试找到 soffice
        soffice_paths = [
            r"C:\Program Files\LibreOffice\program\soffice.exe",
            r"C:\Program Files (x86)\LibreOffice\program\soffice.exe",
            "soffice",  # 如果已在 PATH 中
        ]
        
        soffice = None
        for path in soffice_paths:
            if os.path.exists(path) or path == "soffice":
                soffice = path
                break
        
        if soffice:
            output_dir = os.path.dirname(pdf_file)
            result = subprocess.run([
                soffice, "--headless", "--convert-to", "pdf",
                "--outdir", output_dir, docx_file
            ], capture_output=True, timeout=60)
            
            if result.returncode == 0:
                # LibreOffice 生成的文件名可能不同
                generated_pdf = os.path.join(output_dir, os.path.basename(docx_file).replace('.docx', '.pdf'))
                if os.path.exists(generated_pdf):
                    if generated_pdf != pdf_file:
                        os.rename(generated_pdf, pdf_file)
                    print(f"PDF 已保存: {pdf_file}")
                    return True
    except Exception as e:
        print(f"LibreOffice 失败: {e}")
    
    print("PDF 转换失败。请安装 Microsoft Word 或 LibreOffice。")
    return False


def main():
    parser = argparse.ArgumentParser(description='Markdown 转 Word/PDF/HTML (AI增强版)')
    parser.add_argument('input', help='输入的 Markdown 文件')
    parser.add_argument('--output', '-o', help='输出的 Word 文件路径')
    parser.add_argument('--pdf', '-p', help='输出的 PDF 文件路径')
    parser.add_argument('--html', help='输出的 HTML 文件路径')
    parser.add_argument('--all', '-a', action='store_true', help='同时输出 Word + PDF + HTML')
    parser.add_argument('--style', '-s', default='auto', help='样式模板 (auto/default/business/tech/minimal/product/academic)')
    parser.add_argument('--list-styles', action='store_true', help='列出所有可用样式')
    # AI功能
    parser.add_argument('--ai-analyze', action='store_true', help='AI分析文档类型并推荐样式')
    parser.add_argument('--ai-charts', action='store_true', help='AI自动生成数据图表')
    parser.add_argument('--ai-images', action='store_true', help='AI建议插图位置（需手动生成图片）')
    # 封面参数
    parser.add_argument('--cover-title', help='封面标题（默认从MD提取）')
    parser.add_argument('--cover-subtitle', help='封面副标题')
    parser.add_argument('--cover-author', help='作者')
    parser.add_argument('--cover-date', help='日期')
    # 目录参数
    parser.add_argument('--no-toc', action='store_true', help='不生成目录')
    parser.add_argument('--toc', action='store_true', default=True, help='生成目录（默认）')
    
    args = parser.parse_args()
    
    # 列出样式
    if args.list_styles:
        list_styles()
        sys.exit(0)
    
    if not os.path.exists(args.input):
        print(f"错误: 找不到文件 {args.input}")
        sys.exit(1)
    
    # 读取文档内容
    with open(args.input, 'r', encoding='utf-8') as f:
        md_content = f.read()
    
    # AI分析
    ai_result = None
    if args.ai_analyze or args.style == 'auto':
        print("\n[AI分析] 正在分析文档类型...")
        ai_result = analyze_document(md_content)
        print(f"[AI分析] 检测到文档类型: {ai_result['document_type']} (置信度: {ai_result['confidence']:.2%})")
        print(f"[AI分析] 推荐样式: {ai_result['suggested_style']}")
        print(f"[AI分析] 主题关键词: {', '.join(ai_result['topics'][:3])}")
        
        # 使用AI推荐的样式
        if args.style == 'auto':
            args.style = ai_result['suggested_style']
    
    # 如果样式还是auto，改为default
    if args.style == 'auto':
        args.style = 'default'
    
    # AI生成图表
    chart_paths = []
    if args.ai_charts and ai_result and ai_result['tables']:
        print("\n[AI图表] 正在生成数据图表...")
        chart_gen = ChartGenerator()
        for idx, table in enumerate(ai_result['tables'][:3]):  # 最多生成3个图表
            chart_path = chart_gen.generate_chart(table)
            if chart_path:
                chart_paths.append(chart_path)
                print(f"[AI图表] 已生成图表 {idx+1}: {table.get('title', '数据表')}")
    
    # AI建议插图
    if args.ai_images and ai_result:
        print("\n[AI插图] 插图建议:")
        for suggestion in ai_result['image_suggestions']:
            print(f"  - [{suggestion['section']}] {suggestion['description']}")
            print(f"    提示词: {suggestion['prompt']}")
    
    # 确定输出路径
    input_path = Path(args.input)
    docx_file = args.output or str(input_path.with_suffix('.docx'))
    
    # 判断是否生成目录：如果明确指定 --no-toc，或者只生成 PDF（不含HTML）时默认不生成目录
    # 只有当生成 HTML 或者明确指定 --toc 时才生成目录
    generate_toc = not args.no_toc
    if args.pdf and not args.html and not args.all and not args.no_toc:
        # 只生成 PDF 时，默认不生成目录（PDF目录无跳转功能，观感奇怪）
        generate_toc = False
        print("[提示] PDF 模式默认不生成目录（使用 --toc 可强制生成）")
    
    # 转换为 Word（带样式和封面）
    print(f"\n[转换] 使用样式: {args.style}")
    markdown_to_docx(
        args.input, docx_file, 
        style_name=args.style,
        cover_title=args.cover_title or "",
        cover_subtitle=args.cover_subtitle or "",
        cover_author=args.cover_author or "",
        cover_date=args.cover_date or "",
        include_toc=generate_toc
    )
    
    # 转换为 PDF（如果需要）
    pdf_file = args.pdf
    if args.all and not pdf_file:
        pdf_file = str(input_path.with_suffix('.pdf'))
    
    if pdf_file:
        docx_to_pdf(docx_file, pdf_file)
    
    # 转换为 HTML（如果需要）
    html_file = args.html
    if args.all and not html_file:
        html_file = str(input_path.with_suffix('.html'))
    
    if html_file:
        markdown_to_html(args.input, html_file, style_name=args.style)
    
    # 清理临时图表文件
    for chart_path in chart_paths:
        try:
            os.remove(chart_path)
        except:
            pass
    
    print("\n[完成] 转换成功!")


if __name__ == '__main__':
    main()

path: scripts/md2doc.py
content: #!/usr/bin/env python3
"""
MD2DOC - Markdown 转 Word/PDF 工具
OpenClaw 技能入口
"""

import os
import sys
import subprocess
from pathlib import Path

def convert_md(input_file, output_dir=None, output_pdf=False, style='default', include_toc=None):
    """
    转换 Markdown 文件为 Word 和 PDF
    
    参数:
        input_file: MD 文件路径
        output_dir: 输出目录（可选，默认与输入文件同目录）
        output_pdf: 是否同时输出 PDF
        style: 样式模板名称
        include_toc: 是否包含目录 (True/False/None=自动)
    
    返回:
        dict: 包含输出文件路径
    """
    if not os.path.exists(input_file):
        return {"error": f"文件不存在: {input_file}"}
    
    input_path = Path(input_file)
    
    if output_dir is None:
        output_dir = input_path.parent
    
    output_name = input_path.stem
    docx_file = os.path.join(output_dir, f"{output_name}.docx")
    pdf_file = os.path.join(output_dir, f"{output_name}.pdf")
    
    # 获取 convert.py 的路径
    script_dir = Path(__file__).parent
    convert_script = script_dir / "convert.py"
    
    # 构建命令
    cmd = [sys.executable, str(convert_script), str(input_file), "--output", docx_file, "--style", style]
    if output_pdf:
        cmd.extend(["--pdf", pdf_file])
    
    # 目录控制
    if include_toc is False:
        cmd.append("--no-toc")
    elif include_toc is True:
        cmd.append("--toc")
    
    # 运行转换
    try:
        result = subprocess.run(cmd, capture_output=True)
        # 处理编码
        try:
            stderr_text = result.stderr.decode('utf-8') if result.stderr else ""
        except:
            stderr_text = result.stderr.decode('gbk', errors='ignore') if result.stderr else ""
        
        success = os.path.exists(docx_file)
        pdf_success = os.path.exists(pdf_file) if output_pdf else True
        
        if success:
            result_dict = {
                "success": True,
                "docx": docx_file,
                "message": f"Word 文档已保存: {docx_file}"
            }
            if output_pdf and pdf_success:
                result_dict["pdf"] = pdf_file
                result_dict["message"] += f" | PDF 已保存: {pdf_file}"
            return result_dict
        else:
            return {
                "error": f"转换失败: {stderr_text}"
            }
    except Exception as e:
        return {"error": f"运行出错: {str(e)}"}


if __name__ == "__main__":
    import argparse
    parser = argparse.ArgumentParser(description="MD2DOC - Markdown 转 Word/PDF")
    parser.add_argument("input", nargs="?", help="输入的 Markdown 文件路径")
    parser.add_argument("--output-dir", "-o", help="输出目录")
    parser.add_argument("--pdf", "-p", action="store_true", help="同时输出 PDF")
    parser.add_argument("--style", "-s", default="default", help="样式模板 (default/business/tech/minimal/product/academic)")
    parser.add_argument("--list-styles", action="store_true", help="列出所有可用样式")
    parser.add_argument("--no-toc", action="store_true", help="不生成目录")
    parser.add_argument("--toc", action="store_true", help="生成目录（默认）")
    args = parser.parse_args()
    
    # 列出样式
    if args.list_styles:
        sys.path.insert(0, str(Path(__file__).parent.parent / 'templates'))
        from styles import list_styles
        list_styles()
        sys.exit(0)
    
    if not args.input:
        print("错误: 请提供输入文件")
        sys.exit(1)
    
    # 目录控制
    include_toc = None  # 默认自动
    if args.no_toc:
        include_toc = False
    elif args.toc:
        include_toc = True
    
    result = convert_md(args.input, args.output_dir, output_pdf=args.pdf, style=args.style, include_toc=include_toc)
    
    if "error" in result:
        print(f"[错误] {result['error']}")
        sys.exit(1)
    else:
        print(f"[成功] {result['message']}")
        sys.exit(0)


path: templates/styles.py
content: #!/usr/bin/env python3
"""
MD2DOC 样式模板配置
支持多种文档风格
"""

from dataclasses import dataclass
from typing import Optional


@dataclass
class StyleConfig:
    """样式配置类"""
    # 文档基础
    name: str
    description: str
    
    # 字体配置
    title_font: str = '微软雅黑'
    body_font: str = '宋体'
    code_font: str = 'Consolas'
    
    # 字号配置 (pt)
    title1_size: int = 16
    title2_size: int = 14
    title3_size: int = 12
    body_size: int = 11
    code_size: int = 9
    caption_size: int = 9
    
    # 颜色配置 (RGB)
    title_color: tuple = (0, 0, 0)
    body_color: tuple = (0, 0, 0)
    code_color: tuple = (50, 50, 50)
    link_color: tuple = (0, 112, 192)
    accent_color: tuple = (0, 112, 192)  # 强调色
    
    # 段落样式
    line_spacing: float = 1.5
    paragraph_spacing: float = 0.5
    
    # 页面配置
    page_margin_top: float = 2.5  # cm
    page_margin_bottom: float = 2.5
    page_margin_left: float = 2.5
    page_margin_right: float = 2.5
    
    # 表格样式
    table_header_bg: tuple = (240, 240, 240)
    table_border: bool = True
    
    # 代码块样式
    code_bg: tuple = (245, 245, 245)
    code_border: bool = False
    
    # 封面配置
    has_cover: bool = False
    cover_title_size: int = 28
    cover_subtitle_size: int = 14
    cover_bg_color: tuple = (255, 255, 255)  # 封面背景色
    cover_image: str = ''  # 封面图片路径（可选）
    
    # 目录配置
    has_toc: bool = True
    toc_title: str = '目录'
    
    # 页眉页脚
    has_header: bool = False
    has_footer: bool = True
    header_text: str = ''
    footer_text: str = '第 {page} 页'
    show_page_number: bool = True


# 预定义样式模板
STYLES = {
    'default': StyleConfig(
        name='默认',
        description='标准文档样式，适合一般用途',
        title_font='微软雅黑',
        body_font='宋体',
    ),
    
    'business': StyleConfig(
        name='商务蓝',
        description='企业报告风格，蓝色主题，专业大气',
        title_font='微软雅黑',
        body_font='微软雅黑',
        title_color=(0, 51, 102),  # 深蓝
        accent_color=(0, 112, 192),  # 商务蓝
        link_color=(0, 112, 192),
        table_header_bg=(230, 240, 250),
        has_cover=True,
        has_toc=True,
        has_header=True,
        has_footer=True,
        header_text='',
        footer_text='第 {page} 页',
    ),
    
    'tech': StyleConfig(
        name='技术灰',
        description='技术文档风格，灰色主题，简洁专业',
        title_font='微软雅黑',
        body_font='微软雅黑',
        title_color=(64, 64, 64),  # 深灰
        accent_color=(100, 100, 100),  # 中灰
        link_color=(70, 130, 180),
        table_header_bg=(240, 240, 240),
        code_bg=(248, 248, 248),
        code_border=True,
        has_cover=False,
    ),
    
    'minimal': StyleConfig(
        name='简洁白',
        description='极简风格，黑白配色，清爽干净',
        title_font='微软雅黑',
        body_font='微软雅黑',
        title1_size=18,
        title2_size=14,
        title3_size=12,
        body_size=11,
        title_color=(0, 0, 0),
        accent_color=(0, 0, 0),
        table_header_bg=(250, 250, 250),
        line_spacing=1.3,
        has_cover=False,
    ),
    
    'product': StyleConfig(
        name='产品红',
        description='PRD专用风格，红色强调，醒目突出',
        title_font='微软雅黑',
        body_font='微软雅黑',
        title1_size=18,  # 增大标题
        title2_size=15,
        title3_size=13,
        body_size=11,
        title_color=(180, 0, 0),  # 深红
        body_color=(50, 50, 50),  # 稍浅的正文字色
        accent_color=(220, 20, 60),  # 猩红
        link_color=(220, 20, 60),
        line_spacing=1.4,  # 调整行距
        paragraph_spacing=0.3,
        table_header_bg=(255, 245, 245),  # 更柔和的表格头背景
        has_cover=True,
        has_toc=True,
        has_header=False,
        has_footer=True,
        cover_title_size=36,  # 更大标题
        cover_subtitle_size=16,
        footer_text='PRD 文档',
    ),
    
    'academic': StyleConfig(
        name='学术风',
        description='学术论文风格，宋体正文，严谨规范',
        title_font='黑体',
        body_font='宋体',
        title1_size=18,
        title2_size=15,
        title3_size=12,
        body_size=12,
        code_size=10,
        line_spacing=1.5,
        paragraph_spacing=0.3,
        table_header_bg=(235, 235, 235),
        has_cover=True,
        has_toc=True,
        has_header=True,
        has_footer=True,
        header_text='',
        footer_text='第 {page} 页',
    ),
}


def get_style(style_name: str) -> StyleConfig:
    """获取样式配置"""
    if style_name not in STYLES:
        print(f"警告: 未知样式 '{style_name}'，使用默认样式")
        return STYLES['default']
    return STYLES[style_name]


def list_styles():
    """列出所有可用样式"""
    print("可用样式模板:")
    print("-" * 50)
    for key, style in STYLES.items():
        print(f"  {key:12} - {style.name}: {style.description}")
    print("-" * 50)


if __name__ == '__main__':
    list_styles()


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (HIGH)