diff --git a/mcp_agent/agent.py b/mcp_agent/agent.py
index 15bca89..ec21534 100644
--- a/mcp_agent/agent.py
+++ b/mcp_agent/agent.py
@@ -134,13 +134,15 @@ class Agent:
# ---------- 主循环(流式) ----------
async def run_stream(self, prompt: str, serial: str = "",
- history=None, on_delta=None, on_tool=None):
+ history=None, on_delta=None, on_tool=None,
+ should_stop=None):
"""流式执行一轮指令,返回最终完整文本。
history:上一轮的 [{"role": "user"|"assistant", "content": 文本}] 列表,
用于多轮对话保持上下文(截图/工具消息不入历史,控制 token)。
on_delta(text, kind):content/reasoning 文本增量(实时推给前端)
on_tool(step):工具调用完成(实时显示 MCP 步骤)
+ should_stop:可调用 fn() -> bool,每轮模型调用前检查(用户中断用)
"""
self.on_delta = on_delta
self.on_tool = on_tool
@@ -155,6 +157,9 @@ class Agent:
self.messages.append({"role": "user", "content": prompt})
for _step in range(self.s.max_steps):
+ if should_stop and should_stop():
+ _log.info("Agent 被用户中断")
+ return "(已按用户要求停止操作)"
content_parts = []
tool_acc = {} # index -> {id, name, args}
has_tool = False
diff --git a/mcp_server/mcp_server.py b/mcp_server/mcp_server.py
index 630fd24..74e0dcc 100644
--- a/mcp_server/mcp_server.py
+++ b/mcp_server/mcp_server.py
@@ -228,6 +228,23 @@ def de_tap_element(serial: str, by: str, value: str, index: int = 1) -> dict:
"by": by, "value": value, "index": index})
+@mcp.tool()
+def de_read_clipboard(serial: str) -> dict:
+ """读取设备当前剪贴板内容(ClipInject/atx-agent 通道读回,M1 起支持)。"""
+ try:
+ serial = _check_serial(serial)
+ import uiautomator2 as u2
+ d = u2.connect(serial)
+ text = d.clipboard
+ except PlatformError as e:
+ return _err(e)
+ except Exception as e:
+ return _err(PlatformError("platform_unavailable",
+ f"剪贴板读取失败: {type(e).__name__}: {str(e)[:100]}"))
+ audit.audit("de_read_clipboard", serial, "", f"{len(text or '')} 字符")
+ return _ok({"clipboard": text or ""})
+
+
@mcp.tool()
def de_wake(serial: str) -> dict:
"""点亮设备屏幕并解锁(熄屏时先调用它再截图)。"""
diff --git a/static/admin/agent.js b/static/admin/agent.js
index 2683e7b..2a23ef0 100644
--- a/static/admin/agent.js
+++ b/static/admin/agent.js
@@ -14,6 +14,8 @@ function initAgentChat(){
+ '例如:「打开抖音搜索奚学东,告诉我第一个视频的标题」
'
+ '点击右上角 ⚙ 配置模型与 API Key。';
}
+ bindChatScroll();
+ loadLiveDevices();
// 输入框快捷键
const inp = document.getElementById('agent-input');
inp.addEventListener('keydown', ev=>{
@@ -88,9 +90,18 @@ function clearEmpty(){
const empty = document.querySelector('#agent-chat .agent-empty');
if(empty) empty.remove();
}
-function scrollChat(){
+let _chatPinned = true; // 用户是否在底部(贴底才自动跟随滚动)
+function scrollChat(force){
const chat = document.getElementById('agent-chat');
- chat.scrollTop = chat.scrollHeight;
+ if(force || _chatPinned){
+ chat.scrollTop = chat.scrollHeight;
+ }
+}
+function bindChatScroll(){
+ const chat = document.getElementById('agent-chat');
+ chat.addEventListener('scroll', ()=>{
+ _chatPinned = chat.scrollHeight - chat.scrollTop - chat.clientHeight < 60;
+ });
}
function clearAgentChat(){
document.getElementById('agent-chat').innerHTML =
@@ -109,6 +120,18 @@ function newAgentSession(){
}
// ================== 发送与流式接收 ==================
+function setRunning(on){
+ _agentBusy = on;
+ document.getElementById('agent-running-tag').style.display = on ? 'inline' : 'none';
+ document.getElementById('btn-agent-stop').style.display = on ? 'inline-block' : 'none';
+ document.getElementById('btn-agent-send').disabled = on;
+}
+function stopAgent(){
+ apiPost('/api/agent/stop',{}).then(r=>{
+ if(r&&r.ok) showToast('已请求停止,正在中断…','success');
+ else showToast((r&&r.error)||'停止失败','error');
+ });
+}
function sendAgentMsg(){
if(_agentBusy){showToast('上一轮还在运行','error');return;}
const inp = document.getElementById('agent-input');
@@ -125,9 +148,7 @@ function sendAgentMsg(){
showToast(msg,'error');
return;
}
- _agentBusy = true;
- document.getElementById('agent-running-tag').style.display = 'inline';
- document.getElementById('btn-agent-send').disabled = true;
+ setRunning(true);
listenStream(r.run_id);
});
}
@@ -158,6 +179,7 @@ function listenStream(runId){
es.addEventListener('step', ev=>{
const d = JSON.parse(ev.data);
+ followSerialFromArgs(d.args);
if(!msgEl) msgEl = newAssistantMsg();
const cards = msgEl.querySelector('.agent-toolcards');
const card = document.createElement('div');
@@ -199,6 +221,49 @@ function listenStream(runId){
};
}
+// ============ 实时画面(右侧,MJPEG 流) ============
+let _liveSerial = '';
+function showLive(serial){
+ if(!serial || serial === _liveSerial) return;
+ _liveSerial = serial;
+ const img = document.getElementById('agent-live-img');
+ img.src = '/api/screen/stream?serial=' + encodeURIComponent(serial) + '&q=80&fps=8&t=' + Date.now();
+ img.style.display = 'block';
+ document.getElementById('agent-live-empty').style.display = 'none';
+ document.getElementById('agent-live-serial').textContent = serial;
+ const sel = document.getElementById('agent-live-select');
+ if(sel) sel.value = serial;
+}
+function stopLive(){
+ _liveSerial = '';
+ const img = document.getElementById('agent-live-img');
+ img.src = '';
+ img.style.display = 'none';
+ document.getElementById('agent-live-empty').style.display = 'block';
+ document.getElementById('agent-live-serial').textContent = '';
+}
+function followSerialFromArgs(argsStr){
+ try{
+ const a = JSON.parse(argsStr || '{}');
+ if(a.serial) showLive(a.serial);
+ }catch(e){}
+}
+function watchDevice(serial){
+ if(!serial){ stopLive(); return; }
+ showLive(serial);
+}
+function loadLiveDevices(){
+ apiGet('/api/devices').then(r=>{
+ if(!r||!r.ok)return;
+ const sel = document.getElementById('agent-live-select');
+ if(!sel)return;
+ const cur = sel.value;
+ const devs = (r.devices||[]).filter(x=>x.indexOf(':')>=0);
+ sel.innerHTML = ''
+ + devs.map(s=>'').join('');
+ });
+}
+
// 截图点击放大查看
function zoomScreenshot(img){
const ov = document.getElementById('agent-zoom');
@@ -226,10 +291,8 @@ document.addEventListener('click', ev=>{
});
function endRun(){
- _agentBusy = false;
+ setRunning(false);
_agentStream && _agentStream.close();
_agentStream = null;
- document.getElementById('agent-running-tag').style.display = 'none';
- document.getElementById('btn-agent-send').disabled = false;
scrollChat();
}
diff --git a/templates/admin/monitor.html b/templates/admin/monitor.html
index 492d92c..368980e 100644
--- a/templates/admin/monitor.html
+++ b/templates/admin/monitor.html
@@ -305,6 +305,15 @@ body{background:var(--bg);font-family:var(--body);color:var(--text);font-size:14
.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 控制台(聊天界面,浅色内容区——DeepSeek 风格,图片文字清晰) ===== */
+.agent-body{display:flex;flex:1;min-height:0}
+.agent-col-chat{flex:1;display:flex;flex-direction:column;min-width:0}
+.agent-col-live{width:300px;border-left:1px solid #e5e7eb;background:#fff;display:flex;flex-direction:column;padding:10px}
+.agent-live-head{font-size:12.5px;font-weight:600;color:#374151;display:flex;align-items:center;justify-content:space-between;margin-bottom:8px}
+.agent-live-serial{font-family:var(--mono);font-size:10.5px;color:#6b7280}
+.agent-live-body{flex:1;background:#0b0f14;border-radius:10px;display:flex;align-items:center;justify-content:center;overflow:hidden;min-height:200px}
+.agent-live-body img{width:100%;height:100%;object-fit:contain}
+.agent-live-body .agent-empty{color:#4b5563;padding:20px;font-size:12px;line-height:1.8}
+.agent-shell{display:flex;flex-direction:column;height:calc(100vh - 150px);min-height:520px;border:1px solid #e5e7eb;border-radius:14px;overflow:hidden;background:#fff}
.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}
@@ -458,11 +467,25 @@ body{background:var(--bg);font-family:var(--body);color:var(--text);font-size:14