Initial commit: 七牛云上传飞书机器人
功能: - 飞书交互卡片支持 - 七牛云文件上传 - 自动 CDN 刷新 - 多存储桶配置 - 跨平台部署(Linux/macOS/Windows) - Docker 支持
This commit is contained in:
16
.env.example
Normal file
16
.env.example
Normal file
@@ -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
|
||||||
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
node_modules/
|
||||||
|
.env
|
||||||
|
config/qiniu-config.json
|
||||||
|
temp/
|
||||||
|
*.log
|
||||||
|
.DS_Store
|
||||||
432
DEPLOY.md
Normal file
432
DEPLOY.md
Normal file
@@ -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 <repo-url> 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 <repo-url> 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 <repo-url> 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 <PID>
|
||||||
|
|
||||||
|
# Windows
|
||||||
|
netstat -ano | findstr :3000
|
||||||
|
taskkill /PID <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
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**🍙 祝你部署顺利!**
|
||||||
20
Dockerfile
Normal file
20
Dockerfile
Normal file
@@ -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"]
|
||||||
211
README.md
Normal file
211
README.md
Normal file
@@ -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
|
||||||
284
WINDOWS.md
Normal file
284
WINDOWS.md
Normal file
@@ -0,0 +1,284 @@
|
|||||||
|
# Windows 快速开始指南
|
||||||
|
|
||||||
|
🪟 **Windows 用户专用部署指南**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 方法一:双击启动(最简单)
|
||||||
|
|
||||||
|
### 1️⃣ 下载项目
|
||||||
|
|
||||||
|
- 从 GitHub 下载 ZIP 文件
|
||||||
|
- 或使用 Git:`git clone <repo-url>`
|
||||||
|
|
||||||
|
### 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
|
||||||
|
|
||||||
|
# 杀死进程(替换 <PID> 为实际进程 ID)
|
||||||
|
taskkill /PID <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` 窗口,重新运行
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**🍙 祝你使用愉快!**
|
||||||
17
config/qiniu-config.json.example
Normal file
17
config/qiniu-config.json.example
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"buckets": {
|
||||||
|
"default": {
|
||||||
|
"accessKey": "YOUR_ACCESS_KEY_HERE",
|
||||||
|
"secretKey": "YOUR_SECRET_KEY_HERE",
|
||||||
|
"bucket": "your-bucket-name",
|
||||||
|
"region": "z0",
|
||||||
|
"domain": "https://your-cdn-domain.com"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"_comment": {
|
||||||
|
"region": "区域代码:z0=华东,z1=华北,z2=华南,na0=北美,as0=东南亚",
|
||||||
|
"setup": "1. 获取七牛 AccessKey/SecretKey: https://portal.qiniu.com/user/key",
|
||||||
|
"setup2": "2. 创建存储桶并配置 CDN 域名",
|
||||||
|
"setup3": "3. 修改此配置文件或发送 /config set 命令"
|
||||||
|
}
|
||||||
|
}
|
||||||
28
package.json
Normal file
28
package.json
Normal file
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
185
src/cards/config-card.js
Normal file
185
src/cards/config-card.js
Normal file
@@ -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 <key> <value>`\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 };
|
||||||
230
src/cards/upload-card.js
Normal file
230
src/cards/upload-card.js
Normal file
@@ -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 };
|
||||||
151
src/feishu-api.js
Normal file
151
src/feishu-api.js
Normal file
@@ -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 };
|
||||||
311
src/index.js
Normal file
311
src/index.js
Normal file
@@ -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 <key> <value>');
|
||||||
|
}
|
||||||
|
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 <key> <value> # 修改配置
|
||||||
|
|
||||||
|
💡 提示:
|
||||||
|
- 直接发送文件给我也会收到上传卡片
|
||||||
|
- 支持多存储桶配置
|
||||||
|
- 上传同名文件会自动覆盖
|
||||||
|
`;
|
||||||
|
|
||||||
|
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`);
|
||||||
|
});
|
||||||
253
src/qiniu-uploader.js
Normal file
253
src/qiniu-uploader.js
Normal file
@@ -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 };
|
||||||
73
start.bat
Normal file
73
start.bat
Normal file
@@ -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
|
||||||
69
start.sh
Normal file
69
start.sh
Normal file
@@ -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
|
||||||
Reference in New Issue
Block a user