From 0426591d4dfdc1bfdcabf3d41c5d3f9c12c14976 Mon Sep 17 00:00:00 2001 From: axibayuit Date: Sat, 3 Jan 2026 00:35:27 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=85=AC=E5=BC=80=E6=B5=8F=E8=A7=88?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=E5=A2=9E=E5=BC=BA=20&=20Discord=20API=20429?= =?UTF-8?q?=E9=87=8D=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- functions/api/public/list.js | 67 ++++++++++++++++++++++++++++++----- functions/utils/discordAPI.js | 48 +++++++++++++++++-------- 2 files changed, 92 insertions(+), 23 deletions(-) diff --git a/functions/api/public/list.js b/functions/api/public/list.js index 4c23985..0be1e2a 100644 --- a/functions/api/public/list.js +++ b/functions/api/public/list.js @@ -84,9 +84,17 @@ export async function onRequest(context) { const allowedDirStr = publicBrowse.allowedDir || ''; let allowedDirs = allowedDirStr.split(',').map(d => d.trim()).filter(d => d); - // 获取请求的目录 + // 获取请求的目录和搜索参数 let dir = url.searchParams.get('dir') || ''; - + let search = url.searchParams.get('search') || ''; + if (search) { + search = decodeURIComponent(search).trim(); + } + + // 获取高级搜索参数 + const recursive = url.searchParams.get('recursive') === 'true'; + const fileType = url.searchParams.get('type') || ''; // image, video, audio, other + // 检查目录权限 if (!isAllowedDirectory(dir, allowedDirs)) { return new Response(JSON.stringify({ error: 'Directory not allowed' }), { @@ -107,12 +115,13 @@ export async function onRequest(context) { const start = parseInt(url.searchParams.get('start'), 10) || 0; const count = parseInt(url.searchParams.get('count'), 10) || 50; - // 读取文件列表 + // 读取文件列表(获取全部,因为需要先过滤 block/adult) const result = await readIndex(context, { directory: dir, - start, - count, - includeSubdirFiles: false, + search, + start: 0, + count: -1, // 获取全部 + includeSubdirFiles: recursive, }); if (!result.success) { @@ -127,8 +136,48 @@ export async function onRequest(context) { return isAllowedDirectory(subDir, allowedDirs); }); + // 文件类型过滤辅助函数 + const imageExts = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'svg', 'avif']; + const videoExts = ['mp4', 'webm', 'ogg', 'mov', 'm4v', 'mkv', 'avi', '3gp', 'mpeg', 'mpg', 'flv', 'wmv', 'ts', 'rmvb']; + const audioExts = ['mp3', 'wav', 'ogg', 'flac', 'aac', 'm4a', 'wma', 'ape', 'opus']; + + const getFileExt = (name) => (name.split('.').pop() || '').toLowerCase(); + const isImageFile = (name) => imageExts.includes(getFileExt(name)); + const isVideoFile = (name) => videoExts.includes(getFileExt(name)); + const isAudioFile = (name) => audioExts.includes(getFileExt(name)); + + // 过滤掉 block 和 adult 图片(公开浏览不应显示这些内容) + let filteredFiles = result.files.filter(file => { + const listType = file.metadata?.ListType; + const label = file.metadata?.Label; + // 排除被屏蔽的和成人内容 + if (listType === 'Block' || label === 'adult') { + return false; + } + return true; + }); + + // 按文件类型过滤 + if (fileType) { + filteredFiles = filteredFiles.filter(file => { + const name = file.id; + switch (fileType) { + case 'image': return isImageFile(name); + case 'video': return isVideoFile(name); + case 'audio': return isAudioFile(name); + case 'other': return !isImageFile(name) && !isVideoFile(name) && !isAudioFile(name); + default: return true; + } + }); + } + + // 计算过滤后的总数和分页 + const filteredTotalCount = filteredFiles.length; + // 过滤后再分页 + filteredFiles = filteredFiles.slice(start, start + count); + // 转换文件格式(只返回必要信息,隐藏敏感元数据) - const safeFiles = result.files.map(file => ({ + const safeFiles = filteredFiles.map(file => ({ name: file.id, metadata: { FileType: file.metadata?.FileType, @@ -141,8 +190,8 @@ export async function onRequest(context) { return new Response(JSON.stringify({ files: safeFiles, directories: filteredDirectories, - totalCount: result.totalCount, - returnedCount: result.returnedCount, + totalCount: fileType ? filteredTotalCount : result.totalCount, + returnedCount: safeFiles.length, allowedDirs: allowedDirs, // 返回允许的目录列表供前端使用 }), { headers: { 'Content-Type': 'application/json', ...corsHeaders } diff --git a/functions/utils/discordAPI.js b/functions/utils/discordAPI.js index 063a940..15eba2d 100644 --- a/functions/utils/discordAPI.js +++ b/functions/utils/discordAPI.js @@ -83,26 +83,46 @@ export class DiscordAPI { * 获取消息信息(用于获取文件 URL) * @param {string} channelId - 频道 ID * @param {string} messageId - 消息 ID + * @param {number} maxRetries - 最大重试次数(默认 3 次) * @returns {Promise} 消息数据或 null */ - async getMessage(channelId, messageId) { - try { - const response = await fetch(`${this.baseURL}/channels/${channelId}/messages/${messageId}`, { - method: 'GET', - headers: this.defaultHeaders - }); + async getMessage(channelId, messageId, maxRetries = 3) { + for (let attempt = 0; attempt <= maxRetries; attempt++) { + try { + const response = await fetch(`${this.baseURL}/channels/${channelId}/messages/${messageId}`, { + method: 'GET', + headers: this.defaultHeaders + }); - if (!response.ok) { - console.error('Discord getMessage error:', response.status, response.statusText); + // 429 速率限制:等待后重试 + if (response.status === 429) { + const retryAfter = response.headers.get('Retry-After'); + const waitTime = retryAfter ? parseFloat(retryAfter) * 1000 : 1000 * (attempt + 1); + console.warn(`Discord 429 rate limit, waiting ${waitTime}ms before retry ${attempt + 1}/${maxRetries}`); + + if (attempt < maxRetries) { + await new Promise(resolve => setTimeout(resolve, waitTime)); + continue; + } + } + + if (!response.ok) { + console.error('Discord getMessage error:', response.status, response.statusText); + return null; + } + + const messageData = await response.json(); + return messageData; + } catch (error) { + console.error('Error getting Discord message:', error.message); + if (attempt < maxRetries) { + await new Promise(resolve => setTimeout(resolve, 500 * (attempt + 1))); + continue; + } return null; } - - const messageData = await response.json(); - return messageData; - } catch (error) { - console.error('Error getting Discord message:', error.message); - return null; } + return null; } /**