feat(repo): 整理 Qoder Skills 和 MCP 配置到仓库

- 添加 5 个用户级别 Skills:
  - auto-commit: 自动 Git 提交
  - karpathy-guidelines: 编码规范指南
  - opencli-websearch: 多源网络搜索
  - pdf-reader: PDF 内容提取
  - repo-analyzer: 项目深度分析

- 添加 Playwright MCP 配置 (21 个浏览器自动化工具)
- 创建完整的 README.md 文档说明
This commit is contained in:
2026-04-18 11:17:41 +08:00
commit c3ea38c045
33 changed files with 2677 additions and 0 deletions

189
README.md Normal file
View File

@@ -0,0 +1,189 @@
# Qoder 配置仓库
本仓库整理了 Qoder IDE 的 Skills 和 MCP (Model Context Protocol) 配置。
## 📁 目录结构
```
qoder-config/
├── skills/ # Skills 配置
│ ├── karpathy-guidelines/ # Karpathy 编码规范指南
│ └── repo-analyzer/ # 项目深度分析技能
└── mcps/ # MCP 服务器配置
└── playwright/ # Playwright 浏览器自动化
```
## 🛠️ Skills
### 1. auto-commit
**描述**: 自动将更改提交到 Git并生成描述性消息。
**使用场景**:
- 代码、文档、测试结果或实验结果发生变更时
- 用户提到"保存"、"记录"、"追踪"、"commit"、"提交"时
- 需要保持修改历史记录时
**核心功能**:
- 自动检测文件变更
- 生成符合规范的 Git 提交消息(遵循 Conventional Commits
- 支持多种提交类型feat, fix, docs, test, refactor, chore, experiment
- 原子化提交,一个逻辑变更一次提交
### 2. karpathy-guidelines
**描述**: 基于 Andrej Karpathy 观察的编码行为准则,减少常见 LLM 编码错误。
**使用场景**:
- 编写、审查或重构代码时
- 避免过度复杂化
- 进行精确的代码修改
- 明确假设条件
- 定义可验证的成功标准
**核心原则**:
1. **编码前先思考** - 明确假设,不要隐藏困惑
2. **简洁优先** - 最少代码解决问题,不写投机性代码
3. **精确修改** - 只修改必须的部分
4. **目标驱动** - 定义成功标准,循环验证
### 3. opencli-websearch
**描述**: 使用 Qoder WebSearch 和 OpenCLI 同时进行多源网络搜索,合并结果并提供全面的信息检索。
**使用场景**:
- 搜索信息、研究话题
- 获取多源数据
- 学术论文检索
- 技术问题查询
- 中文社区内容搜索
**支持的数据源**:
- **学术源**: arxiv
- **技术源**: stackoverflow, hackernews
- **中文社交**: zhihu, xiaohongshu需浏览器
- **新闻源**: 36kr, bbc, reuters
- **通用源**: google需浏览器
**核心特性**:
- 并行搜索架构,同时调用多个数据源
- 智能结果去重和排序
- 支持深度内容获取(下载完整网页为 Markdown
- 临时数据管理和存储
### 4. pdf-reader
**描述**: 使用 pymupdf4llm 从 PDF 文件中提取文本和表格,并转换为 Markdown 格式。
**使用场景**:
- 处理 PDF 文件
- 从 PDF 中提取内容
- 将 PDF 转换为文本或 Markdown
- 阅读或处理 PDF 文档
**核心功能**:
- 自动提取文本、表格和基本格式
- 保留文档结构Markdown 格式)
- 处理多栏布局和复杂格式
- 支持指定页面范围提取
- 批量处理多个 PDF 文件
**依赖**:
```bash
pip install pymupdf4llm
```
### 5. repo-analyzer
**描述**: 深度分析开源项目并生成专业架构报告。
**使用场景**:
- 分析开源项目的架构和设计
- 对比两个同类项目的设计差异
- 深入研究框架或库的实现思路
**核心特性**:
- 业务视角优先,从"解决什么问题"出发
- 抽象层次把控,讲设计而非贴代码
- 全局关联,连接项目整体设计哲学
- 启发性写作,让读者学到东西
- 深度洞察,解释 Why > What
**分析工作流**:
1. 项目获取与初始化
2. 项目规模评估与分析模式选择
3. 外部调研 + 项目文档研读
4. 项目特征识别 + 自适应提问
5. 动态报告结构设计
6. 并行深度分析subagent 团队)
7. 交叉验证 + 质量管控
8. 多源融合与最终报告
## 🔌 MCP (Model Context Protocol)
### 1. playwright
**描述**: Playwright 浏览器自动化工具集,提供 21 个浏览器交互工具。
**工具列表**:
- `browser_navigate` - 导航到 URL
- `browser_click` - 点击页面元素
- `browser_type` - 输入文本
- `browser_fill_form` - 填写表单
- `browser_select_option` - 选择选项
- `browser_hover` - 悬停元素
- `browser_press_key` - 按键
- `browser_drag` - 拖拽元素
- `browser_take_screenshot` - 截图
- `browser_snapshot` - 获取页面快照
- `browser_evaluate` - 执行 JavaScript
- `browser_run_code` - 运行代码
- `browser_tabs` - 标签页管理
- `browser_navigate_back` - 后退
- `browser_wait_for` - 等待条件
- `browser_console_messages` - 获取控制台消息
- `browser_network_requests` - 获取网络请求
- `browser_handle_dialog` - 处理对话框
- `browser_resize` - 调整窗口大小
- `browser_file_upload` - 上传文件
- `browser_close` - 关闭浏览器
**使用场景**:
- 网页自动化测试
- 网页数据抓取
- UI 交互验证
- 浏览器行为分析
## 📝 使用说明
### 安装 Skills
`skills/` 目录下的 skill 复制到 Qoder 的 skills 目录:
```bash
# macOS/Linux
cp -r skills/* ~/Library/Application\ Support/Qoder/User/skills/
# Windows
xcopy /E /I skills\* %USERPROFILE%\AppData\Roaming\Qoder\User\skills\
```
### 配置 MCP
MCP 配置通常需要添加到 Qoder 的 MCP 配置文件中。参考每个 MCP 服务器目录下的 `SERVER_METADATA.json``tools/` 目录中的工具定义。
## 📊 统计信息
- **Skills 数量**: 5
- **MCP 服务器数量**: 1
- **MCP 工具总数**: 21
## 📄 许可证
各 Skills 和 MCP 可能使用不同的许可证,请参阅各自目录下的 LICENSE 文件。
- `karpathy-guidelines`: MIT License
---
**最后更新**: 2026-04-18

View File

@@ -0,0 +1,5 @@
{
"name": "playwright",
"source": "user",
"toolCount": 21
}

View File

@@ -0,0 +1,47 @@
{
"name": "browser_click",
"description": "Perform click on a web page",
"inputSchema": {
"properties": {
"button": {
"description": "Button to click, defaults to left",
"enum": [
"left",
"right",
"middle"
],
"type": "string"
},
"doubleClick": {
"description": "Whether to perform a double click instead of a single click",
"type": "boolean"
},
"element": {
"description": "Human-readable element description used to obtain permission to interact with the element",
"type": "string"
},
"modifiers": {
"description": "Modifier keys to press",
"items": {
"enum": [
"Alt",
"Control",
"ControlOrMeta",
"Meta",
"Shift"
],
"type": "string"
},
"type": "array"
},
"ref": {
"description": "Exact target element reference from the page snapshot",
"type": "string"
}
},
"required": [
"ref"
],
"type": "object"
}
}

View File

@@ -0,0 +1,9 @@
{
"name": "browser_close",
"description": "Close the page",
"inputSchema": {
"properties": {},
"required": null,
"type": "object"
}
}

View File

@@ -0,0 +1,31 @@
{
"name": "browser_console_messages",
"description": "Returns all console messages",
"inputSchema": {
"properties": {
"all": {
"description": "Return all console messages since the beginning of the session, not just since the last navigation. Defaults to false.",
"type": "boolean"
},
"filename": {
"description": "Filename to save the console messages to. If not provided, messages are returned as text.",
"type": "string"
},
"level": {
"default": "info",
"description": "Level of the console messages to return. Each level includes the messages of more severe levels. Defaults to \"info\".",
"enum": [
"error",
"warning",
"info",
"debug"
],
"type": "string"
}
},
"required": [
"level"
],
"type": "object"
}
}

View File

@@ -0,0 +1,31 @@
{
"name": "browser_drag",
"description": "Perform drag and drop between two elements",
"inputSchema": {
"properties": {
"endElement": {
"description": "Human-readable target element description used to obtain the permission to interact with the element",
"type": "string"
},
"endRef": {
"description": "Exact target element reference from the page snapshot",
"type": "string"
},
"startElement": {
"description": "Human-readable source element description used to obtain the permission to interact with the element",
"type": "string"
},
"startRef": {
"description": "Exact source element reference from the page snapshot",
"type": "string"
}
},
"required": [
"startElement",
"startRef",
"endElement",
"endRef"
],
"type": "object"
}
}

View File

@@ -0,0 +1,28 @@
{
"name": "browser_evaluate",
"description": "Evaluate JavaScript expression on page or element",
"inputSchema": {
"properties": {
"element": {
"description": "Human-readable element description used to obtain permission to interact with the element",
"type": "string"
},
"filename": {
"description": "Filename to save the result to. If not provided, result is returned as text.",
"type": "string"
},
"function": {
"description": "() =\u003e { /* code */ } or (element) =\u003e { /* code */ } when element is provided",
"type": "string"
},
"ref": {
"description": "Exact target element reference from the page snapshot",
"type": "string"
}
},
"required": [
"function"
],
"type": "object"
}
}

View File

@@ -0,0 +1,17 @@
{
"name": "browser_file_upload",
"description": "Upload one or multiple files",
"inputSchema": {
"properties": {
"paths": {
"description": "The absolute paths to the files to upload. Can be single file or multiple files. If omitted, file chooser is cancelled.",
"items": {
"type": "string"
},
"type": "array"
}
},
"required": null,
"type": "object"
}
}

View File

@@ -0,0 +1,55 @@
{
"name": "browser_fill_form",
"description": "Fill multiple form fields",
"inputSchema": {
"properties": {
"fields": {
"description": "Fields to fill in",
"items": {
"additionalProperties": false,
"properties": {
"name": {
"description": "Human-readable field name",
"type": "string"
},
"ref": {
"description": "Exact target field reference from the page snapshot",
"type": "string"
},
"selector": {
"description": "CSS or role selector for the field element, when \"ref\" is not available. Either \"selector\" or \"ref\" is required.",
"type": "string"
},
"type": {
"description": "Type of the field",
"enum": [
"textbox",
"checkbox",
"radio",
"combobox",
"slider"
],
"type": "string"
},
"value": {
"description": "Value to fill in the field. If the field is a checkbox, the value should be `true` or `false`. If the field is a combobox, the value should be the text of the option.",
"type": "string"
}
},
"required": [
"name",
"type",
"ref",
"value"
],
"type": "object"
},
"type": "array"
}
},
"required": [
"fields"
],
"type": "object"
}
}

View File

