chore(unity): 固定Unity CLI Loop本地包并移除UnitySkills依赖

This commit is contained in:
Liubing\LB
2026-09-28 18:14:59 +08:00
parent 0eb43b2b40
commit c4ae02def9
952 changed files with 70137 additions and 11 deletions

View File

@@ -0,0 +1,48 @@
using System;
using System.Collections.Generic;
using System.IO;
using UnityEditor;
using UnityEditor.Build;
using UnityEngine;
namespace io.github.hatayama.uLoopMCP
{
// Removes uLoop's prefab-only runtime assembly from Player builds after Editor compilation keeps prefab scripts resolvable.
internal sealed class RuntimeAssemblyBuildFilter : IFilterBuildAssemblies
{
public int callbackOrder => 0;
public string[] OnFilterAssemblies(BuildOptions buildOptions, string[] assemblies)
{
Debug.Assert(assemblies != null, "assemblies must be provided by Unity build pipeline");
if ((buildOptions & BuildOptions.IncludeTestAssemblies) == BuildOptions.IncludeTestAssemblies)
{
// Player test builds need runtime references from test assemblies, so filtering here breaks PlayMode test players.
return assemblies;
}
List<string> filteredAssemblies = new List<string>(assemblies.Length);
for (int i = 0; i < assemblies.Length; i++)
{
string assemblyPath = assemblies[i];
if (IsRuntimeAssembly(assemblyPath))
{
continue;
}
filteredAssemblies.Add(assemblyPath);
}
return filteredAssemblies.ToArray();
}
private static bool IsRuntimeAssembly(string assemblyPath)
{
Debug.Assert(!string.IsNullOrEmpty(assemblyPath), "assemblyPath must be provided by Unity build pipeline");
string fileName = Path.GetFileName(assemblyPath);
return string.Equals(fileName, McpConstants.RUNTIME_ASSEMBLY_FILE_NAME, StringComparison.Ordinal);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 042727ba0224740ea88a475b774caf52
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,296 @@
using UnityEngine;
using System.IO;
using SystemDiagnostics = System.Diagnostics;
namespace io.github.hatayama.uLoopMCP
{
/// <summary>
/// Class responsible for TypeScript server build processing
/// </summary>
public class TypeScriptBuilder
{
private const string NPM_COMMAND_CI = "ci";
private const string NPM_COMMAND_BUILD_BUNDLE = "run build:bundle";
private const string NPM_COMMAND_INSTALL = "install";
/// <summary>
/// Callback for build completion
/// </summary>
/// <param name="success">Whether the build was successful</param>
/// <param name="output">Build output</param>
/// <param name="error">Error output</param>
public delegate void BuildCompleteCallback(bool success, string output, string error);
/// <summary>
/// Build the TypeScript server
/// </summary>
/// <param name="onComplete">Callback for build completion</param>
public void BuildTypeScriptServer(BuildCompleteCallback onComplete = null)
{
string packageBasePath = UnityMcpPathResolver.GetPackageBasePath();
if (string.IsNullOrEmpty(packageBasePath))
{
Debug.LogError("Package base path not found. Cannot build TypeScript server.");
onComplete?.Invoke(false, "", "Package base path not found. Cannot build TypeScript server.");
return;
}
string typeScriptDir = Path.Combine(packageBasePath, McpConstants.TYPESCRIPT_SERVER_DIR);
if (!Directory.Exists(typeScriptDir))
{
Debug.LogError($"TypeScript directory not found: {typeScriptDir}");
onComplete?.Invoke(false, "", $"TypeScript directory not found: {typeScriptDir}");
return;
}
// Get npm path
string npmPath = GetNpmPath();
if (string.IsNullOrEmpty(npmPath))
{
Debug.LogError("npm command not found. Please make sure Node.js and npm are installed.");
onComplete?.Invoke(false, "", "npm command not found");
return;
}
Debug.Log($"Building TypeScript server in: {typeScriptDir}");
Debug.Log($"Using npm at: {npmPath}");
// Run npm ci (strict installation from package-lock.json)
RunCommand(npmPath, NPM_COMMAND_CI, typeScriptDir);
// Run bundle build with esbuild
RunCommand(npmPath, NPM_COMMAND_BUILD_BUNDLE, typeScriptDir);
Debug.Log("TypeScript server build completed.");
onComplete?.Invoke(true, "", "TypeScript server build completed.");
}
/// <summary>
/// Execute npm install
/// </summary>
/// <param name="npmPath">Path to npm</param>
/// <param name="workingDirectory">Working directory</param>
/// <returns>Whether it was successful</returns>
private bool RunNpmInstall(string npmPath, string workingDirectory)
{
Debug.Log("Running npm install...");
SystemDiagnostics.ProcessStartInfo startInfo = new SystemDiagnostics.ProcessStartInfo
{
FileName = npmPath,
Arguments = NPM_COMMAND_INSTALL,
WorkingDirectory = workingDirectory,
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};
// Set PATH environment variable
SetupEnvironmentPath(startInfo, npmPath);
try
{
using (SystemDiagnostics.Process process = SystemDiagnostics.Process.Start(startInfo))
{
if (process == null)
{
Debug.LogError("Failed to start npm install process");
return false;
}
string output = process.StandardOutput.ReadToEnd();
string error = process.StandardError.ReadToEnd();
process.WaitForExit();
if (process.ExitCode == 0)
{
Debug.Log("npm install completed successfully!");
if (!string.IsNullOrEmpty(output))
{
Debug.Log($"Install output:\n{output}");
}
return true;
}
else
{
Debug.LogError($"npm install failed with exit code {process.ExitCode}");
if (!string.IsNullOrEmpty(error))
{
Debug.LogError($"Install error:\n{error}");
}
if (!string.IsNullOrEmpty(output))
{
Debug.LogError($"Install output:\n{output}");
}
return false;
}
}
}
catch (System.Exception ex)
{
Debug.LogError($"Failed to start npm install process: {ex.Message}");
return false;
}
}
/// <summary>
/// Get npm path using NodeEnvironmentResolver
/// </summary>
private string GetNpmPath()
{
Debug.Log("Searching for npm command...");
string npmPath = NodeEnvironmentResolver.FindNpmPath();
if (!string.IsNullOrEmpty(npmPath))
{
Debug.Log($"Found npm at: {npmPath}");
return npmPath;
}
Debug.LogError("npm command not found in any of the expected locations");
return null;
}
/// <summary>
/// Set PATH environment variable (so node command can be found)
/// </summary>
private void SetupEnvironmentPath(SystemDiagnostics.ProcessStartInfo startInfo, string npmPath)
{
NodeEnvironmentResolver.SetupEnvironmentPath(startInfo, npmPath);
Debug.Log($"Updated PATH for npm process");
}
private void RunCommand(string command, string arguments, string workingDirectory)
{
// Security: Validate command and arguments
if (string.IsNullOrEmpty(command) || string.IsNullOrEmpty(arguments) || string.IsNullOrEmpty(workingDirectory))
{
throw new System.ArgumentException("Command, arguments, and working directory cannot be null or empty");
}
// Security: Only allow predefined npm commands
if (!IsValidNpmCommand(arguments))
{
throw new System.ArgumentException($"Invalid npm command: {arguments}. Only predefined commands are allowed.");
}
// Security: Validate working directory path
if (!IsValidWorkingDirectory(workingDirectory))
{
throw new System.ArgumentException($"Invalid working directory: {workingDirectory}");
}
Debug.Log($"Running command: {command} {arguments} in directory: {workingDirectory}");
SystemDiagnostics.ProcessStartInfo startInfo = new SystemDiagnostics.ProcessStartInfo
{
FileName = command,
Arguments = arguments,
WorkingDirectory = workingDirectory,
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};
// Set PATH environment variable
SetupEnvironmentPath(startInfo, GetNpmPath());
try
{
using (SystemDiagnostics.Process process = SystemDiagnostics.Process.Start(startInfo))
{
if (process == null)
{
Debug.LogError($"Failed to start {command} {arguments} process");
return;
}
string output = process.StandardOutput.ReadToEnd();
string error = process.StandardError.ReadToEnd();
process.WaitForExit();
bool success = process.ExitCode == 0;
if (success)
{
Debug.Log($"{command} {arguments} completed successfully!");
if (!string.IsNullOrEmpty(output))
{
Debug.Log($"Output:\n{output}");
}
}
else
{
Debug.LogError($"{command} {arguments} failed with exit code {process.ExitCode}");
if (!string.IsNullOrEmpty(error))
{
Debug.LogError($"Error:\n{error}");
}
if (!string.IsNullOrEmpty(output))
{
Debug.LogError($"Output:\n{output}");
}
}
}
}
catch (System.Exception ex)
{
string errorMsg = $"Failed to run command: {command} {arguments}: {ex.Message}";
Debug.LogError(errorMsg);
Debug.LogError("Make sure npm is installed and available in PATH");
// This is a critical failure for TypeScript server building
throw new System.InvalidOperationException(
$"TypeScript build command failed: {command} {arguments}. " +
"Ensure Node.js and npm are properly installed and accessible from Unity Editor.", ex);
}
}
/// <summary>
/// Security: Validate npm command arguments
/// </summary>
private bool IsValidNpmCommand(string arguments)
{
// Only allow predefined npm commands
return arguments == NPM_COMMAND_CI ||
arguments == NPM_COMMAND_BUILD_BUNDLE ||
arguments == NPM_COMMAND_INSTALL;
}
/// <summary>
/// Security: Validate working directory path
/// </summary>
private bool IsValidWorkingDirectory(string workingDirectory)
{
try
{
// Normalize the path to prevent path traversal attacks
string normalizedPath = Path.GetFullPath(workingDirectory);
// Check if the path exists
if (!Directory.Exists(normalizedPath))
{
return false;
}
// Ensure the path is within the package directory
string packageBasePath = UnityMcpPathResolver.GetPackageBasePath();
if (string.IsNullOrEmpty(packageBasePath))
{
return false;
}
string normalizedPackagePath = Path.GetFullPath(packageBasePath);
return normalizedPath.StartsWith(normalizedPackagePath, System.StringComparison.OrdinalIgnoreCase);
}
catch (System.Exception)
{
return false;
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b231b64c0957e4962bb6ae72fe34ed8f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: