- download_stat 新增 _task_queue / _completed_chats 与三个辅助函数 - 切频道前自动把上一个频道的最终进度快照进 completed_chats - save_and_restart(_multi) 启动任务时设置队列,供前端渲染总览 - 前端 banner 改为"任务队列"多卡片列表(✅已完成 / 🚀当前 / ⏳排队中), 含进度条、跳过明细;兼容单频道场景 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -18,6 +18,7 @@ from module.download_stat import (
|
||||
update_download_status,
|
||||
update_task_progress,
|
||||
reset_task_progress,
|
||||
snapshot_current_chat,
|
||||
increment_task_stat,
|
||||
get_task_progress,
|
||||
is_message_skipped,
|
||||
@@ -602,6 +603,8 @@ async def download_chat_task(
|
||||
node: TaskNode,
|
||||
):
|
||||
"""Download all task"""
|
||||
# 切到下一个频道前,先把上一个频道的最终进度快照进 _completed_chats
|
||||
snapshot_current_chat()
|
||||
# Reset and update task progress
|
||||
reset_task_progress()
|
||||
|
||||
|
||||
+55
-2
@@ -43,6 +43,12 @@ _task_progress: dict = {
|
||||
"last_update": 0,
|
||||
}
|
||||
|
||||
# 多频道任务队列与已完成快照(独立于 _task_progress,不被单频道 reset 影响)
|
||||
# _task_queue: 任务开始时由 API 设置,[{chat_id, chat_title}, ...]
|
||||
# _completed_chats: 每切换一个频道前把上一个的最终进度快照到这里
|
||||
_task_queue: list = []
|
||||
_completed_chats: list = []
|
||||
|
||||
|
||||
def get_download_result() -> dict:
|
||||
"""get global download result"""
|
||||
@@ -62,7 +68,7 @@ def get_download_state() -> DownloadState:
|
||||
def get_task_progress() -> dict:
|
||||
"""get task progress with auto-detection of checking state"""
|
||||
progress = _task_progress.copy()
|
||||
|
||||
|
||||
# Auto-detect if still checking based on last_update time
|
||||
# If last update was within 3 seconds, consider it still active
|
||||
if progress["current_chat"] and progress["last_update"] > 0:
|
||||
@@ -73,10 +79,57 @@ def get_task_progress() -> dict:
|
||||
progress["is_checking"] = True
|
||||
elif progress["skipped_files"] > 0 or progress["checked_messages"] > 0:
|
||||
progress["is_checking"] = False
|
||||
|
||||
|
||||
# 附加多频道任务总览,供前端渲染队列卡片
|
||||
progress["task_queue"] = list(_task_queue)
|
||||
progress["completed_chats"] = list(_completed_chats)
|
||||
return progress
|
||||
|
||||
|
||||
def snapshot_current_chat():
|
||||
"""把当前 _task_progress 的核心字段快照进 _completed_chats。current_chat 为空时 no-op。
|
||||
在多频道串行下载时,每个 download_chat_task 开始前调用,保留上一个频道的最终进度。
|
||||
"""
|
||||
global _completed_chats
|
||||
chat_id = _task_progress.get("current_chat", "")
|
||||
if not chat_id:
|
||||
return
|
||||
qual = _task_progress.get("qualified_files", 0) or 0
|
||||
est = _task_progress.get("estimated_total", 0) or 0
|
||||
existing = _task_progress.get("existing_skipped", 0) or 0
|
||||
raw_total = est or qual
|
||||
real_total = max(0, raw_total - existing)
|
||||
_completed_chats.append({
|
||||
"chat_id": chat_id,
|
||||
"chat_title": _task_progress.get("current_chat_title", "") or chat_id,
|
||||
"done": _task_progress.get("completed_files", 0) or 0,
|
||||
"total": real_total,
|
||||
"skip": _task_progress.get("skipped_files", 0) or 0,
|
||||
"existing_skip": existing,
|
||||
"failed": _task_progress.get("failed_files", 0) or 0,
|
||||
})
|
||||
|
||||
|
||||
def set_task_queue(items: list):
|
||||
"""设置本次任务的完整频道队列。items 每项 {chat_id, chat_title}。"""
|
||||
global _task_queue
|
||||
_task_queue = []
|
||||
for it in (items or []):
|
||||
cid = str(it.get("chat_id", "") or "").strip()
|
||||
if not cid:
|
||||
continue
|
||||
_task_queue.append({
|
||||
"chat_id": cid,
|
||||
"chat_title": it.get("chat_title", "") or cid,
|
||||
})
|
||||
|
||||
|
||||
def clear_completed_chats():
|
||||
"""新任务启动时清空已完成列表。"""
|
||||
global _completed_chats
|
||||
_completed_chats = []
|
||||
|
||||
|
||||
def update_task_progress(
|
||||
current_chat: str = None,
|
||||
current_chat_title: str = None,
|
||||
|
||||
+152
-72
@@ -195,17 +195,42 @@
|
||||
.card-hd-badge { font-size: 11px; font-weight: 400; color: var(--muted); }
|
||||
.card-bd { padding: 12px 14px; }
|
||||
|
||||
/* 任务横幅 */
|
||||
/* 任务队列总览 */
|
||||
.task-banner {
|
||||
display: none; padding: 10px 14px;
|
||||
border-radius: 8px;
|
||||
background: var(--surface); border: 1px solid var(--border);
|
||||
}
|
||||
.task-banner.show { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
|
||||
.task-banner-name { font-weight: 600; color: var(--accent); font-size: 13px; }
|
||||
.task-banner-filter { font-size: 11px; color: var(--muted); margin-top: 3px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 480px; }
|
||||
.task-banner-state { font-size: 12px; color: var(--muted); margin-top: 2px; }
|
||||
.task-banner-left { min-width: 0; flex: 1; }
|
||||
.task-banner.show { display: block; }
|
||||
.qbanner-head {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
font-size: 12px; color: var(--muted); margin-bottom: 8px;
|
||||
}
|
||||
.qbanner-title { font-weight: 600; color: var(--text); font-size: 13px; }
|
||||
.qbanner-summary { font-size: 11px; }
|
||||
|
||||
/* 队列卡片(已完成 / 当前 / 排队中) */
|
||||
.qcard {
|
||||
display: flex; align-items: flex-start; gap: 10px;
|
||||
padding: 8px 10px; border-radius: 6px;
|
||||
margin-bottom: 6px;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
.qcard:last-child { margin-bottom: 0; }
|
||||
.qcard-icon { font-size: 14px; line-height: 1.5; flex-shrink: 0; }
|
||||
.qcard-main { flex: 1; min-width: 0; }
|
||||
.qcard-name { font-weight: 600; font-size: 13px; color: var(--text);
|
||||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.qcard-state { font-size: 11px; color: var(--muted); margin-top: 2px; }
|
||||
.qcard-prog {
|
||||
height: 4px; background: rgba(255,255,255,.12);
|
||||
border-radius: 2px; overflow: hidden; margin-top: 6px;
|
||||
}
|
||||
.qcard-prog-fill { height: 100%; width: 0%; background: #4caf50; transition: width .3s; }
|
||||
.qcard-active { background: rgba(74,144,226,.08); border-color: rgba(74,144,226,.25); }
|
||||
.qcard-active .qcard-name { color: var(--accent); }
|
||||
.qcard-done { background: rgba(76,175,80,.07); opacity: .85; }
|
||||
.qcard-pending { background: rgba(128,128,128,.06); opacity: .6; }
|
||||
|
||||
/* 历史频道卡片 */
|
||||
.hist-head {
|
||||
@@ -687,9 +712,6 @@
|
||||
/* 下载项操作按钮加大便于点按 */
|
||||
.dl-btn { width: 28px; height: 28px; font-size: 13px; }
|
||||
|
||||
/* 任务横幅过滤表达式不限宽 */
|
||||
.task-banner-filter { max-width: none; }
|
||||
|
||||
/* 列表高度减小 */
|
||||
#dl-list, #done-list { height: 220px; }
|
||||
|
||||
@@ -980,23 +1002,13 @@
|
||||
<!-- ════ MAIN CONTENT ════ -->
|
||||
<div class="main-scroll">
|
||||
|
||||
<!-- 当前任务横幅 -->
|
||||
<!-- 任务队列总览 -->
|
||||
<div class="task-banner" id="task-banner">
|
||||
<div class="task-banner-left">
|
||||
<div class="task-banner-name" id="banner-chat">—</div>
|
||||
<div class="task-banner-filter" id="banner-filter" style="display:none"></div>
|
||||
<div class="task-banner-state" id="banner-state"></div>
|
||||
<!-- 进度条:estimated_total > 0 才展示,展示 已完成 X / N -->
|
||||
<div id="banner-progress"
|
||||
style="display:none;margin-top:6px;display:flex;align-items:center;gap:8px;">
|
||||
<div style="flex:1;height:6px;background:rgba(255,255,255,.12);border-radius:3px;overflow:hidden;">
|
||||
<div id="tbp-fill"
|
||||
style="height:100%;width:0%;background:#4caf50;transition:width .3s;"></div>
|
||||
</div>
|
||||
<div id="tbp-text" style="font-size:11px;color:var(--muted);white-space:nowrap;">0 / 0</div>
|
||||
</div>
|
||||
<div class="qbanner-head">
|
||||
<span class="qbanner-title">任务队列</span>
|
||||
<span class="qbanner-summary" id="qbanner-summary"></span>
|
||||
</div>
|
||||
<div style="font-size:11px;color:var(--muted);flex-shrink:0;">当前任务</div>
|
||||
<div id="qbanner-list"></div>
|
||||
</div>
|
||||
|
||||
<!-- 下载队列(待处理) -->
|
||||
@@ -1126,13 +1138,6 @@
|
||||
return parts.join(' · ');
|
||||
}
|
||||
|
||||
function updateBannerFilter() {
|
||||
const el = document.getElementById('banner-filter');
|
||||
const txt = parseFilterDisplay(currentFilter);
|
||||
if (txt) { el.textContent = txt; el.style.display = ''; }
|
||||
else { el.textContent = ''; el.style.display = 'none'; }
|
||||
}
|
||||
|
||||
function loadConfig() {
|
||||
fetch('/api/get_config').then(r => r.json()).then(d => {
|
||||
const p = d.save_path || '默认路径';
|
||||
@@ -1140,7 +1145,6 @@
|
||||
el.textContent = p; el.title = p;
|
||||
document.getElementById('path-input').value = d.save_path || '';
|
||||
currentFilter = d.download_filter || '';
|
||||
updateBannerFilter();
|
||||
}).catch(() => {});
|
||||
}
|
||||
|
||||
@@ -1448,6 +1452,121 @@
|
||||
}).catch(() => {}).finally(() => { btn.disabled = false; });
|
||||
}
|
||||
|
||||
// ── 任务队列总览渲染 ──
|
||||
function renderTaskQueue(p) {
|
||||
const banner = document.getElementById('task-banner');
|
||||
const list = document.getElementById('qbanner-list');
|
||||
const summary = document.getElementById('qbanner-summary');
|
||||
|
||||
const queue = Array.isArray(p.task_queue) ? p.task_queue : [];
|
||||
const completed = Array.isArray(p.completed_chats) ? p.completed_chats : [];
|
||||
const currentChat = p.current_chat || '';
|
||||
|
||||
// 若没有队列也没有当前任务,隐藏整个 banner
|
||||
if (!queue.length && !currentChat) {
|
||||
banner.classList.remove('show');
|
||||
list.innerHTML = '';
|
||||
summary.textContent = '';
|
||||
return;
|
||||
}
|
||||
banner.classList.add('show');
|
||||
|
||||
// completed 按 chat_id 建索引
|
||||
const completedMap = {};
|
||||
completed.forEach(c => { completedMap[c.chat_id] = c; });
|
||||
|
||||
// 兜底:队列为空但有 current(多频道队列元信息还没写入时),用 current 顶一下
|
||||
const effectiveQueue = queue.length
|
||||
? queue
|
||||
: [{ chat_id: currentChat, chat_title: p.current_chat_title || currentChat }];
|
||||
|
||||
const paused = dlState === 'continue';
|
||||
let doneCount = 0, runningCount = 0;
|
||||
|
||||
list.innerHTML = effectiveQueue.map(q => {
|
||||
if (completedMap[q.chat_id]) {
|
||||
doneCount++;
|
||||
return renderDoneCard(q, completedMap[q.chat_id]);
|
||||
} else if (q.chat_id === currentChat) {
|
||||
runningCount++;
|
||||
return renderActiveCard(q, p, paused);
|
||||
} else {
|
||||
return renderPendingCard(q);
|
||||
}
|
||||
}).join('');
|
||||
|
||||
summary.textContent = `${doneCount}/${effectiveQueue.length} 完成`;
|
||||
}
|
||||
|
||||
function renderDoneCard(q, c) {
|
||||
const title = escapeHtml(q.chat_title || q.chat_id);
|
||||
const skipStr = (c.existing_skip && c.existing_skip > 0)
|
||||
? `跳过${c.skip||0},其中本次跳过${c.existing_skip}`
|
||||
: `跳过${c.skip||0}`;
|
||||
const totalStr = (c.total && c.total > 0)
|
||||
? `${c.done||0} / ${c.total} 完成`
|
||||
: `${c.done||0} 个完成`;
|
||||
return `
|
||||
<div class="qcard qcard-done">
|
||||
<div class="qcard-icon">✅</div>
|
||||
<div class="qcard-main">
|
||||
<div class="qcard-name">${title}</div>
|
||||
<div class="qcard-state">${totalStr},${skipStr}</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function renderActiveCard(q, p, paused) {
|
||||
const title = escapeHtml(q.chat_title || p.current_chat_title || q.chat_id);
|
||||
const dl = p.downloading_files||0, done = p.completed_files||0, skip = p.skipped_files||0;
|
||||
const qual = p.qualified_files||0, est = p.estimated_total||0;
|
||||
const existingSkip = p.existing_skipped || 0;
|
||||
const rawTotal = est || qual;
|
||||
const realTotal = Math.max(0, rawTotal - existingSkip);
|
||||
const doneStr = rawTotal ? `${done} / ${realTotal}` : `${done}`;
|
||||
const skipStr = existingSkip > 0 ? `跳过${skip},其中本次跳过${existingSkip}` : `跳过${skip}`;
|
||||
|
||||
let st = '';
|
||||
if (p.is_checking && paused) st = `⏸ 已暂停 · 扫描中… (${skipStr})`;
|
||||
else if (p.is_checking && dl>0) st = `🔍 扫描+下载中 (${dl}个,${doneStr},${skipStr})`;
|
||||
else if (p.is_checking) st = `🔍 扫描中… (${doneStr},${skipStr})`;
|
||||
else if (paused) st = `⏸ 已暂停 (${doneStr},${skipStr})`;
|
||||
else if (dl>0) st = `🚀 下载中 (${dl}个,${doneStr},${skipStr})`;
|
||||
else if (done>0||skip>0) st = `✅ 完成 (${doneStr},${skipStr})`;
|
||||
else st = '⏳ 等待开始';
|
||||
|
||||
let progBar = '';
|
||||
if (realTotal > 0) {
|
||||
const pct = Math.min(100, Math.round(done * 100 / realTotal));
|
||||
const suffix = (!est && p.is_checking) ? '(扫描中…)' : '';
|
||||
progBar = `
|
||||
<div class="qcard-prog"><div class="qcard-prog-fill" style="width:${pct}%"></div></div>
|
||||
<div class="qcard-state" style="margin-top:3px;">${done} / ${realTotal}${suffix}</div>`;
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="qcard qcard-active">
|
||||
<div class="qcard-icon">🚀</div>
|
||||
<div class="qcard-main">
|
||||
<div class="qcard-name">${title}</div>
|
||||
<div class="qcard-state">${st}</div>
|
||||
${progBar}
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function renderPendingCard(q) {
|
||||
const title = escapeHtml(q.chat_title || q.chat_id);
|
||||
return `
|
||||
<div class="qcard qcard-pending">
|
||||
<div class="qcard-icon">⏳</div>
|
||||
<div class="qcard-main">
|
||||
<div class="qcard-name">${title}</div>
|
||||
<div class="qcard-state">排队中…</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// ── 轮询 ──
|
||||
function poll() {
|
||||
fetch('/get_download_status').then(r => r.json()).then(d => {
|
||||
@@ -1457,46 +1576,7 @@
|
||||
|
||||
fetch('/api/task_progress').then(r => r.json()).then(p => {
|
||||
document.getElementById('h-skip').textContent = p.skipped_files || 0;
|
||||
const banner = document.getElementById('task-banner');
|
||||
const paused = dlState === 'continue';
|
||||
if (p.current_chat) {
|
||||
banner.classList.add('show');
|
||||
const title = p.current_chat_title || p.current_chat;
|
||||
document.getElementById('banner-chat').textContent = title;
|
||||
const dl = p.downloading_files||0, done = p.completed_files||0, skip = p.skipped_files||0;
|
||||
const qual = p.qualified_files||0, est = p.estimated_total||0;
|
||||
const existingSkip = p.existing_skipped || 0;
|
||||
// 分母优先用缓存值;没有就用当次遍历实时累加的 qualified。再扣除"本次任务已跳过",得到真正要下载的数量
|
||||
const rawTotal = est || qual;
|
||||
const realTotal = Math.max(0, rawTotal - existingSkip);
|
||||
const doneStr = rawTotal ? `${done} / ${realTotal}` : `${done}`;
|
||||
// 跳过文案:有本次跳过时追加说明
|
||||
const skipStr = existingSkip > 0 ? `跳过${skip},其中本次跳过${existingSkip}` : `跳过${skip}`;
|
||||
let st = '';
|
||||
if (p.is_checking && paused) st = `⏸ 已暂停 · 扫描中… (${skipStr})`;
|
||||
else if (p.is_checking && dl>0) st = `🔍 扫描+下载中 (${dl}个,${doneStr},${skipStr})`;
|
||||
else if (p.is_checking) st = `🔍 扫描中… (${doneStr},${skipStr})`;
|
||||
else if (paused) st = `⏸ 已暂停 (${doneStr},${skipStr})`;
|
||||
else if (dl>0) st = `🚀 下载中 (${dl}个,${doneStr},${skipStr})`;
|
||||
else if (done>0||skip>0) st = `✅ 完成 (${doneStr},${skipStr})`;
|
||||
else st = '⏳ 等待开始';
|
||||
document.getElementById('banner-state').textContent = st;
|
||||
|
||||
// 进度条:有总数才显示;扫描中且无缓存时给"扫描中…"后缀
|
||||
const prog = document.getElementById('banner-progress');
|
||||
if (realTotal > 0) {
|
||||
prog.style.display = 'flex';
|
||||
const pct = Math.min(100, Math.round(done * 100 / realTotal));
|
||||
document.getElementById('tbp-fill').style.width = pct + '%';
|
||||
const suffix = (!est && p.is_checking) ? '(扫描中…)' : '';
|
||||
document.getElementById('tbp-text').textContent = `${done} / ${realTotal}${suffix}`;
|
||||
} else {
|
||||
prog.style.display = 'none';
|
||||
}
|
||||
} else {
|
||||
banner.classList.remove('show');
|
||||
document.getElementById('banner-progress').style.display = 'none';
|
||||
}
|
||||
renderTaskQueue(p);
|
||||
}).catch(() => {});
|
||||
|
||||
fetch('/get_download_list?already_down=false').then(r => r.json()).then(data => {
|
||||
|
||||
+15
-2
@@ -19,6 +19,8 @@ import utils
|
||||
from module.app import Application, ChatDownloadConfig, TaskNode
|
||||
from module.download_stat import (
|
||||
DownloadState,
|
||||
clear_completed_chats,
|
||||
set_task_queue,
|
||||
get_download_result,
|
||||
get_download_state,
|
||||
get_total_download_speed,
|
||||
@@ -840,12 +842,16 @@ def api_save_and_restart():
|
||||
|
||||
# Add to channel history
|
||||
_add_to_channel_history(chat_id, chat_title, chat_type)
|
||||
|
||||
|
||||
# 重置队列总览元信息(单频道场景队列就一项)
|
||||
clear_completed_chats()
|
||||
set_task_queue([{"chat_id": chat_id, "chat_title": chat_title or chat_id}])
|
||||
|
||||
# Trigger restart
|
||||
if _app:
|
||||
_app.restart_program = True
|
||||
logger.info(f"Restart flag set, new chat_id: {chat_id} ({chat_title})")
|
||||
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"message": f"配置已保存,正在重启下载 {chat_title or chat_id}..."
|
||||
@@ -897,6 +903,13 @@ def api_save_and_restart_multi():
|
||||
for it in normalized:
|
||||
_add_to_channel_history(it["chat_id"], it["chat_title"], it["chat_type"])
|
||||
|
||||
# 重置并设置本次任务的完整队列,供前端渲染"任务队列"卡片
|
||||
clear_completed_chats()
|
||||
set_task_queue([
|
||||
{"chat_id": it["chat_id"], "chat_title": it["chat_title"] or it["chat_id"]}
|
||||
for it in normalized
|
||||
])
|
||||
|
||||
# 触发重启
|
||||
if _app:
|
||||
_app.restart_program = True
|
||||
|
||||
Reference in New Issue
Block a user