chore(unity): 固定Unity CLI Loop本地包并移除UnitySkills依赖
This commit is contained in:
@@ -0,0 +1,369 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using UnityEditor;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
/// <summary>
|
||||
/// Runs the prepared wrapper entry point while keeping Undo and cancellation handling consistent.
|
||||
/// </summary>
|
||||
public class CommandRunner : ICompiledCommandInvoker
|
||||
{
|
||||
private readonly CompiledCommandEntryPointResolver _entryPointResolver;
|
||||
private bool _isRunning = false;
|
||||
private CancellationTokenSource _cancellationTokenSource;
|
||||
|
||||
public bool IsRunning => _isRunning;
|
||||
|
||||
public CommandRunner()
|
||||
: this(DynamicCodeServices.CommandEntryPointResolver)
|
||||
{
|
||||
}
|
||||
|
||||
internal CommandRunner(CompiledCommandEntryPointResolver entryPointResolver)
|
||||
{
|
||||
_entryPointResolver = entryPointResolver;
|
||||
}
|
||||
|
||||
private static void LogExecutionError(Exception ex, string correlationId)
|
||||
{
|
||||
VibeLogger.LogError(
|
||||
"command_execution_error",
|
||||
"Command execution failed with exception",
|
||||
new {
|
||||
error = ex.Message,
|
||||
stackTrace = ex.StackTrace
|
||||
},
|
||||
correlationId,
|
||||
"Unexpected execution error occurred",
|
||||
"Investigate execution failures and improve error handling"
|
||||
);
|
||||
}
|
||||
|
||||
private CancellationTokenSource CreateCombinedCancellationTokenSource(ExecutionContext context)
|
||||
{
|
||||
return CancellationTokenSource.CreateLinkedTokenSource(
|
||||
_cancellationTokenSource.Token,
|
||||
context.CancellationToken
|
||||
);
|
||||
}
|
||||
|
||||
public ExecutionResult Execute(ExecutionContext context)
|
||||
{
|
||||
string correlationId = McpConstants.GenerateCorrelationId();
|
||||
if (!TryBeginExecution(out int undoGroup))
|
||||
{
|
||||
return CreateErrorResult(McpConstants.ERROR_MESSAGE_EXECUTION_IN_PROGRESS);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using CancellationTokenSource combinedCts = CreateCombinedCancellationTokenSource(context);
|
||||
return ExecuteInternal(context, combinedCts.Token);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return CreateCancelledResult();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogExecutionError(ex, correlationId);
|
||||
|
||||
return CreateErrorResult(
|
||||
ex.Message,
|
||||
new List<string> { $"Exception: {ex.Message}" });
|
||||
}
|
||||
finally
|
||||
{
|
||||
EndExecution(undoGroup);
|
||||
}
|
||||
}
|
||||
|
||||
public void Cancel()
|
||||
{
|
||||
_cancellationTokenSource?.Cancel();
|
||||
}
|
||||
|
||||
public async Task<ExecutionResult> ExecuteAsync(ExecutionContext context)
|
||||
{
|
||||
string correlationId = McpConstants.GenerateCorrelationId();
|
||||
if (!TryBeginExecution(out int undoGroup))
|
||||
{
|
||||
return CreateErrorResult(McpConstants.ERROR_MESSAGE_EXECUTION_IN_PROGRESS);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using CancellationTokenSource combinedCts = CreateCombinedCancellationTokenSource(context);
|
||||
return await ExecuteInternalAsync(context, combinedCts.Token);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return CreateCancelledResult();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogExecutionError(ex, correlationId);
|
||||
|
||||
return CreateErrorResult(
|
||||
ex.Message,
|
||||
new List<string> { $"Exception: {ex.Message}" });
|
||||
}
|
||||
finally
|
||||
{
|
||||
EndExecution(undoGroup);
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryBeginExecution(out int undoGroup)
|
||||
{
|
||||
undoGroup = -1;
|
||||
if (_isRunning)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_isRunning = true;
|
||||
_cancellationTokenSource = new CancellationTokenSource();
|
||||
undoGroup = Undo.GetCurrentGroup();
|
||||
Undo.SetCurrentGroupName("ExecuteDynamicCode");
|
||||
return true;
|
||||
}
|
||||
|
||||
private void EndExecution(int undoGroup)
|
||||
{
|
||||
Undo.CollapseUndoOperations(undoGroup);
|
||||
_isRunning = false;
|
||||
_cancellationTokenSource?.Dispose();
|
||||
_cancellationTokenSource = null;
|
||||
}
|
||||
|
||||
private static ExecutionResult CreateErrorResult(string errorMessage, List<string> logs = null)
|
||||
{
|
||||
return new ExecutionResult
|
||||
{
|
||||
Success = false,
|
||||
ErrorMessage = errorMessage,
|
||||
ExecutionTime = TimeSpan.Zero,
|
||||
Logs = logs ?? new List<string>()
|
||||
};
|
||||
}
|
||||
|
||||
private static ExecutionResult CreateCancelledResult()
|
||||
{
|
||||
return CreateErrorResult(
|
||||
McpConstants.ERROR_MESSAGE_EXECUTION_CANCELLED,
|
||||
new List<string> { "Execution cancelled" });
|
||||
}
|
||||
|
||||
private static ExecutionResult CreateSuccessResult(string result, List<string> logs = null)
|
||||
{
|
||||
return new ExecutionResult
|
||||
{
|
||||
Success = true,
|
||||
Result = result,
|
||||
ExecutionTime = TimeSpan.Zero, // Will be set by the caller
|
||||
Logs = logs ?? new List<string> { "Execution completed successfully" }
|
||||
};
|
||||
}
|
||||
|
||||
private static object CreateInstance(Type targetType)
|
||||
{
|
||||
return Activator.CreateInstance(targetType);
|
||||
}
|
||||
|
||||
private static object InvokeExecuteMethod(
|
||||
MethodInfo executeMethod,
|
||||
object instance,
|
||||
Dictionary<string, object> parameters,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
object[] callArgs = BuildSupportedArguments(executeMethod, parameters, cancellationToken);
|
||||
return executeMethod.Invoke(instance, callArgs);
|
||||
}
|
||||
|
||||
private ExecutionResult ExecuteInternal(ExecutionContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
if (context.CompiledAssembly == null)
|
||||
{
|
||||
return CreateErrorResult(McpConstants.ERROR_MESSAGE_NO_COMPILED_ASSEMBLY);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Find executable type from assembly (sync only)
|
||||
(Type targetType, MethodInfo executeMethod) = _entryPointResolver.TryFindExecuteMethod(
|
||||
context.CompiledAssembly);
|
||||
|
||||
if (targetType == null || executeMethod == null)
|
||||
{
|
||||
return CreateErrorResult(
|
||||
McpConstants.ERROR_MESSAGE_NO_EXECUTE_METHOD,
|
||||
new List<string> { "Assembly types checked but no Execute method found" });
|
||||
}
|
||||
|
||||
// Create instance
|
||||
object instance = CreateInstance(targetType);
|
||||
if (instance == null)
|
||||
{
|
||||
return CreateErrorResult(McpConstants.ERROR_MESSAGE_FAILED_TO_CREATE_INSTANCE);
|
||||
}
|
||||
|
||||
// Check cancellation
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
try
|
||||
{
|
||||
// Execute method
|
||||
object executionResult = InvokeExecuteMethod(
|
||||
executeMethod,
|
||||
instance,
|
||||
context.Parameters,
|
||||
cancellationToken);
|
||||
|
||||
// Convert result to string
|
||||
string resultString = executionResult?.ToString() ?? "";
|
||||
|
||||
return CreateSuccessResult(resultString);
|
||||
}
|
||||
catch (NotSupportedException ex)
|
||||
{
|
||||
return CreateErrorResult(
|
||||
McpConstants.ERROR_MESSAGE_UNSUPPORTED_SIGNATURE,
|
||||
new List<string> { ex.Message });
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (TargetInvocationException ex)
|
||||
{
|
||||
// Retrieve actual exception
|
||||
Exception innerException = ex.InnerException ?? ex;
|
||||
if (innerException is OperationCanceledException)
|
||||
{
|
||||
throw innerException;
|
||||
}
|
||||
|
||||
return CreateErrorResult(
|
||||
innerException.Message,
|
||||
new List<string>
|
||||
{
|
||||
$"Target invocation exception: {innerException.Message}",
|
||||
$"Stack trace: {innerException.StackTrace}"
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return CreateErrorResult(
|
||||
ex.Message,
|
||||
new List<string>
|
||||
{
|
||||
$"Execution exception: {ex.Message}",
|
||||
$"Stack trace: {ex.StackTrace}"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<ExecutionResult> ExecuteInternalAsync(ExecutionContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
if (context.CompiledAssembly == null)
|
||||
{
|
||||
return CreateErrorResult(McpConstants.ERROR_MESSAGE_NO_COMPILED_ASSEMBLY);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Prefer async ExecuteAsync; fallback to sync Execute
|
||||
(Type asyncType, MethodInfo executeAsyncMethod) = _entryPointResolver.TryFindExecuteAsyncMethod(
|
||||
context.CompiledAssembly);
|
||||
if (asyncType != null && executeAsyncMethod != null)
|
||||
{
|
||||
object instance = CreateInstance(asyncType);
|
||||
if (instance == null)
|
||||
{
|
||||
return CreateErrorResult(McpConstants.ERROR_MESSAGE_FAILED_TO_CREATE_INSTANCE);
|
||||
}
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
object[] callArgs = BuildSupportedArguments(
|
||||
executeAsyncMethod,
|
||||
context.Parameters,
|
||||
cancellationToken);
|
||||
object invoked = executeAsyncMethod.Invoke(instance, callArgs);
|
||||
|
||||
object awaitedResult = await io.github.hatayama.uLoopMCP.AwaitableHelper.AwaitIfNeeded(invoked);
|
||||
string resultString = awaitedResult?.ToString() ?? "";
|
||||
|
||||
return CreateSuccessResult(resultString);
|
||||
}
|
||||
|
||||
// Fallback to sync path if no async method found
|
||||
ExecutionResult syncResult = ExecuteInternal(context, cancellationToken);
|
||||
return syncResult;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (TargetInvocationException ex)
|
||||
{
|
||||
Exception innerException = ex.InnerException ?? ex;
|
||||
if (innerException is OperationCanceledException)
|
||||
{
|
||||
throw innerException;
|
||||
}
|
||||
|
||||
return CreateErrorResult(
|
||||
innerException.Message,
|
||||
new List<string>
|
||||
{
|
||||
$"Target invocation exception: {innerException.Message}",
|
||||
$"Stack trace: {innerException.StackTrace}"
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return CreateErrorResult(
|
||||
ex.Message,
|
||||
new List<string>
|
||||
{
|
||||
$"Execution exception: {ex.Message}",
|
||||
$"Stack trace: {ex.StackTrace}"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private static object[] BuildSupportedArguments(
|
||||
MethodInfo method,
|
||||
Dictionary<string, object> parameters,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
System.Reflection.ParameterInfo[] methodParameters = method.GetParameters();
|
||||
if (methodParameters.Length == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
if (methodParameters.Length == 2 && methodParameters[0].ParameterType == typeof(Dictionary<string, object>) && methodParameters[1].ParameterType == typeof(CancellationToken))
|
||||
{
|
||||
return new object[] { parameters, cancellationToken };
|
||||
}
|
||||
if (methodParameters.Length == 1 && methodParameters[0].ParameterType == typeof(Dictionary<string, object>))
|
||||
{
|
||||
return new object[] { parameters };
|
||||
}
|
||||
if (methodParameters.Length == 1 && methodParameters[0].ParameterType == typeof(CancellationToken))
|
||||
{
|
||||
return new object[] { cancellationToken };
|
||||
}
|
||||
|
||||
throw new NotSupportedException(
|
||||
"Expected Execute/ExecuteAsync overloads with Dictionary<string,object> and/or CancellationToken");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user