Skill: aohoyo/qiniu-kodo (version 1.0.3)

--- name: qiniu-kodo description: | 七牛云 KODO 对象存储技能。支持文件上传、下载、列出、删除、获取 URL 等操作。 三层架构：MCP 工具（优先）→ Node.js SDK → qshell CLI。 metadata: { "openclaw": { "emoji": "☁️", "requires": { "bins": ["node", "python3", "pip3"], "packages": ["qiniu", "qiniu-mcp-server"] }, "install": [ { "id": "qiniu-sdk", "kind": "node", "package": "qiniu", "label": "Install qiniu Node.js SDK" }, { "id": "qiniu-mcp", "kind": "python", "package": "qiniu-mcp-server", "label": "Install qiniu-mcp-server" }, ], }, } --- # ☁️ 七牛云 KODO 技能 通过 **MCP 工具** / **Node.js SDK** / **qshell CLI** 管理七牛云对象存储。 --- ## 🎯 执行策略（三层降级） | 优先级 | 工具 | 使用场景 | |--------|------|----------| | **1** | `qiniu-mcp-server` | 功能最全，优先使用 | | **2** | Node.js SDK | MCP 不可用时降级 | | **3** | qshell CLI | 最后备选 | --- ## 🚀 快速开始 ### 1. 安装依赖 ```bash # 进入技能目录 cd ~/.openclaw/workspace/skills/qiniu-kodo # 运行自动安装 bash scripts/setup.sh ``` ### 2. 配置凭证 ```bash bash scripts/setup.sh \ --access-key "YOUR_ACCESS_KEY" \ --secret-key "YOUR_SECRET_KEY" \ --region "z0" \ --bucket "mybucket" ``` ### 3. 测试连接 ```bash node scripts/qiniu_node.mjs test-connection ``` --- ## 📋 使用示例 ### 上传文件 ```bash node scripts/qiniu_node.mjs upload \ --local "/path/to/file.txt" \ --key "uploads/file.txt" ``` ### 列出文件 ```bash node scripts/qiniu_node.mjs list --prefix "uploads/" --limit 100 ``` ### 下载文件 ```bash node scripts/qiniu_node.mjs download \ --key "uploads/file.txt" \ --local "/path/to/save.txt" ``` ### 删除文件 ```bash node scripts/qiniu_node.mjs delete --key "uploads/file.txt" --force ``` ### 获取文件 URL ```bash # 公开空间 node scripts/qiniu_node.mjs url --key "uploads/file.txt" # 私有空间（1小时有效） node scripts/qiniu_node.mjs url --key "uploads/file.txt" --private --expires 3600 ``` --- ## 🔧 Node.js SDK API | 命令 | 说明 | |------|------| | `upload --local <path> --key <key>` | 上传文件 | | `download --key <key> --local <path>` | 下载文件 | | `list [--prefix <p>] [--limit <n>]` | 列出文件 | | `delete --key <key> [--force]` | 删除文件 | | `url --key <key> [--private] [--expires 
]` | 获取 URL | | `stat --key <key>` | 文件信息 | | `move --src-key <a> --dest-key <b>` | 移动文件 | | `copy --src-key <a> --dest-key <b>` | 复制文件 | | `test-connection` | 测试连接 | --- ## ⚙️ 配置文件 **config/qiniu-config.json** ```json { "accessKey": "YOUR_ACCESS_KEY", "secretKey": "YOUR_SECRET_KEY", "bucket": "mybucket", "region": "z0", "domain": "https://cdn.example.com" } ``` **区域代码**： - `z0` - 华东（杭州） - `z1` - 华北（河北） - `z2` - 华南（广州） - `na0` - 北美（洛杉矶） - `as0` - 东南亚（新加坡） --- ## 🐛 故障排查 | 问题 | 解决 | |------|------| | `Cannot find module 'qiniu'` | `npm install qiniu` | | `401 Unauthorized` | 检查 AccessKey/SecretKey | | `连接超时` | 检查区域代码和网络 | --- ## 📚 相关链接 - [七牛云 MCP Server](https://github.com/qiniu/qiniu-mcp-server) - [七牛云 Node.js SDK](https://developer.qiniu.com/kodo/sdk/1289/nodejs) - [qshell 工具](https://developer.qiniu.com/kodo/tools/1302/qshell) --- ## 📄 许可证 MIT License

path: README.md
content: # ☁️ 七牛云 KODO 技能

OpenClaw 技能，用于管理七牛云对象存储（KODO）。

## ✨ 功能

- 📤 上传文件
- 📥 下载文件
- 📋 列出文件
- 🗑️ 删除文件
- 🔗 获取文件 URL（支持私有空间签名）
- 📊 查看文件信息
- 📁 移动/复制文件

## 🚀 快速开始

```bash
# 安装依赖
npm install

# 配置凭证
bash scripts/setup.sh --access-key "xxx" --secret-key "xxx" --region "z0" --bucket "mybucket"

# 测试连接
node scripts/qiniu_node.mjs test-connection
```

## 📖 使用示例

```bash
# 上传
node scripts/qiniu_node.mjs upload --local file.txt --key uploads/file.txt

# 列出
node scripts/qiniu_node.mjs list --prefix uploads/

# 下载
node scripts/qiniu_node.mjs download --key uploads/file.txt --local file.txt

# 删除
node scripts/qiniu_node.mjs delete --key uploads/file.txt --force

