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
JavaScript
/**
* 项目管理服务 - 实现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,