UNPKG

sce-tools-mcp

Version:

SCE Tools MCP Server with full Python CLI feature parity - Model Context Protocol server for SCE (Spark Creative Editor) game development

1,180 lines (1,174 loc) 81.3 kB
/** * 项目管理服务 - 实现Python CLI的所有项目管理功能 */ import axios from 'axios'; import { exec } from 'child_process'; import { createHash } from 'crypto'; import { copyFileSync, existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'fs'; import { basename, join } from 'path'; import { promisify } from 'util'; import { getOutputPath, getSceOpenhandsKitPath, getSceOpenhandsKitPathWithInit, getSceTemplatePathWithInit, getSceWorkspacePath, getWasiCoreSDKPathWithInit } from '../utils/path-resolver.js'; import { validateProjectStructure, validateTemplatePath } from '../utils/validation.js'; const execAsync = promisify(exec); export class ProjectManager { constructor(authService, configService, rootsManager) { this.authService = authService; this.configService = configService; this.rootsManager = rootsManager; } /** * 创建新项目 * 对应Python: run_create_project() */ async createProject(projectPath, options = {}) { try { console.error(`🚀 Creating new SCE project at: ${projectPath}`); const config = this.configService.getConfig(); // 优先使用用户指定的模板路径,否则使用自动初始化的默认模板 let templatePath; if (options.template_path || config.template_path) { templatePath = options.template_path || config.template_path; } else { // 自动初始化并获取模板路径 templatePath = await getSceTemplatePathWithInit(this.configService); } const env = options.env || config.env; // 验证模板路径 validateTemplatePath(templatePath); // 检查项目路径是否已存在 if (existsSync(projectPath)) { throw new Error(`Project path already exists: ${projectPath}`); } // 认证 await this.authService.login(env, options.tap_token); // 申请项目名称 const projectResult = await this.authService.callCreateProject(env); if (!projectResult.success) { throw new Error(`Failed to apply SCE project: ${projectResult.project_name}`); } console.error(`✅ SCE project applied: ${projectResult.project_name}`); // 复制模板到项目目录 mkdirSync(projectPath, { recursive: true }); await this.copyDirectory(templatePath, projectPath); // 更新项目配置 await this.updateProjectConfig(projectPath, projectResult.project_name); // 文件名规范化 await this.projectToLower(projectPath); console.error(`🎉 Project created successfully!`); return { success: true, project_name: projectResult.project_name }; } catch (error) { console.error(`❌ Create project failed: ${error}`); return { success: false, project_name: `Error: ${error}` }; } } /** * 上传项目 * 对应Python: run_upload() */ async uploadProject(projectPath, options = {}) { try { console.error(`📤 Uploading project: ${projectPath}`); const config = this.configService.getConfig(); const env = options.env || config.env; // 验证项目结构 validateProjectStructure(projectPath); // 认证 await this.authService.login(env, options.tap_token); // 打包项目 const zipPath = await this.zipProject(projectPath); // 获取项目名称 const projectName = await this.getProjectName(projectPath); if (!projectName) { throw new Error('Could not find project name in map_settings.json'); } // 上传 const result = await this.authService.callUploadPackage(zipPath, projectName); console.error(`🎉 Upload completed!`); return result; } catch (error) { console.error(`❌ Upload failed: ${error}`); throw new Error(`Upload failed: ${error}`); } } /** * 创建新的SCE项目 * 对应Python: run_tapcode_init() */ async create(workspacePath) { const resolvedWorkspacePath = workspacePath || getSceWorkspacePath(this.configService, this.rootsManager); console.error('🌟 === TapCode环境SCE项目初始化 ==='); console.error('⚙️ 正在设置TapTap Code环境...'); try { const openhandsKitPath = getSceOpenhandsKitPath(this.configService); console.error(`📦 SCE OpenHands Kit路径: ${openhandsKitPath}`); console.error(`📁 工作空间路径: ${resolvedWorkspacePath}`); // 步骤1: 申请新项目ID console.error('\n🔄 步骤1: 申请新项目ID...'); const projectId = await this.applyNewProjectId(); console.error(`✅ 新项目ID: ${projectId}`); // 步骤2: 创建项目副本 console.error('\n📦 步骤2: 创建项目副本...'); const projectCopyPath = await this.createProjectCopy(projectId); console.error(`✅ 项目副本路径: ${projectCopyPath}`); // 步骤3: 更新项目副本中的配置文件 console.error('\n⚙️ 步骤3: 更新项目配置...'); await this.updateProjectConfig(projectCopyPath, projectId); console.error(`✅ 项目配置更新完成`); // 步骤3.5: 文件名规范化 console.error('\n🔤 步骤3.5: 文件名规范化...'); await this.projectToLower(projectCopyPath); console.error(`✅ 文件名规范化完成`); // 步骤4: 上传项目(从项目副本) console.error('\n📤 步骤4: 上传项目...'); const uploadResult = await this.uploadProject(projectCopyPath); console.error(`✅ 项目上传完成`); // 步骤5: 拷贝到工作空间目录 console.error('\n📁 步骤5: 拷贝项目到工作空间...'); const copyStatus = await this.copyToWorkspace(projectCopyPath, resolvedWorkspacePath); console.error(`✅ 项目拷贝完成`); // 步骤6: 保存项目绑定信息 console.error('\n💾 步骤6: 保存项目绑定信息...'); await this.saveProjectBinding(resolvedWorkspacePath, projectId, projectCopyPath, copyStatus); console.error(`✅ 项目绑定信息已保存`); console.error('\n🎉 === TapCode环境SCE项目初始化完成 ==='); return { success: true, message: 'TapCode环境SCE项目初始化完成', project_name: projectId, project_copy_path: projectCopyPath, upload_result: uploadResult, workspace_path: resolvedWorkspacePath, copy_status: copyStatus, documentation: { available: true, path: join(resolvedWorkspacePath, 'docs'), important_files: [ 'docs/AI_DEVELOPER_GUIDE.md - ⭐ 最重要:AI开发者一站式指南(索引式导引)', 'docs/AI_QUICK_RULES.md - 🚨 关键:AI必须遵循的核心规则', 'docs/README.md - 📚 WasiCore API文档总索引', 'docs/api-client-reference/ - 📖 客户端API文档路径', 'docs/api-server-reference/ - 📖 服务端API文档路径', 'docs/patterns/ - 💡 8个完整编程模式(含完整代码示例)', 'docs/patterns/Pattern01_SystemInit.md - 游戏系统注册模式', 'docs/patterns/Pattern02_DataDriven.md - 数据驱动对象创建', 'docs/patterns/Pattern03_FluentUI.md - 现代流式UI构建', 'docs/patterns/Pattern04_Events.md - 事件驱动游戏逻辑', 'docs/patterns/Pattern05_Async.md - WebAssembly安全异步编程', 'docs/patterns/Pattern06_SceneCreation.md - 快速场景创建', 'docs/patterns/Pattern07_ErrorHandling.md - 错误处理和调试', 'docs/patterns/Pattern08_Physics.md - 物理系统(仅客户端)', 'docs/api-client-reference/ - 客户端API参考文档(XML格式)', 'docs/api-server-reference/ - 服务端API参考文档(XML格式)', 'docs/dev-guide/ - 详细开发指南和系统架构' ], note: '📚 WasiCore开发文档已生成到工作空间!🚨 **强烈建议立即阅读顺序**: 1️⃣ docs/AI_QUICK_RULES.md (必读规则) → 2️⃣ docs/AI_DEVELOPER_GUIDE.md (索引导引) → 3️⃣ docs/patterns/ (完整代码模式)' } }; } catch (error) { console.error(`❌ TapCode初始化失败: ${error}`); throw new Error(`TapCode初始化失败: ${error}`); } } /** * 调试部署SCE项目 * 对应Python: run_tapcode_debug() */ async debug(workspacePath) { const resolvedWorkspacePath = workspacePath || getSceWorkspacePath(this.configService, this.rootsManager); const outputPath = getOutputPath(this.configService); console.error('🐛 === TapCode环境SCE调试部署 ==='); console.error('⚙️ 正在设置TapTap Code环境...'); try { // 设置WasiCoreSDK路径(自动初始化) const openhandsKitPath = await getSceOpenhandsKitPathWithInit(this.configService); const wasiCoreSDKPath = await getWasiCoreSDKPathWithInit(this.configService); this.configService.updateConfig({ wasi_core_sdk_path: wasiCoreSDKPath }); console.error(`🔧 WASI_CORE_SDK_PATH设置为: ${wasiCoreSDKPath}`); console.error(`📦 SCE OpenHands Kit路径: ${openhandsKitPath}`); console.error(`📁 工作空间路径: ${resolvedWorkspacePath}`); console.error(`📁 输出路径: ${outputPath}`); // 认证 const config = this.configService.getConfig(); await this.authService.login(config.env); // 调试部署逻辑 console.error('\n🏗️ 正在启动调试部署...'); // 查找现有项目副本进行调试编译 const projectBinding = await this.loadProjectBinding(resolvedWorkspacePath); const projectCopyPath = projectBinding.projectCopyPath; console.error(`🔍 使用项目副本进行调试: ${projectCopyPath}`); // 完整的调试部署流程 console.error('\n🔨 步骤1: 在用户工作空间编译项目...'); await this.compileProject(resolvedWorkspacePath); console.error('✅ 项目编译完成'); // 🆕 步骤1.5: 编译完成后拷贝AppBundle和ui回项目副本 console.error('\n🔄 步骤1.5: 拷贝编译结果回项目副本...'); await this.copyBackAfterCompile(resolvedWorkspacePath, projectCopyPath); console.error('✅ 编译结果拷贝完成'); console.error('\n📦 步骤2: 生成引用包...'); const refPackages = await this.generateRefPackages(projectCopyPath); console.error(`✅ 引用包生成完成,共${refPackages.length}个`); console.error('\n🗂️ 步骤3: 打包项目...'); const { clientZipPath, serverZipPath } = await this.zipDebugProject(projectCopyPath, outputPath); console.error('✅ 项目打包完成'); console.error('\n📋 步骤4: 生成项目信息...'); const projectInfo = await this.generateProjectInfo(clientZipPath, serverZipPath, projectCopyPath); console.error('✅ 项目信息生成完成'); console.error('\n🚀 步骤5: 调用tapcode_helper调试部署...'); const debugResult = await this.callStartDebug(projectInfo, refPackages, clientZipPath, serverZipPath, projectCopyPath); console.error('✅ 调试部署完成'); console.error('\n🎉 === TapCode环境SCE调试部署完成 ==='); return { success: true, message: 'TapCode环境SCE调试部署完成', project_copy_path: projectCopyPath, workspace_path: resolvedWorkspacePath, output_path: outputPath }; } catch (error) { // 提供详细的调试失败信息 console.error(`❌ TapCode调试失败详情:`, { message: error.message, stack: error.stack, name: error.name, code: error.code }); const errorMessage = this.formatDebugError(error); throw new Error(`TapCode调试失败: ${errorMessage}`); } } /** * 仅编译项目,不进行部署 * 对应需求:快速验证代码、CI/CD集成、本地开发 */ async compileOnly(workspacePath) { const resolvedWorkspacePath = workspacePath || getSceWorkspacePath(this.configService, this.rootsManager); console.error('🔨 === SCE项目编译 (仅编译模式) ==='); console.error(`📁 工作空间路径: ${resolvedWorkspacePath}`); try { // 设置WasiCoreSDK路径(自动初始化) const wasiCoreSDKPath = await getWasiCoreSDKPathWithInit(this.configService); this.configService.updateConfig({ wasi_core_sdk_path: wasiCoreSDKPath }); console.error(`🔧 WASI_CORE_SDK_PATH设置为: ${wasiCoreSDKPath}`); // 验证编译环境(仅检查编译所需的基本文件) console.error('📋 验证编译环境...'); const gameEntryPath = join(resolvedWorkspacePath, 'GameEntry', 'GameEntry.csproj'); if (!existsSync(gameEntryPath)) { throw new Error(`编译文件缺失: 找不到 GameEntry/GameEntry.csproj 文件,路径: ${gameEntryPath}`); } console.error('✅ 编译环境验证通过'); // 直接调用编译逻辑 console.error('\n🔨 开始编译项目...'); const startTime = Date.now(); await this.compileProject(resolvedWorkspacePath); const duration = Date.now() - startTime; console.error('🎉 === SCE项目编译完成 ==='); const result = { success: true, message: 'SCE项目编译成功完成', details: { project_path: resolvedWorkspacePath, compile_duration: `${duration}ms`, targets: ['Server-Debug', 'Client-Debug'], output_files: [ `${resolvedWorkspacePath}/AppBundle/managed/GameEntry.dll`, `${resolvedWorkspacePath}/ui/AppBundle/managed/GameEntry.dll` ] } }; return result; } catch (error) { // 简化错误处理 - 避免与底层compileProject重复输出 console.error(`❌ 编译失败: ${error.message?.split('\n')[0] || error.toString()}`); const errorMessage = this.formatCompileError(error); throw new Error(`SCE项目编译失败: ${errorMessage}`); } } /** * 格式化编译错误信息 */ formatCompileError(error) { if (error.message && error.message.includes('Compilation failed')) { return `编译失败 - ${error.message}. 请检查项目代码是否有语法错误,以及是否正确设置了开发环境。`; } if (error.message && error.message.includes('Project file not found')) { return `项目文件未找到 - ${error.message}. 请确保项目已正确初始化且包含 GameEntry.csproj 文件。`; } if (error.message && error.message.includes('dotnet binary not found')) { return `dotnet 环境未安装或配置 - ${error.message}. 请安装 .NET SDK 并确保可通过命令行访问。`; } if (error.message && error.message.includes('项目结构验证失败')) { return `项目结构验证失败 - ${error.message}. 请确保这是一个有效的SCE项目目录。`; } return error.message || error.toString(); } /** * 格式化调试错误信息 */ formatDebugError(error) { if (error.message && error.message.includes('Compilation failed')) { return `编译失败 - ${error.message}. 请检查项目代码是否有语法错误,以及是否正确设置了开发环境。`; } if (error.message && error.message.includes('Project file not found')) { return `项目文件未找到 - ${error.message}. 请确保项目已正确初始化且包含 GameEntry.csproj 文件。`; } if (error.message && error.message.includes('dotnet binary not found')) { return `dotnet 环境未安装或配置 - ${error.message}. 请安装 .NET SDK 并确保可通过命令行访问。`; } if (error.message && error.message.includes('读取项目绑定文件失败')) { return `项目绑定文件问题 - ${error.message}. 请重新运行 create 来重新初始化项目。`; } return error.message || error.toString(); } // === 私有辅助方法 === /** * 仅申请新项目ID(不修改配置文件) */ async applyNewProjectId() { const config = this.configService.getConfig(); const env = config.env; // 认证 await this.authService.login(env); // 申请新的项目名称 const projectResult = await this.authService.callCreateProject(env); if (!projectResult.success) { throw new Error(`Failed to apply project ID: ${projectResult.project_name}`); } return projectResult.project_name; } /** * 为项目创建专用副本 * 从sce-template-for-ai复制完整项目内容到projects/项目ID/ */ async createProjectCopy(projectName) { const openhandsKitPath = await getSceOpenhandsKitPathWithInit(this.configService); const projectCopyPath = join(openhandsKitPath, 'projects', projectName); const templatePath = join(openhandsKitPath, 'sce-template-for-ai'); const docsPath = join(openhandsKitPath, 'docs'); console.error(`📦 Creating project copy at: ${projectCopyPath}`); // 清理现有项目副本(如果存在) if (existsSync(projectCopyPath)) { console.error(`🗑️ Removing existing project copy...`); await this.removeDirectory(projectCopyPath); } // 确保项目副本目录存在 mkdirSync(projectCopyPath, { recursive: true }); // 复制完整的sce-template-for-ai内容(不只是3个文件夹) if (existsSync(templatePath)) { console.error(`📂 Copying complete template from: ${templatePath}`); await this.copyDirectory(templatePath, projectCopyPath); } else { throw new Error(`Template path not found: ${templatePath}`); } // docs目录将在创建软链接阶段处理,不在这里复制 console.error(`✅ Project copy created successfully`); return projectCopyPath; } /** * 删除目录的辅助方法 */ async removeDirectory(dirPath) { const { rmdir } = await import('fs/promises'); await rmdir(dirPath, { recursive: true }); } /** * 更新项目配置文件 * 对应Python: update_project_config() */ async updateProjectConfig(projectPath, projectName) { console.error(`⚙️ Updating project configuration with name: ${projectName}`); // 更新map_settings.json const mapSettingsPath = join(projectPath, 'project', 'map_settings.json'); if (existsSync(mapSettingsPath)) { const mapSettings = JSON.parse(readFileSync(mapSettingsPath, 'utf-8')); mapSettings.ProjectName = projectName; writeFileSync(mapSettingsPath, JSON.stringify(mapSettings, null, 4)); console.error(`✅ Updated ProjectName in map_settings.json`); } // 更新config.ini中的score_name const configIniPath = join(projectPath, 'config.ini'); if (existsSync(configIniPath)) { let content = readFileSync(configIniPath, 'utf-8'); const pattern = /(score_name\s*=\s*')[^']*(')/; content = content.replace(pattern, `$1${projectName}$2`); writeFileSync(configIniPath, content); console.error(`✅ Updated score_name in config.ini`); } } /** * 获取项目名称 */ async getProjectName(projectPath) { try { const mapSettingsPath = join(projectPath, 'project', 'map_settings.json'); if (existsSync(mapSettingsPath)) { const mapSettings = JSON.parse(readFileSync(mapSettingsPath, 'utf-8')); return mapSettings.ProjectName; } } catch (error) { console.error(`⚠️ Could not read project name: ${error}`); } return null; } /** * 复制目录(递归) */ async copyDirectory(src, dest) { mkdirSync(dest, { recursive: true }); const entries = readdirSync(src, { withFileTypes: true }); for (const entry of entries) { const srcPath = join(src, entry.name); const destPath = join(dest, entry.name); // 跳过.git目录 if (entry.name === '.git') { continue; } if (entry.isDirectory()) { await this.copyDirectory(srcPath, destPath); } else { copyFileSync(srcPath, destPath); } } } /** * 编译项目 * 对应Python: UploadTask.compile_project() */ async compileProject(projectPath) { const config = this.configService.getConfig(); const dotnetBinary = config.dotnet_path || 'dotnet'; // 设置WASI_CORE_SDK_PATH环境变量 let wasiCoreSDKPath = config.wasi_core_sdk_path; if (!wasiCoreSDKPath) { // 自动检测WasiCoreSDK路径 wasiCoreSDKPath = await getWasiCoreSDKPathWithInit(this.configService); console.error(`🔍 Auto-detected WasiCoreSDK path: ${wasiCoreSDKPath}`); } if (wasiCoreSDKPath) { process.env.WASI_CORE_SDK_PATH = wasiCoreSDKPath; console.error(`⚙️ Set WASI_CORE_SDK_PATH=${wasiCoreSDKPath}`); } console.error(`🔨 Compiling project with: ${dotnetBinary}`); // 编译前的环境检查 await this.validateCompileEnvironment(projectPath, dotnetBinary); try { // Server-Debug编译 const serverCmd = `"${dotnetBinary}" build "${projectPath}/GameEntry/GameEntry.csproj" -c Server-Debug`; console.error(`🔨 Server compile: ${serverCmd}`); const serverResult = await execAsync(serverCmd); if (serverResult.stderr && serverResult.stderr.trim()) { console.error(`⚠️ Server compile warnings: ${serverResult.stderr}`); } // Client-Debug编译 const clientCmd = `"${dotnetBinary}" build "${projectPath}/GameEntry/GameEntry.csproj" -c Client-Debug`; console.error(`🔨 Client compile: ${clientCmd}`); const clientResult = await execAsync(clientCmd); if (clientResult.stderr && clientResult.stderr.trim()) { console.error(`⚠️ Client compile warnings: ${clientResult.stderr}`); } console.error(`✅ Compilation completed`); } catch (error) { // 提供详细的编译错误信息 console.error(`❌ Detailed compilation error:`, { message: error.message, cmd: error.cmd, code: error.code, signal: error.signal, stdout: error.stdout, stderr: error.stderr }); const errorDetails = this.formatCompilationError(error); throw new Error(`Compilation failed: ${errorDetails}`); } } /** * 验证编译环境 */ async validateCompileEnvironment(projectPath, dotnetBinary) { console.error('🔍 Validating compilation environment...'); // 检查 dotnet 是否可用 let dotnetFound = false; let workingDotnetPath = dotnetBinary; // 尝试默认的dotnet路径 const homePath = process.env.HOME || process.env.USERPROFILE || ''; const dotnetPaths = [ dotnetBinary, 'dotnet', 'dotnet.exe', // Windows常见路径 'C:\\Program Files\\dotnet\\dotnet.exe', 'C:\\Program Files (x86)\\dotnet\\dotnet.exe', // Linux/macOS常见路径 '/usr/local/share/dotnet/dotnet', '/usr/share/dotnet/dotnet', '/usr/bin/dotnet', // 用户级安装路径(常见于Docker容器和用户级安装) '/root/.dotnet/dotnet', ...(homePath ? [`${homePath}/.dotnet/dotnet`, `${homePath}/.dotnet/dotnet.exe`] : []), // 更多Linux发行版路径 '/opt/dotnet/dotnet', '/snap/dotnet-sdk/current/dotnet' ].filter(path => path && path !== 'undefined' && path !== 'null'); // 过滤无效路径 console.error(`🔍 Will try ${dotnetPaths.length} dotnet paths, including user directory: ${homePath}`); for (const dotnetPath of dotnetPaths) { try { console.error(`🔍 Trying dotnet at: ${dotnetPath}`); const dotnetVersion = await execAsync(`"${dotnetPath}" --version`); console.error(`✅ .NET version: ${dotnetVersion.stdout.trim()}`); workingDotnetPath = dotnetPath; dotnetFound = true; break; } catch (error) { console.error(`⚠️ dotnet not found at: ${dotnetPath}`); } } if (!dotnetFound) { throw new Error(`dotnet binary not found or not working: ${dotnetBinary}. 🚨 .NET SDK 检测失败解决方案: 1. 📥 安装 .NET SDK: - Windows: https://dotnet.microsoft.com/download - Linux: sudo apt-get install dotnet-sdk-9.0 - macOS: brew install dotnet 2. 🔄 重启MCP服务器进程: 如果agent刚安装了dotnet,MCP服务器进程可能还在使用旧的环境变量。 需要重启AI助手或MCP服务器进程以获取新的PATH环境变量。 3. 🛠️ 手动设置dotnet路径: 在配置文件中指定完整的dotnet路径: ~/.sce/config.yaml: dotnet_path: "C:\\Program Files\\dotnet\\dotnet.exe" # Windows dotnet_path: "/usr/bin/dotnet" # Linux/macOS 4. 🔍 验证安装: 运行命令行测试: dotnet --version ⚠️ 注意:MCP服务器进程继承启动时的环境变量,新安装的程序可能需要重启进程才能生效。`); } // 如果找到了不同的dotnet路径,更新配置 if (workingDotnetPath !== dotnetBinary) { console.error(`🔧 Found working dotnet at: ${workingDotnetPath}`); this.configService.updateConfig({ dotnet_path: workingDotnetPath }); } // 检查项目文件是否存在 const projectFile = join(projectPath, 'GameEntry', 'GameEntry.csproj'); if (!existsSync(projectFile)) { throw new Error(`Project file not found: ${projectFile}`); } console.error(`✅ Project file found: ${projectFile}`); // 检查 WASI_CORE_SDK_PATH if (!process.env.WASI_CORE_SDK_PATH) { console.error('⚠️ WASI_CORE_SDK_PATH not set, compilation may fail'); } else { console.error(`✅ WASI_CORE_SDK_PATH: ${process.env.WASI_CORE_SDK_PATH}`); } } /** * 格式化编译错误信息 */ formatCompilationError(error) { let errorMsg = ''; if (error.cmd) { errorMsg += `Command: ${error.cmd}\n`; } if (error.code !== undefined) { errorMsg += `Exit code: ${error.code}\n`; } if (error.signal) { errorMsg += `Signal: ${error.signal}\n`; } if (error.stdout && error.stdout.trim()) { errorMsg += `STDOUT:\n${error.stdout}\n`; } if (error.stderr && error.stderr.trim()) { errorMsg += `STDERR:\n${error.stderr}\n`; } if (!errorMsg && error.message) { errorMsg = error.message; } return errorMsg || 'Unknown compilation error'; } /** * 打包项目 * 对应Python: UploadTask.zip_project() */ async zipProject(projectPath) { console.error(`📦 Creating project zip...`); const { createWriteStream } = await import('fs'); const archiver = await import('archiver'); const projectName = basename(projectPath); const zipPath = join(projectPath, `${projectName}.zip`); // 根据Python版本的upload_list定义要打包的文件 const uploadList = [ 'AppBundle/managed/GameEntry.dll', 'ui/AppBundle/managed/GameEntry.dll', 'atmosphere', 'editor', 'game_hud', 'i18n', 'info', 'project', 'ref', 'res', 'scene', 'script', 'src', 'table', 'ui/image', 'ui/script', 'ui/src', 'config.ini', 'libs.json', 'project.sce' ]; const output = createWriteStream(zipPath); const archive = archiver.default('zip', { zlib: { level: 9 } }); return new Promise((resolve, reject) => { output.on('close', () => { console.error(`✅ ZIP file created: ${zipPath} (${archive.pointer()} bytes)`); resolve(zipPath); }); archive.on('error', (err) => { console.error(`❌ Archive error: ${err}`); reject(err); }); archive.pipe(output); // 添加文件和目录到ZIP for (const item of uploadList) { const itemPath = join(projectPath, item); if (existsSync(itemPath)) { const stat = statSync(itemPath); if (stat.isFile()) { archive.file(itemPath, { name: item }); console.error(`📄 Added file: ${item}`); } else if (stat.isDirectory()) { archive.directory(itemPath, item); console.error(`📁 Added directory: ${item}`); } } else { console.error(`⚠️ Path not found, skipping: ${item}`); } } archive.finalize(); }); } /** * 文件名规范化 * 对应Python: project_to_lower() */ async projectToLower(projectPath) { console.error(`🔤 Converting file names to lowercase...`); await this.renameToLower(projectPath); console.error(`✅ File name conversion completed`); } /** * 检查路径是否需要跳过重命名 */ shouldSkipPath(filePath) { const normalizedPath = filePath.replace(/\\/g, '/'); const baseName = basename(filePath); // 跳过AppBundle目录本身及其内部所有内容 if (normalizedPath.includes('/AppBundle/') || normalizedPath.endsWith('/AppBundle') || baseName === 'AppBundle') { return true; } // 跳过GameEntry目录本身及其内部所有内容 if (normalizedPath.includes('/GameEntry/') || normalizedPath.endsWith('/GameEntry') || baseName === 'GameEntry') { return true; } // 跳过所有.cs文件(大小写不敏感) if (filePath.toLowerCase().endsWith('.cs')) { return true; } return false; } /** * 递归重命名文件和目录为小写 */ async renameToLower(currentPath) { if (!existsSync(currentPath)) { return; } let items = []; try { items = readdirSync(currentPath); } catch (error) { console.error(`⚠️ Permission denied accessing ${currentPath}: ${error.message}`); return; } // 首先递归处理子目录 for (const item of items) { const itemPath = join(currentPath, item); // 跳过需要跳过的路径 if (this.shouldSkipPath(itemPath)) { continue; } if (lstatSync(itemPath).isDirectory()) { // 递归处理子目录 await this.renameToLower(itemPath); } } // 然后处理当前目录的重命名 for (const item of items) { const itemPath = join(currentPath, item); // 跳过需要跳过的路径 if (this.shouldSkipPath(itemPath)) { continue; } // 检查是否需要重命名 const lowerName = item.toLowerCase(); if (item !== lowerName) { const newPath = join(currentPath, lowerName); // 避免命名冲突 if (existsSync(newPath) && newPath.toLowerCase() !== itemPath.toLowerCase()) { console.error(`⚠️ Cannot rename ${itemPath} to ${newPath}, target already exists`); continue; } try { const { rename } = await import('fs/promises'); await rename(itemPath, newPath); // console.error(`✅ Renamed: ${item} -> ${lowerName}`); } catch (error) { console.error(`⚠️ Failed to rename ${itemPath}: ${error.message}`); } } } } /** * 拷贝项目到工作空间目录 * 替代原来的软链接方式,直接拷贝项目文件到用户工作空间 * @param projectCopyPath 项目副本路径(sce-openhands-kit/projects/项目ID/) * @param workspacePath 用户工作空间路径 * @returns 拷贝状态信息 */ async copyToWorkspace(projectCopyPath, workspacePath) { const resolvedWorkspacePath = workspacePath || getSceWorkspacePath(this.configService, this.rootsManager); console.error(`📁 Copying project from: ${projectCopyPath}`); console.error(`📁 Copying project to: ${resolvedWorkspacePath}`); // 确保workspace目录存在 mkdirSync(resolvedWorkspacePath, { recursive: true }); const copyDirs = ['GameEntry', 'AppBundle', 'ui']; const copyStatus = { method: 'copy', directories: {} }; for (const dirName of copyDirs) { const sourcePath = join(projectCopyPath, dirName); const targetPath = join(resolvedWorkspacePath, dirName); console.error(`📁 Copying: ${sourcePath} -> ${targetPath}`); if (!existsSync(sourcePath)) { console.error(`⚠️ Source directory not found: ${sourcePath}`); copyStatus.directories[dirName] = { success: false, method: 'copy', needsSync: false }; continue; } try { // 如果目标已存在,先删除 if (existsSync(targetPath)) { console.error(`🗑️ Removing existing: ${targetPath}`); await this.removeExisting(targetPath); } // 拷贝目录 await this.copyDirectory(sourcePath, targetPath); console.error(`✅ Copied directory: ${dirName}`); copyStatus.directories[dirName] = { success: true, method: 'copy', needsSync: false }; } catch (error) { console.error(`⚠️ Failed to copy ${dirName}: ${error}`); copyStatus.directories[dirName] = { success: false, method: 'copy', needsSync: false }; } } // 单独处理docs目录(从OpenHands Kit根目录拷贝) try { const openhandsKitPath = getSceOpenhandsKitPath(this.configService); const docsSourcePath = join(openhandsKitPath, 'docs'); const docsTargetPath = join(resolvedWorkspacePath, 'docs'); if (existsSync(docsSourcePath)) { console.error(`📚 Copying docs: ${docsSourcePath} -> ${docsTargetPath}`); // 如果目标已存在,先删除 if (existsSync(docsTargetPath)) { await this.removeExisting(docsTargetPath); } await this.copyDirectory(docsSourcePath, docsTargetPath); console.error(`✅ Copied docs directory`); copyStatus.directories['docs'] = { success: true, method: 'copy', needsSync: false }; } else { console.error(`⚠️ Docs source not found: ${docsSourcePath}`); copyStatus.directories['docs'] = { success: false, method: 'copy', needsSync: false }; } } catch (error) { console.error(`⚠️ Failed to copy docs: ${error}`); copyStatus.directories['docs'] = { success: false, method: 'copy', needsSync: false }; } return copyStatus; } /** * 编译完成后拷贝AppBundle和ui目录回项目副本 * @param workspacePath 用户工作空间路径 * @param projectCopyPath 项目副本路径 */ async copyBackAfterCompile(workspacePath, projectCopyPath) { console.error(`🔄 Copying compiled results back to project copy...`); console.error(` From: ${workspacePath}`); console.error(` To: ${projectCopyPath}`); // 需要拷贝回去的目录 const dirsToSync = ['AppBundle', 'ui']; for (const dirName of dirsToSync) { const sourcePath = join(workspacePath, dirName); const targetPath = join(projectCopyPath, dirName); if (!existsSync(sourcePath)) { console.error(`⚠️ Source directory not found: ${sourcePath}`); continue; } console.error(`🔄 Copying ${dirName}: ${sourcePath} -> ${targetPath}`); try { // 删除项目副本中的旧目录 if (existsSync(targetPath)) { console.error(`🗑️ Removing old ${dirName} in project copy...`); await this.removeExisting(targetPath); } // 从工作空间拷贝最新的编译结果 await this.copyDirectory(sourcePath, targetPath); console.error(`✅ Successfully copied ${dirName} back to project copy`); } catch (error) { console.error(`⚠️ Failed to copy ${dirName} back: ${error}`); // 不抛出异常,继续处理其他目录 } } console.error(`✅ Completed copying compiled results back to project copy`); } /** * 创建工作空间软链接 * 对应Python: create_workspace_links() * @param projectCopyPath 项目副本路径(sce-openhands-kit/projects/项目ID/) * @returns 链接状态信息,包含每个目录的链接类型和是否需要同步 */ async createWorkspaceLinks(projectCopyPath, workspacePath) { const resolvedWorkspacePath = workspacePath || getSceWorkspacePath(this.configService, this.rootsManager); console.error(`🔗 Creating workspace links from: ${projectCopyPath}`); console.error(`🔗 Creating workspace links to: ${resolvedWorkspacePath}`); // 确保workspace目录存在 mkdirSync(resolvedWorkspacePath, { recursive: true }); const linkDirs = ['GameEntry', 'AppBundle', 'ui', 'docs']; const linkStatus = { linkType: 'symlink', directories: {} }; for (const dirName of linkDirs) { let sourcePath; // docs目录从OpenHands Kit根目录链接,其他目录从项目副本链接 if (dirName === 'docs') { const openhandsKitPath = getSceOpenhandsKitPath(this.configService); sourcePath = join(openhandsKitPath, 'docs'); } else { sourcePath = join(projectCopyPath, dirName); // 从项目副本 } const targetPath = join(resolvedWorkspacePath, dirName); // 到工作空间 console.error(`🔗 Linking: ${sourcePath} -> ${targetPath}`); if (!existsSync(sourcePath)) { console.error(`⚠️ Source directory not found: ${sourcePath}`); linkStatus.directories[dirName] = { success: false, method: 'none', needsSync: false }; continue; } // 如果目标已存在,先删除 if (existsSync(targetPath) || this.isSymlink(targetPath)) { console.error(`🗑️ Removing existing: ${targetPath}`); await this.removeExisting(targetPath); } try { if (process.platform === 'win32') { // Windows: 分级降级策略 const result = await this.createWindowsLinkWithFallback(sourcePath, targetPath, dirName); linkStatus.directories[dirName] = result; // 更新全局链接类型(使用最保守的方法) if (result.method === 'hardcopy' && linkStatus.linkType !== 'hardcopy') { linkStatus.linkType = 'hardcopy'; } else if (result.method === 'junction' && linkStatus.linkType === 'symlink') { linkStatus.linkType = 'junction'; } } else { // Unix使用ln const cmd = `ln -sf "${sourcePath}" "${targetPath}"`; await execAsync(cmd); console.error(`✅ Created symlink: ${dirName}`); linkStatus.directories[dirName] = { success: true, method: 'symlink', needsSync: false }; } } catch (error) { console.error(`⚠️ Failed to create link for ${dirName}: ${error}`); linkStatus.directories[dirName] = { success: false, method: 'failed', needsSync: false }; } } return linkStatus; } /** * Windows分级降级链接创建策略 */ async createWindowsLinkWithFallback(sourcePath, targetPath, dirName) { // 方法1: 尝试Junction (推荐,不需要管理员权限) try { // Windows mklink 严格要求反斜杠路径分隔符,并且需要规范化连续的反斜杠 const normalizedSourcePath = sourcePath.replace(/\//g, '\\').replace(/\\+/g, '\\'); const normalizedTargetPath = targetPath.replace(/\//g, '\\').replace(/\\+/g, '\\'); console.error(`🔧 Path normalization: "${sourcePath}" -> "${normalizedSourcePath}"`); console.error(`🔧 Path normalization: "${targetPath}" -> "${normalizedTargetPath}"`); await execAsync(`mklink /J "${normalizedTargetPath}" "${normalizedSourcePath}"`); console.error(`✅ Created junction: ${dirName}`); return { success: true, method: 'junction', needsSync: false }; } catch (junctionError) { console.error(`⚠️ Junction failed for ${dirName}: ${junctionError instanceof Error ? junctionError.message : String(junctionError)}`); } // 方法2: 硬拷贝备选 (直接的回退) try { await this.createHardCopy(sourcePath, targetPath); console.error(`✅ Created hardcopy: ${dirName} (fallback method)`); console.error(` 💡 Changes won't sync automatically. Re-run create after major changes.`); return { success: true, method: 'hardcopy', needsSync: true }; } catch (copyError) { console.error(`⚠️ Hardcopy failed for ${dirName}: ${copyError instanceof Error ? copyError.message : String(copyError)}`); return { success: false, method: 'failed', needsSync: false }; } } /** * 创建硬拷贝(递归复制目录) */ async createHardCopy(sourcePath, targetPath) { const { promisify } = await import('util'); const { exec } = await import('child_process'); const execAsync = promisify(exec); if (process.platform === 'win32') { // Windows使用robocopy,也需要规范化路径和连续反斜杠 const normalizedSourcePath = sourcePath.replace(/\//g, '\\').replace(/\\+/g, '\\'); const normalizedTargetPath = targetPath.replace(/\//g, '\\').replace(/\\+/g, '\\'); await execAsync(`robocopy "${normalizedSourcePath}" "${normalizedTargetPath}" /E /NFL /NDL /NJH /NJS`); } else { // Unix使用cp await execAsync(`cp -r "${sourcePath}" "${targetPath}"`); } } /** * 同步硬拷贝目录(从工作空间同步回项目副本) */ async syncHardCopyBack(workspacePath, projectCopyPath, directories) { console.error(`🔄 开始同步硬拷贝目录...`); for (const [dirName, status] of Object.entries(directories)) { if (status.method === 'hardcopy' && status.needsSync) { const workspaceDir = join(workspacePath, dirName); const projectDir = join(projectCopyPath, dirName); console.error(`🔄 同步 ${dirName}: ${workspaceDir} -> ${projectDir}`); try { // 先备份项目副本中的原始目录 const backupDir = `${projectDir}.backup.${Date.now()}`; if (existsSync(projectDir)) { await this.createHardCopy(projectDir, backupDir); console.error(`📦 备份原始目录到: ${backupDir}`); } // 删除项目副本中的旧目录 if (existsSync(projectDir)) { await this.removeExisting(projectDir); } // 从工作空间复制最新版本 await this.createHardCopy(workspaceDir, projectDir); console.error(`✅ 同步完成: ${dirName}`); } catch (error) { console.error(`⚠️ 同步失败 ${dirName}: ${error}`); throw error; } } } console.error(`✅ 硬拷贝目录同步完成`); } /** * 检查是否为符号链接 */ isSymlink(path) { try { return lstatSync(path).isSymbolicLink(); } catch { return false; } } /** * 删除现有文件/目录/符号链接 */ async removeExisting(path) { try { if (this.isSymlink(path)) { // 符号链接直接删除 const { unlink } = await import('fs/promises'); await unlink(path); } else if (existsSync(path)) { // 普通文件或目录 const stat = statSync(path); if (stat.isDirectory()) { await this.removeDirectory(path); } else { const { unlink } = await import('fs/promises'); await unlink(path); } } } catch (error) { console.error(`⚠️ Failed to remove existing: ${path}, ${error}`); } } /** * 生成引用包列表 * 对应Python: ProjectTask.generate_ref_packages() */ async generateRefPackages(projectPath) { console.error('📋 Generating reference packages...'); try { // 步骤1: 获取全部资源引用 const fullRefs = await this.getFullRefs(); // 步骤2: 加载本地表格引用 const refPaths = await this.loadTableRefs(projectPath); // 步骤3: 加载版本信息 const aimVersion = await this.loadAimVersion(projectPath); // 步骤4: 处理引用路径 const refMap = {}; for (const refPath of refPaths) { const pathParts = refPath.split('/'); // 特殊处理 characters1 路径 if (pathParts.length > 1 && pathParts[1] === 'characters1') { if (pathParts.length > 2 && pathParts[2] === '_user') { if (pathParts.length > 3) { refMap[pathParts[3].toLowerCase()] = pathParts.slice(0, 4).join('/'); } } else { if (pathParts.length > 2) { refMap[pathParts[2].toLowerCase()] = pathParts.slice(0, 3).join('/'); } } } else { // 普通路径处理 let level = fullRefs; for (let i = 1; i < pathParts.length; i++) { const part = pathParts[i]; let baseName; if (i === pathParts.length - 1) { // 最后一部分,去掉扩展名 baseName = part.split('.')[0]; } else { baseName = part; } if (level && level.sub && level.sub[baseName]) { level = level.sub[baseName]; if (level.packageName) { if (aimVersion[level.packageName]) { refMap[level.packageName] = { path: level.ref, version: aimVersion[level.packageName] }; } else { refMap[level.packageName] = level.ref; } break; } } else { break; } } } } // 步骤5: 生成最终的引用包名(带哈希) const refPackages = []; for (const [key, value] of Object.entries(refMap)) { let resPath; if (typeof value === 'object' && value.path) { resPath = value.path; } else { resPath = value; } // 获取父目录路径并转小写 const parentPath = resPath.split('/').slice(0, -1).join('/').toLowerCase(); // 计算MD5哈希并转换为36进制(取前3位) let hashValue = this.hashTo36(this.getMd5Hash(parentPath)).substring(0, 3); // 特殊处理:mob -> mon if (hashValue === 'mob') { hashValue = 'mon'; } refPackages.push(`${key}_${hashValue}`); } console.error(`📋 Generated ${refPackages.length} reference packages`); return refPackages; } catch (error) { console.error(`❌ Reference packages generation failed: ${error}`); throw new Error(`Failed to generate reference packages: ${error instanceof Error ? error.message : String(error)}. This is critical for the debug deployment process.`); } } /** * 获取全部资源引用(通过tapcode helper API) */ async getFullRefs() { try { const tapcodeHelperUrl = this.configService.getTapCodeHelperUrl(); console.error("getFullRefs tapcode_helper_url:", tapcodeHelperUrl); const url = `${tapcodeHelperUrl}/api/v1/tap-code-helper/query-all-resource-list`; // 直接使用axios发送请求 const headers = this.authService.buildSceHeaders(); const response = await axios.post(url, {}, { headers: headers, proxy: false }); if (response.status !== 200) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const responseData = response.data; if (responseData.result !== 0) { throw new Error(`API call failed: ${responseData.message}`); } const rows = responseData.resource_list; const root = { name: 'Res', sub: {}, ref: null, packageName: null }; for (const row of rows) { let path = row.path.toLowerCase(); if (path.startsWith('res/')) { path = 'Res/' + path.substring(4); } else if (path === 'res') { path = 'Res/'; } const fullPath = path + '/' + row.alias.toLowerCase(); const parts = fullPath.split('/').filter(p => p !== 'Res' && p.length > 0); let level = root; for (const part of parts) { if (!level.sub[part]) { level.sub[part] = { name: part,