# 获取 URL
node scripts/qiniu_node.mjs url --key uploads/file.txt
```

## 🔧 架构

三层降级策略：
1. **MCP 工具** (`qiniu-mcp-server`) - 功能最全
2. **Node.js SDK** (`qiniu`) - 稳定可靠
3. **qshell CLI** - 官方命令行

## 📄 许可证

MIT


path: _meta.json
content: {
  "name": "qiniu-kodo",
  "version": "1.0.0",
  "description": "七牛云 KODO 对象存储技能",
  "author": "33",
  "created": "2026-03-06",
  "tags": ["qiniu", "kodo", "oss", "cloud", "storage"],
  "requirements": {
    "bins": ["node", "npm"],
    "packages": ["qiniu"]
  },
  "config": {
    "configFile": "config/qiniu-config.json"
  }
}


path: config/qiniu-config.example.json
content: {
  "accessKey": "你的AccessKey",
  "secretKey": "你的SecretKey",
  "bucket": "你的存储桶名称",
  "region": "z0",
  "domain": "http://你的域名.com",
  "options": {
    "use_https": true,
    "use_cdn": true,
    "timeout": 30,
    "upload_threshold": 4194304,
    "chunk_size": 4194304,
    "retry_times": 3
  }
}


path: docs/EXAMPLES.md
content: # 七牛云 KODO 技能 - 使用示例

本文档包含七牛云 KODO 技能的各种使用示例。

---

## 📤 示例1：上传文件

### 在 OpenClaw 中

**用户说：**
```
帮我上传 /backups/daily-20260301.tar.gz 到七牛云
```

**AI 执行：**
```
优先级 1：尝试使用 qiniu-mcp MCP 工具
优先级 2：如果 MCP 不可用，使用 Python SDK
优先级 3：如果 SDK 也不可用，使用 qshell
```

**返回：**
```
✅ 上传成功！

文件：daily-20260301.tar.gz
大小：15.2 MB
URL：http://your-domain.com/backups/daily-20260301.tar.gz
```

### 命令行方式

```bash
node scripts/qiniu_node.mjs upload \
  --local /backups/daily-20260301.tar.gz \
  --key backups/daily-20260301.tar.gz
```

---

## 📋 示例2：列出文件

### 在 OpenClaw 中

**用户说：**
```
列出七牛云 backups 目录下的所有文件
```

**返回：**
```
📋 共找到 15 个文件：

backups/backup-20260301.tar.gz  - 15.2 MB  - 2026-03-01 20:00
backups/backup-20260228.tar.gz  - 14.8 MB  - 2026-02-28 20:00
backups/backup-20260227.tar.gz  - 16.1 MB  - 2026-02-27 20:00
...
```

### 命令行方式

```bash
# 列出所有文件
node scripts/qiniu_node.mjs list

# 列出指定前缀的文件
node scripts/qiniu_node.mjs list --prefix backups/ --limit 50

# JSON 格式输出
node scripts/qiniu_node.mjs list --prefix backups/ --format json
```

---

## 🔗 示例3：获取文件 URL

### 在 OpenClaw 中

**用户说：**
```
给我 images/photo.jpg 的访问链接，1小时有效
```

**返回：**
```
🔗 临时访问链接（1小时有效）：

http://your-domain.com/images/photo.jpg?e=1583097600&token=...
```

### 命令行方式

```bash
# 公开空间 URL
node scripts/qiniu_node.mjs url --key images/photo.jpg

# 私有空间 URL（1小时有效）
node scripts/qiniu_node.mjs url --key documents/report.pdf --private --expires 3600
```

---

## 🗑️ 示例4：删除文件

### 在 OpenClaw 中

**用户说：**
```
删除七牛云上的 old-backup.tar.gz
```

**AI 执行：**
```
1. 先确认：是否真的要删除？
2. 用户确认后执行删除
```

**返回：**
```
✅ 删除成功！

文件：old-backup.tar.gz
```

### 命令行方式

```bash
# 删除单个文件（需要确认）
node scripts/qiniu_node.mjs delete --key backups/old-backup.tar.gz

# 强制删除（不需要确认）
node scripts/qiniu_node.mjs delete --key backups/old-backup.tar.gz --force

# 批量删除
cat > delete-list.txt << EOF
backups/backup-20260101.tar.gz
backups/backup-20260102.tar.gz
EOF

node scripts/qiniu_node.mjs batch-delete --file delete-list.txt
```

---

## 📊 示例5：获取文件信息

### 在 OpenClaw 中

**用户说：**
```
查看 documents/report.pdf 的详细信息
```

**返回：**
```
📊 文件信息：

文件名：documents/report.pdf
大小：2.5 MB
类型：application/pdf
Hash：FhGxK...（文件哈希值）
修改时间：2026-03-01 15:30:00
```

### 命令行方式

```bash
node scripts/qiniu_node.mjs stat --key documents/report.pdf
```

---

## 📥 示例6：下载文件

### 在 OpenClaw 中

**用户说：**
```
从七牛云下载 documents/report.pdf 到本地
```

**返回：**
```
✅ 下载成功！

文件：documents/report.pdf
大小：2.5 MB
保存到：/downloads/report.pdf
```

### 命令行方式

```bash
node scripts/qiniu_node.mjs download \
  --key documents/report.pdf \
  --local /downloads/report.pdf
