#Requires -Version 5.1 <# .SYNOPSIS Windows 原生 Claude Code CLI + GitHub Copilot CLI + MCP + Rust Token Killer 一键部署脚本 .DESCRIPTION 不依赖 WSL2,全部组件原生运行在 Windows 上。完成以下步骤: 1. Node.js LTS 检测 / 安装(winget) 2. Claude Code CLI (@anthropic-ai/claude-code) 安装 3. GitHub CLI + Copilot CLI 扩展安装(winget + gh extension) 4. Unity MCP Server 安装 & 写入各 AI 客户端 MCP 配置 5. Godot MCP Pro 安装(本地包)& 写入各 AI 客户端 MCP 配置 6. Rust 工具链 + RTK (Rust Token Killer) 安装 7. RTK hook 注册到 Claude Code + Copilot Instructions 生成 8. PowerShell Profile 快捷命令写入 .PARAMETER BridgePort Unity MCP Bridge 端口,默认 7890 .PARAMETER SkipFirewall 跳过防火墙规则配置 .NOTES 防火墙规则需要管理员权限,其余步骤普通用户即可运行。 脚本可重复运行(幂等),已安装的组件自动跳过。 #> param( [int] $BridgePort = 7890, [string]$GodotMcpVersion = "godot-mcp-pro-v1.14.1", [switch]$SkipFirewall ) 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 } } $Cfg = @{} 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("'") $Cfg[$key] = $val } Write-Info "已加载配置:$EnvFile" } $ANTHROPIC_API_KEY = $Cfg["ANTHROPIC_API_KEY"] ?? "" $ANTHROPIC_AUTH_TOKEN = $Cfg["ANTHROPIC_AUTH_TOKEN"] ?? "" $ANTHROPIC_BASE_URL = $Cfg["ANTHROPIC_BASE_URL"] ?? "https://api.lmuai.com" $CLAUDE_MODEL = $Cfg["CLAUDE_MODEL"] ?? "claude-sonnet-4-6" $BRIDGE_PORT_CFG = $Cfg["BRIDGE_PORT"] ?? "$BridgePort" $UnityHubPath = $Cfg["UNITY_HUB_PATH"] ?? "C:\Program Files\Unity Hub\Unity Hub.exe" $UnityMcpInstallDir = $Cfg["UNITY_MCP_INSTALL_DIR"] ?? "$env:USERPROFILE\unity-mcp-server" $GodotMcpPackageDir = $Cfg["GODOT_MCP_PACKAGE_DIR"] ?? "." # 参数 BridgePort 优先于 .env if ($BridgePort -eq 7890 -and $BRIDGE_PORT_CFG -ne "7890") { $BridgePort = [int]$BRIDGE_PORT_CFG } # 判断认证方式 $UseLmuAuth = ($ANTHROPIC_AUTH_TOKEN -ne "" -and $ANTHROPIC_BASE_URL -ne "https://api.anthropic.com") # ────────────────────────────────────────────────────────────── # Banner # ────────────────────────────────────────────────────────────── Write-Host "" Write-Host "╔══════════════════════════════════════════════════════════════╗" -ForegroundColor Cyan Write-Host "║ Windows Native: Claude Code + Copilot CLI + MCP + RTK ║" -ForegroundColor Cyan Write-Host "║ Unity MCP · Godot MCP Pro · Rust Token Killer ║" -ForegroundColor Cyan Write-Host "╚══════════════════════════════════════════════════════════════╝" -ForegroundColor Cyan Write-Host "" # ────────────────────────────────────────────────────────────── # 辅助:刷新当前进程 PATH # ────────────────────────────────────────────────────────────── function Refresh-Path { $env:Path = [System.Environment]::GetEnvironmentVariable("Path", "Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path", "User") } # ────────────────────────────────────────────────────────────── # 辅助:winget 安装(静默) # ────────────────────────────────────────────────────────────── function Install-Winget { param([string]$Id, [string]$DisplayName) # pwsh (Core) 里 winget 不在 PATH,尝试固定路径 $wgPath = (Get-Command winget -ErrorAction SilentlyContinue)?.Source if (-not $wgPath) { $wgPath = "$env:LOCALAPPDATA\Microsoft\WindowsApps\winget.exe" if (-not (Test-Path $wgPath)) { $wgPath = $null } } if (-not $wgPath) { Write-Warn "未找到 winget,请手动安装 $DisplayName" return $false } Write-Info "winget install $Id ..." & $wgPath install --id $Id --silent --accept-source-agreements --accept-package-agreements 2>&1 | Out-Null Refresh-Path return $true } # ────────────────────────────────────────────────────────────── # 辅助:写入 / 合并 MCP 配置文件(JSON) # ────────────────────────────────────────────────────────────── function Merge-McpConfig { param( [string] $ConfigFile, [hashtable] $Entry, [string] $Label, [string] $ServerName = "unity-mcp" ) $dir = Split-Path $ConfigFile if (-not (Test-Path $dir)) { New-Item -ItemType Directory -Path $dir -Force | Out-Null } 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"][$ServerName] = $Entry $cfg | ConvertTo-Json -Depth 10 | Set-Content $ConfigFile -Encoding UTF8 Write-OK "$Label -> $ConfigFile" } # ────────────────────────────────────────────────────────────── # 辅助:写入 VS Code settings.json 中的 mcp.servers(VS Code 专用格式) # VS Code Copilot Agent 读取 settings.json -> "mcp" -> "servers" # 而非 mcp.json 中的 "mcpServers"(那是 Claude Desktop / Cursor 格式) # ────────────────────────────────────────────────────────────── function Merge-VsCodeMcpSettings { param( [string] $ServerName, [hashtable] $Entry, [string] $Label ) $settingsFile = "$env:APPDATA\Code\User\settings.json" $dir = Split-Path $settingsFile if (-not (Test-Path $dir)) { New-Item -ItemType Directory -Path $dir -Force | Out-Null } function ConvertTo-OrderedHashtable($obj) { if ($obj -is [System.Management.Automation.PSCustomObject]) { $h = [ordered]@{} foreach ($p in $obj.PSObject.Properties) { $h[$p.Name] = ConvertTo-OrderedHashtable $p.Value } return $h } elseif ($obj -is [System.Collections.IEnumerable] -and $obj -isnot [string]) { return @($obj | ForEach-Object { ConvertTo-OrderedHashtable $_ }) } return $obj } $cfg = [ordered]@{} if (Test-Path $settingsFile) { try { $raw = Get-Content $settingsFile -Raw | ConvertFrom-Json $cfg = ConvertTo-OrderedHashtable $raw } catch { Write-Warn "settings.json 解析失败,将新建 mcp 节点:$_" } } if (-not $cfg.ContainsKey("mcp")) { $cfg["mcp"] = [ordered]@{} } if (-not $cfg["mcp"].ContainsKey("servers")) { $cfg["mcp"]["servers"] = [ordered]@{} } $cfg["mcp"]["servers"][$ServerName] = $Entry $cfg | ConvertTo-Json -Depth 15 | Set-Content $settingsFile -Encoding UTF8 Write-OK "$Label -> $settingsFile [mcp.servers.$ServerName]" } # ══════════════════════════════════════════════════════════════ # Step 1: Node.js LTS # ══════════════════════════════════════════════════════════════ Write-Step "1/8 Node.js LTS" $nodeCmd = Get-Command node -ErrorAction SilentlyContinue if ($nodeCmd) { Write-OK "Node.js 已安装: $(& node --version)" } else { Write-Info "通过 winget 安装 Node.js LTS..." $ok = Install-Winget "OpenJS.NodeJS.LTS" "Node.js LTS" $nodeCmd = Get-Command node -ErrorAction SilentlyContinue if ($nodeCmd) { Write-OK "Node.js 安装完成: $(& node --version)" } else { Write-Warn "未能自动安装 Node.js,请手动安装后重新运行:" Write-Warn " https://nodejs.org/zh-cn/download 或 winget install OpenJS.NodeJS.LTS" } } # ══════════════════════════════════════════════════════════════ # Step 2: Claude Code CLI # ══════════════════════════════════════════════════════════════ Write-Step "2/8 Claude Code CLI" $claudeCmd = Get-Command claude -ErrorAction SilentlyContinue if ($claudeCmd) { Write-OK "Claude Code 已安装: $(& claude --version 2>&1 | Select-Object -First 1)" } else { if (Get-Command node -ErrorAction SilentlyContinue) { Write-Info "安装 @anthropic-ai/claude-code ..." & npm install -g @anthropic-ai/claude-code --quiet 2>&1 | Out-Null Refresh-Path $claudeCmd = Get-Command claude -ErrorAction SilentlyContinue if ($claudeCmd) { Write-OK "Claude Code 安装完成: $(& claude --version 2>&1 | Select-Object -First 1)" } else { Write-Warn "安装后未找到 claude 命令,请重开终端后重试" } } else { Write-Warn "Node.js 未就绪,跳过 Claude Code 安装" } } # ── 写入 Claude Code 配置(settings.json)────────────────── $mcpAllowedTools = @( "Bash","Read","Write","Edit","MultiEdit","Glob","Grep","LS","WebFetch","TodoRead","TodoWrite", # Unity MCP "mcp__unity-mcp__unity_editor_ping","mcp__unity-mcp__unity_editor_state","mcp__unity-mcp__unity_project_info", "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", "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", "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", "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", "mcp__unity-mcp__unity_script_create","mcp__unity-mcp__unity_script_read", "mcp__unity-mcp__unity_script_update","mcp__unity-mcp__unity_execute_code", "mcp__unity-mcp__unity_material_create","mcp__unity-mcp__unity_renderer_set_material", "mcp__unity-mcp__unity_build","mcp__unity-mcp__unity_play_mode", "mcp__unity-mcp__unity_console_log","mcp__unity-mcp__unity_console_clear", "mcp__unity-mcp__unity_get_compilation_errors", "mcp__unity-mcp__unity_execute_menu_item","mcp__unity-mcp__unity_undo", "mcp__unity-mcp__unity_redo","mcp__unity-mcp__unity_undo_history", "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", "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", "mcp__unity-mcp__unity_prefab_info","mcp__unity-mcp__unity_set_object_reference", "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", "mcp__unity-mcp__unity_queue_info","mcp__unity-mcp__unity_agents_list","mcp__unity-mcp__unity_agent_log", "mcp__unity-mcp__unity_list_advanced_tools","mcp__unity-mcp__unity_advanced_tool", "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", "mcp__unity-mcp__unity_list_instances","mcp__unity-mcp__unity_select_instance", "mcp__unity-mcp__unity_get_project_context", # Godot MCP Pro "mcp__godot-mcp-pro__get_project_info","mcp__godot-mcp-pro__get_filesystem_tree", "mcp__godot-mcp-pro__search_files","mcp__godot-mcp-pro__search_in_files", "mcp__godot-mcp-pro__get_project_settings","mcp__godot-mcp-pro__get_project_statistics", "mcp__godot-mcp-pro__uid_to_project_path","mcp__godot-mcp-pro__project_path_to_uid", "mcp__godot-mcp-pro__get_scene_tree","mcp__godot-mcp-pro__get_scene_file_content", "mcp__godot-mcp-pro__get_scene_exports","mcp__godot-mcp-pro__get_scene_dependencies", "mcp__godot-mcp-pro__create_scene","mcp__godot-mcp-pro__open_scene", "mcp__godot-mcp-pro__save_scene","mcp__godot-mcp-pro__add_scene_instance", "mcp__godot-mcp-pro__get_node_properties","mcp__godot-mcp-pro__get_node_groups", "mcp__godot-mcp-pro__get_signals","mcp__godot-mcp-pro__find_nodes_in_group", "mcp__godot-mcp-pro__find_nodes_by_type","mcp__godot-mcp-pro__find_signal_connections", "mcp__godot-mcp-pro__find_node_references", "mcp__godot-mcp-pro__add_node","mcp__godot-mcp-pro__duplicate_node", "mcp__godot-mcp-pro__move_node","mcp__godot-mcp-pro__rename_node", "mcp__godot-mcp-pro__update_property","mcp__godot-mcp-pro__add_resource", "mcp__godot-mcp-pro__set_anchor_preset", "mcp__godot-mcp-pro__connect_signal","mcp__godot-mcp-pro__disconnect_signal", "mcp__godot-mcp-pro__set_node_groups", "mcp__godot-mcp-pro__batch_set_property","mcp__godot-mcp-pro__cross_scene_set_property", "mcp__godot-mcp-pro__list_scripts","mcp__godot-mcp-pro__read_script", "mcp__godot-mcp-pro__get_open_scripts","mcp__godot-mcp-pro__validate_script", "mcp__godot-mcp-pro__create_script","mcp__godot-mcp-pro__edit_script", "mcp__godot-mcp-pro__attach_script", "mcp__godot-mcp-pro__get_editor_errors","mcp__godot-mcp-pro__get_output_log", "mcp__godot-mcp-pro__get_editor_screenshot","mcp__godot-mcp-pro__get_editor_performance", "mcp__godot-mcp-pro__clear_output","mcp__godot-mcp-pro__reload_plugin", "mcp__godot-mcp-pro__reload_project", "mcp__godot-mcp-pro__read_resource","mcp__godot-mcp-pro__get_resource_preview", "mcp__godot-mcp-pro__create_resource","mcp__godot-mcp-pro__edit_resource", "mcp__godot-mcp-pro__read_shader","mcp__godot-mcp-pro__get_shader_params", "mcp__godot-mcp-pro__create_shader","mcp__godot-mcp-pro__edit_shader", "mcp__godot-mcp-pro__assign_shader_material","mcp__godot-mcp-pro__set_shader_param", "mcp__godot-mcp-pro__list_animations","mcp__godot-mcp-pro__get_animation_info", "mcp__godot-mcp-pro__create_animation","mcp__godot-mcp-pro__add_animation_track", "mcp__godot-mcp-pro__set_animation_keyframe", "mcp__godot-mcp-pro__get_animation_tree_structure", "mcp__godot-mcp-pro__create_animation_tree","mcp__godot-mcp-pro__add_state_machine_state", "mcp__godot-mcp-pro__add_state_machine_transition", "mcp__godot-mcp-pro__set_blend_tree_node","mcp__godot-mcp-pro__set_tree_parameter", "mcp__godot-mcp-pro__get_audio_bus_layout","mcp__godot-mcp-pro__get_audio_info", "mcp__godot-mcp-pro__add_audio_bus","mcp__godot-mcp-pro__set_audio_bus", "mcp__godot-mcp-pro__add_audio_bus_effect","mcp__godot-mcp-pro__add_audio_player", "mcp__godot-mcp-pro__get_input_actions","mcp__godot-mcp-pro__set_input_action", "mcp__godot-mcp-pro__get_physics_layers","mcp__godot-mcp-pro__get_collision_info", "mcp__godot-mcp-pro__set_physics_layers", "mcp__godot-mcp-pro__add_mesh_instance","mcp__godot-mcp-pro__setup_lighting", "mcp__godot-mcp-pro__set_material_3d","mcp__godot-mcp-pro__setup_environment", "mcp__godot-mcp-pro__setup_camera_3d","mcp__godot-mcp-pro__add_gridmap", "mcp__godot-mcp-pro__setup_collision","mcp__godot-mcp-pro__add_raycast", "mcp__godot-mcp-pro__setup_physics_body", "mcp__godot-mcp-pro__get_navigation_info", "mcp__godot-mcp-pro__setup_navigation_region","mcp__godot-mcp-pro__bake_navigation_mesh", "mcp__godot-mcp-pro__setup_navigation_agent","mcp__godot-mcp-pro__set_navigation_layers", "mcp__godot-mcp-pro__get_particle_info", "mcp__godot-mcp-pro__create_particles","mcp__godot-mcp-pro__set_particle_material", "mcp__godot-mcp-pro__set_particle_color_gradient","mcp__godot-mcp-pro__apply_particle_preset", "mcp__godot-mcp-pro__get_theme_info", "mcp__godot-mcp-pro__create_theme","mcp__godot-mcp-pro__set_theme_color", "mcp__godot-mcp-pro__set_theme_constant","mcp__godot-mcp-pro__set_theme_font_size", "mcp__godot-mcp-pro__set_theme_stylebox", "mcp__godot-mcp-pro__tilemap_get_cell","mcp__godot-mcp-pro__tilemap_get_info", "mcp__godot-mcp-pro__tilemap_get_used_cells", "mcp__godot-mcp-pro__tilemap_set_cell","mcp__godot-mcp-pro__tilemap_fill_rect", "mcp__godot-mcp-pro__get_autoload","mcp__godot-mcp-pro__add_autoload", "mcp__godot-mcp-pro__set_project_setting", "mcp__godot-mcp-pro__find_unused_resources","mcp__godot-mcp-pro__analyze_signal_flow", "mcp__godot-mcp-pro__analyze_scene_complexity","mcp__godot-mcp-pro__find_script_references", "mcp__godot-mcp-pro__detect_circular_dependencies", "mcp__godot-mcp-pro__get_performance_monitors", "mcp__godot-mcp-pro__get_export_info","mcp__godot-mcp-pro__list_export_presets", "mcp__godot-mcp-pro__play_scene","mcp__godot-mcp-pro__stop_scene", "mcp__godot-mcp-pro__get_game_scene_tree","mcp__godot-mcp-pro__get_game_node_properties", "mcp__godot-mcp-pro__get_game_screenshot","mcp__godot-mcp-pro__set_game_node_property", "mcp__godot-mcp-pro__find_nodes_by_script", "mcp__godot-mcp-pro__simulate_key","mcp__godot-mcp-pro__simulate_mouse_click", "mcp__godot-mcp-pro__simulate_mouse_move","mcp__godot-mcp-pro__simulate_action", "mcp__godot-mcp-pro__simulate_sequence", "mcp__godot-mcp-pro__capture_frames","mcp__godot-mcp-pro__record_frames", "mcp__godot-mcp-pro__monitor_properties","mcp__godot-mcp-pro__batch_get_properties", "mcp__godot-mcp-pro__start_recording","mcp__godot-mcp-pro__stop_recording", "mcp__godot-mcp-pro__replay_recording", "mcp__godot-mcp-pro__find_ui_elements","mcp__godot-mcp-pro__click_button_by_text", "mcp__godot-mcp-pro__wait_for_node","mcp__godot-mcp-pro__find_nearby_nodes", "mcp__godot-mcp-pro__navigate_to","mcp__godot-mcp-pro__move_to", "mcp__godot-mcp-pro__run_test_scenario","mcp__godot-mcp-pro__assert_node_state", "mcp__godot-mcp-pro__assert_screen_text","mcp__godot-mcp-pro__run_stress_test", "mcp__godot-mcp-pro__get_test_report","mcp__godot-mcp-pro__compare_screenshots" ) $claudeSettingsJson = if ($UseLmuAuth) { [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 { [ordered]@{ model = $CLAUDE_MODEL; allowedTools = $mcpAllowedTools } | ConvertTo-Json -Depth 3 } $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) { [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 配置已写入 $claudeDir\settings.json" # ══════════════════════════════════════════════════════════════ # Step 3: GitHub CLI + Copilot CLI 扩展 # ══════════════════════════════════════════════════════════════ Write-Step "3/8 GitHub CLI + Copilot CLI" $ghCmd = Get-Command gh -ErrorAction SilentlyContinue if ($ghCmd) { Write-OK "GitHub CLI 已安装: $(& gh --version 2>&1 | Select-Object -First 1)" } else { Write-Info "通过 winget 安装 GitHub CLI..." $ok = Install-Winget "GitHub.cli" "GitHub CLI" $ghCmd = Get-Command gh -ErrorAction SilentlyContinue if ($ghCmd) { Write-OK "GitHub CLI 安装完成: $(& gh --version 2>&1 | Select-Object -First 1)" } else { Write-Warn "GitHub CLI 安装后未找到,请重开终端或手动安装:winget install GitHub.cli" } } if ($ghCmd) { $extList = & gh extension list 2>&1 | Out-String if ($extList -match "github/gh-copilot") { Write-OK "GitHub Copilot CLI 扩展已安装" } else { Write-Info "安装 GitHub Copilot CLI 扩展..." try { & gh extension install github/gh-copilot 2>&1 | Out-Null Write-OK "GitHub Copilot CLI 扩展安装完成(入口:gh copilot)" } catch { Write-Warn "安装失败(可能需要先 gh auth login),请手动运行:gh extension install github/gh-copilot" } } } # ══════════════════════════════════════════════════════════════ # Step 4: Unity MCP Server # ══════════════════════════════════════════════════════════════ Write-Step "4/8 Unity MCP Server" Write-Info "安装目录: $UnityMcpInstallDir" $gitCmd = Get-Command git -ErrorAction SilentlyContinue if (-not $gitCmd) { Write-Warn "未找到 git,跳过 Unity MCP Server 安装" Write-Warn "请安装 Git for Windows:winget install Git.Git" $UnityMcpReady = $false } else { if (Test-Path "$UnityMcpInstallDir\.git") { Write-Info "已存在,更新至最新..." Push-Location $UnityMcpInstallDir try { & git pull --quiet 2>&1 | Out-Null } finally { Pop-Location } } else { if (Test-Path $UnityMcpInstallDir) { Remove-Item $UnityMcpInstallDir -Recurse -Force } & git clone --quiet --depth 1 https://github.com/AnkleBreaker-Studio/unity-mcp-server.git $UnityMcpInstallDir } if (Get-Command node -ErrorAction SilentlyContinue) { Push-Location $UnityMcpInstallDir try { & npm install --prefer-offline --quiet 2>&1 | Out-Null } finally { Pop-Location } Write-OK "Unity MCP Server 就绪" $UnityMcpReady = $true } else { Write-Warn "Node.js 未就绪,跳过 npm install" $UnityMcpReady = $false } } # ── 确定入口文件 ────────────────────────────────────────────── $unityEntry = "" if ($UnityMcpReady) { foreach ($f in @("src/index.js","build/index.js","index.js")) { $fp = Join-Path $UnityMcpInstallDir $f if (Test-Path $fp) { $unityEntry = $fp -replace "\\","/"; break } } } # ── 写入 MCP 配置(四大 AI 客户端 + Claude Code CLI)───────── if ($unityEntry) { $unityMcpEntry = @{ command = "node" args = @($unityEntry) env = @{ UNITY_HUB_PATH = $UnityHubPath; UNITY_BRIDGE_PORT = "$BridgePort" } } Merge-McpConfig "$env:APPDATA\Claude\claude_desktop_config.json" $unityMcpEntry "Claude Desktop" Merge-McpConfig "$env:USERPROFILE\.cursor\mcp.json" $unityMcpEntry "Cursor" $wsDirC = if (Test-Path "$env:APPDATA\Windsurf") { "$env:APPDATA\Windsurf" } else { "$env:USERPROFILE\.codeium\windsurf" } Merge-McpConfig "$wsDirC\mcp_config.json" $unityMcpEntry "Windsurf" Merge-VsCodeMcpSettings "unity-mcp" $unityMcpEntry "VS Code Copilot Agent (Unity MCP)" # Claude Code CLI(Windows) if (Get-Command claude -ErrorAction SilentlyContinue) { & claude mcp remove unity-mcp --scope user 2>&1 | Out-Null & claude mcp add --scope user unity-mcp node $unityEntry -e "UNITY_BRIDGE_PORT=$BridgePort" 2>&1 | Out-Null Write-OK "Claude Code CLI MCP -> unity-mcp" } } # ══════════════════════════════════════════════════════════════ # Step 5: Godot MCP Pro # ══════════════════════════════════════════════════════════════ Write-Step "5/8 Godot MCP Pro" # 解析 Godot MCP Pro 本地包路径(相对于本脚本目录) $godotPkgBase = if ([System.IO.Path]::IsPathRooted($GodotMcpPackageDir)) { $GodotMcpPackageDir } else { [System.IO.Path]::GetFullPath((Join-Path $ScriptDir $GodotMcpPackageDir)) } $godotSrcDir = Join-Path $godotPkgBase "$GodotMcpVersion\server" $godotWinDir = "$env:USERPROFILE\godot-mcp-pro" if (-not (Test-Path $godotSrcDir)) { Write-Warn "未找到 Godot MCP Pro 源目录: $godotSrcDir" Write-Warn "请确认 .env 中 GODOT_MCP_PACKAGE_DIR 指向包含 $GodotMcpVersion\ 的目录" $godotEntry = "" } else { Write-Info "同步 server/ -> $godotWinDir" $rcArgs = @($godotSrcDir, $godotWinDir, "/E", "/XD", "node_modules", "/NFL", "/NDL", "/NJH", "/NJS", "/nc", "/ns", "/np") & robocopy @rcArgs 2>&1 | Out-Null if (Get-Command node -ErrorAction SilentlyContinue) { Push-Location $godotWinDir try { & node build/setup.js install 2>&1 | Out-Null } finally { Pop-Location } Write-OK "Godot MCP Pro 就绪: $godotWinDir" } else { Write-Warn "Node.js 未就绪,跳过 Godot MCP Pro setup" } $godotEntry = ($godotWinDir -replace "\\","/") + "/build/index.js" $godotMcpEntry = @{ command = "node"; args = @($godotEntry) } Merge-McpConfig "$env:APPDATA\Claude\claude_desktop_config.json" $godotMcpEntry "Claude Desktop (Godot)" "godot-mcp-pro" Merge-McpConfig "$env:USERPROFILE\.cursor\mcp.json" $godotMcpEntry "Cursor (Godot)" "godot-mcp-pro" $wsDirG = if (Test-Path "$env:APPDATA\Windsurf") { "$env:APPDATA\Windsurf" } else { "$env:USERPROFILE\.codeium\windsurf" } Merge-McpConfig "$wsDirG\mcp_config.json" $godotMcpEntry "Windsurf (Godot)" "godot-mcp-pro" Merge-VsCodeMcpSettings "godot-mcp-pro" $godotMcpEntry "VS Code Copilot Agent (Godot MCP Pro)" if (Get-Command claude -ErrorAction SilentlyContinue) { & claude mcp remove godot-mcp-pro --scope user 2>&1 | Out-Null & claude mcp add --scope user godot-mcp-pro node $godotEntry 2>&1 | Out-Null Write-OK "Claude Code CLI MCP -> godot-mcp-pro" } } # ══════════════════════════════════════════════════════════════ # Step 6: 防火墙放行 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 "非管理员权限,尝试以提升权限添加防火墙规则..." $fwCmd = @" `$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($fwCmd)) $proc = Start-Process powershell.exe -ArgumentList "-NoProfile -EncodedCommand $enc" ` -Verb RunAs -Wait -PassThru -ErrorAction SilentlyContinue if ($proc -and $proc.ExitCode -eq 0) { Write-OK "防火墙规则已添加 (TCP $portRange)" } else { Write-Warn "防火墙配置未成功,请手动以管理员运行:New-NetFirewallRule ... -LocalPort $portRange" } } } # ══════════════════════════════════════════════════════════════ # Step 7: Rust 工具链 & RTK # ══════════════════════════════════════════════════════════════ Write-Step "7/8 Rust 工具链 & Token Killer" # 若 .env 配置了国内镜像,提前注入环境变量 $rustupMirror = $Cfg["RUSTUP_DIST_SERVER"] ?? "" $rustupRoot = $Cfg["RUSTUP_UPDATE_ROOT"] ?? "" if ($rustupMirror) { $env:RUSTUP_DIST_SERVER = $rustupMirror } if ($rustupRoot) { $env:RUSTUP_UPDATE_ROOT = $rustupRoot } # 先把 cargo\bin 加入 PATH(rustup 通过 winget 安装后 PATH 需刷新) $cargoBin = "$env:USERPROFILE\.cargo\bin" if (Test-Path $cargoBin) { $env:Path = "$cargoBin;$env:Path" } Refresh-Path $cargoCmd = Get-Command cargo -ErrorAction SilentlyContinue if ($cargoCmd) { # 检查是否有默认 toolchain,若没有则安装 stable $rustcVer = & rustc --version 2>&1 | Out-String if ($rustcVer -match "no default is configured" -or $rustcVer -match "error:") { Write-Info "rustup 无默认 toolchain,安装 stable..." & rustup toolchain install stable 2>&1 | Select-Object -Last 3 & rustup default stable 2>&1 | Out-Null Refresh-Path if (Test-Path $cargoBin) { $env:Path = "$cargoBin;$env:Path" } $rustcVer = & rustc --version 2>&1 | Out-String } Write-OK "Rust 已安装: $($rustcVer.Trim())" } else { # 检查 winget 是否已记录安装(避免重复安装时阻塞) $wgPath = (Get-Command winget -ErrorAction SilentlyContinue)?.Source if (-not $wgPath) { $fallback = "$env:LOCALAPPDATA\Microsoft\WindowsApps\winget.exe" if (Test-Path $fallback) { $wgPath = $fallback } } $wgList = if ($wgPath) { & $wgPath list --id Rustlang.Rustup --accept-source-agreements 2>&1 | Out-String } else { "" } if ($wgList -match "Rustlang.Rustup") { Write-Info "rustup 已安装,但当前会话 PATH 未更新。已将 $cargoBin 加入 PATH" # rustup 首次运行会初始化 stable 工具链 if (Test-Path "$cargoBin\rustup.exe") { & "$cargoBin\rustup.exe" show 2>&1 | Out-Null } $cargoCmd = Get-Command cargo -ErrorAction SilentlyContinue if ($cargoCmd) { Write-OK "Rust: $(& rustc --version 2>&1)" } } else { Write-Info "通过 winget 安装 Rust (rustup)..." $ok = Install-Winget "Rustlang.Rustup" "Rustup" if ($ok) { if (Test-Path $cargoBin) { $env:Path = "$cargoBin;$env:Path" } Refresh-Path $cargoCmd = Get-Command cargo -ErrorAction SilentlyContinue if ($cargoCmd) { Write-OK "Rust 安装完成: $(& rustc --version 2>&1)" } else { Write-Warn "Rust 安装后未找到 cargo,请关闭并重开终端后继续" Write-Warn " 或手动安装:https://rustup.rs" } } } } # 先把 ~/.local/bin 加入 PATH(预编译包安装目录) $localBin = "$env:USERPROFILE\.local\bin" if (Test-Path $localBin) { $env:Path = "$localBin;$env:Path" } $rtkCmd = Get-Command rtk -ErrorAction SilentlyContinue if ($rtkCmd) { Write-OK "rtk 已安装: $(& rtk --version 2>&1)" } else { # 从 GitHub Releases 下载预编译包(比 cargo install 快得多,且无需编译环境) Write-Info "安装 rtk (Rust Token Killer) - 下载预编译包..." try { $rtkVer = "v0.42.0" $rtkUrl = "https://github.com/rtk-ai/rtk/releases/download/$rtkVer/rtk-x86_64-pc-windows-msvc.zip" New-Item -ItemType Directory -Force -Path $localBin | Out-Null $rtkZip = "$env:TEMP\rtk-windows.zip" Invoke-WebRequest -Uri $rtkUrl -OutFile $rtkZip -UseBasicParsing Expand-Archive -Path $rtkZip -DestinationPath $localBin -Force Remove-Item $rtkZip -ErrorAction SilentlyContinue $env:Path = "$localBin;$env:Path" $rtkCmd = Get-Command rtk -ErrorAction SilentlyContinue if ($rtkCmd) { Write-OK "rtk 安装完成: $(& rtk --version 2>&1)" } else { Write-Warn "rtk 安装失败,可手动运行:cargo install --git https://github.com/rtk-ai/rtk" } } catch { Write-Warn "rtk 下载失败: $_" Write-Warn "可手动运行:cargo install --git https://github.com/rtk-ai/rtk" } } # ── rtk init -g:注册 Claude Code PreToolUse hook ───────────── if (Get-Command rtk -ErrorAction SilentlyContinue) { Write-Info "注册 rtk hook 到 Claude Code..." # 先禁用遥测,避免交互式确认提示阻塞管道 $prevEAP = $ErrorActionPreference $ErrorActionPreference = "Continue" & rtk telemetry disable 2>&1 | Out-Null try { & rtk init -g --auto-patch 2>&1 | Out-Null Write-OK "rtk hook 已注册(重启 Claude Code 后生效)" } catch { Write-Warn "rtk init 异常(hook 可能已注册): $_" } finally { $ErrorActionPreference = $prevEAP } # ── rtk Copilot Instructions:写入 .github/copilot-instructions.md ── # 使用已有的(位于仓库根 .github/),仅在缺失时生成 $copilotDir = Join-Path $ScriptDir ".." ".github" $copilotInst = Join-Path $copilotDir "copilot-instructions.md" if (-not (Test-Path $copilotInst)) { Write-Info "生成 .github/copilot-instructions.md ..." if (-not (Test-Path $copilotDir)) { New-Item -ItemType Directory -Path $copilotDir | Out-Null } & rtk init -g --copilot 2>&1 | Out-Null Write-OK "Copilot Instructions 已生成" } else { Write-OK "Copilot Instructions 已存在,跳过生成" } } # ══════════════════════════════════════════════════════════════ # 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 = @" # ── Windows Dev Stack (generated by windows-dev-stack\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" # 将 rtk / cargo 加入 PATH(如尚未在系统 PATH 中) `$_localBin = "`$env:USERPROFILE\.local\bin" if ((Test-Path `$_localBin) -and `$env:Path -notlike "*.local\bin*") { `$env:Path = "`$_localBin;`$env:Path" } `$_cargoBin = "`$env:USERPROFILE\.cargo\bin" if ((Test-Path `$_cargoBin) -and `$env:Path -notlike "*cargo\bin*") { `$env:Path = "`$_cargoBin;`$env:Path" } 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 "Windows Dev Stack") { # 注意:直接用 -replace 会将 $_ 等解释为正则反向引用导致内容爆炸 # 使用 MatchEvaluator scriptblock 让替换字符串按字面量处理 $blockCapture = $profileBlock $existing = [regex]::Replace($existing, '(?ms)# ── Windows Dev Stack.*?# ─+', { $blockCapture }) 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 "║ ✅ Windows 原生全栈部署完成 ║" -ForegroundColor Green Write-Host "╚══════════════════════════════════════════════════════════════╝" -ForegroundColor Green Write-Host "" $nodeVer = if (Get-Command node -ErrorAction SilentlyContinue) { & node --version 2>&1 } else { "(未安装)" } $claudeVer = if (Get-Command claude -ErrorAction SilentlyContinue) { & claude --version 2>&1 | Select-Object -First 1 } else { "(未安装)" } $ghVer = if (Get-Command gh -ErrorAction SilentlyContinue) { & gh --version 2>&1 | Select-Object -First 1 } else { "(未安装)" } $rustVer = if (Get-Command rustc -ErrorAction SilentlyContinue) { & rustc --version 2>&1 } else { "(未安装)" } $rtkVer = if (Get-Command rtk -ErrorAction SilentlyContinue) { & rtk --version 2>&1 } else { "(未安装)" } $items = @( @("Node.js", $nodeVer), @("Claude Code", $claudeVer), @("GitHub CLI", $ghVer), @("Rust", $rustVer), @("rtk", $rtkVer), @("Unity MCP", $(if ($unityEntry) { $unityEntry } else { "(未安装)" })), @("Godot MCP Pro", $(if ($godotEntry) { $godotEntry } else { "(未安装)" })), @("Bridge Port", "TCP $BridgePort (防火墙已放行 $BridgePort-$($BridgePort+9))") ) foreach ($item in $items) { $label = $item[0].PadRight(14) Write-Host " $label $($item[1])" -ForegroundColor White } Write-Host "" Write-Host " 后续手动步骤:" -ForegroundColor Yellow Write-Host " 1. gh auth login # GitHub CLI 登录(首次)" -ForegroundColor DarkGray Write-Host " 2. Unity Editor 安装 unity-mcp-plugin:" -ForegroundColor DarkGray Write-Host " https://github.com/AnkleBreaker-Studio/unity-mcp-plugin.git" -ForegroundColor DarkGray Write-Host " 3. Godot 项目:复制 addons/godot_mcp/ 并在 Plugins 中启用" -ForegroundColor DarkGray Write-Host " 源:$(Join-Path $godotPkgBase "$GodotMcpVersion\addons\godot_mcp")" -ForegroundColor DarkGray Write-Host " 4. 重启 AI 客户端(Claude Desktop / Cursor / Windsurf / VS Code)" -ForegroundColor DarkGray Write-Host ""