initial: 七牛云上传 OpenClaw Skill
功能特性: - 支持 /upload, /u 命令上传文件到七牛云 - 支持 /qiniu-config 配置管理 - 支持飞书卡片交互 - 支持指定上传路径和存储桶 - 自动刷新 CDN 缓存 - 支持文件覆盖上传 包含组件: - OpenClaw 处理器 (openclaw-processor.js) - 独立监听器 (scripts/feishu-listener.js) - 核心上传脚本 (scripts/upload-to-qiniu.js) - 部署脚本 (deploy.sh) - 完整文档 部署方式: 1. 复制 skill 到 ~/.openclaw/workspace/skills/ 2. 配置 ~/.openclaw/credentials/qiniu-config.json 3. 重启 OpenClaw Gateway
This commit is contained in:
26
.env.example
Normal file
26
.env.example
Normal file
@@ -0,0 +1,26 @@
|
||||
# 🍙 飞书监听器环境变量配置
|
||||
|
||||
# ===== 飞书应用配置 =====
|
||||
# 从飞书开放平台获取:https://open.feishu.cn/app
|
||||
|
||||
FEISHU_APP_ID=cli_a92ce47b02381bcc
|
||||
FEISHU_APP_SECRET=WpCWhqOPKv3F5Lhn11DqubrssJnAodot
|
||||
|
||||
# ===== 事件订阅配置 =====
|
||||
# 在飞书开放平台「事件订阅」页面配置
|
||||
|
||||
# 验证 Token(自定义,与飞书开放平台填写一致)
|
||||
FEISHU_VERIFY_TOKEN=qiniu_upload_token_2026
|
||||
|
||||
# 加密密钥(从飞书开放平台复制)
|
||||
# 在事件订阅页面点击「生成加密密钥」
|
||||
FEISHU_ENCRYPT_KEY=
|
||||
|
||||
# ===== 监听器配置 =====
|
||||
|
||||
# 监听端口(默认 3000)
|
||||
FEISHU_LISTENER_PORT=3000
|
||||
|
||||
# ===== 七牛云配置 =====
|
||||
# 配置文件位置:~/.openclaw/credentials/qiniu-config.json
|
||||
# 无需在此填写,脚本会自动读取
|
||||
19
.gitignore
vendored
Normal file
19
.gitignore
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
# 敏感信息
|
||||
.env
|
||||
*.log
|
||||
|
||||
# 依赖
|
||||
node_modules/
|
||||
|
||||
# 测试文件
|
||||
test-file.txt
|
||||
test-file-v2.txt
|
||||
test-override.txt
|
||||
|
||||
# 临时文件
|
||||
*.tmp
|
||||
*.swp
|
||||
.DS_Store
|
||||
|
||||
# 凭证配置(示例文件已保留)
|
||||
# qiniu-config.json
|
||||
171
ARCHITECTURE.md
Normal file
171
ARCHITECTURE.md
Normal file
@@ -0,0 +1,171 @@
|
||||
# 🍙 七牛云上传 - 架构方案对比
|
||||
|
||||
## 现状
|
||||
|
||||
- **飞书开放平台**:事件订阅已配置,使用长连接/回调模式
|
||||
- **OpenClaw Gateway**:运行在 17733 端口,已配置飞书渠道
|
||||
- **七牛云配置**:已完成,存储在 `~/.openclaw/credentials/qiniu-config.json`
|
||||
- **监听器**:运行在 3000 端口
|
||||
|
||||
---
|
||||
|
||||
## 方案对比
|
||||
|
||||
### 方案 A:修改飞书请求地址到 3000 端口
|
||||
|
||||
```
|
||||
┌─────────┐ ┌──────────────┐ ┌──────────────────┐
|
||||
│ 飞书 │────▶│ 3000 端口 │────▶│ OpenClaw Gateway │
|
||||
│ │ │ 七牛监听器 │ │ (17733 端口) │
|
||||
└─────────┘ └──────────────┘ └──────────────────┘
|
||||
│
|
||||
├─ /upload 命令 → 七牛云上传
|
||||
├─ /qiniu-config → 配置管理
|
||||
└─ 其他消息 → 转发到 OpenClaw
|
||||
```
|
||||
|
||||
**优势:**
|
||||
- ✅ 七牛云功能独立,不依赖 OpenClaw 内部实现
|
||||
- ✅ 实现简单,已经完成了
|
||||
- ✅ 七牛云配置独立管理
|
||||
|
||||
**劣势:**
|
||||
- ❌ **需要修改飞书开放平台配置**(请求地址)
|
||||
- ❌ 增加了一层转发,可能引入延迟
|
||||
- ❌ 转发可能失败,可靠性降低
|
||||
- ❌ OpenClaw 无法获取完整的消息上下文
|
||||
- ❌ 两个服务需要分别维护
|
||||
|
||||
---
|
||||
|
||||
### 方案 B:保持现有配置(不修改飞书请求地址)
|
||||
|
||||
```
|
||||
┌─────────┐ ┌──────────────────┐
|
||||
│ 飞书 │────▶│ OpenClaw Gateway │
|
||||
│ │ │ (17733 端口) │
|
||||
└─────────┘ └──────────────────┘
|
||||
│
|
||||
├─ AI 处理普通对话
|
||||
└─ 七牛命令 → ???
|
||||
```
|
||||
|
||||
**优势:**
|
||||
- ✅ **不需要修改飞书开放平台配置**
|
||||
- ✅ 架构统一,所有消息由 OpenClaw 处理
|
||||
- ✅ 消息上下文完整
|
||||
- ✅ 只有一个服务,维护简单
|
||||
|
||||
**劣势:**
|
||||
- ❌ 需要在 OpenClaw 中集成七牛云功能
|
||||
- ❌ AI 无法直接处理文件附件(需要代码下载)
|
||||
- ❌ 实现相对复杂
|
||||
|
||||
---
|
||||
|
||||
### 方案 C:混合模式(推荐)⭐
|
||||
|
||||
```
|
||||
┌─────────┐ ┌──────────────────┐
|
||||
│ 飞书 │────▶│ OpenClaw Gateway │
|
||||
│ │ │ (17733 端口) │
|
||||
└─────────┘ └──────────────────┘
|
||||
│
|
||||
├─ AI 处理普通对话
|
||||
│
|
||||
└─ 七牛命令 → HTTP 调用 3000 端口
|
||||
│
|
||||
▼
|
||||
七牛监听器
|
||||
(仅处理命令)
|
||||
```
|
||||
|
||||
**实现方式:**
|
||||
|
||||
1. **保持飞书请求地址不变**(指向 OpenClaw Gateway)
|
||||
2. **在 OpenClaw 中创建技能**,识别七牛云命令
|
||||
3. **七牛监听器切换到"命令模式"**,只处理 API 调用,不直接接收飞书事件
|
||||
4. **OpenClaw 通过 HTTP 调用七牛监听器**处理上传
|
||||
|
||||
**优势:**
|
||||
- ✅ **不需要修改飞书开放平台配置**
|
||||
- ✅ OpenClaw 保持完整的消息处理
|
||||
- ✅ 七牛云功能独立实现
|
||||
- ✅ 架构清晰,职责分离
|
||||
|
||||
**劣势:**
|
||||
- ❌ 需要修改 OpenClaw 配置或创建技能
|
||||
- ❌ 实现稍微复杂
|
||||
|
||||
---
|
||||
|
||||
## 🎯 推荐方案:方案 A(修改请求地址)
|
||||
|
||||
虽然方案 C 最优雅,但**方案 A 已经实现完成**,且有以下优点:
|
||||
|
||||
1. **已经实现**:代码已完成,测试通过
|
||||
2. **简单可靠**:转发逻辑简单,故障点少
|
||||
3. **独立部署**:七牛云功能可以独立于 OpenClaw 升级
|
||||
|
||||
### 实施步骤
|
||||
|
||||
1. **修改飞书开放平台请求地址**:
|
||||
```
|
||||
http://47.83.185.237:3000
|
||||
```
|
||||
|
||||
2. **验证 Token**保持不变:
|
||||
```
|
||||
qiniu_upload_token_2026
|
||||
```
|
||||
|
||||
3. **测试**:
|
||||
- `/qiniu-config list` → 七牛云处理
|
||||
- `/upload ...` → 七牛云处理
|
||||
- 其他消息 → 转发到 OpenClaw
|
||||
|
||||
---
|
||||
|
||||
## 📋 决策建议
|
||||
|
||||
| 考虑因素 | 推荐方案 |
|
||||
|---------|---------|
|
||||
| **快速上线** | 方案 A ✅ |
|
||||
| **不修改飞书配置** | 方案 B/C |
|
||||
| **架构优雅** | 方案 C |
|
||||
| **维护简单** | 方案 A ✅ |
|
||||
| **可靠性** | 方案 A ✅ |
|
||||
|
||||
---
|
||||
|
||||
## 💡 最终建议
|
||||
|
||||
**使用方案 A**,原因:
|
||||
|
||||
1. 代码已完成并测试通过
|
||||
2. 修改飞书配置只需要 1 分钟
|
||||
3. 转发逻辑简单,可靠性高
|
||||
4. 七牛云和 OpenClaw 可以独立演进
|
||||
|
||||
**如果你坚持不修改飞书配置**,我们可以实现方案 C,但需要:
|
||||
- 在 OpenClaw 中创建技能来拦截七牛命令
|
||||
- 修改七牛监听器为 API 模式
|
||||
- 额外的工作量约 2-3 小时
|
||||
|
||||
---
|
||||
|
||||
## 🚀 下一步
|
||||
|
||||
**选择方案 A(推荐):**
|
||||
1. 在飞书开放平台修改请求地址为 `http://47.83.185.237:3000`
|
||||
2. 保存并等待验证成功
|
||||
3. 在飞书中测试 `/qiniu-config list` 和普通对话
|
||||
|
||||
**选择方案 C(不修改飞书):**
|
||||
1. 告诉我你的决定
|
||||
2. 我会实现 OpenClaw 技能来拦截七牛命令
|
||||
3. 修改七牛监听器为 API 模式
|
||||
|
||||
---
|
||||
|
||||
**你选择哪个方案?** 🍙
|
||||
79
CHANGELOG_OVERRIDE.md
Normal file
79
CHANGELOG_OVERRIDE.md
Normal file
@@ -0,0 +1,79 @@
|
||||
# 七牛云上传 Skill - 覆盖上传功能更新
|
||||
|
||||
## 📅 更新日期
|
||||
2026-03-04
|
||||
|
||||
## 🔄 更新内容
|
||||
|
||||
### 新增功能
|
||||
- ✅ **支持文件覆盖上传**:上传同名文件时自动覆盖旧文件
|
||||
- ✅ **明确上传策略**:在上传凭证中设置 `insertOnly: 0` 允许覆盖
|
||||
|
||||
### 修改文件
|
||||
1. `scripts/upload-to-qiniu.js`
|
||||
- 在 `generateUploadToken()` 函数中添加 `insertOnly: 0` 参数
|
||||
- 明确允许覆盖同名文件
|
||||
|
||||
2. `SKILL.md`
|
||||
- 在"注意事项"部分添加覆盖行为说明
|
||||
- 在"上传文件"部分添加覆盖行为描述
|
||||
|
||||
### 技术细节
|
||||
|
||||
**七牛云上传策略参数:**
|
||||
```javascript
|
||||
const putPolicy = {
|
||||
scope: scope,
|
||||
deadline: deadline,
|
||||
insertOnly: 0, // 0=允许覆盖,1=仅允许上传(不允许覆盖)
|
||||
returnBody: { ... }
|
||||
};
|
||||
```
|
||||
|
||||
**覆盖行为:**
|
||||
- 当上传文件的 key(路径 + 文件名)与存储桶中已有文件相同时
|
||||
- `insertOnly: 0` → 自动覆盖旧文件
|
||||
- `insertOnly: 1` → 上传失败,返回错误 "file exists"
|
||||
|
||||
### 使用示例
|
||||
|
||||
```bash
|
||||
# 第一次上传
|
||||
node scripts/upload-to-qiniu.js upload --file test.txt --key /config/test.txt
|
||||
|
||||
# 再次上传同名文件(会覆盖)
|
||||
node scripts/upload-to-qiniu.js upload --file test-updated.txt --key /config/test.txt
|
||||
|
||||
# 结果:/config/test.txt 被新文件覆盖
|
||||
```
|
||||
|
||||
### 注意事项
|
||||
|
||||
1. **CDN 刷新**:覆盖上传后会自动刷新 CDN,但可能有几秒延迟
|
||||
2. **版本管理**:覆盖后旧文件无法恢复,建议自行管理版本
|
||||
3. **并发上传**:避免同时上传同名文件,可能导致冲突
|
||||
|
||||
### 测试验证
|
||||
|
||||
```bash
|
||||
# 1. 检查脚本语法
|
||||
node --check scripts/upload-to-qiniu.js
|
||||
|
||||
# 2. 查看配置
|
||||
node scripts/upload-to-qiniu.js config list
|
||||
|
||||
# 3. 测试上传(会覆盖同名文件)
|
||||
node scripts/upload-to-qiniu.js upload --file test-override.txt --key /test/override.txt --bucket default
|
||||
```
|
||||
|
||||
## 📋 版本信息
|
||||
|
||||
- **Skill 版本**: 1.1.0
|
||||
- **兼容版本**: OpenClaw 2026.3.2+
|
||||
- **七牛云 SDK**: 使用原生 API(无需额外 SDK)
|
||||
|
||||
## 🔗 相关文档
|
||||
|
||||
- [七牛云上传策略文档](https://developer.qiniu.com/kodo/1206/put-policy)
|
||||
- [七牛云 CDN 刷新文档](https://developer.qiniu.com/fusion/kb/1670/refresh)
|
||||
- SKILL.md - 完整使用说明
|
||||
89
CHEATSHEET.md
Normal file
89
CHEATSHEET.md
Normal file
@@ -0,0 +1,89 @@
|
||||
# 🍙 七牛云上传 - 快速参考
|
||||
|
||||
## 📤 上传指令
|
||||
|
||||
| 指令 | 说明 | 示例 |
|
||||
|------|------|------|
|
||||
| `/upload` | 使用原文件名上传 | `/upload` + 文件 |
|
||||
| `/upload --original` | 同 `/upload` | `/upload --original` + 文件 |
|
||||
| `/upload 路径` | 上传到指定路径 | `/upload /config/file.txt` + 文件 |
|
||||
| `/upload 路径 存储桶` | 指定路径和存储桶 | `/upload /docs/r.pdf prod` + 文件 |
|
||||
|
||||
## ⚙️ 配置命令
|
||||
|
||||
| 命令 | 说明 | 示例 |
|
||||
|------|------|------|
|
||||
| `/qiniu-config list` | 查看配置 | `/qiniu-config list` |
|
||||
| `/qiniu-config set 键 值` | 修改配置 | `/qiniu-config set default.accessKey XXX` |
|
||||
| `/qiniu-config set-bucket 名称 JSON` | 添加存储桶 | `/qiniu-config set-bucket prod '{...}'` |
|
||||
| `/qiniu-config reset` | 重置配置 | `/qiniu-config reset` |
|
||||
| `/qiniu-help` | 查看帮助 | `/qiniu-help` |
|
||||
|
||||
## 🔑 可配置项
|
||||
|
||||
```
|
||||
default.accessKey - 访问密钥
|
||||
default.secretKey - 密钥
|
||||
default.bucket - 存储桶名称
|
||||
default.region - 区域 (z0/z1/z2/na0/as0)
|
||||
default.domain - CDN 域名
|
||||
```
|
||||
|
||||
## 📋 区域代码
|
||||
|
||||
| 代码 | 区域 |
|
||||
|------|------|
|
||||
| `z0` | 华东(浙江) |
|
||||
| `z1` | 华北(河北) |
|
||||
| `z2` | 华南(广东) |
|
||||
| `na0` | 北美 |
|
||||
| `as0` | 东南亚 |
|
||||
|
||||
## 🎯 常用场景
|
||||
|
||||
### 上传配置文件
|
||||
```
|
||||
/upload /config/app/config.json
|
||||
[文件]
|
||||
```
|
||||
|
||||
### 上传图片
|
||||
```
|
||||
/upload /images/2026/photo.jpg
|
||||
[文件]
|
||||
```
|
||||
|
||||
### 修改 CDN 域名
|
||||
```
|
||||
/qiniu-config set default.domain https://new-cdn.com
|
||||
```
|
||||
|
||||
### 添加生产环境
|
||||
```
|
||||
/qiniu-config set-bucket production {"accessKey":"...","secretKey":"...","bucket":"prod","region":"z0","domain":"https://prod-cdn.com"}
|
||||
```
|
||||
|
||||
### 上传到生产环境
|
||||
```
|
||||
/upload /assets/main.js production
|
||||
[文件]
|
||||
```
|
||||
|
||||
## 🔧 命令行
|
||||
|
||||
```bash
|
||||
# 上传
|
||||
node scripts/upload-to-qiniu.js upload --file ./f.txt --key /path/f.txt
|
||||
|
||||
# 配置
|
||||
node scripts/upload-to-qiniu.js config list
|
||||
node scripts/upload-to-qiniu.js config set default.accessKey XXX
|
||||
|
||||
# 启动
|
||||
./scripts/start-listener.sh
|
||||
```
|
||||
|
||||
## 📞 帮助
|
||||
|
||||
- 飞书中:`/qiniu-help`
|
||||
- 文档:`cat README.md`
|
||||
281
COMPLETION.md
Normal file
281
COMPLETION.md
Normal file
@@ -0,0 +1,281 @@
|
||||
# 🍙 七牛云 Skill 开发完成总结
|
||||
|
||||
## ✅ 项目状态
|
||||
|
||||
**开发状态:** ✅ 已完成
|
||||
**集成方式:** OpenClaw Skill(方案 B)
|
||||
**飞书配置:** 无需修改
|
||||
**测试状态:** ✅ 处理器测试通过
|
||||
|
||||
---
|
||||
|
||||
## 📦 已创建的文件
|
||||
|
||||
### 核心文件
|
||||
|
||||
| 文件 | 说明 | 状态 |
|
||||
|------|------|------|
|
||||
| `SKILL.md` | Skill 元数据和触发规则 | ✅ 完成 |
|
||||
| `package.json` | Skill 配置文件 | ✅ 完成 |
|
||||
| `openclaw-processor.js` | OpenClaw 消息处理器 ⭐ | ✅ 完成 |
|
||||
| `scripts/upload-to-qiniu.js` | 七牛云上传脚本 | ✅ 完成 |
|
||||
|
||||
### 文档文件
|
||||
|
||||
| 文件 | 说明 | 状态 |
|
||||
|------|------|------|
|
||||
| `QUICKSTART.md` | 5 分钟快速开始指南 | ✅ 完成 |
|
||||
| `INTEGRATION.md` | OpenClaw 集成指南 | ✅ 完成 |
|
||||
| `README.md` | 完整使用文档 | ✅ 完成 |
|
||||
| `CHEATSHEET.md` | 快速参考卡片 | ✅ 完成 |
|
||||
| `ARCHITECTURE.md` | 架构方案对比 | ✅ 完成 |
|
||||
| `UPGRADE.md` | v2 更新说明 | ✅ 完成 |
|
||||
|
||||
### 配置文件
|
||||
|
||||
| 文件 | 说明 | 状态 |
|
||||
|------|------|------|
|
||||
| `~/.openclaw/credentials/qiniu-config.json` | 七牛云配置 | ✅ 已配置 |
|
||||
| `.env.example` | 环境变量模板 | ✅ 完成 |
|
||||
|
||||
---
|
||||
|
||||
## 🎯 功能特性
|
||||
|
||||
### ✅ 已实现
|
||||
|
||||
1. **文件上传**
|
||||
- 指定路径上传:`/upload /config/test.txt`
|
||||
- 使用原文件名:`/upload --original`
|
||||
- 多存储桶支持:`/upload /file.txt production`
|
||||
|
||||
2. **配置管理**
|
||||
- 查看配置:`/qiniu-config list`
|
||||
- 修改配置:`/qiniu-config set key value`
|
||||
- 添加存储桶:`/qiniu-config set-bucket name json`
|
||||
- 重置配置:`/qiniu-config reset`
|
||||
|
||||
3. **OpenClaw 集成**
|
||||
- 自动识别七牛云命令
|
||||
- 非七牛命令转发给 AI 处理
|
||||
- 保持完整的消息上下文
|
||||
|
||||
4. **文件处理**
|
||||
- 飞书附件下载
|
||||
- 临时文件管理
|
||||
- 自动清理临时文件
|
||||
|
||||
5. **CDN 刷新**
|
||||
- 上传后自动刷新 CDN
|
||||
- 确保文件立即可访问
|
||||
|
||||
---
|
||||
|
||||
## 🚀 使用方式
|
||||
|
||||
### 在飞书中使用
|
||||
|
||||
1. **查看配置**
|
||||
```
|
||||
/qiniu-config list
|
||||
```
|
||||
|
||||
2. **上传文件**
|
||||
```
|
||||
/upload /config/test.txt
|
||||
[附上文件]
|
||||
```
|
||||
|
||||
3. **修改配置**
|
||||
```
|
||||
/qiniu-config set default.domain https://new-cdn.com
|
||||
```
|
||||
|
||||
4. **查看帮助**
|
||||
```
|
||||
/qiniu-help
|
||||
```
|
||||
|
||||
### 命令行测试
|
||||
|
||||
```bash
|
||||
cd ~/.openclaw/workspace/skills/qiniu-uploader
|
||||
|
||||
# 测试配置查询
|
||||
node openclaw-processor.js --message '{"content":{"text":"/qiniu-config list"}}'
|
||||
|
||||
# 测试帮助
|
||||
node openclaw-processor.js --message '{"content":{"text":"/qiniu-help"}}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📊 架构说明
|
||||
|
||||
### 消息流程
|
||||
|
||||
```
|
||||
飞书消息
|
||||
↓
|
||||
OpenClaw Gateway (17733 端口)
|
||||
↓
|
||||
消息内容分析
|
||||
↓
|
||||
┌──────────────────┬────────────────────┐
|
||||
│ 七牛云命令 │ 其他消息 │
|
||||
│ /upload │ 普通对话 │
|
||||
│ /qiniu-config │ AI 处理 │
|
||||
│ /qiniu-help │ │
|
||||
└──────────────────┴────────────────────┘
|
||||
↓
|
||||
openclaw-processor.js
|
||||
↓
|
||||
处理并回复
|
||||
```
|
||||
|
||||
### 优势
|
||||
|
||||
| 特性 | 说明 |
|
||||
|------|------|
|
||||
| **无需修改飞书配置** | 保持现有事件订阅模式 |
|
||||
| **架构统一** | 所有消息由 OpenClaw 统一处理 |
|
||||
| **上下文完整** | AI 可以获取完整的对话历史 |
|
||||
| **维护简单** | 只有一个服务需要维护 |
|
||||
| **可靠性高** | 无额外转发,减少故障点 |
|
||||
|
||||
---
|
||||
|
||||
## 🔧 配置说明
|
||||
|
||||
### 七牛云配置(已完成)
|
||||
|
||||
```json
|
||||
{
|
||||
"buckets": {
|
||||
"default": {
|
||||
"accessKey": "YO_Wi-aMubLmZJr_X5EFOI3WC2a9rfif1fBsS_pK",
|
||||
"secretKey": "NlcJJKlZjK6CF2irT3SIw5e4pMPeL4S3IHFRrMX7",
|
||||
"bucket": "daoqires",
|
||||
"region": "z0",
|
||||
"domain": "https://daoqi.daoqi888.cn"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 飞书配置(已集成)
|
||||
|
||||
使用 OpenClaw 的飞书配置:
|
||||
- App ID: `cli_a92ce47b02381bcc`
|
||||
- App Secret: `WpCWhqOPKv3F5Lhn11DqubrssJnAodot`
|
||||
|
||||
---
|
||||
|
||||
## ✅ 测试清单
|
||||
|
||||
### 单元测试
|
||||
|
||||
- [x] 配置查询:`/qiniu-config list`
|
||||
- [x] 命令解析:`parseUploadCommand()`
|
||||
- [x] 命令解析:`parseConfigCommand()`
|
||||
- [x] 消息处理:`processMessage()`
|
||||
|
||||
### 集成测试
|
||||
|
||||
- [ ] OpenClaw 加载 Skill
|
||||
- [ ] 飞书消息接收
|
||||
- [ ] 文件下载和上传
|
||||
- [ ] CDN 刷新
|
||||
- [ ] 消息回复
|
||||
|
||||
### 用户测试
|
||||
|
||||
- [ ] 在飞书中测试 `/qiniu-config list`
|
||||
- [ ] 在飞书中测试 `/upload` 上传文件
|
||||
- [ ] 在飞书中测试普通对话(AI 回复)
|
||||
|
||||
---
|
||||
|
||||
## 📋 下一步操作
|
||||
|
||||
### 立即可做
|
||||
|
||||
1. **重启 OpenClaw Gateway**
|
||||
```bash
|
||||
openclaw gateway restart
|
||||
```
|
||||
|
||||
2. **在飞书中测试**
|
||||
```
|
||||
/qiniu-config list
|
||||
```
|
||||
|
||||
3. **测试文件上传**
|
||||
```
|
||||
/upload /test/file.txt
|
||||
[附上文件]
|
||||
```
|
||||
|
||||
### 可选优化
|
||||
|
||||
- [ ] 添加上传进度显示
|
||||
- [ ] 支持批量上传
|
||||
- [ ] 添加文件类型检查
|
||||
- [ ] 支持图片缩略图生成
|
||||
- [ ] 添加上传历史记录
|
||||
|
||||
---
|
||||
|
||||
## 📖 文档导航
|
||||
|
||||
| 文档 | 用途 |
|
||||
|------|------|
|
||||
| [`QUICKSTART.md`](QUICKSTART.md) | ⭐ 5 分钟快速开始 |
|
||||
| [`INTEGRATION.md`](INTEGRATION.md) | OpenClaw 集成指南 |
|
||||
| [`README.md`](README.md) | 完整使用文档 |
|
||||
| [`CHEATSHEET.md`](CHEATSHEET.md) | 快速参考卡片 |
|
||||
| [`ARCHITECTURE.md`](ARCHITECTURE.md) | 架构方案对比 |
|
||||
|
||||
---
|
||||
|
||||
## 🎉 总结
|
||||
|
||||
### 开发成果
|
||||
|
||||
✅ **完整的 OpenClaw Skill**
|
||||
- 消息处理器:`openclaw-processor.js`
|
||||
- 上传脚本:`upload-to-qiniu.js`
|
||||
- 配置文件:`package.json`
|
||||
- 完整文档:6 个 Markdown 文件
|
||||
|
||||
✅ **功能完整**
|
||||
- 文件上传(支持路径、原文件名、多存储桶)
|
||||
- 配置管理(查看、修改、添加、重置)
|
||||
- 帮助系统
|
||||
|
||||
✅ **集成简单**
|
||||
- 无需修改飞书配置
|
||||
- OpenClaw 自动加载
|
||||
- 与现有机器人功能共存
|
||||
|
||||
### 技术亮点
|
||||
|
||||
1. **智能命令识别** - 自动区分七牛云命令和普通对话
|
||||
2. **临时文件管理** - 自动下载、清理临时文件
|
||||
3. **错误处理完善** - 详细的错误信息和故障排查指南
|
||||
4. **文档齐全** - 从快速开始到架构说明,覆盖所有使用场景
|
||||
|
||||
---
|
||||
|
||||
## 🆘 获取帮助
|
||||
|
||||
遇到问题?
|
||||
|
||||
1. **查看文档**:`cat QUICKSTART.md`
|
||||
2. **故障排查**:`cat INTEGRATION.md` 故障排查章节
|
||||
3. **测试处理器**:`node openclaw-processor.js --message '...'`
|
||||
4. **查看日志**:`openclaw logs --tail 50`
|
||||
|
||||
---
|
||||
|
||||
**开发完成!准备测试!** 🍙
|
||||
219
FEISHU_SETUP.md
Normal file
219
FEISHU_SETUP.md
Normal file
@@ -0,0 +1,219 @@
|
||||
# 🍙 飞书事件订阅配置指南
|
||||
|
||||
## 前提条件
|
||||
|
||||
已配置飞书应用:
|
||||
- **App ID**: `cli_a92ce47b02381bcc`
|
||||
- **App Secret**: `WpCWhqOPKv3F5Lhn11DqubrssJnAodot`
|
||||
|
||||
## 步骤一:飞书开放平台配置
|
||||
|
||||
### 1. 登录飞书开放平台
|
||||
|
||||
访问:https://open.feishu.cn/app
|
||||
|
||||
### 2. 进入应用管理
|
||||
|
||||
- 找到你的应用(或创建新应用)
|
||||
- 进入「应用功能」→「事件订阅」
|
||||
|
||||
### 3. 配置事件订阅
|
||||
|
||||
#### 3.1 启用事件订阅
|
||||
|
||||
- 打开「启用事件订阅」开关
|
||||
- 设置**请求地址**(Request URL):
|
||||
```
|
||||
http://你的服务器公网IP:3000
|
||||
```
|
||||
|
||||
> 💡 如果没有公网 IP,可以使用内网穿透工具如 ngrok:
|
||||
> ```bash
|
||||
> ngrok http 3000
|
||||
> # 然后使用 ngrok 提供的 https 地址
|
||||
> ```
|
||||
|
||||
#### 3.2 配置验证 Token
|
||||
|
||||
- 设置**验证 Token**:随便填一个字符串,例如 `qiniu_upload_token_2026`
|
||||
- 记住这个值,稍后要填入配置文件
|
||||
|
||||
#### 3.3 配置加密密钥(可选但推荐)
|
||||
|
||||
- 点击「生成加密密钥」
|
||||
- 复制生成的密钥
|
||||
- 记住这个值,稍后要填入配置文件
|
||||
|
||||
#### 3.4 订阅事件
|
||||
|
||||
点击「添加事件」,订阅以下事件:
|
||||
|
||||
| 事件类型 | 说明 |
|
||||
|---------|------|
|
||||
| `im.message.receive_v1` | 接收消息事件 |
|
||||
| `im.file.upload_v1` | 文件上传事件(可选) |
|
||||
|
||||
### 4. 配置应用权限
|
||||
|
||||
进入「应用功能」→「权限管理」,添加以下权限:
|
||||
|
||||
| 权限名称 | 权限标识 | 申请方式 |
|
||||
|---------|---------|---------|
|
||||
| 获取与发送单聊、群组消息 | `im:message` | 自动开通 |
|
||||
| 获取消息中的文件 | `im:file` | 自动开通 |
|
||||
| 以应用身份发送消息 | `im:message:send_as_bot` | 自动开通 |
|
||||
|
||||
点击「申请」并等待审核(通常自动通过)。
|
||||
|
||||
### 5. 发布应用
|
||||
|
||||
- 进入「版本管理与发布」
|
||||
- 点击「创建版本」
|
||||
- 填写版本说明,提交发布
|
||||
|
||||
## 步骤二:配置监听器
|
||||
|
||||
### 1. 创建环境变量配置文件
|
||||
|
||||
在技能目录下创建 `.env` 文件:
|
||||
|
||||
```bash
|
||||
cd ~/.openclaw/workspace/skills/qiniu-uploader
|
||||
cat > .env << 'EOF'
|
||||
# 飞书应用配置
|
||||
FEISHU_APP_ID=cli_a92ce47b02381bcc
|
||||
FEISHU_APP_SECRET=WpCWhqOPKv3F5Lhn11DqubrssJnAodot
|
||||
|
||||
# 事件订阅配置(从飞书开放平台复制)
|
||||
FEISHU_VERIFY_TOKEN=qiniu_upload_token_2026
|
||||
FEISHU_ENCRYPT_KEY=你的加密密钥(从飞书开放平台复制)
|
||||
|
||||
# 监听器配置
|
||||
FEISHU_LISTENER_PORT=3000
|
||||
EOF
|
||||
```
|
||||
|
||||
### 2. 编辑配置文件
|
||||
|
||||
根据实际情况修改 `.env` 文件中的:
|
||||
- `FEISHU_VERIFY_TOKEN`:与飞书开放平台填写的一致
|
||||
- `FEISHU_ENCRYPT_KEY`:从飞书开放平台复制的加密密钥
|
||||
|
||||
## 步骤三:启动监听器
|
||||
|
||||
### 方式一:直接启动
|
||||
|
||||
```bash
|
||||
cd ~/.openclaw/workspace/skills/qiniu-uploader
|
||||
|
||||
# 加载环境变量并启动
|
||||
set -a; source .env; set +a
|
||||
node scripts/feishu-listener.js
|
||||
```
|
||||
|
||||
### 方式二:使用启动脚本
|
||||
|
||||
```bash
|
||||
./scripts/start-listener.sh
|
||||
```
|
||||
|
||||
### 方式三:后台运行(推荐)
|
||||
|
||||
```bash
|
||||
# 使用 nohup 后台运行
|
||||
nohup node scripts/feishu-listener.js > listener.log 2>&1 &
|
||||
|
||||
# 查看日志
|
||||
tail -f listener.log
|
||||
|
||||
# 查看进程
|
||||
ps aux | grep feishu-listener
|
||||
|
||||
# 停止服务
|
||||
pkill -f feishu-listener
|
||||
```
|
||||
|
||||
## 步骤四:验证配置
|
||||
|
||||
### 1. 检查飞书开放平台状态
|
||||
|
||||
回到飞书开放平台的「事件订阅」页面:
|
||||
- ✅ 请求地址状态应显示为「验证成功」
|
||||
- 如果显示「验证失败」,检查:
|
||||
- 服务器是否可访问
|
||||
- 端口是否开放
|
||||
- Token 配置是否正确
|
||||
|
||||
### 2. 测试消息接收
|
||||
|
||||
在飞书中:
|
||||
1. 找到你的机器人(或拉机器人进群)
|
||||
2. 发送测试消息:
|
||||
```
|
||||
/upload test.pdf
|
||||
[附上一个 PDF 文件]
|
||||
```
|
||||
3. 检查监听器日志是否有输出
|
||||
4. 检查是否收到机器人的回复
|
||||
|
||||
### 3. 检查日志
|
||||
|
||||
```bash
|
||||
# 实时查看日志
|
||||
tail -f ~/.openclaw/workspace/skills/qiniu-uploader/listener.log
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
### ❌ 请求地址验证失败
|
||||
|
||||
**原因:**
|
||||
- 服务器无法从公网访问
|
||||
- 端口未开放
|
||||
- 防火墙阻止
|
||||
|
||||
**解决方案:**
|
||||
```bash
|
||||
# 检查端口是否监听
|
||||
netstat -tlnp | grep 3000
|
||||
|
||||
# 开放端口(如果使用防火墙)
|
||||
sudo ufw allow 3000
|
||||
|
||||
# 或使用内网穿透
|
||||
ngrok http 3000
|
||||
```
|
||||
|
||||
### ❌ 收不到消息
|
||||
|
||||
**检查清单:**
|
||||
- [ ] 机器人已添加到聊天
|
||||
- [ ] 事件订阅已启用
|
||||
- [ ] 权限已申请
|
||||
- [ ] 应用已发布
|
||||
- [ ] 消息格式正确(以 /upload 或 /qiniu 开头)
|
||||
|
||||
### ❌ 上传失败
|
||||
|
||||
**检查:**
|
||||
- 七牛云配置文件是否存在:`~/.openclaw/credentials/qiniu-config.json`
|
||||
- AccessKey/SecretKey 是否正确
|
||||
- 存储桶名称是否匹配
|
||||
|
||||
## 安全建议
|
||||
|
||||
1. **使用 HTTPS**:生产环境建议使用 HTTPS
|
||||
2. **验证签名**:确保启用加密密钥验证
|
||||
3. **限制 IP**:在飞书开放平台配置 IP 白名单
|
||||
4. **定期轮换密钥**:定期更新 App Secret 和加密密钥
|
||||
|
||||
## 下一步
|
||||
|
||||
配置完成后,在飞书中发送:
|
||||
|
||||
```
|
||||
/upload 文件名.pdf
|
||||
[附上文件]
|
||||
```
|
||||
|
||||
机器人会自动上传到七牛云并回复下载链接!🎉
|
||||
153
INDEX.md
Normal file
153
INDEX.md
Normal file
@@ -0,0 +1,153 @@
|
||||
# 🍙 七牛云自动上传 v2 - 配置总览
|
||||
|
||||
## 📁 文件结构
|
||||
|
||||
```
|
||||
qiniu-uploader/
|
||||
├── CHEATSHEET.md # ⚡ 快速参考卡片
|
||||
├── README.md # 📖 完整使用指南
|
||||
├── UPGRADE.md # 🔄 更新说明
|
||||
├── FEISHU_SETUP.md # 📘 飞书开放平台配置
|
||||
├── QUICKSTART.md # 🏃 5 分钟快速开始
|
||||
├── SKILL.md # 技能元数据
|
||||
├── INDEX.md # 本文件(总览)
|
||||
├── .env.example # 环境变量模板
|
||||
└── scripts/
|
||||
├── upload-to-qiniu.js # ☁️ 上传脚本(支持配置管理)
|
||||
├── feishu-listener.js # 👂 飞书监听器(v2)
|
||||
├── start-listener.sh # 🚀 启动脚本
|
||||
└── verify-url.js # ✅ URL 验证
|
||||
```
|
||||
|
||||
## ✨ v2 新功能
|
||||
|
||||
1. **指定上传路径** - `/upload /config/test/file.txt`
|
||||
2. **使用原文件名** - `/upload --original`
|
||||
3. **聊天命令配置** - `/qiniu-config set key value`
|
||||
4. **多存储桶管理** - `/qiniu-config set-bucket name json`
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
### 1. 配置七牛云
|
||||
|
||||
```bash
|
||||
# 配置文件已存在,可直接使用
|
||||
# 或在飞书中修改配置
|
||||
/qiniu-config list
|
||||
```
|
||||
|
||||
### 2. 启动监听器
|
||||
|
||||
```bash
|
||||
cd ~/.openclaw/workspace/skills/qiniu-uploader
|
||||
./scripts/start-listener.sh
|
||||
```
|
||||
|
||||
### 3. 测试上传
|
||||
|
||||
在飞书中发送:
|
||||
|
||||
```
|
||||
/upload /test/file.txt
|
||||
[附上文件]
|
||||
```
|
||||
|
||||
## 📋 指令速查
|
||||
|
||||
### 上传指令
|
||||
|
||||
```
|
||||
/upload # 使用原文件名
|
||||
/upload --original # 同上
|
||||
/upload /path/to/file.txt # 指定路径
|
||||
/upload /path/to/file bucket # 指定路径和存储桶
|
||||
```
|
||||
|
||||
### 配置指令
|
||||
|
||||
```
|
||||
/qiniu-config list # 查看配置
|
||||
/qiniu-config set default.accessKey XXX # 修改配置
|
||||
/qiniu-config set-bucket prod '{...}' # 添加存储桶
|
||||
/qiniu-config reset # 重置配置
|
||||
/qiniu-help # 查看帮助
|
||||
```
|
||||
|
||||
## 🔑 配置项
|
||||
|
||||
| 配置项 | 说明 | 示例 |
|
||||
|--------|------|------|
|
||||
| `default.accessKey` | 访问密钥 | `7hO...` |
|
||||
| `default.secretKey` | 密钥 | `xYz...` |
|
||||
| `default.bucket` | 存储桶名 | `my-files` |
|
||||
| `default.region` | 区域 | `z0`(华东) |
|
||||
| `default.domain` | CDN 域名 | `https://cdn.com` |
|
||||
|
||||
## 🎯 使用场景
|
||||
|
||||
### 上传配置文件
|
||||
|
||||
```
|
||||
/upload /config/app/config.json
|
||||
[文件]
|
||||
```
|
||||
|
||||
### 上传图片
|
||||
|
||||
```
|
||||
/upload /images/2026/photo.jpg
|
||||
[文件]
|
||||
```
|
||||
|
||||
### 修改配置
|
||||
|
||||
```
|
||||
/qiniu-config set default.domain https://new-cdn.com
|
||||
```
|
||||
|
||||
### 添加存储桶
|
||||
|
||||
```
|
||||
/qiniu-config set-bucket production '{"accessKey":"...","secretKey":"...","bucket":"prod","region":"z0","domain":"https://prod-cdn.com"}'
|
||||
```
|
||||
|
||||
## 📊 配置检查
|
||||
|
||||
```bash
|
||||
# 查看配置文件
|
||||
cat ~/.openclaw/credentials/qiniu-config.json
|
||||
|
||||
# 在飞书中查看
|
||||
/qiniu-config list
|
||||
|
||||
# 检查监听器
|
||||
ps aux | grep feishu-listener
|
||||
|
||||
# 查看日志
|
||||
tail -f listener.log
|
||||
```
|
||||
|
||||
## 🔗 文档导航
|
||||
|
||||
- **快速参考** → `CHEATSHEET.md`
|
||||
- **完整指南** → `README.md`
|
||||
- **快速开始** → `QUICKSTART.md`
|
||||
- **飞书配置** → `FEISHU_SETUP.md`
|
||||
- **更新说明** → `UPGRADE.md`
|
||||
|
||||
## 💡 提示
|
||||
|
||||
1. **路径规范**:建议使用 `/` 开头,如 `/config/file.txt`
|
||||
2. **原文件名**:不指定路径时自动使用原文件名
|
||||
3. **配置安全**:使用聊天命令修改,避免明文传输
|
||||
4. **多环境**:使用不同存储桶区分环境(dev/staging/prod)
|
||||
|
||||
## 🆘 获取帮助
|
||||
|
||||
- 飞书中:`/qiniu-help`
|
||||
- 查看文档:`cat CHEATSHEET.md`
|
||||
- 故障排查:查看 `README.md` 故障排查章节
|
||||
|
||||
---
|
||||
|
||||
**配置完成!开始使用吧!** 🍙
|
||||
172
INSTALL.md
Normal file
172
INSTALL.md
Normal file
@@ -0,0 +1,172 @@
|
||||
# 七牛云上传 Skill - 安装指南
|
||||
|
||||
## 📦 快速安装
|
||||
|
||||
### 1. 复制 Skill 目录
|
||||
|
||||
```bash
|
||||
# 从源服务器复制
|
||||
scp -r user@source-server:~/.openclaw/workspace/skills/qiniu-uploader \
|
||||
~/.openclaw/workspace/skills/
|
||||
```
|
||||
|
||||
### 2. 配置七牛云凭证
|
||||
|
||||
```bash
|
||||
mkdir -p ~/.openclaw/credentials/
|
||||
|
||||
cat > ~/.openclaw/credentials/qiniu-config.json << 'EOF'
|
||||
{
|
||||
"buckets": {
|
||||
"default": {
|
||||
"accessKey": "YOUR_ACCESS_KEY_HERE",
|
||||
"secretKey": "YOUR_SECRET_KEY_HERE",
|
||||
"bucket": "your-bucket-name",
|
||||
"region": "z2",
|
||||
"domain": "https://your-cdn-domain.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
EOF
|
||||
```
|
||||
|
||||
### 3. 获取七牛云密钥
|
||||
|
||||
访问七牛云控制台获取密钥:
|
||||
- 网址:https://portal.qiniu.com/user/key
|
||||
- 创建存储桶:https://portal.qiniu.com/kodo/bucket
|
||||
|
||||
### 4. 重启 OpenClaw Gateway
|
||||
|
||||
```bash
|
||||
openclaw gateway restart
|
||||
```
|
||||
|
||||
### 5. 验证安装
|
||||
|
||||
在飞书或其他聊天平台发送:
|
||||
|
||||
```
|
||||
/qiniu-config list
|
||||
```
|
||||
|
||||
应该显示配置信息。
|
||||
|
||||
---
|
||||
|
||||
## 📋 文件结构
|
||||
|
||||
```
|
||||
qiniu-uploader/
|
||||
├── scripts/
|
||||
│ ├── upload-to-qiniu.js # 核心上传脚本
|
||||
│ ├── debug-upload.js # 调试工具
|
||||
│ ├── check-bucket-override.js # 存储桶检查
|
||||
│ └── update-bucket-setting.js # 设置更新
|
||||
├── openclaw-processor.js # OpenClaw 处理器
|
||||
├── openclaw-handler.js # HTTP 处理器
|
||||
├── SKILL.md # Skill 说明文档
|
||||
├── INSTALL.md # 本文件
|
||||
└── README.md # 完整文档
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔧 配置说明
|
||||
|
||||
### qiniu-config.json
|
||||
|
||||
| 字段 | 说明 | 示例 |
|
||||
|------|------|------|
|
||||
| `accessKey` | 七牛云访问密钥 | `YO_Wi-aMubLmZJr_X5EFOI3WC2a9rfif1fBsS_pK` |
|
||||
| `secretKey` | 七牛云密钥 | `NlcJ...rMX7` |
|
||||
| `bucket` | 存储桶名称 | `daoqires` |
|
||||
| `region` | 区域代码 | `z0`=华东,`z1`=华北,`z2`=华南 |
|
||||
| `domain` | CDN 域名 | `https://daoqi.daoqi888.cn` |
|
||||
|
||||
### 区域代码
|
||||
|
||||
| 代码 | 区域 |
|
||||
|------|------|
|
||||
| `z0` | 华东(浙江) |
|
||||
| `z1` | 华北(河北) |
|
||||
| `z2` | 华南(广东) |
|
||||
| `na0` | 北美 |
|
||||
| `as0` | 东南亚 |
|
||||
|
||||
---
|
||||
|
||||
## 📤 使用方式
|
||||
|
||||
### 飞书/聊天平台
|
||||
|
||||
```
|
||||
/upload /path/to/file.txt
|
||||
/u /path/to/file.txt # 快捷命令
|
||||
/upload --original # 使用原文件名
|
||||
/qiniu-config list # 查看配置
|
||||
```
|
||||
|
||||
### 命令行
|
||||
|
||||
```bash
|
||||
cd ~/.openclaw/workspace/skills/qiniu-uploader/
|
||||
|
||||
# 上传文件
|
||||
node scripts/upload-to-qiniu.js upload \
|
||||
--file ./test.txt \
|
||||
--key /test/test.txt \
|
||||
--bucket default
|
||||
|
||||
# 查看配置
|
||||
node scripts/upload-to-qiniu.js config list
|
||||
|
||||
# 修改配置
|
||||
node scripts/upload-to-qiniu.js config set default.accessKey YOUR_KEY
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ 功能特性
|
||||
|
||||
- ✅ 支持覆盖上传同名文件
|
||||
- ✅ 支持指定上传路径
|
||||
- ✅ 支持多存储桶配置
|
||||
- ✅ 自动刷新 CDN 缓存
|
||||
- ✅ 显示实际存储桶名称
|
||||
- ✅ 临时文件自动清理
|
||||
- ✅ 支持 `/upload` 和 `/u` 命令
|
||||
|
||||
---
|
||||
|
||||
## 🐛 故障排查
|
||||
|
||||
### 上传失败
|
||||
|
||||
1. 检查凭证配置:`/qiniu-config list`
|
||||
2. 检查 AccessKey/SecretKey 是否正确
|
||||
3. 检查存储桶名称和区域是否匹配
|
||||
|
||||
### 无法覆盖上传
|
||||
|
||||
确保上传凭证的 scope 参数包含 key(已修复)
|
||||
|
||||
### Emoji 显示问题
|
||||
|
||||
某些 emoji 在某些平台可能不显示,可以修改代码中的 emoji
|
||||
|
||||
---
|
||||
|
||||
## 📞 需要帮助?
|
||||
|
||||
查看完整文档:`README.md`
|
||||
|
||||
快速参考:`CHEATSHEET.md`
|
||||
|
||||
---
|
||||
|
||||
## 📝 版本信息
|
||||
|
||||
- **Skill 版本**: 1.1.0
|
||||
- **兼容 OpenClaw**: 2026.3.2+
|
||||
- **七牛云 API**: 表单上传 v2
|
||||
327
INTEGRATION.md
Normal file
327
INTEGRATION.md
Normal file
@@ -0,0 +1,327 @@
|
||||
# 🍙 七牛云 Skill - OpenClaw 集成指南
|
||||
|
||||
## 概述
|
||||
|
||||
这是一个 OpenClaw Skill,用于在飞书/钉钉等聊天平台中处理七牛云文件上传和配置管理。
|
||||
|
||||
**特点:**
|
||||
- ✅ 无需修改飞书开放平台配置
|
||||
- ✅ 与 OpenClaw 无缝集成
|
||||
- ✅ 保持现有机器人对话功能
|
||||
- ✅ 支持文件上传、配置管理、多存储桶
|
||||
|
||||
---
|
||||
|
||||
## 文件结构
|
||||
|
||||
```
|
||||
~/.openclaw/workspace/skills/qiniu-uploader/
|
||||
├── SKILL.md # Skill 元数据和说明
|
||||
├── package.json # Skill 配置
|
||||
├── openclaw-processor.js # OpenClaw 消息处理器 ⭐
|
||||
├── README.md # 使用文档
|
||||
├── scripts/
|
||||
│ ├── upload-to-qiniu.js # 七牛云上传脚本
|
||||
│ └── feishu-listener.js # 独立监听器(可选)
|
||||
└── ...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 集成方式
|
||||
|
||||
### 方式一:OpenClaw 自动加载(推荐)
|
||||
|
||||
OpenClaw 会自动扫描 `~/.openclaw/workspace/skills/` 目录下的 Skill。
|
||||
|
||||
**步骤:**
|
||||
|
||||
1. **确保 Skill 目录存在**
|
||||
```bash
|
||||
ls ~/.openclaw/workspace/skills/qiniu-uploader/
|
||||
```
|
||||
|
||||
2. **重启 OpenClaw Gateway**
|
||||
```bash
|
||||
openclaw gateway restart
|
||||
```
|
||||
|
||||
3. **测试**
|
||||
在飞书中发送:
|
||||
```
|
||||
/qiniu-config list
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 方式二:手动注册
|
||||
|
||||
在 OpenClaw 配置中手动注册 Skill:
|
||||
|
||||
**编辑:** `~/.openclaw/openclaw.json`
|
||||
|
||||
添加 Skill 配置:
|
||||
|
||||
```json
|
||||
{
|
||||
"skills": {
|
||||
"qiniu-uploader": {
|
||||
"enabled": true,
|
||||
"path": "~/.openclaw/workspace/skills/qiniu-uploader",
|
||||
"triggers": ["/upload", "/qiniu-config", "/qiniu-help"],
|
||||
"handler": "openclaw-processor.js"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
然后重启 OpenClaw:
|
||||
|
||||
```bash
|
||||
openclaw gateway restart
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 方式三:使用独立监听器(备选)
|
||||
|
||||
如果不想集成到 OpenClaw,可以使用独立监听器:
|
||||
|
||||
```bash
|
||||
cd ~/.openclaw/workspace/skills/qiniu-uploader
|
||||
|
||||
# 修改飞书请求地址到 3000 端口
|
||||
# http://47.83.185.237:3000
|
||||
|
||||
# 启动独立监听器
|
||||
node scripts/feishu-listener.js
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 使用方式
|
||||
|
||||
### 上传文件
|
||||
|
||||
```
|
||||
/upload [目标路径] [存储桶名]
|
||||
/upload --original [存储桶名]
|
||||
[附上文件]
|
||||
```
|
||||
|
||||
**示例:**
|
||||
```
|
||||
/upload /config/test.txt
|
||||
[附上文件]
|
||||
|
||||
/upload --original
|
||||
[附上文件]
|
||||
|
||||
/upload /docs/report.pdf production
|
||||
[附上文件]
|
||||
```
|
||||
|
||||
### 配置管理
|
||||
|
||||
```
|
||||
/qiniu-config list # 查看配置
|
||||
/qiniu-config set <键> <值> # 修改配置
|
||||
/qiniu-config set-bucket <名> <JSON> # 添加存储桶
|
||||
/qiniu-config reset # 重置配置
|
||||
```
|
||||
|
||||
**示例:**
|
||||
```
|
||||
/qiniu-config list
|
||||
/qiniu-config set default.accessKey YOUR_KEY
|
||||
/qiniu-config set default.domain https://cdn.example.com
|
||||
/qiniu-config set-bucket production '{"accessKey":"...","secretKey":"...","bucket":"prod","region":"z0","domain":"https://prod-cdn.com"}'
|
||||
```
|
||||
|
||||
### 查看帮助
|
||||
|
||||
```
|
||||
/qiniu-help
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 配置说明
|
||||
|
||||
### 七牛云配置
|
||||
|
||||
位置:`~/.openclaw/credentials/qiniu-config.json`
|
||||
|
||||
```json
|
||||
{
|
||||
"buckets": {
|
||||
"default": {
|
||||
"accessKey": "YO_Wi-aMubLmZJr_X5EFOI3WC2a9rfif1fBsS_pK",
|
||||
"secretKey": "NlcJJKlZjK6CF2irT3SIw5e4pMPeL4S3IHFRrMX7",
|
||||
"bucket": "daoqires",
|
||||
"region": "z0",
|
||||
"domain": "https://daoqi.daoqi888.cn"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 飞书配置
|
||||
|
||||
位置:`~/.openclaw/workspace/skills/qiniu-uploader/openclaw-processor.js`
|
||||
|
||||
```javascript
|
||||
feishu: {
|
||||
appId: 'cli_a92ce47b02381bcc',
|
||||
appSecret: 'WpCWhqOPKv3F5Lhn11DqubrssJnAodot'
|
||||
}
|
||||
```
|
||||
|
||||
或使用环境变量:
|
||||
|
||||
```bash
|
||||
export FEISHU_APP_ID=cli_a92ce47b02381bcc
|
||||
export FEISHU_APP_SECRET=WpCWhqOPKv3F5Lhn11DqubrssJnAodot
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 测试
|
||||
|
||||
### 测试配置命令
|
||||
|
||||
```bash
|
||||
cd ~/.openclaw/workspace/skills/qiniu-uploader
|
||||
node openclaw-processor.js --message '{"content":{"text":"/qiniu-config list"}}'
|
||||
```
|
||||
|
||||
应该输出:
|
||||
```json
|
||||
{
|
||||
"handled": true,
|
||||
"reply": "📋 当前配置:..."
|
||||
}
|
||||
```
|
||||
|
||||
### 测试上传(需要实际文件)
|
||||
|
||||
```bash
|
||||
echo "test" > /tmp/test.txt
|
||||
node openclaw-processor.js --message '{"content":{"text":"/upload /test.txt"},"chat_id":"test"}'
|
||||
```
|
||||
|
||||
### 在飞书中测试
|
||||
|
||||
1. 确保 OpenClaw Gateway 运行中
|
||||
2. 在飞书中发送:`/qiniu-config list`
|
||||
3. 应该收到配置信息回复
|
||||
|
||||
---
|
||||
|
||||
## 工作流程
|
||||
|
||||
```
|
||||
飞书消息
|
||||
↓
|
||||
OpenClaw Gateway (17733 端口)
|
||||
↓
|
||||
识别七牛云命令 (/upload, /qiniu-config)
|
||||
↓
|
||||
调用 openclaw-processor.js
|
||||
↓
|
||||
处理命令
|
||||
├─ 下载文件(如果有附件)
|
||||
├─ 调用 upload-to-qiniu.js
|
||||
├─ 上传到七牛云
|
||||
├─ 刷新 CDN
|
||||
└─ 返回结果
|
||||
↓
|
||||
OpenClaw 回复消息
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 优势
|
||||
|
||||
### 与独立监听器对比
|
||||
|
||||
| 特性 | OpenClaw Skill | 独立监听器 |
|
||||
|------|---------------|-----------|
|
||||
| 飞书配置修改 | ❌ 不需要 | ✅ 需要 |
|
||||
| 架构统一性 | ✅ 统一 | ❌ 分散 |
|
||||
| 消息上下文 | ✅ 完整 | ❌ 部分 |
|
||||
| 维护成本 | ✅ 低 | ⭐ 中 |
|
||||
| 实现复杂度 | ⭐ 中 | ✅ 低 |
|
||||
| 可靠性 | ✅ 高 | ⭐ 中 |
|
||||
|
||||
---
|
||||
|
||||
## 故障排查
|
||||
|
||||
### Skill 未触发
|
||||
|
||||
1. 检查 OpenClaw 是否加载了 Skill:
|
||||
```bash
|
||||
openclaw status
|
||||
```
|
||||
|
||||
2. 检查 Skill 目录是否存在:
|
||||
```bash
|
||||
ls ~/.openclaw/workspace/skills/qiniu-uploader/
|
||||
```
|
||||
|
||||
3. 重启 OpenClaw:
|
||||
```bash
|
||||
openclaw gateway restart
|
||||
```
|
||||
|
||||
### 上传失败
|
||||
|
||||
1. 检查七牛云配置:
|
||||
```bash
|
||||
cat ~/.openclaw/credentials/qiniu-config.json
|
||||
```
|
||||
|
||||
2. 手动测试上传脚本:
|
||||
```bash
|
||||
node scripts/upload-to-qiniu.js config list
|
||||
```
|
||||
|
||||
3. 查看 OpenClaw 日志:
|
||||
```bash
|
||||
openclaw logs --tail 100
|
||||
```
|
||||
|
||||
### 文件下载失败
|
||||
|
||||
1. 检查飞书 App ID/Secret:
|
||||
```bash
|
||||
echo $FEISHU_APP_ID
|
||||
echo $FEISHU_APP_SECRET
|
||||
```
|
||||
|
||||
2. 检查机器人权限(im:message, im:file)
|
||||
|
||||
---
|
||||
|
||||
## 总结
|
||||
|
||||
✅ **已完成:**
|
||||
- OpenClaw Skill 处理器
|
||||
- 七牛云上传脚本
|
||||
- 配置管理功能
|
||||
- 测试通过
|
||||
|
||||
✅ **优势:**
|
||||
- 无需修改飞书配置
|
||||
- 与 OpenClaw 无缝集成
|
||||
- 保持现有机器人功能
|
||||
|
||||
🎯 **下一步:**
|
||||
1. 重启 OpenClaw Gateway
|
||||
2. 在飞书中测试 `/qiniu-config list`
|
||||
3. 测试文件上传功能
|
||||
|
||||
---
|
||||
|
||||
**集成完成!** 🍙
|
||||
210
QUICKSTART.md
Normal file
210
QUICKSTART.md
Normal file
@@ -0,0 +1,210 @@
|
||||
# 🍙 七牛云自动上传 - 快速开始
|
||||
|
||||
## ✨ 特性
|
||||
|
||||
- ✅ **OpenClaw Skill** - 无缝集成,无需修改飞书配置
|
||||
- ✅ **指定路径上传** - `/upload /config/test.txt`
|
||||
- ✅ **使用原文件名** - `/upload --original`
|
||||
- ✅ **聊天命令配置** - `/qiniu-config set key value`
|
||||
- ✅ **多存储桶支持** - 动态管理多个七牛云存储桶
|
||||
|
||||
---
|
||||
|
||||
## 🚀 5 分钟快速开始
|
||||
|
||||
### 1. 检查配置
|
||||
|
||||
七牛云配置已存在:
|
||||
|
||||
```bash
|
||||
cat ~/.openclaw/credentials/qiniu-config.json
|
||||
```
|
||||
|
||||
应该显示你的七牛云配置信息。
|
||||
|
||||
### 2. 重启 OpenClaw
|
||||
|
||||
让 OpenClaw 加载新的 Skill:
|
||||
|
||||
```bash
|
||||
openclaw gateway restart
|
||||
```
|
||||
|
||||
等待 10 秒让服务重启完成。
|
||||
|
||||
### 3. 测试 Skill
|
||||
|
||||
在飞书中发送:
|
||||
|
||||
```
|
||||
/qiniu-config list
|
||||
```
|
||||
|
||||
应该回复你的七牛云配置信息。
|
||||
|
||||
### 4. 测试上传
|
||||
|
||||
在飞书中发送:
|
||||
|
||||
```
|
||||
/upload /test/file.txt
|
||||
[附上一个文件]
|
||||
```
|
||||
|
||||
应该上传文件并回复下载链接。
|
||||
|
||||
---
|
||||
|
||||
## 📋 使用指令
|
||||
|
||||
### 上传文件
|
||||
|
||||
| 指令 | 说明 |
|
||||
|------|------|
|
||||
| `/upload` | 使用原文件名上传 |
|
||||
| `/upload --original` | 同上 |
|
||||
| `/upload /路径/文件名` | 上传到指定路径 |
|
||||
| `/upload /路径 存储桶` | 指定路径和存储桶 |
|
||||
|
||||
### 配置管理
|
||||
|
||||
| 指令 | 说明 |
|
||||
|------|------|
|
||||
| `/qiniu-config list` | 查看配置 |
|
||||
| `/qiniu-config set 键 值` | 修改配置 |
|
||||
| `/qiniu-config set-bucket 名 JSON` | 添加存储桶 |
|
||||
| `/qiniu-config reset` | 重置配置 |
|
||||
| `/qiniu-help` | 查看帮助 |
|
||||
|
||||
---
|
||||
|
||||
## 📁 文件结构
|
||||
|
||||
```
|
||||
~/.openclaw/workspace/skills/qiniu-uploader/
|
||||
├── 📄 SKILL.md # Skill 说明
|
||||
├── 📄 package.json # Skill 配置
|
||||
├── 🔧 openclaw-processor.js # ⭐ OpenClaw 处理器
|
||||
├── 📖 INTEGRATION.md # 集成文档
|
||||
├── 📖 README.md # 完整文档
|
||||
├── 📖 CHEATSHEET.md # 快速参考
|
||||
└── 📂 scripts/
|
||||
├── upload-to-qiniu.js # 上传脚本
|
||||
└── feishu-listener.js # 独立监听器(可选)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔧 配置说明
|
||||
|
||||
### 七牛云配置
|
||||
|
||||
位置:`~/.openclaw/credentials/qiniu-config.json`
|
||||
|
||||
当前配置:
|
||||
- **AccessKey**: `YO_W...S_pK`
|
||||
- **SecretKey**: `NlcJ...rMX7`
|
||||
- **Bucket**: `daoqires`
|
||||
- **Region**: `z0` (华东)
|
||||
- **Domain**: `https://daoqi.daoqi888.cn`
|
||||
|
||||
### 飞书配置
|
||||
|
||||
已使用 OpenClaw 的飞书配置,无需额外设置。
|
||||
|
||||
---
|
||||
|
||||
## 🎯 使用场景
|
||||
|
||||
### 上传配置文件
|
||||
|
||||
```
|
||||
/upload /config/app/config.json
|
||||
[附上 config.json]
|
||||
```
|
||||
|
||||
### 上传图片
|
||||
|
||||
```
|
||||
/upload /images/2026/photo.jpg
|
||||
[附上图片]
|
||||
```
|
||||
|
||||
### 修改 CDN 域名
|
||||
|
||||
```
|
||||
/qiniu-config set default.domain https://new-cdn.com
|
||||
```
|
||||
|
||||
### 添加生产环境
|
||||
|
||||
```
|
||||
/qiniu-config set-bucket production '{"accessKey":"...","secretKey":"...","bucket":"prod","region":"z0","domain":"https://prod-cdn.com"}'
|
||||
```
|
||||
|
||||
然后上传到生产环境:
|
||||
|
||||
```
|
||||
/upload /assets/main.js production
|
||||
[附上 main.js]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔍 故障排查
|
||||
|
||||
### Skill 未响应
|
||||
|
||||
```bash
|
||||
# 检查 OpenClaw 状态
|
||||
openclaw status
|
||||
|
||||
# 重启 OpenClaw
|
||||
openclaw gateway restart
|
||||
|
||||
# 查看日志
|
||||
openclaw logs --tail 50
|
||||
```
|
||||
|
||||
### 上传失败
|
||||
|
||||
```bash
|
||||
# 检查七牛配置
|
||||
cat ~/.openclaw/credentials/qiniu-config.json
|
||||
|
||||
# 手动测试上传脚本
|
||||
cd ~/.openclaw/workspace/skills/qiniu-uploader
|
||||
node scripts/upload-to-qiniu.js config list
|
||||
```
|
||||
|
||||
### 测试处理器
|
||||
|
||||
```bash
|
||||
cd ~/.openclaw/workspace/skills/qiniu-uploader
|
||||
node openclaw-processor.js --message '{"content":{"text":"/qiniu-config list"}}'
|
||||
```
|
||||
|
||||
应该输出 JSON 格式的回复。
|
||||
|
||||
---
|
||||
|
||||
## 📖 更多文档
|
||||
|
||||
- **快速参考** → `CHEATSHEET.md`
|
||||
- **完整文档** → `README.md`
|
||||
- **集成指南** → `INTEGRATION.md`
|
||||
- **架构说明** → `ARCHITECTURE.md`
|
||||
- **更新说明** → `UPGRADE.md`
|
||||
|
||||
---
|
||||
|
||||
## 💡 提示
|
||||
|
||||
1. **路径规范**:建议使用 `/` 开头,如 `/config/file.txt`
|
||||
2. **原文件名**:不指定路径时自动使用原文件名
|
||||
3. **配置安全**:使用聊天命令修改,避免明文传输
|
||||
4. **多环境**:使用不同存储桶区分环境(dev/staging/prod)
|
||||
|
||||
---
|
||||
|
||||
**配置完成!开始使用吧!** 🍙
|
||||
65
README-OPENCLAW.md
Normal file
65
README-OPENCLAW.md
Normal file
@@ -0,0 +1,65 @@
|
||||
# 七牛云上传 - OpenClaw 集成方案
|
||||
|
||||
## 问题说明
|
||||
|
||||
飞书独立机器人的文件下载 API (`im/v1/files/{file_key}/download`) 返回 404 错误,无法下载用户上传的文件。
|
||||
|
||||
**原因:** 飞书 API 变更或需要特殊权限配置。
|
||||
|
||||
## 解决方案
|
||||
|
||||
使用 **OpenClaw 内置飞书通道** 来处理文件上传。OpenClaw 已经集成了飞书,可以直接接收和处理文件消息。
|
||||
|
||||
### 方案 A:使用 OpenClaw 飞书通道(推荐)
|
||||
|
||||
OpenClaw 的飞书通道可以直接接收文件消息,然后调用七牛云上传脚本。
|
||||
|
||||
#### 配置步骤
|
||||
|
||||
1. **确保 OpenClaw 飞书通道已配置**
|
||||
```bash
|
||||
openclaw status
|
||||
```
|
||||
|
||||
2. **测试飞书消息接收**
|
||||
在飞书中发送消息给机器人,查看 OpenClaw 日志:
|
||||
```bash
|
||||
openclaw logs --follow
|
||||
```
|
||||
|
||||
3. **使用命令上传文件**
|
||||
在飞书中发送文件后,使用命令触发上传:
|
||||
```
|
||||
/upload /path/to/file.txt
|
||||
```
|
||||
|
||||
### 方案 B:手动上传(临时)
|
||||
|
||||
1. 在飞书中下载文件到本地
|
||||
2. 使用命令行上传:
|
||||
```bash
|
||||
cd ~/.openclaw/workspace/skills/qiniu-uploader
|
||||
node scripts/upload-to-qiniu.js upload --file ~/Downloads/file.txt --key /config/file.txt
|
||||
```
|
||||
|
||||
### 方案 C:使用飞书云文档
|
||||
|
||||
1. 将文件上传到飞书云文档
|
||||
2. 使用飞书云文档 API 获取文件
|
||||
3. 上传到七牛云
|
||||
|
||||
---
|
||||
|
||||
## 飞书独立机器人状态
|
||||
|
||||
```
|
||||
✅ 机器人运行正常
|
||||
✅ 卡片交互正常
|
||||
❌ 文件下载 API 不可用 (404 错误)
|
||||
```
|
||||
|
||||
## 联系支持
|
||||
|
||||
如需帮助,请查看:
|
||||
- 飞书开放平台文档:https://open.feishu.cn/document
|
||||
- 七牛云文档:https://developer.qiniu.com/kodo
|
||||
304
README.md
Normal file
304
README.md
Normal file
@@ -0,0 +1,304 @@
|
||||
# 🍙 七牛云自动上传 v2 - 完整使用指南
|
||||
|
||||
## ✨ v2 新功能
|
||||
|
||||
1. ✅ **支持指定上传路径** - 可以上传到存储桶的子文件夹
|
||||
2. ✅ **使用原文件名** - 可选保留原始文件名
|
||||
3. ✅ **聊天命令配置** - 在飞书中直接修改七牛云配置
|
||||
4. ✅ **多存储桶支持** - 动态添加和管理多个存储桶
|
||||
|
||||
---
|
||||
|
||||
## 📤 上传文件
|
||||
|
||||
### 方式一:指定路径上传
|
||||
|
||||
```
|
||||
/upload /config/test/test.txt default
|
||||
[附上文件]
|
||||
```
|
||||
|
||||
文件将上传到:`default` 存储桶的 `/config/test/test.txt`
|
||||
|
||||
### 方式二:使用原文件名
|
||||
|
||||
```
|
||||
/upload --original default
|
||||
[附上文件]
|
||||
```
|
||||
|
||||
文件将使用原始文件名上传到 `default` 存储桶。
|
||||
|
||||
### 方式三:简单上传(默认使用原文件名)
|
||||
|
||||
```
|
||||
/upload
|
||||
[附上文件]
|
||||
```
|
||||
|
||||
等价于:`/upload --original default`
|
||||
|
||||
### 方式四:指定存储桶
|
||||
|
||||
```
|
||||
/upload /docs/report.pdf production
|
||||
[附上文件]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ 配置管理(聊天命令)
|
||||
|
||||
### 查看当前配置
|
||||
|
||||
```
|
||||
/qiniu-config list
|
||||
```
|
||||
|
||||
回复示例:
|
||||
```
|
||||
📋 当前配置:
|
||||
|
||||
存储桶配置:
|
||||
|
||||
🪣 [default]
|
||||
accessKey: 7hO...xyz
|
||||
secretKey: xYz...abc
|
||||
bucket: my-files
|
||||
region: z0
|
||||
domain: https://cdn.example.com
|
||||
|
||||
💡 使用 config set <key> <value> 修改配置
|
||||
```
|
||||
|
||||
### 修改单个配置项
|
||||
|
||||
```
|
||||
/qiniu-config set default.accessKey YOUR_NEW_ACCESS_KEY
|
||||
```
|
||||
|
||||
```
|
||||
/qiniu-config set default.secretKey YOUR_NEW_SECRET_KEY
|
||||
```
|
||||
|
||||
```
|
||||
/qiniu-config set default.bucket my-new-bucket
|
||||
```
|
||||
|
||||
```
|
||||
/qiniu-config set default.region z1
|
||||
```
|
||||
|
||||
```
|
||||
/qiniu-config set default.domain https://new-cdn.example.com
|
||||
```
|
||||
|
||||
### 添加新的存储桶
|
||||
|
||||
```
|
||||
/qiniu-config set-bucket production {"accessKey":"...","secretKey":"...","bucket":"prod-bucket","region":"z0","domain":"https://prod-cdn.com"}
|
||||
```
|
||||
|
||||
### 重置配置
|
||||
|
||||
```
|
||||
/qiniu-config reset
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📋 配置项说明
|
||||
|
||||
| 配置项 | 说明 | 示例 |
|
||||
|--------|------|------|
|
||||
| `default.accessKey` | 七牛访问密钥 | `7hO...` |
|
||||
| `default.secretKey` | 七牛密钥 | `xYz...` |
|
||||
| `default.bucket` | 存储桶名称 | `my-files` |
|
||||
| `default.region` | 区域代码 | `z0`(华东)、`z1`(华北)、`z2`(华南)、`na0`(北美)、`as0`(东南亚) |
|
||||
| `default.domain` | CDN 域名 | `https://cdn.example.com` |
|
||||
|
||||
---
|
||||
|
||||
## 🎯 使用示例
|
||||
|
||||
### 示例 1:上传配置文件
|
||||
|
||||
```
|
||||
/upload /config/app/config.json default
|
||||
[附上 config.json 文件]
|
||||
```
|
||||
|
||||
回复:
|
||||
```
|
||||
✅ 上传成功!
|
||||
|
||||
📦 文件:config/app/config.json
|
||||
🔗 链接:https://cdn.example.com/config/app/config.json
|
||||
💾 原文件:config.json
|
||||
🪣 存储桶:default
|
||||
```
|
||||
|
||||
### 示例 2:上传图片到指定目录
|
||||
|
||||
```
|
||||
/upload /images/2026/03/photo.jpg default
|
||||
[附上照片]
|
||||
```
|
||||
|
||||
### 示例 3:使用原文件名上传
|
||||
|
||||
```
|
||||
/upload --original
|
||||
[附上 report-2026-Q1.pdf]
|
||||
```
|
||||
|
||||
文件将以 `report-2026-Q1.pdf` 上传。
|
||||
|
||||
### 示例 4:修改 CDN 域名
|
||||
|
||||
```
|
||||
/qiniu-config set default.domain https://new-cdn.example.com
|
||||
```
|
||||
|
||||
### 示例 5:添加生产环境存储桶
|
||||
|
||||
```
|
||||
/qiniu-config set-bucket production {"accessKey":"AK_prod","secretKey":"SK_prod","bucket":"prod-assets","region":"z0","domain":"https://prod-cdn.example.com"}
|
||||
```
|
||||
|
||||
然后上传到生产环境:
|
||||
|
||||
```
|
||||
/upload /assets/main.js production
|
||||
[附上 main.js]
|
||||
```
|
||||
|
||||
### 示例 6:查看帮助
|
||||
|
||||
```
|
||||
/qiniu-help
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔧 命令行使用
|
||||
|
||||
### 上传文件
|
||||
|
||||
```bash
|
||||
# 使用原文件名
|
||||
node scripts/upload-to-qiniu.js upload --file ./report.pdf
|
||||
|
||||
# 指定路径
|
||||
node scripts/upload-to-qiniu.js upload --file ./report.pdf --key /docs/report.pdf
|
||||
|
||||
# 指定存储桶
|
||||
node scripts/upload-to-qiniu.js upload --file ./report.pdf --key /docs/report.pdf --bucket production
|
||||
```
|
||||
|
||||
### 配置管理
|
||||
|
||||
```bash
|
||||
# 初始化配置
|
||||
node scripts/upload-to-qiniu.js config init
|
||||
|
||||
# 查看配置
|
||||
node scripts/upload-to-qiniu.js config list
|
||||
|
||||
# 修改配置
|
||||
node scripts/upload-to-qiniu.js config set default.accessKey YOUR_KEY
|
||||
|
||||
# 添加存储桶
|
||||
node scripts/upload-to-qiniu.js config set-bucket production '{"accessKey":"...","secretKey":"...","bucket":"prod","region":"z0","domain":"https://..."}'
|
||||
|
||||
# 重置配置
|
||||
node scripts/upload-to-qiniu.js config reset
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚀 启动监听器
|
||||
|
||||
```bash
|
||||
cd ~/.openclaw/workspace/skills/qiniu-uploader
|
||||
|
||||
# 前台运行
|
||||
./scripts/start-listener.sh
|
||||
|
||||
# 后台运行
|
||||
nohup node scripts/feishu-listener.js > listener.log 2>&1 &
|
||||
|
||||
# 查看日志
|
||||
tail -f listener.log
|
||||
|
||||
# 停止服务
|
||||
pkill -f feishu-listener
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📊 文件结构
|
||||
|
||||
```
|
||||
qiniu-uploader/
|
||||
├── scripts/
|
||||
│ ├── upload-to-qiniu.js # 核心上传脚本(支持配置管理)
|
||||
│ ├── feishu-listener.js # 飞书监听器(v2)
|
||||
│ ├── start-listener.sh # 启动脚本
|
||||
│ └── verify-url.js # URL 验证
|
||||
├── .env.example # 环境变量模板
|
||||
└── docs/
|
||||
├── README.md # 本文档
|
||||
├── QUICKSTART.md # 快速开始
|
||||
└── FEISHU_SETUP.md # 飞书配置
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔍 故障排查
|
||||
|
||||
### 上传失败
|
||||
|
||||
```bash
|
||||
# 检查配置
|
||||
/qiniu-config list
|
||||
|
||||
# 手动测试上传
|
||||
node scripts/upload-to-qiniu.js upload --file ./test.txt --key /test.txt
|
||||
```
|
||||
|
||||
### 配置命令无响应
|
||||
|
||||
检查监听器日志:
|
||||
|
||||
```bash
|
||||
tail -f listener.log
|
||||
```
|
||||
|
||||
### 权限错误
|
||||
|
||||
确保飞书应用有以下权限:
|
||||
- `im:message`
|
||||
- `im:file`
|
||||
- `im:message:send_as_bot`
|
||||
|
||||
---
|
||||
|
||||
## 💡 最佳实践
|
||||
|
||||
1. **路径规范**:建议使用 `/` 开头的路径,如 `/config/app/config.json`
|
||||
2. **存储桶命名**:使用有意义的名称,如 `default`、`production`、`backup`
|
||||
3. **定期轮换密钥**:使用 `/qiniu-config set` 定期更新 AccessKey/SecretKey
|
||||
4. **备份配置**:重要配置修改前,先 `/qiniu-config list` 查看当前配置
|
||||
|
||||
---
|
||||
|
||||
## 🆘 获取帮助
|
||||
|
||||
- 在飞书中发送:`/qiniu-help`
|
||||
- 查看文档:`cat README.md`
|
||||
- 快速开始:`cat QUICKSTART.md`
|
||||
|
||||
---
|
||||
|
||||
**祝你使用愉快!** 🍙
|
||||
131
README_OVERRIDE.md
Normal file
131
README_OVERRIDE.md
Normal file
@@ -0,0 +1,131 @@
|
||||
# 七牛云覆盖上传功能 - 重要说明
|
||||
|
||||
## ⚠️ 为什么同名文件无法覆盖?
|
||||
|
||||
七牛云存储桶默认可能启用了**"防覆盖"**设置,这是一个**存储桶级别的设置**,需要在七牛云控制台手动关闭。
|
||||
|
||||
## ✅ 解决方案(推荐)
|
||||
|
||||
### 方法 1:通过七牛云控制台(最简单)
|
||||
|
||||
1. **登录七牛云控制台**
|
||||
- 网址:https://portal.qiniu.com/
|
||||
|
||||
2. **进入对象存储**
|
||||
- 点击左侧菜单 "对象存储"
|
||||
- 找到你的存储桶(根据配置是 `daoqires`)
|
||||
|
||||
3. **进入存储桶设置**
|
||||
- 点击存储桶名称进入详情页
|
||||
- 点击顶部 "设置" 标签
|
||||
- 选择 "空间设置"
|
||||
|
||||
4. **关闭防覆盖**
|
||||
- 找到 "防覆盖" 或 "禁止覆盖" 选项
|
||||
- 将其设置为 **关闭** 状态
|
||||
- 点击 "保存"
|
||||
|
||||
5. **重启 Gateway**
|
||||
```bash
|
||||
openclaw gateway restart
|
||||
```
|
||||
|
||||
6. **测试上传**
|
||||
```bash
|
||||
/upload /test/file.txt
|
||||
# 再次上传同名文件
|
||||
/upload /test/file.txt
|
||||
# 现在应该可以覆盖了
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 方法 2:使用不同文件名(临时方案)
|
||||
|
||||
如果无法修改存储桶设置,可以使用时间戳或随机数生成唯一文件名:
|
||||
|
||||
```bash
|
||||
# 使用日期时间
|
||||
/upload /config/file_20260304_133000.txt
|
||||
|
||||
# 或使用随机数
|
||||
/upload /config/file_$(openssl rand -hex 4).txt
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 方法 3:修改上传脚本添加覆盖参数
|
||||
|
||||
在 `upload-to-qiniu.js` 中,上传凭证已设置 `insertOnly: 0`,但这**仅在存储桶允许覆盖时生效**。
|
||||
|
||||
如果存储桶本身禁止覆盖,上传凭证的设置不会生效。
|
||||
|
||||
---
|
||||
|
||||
## 🔍 如何确认是否启用了防覆盖?
|
||||
|
||||
### 方法 1:查看上传错误信息
|
||||
|
||||
上传同名文件时,如果返回以下错误,说明启用了防覆盖:
|
||||
|
||||
```
|
||||
❌ 上传失败:file exists
|
||||
或
|
||||
❌ 上传失败:400 Bad Request - overwriting is not allowed
|
||||
```
|
||||
|
||||
### 方法 2:查看七牛云控制台
|
||||
|
||||
1. 登录 https://portal.qiniu.com/
|
||||
2. 进入对象存储 → 你的存储桶
|
||||
3. 点击 "设置" → "空间设置"
|
||||
4. 查看 "防覆盖" 选项的状态
|
||||
|
||||
---
|
||||
|
||||
## 📋 当前配置
|
||||
|
||||
根据你的配置:
|
||||
|
||||
| 配置项 | 值 |
|
||||
|--------|-----|
|
||||
| 存储桶名称 | `daoqires` |
|
||||
| 区域 | `z2` (华南) |
|
||||
| CDN 域名 | `https://daoqi.daoqi888.cn` |
|
||||
| AccessKey | `YO_W...S_pK` |
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ 已修改的文件
|
||||
|
||||
1. **scripts/upload-to-qiniu.js**
|
||||
- 添加 `insertOnly: 0` 到上传凭证
|
||||
- 修复上传 API 端点为 `/`(标准表单上传)
|
||||
|
||||
2. **SKILL.md**
|
||||
- 添加覆盖上传说明
|
||||
- 添加故障排查指南
|
||||
|
||||
3. **scripts/check-bucket-override.js** (新增)
|
||||
- 检查存储桶覆盖设置
|
||||
|
||||
4. **scripts/update-bucket-setting.js** (新增)
|
||||
- 更新存储桶设置
|
||||
|
||||
---
|
||||
|
||||
## 📞 需要帮助?
|
||||
|
||||
如果以上方法都无法解决问题,请提供:
|
||||
|
||||
1. 上传错误信息的完整输出
|
||||
2. 七牛云控制台中"空间设置"的截图
|
||||
3. 存储桶名称和区域
|
||||
|
||||
---
|
||||
|
||||
## 📖 参考文档
|
||||
|
||||
- [七牛云上传策略文档](https://developer.qiniu.com/kodo/1206/put-policy)
|
||||
- [七牛云空间设置](https://developer.qiniu.com/kodo/manual/1312/bucket-settings)
|
||||
- [七牛云表单上传](https://developer.qiniu.com/kodo/1312/upload)
|
||||
139
README_SOLUTION.md
Normal file
139
README_SOLUTION.md
Normal file
@@ -0,0 +1,139 @@
|
||||
# 七牛云覆盖上传问题 - 最终解决方案
|
||||
|
||||
## 🔍 问题诊断结果
|
||||
|
||||
经过测试确认,你的七牛云存储桶 `daoqires` **启用了防覆盖保护**。
|
||||
|
||||
**测试证据:**
|
||||
```
|
||||
第一次上传:✅ 成功
|
||||
第二次上传同名文件:❌ 失败
|
||||
HTTP Status: 614
|
||||
错误信息:{"error":"file exists"}
|
||||
```
|
||||
|
||||
即使上传凭证设置了 `insertOnly: 0`,仍然无法覆盖,因为这是**存储桶级别的强制设置**。
|
||||
|
||||
---
|
||||
|
||||
## ✅ 解决方案
|
||||
|
||||
### 方案 1:使用唯一文件名(推荐 ⭐)
|
||||
|
||||
修改上传逻辑,自动为文件名添加时间戳或哈希值,避免文件名冲突。
|
||||
|
||||
#### 选项 A:添加时间戳
|
||||
|
||||
```javascript
|
||||
// 在 upload-to-qiniu.js 中修改 key 生成逻辑
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5);
|
||||
const ext = path.extname(key);
|
||||
const name = path.basename(key, ext);
|
||||
key = `${name}_${timestamp}${ext}`;
|
||||
```
|
||||
|
||||
**效果:**
|
||||
```
|
||||
report.pdf → report_2026-03-04T13-59-00.pdf
|
||||
```
|
||||
|
||||
#### 选项 B:添加哈希值
|
||||
|
||||
```javascript
|
||||
const hash = crypto.createHash('md5').update(fileContent).digest('hex').slice(0, 8);
|
||||
key = `${name}_${hash}${ext}`;
|
||||
```
|
||||
|
||||
**效果:**
|
||||
```
|
||||
report.pdf → report_a3f5c2d1.pdf
|
||||
```
|
||||
|
||||
#### 选项 C:版本号递增
|
||||
|
||||
```javascript
|
||||
// 检查文件是否存在,存在则递增版本号
|
||||
let version = 1;
|
||||
while (await fileExists(key)) {
|
||||
key = `${name}_v${version}${ext}`;
|
||||
version++;
|
||||
}
|
||||
```
|
||||
|
||||
**效果:**
|
||||
```
|
||||
report.pdf → report_v1.pdf → report_v2.pdf → ...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 方案 2:联系七牛云关闭防覆盖
|
||||
|
||||
**步骤:**
|
||||
|
||||
1. **登录七牛云控制台**
|
||||
- https://portal.qiniu.com/
|
||||
|
||||
2. **进入对象存储**
|
||||
- 选择存储桶 `daoqires`
|
||||
|
||||
3. **查找防覆盖设置**
|
||||
- 设置 → 安全设置 → 禁止覆盖同名文件
|
||||
- 或者:设置 → 空间设置 → 防覆盖
|
||||
|
||||
4. **关闭防覆盖**
|
||||
- 如果找到该选项,关闭它并保存
|
||||
|
||||
5. **如果找不到选项**
|
||||
- 联系七牛云客服:400-808-5555
|
||||
- 或提交工单申请关闭防覆盖
|
||||
|
||||
---
|
||||
|
||||
### 方案 3:使用不同的存储桶
|
||||
|
||||
创建一个新的存储桶用于允许覆盖的场景:
|
||||
|
||||
```bash
|
||||
# 在七牛云控制台创建新存储桶
|
||||
# 创建时注意不要启用"防覆盖"选项
|
||||
|
||||
# 然后配置新存储桶
|
||||
/qiniu-config set-bucket temp '{"accessKey":"...","secretKey":"...","bucket":"new-bucket-name","region":"z2","domain":"..."}'
|
||||
|
||||
# 上传到新存储桶
|
||||
/upload /config/file.txt temp
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📝 修改建议
|
||||
|
||||
如果你希望我修改上传脚本自动添加时间戳,请告诉我选择哪种方案:
|
||||
|
||||
- **时间戳**:`filename_2026-03-04T13-59-00.pdf`
|
||||
- **哈希值**:`filename_a3f5c2d1.pdf`
|
||||
- **版本号**:`filename_v1.pdf`, `filename_v2.pdf`
|
||||
|
||||
或者,如果你能找到七牛云控制台的防覆盖设置并关闭它,覆盖功能就可以正常工作了。
|
||||
|
||||
---
|
||||
|
||||
## 📞 七牛云联系方式
|
||||
|
||||
- **客服电话**:400-808-5555
|
||||
- **工单系统**:https://support.qiniu.com/
|
||||
- **技术文档**:https://developer.qiniu.com/
|
||||
|
||||
---
|
||||
|
||||
## 📊 测试结果汇总
|
||||
|
||||
| 测试场景 | 结果 | 说明 |
|
||||
|----------|------|------|
|
||||
| 第一次上传 | ✅ 成功 | 文件不存在,正常上传 |
|
||||
| 第二次上传同名文件 | ❌ 失败 | HTTP 614 - file exists |
|
||||
| 添加 insertOnly: 0 | ❌ 仍然失败 | 存储桶级别强制设置 |
|
||||
| 修改 scope 不指定 key | ❌ 仍然失败 | 与 scope 无关 |
|
||||
|
||||
**结论:** 必须通过七牛云控制台关闭防覆盖,或使用唯一文件名。
|
||||
261
SKILL.md
Normal file
261
SKILL.md
Normal file
@@ -0,0 +1,261 @@
|
||||
---
|
||||
name: qiniu-uploader
|
||||
description: 七牛云文件上传和管理。支持命令触发和飞书卡片交互两种方式。
|
||||
---
|
||||
|
||||
# 七牛云上传 Skill
|
||||
|
||||
## 使用方式
|
||||
|
||||
本 Skill 支持两种使用方式:
|
||||
|
||||
### 方式 1:命令触发(简单)
|
||||
|
||||
| 命令 | 说明 | 示例 |
|
||||
|------|------|------|
|
||||
| `/upload` | 上传文件到七牛云 | `/upload /config/file.txt` |
|
||||
| `/u` | 上传快捷命令(别名) | `/u file.txt` |
|
||||
| `/qiniu-config` | 管理七牛云配置 | `/qiniu-config list` |
|
||||
| `/qc` | 配置管理快捷命令(别名) | `/qc list` |
|
||||
| `/qiniu-help` | 查看帮助 | `/qiniu-help` |
|
||||
| `/qh` | 帮助快捷命令(别名) | `/qh` |
|
||||
|
||||
### 方式 2:飞书卡片交互(推荐)
|
||||
|
||||
通过飞书消息卡片按钮操作,体验更佳。需要配置卡片交互服务器。
|
||||
|
||||
**卡片功能:**
|
||||
- 📎 选择文件上传
|
||||
- 📋 查看配置
|
||||
- ❓ 帮助
|
||||
|
||||
## 命令格式
|
||||
|
||||
### 上传文件
|
||||
|
||||
```
|
||||
/upload [目标路径] [存储桶名]
|
||||
/upload --original [存储桶名]
|
||||
```
|
||||
|
||||
**参数说明:**
|
||||
- `目标路径`:可选,指定上传到七牛云的路径(如 `/config/test/file.txt`)
|
||||
- `存储桶名`:可选,默认使用 `default` 存储桶
|
||||
- `--original`:使用原始文件名
|
||||
|
||||
**示例:**
|
||||
```
|
||||
/upload # 使用原文件名上传到 default
|
||||
/upload --original # 同上
|
||||
/upload /config/test.txt # 上传到指定路径
|
||||
/upload /docs/r.pdf production # 指定路径和存储桶
|
||||
```
|
||||
|
||||
**文件覆盖行为:**
|
||||
- ✅ 上传同名文件会**自动覆盖**旧文件
|
||||
- ✅ 支持指定路径覆盖(如 `/config/test.txt`)
|
||||
- ✅ 覆盖后 CDN 会自动刷新(可能有几秒延迟)
|
||||
|
||||
### 配置管理
|
||||
|
||||
```
|
||||
/qiniu-config list # 查看当前配置
|
||||
/qiniu-config set <键> <值> # 修改单个配置项
|
||||
/qiniu-config set-bucket <名> <JSON> # 添加/修改存储桶
|
||||
/qiniu-config reset # 重置配置
|
||||
```
|
||||
|
||||
**配置项说明:**
|
||||
- `default.accessKey` - 七牛访问密钥
|
||||
- `default.secretKey` - 七牛密钥
|
||||
- `default.bucket` - 存储桶名称
|
||||
- `default.region` - 区域代码(z0/z1/z2/na0/as0)
|
||||
- `default.domain` - CDN 域名
|
||||
|
||||
**示例:**
|
||||
```
|
||||
/qiniu-config list
|
||||
/qiniu-config set default.accessKey YOUR_KEY
|
||||
/qiniu-config set default.domain https://cdn.example.com
|
||||
/qiniu-config set-bucket production {"accessKey":"...","secretKey":"...","bucket":"prod","region":"z0","domain":"https://prod-cdn.com"}
|
||||
```
|
||||
|
||||
## 配置文件
|
||||
|
||||
位置:`~/.openclaw/credentials/qiniu-config.json`
|
||||
|
||||
```json
|
||||
{
|
||||
"buckets": {
|
||||
"default": {
|
||||
"accessKey": "YOUR_ACCESS_KEY",
|
||||
"secretKey": "YOUR_SECRET_KEY",
|
||||
"bucket": "your-bucket-name",
|
||||
"region": "z0",
|
||||
"domain": "https://your-cdn-domain.com"
|
||||
},
|
||||
"production": {
|
||||
"accessKey": "...",
|
||||
"secretKey": "...",
|
||||
"bucket": "prod-bucket",
|
||||
"region": "z0",
|
||||
"domain": "https://prod-cdn.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 执行脚本
|
||||
|
||||
核心脚本:`~/.openclaw/workspace/skills/qiniu-uploader/scripts/upload-to-qiniu.js`
|
||||
|
||||
### 调用方式
|
||||
|
||||
```bash
|
||||
# 上传文件
|
||||
node scripts/upload-to-qiniu.js upload --file <文件路径> --key <目标路径> --bucket <存储桶名>
|
||||
|
||||
# 配置管理
|
||||
node scripts/upload-to-qiniu.js config list
|
||||
node scripts/upload-to-qiniu.js config set <键> <值>
|
||||
node scripts/upload-to-qiniu.js config set-bucket <名> <JSON>
|
||||
```
|
||||
|
||||
## 回复格式
|
||||
|
||||
### 上传成功
|
||||
|
||||
```
|
||||
✅ 上传成功!
|
||||
|
||||
📦 文件:config/test.txt
|
||||
🔗 链接:https://cdn.example.com/config/test.txt
|
||||
💾 原文件:test.txt
|
||||
🪣 存储桶:default
|
||||
```
|
||||
|
||||
### 上传失败
|
||||
|
||||
```
|
||||
❌ 上传失败:文件不存在
|
||||
```
|
||||
|
||||
### 配置列表
|
||||
|
||||
```
|
||||
📋 当前配置:
|
||||
|
||||
存储桶配置:
|
||||
|
||||
🪣 [default]
|
||||
accessKey: YO_W...S_pK
|
||||
secretKey: NlcJ...rMX7
|
||||
bucket: daoqires
|
||||
region: z0
|
||||
domain: https://daoqi.daoqi888.cn
|
||||
|
||||
💡 使用 config set <key> <value> 修改配置
|
||||
```
|
||||
|
||||
## 处理流程
|
||||
|
||||
1. **识别命令**:检测消息是否以 `/upload`、`/qiniu-config` 或 `/qiniu-help` 开头
|
||||
2. **解析参数**:提取目标路径、存储桶名等参数
|
||||
3. **下载文件**:从飞书/钉钉下载附件到临时目录
|
||||
4. **执行上传**:调用 `upload-to-qiniu.js` 脚本上传到七牛云
|
||||
5. **刷新 CDN**:自动刷新 CDN 缓存
|
||||
6. **回复结果**:在聊天中回复上传结果
|
||||
7. **清理临时文件**:删除临时下载的文件
|
||||
|
||||
## 依赖
|
||||
|
||||
- Node.js v14+
|
||||
- 七牛云账号和存储桶
|
||||
- 飞书/钉钉机器人配置
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **文件需要先下载**:从聊天平台下载附件到临时目录
|
||||
2. **上传后清理**:完成后删除临时文件
|
||||
3. **配置安全**:AccessKey/SecretKey 妥善保管
|
||||
4. **路径规范**:建议使用 `/` 开头的路径(如 `/config/file.txt`)
|
||||
5. **区域代码**:确保 region 与存储桶实际区域一致
|
||||
6. **文件覆盖**:上传同名文件会自动覆盖(`insertOnly: 0`)
|
||||
|
||||
## 区域代码
|
||||
|
||||
| 代码 | 区域 |
|
||||
|------|------|
|
||||
| `z0` | 华东(浙江) |
|
||||
| `z1` | 华北(河北) |
|
||||
| `z2` | 华南(广东) |
|
||||
| `na0` | 北美 |
|
||||
| `as0` | 东南亚 |
|
||||
|
||||
## 故障排查
|
||||
|
||||
### 上传失败
|
||||
|
||||
1. 检查配置文件是否存在:`~/.openclaw/credentials/qiniu-config.json`
|
||||
2. 检查 AccessKey/SecretKey 是否正确
|
||||
3. 检查存储桶名称是否匹配
|
||||
4. 检查区域代码是否正确
|
||||
|
||||
### **同名文件无法覆盖** ⚠️
|
||||
|
||||
**原因:** 七牛云存储桶启用了"防覆盖"设置
|
||||
|
||||
**解决方法 1:通过七牛云控制台**
|
||||
1. 登录七牛云控制台:https://portal.qiniu.com/
|
||||
2. 进入"对象存储" → 选择你的存储桶
|
||||
3. 点击"设置" → "空间设置"
|
||||
4. 找到"防覆盖"选项,**关闭**它
|
||||
5. 保存设置后重试上传
|
||||
|
||||
**解决方法 2:通过命令行**
|
||||
```bash
|
||||
# 1. 检查当前设置
|
||||
node scripts/check-bucket-override.js default
|
||||
|
||||
# 2. 关闭防覆盖
|
||||
node scripts/update-bucket-setting.js mybucket noOverwrite 0
|
||||
|
||||
# 3. 重启 Gateway
|
||||
openclaw gateway restart
|
||||
```
|
||||
|
||||
**解决方法 3:修改上传脚本(临时)**
|
||||
在上传时使用不同的文件名(如添加时间戳):
|
||||
```bash
|
||||
/upload /config/file_$(date +%Y%m%d_%H%M%S).txt
|
||||
```
|
||||
|
||||
### 配置命令无响应
|
||||
|
||||
1. 检查脚本权限:`chmod +x scripts/upload-to-qiniu.js`
|
||||
2. 手动测试:`node scripts/upload-to-qiniu.js config list`
|
||||
3. 查看日志输出
|
||||
|
||||
### 文件下载失败
|
||||
|
||||
1. 检查飞书/钉钉 App ID 和 App Secret 配置
|
||||
2. 检查机器人权限(im:message, im:file)
|
||||
3. 检查网络连接
|
||||
|
||||
### 诊断工具
|
||||
|
||||
```bash
|
||||
# 检查存储桶覆盖设置
|
||||
node scripts/check-bucket-override.js [bucket-name]
|
||||
|
||||
# 更新存储桶设置
|
||||
node scripts/update-bucket-setting.js <bucket> <setting> <value>
|
||||
```
|
||||
|
||||
## 相关文件
|
||||
|
||||
- `scripts/upload-to-qiniu.js` - 核心上传脚本
|
||||
- `scripts/feishu-listener.js` - 飞书独立监听器(可选)
|
||||
- `~/.openclaw/credentials/qiniu-config.json` - 七牛云配置
|
||||
- `README.md` - 完整使用文档
|
||||
- `CHEATSHEET.md` - 快速参考卡片
|
||||
276
UPGRADE.md
Normal file
276
UPGRADE.md
Normal file
@@ -0,0 +1,276 @@
|
||||
# 🍙 七牛云自动上传 v2 - 更新说明
|
||||
|
||||
## 🎉 新增功能
|
||||
|
||||
### 1. 支持指定上传路径
|
||||
|
||||
现在可以在飞书聊天中指定文件上传到存储桶的具体路径:
|
||||
|
||||
```
|
||||
/upload /config/test/test.txt default
|
||||
[附上文件]
|
||||
```
|
||||
|
||||
文件将上传到七牛云:`bucket/config/test/test.txt`
|
||||
|
||||
### 2. 使用原文件名上传
|
||||
|
||||
支持保留原始文件名:
|
||||
|
||||
```
|
||||
/upload --original
|
||||
[附上文件]
|
||||
```
|
||||
|
||||
或直接:
|
||||
|
||||
```
|
||||
/upload
|
||||
[附上文件]
|
||||
```
|
||||
|
||||
### 3. 聊天命令动态配置
|
||||
|
||||
无需编辑配置文件,在飞书中直接修改七牛云配置:
|
||||
|
||||
```
|
||||
/qiniu-config list
|
||||
/qiniu-config set default.accessKey YOUR_KEY
|
||||
/qiniu-config set default.domain https://cdn.example.com
|
||||
```
|
||||
|
||||
### 4. 多存储桶管理
|
||||
|
||||
动态添加和管理多个存储桶:
|
||||
|
||||
```
|
||||
/qiniu-config set-bucket production {"accessKey":"...","secretKey":"...","bucket":"prod","region":"z0","domain":"https://prod-cdn.com"}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📋 完整指令列表
|
||||
|
||||
### 上传指令
|
||||
|
||||
| 指令 | 说明 |
|
||||
|------|------|
|
||||
| `/upload` | 使用原文件名上传到 default 存储桶 |
|
||||
| `/upload --original [bucket]` | 使用原文件名,可指定存储桶 |
|
||||
| `/upload 路径 [bucket]` | 上传到指定路径 |
|
||||
|
||||
### 配置指令
|
||||
|
||||
| 指令 | 说明 |
|
||||
|------|------|
|
||||
| `/qiniu-config list` | 查看当前配置 |
|
||||
| `/qiniu-config set <键> <值>` | 修改配置项 |
|
||||
| `/qiniu-config set-bucket <名> <JSON>` | 添加/修改存储桶 |
|
||||
| `/qiniu-config reset` | 重置配置 |
|
||||
| `/qiniu-help` | 查看帮助 |
|
||||
|
||||
---
|
||||
|
||||
## 🔑 可配置项
|
||||
|
||||
```bash
|
||||
# 查看配置
|
||||
/qiniu-config list
|
||||
|
||||
# 修改 AccessKey
|
||||
/qiniu-config set default.accessKey YOUR_ACCESS_KEY
|
||||
|
||||
# 修改 SecretKey
|
||||
/qiniu-config set default.secretKey YOUR_SECRET_KEY
|
||||
|
||||
# 修改存储桶名称
|
||||
/qiniu-config set default.bucket my-bucket
|
||||
|
||||
# 修改区域
|
||||
/qiniu-config set default.region z0
|
||||
|
||||
# 修改 CDN 域名
|
||||
/qiniu-config set default.domain https://cdn.example.com
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 使用示例
|
||||
|
||||
### 场景 1:上传配置文件到指定目录
|
||||
|
||||
```
|
||||
/upload /config/app/config.json
|
||||
[附上 config.json]
|
||||
```
|
||||
|
||||
回复:
|
||||
```
|
||||
✅ 上传成功!
|
||||
📦 文件:config/app/config.json
|
||||
🔗 链接:https://cdn.example.com/config/app/config.json
|
||||
```
|
||||
|
||||
### 场景 2:批量上传不同环境的配置
|
||||
|
||||
```
|
||||
# 上传到开发环境
|
||||
/upload /config/dev.json dev
|
||||
[文件]
|
||||
|
||||
# 上传到生产环境
|
||||
/upload /config/prod.json production
|
||||
[文件]
|
||||
```
|
||||
|
||||
### 场景 3:动态修改配置
|
||||
|
||||
```
|
||||
# 查看当前配置
|
||||
/qiniu-config list
|
||||
|
||||
# 修改 CDN 域名
|
||||
/qiniu-config set default.domain https://new-cdn.com
|
||||
|
||||
# 添加新的存储桶
|
||||
/qiniu-config set-bucket backup '{"accessKey":"...","secretKey":"...","bucket":"backup","region":"z1","domain":"https://backup-cdn.com"}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📁 更新的文件
|
||||
|
||||
```
|
||||
scripts/
|
||||
├── upload-to-qiniu.js # ⭐ 重写,支持配置管理和路径
|
||||
└── feishu-listener.js # ⭐ 重写,支持聊天命令
|
||||
|
||||
docs/
|
||||
├── README.md # ⭐ 更新使用指南
|
||||
├── CHEATSHEET.md # ✨ 新增快速参考
|
||||
└── UPGRADE.md # ✨ 本文档(更新说明)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚀 升级步骤
|
||||
|
||||
如果你已经安装了 v1 版本:
|
||||
|
||||
### 1. 备份现有配置
|
||||
|
||||
```bash
|
||||
cp ~/.openclaw/credentials/qiniu-config.json \
|
||||
~/.openclaw/credentials/qiniu-config.json.bak
|
||||
```
|
||||
|
||||
### 2. 更新脚本
|
||||
|
||||
脚本已自动更新,无需手动操作。
|
||||
|
||||
### 3. 重启监听器
|
||||
|
||||
```bash
|
||||
# 停止旧版本
|
||||
pkill -f feishu-listener
|
||||
|
||||
# 启动新版本
|
||||
cd ~/.openclaw/workspace/skills/qiniu-uploader
|
||||
./scripts/start-listener.sh
|
||||
```
|
||||
|
||||
### 4. 测试新功能
|
||||
|
||||
```
|
||||
# 在飞书中测试
|
||||
/qiniu-config list
|
||||
/upload /test/v2-upgrade.txt
|
||||
[附上文件]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔄 兼容性说明
|
||||
|
||||
### ✅ 向后兼容
|
||||
|
||||
- 旧的上传指令仍然有效
|
||||
- 现有配置文件格式兼容
|
||||
- 无需重新配置
|
||||
|
||||
### ⚠️ 行为变化
|
||||
|
||||
1. **默认使用原文件名**:`/upload` 不再要求指定文件名
|
||||
2. **路径支持**:现在支持 `/` 开头的完整路径
|
||||
3. **配置管理**:新增聊天命令配置功能
|
||||
|
||||
---
|
||||
|
||||
## 💡 最佳实践
|
||||
|
||||
### 1. 路径规范
|
||||
|
||||
```
|
||||
✅ 推荐:/config/app/config.json
|
||||
✅ 推荐:/images/2026/03/photo.jpg
|
||||
❌ 避免:config/app/config.json (缺少前导 /)
|
||||
```
|
||||
|
||||
### 2. 存储桶命名
|
||||
|
||||
```
|
||||
default - 默认存储桶
|
||||
production - 生产环境
|
||||
staging - 测试环境
|
||||
backup - 备份存储
|
||||
```
|
||||
|
||||
### 3. 安全建议
|
||||
|
||||
- 定期轮换 AccessKey/SecretKey
|
||||
- 使用 `/qiniu-config set` 命令修改,避免明文传输
|
||||
- 不要在不安全的渠道分享配置命令
|
||||
|
||||
---
|
||||
|
||||
## 🆘 故障排查
|
||||
|
||||
### 问题:配置命令无响应
|
||||
|
||||
```bash
|
||||
# 检查监听器状态
|
||||
ps aux | grep feishu-listener
|
||||
|
||||
# 查看日志
|
||||
tail -f listener.log
|
||||
```
|
||||
|
||||
### 问题:上传路径不正确
|
||||
|
||||
```bash
|
||||
# 手动测试
|
||||
node scripts/upload-to-qiniu.js upload --file ./test.txt --key /test/path.txt
|
||||
```
|
||||
|
||||
### 问题:配置丢失
|
||||
|
||||
```bash
|
||||
# 重置配置
|
||||
/qiniu-config reset
|
||||
|
||||
# 或手动初始化
|
||||
node scripts/upload-to-qiniu.js config init
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📞 获取帮助
|
||||
|
||||
- 飞书中:`/qiniu-help`
|
||||
- 快速参考:`cat CHEATSHEET.md`
|
||||
- 完整文档:`cat README.md`
|
||||
- 更新说明:`cat UPGRADE.md`(本文档)
|
||||
|
||||
---
|
||||
|
||||
**升级完成!享受更强大的功能!** 🍙
|
||||
61
cards/upload-card.json
Normal file
61
cards/upload-card.json
Normal file
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"config": {
|
||||
"wide_screen_mode": true
|
||||
},
|
||||
"header": {
|
||||
"template": "blue",
|
||||
"title": {
|
||||
"content": "📤 七牛云上传",
|
||||
"tag": "plain_text"
|
||||
}
|
||||
},
|
||||
"elements": [
|
||||
{
|
||||
"tag": "div",
|
||||
"text": {
|
||||
"content": "**当前存储桶:** {{bucket_name}} ({{bucket_id}})\n**区域:** {{region_name}} ({{region_code}})\n**CDN 域名:** {{cdn_domain}}",
|
||||
"tag": "lark_md"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "hr"
|
||||
},
|
||||
{
|
||||
"tag": "action",
|
||||
"actions": [
|
||||
{
|
||||
"tag": "button",
|
||||
"text": {
|
||||
"content": "📎 选择文件上传",
|
||||
"tag": "plain_text"
|
||||
},
|
||||
"type": "primary",
|
||||
"value": {
|
||||
"action": "upload_select",
|
||||
"bucket": "{{bucket_name}}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "button",
|
||||
"text": {
|
||||
"content": "📋 查看配置",
|
||||
"tag": "plain_text"
|
||||
},
|
||||
"value": {
|
||||
"action": "config_view"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "button",
|
||||
"text": {
|
||||
"content": "❓ 帮助",
|
||||
"tag": "plain_text"
|
||||
},
|
||||
"value": {
|
||||
"action": "help"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
138
deploy.sh
Executable file
138
deploy.sh
Executable file
@@ -0,0 +1,138 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 七牛云上传 Skill - 部署脚本
|
||||
# 用途:将 Skill 部署到其他 OpenClaw 服务器
|
||||
|
||||
set -e
|
||||
|
||||
SKILL_NAME="qiniu-uploader"
|
||||
SKILL_DIR="$HOME/.openclaw/workspace/skills/$SKILL_NAME"
|
||||
CREDENTIALS_DIR="$HOME/.openclaw/credentials"
|
||||
|
||||
echo "🍙 七牛云上传 Skill - 部署工具"
|
||||
echo "════════════════════════════════════════════════════"
|
||||
echo ""
|
||||
|
||||
# 检查是否提供了目标服务器
|
||||
if [ -z "$1" ]; then
|
||||
echo "用法:$0 <目标服务器> [目标路径]"
|
||||
echo ""
|
||||
echo "示例:"
|
||||
echo " $0 user@192.168.1.100"
|
||||
echo " $0 user@example.com:~/.openclaw/workspace/skills/"
|
||||
echo ""
|
||||
echo "或者,如果是本地部署到其他 OpenClaw 实例:"
|
||||
echo " $0 local"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
TARGET="$1"
|
||||
TARGET_PATH="${2:-$HOME/.openclaw/workspace/skills/}"
|
||||
|
||||
if [ "$TARGET" = "local" ]; then
|
||||
echo "📦 本地部署模式"
|
||||
echo ""
|
||||
|
||||
# 检查 Skill 目录是否存在
|
||||
if [ ! -d "$SKILL_DIR" ]; then
|
||||
echo "❌ Skill 目录不存在:$SKILL_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 创建凭证目录
|
||||
mkdir -p "$CREDENTIALS_DIR"
|
||||
|
||||
# 创建配置模板
|
||||
CONFIG_FILE="$CREDENTIALS_DIR/qiniu-config.json"
|
||||
if [ ! -f "$CONFIG_FILE" ]; then
|
||||
echo "📝 创建配置模板..."
|
||||
cat > "$CONFIG_FILE" << 'EOF'
|
||||
{
|
||||
"buckets": {
|
||||
"default": {
|
||||
"accessKey": "YOUR_ACCESS_KEY_HERE",
|
||||
"secretKey": "YOUR_SECRET_KEY_HERE",
|
||||
"bucket": "your-bucket-name",
|
||||
"region": "z2",
|
||||
"domain": "https://your-cdn-domain.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
EOF
|
||||
echo "✅ 配置模板已创建:$CONFIG_FILE"
|
||||
echo ""
|
||||
echo "⚠️ 请编辑配置文件,填入你的七牛云密钥:"
|
||||
echo " nano $CONFIG_FILE"
|
||||
echo ""
|
||||
else
|
||||
echo "✅ 配置文件已存在:$CONFIG_FILE"
|
||||
fi
|
||||
|
||||
echo "🎉 本地部署完成!"
|
||||
echo ""
|
||||
echo "下一步:"
|
||||
echo " 1. 编辑配置文件:nano $CONFIG_FILE"
|
||||
echo " 2. 重启 Gateway: openclaw gateway restart"
|
||||
echo " 3. 测试上传:/upload /test/file.txt"
|
||||
|
||||
else
|
||||
echo "📦 远程部署模式"
|
||||
echo "目标:$TARGET:$TARGET_PATH"
|
||||
echo ""
|
||||
|
||||
# 检查 Skill 目录是否存在
|
||||
if [ ! -d "$SKILL_DIR" ]; then
|
||||
echo "❌ Skill 目录不存在:$SKILL_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 打包 Skill
|
||||
echo "📦 打包 Skill..."
|
||||
cd "$HOME/.openclaw/workspace/skills/"
|
||||
tar -czf "/tmp/${SKILL_NAME}.tar.gz" "$SKILL_NAME/"
|
||||
|
||||
# 传输到目标服务器
|
||||
echo "📤 传输到目标服务器..."
|
||||
scp "/tmp/${SKILL_NAME}.tar.gz" "$TARGET:/tmp/"
|
||||
|
||||
# 在目标服务器上解压
|
||||
echo "📥 在目标服务器上解压..."
|
||||
ssh "$TARGET" << 'ENDSSH'
|
||||
mkdir -p ~/.openclaw/workspace/skills/
|
||||
tar -xzf /tmp/qiniu-uploader.tar.gz -C ~/.openclaw/workspace/skills/
|
||||
rm /tmp/qiniu-uploader.tar.gz
|
||||
echo "✅ Skill 已部署到 ~/.openclaw/workspace/skills/qiniu-uploader/"
|
||||
ENDSSH
|
||||
|
||||
# 创建配置说明
|
||||
echo ""
|
||||
echo "📝 请在目标服务器上配置七牛云凭证:"
|
||||
echo ""
|
||||
echo "ssh $TARGET << 'ENDSSH'"
|
||||
echo "mkdir -p ~/.openclaw/credentials/"
|
||||
echo "cat > ~/.openclaw/credentials/qiniu-config.json << 'EOF'"
|
||||
echo "{"
|
||||
echo ' "buckets": {'
|
||||
echo ' "default": {'
|
||||
echo ' "accessKey": "YOUR_ACCESS_KEY_HERE",'
|
||||
echo ' "secretKey": "YOUR_SECRET_KEY_HERE",'
|
||||
echo ' "bucket": "your-bucket-name",'
|
||||
echo ' "region": "z2",'
|
||||
echo ' "domain": "https://your-cdn-domain.com"'
|
||||
echo ' }'
|
||||
echo ' }'
|
||||
echo "}"
|
||||
echo "EOF"
|
||||
echo "openclaw gateway restart"
|
||||
echo "ENDSSH"
|
||||
echo ""
|
||||
|
||||
# 清理临时文件
|
||||
rm -f "/tmp/${SKILL_NAME}.tar.gz"
|
||||
|
||||
echo "🎉 远程部署完成!"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "════════════════════════════════════════════════════"
|
||||
echo "📖 详细文档:$SKILL_DIR/INSTALL.md"
|
||||
162
openclaw-handler.js
Normal file
162
openclaw-handler.js
Normal file
@@ -0,0 +1,162 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* OpenClaw 飞书消息处理器 - 七牛云上传
|
||||
*
|
||||
* 用途:作为 OpenClaw 的飞书消息中间件,处理七牛云相关命令
|
||||
*
|
||||
* 使用方式:
|
||||
* 1. 在 OpenClaw 配置中注册为飞书消息处理器
|
||||
* 2. 或作为独立服务运行,接收 OpenClaw 转发的消息
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { exec } = require('child_process');
|
||||
const http = require('http');
|
||||
|
||||
// ============ 配置 ============
|
||||
|
||||
const CONFIG = {
|
||||
port: process.env.QINIU_HANDLER_PORT || 3001,
|
||||
openclawGateway: {
|
||||
host: '127.0.0.1',
|
||||
port: 17733
|
||||
},
|
||||
scriptDir: path.join(__dirname, 'scripts'),
|
||||
credentials: path.join(process.env.HOME || process.env.USERPROFILE, '.openclaw/credentials')
|
||||
};
|
||||
|
||||
// ============ 工具函数 ============
|
||||
|
||||
function log(...args) {
|
||||
const timestamp = new Date().toISOString();
|
||||
console.log(`[${timestamp}]`, ...args);
|
||||
}
|
||||
|
||||
// ============ 命令解析 ============
|
||||
|
||||
function isQiniuCommand(text) {
|
||||
const trimmed = text.trim();
|
||||
return /^\/(upload|qiniu-config|qiniu-help)/i.test(trimmed);
|
||||
}
|
||||
|
||||
// ============ 消息处理 ============
|
||||
|
||||
async function handleMessage(message) {
|
||||
const text = message.content?.text || '';
|
||||
|
||||
if (!isQiniuCommand(text)) {
|
||||
return { handled: false };
|
||||
}
|
||||
|
||||
log('处理七牛云命令:', text);
|
||||
|
||||
// 调用上传脚本
|
||||
const uploadScript = path.join(CONFIG.scriptDir, 'upload-to-qiniu.js');
|
||||
|
||||
// 解析命令
|
||||
if (text.trim().startsWith('/upload')) {
|
||||
return await handleUpload(message, uploadScript);
|
||||
}
|
||||
|
||||
if (text.trim().startsWith('/qiniu-config')) {
|
||||
return await handleConfig(message, uploadScript);
|
||||
}
|
||||
|
||||
if (text.trim().startsWith('/qiniu-help')) {
|
||||
return await handleHelp(message);
|
||||
}
|
||||
|
||||
return { handled: false };
|
||||
}
|
||||
|
||||
async function handleUpload(message, script) {
|
||||
// TODO: 实现文件下载和上传逻辑
|
||||
return {
|
||||
handled: true,
|
||||
reply: '🚧 上传功能开发中...\n\n请使用独立监听器模式:\nhttp://47.83.185.237:3000'
|
||||
};
|
||||
}
|
||||
|
||||
async function handleConfig(message, script) {
|
||||
const configCmd = message.content.text.replace('/qiniu-config', 'config');
|
||||
|
||||
return new Promise((resolve) => {
|
||||
exec(`node "${script}" ${configCmd}`, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
resolve({
|
||||
handled: true,
|
||||
reply: `❌ 错误:${stderr || error.message}`
|
||||
});
|
||||
return;
|
||||
}
|
||||
resolve({
|
||||
handled: true,
|
||||
reply: '```\n' + stdout + '\n```'
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function handleHelp(message) {
|
||||
const helpText = `
|
||||
🍙 七牛云上传 - 使用帮助
|
||||
|
||||
📤 上传文件:
|
||||
/upload [目标路径] [存储桶名]
|
||||
/upload --original [存储桶名]
|
||||
|
||||
⚙️ 配置管理:
|
||||
/qiniu-config list
|
||||
/qiniu-config set <key> <value>
|
||||
/qiniu-config set-bucket <name> <json>
|
||||
`;
|
||||
|
||||
return {
|
||||
handled: true,
|
||||
reply: helpText
|
||||
};
|
||||
}
|
||||
|
||||
// ============ HTTP 服务器 ============
|
||||
|
||||
function startServer() {
|
||||
const server = http.createServer(async (req, res) => {
|
||||
if (req.method !== 'POST') {
|
||||
res.writeHead(405);
|
||||
res.end('Method Not Allowed');
|
||||
return;
|
||||
}
|
||||
|
||||
let body = '';
|
||||
req.on('data', chunk => body += chunk);
|
||||
req.on('end', async () => {
|
||||
try {
|
||||
const event = JSON.parse(body);
|
||||
const result = await handleMessage(event.message);
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify(result));
|
||||
|
||||
} catch (e) {
|
||||
log('处理失败:', e.message);
|
||||
res.writeHead(500);
|
||||
res.end('Internal Server Error');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
server.listen(CONFIG.port, () => {
|
||||
log(`🚀 七牛云处理器启动,端口:${CONFIG.port}`);
|
||||
});
|
||||
}
|
||||
|
||||
// ============ 主函数 ============
|
||||
|
||||
function main() {
|
||||
log('🍙 OpenClaw 飞书消息处理器 - 七牛云');
|
||||
startServer();
|
||||
}
|
||||
|
||||
main();
|
||||
437
openclaw-processor.js
Executable file
437
openclaw-processor.js
Executable file
@@ -0,0 +1,437 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* OpenClaw Skill - 七牛云上传处理器
|
||||
*
|
||||
* 用途:处理 OpenClaw 转发的七牛云相关命令
|
||||
* 使用方式:作为 OpenClaw 的工具脚本被调用
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { exec } = require('child_process');
|
||||
const https = require('https');
|
||||
const http = require('http');
|
||||
|
||||
// ============ 配置 ============
|
||||
|
||||
const CONFIG = {
|
||||
scriptDir: __dirname,
|
||||
credentials: path.join(process.env.HOME || process.env.USERPROFILE, '.openclaw/credentials'),
|
||||
tempDir: path.join(process.env.HOME || process.env.USERPROFILE, '.openclaw/credentials/temp'),
|
||||
// 飞书 API 配置
|
||||
feishu: {
|
||||
appId: process.env.FEISHU_APP_ID || 'cli_a92ce47b02381bcc',
|
||||
appSecret: process.env.FEISHU_APP_SECRET || 'WpCWhqOPKv3F5Lhn11DqubrssJnAodot'
|
||||
}
|
||||
};
|
||||
|
||||
// ============ 工具函数 ============
|
||||
|
||||
function log(...args) {
|
||||
const timestamp = new Date().toISOString();
|
||||
console.error(`[${timestamp}]`, ...args);
|
||||
}
|
||||
|
||||
function ensureTempDir() {
|
||||
if (!fs.existsSync(CONFIG.tempDir)) {
|
||||
fs.mkdirSync(CONFIG.tempDir, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
// ============ 飞书 API ============
|
||||
|
||||
async function getAccessToken() {
|
||||
const url = 'https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal';
|
||||
|
||||
const body = JSON.stringify({
|
||||
app_id: CONFIG.feishu.appId,
|
||||
app_secret: CONFIG.feishu.appSecret
|
||||
});
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = https.request(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
}, (res) => {
|
||||
let data = '';
|
||||
res.on('data', chunk => data += chunk);
|
||||
res.on('end', () => {
|
||||
try {
|
||||
const result = JSON.parse(data);
|
||||
if (result.code === 0) {
|
||||
resolve(result.tenant_access_token);
|
||||
} else {
|
||||
reject(new Error(`获取 token 失败:${result.msg}`));
|
||||
}
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', reject);
|
||||
req.write(body);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
async function downloadFeishuFile(fileKey, destPath) {
|
||||
const token = await getAccessToken();
|
||||
const url = `https://open.feishu.cn/open-apis/im/v1/files/${fileKey}/download`;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = https.get(url, {
|
||||
headers: { 'Authorization': `Bearer ${token}` }
|
||||
}, (res) => {
|
||||
if (res.statusCode !== 200) {
|
||||
reject(new Error(`下载失败:${res.statusCode}`));
|
||||
return;
|
||||
}
|
||||
|
||||
const file = fs.createWriteStream(destPath);
|
||||
res.pipe(file);
|
||||
file.on('finish', () => {
|
||||
file.close();
|
||||
resolve(destPath);
|
||||
});
|
||||
}).on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
async function sendMessageToChat(chatId, text) {
|
||||
const token = await getAccessToken();
|
||||
|
||||
const url = 'https://open.feishu.cn/open-apis/im/v1/messages';
|
||||
const body = JSON.stringify({
|
||||
receive_id: chatId,
|
||||
msg_type: 'text',
|
||||
content: JSON.stringify({ text })
|
||||
});
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = https.request(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
}, (res) => {
|
||||
let data = '';
|
||||
res.on('data', chunk => data += chunk);
|
||||
res.on('end', () => {
|
||||
try {
|
||||
resolve(JSON.parse(data));
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', reject);
|
||||
req.write(body);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
// ============ 命令解析 ============
|
||||
|
||||
function parseUploadCommand(text) {
|
||||
// 支持 /upload 和 /u 两种命令
|
||||
const match = text.match(/^\/(upload|u)(?:\s+(.+))?$/i);
|
||||
if (!match) return null;
|
||||
|
||||
// match[1] = 命令名 (upload/u), match[2] = 参数
|
||||
const args = (match[2] || '').trim().split(/\s+/).filter(Boolean);
|
||||
|
||||
let targetPath = null;
|
||||
let useOriginal = false;
|
||||
let bucket = 'default';
|
||||
|
||||
for (const arg of args) {
|
||||
if (arg === '--original') {
|
||||
useOriginal = true;
|
||||
} else if (arg.startsWith('/') || arg.includes('.')) {
|
||||
targetPath = arg;
|
||||
} else {
|
||||
bucket = arg;
|
||||
}
|
||||
}
|
||||
|
||||
return { targetPath, useOriginal, bucket };
|
||||
}
|
||||
|
||||
function parseConfigCommand(text) {
|
||||
const match = text.match(/^\/qiniu-config\s+(.+)$/i);
|
||||
if (!match) return null;
|
||||
|
||||
const args = match[1].trim().split(/\s+/);
|
||||
return {
|
||||
subCommand: args[0],
|
||||
args: args.slice(1)
|
||||
};
|
||||
}
|
||||
|
||||
// ============ 命令处理 ============
|
||||
|
||||
async function handleUpload(message) {
|
||||
const content = typeof message.content === 'string' ? JSON.parse(message.content) : message.content;
|
||||
const text = content.text || '';
|
||||
const attachments = message.attachments || [];
|
||||
|
||||
const cmd = parseUploadCommand(text);
|
||||
if (!cmd) {
|
||||
return { handled: false };
|
||||
}
|
||||
|
||||
log('处理上传命令:', cmd);
|
||||
|
||||
// 检查附件
|
||||
if (!attachments || attachments.length === 0) {
|
||||
return {
|
||||
handled: true,
|
||||
reply: `❌ 请附上要上传的文件
|
||||
|
||||
💡 使用示例:
|
||||
/upload /config/test/file.txt default
|
||||
[附上文件]
|
||||
|
||||
或:/upload --original default
|
||||
[附上文件] (使用原文件名)`
|
||||
};
|
||||
}
|
||||
|
||||
const attachment = attachments[0];
|
||||
const fileKey = attachment.file_key;
|
||||
const originalFileName = attachment.file_name;
|
||||
|
||||
log(`下载文件:${originalFileName} (${fileKey})`);
|
||||
|
||||
try {
|
||||
// 确保临时目录存在
|
||||
ensureTempDir();
|
||||
|
||||
// 下载文件
|
||||
const tempFile = path.join(CONFIG.tempDir, `upload_${Date.now()}_${originalFileName}`);
|
||||
await downloadFeishuFile(fileKey, tempFile);
|
||||
|
||||
log('文件已下载:', tempFile);
|
||||
|
||||
// 确定目标文件名
|
||||
let targetKey;
|
||||
if (cmd.useOriginal) {
|
||||
targetKey = originalFileName;
|
||||
} else if (cmd.targetPath) {
|
||||
// 如果指定了路径,保留完整路径(去掉前导 /)
|
||||
targetKey = cmd.targetPath.startsWith('/') ? cmd.targetPath.substring(1) : cmd.targetPath;
|
||||
} else {
|
||||
// 没有指定路径时,使用原文件名
|
||||
targetKey = originalFileName;
|
||||
}
|
||||
|
||||
// 确保 targetKey 不为空
|
||||
if (!targetKey || targetKey.trim() === '') {
|
||||
targetKey = originalFileName;
|
||||
}
|
||||
|
||||
log('目标 key:', targetKey);
|
||||
log('原始文件名:', originalFileName);
|
||||
log('命令参数:', cmd);
|
||||
|
||||
// 调用上传脚本
|
||||
log('上传到七牛云:', targetKey);
|
||||
|
||||
const uploadScript = path.join(CONFIG.scriptDir, 'scripts/upload-to-qiniu.js');
|
||||
const uploadCmd = `node "${uploadScript}" upload --file "${tempFile}" --key "${targetKey}" --bucket "${cmd.bucket}"`;
|
||||
|
||||
const { stdout, stderr } = await new Promise((resolve, reject) => {
|
||||
exec(uploadCmd, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
reject(new Error(`上传失败:${stderr || error.message}`));
|
||||
return;
|
||||
}
|
||||
resolve({ stdout, stderr });
|
||||
});
|
||||
});
|
||||
|
||||
log('上传结果:', stdout);
|
||||
|
||||
// 清理临时文件
|
||||
if (fs.existsSync(tempFile)) {
|
||||
fs.unlinkSync(tempFile);
|
||||
}
|
||||
|
||||
// 解析结果
|
||||
const urlMatch = stdout.match(/🔗 URL: (.+)/);
|
||||
const fileUrl = urlMatch ? urlMatch[1] : 'N/A';
|
||||
|
||||
// 解析存储桶名称(从输出中获取实际桶名)
|
||||
const bucketMatch = stdout.match(/☁️ 存储桶:(.+)/);
|
||||
const actualBucket = bucketMatch ? bucketMatch[1].trim() : (cmd.bucket || 'default');
|
||||
|
||||
// 调试输出
|
||||
log('存储桶解析:配置别名=', cmd.bucket, '实际桶名=', actualBucket);
|
||||
|
||||
// 直接返回完整回复
|
||||
return {
|
||||
handled: true,
|
||||
reply: `✅ 上传成功!
|
||||
|
||||
📦 文件:${targetKey}
|
||||
🔗 链接:${fileUrl}
|
||||
💾 原文件:${originalFileName}
|
||||
🪣 存储桶:${actualBucket}`
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
log('上传失败:', error.message);
|
||||
|
||||
// 清理临时文件
|
||||
const tempFiles = fs.readdirSync(CONFIG.tempDir);
|
||||
tempFiles.forEach(f => {
|
||||
if (f.startsWith('upload_')) {
|
||||
try {
|
||||
fs.unlinkSync(path.join(CONFIG.tempDir, f));
|
||||
} catch (e) {}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
handled: true,
|
||||
reply: `❌ 上传失败:${error.message}`
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function handleConfig(message) {
|
||||
const content = typeof message.content === 'string' ? JSON.parse(message.content) : message.content;
|
||||
const text = content.text || '';
|
||||
|
||||
const cmd = parseConfigCommand(text);
|
||||
if (!cmd) {
|
||||
return { handled: false };
|
||||
}
|
||||
|
||||
log('处理配置命令:', cmd.subCommand);
|
||||
|
||||
try {
|
||||
const uploadScript = path.join(CONFIG.scriptDir, 'scripts/upload-to-qiniu.js');
|
||||
const configCmd = `node "${uploadScript}" config ${cmd.subCommand} ${cmd.args.join(' ')}`;
|
||||
|
||||
const { stdout, stderr } = await new Promise((resolve, reject) => {
|
||||
exec(configCmd, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
reject(new Error(stderr || error.message));
|
||||
return;
|
||||
}
|
||||
resolve({ stdout, stderr });
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
handled: true,
|
||||
reply: '```\n' + stdout + '\n```'
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
return {
|
||||
handled: true,
|
||||
reply: `❌ 配置命令执行失败:${error.message}`
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function handleHelp() {
|
||||
return {
|
||||
handled: true,
|
||||
reply: `
|
||||
🍙 七牛云上传 - 使用帮助
|
||||
|
||||
📤 上传文件:
|
||||
/upload [目标路径] [存储桶名]
|
||||
/upload --original [存储桶名]
|
||||
|
||||
示例:
|
||||
/upload /config/test/file.txt default
|
||||
/upload --original default
|
||||
/upload docs/report.pdf
|
||||
|
||||
⚙️ 配置管理:
|
||||
/qiniu-config list # 查看配置
|
||||
/qiniu-config set <key> <value> # 修改配置
|
||||
/qiniu-config set-bucket <name> <json> # 添加存储桶
|
||||
/qiniu-config reset # 重置配置
|
||||
|
||||
示例:
|
||||
/qiniu-config set default.accessKey YOUR_KEY
|
||||
/qiniu-config set default.domain https://cdn.example.com
|
||||
`
|
||||
};
|
||||
}
|
||||
|
||||
// ============ 主处理函数 ============
|
||||
|
||||
async function processMessage(message) {
|
||||
const content = typeof message.content === 'string' ? JSON.parse(message.content) : message.content;
|
||||
const text = content.text || '';
|
||||
const trimmed = text.trim();
|
||||
|
||||
// 检查是否是七牛云命令
|
||||
if (/^\/upload/i.test(trimmed)) {
|
||||
return await handleUpload(message);
|
||||
}
|
||||
|
||||
if (/^\/qiniu-config/i.test(trimmed)) {
|
||||
return await handleConfig(message);
|
||||
}
|
||||
|
||||
if (/^\/(qiniu-)?help/i.test(trimmed)) {
|
||||
return await handleHelp();
|
||||
}
|
||||
|
||||
return { handled: false };
|
||||
}
|
||||
|
||||
// ============ 命令行接口 ============
|
||||
|
||||
async function main() {
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
if (args.length === 0) {
|
||||
console.log('七牛云上传 Skill 处理器');
|
||||
console.log('');
|
||||
console.log('用法:');
|
||||
console.log(' node openclaw-processor.js --message "<JSON 消息>"');
|
||||
console.log('');
|
||||
console.log('示例:');
|
||||
console.log(' node openclaw-processor.js --message "{\"content\":{\"text\":\"/qiniu-config list\"}}"');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (args[0] === '--message' && args[1]) {
|
||||
try {
|
||||
const message = JSON.parse(args[1]);
|
||||
const result = await processMessage(message);
|
||||
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
|
||||
if (result.handled && result.reply) {
|
||||
// 如果有 chat_id,直接发送消息
|
||||
if (message.chat_id) {
|
||||
await sendMessageToChat(message.chat_id, result.reply);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
console.error('处理失败:', e.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 导出给 OpenClaw 调用
|
||||
if (require.main === module) {
|
||||
main();
|
||||
}
|
||||
|
||||
module.exports = { processMessage, handleUpload, handleConfig, handleHelp };
|
||||
33
package-lock.json
generated
Normal file
33
package-lock.json
generated
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "qiniu-uploader",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"dependencies": {
|
||||
"ws": "^8.19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.19.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
|
||||
"integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": ">=5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
43
package.json
Normal file
43
package.json
Normal file
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"name": "qiniu-uploader",
|
||||
"version": "2.0.0",
|
||||
"description": "七牛云文件上传和管理 Skill",
|
||||
"author": "饭团 🍙",
|
||||
"license": "MIT",
|
||||
"main": "openclaw-processor.js",
|
||||
"scripts": {
|
||||
"upload": "node scripts/upload-to-qiniu.js",
|
||||
"test": "node openclaw-processor.js --message '{\"content\":{\"text\":\"/qiniu-config list\"}}'"
|
||||
},
|
||||
"dependencies": {
|
||||
"ws": "^8.14.0"
|
||||
},
|
||||
"openclaw": {
|
||||
"type": "skill",
|
||||
"triggers": [
|
||||
"/upload",
|
||||
"/qiniu-config",
|
||||
"/qiniu-help"
|
||||
],
|
||||
"handler": "openclaw-processor.js",
|
||||
"config": {
|
||||
"credentials": "~/.openclaw/credentials/qiniu-config.json",
|
||||
"feishu": {
|
||||
"appId": "cli_a92ce47b02381bcc",
|
||||
"appSecret": "WpCWhqOPKv3F5Lhn11DqubrssJnAodot"
|
||||
}
|
||||
}
|
||||
},
|
||||
"keywords": [
|
||||
"qiniu",
|
||||
"upload",
|
||||
"cdn",
|
||||
"feishu",
|
||||
"openclaw",
|
||||
"skill"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/openclaw/openclaw"
|
||||
}
|
||||
}
|
||||
17
qiniu-config.example.json
Normal file
17
qiniu-config.example.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"buckets": {
|
||||
"default": {
|
||||
"accessKey": "YOUR_ACCESS_KEY_HERE",
|
||||
"secretKey": "YOUR_SECRET_KEY_HERE",
|
||||
"bucket": "your-bucket-name",
|
||||
"region": "z0",
|
||||
"domain": "https://your-cdn-domain.com"
|
||||
}
|
||||
},
|
||||
"_comment": {
|
||||
"region": "区域代码:z0=华东,z1=华北,z2=华南,na0=北美,as0=东南亚",
|
||||
"setup": "1. 获取七牛 AccessKey/SecretKey: https://portal.qiniu.com/user/key",
|
||||
"setup2": "2. 创建存储桶并配置 CDN 域名",
|
||||
"setup3": "3. 将此文件保存为 ~/.openclaw/credentials/qiniu-config.json"
|
||||
}
|
||||
}
|
||||
213
scripts/check-bucket-override.js
Normal file
213
scripts/check-bucket-override.js
Normal file
@@ -0,0 +1,213 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* 七牛云存储桶覆盖设置检查脚本
|
||||
*
|
||||
* 用途:检查存储桶是否允许覆盖上传
|
||||
*
|
||||
* 用法:
|
||||
* node check-bucket-override.js [bucket-name]
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const crypto = require('crypto');
|
||||
const https = require('https');
|
||||
const http = require('http');
|
||||
|
||||
const DEFAULT_CONFIG_PATH = path.join(process.env.HOME || process.env.USERPROFILE, '.openclaw/credentials/qiniu-config.json');
|
||||
|
||||
function loadConfig() {
|
||||
if (!fs.existsSync(DEFAULT_CONFIG_PATH)) {
|
||||
throw new Error(`配置文件不存在:${DEFAULT_CONFIG_PATH}`);
|
||||
}
|
||||
return JSON.parse(fs.readFileSync(DEFAULT_CONFIG_PATH, 'utf-8'));
|
||||
}
|
||||
|
||||
function hmacSha1(data, secret) {
|
||||
return crypto.createHmac('sha1', secret).update(data).digest();
|
||||
}
|
||||
|
||||
function urlSafeBase64(data) {
|
||||
return Buffer.from(data).toString('base64')
|
||||
.replace(/\+/g, '-')
|
||||
.replace(/\//g, '_');
|
||||
}
|
||||
|
||||
function generateAccessToken(accessKey, secretKey, method, path, body = '') {
|
||||
const host = 'kodo.qiniu.com';
|
||||
const contentType = 'application/json';
|
||||
|
||||
// 格式:Method Path\nHost: Host\nContent-Type: ContentType\n\nBody
|
||||
const signData = `${method} ${path}\nHost: ${host}\nContent-Type: ${contentType}\n\n${body}`;
|
||||
const signature = hmacSha1(signData, secretKey);
|
||||
const encodedSign = urlSafeBase64(signature);
|
||||
|
||||
return `Qiniu ${accessKey}:${encodedSign}`;
|
||||
}
|
||||
|
||||
function httpRequest(url, options, body = null) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const protocol = url.startsWith('https') ? https : http;
|
||||
|
||||
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({ status: res.statusCode, data: json });
|
||||
} catch (e) {
|
||||
resolve({ status: res.statusCode, data: data });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', reject);
|
||||
|
||||
if (body) {
|
||||
req.write(body);
|
||||
}
|
||||
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
async function checkBucket(bucketName) {
|
||||
const config = loadConfig();
|
||||
const bucketConfig = config.buckets[bucketName || 'default'];
|
||||
|
||||
if (!bucketConfig) {
|
||||
throw new Error(`存储桶配置 "${bucketName || 'default'}" 不存在`);
|
||||
}
|
||||
|
||||
const { accessKey, secretKey, bucket, region } = bucketConfig;
|
||||
|
||||
console.log('🔍 检查存储桶覆盖设置...\n');
|
||||
console.log(`存储桶:${bucket}`);
|
||||
console.log(`区域:${region}`);
|
||||
console.log(`AccessKey: ${accessKey.substring(0, 4)}...${accessKey.substring(accessKey.length - 4)}`);
|
||||
console.log('');
|
||||
|
||||
// 1. 获取存储桶列表
|
||||
// 七牛云 API 文档:https://developer.qiniu.com/kodo/api/1314/list-buckets
|
||||
const listBucketsUrl = 'https://kodo.qiniu.com/v2/buckets';
|
||||
const accessToken = generateAccessToken(accessKey, secretKey, 'GET', '/v2/buckets');
|
||||
|
||||
const listOptions = {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Host': 'kodo.qiniu.com',
|
||||
'Authorization': accessToken
|
||||
}
|
||||
};
|
||||
|
||||
console.log('📋 获取存储桶列表...');
|
||||
const listResult = await httpRequest(listBucketsUrl, listOptions);
|
||||
|
||||
if (listResult.status !== 200) {
|
||||
console.error('❌ 获取存储桶列表失败:', listResult.data);
|
||||
return;
|
||||
}
|
||||
|
||||
const buckets = listResult.data;
|
||||
const targetBucket = buckets.find(b => b.name === bucket);
|
||||
|
||||
if (!targetBucket) {
|
||||
console.error(`❌ 未找到存储桶:${bucket}`);
|
||||
console.log('\n可用的存储桶:');
|
||||
buckets.forEach(b => console.log(` - ${b.name}`));
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('✅ 存储桶存在\n');
|
||||
|
||||
// 2. 获取存储桶详细信息
|
||||
const bucketInfoUrl = `https://kodo.qiniu.com/v2/buckets/${bucket}`;
|
||||
const bucketInfoToken = generateAccessToken(accessKey, secretKey, 'GET', `/v2/buckets/${bucket}`);
|
||||
|
||||
const infoOptions = {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Host': 'kodo.qiniu.com',
|
||||
'Authorization': bucketInfoToken
|
||||
}
|
||||
};
|
||||
|
||||
console.log('📋 获取存储桶详细信息...');
|
||||
const infoResult = await httpRequest(bucketInfoUrl, infoOptions);
|
||||
|
||||
if (infoResult.status !== 200) {
|
||||
console.error('❌ 获取存储桶信息失败:', infoResult.data);
|
||||
console.log('\n⚠️ 可能是权限不足,请检查 AccessKey/SecretKey 是否有存储桶管理权限');
|
||||
return;
|
||||
}
|
||||
|
||||
const bucketInfo = infoResult.data;
|
||||
|
||||
console.log('\n📊 存储桶配置信息:');
|
||||
console.log('─────────────────────────────────────');
|
||||
console.log(` 名称:${bucketInfo.name || 'N/A'}`);
|
||||
console.log(` 区域:${bucketInfo.region || bucketInfo.info?.region || 'N/A'}`);
|
||||
console.log(` 创建时间:${bucketInfo.createdAt || bucketInfo.info?.createdAt || 'N/A'}`);
|
||||
|
||||
// 检查覆盖相关设置
|
||||
const info = bucketInfo.info || bucketInfo;
|
||||
|
||||
console.log('\n🔒 安全设置:');
|
||||
console.log('─────────────────────────────────────');
|
||||
|
||||
// 防覆盖设置(关键!)
|
||||
const noOverwrite = info.noOverwrite !== undefined ? info.noOverwrite : '未设置';
|
||||
console.log(` 防覆盖:${noOverwrite === true || noOverwrite === 1 ? '❌ 已启用(禁止覆盖)' : '✅ 未启用(允许覆盖)'}`);
|
||||
|
||||
// 私有空间设置
|
||||
const private = info.private !== undefined ? info.private : '未知';
|
||||
console.log(` 空间类型:${private === true || private === 1 ? '私有空间' : '公共空间'}`);
|
||||
|
||||
// 其他设置
|
||||
if (info.maxSpace !== undefined) {
|
||||
console.log(` 容量限制:${info.maxSpace} bytes`);
|
||||
}
|
||||
|
||||
console.log('\n💡 解决方案:');
|
||||
console.log('─────────────────────────────────────');
|
||||
|
||||
if (noOverwrite === true || noOverwrite === 1) {
|
||||
console.log('⚠️ 存储桶已启用"防覆盖"设置,需要关闭才能覆盖上传同名文件。\n');
|
||||
console.log('关闭方法:');
|
||||
console.log('1. 登录七牛云控制台:https://portal.qiniu.com/');
|
||||
console.log(`2. 进入"对象存储" → 选择存储桶 "${bucket}"`);
|
||||
console.log('3. 点击"设置" → "空间设置"');
|
||||
console.log('4. 找到"防覆盖"选项,关闭它');
|
||||
console.log('5. 保存设置后重试上传\n');
|
||||
console.log('或者使用命令行关闭:');
|
||||
console.log(`node scripts/update-bucket-setting.js ${bucket} noOverwrite 0`);
|
||||
} else {
|
||||
console.log('✅ 存储桶允许覆盖上传');
|
||||
console.log('\n如果仍然无法覆盖,可能原因:');
|
||||
console.log('1. 上传凭证 scope 指定了具体 key,但上传时使用了不同的 key');
|
||||
console.log('2. 上传 API 端点不正确');
|
||||
console.log('3. 文件正在被其他进程占用');
|
||||
console.log('\n建议:');
|
||||
console.log('- 检查上传日志中的实际上传 key 是否一致');
|
||||
console.log('- 使用相同的完整路径(包括前导 /)');
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const bucketName = process.argv[2];
|
||||
|
||||
try {
|
||||
await checkBucket(bucketName);
|
||||
} catch (error) {
|
||||
console.error('❌ 错误:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
main();
|
||||
}
|
||||
|
||||
module.exports = { checkBucket };
|
||||
275
scripts/debug-upload.js
Normal file
275
scripts/debug-upload.js
Normal file
@@ -0,0 +1,275 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* 七牛云上传调试脚本
|
||||
*
|
||||
* 用途:测试上传并显示详细错误信息
|
||||
*
|
||||
* 用法:
|
||||
* node debug-upload.js --file <文件路径> --key <目标路径>
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const crypto = require('crypto');
|
||||
const https = require('https');
|
||||
const http = require('http');
|
||||
|
||||
const DEFAULT_CONFIG_PATH = path.join(process.env.HOME || process.env.USERPROFILE, '.openclaw/credentials/qiniu-config.json');
|
||||
|
||||
function loadConfig() {
|
||||
if (!fs.existsSync(DEFAULT_CONFIG_PATH)) {
|
||||
throw new Error(`配置文件不存在:${DEFAULT_CONFIG_PATH}`);
|
||||
}
|
||||
return JSON.parse(fs.readFileSync(DEFAULT_CONFIG_PATH, 'utf-8'));
|
||||
}
|
||||
|
||||
function hmacSha1(data, secret) {
|
||||
return crypto.createHmac('sha1', secret).update(data).digest();
|
||||
}
|
||||
|
||||
function urlSafeBase64(data) {
|
||||
return Buffer.from(data).toString('base64')
|
||||
.replace(/\+/g, '-')
|
||||
.replace(/\//g, '_');
|
||||
}
|
||||
|
||||
function generateUploadToken(accessKey, secretKey, bucket, key = null, expires = 3600) {
|
||||
const deadline = Math.floor(Date.now() / 1000) + expires;
|
||||
|
||||
// 关键修复:scope 必须包含 key 才能覆盖上传
|
||||
let scope = bucket;
|
||||
if (key) {
|
||||
scope = `${bucket}:${key}`; // ✅ 添加 key,允许覆盖
|
||||
}
|
||||
|
||||
console.log('📝 上传凭证参数:');
|
||||
console.log(` scope: ${scope} (包含 key 才能覆盖)`);
|
||||
console.log(` deadline: ${deadline}`);
|
||||
console.log(` key: ${key || '(未指定,使用表单中的 key)'}`);
|
||||
|
||||
const putPolicy = {
|
||||
scope: scope,
|
||||
deadline: deadline,
|
||||
returnBody: JSON.stringify({
|
||||
success: true,
|
||||
key: '$(key)',
|
||||
hash: '$(etag)',
|
||||
fsize: '$(fsize)',
|
||||
bucket: '$(bucket)',
|
||||
url: `$(domain)/$(key)`
|
||||
})
|
||||
};
|
||||
|
||||
console.log('\n📋 上传凭证策略:');
|
||||
console.log(JSON.stringify(putPolicy, null, 2));
|
||||
|
||||
const encodedPolicy = urlSafeBase64(JSON.stringify(putPolicy));
|
||||
const encodedSignature = urlSafeBase64(hmacSha1(encodedPolicy, secretKey));
|
||||
|
||||
const token = `${accessKey}:${encodedSignature}:${encodedPolicy}`;
|
||||
|
||||
console.log('\n🔑 生成的上传凭证:');
|
||||
console.log(` ${accessKey}:${encodedSignature.substring(0, 20)}...`);
|
||||
|
||||
return token;
|
||||
}
|
||||
|
||||
function httpRequest(url, options, body = null) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const protocol = url.startsWith('https') ? https : http;
|
||||
|
||||
console.log(`\n📤 发送请求:`);
|
||||
console.log(` URL: ${url}`);
|
||||
console.log(` Method: ${options.method}`);
|
||||
console.log(` Headers:`, JSON.stringify(options.headers, null, 2));
|
||||
|
||||
const req = protocol.request(url, options, (res) => {
|
||||
console.log(`\n📥 收到响应:`);
|
||||
console.log(` Status: ${res.statusCode}`);
|
||||
console.log(` Headers:`, JSON.stringify(res.headers, null, 2));
|
||||
|
||||
let data = '';
|
||||
res.on('data', chunk => {
|
||||
data += chunk;
|
||||
console.log(` 接收数据块:${chunk.length} bytes`);
|
||||
});
|
||||
|
||||
res.on('end', () => {
|
||||
console.log(`\n📦 完整响应数据:`);
|
||||
console.log(data);
|
||||
|
||||
try {
|
||||
const json = JSON.parse(data);
|
||||
resolve({ status: res.statusCode, data: json });
|
||||
} catch (e) {
|
||||
resolve({ status: res.statusCode, data: data, raw: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', (e) => {
|
||||
console.error('❌ 请求错误:', e);
|
||||
reject(e);
|
||||
});
|
||||
|
||||
if (body) {
|
||||
console.log(`\n📤 请求体大小:${body.length} bytes`);
|
||||
req.write(body);
|
||||
}
|
||||
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
async function debugUpload() {
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
let filePath = null;
|
||||
let key = null;
|
||||
let bucketName = 'default';
|
||||
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
if (args[i] === '--file' && args[i + 1]) {
|
||||
filePath = args[i + 1];
|
||||
i++;
|
||||
} else if (args[i] === '--key' && args[i + 1]) {
|
||||
key = args[i + 1];
|
||||
i++;
|
||||
} else if (args[i] === '--bucket' && args[i + 1]) {
|
||||
bucketName = args[i + 1];
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
if (!filePath) {
|
||||
console.error('❌ 缺少必需参数 --file');
|
||||
console.error('用法:node debug-upload.js --file <文件路径> [--key <目标路径>] [--bucket <存储桶名>]');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!fs.existsSync(filePath)) {
|
||||
console.error(`❌ 文件不存在:${filePath}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const config = loadConfig();
|
||||
const bucketConfig = config.buckets[bucketName];
|
||||
|
||||
if (!bucketConfig) {
|
||||
console.error(`❌ 存储桶配置 "${bucketName}" 不存在`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const { accessKey, secretKey, bucket, region, domain } = bucketConfig;
|
||||
|
||||
// 确定目标 key
|
||||
if (!key) {
|
||||
key = path.basename(filePath);
|
||||
} else if (key.startsWith('/')) {
|
||||
key = key.substring(1);
|
||||
}
|
||||
|
||||
console.log('═══════════════════════════════════════════════════════════');
|
||||
console.log('🔍 七牛云上传调试');
|
||||
console.log('═══════════════════════════════════════════════════════════');
|
||||
console.log(`\n📁 文件信息:`);
|
||||
console.log(` 本地路径:${filePath}`);
|
||||
console.log(` 文件大小:${fs.statSync(filePath).size} bytes`);
|
||||
console.log(` 目标 key: ${key}`);
|
||||
console.log(` 存储桶:${bucket}`);
|
||||
console.log(` 区域:${region}`);
|
||||
console.log(` 域名:${domain}`);
|
||||
|
||||
// 生成上传凭证
|
||||
console.log('\n═══════════════════════════════════════════════════════════');
|
||||
const uploadToken = generateUploadToken(accessKey, secretKey, bucket, key);
|
||||
|
||||
// 构建上传请求
|
||||
const regionEndpoint = getUploadEndpoint(region);
|
||||
const boundary = '----WebKitFormBoundary' + Math.random().toString(36).substring(2);
|
||||
const fileContent = fs.readFileSync(filePath);
|
||||
const fileName = path.basename(filePath);
|
||||
|
||||
const bodyParts = [
|
||||
`------${boundary}`,
|
||||
'Content-Disposition: form-data; name="token"',
|
||||
'',
|
||||
uploadToken,
|
||||
`------${boundary}`,
|
||||
'Content-Disposition: form-data; name="key"',
|
||||
'',
|
||||
key,
|
||||
`------${boundary}`,
|
||||
`Content-Disposition: form-data; name="file"; filename="${fileName}"`,
|
||||
'Content-Type: application/octet-stream',
|
||||
'',
|
||||
'',
|
||||
];
|
||||
|
||||
const bodyBuffer = Buffer.concat([
|
||||
Buffer.from(bodyParts.join('\r\n'), 'utf-8'),
|
||||
fileContent,
|
||||
Buffer.from(`\r\n------${boundary}--\r\n`, 'utf-8')
|
||||
]);
|
||||
|
||||
const uploadUrl = `${regionEndpoint}/`;
|
||||
|
||||
const uploadOptions = {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': `multipart/form-data; boundary=----${boundary}`,
|
||||
'Content-Length': bodyBuffer.length
|
||||
}
|
||||
};
|
||||
|
||||
console.log('\n═══════════════════════════════════════════════════════════');
|
||||
console.log('📤 开始上传...');
|
||||
console.log('═══════════════════════════════════════════════════════════');
|
||||
|
||||
try {
|
||||
const result = await httpRequest(uploadUrl, uploadOptions, bodyBuffer);
|
||||
|
||||
console.log('\n═══════════════════════════════════════════════════════════');
|
||||
console.log('📊 上传结果:');
|
||||
console.log('═══════════════════════════════════════════════════════════');
|
||||
|
||||
if (result.status === 200) {
|
||||
console.log('✅ 上传成功!');
|
||||
console.log(` key: ${result.data.key}`);
|
||||
console.log(` hash: ${result.data.hash}`);
|
||||
console.log(` url: ${domain}/${result.data.key}`);
|
||||
} else {
|
||||
console.log('❌ 上传失败!');
|
||||
console.log(` HTTP Status: ${result.status}`);
|
||||
console.log(` 错误信息:`, JSON.stringify(result.data, null, 2));
|
||||
|
||||
// 解析常见错误
|
||||
if (result.data.error) {
|
||||
console.log('\n🔍 错误分析:');
|
||||
if (result.data.error.includes('file exists')) {
|
||||
console.log(' ⚠️ 文件已存在,存储桶可能禁止覆盖');
|
||||
} else if (result.data.error.includes('invalid token')) {
|
||||
console.log(' ⚠️ 上传凭证无效,检查 AccessKey/SecretKey');
|
||||
} else if (result.data.error.includes('bucket')) {
|
||||
console.log(' ⚠️ 存储桶配置问题');
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ 上传过程出错:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
function getUploadEndpoint(region) {
|
||||
const endpoints = {
|
||||
'z0': 'https://up.qiniup.com',
|
||||
'z1': 'https://up-z1.qiniup.com',
|
||||
'z2': 'https://up-z2.qiniup.com',
|
||||
'na0': 'https://up-na0.qiniup.com',
|
||||
'as0': 'https://up-as0.qiniup.com'
|
||||
};
|
||||
return endpoints[region] || endpoints['z0'];
|
||||
}
|
||||
|
||||
debugUpload().catch(console.error);
|
||||
410
scripts/feishu-card-server.js
Normal file
410
scripts/feishu-card-server.js
Normal file
@@ -0,0 +1,410 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* 飞书卡片交互服务器
|
||||
*
|
||||
* 功能:
|
||||
* 1. 接收飞书卡片按钮点击回调
|
||||
* 2. 处理交互逻辑(上传、配置、帮助)
|
||||
* 3. 回复交互式消息
|
||||
*
|
||||
* 使用方式:
|
||||
* node scripts/feishu-card-server.js [port]
|
||||
*
|
||||
* 默认端口:3000
|
||||
*/
|
||||
|
||||
const http = require('http');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const crypto = require('crypto');
|
||||
|
||||
// ============ 配置 ============
|
||||
|
||||
const PORT = process.argv[2] || 3000;
|
||||
const CARD_TEMPLATE_PATH = path.join(__dirname, '../cards/upload-card.json');
|
||||
const QINIU_CONFIG_PATH = path.join(process.env.HOME || process.env.USERPROFILE, '.openclaw/credentials/qiniu-config.json');
|
||||
|
||||
// 飞书验证令牌(在飞书开发者后台设置)
|
||||
const FEISHU_VERIFICATION_TOKEN = process.env.FEISHU_VERIFICATION_TOKEN || 'your_verification_token';
|
||||
|
||||
// ============ 工具函数 ============
|
||||
|
||||
function loadConfig(configPath = QINIU_CONFIG_PATH) {
|
||||
if (!fs.existsSync(configPath)) {
|
||||
throw new Error(`配置文件不存在:${configPath}`);
|
||||
}
|
||||
return JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
||||
}
|
||||
|
||||
function loadCardTemplate(templatePath = CARD_TEMPLATE_PATH) {
|
||||
if (!fs.existsSync(templatePath)) {
|
||||
throw new Error(`卡片模板不存在:${templatePath}`);
|
||||
}
|
||||
return JSON.parse(fs.readFileSync(templatePath, 'utf-8'));
|
||||
}
|
||||
|
||||
function renderCard(template, variables) {
|
||||
let cardJson = JSON.stringify(template);
|
||||
for (const [key, value] of Object.entries(variables)) {
|
||||
cardJson = cardJson.replace(new RegExp(`{{${key}}}`, 'g'), value);
|
||||
}
|
||||
return JSON.parse(cardJson);
|
||||
}
|
||||
|
||||
function getRegionName(regionCode) {
|
||||
const regions = {
|
||||
'z0': '华东',
|
||||
'z1': '华北',
|
||||
'z2': '华南',
|
||||
'na0': '北美',
|
||||
'as0': '东南亚'
|
||||
};
|
||||
return regions[regionCode] || '未知';
|
||||
}
|
||||
|
||||
// ============ 飞书鉴权 ============
|
||||
|
||||
/**
|
||||
* 验证飞书请求签名
|
||||
* 文档:https://open.feishu.cn/document/ukTMukTMukTM/uYjNwYjL2YDM14SM2ATN
|
||||
*/
|
||||
function verifyFeishuSignature(req, body) {
|
||||
const signature = req.headers['x-feishu-signature'];
|
||||
if (!signature) return false;
|
||||
|
||||
// 简单验证,生产环境需要严格验证
|
||||
return true;
|
||||
}
|
||||
|
||||
// ============ 卡片交互处理 ============
|
||||
|
||||
/**
|
||||
* 处理卡片按钮点击
|
||||
*/
|
||||
async function handleCardInteraction(req, res) {
|
||||
let body = '';
|
||||
req.on('data', chunk => body += chunk);
|
||||
req.on('end', async () => {
|
||||
try {
|
||||
const data = JSON.parse(body);
|
||||
|
||||
// 飞书挑战验证
|
||||
if (data.type === 'url_verification') {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ challenge: data.challenge }));
|
||||
return;
|
||||
}
|
||||
|
||||
// 处理交互事件
|
||||
if (data.type === 'interactive_card.action') {
|
||||
const action = data.action?.value?.action;
|
||||
const userId = data.user?.user_id;
|
||||
const openId = data.user?.open_id;
|
||||
const tenantKey = data.tenant_key;
|
||||
|
||||
console.log(`收到卡片交互:${action}, 用户:${userId}`);
|
||||
|
||||
let responseCard;
|
||||
|
||||
switch (action) {
|
||||
case 'upload_select':
|
||||
responseCard = await handleUploadSelect(data);
|
||||
break;
|
||||
case 'config_view':
|
||||
responseCard = await handleConfigView(data);
|
||||
break;
|
||||
case 'help':
|
||||
responseCard = await handleHelp(data);
|
||||
break;
|
||||
default:
|
||||
responseCard = createErrorResponse('未知操作');
|
||||
}
|
||||
|
||||
// 回复卡片
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({
|
||||
type: 'interactive_card.response',
|
||||
card: responseCard
|
||||
}));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// 未知类型
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ status: 'ok' }));
|
||||
|
||||
} catch (error) {
|
||||
console.error('处理交互失败:', error);
|
||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: error.message }));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理"选择文件上传"按钮
|
||||
*/
|
||||
async function handleUploadSelect(data) {
|
||||
const config = loadConfig();
|
||||
const bucketName = data.action?.value?.bucket || 'default';
|
||||
const bucketConfig = config.buckets[bucketName];
|
||||
|
||||
if (!bucketConfig) {
|
||||
return createErrorResponse(`存储桶 "${bucketName}" 不存在`);
|
||||
}
|
||||
|
||||
// 回复引导用户上传文件
|
||||
return {
|
||||
config: {
|
||||
wide_screen_mode: true
|
||||
},
|
||||
header: {
|
||||
template: "green",
|
||||
title: {
|
||||
content: "📎 选择文件",
|
||||
tag: "plain_text"
|
||||
}
|
||||
},
|
||||
elements: [
|
||||
{
|
||||
tag: "div",
|
||||
text: {
|
||||
content: `请点击下方按钮选择要上传的文件,文件将上传到 **${bucketName}** 存储桶。`,
|
||||
tag: "lark_md"
|
||||
}
|
||||
},
|
||||
{
|
||||
tag: "action",
|
||||
actions: [
|
||||
{
|
||||
tag: "button",
|
||||
text: {
|
||||
content: "📁 选择文件",
|
||||
tag: "plain_text"
|
||||
},
|
||||
type: "primary",
|
||||
url: "feishu://attachment/select" // 飞书内部协议,触发文件选择
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理"查看配置"按钮
|
||||
*/
|
||||
async function handleConfigView(data) {
|
||||
const config = loadConfig();
|
||||
|
||||
let bucketList = '';
|
||||
for (const [name, bucket] of Object.entries(config.buckets)) {
|
||||
bucketList += `**${name}**: ${bucket.bucket} (${bucket.region})\n`;
|
||||
}
|
||||
|
||||
return {
|
||||
config: {
|
||||
wide_screen_mode: true
|
||||
},
|
||||
header: {
|
||||
template: "blue",
|
||||
title: {
|
||||
content: "📋 当前配置",
|
||||
tag: "plain_text"
|
||||
}
|
||||
},
|
||||
elements: [
|
||||
{
|
||||
tag: "div",
|
||||
text: {
|
||||
content: bucketList || '暂无配置',
|
||||
tag: "lark_md"
|
||||
}
|
||||
},
|
||||
{
|
||||
tag: "hr"
|
||||
},
|
||||
{
|
||||
tag: "note",
|
||||
elements: [
|
||||
{
|
||||
tag: "plain_text",
|
||||
content: `配置文件:${QINIU_CONFIG_PATH}`
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理"帮助"按钮
|
||||
*/
|
||||
async function handleHelp(data) {
|
||||
return {
|
||||
config: {
|
||||
wide_screen_mode: true
|
||||
},
|
||||
header: {
|
||||
template: "grey",
|
||||
title: {
|
||||
content: "❓ 帮助",
|
||||
tag: "plain_text"
|
||||
}
|
||||
},
|
||||
elements: [
|
||||
{
|
||||
tag: "div",
|
||||
text: {
|
||||
content: `**七牛云上传帮助**
|
||||
|
||||
📤 **上传文件**
|
||||
- 点击"选择文件上传"按钮
|
||||
- 选择要上传的文件
|
||||
- 自动上传到七牛云
|
||||
|
||||
⚙️ **快捷命令**
|
||||
- \`/u\` - 快速上传
|
||||
- \`/qc\` - 查看配置
|
||||
- \`/qh\` - 显示帮助
|
||||
|
||||
📦 **存储桶**
|
||||
- 支持多存储桶配置
|
||||
- 上传时可指定目标桶`,
|
||||
tag: "lark_md"
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建错误响应
|
||||
*/
|
||||
function createErrorResponse(message) {
|
||||
return {
|
||||
config: {
|
||||
wide_screen_mode: true
|
||||
},
|
||||
header: {
|
||||
template: "red",
|
||||
title: {
|
||||
content: "❌ 错误",
|
||||
tag: "plain_text"
|
||||
}
|
||||
},
|
||||
elements: [
|
||||
{
|
||||
tag: "div",
|
||||
text: {
|
||||
content: message,
|
||||
tag: "lark_md"
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
// ============ 主页面(测试用) ============
|
||||
|
||||
function serveHomePage(res) {
|
||||
const html = `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>七牛云上传 - 飞书卡片服务器</title>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; max-width: 800px; margin: 50px auto; padding: 20px; }
|
||||
h1 { color: #333; }
|
||||
.status { padding: 10px; background: #e8f5e9; border-radius: 4px; margin: 20px 0; }
|
||||
.config { background: #f5f5f5; padding: 15px; border-radius: 4px; }
|
||||
code { background: #eee; padding: 2px 6px; border-radius: 3px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>🍙 七牛云上传 - 飞书卡片服务器</h1>
|
||||
|
||||
<div class="status">
|
||||
✅ 服务器运行中
|
||||
<br>端口:<code>${PORT}</code>
|
||||
</div>
|
||||
|
||||
<div class="config">
|
||||
<h3>配置信息</h3>
|
||||
<p>卡片模板:<code>${CARD_TEMPLATE_PATH}</code></p>
|
||||
<p>七牛配置:<code>${QINIU_CONFIG_PATH}</code></p>
|
||||
</div>
|
||||
|
||||
<h3>飞书开发者后台配置</h3>
|
||||
<ol>
|
||||
<li>请求网址:<code>http://你的服务器IP:${PORT}/feishu/card</code></li>
|
||||
<li>数据加密方式:选择"不加密"</li>
|
||||
<li>验证令牌:在环境变量中设置 <code>FEISHU_VERIFICATION_TOKEN</code></li>
|
||||
</ol>
|
||||
|
||||
<h3>测试</h3>
|
||||
<p>使用 curl 测试:</p>
|
||||
<pre><code>curl -X POST http://localhost:${PORT}/feishu/card \\
|
||||
-H "Content-Type: application/json" \\
|
||||
-d '{"type":"url_verification","challenge":"test123"}'</code></pre>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
||||
res.end(html);
|
||||
}
|
||||
|
||||
// ============ HTTP 服务器 ============
|
||||
|
||||
const server = http.createServer((req, res) => {
|
||||
console.log(`${new Date().toISOString()} ${req.method} ${req.url}`);
|
||||
|
||||
// CORS 头(飞书回调需要)
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
res.setHeader('Access-Control-Allow-Methods', 'POST, GET, OPTIONS');
|
||||
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, X-Feishu-Signature');
|
||||
|
||||
if (req.method === 'OPTIONS') {
|
||||
res.writeHead(200);
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
|
||||
// 主页
|
||||
if (req.url === '/' || req.url === '/health') {
|
||||
serveHomePage(res);
|
||||
return;
|
||||
}
|
||||
|
||||
// 卡片交互回调
|
||||
if (req.url === '/feishu/card' && req.method === 'POST') {
|
||||
handleCardInteraction(req, res);
|
||||
return;
|
||||
}
|
||||
|
||||
// 404
|
||||
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
||||
res.end('Not Found');
|
||||
});
|
||||
|
||||
// ============ 启动服务器 ============
|
||||
|
||||
server.listen(PORT, () => {
|
||||
console.log(`🍙 七牛云卡片服务器已启动`);
|
||||
console.log(`端口:${PORT}`);
|
||||
console.log(`主页:http://localhost:${PORT}/`);
|
||||
console.log(`回调地址:http://localhost:${PORT}/feishu/card`);
|
||||
console.log(`\n在飞书开发者后台配置请求网址为:http://你的服务器IP:${PORT}/feishu/card`);
|
||||
});
|
||||
|
||||
// 优雅退出
|
||||
process.on('SIGINT', () => {
|
||||
console.log('\n正在关闭服务器...');
|
||||
server.close(() => {
|
||||
console.log('服务器已关闭');
|
||||
process.exit(0);
|
||||
});
|
||||
});
|
||||
514
scripts/feishu-listener.js
Executable file
514
scripts/feishu-listener.js
Executable file
@@ -0,0 +1,514 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* 飞书消息监听器 - 七牛云上传自动化 v3
|
||||
*
|
||||
* 功能:
|
||||
* 1. 监听飞书消息,解析上传指令
|
||||
* 2. 支持指定上传路径:/upload /path/to/file.txt
|
||||
* 3. 支持使用原文件名:/upload --original
|
||||
* 4. 支持聊天命令修改配置:/qiniu-config set key value
|
||||
* 5. 非上传命令转发到 OpenClaw Gateway
|
||||
* 6. 下载附件并上传到七牛云
|
||||
* 7. 刷新 CDN 并回复结果
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const http = require('http');
|
||||
const https = require('https');
|
||||
const { exec } = require('child_process');
|
||||
const crypto = require('crypto');
|
||||
|
||||
// ============ 配置 ============
|
||||
|
||||
const CONFIG = {
|
||||
port: process.env.FEISHU_LISTENER_PORT || 3000,
|
||||
verifyToken: process.env.FEISHU_VERIFY_TOKEN || '',
|
||||
encryptKey: process.env.FEISHU_ENCRYPT_KEY || '',
|
||||
appSecret: process.env.FEISHU_APP_SECRET || '',
|
||||
openclawCredentials: path.join(process.env.HOME || process.env.USERPROFILE, '.openclaw/credentials'),
|
||||
scriptDir: __dirname,
|
||||
// OpenClaw Gateway 配置
|
||||
openclawGateway: {
|
||||
host: '127.0.0.1',
|
||||
port: 17733
|
||||
}
|
||||
};
|
||||
|
||||
// ============ 工具函数 ============
|
||||
|
||||
function log(...args) {
|
||||
const timestamp = new Date().toISOString();
|
||||
console.log(`[${timestamp}]`, ...args);
|
||||
}
|
||||
|
||||
function verifySignature(timestamp, nonce, signature) {
|
||||
if (!CONFIG.encryptKey) return true;
|
||||
|
||||
const arr = [CONFIG.encryptKey, timestamp, nonce];
|
||||
arr.sort();
|
||||
const str = arr.join('');
|
||||
const hash = crypto.createHash('sha1').update(str).digest('hex');
|
||||
|
||||
return hash === signature;
|
||||
}
|
||||
|
||||
// ============ 命令解析 ============
|
||||
|
||||
function isUploadCommand(text) {
|
||||
return /^\/upload(?:\s+.+)?$/i.test(text.trim());
|
||||
}
|
||||
|
||||
function isConfigCommand(text) {
|
||||
return /^\/qiniu-config\s+.+$/i.test(text.trim());
|
||||
}
|
||||
|
||||
function isHelpCommand(text) {
|
||||
return /^\/(qiniu-)?help$/i.test(text.trim());
|
||||
}
|
||||
|
||||
function parseUploadCommand(text) {
|
||||
const match = text.match(/^\/upload(?:\s+(.+))?$/i);
|
||||
if (!match) return null;
|
||||
|
||||
const args = (match[1] || '').trim().split(/\s+/).filter(Boolean);
|
||||
|
||||
let targetPath = null;
|
||||
let useOriginal = false;
|
||||
let bucket = 'default';
|
||||
|
||||
for (const arg of args) {
|
||||
if (arg === '--original') {
|
||||
useOriginal = true;
|
||||
} else if (arg.startsWith('/') || arg.includes('.')) {
|
||||
targetPath = arg;
|
||||
} else {
|
||||
bucket = arg;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
command: 'upload',
|
||||
targetPath: targetPath,
|
||||
useOriginal: useOriginal,
|
||||
bucket: bucket
|
||||
};
|
||||
}
|
||||
|
||||
function parseConfigCommand(text) {
|
||||
const match = text.match(/^\/qiniu-config\s+(.+)$/i);
|
||||
if (!match) return null;
|
||||
|
||||
const args = match[1].trim().split(/\s+/);
|
||||
const subCommand = args[0];
|
||||
|
||||
return {
|
||||
command: 'config',
|
||||
subCommand: subCommand,
|
||||
args: args.slice(1)
|
||||
};
|
||||
}
|
||||
|
||||
// ============ 飞书 API ============
|
||||
|
||||
async function getAccessToken(appId, appSecret) {
|
||||
const url = 'https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal';
|
||||
|
||||
const body = JSON.stringify({
|
||||
app_id: appId,
|
||||
app_secret: appSecret
|
||||
});
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = https.request(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}, (res) => {
|
||||
let data = '';
|
||||
res.on('data', chunk => data += chunk);
|
||||
res.on('end', () => {
|
||||
try {
|
||||
const result = JSON.parse(data);
|
||||
if (result.code === 0) {
|
||||
resolve(result.tenant_access_token);
|
||||
} else {
|
||||
reject(new Error(`获取 token 失败:${result.msg}`));
|
||||
}
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', reject);
|
||||
req.write(body);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
async function sendMessageToChat(chatId, text, msgType = 'text') {
|
||||
try {
|
||||
const appId = process.env.FEISHU_APP_ID;
|
||||
const appSecret = process.env.FEISHU_APP_SECRET;
|
||||
|
||||
if (!appId || !appSecret) {
|
||||
log('❌ 缺少飞书 App ID 或 Secret');
|
||||
return;
|
||||
}
|
||||
|
||||
const token = await getAccessToken(appId, appSecret);
|
||||
|
||||
const url = 'https://open.feishu.cn/open-apis/im/v1/messages';
|
||||
const body = JSON.stringify({
|
||||
receive_id: chatId,
|
||||
msg_type: msgType,
|
||||
content: msgType === 'text' ? JSON.stringify({ text }) : text
|
||||
});
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
const req = https.request(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
}, (res) => {
|
||||
let data = '';
|
||||
res.on('data', chunk => data += chunk);
|
||||
res.on('end', () => {
|
||||
try {
|
||||
resolve(JSON.parse(data));
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', reject);
|
||||
req.write(body);
|
||||
req.end();
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
log('发送消息失败:', e.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function downloadFeishuFile(token, fileKey, destPath) {
|
||||
const url = `https://open.feishu.cn/open-apis/im/v1/files/${fileKey}/download`;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = https.get(url, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
}, (res) => {
|
||||
if (res.statusCode !== 200) {
|
||||
reject(new Error(`下载失败:${res.statusCode}`));
|
||||
return;
|
||||
}
|
||||
|
||||
const file = fs.createWriteStream(destPath);
|
||||
res.pipe(file);
|
||||
file.on('finish', () => {
|
||||
file.close();
|
||||
resolve(destPath);
|
||||
});
|
||||
}).on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
// ============ 转发到 OpenClaw ============
|
||||
|
||||
async function forwardToOpenClaw(event) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const body = JSON.stringify(event);
|
||||
|
||||
const options = {
|
||||
hostname: CONFIG.openclawGateway.host,
|
||||
port: CONFIG.openclawGateway.port,
|
||||
path: '/feishu/event',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Content-Length': body.length
|
||||
}
|
||||
};
|
||||
|
||||
const req = http.request(options, (res) => {
|
||||
let data = '';
|
||||
res.on('data', chunk => data += chunk);
|
||||
res.on('end', () => {
|
||||
if (res.statusCode === 200) {
|
||||
resolve(JSON.parse(data));
|
||||
} else {
|
||||
reject(new Error(`OpenClaw 返回错误:${res.statusCode}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', reject);
|
||||
req.write(body);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
// ============ 消息处理 ============
|
||||
|
||||
async function handleUploadCommand(message, cmd) {
|
||||
const { chat_id, attachments } = message;
|
||||
|
||||
if (!attachments || attachments.length === 0) {
|
||||
await sendMessageToChat(chat_id,
|
||||
'❌ 请附上要上传的文件\n\n' +
|
||||
'💡 使用示例:\n' +
|
||||
'/upload /config/test/file.txt default\n' +
|
||||
'[附上文件]\n\n' +
|
||||
'或:/upload --original default\n' +
|
||||
'[附上文件] (使用原文件名)'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const attachment = attachments[0];
|
||||
const fileKey = attachment.file_key;
|
||||
const originalFileName = attachment.file_name;
|
||||
|
||||
log(`处理附件:${originalFileName} (${fileKey})`);
|
||||
|
||||
let targetKey;
|
||||
if (cmd.useOriginal) {
|
||||
targetKey = originalFileName;
|
||||
} else if (cmd.targetPath) {
|
||||
targetKey = cmd.targetPath.startsWith('/') ? cmd.targetPath.substring(1) : cmd.targetPath;
|
||||
} else {
|
||||
targetKey = originalFileName;
|
||||
}
|
||||
|
||||
const tempDir = path.join(CONFIG.openclawCredentials, 'temp');
|
||||
if (!fs.existsSync(tempDir)) {
|
||||
fs.mkdirSync(tempDir, { recursive: true });
|
||||
}
|
||||
|
||||
const tempFile = path.join(tempDir, `upload_${Date.now()}_${originalFileName}`);
|
||||
|
||||
try {
|
||||
const appId = process.env.FEISHU_APP_ID;
|
||||
const appSecret = process.env.FEISHU_APP_SECRET;
|
||||
|
||||
if (!appId || !appSecret) {
|
||||
throw new Error('缺少飞书 App ID 或 App Secret 配置');
|
||||
}
|
||||
|
||||
const token = await getAccessToken(appId, appSecret);
|
||||
|
||||
log('下载文件中...');
|
||||
await sendMessageToChat(chat_id, `📥 正在下载文件:${originalFileName}`);
|
||||
await downloadFeishuFile(token, fileKey, tempFile);
|
||||
|
||||
log('上传到七牛云...');
|
||||
await sendMessageToChat(chat_id, `📤 正在上传到七牛云:${targetKey}\n存储桶:${cmd.bucket}`);
|
||||
|
||||
const uploadScript = path.join(CONFIG.scriptDir, 'upload-to-qiniu.js');
|
||||
const uploadCmd = `node "${uploadScript}" upload --file "${tempFile}" --key "${targetKey}" --bucket "${cmd.bucket}"`;
|
||||
|
||||
const { stdout, stderr } = await new Promise((resolve, reject) => {
|
||||
exec(uploadCmd, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
reject(new Error(`上传失败:${stderr || error.message}`));
|
||||
return;
|
||||
}
|
||||
resolve({ stdout, stderr });
|
||||
});
|
||||
});
|
||||
|
||||
log(stdout);
|
||||
|
||||
const urlMatch = stdout.match(/🔗 URL: (.+)/);
|
||||
const fileUrl = urlMatch ? urlMatch[1] : 'N/A';
|
||||
|
||||
await sendMessageToChat(chat_id,
|
||||
`✅ 上传成功!\n\n` +
|
||||
`📦 文件:${targetKey}\n` +
|
||||
`🔗 链接:${fileUrl}\n` +
|
||||
`💾 原文件:${originalFileName}\n` +
|
||||
`🪣 存储桶:${cmd.bucket}`
|
||||
);
|
||||
|
||||
} catch (error) {
|
||||
log('处理失败:', error.message);
|
||||
await sendMessageToChat(chat_id, `❌ 上传失败:${error.message}`);
|
||||
} finally {
|
||||
if (fs.existsSync(tempFile)) {
|
||||
fs.unlinkSync(tempFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function handleConfigCommand(message, cmd) {
|
||||
const { chat_id } = message;
|
||||
|
||||
const uploadScript = path.join(CONFIG.scriptDir, 'upload-to-qiniu.js');
|
||||
const configCmd = `node "${uploadScript}" config ${cmd.subCommand} ${cmd.args.join(' ')}`;
|
||||
|
||||
try {
|
||||
const { stdout, stderr } = await new Promise((resolve, reject) => {
|
||||
exec(configCmd, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
reject(new Error(stderr || error.message));
|
||||
return;
|
||||
}
|
||||
resolve({ stdout, stderr });
|
||||
});
|
||||
});
|
||||
|
||||
await sendMessageToChat(chat_id, '```\n' + stdout + '\n```');
|
||||
|
||||
} catch (error) {
|
||||
await sendMessageToChat(chat_id, `❌ 配置命令执行失败:${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function showHelp(message) {
|
||||
const helpText = `
|
||||
🍙 七牛云上传 - 使用帮助
|
||||
|
||||
📤 上传文件:
|
||||
/upload [目标路径] [存储桶名]
|
||||
/upload --original [存储桶名]
|
||||
|
||||
示例:
|
||||
/upload /config/test/file.txt default
|
||||
/upload --original default
|
||||
|
||||
⚙️ 配置管理:
|
||||
/qiniu-config list # 查看配置
|
||||
/qiniu-config set <key> <value> # 修改配置
|
||||
/qiniu-config set-bucket <name> <json> # 添加存储桶
|
||||
/qiniu-config reset # 重置配置
|
||||
|
||||
示例:
|
||||
/qiniu-config set default.accessKey YOUR_KEY
|
||||
/qiniu-config set default.domain https://cdn.example.com
|
||||
`;
|
||||
|
||||
await sendMessageToChat(message.chat_id, helpText);
|
||||
}
|
||||
|
||||
async function processMessage(event) {
|
||||
const { message } = event;
|
||||
|
||||
log('收到消息:', message.message_id);
|
||||
|
||||
const content = JSON.parse(message.content);
|
||||
const text = content.text || '';
|
||||
|
||||
// 检查是否是七牛云命令
|
||||
if (isUploadCommand(text)) {
|
||||
const cmd = parseUploadCommand(text);
|
||||
log('上传命令:', cmd);
|
||||
await handleUploadCommand(message, cmd);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isConfigCommand(text)) {
|
||||
const cmd = parseConfigCommand(text);
|
||||
log('配置命令:', cmd.subCommand);
|
||||
await handleConfigCommand(message, cmd);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isHelpCommand(text)) {
|
||||
await showHelp(message);
|
||||
return;
|
||||
}
|
||||
|
||||
// 非七牛云命令,转发到 OpenClaw
|
||||
log('转发到 OpenClaw:', text.substring(0, 50));
|
||||
|
||||
try {
|
||||
await forwardToOpenClaw(event);
|
||||
log('✅ 已转发到 OpenClaw');
|
||||
} catch (error) {
|
||||
log('❌ 转发失败:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// ============ HTTP 服务器 ============
|
||||
|
||||
function startServer() {
|
||||
const server = http.createServer(async (req, res) => {
|
||||
if (req.method !== 'POST') {
|
||||
res.writeHead(405);
|
||||
res.end('Method Not Allowed');
|
||||
return;
|
||||
}
|
||||
|
||||
let body = '';
|
||||
req.on('data', chunk => body += chunk);
|
||||
req.on('end', async () => {
|
||||
try {
|
||||
const event = JSON.parse(body);
|
||||
|
||||
// 验证签名
|
||||
const timestamp = req.headers['x-feishu-request-timestamp'];
|
||||
const nonce = req.headers['x-feishu-request-nonce'];
|
||||
const signature = req.headers['x-feishu-request-signature'];
|
||||
|
||||
if (!verifySignature(timestamp, nonce, signature)) {
|
||||
res.writeHead(401);
|
||||
res.end('Invalid signature');
|
||||
return;
|
||||
}
|
||||
|
||||
// 处理 URL 验证
|
||||
if (event.type === 'url_verification') {
|
||||
log('✅ 收到 URL 验证请求');
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ challenge: event.challenge }));
|
||||
return;
|
||||
}
|
||||
|
||||
// 处理消息事件
|
||||
if (event.type === 'im.message.receive_v1') {
|
||||
await processMessage(event);
|
||||
}
|
||||
|
||||
res.writeHead(200);
|
||||
res.end('OK');
|
||||
|
||||
} catch (e) {
|
||||
log('处理请求失败:', e.message);
|
||||
res.writeHead(500);
|
||||
res.end('Internal Server Error');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
server.listen(CONFIG.port, () => {
|
||||
log(`🚀 飞书监听器启动,端口:${CONFIG.port}`);
|
||||
log(`📍 请求地址:http://47.83.185.237:${CONFIG.port}`);
|
||||
log(`🔄 非七牛云命令将转发到 OpenClaw Gateway (${CONFIG.openclawGateway.host}:${CONFIG.openclawGateway.port})`);
|
||||
});
|
||||
}
|
||||
|
||||
// ============ 主函数 ============
|
||||
|
||||
function main() {
|
||||
log('🍙 七牛云上传 - 飞书监听器 v3 (带 OpenClaw 转发)');
|
||||
log('配置文件:~/.openclaw/credentials/qiniu-config.json');
|
||||
log('');
|
||||
|
||||
const configPath = path.join(CONFIG.openclawCredentials, 'qiniu-config.json');
|
||||
if (!fs.existsSync(configPath)) {
|
||||
log('⚠️ 警告:七牛云配置文件不存在');
|
||||
log(' 运行:node upload-to-qiniu.js config init');
|
||||
}
|
||||
|
||||
startServer();
|
||||
}
|
||||
|
||||
main();
|
||||
477
scripts/feishu-websocket-listener.js
Executable file
477
scripts/feishu-websocket-listener.js
Executable file
@@ -0,0 +1,477 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* 飞书长连接监听器 - 七牛云上传自动化
|
||||
*
|
||||
* 使用飞书 WebSocket 长连接接收事件
|
||||
*
|
||||
* 使用方式:
|
||||
* node scripts/feishu-websocket-listener.js
|
||||
*/
|
||||
|
||||
const WebSocket = require('ws');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const crypto = require('crypto');
|
||||
const https = require('https');
|
||||
const { exec } = require('child_process');
|
||||
|
||||
// ============ 配置 ============
|
||||
|
||||
const CONFIG = {
|
||||
appId: process.env.FEISHU_APP_ID || 'cli_a92ce47b02381bcc',
|
||||
appSecret: process.env.FEISHU_APP_SECRET || 'WpCWhqOPKv3F5Lhn11DqubrssJnAodot',
|
||||
encryptKey: process.env.FEISHU_ENCRYPT_KEY || '',
|
||||
openclawCredentials: path.join(process.env.HOME || process.env.USERPROFILE, '.openclaw/credentials'),
|
||||
scriptDir: __dirname
|
||||
};
|
||||
|
||||
// ============ 工具函数 ============
|
||||
|
||||
function log(...args) {
|
||||
const timestamp = new Date().toISOString();
|
||||
console.log(`[${timestamp}]`, ...args);
|
||||
}
|
||||
|
||||
function verifySignature(timestamp, nonce, signature) {
|
||||
if (!CONFIG.encryptKey) return true;
|
||||
|
||||
const arr = [CONFIG.encryptKey, timestamp, nonce];
|
||||
arr.sort();
|
||||
const str = arr.join('');
|
||||
const hash = crypto.createHash('sha1').update(str).digest('hex');
|
||||
|
||||
return hash === signature;
|
||||
}
|
||||
|
||||
// ============ 命令解析 ============
|
||||
|
||||
function parseUploadCommand(text) {
|
||||
const match = text.match(/^\/upload(?:\s+(.+))?$/i);
|
||||
if (!match) return null;
|
||||
|
||||
const args = (match[1] || '').trim().split(/\s+/).filter(Boolean);
|
||||
|
||||
let targetPath = null;
|
||||
let useOriginal = false;
|
||||
let bucket = 'default';
|
||||
|
||||
for (const arg of args) {
|
||||
if (arg === '--original') {
|
||||
useOriginal = true;
|
||||
} else if (arg.startsWith('/') || arg.includes('.')) {
|
||||
targetPath = arg;
|
||||
} else {
|
||||
bucket = arg;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
command: 'upload',
|
||||
targetPath: targetPath,
|
||||
useOriginal: useOriginal,
|
||||
bucket: bucket
|
||||
};
|
||||
}
|
||||
|
||||
function parseConfigCommand(text) {
|
||||
const match = text.match(/^\/qiniu-config\s+(.+)$/i);
|
||||
if (!match) return null;
|
||||
|
||||
const args = match[1].trim().split(/\s+/);
|
||||
const subCommand = args[0];
|
||||
|
||||
return {
|
||||
command: 'config',
|
||||
subCommand: subCommand,
|
||||
args: args.slice(1)
|
||||
};
|
||||
}
|
||||
|
||||
// ============ 飞书 API ============
|
||||
|
||||
async function getAccessToken(appId, appSecret) {
|
||||
const url = 'https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal';
|
||||
|
||||
const body = JSON.stringify({
|
||||
app_id: appId,
|
||||
app_secret: appSecret
|
||||
});
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = https.request(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
}, (res) => {
|
||||
let data = '';
|
||||
res.on('data', chunk => data += chunk);
|
||||
res.on('end', () => {
|
||||
try {
|
||||
const result = JSON.parse(data);
|
||||
if (result.code === 0) {
|
||||
resolve(result.tenant_access_token);
|
||||
} else {
|
||||
reject(new Error(`获取 token 失败:${result.msg}`));
|
||||
}
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', reject);
|
||||
req.write(body);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
async function sendMessageToChat(chatId, text) {
|
||||
try {
|
||||
const token = await getAccessToken(CONFIG.appId, CONFIG.appSecret);
|
||||
|
||||
const url = 'https://open.feishu.cn/open-apis/im/v1/messages';
|
||||
const body = JSON.stringify({
|
||||
receive_id: chatId,
|
||||
msg_type: 'text',
|
||||
content: JSON.stringify({ text })
|
||||
});
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
const req = https.request(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
}, (res) => {
|
||||
let data = '';
|
||||
res.on('data', chunk => data += chunk);
|
||||
res.on('end', () => {
|
||||
try {
|
||||
resolve(JSON.parse(data));
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', reject);
|
||||
req.write(body);
|
||||
req.end();
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
log('发送消息失败:', e.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function downloadFeishuFile(token, fileKey, destPath) {
|
||||
const url = `https://open.feishu.cn/open-apis/im/v1/files/${fileKey}/download`;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = https.get(url, {
|
||||
headers: { 'Authorization': `Bearer ${token}` }
|
||||
}, (res) => {
|
||||
if (res.statusCode !== 200) {
|
||||
reject(new Error(`下载失败:${res.statusCode}`));
|
||||
return;
|
||||
}
|
||||
|
||||
const file = fs.createWriteStream(destPath);
|
||||
res.pipe(file);
|
||||
file.on('finish', () => {
|
||||
file.close();
|
||||
resolve(destPath);
|
||||
});
|
||||
}).on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
// ============ 消息处理 ============
|
||||
|
||||
async function handleUploadCommand(message, cmd) {
|
||||
const { chat_id, attachments } = message;
|
||||
|
||||
if (!attachments || attachments.length === 0) {
|
||||
await sendMessageToChat(chat_id,
|
||||
'❌ 请附上要上传的文件\n\n' +
|
||||
'💡 使用示例:\n' +
|
||||
'/upload /config/test/file.txt default\n' +
|
||||
'[附上文件]\n\n' +
|
||||
'或:/upload --original default\n' +
|
||||
'[附上文件] (使用原文件名)'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const attachment = attachments[0];
|
||||
const fileKey = attachment.file_key;
|
||||
const originalFileName = attachment.file_name;
|
||||
|
||||
log(`处理附件:${originalFileName} (${fileKey})`);
|
||||
|
||||
let targetKey;
|
||||
if (cmd.useOriginal) {
|
||||
targetKey = originalFileName;
|
||||
} else if (cmd.targetPath) {
|
||||
targetKey = cmd.targetPath.startsWith('/') ? cmd.targetPath.substring(1) : cmd.targetPath;
|
||||
} else {
|
||||
targetKey = originalFileName;
|
||||
}
|
||||
|
||||
const tempDir = path.join(CONFIG.openclawCredentials, 'temp');
|
||||
if (!fs.existsSync(tempDir)) {
|
||||
fs.mkdirSync(tempDir, { recursive: true });
|
||||
}
|
||||
|
||||
const tempFile = path.join(tempDir, `upload_${Date.now()}_${originalFileName}`);
|
||||
|
||||
try {
|
||||
const token = await getAccessToken(CONFIG.appId, CONFIG.appSecret);
|
||||
|
||||
log('下载文件中...');
|
||||
await sendMessageToChat(chat_id, `📥 正在下载文件:${originalFileName}`);
|
||||
await downloadFeishuFile(token, fileKey, tempFile);
|
||||
|
||||
log('上传到七牛云...');
|
||||
await sendMessageToChat(chat_id, `📤 正在上传到七牛云:${targetKey}\n存储桶:${cmd.bucket}`);
|
||||
|
||||
const uploadScript = path.join(CONFIG.scriptDir, 'upload-to-qiniu.js');
|
||||
const uploadCmd = `node "${uploadScript}" upload --file "${tempFile}" --key "${targetKey}" --bucket "${cmd.bucket}"`;
|
||||
|
||||
const { stdout, stderr } = await new Promise((resolve, reject) => {
|
||||
exec(uploadCmd, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
reject(new Error(`上传失败:${stderr || error.message}`));
|
||||
return;
|
||||
}
|
||||
resolve({ stdout, stderr });
|
||||
});
|
||||
});
|
||||
|
||||
log(stdout);
|
||||
|
||||
const urlMatch = stdout.match(/🔗 URL: (.+)/);
|
||||
const fileUrl = urlMatch ? urlMatch[1] : 'N/A';
|
||||
|
||||
await sendMessageToChat(chat_id,
|
||||
`✅ 上传成功!\n\n` +
|
||||
`📦 文件:${targetKey}\n` +
|
||||
`🔗 链接:${fileUrl}\n` +
|
||||
`💾 原文件:${originalFileName}\n` +
|
||||
`🪣 存储桶:${cmd.bucket}`
|
||||
);
|
||||
|
||||
} catch (error) {
|
||||
log('处理失败:', error.message);
|
||||
await sendMessageToChat(chat_id, `❌ 上传失败:${error.message}`);
|
||||
} finally {
|
||||
if (fs.existsSync(tempFile)) {
|
||||
fs.unlinkSync(tempFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function handleConfigCommand(message, cmd) {
|
||||
const { chat_id } = message;
|
||||
|
||||
const uploadScript = path.join(CONFIG.scriptDir, 'upload-to-qiniu.js');
|
||||
const configCmd = `node "${uploadScript}" config ${cmd.subCommand} ${cmd.args.join(' ')}`;
|
||||
|
||||
try {
|
||||
const { stdout, stderr } = await new Promise((resolve, reject) => {
|
||||
exec(configCmd, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
reject(new Error(stderr || error.message));
|
||||
return;
|
||||
}
|
||||
resolve({ stdout, stderr });
|
||||
});
|
||||
});
|
||||
|
||||
await sendMessageToChat(chat_id, '```\n' + stdout + '\n```');
|
||||
|
||||
} catch (error) {
|
||||
await sendMessageToChat(chat_id, `❌ 配置命令执行失败:${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function showHelp(message) {
|
||||
const helpText = `
|
||||
🍙 七牛云上传 - 使用帮助
|
||||
|
||||
📤 上传文件:
|
||||
/upload [目标路径] [存储桶名]
|
||||
/upload --original [存储桶名]
|
||||
|
||||
示例:
|
||||
/upload /config/test/file.txt default
|
||||
/upload --original default
|
||||
|
||||
⚙️ 配置管理:
|
||||
/qiniu-config list # 查看配置
|
||||
/qiniu-config set <key> <value> # 修改配置
|
||||
/qiniu-config set-bucket <name> <json> # 添加存储桶
|
||||
/qiniu-config reset # 重置配置
|
||||
|
||||
示例:
|
||||
/qiniu-config set default.accessKey YOUR_KEY
|
||||
/qiniu-config set default.domain https://cdn.example.com
|
||||
`;
|
||||
|
||||
await sendMessageToChat(message.chat_id, helpText);
|
||||
}
|
||||
|
||||
async function processMessage(message) {
|
||||
log('收到消息:', message.message_id);
|
||||
|
||||
const content = JSON.parse(message.content);
|
||||
const text = content.text || '';
|
||||
|
||||
const configCmd = parseConfigCommand(text.trim());
|
||||
if (configCmd) {
|
||||
log('配置命令:', configCmd.subCommand);
|
||||
await handleConfigCommand(message, configCmd);
|
||||
return;
|
||||
}
|
||||
|
||||
if (text.trim() === '/qiniu-help' || text.trim() === '/help') {
|
||||
await showHelp(message);
|
||||
return;
|
||||
}
|
||||
|
||||
const uploadCmd = parseUploadCommand(text.trim());
|
||||
if (uploadCmd) {
|
||||
log('上传命令:', uploadCmd);
|
||||
await handleUploadCommand(message, uploadCmd);
|
||||
return;
|
||||
}
|
||||
|
||||
log('不是已知命令,跳过');
|
||||
}
|
||||
|
||||
// ============ WebSocket 长连接 ============
|
||||
|
||||
let ws = null;
|
||||
let reconnectAttempts = 0;
|
||||
const MAX_RECONNECT_ATTEMPTS = 10;
|
||||
const RECONNECT_DELAY = 5000;
|
||||
|
||||
async function getWebSocketUrl() {
|
||||
// 获取 WebSocket 连接地址
|
||||
const token = await getAccessToken(CONFIG.appId, CONFIG.appSecret);
|
||||
|
||||
const url = 'https://open.feishu.cn/open-apis/connect/v1/ws';
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = https.request(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}, (res) => {
|
||||
let data = '';
|
||||
res.on('data', chunk => data += chunk);
|
||||
res.on('end', () => {
|
||||
try {
|
||||
const result = JSON.parse(data);
|
||||
if (result.code === 0) {
|
||||
resolve(result.data.ws_url);
|
||||
} else {
|
||||
reject(new Error(`获取 WebSocket URL 失败:${result.msg}`));
|
||||
}
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', reject);
|
||||
req.write(JSON.stringify({}));
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
function connectWebSocket() {
|
||||
getWebSocketUrl().then((wsUrl) => {
|
||||
log('🔌 连接 WebSocket:', wsUrl);
|
||||
|
||||
ws = new WebSocket(wsUrl);
|
||||
|
||||
ws.on('open', () => {
|
||||
log('✅ WebSocket 已连接');
|
||||
reconnectAttempts = 0;
|
||||
});
|
||||
|
||||
ws.on('message', async (data) => {
|
||||
try {
|
||||
const event = JSON.parse(data.toString());
|
||||
|
||||
// 处理不同类型的事件
|
||||
if (event.type === 'im.message.receive_v1') {
|
||||
await processMessage(event.event.message);
|
||||
} else if (event.type === 'verification') {
|
||||
// 验证挑战
|
||||
log('收到验证挑战');
|
||||
ws.send(JSON.stringify({ challenge: event.challenge }));
|
||||
} else {
|
||||
log('未知事件类型:', event.type);
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
log('处理消息失败:', e.message);
|
||||
}
|
||||
});
|
||||
|
||||
ws.on('close', () => {
|
||||
log('⚠️ WebSocket 已断开');
|
||||
scheduleReconnect();
|
||||
});
|
||||
|
||||
ws.on('error', (error) => {
|
||||
log('❌ WebSocket 错误:', error.message);
|
||||
});
|
||||
|
||||
}).catch((error) => {
|
||||
log('❌ 获取 WebSocket URL 失败:', error.message);
|
||||
scheduleReconnect();
|
||||
});
|
||||
}
|
||||
|
||||
function scheduleReconnect() {
|
||||
if (reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
|
||||
log('❌ 重连次数已达上限,停止重连');
|
||||
return;
|
||||
}
|
||||
|
||||
reconnectAttempts++;
|
||||
const delay = RECONNECT_DELAY * reconnectAttempts;
|
||||
|
||||
log(`🔄 ${delay/1000}秒后尝试重连 (${reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS})`);
|
||||
|
||||
setTimeout(() => {
|
||||
connectWebSocket();
|
||||
}, delay);
|
||||
}
|
||||
|
||||
// ============ 主函数 ============
|
||||
|
||||
function main() {
|
||||
log('🍙 七牛云上传 - 飞书长连接监听器');
|
||||
log('配置文件:~/.openclaw/credentials/qiniu-config.json');
|
||||
log('应用 ID:', CONFIG.appId);
|
||||
log('');
|
||||
|
||||
// 检查配置
|
||||
const configPath = path.join(CONFIG.openclawCredentials, 'qiniu-config.json');
|
||||
if (!fs.existsSync(configPath)) {
|
||||
log('⚠️ 警告:七牛云配置文件不存在');
|
||||
log(' 运行:node upload-to-qiniu.js config init');
|
||||
}
|
||||
|
||||
// 连接 WebSocket
|
||||
connectWebSocket();
|
||||
}
|
||||
|
||||
main();
|
||||
82
scripts/openclaw-bridge.js
Normal file
82
scripts/openclaw-bridge.js
Normal file
@@ -0,0 +1,82 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* OpenClaw 桥接脚本
|
||||
*
|
||||
* 功能:
|
||||
* 1. 从 OpenClaw 接收消息
|
||||
* 2. 调用上传脚本
|
||||
* 3. 回复结果
|
||||
*
|
||||
* 使用方式(由 OpenClaw 调用):
|
||||
* node scripts/openclaw-bridge.js <command> [args...]
|
||||
*/
|
||||
|
||||
const { exec } = require('child_process');
|
||||
const path = require('path');
|
||||
|
||||
const UPLOAD_SCRIPT = path.join(__dirname, 'upload-to-qiniu.js');
|
||||
|
||||
// 从命令行获取参数
|
||||
const args = process.argv.slice(2);
|
||||
const command = args[0];
|
||||
|
||||
if (!command) {
|
||||
console.error('用法:node openclaw-bridge.js <command> [args...]');
|
||||
console.error('命令:upload, config, help');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// 执行对应的命令
|
||||
switch (command) {
|
||||
case 'upload':
|
||||
executeUpload(args.slice(1));
|
||||
break;
|
||||
case 'config':
|
||||
executeConfig(args.slice(1));
|
||||
break;
|
||||
case 'help':
|
||||
executeHelp();
|
||||
break;
|
||||
default:
|
||||
console.error(`未知命令:${command}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
function executeUpload(uploadArgs) {
|
||||
const cmd = `node ${UPLOAD_SCRIPT} upload ${uploadArgs.join(' ')}`;
|
||||
console.log(`执行:${cmd}`);
|
||||
|
||||
exec(cmd, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
console.error(`上传失败:${error.message}`);
|
||||
console.error(stderr);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(stdout);
|
||||
});
|
||||
}
|
||||
|
||||
function executeConfig(configArgs) {
|
||||
const cmd = `node ${UPLOAD_SCRIPT} config ${configArgs.join(' ')}`;
|
||||
console.log(`执行:${cmd}`);
|
||||
|
||||
exec(cmd, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
console.error(`配置操作失败:${error.message}`);
|
||||
console.error(stderr);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(stdout);
|
||||
});
|
||||
}
|
||||
|
||||
function executeHelp() {
|
||||
const cmd = `node ${UPLOAD_SCRIPT} --help`;
|
||||
exec(cmd, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
// 忽略帮助命令的错误
|
||||
}
|
||||
console.log(stdout);
|
||||
});
|
||||
}
|
||||
84
scripts/setup.sh
Executable file
84
scripts/setup.sh
Executable file
@@ -0,0 +1,84 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 🍙 七牛云上传技能 - 快速配置脚本
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
cd "$SCRIPT_DIR"
|
||||
|
||||
echo "🍙 七牛云上传技能 - 快速配置"
|
||||
echo "================================"
|
||||
echo ""
|
||||
|
||||
# 1. 检查七牛云配置
|
||||
QINIU_CONFIG="$HOME/.openclaw/credentials/qiniu-config.json"
|
||||
|
||||
if [ ! -f "$QINIU_CONFIG" ]; then
|
||||
echo "📝 配置七牛云凭证..."
|
||||
echo ""
|
||||
echo "请复制配置模板并编辑:"
|
||||
echo " cp qiniu-config.example.json ~/.openclaw/credentials/qiniu-config.json"
|
||||
echo ""
|
||||
read -p "按回车继续..."
|
||||
|
||||
if [ ! -f "$QINIU_CONFIG" ]; then
|
||||
cp qiniu-config.example.json "$QINIU_CONFIG"
|
||||
echo "✅ 已复制配置模板到:$QINIU_CONFIG"
|
||||
echo ""
|
||||
echo "请编辑文件并填写你的七牛云信息:"
|
||||
echo " - AccessKey"
|
||||
echo " - SecretKey"
|
||||
echo " - Bucket 名称"
|
||||
echo " - 区域代码"
|
||||
echo " - CDN 域名"
|
||||
echo ""
|
||||
read -p "编辑完成后按回车继续..."
|
||||
fi
|
||||
else
|
||||
echo "✅ 七牛云配置已存在"
|
||||
fi
|
||||
|
||||
# 2. 配置飞书环境变量
|
||||
if [ ! -f ".env" ]; then
|
||||
echo ""
|
||||
echo "📝 配置飞书环境变量..."
|
||||
cp .env.example .env
|
||||
echo "✅ 已创建 .env 文件"
|
||||
echo ""
|
||||
echo "请编辑 .env 文件并填写:"
|
||||
echo " - FEISHU_VERIFY_TOKEN(自定义)"
|
||||
echo " - FEISHU_ENCRYPT_KEY(从飞书开放平台获取)"
|
||||
echo ""
|
||||
read -p "按回车继续..."
|
||||
else
|
||||
echo "✅ 飞书环境变量已配置"
|
||||
fi
|
||||
|
||||
# 3. 检查 Node.js
|
||||
if ! command -v node &> /dev/null; then
|
||||
echo "❌ 未找到 Node.js,请先安装 Node.js"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "✅ 配置完成!"
|
||||
echo ""
|
||||
echo "================================"
|
||||
echo "📋 下一步:"
|
||||
echo ""
|
||||
echo "1️⃣ 配置飞书开放平台事件订阅"
|
||||
echo " 查看详细说明:cat FEISHU_SETUP.md"
|
||||
echo ""
|
||||
echo "2️⃣ 启动 URL 验证服务(首次配置)"
|
||||
echo " ./scripts/verify-url.js"
|
||||
echo ""
|
||||
echo "3️⃣ 验证通过后,启动正式监听器"
|
||||
echo " ./scripts/start-listener.sh"
|
||||
echo ""
|
||||
echo "4️⃣ 在飞书中测试"
|
||||
echo " 发送:/upload 文件名.pdf"
|
||||
echo " 附上文件"
|
||||
echo ""
|
||||
echo "================================"
|
||||
echo ""
|
||||
36
scripts/start-listener.sh
Executable file
36
scripts/start-listener.sh
Executable file
@@ -0,0 +1,36 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 🍙 七牛云上传 - 飞书监听器启动脚本
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
cd "$SCRIPT_DIR"
|
||||
|
||||
# 检查配置文件
|
||||
if [ ! -f ".env" ]; then
|
||||
echo "❌ 配置文件 .env 不存在"
|
||||
echo ""
|
||||
echo "请先创建配置文件:"
|
||||
echo " cp .env.example .env"
|
||||
echo " # 然后编辑 .env 填写你的配置"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 加载环境变量
|
||||
set -a
|
||||
source .env
|
||||
set +a
|
||||
|
||||
# 检查必要的环境变量
|
||||
if [ -z "$FEISHU_APP_ID" ] || [ -z "$FEISHU_APP_SECRET" ]; then
|
||||
echo "❌ 缺少必要的环境变量"
|
||||
echo "请检查 .env 文件中的 FEISHU_APP_ID 和 FEISHU_APP_SECRET"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 启动监听器
|
||||
echo "🍙 启动飞书监听器..."
|
||||
echo "📍 工作目录:$SCRIPT_DIR"
|
||||
echo "🔌 端口:${FEISHU_LISTENER_PORT:-3000}"
|
||||
echo ""
|
||||
|
||||
node scripts/feishu-listener.js
|
||||
166
scripts/update-bucket-setting.js
Normal file
166
scripts/update-bucket-setting.js
Normal file
@@ -0,0 +1,166 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* 七牛云存储桶设置更新脚本
|
||||
*
|
||||
* 用途:更新存储桶的防覆盖等设置
|
||||
*
|
||||
* 用法:
|
||||
* node update-bucket-setting.js <bucket-name> <setting> <value>
|
||||
*
|
||||
* 示例:
|
||||
* node update-bucket-setting.js mybucket noOverwrite 0 # 允许覆盖
|
||||
* node update-bucket-setting.js mybucket noOverwrite 1 # 禁止覆盖
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const crypto = require('crypto');
|
||||
const https = require('https');
|
||||
const http = require('http');
|
||||
|
||||
const DEFAULT_CONFIG_PATH = path.join(process.env.HOME || process.env.USERPROFILE, '.openclaw/credentials/qiniu-config.json');
|
||||
|
||||
function loadConfig() {
|
||||
if (!fs.existsSync(DEFAULT_CONFIG_PATH)) {
|
||||
throw new Error(`配置文件不存在:${DEFAULT_CONFIG_PATH}`);
|
||||
}
|
||||
return JSON.parse(fs.readFileSync(DEFAULT_CONFIG_PATH, 'utf-8'));
|
||||
}
|
||||
|
||||
function hmacSha1(data, secret) {
|
||||
return crypto.createHmac('sha1', secret).update(data).digest();
|
||||
}
|
||||
|
||||
function urlSafeBase64(data) {
|
||||
return Buffer.from(data).toString('base64')
|
||||
.replace(/\+/g, '-')
|
||||
.replace(/\//g, '_');
|
||||
}
|
||||
|
||||
function generateAccessToken(accessKey, secretKey, method, path, body = '') {
|
||||
const host = 'api.qiniu.com';
|
||||
const contentType = 'application/json';
|
||||
|
||||
const signData = `${method} ${path}\nHost: ${host}\nContent-Type: ${contentType}\n\n${body}`;
|
||||
const signature = hmacSha1(signData, secretKey);
|
||||
const encodedSign = urlSafeBase64(signature);
|
||||
|
||||
return `Qiniu ${accessKey}:${encodedSign}`;
|
||||
}
|
||||
|
||||
function httpRequest(url, options, body = null) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const protocol = url.startsWith('https') ? https : http;
|
||||
|
||||
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({ status: res.statusCode, data: json });
|
||||
} catch (e) {
|
||||
resolve({ status: res.statusCode, data: data });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', reject);
|
||||
|
||||
if (body) {
|
||||
req.write(body);
|
||||
}
|
||||
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
async function updateBucketSetting(bucketName, setting, value) {
|
||||
const config = loadConfig();
|
||||
const bucketConfig = config.buckets['default'];
|
||||
|
||||
if (!bucketConfig) {
|
||||
throw new Error(`默认存储桶配置不存在`);
|
||||
}
|
||||
|
||||
const { accessKey, secretKey, bucket } = bucketConfig;
|
||||
|
||||
console.log('🔄 更新存储桶设置...\n');
|
||||
console.log(`存储桶:${bucket}`);
|
||||
console.log(`设置项:${setting}`);
|
||||
console.log(`值:${value}`);
|
||||
console.log('');
|
||||
|
||||
// 七牛云存储桶设置 API
|
||||
// 文档:https://developer.qiniu.com/kodo/api/1313/bucket-settings-update
|
||||
const updateUrl = `https://api.qiniu.com/buckets/${bucket}/settings`;
|
||||
|
||||
const body = JSON.stringify({
|
||||
[setting]: value === '1' || value === 'true' ? 1 : 0
|
||||
});
|
||||
|
||||
const accessToken = generateAccessToken(accessKey, secretKey, 'PUT', `/buckets/${bucket}/settings`, body);
|
||||
|
||||
const options = {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Host': 'api.qiniu.com',
|
||||
'Content-Type': 'application/json',
|
||||
'Content-Length': body.length,
|
||||
'Authorization': accessToken
|
||||
}
|
||||
};
|
||||
|
||||
console.log('📤 发送更新请求...');
|
||||
const result = await httpRequest(updateUrl, options, body);
|
||||
|
||||
if (result.status !== 200) {
|
||||
console.error('❌ 更新失败:', result.data);
|
||||
console.log('\n可能原因:');
|
||||
console.log('1. AccessKey/SecretKey 权限不足,需要存储桶管理权限');
|
||||
console.log('2. 存储桶名称不正确');
|
||||
console.log('3. 设置项不支持通过 API 修改');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('✅ 设置已更新成功!\n');
|
||||
console.log('提示:设置可能需要几分钟生效,请稍后重试上传。');
|
||||
}
|
||||
|
||||
function printUsage() {
|
||||
console.log(`
|
||||
用法:node update-bucket-setting.js <bucket-name> <setting> <value>
|
||||
|
||||
设置项:
|
||||
noOverwrite - 防覆盖设置 (0=允许覆盖,1=禁止覆盖)
|
||||
|
||||
示例:
|
||||
node update-bucket-setting.js mybucket noOverwrite 0 # 允许覆盖
|
||||
node update-bucket-setting.js mybucket noOverwrite 1 # 禁止覆盖
|
||||
`);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
if (args.length < 3) {
|
||||
printUsage();
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const [bucketName, setting, value] = args;
|
||||
|
||||
try {
|
||||
await updateBucketSetting(bucketName, setting, value);
|
||||
} catch (error) {
|
||||
console.error('❌ 错误:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
main();
|
||||
}
|
||||
|
||||
module.exports = { updateBucketSetting };
|
||||
574
scripts/upload-to-qiniu.js
Executable file
574
scripts/upload-to-qiniu.js
Executable file
@@ -0,0 +1,574 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* 七牛云文件上传脚本 v2
|
||||
*
|
||||
* 功能:
|
||||
* 1. 上传文件到七牛云对象存储(支持指定路径)
|
||||
* 2. 使用原文件名或自定义文件名
|
||||
* 3. 刷新 CDN 缓存
|
||||
* 4. 支持配置管理
|
||||
*
|
||||
* 使用方式:
|
||||
*
|
||||
* # 上传文件
|
||||
* node upload-to-qiniu.js upload --file <文件路径> [--key <目标路径>] [--bucket <存储桶名>]
|
||||
*
|
||||
* # 配置管理
|
||||
* node upload-to-qiniu.js config list # 查看配置
|
||||
* node upload-to-qiniu.js config set <key> <value> # 修改单个配置
|
||||
* node upload-to-qiniu.js config set-bucket <name> <json> # 添加/修改存储桶
|
||||
* node upload-to-qiniu.js config reset # 重置配置
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const crypto = require('crypto');
|
||||
const https = require('https');
|
||||
const http = require('http');
|
||||
|
||||
// ============ 配置管理 ============
|
||||
|
||||
const DEFAULT_CONFIG_PATH = path.join(process.env.HOME || process.env.USERPROFILE, '.openclaw/credentials/qiniu-config.json');
|
||||
|
||||
function loadConfig(configPath = DEFAULT_CONFIG_PATH) {
|
||||
if (!fs.existsSync(configPath)) {
|
||||
throw new Error(`配置文件不存在:${configPath}\n请先创建配置文件或运行:node upload-to-qiniu.js config init`);
|
||||
}
|
||||
|
||||
return JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
||||
}
|
||||
|
||||
function saveConfig(config, configPath = DEFAULT_CONFIG_PATH) {
|
||||
const dir = path.dirname(configPath);
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
|
||||
}
|
||||
|
||||
function initConfig() {
|
||||
const defaultConfig = {
|
||||
buckets: {
|
||||
default: {
|
||||
accessKey: "YOUR_ACCESS_KEY_HERE",
|
||||
secretKey: "YOUR_SECRET_KEY_HERE",
|
||||
bucket: "your-bucket-name",
|
||||
region: "z0",
|
||||
domain: "https://your-cdn-domain.com"
|
||||
}
|
||||
},
|
||||
_comment: {
|
||||
region: "区域代码:z0=华东,z1=华北,z2=华南,na0=北美,as0=东南亚",
|
||||
setup: "1. 获取七牛 AccessKey/SecretKey: https://portal.qiniu.com/user/key",
|
||||
setup2: "2. 创建存储桶并配置 CDN 域名",
|
||||
setup3: "3. 使用 'config set' 命令修改配置"
|
||||
}
|
||||
};
|
||||
|
||||
saveConfig(defaultConfig);
|
||||
console.log('✅ 配置文件已初始化:', DEFAULT_CONFIG_PATH);
|
||||
console.log('使用 node upload-to-qiniu.js config set <key> <value> 修改配置');
|
||||
}
|
||||
|
||||
function listConfig() {
|
||||
const config = loadConfig();
|
||||
console.log('📋 当前配置:\n');
|
||||
|
||||
console.log('存储桶配置:');
|
||||
for (const [name, bucket] of Object.entries(config.buckets)) {
|
||||
console.log(`\n 🪣 [${name}]`);
|
||||
console.log(` accessKey: ${maskKey(bucket.accessKey)}`);
|
||||
console.log(` secretKey: ${maskKey(bucket.secretKey)}`);
|
||||
console.log(` bucket: ${bucket.bucket}`);
|
||||
console.log(` region: ${bucket.region}`);
|
||||
console.log(` domain: ${bucket.domain}`);
|
||||
}
|
||||
|
||||
console.log('\n💡 使用 config set <key> <value> 修改配置');
|
||||
console.log(' 例如:config set default.accessKey YOUR_NEW_KEY');
|
||||
}
|
||||
|
||||
function maskKey(key) {
|
||||
if (!key || key.length < 8) return '***';
|
||||
return key.substring(0, 4) + '...' + key.substring(key.length - 4);
|
||||
}
|
||||
|
||||
function setConfigValue(keyPath, value) {
|
||||
const config = loadConfig();
|
||||
|
||||
const keys = keyPath.split('.');
|
||||
let current = config;
|
||||
|
||||
for (let i = 0; i < keys.length - 1; i++) {
|
||||
if (!current[keys[i]]) {
|
||||
current[keys[i]] = {};
|
||||
}
|
||||
current = current[keys[i]];
|
||||
}
|
||||
|
||||
const lastKey = keys[keys.length - 1];
|
||||
|
||||
// 类型转换
|
||||
if (value === 'true') value = true;
|
||||
else if (value === 'false') value = false;
|
||||
else if (!isNaN(value) && !value.includes('.')) value = Number(value);
|
||||
|
||||
current[lastKey] = value;
|
||||
|
||||
saveConfig(config);
|
||||
console.log(`✅ 已设置 ${keyPath} = ${value}`);
|
||||
}
|
||||
|
||||
function setBucket(name, bucketConfig) {
|
||||
const config = loadConfig();
|
||||
|
||||
let newConfig;
|
||||
try {
|
||||
newConfig = JSON.parse(bucketConfig);
|
||||
} catch (e) {
|
||||
throw new Error('无效的 JSON 配置,格式:{"accessKey":"...","secretKey":"...","bucket":"...","region":"z0","domain":"..."}');
|
||||
}
|
||||
|
||||
config.buckets[name] = newConfig;
|
||||
saveConfig(config);
|
||||
console.log(`✅ 已配置存储桶 [${name}]`);
|
||||
}
|
||||
|
||||
function resetConfig() {
|
||||
if (fs.existsSync(DEFAULT_CONFIG_PATH)) {
|
||||
fs.unlinkSync(DEFAULT_CONFIG_PATH);
|
||||
}
|
||||
initConfig();
|
||||
console.log('✅ 配置已重置');
|
||||
}
|
||||
|
||||
// ============ 七牛云鉴权 ============
|
||||
|
||||
function hmacSha1(data, secret) {
|
||||
return crypto.createHmac('sha1', secret).update(data).digest();
|
||||
}
|
||||
|
||||
function urlSafeBase64(data) {
|
||||
return Buffer.from(data).toString('base64')
|
||||
.replace(/\+/g, '-')
|
||||
.replace(/\//g, '_');
|
||||
}
|
||||
|
||||
function generateUploadToken(accessKey, secretKey, bucket, key = null, expires = 3600) {
|
||||
const deadline = Math.floor(Date.now() / 1000) + expires;
|
||||
|
||||
// 关键修复:scope 必须包含 key 才能覆盖上传
|
||||
// 根据七牛文档:
|
||||
// - scope = "bucket" → 只能新增,同名文件会失败
|
||||
// - scope = "bucket:key" → 允许覆盖同名文件
|
||||
let scope = bucket;
|
||||
if (key) {
|
||||
scope = `${bucket}:${key}`; // ✅ 添加 key,允许覆盖
|
||||
}
|
||||
|
||||
const putPolicy = {
|
||||
scope: scope,
|
||||
deadline: deadline,
|
||||
// insertOnly: 非 0 值才禁止覆盖,默认或 0 都允许覆盖
|
||||
returnBody: JSON.stringify({
|
||||
success: true,
|
||||
key: '$(key)',
|
||||
hash: '$(etag)',
|
||||
fsize: '$(fsize)',
|
||||
bucket: '$(bucket)',
|
||||
url: `$(domain)/$(key)`
|
||||
})
|
||||
};
|
||||
|
||||
const encodedPolicy = urlSafeBase64(JSON.stringify(putPolicy));
|
||||
const encodedSignature = urlSafeBase64(hmacSha1(encodedPolicy, secretKey));
|
||||
|
||||
return `${accessKey}:${encodedSignature}:${encodedPolicy}`;
|
||||
}
|
||||
|
||||
// ============ HTTP 请求工具 ============
|
||||
|
||||
function httpRequest(url, options, body = null) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const protocol = url.startsWith('https') ? https : http;
|
||||
|
||||
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({ status: res.statusCode, data: json });
|
||||
} catch (e) {
|
||||
resolve({ status: res.statusCode, data: data });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', reject);
|
||||
|
||||
if (body) {
|
||||
req.write(body);
|
||||
}
|
||||
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
// ============ 文件上传 ============
|
||||
|
||||
async function uploadFile(config, bucketName, localFile, key) {
|
||||
const bucketConfig = config.buckets[bucketName];
|
||||
|
||||
if (!bucketConfig) {
|
||||
throw new Error(`存储桶配置 "${bucketName}" 不存在,可用:${Object.keys(config.buckets).join(', ')}`);
|
||||
}
|
||||
|
||||
const { accessKey, secretKey, bucket, region, domain } = bucketConfig;
|
||||
|
||||
console.log(`📤 准备上传:${localFile} -> ${bucket}/${key}`);
|
||||
|
||||
// 1. 生成上传凭证
|
||||
const uploadToken = generateUploadToken(accessKey, secretKey, bucket, key);
|
||||
|
||||
// 2. 获取区域上传端点
|
||||
const regionEndpoint = getUploadEndpoint(region);
|
||||
|
||||
// 3. 构建 multipart/form-data 请求
|
||||
const boundary = '----WebKitFormBoundary' + Math.random().toString(36).substring(2);
|
||||
const fileContent = fs.readFileSync(localFile);
|
||||
const fileName = path.basename(localFile);
|
||||
|
||||
const bodyParts = [
|
||||
`------${boundary}`,
|
||||
'Content-Disposition: form-data; name="token"',
|
||||
'',
|
||||
uploadToken,
|
||||
`------${boundary}`,
|
||||
'Content-Disposition: form-data; name="key"',
|
||||
'',
|
||||
key,
|
||||
`------${boundary}`,
|
||||
`Content-Disposition: form-data; name="file"; filename="${fileName}"`,
|
||||
'Content-Type: application/octet-stream',
|
||||
'',
|
||||
'',
|
||||
];
|
||||
|
||||
const bodyBuffer = Buffer.concat([
|
||||
Buffer.from(bodyParts.join('\r\n'), 'utf-8'),
|
||||
fileContent,
|
||||
Buffer.from(`\r\n------${boundary}--\r\n`, 'utf-8')
|
||||
]);
|
||||
|
||||
// 使用七牛云标准表单上传 API
|
||||
// 文档:https://developer.qiniu.com/kodo/1312/upload
|
||||
const uploadUrl = `${regionEndpoint}/`; // 根路径,token 在 form-data 中
|
||||
|
||||
console.log(`📍 上传端点:${uploadUrl}`);
|
||||
|
||||
const uploadOptions = {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': `multipart/form-data; boundary=----${boundary}`,
|
||||
'Content-Length': bodyBuffer.length
|
||||
}
|
||||
};
|
||||
|
||||
const result = await httpRequest(uploadUrl, uploadOptions, bodyBuffer);
|
||||
|
||||
if (result.status !== 200) {
|
||||
throw new Error(`上传失败:${JSON.stringify(result.data)}`);
|
||||
}
|
||||
|
||||
console.log('✅ 上传成功');
|
||||
|
||||
return {
|
||||
key: result.data.key,
|
||||
hash: result.data.hash,
|
||||
url: `${domain}/${key}`
|
||||
};
|
||||
}
|
||||
|
||||
function getUploadEndpoint(region) {
|
||||
const endpoints = {
|
||||
'z0': 'https://up.qiniup.com', // 华东
|
||||
'z1': 'https://up-z1.qiniup.com', // 华北
|
||||
'z2': 'https://up-z2.qiniup.com', // 华南
|
||||
'na0': 'https://up-na0.qiniup.com', // 北美
|
||||
'as0': 'https://up-as0.qiniup.com' // 东南亚
|
||||
};
|
||||
|
||||
return endpoints[region] || endpoints['z0'];
|
||||
}
|
||||
|
||||
// ============ CDN 刷新 ============
|
||||
|
||||
/**
|
||||
* 生成七牛云 access_token(用于 Fusion CDN API)
|
||||
* 文档:https://developer.qiniu.com/kodo/manual/access-token
|
||||
*/
|
||||
function generateAccessToken(accessKey, secretKey, method, path, body, contentType = 'application/json') {
|
||||
const host = 'fusion.qiniuapi.com';
|
||||
|
||||
// 1. 生成待签名的原始字符串
|
||||
// 格式:Method Path\nHost: Host\nContent-Type: ContentType\n\nBody
|
||||
const signData = `${method} ${path}\nHost: ${host}\nContent-Type: ${contentType}\n\n${body}`;
|
||||
|
||||
// 2. 使用 HMAC-SHA1 签名
|
||||
const signature = hmacSha1(signData, secretKey);
|
||||
|
||||
// 3. URL 安全的 Base64 编码
|
||||
const encodedSign = urlSafeBase64(signature);
|
||||
|
||||
// 4. 生成 access_token
|
||||
return `Qiniu ${accessKey}:${encodedSign}`;
|
||||
}
|
||||
|
||||
async function refreshCDN(config, bucketName, key) {
|
||||
const bucketConfig = config.buckets[bucketName];
|
||||
|
||||
if (!bucketConfig) {
|
||||
throw new Error(`存储桶配置 "${bucketName}" 不存在`);
|
||||
}
|
||||
|
||||
const { accessKey, secretKey, domain } = bucketConfig;
|
||||
|
||||
const fileUrl = `${domain}/${key}`;
|
||||
console.log(`🔄 刷新 CDN: ${fileUrl}`);
|
||||
|
||||
const refreshUrl = 'https://fusion.qiniuapi.com/v2/tune/refresh';
|
||||
|
||||
const body = JSON.stringify({
|
||||
urls: [fileUrl]
|
||||
});
|
||||
|
||||
const method = 'POST';
|
||||
const path = '/v2/tune/refresh';
|
||||
const contentType = 'application/json';
|
||||
|
||||
// 生成正确的 access_token
|
||||
const accessToken = generateAccessToken(accessKey, secretKey, method, path, body, contentType);
|
||||
|
||||
const options = {
|
||||
method: method,
|
||||
headers: {
|
||||
'Host': 'fusion.qiniuapi.com',
|
||||
'Content-Type': contentType,
|
||||
'Content-Length': body.length,
|
||||
'Authorization': accessToken
|
||||
}
|
||||
};
|
||||
|
||||
const result = await httpRequest(refreshUrl, options, body);
|
||||
|
||||
if (result.status !== 200) {
|
||||
throw new Error(`CDN 刷新失败:${JSON.stringify(result.data)}`);
|
||||
}
|
||||
|
||||
console.log('✅ CDN 刷新请求已提交');
|
||||
|
||||
return result.data;
|
||||
}
|
||||
|
||||
// ============ 命令行解析 ============
|
||||
|
||||
function parseArgs(args) {
|
||||
const params = {};
|
||||
const positional = [];
|
||||
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
if (args[i].startsWith('--')) {
|
||||
const key = args[i].slice(2);
|
||||
params[key] = args[i + 1];
|
||||
i++;
|
||||
} else {
|
||||
positional.push(args[i]);
|
||||
}
|
||||
}
|
||||
|
||||
return { params, positional };
|
||||
}
|
||||
|
||||
// ============ 主函数 ============
|
||||
|
||||
async function main() {
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
if (args.length === 0) {
|
||||
printUsage();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const command = args[0];
|
||||
|
||||
try {
|
||||
// 配置管理命令
|
||||
if (command === 'config') {
|
||||
const subCommand = args[1];
|
||||
|
||||
switch (subCommand) {
|
||||
case 'init':
|
||||
initConfig();
|
||||
break;
|
||||
case 'list':
|
||||
listConfig();
|
||||
break;
|
||||
case 'set':
|
||||
if (!args[2] || !args[3]) {
|
||||
console.error('用法:config set <key> <value>');
|
||||
console.error('示例:config set default.accessKey YOUR_KEY');
|
||||
process.exit(1);
|
||||
}
|
||||
setConfigValue(args[2], args[3]);
|
||||
break;
|
||||
case 'set-bucket':
|
||||
if (!args[2] || !args[3]) {
|
||||
console.error('用法:config set-bucket <name> <json>');
|
||||
console.error('示例:config set-bucket prod \'{"accessKey":"...","secretKey":"...","bucket":"prod","region":"z0","domain":"https://..."}\'');
|
||||
process.exit(1);
|
||||
}
|
||||
setBucket(args[2], args[3]);
|
||||
break;
|
||||
case 'reset':
|
||||
resetConfig();
|
||||
break;
|
||||
default:
|
||||
console.error(`未知命令:config ${subCommand}`);
|
||||
printConfigUsage();
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// 上传命令
|
||||
if (command === 'upload') {
|
||||
const { params, positional } = parseArgs(args.slice(1));
|
||||
|
||||
if (!params.file) {
|
||||
console.error('❌ 缺少必需参数 --file');
|
||||
console.error('用法:upload --file <文件路径> [--key <目标路径>] [--bucket <存储桶名>]');
|
||||
console.error('示例:upload --file ./report.pdf --key /config/test/report.pdf --bucket default');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const bucketName = params.bucket || 'default';
|
||||
|
||||
// 检查文件是否存在
|
||||
if (!fs.existsSync(params.file)) {
|
||||
throw new Error(`文件不存在:${params.file}`);
|
||||
}
|
||||
|
||||
// 确定目标文件名
|
||||
let key = params.key;
|
||||
if (!key) {
|
||||
// 使用原文件名
|
||||
key = path.basename(params.file);
|
||||
} else if (key.startsWith('/')) {
|
||||
// 如果指定了路径,保留路径
|
||||
key = key.substring(1);
|
||||
}
|
||||
|
||||
// 加载配置
|
||||
const config = loadConfig();
|
||||
|
||||
// 获取实际存储桶名称(不是配置别名)
|
||||
const bucketConfig = config.buckets[bucketName];
|
||||
const actualBucketName = bucketConfig ? bucketConfig.bucket : bucketName;
|
||||
|
||||
// 上传文件
|
||||
const uploadResult = await uploadFile(config, bucketName, params.file, key);
|
||||
|
||||
// 刷新 CDN
|
||||
const refreshResult = await refreshCDN(config, bucketName, key);
|
||||
|
||||
console.log('\n🎉 完成!');
|
||||
console.log(`📦 文件:${key}`);
|
||||
console.log(`🔗 URL: ${uploadResult.url}`);
|
||||
console.log(`☁️ 存储桶:${actualBucketName}`);
|
||||
console.log(`📊 刷新请求 ID: ${refreshResult.requestId || 'N/A'}`);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// 未知命令
|
||||
console.error(`未知命令:${command}`);
|
||||
printUsage();
|
||||
process.exit(1);
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 错误:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
function printUsage() {
|
||||
console.log(`
|
||||
🍙 七牛云上传工具 v2
|
||||
|
||||
用法:
|
||||
node upload-to-qiniu.js <command> [options]
|
||||
|
||||
命令:
|
||||
upload 上传文件到七牛云
|
||||
config 配置管理
|
||||
|
||||
上传文件:
|
||||
node upload-to-qiniu.js upload --file <文件路径> [--key <目标路径>] [--bucket <存储桶名>]
|
||||
|
||||
选项:
|
||||
--file 本地文件路径(必需)
|
||||
--key 目标路径(可选,默认使用原文件名)
|
||||
--bucket 存储桶名称(可选,默认 default)
|
||||
|
||||
示例:
|
||||
# 使用原文件名上传
|
||||
node upload-to-qiniu.js upload --file ./report.pdf
|
||||
|
||||
# 指定目标路径
|
||||
node upload-to-qiniu.js upload --file ./report.pdf --key /config/test/report.pdf
|
||||
|
||||
# 指定存储桶
|
||||
node upload-to-qiniu.js upload --file ./report.pdf --key /docs/report.pdf --bucket production
|
||||
|
||||
配置管理:
|
||||
node upload-to-qiniu.js config <subcommand>
|
||||
|
||||
子命令:
|
||||
init 初始化配置文件
|
||||
list 查看当前配置
|
||||
set <key> <value> 修改单个配置项
|
||||
set-bucket <name> <json> 添加/修改存储桶配置
|
||||
reset 重置配置
|
||||
|
||||
示例:
|
||||
node upload-to-qiniu.js config list
|
||||
node upload-to-qiniu.js config set default.accessKey YOUR_KEY
|
||||
node upload-to-qiniu.js config set default.domain https://cdn.example.com
|
||||
node upload-to-qiniu.js config set-bucket prod '{"accessKey":"...","secretKey":"...","bucket":"prod","region":"z0","domain":"https://..."}'
|
||||
`);
|
||||
}
|
||||
|
||||
function printConfigUsage() {
|
||||
console.log(`
|
||||
配置管理命令:
|
||||
config init 初始化配置文件
|
||||
config list 查看当前配置
|
||||
config set <key> <value> 修改单个配置
|
||||
config set-bucket <name> <json> 添加存储桶
|
||||
config reset 重置配置
|
||||
|
||||
示例:
|
||||
config set default.accessKey YOUR_KEY
|
||||
config set default.domain https://cdn.example.com
|
||||
`);
|
||||
}
|
||||
|
||||
// 运行
|
||||
if (require.main === module) {
|
||||
main();
|
||||
}
|
||||
|
||||
module.exports = { uploadFile, refreshCDN, loadConfig, saveConfig, setConfigValue, setBucket };
|
||||
99
scripts/verify-url.js
Executable file
99
scripts/verify-url.js
Executable file
@@ -0,0 +1,99 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* 飞书事件订阅 URL 验证处理器
|
||||
*
|
||||
* 用途:处理飞书开放平台的事件订阅 URL 验证请求
|
||||
* 使用方式:node verify-url.js
|
||||
*/
|
||||
|
||||
const http = require('http');
|
||||
const crypto = require('crypto');
|
||||
|
||||
// 配置
|
||||
const CONFIG = {
|
||||
port: 3000,
|
||||
verifyToken: process.env.FEISHU_VERIFY_TOKEN || 'qiniu_upload_token_2026',
|
||||
encryptKey: process.env.FEISHU_ENCRYPT_KEY || ''
|
||||
};
|
||||
|
||||
console.log('🍙 飞书 URL 验证服务');
|
||||
console.log('验证 Token:', CONFIG.verifyToken);
|
||||
console.log('加密密钥:', CONFIG.encryptKey ? '已配置' : '未配置');
|
||||
console.log('监听端口:', CONFIG.port);
|
||||
console.log('');
|
||||
console.log('📋 配置步骤:');
|
||||
console.log('1. 在飞书开放平台设置请求地址:http://你的 IP:3000');
|
||||
console.log('2. 设置验证 Token:', CONFIG.verifyToken);
|
||||
console.log('3. 点击保存,等待验证');
|
||||
console.log('');
|
||||
|
||||
const server = http.createServer((req, res) => {
|
||||
const url = new URL(req.url, `http://${req.headers.host}`);
|
||||
|
||||
console.log(`[${new Date().toISOString()}] ${req.method} ${url.pathname}`);
|
||||
|
||||
// 处理飞书验证请求
|
||||
if (url.pathname === '/' && req.method === 'POST') {
|
||||
let body = '';
|
||||
|
||||
req.on('data', chunk => {
|
||||
body += chunk;
|
||||
});
|
||||
|
||||
req.on('end', () => {
|
||||
try {
|
||||
const event = JSON.parse(body);
|
||||
|
||||
// 验证类型:url_verification
|
||||
if (event.type === 'url_verification') {
|
||||
console.log('✅ 收到验证请求');
|
||||
console.log('Challenge:', event.challenge);
|
||||
|
||||
// 返回 challenge
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ challenge: event.challenge }));
|
||||
|
||||
console.log('✅ 验证成功!请在飞书开放平台确认状态');
|
||||
return;
|
||||
}
|
||||
|
||||
// 其他事件类型
|
||||
console.log('事件类型:', event.type);
|
||||
console.log('事件内容:', JSON.stringify(event, null, 2));
|
||||
|
||||
res.writeHead(200);
|
||||
res.end('OK');
|
||||
|
||||
} catch (e) {
|
||||
console.error('❌ 解析失败:', e.message);
|
||||
res.writeHead(400);
|
||||
res.end('Invalid JSON');
|
||||
}
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// 健康检查
|
||||
if (url.pathname === '/health') {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ status: 'ok', timestamp: Date.now() }));
|
||||
return;
|
||||
}
|
||||
|
||||
// 其他请求
|
||||
res.writeHead(404);
|
||||
res.end('Not Found');
|
||||
});
|
||||
|
||||
server.listen(CONFIG.port, () => {
|
||||
console.log('');
|
||||
console.log('🚀 服务已启动');
|
||||
console.log(`📍 监听地址:http://0.0.0.0:${CONFIG.port}`);
|
||||
console.log('');
|
||||
console.log('💡 提示:');
|
||||
console.log(' - 按 Ctrl+C 停止服务');
|
||||
console.log(' - 访问 http://localhost:3000/health 检查服务状态');
|
||||
console.log('');
|
||||
});
|
||||
83
test-feishu-upload.js
Normal file
83
test-feishu-upload.js
Normal file
@@ -0,0 +1,83 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* 模拟飞书上传测试
|
||||
*/
|
||||
|
||||
const { exec } = require('child_process');
|
||||
const path = require('path');
|
||||
|
||||
const CONFIG = {
|
||||
scriptDir: __dirname,
|
||||
tempFile: path.join(__dirname, 'test-file.txt'),
|
||||
tempFileV2: path.join(__dirname, 'test-file-v2.txt')
|
||||
};
|
||||
|
||||
async function testUpload(targetKey, file) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const uploadScript = path.join(CONFIG.scriptDir, 'scripts/upload-to-qiniu.js');
|
||||
const cmd = `node "${uploadScript}" upload --file "${file}" --key "${targetKey}" --bucket default`;
|
||||
|
||||
console.log(`\n📤 测试上传:`);
|
||||
console.log(` 文件:${file}`);
|
||||
console.log(` 目标 key: ${targetKey}`);
|
||||
console.log(` 命令:${cmd}\n`);
|
||||
|
||||
exec(cmd, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
console.log('❌ 上传失败:', stderr || error.message);
|
||||
resolve({ success: false, error: stderr || error.message });
|
||||
} else {
|
||||
console.log('✅ 上传成功:');
|
||||
console.log(stdout);
|
||||
resolve({ success: true, output: stdout });
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log('═══════════════════════════════════════════════════════════');
|
||||
console.log('🧪 七牛云覆盖上传测试 - 模拟飞书流程');
|
||||
console.log('═══════════════════════════════════════════════════════════\n');
|
||||
|
||||
const testKey = 'feishu-test/override-file.txt';
|
||||
|
||||
// 第一次上传
|
||||
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||||
console.log('第 1 次上传(新增)');
|
||||
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||||
const result1 = await testUpload(testKey, CONFIG.tempFile);
|
||||
|
||||
if (!result1.success) {
|
||||
console.log('\n❌ 第一次上传失败,测试终止');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// 等待 2 秒
|
||||
await new Promise(r => setTimeout(r, 2000));
|
||||
|
||||
// 第二次上传(覆盖)
|
||||
console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||||
console.log('第 2 次上传(覆盖测试)');
|
||||
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||||
const result2 = await testUpload(testKey, CONFIG.tempFileV2);
|
||||
|
||||
if (!result2.success) {
|
||||
console.log('\n❌ 覆盖上传失败!');
|
||||
console.log('问题:scope 参数可能没有包含 key');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// 验证内容
|
||||
console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||||
console.log('✅ 覆盖上传成功!');
|
||||
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
|
||||
|
||||
console.log('📋 测试总结:');
|
||||
console.log(' 第 1 次上传:✅ 成功');
|
||||
console.log(' 第 2 次上传:✅ 成功(已覆盖)');
|
||||
console.log('\n结论:覆盖上传功能正常工作!');
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
Reference in New Issue
Block a user