diff --git a/backend/dist/index.js b/backend/dist/index.js index c6eca27..691cf72 100644 --- a/backend/dist/index.js +++ b/backend/dist/index.js @@ -1958,18 +1958,21 @@ var PROVIDER_DISPLAY_MAP = { function getProviderDisplayName(providerName) { return PROVIDER_DISPLAY_MAP[providerName] || `\u{1F4E6} ${providerName}`; } -function buildTaskControlLines(taskId) { +function buildTaskControlLines(taskId, queuePaused = false, pauseReason) { if (!taskId) return [`\u{1F4A1} \u53D1\u9001 /tasks \u67E5\u770B\u5B9E\u65F6\u4EFB\u52A1\u72B6\u6001`]; + if (queuePaused) { + return [ + `\u23F8\uFE0F **\u5F53\u524D\u72B6\u6001\uFF1A\u5DF2\u6682\u505C**`, + pauseReason ? `\u{1F4CC} \u539F\u56E0\uFF1A${pauseReason}` : `\u{1F4CC} \u7B49\u5F85\u4E2D\u7684\u4E0B\u8F7D\u4EFB\u52A1\u4E0D\u4F1A\u7EE7\u7EED\u5F00\u59CB`, + `\u25B6\uFE0F \u70B9\u51FB\u4E0B\u65B9\u201C\u7EE7\u7EED\u201D\u53EF\u6062\u590D\u540E\u53F0\u4E0B\u8F7D\u961F\u5217`, + `\u{1F6D1} \u70B9\u51FB\u201C\u53D6\u6D88\u201D\u53EF\u505C\u6B62\u5F53\u524D\u540E\u53F0\u4EFB\u52A1\u5E76\u6E05\u7A7A\u7B49\u5F85\u9879` + ]; + } return [ `\u{1F4A1} \u4E0B\u8F7D\u961F\u5217\u63A7\u5236\uFF1A\u8BF7\u4F18\u5148\u70B9\u4E0B\u65B9\u6309\u94AE`, `\u23F8 \u6682\u505C\uFF1A\u6682\u65F6\u505C\u6B62\u7EE7\u7EED\u4E0B\u8F7D\uFF0C\u5DF2\u5728\u5904\u7406\u7684\u4EFB\u52A1\u4F1A\u5C3D\u5FEB\u505C\u4F4F`, `\u25B6\uFE0F \u7EE7\u7EED\uFF1A\u4ECE\u6682\u505C\u5904\u6062\u590D\u540E\u53F0\u4E0B\u8F7D\u961F\u5217`, - `\u{1F6D1} \u53D6\u6D88\uFF1A\u505C\u6B62\u5F53\u524D\u540E\u53F0\u4EFB\u52A1\u5E76\u6E05\u7A7A\u7B49\u5F85\u9879`, - ``, - `\u5907\u7528\u624B\u52A8\u547D\u4EE4\uFF1A`, - `/task_pause ${taskId}`, - `/task_resume ${taskId}`, - `/task_cancel ${taskId}` + `\u{1F6D1} \u53D6\u6D88\uFF1A\u505C\u6B62\u5F53\u524D\u540E\u53F0\u4EFB\u52A1\u5E76\u6E05\u7A7A\u7B49\u5F85\u9879` ]; } function buildTaskControlButtons(taskId) { @@ -2310,17 +2313,17 @@ function buildDeleteSuccess(fileName, fileId) { `\u{1F5D1}\uFE0F ID: ${fileId}` ].join("\n"); } -function buildSilentModeNotice(fileCount, taskId) { +function buildSilentModeNotice(fileCount, taskId, queuePaused = false, pauseReason) { return [ - `\u{1F910} **\u5DF2\u5207\u6362\u5230\u9759\u9ED8\u6A21\u5F0F**`, + queuePaused ? `\u23F8\uFE0F **\u540E\u53F0\u4E0B\u8F7D\u5DF2\u6682\u505C**` : `\u{1F910} **\u5DF2\u5207\u6362\u5230\u9759\u9ED8\u6A21\u5F0F**`, ...taskId ? [`\u{1F194} \u4EFB\u52A1\uFF1A\`${taskId}\``] : [], ``, - `Bot \u5C06\u5728\u540E\u53F0\u7EE7\u7EED\u5904\u7406\u6240\u6709\u6587\u4EF6\uFF0C\u8BF7\u8010\u5FC3\u7B49\u5F85\u3002`, + queuePaused ? `\u7B49\u5F85\u4EFB\u52A1\u5DF2\u6682\u505C\uFF0C\u4E0D\u4F1A\u7EE7\u7EED\u5F00\u59CB\u65B0\u7684\u4E0B\u8F7D\u3002` : `Bot \u5C06\u5728\u540E\u53F0\u7EE7\u7EED\u5904\u7406\u6240\u6709\u6587\u4EF6\uFF0C\u8BF7\u8010\u5FC3\u7B49\u5F85\u3002`, ``, - ...buildTaskControlLines(taskId) + ...buildTaskControlLines(taskId, queuePaused, pauseReason) ].join("\n"); } -function buildSilentProgress(sessionTotal, batches, singleFiles = [], sessionCompleted = 0, sessionFailed = 0, taskId) { +function buildSilentProgress(sessionTotal, batches, singleFiles = [], sessionCompleted = 0, sessionFailed = 0, taskId, queuePaused = false, pauseReason) { const totalBatchFiles = batches.reduce((sum, batch) => sum + batch.totalFiles, 0); const completedBatchFiles = batches.reduce((sum, batch) => sum + batch.completed, 0); const successfulBatchFiles = batches.reduce((sum, batch) => sum + batch.successful, 0); @@ -2332,12 +2335,16 @@ function buildSilentProgress(sessionTotal, batches, singleFiles = [], sessionCom const failedFiles = Math.max(sessionFailed, failedBatchFiles + failedSingleFiles); const successfulFiles = Math.max(0, completedFiles - failedFiles); const remainingFiles = Math.max(0, totalFiles - completedFiles); + const isComplete = totalFiles > 0 && remainingFiles === 0; const activeBatch = batches.find((batch) => batch.completed < batch.totalFiles); const activeSingle = singleFiles.find((file) => !["success", "failed"].includes(file.phase)); const currentFile = activeBatch?.currentFileName || activeSingle?.fileName; const progress = generateProgressBar(completedFiles, Math.max(totalFiles, 1)); + if (isComplete) { + return buildSilentAllTasksComplete(totalFiles, failedFiles, taskId, singleFiles, batches); + } return [ - `\u{1F910} **\u540E\u53F0\u6279\u91CF\u5904\u7406\u4E2D**`, + queuePaused ? `\u23F8\uFE0F **\u540E\u53F0\u4E0B\u8F7D\u5DF2\u6682\u505C**` : `\u{1F910} **\u540E\u53F0\u6279\u91CF\u5904\u7406\u4E2D**`, `${progress} (${completedFiles}/${totalFiles})`, ``, `\u2705 \u6210\u529F: ${successfulFiles}\u3000\u274C \u5931\u8D25: ${failedFiles}\u3000\u23F3 \u5269\u4F59: ${remainingFiles}`, @@ -2345,7 +2352,7 @@ function buildSilentProgress(sessionTotal, batches, singleFiles = [], sessionCom ...activeBatch ? [`\u{1F4C1} \u6279\u6B21: ${activeBatch.folderName}`] : [], ...activeBatch?.queuePending ? [`\u{1F552} \u961F\u5217\u7B49\u5F85: ${activeBatch.queuePending}`] : [], ``, - ...buildTaskControlLines(taskId), + ...buildTaskControlLines(taskId, queuePaused, pauseReason), ...taskId && failedFiles > 0 && remainingFiles === 0 ? [`\u{1F504} \u68C0\u6D4B\u5230\u5931\u8D25\u4EFB\u52A1\uFF0C\u53EF\u53D1\u9001 /tg_retry ${taskId} \u91CD\u8BD5\u6700\u8FD1\u5931\u8D25\u9879`] : [] ].join("\n"); } @@ -3137,7 +3144,8 @@ async function ensureSilentNotice(client2, chatId, fileCount, replyToMsg) { } if (silentNoticeMessageIdMap.get(chatIdStr)) return; } - const text = buildSilentModeNotice(fileCount, getSessionTaskId(chatIdStr)); + const queueStats = getDownloadQueueStats(); + const text = buildSilentModeNotice(fileCount, getSessionTaskId(chatIdStr), queueStats.paused, queueStats.pauseReason); const sendPromise = (async () => { let sMsg; if (replyToMsg) { @@ -3256,7 +3264,8 @@ var BetterDownloadQueue = class { total: this.active.length + this.queue.length, paused: this.paused, diskPressurePaused: this.diskPressurePaused, - diskPressureReason: this.diskPressureReason + diskPressureReason: this.diskPressureReason, + pauseReason: this.diskPressureReason || (this.paused ? "\u7528\u6237\u5DF2\u6682\u505C\u4E0B\u8F7D\u961F\u5217" : void 0) }; } getDetailedStatus() { @@ -3266,7 +3275,8 @@ var BetterDownloadQueue = class { history: [...this.history], paused: this.paused, diskPressurePaused: this.diskPressurePaused, - diskPressureReason: this.diskPressureReason + diskPressureReason: this.diskPressureReason, + pauseReason: this.diskPressureReason || (this.paused ? "\u7528\u6237\u5DF2\u6682\u505C\u4E0B\u8F7D\u961F\u5217" : void 0) }; } // Update progress method @@ -3431,7 +3441,8 @@ async function finalizeSilentSessionIfDone(client2, chatId) { })) ] ); - await safeEditMessage(client2, chatId, { message: silentMsgId, text, buttons: new Api2.ReplyInlineMarkup({ rows: [] }) }); + const controls = s?.failed ? buildTaskControlButtons(s?.taskId) : new Api2.ReplyInlineMarkup({ rows: [] }); + await safeEditMessage(client2, chatId, { message: silentMsgId, text, buttons: controls }); } silentSessionMap.delete(chatIdStr); if (s?.taskId) taskIdToChatId.delete(s.taskId); @@ -3671,8 +3682,25 @@ async function refreshSilentProgress(client2, chatId) { const session = syncSilentSessionTotals(chatIdStr) || getSilentSession(chatIdStr); const batches = getConsolidatedBatches(chatIdStr); const files = getConsolidatedFiles(chatIdStr); - const text = buildSilentProgress(session.total, batches, files, session.completed, session.failed, session.taskId); - await safeEditMessage(client2, chatId, { message: silentMsgId, text, buttons: buildTaskControlButtons(session.taskId) }); + const totalBatchFiles = batches.reduce((sum, batch) => sum + batch.totalFiles, 0); + const completedBatchFiles = batches.reduce((sum, batch) => sum + batch.completed, 0); + const completedSingleFiles = files.filter((file) => file.phase === "success" || file.phase === "failed").length; + const totalFiles = Math.max(session.total, totalBatchFiles + files.length, completedBatchFiles + completedSingleFiles, session.completed); + const completedFiles = Math.max(session.completed, completedBatchFiles + completedSingleFiles); + const isComplete = totalFiles > 0 && completedFiles >= totalFiles; + const queueStats = getDownloadQueueStats(); + const text = buildSilentProgress( + session.total, + batches, + files, + session.completed, + session.failed, + session.taskId, + queueStats.paused, + queueStats.pauseReason + ); + const buttons = isComplete && session.failed === 0 ? new Api2.ReplyInlineMarkup({ rows: [] }) : buildTaskControlButtons(session.taskId); + await safeEditMessage(client2, chatId, { message: silentMsgId, text, buttons }); } async function checkAndResetSession(client2, chatId) { const chatIdStr = chatId.toString(); @@ -3752,6 +3780,39 @@ function cancelDownloadTask(selector) { } return downloadQueue.cancel(normalized, "\u7528\u6237\u901A\u8FC7 /task_cancel \u53D6\u6D88\u4EFB\u52A1"); } +async function cancelSilentTask(client2, chatId, taskId, fallbackMessageId) { + const mappedChatId = resolveTaskChatId(taskId); + const chatIdStr = mappedChatId || chatId.toString(); + const editChatId = mappedChatId || chatId; + const session = silentSessionMap.get(chatIdStr); + const silentMsgId = silentNoticeMessageIdMap.get(chatIdStr) || fallbackMessageId; + const result = cancelDownloadTask(taskId); + const total = Math.max(session?.total || 0, result.total); + const completed = session?.completed || 0; + const failed = session?.failed || 0; + const pendingOrActive = Math.max(0, total - completed); + if (silentMsgId) { + const text = [ + `\u{1F6D1} **\u540E\u53F0\u4EFB\u52A1\u5DF2\u53D6\u6D88**`, + ``, + `\u{1F194} \u4EFB\u52A1\uFF1A\`${taskId}\``, + `\u2705 \u5DF2\u5B8C\u6210: ${completed} \u4E2A\u6587\u4EF6`, + ...failed > 0 ? [`\u274C \u5931\u8D25: ${failed} \u4E2A\u6587\u4EF6`] : [], + `\u{1F6AB} \u5DF2\u505C\u6B62/\u6E05\u7A7A: ${pendingOrActive} \u4E2A\u7B49\u5F85\u6216\u8FDB\u884C\u4E2D\u7684\u4EFB\u52A1`, + ``, + `\u5DF2\u79FB\u9664\u6682\u505C / \u7EE7\u7EED / \u53D6\u6D88\u6309\u94AE\uFF0C\u6B64\u4EFB\u52A1\u4E0D\u4F1A\u518D\u54CD\u5E94\u65E7\u6309\u94AE\u64CD\u4F5C\u3002` + ].join("\n"); + await safeEditMessage(client2, editChatId, { message: silentMsgId, text, buttons: new Api2.ReplyInlineMarkup({ rows: [] }) }); + } + silentSessionMap.delete(chatIdStr); + if (session?.taskId) taskIdToChatId.delete(session.taskId); + taskIdToChatId.delete(taskId); + silentNoticeMessageIdMap.delete(chatIdStr); + lastSilentNotificationTimeMap.delete(chatIdStr); + clearConsolidatedState(chatIdStr); + resetChatTransferSession(chatIdStr); + return result; +} function retryFailedDownloadTasks(limit = 10, _taskId) { return downloadQueue.retryFailed(limit); } @@ -6369,15 +6430,17 @@ async function handleTaskQueueCallback(update, data) { try { if (action === "pause") { pauseDownloadTasks(taskId); + await refreshSilentProgress(client, update.peer); await client.invoke(new Api4.messages.SetBotCallbackAnswer({ queryId: update.queryId, message: "\u5DF2\u6682\u505C\u4E0B\u8F7D\u961F\u5217" })); return; } if (action === "resume") { resumeDownloadTasks(taskId); + await refreshSilentProgress(client, update.peer); await client.invoke(new Api4.messages.SetBotCallbackAnswer({ queryId: update.queryId, message: "\u5DF2\u7EE7\u7EED\u4E0B\u8F7D\u961F\u5217" })); return; } - cancelDownloadTask(taskId); + await cancelSilentTask(client, update.peer, taskId, update.msgId); await client.invoke(new Api4.messages.SetBotCallbackAnswer({ queryId: update.queryId, message: "\u5DF2\u53D6\u6D88\u540E\u53F0\u4EFB\u52A1", alert: true })); } catch (error) { await client.invoke(new Api4.messages.SetBotCallbackAnswer({ diff --git a/backend/src/services/telegramBot.ts b/backend/src/services/telegramBot.ts index 3ab21a4..c55fb68 100644 --- a/backend/src/services/telegramBot.ts +++ b/backend/src/services/telegramBot.ts @@ -8,7 +8,7 @@ import { storageManager } from '../services/storage.js'; import { authenticatedUsers, passwordInputState, isAuthenticated, loadAuthenticatedUsers, persistAuthenticatedUser, userStates, TelegramUserState } from './telegramState.js'; import { is2FAEnabled, generateOTPAuthUrl, verifyTOTP, activate2FA } from '../utils/security.js'; import { handleStart, handleHelp, handleStorage, handleList, handleDelete, handleTasks, handleStopTasks, handlePauseTasks, handleResumeTasks, handleCancelTask, handleRetryFailedTasks, handleDownloadWorkers, handleDownloadWorkersCallback, handlePathRules, handlePathRulesCallback, handleDuplicateMode, handleDuplicateModeCallback, handleCleanupSettings, handleCleanupSettingsCallback } from './telegramCommands.js'; -import { handleFileUpload, handleCleanupCallback, pauseDownloadTasks, resumeDownloadTasks, cancelDownloadTask, resolveTaskChatIdForControl } from './telegramUpload.js'; +import { handleFileUpload, handleCleanupCallback, pauseDownloadTasks, resumeDownloadTasks, resolveTaskChatIdForControl, refreshSilentProgress, cancelSilentTask } from './telegramUpload.js'; import { handleYtDlpCommand } from './ytDlpDownload.js'; import { enqueueTelegramDateDownload, @@ -523,15 +523,17 @@ async function handleTaskQueueCallback(update: Api.UpdateBotCallbackQuery, data: try { if (action === 'pause') { pauseDownloadTasks(taskId); + await refreshSilentProgress(client, update.peer); await client.invoke(new Api.messages.SetBotCallbackAnswer({ queryId: update.queryId, message: '已暂停下载队列' })); return; } if (action === 'resume') { resumeDownloadTasks(taskId); + await refreshSilentProgress(client, update.peer); await client.invoke(new Api.messages.SetBotCallbackAnswer({ queryId: update.queryId, message: '已继续下载队列' })); return; } - cancelDownloadTask(taskId); + await cancelSilentTask(client, update.peer, taskId, update.msgId); await client.invoke(new Api.messages.SetBotCallbackAnswer({ queryId: update.queryId, message: '已取消后台任务', alert: true })); } catch (error) { await client.invoke(new Api.messages.SetBotCallbackAnswer({ diff --git a/backend/src/services/telegramUpload.ts b/backend/src/services/telegramUpload.ts index 71932a8..a65209e 100644 --- a/backend/src/services/telegramUpload.ts +++ b/backend/src/services/telegramUpload.ts @@ -262,7 +262,8 @@ async function ensureSilentNotice(client: TelegramClient, chatId: Api.TypeEntity if (silentNoticeMessageIdMap.get(chatIdStr)) return; } - const text = buildSilentModeNotice(fileCount, getSessionTaskId(chatIdStr)); + const queueStats = getDownloadQueueStats(); + const text = buildSilentModeNotice(fileCount, getSessionTaskId(chatIdStr), queueStats.paused, queueStats.pauseReason); const sendPromise = (async () => { let sMsg: any; @@ -416,6 +417,7 @@ class BetterDownloadQueue { paused: this.paused, diskPressurePaused: this.diskPressurePaused, diskPressureReason: this.diskPressureReason, + pauseReason: this.diskPressureReason || (this.paused ? '用户已暂停下载队列' : undefined), }; } @@ -427,6 +429,7 @@ class BetterDownloadQueue { paused: this.paused, diskPressurePaused: this.diskPressurePaused, diskPressureReason: this.diskPressureReason, + pauseReason: this.diskPressureReason || (this.paused ? '用户已暂停下载队列' : undefined), }; } @@ -635,7 +638,8 @@ async function finalizeSilentSessionIfDone(client: TelegramClient, chatId: Api.T })), ], ); - await safeEditMessage(client, chatId, { message: silentMsgId, text, buttons: new Api.ReplyInlineMarkup({ rows: [] }) }); + const controls = s?.failed ? buildTaskControlButtons(s?.taskId) : new Api.ReplyInlineMarkup({ rows: [] }); + await safeEditMessage(client, chatId, { message: silentMsgId, text, buttons: controls }); } // 清理静默状态 @@ -956,7 +960,7 @@ function syncSilentSessionTotals(chatIdStr: string): SilentSession | null { return session; } -async function refreshSilentProgress(client: TelegramClient, chatId: Api.TypeEntityLike) { +export async function refreshSilentProgress(client: TelegramClient, chatId: Api.TypeEntityLike) { const chatIdStr = chatId.toString(); if (!silentSessionMap.has(chatIdStr)) return; const silentMsgId = silentNoticeMessageIdMap.get(chatIdStr); @@ -965,8 +969,27 @@ async function refreshSilentProgress(client: TelegramClient, chatId: Api.TypeEnt const session = syncSilentSessionTotals(chatIdStr) || getSilentSession(chatIdStr); const batches = getConsolidatedBatches(chatIdStr); const files = getConsolidatedFiles(chatIdStr); - const text = buildSilentProgress(session.total, batches, files, session.completed, session.failed, session.taskId); - await safeEditMessage(client, chatId, { message: silentMsgId, text, buttons: buildTaskControlButtons(session.taskId) }); + const totalBatchFiles = batches.reduce((sum, batch) => sum + batch.totalFiles, 0); + const completedBatchFiles = batches.reduce((sum, batch) => sum + batch.completed, 0); + const completedSingleFiles = files.filter(file => file.phase === 'success' || file.phase === 'failed').length; + const totalFiles = Math.max(session.total, totalBatchFiles + files.length, completedBatchFiles + completedSingleFiles, session.completed); + const completedFiles = Math.max(session.completed, completedBatchFiles + completedSingleFiles); + const isComplete = totalFiles > 0 && completedFiles >= totalFiles; + const queueStats = getDownloadQueueStats(); + const text = buildSilentProgress( + session.total, + batches, + files, + session.completed, + session.failed, + session.taskId, + queueStats.paused, + queueStats.pauseReason, + ); + const buttons = isComplete && session.failed === 0 + ? new Api.ReplyInlineMarkup({ rows: [] }) + : buildTaskControlButtons(session.taskId); + await safeEditMessage(client, chatId, { message: silentMsgId, text, buttons }); } @@ -1079,6 +1102,43 @@ export function cancelDownloadTask(selector?: string) { return downloadQueue.cancel(normalized, '用户通过 /task_cancel 取消任务'); } +export async function cancelSilentTask(client: TelegramClient, chatId: Api.TypeEntityLike, taskId: string, fallbackMessageId?: number) { + const mappedChatId = resolveTaskChatId(taskId); + const chatIdStr = mappedChatId || chatId.toString(); + const editChatId = mappedChatId || chatId; + const session = silentSessionMap.get(chatIdStr); + const silentMsgId = silentNoticeMessageIdMap.get(chatIdStr) || fallbackMessageId; + const result = cancelDownloadTask(taskId); + const total = Math.max(session?.total || 0, result.total); + const completed = session?.completed || 0; + const failed = session?.failed || 0; + const pendingOrActive = Math.max(0, total - completed); + + if (silentMsgId) { + const text = [ + `🛑 **后台任务已取消**`, + ``, + `🆔 任务:\`${taskId}\``, + `✅ 已完成: ${completed} 个文件`, + ...(failed > 0 ? [`❌ 失败: ${failed} 个文件`] : []), + `🚫 已停止/清空: ${pendingOrActive} 个等待或进行中的任务`, + ``, + `已移除暂停 / 继续 / 取消按钮,此任务不会再响应旧按钮操作。`, + ].join('\n'); + await safeEditMessage(client, editChatId, { message: silentMsgId, text, buttons: new Api.ReplyInlineMarkup({ rows: [] }) }); + } + + silentSessionMap.delete(chatIdStr); + if (session?.taskId) taskIdToChatId.delete(session.taskId); + taskIdToChatId.delete(taskId); + silentNoticeMessageIdMap.delete(chatIdStr); + lastSilentNotificationTimeMap.delete(chatIdStr); + clearConsolidatedState(chatIdStr); + resetChatTransferSession(chatIdStr); + + return result; +} + export function retryFailedDownloadTasks(limit = 10, _taskId?: string) { // 当前失败历史来自全局队列;taskId 仅用于用户侧定位任务提示。 return downloadQueue.retryFailed(limit); diff --git a/backend/src/utils/telegramMessages.ts b/backend/src/utils/telegramMessages.ts index 11d04a0..7b02c3b 100644 --- a/backend/src/utils/telegramMessages.ts +++ b/backend/src/utils/telegramMessages.ts @@ -22,18 +22,21 @@ export function getProviderDisplayName(providerName: string): string { return PROVIDER_DISPLAY_MAP[providerName] || `📦 ${providerName}`; } -function buildTaskControlLines(taskId?: string): string[] { +function buildTaskControlLines(taskId?: string, queuePaused = false, pauseReason?: string): string[] { if (!taskId) return [`💡 发送 /tasks 查看实时任务状态`]; + if (queuePaused) { + return [ + `⏸️ **当前状态:已暂停**`, + pauseReason ? `📌 原因:${pauseReason}` : `📌 等待中的下载任务不会继续开始`, + `▶️ 点击下方“继续”可恢复后台下载队列`, + `🛑 点击“取消”可停止当前后台任务并清空等待项`, + ]; + } return [ `💡 下载队列控制:请优先点下方按钮`, `⏸ 暂停:暂时停止继续下载,已在处理的任务会尽快停住`, `▶️ 继续:从暂停处恢复后台下载队列`, `🛑 取消:停止当前后台任务并清空等待项`, - ``, - `备用手动命令:`, - `/task_pause ${taskId}`, - `/task_resume ${taskId}`, - `/task_cancel ${taskId}`, ]; } @@ -505,14 +508,14 @@ export function buildDeleteSuccess(fileName: string, fileId: string): string { // ─── 多文件上传 ────────────────────────────────────────────── /** 静默模式通知 */ -export function buildSilentModeNotice(fileCount: number, taskId?: string): string { +export function buildSilentModeNotice(fileCount: number, taskId?: string, queuePaused = false, pauseReason?: string): string { return [ - `🤐 **已切换到静默模式**`, + queuePaused ? `⏸️ **后台下载已暂停**` : `🤐 **已切换到静默模式**`, ...(taskId ? [`🆔 任务:\`${taskId}\``] : []), ``, - `Bot 将在后台继续处理所有文件,请耐心等待。`, + queuePaused ? `等待任务已暂停,不会继续开始新的下载。` : `Bot 将在后台继续处理所有文件,请耐心等待。`, ``, - ...buildTaskControlLines(taskId), + ...buildTaskControlLines(taskId, queuePaused, pauseReason), ].join('\n'); } @@ -544,6 +547,8 @@ export function buildSilentProgress( sessionCompleted: number = 0, sessionFailed: number = 0, taskId?: string, + queuePaused = false, + pauseReason?: string, ): string { const totalBatchFiles = batches.reduce((sum, batch) => sum + batch.totalFiles, 0); const completedBatchFiles = batches.reduce((sum, batch) => sum + batch.completed, 0); @@ -556,13 +561,17 @@ export function buildSilentProgress( const failedFiles = Math.max(sessionFailed, failedBatchFiles + failedSingleFiles); const successfulFiles = Math.max(0, completedFiles - failedFiles); const remainingFiles = Math.max(0, totalFiles - completedFiles); + const isComplete = totalFiles > 0 && remainingFiles === 0; const activeBatch = batches.find(batch => batch.completed < batch.totalFiles); const activeSingle = singleFiles.find(file => !['success', 'failed'].includes(file.phase)); const currentFile = activeBatch?.currentFileName || activeSingle?.fileName; const progress = generateProgressBar(completedFiles, Math.max(totalFiles, 1)); + if (isComplete) { + return buildSilentAllTasksComplete(totalFiles, failedFiles, taskId, singleFiles, batches); + } return [ - `🤐 **后台批量处理中**`, + queuePaused ? `⏸️ **后台下载已暂停**` : `🤐 **后台批量处理中**`, `${progress} (${completedFiles}/${totalFiles})`, ``, `✅ 成功: ${successfulFiles} ❌ 失败: ${failedFiles} ⏳ 剩余: ${remainingFiles}`, @@ -570,7 +579,7 @@ export function buildSilentProgress( ...(activeBatch ? [`📁 批次: ${activeBatch.folderName}`] : []), ...(activeBatch?.queuePending ? [`🕒 队列等待: ${activeBatch.queuePending}`] : []), ``, - ...buildTaskControlLines(taskId), + ...buildTaskControlLines(taskId, queuePaused, pauseReason), ...(taskId && failedFiles > 0 && remainingFiles === 0 ? [`🔄 检测到失败任务,可发送 /tg_retry ${taskId} 重试最近失败项`] : []), ].join('\n'); }