v5 - 简化流程:上传配置 + 一键上传
This commit is contained in:
718
src/index.js
718
src/index.js
@@ -1,13 +1,12 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 七牛云上传 - 飞书独立应用 v4
|
* 七牛云上传 - 飞书独立应用 v5
|
||||||
* 主动触发 + 确认上传流程
|
* 简化流程:上传配置 + 一键上传
|
||||||
*/
|
*/
|
||||||
|
|
||||||
require('dotenv').config();
|
require('dotenv').config();
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const crypto = require('crypto');
|
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
|
|
||||||
@@ -30,7 +29,7 @@ function loadFullConfig() {
|
|||||||
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) {
|
||||||
@@ -56,7 +55,6 @@ async function handleFeishuEvent(req, res) {
|
|||||||
const { decrypt } = require('@larksuiteoapi/node-sdk');
|
const { decrypt } = require('@larksuiteoapi/node-sdk');
|
||||||
decryptedEvent = decrypt(event.encrypt, process.env.FEISHU_ENCRYPT_KEY);
|
decryptedEvent = decrypt(event.encrypt, process.env.FEISHU_ENCRYPT_KEY);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log('❌ 解密失败:', e.message);
|
|
||||||
res.status(500).send('Decrypt error');
|
res.status(500).send('Decrypt error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -65,7 +63,6 @@ async function handleFeishuEvent(req, res) {
|
|||||||
const eventType = decryptedEvent.event_type ||
|
const eventType = decryptedEvent.event_type ||
|
||||||
decryptedEvent.header?.event_type ||
|
decryptedEvent.header?.event_type ||
|
||||||
decryptedEvent.type;
|
decryptedEvent.type;
|
||||||
log('收到飞书事件:', eventType);
|
|
||||||
|
|
||||||
if (eventType === 'url_verification') {
|
if (eventType === 'url_verification') {
|
||||||
res.json({ challenge: decryptedEvent.challenge || event.challenge });
|
res.json({ challenge: decryptedEvent.challenge || event.challenge });
|
||||||
@@ -86,213 +83,199 @@ async function handleFeishuEvent(req, res) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function handleMessage(event) {
|
async function handleMessage(event) {
|
||||||
try {
|
const messageData = event.event?.message || event.message;
|
||||||
const messageData = event.event?.message || event.message;
|
if (!messageData) return;
|
||||||
if (!messageData) {
|
|
||||||
log('❌ 未找到消息数据');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const messageContent = JSON.parse(messageData.content);
|
const messageContent = JSON.parse(messageData.content);
|
||||||
const text = messageContent.text || '';
|
const text = messageContent.text || '';
|
||||||
const chatId = messageData.chat_id;
|
const chatId = messageData.chat_id;
|
||||||
const messageType = messageData.message_type || 'text';
|
const messageType = messageData.message_type || 'text';
|
||||||
|
|
||||||
log(`处理消息:${chatId} - 类型:${messageType}`);
|
const feishu = new FeishuAPI();
|
||||||
|
const uploader = new QiniuUploader();
|
||||||
|
|
||||||
const feishu = new FeishuAPI();
|
if (text.startsWith('/upload') || text.startsWith('/u ')) {
|
||||||
const uploader = new QiniuUploader();
|
// 显示上传配置卡片
|
||||||
|
await showProfileCard(chatId, feishu, uploader);
|
||||||
if (text.startsWith('/upload') || text.startsWith('/u ')) {
|
} else if (text.startsWith('/config') || text.startsWith('/qc ')) {
|
||||||
// 主动触发上传流程
|
await handleConfigCommandV2(messageData, messageContent, feishu, uploader);
|
||||||
log('📤 用户主动触发上传流程');
|
} else if (text.startsWith('/path')) {
|
||||||
setUserState(chatId, { uploadFlow: true });
|
await handlePathCommandV2(messageData, messageContent, feishu);
|
||||||
await feishu.sendMessage(chatId, {
|
} else if (text.startsWith('/help') || text.startsWith('/qh')) {
|
||||||
msg_type: 'text',
|
await handleHelpCommandV2(chatId, feishu);
|
||||||
content: { text: '📎 请发送要上传的文件' }
|
} else if (messageType === 'file' || messageContent.file_key) {
|
||||||
});
|
// 收到文件,显示配置选择
|
||||||
} else if (text.startsWith('/config') || text.startsWith('/qc ')) {
|
await handleFileReceived(messageData, feishu, uploader);
|
||||||
await handleConfigCommandV2(messageData, messageContent, feishu, uploader);
|
} else {
|
||||||
} else if (text.startsWith('/path')) {
|
await sendWelcomeCard(chatId, feishu);
|
||||||
await handlePathCommandV2(messageData, messageContent, feishu);
|
|
||||||
} 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.uploadFlow) {
|
|
||||||
// 用户上传流程中的文件
|
|
||||||
log('🔍 用户上传流程 - 发送选择卡片');
|
|
||||||
await handleFileReceivedWithCard(messageData, feishu, uploader);
|
|
||||||
} else {
|
|
||||||
// 普通消息,显示欢迎卡片
|
|
||||||
await sendWelcomeCard(chatId, feishu);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
await sendWelcomeCard(chatId, feishu);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
log('❌ 消息处理失败:', error.message);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleCardInteraction(event) {
|
async function handleCardInteraction(event) {
|
||||||
try {
|
const eventData = event.event;
|
||||||
const eventData = event.event;
|
const actionData = eventData?.action;
|
||||||
const operator = eventData?.operator;
|
const message = eventData?.message;
|
||||||
const actionData = eventData?.action;
|
|
||||||
const message = eventData?.message;
|
|
||||||
|
|
||||||
if (!actionData) {
|
if (!actionData) return;
|
||||||
log('❌ 卡片交互:缺少 action 数据');
|
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
const action = actionData.value?.action;
|
case 'upload_with_profile': {
|
||||||
let chatId = message?.chat_id || actionData.value?.chat_id;
|
// 从配置卡片直接上传(需要先发送文件)
|
||||||
|
const { profile_name, bucket, path: upload_path } = actionData.value;
|
||||||
|
const state = getUserState(chatId);
|
||||||
|
|
||||||
if (!chatId && operator?.open_id) {
|
if (!state.file_key) {
|
||||||
chatId = operator.open_id;
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
log('卡片交互:', action, 'chatId:', chatId);
|
case 'confirm_upload': {
|
||||||
|
const { file_key, file_name, message_id, bucket, upload_path, path_label } = actionData.value;
|
||||||
const feishu = new FeishuAPI();
|
await doUpload(chatId, feishu, uploader, {
|
||||||
const uploader = new QiniuUploader();
|
file_key, file_name, message_id, bucket, upload_path, path_label
|
||||||
|
});
|
||||||
switch (action) {
|
clearUserState(chatId);
|
||||||
case 'start_upload':
|
break;
|
||||||
// 用户点击欢迎卡片的"上传文件"
|
|
||||||
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, 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);
|
|
||||||
|
|
||||||
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: `📤 上传中:${targetKey} → ${targetBucket}` }
|
|
||||||
});
|
|
||||||
|
|
||||||
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` +
|
|
||||||
`💾 原文件:${file_name}\n` +
|
|
||||||
`🪣 存储桶:${targetBucket}\n` +
|
|
||||||
`📁 路径:${pathDesc}`
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
fs.unlinkSync(tempFile);
|
|
||||||
log('🗑️ 临时文件已清理:', tempFile);
|
|
||||||
clearUserState(chatId);
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
log('上传失败:', error.message);
|
|
||||||
await feishu.sendMessage(chatId, {
|
|
||||||
msg_type: 'text',
|
|
||||||
content: { text: `❌ 上传失败:${error.message}` }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'cancel_upload':
|
|
||||||
log('❌ 取消上传');
|
|
||||||
clearUserState(chatId);
|
|
||||||
await feishu.sendMessage(chatId, {
|
|
||||||
msg_type: 'text',
|
|
||||||
content: { text: '❌ 已取消上传' }
|
|
||||||
});
|
|
||||||
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);
|
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 handleFileReceivedWithCard(messageData, feishu, uploader) {
|
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 chatId = messageData.chat_id;
|
||||||
const messageId = messageData.message_id;
|
const messageId = messageData.message_id;
|
||||||
const messageContent = JSON.parse(messageData.content);
|
const messageContent = JSON.parse(messageData.content);
|
||||||
@@ -302,90 +285,6 @@ async function handleFileReceivedWithCard(messageData, feishu, uploader) {
|
|||||||
|
|
||||||
if (!fileKey) return;
|
if (!fileKey) return;
|
||||||
|
|
||||||
log('📎 收到文件,发送选择卡片:', fileName);
|
|
||||||
|
|
||||||
// 获取存储桶列表
|
|
||||||
const configData = await uploader.listConfig();
|
|
||||||
const bucketNames = Object.keys(configData.buckets);
|
|
||||||
|
|
||||||
// 获取预设路径
|
|
||||||
const fullConfig = loadFullConfig();
|
|
||||||
const uploadPaths = fullConfig.uploadPaths || { '原文件名': '' };
|
|
||||||
|
|
||||||
// 构建存储桶选择按钮
|
|
||||||
const bucketButtons = bucketNames.map(name => ({
|
|
||||||
tag: 'button',
|
|
||||||
text: { tag: 'plain_text', content: name },
|
|
||||||
type: 'primary',
|
|
||||||
value: {
|
|
||||||
action: 'set_bucket',
|
|
||||||
bucket: name,
|
|
||||||
chat_id: chatId
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
// 构建路径选择按钮
|
|
||||||
const pathButtons = Object.entries(uploadPaths).map(([label, pathValue]) => ({
|
|
||||||
tag: 'button',
|
|
||||||
text: { tag: 'plain_text', content: label },
|
|
||||||
type: 'default',
|
|
||||||
value: {
|
|
||||||
action: 'set_path',
|
|
||||||
upload_path: pathValue,
|
|
||||||
path_label: label,
|
|
||||||
chat_id: chatId,
|
|
||||||
// 保存文件信息用于确认卡片
|
|
||||||
file_key: fileKey,
|
|
||||||
file_name: fileName,
|
|
||||||
message_id: messageId
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
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**1️⃣ 选择存储桶:**`
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
tag: 'action',
|
|
||||||
actions: bucketButtons
|
|
||||||
},
|
|
||||||
{
|
|
||||||
tag: 'hr'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
tag: 'div',
|
|
||||||
text: {
|
|
||||||
tag: 'lark_md',
|
|
||||||
content: `**2️⃣ 选择路径:**`
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
tag: 'action',
|
|
||||||
actions: pathButtons
|
|
||||||
},
|
|
||||||
{
|
|
||||||
tag: 'hr'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
tag: 'div',
|
|
||||||
text: {
|
|
||||||
tag: 'lark_md',
|
|
||||||
content: `💡 **提示:**\n• 选择存储桶和路径后,会显示确认卡片\n• 点击"确认上传"才开始上传`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
// 保存文件信息到状态
|
// 保存文件信息到状态
|
||||||
setUserState(chatId, {
|
setUserState(chatId, {
|
||||||
file_key: fileKey,
|
file_key: fileKey,
|
||||||
@@ -393,23 +292,91 @@ async function handleFileReceivedWithCard(messageData, feishu, uploader) {
|
|||||||
message_id: messageId
|
message_id: messageId
|
||||||
});
|
});
|
||||||
|
|
||||||
await feishu.sendCard(chatId, card);
|
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, uploader, state) {
|
async function showConfirmCard(chatId, feishu, info) {
|
||||||
const fileKey = state.file_key;
|
const { file_name, bucket, upload_path, path_label } = info;
|
||||||
const fileName = state.file_name;
|
let targetKey = upload_path || 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);
|
if (targetKey.startsWith('/')) targetKey = targetKey.substring(1);
|
||||||
|
|
||||||
log('📋 显示确认卡片:', fileName, 'bucket:', bucket, 'path:', targetKey);
|
|
||||||
|
|
||||||
const card = {
|
const card = {
|
||||||
config: { wide_screen_mode: true },
|
config: { wide_screen_mode: true },
|
||||||
header: {
|
header: {
|
||||||
@@ -421,7 +388,7 @@ async function showConfirmCard(chatId, feishu, uploader, state) {
|
|||||||
tag: 'div',
|
tag: 'div',
|
||||||
text: {
|
text: {
|
||||||
tag: 'lark_md',
|
tag: 'lark_md',
|
||||||
content: `**文件:** ${fileName}\n**存储桶:** ${bucket}\n**路径:** ${path_label}\n**目标:** ${targetKey}\n\n点击"确认上传"开始上传到七牛云`
|
content: `**文件:** ${file_name}\n**配置:** ${path_label}\n**存储桶:** ${bucket}\n**路径:** ${targetKey || '(原文件名)'}\n\n点击"确认上传"开始上传`
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -433,22 +400,20 @@ async function showConfirmCard(chatId, feishu, uploader, state) {
|
|||||||
type: 'primary',
|
type: 'primary',
|
||||||
value: {
|
value: {
|
||||||
action: 'confirm_upload',
|
action: 'confirm_upload',
|
||||||
file_key: fileKey,
|
file_key: info.file_key,
|
||||||
file_name: fileName,
|
file_name: info.file_name,
|
||||||
message_id: messageId,
|
message_id: info.message_id,
|
||||||
chat_id: chatId,
|
chat_id: chatId,
|
||||||
bucket: bucket,
|
bucket,
|
||||||
upload_path: upload_path,
|
upload_path,
|
||||||
path_label: path_label
|
path_label
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
tag: 'button',
|
tag: 'button',
|
||||||
text: { tag: 'plain_text', content: '❌ 取消' },
|
text: { tag: 'plain_text', content: '❌ 取消' },
|
||||||
type: 'default',
|
type: 'default',
|
||||||
value: {
|
value: { action: 'cancel' }
|
||||||
action: 'cancel_upload'
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -458,6 +423,55 @@ async function showConfirmCard(chatId, feishu, uploader, state) {
|
|||||||
await feishu.sendCard(chatId, card);
|
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) {
|
async function handleConfigCommandV2(message, content, feishu, uploader) {
|
||||||
const chatId = message.chat_id;
|
const chatId = message.chat_id;
|
||||||
const text = content.text || '';
|
const text = content.text || '';
|
||||||
@@ -467,18 +481,14 @@ async function handleConfigCommandV2(message, content, feishu, uploader) {
|
|||||||
try {
|
try {
|
||||||
if (subCommand === 'list' || !subCommand) {
|
if (subCommand === 'list' || !subCommand) {
|
||||||
const configData = await uploader.listConfig();
|
const configData = await uploader.listConfig();
|
||||||
const configCard = createConfigCard(configData);
|
await feishu.sendCard(chatId, createConfigCard(configData));
|
||||||
await feishu.sendCard(chatId, configCard);
|
|
||||||
} else if (subCommand === 'set') {
|
} else if (subCommand === 'set') {
|
||||||
const [keyPath, value] = args.slice(1);
|
const [keyPath, value] = args.slice(1);
|
||||||
if (!keyPath || !value) throw new Error('用法:/config set <key> <value>');
|
|
||||||
await uploader.setConfigValue(keyPath, value);
|
await uploader.setConfigValue(keyPath, value);
|
||||||
await feishu.sendMessage(chatId, {
|
await feishu.sendMessage(chatId, {
|
||||||
msg_type: 'text',
|
msg_type: 'text',
|
||||||
content: { text: `✅ 已设置 ${keyPath} = ${value}` }
|
content: { text: `✅ 已设置 ${keyPath} = ${value}` }
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
throw new Error(`未知命令:${subCommand}`);
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await feishu.sendMessage(chatId, {
|
await feishu.sendMessage(chatId, {
|
||||||
@@ -499,7 +509,7 @@ async function handlePathCommandV2(message, content, feishu) {
|
|||||||
try {
|
try {
|
||||||
const fullConfig = loadFullConfig();
|
const fullConfig = loadFullConfig();
|
||||||
|
|
||||||
if (subCommand === 'list' || !subCommand) {
|
if (subCommand === 'list') {
|
||||||
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)) {
|
||||||
@@ -510,38 +520,14 @@ async function handlePathCommandV2(message, content, feishu) {
|
|||||||
content: { text: pathText }
|
content: { text: pathText }
|
||||||
});
|
});
|
||||||
} else if (subCommand === 'add') {
|
} else if (subCommand === 'add') {
|
||||||
if (args.length < 3) {
|
|
||||||
throw new Error('用法:/path add <名称> <路径>');
|
|
||||||
}
|
|
||||||
const name = args[1];
|
const name = args[1];
|
||||||
const pathValue = args[2];
|
const pathValue = args[2];
|
||||||
|
|
||||||
fullConfig.uploadPaths = fullConfig.uploadPaths || {};
|
|
||||||
fullConfig.uploadPaths[name] = pathValue;
|
fullConfig.uploadPaths[name] = pathValue;
|
||||||
|
|
||||||
fs.writeFileSync(configPath, JSON.stringify(fullConfig, null, 2));
|
fs.writeFileSync(configPath, JSON.stringify(fullConfig, null, 2));
|
||||||
await feishu.sendMessage(chatId, {
|
await feishu.sendMessage(chatId, {
|
||||||
msg_type: 'text',
|
msg_type: 'text',
|
||||||
content: { text: `✅ 已添加预设路径:**${name}** → ${pathValue}` }
|
content: { text: `✅ 已添加预设路径:**${name}** → ${pathValue}` }
|
||||||
});
|
});
|
||||||
} else if (subCommand === 'remove' || subCommand === 'del') {
|
|
||||||
if (args.length < 2) {
|
|
||||||
throw new Error('用法:/path remove <名称>');
|
|
||||||
}
|
|
||||||
const name = args[1];
|
|
||||||
|
|
||||||
if (!fullConfig.uploadPaths || !fullConfig.uploadPaths[name]) {
|
|
||||||
throw new Error(`预设路径 "${name}" 不存在`);
|
|
||||||
}
|
|
||||||
|
|
||||||
delete fullConfig.uploadPaths[name];
|
|
||||||
fs.writeFileSync(configPath, JSON.stringify(fullConfig, null, 2));
|
|
||||||
await feishu.sendMessage(chatId, {
|
|
||||||
msg_type: 'text',
|
|
||||||
content: { text: `✅ 已删除预设路径:**${name}**` }
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
throw new Error(`未知命令:${subCommand}\n可用命令:list, add, remove`);
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await feishu.sendMessage(chatId, {
|
await feishu.sendMessage(chatId, {
|
||||||
@@ -557,25 +543,27 @@ async function handleHelpCommandV2(chatId, feishu) {
|
|||||||
content: { text: `
|
content: { text: `
|
||||||
🍙 七牛云上传 - 使用帮助
|
🍙 七牛云上传 - 使用帮助
|
||||||
|
|
||||||
📤 上传文件:
|
📤 上传方式:
|
||||||
1. 发送 /upload 命令
|
|
||||||
2. 或点击"📎 上传文件"按钮
|
**方式 1:选择配置 → 发送文件**
|
||||||
3. 发送文件
|
1. 发送 /upload
|
||||||
4. 选择存储桶和路径
|
2. 选择上传配置
|
||||||
5. 点击"确认上传"
|
3. 发送文件
|
||||||
|
4. 确认上传
|
||||||
|
|
||||||
|
**方式 2:发送文件 → 选择配置**
|
||||||
|
1. 直接发送文件
|
||||||
|
2. 选择上传配置
|
||||||
|
3. 确认上传
|
||||||
|
|
||||||
⚙️ 配置管理:
|
⚙️ 配置管理:
|
||||||
/config list - 查看配置
|
/config list - 查看配置
|
||||||
/config set <key> <value> - 修改配置
|
/path list - 查看预设路径
|
||||||
|
/path add <名称> <路径> - 添加路径
|
||||||
📁 路径管理:
|
|
||||||
/path list - 查看预设路径
|
|
||||||
/path add <名称> <路径> - 添加预设路径
|
|
||||||
/path remove <名称> - 删除预设路径
|
|
||||||
|
|
||||||
💡 提示:
|
💡 提示:
|
||||||
- 支持多存储桶配置
|
- 上传配置在 config/qiniu-config.json 中配置
|
||||||
- 支持预设路径
|
- 支持多存储桶
|
||||||
- 上传同名文件会自动覆盖
|
- 上传同名文件会自动覆盖
|
||||||
` }
|
` }
|
||||||
});
|
});
|
||||||
@@ -593,7 +581,7 @@ async function sendWelcomeCard(chatId, feishu) {
|
|||||||
tag: 'div',
|
tag: 'div',
|
||||||
text: {
|
text: {
|
||||||
tag: 'lark_md',
|
tag: 'lark_md',
|
||||||
content: '你好!我是七牛云上传机器人。\n\n**使用方式:**\n• 发送 /upload 命令\n• 或点击下方"📎 上传文件"\n• 发送文件后选择配置\n\n**命令:**\n• /config - 查看配置\n• /path - 路径管理\n• /help - 查看帮助'
|
content: '你好!我是七牛云上传机器人。\n\n**使用方式:**\n• /upload - 选择配置上传\n• 直接发送文件\n\n**命令:**\n• /config - 配置\n• /path - 路径\n• /help - 帮助'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -601,7 +589,7 @@ async function sendWelcomeCard(chatId, feishu) {
|
|||||||
actions: [
|
actions: [
|
||||||
{
|
{
|
||||||
tag: 'button',
|
tag: 'button',
|
||||||
text: { tag: 'plain_text', content: '📎 上传文件' },
|
text: { tag: 'plain_text', content: '📤 上传文件' },
|
||||||
type: 'primary',
|
type: 'primary',
|
||||||
value: { action: 'start_upload' }
|
value: { action: 'start_upload' }
|
||||||
},
|
},
|
||||||
@@ -610,12 +598,6 @@ async function sendWelcomeCard(chatId, feishu) {
|
|||||||
text: { tag: 'plain_text', content: '⚙️ 配置' },
|
text: { tag: 'plain_text', content: '⚙️ 配置' },
|
||||||
type: 'default',
|
type: 'default',
|
||||||
value: { action: 'config' }
|
value: { action: 'config' }
|
||||||
},
|
|
||||||
{
|
|
||||||
tag: 'button',
|
|
||||||
text: { tag: 'plain_text', content: '❓ 帮助' },
|
|
||||||
type: 'default',
|
|
||||||
value: { action: 'help' }
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -644,31 +626,7 @@ app.get('/health', (req, res) => {
|
|||||||
res.json({ status: 'ok', timestamp: new Date().toISOString(), port: PORT });
|
res.json({ status: 'ok', timestamp: new Date().toISOString(), port: PORT });
|
||||||
});
|
});
|
||||||
|
|
||||||
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++;
|
|
||||||
}
|
|
||||||
} catch (e) {}
|
|
||||||
});
|
|
||||||
if (cleaned > 0) log(`✅ 清理完成:${cleaned} 个文件`);
|
|
||||||
}
|
|
||||||
|
|
||||||
setInterval(cleanupTempFiles, 60 * 60 * 1000);
|
|
||||||
log('⏰ 临时文件清理任务已启动(每小时执行一次)');
|
|
||||||
|
|
||||||
app.listen(PORT, () => {
|
app.listen(PORT, () => {
|
||||||
log(`🚀 七牛云上传机器人启动 (v4 - 主动触发 + 确认上传)`);
|
log(`🚀 七牛云上传机器人启动 (v5 - 简化流程)`);
|
||||||
log(`📍 端口:${PORT}`);
|
log(`📍 端口:${PORT}`);
|
||||||
setTimeout(cleanupTempFiles, 5000);
|
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user