From ce4e5464b94b65708c9b2b10900cea439e65850f Mon Sep 17 00:00:00 2001
From: butubb <1422726308@qq.com>
Date: Fri, 4 Sep 2026 13:18:36 +0800
Subject: [PATCH] =?UTF-8?q?feat:=20AI=20=E6=8E=A7=E5=88=B6=E5=8F=B0?=
=?UTF-8?q?=E4=BC=9A=E8=AF=9D=E5=8C=96=20+=20=E8=A7=86=E8=A7=89=E4=BC=98?=
=?UTF-8?q?=E5=8C=96=E2=80=94=E2=80=94=E2=91=A0=E5=A4=9A=E8=BD=AE=E5=AF=B9?=
=?UTF-8?q?=E8=AF=9D=E8=BF=9E=E7=BB=AD=E6=80=A7=EF=BC=88=E5=90=8E=E7=AB=AF?=
=?UTF-8?q?=20history=20=E6=8C=81=E4=B9=85=E5=8C=96=2012=20=E8=BD=AE?=
=?UTF-8?q?=EF=BC=8CAgent=20=E6=B3=A8=E5=85=A5=E5=8E=86=E5=8F=B2=E4=B8=8A?=
=?UTF-8?q?=E4=B8=8B=E6=96=87=EF=BC=8CDeepSeek=20=E5=BC=8F=E4=BC=9A?=
=?UTF-8?q?=E8=AF=9D=EF=BC=9A=E6=96=B0=E5=BB=BA=E4=BC=9A=E8=AF=9D=E6=89=8D?=
=?UTF-8?q?=E6=B8=85=E7=A9=BA=EF=BC=89=EF=BC=9B=E2=91=A1=E8=81=8A=E5=A4=A9?=
=?UTF-8?q?=E5=8C=BA=E6=94=B9=E6=B5=85=E8=89=B2=EF=BC=88DeepSeek=20?=
=?UTF-8?q?=E9=A3=8E=E6=A0=BC=EF=BC=8C=E5=9B=BE=E7=89=87=E6=96=87=E5=AD=97?=
=?UTF-8?q?=E6=B8=85=E6=99=B0=EF=BC=89=EF=BC=9B=E2=91=A2=E6=88=AA=E5=9B=BE?=
=?UTF-8?q?=E7=82=B9=E5=87=BB=E6=94=BE=E5=A4=A7=E6=9F=A5=E7=9C=8B?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
mcp_agent/agent.py | 11 ++++++---
static/admin/agent.js | 37 ++++++++++++++++++++++++++++
templates/admin/monitor.html | 47 +++++++++++++++++++++---------------
web/agent_api.py | 31 +++++++++++++++++++++---
4 files changed, 99 insertions(+), 27 deletions(-)
diff --git a/mcp_agent/agent.py b/mcp_agent/agent.py
index 3d7f28e..41f4781 100644
--- a/mcp_agent/agent.py
+++ b/mcp_agent/agent.py
@@ -132,9 +132,11 @@ class Agent:
# ---------- 主循环(流式) ----------
async def run_stream(self, prompt: str, serial: str = "",
- on_delta=None, on_tool=None):
+ history=None, on_delta=None, on_tool=None):
"""流式执行一轮指令,返回最终完整文本。
+ history:上一轮的 [{"role": "user"|"assistant", "content": 文本}] 列表,
+ 用于多轮对话保持上下文(截图/工具消息不入历史,控制 token)。
on_delta(text, kind):content/reasoning 文本增量(实时推给前端)
on_tool(step):工具调用完成(实时显示 MCP 步骤)
"""
@@ -144,8 +146,11 @@ class Agent:
sys_txt = SYSTEM_PROMPT
if target:
sys_txt += f"\n\n本次默认目标设备 serial:{target}(未指定设备时用它)。"
- self.messages = [{"role": "system", "content": sys_txt},
- {"role": "user", "content": prompt}]
+ self.messages = [{"role": "system", "content": sys_txt}]
+ for h in (history or []):
+ if h.get("role") in ("user", "assistant") and h.get("content"):
+ self.messages.append({"role": h["role"], "content": h["content"]})
+ self.messages.append({"role": "user", "content": prompt})
for _step in range(self.s.max_steps):
content_parts = []
diff --git a/static/admin/agent.js b/static/admin/agent.js
index 4f5eb38..2683e7b 100644
--- a/static/admin/agent.js
+++ b/static/admin/agent.js
@@ -96,6 +96,17 @@ function clearAgentChat(){
document.getElementById('agent-chat').innerHTML =
'
对话已清空。给 AI 下达新指令…
';
}
+// 新建会话:清后端历史 + 清前端对话区(像 DeepSeek 的新建会话)
+function newAgentSession(){
+ if(_agentBusy){showToast('运行中不能新建会话','error');return;}
+ if(!confirm('新建会话将清空当前对话历史,确定?'))return;
+ apiPost('/api/agent/clear',{}).then(r=>{
+ if(r&&r.ok){
+ clearAgentChat();
+ showToast('已新建会话','success');
+ }else showToast('操作失败','error');
+ });
+}
// ================== 发送与流式接收 ==================
function sendAgentMsg(){
@@ -188,6 +199,32 @@ function listenStream(runId){
};
}
+// 截图点击放大查看
+function zoomScreenshot(img){
+ const ov = document.getElementById('agent-zoom');
+ if(!ov){
+ const d = document.createElement('div');
+ d.id = 'agent-zoom';
+ d.style.cssText = 'display:none;position:fixed;inset:0;background:rgba(0,0,0,.8);z-index:1400;align-items:center;justify-content:center;cursor:zoom-out';
+ d.onclick = ()=>d.style.display='none';
+ document.body.appendChild(d);
+ }
+ const ovEl = document.getElementById('agent-zoom');
+ ovEl.innerHTML = '';
+ const big = document.createElement('img');
+ big.src = img.src;
+ big.style.cssText = 'max-width:88vw;max-height:88vh;border-radius:8px';
+ ovEl.appendChild(big);
+ ovEl.style.display = 'flex';
+}
+// 截图 img 绑定点击放大
+document.addEventListener('click', ev=>{
+ const t = ev.target;
+ if(t && t.tagName==='IMG' && t.closest('.agent-toolcard') && !t.closest('#agent-zoom')){
+ zoomScreenshot(t);
+ }
+});
+
function endRun(){
_agentBusy = false;
_agentStream && _agentStream.close();
diff --git a/templates/admin/monitor.html b/templates/admin/monitor.html
index ecedc7b..492d92c 100644
--- a/templates/admin/monitor.html
+++ b/templates/admin/monitor.html
@@ -304,26 +304,33 @@ body{background:var(--bg);font-family:var(--body);color:var(--text);font-size:14
.el-picker-item .ep-info .ep-attrs span{margin-right:8px}
.el-picker-item .ep-sel{font-size:10px;color:#fff;background:var(--primary);padding:1px 7px;border-radius:99px;flex-shrink:0;font-weight:600}
-/* ===== AI 控制台(聊天界面) ===== */
-.agent-shell{display:flex;flex-direction:column;height:calc(100vh - 150px);min-height:480px;border:1px solid var(--card-line);border-radius:14px;overflow:hidden;background:#0d1117}
-.agent-topbar{display:flex;align-items:center;justify-content:space-between;padding:10px 16px;border-bottom:1px solid var(--ink-line);background:#11151c}
-.agent-title{font-size:14px;font-weight:700}
-.agent-model-tag{margin-left:8px;font-size:11px;color:var(--teal);font-family:var(--mono);background:rgba(45,212,191,.08);padding:2px 8px;border-radius:10px}
-.agent-running-tag{color:#f87171;font-size:12px;margin-right:8px}
+/* ===== AI 控制台(聊天界面,浅色内容区——DeepSeek 风格,图片文字清晰) ===== */
+.agent-shell{display:flex;flex-direction:column;height:calc(100vh - 150px);min-height:480px;border:1px solid #e5e7eb;border-radius:14px;overflow:hidden;background:#fff}
+.agent-topbar{display:flex;align-items:center;justify-content:space-between;padding:10px 16px;border-bottom:1px solid #e5e7eb;background:#f9fafb}
+.agent-title{font-size:14px;font-weight:700;color:#111827}
+.agent-model-tag{margin-left:8px;font-size:11px;color:#0e7490;font-family:var(--mono);background:#e0f2fe;padding:2px 8px;border-radius:10px}
+.agent-running-tag{color:#dc2626;font-size:12px;margin-right:8px}
.agent-top-actions{display:flex;align-items:center;gap:6px}
-.agent-chat{flex:1;overflow-y:auto;padding:18px 22px;display:flex;flex-direction:column;gap:14px}
-.agent-msg{max-width:78%;padding:10px 14px;border-radius:12px;font-size:13.5px;line-height:1.65;white-space:pre-wrap;word-break:break-word}
-.agent-msg.user{align-self:flex-end;background:#155e63;color:#e6fffb;border-bottom-right-radius:3px}
-.agent-msg.assistant{align-self:flex-start;background:#161c26;border:1px solid var(--card-line);border-bottom-left-radius:3px;color:var(--text)}
-.agent-msg .reasoning{color:#8b95a7;font-size:12.5px;border-left:2px solid #2b3441;padding-left:8px;margin:6px 0}
-.agent-toolcards{display:flex;flex-direction:column;gap:4px;margin-bottom:8px}
-.agent-toolcard{display:flex;align-items:center;gap:8px;background:#0d1117;border:1px solid #232936;border-radius:8px;padding:5px 10px;font-size:12px;font-family:var(--mono);flex-wrap:wrap}
-.agent-toolcard .dot{width:7px;height:7px;border-radius:50%;background:var(--teal);flex:none}
-.agent-toolcard img{max-width:130px;border-radius:6px;margin-top:3px}
-.agent-inputbar{display:flex;gap:10px;padding:12px 14px;border-top:1px solid var(--ink-line);background:#11151c}
-.agent-inputbar textarea{flex:1;resize:none;background:#0d1117;border:1px solid var(--card-line);border-radius:10px;padding:9px 12px;color:var(--text);font-size:13.5px;outline:none;line-height:1.5}
-.agent-inputbar textarea:focus{border-color:var(--teal)}
-.agent-empty{color:#4b5563;font-size:13px;text-align:center;padding:40px 0}
+.agent-top-actions .btn{background:#fff;border:1px solid #d1d5db;color:#374151}
+.agent-top-actions .btn:hover{border-color:#0e7490;color:#0e7490}
+.agent-chat{flex:1;overflow-y:auto;padding:20px 26px;display:flex;flex-direction:column;gap:16px;background:#f7f8fa}
+.agent-msg{max-width:80%;padding:11px 15px;border-radius:13px;font-size:13.5px;line-height:1.7;white-space:pre-wrap;word-break:break-word}
+.agent-msg.user{align-self:flex-end;background:#0e7490;color:#fff;border-bottom-right-radius:3px}
+.agent-msg.assistant{align-self:flex-start;background:#fff;border:1px solid #e5e7eb;border-bottom-left-radius:3px;color:#1f2937;box-shadow:0 1px 2px rgba(0,0,0,.04)}
+.agent-msg.assistant .reasoning{color:#6b7280;font-size:12.5px;border-left:3px solid #d1d5db;padding-left:10px;margin:8px 0}
+.agent-toolcards{display:flex;flex-direction:column;gap:5px;margin-bottom:9px}
+.agent-toolcard{display:flex;align-items:flex-start;gap:8px;background:#f3f4f6;border:1px solid #e5e7eb;border-radius:9px;padding:6px 11px;font-size:12px;font-family:var(--mono);color:#374151;flex-wrap:wrap}
+.agent-toolcard .dot{width:7px;height:7px;border-radius:50%;background:#10b981;flex:none;margin-top:5px}
+.agent-toolcard code{color:#0e7490}
+.agent-toolcard .text-muted{color:#6b7280}
+.agent-toolcard img{max-width:190px;border-radius:7px;margin-top:4px;border:1px solid #e5e7eb;display:block;cursor:zoom-in}
+.agent-inputbar{display:flex;gap:10px;padding:12px 14px;border-top:1px solid #e5e7eb;background:#fff}
+.agent-inputbar textarea{flex:1;resize:none;background:#f9fafb;border:1px solid #d1d5db;border-radius:10px;padding:9px 12px;color:#111827;font-size:13.5px;outline:none;line-height:1.5}
+.agent-inputbar textarea:focus{border-color:#0e7490;box-shadow:0 0 0 2px rgba(14,116,144,.12)}
+.agent-inputbar .btn-primary{background:#0e7490;border:none}
+.agent-inputbar .btn-primary:hover{background:#155e75}
+.agent-inputbar .btn-primary:disabled{background:#9ca3af}
+.agent-empty{color:#6b7280;font-size:13px;text-align:center;padding:44px 0}
@@ -451,7 +458,7 @@ body{background:var(--bg);font-family:var(--body);color:var(--text);font-size:14
🤖 AI 控制台
● 运行中
-
+
diff --git a/web/agent_api.py b/web/agent_api.py
index c715f17..6d2db18 100644
--- a/web/agent_api.py
+++ b/web/agent_api.py
@@ -38,7 +38,8 @@ _CFG_KEYS = {"api_base": "agent_api_base",
# ---------- 运行状态(单实例 + 事件队列) ----------
_run = {"id": None, "state": "idle", "prompt": "", "serial": "",
- "answer": "", "error": ""}
+ "answer": "", "error": "",
+ "history": []} # 多轮对话历史 [{role: user|assistant, content}]
_queues = {} # run_id -> queue.Queue(SSE 消费者读取)
_lock = threading.Lock()
@@ -91,6 +92,7 @@ def agent_run():
prompt = (data.get("prompt") or "").strip()
if not prompt:
return jsonify({"ok": False, "error": "请输入指令"}), 400
+ serial = (data.get("serial") or "").strip()
cfg = _read_cfg()
if not cfg.get("api_key"):
return jsonify({"ok": False, "error": "请先在配置区填写 API Key"}), 400
@@ -103,9 +105,11 @@ def agent_run():
_run.update(id=run_id, state="running", prompt=prompt,
serial=(data.get("serial") or "").strip(),
answer="", error="")
+ # history 保留(同会话多轮对话),由前端「清空对话」调用 clear 重置
_queues[run_id] = queue.Queue()
_log.info(f"Agent 启动: {prompt[:60]}")
- threading.Thread(target=_agent_thread, args=(run_id, prompt, cfg),
+ threading.Thread(target=_agent_thread,
+ args=(run_id, prompt, serial, cfg),
daemon=True).start()
return jsonify({"ok": True, "run_id": run_id})
@@ -139,6 +143,15 @@ def agent_stream():
"X-Accel-Buffering": "no"})
+@bp.route("/api/agent/clear", methods=["POST"])
+@admin_required
+def agent_clear():
+ """清空对话历史。"""
+ with _lock:
+ _run["history"] = []
+ return jsonify({"ok": True, "msg": "已清空"})
+
+
def _shrink_image(b64, width=220, quality=50):
"""截图降采样(SSE step 事件用,控制传输体积)。失败原样返回。"""
try:
@@ -153,7 +166,7 @@ def _shrink_image(b64, width=220, quality=50):
return b64
-def _agent_thread(run_id, prompt, cfg):
+def _agent_thread(run_id, prompt, serial, cfg):
"""后台线程:Agent 流式执行,事件推入队列供 SSE 消费。"""
q = _queues.get(run_id)
try:
@@ -177,9 +190,14 @@ def _agent_thread(run_id, prompt, cfg):
agent.s.api_key = cfg.get("api_key") or agent.s.api_key
agent.s.default_serial = cfg.get("default_serial") or agent.s.default_serial
+ with _lock:
+ history = list(_run.get("history") or [])
+ target = serial or cfg.get("default_serial") or ""
+
async def _execute():
await agent._load_tools()
- return await agent.run_stream(prompt, cfg.get("default_serial") or "",
+ return await agent.run_stream(prompt, target,
+ history=history,
on_delta=on_delta, on_tool=on_tool)
# 整体超时保护:卡死时结束,释放单实例
@@ -187,6 +205,11 @@ def _agent_thread(run_id, prompt, cfg):
with _lock:
_run["state"] = "done"
_run["answer"] = answer
+ # 追加本轮进历史(多轮连续性;上限 12 轮防 token 膨胀)
+ hist = _run.setdefault("history", [])
+ hist.append({"role": "user", "content": prompt[:2000]})
+ hist.append({"role": "assistant", "content": (answer or "")[:4000]})
+ _run["history"] = hist[-24:]
q.put(("done", {"answer": answer}))
except Exception as e:
_log.warning(f"Agent 运行异常: {e}")