Przeglądaj źródła

add remote control

welin 3 miesięcy temu
rodzic
commit
bf59f6c591

+ 171 - 1
app/api/router.py

@@ -2,8 +2,10 @@ import time
 import uuid
 import json
 import requests
-from typing import List
+import stripe
+from typing import List, Dict, Any, Optional
 from app.core.logger import logger
+from app.core.config import settings
 from fastapi import APIRouter, Request, Response, Query, Depends, Body, UploadFile, File, HTTPException
 from fastapi.responses import RedirectResponse
 from sqlalchemy.orm import Session
@@ -49,6 +51,8 @@ from app.schemas.wechat import WechatIn
 from app.schemas.resource import FileUploadOut
 from app.schemas.statistics import VasStatisticsOverviewOut
 from app.schemas.llm import ParseUserInputsPayload, ParseUserInputsOut
+from app.schemas.docker_remote import RemoteServerConfig, DockerStatusOut, DockerLogsRequest, DockerLogsOut, ConfigReadOut, ConfigReadRequest, ConfigUpdateRequest, LogReadRequest, LogReadOut, LogListOut, DockerContainerStatus, DockerActionRequest, ServerConfigItem, ServerListOut, RemoteActionRequest
+from app.services.docker_remote_service import DockerRemoteService
 from app.services.configuration_service import ConfigurationService
 from app.services.troov_service import TroovService
 from app.services.sms_service import save_short_message, query_short_message
@@ -91,6 +95,172 @@ admin_required_router = APIRouter()
 async def ping():
     return {"message": "pong"}
 
