Files
2026-07-13 12:49:17 +08:00

269 lines
9.1 KiB
C#

using System;
using System.Collections.Concurrent;
using System.IO;
using System.Linq;
using System.Text;
using UnityEditor;
using UnityEngine;
namespace MCPForUnity.Editor.Setup
{
public class McpForUnitySkillInstaller : EditorWindow
{
private const string RepoUrlKey = "UnityMcpSkillSync.RepoUrl";
private const string BranchKey = "UnityMcpSkillSync.Branch";
private const string CliKey = "UnityMcpSkillSync.Cli";
private const string InstallDirKey = "UnityMcpSkillSync.InstallDir";
private const string CodexCli = "codex";
private const string ClaudeCli = "claude";
private static readonly string[] BranchOptions = { "beta", "main" };
private static readonly string[] CliOptions = { CodexCli, ClaudeCli };
private string _repoUrl;
private string _targetBranch;
private string _cliType;
private string _installDir;
private Vector2 _scroll;
private volatile bool _isRunning;
private readonly ConcurrentQueue<string> _pendingLogs = new();
private readonly StringBuilder _logBuilder = new(4096);
public static void OpenWindow()
{
GetWindow<McpForUnitySkillInstaller>("Unity MCP Skill Install(Sync)");
}
private void OnEnable()
{
var userHome = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
_repoUrl = EditorPrefs.GetString(RepoUrlKey, "https://github.com/CoplayDev/unity-mcp");
_targetBranch = EditorPrefs.GetString(BranchKey, "beta");
if (!BranchOptions.Contains(_targetBranch))
{
_targetBranch = "beta";
}
_cliType = EditorPrefs.GetString(CliKey, CodexCli);
if (!CliOptions.Contains(_cliType))
{
_cliType = CodexCli;
}
_installDir = EditorPrefs.GetString(InstallDirKey, GetDefaultInstallDir(userHome, _cliType));
EditorApplication.update += OnEditorUpdate;
}
private void OnDisable()
{
EditorApplication.update -= OnEditorUpdate;
EditorPrefs.SetString(RepoUrlKey, _repoUrl);
EditorPrefs.SetString(BranchKey, _targetBranch);
EditorPrefs.SetString(CliKey, _cliType);
EditorPrefs.SetString(InstallDirKey, _installDir);
}
private void OnGUI()
{
FlushPendingLogs();
EditorGUILayout.HelpBox("Sync Unity MCP Skill to the latest on the selected branch and output the changed file list.", MessageType.Info);
EditorGUILayout.Space(4f);
EditorGUILayout.LabelField("Config", EditorStyles.boldLabel);
using (new EditorGUI.DisabledScope(_isRunning))
{
_repoUrl = EditorGUILayout.TextField("Repo URL", _repoUrl);
var branchIndex = Array.IndexOf(BranchOptions, _targetBranch);
if (branchIndex < 0)
{
branchIndex = 0;
}
var selectedBranchIndex = EditorGUILayout.Popup("Branch", branchIndex, BranchOptions);
_targetBranch = BranchOptions[selectedBranchIndex];
var cliIndex = Array.IndexOf(CliOptions, _cliType);
if (cliIndex < 0)
{
cliIndex = 0;
}
var selectedCliIndex = EditorGUILayout.Popup("CLI", cliIndex, CliOptions);
if (selectedCliIndex != cliIndex)
{
var previousCli = _cliType;
_cliType = CliOptions[selectedCliIndex];
TryApplyCliDefaultInstallPath(previousCli, _cliType);
}
_installDir = EditorGUILayout.TextField("Install Dir", _installDir);
}
EditorGUILayout.Space(8f);
EditorGUILayout.BeginHorizontal();
using (new EditorGUI.DisabledScope(_isRunning))
{
if (GUILayout.Button($"Sync Latest ({_targetBranch})", GUILayout.Height(32f)))
{
AppendLineImmediate("Sync task queued...");
AppendLineImmediate("Will use GitHub API to read the remote directory tree and perform incremental sync (no repository clone).");
RunSyncLatest();
}
}
if (GUILayout.Button("Clear Log", GUILayout.Width(100f), GUILayout.Height(32f)))
{
_logBuilder.Clear();
while (_pendingLogs.TryDequeue(out _))
{
}
}
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space(8f);
EditorGUILayout.LabelField("Output", EditorStyles.boldLabel);
_scroll = EditorGUILayout.BeginScrollView(_scroll);
EditorGUILayout.TextArea(_logBuilder.ToString(), GUILayout.ExpandHeight(true));
EditorGUILayout.EndScrollView();
}
private void OnEditorUpdate()
{
var changed = FlushPendingLogs();
if (_isRunning || changed)
{
Repaint();
}
}
private void RunSyncLatest()
{
if (_isRunning)
{
return;
}
_isRunning = true;
SkillSyncService.SyncAsync(_repoUrl, _installDir, _targetBranch,
line => _pendingLogs.Enqueue($"[{DateTime.Now:HH:mm:ss}] {SanitizeLogLine(line)}"),
result =>
{
_isRunning = false;
if (result.Success)
{
_pendingLogs.Enqueue($"[{DateTime.Now:HH:mm:ss}] Sync complete: +{result.Added} ~{result.Updated} -{result.Deleted}");
}
else
{
_pendingLogs.Enqueue($"[{DateTime.Now:HH:mm:ss}] [ERROR] {result.Error}");
}
});
}
private void TryApplyCliDefaultInstallPath(string previousCli, string currentCli)
{
var userHome = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
var previousDefaultInstall = GetDefaultInstallDir(userHome, previousCli);
var currentDefaultInstall = GetDefaultInstallDir(userHome, currentCli);
if (string.IsNullOrWhiteSpace(_installDir) || PathsEqual(_installDir, previousDefaultInstall))
{
_installDir = currentDefaultInstall;
}
}
private static string GetDefaultInstallDir(string userHome, string cliType)
{
var baseDir = IsClaudeCli(cliType) ? ".claude" : ".codex";
return Path.Combine(userHome, baseDir, "skills/unity-mcp-skill");
}
private static bool IsClaudeCli(string cliType)
{
return string.Equals(cliType, ClaudeCli, StringComparison.Ordinal);
}
private static bool PathsEqual(string left, string right)
{
if (string.IsNullOrWhiteSpace(left) || string.IsNullOrWhiteSpace(right))
{
return false;
}
try
{
return string.Equals(
SkillSyncService.ExpandPath(left),
SkillSyncService.ExpandPath(right),
StringComparison.Ordinal);
}
catch
{
return false;
}
}
private void AppendLineImmediate(string line)
{
var sanitized = SanitizeLogLine(line);
if (string.IsNullOrWhiteSpace(sanitized))
{
return;
}
_logBuilder.AppendLine($"[{DateTime.Now:HH:mm:ss}] {sanitized}");
_scroll.y = float.MaxValue;
Repaint();
}
private bool FlushPendingLogs()
{
var hasNewLine = false;
while (_pendingLogs.TryDequeue(out var line))
{
_logBuilder.AppendLine(line);
hasNewLine = true;
}
if (hasNewLine)
{
_scroll.y = float.MaxValue;
}
return hasNewLine;
}
private static string SanitizeLogLine(string line)
{
if (string.IsNullOrEmpty(line))
{
return string.Empty;
}
var sb = new StringBuilder(line.Length);
var inEscape = false;
foreach (var ch in line)
{
if (inEscape)
{
if (ch >= '@' && ch <= '~')
{
inEscape = false;
}
continue;
}
if (ch == '\u001b')
{
inEscape = true;
continue;
}
if (ch == '\t' || (ch >= ' ' && ch != 127))
{
sb.Append(ch);
}
}
return sb.ToString().Trim();
}
}
}