#!/usr/bin/env node /** * 七牛云上传 - 飞书独立应用 v5 * 简化流程:上传配置 + 一键上传 */ require('dotenv').config(); const express = require('express'); const path = require('path'); const fs = require('fs'); const { FeishuAPI } = require('./feishu-api'); const { QiniuUploader } = require('./qiniu-uploader'); const app = express(); const PORT = process.env.PORT || 3030; app.use(express.json()); function log(...args) { const timestamp = new Date().toISOString(); console.log(`[${timestamp}]`, ...args); } // 加载完整配置 function loadFullConfig() { const configPath = path.join(process.cwd(), 'config', 'qiniu-config.json'); return JSON.parse(fs.readFileSync(configPath, 'utf-8')); } // 用户状态存储 const userStates = {}; function getUserState(chatId) { return userStates[chatId] || {}; } function setUserState(chatId, state) { userStates[chatId] = { ...userStates[chatId], ...state }; setTimeout(() => { delete userStates[chatId]; }, 5 * 60 * 1000); } function clearUserState(chatId) { delete userStates[chatId]; } async function handleFeishuEvent(req, res) { const event = req.body; log('📩 收到飞书请求'); let decryptedEvent = event; if (event.encrypt) { try { const { decrypt } = require('@larksuiteoapi/node-sdk'); decryptedEvent = decrypt(event.encrypt, process.env.FEISHU_ENCRYPT_KEY); } catch (e) { res.status(500).send('Decrypt error'); return; } } const eventType = decryptedEvent.event_type || decryptedEvent.header?.event_type || decryptedEvent.type; if (eventType === 'url_verification') { res.json({ challenge: decryptedEvent.challenge || event.challenge }); return; } if (eventType === 'im.message.receive_v1') { await handleMessage(decryptedEvent); } if (eventType === 'card.action.trigger') { await handleCardInteraction(decryptedEvent); res.status(200).json({}); return; } res.status(200).send('OK'); } async function handleMessage(event) { const messageData = event.event?.message || event.message; if (!messageData) return; const messageContent = JSON.parse(messageData.content); const text = messageContent.text || ''; const chatId = messageData.chat_id; const messageType = messageData.message_type || 'text'; const feishu = new FeishuAPI(); const uploader = new QiniuUploader(); if (text.startsWith('/upload') || text.startsWith('/u ')) { // 显示上传配置卡片 await showProfileCard(chatId, feishu, uploader); } else if (text.startsWith('/config') || text.startsWith('/qc ')) { await handleConfigCommandV2(messageData, messageContent, feishu, uploader); } else if (text.startsWith('/path')) { await handlePathCommandV2(messageData, messageContent, feishu); } else if (text.startsWith('/help') || text.startsWith('/qh')) { await handleHelpCommandV2(chatId, feishu); } else if (messageType === 'file' || messageContent.file_key) { // 收到文件,显示配置选择 await handleFileReceived(messageData, feishu, uploader); } else { await sendWelcomeCard(chatId, feishu); } } async function handleCardInteraction(event) { const eventData = event.event; const actionData = eventData?.action; const message = eventData?.message; if (!actionData) return; const action = actionData.value?.action; let chatId = message?.chat_id || actionData.value?.chat_id; if (!chatId && eventData?.operator?.open_id) { chatId = eventData.operator.open_id; } log('卡片交互:', action, 'chatId:', chatId); const feishu = new FeishuAPI(); const uploader = new QiniuUploader(); switch (action) { case 'start_upload': await showProfileCard(chatId, feishu, uploader); break; case 'select_profile': { const { profile_name, bucket, path: upload_path } = actionData.value; log('📋 选择上传配置:', profile_name); setUserState(chatId, { profile_name, bucket, upload_path }); await feishu.sendMessage(chatId, { msg_type: 'text', content: { text: `✅ 已选择配置:**${profile_name}**\n\n📤 请发送文件,或点击"📎 选择文件上传"` } }); break; } case 'upload_with_profile': { // 从配置卡片直接上传(需要先发送文件) const { profile_name, bucket, path: upload_path } = actionData.value; const state = getUserState(chatId); if (!state.file_key) { await feishu.sendMessage(chatId, { msg_type: 'text', content: { text: '📎 请先发送文件' } }); return; } await doUpload(chatId, feishu, uploader, { file_key: state.file_key, file_name: state.file_name, message_id: state.message_id, bucket, upload_path, path_label: profile_name }); clearUserState(chatId); break; } case 'confirm_upload': { const { file_key, file_name, message_id, bucket, upload_path, path_label } = actionData.value; await doUpload(chatId, feishu, uploader, { file_key, file_name, message_id, bucket, upload_path, path_label }); clearUserState(chatId); break; } case 'cancel': clearUserState(chatId); await feishu.sendMessage(chatId, { msg_type: 'text', content: { text: '❌ 已取消' } }); break; case 'config': { const configData = await uploader.listConfig(); await feishu.sendCard(chatId, createConfigCard(configData)); break; } case 'help': await handleHelpCommandV2(chatId, feishu); break; } } // 显示上传配置卡片 async function showProfileCard(chatId, feishu, uploader) { const fullConfig = loadFullConfig(); const profiles = fullConfig.uploadProfiles || {}; const profileButtons = Object.entries(profiles).map(([name, config]) => ({ tag: 'button', text: { tag: 'plain_text', content: name }, type: 'primary', value: { action: 'select_profile', profile_name: name, bucket: config.bucket, path: config.path || '' } })); const card = { config: { wide_screen_mode: true }, header: { template: 'blue', title: { content: '📤 选择上传配置', tag: 'plain_text' } }, elements: [ { tag: 'div', text: { tag: 'lark_md', content: '**选择一个上传配置,然后发送文件:**' } }, { tag: 'action', actions: profileButtons }, { tag: 'hr' }, { tag: 'div', text: { tag: 'lark_md', content: '💡 **提示:**\n• 选择配置后发送文件\n• 或直接发送文件后选择配置' } }, { tag: 'action', actions: [ { 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); } // 处理文件接收 async function handleFileReceived(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; // 保存文件信息到状态 setUserState(chatId, { file_key: fileKey, file_name: fileName, message_id: messageId }); const state = getUserState(chatId); // 如果已选择配置,显示确认卡片 if (state.bucket && state.upload_path !== undefined) { await showConfirmCard(chatId, feishu, { file_key: fileKey, file_name: fileName, message_id: messageId, bucket: state.bucket, upload_path: state.upload_path, path_label: state.profile_name || '自定义' }); } else { // 未选择配置,显示配置选择卡片 const fullConfig = loadFullConfig(); const profiles = fullConfig.uploadProfiles || {}; const profileButtons = Object.entries(profiles).map(([name, config]) => ({ tag: 'button', text: { tag: 'plain_text', content: `${name}` }, type: 'primary', value: { action: 'confirm_upload', file_key: fileKey, file_name: fileName, message_id: messageId, chat_id: chatId, bucket: config.bucket, upload_path: config.path || '', path_label: name } })); 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\n**选择配置后确认上传:**` } }, { tag: 'action', actions: profileButtons }, { tag: 'hr' }, { tag: 'div', text: { tag: 'lark_md', content: '💡 点击配置按钮直接上传' } }, { tag: 'action', actions: [ { tag: 'button', text: { tag: 'plain_text', content: '❌ 取消' }, type: 'default', value: { action: 'cancel' } } ] } ] }; await feishu.sendCard(chatId, card); } } // 显示确认卡片 async function showConfirmCard(chatId, feishu, info) { const { file_name, bucket, upload_path, path_label } = info; let targetKey = upload_path || file_name; if (targetKey.startsWith('/')) targetKey = targetKey.substring(1); const card = { config: { wide_screen_mode: true }, header: { template: 'green', title: { content: '✅ 确认上传', tag: 'plain_text' } }, elements: [ { tag: 'div', text: { tag: 'lark_md', content: `**文件:** ${file_name}\n**配置:** ${path_label}\n**存储桶:** ${bucket}\n**路径:** ${targetKey || '(原文件名)'}\n\n点击"确认上传"开始上传` } }, { tag: 'action', actions: [ { tag: 'button', text: { tag: 'plain_text', content: '✅ 确认上传' }, type: 'primary', value: { action: 'confirm_upload', file_key: info.file_key, file_name: info.file_name, message_id: info.message_id, chat_id: chatId, bucket, upload_path, path_label } }, { tag: 'button', text: { tag: 'plain_text', content: '❌ 取消' }, type: 'default', value: { action: 'cancel' } } ] } ] }; await feishu.sendCard(chatId, card); } // 执行上传 async function doUpload(chatId, feishu, uploader, info) { const { file_key, file_name, message_id, bucket, upload_path, path_label } = info; let targetKey = upload_path || file_name; if (targetKey.startsWith('/')) targetKey = targetKey.substring(1); log('📤 开始上传:', file_name, '→', bucket, '/', targetKey); try { await feishu.sendMessage(chatId, { msg_type: 'text', content: { text: `📥 正在下载:${file_name}` } }); const tempFile = await feishu.downloadFile(file_key, message_id, chatId); log('✅ 文件下载完成:', tempFile); await feishu.sendMessage(chatId, { msg_type: 'text', content: { text: `📤 上传中:${targetKey} → ${bucket}` } }); const result = await uploader.upload(tempFile, targetKey, bucket); await uploader.refreshCDN(bucket, targetKey); await feishu.sendMessage(chatId, { msg_type: 'text', content: { text: `✅ 上传成功!\n\n` + `📦 文件:${targetKey}\n` + `🔗 链接:${result.url}\n` + `💾 原文件:${file_name}\n` + `🪣 存储桶:${bucket}\n` + `📁 配置:${path_label}` } }); fs.unlinkSync(tempFile); log('🗑️ 临时文件已清理'); } catch (error) { log('❌ 上传失败:', error.message); await feishu.sendMessage(chatId, { msg_type: 'text', content: { text: `❌ 上传失败:${error.message}` } }); } } 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+/); const subCommand = args[0]; try { if (subCommand === 'list' || !subCommand) { const configData = await uploader.listConfig(); await feishu.sendCard(chatId, createConfigCard(configData)); } else if (subCommand === 'set') { const [keyPath, value] = args.slice(1); await uploader.setConfigValue(keyPath, value); await feishu.sendMessage(chatId, { msg_type: 'text', content: { text: `✅ 已设置 ${keyPath} = ${value}` } }); } } catch (error) { await feishu.sendMessage(chatId, { msg_type: 'text', content: { text: `❌ 配置失败:${error.message}` } }); } } async function handlePathCommandV2(message, content, feishu) { const chatId = message.chat_id; const text = content.text || ''; const args = text.replace(/^\/path\s*/i, '').trim().split(/\s+/); const subCommand = args[0]; const configPath = path.join(process.cwd(), 'config', 'qiniu-config.json'); try { const fullConfig = loadFullConfig(); if (subCommand === 'list') { const paths = fullConfig.uploadPaths || {}; let pathText = '**预设路径列表:**\n\n'; for (const [name, pathValue] of Object.entries(paths)) { pathText += `• **${name}**: ${pathValue || '(原文件名)'}\n`; } await feishu.sendMessage(chatId, { msg_type: 'text', content: { text: pathText } }); } else if (subCommand === 'add') { const name = args[1]; const pathValue = args[2]; fullConfig.uploadPaths[name] = pathValue; fs.writeFileSync(configPath, JSON.stringify(fullConfig, null, 2)); await feishu.sendMessage(chatId, { msg_type: 'text', content: { text: `✅ 已添加预设路径:**${name}** → ${pathValue}` } }); } } catch (error) { await feishu.sendMessage(chatId, { msg_type: 'text', content: { text: `❌ 路径管理失败:${error.message}` } }); } } async function handleHelpCommandV2(chatId, feishu) { await feishu.sendMessage(chatId, { msg_type: 'text', content: { text: ` 🍙 七牛云上传 - 使用帮助 📤 上传方式: **方式 1:选择配置 → 发送文件** 1. 发送 /upload 2. 选择上传配置 3. 发送文件 4. 确认上传 **方式 2:发送文件 → 选择配置** 1. 直接发送文件 2. 选择上传配置 3. 确认上传 ⚙️ 配置管理: /config list - 查看配置 /path list - 查看预设路径 /path add <名称> <路径> - 添加路径 💡 提示: - 上传配置在 config/qiniu-config.json 中配置 - 支持多存储桶 - 上传同名文件会自动覆盖 ` } }); } async function sendWelcomeCard(chatId, feishu) { 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• /upload - 选择配置上传\n• 直接发送文件\n\n**命令:**\n• /config - 配置\n• /path - 路径\n• /help - 帮助' } }, { tag: 'action', actions: [ { tag: 'button', text: { tag: 'plain_text', content: '📤 上传文件' }, type: 'primary', value: { action: 'start_upload' } }, { tag: 'button', text: { tag: 'plain_text', content: '⚙️ 配置' }, type: 'default', value: { action: 'config' } } ] } ] }; 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(), port: PORT }); }); app.listen(PORT, () => { log(`🚀 七牛云上传机器人启动 (v5 - 简化流程)`); log(`📍 端口:${PORT}`); });