Skill: zlx/experience-manager (version 1.3.1)

--- name: experience-manager description: "经验管理工具：提取经验生成标准格式zip包，学习经验并转化为自身能力。搜索和发布经验包到 Experience Hub 平台。" metadata: version: 1.3.1 author: [REDACTED_SECRET] --- # Experience Manager Skill 经验管理工具，支持经验的提取、学习、列表、搜索和发布。 ## 功能 ### 1. 创建经验 (create) 将自然语言描述的经验转化为标准格式的 zip 包。 **使用方式：** ``` 创建经验 <经验描述> ``` **示例：** ``` 创建经验 用 feishu_doc 写入后必须验证 block_count，不然会静默失败 ``` **命令行用法：** ```bash # 自动提取 name node create.mjs "feishu_doc 写入后必须验证 block_count" # 手动指定 name（中文描述时使用） node create.mjs "中文描述" --name=feishu-doc-validation ``` **流程：** 1. 解析用户输入，提取结构化信息 2. 识别依赖的技能和知识领域 3. 生成标准格式的 exp.yml 4. 生成 references/ 目录下的依赖文件 5. 打包为 zip 文件 6. 保存到 ~/.openclaw/experiences/packages/ **输出：** - 生成 zip 文件：~/.openclaw/experiences/packages/{name}.zip - 包含 exp.yml 和 references/ 目录 - 显示经验包预览和保存路径 ### 2. 学习经验 (learn) 从 zip 包中学习经验，转化为自己的 SOUL/AGENTS/TOOLS。 **使用方式：** ``` 学习经验 <zip包路径或URL> ``` **支持的来源：** - 在线地址：`https://example.com/exp.zip` - 本地路径：`file:///path/to/exp.zip` 或 `/path/to/exp.zip` - 已下载的经验：`~/.openclaw/experiences/packages/exp.zip` **示例：** ``` 学习经验 https://example.com/feishu-doc-write-validation.zip 学习经验 ~/.openclaw/experiences/packages/feishu-doc-write-validation.zip ``` **流程：** 1. 下载/读取 zip 包 2. 检查是否已学习过（name + version 判断） 3. 检查依赖是否满足 4. 分析相关性 5. 生成转化方案 6. 预览待学习的内容，待用户确认是否开始学习 7. 应用经验并记录学习状态 **冲突处理：** - name 相同，version 相同：提示已学习，跳过 - name 相同，version 不同：提示有新版本，可选更新 - name 不同：正常学习 ### 3. 搜索经验 (search) 从 Experience Hub 搜索已发布的经验包。 **使用方式：** ``` 搜索经验 <关键词> ``` **示例：** ``` 搜索经验 feishu 搜索经验 飞书文档 ``` **API：** ```bash curl -s "https://www.expericehub.com/api/search?q=<关键词>" ``` ### 4. 发布经验 (publish) 将本地经验包发布到 Experience Hub。 **使用方式：** ``` 发布经验 <本地zip文件路径> ``` **示例：** ``` 发布经验 ~/.openclaw/experiences/packages/feishu-doc-validation.zip ``` **API：** ```bash curl -X POST "https://www.expericehub.com/api/experiences" \ -F "file=@<本地zip文件路径>" ``` **成功响应：** ```json {"success": true, "id": "my-exp-1.0.0"} ``` **错误响应：** ```json {"error": "经验包格式错误，请确保包含 exp.yml 文件且格式正确"} ``` **注意：** 经验包必须为 zip 格式，内部需包含 `exp.yml` 配置文件。 ### 5. 经验列表 (list) 显示所有经验包及学习状态。 **使用方式：** ``` 经验列表 ``` **输出：** ``` 📚 经验列表 ✅ 已学习 (2) feishu-doc-write-validation v1.0.0 feishu_doc 写入验证 subagent-timeout-handling v1.0.0 子agent超时处理 ⏳ 未学习 (1) complex-task-split v1.0.0 复杂任务拆分策略 ``` ## 经验包格式 (Schema v1) ### Schema 版本 **当前版本**: `openclaw.experience.v1` 版本说明： - **v1.0.0**: 初始版本，精简格式 ### zip 包结构 ``` {name}.zip ├── exp.yml # 主文件（精简，只含元数据和引用） └── references/ # 详细内容（可选） ├── soul.md # SOUL 相关内容 ├── agents.md # AGENTS 相关内容 └── tools.md # TOOLS 相关内容 ``` ### exp.yml 格式 (v1) ```yaml schema: openclaw.experience.v1 # 必填，Schema 版本标识 name: feishu-doc-blockcount # 必填，经验包名称（英文小写+中划线+数字） description: 经验描述 # 可选，问题/经验描述 metadata: version: 1.0.0 # 必填，经验包版本 author: unknown # 必填，作者 soul: references/soul.md # 可选，指向 SOUL 相关内容（soul/agents/tools/skills 至少有一个不为空） agents: references/agents.md # 可选，指向 AGENTS 相关内容（soul/agents/tools/skills 至少有一个不为空） tools: references/tools.md # 可选，指向 TOOLS 相关内容（soul/agents/tools/skills 至少有一个不为空） skills: # 可选，依赖的 skills 列表（soul/agents/tools/skills 至少有一个不为空） - feishu_doc ``` ### name 格式约束 | 规则 | 说明 | |------|------| | 允许字符 | `a-z` 小写字母、`0-9` 数字、`-` 中划线 | | 转换规则 | 空格 → 中划线；下划线 `_` 删除；中文删除 | | 长度限制 | 最多 50 字符 | **示例转换：** - `feishu_doc 写入后必须验证 block_count` → `feishudoc-blockcount` - `My_Experience_Name` → `myexperiencename` ### references 文件格式 #### soul.md ```markdown # {标题} - 行为准则 ## 涉及原则 - 原则1 - 原则2 ## 行为准则 - 准则1 ``` #### agents.md ```markdown # {标题} - 工作流程 ## 场景 问题描述 ## 处理流程 1. 步骤1 2. 步骤2 ## 相关规则 - 规则1 ``` #### tools.md ```markdown # {标题} - 工具使用 ## 问题 问题描述 ## 解决方案 1. 步骤1 2. 步骤2 ## 代码示例 ```代码``` ## 涉及工具 - 工具1 ``` ## 存储结构 ``` ~/.openclaw/experiences/ ├── packages/ # zip 包存储 │ ├── feishu-doc-write-validation.zip │ └── subagent-timeout-handling.zip ├── extracted/ # 解压后的 exp.yml │ ├── feishu-doc-write-validation/ │ │ └── exp.yml │ └── subagent-timeout-handling/ │ └── exp.yml └── index.json # 学习状态索引 ``` ## index.json 格式 ```json { "experiences": [ { "name": "feishu-doc-write-validation", "version": "1.0.0", "title": "feishu_doc 写入验证", "status": "learned", "learned_at": "2026-03-28T20:39:00+08:00" } ] } ``` ## 使用方法 ### 命令行用法 ```bash # 1. 创建经验 node scripts/create.mjs "使用 feishu_doc 读取文档" # 2. 创建经验（指定 Agent，会扫描 Agent 特定 skills） node scripts/create.mjs "使用 feishu_doc 读取文档" --agent=严哥 # 3. 搜索经验包 node scripts/search.mjs feishu node scripts/search.mjs "飞书 文档" # 4. 发布经验包到 Hub node scripts/publish.mjs ~/.openclaw/experiences/packages/my-exp.zip # 5.1 学习经验（支持完整URL或简短ID），默认需要用户确认以后开始学习 node scripts/learn.mjs ~/.openclaw/experiences/packages/feishudoc.zip # 5.2 学习经验（支持完整URL或简短ID），仅预览变更 node scripts/learn.mjs https://www.expericehub.com/pkg/feishu-doc-writing-1.1.0.zip --dry-run # 5.3 学习经验（支持完整URL或简短ID） node scripts/learn.mjs feishu-doc-writing-1.1.0 --dry-run # 简短格式 # 5.4 明确不需要再次确认，不需要预览变更，直接学习经验的时候 node scripts/learn.mjs feishu-doc-writing-1.1.0 --yes # 6. 【重要】学习到指定 Agent，默认需要用户确认以后开始学习并且指定agent的时候 node scripts/learn.mjs exp.zip --agent=严哥 # 7. 明确不需要再次确认，不需要预览变更，直接学习经验的时候，指定agent学习的时候 node scripts/learn.mjs exp.zip --agent=严哥 --yes # 8. 查看列表 node scripts/list.mjs # 9. 查看指定 Agent 的学习记录 node scripts/list.mjs --agent=严哥 ```