```

---

## 🔄 示例7：移动和复制文件

### 在 OpenClaw 中

**用户说：**
```
把 temp/file.txt 移动到 archive/file.txt
```

**返回：**
```
✅ 文件已移动

原路径：temp/file.txt
新路径：archive/file.txt
```

### 命令行方式（需要扩展）

```bash
# 移动文件（需要扩展 Python 脚本）
# 复制文件（需要扩展 Python 脚本）
```

---

## 🖼️ 示例8：图片处理（需要 MCP）

### 在 OpenClaw 中

**用户说：**
```
把 images/photo.jpg 缩小到 800x600，加水印
```

**AI 执行：**
```
使用 qiniu-mcp MCP 工具进行图片处理
（需要 MCP 工具支持）
```

**返回：**
```
✅ 图片处理完成！

原图：http://your-domain.com/images/photo.jpg
处理后：http://your-domain.com/images/photo.jpg?imageView2/1/w/800/h/600|watermark/...
```

---

## 🤖 示例9：自动化备份脚本

### 完整脚本

```bash
#!/bin/bash

# 每日备份脚本
BACKUP_DIR="/backups"
DATE=$(date +%Y%m%d)
BACKUP_FILE="backup-${DATE}.tar.gz"

# 1. 创建备份
echo "📦 创建备份..."
tar -czf "${BACKUP_DIR}/${BACKUP_FILE}" /data

# 2. 上传到七牛云
echo "📤 上传到七牛云..."
cd /home/node/.openclaw/workspace/skills/qiniu-kodo
node scripts/qiniu_node.mjs upload \
  --local "${BACKUP_DIR}/${BACKUP_FILE}" \
  --key "backups/${BACKUP_FILE}"

# 3. 清理本地旧备份（保留7天）
echo "🗑️  清理旧备份..."
find ${BACKUP_DIR} -name "backup-*.tar.gz" -mtime +7 -delete

# 4. 清理云端旧备份（保留30天）
echo "🗑️  清理云端旧备份..."
python -c "
from scripts.qiniu_python import QiniuKodo
from datetime import datetime, timedelta

kodo = QiniuKodo()
cutoff = datetime.now() - timedelta(days=30)

for file in kodo.list_files(prefix='backups/'):
    if datetime.fromtimestamp(file['mtime']) < cutoff:
        kodo.delete(file['key'])
        print(f'已删除: {file[\"key\"]}')
"

echo "✅ 备份完成！"
```

---

## 📝 示例10：批量操作

### 批量上传图片

```python
from scripts.qiniu_python import QiniuKodo
import os

kodo = QiniuKodo()

image_dir = "/path/to/images"
uploaded = 0

for filename in os.listdir(image_dir):
    if filename.endswith(('.jpg', '.png', '.gif')):
        local_path = os.path.join(image_dir, filename)
        key = f"images/{filename}"

        try:
            result = kodo.upload(local_path, key)
            print(f"✅ {filename} -> {result['url']}")
            uploaded += 1
        except Exception as e:
            print(f"❌ {filename} 上传失败: {e}")

print(f"\n✅ 共上传 {uploaded} 个文件")
```

---

## 🔧 高级用法

### 使用配置文件

```python
from scripts.qiniu_python import QiniuKodo

# 使用自定义配置文件
kodo = QiniuKodo(config_path='/path/to/custom-config.json')

# 上传文件
result = kodo.upload('/path/to/file.txt', 'uploads/file.txt')
```

### 错误处理

```python
from scripts.qiniu_python import QiniuKodo

kodo = QiniuKodo()

try:
    result = kodo.upload('/path/to/file.txt', 'uploads/file.txt')
    print(f"上传成功: {result['url']}")
except FileNotFoundError as e:
    print(f"文件不存在: {e}")
except Exception as e:
    print(f"上传失败: {e}")
