206 lines
6.6 KiB
Python
206 lines
6.6 KiB
Python
"""
|
||
工作手机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)
|