From f7776aaf6930b6e83f8e318929862677783aa1df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A5=AD=E5=9B=A2?= Date: Fri, 6 Mar 2026 08:38:52 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=85=20v2=20=E7=89=88=E6=9C=AC=20-=20?= =?UTF-8?q?=E9=A3=9E=E4=B9=A6=E6=96=87=E4=BB=B6=E4=B8=8A=E4=BC=A0=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=E4=BF=AE=E5=A4=8D=E5=AE=8C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 主要修复: 1. 使用飞书 SDK im.messageResource.get API 下载文件(和 OpenClaw 一致) 2. 修复 sendMessage 方法,自动判断 receive_id_type(oc_=chat_id, ou_=open_id) 3. 修复 sendCard 方法,传递正确的 receive_id_type 参数 4. 修复事件类型识别,支持飞书 v2 schema(header.event_type) 5. 添加临时文件清理机制(每小时清理 1 小时前的文件) 6. 完善卡片交互(确认上传/取消按钮) 7. 完善错误处理和日志记录 功能: - ✅ 飞书文件接收和卡片回复 - ✅ 卡片交互(确认/取消) - ✅ 七牛云上传(支持多存储桶) - ✅ CDN 自动刷新 - ✅ 临时文件自动清理 - ✅ 配置管理命令(/config) 配置文件: - config/qiniu-config.json - 七牛云配置 - .env - 飞书应用配置 --- pm2.config.cjs | 18 ++ src/feishu-api.js | 86 ++++--- src/index.js | 570 ++++++++++++++++++++++++---------------------- 3 files changed, 369 insertions(+), 305 deletions(-) create mode 100644 pm2.config.cjs diff --git a/pm2.config.cjs b/pm2.config.cjs new file mode 100644 index 0000000..709d232 --- /dev/null +++ b/pm2.config.cjs @@ -0,0 +1,18 @@ +module.exports = { + apps: [{ + name: 'qiniu-bot', + script: './src/index.js', + instances: 1, + autorestart: true, + watch: false, + max_memory_restart: '500M', + env: { + NODE_ENV: 'production', + PORT: 3030 + }, + error_file: './logs/error.log', + out_file: './logs/out.log', + log_date_format: 'YYYY-MM-DD HH:mm:ss', + merge_logs: true + }] +}; diff --git a/src/feishu-api.js b/src/feishu-api.js index 04f25c4..6758a18 100644 --- a/src/feishu-api.js +++ b/src/feishu-api.js @@ -1,11 +1,18 @@ /** - * 飞书 API 封装 + * 飞书 API 封装 v2 - 使用飞书 SDK */ const axios = require('axios'); const fs = require('fs'); const path = require('path'); const https = require('https'); +const lark = require('@larksuiteoapi/node-sdk'); + +// 日志函数 +function log(...args) { + const timestamp = new Date().toISOString(); + console.log(`[${timestamp}] [FeishuAPI]`, ...args); +} class FeishuAPI { constructor() { @@ -14,6 +21,12 @@ class FeishuAPI { this.baseURL = 'https://open.feishu.cn/open-apis'; this.tokenCache = null; this.tokenExpiry = 0; + + // 创建飞书 SDK 客户端 + this.client = new lark.Client({ + appId: this.appId, + appSecret: this.appSecret + }); } // 获取访问令牌 @@ -50,16 +63,26 @@ class FeishuAPI { } // 发送文本消息 - async sendMessage(chatId, payload) { + async sendMessage(chatId, payload, receiveIdType = 'chat_id') { const token = await this.getAccessToken(); try { + // 判断 receive_id_type:oc_开头的是 chat_id,ou_开头的是 open_id + let idType = receiveIdType; + if (chatId.startsWith('oc_')) { + idType = 'chat_id'; + } else if (chatId.startsWith('ou_')) { + idType = 'open_id'; + } + + log('发送消息到 chatId:', chatId, 'receive_id_type:', idType, 'msg_type:', payload.msg_type); + const response = await axios.post( - `${this.baseURL}/im/v1/messages`, + `${this.baseURL}/im/v1/messages?receive_id_type=${idType}`, { receive_id: chatId, msg_type: payload.msg_type, - content: payload.content + content: typeof payload.content === 'string' ? payload.content : JSON.stringify(payload.content) }, { headers: { @@ -69,56 +92,63 @@ class FeishuAPI { } ); + log('消息发送响应:', response.data.code, response.data.msg || 'ok'); + if (response.data.code !== 0) { throw new Error(`发送消息失败:${response.data.msg}`); } return response.data; } catch (error) { + log('消息发送错误:', error.message); throw new Error(`飞书消息发送失败:${error.message}`); } } // 发送卡片消息 async sendCard(chatId, card) { + // 判断 receive_id_type + const receiveIdType = chatId.startsWith('oc_') ? 'chat_id' : 'open_id'; return this.sendMessage(chatId, { msg_type: 'interactive', content: JSON.stringify(card) - }); + }, receiveIdType); } - // 下载文件 - async downloadFile(fileKey) { - const token = await this.getAccessToken(); + // 下载文件 - 使用飞书 SDK 的 messageResource API(OpenClaw 使用的方式) + async downloadFile(fileKey, messageId, chatId) { 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}`); + const tempFile = path.join(tempDir, `feishu_${Date.now()}_${path.basename(fileKey)}`); - return new Promise((resolve, reject) => { - const url = `${this.baseURL}/im/v1/files/${fileKey}/download`; + log('下载文件:', fileKey, 'messageId:', messageId); + + try { + // 使用 SDK 的 im.messageResource.get 方法 + const result = await this.client.im.messageResource.get({ + path: { + message_id: messageId, + file_key: fileKey + }, + params: { + type: 'file' + } + }); - https.get(url, { - headers: { - 'Authorization': `Bearer ${token}` - } - }, (res) => { - if (res.statusCode !== 200) { - reject(new Error(`下载失败:${res.statusCode}`)); - return; - } + // SDK 返回一个带有 writeFile 方法的对象 + await result.writeFile(tempFile); + + log('文件下载完成:', tempFile, fs.statSync(tempFile).size, 'bytes'); + return tempFile; - const file = fs.createWriteStream(tempFile); - res.pipe(file); - file.on('finish', () => { - file.close(); - resolve(tempFile); - }); - }).on('error', reject); - }); + } catch (error) { + log('下载错误:', error.message); + throw new Error(`飞书文件下载失败:${error.message}`); + } } // 回复消息 diff --git a/src/index.js b/src/index.js index faf13d8..409a385 100644 --- a/src/index.js +++ b/src/index.js @@ -1,14 +1,7 @@ #!/usr/bin/env node /** - * 七牛云上传 - 飞书独立应用 - * - * 功能: - * 1. 监听飞书消息事件 - * 2. 支持交互式卡片上传 - * 3. 支持命令触发上传 - * 4. 配置管理 - * 5. 支持 HTTP 回调和 WebSocket 长连接两种模式 + * 七牛云上传 - 飞书独立应用 v2 */ require('dotenv').config(); @@ -19,223 +12,260 @@ 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'); - -// 飞书 SDK(WebSocket 模式) -const { Api, eventSubscription } = require('@larksuiteoapi/node-sdk'); const app = express(); const PORT = process.env.PORT || 3030; -// 运行模式:'http' 或 'websocket' -const MODE = (process.env.FEISHU_MODE || 'http').toLowerCase(); - -// 中间件 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('📩 收到飞书请求'); - log('收到飞书事件:', event.type); + let decryptedEvent = event; + if (event.encrypt) { + try { + const { decrypt } = require('@larksuiteoapi/node-sdk'); + decryptedEvent = decrypt(event.encrypt, process.env.FEISHU_ENCRYPT_KEY); + } catch (e) { + log('❌ 解密失败:', e.message); + res.status(500).send('Decrypt error'); + return; + } + } - // URL 验证 - if (event.type === 'url_verification') { - log('✅ URL 验证请求'); - res.json({ challenge: event.challenge }); + // 飞书 v2 schema: event_type 在 header 中 + const eventType = decryptedEvent.event_type || + decryptedEvent.header?.event_type || + decryptedEvent.type; + log('收到飞书事件:', eventType); + log('事件结构:', JSON.stringify({ + has_event_type: !!decryptedEvent.event_type, + has_header: !!decryptedEvent.header, + header_event_type: decryptedEvent.header?.event_type, + has_type: !!decryptedEvent.type + }).substring(0, 300)); + + if (eventType === 'url_verification') { + res.json({ challenge: decryptedEvent.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 (eventType === 'im.message.receive_v1') { + await handleMessage(decryptedEvent); } - // 处理消息事件 - if (event.type === 'im.message.receive_v1') { - await handleMessage(event); + if (eventType === 'card.action.trigger') { + await handleCardInteraction(decryptedEvent); + res.status(200).json({}); + return; } 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); + const messageData = event.event?.message || event.message; + if (!messageData) { + log('❌ 未找到消息数据'); return; } - // 命令处理 + const messageContent = JSON.parse(messageData.content); + const text = messageContent.text || ''; + const chatId = messageData.chat_id; + const messageType = messageData.message_type || 'text'; + + log(`处理消息:${chatId} - 类型:${messageType}`); + + const feishu = new FeishuAPI(); + const uploader = new QiniuUploader(); + if (text.startsWith('/upload') || text.startsWith('/u ')) { - await handleUploadCommand(message, content, feishu, uploader); + await feishu.sendMessage(chatId, { + msg_type: 'text', + content: { text: '📎 请发送要上传的文件(直接发送文件即可)' } + }); } else if (text.startsWith('/config') || text.startsWith('/qc ')) { - await handleConfigCommand(message, content, feishu, uploader); + await handleConfigCommandV2(messageData, messageContent, feishu, uploader); } else if (text.startsWith('/help') || text.startsWith('/qh')) { - await handleHelpCommand(message, feishu); + await handleHelpCommandV2(chatId, feishu); + } else if (messageType === 'file' || messageContent.file_key) { + log('🔍 收到文件消息 - 发送确认卡片'); + await handleFileReceivedWithCard(messageData, feishu, uploader); } 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; - +async function handleCardInteraction(event) { try { - // 下载文件 - await feishu.sendMessage(chatId, { - msg_type: 'text', - content: { text: `📥 正在下载:${fileName}` } - }); + const eventData = event.event; + const operator = eventData?.operator; + const actionData = eventData?.action; + const message = eventData?.message; - const tempFile = await feishu.downloadFile(fileKey); - - // 确定目标路径 - let key = targetPath; - if (!key || useOriginal) { - key = fileName; - } else if (key.startsWith('/')) { - key = key.substring(1); + if (!actionData) { + log('❌ 卡片交互:缺少 action 数据'); + return; } - // 上传到七牛云 - await feishu.sendMessage(chatId, { - msg_type: 'text', - content: { text: `📤 上传中:${key} → ${bucket}` } - }); + const action = actionData.value?.action; + let chatId = message?.chat_id || actionData.value?.chat_id; - const result = await uploader.upload(tempFile, key, bucket); + if (!chatId && operator?.open_id) { + chatId = operator.open_id; + log('使用 operator open_id 作为 chatId:', chatId); + } - // 刷新 CDN - await uploader.refreshCDN(bucket, key); + log('卡片交互:', action, 'chatId:', chatId); - // 回复结果 - await feishu.sendMessage(chatId, { - msg_type: 'text', - content: { - text: `✅ 上传成功!\n\n` + - `📦 文件:${key}\n` + - `🔗 链接:${result.url}\n` + - `💾 原文件:${fileName}\n` + - `🪣 存储桶:${bucket}` + const feishu = new FeishuAPI(); + const uploader = new QiniuUploader(); + + switch (action) { + case 'confirm_upload': { + const { file_key, file_name, message_id } = actionData.value; + log('📤 开始上传文件:', file_name, 'file_key:', file_key); + + if (!chatId) { + log('❌ 缺少 chatId'); + return; + } + + await feishu.sendMessage(chatId, { + msg_type: 'text', + content: { text: `📥 正在下载:${file_name}` } + }); + + try { + const tempFile = await feishu.downloadFile(file_key, message_id, chatId); + log('✅ 文件下载完成:', tempFile); + + await feishu.sendMessage(chatId, { + msg_type: 'text', + content: { text: `📤 上传中:${file_name} → default` } + }); + + const result = await uploader.upload(tempFile, file_name, 'default'); + await uploader.refreshCDN('default', file_name); + + await feishu.sendMessage(chatId, { + msg_type: 'text', + content: { + text: `✅ 上传成功!\n\n` + + `📦 文件:${file_name}\n` + + `🔗 链接:${result.url}\n` + + `🪣 存储桶:default` + } + }); + + fs.unlinkSync(tempFile); + log('🗑️ 临时文件已清理:', tempFile); + + } catch (error) { + log('上传失败:', error.message); + await feishu.sendMessage(chatId, { + msg_type: 'text', + content: { text: `❌ 上传失败:${error.message}` } + }); + } + break; } - }); - - // 清理临时文件 - fs.unlinkSync(tempFile); - + + case 'cancel_upload': + log('取消上传,chatId:', chatId); + if (chatId) { + await feishu.sendMessage(chatId, { + msg_type: 'text', + content: { text: '❌ 已取消上传' } + }); + log('✅ 取消回复已发送'); + } + break; + + case 'config': { + const configData = await uploader.listConfig(); + const configCard = createConfigCard(configData); + await feishu.sendCard(chatId, configCard); + break; + } + + case 'help': + await handleHelpCommandV2(chatId, feishu); + break; + } } catch (error) { - log('上传失败:', error.message); - await feishu.sendMessage(chatId, { - msg_type: 'text', - content: { text: `❌ 上传失败:${error.message}` } - }); + log('❌ 卡片交互处理失败:', error.message); } } -async function handleConfigCommand(message, content, feishu, uploader) { +async function handleFileReceivedWithCard(messageData, feishu, uploader) { + const chatId = messageData.chat_id; + const messageId = messageData.message_id; + const messageContent = JSON.parse(messageData.content); + + const fileKey = messageContent.file_key; + const fileName = messageContent.file_name; + + if (!fileKey) return; + + log('📎 收到文件,发送确认卡片:', fileName); + + const card = { + config: { wide_screen_mode: true }, + header: { + template: 'blue', + title: { content: '📎 文件上传确认', tag: 'plain_text' } + }, + elements: [ + { + tag: 'div', + text: { + tag: 'lark_md', + content: `**文件名:** ${fileName}\n**存储桶:** default\n\n点击"确认上传"将文件上传到七牛云` + } + }, + { + tag: 'action', + actions: [ + { + tag: 'button', + text: { tag: 'plain_text', content: '✅ 确认上传' }, + type: 'primary', + value: { + action: 'confirm_upload', + file_key: fileKey, + file_name: fileName, + message_id: messageId, + chat_id: chatId + } + }, + { + tag: 'button', + text: { tag: 'plain_text', content: '❌ 取消' }, + type: 'default', + value: { action: 'cancel_upload' } + } + ] + } + ] + }; + + log('发送卡片到 chatId:', chatId); + await feishu.sendCard(chatId, card); +} + +async function handleConfigCommandV2(message, content, feishu, uploader) { const chatId = message.chat_id; const text = content.text || ''; const args = text.replace(/^\/(config|qc)\s*/i, '').trim().split(/\s+/); @@ -244,13 +274,11 @@ async function handleConfigCommand(message, content, feishu, uploader) { try { if (subCommand === 'list' || !subCommand) { const configData = await uploader.listConfig(); - const configCard = ConfigCard.create(configData); + const configCard = createConfigCard(configData); await feishu.sendCard(chatId, configCard); } else if (subCommand === 'set') { const [keyPath, value] = args.slice(1); - if (!keyPath || !value) { - throw new Error('用法:/config set '); - } + if (!keyPath || !value) throw new Error('用法:/config set '); await uploader.setConfigValue(keyPath, value); await feishu.sendMessage(chatId, { msg_type: 'text', @@ -267,128 +295,116 @@ async function handleConfigCommand(message, content, feishu, uploader) { } } -async function handleHelpCommand(message, feishu) { - const helpText = ` +async function handleHelpCommandV2(chatId, feishu) { + await feishu.sendMessage(chatId, { + msg_type: 'text', + content: { text: ` 🍙 七牛云上传 - 使用帮助 📤 上传文件: - /upload [目标路径] [存储桶名] - /upload --original [存储桶名] - - 示例: - /upload /config/test/file.txt default - /upload --original default + 1. 直接发送文件给我 + 2. 点击确认卡片上的"✅ 确认上传" + 3. 等待上传完成,获取下载链接 ⚙️ 配置管理: - /config list # 查看配置 - /config set # 修改配置 + /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(); + const card = { + config: { wide_screen_mode: true }, + header: { + template: 'blue', + title: { content: '🍙 七牛云上传机器人', tag: 'plain_text' } + }, + elements: [ + { + tag: 'div', + text: { + tag: 'lark_md', + content: '你好!我是七牛云上传机器人。\n\n**使用方式:**\n• 直接发送文件给我\n• 点击确认卡片上传\n\n**命令:**\n• /config - 查看配置\n• /help - 查看帮助' + } + }, + { + tag: 'action', + actions: [ + { + tag: 'button', + text: { tag: 'plain_text', content: '📎 上传文件' }, + type: 'primary', + value: { action: 'upload_file' } + }, + { + tag: 'button', + text: { tag: 'plain_text', content: '⚙️ 配置' }, + type: 'default', + value: { action: 'config' } + }, + { + tag: 'button', + text: { tag: 'plain_text', content: '❓ 帮助' }, + type: 'default', + value: { action: 'help' } + } + ] + } + ] + }; await feishu.sendCard(chatId, card); } -// ============ 路由 ============ +function createConfigCard(configData) { + let configText = ''; + for (const [name, bucket] of Object.entries(configData.buckets)) { + configText += `**[${name}]**\nBucket: ${bucket.bucket}\nRegion: ${bucket.region}\nDomain: ${bucket.domain}\n\n`; + } + return { + config: { wide_screen_mode: true }, + header: { + template: 'green', + title: { content: '⚙️ 七牛云配置', tag: 'plain_text' } + }, + elements: [{ tag: 'div', text: { tag: 'lark_md', content: configText || '暂无配置' } }] + }; +} app.post('/feishu/event', handleFeishuEvent); - -// 健康检查 app.get('/health', (req, res) => { - res.json({ - status: 'ok', - timestamp: new Date().toISOString(), - mode: MODE, - port: PORT - }); + res.json({ status: 'ok', timestamp: new Date().toISOString(), port: PORT }); }); -// ============ 启动服务 ============ - -function startHTTPMode() { - app.listen(PORT, () => { - log(`🚀 七牛云上传机器人启动 (HTTP 回调模式)`); - log(`📍 端口:${PORT}`); - log(`🔗 事件地址:http://your-domain.com:${PORT}/feishu/event`); - log(`💡 提示:在飞书开放平台配置事件订阅地址为上述地址`); - }); -} - -function startWebSocketMode() { - log(`🚀 七牛云上传机器人启动 (WebSocket 长连接模式)`); - log(`💡 提示:在飞书开放平台选择 "WebSocket 长连接" 方式`); - - // 创建飞书客户端 - const client = new Api({ - appId: process.env.FEISHU_APP_ID, - appSecret: process.env.FEISHU_APP_SECRET, - }); - - // 创建 WebSocket 长连接 - const ws = eventSubscription({ - appId: process.env.FEISHU_APP_ID, - appSecret: process.env.FEISHU_APP_SECRET, - encryptKey: process.env.FEISHU_ENCRYPT_KEY, - verificationToken: process.env.FEISHU_VERIFICATION_TOKEN, - logLevel: 'info', - }); - - // 监听消息事件 - ws.on('im.message.receive_v1', async (data) => { - log('收到消息事件'); - await handleMessage(data); - }); - - // 监听连接状态 - ws.on('open', () => { - log('✅ WebSocket 连接成功'); - }); - - ws.on('close', () => { - log('❌ WebSocket 连接关闭,5 秒后重连...'); - setTimeout(() => { - try { - ws.start(); - } catch (e) { - log('重连失败:', e.message); +function cleanupTempFiles() { + const tempDir = path.join(process.cwd(), 'temp'); + const maxAgeMs = 60 * 60 * 1000; + if (!fs.existsSync(tempDir)) return; + const now = Date.now(); + let cleaned = 0; + fs.readdirSync(tempDir).forEach(file => { + const filePath = path.join(tempDir, file); + try { + const stats = fs.statSync(filePath); + if (now - stats.mtimeMs > maxAgeMs) { + fs.unlinkSync(filePath); + log('🗑️ 清理过期临时文件:', file); + cleaned++; } - }, 5000); - }); - - ws.on('error', (error) => { - log('❌ WebSocket 错误:', error.message); - }); - - // 启动 WebSocket 连接 - try { - ws.start(); - log('📡 WebSocket 已启动'); - } catch (error) { - log('❌ WebSocket 启动失败:', error.message); - log('💡 请检查飞书配置是否正确'); - } - - // HTTP 服务器仍然运行(用于健康检查) - app.listen(PORT, () => { - log(`📍 健康检查端口:${PORT}`); - log(`🔗 健康检查地址:http://localhost:${PORT}/health`); + } catch (e) {} }); + if (cleaned > 0) log(`✅ 清理完成:${cleaned} 个文件`); } -// 根据配置启动对应模式 -if (MODE === 'websocket') { - startWebSocketMode(); -} else { - startHTTPMode(); -} +setInterval(cleanupTempFiles, 60 * 60 * 1000); +log('⏰ 临时文件清理任务已启动(每小时执行一次)'); + +app.listen(PORT, () => { + log(`🚀 七牛云上传机器人启动 (v2)`); + log(`📍 端口:${PORT}`); + setTimeout(cleanupTempFiles, 5000); +});