Browse Source

add remote control

welin 3 months ago
parent
commit
bf59f6c591

+ 171 - 1
app/api/router.py

@@ -2,8 +2,10 @@ import time
 import uuid
 import uuid
 import json
 import json
 import requests
 import requests
-from typing import List
+import stripe
+from typing import List, Dict, Any, Optional
 from app.core.logger import logger
 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 import APIRouter, Request, Response, Query, Depends, Body, UploadFile, File, HTTPException
 from fastapi.responses import RedirectResponse
 from fastapi.responses import RedirectResponse
 from sqlalchemy.orm import Session
 from sqlalchemy.orm import Session
@@ -49,6 +51,8 @@ from app.schemas.wechat import WechatIn
 from app.schemas.resource import FileUploadOut
 from app.schemas.resource import FileUploadOut
 from app.schemas.statistics import VasStatisticsOverviewOut
 from app.schemas.statistics import VasStatisticsOverviewOut
 from app.schemas.llm import ParseUserInputsPayload, ParseUserInputsOut
 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.configuration_service import ConfigurationService
 from app.services.troov_service import TroovService
 from app.services.troov_service import TroovService
 from app.services.sms_service import save_short_message, query_short_message
 from app.services.sms_service import save_short_message, query_short_message
@@ -91,6 +95,172 @@ admin_required_router = APIRouter()
 async def ping():
 async def ping():
     return {"message": "pong"}
     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])
 @admin_required_router.get("/sms/upload", summary="上报短信", tags=["短信接口"], response_model=ApiResponse[ShortMessageDetail])
 async def sms_upload(
 async def sms_upload(
     phone: str = Query(..., description="手机号"),
     phone: str = Query(..., description="手机号"),

+ 27 - 0
app/core/config.py

@@ -23,6 +23,33 @@ class Settings(BaseSettings):
     openai_api_key: str
     openai_api_key: str
     stripe_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(
     model_config = SettingsConfigDict(
         env_file=".env",
         env_file=".env",
         env_file_encoding="utf-8",
         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
 alembic
 APScheduler
 APScheduler
 python-dotenv
 python-dotenv
+paramiko

+ 4 - 1
starter.py

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