@@ -0,0 +1,20 @@
{
"name": "browser_handle_dialog",
"description": "Handle a dialog",
"inputSchema": {
"properties": {
"accept": {
"description": "Whether to accept the dialog.",
"type": "boolean"
},
"promptText": {
"description": "The text of the prompt in case of a prompt dialog.",
"type": "string"
}
},
"required": [
"accept"
],
"type": "object"
}
}

View File

@@ -0,0 +1,20 @@
{
"name": "browser_hover",
"description": "Hover over element on page",
"inputSchema": {
"properties": {
"element": {
"description": "Human-readable element description used to obtain permission to interact with the element",
"type": "string"
},
"ref": {
"description": "Exact target element reference from the page snapshot",
"type": "string"
}
},
"required": [
"ref"
],
"type": "object"
}
}

View File

@@ -0,0 +1,16 @@
{
"name": "browser_navigate",
"description": "Navigate to a URL",
"inputSchema": {
"properties": {
"url": {
"description": "The URL to navigate to",
"type": "string"
}
},
"required": [
"url"
],
"type": "object"
}
}

View File

@@ -0,0 +1,9 @@
{
"name": "browser_navigate_back",
"description": "Go back to the previous page in the history",
"inputSchema": {
"properties": {},
"required": null,
"type": "object"
}
}

View File

@@ -0,0 +1,37 @@
{
"name": "browser_network_requests",
"description": "Returns all network requests since loading the page",
"inputSchema": {
"properties": {
"filename": {
"description": "Filename to save the network requests to. If not provided, requests are returned as text.",
"type": "string"
},
"filter": {
"description": "Only return requests whose URL matches this regexp (e.g. \"/api/.*user\").",
"type": "string"
},
"requestBody": {
"default": false,
"description": "Whether to include request body. Defaults to false.",
"type": "boolean"
},
"requestHeaders": {
"default": false,
"description": "Whether to include request headers. Defaults to false.",
"type": "boolean"
},
"static": {
"default": false,
"description": "Whether to include successful static resources like images, fonts, scripts, etc. Defaults to false.",
"type": "boolean"
}
},
"required": [
"static",
"requestBody",
"requestHeaders"
],
"type": "object"
}
}

View File

@@ -0,0 +1,16 @@
{
"name": "browser_press_key",
"description": "Press a key on the keyboard",
"inputSchema": {
"properties": {
"key": {
"description": "Name of the key to press or a character to generate, such as `ArrowLeft` or `a`",
"type": "string"
}
},
"required": [
"key"
],
"type": "object"
}
}

View File

@@ -0,0 +1,21 @@
{
"name": "browser_resize",
"description": "Resize the browser window",
"inputSchema": {
"properties": {
"height": {
"description": "Height of the browser window",
"type": "number"
},
"width": {
"description": "Width of the browser window",
"type": "number"
}
},
"required": [
"width",
"height"
],
"type": "object"
}
}

View File

@@ -0,0 +1,18 @@
{
"name": "browser_run_code",
"description": "Run Playwright code snippet",
"inputSchema": {
"properties": {
"code": {
"description": "A JavaScript function containing Playwright code to execute. It will be invoked with a single argument, page, which you can use for any page interaction. For example: `async (page) =\u003e { await page.getByRole('button', { name: 'Submit' }).click(); return await page.title(); }`",
"type": "string"
},
"filename": {
"description": "Load code from the specified file. If both code and filename are provided, code will be ignored.",
"type": "string"
}
},
"required": null,
"type": "object"
}
}

View File

@@ -0,0 +1,28 @@
{
"name": "browser_select_option",
"description": "Select an option in a dropdown",
"inputSchema": {
"properties": {
"element": {
"description": "Human-readable element description used to obtain permission to interact with the element",
"type": "string"
},
"ref": {
"description": "Exact target element reference from the page snapshot",
"type": "string"
},
"values": {
"description": "Array of values to select in the dropdown. This can be a single value or multiple values.",
"items": {
"type": "string"
},
"type": "array"
}
},
"required": [
"ref",
"values"
],
"type": "object"
}
}

View File

@@ -0,0 +1,18 @@
{
"name": "browser_snapshot",
"description": "Capture accessibility snapshot of the current page, this is better than screenshot",
"inputSchema": {
"properties": {
"depth": {
"description": "Limit the depth of the snapshot tree",
"type": "number"
},
"filename": {
"description": "Save snapshot to markdown file instead of returning it in the response.",
"type": "string"
}
},
"required": null,
"type": "object"
}
}

View File

@@ -0,0 +1,26 @@
{
"name": "browser_tabs",
"description": "List, create, close, or select a browser tab.",
"inputSchema": {
"properties": {
"action": {
"description": "Operation to perform",
"enum": [
"list",
"new",
"close",
"select"
],
"type": "string"
},
"index": {
"description": "Tab index, used for close/select. If omitted for close, current tab is closed.",
"type": "number"
}
},
"required": [
"action"
],
"type": "object"
}
}

View File

@@ -0,0 +1,37 @@
{
"name": "browser_take_screenshot",
"description": "Take a screenshot of the current page. You can't perform actions based on the screenshot, use browser_snapshot for actions.",
"inputSchema": {
"properties": {
"element": {
"description": "Human-readable element description used to obtain permission to screenshot the element. If not provided, the screenshot will be taken of viewport. If element is provided, ref must be provided too.",
"type": "string"
},
"filename": {
"description": "File name to save the screenshot to. Defaults to `page-{timestamp}.{png|jpeg}` if not specified. Prefer relative file names to stay within the output directory.",
"type": "string"
},
"fullPage": {
"description": "When true, takes a screenshot of the full scrollable page, instead of the currently visible viewport. Cannot be used with element screenshots.",
"type": "boolean"
},
"ref": {
"description": "Exact target element reference from the page snapshot. If not provided, the screenshot will be taken of viewport. If ref is provided, element must be provided too.",
"type": "string"
},
"type": {
"default": "png",
"description": "Image format for the screenshot. Default is png.",
"enum": [
"png",
"jpeg"
],
"type": "string"
}
},
"required": [
"type"
],
"type": "object"
}
}

View File

@@ -0,0 +1,33 @@
{
"name": "browser_type",
"description": "Type text into editable element",
"inputSchema": {
"properties": {
"element": {
"description": "Human-readable element description used to obtain permission to interact with the element",
"type": "string"
},
"ref": {
"description": "Exact target element reference from the page snapshot",
"type": "string"
},
"slowly": {
"description": "Whether to type one character at a time. Useful for triggering key handlers in the page. By default entire text is filled in at once.",
"type": "boolean"
},
"submit": {
"description": "Whether to submit entered text (press Enter after)",
"type": "boolean"
},
"text": {
"description": "Text to type into the element",
"type": "string"
}
},
"required": [
"ref",
"text"
],
"type": "object"
}
}

View File

@@ -0,0 +1,22 @@
{
"name": "browser_wait_for",
"description": "Wait for text to appear or disappear or a specified time to pass",
"inputSchema": {
"properties": {
"text": {
"description": "The text to wait for",
"type": "string"
},
"textGone": {
"description": "The text to wait for to disappear",
"type": "string"
},
"time": {
"description": "The time to wait in seconds",
"type": "number"
}
},
"required": null,
"type": "object"
}
}

111
skills/auto-commit/SKILL.md Normal file
View File

@@ -0,0 +1,111 @@
---
name: auto-commit
description: Automatically commit changes to Git with descriptive messages. Use when code, documents, test results, or experiment results have changed, when the user mentions tracking changes, saving progress, or wants to keep a history of modifications.
---
# Auto Commit Skill
## Purpose
Whenever code, documents, test results, experiment results, or any project files change, automatically create a Git commit to track those changes with a descriptive message.
## When to Apply
Apply this skill when:
- Code files are modified, added, or deleted
- Documentation (markdown, docs) is updated
- Test results or experiment results change
- Configuration files are modified
- Any significant project state change occurs
- User mentions "保存", "记录", "追踪", "commit", "提交"
## Commit Message Format
Follow this format for commit messages:
```
<type>(<scope>): <subject>
<body>
```
### Types
- `feat`: New feature or functionality
- `fix`: Bug fix
- `docs`: Documentation changes
- `test`: Test-related changes
- `refactor`: Code refactoring
- `chore`: Maintenance tasks
- `experiment`: Experiment results or changes
### Scope (optional but recommended)
- File name or module name (e.g., `parser`, `README`, `config`)
### Subject
- Brief description of what changed (max 50 chars)
- Use imperative mood ("Add" not "Added")
### Body (optional but recommended for significant changes)
- Detailed explanation of what and why
- Can include before/after comparisons
## Workflow
1. **Check Git Status**
```bash
git status --short
```
2. **Review Changes**
- See what files were modified
- Understand the nature of changes
- For code changes, review the diff if needed
3. **Stage Changes**
```bash
git add <files>
```
Or stage all changes:
```bash
git add .
```
4. **Create Commit**
```bash
git commit -m "<type>(<scope>): <subject>" -m "<body>"
```
5. **Verify**
```bash
git log --oneline -1
```
## Examples
**Code change:**
```bash
git commit -m "feat(parser): add support for nested JSON objects" -m "Implement recursive parsing for nested structures"
```
**Documentation update:**
```bash
git commit -m "docs(README): update installation instructions" -m "Add macOS-specific setup steps"
```
**Test results:**
```bash
git commit -m "test(benchmark): add performance test results" -m "Baseline metrics for v2.0 parser"
```
**Experiment results:**
```bash
git commit -m "experiment(model): test accuracy with new dataset" -m "Accuracy improved from 85% to 92%"
```
## Important Notes
- Always commit after significant changes
- Write clear, descriptive commit messages
- Include context in the body for complex changes
- Keep commits atomic (one logical change per commit)
- Never skip commit hooks unless explicitly requested

View File

