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")