From 9b584cdad440bcdc3d7ee3754776bd2045316dd0 Mon Sep 17 00:00:00 2001 From: daoqi Date: Sat, 7 Mar 2026 16:08:47 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E6=B8=85=E7=90=86=E8=B0=83?= =?UTF-8?q?=E8=AF=95=E4=BB=A3=E7=A0=81=E5=92=8C=E5=BA=9F=E5=BC=83=E6=96=87?= =?UTF-8?q?=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 删除的废弃文件: - test-feishu-upload.js (测试文件) - debug-upload.js (调试工具) - check-bucket-override.js (诊断工具) - feishu-card-server.js (废弃的卡片服务器) - feishu-websocket-listener.js (废弃的 WebSocket 监听器) - openclaw-bridge.js (废弃的桥接代码) - setup.sh, start-listener.sh, verify-url.js (废弃脚本) - cards/ 目录 (未使用的卡片模板) - ARCHITECTURE.md, INTEGRATION.md 等废弃文档 优化: - openclaw-processor.js: 添加 DEBUG 环境变量控制日志输出 - 移除生产环境不必要的调试日志 清理后核心文件: - openclaw-processor.js (OpenClaw 处理器) - openclaw-handler.js (HTTP 处理器) - scripts/upload-to-qiniu.js (核心上传脚本) - scripts/feishu-listener.js (独立监听器) - scripts/update-bucket-setting.js (存储桶设置工具) - deploy.sh (部署脚本) --- ARCHITECTURE.md | 171 ---------- CHANGELOG_OVERRIDE.md | 79 ----- INTEGRATION.md | 327 ------------------ README_OVERRIDE.md | 131 -------- README_SOLUTION.md | 139 -------- cards/upload-card.json | 61 ---- openclaw-processor.js | 6 +- scripts/check-bucket-override.js | 213 ------------ scripts/debug-upload.js | 275 --------------- scripts/feishu-card-server.js | 410 ----------------------- scripts/feishu-websocket-listener.js | 477 --------------------------- scripts/openclaw-bridge.js | 82 ----- scripts/setup.sh | 84 ----- scripts/start-listener.sh | 36 -- scripts/verify-url.js | 99 ------ test-feishu-upload.js | 83 ----- 16 files changed, 3 insertions(+), 2670 deletions(-) delete mode 100644 ARCHITECTURE.md delete mode 100644 CHANGELOG_OVERRIDE.md delete mode 100644 INTEGRATION.md delete mode 100644 README_OVERRIDE.md delete mode 100644 README_SOLUTION.md delete mode 100644 cards/upload-card.json delete mode 100644 scripts/check-bucket-override.js delete mode 100644 scripts/debug-upload.js delete mode 100644 scripts/feishu-card-server.js delete mode 100755 scripts/feishu-websocket-listener.js delete mode 100644 scripts/openclaw-bridge.js delete mode 100755 scripts/setup.sh delete mode 100755 scripts/start-listener.sh delete mode 100755 scripts/verify-url.js delete mode 100644 test-feishu-upload.js diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md deleted file mode 100644 index 7dbd78a..0000000 --- a/ARCHITECTURE.md +++ /dev/null @@ -1,171 +0,0 @@ -# 🍙 七牛云上传 - 架构方案对比 - -## 现状 - -- **飞书开放平台**:事件订阅已配置,使用长连接/回调模式 -- **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 模式 - ---- - -**你选择哪个方案?** 🍙 diff --git a/CHANGELOG_OVERRIDE.md b/CHANGELOG_OVERRIDE.md deleted file mode 100644 index 4c46858..0000000 --- a/CHANGELOG_OVERRIDE.md +++ /dev/null @@ -1,79 +0,0 @@ -# 七牛云上传 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 - 完整使用说明 diff --git a/INTEGRATION.md b/INTEGRATION.md deleted file mode 100644 index cf03b6b..0000000 --- a/INTEGRATION.md +++ /dev/null @@ -1,327 +0,0 @@ -# 🍙 七牛云 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 <名> # 添加存储桶 -/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. 测试文件上传功能 - ---- - -**集成完成!** 🍙 diff --git a/README_OVERRIDE.md b/README_OVERRIDE.md deleted file mode 100644 index 3ba8fc6..0000000 --- a/README_OVERRIDE.md +++ /dev/null @@ -1,131 +0,0 @@ -# 七牛云覆盖上传功能 - 重要说明 - -## ⚠️ 为什么同名文件无法覆盖? - -七牛云存储桶默认可能启用了**"防覆盖"**设置,这是一个**存储桶级别的设置**,需要在七牛云控制台手动关闭。 - -## ✅ 解决方案(推荐) - -### 方法 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) diff --git a/README_SOLUTION.md b/README_SOLUTION.md deleted file mode 100644 index b1248b0..0000000 --- a/README_SOLUTION.md +++ /dev/null @@ -1,139 +0,0 @@ -# 七牛云覆盖上传问题 - 最终解决方案 - -## 🔍 问题诊断结果 - -经过测试确认,你的七牛云存储桶 `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 无关 | - -**结论:** 必须通过七牛云控制台关闭防覆盖,或使用唯一文件名。 diff --git a/cards/upload-card.json b/cards/upload-card.json deleted file mode 100644 index 5c09c22..0000000 --- a/cards/upload-card.json +++ /dev/null @@ -1,61 +0,0 @@ -{ - "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" - } - } - ] - } - ] -} diff --git a/openclaw-processor.js b/openclaw-processor.js index 7aeb098..e2c186e 100755 --- a/openclaw-processor.js +++ b/openclaw-processor.js @@ -28,7 +28,10 @@ const CONFIG = { // ============ 工具函数 ============ +// 调试日志(生产环境可禁用) +const DEBUG = process.env.QINIU_DEBUG === 'true'; function log(...args) { + if (!DEBUG) return; const timestamp = new Date().toISOString(); console.error(`[${timestamp}]`, ...args); } @@ -269,9 +272,6 @@ async function handleUpload(message) { const bucketMatch = stdout.match(/☁️ 存储桶:(.+)/); const actualBucket = bucketMatch ? bucketMatch[1].trim() : (cmd.bucket || 'default'); - // 调试输出 - log('存储桶解析:配置别名=', cmd.bucket, '实际桶名=', actualBucket); - // 直接返回完整回复 return { handled: true, diff --git a/scripts/check-bucket-override.js b/scripts/check-bucket-override.js deleted file mode 100644 index f392304..0000000 --- a/scripts/check-bucket-override.js +++ /dev/null @@ -1,213 +0,0 @@ -#!/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 }; diff --git a/scripts/debug-upload.js b/scripts/debug-upload.js deleted file mode 100644 index b9a9624..0000000 --- a/scripts/debug-upload.js +++ /dev/null @@ -1,275 +0,0 @@ -#!/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); diff --git a/scripts/feishu-card-server.js b/scripts/feishu-card-server.js deleted file mode 100644 index 56c94a2..0000000 --- a/scripts/feishu-card-server.js +++ /dev/null @@ -1,410 +0,0 @@ -#!/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 = ` - - - - 七牛云上传 - 飞书卡片服务器 - - - -

