- 安装后自动写入 /etc/wsl.conf (default=root, systemd=false) - Invoke-WSL 改为 --user root 执行所有命令 - 跳过 OOBE 用户创建流程(--user root 启动) - 重启 WSL2 使 wsl.conf 立即生效 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
758 lines
37 KiB
PowerShell
758 lines
37 KiB
PowerShell
#Requires -Version 5.1
|
||
<#
|
||
.SYNOPSIS
|
||
WSL2 + Claude Code CLI + Unity MCP + Rust Token Killer 全栈一键部署脚本
|
||
.DESCRIPTION
|
||
自动完成以下步骤:
|
||
1. 启用 WSL2 功能 & 安装 Ubuntu 发行版
|
||
2. 检测/安装 Windows 本机 Node.js(AI 客户端 MCP 需要)
|
||
3. WSL2 内安装 Node.js LTS(Claude Code CLI 需要)
|
||
4. 安装 Claude Code CLI (@anthropic-ai/claude-code)
|
||
5. 安装 Unity MCP Server(Windows + WSL2 双侧)& 写入各 AI 客户端配置
|
||
6. 配置 Windows 防火墙放行 MCP Bridge 端口
|
||
7. 安装 Rust 工具链 & Token Killer (rtk)
|
||
8. 写入 PowerShell Profile 快捷命令
|
||
.PARAMETER BridgePort
|
||
Unity MCP Bridge 端口,默认 7890
|
||
.PARAMETER InstallDir
|
||
Windows 侧 unity-mcp-server 安装目录
|
||
.PARAMETER UnityHubPath
|
||
Unity Hub 路径(写入 MCP 配置 env)
|
||
.PARAMETER SkipFirewall
|
||
跳过防火墙规则配置
|
||
.PARAMETER SkipWSL
|
||
跳过 WSL2 安装步骤
|
||
.NOTES
|
||
首次安装 WSL2 需以管理员身份运行。
|
||
已有 WSL2 可普通终端运行,加 -SkipWSL 跳过 WSL2 安装检查。
|
||
#>
|
||
param(
|
||
[int] $BridgePort = 7890,
|
||
[string]$InstallDir = "$env:USERPROFILE\unity-mcp-server",
|
||
[string]$UnityHubPath = "C:\Program Files\Unity Hub\Unity Hub.exe",
|
||
[switch]$SkipFirewall,
|
||
[switch]$SkipWSL
|
||
)
|
||
|
||
Set-StrictMode -Version Latest
|
||
$ErrorActionPreference = "Stop"
|
||
|
||
# ──────────────────────────────────────────────────────────────
|
||
# 颜色日志辅助
|
||
# ──────────────────────────────────────────────────────────────
|
||
function Write-Step { param($msg) Write-Host "`n==== $msg ====" -ForegroundColor Cyan }
|
||
function Write-OK { param($msg) Write-Host " [OK] $msg" -ForegroundColor Green }
|
||
function Write-Info { param($msg) Write-Host " [..] $msg" -ForegroundColor DarkGray }
|
||
function Write-Warn { param($msg) Write-Host " [!!] $msg" -ForegroundColor Yellow }
|
||
function Write-Fail { param($msg) Write-Host " [ERR] $msg" -ForegroundColor Red }
|
||
|
||
# ──────────────────────────────────────────────────────────────
|
||
# 加载 .env 配置
|
||
# ──────────────────────────────────────────────────────────────
|
||
$ScriptDir = $PSScriptRoot
|
||
$EnvFile = Join-Path $ScriptDir ".env"
|
||
$EnvExample = Join-Path $ScriptDir ".env.example"
|
||
|
||
if (-not (Test-Path $EnvFile)) {
|
||
if (Test-Path $EnvExample) {
|
||
Copy-Item $EnvExample $EnvFile
|
||
Write-Warn "已从 .env.example 创建 .env,请按需编辑后重新运行"
|
||
Write-Warn " notepad $EnvFile"
|
||
exit 0
|
||
}
|
||
}
|
||
|
||
$Config = @{}
|
||
if (Test-Path $EnvFile) {
|
||
Get-Content $EnvFile | Where-Object { $_ -match "^\s*[^#].*=" } | ForEach-Object {
|
||
$parts = $_ -split "=", 2
|
||
$key = $parts[0].Trim()
|
||
$val = $parts[1].Trim().Trim('"').Trim("'")
|
||
$Config[$key] = $val
|
||
}
|
||
Write-Info "已加载配置:$EnvFile"
|
||
}
|
||
|
||
$ANTHROPIC_API_KEY = if ($Config["ANTHROPIC_API_KEY"]) { $Config["ANTHROPIC_API_KEY"] } else { "" }
|
||
$ANTHROPIC_AUTH_TOKEN = if ($Config["ANTHROPIC_AUTH_TOKEN"]) { $Config["ANTHROPIC_AUTH_TOKEN"] } else { "" }
|
||
$ANTHROPIC_BASE_URL = if ($Config["ANTHROPIC_BASE_URL"]) { $Config["ANTHROPIC_BASE_URL"] } else { "https://api.lmuai.com" }
|
||
$CLAUDE_MODEL = if ($Config["CLAUDE_MODEL"]) { $Config["CLAUDE_MODEL"] } else { "claude-sonnet-4-6" }
|
||
$WSL_DISTRO = if ($Config["WSL_DISTRO"]) { $Config["WSL_DISTRO"] } else { "Ubuntu" }
|
||
$SKIP_WSL_INSTALL = if ($Config["SKIP_WSL_INSTALL"]) { $Config["SKIP_WSL_INSTALL"] } else { "false" }
|
||
|
||
# 判断认证方式:灵眸/中转用 AUTH_TOKEN,官方用 API_KEY
|
||
$UseLmuAuth = ($ANTHROPIC_AUTH_TOKEN -ne "" -and $ANTHROPIC_BASE_URL -ne "https://api.anthropic.com")
|
||
|
||
# ──────────────────────────────────────────────────────────────
|
||
# Banner
|
||
# ──────────────────────────────────────────────────────────────
|
||
Write-Host ""
|
||
Write-Host "╔══════════════════════════════════════════════════════════╗" -ForegroundColor Cyan
|
||
Write-Host "║ WSL2 + Claude Code CLI + Unity MCP + RTK 全栈部署 ║" -ForegroundColor Cyan
|
||
Write-Host "║ AnkleBreaker Unity MCP · WSL2 Mirror Mode ║" -ForegroundColor Cyan
|
||
Write-Host "╚══════════════════════════════════════════════════════════╝" -ForegroundColor Cyan
|
||
Write-Host ""
|
||
|
||
# ══════════════════════════════════════════════════════════════
|
||
# Step 1: WSL2 安装
|
||
# ══════════════════════════════════════════════════════════════
|
||
Write-Step "1/8 WSL2 环境检测 & 安装"
|
||
|
||
$distroInstalled = $false
|
||
try {
|
||
$dl = wsl --list --quiet 2>&1 | ForEach-Object { $_ -replace "`0","" }
|
||
if ($dl -match [regex]::Escape($WSL_DISTRO)) { $distroInstalled = $true }
|
||
} catch {}
|
||
|
||
if ($distroInstalled) {
|
||
Write-OK "WSL2 + $WSL_DISTRO 已安装,跳过"
|
||
} elseif ($SkipWSL -or $SKIP_WSL_INSTALL -eq "true") {
|
||
Write-Warn "跳过 WSL2 安装"
|
||
} else {
|
||
$isAdmin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()
|
||
).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
|
||
if (-not $isAdmin) {
|
||
Write-Fail "安装 WSL2 需要管理员权限,请以管理员身份运行 PowerShell"
|
||
Write-Info "如已安装 WSL2,请加 -SkipWSL 参数或在 .env 设置 SKIP_WSL_INSTALL=true"
|
||
exit 1
|
||
}
|
||
$f1 = Get-WindowsOptionalFeature -Online -FeatureName "Microsoft-Windows-Subsystem-Linux" -ErrorAction SilentlyContinue
|
||
if ($f1.State -ne "Enabled") {
|
||
Enable-WindowsOptionalFeature -Online -FeatureName "Microsoft-Windows-Subsystem-Linux" -NoRestart | Out-Null
|
||
}
|
||
$f2 = Get-WindowsOptionalFeature -Online -FeatureName "VirtualMachinePlatform" -ErrorAction SilentlyContinue
|
||
if ($f2.State -ne "Enabled") {
|
||
Enable-WindowsOptionalFeature -Online -FeatureName "VirtualMachinePlatform" -NoRestart | Out-Null
|
||
}
|
||
wsl --update 2>&1 | Out-Null
|
||
wsl --set-default-version 2 2>&1 | Out-Null
|
||
wsl --install -d $WSL_DISTRO --no-launch 2>&1
|
||
if ($LASTEXITCODE -ne 0) {
|
||
Write-Fail "WSL2 安装失败,系统可能需要重启后重新运行"
|
||
exit 1
|
||
}
|
||
Write-OK "WSL2 $WSL_DISTRO 安装完成"
|
||
Write-Warn "首次安装可能需要重启,重启后重新运行脚本"
|
||
}
|
||
|
||
# ── Step 1b: 首次 OOBE 处理 & 设置 root 为默认用户 ──────────
|
||
# Ubuntu 首次安装会停在 OOBE 要求创建用户;用 --root 启动可跳过
|
||
Write-Info "初始化 WSL2 发行版(确保 root 可用)..."
|
||
$oobeDone = $false
|
||
for ($i = 0; $i -lt 3; $i++) {
|
||
wsl -d $WSL_DISTRO --user root -- bash -c "exit 0" 2>$null
|
||
if ($LASTEXITCODE -eq 0) { $oobeDone = $true; break }
|
||
Start-Sleep 3
|
||
}
|
||
if (-not $oobeDone) {
|
||
# 触发 OOBE 完成(无交互,接受默认)
|
||
wsl -d $WSL_DISTRO -- bash -c "exit 0" 2>$null
|
||
Start-Sleep 5
|
||
}
|
||
|
||
# 写入 /etc/wsl.conf(完整覆盖,避免重复块)
|
||
$wslConfContent = "[boot]`nsystemd=false`n`n[user]`ndefault=root`n"
|
||
wsl -d $WSL_DISTRO --user root -- bash -c "printf '[boot]\nsystemd=false\n\n[user]\ndefault=root\n' > /etc/wsl.conf" 2>$null
|
||
Write-OK "/etc/wsl.conf 已写入 (default=root, systemd=false)"
|
||
|
||
# 重启使 wsl.conf 生效
|
||
wsl --terminate $WSL_DISTRO 2>$null
|
||
Start-Sleep 2
|
||
Write-OK "WSL2 已重启,后续命令将以 root 运行"
|
||
|
||
# ── WSL2 执行辅助函数 ─────────────────────────────────────────
|
||
function Invoke-WSL {
|
||
param([string]$Command, [switch]$IgnoreError)
|
||
|
||
if ($Command -match "`n") {
|
||
# 多行命令:base64 编码后在 bash 内解码执行,完全规避 PowerShell 管道 CRLF 问题
|
||
$CleanCmd = $Command -replace "`r`n","`n" -replace "`r","`n"
|
||
$b64 = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($CleanCmd))
|
||
$result = wsl -d $WSL_DISTRO --user root -- bash -c "echo '$b64' | base64 -d | bash" 2>&1 |
|
||
ForEach-Object { ($_ -replace "`0","").ToString() }
|
||
} else {
|
||
$result = wsl -d $WSL_DISTRO --user root -- bash -c $Command 2>&1 |
|
||
ForEach-Object { ($_ -replace "`0","").ToString() }
|
||
}
|
||
|
||
if ($LASTEXITCODE -ne 0 -and -not $IgnoreError) {
|
||
Write-Fail "WSL 命令失败 (exit $LASTEXITCODE)"
|
||
Write-Fail ($result -join "`n")
|
||
exit 1
|
||
}
|
||
return ($result -join "`n")
|
||
}
|
||
|
||
# 用 exit code 方式测试连通性,避免输出编码干扰
|
||
wsl -d $WSL_DISTRO --user root -- bash -c "exit 0" 2>$null
|
||
if ($LASTEXITCODE -ne 0) {
|
||
Write-Fail "无法访问 WSL2 发行版 '$WSL_DISTRO'"
|
||
exit 1
|
||
}
|
||
Write-OK "WSL2 ($WSL_DISTRO) 连接正常 (root)"
|
||
|
||
# ── Step 1a: 配置 .wslconfig(mirrored 网络模式) ────────────
|
||
$wslCfgPath = "$env:USERPROFILE\.wslconfig"
|
||
$wslCfgContent = @"
|
||
[wsl2]
|
||
networkingMode=mirrored
|
||
dnsTunneling=true
|
||
firewall=true
|
||
autoProxy=true
|
||
"@
|
||
$needRestart = $false
|
||
if (Test-Path $wslCfgPath) {
|
||
$existing = Get-Content $wslCfgPath -Raw
|
||
if ($existing -notmatch "networkingMode=mirrored") {
|
||
Set-Content $wslCfgPath $wslCfgContent -Encoding UTF8
|
||
$needRestart = $true
|
||
Write-OK ".wslconfig 已更新 -> networkingMode=mirrored"
|
||
}
|
||
} else {
|
||
Set-Content $wslCfgPath $wslCfgContent -Encoding UTF8
|
||
$needRestart = $true
|
||
Write-OK ".wslconfig 已创建 -> networkingMode=mirrored"
|
||
}
|
||
if ($needRestart) {
|
||
Write-Info "重启 WSL2 以应用镜像网络模式..."
|
||
wsl --shutdown 2>$null
|
||
Start-Sleep 3
|
||
}
|
||
|
||
# ══════════════════════════════════════════════════════════════
|
||
# Step 2: Windows 本机 Node.js 检查(AI 客户端 MCP 需要)
|
||
# ══════════════════════════════════════════════════════════════
|
||
Write-Step "2/8 Windows Node.js 检查"
|
||
|
||
$winNode = Get-Command node -ErrorAction SilentlyContinue
|
||
if ($winNode) {
|
||
$winNodeVer = & node --version
|
||
Write-OK "Windows Node.js 已安装: $winNodeVer"
|
||
} else {
|
||
Write-Warn "Windows 本机未检测到 Node.js"
|
||
Write-Info "尝试通过 winget 安装 Node.js LTS..."
|
||
$wingetCmd = Get-Command winget -ErrorAction SilentlyContinue
|
||
if ($wingetCmd) {
|
||
winget install --id OpenJS.NodeJS.LTS --silent --accept-source-agreements --accept-package-agreements 2>&1 | Out-Null
|
||
# 刷新 PATH
|
||
$env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" +
|
||
[System.Environment]::GetEnvironmentVariable("Path","User")
|
||
$winNode = Get-Command node -ErrorAction SilentlyContinue
|
||
if ($winNode) {
|
||
Write-OK "Node.js 安装成功: $(& node --version)"
|
||
} else {
|
||
Write-Warn "winget 安装后未找到 node,请手动安装 Node.js 18+:"
|
||
Write-Warn " https://nodejs.org/zh-cn/download"
|
||
Write-Warn " 或: winget install OpenJS.NodeJS.LTS"
|
||
}
|
||
} else {
|
||
Write-Warn "未找到 winget,请手动安装 Node.js 18+:"
|
||
Write-Warn " https://nodejs.org/zh-cn/download"
|
||
}
|
||
}
|
||
|
||
# ══════════════════════════════════════════════════════════════
|
||
# Step 3: WSL2 系统依赖 & Node.js
|
||
# ══════════════════════════════════════════════════════════════
|
||
Write-Step "3/8 WSL2 Node.js LTS"
|
||
|
||
# 检测 WSL2 原生 node(排除通过 WSL interop 调用的 Windows node,路径含 /mnt/)
|
||
$nodeVer = Invoke-WSL @'
|
||
export NVM_DIR="$HOME/.nvm"
|
||
[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"
|
||
_n=$(which node 2>/dev/null)
|
||
if [ -n "$_n" ] && ! echo "$_n" | grep -q '/mnt/'; then
|
||
node --version
|
||
else
|
||
echo MISSING
|
||
fi
|
||
true
|
||
'@ -IgnoreError
|
||
if ($nodeVer -match "v\d+") {
|
||
Write-OK "WSL2 Node.js 已安装 (原生): $($nodeVer.Trim())"
|
||
} else {
|
||
Write-Info "安装 WSL2 Node.js LTS (via nvm)..."
|
||
$installNodeCmd = @'
|
||
export DEBIAN_FRONTEND=noninteractive
|
||
sudo apt-get install -y -qq curl ca-certificates 2>/dev/null
|
||
# 安装 nvm
|
||
export NVM_DIR="$HOME/.nvm"
|
||
if [ ! -d "$NVM_DIR" ]; then
|
||
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash
|
||
fi
|
||
[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"
|
||
# 安装 Node.js LTS
|
||
nvm install --lts
|
||
nvm use --lts
|
||
# 写入 .bashrc(幂等)
|
||
grep -q 'NVM_DIR' ~/.bashrc || cat >> ~/.bashrc << 'NVMEOF'
|
||
|
||
# nvm (Node Version Manager)
|
||
export NVM_DIR="$HOME/.nvm"
|
||
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
|
||
[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion"
|
||
NVMEOF
|
||
node --version
|
||
npm --version
|
||
true
|
||
'@
|
||
Invoke-WSL $installNodeCmd
|
||
Write-OK "WSL2 Node.js 安装完成"
|
||
}
|
||
|
||
# ══════════════════════════════════════════════════════════════
|
||
# Step 4: Claude Code CLI (WSL2)
|
||
# ══════════════════════════════════════════════════════════════
|
||
Write-Step "4/8 Claude Code CLI"
|
||
|
||
# 检测 WSL2 原生 claude(排除通过 WSL interop 调用的 Windows claude.exe,路径含 /mnt/)
|
||
$claudeVer = Invoke-WSL @'
|
||
export NVM_DIR="$HOME/.nvm"
|
||
[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"
|
||
_c=$(which claude 2>/dev/null)
|
||
if [ -n "$_c" ] && ! echo "$_c" | grep -q '/mnt/'; then
|
||
claude --version 2>/dev/null
|
||
else
|
||
echo MISSING
|
||
fi
|
||
true
|
||
'@ -IgnoreError
|
||
if ($claudeVer -notmatch "MISSING") {
|
||
Write-OK "Claude Code 已安装 (WSL2 原生): $($claudeVer.Trim())"
|
||
} else {
|
||
Write-Info "安装 @anthropic-ai/claude-code (WSL2 原生)..."
|
||
$installClaudeCmd = @'
|
||
export NVM_DIR="$HOME/.nvm"
|
||
[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"
|
||
npm install -g @anthropic-ai/claude-code --quiet
|
||
which claude
|
||
claude --version
|
||
true
|
||
'@
|
||
Invoke-WSL $installClaudeCmd
|
||
Write-OK "Claude Code 安装完成"
|
||
}
|
||
|
||
# 写入 Claude 配置(WSL2 侧)— 含 allowedTools 完整白名单(自动信任,无需每次确认)
|
||
$mcpAllowedTools = @(
|
||
# Claude 内置工具
|
||
"Bash","Read","Write","Edit","MultiEdit","Glob","Grep","LS","WebFetch","TodoRead","TodoWrite",
|
||
# Editor State
|
||
"mcp__unity-mcp__unity_editor_ping","mcp__unity-mcp__unity_editor_state","mcp__unity-mcp__unity_project_info",
|
||
# Scene
|
||
"mcp__unity-mcp__unity_scene_info","mcp__unity-mcp__unity_scene_open","mcp__unity-mcp__unity_scene_save",
|
||
"mcp__unity-mcp__unity_scene_new","mcp__unity-mcp__unity_scene_hierarchy","mcp__unity-mcp__unity_scene_stats",
|
||
# GameObject
|
||
"mcp__unity-mcp__unity_gameobject_create","mcp__unity-mcp__unity_gameobject_delete",
|
||
"mcp__unity-mcp__unity_gameobject_info","mcp__unity-mcp__unity_gameobject_set_transform",
|
||
"mcp__unity-mcp__unity_gameobject_duplicate","mcp__unity-mcp__unity_gameobject_set_active",
|
||
"mcp__unity-mcp__unity_gameobject_reparent",
|
||
# Component
|
||
"mcp__unity-mcp__unity_component_add","mcp__unity-mcp__unity_component_remove",
|
||
"mcp__unity-mcp__unity_component_get_properties","mcp__unity-mcp__unity_component_set_property",
|
||
"mcp__unity-mcp__unity_component_set_reference","mcp__unity-mcp__unity_component_batch_wire",
|
||
"mcp__unity-mcp__unity_component_get_referenceable",
|
||
# Asset
|
||
"mcp__unity-mcp__unity_asset_list","mcp__unity-mcp__unity_asset_import",
|
||
"mcp__unity-mcp__unity_asset_delete","mcp__unity-mcp__unity_asset_create_prefab",
|
||
"mcp__unity-mcp__unity_asset_instantiate_prefab",
|
||
# Script & Code
|
||
"mcp__unity-mcp__unity_script_create","mcp__unity-mcp__unity_script_read",
|
||
"mcp__unity-mcp__unity_script_update","mcp__unity-mcp__unity_execute_code",
|
||
# Material
|
||
"mcp__unity-mcp__unity_material_create","mcp__unity-mcp__unity_renderer_set_material",
|
||
# Build & Play Mode
|
||
"mcp__unity-mcp__unity_build","mcp__unity-mcp__unity_play_mode",
|
||
# Console & Compilation
|
||
"mcp__unity-mcp__unity_console_log","mcp__unity-mcp__unity_console_clear",
|
||
"mcp__unity-mcp__unity_get_compilation_errors",
|
||
# Editor Actions
|
||
"mcp__unity-mcp__unity_execute_menu_item","mcp__unity-mcp__unity_undo",
|
||
"mcp__unity-mcp__unity_redo","mcp__unity-mcp__unity_undo_history",
|
||
# Selection & Search
|
||
"mcp__unity-mcp__unity_selection_get","mcp__unity-mcp__unity_selection_set",
|
||
"mcp__unity-mcp__unity_selection_focus_scene_view","mcp__unity-mcp__unity_selection_find_by_type",
|
||
"mcp__unity-mcp__unity_search_by_component","mcp__unity-mcp__unity_search_by_tag",
|
||
"mcp__unity-mcp__unity_search_by_layer","mcp__unity-mcp__unity_search_by_name",
|
||
"mcp__unity-mcp__unity_search_assets","mcp__unity-mcp__unity_search_missing_references",
|
||
# Screenshots & Graphics
|
||
"mcp__unity-mcp__unity_screenshot_game","mcp__unity-mcp__unity_screenshot_scene",
|
||
"mcp__unity-mcp__unity_graphics_scene_capture","mcp__unity-mcp__unity_graphics_game_capture",
|
||
# Prefab
|
||
"mcp__unity-mcp__unity_prefab_info","mcp__unity-mcp__unity_set_object_reference",
|
||
# Packages
|
||
"mcp__unity-mcp__unity_packages_list","mcp__unity-mcp__unity_packages_add",
|
||
"mcp__unity-mcp__unity_packages_remove","mcp__unity-mcp__unity_packages_search",
|
||
"mcp__unity-mcp__unity_packages_info",
|
||
# Queue & Multi-Agent
|
||
"mcp__unity-mcp__unity_queue_info","mcp__unity-mcp__unity_agents_list","mcp__unity-mcp__unity_agent_log",
|
||
# Advanced Tools proxy(200+ 工具通过此代理访问)
|
||
"mcp__unity-mcp__unity_list_advanced_tools","mcp__unity-mcp__unity_advanced_tool",
|
||
# Unity Hub
|
||
"mcp__unity-mcp__unity_hub_list_editors","mcp__unity-mcp__unity_hub_available_releases",
|
||
"mcp__unity-mcp__unity_hub_install_editor","mcp__unity-mcp__unity_hub_install_modules",
|
||
"mcp__unity-mcp__unity_hub_get_install_path","mcp__unity-mcp__unity_hub_set_install_path",
|
||
# Multi-Instance & Project Context
|
||
"mcp__unity-mcp__unity_list_instances","mcp__unity-mcp__unity_select_instance",
|
||
"mcp__unity-mcp__unity_get_project_context"
|
||
)
|
||
$claudeSettingsJson = if ($UseLmuAuth) {
|
||
# 灵眸 / 中转 API:使用 env.ANTHROPIC_AUTH_TOKEN(避免与 ANTHROPIC_API_KEY 冲突)
|
||
[ordered]@{
|
||
env = [ordered]@{
|
||
ANTHROPIC_BASE_URL = $ANTHROPIC_BASE_URL
|
||
ANTHROPIC_AUTH_TOKEN = $ANTHROPIC_AUTH_TOKEN
|
||
API_TIMEOUT_MS = "3000000"
|
||
CLAUDE_CODE_ATTRIBUTION_HEADER = "0"
|
||
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC = "1"
|
||
}
|
||
model = $CLAUDE_MODEL
|
||
allowedTools = $mcpAllowedTools
|
||
} | ConvertTo-Json -Depth 4
|
||
} else {
|
||
# Anthropic 官方:使用 model + allowedTools,API Key 由环境变量注入
|
||
[ordered]@{ model = $CLAUDE_MODEL; allowedTools = $mcpAllowedTools } | ConvertTo-Json -Depth 3
|
||
}
|
||
$writeClaudeSettingsCmd = @"
|
||
mkdir -p ~/.claude
|
||
cat > ~/.claude/settings.json << 'SETTINGS'
|
||
$claudeSettingsJson
|
||
SETTINGS
|
||
true
|
||
"@
|
||
Invoke-WSL $writeClaudeSettingsCmd -IgnoreError | Out-Null
|
||
|
||
# 写入 bash 环境变量(WSL2 侧)
|
||
# 灵眸模式:auth token 已在 settings.json env 块中,清理旧 bashrc 变量避免冲突
|
||
# 官方模式:写入 ANTHROPIC_API_KEY 到 bashrc
|
||
$cleanOldVarsCmd = @'
|
||
sed -i '/ANTHROPIC_API_KEY/d' ~/.bashrc ~/.profile 2>/dev/null || true
|
||
sed -i '/ANTHROPIC_BASE_URL/d' ~/.bashrc ~/.profile 2>/dev/null || true
|
||
sed -i '/CLAUDE_MODEL/d' ~/.bashrc ~/.profile 2>/dev/null || true
|
||
sed -i '/# Claude Code CLI/d' ~/.bashrc ~/.profile 2>/dev/null || true
|
||
true
|
||
'@
|
||
Invoke-WSL $cleanOldVarsCmd -IgnoreError | Out-Null
|
||
|
||
if (-not $UseLmuAuth -and $ANTHROPIC_API_KEY) {
|
||
$profBlock = "export ANTHROPIC_API_KEY='$ANTHROPIC_API_KEY'\n"
|
||
$profBlock += "export ANTHROPIC_BASE_URL='$ANTHROPIC_BASE_URL'\n"
|
||
$profBlock += "export CLAUDE_MODEL='$CLAUDE_MODEL'\n"
|
||
$addEnvCmd = @"
|
||
printf '\n# Claude Code CLI\n$profBlock' >> ~/.bashrc
|
||
true
|
||
"@
|
||
Invoke-WSL $addEnvCmd -IgnoreError | Out-Null
|
||
}
|
||
|
||
# Windows 侧 Claude 配置
|
||
$claudeDir = "$env:USERPROFILE\.claude"
|
||
if (-not (Test-Path $claudeDir)) { New-Item -ItemType Directory -Path $claudeDir | Out-Null }
|
||
$claudeSettingsJson | Set-Content "$claudeDir\settings.json" -Encoding UTF8
|
||
if ($UseLmuAuth) {
|
||
# 灵眸:token 已在 settings.json 中,清除可能冲突的 Windows 用户级变量
|
||
[System.Environment]::SetEnvironmentVariable("ANTHROPIC_API_KEY", $null, "User")
|
||
[System.Environment]::SetEnvironmentVariable("ANTHROPIC_BASE_URL", $null, "User")
|
||
[System.Environment]::SetEnvironmentVariable("CLAUDE_MODEL", $null, "User")
|
||
} elseif ($ANTHROPIC_API_KEY) {
|
||
[System.Environment]::SetEnvironmentVariable("ANTHROPIC_API_KEY", $ANTHROPIC_API_KEY, "User")
|
||
[System.Environment]::SetEnvironmentVariable("ANTHROPIC_BASE_URL", $ANTHROPIC_BASE_URL, "User")
|
||
[System.Environment]::SetEnvironmentVariable("CLAUDE_MODEL", $CLAUDE_MODEL, "User")
|
||
}
|
||
Write-OK "Claude Code 配置已写入(Windows + WSL2)"
|
||
|
||
# ══════════════════════════════════════════════════════════════
|
||
# Step 5: Unity MCP Server 安装
|
||
# ══════════════════════════════════════════════════════════════
|
||
Write-Step "5/8 Unity MCP Server"
|
||
Write-Info "MCP Server: https://github.com/AnkleBreaker-Studio/unity-mcp-server"
|
||
Write-Info "Unity Plugin: https://github.com/AnkleBreaker-Studio/unity-mcp-plugin"
|
||
|
||
# ── 5a. Windows 侧安装(供 Claude Desktop / Cursor / Windsurf / VS Code)────
|
||
Write-Info "安装 Windows 侧 MCP Server -> $InstallDir"
|
||
if (Test-Path "$InstallDir\.git") {
|
||
Write-Info "已存在,执行 git pull..."
|
||
Push-Location $InstallDir
|
||
try { git pull --quiet 2>&1 | Out-Null } finally { Pop-Location }
|
||
} else {
|
||
if (Test-Path $InstallDir) { Remove-Item $InstallDir -Recurse -Force }
|
||
git clone --quiet --depth 1 https://github.com/AnkleBreaker-Studio/unity-mcp-server.git $InstallDir
|
||
}
|
||
|
||
$winNodeExists = (Get-Command node -ErrorAction SilentlyContinue) -ne $null
|
||
if ($winNodeExists) {
|
||
Push-Location $InstallDir
|
||
try { & npm install --prefer-offline --quiet 2>&1 | Out-Null } finally { Pop-Location }
|
||
Write-OK "Windows MCP Server 就绪: $InstallDir"
|
||
} else {
|
||
Write-Warn "Windows Node.js 未就绪,跳过 Windows 侧 npm install(请安装 Node.js 后重新运行)"
|
||
}
|
||
|
||
# ── 5b. WSL2 侧安装(供 Claude Code CLI)─────────────────────
|
||
Write-Info "安装 WSL2 侧 MCP Server..."
|
||
$wslMcpDir = "~/.mcp-servers/unity-mcp-server"
|
||
$wslMcpCmd = @'
|
||
export NVM_DIR="$HOME/.nvm"
|
||
[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"
|
||
mkdir -p $HOME/.mcp-servers
|
||
if [ -d $HOME/.mcp-servers/unity-mcp-server/.git ]; then
|
||
git -C $HOME/.mcp-servers/unity-mcp-server pull --quiet 2>/dev/null || true
|
||
else
|
||
git clone --quiet --depth 1 https://github.com/AnkleBreaker-Studio/unity-mcp-server.git \
|
||
$HOME/.mcp-servers/unity-mcp-server 2>/dev/null \
|
||
|| git -c http.proxy="" -c https.proxy="" clone --quiet --depth 1 \
|
||
https://github.com/AnkleBreaker-Studio/unity-mcp-server.git \
|
||
$HOME/.mcp-servers/unity-mcp-server
|
||
fi
|
||
if [ -d $HOME/.mcp-servers/unity-mcp-server ]; then
|
||
cd $HOME/.mcp-servers/unity-mcp-server
|
||
# 修复可能由 root 遗留的权限问题
|
||
sudo chown -R $(whoami):$(whoami) . 2>/dev/null || true
|
||
npm install --prefer-offline --quiet 2>/dev/null || npm install --quiet
|
||
fi
|
||
echo MCP_WSL_OK
|
||
true
|
||
'@
|
||
Invoke-WSL $wslMcpCmd
|
||
|
||
$wslHome = (Invoke-WSL "echo `$HOME" -IgnoreError).Trim()
|
||
$wslScript = "$wslHome/.mcp-servers/unity-mcp-server/src/index.js"
|
||
Write-OK "WSL2 MCP Server 就绪: $wslScript"
|
||
|
||
# ── 5c. 写入各 AI 客户端 MCP 配置 ───────────────────────────
|
||
$winScript = ($InstallDir -replace "\\","/") + "/src/index.js"
|
||
|
||
$mcpEntry = @{
|
||
command = "node"
|
||
args = @($winScript)
|
||
env = @{
|
||
UNITY_HUB_PATH = $UnityHubPath
|
||
UNITY_BRIDGE_PORT = "$BridgePort"
|
||
}
|
||
}
|
||
|
||
$wslMcpEntry = @{
|
||
command = "wsl"
|
||
args = @("-d", $WSL_DISTRO, "--", "node", $wslScript)
|
||
env = @{
|
||
UNITY_BRIDGE_PORT = "$BridgePort"
|
||
}
|
||
}
|
||
|
||
function Merge-McpConfig {
|
||
param([string]$ConfigFile, [hashtable]$Entry, [string]$Label)
|
||
$dir = Split-Path $ConfigFile
|
||
if (-not (Test-Path $dir)) { New-Item -ItemType Directory -Path $dir -Force | Out-Null }
|
||
# PS5.1 兼容:手动将 PSCustomObject 转为普通 Hashtable
|
||
function ConvertTo-Hashtable($obj) {
|
||
if ($obj -is [System.Management.Automation.PSCustomObject]) {
|
||
$h = @{}
|
||
foreach ($p in $obj.PSObject.Properties) { $h[$p.Name] = ConvertTo-Hashtable $p.Value }
|
||
return $h
|
||
} elseif ($obj -is [System.Collections.IEnumerable] -and $obj -isnot [string]) {
|
||
return @($obj | ForEach-Object { ConvertTo-Hashtable $_ })
|
||
}
|
||
return $obj
|
||
}
|
||
$cfg = @{ mcpServers = @{} }
|
||
if (Test-Path $ConfigFile) {
|
||
try {
|
||
$raw = Get-Content $ConfigFile -Raw | ConvertFrom-Json
|
||
$converted = ConvertTo-Hashtable $raw
|
||
if ($converted -is [hashtable]) { $cfg = $converted }
|
||
} catch {}
|
||
}
|
||
if (-not $cfg.ContainsKey("mcpServers")) { $cfg["mcpServers"] = @{} }
|
||
$cfg["mcpServers"]["unity-mcp"] = $Entry
|
||
$cfg | ConvertTo-Json -Depth 10 | Set-Content $ConfigFile -Encoding UTF8
|
||
Write-OK "$Label -> $ConfigFile"
|
||
}
|
||
|
||
# Claude Desktop(Windows)
|
||
Merge-McpConfig "$env:APPDATA\Claude\claude_desktop_config.json" $mcpEntry "Claude Desktop"
|
||
# Cursor
|
||
Merge-McpConfig "$env:USERPROFILE\.cursor\mcp.json" $mcpEntry "Cursor"
|
||
# Windsurf(两种路径)
|
||
$windsurfDir = if (Test-Path "$env:APPDATA\Windsurf") { "$env:APPDATA\Windsurf" } else { "$env:USERPROFILE\.codeium\windsurf" }
|
||
Merge-McpConfig "$windsurfDir\mcp_config.json" $mcpEntry "Windsurf"
|
||
# VS Code
|
||
Merge-McpConfig "$env:APPDATA\Code\User\mcp.json" $mcpEntry "VS Code"
|
||
# Claude Code CLI(WSL2)——用 `claude mcp add --scope user` 写入全局用户级 MCP 配置
|
||
$wslMcpCfgCmd = @"
|
||
export NVM_DIR="`$HOME/.nvm"
|
||
[ -s "`$NVM_DIR/nvm.sh" ] && . "`$NVM_DIR/nvm.sh"
|
||
# 先移除旧条目(幂等),再添加
|
||
claude mcp remove unity-mcp --scope user 2>/dev/null || true
|
||
claude mcp add --scope user unity-mcp node $wslScript -e UNITY_BRIDGE_PORT=$BridgePort
|
||
echo "Claude Code MCP configured"
|
||
true
|
||
"@
|
||
Invoke-WSL $wslMcpCfgCmd -IgnoreError | Out-Null
|
||
Write-OK "Claude Code CLI (WSL2) MCP -> unity-mcp (node $wslScript)"
|
||
|
||
# ══════════════════════════════════════════════════════════════
|
||
# Step 6: Windows 防火墙放行 MCP Bridge 端口
|
||
# ══════════════════════════════════════════════════════════════
|
||
Write-Step "6/8 Windows 防火墙规则"
|
||
|
||
if ($SkipFirewall) {
|
||
Write-Warn "已跳过防火墙配置(-SkipFirewall)"
|
||
} else {
|
||
$portRange = "$BridgePort-$($BridgePort + 9)"
|
||
$isAdmin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()
|
||
).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
|
||
if ($isAdmin) {
|
||
foreach ($dir in @("Inbound","Outbound")) {
|
||
$name = "Unity MCP Bridge $dir"
|
||
Get-NetFirewallRule -DisplayName $name -ErrorAction SilentlyContinue |
|
||
Remove-NetFirewallRule -ErrorAction SilentlyContinue
|
||
New-NetFirewallRule -DisplayName $name -Direction $dir `
|
||
-LocalPort $portRange -Protocol TCP -Action Allow -Profile Any | Out-Null
|
||
Write-OK "防火墙规则: $name (TCP $portRange)"
|
||
}
|
||
} else {
|
||
Write-Warn "非管理员权限,使用提升权限添加防火墙规则..."
|
||
$cmd = @"
|
||
`$p = '$portRange'
|
||
foreach (`$d in @('Inbound','Outbound')) {
|
||
`$n = "Unity MCP Bridge `$d"
|
||
Get-NetFirewallRule -DisplayName `$n -ErrorAction SilentlyContinue | Remove-NetFirewallRule -ErrorAction SilentlyContinue
|
||
New-NetFirewallRule -DisplayName `$n -Direction `$d -LocalPort `$p -Protocol TCP -Action Allow -Profile Any | Out-Null
|
||
Write-Host "OK: `$n"
|
||
}
|
||
Start-Sleep 1
|
||
"@
|
||
$enc = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($cmd))
|
||
$proc = Start-Process powershell.exe -ArgumentList "-NoProfile -EncodedCommand $enc" `
|
||
-Verb RunAs -Wait -PassThru
|
||
if ($proc.ExitCode -eq 0) {
|
||
Write-OK "防火墙规则已添加 (TCP $portRange)"
|
||
} else {
|
||
Write-Warn "防火墙配置可能未成功,请手动放行 TCP $portRange"
|
||
}
|
||
}
|
||
}
|
||
|
||
# ══════════════════════════════════════════════════════════════
|
||
# Step 7: Rust 工具链 & Token Killer (WSL2)
|
||
# ══════════════════════════════════════════════════════════════
|
||
Write-Step "7/8 Rust & Token Killer"
|
||
|
||
$rustVer = Invoke-WSL ". ~/.cargo/env 2>/dev/null; rustc --version 2>/dev/null || echo MISSING" -IgnoreError
|
||
if ($rustVer -notmatch "MISSING") {
|
||
Write-OK "Rust 已安装: $($rustVer.Trim())"
|
||
} else {
|
||
Write-Info "安装 Rust 工具链..."
|
||
$installRustCmd = @"
|
||
sudo apt-get install -y -qq build-essential pkg-config libssl-dev 2>/dev/null || true
|
||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs -o /tmp/rustup-init.sh
|
||
sh /tmp/rustup-init.sh -y --default-toolchain stable --no-modify-path
|
||
source ~/.cargo/env
|
||
rustc --version
|
||
"@
|
||
Invoke-WSL $installRustCmd
|
||
Invoke-WSL "grep -q 'cargo/env' ~/.bashrc || echo 'source ~/.cargo/env 2>/dev/null || true' >> ~/.bashrc" -IgnoreError | Out-Null
|
||
Invoke-WSL "grep -q 'cargo/env' ~/.profile || echo 'source ~/.cargo/env 2>/dev/null || true' >> ~/.profile" -IgnoreError | Out-Null
|
||
Write-OK "Rust 安装完成"
|
||
}
|
||
|
||
$rtkVer = Invoke-WSL ". ~/.cargo/env 2>/dev/null; rtk --version 2>/dev/null || echo MISSING" -IgnoreError
|
||
if ($rtkVer -notmatch "MISSING") {
|
||
Write-OK "rtk 已安装: $($rtkVer.Trim())"
|
||
} else {
|
||
Write-Info "安装 rtk (Rust Token Killer)..."
|
||
$installRtkCmd = @"
|
||
. ~/.cargo/env 2>/dev/null || true
|
||
CARGO_NET_GIT_FETCH_WITH_CLI=true cargo install --git https://github.com/rtk-ai/rtk 2>&1 | tail -5
|
||
"@
|
||
Invoke-WSL $installRtkCmd -IgnoreError
|
||
$rtkCheck = Invoke-WSL ". ~/.cargo/env 2>/dev/null; rtk --version 2>/dev/null || echo FAILED" -IgnoreError
|
||
if ($rtkCheck -match "FAILED") {
|
||
Write-Warn "rtk 安装失败,可手动运行: cargo install --git https://github.com/rtk-ai/rtk"
|
||
} else {
|
||
Write-OK "rtk 安装成功: $($rtkCheck.Trim())"
|
||
}
|
||
}
|
||
|
||
# rtk init -g:安装 Claude Code PreToolUse hook(幂等)
|
||
Invoke-WSL ". ~/.cargo/env 2>/dev/null; rtk init -g --auto-patch 2>/dev/null || true" -IgnoreError | Out-Null
|
||
Write-OK "rtk hook 已注册 (rtk init -g),重启 Claude Code 后生效"
|
||
|
||
# ══════════════════════════════════════════════════════════════
|
||
# Step 8: PowerShell Profile 配置
|
||
# ══════════════════════════════════════════════════════════════
|
||
Write-Step "8/8 PowerShell Profile"
|
||
|
||
$pwshProfile = "$env:USERPROFILE\Documents\PowerShell\Microsoft.PowerShell_profile.ps1"
|
||
$profileDir = Split-Path $pwshProfile
|
||
if (-not (Test-Path $profileDir)) { New-Item -ItemType Directory -Path $profileDir | Out-Null }
|
||
|
||
$profileBlock = @"
|
||
|
||
# ── Claude Dev Stack (generated by deploy.ps1) ────────────────
|
||
`$env:ANTHROPIC_BASE_URL = "$ANTHROPIC_BASE_URL"
|
||
$(if ($ANTHROPIC_API_KEY) { "`$env:ANTHROPIC_API_KEY = `"$ANTHROPIC_API_KEY`"" } else { "# ANTHROPIC_API_KEY= (配置 .env 后重新运行)" })
|
||
`$env:CLAUDE_MODEL = "$CLAUDE_MODEL"
|
||
function claude-wsl { wsl -d $WSL_DISTRO -- bash -ic 'claude' }
|
||
function unity-mcp-status { Invoke-RestMethod http://127.0.0.1:$BridgePort/api/ping -ErrorAction SilentlyContinue }
|
||
# ─────────────────────────────────────────────────────────────
|
||
"@
|
||
|
||
$existing = if (Test-Path $pwshProfile) { Get-Content $pwshProfile -Raw } else { "" }
|
||
if ($existing -match "Claude Dev Stack") {
|
||
$existing = $existing -replace "(?ms)# ── Claude Dev Stack.*?# ─{60}", $profileBlock
|
||
Set-Content $pwshProfile $existing -Encoding UTF8
|
||
} else {
|
||
Add-Content $pwshProfile $profileBlock -Encoding UTF8
|
||
}
|
||
|
||
$policy = Get-ExecutionPolicy -Scope CurrentUser
|
||
if ($policy -in @("Restricted","Undefined")) {
|
||
Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned -Force
|
||
}
|
||
Write-OK "PowerShell Profile 已配置: $pwshProfile"
|
||
|
||
# ══════════════════════════════════════════════════════════════
|
||
# 安装总结
|
||
# ══════════════════════════════════════════════════════════════
|
||
Write-Host ""
|
||
Write-Host "╔══════════════════════════════════════════════════════════════╗" -ForegroundColor Green
|
||
Write-Host "║ ✅ 全栈部署完成 ║" -ForegroundColor Green
|
||
Write-Host "╚══════════════════════════════════════════════════════════════╝" -ForegroundColor Green
|
||
|
||
$items = @(
|
||
@("WSL2 内核", (Invoke-WSL "uname -r 2>/dev/null" -IgnoreError).Trim()),
|
||
@("Node.js (Win)", (& node --version 2>$null)),
|
||
@("Node.js (WSL)", $(if ($nodeVer -match "v\d+") { $nodeVer.Trim() } else { "" })),
|
||
@("Claude Code", $(if ($claudeVer -notmatch "MISSING") { $claudeVer.Trim() } else { "" })),
|
||
@("Rust", (Invoke-WSL ". ~/.cargo/env && rustc --version 2>/dev/null" -IgnoreError).Trim()),
|
||
@("rtk", (Invoke-WSL ". ~/.cargo/env && rtk --version 2>/dev/null" -IgnoreError).Trim()),
|
||
@("MCP Server", $InstallDir),
|
||
@("Bridge Port", "TCP $BridgePort (防火墙已放行 $BridgePort-$($BridgePort+9))")
|
||
)
|
||
foreach ($it in $items) {
|
||
Write-Host (" {0,-16} {1}" -f $it[0], $(if ($it[1]) { $it[1] } else { "(未安装)" })) -ForegroundColor White
|
||
}
|
||
|
||
Write-Host ""
|
||
Write-Host " ━━━━ 后续步骤 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" -ForegroundColor Cyan
|
||
Write-Host " 1) 在 Unity 项目中安装 MCP Plugin(每个项目一次):" -ForegroundColor White
|
||
Write-Host " Window > Package Manager > + > Add from git URL:" -ForegroundColor DarkGray
|
||
Write-Host " https://github.com/AnkleBreaker-Studio/unity-mcp-plugin.git" -ForegroundColor Yellow
|
||
Write-Host ""
|
||
Write-Host " 2) 打开 Unity 后确认 Bridge 在线(浏览器验证):" -ForegroundColor White
|
||
Write-Host " http://127.0.0.1:$BridgePort/api/ping" -ForegroundColor Yellow
|
||
Write-Host ""
|
||
Write-Host " 3) 重启 AI 客户端(Claude Desktop / Cursor / Windsurf)" -ForegroundColor White
|
||
Write-Host " MCP 配置已自动写入各客户端配置文件" -ForegroundColor DarkGray
|
||
Write-Host ""
|
||
Write-Host " 4) 在 Claude Code (WSL2) 中使用:" -ForegroundColor White
|
||
Write-Host " claude-wsl → claude → /mcp" -ForegroundColor DarkGray
|
||
Write-Host ""
|
||
if (-not $ANTHROPIC_API_KEY -and -not $ANTHROPIC_AUTH_TOKEN) {
|
||
Write-Warn " ⚠ 未设置 API Key,请编辑 .env 后重新运行"
|
||
Write-Warn " 灵眸用户:填写 ANTHROPIC_AUTH_TOKEN=sk-xxx"
|
||
Write-Warn " 官方用户:填写 ANTHROPIC_API_KEY=sk-xxx"
|
||
}
|
||
Write-Host "╚══════════════════════════════════════════════════════════════╝" -ForegroundColor Green
|