diff --git a/static/admin/agent.js b/static/admin/agent.js index 82c5727..2a64793 100644 --- a/static/admin/agent.js +++ b/static/admin/agent.js @@ -19,6 +19,7 @@ function initAgentChat(){ loadLiveDevices(); loadAgentTargetDevices(); startRunPoll(); + restoreAgentView(); // 输入框快捷键 const inp = document.getElementById('agent-input'); inp.addEventListener('keydown', ev=>{ @@ -29,6 +30,43 @@ function initAgentChat(){ }); } +// ================== 页面刷新/重进恢复 ================== +// 刷新后聊天区是空的,但后端任务可能还在跑、历史还在——从这里恢复: +// 1) 渲染会话历史(user/assistant 文本轮) +// 2) 任务运行中 → 重新订阅事件流(服务端队列保留积压,重连后补发) +function restoreAgentView(){ + const chat = document.getElementById('agent-chat'); + if(!chat) return; + const hasContent = chat.children.length > 0 && !chat.querySelector('.agent-empty'); + if(hasContent) return; // 页面未刷新(切 Tab 回来 DOM 还在),无需恢复 + apiGet('/api/agent/run').then(r=>{ + if(!r||!r.ok) return; + const hist = r.history || []; + hist.forEach(h=>{ + const txt = (h.content || '').trim(); + if(!txt) return; + if(h.role === 'user'){ addUserMsg(txt); } + else{ + const div = newAssistantMsg(); + div.querySelector('.agent-text').textContent = txt; + } + }); + if(r.state === 'running' && r.run_id){ + const div = newAssistantMsg(); + div.querySelector('.agent-text').textContent = '↻ 已恢复连接,任务仍在后台运行(此前的内容可在上方查看)…'; + setRunning(true); + listenStream(r.run_id); + }else if(r.state === 'error' && r.error){ + const div = newAssistantMsg(); + div.querySelector('.agent-text').textContent = '⚠ ' + r.error; + }else if(!hist.length && r.state === 'idle'){ + clearEmpty(); + document.getElementById('agent-chat').innerHTML = + '
👋 给 AI 下达指令…(先选 🎯 目标设备)
'; + } + }); +} + // ================== 配置 ================== function loadAgentConfig(){ apiGet('/api/agent/config').then(r=>{ @@ -284,8 +322,22 @@ function listenStream(runId){ }); es.onerror = ()=>{ - // 事件流正常结束(done 后服务器关流)会触发一次 error——done 已处理则忽略 - if(_agentBusy) endRun(); + // 重要:不能在这里 endRun()——断网/后台标签页节流/服务端瞬时抖动都会触发 + // onerror,但后端任务仍在跑。EventSource 会自动重连,服务端事件队列保留 + // 积压(delta/step/done),重连成功后全部补发,界面无缝续上。 + // 只有连接彻底关闭(非自动重连态)且任务还在跑时才提示。 + if(_agentBusy && es.readyState === EventSource.CLOSED){ + const hint = document.getElementById('agent-target-hint'); + if(hint) hint.textContent = '⚠ 实时连接已中断,任务仍在后台运行——可刷新页面恢复查看'; + // 连接彻底失败时周期探测,恢复后重订阅 + setTimeout(()=>{ + if(!_agentStream && _agentBusy){ + apiGet('/api/agent/run').then(r=>{ + if(r&&r.ok&&r.state==='running'&&r.run_id) listenStream(r.run_id); + }); + } + }, 5000); + } }; } diff --git a/web/agent_api.py b/web/agent_api.py index b6f7ffe..7e80371 100644 --- a/web/agent_api.py +++ b/web/agent_api.py @@ -541,16 +541,24 @@ def agent_run(): @bp.route("/api/agent/run", methods=["GET"]) @admin_required def agent_run_status(): - """当前 Agent 运行状态(多窗口/多人可见):idle / running + 任务摘要。 + """当前 Agent 运行状态(多窗口/页面刷新恢复用): + idle/running/done + run_id + 最终答案 + 会话历史。 - 前端轮询它同步「运行中」状态(换浏览器/他人启动的任务也能看到并停止)。 + 前端刷新后据此:渲染历史轮次、running 时重新订阅事件流(服务端队列 + 保留积压事件,重连后补发)、done 时展示最终答案。 """ with _lock: + hist = [{"role": h.get("role"), "content": (h.get("content") or "")[:4000]} + for h in (_run.get("history") or [])] return jsonify({"ok": True, "state": _run.get("state", "idle"), + "run_id": _run.get("id") or "", "prompt": _run.get("prompt", ""), "serial": _run.get("serial", ""), - "started": _run.get("started", "")}) + "started": _run.get("started", ""), + "answer": (_run.get("answer") or "")[:4000], + "error": (_run.get("error") or "")[:400], + "history": hist[-16:]}) @bp.route("/api/agent/devices")