diff --git a/README.md b/README.md index e69de29..67b7dbc 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,32 @@ +在“游戏部署”-“更多游戏部署”的右边加一个新的标签页“在线部署”并按照下面要求实现功能 +进入这个页面首先先校验赞助者密钥是否存在以及在有效期内 +然后需要使用后端代理请求向第二后端发送请求 + +# 获取在线部署游戏 +请求地址 POST http://langlangy.server.xiaozhuhouses.asia:10002/api/online-games +请求体 +```json +{ + "system": "string", + "key": "string" +} +``` +system 提交操作系统类型 Windows提交为Windows Linux平台提交Linux +key 提交赞助者密钥 +返回 +```json +{ + "status": "success", + "message": "获取在线部署游戏列表成功", + "system": "Windows", + "data": { + "我的世界基岩版": { + "txt": "我的世界是一款游戏", + "image": "http://images.server.xiaozhuhouses.asia:40061/i/2025/06/12/t0i9wh.jpg", + "download": "http://langlangy.server.xiaozhuhouses.asia:8082/disk1/MC%e5%86%85%e5%ae%b9/%e6%95%b4%e5%90%88%e5%8c%85/%e5%8e%9f%e7%89%88/BE/bedrock-server.zip" + } + } +} +``` +前端需要根据返回data中的信息按照steam游戏部署风格进行展示 +当用户点击安装,需要输入安装路径,然后根据download链接后端下载压缩包解压到用户安装的路径。要求下载过程需要做成实时进度并且支持取消功能 \ No newline at end of file diff --git a/client/src/pages/GameDeploymentPage.tsx b/client/src/pages/GameDeploymentPage.tsx index 3312e64..adb9067 100644 --- a/client/src/pages/GameDeploymentPage.tsx +++ b/client/src/pages/GameDeploymentPage.tsx @@ -115,10 +115,26 @@ const GameDeploymentPage: React.FC = () => { const [mrpackInstanceDescription, setMrpackInstanceDescription] = useState('') const [creatingMrpackInstance, setCreatingMrpackInstance] = useState(false) + // 在线部署相关状态 + const [onlineGames, setOnlineGames] = useState([]) + const [onlineGamesLoading, setOnlineGamesLoading] = useState(false) + const [sponsorKeyValid, setSponsorKeyValid] = useState(null) + const [sponsorKeyChecking, setSponsorKeyChecking] = useState(false) + const [selectedOnlineGame, setSelectedOnlineGame] = useState(null) + const [onlineGameInstallPath, setOnlineGameInstallPath] = useState('') + const [onlineGameDeploying, setOnlineGameDeploying] = useState(false) + const [onlineGameDeployProgress, setOnlineGameDeployProgress] = useState(null) + const [onlineGameDeployLogs, setOnlineGameDeployLogs] = useState([]) + const [onlineGameDeployComplete, setOnlineGameDeployComplete] = useState(false) + const [onlineGameDeployResult, setOnlineGameDeployResult] = useState(null) + const [showOnlineGameInstallModal, setShowOnlineGameInstallModal] = useState(false) + const [onlineGameInstallModalAnimating, setOnlineGameInstallModalAnimating] = useState(false) + const socketRef = useRef(null) const currentDownloadId = useRef(null) const currentMoreGameDeploymentId = useRef(null) const currentMrpackDeploymentId = useRef(null) + const currentOnlineGameDeploymentId = useRef(null) // 获取游戏列表 const fetchGames = async () => { @@ -166,6 +182,34 @@ const GameDeploymentPage: React.FC = () => { } } + // 检查赞助者密钥 + const checkSponsorKey = async () => { + try { + setSponsorKeyChecking(true) + const response = await apiClient.getSponsorKeyInfo() + + if (response.success && response.data) { + setSponsorKeyValid(response.data.isValid) + if (!response.data.isValid) { + addNotification({ + type: 'warning', + title: '密钥已过期', + message: '您的赞助者密钥已过期,请前往设置页面更新密钥' + }) + } + } else { + setSponsorKeyValid(false) + } + } catch (error: any) { + console.error('检查赞助者密钥失败:', error) + setSponsorKeyValid(false) + } finally { + setSponsorKeyChecking(false) + } + } + + + // 部署更多游戏 const deployMoreGame = async () => { if (!selectedMoreGame || !moreGameInstallPath) { @@ -622,6 +666,36 @@ const GameDeploymentPage: React.FC = () => { }) } }) + + // 监听在线游戏部署日志 + socketRef.current.on('online-deploy-log', (data) => { + if (data.deploymentId === currentOnlineGameDeploymentId.current) { + const message = typeof data.message === 'string' ? data.message : JSON.stringify(data.message) + setOnlineGameDeployLogs(prev => [...prev, message]) + } + }) + + // 监听在线游戏部署进度 + socketRef.current.on('online-deploy-progress', (data) => { + console.log('收到在线部署进度:', data) + if (data.deploymentId === currentOnlineGameDeploymentId.current) { + setOnlineGameDeployProgress(data) + } + }) + + // 监听在线游戏部署完成 + socketRef.current.on('online-deploy-complete', (data) => { + console.log('收到在线部署完成事件:', data) + if (data.deploymentId === currentOnlineGameDeploymentId.current) { + setOnlineGameDeploying(false) + setOnlineGameDeployComplete(true) + setOnlineGameDeployResult(data.result) + currentOnlineGameDeploymentId.current = null + + // 不显示通知,让用户在模态框中看到结果 + // 成功或失败的状态会在UI中显示 + } + }) } // 下载Minecraft服务端 @@ -926,7 +1000,13 @@ const GameDeploymentPage: React.FC = () => { if (activeTab === 'more-games') { fetchMoreGames() } - }, [activeTab]) + if (activeTab === 'online-deploy') { + checkSponsorKey() + if (sponsorKeyValid) { + fetchOnlineGames() + } + } + }, [activeTab, sponsorKeyValid]) // 清理WebSocket连接和定时器 useEffect(() => { @@ -1122,6 +1202,172 @@ const GameDeploymentPage: React.FC = () => { } } + // 获取在线游戏列表 + const fetchOnlineGames = async () => { + try { + setOnlineGamesLoading(true) + const response = await apiClient.getOnlineGames() + + if (response.success) { + setOnlineGames(response.data || []) + } else { + throw new Error(response.message || '获取在线游戏列表失败') + } + } catch (error: any) { + console.error('获取在线游戏列表失败:', error) + addNotification({ + type: 'error', + title: '获取失败', + message: error.message || '无法获取在线游戏列表' + }) + } finally { + setOnlineGamesLoading(false) + } + } + + // 打开在线游戏安装对话框 + const handleOpenOnlineGameInstallModal = (game: any) => { + setSelectedOnlineGame(game) + setOnlineGameInstallPath('') + setShowOnlineGameInstallModal(true) + setTimeout(() => setOnlineGameInstallModalAnimating(true), 10) + } + + // 关闭在线游戏安装对话框 + const handleCloseOnlineGameInstallModal = () => { + setOnlineGameInstallModalAnimating(false) + setTimeout(() => { + setShowOnlineGameInstallModal(false) + setSelectedOnlineGame(null) + setOnlineGameInstallPath('') + // 重置部署相关状态 + setOnlineGameDeploying(false) + setOnlineGameDeployProgress(null) + setOnlineGameDeployLogs([]) + setOnlineGameDeployComplete(false) + setOnlineGameDeployResult(null) + currentOnlineGameDeploymentId.current = null + }, 300) + } + + // 开始在线游戏部署 + const startOnlineGameDeployment = async () => { + if (!selectedOnlineGame || !onlineGameInstallPath.trim()) { + addNotification({ + type: 'error', + title: '参数错误', + message: '请选择游戏并填写安装路径' + }) + return + } + + try { + // 重置状态 + setOnlineGameDeploying(true) + setOnlineGameDeployProgress(null) + setOnlineGameDeployLogs([]) + setOnlineGameDeployComplete(false) + setOnlineGameDeployResult(null) + + // 初始化WebSocket连接 + initializeSocket() + + // 等待WebSocket连接建立 + const waitForConnection = () => { + return new Promise((resolve, reject) => { + if (socketRef.current?.connected && socketRef.current?.id) { + resolve(socketRef.current.id) + return + } + + const timeout = setTimeout(() => { + reject(new Error('WebSocket连接超时')) + }, 10000) + + const checkConnection = () => { + if (socketRef.current?.connected && socketRef.current?.id) { + clearTimeout(timeout) + resolve(socketRef.current.id) + } else { + setTimeout(checkConnection, 100) + } + } + + checkConnection() + }) + } + + const socketId = await waitForConnection() + + // 调用部署API + const response = await apiClient.deployOnlineGame({ + gameId: selectedOnlineGame.id, + installPath: onlineGameInstallPath.trim(), + socketId + }) + + if (response.success && response.data?.deploymentId) { + currentOnlineGameDeploymentId.current = response.data.deploymentId + + addNotification({ + type: 'success', + title: '部署已启动', + message: `${selectedOnlineGame.name} 部署已开始` + }) + + // 不关闭模态框,保持打开状态以显示部署进度 + } else { + throw new Error(response.message || '启动部署失败') + } + } catch (error: any) { + console.error('启动在线游戏部署失败:', error) + setOnlineGameDeploying(false) + + addNotification({ + type: 'error', + title: '部署失败', + message: error.message || '无法启动在线游戏部署' + }) + } + } + + // 取消在线游戏部署 + const cancelOnlineGameDeployment = async () => { + if (!currentOnlineGameDeploymentId.current) { + addNotification({ + type: 'warning', + title: '无法取消', + message: '没有正在进行的在线游戏部署' + }) + return + } + + try { + const response = await apiClient.cancelOnlineGameDeployment(currentOnlineGameDeploymentId.current) + + if (response.success) { + setOnlineGameDeploying(false) + setOnlineGameDeployProgress(null) + currentOnlineGameDeploymentId.current = null + + addNotification({ + type: 'info', + title: '部署已取消', + message: '在线游戏部署已取消' + }) + } else { + throw new Error(response.message || '取消部署失败') + } + } catch (error: any) { + console.error('取消在线游戏部署失败:', error) + addNotification({ + type: 'error', + title: '取消失败', + message: error.message || '无法取消在线游戏部署' + }) + } + } + // 筛选游戏 const filteredGames = Object.entries(games).filter(([gameKey, gameInfo]) => { // 搜索筛选 @@ -1148,7 +1394,8 @@ const GameDeploymentPage: React.FC = () => { { id: 'steamcmd', name: 'SteamCMD', icon: Download }, { id: 'minecraft', name: 'Minecraft部署', icon: Pickaxe }, { id: 'mrpack', name: 'Minecraft整合包部署', icon: Package }, - { id: 'more-games', name: '更多游戏部署', icon: Server } + { id: 'more-games', name: '更多游戏部署', icon: Server }, + { id: 'online-deploy', name: '在线部署', icon: ExternalLink } ] if (loading) { @@ -1361,6 +1608,124 @@ const GameDeploymentPage: React.FC = () => { )} + {/* 在线部署标签页内容 */} + {activeTab === 'online-deploy' && ( +
+ {/* 赞助者密钥状态 */} +
+
+ {sponsorKeyChecking ? ( + + ) : sponsorKeyValid ? ( + + ) : ( + + )} +
+

