|
|
@@ -0,0 +1,396 @@
|
|
|
+'use client';
|
|
|
+
|
|
|
+import { useState, useEffect } from 'react';
|
|
|
+import api from '@/lib/api';
|
|
|
+import { Server, Play, Square, RotateCw, FileText, Settings, Search, RefreshCw } from 'lucide-react';
|
|
|
+import DockerControl from './DockerControl';
|
|
|
+import LogViewer from './LogViewer';
|
|
|
+import ConfigManager from './ConfigManager';
|
|
|
+
|
|
|
+interface ServerConfig {
|
|
|
+ host: string;
|
|
|
+ port: number;
|
|
|
+ username: string;
|
|
|
+ password?: string;
|
|
|
+ key_file?: string;
|
|
|
+ project_path: string;
|
|
|
+}
|
|
|
+
|
|
|
+interface PreConfiguredServer {
|
|
|
+ id: string;
|
|
|
+ name: string;
|
|
|
+ host: string;
|
|
|
+}
|
|
|
+
|
|
|
+export default function RemoteServerControl() {
|
|
|
+ const [activeTab, setActiveTab] = useState<'docker' | 'logs' | 'config'>('docker');
|
|
|
+ const [usePreConfigured, setUsePreConfigured] = useState(true);
|
|
|
+ const [preConfiguredServers, setPreConfiguredServers] = useState<PreConfiguredServer[]>([]);
|
|
|
+ const [selectedServerId, setSelectedServerId] = useState<string>('');
|
|
|
+ const [serverConfig, setServerConfig] = useState<ServerConfig>({
|
|
|
+ host: '',
|
|
|
+ port: 22,
|
|
|
+ username: 'root',
|
|
|
+ password: '',
|
|
|
+ key_file: '',
|
|
|
+ project_path: '/root/troov-asyncio',
|
|
|
+ });
|
|
|
+ const [isConnected, setIsConnected] = useState(false);
|
|
|
+ const [connecting, setConnecting] = useState(false);
|
|
|
+ const [connectionError, setConnectionError] = useState<string | null>(null);
|
|
|
+
|
|
|
+ // 获取预配置服务器列表
|
|
|
+ useEffect(() => {
|
|
|
+ const fetchServers = async () => {
|
|
|
+ try {
|
|
|
+ const response = await api.get('/api/remote/servers');
|
|
|
+ if (response.data.code === 0) {
|
|
|
+ const servers = response.data.data.servers || [];
|
|
|
+ setPreConfiguredServers(servers);
|
|
|
+ if (servers.length > 0) {
|
|
|
+ setSelectedServerId(servers[0].id);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (err) {
|
|
|
+ console.error('获取服务器列表失败:', err);
|
|
|
+ }
|
|
|
+ };
|
|
|
+ fetchServers();
|
|
|
+ }, []);
|
|
|
+
|
|
|
+ const handleConnect = async () => {
|
|
|
+ setConnecting(true);
|
|
|
+ setConnectionError(null);
|
|
|
+
|
|
|
+ try {
|
|
|
+ let response;
|
|
|
+ if (usePreConfigured) {
|
|
|
+ if (!selectedServerId) {
|
|
|
+ setConnectionError('请选择服务器');
|
|
|
+ setConnecting(false);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ // 使用预配置接口测试连接
|
|
|
+ response = await api.post('/api/remote/server/docker/status', {
|
|
|
+ server_id: selectedServerId
|
|
|
+ });
|
|
|
+ } else {
|
|
|
+ // 使用手动配置接口测试连接
|
|
|
+ if (!serverConfig.host || !serverConfig.username) {
|
|
|
+ setConnectionError('请填写服务器地址和用户名');
|
|
|
+ setConnecting(false);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ response = await api.post('/api/remote/docker/status', serverConfig);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (response.data.code === 0) {
|
|
|
+ setIsConnected(true);
|
|
|
+ setConnectionError(null);
|
|
|
+ } else {
|
|
|
+ setConnectionError(response.data.message || '连接失败');
|
|
|
+ setIsConnected(false);
|
|
|
+ }
|
|
|
+ } catch (err: any) {
|
|
|
+ // ... 错误处理逻辑保持不变 ... console.error('连接错误详情:', err);
|
|
|
+ console.error('错误响应:', err.response);
|
|
|
+ console.error('错误请求:', err.request);
|
|
|
+
|
|
|
+ let errorMsg = '连接失败,请检查服务器配置';
|
|
|
+
|
|
|
+ if (err.response) {
|
|
|
+ // 服务器返回了响应
|
|
|
+ const status = err.response.status;
|
|
|
+ const data = err.response.data;
|
|
|
+
|
|
|
+ if (status === 404) {
|
|
|
+ errorMsg = `API 路由未找到 (404)。请确认:
|
|
|
+1. 后端服务已重启并加载了新路由
|
|
|
+2. 访问 http://localhost:8888/docs 确认路由是否存在
|
|
|
+3. 检查 web-ui/.env.local 中的 NEXT_PUBLIC_API_URL 是否指向了正确的后端地址 (当前: ${process.env.NEXT_PUBLIC_API_URL})`;
|
|
|
+ } else if (status === 401) {
|
|
|
+ errorMsg = '未授权 (401),请确认您已登录且有管理员权限';
|
|
|
+ } else if (status === 403) {
|
|
|
+ errorMsg = '权限不足 (403),需要管理员权限';
|
|
|
+ } else if (data?.message) {
|
|
|
+ errorMsg = `${data.message} (${status})`;
|
|
|
+ } else if (data?.detail) {
|
|
|
+ errorMsg = `${data.detail} (${status})`;
|
|
|
+ } else {
|
|
|
+ errorMsg = `服务器错误 (${status}): ${err.response.statusText}`;
|
|
|
+ }
|
|
|
+ } else if (err.request) {
|
|
|
+ // 请求已发出但没有收到响应
|
|
|
+ errorMsg = `无法连接到后端服务器,请检查:
|
|
|
+1. 后端服务是否运行在 http://localhost:8888
|
|
|
+2. 网络连接是否正常
|
|
|
+3. 浏览器控制台是否有 CORS 错误`;
|
|
|
+ } else {
|
|
|
+ // 其他错误
|
|
|
+ errorMsg = err.message || '连接失败,请检查服务器配置';
|
|
|
+ }
|
|
|
+
|
|
|
+ setConnectionError(errorMsg);
|
|
|
+ setIsConnected(false);
|
|
|
+ } finally {
|
|
|
+ setConnecting(false);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleDisconnect = () => {
|
|
|
+ setIsConnected(false);
|
|
|
+ setConnectionError(null);
|
|
|
+ };
|
|
|
+
|
|
|
+ return (
|
|
|
+ <div className="space-y-6">
|
|
|
+ {/* 服务器连接配置 */}
|
|
|
+ <div className="bg-white rounded-lg shadow-sm border border-slate-200 p-6">
|
|
|
+ <div className="flex items-center justify-between mb-4">
|
|
|
+ <div className="flex items-center gap-2">
|
|
|
+ <Server className="text-blue-600" size={24} />
|
|
|
+ <h2 className="text-xl font-semibold text-slate-800">服务器连接配置</h2>
|
|
|
+ </div>
|
|
|
+ <div className="flex bg-slate-100 p-1 rounded-md">
|
|
|
+ <button
|
|
|
+ onClick={() => {
|
|
|
+ if (!isConnected) setUsePreConfigured(true);
|
|
|
+ }}
|
|
|
+ className={`px-3 py-1 text-xs font-medium rounded-md transition-colors ${
|
|
|
+ usePreConfigured
|
|
|
+ ? 'bg-white text-blue-600 shadow-sm'
|
|
|
+ : 'text-slate-600 hover:text-slate-800'
|
|
|
+ }`}
|
|
|
+ disabled={isConnected}
|
|
|
+ >
|
|
|
+ 预配置服务器
|
|
|
+ </button>
|
|
|
+ <button
|
|
|
+ onClick={() => {
|
|
|
+ if (!isConnected) setUsePreConfigured(false);
|
|
|
+ }}
|
|
|
+ className={`px-3 py-1 text-xs font-medium rounded-md transition-colors ${
|
|
|
+ !usePreConfigured
|
|
|
+ ? 'bg-white text-blue-600 shadow-sm'
|
|
|
+ : 'text-slate-600 hover:text-slate-800'
|
|
|
+ }`}
|
|
|
+ disabled={isConnected}
|
|
|
+ >
|
|
|
+ 手动直连
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {usePreConfigured ? (
|
|
|
+ <div className="space-y-4">
|
|
|
+ <div>
|
|
|
+ <label className="block text-sm font-medium text-slate-700 mb-1">选择服务器</label>
|
|
|
+ <select
|
|
|
+ value={selectedServerId}
|
|
|
+ onChange={(e) => setSelectedServerId(e.target.value)}
|
|
|
+ className="w-full px-3 py-2 border border-slate-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
|
+ disabled={isConnected}
|
|
|
+ >
|
|
|
+ <option value="">-- 请选择服务器 --</option>
|
|
|
+ {preConfiguredServers.map((server) => (
|
|
|
+ <option key={server.id} value={server.id}>
|
|
|
+ {server.name} ({server.host})
|
|
|
+ </option>
|
|
|
+ ))}
|
|
|
+ </select>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ ) : (
|
|
|
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
|
+ <div>
|
|
|
+ <label className="block text-sm font-medium text-slate-700 mb-1">服务器地址 *</label>
|
|
|
+ <input
|
|
|
+ type="text"
|
|
|
+ value={serverConfig.host}
|
|
|
+ onChange={(e) => setServerConfig({ ...serverConfig, host: e.target.value })}
|
|
|
+ placeholder="192.168.1.100"
|
|
|
+ className="w-full px-3 py-2 border border-slate-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
|
+ disabled={isConnected}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div>
|
|
|
+ <label className="block text-sm font-medium text-slate-700 mb-1">SSH端口</label>
|
|
|
+ <input
|
|
|
+ type="number"
|
|
|
+ value={serverConfig.port}
|
|
|
+ onChange={(e) => setServerConfig({ ...serverConfig, port: parseInt(e.target.value) || 22 })}
|
|
|
+ placeholder="22"
|
|
|
+ className="w-full px-3 py-2 border border-slate-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
|
+ disabled={isConnected}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div>
|
|
|
+ <label className="block text-sm font-medium text-slate-700 mb-1">用户名 *</label>
|
|
|
+ <input
|
|
|
+ type="text"
|
|
|
+ value={serverConfig.username}
|
|
|
+ onChange={(e) => setServerConfig({ ...serverConfig, username: e.target.value })}
|
|
|
+ placeholder="root"
|
|
|
+ className="w-full px-3 py-2 border border-slate-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
|
+ disabled={isConnected}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div>
|
|
|
+ <label className="block text-sm font-medium text-slate-700 mb-1">密码</label>
|
|
|
+ <input
|
|
|
+ type="password"
|
|
|
+ value={serverConfig.password || ''}
|
|
|
+ onChange={(e) => setServerConfig({ ...serverConfig, password: e.target.value })}
|
|
|
+ placeholder="SSH密码"
|
|
|
+ className="w-full px-3 py-2 border border-slate-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
|
+ disabled={isConnected}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div>
|
|
|
+ <label className="block text-sm font-medium text-slate-700 mb-1">私钥文件路径</label>
|
|
|
+ <input
|
|
|
+ type="text"
|
|
|
+ value={serverConfig.key_file || ''}
|
|
|
+ onChange={(e) => setServerConfig({ ...serverConfig, key_file: e.target.value })}
|
|
|
+ placeholder="/path/to/key.pem"
|
|
|
+ className="w-full px-3 py-2 border border-slate-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
|
+ disabled={isConnected}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div>
|
|
|
+ <label className="block text-sm font-medium text-slate-700 mb-1">项目路径</label>
|
|
|
+ <input
|
|
|
+ type="text"
|
|
|
+ value={serverConfig.project_path}
|
|
|
+ onChange={(e) => setServerConfig({ ...serverConfig, project_path: e.target.value })}
|
|
|
+ placeholder="/root/troov-asyncio"
|
|
|
+ className="w-full px-3 py-2 border border-slate-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
|
+ disabled={isConnected}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+
|
|
|
+ <div className="mt-4 space-y-2">
|
|
|
+ {connectionError && (
|
|
|
+ <div className="bg-red-50 border border-red-200 text-red-700 px-4 py-2 rounded-md text-sm">
|
|
|
+ {connectionError}
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ {isConnected && (
|
|
|
+ <div className="bg-green-50 border border-green-200 text-green-700 px-4 py-2 rounded-md text-sm flex items-center gap-2">
|
|
|
+ <Server size={16} />
|
|
|
+ 已连接到服务器
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ <div className="flex gap-2">
|
|
|
+ {!isConnected ? (
|
|
|
+ <>
|
|
|
+ <button
|
|
|
+ onClick={handleConnect}
|
|
|
+ disabled={connecting}
|
|
|
+ className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
|
+ >
|
|
|
+ <Server size={18} className={connecting ? 'animate-pulse' : ''} />
|
|
|
+ {connecting ? '连接中...' : '连接服务器'}
|
|
|
+ </button>
|
|
|
+ <button
|
|
|
+ onClick={async () => {
|
|
|
+ try {
|
|
|
+ const res = await api.get('/api/ping');
|
|
|
+ alert(`API 测试成功: ${JSON.stringify(res.data)}`);
|
|
|
+ } catch (err: any) {
|
|
|
+ alert(`API 测试失败: ${err.response?.status || '无响应'} - ${err.response?.data?.message || err.message}`);
|
|
|
+ }
|
|
|
+ }}
|
|
|
+ className="px-4 py-2 bg-gray-600 text-white rounded-md hover:bg-gray-700 transition-colors flex items-center gap-2"
|
|
|
+ title="测试后端 API 连接"
|
|
|
+ >
|
|
|
+ <RefreshCw size={18} />
|
|
|
+ 测试 API
|
|
|
+ </button>
|
|
|
+ </>
|
|
|
+ ) : (
|
|
|
+ <button
|
|
|
+ onClick={handleDisconnect}
|
|
|
+ className="px-4 py-2 bg-red-600 text-white rounded-md hover:bg-red-700 transition-colors flex items-center gap-2"
|
|
|
+ >
|
|
|
+ <Server size={18} />
|
|
|
+ 断开连接
|
|
|
+ </button>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {/* 功能标签页 */}
|
|
|
+ {isConnected && (
|
|
|
+ <div className="bg-white rounded-lg shadow-sm border border-slate-200">
|
|
|
+ <div className="border-b border-slate-200">
|
|
|
+ <nav className="flex -mb-px">
|
|
|
+ <button
|
|
|
+ onClick={() => setActiveTab('docker')}
|
|
|
+ className={`px-6 py-3 text-sm font-medium border-b-2 transition-colors flex items-center gap-2 ${
|
|
|
+ activeTab === 'docker'
|
|
|
+ ? 'border-blue-600 text-blue-600'
|
|
|
+ : 'border-transparent text-slate-500 hover:text-slate-700 hover:border-slate-300'
|
|
|
+ }`}
|
|
|
+ >
|
|
|
+ <Play size={18} />
|
|
|
+ Docker控制
|
|
|
+ </button>
|
|
|
+ <button
|
|
|
+ onClick={() => setActiveTab('logs')}
|
|
|
+ className={`px-6 py-3 text-sm font-medium border-b-2 transition-colors flex items-center gap-2 ${
|
|
|
+ activeTab === 'logs'
|
|
|
+ ? 'border-blue-600 text-blue-600'
|
|
|
+ : 'border-transparent text-slate-500 hover:text-slate-700 hover:border-slate-300'
|
|
|
+ }`}
|
|
|
+ >
|
|
|
+ <FileText size={18} />
|
|
|
+ 日志查看
|
|
|
+ </button>
|
|
|
+ <button
|
|
|
+ onClick={() => setActiveTab('config')}
|
|
|
+ className={`px-6 py-3 text-sm font-medium border-b-2 transition-colors flex items-center gap-2 ${
|
|
|
+ activeTab === 'config'
|
|
|
+ ? 'border-blue-600 text-blue-600'
|
|
|
+ : 'border-transparent text-slate-500 hover:text-slate-700 hover:border-slate-300'
|
|
|
+ }`}
|
|
|
+ >
|
|
|
+ <Settings size={18} />
|
|
|
+ 配置文件
|
|
|
+ </button>
|
|
|
+ </nav>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div className="p-6">
|
|
|
+ {activeTab === 'docker' && (
|
|
|
+ <DockerControl
|
|
|
+ serverConfig={serverConfig}
|
|
|
+ serverId={usePreConfigured ? selectedServerId : undefined}
|
|
|
+ />
|
|
|
+ )}
|
|
|
+ {activeTab === 'logs' && (
|
|
|
+ <LogViewer
|
|
|
+ serverConfig={serverConfig}
|
|
|
+ serverId={usePreConfigured ? selectedServerId : undefined}
|
|
|
+ />
|
|
|
+ )}
|
|
|
+ {activeTab === 'config' && (
|
|
|
+ <ConfigManager
|
|
|
+ serverConfig={serverConfig}
|
|
|
+ serverId={usePreConfigured ? selectedServerId : undefined}
|
|
|
+ />
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+}
|