From 0f75db289daf5501c6c8f5959f1dbcc3201620f1 Mon Sep 17 00:00:00 2001 From: butubb <1422726308@qq.com> Date: Sun, 6 Sep 2026 09:32:11 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20AI=20=E7=9B=AE=E6=A0=87=E8=AE=BE?= =?UTF-8?q?=E5=A4=87=E5=BF=85=E9=80=89=20+=20=E8=AE=BE=E5=A4=87=E4=BB=BB?= =?UTF-8?q?=E5=8A=A1=E5=8D=A0=E7=94=A8=E9=94=81=E2=80=94=E2=80=94=E2=91=A0?= =?UTF-8?q?=E5=89=8D=E7=AB=AF=E6=96=B0=E5=A2=9E=E3=80=8C=F0=9F=8E=AF=20?= =?UTF-8?q?=E7=9B=AE=E6=A0=87=E8=AE=BE=E5=A4=87=E3=80=8D=E9=80=89=E6=8B=A9?= =?UTF-8?q?=E5=99=A8=EF=BC=88=E5=9C=A8=E7=BA=BF=E8=AE=BE=E5=A4=87=E5=90=AB?= =?UTF-8?q?=E5=9E=8B=E5=8F=B7=EF=BC=8C=E4=BB=BB=E5=8A=A1=E8=BF=90=E8=A1=8C?= =?UTF-8?q?=E4=B8=AD=E7=9A=84=E8=AE=BE=E5=A4=87=E7=A6=81=E9=80=89=EF=BC=89?= =?UTF-8?q?=EF=BC=8C=E5=8F=91=E9=80=81=E5=BF=85=E9=A1=BB=E9=80=89=E5=AE=9A?= =?UTF-8?q?=E8=AE=BE=E5=A4=87=EF=BC=8CAI=20=E5=8F=AA=E6=93=8D=E4=BD=9C?= =?UTF-8?q?=E4=BD=A0=E6=8C=87=E5=AE=9A=E7=9A=84=E8=AE=BE=E5=A4=87=EF=BC=9B?= =?UTF-8?q?=E2=91=A1/api/agent/run=20=E6=A0=A1=E9=AA=8C=EF=BC=9Aserial=20?= =?UTF-8?q?=E4=B8=8D=E5=9C=A8=E6=B1=A0/=E7=A6=BB=E7=BA=BF/=E4=BB=BB?= =?UTF-8?q?=E5=8A=A1=20running-connecting=20=E2=86=92=20=E6=8B=92=E7=BB=9D?= =?UTF-8?q?=EF=BC=88409=20=E6=8F=90=E7=A4=BA=E4=BB=BB=E5=8A=A1=E5=90=8D?= =?UTF-8?q?=EF=BC=89=EF=BC=9B=E2=91=A2MCP=2011=20=E4=B8=AA=E5=86=99?= =?UTF-8?q?=E5=B7=A5=E5=85=B7=E5=8A=A0=20busy=20=E9=94=81=EF=BC=88=5Fensur?= =?UTF-8?q?e=5Fdevice=5Ffree=EF=BC=8C5s=20=E7=BC=93=E5=AD=98=EF=BC=89?= =?UTF-8?q?=EF=BC=9A=E4=BB=BB=E5=8A=A1=E8=BF=90=E8=A1=8C=E4=B8=AD=E7=9A=84?= =?UTF-8?q?=E8=AE=BE=E5=A4=87=20AI=20=E4=B8=80=E5=BE=8B=E6=8B=92=E7=BB=9D?= =?UTF-8?q?=EF=BC=8CAI=20=E4=B8=8D=E4=B8=8E=E4=BB=BB=E5=8A=A1=E6=8A=A2?= =?UTF-8?q?=E8=AE=BE=E5=A4=87=EF=BC=88=E5=A4=96=E9=83=A8=20MCP=20=E5=AE=A2?= =?UTF-8?q?=E6=88=B7=E7=AB=AF=E5=90=8C=E6=A0=B7=E5=8F=97=E4=BF=9D=E6=8A=A4?= =?UTF-8?q?=EF=BC=89=EF=BC=9B=E2=91=A3=E8=BF=90=E8=A1=8C=E7=8A=B6=E6=80=81?= =?UTF-8?q?=E7=AB=AF=E7=82=B9=20GET=20/api/agent/run=20+=20=E5=89=8D?= =?UTF-8?q?=E7=AB=AF=208s=20=E8=BD=AE=E8=AF=A2=EF=BC=9A=E5=A4=9A=E4=BA=BA/?= =?UTF-8?q?=E5=A4=9A=E7=AA=97=E5=8F=A3=E8=83=BD=E7=9C=8B=E5=88=B0=E8=BF=90?= =?UTF-8?q?=E8=A1=8C=E4=B8=AD=E4=BB=BB=E5=8A=A1=EF=BC=88=E5=8F=91=E8=B5=B7?= =?UTF-8?q?=E6=97=B6=E9=97=B4/=E4=BB=BB=E5=8A=A1/=E8=AE=BE=E5=A4=87?= =?UTF-8?q?=EF=BC=89=E5=B9=B6=E5=8F=AF=E5=81=9C=E6=AD=A2=EF=BC=88stop=20?= =?UTF-8?q?=E5=8A=A0=E7=A1=AE=E8=AE=A4=EF=BC=89=EF=BC=9B=E6=9C=AA=E9=80=89?= =?UTF-8?q?=E8=AE=BE=E5=A4=87=E7=9B=B4=E6=8E=A5=20400=20=E5=BC=95=E5=AF=BC?= =?UTF-8?q?=E8=AF=B7=E9=80=89=E6=8B=A9=E8=AE=BE=E5=A4=87=EF=BC=8C=E5=8E=BB?= =?UTF-8?q?=E6=8E=89=E6=A8=A1=E5=9E=8B=E8=87=AA=E5=B7=B1=E4=B9=B1=E6=8C=91?= =?UTF-8?q?=E8=AE=BE=E5=A4=87=E7=9A=84=E8=A1=8C=E4=B8=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mcp_server/mcp_server.py | 37 ++++++++++++++++++++ static/admin/agent.js | 68 ++++++++++++++++++++++++++++++++++-- templates/admin/monitor.html | 21 ++++++++--- web/agent_api.py | 68 ++++++++++++++++++++++++++++++++++-- 4 files changed, 184 insertions(+), 10 deletions(-) diff --git a/mcp_server/mcp_server.py b/mcp_server/mcp_server.py index 2abbbda..2728a90 100644 --- a/mcp_server/mcp_server.py +++ b/mcp_server/mcp_server.py @@ -52,6 +52,32 @@ def _check_write(): raise PlatformError("write_disabled", "写操作未启用(MCP_ALLOW_WRITE=1 开启)") +# 设备任务占用锁:serial -> (ts, worker_status, task_job)。AI 写操作前检查, +# 任务 running/connecting 的设备拒绝操作(AI 不与任务抢设备)。5s 缓存。 +_busy_cache = {} + + +def _ensure_device_free(serial): + """写操作前检查设备是否有任务在跑(worker running/connecting → 拒绝)。""" + import time + now = time.time() + c = _busy_cache.get(serial) + if not c or now - c[0] > 5: + try: + devs = platform().list_devices() + info = next((d for d in devs if d.get("serial") == serial), {}) + c = (now, info.get("worker_status") or "idle", + info.get("task_job") or "") + _busy_cache[serial] = c + except Exception: + return # 状态查询失败不阻塞(操作失败会另行报错) + if c[1] in ("running", "connecting"): + raise PlatformError( + "device_busy", + f"设备正在执行任务「{c[2] or '未知'}」——AI 不与任务抢设备," + f"任务结束后才能操作(可在平台任务页先停止任务)") + + def _to_native(serial, x, y): """截图坐标 → 设备原生坐标(按最近一次截图的比例换算)。""" c = _coord.get(serial) @@ -138,6 +164,7 @@ def de_tap(serial: str, x: int, y: int) -> dict: try: _check_write() serial = _check_serial(serial) + _ensure_device_free(serial) if x < 0 or y < 0: raise PlatformError("invalid_param", "坐标不能为负") nx, ny = _to_native(serial, x, y) @@ -160,6 +187,7 @@ def de_swipe(serial: str, x1: int, y1: int, x2: int, y2: int, try: _check_write() serial = _check_serial(serial) + _ensure_device_free(serial) nx1, ny1 = _to_native(serial, x1, y1) nx2, ny2 = _to_native(serial, x2, y2) platform().swipe(serial, nx1, ny1, nx2, ny2, duration) @@ -217,6 +245,7 @@ def de_tap_element(serial: str, by: str, value: str, index: int = 1) -> dict: try: _check_write() serial = _check_serial(serial) + _ensure_device_free(serial) if by not in ("text", "id", "desc", "text_contains", "desc_contains"): raise PlatformError("invalid_param", "by 可选 text/id/desc/text_contains/desc_contains") @@ -270,6 +299,7 @@ def de_wake(serial: str) -> dict: try: _check_write() serial = _check_serial(serial) + _ensure_device_free(serial) platform().wake(serial) except PlatformError as e: return _err(e) @@ -283,6 +313,7 @@ def de_press_key(serial: str, key: str) -> dict: try: _check_write() serial = _check_serial(serial) + _ensure_device_free(serial) if key not in _KEYS: raise PlatformError("invalid_param", f"不支持的按键: {key}(可选 {_KEYS})") platform().press_key(serial, key) @@ -299,6 +330,7 @@ def de_open_app(serial: str, package: str) -> dict: try: _check_write() serial = _check_serial(serial) + _ensure_device_free(serial) if not package: raise PlatformError("invalid_param", "缺少包名") from mcp_server import direct_ops @@ -317,6 +349,7 @@ def de_stop_app(serial: str, package: str) -> dict: try: _check_write() serial = _check_serial(serial) + _ensure_device_free(serial) if not package: raise PlatformError("invalid_param", "缺少包名") from mcp_server import direct_ops @@ -350,6 +383,7 @@ def de_type_text(serial: str, text: str) -> dict: try: _check_write() serial = _check_serial(serial) + _ensure_device_free(serial) if not text: raise PlatformError("invalid_param", "内容为空") from mcp_server import direct_ops @@ -370,6 +404,7 @@ def de_set_clipboard(serial: str, text: str) -> dict: try: _check_write() serial = _check_serial(serial) + _ensure_device_free(serial) if not text: raise PlatformError("invalid_param", "内容为空") from mcp_server import direct_ops @@ -390,6 +425,7 @@ def de_sleep(serial: str) -> dict: try: _check_write() serial = _check_serial(serial) + _ensure_device_free(serial) platform().sleep(serial) except PlatformError as e: return _err(e) @@ -429,6 +465,7 @@ def de_tap_text(serial: str, text: str) -> dict: try: _check_write() serial = _check_serial(serial) + _ensure_device_free(serial) if not text or len(text) > 100: raise PlatformError("invalid_param", "text 不能为空且 ≤100 字符") res = platform().tap_text(serial, text) diff --git a/static/admin/agent.js b/static/admin/agent.js index 27bf38c..82c5727 100644 --- a/static/admin/agent.js +++ b/static/admin/agent.js @@ -4,6 +4,7 @@ let _agentStream = null; // EventSource let _agentCfgLoaded = false; // ================== 初始化 ================== +let _agentDefaultSerial = ''; function initAgentChat(){ if(_agentCfgLoaded)return; _agentCfgLoaded = true; @@ -12,10 +13,12 @@ function initAgentChat(){ if(chat && !chat.children.length){ chat.innerHTML = '