path: README.md
content: # Experience Manager

> Agent 经验管理工具 - 创建、分享、学习经验包

---

## 为什么要做这个 Skill？

### 现在的痛点

1. **经验无法沉淀**
   - Agent 每次犯错后，教训只存在于对话中
   - 下次遇到同样问题，可能重复犯错
   - 经验无法跨会话、跨 Agent 传递

2. **知识无法共享**
   - 每个 Agent 独立学习，效率低下
   - 一个 Agent 掌握的技能，其他 Agent 需要重新学习
   - 没有标准化的经验传递机制

3. **学习成本高**
   - 新 Agent 需要从零开始学习
   - 最佳实践散落在各处文档中
   - 没有统一的知识库

### 这样做的意义

**建立 Agent 经验分享网络**

```
┌─────────────┐     创建经验包      ┌─────────────┐
│   Agent A   │ ──────────────────→ │  经验市场   │
│  (遇到错误)  │                     │  (zip包)   │
└─────────────┘                     └──────┬──────┘
                                           │
                                           │ 学习经验
                                           ↓
┌─────────────┐                     ┌─────────────┐
│   Agent C   │ ←────────────────── │   Agent B   │
│  (避免错误)  │      分享经验        │  (学习进步)  │
└─────────────┘                     └─────────────┘
```