+# -----------------------
+# Docker 远程控制 (预配置服务器)
+# -----------------------
+
+def get_server_config(server_id: str) -> RemoteServerConfig:
+    """从配置中获取服务器连接信息"""
+    if server_id not in settings.remote_servers:
+        raise HTTPException(status_code=404, detail=f"Server {server_id} not found in configuration")
+    
+    cfg = settings.remote_servers[server_id]
+    return RemoteServerConfig(**cfg)
+
+@admin_required_router.get("/remote/servers", summary="获取所有预配置服务器", tags=["Docker远程控制"], response_model=ApiResponse[ServerListOut])
+async def list_remote_servers():
+    servers = [
+        ServerConfigItem(id=sid, name=sid, host=info["host"])
+        for sid, info in settings.remote_servers.items()
+    ]
+    return success(data=ServerListOut(servers=servers))
+
+@admin_required_router.post("/remote/server/docker/status", summary="获取预配置服务器容器状态", tags=["Docker远程控制"], response_model=ApiResponse[DockerStatusOut])
+async def server_docker_status(request: RemoteActionRequest):
+    config = get_server_config(request.server_id)
+    res = await DockerRemoteService.get_container_status(config)
+    return success(data=DockerStatusOut(containers=res))
+
+@admin_required_router.post("/remote/server/docker/up", summary="启动预配置服务器Docker Compose", tags=["Docker远程控制"], response_model=ApiResponse[bool])
+async def server_docker_up(request: RemoteActionRequest):
+    config = get_server_config(request.server_id)
+    res = await DockerRemoteService.docker_compose_up(config, request.services)
+    return success(data=res)
+
+@admin_required_router.post("/remote/server/docker/down", summary="停止预配置服务器Docker Compose", tags=["Docker远程控制"], response_model=ApiResponse[bool])
+async def server_docker_down(request: RemoteActionRequest):
+    config = get_server_config(request.server_id)
+    res = await DockerRemoteService.docker_compose_down(config, request.services)
+    return success(data=res)
+
+@admin_required_router.post("/remote/server/docker/restart", summary="重启预配置服务器容器", tags=["Docker远程控制"], response_model=ApiResponse[bool])
+async def server_docker_restart(request: RemoteActionRequest):
+    config = get_server_config(request.server_id)
+    res = await DockerRemoteService.docker_restart(config, request.container_name)
+    return success(data=res)
+
+@admin_required_router.post("/remote/server/docker/start", summary="启动预配置服务器容器", tags=["Docker远程控制"], response_model=ApiResponse[bool])
+async def server_docker_start(request: RemoteActionRequest):
+    config = get_server_config(request.server_id)
+    res = await DockerRemoteService.docker_start(config, request.container_name)
+    return success(data=res)
+
+@admin_required_router.post("/remote/server/docker/stop", summary="停止预配置服务器容器", tags=["Docker远程控制"], response_model=ApiResponse[bool])
+async def server_docker_stop(request: RemoteActionRequest):
+    config = get_server_config(request.server_id)
+    res = await DockerRemoteService.docker_stop(config, request.container_name)
+    return success(data=res)
+
+@admin_required_router.post("/remote/server/docker/logs", summary="查看预配置服务器容器日志", tags=["Docker远程控制"], response_model=ApiResponse[DockerLogsOut])
+async def server_docker_logs(request: RemoteActionRequest):
+    config = get_server_config(request.server_id)
+    # 构造 DockerLogsRequest
+    log_req = DockerLogsRequest(
+        **config.model_dump(),
+        container_name=request.container_name,
+        lines=request.lines,
+        follow=request.follow
+    )
+    res = await DockerRemoteService.docker_logs(config, log_req)
+    return success(data=DockerLogsOut(logs=res))
+
+@admin_required_router.post("/remote/server/config/read", summary="读取预配置服务器配置文件", tags=["Docker远程控制"], response_model=ApiResponse[Dict[str, Any]])
+async def server_docker_config_read(request: RemoteActionRequest):
+    config = get_server_config(request.server_id)
+    res = await DockerRemoteService.read_config(config, request.config_file)
+    return success(data={"config": res})
+
+@admin_required_router.post("/remote/server/config/update", summary="更新预配置服务器配置文件", tags=["Docker远程控制"], response_model=ApiResponse[bool])
+async def server_docker_config_update(request: RemoteActionRequest):
+    config = get_server_config(request.server_id)
+    # 构造 ConfigUpdateRequest
+    update_req = ConfigUpdateRequest(
+        **config.model_dump(),
+        config_file=request.config_file,
+        key_path=request.key_path,
+        value=request.value
+    )
+    res = await DockerRemoteService.update_config(config, update_req)
+    return success(data=res)
+
+@admin_required_router.post("/remote/server/log/list", summary="列出预配置服务器日志文件", tags=["Docker远程控制"], response_model=ApiResponse[LogListOut])
+async def server_docker_logs_list(request: RemoteActionRequest):
+    config = get_server_config(request.server_id)
+    res = await DockerRemoteService.list_logs(config)
+    return success(data=LogListOut(log_files=res))
+
+@admin_required_router.post("/remote/server/log/read", summary="读取预配置服务器日志文件内容", tags=["Docker远程控制"], response_model=ApiResponse[LogReadOut])
+async def server_docker_logs_read(request: RemoteActionRequest):
+    config = get_server_config(request.server_id)
+    # 构造 LogReadRequest
+    read_req = LogReadRequest(
+        **config.model_dump(),
+        log_file=request.log_file,
+        lines=request.lines,
+        from_head=request.from_head,
+        full=request.full
+    )
+    res = await DockerRemoteService.read_log(config, read_req)
+    return success(data=LogReadOut(content=res))
+
+# -----------------------
+# Docker 远程控制 (直连模式 - 仅供调试)
+# -----------------------
+@admin_required_router.post("/remote/docker/status", summary="获取容器状态", tags=["Docker远程控制"], response_model=ApiResponse[DockerStatusOut])
+async def docker_status(config: RemoteServerConfig):
+    res = await DockerRemoteService.get_container_status(config)
+    return success(data=DockerStatusOut(containers=res))
+
+@admin_required_router.post("/remote/docker/up", summary="启动Docker Compose服务", tags=["Docker远程控制"], response_model=ApiResponse[bool])
+async def docker_up(config: RemoteServerConfig, services: Optional[List[str]] = None):
+    res = await DockerRemoteService.docker_compose_up(config, services)
+    return success(data=res)
+
+@admin_required_router.post("/remote/docker/down", summary="停止Docker Compose服务", tags=["Docker远程控制"], response_model=ApiResponse[bool])
+async def docker_down(config: RemoteServerConfig, services: Optional[List[str]] = None):
+    res = await DockerRemoteService.docker_compose_down(config, services)
+    return success(data=res)
+
+@admin_required_router.post("/remote/docker/restart", summary="重启容器", tags=["Docker远程控制"], response_model=ApiResponse[bool])
+async def docker_restart(request: DockerActionRequest):
+    res = await DockerRemoteService.docker_restart(request, request.container_name)
+    return success(data=res)
+
+@admin_required_router.post("/remote/docker/start", summary="启动容器", tags=["Docker远程控制"], response_model=ApiResponse[bool])
+async def docker_start(request: DockerActionRequest):
+    res = await DockerRemoteService.docker_start(request, request.container_name)
+    return success(data=res)
+
+@admin_required_router.post("/remote/docker/stop", summary="停止容器", tags=["Docker远程控制"], response_model=ApiResponse[bool])
+async def docker_stop(request: DockerActionRequest):
+    res = await DockerRemoteService.docker_stop(request, request.container_name)
+    return success(data=res)
+
+@admin_required_router.post("/remote/docker/logs", summary="查看容器日志", tags=["Docker远程控制"], response_model=ApiResponse[DockerLogsOut])
+async def docker_logs(request: DockerLogsRequest):
+    res = await DockerRemoteService.docker_logs(request, request)
+    return success(data=DockerLogsOut(logs=res))
+
+@admin_required_router.post("/remote/config/read", summary="读取配置文件", tags=["Docker远程控制"], response_model=ApiResponse[Dict[str, Any]])
+async def docker_config_read(request: ConfigReadRequest):
+    res = await DockerRemoteService.read_config(request, request.config_file)
+    return success(data={"config": res})
+
+@admin_required_router.post("/remote/config/update", summary="更新配置文件", tags=["Docker远程控制"], response_model=ApiResponse[bool])
+async def docker_config_update(request: ConfigUpdateRequest):
+    res = await DockerRemoteService.update_config(request, request)
+    return success(data=res)
+
+@admin_required_router.post("/remote/log/list", summary="列出日志文件", tags=["Docker远程控制"], response_model=ApiResponse[LogListOut])
+async def docker_logs_list(config: RemoteServerConfig):
+    res = await DockerRemoteService.list_logs(config)
+    return success(data=LogListOut(log_files=res))
+
+@admin_required_router.post("/remote/log/read", summary="读取日志文件内容", tags=["Docker远程控制"], response_model=ApiResponse[LogReadOut])
+async def docker_logs_read(request: LogReadRequest):
+    res = await DockerRemoteService.read_log(request, request)
+    return success(data=LogReadOut(content=res))
+
 @admin_required_router.get("/sms/upload", summary="上报短信", tags=["短信接口"], response_model=ApiResponse[ShortMessageDetail])
 async def sms_upload(
     phone: str = Query(..., description="手机号"),

+ 27 - 0
app/core/config.py

@@ -23,6 +23,33 @@ class Settings(BaseSettings):
     openai_api_key: str
     stripe_api_key: str
 
+    # -----------------------
+    # Remote Servers
+    # -----------------------
+    remote_servers: dict = {
+        "MCP1": {
+            "host": "45.137.220.138",
+            "port": 22,
+            "username": "root",
+            "password": "s3UqbkWxW72",
+            "project_path": "/root/troov-asyncio"
+        },
+        "MCP2": {
+            "host": "185.148.147.103",
+            "port": 22,
+            "username": "root",
+            "password": "nBEqFzWe7z7pbprypmUt",
+            "project_path": "/root/troov-asyncio"
+        },
+        "MCP3": {
+            "host": "185.148.147.119",
+            "port": 22,
+            "username": "root",
+            "password": "5hcm07IAnBAv87Ey",
+            "project_path": "/root/troov-asyncio"
+        }
+    }
+
     model_config = SettingsConfigDict(
         env_file=".env",
         env_file_encoding="utf-8",

+ 103 - 0
app/schemas/docker_remote.py

@@ -0,0 +1,103 @@
+# app/schemas/docker_remote.py
+from typing import Optional, Dict, Any, List
+from pydantic import BaseModel, Field
+
+
+class RemoteServerConfig(BaseModel):
+    """远程服务器连接配置"""
+    host: str = Field(..., description="服务器地址")
+    port: int = Field(22, description="SSH端口")
+    username: str = Field("root", description="SSH用户名")
+    password: Optional[str] = Field(None, description="SSH密码")
+    key_file: Optional[str] = Field(None, description="SSH私钥文件路径")
+    project_path: str = Field("/root/troov-asyncio", description="项目路径")
+
+
+class DockerContainerStatus(BaseModel):
+    """Docker容器状态"""
+    name: str
+    status: str
+    image: str
+
+
+class DockerStatusOut(BaseModel):
+    """Docker状态输出"""
+    containers: Dict[str, DockerContainerStatus]
+
+
+class DockerActionRequest(RemoteServerConfig):
+    """Docker容器操作请求"""
+    container_name: str
+
+
+class DockerLogsRequest(RemoteServerConfig):
+    """查看Docker日志请求"""
+    container_name: str
+    lines: int = Field(100, description="显示行数")
+    follow: bool = Field(False, description="是否实时跟踪")
+
+
+class DockerLogsOut(BaseModel):
+    """Docker日志输出"""
+    logs: str
+
+
+class ConfigReadOut(BaseModel):
+    """配置文件读取输出"""
+    config: Dict[str, Any]
+
+
+class ConfigReadRequest(RemoteServerConfig):
+    """配置文件读取请求"""
+    config_file: str = Field(..., description="配置文件路径(相对于项目根目录)")
+
+
+class ConfigUpdateRequest(RemoteServerConfig):
+    """配置文件更新请求"""
+    config_file: str = Field(..., description="配置文件路径")
+    key_path: str = Field(..., description="键路径,如 lockV1.sessionLimit")
+    value: Any = Field(..., description="新值")
+
+
+class LogReadRequest(RemoteServerConfig):
+    """日志读取请求"""
+    log_file: str = Field(..., description="日志文件名或路径")
+    lines: int = Field(100, description="行数")
+    from_head: bool = Field(False, description="从开头读取")
+    full: bool = Field(False, description="读取整个文件")
+
+
+class LogReadOut(BaseModel):
+    """日志读取输出"""
+    content: str
+
+
+class LogListOut(BaseModel):
+    """日志列表输出"""
+    log_files: List[str]
+
+
+class ServerConfigItem(BaseModel):
+    """预配置服务器项"""
+    id: str
+    name: str
+    host: str
+
+
+class ServerListOut(BaseModel):
+    """预配置服务器列表输出"""
+    servers: List[ServerConfigItem]
+
+
+class RemoteActionRequest(BaseModel):
+    """基于服务器ID的远程操作请求"""
+    server_id: str
+    container_name: Optional[str] = None
+    services: Optional[List[str]] = None
+    config_file: Optional[str] = None
+    key_path: Optional[str] = None
+    value: Any = None
+    log_file: Optional[str] = None
+    lines: int = 100
+    from_head: bool = False
+    full: bool = False

+ 606 - 0
app/services/docker_remote_control.py

@@ -0,0 +1,606 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+Docker远程控制脚本
+功能:
+1. 远程控制Docker容器的启动、停止、重启
+2. 修改配置文件
+3. 查看日志文件
+"""
+
+import json
+import os
+import sys
+import argparse
+from pathlib import Path
+from typing import Optional, Dict, Any, List
+import paramiko
+from io import StringIO
+
+
+class DockerRemoteController:
+    """Docker远程控制器"""
+    
+    def __init__(self, host: str, port: int = 22, username: str = "root", 
+                 password: Optional[str] = None, key_file: Optional[str] = None,
+                 project_path: str = "/root/troov-asyncio"):
+        """
+        初始化远程控制器
+        
+        Args:
+            host: 远程服务器地址
+            port: SSH端口
+            username: SSH用户名
+            password: SSH密码
+            key_file: SSH私钥文件路径
+            project_path: 项目在远程服务器上的路径
+        """
+        self.host = host
+        self.port = port
+        self.username = username
+        self.password = password
+        self.key_file = key_file
+        self.project_path = project_path
+        self.ssh_client = None
+        self.sftp_client = None
+        
+    def connect(self):
+        """建立SSH连接"""
+        try:
+            self.ssh_client = paramiko.SSHClient()
+            self.ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
+            
+            if self.key_file:
+                # 使用密钥文件连接
+                self.ssh_client.connect(
+                    hostname=self.host,
+                    port=self.port,
+                    username=self.username,
+                    key_filename=self.key_file
+                )
+            else:
+                # 使用密码连接
+                self.ssh_client.connect(
+                    hostname=self.host,
+                    port=self.port,
+                    username=self.username,
+                    password=self.password
+                )
+            
+            # 创建SFTP客户端用于文件传输
+            self.sftp_client = self.ssh_client.open_sftp()
+            print(f"✓ 成功连接到 {self.host}:{self.port}")
+            return True
+        except Exception as e:
+            print(f"✗ 连接失败: {e}")
+            return False
+    
+    def disconnect(self):
+        """关闭连接"""
+        if self.sftp_client:
+            self.sftp_client.close()
+        if self.ssh_client:
+            self.ssh_client.close()
+    
+    def execute_command(self, command: str) -> tuple[str, str, int]:
+        """
+        执行远程命令
+        
+        Returns:
+            (stdout, stderr, exit_code)
+        """
+        if not self.ssh_client:
+            raise Exception("未建立SSH连接,请先调用connect()")
+        
+        stdin, stdout, stderr = self.ssh_client.exec_command(command)
+        exit_code = stdout.channel.recv_exit_status()
+        stdout_text = stdout.read().decode('utf-8')
+        stderr_text = stderr.read().decode('utf-8')
+        return stdout_text, stderr_text, exit_code
+    
+    # ========== Docker控制方法 ==========
+    
+    def docker_compose_up(self, services: Optional[List[str]] = None) -> bool:
+        """启动Docker Compose服务"""
+        cmd = f"cd {self.project_path} && docker-compose up -d"
+        if services:
+            cmd += " " + " ".join(services)
+        
+        print(f"正在启动Docker服务...")
+        stdout, stderr, exit_code = self.execute_command(cmd)
+        
+        if exit_code == 0:
+            print("✓ Docker服务启动成功")
+            if stdout:
+                print(stdout)
+            return True
+        else:
+            print(f"✗ Docker服务启动失败: {stderr}")
+            return False
+    
+    def docker_compose_down(self, services: Optional[List[str]] = None) -> bool:
+        """停止Docker Compose服务"""
+        cmd = f"cd {self.project_path} && docker-compose down"
+        if services:
+            # docker-compose down不支持指定服务,需要单独停止
+            for service in services:
+                self.docker_stop(service)
+            return True
+        
+        print(f"正在停止Docker服务...")
+        stdout, stderr, exit_code = self.execute_command(cmd)
+        
+        if exit_code == 0:
+            print("✓ Docker服务停止成功")
+            if stdout:
+                print(stdout)
+            return True
+        else:
+            print(f"✗ Docker服务停止失败: {stderr}")
+            return False
+    
+    def docker_restart(self, container_name: str) -> bool:
+        """重启指定容器"""
+        print(f"正在重启容器: {container_name}")
+        stdout, stderr, exit_code = self.execute_command(
+            f"docker restart {container_name}"
+        )
+        
+        if exit_code == 0:
+            print(f"✓ 容器 {container_name} 重启成功")
+            if stdout:
+                print(stdout)
+            return True
+        else:
+            print(f"✗ 容器 {container_name} 重启失败: {stderr}")
+            return False
+    
+    def docker_start(self, container_name: str) -> bool:
+        """启动指定容器"""
+        print(f"正在启动容器: {container_name}")
+        stdout, stderr, exit_code = self.execute_command(
+            f"docker start {container_name}"
+        )
+        
+        if exit_code == 0:
+            print(f"✓ 容器 {container_name} 启动成功")
+            if stdout:
+                print(stdout)
+            return True
+        else:
+            print(f"✗ 容器 {container_name} 启动失败: {stderr}")
+            return False
+    
+    def docker_stop(self, container_name: str) -> bool:
+        """停止指定容器"""
+        print(f"正在停止容器: {container_name}")
+        stdout, stderr, exit_code = self.execute_command(
+            f"docker stop {container_name}"
+        )
+        
+        if exit_code == 0:
+            print(f"✓ 容器 {container_name} 停止成功")
+            if stdout:
+                print(stdout)
+            return True
+        else:
+            print(f"✗ 容器 {container_name} 停止失败: {stderr}")
+            return False
+    
+    def docker_status(self) -> Dict[str, Any]:
+        """查看所有容器状态"""
+        stdout, stderr, exit_code = self.execute_command(
+            "docker ps -a --format '{{.Names}}\t{{.Status}}\t{{.Image}}'"
+        )
+        
+        if exit_code == 0:
+            containers = {}
+            for line in stdout.strip().split('\n'):
+                if line.strip():
+                    parts = line.split('\t')
+                    if len(parts) >= 2:
+                        name = parts[0]
+                        status = parts[1]
+                        image = parts[2] if len(parts) > 2 else ""
+                        containers[name] = {
+                            'status': status,
+                            'image': image
+                        }
+            return containers
+        else:
+            print(f"✗ 获取容器状态失败: {stderr}")
+            return {}
+    
+    def docker_logs(self, container_name: str, lines: int = 100, follow: bool = False) -> str:
+        """查看容器日志"""
+        cmd = f"docker logs --tail {lines} {container_name}"
+        if follow:
+            cmd += " -f"
+        
+        stdout, stderr, exit_code = self.execute_command(cmd)
+        
+        if exit_code == 0:
+            return stdout
+        else:
+            return f"获取日志失败: {stderr}"
+    
+    # ========== 配置文件操作方法 ==========
+    
+    def read_config(self, config_file: str) -> Optional[Dict[str, Any]]:
+        """读取远程配置文件"""
+        remote_path = f"{self.project_path}/{config_file}"
+        
+        try:
+            # 读取文件内容
+            with self.sftp_client.open(remote_path, 'r') as f:
+                content = f.read().decode('utf-8')
+                return json.loads(content)
+        except FileNotFoundError:
+            print(f"✗ 配置文件不存在: {remote_path}")
+            return None
+        except json.JSONDecodeError as e:
+            print(f"✗ JSON解析失败: {e}")
+            return None
+        except Exception as e:
+            print(f"✗ 读取配置文件失败: {e}")
+            return None
+    
+    def write_config(self, config_file: str, config_data: Dict[str, Any]) -> bool:
+        """写入远程配置文件"""
+        remote_path = f"{self.project_path}/{config_file}"
+        
+        try:
+            # 将配置转换为JSON字符串
+            json_str = json.dumps(config_data, indent=2, ensure_ascii=False)
+            
+            # 写入文件
+            with self.sftp_client.open(remote_path, 'w') as f:
+                f.write(json_str.encode('utf-8'))
+            
+            print(f"✓ 配置文件已更新: {config_file}")
+            return True
+        except Exception as e:
+            print(f"✗ 写入配置文件失败: {e}")
+            return False
+    
+    def update_config(self, config_file: str, key_path: str, value: Any) -> bool:
+        """
+        更新配置文件中的指定键值
+        
+        Args:
+            config_file: 配置文件路径(相对于项目根目录)
+            key_path: 键路径,支持嵌套,如 "lockV1.sessionLimit" 或 "releaseTimePoint.hour"
+            value: 新值
+        """
+        config = self.read_config(config_file)
+        if config is None:
+            return False
+        
+        # 解析键路径
+        keys = key_path.split('.')
+        current = config
+        
+        # 导航到目标位置
+        for key in keys[:-1]:
+            if key not in current:
+                current[key] = {}
+            current = current[key]
+        
+        # 设置值
+        old_value = current.get(keys[-1])
+        current[keys[-1]] = value
+        
+        print(f"配置更新: {key_path}")
+        print(f"  旧值: {old_value}")
+        print(f"  新值: {value}")
+        
+        return self.write_config(config_file, config)
+    
+    # ========== 日志文件查看方法 ==========
+    
+    def view_log(self, log_file: str, lines: int = 100, follow: bool = False) -> str:
+        """
+        查看日志文件
+        
+        Args:
+            log_file: 日志文件路径(相对于项目根目录或绝对路径)
+            lines: 显示的行数
+            follow: 是否实时跟踪(注意:follow模式在SSH中可能不稳定)
+        """
+        # 如果是相对路径,添加项目路径前缀
+        if not log_file.startswith('/'):
+            remote_path = f"{self.project_path}/{log_file}"
+        else:
+            remote_path = log_file
+        
+        if follow:
+            # follow模式需要特殊处理
+            cmd = f"tail -f {remote_path}"
+        else:
+            cmd = f"tail -n {lines} {remote_path}"
+        
+        stdout, stderr, exit_code = self.execute_command(cmd)
+        
+        if exit_code == 0:
+            return stdout
+        else:
+            return f"查看日志失败: {stderr}"
+    
+    def read_log(
+        self,
+        log_file: str,
+        lines: int = 100,
+        from_head: bool = False,
+        full: bool = False,
+        output_path: Optional[str] = None,
+    ) -> str:
+        """
+        远程读取 logs 目录下指定的日志文件。
+        若 log_file 仅为文件名(如 slot_monitor.log),自动视为 logs/ 下文件。
+        
+        Args:
+            log_file: 日志文件名或路径。纯文件名则自动加 logs/ 前缀。
+            lines: 读取行数(默认 100)。full 时忽略。
+            from_head: True 则读开头 N 行,否则读末尾 N 行。
+            full: True 则读取整个文件。
+            output_path: 若指定,将读取内容保存到本地该路径。
+        
+        Returns:
+            读取到的日志内容;失败时返回错误信息。
+        """
+        # 纯文件名(无路径)视为 logs 下
+        if not log_file.startswith('/') and '/' not in log_file:
+            remote_path = f"{self.project_path}/logs/{log_file}"
+        elif not log_file.startswith('/'):
+            remote_path = f"{self.project_path}/{log_file}"
+        else:
+            remote_path = log_file
+        
+        if full:
+            cmd = f"cat {remote_path}"
+        elif from_head:
+            cmd = f"head -n {lines} {remote_path}"
+        else:
+            cmd = f"tail -n {lines} {remote_path}"
+        
+        stdout, stderr, exit_code = self.execute_command(cmd)
+        
+        if exit_code != 0:
+            return f"读取日志失败: {stderr}"
+        
+        if output_path:
+            try:
+                path = Path(output_path)
+                path.parent.mkdir(parents=True, exist_ok=True)
+                path.write_text(stdout, encoding='utf-8')
+                print(f"✓ 已保存到本地: {output_path}")
+            except Exception as e:
+                return f"保存到本地失败: {e}\n\n{stdout}"
+        
+        return stdout
+    
+    def list_logs(self) -> List[str]:
+        """列出所有日志文件"""
+        logs_dir = f"{self.project_path}/logs"
+        stdout, stderr, exit_code = self.execute_command(
+            f"ls -1 {logs_dir}/*.log 2>/dev/null || echo ''"
+        )
+        
+        if exit_code == 0 and stdout.strip():
+            log_files = [os.path.basename(f) for f in stdout.strip().split('\n') if f.strip()]
+            return log_files
+        return []
+
+
+def main():
+    parser = argparse.ArgumentParser(
+        description='Docker远程控制脚本',
+        formatter_class=argparse.RawDescriptionHelpFormatter,
+        epilog="""
+使用示例:
+  # 连接并查看容器状态
+  python docker_remote_control.py --host 192.168.1.100 --password yourpass status
+
+  # 重启容器
+  python docker_remote_control.py --host 192.168.1.100 --password yourpass restart troov_monitor
+
+  # 启动所有服务
+  python docker_remote_control.py --host 192.168.1.100 --password yourpass up
+
+  # 查看日志
+  python docker_remote_control.py --host 192.168.1.100 --password yourpass logs troov_monitor --lines 50
+
+  # 读取配置文件
+  python docker_remote_control.py --host 192.168.1.100 --password yourpass read-config config/troov_config.json
+
+  # 更新配置文件
+  python docker_remote_control.py --host 192.168.1.100 --password yourpass update-config config/troov_config.json lockV1.sessionLimit 10
+
+  # 查看日志文件
+  python docker_remote_control.py --host 192.168.1.100 --password yourpass view-log logs/slot_monitor.log --lines 100
+
+  # 远程读取 logs 下指定日志(仅文件名自动加 logs/ 前缀)
+  python docker_remote_control.py --host 192.168.1.100 --password yourpass read-log slot_monitor.log --lines 200
+  python docker_remote_control.py --host 192.168.1.100 --password yourpass read-log slot_monitor.log --full --output ./downloaded.log
+  python docker_remote_control.py --host 192.168.1.100 --password yourpass read-log session_creator.log --head --lines 50
+        """
+    )
+    
+    # 连接参数
+    parser.add_argument('--host', required=True, help='远程服务器地址')
+    parser.add_argument('--port', type=int, default=22, help='SSH端口 (默认: 22)')
+    parser.add_argument('--username', default='root', help='SSH用户名 (默认: root)')
+    parser.add_argument('--password', help='SSH密码')
+    parser.add_argument('--key-file', help='SSH私钥文件路径')
+    parser.add_argument('--project-path', default='/root/troov-asyncio', 
+                       help='项目在远程服务器上的路径 (默认: /root/troov-asyncio)')
+    
+    # 子命令
+    subparsers = parser.add_subparsers(dest='command', help='可用命令')
+    
+    # status命令
+    subparsers.add_parser('status', help='查看所有容器状态')
+    
+    # docker控制命令
+    up_parser = subparsers.add_parser('up', help='启动Docker Compose服务')
+    up_parser.add_argument('services', nargs='*', help='要启动的服务名称(可选)')
+    
+    down_parser = subparsers.add_parser('down', help='停止Docker Compose服务')
+    down_parser.add_argument('services', nargs='*', help='要停止的服务名称(可选)')
+    
+    restart_parser = subparsers.add_parser('restart', help='重启容器')
+    restart_parser.add_argument('container', help='容器名称')
+    
+    start_parser = subparsers.add_parser('start', help='启动容器')
+    start_parser.add_argument('container', help='容器名称')
+    
+    stop_parser = subparsers.add_parser('stop', help='停止容器')
+    stop_parser.add_argument('container', help='容器名称')
+    
+    logs_parser = subparsers.add_parser('logs', help='查看容器日志')
+    logs_parser.add_argument('container', help='容器名称')
+    logs_parser.add_argument('--lines', type=int, default=100, help='显示行数 (默认: 100)')
+    logs_parser.add_argument('--follow', action='store_true', help='实时跟踪日志')
+    
+    # 配置文件命令
+    read_config_parser = subparsers.add_parser('read-config', help='读取配置文件')
+    read_config_parser.add_argument('config_file', help='配置文件路径')
+    
+    update_config_parser = subparsers.add_parser('update-config', help='更新配置文件')
+    update_config_parser.add_argument('config_file', help='配置文件路径')
+    update_config_parser.add_argument('key_path', help='键路径,如 lockV1.sessionLimit')
+    update_config_parser.add_argument('value', help='新值')
+    
+    # 日志文件命令
+    view_log_parser = subparsers.add_parser('view-log', help='查看日志文件')
+    view_log_parser.add_argument('log_file', help='日志文件路径')
+    view_log_parser.add_argument('--lines', type=int, default=100, help='显示行数 (默认: 100)')
+    view_log_parser.add_argument('--follow', action='store_true', help='实时跟踪日志')
+    
+    read_log_parser = subparsers.add_parser(
+        'read-log',
+        help='远程读取 logs 下指定日志文件;仅文件名时自动加 logs/ 前缀',
+    )
+    read_log_parser.add_argument(
+        'log_file',
+        help='日志文件名(如 slot_monitor.log)或路径(如 logs/slot_monitor.log)',
+    )
+    read_log_parser.add_argument('--lines', type=int, default=100, help='行数 (默认: 100)')
+    read_log_parser.add_argument(
+        '--head',
+        action='store_true',
+        help='读开头 N 行;默认读末尾 N 行',
+    )
+    read_log_parser.add_argument('--full', action='store_true', help='读取整个文件')
+    read_log_parser.add_argument(
+        '--output',
+        dest='output_path',
+        metavar='PATH',
+        help='将读取内容保存到本地文件',
+    )
+    
+    list_logs_parser = subparsers.add_parser('list-logs', help='列出所有日志文件')
+    
+    args = parser.parse_args()
+    
+    if not args.command:
+        parser.print_help()
+        sys.exit(1)
+    
+    # 检查认证方式
+    if not args.password and not args.key_file:
+        print("错误: 必须提供 --password 或 --key-file")
+        sys.exit(1)
+    
+    # 创建控制器并连接
+    controller = DockerRemoteController(
+        host=args.host,
+        port=args.port,
+        username=args.username,
+        password=args.password,
+        key_file=args.key_file,
+        project_path=args.project_path
+    )
+    
+    if not controller.connect():
+        sys.exit(1)
+    
+    try:
+        # 执行命令
+        if args.command == 'status':
+            containers = controller.docker_status()
+            print("\n容器状态:")
+            print("-" * 80)
+            for name, info in containers.items():
+                print(f"{name:30} {info['status']:30} {info['image']}")
+        
+        elif args.command == 'up':
+            controller.docker_compose_up(args.services if args.services else None)
+        
+        elif args.command == 'down':
+            controller.docker_compose_down(args.services if args.services else None)
+        
+        elif args.command == 'restart':
+            controller.docker_restart(args.container)
+        
+        elif args.command == 'start':
+            controller.docker_start(args.container)
+        
+        elif args.command == 'stop':
+            controller.docker_stop(args.container)
+        
+        elif args.command == 'logs':
+            output = controller.docker_logs(args.container, args.lines, args.follow)
+            print(output)
+        
+        elif args.command == 'read-config':
+            config = controller.read_config(args.config_file)
+            if config:
+                print(json.dumps(config, indent=2, ensure_ascii=False))
+        
+        elif args.command == 'update-config':
+            # 尝试将值转换为合适的类型
+            value = args.value
+            try:
+                # 尝试转换为数字
+                if value.isdigit() or (value.startswith('-') and value[1:].isdigit()):
+                    value = int(value)
+                elif '.' in value and value.replace('.', '').replace('-', '').isdigit():
+                    value = float(value)
+                # 尝试转换为布尔值
+                elif value.lower() in ('true', 'false'):
+                    value = value.lower() == 'true'
+                # 尝试转换为JSON
+                elif value.startswith('{') or value.startswith('['):
+                    value = json.loads(value)
+            except:
+                pass  # 保持为字符串
+            
+            controller.update_config(args.config_file, args.key_path, value)
+        
+        elif args.command == 'view-log':
+            output = controller.view_log(args.log_file, args.lines, args.follow)
+            print(output)
+        
+        elif args.command == 'read-log':
+            output = controller.read_log(
+                args.log_file,
+                lines=args.lines,
+                from_head=args.head,
+                full=args.full,
+                output_path=args.output_path,
+            )
+            print(output)
+        
+        elif args.command == 'list-logs':
+            log_files = controller.list_logs()
+            print("日志文件列表:")
+            for log_file in log_files:
+                print(f"  - {log_file}")
+    
+    finally:
+        controller.disconnect()
+
+
+if __name__ == '__main__':
+    main()

+ 275 - 0
app/services/docker_remote_service.py

@@ -0,0 +1,275 @@
+# app/services/docker_remote_service.py
+import json
+from typing import Optional, Dict, Any, List
+from app.services.docker_remote_control import DockerRemoteController
+from app.schemas.docker_remote import (
+    RemoteServerConfig,
+    DockerContainerStatus,
+    DockerLogsRequest,
+    ConfigUpdateRequest,
+    LogReadRequest,
+)
+from app.core.logger import logger
+
+
+class DockerRemoteService:
+    """Docker远程控制服务"""
+    
+    @staticmethod
+    async def get_container_status(config: RemoteServerConfig) -> Dict[str, DockerContainerStatus]:
+        """获取容器状态"""
+        controller = DockerRemoteController(
+            host=config.host,
+            port=config.port,
+            username=config.username,
+            password=config.password,
+            key_file=config.key_file,
+            project_path=config.project_path
+        )
+        
+        try:
+            if not controller.connect():
+                raise Exception("连接失败")
+            
+            containers = controller.docker_status()
+            result = {}
+            for name, info in containers.items():
+                result[name] = DockerContainerStatus(
+                    name=name,
+                    status=info['status'],
+                    image=info['image']
+                )
+            return result
+        except Exception as e:
+            logger.error(f"获取容器状态失败: {e}")
+            raise
+        finally:
+            controller.disconnect()
+    
+    @staticmethod
+    async def docker_compose_up(config: RemoteServerConfig, services: Optional[List[str]] = None) -> bool:
+        """启动Docker Compose服务"""
+        controller = DockerRemoteController(
+            host=config.host,
+            port=config.port,
+            username=config.username,
+            password=config.password,
+            key_file=config.key_file,
+            project_path=config.project_path
+        )
+        
+        try:
+            if not controller.connect():
+                raise Exception("连接失败")
+            return controller.docker_compose_up(services)
+        except Exception as e:
+            logger.error(f"启动Docker服务失败: {e}")
+            raise
+        finally:
+            controller.disconnect()
+    
+    @staticmethod
+    async def docker_compose_down(config: RemoteServerConfig, services: Optional[List[str]] = None) -> bool:
+        """停止Docker Compose服务"""
+        controller = DockerRemoteController(
+            host=config.host,
+            port=config.port,
+            username=config.username,
+            password=config.password,
+            key_file=config.key_file,
+            project_path=config.project_path
+        )
+        
+        try:
+            if not controller.connect():
+                raise Exception("连接失败")
+            return controller.docker_compose_down(services)
+        except Exception as e:
+            logger.error(f"停止Docker服务失败: {e}")
+            raise
+        finally:
+            controller.disconnect()
+    
+    @staticmethod
+    async def docker_restart(config: RemoteServerConfig, container_name: str) -> bool:
+        """重启容器"""
+        controller = DockerRemoteController(
+            host=config.host,
+            port=config.port,
+            username=config.username,
+            password=config.password,
+            key_file=config.key_file,
+            project_path=config.project_path
+        )
+        
+        try:
+            if not controller.connect():
+                raise Exception("连接失败")
+            return controller.docker_restart(container_name)
+        except Exception as e:
+            logger.error(f"重启容器失败: {e}")
+            raise
+        finally:
+            controller.disconnect()
+    
+    @staticmethod
+    async def docker_start(config: RemoteServerConfig, container_name: str) -> bool:
+        """启动容器"""
+        controller = DockerRemoteController(
+            host=config.host,
+            port=config.port,
+            username=config.username,
+            password=config.password,
+            key_file=config.key_file,
+            project_path=config.project_path
+        )
+        
+        try:
+            if not controller.connect():
+                raise Exception("连接失败")
+            return controller.docker_start(container_name)
+        except Exception as e:
+            logger.error(f"启动容器失败: {e}")
+            raise
+        finally:
+            controller.disconnect()
+    
+    @staticmethod
+    async def docker_stop(config: RemoteServerConfig, container_name: str) -> bool:
+        """停止容器"""
+        controller = DockerRemoteController(
+            host=config.host,
+            port=config.port,
+            username=config.username,
+            password=config.password,
+            key_file=config.key_file,
+            project_path=config.project_path
+        )
+        
+        try:
+            if not controller.connect():
+                raise Exception("连接失败")
+            return controller.docker_stop(container_name)
+        except Exception as e:
+            logger.error(f"停止容器失败: {e}")
+            raise
+        finally:
+            controller.disconnect()
+    
+    @staticmethod
+    async def docker_logs(config: RemoteServerConfig, request: DockerLogsRequest) -> str:
+        """查看容器日志"""
+        controller = DockerRemoteController(
+            host=config.host,
+            port=config.port,
+            username=config.username,
+            password=config.password,
+            key_file=config.key_file,
+            project_path=config.project_path
+        )
+        
+        try:
+            if not controller.connect():
+                raise Exception("连接失败")
+            return controller.docker_logs(request.container_name, request.lines, request.follow)
+        except Exception as e:
+            logger.error(f"查看容器日志失败: {e}")
+            raise
+        finally:
+            controller.disconnect()
+    
+    @staticmethod
+    async def read_config(config: RemoteServerConfig, config_file: str) -> Dict[str, Any]:
+        """读取配置文件"""
+        controller = DockerRemoteController(
+            host=config.host,
+            port=config.port,
+            username=config.username,
+            password=config.password,
+            key_file=config.key_file,
+            project_path=config.project_path
+        )
+        
+        try:
+            if not controller.connect():
+                raise Exception("连接失败")
+            result = controller.read_config(config_file)
+            if result is None:
+                raise Exception("配置文件不存在或读取失败")
+            return result
+        except Exception as e:
+            logger.error(f"读取配置文件失败: {e}")
+            raise
+        finally:
+            controller.disconnect()
+    
+    @staticmethod
+    async def update_config(config: RemoteServerConfig, request: ConfigUpdateRequest) -> bool:
+        """更新配置文件"""
+        controller = DockerRemoteController(
+            host=config.host,
+            port=config.port,
+            username=config.username,
+            password=config.password,
+            key_file=config.key_file,
+            project_path=config.project_path
+        )
+        
+        try:
+            if not controller.connect():
+                raise Exception("连接失败")
+            return controller.update_config(request.config_file, request.key_path, request.value)
+        except Exception as e:
+            logger.error(f"更新配置文件失败: {e}")
+            raise
+        finally:
+            controller.disconnect()
+    
+    @staticmethod
+    async def read_log(config: RemoteServerConfig, request: LogReadRequest) -> str:
+        """读取日志文件"""
+        controller = DockerRemoteController(
+            host=config.host,
+            port=config.port,
+            username=config.username,
+            password=config.password,
+            key_file=config.key_file,
+            project_path=config.project_path
+        )
+        
+        try:
+            if not controller.connect():
+                raise Exception("连接失败")
+            return controller.read_log(
+                request.log_file,
+                lines=request.lines,
+                from_head=request.from_head,
+                full=request.full
+            )
+        except Exception as e:
+            logger.error(f"读取日志文件失败: {e}")
+            raise
+        finally:
+            controller.disconnect()
+    
+    @staticmethod
+    async def list_logs(config: RemoteServerConfig) -> List[str]:
+        """列出所有日志文件"""
+        controller = DockerRemoteController(
+            host=config.host,
+            port=config.port,
+            username=config.username,
+            password=config.password,
+            key_file=config.key_file,
+            project_path=config.project_path
+        )
+        
+        try:
+            if not controller.connect():
+                raise Exception("连接失败")
+            return controller.list_logs()
+        except Exception as e:
+            logger.error(f"列出日志文件失败: {e}")
+            raise
+        finally:
+            controller.disconnect()

+ 1 - 0
requirements.txt

@@ -19,3 +19,4 @@ httpx
 alembic
 APScheduler
 python-dotenv
+paramiko

+ 4 - 1
starter.py

@@ -1,5 +1,6 @@
 #!/usr/bin/env python3
 import os
+import sys
 import subprocess
 
 
@@ -23,8 +24,10 @@ def main():
     port = "8888"
     app_module = "app.main:app"
 
+    # 使用 python -m uvicorn 确保在所有平台(特别是 Windows)上都能正常工作
     base_cmd = [
-        "uvicorn",
+        sys.executable,  # 使用当前 Python 解释器
+        "-m", "uvicorn",
         app_module,
         "--host", host,
         "--port", port,