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; }