修改:
- profile 配置中的 path 字段现在存储路径键名,而不是直接路径值
- /profile add 命令现在使用路径键名:/profile add IPA 上传 default ipa
- 上传时根据路径键名从 uploadPaths 中获取实际路径
- 列表卡片显示路径键名和对应的值
配置示例:
{
"uploadPaths": {
"ipa": "/ipa/gamehall.ipa"
},
"uploadProfiles": {
"IPA 上传": {
"bucket": "default",
"path": "ipa" // 引用 uploadPaths 中的键名
}
}
}
858 lines
24 KiB
JavaScript
858 lines
24 KiB
JavaScript
#!/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('/profile')) {
|
||
await handleProfileCommandV2(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_key } = actionData.value;
|
||
log('📋 选择上传配置:', profile_name);
|
||
|
||
// path_key 是预设路径的名称,需要从配置中获取实际路径
|
||
setUserState(chatId, {
|
||
profile_name,
|
||
bucket,
|
||
path_key
|
||
});
|
||
|
||
await feishu.sendMessage(chatId, {
|
||
msg_type: 'text',
|
||
content: { text: `✅ 已选择配置:**${profile_name}**\n\n📤 请发送文件` }
|
||
});
|
||
break;
|
||
}
|
||
|
||
case 'upload_with_profile': {
|
||
// 从配置卡片直接上传(需要先发送文件)
|
||
const { profile_name, bucket, path_key } = actionData.value;
|
||
const state = getUserState(chatId);
|
||
|
||
if (!state.file_key) {
|
||
await feishu.sendMessage(chatId, {
|
||
msg_type: 'text',
|
||
content: { text: '📎 请先发送文件' }
|
||
});
|
||
return;
|
||
}
|
||
|
||
// 从预设路径配置中获取实际路径
|
||
const fullConfig = loadFullConfig();
|
||
const uploadPaths = fullConfig.uploadPaths || {};
|
||
const upload_path = uploadPaths[path_key] || '';
|
||
|
||
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,
|
||
path_key
|
||
});
|
||
clearUserState(chatId);
|
||
break;
|
||
}
|
||
|
||
case 'confirm_upload': {
|
||
const { file_key, file_name, message_id, bucket, path_key, path_label } = actionData.value;
|
||
|
||
// 从预设路径配置中获取实际路径
|
||
const fullConfig = loadFullConfig();
|
||
const uploadPaths = fullConfig.uploadPaths || {};
|
||
const upload_path = uploadPaths[path_key] || '';
|
||
|
||
await doUpload(chatId, feishu, uploader, {
|
||
file_key, file_name, message_id, bucket, upload_path, path_label, path_key
|
||
});
|
||
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]) => {
|
||
// config.path 是预设路径的名称(键)
|
||
const pathKey = config.path || '';
|
||
const uploadPaths = fullConfig.uploadPaths || {};
|
||
const pathValue = uploadPaths[pathKey] || '';
|
||
const pathDisplay = pathValue || '(原文件名)';
|
||
|
||
return {
|
||
tag: 'button',
|
||
text: { tag: 'plain_text', content: `${name}` },
|
||
type: 'primary',
|
||
value: {
|
||
action: 'select_profile',
|
||
profile_name: name,
|
||
bucket: config.bucket,
|
||
path_key: pathKey
|
||
}
|
||
};
|
||
});
|
||
|
||
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 uploadPaths = fullConfig.uploadPaths || {};
|
||
|
||
const profileButtons = Object.entries(profiles).map(([name, config]) => {
|
||
// config.path 是预设路径的名称(键)
|
||
const pathKey = config.path || '';
|
||
|
||
return {
|
||
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,
|
||
path_key: pathKey,
|
||
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, path_key, path_label } = info;
|
||
|
||
// 从预设路径配置中获取实际路径
|
||
const fullConfig = loadFullConfig();
|
||
const uploadPaths = fullConfig.uploadPaths || {};
|
||
const upload_path = uploadPaths[path_key] || '';
|
||
|
||
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,
|
||
path_key,
|
||
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, createBucketsListCard(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 || {};
|
||
await feishu.sendCard(chatId, createPathsListCard(paths));
|
||
} 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 handleProfileCommandV2(message, content, feishu) {
|
||
const chatId = message.chat_id;
|
||
const text = content.text || '';
|
||
const args = text.replace(/^\/profile\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' || !subCommand) {
|
||
const profiles = fullConfig.uploadProfiles || {};
|
||
const uploadPaths = fullConfig.uploadPaths || {};
|
||
await feishu.sendCard(chatId, createProfilesListCard(profiles, uploadPaths));
|
||
} else if (subCommand === 'add') {
|
||
// /profile add <名称> <存储桶> [路径键名]
|
||
if (args.length < 3) {
|
||
throw new Error('用法:/profile add <名称> <存储桶> [路径键名]\n示例:/profile add IPA 上传 default ipa');
|
||
}
|
||
const name = args[1];
|
||
const bucket = args[2];
|
||
const pathKey = args[3] || '';
|
||
|
||
// 验证存储桶是否存在
|
||
if (!fullConfig.buckets[bucket]) {
|
||
throw new Error(`存储桶 "${bucket}" 不存在,可用:${Object.keys(fullConfig.buckets).join(', ')}`);
|
||
}
|
||
|
||
// 验证路径键名是否存在(如果有提供)
|
||
if (pathKey && (!fullConfig.uploadPaths || !fullConfig.uploadPaths[pathKey])) {
|
||
const availablePaths = Object.keys(fullConfig.uploadPaths || {}).join(', ');
|
||
throw new Error(`路径 "${pathKey}" 不存在,可用:${availablePaths || '无'}`);
|
||
}
|
||
|
||
fullConfig.uploadProfiles = fullConfig.uploadProfiles || {};
|
||
fullConfig.uploadProfiles[name] = {
|
||
bucket: bucket,
|
||
path: pathKey // 存储路径键名,不是路径值
|
||
};
|
||
|
||
fs.writeFileSync(configPath, JSON.stringify(fullConfig, null, 2));
|
||
|
||
const pathDisplay = pathKey ? `${pathKey} (${fullConfig.uploadPaths[pathKey]})` : '(原文件名)';
|
||
await feishu.sendMessage(chatId, {
|
||
msg_type: 'text',
|
||
content: { text: `✅ 已添加上传配置:**${name}**\n存储桶:${bucket}\n路径:${pathDisplay}` }
|
||
});
|
||
} else if (subCommand === 'remove' || subCommand === 'del') {
|
||
if (args.length < 2) {
|
||
throw new Error('用法:/profile remove <名称>');
|
||
}
|
||
const name = args[1];
|
||
|
||
if (!fullConfig.uploadProfiles || !fullConfig.uploadProfiles[name]) {
|
||
throw new Error(`上传配置 "${name}" 不存在`);
|
||
}
|
||
|
||
delete fullConfig.uploadProfiles[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) {
|
||
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 - 查看七牛云配置
|
||
/profile list - 查看上传配置模板
|
||
/profile add <名称> <存储桶> [路径] - 添加上传配置
|
||
/profile remove <名称> - 删除上传配置
|
||
|
||
📁 路径管理:
|
||
/path list - 查看预设路径
|
||
/path add <名称> <路径> - 添加预设路径
|
||
|
||
💡 示例:
|
||
/profile add IPA 上传 default /ipa/
|
||
/profile add 备份 default /backup/
|
||
/profile list
|
||
/profile remove 备份
|
||
|
||
**提示:**
|
||
- 上传同名文件会自动覆盖
|
||
` }
|
||
});
|
||
}
|
||
|
||
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 createBucketsListCard(configData) {
|
||
const buckets = configData.buckets || {};
|
||
const entries = Object.entries(buckets);
|
||
|
||
if (entries.length === 0) {
|
||
return {
|
||
config: { wide_screen_mode: true },
|
||
header: {
|
||
template: 'grey',
|
||
title: { content: '⚙️ 七牛云存储桶配置', tag: 'plain_text' }
|
||
},
|
||
elements: [{ tag: 'div', text: { tag: 'lark_md', content: '暂无配置' } }]
|
||
};
|
||
}
|
||
|
||
// 构建表格内容
|
||
let tableContent = '| 名称 | 存储桶 | 区域 | CDN 域名 |\n|------|--------|------|----------|\n';
|
||
for (const [name, bucket] of entries) {
|
||
const domain = bucket.domain.length > 30 ? bucket.domain.substring(0, 27) + '...' : bucket.domain;
|
||
tableContent += `| ${name} | ${bucket.bucket} | ${bucket.region} | ${domain} |\n`;
|
||
}
|
||
|
||
return {
|
||
config: { wide_screen_mode: true },
|
||
header: {
|
||
template: 'green',
|
||
title: { content: '⚙️ 七牛云存储桶配置', tag: 'plain_text' }
|
||
},
|
||
elements: [
|
||
{
|
||
tag: 'div',
|
||
text: {
|
||
tag: 'lark_md',
|
||
content: tableContent
|
||
}
|
||
}
|
||
]
|
||
};
|
||
}
|
||
|
||
// 预设路径列表卡片(表格形式)
|
||
function createPathsListCard(paths) {
|
||
const entries = Object.entries(paths);
|
||
|
||
if (entries.length === 0) {
|
||
return {
|
||
config: { wide_screen_mode: true },
|
||
header: {
|
||
template: 'grey',
|
||
title: { content: '📁 预设路径列表', tag: 'plain_text' }
|
||
},
|
||
elements: [{ tag: 'div', text: { tag: 'lark_md', content: '暂无预设路径' } }]
|
||
};
|
||
}
|
||
|
||
// 构建表格内容
|
||
let tableContent = '| 名称 | 路径 |\n|------|------|\n';
|
||
for (const [name, pathValue] of entries) {
|
||
const pathDisplay = pathValue || '(原文件名)';
|
||
tableContent += `| **${name}** | ${pathDisplay} |\n`;
|
||
}
|
||
|
||
return {
|
||
config: { wide_screen_mode: true },
|
||
header: {
|
||
template: 'blue',
|
||
title: { content: '📁 预设路径列表', tag: 'plain_text' }
|
||
},
|
||
elements: [
|
||
{
|
||
tag: 'div',
|
||
text: {
|
||
tag: 'lark_md',
|
||
content: tableContent
|
||
}
|
||
}
|
||
]
|
||
};
|
||
}
|
||
|
||
// 上传配置模板列表卡片(表格形式)
|
||
function createProfilesListCard(profiles, uploadPaths) {
|
||
const entries = Object.entries(profiles);
|
||
|
||
if (entries.length === 0) {
|
||
return {
|
||
config: { wide_screen_mode: true },
|
||
header: {
|
||
template: 'grey',
|
||
title: { content: '📤 上传配置模板', tag: 'plain_text' }
|
||
},
|
||
elements: [{ tag: 'div', text: { tag: 'lark_md', content: '暂无上传配置模板' } }]
|
||
};
|
||
}
|
||
|
||
// 构建表格内容
|
||
let tableContent = '| 名称 | 存储桶 | 路径 |\n|------|--------|------|\n';
|
||
for (const [name, config] of entries) {
|
||
const pathKey = config.path || '';
|
||
const pathValue = uploadPaths && uploadPaths[pathKey] ? uploadPaths[pathKey] : '(原文件名)';
|
||
const pathDisplay = pathKey ? `${pathKey} → ${pathValue}` : '(原文件名)';
|
||
tableContent += `| **${name}** | ${config.bucket} | ${pathDisplay} |\n`;
|
||
}
|
||
|
||
return {
|
||
config: { wide_screen_mode: true },
|
||
header: {
|
||
template: 'blue',
|
||
title: { content: '📤 上传配置模板', tag: 'plain_text' }
|
||
},
|
||
elements: [
|
||
{
|
||
tag: 'div',
|
||
text: {
|
||
tag: 'lark_md',
|
||
content: tableContent
|
||
}
|
||
}
|
||
]
|
||
};
|
||
}
|
||
|
||
// 旧的配置卡片(保留兼容)
|
||
function createConfigCard(configData) {
|
||
return createBucketsListCard(configData);
|
||
}
|
||
|
||
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}`);
|
||
});
|