feat(web): 任务列表加运行态标识——多 task 并发时可见哪些在跑(bump 0.38.3)
列表行 run_status(后端本就返回,前端一直没用)渲成状态徽章旁的标识: running 绿脉冲点/cancelling 橙/error 红点(hover 出 run_error)。取值叠加本地 liveRuns;run 开始与点停止时就地 patch 行 DOM(不重拉列表保分页),run 结束 沿用收尾 loadTaskList() 重拉。⋯ 菜单"清空对话"的 running 判断同源修正。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
941554f9d7
commit
0ad7d08242
|
|
@ -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 仅配色预设),用户点头再按明确路径套入;这是唯一鼓励主动提模板的场景,其余仍等明确路径,不模糊匹配。
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
# zcbot 版本号单一事实源:web/app.py 的 FastAPI version、/healthz 返回、前端展示都引这里。
|
||||
# 改版本只动这一行。
|
||||
__version__ = "0.38.2"
|
||||
__version__ = "0.38.3"
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
|
|
|||
|
|
@ -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 `<span class="run-ind ${rs}" title="${escapeHtml(title)}"><span class="run-dot"></span>${cfg.label}</span>`;
|
||||
}
|
||||
|
||||
// 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 ? `<div class="meta muted" title="${escapeHtml(desc)}" style="display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">${escapeHtml(desc)}</div>` : ""}
|
||||
<div class="meta">
|
||||
<span class="badge ${t.status}">${statusLabel}</span>
|
||||
${runIndicatorHtml(t)}
|
||||
${t.skill ? `<span class="muted" title="${escapeHtml(t.skill)}">${escapeHtml(t.skill)}</span>` : ""}
|
||||
<span class="num right-group">${t.n_messages || 0} 条</span>
|
||||
<span class="num" title="${escapeHtml(taskUsageTooltip(t))}">${fmtTokens(t.tokens)} tok</span>
|
||||
|
|
@ -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");
|
||||
|
|
|
|||
Loading…
Reference in New Issue