Initial commit: 七牛云上传飞书机器人

功能:
- 飞书交互卡片支持
- 七牛云文件上传
- 自动 CDN 刷新
- 多存储桶配置
- 跨平台部署(Linux/macOS/Windows)
- Docker 支持
This commit is contained in:
饭团
2026-03-05 14:22:26 +08:00
commit b00567762f
15 changed files with 2286 additions and 0 deletions

311
src/index.js Normal file
View 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`);
});