🍙 七牛云上传 - 飞书卡片服务器

- -
- ✅ 服务器运行中 -
端口:${PORT} -
- -
-

配置信息

-

卡片模板:${CARD_TEMPLATE_PATH}

-

七牛配置:${QINIU_CONFIG_PATH}

-
- -

飞书开发者后台配置

-
    -
  1. 请求网址:http://你的服务器IP:${PORT}/feishu/card
  2. -
  3. 数据加密方式:选择"不加密"
  4. -
  5. 验证令牌:在环境变量中设置 FEISHU_VERIFICATION_TOKEN
  6. -
- -

测试

-

使用 curl 测试:

-
curl -X POST http://localhost:${PORT}/feishu/card \\
-  -H "Content-Type: application/json" \\
-  -d '{"type":"url_verification","challenge":"test123"}'
- - - `; - - 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); - }); -}); diff --git a/scripts/feishu-websocket-listener.js b/scripts/feishu-websocket-listener.js deleted file mode 100755 index 7a9fbb0..0000000 --- a/scripts/feishu-websocket-listener.js +++ /dev/null @@ -1,477 +0,0 @@ -#!/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 # 修改配置 - /qiniu-config set-bucket # 添加存储桶 - /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(); diff --git a/scripts/openclaw-bridge.js b/scripts/openclaw-bridge.js deleted file mode 100644 index 3d2ce87..0000000 --- a/scripts/openclaw-bridge.js +++ /dev/null @@ -1,82 +0,0 @@ -#!/usr/bin/env node - -/** - * OpenClaw 桥接脚本 - * - * 功能: - * 1. 从 OpenClaw 接收消息 - * 2. 调用上传脚本 - * 3. 回复结果 - * - * 使用方式(由 OpenClaw 调用): - * node scripts/openclaw-bridge.js [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 [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); - }); -} diff --git a/scripts/setup.sh b/scripts/setup.sh deleted file mode 100755 index 7e7026b..0000000 --- a/scripts/setup.sh +++ /dev/null @@ -1,84 +0,0 @@ -#!/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 "" diff --git a/scripts/start-listener.sh b/scripts/start-listener.sh deleted file mode 100755 index d2ea361..0000000 --- a/scripts/start-listener.sh +++ /dev/null @@ -1,36 +0,0 @@ -#!/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 diff --git a/scripts/verify-url.js b/scripts/verify-url.js deleted file mode 100755 index be0d231..0000000 --- a/scripts/verify-url.js +++ /dev/null @@ -1,99 +0,0 @@ -#!/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(''); -}); diff --git a/test-feishu-upload.js b/test-feishu-upload.js deleted file mode 100644 index 5c8cb4c..0000000 --- a/test-feishu-upload.js +++ /dev/null @@ -1,83 +0,0 @@ -#!/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);