From 9f04dac50b3b4fbbba710f029b15e78cb628ab5a Mon Sep 17 00:00:00 2001 From: lanyuanxiaoyao Date: Tue, 17 Feb 2026 22:50:06 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0lyxy-reader-office=20skill?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .claude/settings.local.json | 9 +- .../.openspec.yaml | 2 + .../design.md | 76 +++ .../proposal.md | 30 + .../specs/docx-text-extraction/spec.md | 29 + .../specs/office-document-parsing/spec.md | 115 ++++ .../tasks.md | 22 + openspec/specs/docx-text-extraction/spec.md | 132 ----- .../specs/office-document-parsing/spec.md | 115 ++++ skills/lyxy-reader-docx/SKILL.md | 279 --------- skills/lyxy-reader-docx/docx_parser.md | 319 ---------- .../lyxy-reader-docx/scripts/docx_parser.py | 551 ------------------ skills/lyxy-reader-office/SKILL.md | 212 +++++++ .../lyxy-reader-office}/scripts/README.md | 0 .../__pycache__/common.cpython-311.pyc | Bin 0 -> 18181 bytes .../__pycache__/docx_parser.cpython-311.pyc | Bin 0 -> 16060 bytes .../__pycache__/pdf_parser.cpython-311.pyc | Bin 0 -> 6948 bytes .../__pycache__/pptx_parser.cpython-311.pyc | Bin 0 -> 15272 bytes .../__pycache__/xlsx_parser.cpython-311.pyc | Bin 0 -> 16514 bytes .../lyxy-reader-office}/scripts/common.py | 0 .../scripts/docx_parser.py | 0 .../lyxy-reader-office}/scripts/parser.py | 0 .../lyxy-reader-office}/scripts/pdf_parser.py | 0 .../scripts/pptx_parser.py | 0 .../scripts/xlsx_parser.py | 0 25 files changed, 609 insertions(+), 1282 deletions(-) create mode 100644 openspec/changes/archive/2026-02-17-create-lyxy-reader-office/.openspec.yaml create mode 100644 openspec/changes/archive/2026-02-17-create-lyxy-reader-office/design.md create mode 100644 openspec/changes/archive/2026-02-17-create-lyxy-reader-office/proposal.md create mode 100644 openspec/changes/archive/2026-02-17-create-lyxy-reader-office/specs/docx-text-extraction/spec.md create mode 100644 openspec/changes/archive/2026-02-17-create-lyxy-reader-office/specs/office-document-parsing/spec.md create mode 100644 openspec/changes/archive/2026-02-17-create-lyxy-reader-office/tasks.md delete mode 100644 openspec/specs/docx-text-extraction/spec.md create mode 100644 openspec/specs/office-document-parsing/spec.md delete mode 100644 skills/lyxy-reader-docx/SKILL.md delete mode 100644 skills/lyxy-reader-docx/docx_parser.md delete mode 100644 skills/lyxy-reader-docx/scripts/docx_parser.py create mode 100644 skills/lyxy-reader-office/SKILL.md rename {temp => skills/lyxy-reader-office}/scripts/README.md (100%) create mode 100644 skills/lyxy-reader-office/scripts/__pycache__/common.cpython-311.pyc create mode 100644 skills/lyxy-reader-office/scripts/__pycache__/docx_parser.cpython-311.pyc create mode 100644 skills/lyxy-reader-office/scripts/__pycache__/pdf_parser.cpython-311.pyc create mode 100644 skills/lyxy-reader-office/scripts/__pycache__/pptx_parser.cpython-311.pyc create mode 100644 skills/lyxy-reader-office/scripts/__pycache__/xlsx_parser.cpython-311.pyc rename {temp => skills/lyxy-reader-office}/scripts/common.py (100%) rename {temp => skills/lyxy-reader-office}/scripts/docx_parser.py (100%) rename {temp => skills/lyxy-reader-office}/scripts/parser.py (100%) rename {temp => skills/lyxy-reader-office}/scripts/pdf_parser.py (100%) rename {temp => skills/lyxy-reader-office}/scripts/pptx_parser.py (100%) rename {temp => skills/lyxy-reader-office}/scripts/xlsx_parser.py (100%) diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 3dd7f3a..00e8bd7 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -6,7 +6,14 @@ "WebFetch(domain:pypi.org)", "WebFetch(domain:github.com)", "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:*)" ] } } diff --git a/openspec/changes/archive/2026-02-17-create-lyxy-reader-office/.openspec.yaml b/openspec/changes/archive/2026-02-17-create-lyxy-reader-office/.openspec.yaml new file mode 100644 index 0000000..c8d3976 --- /dev/null +++ b/openspec/changes/archive/2026-02-17-create-lyxy-reader-office/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-02-17 diff --git a/openspec/changes/archive/2026-02-17-create-lyxy-reader-office/design.md b/openspec/changes/archive/2026-02-17-create-lyxy-reader-office/design.md new file mode 100644 index 0000000..01bfc76 --- /dev/null +++ b/openspec/changes/archive/2026-02-17-create-lyxy-reader-office/design.md @@ -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 + +### 决策 1:SKILL.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 解决详细信息的需求 diff --git a/openspec/changes/archive/2026-02-17-create-lyxy-reader-office/proposal.md b/openspec/changes/archive/2026-02-17-create-lyxy-reader-office/proposal.md new file mode 100644 index 0000000..fc3d8f8 --- /dev/null +++ b/openspec/changes/archive/2026-02-17-create-lyxy-reader-office/proposal.md @@ -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 diff --git a/openspec/changes/archive/2026-02-17-create-lyxy-reader-office/specs/docx-text-extraction/spec.md b/openspec/changes/archive/2026-02-17-create-lyxy-reader-office/specs/docx-text-extraction/spec.md new file mode 100644 index 0000000..d04cc33 --- /dev/null +++ b/openspec/changes/archive/2026-02-17-create-lyxy-reader-office/specs/docx-text-extraction/spec.md @@ -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 四种格式的多策略降级 diff --git a/openspec/changes/archive/2026-02-17-create-lyxy-reader-office/specs/office-document-parsing/spec.md b/openspec/changes/archive/2026-02-17-create-lyxy-reader-office/specs/office-document-parsing/spec.md new file mode 100644 index 0000000..78a3893 --- /dev/null +++ b/openspec/changes/archive/2026-02-17-create-lyxy-reader-office/specs/office-document-parsing/spec.md @@ -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 才能解析 diff --git a/openspec/changes/archive/2026-02-17-create-lyxy-reader-office/tasks.md b/openspec/changes/archive/2026-02-17-create-lyxy-reader-office/tasks.md new file mode 100644 index 0000000..940b170 --- /dev/null +++ b/openspec/changes/archive/2026-02-17-create-lyxy-reader-office/tasks.md @@ -0,0 +1,22 @@ +## 1. 创建 SKILL.md + +- [x] 1.1 创建 `skills/lyxy-reader-office/SKILL.md` 文件,包含符合规范的 YAML frontmatter(name、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 可正常运行 diff --git a/openspec/specs/docx-text-extraction/spec.md b/openspec/specs/docx-text-extraction/spec.md deleted file mode 100644 index d065860..0000000 --- a/openspec/specs/docx-text-extraction/spec.md +++ /dev/null @@ -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** 列出每种解析策略的失败原因 diff --git a/openspec/specs/office-document-parsing/spec.md b/openspec/specs/office-document-parsing/spec.md new file mode 100644 index 0000000..3c90934 --- /dev/null +++ b/openspec/specs/office-document-parsing/spec.md @@ -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 才能解析 diff --git a/skills/lyxy-reader-docx/SKILL.md b/skills/lyxy-reader-docx/SKILL.md deleted file mode 100644 index 0bbee9e..0000000 --- a/skills/lyxy-reader-docx/SKILL.md +++ /dev/null @@ -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` | 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 或其他格式) diff --git a/skills/lyxy-reader-docx/docx_parser.md b/skills/lyxy-reader-docx/docx_parser.md deleted file mode 100644 index dc1fa20..0000000 --- a/skills/lyxy-reader-docx/docx_parser.md +++ /dev/null @@ -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` | 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. (有序列表) | - -### 文本格式支持 - -| 格式 | 转换结果 | -| ------ | ----------------- | -| 粗体 | `**文本**` | -| 斜体 | `*文本*` | -| 下划线 | `文本` | -| 表格 | Markdown 表格格式 | diff --git a/skills/lyxy-reader-docx/scripts/docx_parser.py b/skills/lyxy-reader-docx/scripts/docx_parser.py deleted file mode 100644 index 94f5a3e..0000000 --- a/skills/lyxy-reader-docx/scripts/docx_parser.py +++ /dev/null @@ -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"{text}" - 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() diff --git a/skills/lyxy-reader-office/SKILL.md b/skills/lyxy-reader-office/SKILL.md new file mode 100644 index 0000000..ee73976 --- /dev/null +++ b/skills/lyxy-reader-office/SKILL.md @@ -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 [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 不含 # 号) | +| `-s ` | 正则表达式搜索 | +| `-n ` | 与 `-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 diff --git a/temp/scripts/README.md b/skills/lyxy-reader-office/scripts/README.md similarity index 100% rename from temp/scripts/README.md rename to skills/lyxy-reader-office/scripts/README.md diff --git a/skills/lyxy-reader-office/scripts/__pycache__/common.cpython-311.pyc b/skills/lyxy-reader-office/scripts/__pycache__/common.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3bcdf2c2f10d2fec645071e6938fb13558f8413e GIT binary patch literal 18181 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 literal 0 HcmV?d00001 diff --git a/skills/lyxy-reader-office/scripts/__pycache__/docx_parser.cpython-311.pyc b/skills/lyxy-reader-office/scripts/__pycache__/docx_parser.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5bfc924c3681eab38156533e2711fcc80ccb4ade GIT binary patch literal 16060 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 literal 0 HcmV?d00001 diff --git a/skills/lyxy-reader-office/scripts/__pycache__/pptx_parser.cpython-311.pyc b/skills/lyxy-reader-office/scripts/__pycache__/pptx_parser.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..104e349c575fb15c5a8f52ebd986489c3f173977 GIT binary patch literal 15272 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 literal 0 HcmV?d00001 diff --git a/skills/lyxy-reader-office/scripts/__pycache__/xlsx_parser.cpython-311.pyc b/skills/lyxy-reader-office/scripts/__pycache__/xlsx_parser.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a4bcec09988f3467fddb5769ee803fd001448612 GIT binary patch literal 16514 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 kC>&q(j