From 5dc15e128e03c08ca7026c9d78a213d3dd15e4a1 Mon Sep 17 00:00:00 2001 From: butubb <1422726308@qq.com> Date: Mon, 14 Sep 2026 08:25:35 +0800 Subject: [PATCH] =?UTF-8?q?fix(AI=20=E5=BB=BA=E4=BB=BB=E5=8A=A1):=20?= =?UTF-8?q?=E5=88=87=E8=B5=B0=E5=86=8D=E5=88=87=E5=9B=9E=E6=9D=A5=E4=B8=8D?= =?UTF-8?q?=E5=86=8D=E9=87=8D=E7=BD=AE/=E9=87=8D=E5=A4=8D=E6=8E=A2?= =?UTF-8?q?=E7=B4=A2=E5=9B=9E=E6=94=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 现象(用户报):离开「AI 建任务」子页再回来,回放与步骤计数像是被重置了。 真因与修法(`static/admin/taskgen.js`): 1. 每次进入子页 `initTaskGen → tgRestore` 都会重新订阅 SSE。已在盯同一轮时这是多余的: 服务端 `_Fanout` 带历史缓冲,会把这一轮**从头补发**一遍 → 回放区里卡片变两份、 步骤计数从 0 重数。现在用 `_tgRunId` 记当前订阅,同一轮直接复用连接; 换了一轮/刚刷新过页面才清空回放区重新订阅(历史缓冲会把已发生的事件补回来)。 2. 跟随画面的 MJPEG 长连接在切 Tab 时可能被浏览器挂起(画面定格)—— 切回来时重新拉一次流(`tgRearmLive`)。 实测(真机,12 步探索):切走 6 秒再切回 → 卡片 11→14 无重复、状态继续计数; 跑完后再切两次 → 14 张卡片与草稿预览均保持、跟随画面正常、无 JS 报错。 --- doc/AI_TASK_GEN.md | 6 +++++- static/admin/taskgen.js | 27 ++++++++++++++++++++++++++- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/doc/AI_TASK_GEN.md b/doc/AI_TASK_GEN.md index afede4d..f65b868 100644 --- a/doc/AI_TASK_GEN.md +++ b/doc/AI_TASK_GEN.md @@ -214,7 +214,11 @@ `static/admin/base.js` 的 `showSubTab`)。草稿预览能直接点「在步骤编辑器中打开」→ `openTaskModal(null, prefill)` 预填(用唯一 draftKey,避开 localStorage 旧草稿覆盖)。 - **两个页面共用一个运行槽**:`GET /api/agent/run` 回 `mode`,两个前端各按 mode 决定 - 是否订阅 SSE(一个 run 只有一个事件队列,两个 EventSource 同时消费会互相瓜分事件)。 + 是否订阅 SSE;服务端用 `_Fanout` 给**每个订阅者一条自己的队列**(谁都不丢事件)。 +- **切 Tab 不丢回放**:`initTaskGen()` 每次进入子页都会 `tgRestore()`——已经在盯同一轮就 + **不重复订阅**(否则服务端会把这一轮从头补发一遍,卡片成两份);换了一轮/刚刷新过页面才清空 + 回放区重新订阅(`_Fanout` 的历史缓冲会把已发生的事件补回来)。跟随画面的 MJPEG 长连接 + 在切回来时重新拉一次(`tgRearmLive`),避免定格。 - **落库出口**:页面上的「✓ 直接创建任务」(`POST /api/agent/task_draft/create`)与 「在步骤编辑器中核对」(预填编辑器 → 用户点保存)——两条路最终都汇到 `add_job`。 diff --git a/static/admin/taskgen.js b/static/admin/taskgen.js index e698fcf..cafca5b 100644 --- a/static/admin/taskgen.js +++ b/static/admin/taskgen.js @@ -10,6 +10,7 @@ let _tgInit = false; let _tgStream = null; // EventSource let _tgBusy = false; +let _tgRunId = ''; // 当前已订阅的 run(切 Tab 回来时避免重复订阅) let _tgDraft = null; // {summary, task, notes, evidence} let _tgWarnings = []; let _tgPrompt = ''; // 本次需求(回存草稿时带上) @@ -29,6 +30,17 @@ function initTaskGen(){ tgLoadDevices(); tgLoadGroups(); tgRestore(); + tgRearmLive(); +} + +// 切走再切回来时,观众侧的 MJPEG 长连接可能被浏览器挂起(画面定格)—— +// 重新拉一次流,画面接着动 +function tgRearmLive(){ + const card = document.getElementById('tg-live-card'); + const img = document.getElementById('tg-live-img'); + if(!card || !img || !_tgLiveSerial || card.style.display === 'none') return; + img.src = '/api/screen/stream?serial=' + encodeURIComponent(_tgLiveSerial) + + '&q=80&fps=8&t=' + Date.now(); } function tgExample(){ @@ -178,6 +190,7 @@ function tgStatus(t){ // ---------------- 事件流 ---------------- function listenTaskGenStream(runId){ if(_tgStream) _tgStream.close(); + _tgRunId = runId; const es = new EventSource('/api/agent/stream?run_id=' + runId); _tgStream = es; let steps = 0; @@ -461,7 +474,19 @@ function tgRestore(){ if(!r||!r.ok) return; // 运行中的 designer 由本页接管订阅;聊天页见 agent.js 的 mode 判断 if(r.running && r.mode === 'designer' && r.run_id){ - tgSetBusy(true, '探索进行中…(刷新后已重新连接)'); + tgSetBusy(true, '探索进行中…'); + // 已经在盯着同一轮了(切走再切回来)→ 别重复订阅: + // 重订阅会让服务端把这一轮从头发一遍(历史缓冲),卡片就成两份了 + if(_tgRunId === r.run_id && _tgStream && _tgStream.readyState !== 2){ + tgStatus('探索进行中…'); + return; + } + // 换了一轮 / 刚刷新过页面:回放区清空重建(服务端会把已发生的事件补发回来) + const box = document.getElementById('tg-stream'); + if(box) box.innerHTML = ''; + const db = document.getElementById('tg-draft'); + if(db) db.innerHTML = ''; + tgStatus('探索进行中…(已连接,回放会从头补齐)'); listenTaskGenStream(r.run_id); return; }