mirror of
https://github.com/GSManagerXZ/GameServerManager.git
synced 2026-06-02 02:49:33 +08:00
新增在线部署
This commit is contained in:
32
README.md
32
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链接后端下载压缩包解压到用户安装的路径。要求下载过程需要做成实时进度并且支持取消功能
|
||||
@@ -115,10 +115,26 @@ const GameDeploymentPage: React.FC = () => {
|
||||
const [mrpackInstanceDescription, setMrpackInstanceDescription] = useState('')
|
||||
const [creatingMrpackInstance, setCreatingMrpackInstance] = useState(false)
|
||||
|
||||
// 在线部署相关状态
|
||||
const [onlineGames, setOnlineGames] = useState<any[]>([])
|
||||
const [onlineGamesLoading, setOnlineGamesLoading] = useState(false)
|
||||
const [sponsorKeyValid, setSponsorKeyValid] = useState<boolean | null>(null)
|
||||
const [sponsorKeyChecking, setSponsorKeyChecking] = useState(false)
|
||||
const [selectedOnlineGame, setSelectedOnlineGame] = useState<any>(null)
|
||||
const [onlineGameInstallPath, setOnlineGameInstallPath] = useState('')
|
||||
const [onlineGameDeploying, setOnlineGameDeploying] = useState(false)
|
||||
const [onlineGameDeployProgress, setOnlineGameDeployProgress] = useState<any>(null)
|
||||
const [onlineGameDeployLogs, setOnlineGameDeployLogs] = useState<string[]>([])
|
||||
const [onlineGameDeployComplete, setOnlineGameDeployComplete] = useState(false)
|
||||
const [onlineGameDeployResult, setOnlineGameDeployResult] = useState<any>(null)
|
||||
const [showOnlineGameInstallModal, setShowOnlineGameInstallModal] = useState(false)
|
||||
const [onlineGameInstallModalAnimating, setOnlineGameInstallModalAnimating] = useState(false)
|
||||
|
||||
const socketRef = useRef<Socket | null>(null)
|
||||
const currentDownloadId = useRef<string | null>(null)
|
||||
const currentMoreGameDeploymentId = useRef<string | null>(null)
|
||||
const currentMrpackDeploymentId = useRef<string | null>(null)
|
||||
const currentOnlineGameDeploymentId = useRef<string | null>(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<string>((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 = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 在线部署标签页内容 */}
|
||||
{activeTab === 'online-deploy' && (
|
||||
<div className="space-y-6">
|
||||
{/* 赞助者密钥状态 */}
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-md p-4">
|
||||
<div className="flex items-center space-x-3">
|
||||
{sponsorKeyChecking ? (
|
||||
<Loader className="w-5 h-5 animate-spin text-blue-500" />
|
||||
) : sponsorKeyValid ? (
|
||||
<CheckCircle className="w-5 h-5 text-green-500" />
|
||||
) : (
|
||||
<AlertCircle className="w-5 h-5 text-red-500" />
|
||||
)}
|
||||
<div>
|
||||
<h3 className="font-medium text-gray-900 dark:text-white">
|
||||
赞助者密钥状态
|
||||
</h3>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||
{sponsorKeyChecking
|
||||
? '检查中...'
|
||||
: sponsorKeyValid
|
||||
? '密钥有效,可以使用在线部署功能'
|
||||
: '密钥无效或未设置,请前往设置页面配置赞助者密钥'}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={checkSponsorKey}
|
||||
className="ml-auto px-3 py-1 text-sm bg-blue-600 hover:bg-blue-700 text-white rounded transition-colors"
|
||||
>
|
||||
重新检查
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 在线游戏列表 */}
|
||||
{sponsorKeyValid ? (
|
||||
onlineGamesLoading ? (
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<Loader className="w-8 h-8 animate-spin text-blue-500" />
|
||||
<span className="ml-2 text-gray-600 dark:text-gray-400">加载在线游戏列表中...</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
|
||||
{onlineGames.length === 0 ? (
|
||||
<div className="col-span-full text-center py-12">
|
||||
<div className="text-gray-500 dark:text-gray-400">
|
||||
<Server className="w-12 h-12 mx-auto mb-4 opacity-50" />
|
||||
<p className="text-lg font-medium mb-2">暂无可用的在线游戏</p>
|
||||
<p className="text-sm">请稍后再试或联系管理员</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
onlineGames.map((game) => (
|
||||
<div
|
||||
key={game.id}
|
||||
className="bg-white dark:bg-gray-800 rounded-lg shadow-md overflow-hidden hover:shadow-lg transition-shadow"
|
||||
>
|
||||
{/* 游戏图片 */}
|
||||
<div className="aspect-video bg-gray-200 dark:bg-gray-700 relative">
|
||||
<img
|
||||
src={game.image || 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjEwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSIjZGRkIi8+PHRleHQgeD0iNTAlIiB5PSI1MCUiIGZvbnQtZmFtaWx5PSJBcmlhbCIgZm9udC1zaXplPSIxNCIgZmlsbD0iIzk5OSIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZHk9Ii4zZW0iPuaXoOazleWKoOi9veWbvueJhzwvdGV4dD48L3N2Zz4='}
|
||||
alt={game.name}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
<div className="absolute top-2 right-2">
|
||||
<div className="bg-black/50 text-white px-2 py-1 rounded text-xs">
|
||||
在线部署
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 游戏信息 */}
|
||||
<div className="p-4">
|
||||
<h3 className="font-semibold text-gray-900 dark:text-white mb-2 text-center">
|
||||
{game.name}
|
||||
</h3>
|
||||
|
||||
{/* 游戏描述 */}
|
||||
{game.description && (
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4 text-center line-clamp-2">
|
||||
{game.description}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* 操作按钮 */}
|
||||
<button
|
||||
onClick={() => handleOpenOnlineGameInstallModal(game)}
|
||||
className="w-full bg-green-600 hover:bg-green-700 text-white py-2 px-4 rounded-lg transition-colors flex items-center justify-center space-x-2"
|
||||
>
|
||||
<Download className="w-4 h-4" />
|
||||
<span>部署游戏</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<div className="bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg p-6 text-center">
|
||||
<AlertCircle className="w-12 h-12 mx-auto mb-4 text-yellow-600 dark:text-yellow-400" />
|
||||
<h3 className="text-lg font-medium text-yellow-800 dark:text-yellow-200 mb-2">
|
||||
需要赞助者密钥
|
||||
</h3>
|
||||
<p className="text-yellow-700 dark:text-yellow-300 mb-4">
|
||||
在线部署功能需要有效的赞助者密钥才能使用。请前往设置页面配置您的密钥。
|
||||
</p>
|
||||
<button
|
||||
onClick={() => navigate('/settings')}
|
||||
className="bg-yellow-600 hover:bg-yellow-700 text-white px-4 py-2 rounded-lg transition-colors"
|
||||
>
|
||||
前往设置
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Minecraft 标签页内容 */}
|
||||
{activeTab === 'minecraft' && (
|
||||
<div className="space-y-6">
|
||||
@@ -2571,6 +2936,179 @@ const GameDeploymentPage: React.FC = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 在线游戏安装对话框 */}
|
||||
{showOnlineGameInstallModal && selectedOnlineGame && (
|
||||
<div className={`fixed inset-0 bg-black/50 flex items-center justify-center z-50 transition-opacity duration-300 ${
|
||||
onlineGameInstallModalAnimating ? 'opacity-100' : 'opacity-0'
|
||||
}`}>
|
||||
<div className={`bg-white dark:bg-gray-800 rounded-lg shadow-xl w-full max-w-md mx-4 transform transition-all duration-300 ${
|
||||
onlineGameInstallModalAnimating ? 'scale-100 opacity-100' : 'scale-95 opacity-0'
|
||||
}`}>
|
||||
<div className="flex items-center justify-between p-6 border-b border-gray-200 dark:border-gray-700">
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
部署 {selectedOnlineGame.name}
|
||||
</h3>
|
||||
<button
|
||||
onClick={handleCloseOnlineGameInstallModal}
|
||||
className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="p-6 space-y-4">
|
||||
{/* 游戏信息 */}
|
||||
<div className="bg-gray-50 dark:bg-gray-700 rounded-lg p-3">
|
||||
<h4 className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
游戏信息
|
||||
</h4>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||
<strong>名称:</strong> {selectedOnlineGame.name}
|
||||
</p>
|
||||
{selectedOnlineGame.description && (
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mt-1">
|
||||
<strong>描述:</strong> {selectedOnlineGame.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 安装路径 */}
|
||||
{!onlineGameDeploying && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
安装路径 *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={onlineGameInstallPath}
|
||||
onChange={(e) => 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="输入游戏安装路径"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 部署进度 */}
|
||||
{onlineGameDeploying && onlineGameDeployProgress && (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
部署进度
|
||||
</span>
|
||||
<span className="text-sm text-gray-500 dark:text-gray-400">
|
||||
{onlineGameDeployProgress.percentage}%
|
||||
</span>
|
||||
</div>
|
||||
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2">
|
||||
<div
|
||||
className="bg-green-600 h-2 rounded-full transition-all duration-300"
|
||||
style={{ width: `${onlineGameDeployProgress.percentage}%` }}
|
||||
></div>
|
||||
</div>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||
{onlineGameDeployProgress.currentStep}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 部署日志 */}
|
||||
{onlineGameDeploying && onlineGameDeployLogs.length > 0 && (
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
部署日志
|
||||
</h4>
|
||||
<div className="bg-gray-900 text-green-400 p-3 rounded-lg text-xs font-mono max-h-32 overflow-y-auto">
|
||||
{onlineGameDeployLogs.map((log, index) => (
|
||||
<div key={index} className="mb-1">
|
||||
{log}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 部署完成结果 */}
|
||||
{onlineGameDeployComplete && (
|
||||
<div className={`rounded-lg p-3 ${
|
||||
onlineGameDeployResult?.success !== false
|
||||
? 'bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800'
|
||||
: 'bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800'
|
||||
}`}>
|
||||
<div className="flex items-center space-x-2">
|
||||
{onlineGameDeployResult?.success !== false ? (
|
||||
<CheckCircle className="w-5 h-5 text-green-600" />
|
||||
) : (
|
||||
<AlertCircle className="w-5 h-5 text-red-600" />
|
||||
)}
|
||||
<h4 className={`text-sm font-medium ${
|
||||
onlineGameDeployResult?.success !== false
|
||||
? 'text-green-800 dark:text-green-400'
|
||||
: 'text-red-800 dark:text-red-400'
|
||||
}`}>
|
||||
{onlineGameDeployResult?.success !== false ? '部署完成' : '部署失败'}
|
||||
</h4>
|
||||
</div>
|
||||
<p className={`text-sm mt-1 ${
|
||||
onlineGameDeployResult?.success !== false
|
||||
? 'text-green-700 dark:text-green-300'
|
||||
: 'text-red-700 dark:text-red-300'
|
||||
}`}>
|
||||
{onlineGameDeployResult?.message || (onlineGameDeployResult?.success !== false ? '在线游戏部署完成!' : '部署过程中发生错误')}
|
||||
</p>
|
||||
{onlineGameDeployResult?.installPath && (
|
||||
<p className={`text-xs mt-1 ${
|
||||
onlineGameDeployResult?.success !== false
|
||||
? 'text-green-600 dark:text-green-400'
|
||||
: 'text-red-600 dark:text-red-400'
|
||||
}`}>
|
||||
安装路径: {onlineGameDeployResult.installPath}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex space-x-3 p-6 border-t border-gray-200 dark:border-gray-700">
|
||||
{onlineGameDeployComplete ? (
|
||||
<button
|
||||
onClick={handleCloseOnlineGameInstallModal}
|
||||
className="w-full px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded-lg transition-colors flex items-center justify-center space-x-2"
|
||||
>
|
||||
<CheckCircle className="w-4 h-4" />
|
||||
<span>完成</span>
|
||||
</button>
|
||||
) : (
|
||||
<>
|
||||
<button
|
||||
onClick={onlineGameDeploying ? cancelOnlineGameDeployment : handleCloseOnlineGameInstallModal}
|
||||
className="flex-1 px-4 py-2 text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-600 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-500 transition-colors"
|
||||
>
|
||||
{onlineGameDeploying ? '取消部署' : '取消'}
|
||||
</button>
|
||||
<button
|
||||
onClick={startOnlineGameDeployment}
|
||||
disabled={!onlineGameInstallPath.trim() || onlineGameDeploying}
|
||||
className="flex-1 px-4 py-2 bg-green-600 hover:bg-green-700 disabled:bg-gray-400 text-white rounded-lg transition-colors flex items-center justify-center space-x-2"
|
||||
>
|
||||
{onlineGameDeploying ? (
|
||||
<>
|
||||
<Loader className="w-4 h-4 animate-spin" />
|
||||
<span>部署中...</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Download className="w-4 h-4" />
|
||||
<span>开始部署</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 创建整合包实例对话框 */}
|
||||
{showCreateMrpackInstanceModal && mrpackDeployResult && (
|
||||
<div className={`fixed inset-0 bg-black/50 flex items-center justify-center z-50 transition-opacity duration-300 ${
|
||||
|
||||
@@ -591,6 +591,23 @@ class ApiClient {
|
||||
async checkPythonEnvironment() {
|
||||
return this.get('/instances/python/check')
|
||||
}
|
||||
|
||||
// 在线部署API
|
||||
async getOnlineGames() {
|
||||
return this.get('/online-deploy/games')
|
||||
}
|
||||
|
||||
async deployOnlineGame(data: {
|
||||
gameId: string
|
||||
installPath: string
|
||||
socketId?: string
|
||||
}) {
|
||||
return this.post('/online-deploy/deploy', data)
|
||||
}
|
||||
|
||||
async cancelOnlineGameDeployment(deploymentId: string) {
|
||||
return this.post('/online-deploy/cancel', { deploymentId })
|
||||
}
|
||||
}
|
||||
|
||||
// 创建单例实例
|
||||
|
||||
21
server/package-lock.json
generated
21
server/package-lock.json
generated
@@ -8,6 +8,7 @@
|
||||
"name": "gsm3-server",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"adm-zip": "^0.5.10",
|
||||
"archiver": "^6.0.1",
|
||||
"axios": "^1.6.2",
|
||||
"bcryptjs": "^2.4.3",
|
||||
@@ -31,6 +32,7 @@
|
||||
"yauzl": "^2.10.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/adm-zip": "^0.5.5",
|
||||
"@types/archiver": "^6.0.2",
|
||||
"@types/bcryptjs": "^2.4.6",
|
||||
"@types/cors": "^2.8.17",
|
||||
@@ -1482,6 +1484,16 @@
|
||||
"integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/adm-zip": {
|
||||
"version": "0.5.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/adm-zip/-/adm-zip-0.5.7.tgz",
|
||||
"integrity": "sha512-DNEs/QvmyRLurdQPChqq0Md4zGvPwHerAJYWk9l2jCbD1VPpnzRJorOdiq4zsw09NFbYnhfsoEhWtxIzXpn2yw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/archiver": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/archiver/-/archiver-6.0.3.tgz",
|
||||
@@ -1852,6 +1864,15 @@
|
||||
"node": ">= 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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
516
server/src/routes/onlineDeploy.ts
Normal file
516
server/src/routes/onlineDeploy.ts
Normal file
@@ -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<string, any>()
|
||||
|
||||
// 获取当前平台
|
||||
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<boolean> {
|
||||
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
|
||||
Reference in New Issue
Block a user