Files
workphone-sdk/sdk/app/main.py

206 lines
6.6 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.

"""
工作手机SDK v3.0 - 主入口
存客宝的AI手机控制引擎
"""
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse, Response
from contextlib import asynccontextmanager
import logging
import os
import subprocess
import asyncio
from config import settings
from routers import devices, unified, agent, adb, experience, projects, qrcode, voice, capture, hook_modules, connection
from services.ws_hub import ws_hub
from services.device_manager import device_manager
# 配置日志
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
@asynccontextmanager
async def lifespan(app: FastAPI):
"""应用生命周期管理"""
# 启动
logger.info("🚀 工作手机SDK v3.0 启动中...")
await device_manager.init()
logger.info("✅ 数据库连接成功")
heartbeat_task = asyncio.create_task(_heartbeat_sweeper())
yield
# 关闭
logger.info("🛑 工作手机SDK 关闭中...")
heartbeat_task.cancel()
try:
await heartbeat_task
except asyncio.CancelledError:
pass
await device_manager.close()
async def _heartbeat_sweeper():
"""后台心跳巡检任务:清理长时间未上报心跳的设备"""
while True:
await asyncio.sleep(max(5, settings.WS_HEARTBEAT_INTERVAL))
await ws_hub.sweep_stale_devices(timeout_seconds=max(settings.WS_TIMEOUT, settings.WS_HEARTBEAT_INTERVAL * 3))
# 创建FastAPI应用
app = FastAPI(
title="工作手机SDK v3.0",
description="存客宝的AI手机控制引擎 - 支持微信/抖音/小红书等任意APP",
version="3.0.0",
lifespan=lifespan,
)
# CORS配置
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# 注册路由
app.include_router(devices.router, prefix="/api/v3", tags=["设备管理"])
app.include_router(unified.router, prefix="/api/v3", tags=["统一接口"])
app.include_router(agent.router, prefix="/api/v3", tags=["AI Agent"])
app.include_router(adb.router, tags=["ADB设备控制"])
app.include_router(experience.router, tags=["经验库"])
app.include_router(projects.router, prefix="/api/v3", tags=["项目管理"])
app.include_router(qrcode.router, prefix="/api/v3", tags=["二维码"])
app.include_router(voice.router, prefix="/api/v3", tags=["语音控制"])
app.include_router(capture.router, tags=["抓包"])
app.include_router(hook_modules.router, prefix="/api/v3", tags=["Hook模块管理"])
app.include_router(connection.router, prefix="/api/v3", tags=["连接协议"])
# ========== 健康检查 ==========
@app.get("/health")
async def health_check():
"""健康检查"""
from services.adb_device import adb_manager
adb_devices = adb_manager.scan_devices()
return {
"status": "healthy",
"version": "3.0.0",
"devices_online": len(ws_hub.connections),
"device_ids": list(ws_hub.connections.keys()),
"adb_devices": len(adb_devices),
"adb_serials": adb_devices
}
@app.get("/ready")
async def ready():
"""就绪探针(部署/负载均衡用):进程已启动且可接收流量"""
return {"ready": True, "version": "3.0.0"}
@app.get("/")
async def root():
"""根路由 → 控制面板"""
static_path = os.path.join(os.path.dirname(__file__), "static", "index.html")
if os.path.exists(static_path):
return FileResponse(static_path)
return {
"name": "工作手机SDK v3.0",
"description": "存客宝的AI手机控制引擎",
"docs": "/docs",
"health": "/health",
"ready": "/ready",
"voice_control": "/voice"
}
@app.get("/voice")
async def voice_control_page():
"""语音控制页面"""
static_path = os.path.join(os.path.dirname(__file__), "static", "voice_control.html")
return FileResponse(static_path)
# 挂载静态文件目录
static_dir = os.path.join(os.path.dirname(__file__), "static")
if os.path.exists(static_dir):
app.mount("/static", StaticFiles(directory=static_dir), name="static")
# ========== Agent 分发接口 ==========
# Agent 代码根目录
_AGENT_DIR = os.path.normpath(os.path.join(os.path.dirname(__file__), "..", "agent"))
_AGENT_DIST = os.path.join(_AGENT_DIR, "dist", "agent.tar.gz")
@app.get("/api/v3/agent/download")
async def download_agent():
"""
下载设备端 Agent 打包文件agent.tar.gz
设备端 install.sh 会调用此接口自动下载代码。
如果 dist/agent.tar.gz 不存在,自动运行 package.sh 打包。
"""
# 自动打包(如果 dist 不存在或过期)
if not os.path.exists(_AGENT_DIST):
package_sh = os.path.join(_AGENT_DIR, "package.sh")
if os.path.exists(package_sh):
try:
subprocess.run(["bash", package_sh], cwd=_AGENT_DIR, check=True, timeout=30)
logger.info("Agent 代码已自动打包")
except Exception as e:
logger.error(f"自动打包失败: {e}")
return {"error": "Agent 打包失败,请在服务器手动执行 agent/package.sh"}
if os.path.exists(_AGENT_DIST):
return FileResponse(
_AGENT_DIST,
media_type="application/gzip",
filename="agent.tar.gz",
)
return {"error": "agent.tar.gz 不存在"}
@app.get("/install.sh")
async def serve_install_script():
"""
提供 Termux 一键安装脚本
用法: curl -sL http://服务器IP:8899/install.sh | bash -s -- --server ws://服务器IP:8899/ws/device
"""
install_sh = os.path.join(_AGENT_DIR, "install.sh")
if os.path.exists(install_sh):
return FileResponse(install_sh, media_type="text/plain", filename="install.sh")
return Response(content="echo '❌ install.sh 不存在'\n", media_type="text/plain")
# ========== WebSocket 设备连接 ==========
@app.websocket("/ws/device/{device_id}")
async def device_websocket(websocket: WebSocket, device_id: str):
"""设备WebSocket连接入口"""
await ws_hub.connect(websocket, device_id)
try:
while True:
data = await websocket.receive_json()
await ws_hub.handle_message(device_id, data)
except WebSocketDisconnect:
await ws_hub.disconnect(device_id)
except Exception as e:
logger.error(f"WebSocket错误 [{device_id}]: {e}")
await ws_hub.disconnect(device_id)
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8899)