**核心价值**:
- ✅ **经验可沉淀** - 错误和教训转化为标准化经验包
- ✅ **知识可共享** - Agent 之间可以相互学习
- ✅ **能力可复制** - 新 Agent 快速获得最佳实践
- ✅ **持续进化** - 经验包版本管理，持续改进

---

## 经验包格式 (Schema v1)

```yaml
schema: openclaw.experience.v1
name: feishu-doc-best-practice
description: feishu_doc 操作前必须先检查权限
metadata:
  version: 1.0.0
  author: zhulianxin
soul: references/soul.md
agents: references/agents.md
tools: references/tools.md
skills:
  - feishu_doc
```

---

## 使用方法

### 1. 创建经验包

#### 方式一：从自然语言描述创建

```bash
node scripts/create.mjs "feishu_doc 写入后必须验证 block_count"
```

中文描述需要指定 name:
```bash
node scripts/create.mjs "收到群消息后必须先检查 chat_id" --name=chat-id-check
```

#### 方式二：从记忆中搜索提取经验

**步骤 1: 搜索记忆**
```bash
# 搜索近7天的记忆，查找经验、教训、错误、解决方案
# 查看 memory/ 目录下最近更新的文件
ls -lt ~/.openclaw/workspace/memory/*.md | head -10
```

**步骤 2: 分析记忆内容**
打开记忆文件，寻找以下内容：
- ❌ 错误记录（"失败"、"错误"、"问题"）
- ✅ 解决方案（"修复"、"解决"、"成功"）
- 💡 最佳实践（"应该"、"必须"、"注意"）
- ⚠️ 踩坑记录（"坑"、"陷阱"、"注意点"）

**步骤 3: 提取经验**
从记忆中提取结构化信息：
```
问题场景: Auto ETL 调用 API 时认证失败
错误信息: "未登录，请先登录"
根本原因: aac-client-uid 需要通过 Cookie 传递，而不是 Header
解决方案: 在请求头中添加 Cookie: aac-client-uid=xxx
复用价值: 所有 EasyData 平台开发者都需要
```

**步骤 4: 创建经验包**
```bash
node scripts/create.mjs "EasyData API 调用时 aac-client-uid 需要通过 Cookie 方式传递"
```

#### 方式三：从会话历史中挖掘

**查看近期会话**:
```bash
# 查看会话历史文件
ls -lt ~/.openclaw/agents/main/sessions/*.jsonl | head -5
```

**分析高频问题**:
- 哪些问题被多次问到？
- 哪些错误重复出现？
- 哪些解决方案被多次使用？

