1
0

增加lyxy-reader-office skill

This commit is contained in:
2026-02-17 22:50:06 +08:00
parent 9f686270c2
commit 9f04dac50b
25 changed files with 609 additions and 1282 deletions

View File

@@ -6,7 +6,14 @@
"WebFetch(domain:pypi.org)", "WebFetch(domain:pypi.org)",
"WebFetch(domain:github.com)", "WebFetch(domain:github.com)",
"Bash(pip index:*)", "Bash(pip index:*)",
"Bash(pip show:*)" "Bash(pip show:*)",
"Bash(openspec status:*)",
"Bash(openspec instructions proposal:*)",
"Bash(openspec instructions design:*)",
"Bash(openspec instructions specs:*)",
"Bash(openspec instructions tasks:*)",
"Bash(openspec new:*)",
"Bash(openspec instructions apply:*)"
] ]
} }
} }

View File

@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-02-17

View File

@@ -0,0 +1,76 @@
## Context
当前项目中存在 `lyxy-reader-docx` skill仅支持 DOCX 格式的文档解析。`skills/lyxy-reader-office/scripts/` 目录下已完成全部解析脚本的开发parser.py、common.py、docx_parser.py、pptx_parser.py、xlsx_parser.py、pdf_parser.py、README.md支持 DOCX、PPTX、XLSX、PDF 四种格式。现在需要创建 `SKILL.md` 作为 skill 的入口文件,并清理被替代的旧 skill。
已有资源:
- `skills/lyxy-reader-docx/SKILL.md`:参考模板,已验证的 skill 结构
- `skills/lyxy-runner-python/SKILL.md`Python 执行 skill与本 skill 协作
- `skills/lyxy-reader-office/scripts/README.md`:完整的脚本使用文档
- `document/specification.md`skill 格式规范
## Goals / Non-Goals
**Goals:**
- 创建 `skills/lyxy-reader-office/SKILL.md`,遵循 skill 格式规范
- SKILL.md 的 description 能覆盖 .docx、.xlsx、.pptx、.pdf 四种文件扩展名关键词,确保大模型在发现阶段能正确匹配
- 引导大模型优先使用该 skill 读取这四种格式的文件
- 引导大模型阅读 `scripts/README.md` 获取详细的脚本使用说明
- 引导大模型在 `lyxy-runner-python` skill 可用时必须使用该 skill 来运行 Python 脚本
- 删除 `skills/lyxy-reader-docx` 目录
**Non-Goals:**
- 不修改已有的解析脚本代码
- 不修改 `lyxy-runner-python` skill
- 不新增任何解析功能
- 不创建单独的文档说明文件(如旧 skill 的 `docx_parser.md`),详细用法通过引导阅读 README.md 解决
## Decisions
### 决策 1SKILL.md 结构参考 lyxy-reader-docx 但大幅精简
**选择**SKILL.md 专注于发现description、激活引导何时使用、触发条件和执行入口指向 README.md不在 SKILL.md 中重复 README.md 的内容。
**理由**
- 规范建议 SKILL.md 保持在 500 行以下
- README.md 已包含完整的命令行用法、参数说明、安装指南、错误处理等
- 重复内容会导致维护负担,且增加不必要的 token 消耗
- 渐进式披露SKILL.md 负责激活判断README.md 负责执行细节
**替代方案**:将 README.md 全部内容嵌入 SKILL.md — 拒绝,因为会超过 500 行限制且违反渐进式披露原则。
### 决策 2强制引导使用 lyxy-runner-python
**选择**:在 SKILL.md 中明确要求大模型在 `lyxy-runner-python` skill 可用时**必须**使用该 skill 来运行 parser.py不提供直接 Python 执行的选项。仅在 `lyxy-runner-python` 不可用时降级到直接 Python 执行。
**理由**
- lyxy-runner-python 使用 uv 管理依赖,能自动安装解析器所需的第三方库
- 环境隔离,不污染系统 Python
- 与 lyxy-reader-docx 保持一致的执行策略
**替代方案**:将 lyxy-runner-python 作为可选推荐 — 拒绝,因为用户明确要求"必须使用"。
### 决策 3通过引导阅读 README.md 提供详细用法
**选择**SKILL.md 中仅给出基本的执行命令格式和常见示例,详细的参数说明、依赖安装、解析器对比等内容通过引导大模型阅读 `scripts/README.md` 获取。
**理由**
- 符合渐进式披露原则SKILL.md 是指令层README.md 是资源层
- README.md 已有完整且结构化的文档,无需重复
- 减少 SKILL.md 的体积,提高激活阶段的效率
### 决策 4删除 lyxy-reader-docx 而非保留兼容
**选择**:直接删除整个 `skills/lyxy-reader-docx/` 目录。
**理由**
- lyxy-reader-office 完全覆盖了 lyxy-reader-docx 的功能
- 保留旧 skill 会导致两个 skill 争抢 .docx 文件的处理权,造成歧义
- 用户明确要求删除
## Risks / Trade-offs
- **[旧 skill 引用残留]** → 检查项目中是否有其他地方引用了 `lyxy-reader-docx`(如 .claude/settings 中的 skill 配置),删除时需同步清理
- **[description 覆盖度]** → description 需要包含 docx、xlsx、pptx、pdf 等关键词,确保大模型在发现阶段能匹配到四种文件类型。但 description 有 1024 字符限制,需要精简表达
- **[SKILL.md 体积控制]** → 需在"提供足够的执行引导"和"保持 500 行以内"之间平衡。通过引导阅读 README.md 解决详细信息的需求

View File