+ 赞助者密钥状态 +

+

+ {sponsorKeyChecking + ? '检查中...' + : sponsorKeyValid + ? '密钥有效,可以使用在线部署功能' + : '密钥无效或未设置,请前往设置页面配置赞助者密钥'} +

+
+ +
+
+ + {/* 在线游戏列表 */} + {sponsorKeyValid ? ( + onlineGamesLoading ? ( +
+ + 加载在线游戏列表中... +
+ ) : ( +
+ {onlineGames.length === 0 ? ( +
+
+ +

暂无可用的在线游戏

+

请稍后再试或联系管理员

+
+
+ ) : ( + onlineGames.map((game) => ( +
+ {/* 游戏图片 */} +
+ + 前往设置 + +
+ )} +
+ )} + {/* Minecraft 标签页内容 */} {activeTab === 'minecraft' && (
@@ -2571,6 +2936,179 @@ const GameDeploymentPage: React.FC = () => {
)} + {/* 在线游戏安装对话框 */} + {showOnlineGameInstallModal && selectedOnlineGame && ( +
+
+
+

+ 部署 {selectedOnlineGame.name} +

+ +
+ +
+ {/* 游戏信息 */} +
+

+ 游戏信息 +

+

+ 名称: {selectedOnlineGame.name} +

+ {selectedOnlineGame.description && ( +

+ 描述: {selectedOnlineGame.description} +

+ )} +
+ + {/* 安装路径 */} + {!onlineGameDeploying && ( +
+ + setOnlineGameInstallPath(e.target.value)} + className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white dark:bg-gray-700 text-gray-900 dark:text-white" + placeholder="输入游戏安装路径" + /> +
+ )} + + {/* 部署进度 */} + {onlineGameDeploying && onlineGameDeployProgress && ( +
+
+ + 部署进度 + + + {onlineGameDeployProgress.percentage}% + +
+
+
+
+

+ {onlineGameDeployProgress.currentStep} +

+
+ )} + + {/* 部署日志 */} + {onlineGameDeploying && onlineGameDeployLogs.length > 0 && ( +
+

+ 部署日志 +

+
+ {onlineGameDeployLogs.map((log, index) => ( +
+ {log} +
+ ))} +
+
+ )} + + {/* 部署完成结果 */} + {onlineGameDeployComplete && ( +
+
+ {onlineGameDeployResult?.success !== false ? ( + + ) : ( + + )} +

+ {onlineGameDeployResult?.success !== false ? '部署完成' : '部署失败'} +

+
+

+ {onlineGameDeployResult?.message || (onlineGameDeployResult?.success !== false ? '在线游戏部署完成!' : '部署过程中发生错误')} +

+ {onlineGameDeployResult?.installPath && ( +

+ 安装路径: {onlineGameDeployResult.installPath} +

+ )} +
+ )} +
+ +
+ {onlineGameDeployComplete ? ( + + ) : ( + <> + + + + )} +
+
+
+ )} + {/* 创建整合包实例对话框 */} {showCreateMrpackInstanceModal && mrpackDeployResult && (
= 0.6" } }, + "node_modules/adm-zip": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.16.tgz", + "integrity": "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==", + "license": "MIT", + "engines": { + "node": ">=12.0" + } + }, "node_modules/ansi-escapes": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", diff --git a/server/package.json b/server/package.json index 38eedbb..dc03b39 100644 --- a/server/package.json +++ b/server/package.json @@ -30,7 +30,8 @@ "axios": "^1.6.2", "fs-extra": "^11.2.0", "yauzl": "^2.10.0", - "mime-types": "^2.1.35" + "mime-types": "^2.1.35", + "adm-zip": "^0.5.10" }, "devDependencies": { "@types/cors": "^2.8.17", @@ -47,6 +48,7 @@ "@types/fs-extra": "^11.0.4", "@types/yauzl": "^2.10.3", "@types/mime-types": "^2.1.4", + "@types/adm-zip": "^0.5.5", "jest": "^29.7.0", "tsx": "^4.6.2", "typescript": "^5.3.3" diff --git a/server/src/index.ts b/server/src/index.ts index af5b9ff..bf52ef3 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -35,6 +35,7 @@ import weatherRouter from './routes/weather.js' import pluginsRouter, { setPluginManager } from './routes/plugins.js' import pluginApiRouter, { setPluginApiDependencies } from './routes/pluginApi.js' import sponsorRouter, { setSponsorDependencies } from './routes/sponsor.js' +import onlineDeployRouter from './routes/onlineDeploy.js' // 获取当前文件目录 const __filename = fileURLToPath(import.meta.url) @@ -511,6 +512,11 @@ async function startServer() { // 设置赞助者路由 setSponsorDependencies(configManager) app.use('/api/sponsor', sponsorRouter) + + // 设置在线部署路由 + const { setOnlineDeployDependencies } = await import('./routes/onlineDeploy.js') + setOnlineDeployDependencies(io, configManager) + app.use('/api/online-deploy', onlineDeployRouter) // 前端路由处理(SPA支持) app.get('*', (req, res) => { diff --git a/server/src/routes/gameDeployment.ts b/server/src/routes/gameDeployment.ts index 2b6cf65..822c8da 100644 --- a/server/src/routes/gameDeployment.ts +++ b/server/src/routes/gameDeployment.ts @@ -80,8 +80,8 @@ export function setGameDeploymentManagers( // 获取可安装的游戏列表 router.get('/games', authenticateToken, async (req: Request, res: Response) => { try { - // const gamesFilePath = path.join(__dirname, '../../data/games/installgame.json') - const gamesFilePath = path.join(__dirname, '../data/games/installgame.json') + const gamesFilePath = path.join(__dirname, '../../data/games/installgame.json') + // const gamesFilePath = path.join(__dirname, '../data/games/installgame.json') const gamesData = await fs.readFile(gamesFilePath, 'utf-8') const allGames: { [key: string]: SteamGameInfo } = JSON.parse(gamesData) diff --git a/server/src/routes/index.ts b/server/src/routes/index.ts index 109107d..028cafa 100644 --- a/server/src/routes/index.ts +++ b/server/src/routes/index.ts @@ -5,6 +5,7 @@ import systemRoutes from './system.js' import fileRoutes from './files.js' import instanceRoutes from './instances.js' import { minecraftRouter } from './minecraft.js' +import onlineDeployRoutes from './onlineDeploy.js' const router = Router() @@ -35,5 +36,6 @@ router.use('/system', systemRoutes) router.use('/files', fileRoutes) router.use('/instances', instanceRoutes) router.use('/minecraft', minecraftRouter) +router.use('/online-deploy', onlineDeployRoutes) export default router \ No newline at end of file diff --git a/server/src/routes/onlineDeploy.ts b/server/src/routes/onlineDeploy.ts new file mode 100644 index 0000000..0443a0f --- /dev/null +++ b/server/src/routes/onlineDeploy.ts @@ -0,0 +1,516 @@ +import { Router, Request, Response } from 'express' +import { authenticateToken } from '../middleware/auth.js' +import { ConfigManager } from '../modules/config/ConfigManager.js' +import logger from '../utils/logger.js' +import { Server as SocketIOServer } from 'socket.io' +import { v4 as uuidv4 } from 'uuid' +import axios from 'axios' +import fs from 'fs/promises' +import path from 'path' +import { createWriteStream, createReadStream } from 'fs' +import { pipeline } from 'stream/promises' +import AdmZip from 'adm-zip' + +const router = Router() +let io: SocketIOServer +let configManager: ConfigManager + +// 设置依赖 +export function setOnlineDeployDependencies(socketIO: SocketIOServer, config: ConfigManager) { + io = socketIO + configManager = config +} + +// 平台类型枚举 +export enum Platform { + WINDOWS = 'windows', + LINUX = 'linux', + MACOS = 'macos' +} + +// 在线游戏信息接口 +export interface OnlineGameInfo { + id: string + name: string + description?: string + image?: string + downloadUrl?: string + category?: string + supportedPlatforms: Platform[] + deploymentScript?: string + version?: string +} + +// 部署选项接口 +export interface OnlineDeploymentOptions { + gameId: string + installPath: string + options?: any +} + +// 部署结果接口 +export interface OnlineDeploymentResult { + success: boolean + message: string + data?: any +} + +// 活动部署映射 +const activeDeployments = new Map() + +// 获取当前平台 +function getCurrentPlatform(): Platform { + const platform = process.platform + switch (platform) { + case 'win32': + return Platform.WINDOWS + case 'linux': + return Platform.LINUX + case 'darwin': + return Platform.MACOS + default: + return Platform.LINUX + } +} + +// 检查游戏是否支持当前平台 +function isGameSupportedOnCurrentPlatform(game: OnlineGameInfo): boolean { + const currentPlatform = getCurrentPlatform() + return game.supportedPlatforms.includes(currentPlatform) +} + +// 验证赞助者密钥 +async function validateSponsorKey(): Promise { + try { + const sponsorConfig = await configManager.getSponsorConfig() + if (!sponsorConfig || !sponsorConfig.key || !sponsorConfig.isValid) { + return false + } + + // 检查密钥是否过期 + if (sponsorConfig.expiryTime && new Date() > new Date(sponsorConfig.expiryTime)) { + return false + } + + return true + } catch (error) { + logger.error('验证赞助者密钥失败:', error) + return false + } +} + +// 获取在线游戏列表 +router.get('/games', authenticateToken, async (req: Request, res: Response) => { + try { + // 验证赞助者密钥 + const isValidSponsor = await validateSponsorKey() + if (!isValidSponsor) { + return res.status(403).json({ + success: false, + message: '需要有效的赞助者密钥才能访问在线部署功能' + }) + } + + const currentPlatform = getCurrentPlatform() + + // 获取赞助者密钥 + const sponsorConfig = await configManager.getSponsorConfig() + if (!sponsorConfig || !sponsorConfig.key) { + return res.status(403).json({ + success: false, + message: '未找到赞助者密钥' + }) + } + + // 映射平台名称 + const systemName = currentPlatform === Platform.WINDOWS ? 'Windows' : + currentPlatform === Platform.LINUX ? 'Linux' : 'Linux' + + try { + // 向第三方API请求在线游戏列表 + const response = await axios.post('http://langlangy.server.xiaozhuhouses.asia:10002/api/online-games', { + system: systemName, + key: sponsorConfig.key + }, { + timeout: 30000, // 30秒超时 + headers: { + 'Content-Type': 'application/json' + } + }) + + if (response.data.status !== 'success') { + throw new Error(response.data.message || '获取在线游戏列表失败') + } + + // 转换API返回的数据格式 + const gameData = response.data.data || {} + const supportedGames = Object.entries(gameData).map(([gameName, gameInfo]: [string, any]) => ({ + id: gameName.toLowerCase().replace(/\s+/g, '-'), + name: gameName, + description: gameInfo.txt || '', + image: gameInfo.image || '', + downloadUrl: gameInfo.download || '', + supportedPlatforms: [currentPlatform], // 基于请求的系统类型 + supported: true, + currentPlatform + })) + + logger.info(`成功获取到 ${supportedGames.length} 个在线游戏`) + + res.json({ + success: true, + data: supportedGames + }) + + } catch (apiError: any) { + logger.error('请求第三方API失败:', apiError.message) + + // 如果API请求失败,返回空列表而不是错误 + res.json({ + success: true, + data: [], + message: '暂时无法获取在线游戏列表,请稍后重试' + }) + } + } catch (error) { + logger.error('获取在线游戏列表失败:', error) + res.status(500).json({ + success: false, + message: '获取在线游戏列表失败' + }) + } +}) + +// 部署在线游戏 +router.post('/deploy', authenticateToken, async (req: Request, res: Response) => { + try { + // 验证赞助者密钥 + const isValidSponsor = await validateSponsorKey() + if (!isValidSponsor) { + return res.status(403).json({ + success: false, + message: '需要有效的赞助者密钥才能使用在线部署功能' + }) + } + + const { gameId, installPath, socketId } = req.body + + if (!gameId || !installPath) { + return res.status(400).json({ + success: false, + message: '缺少必要参数' + }) + } + + const deploymentId = uuidv4() + + // 模拟部署过程 + const deploymentProcess = { + id: deploymentId, + gameId, + installPath, + status: 'running', + startTime: new Date() + } + + activeDeployments.set(deploymentId, deploymentProcess) + + // 异步执行部署 + setImmediate(async () => { + try { + // 检查部署是否被取消 + if (deploymentProcess.status === 'cancelled') { + return + } + + // 发送开始部署消息 + if (io && socketId) { + io.to(socketId).emit('online-deploy-log', { + deploymentId, + message: `开始部署 ${gameId}...`, + type: 'info', + timestamp: new Date().toISOString() + }) + } + + // 步骤1: 验证安装路径 + if (io && socketId) { + io.to(socketId).emit('online-deploy-log', { + deploymentId, + message: '正在验证安装路径...', + type: 'info', + timestamp: new Date().toISOString() + }) + io.to(socketId).emit('online-deploy-progress', { + deploymentId, + percentage: 10, + currentStep: '验证安装路径' + }) + } + + // 确保安装目录存在 + await fs.mkdir(installPath, { recursive: true }) + + // 步骤2: 获取游戏信息和下载链接 + if (deploymentProcess.status === 'cancelled') return + + if (io && socketId) { + io.to(socketId).emit('online-deploy-log', { + deploymentId, + message: '正在获取游戏下载信息...', + type: 'info', + timestamp: new Date().toISOString() + }) + io.to(socketId).emit('online-deploy-progress', { + deploymentId, + percentage: 20, + currentStep: '获取下载信息' + }) + } + + // 重新获取游戏列表以获取下载链接 + const sponsorConfig = await configManager.getSponsorConfig() + const currentPlatform = getCurrentPlatform() + const systemName = currentPlatform === Platform.WINDOWS ? 'Windows' : 'Linux' + + const gameListResponse = await axios.post('http://langlangy.server.xiaozhuhouses.asia:10002/api/online-games', { + system: systemName, + key: sponsorConfig.key + }) + + if (gameListResponse.data.status !== 'success') { + throw new Error('无法获取游戏下载信息') + } + + // 查找对应的游戏 + const gameData = gameListResponse.data.data || {} + const gameEntry = Object.entries(gameData).find(([name]) => + name.toLowerCase().replace(/\s+/g, '-') === gameId + ) as [string, { download?: string; [key: string]: any }] | undefined + + if (!gameEntry || !gameEntry[1].download) { + throw new Error('未找到游戏下载链接') + } + + const downloadUrl = gameEntry[1].download + const gameName = gameEntry[0] + + // 步骤3: 下载游戏文件 + if (deploymentProcess.status === 'cancelled') return + + if (io && socketId) { + io.to(socketId).emit('online-deploy-log', { + deploymentId, + message: `正在下载 ${gameName}...`, + type: 'info', + timestamp: new Date().toISOString() + }) + } + + const fileName = path.basename(downloadUrl) || `${gameId}.zip` + const downloadPath = path.join(installPath, fileName) + + // 下载文件 + const response = await axios({ + method: 'GET', + url: downloadUrl, + responseType: 'stream', + timeout: 300000 // 5分钟超时 + }) + + const totalSize = parseInt(response.headers['content-length'] || '0') + let downloadedSize = 0 + + const writer = createWriteStream(downloadPath) + + response.data.on('data', (chunk: Buffer) => { + if (deploymentProcess.status === 'cancelled') { + response.data.destroy() + writer.destroy() + return + } + + downloadedSize += chunk.length + const percentage = totalSize > 0 ? Math.round((downloadedSize / totalSize) * 50) + 20 : 30 + + if (io && socketId) { + io.to(socketId).emit('online-deploy-progress', { + deploymentId, + percentage, + currentStep: `下载中... ${Math.round(downloadedSize / 1024 / 1024)}MB${totalSize > 0 ? `/${Math.round(totalSize / 1024 / 1024)}MB` : ''}` + }) + } + }) + + await pipeline(response.data, writer) + + if (deploymentProcess.status === 'cancelled') { + // 清理下载的文件 + try { + await fs.unlink(downloadPath) + } catch {} + return + } + + // 步骤4: 解压文件 + if (io && socketId) { + io.to(socketId).emit('online-deploy-log', { + deploymentId, + message: '正在解压游戏文件...', + type: 'info', + timestamp: new Date().toISOString() + }) + io.to(socketId).emit('online-deploy-progress', { + deploymentId, + percentage: 80, + currentStep: '解压文件' + }) + } + + // 解压ZIP文件 + const zip = new AdmZip(downloadPath) + const extractPath = path.join(installPath, 'game') + await fs.mkdir(extractPath, { recursive: true }) + zip.extractAllTo(extractPath, true) + + // 删除下载的ZIP文件 + await fs.unlink(downloadPath) + + if (deploymentProcess.status === 'cancelled') return + + // 步骤5: 完成部署 + if (io && socketId) { + io.to(socketId).emit('online-deploy-log', { + deploymentId, + message: '部署完成!', + type: 'success', + timestamp: new Date().toISOString() + }) + io.to(socketId).emit('online-deploy-progress', { + deploymentId, + percentage: 100, + currentStep: '部署完成' + }) + } + + // 部署完成 + deploymentProcess.status = 'completed' + + if (io && socketId) { + io.to(socketId).emit('online-deploy-complete', { + deploymentId, + success: true, + result: { + installPath: extractPath, + gameId, + gameName, + message: `${gameName} 部署成功!` + } + }) + } + + } catch (error) { + logger.error('在线游戏部署失败:', error) + deploymentProcess.status = 'failed' + + if (io && socketId) { + io.to(socketId).emit('online-deploy-complete', { + deploymentId, + success: false, + error: error instanceof Error ? error.message : '部署失败' + }) + } + } finally { + // 清理部署记录 + setTimeout(() => { + activeDeployments.delete(deploymentId) + }, 300000) // 5分钟后清理 + } + }) + + res.json({ + success: true, + data: { + deploymentId + }, + message: '部署已开始' + }) + + } catch (error) { + logger.error('启动在线游戏部署失败:', error) + res.status(500).json({ + success: false, + message: '启动部署失败' + }) + } +}) + +// 取消部署 +router.post('/cancel/:deploymentId', authenticateToken, async (req: Request, res: Response) => { + try { + const { deploymentId } = req.params + + const deployment = activeDeployments.get(deploymentId) + if (!deployment) { + return res.status(404).json({ + success: false, + message: '部署任务不存在' + }) + } + + // 标记为已取消 + deployment.status = 'cancelled' + + // 发送取消通知 + if (io) { + io.emit('online-deploy-log', { + deploymentId, + message: '部署已被用户取消', + type: 'warning', + timestamp: new Date().toISOString() + }) + + io.emit('online-deploy-complete', { + deploymentId, + success: false, + error: '部署已取消' + }) + } + + // 延迟删除,给清理操作一些时间 + setTimeout(() => { + activeDeployments.delete(deploymentId) + }, 5000) + + res.json({ + success: true, + message: '部署已取消' + }) + + } catch (error) { + logger.error('取消在线游戏部署失败:', error) + res.status(500).json({ + success: false, + message: '取消部署失败' + }) + } +}) + +// 获取活动部署列表 +router.get('/deployments', authenticateToken, async (req: Request, res: Response) => { + try { + const deployments = Array.from(activeDeployments.values()) + res.json({ + success: true, + data: deployments + }) + } catch (error) { + logger.error('获取活动部署列表失败:', error) + res.status(500).json({ + success: false, + message: '获取部署列表失败' + }) + } +}) + +export default router \ No newline at end of file