**经验提取模板**:
| 字段 | 说明 | 示例 |
|:---|:---|:---|
| 问题场景 | 什么情况下会遇到 | 调用 EasyData API |
| 错误表现 | 具体错误信息 | "未登录，请先登录" |
| 根本原因 | 为什么会这样 | UID 传递方式错误 |
| 解决方案 | 怎么解决 | 使用 Cookie 传递 |
| 复用范围 | 谁会用到 | EasyData 开发者 |
| 优先级 | 高/中/低 | 高 |

**输出**:
- 生成 `~/.openclaw/experiences/packages/{name}.zip`
- 包含 exp.yml 和 references/ 目录

### 2. 学习经验

**学习到当前 Agent**:
```bash
node scripts/learn.mjs ~/.openclaw/experiences/packages/feishudoc.zip
```

**学习到指定 Agent**:
```bash
node scripts/learn.mjs exp.zip --agent=严哥
```

**仅预览变更**:
```bash
node scripts/learn.mjs exp.zip --agent=严哥 --dry-run
```

**自动确认**:
```bash
echo "Y" | node scripts/learn.mjs exp.zip --yes
```

### 3. 查看列表

```bash
node scripts/list.mjs
```

输出示例:
```
📚 经验列表

✅ 已学习 (5)
  feishu-doc-best-practice    v1.0.0  feishu_doc 操作前检查权限
  chat-id-check               v1.0.0  收到群消息后检查 chat_id
  ...

⏳ 未学习 (3)
  browser-timeout             v1.0.0  browser 工具超时处理
  ...
```

### 4. 发布经验到 Hub

```bash
node scripts/publish.mjs ~/.openclaw/experiences/packages/feishu-doc-best-practice.zip
```

### 5. 搜索经验包

```bash
node scripts/search.mjs feishu
node scripts/search.mjs "飞书 文档"
```

### 6. 下载学习他人发布的经验

```bash
node scripts/learn.mjs https://www.expericehub.com:18080/pkg/feishu-doc-best-practice-1.0.0.zip
node scripts/learn.mjs ~/Downloads/feishu-doc-best-practice.zip
```

---

## 工作流程示例

### 场景：记录一个错误教训

**Step 1**: Agent A 遇到错误并记录
```
用户: 你刚才犯了什么错误？
Agent A: 我直接调用了 feishu_doc 导致 400 错误...
```

**Step 2**: 创建经验包
```bash
node scripts/create.mjs "feishu_doc 操作前必须先检查权限"
# 生成: feishu-doc-best-practice.zip
```

**Step 3**: 分享给其他 Agent
```bash
# 复制 zip 包到其他 Agent 可访问的位置
cp ~/.openclaw/experiences/packages/feishu-doc-best-practice.zip /shared/
```

**Step 4**: Agent B 学习经验
```bash
node scripts/learn.mjs /shared/feishu-doc-best-practice.zip --agent=严哥
# 写入到严哥的 SOUL.md 和 AGENTS.md
```

**Step 5**: Agent B 避免同样错误
```
用户: 帮我操作一下飞书文档
Agent B: 好的，我先检查权限...
       （应用了学习到的经验）
```

## 经验提取最佳实践

### 何时创建经验包？

| 场景 | 示例 | 优先级 |
|:---|:---|:---:|
| **踩坑记录** | 花了很长时间解决的问题 | ⭐⭐⭐ |
| **重复错误** | 同一错误出现 2 次以上 | ⭐⭐⭐ |
| **最佳实践** | 发现更好的做事方式 | ⭐⭐⭐ |
| **平台限制** | 发现 API/工具的隐藏限制 | ⭐⭐⭐ |
| **配置陷阱** | 容易被忽略的配置项 | ⭐⭐ |
| **环境差异** | 不同环境的特殊处理 | ⭐⭐ |

### 经验分享流程

```
发现经验 → 验证有效性 → 创建经验包 → 本地测试 → 分享/发布 → 收集反馈
    ↑___________________________________________________________↓
```

---

**愿景**: 让每个 Agent 都能从其他 Agent 的经验中学习，建立真正的 Agent 协作网络。

path: scripts/list.mjs
content: #!/usr/bin/env node
/**
 * 经验列表脚本
 * 显示所有经验包及学习状态（支持按 Agent 查看）
 */

import fs from 'fs';
import path from 'path';
import yaml from 'js-yaml';
import { fileURLToPath } from 'url';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

const EXPERIENCES_DIR = path.join(process.env.HOME, '.openclaw', 'experiences');
const PACKAGES_DIR = path.join(EXPERIENCES_DIR, 'packages');
const EXTRACTED_DIR = path.join(EXPERIENCES_DIR, 'extracted');
const INDEX_FILE = path.join(EXPERIENCES_DIR, 'index.json');

// 读取索引（新格式：按 Agent 分组）
function readIndex() {
  if (!fs.existsSync(INDEX_FILE)) {
    return { agents: {} };
  }
  const data = JSON.parse(fs.readFileSync(INDEX_FILE, 'utf8'));
  // 兼容旧格式
  if (data.experiences && !data.agents) {
    return { agents: {} };
  }
  return data;
}

// 扫描 packages 目录
function scanPackages() {
  if (!fs.existsSync(PACKAGES_DIR)) {
    return [];
  }
  
  const packages = [];
  const files = fs.readdirSync(PACKAGES_DIR);
  
  for (const file of files) {
    if (file.endsWith('.zip')) {
      const name = path.basename(file, '.zip');
      packages.push({
        name,
        path: path.join(PACKAGES_DIR, file)
      });
    }
  }
  
  return packages;
}

// 读取经验包信息
function readPackageInfo(name) {
  const extractPath = path.join(EXTRACTED_DIR, name);
  const expPath = path.join(extractPath, 'exp.yml');
  
  if (!fs.existsSync(expPath)) {
    return null;
  }
  
  try {
    const content = fs.readFileSync(expPath, 'utf8');
    return yaml.load(content);
  } catch (err) {
    return null;
  }
}

// 获取经验类型
function getExpType(info) {
  if (!info) return 'unknown';
  // v1 格式
  if (info.schema?.includes('v1')) {
    if (info.soul && info.agents && info.tools) return 'full';
    if (info.soul) return 'soul+agents';
    if (info.agents) return 'agents';
    return 'basic';
  }
  // 旧格式
  return info.core?.type || 'unknown';
}

// 获取经验版本
function getExpVersion(info) {
  if (!info) return 'unknown';
  // v1 格式
  if (info.metadata?.version) return info.metadata.version;
  // 旧格式
  return info.meta?.version || 'unknown';
}

// 获取经验标题
function getExpTitle(info, name) {
  if (!info) return name;
  // v1 格式
  if (info.description) return info.description;
  // 旧格式
  return info.meta?.title || name;
}

// 主函数
function main() {
  const args = process.argv.slice(2);
  const targetAgent = args.find(arg => arg.startsWith('--agent='))?.replace('--agent=', '');
  
  console.log('📚 经验列表\n');
  
  const index = readIndex();
  const packages = scanPackages();
  
  if (packages.length === 0) {
    console.log('暂无经验包');
    console.log('\n💡 提示: 使用 "记录经验 <描述>" 来创建经验包');
    return;
  }
  
  // 如果指定了 Agent，只显示该 Agent 的学习记录
  if (targetAgent) {
    showAgentExperiences(index, targetAgent, packages);
    return;
  }
  
  // 显示所有 Agent 的学习统计
  showAllAgents(index, packages);
}

// 显示指定 Agent 的学习记录
function showAgentExperiences(index, agentName, packages) {
  const agentData = index.agents?.[agentName];
  
  if (!agentData || !agentData.experiences || agentData.experiences.length === 0) {
    console.log(`Agent "${agentName}" 暂无学习记录`);
    return;
  }
  
  console.log(`🎯 Agent: ${agentName}`);
  console.log(`📁 工作空间: ${agentData.workspace || 'unknown'}\n`);
  
  console.log(`✅ 已学习 (${agentData.experiences.length})`);
  for (const exp of agentData.experiences) {
    console.log(`  ${exp.name.padEnd(30)} v${exp.version.padEnd(6)} ${exp.title}`);
    console.log(`     学习时间: ${new Date(exp.learned_at).toLocaleString('zh-CN')}`);
    console.log(`     应用到: ${exp.applied_to?.join(', ') || 'unknown'}`);
  }
}