@@ -0,0 +1,67 @@
---
name: karpathy-guidelines
description: Behavioral guidelines to reduce common LLM coding mistakes. Use when writing, reviewing, or refactoring code to avoid overcomplication, make surgical changes, surface assumptions, and define verifiable success criteria.
license: MIT
---
# Karpathy Guidelines
Behavioral guidelines to reduce common LLM coding mistakes, derived from [Andrej Karpathy's observations](https://x.com/karpathy/status/2015883857489522876) on LLM coding pitfalls.
**Tradeoff:** These guidelines bias toward caution over speed. For trivial tasks, use judgment.
## 1. Think Before Coding
**Don't assume. Don't hide confusion. Surface tradeoffs.**
Before implementing:
- State your assumptions explicitly. If uncertain, ask.
- If multiple interpretations exist, present them - don't pick silently.
- If a simpler approach exists, say so. Push back when warranted.
- If something is unclear, stop. Name what's confusing. Ask.
## 2. Simplicity First
**Minimum code that solves the problem. Nothing speculative.**
- No features beyond what was asked.
- No abstractions for single-use code.
- No "flexibility" or "configurability" that wasn't requested.
- No error handling for impossible scenarios.
- If you write 200 lines and it could be 50, rewrite it.
Ask yourself: "Would a senior engineer say this is overcomplicated?" If yes, simplify.
## 3. Surgical Changes
**Touch only what you must. Clean up only your own mess.**
When editing existing code:
- Don't "improve" adjacent code, comments, or formatting.
- Don't refactor things that aren't broken.
- Match existing style, even if you'd do it differently.
- If you notice unrelated dead code, mention it - don't delete it.
When your changes create orphans:
- Remove imports/variables/functions that YOUR changes made unused.
- Don't remove pre-existing dead code unless asked.
The test: Every changed line should trace directly to the user's request.
## 4. Goal-Driven Execution
**Define success criteria. Loop until verified.**
Transform tasks into verifiable goals:
- "Add validation" → "Write tests for invalid inputs, then make them pass"
- "Fix the bug" → "Write a test that reproduces it, then make it pass"
- "Refactor X" → "Ensure tests pass before and after"
For multi-step tasks, state a brief plan:
```
1. [Step] → verify: [check]
2. [Step] → verify: [check]
3. [Step] → verify: [check]
```
Strong success criteria let you loop independently. Weak criteria ("make it work") require constant clarification.

View File

@@ -0,0 +1,286 @@
---
name: opencli-websearch
description: 使用 Qoder WebSearch 和 OpenCLI 同时进行多源网络搜索合并结果并提供全面的信息检索。支持谷歌、知乎、ArXiv、小红书、StackOverflow、HackerNews 等多个数据源。当用户需要搜索信息、研究话题或获取多源数据时自动使用。
---
# OpenCLI 多源 Web 搜索
## 概述
本 Skill 提供**并行搜索能力**,同时使用 Qoder 内置 WebSearch 和 OpenCLI 的多源适配器进行搜索,合并结果以提供更全面的信息覆盖。
## 搜索策略
### 并行搜索架构
```
用户查询
├──→ Qoder WebSearch (通用搜索)
└──→ OpenCLI 多源搜索
├──→ 学术源: arxiv
├──→ 技术源: stackoverflow, hackernews
├──→ 社交源: xiaohongshu, zhihu (需浏览器)
├──→ 新闻源: 36kr, bbc, reuters
└──→ 通用源: google (需浏览器)
```
### 数据源分类
| 类别 | 数据源 | 模式 | 适用场景 |
|-----|-------|------|---------|
| **学术** | arxiv | 公开 | 论文、研究 |
| **技术** | stackoverflow, hackernews | 公开 | 编程、技术讨论 |
| **中文社交** | zhihu, xiaohongshu | 浏览器 | 中文社区内容 |
| **新闻** | 36kr, bbc, reuters | 公开 | 时事新闻 |
| **通用** | google | 浏览器 | 广泛搜索 |
## 使用方法
### 基本搜索
当用户需要搜索信息时,自动执行以下流程:
1. **启动并行搜索**
- 调用 Qoder WebSearch
- 同时调用 OpenCLI 多源搜索
2. **OpenCLI 搜索执行**
```bash
# 学术搜索
opencli arxiv search "{query}" --limit 5
# 技术搜索
opencli stackoverflow search "{query}" --limit 5
opencli hackernews top # 或搜索相关
# 新闻搜索 (36kr 支持中文)
opencli 36kr search "{query}" --limit 5
# 其他公开源
opencli gitee search "{query}" --limit 5
```
3. **结果合并与去重**
- 合并所有来源的结果
- 按相关性和来源多样性排序
- 标注每个结果的来源
### 深度内容获取
对于重要结果,使用 OpenCLI 下载完整内容:
```bash
# 下载网页内容为 Markdown
opencli web read --url "{url}" --output "{output_path}"
```
### 临时数据存储
所有下载的内容存储在:
```
~/Downloads/opencli-websearch-data/
├── {timestamp}_{query_hash}/
│ ├── metadata.json # 搜索元数据
│ ├── results.json # 合并后的搜索结果
│ └── content/
│ ├── arxiv_{id}.md
│ ├── web_{hash}.md
│ └── ...
```
## 执行流程
### Step 1: 分析查询意图
判断查询类型以选择最佳数据源:
- **学术/研究** → 优先 arxiv, google-scholar
- **编程/技术** → 优先 stackoverflow, hackernews
- **中文内容** → 优先 zhihu, xiaohongshu, 36kr
- **新闻/时事** → 优先 bbc, reuters, 36kr
- **通用查询** → 全源搜索
### Step 2: 并行执行搜索
```python
# 伪代码示意
sources = select_sources(query_intent)
results = {}
# Qoder WebSearch
results['qoder'] = websearch(query)
# OpenCLI 多源搜索
for source in sources:
results[source] = opencli_search(source, query)
```
### Step 3: 结果处理
1. **格式化**: 统一不同来源的结果格式
2. **去重**: 基于 URL 和标题相似度去重
3. **排序**: 按来源权威性和相关性排序
4. **摘要**: 为每个结果生成简要摘要
### Step 4: 深度获取(可选)
对于高相关性结果:
1. 使用 `opencli web read` 获取完整内容
2. 存储到本地临时目录
3. 提供内容摘要给用户
## 输出格式
### 搜索结果报告
```markdown
## 搜索结果: {query}
### 概览
- 搜索源: {sources}
- 总结果数: {count}
- 存储位置: ~/Downloads/opencli-websearch-data/{timestamp}/
### 按来源分类
#### 学术来源
1. [标题](url) - arxiv
- 摘要: ...
#### 技术来源
1. [标题](url) - stackoverflow
- 摘要: ...
#### 中文来源
1. [标题](url) - zhihu
- 摘要: ...
### 推荐深度阅读
- [文档1](path) - 已下载完整内容
- [文档2](path) - 已下载完整内容
```
## 工具函数
### 执行 OpenCLI 搜索
```python
def opencli_search(source: str, query: str, limit: int = 5) -> list:
"""
使用 OpenCLI 搜索指定数据源
Args:
source: 数据源名称 (arxiv, stackoverflow, etc.)
query: 搜索查询
limit: 结果数量限制
Returns:
搜索结果列表
"""
# 构建命令
cmd = f"opencli {source} search '{query}' --limit {limit}"
# 执行并解析结果
...
```
### 下载文档内容
```python
def download_content(url: str, output_dir: str) -> str:
"""
使用 OpenCLI web read 下载文档
Args:
url: 文档 URL
output_dir: 输出目录
Returns:
下载文件的本地路径
"""
filename = hash(url) + ".md"
output_path = os.path.join(output_dir, filename)
cmd = f"opencli web read --url '{url}' --output '{output_path}'"
# 执行命令
...
return output_path
```
### 创建存储目录
```python
def create_storage_dir(query: str) -> str:
"""
创建临时存储目录
Returns:
存储目录路径
"""
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
query_hash = hashlib.md5(query.encode()).hexdigest()[:8]
dir_name = f"{timestamp}_{query_hash}"
storage_path = os.path.expanduser(f"~/Downloads/opencli-websearch-data/{dir_name}")
os.makedirs(storage_path, exist_ok=True)
os.makedirs(os.path.join(storage_path, "content"), exist_ok=True)
return storage_path
```
## 错误处理
### 常见错误
| 错误码 | 原因 | 解决方案 |
|-------|------|---------|
| BROWSER_CONNECT | 浏览器扩展未连接 | 提示用户打开 Chrome 并启用扩展 |
| TIMEOUT | 搜索超时 | 减少结果数量或更换数据源 |
| NOT_FOUND | 无搜索结果 | 尝试其他数据源或修改查询词 |
### 降级策略
当某个数据源失败时:
1. 记录错误信息
2. 继续处理其他数据源
3. 在结果中标注失败的数据源
4. 建议用户可选的替代方案
## 最佳实践
1. **查询优化**: 对于中文查询,优先使用中文数据源
2. **结果数量**: 每个源默认获取 5 条,避免过多噪声
3. **深度获取**: 只对高相关性结果下载完整内容
4. **存储管理**: 定期清理 ~/Downloads/opencli-websearch-data/ 下的旧数据
5. **来源标注**: 始终标注每个结果的来源,便于用户判断可信度
## 示例
### 示例 1: 学术研究
用户: "搜索关于大语言模型路由的论文"
执行:
```bash
# 并行搜索
opencli arxiv search "large language model routing" --limit 5
opencli arxiv search "LLM router" --limit 5
# Qoder websearch 同时执行
```
### 示例 2: 技术问题
用户: "Python 异步编程最佳实践"
执行:
```bash
opencli stackoverflow search "python async best practices" --limit 5
opencli hackernews top | grep -i python
# Qoder websearch 同时执行
```
### 示例 3: 中文内容
用户: "小红书上的 AI 工具推荐"
执行:
```bash
opencli xiaohongshu search "AI工具推荐" --limit 5
# 注意: 需要浏览器扩展已连接
```

View File

@@ -0,0 +1,193 @@
#!/usr/bin/env python3
"""
使用 OpenCLI web read 下载文档内容
"""
import os
import sys
import json
import hashlib
import subprocess
import argparse
from pathlib import Path
from typing import Optional, List
from urllib.parse import urlparse
def download_with_opencli(url: str, output_dir: str, timeout: int = 60) -> Optional[str]:
"""
使用 OpenCLI web read 下载文档内容
Args:
url: 要下载的 URL
output_dir: 输出目录
timeout: 超时时间(秒)
Returns:
下载文件的本地路径,失败返回 None
"""
# 生成文件名
url_hash = hashlib.md5(url.encode()).hexdigest()[:12]
parsed = urlparse(url)
domain = parsed.netloc.replace(".", "_")
filename = f"{domain}_{url_hash}.md"
output_path = os.path.join(output_dir, filename)
# 构建命令
cmd = ["opencli", "web", "read", "--url", url, "--output", output_path]
print(f"下载: {url}")
print(f"输出: {output_path}")
try:
result = subprocess.run(
cmd,
capture_output=True,
text=True,
timeout=timeout
)
if result.returncode == 0:
if os.path.exists(output_path):
file_size = os.path.getsize(output_path)
print(f"✓ 成功下载 ({file_size} bytes)")
return output_path
else:
print(f"✗ 文件未生成")
return None
else:
print(f"✗ 下载失败: {result.stderr[:200]}")
return None
except subprocess.TimeoutExpired:
print(f"✗ 下载超时")
return None
except Exception as e:
print(f"✗ 错误: {str(e)}")
return None
def batch_download(urls: List[str], output_dir: str, max_workers: int = 3) -> dict:
"""
批量下载多个 URL
Args:
urls: URL 列表
output_dir: 输出目录
max_workers: 最大并行数
Returns:
下载结果字典 {url: local_path or None}
"""
from concurrent.futures import ThreadPoolExecutor, as_completed
results = {}
with ThreadPoolExecutor(max_workers=max_workers) as executor:
future_to_url = {
executor.submit(download_with_opencli, url, output_dir): url
for url in urls
}
for future in as_completed(future_to_url):
url = future_to_url[future]
try:
results[url] = future.result()
except Exception as e:
print(f"{url} 异常: {str(e)}")
results[url] = None
return results
def load_results_from_search(search_dir: str) -> List[str]:
"""
从之前的搜索结果中加载 URL 列表
Args:
search_dir: 搜索结果目录
Returns:
URL 列表
"""
results_file = os.path.join(search_dir, "results.json")
if not os.path.exists(results_file):
print(f"未找到结果文件: {results_file}")
return []
with open(results_file, "r", encoding="utf-8") as f:
data = json.load(f)
urls = []
for source, result in data.items():
if result.get("success") and result.get("output"):
# 简单解析输出中的 URL
output = result["output"]
for line in output.split("\n"):
if "url:" in line.lower() or "http" in line:
# 提取 URL
import re
url_match = re.search(r'https?://[^\s\'"<>]+', line)
if url_match:
urls.append(url_match.group())
return list(set(urls))
def main():
parser = argparse.ArgumentParser(description="使用 OpenCLI 下载文档内容")
parser.add_argument("--url", help="单个 URL 下载")
parser.add_argument("--urls", nargs="+", help="多个 URL 下载")
parser.add_argument("--from-search", help="从搜索结果目录加载 URL")
parser.add_argument("--output-dir", required=True, help="输出目录")
parser.add_argument("--max-workers", type=int, default=3, help="最大并行数")
args = parser.parse_args()
# 确保输出目录存在
os.makedirs(args.output_dir, exist_ok=True)
# 收集 URL 列表
urls = []
if args.url:
urls.append(args.url)
if args.urls:
urls.extend(args.urls)
if args.from_search:
search_urls = load_results_from_search(args.from_search)
urls.extend(search_urls)
print(f"从搜索结果加载 {len(search_urls)} 个 URL")
if not urls:
print("错误: 未提供 URL")
return 1
# 去重
urls = list(set(urls))
print(f"\n{len(urls)} 个唯一 URL 待下载\n")
# 批量下载
results = batch_download(urls, args.output_dir, args.max_workers)
# 统计
success_count = sum(1 for v in results.values() if v is not None)
print(f"\n{'='*60}")
print(f"下载完成: {success_count}/{len(urls)} 成功")
print(f"{'='*60}")
# 保存下载记录
record_file = os.path.join(args.output_dir, "download_record.json")
with open(record_file, "w", encoding="utf-8") as f:
json.dump(results, f, ensure_ascii=False, indent=2)
print(f"下载记录: {record_file}")
return 0
if __name__ == "__main__":
sys.exit(main())

View File

@@ -0,0 +1,245 @@
#!/usr/bin/env python3
"""
统一搜索入口 - 整合 Qoder WebSearch 和 OpenCLI 多源搜索
"""
import os
import sys
import json
import hashlib
import subprocess
import argparse
from datetime import datetime
from typing import List, Dict, Optional
from concurrent.futures import ThreadPoolExecutor, as_completed
def create_storage_dir(query: str) -> str:
"""创建临时存储目录"""
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
query_hash = hashlib.md5(query.encode()).hexdigest()[:8]
dir_name = f"{timestamp}_{query_hash}"
storage_path = os.path.expanduser(f"~/Downloads/opencli-websearch-data/{dir_name}")
os.makedirs(storage_path, exist_ok=True)
os.makedirs(os.path.join(storage_path, "content"), exist_ok=True)
return storage_path
def run_opencli_search(source: str, query: str, limit: int = 5) -> Dict:
"""执行 OpenCLI 搜索"""
if source == "hackernews":
cmd = ["opencli", "hackernews", "top", "--limit", str(limit)]
else:
cmd = ["opencli", source, "search", query, "--limit", str(limit)]
try:
result = subprocess.run(
cmd,
capture_output=True,
text=True,
timeout=60
)
return {
"source": source,
"success": result.returncode == 0,
"output": result.stdout if result.returncode == 0 else None,
"error": result.stderr if result.returncode != 0 else None
}
except subprocess.TimeoutExpired:
return {"source": source, "success": False, "output": None, "error": "Timeout"}
except Exception as e:
return {"source": source, "success": False, "output": None, "error": str(e)}
def run_qoder_websearch(query: str) -> Dict:
"""
执行 Qoder WebSearch
注意:此函数需要 Qoder 环境支持,实际使用时通过 Qoder 工具调用
"""
# 这是一个占位符,实际使用时 Qoder 会直接调用 websearch
# 这里返回一个标记,表示需要 Qoder 处理
return {
"source": "qoder_websearch",
"success": True,
"output": "[Qoder WebSearch 结果将在此处合并]",
"error": None,
"needs_qoder": True
}
def parallel_search(query: str, sources: List[str], use_qoder: bool = True) -> Dict[str, Dict]:
"""并行执行多源搜索"""
results = {}
# 如果启用 Qoder先标记
if use_qoder:
results["qoder_websearch"] = run_qoder_websearch(query)
# 并行执行 OpenCLI 搜索
with ThreadPoolExecutor(max_workers=5) as executor:
future_to_source = {
executor.submit(run_opencli_search, source, query, 5): source
for source in sources
}
for future in as_completed(future_to_source):
source = future_to_source[future]
try:
results[source] = future.result()
except Exception as e:
results[source] = {"source": source, "success": False, "output": None, "error": str(e)}
return results
def select_sources(query: str, intent: Optional[str] = None) -> List[str]:
"""根据查询意图选择数据源"""
sources = []
# 数据源配置
SOURCE_CONFIG = {
"academic": ["arxiv"],
"technical": ["stackoverflow", "hackernews", "gitee"],
"chinese": ["36kr", "zhihu", "xiaohongshu"],
"news": ["bbc", "reuters"],
"general": ["google"]
}
if intent and intent in SOURCE_CONFIG:
sources = SOURCE_CONFIG[intent]
else:
# 自动判断
query_lower = query.lower()
# 学术关键词
if any(kw in query_lower for kw in ["paper", "论文", "arxiv", "research", "study"]):
sources.extend(SOURCE_CONFIG["academic"])
# 技术关键词
if any(kw in query_lower for kw in ["python", "javascript", "code", "programming", "bug", "error"]):
sources.extend(["stackoverflow", "hackernews"])
# 中文关键词 - 优先公开源
if any('\u4e00' <= char <= '\u9fff' for char in query):
sources.extend(["36kr"])
# 默认源
if not sources:
sources = ["arxiv", "stackoverflow", "36kr", "hackernews"]
return list(set(sources))
def generate_report(query: str, results: Dict, storage_path: str) -> str:
"""生成 Markdown 格式搜索报告"""
report = []
report.append(f"# 搜索报告: {query}\n")
report.append(f"**搜索时间**: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
report.append(f"**存储位置**: `{storage_path}`\n")
# 统计
success_count = sum(1 for r in results.values() if r.get("success"))
report.append(f"**数据源**: {len(results)} 个 | **成功**: {success_count}\n")
report.append("---\n")
# 按来源分类展示
for source, result in sorted(results.items()):
status_icon = "" if result.get("success") else ""
report.append(f"\n## {status_icon} {source.upper()}\n")
if result.get("success") and result.get("output"):
output = result["output"]
# 截断过长输出
if len(output) > 2000:
output = output[:2000] + "\n\n... (内容已截断)"
report.append(f"```\n{output}\n```\n")
elif result.get("error"):
report.append(f"```\n错误: {result['error'][:200]}\n```\n")
if result.get("needs_qoder"):
report.append("> 📝 **注意**: Qoder WebSearch 结果将通过 Qoder 工具直接提供\n")
report.append("\n---\n")
report.append("*由 OpenCLI WebSearch Skill 生成*\n")
return "\n".join(report)
def save_results(storage_path: str, query: str, results: Dict, report: str):
"""保存搜索结果"""
# 保存元数据
metadata = {
"query": query,
"timestamp": datetime.now().isoformat(),
"sources": list(results.keys()),
"success_count": sum(1 for r in results.values() if r.get("success"))
}
with open(os.path.join(storage_path, "metadata.json"), "w", encoding="utf-8") as f:
json.dump(metadata, f, ensure_ascii=False, indent=2)
# 保存原始结果
with open(os.path.join(storage_path, "results.json"), "w", encoding="utf-8") as f:
json.dump(results, f, ensure_ascii=False, indent=2)
# 保存报告
with open(os.path.join(storage_path, "report.md"), "w", encoding="utf-8") as f:
f.write(report)
def main():
parser = argparse.ArgumentParser(description="统一搜索入口 - Qoder + OpenCLI")
parser.add_argument("query", help="搜索查询")
parser.add_argument("--intent", choices=["academic", "technical", "chinese", "news", "general"],
help="搜索意图类型")
parser.add_argument("--sources", nargs="+", help="指定 OpenCLI 数据源")
parser.add_argument("--no-qoder", action="store_true", help="不使用 Qoder WebSearch")
parser.add_argument("--output", help="输出目录")
args = parser.parse_args()
# 创建存储目录
if args.output:
storage_path = args.output
os.makedirs(storage_path, exist_ok=True)
else:
storage_path = create_storage_dir(args.query)
print(f"📁 存储路径: {storage_path}\n")
# 选择数据源
if args.sources:
sources = args.sources
else:
sources = select_sources(args.query, args.intent)
print(f"🔍 OpenCLI 数据源: {', '.join(sources)}")
print(f"🔍 Qoder WebSearch: {'禁用' if args.no_qoder else '启用'}\n")
# 执行并行搜索
print("⏳ 正在并行搜索...\n")
results = parallel_search(args.query, sources, use_qoder=not args.no_qoder)
# 生成报告
report = generate_report(args.query, results, storage_path)
# 保存结果
save_results(storage_path, args.query, results, report)
# 输出报告
print(report)
print(f"\n✅ 搜索完成!")
print(f"📄 报告: {os.path.join(storage_path, 'report.md')}")
print(f"📊 数据: {os.path.join(storage_path, 'results.json')}")
return 0
if __name__ == "__main__":
sys.exit(main())

View File

@@ -0,0 +1,322 @@
#!/usr/bin/env python3
"""
OpenCLI 多源 Web 搜索脚本
支持 Qoder WebSearch 和 OpenCLI 并行搜索
"""
import os
import sys
import json
import hashlib
import subprocess
import argparse
from datetime import datetime
from pathlib import Path
from typing import List, Dict, Optional
from concurrent.futures import ThreadPoolExecutor, as_completed
# 数据源配置
SOURCES = {
"academic": {
"arxiv": {"type": "public", "limit": 5},
},
"technical": {
"stackoverflow": {"type": "public", "limit": 5},
"hackernews": {"type": "public", "limit": 5},
"gitee": {"type": "public", "limit": 5},
},
"chinese": {
"zhihu": {"type": "browser", "limit": 5},
"xiaohongshu": {"type": "browser", "limit": 5},
"36kr": {"type": "public", "limit": 5},
},
"news": {
"bbc": {"type": "public", "limit": 5},
"reuters": {"type": "public", "limit": 5},
},
"general": {
"google": {"type": "browser", "limit": 5},
}
}
def create_storage_dir(query: str) -> str:
"""创建临时存储目录"""
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
query_hash = hashlib.md5(query.encode()).hexdigest()[:8]
dir_name = f"{timestamp}_{query_hash}"
storage_path = os.path.expanduser(f"~/Downloads/opencli-websearch-data/{dir_name}")
os.makedirs(storage_path, exist_ok=True)
os.makedirs(os.path.join(storage_path, "content"), exist_ok=True)
return storage_path
def run_opencli_search(source: str, query: str, limit: int = 5) -> Dict:
"""执行 OpenCLI 搜索"""
cmd = ["opencli", source, "search", query, "--limit", str(limit)]
try:
result = subprocess.run(
cmd,
capture_output=True,
text=True,
timeout=60
)
if result.returncode == 0:
return {
"source": source,
"success": True,
"output": result.stdout,
"error": None
}
else:
return {
"source": source,
"success": False,
"output": None,
"error": result.stderr
}
except subprocess.TimeoutExpired:
return {
"source": source,
"success": False,
"output": None,
"error": "Timeout"
}
except Exception as e:
return {
"source": source,
"success": False,
"output": None,
"error": str(e)
}
def run_opencli_hackernews(limit: int = 5) -> Dict:
"""获取 HackerNews 热门内容"""
cmd = ["opencli", "hackernews", "top", "--limit", str(limit)]
try:
result = subprocess.run(
cmd,
capture_output=True,
text=True,
timeout=30
)
return {
"source": "hackernews",
"success": result.returncode == 0,
"output": result.stdout if result.returncode == 0 else None,
"error": result.stderr if result.returncode != 0 else None
}
except Exception as e:
return {
"source": "hackernews",
"success": False,
"output": None,
"error": str(e)
}
def download_content(url: str, output_dir: str) -> Optional[str]:
"""使用 OpenCLI web read 下载文档内容"""
url_hash = hashlib.md5(url.encode()).hexdigest()[:12]
filename = f"web_{url_hash}.md"
output_path = os.path.join(output_dir, filename)
cmd = ["opencli", "web", "read", "--url", url, "--output", output_path]
try:
result = subprocess.run(
cmd,
capture_output=True,
text=True,
timeout=60
)
if result.returncode == 0 and os.path.exists(output_path):
return output_path
return None
except Exception:
return None
def select_sources(query: str, intent: Optional[str] = None) -> List[str]:
"""根据查询意图选择数据源"""
sources = []
if intent:
if intent == "academic":
sources.extend(SOURCES["academic"].keys())
elif intent == "technical":
sources.extend(SOURCES["technical"].keys())
elif intent == "chinese":
sources.extend(SOURCES["chinese"].keys())
elif intent == "news":
sources.extend(SOURCES["news"].keys())
else:
# 通用搜索 - 选择所有公开源
for category in ["academic", "technical", "chinese", "news"]:
for source, config in SOURCES[category].items():
if config["type"] == "public":
sources.append(source)
else:
# 自动判断
query_lower = query.lower()
# 学术关键词
if any(kw in query_lower for kw in ["paper", "论文", "arxiv", "research", "study"]):
sources.extend(SOURCES["academic"].keys())
# 技术关键词
if any(kw in query_lower for kw in ["python", "javascript", "code", "programming", "bug", "error"]):
sources.extend(["stackoverflow", "hackernews"])
# 中文关键词
if any('\u4e00' <= char <= '\u9fff' for char in query):
sources.extend(["36kr"]) # 优先公开源
# 默认添加通用源
if not sources:
sources = ["arxiv", "stackoverflow", "36kr"]
return list(set(sources))
def parallel_search(query: str, sources: List[str]) -> Dict[str, Dict]:
"""并行执行多源搜索"""
results = {}
with ThreadPoolExecutor(max_workers=5) as executor:
future_to_source = {}
for source in sources:
if source == "hackernews":
future = executor.submit(run_opencli_hackernews)
else:
limit = 5
for category in SOURCES.values():
if source in category:
limit = category[source].get("limit", 5)
break
future = executor.submit(run_opencli_search, source, query, limit)
future_to_source[future] = source
for future in as_completed(future_to_source):
source = future_to_source[future]
try:
results[source] = future.result()
except Exception as e:
results[source] = {
"source": source,
"success": False,
"output": None,
"error": str(e)
}
return results
def save_results(storage_path: str, query: str, results: Dict) -> str:
"""保存搜索结果到本地"""
# 保存元数据
metadata = {
"query": query,
"timestamp": datetime.now().isoformat(),
"sources": list(results.keys()),
"success_count": sum(1 for r in results.values() if r["success"])
}
metadata_path = os.path.join(storage_path, "metadata.json")
with open(metadata_path, "w", encoding="utf-8") as f:
json.dump(metadata, f, ensure_ascii=False, indent=2)
# 保存完整结果
results_path = os.path.join(storage_path, "results.json")
with open(results_path, "w", encoding="utf-8") as f:
json.dump(results, f, ensure_ascii=False, indent=2)
return storage_path
def print_report(query: str, results: Dict, storage_path: str):
"""打印搜索结果报告"""
print(f"\n{'='*60}")
print(f"搜索报告: {query}")
print(f"{'='*60}")
print(f"\n存储位置: {storage_path}")
print(f"数据源: {', '.join(results.keys())}")
success_count = sum(1 for r in results.values() if r["success"])
print(f"成功: {success_count}/{len(results)}")
print("\n" + "-"*60)
print("各源结果:")
print("-"*60)
for source, result in results.items():
status = "" if result["success"] else ""
print(f"\n[{status}] {source}")
if result["success"] and result["output"]:
# 截断输出,避免过长
output = result["output"][:500]
if len(result["output"]) > 500:
output += "..."
print(output)
elif result["error"]:
print(f" 错误: {result['error'][:100]}")
print(f"\n{'='*60}\n")
def main():
parser = argparse.ArgumentParser(description="OpenCLI 多源 Web 搜索")
parser.add_argument("query", help="搜索查询")
parser.add_argument("--intent", choices=["academic", "technical", "chinese", "news", "general"],
help="搜索意图类型")
parser.add_argument("--sources", nargs="+", help="指定数据源")
parser.add_argument("--download", action="store_true", help="下载高相关性文档")
parser.add_argument("--output", help="输出目录")
args = parser.parse_args()
# 创建存储目录
if args.output:
storage_path = args.output
os.makedirs(storage_path, exist_ok=True)
else:
storage_path = create_storage_dir(args.query)
print(f"存储路径: {storage_path}")
# 选择数据源
if args.sources:
sources = args.sources
else:
sources = select_sources(args.query, args.intent)
print(f"搜索源: {', '.join(sources)}")
print("正在并行搜索...")
# 执行并行搜索
results = parallel_search(args.query, sources)
# 保存结果
save_results(storage_path, args.query, results)
# 打印报告
print_report(args.query, results, storage_path)
return 0
if __name__ == "__main__":
sys.exit(main())

130
skills/pdf-reader/SKILL.md Normal file
View File

@@ -0,0 +1,130 @@
---
name: pdf-reader
description: Extract text and tables from PDF files and convert to Markdown format using pymupdf4llm. Use when working with PDF files, extracting content from PDFs, converting PDFs to text or Markdown, or when the user mentions reading or processing PDF documents.
---
# PDF Reader
Extract text and tables from PDF files and convert them to Markdown format using pymupdf4llm.
## Prerequisites
Before using this skill, ensure pymupdf4llm is installed:
```bash
python -c "import pymupdf4llm" 2>/dev/null && echo "Installed" || echo "Not installed"
```
If not installed, run:
```bash
pip install pymupdf4llm
```
## Basic Usage
### Extract PDF to Markdown
```python
import pymupdf4llm
# Convert PDF to Markdown
md_text = pymupdf4llm.to_markdown("path/to/file.pdf")
# Save to file
with open("output.md", "w", encoding="utf-8") as f:
f.write(md_text)
# Or print directly
print(md_text)
```
### Extract with Options
```python
import pymupdf4llm
# Convert specific pages
md_text = pymupdf4llm.to_markdown(
"document.pdf",
pages=[0, 1, 2], # First 3 pages (0-indexed)
)
# Convert all pages (default)
md_text = pymupdf4llm.to_markdown("document.pdf")
```
## Workflow
When processing a PDF:
1. **Check dependencies**: Verify pymupdf4llm is installed
2. **Extract content**: Use `pymupdf4llm.to_markdown()` to convert the PDF
3. **Save to same directory**: Output the Markdown to the same directory as the PDF
4. **Verify**: Check the output for completeness
### Quick Checklist
```
Task Progress:
- [ ] Step 1: Verify pymupdf4llm is installed
- [ ] Step 2: Extract PDF content to Markdown
- [ ] Step 3: Save .md file to same directory as PDF
- [ ] Step 4: Verify extraction quality
```
## Common Patterns
### Read and Display PDF Content
```python
import pymupdf4llm
# Extract and print
md_content = pymupdf4llm.to_markdown("report.pdf")
print(md_content)
```
### Extract and Save to Same Directory
```python
import pymupdf4llm
from pathlib import Path
# Convert and save to same directory as PDF
pdf_path = Path("input.pdf")
md_text = pymupdf4llm.to_markdown(pdf_path)
# Create output path with same name but .md extension
output_path = pdf_path.with_suffix('.md')
with open(output_path, "w", encoding="utf-8") as f:
f.write(md_text)
print(f"Saved to {output_path} ({len(md_text)} characters)")
```
### Process Multiple PDFs
```python
import pymupdf4llm
from pathlib import Path
pdf_files = ["doc1.pdf", "doc2.pdf", "doc3.pdf"]
for pdf_file in pdf_files:
pdf_path = Path(pdf_file)
md_text = pymupdf4llm.to_markdown(pdf_path)
# Save to same directory as PDF
output_path = pdf_path.with_suffix('.md')
with open(output_path, "w", encoding="utf-8") as f:
f.write(md_text)
print(f"Converted {pdf_file} -> {output_path}")
```
## Tips
- pymupdf4llm automatically extracts text, tables, and basic formatting
- Output is in Markdown format, preserving document structure
- Handles multi-column layouts and complex formatting
- For scanned PDFs requiring OCR, additional tools may be needed

View File

@@ -0,0 +1,274 @@
---
name: repo-analyzer
description: Use when the user mentions "分析项目"、"分析仓库"、"分析 GitHub"、"项目分析"、"源码分析"、"架构分析"、"代码分析"、"学习这个项目"、"研究这个框架"、"看看这个库怎么实现的"、"对比两个项目"、"项目评测"、"框架评测"
---
# Git 项目深度分析技能
深度分析开源项目并生成专业架构报告。报告是有深度洞察的技术研究,读完后读者能理解业务问题、掌握架构设计、产生自己的思考。
## When to Use
- 分析开源项目的架构和设计
- 对比两个同类项目的设计差异
- 深入研究一个框架或库的实现思路
## When NOT to Use
- 简单的代码问题或调试
- 单文件分析或代码审查
- 不涉及架构层面的代码修改
## 输出语言
默认中文。如果用户使用其他语言提问,则跟随用户语言。
## 核心原则
### 1. 业务视角优先
从"这个项目解决什么问题"出发,不是"这个文件里有什么函数"。
| 不要 | 要 |
|------|-----|
| `handleRequest(ctx)` 函数接收一个 Context 参数... | 请求进来后,系统会经过鉴权、限流、路由分发三个阶段... |
| `interface MessageQueue { push(); pop() }` | 模块间通过消息队列解耦,生产者只管投递,消费者按优先级拉取 |
### 2. 抽象层次把控:不贴代码,讲设计
默认在设计模式和架构层面描述,**非必要情况下不贴原始代码**。重点突出流程、逻辑、设计思想用架构图Mermaid、流程图、表格来表达而非代码片段。只有设计特别精妙、项目自创独特概念、或实现是核心卖点时才展示代码且必须先用自然语言解释。
### 3. 全局关联
每个局部分析都必须连接到项目整体设计哲学——这是区分"代码说明书"和"架构分析"的关键。详见 [analysis-guide.md](references/analysis-guide.md) 的全局关联章节。
### 4. 启发性写作
目标是让读者**学到东西、产生思考**,而不是获得一份代码说明书。像资深工程师在白板前讲解——有观点、有推理、有对比。详见 [analysis-guide.md](references/analysis-guide.md) 的启发性写作章节。
### 5. 深度洞察Why > What强制
每个设计决策必须解释动机、权衡、替代方案代价。描述"是什么"只是起点,解释"为什么"才是分析的价值所在。每个核心模块和整体架构都要回答:
- **为什么这样设计?** 不只是"用了什么模式",而是"为什么适合这个场景"
- **如果不这样会怎样?** 替代方案的代价
- **与业界最佳实践的差距?** 领先之处和改进空间
- **如果让你重新设计?** 展示更深层理解
- **系统性设计哲学?** 贯穿整个项目的风格(如"约定优于配置"、"零成本抽象"
示例:
> ❌ 路由系统采用了中间件模式,支持链式调用。
>
> ✅ 路由系统选择了洋葱模型而非线性管道。线性管道实现更简单但洋葱模型让每个中间件都能同时处理请求和响应阶段——这对日志、计时、错误恢复至关重要。Express 当年选择线性模型,后来不得不用各种 hack 处理响应后逻辑Koa 吸取教训才转向洋葱模型。如果让我重新设计,我会考虑加入中间件依赖声明,让框架自动排序——这是 Fastify 的做法,能避免顺序导致的隐蔽 bug。
### 补充要求
- **代码为准** — 一切结论有代码依据,标注 `文件路径``文件路径:行号范围`,禁止模糊表述
- **有温度** — 像资深工程师给新同事做 onboarding加入主观评价和建议避免 AI 味套话
- **重点深入次要简略** — 核心创新点深入分析,通用工具函数一句话带过
- **批判性思考** — 与业界实践对比,指出真实问题,不回避缺陷。参考 [analysis-guide.md](references/analysis-guide.md)
- **行文流畅易懂** — 整体行文需要流畅自然,让入门的工程师也能看懂并学习到东西。避免过于学术化或堆砌术语
- **拒绝流水账** — 每个模块要体现深度细节,不能一句带过或泛泛而谈。每个模块如果合适要加上对应的 Mermaid 架构图,让读者看完有启发、能学到设计精髓
## 分析工作流
**灵活性原则**以下所有阶段和章节都是建议性的指引不是必须严格执行的清单。agent 应根据当前分析的项目特性动态决策——如果某个阶段或环节对当前项目没有意义,可以跳过或简化。一切以最终报告的质量为准。
### 阶段 1: 项目获取与初始化
1. 解析用户输入(支持 `owner/repo`、GitHub/GitLab/Gitee URL、本地路径、项目名称
2. 创建工作区:在用户主目录下创建 `repo-analyses/${REPO_NAME}-{YYYYMMDD}` 目录作为 `$WORK_DIR`跨平台macOS/Linux 使用 `$HOME`Windows 使用 `$USERPROFILE``$HOME`
3. 如果用户提供本地路径则跳过 clone否则 `git clone --depth=1` 克隆仓库
4. 获取基本元数据Star、Fork、贡献者、代码统计
### 阶段 2: 项目规模评估与分析模式选择
1. **统计有效代码行数**(排除可跳过代码),按模块列出分布
- 可跳过代码定义:测试代码、构建/部署配置Dockerfile、CI yaml 等、自动生成代码protobuf 生成、lock 文件等)、示例/文档代码
- 使用 `find` + `wc -l``cloc` 等工具统计,按顶层目录分组
2. **向用户报告代码规模**,使用 AskUserQuestion 让用户选择分析模式:
| 模式 | 核心模块覆盖率 | 次要模块覆盖率 | 适用场景 |
|------|-------------|-------------|---------|
| 快速分析 | ≥30% | ≥10% | 快速了解项目全貌 |
| 标准分析(推荐) | ≥60% | ≥30% | 常规架构分析 |
| 深度分析 | ≥90% | ≥60% | 深入研究每个设计决策 |
3. 将代码规模统计和用户选择的分析模式写入 `drafts/03-plan.md`,后续阶段据此控制分析深度
**覆盖率计算规则**
- 覆盖率 = 通过 Read 工具实际请求过的行范围之并集 / 文件总行数
- 对于大文件(>500 行),必须分段读取,确保以下关键段落被覆盖:
- 文件头部的类型定义和导入(前 100 行)
- 核心业务逻辑函数(通过目录结构或函数名定位)
- 文件尾部的测试代码(如有)
- 只读了文件的一小部分(<30%不计入覆盖率视为未读
- 自动生成代码proto 生成lock 文件等可降低覆盖率要求扫描结构即可不需要逐行阅读
### 阶段 3: 外部调研 + 项目文档研读(先搜再读)
1. WebSearch 搜索项目评价对比架构讨论至少 3-5 次搜索
2. **遍历项目官网**如果存在
- README GitHub 页面提取官网 URL
- 使用 WebFetch/tavily_crawl 遍历官网关键页面首页FeaturesUse CasesComparisonBlog
- 重点提取产品定位语tagline)、典型使用场景官方竞品对比用户案例/testimonial
- 官网内容往往是理解"为什么需要这个产品"的最佳来源比代码和技术文档更直接
3. **通读项目自带文档**
- 架构文档`architecture/``docs/``design/` 等目录
- CONTRIBUTING.mdAGENTS.md 等开发者指引
- RFCADRArchitecture Decision Records)、设计提案等
- 这些文档往往包含开发者的设计思路权衡取舍历史决策背景是理解"为什么这样设计"的第一手资料
- 将文档中的关键设计决策和思路摘录到调研笔记中
4. 整理调研发现写入 `drafts/03-research.md`必须包含以下结构化段落信息不足则标注"未找到"
- **项目解决的核心问题** 1-3 个具体场景描述痛点在什么情况下遇到什么问题现有方案为什么不够
- **竞品/同类项目对比**列出 3-5 个最相关的竞品说明各自定位差异和技术路线差异
- **为什么需要单独做这个项目**不能用现有方案组合解决吗这个项目的独特价值主张是什么
- **项目背后的组织动机**如适用商业公司的战略考量开源社区的生态定位
5. 生成分析规划写入 `drafts/03-plan.md`
### 阶段 4: 项目特征识别 + 自适应提问
这是核心阶段不使用固定问题列表而是根据项目特征生成针对性问题
**步骤:**
1. **快速扫描**扫描入口文件目录结构依赖声明项目文档README
2. **识别项目核心特征**
- 项目类型与定位/框架/应用/工具
- 规模与成熟度
- 设计风格信号类型体操极简 API配置驱动等
- 技术栈特点新兴技术多语言特定运行时
- 社区定位核心基础设施应用层工具教学项目等
3. **从特征中提炼问题**根据观察到的项目特征生成针对性问题问题应该帮助聚焦分析方向而不是走流程
**思维过程**——每个观察都可能暗示一个值得问用户的问题
- 观察到的技术选择 问动机不常见的技术组合自己实现了通常用第三方库解决的功能
- 观察到的架构特征 问优先级性能优化痕迹复杂的插件/扩展系统
- 观察到的设计张力 问取舍简单性 vs 灵活性向后兼容的包袱
- 观察到的项目定位 问受众目标用户是谁在生态中是替代还是填补空白
**维度启发**——什么样的项目特征暗示什么样的分析角度
- 小而精的库 API 设计哲学边界划定大型框架 模块化策略向后兼容生态治理
- 使用新兴技术 为什么选择它迁移成本多语言/多范式 语言边界设计
- 大量泛型/类型体操 类型安全 vs 复杂度权衡极简 API 简单性如何实现牺牲了什么
**好问题的特征**具体基于代码中观察到的现象)、有分析价值答案会影响分析方向)、用户能答问关注点和偏好不问需要深入代码才能回答的技术细节)、不重复不问通过代码就能回答的问题
4. **向用户提问**使用 AskUserQuestion 工具向用户提问每次不超过 3 个问题
- 其中一个问题应确认**报告开头的详略程度**对于知名项目用户可能不需要冗长的产品介绍和竞品对比只想直接进入代码分析询问用户是否需要场景化引入和竞品定位章节还是直接从项目全景和代码分析开始
5. **不限轮次**可多轮提问直到方向明确分析过程中发现新的关键分歧点可以再追问
**关键原则**问题完全由项目特征驱动不预设类别不同项目应该产生完全不同的问题
### 阶段 5: 动态报告结构设计
根据用户回答 + 项目特征设计本次报告的章节结构
**步骤:**
1. **综合信息**结合阶段 3 的调研阶段 4 的项目特征和用户回答
2. **设计章节结构**不使用固定模板但必须满足骨架约束见下方
3. **输出报告大纲**将设计好的报告大纲输出给用户确认后再继续
4. **识别模块**追踪核心数据流识别 N 个逻辑模块按业务功能划分分为核心模块和次要模块
5. **设计模块叙事线**确定模块在报告中的呈现顺序和过渡逻辑不按目录结构排列而是按读者理解的最佳路径组织
- 选择叙事主线数据流驱动请求从进入到离开经过哪些模块)、分层驱动从底层到上层)、或问题驱动从核心问题到解决方案逐层展开
- 每两个相邻模块之间写明过渡逻辑上一个模块的输出/问题/局限 引出下一个模块的必要性
- 将叙事线写入 `drafts/05-modules-plan.md`格式示例模块 A →[A 的输出需要 B 来消费]→ 模块 B →[B 解决了 X 但引出 Y 问题]→ 模块 C
6. **写入计划**输出模块清单和报告大纲写入 `drafts/05-modules-plan.md`
**骨架约束**报告不规定具体章节但必须满足
- **场景化问题引入**用具体场景讲清楚项目解决什么问题现有方案的不足为什么需要这个项目——素材来自阶段 3 调研笔记)。**注意**如果用户在阶段 4 表示不需要冗长介绍如项目已经很知名可以精简或跳过此章节直接从项目全景开始
- **竞品定位**与同类项目的关键差异不是功能清单对比而是设计哲学和技术路线的差异)。**注意**同上用户可选择跳过
- **项目全景**让读者快速理解项目是什么解决什么问题
- **深度分析**核心设计的 Why权衡与业界对比
- **评价与启发**诚实的优缺点读者能从中学到什么
- **架构可视化**Mermaid 图表
- 所有结论有代码依据
### 阶段 6: 并行深度分析subagent 团队)
必须使用 Agent 工具并行启动 subagent参考 [module-analysis-guide.md](references/module-analysis-guide.md) 中的 prompt 模板和协作规范
每个 subagent prompt 中必须包含项目整体设计哲学和全局视角要求确保模块分析不是孤立的
每个 subagent prompt 中还必须包含该模块的叙事上下文来自阶段 5 的叙事线设计前一个模块讲了什么读者带着什么问题进入本模块本模块需要为下一个模块铺垫什么subagent 应在草稿开头用 1-2 句衔接前一模块草稿结尾用 1 句铺垫下一模块
每个 subagent prompt 末尾必须附加覆盖率要求参考 module-analysis-guide.md 中的覆盖率要求段落告知当前分析模式和最低覆盖率目标要求草稿末尾附覆盖率明细表
**subagent 写入策略**
对于大模块文件总行数 > 5000 行),必须在 subagent prompt 中要求增量写入草稿:
- 每完成一个子系统/子模块的分析后,立即将该部分写入草稿文件
- 第一个子系统用 Write 创建文件,后续子系统用 Edit 追加
- 不要等全部文件读完再一次性写入
- 覆盖率明细表在最后追加
**主 agent 等待纪律**
- subagent 启动后,主 agent 不得阅读 subagent 负责的源码文件
- 主 agent 在等待期间应专注于阅读项目文档architecture/、docs/)、外部调研、设计报告骨架、准备阶段 8 的融合框架
- 判断 subagent 是否卡住的标准output 文件超过 5 分钟无新增行。只有确认卡住后,主 agent 才可以接管该模块的分析
- **严禁提前合并**:必须等所有 subagent 全部完成后,再开始阶段 7 和阶段 8 的合并工作。不要在部分 subagent 还在运行时就开始写最终报告
### 阶段 7: 交叉验证 + 质量管控(主 agent
**7.1 覆盖率门控**
1. 读取每个 `drafts/06-module-*.md` 末尾的覆盖率明细表
2. 快速检查:每个草稿末尾是否有覆盖率表、合计行是否标注达标(✅/❌)
3. 只有标注 ❌ 或缺少覆盖率表的模块才需要深入检查
4. 不达标模块 → 主 agent 自动补充阅读未覆盖的关键文件,将补充发现追加到对应草稿
5. 补充后仍不达标 → 向用户报告哪些模块未达标及原因(如文件过大、二进制文件等)
**7.2 抽查验证**
1. 从每个核心模块草稿中选取 2-3 个关键结论
2. 回到源码逐行验证结论准确性
3. 发现偏差则修正草稿中的对应内容
**7.3 交叉验证**
1. 交叉验证【待主 agent 验证】标注的跨模块结论
2. 综合回答探索问题,识别跨模块设计模式
3. 验证全局关联:每个模块的分析是否都连接到了项目整体设计哲学
4. 写入 `drafts/07-cross-validation.md`
### 阶段 8: 多源融合与最终报告(主 agent
1. 提炼架构洞察和系统性设计哲学
2. 基于阶段 3 调研结果深化竞品对比(仅在阶段 3 信息不足时补充搜索)
3. 提出"如果重新设计"的改进建议
4. 写入 `drafts/08-insights.md`
5. **多源融合**:以阶段 5 设计的报告章节结构为骨架,从各草稿中抽取内容填充。同一概念在多个草稿中出现时,取最详细版本并补充其他版本独有信息。融合后消除所有"见草稿 X"、"详见附录"等跳转指示
- **叙事连贯**:按阶段 5 设计的叙事线组织模块章节。每个模块章节的开头必须有 1-2 句过渡,连接上一个模块的结论或问题。避免"接下来我们分析 X 模块"这种生硬转折,改用自然过渡(如"Gateway 完成了请求的认证和路由,但它只负责'谁可以进来',不负责'进来之后能做什么'。这个行为控制的职责,由策略引擎承担。"
6. **分段写入**:最终报告通常超过 500 行,先 Write 前几个章节200-300 行),后续用 Edit 追加,每次追加前 Read 确认末尾位置
7. **覆盖率汇总**:将覆盖率数据汇总写入 `drafts/08-coverage.md`(不放入最终报告)
- 数据直接从各 subagent 草稿末尾的覆盖率明细表中提取,不需要主 agent 重新计算
- 如果主 agent 在阶段 7 补充了阅读,将补充的行数加到对应模块的「已读行数」中
- 汇总表格式:
| 模块 | 类型 | 文件数 | 有效代码行 | 已读行数 | 覆盖率 | 达标 |
|------|------|--------|-----------|---------|--------|------|
| ... | 核心/次要 | ... | ... | ... | ...% | ✅/❌ |
8. 汇总生成最终报告(不包含覆盖率章节)
### 草稿文件清单
所有中间过程保存到 `$WORK_DIR/drafts/`
| 阶段 | 文件 |
|------|------|
| 3 | `03-research.md`, `03-plan.md` |
| 5 | `05-modules-plan.md` |
| 6 | `06-module-{name}.md`subagent 生成) |
| 7 | `07-cross-validation.md` |
| 8 | `08-insights.md`, `08-coverage.md` |
文件写入分块,单次不超过 300 行或 15KB。
## 特殊场景
- **超大型项目(>50000 行)**:优先分析核心模块,使用 Agent 并行分析
- **对比分析模式**:两个项目分别完成阶段 1-4然后在阶段 5 设计对比式报告结构,骨架约束中增加"设计决策对比"和"选型建议"
## 输出要求
1. 最终报告为单一 markdown 文件:`$WORK_DIR/ANALYSIS_REPORT.md`
2. 大量使用 Mermaid 图表展示架构、流程、数据流
3. 面向需要理解业务架构的开发者
4. 亮点和问题的评价思维框架参考 [analysis-guide.md](references/analysis-guide.md)
5. 分析哲学和深度标准参考 [analysis-guide.md](references/analysis-guide.md)

