Claude Code / Codex 一键安装器 (附带C#源码,MIT开源)
(开源地址在文末gitee链接)
Avalonia 12 · .NET 8 · MVVM · TDD
作者:Wesky / Dotnet Dancer(公众号)
这是一款基于 Avalonia 12 + .NET 8 的 Windows 桌面工具,让用户零命令、无感地装好 Claude Code 与 Codex 环境:自动检测并安装 Node、git 等前置依赖,自动初始化配置模板、断点续传优化等。
为什么我要做这一个教程?我无意间看到,咸鱼、淘宝上有卖安装claude code或者codex安装教程的,一份一二十块。嘿,太黑了!这不是很简单的事情吗?也有人说,还一些非技术的啊!他们可能不太会安装。于是,我写了个开源项目,默认是Windows环境的,感兴趣的技术大神,可以自行拓展为mac、linux环境。理论上玩mac或者linux的大神,应该动手能力好,也不需要我这个安装程序。
客户端其实很简陋,打开效果如下:
源码架构整体截图:
本文目录
一、开发实现说明
二、核心源码解析
三、用户使用说明
四、配置文件配置详解
五、开源项目地址
01 一、开发实现说明
1.1 技术栈
整个项目仅依赖一组轻量、主流的 .NET 生态库,构建产物为单一 Windows 桌面程序:
•Avalonia 12.0.3:跨平台 .NET UI 框架;本项目用其桌面后端(Avalonia.Desktop)、Fluent 主题与 Inter 字体。
•.NET 8(net8.0):开启 ImplicitUsings、Nullable 与 LangVersion=latest;输出类型 WinExe。
•CommunityToolkit.Mvvm 8.4.2:MVVM 源生成器,用 [ObservableProperty] 生成属性、[RelayCommand] 生成命令。
•Avalonia.Controls.WebView 12.0.1:内嵌 NativeWebView(底层 WebView2)展示官方教程。
•Microsoft.Win32.Registry 5.0.0:读注册表判断 WebView2 运行时是否已安装。
•xUnit:单元测试,12 个测试类、约 45 个用例全通过;核心安装链路全部脱网可测。
1.2 架构思想:接口化 + 构造函数注入
核心原则只有一句:把所有外部副作用(执行进程、HTTP 下载、刷新 PATH、读注册表、打开文件、弹窗)全部收敛到接口化的服务层,再以构造函数注入装配。这样 ViewModel 与各安装器只依赖抽象,测试时用 fake 替换即可在无网络、无管理员权限的环境下验证完整安装链路。
服务契约(节选):
•IProcessRunner —— 唯一执行进程的出口,是整个体系最关键的可测试缝隙;
•IFileDownloader / IEnvironmentDetector / IWingetBootstrapper —— 下载、探测、winget 引导;
•INodeInstaller / IGitInstaller / IPackageInstaller —— 三条安装链;
•IConfigFileService / ITutorialService / IPathRefresher / IWebViewBootstrapper —— 配置、教程、PATH、WebView 运行时。
NOTE 没有 DI 容器,手动 Composition Root 依赖装配集中在 App.axaml.cs 的 OnFrameworkInitializationCompleted 里手写 new 出来——对这种规模的工具,手动组合根比引入容器更直观、启动更快、也更易读。Node 与 git 安装器共享同一个 IWingetBootstrapper 与 IFileDownloader 实例。 |
•MainWindowViewModel 只依赖 6 个接口,对具体实现一无所知;
•应用清单 requireAdministrator,一次提权、全程无二次弹窗(装 MSI/EXE、Add-AppxPackage 都需要管理员);
•遵循 TDD:先写测试再写实现,安装链路的每个分支(已装跳过、winget 成功、回退镜像、源全失败)都有对应用例。
1.3 项目结构
[TXT] AutoInstall.sln
AutoInstall.sln
├─ src/AutoInstall/ Avalonia 应用
│ ├─ App.axaml(.cs) 程序入口 + 依赖装配(组合根)
│ ├─ Models/InstallTypes.cs 领域模型(record)
│ ├─ Services/ 环境检测、安装链路、下载、配置、教程(接口化)
│ ├─ ViewModels/ MVVM 视图模型
│ └─ Views/ 主窗口、教程窗口
└─ tests/AutoInstall.Tests/ xUnit 单元测试 + Fakes.cs
领域模型都是不可变 record:ProcessResult、ToolInfo、EnvironmentStatus、InstallResult,以及枚举 TutorialTopic。简单、无副作用、天然好测。
02 二、核心源码解析
2.1 ProcessRunner:超时 + 杀进程树
所有外部命令的唯一出口。它把 stdout/stderr 异步重定向并实时回吐到日志,关键在于超时控制:用一个由 timeout 派生的 CancellationTokenSource 与外部 ct 链接,超时即终止整棵进程树。
踩坑 → 修复 winget 拉起的常驻进程导致永久挂起 早期直接 await WaitForExitAsync 等待 stdout EOF。但 winget 会拉起常驻后台进程 WindowsPackageManagerServer,它继承了被重定向的输出句柄且不关闭,导致 EOF 永不到达、进程视为“未退出”,UI 永久卡死。 修复:给等待加超时;超时则 Kill(entireProcessTree: true) 杀掉整棵树并返回退出码 -1,交由上层走镜像兜底。 |
[C#] src/AutoInstall/Services/ProcessRunner.cs
1 │ usingvar timeoutCts = timeout is { } span ? newCancellationTokenSource(span) : null;
2 │ usingvar linked = timeoutCts isnull
3 │ ? null
4 │ : CancellationTokenSource.CreateLinkedTokenSource(ct, timeoutCts.Token);
5 │ var waitToken = linked?.Token ?? ct;
6 │
7 │ try
8 │ {
9 │ await process.WaitForExitAsync(waitToken);
10 │ }
11 │ catch (OperationCanceledException) when (timeoutCts is not null
12 │ && timeoutCts.IsCancellationRequested && !ct.IsCancellationRequested)
13 │ {
14 │ // 超时:终止整棵进程树并返回,避免被子进程残留的输出句柄无限期阻塞。
15 │ TryKillTree(process);
16 │ returnnewProcessResult(-1, stdout.ToString(), stderr.ToString());
17 │ }
注意 when 过滤器精确区分两种取消:超时(吞掉、返回 -1)与外部取消(清理后向上 throw),二者语义不同。
2.2 EnvironmentDetector:探测原语
探测的最小单元是 DetectToolAsync:用 cmd.exe /c--version 执行,退出码 0 即视为已装,版本号取输出首行(去掉 CR、按空行切分)。带 15 秒超时兜底,避免某个工具卡死探测。
[C#] src/AutoInstall/Services/EnvironmentDetector.cs
1 │ publicasyncTask<ToolInfo> DetectToolAsync(string command, string versionArg, CancellationToken ct = default)
2 │ {
3 │ var r = await _runner.RunAsync("cmd.exe", $"/c {command} {versionArg}", null, ct, TimeSpan.FromSeconds(15));
4 │ if (r.ExitCode != 0) returnnewToolInfo(false, null);
5 │
6 │ var version = (r.StdOut ?? string.Empty)
7 │ .Replace("\r", "")
8 │ .Split('\n', StringSplitOptions.RemoveEmptyEntries)
9 │ .FirstOrDefault()?.Trim();
10 │
11 │ returnnewToolInfo(true, string.IsNullOrWhiteSpace(version) ? null : version);
12 │ }
NOTE 并行探测在 ViewModel,而非这里 本类的 DetectAsync 是顺序探测(一次性快照)。界面上“逐项就绪即刷新”的并行探测其实在 MainWindowViewModel.RefreshAsync 里用 Task.WhenAll 实现——见 2.9。两者职责分离:本类只提供探测原语。 |
2.3 WindowsWingetBootstrapper:确保 winget 可用
封装“确保 winget 存在”:已装直接返回 true;否则下载 VCLibs 依赖包与 App Installer 捆绑包,经 PowerShell Add-AppxPackage 安装后复检。Node 与 git 安装链共用它。
[C#] src/AutoInstall/Services/WindowsWingetBootstrapper.cs
1 │ await _downloader.DownloadAsync("https://aka.ms/Microsoft.VCLibs.x64.14.00.Desktop.appx", vclibs, log, ct);
2 │ await _downloader.DownloadAsync("https://aka.ms/getwinget", bundle, log, ct);
3 │
4 │ var safeVclibs = vclibs.Replace("'", "''");
5 │ var safeBundle = bundle.Replace("'", "''");
6 │ var r1 = await _runner.RunAsync("cmd.exe",
7 │ $"/c powershell -NoProfile -ExecutionPolicy Bypass -Command \"Add-AppxPackage -LiteralPath '{safeVclibs}'\"", log, ct);
TIP 路径转义防注入 传给 PowerShell 单引号字符串前,先把路径里的 ' 替换为 ''(safeVclibs),并用 -LiteralPath 避免通配符与命令注入。 |
2.4 WindowsNodeInstaller:winget → 自装 winget → 镜像/官方 MSI
先用 DetectToolAsync 检测 node + npm,两者都在则直接返回“已安装”。否则先试 winget(OpenJS.NodeJS.LTS,3 分钟超时),失败回退 MSI。MSI 源国内镜像优先:
[C#] src/AutoInstall/Services/WindowsNodeInstaller.cs
1 │ var sources = new (string Name, string IndexUrl, string Prefix)[]
2 │ {
3 │ ("国内镜像(npmmirror)", "https://registry.npmmirror.com/-/binary/node/index.json",
4 │ "https://registry.npmmirror.com/-/binary/node"),
5 │ ("官方(nodejs.org)", "https://nodejs.org/dist/index.json", "https://nodejs.org/dist"),
6 │ };
7 │ // 解析最新 LTS → 拼出 node--x64.msi → 下载 → msiexec /i ... /qn;任一源成功即返回
版本解析由纯函数 NodeVersionResolver.ParseLatestLts 完成:遍历 index.json,取第一个 lts 字段不为 false 的条目即为最新 LTS。两个源 index 与路径格式一致,所以同一套解析逻辑通吃。安装完成后调 RefreshProcessPath() 再复检(见 2.10)。
2.5 WindowsGitInstaller:winget → 清华 TUNA → GitHub
git 是运行期依赖(Claude Code/Codex 都要用)。缺失则 winget(Git.Git,3 分钟超时)→ 失败则下载官方静默安装包,源清华 TUNA 优先、GitHub 兜底:
[C#] src/AutoInstall/Services/WindowsGitInstaller.cs
1 │ var sources = new (string Name, Func<CancellationToken, TaskResolve)[]
2 │ {
3 │ ("国内镜像(清华 TUNA)", ResolveExeUrlFromTunaAsync), // LatestRelease 永远指向最新版
4 │ ("GitHub", ResolveExeUrlFromGitHubAsync),
5 │ };
6 │ // 解析 Git--64-bit.exe → 下载 → /VERYSILENT /NORESTART /SP- /NOCANCEL
•ResolveExeUrlFromTunaAsync:抓取 TUNA 的 LatestRelease 目录 HTML,用正则 Git-[0-9][0-9.]*-64-bit\.exe 提取文件名(GitMirrorResolver)。
•ResolveExeUrlFromGitHubAsync:调 GitHub Releases API,从 assets 里找以 -64-bit.exe 结尾的 browser_download_url(GitReleaseResolver)。
2.6 HttpFileDownloader:断点续传 + 重试
踩坑 → 修复 弱网大文件频繁报 “copying content to a stream” 大陆弱网下载 Node MSI / Git EXE 这类几十 MB 的文件,常在中途断流抛 IOException。一次性下载几乎必失败。 修复:下载到 .part 临时文件;断开后用 HTTP Range 头从断点续传,最多重试 4 次、单次 10 分钟超时;全部完成后原子改名到目标路径。 |
[C#] src/AutoInstall/Services/HttpFileDownloader.cs
1 │ var existing = File.Exists(partPath) ? newFileInfo(partPath).Length : 0L;
2 │
3 │ usingvar req = newHttpRequestMessage(HttpMethod.Get, url);
4 │ if (existing > 0) req.Headers.Range = newRangeHeaderValue(existing, null);
5 │
6 │ usingvar resp = awaitHttp.SendAsync(req, HttpCompletionOption.ResponseHeadersRead, token);
7 │ // 服务器不支持续传(返回 200 而非 206)→ 删除 .part 从头开始
8 │ if (existing > 0 && resp.StatusCode != HttpStatusCode.PartialContent) { File.Delete(partPath); existing = 0; }
9 │ resp.EnsureSuccessStatusCode();
10 │
11 │ awaitusingvar fs = newFileStream(partPath,
12 │ existing > 0 ? FileMode.Append : FileMode.Create, FileAccess.Write, FileShare.None);
13 │ await src.CopyToAsync(fs, token);
NOTE 全局超时设为无限,按“单次尝试”限时 共享的 HttpClient.Timeout 设为 InfiniteTimeSpan,否则慢速大文件会被全局超时误杀;真正的时限由每次尝试的 CancelAfter(10min) 控制。外部取消会保留 .part 以便下次续传。 |
2.7 ConfigFileService:缺失则生成模板
打开配置时,若文件不存在则建目录并写入带占位项的合法模板(JSON / TOML);已存在则绝不覆盖。模板自带 _说明 提示键(会被 Claude Code 忽略)与 TOML 行内 # 注释,引导用户填 URL、密钥与模型。模板全文见第四章。
[C#] src/AutoInstall/Services/ConfigFileService.cs
1 │ privatestatic string EnsureFile(string dir, string fileName, string defaultContent)
2 │ {
3 │ Directory.CreateDirectory(dir);
4 │ var path = Path.Combine(dir, fileName);
5 │ if (!File.Exists(path))
6 │ File.WriteAllText(path, defaultContent);
7 │ return path;
8 │ }
2.8 教程:WebView2Bootstrapper + TutorialService
教程在内嵌 NativeWebView 打开官方文档。启动时后台静默确保 WebView2 运行时:先读注册表 EdgeUpdate\Clients\{...} 的 pv 值判断是否已装,缺失则下载官方 MicrosoftEdgeWebview2Setup.exe 静默安装;任何失败都不阻断启动,教程会回退到系统默认浏览器。
TutorialService 仅做 URL 映射。
2.9 MainWindowViewModel:编排(已装跳过 / 非阻塞 / 并行刷新)
安装命令的编排顺序:确保 Node(失败则中止)→ 确保 git(失败仅告警,不阻断)→ 目标 CLI 已装则跳过、未装才 npm 全局安装 → 刷新状态。每个环节都先检测后动作,绝不重复安装。
[C#] src/AutoInstall/ViewModels/MainWindowViewModel.cs
1 │ var node = await _nodeInstaller.EnsureNodeAsync(log);
2 │ ((IProgress)log).Report(node.Message);
3 │ if (!node.Success) return; // Node 失败 → 中止
4 │
5 │ var git = await _gitInstaller.EnsureGitAsync(log);
6 │ ((IProgress)log).Report(git.Message); // git 失败 → 仅告警,继续
7 │
8 │ var existing = await _detector.DetectToolAsync(toolCommand, "--version");
9 │ if (existing.Installed)
10 │ ((IProgress)log).Report($"{toolCommand} 已安装({existing.Version}),跳过安装。");
11 │ else
12 │ await _packageInstaller.InstallAsync(npmPackage, $"{toolCommand} --version", log);
界面刷新则是并行的——这正是“逐项落定”观感的来源:
[C#] src/AutoInstall/ViewModels/MainWindowViewModel.cs
1 │ awaitTask.WhenAll(
2 │ ProbeAsync("node", v => NodeStatus = v),
3 │ ProbeAsync("npm", v => NpmStatus = v),
4 │ ProbeAsync("winget", v => WingetStatus = v),
5 │ ProbeAsync("claude", v => ClaudeStatus = v),
6 │ ProbeAsync("codex", v => CodexStatus = v),
7 │ ProbeAsync("git", v => GitStatus = v));
状态属性由 [ObservableProperty] 生成、命令由 [RelayCommand] 生成;IsBusy / IsRefreshing 做重入保护,单项探测异常被局部 try/catch 兜住、不影响其余。
2.10 WindowsPathRefresher:让新装工具立即可见
刚装好的 Node/git 会把自己写进系统/用户级 PATH,但当前进程的 PATH 是启动时的快照,感知不到新值,于是复检会误判“仍未安装”。RefreshProcessPath 重新读取 Machine + User 两级 PATH 并合并写回当前进程,安装链路调用它后再复检即可命中。
[C#] src/AutoInstall/Services/WindowsPathRefresher.cs
1 │ var machine = Environment.GetEnvironmentVariable("PATH", EnvironmentVariableTarget.Machine) ?? "";
2 │ var user = Environment.GetEnvironmentVariable("PATH", EnvironmentVariableTarget.User) ?? "";
3 │ Environment.SetEnvironmentVariable("PATH", machine + ";" + user, EnvironmentVariableTarget.Process);
03 三、用户使用说明
3.1 环境要求
•Windows 10 / 11(x64)。
•(仅从源码构建时)需要 .NET 8 SDK;普通使用者直接运行已构建的程序即可。
•以管理员权限运行:应用清单已声明 requireAdministrator,首次启动弹一次 UAC,之后全程无弹窗。
3.2 获取与运行
[Shell] PowerShell / CMD
1 │ git clone https://gitee.com/dreamer_j/auto-install-cc.git
2 │ cd auto-install-cc
3 │ dotnet run --project src/AutoInstall
4 │ # 或:dotnet build -c Release 后运行生成的 AutoInstall.exe
3.3 主界面功能
•状态卡片:启动即并行检测 Node / npm / winget / git / Claude Code / Codex,显示“已安装 版本 / 未安装”。
•一键安装 Claude Code:自动装好 Node、git,再 npm 全局安装 @anthropic-ai/claude-code。
•一键安装 Codex:同上,安装 @openai/codex。
•打开配置:打开 Claude Code / Codex 配置文件,缺失则先生成模板再用系统默认程序打开。
•教程:内嵌网页打开官方文档,WebView2 不可用时回退系统浏览器。
•日志面板:实时显示安装/下载/重试输出,过程完全透明。
3.4 中国大陆网络优化
•Node 优先 npmmirror、git 优先清华 TUNA,失败再回退官方源;
•大文件断点续传 + 自动重试(最多 4 次,单次 10 分钟);
•winget 步骤带 3 分钟超时,卡住即转国内镜像,不会无限等待。
TIP 下载仍失败怎么办 把日志面板的输出复制下来到仓库提 Issue:里面带有命中的源、断点续传进度与重试次数,便于定位是哪个源不可达,也方便我们持续补充新的国内镜像。 |
04 四、配置文件配置详解
4.1 Claude Code:~/.claude/settings.json
配置项位于 env 块;首次打开时由程序写入下列模板(_说明 键仅作提示、会被忽略):
[JSON] ~/.claude/settings.json
1 │ {
2 │ "_说明": "首次使用请填写 env 下的 ANTHROPIC_BASE_URL/ANTHROPIC_AUTH_TOKEN/ANTHROPIC_MODEL 后保存。",
3 │ "env": {
4 │ "ANTHROPIC_BASE_URL": "在此填写API地址",
5 │ "ANTHROPIC_AUTH_TOKEN": "在此填写你的密钥或Token",
6 │ "ANTHROPIC_MODEL": "在此填写模型名",
7 │ "API_TIMEOUT_MS": "3000000",
8 │ "CLAUDE_CODE_ATTRIBUTION_HEADER": "0"
9 │ }
10 │ }
字段 | 说明 | 默认 / 示例 |
ANTHROPIC_BASE_URL | API 接口地址(官方或中转) | 待填写 |
ANTHROPIC_AUTH_TOKEN | 鉴权密钥 / Token | 待填写 |
ANTHROPIC_MODEL | 默认模型名 | 待填写 |
API_TIMEOUT_MS | API 请求超时(毫秒) | 3000000 |
CLAUDE_CODE_ATTRIBUTION_HEADER | 是否附带归属标识头(0 关闭) | 0 |
4.2 Codex:~/.codex/config.toml
首次打开时写入的 TOML 模板(含行内注释,支持自定义服务商):
[TOML] ~/.codex/config.toml
1 │ # Codex 配置模板 —— 首次使用请填写 [model_providers.newapi] 下的 base_url 等后保存。
2 │ # 文档:https://developers.openai.com/codex/cli
3 │
4 │ model = "gpt-5.5"
5 │ model_provider = "newapi"
6 │ model_reasoning_effort = "medium"
7 │ disable_response_storage = true
8 │ multi_agent = true
9 │
10 │ [model_providers.newapi]
11 │ name = "newapi"
12 │ base_url = "https://在此填写你的API地址/v1"
13 │ wire_api = "responses"
14 │ requires_openai_auth = true
15 │ env_key = "OPENAI_API_KEY"
字段 | 说明 | 默认 / 示例 |
model | 默认模型名 | gpt-5.5 |
model_provider | 使用的提供方 | newapi |
model_reasoning_effort | 推理强度 | medium |
disable_response_storage | 禁用响应存储 | true |
multi_agent | 启用多 Agent | true |
name | 提供方显示名 | newapi |
base_url | API 基础地址(通常以 /v1 结尾) | 待填写 |
wire_api | 接口协议:chat 或 responses | responses |
requires_openai_auth | 是否需要 OpenAI 式鉴权 | true |
env_key | 读取密钥的环境变量名 | OPENAI_API_KEY |
WARNING env_key 指向的是环境变量名,不是密钥本身 Codex 的 env_key = "OPENAI_API_KEY" 表示 Codex 会去读名为 OPENAI_API_KEY 的系统环境变量取密钥。请在系统环境变量里设置它的值,而不是把密钥直接写进 config.toml。 |
05 五、开源项目地址
•Gitee 仓库:https://gitee.com/dreamer_j/auto-install-cc
•开源许可:MIT License
•作者:Wesky / Dotnet Dancer(公众号)
claude/codex官方文档:
claude code:https://docs.claude.com/en/docs/claude-code/overview
codex:https://developers.openai.com/codex/cli
欢迎 Star、Issue 与 PR。
结尾:创作不易,开源撸码更不容易。如果对你有帮助,欢迎打个赏。当然,小手一抖,点个赞或者喜欢,也不是不可以。