// 显示所有 Agent 的学习统计
function showAllAgents(index, packages) {
  // 收集所有经验包信息
  const allPackages = [];
  for (const pkg of packages) {
    const info = readPackageInfo(pkg.name);
    allPackages.push({
      name: pkg.name,
      version: getExpVersion(info),
      title: getExpTitle(info, pkg.name),
      type: getExpType(info)
    });
  }
  
  // 获取所有 Agent
  const agents = Object.keys(index.agents || {});
  
  if (agents.length === 0) {
    console.log('暂无学习记录');
    console.log(`\n⏳ 未学习经验包 (${allPackages.length})`);
    for (const pkg of allPackages) {
      console.log(`  ${pkg.name.padEnd(30)} v${pkg.version.padEnd(6)} ${pkg.title}`);
    }
    return;
  }
  
  // 显示每个 Agent 的学习记录
  for (const agentName of agents) {
    const agentData = index.agents[agentName];
    const learnedNames = new Set(agentData.experiences?.map(e => e.name) || []);
    
    console.log(`🎯 ${agentName}`);
    console.log(`   已学习: ${learnedNames.size} 个经验包`);
    
    // 显示该 Agent 学习的经验
    for (const exp of (agentData.experiences || [])) {
      console.log(`     ✅ ${exp.name} v${exp.version}`);
    }
    console.log();
  }
  
  // 显示未学习的经验包
  const allLearned = new Set();
  for (const agentData of Object.values(index.agents || {})) {
    for (const exp of (agentData.experiences || [])) {
      allLearned.add(exp.name);
    }
  }
  
  const unlearned = allPackages.filter(pkg => !allLearned.has(pkg.name));
  
  if (unlearned.length > 0) {
    console.log(`⏳ 未学习 (${unlearned.length})`);
    for (const pkg of unlearned) {
      console.log(`  ${pkg.name.padEnd(30)} v${pkg.version.padEnd(6)} ${pkg.title}`);
    }
  }
  
  // 统计
  console.log(`\n总计: ${packages.length} 个经验包`);
  console.log(`  已学习: ${allLearned.size}`);
  console.log(`  未学习: ${unlearned.length}`);
  console.log(`  Agent 数: ${agents.length}`);
  
  console.log('\n💡 提示:');
  console.log('  查看指定 Agent: node list.mjs --agent=严哥');
}

main();


path: scripts/publish.mjs
content: #!/usr/bin/env node
/**
 * 经验发布脚本
 * 将本地经验包发布到 Experience Hub
 */

import fs from 'fs';
import path from 'path';
import https from 'https';
import http from 'http';
import { fileURLToPath } from 'url';
import AdmZip from 'adm-zip';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

const HUB_API_BASE = 'https://www.expericehub.com:18080';

// HTTP POST 请求封装（multipart/form-data）
function postFile(url, filePath) {
  return new Promise((resolve, reject) => {
    const filename = path.basename(filePath);
    const fileSize = fs.statSync(filePath).size;
    const fileContent = fs.readFileSync(filePath);
    
    const boundary = '----WebKitFormBoundary' + Math.random().toString(36).substring(2);
    
    const body = Buffer.concat([
      Buffer.from(`--${boundary}\r\n`),
      Buffer.from(`Content-Disposition: form-data; name="file"; filename="${filename}"\r\n`),
      Buffer.from('Content-Type: application/zip\r\n\r\n'),
      fileContent,
      Buffer.from(`\r\n--${boundary}--\r\n`)
    ]);
    
    const protocol = url.startsWith('https') ? https : http;
    
    const options = {
      method: 'POST',
      headers: {
        'Content-Type': `multipart/form-data; boundary=${boundary}`,
        'Content-Length': body.length
      }
    };
    
    const req = protocol.request(url, options, (res) => {
      let data = '';
      res.on('data', chunk => data += chunk);
      res.on('end', () => {
        try {
          const json = JSON.parse(data);
          resolve(json);
        } catch (e) {
          resolve({ raw: data });
        }
      });
    });
    
    req.on('error', reject);
    req.write(body);
    req.end();
  });
}