View File

@@ -0,0 +1,166 @@
# 分析哲学与思维框架
## 核心立场
分析的目标是让读者**学到东西、产生思考**,而不是获得一份代码说明书。好的分析像一位资深工程师在白板前讲解——有观点、有推理、有对比,读完后读者能参与架构讨论。
每个评价都需要:代码依据、对比基准、推理过程。"代码质量不错"不是评价,"项目在错误处理上采用了统一的 Result 类型而非异常,这让错误路径在类型层面可见,代价是增加了调用方的样板代码——对于这个强调可靠性的基础设施项目来说,这个权衡是合理的"才是评价。
## 从项目特征中发现值得深挖的点
不要套用模板。每个项目都有自己的"有趣之处",发现它们的方法:
**追问设计动机**:看到一个设计选择时,问"为什么不用更常见的方案?"如果答案是"没有特别原因",这可能是一个改进点;如果答案揭示了深层约束,这就是值得展开的洞察。
**寻找张力点**:好的架构是在矛盾需求之间找平衡。寻找项目中的张力——性能 vs 可读性、灵活性 vs 简单性、一致性 vs 自治性。这些张力点往往是最有分析价值的地方。
**识别设计哲学**:大多数成熟项目有贯穿始终的设计风格("约定优于配置"、"零成本抽象"、"显式优于隐式")。识别这个哲学,然后检验它是否被一致地贯彻——不一致的地方往往有故事。
**关注边界**:模块边界、抽象层边界、系统边界——边界的设计往往比内部实现更能体现架构思考。一个模块内部可以随时重写,但边界一旦确定就很难改。
## 如何发现真正的设计亮点
亮点是**在特定约束下做出的聪明权衡**,不是"代码整洁"或"注释完善"这种泛泛之谈。
### 发现方法
**对比法**"如果是我来设计,我会怎么做?"——如果你的第一反应方案和项目的方案不同,深入比较两者的权衡,往往能发现项目方案的精妙之处。
**追问法**"这个设计解决了什么不明显的问题?"——表面上看起来过度设计的地方,可能是在防御一个你还没注意到的边界情况。
**场景法**"在极端场景下会怎样?"——高并发、网络分区、数据量暴增、下游服务挂掉——好的设计在极端场景下优雅降级,差的设计在极端场景下崩溃。
**演进法**"这个设计是如何演变到现在的?"——git 历史和代码中的注释有时能揭示设计演进的故事,当前的"复杂"设计可能是多次踩坑后的智慧结晶。
### 亮点的层次
| 层次 | 示例 | 分析价值 |
|------|------|----------|
| 架构级 | 独特的模块化策略、创新的扩展机制 | 高——值得深入展开 |
| 设计模式级 | 巧妙的状态管理、优雅的错误传播 | 中——值得一段解释 |
| 实现级 | 精巧的算法选择、高效的数据结构 | 视情况——只有当它体现设计思想时才值得展开 |
## 如何写出有启发性的分析
### 对比式思考
每个设计决策都存在于一个选择空间中。好的分析不只描述"选了什么",还要说明"没选什么以及为什么"。
> ❌ 该项目使用事件驱动架构进行模块间通信。
>
> ✅ 模块间通信选择了事件总线而非直接调用。直接调用更简单、调试更容易,但会让模块间产生编译期依赖——任何模块的接口变更都会波及调用方。事件总线的代价是运行时才能发现通信错误,但换来了模块可以独立开发和部署。对于这个插件化架构来说,这个权衡是合理的。
### 反事实推理
"如果不这样做会怎样"是检验设计必要性的利器。如果去掉一个设计元素后系统仍然正常工作,那它可能是过度设计;如果会导致严重问题,那就值得深入解释。
### 设计权衡三角
大多数架构决策涉及三个维度的权衡。找到这三个维度并说明项目在哪个方向上做了倾斜,比简单说"好"或"不好"有价值得多。
常见的权衡三角:
- 简单性 / 灵活性 / 性能
- 一致性 / 可用性 / 分区容忍
- 开发速度 / 运行时安全 / 学习曲线
## 全局关联
**每个局部分析都必须连接到项目整体设计哲学。** 这是区分"代码说明书"和"架构分析"的关键。
做法:
- 分析一个模块时,先说明它在整个系统中扮演什么角色
- 解释一个设计决策时,说明它如何服务于项目的整体设计哲学
- 发现一个问题时,评估它对整个系统的影响范围
- 描述模块间协作时,解释这种协作模式是否与项目其他部分一致
反面教材:孤立地分析每个模块,最后拼在一起——这样的报告读起来像一堆独立的代码审查,缺乏整体叙事。
## 叙事连贯
**模块分析不是独立章节的拼接,而是一条有逻辑的叙事线。**
好的模块叙事像一本书的章节——每章结尾自然引出下一章的主题。读者不需要目录就能理解为什么先讲 A 再讲 B。
常见的叙事主线:
- **数据流驱动**:跟随一个请求从进入系统到返回响应的完整路径,沿途讲解每个模块的职责。适合 Web 框架、API 网关等请求驱动的系统
- **分层驱动**:从最底层的基础设施讲到最上层的用户接口,每层依赖下层的能力。适合操作系统、编译器等分层架构
- **问题驱动**:从核心业务问题出发,逐步引入解决每个子问题的模块。适合领域复杂的业务系统
反面教材:按目录结构或字母顺序排列模块——这样的报告读起来像一本字典,不像一篇分析。
过渡句示例:
> ✅ Gateway 完成了请求的认证和路由分发,但它只负责"谁可以进来",不负责"进来之后能做什么"。这个行为控制的职责,由沙箱运行时的策略引擎承担。
>
> ❌ 接下来我们分析策略引擎模块。
## 深度标准
### 有深度的分析长这样
> 路由系统选择了基数树radix tree而非哈希表。哈希表查找是 O(1),但不支持参数路由(`/users/:id`)和通配符——要支持这些就得退化为线性扫描。基数树在静态路由上接近 O(1)同时天然支持前缀匹配让参数路由和通配符成为一等公民。代价是实现复杂度高、内存占用略大但对于一个以路由性能为卖点的框架来说这个投入是值得的。值得注意的是Fastify 和 Hono 也做了同样的选择,这已经成为高性能路由的事实标准。
特征:有具体的技术推理、有量化的权衡、有业界对比、有"为什么适合这个项目"的判断。
### 没有深度的分析长这样
> 路由系统采用了高效的数据结构来存储和匹配路由,支持参数路由和通配符匹配,性能表现优秀。
特征:泛泛而谈、没有具体技术细节、没有推理过程、换一个项目也能用。
## 批判性思考指引
### 如何诚实评价
- **有代码依据**:每个评价都要指向具体的代码证据,不能凭印象
- **有对比基准**:说"好"或"不好"时,对比的是什么?业界最佳实践?同类项目?项目自身的设计目标?
- **区分"不同"和"不好"**:非主流的设计选择不等于错误,可能是在不同约束下的合理权衡
### 如何与业界对比
- 选择真正可比的项目(同领域、同规模、同目标用户)
- 对比设计选择而非实现质量——不同项目有不同的成熟度
- 承认各自的约束差异——一个 2 人项目和一个 200 人项目面对的问题不同
### 如何指出问题而不流于吹毛求疵
- 只指出架构级别的问题,不纠结命名风格或代码格式
- 解释问题的实际影响——"这会导致什么具体后果"
- 如果可能,提供改进方向(不需要完整方案)
- 承认项目的约束——有些"问题"在特定约束下是合理的妥协
### 如何识别真正的问题
问题是**对系统产生实际影响的架构缺陷**,不是命名风格或代码格式。
**影响面分析**:这个问题影响多少模块?影响的是核心路径还是边缘场景?影响面越大,问题越值得指出。
**演进风险**:这个问题现在可能不严重,但随着项目增长会恶化吗?"技术债务"的本质是利息——现在不还,将来要付更多。
**替代方案可行性**:指出问题时,心里要有一个可行的改进方向。如果你想不到更好的方案,那可能不是问题,而是在当前约束下的合理妥协。
### 问题的层次
| 层次 | 示例 | 如何表述 |
|------|------|----------|
| 架构级 | 循环依赖、职责混乱、单点瓶颈 | 详细分析影响 + 改进方向 |
| 设计级 | 抽象泄漏、过度设计、状态管理混乱 | 说明具体后果 + 对比更好的做法 |
| 工程级 | 测试策略缺失、可观测性不足 | 简要指出 + 建议 |
### 评价的诚实性
- 项目质量高就多写亮点,不需要凑问题
- 项目有明显缺陷就直说,不需要找补
- 承认不确定性——"从代码来看可能是 X但也可能有我没看到的约束"
- 区分"设计选择"和"设计缺陷"——非主流不等于错误
### 综合评价维度
不使用固定评分表,但评价应覆盖以下维度(根据项目特征选择最相关的):
- **架构设计**:模块化程度、关注点分离、扩展性
- **设计哲学一致性**:项目是否贯彻了自己的设计理念
- **工程成熟度**:测试、文档、错误处理、可观测性
- **生态适应性**:在目标生态中的定位是否合理
- **演进健康度**:技术债务水平、架构是否支持未来演进
每个维度的评价都要有具体依据,不要给出没有论证的分数。

