feat: AI 目标设备必选 + 设备任务占用锁——①前端新增「🎯 目标设备」选择器(在线设备含型号,任务运行中的设备禁选),发送必须选定设备,AI 只操作你指定的设备;②/api/agent/run 校验:serial 不在池/离线/任务 running-connecting → 拒绝(409 提示任务名);③MCP 11 个写工具加 busy 锁(_ensure_device_free,5s 缓存):任务运行中的设备 AI 一律拒绝,AI 不与任务抢设备(外部 MCP 客户端同样受保护);④运行状态端点 GET /api/agent/run + 前端 8s 轮询:多人/多窗口能看到运行中任务(发起时间/任务/设备)并可停止(stop 加确认);未选设备直接 400 引导请选择设备,去掉模型自己乱挑设备的行为
This commit is contained in:
+65
-3
@@ -498,23 +498,85 @@ def agent_run():
|
||||
return jsonify({"ok": False, "error": "请先在配置区填写 API Key"}), 400
|
||||
if not cfg.get("model"):
|
||||
return jsonify({"ok": False, "error": "请先填写模型名"}), 400
|
||||
if not serial and not (cfg.get("default_serial") or "").strip():
|
||||
return jsonify({"ok": False, "error": "请先选择目标设备(AI 只操作你指定的设备)"}), 400
|
||||
# 目标设备校验:必须在池、在线、且无任务运行(AI 不与任务抢设备;
|
||||
# MCP 层另有 busy 锁兜底外部客户端)
|
||||
if serial:
|
||||
try:
|
||||
from web import context
|
||||
devs, err = context.mgr.get_status()
|
||||
dev = next((x for x in (devs or []) if x.get("serial") == serial), None)
|
||||
if err or dev is None:
|
||||
return jsonify({"ok": False, "error": f"设备 {serial} 不在设备池"}), 400
|
||||
if not dev.get("present"):
|
||||
return jsonify({"ok": False, "error": f"设备 {serial} 当前离线,请稍后再试"}), 400
|
||||
if dev.get("worker_status") in ("running", "connecting"):
|
||||
return jsonify({"ok": False, "error":
|
||||
f"设备 {serial} 正在执行任务「{dev.get('task_job') or ''}」——"
|
||||
f"AI 不与任务抢设备,任务结束后才能操作(或在任务页先停止)"}), 409
|
||||
except Exception:
|
||||
pass # 状态服务异常不阻塞(MCP busy 锁兜底)
|
||||
with _lock:
|
||||
if _run["state"] == "running":
|
||||
return jsonify({"ok": False, "error": "已有 Agent 运行中,请等待完成"}), 409
|
||||
return jsonify({"ok": False, "error":
|
||||
f"已有 Agent 运行中({_run.get('prompt', '')[:40]}…),"
|
||||
f"请等待完成或先停止"}), 409
|
||||
run_id = uuid.uuid4().hex[:8]
|
||||
from datetime import datetime as _dt
|
||||
_run.update(id=run_id, state="running", prompt=prompt,
|
||||
serial=(data.get("serial") or "").strip(),
|
||||
serial=serial,
|
||||
started=_dt.now().strftime("%H:%M:%S"),
|
||||
answer="", error="")
|
||||
# history 保留(同会话多轮对话),由前端「清空对话」调用 clear 重置
|
||||
_queues[run_id] = queue.Queue()
|
||||
_stop_events[run_id] = threading.Event()
|
||||
_log.info(f"Agent 启动: {prompt[:60]}")
|
||||
_log.info(f"Agent 启动: {prompt[:60]} @ {serial or 'default'}")
|
||||
threading.Thread(target=_agent_thread,
|
||||
args=(run_id, prompt, serial, cfg),
|
||||
daemon=True).start()
|
||||
return jsonify({"ok": True, "run_id": run_id})
|
||||
|
||||
|
||||
@bp.route("/api/agent/run", methods=["GET"])
|
||||
@admin_required
|
||||
def agent_run_status():
|
||||
"""当前 Agent 运行状态(多窗口/多人可见):idle / running + 任务摘要。
|
||||
|
||||
前端轮询它同步「运行中」状态(换浏览器/他人启动的任务也能看到并停止)。
|
||||
"""
|
||||
with _lock:
|
||||
return jsonify({"ok": True,
|
||||
"state": _run.get("state", "idle"),
|
||||
"prompt": _run.get("prompt", ""),
|
||||
"serial": _run.get("serial", ""),
|
||||
"started": _run.get("started", "")})
|
||||
|
||||
|
||||
@bp.route("/api/agent/devices")
|
||||
@admin_required
|
||||
def agent_devices():
|
||||
"""AI 可用设备列表:在线状态 + 是否有任务运行(前端选择器 busy 设备禁选)。
|
||||
|
||||
worker 状态实时(内存);busy = 任务 running/connecting。
|
||||
"""
|
||||
try:
|
||||
from web import context
|
||||
devs, err = context.mgr.get_status()
|
||||
except Exception as e:
|
||||
return jsonify({"ok": False, "error": str(e)[:120]}), 503
|
||||
out = []
|
||||
for d in (devs or []):
|
||||
busy = d.get("worker_status") in ("running", "connecting")
|
||||
out.append({"serial": d.get("serial"),
|
||||
"model": d.get("model") or "",
|
||||
"online": bool(d.get("present")),
|
||||
"busy": busy,
|
||||
"worker_status": d.get("worker_status") or "idle",
|
||||
"task_job": d.get("task_job") or ""})
|
||||
return jsonify({"ok": True, "devices": out})
|
||||
|
||||
|
||||
@bp.route("/api/agent/stream")
|
||||
@admin_required
|
||||
def agent_stream():
|
||||
|
||||
Reference in New Issue
Block a user