1
0

Compare commits

..

15 Commits

Author SHA1 Message Date
2d2404e3cf 移除技能 2026-03-18 22:28:57 +08:00
3b27d2cfd4 移除未开发好的技能 2026-03-18 22:28:40 +08:00
6b4fcf2647 创建 lyxy-reader-html skill
- 新增 skill: lyxy-reader-html,用于解析 HTML 文件和 URL 网页内容
- 支持 URL 下载(pyppeteer → selenium → httpx → urllib 优先级回退)
- 支持 HTML 解析(trafilatura → domscribe → MarkItDown → html2text 优先级回退)
- 支持查询功能:全文提取、字数统计、行数统计、标题提取、章节提取、正则搜索
- 新增 spec: html-document-parsing
- 归档 change: create-lyxy-reader-html-skill
2026-03-08 02:02:03 +08:00
0bd9ec8a36 补充mac运行 2026-02-26 18:09:06 +08:00
e35d16a92e 忽略temp目录 2026-02-26 09:06:11 +08:00
d296f97a71 优化路径 2026-02-25 18:28:45 +08:00
1362972630 补充skill编写技巧 2026-02-25 17:39:55 +08:00
cd40a58f33 优化skill提示词 2026-02-25 17:36:42 +08:00
ae3b123eeb 补充MCP规范文档 2026-02-25 16:19:51 +08:00
2d327b5af8 完成一个简易的全局skill、command管理器 2026-02-25 14:33:56 +08:00
f4cb809f9d 使用扩展版的openspec 2026-02-25 10:12:23 +08:00
495a9dea3d 链接开发好的skills 2026-02-25 09:16:33 +08:00
6544c5fc4c 移除codex适配 2026-02-25 09:16:06 +08:00
3d26ea1b8d 更新openspec 2026-02-24 15:38:05 +08:00
ebf4bbecd8 补充前缀说明 2026-02-24 15:14:31 +08:00
101 changed files with 4235 additions and 5214 deletions

View File

@@ -59,7 +59,7 @@ Archive a completed change in the experimental workflow.
- If changes needed: "Sync now (recommended)", "Archive without syncing"
- If already synced: "Archive now", "Sync anyway", "Cancel"
If user chooses sync, execute `/opsx:sync` logic. Proceed to archive regardless of choice.
If user chooses sync, use Task tool (subagent_type: "general-purpose", prompt: "Use Skill tool to invoke openspec-sync-specs for change '<name>'. Delta spec analysis: <include the analyzed delta spec summary>"). Proceed to archive regardless of choice.
5. **Perform the archive**
@@ -153,5 +153,5 @@ Target archive directory already exists.
- Don't block archive on warnings - just inform and confirm
- Preserve .openspec.yaml when moving to archive (it moves with the directory)
- Show clear summary of what happened
- If sync is requested, use /opsx:sync approach (agent-driven)
- If sync is requested, use the Skill tool to invoke `openspec-sync-specs` (agent-driven)
- If delta specs exist, always run the sync assessment and show the combined summary before prompting

View File

@@ -225,7 +225,7 @@ Failed K changes:
```
## No Changes to Archive
No active changes found. Use `/opsx:new` to create a new change.
No active changes found. Create a new change to get started.
```
**Guardrails**

View File

@@ -7,7 +7,7 @@ tags: [workflow, explore, experimental, thinking]
Enter explore mode. Think deeply. Visualize freely. Follow the conversation wherever it goes.
**IMPORTANT: Explore mode is for thinking, not implementing.** You may read files, search code, and investigate the codebase, but you must NEVER write code or implement features. If the user asks you to implement something, remind them to exit explore mode first (e.g., start a change with `/opsx:new` or `/opsx:ff`). You MAY create OpenSpec artifacts (proposals, designs, specs) if the user asks—that's capturing thinking, not implementing.
**IMPORTANT: Explore mode is for thinking, not implementing.** You may read files, search code, and investigate the codebase, but you must NEVER write code or implement features. If the user asks you to implement something, remind them to exit explore mode first and create a change proposal. You MAY create OpenSpec artifacts (proposals, designs, specs) if the user asks—that's capturing thinking, not implementing.
**This is a stance, not a workflow.** There are no fixed steps, no required sequence, no mandatory outputs. You're a thinking partner helping the user explore.
@@ -100,8 +100,7 @@ If the user mentioned a specific change name, read its artifacts for context.
Think freely. When insights crystallize, you might offer:
- "This feels solid enough to start a change. Want me to create one?"
→ Can transition to `/opsx:new` or `/opsx:ff`
- "This feels solid enough to start a change. Want me to create a proposal?"
- Or keep exploring - no pressure to formalize
### When a change exists
@@ -153,7 +152,7 @@ If the user mentions a change or you detect one is relevant:
There's no required ending. Discovery might:
- **Flow into action**: "Ready to start? `/opsx:new` or `/opsx:ff`"
- **Flow into a proposal**: "Ready to start? I can create a change proposal."
- **Result in artifact updates**: "Updated design.md with these decisions"
- **Just provide clarity**: User has what they need, moves on
- **Continue later**: "We can pick this up anytime"

View File

@@ -84,7 +84,10 @@ After completing all artifacts, summarize:
- Follow the `instruction` field from `openspec instructions` for each artifact type
- The schema defines what each artifact should contain - follow it
- Read dependency artifacts for context before creating new ones
- Use the `template` as a starting point, filling in based on context
- Use `template` as the structure for your output file - fill in its sections
- **IMPORTANT**: `context` and `rules` are constraints for YOU, not content for the file
- Do NOT copy `<context>`, `<rules>`, `<project_context>` blocks into the artifact
- These guide what you write, but should never appear in the output
**Guardrails**
- Create ALL artifacts needed for implementation (as defined by schema's `apply.requires`)

View File

@@ -11,16 +11,19 @@ Guide the user through their first complete OpenSpec workflow cycle. This is a t
## Preflight
Before starting, check if OpenSpec is initialized:
Before starting, check if the OpenSpec CLI is installed:
```bash
openspec status --json 2>&1 || echo "NOT_INITIALIZED"
# Unix/macOS
openspec --version 2>&1 || echo "CLI_NOT_INSTALLED"
# Windows (PowerShell)
# if (Get-Command openspec -ErrorAction SilentlyContinue) { openspec --version } else { echo "CLI_NOT_INSTALLED" }
```
**If not initialized:**
> OpenSpec isn't set up in this project yet. Run `openspec init` first, then come back to `/opsx:onboard`.
**If CLI not installed:**
> OpenSpec CLI is not installed. Install it first, then come back to `/opsx:onboard`.
Stop here if not initialized.
Stop here if not installed.
---
@@ -63,7 +66,10 @@ Scan the codebase for small improvement opportunities. Look for:
Also check recent git activity:
```bash
# Unix/macOS
git log --oneline -10 2>/dev/null || echo "No git history"
# Windows (PowerShell)
# git log --oneline -10 2>$null; if ($LASTEXITCODE -ne 0) { echo "No git history" }
```
### Present Suggestions
@@ -258,7 +264,10 @@ For a small task like this, we might only need one spec file.
**DO:** Create the spec file:
```bash
# Unix/macOS
mkdir -p openspec/changes/<name>/specs/<capability-name>
# Windows (PowerShell)
# New-Item -ItemType Directory -Force -Path "openspec/changes/<name>/specs/<capability-name>"
```
Draft the spec content:
@@ -453,21 +462,29 @@ This same rhythm works for any size change—a small fix or a major feature.
## Command Reference
**Core workflow:**
| Command | What it does |
|---------|--------------|
| `/opsx:propose` | Create a change and generate all artifacts |
| `/opsx:explore` | Think through problems before/during work |
| `/opsx:new` | Start a new change, step through artifacts |
| `/opsx:ff` | Fast-forward: create all artifacts at once |
| `/opsx:continue` | Continue working on an existing change |
| `/opsx:apply` | Implement tasks from a change |
| `/opsx:verify` | Verify implementation matches artifacts |
| `/opsx:archive` | Archive a completed change |
**Additional commands:**
| Command | What it does |
|---------|--------------|
| `/opsx:new` | Start a new change, step through artifacts one at a time |
| `/opsx:continue` | Continue working on an existing change |
| `/opsx:ff` | Fast-forward: create all artifacts at once |
| `/opsx:verify` | Verify implementation matches artifacts |
---
## What's Next?
Try `/opsx:new` or `/opsx:ff` on something you actually want to build. You've got the rhythm now!
Try `/opsx:propose` on something you actually want to build. You've got the rhythm now!
```
---
@@ -497,17 +514,25 @@ If the user says they just want to see the commands or skip the tutorial:
```
## OpenSpec Quick Reference
**Core workflow:**
| Command | What it does |
|---------|--------------|
| `/opsx:propose <name>` | Create a change and generate all artifacts |
| `/opsx:explore` | Think through problems (no code changes) |
| `/opsx:new <name>` | Start a new change, step by step |
| `/opsx:ff <name>` | Fast-forward: all artifacts at once |
| `/opsx:continue <name>` | Continue an existing change |
| `/opsx:apply <name>` | Implement tasks |
| `/opsx:verify <name>` | Verify implementation |
| `/opsx:archive <name>` | Archive when done |
Try `/opsx:new` to start your first change, or `/opsx:ff` if you want to move fast.
**Additional commands:**
| Command | What it does |
|---------|--------------|
| `/opsx:new <name>` | Start a new change, step by step |
| `/opsx:continue <name>` | Continue an existing change |
| `/opsx:ff <name>` | Fast-forward: all artifacts at once |
| `/opsx:verify <name>` | Verify implementation |
Try `/opsx:propose` to start your first change.
```
Exit gracefully.

View File

@@ -0,0 +1,106 @@
---
name: "OPSX: Propose"
description: Propose a new change - create it and generate all artifacts in one step
category: Workflow
tags: [workflow, artifacts, experimental]
---
Propose a new change - create the change and generate all artifacts in one step.
I'll create a change with artifacts:
- proposal.md (what & why)
- design.md (how)
- tasks.md (implementation steps)
When ready to implement, run /opsx:apply
---
**Input**: The argument after `/opsx:propose` is the change name (kebab-case), OR a description of what the user wants to build.
**Steps**
1. **If no input provided, ask what they want to build**
Use the **AskUserQuestion tool** (open-ended, no preset options) to ask:
> "What change do you want to work on? Describe what you want to build or fix."
From their description, derive a kebab-case name (e.g., "add user authentication" → `add-user-auth`).
**IMPORTANT**: Do NOT proceed without understanding what the user wants to build.
2. **Create the change directory**
```bash
openspec new change "<name>"
```
This creates a scaffolded change at `openspec/changes/<name>/` with `.openspec.yaml`.
3. **Get the artifact build order**
```bash
openspec status --change "<name>" --json
```
Parse the JSON to get:
- `applyRequires`: array of artifact IDs needed before implementation (e.g., `["tasks"]`)
- `artifacts`: list of all artifacts with their status and dependencies
4. **Create artifacts in sequence until apply-ready**
Use the **TodoWrite tool** to track progress through the artifacts.
Loop through artifacts in dependency order (artifacts with no pending dependencies first):
a. **For each artifact that is `ready` (dependencies satisfied)**:
- Get instructions:
```bash
openspec instructions <artifact-id> --change "<name>" --json
```
- The instructions JSON includes:
- `context`: Project background (constraints for you - do NOT include in output)
- `rules`: Artifact-specific rules (constraints for you - do NOT include in output)
- `template`: The structure to use for your output file
- `instruction`: Schema-specific guidance for this artifact type
- `outputPath`: Where to write the artifact
- `dependencies`: Completed artifacts to read for context
- Read any completed dependency files for context
- Create the artifact file using `template` as the structure
- Apply `context` and `rules` as constraints - but do NOT copy them into the file
- Show brief progress: "Created <artifact-id>"
b. **Continue until all `applyRequires` artifacts are complete**
- After creating each artifact, re-run `openspec status --change "<name>" --json`
- Check if every artifact ID in `applyRequires` has `status: "done"` in the artifacts array
- Stop when all `applyRequires` artifacts are done
c. **If an artifact requires user input** (unclear context):
- Use **AskUserQuestion tool** to clarify
- Then continue with creation
5. **Show final status**
```bash
openspec status --change "<name>"
```
**Output**
After completing all artifacts, summarize:
- Change name and location
- List of artifacts created with brief descriptions
- What's ready: "All artifacts created! Ready for implementation."
- Prompt: "Run `/opsx:apply` to start implementing."
**Artifact Creation Guidelines**
- Follow the `instruction` field from `openspec instructions` for each artifact type
- The schema defines what each artifact should contain - follow it
- Read dependency artifacts for context before creating new ones
- Use `template` as the structure for your output file - fill in its sections
- **IMPORTANT**: `context` and `rules` are constraints for YOU, not content for the file
- Do NOT copy `<context>`, `<rules>`, `<project_context>` blocks into the artifact
- These guide what you write, but should never appear in the output
**Guardrails**
- Create ALL artifacts needed for implementation (as defined by schema's `apply.requires`)
- Always read dependency artifacts before creating a new one
- If context is critically unclear, ask the user - but prefer making reasonable decisions to keep momentum
- If a change with that name already exists, ask if user wants to continue it or create a new one
- Verify each artifact file exists after writing before proceeding to next

View File

@@ -2,18 +2,18 @@
"permissions": {
"allow": [
"Bash(uv:*)",
"Bash(openspec:*)",
"WebSearch",
"WebFetch(domain:pypi.org)",
"WebFetch(domain:github.com)",
"WebFetch(*)",
"Bash(pip index:*)",
"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:*)"
"Bash(mkdir:*)",
"Bash(ls:*)",
"Bash(git:*)",
"mcp__context7__resolve-library-id",
"mcp__context7__query-docs",
"mcp__exa__get_code_context_exa",
"mcp__exa__web_search_exa"
]
}
}

View File

@@ -6,7 +6,7 @@ compatibility: Requires openspec CLI.
metadata:
author: openspec
version: "1.0"
generatedBy: "1.1.1"
generatedBy: "1.2.0"
---
Implement tasks from an OpenSpec change.

View File

@@ -6,7 +6,7 @@ compatibility: Requires openspec CLI.
metadata:
author: openspec
version: "1.0"
generatedBy: "1.1.1"
generatedBy: "1.2.0"
---
Archive a completed change in the experimental workflow.
@@ -63,7 +63,7 @@ Archive a completed change in the experimental workflow.
- If changes needed: "Sync now (recommended)", "Archive without syncing"
- If already synced: "Archive now", "Sync anyway", "Cancel"
If user chooses sync, execute /opsx:sync logic (use the openspec-sync-specs skill). Proceed to archive regardless of choice.
If user chooses sync, use Task tool (subagent_type: "general-purpose", prompt: "Use Skill tool to invoke openspec-sync-specs for change '<name>'. Delta spec analysis: <include the analyzed delta spec summary>"). Proceed to archive regardless of choice.
5. **Perform the archive**

View File

@@ -6,7 +6,7 @@ compatibility: Requires openspec CLI.
metadata:
author: openspec
version: "1.0"
generatedBy: "1.1.1"
generatedBy: "1.2.0"
---
Archive multiple completed changes in a single operation.
@@ -229,7 +229,7 @@ Failed K changes:
```
## No Changes to Archive
No active changes found. Use `/opsx:new` to create a new change.
No active changes found. Create a new change to get started.
```
**Guardrails**

View File

@@ -6,7 +6,7 @@ compatibility: Requires openspec CLI.
metadata:
author: openspec
version: "1.0"
generatedBy: "1.1.1"
generatedBy: "1.2.0"
---
Continue working on a change by creating the next artifact.

View File

@@ -6,12 +6,12 @@ compatibility: Requires openspec CLI.
metadata:
author: openspec
version: "1.0"
generatedBy: "1.1.1"
generatedBy: "1.2.0"
---
Enter explore mode. Think deeply. Visualize freely. Follow the conversation wherever it goes.
**IMPORTANT: Explore mode is for thinking, not implementing.** You may read files, search code, and investigate the codebase, but you must NEVER write code or implement features. If the user asks you to implement something, remind them to exit explore mode first (e.g., start a change with `/opsx:new` or `/opsx:ff`). You MAY create OpenSpec artifacts (proposals, designs, specs) if the user asks—that's capturing thinking, not implementing.
**IMPORTANT: Explore mode is for thinking, not implementing.** You may read files, search code, and investigate the codebase, but you must NEVER write code or implement features. If the user asks you to implement something, remind them to exit explore mode first and create a change proposal. You MAY create OpenSpec artifacts (proposals, designs, specs) if the user asks—that's capturing thinking, not implementing.
**This is a stance, not a workflow.** There are no fixed steps, no required sequence, no mandatory outputs. You're a thinking partner helping the user explore.
@@ -95,8 +95,7 @@ This tells you:
Think freely. When insights crystallize, you might offer:
- "This feels solid enough to start a change. Want me to create one?"
→ Can transition to `/opsx:new` or `/opsx:ff`
- "This feels solid enough to start a change. Want me to create a proposal?"
- Or keep exploring - no pressure to formalize
### When a change exists
@@ -252,7 +251,7 @@ You: That changes everything.
There's no required ending. Discovery might:
- **Flow into action**: "Ready to start? /opsx:new or /opsx:ff"
- **Flow into a proposal**: "Ready to start? I can create a change proposal."
- **Result in artifact updates**: "Updated design.md with these decisions"
- **Just provide clarity**: User has what they need, moves on
- **Continue later**: "We can pick this up anytime"
@@ -269,8 +268,7 @@ When it feels like things are crystallizing, you might summarize:
**Open questions**: [if any remain]
**Next steps** (if ready):
- Create a change: /opsx:new <name>
- Fast-forward to tasks: /opsx:ff <name>
- Create a change proposal
- Keep exploring: just keep talking
```

View File

@@ -6,7 +6,7 @@ compatibility: Requires openspec CLI.
metadata:
author: openspec
version: "1.0"
generatedBy: "1.1.1"
generatedBy: "1.2.0"
---
Fast-forward through artifact creation - generate everything needed to start implementation in one go.

View File

@@ -6,7 +6,7 @@ compatibility: Requires openspec CLI.
metadata:
author: openspec
version: "1.0"
generatedBy: "1.1.1"
generatedBy: "1.2.0"
---
Start a new change using the experimental artifact-driven approach.

View File

@@ -6,7 +6,7 @@ compatibility: Requires openspec CLI.
metadata:
author: openspec
version: "1.0"
generatedBy: "1.1.1"
generatedBy: "1.2.0"
---
Guide the user through their first complete OpenSpec workflow cycle. This is a teaching experience—you'll do real work in their codebase while explaining each step.
@@ -15,16 +15,19 @@ Guide the user through their first complete OpenSpec workflow cycle. This is a t
## Preflight
Before starting, check if OpenSpec is initialized:
Before starting, check if the OpenSpec CLI is installed:
```bash
openspec status --json 2>&1 || echo "NOT_INITIALIZED"
# Unix/macOS
openspec --version 2>&1 || echo "CLI_NOT_INSTALLED"
# Windows (PowerShell)
# if (Get-Command openspec -ErrorAction SilentlyContinue) { openspec --version } else { echo "CLI_NOT_INSTALLED" }
```
**If not initialized:**
> OpenSpec isn't set up in this project yet. Run `openspec init` first, then come back to `/opsx:onboard`.
**If CLI not installed:**
> OpenSpec CLI is not installed. Install it first, then come back to `/opsx:onboard`.
Stop here if not initialized.
Stop here if not installed.
---
@@ -67,7 +70,10 @@ Scan the codebase for small improvement opportunities. Look for:
Also check recent git activity:
```bash
# Unix/macOS
git log --oneline -10 2>/dev/null || echo "No git history"
# Windows (PowerShell)
# git log --oneline -10 2>$null; if ($LASTEXITCODE -ne 0) { echo "No git history" }
```
### Present Suggestions
@@ -262,7 +268,10 @@ For a small task like this, we might only need one spec file.
**DO:** Create the spec file:
```bash
# Unix/macOS
mkdir -p openspec/changes/<name>/specs/<capability-name>
# Windows (PowerShell)
# New-Item -ItemType Directory -Force -Path "openspec/changes/<name>/specs/<capability-name>"
```
Draft the spec content:
@@ -457,21 +466,29 @@ This same rhythm works for any size change—a small fix or a major feature.
## Command Reference
**Core workflow:**
| Command | What it does |
|---------|--------------|
| `/opsx:propose` | Create a change and generate all artifacts |
| `/opsx:explore` | Think through problems before/during work |
| `/opsx:new` | Start a new change, step through artifacts |
| `/opsx:ff` | Fast-forward: create all artifacts at once |
| `/opsx:continue` | Continue working on an existing change |
| `/opsx:apply` | Implement tasks from a change |
| `/opsx:verify` | Verify implementation matches artifacts |
| `/opsx:archive` | Archive a completed change |
**Additional commands:**
| Command | What it does |
|---------|--------------|
| `/opsx:new` | Start a new change, step through artifacts one at a time |
| `/opsx:continue` | Continue working on an existing change |
| `/opsx:ff` | Fast-forward: create all artifacts at once |
| `/opsx:verify` | Verify implementation matches artifacts |
---
## What's Next?
Try `/opsx:new` or `/opsx:ff` on something you actually want to build. You've got the rhythm now!
Try `/opsx:propose` on something you actually want to build. You've got the rhythm now!
```
---
@@ -501,17 +518,25 @@ If the user says they just want to see the commands or skip the tutorial:
```
## OpenSpec Quick Reference
**Core workflow:**
| Command | What it does |
|---------|--------------|
| `/opsx:propose <name>` | Create a change and generate all artifacts |
| `/opsx:explore` | Think through problems (no code changes) |
| `/opsx:new <name>` | Start a new change, step by step |
| `/opsx:ff <name>` | Fast-forward: all artifacts at once |
| `/opsx:continue <name>` | Continue an existing change |
| `/opsx:apply <name>` | Implement tasks |
| `/opsx:verify <name>` | Verify implementation |
| `/opsx:archive <name>` | Archive when done |
Try `/opsx:new` to start your first change, or `/opsx:ff` if you want to move fast.
**Additional commands:**
| Command | What it does |
|---------|--------------|
| `/opsx:new <name>` | Start a new change, step by step |
| `/opsx:continue <name>` | Continue an existing change |
| `/opsx:ff <name>` | Fast-forward: all artifacts at once |
| `/opsx:verify <name>` | Verify implementation |
Try `/opsx:propose` to start your first change.
```
Exit gracefully.

View File

@@ -1,15 +1,24 @@
---
name: openspec-ff-change
description: Fast-forward through OpenSpec artifact creation. Use when the user wants to quickly create all artifacts needed for implementation without stepping through each one individually.
name: openspec-propose
description: Propose a new change with all artifacts generated in one step. Use when the user wants to quickly describe what they want to build and get a complete proposal with design, specs, and tasks ready for implementation.
license: MIT
compatibility: Requires openspec CLI.
metadata:
author: openspec
version: "1.0"
generatedBy: "1.1.1"
generatedBy: "1.2.0"
---
Fast-forward through artifact creation - generate everything needed to start implementation in one go.
Propose a new change - create the change and generate all artifacts in one step.
I'll create a change with artifacts:
- proposal.md (what & why)
- design.md (how)
- tasks.md (implementation steps)
When ready to implement, run /opsx:apply
---
**Input**: The user's request should include a change name (kebab-case) OR a description of what they want to build.
@@ -28,7 +37,7 @@ Fast-forward through artifact creation - generate everything needed to start imp
```bash
openspec new change "<name>"
```
This creates a scaffolded change at `openspec/changes/<name>/`.
This creates a scaffolded change at `openspec/changes/<name>/` with `.openspec.yaml`.
3. **Get the artifact build order**
```bash
@@ -59,7 +68,7 @@ Fast-forward through artifact creation - generate everything needed to start imp
- Read any completed dependency files for context
- Create the artifact file using `template` as the structure
- Apply `context` and `rules` as constraints - but do NOT copy them into the file
- Show brief progress: "Created <artifact-id>"
- Show brief progress: "Created <artifact-id>"
b. **Continue until all `applyRequires` artifacts are complete**
- After creating each artifact, re-run `openspec status --change "<name>" --json`
@@ -97,5 +106,5 @@ After completing all artifacts, summarize:
- Create ALL artifacts needed for implementation (as defined by schema's `apply.requires`)
- Always read dependency artifacts before creating a new one
- If context is critically unclear, ask the user - but prefer making reasonable decisions to keep momentum
- If a change with that name already exists, suggest continuing that change instead
- If a change with that name already exists, ask if user wants to continue it or create a new one
- Verify each artifact file exists after writing before proceeding to next

View File

@@ -6,7 +6,7 @@ compatibility: Requires openspec CLI.
metadata:
author: openspec
version: "1.0"
generatedBy: "1.1.1"
generatedBy: "1.2.0"
---
Sync delta specs from a change to main specs.

View File

@@ -6,7 +6,7 @@ compatibility: Requires openspec CLI.
metadata:
author: openspec
version: "1.0"
generatedBy: "1.1.1"
generatedBy: "1.2.0"
---
Verify that an implementation matches the change artifacts (specs, tasks, design).

View File

@@ -1,156 +0,0 @@
---
name: openspec-apply-change
description: Implement tasks from an OpenSpec change. Use when the user wants to start implementing, continue implementation, or work through tasks.
license: MIT
compatibility: Requires openspec CLI.
metadata:
author: openspec
version: "1.0"
generatedBy: "1.1.1"
---
Implement tasks from an OpenSpec change.
**Input**: Optionally specify a change name. If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
**Steps**
1. **Select the change**
If a name is provided, use it. Otherwise:
- Infer from conversation context if the user mentioned a change
- Auto-select if only one active change exists
- If ambiguous, run `openspec list --json` to get available changes and use the **AskUserQuestion tool** to let the user select
Always announce: "Using change: <name>" and how to override (e.g., `/opsx:apply <other>`).
2. **Check status to understand the schema**
```bash
openspec status --change "<name>" --json
```
Parse the JSON to understand:
- `schemaName`: The workflow being used (e.g., "spec-driven")
- Which artifact contains the tasks (typically "tasks" for spec-driven, check status for others)
3. **Get apply instructions**
```bash
openspec instructions apply --change "<name>" --json
```
This returns:
- Context file paths (varies by schema - could be proposal/specs/design/tasks or spec/tests/implementation/docs)
- Progress (total, complete, remaining)
- Task list with status
- Dynamic instruction based on current state
**Handle states:**
- If `state: "blocked"` (missing artifacts): show message, suggest using openspec-continue-change
- If `state: "all_done"`: congratulate, suggest archive
- Otherwise: proceed to implementation
4. **Read context files**
Read the files listed in `contextFiles` from the apply instructions output.
The files depend on the schema being used:
- **spec-driven**: proposal, specs, design, tasks
- Other schemas: follow the contextFiles from CLI output
5. **Show current progress**
Display:
- Schema being used
- Progress: "N/M tasks complete"
- Remaining tasks overview
- Dynamic instruction from CLI
6. **Implement tasks (loop until done or blocked)**
For each pending task:
- Show which task is being worked on
- Make the code changes required
- Keep changes minimal and focused
- Mark task complete in the tasks file: `- [ ]` → `- [x]`
- Continue to next task
**Pause if:**
- Task is unclear → ask for clarification
- Implementation reveals a design issue → suggest updating artifacts
- Error or blocker encountered → report and wait for guidance
- User interrupts
7. **On completion or pause, show status**
Display:
- Tasks completed this session
- Overall progress: "N/M tasks complete"
- If all done: suggest archive
- If paused: explain why and wait for guidance
**Output During Implementation**
```
## Implementing: <change-name> (schema: <schema-name>)
Working on task 3/7: <task description>
[...implementation happening...]
✓ Task complete
Working on task 4/7: <task description>
[...implementation happening...]
✓ Task complete
```
**Output On Completion**
```
## Implementation Complete
**Change:** <change-name>
**Schema:** <schema-name>
**Progress:** 7/7 tasks complete ✓
### Completed This Session
- [x] Task 1
- [x] Task 2
...
All tasks complete! Ready to archive this change.
```
**Output On Pause (Issue Encountered)**
```
## Implementation Paused
**Change:** <change-name>
**Schema:** <schema-name>
**Progress:** 4/7 tasks complete
### Issue Encountered
<description of the issue>
**Options:**
1. <option 1>
2. <option 2>
3. Other approach
What would you like to do?
```
**Guardrails**
- Keep going through tasks until done or blocked
- Always read context files before starting (from the apply instructions output)
- If task is ambiguous, pause and ask before implementing
- If implementation reveals issues, pause and suggest artifact updates
- Keep code changes minimal and scoped to each task
- Update task checkbox immediately after completing each task
- Pause on errors, blockers, or unclear requirements - don't guess
- Use contextFiles from CLI output, don't assume specific file names
**Fluid Workflow Integration**
This skill supports the "actions on a change" model:
- **Can be invoked anytime**: Before all artifacts are done (if tasks exist), after partial implementation, interleaved with other actions
- **Allows artifact updates**: If implementation reveals design issues, suggest updating artifacts - not phase-locked, work fluidly

View File

@@ -1,114 +0,0 @@
---
name: openspec-archive-change
description: Archive a completed change in the experimental workflow. Use when the user wants to finalize and archive a change after implementation is complete.
license: MIT
compatibility: Requires openspec CLI.
metadata:
author: openspec
version: "1.0"
generatedBy: "1.1.1"
---
Archive a completed change in the experimental workflow.
**Input**: Optionally specify a change name. If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
**Steps**
1. **If no change name provided, prompt for selection**
Run `openspec list --json` to get available changes. Use the **AskUserQuestion tool** to let the user select.
Show only active changes (not already archived).
Include the schema used for each change if available.
**IMPORTANT**: Do NOT guess or auto-select a change. Always let the user choose.
2. **Check artifact completion status**
Run `openspec status --change "<name>" --json` to check artifact completion.
Parse the JSON to understand:
- `schemaName`: The workflow being used
- `artifacts`: List of artifacts with their status (`done` or other)
**If any artifacts are not `done`:**
- Display warning listing incomplete artifacts
- Use **AskUserQuestion tool** to confirm user wants to proceed
- Proceed if user confirms
3. **Check task completion status**
Read the tasks file (typically `tasks.md`) to check for incomplete tasks.
Count tasks marked with `- [ ]` (incomplete) vs `- [x]` (complete).
**If incomplete tasks found:**
- Display warning showing count of incomplete tasks
- Use **AskUserQuestion tool** to confirm user wants to proceed
- Proceed if user confirms
**If no tasks file exists:** Proceed without task-related warning.
4. **Assess delta spec sync state**
Check for delta specs at `openspec/changes/<name>/specs/`. If none exist, proceed without sync prompt.
**If delta specs exist:**
- Compare each delta spec with its corresponding main spec at `openspec/specs/<capability>/spec.md`
- Determine what changes would be applied (adds, modifications, removals, renames)
- Show a combined summary before prompting
**Prompt options:**
- If changes needed: "Sync now (recommended)", "Archive without syncing"
- If already synced: "Archive now", "Sync anyway", "Cancel"
If user chooses sync, execute /opsx:sync logic (use the openspec-sync-specs skill). Proceed to archive regardless of choice.
5. **Perform the archive**
Create the archive directory if it doesn't exist:
```bash
mkdir -p openspec/changes/archive
```
Generate target name using current date: `YYYY-MM-DD-<change-name>`
**Check if target already exists:**
- If yes: Fail with error, suggest renaming existing archive or using different date
- If no: Move the change directory to archive
```bash
mv openspec/changes/<name> openspec/changes/archive/YYYY-MM-DD-<name>
```
6. **Display summary**
Show archive completion summary including:
- Change name
- Schema that was used
- Archive location
- Whether specs were synced (if applicable)
- Note about any warnings (incomplete artifacts/tasks)
**Output On Success**
```
## Archive Complete
**Change:** <change-name>
**Schema:** <schema-name>
**Archived to:** openspec/changes/archive/YYYY-MM-DD-<name>/
**Specs:** ✓ Synced to main specs (or "No delta specs" or "Sync skipped")
All artifacts complete. All tasks complete.
```
**Guardrails**
- Always prompt for change selection if not provided
- Use artifact graph (openspec status --json) for completion checking
- Don't block archive on warnings - just inform and confirm
- Preserve .openspec.yaml when moving to archive (it moves with the directory)
- Show clear summary of what happened
- If sync is requested, use openspec-sync-specs approach (agent-driven)
- If delta specs exist, always run the sync assessment and show the combined summary before prompting

View File

@@ -1,246 +0,0 @@
---
name: openspec-bulk-archive-change
description: Archive multiple completed changes at once. Use when archiving several parallel changes.
license: MIT
compatibility: Requires openspec CLI.
metadata:
author: openspec
version: "1.0"
generatedBy: "1.1.1"
---
Archive multiple completed changes in a single operation.
This skill allows you to batch-archive changes, handling spec conflicts intelligently by checking the codebase to determine what's actually implemented.
**Input**: None required (prompts for selection)
**Steps**
1. **Get active changes**
Run `openspec list --json` to get all active changes.
If no active changes exist, inform user and stop.
2. **Prompt for change selection**
Use **AskUserQuestion tool** with multi-select to let user choose changes:
- Show each change with its schema
- Include an option for "All changes"
- Allow any number of selections (1+ works, 2+ is the typical use case)
**IMPORTANT**: Do NOT auto-select. Always let the user choose.
3. **Batch validation - gather status for all selected changes**
For each selected change, collect:
a. **Artifact status** - Run `openspec status --change "<name>" --json`
- Parse `schemaName` and `artifacts` list
- Note which artifacts are `done` vs other states
b. **Task completion** - Read `openspec/changes/<name>/tasks.md`
- Count `- [ ]` (incomplete) vs `- [x]` (complete)
- If no tasks file exists, note as "No tasks"
c. **Delta specs** - Check `openspec/changes/<name>/specs/` directory
- List which capability specs exist
- For each, extract requirement names (lines matching `### Requirement: <name>`)
4. **Detect spec conflicts**
Build a map of `capability -> [changes that touch it]`:
```
auth -> [change-a, change-b] <- CONFLICT (2+ changes)
api -> [change-c] <- OK (only 1 change)
```
A conflict exists when 2+ selected changes have delta specs for the same capability.
5. **Resolve conflicts agentically**
**For each conflict**, investigate the codebase:
a. **Read the delta specs** from each conflicting change to understand what each claims to add/modify
b. **Search the codebase** for implementation evidence:
- Look for code implementing requirements from each delta spec
- Check for related files, functions, or tests
c. **Determine resolution**:
- If only one change is actually implemented -> sync that one's specs
- If both implemented -> apply in chronological order (older first, newer overwrites)
- If neither implemented -> skip spec sync, warn user
d. **Record resolution** for each conflict:
- Which change's specs to apply
- In what order (if both)
- Rationale (what was found in codebase)
6. **Show consolidated status table**
Display a table summarizing all changes:
```
| Change | Artifacts | Tasks | Specs | Conflicts | Status |
|---------------------|-----------|-------|---------|-----------|--------|
| schema-management | Done | 5/5 | 2 delta | None | Ready |
| project-config | Done | 3/3 | 1 delta | None | Ready |
| add-oauth | Done | 4/4 | 1 delta | auth (!) | Ready* |
| add-verify-skill | 1 left | 2/5 | None | None | Warn |
```
For conflicts, show the resolution:
```
* Conflict resolution:
- auth spec: Will apply add-oauth then add-jwt (both implemented, chronological order)
```
For incomplete changes, show warnings:
```
Warnings:
- add-verify-skill: 1 incomplete artifact, 3 incomplete tasks
```
7. **Confirm batch operation**
Use **AskUserQuestion tool** with a single confirmation:
- "Archive N changes?" with options based on status
- Options might include:
- "Archive all N changes"
- "Archive only N ready changes (skip incomplete)"
- "Cancel"
If there are incomplete changes, make clear they'll be archived with warnings.
8. **Execute archive for each confirmed change**
Process changes in the determined order (respecting conflict resolution):
a. **Sync specs** if delta specs exist:
- Use the openspec-sync-specs approach (agent-driven intelligent merge)
- For conflicts, apply in resolved order
- Track if sync was done
b. **Perform the archive**:
```bash
mkdir -p openspec/changes/archive
mv openspec/changes/<name> openspec/changes/archive/YYYY-MM-DD-<name>
```
c. **Track outcome** for each change:
- Success: archived successfully
- Failed: error during archive (record error)
- Skipped: user chose not to archive (if applicable)
9. **Display summary**
Show final results:
```
## Bulk Archive Complete
Archived 3 changes:
- schema-management-cli -> archive/2026-01-19-schema-management-cli/
- project-config -> archive/2026-01-19-project-config/
- add-oauth -> archive/2026-01-19-add-oauth/
Skipped 1 change:
- add-verify-skill (user chose not to archive incomplete)
Spec sync summary:
- 4 delta specs synced to main specs
- 1 conflict resolved (auth: applied both in chronological order)
```
If any failures:
```
Failed 1 change:
- some-change: Archive directory already exists
```
**Conflict Resolution Examples**
Example 1: Only one implemented
```
Conflict: specs/auth/spec.md touched by [add-oauth, add-jwt]
Checking add-oauth:
- Delta adds "OAuth Provider Integration" requirement
- Searching codebase... found src/auth/oauth.ts implementing OAuth flow
Checking add-jwt:
- Delta adds "JWT Token Handling" requirement
- Searching codebase... no JWT implementation found
Resolution: Only add-oauth is implemented. Will sync add-oauth specs only.
```
Example 2: Both implemented
```
Conflict: specs/api/spec.md touched by [add-rest-api, add-graphql]
Checking add-rest-api (created 2026-01-10):
- Delta adds "REST Endpoints" requirement
- Searching codebase... found src/api/rest.ts
Checking add-graphql (created 2026-01-15):
- Delta adds "GraphQL Schema" requirement
- Searching codebase... found src/api/graphql.ts
Resolution: Both implemented. Will apply add-rest-api specs first,
then add-graphql specs (chronological order, newer takes precedence).
```
**Output On Success**
```
## Bulk Archive Complete
Archived N changes:
- <change-1> -> archive/YYYY-MM-DD-<change-1>/
- <change-2> -> archive/YYYY-MM-DD-<change-2>/
Spec sync summary:
- N delta specs synced to main specs
- No conflicts (or: M conflicts resolved)
```
**Output On Partial Success**
```
## Bulk Archive Complete (partial)
Archived N changes:
- <change-1> -> archive/YYYY-MM-DD-<change-1>/
Skipped M changes:
- <change-2> (user chose not to archive incomplete)
Failed K changes:
- <change-3>: Archive directory already exists
```
**Output When No Changes**
```
## No Changes to Archive
No active changes found. Use `/opsx:new` to create a new change.
```
**Guardrails**
- Allow any number of changes (1+ is fine, 2+ is the typical use case)
- Always prompt for selection, never auto-select
- Detect spec conflicts early and resolve by checking codebase
- When both changes are implemented, apply specs in chronological order
- Skip spec sync only when implementation is missing (warn user)
- Show clear per-change status before confirming
- Use single confirmation for entire batch
- Track and report all outcomes (success/skip/fail)
- Preserve .openspec.yaml when moving to archive
- Archive directory target uses current date: YYYY-MM-DD-<name>
- If archive target exists, fail that change but continue with others

View File

@@ -1,118 +0,0 @@
---
name: openspec-continue-change
description: Continue working on an OpenSpec change by creating the next artifact. Use when the user wants to progress their change, create the next artifact, or continue their workflow.
license: MIT
compatibility: Requires openspec CLI.
metadata:
author: openspec
version: "1.0"
generatedBy: "1.1.1"
---
Continue working on a change by creating the next artifact.
**Input**: Optionally specify a change name. If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
**Steps**
1. **If no change name provided, prompt for selection**
Run `openspec list --json` to get available changes sorted by most recently modified. Then use the **AskUserQuestion tool** to let the user select which change to work on.
Present the top 3-4 most recently modified changes as options, showing:
- Change name
- Schema (from `schema` field if present, otherwise "spec-driven")
- Status (e.g., "0/5 tasks", "complete", "no tasks")
- How recently it was modified (from `lastModified` field)
Mark the most recently modified change as "(Recommended)" since it's likely what the user wants to continue.
**IMPORTANT**: Do NOT guess or auto-select a change. Always let the user choose.
2. **Check current status**
```bash
openspec status --change "<name>" --json
```
Parse the JSON to understand current state. The response includes:
- `schemaName`: The workflow schema being used (e.g., "spec-driven")
- `artifacts`: Array of artifacts with their status ("done", "ready", "blocked")
- `isComplete`: Boolean indicating if all artifacts are complete
3. **Act based on status**:
---
**If all artifacts are complete (`isComplete: true`)**:
- Congratulate the user
- Show final status including the schema used
- Suggest: "All artifacts created! You can now implement this change or archive it."
- STOP
---
**If artifacts are ready to create** (status shows artifacts with `status: "ready"`):
- Pick the FIRST artifact with `status: "ready"` from the status output
- Get its instructions:
```bash
openspec instructions <artifact-id> --change "<name>" --json
```
- Parse the JSON. The key fields are:
- `context`: Project background (constraints for you - do NOT include in output)
- `rules`: Artifact-specific rules (constraints for you - do NOT include in output)
- `template`: The structure to use for your output file
- `instruction`: Schema-specific guidance
- `outputPath`: Where to write the artifact
- `dependencies`: Completed artifacts to read for context
- **Create the artifact file**:
- Read any completed dependency files for context
- Use `template` as the structure - fill in its sections
- Apply `context` and `rules` as constraints when writing - but do NOT copy them into the file
- Write to the output path specified in instructions
- Show what was created and what's now unlocked
- STOP after creating ONE artifact
---
**If no artifacts are ready (all blocked)**:
- This shouldn't happen with a valid schema
- Show status and suggest checking for issues
4. **After creating an artifact, show progress**
```bash
openspec status --change "<name>"
```
**Output**
After each invocation, show:
- Which artifact was created
- Schema workflow being used
- Current progress (N/M complete)
- What artifacts are now unlocked
- Prompt: "Want to continue? Just ask me to continue or tell me what to do next."
**Artifact Creation Guidelines**
The artifact types and their purpose depend on the schema. Use the `instruction` field from the instructions output to understand what to create.
Common artifact patterns:
**spec-driven schema** (proposal → specs → design → tasks):
- **proposal.md**: Ask user about the change if not clear. Fill in Why, What Changes, Capabilities, Impact.
- The Capabilities section is critical - each capability listed will need a spec file.
- **specs/<capability>/spec.md**: Create one spec per capability listed in the proposal's Capabilities section (use the capability name, not the change name).
- **design.md**: Document technical decisions, architecture, and implementation approach.
- **tasks.md**: Break down implementation into checkboxed tasks.
For other schemas, follow the `instruction` field from the CLI output.
**Guardrails**
- Create ONE artifact per invocation
- Always read dependency artifacts before creating a new one
- Never skip artifacts or create out of order
- If context is unclear, ask the user before creating
- Verify the artifact file exists after writing before marking progress
- Use the schema's artifact sequence, don't assume specific artifact names
- **IMPORTANT**: `context` and `rules` are constraints for YOU, not content for the file
- Do NOT copy `<context>`, `<rules>`, `<project_context>` blocks into the artifact
- These guide what you write, but should never appear in the output

View File

@@ -1,290 +0,0 @@
---
name: openspec-explore
description: Enter explore mode - a thinking partner for exploring ideas, investigating problems, and clarifying requirements. Use when the user wants to think through something before or during a change.
license: MIT
compatibility: Requires openspec CLI.
metadata:
author: openspec
version: "1.0"
generatedBy: "1.1.1"
---
Enter explore mode. Think deeply. Visualize freely. Follow the conversation wherever it goes.
**IMPORTANT: Explore mode is for thinking, not implementing.** You may read files, search code, and investigate the codebase, but you must NEVER write code or implement features. If the user asks you to implement something, remind them to exit explore mode first (e.g., start a change with `/opsx:new` or `/opsx:ff`). You MAY create OpenSpec artifacts (proposals, designs, specs) if the user asks—that's capturing thinking, not implementing.
**This is a stance, not a workflow.** There are no fixed steps, no required sequence, no mandatory outputs. You're a thinking partner helping the user explore.
---
## The Stance
- **Curious, not prescriptive** - Ask questions that emerge naturally, don't follow a script
- **Open threads, not interrogations** - Surface multiple interesting directions and let the user follow what resonates. Don't funnel them through a single path of questions.
- **Visual** - Use ASCII diagrams liberally when they'd help clarify thinking
- **Adaptive** - Follow interesting threads, pivot when new information emerges
- **Patient** - Don't rush to conclusions, let the shape of the problem emerge
- **Grounded** - Explore the actual codebase when relevant, don't just theorize
---
## What You Might Do
Depending on what the user brings, you might:
**Explore the problem space**
- Ask clarifying questions that emerge from what they said
- Challenge assumptions
- Reframe the problem
- Find analogies
**Investigate the codebase**
- Map existing architecture relevant to the discussion
- Find integration points
- Identify patterns already in use
- Surface hidden complexity
**Compare options**
- Brainstorm multiple approaches
- Build comparison tables
- Sketch tradeoffs
- Recommend a path (if asked)
**Visualize**
```
┌─────────────────────────────────────────┐
│ Use ASCII diagrams liberally │
├─────────────────────────────────────────┤
│ │
│ ┌────────┐ ┌────────┐ │
│ │ State │────────▶│ State │ │
│ │ A │ │ B │ │
│ └────────┘ └────────┘ │
│ │
│ System diagrams, state machines, │
│ data flows, architecture sketches, │
│ dependency graphs, comparison tables │
│ │
└─────────────────────────────────────────┘
```
**Surface risks and unknowns**
- Identify what could go wrong
- Find gaps in understanding
- Suggest spikes or investigations
---
## OpenSpec Awareness
You have full context of the OpenSpec system. Use it naturally, don't force it.
### Check for context
At the start, quickly check what exists:
```bash
openspec list --json
```
This tells you:
- If there are active changes
- Their names, schemas, and status
- What the user might be working on
### When no change exists
Think freely. When insights crystallize, you might offer:
- "This feels solid enough to start a change. Want me to create one?"
→ Can transition to `/opsx:new` or `/opsx:ff`
- Or keep exploring - no pressure to formalize
### When a change exists
If the user mentions a change or you detect one is relevant:
1. **Read existing artifacts for context**
- `openspec/changes/<name>/proposal.md`
- `openspec/changes/<name>/design.md`
- `openspec/changes/<name>/tasks.md`
- etc.
2. **Reference them naturally in conversation**
- "Your design mentions using Redis, but we just realized SQLite fits better..."
- "The proposal scopes this to premium users, but we're now thinking everyone..."
3. **Offer to capture when decisions are made**
| Insight Type | Where to Capture |
|--------------|------------------|
| New requirement discovered | `specs/<capability>/spec.md` |
| Requirement changed | `specs/<capability>/spec.md` |
| Design decision made | `design.md` |
| Scope changed | `proposal.md` |
| New work identified | `tasks.md` |
| Assumption invalidated | Relevant artifact |
Example offers:
- "That's a design decision. Capture it in design.md?"
- "This is a new requirement. Add it to specs?"
- "This changes scope. Update the proposal?"
4. **The user decides** - Offer and move on. Don't pressure. Don't auto-capture.
---
## What You Don't Have To Do
- Follow a script
- Ask the same questions every time
- Produce a specific artifact
- Reach a conclusion
- Stay on topic if a tangent is valuable
- Be brief (this is thinking time)
---
## Handling Different Entry Points
**User brings a vague idea:**
```
User: I'm thinking about adding real-time collaboration
You: Real-time collab is a big space. Let me think about this...
COLLABORATION SPECTRUM
════════════════════════════════════════════
Awareness Coordination Sync
│ │ │
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐
│Presence│ │Cursors │ │ CRDT │
│ "3 │ │ Multi │ │Conflict│
│online" │ │ select │ │ free │
└────────┘ └────────┘ └────────┘
│ │ │
trivial moderate complex
Where's your head at?
```
**User brings a specific problem:**
```
User: The auth system is a mess
You: [reads codebase]
┌─────────────────────────────────────────────┐
│ CURRENT AUTH FLOW │
└─────────────────────────────────────────────┘
┌─────────────┼─────────────┐
▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐
│ Google │ │ GitHub │ │ Email │
│ OAuth │ │ OAuth │ │ Magic │
└────┬────┘ └────┬────┘ └────┬────┘
│ │ │
└─────────────┼─────────────┘
┌───────────┐
│ Session │
└─────┬─────┘
┌───────────┐
│ Perms │
└───────────┘
I see three tangles. Which one's burning?
```
**User is stuck mid-implementation:**
```
User: /opsx:explore add-auth-system
The OAuth integration is more complex than expected
You: [reads change artifacts]
You're on task 4: "Implement OAuth flow"
Let me trace what's involved...
[draws diagram, explores options, suggests paths]
Want to update the design to reflect this?
Or add a spike task to investigate?
```
**User wants to compare options:**
```
User: Should we use Postgres or SQLite?
You: Generic answer is boring. What's the context?
User: A CLI tool that tracks local dev environments
You: That changes everything.
┌─────────────────────────────────────────────────┐
│ CLI TOOL DATA STORAGE │
└─────────────────────────────────────────────────┘
Key constraints:
• No daemon running
• Must work offline
• Single user
SQLite Postgres
Deployment embedded ✓ needs server ✗
Offline yes ✓ no ✗
Single file yes ✓ no ✗
SQLite. Not even close.
Unless... is there a sync component?
```
---
## Ending Discovery
There's no required ending. Discovery might:
- **Flow into action**: "Ready to start? /opsx:new or /opsx:ff"
- **Result in artifact updates**: "Updated design.md with these decisions"
- **Just provide clarity**: User has what they need, moves on
- **Continue later**: "We can pick this up anytime"
When it feels like things are crystallizing, you might summarize:
```
## What We Figured Out
**The problem**: [crystallized understanding]
**The approach**: [if one emerged]
**Open questions**: [if any remain]
**Next steps** (if ready):
- Create a change: /opsx:new <name>
- Fast-forward to tasks: /opsx:ff <name>
- Keep exploring: just keep talking
```
But this summary is optional. Sometimes the thinking IS the value.
---
## Guardrails
- **Don't implement** - Never write code or implement features. Creating OpenSpec artifacts is fine, writing application code is not.
- **Don't fake understanding** - If something is unclear, dig deeper
- **Don't rush** - Discovery is thinking time, not task time
- **Don't force structure** - Let patterns emerge naturally
- **Don't auto-capture** - Offer to save insights, don't just do it
- **Do visualize** - A good diagram is worth many paragraphs
- **Do explore the codebase** - Ground discussions in reality
- **Do question assumptions** - Including the user's and your own

View File

@@ -1,74 +0,0 @@
---
name: openspec-new-change
description: Start a new OpenSpec change using the experimental artifact workflow. Use when the user wants to create a new feature, fix, or modification with a structured step-by-step approach.
license: MIT
compatibility: Requires openspec CLI.
metadata:
author: openspec
version: "1.0"
generatedBy: "1.1.1"
---
Start a new change using the experimental artifact-driven approach.
**Input**: The user's request should include a change name (kebab-case) OR a description of what they want to build.
**Steps**
1. **If no clear input provided, ask what they want to build**
Use the **AskUserQuestion tool** (open-ended, no preset options) to ask:
> "What change do you want to work on? Describe what you want to build or fix."
From their description, derive a kebab-case name (e.g., "add user authentication" → `add-user-auth`).
**IMPORTANT**: Do NOT proceed without understanding what the user wants to build.
2. **Determine the workflow schema**
Use the default schema (omit `--schema`) unless the user explicitly requests a different workflow.
**Use a different schema only if the user mentions:**
- A specific schema name → use `--schema <name>`
- "show workflows" or "what workflows" → run `openspec schemas --json` and let them choose
**Otherwise**: Omit `--schema` to use the default.
3. **Create the change directory**
```bash
openspec new change "<name>"
```
Add `--schema <name>` only if the user requested a specific workflow.
This creates a scaffolded change at `openspec/changes/<name>/` with the selected schema.
4. **Show the artifact status**
```bash
openspec status --change "<name>"
```
This shows which artifacts need to be created and which are ready (dependencies satisfied).
5. **Get instructions for the first artifact**
The first artifact depends on the schema (e.g., `proposal` for spec-driven).
Check the status output to find the first artifact with status "ready".
```bash
openspec instructions <first-artifact-id> --change "<name>"
```
This outputs the template and context for creating the first artifact.
6. **STOP and wait for user direction**
**Output**
After completing the steps, summarize:
- Change name and location
- Schema/workflow being used and its artifact sequence
- Current status (0/N artifacts complete)
- The template for the first artifact
- Prompt: "Ready to create the first artifact? Just describe what this change is about and I'll draft it, or ask me to continue."
**Guardrails**
- Do NOT create any artifacts yet - just show the instructions
- Do NOT advance beyond showing the first artifact template
- If the name is invalid (not kebab-case), ask for a valid name
- If a change with that name already exists, suggest continuing that change instead
- Pass --schema if using a non-default workflow

View File

@@ -1,529 +0,0 @@
---
name: openspec-onboard
description: Guided onboarding for OpenSpec - walk through a complete workflow cycle with narration and real codebase work.
license: MIT
compatibility: Requires openspec CLI.
metadata:
author: openspec
version: "1.0"
generatedBy: "1.1.1"
---
Guide the user through their first complete OpenSpec workflow cycle. This is a teaching experience—you'll do real work in their codebase while explaining each step.
---
## Preflight
Before starting, check if OpenSpec is initialized:
```bash
openspec status --json 2>&1 || echo "NOT_INITIALIZED"
```
**If not initialized:**
> OpenSpec isn't set up in this project yet. Run `openspec init` first, then come back to `/opsx:onboard`.
Stop here if not initialized.
---
## Phase 1: Welcome
Display:
```
## Welcome to OpenSpec!
I'll walk you through a complete change cycle—from idea to implementation—using a real task in your codebase. Along the way, you'll learn the workflow by doing it.
**What we'll do:**
1. Pick a small, real task in your codebase
2. Explore the problem briefly
3. Create a change (the container for our work)
4. Build the artifacts: proposal → specs → design → tasks
5. Implement the tasks
6. Archive the completed change
**Time:** ~15-20 minutes
Let's start by finding something to work on.
```
---
## Phase 2: Task Selection
### Codebase Analysis
Scan the codebase for small improvement opportunities. Look for:
1. **TODO/FIXME comments** - Search for `TODO`, `FIXME`, `HACK`, `XXX` in code files
2. **Missing error handling** - `catch` blocks that swallow errors, risky operations without try-catch
3. **Functions without tests** - Cross-reference `src/` with test directories
4. **Type issues** - `any` types in TypeScript files (`: any`, `as any`)
5. **Debug artifacts** - `console.log`, `console.debug`, `debugger` statements in non-debug code
6. **Missing validation** - User input handlers without validation
Also check recent git activity:
```bash
git log --oneline -10 2>/dev/null || echo "No git history"
```
### Present Suggestions
From your analysis, present 3-4 specific suggestions:
```
## Task Suggestions
Based on scanning your codebase, here are some good starter tasks:
**1. [Most promising task]**
Location: `src/path/to/file.ts:42`
Scope: ~1-2 files, ~20-30 lines
Why it's good: [brief reason]
**2. [Second task]**
Location: `src/another/file.ts`
Scope: ~1 file, ~15 lines
Why it's good: [brief reason]
**3. [Third task]**
Location: [location]
Scope: [estimate]
Why it's good: [brief reason]
**4. Something else?**
Tell me what you'd like to work on.
Which task interests you? (Pick a number or describe your own)
```
**If nothing found:** Fall back to asking what the user wants to build:
> I didn't find obvious quick wins in your codebase. What's something small you've been meaning to add or fix?
### Scope Guardrail
If the user picks or describes something too large (major feature, multi-day work):
```
That's a valuable task, but it's probably larger than ideal for your first OpenSpec run-through.
For learning the workflow, smaller is better—it lets you see the full cycle without getting stuck in implementation details.
**Options:**
1. **Slice it smaller** - What's the smallest useful piece of [their task]? Maybe just [specific slice]?
2. **Pick something else** - One of the other suggestions, or a different small task?
3. **Do it anyway** - If you really want to tackle this, we can. Just know it'll take longer.
What would you prefer?
```
Let the user override if they insist—this is a soft guardrail.
---
## Phase 3: Explore Demo
Once a task is selected, briefly demonstrate explore mode:
```
Before we create a change, let me quickly show you **explore mode**—it's how you think through problems before committing to a direction.
```
Spend 1-2 minutes investigating the relevant code:
- Read the file(s) involved
- Draw a quick ASCII diagram if it helps
- Note any considerations
```
## Quick Exploration
[Your brief analysis—what you found, any considerations]
┌─────────────────────────────────────────┐
│ [Optional: ASCII diagram if helpful] │
└─────────────────────────────────────────┘
Explore mode (`/opsx:explore`) is for this kind of thinking—investigating before implementing. You can use it anytime you need to think through a problem.
Now let's create a change to hold our work.
```
**PAUSE** - Wait for user acknowledgment before proceeding.
---
## Phase 4: Create the Change
**EXPLAIN:**
```
## Creating a Change
A "change" in OpenSpec is a container for all the thinking and planning around a piece of work. It lives in `openspec/changes/<name>/` and holds your artifacts—proposal, specs, design, tasks.
Let me create one for our task.
```
**DO:** Create the change with a derived kebab-case name:
```bash
openspec new change "<derived-name>"
```
**SHOW:**
```
Created: `openspec/changes/<name>/`
The folder structure:
```
openspec/changes/<name>/
├── proposal.md ← Why we're doing this (empty, we'll fill it)
├── design.md ← How we'll build it (empty)
├── specs/ ← Detailed requirements (empty)
└── tasks.md ← Implementation checklist (empty)
```
Now let's fill in the first artifact—the proposal.
```
---
## Phase 5: Proposal
**EXPLAIN:**
```
## The Proposal
The proposal captures **why** we're making this change and **what** it involves at a high level. It's the "elevator pitch" for the work.
I'll draft one based on our task.
```
**DO:** Draft the proposal content (don't save yet):
```
Here's a draft proposal:
---
## Why
[1-2 sentences explaining the problem/opportunity]
## What Changes
[Bullet points of what will be different]
## Capabilities
### New Capabilities
- `<capability-name>`: [brief description]
### Modified Capabilities
<!-- If modifying existing behavior -->
## Impact
- `src/path/to/file.ts`: [what changes]
- [other files if applicable]
---
Does this capture the intent? I can adjust before we save it.
```
**PAUSE** - Wait for user approval/feedback.
After approval, save the proposal:
```bash
openspec instructions proposal --change "<name>" --json
```
Then write the content to `openspec/changes/<name>/proposal.md`.
```
Proposal saved. This is your "why" document—you can always come back and refine it as understanding evolves.
Next up: specs.
```
---
## Phase 6: Specs
**EXPLAIN:**
```
## Specs
Specs define **what** we're building in precise, testable terms. They use a requirement/scenario format that makes expected behavior crystal clear.
For a small task like this, we might only need one spec file.
```
**DO:** Create the spec file:
```bash
mkdir -p openspec/changes/<name>/specs/<capability-name>
```
Draft the spec content:
```
Here's the spec:
---
## ADDED Requirements
### Requirement: <Name>
<Description of what the system should do>
#### Scenario: <Scenario name>
- **WHEN** <trigger condition>
- **THEN** <expected outcome>
- **AND** <additional outcome if needed>
---
This format—WHEN/THEN/AND—makes requirements testable. You can literally read them as test cases.
```
Save to `openspec/changes/<name>/specs/<capability>/spec.md`.
---
## Phase 7: Design
**EXPLAIN:**
```
## Design
The design captures **how** we'll build it—technical decisions, tradeoffs, approach.
For small changes, this might be brief. That's fine—not every change needs deep design discussion.
```
**DO:** Draft design.md:
```
Here's the design:
---
## Context
[Brief context about the current state]
## Goals / Non-Goals
**Goals:**
- [What we're trying to achieve]
**Non-Goals:**
- [What's explicitly out of scope]
## Decisions
### Decision 1: [Key decision]
[Explanation of approach and rationale]
---
For a small task, this captures the key decisions without over-engineering.
```
Save to `openspec/changes/<name>/design.md`.
---
## Phase 8: Tasks
**EXPLAIN:**
```
## Tasks
Finally, we break the work into implementation tasks—checkboxes that drive the apply phase.
These should be small, clear, and in logical order.
```
**DO:** Generate tasks based on specs and design:
```
Here are the implementation tasks:
---
## 1. [Category or file]
- [ ] 1.1 [Specific task]
- [ ] 1.2 [Specific task]
## 2. Verify
- [ ] 2.1 [Verification step]
---
Each checkbox becomes a unit of work in the apply phase. Ready to implement?
```
**PAUSE** - Wait for user to confirm they're ready to implement.
Save to `openspec/changes/<name>/tasks.md`.
---
## Phase 9: Apply (Implementation)
**EXPLAIN:**
```
## Implementation
Now we implement each task, checking them off as we go. I'll announce each one and occasionally note how the specs/design informed the approach.
```
**DO:** For each task:
1. Announce: "Working on task N: [description]"
2. Implement the change in the codebase
3. Reference specs/design naturally: "The spec says X, so I'm doing Y"
4. Mark complete in tasks.md: `- [ ]``- [x]`
5. Brief status: "✓ Task N complete"
Keep narration light—don't over-explain every line of code.
After all tasks:
```
## Implementation Complete
All tasks done:
- [x] Task 1
- [x] Task 2
- [x] ...
The change is implemented! One more step—let's archive it.
```
---
## Phase 10: Archive
**EXPLAIN:**
```
## Archiving
When a change is complete, we archive it. This moves it from `openspec/changes/` to `openspec/changes/archive/YYYY-MM-DD-<name>/`.
Archived changes become your project's decision history—you can always find them later to understand why something was built a certain way.
```
**DO:**
```bash
openspec archive "<name>"
```
**SHOW:**
```
Archived to: `openspec/changes/archive/YYYY-MM-DD-<name>/`
The change is now part of your project's history. The code is in your codebase, the decision record is preserved.
```
---
## Phase 11: Recap & Next Steps
```
## Congratulations!
You just completed a full OpenSpec cycle:
1. **Explore** - Thought through the problem
2. **New** - Created a change container
3. **Proposal** - Captured WHY
4. **Specs** - Defined WHAT in detail
5. **Design** - Decided HOW
6. **Tasks** - Broke it into steps
7. **Apply** - Implemented the work
8. **Archive** - Preserved the record
This same rhythm works for any size change—a small fix or a major feature.
---
## Command Reference
| Command | What it does |
|---------|--------------|
| `/opsx:explore` | Think through problems before/during work |
| `/opsx:new` | Start a new change, step through artifacts |
| `/opsx:ff` | Fast-forward: create all artifacts at once |
| `/opsx:continue` | Continue working on an existing change |
| `/opsx:apply` | Implement tasks from a change |
| `/opsx:verify` | Verify implementation matches artifacts |
| `/opsx:archive` | Archive a completed change |
---
## What's Next?
Try `/opsx:new` or `/opsx:ff` on something you actually want to build. You've got the rhythm now!
```
---
## Graceful Exit Handling
### User wants to stop mid-way
If the user says they need to stop, want to pause, or seem disengaged:
```
No problem! Your change is saved at `openspec/changes/<name>/`.
To pick up where we left off later:
- `/opsx:continue <name>` - Resume artifact creation
- `/opsx:apply <name>` - Jump to implementation (if tasks exist)
The work won't be lost. Come back whenever you're ready.
```
Exit gracefully without pressure.
### User just wants command reference
If the user says they just want to see the commands or skip the tutorial:
```
## OpenSpec Quick Reference
| Command | What it does |
|---------|--------------|
| `/opsx:explore` | Think through problems (no code changes) |
| `/opsx:new <name>` | Start a new change, step by step |
| `/opsx:ff <name>` | Fast-forward: all artifacts at once |
| `/opsx:continue <name>` | Continue an existing change |
| `/opsx:apply <name>` | Implement tasks |
| `/opsx:verify <name>` | Verify implementation |
| `/opsx:archive <name>` | Archive when done |
Try `/opsx:new` to start your first change, or `/opsx:ff` if you want to move fast.
```
Exit gracefully.
---
## Guardrails
- **Follow the EXPLAIN → DO → SHOW → PAUSE pattern** at key transitions (after explore, after proposal draft, after tasks, after archive)
- **Keep narration light** during implementation—teach without lecturing
- **Don't skip phases** even if the change is small—the goal is teaching the workflow
- **Pause for acknowledgment** at marked points, but don't over-pause
- **Handle exits gracefully**—never pressure the user to continue
- **Use real codebase tasks**—don't simulate or use fake examples
- **Adjust scope gently**—guide toward smaller tasks but respect user choice

View File

@@ -1,138 +0,0 @@
---
name: openspec-sync-specs
description: Sync delta specs from a change to main specs. Use when the user wants to update main specs with changes from a delta spec, without archiving the change.
license: MIT
compatibility: Requires openspec CLI.
metadata:
author: openspec
version: "1.0"
generatedBy: "1.1.1"
---
Sync delta specs from a change to main specs.
This is an **agent-driven** operation - you will read delta specs and directly edit main specs to apply the changes. This allows intelligent merging (e.g., adding a scenario without copying the entire requirement).
**Input**: Optionally specify a change name. If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
**Steps**
1. **If no change name provided, prompt for selection**
Run `openspec list --json` to get available changes. Use the **AskUserQuestion tool** to let the user select.
Show changes that have delta specs (under `specs/` directory).
**IMPORTANT**: Do NOT guess or auto-select a change. Always let the user choose.
2. **Find delta specs**
Look for delta spec files in `openspec/changes/<name>/specs/*/spec.md`.
Each delta spec file contains sections like:
- `## ADDED Requirements` - New requirements to add
- `## MODIFIED Requirements` - Changes to existing requirements
- `## REMOVED Requirements` - Requirements to remove
- `## RENAMED Requirements` - Requirements to rename (FROM:/TO: format)
If no delta specs found, inform user and stop.
3. **For each delta spec, apply changes to main specs**
For each capability with a delta spec at `openspec/changes/<name>/specs/<capability>/spec.md`:
a. **Read the delta spec** to understand the intended changes
b. **Read the main spec** at `openspec/specs/<capability>/spec.md` (may not exist yet)
c. **Apply changes intelligently**:
**ADDED Requirements:**
- If requirement doesn't exist in main spec → add it
- If requirement already exists → update it to match (treat as implicit MODIFIED)
**MODIFIED Requirements:**
- Find the requirement in main spec
- Apply the changes - this can be:
- Adding new scenarios (don't need to copy existing ones)
- Modifying existing scenarios
- Changing the requirement description
- Preserve scenarios/content not mentioned in the delta
**REMOVED Requirements:**
- Remove the entire requirement block from main spec
**RENAMED Requirements:**
- Find the FROM requirement, rename to TO
d. **Create new main spec** if capability doesn't exist yet:
- Create `openspec/specs/<capability>/spec.md`
- Add Purpose section (can be brief, mark as TBD)
- Add Requirements section with the ADDED requirements
4. **Show summary**
After applying all changes, summarize:
- Which capabilities were updated
- What changes were made (requirements added/modified/removed/renamed)
**Delta Spec Format Reference**
```markdown
## ADDED Requirements
### Requirement: New Feature
The system SHALL do something new.
#### Scenario: Basic case
- **WHEN** user does X
- **THEN** system does Y
## MODIFIED Requirements
### Requirement: Existing Feature
#### Scenario: New scenario to add
- **WHEN** user does A
- **THEN** system does B
## REMOVED Requirements
### Requirement: Deprecated Feature
## RENAMED Requirements
- FROM: `### Requirement: Old Name`
- TO: `### Requirement: New Name`
```
**Key Principle: Intelligent Merging**
Unlike programmatic merging, you can apply **partial updates**:
- To add a scenario, just include that scenario under MODIFIED - don't copy existing scenarios
- The delta represents *intent*, not a wholesale replacement
- Use your judgment to merge changes sensibly
**Output On Success**
```
## Specs Synced: <change-name>
Updated main specs:
**<capability-1>**:
- Added requirement: "New Feature"
- Modified requirement: "Existing Feature" (added 1 scenario)
**<capability-2>**:
- Created new spec file
- Added requirement: "Another Feature"
Main specs are now updated. The change remains active - archive when implementation is complete.
```
**Guardrails**
- Read both delta and main specs before making changes
- Preserve existing content not mentioned in delta
- If something is unclear, ask for clarification
- Show what you're changing as you go
- The operation should be idempotent - running twice should give same result

View File

@@ -1,168 +0,0 @@
---
name: openspec-verify-change
description: Verify implementation matches change artifacts. Use when the user wants to validate that implementation is complete, correct, and coherent before archiving.
license: MIT
compatibility: Requires openspec CLI.
metadata:
author: openspec
version: "1.0"
generatedBy: "1.1.1"
---
Verify that an implementation matches the change artifacts (specs, tasks, design).
**Input**: Optionally specify a change name. If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
**Steps**
1. **If no change name provided, prompt for selection**
Run `openspec list --json` to get available changes. Use the **AskUserQuestion tool** to let the user select.
Show changes that have implementation tasks (tasks artifact exists).
Include the schema used for each change if available.
Mark changes with incomplete tasks as "(In Progress)".
**IMPORTANT**: Do NOT guess or auto-select a change. Always let the user choose.
2. **Check status to understand the schema**
```bash
openspec status --change "<name>" --json
```
Parse the JSON to understand:
- `schemaName`: The workflow being used (e.g., "spec-driven")
- Which artifacts exist for this change
3. **Get the change directory and load artifacts**
```bash
openspec instructions apply --change "<name>" --json
```
This returns the change directory and context files. Read all available artifacts from `contextFiles`.
4. **Initialize verification report structure**
Create a report structure with three dimensions:
- **Completeness**: Track tasks and spec coverage
- **Correctness**: Track requirement implementation and scenario coverage
- **Coherence**: Track design adherence and pattern consistency
Each dimension can have CRITICAL, WARNING, or SUGGESTION issues.
5. **Verify Completeness**
**Task Completion**:
- If tasks.md exists in contextFiles, read it
- Parse checkboxes: `- [ ]` (incomplete) vs `- [x]` (complete)
- Count complete vs total tasks
- If incomplete tasks exist:
- Add CRITICAL issue for each incomplete task
- Recommendation: "Complete task: <description>" or "Mark as done if already implemented"
**Spec Coverage**:
- If delta specs exist in `openspec/changes/<name>/specs/`:
- Extract all requirements (marked with "### Requirement:")
- For each requirement:
- Search codebase for keywords related to the requirement
- Assess if implementation likely exists
- If requirements appear unimplemented:
- Add CRITICAL issue: "Requirement not found: <requirement name>"
- Recommendation: "Implement requirement X: <description>"
6. **Verify Correctness**
**Requirement Implementation Mapping**:
- For each requirement from delta specs:
- Search codebase for implementation evidence
- If found, note file paths and line ranges
- Assess if implementation matches requirement intent
- If divergence detected:
- Add WARNING: "Implementation may diverge from spec: <details>"
- Recommendation: "Review <file>:<lines> against requirement X"
**Scenario Coverage**:
- For each scenario in delta specs (marked with "#### Scenario:"):
- Check if conditions are handled in code
- Check if tests exist covering the scenario
- If scenario appears uncovered:
- Add WARNING: "Scenario not covered: <scenario name>"
- Recommendation: "Add test or implementation for scenario: <description>"
7. **Verify Coherence**
**Design Adherence**:
- If design.md exists in contextFiles:
- Extract key decisions (look for sections like "Decision:", "Approach:", "Architecture:")
- Verify implementation follows those decisions
- If contradiction detected:
- Add WARNING: "Design decision not followed: <decision>"
- Recommendation: "Update implementation or revise design.md to match reality"
- If no design.md: Skip design adherence check, note "No design.md to verify against"
**Code Pattern Consistency**:
- Review new code for consistency with project patterns
- Check file naming, directory structure, coding style
- If significant deviations found:
- Add SUGGESTION: "Code pattern deviation: <details>"
- Recommendation: "Consider following project pattern: <example>"
8. **Generate Verification Report**
**Summary Scorecard**:
```
## Verification Report: <change-name>
### Summary
| Dimension | Status |
|--------------|------------------|
| Completeness | X/Y tasks, N reqs|
| Correctness | M/N reqs covered |
| Coherence | Followed/Issues |
```
**Issues by Priority**:
1. **CRITICAL** (Must fix before archive):
- Incomplete tasks
- Missing requirement implementations
- Each with specific, actionable recommendation
2. **WARNING** (Should fix):
- Spec/design divergences
- Missing scenario coverage
- Each with specific recommendation
3. **SUGGESTION** (Nice to fix):
- Pattern inconsistencies
- Minor improvements
- Each with specific recommendation
**Final Assessment**:
- If CRITICAL issues: "X critical issue(s) found. Fix before archiving."
- If only warnings: "No critical issues. Y warning(s) to consider. Ready for archive (with noted improvements)."
- If all clear: "All checks passed. Ready for archive."
**Verification Heuristics**
- **Completeness**: Focus on objective checklist items (checkboxes, requirements list)
- **Correctness**: Use keyword search, file path analysis, reasonable inference - don't require perfect certainty
- **Coherence**: Look for glaring inconsistencies, don't nitpick style
- **False Positives**: When uncertain, prefer SUGGESTION over WARNING, WARNING over CRITICAL
- **Actionability**: Every issue must have a specific recommendation with file/line references where applicable
**Graceful Degradation**
- If only tasks.md exists: verify task completion only, skip spec/design checks
- If tasks + specs exist: verify completeness and correctness, skip design
- If full artifacts: verify all three dimensions
- Always note which checks were skipped and why
**Output Format**
Use clear markdown with:
- Table for summary scorecard
- Grouped lists for issues (CRITICAL/WARNING/SUGGESTION)
- Code references in format: `file.ts:123`
- Specific, actionable recommendations
- No vague suggestions like "consider reviewing"

2
.gitignore vendored
View File

@@ -41,3 +41,5 @@ $RECYCLE.BIN/
*.swo
skills/**/test
__pycache__
temp

View File

@@ -56,7 +56,7 @@ Archive a completed change in the experimental workflow.
- If changes needed: "Sync now (recommended)", "Archive without syncing"
- If already synced: "Archive now", "Sync anyway", "Cancel"
If user chooses sync, execute `/opsx-sync` logic. Proceed to archive regardless of choice.
If user chooses sync, use Task tool (subagent_type: "general-purpose", prompt: "Use Skill tool to invoke openspec-sync-specs for change '<name>'. Delta spec analysis: <include the analyzed delta spec summary>"). Proceed to archive regardless of choice.
5. **Perform the archive**
@@ -150,5 +150,5 @@ Target archive directory already exists.
- Don't block archive on warnings - just inform and confirm
- Preserve .openspec.yaml when moving to archive (it moves with the directory)
- Show clear summary of what happened
- If sync is requested, use /opsx-sync approach (agent-driven)
- If sync is requested, use the Skill tool to invoke `openspec-sync-specs` (agent-driven)
- If delta specs exist, always run the sync assessment and show the combined summary before prompting

View File

@@ -222,7 +222,7 @@ Failed K changes:
```
## No Changes to Archive
No active changes found. Use `/opsx-new` to create a new change.
No active changes found. Create a new change to get started.
```
**Guardrails**

View File

@@ -4,7 +4,7 @@ description: Enter explore mode - think through ideas, investigate problems, cla
Enter explore mode. Think deeply. Visualize freely. Follow the conversation wherever it goes.
**IMPORTANT: Explore mode is for thinking, not implementing.** You may read files, search code, and investigate the codebase, but you must NEVER write code or implement features. If the user asks you to implement something, remind them to exit explore mode first (e.g., start a change with `/opsx-new` or `/opsx-ff`). You MAY create OpenSpec artifacts (proposals, designs, specs) if the user asks—that's capturing thinking, not implementing.
**IMPORTANT: Explore mode is for thinking, not implementing.** You may read files, search code, and investigate the codebase, but you must NEVER write code or implement features. If the user asks you to implement something, remind them to exit explore mode first and create a change proposal. You MAY create OpenSpec artifacts (proposals, designs, specs) if the user asks—that's capturing thinking, not implementing.
**This is a stance, not a workflow.** There are no fixed steps, no required sequence, no mandatory outputs. You're a thinking partner helping the user explore.
@@ -97,8 +97,7 @@ If the user mentioned a specific change name, read its artifacts for context.
Think freely. When insights crystallize, you might offer:
- "This feels solid enough to start a change. Want me to create one?"
→ Can transition to `/opsx-new` or `/opsx-ff`
- "This feels solid enough to start a change. Want me to create a proposal?"
- Or keep exploring - no pressure to formalize
### When a change exists
@@ -150,7 +149,7 @@ If the user mentions a change or you detect one is relevant:
There's no required ending. Discovery might:
- **Flow into action**: "Ready to start? `/opsx-new` or `/opsx-ff`"
- **Flow into a proposal**: "Ready to start? I can create a change proposal."
- **Result in artifact updates**: "Updated design.md with these decisions"
- **Just provide clarity**: User has what they need, moves on
- **Continue later**: "We can pick this up anytime"

View File

@@ -81,7 +81,10 @@ After completing all artifacts, summarize:
- Follow the `instruction` field from `openspec instructions` for each artifact type
- The schema defines what each artifact should contain - follow it
- Read dependency artifacts for context before creating new ones
- Use the `template` as a starting point, filling in based on context
- Use `template` as the structure for your output file - fill in its sections
- **IMPORTANT**: `context` and `rules` are constraints for YOU, not content for the file
- Do NOT copy `<context>`, `<rules>`, `<project_context>` blocks into the artifact
- These guide what you write, but should never appear in the output
**Guardrails**
- Create ALL artifacts needed for implementation (as defined by schema's `apply.requires`)

View File

@@ -8,16 +8,19 @@ Guide the user through their first complete OpenSpec workflow cycle. This is a t
## Preflight
Before starting, check if OpenSpec is initialized:
Before starting, check if the OpenSpec CLI is installed:
```bash
openspec status --json 2>&1 || echo "NOT_INITIALIZED"
# Unix/macOS
openspec --version 2>&1 || echo "CLI_NOT_INSTALLED"
# Windows (PowerShell)
# if (Get-Command openspec -ErrorAction SilentlyContinue) { openspec --version } else { echo "CLI_NOT_INSTALLED" }
```
**If not initialized:**
> OpenSpec isn't set up in this project yet. Run `openspec init` first, then come back to `/opsx-onboard`.
**If CLI not installed:**
> OpenSpec CLI is not installed. Install it first, then come back to `/opsx-onboard`.
Stop here if not initialized.
Stop here if not installed.
---
@@ -60,7 +63,10 @@ Scan the codebase for small improvement opportunities. Look for:
Also check recent git activity:
```bash
# Unix/macOS
git log --oneline -10 2>/dev/null || echo "No git history"
# Windows (PowerShell)
# git log --oneline -10 2>$null; if ($LASTEXITCODE -ne 0) { echo "No git history" }
```
### Present Suggestions
@@ -255,7 +261,10 @@ For a small task like this, we might only need one spec file.
**DO:** Create the spec file:
```bash
# Unix/macOS
mkdir -p openspec/changes/<name>/specs/<capability-name>
# Windows (PowerShell)
# New-Item -ItemType Directory -Force -Path "openspec/changes/<name>/specs/<capability-name>"
```
Draft the spec content:
@@ -450,21 +459,29 @@ This same rhythm works for any size change—a small fix or a major feature.
## Command Reference
**Core workflow:**
| Command | What it does |
|---------|--------------|
| `/opsx-propose` | Create a change and generate all artifacts |
| `/opsx-explore` | Think through problems before/during work |
| `/opsx-new` | Start a new change, step through artifacts |
| `/opsx-ff` | Fast-forward: create all artifacts at once |
| `/opsx-continue` | Continue working on an existing change |
| `/opsx-apply` | Implement tasks from a change |
| `/opsx-verify` | Verify implementation matches artifacts |
| `/opsx-archive` | Archive a completed change |
**Additional commands:**
| Command | What it does |
|---------|--------------|
| `/opsx-new` | Start a new change, step through artifacts one at a time |
| `/opsx-continue` | Continue working on an existing change |
| `/opsx-ff` | Fast-forward: create all artifacts at once |
| `/opsx-verify` | Verify implementation matches artifacts |
---
## What's Next?
Try `/opsx-new` or `/opsx-ff` on something you actually want to build. You've got the rhythm now!
Try `/opsx-propose` on something you actually want to build. You've got the rhythm now!
```
---
@@ -494,17 +511,25 @@ If the user says they just want to see the commands or skip the tutorial:
```
## OpenSpec Quick Reference
**Core workflow:**
| Command | What it does |
|---------|--------------|
| `/opsx-propose <name>` | Create a change and generate all artifacts |
| `/opsx-explore` | Think through problems (no code changes) |
| `/opsx-new <name>` | Start a new change, step by step |
| `/opsx-ff <name>` | Fast-forward: all artifacts at once |
| `/opsx-continue <name>` | Continue an existing change |
| `/opsx-apply <name>` | Implement tasks |
| `/opsx-verify <name>` | Verify implementation |
| `/opsx-archive <name>` | Archive when done |
Try `/opsx-new` to start your first change, or `/opsx-ff` if you want to move fast.
**Additional commands:**
| Command | What it does |
|---------|--------------|
| `/opsx-new <name>` | Start a new change, step by step |
| `/opsx-continue <name>` | Continue an existing change |
| `/opsx-ff <name>` | Fast-forward: all artifacts at once |
| `/opsx-verify <name>` | Verify implementation |
Try `/opsx-propose` to start your first change.
```
Exit gracefully.

View File

@@ -0,0 +1,103 @@
---
description: Propose a new change - create it and generate all artifacts in one step
---
Propose a new change - create the change and generate all artifacts in one step.
I'll create a change with artifacts:
- proposal.md (what & why)
- design.md (how)
- tasks.md (implementation steps)
When ready to implement, run /opsx-apply
---
**Input**: The argument after `/opsx-propose` is the change name (kebab-case), OR a description of what the user wants to build.
**Steps**
1. **If no input provided, ask what they want to build**
Use the **AskUserQuestion tool** (open-ended, no preset options) to ask:
> "What change do you want to work on? Describe what you want to build or fix."
From their description, derive a kebab-case name (e.g., "add user authentication" → `add-user-auth`).
**IMPORTANT**: Do NOT proceed without understanding what the user wants to build.
2. **Create the change directory**
```bash
openspec new change "<name>"
```
This creates a scaffolded change at `openspec/changes/<name>/` with `.openspec.yaml`.
3. **Get the artifact build order**
```bash
openspec status --change "<name>" --json
```
Parse the JSON to get:
- `applyRequires`: array of artifact IDs needed before implementation (e.g., `["tasks"]`)
- `artifacts`: list of all artifacts with their status and dependencies
4. **Create artifacts in sequence until apply-ready**
Use the **TodoWrite tool** to track progress through the artifacts.
Loop through artifacts in dependency order (artifacts with no pending dependencies first):
a. **For each artifact that is `ready` (dependencies satisfied)**:
- Get instructions:
```bash
openspec instructions <artifact-id> --change "<name>" --json
```
- The instructions JSON includes:
- `context`: Project background (constraints for you - do NOT include in output)
- `rules`: Artifact-specific rules (constraints for you - do NOT include in output)
- `template`: The structure to use for your output file
- `instruction`: Schema-specific guidance for this artifact type
- `outputPath`: Where to write the artifact
- `dependencies`: Completed artifacts to read for context
- Read any completed dependency files for context
- Create the artifact file using `template` as the structure
- Apply `context` and `rules` as constraints - but do NOT copy them into the file
- Show brief progress: "Created <artifact-id>"
b. **Continue until all `applyRequires` artifacts are complete**
- After creating each artifact, re-run `openspec status --change "<name>" --json`
- Check if every artifact ID in `applyRequires` has `status: "done"` in the artifacts array
- Stop when all `applyRequires` artifacts are done
c. **If an artifact requires user input** (unclear context):
- Use **AskUserQuestion tool** to clarify
- Then continue with creation
5. **Show final status**
```bash
openspec status --change "<name>"
```
**Output**
After completing all artifacts, summarize:
- Change name and location
- List of artifacts created with brief descriptions
- What's ready: "All artifacts created! Ready for implementation."
- Prompt: "Run `/opsx-apply` to start implementing."
**Artifact Creation Guidelines**
- Follow the `instruction` field from `openspec instructions` for each artifact type
- The schema defines what each artifact should contain - follow it
- Read dependency artifacts for context before creating new ones
- Use `template` as the structure for your output file - fill in its sections
- **IMPORTANT**: `context` and `rules` are constraints for YOU, not content for the file
- Do NOT copy `<context>`, `<rules>`, `<project_context>` blocks into the artifact
- These guide what you write, but should never appear in the output
**Guardrails**
- Create ALL artifacts needed for implementation (as defined by schema's `apply.requires`)
- Always read dependency artifacts before creating a new one
- If context is critically unclear, ask the user - but prefer making reasonable decisions to keep momentum
- If a change with that name already exists, ask if user wants to continue it or create a new one
- Verify each artifact file exists after writing before proceeding to next

View File

@@ -0,0 +1 @@
../../skills/lyxy-reader-office

View File

@@ -0,0 +1 @@
../../skills/lyxy-runner-js

View File

@@ -0,0 +1 @@
../../skills/lyxy-runner-python

View File

@@ -6,7 +6,7 @@ compatibility: Requires openspec CLI.
metadata:
author: openspec
version: "1.0"
generatedBy: "1.1.1"
generatedBy: "1.2.0"
---
Implement tasks from an OpenSpec change.

View File

@@ -6,7 +6,7 @@ compatibility: Requires openspec CLI.
metadata:
author: openspec
version: "1.0"
generatedBy: "1.1.1"
generatedBy: "1.2.0"
---
Archive a completed change in the experimental workflow.
@@ -63,7 +63,7 @@ Archive a completed change in the experimental workflow.
- If changes needed: "Sync now (recommended)", "Archive without syncing"
- If already synced: "Archive now", "Sync anyway", "Cancel"
If user chooses sync, execute /opsx-sync logic (use the openspec-sync-specs skill). Proceed to archive regardless of choice.
If user chooses sync, use Task tool (subagent_type: "general-purpose", prompt: "Use Skill tool to invoke openspec-sync-specs for change '<name>'. Delta spec analysis: <include the analyzed delta spec summary>"). Proceed to archive regardless of choice.
5. **Perform the archive**

View File

@@ -6,7 +6,7 @@ compatibility: Requires openspec CLI.
metadata:
author: openspec
version: "1.0"
generatedBy: "1.1.1"
generatedBy: "1.2.0"
---
Archive multiple completed changes in a single operation.
@@ -229,7 +229,7 @@ Failed K changes:
```
## No Changes to Archive
No active changes found. Use `/opsx-new` to create a new change.
No active changes found. Create a new change to get started.
```
**Guardrails**

View File

@@ -6,7 +6,7 @@ compatibility: Requires openspec CLI.
metadata:
author: openspec
version: "1.0"
generatedBy: "1.1.1"
generatedBy: "1.2.0"
---
Continue working on a change by creating the next artifact.

View File

@@ -6,12 +6,12 @@ compatibility: Requires openspec CLI.
metadata:
author: openspec
version: "1.0"
generatedBy: "1.1.1"
generatedBy: "1.2.0"
---
Enter explore mode. Think deeply. Visualize freely. Follow the conversation wherever it goes.
**IMPORTANT: Explore mode is for thinking, not implementing.** You may read files, search code, and investigate the codebase, but you must NEVER write code or implement features. If the user asks you to implement something, remind them to exit explore mode first (e.g., start a change with `/opsx-new` or `/opsx-ff`). You MAY create OpenSpec artifacts (proposals, designs, specs) if the user asks—that's capturing thinking, not implementing.
**IMPORTANT: Explore mode is for thinking, not implementing.** You may read files, search code, and investigate the codebase, but you must NEVER write code or implement features. If the user asks you to implement something, remind them to exit explore mode first and create a change proposal. You MAY create OpenSpec artifacts (proposals, designs, specs) if the user asks—that's capturing thinking, not implementing.
**This is a stance, not a workflow.** There are no fixed steps, no required sequence, no mandatory outputs. You're a thinking partner helping the user explore.
@@ -95,8 +95,7 @@ This tells you:
Think freely. When insights crystallize, you might offer:
- "This feels solid enough to start a change. Want me to create one?"
→ Can transition to `/opsx-new` or `/opsx-ff`
- "This feels solid enough to start a change. Want me to create a proposal?"
- Or keep exploring - no pressure to formalize
### When a change exists
@@ -252,7 +251,7 @@ You: That changes everything.
There's no required ending. Discovery might:
- **Flow into action**: "Ready to start? /opsx-new or /opsx-ff"
- **Flow into a proposal**: "Ready to start? I can create a change proposal."
- **Result in artifact updates**: "Updated design.md with these decisions"
- **Just provide clarity**: User has what they need, moves on
- **Continue later**: "We can pick this up anytime"
@@ -269,8 +268,7 @@ When it feels like things are crystallizing, you might summarize:
**Open questions**: [if any remain]
**Next steps** (if ready):
- Create a change: /opsx-new <name>
- Fast-forward to tasks: /opsx-ff <name>
- Create a change proposal
- Keep exploring: just keep talking
```

View File

@@ -6,7 +6,7 @@ compatibility: Requires openspec CLI.
metadata:
author: openspec
version: "1.0"
generatedBy: "1.1.1"
generatedBy: "1.2.0"
---
Fast-forward through artifact creation - generate everything needed to start implementation in one go.

View File

@@ -6,7 +6,7 @@ compatibility: Requires openspec CLI.
metadata:
author: openspec
version: "1.0"
generatedBy: "1.1.1"
generatedBy: "1.2.0"
---
Start a new change using the experimental artifact-driven approach.

View File

@@ -6,7 +6,7 @@ compatibility: Requires openspec CLI.
metadata:
author: openspec
version: "1.0"
generatedBy: "1.1.1"
generatedBy: "1.2.0"
---
Guide the user through their first complete OpenSpec workflow cycle. This is a teaching experience—you'll do real work in their codebase while explaining each step.
@@ -15,16 +15,19 @@ Guide the user through their first complete OpenSpec workflow cycle. This is a t
## Preflight
Before starting, check if OpenSpec is initialized:
Before starting, check if the OpenSpec CLI is installed:
```bash
openspec status --json 2>&1 || echo "NOT_INITIALIZED"
# Unix/macOS
openspec --version 2>&1 || echo "CLI_NOT_INSTALLED"
# Windows (PowerShell)
# if (Get-Command openspec -ErrorAction SilentlyContinue) { openspec --version } else { echo "CLI_NOT_INSTALLED" }
```
**If not initialized:**
> OpenSpec isn't set up in this project yet. Run `openspec init` first, then come back to `/opsx-onboard`.
**If CLI not installed:**
> OpenSpec CLI is not installed. Install it first, then come back to `/opsx-onboard`.
Stop here if not initialized.
Stop here if not installed.
---
@@ -67,7 +70,10 @@ Scan the codebase for small improvement opportunities. Look for:
Also check recent git activity:
```bash
# Unix/macOS
git log --oneline -10 2>/dev/null || echo "No git history"
# Windows (PowerShell)
# git log --oneline -10 2>$null; if ($LASTEXITCODE -ne 0) { echo "No git history" }
```
### Present Suggestions
@@ -262,7 +268,10 @@ For a small task like this, we might only need one spec file.
**DO:** Create the spec file:
```bash
# Unix/macOS
mkdir -p openspec/changes/<name>/specs/<capability-name>
# Windows (PowerShell)
# New-Item -ItemType Directory -Force -Path "openspec/changes/<name>/specs/<capability-name>"
```
Draft the spec content:
@@ -457,21 +466,29 @@ This same rhythm works for any size change—a small fix or a major feature.
## Command Reference
**Core workflow:**
| Command | What it does |
|---------|--------------|
| `/opsx-propose` | Create a change and generate all artifacts |
| `/opsx-explore` | Think through problems before/during work |
| `/opsx-new` | Start a new change, step through artifacts |
| `/opsx-ff` | Fast-forward: create all artifacts at once |
| `/opsx-continue` | Continue working on an existing change |
| `/opsx-apply` | Implement tasks from a change |
| `/opsx-verify` | Verify implementation matches artifacts |
| `/opsx-archive` | Archive a completed change |
**Additional commands:**
| Command | What it does |
|---------|--------------|
| `/opsx-new` | Start a new change, step through artifacts one at a time |
| `/opsx-continue` | Continue working on an existing change |
| `/opsx-ff` | Fast-forward: create all artifacts at once |
| `/opsx-verify` | Verify implementation matches artifacts |
---
## What's Next?
Try `/opsx-new` or `/opsx-ff` on something you actually want to build. You've got the rhythm now!
Try `/opsx-propose` on something you actually want to build. You've got the rhythm now!
```
---
@@ -501,17 +518,25 @@ If the user says they just want to see the commands or skip the tutorial:
```
## OpenSpec Quick Reference
**Core workflow:**
| Command | What it does |
|---------|--------------|
| `/opsx-propose <name>` | Create a change and generate all artifacts |
| `/opsx-explore` | Think through problems (no code changes) |
| `/opsx-new <name>` | Start a new change, step by step |
| `/opsx-ff <name>` | Fast-forward: all artifacts at once |
| `/opsx-continue <name>` | Continue an existing change |
| `/opsx-apply <name>` | Implement tasks |
| `/opsx-verify <name>` | Verify implementation |
| `/opsx-archive <name>` | Archive when done |
Try `/opsx-new` to start your first change, or `/opsx-ff` if you want to move fast.
**Additional commands:**
| Command | What it does |
|---------|--------------|
| `/opsx-new <name>` | Start a new change, step by step |
| `/opsx-continue <name>` | Continue an existing change |
| `/opsx-ff <name>` | Fast-forward: all artifacts at once |
| `/opsx-verify <name>` | Verify implementation |
Try `/opsx-propose` to start your first change.
```
Exit gracefully.

View File

@@ -0,0 +1,110 @@
---
name: openspec-propose
description: Propose a new change with all artifacts generated in one step. Use when the user wants to quickly describe what they want to build and get a complete proposal with design, specs, and tasks ready for implementation.
license: MIT
compatibility: Requires openspec CLI.
metadata:
author: openspec
version: "1.0"
generatedBy: "1.2.0"
---
Propose a new change - create the change and generate all artifacts in one step.
I'll create a change with artifacts:
- proposal.md (what & why)
- design.md (how)
- tasks.md (implementation steps)
When ready to implement, run /opsx-apply
---
**Input**: The user's request should include a change name (kebab-case) OR a description of what they want to build.
**Steps**
1. **If no clear input provided, ask what they want to build**
Use the **AskUserQuestion tool** (open-ended, no preset options) to ask:
> "What change do you want to work on? Describe what you want to build or fix."
From their description, derive a kebab-case name (e.g., "add user authentication" → `add-user-auth`).
**IMPORTANT**: Do NOT proceed without understanding what the user wants to build.
2. **Create the change directory**
```bash
openspec new change "<name>"
```
This creates a scaffolded change at `openspec/changes/<name>/` with `.openspec.yaml`.
3. **Get the artifact build order**
```bash
openspec status --change "<name>" --json
```
Parse the JSON to get:
- `applyRequires`: array of artifact IDs needed before implementation (e.g., `["tasks"]`)
- `artifacts`: list of all artifacts with their status and dependencies
4. **Create artifacts in sequence until apply-ready**
Use the **TodoWrite tool** to track progress through the artifacts.
Loop through artifacts in dependency order (artifacts with no pending dependencies first):
a. **For each artifact that is `ready` (dependencies satisfied)**:
- Get instructions:
```bash
openspec instructions <artifact-id> --change "<name>" --json
```
- The instructions JSON includes:
- `context`: Project background (constraints for you - do NOT include in output)
- `rules`: Artifact-specific rules (constraints for you - do NOT include in output)
- `template`: The structure to use for your output file
- `instruction`: Schema-specific guidance for this artifact type
- `outputPath`: Where to write the artifact
- `dependencies`: Completed artifacts to read for context
- Read any completed dependency files for context
- Create the artifact file using `template` as the structure
- Apply `context` and `rules` as constraints - but do NOT copy them into the file
- Show brief progress: "Created <artifact-id>"
b. **Continue until all `applyRequires` artifacts are complete**
- After creating each artifact, re-run `openspec status --change "<name>" --json`
- Check if every artifact ID in `applyRequires` has `status: "done"` in the artifacts array
- Stop when all `applyRequires` artifacts are done
c. **If an artifact requires user input** (unclear context):
- Use **AskUserQuestion tool** to clarify
- Then continue with creation
5. **Show final status**
```bash
openspec status --change "<name>"
```
**Output**
After completing all artifacts, summarize:
- Change name and location
- List of artifacts created with brief descriptions
- What's ready: "All artifacts created! Ready for implementation."
- Prompt: "Run `/opsx-apply` or ask me to implement to start working on the tasks."
**Artifact Creation Guidelines**
- Follow the `instruction` field from `openspec instructions` for each artifact type
- The schema defines what each artifact should contain - follow it
- Read dependency artifacts for context before creating new ones
- Use `template` as the structure for your output file - fill in its sections
- **IMPORTANT**: `context` and `rules` are constraints for YOU, not content for the file
- Do NOT copy `<context>`, `<rules>`, `<project_context>` blocks into the artifact
- These guide what you write, but should never appear in the output
**Guardrails**
- Create ALL artifacts needed for implementation (as defined by schema's `apply.requires`)
- Always read dependency artifacts before creating a new one
- If context is critically unclear, ask the user - but prefer making reasonable decisions to keep momentum
- If a change with that name already exists, ask if user wants to continue it or create a new one
- Verify each artifact file exists after writing before proceeding to next

View File

@@ -6,7 +6,7 @@ compatibility: Requires openspec CLI.
metadata:
author: openspec
version: "1.0"
generatedBy: "1.1.1"
generatedBy: "1.2.0"
---
Sync delta specs from a change to main specs.

View File

@@ -6,7 +6,7 @@ compatibility: Requires openspec CLI.
metadata:
author: openspec
version: "1.0"
generatedBy: "1.1.1"
generatedBy: "1.2.0"
---
Verify that an implementation matches the change artifacts (specs, tasks, design).

View File

@@ -1,55 +0,0 @@
基于知识库项目进行问答。
**输入**: `/lyxy-kb-ask` 后的参数为项目名称,可选附带问题。例如:
- `/lyxy-kb-ask my-project` — 进入问答模式
- `/lyxy-kb-ask my-project 这个系统用了什么技术栈?` — 直接提问
**前置条件**: 查找并阅读名为 **lyxy-kb** 的 skill了解渐进式查询策略和来源引用格式。
**步骤**
1. **获取项目名称并验证结构**
从参数中获取项目名称。如果未提供参数,提示用户输入。
按照 lyxy-kb skill「结构完整性验证」规则检查项目目录不完整则提示用户先 init。
2. **加载项目摘要**
读取 `<project-name>/project.md`,获取项目概述和文件索引。
按照 lyxy-kb skill「空知识库」规则如果文件索引为空尚无已入库文件告知用户知识库为空建议先使用 `/lyxy-kb-ingest <project-name>` 入库文档,终止操作。
3. **进入问答模式**
如果用户在参数中附带了问题,直接回答该问题。否则提示用户可以开始提问。
**对每个问题,按照 lyxy-kb skill「渐进式查询策略」执行**
**a) 分析问题与文件索引的关联**
根据用户的问题内容,对照 project.md 文件索引表中各文件的摘要,判断需要查阅哪些 parsed 文件。
**b) 按需加载 parsed 文件**
读取相关的 `<project-name>/parsed/<文件名>.md` 文件。如果文件较大,可以先提取标题结构,再读取相关章节。
**c) 回答并标注来源**
基于获取的信息回答问题。按照 lyxy-kb skill「来源引用格式」标注来源
```
根据《文件名》(parsed/文件名.md)...
```
如果回答综合了多个文件的信息,分别标注各信息点的来源。
**d) 无相关信息处理**
按照 lyxy-kb skill「无相关信息」规则明确告知用户当前知识库中未找到相关信息不编造答案。
4. **保持会话上下文**
回答完成后,保持当前的知识库上下文。用户可以继续提问,无需每次重新加载 project.md。已加载的 parsed 文件内容可在后续问答中复用。
会话的退出由用户自然决定(开启新话题或新会话),不主动终止问答模式。

View File

@@ -1,65 +0,0 @@
解析 sources/ 中的新文件并增量更新知识库。
**输入**: `/lyxy-kb-ingest` 后的参数为项目名称。
**前置条件**: 查找并阅读名为 **lyxy-kb** 的 skill了解知识库的完整规范。
**步骤**
1. **获取项目名称并验证结构**
从参数中获取项目名称。如果未提供参数,提示用户输入。
按照 lyxy-kb skill「结构完整性验证」规则检查项目目录不完整则提示用户先 init。
2. **检查 office 文档解析能力**
按照 lyxy-kb skill「Office 文档解析」规则,查找当前环境中名为 **lyxy-reader-office** 的 skill。如果不存在且无其他可替代的文档解析 skill则提示用户无法处理 office 文档并中止流程。
3. **读取 manifest.json**
读取 `<project-name>/manifest.json`,获取已入库文件的信息。
4. **递归扫描 sources/ 目录**
按照 lyxy-kb skill「sources/ 扫描规则」,递归检查 sources/ 及其所有子目录中的文件。如果无任何文件,提示用户无待处理文件并终止。
5. **预检查**
**空文件检测**:按照 lyxy-kb skill「空文件处理」规则识别 0 字节文件,标记为跳过。
**同名不同扩展名冲突检测**:按照 lyxy-kb skill「同名不同扩展名冲突检测」中的两条检测规则执行。冲突文件标记为跳过。
如果有跳过的文件,列出详情(空文件 / 冲突文件分别列出)。如果所有文件都被跳过,终止流程。
6. **逐个处理文件**
对每个通过预检查的文件:
**a) 解析**:按照 lyxy-kb skill「文件类型解析策略」判断解析方式。office 文档使用 lyxy-reader-office skill查找并阅读该 skill 获取具体命令),其他文件直接读取。
**b) 写入 parsed**:按照 lyxy-kb skill「parsed 文件元信息标记」格式,在内容头部添加元信息注释,写入 `<project-name>/parsed/<文件名>.md`(同名覆盖)。
**c) 归档**:按照 lyxy-kb skill「归档命名规则」移动原始文件到 archive/(带时间戳后缀 `YYYYMMDDHHmm`)。
**d) 更新 manifest.json**:新文件追加条目,已有文件在 versions 数组追加新版本。使用 `sha256sum` 计算文件哈希。更新 `last_ingest`
**e) 解析失败处理**:按照 lyxy-kb skill「解析失败处理」规则失败文件保留在 sources/ 中不移动,报告错误,继续处理下一个文件。
7. **增量更新 project.md**
按照 lyxy-kb skill「增量追加」策略
- 对每个新处理的文件,读取其 parsed 内容生成简要摘要1-2 句话)
- 新文件:在文件索引表追加新行
- 已有文件更新:更新文件索引表中对应行
- 在更新记录追加本次 ingest 条目
- 不修改概述和关键信息部分
8. **输出结果**
汇总显示:
- 成功处理的文件列表
- 跳过的文件(空文件 / 冲突文件 / 解析失败文件,分别列出)
- 当前项目已入库文件总数
- 提示可使用 `/lyxy-kb-rebuild <project-name>` 更新概述和关键信息
- 提示可使用 `/lyxy-kb-ask <project-name>` 进行知识问答

View File

@@ -1,67 +0,0 @@
初始化一个知识库项目。
**输入**: `/lyxy-kb-init` 后的参数为项目名称。
**前置条件**: 查找并阅读名为 **lyxy-kb** 的 skill了解知识库的目录结构规范、项目名称规则和 project.md 格式规范。
**步骤**
1. **获取项目名称**
从参数中获取项目名称。如果未提供参数,提示用户输入项目名称。
2. **验证项目名称**
按照 lyxy-kb skill 中的「项目名称规则」验证名称是否合法(只允许中文、英文、数字、短横线、下划线,不允许空格和其他特殊字符)。不合法时提示用户修改。
3. **检查目标目录是否已存在**
检查 CWD 下是否已存在同名目录。如果目录已存在,提示用户该目录已存在,不覆盖任何现有内容,终止操作。
4. **创建目录结构**
```bash
mkdir -p <project-name>/parsed <project-name>/sources <project-name>/archive
```
5. **创建 project.md**
按照 lyxy-kb skill 中定义的「project.md 格式规范」,生成初始内容:
```markdown
# <项目名称>
## 概述
(待补充)
## 关键信息
(待补充)
## 文件索引
| 文件名 | 解析文件 | 最新归档 | 摘要 |
|--------|----------|----------|------|
## 更新记录
- <YYYY-MM-DD HH:mm>: 初始化项目
```
6. **创建 manifest.json**
```json
{
"project": "<项目名称>",
"created_at": "<当前时间 ISO 格式>",
"last_ingest": null,
"files": []
}
```
7. **输出结果**
提示用户:
- 项目已创建,显示完整的目录结构
- 引导用户将文档放入 `<project-name>/sources/` 目录
- 提示使用 `/lyxy-kb-ingest <project-name>` 解析入库

View File

@@ -1,55 +0,0 @@
全量重新生成 project.md。
**输入**: `/lyxy-kb-rebuild` 后的参数为项目名称。
**前置条件**: 查找并阅读名为 **lyxy-kb** 的 skill了解 project.md 格式规范和全量重写策略。
**步骤**
1. **获取项目名称并验证结构**
从参数中获取项目名称。如果未提供参数,提示用户输入。
按照 lyxy-kb skill「结构完整性验证」规则检查项目目录不完整则提示用户先 init。
2. **检查 parsed 目录**
列出 `<project-name>/parsed/` 下的所有 `.md` 文件。如果为空,提示用户尚无已解析文件,建议先执行 `/lyxy-kb-ingest <project-name>`
3. **检查 sources/ 待处理文件**
检查 `<project-name>/sources/` 中是否还有未 ingest 的文件。如果有,提醒用户 sources/ 中存在未入库文件rebuild 将仅基于已有的 parsed 文件生成,建议先执行 ingest。
4. **确认操作**
向用户说明 rebuild 将覆盖当前 project.md 的概述、关键信息和文件索引(更新记录会保留),请求用户确认是否继续。用户确认后再执行。
5. **读取所有 parsed 文件**
逐个读取 `<project-name>/parsed/` 下的所有 `.md` 文件内容。
6. **读取 manifest.json**
读取 `<project-name>/manifest.json`,获取文件元信息(用于生成文件索引表中的归档路径等信息)。
7. **读取现有更新记录**
读取当前 `<project-name>/project.md`,提取 `## 更新记录` 部分的内容以保留历史记录。
8. **全量重新生成 project.md**
按照 lyxy-kb skill「全量重写」策略和 project.md 格式规范,基于所有 parsed 文件内容重新生成:
- **概述**:基于所有文件内容,生成高度总结的项目信息(几百字以内)
- **关键信息**:从所有文档中提炼核心要点
- **文件索引**:基于 manifest.json 和 parsed 文件,重新生成完整索引表(文件名、解析文件路径、最新归档路径、简要摘要)
- **更新记录**:保留历史记录,追加本次 rebuild 条目,格式:`- <YYYY-MM-DD HH:mm>: 全量重建 project.md`
将生成的内容写入 `<project-name>/project.md`,覆盖原有内容。
9. **输出结果**
提示用户:
- project.md 已全量重建
- 显示处理的文件数量
- 提示可使用 `/lyxy-kb-ask <project-name>` 进行知识问答

View File

@@ -0,0 +1,903 @@
# Claude 技能构建完全指南
## 引言
[技能Skill](https://claude.com/blog/skills)是一组指令——以简单文件夹的形式打包——用于教会 Claude 如何处理特定任务或工作流程。技能是自定义 Claude 以满足特定需求的最强大方式之一。它让你无需在每次对话中反复解释你的偏好、流程和领域专业知识,而是只需教会 Claude 一次,便可持续受益。
当你有可重复的工作流程时,技能尤其强大:根据规格生成前端设计、使用一致的方法论进行研究、创建符合团队风格指南的文档,或者编排多步骤流程。它们与 Claude 的内置功能(如代码执行和文档创建)配合良好。对于正在构建 MCP 集成的开发者来说,技能增加了另一个强大的层次,帮助将原始工具访问转化为可靠、优化的工作流程。
本指南涵盖了构建有效技能所需的一切——从规划、结构到测试和分发。无论你是为自己、团队还是社区构建技能,你都能在全文中找到实用的模式和真实案例。
### 你将学到:
- 技能结构的技术要求和最佳实践
- 独立技能和 MCP 增强工作流程的模式
- 我们在不同用例中观察到的有效模式
- 如何测试、迭代和分发你的技能
### 适用人群:
- 希望 Claude 一致地遵循特定工作流程的开发者
- 希望 Claude 遵循特定工作流程的高级用户
- 寻求在整个组织中标准化 Claude 使用方式的团队
### 本指南的两条路径
构建独立技能?重点关注"基础知识"、"规划与设计"以及第 1-2 类。增强 MCP 集成?"技能 + MCP"部分和第 3 类适合你。两条路径共享相同的技术要求,但你可以选择与你的用例相关的内容。
**本指南的收获**:阅读完本指南后,你将能够在一次工作时段内构建一个功能完整的技能。使用 skill-creator 构建和测试你的第一个可用技能大约需要 15-30 分钟。
让我们开始吧。
# 第 1 章:基础知识
## 什么是技能?
技能是一个包含以下内容的文件夹:
- **SKILL.md必需**:带有 YAML 前置元数据的 Markdown 格式指令
- **scripts/(可选)**可执行代码Python、Bash 等)
- **references/(可选)**:按需加载的文档
- **assets/(可选)**:输出中使用的模板、字体、图标
## 核心设计原则
### 渐进式披露
技能采用三级系统:
- **第一级YAML 前置元数据)**:始终加载到 Claude 的系统提示中。仅提供足够的信息让 Claude 知道何时应该使用每个技能,而无需将所有内容加载到上下文中。
- **第二级SKILL.md 正文)**:当 Claude 认为该技能与当前任务相关时加载。包含完整的指令和指南。
- **第三级(关联文件)**技能目录中捆绑的附加文件Claude 可以根据需要选择导航和发现。
这种渐进式披露在保持专业能力的同时最大限度地减少了 token 使用。
### 可组合性
Claude 可以同时加载多个技能。你的技能应该能够与其他技能良好协作,而不是假设它是唯一可用的能力。
### 可移植性
技能在 Claude.ai、Claude Code 和 API 中的工作方式完全相同。只需创建一次技能,它就可以在所有平台上无需修改地运行,前提是环境支持技能所需的任何依赖项。
## 面向 MCP 构建者:技能 + 连接器
_💡 构建不含 MCP 的独立技能?跳至"规划与设计"——你随时可以回来查阅此部分。_
如果你已经有一个[可用的 MCP 服务器](https://support.claude.com/en/articles/10949351-getting-started-with-local-mcp-servers-on-claude-desktop),那么你已经完成了最困难的部分。技能是其上的知识层——捕获你已经了解的工作流程和最佳实践,以便 Claude 能够一致地应用它们。
### 厨房比喻
**MCP 提供专业厨房**:访问工具、食材和设备。
**技能提供食谱**:关于如何创造有价值产出的分步指令。
两者结合,使用户能够完成复杂任务,而无需自己弄清楚每一步。
它们如何协同工作:
| MCP连接性 | 技能(知识) |
| ---------------------------------------------------------- | ---------------------------- |
| Notion、Asana、Linear 等)有效地将 Claude 连接到你的服务 | 教会 Claude 如何使用你的服务 |
| 提供实时数据访问和工具调用 | 捕获工作流程和最佳实践 |
| Claude 能做什么 | Claude 应该怎么做 |
### 这对你的 MCP 用户为何重要
#### 没有技能时:
- 用户连接了你的 MCP 但不知道下一步该做什么
- 支持工单询问"如何用你的集成做 X"
- 每次对话都从零开始
- 结果不一致,因为用户每次的提示方式不同
- 当真正的问题是工作流程指导时,用户却责怪你的连接器
#### 有技能时:
- 预构建的工作流程在需要时自动激活
- 一致、可靠的工具使用
- 最佳实践嵌入到每次交互中
- 降低你的集成的学习曲线
# 第 2 章:规划与设计
## 从用例开始
在编写任何代码之前,确定 2-3 个你的技能应该支持的具体用例。
### 良好的用例定义:
```
用例:项目冲刺计划
触发器:用户说"帮我规划这个冲刺"或"创建冲刺任务"
步骤:
1. 从 Linear 获取当前项目状态(通过 MCP
2. 分析团队速度和产能
3. 建议任务优先级
4. 在 Linear 中创建带有适当标签和估算的任务
结果:冲刺完全规划完成,任务已创建
```
### 问问自己:
- 用户想要完成什么?
- 这需要哪些多步骤工作流程?
- 需要哪些工具(内置的还是 MCP 的)?
- 应该嵌入哪些领域知识或最佳实践?
## 常见技能用例类别
在 Anthropic我们观察到三种常见用例
### 类别 1文档与资产创建
用途:创建一致、高质量的输出,包括文档、演示文稿、应用程序、设计、代码等。
_真实案例_[frontend-design 技能](https://github.com/anthropics/skills/tree/main/skills/frontend-design)(另见 [docx、pptx、xlsx 和 ppt 技能](https://github.com/anthropics/skills/tree/main/skills)
"创建独特的、生产级的前端界面,具有高设计质量。在构建 Web 组件、页面、产物、海报或应用程序时使用。"
**关键技术**
- 嵌入式风格指南和品牌标准
- 用于一致输出的模板结构
- 最终确定前的质量检查清单
- 无需外部工具——使用 Claude 的内置功能
### 类别 2工作流程自动化
用途:受益于一致方法论的多步骤流程,包括跨多个 MCP 服务器的协调。
_真实案例_[skill-creator 技能](https://github.com/anthropics/skills/blob/main/skills/skill-creator/SKILL.md)
"用于创建新技能的交互式指南。引导用户完成用例定义、前置元数据生成、指令编写和验证。"
**关键技术**
- 带有验证关卡的分步工作流程
- 常见结构的模板
- 内置的审查和改进建议
- 迭代优化循环
### 类别 3MCP 增强
用途:增强 MCP 服务器提供的工具访问的工作流程指导。
_真实案例_[sentry-code-review 技能(来自 Sentry](https://github.com/getsentry/sentry-for-claude/tree/main/skills)
"使用 Sentry 通过其 MCP 服务器提供的错误监控数据,自动分析和修复 GitHub Pull Request 中检测到的 bug。"
**关键技术**
- 按顺序协调多个 MCP 调用
- 嵌入领域专业知识
- 提供用户原本需要指定的上下文
- 常见 MCP 问题的错误处理
## 定义成功标准
### 你如何知道你的技能正在工作?
这些是理想目标——粗略的基准而非精确的阈值。追求严谨,但接受会有一定程度的主观评估成分。我们正在积极开发更强大的衡量指导和工具。
### 定量指标:
- 技能在 90% 的相关查询上触发
- 如何衡量:运行 10-20 个应该触发你技能的测试查询。跟踪它自动加载的次数与需要显式调用的次数。
- 在 X 次工具调用内完成工作流程
- 如何衡量:比较启用和未启用技能时执行相同任务的情况。计算工具调用次数和消耗的总 token 数。
- 每个工作流程 0 次失败的 API 调用
- 如何衡量:在测试运行期间监控 MCP 服务器日志。跟踪重试率和错误代码。
### 定性指标:
- 用户不需要提示 Claude 下一步
- 如何评估:在测试期间,记录你需要重定向或澄清的频率。征求 beta 用户的反馈。
- 工作流程完成时无需用户纠正
- 如何评估:运行相同的请求 3-5 次。比较输出的结构一致性和质量。
- 跨会话的一致结果
- 如何评估:新用户能否在第一次尝试时以最少的指导完成任务?
## 技术要求
### 文件结构
```
your-skill-name/
├── SKILL.md # 必需 - 主技能文件
├── scripts/ # 可选 - 可执行代码
│ ├── process_data.py # 示例
│ └── validate.sh # 示例
├── references/ # 可选 - 文档
│ ├── api-guide.md # 示例
│ └── examples/ # 示例
└── assets/ # 可选 - 模板等
└── report-template.md # 示例
```
### 关键规则
#### SKILL.md 命名:
- 必须完全是 SKILL.md区分大小写
- 不接受任何变体skill.md、Skill.md 等)
#### 技能文件夹命名:
- 使用 kebab-casenotion-project-setup ✅
- 不要有空格Notion Project Setup ❌
- 不要有下划线notion_project_setup ❌
- 不要有大写字母NotionProjectSetup ❌
#### 不要包含 README.md
- 不要在技能文件夹内包含 README.md
- 所有文档都放在 SKILL.md 或 references/ 中
- 注意:通过 GitHub 分发时,你仍然需要一个仓库级别的 README 供人类用户阅读——参见"分发与共享"。
### YAML 前置元数据:最重要的部分
YAML 前置元数据是 Claude 决定是否加载你的技能的依据。务必做好这一点。
#### 最小必需格式
```
name: your-skill-name
description: 它做什么。当用户要求 [特定短语] 时使用。
```
这就是你开始所需的全部内容。
### 字段要求
**name**(必需):
- 仅限 kebab-case
- 不能有空格或大写字母
- 应与文件夹名称匹配
**description**(必需):
- **必须同时包含**
- 技能做什么
- 何时使用它(触发条件)
- 少于 1024 个字符
- 不能有 XML 标签(< 或 >
- 包含用户可能会说的特定任务
- 如果相关,提及文件类型
**license**(可选):
- 如果将技能开源则使用
- 常见MIT、Apache-2.0
**compatibility**(可选)
- 1-500 个字符
- 指示环境要求:例如目标产品、所需系统包、网络访问需求等
**metadata**(可选):
- 任何自定义键值对
- 建议author、version、mcp-server
- 示例:
```YAML
metadata:
author: ProjectHub
version: 1.0.0
mcp-server: projecthub
```
#### 安全限制
**前置元数据中禁止的内容**
- XML 尖括号(< >
- 名称中包含"claude"或"anthropic"的技能(保留字)
**原因**:前置元数据出现在 Claude 的系统提示中。恶意内容可能注入指令。
### 编写有效的技能
#### description 字段
根据 Anthropic 的[工程博客](https://www.anthropic.com/engineering/equipping-agents-for-the-real-world-with-agent-skills)"此元数据……提供足够的信息让 Claude 知道何时应该使用每个技能,而无需将所有内容加载到上下文中。"这是渐进式披露的第一级。
**结构**
```
[它做什么] + [何时使用] + [关键能力]
```
**好的 description 示例**
```
# 好 - 具体且可操作
description: 分析 Figma 设计文件并生成开发者交接文档。当用户上传 .fig 文件、要求"设计规格"、"组件文档"或"设计到代码交接"时使用。
# 好 - 包含触发短语
description: 管理 Linear 项目工作流程,包括冲刺计划、任务创建和状态跟踪。当用户提到"冲刺"、"Linear 任务"、"项目计划"或要求"创建工单"时使用。
# 好 - 清晰的价值主张
description: PayFlow 的端到端客户入职工作流程。处理账户创建、支付设置和订阅管理。当用户说"引导新客户"、"设置订阅"或"创建 PayFlow 账户"时使用。
```
**差的 description 示例**
```
# 太模糊
description: 帮助处理项目。
# 缺少触发器
description: 创建复杂的多页文档系统。
# 太技术化,没有用户触发器
description: 实现具有层级关系的 Project 实体模型。
```
#### 编写主要指令
在前置元数据之后,用 Markdown 编写实际的指令。
**推荐结构**
根据你的技能调整此模板。用你的具体内容替换括号中的部分。
````
---
name: your-skill
description: [...]
---
# 你的技能名称
## 指令
### 步骤 1[第一个主要步骤]
清晰解释会发生什么。
示例:```bash python scripts/fetch_data.py --project-id PROJECT_ID```
预期输出:[描述成功的样子]
````
(根据需要添加更多步骤)
## 示例
### 示例 1[常见场景]
用户说:"设置一个新的营销活动"
操作:
1. 通过 MCP 获取现有活动
2. 使用提供的参数创建新活动
结果:活动已创建,附带确认链接
(根据需要添加更多示例)
## 故障排除
### 错误:[常见错误消息]
**原因**[为什么会发生]
**解决方案**[如何修复]
(根据需要添加更多错误案例)
### 指令最佳实践
#### 具体且可操作
✅ **好**
```
运行 python scripts/validate.py --input {filename} 检查数据格式。
如果验证失败,常见问题包括:
- 缺少必需字段(将它们添加到 CSV
- 无效的日期格式(使用 YYYY-MM-DD
```
❌ **差**
```
在继续之前验证数据。
```
#### 包含错误处理
```
## 常见问题
### MCP 连接失败
如果你看到"Connection refused"
1. 验证 MCP 服务器正在运行:检查设置 > 扩展
2. 确认 API 密钥有效
3. 尝试重新连接:设置 > 扩展 > [你的服务] > 重新连接
```
#### 清晰地引用捆绑资源
```
在编写查询之前,查阅 `references/api-patterns.md` 了解:
- 速率限制指南
- 分页模式
- 错误代码和处理
```
#### 使用渐进式披露
保持 SKILL.md 专注于核心指令。将详细文档移至 `references/` 并链接到它。(参见[核心设计原则](https://www.anthropic.com/engineering/equipping-agents-for-the-real-world-with-agent-skills)了解三级系统的工作原理。)
# 第 3 章:测试与迭代
技能可以根据你的需求在不同的严格程度下进行测试:
- **在 Claude.ai 中手动测试** - 直接运行查询并观察行为。快速迭代,无需设置。
- **在 Claude Code 中脚本化测试** - 自动化测试用例以在更改间进行可重复的验证。
- **通过技能 API 进行程序化测试** - 构建评估套件,针对定义的测试集系统地运行。
选择与你的质量要求和技能可见度相匹配的方法。一个由小团队内部使用的技能与一个部署给数千企业用户的技能有不同的测试需求。
> **专业提示**:先在单个任务上迭代,然后再扩展
我们发现,最有效的技能创建者会在单个具有挑战性的任务上迭代,直到 Claude 成功,然后将成功的方法提取到技能中。这利用了 Claude 的上下文学习能力,比广泛测试提供更快的信号。一旦你有了一个可用的基础,再扩展到多个测试用例以确保覆盖率。
## 推荐的测试方法
根据早期经验,有效的技能测试通常涵盖三个领域:
### 1: 触发测试
**目标**:确保你的技能在正确的时间加载。
**测试用例**
- ✅ 在明显任务上触发
- ✅ 在改述的请求上触发
- ❌ 不在无关主题上触发
**示例测试套件**
```
应该触发:
- "帮我设置一个新的 ProjectHub 工作区"
- "我需要在 ProjectHub 中创建一个项目"
- "为 Q4 计划初始化一个 ProjectHub 项目"
不应该触发:
- "旧金山的天气怎么样?"
- "帮我写 Python 代码"
- "创建一个电子表格"(除非 ProjectHub 技能处理表格)
```
### 2: 功能测试
**目标**:验证技能产生正确的输出。
**测试用例**
- 生成有效输出
- API 调用成功
- 错误处理正常工作
- 覆盖边缘情况
**示例**
```
测试:创建包含 5 个任务的项目
给定:项目名称"Q4 计划"5 个任务描述
当:技能执行工作流程
那么:
- 项目在 ProjectHub 中创建
- 5 个任务以正确的属性创建
- 所有任务链接到项目
- 无 API 错误
```
### 3: 性能比较
**目标**:证明技能相比基线改善了结果。
使用"定义成功标准"中的指标。以下是一个比较可能的样子。
**基线比较**
```
没有技能时:
- 用户每次都提供指令
- 15 次来回消息
- 3 次失败的 API 调用需要重试
- 消耗 12,000 个 token
有技能时:
- 自动工作流程执行
- 仅 2 个澄清问题
- 0 次失败的 API 调用
- 消耗 6,000 个 token
```
## 使用 skill-creator 技能
skill-creator 技能——可在 Claude.ai 通过插件目录获取,或下载用于 Claude Code——可以帮助你构建和迭代技能。如果你有 MCP 服务器并了解你的前 2-3 个工作流程,你可以在一次工作时段内构建和测试一个功能完整的技能——通常在 15-30 分钟内。
### 创建技能:
- 从自然语言描述生成技能
- 生成带有前置元数据的正确格式 SKILL.md
- 建议触发短语和结构
### 审查技能:
- 标记常见问题(模糊的描述、缺失的触发器、结构问题)
- 识别潜在的过度/不足触发风险
- 根据技能的声明目的建议测试用例
### 迭代改进:
- 使用你的技能并遇到边缘情况或失败后,将这些示例带回 skill-creator
- 示例:"使用此聊天中识别的问题和解决方案来改进技能处理 [特定边缘情况] 的方式"
**使用方法**
```
"使用 skill-creator 技能帮我为 [你的用例] 构建一个技能"
```
_注意skill-creator 帮助你设计和完善技能但不执行自动化测试套件或产生定量评估结果。_
## 基于反馈的迭代
技能是活的文档。计划根据以下情况进行迭代:
### 触发不足的信号:
- 技能在应该加载时没有加载
- 用户手动启用它
- 关于何时使用它的支持问题
> **解决方案**:在 description 中添加更多细节和细微差别——这可能包括关键字,特别是技术术语
### 过度触发的信号:
- 技能为无关查询加载
- 用户禁用它
- 对目的感到困惑
> **解决方案**:添加负面触发器,更加具体
### 执行问题:
- 结果不一致
- API 调用失败
- 需要用户纠正
> **解决方案**:改进指令,添加错误处理
# 第 4 章:模式与故障排除
这些模式来自早期采用者和内部团队创建的技能。它们代表了我们观察到的有效的常见方法,而非规定性模板。
## 选择你的方法:问题优先 vs. 工具优先
可以把它想象成 Home Depot家得宝。你可能带着问题走进去——"我需要修理厨房柜子"——然后员工指引你找到合适的工具。或者你可能挑选了一把新电钻,然后询问如何将它用于你的特定工作。
技能的工作方式相同:
- **问题优先**"我需要设置一个项目工作区" → 你的技能按正确的顺序编排正确的 MCP 调用。用户描述结果;技能处理工具。
- **工具优先**"我连接了 Notion MCP" → 你的技能教会 Claude 最佳工作流程和最佳实践。用户有访问权限;技能提供专业知识。
大多数技能倾向于一个方向。知道哪种框架适合你的用例有助于你从下面选择正确的模式。
## 模式 1顺序工作流程编排
**适用场景**:你的用户需要按特定顺序执行的多步骤流程。
### 示例结构:
```
## 工作流程:引导新客户
### 步骤 1创建账户
调用 MCP 工具:`create_customer`
参数name、email、company
### 步骤 2设置支付
调用 MCP 工具:`setup_payment_method`
等待:支付方式验证
### 步骤 3创建订阅
调用 MCP 工具:`create_subscription`
参数plan_id、customer_id来自步骤 1
### 步骤 4发送欢迎邮件
调用 MCP 工具:`send_email`
模板welcome_email_template
```
**关键技术**
- 明确的步骤顺序
- 步骤之间的依赖关系
- 每个阶段的验证
- 失败时的回滚指令
## 模式 2多 MCP 协调
**适用场景**:工作流程跨越多个服务。
### 示例:设计到开发交接
```
### 阶段 1设计导出Figma MCP
1. 从 Figma 导出设计资产
2. 生成设计规格
3. 创建资产清单
### 阶段 2资产存储Drive MCP
1. 在 Drive 中创建项目文件夹
2. 上传所有资产
3. 生成可共享链接
### 阶段 3任务创建Linear MCP
1. 创建开发任务
2. 将资产链接附加到任务
3. 分配给工程团队
### 阶段 4通知Slack MCP
1. 在 #engineering 发布交接摘要
2. 包含资产链接和任务引用
```
**关键技术**
- 清晰的阶段分离
- MCP 之间的数据传递
- 进入下一阶段前的验证
- 集中式错误处理
## 模式 3迭代优化
**适用场景**:输出质量通过迭代得到改善。
### 示例:报告生成
```
## 迭代报告创建
### 初稿
1. 通过 MCP 获取数据
2. 生成第一版报告草稿
3. 保存到临时文件
### 质量检查
1. 运行验证脚本:`scripts/check_report.py`
2. 识别问题:
- 缺失的章节
- 不一致的格式
- 数据验证错误
### 优化循环
1. 解决每个识别的问题
2. 重新生成受影响的章节
3. 重新验证
4. 重复直到达到质量阈值
### 最终化
1. 应用最终格式
2. 生成摘要
3. 保存最终版本
```
**关键技术**
- 明确的质量标准
- 迭代改进
- 验证脚本
- 知道何时停止迭代
## 模式 4上下文感知工具选择
**适用场景**:相同的结果,但根据上下文使用不同的工具。
### 示例:文件存储
```
## 智能文件存储
### 决策树
1. 检查文件类型和大小
2. 确定最佳存储位置:
- 大文件(>10MB使用云存储 MCP
- 协作文档:使用 Notion/Docs MCP
- 代码文件:使用 GitHub MCP
- 临时文件:使用本地存储
### 执行存储
基于决策:
- 调用适当的 MCP 工具
- 应用特定于服务的元数据
- 生成访问链接
### 向用户提供上下文
解释为什么选择该存储
```
**关键技术**
- 清晰的决策标准
- 备用选项
- 关于选择的透明度
## 模式 5领域特定智能
**适用场景**:你的技能添加了超越工具访问的专业知识。
### 示例:金融合规
```
## 带合规的支付处理
### 处理前(合规检查)
1. 通过 MCP 获取交易详情
2. 应用合规规则:
- 检查制裁名单
- 验证司法管辖区许可
- 评估风险级别
3. 记录合规决策
### 处理
如果合规通过:
- 调用支付处理 MCP 工具
- 应用适当的欺诈检查
- 处理交易
否则:
- 标记以供审查
- 创建合规案例
### 审计追踪
- 记录所有合规检查
- 记录处理决策
- 生成审计报告
```
**关键技术**
- 领域专业知识嵌入逻辑
- 行动前的合规性
- 全面的文档记录
- 清晰的治理
## 故障排除
### 技能不触发
**症状**:技能从不自动加载
**修复**
修改你的 description 字段。参见"description 字段"中的好/差示例。
**快速检查清单**
- 是否太通用?("帮助处理项目"不会工作)
- 是否包含用户实际会说的触发短语?
- 如果适用,是否提到相关文件类型?
**调试方法**
询问 Claude"你什么时候会使用 [技能名称] 技能?"Claude 会引用 description。根据缺失的内容进行调整。
### 技能触发太频繁
**症状**:技能为无关查询加载
**解决方案**
1. **添加负面触发器**
```
description: CSV 文件的高级数据分析。用于统计建模、回归、聚类。不要用于简单的数据探索(改用 data-viz 技能)。
```
2. **更加具体**
```
# 太宽泛
description: 处理文档
# 更具体
description: 处理用于合同审查的 PDF 法律文档
```
3. **明确范围**
```
description: 用于电子商务的 PayFlow 支付处理。专门用于在线支付工作流程,而非一般财务查询。
```
### MCP 连接问题
**症状**:技能加载但 MCP 调用失败
**检查清单**
1. **验证 MCP 服务器已连接**
- Claude.ai设置 > 扩展 > [你的服务] 应显示"已连接"状态
2. **检查身份验证**
- API 密钥有效且未过期
- 已授予适当的权限/范围
- OAuth 令牌已刷新
3. **独立测试 MCP**
- 让 Claude 直接调用 MCP不使用技能
- "使用 [服务] MCP 获取我的项目"
- 如果这失败了,问题在 MCP 而非技能
4. **验证工具名称**
- 技能引用正确的 MCP 工具名称
- 检查 MCP 服务器文档
- 工具名称区分大小写
### 指令未被遵循
**症状**:技能加载但 Claude 不遵循指令
**常见原因**
1. **指令太冗长**
- 保持指令简洁
- 使用项目符号和编号列表
- 将详细参考移至单独的文件
2. **指令被埋没**
- 将关键指令放在顶部
- 使用 ## 重要 或 ## 关键 标题
- 如果需要,重复关键点
3. **语言模糊**
```
# 差
确保正确验证内容
# 好
关键:在调用 create_project 之前,验证:
- 项目名称非空
- 至少分配一名团队成员
- 开始日期不在过去
```
**高级技术**:对于关键验证,考虑捆绑一个以编程方式执行检查的脚本,而不是依赖语言指令。代码是确定性的;语言解释不是。参见 Office 技能中此模式的示例。
4. **模型"懒惰"** 添加明确的鼓励:
```
## 性能说明
- 花时间彻底完成这项工作
- 质量比速度更重要
- 不要跳过验证步骤
```
注意:将此添加到用户提示中比添加到 SKILL.md 中更有效
### 大上下文问题
**症状**:技能似乎很慢或响应质量下降
**原因**
- 技能内容太大
- 同时启用了太多技能
- 所有内容都被加载而不是渐进式披露
**解决方案**
1. **优化 SKILL.md 大小**
- 将详细文档移至 references/
- 链接到引用而不是内联
- SKILL.md 保持在 5,000 字以下
2. **减少启用的技能**
- 评估是否同时启用了超过 20-50 个技能
- 建议选择性启用
- 考虑为相关功能使用技能"包"

View File

@@ -17,9 +17,7 @@ my-skill/
技能使用**渐进式披露**来高效管理上下文:
1. **发现**:在启动时,代理仅加载每个可用技能的名称和描述,足以了解何时可能相关。
2. **激活**:当任务匹配技能的描述时,代理将完整的 `SKILL.md` 指令读入上下文。
3. **执行**:代理遵循指令,根据需要可选择性地加载引用文件或执行打包的代码。
这种方法使代理保持快速,同时能够按需访问更多上下文。
@@ -55,11 +53,8 @@ Use this skill when the user needs to work with PDF files...
- `description`:何时使用此技能
Markdown 正文包含实际指令,对结构或内容没有特定限制。
这种简单格式具有一些关键优势:
- **自文档化**:技能作者或用户可以阅读 `SKILL.md` 并了解其功能,使技能易于审核和改进。
- **可扩展**:技能的复杂度可以范围从仅文本指令到可执行代码、资产和模板。
- **可移植**:技能只是文件,因此易于编辑、版本控制和共享。

View File

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

View File

@@ -0,0 +1,322 @@
## Context
当前手动管理 AI 编程平台的 skills/commands 面临以下挑战:
- **平台差异**Claude Code 使用目录结构OpenCode 使用扁平化文件命名(如 `lyxy-kb-init.md`
- **手动操作**:需要手动理解每个平台的规范、复制文件、重命名
- **追踪困难**:无法知道哪些 skills/commands 已安装、来自哪个源、何时安装
- **更新风险**:手动更新容易遗漏文件或破坏已有配置
目标用户是作者本人,工具需要简单直接,优先实现核心功能而非过度设计。
**约束条件**
- 单用户场景,不考虑多用户协作
- 仅支持最新版本,不处理多版本共存
- 不解析依赖关系,用户手动管理依赖
- 平台支持范围Claude Code、OpenCode未来可扩展
## Goals / Non-Goals
**Goals:**
- 自动化从 git 源仓库到目标平台的完整安装流程
- 支持全局和项目级两种安装作用域
- 内置 Claude Code 和 OpenCode 的平台适配规则
- 记录所有安装操作,支持查询、更新、卸载、清理
- 事务性安装机制,避免部分失败导致的不一致状态
- 用户友好的交互体验(确认覆盖、清晰的错误提示)
**Non-Goals:**
- ❌ 多版本管理(语义化版本、版本锁定、版本冲突解决)
- ❌ 依赖解析和自动安装依赖
- ❌ 插件化的平台适配器系统
- ❌ 复杂的仓库注册中心或包索引服务
- ❌ 跨平台迁移或批量同步
## Decisions
### 1. 技术栈选择Go + Cobra
**决策**:使用 Go 语言开发CLI 框架选择 Cobra。
**理由**
- **Go**:编译为单一可执行文件,无运行时依赖,跨平台分发简单
- **Cobra**:业界标准 CLI 框架kubectl、docker 都用它),支持子命令、自动帮助生成、参数验证
- **替代方案**:标准库 `flag` 包功能过于简单,不适合多子命令场景
**影响**:开发者需要熟悉 Go 和 Cobra 的基本用法。
---
### 2. 配置文件格式JSON
**决策**:使用 JSON 格式存储配置repository.json、install.json
**理由**
- Go 标准库原生支持,无需第三方依赖
- 结构清晰,易于程序读写和人工检查
- **替代方案**YAML需要第三方库、TOML生态较小
**配置结构**
```
~/.skillmgr/
├── repository.json # 源仓库列表
├── install.json # 安装记录
└── cache/ # git 仓库缓存
```
---
### 3. 事务性安装Tmp Staging
**决策**采用三阶段事务性安装Stage → Commit → Rollback
**理由**
- 避免部分文件复制失败导致目标目录处于不一致状态
- 先在系统临时目录(`os.TempDir()`)组装完整的目标文件树
- 验证成功后一次性移动到最终位置
- 失败时自动清理临时目录,不污染目标
**流程**
```
1. 创建 staging 目录(/tmp/skillmgr-xxxxx/
2. 复制所有文件到 staging应用平台适配规则
3. 验证 staging 目录完整性
4. 移动 staging 到目标位置(原子操作)
5. 失败则删除 staging不影响目标
```
**替代方案**:直接复制到目标(风险高)、使用 Git worktree过于复杂
---
### 4. 平台适配器:内置而非插件化
**决策**:将 Claude Code 和 OpenCode 的适配规则硬编码在程序内,不支持用户自定义适配器。
**理由**
- 目标平台数量少且稳定2 个),插件系统收益低
- 硬编码保证规则的正确性和一致性
- 简化实现和维护成本
- **替代方案**:配置文件定义适配规则(增加复杂度)、插件系统(过度设计)
**适配器接口**
```go
type PlatformAdapter interface {
GetSkillInstallPath(scope, name) (string, error)
GetCommandInstallPath(scope, group) (string, error)
AdaptSkill(sourcePath, destPath) (map[string]string, error)
AdaptCommand(sourcePath, destPath, group) (map[string]string, error)
}
```
**差异处理**
- **Skills**:两个平台都保持目录结构,直接复制
- **Commands**
- Claude Code 保持目录结构(`commands/lyxy-kb/init.md`
- OpenCode 扁平化文件名(`command/lyxy-kb-init.md`
---
### 5. 安装策略:复制而非符号链接
**决策**:全局和项目级安装都使用文件复制,不使用符号链接。
**理由**
- 避免符号链接在跨平台和跨文件系统时的兼容性问题(尤其是 Windows
- 项目可以独立于全局安装,避免意外修改影响其他项目
- 磁盘空间在现代系统中不是瓶颈
- **替代方案**:全局符号链接(复杂度高,跨平台问题)
**影响**
- 更新全局安装不会自动影响项目级安装(需显式更新)
- 多个项目可以独立更新各自的 skills/commands
---
### 6. 命令文件组织:命令组概念
**决策**:将 commands 按目录组织,整个目录作为"命令组"一起安装。
**理由**
- 源仓库中 commands 按功能分组(如 `commands/lyxy-kb/` 包含 init/ask/ingest/rebuild
- 命令组内的命令通常有关联,应一起安装
- 简化用户操作,避免逐个命令安装
**命令组到命令的映射**
- Claude Code`/lyxy-kb-init``commands/lyxy-kb/init.md`
- OpenCode`/lyxy-kb:init``command/lyxy-kb-init.md`
---
### 7. 安装记录清理Clean 命令
**决策**:提供 `clean` 命令扫描并清理孤立记录install.json 中存在但目标路径已删除)。
**理由**
- 用户可能手动删除已安装的目录
- 避免 install.json 与实际文件系统状态不一致
- 不自动清理(避免误删),由用户显式触发
**实现**
```bash
skillmgr clean
# 扫描 install.json 中所有记录
# 检查 install_path 是否存在
# 列出孤立记录并确认删除
```
---
### 8. 目录冲突处理:用户决策
**决策**:安装前检查目标目录是否存在,存在时由用户决定是否覆盖。
**场景**
1. **install.json 有记录 + 目录存在**:已安装,询问是否覆盖
2. **install.json 无记录 + 目录存在**:未被 skillmgr 管理的目录,询问是否覆盖
3. **install.json 有记录 + 目录不存在**:孤立记录,清理记录后继续安装
**用户交互**
```
Skill 'lyxy-kb' is already installed. Overwrite? [y/N]:
```
---
### 9. 项目结构:独立 Go 项目
**决策**:在 `manager/` 目录下创建独立的 Go 项目,与现有 skills 仓库分离。
**目录结构**
```
manager/
├── cmd/skillmgr/ # CLI 命令实现
├── internal/ # 内部包(不对外暴露)
│ ├── config/ # 配置文件读写
│ ├── repo/ # Git 仓库管理
│ ├── adapter/ # 平台适配器
│ ├── installer/ # 安装逻辑
│ └── prompt/ # 用户交互
├── pkg/ # 可对外暴露的包
│ └── fileutil/ # 文件工具
├── go.mod
└── README.md
```
**理由**
- 不污染现有 skills 仓库结构
- 工具本身可以独立开发、测试、发布
- 清晰的模块边界
---
### 10. 测试隔离:环境变量注入
**决策**:通过环境变量覆盖配置和目标路径,实现零污染测试。
**理由**
- 测试不应影响用户的实际配置(`~/.skillmgr/`)和安装目录(`~/.claude/`
- 环境变量注入是轻量级且侵入性最小的方案
- 支持并行测试(每个测试独立目录)
**实现**
```go
// 配置路径注入
func GetConfigRoot() (string, error) {
if testRoot := os.Getenv("SKILLMGR_TEST_ROOT"); testRoot != "" {
return testRoot, nil
}
// 生产模式...
}
// 目标路径注入
func getBasePath(scope Scope) (string, error) {
if testBase := os.Getenv("SKILLMGR_TEST_BASE"); testBase != "" {
return testBase, nil
}
// 生产模式...
}
```
**测试使用**
```go
func TestInstall(t *testing.T) {
testRoot := t.TempDir()
testBase := t.TempDir()
os.Setenv("SKILLMGR_TEST_ROOT", testRoot)
os.Setenv("SKILLMGR_TEST_BASE", testBase)
defer os.Unsetenv("SKILLMGR_TEST_ROOT")
defer os.Unsetenv("SKILLMGR_TEST_BASE")
// 测试代码...
}
```
**替代方案**
- **依赖注入**(将路径作为参数传递):侵入性强,需要重构所有函数签名
- **Mock 文件系统**(如 afero复杂度高且无法测试真实文件系统行为
- **专用测试模式标志**:需要额外的全局状态管理
## Risks / Trade-offs
### 1. 无版本管理
**风险**:用户无法回退到旧版本的 skill/command更新可能引入破坏性变更。
**缓解**
- 文档中建议用户在重要项目中使用 git 管理项目配置目录(如 `.claude/`
- 工具记录 `updated_at` 时间,方便追溯
---
### 2. 无依赖解析
**风险**:安装 command 时,依赖的 skill 可能未安装(如 `lyxy-kb` 命令依赖 `lyxy-reader-office` skill
**缓解**
- 在 skill 的 SKILL.md 中明确记录依赖关系(如 `compatibility` 字段)
- 错误提示中建议用户检查依赖
- 未来可选增强:扫描 SKILL.md 提示缺失依赖
---
### 3. Git 依赖
**风险**:工具依赖系统中已安装 Git 客户端,无 Git 则无法拉取仓库。
**缓解**
- 在 README 中明确前置条件
- 首次运行时检测 Git 是否可用,提示安装
- 错误消息中包含 Git 安装指引
---
### 4. 跨文件系统移动失败
**风险**`os.Rename()` 在跨文件系统时会失败(如 tmp 在 tmpfs目标在 ext4
**缓解**
- 捕获 Rename 错误fallback 到递归复制 + 删除 staging
- 在事务实现中明确处理两种路径
---
### 5. 平台适配规则变化
**风险**Claude Code 或 OpenCode 未来修改目录结构规范,导致工具失效。
**缓解**
- 将适配规则集中在 `internal/adapter/` 包中,便于修改
- 提供版本号,用户可锁定工具版本以保证稳定性
- 文档中建议关注平台更新公告
---
### 6. 手动修改配置文件
**风险**:用户手动编辑 repository.json 或 install.json 可能破坏格式,导致解析失败。
**缓解**
- JSON 解析错误时提示备份并重建配置文件
- 提供 `doctor` 命令(未来增强)诊断和修复配置

View File

@@ -0,0 +1,37 @@
## Why
当前手动管理和分发 AI 编程平台的 skills/commands 存在诸多问题需要手动理解不同平台Claude Code、OpenCode的目录结构差异、手动复制文件、手动处理命名转换如 OpenCode 的扁平化命名),且难以追踪已安装的内容和版本。随着 skills 数量增长和多平台支持需求,这种手动流程变得不可维护。需要一个自动化的管理工具来简化从源仓库到目标平台的完整流程。
## What Changes
- **新增**:创建独立的 Go CLI 工具skillmgr提供命令行界面管理 skills/commands 的完整生命周期
- **新增**:支持从 git 仓库拉取和缓存 skills/commands 源代码
- **新增**:内置 Claude Code 和 OpenCode 两个平台的适配规则
- **新增**:支持全局安装(~/.claude/、~/.opencode/)和项目级安装(./.claude/、./.opencode/
- **新增**:安装记录追踪系统,支持更新、卸载、清理孤立记录
- **新增**:事务性安装机制,通过 tmp staging 避免部分失败导致的不一致状态
- **新增**:用户交互确认(目录覆盖、冲突解决)
## Capabilities
### New Capabilities
- `repository-management`:管理源仓库配置(添加、移除、同步 git 仓库)
- `skill-installation`:安装 skills 到目标平台(支持全局/项目作用域)
- `command-installation`:安装 commands 到目标平台(处理命令组和平台特定命名)
- `platform-adaptation`:适配不同 AI 编程平台的目录结构和命名规则
- `install-tracking`:跟踪和管理安装记录(查询、更新、清理)
- `transactional-install`事务性文件安装staging → commit → rollback
- `test-infrastructure`测试环境隔离和自动化零污染测试、fixture 管理、CI 集成)
### Modified Capabilities
(无现有能力需要修改)
## Impact
- **新增项目**:在 `manager/` 目录下创建独立的 Go 项目(不影响现有 skills 仓库结构)
- **用户配置**:在用户目录创建 `~/.skillmgr/` 配置目录repository.json、install.json、cache/
- **目标平台**:修改目标平台的 `.claude/``.opencode/` 目录(根据用户操作)
- **依赖**:需要 Git 客户端(用于 clone/pull 操作)
- **兼容性**:工具设计为独立运行,不破坏现有手动管理的 skills/commands

View File

@@ -0,0 +1,95 @@
## ADDED Requirements
### Requirement: 用户可以安装命令组到全局目录
工具必须支持将整个命令组commands 目录下的子目录)安装到平台配置目录。
#### Scenario: 全局安装命令组到 Claude Code
- **WHEN** 用户执行 `skillmgr install command <group> --platform claude --global`
- **THEN** 系统将 `commands/<group>/` 下所有 .md 文件复制到 `~/.claude/commands/<group>/`
#### Scenario: 全局安装命令组到 OpenCode
- **WHEN** 用户执行 `skillmgr install command <group> --platform opencode --global`
- **THEN** 系统将 `commands/<group>/` 下所有 .md 文件重命名为 `<group>-<action>.md` 并复制到 `~/.opencode/command/`
---
### Requirement: 用户可以安装命令组到项目目录
工具必须支持将命令组安装到当前项目的平台配置目录。
#### Scenario: 项目级安装命令组到 Claude Code
- **WHEN** 用户在项目目录执行 `skillmgr install command <group> --platform claude`
- **THEN** 系统将命令组复制到 `./.claude/commands/<group>/`
#### Scenario: 项目级安装命令组到 OpenCode
- **WHEN** 用户在项目目录执行 `skillmgr install command <group> --platform opencode`
- **THEN** 系统将命令组扁平化复制到 `./.opencode/command/`
---
### Requirement: 系统必须在所有源仓库中查找命令组
工具必须在所有已配置源仓库的 `commands/` 目录中搜索指定命令组。
#### Scenario: 找到命令组
- **WHEN** 源仓库包含 `commands/<group>/` 目录且内有 .md 文件
- **THEN** 系统使用该命令组进行安装
#### Scenario: 命令组不存在
- **WHEN** 所有源仓库都不包含指定命令组
- **THEN** 系统报错"command '<group>' not found in any repository"
#### Scenario: 命令组目录为空
- **WHEN** 找到命令组目录但其中没有 .md 文件
- **THEN** 系统报错"command group '<group>' contains no command files"
---
### Requirement: OpenCode 平台必须扁平化命令文件名
工具必须在安装到 OpenCode 平台时,将命令文件重命名为 `<group>-<action>.md` 格式。
#### Scenario: 转换命令文件名
- **WHEN** 安装 `commands/lyxy-kb/init.md` 到 OpenCode
- **THEN** 文件被重命名为 `lyxy-kb-init.md`
#### Scenario: 保留 .md 扩展名
- **WHEN** 转换文件名时
- **THEN** 系统保留 `.md` 扩展名
---
### Requirement: 系统必须处理命令组目录冲突
工具必须在安装前检查目标目录或文件是否已存在。
#### Scenario: Claude Code 命令组目录冲突
- **WHEN** `~/.claude/commands/<group>/` 目录已存在
- **THEN** 系统询问用户是否覆盖
#### Scenario: OpenCode 命令文件冲突
- **WHEN** 目标 `~/.opencode/command/` 中已存在同名的 `<group>-*.md` 文件
- **THEN** 系统询问用户是否覆盖所有冲突文件
---
### Requirement: 系统必须记录命令组安装
工具必须在成功安装后将记录写入 install.json。
#### Scenario: 记录命令组安装信息
- **WHEN** 命令组安装成功
- **THEN** 系统在 install.json 中添加 type 为 "command"、包含命令组名称和安装路径的记录

View File

@@ -0,0 +1,124 @@
## ADDED Requirements
### Requirement: 系统必须持久化安装记录
工具必须在每次成功安装后,将安装信息写入 `~/.skillmgr/install.json`
#### Scenario: 创建新记录
- **WHEN** 首次安装某个 skill/command
- **THEN** 系统在 install.json 的 installations 数组中添加新记录
#### Scenario: 记录包含必要字段
- **WHEN** 创建安装记录
- **THEN** 记录必须包含 type、name、source_repo、platform、scope、install_path、installed_at、updated_at
---
### Requirement: 用户可以查询已安装项
工具必须提供命令列出所有已安装的 skills 和 commands。
#### Scenario: 列出所有已安装项
- **WHEN** 用户执行 `skillmgr list` 命令
- **THEN** 系统显示所有 install.json 中的记录
#### Scenario: 按类型过滤
- **WHEN** 用户执行 `skillmgr list --type skill``--type command`
- **THEN** 系统仅显示对应类型的安装记录
#### Scenario: 按平台过滤
- **WHEN** 用户执行 `skillmgr list --platform claude``--platform opencode`
- **THEN** 系统仅显示对应平台的安装记录
#### Scenario: 按作用域过滤
- **WHEN** 用户执行 `skillmgr list --global` 或省略该参数
- **THEN** 系统仅显示对应作用域的安装记录
#### Scenario: 无已安装项
- **WHEN** install.json 为空或不存在
- **THEN** 系统提示"无已安装的 skills/commands"
---
### Requirement: 用户可以卸载已安装项
工具必须提供卸载功能,删除文件并移除安装记录。
#### Scenario: 卸载 skill
- **WHEN** 用户执行 `skillmgr uninstall skill <name> --platform <platform> --global`
- **THEN** 系统从 install.json 查找记录,删除对应目录,移除记录
#### Scenario: 卸载 command
- **WHEN** 用户执行 `skillmgr uninstall command <group> --platform <platform> --global`
- **THEN** 系统删除对应的命令文件或目录,移除记录
#### Scenario: 卸载不存在的项
- **WHEN** install.json 中无对应记录
- **THEN** 系统提示"未找到安装记录",不执行删除
#### Scenario: 安装路径已被手动删除
- **WHEN** install.json 有记录但文件已不存在
- **THEN** 系统仅移除记录,不报错
---
### Requirement: 用户可以更新已安装项
工具必须提供更新功能,重新从源仓库安装最新版本。
#### Scenario: 更新单个 skill
- **WHEN** 用户执行 `skillmgr update skill <name> --platform <platform> --global`
- **THEN** 系统从源重新安装到原路径,更新 updated_at 字段
#### Scenario: 更新单个 command
- **WHEN** 用户执行 `skillmgr update command <group> --platform <platform> --global`
- **THEN** 系统从源重新安装到原路径,更新 updated_at 字段
#### Scenario: 更新所有已安装项
- **WHEN** 用户执行 `skillmgr update --all`
- **THEN** 系统遍历 install.json 中所有记录,逐个更新
#### Scenario: 源仓库找不到原始项
- **WHEN** 更新时源仓库中不再存在该 skill/command
- **THEN** 系统报错,不修改已安装文件和记录
---
### Requirement: 用户可以清理孤立记录
工具必须提供命令扫描并清理 install.json 中文件路径已不存在的记录。
#### Scenario: 扫描孤立记录
- **WHEN** 用户执行 `skillmgr clean` 命令
- **THEN** 系统遍历 install.json 中所有记录,检查 install_path 是否存在
#### Scenario: 清理孤立记录
- **WHEN** 发现安装路径不存在的记录
- **THEN** 系统列出这些记录并从 install.json 中删除
#### Scenario: 无孤立记录
- **WHEN** 所有记录的安装路径都存在
- **THEN** 系统提示"无孤立记录"
#### Scenario: 显示清理结果
- **WHEN** 清理完成
- **THEN** 系统显示清理的记录数量和详情type、name、platform、scope、路径

View File

@@ -0,0 +1,104 @@
## ADDED Requirements
### Requirement: 系统必须支持 Claude Code 平台
工具必须内置 Claude Code 平台的目录结构和命名规则。
#### Scenario: Skill 安装路径
- **WHEN** 安装 skill 到 Claude Code
- **THEN** 目标路径为 `<base>/.claude/skills/<skill-name>/`
#### Scenario: Command 安装路径
- **WHEN** 安装 command 到 Claude Code
- **THEN** 目标路径为 `<base>/.claude/commands/<command-group>/`
#### Scenario: 保持源目录结构
- **WHEN** 复制文件到 Claude Code
- **THEN** 系统保持源仓库的目录结构不变
---
### Requirement: 系统必须支持 OpenCode 平台
工具必须内置 OpenCode 平台的目录结构和命名规则。
#### Scenario: Skill 全局安装路径
- **WHEN** 全局安装 skill 到 OpenCode
- **THEN** 目标路径为 `~/.config/opencode/skills/<skill-name>/`
#### Scenario: Skill 项目级安装路径
- **WHEN** 项目级安装 skill 到 OpenCode
- **THEN** 目标路径为 `./.opencode/skills/<skill-name>/`
#### Scenario: Command 全局安装路径
- **WHEN** 全局安装 command 到 OpenCode
- **THEN** 目标路径为 `~/.config/opencode/commands/`
#### Scenario: Command 项目级安装路径
- **WHEN** 项目级安装 command 到 OpenCode
- **THEN** 目标路径为 `./.opencode/commands/`
#### Scenario: Skill 保持结构
- **WHEN** 复制 skill 到 OpenCode
- **THEN** 系统保持源目录结构
#### Scenario: Command 扁平化
- **WHEN** 复制 command 到 OpenCode
- **THEN** 系统将文件重命名为 `<group>-<action>.md` 并放置在 commands/ 目录下
---
### Requirement: 系统必须根据作用域确定基础路径
工具必须根据全局/项目作用域计算正确的基础路径。
#### Scenario: 全局作用域
- **WHEN** 用户指定 `--global` 参数
- **THEN** 基础路径为用户主目录(`~``$HOME`
#### Scenario: 项目作用域
- **WHEN** 用户未指定 `--global` 参数
- **THEN** 基础路径为当前工作目录
---
### Requirement: 系统必须生成文件映射表
适配器必须生成源文件到目标文件的完整映射表,供事务性安装使用。
#### Scenario: Skill 文件映射
- **WHEN** 适配 skill 文件
- **THEN** 系统返回源路径到目标路径的 map包括所有子目录和文件
#### Scenario: Command 文件映射Claude
- **WHEN** 适配 command 到 Claude Code
- **THEN** 系统返回 `commands/<group>/<action>.md``<base>/.claude/commands/<group>/<action>.md` 的映射
#### Scenario: Command 文件映射OpenCode
- **WHEN** 适配 command 到 OpenCode
- **THEN** 系统返回 `commands/<group>/<action>.md``<base>/commands/<group>-<action>.md` 的映射(全局时 base 为 `~/.config/opencode`,项目级时为 `./.opencode`
---
### Requirement: 系统必须处理不支持的平台
工具必须拒绝处理未实现的平台。
#### Scenario: 不支持的平台参数
- **WHEN** 用户指定未实现的平台(如 `--platform cursor`
- **THEN** 系统报错"unsupported platform: cursor"

View File

@@ -0,0 +1,78 @@
## ADDED Requirements
### Requirement: 用户可以添加源仓库
工具必须允许用户添加 git 仓库作为 skills/commands 的源,并将配置持久化到 `~/.skillmgr/repository.json`
#### Scenario: 成功添加新仓库
- **WHEN** 用户执行 `skillmgr add <git-url>` 命令
- **THEN** 系统克隆仓库到 `~/.skillmgr/cache/` 并将配置写入 repository.json
#### Scenario: 添加已存在的仓库
- **WHEN** 用户添加已存在的仓库(同名)
- **THEN** 系统提示"仓库名称已存在,请先使用 `skillmgr remove <name>` 移除",拒绝添加
#### Scenario: 指定仓库别名
- **WHEN** 用户使用 `--name` 参数指定仓库别名
- **THEN** 系统使用指定的别名作为仓库名称
#### Scenario: 指定分支
- **WHEN** 用户使用 `--branch` 参数指定分支
- **THEN** 系统克隆指定分支而非默认分支
---
### Requirement: 用户可以移除源仓库
工具必须允许用户从配置中移除已添加的源仓库。
#### Scenario: 成功移除仓库
- **WHEN** 用户执行 `skillmgr remove <name>` 命令
- **THEN** 系统从 repository.json 中删除对应配置
#### Scenario: 移除不存在的仓库
- **WHEN** 用户尝试移除不存在的仓库名称
- **THEN** 系统提示仓库不存在,不报错
---
### Requirement: 用户可以列出已配置的源仓库
工具必须提供命令列出所有已添加的源仓库及其信息。
#### Scenario: 列出所有仓库
- **WHEN** 用户执行 `skillmgr list-repos` 命令
- **THEN** 系统显示所有仓库的名称、URL、分支和添加时间
#### Scenario: 无已配置仓库
- **WHEN** 用户执行列表命令但 repository.json 为空
- **THEN** 系统提示"无已配置的源仓库"
---
### Requirement: 用户可以同步源仓库
工具必须提供命令从远程拉取最新代码,更新本地缓存。
#### Scenario: 同步单个仓库
- **WHEN** 用户执行 `skillmgr sync <name>` 命令
- **THEN** 系统对指定仓库执行 `git pull`
#### Scenario: 同步所有仓库
- **WHEN** 用户执行 `skillmgr sync` 不带参数
- **THEN** 系统对所有已配置仓库执行 `git pull`
#### Scenario: Git 操作失败
- **WHEN** git pull 失败(网络错误、冲突等)
- **THEN** 系统显示 git 错误信息并继续处理其他仓库

View File

@@ -0,0 +1,95 @@
## ADDED Requirements
### Requirement: 用户可以安装 skill 到全局目录
工具必须支持将 skill 安装到用户主目录下的平台配置目录(如 `~/.claude/skills/`)。
#### Scenario: 全局安装到 Claude Code
- **WHEN** 用户执行 `skillmgr install skill <name> --platform claude --global`
- **THEN** 系统将 skill 复制到 `~/.claude/skills/<name>/`
#### Scenario: 全局安装到 OpenCode
- **WHEN** 用户执行 `skillmgr install skill <name> --platform opencode --global`
- **THEN** 系统将 skill 复制到 `~/.config/opencode/skills/<name>/`
---
### Requirement: 用户可以安装 skill 到项目目录
工具必须支持将 skill 安装到当前项目目录下的平台配置目录。
#### Scenario: 项目级安装到 Claude Code
- **WHEN** 用户在项目目录执行 `skillmgr install skill <name> --platform claude`
- **THEN** 系统将 skill 复制到 `./claude/skills/<name>/`
#### Scenario: 项目级安装到 OpenCode
- **WHEN** 用户在项目目录执行 `skillmgr install skill <name> --platform opencode`
- **THEN** 系统将 skill 复制到 `./.opencode/skills/<name>/`
---
### Requirement: 系统必须在所有源仓库中查找 skill
工具必须在所有已配置的源仓库缓存中搜索指定的 skill。
#### Scenario: 在第一个仓库找到
- **WHEN** 第一个仓库包含目标 skill
- **THEN** 系统使用该仓库的 skill 进行安装
#### Scenario: 在后续仓库找到
- **WHEN** 前面的仓库不包含目标 skill但后续仓库包含
- **THEN** 系统使用找到的第一个匹配仓库
#### Scenario: 所有仓库都不包含
- **WHEN** 所有源仓库都不包含目标 skill
- **THEN** 系统报错"skill '<name>' not found in any repository"
---
### Requirement: 用户可以临时指定源仓库
工具必须支持通过 `--from` 参数临时指定源仓库 URL不保存到配置文件。
#### Scenario: 使用临时仓库安装
- **WHEN** 用户执行 `skillmgr install skill <name> --platform claude --global --from <git-url>`
- **THEN** 系统从指定 URL 拉取仓库并安装,不修改 repository.json
---
### Requirement: 系统必须处理目录已存在的情况
工具必须在安装前检查目标目录是否已存在,并根据情况处理。
#### Scenario: install.json 有记录且目录存在
- **WHEN** 目标 skill 已通过 skillmgr 安装
- **THEN** 系统询问用户是否覆盖,默认为否
#### Scenario: install.json 无记录但目录存在
- **WHEN** 目标目录存在但不在 install.json 中
- **THEN** 系统询问用户是否覆盖该目录,默认为否
#### Scenario: 用户拒绝覆盖
- **WHEN** 用户选择不覆盖
- **THEN** 系统取消安装,不修改任何文件
---
### Requirement: 系统必须记录安装操作
工具必须在成功安装后将记录写入 `~/.skillmgr/install.json`
#### Scenario: 记录包含完整信息
- **WHEN** 安装成功完成
- **THEN** 系统在 install.json 中添加包含 type、name、platform、scope、install_path、installed_at、updated_at 的记录

View File

@@ -0,0 +1,153 @@
## ADDED Requirements
### Requirement: 测试必须不污染用户环境
工具的所有测试必须通过环境变量隔离配置和安装目录,不影响用户的实际数据和系统配置。
#### Scenario: 配置目录隔离
- **WHEN** 测试运行时设置 `SKILLMGR_TEST_ROOT` 环境变量
- **THEN** 系统使用该环境变量指定的目录作为配置根目录,而非 `~/.skillmgr/`
#### Scenario: 安装目标目录隔离
- **WHEN** 测试运行时设置 `SKILLMGR_TEST_BASE` 环境变量
- **THEN** 系统使用该环境变量指定的目录作为全局/项目基础路径,而非用户主目录或当前工作目录
#### Scenario: 生产模式不受影响
- **WHEN** 环境变量未设置(生产模式)
- **THEN** 系统使用默认路径(`~/.skillmgr/``~/.claude/` 等)
---
### Requirement: 测试必须自动清理临时资源
所有测试创建的临时目录、文件和 git 仓库必须在测试结束后自动清理,不留垃圾文件。
#### Scenario: 使用 Go 测试框架自动清理
- **WHEN** 测试使用 `t.TempDir()` 创建临时目录
- **THEN** Go 测试框架在测试结束时自动删除该目录及其所有内容
#### Scenario: 测试失败时也清理
- **WHEN** 测试失败或 panic
- **THEN** 临时资源仍然被自动清理
---
### Requirement: 测试必须支持并行执行
测试设计必须允许多个测试并行运行,互不干扰,充分利用多核性能。
#### Scenario: 独立测试环境
- **WHEN** 使用 `go test -parallel N` 并行运行多个测试
- **THEN** 每个测试使用独立的临时目录,不产生竞态条件
#### Scenario: 配置隔离
- **WHEN** 多个测试同时设置环境变量
- **THEN** 每个测试的环境变量设置独立生效(通过 t.Setenv 或 defer os.Unsetenv
---
### Requirement: 用户交互必须可 mock
所有涉及用户输入的功能必须支持测试时注入 mock 输入,不依赖真实的标准输入。
#### Scenario: Mock 用户确认输入
- **WHEN** 测试需要模拟用户输入 "y" 或 "n"
- **THEN** `prompt.ConfirmWithReader` 函数接受 `io.Reader` 参数,测试时传入 `strings.NewReader("y\n")`
#### Scenario: 生产模式使用真实输入
- **WHEN** 生产代码调用 `prompt.Confirm`
- **THEN** 内部调用 `ConfirmWithReader(message, os.Stdin)` 读取真实用户输入
---
### Requirement: 测试必须使用真实文件系统
测试应使用真实的文件系统操作和 git 命令,而非 mock以确保行为与生产一致。
#### Scenario: 真实文件复制测试
- **WHEN** 测试文件复制功能
- **THEN** 在临时目录中创建真实文件,执行复制,验证结果
#### Scenario: 真实 git 操作测试
- **WHEN** 测试 git clone/pull 功能
- **THEN** 在临时目录中初始化真实的 git 仓库,执行 git 命令
---
### Requirement: 测试数据必须可复用
测试应提供标准的 fixture 数据和辅助函数,避免每个测试重复构建测试环境。
#### Scenario: Fixture 仓库
- **WHEN** 测试需要一个标准的 skills/commands 仓库
- **THEN** 从 `testdata/fixtures/test-repo/` 复制 fixture 并初始化为 git 仓库
#### Scenario: 测试辅助函数
- **WHEN** 测试需要设置隔离环境
- **THEN** 调用 `setupTestEnv(t)` 函数自动设置环境变量和临时目录
---
### Requirement: 测试覆盖必须全面
测试必须覆盖所有核心模块、关键路径和边界条件。
#### Scenario: 单元测试覆盖
- **WHEN** 实现任何核心函数config、adapter、repo、installer
- **THEN** 必须编写对应的单元测试,覆盖正常和异常情况
#### Scenario: 集成测试覆盖
- **WHEN** 实现跨模块功能(完整安装流程)
- **THEN** 必须编写集成测试,验证端到端行为
#### Scenario: 平台兼容性测试
- **WHEN** 支持多个平台Claude Code、OpenCode
- **THEN** 每个平台都必须有独立的兼容性测试
---
### Requirement: 测试脚本必须简化运行
必须提供自动化脚本,简化测试环境设置和测试执行。
#### Scenario: 自动化测试脚本
- **WHEN** 开发者运行 `./scripts/test.sh`
- **THEN** 脚本自动设置测试环境变量、运行所有测试、清理临时资源
#### Scenario: 沙盒测试环境
- **WHEN** 开发者运行 `./scripts/sandbox.sh`
- **THEN** 脚本创建隔离的沙盒环境,允许手动测试而不影响系统
---
### Requirement: CI/CD 集成必须无缝
测试必须能在 CI/CD 环境中稳定运行,不依赖本地环境配置。
#### Scenario: GitHub Actions 集成
- **WHEN** 在 CI 中运行测试
- **THEN** 通过环境变量设置测试路径,所有测试通过且自动清理
#### Scenario: 测试失败报告
- **WHEN** 测试失败
- **THEN** CI 系统捕获失败信息、日志和覆盖率报告

View File

@@ -0,0 +1,126 @@
## ADDED Requirements
### Requirement: 系统必须使用临时目录进行 staging
工具必须在系统临时目录中创建 staging 区域,组装完整的目标文件树后再移动到最终位置。
#### Scenario: 创建 staging 目录
- **WHEN** 开始安装事务
- **THEN** 系统在 `/tmp/``os.TempDir()` 创建唯一的临时目录(如 `skillmgr-xxxxx/`
#### Scenario: Staging 目录结构与目标一致
- **WHEN** 在 staging 中组装文件
- **THEN** staging 目录结构必须与最终目标目录结构完全一致
---
### Requirement: 系统必须先完整复制到 staging
工具必须将所有源文件完整复制到 staging 目录,应用平台适配规则。
#### Scenario: 复制所有文件到 staging
- **WHEN** 执行 Stage 阶段
- **THEN** 系统根据平台适配器返回的文件映射,将所有文件复制到 staging
#### Scenario: 复制失败回滚
- **WHEN** 复制过程中任何文件失败
- **THEN** 系统删除 staging 目录,报错终止,不影响目标目录
---
### Requirement: 系统必须验证 staging 完整性
工具必须在提交前验证 staging 目录中的文件完整性。
#### Scenario: 验证文件数量
- **WHEN** 所有文件复制到 staging
- **THEN** 系统验证 staging 中文件数量与预期映射表一致
---
### Requirement: 系统必须原子性提交 staging
工具必须在 staging 验证通过后,将整个 staging 目录移动到目标位置。
#### Scenario: 同文件系统移动
- **WHEN** staging 和目标在同一文件系统
- **THEN** 系统使用 `os.Rename()` 原子性移动
#### Scenario: 跨文件系统复制
- **WHEN** `os.Rename()` 失败(跨文件系统)
- **THEN** 系统 fallback 到递归复制 + 删除 staging
#### Scenario: 覆盖已存在目录
- **WHEN** 目标目录已存在(用户已确认覆盖)
- **THEN** 系统先删除目标目录,再移动 staging
---
### Requirement: 系统必须在失败时自动回滚
工具必须在任何阶段失败时,自动清理 staging 目录,不留垃圾文件。
#### Scenario: Stage 阶段失败
- **WHEN** 文件复制到 staging 失败
- **THEN** 系统删除 staging 目录,不修改目标
#### Scenario: Commit 阶段失败
- **WHEN** 移动 staging 到目标失败
- **THEN** 系统删除 staging 目录,目标目录保持原状(或已删除的状态)
#### Scenario: defer 确保清理
- **WHEN** 事务对象被销毁
- **THEN** 系统使用 defer 确保 staging 目录被清理
---
### Requirement: 系统必须提供事务接口
工具必须提供清晰的事务接口,包含 Stage、Commit、Rollback 方法。
#### Scenario: 创建事务对象
- **WHEN** 开始安装流程
- **THEN** 系统创建 Transaction 对象,包含 stagingDir、targetDir、fileMap 字段
#### Scenario: 调用 Stage 方法
- **WHEN** 调用 `transaction.Stage()`
- **THEN** 系统执行文件复制到 staging
#### Scenario: 调用 Commit 方法
- **WHEN** 调用 `transaction.Commit()`
- **THEN** 系统将 staging 移动到目标
#### Scenario: 调用 Rollback 方法
- **WHEN** 调用 `transaction.Rollback()`
- **THEN** 系统删除 staging 目录
---
### Requirement: 系统必须确保目标目录的父目录存在
工具必须在提交前确保目标目录的父目录已创建。
#### Scenario: 创建父目录
- **WHEN** 提交 staging 到目标
- **THEN** 系统先创建目标目录的父目录(如 `~/.claude/skills/`
#### Scenario: 父目录创建失败
- **WHEN** 父目录创建失败(权限不足等)
- **THEN** 系统报错,回滚 staging

View File

@@ -0,0 +1,191 @@
## 1. 项目初始化
- [x] 1.1 在 `manager/` 目录创建 Go 项目,初始化 go.modmodule: skillmgr
- [x] 1.2 创建项目目录结构cmd/、internal/、pkg/、testdata/
- [x] 1.3 添加 Cobra 依赖github.com/spf13/cobra
- [x] 1.4 创建 .gitignore 文件
- [x] 1.5 创建测试脚本目录scripts/
## 2. 核心类型定义
- [x] 2.1 创建 `internal/types/types.go`,定义 Platform、ItemType、Scope 枚举类型
- [x] 2.2 定义 Repository 结构体name、url、branch、added_at
- [x] 2.3 定义 RepositoryConfig 结构体repositories 数组)
- [x] 2.4 定义 InstallRecord 结构体type、name、source_repo、platform、scope、install_path、installed_at、updated_at
- [x] 2.5 定义 InstallConfig 结构体installations 数组)
- [x] 2.6 定义 SkillMetadata 和 CommandGroup 结构体
## 3. 配置管理模块
- [x] 3.1 创建 `internal/config/paths.go`实现配置目录路径函数GetConfigRoot、GetRepositoryConfigPath、GetInstallConfigPath、GetCachePath
- [x] 3.2 在 GetConfigRoot 中添加环境变量 SKILLMGR_TEST_ROOT 支持(测试隔离)
- [x] 3.3 实现 EnsureConfigDirs 函数,确保配置目录存在
- [x] 3.4 创建 `internal/config/repository.go`,实现 LoadRepositoryConfig 和 SaveRepositoryConfig
- [x] 3.5 实现 AddRepository 函数(检查同名仓库,存在则拒绝并提示先移除)
- [x] 3.6 实现 RemoveRepository 和 FindRepository 函数
- [x] 3.7 创建 `internal/config/install.go`,实现 LoadInstallConfig 和 SaveInstallConfig
- [x] 3.8 实现 AddInstallRecord、RemoveInstallRecord、FindInstallRecord、UpdateInstallRecord 函数
- [x] 3.9 实现 CleanOrphanRecords 函数(扫描并清理安装路径不存在的记录)
## 4. 文件工具模块
- [x] 4.1 创建 `pkg/fileutil/fileutil.go`
- [x] 4.2 实现 CopyFile 函数(复制单个文件并保留权限)
- [x] 4.3 实现 CopyDir 函数(递归复制目录)
## 5. Git 仓库管理模块
- [x] 5.1 创建 `internal/repo/git.go`
- [x] 5.2 实现 URLToPathName 函数(将 git URL 转换为缓存目录名)
- [x] 5.3 实现 CloneOrPull 函数(检查仓库是否存在,不存在则 clone存在则 pull
- [x] 5.4 实现 cloneRepo 函数(执行 git clone --depth 1
- [x] 5.5 实现 pullRepo 函数(执行 git pull
- [x] 5.6 创建 `internal/repo/scanner.go`
- [x] 5.7 实现 ScanSkills 函数(扫描 skills/ 目录,返回 skill 列表)
- [x] 5.8 实现 ScanCommands 函数(扫描 commands/ 目录,返回命令组列表)
- [x] 5.9 实现 FindSkill 函数(在所有仓库缓存中查找指定 skill
- [x] 5.10 实现 FindCommand 函数(在所有仓库缓存中查找指定命令组)
## 6. 平台适配器模块
- [x] 6.1 创建 `internal/adapter/adapter.go`,定义 PlatformAdapter 接口
- [x] 6.2 实现 GetAdapter 函数(根据 Platform 返回对应适配器,不支持则报错)
- [x] 6.3 实现 getBasePath 辅助函数(根据 Scope 返回基础路径,支持 SKILLMGR_TEST_BASE 环境变量)
- [x] 6.4 创建 `internal/adapter/claude.go`,实现 ClaudeAdapter 结构体
- [x] 6.5 实现 Claude 的 GetSkillInstallPath 和 GetCommandInstallPath 方法
- [x] 6.6 实现 Claude 的 AdaptSkill 方法(遍历源目录,生成文件映射)
- [x] 6.7 实现 Claude 的 AdaptCommand 方法(保持目录结构)
- [x] 6.8 创建 `internal/adapter/opencode.go`,实现 OpenCodeAdapter 结构体
- [x] 6.9 实现 OpenCode 的 GetSkillInstallPath注意 command 是单数)和 GetCommandInstallPath 方法
- [x] 6.10 实现 OpenCode 的 AdaptSkill 方法(与 Claude 相同)
- [x] 6.11 实现 OpenCode 的 AdaptCommand 方法(扁平化文件名:<group>-<action>.md
## 7. 事务性安装模块
- [x] 7.1 创建 `internal/installer/transaction.go`
- [x] 7.2 定义 Transaction 结构体stagingDir、targetDir、fileMap
- [x] 7.3 实现 NewTransaction 函数(在系统临时目录创建 staging
- [x] 7.4 实现 Stage 方法(复制所有文件到 staging创建必要的子目录
- [x] 7.5 实现 Commit 方法(确保父目录存在,删除已存在的目标,移动 staging 到目标)
- [x] 7.6 处理 Commit 中的跨文件系统情况Rename 失败则 fallback 到 CopyDir
- [x] 7.7 实现 Rollback 方法(删除 staging 目录)
- [x] 7.8 在 NewTransaction 中使用 defer 确保清理
## 8. 用户交互模块
- [x] 8.1 创建 `internal/prompt/prompt.go`
- [x] 8.2 实现 ConfirmWithReader 函数(接受 io.Reader支持测试 mock
- [x] 8.3 实现 Confirm 函数(调用 ConfirmWithReader使用 os.Stdin
## 9. 安装器模块
- [x] 9.1 创建 `internal/installer/installer.go`
- [x] 9.2 实现 checkExistingInstallation 函数(检查 install.json 记录和目录存在性,询问用户是否覆盖)
- [x] 9.3 实现 InstallSkill 函数(查找 skill、获取适配器、确定路径、检查冲突、适配、事务安装、记录
- [x] 9.4 实现 InstallCommand 函数(查找 command、获取适配器、确定路径、检查冲突、适配、事务安装、记录
- [x] 9.5 创建 `internal/installer/uninstaller.go`
- [x] 9.6 实现 UninstallSkill 函数(查找记录、删除目录、移除记录)
- [x] 9.7 实现 UninstallCommand 函数(查找记录、删除目录或文件、移除记录)
## 10. CLI 根命令
- [x] 10.1 创建 `cmd/skillmgr/root.go`
- [x] 10.2 定义 rootCmd设置 Use、Short、Long
- [x] 10.3 实现 Execute 函数
- [x] 10.4 在 init 中调用 config.EnsureConfigDirs 初始化配置目录
- [x] 10.5 创建 `cmd/skillmgr/main.go`,调用 Execute
## 11. 仓库管理命令
- [x] 11.1 创建 `cmd/skillmgr/add.go`,实现 addCmd
- [x] 11.2 添加 --name 和 --branch 参数
- [x] 11.3 实现 RunE解析参数、调用 repo.CloneOrPull、调用 config.AddRepository、显示成功信息
- [x] 11.4 创建 `cmd/skillmgr/remove.go`,实现 removeCmd
- [x] 11.5 实现 RunE调用 config.RemoveRepository
- [x] 11.6 创建 `cmd/skillmgr/list_repos.go`,实现 listReposCmd
- [x] 11.7 实现 RunE调用 config.LoadRepositoryConfig、格式化输出
- [x] 11.8 创建 `cmd/skillmgr/sync.go`,实现 syncCmd
- [x] 11.9 实现 RunE支持指定仓库名或同步所有调用 repo.CloneOrPull
## 12. 安装命令
- [x] 12.1 创建 `cmd/skillmgr/install.go`,实现 installCmd
- [x] 12.2 添加 --platform必需、--global、--from 参数
- [x] 12.3 实现 Args 验证(必须有 2 个参数type 和 name
- [x] 12.4 实现 RunE解析 typeskill/command、调用对应安装函数
- [x] 12.5 处理 --from 参数TODO临时仓库暂时跳过实现
## 13. 追踪管理命令
- [x] 13.1 创建 `cmd/skillmgr/list.go`,实现 listCmd
- [x] 13.2 添加 --type、--platform、--global 参数
- [x] 13.3 实现 RunE加载 install.json、根据参数过滤、格式化输出
- [x] 13.4 创建 `cmd/skillmgr/uninstall.go`,实现 uninstallCmd
- [x] 13.5 添加 --platform必需、--global 参数
- [x] 13.6 实现 Args 验证和 RunE调用对应卸载函数
- [x] 13.7 创建 `cmd/skillmgr/update.go`,实现 updateCmd
- [x] 13.8 添加 --platform、--global、--all 参数
- [x] 13.9 实现 RunE支持更新单个或全部已安装项
- [x] 13.10 创建 `cmd/skillmgr/clean.go`,实现 cleanCmd
- [x] 13.11 实现 RunE调用 config.CleanOrphanRecords、显示清理结果
## 14. 搜索命令(可选)
- [x] 14.1 创建 `cmd/skillmgr/search.go`,实现 searchCmd
- [x] 14.2 实现 RunE遍历所有仓库缓存、扫描 skills 和 commands、匹配关键词、输出结果
## 15. 错误处理和用户体验优化
- [x] 15.1 确保所有 Git 操作失败时显示清晰错误信息
- [x] 15.2 安装/卸载成功时显示 ✓ 符号和路径信息
- [x] 15.3 配置文件解析错误时提示用户检查 JSON 格式
- [x] 15.4 未找到 skill/command 时列出可用项
## 16. 测试基础设施
- [x] 16.1 创建 `testdata/fixtures/` 目录
- [x] 16.2 创建测试用 fixture 仓库test-repo包含 2 个 skills 和 1 个 command 组)
- [x] 16.3 编写测试辅助函数 setupTestRepo初始化临时 git 仓库)
- [x] 16.4 编写测试辅助函数 copyFixtureRepo复制 fixture 并初始化 git
- [x] 16.5 创建 `scripts/test.sh`(设置测试环境变量并运行测试)
- [x] 16.6 创建 `scripts/sandbox.sh`(手动测试沙盒环境)
## 17. 单元测试
- [x] 17.1 创建 `internal/config/paths_test.go`,测试环境变量覆盖
- [x] 17.2 创建 `internal/config/repository_test.go`,测试仓库配置增删改查
- [x] 17.3 测试 AddRepository 拒绝同名仓库场景
- [x] 17.4 创建 `internal/config/install_test.go`,测试安装记录管理
- [x] 17.5 测试 CleanOrphanRecords 功能
- [x] 17.6 创建 `internal/adapter/claude_test.go`,测试路径计算和文件映射
- [x] 17.7 创建 `internal/adapter/opencode_test.go`,测试扁平化命名转换
- [x] 17.8 创建 `internal/repo/git_test.go`,测试 URL 转换
- [x] 17.9 创建 `internal/installer/transaction_test.go`,测试 Stage/Commit/Rollback
- [x] 17.10 创建 `internal/prompt/prompt_test.go`,测试用户输入 mock
- [x] 17.11 创建 `pkg/fileutil/fileutil_test.go`,测试文件复制
## 18. 集成测试
- [x] 18.1 创建 `internal/installer/installer_test.go`
- [x] 18.2 测试完整安装流程add repo → install skill → 验证文件和记录)
- [x] 18.3 测试冲突覆盖场景(已安装 → 再次安装 → 用户确认)
- [x] 18.4 测试事务回滚Stage 失败 → 验证 staging 清理)
- [x] 18.5 测试卸载流程install → uninstall → 验证删除)
- [x] 18.6 测试更新流程install → update → 验证更新)
- [x] 18.7 测试清理孤立记录install → 手动删除 → clean
- [x] 18.8 测试 Claude Code 平台安装skill 和 command
- [x] 18.9 测试 OpenCode 平台安装skill 和 command 扁平化)
## 19. 构建和手动验证
- [x] 19.1 编写 Makefile 或构建脚本,支持 `go build -o skillmgr`
- [x] 19.2 在沙盒环境手动测试基本流程
- [x] 19.3 验证全局和项目级安装
- [x] 19.4 验证两个平台的适配正确性
## 20. 文档
- [x] 20.1 编写 README.md包含安装说明、使用示例、命令参考
- [x] 20.2 记录配置文件格式和路径
- [x] 20.3 添加常见问题和故障排除指南
- [x] 20.4 添加测试说明(如何运行测试、测试环境变量)

View File

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

View File

@@ -0,0 +1,76 @@
## Context
当前项目包含 4 个 skillslyxy-runner-python、lyxy-runner-js、lyxy-reader-office、lyxy-kb每个 SKILL.md 文件内容在 200-300 行之间。根据 `document/the-complete-guide-to-building-skill.md` 中的渐进式披露原则SKILL.md 应保持精简(建议 5000 字以内),详细文档应放在 `references/` 目录中。
**当前状态**
- SKILL.md 包含完整的工作流程、示例、错误处理、最佳实践等所有内容
- 无 references/ 目录结构
- 每次 Claude 加载 skill 时需要处理全部内容
## Goals / Non-Goals
**Goals:**
- 遵循三层渐进式披露结构YAML 前置元数据 → SKILL.md 核心指令 → references/ 详细文档
- 减少 skill 加载时的 token 消耗
- 保持 lyxy-runner-python 和 lyxy-runner-js 的 description 宽泛性
- 为 4 个现有 skills 创建 references/ 目录并迁移内容
**Non-Goals:**
- 不修改 skill 的功能逻辑
- 不修改 scripts/ 目录中的可执行代码
- 不新增 skill 或删除现有 skill
## Decisions
### 决策 1三层结构内容划分
| 层级 | 位置 | 内容 | 加载时机 |
|-----|------|------|---------|
| 第一层 | YAML 前置元数据 | name、description、compatibility | 始终加载到系统提示 |
| 第二层 | SKILL.md 正文 | 核心工作流、关键命令、快速参考 | Claude 判断相关时加载 |
| 第三层 | references/*.md | 详细示例、错误处理、最佳实践、API 参考 | 按需导航和发现 |
**理由**:遵循官方指南的渐进式披露原则,最小化 token 消耗的同时保持完整能力。
### 决策 2SKILL.md 保留内容
每个 SKILL.md 正文保留以下核心部分:
1. **Purpose**:简要说明用途和关键依赖
2. **When to Use**:适用/不适用场景(简化版)
3. **Quick Reference**:核心命令表格或流程图
4. **Workflow**:简化的执行步骤
5. **References 链接**:指向 references/ 中详细文档的链接
**理由**:保留足够信息让 Claude 完成任务,详细内容按需加载。
### 决策 3references/ 目录结构
```
references/
├── examples.md # 详细示例
├── error-handling.md # 错误处理和故障排除
├── best-practices.md # 最佳实践和注意事项
└── api-reference.md # API/命令参数详细说明(可选)
```
**理由**:按主题分类,便于 Claude 按需定位和加载。
### 决策 4Description 格式
采用统一格式:`[做什么] + [何时使用] + [关键能力]`
- lyxy-runner-python保持 "Any task that requires Python processing should use this skill."(宽泛)
- lyxy-runner-js保持 "Any task that requires Javascript/Typescript processing should use this skill."(宽泛)
- lyxy-reader-office保持当前描述已包含具体文件类型和操作
- lyxy-kb可优化添加用户触发短语
**理由**runner skills 需要宽泛以匹配所有相关任务reader/kb skills 可以更具体以精准触发。
## Risks / Trade-offs
| 风险 | 缓解措施 |
|-----|---------|
| references/ 文件未被 Claude 发现 | 在 SKILL.md 中明确列出 references/ 文件并说明用途 |
| 拆分后信息不完整 | 核心工作流程保留在 SKILL.md确保基本任务可完成 |
| 迁移过程中遗漏内容 | 逐个 skill 处理,对比前后内容确保完整性 |
| 文件数量增加 | references/ 文件按需加载,不影响初始 token 消耗 |

View File

@@ -0,0 +1,30 @@
## Why
当前 skills 的 SKILL.md 文件内容较长200-300 行未遵循渐进式披露Progressive Disclosure原则。根据 skill 编写指南应采用三层结构YAML 前置元数据(触发判断)→ SKILL.md 正文(核心指令)→ references/(详细文档)。这样可以减少 token 消耗,提高 Claude 的响应效率。
## What Changes
- **结构优化**:为每个 skill 创建 `references/` 目录,将详细文档、示例、错误处理等内容从 SKILL.md 正文移至 references/
- **SKILL.md 精简**:保留核心工作流程和关键指令,将详细说明改为链接到 references/ 中的文档
- **Description 优化**:确保 description 同时包含「做什么」和「何时使用」两部分,便于 Claude 判断是否加载
- **lyxy-runner-python/js 保持宽泛**:这两个 runner skill 的 description 需要保持宽泛,以便匹配所有 Python/JS 处理任务
## Capabilities
### New Capabilities
- `skill-progressive-disclosure`: 定义 skill 渐进式披露的三层结构规范,包括 YAML 前置元数据、SKILL.md 正文、references/ 目录的职责划分和内容组织方式
### Modified Capabilities
(无现有 spec 需要修改)
## Impact
- **文件结构变更**:每个 skill 目录新增 `references/` 子目录
- **SKILL.md 重构**4 个 skills 的 SKILL.md 文件需要精简,详细内容迁移到 references/
- `skills/lyxy-runner-python/SKILL.md`
- `skills/lyxy-runner-js/SKILL.md`
- `skills/lyxy-reader-office/SKILL.md`
- `skills/lyxy-kb/SKILL.md`
- **向后兼容**:功能不变,仅优化文档结构,**无破坏性变更**

View File

@@ -0,0 +1,77 @@
## ADDED Requirements
### Requirement: 三层渐进式披露结构
每个 skill 目录 SHALL 采用三层渐进式披露结构按加载时机分为第一层YAML 前置元数据、第二层SKILL.md 正文、第三层references/ 目录)。
#### Scenario: skill 目录包含完整三层结构
- **WHEN** 检查任意 skill 目录结构
- **THEN** 目录 SHALL 包含 SKILL.md 文件和 references/ 子目录
#### Scenario: 第一层始终加载
- **WHEN** Claude 启动会话时
- **THEN** 所有 skill 的 YAML 前置元数据 SHALL 被加载到系统提示中
### Requirement: YAML 前置元数据内容规范
YAML 前置元数据 SHALL 仅包含触发判断所需的最小信息,使 Claude 能够判断何时使用该 skill。
#### Scenario: 必需字段
- **WHEN** 编写 SKILL.md 的 YAML 前置元数据
- **THEN** SHALL 包含 name 和 description 字段
#### Scenario: description 格式
- **WHEN** 编写 description 字段
- **THEN** SHALL 同时包含「做什么」和「何时使用」两部分信息
#### Scenario: description 长度限制
- **WHEN** 检查 description 字段
- **THEN** 内容 SHALL 少于 1024 个字符
#### Scenario: runner skill 的 description 宽泛性
- **WHEN** skill 用途为通用脚本执行(如 Python/JavaScript runner
- **THEN** description SHALL 保持宽泛,以匹配所有相关处理任务
### Requirement: SKILL.md 正文内容规范
SKILL.md 正文 SHALL 包含核心工作流程和关键指令,保持精简以减少 token 消耗。
#### Scenario: SKILL.md 必需章节
- **WHEN** 编写 SKILL.md 正文
- **THEN** SHALL 包含以下章节Purpose、When to Use、Quick Reference、Workflow、References 链接
#### Scenario: SKILL.md 长度建议
- **WHEN** 检查 SKILL.md 总字数
- **THEN** 建议控制在 5000 字以内(含 YAML 前置元数据)
#### Scenario: 详细内容迁移
- **WHEN** SKILL.md 包含详细示例、完整错误处理、最佳实践等冗长内容
- **THEN** 这些内容 SHALL 迁移到 references/ 目录SKILL.md 中保留链接引用
### Requirement: references 目录结构规范
每个 skill SHALL 包含 references/ 子目录,用于存放按需加载的详细文档。
#### Scenario: references 目录存在性
- **WHEN** 检查 skill 目录结构
- **THEN** SHALL 存在 references/ 子目录
#### Scenario: references 文件分类
- **WHEN** 组织 references/ 目录内容
- **THEN** SHALL 按主题分类为独立的 markdown 文件,如 examples.md、error-handling.md、best-practices.md
#### Scenario: SKILL.md 引用 references
- **WHEN** SKILL.md 需要引用详细文档
- **THEN** SHALL 在 SKILL.md 中明确列出 references/ 中的文件并说明用途
### Requirement: 内容迁移完整性
从 SKILL.md 迁移内容到 references/ 时SHALL 确保所有原有信息被完整保留。
#### Scenario: 迁移前后对比
- **WHEN** 完成单个 skill 的内容迁移
- **THEN** references/ 中的文件 SHALL 包含原 SKILL.md 中被移除的所有详细内容
#### Scenario: 功能不变性
- **WHEN** 完成结构优化后
- **THEN** skill 的功能逻辑 SHALL 保持不变,仅文档结构发生变化

View File

@@ -0,0 +1,45 @@
## 1. lyxy-runner-python 结构优化
- [x] 1.1 创建 `skills/lyxy-runner-python/references/` 目录
- [x] 1.2 提取示例代码到 `references/examples.md`
- [x] 1.3 提取错误处理内容到 `references/error-handling.md`
- [x] 1.4 提取最佳实践和注意事项到 `references/best-practices.md`
- [x] 1.5 精简 SKILL.md保留 Purpose、When to Use、Quick Reference、Workflow 章节
- [x] 1.6 在 SKILL.md 末尾添加 References 链接章节
- [x] 1.7 验证 description 保持宽泛("Any task that requires Python processing..."
## 2. lyxy-runner-js 结构优化
- [x] 2.1 创建 `skills/lyxy-runner-js/references/` 目录
- [x] 2.2 提取示例代码到 `references/examples.md`
- [x] 2.3 提取错误处理内容到 `references/error-handling.md`
- [x] 2.4 提取最佳实践和输出处理到 `references/best-practices.md`
- [x] 2.5 精简 SKILL.md保留 Purpose、When to Use、Quick Reference、Workflow 章节
- [x] 2.6 在 SKILL.md 末尾添加 References 链接章节
- [x] 2.7 验证 description 保持宽泛("Any task that requires Javascript/Typescript processing..."
## 3. lyxy-reader-office 结构优化
- [x] 3.1 创建 `skills/lyxy-reader-office/references/` 目录
- [x] 3.2 提取详细示例到 `references/examples.md`
- [x] 3.3 提取解析器说明和依赖安装到 `references/parsers.md`
- [x] 3.4 提取错误处理和限制说明到 `references/error-handling.md`
- [x] 3.5 精简 SKILL.md保留 Purpose、When to Use、Quick Reference、Workflow 章节
- [x] 3.6 在 SKILL.md 末尾添加 References 链接章节
## 4. lyxy-kb 结构优化
- [x] 4.1 创建 `skills/lyxy-kb/references/` 目录
- [x] 4.2 提取目录结构和文件格式详细说明到 `references/structure.md`
- [x] 4.3 提取文档生命周期和处理策略到 `references/workflow.md`
- [x] 4.4 提取渐进式查询策略到 `references/query-strategy.md`
- [x] 4.5 精简 SKILL.md保留 Purpose、When to Use、Quick Reference、Workflow 章节
- [x] 4.6 在 SKILL.md 末尾添加 References 链接章节
- [x] 4.7 优化 description 添加用户触发短语
## 5. 验证与收尾
- [x] 5.1 对比每个 skill 的原始 SKILL.md 和重构后内容,确保信息完整
- [x] 5.2 验证每个 skill 的 references/ 目录包含必要文件
- [x] 5.3 验证每个 SKILL.md 的 References 章节正确链接到 references/ 文件
- [x] 5.4 验证所有 description 符合格式要求(<1024 字符)

View File

@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-03-07

View File

@@ -0,0 +1,124 @@
## Context
当前项目已有 `lyxy-reader-office` skill 用于解析办公文档,其架构设计成熟,包含统一命令行入口、多解析器回退机制、多功能查询等特性。现在需要创建 `lyxy-reader-html` skill功能上类似但针对 HTML 内容,同时需支持 URL 下载能力。
**约束条件**
-`lyxy-reader-office` 保持相同的用户体验(参数、输出格式)
- 代码完全独立,不复用 `lyxy-reader-office` 的代码
- 参考 `temp/downloader/download.py``temp/parser/parse.py` 的现有实现
## Goals / Non-Goals
**Goals:**
- 创建完整的 `lyxy-reader-html` skill 目录结构
- 实现统一的命令行入口 `parser.py`,支持 URL 和 HTML 文件输入
- 实现下载器模块,按 pyppeteer → selenium → httpx → urllib 优先级回退
- 实现解析器模块,按 trafilatura → domscribe → MarkItDown → html2text 优先级回退
- 实现 HTML 预处理清理和 Markdown 后处理
- 实现与 `lyxy-reader-office` 一致的查询功能(全文、字数、行数、标题、章节、搜索)
**Non-Goals:**
- 不支持可配置的 HTML 清理选项
- 不支持 JavaScript 渲染开关(默认启用完整下载链)
- 不支持正文/全文切换(默认使用解析器的正文提取)
## Decisions
### 1. 目录结构参考 lyxy-reader-office
**决策**:采用与 `lyxy-reader-office` 相同的目录结构
```
lyxy-reader-html/
├── SKILL.md
├── scripts/
│ ├── parser.py # 统一入口
│ ├── common.py # 公共函数
│ ├── downloader.py # URL 下载
│ ├── html_parser.py # HTML 解析
│ └── README.md
└── references/
├── examples.md
├── parsers.md
└── error-handling.md
```
**理由**:保持项目一致性,降低用户学习成本
---
### 2. 下载器优先级pyppeteer → selenium → httpx → urllib
**决策**:直接采用 `temp/downloader/download.py` 的优先级顺序
- pyppeteer支持 JS 渲染)
- selenium支持 JS 渲染,作为 pyppeteer 的备选)
- httpx轻量级 HTTP 客户端)
- urllib标准库兜底
**备选方案考虑**
- httpx → urllib → pyppeteer → selenium速度优先
- 选择保持原顺序,因为 JS 渲染能力对许多现代网页很重要
---
### 3. 解析器优先级trafilatura → domscribe → MarkItDown → html2text
**决策**:采用精简后的 4 个解析器,顺序参考 `temp/parser/parse.py`
1. trafilatura - 专门用于网页正文提取,质量高
2. domscribe - 专注内容提取
3. MarkItDown - 微软官方,格式规范
4. html2text - 经典库,作为兜底
**备选方案考虑**
- 保留原 6 个解析器(增加 markdownify 和 html-to-markdown
- 选择精简为 4 个,减少维护复杂度
---
### 4. HTML 预处理清理默认开启且不可配置
**决策**:解析前固定执行 HTML 清理,移除 script/style/link/svg 标签和 URL 属性
- 使用 `temp/clean_html.py` 的清理逻辑
- 不提供 `--no-clean` 参数
**理由**:简化设计,减少用户选择负担
---
### 5. Markdown 处理函数独立实现
**决策**:在 `common.py` 中独立实现以下函数,不复用 `lyxy-reader-office`
- `remove_markdown_images()` - 移除图片标记
- `normalize_markdown_whitespace()` - 规范化空行
- `extract_titles()` - 提取标题
- `extract_title_content()` - 提取章节内容
- `search_markdown()` - 正则搜索
**理由**:保持 skill 之间完全隔离
---
### 6. 命令行参数与 lyxy-reader-office 保持一致
**决策**:支持以下参数,与 `lyxy-reader-office` 完全一致:
- (无参数):全文输出
- `-c` / `--count`:字数统计
- `-l` / `--lines`:行数统计
- `-t` / `--titles`:提取标题
- `-tc <name>` / `--title-content <name>`:提取章节
- `-s <pattern>` / `--search <pattern>`:正则搜索
- `-n <num>` / `--context <num>`:搜索上下文行数
**不添加** HTML 专用参数
**理由**:统一用户体验
## Risks / Trade-offs
| 风险 | 影响 | 缓解措施 |
|------|------|----------|
| pyppeteer/selenium 依赖重,安装困难 | 中 | 提供 httpx/urllib 作为轻量备选 |
| trafilatura 可能提取不到正文 | 低 | 后续解析器会继续尝试 |
| 不同解析器输出质量差异大 | 中 | 用户可通过安装不同依赖来间接选择解析器 |
| URL 下载超时或被反爬 | 中 | 多下载器回退增加成功率 |
## Migration Plan
不适用 - 这是新 skill 创建,无迁移需求。
## Open Questions
无 - 所有决策已明确。

View File

@@ -0,0 +1,26 @@
## Why
当前已有 `lyxy-reader-office` skill 用于解析办公文档,但缺少对 HTML 网页内容的解析能力。用户需要从 URL 或本地 HTML 文件中提取内容并转换为 Markdown 格式,同时支持标题提取、内容搜索等查询功能。
## What Changes
- 创建新 skill `lyxy-reader-html`,目录结构参考 `lyxy-reader-office`
- 实现命令行工具 `scripts/parser.py`,接受 URL 或 HTML 文件作为输入
- URL 模式下按优先级尝试下载器pyppeteer → selenium → httpx → urllib
- HTML 解析按优先级尝试trafilatura → domscribe → MarkItDown → html2text
- 支持 HTML 预处理清理(移除 script/style/link 等标签和 URL 属性)
- 实现查询功能:全文输出、字数统计、行数统计、标题提取、章节提取、正则搜索
## Capabilities
### New Capabilities
- `html-document-parsing`: HTML 文档和 URL 内容解析能力,将 HTML 转换为 Markdown 并支持多种查询模式
### Modified Capabilities
(无)
## Impact
- 新增目录 `skills/lyxy-reader-html/`
- 新增 Python 脚本依赖trafilatura、domscribe、markitdown、html2text、httpx、pyppeteer、selenium、beautifulsoup4
- 与现有 skill 完全隔离,不影响其他功能

View File

@@ -0,0 +1,137 @@
## ADDED Requirements
### Requirement: 优先使用 lyxy-reader-html 解析 HTML 内容
大模型在遇到 URL、.html 或 .htm 文件时SHALL 优先激活并使用 lyxy-reader-html skill 来读取内容。
#### Scenario: 用户请求读取 URL
- **WHEN** 用户提供以 http:// 或 https:// 开头的 URL
- **THEN** 大模型 SHALL 激活 lyxy-reader-html skill
- **AND** 使用 skill 目录下的 `scripts/parser.py` 执行解析
#### Scenario: 用户请求读取 HTML 文件
- **WHEN** 用户提供的文件路径以 .html 或 .htm 结尾
- **THEN** 大模型 SHALL 激活 lyxy-reader-html skill
- **AND** 使用 skill 目录下的 `scripts/parser.py` 执行解析
### 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-html skill 或不确定具体参数用法
- **THEN** 大模型 SHALL 阅读 `scripts/README.md` 获取命令行参数、依赖安装和使用示例等详细信息
#### Scenario: 遇到特殊参数需求
- **WHEN** 用户请求使用特殊功能(如章节提取、正则搜索等)
- **THEN** 大模型 SHALL 参考 `scripts/README.md` 中的对应参数说明
### Requirement: 支持 URL 和 HTML 文件两种输入源
系统 SHALL 支持从 URL 下载 HTML 内容或直接读取本地 HTML 文件两种输入方式。
#### Scenario: 输入为 URL
- **WHEN** 输入以 http:// 或 https:// 开头
- **THEN** 系统 SHALL 识别为 URL 模式
- **AND** 尝试下载 URL 对应的 HTML 内容
#### Scenario: 输入为本地 HTML 文件
- **WHEN** 输入是存在的本地文件且扩展名为 .html 或 .htm
- **THEN** 系统 SHALL 识别为 HTML 文件模式
- **AND** 直接读取文件内容进行解析
#### Scenario: 输入无法识别
- **WHEN** 输入既不是 URL 也不是有效的 HTML 文件
- **THEN** 系统 SHALL 输出错误信息
- **AND** 以退出码 1 退出
### Requirement: URL 下载器按优先级降级
系统 SHALL 按 pyppeteer → selenium → httpx → urllib 的优先级尝试下载 URL 内容。
#### Scenario: 下载器按优先级降级
- **WHEN** 优先级最高的下载器不可用或下载失败
- **THEN** 系统自动尝试下一优先级的下载器
- **AND** 记录每个下载器的失败原因
#### Scenario: 所有下载器失败
- **WHEN** 所有下载策略均失败
- **THEN** 系统返回详细的失败信息
- **AND** 列出每种下载策略的失败原因
- **AND** 以退出码 1 退出
### Requirement: HTML 解析器按优先级降级
系统 SHALL 按 trafilatura → domscribe → MarkItDown → html2text 的优先级尝试解析 HTML 内容。
#### Scenario: 解析器按优先级降级
- **WHEN** 优先级最高的解析器不可用或解析失败
- **THEN** 系统自动尝试下一优先级的解析器
- **AND** 记录每个解析器的失败原因
#### Scenario: 所有解析器失败
- **WHEN** 所有解析策略均失败
- **THEN** 系统返回详细的失败信息
- **AND** 列出每种解析策略的失败原因
- **AND** 以退出码 1 退出
### Requirement: HTML 内容预处理清理
系统 SHALL 在解析前对 HTML 内容进行预处理清理,移除噪声元素。
#### Scenario: 清理 script 和 style 标签
- **WHEN** HTML 内容包含 &lt;script&gt;、&lt;style&gt;、&lt;link&gt;、&lt;svg&gt; 标签
- **THEN** 系统 SHALL 移除这些标签及其内容
#### Scenario: 清理 URL 属性
- **WHEN** HTML 标签包含 href、src、srcset、action、data-*src 等 URL 属性
- **THEN** 系统 SHALL 移除这些属性
#### Scenario: 清理 style 属性
- **WHEN** HTML 标签包含 style 属性
- **THEN** 系统 SHALL 移除 style 属性
### 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: Markdown 后处理
系统 SHALL 对解析后的 Markdown 内容进行后处理,提升可读性。
#### Scenario: 移除图片标记
- **WHEN** 解析结果包含 Markdown 图片语法 `![alt](url)`
- **THEN** 系统 SHALL 移除这些图片标记
#### Scenario: 规范化空行
- **WHEN** 解析结果包含连续 3 个或更多空行
- **THEN** 系统 SHALL 将其合并为单个空行

View File

@@ -0,0 +1,58 @@
## 1. 初始化 Skill 目录结构
- [x] 1.1 创建 `skills/lyxy-reader-html/` 目录
- [x] 1.2 创建 `skills/lyxy-reader-html/scripts/` 子目录
- [x] 1.3 创建 `skills/lyxy-reader-html/references/` 子目录
## 2. 创建 SKILL.md 主文档
- [x] 2.1 编写 YAML 前置元数据name、description、compatibility
- [x] 2.2 编写 Purpose 章节
- [x] 2.3 编写 When to Use 章节(含触发词)
- [x] 2.4 编写 Quick Reference 章节(参数表)
- [x] 2.5 编写 Workflow 章节
- [x] 2.6 编写 References 章节
## 3. 实现 common.py 公共模块
- [x] 3.1 实现 HTML 清理函数 `clean_html_content()`
- [x] 3.2 实现 Markdown 图片移除函数 `remove_markdown_images()`
- [x] 3.3 实现 Markdown 空行规范化函数 `normalize_markdown_whitespace()`
- [x] 3.4 实现标题级别检测函数 `get_heading_level()`
- [x] 3.5 实现标题提取函数 `extract_titles()`
- [x] 3.6 实现章节内容提取函数 `extract_title_content()`
- [x] 3.7 实现正则搜索函数 `search_markdown()`
## 4. 实现 downloader.py URL 下载模块
- [x] 4.1 实现 `download_with_pyppeteer()` 函数
- [x] 4.2 实现 `download_with_selenium()` 函数
- [x] 4.3 实现 `download_with_httpx()` 函数
- [x] 4.4 实现 `download_with_urllib()` 函数
- [x] 4.5 实现统一的 `download_html()` 入口函数,按优先级尝试各下载器
## 5. 实现 html_parser.py HTML 解析模块
- [x] 5.1 实现 `parse_with_trafilatura()` 函数
- [x] 5.2 实现 `parse_with_domscribe()` 函数
- [x] 5.3 实现 `parse_with_markitdown()` 函数
- [x] 5.4 实现 `parse_with_html2text()` 函数
- [x] 5.5 实现统一的 `parse_html()` 入口函数,按优先级尝试各解析器
## 6. 实现 parser.py 命令行入口
- [x] 6.1 实现命令行参数解析argparse
- [x] 6.2 实现输入源判断URL / HTML 文件)
- [x] 6.3 实现 URL 下载流程(如需要)
- [x] 6.4 实现 HTML 清理流程
- [x] 6.5 实现 HTML 解析流程
- [x] 6.6 实现 Markdown 后处理(移除图片、规范化空行)
- [x] 6.7 实现各查询模式(全文、字数、行数、标题、章节、搜索)
- [x] 6.8 实现错误处理和退出码
## 7. 创建参考文档
- [x] 7.1 创建 `scripts/README.md` 详细使用文档
- [x] 7.2 创建 `references/examples.md` 使用示例
- [x] 7.3 创建 `references/parsers.md` 解析器说明
- [x] 7.4 创建 `references/error-handling.md` 错误处理指南

View File

@@ -4,5 +4,18 @@ context: |
忽略项目目录下的「.opencode」、「opencode」、「.claude」、「.codex」这几个目录与开发的skill无关
这个项目是专门用于开发用于大模型工具的 skill
所有开发的 skill 都放在「skills」目录下每个子目录都代表一个 skill目录名为 skill 的名称;
skill 名称统一加上「lyxy-」前缀;
开发过程中的文档使用中文,面向中文开发者进行交流;
「document」下是 skill 开发的相关规范文档;
Skill 编写规范:
渐进式披露三层结构:
第一层YAML前置元数据始终加载用于触发判断第二层SKILL.md正文相关时加载含核心指令第三层references/)按需加载存放详细文档。
YAML前置元数据
name用kebab-case与文件夹名一致description少于1024字符必须包含「做什么」和「何时使用」禁止XML标签可选compatibility和metadata字段。
description编写[功能描述]+[触发条件/用户短语]+[关键能力]。具体场景包含触发短语和文件类型,通用工具保持宽泛。
SKILL.md正文章节Purpose简要用途、When to Use适用场景、Quick Reference核心命令、Workflow执行步骤、References链接。长度建议少于5000字详细内容移至references/。
references/目录examples.md详细示例、error-handling.md错误处理、best-practices.md最佳实践。
常见模式:顺序工作流编排带验证关卡、迭代优化循环、上下文感知选择工具、领域智能嵌入专业知识。
故障排除不触发检查description触发短语过度触发添加负面触发器更具体化指令不遵循保持简洁关键指令放顶部用项目符号。
更多skill开发技巧和规范参考「document」下的文档

View File

@@ -0,0 +1,137 @@
## Requirements
### Requirement: 优先使用 lyxy-reader-html 解析 HTML 内容
大模型在遇到 URL、.html 或 .htm 文件时SHALL 优先激活并使用 lyxy-reader-html skill 来读取内容。
#### Scenario: 用户请求读取 URL
- **WHEN** 用户提供以 http:// 或 https:// 开头的 URL
- **THEN** 大模型 SHALL 激活 lyxy-reader-html skill
- **AND** 使用 skill 目录下的 `scripts/parser.py` 执行解析
#### Scenario: 用户请求读取 HTML 文件
- **WHEN** 用户提供的文件路径以 .html 或 .htm 结尾
- **THEN** 大模型 SHALL 激活 lyxy-reader-html skill
- **AND** 使用 skill 目录下的 `scripts/parser.py` 执行解析
### 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-html skill 或不确定具体参数用法
- **THEN** 大模型 SHALL 阅读 `scripts/README.md` 获取命令行参数、依赖安装和使用示例等详细信息
#### Scenario: 遇到特殊参数需求
- **WHEN** 用户请求使用特殊功能(如章节提取、正则搜索等)
- **THEN** 大模型 SHALL 参考 `scripts/README.md` 中的对应参数说明
### Requirement: 支持 URL 和 HTML 文件两种输入源
系统 SHALL 支持从 URL 下载 HTML 内容或直接读取本地 HTML 文件两种输入方式。
#### Scenario: 输入为 URL
- **WHEN** 输入以 http:// 或 https:// 开头
- **THEN** 系统 SHALL 识别为 URL 模式
- **AND** 尝试下载 URL 对应的 HTML 内容
#### Scenario: 输入为本地 HTML 文件
- **WHEN** 输入是存在的本地文件且扩展名为 .html 或 .htm
- **THEN** 系统 SHALL 识别为 HTML 文件模式
- **AND** 直接读取文件内容进行解析
#### Scenario: 输入无法识别
- **WHEN** 输入既不是 URL 也不是有效的 HTML 文件
- **THEN** 系统 SHALL 输出错误信息
- **AND** 以退出码 1 退出
### Requirement: URL 下载器按优先级降级
系统 SHALL 按 pyppeteer → selenium → httpx → urllib 的优先级尝试下载 URL 内容。
#### Scenario: 下载器按优先级降级
- **WHEN** 优先级最高的下载器不可用或下载失败
- **THEN** 系统自动尝试下一优先级的下载器
- **AND** 记录每个下载器的失败原因
#### Scenario: 所有下载器失败
- **WHEN** 所有下载策略均失败
- **THEN** 系统返回详细的失败信息
- **AND** 列出每种下载策略的失败原因
- **AND** 以退出码 1 退出
### Requirement: HTML 解析器按优先级降级
系统 SHALL 按 trafilatura → domscribe → MarkItDown → html2text 的优先级尝试解析 HTML 内容。
#### Scenario: 解析器按优先级降级
- **WHEN** 优先级最高的解析器不可用或解析失败
- **THEN** 系统自动尝试下一优先级的解析器
- **AND** 记录每个解析器的失败原因
#### Scenario: 所有解析器失败
- **WHEN** 所有解析策略均失败
- **THEN** 系统返回详细的失败信息
- **AND** 列出每种解析策略的失败原因
- **AND** 以退出码 1 退出
### Requirement: HTML 内容预处理清理
系统 SHALL 在解析前对 HTML 内容进行预处理清理,移除噪声元素。
#### Scenario: 清理 script 和 style 标签
- **WHEN** HTML 内容包含 &lt;script&gt;、&lt;style&gt;、&lt;link&gt;、&lt;svg&gt; 标签
- **THEN** 系统 SHALL 移除这些标签及其内容
#### Scenario: 清理 URL 属性
- **WHEN** HTML 标签包含 href、src、srcset、action、data-*src 等 URL 属性
- **THEN** 系统 SHALL 移除这些属性
#### Scenario: 清理 style 属性
- **WHEN** HTML 标签包含 style 属性
- **THEN** 系统 SHALL 移除 style 属性
### 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: Markdown 后处理
系统 SHALL 对解析后的 Markdown 内容进行后处理,提升可读性。
#### Scenario: 移除图片标记
- **WHEN** 解析结果包含 Markdown 图片语法 `![alt](url)`
- **THEN** 系统 SHALL 移除这些图片标记
#### Scenario: 规范化空行
- **WHEN** 解析结果包含连续 3 个或更多空行
- **THEN** 系统 SHALL 将其合并为单个空行

View File

@@ -0,0 +1,83 @@
# skill-progressive-disclosure
## Purpose
定义 skill 的渐进式披露Progressive Disclosure结构规范将 skill 内容按加载时机分为三层YAML 前置元数据、SKILL.md 正文、references/ 详细文档。通过分层加载减少 token 消耗,提高 Claude 的响应效率。
## Requirements
### Requirement: 三层渐进式披露结构
每个 skill 目录 SHALL 采用三层渐进式披露结构按加载时机分为第一层YAML 前置元数据、第二层SKILL.md 正文、第三层references/ 目录)。
#### Scenario: skill 目录包含完整三层结构
- **WHEN** 检查任意 skill 目录结构
- **THEN** 目录 SHALL 包含 SKILL.md 文件和 references/ 子目录
#### Scenario: 第一层始终加载
- **WHEN** Claude 启动会话时
- **THEN** 所有 skill 的 YAML 前置元数据 SHALL 被加载到系统提示中
### Requirement: YAML 前置元数据内容规范
YAML 前置元数据 SHALL 仅包含触发判断所需的最小信息,使 Claude 能够判断何时使用该 skill。
#### Scenario: 必需字段
- **WHEN** 编写 SKILL.md 的 YAML 前置元数据
- **THEN** SHALL 包含 name 和 description 字段
#### Scenario: description 格式
- **WHEN** 编写 description 字段
- **THEN** SHALL 同时包含「做什么」和「何时使用」两部分信息
#### Scenario: description 长度限制
- **WHEN** 检查 description 字段
- **THEN** 内容 SHALL 少于 1024 个字符
#### Scenario: runner skill 的 description 宽泛性
- **WHEN** skill 用途为通用脚本执行(如 Python/JavaScript runner
- **THEN** description SHALL 保持宽泛,以匹配所有相关处理任务
### Requirement: SKILL.md 正文内容规范
SKILL.md 正文 SHALL 包含核心工作流程和关键指令,保持精简以减少 token 消耗。
#### Scenario: SKILL.md 必需章节
- **WHEN** 编写 SKILL.md 正文
- **THEN** SHALL 包含以下章节Purpose、When to Use、Quick Reference、Workflow、References 链接
#### Scenario: SKILL.md 长度建议
- **WHEN** 检查 SKILL.md 总字数
- **THEN** 建议控制在 5000 字以内(含 YAML 前置元数据)
#### Scenario: 详细内容迁移
- **WHEN** SKILL.md 包含详细示例、完整错误处理、最佳实践等冗长内容
- **THEN** 这些内容 SHALL 迁移到 references/ 目录SKILL.md 中保留链接引用
### Requirement: references 目录结构规范
每个 skill SHALL 包含 references/ 子目录,用于存放按需加载的详细文档。
#### Scenario: references 目录存在性
- **WHEN** 检查 skill 目录结构
- **THEN** SHALL 存在 references/ 子目录
#### Scenario: references 文件分类
- **WHEN** 组织 references/ 目录内容
- **THEN** SHALL 按主题分类为独立的 markdown 文件,如 examples.md、error-handling.md、best-practices.md
#### Scenario: SKILL.md 引用 references
- **WHEN** SKILL.md 需要引用详细文档
- **THEN** SHALL 在 SKILL.md 中明确列出 references/ 中的文件并说明用途
### Requirement: 内容迁移完整性
从 SKILL.md 迁移内容到 references/ 时SHALL 确保所有原有信息被完整保留。
#### Scenario: 迁移前后对比
- **WHEN** 完成单个 skill 的内容迁移
- **THEN** references/ 中的文件 SHALL 包含原 SKILL.md 中被移除的所有详细内容
#### Scenario: 功能不变性
- **WHEN** 完成结构优化后
- **THEN** skill 的功能逻辑 SHALL 保持不变,仅文档结构发生变化

134
publish.sh Normal file
View File

@@ -0,0 +1,134 @@
#!/bin/bash
#
# Skill 发布脚本
#
# 使用方式:
# ./publish.sh <skill-name>
#
# 示例:
# ./publish.sh lyxy-kb
# ./publish.sh lyxy-reader-office
#
set -e
# 获取脚本所在目录
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
cd "$SCRIPT_DIR"
# 配置
TARGET_REPO_URL="https://github.com/lanyuanxiaoyao/skills.git"
TEMP_DIR_BASE="${TMPDIR:-/tmp}/lyxy-skill-publish"
# 颜色输出
GREEN='\033[0;32m'
RED='\033[0;31m'
NC='\033[0m' # No Color
# 打印信息
info() {
echo -e "${GREEN}>>>${NC} $1"
}
error() {
echo -e "${RED}错误:${NC} $1"
}
# 显示使用说明
show_usage() {
echo "使用方式:"
echo " $0 <skill-name>"
echo ""
echo "可用的 skills:"
for dir in skills/*/; do
if [ -d "$dir" ]; then
name="$(basename "$dir")"
echo " - $name"
fi
done
echo ""
echo "示例:"
echo " $0 lyxy-kb"
echo " $0 lyxy-reader-office"
exit 1
}
# 检查参数
if [ $# -eq 0 ]; then
error "缺少 skill 名称参数"
show_usage
fi
SKILL_NAME="$1"
SKILL_DIR="$SCRIPT_DIR/skills/$SKILL_NAME"
TARGET_PATH="skills/$SKILL_NAME"
# 检查 skill 目录是否存在
if [ ! -d "$SKILL_DIR" ]; then
error "Skill 目录不存在: $SKILL_DIR"
echo ""
echo "可用的 skills:"
for dir in skills/*/; do
if [ -d "$dir" ]; then
name="$(basename "$dir")"
echo " - $name"
fi
done
exit 1
fi
info "发布 Skill: $SKILL_NAME"
info "目标仓库: $TARGET_REPO_URL"
info "目标路径: $TARGET_PATH"
echo ""
# 创建临时目录
TIMESTAMP=$(date +%s)
TEMP_DIR="$TEMP_DIR_BASE-$TIMESTAMP"
mkdir -p "$TEMP_DIR"
# 清理函数
cleanup() {
if [ -d "$TEMP_DIR" ]; then
rm -rf "$TEMP_DIR"
fi
}
trap cleanup EXIT
# Clone 仓库
info "[1/4] Clone 仓库..."
REPO_DIR="$TEMP_DIR/skills-repo"
git clone --depth 1 "$TARGET_REPO_URL" "$REPO_DIR"
# 清空目标目录
info "[2/4] 清空目标目录..."
TARGET_DIR="$REPO_DIR/$TARGET_PATH"
if [ -d "$TARGET_DIR" ]; then
rm -rf "$TARGET_DIR"
fi
mkdir -p "$TARGET_DIR"
# 复制内容
info "[3/4] 复制 Skill 文件..."
# 复制除了 __pycache__ 之外的所有内容
for item in "$SKILL_DIR"/*; do
basename_item="$(basename "$item")"
# 跳过 __pycache__
if [ "$basename_item" = "__pycache__" ]; then
continue
fi
if [ -d "$item" ]; then
cp -r "$item" "$TARGET_DIR/"
else
cp "$item" "$TARGET_DIR/"
fi
done
# 提交并推送
info "[4/4] 提交并推送到 GitHub..."
cd "$REPO_DIR"
git add .
git commit -m "publish: $SKILL_NAME"
git push
info "发布成功!"

View File

@@ -1,293 +0,0 @@
---
name: lyxy-kb
description: 基于文件的个人知识库管理 skill提供知识项目的目录结构规范、文档解析入库、渐进式问答等底层能力定义。配合 commands/lyxy-kb/ 下的 command 使用。
compatibility: 依赖 lyxy-reader-office skill 解析 office 文档(.docx/.pdf/.pptx/.xlsx依赖 lyxy-runner-python skill 执行 Python 脚本。
---
# 个人知识库 Skill
基于文件的个人知识库管理系统。将项目相关文档组织为可被大模型高效检索和问答的知识库,支持文档解析入库、增量摘要、渐进式问答。
## Purpose
**纯文件驱动**:不依赖数据库或向量存储,所有数据以文件形式存在于项目目录中。
**渐进式查询**:通过 project.md 摘要索引 + parsed 详细文件的分层结构,优先读取摘要,按需加载详细内容,节省 token 消耗。
**增量管理**:支持增量解析入库和增量更新摘要,避免重复处理已入库的文档。
## When to Use
任何需要基于一组项目文档进行知识管理和问答的场景。
### 典型场景
- **项目文档管理**:将需求文档、技术方案、数据表等组织为结构化知识库
- **文档解析入库**:将 office 文档和纯文本文件解析为 markdown 并生成摘要
- **知识问答**:基于已入库的文档回答问题,并标注信息来源
### 不适用场景
- 需要语义搜索或向量化检索
- 需要跨多个知识项目关联查询
- 需要多人协作或权限控制
## 配套 Commands
| Command | 触发方式 | 说明 |
|---------|----------|------|
| init | `/lyxy-kb-init <name>` | 初始化知识项目目录结构 |
| ingest | `/lyxy-kb-ingest <name>` | 解析 sources/ 中新文件,增量更新 project.md |
| rebuild | `/lyxy-kb-rebuild <name>` | 全量重新生成 project.md |
| ask | `/lyxy-kb-ask <name>` | 基于知识库进行会话问答 |
## 项目名称规则
项目名称只允许使用以下字符:
- 中文字符
- 英文字母a-z、A-Z
- 数字0-9
- 短横线(-
- 下划线_
**不允许包含空格或其他特殊字符。** 不符合规则时应提示用户修改。
## 知识项目目录结构
每个知识项目是当前工作目录CWD下的一个子目录包含以下固定结构
```
<project-name>/
├── project.md # 高度摘要 + 文件索引
├── manifest.json # 增量追踪
├── parsed/ # 解析后的 markdown中间产物
├── sources/ # 待处理区(用户放入原始文档)
└── archive/ # 原始文件备份(带时间戳)
```
### 各目录/文件职责
| 路径 | 职责 |
|------|------|
| `project.md` | 项目的高度摘要和文件索引,作为问答时的入口文件 |
| `manifest.json` | 记录已处理文件的元信息,用于增量检测和版本追踪 |
| `parsed/` | 存放解析后的 markdown 文件,便于大模型读取分析 |
| `sources/` | 用户放入待处理文档的目录,解析后文件会被移走 |
| `archive/` | 原始文件的备份,每个文件都带时间戳后缀 |
### 结构完整性验证
执行任何 commandingest / rebuild / ask必须先验证项目目录结构是否完整即以下 5 项是否全部存在:
- `<project-name>/project.md`
- `<project-name>/manifest.json`
- `<project-name>/parsed/`
- `<project-name>/sources/`
- `<project-name>/archive/`
若不完整,提示用户先执行 `/lyxy-kb-init <project-name>`,终止当前操作。
## project.md 格式规范
```markdown
# <项目名称>
## 概述
(高度总结的项目信息,几百字以内)
## 关键信息
(从所有文档中提炼的核心要点)
## 文件索引
| 文件名 | 解析文件 | 最新归档 | 摘要 |
|--------|----------|----------|------|
| 需求文档 | parsed/需求文档.md | archive/需求文档_202602181600.docx | 简要摘要... |
## 更新记录
- 2026-02-18 16:00: 解析 需求文档.docx
```
### 更新策略
**增量追加**(默认,由 ingest 触发):
- 新文件:在文件索引表追加新行,在更新记录追加条目
- 已有文件更新:覆盖文件索引表中对应行的最新归档路径和摘要
- 概述和关键信息部分**不**自动更新
**全量重写**(由 rebuild 触发):
- 读取所有 parsed/*.md 文件
- 重新生成概述、关键信息、文件索引
- 保留历史更新记录,追加本次 rebuild 条目
## manifest.json 结构
```json
{
"project": "<项目名称>",
"created_at": "2026-02-18T16:00",
"last_ingest": "2026-02-18T17:25",
"files": [
{
"name": "需求文档",
"ext": ".docx",
"parsed": "parsed/需求文档.md",
"versions": [
{
"archived": "archive/需求文档_202602181600.docx",
"hash": "sha256:abc123...",
"ingested_at": "2026-02-18T16:00"
}
]
}
]
}
```
### 字段说明
| 字段 | 说明 |
|------|------|
| `project` | 项目名称 |
| `created_at` | 项目创建时间 |
| `last_ingest` | 最近一次 ingest 的时间 |
| `files[].name` | 文件名(不含扩展名) |
| `files[].ext` | 原始文件扩展名 |
| `files[].parsed` | 解析产物的相对路径 |
| `files[].versions` | 版本历史数组 |
| `files[].versions[].archived` | 归档文件的相对路径 |
| `files[].versions[].hash` | 文件内容的 SHA-256 哈希(使用 `sha256sum` 命令计算) |
| `files[].versions[].ingested_at` | 该版本的入库时间 |
## 文件类型解析策略
| 文件类型 | 解析方式 |
|----------|----------|
| `.docx`, `.pdf`, `.pptx`, `.xlsx` | 使用名为 **lyxy-reader-office** 的 skill 解析 |
| 其他所有文件(`.md`, `.txt`, `.csv`, `.json`, `.xml`, `.yaml`, `.yml`, `.log`, `.html` 等) | 直接读取文件内容 |
### Office 文档解析
解析 office 文档时,必须查找当前环境中名为 **lyxy-reader-office** 的 skill阅读其 SKILL.md 获取具体的执行方式和命令。
**如果环境中不存在 lyxy-reader-office skill且没有其他可替代的文档解析 skill则提示用户无法处理 office 文档,中止整个 ingest 流程。**
### sources/ 扫描规则
扫描 sources/ 时**递归检查所有子目录**中的文件。parsed 产物的路径仍为 `parsed/<文件名>.md`(扁平化存放),不保留 sources 中的子目录结构。
### 空文件处理
sources/ 中 0 字节的空文件应**跳过处理**,不解析、不归档、不更新 manifest。处理完成后向用户列出被跳过的空文件列表提示用户检查。
### 解析失败处理
如果某个文件解析失败(如文档损坏、解析器报错),该文件**保留在 sources/ 中不移动**,报告错误信息,继续处理其他文件。
## parsed 文件元信息标记
每个 parsed markdown 文件头部必须包含元信息注释:
```markdown
<!-- source: 技术方案.pdf -->
<!-- archived: archive/技术方案_202602181725.pdf -->
<!-- parsed_at: 2026-02-18 17:25 -->
# 技术方案
(文档正文内容...
```
| 元信息 | 说明 |
|--------|------|
| `source` | 原始文件名(含扩展名) |
| `archived` | 对应的归档文件相对路径 |
| `parsed_at` | 解析时间YYYY-MM-DD HH:mm 格式) |
## 文档生命周期
```
用户放入 sources/(支持子目录)
检查文件(跳过空文件、检测冲突)
解析文件内容(失败则保留在 sources/
├──▶ 写入 parsed/<文件名>.md含头部元信息
├──▶ 移动原始文件到 archive/<文件名_YYYYMMDDHHmm>.<ext>
├──▶ 更新 manifest.json
└──▶ 增量更新 project.md追加文件索引和更新记录
```
### 归档命名规则
所有进入 archive 的文件都必须带时间戳后缀,格式为 `<文件名_YYYYMMDDHHmm>.<扩展名>`,即使只有一个版本。
示例:
- `需求文档.docx``archive/需求文档_202602181600.docx`
- `技术方案.pdf`(第二次入库)→ `archive/技术方案_202602181725.pdf`
### 同名文件更新
同名同扩展名的文件再次入库时:
- `parsed/` 中的 markdown 文件被覆盖为最新版本
- `archive/` 中保留所有历史版本(每个版本独立的时间戳文件)
- `manifest.json` 中该文件条目的 `versions` 数组追加新版本记录
## 同名不同扩展名冲突检测
因为 parsed 产物以文件名(不含扩展名)+ `.md` 命名,同名不同扩展名的文件会产生冲突。
### 检测规则
1. **sources/ 内部检测**:扫描 sources/ 中所有文件(含子目录),如果存在同名但不同扩展名的文件(如 `技术方案.pdf``技术方案.docx`),拒绝处理并提示用户重命名
2. **与已入库文件检测**:将 sources/ 中文件的名称(不含扩展名)与 manifest.json 中已有记录对比,如果名称相同但扩展名不同,拒绝处理并提示用户重命名
### 处理方式
冲突文件不予处理,保留在 sources/ 中,提示用户重命名后重新执行 ingest。非冲突文件正常处理。
## 渐进式查询策略
问答时采用分层加载策略,节省 token
1. **读取 project.md**:获取项目概述和文件索引(低 token 开销)
2. **判断相关文件**:根据用户问题和文件索引中的摘要,判断需要查阅哪些 parsed 文件
3. **按需加载**:读取相关 parsed 文件的全部或部分内容
4. **回答并标注来源**:基于获取的信息回答问题
### 来源引用格式
回答中引用具体信息时,使用以下格式标注来源:
```
根据《文件名》(parsed/文件名.md)...
```
多个来源时分别标注各信息点的来源文件。
### 无相关信息
当知识库中未找到与用户问题相关的信息时,明确告知用户,不编造答案。
### 空知识库
如果 project.md 文件索引为空(尚无已入库文件),应告知用户知识库为空,建议先使用 `/lyxy-kb-ingest` 入库文档。
## Notes
### 依赖关系
| 依赖 | 用途 |
|------|------|
| lyxy-reader-office | 解析 .docx、.pdf、.pptx、.xlsx 文件为 markdown |
| lyxy-runner-python | 通过 uv 执行 lyxy-reader-office 的 Python 解析脚本 |
### 限制
- 不支持向量化语义搜索
- 不支持跨知识项目关联查询
- 不支持文档版本对比或 diff
- 不支持多用户协作或权限控制
- 大量文件全量重写时 token 消耗较高

View File

@@ -1,212 +0,0 @@
---
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

View File

@@ -1,449 +0,0 @@
# Document Parser 使用说明
模块化文档解析器,将 DOCX、PPTX、XLSX、PDF 文件转换为 Markdown 格式。
每种文档类型配备多个解析器按优先级依次尝试前一个失败自动回退到下一个。不安装任何第三方库时DOCX/PPTX/XLSX 仍可通过内置 XML 原生解析工作PDF 至少需要 pypdf
## 快速开始
```bash
# 最简运行XML 原生解析,无需安装依赖)
python parser.py report.docx
# 安装推荐依赖后运行
pip install "markitdown[docx]"
python parser.py report.docx
# 使用 uv 一键运行(自动安装依赖,无需手动 pip install
uv run --with "markitdown[docx]" parser.py report.docx
```
## 命令行用法
### 基本语法
```bash
python parser.py <file_path> [options]
```
`file_path` 为 DOCX、PPTX、XLSX 或 PDF 文件路径(相对或绝对路径)。不带任何选项时输出完整 Markdown 内容。
### 参数说明
以下参数互斥,一次只能使用一个:
| 短选项 | 长选项 | 说明 |
|--------|--------|------|
| `-c` | `--count` | 输出解析后文档的总字符数(不含换行符) |
| `-l` | `--lines` | 输出解析后文档的总行数 |
| `-t` | `--titles` | 输出所有标题行1-6 级,含 `#` 前缀) |
| `-tc <name>` | `--title-content <name>` | 提取指定标题及其下级内容(`name` 不包含 `#` 号) |
| `-s <pattern>` | `--search <pattern>` | 使用正则表达式搜索文档,返回匹配结果 |
搜索辅助参数(与 `-s` 配合使用):
| 短选项 | 长选项 | 说明 |
|--------|--------|------|
| `-n <num>` | `--context <num>` | 每个匹配结果包含的前后非空行数默认2 |
PDF 专用参数:
| 长选项 | 说明 |
|--------|------|
| `--high-res` | 启用 OCR 版面分析(需要额外依赖,处理较慢) |
### 退出码
| 退出码 | 含义 |
|--------|------|
| `0` | 解析成功 |
| `1` | 错误(文件不存在、格式无效、所有解析器失败、标题未找到、正则无效或无匹配) |
### 使用示例
**输出完整 Markdown**
```bash
python parser.py report.docx # 输出到终端
python parser.py report.docx > output.md # 重定向到文件
```
**统计信息(`-c` / `-l`**
输出单个数字,适合管道处理。
```bash
$ python parser.py report.docx -c
8500
$ python parser.py report.docx -l
215
```
**提取标题(`-t`**
每行一个标题,保留 `#` 前缀和层级。PDF 通常不包含语义化标题层级。
```bash
$ python parser.py report.docx -t
# 第一章 概述
## 1.1 背景
## 1.2 目标
# 第二章 实现
```
**提取标题内容(`-tc`**
输出指定标题及其下级内容。如果文档中有多个同名标题,用 `---` 分隔。每段输出包含上级标题链。
```bash
$ python parser.py report.docx -tc "1.1 背景"
# 第一章 概述
## 1.1 背景
这是背景的详细内容...
```
**搜索(`-s`**
支持 Python 正则表达式语法。多个匹配结果用 `---` 分隔。`-n` 控制上下文行数。
```bash
$ python parser.py report.docx -s "测试" -n 1
上一行内容
包含**测试**关键词的行
下一行内容
---
另一处上一行
另一处**测试**内容
另一处下一行
```
### 批量处理
```bash
# Linux/Mac
for file in *.docx; do
python parser.py "$file" > "${file%.docx}.md"
done
# Windows PowerShell
Get-ChildItem *.docx | ForEach-Object {
python parser.py $_.FullName > ($_.BaseName + ".md")
}
```
### 管道使用
```bash
# 过滤包含关键词的行
python parser.py report.docx | grep "重要" > important.md
# 统计含表格行数
python parser.py data.xlsx | grep -c "^|"
```
## 安装
脚本基于 Python 3.6+ 运行。每种文档类型有多个解析器按优先级依次尝试,建议安装对应类型的**所有**依赖以获得最佳兼容性。也可以只安装部分依赖,脚本会自动选择可用的解析器。
### DOCX
优先级Docling → unstructured → pypandoc-binary → MarkItDown → python-docx → XML 原生
```bash
# pip
pip install docling "unstructured[docx]" markdownify pypandoc-binary "markitdown[docx]" python-docx
# uv一键运行无需预安装
uv run --with docling --with "unstructured[docx]" --with markdownify --with pypandoc-binary --with "markitdown[docx]" --with python-docx parser.py report.docx
```
### PPTX
优先级Docling → unstructured → MarkItDown → python-pptx → XML 原生
```bash
# pip
pip install docling "unstructured[pptx]" markdownify "markitdown[pptx]" python-pptx
# uv
uv run --with docling --with "unstructured[pptx]" --with markdownify --with "markitdown[pptx]" --with python-pptx parser.py presentation.pptx
```
### XLSX
优先级Docling → unstructured → MarkItDown → pandas → XML 原生
```bash
# pip
pip install docling "unstructured[xlsx]" markdownify "markitdown[xlsx]" pandas tabulate
# uv
uv run --with docling --with "unstructured[xlsx]" --with markdownify --with "markitdown[xlsx]" --with pandas --with tabulate parser.py data.xlsx
```
### PDF
默认优先级Docling → unstructured (fast) → MarkItDown → pypdf
`--high-res` 优先级Docling OCR → unstructured OCR (hi_res) → Docling → unstructured (fast) → MarkItDown → pypdf
```bash
# pip - 基础文本提取fast 策略,无需 OCR
pip install docling "unstructured[pdf]" markdownify "markitdown[pdf]" pypdf
# pip - OCR 版面分析(--high-res 所需依赖)
pip install docling "unstructured[pdf]" unstructured-paddleocr "paddlepaddle==2.6.2" ml-dtypes markdownify "markitdown[pdf]" pypdf
# uv - 基础文本提取
uv run --with docling --with "unstructured[pdf]" --with markdownify --with "markitdown[pdf]" --with pypdf parser.py report.pdf
# uv - OCR 版面分析
uv run --with docling --with "unstructured[pdf]" --with unstructured-paddleocr --with "paddlepaddle==2.6.2" --with ml-dtypes --with markdownify --with "markitdown[pdf]" --with pypdf parser.py report.pdf --high-res
```
> PDF 无内置 XML 原生解析,至少需要安装 pypdf。默认模式下 Docling 不启用 OCRunstructured 使用 fast 策略。指定 `--high-res` 后Docling 启用 OCRunstructured 使用 hi_res 策略配合 PaddleOCR 进行版面分析。hi_res 策略需要额外安装 `unstructured-paddleocr`、`paddlepaddle==2.6.2`、`ml-dtypes`。PaddlePaddle 必须锁定 2.x 版本3.x 在 Windows 上有 OneDNN 兼容问题。
>
### 安装所有依赖
```bash
# pip - 基础文本提取(不包含 PDF OCR
pip install docling "unstructured[docx,pptx,xlsx,pdf]" markdownify pypandoc-binary "markitdown[docx,pptx,xlsx]" python-docx python-pptx pandas tabulate pypdf
# pip - 完整版(包含 PDF OCR
pip install docling "unstructured[docx,pptx,xlsx,pdf]" markdownify unstructured-paddleocr "paddlepaddle==2.6.2" ml-dtypes pypandoc-binary "markitdown[docx,pptx,xlsx,pdf]" python-docx python-pptx pandas tabulate pypdf
# uv - 基础文本提取
uv run --with docling --with "unstructured[docx,pptx,xlsx,pdf]" --with markdownify --with pypandoc-binary --with "markitdown[docx,pptx,xlsx]" --with python-docx --with python-pptx --with pandas --with tabulate --with pypdf parser.py file.docx
# uv - 完整版(包含 PDF OCR
uv run --with docling --with "unstructured[docx,pptx,xlsx,pdf]" --with markdownify --with unstructured-paddleocr --with "paddlepaddle==2.6.2" --with ml-dtypes --with pypandoc-binary --with "markitdown[docx,pptx,xlsx,pdf]" --with python-docx --with python-pptx --with pandas --with tabulate --with pypdf parser.py file.docx
```
### 依赖说明
**MarkItDown**:需要按文档类型安装可选依赖,直接 `pip install markitdown` 不包含任何格式支持。
```bash
pip install "markitdown[docx]" # DOCX
pip install "markitdown[pptx]" # PPTX
pip install "markitdown[xlsx]" # XLSX
pip install "markitdown[pdf]" # PDF
pip install "markitdown[docx,pptx,xlsx,pdf]" # 全部
```
**Docling**DOCX/PPTX/XLSX 使用 SimplePipeline 直接解析 XML 结构,不涉及 OCR。PDF 默认不启用 OCR`do_ocr=False`),指定 `--high-res` 后启用 OCR`do_ocr=True`)。首次运行 OCR 模式会自动下载模型到缓存目录,需保持网络连通。
**unstructured**:需同时安装 `markdownify`。支持按文档类型安装特定 extras 以减少依赖量:
- `unstructured[docx]` - DOCX 处理(仅需 `python-docx`
- `unstructured[pptx]` - PPTX 处理(仅需 `python-pptx`
- `unstructured[xlsx]` - XLSX 处理(需 `openpyxl``xlrd``pandas` 等)
- `unstructured` - 基础包(用于 PDF fast 策略)
- `unstructured[all-docs]` - 所有文档类型(包含大量不必要的 OCR/视觉依赖)
**PaddleOCR**:不能用 `paddleocr` 代替 `unstructured-paddleocr`unstructured 查找的模块名是 `unstructured_paddleocr`
## 输出格式
### Markdown 文档结构
无选项时输出完整 Markdown包含以下元素
```markdown
# 一级标题
正文段落
## 二级标题
- 无序列表项
- 无序列表项
1. 有序列表项
2. 有序列表项
| 列1 | 列2 | 列3 |
|------|------|------|
| 数据1 | 数据2 | 数据3 |
**粗体** *斜体* <u>下划线</u>
```
### 各格式特有结构
**PPTX** — 每张幻灯片以 `## Slide N` 为标题,幻灯片之间以 `---` 分隔:
```markdown
## Slide 1
幻灯片 1 的内容
---
## Slide 2
幻灯片 2 的内容
---
```
**XLSX** — 以 `## SheetName` 区分工作表,数据以 Markdown 表格呈现:
```markdown
# Excel数据转换结果 (原生XML解析)
## Sheet1
| 列1 | 列2 | 列3 |
|------|------|------|
| 数据1 | 数据2 | 数据3 |
## Sheet2
| 列A | 列B |
|------|------|
| 值1 | 值2 |
```
**PDF** — 纯文本流通常不包含语义化标题层级PDF 是版面描述格式,标题只是视觉样式)。使用 Docling 或 unstructured hi_res 策略可通过版面分析识别部分标题,但准确度取决于排版质量。
### 内容自动处理
输出前会自动进行以下处理:
| 处理 | 说明 |
|------|------|
| 图片移除 | 删除 `![alt](url)` 语法 |
| 空行规范化 | 连续多个空行合并为一个 |
| RGB 噪声过滤 | 移除 `R:255 G:128 B:0` 格式的颜色值行(仅 unstructured 解析器) |
| 页码噪声过滤 | 移除 `— 3 —` 格式的页码行(仅 unstructured 解析器) |
| 页眉/页脚过滤 | 自动跳过 Header/Footer 元素(仅 unstructured 解析器) |
## 错误处理
### 错误消息
```bash
# 文件不存在
$ python parser.py missing.docx
错误: 文件不存在: missing.docx
# 格式无效
$ python parser.py readme.txt
错误: 不是有效的 DOCX、PPTX、XLSX 或 PDF 格式: readme.txt
# 所有解析器失败DOCX 示例)
$ python parser.py report.docx
所有解析方法均失败:
- Docling: docling 库未安装
- unstructured: unstructured 库未安装
- pypandoc-binary: pypandoc-binary 库未安装
- MarkItDown: MarkItDown 库未安装
- python-docx: python-docx 库未安装
- XML 原生解析: document.xml 不存在或无法访问
# 标题未找到
$ python parser.py report.docx -tc "不存在的标题"
错误: 未找到标题 '不存在的标题'
# 无效正则或无匹配
$ python parser.py report.docx -s "[invalid"
错误: 正则表达式无效或未找到匹配: '[invalid'
```
### 解析器回退机制
脚本按优先级依次尝试各解析器。每个解析器失败后记录原因(库未安装 / 解析失败 / 文档为空),然后自动尝试下一个。全部失败时输出汇总信息并以退出码 1 退出。
## 解析器对比
### DOCX
| 解析器 | 优点 | 缺点 | 适用场景 |
|---------|------|--------|---------|
| **Docling** | 单一依赖覆盖全格式;自动 OCR输出结构稳定 | 首次需下载模型;内存占用较高 | 一键解析;需要 OCR |
| **unstructured** | 元素类型感知自动过滤噪声HTML 表格转 Markdown | 需 `unstructured[docx]` / `[pptx]` / `[xlsx]` + `markdownify` | 结构化输出;表格转换 |
| **pypandoc-binary** | 自带 Pandoc输出整洁错误信息清晰 | 仅 DOCX包体积大 | 标准化 Markdown |
| **MarkItDown** | 微软官方;格式规范 | 输出简洁 | 标准格式;自动化处理 |
| **python-docx** | 输出最详细;保留完整结构;支持复杂样式 | 可能含多余空行 | 精确控制输出 |
| **XML 原生** | 无需依赖;速度快 | 样式处理有限 | 依赖不可用时兜底 |
### PPTX
| 解析器 | 优点 | 缺点 | 适用场景 |
|---------|------|--------|---------|
| **Docling** | 文本/表格/图片 OCR统一 Markdown | 需下载模型 | 一次性转换;含图片的 PPTX |
| **unstructured** | 元素感知;过滤 RGB 噪声;表格转换 | 需 `unstructured[pptx]` + `markdownify` | 结构化输出 |
| **MarkItDown** | 自动 Slide 分隔;简洁 | 详细度低 | 快速预览 |
| **python-pptx** | 输出最详细;支持层级列表 | 依赖私有 API | 完整内容提取 |
| **XML 原生** | 无需依赖;速度快 | 分组简单 | 依赖不可用时兜底 |
### XLSX
| 解析器 | 优点 | 缺点 | 适用场景 |
|---------|------|--------|---------|
| **Docling** | 全表导出;处理合并单元格/图像 OCR | 大表可能慢 | 快速全表转换 |
| **unstructured** | 元素感知;过滤噪声;表格转换 | 需 `unstructured[xlsx]` + `markdownify` | 结构化输出 |
| **MarkItDown** | 支持多工作表;简洁 | 详细度低 | 快速预览 |
| **pandas** | 功能强大;支持复杂表格 | 需 `pandas` + `tabulate` | 数据分析 |
| **XML 原生** | 无需依赖;支持所有单元格类型 | 无数据处理能力 | 依赖不可用时兜底 |
### PDF
| 解析器 | 模式 | 优点 | 缺点 | 适用场景 |
|---------|------|------|--------|---------|
| **Docling** | 默认 | 结构化 Markdown表格/图片占位 | 首次需下载模型 | 有文本层的 PDF |
| **Docling OCR** | `--high-res` | 内置 OCR结构化 Markdown | 模型体积大OCR 耗时长 | 扫描版 PDF多语言 |
| **unstructured** | 默认 | fast 策略;速度快 | 不做版面分析;标题不可靠 | 快速文本提取 |
| **unstructured OCR** | `--high-res` | hi_res 版面分析 + PaddleOCR标题识别 | 需额外 PaddleOCR 依赖 | 版面分析OCR |
| **MarkItDown** | 通用 | 微软官方;格式规范 | 输出简洁 | 标准格式 |
| **pypdf** | 通用 | 轻量;速度快;安装简单 | 功能简单 | 快速文本提取 |
## 常见问题
### 为什么有些内容没有提取到?
不同解析器输出详细度不同。优先级高的解析器不一定输出最详细——Docling 和 unstructured 侧重结构化python-docx/python-pptx 输出最详细但不做噪声过滤。建议安装对应类型的所有依赖,脚本会自动选择优先级最高的可用解析器。
### PDF 文件没有标题层级?
PDF 是版面描述格式,不包含语义化标题结构。使用 `--high-res` 参数可启用 Docling OCR 或 unstructured hi_res 策略,通过版面分析识别部分标题,准确度取决于排版质量。默认模式下建议用 `-s` 搜索定位内容,或用 `-c` / `-l` 了解文档规模。
### 表格格式不正确?
XML 原生解析器对复杂表格(合并单元格、嵌套表格)支持有限。安装 Docling、unstructured 或对应的专用库可获得更好的表格处理效果。
### 中文显示乱码?
脚本输出 UTF-8 编码,确保终端支持:
```bash
# Linux/Mac
export LANG=en_US.UTF-8
# Windows PowerShell
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
```
### 如何只使用特定解析器?
当前版本不支持指定解析器,总是按优先级自动选择。可以通过只安装目标解析器的依赖来间接控制——未安装的解析器会被跳过。
### 大文件处理慢?
Docling 和 unstructured 对大文件较慢(尤其是 OCR。如果只需要快速提取文本可以只安装轻量依赖如 pypdf、python-docx让脚本回退到这些解析器。DOCX/PPTX/XLSX 不安装任何依赖时使用 XML 原生解析,速度最快。
## 文件结构
```
scripts/
├── common.py # 公共函数和常量
├── docx_parser.py # DOCX 文件解析
├── pptx_parser.py # PPTX 文件解析
├── xlsx_parser.py # XLSX 文件解析
├── pdf_parser.py # PDF 文件解析
├── parser.py # 命令行入口
└── README.md # 本文档
```

View File

@@ -1,337 +0,0 @@
#!/usr/bin/env python3
"""文档解析器的公共模块,包含所有格式共享的工具函数和验证函数。"""
import os
import re
import zipfile
from typing import List, Optional, Tuple
IMAGE_PATTERN = re.compile(r"!\[[^\]]*\]\([^)]+\)")
# unstructured 噪声匹配: pptx 中的 RGB 颜色值(如 "R:255 G:128 B:0"
_RGB_PATTERN = re.compile(r"^R:\d+\s+G:\d+\s+B:\d+$")
# unstructured 噪声匹配: 破折号页码(如 "— 3 —"
_PAGE_NUMBER_PATTERN = re.compile(r"^—\s*\d+\s*—$")
def parse_with_markitdown(
file_path: str,
) -> Tuple[Optional[str], Optional[str]]:
"""使用 MarkItDown 库解析文件"""
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_docling(file_path: str) -> Tuple[Optional[str], Optional[str]]:
"""使用 docling 库解析文件"""
try:
from docling.document_converter import DocumentConverter
except ImportError:
return None, "docling 库未安装"
try:
converter = DocumentConverter()
result = converter.convert(file_path)
markdown_content = result.document.export_to_markdown()
if not markdown_content.strip():
return None, "文档为空"
return markdown_content, None
except Exception as e:
return None, f"docling 解析失败: {str(e)}"
def build_markdown_table(rows_data: List[List[str]]) -> str:
"""将二维列表转换为 Markdown 表格格式"""
if not rows_data or not rows_data[0]:
return ""
md_lines = []
for i, row_data in enumerate(rows_data):
row_text = [cell if cell else "" for cell in row_data]
md_lines.append("| " + " | ".join(row_text) + " |")
if i == 0:
md_lines.append("| " + " | ".join(["---"] * len(row_text)) + " |")
return "\n".join(md_lines) + "\n\n"
def flush_list_stack(list_stack: List[str], target: List[str]) -> None:
"""将列表堆栈中的非空项添加到目标列表并清空堆栈"""
for item in list_stack:
if item:
target.append(item + "\n")
list_stack.clear()
def safe_open_zip(zip_file: zipfile.ZipFile, name: str) -> Optional[zipfile.ZipExtFile]:
"""安全地从 ZipFile 中打开文件,防止路径遍历攻击"""
if not name:
return None
if name.startswith("/") or name.startswith(".."):
return None
if "/../" in name or name.endswith("/.."):
return None
if "\\" in name:
return None
return zip_file.open(name)
_CONSECUTIVE_BLANK_LINES = re.compile(r"\n{3,}")
def normalize_markdown_whitespace(content: str) -> str:
"""规范化 Markdown 空白字符,保留单行空行"""
return _CONSECUTIVE_BLANK_LINES.sub("\n\n", content)
def _is_valid_ooxml(file_path: str, required_files: List[str]) -> bool:
try:
with zipfile.ZipFile(file_path, "r") as zip_file:
names = set(zip_file.namelist())
return all(r in names for r in required_files)
except (zipfile.BadZipFile, zipfile.LargeZipFile):
return False
_DOCX_REQUIRED = ["[Content_Types].xml", "_rels/.rels", "word/document.xml"]
_PPTX_REQUIRED = ["[Content_Types].xml", "_rels/.rels", "ppt/presentation.xml"]
_XLSX_REQUIRED = ["[Content_Types].xml", "_rels/.rels", "xl/workbook.xml"]
def is_valid_docx(file_path: str) -> bool:
"""验证文件是否为有效的 DOCX 格式"""
return _is_valid_ooxml(file_path, _DOCX_REQUIRED)
def is_valid_pptx(file_path: str) -> bool:
"""验证文件是否为有效的 PPTX 格式"""
return _is_valid_ooxml(file_path, _PPTX_REQUIRED)
def is_valid_xlsx(file_path: str) -> bool:
"""验证文件是否为有效的 XLSX 格式"""
return _is_valid_ooxml(file_path, _XLSX_REQUIRED)
def is_valid_pdf(file_path: str) -> bool:
"""验证文件是否为有效的 PDF 格式"""
try:
with open(file_path, "rb") as f:
header = f.read(4)
return header == b"%PDF"
except (IOError, OSError):
return False
def remove_markdown_images(markdown_text: str) -> str:
"""移除 Markdown 文本中的图片标记"""
return IMAGE_PATTERN.sub("", markdown_text)
def get_heading_level(line: str) -> int:
"""获取 Markdown 行的标题级别1-6非标题返回 0"""
stripped = line.lstrip()
if not stripped.startswith("#"):
return 0
without_hash = stripped.lstrip("#")
level = len(stripped) - len(without_hash)
if not (1 <= level <= 6):
return 0
if len(stripped) == level:
return level
if stripped[level] != " ":
return 0
return level
def extract_titles(markdown_text: str) -> List[str]:
"""提取 markdown 文本中的所有标题行1-6级"""
title_lines = []
for line in markdown_text.split("\n"):
if get_heading_level(line) > 0:
title_lines.append(line.lstrip())
return title_lines
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 match_num, idx in enumerate(match_indices):
if match_num > 0:
result_lines.append("\n---\n")
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:
context_start_idx = max(0, start - context_lines)
context_end_idx = min(len(non_empty_indices) - 1, end + context_lines)
start_line_idx = non_empty_indices[context_start_idx]
end_line_idx = non_empty_indices[context_end_idx]
result_lines = [
line
for i, line in enumerate(lines)
if start_line_idx <= i <= end_line_idx
]
results.append("\n".join(result_lines))
return "\n---\n".join(results)
_FILE_TYPE_VALIDATORS = {
".docx": is_valid_docx,
".pptx": is_valid_pptx,
".xlsx": is_valid_xlsx,
".pdf": is_valid_pdf,
}
def detect_file_type(file_path: str) -> Optional[str]:
"""检测文件类型,返回 'docx''pptx''xlsx''pdf'"""
ext = os.path.splitext(file_path)[1].lower()
validator = _FILE_TYPE_VALIDATORS.get(ext)
if validator and validator(file_path):
return ext.lstrip(".")
return None
def _unstructured_elements_to_markdown(
elements: list, trust_titles: bool = True
) -> str:
"""将 unstructured 解析出的元素列表转换为 Markdown 文本"""
try:
import markdownify as md_lib
from unstructured.documents.elements import (
Footer,
Header,
Image,
ListItem,
PageBreak,
PageNumber,
Table,
Title,
)
except ImportError:
return "\n\n".join(
el.text for el in elements if hasattr(el, "text") and el.text and el.text.strip()
)
skip_types = (Header, Footer, PageBreak, PageNumber)
parts = []
for el in elements:
if isinstance(el, skip_types):
continue
text = el.text.strip() if hasattr(el, "text") else str(el).strip()
if not text or _RGB_PATTERN.match(text) or _PAGE_NUMBER_PATTERN.match(text):
continue
if isinstance(el, Table):
html = getattr(el.metadata, "text_as_html", None)
if html:
parts.append(md_lib.markdownify(html, strip=["img"]).strip())
else:
parts.append(str(el))
elif isinstance(el, Title) and trust_titles:
depth = getattr(el.metadata, "category_depth", None) or 1
depth = min(max(depth, 1), 4)
parts.append(f"{'#' * depth} {text}")
elif isinstance(el, ListItem):
parts.append(f"- {text}")
elif isinstance(el, Image):
path = getattr(el.metadata, "image_path", None) or ""
if path:
parts.append(f"![image]({path})")
else:
parts.append(text)
return "\n\n".join(parts)

View File

@@ -1,308 +0,0 @@
#!/usr/bin/env python3
"""DOCX 文件解析模块,提供多种解析方法。"""
import xml.etree.ElementTree as ET
import zipfile
from typing import Any, List, Optional, Tuple
from common import (
_unstructured_elements_to_markdown,
build_markdown_table,
parse_with_docling,
parse_with_markitdown,
safe_open_zip,
)
def parse_docx_with_docling(file_path: str) -> Tuple[Optional[str], Optional[str]]:
"""使用 docling 库解析 DOCX 文件"""
return parse_with_docling(file_path)
def parse_docx_with_unstructured(file_path: str) -> Tuple[Optional[str], Optional[str]]:
"""使用 unstructured 库解析 DOCX 文件"""
try:
from unstructured.partition.docx import partition_docx
except ImportError:
return None, "unstructured 库未安装"
try:
elements = partition_docx(filename=file_path, infer_table_structure=True)
content = _unstructured_elements_to_markdown(elements)
if not content.strip():
return None, "文档为空"
return content, None
except Exception as e:
return None, f"unstructured 解析失败: {str(e)}"
def parse_docx_with_pypandoc(file_path: str) -> Tuple[Optional[str], Optional[str]]:
"""使用 pypandoc-binary 库解析 DOCX 文件。"""
try:
import pypandoc
except ImportError:
return None, "pypandoc-binary 库未安装"
try:
content = pypandoc.convert_file(
source_file=file_path,
to="md",
format="docx",
outputfile=None,
extra_args=["--wrap=none"],
)
except OSError as exc:
return None, f"pypandoc-binary 缺少 Pandoc 可执行文件: {exc}"
except RuntimeError as exc:
return None, f"pypandoc-binary 解析失败: {exc}"
content = content.strip()
if not content:
return None, "文档为空"
return content, None
def parse_docx_with_markitdown(file_path: str) -> Tuple[Optional[str], Optional[str]]:
"""使用 MarkItDown 库解析 DOCX 文件"""
return parse_with_markitdown(file_path)
def parse_docx_with_python_docx(file_path: str) -> Tuple[Optional[str], Optional[str]]:
"""使用 python-docx 库解析 DOCX 文件"""
try:
from docx import Document
except ImportError:
return None, "python-docx 库未安装"
try:
doc = Document(file_path)
_HEADING_LEVELS = {
"Title": 1, "Heading 1": 1, "Heading 2": 2, "Heading 3": 3,
"Heading 4": 4, "Heading 5": 5, "Heading 6": 6,
}
def get_heading_level(para: Any) -> int:
if para.style and para.style.name:
return _HEADING_LEVELS.get(para.style.name, 0)
return 0
_LIST_STYLES = {
"Bullet": "bullet", "Number": "number",
}
def get_list_style(para: Any) -> Optional[str]:
if not para.style or not para.style.name:
return None
style_name = para.style.name
if style_name in _LIST_STYLES:
return _LIST_STYLES[style_name]
if style_name.startswith("List Bullet"):
return "bullet"
if style_name.startswith("List Number"):
return "number"
return None
def convert_runs_to_markdown(runs: List[Any]) -> 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: Any) -> str:
rows_data = []
for row in table.rows:
row_data = []
for cell in row.cells:
cell_text = cell.text.strip().replace("\n", " ")
row_data.append(cell_text)
rows_data.append(row_data)
return build_markdown_table(rows_data)
markdown_lines = []
prev_was_list = False
from docx.table import Table as DocxTable
from docx.text.paragraph import Paragraph
for element in doc.element.body:
if element.tag.endswith('}p'):
para = Paragraph(element, doc)
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}")
prev_was_list = False
else:
list_style = get_list_style(para)
if list_style == "bullet":
if not prev_was_list and markdown_lines:
markdown_lines.append("")
markdown_lines.append(f"- {text}")
prev_was_list = True
elif list_style == "number":
if not prev_was_list and markdown_lines:
markdown_lines.append("")
markdown_lines.append(f"1. {text}")
prev_was_list = True
else:
if prev_was_list and markdown_lines:
markdown_lines.append("")
markdown_lines.append(text)
markdown_lines.append("")
prev_was_list = False
elif element.tag.endswith('}tbl'):
table = DocxTable(element, doc)
table_md = convert_table_to_markdown(table)
if table_md:
markdown_lines.append(table_md)
markdown_lines.append("")
prev_was_list = False
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_docx_with_xml(file_path: str) -> Tuple[Optional[str], Optional[str]]:
"""使用 XML 原生解析 DOCX 文件"""
word_namespace = "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
namespaces = {"w": word_namespace}
_STYLE_NAME_TO_HEADING = {
"title": 1, "heading 1": 1, "heading 2": 2, "heading 3": 3,
"heading 4": 4, "heading 5": 5, "heading 6": 6,
}
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: Any, 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: Any, namespaces: dict) -> str:
rows = table_elem.findall(".//w:tr", namespaces=namespaces)
if not rows:
return ""
rows_data = []
for row in 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:
rows_data.append(cell_texts)
return build_markdown_table(rows_data)
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).getroot()
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:
style_name_lower = style_name.lower()
if style_name_lower in _STYLE_NAME_TO_HEADING:
style_to_level[style_id] = _STYLE_NAME_TO_HEADING[style_name_lower]
elif (
style_name_lower.startswith("list bullet")
or style_name_lower == "bullet"
):
style_to_list[style_id] = "bullet"
elif (
style_name_lower.startswith("list number")
or style_name_lower == "number"
):
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).getroot()
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)}"

View File

@@ -1,166 +0,0 @@
#!/usr/bin/env python3
"""文档解析器命令行交互模块,提供命令行接口。支持 DOCX、PPTX、XLSX 和 PDF 文件。"""
import argparse
import logging
import os
import sys
import warnings
# 抑制第三方库的进度条和日志,仅保留解析结果输出
os.environ["HF_HUB_DISABLE_PROGRESS_BARS"] = "1"
os.environ["HF_HUB_DISABLE_TELEMETRY"] = "1"
os.environ["TQDM_DISABLE"] = "1"
warnings.filterwarnings("ignore")
logging.disable(logging.WARNING)
import common
import docx_parser
import pdf_parser
import pptx_parser
import xlsx_parser
def main() -> None:
parser = argparse.ArgumentParser(
description="将 DOCX、PPTX、XLSX 或 PDF 文件解析为 Markdown"
)
parser.add_argument("file_path", help="DOCX、PPTX、XLSX 或 PDF 文件的绝对路径")
parser.add_argument(
"-n",
"--context",
type=int,
default=2,
help="与 -s 配合使用,指定每个检索结果包含的前后行数(不包含空行)",
)
parser.add_argument(
"--high-res",
action="store_true",
help="PDF 解析时启用 OCR 版面分析(需要额外依赖,处理较慢)",
)
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)
file_type = common.detect_file_type(args.file_path)
if not file_type:
print(f"错误: 不是有效的 DOCX、PPTX、XLSX 或 PDF 格式: {args.file_path}")
sys.exit(1)
if file_type == "docx":
parsers = [
("docling", docx_parser.parse_docx_with_docling),
("unstructured", docx_parser.parse_docx_with_unstructured),
("pypandoc-binary", docx_parser.parse_docx_with_pypandoc),
("MarkItDown", docx_parser.parse_docx_with_markitdown),
("python-docx", docx_parser.parse_docx_with_python_docx),
("XML 原生解析", docx_parser.parse_docx_with_xml),
]
elif file_type == "pptx":
parsers = [
("docling", pptx_parser.parse_pptx_with_docling),
("unstructured", pptx_parser.parse_pptx_with_unstructured),
("MarkItDown", pptx_parser.parse_pptx_with_markitdown),
("python-pptx", pptx_parser.parse_pptx_with_python_pptx),
("XML 原生解析", pptx_parser.parse_pptx_with_xml),
]
elif file_type == "xlsx":
parsers = [
("docling", xlsx_parser.parse_xlsx_with_docling),
("unstructured", xlsx_parser.parse_xlsx_with_unstructured),
("MarkItDown", xlsx_parser.parse_xlsx_with_markitdown),
("pandas", xlsx_parser.parse_xlsx_with_pandas),
("XML 原生解析", xlsx_parser.parse_xlsx_with_xml),
]
else:
if args.high_res:
parsers = [
("docling OCR", pdf_parser.parse_pdf_with_docling_ocr),
("unstructured OCR", pdf_parser.parse_pdf_with_unstructured_ocr),
("docling", pdf_parser.parse_pdf_with_docling),
("unstructured", pdf_parser.parse_pdf_with_unstructured),
("MarkItDown", pdf_parser.parse_pdf_with_markitdown),
("pypdf", pdf_parser.parse_pdf_with_pypdf),
]
else:
parsers = [
("docling", pdf_parser.parse_pdf_with_docling),
("unstructured", pdf_parser.parse_pdf_with_unstructured),
("MarkItDown", pdf_parser.parse_pdf_with_markitdown),
("pypdf", pdf_parser.parse_pdf_with_pypdf),
]
failures = []
content = None
for parser_name, parser_func in parsers:
content, error = parser_func(args.file_path)
if content is not None:
content = common.remove_markdown_images(content)
content = common.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 = common.extract_titles(content)
for title in titles:
print(title)
elif args.title_content:
title_content = common.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 = common.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

@@ -1,134 +0,0 @@
#!/usr/bin/env python3
"""PDF 文件解析模块,提供多种解析方法。"""
from typing import Optional, Tuple
from common import _unstructured_elements_to_markdown, parse_with_markitdown
def parse_pdf_with_docling(file_path: str) -> Tuple[Optional[str], Optional[str]]:
"""使用 docling 库解析 PDF 文件(不启用 OCR"""
try:
from docling.datamodel.base_models import InputFormat
from docling.datamodel.pipeline_options import PdfPipelineOptions
from docling.document_converter import DocumentConverter, PdfFormatOption
except ImportError:
return None, "docling 库未安装"
try:
converter = DocumentConverter(
format_options={
InputFormat.PDF: PdfFormatOption(
pipeline_options=PdfPipelineOptions(do_ocr=False)
)
}
)
result = converter.convert(file_path)
markdown_content = result.document.export_to_markdown()
if not markdown_content.strip():
return None, "文档为空"
return markdown_content, None
except Exception as e:
return None, f"docling 解析失败: {str(e)}"
def parse_pdf_with_docling_ocr(file_path: str) -> Tuple[Optional[str], Optional[str]]:
"""使用 docling 库解析 PDF 文件(启用 OCR"""
try:
from docling.document_converter import DocumentConverter
except ImportError:
return None, "docling 库未安装"
try:
converter = DocumentConverter()
result = converter.convert(file_path)
markdown_content = result.document.export_to_markdown()
if not markdown_content.strip():
return None, "文档为空"
return markdown_content, None
except Exception as e:
return None, f"docling OCR 解析失败: {str(e)}"
def parse_pdf_with_unstructured(file_path: str) -> Tuple[Optional[str], Optional[str]]:
"""使用 unstructured 库解析 PDF 文件fast 策略)"""
try:
from unstructured.partition.pdf import partition_pdf
except ImportError:
return None, "unstructured 库未安装"
try:
elements = partition_pdf(
filename=file_path,
infer_table_structure=True,
strategy="fast",
languages=["chi_sim"],
)
# fast 策略不做版面分析Title 类型标注不可靠
content = _unstructured_elements_to_markdown(elements, trust_titles=False)
if not content.strip():
return None, "文档为空"
return content, None
except Exception as e:
return None, f"unstructured 解析失败: {str(e)}"
def parse_pdf_with_unstructured_ocr(
file_path: str,
) -> Tuple[Optional[str], Optional[str]]:
"""使用 unstructured 库解析 PDF 文件hi_res 策略 + PaddleOCR"""
try:
from unstructured.partition.pdf import partition_pdf
except ImportError:
return None, "unstructured 库未安装"
try:
from unstructured.partition.utils.constants import OCR_AGENT_PADDLE
except ImportError:
return None, "unstructured-paddleocr 库未安装"
try:
elements = partition_pdf(
filename=file_path,
infer_table_structure=True,
strategy="hi_res",
languages=["chi_sim"],
ocr_agent=OCR_AGENT_PADDLE,
table_ocr_agent=OCR_AGENT_PADDLE,
)
content = _unstructured_elements_to_markdown(elements, trust_titles=True)
if not content.strip():
return None, "文档为空"
return content, None
except Exception as e:
return None, f"unstructured OCR 解析失败: {str(e)}"
def parse_pdf_with_markitdown(file_path: str) -> Tuple[Optional[str], Optional[str]]:
"""使用 MarkItDown 库解析 PDF 文件"""
return parse_with_markitdown(file_path)
def parse_pdf_with_pypdf(file_path: str) -> Tuple[Optional[str], Optional[str]]:
"""使用 pypdf 库解析 PDF 文件"""
try:
from pypdf import PdfReader
except ImportError:
return None, "pypdf 库未安装"
try:
reader = PdfReader(file_path)
md_content = []
for page in reader.pages:
text = page.extract_text(extraction_mode="plain")
if text and text.strip():
md_content.append(text.strip())
md_content.append("")
content = "\n".join(md_content).strip()
if not content:
return None, "文档为空"
return content, None
except Exception as e:
return None, f"pypdf 解析失败: {str(e)}"

View File

@@ -1,330 +0,0 @@
#!/usr/bin/env python3
"""PPTX 文件解析模块,提供三种解析方法。"""
import re
import xml.etree.ElementTree as ET
import zipfile
from typing import Any, List, Optional, Tuple
from common import (
_unstructured_elements_to_markdown,
build_markdown_table,
flush_list_stack,
parse_with_docling,
parse_with_markitdown,
)
def parse_pptx_with_docling(file_path: str) -> Tuple[Optional[str], Optional[str]]:
"""使用 docling 库解析 PPTX 文件"""
return parse_with_docling(file_path)
def parse_pptx_with_unstructured(file_path: str) -> Tuple[Optional[str], Optional[str]]:
"""使用 unstructured 库解析 PPTX 文件"""
try:
from unstructured.partition.pptx import partition_pptx
except ImportError:
return None, "unstructured 库未安装"
try:
elements = partition_pptx(
filename=file_path, infer_table_structure=True, include_metadata=True
)
content = _unstructured_elements_to_markdown(elements)
if not content.strip():
return None, "文档为空"
return content, None
except Exception as e:
return None, f"unstructured 解析失败: {str(e)}"
def parse_pptx_with_markitdown(file_path: str) -> Tuple[Optional[str], Optional[str]]:
"""使用 MarkItDown 库解析 PPTX 文件"""
return parse_with_markitdown(file_path)
def extract_formatted_text_pptx(runs: List[Any]) -> str:
"""从 PPTX 文本运行中提取带有格式的文本"""
result = []
for run in runs:
if not run.text:
continue
text = run.text
font = run.font
is_bold = getattr(font, "bold", False) or False
is_italic = getattr(font, "italic", False) or False
if is_bold and is_italic:
text = f"***{text}***"
elif is_bold:
text = f"**{text}**"
elif is_italic:
text = f"*{text}*"
result.append(text)
return "".join(result).strip()
def convert_table_to_md_pptx(table: Any) -> str:
"""将 PPTX 表格转换为 Markdown 格式"""
rows_data = []
for row in table.rows:
row_data = []
for cell in row.cells:
cell_content = []
for para in cell.text_frame.paragraphs:
text = extract_formatted_text_pptx(para.runs)
if text:
cell_content.append(text)
cell_text = " ".join(cell_content).strip()
row_data.append(cell_text if cell_text else "")
rows_data.append(row_data)
return build_markdown_table(rows_data)
def parse_pptx_with_python_pptx(file_path: str) -> Tuple[Optional[str], Optional[str]]:
"""使用 python-pptx 库解析 PPTX 文件"""
try:
from pptx import Presentation
from pptx.enum.shapes import MSO_SHAPE_TYPE
except ImportError:
return None, "python-pptx 库未安装"
_A_NS = {"a": "http://schemas.openxmlformats.org/drawingml/2006/main"}
try:
prs = Presentation(file_path)
md_content = []
for slide_num, slide in enumerate(prs.slides, 1):
md_content.append(f"\n## Slide {slide_num}\n")
list_stack = []
for shape in slide.shapes:
if shape.shape_type == MSO_SHAPE_TYPE.PICTURE:
continue
if hasattr(shape, "has_table") and shape.has_table:
if list_stack:
flush_list_stack(list_stack, md_content)
table_md = convert_table_to_md_pptx(shape.table)
md_content.append(table_md)
if hasattr(shape, "text_frame"):
for para in shape.text_frame.paragraphs:
pPr = para._element.pPr
is_list = False
if pPr is not None:
is_list = (
para.level > 0
or pPr.find(".//a:buChar", namespaces=_A_NS) is not None
or pPr.find(".//a:buAutoNum", namespaces=_A_NS) is not None
)
if is_list:
level = para.level
while len(list_stack) <= level:
list_stack.append("")
text = extract_formatted_text_pptx(para.runs)
if text:
is_ordered = (
pPr is not None
and pPr.find(".//a:buAutoNum", namespaces=_A_NS) is not None
)
marker = "1. " if is_ordered else "- "
indent = " " * level
list_stack[level] = f"{indent}{marker}{text}"
for i in range(len(list_stack)):
if list_stack[i]:
md_content.append(list_stack[i] + "\n")
list_stack[i] = ""
else:
if list_stack:
flush_list_stack(list_stack, md_content)
text = extract_formatted_text_pptx(para.runs)
if text:
md_content.append(f"{text}\n")
if list_stack:
flush_list_stack(list_stack, md_content)
md_content.append("---\n")
content = "\n".join(md_content)
if not content.strip():
return None, "文档为空"
return content, None
except Exception as e:
return None, f"python-pptx 解析失败: {str(e)}"
def parse_pptx_with_xml(file_path: str) -> Tuple[Optional[str], Optional[str]]:
"""使用 XML 原生解析 PPTX 文件"""
pptx_namespace = {
"a": "http://schemas.openxmlformats.org/drawingml/2006/main",
"p": "http://schemas.openxmlformats.org/presentationml/2006/main",
"r": "http://schemas.openxmlformats.org/officeDocument/2006/relationships",
}
def extract_text_with_formatting_xml(text_elem: Any, namespaces: dict) -> str:
result = []
runs = text_elem.findall(".//a:r", namespaces=namespaces)
for run in runs:
t_elem = run.find(".//a:t", namespaces=namespaces)
if t_elem is None or not t_elem.text:
continue
text = t_elem.text
rPr = run.find(".//a:rPr", namespaces=namespaces)
is_bold = False
is_italic = False
if rPr is not None:
is_bold = rPr.find(".//a:b", namespaces=namespaces) is not None
is_italic = rPr.find(".//a:i", namespaces=namespaces) is not None
if is_bold and is_italic:
text = f"***{text}***"
elif is_bold:
text = f"**{text}**"
elif is_italic:
text = f"*{text}*"
result.append(text)
return "".join(result).strip() if result else ""
def convert_table_to_md_xml(table_elem: Any, namespaces: dict) -> str:
rows = table_elem.findall(".//a:tr", namespaces=namespaces)
if not rows:
return ""
rows_data = []
for row in rows:
cells = row.findall(".//a:tc", namespaces=namespaces)
row_data = []
for cell in cells:
cell_text = extract_text_with_formatting_xml(cell, namespaces)
if cell_text:
cell_text = cell_text.replace("\n", " ").replace("\r", "")
row_data.append(cell_text if cell_text else "")
rows_data.append(row_data)
return build_markdown_table(rows_data)
def is_list_item_xml(p_elem: Any, namespaces: dict) -> Tuple[bool, bool]:
if p_elem is None:
return False, False
pPr = p_elem.find(".//a:pPr", namespaces=namespaces)
if pPr is None:
return False, False
buChar = pPr.find(".//a:buChar", namespaces=namespaces)
if buChar is not None:
return True, False
buAutoNum = pPr.find(".//a:buAutoNum", namespaces=namespaces)
if buAutoNum is not None:
return True, True
return False, False
def get_indent_level_xml(p_elem: Any, namespaces: dict) -> int:
if p_elem is None:
return 0
pPr = p_elem.find(".//a:pPr", namespaces=namespaces)
if pPr is None:
return 0
lvl = pPr.get("lvl")
return int(lvl) if lvl else 0
try:
md_content = []
with zipfile.ZipFile(file_path) as zip_file:
slide_files = [
f
for f in zip_file.namelist()
if re.match(r"ppt/slides/slide\d+\.xml$", f)
]
slide_files.sort(
key=lambda f: int(re.search(r"slide(\d+)\.xml$", f).group(1))
)
for slide_idx, slide_file in enumerate(slide_files, 1):
md_content.append("\n## Slide {}\n".format(slide_idx))
with zip_file.open(slide_file) as slide_xml:
slide_root = ET.parse(slide_xml).getroot()
tx_bodies = slide_root.findall(
".//p:sp/p:txBody", namespaces=pptx_namespace
)
tables = slide_root.findall(".//a:tbl", namespaces=pptx_namespace)
for table in tables:
table_md = convert_table_to_md_xml(table, pptx_namespace)
if table_md:
md_content.append(table_md)
for tx_body in tx_bodies:
paragraphs = tx_body.findall(
".//a:p", namespaces=pptx_namespace
)
list_stack = []
for para in paragraphs:
is_list, is_ordered = is_list_item_xml(para, pptx_namespace)
if is_list:
level = get_indent_level_xml(para, pptx_namespace)
while len(list_stack) <= level:
list_stack.append("")
text = extract_text_with_formatting_xml(
para, pptx_namespace
)
if text:
marker = "1. " if is_ordered else "- "
indent = " " * level
list_stack[level] = f"{indent}{marker}{text}"
for i in range(len(list_stack)):
if list_stack[i]:
md_content.append(list_stack[i] + "\n")
list_stack[i] = ""
else:
if list_stack:
flush_list_stack(list_stack, md_content)
text = extract_text_with_formatting_xml(
para, pptx_namespace
)
if text:
md_content.append(f"{text}\n")
if list_stack:
flush_list_stack(list_stack, md_content)
md_content.append("---\n")
content = "\n".join(md_content)
if not content.strip():
return None, "文档为空"
return content, None
except Exception as e:
return None, f"XML 解析失败: {str(e)}"

View File

@@ -1,286 +0,0 @@
#!/usr/bin/env python3
"""XLSX 文件解析模块,提供三种解析方法。"""
import xml.etree.ElementTree as ET
import zipfile
from typing import List, Optional, Tuple
from common import _unstructured_elements_to_markdown, parse_with_docling, parse_with_markitdown
def parse_xlsx_with_docling(file_path: str) -> Tuple[Optional[str], Optional[str]]:
"""使用 docling 库解析 XLSX 文件"""
return parse_with_docling(file_path)
def parse_xlsx_with_unstructured(file_path: str) -> Tuple[Optional[str], Optional[str]]:
"""使用 unstructured 库解析 XLSX 文件"""
try:
from unstructured.partition.xlsx import partition_xlsx
except ImportError:
return None, "unstructured 库未安装"
try:
elements = partition_xlsx(filename=file_path, infer_table_structure=True)
content = _unstructured_elements_to_markdown(elements)
if not content.strip():
return None, "文档为空"
return content, None
except Exception as e:
return None, f"unstructured 解析失败: {str(e)}"
def parse_xlsx_with_markitdown(file_path: str) -> Tuple[Optional[str], Optional[str]]:
"""使用 MarkItDown 库解析 XLSX 文件"""
return parse_with_markitdown(file_path)
def parse_xlsx_with_pandas(file_path: str) -> Tuple[Optional[str], Optional[str]]:
"""使用 pandas 库解析 XLSX 文件"""
try:
import pandas as pd
from tabulate import tabulate
except ImportError as e:
missing_lib = "pandas" if "pandas" in str(e) else "tabulate"
return None, f"{missing_lib} 库未安装"
try:
sheets = pd.read_excel(file_path, sheet_name=None)
markdown_parts = []
for sheet_name, df in sheets.items():
if len(df) == 0:
markdown_parts.append(f"## {sheet_name}\n\n*工作表为空*")
continue
table_md = tabulate(
df, headers="keys", tablefmt="pipe", showindex=True, missingval=""
)
markdown_parts.append(f"## {sheet_name}\n\n{table_md}")
if not markdown_parts:
return None, "Excel 文件为空"
markdown_content = "# Excel数据转换结果\n\n" + "\n\n".join(markdown_parts)
return markdown_content, None
except Exception as e:
return None, f"pandas 解析失败: {str(e)}"
def parse_xlsx_with_xml(file_path: str) -> Tuple[Optional[str], Optional[str]]:
"""使用 XML 原生解析 XLSX 文件"""
xlsx_namespace = {
"main": "http://schemas.openxmlformats.org/spreadsheetml/2006/main"
}
def parse_col_index(cell_ref: str) -> int:
col_index = 0
for char in cell_ref:
if char.isalpha():
col_index = col_index * 26 + (ord(char) - ord("A") + 1)
else:
break
return col_index - 1
def parse_cell_value(cell: ET.Element, shared_strings: List[str]) -> str:
cell_type = cell.attrib.get("t")
if cell_type == "inlineStr":
is_elem = cell.find("main:is", xlsx_namespace)
if is_elem is not None:
t_elem = is_elem.find("main:t", xlsx_namespace)
if t_elem is not None and t_elem.text:
return t_elem.text.replace("\n", " ").replace("\r", "")
return ""
cell_value_elem = cell.find("main:v", xlsx_namespace)
if cell_value_elem is None or not cell_value_elem.text:
return ""
cell_value = cell_value_elem.text
if cell_type == "s":
try:
idx = int(cell_value)
if 0 <= idx < len(shared_strings):
text = shared_strings[idx]
return text.replace("\n", " ").replace("\r", "")
except (ValueError, IndexError):
pass
return ""
elif cell_type == "b":
return "TRUE" if cell_value == "1" else "FALSE"
elif cell_type == "str":
return cell_value.replace("\n", " ").replace("\r", "")
elif cell_type == "e":
_ERROR_CODES = {
"#NULL!": "空引用错误",
"#DIV/0!": "除零错误",
"#VALUE!": "值类型错误",
"#REF!": "无效引用",
"#NAME?": "名称错误",
"#NUM!": "数值错误",
"#N/A": "值不可用",
}
return _ERROR_CODES.get(cell_value, f"错误: {cell_value}")
elif cell_type == "d":
return f"[日期] {cell_value}"
elif cell_type == "n":
return cell_value
elif cell_type is None:
try:
float_val = float(cell_value)
if float_val.is_integer():
return str(int(float_val))
return cell_value
except ValueError:
return cell_value
else:
return cell_value
def get_non_empty_columns(data: List[List[str]]) -> set:
non_empty_cols = set()
for row in data:
for col_idx, cell in enumerate(row):
if cell and cell.strip():
non_empty_cols.add(col_idx)
return non_empty_cols
def filter_columns(row: List[str], non_empty_cols: set) -> List[str]:
return [row[i] if i < len(row) else "" for i in sorted(non_empty_cols)]
def data_to_markdown(data: List[List[str]], sheet_name: str) -> str:
if not data or not data[0]:
return f"## {sheet_name}\n\n*工作表为空*"
md_lines = []
md_lines.append(f"## {sheet_name}")
md_lines.append("")
headers = data[0]
non_empty_cols = get_non_empty_columns(data)
if not non_empty_cols:
return f"## {sheet_name}\n\n*工作表为空*"
filtered_headers = filter_columns(headers, non_empty_cols)
header_line = "| " + " | ".join(filtered_headers) + " |"
md_lines.append(header_line)
separator_line = "| " + " | ".join(["---"] * len(filtered_headers)) + " |"
md_lines.append(separator_line)
for row in data[1:]:
filtered_row = filter_columns(row, non_empty_cols)
row_line = "| " + " | ".join(filtered_row) + " |"
md_lines.append(row_line)
md_lines.append("")
return "\n".join(md_lines)
try:
with zipfile.ZipFile(file_path, "r") as zip_file:
sheet_names = []
sheet_rids = []
try:
with zip_file.open("xl/workbook.xml") as f:
root = ET.parse(f).getroot()
rel_ns = "http://schemas.openxmlformats.org/officeDocument/2006/relationships"
sheet_elements = root.findall(".//main:sheet", xlsx_namespace)
for sheet in sheet_elements:
sheet_name = sheet.attrib.get("name", "")
rid = sheet.attrib.get(f"{{{rel_ns}}}id", "")
if sheet_name:
sheet_names.append(sheet_name)
sheet_rids.append(rid)
except KeyError:
return None, "无法解析工作表名称"
if not sheet_names:
return None, "未找到工作表"
rid_to_target = {}
try:
rels_ns = "http://schemas.openxmlformats.org/package/2006/relationships"
with zip_file.open("xl/_rels/workbook.xml.rels") as f:
rels_root = ET.parse(f).getroot()
for rel in rels_root.findall(f"{{{rels_ns}}}Relationship"):
rid = rel.attrib.get("Id", "")
target = rel.attrib.get("Target", "")
if rid and target:
rid_to_target[rid] = target
except KeyError:
pass
shared_strings = []
try:
with zip_file.open("xl/sharedStrings.xml") as f:
root = ET.parse(f).getroot()
for si in root.findall(".//main:si", xlsx_namespace):
t_elem = si.find(".//main:t", xlsx_namespace)
if t_elem is not None and t_elem.text:
shared_strings.append(t_elem.text)
else:
shared_strings.append("")
except KeyError:
pass
markdown_content = "# Excel数据转换结果 (原生XML解析)\n\n"
for sheet_index, sheet_name in enumerate(sheet_names):
rid = sheet_rids[sheet_index] if sheet_index < len(sheet_rids) else ""
target = rid_to_target.get(rid, "")
if target:
if target.startswith("/"):
worksheet_path = target.lstrip("/")
else:
worksheet_path = f"xl/{target}"
else:
worksheet_path = f"xl/worksheets/sheet{sheet_index + 1}.xml"
try:
with zip_file.open(worksheet_path) as f:
root = ET.parse(f).getroot()
sheet_data = root.find("main:sheetData", xlsx_namespace)
rows = []
if sheet_data is not None:
row_elements = sheet_data.findall(
"main:row", xlsx_namespace
)
for row_elem in row_elements:
cells = row_elem.findall("main:c", xlsx_namespace)
col_dict = {}
for cell in cells:
cell_ref = cell.attrib.get("r", "")
if not cell_ref:
continue
col_index = parse_col_index(cell_ref)
cell_value = parse_cell_value(cell, shared_strings)
col_dict[col_index] = cell_value
if col_dict:
max_col = max(col_dict.keys())
row_data = [
col_dict.get(i, "") for i in range(max_col + 1)
]
rows.append(row_data)
table_md = data_to_markdown(rows, sheet_name)
markdown_content += table_md + "\n\n"
except KeyError:
markdown_content += f"## {sheet_name}\n\n*工作表解析失败*\n\n"
if not markdown_content.strip():
return None, "解析结果为空"
return markdown_content, None
except Exception as e:
return None, f"XML 解析失败: {str(e)}"

View File

@@ -8,319 +8,69 @@ compatibility: Requires Bun runtime (https://bun.sh)
基于 Bun 的 JavaScript/TypeScript 执行技能,提供隔离的脚本执行和自动依赖管理。
## 快速参考
## Purpose
根据您的需求选择使用方式:
**必需依赖**: 此 skill 需要 Bun 运行时,不兼容其他 JavaScript 运行时。
| 场景 | 描述 | 命令 |
| --------- | ------------------------ | ---------------------------------- |
| **场景1** | 直接执行已存在的脚本文件 | `bun <script-file>` |
| **场景2** | 在指定路径创建脚本并执行 | 使用 Write 工具创建 → `bun <path>` |
| **场景3** | 使用临时路径执行(默认) | 生成临时路径 → `bun <temp-file>` |
**重要提示:** 所有场景在执行脚本前都必须先检查 Bun 环境:`bun --version`
## 前置条件
### 安装 Bun
lyxy-runner-js 需要安装 Bun。Bun 是一个快速的 JavaScript 运行时,内置包管理器。
**macOS/Linux:**
```bash
curl -fsSL https://bun.sh/install | bash
```
**Windows:**
```powershell
powershell -c "irm bun.sh/install.ps1 | iex"
```
**验证安装:**
```bash
bun --version
```
## 使用流程
**执行流程决策树:**
1. **步骤1检查 Bun 环境**
- 执行:`bun --version`
- 失败 → 输出错误信息(包含安装说明)并停止执行
- 成功 → 进入下一步
- **重要:** 禁止使用 nodejs、npm、yarn、pnpm 等其他工具
2. **步骤2选择执行场景**
- 场景1用户提供了已存在的脚本文件路径
- 是 → 直接执行:`bun <script-file>`
- 否 → 进入下一步
- 场景2用户指定了脚本的生成路径
- 是 → 使用 Write 工具创建脚本,然后执行
- 否 → 进入场景3
3. **场景3默认**:使用临时路径执行
- 生成临时文件路径
- 将脚本内容写入临时文件
- 使用 Bun 运行脚本
- 临时文件由系统自动处理
---
### 场景1执行已存在的脚本文件
```bash
# 步骤 1: 检查 Bun 是否已安装
bun --version
# 步骤 2: 直接执行已存在的脚本
bun ./scripts/my-script.js
# 脚本的输出将自动显示
```
**关键特点:**
-**无需生成临时文件** - 直接执行用户提供的脚本
-**保持脚本位置** - 脚本留在原位置,不会被移动或复制
-**简洁快速** - 跳过文件生成步骤,直接执行
### 场景2在指定路径创建并执行脚本
```bash
# 步骤 1: 检查 Bun 是否已安装
bun --version
# 步骤 2: 使用 Write 工具在指定路径创建脚本
# (以下步骤由大模型使用 Write 工具完成)
# write content to "./scripts/new-script.js"
const greeting = "Hello from custom path!";
console.log(greeting);
# 步骤 3: 执行脚本
bun ./scripts/new-script.js
```
**关键特点:**
-**自定义路径** - 脚本创建到用户指定的位置
-**持久化存储** - 脚本文件保存在指定位置,不会被自动清理
-**灵活控制** - 用户可以精确控制脚本位置和命名
### 场景3使用临时路径执行默认流程
当用户未提供任何路径信息时,使用临时路径执行脚本(默认流程):
#### 基础示例
```bash
# 步骤 1: 检查 Bun 是否已安装
bun --version
# 步骤 2: 生成临时文件路径
TEMP_FILE=$(bun skills/lyxy-runner-js/scripts/get_temp_path.js js)
# 步骤 3: 将脚本内容写入临时文件
cat <<EOF > "$TEMP_FILE"
const greeting = "Hello from lyxy-runner-js!";
console.log(greeting);
EOF
# 步骤 4: 执行脚本
bun "$TEMP_FILE"
# 步骤 5: 输出已在上面捕获
# 临时文件将由系统自动清理
```
#### TypeScript 示例
```bash
# 生成 TypeScript 临时文件
TEMP_TS=$(bun skills/lyxy-runner-js/scripts/get_temp_path.js ts)
# 写入 TypeScript 脚本
cat <<EOF > "$TEMP_TS"
const message: string = 'TypeScript execution';
console.log(message);
EOF
# 执行 - Bun 会自动转译 TypeScript
bun "$TEMP_TS"
```
**Bun 自动处理:**
- 检测 `import` 语句
Bun 特性:
- 自动检测和下载依赖(无需 package.json
- 即时转译 TypeScript
- 下载并缓存依赖(到 `~/.bun/install/cache`
- 无需 `package.json` 或手动安装
- 跨平台兼容Windows/macOS/Linux
## 依赖管理
**重要**: 如果 Bun 未安装,立即停止任务并引导用户安装。禁止使用 nodejs、npm、yarn、pnpm 等替代工具。
Bun 提供自动依赖管理,无需手动配置:
## When to Use
### 导入外部包
任何 JavaScript/TypeScript 处理任务都应使用此 skill。
```javascript
// ESM import推荐
import axios from 'axios'
import lodash from 'lodash'
### 典型场景
- **数据处理**: JSON/CSV 解析、数据转换
- **API 交互**: HTTP 请求、Web API 调用
- **文件操作**: 文件读写、批量处理
- **脚本自动化**: 构建脚本、任务自动化
// CommonJS import也支持
const axios = require('axios')
```
### 不适用场景
- ✗ 需要 Node.js 特定 API如部分原生模块
- ✗ 需要持久化进程(如服务器)
首次执行带有外部导入的脚本时Bun 会:
## Quick Reference
1. 分析导入
2. 从 npm 下载缺失的依赖
3. 全局缓存到 `~/.bun/install/cache`
4. 后续运行使用缓存版本
| 场景 | 描述 | 命令 |
|------|------|------|
| 场景1 | 直接执行已存在的脚本 | `bun <script-file>` |
| 场景2 | 在指定路径创建脚本并执行 | 使用 Write 工具创建 → `bun <path>` |
| 场景3 | 使用临时路径执行(默认) | 生成临时路径 → `bun <temp-file>` |
### 不需要 package.json
**重要**: 所有场景执行前必须先检查 Bun 环境:`bun --version`
与 Node.js 不同,你无需创建 `package.json` 或单独运行 `bun install`。Bun 在运行时自动处理所有操作。
## Workflow
## 辅助函数 API
1. **检查 Bun 环境**:执行 `bun --version`,失败则停止并提示安装
2. **选择执行场景**根据用户意图选择场景1/2/3
3. **执行脚本**:使用 `bun <script>` 运行
4. **捕获输出**stdout/stderr 分别处理
### `get_temp_path.js`
为脚本执行生成唯一的临时文件路径。
**CLI 使用方式:**
### 临时路径执行场景3
```bash
bun skills/lyxy-runner-js/scripts/get_temp_path.js <extension>
# 生成临时文件路径
TEMP_FILE=$(bun scripts/get_temp_path.js js)
# 写入脚本内容
cat <<EOF > "$TEMP_FILE"
console.log("Hello from lyxy-runner-js!");
EOF
# 执行脚本
bun "$TEMP_FILE"
```
**参数:**
## References
- `extension` (可选): 文件扩展名。默认为 `js`。常用值: `js`, `ts`, `mjs`, `mts`
详细文档请参阅 `references/` 目录:
**输出:** 返回类似 `/var/folders/.../lyxy-runner-js-1234567890-abc123.js` 的路径
**路径格式:**
- 使用操作系统临时目录Unix 上为 `/tmp`Windows 上为 `%TEMP%`
- 前缀: `lyxy-runner-js-`
- 时间戳: 自纪元以来的毫秒数
- 随机字符串: 7 字符字母数字
- 扩展名: 参数中提供的值
**示例:**
```bash
$ bun skills/lyxy-runner-js/scripts/get_temp_path.js js
/var/folders/8m/0hm18pdd7ts2bwp0530drz500000gn/T/lyxy-runner-js-1770257905333-na6ujx.js
$ bun skills/lyxy-runner-js/scripts/get_temp_path.js ts
/var/folders/8m/0hm18pdd7ts2bwp0530drz500000gn/T/lyxy-runner-js-1770257905333-v8yzt.ts
```
## 错误处理
### 未安装 Bun
**症状:** `bun --version` 失败或返回 "command not found: bun"
**错误处理:**
当检测到 Bun 未安装时,必须:
1. **停止执行** - 不进行任何后续操作
2. **输出明确错误信息** - 清晰说明 "Bun 运行时未安装" 或类似提示
3. **提供安装说明** - 参考"前置条件"章节的安装命令
**重要限制:**
-**禁止自动安装** - 不要尝试自动安装 Bun由用户自行决定
-**禁止使用其他工具** - 不要尝试使用 nodejs、npm、yarn、pnpm 等其他 JavaScript 运行时或包管理工具
-**禁止格式转换** - 不要建议用户将脚本转换为其他运行时格式
**正确做法:**
- ✅ 仅输出错误信息和安装说明
- ✅ 等待用户安装 Bun 后再继续
- ✅ 保持使用 Bun 作为唯一运行时
### 脚本语法错误
Bun 提供详细的语法错误信息:
```bash
$ bun "$TEMP_FILE"
error: Unexpected token
--> /var/folders/.../script.js:2:10
|
2 | const = 123;
| ^
```
错误信息包括:
- 文件路径和行号
- 错误的确切位置
- 问题描述
### 运行时错误
运行时错误包含完整的堆栈跟踪:
```bash
$ bun "$TEMP_FILE"
ReferenceError: foo is not defined
at script.js:3:5
at main (script.js:1:1)
```
### 其他错误
其他任何形式的错误都原样输出
## 输出处理
### 标准输出
所有 `console.log()`, `console.info()`, `console.warn()` 输出都到 stdout
```bash
bun "$TEMP_FILE" # stdout 由调用代码捕获
```
### 错误输出
`console.error()` 输出到 stderr
```bash
bun "$TEMP_FILE" 2>error.log # 单独捕获 stderr
```
### 退出码
脚本可以设置自定义退出码:
```javascript
process.exit(1) // 错误
process.exit(0) // 成功
```
调用者接收这些退出码以确定执行状态。
## 临时文件管理
执行后 **不会主动删除** 临时文件。这是设计使然:
- 操作系统自动管理临时目录空间
- 文件可以保留用于调试目的
- 大多数操作系统定期清理旧的临时文件
## 最佳实践
1. **始终先检查 Bun 环境** - 所有场景第一步都执行 `bun --version`
2. **根据用户意图选择场景** - 查看快速参考选择合适的使用方式
3. **单独处理 stdout/stderr** - 以区分输出和错误
4. **检查退出码** - 以检测脚本失败
5. **使用 ESM imports** - 使用 `import from` 编写现代 JavaScript
6. **捕获并显示错误** - 以帮助用户调试问题
| 文件 | 内容 |
|------|------|
| `references/examples.md` | 各场景完整示例、TypeScript 示例、依赖管理示例 |
| `references/error-handling.md` | Bun 未安装、语法错误、运行时错误处理 |
| `references/best-practices.md` | 输出处理、临时文件管理、辅助函数 API |

View File

@@ -0,0 +1,83 @@
# 最佳实践和输出处理
## 最佳实践
1. **始终先检查 Bun 环境** - 所有场景第一步都执行 `bun --version`
2. **根据用户意图选择场景** - 查看快速参考选择合适的使用方式
3. **单独处理 stdout/stderr** - 以区分输出和错误
4. **检查退出码** - 以检测脚本失败
5. **使用 ESM imports** - 使用 `import from` 编写现代 JavaScript
6. **捕获并显示错误** - 以帮助用户调试问题
## 输出处理
### 标准输出
所有 `console.log()`, `console.info()`, `console.warn()` 输出都到 stdout
```bash
bun "$TEMP_FILE" # stdout 由调用代码捕获
```
### 错误输出
`console.error()` 输出到 stderr
```bash
bun "$TEMP_FILE" 2>error.log # 单独捕获 stderr
```
### 退出码
脚本可以设置自定义退出码:
```javascript
process.exit(1) // 错误
process.exit(0) // 成功
```
调用者接收这些退出码以确定执行状态。
## 临时文件管理
执行后 **不会主动删除** 临时文件。这是设计使然:
- 操作系统自动管理临时目录空间
- 文件可以保留用于调试目的
- 大多数操作系统定期清理旧的临时文件
## 辅助函数 API
### `get_temp_path.js`
为脚本执行生成唯一的临时文件路径。
**CLI 使用方式:**
```bash
bun scripts/get_temp_path.js <extension>
```
**参数:**
- `extension` (可选): 文件扩展名。默认为 `js`。常用值: `js`, `ts`, `mjs`, `mts`
**输出:** 返回类似 `/var/folders/.../lyxy-runner-js-1234567890-abc123.js` 的路径
**路径格式:**
- 使用操作系统临时目录Unix 上为 `/tmp`Windows 上为 `%TEMP%`
- 前缀: `lyxy-runner-js-`
- 时间戳: 自纪元以来的毫秒数
- 随机字符串: 7 字符字母数字
- 扩展名: 参数中提供的值
**示例:**
```bash
$ bun scripts/get_temp_path.js js
/var/folders/8m/0hm18pdd7ts2bwp0530drz500000gn/T/lyxy-runner-js-1770257905333-na6ujx.js
$ bun scripts/get_temp_path.js ts
/var/folders/8m/0hm18pdd7ts2bwp0530drz500000gn/T/lyxy-runner-js-1770257905333-v8yzt.ts
```

View File

@@ -0,0 +1,71 @@
# 错误处理
## 未安装 Bun
**症状:** `bun --version` 失败或返回 "command not found: bun"
**错误处理:**
当检测到 Bun 未安装时,必须:
1. **停止执行** - 不进行任何后续操作
2. **输出明确错误信息** - 清晰说明 "Bun 运行时未安装" 或类似提示
3. **提供安装说明** - 参考下方安装命令
**安装 Bun**
**macOS/Linux:**
```bash
curl -fsSL https://bun.sh/install | bash
```
**Windows:**
```powershell
powershell -c "irm bun.sh/install.ps1 | iex"
```
**重要限制:**
-**禁止自动安装** - 不要尝试自动安装 Bun由用户自行决定
-**禁止使用其他工具** - 不要尝试使用 nodejs、npm、yarn、pnpm 等其他 JavaScript 运行时或包管理工具
-**禁止格式转换** - 不要建议用户将脚本转换为其他运行时格式
**正确做法:**
- ✅ 仅输出错误信息和安装说明
- ✅ 等待用户安装 Bun 后再继续
- ✅ 保持使用 Bun 作为唯一运行时
## 脚本语法错误
Bun 提供详细的语法错误信息:
```bash
$ bun "$TEMP_FILE"
error: Unexpected token
--> /var/folders/.../script.js:2:10
|
2 | const = 123;
| ^
```
错误信息包括:
- 文件路径和行号
- 错误的确切位置
- 问题描述
## 运行时错误
运行时错误包含完整的堆栈跟踪:
```bash
$ bun "$TEMP_FILE"
ReferenceError: foo is not defined
at script.js:3:5
at main (script.js:1:1)
```
## 其他错误
其他任何形式的错误都原样输出。

View File

@@ -0,0 +1,104 @@
# 示例
## 场景1执行已存在的脚本文件
```bash
# 步骤 1: 检查 Bun 是否已安装
bun --version
# 步骤 2: 直接执行已存在的脚本
bun ./scripts/my-script.js
# 脚本的输出将自动显示
```
**关键特点:**
-**无需生成临时文件** - 直接执行用户提供的脚本
-**保持脚本位置** - 脚本留在原位置,不会被移动或复制
-**简洁快速** - 跳过文件生成步骤,直接执行
## 场景2在指定路径创建并执行脚本
```bash
# 步骤 1: 检查 Bun 是否已安装
bun --version
# 步骤 2: 使用 Write 工具在指定路径创建脚本
# (以下步骤由大模型使用 Write 工具完成)
# write content to "./scripts/new-script.js"
const greeting = "Hello from custom path!";
console.log(greeting);
# 步骤 3: 执行脚本
bun ./scripts/new-script.js
```
**关键特点:**
-**自定义路径** - 脚本创建到用户指定的位置
-**持久化存储** - 脚本文件保存在指定位置,不会被自动清理
-**灵活控制** - 用户可以精确控制脚本位置和命名
## 场景3使用临时路径执行默认流程
### 基础示例
```bash
# 步骤 1: 检查 Bun 是否已安装
bun --version
# 步骤 2: 生成临时文件路径
TEMP_FILE=$(bun scripts/get_temp_path.js js)
# 步骤 3: 将脚本内容写入临时文件
cat <<EOF > "$TEMP_FILE"
const greeting = "Hello from lyxy-runner-js!";
console.log(greeting);
EOF
# 步骤 4: 执行脚本
bun "$TEMP_FILE"
# 步骤 5: 输出已在上面捕获
# 临时文件将由系统自动清理
```
### TypeScript 示例
```bash
# 生成 TypeScript 临时文件
TEMP_TS=$(bun scripts/get_temp_path.js ts)
# 写入 TypeScript 脚本
cat <<EOF > "$TEMP_TS"
const message: string = 'TypeScript execution';
console.log(message);
EOF
# 执行 - Bun 会自动转译 TypeScript
bun "$TEMP_TS"
```
## 依赖管理示例
### 导入外部包
```javascript
// ESM import推荐
import axios from 'axios'
import lodash from 'lodash'
// CommonJS import也支持
const axios = require('axios')
```
首次执行带有外部导入的脚本时Bun 会:
1. 分析导入
2. 从 npm 下载缺失的依赖
3. 全局缓存到 `~/.bun/install/cache`
4. 后续运行使用缓存版本
### 不需要 package.json
与 Node.js 不同,你无需创建 `package.json` 或单独运行 `bun install`。Bun 在运行时自动处理所有操作。

View File

@@ -38,55 +38,7 @@ description: Any task that requires Python processing should use this skill.
- ✗ 需要命令行参数
- ✗ 需要从stdin读取
## Automatic Dependency Parsing
分析import语句提取外部包名排除标准库。
### 标准库(排除)
**核心**: os, sys, pathlib, shutil, json, csv, re, datetime, math
**网络**: http.client, urllib, socket, io, logging
**高级**: itertools, functools, typing, dataclasses, enum
### 解析规则
1. 提取:`import pandas``pandas`, `from numpy import array``numpy`
2. 排除标准库
3. 去重
### 示例
```python
import pandas as pd
import numpy as np
import json # 标准库,排除
from pathlib import Path # 标准库,排除
# 结果: [pandas, numpy]
```
## Smart Project Detection
### 检测命令
```bash
uv sync --dry-run
```
### 判断逻辑
- Exit code 0 → uv项目
- 非零退出码 → 非uv项目
- 失败 → 回退到非uv项目模式使用`--with`),不阻塞执行
## Path Handling
### 三层逻辑
1. **用户指定存储路径** → 写入指定路径
2. **用户指定现有脚本** → 直接执行
3. **未指定** → 临时文件
```bash
# 临时文件获取
temp_file_path=$(uv run ./scripts/get_temp_path.py)
```
## Execution Commands
## Quick Reference
| 场景 | 命令 |
|------|------|
@@ -94,154 +46,20 @@ temp_file_path=$(uv run ./scripts/get_temp_path.py)
| 非uv+有依赖 | `uv run --with pkg1 --with pkg2 <script_path>` |
| 非uv+无依赖 | `uv run <script_path>` |
**特点**:
- uv项目使用项目环境`--with`
- 非uv有依赖每个`--with`创建隔离环境
- 非uv无依赖使用标准Python环境
## Workflow
**步骤1**: 解析依赖(见"Automatic Dependency Parsing"
1. **解析依赖**分析import语句提取外部包名排除标准库
2. **检测项目**:执行`uv sync --dry-run`判断是否为uv项目
3. **确定路径**:用户指定路径 → 现有脚本 → 临时文件
4. **构造命令**:根据项目类型和依赖构造执行命令
5. **执行脚本**:运行并捕获输出
**步骤2**: 检测项目(见"Smart Project Detection"
## References
**步骤3**: 确定路径(见"Path Handling"
详细文档请参阅 `references/` 目录:
**步骤4**: 构造并执行命令(见"Execution Commands"
执行命令并捕获输出。
## Error Handling
### uv未安装
**检测**: `uv`命令失败
**错误消息**:
```
uv not found
此skill依赖uv工具运行Python脚本。
请安装uv: https://docs.astral.sh/uv/getting-started/installation/
安装命令示例:
curl -LsSf https://astral.sh/uv/install.sh | sh # Linux/macOS
powershell -c "irm https://astral.sh/uv/install.ps1 | iex" # Windows
```
**重要提示**:
- **立即停止任务**等待用户安装uv后再继续
- **不要尝试使用**python, pip, poetry, venv, virtualenv等
- **不要自动安装**uv
- 用户安装uv完成后可以重新执行任务
**操作**: 立即停止所有执行等待用户安装uv
### 其他错误
| 场景 | 错误消息 | 操作 |
|------|---------|------|
| 项目检测失败 | 回退到非uv模式使用`--with` | 警告后继续 |
| 依赖解析不准确 | 依赖可能不完整<br>Traceback: [traceback] | 停止,保留脚本调试 |
| 语法错误 | Python语法错误: [描述]<br>文件: <path><br>行号: <line> | 停止 |
| 路径权限问题 | 无法写入: <path><br>建议: 使用临时文件模式 | 回退到临时文件 |
## Examples
### 示例1: 数据分析
```python
import pandas as pd
import numpy as np
df = pd.read_csv('data.csv')
print(f"形状: {df.shape}")
print(df.describe())
```
**执行**: `uv run --with pandas --with numpy /tmp/script_xxx.py`
### 示例2: API交互
```python
import requests
resp = requests.get('https://api.github.com/repos/python/cpython')
data = resp.json()
print(f"仓库: {data['full_name']}, Stars: {data['stargazers_count']}")
```
**执行**: `uv run --with requests /tmp/script_xxx.py`
### 示例3: 文件操作
```python
import os
import glob
for i, file in enumerate(glob.glob('*.txt')):
os.rename(file, f"file_{i:03d}.txt")
```
**执行**: `uv run /tmp/script_xxx.py`(无依赖)
### 示例4: uv项目内执行
```python
import pandas as pd
from my_project import helper
df = pd.read_csv('data.csv')
result = helper.process(df)
print(result)
```
**执行**: `uv run scripts/data_process.py`(使用项目环境)
### 示例5: 用户指定路径
```python
import requests
resp = requests.get('https://api.example.com/data')
print(f"处理完成: {len(resp.json())}")
```
**执行**: `uv run --with requests scripts/api_analyzer.py`(写入指定路径)
## Notes
### 为什么使用uv
| 特性 | 优势 |
| 文件 | 内容 |
|------|------|
| 环境隔离 | 不污染系统Python |
| 自动依赖 | `--with`语法无需pip install |
| 快速启动 | 比venv快10-100倍 |
| 项目集成 | 自动检测uv项目 |
| 零配置 | 开箱即用无需PEP 723 |
### 最佳实践
1. **依赖解析**: 排除标准库失败时检查遗漏依赖复杂项目用uv项目模式
2. **路径**: 用户指定优先;临时文件用于自主生成;跨平台由辅助脚本保证
3. **错误**: 预期错误脚本内处理;意外错误立即停止;检测失败自动回退
4. **清理**: 临时文件使用系统目录,自动清理,失败时手动删除
### 限制
- ✗ 不支持命令行参数、stdin输入、持久化环境
- ✗ 使用uv默认Python版本项目可在pyproject.toml指定
- ✗ 依赖解析可能不完整(动态导入、条件导入可能遗漏)
- ✗ 项目检测可能误判(网络问题导致回退)
### uv工具要求
- **uv是此skill的必需依赖不可替代**
- **不支持**: python, pip, poetry, venv, virtualenv
- 如果检测到uv未安装必须停止任务并引导用户安装
- 不要尝试使用替代方案或自动安装uv
## Workflow Summary
**完整流程**:
1. 解析import语句提取外部包名排除标准库
2. 执行`uv sync --dry-run`检测项目
3. 确定脚本路径(用户指定/现有脚本/临时文件)
4. 构造执行命令(根据项目类型和依赖)
5. 执行并捕获输出
**关键特点**:
- 自动依赖解析分析import自动提取
- 智能项目检测自动识别uv项目
- 灵活路径:支持指定/现有/临时三种模式
- 跨平台自动适配Windows/macOS/Linux
- 环境隔离:独立虚拟环境
| `references/examples.md` | 数据分析、API交互、文件操作等完整示例 |
| `references/error-handling.md` | uv未安装、依赖解析失败等错误处理指南 |
| `references/best-practices.md` | 依赖解析、路径处理、项目检测等最佳实践 |

View File

@@ -0,0 +1,86 @@
# 最佳实践和注意事项
## 为什么使用uv
| 特性 | 优势 |
|------|------|
| 环境隔离 | 不污染系统Python |
| 自动依赖 | `--with`语法无需pip install |
| 快速启动 | 比venv快10-100倍 |
| 项目集成 | 自动检测uv项目 |
| 零配置 | 开箱即用无需PEP 723 |
## 最佳实践
1. **依赖解析**: 排除标准库失败时检查遗漏依赖复杂项目用uv项目模式
2. **路径**: 用户指定优先;临时文件用于自主生成;跨平台由辅助脚本保证
3. **错误**: 预期错误脚本内处理;意外错误立即停止;检测失败自动回退
4. **清理**: 临时文件使用系统目录,自动清理,失败时手动删除
## 限制
- ✗ 不支持命令行参数、stdin输入、持久化环境
- ✗ 使用uv默认Python版本项目可在pyproject.toml指定
- ✗ 依赖解析可能不完整(动态导入、条件导入可能遗漏)
- ✗ 项目检测可能误判(网络问题导致回退)
## uv工具要求
- **uv是此skill的必需依赖不可替代**
- **不支持**: python, pip, poetry, venv, virtualenv
- 如果检测到uv未安装必须停止任务并引导用户安装
- 不要尝试使用替代方案或自动安装uv
## 自动依赖解析详细说明
分析import语句提取外部包名排除标准库。
### 标准库(排除)
**核心**: os, sys, pathlib, shutil, json, csv, re, datetime, math
**网络**: http.client, urllib, socket, io, logging
**高级**: itertools, functools, typing, dataclasses, enum
### 解析规则
1. 提取:`import pandas``pandas`, `from numpy import array``numpy`
2. 排除标准库
3. 去重
### 示例
```python
import pandas as pd
import numpy as np
import json # 标准库,排除
from pathlib import Path # 标准库,排除
# 结果: [pandas, numpy]
```
## 智能项目检测
### 检测命令
```bash
uv sync --dry-run
```
### 判断逻辑
- Exit code 0 → uv项目
- 非零退出码 → 非uv项目
- 失败 → 回退到非uv项目模式使用`--with`),不阻塞执行
## 路径处理
### 三层逻辑
1. **用户指定存储路径** → 写入指定路径
2. **用户指定现有脚本** → 直接执行
3. **未指定** → 临时文件
```bash
# 临时文件获取
temp_file_path=$(uv run ./scripts/get_temp_path.py)
```

View File

@@ -0,0 +1,35 @@
# 错误处理
## uv未安装
**检测**: `uv`命令失败
**错误消息**:
```
uv not found
此skill依赖uv工具运行Python脚本。
请安装uv: https://docs.astral.sh/uv/getting-started/installation/
安装命令示例:
curl -LsSf https://astral.sh/uv/install.sh | sh # Linux/macOS
powershell -c "irm https://astral.sh/uv/install.ps1 | iex" # Windows
```
**重要提示**:
- **立即停止任务**等待用户安装uv后再继续
- **不要尝试使用**python, pip, poetry, venv, virtualenv等
- **不要自动安装**uv
- 用户安装uv完成后可以重新执行任务
**操作**: 立即停止所有执行等待用户安装uv
## 其他错误
| 场景 | 错误消息 | 操作 |
|------|---------|------|
| 项目检测失败 | 回退到非uv模式使用`--with` | 警告后继续 |
| 依赖解析不准确 | 依赖可能不完整<br>Traceback: [traceback] | 停止,保留脚本调试 |
| 语法错误 | Python语法错误: [描述]<br>文件: <path><br>行号: <line> | 停止 |
| 路径权限问题 | 无法写入: <path><br>建议: 使用临时文件模式 | 回退到临时文件 |

Some files were not shown because too many files have changed in this diff Show More