v4 - 主动触发 + 确认上传流程

This commit is contained in:
饭团
2026-03-06 10:05:34 +08:00
parent e13b86fcd0
commit 6deae77a15

View File

@@ -1,8 +1,8 @@
#!/usr/bin/env node #!/usr/bin/env node
/** /**
* 七牛云上传 - 飞书独立应用 v3 * 七牛云上传 - 飞书独立应用 v4
* 支持存储桶和路径选择 * 主动触发 + 确认上传流程
*/ */
require('dotenv').config(); require('dotenv').config();
@@ -24,13 +24,13 @@ function log(...args) {
console.log(`[${timestamp}]`, ...args); console.log(`[${timestamp}]`, ...args);
} }
// 加载完整配置(包括预设路径) // 加载完整配置
function loadFullConfig() { function loadFullConfig() {
const configPath = path.join(process.cwd(), 'config', 'qiniu-config.json'); const configPath = path.join(process.cwd(), 'config', 'qiniu-config.json');
return JSON.parse(fs.readFileSync(configPath, 'utf-8')); return JSON.parse(fs.readFileSync(configPath, 'utf-8'));
} }
// 用户临时状态存储(内存) // 用户临时状态存储
const userStates = {}; const userStates = {};
function getUserState(chatId) { function getUserState(chatId) {
@@ -39,10 +39,7 @@ function getUserState(chatId) {
function setUserState(chatId, state) { function setUserState(chatId, state) {
userStates[chatId] = { ...userStates[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) { function clearUserState(chatId) {
@@ -107,9 +104,12 @@ async function handleMessage(event) {
const uploader = new QiniuUploader(); const uploader = new QiniuUploader();
if (text.startsWith('/upload') || text.startsWith('/u ')) { if (text.startsWith('/upload') || text.startsWith('/u ')) {
// 主动触发上传流程
log('📤 用户主动触发上传流程');
setUserState(chatId, { uploadFlow: true });
await feishu.sendMessage(chatId, { await feishu.sendMessage(chatId, {
msg_type: 'text', msg_type: 'text',
content: { text: '📎 请发送要上传的文件(直接发送文件即可)' } content: { text: '📎 请发送要上传的文件' }
}); });
} else if (text.startsWith('/config') || text.startsWith('/qc ')) { } else if (text.startsWith('/config') || text.startsWith('/qc ')) {
await handleConfigCommandV2(messageData, messageContent, feishu, uploader); await handleConfigCommandV2(messageData, messageContent, feishu, uploader);
@@ -118,15 +118,14 @@ async function handleMessage(event) {
} else if (text.startsWith('/help') || text.startsWith('/qh')) { } else if (text.startsWith('/help') || text.startsWith('/qh')) {
await handleHelpCommandV2(chatId, feishu); await handleHelpCommandV2(chatId, feishu);
} else if (messageType === 'file' || messageContent.file_key) { } else if (messageType === 'file' || messageContent.file_key) {
// 检查是否有已保存的配置
const state = getUserState(chatId); const state = getUserState(chatId);
if (state.bucket || state.upload_path) { if (state.uploadFlow) {
log('🔍 收到文件消息 - 使用已保存的配置'); // 用户上传流程中的文件
await handleFileUploadWithState(messageData, feishu, uploader, state); log('🔍 用户上传流程 - 发送选择卡片');
clearUserState(chatId);
} else {
log('🔍 收到文件消息 - 发送配置卡片');
await handleFileReceivedWithCard(messageData, feishu, uploader); await handleFileReceivedWithCard(messageData, feishu, uploader);
} else {
// 普通消息,显示欢迎卡片
await sendWelcomeCard(chatId, feishu);
} }
} else { } else {
await sendWelcomeCard(chatId, feishu); await sendWelcomeCard(chatId, feishu);
@@ -161,18 +160,68 @@ async function handleCardInteraction(event) {
const uploader = new QiniuUploader(); const uploader = new QiniuUploader();
switch (action) { 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': { 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'; const targetBucket = bucket || 'default';
let targetKey = upload_path || file_name; let targetKey = upload_path || file_name;
const pathDesc = path_label || '原文件名';
if (targetKey.startsWith('/')) targetKey = targetKey.substring(1); if (targetKey.startsWith('/')) targetKey = targetKey.substring(1);
log('📤 开始上传文件:', file_name, 'bucket:', targetBucket, 'path:', targetKey); log('✅ 确认上传:', file_name, 'bucket:', targetBucket, 'path:', targetKey);
if (!chatId) {
log('❌ 缺少 chatId');
return;
}
await feishu.sendMessage(chatId, { await feishu.sendMessage(chatId, {
msg_type: 'text', msg_type: 'text',
@@ -198,12 +247,14 @@ async function handleCardInteraction(event) {
`📦 文件:${targetKey}\n` + `📦 文件:${targetKey}\n` +
`🔗 链接:${result.url}\n` + `🔗 链接:${result.url}\n` +
`💾 原文件:${file_name}\n` + `💾 原文件:${file_name}\n` +
`🪣 存储桶:${targetBucket}` `🪣 存储桶:${targetBucket}\n` +
`📁 路径:${pathDesc}`
} }
}); });
fs.unlinkSync(tempFile); fs.unlinkSync(tempFile);
log('🗑️ 临时文件已清理:', tempFile); log('🗑️ 临时文件已清理:', tempFile);
clearUserState(chatId);
} catch (error) { } catch (error) {
log('上传失败:', error.message); log('上传失败:', error.message);
@@ -215,42 +266,13 @@ async function handleCardInteraction(event) {
break; 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': case 'cancel_upload':
log('取消上传chatId:', chatId); log('取消上传');
if (chatId) { clearUserState(chatId);
await feishu.sendMessage(chatId, { await feishu.sendMessage(chatId, {
msg_type: 'text', msg_type: 'text',
content: { text: '❌ 已取消上传' } content: { text: '❌ 已取消上传' }
}); });
log('✅ 取消回复已发送');
}
break; break;
case 'config': { case 'config': {
@@ -269,6 +291,7 @@ async function handleCardInteraction(event) {
} }
} }
// 显示选择卡片(存储桶 + 路径)
async function handleFileReceivedWithCard(messageData, feishu, uploader) { async function handleFileReceivedWithCard(messageData, feishu, uploader) {
const chatId = messageData.chat_id; const chatId = messageData.chat_id;
const messageId = messageData.message_id; const messageId = messageData.message_id;
@@ -279,11 +302,10 @@ async function handleFileReceivedWithCard(messageData, feishu, uploader) {
if (!fileKey) return; if (!fileKey) return;
log('📎 收到文件,发送配置卡片:', fileName); log('📎 收到文件,发送选择卡片:', fileName);
// 获取存储桶列表 // 获取存储桶列表
const uploader2 = new QiniuUploader(); const configData = await uploader.listConfig();
const configData = await uploader2.listConfig();
const bucketNames = Object.keys(configData.buckets); const bucketNames = Object.keys(configData.buckets);
// 获取预设路径 // 获取预设路径
@@ -297,7 +319,8 @@ async function handleFileReceivedWithCard(messageData, feishu, uploader) {
type: 'primary', type: 'primary',
value: { value: {
action: 'set_bucket', action: 'set_bucket',
bucket: name bucket: name,
chat_id: chatId
} }
})); }));
@@ -309,35 +332,14 @@ async function handleFileReceivedWithCard(messageData, feishu, uploader) {
value: { value: {
action: 'set_path', action: 'set_path',
upload_path: pathValue, upload_path: pathValue,
path_label: label path_label: label,
} chat_id: chatId,
})); // 保存文件信息用于确认卡片
// 构建快速上传按钮(使用默认配置)
const quickUploadButtons = [
{
tag: 'button',
text: { tag: 'plain_text', content: '✅ 使用默认配置上传' },
type: 'primary',
value: {
action: 'confirm_upload',
file_key: fileKey, file_key: fileKey,
file_name: fileName, file_name: fileName,
message_id: messageId, 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 = { const card = {
config: { wide_screen_mode: true }, config: { wide_screen_mode: true },
@@ -378,88 +380,82 @@ async function handleFileReceivedWithCard(messageData, feishu, uploader) {
tag: 'div', tag: 'div',
text: { text: {
tag: 'lark_md', tag: 'lark_md',
content: `**3开始上传**` content: `💡 **提示:**\n• 选择存储桶和路径后,会显示确认卡片\n• 点击"确认上传"才开始上传`
}
},
{
tag: 'action',
actions: quickUploadButtons
},
{
tag: 'hr'
},
{
tag: 'div',
text: {
tag: 'lark_md',
content: `💡 **提示:**\n• 先选择存储桶和路径\n• 点击"使用默认配置上传"开始\n• 或使用命令:/upload /路径/文件名 存储桶名`
} }
} }
] ]
}; };
log('发送卡片到 chatId:', chatId); // 保存文件信息到状态
setUserState(chatId, {
file_key: fileKey,
file_name: fileName,
message_id: messageId
});
await feishu.sendCard(chatId, card); await feishu.sendCard(chatId, card);
} }
// 使用已保存的状态直接上传文件 // 显示确认卡片
async function handleFileUploadWithState(messageData, feishu, uploader, state) { async function showConfirmCard(chatId, feishu, uploader, state) {
const chatId = messageData.chat_id; const fileKey = state.file_key;
const messageId = messageData.message_id; const fileName = state.file_name;
const messageContent = JSON.parse(messageData.content); const messageId = state.message_id;
const bucket = state.bucket || 'default';
const fileKey = messageContent.file_key; const upload_path = state.upload_path || '';
const fileName = messageContent.file_name; const path_label = state.path_label || '原文件名';
if (!fileKey) return;
const targetBucket = state.bucket || 'default';
let targetKey = state.upload_path || fileName;
const pathLabel = state.path_label || '原文件名';
let targetKey = upload_path || fileName;
if (targetKey.startsWith('/')) targetKey = targetKey.substring(1); if (targetKey.startsWith('/')) targetKey = targetKey.substring(1);
log('📤 使用已保存配置上传:', fileName, 'bucket:', targetBucket, 'path:', targetKey); log('📋 显示确认卡片:', fileName, 'bucket:', bucket, 'path:', targetKey);
try { const card = {
await feishu.sendMessage(chatId, { config: { wide_screen_mode: true },
msg_type: 'text', header: {
content: { text: `📥 正在下载:${fileName}` } template: 'green',
}); title: { content: '✅ 确认上传', tag: 'plain_text' }
},
const tempFile = await feishu.downloadFile(fileKey, messageId, chatId); elements: [
log('✅ 文件下载完成:', tempFile); {
tag: 'div',
await feishu.sendMessage(chatId, { text: {
msg_type: 'text', tag: 'lark_md',
content: { text: `📤 上传中:${targetKey}${targetBucket} (路径:${pathLabel})` } content: `**文件:** ${fileName}\n**存储桶:** ${bucket}\n**路径:** ${path_label}\n**目标:** ${targetKey}\n\n点击"确认上传"开始上传到七牛云`
});
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}`
} }
}); },
{
fs.unlinkSync(tempFile); tag: 'action',
log('🗑️ 临时文件已清理:', tempFile); actions: [
{
} catch (error) { tag: 'button',
log('上传失败:', error.message); text: { tag: 'plain_text', content: '✅ 确认上传' },
await feishu.sendMessage(chatId, { type: 'primary',
msg_type: 'text', value: {
content: { text: `❌ 上传失败:${error.message}` } 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'
}
}
]
}
]
};
await feishu.sendCard(chatId, card);
} }
async function handleConfigCommandV2(message, content, feishu, uploader) { async function handleConfigCommandV2(message, content, feishu, uploader) {
@@ -504,7 +500,6 @@ async function handlePathCommandV2(message, content, feishu) {
const fullConfig = loadFullConfig(); const fullConfig = loadFullConfig();
if (subCommand === 'list' || !subCommand) { if (subCommand === 'list' || !subCommand) {
// 列出所有预设路径
const paths = fullConfig.uploadPaths || {}; const paths = fullConfig.uploadPaths || {};
let pathText = '**预设路径列表:**\n\n'; let pathText = '**预设路径列表:**\n\n';
for (const [name, pathValue] of Object.entries(paths)) { for (const [name, pathValue] of Object.entries(paths)) {
@@ -515,9 +510,8 @@ async function handlePathCommandV2(message, content, feishu) {
content: { text: pathText } content: { text: pathText }
}); });
} else if (subCommand === 'add') { } else if (subCommand === 'add') {
// 添加预设路径:/path add <名称> <路径>
if (args.length < 3) { 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 name = args[1];
const pathValue = args[2]; const pathValue = args[2];
@@ -531,7 +525,6 @@ async function handlePathCommandV2(message, content, feishu) {
content: { text: `✅ 已添加预设路径:**${name}** → ${pathValue}` } content: { text: `✅ 已添加预设路径:**${name}** → ${pathValue}` }
}); });
} else if (subCommand === 'remove' || subCommand === 'del') { } else if (subCommand === 'remove' || subCommand === 'del') {
// 删除预设路径:/path remove <名称>
if (args.length < 2) { if (args.length < 2) {
throw new Error('用法:/path remove <名称>'); throw new Error('用法:/path remove <名称>');
} }
@@ -565,9 +558,11 @@ async function handleHelpCommandV2(chatId, feishu) {
🍙 七牛云上传 - 使用帮助 🍙 七牛云上传 - 使用帮助
📤 上传文件: 📤 上传文件:
1. 直接发送文件给我 1. 发送 /upload 命令
2. 选择存储桶和路径 2. 或点击"📎 上传文件"按钮
3. 点击"使用默认配置上传" 3. 发送文件
4. 选择存储桶和路径
5. 点击"确认上传"
⚙️ 配置管理: ⚙️ 配置管理:
/config list - 查看配置 /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', tag: 'div',
text: { text: {
tag: 'lark_md', 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', tag: 'action',
actions: [ actions: [
{
tag: 'button',
text: { tag: 'plain_text', content: '📎 上传文件' },
type: 'primary',
value: { action: 'start_upload' }
},
{ {
tag: 'button', tag: 'button',
text: { tag: 'plain_text', content: '⚙️ 配置' }, text: { tag: 'plain_text', content: '⚙️ 配置' },
@@ -671,7 +668,7 @@ setInterval(cleanupTempFiles, 60 * 60 * 1000);
log('⏰ 临时文件清理任务已启动(每小时执行一次)'); log('⏰ 临时文件清理任务已启动(每小时执行一次)');
app.listen(PORT, () => { app.listen(PORT, () => {
log(`🚀 七牛云上传机器人启动 (v3 - 支持存储桶和路径选择)`); log(`🚀 七牛云上传机器人启动 (v4 - 主动触发 + 确认上传)`);
log(`📍 端口:${PORT}`); log(`📍 端口:${PORT}`);
setTimeout(cleanupTempFiles, 5000); setTimeout(cleanupTempFiles, 5000);
}); });