```

---

## 🎯 更多示例

查看 [SKILL.md](../SKILL.md) 获取更多详细信息和 API 文档。


path: package.json
content: {
  "name": "qiniu-kodo-skill",
  "version": "1.0.0",
  "description": "七牛云 KODO 对象存储技能",
  "main": "scripts/qiniu_node.mjs",
  "type": "module",
  "scripts": {
    "setup": "bash scripts/setup.sh",
    "test": "node scripts/qiniu_node.mjs test-connection"
  },
  "dependencies": {
    "qiniu": "^7.14.0"
  },
  "keywords": ["qiniu", "kodo", "oss", "openclaw", "skill"],
  "author": "33",
  "license": "MIT"
}


path: scripts/qiniu_node.mjs
content: #!/usr/bin/env node

/**
 * 七牛云 KODO Node.js SDK 脚本
 * 功能：文件上传、下载、列出、删除、获取URL等操作
 */

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

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

// 尝试加载 qiniu SDK
import qiniu from 'qiniu';

// 配置文件路径
const SKILL_DIR = path.dirname(__dirname);
const CONFIG_DIR = path.join(SKILL_DIR, 'config');
const CONFIG_FILE = path.join(CONFIG_DIR, 'qiniu-config.json');

/**
 * 加载配置文件
 */
function loadConfig() {
  if (!fs.existsSync(CONFIG_FILE)) {
    throw new Error(
      `配置文件不存在: ${CONFIG_FILE}\n` +
      `请复制 config/qiniu-config.example.json 为 qiniu-config.json 并填写配置`
    );
  }

  const config = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf-8'));

  // 验证必填配置
  const required = ['accessKey', 'secretKey', 'bucket'];
  for (const key of required) {
    if (!config[key] || config[key].startsWith('你的')) {
      throw new Error(`配置项 ${key} 不能为空或使用示例值`);
    }
  }

  return config;
}

/**
 * 七牛云 KODO 操作类
 */
class QiniuKodo {
  constructor(config) {
    this.config = config;
    this.mac = new qiniu.auth.digest.Mac(config.accessKey, config.secretKey);
    this.bucketManager = new qiniu.rs.BucketManager(this.mac);
    this.bucket = config.bucket;
    this.domain = config.domain || '';
    
    // 配置上传选项
    this.uploadOptions = {
      uphost: this.getUpHost(config.region)
    };
  }

  /**
   * 获取上传域名
   */
  getUpHost(region) {
    const hosts = {
      'z0': 'https://upload.qiniup.com',
      'z1': 'https://upload-z1.qiniup.com',
      'z2': 'https://upload-z2.qiniup.com',
      'na0': 'https://upload-na0.qiniup.com',
      'as0': 'https://upload-as0.qiniup.com'
    };
    return hosts[region] || hosts['z0'];
  }

  /**
   * 上传文件
   */
  async upload(localPath, key) {
    if (!fs.existsSync(localPath)) {
      throw new Error(`文件不存在: ${localPath}`);
    }

    return new Promise((resolve, reject) => {
      const putPolicy = new qiniu.rs.PutPolicy({ scope: `${this.bucket}:${key}` });
      const uploadToken = putPolicy.uploadToken(this.mac);
      
      const config = new qiniu.conf.Config();
      config.zone = qiniu.zone.Zone_z0; // 根据区域设置
      
      const formUploader = new qiniu.form_up.FormUploader(config);
      const putExtra = new qiniu.form_up.PutExtra();
      
      formUploader.putFile(uploadToken, key, localPath, putExtra, (respErr, respBody, respInfo) => {
        if (respErr) {
          reject(respErr);
          return;
        }
        
        if (respInfo.statusCode === 200) {
          resolve({
            success: true,
            key: respBody.key,
            hash: respBody.hash,
            url: this.domain ? this.getUrl(key) : null,
            size: fs.statSync(localPath).size
          });
        } else {
          reject(new Error(`上传失败: ${respInfo.statusCode} ${JSON.stringify(respBody)}`));
        }
      });
    });
  }

  /**
   * 下载文件
   */
  async download(key, localPath) {
    const url = this.getUrl(key, true);
    
    return new Promise((resolve, reject) => {
      const protocol = url.startsWith('https') ? https : http;
      
      protocol.get(url, (response) => {
        if (response.statusCode !== 200) {
          reject(new Error(`下载失败: ${response.statusCode}`));
          return;
        }
        
        // 创建目录
        const dir = path.dirname(localPath);
        if (!fs.existsSync(dir)) {
          fs.mkdirSync(dir, { recursive: true });
        }
        
        const fileStream = fs.createWriteStream(localPath);
        response.pipe(fileStream);
        
        fileStream.on('finish', () => {
          fileStream.close();
          resolve({
            success: true,
            key: key,
            size: fs.statSync(localPath).size
          });
        });
      }).on('error', reject);
    });
  }

  /**
   * 列出文件
   */
  async listFiles(prefix = '', limit = 100) {
    return new Promise((resolve, reject) => {
      this.bucketManager.listPrefix(this.bucket, {
        prefix: prefix,
        limit: limit
      }, (err, respBody, respInfo) => {
        if (err) {
          reject(err);
          return;
        }
        
        if (respInfo.statusCode === 200) {
          const files = respBody.items.map(item => ({
            key: item.key,
            size: item.fsize,
            mtime: item.putTime / 1000000,
            hash: item.hash,
            mimeType: item.mimeType
          }));
          resolve(files);
        } else {
          reject(new Error(`列出文件失败: ${respInfo.statusCode}`));
        }
      });
    });
  }

  /**
   * 删除文件
   */
  async delete(key) {
    return new Promise((resolve, reject) => {
      this.bucketManager.delete(this.bucket, key, (err, respBody, respInfo) => {
        if (err) {
          if (respInfo && respInfo.statusCode === 612) {
            reject(new Error(`文件不存在: ${key}`));
          } else {
            reject(err);
          }
          return;
        }
        
        if (respInfo.statusCode === 200) {
          resolve({ success: true, key: key });
        } else {
          reject(new Error(`删除失败: ${respInfo.statusCode}`));
        }
      });
    });
  }

  /**
   * 批量删除
   */
  async batchDelete(keys) {
    const deleteOperations = keys.map(key => 
      qiniu.rs.deleteOp(this.bucket, key)
    );
    
    return new Promise((resolve, reject) => {
      this.bucketManager.batch(deleteOperations, (err, respBody, respInfo) => {
        if (err) {
          reject(err);
          return;
        }
        
        let success = 0;
        let failed = 0;
        
        respBody.forEach(item => {
          if (item.code === 200) {
            success++;
          } else {
            failed++;
          }
        });
        
        resolve({ success, failed });
      });
    });
  }

  /**
   * 获取文件信息
   */
  async stat(key) {
    return new Promise((resolve, reject) => {
      this.bucketManager.stat(this.bucket, key, (err, respBody, respInfo) => {
        if (err) {
          reject(err);
          return;
        }
        
        if (respInfo.statusCode === 200) {
          resolve({
            key: key,
            size: respBody.fsize,
            hash: respBody.hash,
            mimeType: respBody.mimeType,
            mtime: respBody.putTime / 1000000
          });
        } else {
          reject(new Error(`获取文件信息失败: ${respInfo.statusCode}`));
        }
      });
    });
  }

  /**
   * 获取文件 URL
   */
  getUrl(key, isPrivate = false, expires = 3600) {
    if (!this.domain) {
      throw new Error('配置中缺少 domain 字段');
    }

    const baseUrl = `${this.domain}/${key}`;

    if (isPrivate) {
      const deadline = Math.floor(Date.now() / 1000) + expires;
      const sign = qiniu.util.hmacSha1(baseUrl, this.mac.secretKey);
      const encodedSign = qiniu.util.base64ToUrlSafe(sign);
      const downloadToken = `${this.mac.accessKey}:${encodedSign}`;
      return `${baseUrl}?e=${deadline}&[REDACTED_SECRET]`;
    } else {
      return baseUrl;
    }
  }

  /**
   * 移动文件
   */
  async move(srcKey, destKey, force = false) {
    return new Promise((resolve, reject) => {
      this.bucketManager.move(this.bucket, srcKey, this.bucket, destKey, { force }, (err, respBody, respInfo) => {
        if (err) {
          reject(err);
          return;
        }
        
        if (respInfo.statusCode === 200) {
          resolve({
            success: true,
            srcKey: srcKey,
            destKey: destKey
          });
        } else {
          reject(new Error(`移动失败: ${respInfo.statusCode}`));
        }
      });
    });
  }

  /**
   * 复制文件
   */
  async copy(srcKey, destKey, force = false) {
    return new Promise((resolve, reject) => {
      this.bucketManager.copy(this.bucket, srcKey, this.bucket, destKey, { force }, (err, respBody, respInfo) => {
        if (err) {
          reject(err);
          return;
        }
        
        if (respInfo.statusCode === 200) {
          resolve({
            success: true,
            srcKey: srcKey,
            destKey: destKey
          });
        } else {
          reject(new Error(`复制失败: ${respInfo.statusCode}`));
        }
      });
    });
  }
}

// 工具函数
function formatSize(bytes) {
  const units = ['B', 'KB', 'MB', 'GB', 'TB'];
  let size = bytes;
  let unitIndex = 0;
  
  while (size >= 1024 && unitIndex < units.length - 1) {
    size /= 1024;
    unitIndex++;
  }
  
  return `${size.toFixed(2)} ${units[unitIndex]}`;
}

function formatTime(timestamp) {
  const date = new Date(timestamp * 1000);
  return date.toISOString().replace('T', ' ').substring(0, 19);
}

// 命令行接口
async function main() {
  const args = process.argv.slice(2);
  const command = args[0];
  
  try {
    const config = loadConfig();
    const kodo = new QiniuKodo(config);
    
    switch (command) {
      case 'upload': {
        const localPath = args[args.indexOf('--local') + 1];
        const key = args[args.indexOf('--key') + 1];
        const result = await kodo.upload(localPath, key);
        console.log('✅ 上传成功!');
        console.log(`  文件: ${result.key}`);
        console.log(`  大小: ${formatSize(result.size)}`);
        console.log(`  URL: ${result.url}`);
        break;
      }
      
      case 'download': {
        const key = args[args.indexOf('--key') + 1];
        const localPath = args[args.indexOf('--local') + 1];
        const result = await kodo.download(key, localPath);
        console.log('✅ 下载成功!');
        console.log(`  文件: ${result.key}`);
        console.log(`  大小: ${formatSize(result.size)}`);
        console.log(`  保存到: ${localPath}`);
        break;
      }
      
      case 'list': {
        const prefixIndex = args.indexOf('--prefix');
        const prefix = prefixIndex !== -1 ? args[prefixIndex + 1] : '';
        
        const limitIndex = args.indexOf('--limit');
        const limit = limitIndex !== -1 ? parseInt(args[limitIndex + 1]) : 100;
        
        const formatIndex = args.indexOf('--format');
        const format = formatIndex !== -1 ? args[formatIndex + 1] : 'table';
        
        const files = await kodo.listFiles(prefix, limit);
        
        if (files.length === 0) {
          console.log('📭 没有找到文件');
          return;
        }
        
        if (format === 'json') {
          console.log(JSON.stringify(files, null, 2));
        } else {
          console.log(`📋 共 ${files.length} 个文件:\n`);
          console.log(`${'文件名'.padEnd(50)} ${'大小'.padStart(12)} ${'修改时间'.padEnd(20)}`);
          console.log('-'.repeat(82));
          files.forEach(file => {
            console.log(`${file.key.padEnd(50)} ${formatSize(file.size).padStart(12)} ${formatTime(file.mtime).padEnd(20)}`);
          });
        }
        break;
      }
      
      case 'delete': {
        const key = args[args.indexOf('--key') + 1];
        const forceIndex = args.indexOf('--force');
        
        if (forceIndex === -1) {
          console.log(`⚠️  确定要删除 ${key} 吗？(y/N):`);
          // 注意：这里需要 readline，为简化暂时跳过确认
        }
        
        const result = await kodo.delete(key);
        console.log('✅ 删除成功!');
        console.log(`  文件: ${result.key}`);
        break;
      }
      
      case 'batch-delete': {
        const fileIndex = args.indexOf('--file');
        const listFile = args[fileIndex + 1];
        
        const keys = fs.readFileSync(listFile, 'utf-8')
          .split('\n')
          .map(line => line.trim())
          .filter(line => line);
        
        const result = await kodo.batchDelete(keys);
        console.log('✅ 批量删除完成!');
        console.log(`  成功: ${result.success}`);
        console.log(`  失败: ${result.failed}`);
        break;
      }
      
      case 'url': {
        const key = args[args.indexOf('--key') + 1];
        const privateIndex = args.indexOf('--private');
        const expiresIndex = args.indexOf('--expires');
        
        const isPrivate = privateIndex !== -1;
        const expires = expiresIndex !== -1 ? parseInt(args[expiresIndex + 1]) : 3600;
        
        const url = kodo.getUrl(key, isPrivate, expires);
        console.log('🔗 文件 URL:');
        console.log(`  ${url}`);
        if (isPrivate) {
          console.log(`\n  ⏱️  有效期: ${expires} 秒`);
        }
        break;
      }
      
      case 'stat': {
        const key = args[args.indexOf('--key') + 1];
        const info = await kodo.stat(key);
        console.log('📊 文件信息:\n');
        console.log(`  文件名: ${info.key}`);
        console.log(`  大小: ${formatSize(info.size)}`);
        console.log(`  类型: ${info.mimeType}`);
        console.log(`  Hash: ${info.hash}`);
        console.log(`  修改时间: ${formatTime(info.mtime)}`);
        break;
      }
      
      case 'test-connection': {
        await kodo.listFiles('', 1);
        console.log('✅ 七牛云连接验证成功!');
        break;
      }
      
      default:
        console.log('使用方法:');
        console.log('  node qiniu_node.mjs upload --local <LocalPath> --key <Key>');
        console.log('  node qiniu_node.mjs download --key <Key> --local <LocalPath>');
        console.log('  node qiniu_node.mjs list [--prefix <Prefix>] [--limit <Limit>]');
        console.log('  node qiniu_node.mjs delete --key <Key> [--force]');
        console.log('  node qiniu_node.mjs url --key <Key> [--private] [--expires <Seconds>]');
        console.log('  node qiniu_node.mjs stat --key <Key>');
        console.log('  node qiniu_node.mjs test-connection');
    }
    
  } catch (error) {
    console.error(`❌ 错误: ${error.message}`);
    process.exit(1);
  }
}

main();


path: scripts/setup.sh
content: #!/bin/bash

#######################################
# 七牛云 KODO 技能 - 自动安装配置脚本
# 参考：Tencent Cloud COS 技能
#######################################

set -e

# 颜色输出
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color

# 默认值
CHECK_ONLY=false
INSTALL_MCP=false
INSTALL_SDK=false
ACCESS_KEY=""
SECRET_KEY=""
REGION=""
BUCKET=""
DOMAIN=""

# 脚本目录
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
BASE_DIR="$(dirname "$SCRIPT_DIR")"
CONFIG_DIR="$BASE_DIR/config"
CONFIG_FILE="$CONFIG_DIR/qiniu-config.json"

# 日志函数
log_info() {
    echo -e "${GREEN}[INFO]${NC} $1"
}

log_warn() {
    echo -e "${YELLOW}[WARN]${NC} $1"
}

log_error() {
    echo -e "${RED}[ERROR]${NC} $1"
}

# 解析参数
parse_args() {
    while [[ $# -gt 0 ]]; do
        case $1 in
            --check-only)
                CHECK_ONLY=true
                shift
                ;;
            --install-mcp)
                INSTALL_MCP=true
                shift
                ;;
            --install-sdk)
                INSTALL_SDK=true
                shift
                ;;
            --access-key)
                ACCESS_KEY="$2"
                shift 2
                ;;
            --secret-key)
                SECRET_KEY="$2"
                shift 2
                ;;
            --region)
                REGION="$2"
                shift 2
                ;;
            --bucket)
                BUCKET="$2"
                shift 2
                ;;
            --domain)
                DOMAIN="$2"
                shift 2
                ;;
            *)
                log_error "未知参数: $1"
                exit 1
                ;;
        esac
    done
}

# 检查命令是否存在
command_exists() {
    command -v "$1" >/dev/null 2>&1
}

# 检查 Node.js 环境
check_nodejs() {
    log_info "检查 Node.js 环境..."
    
    if command_exists node; then
        NODE_VERSION=$(node --version 2>&1)
        log_info "✅ Node.js 已安装: $NODE_VERSION"
        return 0
    else
        log_error "❌ Node.js 未安装"
        return 1
    fi
}

# 检查 npm
check_npm() {
    log_info "检查 npm..."
    
    if command_exists npm; then
        NPM_VERSION=$(npm --version 2>&1)
        log_info "✅ npm 已安装: $NPM_VERSION"
        return 0
    else
        log_error "❌ npm 未安装"
        return 1
    fi
}

# 检查 qiniu-mcp 是否已安装
check_mcp() {
    log_info "检查 qiniu-mcp..."
    
    if command_exists qiniu-mcp || [ -f "$HOME/.mcporter/servers/qiniu-mcp" ]; then
        log_info "✅ qiniu-mcp 已安装"
        return 0
    else
        log_warn "⚠️  qiniu-mcp 未安装"
        return 1
    fi
}

# 检查 qiniu Node.js SDK
check_sdk() {
    log_info "检查 qiniu Node.js SDK..."
    
    # 检查技能目录下的 node_modules
    if [ -d "$BASE_DIR/node_modules/qiniu" ]; then
        SDK_VERSION=$(node -p "require('$BASE_DIR/node_modules/qiniu/package.json').version" 2>/dev/null || echo "unknown")
        log_info "✅ qiniu Node.js SDK 已安装: $SDK_VERSION"
        return 0
    else
        log_warn "⚠️  qiniu Node.js SDK 未安装"
        return 1
    fi
}

# 检查 qshell 命令行工具
check_qshell() {
    log_info "检查 qshell..."
    
    if command_exists qshell; then
        QSHELL_VERSION=$(qshell version 2>&1 | head -1)
        log_info "✅ qshell 已安装: $QSHELL_VERSION"
        return 0
    else
        log_warn "⚠️  qshell 未安装"
        return 1
    fi
}

# 检查配置文件
check_config() {
    log_info "检查配置文件..."
    
    if [ -f "$CONFIG_FILE" ]; then
        log_info "✅ 配置文件已存在: $CONFIG_FILE"
        
        # 验证配置（使用 node 检查 JSON）
        if node -e "require('$CONFIG_FILE')" 2>/dev/null; then
            log_info "✅ 配置文件格式正确"
            return 0
        else
            log_error "❌ 配置文件格式错误"
            return 1
        fi
    else
        log_warn "⚠️  配置文件不存在"
        return 1
    fi
}

# 检查凭证是否已配置
check_credentials() {
    log_info "检查凭证配置..."
    
    if [ -f "$CONFIG_FILE" ]; then
        ACCESS_KEY_CHECK=$(node -p "require('$CONFIG_FILE').accessKey || ''" 2>/dev/null)
        
        if [ -n "$ACCESS_KEY_CHECK" ] && [ "$ACCESS_KEY_CHECK" != "你的AccessKey" ]; then
            log_info "✅ 凭证已配置"
            return 0
        else
            log_warn "⚠️  凭证未配置或使用示例值"
            return 1
        fi
    else
        log_warn "⚠️  配置文件不存在"
        return 1
    fi
}

# 安装 qiniu Node.js SDK
install_sdk() {
    log_info "安装 qiniu Node.js SDK..."
    
    cd "$BASE_DIR"
    
    # 初始化 package.json（如果不存在）
    if [ ! -f "package.json" ]; then
        npm init -y > /dev/null 2>&1
    fi
    
    # 安装 qiniu SDK
    if npm install qiniu --save 2>/dev/null; then
        log_info "✅ qiniu Node.js SDK 安装成功"
        return 0
    else
        log_error "❌ qiniu Node.js SDK 安装失败"
        return 1
    fi
}

# 安装 qiniu-mcp
install_mcp() {
    log_info "安装 qiniu-mcp..."
    
    # 检查 Node.js
    if ! check_nodejs; then
        log_error "需要先安装 Node.js"
        return 1
    fi
    
    # 检查 npm
    if ! check_npm; then
        log_error "需要先安装 npm"
        return 1
    fi
    
    # 安装 mcporter
    if ! command_exists mcporter; then
        log_info "安装 mcporter..."
        npm install -g @openclaw/mcporter
    fi
    
    # 安装 qiniu-mcp-server
    if npm install -g @qiniu/qiniu-mcp-server 2>/dev/null; then
        log_info "✅ qiniu-mcp 安装成功"
        
        # 配置 mcporter
        if [ -d "$HOME/.mcporter" ]; then
            cat > "$HOME/.mcporter/mcporter.json" <<EOF
{
  "servers": {
    "qiniu-mcp": {
      "command": "qiniu-mcp-server",
      "env": {
        "QINIU_ACCESS_KEY": "$ACCESS_KEY",
        "QINIU_SECRET_KEY": "$SECRET_KEY"
      }
    }
  }
}
EOF
            log_info "✅ mcporter 配置已更新"
        fi
        
        return 0
    else
        log_error "❌ qiniu-mcp 安装失败"
        return 1
    fi
}

# 安装 qshell
install_qshell() {
    log_info "安装 qshell..."
    
    local OS=$(uname -s | tr '[:upper:]' '[:lower:]')
    local ARCH=$(uname -m)
    
    if [ "$ARCH" = "x86_64" ]; then
        ARCH="x64"
    elif [ "$ARCH" = "aarch64" ]; then
        ARCH="arm64"
    fi
    
    local QSHELL_URL="https://devtools.qiniu.com/qshell-${OS}-${ARCH}-v2.6.2.zip"
    
    log_info "下载 qshell: $QSHELL_URL"
    
    local TEMP_DIR=$(mktemp -d)
    cd "$TEMP_DIR"
    
    if wget -q "$QSHELL_URL" -O qshell.zip && unzip -q qshell.zip && chmod +x qshell; then
        sudo mv qshell /usr/local/bin/ 2>/dev/null || mv qshell "$HOME/.local/bin/"
        log_info "✅ qshell 安装成功"
        cd - > /dev/null
        rm -rf "$TEMP_DIR"
        return 0
    else
        log_error "❌ qshell 安装失败"
        cd - > /dev/null
        rm -rf "$TEMP_DIR"
        return 1
    fi
}

# 创建配置文件
create_config() {
    log_info "创建配置文件..."
    
    mkdir -p "$CONFIG_DIR"
    
    cat > "$CONFIG_FILE" <<EOF
{
  "accessKey": "$ACCESS_KEY",
  "secretKey": "$SECRET_KEY",
  "bucket": "$BUCKET",
  "region": "$REGION",
  "domain": "$DOMAIN",
  "options": {
    "use_https": true,
    "use_cdn": true,
    "timeout": 30,
    "upload_threshold": 4194304,
    "chunk_size": 4194304,
    "retry_times": 3
  }
}
EOF
    
    # 设置权限
    chmod 600 "$CONFIG_FILE"
    
    log_info "✅ 配置文件已创建: $CONFIG_FILE"
}

# 配置 shell 环境
configure_shell() {
    log_info "配置 shell 环境..."
    
    local SHELL_RC=""
    
    if [ -n "$ZSH_VERSION" ]; then
        SHELL_RC="$HOME/.zshrc"
    else
        SHELL_RC="$HOME/.bashrc"
    fi
    
    # 添加环境变量
    if ! grep -q "QINIU_ACCESS_KEY" "$SHELL_RC" 2>/dev/null; then
        cat >> "$SHELL_RC" <<EOF

# 七牛云 KODO 配置
export QINIU_ACCESS_KEY="$ACCESS_KEY"
export QINIU_SECRET_KEY="$SECRET_KEY"
export QINIU_BUCKET="$BUCKET"
export QINIU_REGION="$REGION"
EOF
        log_info "✅ Shell 环境已配置: $SHELL_RC"
    else
        log_info "✅ Shell 环境已存在配置"
    fi
}

# 配置 qshell
configure_qshell() {
    if command_exists qshell; then
        log_info "配置 qshell 账号..."
        
        if qshell account "$ACCESS_KEY" "$SECRET_KEY" "openclaw" 2>/dev/null; then
            log_info "✅ qshell 账号配置成功"
        else
            log_warn "⚠️  qshell 账号配置失败"
        fi
    fi
}

# 验证连接
verify_connection() {
    log_info "验证七牛云连接..."
    
    if node "$SCRIPT_DIR/qiniu_node.mjs" test-connection 2>/dev/null; then
        log_info "✅ 七牛云连接验证成功"
        return 0
    else
        log_error "❌ 七牛云连接验证失败"
        return 1
    fi
}

# 环境检查
check_environment() {
    log_info "=========================================="
    log_info "  七牛云 KODO 环境检查"
    log_info "=========================================="
    echo ""
    
    local ALL_OK=true
    
    check_nodejs || ALL_OK=false
    check_npm || ALL_OK=false
    check_mcp || true     # 可选
    check_sdk || ALL_OK=false
    check_qshell || true  # 可选
    check_config || true
    check_credentials || true
    
    echo ""
    log_info "=========================================="
    
    if $ALL_OK; then
        log_info "✅ 环境检查通过"
        return 0
    else
        log_warn "⚠️  环境检查未完全通过"
        return 1
    fi
}

# 完整安装
full_setup() {
    log_info "=========================================="
    log_info "  七牛云 KODO 自动安装"
    log_info "=========================================="
    echo ""
    
    # 检查必需项
    if ! check_nodejs; then
        log_error "Node.js 是必需的"
        exit 1
    fi
    
    if ! check_npm; then
        log_error "npm 是必需的"
        exit 1
    fi
    
    # 安装 SDK
    if ! check_sdk; then
        install_sdk
    fi
    
    # 安装 MCP（可选）
    if ! check_mcp; then
        log_info "是否安装 qiniu-mcp? [y/N]:"
        read -r INSTALL_MCP_CHOICE
        if [ "$INSTALL_MCP_CHOICE" = "y" ] || [ "$INSTALL_MCP_CHOICE" = "Y" ]; then
            install_mcp
        fi
    fi
    
    # 安装 qshell（可选）
    if ! check_qshell; then
        log_info "是否安装 qshell? [y/N]:"
        read -r INSTALL_QSHELL_CHOICE
        if [ "$INSTALL_QSHELL_CHOICE" = "y" ] || [ "$INSTALL_QSHELL_CHOICE" = "Y" ]; then
            install_qshell
        fi
    fi
    
    # 创建配置
    if [ -n "$ACCESS_KEY" ] && [ -n "$SECRET_KEY" ]; then
        create_config
        configure_shell
        configure_qshell
        verify_connection
    fi
    
    echo ""
    log_info "=========================================="
    log_info "✅ 安装完成！"
    log_info "=========================================="
}

# 主函数
main() {
    parse_args "$@"
    
    if $CHECK_ONLY; then
        check_environment
    elif $INSTALL_MCP; then
        install_mcp
    elif $INSTALL_SDK; then
        install_sdk
    elif [ -n "$ACCESS_KEY" ]; then
        full_setup
    else
        check_environment
        echo ""
        log_info "使用方法："
        log_info "  检查环境: $0 --check-only"
        log_info "  完整安装: $0 --access-key <KEY> --secret-key <KEY> --region <REGION> --bucket <BUCKET>"
        log_info "  安装 SDK: $0 --install-sdk"
        log_info "  安装 MCP: $0 --install-mcp"
    fi
}

main "$@"


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