✅ v3 版本 - 支持存储桶和路径选择
功能:
1. 预设路径配置(在 config/qiniu-config.json 中配置 uploadPaths)
2. 文件上传时显示存储桶选择按钮
3. 文件上传时显示路径选择按钮
4. 快速上传按钮(使用默认配置)
5. 取消按钮
配置示例:
{
"buckets": { ... },
"uploadPaths": {
"原文件名": "",
"/config/": "config/",
"/backup/": "backup/",
"/uploads/": "uploads/"
}
}
使用流程:
1. 发送文件
2. 选择存储桶(可选)
3. 选择路径(可选)
4. 点击"使用默认配置上传"
5. 或重新发送文件(使用选择的配置)
This commit is contained in:
186
src/index.js
186
src/index.js
@@ -1,7 +1,8 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 七牛云上传 - 飞书独立应用 v2
|
* 七牛云上传 - 飞书独立应用 v3
|
||||||
|
* 支持存储桶和路径选择
|
||||||
*/
|
*/
|
||||||
|
|
||||||
require('dotenv').config();
|
require('dotenv').config();
|
||||||
@@ -23,6 +24,12 @@ function log(...args) {
|
|||||||
console.log(`[${timestamp}]`, ...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'));
|
||||||
|
}
|
||||||
|
|
||||||
async function handleFeishuEvent(req, res) {
|
async function handleFeishuEvent(req, res) {
|
||||||
const event = req.body;
|
const event = req.body;
|
||||||
log('📩 收到飞书请求');
|
log('📩 收到飞书请求');
|
||||||
@@ -39,17 +46,10 @@ async function handleFeishuEvent(req, res) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 飞书 v2 schema: event_type 在 header 中
|
|
||||||
const eventType = decryptedEvent.event_type ||
|
const eventType = decryptedEvent.event_type ||
|
||||||
decryptedEvent.header?.event_type ||
|
decryptedEvent.header?.event_type ||
|
||||||
decryptedEvent.type;
|
decryptedEvent.type;
|
||||||
log('收到飞书事件:', eventType);
|
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') {
|
if (eventType === 'url_verification') {
|
||||||
res.json({ challenge: decryptedEvent.challenge || event.challenge });
|
res.json({ challenge: decryptedEvent.challenge || event.challenge });
|
||||||
@@ -97,15 +97,8 @@ 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) {
|
||||||
log('🔍 收到文件消息 - 发送存储桶选择卡片');
|
log('🔍 收到文件消息 - 发送配置卡片');
|
||||||
await handleFileReceivedWithCard(messageData, feishu, uploader);
|
await handleFileReceivedWithCard(messageData, feishu, uploader);
|
||||||
} else if (text.startsWith('/')) {
|
|
||||||
// 处理路径设置命令
|
|
||||||
log('处理路径设置命令:', text);
|
|
||||||
await feishu.sendMessage(chatId, {
|
|
||||||
msg_type: 'text',
|
|
||||||
content: { text: '💡 请先选择存储桶,然后回复路径\n或使用命令:/upload /路径/文件名 存储桶名' }
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
await sendWelcomeCard(chatId, feishu);
|
await sendWelcomeCard(chatId, feishu);
|
||||||
}
|
}
|
||||||
@@ -131,7 +124,6 @@ async function handleCardInteraction(event) {
|
|||||||
|
|
||||||
if (!chatId && operator?.open_id) {
|
if (!chatId && operator?.open_id) {
|
||||||
chatId = operator.open_id;
|
chatId = operator.open_id;
|
||||||
log('使用 operator open_id 作为 chatId:', chatId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
log('卡片交互:', action, 'chatId:', chatId);
|
log('卡片交互:', action, 'chatId:', chatId);
|
||||||
@@ -194,65 +186,28 @@ async function handleCardInteraction(event) {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'select_bucket': {
|
case 'set_bucket': {
|
||||||
const { file_key, file_name, message_id, chat_id, bucket } = actionData.value;
|
const { bucket } = actionData.value;
|
||||||
log('🪣 选择存储桶:', bucket);
|
log('🪣 选择存储桶:', bucket);
|
||||||
|
|
||||||
// 发送确认卡片,带路径输入提示
|
await feishu.sendMessage(chatId, {
|
||||||
const card = {
|
msg_type: 'text',
|
||||||
config: { wide_screen_mode: true },
|
content: { text: `✅ 已选择存储桶:**${bucket}**\n\n请重新发送文件开始上传` }
|
||||||
header: {
|
});
|
||||||
template: 'green',
|
|
||||||
title: { content: '✅ 已选择存储桶', tag: 'plain_text' }
|
|
||||||
},
|
|
||||||
elements: [
|
|
||||||
{
|
|
||||||
tag: 'div',
|
|
||||||
text: {
|
|
||||||
tag: 'lark_md',
|
|
||||||
content: `**存储桶:** ${bucket}\n**文件:** ${file_name}\n\n**请回复消息设置路径:**\n格式:/路径/文件名\n\n例如:/config/test.txt\n\n或直接点击"使用原文件名上传"`
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
tag: 'action',
|
|
||||||
actions: [
|
|
||||||
{
|
|
||||||
tag: 'button',
|
|
||||||
text: { tag: 'plain_text', content: '✅ 使用原文件名上传' },
|
|
||||||
type: 'primary',
|
|
||||||
value: {
|
|
||||||
action: 'confirm_upload',
|
|
||||||
file_key: file_key,
|
|
||||||
file_name: file_name,
|
|
||||||
message_id: message_id,
|
|
||||||
chat_id: chat_id,
|
|
||||||
bucket: bucket
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
tag: 'button',
|
|
||||||
text: { tag: 'plain_text', content: '🔙 重新选择' },
|
|
||||||
type: 'default',
|
|
||||||
value: {
|
|
||||||
action: 'reselect'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
await feishu.sendCard(chatId, card);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'reselect':
|
case 'set_path': {
|
||||||
// 重新发送文件选择卡片(需要用户重新发送文件)
|
const { upload_path, path_label } = actionData.value;
|
||||||
|
const pathDesc = path_label || '原文件名';
|
||||||
|
log('📁 选择路径:', pathDesc);
|
||||||
|
|
||||||
await feishu.sendMessage(chatId, {
|
await feishu.sendMessage(chatId, {
|
||||||
msg_type: 'text',
|
msg_type: 'text',
|
||||||
content: { text: '📎 请重新发送文件' }
|
content: { text: `✅ 已选择路径:**${pathDesc}**\n\n请重新发送文件开始上传` }
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case 'cancel_upload':
|
case 'cancel_upload':
|
||||||
log('取消上传,chatId:', chatId);
|
log('取消上传,chatId:', chatId);
|
||||||
@@ -298,21 +253,59 @@ async function handleFileReceivedWithCard(messageData, feishu, uploader) {
|
|||||||
const configData = await uploader2.listConfig();
|
const configData = await uploader2.listConfig();
|
||||||
const bucketNames = Object.keys(configData.buckets);
|
const bucketNames = Object.keys(configData.buckets);
|
||||||
|
|
||||||
|
// 获取预设路径
|
||||||
|
const fullConfig = loadFullConfig();
|
||||||
|
const uploadPaths = fullConfig.uploadPaths || { '原文件名': '' };
|
||||||
|
|
||||||
// 构建存储桶选择按钮
|
// 构建存储桶选择按钮
|
||||||
const bucketButtons = bucketNames.map(name => ({
|
const bucketButtons = bucketNames.map(name => ({
|
||||||
tag: 'button',
|
tag: 'button',
|
||||||
text: { tag: 'plain_text', content: name },
|
text: { tag: 'plain_text', content: name },
|
||||||
type: 'default',
|
type: 'primary',
|
||||||
value: {
|
value: {
|
||||||
action: 'select_bucket',
|
action: 'set_bucket',
|
||||||
file_key: fileKey,
|
|
||||||
file_name: fileName,
|
|
||||||
message_id: messageId,
|
|
||||||
chat_id: chatId,
|
|
||||||
bucket: name
|
bucket: name
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// 构建路径选择按钮
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 构建快速上传按钮(使用默认配置)
|
||||||
|
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 = {
|
const card = {
|
||||||
config: { wide_screen_mode: true },
|
config: { wide_screen_mode: true },
|
||||||
header: {
|
header: {
|
||||||
@@ -324,7 +317,7 @@ async function handleFileReceivedWithCard(messageData, feishu, uploader) {
|
|||||||
tag: 'div',
|
tag: 'div',
|
||||||
text: {
|
text: {
|
||||||
tag: 'lark_md',
|
tag: 'lark_md',
|
||||||
content: `**文件名:** ${fileName}\n\n**请选择存储桶:**`
|
content: `**文件名:** ${fileName}\n\n**1️⃣ 选择存储桶:**`
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -338,7 +331,35 @@ async function handleFileReceivedWithCard(messageData, feishu, uploader) {
|
|||||||
tag: 'div',
|
tag: 'div',
|
||||||
text: {
|
text: {
|
||||||
tag: 'lark_md',
|
tag: 'lark_md',
|
||||||
content: `💡 **提示:**\n• 选择存储桶后,可以输入路径\n• 使用命令:/upload /路径/文件名 存储桶名`
|
content: `**2️⃣ 选择路径:**`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tag: 'action',
|
||||||
|
actions: pathButtons
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tag: 'hr'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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 /路径/文件名 存储桶名`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -386,8 +407,8 @@ async function handleHelpCommandV2(chatId, feishu) {
|
|||||||
|
|
||||||
📤 上传文件:
|
📤 上传文件:
|
||||||
1. 直接发送文件给我
|
1. 直接发送文件给我
|
||||||
2. 点击确认卡片上的"✅ 确认上传"
|
2. 选择存储桶和路径
|
||||||
3. 等待上传完成,获取下载链接
|
3. 点击"使用默认配置上传"
|
||||||
|
|
||||||
⚙️ 配置管理:
|
⚙️ 配置管理:
|
||||||
/config list - 查看配置
|
/config list - 查看配置
|
||||||
@@ -395,6 +416,7 @@ async function handleHelpCommandV2(chatId, feishu) {
|
|||||||
|
|
||||||
💡 提示:
|
💡 提示:
|
||||||
- 支持多存储桶配置
|
- 支持多存储桶配置
|
||||||
|
- 支持预设路径
|
||||||
- 上传同名文件会自动覆盖
|
- 上传同名文件会自动覆盖
|
||||||
` }
|
` }
|
||||||
});
|
});
|
||||||
@@ -412,18 +434,12 @@ 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• /config - 查看配置\n• /help - 查看帮助'
|
content: '你好!我是七牛云上传机器人。\n\n**使用方式:**\n• 直接发送文件给我\n• 选择存储桶和路径\n• 确认上传\n\n**命令:**\n• /config - 查看配置\n• /help - 查看帮助'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
tag: 'action',
|
tag: 'action',
|
||||||
actions: [
|
actions: [
|
||||||
{
|
|
||||||
tag: 'button',
|
|
||||||
text: { tag: 'plain_text', content: '📎 上传文件' },
|
|
||||||
type: 'primary',
|
|
||||||
value: { action: 'upload_file' }
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
tag: 'button',
|
tag: 'button',
|
||||||
text: { tag: 'plain_text', content: '⚙️ 配置' },
|
text: { tag: 'plain_text', content: '⚙️ 配置' },
|
||||||
@@ -487,7 +503,7 @@ setInterval(cleanupTempFiles, 60 * 60 * 1000);
|
|||||||
log('⏰ 临时文件清理任务已启动(每小时执行一次)');
|
log('⏰ 临时文件清理任务已启动(每小时执行一次)');
|
||||||
|
|
||||||
app.listen(PORT, () => {
|
app.listen(PORT, () => {
|
||||||
log(`🚀 七牛云上传机器人启动 (v2)`);
|
log(`🚀 七牛云上传机器人启动 (v3 - 支持存储桶和路径选择)`);
|
||||||
log(`📍 端口:${PORT}`);
|
log(`📍 端口:${PORT}`);
|
||||||
setTimeout(cleanupTempFiles, 5000);
|
setTimeout(cleanupTempFiles, 5000);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user