feat(web): 后台 running task 自动挂 SSE——运行态标识刷新后也实时(bump 0.38.4)
loadTaskList 收尾 subscribeRunningRows:列表带出的 running/cancelling 行本地 未订阅的自动挂事件流(上限 4 条防同源连接占满),done/error 走现有收尾清标识 + 重拉列表,零轮询。ensureRunningTaskSubscribed 的 cancelling/workingDir 改由 调用方传 seed(后台 task 媒体 rel 解析要用各自 working_dir);后台订阅不再调 renderLiveRunIfVisible(避免重挂卡强制滚底误伤当前对话)。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
0ad7d08242
commit
640bd0a1a3
|
|
@ -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)。
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
# zcbot 版本号单一事实源:web/app.py 的 FastAPI version、/healthz 返回、前端展示都引这里。
|
||||
# 改版本只动这一行。
|
||||
__version__ = "0.38.3"
|
||||
__version__ = "0.38.4"
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue