From 6deae77a1570c3ac9e981574e3c1917260b3e89f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A5=AD=E5=9B=A2?= Date: Fri, 6 Mar 2026 10:05:34 +0800 Subject: [PATCH] =?UTF-8?q?v4=20-=20=E4=B8=BB=E5=8A=A8=E8=A7=A6=E5=8F=91?= =?UTF-8?q?=20+=20=E7=A1=AE=E8=AE=A4=E4=B8=8A=E4=BC=A0=E6=B5=81=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/index.js | 341 +++++++++++++++++++++++++-------------------------- 1 file changed, 169 insertions(+), 172 deletions(-) diff --git a/src/index.js b/src/index.js index 98da347..8240d9d 100644 --- a/src/index.js +++ b/src/index.js @@ -1,8 +1,8 @@ #!/usr/bin/env node /** - * 七牛云上传 - 飞书独立应用 v3 - * 支持存储桶和路径选择 + * 七牛云上传 - 飞书独立应用 v4 + * 主动触发 + 确认上传流程 */ require('dotenv').config(); @@ -24,13 +24,13 @@ function log(...args) { 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) { @@ -39,10 +39,7 @@ function getUserState(chatId) { function setUserState(chatId, state) { userStates[chatId] = { ...userStates[chatId], ...state }; - // 5 分钟后清除状态 - setTimeout(() => { - delete userStates[chatId]; - }, 5 * 60 * 1000); + setTimeout(() => { delete userStates[chatId]; }, 5 * 60 * 1000); } function clearUserState(chatId) { @@ -107,9 +104,12 @@ async function handleMessage(event) { const uploader = new QiniuUploader(); if (text.startsWith('/upload') || text.startsWith('/u ')) { + // 主动触发上传流程 + log('📤 用户主动触发上传流程'); + setUserState(chatId, { uploadFlow: true }); await feishu.sendMessage(chatId, { msg_type: 'text', - content: { text: '📎 请发送要上传的文件(直接发送文件即可)' } + content: { text: '📎 请发送要上传的文件' } }); } else if (text.startsWith('/config') || text.startsWith('/qc ')) { await handleConfigCommandV2(messageData, messageContent, feishu, uploader); @@ -118,15 +118,14 @@ async function handleMessage(event) { } else if (text.startsWith('/help') || text.startsWith('/qh')) { await handleHelpCommandV2(chatId, feishu); } else if (messageType === 'file' || messageContent.file_key) { - // 检查是否有已保存的配置 const state = getUserState(chatId); - if (state.bucket || state.upload_path) { - log('🔍 收到文件消息 - 使用已保存的配置'); - await handleFileUploadWithState(messageData, feishu, uploader, state); - clearUserState(chatId); - } else { - log('🔍 收到文件消息 - 发送配置卡片'); + if (state.uploadFlow) { + // 用户上传流程中的文件 + log('🔍 用户上传流程 - 发送选择卡片'); await handleFileReceivedWithCard(messageData, feishu, uploader); + } else { + // 普通消息,显示欢迎卡片 + await sendWelcomeCard(chatId, feishu); } } else { await sendWelcomeCard(chatId, feishu); @@ -161,18 +160,68 @@ async function handleCardInteraction(event) { const uploader = new QiniuUploader(); switch (action) { + case 'start_upload': + // 用户点击欢迎卡片的"上传文件" + log('📤 用户点击上传文件按钮'); + setUserState(chatId, { uploadFlow: true }); + await feishu.sendMessage(chatId, { + msg_type: 'text', + content: { text: '📎 请发送要上传的文件' } + }); + break; + + case 'set_bucket': { + const { bucket, chat_id } = actionData.value; + const stateChatId = chat_id || chatId; + log('🪣 选择存储桶:', bucket); + + setUserState(stateChatId, { bucket }); + + // 检查是否已选择路径 + const state = getUserState(stateChatId); + if (state.upload_path) { + // 已选择路径,显示确认卡片 + await showConfirmCard(stateChatId, feishu, uploader, state); + } else { + await feishu.sendMessage(chatId, { + msg_type: 'text', + content: { text: `✅ 已选择存储桶:**${bucket}**\n\n请继续选择路径` } + }); + } + break; + } + + case 'set_path': { + const { upload_path, path_label, chat_id } = actionData.value; + const stateChatId = chat_id || chatId; + const pathDesc = path_label || '原文件名'; + log('📁 选择路径:', pathDesc); + + setUserState(stateChatId, { upload_path, path_label }); + + // 检查是否已选择存储桶 + const state = getUserState(stateChatId); + if (state.bucket) { + // 已选择存储桶,显示确认卡片 + await showConfirmCard(stateChatId, feishu, uploader, state); + } else { + await feishu.sendMessage(chatId, { + msg_type: 'text', + content: { text: `✅ 已选择路径:**${pathDesc}**\n\n请继续选择存储桶` } + }); + } + break; + } + case 'confirm_upload': { - const { file_key, file_name, message_id, bucket, upload_path } = actionData.value; + const { file_key, file_name, message_id, chat_id, bucket, upload_path, path_label } = actionData.value; const targetBucket = bucket || 'default'; let targetKey = upload_path || file_name; + const pathDesc = path_label || '原文件名'; + if (targetKey.startsWith('/')) targetKey = targetKey.substring(1); - log('📤 开始上传文件:', file_name, 'bucket:', targetBucket, 'path:', targetKey); - - if (!chatId) { - log('❌ 缺少 chatId'); - return; - } + log('✅ 确认上传:', file_name, 'bucket:', targetBucket, 'path:', targetKey); await feishu.sendMessage(chatId, { msg_type: 'text', @@ -198,12 +247,14 @@ async function handleCardInteraction(event) { `📦 文件:${targetKey}\n` + `🔗 链接:${result.url}\n` + `💾 原文件:${file_name}\n` + - `🪣 存储桶:${targetBucket}` + `🪣 存储桶:${targetBucket}\n` + + `📁 路径:${pathDesc}` } }); fs.unlinkSync(tempFile); log('🗑️ 临时文件已清理:', tempFile); + clearUserState(chatId); } catch (error) { log('上传失败:', error.message); @@ -215,42 +266,13 @@ async function handleCardInteraction(event) { break; } - case 'set_bucket': { - const { bucket } = actionData.value; - log('🪣 选择存储桶:', bucket); - - setUserState(chatId, { bucket }); - - await feishu.sendMessage(chatId, { - msg_type: 'text', - content: { text: `✅ 已选择存储桶:**${bucket}**\n\n请重新发送文件开始上传(配置 5 分钟内有效)` } - }); - break; - } - - case 'set_path': { - const { upload_path, path_label } = actionData.value; - const pathDesc = path_label || '原文件名'; - log('📁 选择路径:', pathDesc); - - setUserState(chatId, { upload_path, path_label }); - - await feishu.sendMessage(chatId, { - msg_type: 'text', - content: { text: `✅ 已选择路径:**${pathDesc}**\n\n请重新发送文件开始上传(配置 5 分钟内有效)` } - }); - break; - } - case 'cancel_upload': - log('取消上传,chatId:', chatId); - if (chatId) { - await feishu.sendMessage(chatId, { - msg_type: 'text', - content: { text: '❌ 已取消上传' } - }); - log('✅ 取消回复已发送'); - } + log('❌ 取消上传'); + clearUserState(chatId); + await feishu.sendMessage(chatId, { + msg_type: 'text', + content: { text: '❌ 已取消上传' } + }); break; case 'config': { @@ -269,6 +291,7 @@ async function handleCardInteraction(event) { } } +// 显示选择卡片(存储桶 + 路径) async function handleFileReceivedWithCard(messageData, feishu, uploader) { const chatId = messageData.chat_id; const messageId = messageData.message_id; @@ -279,11 +302,10 @@ async function handleFileReceivedWithCard(messageData, feishu, uploader) { if (!fileKey) return; - log('📎 收到文件,发送配置卡片:', fileName); + log('📎 收到文件,发送选择卡片:', fileName); // 获取存储桶列表 - const uploader2 = new QiniuUploader(); - const configData = await uploader2.listConfig(); + const configData = await uploader.listConfig(); const bucketNames = Object.keys(configData.buckets); // 获取预设路径 @@ -297,7 +319,8 @@ async function handleFileReceivedWithCard(messageData, feishu, uploader) { type: 'primary', value: { action: 'set_bucket', - bucket: name + bucket: name, + chat_id: chatId } })); @@ -309,36 +332,15 @@ async function handleFileReceivedWithCard(messageData, feishu, uploader) { value: { action: 'set_path', upload_path: pathValue, - path_label: label + path_label: label, + chat_id: chatId, + // 保存文件信息用于确认卡片 + file_key: fileKey, + file_name: fileName, + message_id: messageId } })); - // 构建快速上传按钮(使用默认配置) - const quickUploadButtons = [ - { - 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, - bucket: 'default', - upload_path: '' - } - }, - { - tag: 'button', - text: { tag: 'plain_text', content: '❌ 取消' }, - type: 'default', - value: { - action: 'cancel_upload' - } - } - ]; - const card = { config: { wide_screen_mode: true }, header: { @@ -378,88 +380,82 @@ async function handleFileReceivedWithCard(messageData, feishu, uploader) { tag: 'div', text: { tag: 'lark_md', - content: `**3️⃣ 开始上传:**` - } - }, - { - tag: 'action', - actions: quickUploadButtons - }, - { - tag: 'hr' - }, - { - tag: 'div', - text: { - tag: 'lark_md', - content: `💡 **提示:**\n• 先选择存储桶和路径\n• 点击"使用默认配置上传"开始\n• 或使用命令:/upload /路径/文件名 存储桶名` + content: `💡 **提示:**\n• 选择存储桶和路径后,会显示确认卡片\n• 点击"确认上传"才开始上传` } } ] }; - log('发送卡片到 chatId:', chatId); + // 保存文件信息到状态 + setUserState(chatId, { + file_key: fileKey, + file_name: fileName, + message_id: messageId + }); + await feishu.sendCard(chatId, card); } -// 使用已保存的状态直接上传文件 -async function handleFileUploadWithState(messageData, feishu, uploader, state) { - 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; - - const targetBucket = state.bucket || 'default'; - let targetKey = state.upload_path || fileName; - const pathLabel = state.path_label || '原文件名'; +// 显示确认卡片 +async function showConfirmCard(chatId, feishu, uploader, state) { + const fileKey = state.file_key; + const fileName = state.file_name; + const messageId = state.message_id; + const bucket = state.bucket || 'default'; + const upload_path = state.upload_path || ''; + const path_label = state.path_label || '原文件名'; + let targetKey = upload_path || fileName; if (targetKey.startsWith('/')) targetKey = targetKey.substring(1); - log('📤 使用已保存配置上传:', fileName, 'bucket:', targetBucket, 'path:', targetKey); + log('📋 显示确认卡片:', fileName, 'bucket:', bucket, 'path:', targetKey); - try { - await feishu.sendMessage(chatId, { - msg_type: 'text', - content: { text: `📥 正在下载:${fileName}` } - }); - - const tempFile = await feishu.downloadFile(fileKey, messageId, chatId); - log('✅ 文件下载完成:', tempFile); - - await feishu.sendMessage(chatId, { - msg_type: 'text', - content: { text: `📤 上传中:${targetKey} → ${targetBucket} (路径:${pathLabel})` } - }); - - const result = await uploader.upload(tempFile, targetKey, targetBucket); - await uploader.refreshCDN(targetBucket, targetKey); - - await feishu.sendMessage(chatId, { - msg_type: 'text', - content: { - text: `✅ 上传成功!\n\n` + - `📦 文件:${targetKey}\n` + - `🔗 链接:${result.url}\n` + - `💾 原文件:${fileName}\n` + - `🪣 存储桶:${targetBucket}\n` + - `📁 路径:${pathLabel}` + const card = { + config: { wide_screen_mode: true }, + header: { + template: 'green', + title: { content: '✅ 确认上传', tag: 'plain_text' } + }, + elements: [ + { + tag: 'div', + text: { + tag: 'lark_md', + content: `**文件:** ${fileName}\n**存储桶:** ${bucket}\n**路径:** ${path_label}\n**目标:** ${targetKey}\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, + bucket: bucket, + upload_path: upload_path, + path_label: path_label + } + }, + { + tag: 'button', + text: { tag: 'plain_text', content: '❌ 取消' }, + type: 'default', + value: { + action: 'cancel_upload' + } + } + ] } - }); - - fs.unlinkSync(tempFile); - log('🗑️ 临时文件已清理:', tempFile); - - } catch (error) { - log('上传失败:', error.message); - await feishu.sendMessage(chatId, { - msg_type: 'text', - content: { text: `❌ 上传失败:${error.message}` } - }); - } + ] + }; + + await feishu.sendCard(chatId, card); } async function handleConfigCommandV2(message, content, feishu, uploader) { @@ -504,7 +500,6 @@ async function handlePathCommandV2(message, content, feishu) { const fullConfig = loadFullConfig(); if (subCommand === 'list' || !subCommand) { - // 列出所有预设路径 const paths = fullConfig.uploadPaths || {}; let pathText = '**预设路径列表:**\n\n'; for (const [name, pathValue] of Object.entries(paths)) { @@ -515,9 +510,8 @@ async function handlePathCommandV2(message, content, feishu) { content: { text: pathText } }); } else if (subCommand === 'add') { - // 添加预设路径:/path add <名称> <路径> if (args.length < 3) { - throw new Error('用法:/path add <名称> <路径>\n示例:/path add ipa /ipa/gamehall.ipa'); + throw new Error('用法:/path add <名称> <路径>'); } const name = args[1]; const pathValue = args[2]; @@ -531,7 +525,6 @@ async function handlePathCommandV2(message, content, feishu) { content: { text: `✅ 已添加预设路径:**${name}** → ${pathValue}` } }); } else if (subCommand === 'remove' || subCommand === 'del') { - // 删除预设路径:/path remove <名称> if (args.length < 2) { throw new Error('用法:/path remove <名称>'); } @@ -565,9 +558,11 @@ async function handleHelpCommandV2(chatId, feishu) { 🍙 七牛云上传 - 使用帮助 📤 上传文件: - 1. 直接发送文件给我 - 2. 选择存储桶和路径 - 3. 点击"使用默认配置上传" + 1. 发送 /upload 命令 + 2. 或点击"📎 上传文件"按钮 + 3. 发送文件 + 4. 选择存储桶和路径 + 5. 点击"确认上传" ⚙️ 配置管理: /config list - 查看配置 @@ -582,10 +577,6 @@ async function handleHelpCommandV2(chatId, feishu) { - 支持多存储桶配置 - 支持预设路径 - 上传同名文件会自动覆盖 - -示例: - /path add backup /backup/ - /path remove backup ` } }); } @@ -602,12 +593,18 @@ async function sendWelcomeCard(chatId, feishu) { tag: 'div', text: { tag: 'lark_md', - content: '你好!我是七牛云上传机器人。\n\n**使用方式:**\n• 直接发送文件给我\n• 选择存储桶和路径\n• 确认上传\n\n**命令:**\n• /config - 查看配置\n• /help - 查看帮助' + content: '你好!我是七牛云上传机器人。\n\n**使用方式:**\n• 发送 /upload 命令\n• 或点击下方"📎 上传文件"\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: '⚙️ 配置' }, @@ -671,7 +668,7 @@ setInterval(cleanupTempFiles, 60 * 60 * 1000); log('⏰ 临时文件清理任务已启动(每小时执行一次)'); app.listen(PORT, () => { - log(`🚀 七牛云上传机器人启动 (v3 - 支持存储桶和路径选择)`); + log(`🚀 七牛云上传机器人启动 (v4 - 主动触发 + 确认上传)`); log(`📍 端口:${PORT}`); setTimeout(cleanupTempFiles, 5000); });