From 0ad7d082423f7933812df13c7656759f2824e2da Mon Sep 17 00:00:00 2001 From: caoqianming Date: Fri, 3 Jul 2026 16:25:59 +0800 Subject: [PATCH] =?UTF-8?q?feat(web):=20=E4=BB=BB=E5=8A=A1=E5=88=97?= =?UTF-8?q?=E8=A1=A8=E5=8A=A0=E8=BF=90=E8=A1=8C=E6=80=81=E6=A0=87=E8=AF=86?= =?UTF-8?q?=E2=80=94=E2=80=94=E5=A4=9A=20task=20=E5=B9=B6=E5=8F=91?= =?UTF-8?q?=E6=97=B6=E5=8F=AF=E8=A7=81=E5=93=AA=E4=BA=9B=E5=9C=A8=E8=B7=91?= =?UTF-8?q?(bump=200.38.3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 列表行 run_status(后端本就返回,前端一直没用)渲成状态徽章旁的标识: running 绿脉冲点/cancelling 橙/error 红点(hover 出 run_error)。取值叠加本地 liveRuns;run 开始与点停止时就地 patch 行 DOM(不重拉列表保分页),run 结束 沿用收尾 loadTaskList() 重拉。⋯ 菜单"清空对话"的 running 判断同源修正。 Co-Authored-By: Claude Fable 5 --- PROGRESS.md | 3 +++ core/__init__.py | 2 +- web/static/dev.html | 9 ++++++++ web/static/js/chat.js | 48 +++++++++++++++++++++++++++++++++++++++++-- 4 files changed, 59 insertions(+), 3 deletions(-) diff --git a/PROGRESS.md b/PROGRESS.md index 4bcd7d1..016609b 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -21,6 +21,9 @@ ## 已完成关键能力 +### 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)。 + ### 2026-07-03 / ppt 模板 zongyuan_red 逆向重建为真实 中国建材总院 身份(bump 0.38.2) 用户给官方 `总院模板.pptx`(中国建筑材料科学研究总院有限公司)要求"统一按这个来,zongyuan_red"。原 `layouts/zongyuan_red/` 是手搓的红条结构版(深蓝 #1F2A44 + 顶部红条 + 55/45 封面 + PART 章节),与真实文件 DNA 完全不符。PowerPoint COM 渲出 3 档真页(封面/内容/尾页)+ 解 pptx 抽实测:主红 `#D7000E`、目录红 `#D52C24`、近黑 `#181717`、辅灰 `#6F6F6F`/`#BCBDBD`;字体 微软雅黑 + Arial + 方正兰亭黑;八边形品牌 logo(EMF→PNG 透明底)+ 总部大楼灰度实景 + 材料马赛克实景(TIFF→压缩 JPG)。重写 5 页 SVG 忠实还原:封面(实景铺底+顶左 logo&机构全称+居中主红块+白标题)/目录(左上实景+右下大红斜三角+目录标题+白字方块序号,承集团规范斜向分割)/章节(八边形品牌水印+红 PART 胶囊+大标题,原件缺、按八边形 DNA 合成)/内容(左缘红方块+标题+灰分隔线+右上 logo+4 列灰底红顶条卡片+底部红条+页码)/尾页(材料马赛克+"材料创造美好世界"红+Thanks)。打包 logo.png/cover_bg.jpg/ending_bg.jpg 三资产,改写 design_spec.md 反映真实身份,补登记进 layouts_index.json(此前 dir 在但未注册)。质检 --template-mode 5 页零 error;finalize 内嵌 8 图 + svg_preview 全量渲图逐页过目确认与原件一致。**并加主动提示**:strategist.md §e + SKILL.md 默认主题段各补一条 —— 受众/素材/用户机构指向 中国建材总院·CNBM 系(汇报/立项/评审/职称评审/品牌宣讲)时,策略阶段**主动**把 `zongyuan_red` 整套模板作为候选点名给用户(区别于 business-red 仅配色预设),用户点头再按明确路径套入;这是唯一鼓励主动提模板的场景,其余仍等明确路径,不模糊匹配。 diff --git a/core/__init__.py b/core/__init__.py index 4b877ab..2be8621 100644 --- a/core/__init__.py +++ b/core/__init__.py @@ -1,3 +1,3 @@ # zcbot 版本号单一事实源:web/app.py 的 FastAPI version、/healthz 返回、前端展示都引这里。 # 改版本只动这一行。 -__version__ = "0.38.2" +__version__ = "0.38.3" diff --git a/web/static/dev.html b/web/static/dev.html index 306713d..f324221 100644 --- a/web/static/dev.html +++ b/web/static/dev.html @@ -560,6 +560,15 @@ display: inline-block; padding: 0 6px; border-radius: var(--r-md); font-size: 11px; background: #eef; color: #336; } + /* 运行态标识(run_status):running 绿脉冲点、cancelling 橙、error 红(hover 出 run_error)。 + 数据源 = /v1/tasks 行 run_status + 本地 liveRuns 叠加;run 开始/停止就地 patch,结束随列表重拉清掉 */ + .task-row .meta .run-ind { flex-shrink: 0; display: inline-flex; align-items: center; gap: 4px; } + .run-ind.running { color: var(--c-green); } + .run-ind.cancelling { color: var(--c-orange); } + .run-ind.error { color: var(--c-red); } + .run-ind .run-dot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; } + .run-ind.running .run-dot, .run-ind.cancelling .run-dot { animation: run-pulse 1.2s ease-in-out infinite; } + @keyframes run-pulse { 0%, 100% { opacity: 1; } 50% { opacity: .25; } } .badge.completed { background: var(--c-green-bg); color: var(--c-green); } .badge.abandoned { background: var(--accent-soft); color: var(--accent); } .badge.active { background: #eef; color: #336; } diff --git a/web/static/js/chat.js b/web/static/js/chat.js index 74329aa..f40b304 100644 --- a/web/static/js/chat.js +++ b/web/static/js/chat.js @@ -119,6 +119,44 @@ function setSentinel(text) { $("task-sentinel").textContent = text || ""; } +// 列表行运行态:服务端 run_status 快照 + 本地 liveRuns 叠加(本会话刚发出的 run 比列表快照新) +function taskRunState(t) { + const live = state.liveRuns.get(t.task_id); + if (live) return live.cancelling ? "cancelling" : "running"; + return t.run_status || "idle"; +} + +const RUN_IND = { + running: { label: "运行中", title: "正在执行(调用工具 / 回复中)" }, + cancelling: { label: "停止中", title: "正在停止" }, + error: { label: "出错", title: "" }, // title 动态填 run_error +}; + +function runIndicatorHtml(t) { + const rs = taskRunState(t); + const cfg = RUN_IND[rs]; + if (!cfg) return ""; // idle → 不显示 + const title = rs === "error" ? (t.run_error || "上次执行出错") : cfg.title; + return `${cfg.label}`; +} + +// run 开始 / 停止请求后就地刷新对应列表行的运行态标识(不重拉列表 — loadTaskList reset +// 会把滚动加载的分页收回第一页);run 结束由 fetchSse 收尾的 loadTaskList() 全量重拉兜底。 +// 行不在当前列表窗口(被筛掉 / 未滚动加载到)则跳过,下次重拉自然带出。 +function syncTaskRowRunIndicator(tid) { + const row = document.querySelector(`.task-row[data-tid="${CSS.escape(tid)}"]`); + if (!row) return; + const metaLine = row.querySelector(".meta:not(.muted)"); + if (!metaLine) return; + const old = metaLine.querySelector(".run-ind"); + if (old) old.remove(); + const html = runIndicatorHtml((state.tasksById || {})[tid] || { task_id: tid }); + if (!html) return; + const badge = metaLine.querySelector(".badge"); + if (badge) badge.insertAdjacentHTML("afterend", html); + else metaLine.insertAdjacentHTML("afterbegin", html); +} + function renderTaskList(tasks, append = false) { if (!append) state.tasksById = {}; for (const t of tasks) state.tasksById[t.task_id] = t; @@ -145,6 +183,7 @@ function renderTaskList(tasks, append = false) { ${desc ? `
${escapeHtml(desc)}
` : ""}
${statusLabel} + ${runIndicatorHtml(t)} ${t.skill ? `${escapeHtml(t.skill)}` : ""} ${t.n_messages || 0} 条 ${fmtTokens(t.tokens)} tok @@ -274,8 +313,8 @@ function syncChannelCardActive(tid) { function taskMenuItems(t) { const isActive = t.status === "active"; const hasMsg = (t.n_messages || 0) > 0; - // run_status 仅 taskMeta(中栏 ⋯)带;列表行摘要无此字段 → undefined → running=false(与改前一致) - const running = t.run_status === "running" || t.run_status === "cancelling"; + // run_status 列表行与 taskMeta 都带(_task_dict 统一出);running/cancelling 时禁清空 + const running = taskRunState(t) === "running" || taskRunState(t) === "cancelling"; return [ { act: "complete", label: "完成", cls: "act-complete", disabled: !isActive, onclick: () => setTaskStatus(t.task_id, "completed", t.name || "(未命名)") }, @@ -848,6 +887,7 @@ function ensureRunningTaskSubscribed(taskId, url) { }; state.liveRuns.set(taskId, run); state.streaming = true; + syncTaskRowRunIndicator(taskId); renderLiveRunIfVisible(); streamSse(url, run); } @@ -1397,6 +1437,7 @@ async function sendMessage(overrideText) { run.curSeg = { el: asstCard.querySelector(".body"), acc: "", pending: false }; state.liveRuns.set(taskId, run); state.streaming = true; + syncTaskRowRunIndicator(taskId); setActionMode("streaming"); streamSse(r.events_url, run); } catch (e) { @@ -1416,6 +1457,7 @@ async function cancelCurrentTask() { if (!state.taskId || !run) return; run.cancelling = true; setActionMode("cancelling"); + syncTaskRowRunIndicator(state.taskId); $("chat-hint").textContent = "停止中…"; try { await api("POST", `/v1/tasks/${state.taskId}/cancel`); @@ -1426,6 +1468,7 @@ async function cancelCurrentTask() { if (e.status !== 409) appendErrorCard("cancel: " + e.message); run.cancelling = false; setActionMode("streaming"); + syncTaskRowRunIndicator(run.taskId); $("chat-hint").textContent = "接收中…"; } } @@ -1496,6 +1539,7 @@ async function fetchSse(url, run) { if (ctx.card) ctx.card.querySelectorAll(".body.streaming").forEach((b) => b.classList.remove("streaming")); state.liveRuns.delete(ctx.taskId); state.streaming = state.liveRuns.size > 0; + syncTaskRowRunIndicator(ctx.taskId); // 先按本地态即时清标识,loadTaskList 随后带回服务端真相(如 error) if (state.taskId === ctx.taskId) { hint.textContent = ctx.lastUsageHint || "就绪"; setActionMode("idle");