From 640bd0a1a3a66d90cdf4e7c9aceacca7f3667ec6 Mon Sep 17 00:00:00 2001 From: caoqianming Date: Fri, 3 Jul 2026 16:41:26 +0800 Subject: [PATCH] =?UTF-8?q?feat(web):=20=E5=90=8E=E5=8F=B0=20running=20tas?= =?UTF-8?q?k=20=E8=87=AA=E5=8A=A8=E6=8C=82=20SSE=E2=80=94=E2=80=94?= =?UTF-8?q?=E8=BF=90=E8=A1=8C=E6=80=81=E6=A0=87=E8=AF=86=E5=88=B7=E6=96=B0?= =?UTF-8?q?=E5=90=8E=E4=B9=9F=E5=AE=9E=E6=97=B6(bump=200.38.4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit loadTaskList 收尾 subscribeRunningRows:列表带出的 running/cancelling 行本地 未订阅的自动挂事件流(上限 4 条防同源连接占满),done/error 走现有收尾清标识 + 重拉列表,零轮询。ensureRunningTaskSubscribed 的 cancelling/workingDir 改由 调用方传 seed(后台 task 媒体 rel 解析要用各自 working_dir);后台订阅不再调 renderLiveRunIfVisible(避免重挂卡强制滚底误伤当前对话)。 Co-Authored-By: Claude Fable 5 --- PROGRESS.md | 3 +++ core/__init__.py | 2 +- web/static/js/chat.js | 31 ++++++++++++++++++++++++++----- 3 files changed, 30 insertions(+), 6 deletions(-) diff --git a/PROGRESS.md b/PROGRESS.md index 016609b..d7f9a18 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -21,6 +21,9 @@ ## 已完成关键能力 +### 2026-07-03 / web 后台 running task 自动挂 SSE——运行态标识刷新页面后也实时(bump 0.38.4) +0.38.3 留的边界:刷新页面(liveRuns 清空)或 run 由别的标签页/渠道启动时,列表标识只是服务端快照,run 跑完没人通知前端,会一直挂「运行中」。用户点出方向:别轮询,直接复用 SSE。改法:`loadTaskList` 收尾新增 `subscribeRunningRows`——列表带出的 running/cancelling 行,本地未订阅的自动 `ensureRunningTaskSubscribed` 挂上事件流(上限 4 条后台流,防 HTTP/1.1 同源连接数被占满;超限行标识仍显示只是不自动清),done/error 走 fetchSse 现有收尾(清 liveRuns + 就地清标识 + 重拉列表),全程实时零轮询。配套两处:`ensureRunningTaskSubscribed` 的 cancelling/workingDir 从"读全局 state.taskMeta"改为调用方传 seed(taskMeta 或列表行)——后台 task 的媒体产物 rel 解析必须用各自 working_dir;`renderLiveRunIfVisible` 只在订阅的是选中 task 时才调(后台订阅不碰对话区,否则重挂卡 + 强制滚底误伤正看着的对话)。附带收益:刷新后切进 running task,直播卡带着后台累计的文字直接可见(renderMessages 收尾 renderLiveRunIfVisible 挂卡)。只改 `web/static/js/chat.js`。 + ### 2026-07-03 / web 任务列表加运行态标识(bump 0.38.3) 用户报:多个 task 并发执行(调用工具/回复中)时,左栏任务列表看不出哪些在跑。后端 `/v1/tasks` 每行其实早已带 `run_status`(`_task_dict` 统一出),只是前端 `renderTaskList` 没用——`chat.js` 里"列表行摘要无此字段"的注释已过时。修:列表行状态徽章旁新增运行态标识,`running` 绿脉冲点「运行中」、`cancelling` 橙「停止中」、`error` 红点「出错」(hover 出 run_error),`idle` 不显示;取值 = 服务端 run_status 快照 + 本地 `state.liveRuns` 叠加(本会话刚发出的 run 比列表快照新,cancelling 本地标志优先)。实时性三时机:run 开始(sendMessage / ensureRunningTaskSubscribed)与点停止时 `syncTaskRowRunIndicator` 就地 patch 对应行 DOM(不重拉列表,保住滚动加载的分页);run 结束沿用 fetchSse 收尾已有的 `loadTaskList()` 重拉。别处启动的 run(其他标签页/渠道)靠列表任意一次重拉带出,首版不加轮询。顺手把 ⋯ 菜单「清空对话」的 running 判断改走同一 `taskRunState`(列表行此前恒 false)。改 `web/static/js/chat.js` + `web/static/dev.html`(CSS)。 diff --git a/core/__init__.py b/core/__init__.py index 2be8621..42ab90b 100644 --- a/core/__init__.py +++ b/core/__init__.py @@ -1,3 +1,3 @@ # zcbot 版本号单一事实源:web/app.py 的 FastAPI version、/healthz 返回、前端展示都引这里。 # 改版本只动这一行。 -__version__ = "0.38.3" +__version__ = "0.38.4" diff --git a/web/static/js/chat.js b/web/static/js/chat.js index f40b304..21df588 100644 --- a/web/static/js/chat.js +++ b/web/static/js/chat.js @@ -95,6 +95,7 @@ export async function loadTaskList({ append = false } = {}) { state.taskHasMore = state.taskLoaded < state.taskTotal; renderTaskList(results, append); renderTaskCount(); + subscribeRunningRows(results); } catch (e) { if (mySeq !== _taskLoadSeq) return; if (e.status === 401) { logout(); return; } @@ -108,6 +109,21 @@ export async function loadTaskList({ append = false } = {}) { } } +// 列表带出的 running/cancelling 行,本地未订阅的自动挂 SSE 接管(刷新后重新盯上、 +// 别的标签页/渠道启动的 run 也能盯):跑完 done/error 走 fetchSse 现有收尾 +// (清 liveRuns + 就地清标识 + loadTaskList 重拉),标识全程实时,不需要轮询。 +// 上限防浏览器同源连接数被后台流占满(HTTP/1.1 并发约 6,要给选中 task 的 +// 订阅和普通 API 留口子);超限的行标识仍显示(服务端快照),只是不自动清。 +const MAX_BG_SSE = 4; +function subscribeRunningRows(tasks) { + for (const t of tasks) { + if (t.run_status !== "running" && t.run_status !== "cancelling") continue; + if (getLiveRun(t.task_id)) continue; + if (state.liveRuns.size >= MAX_BG_SSE) break; + ensureRunningTaskSubscribed(t.task_id, `/v1/tasks/${t.task_id}/events`, t); + } +} + function renderTaskCount() { $("task-count").textContent = state.taskTotal > 0 ? `共 ${state.taskTotal} 个` : ""; if (state.taskTotal === 0) setSentinel(""); @@ -394,7 +410,7 @@ export async function selectTask(tid) { renderChatMeta(); applyChannelComposerLock(meta); if (meta.run_status === "running" || meta.run_status === "cancelling") { - ensureRunningTaskSubscribed(tid, `/v1/tasks/${tid}/events`); + ensureRunningTaskSubscribed(tid, `/v1/tasks/${tid}/events`, meta); } else { renderLiveRunIfVisible(); } @@ -871,7 +887,10 @@ function renderLiveRunIfVisible() { $("chat-hint").textContent = run.cancelling ? "停止中…" : "接收中…"; } -function ensureRunningTaskSubscribed(taskId, url) { +// seed = 该 task 的 API dict(taskMeta 或列表行),取 run_status/working_dir。 +// 之前从全局 state.taskMeta 读 —— 只对"订阅选中 task"成立;现在列表也会给后台 +// running task 挂订阅,workingDir 必须跟着各自 task 走(媒体产物 rel 解析用它)。 +function ensureRunningTaskSubscribed(taskId, url, seed = {}) { if (!taskId || getLiveRun(taskId)) return; const run = { taskId, @@ -881,14 +900,16 @@ function ensureRunningTaskSubscribed(taskId, url) { terminal: false, card: null, curSeg: null, - cancelling: state.taskMeta && state.taskMeta.run_status === "cancelling", - workingDir: state.taskMeta && state.taskMeta.working_dir, + cancelling: seed.run_status === "cancelling", + workingDir: seed.working_dir || "", progressSteps: cloneProgressSteps(state.taskProgressByTask.get(taskId)), }; state.liveRuns.set(taskId, run); state.streaming = true; syncTaskRowRunIndicator(taskId); - renderLiveRunIfVisible(); + // 只有订阅的是当前选中 task 才挂直播卡(selectTask 路径);后台行订阅不碰 + // 对话区(renderLiveRunIfVisible 会重挂卡 + 强制滚底,误伤正看着的对话) + if (taskId === state.taskId) renderLiveRunIfVisible(); streamSse(url, run); }