From fea5a57fc33a89acc2b70f24ed5e61463b536296 Mon Sep 17 00:00:00 2001
From: butubb <1422726308@qq.com>
Date: Fri, 4 Sep 2026 12:54:17 +0800
Subject: [PATCH] =?UTF-8?q?feat(M2):=20Web=20AI=20=E6=8E=A7=E5=88=B6?=
=?UTF-8?q?=E5=8F=B0=E2=80=94=E2=80=94=E5=B7=A5=E5=85=B7=E9=A1=B5=E6=96=B0?=
=?UTF-8?q?=E5=A2=9E=E5=AD=90=E5=88=86=E6=A0=8F=EF=BC=9A=E6=A8=A1=E5=9E=8B?=
=?UTF-8?q?/Key/=E8=AE=BE=E5=A4=87=E5=89=8D=E7=AB=AF=E5=8F=AF=E9=85=8D?=
=?UTF-8?q?=E7=BD=AE=EF=BC=88app=5Fmeta=20=E5=AD=98=E5=82=A8=EF=BC=8Ckey?=
=?UTF-8?q?=20=E6=89=93=E7=A0=81=E5=9B=9E=E6=98=BE=EF=BC=89=EF=BC=8C?=
=?UTF-8?q?=E6=8C=87=E4=BB=A4=E6=89=A7=E8=A1=8C=20+=20=E6=AD=A5=E9=AA=A4?=
=?UTF-8?q?=E6=B5=81=E8=BD=AE=E8=AF=A2=EF=BC=88=E5=90=AB=E6=88=AA=E5=9B=BE?=
=?UTF-8?q?=E7=BC=A9=E7=95=A5=EF=BC=89+=20=E5=9B=9E=E7=AD=94=E5=B1=95?=
=?UTF-8?q?=E7=A4=BA=EF=BC=9BAgent=20=E5=8A=A0=20on=5Fstep=20=E5=9B=9E?=
=?UTF-8?q?=E8=B0=83=E3=80=81=E5=90=8E=E5=8F=B0=E7=BA=BF=E7=A8=8B=E5=8D=95?=
=?UTF-8?q?=E5=AE=9E=E4=BE=8B=E8=BF=90=E8=A1=8C?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
mcp_agent/agent.py | 13 ++-
static/admin/base.js | 1 +
static/admin/tools.js | 83 ++++++++++++++++++
templates/admin/monitor.html | 32 +++++++
web/__init__.py | 3 +-
web/agent_api.py | 157 +++++++++++++++++++++++++++++++++++
6 files changed, 287 insertions(+), 2 deletions(-)
create mode 100644 web/agent_api.py
diff --git a/mcp_agent/agent.py b/mcp_agent/agent.py
index 826e4a6..09f4661 100644
--- a/mcp_agent/agent.py
+++ b/mcp_agent/agent.py
@@ -39,6 +39,7 @@ class Agent:
self.tools_schema = [] # OpenAI function schema
self._tool_exec = {} # name -> callable
self.messages = []
+ self.on_step = None # 可选回调 fn(step_dict),web 展示进度用
# ---------- MCP 工具桥 ----------
async def _load_tools(self):
@@ -93,12 +94,22 @@ class Agent:
except Exception as e:
return {"ok": False, "error": f"工具执行失败: {e}"}, None
# de_screenshot:图像分离(作为 image_url 追加给模型看)
+ image_b64 = None
if name == "de_screenshot" and isinstance(data, dict) and data.get("ok"):
img = (data.get("data") or {}).get("image") or {}
if img.get("data"):
text_result = {k: v for k, v in (data.get("data") or {}).items()
if k != "image"}
- return text_result, img["data"]
+ image_b64 = img["data"]
+ if self.on_step:
+ try:
+ self.on_step({"tool": name, "args": args,
+ "result": text_result if image_b64 else data,
+ "image_b64": image_b64})
+ except Exception:
+ pass
+ if image_b64:
+ return text_result, image_b64
return data, None
# ---------- 主循环 ----------
diff --git a/static/admin/base.js b/static/admin/base.js
index 27f6e3d..548f9a6 100644
--- a/static/admin/base.js
+++ b/static/admin/base.js
@@ -114,6 +114,7 @@ function showSubTab(tabId, name){
tab.querySelectorAll('.sub-tab').forEach(b=>b.classList.toggle('active', b.dataset.sub===name));
tab.querySelectorAll('.sub-panel').forEach(p=>p.classList.toggle('active', p.id===tabId+'-sub-'+name));
if(name==='groups' && typeof loadGroups==='function') loadGroups();
+ if(name==='agent' && typeof loadAgentConfig==='function') loadAgentConfig();
if(name==='devpool' && typeof loadDevPool==='function'){
loadDevPool();
if(typeof loadDiscovery==='function'){
diff --git a/static/admin/tools.js b/static/admin/tools.js
index d81aaf2..c106461 100644
--- a/static/admin/tools.js
+++ b/static/admin/tools.js
@@ -41,6 +41,89 @@ async function loadToolsDevices(force){
status.textContent = '共 '+_clipDevices.length+' 台设备';
}
+// ================== AI 控制台(模型配置 + 指令执行 + 进度轮询) ==================
+let _agentPoll = null;
+
+function loadAgentConfig(){
+ apiGet('/api/agent/config').then(r=>{
+ if(!r||!r.ok)return;
+ const b=document.getElementById('agent-api-base');
+ if(!b)return; // 面板未渲染
+ b.value=r.api_base||'https://api.deepseek.com';
+ document.getElementById('agent-model').value=r.model||'';
+ document.getElementById('agent-default-serial').value=r.default_serial||'';
+ const hint=document.getElementById('agent-key-hint');
+ hint.textContent=r.api_key_masked?('已配置 '+r.api_key_masked):'未配置 Key';
+ });
+}
+function saveAgentConfig(){
+ const body={api_base:document.getElementById('agent-api-base').value.trim(),
+ model:document.getElementById('agent-model').value.trim(),
+ default_serial:document.getElementById('agent-default-serial').value.trim()};
+ const key=document.getElementById('agent-api-key').value.trim();
+ if(key)body.api_key=key;
+ apiPost('/api/agent/config',body).then(r=>{
+ if(r&&r.ok){showToast('配置已保存','success');loadAgentConfig();}
+ else showToast('保存失败: '+((r&&r.error)||''),'error');
+ });
+}
+function runAgent(){
+ const prompt=document.getElementById('agent-prompt').value.trim();
+ if(!prompt){showToast('请输入指令','error');return;}
+ apiPost('/api/agent/run',{prompt}).then(r=>{
+ if(r&&r.ok){
+ showToast('Agent 已启动','success');
+ document.getElementById('agent-status').textContent='运行中...';
+ document.getElementById('btn-agent-run').disabled=true;
+ document.getElementById('agent-answer-wrap').style.display='none';
+ startAgentPoll();
+ }else showToast('启动失败: '+((r&&r.error)||''),'error');
+ });
+}
+function startAgentPoll(){
+ if(_agentPoll)clearInterval(_agentPoll);
+ _agentPoll=setInterval(pollAgent,2000);
+ pollAgent();
+}
+function stopAgentPoll(){
+ if(_agentPoll){clearInterval(_agentPoll);_agentPoll=null;}
+}
+function pollAgent(){
+ apiGet('/api/agent/status').then(r=>{
+ if(!r||!r.ok)return;
+ const st=document.getElementById('agent-status');
+ if(r.state==='running'){
+ st.textContent='运行中... ('+(r.steps||[]).length+' 步)';
+ }else{
+ st.textContent=r.state==='done'?'完成':'失败';
+ document.getElementById('btn-agent-run').disabled=false;
+ stopAgentPoll();
+ }
+ // 步骤流(含截图缩略)
+ const steps=document.getElementById('agent-steps');
+ const html=(r.steps||[]).map(s=>{
+ const args=esc(s.args||'');
+ const img=s.image
+ ?'':'';
+ return '
'+esc(s.tool||'')+' '
+ +''+args+''+img+'