(开源地址在文末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。

结尾:创作不易,开源撸码更不容易。如果对你有帮助,欢迎打个赏。当然,小手一抖,点个赞或者喜欢,也不是不可以。

Logo

欢迎加入DeepSeek 技术社区。在这里,你可以找到志同道合的朋友,共同探索AI技术的奥秘。

更多推荐