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