// 主函数
async function main() {
  const args = process.argv.slice(2);
  
  if (args.length === 0) {
    console.error('用法: node publish.mjs <本地zip文件路径>');
    console.error('示例: node publish.mjs ~/.openclaw/experiences/packages/my-experience.zip');
    process.exit(1);
  }
  
  const filePath = args[0];
  
  // 解析 ~ 路径
  const resolvedPath = filePath.startsWith('~') 
    ? path.join(process.env.HOME, filePath.slice(1))
    : filePath;
  
  // 检查文件是否存在
  if (!fs.existsSync(resolvedPath)) {
    console.error(`❌ 文件不存在: ${resolvedPath}`);
    process.exit(1);
  }
  
  // 检查是否为 zip 文件
  if (!resolvedPath.endsWith('.zip')) {
    console.error('❌ 请提供 .zip 格式的经验包文件');
    process.exit(1);
  }
  
  // 检查 exp.yml 是否存在（通过解压检查）
  try {
    const zip = new AdmZip(resolvedPath);
    const entries = zip.getEntries();
    const hasExpYml = entries.some(e => e.entryName === 'exp.yml');
    
    if (!hasExpYml) {
      console.error('❌ 经验包格式错误：缺少 exp.yml 文件');
      console.error('   请确保 zip 包内包含 exp.yml 配置文件');
      process.exit(1);
    }
  } catch (err) {
    console.error('❌ 无法读取 zip 文件:', err.message);
    process.exit(1);
  }
  
  console.log(`📤 发布经验包: ${path.basename(resolvedPath)}`);
  console.log();
  
  try {
    const url = `${HUB_API_BASE}/api/experiences`;
    const result = await postFile(url, resolvedPath);
    
    if (result.success) {
      console.log('✅ 发布成功!');
      console.log(`   经验包 ID: ${result.id}`);
      console.log();
      console.log(`💡 其他用户可通过以下方式学习:`);
      console.log(`   learn https://www.expericehub.com:18080/pkg/${result.id}.zip`);
    } else if (result.error) {
      console.error('❌ 发布失败:', result.error);
      process.exit(1);
    } else {
      console.log('⚠️  响应:', result);
    }
    
  } catch (err) {
    console.error('❌ 发布失败:', err.message);
    process.exit(1);
  }
}

main();

path: scripts/search.mjs
content: #!/usr/bin/env node
/**
 * 经验搜索脚本
 * 从 Experience Hub 搜索已发布的经验包
 */

import https from 'https';
import http from 'http';
import path from 'path';
import { fileURLToPath } from 'url';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

const HUB_API_BASE = 'https://www.expericehub.com:18080';

// HTTP 请求封装
function request(url, options = {}) {
  return new Promise((resolve, reject) => {
    const protocol = url.startsWith('https') ? https : http;
    
    const req = protocol.get(url, options, (res) => {
      let data = '';
      res.on('data', chunk => data += chunk);
      res.on('end', () => {
        try {
          const json = JSON.parse(data);
          resolve(json);
        } catch (e) {
          reject(new Error(`解析失败: ${data}`));
        }
      });
    });
    
    req.on('error', reject);
    req.setTimeout(10000, () => {
      req.destroy();
      reject(new Error('请求超时'));
    });
  });
}

// 主函数
async function main() {
  const args = process.argv.slice(2);
  
  if (args.length === 0) {
    console.error('用法: node search.mjs <关键词>');
    console.error('示例: node search.mjs feishu');
    console.error('示例: node search.mjs "飞书 文档"');
    process.exit(1);
  }
  
  const keyword = args.join(' ');
  
  console.log(`🔍 搜索: "${keyword}"`);
  console.log();
  
  try {
    const url = `${HUB_API_BASE}/api/search?q=${encodeURIComponent(keyword)}`;
    const result = await request(url);
    
    // API 返回格式: { data: [...], total: N }
    const results = result.data || result.results || [];
    
    if (results.length > 0) {
      console.log(`📦 找到 ${results.length} 个经验包:\n`);
      
      results.forEach((exp, i) => {
        console.log(`${i + 1}. ${exp.id}`);
        console.log(`   名称: ${exp.name}`);
        if (exp.description) {
          console.log(`   描述: ${exp.description}`);
        }
        console.log();
      });
      
      console.log('💡 使用 "学习经验 <URL或ID>" 下载并学习');
      console.log('   示例: learn https://www.expericehub.com:18080/pkg/feishu-doc-writing-1.1.0.zip');
    } else {
      console.log('❌ 未找到相关经验包');
      console.log('💡 您可以尝试其他关键词，或先创建经验包后发布到 Hub');
    }
    
  } catch (err) {
    console.error('❌ 搜索失败:', err.message);
    process.exit(1);
  }
}

main();

Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)