Files
workphone-sdk/sdk/agent/agent.py

901 lines
36 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""
工作手机Agent v3.0 - AI数字员工
运行在Android手机上主动连接SDK服务器保持实时通信
核心架构:
┌──────────────────────────────────────────────────────┐
│ 手机端 Agent (Python/Termux) │
│ ┌─────────────┐ ┌─────────────┐ ┌──────────────┐ │
│ │ WebSocket │ │ Skill引擎 │ │ 状态监控 │ │
│ │ 实时连接 │ │ 微信/抖音 │ │ 电量/网络 │ │
│ └──────┬──────┘ └──────┬──────┘ └──────┬───────┘ │
│ └────────────────┴─────────────────┘ │
│ ↕ uiautomator2 / AccessibilityService │
│ ┌────────────────────────────────────┐ │
│ │ Android系统 + 微信/抖音等APP │ │
│ └────────────────────────────────────┘ │
└──────────────────────────────────────────────────────┘
↕ WebSocket (wss://)
┌──────────────────────────────────────────────────────┐
│ SDK服务器 (FastAPI) │
│ 存客宝/触客宝通过API调用 → 服务器下发命令到手机 │
└──────────────────────────────────────────────────────┘
连接机制:
- 手机主动发起WebSocket连接到服务器
- 心跳保活: 每30秒发送心跳包服务器10秒无响应则重连
- 指数退避重连: 2s → 4s → 8s → 16s → 30s(最大)
- 断线自动重连,永不断开
- 操作时界面无感通过u2后台操作用户无感知
配置优先级: 环境变量 > 命令行参数 > config.json > 默认值
WP_DEVICE_ID 设备ID
WP_SERVER_URL WebSocket基础地址如 ws://192.168.1.100:8899/ws/device
WP_PROJECT_ID 项目ID
WP_HEARTBEAT 心跳间隔(秒)
@author 卡若
@version 3.0.0
"""
import asyncio
import json
import logging
import argparse
import os
import sys
import signal
import time
import base64
import random
from datetime import datetime
from typing import Optional, Dict, Any
# 确保 agent/ 目录在 sys.path 中,方便 Termux 等环境直接运行
_AGENT_DIR = os.path.dirname(os.path.abspath(__file__))
if _AGENT_DIR not in sys.path:
sys.path.insert(0, _AGENT_DIR)
try:
import websockets
except ImportError:
print("❌ 请安装: pip install websockets>=12.0")
sys.exit(1)
try:
import uiautomator2 as u2
except ImportError:
u2 = None
print("⚠️ uiautomator2未安装部分功能不可用pip install uiautomator2>=3.0.0")
# 配置日志
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s [%(levelname)s] %(message)s'
)
logger = logging.getLogger(__name__)
class WorkPhoneAgent:
"""
AI数字员工 - 手机端Agent
核心职责:
1. 主动连接SDK服务器并保持实时通信
2. 接收服务器命令并在手机上执行
3. 实时上报设备状态(电量/网络/APP状态
4. 支持微信/抖音/小红书等多APP控制
"""
VERSION = "3.0.0"
# 重连策略: 指数退避
MIN_RECONNECT_DELAY = 2 # 最小重连间隔(秒)
MAX_RECONNECT_DELAY = 30 # 最大重连间隔(秒)
def __init__(
self,
device_id: str,
server_url: str,
heartbeat_interval: int = 30,
project_id: str = "default",
):
self.device_id = device_id
self.server_url = server_url
self.heartbeat_interval = heartbeat_interval
self.project_id = project_id
self.ws: Optional[websockets.WebSocketClientProtocol] = None
self.running = False
self.connected = False
self.reconnect_attempts = 0
self.last_heartbeat_ack = time.time()
self.commands_executed = 0
self.start_time = None
# 初始化uiautomator2
self.d = None
if u2:
try:
self.d = u2.connect()
self.d.implicitly_wait(10.0)
# 设置u2操作不显示弹窗
self.d.settings['operation_delay'] = (0, 0)
self.d.settings['operation_delay_methods'] = []
logger.info(f"uiautomator2连接成功: {self.d.info.get('productName', 'Unknown')}")
except Exception as e:
logger.warning(f"uiautomator2连接失败: {e}")
logger.info(f"🤖 AI数字员工初始化完成")
logger.info(f" 设备ID: {device_id}")
logger.info(f" 服务器: {server_url}")
logger.info(f" 心跳间隔: {heartbeat_interval}")
# ====================================================================
# 一、连接管理(核心:主动连接 + 心跳保活 + 指数退避重连)
# ====================================================================
async def connect(self):
"""主连接循环 - 永不停止"""
while self.running:
try:
logger.info(f"📡 正在连接服务器: {self.server_url}")
async with websockets.connect(
self.server_url,
ping_interval=20,
ping_timeout=10,
close_timeout=5,
max_size=10 * 1024 * 1024,
) as ws:
self.ws = ws
self.connected = True
self.reconnect_attempts = 0 # 重置重连计数
self.last_heartbeat_ack = time.time()
logger.info("✅ 服务器连接成功!")
# 1. 发送注册消息
await self._register()
# 注册后上报 agent_started便于服务端记录设备上线
await self._send_event("agent_started", {"device_id": self.device_id, "project_id": self.project_id})
# 向服务端拉取配置如心跳间隔ack 里会更新 self.heartbeat_interval
await self._send_device_request("get_config", {})
# 2. 启动并发任务
tasks = [
asyncio.create_task(self._heartbeat_loop()),
asyncio.create_task(self._status_report_loop()),
asyncio.create_task(self._message_loop()),
]
# 等待任何一个任务结束(通常是消息循环断开)
done, pending = await asyncio.wait(
tasks, return_when=asyncio.FIRST_COMPLETED
)
# 取消剩余任务
for task in pending:
task.cancel()
try:
await task
except asyncio.CancelledError:
pass
except websockets.ConnectionClosed as e:
logger.warning(f"🔌 连接断开: code={e.code}, reason={e.reason}")
except ConnectionRefusedError:
logger.warning("🚫 服务器拒绝连接")
except OSError as e:
logger.warning(f"🌐 网络错误: {e}")
except Exception as e:
logger.error(f"❌ 连接异常: {e}")
finally:
self.connected = False
self.ws = None
if self.running:
self.reconnect_attempts += 1
# 断网时先尝试恢复网络(通过 Hawk 模块,仅合法操作:开 WiFi、打开设置、连接已保存网络
if self.d:
try:
from hawk import try_reconnect_network
nr = try_reconnect_network(self.d)
if nr.get("success"):
logger.info(f"🌐 网络已恢复: {nr.get('message', '')}")
else:
logger.info(f"🌐 网络恢复: {nr.get('message', '')}")
except ImportError:
pass
except Exception as e:
logger.debug(f"网络恢复尝试异常: {e}")
# 指数退避重连
delay = self._get_reconnect_delay()
logger.info(f"{delay:.1f}秒后第{self.reconnect_attempts}次重连...")
await asyncio.sleep(delay)
def _get_reconnect_delay(self) -> float:
"""计算重连延迟(指数退避 + 随机抖动)"""
delay = min(
self.MIN_RECONNECT_DELAY * (2 ** self.reconnect_attempts),
self.MAX_RECONNECT_DELAY
)
# 添加随机抖动避免雪崩
jitter = random.uniform(0, delay * 0.2)
return delay + jitter
async def _register(self):
"""发送设备注册信息"""
device_info = self._get_device_info()
await self.ws.send(json.dumps({
"type": "register",
"data": {
**device_info,
"project_id": self.project_id,
"agent_version": self.VERSION,
"registered_at": datetime.now().isoformat(),
}
}))
logger.info(f"📋 设备已注册: {device_info.get('model', 'Unknown')}")
# ====================================================================
# 二、心跳机制(参考抖音心跳: 应用层心跳 + 状态上报)
# ====================================================================
async def _heartbeat_loop(self):
"""
心跳循环 - 保持连接活性
机制说明参考抖音等APP的心跳设计
1. 每30秒发送应用层心跳不同于WebSocket层ping
2. 心跳包含设备状态摘要(电量、内存、网络)
3. 服务器需要在10秒内响应pong
4. 连续3次无响应则主动断开重连
"""
missed_count = 0
while self.connected:
try:
await asyncio.sleep(self.heartbeat_interval)
if not self.connected or not self.ws:
break
# 发送心跳(含状态摘要)
heartbeat_data = {
"type": "heartbeat",
"timestamp": int(time.time()),
"device_id": self.device_id,
"uptime": int(time.time() - self.start_time) if self.start_time else 0,
"commands_executed": self.commands_executed,
"status": self._get_quick_status(),
}
await self.ws.send(json.dumps(heartbeat_data))
# 检查上次心跳是否有ACK
time_since_ack = time.time() - self.last_heartbeat_ack
if time_since_ack > self.heartbeat_interval * 3:
missed_count += 1
logger.warning(f"⚠️ 心跳无响应 ({missed_count}/3)上次ACK: {time_since_ack:.0f}秒前")
if missed_count >= 3:
logger.error("💔 连续3次心跳无响应主动断开重连")
await self.ws.close()
break
else:
missed_count = 0
except websockets.ConnectionClosed:
break
except Exception as e:
logger.error(f"心跳错误: {e}")
break
async def _status_report_loop(self):
"""定期上报详细设备状态每5分钟"""
while self.connected:
try:
await asyncio.sleep(300) # 5分钟
if not self.connected or not self.ws:
break
status = self._get_full_status()
await self.ws.send(json.dumps({
"type": "status_report",
"data": status
}))
logger.debug(f"📊 状态上报完成")
except Exception:
break
def _get_quick_status(self) -> dict:
"""获取快速状态摘要(心跳用,低开销)"""
status = {"online": True}
if self.d:
try:
info = self.d.info
status["screen_on"] = info.get("screenOn", False)
status["current_app"] = info.get("currentPackageName", "")
except:
pass
return status
def _get_full_status(self) -> dict:
"""获取完整设备状态"""
status = {
"device_id": self.device_id,
"agent_version": self.VERSION,
"uptime": int(time.time() - self.start_time) if self.start_time else 0,
"commands_executed": self.commands_executed,
"connected": self.connected,
"timestamp": datetime.now().isoformat(),
}
if self.d:
try:
info = self.d.info
status.update({
"screen_on": info.get("screenOn", False),
"current_app": info.get("currentPackageName", ""),
"display": {
"width": info.get("displayWidth", 0),
"height": info.get("displayHeight", 0),
},
"rotation": info.get("displayRotation", 0),
})
# 获取电池信息
try:
battery = self.d.shell("dumpsys battery | grep -E 'level|status|plugged'").output
for line in battery.strip().split('\n'):
line = line.strip()
if 'level:' in line:
status["battery_level"] = int(line.split(':')[1].strip())
elif 'status:' in line:
status["battery_status"] = int(line.split(':')[1].strip())
elif 'plugged:' in line:
status["charging"] = int(line.split(':')[1].strip()) > 0
except:
pass
# 获取网络信息
try:
wifi = self.d.shell("dumpsys wifi | grep 'Wi-Fi is'").output.strip()
status["wifi"] = "enabled" in wifi.lower()
except:
pass
except Exception as e:
status["error"] = str(e)
return status
# ====================================================================
# 三、消息处理(接收服务器命令,执行操作)
# ====================================================================
async def _message_loop(self):
"""消息接收循环"""
try:
async for message in self.ws:
await self._handle_message(message)
except websockets.ConnectionClosed:
logger.info("消息循环: 连接已关闭")
except Exception as e:
logger.error(f"消息循环错误: {e}")
async def _handle_message(self, message: str):
"""处理服务器消息"""
try:
data = json.loads(message)
msg_type = data.get("type")
command_id = data.get("command_id")
# 心跳ACK
if msg_type in ("pong", "heartbeat_ack"):
self.last_heartbeat_ack = time.time()
return
# 注册确认
if msg_type == "registered":
logger.info("✅ 服务器确认注册")
return
logger.info(f"📩 收到命令: {msg_type} (id={command_id})")
if msg_type == "execute":
cmd_data = data.get("data", {})
result = await self._execute_command(cmd_data)
self.commands_executed += 1
await self._send_response(command_id, result)
# 技能执行完毕后上报事件,供服务端落库/转发
if cmd_data.get("script"):
await self._send_event("skill_done", {
"script": cmd_data.get("script"),
"action": cmd_data.get("action"),
"code": result.get("code", 200),
"success": result.get("code") == 200,
})
elif msg_type == "agent_execute":
result = await self._execute_agent_task(data.get("data", {}))
self.commands_executed += 1
await self._send_response(command_id, result)
elif msg_type == "config_update":
# 服务器推送配置更新
await self._handle_config_update(data.get("data", {}))
elif msg_type == "device_request_ack":
# 服务端对 device_request 的应答,可应用下发的配置
ack_data = data.get("data") or {}
if ack_data.get("heartbeat_interval") is not None:
self.heartbeat_interval = int(ack_data["heartbeat_interval"])
logger.info(f"已应用服务端配置: heartbeat_interval={self.heartbeat_interval}")
logger.debug(f"device_request_ack: request_id={data.get('request_id')} success={data.get('success')}")
else:
logger.warning(f"未知消息类型: {msg_type}")
except Exception as e:
logger.error(f"处理消息错误: {e}")
if command_id:
await self._send_response(command_id, {
"code": 500,
"message": str(e)
})
async def _send_response(self, command_id: str, result: dict):
"""发送命令执行结果"""
if not self.ws or not self.connected:
return
try:
await self.ws.send(json.dumps({
"type": "response",
"command_id": command_id,
"device_id": self.device_id,
"code": result.get("code", 200),
"message": result.get("message", "success"),
"data": result.get("data", {}),
"timestamp": int(time.time()),
}))
except Exception as e:
logger.error(f"发送响应失败: {e}")
async def _send_event(self, event: str, data: dict = None):
"""设备端事件上报(技能执行完毕、异常等),由服务端处理/落库/转发"""
if not self.ws or not self.connected:
return
try:
await self.ws.send(json.dumps({
"type": "event",
"device_id": self.device_id,
"event": event,
"data": data or {},
"timestamp": int(time.time()),
}))
except Exception as e:
logger.error(f"发送事件失败: {e}")
async def _send_device_request(self, action: str, params: dict = None) -> dict:
"""设备端请求服务端执行操作(拉配置、落库等),等待 device_request_ack可选"""
if not self.ws or not self.connected:
return {"success": False, "error": "未连接"}
request_id = f"req_{int(time.time() * 1000)}_{random.randint(1000, 9999)}"
try:
await self.ws.send(json.dumps({
"type": "device_request",
"device_id": self.device_id,
"request_id": request_id,
"action": action,
"params": params or {},
"timestamp": int(time.time()),
}))
except Exception as e:
logger.error(f"发送 device_request 失败: {e}")
return {"success": False, "error": str(e)}
return {"success": True, "request_id": request_id}
async def _handle_config_update(self, config: dict):
"""处理服务器推送的配置更新"""
if "heartbeat_interval" in config:
self.heartbeat_interval = config["heartbeat_interval"]
logger.info(f"心跳间隔更新为: {self.heartbeat_interval}")
# ====================================================================
# 四、命令执行(操作手机)
# ====================================================================
async def _execute_command(self, cmd: dict) -> dict:
"""执行命令"""
action = cmd.get("action")
params = cmd.get("params", {})
script = cmd.get("script")
try:
if script:
return await self._execute_skill(script, action, params)
if not self.d:
return {"code": 503, "message": "uiautomator2未连接"}
# 基础操作
if action == "screenshot":
return self._screenshot()
elif action == "click":
self.d.click(params["x"], params["y"])
return {"code": 200, "data": {"success": True}}
elif action == "click_text":
text = params["text"]
timeout = params.get("timeout", 10)
if self.d(text=text).wait(timeout=timeout):
self.d(text=text).click()
return {"code": 200, "data": {"success": True, "text": text}}
return {"code": 404, "message": f"未找到: {text}"}
elif action == "input":
if params.get("clear", True):
self.d.clear_text()
self.d.send_keys(params["text"])
return {"code": 200, "data": {"success": True}}
elif action == "swipe":
if "x1" in params and "y1" in params and "x2" in params and "y2" in params:
dur = params.get("duration", 300) / 1000.0
self.d.swipe(params["x1"], params["y1"], params["x2"], params["y2"], duration=dur)
else:
direction = params.get("direction", "up")
self.d.swipe_ext(direction, scale=params.get("scale", 0.8))
return {"code": 200, "data": {"success": True}}
elif action == "ui_tree":
xml = self.d.dump_hierarchy()
return {"code": 200, "data": {"xml": xml, "length": len(xml)}}
elif action == "app_start":
self.d.app_start(params["package"])
return {"code": 200, "data": {"success": True}}
elif action == "app_stop":
self.d.app_stop(params["package"])
return {"code": 200, "data": {"success": True}}
elif action == "press_key":
self.d.press(params.get("key", "home"))
return {"code": 200, "data": {"success": True}}
elif action == "device_info":
return {"code": 200, "data": self._get_device_info()}
elif action == "status":
return {"code": 200, "data": self._get_full_status()}
else:
return {"code": 400, "message": f"未知操作: {action}"}
except Exception as e:
logger.error(f"执行命令错误: {e}")
return {"code": 500, "message": str(e)}
async def _execute_skill(self, script: str, action: str, params: dict) -> dict:
"""执行APP技能"""
try:
if not self.d:
return {"code": 503, "message": "uiautomator2未连接"}
# 动态加载技能
# 通过技能注册表获取
from skills import get_skill
try:
skill_class = get_skill(script)
except ImportError as ie:
return {"code": 404, "message": str(ie)}
skill = skill_class(self.d)
# 调用方法
method = getattr(skill, action, None)
if not method:
return {"code": 404, "message": f"技能{script}不支持操作: {action}"}
# 执行(同步或异步)
if asyncio.iscoroutinefunction(method):
result = await method(**params)
else:
result = method(**params)
return {"code": 200, "data": result}
except Exception as e:
logger.error(f"执行技能错误 [{script}.{action}]: {e}")
return {"code": 500, "message": str(e)}
async def _execute_agent_task(self, data: dict) -> dict:
"""执行AI Agent任务自然语言控制优先走 SkillExecutor 微信/抖音复合任务,无 LLM 也可执行"""
task = (data.get("task") or "").strip()
logger.info(f"🤖 AI任务: {task}")
if not task:
return {"code": 400, "message": "task 为空", "data": {"success": False, "error": "task 为空"}}
try:
if not self.d:
return {"code": 503, "message": "uiautomator2未连接", "data": {"success": False, "error": "设备未连接"}}
try:
from skill_executor import SkillExecutor
executor = SkillExecutor(self.d)
except ImportError:
executor = None
if executor:
result = None
if "微信" in task:
result = executor.execute_wechat_task(task)
elif "抖音" in task:
result = executor.execute_douyin_task(task)
elif "小红书" in task:
result = executor.execute_xhs_task(task)
elif "闲鱼" in task:
result = executor.execute_xianyu_task(task)
elif "Soul" in task or "soul" in task.lower() or "灵魂" in task:
result = executor.execute_soul_task(task)
elif any(k in task for k in ("连接网络", "恢复网络", "打开WiFi", "打开网络", "断网", "连不上网", "上网")):
result = executor.execute_network_reconnect()
else:
result = executor.execute_command(task)
if result is None:
result = {"success": False, "error": "未匹配到执行路径"}
return {"code": 200, "data": result}
return {"code": 200, "data": {"success": False, "error": "AI Agent任务引擎未加载", "task": task}}
except Exception as e:
logger.exception(f"AI任务执行异常: {e}")
return {"code": 500, "message": str(e), "data": {"success": False, "error": str(e), "task": task}}
# ====================================================================
# 五、设备信息
# ====================================================================
def _screenshot(self) -> dict:
"""截图"""
try:
img = self.d.screenshot(format='raw')
b64 = base64.b64encode(img).decode('utf-8')
info = self.d.info
return {
"code": 200,
"data": {
"base64": b64,
"width": info.get("displayWidth", 0),
"height": info.get("displayHeight", 0),
"size": len(img),
}
}
except Exception as e:
return {"code": 500, "message": str(e)}
def _get_device_info(self) -> dict:
"""获取设备信息"""
info = {}
if self.d:
try:
d_info = self.d.info
info = {
"device_id": self.device_id,
"model": d_info.get("productName", "Unknown"),
"brand": d_info.get("brand", "Unknown"),
"android_version": str(d_info.get("sdkVersion", "Unknown")),
"display": {
"width": d_info.get("displayWidth", 0),
"height": d_info.get("displayHeight", 0),
},
"screen_on": d_info.get("screenOn", False),
}
# 检测已安装的APP
apps = []
try:
output = self.d.shell("pm list packages -3").output
app_detect = {
'com.tencent.mm': 'wechat',
'com.ss.android.ugc.aweme': 'douyin',
'com.xingin.xhs': 'xhs',
'com.taobao.idlefish': 'xianyu',
'cn.soulapp.android': 'soul',
}
for line in output.strip().split('\n'):
pkg = line.replace('package:', '').strip()
for check_pkg, name in app_detect.items():
if check_pkg in pkg:
apps.append(name)
except:
pass
info["installed_apps"] = apps
info["capabilities"] = [
"u2", "screenshot", "click", "input", "swipe",
"ui_tree", "app_control", "skill_execute",
"skill_wechat", "skill_douyin", "skill_xhs", "skill_xianyu",
"skill_network_reconnect", "hawk",
"event", "device_request"
]
except Exception as e:
info["error"] = str(e)
else:
info = {
"device_id": self.device_id,
"model": "Unknown (u2未连接)",
"capabilities": [],
}
info["agent_version"] = self.VERSION
info["project_id"] = self.project_id
return info
# ====================================================================
# 六、启动/停止
# ====================================================================
async def start(self):
"""启动Agent"""
self.running = True
self.start_time = time.time()
logger.info("=" * 50)
logger.info("🚀 AI数字员工 v3.0 启动")
logger.info(f" 设备ID: {self.device_id}")
logger.info(f" 服务器: {self.server_url}")
logger.info(f" 项目ID: {self.project_id}")
logger.info("=" * 50)
await self.connect()
def stop(self):
"""停止Agent"""
self.running = False
self.connected = False
logger.info("🛑 AI数字员工已停止")
def _detect_device_id() -> str:
"""自动检测设备IDTermux / ADB / fallback"""
import subprocess
# 1. Termux: getprop
try:
r = subprocess.run(['getprop', 'ro.serialno'], capture_output=True, text=True, timeout=5)
serial = r.stdout.strip()
if serial:
return serial
except Exception:
pass
# 2. ADB serial从环境变量模拟器常用
serial = os.environ.get("ANDROID_SERIAL", "")
if serial:
return serial
# 3. 通过 uiautomator2 获取
if u2:
try:
d = u2.connect()
serial = d.serial
if serial:
return serial
except Exception:
pass
# 4. fallback: 基于时间戳
return f"agent-{int(time.time())}"
def _resolve_config(args) -> dict:
"""
配置级联解析(优先级: 环境变量 > 命令行 > config.json > 默认值)
返回 {device_id, server_url, heartbeat_interval, project_id}
"""
# 1. 读取 config.json
config = {}
config_path = args.config or os.path.join(_AGENT_DIR, 'config.json')
if os.path.exists(config_path):
try:
with open(config_path) as f:
config = json.load(f)
logger.info(f"📄 已加载配置: {config_path}")
except Exception as e:
logger.warning(f"读取配置文件失败: {e}")
# 2. 级联合并
device_id = (
os.environ.get("WP_DEVICE_ID")
or args.device_id
or config.get("device_id")
or _detect_device_id()
)
server_base = (
os.environ.get("WP_SERVER_URL")
or args.server
or config.get("server_url")
or "ws://192.168.1.100:8899/ws/device"
).rstrip("/")
heartbeat = int(
os.environ.get("WP_HEARTBEAT")
or (args.heartbeat if args.heartbeat is not None else 0)
or config.get("heartbeat_interval")
or 10
)
project_id = (
os.environ.get("WP_PROJECT_ID")
or args.project
or config.get("project_id")
or "cunkebao"
)
# 3. 拼接完整 WebSocket URL基础地址 + /设备ID
server_url = f"{server_base}/{device_id}"
return {
"device_id": device_id,
"server_url": server_url,
"heartbeat_interval": heartbeat,
"project_id": project_id,
}
def main():
parser = argparse.ArgumentParser(
description='AI数字员工 - 工作手机Agent v3.0',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
配置优先级: 环境变量 > 命令行参数 > config.json > 默认值
环境变量:
WP_DEVICE_ID 设备ID默认自动检测
WP_SERVER_URL WebSocket基础地址
WP_PROJECT_ID 项目ID
WP_HEARTBEAT 心跳间隔(秒)
示例:
# 基本启动(读 config.json
python agent.py
# 指定服务器
python agent.py -s ws://192.168.1.100:8899/ws/device
# 环境变量
WP_SERVER_URL=ws://10.0.0.1:8899/ws/device python agent.py
"""
)
parser.add_argument('--device-id', '-d', default=None, help='设备ID默认自动检测')
parser.add_argument('--server', '-s', default=None, help='服务器WebSocket基础地址')
parser.add_argument('--heartbeat', '-hb', type=int, default=None, help='心跳间隔(秒)建议5/10/30')
parser.add_argument('--project', '-p', default=None, help='项目ID')
parser.add_argument('--config', '-c', default=None, help='配置文件路径(默认 config.json')
args = parser.parse_args()
cfg = _resolve_config(args)
agent = WorkPhoneAgent(
device_id=cfg["device_id"],
server_url=cfg["server_url"],
heartbeat_interval=cfg["heartbeat_interval"],
project_id=cfg["project_id"],
)
# 信号处理:优雅关闭
def _signal_handler(sig, frame):
logger.info(f"收到信号 {sig},正在停止...")
agent.stop()
signal.signal(signal.SIGINT, _signal_handler)
signal.signal(signal.SIGTERM, _signal_handler)
try:
asyncio.run(agent.start())
except KeyboardInterrupt:
pass
finally:
agent.stop()
logger.info("👋 Agent已退出")
if __name__ == "__main__":
main()