@@ -0,0 +1,30 @@
## Why
当前项目中仅有 `lyxy-reader-docx` skill 用于解析 DOCX 文档,无法覆盖 XLSX、PPTX、PDF 等常见办公文档格式。用户在日常工作中经常需要大模型读取多种格式的文档,缺少统一的多格式文档解析能力会导致大模型无法有效处理这些文件。需要创建一个统一的办公文档解析 skill覆盖四种主流格式并替换功能已被覆盖的旧 skill。
## What Changes
- 新增 `lyxy-reader-office` skill支持解析 DOCX、XLSX、PPTX、PDF 四种格式
- 该 skill 使用 `scripts/parser.py` 作为统一入口,自动识别文件类型并分派到对应的格式解析器
- 引导大模型在遇到这四种文件时优先激活并使用该 skill
- 引导大模型通过阅读 `scripts/README.md` 了解脚本的详细使用方式
- 引导大模型在环境中存在 `lyxy-runner-python` skill 时,必须使用该 skill 来运行 Python 脚本
- **BREAKING**:删除 `skills/lyxy-reader-docx` 目录,因为其功能已完全被 `lyxy-reader-office` 覆盖
## Capabilities
### New Capabilities
- `office-document-parsing`: 统一的办公文档解析能力,覆盖 DOCX、XLSX、PPTX、PDF 四种格式支持全文提取、标题提取、章节提取、正则搜索、字数统计、行数统计等功能PDF 额外支持 OCR 高精度模式
### Modified Capabilities
- `docx-text-extraction`: 该能力将被 `office-document-parsing` 完全替代,原 spec 不再适用
## Impact
- **新增文件**`skills/lyxy-reader-office/SKILL.md`skill 主文件)
- **脚本文件**`skills/lyxy-reader-office/scripts/` 下的所有解析脚本已就绪parser.py、common.py、docx_parser.py、pptx_parser.py、xlsx_parser.py、pdf_parser.py、README.md
- **删除目录**`skills/lyxy-reader-docx/`(整个目录,包含 SKILL.md、docx_parser.md、scripts/docx_parser.py
- **依赖关系**:运行时依赖 Python 3.6+,推荐通过 `lyxy-runner-python` skill 使用 `uv` 自动管理依赖
- **Spec 变更**`docx-text-extraction` spec 将被废弃,新增 `office-document-parsing` spec

View File

@@ -0,0 +1,29 @@
## REMOVED Requirements
### Requirement: Delegate execution to lyxy-runner-python skill
**Reason**: 该能力已被 `office-document-parsing` spec 中的「必须通过 lyxy-runner-python 执行脚本」需求完全替代
**Migration**: 使用 lyxy-reader-office skill 替代 lyxy-reader-docx skill
### Requirement: Extract full text from DOCX file
**Reason**: 该能力已被 `office-document-parsing` spec 中的「支持多格式文档解析」需求完全替代
**Migration**: 使用 lyxy-reader-office skill 的 parser.py 解析 DOCX 文件
### Requirement: Extract document metadata
**Reason**: 该能力已被 `office-document-parsing` spec 中的「统一的查询功能」需求完全替代
**Migration**: 使用 lyxy-reader-office skill 的 `-c``-l` 参数
### Requirement: Extract document titles
**Reason**: 该能力已被 `office-document-parsing` spec 中的「统一的查询功能」需求完全替代
**Migration**: 使用 lyxy-reader-office skill 的 `-t` 参数
### Requirement: Extract chapter content by title name
**Reason**: 该能力已被 `office-document-parsing` spec 中的「统一的查询功能」需求完全替代
**Migration**: 使用 lyxy-reader-office skill 的 `-tc` 参数
### Requirement: Search document with regex
**Reason**: 该能力已被 `office-document-parsing` spec 中的「统一的查询功能」需求完全替代
**Migration**: 使用 lyxy-reader-office skill 的 `-s` 参数
### Requirement: Multi-strategy parsing fallback
**Reason**: 该能力已被 `office-document-parsing` spec 中的「多策略解析降级」需求完全替代,且新版本支持更多格式和更多解析策略
**Migration**: 使用 lyxy-reader-office skill支持 DOCX/PPTX/XLSX/PDF 四种格式的多策略降级

View File

@@ -0,0 +1,115 @@
## ADDED Requirements
### Requirement: 优先使用 lyxy-reader-office 解析办公文档
大模型在遇到 .docx、.xlsx、.pptx、.pdf 文件时SHALL 优先激活并使用 lyxy-reader-office skill 来读取文件内容。
#### Scenario: 用户请求读取支持的文件格式
- **WHEN** 用户请求读取或解析 .docx、.xlsx、.pptx 或 .pdf 文件
- **THEN** 大模型 SHALL 激活 lyxy-reader-office skill
- **AND** 使用 skill 目录下的 `scripts/parser.py` 执行解析
#### Scenario: 文件扩展名自动识别
- **WHEN** 用户提供的文件路径以 .docx、.xlsx、.pptx 或 .pdf 结尾
- **THEN** 大模型 SHALL 自动识别为 lyxy-reader-office skill 的处理范围
- **AND** 无需用户显式指定使用哪个 skill
### Requirement: 必须通过 lyxy-runner-python 执行脚本
当环境中存在 lyxy-runner-python skill 时,大模型 SHALL 必须使用该 skill 来运行 parser.py 脚本。
#### Scenario: lyxy-runner-python 可用
- **WHEN** 大模型环境中存在 lyxy-runner-python skill
- **THEN** 大模型 SHALL 通过 lyxy-runner-python skill 执行 parser.py
- **AND** 利用 lyxy-runner-python 的自动依赖管理功能uv安装所需的 Python 包
#### Scenario: lyxy-runner-python 不可用
- **WHEN** 大模型环境中不存在 lyxy-runner-python skill
- **THEN** 大模型 SHALL 降级到直接使用 Python 执行 parser.py
- **AND** 提示用户当前使用直接执行模式
- **AND** 禁止自动执行 pip install 安装依赖
### Requirement: 引导阅读 README 获取详细用法
大模型在需要了解 parser.py 的详细使用方式时SHALL 阅读 `scripts/README.md` 文件。
#### Scenario: 首次使用 skill 执行解析
- **WHEN** 大模型首次使用 lyxy-reader-office skill 或不确定具体参数用法
- **THEN** 大模型 SHALL 阅读 `scripts/README.md` 获取命令行参数、依赖安装和使用示例等详细信息
#### Scenario: 遇到特殊参数需求
- **WHEN** 用户请求使用特殊功能(如 PDF OCR、章节提取、正则搜索等
- **THEN** 大模型 SHALL 参考 `scripts/README.md` 中的对应参数说明
### Requirement: 支持多格式文档解析
系统 SHALL 支持 DOCX、PPTX、XLSX、PDF 四种格式的文档解析,将文件转换为 Markdown 格式输出。
#### Scenario: 解析 DOCX 文件
- **WHEN** 用户请求解析 .docx 文件
- **THEN** 系统返回完整的 Markdown 格式文本
- **AND** 保留标题、列表、表格、粗体、斜体等格式
- **AND** 移除图片
#### Scenario: 解析 PPTX 文件
- **WHEN** 用户请求解析 .pptx 文件
- **THEN** 系统返回完整的 Markdown 格式文本
- **AND** 每张幻灯片以 `## Slide N` 为标题
- **AND** 幻灯片之间以 `---` 分隔
#### Scenario: 解析 XLSX 文件
- **WHEN** 用户请求解析 .xlsx 文件
- **THEN** 系统返回完整的 Markdown 格式文本
- **AND** 以 `## SheetName` 区分工作表
- **AND** 数据以 Markdown 表格呈现
#### Scenario: 解析 PDF 文件
- **WHEN** 用户请求解析 .pdf 文件
- **THEN** 系统返回完整的 Markdown 格式文本
- **AND** 默认使用普通文本提取模式
#### Scenario: PDF OCR 高精度模式
- **WHEN** 用户请求对 PDF 文件启用 OCR 或高精度解析
- **THEN** 系统使用 `--high-res` 参数启用 OCR 版面分析
- **AND** 通过 Docling OCR 或 unstructured hi_res 策略处理
### Requirement: 统一的查询功能
系统 SHALL 对所有支持的文件格式提供统一的查询接口,包括全文提取、元数据查询、标题提取、章节提取和正则搜索。
#### Scenario: 获取文档字数
- **WHEN** 用户请求获取文档的字数
- **THEN** 系统使用 `-c` 参数返回文档的总字符数
#### Scenario: 获取文档行数
- **WHEN** 用户请求获取文档的行数
- **THEN** 系统使用 `-l` 参数返回文档的总行数
#### Scenario: 提取所有标题
- **WHEN** 用户请求提取文档的标题结构
- **THEN** 系统使用 `-t` 参数返回所有 1-6 级标题
#### Scenario: 提取指定章节内容
- **WHEN** 用户请求提取特定标题名称的章节内容
- **THEN** 系统使用 `-tc` 参数返回该章节的完整内容
- **AND** 包含完整的上级标题链和所有下级内容
#### Scenario: 正则表达式搜索
- **WHEN** 用户请求在文档中搜索关键词或模式
- **THEN** 系统使用 `-s` 参数返回所有匹配结果及上下文
- **AND** 默认包含前后各 2 行非空行上下文
- **AND** 支持 `-n` 参数自定义上下文行数
### Requirement: 多策略解析降级
系统 SHALL 对每种文件格式按优先级尝试多种解析策略,确保最大的兼容性。
#### Scenario: 解析器按优先级降级
- **WHEN** 优先级最高的解析器不可用或解析失败
- **THEN** 系统自动尝试下一优先级的解析器
- **AND** 记录每个解析器的失败原因
#### Scenario: 所有解析器失败
- **WHEN** 某种格式的所有解析策略均失败
- **THEN** 系统返回详细的失败信息
- **AND** 列出每种解析策略的失败原因
- **AND** 以退出码 1 退出
#### Scenario: DOCX/PPTX/XLSX 无依赖运行
- **WHEN** 未安装任何第三方解析库
- **THEN** DOCX、PPTX、XLSX 文件 SHALL 仍可通过内置 XML 原生解析工作
- **AND** PDF 至少需要 pypdf 才能解析

View File

@@ -0,0 +1,22 @@
## 1. 创建 SKILL.md
- [x] 1.1 创建 `skills/lyxy-reader-office/SKILL.md` 文件,包含符合规范的 YAML frontmattername、description、compatibility
- [x] 1.2 编写 SKILL.md 正文Purpose 部分,说明 skill 用途及与 lyxy-runner-python 的协作关系(必须使用 lyxy-runner-python不可用时降级到直接 Python 执行)
- [x] 1.3 编写 SKILL.md 正文When to Use 部分,列出典型场景、触发词和支持的文件扩展名(.docx、.xlsx、.pptx、.pdf
- [x] 1.4 编写 SKILL.md 正文Capabilities 部分,概述四种格式的解析能力和统一查询功能(全文提取、元数据、标题、章节、搜索)
- [x] 1.5 编写 SKILL.md 正文Execution 部分,给出基本的执行命令格式,并引导大模型阅读 `scripts/README.md` 获取详细参数说明和依赖安装指南
- [x] 1.6 编写 SKILL.md 正文Examples 部分,给出各格式的基本使用示例
- [x] 1.7 编写 SKILL.md 正文Notes 部分,说明限制和注意事项
- [x] 1.8 验证 SKILL.md 总行数不超过 500 行,符合渐进式披露原则
## 2. 清理旧 Skill
- [x] 2.1 删除 `skills/lyxy-reader-docx/` 整个目录(包含 SKILL.md、docx_parser.md、scripts/docx_parser.py
- [x] 2.2 检查项目中是否有其他位置引用了 `lyxy-reader-docx`(如 .claude/settings 配置),如有则清理引用
## 3. 验证
- [x] 3.1 确认 `skills/lyxy-reader-office/SKILL.md` 存在且 frontmatter 格式正确
- [x] 3.2 确认 `skills/lyxy-reader-office/scripts/` 下所有脚本文件完整parser.py、common.py、docx_parser.py、pptx_parser.py、xlsx_parser.py、pdf_parser.py、README.md
- [x] 3.3 确认 `skills/lyxy-reader-docx/` 目录已被删除
- [x] 3.4 使用各格式的测试文件temp/test.docx、temp/test.xlsx、temp/test.pptx、temp/test.pdf验证 parser.py 可正常运行

View File

@@ -1,132 +0,0 @@
## Requirements
### Requirement: Delegate execution to lyxy-runner-python skill
当大模型执行 lyxy-reader-docx skill 时,应优先使用 lyxy-runner-python skill 来运行 docx_parser.py 脚本。如果 lyxy-runner-python skill 不可用,则直接使用 Python 执行。
#### Scenario: lyxy-runner-python available
- **WHEN** 大模型环境中存在 lyxy-runner-python skill
- **THEN** 大模型通过 lyxy-runner-python skill 执行 docx_parser.py
- **AND** 利用 lyxy-runner-python 的自动依赖管理功能安装所需的 Python 包markitdown 或 python-docx
#### Scenario: lyxy-runner-python unavailable
- **WHEN** 大模型环境中不存在 lyxy-runner-python skill
- **THEN** 大模型直接使用 Python 执行 docx_parser.py
- **AND** 提示用户正在使用直接执行模式(未使用 lyxy-runner-python
- **AND** 依赖 docx_parser.py 内部的多策略解析降级机制
### Requirement: Extract full text from DOCX file
系统 SHALL 能够将 DOCX 文件完整解析为 Markdown 格式的文本内容,移除所有图片但保留文本格式(如标题、列表、粗体、斜体、表格)。
#### Scenario: Successful full text extraction
- **WHEN** 用户请求解析一个有效的 .docx 文件
- **THEN** 系统返回完整的 Markdown 格式文本
- **AND** 所有文本内容被保留
- **AND** 所有图片被移除
- **AND** 表格、列表、粗体、斜体等格式被转换为 Markdown 语法
- **AND** 连续的空行被合并为一个空行
#### Scenario: Invalid DOCX file
- **WHEN** 提供的文件不是有效的 DOCX 格式或已损坏
- **THEN** 系统返回错误信息
- **AND** 错误信息明确说明文件格式问题
#### Scenario: Empty DOCX file
- **WHEN** 提供的 DOCX 文件为空或无文本内容
- **THEN** 系统返回空字符串或相应的提示信息
### Requirement: Extract document metadata
系统 SHALL 能够提供 DOCX 文档的元数据信息,包括字数和行数。
#### Scenario: Get word count
- **WHEN** 用户请求获取 DOCX 文档的字数
- **THEN** 系统返回文档的总字数(数字)
- **AND** 字数统计基于解析后的 Markdown 内容
#### Scenario: Get line count
- **WHEN** 用户请求获取 DOCX 文档的行数
- **THEN** 系统返回文档的总行数(数字)
- **AND** 行数统计基于解析后的 Markdown 内容
### Requirement: Extract document titles
系统 SHALL 能够提取 DOCX 文档中的所有标题1-6级标题并按原始层级关系返回。
#### Scenario: Extract all titles
- **WHEN** 用户请求提取 DOCX 文档的所有标题
- **THEN** 系统返回所有 1-6 级标题
- **AND** 每个标题以 Markdown 标题格式返回(如 `# 主标题``## 二级标题`
- **AND** 标题按文档中的原始顺序返回
- **AND** 保留原始标题层级关系
#### Scenario: Document with no titles
- **WHEN** DOCX 文档中不包含任何标题
- **THEN** 系统返回空列表或无标题提示
### Requirement: Extract chapter content by title name
系统 SHALL 能够根据标题名称提取指定章节的内容,包括完整的上级标题链和所有下级内容。
#### Scenario: Extract single chapter content
- **WHEN** 用户请求提取特定标题名称的章节内容
- **THEN** 系统返回该章节的完整内容
- **AND** 包含完整的上级标题链(如:主标题 -> 章标题 -> 小节)
- **AND** 包含该标题下的所有下级内容
- **AND** 返回内容使用 Markdown 格式
#### Scenario: Extract multiple chapters with same name
- **WHEN** 文档中存在多个同名标题且用户请求提取该标题内容
- **THEN** 系统返回所有同名标题的章节内容
- **AND** 每个章节都包含其完整的上级标题链
- **AND** 各章节内容之间有明确的分隔
#### Scenario: Title not found
- **WHEN** 用户请求提取的标题名称在文档中不存在
- **THEN** 系统返回未找到标题的错误信息
### Requirement: Search document with regex
系统 SHALL 能够使用正则表达式在 DOCX 文档中搜索关键词,并返回所有匹配结果及其上下文。
#### Scenario: Basic keyword search
- **WHEN** 用户使用关键词搜索 DOCX 文档
- **THEN** 系统返回所有匹配的文本片段
- **AND** 每个结果包含默认的上下文(前后各 2 行)
- **AND** 各结果之间使用 `---` 分隔
- **AND** 上下文行数不包含空行
#### Scenario: Custom context lines
- **WHEN** 用户指定上下文行数进行搜索
- **THEN** 系统按指定行数返回上下文
- **AND** 支持 0 行上下文(仅返回匹配行)
- **AND** 上下文行数不包含空行
#### Scenario: Regex pattern search
- **WHEN** 用户使用正则表达式模式搜索
- **THEN** 系统支持 Python 标准正则表达式语法
- **AND** 正确处理特殊字符的转义
- **AND** 返回所有匹配模式的文本片段
#### Scenario: No matches found
- **WHEN** 搜索关键词或模式在文档中无匹配
- **THEN** 系统返回未找到匹配的提示信息
#### Scenario: Invalid regex pattern
- **WHEN** 用户提供的正则表达式格式无效
- **THEN** 系统返回正则表达式无效的错误信息
### Requirement: Multi-strategy parsing fallback
系统 SHALL 按优先级尝试多种解析策略,确保最大的兼容性。
#### Scenario: Prefer MarkItDown parser
- **WHEN** 系统检测到 `markitdown` 库已安装
- **THEN** 系统优先使用 MarkItDown 解析器解析 DOCX 文件
#### Scenario: Fallback to python-docx
- **WHEN** MarkItDown 解析失败或未安装,且 `python-docx` 库已安装
- **THEN** 系统使用 python-docx 作为备选解析器
#### Scenario: Fallback to XML parsing
- **WHEN** MarkItDown 和 python-docx 均未安装或解析失败
- **THEN** 系统使用 XML 原生解析作为最后备选方案
#### Scenario: All parsers failed
- **WHEN** 所有解析策略均失败
- **THEN** 系统返回详细的失败信息
- **AND** 列出每种解析策略的失败原因

View File

@@ -0,0 +1,115 @@
## Requirements
### Requirement: 优先使用 lyxy-reader-office 解析办公文档
大模型在遇到 .docx、.xlsx、.pptx、.pdf 文件时SHALL 优先激活并使用 lyxy-reader-office skill 来读取文件内容。
#### Scenario: 用户请求读取支持的文件格式
- **WHEN** 用户请求读取或解析 .docx、.xlsx、.pptx 或 .pdf 文件
- **THEN** 大模型 SHALL 激活 lyxy-reader-office skill
- **AND** 使用 skill 目录下的 `scripts/parser.py` 执行解析
#### Scenario: 文件扩展名自动识别
- **WHEN** 用户提供的文件路径以 .docx、.xlsx、.pptx 或 .pdf 结尾
- **THEN** 大模型 SHALL 自动识别为 lyxy-reader-office skill 的处理范围
- **AND** 无需用户显式指定使用哪个 skill
### Requirement: 必须通过 lyxy-runner-python 执行脚本
当环境中存在 lyxy-runner-python skill 时,大模型 SHALL 必须使用该 skill 来运行 parser.py 脚本。
#### Scenario: lyxy-runner-python 可用
- **WHEN** 大模型环境中存在 lyxy-runner-python skill
- **THEN** 大模型 SHALL 通过 lyxy-runner-python skill 执行 parser.py
- **AND** 利用 lyxy-runner-python 的自动依赖管理功能uv安装所需的 Python 包
#### Scenario: lyxy-runner-python 不可用
- **WHEN** 大模型环境中不存在 lyxy-runner-python skill
- **THEN** 大模型 SHALL 降级到直接使用 Python 执行 parser.py
- **AND** 提示用户当前使用直接执行模式
- **AND** 禁止自动执行 pip install 安装依赖
### Requirement: 引导阅读 README 获取详细用法
大模型在需要了解 parser.py 的详细使用方式时SHALL 阅读 `scripts/README.md` 文件。
#### Scenario: 首次使用 skill 执行解析
- **WHEN** 大模型首次使用 lyxy-reader-office skill 或不确定具体参数用法
- **THEN** 大模型 SHALL 阅读 `scripts/README.md` 获取命令行参数、依赖安装和使用示例等详细信息
#### Scenario: 遇到特殊参数需求
- **WHEN** 用户请求使用特殊功能(如 PDF OCR、章节提取、正则搜索等
- **THEN** 大模型 SHALL 参考 `scripts/README.md` 中的对应参数说明
### Requirement: 支持多格式文档解析
系统 SHALL 支持 DOCX、PPTX、XLSX、PDF 四种格式的文档解析,将文件转换为 Markdown 格式输出。
#### Scenario: 解析 DOCX 文件
- **WHEN** 用户请求解析 .docx 文件
- **THEN** 系统返回完整的 Markdown 格式文本
- **AND** 保留标题、列表、表格、粗体、斜体等格式
- **AND** 移除图片
#### Scenario: 解析 PPTX 文件
- **WHEN** 用户请求解析 .pptx 文件
- **THEN** 系统返回完整的 Markdown 格式文本
- **AND** 每张幻灯片以 `## Slide N` 为标题
- **AND** 幻灯片之间以 `---` 分隔
#### Scenario: 解析 XLSX 文件
- **WHEN** 用户请求解析 .xlsx 文件
- **THEN** 系统返回完整的 Markdown 格式文本
- **AND** 以 `## SheetName` 区分工作表
- **AND** 数据以 Markdown 表格呈现
#### Scenario: 解析 PDF 文件
- **WHEN** 用户请求解析 .pdf 文件
- **THEN** 系统返回完整的 Markdown 格式文本
- **AND** 默认使用普通文本提取模式
#### Scenario: PDF OCR 高精度模式
- **WHEN** 用户请求对 PDF 文件启用 OCR 或高精度解析
- **THEN** 系统使用 `--high-res` 参数启用 OCR 版面分析
- **AND** 通过 Docling OCR 或 unstructured hi_res 策略处理
### Requirement: 统一的查询功能
系统 SHALL 对所有支持的文件格式提供统一的查询接口,包括全文提取、元数据查询、标题提取、章节提取和正则搜索。
#### Scenario: 获取文档字数
- **WHEN** 用户请求获取文档的字数
- **THEN** 系统使用 `-c` 参数返回文档的总字符数
#### Scenario: 获取文档行数
- **WHEN** 用户请求获取文档的行数
- **THEN** 系统使用 `-l` 参数返回文档的总行数
#### Scenario: 提取所有标题
- **WHEN** 用户请求提取文档的标题结构
- **THEN** 系统使用 `-t` 参数返回所有 1-6 级标题
#### Scenario: 提取指定章节内容
- **WHEN** 用户请求提取特定标题名称的章节内容
- **THEN** 系统使用 `-tc` 参数返回该章节的完整内容
- **AND** 包含完整的上级标题链和所有下级内容
#### Scenario: 正则表达式搜索
- **WHEN** 用户请求在文档中搜索关键词或模式
- **THEN** 系统使用 `-s` 参数返回所有匹配结果及上下文
- **AND** 默认包含前后各 2 行非空行上下文
- **AND** 支持 `-n` 参数自定义上下文行数
### Requirement: 多策略解析降级
系统 SHALL 对每种文件格式按优先级尝试多种解析策略,确保最大的兼容性。
#### Scenario: 解析器按优先级降级
- **WHEN** 优先级最高的解析器不可用或解析失败
- **THEN** 系统自动尝试下一优先级的解析器
- **AND** 记录每个解析器的失败原因
#### Scenario: 所有解析器失败
- **WHEN** 某种格式的所有解析策略均失败
- **THEN** 系统返回详细的失败信息
- **AND** 列出每种解析策略的失败原因
- **AND** 以退出码 1 退出
#### Scenario: DOCX/PPTX/XLSX 无依赖运行
- **WHEN** 未安装任何第三方解析库
- **THEN** DOCX、PPTX、XLSX 文件 SHALL 仍可通过内置 XML 原生解析工作
- **AND** PDF 至少需要 pypdf 才能解析

View File

@@ -1,279 +0,0 @@
---
name: lyxy-reader-docx
description: 优先解析 docx 文档的 skill将 DOCX 文件转换为纯文本内容,不支持图片和格式提取。
compatibility: Requires Python 3.6+ and at least one of: markitdown or python-docx
---
# DOCX 文档解析 Skill
将 Microsoft Word (.docx) 文档解析为纯文本内容,支持多种解析模式和检索功能。
## Purpose
**依赖选项**: 此 skill 可以使用 lyxy-runner-python skill推荐或直接使用 Python 执行。
### 优先使用 lyxy-runner-python
如果环境中存在 lyxy-runner-python skill应优先使用它来执行 docx_parser.py 脚本:
- lyxy-runner-python 使用 uv 管理依赖,自动安装 markitdown 或 python-docx
- 环境隔离,不污染系统 Python
- 跨平台兼容Windows/macOS/Linux
### 降级到直接执行
如果环境中不存在 lyxy-runner-python skill则直接使用 Python 执行 docx_parser.py
- 需要手动安装 markitdown 或 python-docx
- 脚本内部实现了多策略解析降级MarkItDown → python-docx → XML 原生
## When to Use
任何需要读取或解析 .docx 文件内容的任务都应使用此 skill。
### 典型场景
- **文档内容提取**: 将 Word 文档转换为可读的文本内容
- **文档元数据**: 获取文档的字数、行数等信息
- **标题分析**: 提取文档的标题结构
- **章节提取**: 提取特定章节的内容
- **内容搜索**: 在文档中搜索关键词或模式
### 不适用场景
- ✗ 需要提取图片内容(仅支持纯文本)
- ✗ 需要保留复杂的格式信息(如字体、颜色、布局)
- ✗ 需要编辑或修改 .docx 文件
- ✗ 需要处理 .doc 或其他文档格式
## Capabilities
### 1. 全文转换为 Markdown
将完整的 DOCX 文档解析为 Markdown 格式文本,移除所有图片但保留文本格式。
**支持格式转换**:
- 标题1-6级
- 列表(有序和无序)
- 表格
- 粗体、斜体、下划线
- 连续空行规范化
### 2. 获取文档元信息
提供文档的基本统计信息:
- 字数统计(使用 `-c` 参数)
- 行数统计(使用 `-l` 参数)
### 3. 标题列表提取
提取文档中的所有标题1-6级按原始层级关系返回。
### 4. 指定章节内容提取
根据标题名称提取特定章节的完整内容,包括:
- 完整的上级标题链
- 该标题下的所有下级内容
支持同名标题提取,返回所有匹配章节。
### 5. 正则表达式搜索
在文档中搜索关键词或模式:
- 支持标准正则表达式
- 可自定义上下文行数(默认前后各 2 行)
- 多个匹配结果用 `---` 分隔
- 上下文行数不包含空行
## Triggers
### 中文触发词
- "读取 docx"
- "解析 docx"
- "打开 word 文档"
- "提取 word 文档内容"
- "解析 word 文件"
### 英文触发词
- "read docx"
- "parse docx"
- "extract from word document"
- "parse word file"
- "read word document"
### 文件扩展名
- `.docx` 文件
## Execution
### 优先使用 lyxy-runner-python
**推荐执行方式**(如果 lyxy-runner-python skill 可用):
```bash
# 使用 lyxy-runner-python 执行(推荐)
uv run --with markitdown skills/lyxy-reader-docx/scripts/docx_parser.py /path/to/document.docx
# 或使用 python-docx
uv run --with python-docx skills/lyxy-reader-docx/scripts/docx_parser.py /path/to/document.docx
```
**大模型应优先检查 lyxy-runner-python skill 是否存在,如果存在则使用它执行。**
### 降级到直接执行
如果 lyxy-runner-python skill 不可用,则直接使用 Python
```bash
# 直接使用 Python 执行
python3 skills/lyxy-reader-docx/scripts/docx_parser.py /path/to/document.docx
```
**提示用户**: 建议用户安装 markitdown 或 python-docx 以获得更好的解析效果:
```
建议:安装 markitdown 或 python-docx 以获得更好的解析效果
安装命令:
pip install markitdown # 推荐
pip install python-docx # 备选
```
**禁止自动安装**: 不得自动执行 pip install 命令安装依赖。仅向用户提示安装建议即可。
**降级策略说明**: 脚本内部实现了多策略解析降级MarkItDown → python-docx → XML 原生),即使未安装第三方库也能通过 XML 原生解析工作,只是功能可能受限。
## Command Usage
### 基本语法
```bash
python3 docx_parser.py [options] <file_path>
```
### 参数说明
| 参数 | 说明 |
| ----------- | ------------------- |
| `file_path` | DOCX 文件的绝对路径 |
### 选项参数
| 参数 | 长参数 | 类型 | 默认值 | 说明 |
| ---- | ----------- | ---- | ------ | -------------------------------------------------------------- |
| `-n` | `--context` | 整数 | 2 | 与 `-s` 配合使用,指定每个检索结果包含的前后行数(不包含空行) |
### 互斥参数(只能使用其中一个)
| 参数 | 长参数 | 说明 |
| ----- | ----------------- | ----------------------------------------------------- |
| `-c` | `--count` | 返回解析后的 markdown 文档的总字数 |
| `-l` | `--lines` | 返回解析后的 markdown 文档的总行数 |
| `-t` | `--titles` | 返回解析后的 markdown 文档的标题行1-6级 |
| `-tc` | `--title-content` | 指定标题名称,输出该标题及其下级内容(不包含#号 |
| `-s` | --search | 使用正则表达式搜索文档,返回所有匹配结果(用---分隔) |
## Examples
### 示例 1: 提取完整文档内容
```bash
# 提取完整文档
python3 docx_parser.py /path/to/document.docx
```
输出:完整的 Markdown 格式文档内容
### 示例 2: 获取文档字数
```bash
# 获取字数
python3 docx_parser.py -c /path/to/document.docx
```
输出:文档总字数(数字)
### 示例 3: 提取所有标题
```bash
# 提取标题
python3 docx_parser.py -t /path/to/document.docx
```
输出:所有 1-6 级标题列表
### 示例 4: 提取指定章节
```bash
# 提取 "第一章" 内容
python3 docx_parser.py -tc "第一章" /path/to/document.docx
```
输出:该章节的完整内容(包含上级标题链和所有下级内容)
### 示例 5: 搜索关键词
```bash
# 搜索关键词(默认 2 行上下文)
python3 docx_parser.py -s "关键词" /path/to/document.docx
# 自定义 5 行上下文
python3 docx_parser.py -s "关键词" -n 5 /path/to/document.docx
```
输出:所有匹配结果及其上下文,用 `---` 分隔
## 依赖安装
### 推荐方式:使用 lyxy-runner-python
如果使用 lyxy-runner-python skill依赖会自动管理无需手动安装。
### 手动安装(降级模式)
如果直接使用 Python 执行,需要手动安装至少一个解析库:
```bash
# 安装 MarkItDown推荐
pip install markitdown
# 安装 python-docx备选
pip install python-docx
```
**重要限制**:
-**禁止自动安装**: 不得自动执行 pip install 命令安装依赖
-**仅提示即可**: 向用户展示安装建议,但由用户决定是否安装
-**不阻塞执行**: 即使未安装依赖,脚本也能通过 XML 原生解析运行
### 多策略解析
脚本自动尝试以下解析方法,确保最大兼容性:
1. **MarkItDown**(微软官方库,效果最佳)
2. **python-docx**(成熟的 Python 库)
3. **XML 原生解析**(备选方案,无需任何依赖)
即使未安装任何依赖库,脚本也会尝试使用 XML 原生解析,但功能可能受限。
## Error Handling
### 常见错误
| 错误类型 | 说明 | 解决方案 |
| --------- | ---- | -------- |
| 文件不存在 | 提供的文件路径无效 | 检查文件路径是否正确 |
| 无效的 DOCX | 文件不是有效的 DOCX 格式或已损坏 | 确认文件格式正确 |
| 未找到标题 | 指定的标题名称不存在 | 使用 `-t` 参数查看所有标题 |
| 正则表达式无效 | 提供的正则表达式格式错误 | 检查正则表达式语法 |
| 解析库未安装 | 未安装 markitdown 或 python-docx | 提示用户安装以获得更好的解析效果,但禁止自动安装。脚本会自动降级到 XML 原生解析。 |
## Notes
### 为什么选择 lyxy-runner-python
| 特性 | 优势 |
| ------ | ------ |
| 环境隔离 | 不污染系统 Python |
| 自动依赖 | 自动安装 markitdown 或 python-docx |
| 快速启动 | 比 venv 快 10-100 倍 |
| 跨平台 | 自动适配 Windows/macOS/Linux |
| 零配置 | 开箱即用,无需预安装依赖 |
### 最佳实践
1. **优先使用 lyxy-runner-python**: 如果环境中存在,应优先使用 lyxy-runner-python 执行脚本
2. **大文件处理**: 对于大文档,建议使用章节提取或关键词搜索来限制处理范围
3. **依赖管理**: 使用 lyxy-runner-python 可以自动管理依赖,避免环境配置问题
4. **错误处理**: 脚本会自动尝试多种解析方法,确保最大兼容性
5. **禁止自动安装**: 在降级到直接 Python 执行时,仅向用户提示安装依赖,不得自动执行 pip install
### 限制
- ✗ 不支持图片提取(仅纯文本)
- ✗ 不支持复杂的格式保留(字体、颜色、布局等)
- ✗ 不支持文档编辑或修改
- ✗ 仅支持 .docx 格式(不支持 .doc 或其他格式)

View File

@@ -1,319 +0,0 @@
# DOCX 解析器使用说明
## 简介
`docx_parser.py` 是一个功能强大的 DOCX 文件解析工具,支持将 Microsoft Word (.docx) 文档转换为 Markdown 格式。该脚本采用多策略解析机制,按优先级尝试以下解析方法:
1. **MarkItDown**(微软官方库)
2. **python-docx**(成熟的 Python 库)
3. **XML 原生解析**(备选方案)
## 环境要求
- Python 3.6+
- pip
## 安装依赖
根据你的需求安装相应的解析库:
```bash
# 安装 MarkItDown推荐
pip install markitdown
# 安装 python-docx备选
pip install python-docx
```
> 注意:建议至少安装一种解析库。如果未安装任何库,脚本会尝试使用 XML 原生解析,但功能可能受限。
## 命令行参数
### 基本语法
```bash
python3 docx_parser.py [options] <file_path>
```
### 位置参数
| 参数 | 说明 |
| ----------- | ------------------- |
| `file_path` | DOCX 文件的绝对路径 |
### 选项参数
| 参数 | 长参数 | 类型 | 默认值 | 说明 |
| ---- | ----------- | ---- | ------ | -------------------------------------------------------------- |
| `-n` | `--context` | 整数 | 2 | 与 `-s` 配合使用,指定每个检索结果包含的前后行数(不包含空行) |
### 互斥参数
以下参数只能使用其中一个:
| 参数 | 长参数 | 说明 |
| ----- | ----------------- | ----------------------------------------------------- |
| `-c` | `--count` | 返回解析后的 markdown 文档的总字数 |
| `-l` | `--lines` | 返回解析后的 markdown 文档的总行数 |
| `-t` | `--titles` | 返回解析后的 markdown 文档的标题行1-6级 |
| `-tc` | `--title-content` | 指定标题名称,输出该标题及其下级内容(不包含#号 |
| `-s` | `--search` | 使用正则表达式搜索文档,返回所有匹配结果(用---分隔) |
## 使用示例
### 1. 输出完整 Markdown 内容
```bash
python3 docx_parser.py /path/to/document.docx
```
输出:完整的 Markdown 格式文档内容
### 2. 获取文档字数
```bash
python3 docx_parser.py -c /path/to/document.docx
```
输出:文档总字数(数字)
### 3. 获取文档行数
```bash
python3 docx_parser.py -l /path/to/document.docx
```
输出:文档总行数(数字)
### 4. 提取所有标题
```bash
python3 docx_parser.py -t /path/to/document.docx
```
输出示例:
```
# 主标题
## 第一章
### 1.1 简介
### 1.2 内容
## 第二章
```
### 5. 提取指定标题内容
```bash
python3 docx_parser.py -tc "第一章" /path/to/document.docx
```
输出:包含所有上级标题的指定章节内容
**特点:**
- 支持多个同名标题
- 自动包含完整的上级标题链
- 包含所有下级内容
示例输出:
```
# 主标题
## 第一章
这是第一章的内容
包含所有子章节...
### 1.1 简介
简介内容
### 1.2 内容
详细内容
```
### 6. 搜索关键词
#### 6.1 基本搜索
```bash
python3 docx_parser.py -s "关键词" /path/to/document.docx
```
输出:所有匹配关键词的内容片段,默认前后各 2 行上下文,用 `---` 分隔
#### 6.2 自定义上下文行数
```bash
# 前后各 5 行
python3 docx_parser.py -s "关键词" -n 5 /path/to/document.docx
# 不包含上下文
python3 docx_parser.py -s "关键词" -n 0 /path/to/document.docx
```
#### 6.3 正则表达式搜索
```bash
# 搜索包含数字的行
python3 docx_parser.py -s r"数字\d+" /path/to/document.docx
# 搜索邮箱地址
python3 docx_parser.py -s r"\b[\w.-]+@[\w.-]+\.\w+\b" /path/to/document.docx
# 搜索日期格式
python3 docx_parser.py -s r"\d{4}-\d{2}-\d{2}" /path/to/document.docx
```
输出示例:
```
这是前一行
包含匹配关键词
这是后一行
---
另一个匹配
---
第三个匹配
```
### 7. 将输出保存到文件
```bash
# 保存完整 Markdown
python3 docx_parser.py /path/to/document.docx > output.md
# 保存标题内容
python3 docx_parser.py -tc "第一章" /path/to/document.docx > chapter1.md
# 保存搜索结果
python3 docx_parser.py -s "关键词" /path/to/document.docx > search_results.md
```
## 功能特性
### 多策略解析
脚本自动尝试三种解析方法,确保最大的兼容性:
1. **MarkItDown**:微软官方库,解析效果最佳
2. **python-docx**:功能完善的第三方库
3. **XML 原生解析**:不依赖任何库的备选方案
### 智能匹配
#### 标题提取
- 支持 1-6 级标题识别
- 自动处理不同样式的标题Title、Heading 1-6
- 保留原始标题层级关系
#### 标题内容提取
- 支持同名标题提取
- 自动构建完整上级标题链
- 包含所有下级内容
- 保持文档结构完整
#### 搜索功能
- 支持正则表达式
- 智能合并相近匹配
- 上下文行数控制(不包含空行)
- 结果用 `---` 清晰分隔
### 文档处理
- 自动移除 Markdown 图片
- 规范化空白行(连续多个空行合并为一个)
- 支持表格、列表、粗体、斜体、下划线等格式
### 错误处理
- 文件存在性检查
- DOCX 格式验证
- 解析失败时自动尝试下一种方法
- 详细的错误提示信息
## 常见问题
### Q: 如何处理大文档?
A: 对于非常大的文档,建议:
1. 使用 `-tc` 参数只提取需要的章节
2. 使用 `-s` 参数搜索特定内容
3. 将输出重定向到文件进行处理
### Q: 搜索功能支持哪些正则表达式?
A: 支持所有 Python 标准正则表达式语法。需要注意特殊字符的转义:
```bash
# 错误:括号需要转义
python3 docx_parser.py -s "(关键词)" /path/to/document.docx
# 正确
python3 docx_parser.py -s "\(关键词\)" /path/to/document.docx
```
### Q: 如何获取更多上下文?
A: 使用 `-n` 参数调整上下文行数:
```bash
# 默认 2 行(推荐)
python3 docx_parser.py -s "关键词" /path/to/document.docx
# 更多上下文5 行)
python3 docx_parser.py -s "关键词" -n 5 /path/to/document.docx
# 不包含上下文
python3 docx_parser.py -s "关键词" -n 0 /path/to/document.docx
```
### Q: 多个同名标题如何处理?
A: `-tc` 参数会返回所有同名标题,每个标题都包含其完整的上级标题链:
```markdown
# 主标题
## 同名标题 1
内容1
# 主标题
## 同名标题 2
内容2
```
## 技术细节
### 标题识别规则
| 样式名称 | Markdown 标题级别 |
| --------- | ----------------- |
| Title | # |
| Heading 1 | # |
| Heading 2 | ## |
| Heading 3 | ### |
| Heading 4 | #### |
| Heading 5 | ##### |
| Heading 6 | ###### |
### 列表识别规则
| 样式名称 | Markdown 列表格式 |
| -------------------- | ----------------- |
| List Bullet / Bullet | - (无序列表) |
| List Number / Number | 1. (有序列表) |
### 文本格式支持
| 格式 | 转换结果 |
| ------ | ----------------- |
| 粗体 | `**文本**` |
| 斜体 | `*文本*` |
| 下划线 | `<u>文本</u>` |
| 表格 | Markdown 表格格式 |

View File

@@ -1,551 +0,0 @@
#!/usr/bin/env python3
"""整合的 DOCX 解析器,按优先级尝试多种解析方法:
1. MarkItDown (微软官方库)
2. python-docx (成熟的 Python 库)
3. XML 原生解析 (备选方案)
代码风格要求:
- Python 3.6+ 兼容
- 遵循 PEP 8 规范
- 所有公共 API 函数添加类型提示
- 字符串优先内联使用不提取为常量除非被使用超过3次
- 其他被多次使用的对象根据具体情况可考虑被提取为常量(如正则表达式)
- 模块级和公共 API 函数保留文档字符串
- 内部辅助函数不添加文档字符串(函数名足够描述)
- 变量命名清晰,避免单字母变量名
"""
import argparse
import os
import re
import sys
import zipfile
import xml.etree.ElementTree as ET
from typing import List, Optional, Tuple
IMAGE_PATTERN = re.compile(r"!\[[^\]]*\]\([^)]+\)")
def normalize_markdown_whitespace(content: str) -> str:
lines = content.split("\n")
result = []
empty_count = 0
for line in lines:
stripped = line.strip()
if not stripped:
empty_count += 1
if empty_count == 1:
result.append(line)
else:
empty_count = 0
result.append(line)
return "\n".join(result)
def is_valid_docx(file_path: str) -> bool:
try:
with zipfile.ZipFile(file_path, "r") as zip_file:
required_files = ["[Content_Types].xml", "_rels/.rels", "word/document.xml"]
for required in required_files:
if required not in zip_file.namelist():
return False
return True
except (zipfile.BadZipFile, zipfile.LargeZipFile):
return False
def remove_markdown_images(markdown_text: str) -> str:
return IMAGE_PATTERN.sub("", markdown_text)
def extract_titles(markdown_text: str) -> List[str]:
"""提取 markdown 文本中的所有标题行1-6级"""
title_lines = []
for line in markdown_text.split("\n"):
stripped = line.lstrip()
if stripped.startswith("#"):
level = 0
for char in stripped:
if char == "#":
level += 1
else:
break
if 1 <= level <= 6:
title_lines.append(stripped)
return title_lines
def get_heading_level(line: str) -> int:
stripped = line.lstrip()
if not stripped.startswith("#"):
return 0
level = 0
for char in stripped:
if char == "#":
level += 1
else:
break
return level if 1 <= level <= 6 else 0
def extract_title_content(markdown_text: str, title_name: str) -> Optional[str]:
"""提取所有指定标题及其下级内容(每个包含上级标题)"""
lines = markdown_text.split("\n")
match_indices = []
for i, line in enumerate(lines):
level = get_heading_level(line)
if level > 0:
stripped = line.lstrip()
title_text = stripped[level:].strip()
if title_text == title_name:
match_indices.append(i)
if not match_indices:
return None
result_lines = []
for idx in match_indices:
target_level = get_heading_level(lines[idx])
parent_titles = []
current_level = target_level
for i in range(idx - 1, -1, -1):
line_level = get_heading_level(lines[i])
if line_level > 0 and line_level < current_level:
parent_titles.append(lines[i])
current_level = line_level
if current_level == 1:
break
parent_titles.reverse()
result_lines.extend(parent_titles)
result_lines.append(lines[idx])
for i in range(idx + 1, len(lines)):
line = lines[i]
line_level = get_heading_level(line)
if line_level == 0 or line_level > target_level:
result_lines.append(line)
else:
break
return "\n".join(result_lines)
def search_markdown(
content: str, pattern: str, context_lines: int = 0
) -> Optional[str]:
"""使用正则表达式搜索 markdown 文档,返回匹配结果及其上下文"""
try:
regex = re.compile(pattern)
except re.error:
return None
lines = content.split("\n")
non_empty_indices = []
non_empty_to_original = {}
for i, line in enumerate(lines):
if line.strip():
non_empty_indices.append(i)
non_empty_to_original[i] = len(non_empty_indices) - 1
matched_non_empty_indices = []
for orig_idx in non_empty_indices:
if regex.search(lines[orig_idx]):
matched_non_empty_indices.append(non_empty_to_original[orig_idx])
if not matched_non_empty_indices:
return None
merged_ranges = []
current_start = matched_non_empty_indices[0]
current_end = matched_non_empty_indices[0]
for idx in matched_non_empty_indices[1:]:
if idx - current_end <= context_lines * 2:
current_end = idx
else:
merged_ranges.append((current_start, current_end))
current_start = idx
current_end = idx
merged_ranges.append((current_start, current_end))
results = []
for start, end in merged_ranges:
actual_start = max(0, start - context_lines)
actual_end = min(len(non_empty_indices) - 1, end + context_lines)
start_line_idx = non_empty_indices[actual_start]
end_line_idx = non_empty_indices[actual_end]
selected_indices = set(non_empty_indices[actual_start : actual_end + 1])
result_lines = [
line
for i, line in enumerate(lines)
if start_line_idx <= i <= end_line_idx
and (line.strip() or i in selected_indices)
]
results.append("\n".join(result_lines))
return "\n---\n".join(results)
def parse_with_markitdown(file_path: str) -> Optional[Tuple[str, None]]:
try:
from markitdown import MarkItDown
md = MarkItDown()
result = md.convert(file_path)
if not result.text_content.strip():
return None, "文档为空"
return result.text_content, None
except ImportError:
return None, "MarkItDown 库未安装"
except Exception as e:
return None, f"MarkItDown 解析失败: {str(e)}"
def parse_with_python_docx(file_path: str) -> Optional[Tuple[str, None]]:
try:
from docx import Document
except ImportError:
return None, "python-docx 库未安装"
try:
doc = Document(file_path)
def get_heading_level(para) -> int:
if para.style and para.style.name:
style_name = para.style.name
if "Heading 1" in style_name or "Title" in style_name:
return 1
elif "Heading 2" in style_name:
return 2
elif "Heading 3" in style_name:
return 3
elif "Heading 4" in style_name:
return 4
elif "Heading 5" in style_name:
return 5
elif "Heading 6" in style_name:
return 6
return 0
def get_list_style(para) -> Optional[str]:
if not para.style or not para.style.name:
return None
style_name = para.style.name
if "List Bullet" in style_name or "Bullet" in style_name:
return "bullet"
elif "List Number" in style_name or "Number" in style_name:
return "number"
return None
def convert_runs_to_markdown(runs) -> str:
result = []
for run in runs:
text = run.text
if not text:
continue
if run.bold:
text = f"**{text}**"
if run.italic:
text = f"*{text}*"
if run.underline:
text = f"<u>{text}</u>"
result.append(text)
return "".join(result)
def convert_table_to_markdown(table) -> str:
md_lines = []
for i, row in enumerate(table.rows):
cells = []
for cell in row.cells:
cell_text = cell.text.strip().replace("\n", " ")
cells.append(cell_text)
if cells:
md_line = "| " + " | ".join(cells) + " |"
md_lines.append(md_line)
if i == 0:
sep_line = "| " + " | ".join(["---"] * len(cells)) + " |"
md_lines.append(sep_line)
return "\n".join(md_lines)
markdown_lines = []
for para in doc.paragraphs:
text = convert_runs_to_markdown(para.runs)
if not text.strip():
continue
heading_level = get_heading_level(para)
if heading_level > 0:
markdown_lines.append(f"{'#' * heading_level} {text}")
else:
list_style = get_list_style(para)
if list_style == "bullet":
markdown_lines.append(f"- {text}")
elif list_style == "number":
markdown_lines.append(f"1. {text}")
else:
markdown_lines.append(text)
markdown_lines.append("")
for table in doc.tables:
table_md = convert_table_to_markdown(table)
markdown_lines.append(table_md)
markdown_lines.append("")
content = "\n".join(markdown_lines)
if not content.strip():
return None, "文档为空"
return content, None
except Exception as e:
return None, f"python-docx 解析失败: {str(e)}"
def parse_with_xml(file_path: str) -> Optional[Tuple[str, None]]:
word_namespace = "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
namespaces = {"w": word_namespace}
def safe_open_zip(zip_file: zipfile.ZipFile, name: str):
if name.startswith("..") or "/" not in name:
return None
return zip_file.open(name)
def get_heading_level(style_id: Optional[str], style_to_level: dict) -> int:
return style_to_level.get(style_id, 0)
def get_list_style(style_id: Optional[str], style_to_list: dict) -> Optional[str]:
return style_to_list.get(style_id, None)
def extract_text_with_formatting(para, namespaces: dict) -> str:
texts = []
for run in para.findall(".//w:r", namespaces=namespaces):
text_elem = run.find(".//w:t", namespaces=namespaces)
if text_elem is not None and text_elem.text:
text = text_elem.text
bold = run.find(".//w:b", namespaces=namespaces) is not None
italic = run.find(".//w:i", namespaces=namespaces) is not None
if bold:
text = f"**{text}**"
if italic:
text = f"*{text}*"
texts.append(text)
return "".join(texts).strip()
def convert_table_to_markdown(table_elem, namespaces: dict) -> str:
rows = table_elem.findall(".//w:tr", namespaces=namespaces)
if not rows:
return ""
md_lines = []
for i, row in enumerate(rows):
cells = row.findall(".//w:tc", namespaces=namespaces)
cell_texts = []
for cell in cells:
cell_text = extract_text_with_formatting(cell, namespaces)
cell_text = cell_text.replace("\n", " ").strip()
cell_texts.append(cell_text if cell_text else "")
if cell_texts:
md_line = "| " + " | ".join(cell_texts) + " |"
md_lines.append(md_line)
if i == 0:
sep_line = "| " + " | ".join(["---"] * len(cell_texts)) + " |"
md_lines.append(sep_line)
return "\n".join(md_lines)
try:
style_to_level = {}
style_to_list = {}
markdown_lines = []
with zipfile.ZipFile(file_path) as zip_file:
try:
styles_file = safe_open_zip(zip_file, "word/styles.xml")
if styles_file:
styles_root = ET.parse(styles_file)
for style in styles_root.findall(
".//w:style", namespaces=namespaces
):
style_id = style.get(f"{{{word_namespace}}}styleId")
style_name_elem = style.find("w:name", namespaces=namespaces)
if style_id and style_name_elem is not None:
style_name = style_name_elem.get(f"{{{word_namespace}}}val")
if style_name:
if style_name == "Title":
style_to_level[style_id] = 1
elif style_name == "heading 1":
style_to_level[style_id] = 1
elif style_name == "heading 2":
style_to_level[style_id] = 2
elif style_name == "heading 3":
style_to_level[style_id] = 3
elif style_name == "heading 4":
style_to_level[style_id] = 4
elif style_name == "heading 5":
style_to_level[style_id] = 5
elif style_name == "heading 6":
style_to_level[style_id] = 6
elif (
"List Bullet" in style_name
or "Bullet" in style_name
):
style_to_list[style_id] = "bullet"
elif (
"List Number" in style_name
or "Number" in style_name
):
style_to_list[style_id] = "number"
except Exception:
pass
document_file = safe_open_zip(zip_file, "word/document.xml")
if not document_file:
return None, "document.xml 不存在或无法访问"
root = ET.parse(document_file)
body = root.find(".//w:body", namespaces=namespaces)
if body is None:
return None, "document.xml 中未找到 w:body 元素"
for child in body.findall("./*", namespaces=namespaces):
if child.tag.endswith("}p"):
style_elem = child.find(".//w:pStyle", namespaces=namespaces)
style_id = (
style_elem.get(f"{{{word_namespace}}}val")
if style_elem is not None
else None
)
heading_level = get_heading_level(style_id, style_to_level)
list_style = get_list_style(style_id, style_to_list)
para_text = extract_text_with_formatting(child, namespaces)
if para_text:
if heading_level > 0:
markdown_lines.append(f"{'#' * heading_level} {para_text}")
elif list_style == "bullet":
markdown_lines.append(f"- {para_text}")
elif list_style == "number":
markdown_lines.append(f"1. {para_text}")
else:
markdown_lines.append(para_text)
markdown_lines.append("")
elif child.tag.endswith("}tbl"):
table_md = convert_table_to_markdown(child, namespaces)
if table_md:
markdown_lines.append(table_md)
markdown_lines.append("")
content = "\n".join(markdown_lines)
if not content.strip():
return None, "文档为空"
return content, None
except Exception as e:
return None, f"XML 解析失败: {str(e)}"
def main() -> None:
parser = argparse.ArgumentParser(description="将 DOCX 文件解析为 Markdown")
parser.add_argument("file_path", help="DOCX 文件的绝对路径")
parser.add_argument(
"-n",
"--context",
type=int,
default=2,
help="与 -s 配合使用,指定每个检索结果包含的前后行数(不包含空行)",
)
group = parser.add_mutually_exclusive_group()
group.add_argument(
"-c", "--count", action="store_true", help="返回解析后的 markdown 文档的总字数"
)
group.add_argument(
"-l", "--lines", action="store_true", help="返回解析后的 markdown 文档的总行数"
)
group.add_argument(
"-t",
"--titles",
action="store_true",
help="返回解析后的 markdown 文档的标题行1-6级",
)
group.add_argument(
"-tc",
"--title-content",
help="指定标题名称,输出该标题及其下级内容(不包含#号)",
)
group.add_argument(
"-s",
"--search",
help="使用正则表达式搜索文档,返回所有匹配结果(用---分隔)",
)
args = parser.parse_args()
if not os.path.exists(args.file_path):
print(f"错误: 文件不存在: {args.file_path}")
sys.exit(1)
if not args.file_path.lower().endswith(".docx"):
print(f"警告: 文件扩展名不是 .docx: {args.file_path}")
if not is_valid_docx(args.file_path):
print(f"错误: 文件不是有效的 DOCX 格式或已损坏: {args.file_path}")
sys.exit(1)
parsers = [
("MarkItDown", parse_with_markitdown),
("python-docx", parse_with_python_docx),
("XML 原生解析", parse_with_xml),
]
failures = []
content = None
for parser_name, parser_func in parsers:
content, error = parser_func(args.file_path)
if content is not None:
content = remove_markdown_images(content)
content = normalize_markdown_whitespace(content)
break
else:
failures.append(f"- {parser_name}: {error}")
if content is None:
print("所有解析方法均失败:")
for failure in failures:
print(failure)
sys.exit(1)
if args.count:
print(len(content.replace("\n", "")))
elif args.lines:
print(len(content.split("\n")))
elif args.titles:
titles = extract_titles(content)
for title in titles:
print(title)
elif args.title_content:
title_content = extract_title_content(content, args.title_content)
if title_content is None:
print(f"错误: 未找到标题 '{args.title_content}'")
sys.exit(1)
print(title_content, end="")
elif args.search:
search_result = search_markdown(content, args.search, args.context)
if search_result is None:
print(f"错误: 正则表达式无效或未找到匹配: '{args.search}'")
sys.exit(1)
print(search_result, end="")
else:
print(content, end="")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,212 @@
---
name: lyxy-reader-office
description: 优先解析 docx、xlsx、pptx、pdf 四种办公文档的 skill将文档转换为 Markdown 格式支持全文提取、标题提取、章节提取、正则搜索、字数统计、行数统计PDF 额外支持 OCR 高精度模式。使用时请阅读 scripts/README.md 获取详细用法。
compatibility: Requires Python 3.6+. DOCX/PPTX/XLSX 无需额外依赖XML 原生解析PDF 至少需要 pypdf。推荐通过 lyxy-runner-python skill 使用 uv 自动管理依赖。
---
# 办公文档解析 Skill
将 Microsoft Office 文档(.docx、.pptx、.xlsx和 PDF 文件解析为 Markdown 格式,支持多种查询模式。
## Purpose
**统一入口**:使用 `scripts/parser.py` 作为统一的命令行入口,自动识别文件类型并分派到对应的格式解析器。
**依赖选项**:此 skill 必须优先使用 lyxy-runner-python skill 执行,不可用时降级到直接 Python 执行。
### 必须使用 lyxy-runner-python
如果环境中存在 lyxy-runner-python skill**必须**使用它来执行 parser.py 脚本:
- lyxy-runner-python 使用 uv 管理依赖,自动安装所需的第三方库
- 环境隔离,不污染系统 Python
- 跨平台兼容Windows/macOS/Linux
### 降级到直接执行
**仅当** lyxy-runner-python skill 不存在时,才降级到直接 Python 执行:
- 需要用户手动安装依赖
- DOCX/PPTX/XLSX 无需依赖也可通过 XML 原生解析工作
- PDF 至少需要安装 pypdf
- **禁止自动执行 pip install**,仅向用户提示安装建议
## When to Use
任何需要读取或解析 .docx、.xlsx、.pptx、.pdf 文件内容的任务都应使用此 skill。
### 典型场景
- **文档内容提取**:将 Word/PPT/Excel/PDF 文档转换为可读的 Markdown 文本
- **文档元数据**:获取文档的字数、行数等信息
- **标题分析**:提取文档的标题结构
- **章节提取**:提取特定章节的内容
- **内容搜索**:在文档中搜索关键词或模式
- **PDF OCR**:对扫描版 PDF 启用 OCR 高精度解析
### 不适用场景
- 需要提取图片内容(仅支持纯文本)
- 需要保留复杂的格式信息(字体、颜色、布局)
- 需要编辑或修改文档
- 需要处理 .doc、.xls、.ppt 等旧格式
### 触发词
**中文触发词**
- "读取/解析/打开 docx/word 文档"
- "读取/解析/打开 xlsx/excel 文件"
- "读取/解析/打开 pptx/ppt 文件"
- "读取/解析/打开 pdf 文件"
**英文触发词**
- "read/parse/extract docx/word/xlsx/excel/pptx/powerpoint/pdf"
**文件扩展名**
- `.docx``.xlsx``.pptx``.pdf`
## Capabilities
### 1. 全文转换为 Markdown
将完整文档解析为 Markdown 格式,移除图片但保留文本格式(标题、列表、表格、粗体、斜体等)。
各格式的输出特点:
- **DOCX**:标准 Markdown 文档结构
- **PPTX**:每张幻灯片以 `## Slide N` 为标题,幻灯片之间以 `---` 分隔
- **XLSX**:以 `## SheetName` 区分工作表,数据以 Markdown 表格呈现
- **PDF**:纯文本流,使用 `--high-res` 可启用 OCR 版面分析识别标题
### 2. 获取文档元信息
- 字数统计(`-c` 参数)
- 行数统计(`-l` 参数)
### 3. 标题列表提取
提取文档中所有 1-6 级标题(`-t` 参数),按原始层级关系返回。
### 4. 指定章节内容提取
根据标题名称提取特定章节的完整内容(`-tc` 参数),包含上级标题链和所有下级内容。
### 5. 正则表达式搜索
在文档中搜索关键词或模式(`-s` 参数),支持自定义上下文行数(`-n` 参数,默认 2 行)。
### 6. PDF OCR 高精度模式
对 PDF 文件启用 OCR 版面分析(`--high-res` 参数),适用于扫描版 PDF 或需要识别标题层级的场景。
## Execution
### 详细用法参考
**执行前请阅读 `scripts/README.md`**,其中包含:
- 完整的命令行参数说明
- 各格式的依赖安装指南pip 和 uv 方式)
- 解析器优先级和对比
- 输出格式说明
- 错误处理和常见问题
### 基本语法
```bash
python parser.py <file_path> [options]
```
### 使用 lyxy-runner-python 执行(必须优先使用)
```bash
# DOCX - 推荐依赖
uv run --with "markitdown[docx]" skills/lyxy-reader-office/scripts/parser.py /path/to/file.docx
# PPTX - 推荐依赖
uv run --with "markitdown[pptx]" skills/lyxy-reader-office/scripts/parser.py /path/to/file.pptx
# XLSX - 推荐依赖
uv run --with "markitdown[xlsx]" skills/lyxy-reader-office/scripts/parser.py /path/to/file.xlsx
# PDF - 推荐依赖
uv run --with "markitdown[pdf]" --with pypdf skills/lyxy-reader-office/scripts/parser.py /path/to/file.pdf
# PDF OCR 高精度模式
uv run --with docling --with pypdf skills/lyxy-reader-office/scripts/parser.py /path/to/file.pdf --high-res
```
> **注意**:以上为最小推荐依赖,更多解析器依赖和完整安装命令请查阅 `scripts/README.md` 的安装部分。
### 降级到直接 Python 执行
仅当 lyxy-runner-python skill 不存在时使用:
```bash
python3 skills/lyxy-reader-office/scripts/parser.py /path/to/file.docx
```
### 互斥参数
| 参数 | 说明 |
|------|------|
| (无参数) | 输出完整 Markdown 内容 |
| `-c` | 字数统计 |
| `-l` | 行数统计 |
| `-t` | 提取所有标题 |
| `-tc <name>` | 提取指定标题的章节内容name 不含 # 号) |
| `-s <pattern>` | 正则表达式搜索 |
| `-n <num>` | 与 `-s` 配合,指定上下文行数(默认 2 |
| `--high-res` | PDF 专用,启用 OCR 版面分析 |
## Examples
### 提取完整文档内容
```bash
# DOCX
uv run --with "markitdown[docx]" skills/lyxy-reader-office/scripts/parser.py /path/to/report.docx
# PPTX
uv run --with "markitdown[pptx]" skills/lyxy-reader-office/scripts/parser.py /path/to/slides.pptx
# XLSX
uv run --with "markitdown[xlsx]" skills/lyxy-reader-office/scripts/parser.py /path/to/data.xlsx
# PDF
uv run --with "markitdown[pdf]" --with pypdf skills/lyxy-reader-office/scripts/parser.py /path/to/doc.pdf
```
### 获取文档字数
```bash
uv run --with "markitdown[docx]" skills/lyxy-reader-office/scripts/parser.py -c /path/to/report.docx
```
### 提取所有标题
```bash
uv run --with "markitdown[docx]" skills/lyxy-reader-office/scripts/parser.py -t /path/to/report.docx
```
### 提取指定章节
```bash
uv run --with "markitdown[docx]" skills/lyxy-reader-office/scripts/parser.py -tc "第一章" /path/to/report.docx
```
### 搜索关键词
```bash
uv run --with "markitdown[docx]" skills/lyxy-reader-office/scripts/parser.py -s "关键词" -n 3 /path/to/report.docx
```
### PDF OCR 高精度解析
```bash
uv run --with docling --with pypdf skills/lyxy-reader-office/scripts/parser.py /path/to/scanned.pdf --high-res
```
## Notes
### 多策略解析降级
每种文件格式配备多个解析器,按优先级依次尝试,前一个失败自动回退到下一个。详细的解析器优先级和对比请查阅 `scripts/README.md`
### 限制
- 不支持图片提取(仅纯文本)
- 不支持复杂的格式保留(字体、颜色、布局等)
- 不支持文档编辑或修改
- 仅支持 .docx、.xlsx、.pptx、.pdf 格式(不支持 .doc、.xls、.ppt 等旧格式)
- PDF 无内置 XML 原生解析,至少需要安装 pypdf
### 最佳实践
1. **必须优先使用 lyxy-runner-python**:如果环境中存在,必须使用 lyxy-runner-python 执行脚本
2. **查阅 README**:详细参数、依赖安装、解析器对比等信息请阅读 `scripts/README.md`
3. **大文件处理**:对于大文档,建议使用章节提取(`-tc`)或搜索(`-s`)来限制处理范围
4. **PDF 标题**PDF 是版面描述格式,默认不含语义化标题;需要标题层级时使用 `--high-res`
5. **禁止自动安装**:降级到直接 Python 执行时,仅向用户提示安装依赖,不得自动执行 pip install