View File

@@ -0,0 +1,150 @@
# 模块分析指南
## 核心方法
按业务功能划分模块,不按文件或目录。一个逻辑模块可能跨越多个文件,一个文件也可能包含多个模块的部分实现。
分析深度标准:**交给另一个 AI 能仅凭报告复现系统的设计**。读者能理解模块的设计思路、职责边界、与其他模块的协作方式,并能参与架构讨论。
## 全局视角要求
**每个模块的分析都必须回答两个全局问题:**
1. **在整个项目中的角色**:这个模块为什么存在?去掉它系统会怎样?它服务于项目的哪个核心目标?
2. **与其他模块的设计协同**:它和其他模块之间的契约是什么?这种协作模式是否与项目整体的设计哲学一致?
孤立地分析模块是最常见的错误。一个模块的设计选择往往是被其他模块的约束所驱动的——不理解这些约束,就无法理解设计动机。
## 模块分析完整性四要素
每个核心模块的分析必须覆盖以下四个要素,缺任何一项都不算完整:
1. **核心数据结构** — 贴关键接口/类型定义(不是全部,只贴理解设计必需的)
2. **执行流程** — 用文字或 Mermaid 时序图描述调用链,标注源文件路径和行号
3. **设计决策** — 为什么选这个方案,不选另一个,权衡了什么
4. **模块间依赖** — 谁调用谁,数据怎么流转,共享了什么状态
检验标准:如果另一个 AI 只读你的分析(不看源码),能否画出这个模块的架构图并解释它的工作原理?如果不能,说明缺了某个要素。
必须包含业务问题、设计思路和架构模式、核心流程Mermaid 图)、协作关系、设计权衡。
不需要包含:完整类型定义、所有函数签名、所有参数列表、错误枚举逐项说明。
## 模块识别方法
1. **业务功能角度** — 项目提供哪些核心业务能力?
2. **数据流角度** — 数据从输入到输出经过哪些转换阶段?
3. **职责角度** — 业务需求变化时,哪些代码需要一起改?
## 分析深度
- **核心模块**(创新点、架构关键组件):设计思路讲透、核心流程有图有解读、设计决策解释权衡、协作关系画清楚
- **次要模块**(工具函数、标准封装):一句话职责 + 文件路径 + 特别之处
## Subagent 并行分析
阶段 6 必须使用 Agent 工具为每个核心模块启动独立 subagent 并行分析。
调度策略:
- 每个核心模块 → 一个独立 Agent subagent`subagent_type: "general-purpose"`
- 所有次要模块 → 合并到一个 Agent subagent 批量处理
- 所有 subagent 在同一消息中并行启动
### 核心模块 Subagent Prompt 模板
```
你是一位资深架构师,正在对 {项目名} 的「{模块名}」模块进行深度分析。
## 背景信息
- 项目定位: {一句话描述}
- 整体架构: {简述架构风格和核心设计}
- 项目设计哲学: {贯穿项目的核心设计理念}
- 该模块在系统中的位置: {与其他模块的关系}
- 叙事上下文: {该模块在报告叙事线中的位置——前一个模块讲了什么、读者带着什么问题进入本模块、本模块需要为下一个模块铺垫什么}
## 需要分析的文件
{文件路径列表}
## 分析结构
用自然语言描述设计意图,默认不暴露函数名/参数名/类型定义,只有设计特别精妙时才附代码片段。
1. 在项目中的角色 — 这个模块为什么存在?去掉它系统会怎样?
2. 解决什么问题 — 业务背景,没有它系统会怎样
3. 设计思路 — 方案及理由、放弃的替代方案、核心设计模式
4. 核心数据结构 — 贴理解设计必需的关键接口/类型定义(不是全部)
5. 核心业务流程 — Mermaid 流程图 + 自然语言解读,标注源文件路径和行号
6. 与其他模块的设计协同 — 依赖谁、谁依赖它、协作方式、共享状态、这种协作模式是否与项目整体设计哲学一致。跨模块结论用【待主 agent 验证】标注
7. 关键设计决策 — 1-3 个最重要的决策及权衡(为什么选这个方案,替代方案的代价)
8. Deep Research 洞察 — 替代方案代价、业界对比、如果重新设计
9. 扩展点(如适用)
10. 亮点与问题 — 涉及文件列表
## 全局视角要求
你的分析必须将模块放在项目整体语境中——设计选择如何服务整体哲学、边界为何这样划、变化会如何影响其他模块。(详见文件头部"全局视角要求"
## 相关探索问题
{问题列表}
将答案融入各节中。
## 写入策略
对于大模块(文件总行数 > 5000 行),必须增量写入草稿:
- 每完成一个子系统/子模块的分析后,立即将该部分写入草稿文件
- 第一个子系统用 Write 创建文件,后续子系统用 Edit 追加
- 不要等全部文件读完再一次性写入
- 覆盖率明细表在最后追加
## 输出
写入 {work_dir}/drafts/06-module-{模块名}.md单次写入不超过 300 行。
## 覆盖率要求
当前分析模式: {分析模式},核心模块最低覆盖率: {最低覆盖率}%。
草稿末尾必须附覆盖率明细表(格式:文件名 | 总行数 | 已读行数 | 覆盖率% | 未读原因),最后一行为合计行并标注达标✅/未达标❌。
「已读」指通过 Read 工具实际读取过的行。覆盖率未达标时必须继续阅读直到达标。
```
### 次要模块批量 Prompt 模板
```
你是一位资深架构师,正在对 {项目名} 的次要模块进行批量分析。
## 背景信息
- 项目定位: {一句话描述}
- 整体架构: {简述}
- 项目设计哲学: {贯穿项目的核心设计理念}
## 需要分析的次要模块
{模块列表:名称、职责假设、文件范围}
## 每个模块输出
1. 职责(一句话)
2. 在项目整体中的角色(一句话)
3. 实现方式(一句话)
4. 如有特别之处,展开说明
5. 涉及文件列表
写入 {work_dir}/drafts/06-module-secondary.md
## 覆盖率要求
当前分析模式: {分析模式},次要模块最低覆盖率: {最低覆盖率}%。
草稿末尾必须附覆盖率明细表(格式:文件名 | 总行数 | 已读行数 | 覆盖率% | 未读原因),最后一行为合计行并标注达标✅/未达标❌。
「已读」指通过 Read 工具实际读取过的行。覆盖率未达标时必须继续阅读直到达标。
```
### Subagent 协作规范
- **只分析分配的文件**,不越界
- **跨模块推断用【待主 agent 验证】标注**,主 agent 在阶段 7 交叉验证
- **深度优先于广度**,宁可把一个核心流程讲透
- **全局视角**,将模块放在项目整体语境中分析,解释设计选择如何服务于项目整体
- **叙事衔接**,草稿开头用 1-2 句说明本模块与前一个模块的关系,草稿结尾用 1 句铺垫下一个模块
## 质量检查
- [ ] 模块按业务功能划分,不是按文件/目录
- [ ] 每个模块解释了"在项目中的角色"和"为什么这样设计"
- [ ] 四要素完整:核心数据结构、执行流程(含文件路径行号)、设计决策、模块间依赖
- [ ] 核心流程用 Mermaid 图展示
- [ ] 关键接口/类型定义已贴出(只贴理解设计必需的)
- [ ] 没有暴露不必要的函数名、参数名、类型定义
- [ ] 协作关系清晰,共享状态已标注
- [ ] 每个模块的分析都连接到了项目整体设计哲学
- [ ] 检验:另一个 AI 只读分析(不看源码)能画出模块架构图