From b00567762fd6d180e4b724b88c015880ca38ae4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A5=AD=E5=9B=A2?= Date: Thu, 5 Mar 2026 14:22:26 +0800 Subject: [PATCH] =?UTF-8?q?Initial=20commit:=20=E4=B8=83=E7=89=9B=E4=BA=91?= =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E9=A3=9E=E4=B9=A6=E6=9C=BA=E5=99=A8=E4=BA=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 功能: - 飞书交互卡片支持 - 七牛云文件上传 - 自动 CDN 刷新 - 多存储桶配置 - 跨平台部署(Linux/macOS/Windows) - Docker 支持 --- .env.example | 16 ++ .gitignore | 6 + DEPLOY.md | 432 +++++++++++++++++++++++++++++++ Dockerfile | 20 ++ README.md | 211 +++++++++++++++ WINDOWS.md | 284 ++++++++++++++++++++ config/qiniu-config.json.example | 17 ++ package.json | 28 ++ src/cards/config-card.js | 185 +++++++++++++ src/cards/upload-card.js | 230 ++++++++++++++++ src/feishu-api.js | 151 +++++++++++ src/index.js | 311 ++++++++++++++++++++++ src/qiniu-uploader.js | 253 ++++++++++++++++++ start.bat | 73 ++++++ start.sh | 69 +++++ 15 files changed, 2286 insertions(+) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 DEPLOY.md create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 WINDOWS.md create mode 100644 config/qiniu-config.json.example create mode 100644 package.json create mode 100644 src/cards/config-card.js create mode 100644 src/cards/upload-card.js create mode 100644 src/feishu-api.js create mode 100644 src/index.js create mode 100644 src/qiniu-uploader.js create mode 100644 start.bat create mode 100644 start.sh diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..b01ad4c --- /dev/null +++ b/.env.example @@ -0,0 +1,16 @@ +# 飞书配置 +FEISHU_APP_ID=cli_xxxxxxxxxx +FEISHU_APP_SECRET=xxxxxxxxxxxxxx +FEISHU_VERIFICATION_TOKEN=xxxxxxxxxxxxxx +FEISHU_ENCRYPT_KEY=xxxxxxxxxxxxxx + +# 七牛云配置(可选,也可通过卡片配置) +QINIU_ACCESS_KEY=YOUR_ACCESS_KEY +QINIU_SECRET_KEY=YOUR_SECRET_KEY +QINIU_BUCKET=your-bucket-name +QINIU_REGION=z0 +QINIU_DOMAIN=https://your-cdn.com + +# 服务配置 +PORT=3000 +NODE_ENV=production diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3090994 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +node_modules/ +.env +config/qiniu-config.json +temp/ +*.log +.DS_Store diff --git a/DEPLOY.md b/DEPLOY.md new file mode 100644 index 0000000..f19e2ed --- /dev/null +++ b/DEPLOY.md @@ -0,0 +1,432 @@ +# 七牛云上传机器人 - 跨平台部署指南 + +支持 **Linux**、**macOS** 和 **Windows** 系统。 + +--- + +## 📋 前置要求 + +### 所有平台 + +1. **Node.js 18+** + - 下载地址:https://nodejs.org/ + - 验证:`node --version` + +2. **七牛云账号** + - 官网:https://www.qiniu.com/ + - 需要:AccessKey、SecretKey、存储桶 + +3. **飞书企业管理员权限** + - 用于创建自建应用 + +4. **公网访问能力**(三选一) + - 云服务器(阿里云、腾讯云等) + - 内网穿透工具(ngrok、cloudflared) + - 本地网络有公网 IP + +--- + +## 🐧 Linux 部署 + +### 方式 A:一键脚本(推荐) + +```bash +# 1. 下载项目 +git clone qiniu-feishu-bot +cd qiniu-feishu-bot + +# 2. 运行启动脚本 +chmod +x start.sh +./start.sh + +# 脚本会自动: +# - 检查 Node.js +# - 创建配置文件 +# - 安装依赖 +# - 启动服务 +``` + +### 方式 B:手动部署 + +```bash +# 1. 安装 Node.js +curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash - +sudo apt-get install -y nodejs + +# 2. 安装依赖 +npm install + +# 3. 配置环境变量 +cp .env.example .env +nano .env # 编辑配置 + +# 4. 配置七牛云 +cp config/qiniu-config.json.example config/qiniu-config.json +nano config/qiniu-config.json + +# 5. 启动服务 +npm start + +# 6. 后台运行(可选) +# 使用 systemd +sudo nano /etc/systemd/system/qiniu-bot.service +``` + +**systemd 服务配置:** + +```ini +[Unit] +Description=七牛云上传机器人 +After=network.target + +[Service] +Type=simple +User=admin +WorkingDirectory=/path/to/qiniu-feishu-bot +Environment=NODE_ENV=production +ExecStart=/usr/bin/node src/index.js +Restart=always + +[Install] +WantedBy=multi-user.target +``` + +```bash +# 启用服务 +sudo systemctl daemon-reload +sudo systemctl enable qiniu-bot +sudo systemctl start qiniu-bot +sudo systemctl status qiniu-bot +``` + +### 方式 C:Docker 部署 + +```bash +# 1. 安装 Docker +curl -fsSL https://get.docker.com | sh + +# 2. 构建镜像 +docker build -t qiniu-feishu-bot . + +# 3. 运行容器 +docker run -d \ + --name qiniu-bot \ + -p 3000:3000 \ + --restart unless-stopped \ + --env-file .env \ + -v $(pwd)/config:/app/config \ + -v $(pwd)/qiniu-data:/root/.qiniu \ + qiniu-feishu-bot + +# 4. 查看日志 +docker logs -f qiniu-bot +``` + +--- + +## 🍎 macOS 部署 + +### 方式 A:一键脚本 + +```bash +# 1. 下载项目 +git clone qiniu-feishu-bot +cd qiniu-feishu-bot + +# 2. 运行启动脚本 +chmod +x start.sh +./start.sh +``` + +### 方式 B:Homebrew 安装 Node.js + +```bash +# 1. 安装 Node.js +brew install node@18 + +# 2. 安装依赖 +npm install + +# 3. 配置 +cp .env.example .env +nano .env + +cp config/qiniu-config.json.example config/qiniu-config.json +nano config/qiniu-config.json + +# 4. 启动 +npm start + +# 5. 后台运行(可选) +brew install pm2 +pm2 start src/index.js --name qiniu-bot +pm2 save +pm2 startup +``` + +--- + +## 🪟 Windows 部署 + +### 方式 A:一键启动(推荐) + +```powershell +# 1. 下载项目 +# 下载 ZIP 或 git clone +git clone qiniu-feishu-bot +cd qiniu-feishu-bot + +# 2. 双击运行 +start.bat + +# 脚本会自动: +# - 检查 Node.js +# - 创建配置文件 +# - 安装依赖 +# - 启动服务 +``` + +### 方式 B:手动部署 + +```powershell +# 1. 安装 Node.js +# 下载:https://nodejs.org/ +# 验证:node --version + +# 2. 安装依赖 +npm install + +# 3. 配置环境变量 +copy .env.example .env +notepad .env + +# 4. 配置七牛云 +mkdir config +copy config\qiniu-config.json.example config\qiniu-config.json +notepad config\qiniu-config.json + +# 5. 启动服务 +npm start +``` + +### 方式 C:作为 Windows 服务运行 + +使用 **NSSM** (Non-Sucking Service Manager): + +```powershell +# 1. 下载 NSSM +# https://nssm.cc/download + +# 2. 安装服务 +nssm install QiniuBot "C:\Program Files\nodejs\node.exe" "C:\path\to\qiniu-feishu-bot\src\index.js" + +# 3. 配置工作目录 +nssm set QiniuBot AppDirectory "C:\path\to\qiniu-feishu-bot" +nssm set QiniuBot AppEnvironmentExtra "NODE_ENV=production" + +# 4. 启动服务 +nssm start QiniuBot + +# 5. 管理命令 +nssm stop QiniuBot +nssm restart QiniuBot +nssm remove QiniuBot # 删除服务 +``` + +### 方式 D:Docker Desktop + +```powershell +# 1. 安装 Docker Desktop +# https://www.docker.com/products/docker-desktop + +# 2. 构建镜像 +docker build -t qiniu-feishu-bot . + +# 3. 运行容器 +docker run -d ^ + --name qiniu-bot ^ + -p 3000:3000 ^ + --restart unless-stopped ^ + --env-file .env ^ + -v %cd%\config:/app/config ^ + qiniu-feishu-bot + +# 4. 查看日志 +docker logs -f qiniu-bot +``` + +--- + +## 🌐 配置公网访问 + +### 云服务器(推荐生产环境) + +**阿里云/腾讯云 ECS:** + +1. 购买云服务器(最低配置:1 核 1G) +2. 配置安全组,开放端口 3000 +3. 部署应用 +4. 配置域名和 HTTPS(可选但推荐) + +**Nginx 反向代理:** + +```nginx +server { + listen 80; + server_name your-domain.com; + + location /feishu/event { + proxy_pass http://localhost:3000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} +``` + +### 内网穿透(测试用) + +**ngrok:** + +```bash +# 下载:https://ngrok.com/ +ngrok http 3000 + +# 复制生成的 https 地址到飞书事件订阅 +``` + +**cloudflared:** + +```bash +# 下载:https://github.com/cloudflare/cloudflared/releases +cloudflared tunnel --url http://localhost:3000 +``` + +**cpolar(国内推荐):** + +```bash +# 官网:https://www.cpolar.com/ +cpolar http 3000 +``` + +--- + +## 📝 配置文件说明 + +### .env 文件 + +```env +# 飞书配置(从飞书开放平台获取) +FEISHU_APP_ID=cli_xxxxxxxxxx +FEISHU_APP_SECRET=xxxxxxxxxxxxxx +FEISHU_VERIFICATION_TOKEN=xxxxxxxxxxxxxx +FEISHU_ENCRYPT_KEY=xxxxxxxxxxxxxx + +# 七牛云配置 +QINIU_ACCESS_KEY=xxxxxxxxxxxxxx +QINIU_SECRET_KEY=xxxxxxxxxxxxxx +QINIU_BUCKET=your-bucket-name +QINIU_REGION=z0 +QINIU_DOMAIN=https://your-cdn.com + +# 服务配置 +PORT=3000 +NODE_ENV=production +``` + +### config/qiniu-config.json + +```json +{ + "buckets": { + "default": { + "accessKey": "YOUR_ACCESS_KEY", + "secretKey": "YOUR_SECRET_KEY", + "bucket": "your-bucket-name", + "region": "z0", + "domain": "https://your-cdn.com" + } + } +} +``` + +--- + +## ✅ 验证部署 + +### 1. 检查服务状态 + +```bash +# Linux/macOS +curl http://localhost:3000/health + +# Windows PowerShell +Invoke-WebRequest http://localhost:3000/health +``` + +应返回:`{"status":"ok",...}` + +### 2. 测试飞书消息 + +1. 在飞书中找到机器人 +2. 发送任意消息 +3. 应收到交互式卡片 + +### 3. 测试上传 + +``` +/upload --original default +[附上一个文件] +``` + +--- + +## 🔧 故障排查 + +### 端口被占用 + +```bash +# Linux/macOS +lsof -i :3000 +kill -9 + +# Windows +netstat -ano | findstr :3000 +taskkill /PID /F +``` + +### 权限问题 + +```bash +# Linux/macOS +chmod +x start.sh +chmod -R 755 . + +# Windows +# 以管理员身份运行 start.bat +``` + +### 依赖安装失败 + +```bash +# 清除缓存重试 +npm cache clean --force +npm install + +# 使用淘宝镜像 +npm config set registry https://registry.npmmirror.com +npm install +``` + +--- + +## 📞 获取帮助 + +- 查看日志:`docker logs qiniu-bot` 或查看控制台输出 +- 检查配置:确保 `.env` 和 `qiniu-config.json` 正确 +- 网络问题:确认防火墙开放端口 3000 + +--- + +**🍙 祝你部署顺利!** diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..771fa90 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,20 @@ +FROM node:18-alpine + +WORKDIR /app + +# 复制 package 文件 +COPY package*.json ./ + +# 安装依赖 +RUN npm ci --only=production + +# 复制源代码 +COPY src/ ./src/ +COPY config/ ./config/ + +# 创建配置目录 +RUN mkdir -p /root/.qiniu + +EXPOSE 3000 + +CMD ["node", "src/index.js"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..8fb2b4e --- /dev/null +++ b/README.md @@ -0,0 +1,211 @@ +# 七牛云上传 - 飞书独立应用 + +基于飞书交互卡片的七牛云文件上传工具,不依赖 OpenClaw。 + +## 🚀 快速开始 + +### 1. 创建飞书应用 + +1. 访问 [飞书开放平台](https://open.feishu.cn/) +2. 登录企业管理员账号 +3. 点击"创建应用" → "自建应用" +4. 填写应用信息: + - 应用名称:七牛云上传助手 + - 应用图标:🍙 +5. 进入应用管理页面 + +### 2. 配置权限 + +在"权限管理"中添加以下权限: + +| 权限 | 说明 | +|------|------| +| `im:message` | 发送和接收消息 | +| `im:file` | 文件上传下载 | +| `contact:group:readonly` | 读取群组信息(可选) | + +### 3. 配置事件订阅 + +1. 进入"事件订阅"页面 +2. 开启"启用事件订阅" +3. 填写请求地址:`https://your-domain.com/feishu/event` +4. 配置订阅事件: + - `im.message.receive_v1` - 接收消息 + - `im.message.group_at_msg.receive_v1` - 群组 @ 消息(可选) +5. 保存后复制 Verification Token 和 Encrypt Key + +### 4. 配置机器人 + +1. 进入"机器人"页面 +2. 开启"启用机器人" +3. 配置机器人名称和头像 +4. 在"消息已读状态"中开启"获取用户已读状态"(可选) + +### 5. 安装应用 + +1. 进入"版本管理与发布" +2. 点击"发布应用" +3. 在飞书中搜索并添加该机器人 + +--- + +## 📦 部署 + +### 🐧 Linux / 🍎 macOS + +```bash +# 一键启动 +chmod +x start.sh +./start.sh + +# 或手动部署 +npm install +cp .env.example .env +npm start +``` + +### 🪟 Windows + +```cmd +# 双击运行 +start.bat + +# 或手动部署 +npm install +copy .env.example .env +npm start +``` + +### 🐳 Docker(所有平台) + +```bash +# 构建镜像 +docker build -t qiniu-feishu-bot . + +# 运行容器 +docker run -d \ + --name qiniu-bot \ + -p 3000:3000 \ + --env-file .env \ + -v $(pwd)/config:/app/config \ + qiniu-feishu-bot +``` + +--- + +## ⚙️ 配置 + +### 环境变量 (.env) + +```env +# 飞书配置 +FEISHU_APP_ID=cli_xxxxxxxxxx +FEISHU_APP_SECRET=xxxxxxxxxxxxxx +FEISHU_VERIFICATION_TOKEN=xxxxxxxxxxxxxx +FEISHU_ENCRYPT_KEY=xxxxxxxxxxxxxx + +# 七牛云配置(可选,也可通过卡片配置) +QINIU_ACCESS_KEY=xxxxxxxxxxxxxx +QINIU_SECRET_KEY=xxxxxxxxxxxxxx +QINIU_BUCKET=your-bucket-name +QINIU_REGION=z0 +QINIU_DOMAIN=https://your-cdn.com + +# 服务配置 +PORT=3000 +NODE_ENV=production +``` + +### 七牛云配置 (config/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" + } + } +} +``` + +--- + +## 💡 使用方式 + +### 命令触发 + +| 命令 | 说明 | +|------|------| +| `/upload` | 上传文件到七牛云 | +| `/upload --original` | 使用原文件名上传 | +| `/config` | 管理七牛云配置 | +| `/help` | 查看帮助 | + +### 卡片交互 + +发送任意消息给机器人,会收到交互式卡片: + +- 📎 **选择文件** - 点击后上传附件 +- ⚙️ **配置** - 管理七牛云配置 +- ❓ **帮助** - 查看使用说明 + +--- + +## 📁 项目结构 + +``` +qiniu-feishu-bot/ +├── src/ +│ ├── index.js # 主入口 +│ ├── feishu-api.js # 飞书 API 封装 +│ ├── qiniu-uploader.js # 七牛云上传 +│ └── cards/ # 卡片模板 +│ ├── upload-card.js # 上传卡片 +│ └── config-card.js # 配置卡片 +├── config/ +│ └── qiniu-config.json.example +├── .env.example +├── .gitignore +├── Dockerfile +├── package.json +├── start.sh # Linux/macOS 启动脚本 +├── start.bat # Windows 启动脚本 +├── README.md # 项目说明 +├── DEPLOY.md # 详细部署指南(Linux/macOS/Windows) +└── WINDOWS.md # Windows 专用指南 +``` + +--- + +## 🔧 故障排查 + +### 上传失败 + +1. 检查七牛云配置是否正确 +2. 确认存储桶区域代码匹配 +3. 检查 AccessKey/SecretKey 权限 + +### 消息无响应 + +1. 检查事件订阅配置 +2. 确认服务器可被飞书访问(需要公网 IP) +3. 查看日志输出 + +### 文件无法覆盖 + +在七牛云控制台关闭存储桶的"防覆盖"设置。 + +### 跨平台问题 + +- **Windows**: 查看 [`WINDOWS.md`](./WINDOWS.md) +- **Linux/macOS**: 查看 [`DEPLOY.md`](./DEPLOY.md) + +--- + +## 📝 许可证 + +MIT diff --git a/WINDOWS.md b/WINDOWS.md new file mode 100644 index 0000000..a5aeffb --- /dev/null +++ b/WINDOWS.md @@ -0,0 +1,284 @@ +# Windows 快速开始指南 + +🪟 **Windows 用户专用部署指南** + +--- + +## 方法一:双击启动(最简单) + +### 1️⃣ 下载项目 + +- 从 GitHub 下载 ZIP 文件 +- 或使用 Git:`git clone ` + +### 2️⃣ 安装 Node.js + +1. 访问 https://nodejs.org/ +2. 下载 **LTS 版本**(推荐 18.x 或更高) +3. 双击安装,一路"下一步" +4. 验证安装: + ```cmd + node --version + npm --version + ``` + +### 3️⃣ 配置应用 + +1. 打开项目文件夹 +2. 双击运行 `start.bat` +3. 脚本会自动: + - ✅ 检查 Node.js + - ✅ 创建配置文件 + - ✅ 安装依赖 + - ⚠️ 提示你编辑配置 + +### 4️⃣ 编辑配置 + +脚本会提示你编辑两个文件: + +**`.env` 文件**(飞书配置): +```env +FEISHU_APP_ID=cli_xxxxxx +FEISHU_APP_SECRET=xxxxxx +FEISHU_VERIFICATION_TOKEN=xxxxxx +FEISHU_ENCRYPT_KEY=xxxxxx +``` + +**`config\qiniu-config.json`**(七牛云配置): +```json +{ + "buckets": { + "default": { + "accessKey": "YOUR_ACCESS_KEY", + "secretKey": "YOUR_SECRET_KEY", + "bucket": "your-bucket", + "region": "z0", + "domain": "https://your-cdn.com" + } + } +} +``` + +### 5️⃣ 启动服务 + +配置完成后,再次双击 `start.bat`,看到: + +``` +======================================== + 🍙 七牛云上传机器人 - Windows 版 +======================================== + +✅ Node.js 已安装 +📦 正在安装依赖... +✅ 依赖安装完成 + +🚀 正在启动服务... +📍 监听端口:3000 +``` + +--- + +## 方法二:作为 Windows 服务运行(推荐生产环境) + +使用 **NSSM** 让服务开机自启、后台运行。 + +### 1️⃣ 下载 NSSM + +访问 https://nssm.cc/download 下载最新版 + +### 2️⃣ 安装服务 + +以**管理员身份**打开命令提示符: + +```cmd +cd C:\path\to\qiniu-feishu-bot + +nssm install QiniuBot "C:\Program Files\nodejs\node.exe" "C:\path\to\qiniu-feishu-bot\src\index.js" +``` + +### 3️⃣ 配置服务 + +```cmd +# 设置工作目录 +nssm set QiniuBot AppDirectory "C:\path\to\qiniu-feishu-bot" + +# 设置环境变量 +nssm set QiniuBot AppEnvironmentExtra "NODE_ENV=production" +nssm set QiniuBot AppEnvironmentExtra "PATH=%PATH%" + +# 设置日志 +nssm set QiniuBot AppStdout "C:\path\to\qiniu-feishu-bot\logs\stdout.log" +nssm set QiniuBot AppStderr "C:\path\to\qiniu-feishu-bot\logs\stderr.log" +``` + +### 4️⃣ 启动服务 + +```cmd +nssm start QiniuBot +``` + +### 5️⃣ 管理命令 + +```cmd +nssm stop QiniuBot # 停止 +nssm restart QiniuBot # 重启 +nssm status QiniuBot # 查看状态 +nssm remove QiniuBot # 删除服务(会提示确认) +``` + +--- + +## 方法三:Docker Desktop + +### 1️⃣ 安装 Docker Desktop + +访问 https://www.docker.com/products/docker-desktop 下载 + +### 2️⃣ 构建镜像 + +打开 **PowerShell**: + +```powershell +cd C:\path\to\qiniu-feishu-bot + +docker build -t qiniu-feishu-bot . +``` + +### 3️⃣ 运行容器 + +```powershell +docker run -d ^ + --name qiniu-bot ^ + -p 3000:3000 ^ + --restart unless-stopped ^ + --env-file .env ^ + -v ${PWD}\config:/app/config ^ + qiniu-feishu-bot +``` + +### 4️⃣ 管理命令 + +```powershell +docker logs -f qiniu-bot # 查看日志 +docker stop qiniu-bot # 停止 +docker start qiniu-bot # 启动 +docker rm qiniu-bot # 删除 +``` + +--- + +## 🔧 常见问题 + +### ❌ "node 不是内部或外部命令" + +**解决:** +1. 重新安装 Node.js +2. 安装时勾选"Add to PATH" +3. 重启命令提示符 + +### ❌ 端口 3000 被占用 + +**解决:** +```cmd +# 查找占用端口的进程 +netstat -ano | findstr :3000 + +# 杀死进程(替换 为实际进程 ID) +taskkill /PID /F + +# 或修改端口 +# 编辑 .env 文件,设置 PORT=3001 +``` + +### ❌ 权限不足 + +**解决:** +- 右键 `start.bat` → "以管理员身份运行" +- 或右键命令提示符 → "以管理员身份运行" + +### ❌ 依赖安装失败 + +**解决:** +```cmd +# 使用淘宝镜像 +npm config set registry https://registry.npmmirror.com + +# 清除缓存重试 +npm cache clean --force +npm install +``` + +### ❌ 防火墙阻止访问 + +**解决:** +1. 控制面板 → Windows Defender 防火墙 +2. 高级设置 → 入站规则 → 新建规则 +3. 端口 → TCP → 3000 → 允许连接 + +--- + +## 📝 配置飞书应用 + +### 1️⃣ 创建飞书应用 + +1. 访问 https://open.feishu.cn/ +2. 登录企业管理员账号 +3. 创建自建应用 + +### 2️⃣ 配置权限 + +添加权限: +- `im:message` - 消息 +- `im:file` - 文件 + +### 3️⃣ 配置事件订阅 + +- **请求地址**:`https://your-domain.com/feishu/event` +- **订阅事件**:`im.message.receive_v1` + +> ⚠️ **注意**:飞书需要能访问你的服务器 +> - 云服务器:配置安全组开放 3000 端口 +> - 本地测试:使用内网穿透(ngrok、cpolar) + +### 4️⃣ 获取配置信息 + +从飞书开放平台复制以下信息到 `.env`: +- App ID +- App Secret +- Verification Token +- Encrypt Key + +--- + +## ✅ 验证部署 + +### 1️⃣ 检查服务 + +浏览器访问:http://localhost:3000/health + +应看到:`{"status":"ok",...}` + +### 2️⃣ 测试飞书 + +1. 在飞书中找到机器人 +2. 发送任意消息 +3. 应收到交互式卡片 + +### 3️⃣ 测试上传 + +``` +/upload --original default +[附上一个文件] +``` + +--- + +## 📞 获取帮助 + +- 查看日志:控制台输出或 `logs\` 目录 +- 检查配置:`.env` 和 `config\qiniu-config.json` +- 重启服务:关闭 `start.bat` 窗口,重新运行 + +--- + +**🍙 祝你使用愉快!** diff --git a/config/qiniu-config.json.example b/config/qiniu-config.json.example new file mode 100644 index 0000000..d6f52dd --- /dev/null +++ b/config/qiniu-config.json.example @@ -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. 修改此配置文件或发送 /config set 命令" + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..7efe711 --- /dev/null +++ b/package.json @@ -0,0 +1,28 @@ +{ + "name": "qiniu-feishu-bot", + "version": "1.0.0", + "description": "七牛云文件上传飞书机器人 - 独立部署版本", + "main": "src/index.js", + "scripts": { + "start": "node src/index.js", + "dev": "node --watch src/index.js", + "docker:build": "docker build -t qiniu-feishu-bot .", + "docker:run": "docker run -d --name qiniu-bot -p 3000:3000 --env-file .env qiniu-feishu-bot" + }, + "keywords": [ + "qiniu", + "feishu", + "upload", + "bot" + ], + "author": "饭团 🍙", + "license": "MIT", + "dependencies": { + "axios": "^1.6.0", + "dotenv": "^16.3.1", + "express": "^4.18.2" + }, + "engines": { + "node": ">=18.0.0" + } +} diff --git a/src/cards/config-card.js b/src/cards/config-card.js new file mode 100644 index 0000000..dec993a --- /dev/null +++ b/src/cards/config-card.js @@ -0,0 +1,185 @@ +/** + * 配置交互卡片模板 + */ + +class ConfigCard { + static create(configData) { + const bucketsText = Object.entries(configData.buckets || {}) + .map(([name, bucket]) => { + return `**🪣 [${name}]**\n` + + `• 存储桶:${bucket.bucket}\n` + + `• 区域:${bucket.region}\n` + + `• 域名:${bucket.domain}\n` + + `• AccessKey: ${bucket.accessKey}`; + }) + .join('\n\n'); + + return { + "config": { + "wide_screen_mode": true + }, + "header": { + "template": "grey", + "title": { + "content": "⚙️ 七牛云配置", + "tag": "plain_text" + } + }, + "elements": [ + { + "tag": "div", + "text": { + "content": bucketsText || '暂无配置,请先添加存储桶配置。', + "tag": "lark_md" + } + }, + { + "tag": "hr" + }, + { + "tag": "div", + "text": { + "content": "**💡 修改配置**\n\n使用命令:`/config set `\n\n示例:\n`/config set default.domain https://new-cdn.com`", + "tag": "lark_md" + } + }, + { + "tag": "action", + "actions": [ + { + "tag": "button", + "text": { + "content": "📤 上传文件", + "tag": "plain_text" + }, + "type": "primary", + "value": { + "action": "upload_file", + "type": "upload" + } + }, + { + "tag": "button", + "text": { + "content": "❓ 帮助", + "tag": "plain_text" + }, + "type": "default", + "value": { + "action": "help", + "type": "help" + } + } + ] + } + ] + }; + } + + static createEditForm(bucketName) { + return { + "config": { + "wide_screen_mode": true + }, + "header": { + "template": "blue", + "title": { + "content": `✏️ 编辑配置 - ${bucketName}`, + "tag": "plain_text" + } + }, + "elements": [ + { + "tag": "input", + "label": { + "content": "AccessKey", + "tag": "plain_text" + }, + "placeholder": { + "content": "请输入七牛云 AccessKey", + "tag": "plain_text" + }, + "name": "access_key" + }, + { + "tag": "input", + "label": { + "content": "SecretKey", + "tag": "plain_text" + }, + "placeholder": { + "content": "请输入七牛云 SecretKey", + "tag": "plain_text" + }, + "name": "secret_key" + }, + { + "tag": "input", + "label": { + "content": "存储桶名称", + "tag": "plain_text" + }, + "placeholder": { + "content": "例如:my-bucket", + "tag": "plain_text" + }, + "name": "bucket_name" + }, + { + "tag": "input", + "label": { + "content": "区域", + "tag": "plain_text" + }, + "placeholder": { + "content": "z0/z1/z2/na0/as0", + "tag": "plain_text" + }, + "name": "region" + }, + { + "tag": "input", + "label": { + "content": "CDN 域名", + "tag": "plain_text" + }, + "placeholder": { + "content": "https://cdn.example.com", + "tag": "plain_text" + }, + "name": "domain" + }, + { + "tag": "action", + "actions": [ + { + "tag": "button", + "text": { + "content": "💾 保存", + "tag": "plain_text" + }, + "type": "primary", + "value": { + "action": "save_config", + "bucket": bucketName + } + }, + { + "tag": "button", + "text": { + "content": "❌ 取消", + "tag": "plain_text" + }, + "type": "default", + "value": { + "action": "cancel" + } + } + ] + } + ] + }; + } +} + +module.exports = { ConfigCard }; diff --git a/src/cards/upload-card.js b/src/cards/upload-card.js new file mode 100644 index 0000000..1660808 --- /dev/null +++ b/src/cards/upload-card.js @@ -0,0 +1,230 @@ +/** + * 上传交互卡片模板 + */ + +class UploadCard { + static create() { + return { + "config": { + "wide_screen_mode": true + }, + "header": { + "template": "blue", + "title": { + "content": "🍙 七牛云上传助手", + "tag": "plain_text" + } + }, + "elements": [ + { + "tag": "div", + "text": { + "content": "**📤 快速上传文件到七牛云**\n\n支持指定路径、多存储桶、自动 CDN 刷新", + "tag": "lark_md" + } + }, + { + "tag": "hr" + }, + { + "tag": "div", + "text": { + "content": "**💡 使用方式**\n\n• 直接发送文件给我\n• 或使用命令:`/upload [路径] [存储桶]`\n• 支持 `--original` 使用原文件名", + "tag": "lark_md" + } + }, + { + "tag": "hr" + }, + { + "tag": "action", + "actions": [ + { + "tag": "button", + "text": { + "content": "📎 选择文件上传", + "tag": "plain_text" + }, + "type": "primary", + "value": { + "action": "upload_file", + "type": "upload" + } + }, + { + "tag": "button", + "text": { + "content": "⚙️ 配置", + "tag": "plain_text" + }, + "type": "default", + "value": { + "action": "config", + "type": "config" + } + }, + { + "tag": "button", + "text": { + "content": "❓ 帮助", + "tag": "plain_text" + }, + "type": "default", + "value": { + "action": "help", + "type": "help" + } + } + ] + } + ] + }; + } + + static createUploading(fileName) { + return { + "config": { + "wide_screen_mode": true + }, + "header": { + "template": "blue", + "title": { + "content": "📤 上传中...", + "tag": "plain_text" + } + }, + "elements": [ + { + "tag": "div", + "text": { + "content": `**文件:** ${fileName}\n\n正在上传到七牛云,请稍候...`, + "tag": "lark_md" + } + }, + { + "tag": "progress_bar", + "tag": "div", + "text": { + "content": "上传进度", + "tag": "plain_text" + }, + "value": 50, + "color": "blue" + } + ] + }; + } + + static createSuccess(result) { + return { + "config": { + "wide_screen_mode": true + }, + "header": { + "template": "green", + "title": { + "content": "✅ 上传成功", + "tag": "plain_text" + } + }, + "elements": [ + { + "tag": "div", + "text": { + "content": `**📦 文件:** ${result.key}\n\n` + + `**🔗 链接:** [点击查看](${result.url})\n\n` + + `**🪣 存储桶:** ${result.bucket || 'default'}`, + "tag": "lark_md" + } + }, + { + "tag": "hr" + }, + { + "tag": "action", + "actions": [ + { + "tag": "button", + "text": { + "content": "🔗 复制链接", + "tag": "plain_text" + }, + "type": "default", + "value": { + "action": "copy_link", + "url": result.url + } + }, + { + "tag": "button", + "text": { + "content": "📤 继续上传", + "tag": "plain_text" + }, + "type": "primary", + "value": { + "action": "upload_file", + "type": "upload" + } + } + ] + } + ] + }; + } + + static createError(error) { + return { + "config": { + "wide_screen_mode": true + }, + "header": { + "template": "red", + "title": { + "content": "❌ 上传失败", + "tag": "plain_text" + } + }, + "elements": [ + { + "tag": "div", + "text": { + "content": `**错误信息:**\n${error.message}\n\n请检查配置或联系管理员。`, + "tag": "lark_md" + } + }, + { + "tag": "action", + "actions": [ + { + "tag": "button", + "text": { + "content": "🔄 重试", + "tag": "plain_text" + }, + "type": "primary", + "value": { + "action": "upload_file", + "type": "upload" + } + }, + { + "tag": "button", + "text": { + "content": "⚙️ 检查配置", + "tag": "plain_text" + }, + "type": "default", + "value": { + "action": "config", + "type": "config" + } + } + ] + } + ] + }; + } +} + +module.exports = { UploadCard }; diff --git a/src/feishu-api.js b/src/feishu-api.js new file mode 100644 index 0000000..04f25c4 --- /dev/null +++ b/src/feishu-api.js @@ -0,0 +1,151 @@ +/** + * 飞书 API 封装 + */ + +const axios = require('axios'); +const fs = require('fs'); +const path = require('path'); +const https = require('https'); + +class FeishuAPI { + constructor() { + this.appId = process.env.FEISHU_APP_ID; + this.appSecret = process.env.FEISHU_APP_SECRET; + this.baseURL = 'https://open.feishu.cn/open-apis'; + this.tokenCache = null; + this.tokenExpiry = 0; + } + + // 获取访问令牌 + async getAccessToken() { + if (this.tokenCache && Date.now() < this.tokenExpiry) { + return this.tokenCache; + } + + try { + const response = await axios.post( + `${this.baseURL}/auth/v3/tenant_access_token/internal`, + { + app_id: this.appId, + app_secret: this.appSecret + }, + { + headers: { 'Content-Type': 'application/json' } + } + ); + + const { tenant_access_token, expire } = response.data; + + if (response.data.code !== 0) { + throw new Error(`获取 token 失败:${response.data.msg}`); + } + + this.tokenCache = tenant_access_token; + this.tokenExpiry = Date.now() + (expire - 300) * 1000; // 提前 5 分钟过期 + + return tenant_access_token; + } catch (error) { + throw new Error(`飞书 API 错误:${error.message}`); + } + } + + // 发送文本消息 + async sendMessage(chatId, payload) { + const token = await this.getAccessToken(); + + try { + const response = await axios.post( + `${this.baseURL}/im/v1/messages`, + { + receive_id: chatId, + msg_type: payload.msg_type, + content: payload.content + }, + { + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + } + } + ); + + if (response.data.code !== 0) { + throw new Error(`发送消息失败:${response.data.msg}`); + } + + return response.data; + } catch (error) { + throw new Error(`飞书消息发送失败:${error.message}`); + } + } + + // 发送卡片消息 + async sendCard(chatId, card) { + return this.sendMessage(chatId, { + msg_type: 'interactive', + content: JSON.stringify(card) + }); + } + + // 下载文件 + async downloadFile(fileKey) { + const token = await this.getAccessToken(); + const tempDir = path.join(process.cwd(), 'temp'); + + if (!fs.existsSync(tempDir)) { + fs.mkdirSync(tempDir, { recursive: true }); + } + + const tempFile = path.join(tempDir, `feishu_${Date.now()}_${fileKey}`); + + return new Promise((resolve, reject) => { + const url = `${this.baseURL}/im/v1/files/${fileKey}/download`; + + https.get(url, { + headers: { + 'Authorization': `Bearer ${token}` + } + }, (res) => { + if (res.statusCode !== 200) { + reject(new Error(`下载失败:${res.statusCode}`)); + return; + } + + const file = fs.createWriteStream(tempFile); + res.pipe(file); + file.on('finish', () => { + file.close(); + resolve(tempFile); + }); + }).on('error', reject); + }); + } + + // 回复消息 + async replyMessage(messageId, payload) { + const token = await this.getAccessToken(); + + try { + const response = await axios.post( + `${this.baseURL}/im/v1/messages`, + { + reply_id: messageId, + msg_type: payload.msg_type, + content: payload.content + }, + { + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + } + } + ); + + return response.data; + } catch (error) { + throw new Error(`飞书回复失败:${error.message}`); + } + } +} + +module.exports = { FeishuAPI }; diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..f7c7012 --- /dev/null +++ b/src/index.js @@ -0,0 +1,311 @@ +#!/usr/bin/env node + +/** + * 七牛云上传 - 飞书独立应用 + * + * 功能: + * 1. 监听飞书消息事件 + * 2. 支持交互式卡片上传 + * 3. 支持命令触发上传 + * 4. 配置管理 + */ + +require('dotenv').config(); +const express = require('express'); +const crypto = require('crypto'); +const path = require('path'); +const fs = require('fs'); + +const { FeishuAPI } = require('./feishu-api'); +const { QiniuUploader } = require('./qiniu-uploader'); +const { UploadCard } = require('./cards/upload-card'); +const { ConfigCard } = require('./cards/config-card'); + +const app = express(); +const PORT = process.env.PORT || 3000; + +// 中间件 +app.use(express.json()); + +// 日志 +function log(...args) { + const timestamp = new Date().toISOString(); + console.log(`[${timestamp}]`, ...args); +} + +// ============ 飞书事件处理 ============ + +async function handleFeishuEvent(req, res) { + const event = req.body; + const headers = req.headers; + + log('收到飞书事件:', event.type); + + // URL 验证 + if (event.type === 'url_verification') { + log('✅ URL 验证请求'); + res.json({ challenge: event.challenge }); + return; + } + + // 验证签名 + const timestamp = headers['x-feishu-request-timestamp']; + const nonce = headers['x-feishu-request-nonce']; + const signature = headers['x-feishu-request-signature']; + + if (!verifySignature(timestamp, nonce, signature)) { + log('❌ 签名验证失败'); + res.status(401).send('Invalid signature'); + return; + } + + // 处理消息事件 + if (event.type === 'im.message.receive_v1') { + await handleMessage(event); + } + + res.status(200).send('OK'); +} + +function verifySignature(timestamp, nonce, signature) { + const encryptKey = process.env.FEISHU_ENCRYPT_KEY; + if (!encryptKey) return true; + + const arr = [encryptKey, timestamp, nonce]; + arr.sort(); + const str = arr.join(''); + const hash = crypto.createHash('sha1').update(str).digest('hex'); + + return hash === signature; +} + +// ============ 消息处理 ============ + +async function handleMessage(event) { + try { + const message = event.message; + const content = JSON.parse(message.content); + const text = content.text || ''; + const chatId = message.chat_id; + const senderId = message.sender?.sender_id?.user_id || message.sender?.sender_id?.open_id; + + log(`处理消息:${chatId} - ${text.substring(0, 50)}`); + + // 初始化 API + const feishu = new FeishuAPI(); + const uploader = new QiniuUploader(); + + // 卡片交互回调 + if (event.type === 'im.message.receive_v1' && content.interaction?.type) { + await handleCardInteraction(event, feishu, uploader); + return; + } + + // 命令处理 + if (text.startsWith('/upload') || text.startsWith('/u ')) { + await handleUploadCommand(message, content, feishu, uploader); + } else if (text.startsWith('/config') || text.startsWith('/qc ')) { + await handleConfigCommand(message, content, feishu, uploader); + } else if (text.startsWith('/help') || text.startsWith('/qh')) { + await handleHelpCommand(message, feishu); + } else { + // 默认回复交互卡片 + await sendWelcomeCard(chatId, feishu); + } + + } catch (error) { + log('❌ 消息处理失败:', error.message); + } +} + +async function handleCardInteraction(event, feishu, uploader) { + const interaction = event.message.content.interaction; + const chatId = event.message.chat_id; + const action = interaction.value?.action; + + log('卡片交互:', action); + + switch (action) { + case 'upload_file': + await feishu.sendMessage(chatId, { + msg_type: 'text', + content: { text: '📎 请发送要上传的文件,我会自动处理~' } + }); + break; + + case 'config': + const configData = await uploader.listConfig(); + const configCard = ConfigCard.create(configData); + await feishu.sendCard(chatId, configCard); + break; + + case 'help': + await handleHelpCommand(event.message, feishu); + break; + } +} + +async function handleUploadCommand(message, content, feishu, uploader) { + const chatId = message.chat_id; + const attachments = message.attachments || []; + + // 解析命令参数 + const text = content.text || ''; + const args = text.replace(/^\/(upload|u)\s*/i, '').trim().split(/\s+/); + 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; + } + + if (attachments.length === 0) { + await feishu.sendMessage(chatId, { + msg_type: 'text', + content: { + text: '❌ 请附上要上传的文件\n\n💡 使用示例:\n/upload /config/test/file.txt default\n[附上文件]' + } + }); + return; + } + + const attachment = attachments[0]; + const fileKey = attachment.file_key; + const fileName = attachment.file_name; + + try { + // 下载文件 + await feishu.sendMessage(chatId, { + msg_type: 'text', + content: { text: `📥 正在下载:${fileName}` } + }); + + const tempFile = await feishu.downloadFile(fileKey); + + // 确定目标路径 + let key = targetPath; + if (!key || useOriginal) { + key = fileName; + } else if (key.startsWith('/')) { + key = key.substring(1); + } + + // 上传到七牛云 + await feishu.sendMessage(chatId, { + msg_type: 'text', + content: { text: `📤 上传中:${key} → ${bucket}` } + }); + + const result = await uploader.upload(tempFile, key, bucket); + + // 刷新 CDN + await uploader.refreshCDN(bucket, key); + + // 回复结果 + await feishu.sendMessage(chatId, { + msg_type: 'text', + content: { + text: `✅ 上传成功!\n\n` + + `📦 文件:${key}\n` + + `🔗 链接:${result.url}\n` + + `💾 原文件:${fileName}\n` + + `🪣 存储桶:${bucket}` + } + }); + + // 清理临时文件 + fs.unlinkSync(tempFile); + + } catch (error) { + log('上传失败:', error.message); + await feishu.sendMessage(chatId, { + msg_type: 'text', + content: { text: `❌ 上传失败:${error.message}` } + }); + } +} + +async function handleConfigCommand(message, content, feishu, uploader) { + const chatId = message.chat_id; + const text = content.text || ''; + const args = text.replace(/^\/(config|qc)\s*/i, '').trim().split(/\s+/); + const subCommand = args[0]; + + try { + if (subCommand === 'list' || !subCommand) { + const configData = await uploader.listConfig(); + const configCard = ConfigCard.create(configData); + await feishu.sendCard(chatId, configCard); + } else if (subCommand === 'set') { + const [keyPath, value] = args.slice(1); + if (!keyPath || !value) { + throw new Error('用法:/config set '); + } + await uploader.setConfigValue(keyPath, value); + await feishu.sendMessage(chatId, { + msg_type: 'text', + content: { text: `✅ 已设置 ${keyPath} = ${value}` } + }); + } else { + throw new Error(`未知命令:${subCommand}`); + } + } catch (error) { + await feishu.sendMessage(chatId, { + msg_type: 'text', + content: { text: `❌ 配置失败:${error.message}` } + }); + } +} + +async function handleHelpCommand(message, feishu) { + const helpText = ` +🍙 七牛云上传 - 使用帮助 + +📤 上传文件: + /upload [目标路径] [存储桶名] + /upload --original [存储桶名] + + 示例: + /upload /config/test/file.txt default + /upload --original default + +⚙️ 配置管理: + /config list # 查看配置 + /config set # 修改配置 + +💡 提示: +- 直接发送文件给我也会收到上传卡片 +- 支持多存储桶配置 +- 上传同名文件会自动覆盖 +`; + + await feishu.sendMessage(message.chat_id, { + msg_type: 'text', + content: { text: helpText } + }); +} + +async function sendWelcomeCard(chatId, feishu) { + const card = UploadCard.create(); + await feishu.sendCard(chatId, card); +} + +// ============ 路由 ============ + +app.post('/feishu/event', handleFeishuEvent); + +// 健康检查 +app.get('/health', (req, res) => { + res.json({ status: 'ok', timestamp: new Date().toISOString() }); +}); + +// ============ 启动服务 ============ + +app.listen(PORT, () => { + log(`🚀 七牛云上传机器人启动`); + log(`📍 端口:${PORT}`); + log(`🔗 事件地址:https://your-domain.com/feishu/event`); +}); diff --git a/src/qiniu-uploader.js b/src/qiniu-uploader.js new file mode 100644 index 0000000..36c6c13 --- /dev/null +++ b/src/qiniu-uploader.js @@ -0,0 +1,253 @@ +/** + * 七牛云上传工具 + */ + +const fs = require('fs'); +const path = require('path'); +const crypto = require('crypto'); +const https = require('https'); +const http = require('http'); + +class QiniuUploader { + constructor() { + this.configPath = process.env.QINIU_CONFIG_PATH || + path.join(process.cwd(), 'config', 'qiniu-config.json'); + this.config = this.loadConfig(); + } + + // 加载配置 + loadConfig() { + if (!fs.existsSync(this.configPath)) { + // 创建默认配置 + const defaultConfig = { + buckets: { + default: { + accessKey: process.env.QINIU_ACCESS_KEY || 'YOUR_ACCESS_KEY', + secretKey: process.env.QINIU_SECRET_KEY || 'YOUR_SECRET_KEY', + bucket: process.env.QINIU_BUCKET || 'your-bucket', + region: process.env.QINIU_REGION || 'z0', + domain: process.env.QINIU_DOMAIN || 'https://your-cdn.com' + } + } + }; + + const dir = path.dirname(this.configPath); + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } + fs.writeFileSync(this.configPath, JSON.stringify(defaultConfig, null, 2)); + + return defaultConfig; + } + + return JSON.parse(fs.readFileSync(this.configPath, 'utf-8')); + } + + // 列出配置 + async listConfig() { + const config = this.loadConfig(); + const buckets = {}; + + for (const [name, bucket] of Object.entries(config.buckets)) { + buckets[name] = { + accessKey: this.maskKey(bucket.accessKey), + secretKey: this.maskKey(bucket.secretKey), + bucket: bucket.bucket, + region: bucket.region, + domain: bucket.domain + }; + } + + return { buckets }; + } + + // 设置配置值 + async setConfigValue(keyPath, value) { + const config = this.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; + + fs.writeFileSync(this.configPath, JSON.stringify(config, null, 2)); + this.config = config; + } + + // 上传文件 + async upload(localFile, key, bucketName = 'default') { + const bucketConfig = this.config.buckets[bucketName]; + + if (!bucketConfig) { + throw new Error(`存储桶 "${bucketName}" 不存在`); + } + + const { accessKey, secretKey, bucket, region, domain } = bucketConfig; + + // 生成上传凭证 + const uploadToken = this.generateUploadToken(accessKey, secretKey, bucket, key); + + // 获取上传端点 + const uploadEndpoint = this.getUploadEndpoint(region); + + // 读取文件 + const fileContent = fs.readFileSync(localFile); + const fileName = path.basename(localFile); + + // 构建 multipart/form-data + const boundary = '----WebKitFormBoundary' + Math.random().toString(36).substring(2); + + 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 result = await this.httpRequest(`${uploadEndpoint}/`, { + method: 'POST', + headers: { + 'Content-Type': `multipart/form-data; boundary=----${boundary}`, + 'Content-Length': bodyBuffer.length + } + }, bodyBuffer); + + if (result.status !== 200) { + throw new Error(`上传失败:${JSON.stringify(result.data)}`); + } + + return { + key: result.data.key, + hash: result.data.hash, + url: `${domain}/${key}` + }; + } + + // 刷新 CDN + async refreshCDN(bucketName, key) { + const bucketConfig = this.config.buckets[bucketName]; + if (!bucketConfig) return; + + const { accessKey, secretKey, domain } = bucketConfig; + const fileUrl = `${domain}/${key}`; + + const body = JSON.stringify({ urls: [fileUrl] }); + const accessToken = this.generateAccessToken(accessKey, secretKey, 'POST', '/v2/tune/refresh', body); + + await this.httpRequest('https://fusion.qiniuapi.com/v2/tune/refresh', { + method: 'POST', + headers: { + 'Host': 'fusion.qiniuapi.com', + 'Content-Type': 'application/json', + 'Content-Length': body.length, + 'Authorization': accessToken + } + }, body); + } + + // 生成上传凭证 + generateUploadToken(accessKey, secretKey, bucket, key) { + const deadline = Math.floor(Date.now() / 1000) + 3600; + const scope = key ? `${bucket}:${key}` : bucket; + + const putPolicy = { + scope: scope, + deadline: deadline + }; + + const encodedPolicy = this.urlSafeBase64(JSON.stringify(putPolicy)); + const encodedSignature = this.urlSafeBase64(this.hmacSha1(encodedPolicy, secretKey)); + + return `${accessKey}:${encodedSignature}:${encodedPolicy}`; + } + + // 生成 CDN 刷新令牌 + generateAccessToken(accessKey, secretKey, method, path, body) { + const signData = `${method} ${path}\nHost: fusion.qiniuapi.com\nContent-Type: application/json\n\n${body}`; + const signature = this.hmacSha1(signData, secretKey); + const encodedSign = this.urlSafeBase64(signature); + return `Qiniu ${accessKey}:${encodedSign}`; + } + + // 获取上传端点 + 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']; + } + + // 工具函数 + hmacSha1(data, secret) { + return crypto.createHmac('sha1', secret).update(data).digest(); + } + + urlSafeBase64(data) { + return Buffer.from(data).toString('base64') + .replace(/\+/g, '-') + .replace(/\//g, '_'); + } + + maskKey(key) { + if (!key || key.length < 8) return '***'; + return key.substring(0, 4) + '...' + key.substring(key.length - 4); + } + + 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(); + }); + } +} + +module.exports = { QiniuUploader }; diff --git a/start.bat b/start.bat new file mode 100644 index 0000000..f1b3efc --- /dev/null +++ b/start.bat @@ -0,0 +1,73 @@ +@echo off +REM 七牛云上传机器人 - Windows 启动脚本 + +echo ======================================== +echo 🍙 七牛云上传机器人 - Windows 版 +echo ======================================== +echo. + +REM 检查 Node.js +node --version >nul 2>&1 +if errorlevel 1 ( + echo ❌ 未检测到 Node.js,请先安装 Node.js 18+ + echo 下载地址:https://nodejs.org/ + pause + exit /b 1 +) + +echo ✅ Node.js 已安装 + +REM 检查配置文件 +if not exist ".env" ( + echo ⚠️ 未找到 .env 文件,正在创建... + copy .env.example .env >nul + echo. + echo ⚠️ 请先编辑 .env 文件,填入飞书和七牛云配置 + echo 按任意键打开 .env 文件... + pause + notepad .env + echo. + echo 配置完成后按任意键继续... + pause +) + +REM 检查七牛云配置 +if not exist "config\qiniu-config.json" ( + echo ⚠️ 未找到七牛云配置文件,正在创建... + if not exist "config" mkdir config + copy config\qiniu-config.json.example config\qiniu-config.json >nul + echo. + echo ⚠️ 请先编辑 config\qiniu-config.json 文件,填入七牛云配置 + echo 按任意键打开配置文件... + pause + notepad config\qiniu-config.json + echo. + echo 配置完成后按任意键继续... + pause +) + +REM 检查依赖 +if not exist "node_modules" ( + echo 📦 正在安装依赖... + call npm install + if errorlevel 1 ( + echo ❌ 依赖安装失败 + pause + exit /b 1 + ) + echo ✅ 依赖安装完成 + echo. +) + +REM 启动服务 +echo. +echo 🚀 正在启动服务... +echo 📍 监听端口:3000 +echo 📍 事件地址:https://your-domain.com/feishu/event +echo. +echo 按 Ctrl+C 停止服务 +echo. + +node src\index.js + +pause diff --git a/start.sh b/start.sh new file mode 100644 index 0000000..2e5af10 --- /dev/null +++ b/start.sh @@ -0,0 +1,69 @@ +#!/bin/bash + +# 七牛云上传机器人 - Linux/macOS 启动脚本 + +echo "========================================" +echo " 🍙 七牛云上传机器人" +echo "========================================" +echo "" + +# 检查 Node.js +if ! command -v node &> /dev/null; then + echo "❌ 未检测到 Node.js,请先安装 Node.js 18+" + echo "访问:https://nodejs.org/" + exit 1 +fi + +echo "✅ Node.js 已安装:$(node --version)" + +# 检查配置文件 +if [ ! -f ".env" ]; then + echo "⚠️ 未找到 .env 文件,正在创建..." + cp .env.example .env + echo "" + echo "⚠️ 请先编辑 .env 文件,填入飞书和七牛云配置" + echo "按任意键打开 .env 文件..." + read -p "" + ${EDITOR:-nano} .env + echo "" + echo "配置完成后按任意键继续..." + read -p "" +fi + +# 检查七牛云配置 +if [ ! -f "config/qiniu-config.json" ]; then + echo "⚠️ 未找到七牛云配置文件,正在创建..." + mkdir -p config + cp config/qiniu-config.json.example config/qiniu-config.json + echo "" + echo "⚠️ 请先编辑 config/qiniu-config.json 文件,填入七牛云配置" + echo "按任意键打开配置文件..." + read -p "" + ${EDITOR:-nano} config/qiniu-config.json + echo "" + echo "配置完成后按任意键继续..." + read -p "" +fi + +# 检查依赖 +if [ ! -d "node_modules" ]; then + echo "📦 正在安装依赖..." + npm install + if [ $? -ne 0 ]; then + echo "❌ 依赖安装失败" + exit 1 + fi + echo "✅ 依赖安装完成" + echo "" +fi + +# 启动服务 +echo "" +echo "🚀 正在启动服务..." +echo "📍 监听端口:3000" +echo "📍 事件地址:https://your-domain.com/feishu/event" +echo "" +echo "按 Ctrl+C 停止服务" +echo "" + +node src/index.js