From 9b8ecd63f7e8fcf16926aa40ba197d313cff5f6f Mon Sep 17 00:00:00 2001 From: butubb <1422726308@qq.com> Date: Sun, 6 Sep 2026 09:41:02 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20AI=20=E6=8E=A7=E5=88=B6=E5=8F=B0?= =?UTF-8?q?=E5=88=87=E9=A1=B5/=E5=88=B7=E6=96=B0=E4=B8=8D=E4=B8=A2?= =?UTF-8?q?=E2=80=94=E2=80=94=E2=91=A0EventSource=20onerror=20=E4=B8=8D?= =?UTF-8?q?=E5=86=8D=E8=AF=AF=20endRun=EF=BC=88=E6=96=AD=E7=BD=91/?= =?UTF-8?q?=E5=90=8E=E5=8F=B0=E8=8A=82=E6=B5=81/=E7=9E=AC=E6=97=B6?= =?UTF-8?q?=E6=8A=96=E5=8A=A8=E9=83=BD=E4=BC=9A=E8=A7=A6=E5=8F=91=20onerro?= =?UTF-8?q?r=20=E8=80=8C=E5=90=8E=E7=AB=AF=E4=BB=BB=E5=8A=A1=E4=BB=8D?= =?UTF-8?q?=E5=9C=A8=E8=B7=91=EF=BC=9BEventSource=20=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E9=87=8D=E8=BF=9E=20+=20=E6=9C=8D=E5=8A=A1=E7=AB=AF=E9=98=9F?= =?UTF-8?q?=E5=88=97=E4=BF=9D=E7=95=99=E7=A7=AF=E5=8E=8B=E4=BA=8B=E4=BB=B6?= =?UTF-8?q?=EF=BC=8C=E9=87=8D=E8=BF=9E=E5=90=8E=E8=A1=A5=E5=8F=91=EF=BC=8C?= =?UTF-8?q?=E8=BF=9E=E6=8E=A5=E5=BD=BB=E5=BA=95=E5=85=B3=E9=97=AD=E6=89=8D?= =?UTF-8?q?=E6=8F=90=E7=A4=BA=E5=8F=AF=E5=88=B7=E6=96=B0=E6=81=A2=E5=A4=8D?= =?UTF-8?q?=EF=BC=89=EF=BC=9B=E2=91=A1=E9=A1=B5=E9=9D=A2=E5=88=B7=E6=96=B0?= =?UTF-8?q?=E6=81=A2=E5=A4=8D=EF=BC=9AGET=20/api/agent/run=20=E5=A2=9E?= =?UTF-8?q?=E5=8A=A0=20run=5Fid/answer/error/history=EF=BC=8C=E5=89=8D?= =?UTF-8?q?=E7=AB=AF=20restoreAgentView=20=E6=B8=B2=E6=9F=93=E5=8E=86?= =?UTF-8?q?=E5=8F=B2=E8=BD=AE=E6=AC=A1=E3=80=81running=20=E6=97=B6?= =?UTF-8?q?=E8=87=AA=E5=8A=A8=E9=87=8D=E8=AE=A2=E9=98=85=E4=BA=8B=E4=BB=B6?= =?UTF-8?q?=E6=B5=81=E3=80=81done/error=20=E5=B1=95=E7=A4=BA=E7=BB=93?= =?UTF-8?q?=E6=9E=9C=E2=80=94=E2=80=94=E5=88=87=E8=B5=B0=E5=86=8D=E5=9B=9E?= =?UTF-8?q?=E6=9D=A5=E4=BB=BB=E5=8A=A1=E4=B8=8E=E5=AF=B9=E8=AF=9D=E9=83=BD?= =?UTF-8?q?=E5=9C=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- static/admin/agent.js | 56 +++++++++++++++++++++++++++++++++++++++++-- web/agent_api.py | 14 ++++++++--- 2 files changed, 65 insertions(+), 5 deletions(-) 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")