diff --git a/client/src/pages/GameDeploymentPage.tsx b/client/src/pages/GameDeploymentPage.tsx index 86786a7..72c894e 100644 --- a/client/src/pages/GameDeploymentPage.tsx +++ b/client/src/pages/GameDeploymentPage.tsx @@ -143,6 +143,9 @@ const GameDeploymentPage: React.FC = () => { const [instanceName, setInstanceName] = useState('') const [instanceDescription, setInstanceDescription] = useState('') const [instanceStartCommand, setInstanceStartCommand] = useState('') + const [instanceStartFile, setInstanceStartFile] = useState('') + const [instanceJarArgs, setInstanceJarArgs] = useState('-Xmx2G -Xms1G') + const [availableStartFiles, setAvailableStartFiles] = useState([]) const [creatingInstance, setCreatingInstance] = useState(false) // 更多游戏部署相关状态 @@ -170,6 +173,10 @@ const GameDeploymentPage: React.FC = () => { const [mrpackDeployProgress, setMrpackDeployProgress] = useState(null) const [mrpackDeployLogs, setMrpackDeployLogs] = useState([]) const [mrpackDeployComplete, setMrpackDeployComplete] = useState(false) + const [mrpackApiSource, setMrpackApiSource] = useState<'official' | 'mirror'>(() => { + // 从localStorage读取保存的选择 + return (localStorage.getItem('mrpackApiSource') as 'official' | 'mirror') || 'official' + }) // 整合包悬停详情状态 const [hoveredMrpack, setHoveredMrpack] = useState(null) @@ -181,6 +188,9 @@ const GameDeploymentPage: React.FC = () => { const [mrpackInstanceName, setMrpackInstanceName] = useState('') const [mrpackInstanceDescription, setMrpackInstanceDescription] = useState('') const [mrpackInstanceStartCommand, setMrpackInstanceStartCommand] = useState('') + const [mrpackInstanceStartFile, setMrpackInstanceStartFile] = useState('') + const [mrpackInstanceJarArgs, setMrpackInstanceJarArgs] = useState('-Xmx2G -Xms1G') + const [mrpackAvailableStartFiles, setMrpackAvailableStartFiles] = useState([]) const [creatingMrpackInstance, setCreatingMrpackInstance] = useState(false) // 在线部署相关状态 @@ -524,6 +534,12 @@ const GameDeploymentPage: React.FC = () => { } } + // 保存整合包API源选择到localStorage + const handleMrpackApiSourceChange = (apiSource: 'official' | 'mirror') => { + setMrpackApiSource(apiSource) + localStorage.setItem('mrpackApiSource', apiSource) + } + // 搜索Minecraft整合包 const searchMrpackModpacks = async () => { if (!mrpackSearchQuery.trim()) { @@ -539,7 +555,8 @@ const GameDeploymentPage: React.FC = () => { setMrpackSearchLoading(true) const response = await apiClient.searchMrpackModpacks({ query: mrpackSearchQuery, - limit: 20 + limit: 20, + apiSource: mrpackApiSource }) if (response.success) { @@ -563,7 +580,7 @@ const GameDeploymentPage: React.FC = () => { const fetchMrpackVersions = async (projectId: string) => { try { setMrpackVersionsLoading(true) - const response = await apiClient.getMrpackProjectVersions(projectId) + const response = await apiClient.getMrpackProjectVersions(projectId, mrpackApiSource) if (response.success) { setMrpackVersions(response.data || []) @@ -637,7 +654,8 @@ const GameDeploymentPage: React.FC = () => { projectId: selectedMrpack.project_id, versionId: selectedMrpackVersion.id, installPath: mrpackInstallPath, - socketId + socketId, + apiSource: mrpackApiSource }) if (response.success) { @@ -811,13 +829,26 @@ const GameDeploymentPage: React.FC = () => { }) // 监听Minecraft下载完成 - socketRef.current.on('minecraft-download-complete', (data) => { + socketRef.current.on('minecraft-download-complete', async (data) => { console.log('收到下载完成事件:', data) if (data.downloadId === currentDownloadId.current) { setMinecraftDownloading(false) setDownloadComplete(true) setDownloadResult(data.data) + // 扫描启动文件 + if (data.data?.targetDirectory) { + const files = await scanStartFiles(data.data.targetDirectory) + setAvailableStartFiles(files) + if (files.length > 0) { + // 自动选择第一个文件 + setInstanceStartFile(files[0]) + // 根据文件类型生成启动命令 + const command = generateStartCommandFromFile(files[0], instanceJarArgs) + setInstanceStartCommand(command) + } + } + addNotification({ type: 'success', title: '下载完成', @@ -863,7 +894,7 @@ const GameDeploymentPage: React.FC = () => { }) // 监听Minecraft整合包部署完成 - socketRef.current.on('more-games-deploy-complete', (data) => { + socketRef.current.on('more-games-deploy-complete', async (data) => { // 检查是否是整合包部署的完成事件 if (data.deploymentId && data.deploymentId.startsWith('mrpack-deploy-')) { setMrpackDeploying(false) @@ -871,13 +902,26 @@ const GameDeploymentPage: React.FC = () => { setMrpackDeployResult(data.data) currentMrpackDeploymentId.current = null // 重置整合包部署ID - // 自动生成启动命令 - if (data.data?.serverType) { - setMrpackInstanceStartCommand(generateStartCommand(data.data.serverType, selectedMrpackJava)) + // 扫描启动文件 + if (data.data?.targetDirectory) { + const files = await scanStartFiles(data.data.targetDirectory) + setMrpackAvailableStartFiles(files) + if (files.length > 0) { + // 自动选择第一个文件 + setMrpackInstanceStartFile(files[0]) + // 根据文件类型生成启动命令 + const command = generateStartCommandFromFile(files[0], mrpackInstanceJarArgs) + setMrpackInstanceStartCommand(command) + } } else { - // 默认启动命令 - const defaultCommand = data.data?.serverJarPath ? `java -jar "${data.data.serverJarPath}"` : 'java -jar server.jar' - setMrpackInstanceStartCommand(selectedMrpackJava !== 'default' ? replaceJavaInCommand(defaultCommand, selectedMrpackJava) : defaultCommand) + // 自动生成启动命令(备用逻辑) + if (data.data?.serverType) { + setMrpackInstanceStartCommand(generateStartCommand(data.data.serverType, selectedMrpackJava)) + } else { + // 默认启动命令 + const defaultCommand = data.data?.serverJarPath ? `java -jar "${data.data.serverJarPath}"` : 'java -jar server.jar' + setMrpackInstanceStartCommand(selectedMrpackJava !== 'default' ? replaceJavaInCommand(defaultCommand, selectedMrpackJava) : defaultCommand) + } } addNotification({ @@ -1180,6 +1224,39 @@ const GameDeploymentPage: React.FC = () => { setHoveredMrpack(null) } + // 扫描目录中的启动文件 + const scanStartFiles = async (directory: string) => { + try { + const response = await apiClient.getFiles(directory) + if (response.success && response.data && response.data.files) { + const files = response.data.files.filter((file: any) => { + const fileName = typeof file === 'string' ? file : file.name + const ext = fileName.toLowerCase() + return ext.endsWith('.jar') || ext.endsWith('.bat') || ext.endsWith('.sh') + }).map((file: any) => typeof file === 'string' ? file : file.name) + return files + } + } catch (error) { + console.error('扫描启动文件失败:', error) + } + return [] + } + + // 根据选择的文件生成启动命令 + const generateStartCommandFromFile = (fileName: string, jarArgs: string = '') => { + if (!fileName) return '' + + const ext = fileName.toLowerCase() + if (ext.endsWith('.jar')) { + return `java ${jarArgs} -jar ${fileName}` + } else if (ext.endsWith('.bat')) { + return fileName + } else if (ext.endsWith('.sh')) { + return `./${fileName}` + } + return fileName + } + // 获取选中的Java可执行文件路径 const getSelectedJavaExecutable = (selectedJava: string) => { if (selectedJava === 'default') { @@ -1571,6 +1648,13 @@ const GameDeploymentPage: React.FC = () => { setCreateInstanceModalAnimating(false) setTimeout(() => { setShowCreateInstanceModal(false) + // 重置表单 + setInstanceName('') + setInstanceDescription('') + setInstanceStartCommand('') + setInstanceStartFile('') + setInstanceJarArgs('') + setAvailableStartFiles([]) }, 300) } @@ -1583,6 +1667,9 @@ const GameDeploymentPage: React.FC = () => { setMrpackInstanceName('') setMrpackInstanceDescription('') setMrpackInstanceStartCommand('') + setMrpackInstanceStartFile('') + setMrpackInstanceJarArgs('') + setMrpackAvailableStartFiles([]) }, 300) } @@ -2678,7 +2765,7 @@ const GameDeploymentPage: React.FC = () => { handleMrpackApiSourceChange(e.target.value as 'official' | 'mirror')} + 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" + > + + + +

+ 镜像源可能提供更快的访问速度,但内容可能略有延迟 +

+ +
{ />
- {/* 启动命令 */} + {/* 启动文件选择 */}
- setInstanceStartCommand(e.target.value)} +
+ + {/* JAR启动参数(仅当选择.jar文件时显示) */} + {instanceStartFile && instanceStartFile.toLowerCase().endsWith('.jar') && ( +
+ + { + setInstanceJarArgs(e.target.value) + // 更新启动命令 + const command = generateStartCommandFromFile(instanceStartFile, e.target.value) + setInstanceStartCommand(command) + }} + 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="例如: -Xmx2G -Xms1G" + /> +
+ )} + + {/* 生成的启动命令(只读显示) */} + {instanceStartCommand && ( +
+ + +
+ )}
@@ -4172,19 +4336,66 @@ const GameDeploymentPage: React.FC = () => { />
- {/* 启动命令 */} + {/* 启动文件选择 */}
- setMrpackInstanceStartCommand(e.target.value)} +
+ + {/* JAR启动参数(仅当选择.jar文件时显示) */} + {mrpackInstanceStartFile && mrpackInstanceStartFile.toLowerCase().endsWith('.jar') && ( +
+ + { + setMrpackInstanceJarArgs(e.target.value) + // 更新启动命令 + const command = generateStartCommandFromFile(mrpackInstanceStartFile, e.target.value) + setMrpackInstanceStartCommand(command) + }} + 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="例如: -Xmx2G -Xms1G" + /> +
+ )} + + {/* 生成的启动命令(只读显示) */} + {mrpackInstanceStartCommand && ( +
+ + +
+ )}
diff --git a/client/src/utils/api.ts b/client/src/utils/api.ts index 0fac5da..85e1ff9 100644 --- a/client/src/utils/api.ts +++ b/client/src/utils/api.ts @@ -413,7 +413,7 @@ class ApiClient { // 文件管理API async getFiles(path: string) { - return this.get('/files', { params: { path } }) + return this.get('/files/list', { params: { path } }) } async uploadFile(file: File, path: string) { @@ -628,6 +628,7 @@ class ApiClient { categories?: string[] versions?: string[] loaders?: string[] + apiSource?: 'official' | 'mirror' } = {}) { const params = new URLSearchParams() if (options.query) params.append('query', options.query) @@ -636,12 +637,17 @@ class ApiClient { if (options.categories) params.append('categories', options.categories.join(',')) if (options.versions) params.append('versions', options.versions.join(',')) if (options.loaders) params.append('loaders', options.loaders.join(',')) - + if (options.apiSource) params.append('apiSource', options.apiSource) + return this.get(`/more-games/mrpack/search?${params.toString()}`) } - async getMrpackProjectVersions(projectId: string) { - return this.get(`/more-games/mrpack/project/${projectId}/versions`) + async getMrpackProjectVersions(projectId: string, apiSource?: 'official' | 'mirror') { + const params = new URLSearchParams() + if (apiSource) params.append('apiSource', apiSource) + + const url = `/more-games/mrpack/project/${projectId}/versions${params.toString() ? `?${params.toString()}` : ''}` + return this.get(url) } async deployMrpack(data: { @@ -654,6 +660,7 @@ class ApiClient { minMemory?: string } socketId?: string + apiSource?: 'official' | 'mirror' }) { return this.post('/more-games/deploy/mrpack', data) } diff --git a/server/src/modules/game/othergame/mrpack-server-api.ts b/server/src/modules/game/othergame/mrpack-server-api.ts index b2e0637..c618554 100644 --- a/server/src/modules/game/othergame/mrpack-server-api.ts +++ b/server/src/modules/game/othergame/mrpack-server-api.ts @@ -101,14 +101,40 @@ export interface ModpackDeployResult { // ==================== Mrpack处理器类 ==================== export class MrpackServerAPI { - private static readonly MODRINTH_API_BASE = 'https://api.modrinth.com/v2'; + private static readonly MODRINTH_API_SOURCES = { + official: 'https://api.modrinth.com/v2', + mirror: 'https://mod.mcimirror.top/modrinth/v2' + }; private static readonly SEARCH_ENDPOINT = '/search'; private tempDir: string; private cancelled: boolean = false; private currentProcess?: ChildProcess; + private apiSource: 'official' | 'mirror'; - constructor(tempDir?: string) { + constructor(tempDir?: string, apiSource: 'official' | 'mirror' = 'official') { this.tempDir = tempDir || path.join(process.cwd(), 'temp-mrpack'); + this.apiSource = apiSource; + } + + /** + * 获取当前使用的API基础URL + */ + private getApiBaseUrl(): string { + return MrpackServerAPI.MODRINTH_API_SOURCES[this.apiSource]; + } + + /** + * 设置API源 + */ + setApiSource(apiSource: 'official' | 'mirror'): void { + this.apiSource = apiSource; + } + + /** + * 获取当前API源 + */ + getApiSource(): 'official' | 'mirror' { + return this.apiSource; } /** @@ -157,7 +183,7 @@ export class MrpackServerAPI { params.append('index', options.index || 'relevance'); const response = await axios.get( - `${MrpackServerAPI.MODRINTH_API_BASE}${MrpackServerAPI.SEARCH_ENDPOINT}?${params.toString()}` + `${this.getApiBaseUrl()}${MrpackServerAPI.SEARCH_ENDPOINT}?${params.toString()}` ); return response.data; @@ -175,7 +201,7 @@ export class MrpackServerAPI { async getProjectVersions(projectId: string): Promise { try { const response = await axios.get( - `${MrpackServerAPI.MODRINTH_API_BASE}/project/${projectId}/version`, + `${this.getApiBaseUrl()}/project/${projectId}/version`, { headers: { 'User-Agent': 'GSM3/1.0.0 (game server manager)' diff --git a/server/src/modules/game/othergame/unified-functions.ts b/server/src/modules/game/othergame/unified-functions.ts index b24e23e..bcd7dd1 100644 --- a/server/src/modules/game/othergame/unified-functions.ts +++ b/server/src/modules/game/othergame/unified-functions.ts @@ -180,6 +180,7 @@ export interface MrpackDeployOptions { deploymentId?: string; options?: any; onProgress?: LogCallback; + apiSource?: 'official' | 'mirror'; } export interface MrpackIndex { @@ -1134,13 +1135,13 @@ async function findFactorioExecutable(extractPath: string): Promise { - const mrpackAPI = new MrpackServerAPI(); +export async function searchMrpackModpacks(options: MrpackSearchOptions = {}, apiSource: 'official' | 'mirror' = 'official'): Promise { + const mrpackAPI = new MrpackServerAPI(undefined, apiSource); return await mrpackAPI.searchModpacks(options); } -export async function getMrpackProjectVersions(projectId: string): Promise { - const mrpackAPI = new MrpackServerAPI(); +export async function getMrpackProjectVersions(projectId: string, apiSource: 'official' | 'mirror' = 'official'): Promise { + const mrpackAPI = new MrpackServerAPI(undefined, apiSource); return await mrpackAPI.getProjectVersions(projectId); } @@ -1148,8 +1149,8 @@ export async function getMrpackProjectVersions(projectId: string): Promise { - const mrpackAPI = new MrpackServerAPI(); +export async function downloadAndParseMrpack(mrpackUrl: string, apiSource: 'official' | 'mirror' = 'official'): Promise { + const mrpackAPI = new MrpackServerAPI(undefined, apiSource); return await mrpackAPI.downloadAndParseMrpack(mrpackUrl); } @@ -1157,7 +1158,7 @@ export async function downloadAndParseMrpack(mrpackUrl: string): Promise { - const { projectId, versionId, mrpackUrl, targetDirectory, onProgress } = options; + const { projectId, versionId, mrpackUrl, targetDirectory, onProgress, apiSource = 'official' } = options; // 创建部署实例 const deployment = globalDeploymentManager.createDeployment('mrpack', targetDirectory, onProgress, options.deploymentId); @@ -1191,7 +1192,9 @@ export async function deployMrpackServer(options: MrpackDeployOptions): Promise< onProgress(`正在请求版本信息: ${versionId}`, 'info'); } - const versionResponse = await axios.get(`https://api.modrinth.com/v2/version/${versionId}`, { + // 根据选择的API源构建URL + const apiBaseUrl = apiSource === 'mirror' ? 'https://mod.mcimirror.top/modrinth/v2' : 'https://api.modrinth.com/v2'; + const versionResponse = await axios.get(`${apiBaseUrl}/version/${versionId}`, { headers: { 'User-Agent': 'GSM3/1.0.0' }, @@ -1233,7 +1236,7 @@ export async function deployMrpackServer(options: MrpackDeployOptions): Promise< } // 使用 MrpackServerAPI 进行部署 - const mrpackAPI = new MrpackServerAPI(); + const mrpackAPI = new MrpackServerAPI(undefined, apiSource); // 设置取消监听 deployment.cancellationToken.onCancelled(() => { diff --git a/server/src/routes/moreGames.ts b/server/src/routes/moreGames.ts index e8ef954..f2d04b5 100644 --- a/server/src/routes/moreGames.ts +++ b/server/src/routes/moreGames.ts @@ -570,8 +570,8 @@ router.get('/version/:gameId', authenticateToken, async (req: Request, res: Resp // 搜索Minecraft整合包 router.get('/mrpack/search', authenticateToken, async (req: Request, res: Response) => { try { - const { query, limit = 20, offset = 0, categories, versions, loaders } = req.query - + const { query, limit = 20, offset = 0, categories, versions, loaders, apiSource = 'official' } = req.query + const searchOptions = { query: query as string, limit: parseInt(limit as string), @@ -580,8 +580,8 @@ router.get('/mrpack/search', authenticateToken, async (req: Request, res: Respon versions: versions ? (versions as string).split(',') : undefined, loaders: loaders ? (loaders as string).split(',') : undefined } - - const result = await searchMrpackModpacks(searchOptions) + + const result = await searchMrpackModpacks(searchOptions, apiSource as 'official' | 'mirror') res.json({ success: true, @@ -603,7 +603,8 @@ router.get('/mrpack/search', authenticateToken, async (req: Request, res: Respon router.get('/mrpack/project/:projectId/versions', authenticateToken, async (req: Request, res: Response) => { try { const { projectId } = req.params - + const { apiSource = 'official' } = req.query + if (!projectId) { return res.status(400).json({ success: false, @@ -611,8 +612,8 @@ router.get('/mrpack/project/:projectId/versions', authenticateToken, async (req: message: '项目ID为必填项' }) } - - const versions = await getMrpackProjectVersions(projectId) + + const versions = await getMrpackProjectVersions(projectId, apiSource as 'official' | 'mirror') res.json({ success: true, @@ -633,7 +634,7 @@ router.get('/mrpack/project/:projectId/versions', authenticateToken, async (req: // 部署Minecraft整合包 router.post('/deploy/mrpack', authenticateToken, async (req: Request, res: Response) => { try { - const { projectId, versionId, installPath, options = {}, socketId } = req.body + const { projectId, versionId, installPath, options = {}, socketId, apiSource = 'official' } = req.body if (!projectId || !versionId || !installPath) { return res.status(400).json({ @@ -684,6 +685,7 @@ router.post('/deploy/mrpack', authenticateToken, async (req: Request, res: Respo targetDirectory: installPath, deploymentId, options, + apiSource: apiSource as 'official' | 'mirror', onProgress: (message, type = 'info') => { if (io && socketId) { io.to(socketId).emit('more-games-deploy-log', {