问题: 选择路径后重新发送文件,又显示选择卡片,导致死循环 解决方案: 1. 添加用户状态临时存储(内存) 2. 选择存储桶/路径后保存状态(5 分钟有效) 3. 重新发送文件时,检测是否有保存的状态 4. 有状态则直接上传,无状态则显示选择卡片 使用流程: 1. 发送文件 → 显示选择卡片 2. 选择存储桶 → 保存状态,提示重新发送 3. 选择路径 → 保存状态,提示重新发送 4. 重新发送文件 → 使用保存的状态直接上传 5. 上传完成后清除状态 状态有效期:5 分钟
678 lines
20 KiB
JavaScript
678 lines
20 KiB
JavaScript
#!/usr/bin/env node
|
||
|
||
/**
|
||
* 七牛云上传 - 飞书独立应用 v3
|
||
* 支持存储桶和路径选择
|
||
*/
|
||
|
||
require('dotenv').config();
|
||
const express = require('express');
|
||
const crypto = require('crypto');
|
||
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 };
|
||
// 5 分钟后清除状态
|
||
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) {
|
||
log('❌ 解密失败:', e.message);
|
||
res.status(500).send('Decrypt error');
|
||
return;
|
||
}
|
||
}
|
||
|
||
const eventType = decryptedEvent.event_type ||
|
||
decryptedEvent.header?.event_type ||
|
||
decryptedEvent.type;
|
||
log('收到飞书事件:', eventType);
|
||
|
||
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) {
|
||
try {
|
||
const messageData = event.event?.message || event.message;
|
||
if (!messageData) {
|
||
log('❌ 未找到消息数据');
|
||
return;
|
||
}
|
||
|
||
const messageContent = JSON.parse(messageData.content);
|
||
const text = messageContent.text || '';
|
||
const chatId = messageData.chat_id;
|
||
const messageType = messageData.message_type || 'text';
|
||
|
||
log(`处理消息:${chatId} - 类型:${messageType}`);
|
||
|
||
const feishu = new FeishuAPI();
|
||
const uploader = new QiniuUploader();
|
||
|
||
if (text.startsWith('/upload') || text.startsWith('/u ')) {
|
||
await feishu.sendMessage(chatId, {
|
||
msg_type: 'text',
|
||
content: { text: '📎 请发送要上传的文件(直接发送文件即可)' }
|
||
});
|
||
} 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('/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('🔍 收到文件消息 - 发送配置卡片');
|
||
await handleFileReceivedWithCard(messageData, feishu, uploader);
|
||
}
|
||
} else {
|
||
await sendWelcomeCard(chatId, feishu);
|
||
}
|
||
} catch (error) {
|
||
log('❌ 消息处理失败:', error.message);
|
||
}
|
||
}
|
||
|
||
async function handleCardInteraction(event) {
|
||
try {
|
||
const eventData = event.event;
|
||
const operator = eventData?.operator;
|
||
const actionData = eventData?.action;
|
||
const message = eventData?.message;
|
||
|
||
if (!actionData) {
|
||
log('❌ 卡片交互:缺少 action 数据');
|
||
return;
|
||
}
|
||
|
||
const action = actionData.value?.action;
|
||
let chatId = message?.chat_id || actionData.value?.chat_id;
|
||
|
||
if (!chatId && operator?.open_id) {
|
||
chatId = operator.open_id;
|
||
}
|
||
|
||
log('卡片交互:', action, 'chatId:', chatId);
|
||
|
||
const feishu = new FeishuAPI();
|
||
const uploader = new QiniuUploader();
|
||
|
||
switch (action) {
|
||
case 'confirm_upload': {
|
||
const { file_key, file_name, message_id, bucket, upload_path } = actionData.value;
|
||
const targetBucket = bucket || 'default';
|
||
let targetKey = upload_path || file_name;
|
||
if (targetKey.startsWith('/')) targetKey = targetKey.substring(1);
|
||
|
||
log('📤 开始上传文件:', file_name, 'bucket:', targetBucket, 'path:', targetKey);
|
||
|
||
if (!chatId) {
|
||
log('❌ 缺少 chatId');
|
||
return;
|
||
}
|
||
|
||
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}`
|
||
}
|
||
});
|
||
|
||
fs.unlinkSync(tempFile);
|
||
log('🗑️ 临时文件已清理:', tempFile);
|
||
|
||
} catch (error) {
|
||
log('上传失败:', error.message);
|
||
await feishu.sendMessage(chatId, {
|
||
msg_type: 'text',
|
||
content: { text: `❌ 上传失败:${error.message}` }
|
||
});
|
||
}
|
||
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('✅ 取消回复已发送');
|
||
}
|
||
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);
|
||
}
|
||
}
|
||
|
||
async function handleFileReceivedWithCard(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;
|
||
|
||
log('📎 收到文件,发送配置卡片:', fileName);
|
||
|
||
// 获取存储桶列表
|
||
const uploader2 = new QiniuUploader();
|
||
const configData = await uploader2.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
|
||
}
|
||
}));
|
||
|
||
// 构建路径选择按钮
|
||
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 = {
|
||
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: `**3️⃣ 开始上传:**`
|
||
}
|
||
},
|
||
{
|
||
tag: 'action',
|
||
actions: quickUploadButtons
|
||
},
|
||
{
|
||
tag: 'hr'
|
||
},
|
||
{
|
||
tag: 'div',
|
||
text: {
|
||
tag: 'lark_md',
|
||
content: `💡 **提示:**\n• 先选择存储桶和路径\n• 点击"使用默认配置上传"开始\n• 或使用命令:/upload /路径/文件名 存储桶名`
|
||
}
|
||
}
|
||
]
|
||
};
|
||
|
||
log('发送卡片到 chatId:', chatId);
|
||
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 || '原文件名';
|
||
|
||
if (targetKey.startsWith('/')) targetKey = targetKey.substring(1);
|
||
|
||
log('📤 使用已保存配置上传:', fileName, 'bucket:', targetBucket, '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}`
|
||
}
|
||
});
|
||
|
||
fs.unlinkSync(tempFile);
|
||
log('🗑️ 临时文件已清理:', tempFile);
|
||
|
||
} 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();
|
||
const configCard = createConfigCard(configData);
|
||
await feishu.sendCard(chatId, configCard);
|
||
} else if (subCommand === 'set') {
|
||
const [keyPath, value] = args.slice(1);
|
||
if (!keyPath || !value) throw new Error('用法:/config set <key> <value>');
|
||
await uploader.setConfigValue(keyPath, value);
|
||
await feishu.sendMessage(chatId, {
|
||
msg_type: 'text',
|
||
content: { text: `✅ 已设置 ${keyPath} = ${value}` }
|
||
});
|
||
} else {
|
||
throw new Error(`未知命令:${subCommand}`);
|
||
}
|
||
} 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' || !subCommand) {
|
||
// 列出所有预设路径
|
||
const paths = fullConfig.uploadPaths || {};
|
||
let pathText = '**预设路径列表:**\n\n';
|
||
for (const [name, pathValue] of Object.entries(paths)) {
|
||
pathText += `• **${name}**: ${pathValue || '(原文件名)'}\n`;
|
||
}
|
||
await feishu.sendMessage(chatId, {
|
||
msg_type: 'text',
|
||
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');
|
||
}
|
||
const name = args[1];
|
||
const pathValue = args[2];
|
||
|
||
fullConfig.uploadPaths = fullConfig.uploadPaths || {};
|
||
fullConfig.uploadPaths[name] = pathValue;
|
||
|
||
fs.writeFileSync(configPath, JSON.stringify(fullConfig, null, 2));
|
||
await feishu.sendMessage(chatId, {
|
||
msg_type: 'text',
|
||
content: { text: `✅ 已添加预设路径:**${name}** → ${pathValue}` }
|
||
});
|
||
} else if (subCommand === 'remove' || subCommand === 'del') {
|
||
// 删除预设路径:/path remove <名称>
|
||
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) {
|
||
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. 直接发送文件给我
|
||
2. 选择存储桶和路径
|
||
3. 点击"使用默认配置上传"
|
||
|
||
⚙️ 配置管理:
|
||
/config list - 查看配置
|
||
/config set <key> <value> - 修改配置
|
||
|
||
📁 路径管理:
|
||
/path list - 查看预设路径
|
||
/path add <名称> <路径> - 添加预设路径
|
||
/path remove <名称> - 删除预设路径
|
||
|
||
💡 提示:
|
||
- 支持多存储桶配置
|
||
- 支持预设路径
|
||
- 上传同名文件会自动覆盖
|
||
|
||
示例:
|
||
/path add backup /backup/
|
||
/path remove backup
|
||
` }
|
||
});
|
||
}
|
||
|
||
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• 直接发送文件给我\n• 选择存储桶和路径\n• 确认上传\n\n**命令:**\n• /config - 查看配置\n• /help - 查看帮助'
|
||
}
|
||
},
|
||
{
|
||
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);
|
||
}
|
||
|
||
function createConfigCard(configData) {
|
||
let configText = '';
|
||
for (const [name, bucket] of Object.entries(configData.buckets)) {
|
||
configText += `**[${name}]**\nBucket: ${bucket.bucket}\nRegion: ${bucket.region}\nDomain: ${bucket.domain}\n\n`;
|
||
}
|
||
return {
|
||
config: { wide_screen_mode: true },
|
||
header: {
|
||
template: 'green',
|
||
title: { content: '⚙️ 七牛云配置', tag: 'plain_text' }
|
||
},
|
||
elements: [{ tag: 'div', text: { tag: 'lark_md', content: configText || '暂无配置' } }]
|
||
};
|
||
}
|
||
|
||
app.post('/feishu/event', handleFeishuEvent);
|
||
app.get('/health', (req, res) => {
|
||
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, () => {
|
||
log(`🚀 七牛云上传机器人启动 (v3 - 支持存储桶和路径选择)`);
|
||
log(`📍 端口:${PORT}`);
|
||
setTimeout(cleanupTempFiles, 5000);
|
||
});
|