分享几种 .NET 桌面应用程序自动更新解决方案
做桌面端开发的朋友应该都有过这种经历:产品好不容易发布出去,第二天就有用户反馈“功能怎么还是老的”,一查发现客户端根本不会自己更新,要么让用户手动下载安装包,要么远程指导用户删掉重装。这问题在 .NET 桌面应用(WinForms、WPF 这类)里特别常见,因为应用跑在用户机器上,不像网页改一行代码全局生效。自动更新这个功能,说起来不起眼,但只要你做的是 To B 的桌面工具、行业软件或者任何需要长期维护的客户端产品,它就是一个绕不开的刚需。
这篇文章我就结合自己做 .NET 桌面应用自动更新的实际经验,分享几类主流方案。从微软官方自带机制,到自己写一个轻量更新器,再到引入成熟的开源更新框架,把各自的原理、优缺点、适用场景和实操细节都讲清楚。无论你是刚接手一个桌面项目的新手,还是正在为“客户端永远不更新”发愁的团队负责人,这篇文章都能给你一套可以直接落地的参考思路。
1. 做自动更新前,先把这几个基础问题想清楚
很多人一上来就急着选方案,结果做到一半才发现某个关键环节卡住了。其实自动更新这个功能,拆开来看就是一条固定的链路:版本比对、内容获取、本地替换、失败恢复。你不管用哪种方案,最终都是围绕这四步在做文章。
1.1 更新的本质是一条固定链路
说得直白一点,自动更新就是让程序自己“升级自己”。第一步是版本比对,客户端得知道自己当前是什么版本、服务端上有什么版本,谁新谁旧要能判断出来。第二步是内容获取,确定了有新版之后,把新版的文件从服务器下载到本地。第三步是本地替换,用下载好的新文件覆盖掉正在运行的老文件。最后一步是失败恢复——这一步很多人都忽略,但恰恰是自动更新能不能真正“自动”的关键。
举个例子,用户正在用你的程序处理数据,此时你下载了新版本准备覆盖,但主程序的文件还被进程占用着,覆盖失败怎么办?又比如下载到一半网络断了,本地残留了一个不完整的更新包,下次启动时程序直接崩了,怎么办?这些问题如果你在设计方案之前没有想清楚,后面一定会踩坑。我自己刚开始做的第一版自动更新就是没考虑“失败恢复”,结果测试的时候一切正常,到用户现场就频繁出问题。后来才明白,自动更新这个功能,没人关注你成功了多少次,但只要你失败一次,用户就会记住这次糟糕的体验。
1.2 三个硬指标决定方案选型
在选择具体方案之前,我建议所有团队先用三个指标去衡量一下自己的需求,这样做选型时就不会盲目跟风。
第一是更新成功率。你们的产品跑在什么环境下,是干净的办公电脑,还是各种奇葩环境都有?更新包通常有多大?网络是内网还是公网?这些直接决定了方案的可靠性要求。
第二是可控程度。你需要强制更新还是允许用户自行选择?更新过程中要不要显示进度条?能不能做灰度发布?如果方案对这些问题完全无法定制,那就得考虑是不是该自研。
第三是维护成本。不光是开发成本,还有后续的运营成本。每次发版需要多少人参与?发布流程是拖个文件到服务器就完成,还是需要专门的工具去生成更新包?团队越小,越要重视这个指标。
我把这几点放在最前面说,是因为接下来介绍的每一种方案,本质上都是在这三个指标之间做取舍。没有绝对最好的方案,只有最适合你们当前处境的方案。
1.3 版本号管理是容易被低估的一环
自动更新里最基础但也最容易被忽略的,其实是版本号的定义和比较规则。我见过不少项目,版本号用字符串比较,结果1.10比1.9小的问题就来了。还有的项目版本号一直不更新,导致客户端永远拉不到新版本。
建议统一使用 .NET 里的Version类型,它天然支持四段式版本号(Major.Minor.Build.Revision)的正确比较。服务端存储的是当前最新版本号,客户端请求更新接口时带上自己的版本号,服务端返回“有更新/无更新”的明确结论,同时把更新包地址、版本号、更新说明一起返回。这里有个经验:不要指望客户端自己去对比版本号大小,把判断逻辑统一收敛到服务端。原因很简单,客户端逻辑分散在各个版本里,如果某个老版本本身就存在比较逻辑的 Bug,它永远都无法通过自杀式更新来修复自己。服务端直接告诉它“该更新了,下载这个地址”,可以绕过客户端所有历史包袱。
2. 方案一:走微软官方机制,适合快速上线和中小规模分发
如果你是第一次做自动更新,而且应用场景不复杂,第一个值得考虑的方案就是微软 .NET 框架自带的部署机制,很多人习惯叫它 ClickOnce。这个方案的最大价值在于:你几乎不用写什么业务代码,Visual Studio 里配置一下,发布到服务器或共享目录,客户端启动时就会自动检查更新。
2.1 这个方案的工作流程
ClickOnce 的核心思路是“基于清单文件”的更新。发布工具会把应用打包成一组文件,同时生成一个manifest清单文件,里面记录了所有文件的哈希值、版本号、依赖关系等信息。客户端首次安装时会把这个清单缓存到本地,之后每次启动都用后台线程去请求服务端的新清单,比对版本号。
如果发现有新版本,它会自动下载新清单里列出的所有文件到本地缓存目录,然后用某种映射机制让应用运行在“新版本的文件集合”上。对用户来说,感知通常就是一个“正在更新”的提示,甚至如果配置了静默更新,用户在不知不觉中就已经用上了新版本。
2.2 配置上的几个关键点
用起来确实简单,但我还是要提醒几个配置细节。第一,发布位置很关键。如果你发布到 HTTP 服务器,那客户端走的是标准的 HTTP 下载;如果你发布到 UNC 共享路径,那就是局域网共享方式。对大多数中小企业应用来说,一台内网 Web 服务器或者对象存储服务就够了,成本低,配置也简单。
第二,版本号的管理。在发布的设置项里,你可以选择“每次发布自动递增修订号”。我强烈建议开启这个选项,否则你很可能出现“改了代码但客户端不更新”的灵异事件,真实原因其实是发布版本号没变。
第三,必须对发布产物做签名。ClickOnce 有安全机制,如果清单文件被篡改或者没有正确签名,客户端会直接拒绝运行。这个虽然增加了发布步骤,但也从源头避免了更新包被中间人掉包的风险。
2.3 优点和劝退场景
这个方案最适合的场景是:企业内部工具、中小团队内部使用的管理系统、或者分发渠道相对可控的行业软件。它最大的优势是几乎零代码,Visual Studio 里点点鼠标就能发布,天生支持增量更新——客户端只下载有变化的部分,不会每次全量拉取整个安装包。
但如果你遇到下面这几种情况,我就劝你不要选它了。第一,你的程序需要以管理员权限安装到Program Files目录,ClickOnce 限制很大,它默认跑在用户级缓存里,做不了系统级安装。第二,你需要完全自定义更新界面——比如显示下载百分比、提供“稍后提醒我”的按钮、做公告弹窗,这些 ClickOnce 给的自由度都不够。第三,你的应用需要注册系统服务、安装驱动或者写注册表,ClickOnce 的隔离部署模型会让这些操作变得极其痛苦。
我当初在某个项目里就吃过这亏。产品本身是个配合硬件设备的桌面工具,需要写注册表、装设备驱动,ClickOnce 部署后发现权限模型完全不匹配,最后只能推倒重来自研更新器。所以选这个方案前,先确认你的应用“够传统”,不需要做太多系统级操作。
3. 方案二:自研一个轻量更新器,性价比最高的路线之一
如果你的应用需要做系统级操作、需要自定义更新界面、或者你对整个流程有很强的掌控欲,那自己动手写一个更新器,很可能是性价比最高的路线。别一听“自研”就觉得工程量很大,其实核心逻辑并不复杂。
3.1 整体架构设计
自研更新器通常采用**“主程序 + 更新器”双进程模式**。为什么必须是两个进程而不是在主程序里直接更新?因为你在覆盖文件时,主程序的 exe 文件正在被自己占用,Windows 不允许你直接覆盖一个正在运行的进程对应的文件。所以常规做法是:主程序负责检测更新、下载更新包,然后启动一个独立的更新器进程,更新器负责执行“停掉主程序 → 替换文件 → 重启主程序”这个三部曲。
有些架构还会把“检测更新”和“下载更新”也拆给更新器做,但这会带来一个体验问题:每次启动都要先拉起一个辅助进程,过程会比较慢。我更推荐的做法是主程序自己做检查和下载,只有在真正需要“替换文件”时才让更新器进场,这样平时的启动速度快,更新时的逻辑也足够解耦。
3.2 整个更新流程的关键步骤
我用文字把这个流程拆开,你在实现时照着这个顺序走就不会乱。
第一步,主程序启动后异步请求更新接口,带上当前版本号、操作系统类型、程序位数等参数。服务端判断有新版后,返回新版本号、更新包下载地址、文件大小、哈希值以及更新说明。
第二步,主程序下载更新包到一个临时目录,比如%LocalAppData%\AppUpdater下面。下载一定要用缓冲流写,边下边写,不要一次性 Load 到内存里,否则大包会直接爆掉内存。
第三步,下载完成后做校验。用 SHA256 计算本地文件的哈希值,跟服务端返回的哈希对比,不一致就删除重下,防止下载到损坏的包。
第四步,准备好启动更新器。把下载好的更新包路径、主程序的安装目录、重启参数一起传给更新器进程,然后在界面上提示“更新完成,即将重启”。
第五步,更新器进程启动后,等待主进程退出。这里要注意,Process.WaitForExit()是必须的,不要只Sleep(2000)然后就开始覆盖文件,如果主程序还在做数据保存,直接杀掉进程会导致用户数据丢失。
第六步,真正执行替换。更新器把主程序安装目录下的旧文件逐一替换成更新包里的新文件。为了保底,建议先把旧文件移动到一个备份目录,替换全部成功后再删除备份。这样万一替换到一半出问题,用户下次启动时还能回退到旧版本。
第七步,主程序重新启动,并在启动后通过参数向更新服务端上报新版本号,形成一个闭环。整个更新流程到此才算真正结束。
3.3 一个精简的核心代码骨架
下面是一段精简的 C# 代码骨架,覆盖了“下载更新包并校验”这一步的核心内容,实际项目中你可以在这个基础上扩展。
// 版本检测与下载的简化示范 using System.Net.Http; using System.Security.Cryptography; public class UpdateService { private static readonly HttpClient HttpClient = new HttpClient(); public async Task<bool> TryDownloadPackageAsync( string downloadUrl, string expectedHash, string tempDir) { string tempFile = Path.Combine(tempDir, "update.zip"); Directory.CreateDirectory(tempDir); using (var stream = await HttpClient.GetStreamAsync(downloadUrl)) using (var file = new FileStream(tempFile, FileMode.Create, FileAccess.Write, FileShare.None)) { await stream.CopyToAsync(file); } string actualHash = ComputeSha256(tempFile); if (!string.Equals(actualHash, expectedHash, StringComparison.OrdinalIgnoreCase)) { File.Delete(tempFile); return false; } return true; } private static string ComputeSha256(string filePath) { using (var sha256 = SHA256.Create()) using (var stream = File.OpenRead(filePath)) { return Convert.ToHexString(sha256.ComputeHash(stream)); } } }实际项目中,你还需要补充一个“解压更新包”的方法。这里我的建议是先把 zip 包解压到一个临时目录,然后逐文件比对、逐文件覆盖,不要试图整个目录直接覆盖。逐文件的好处是,如果某个文件正好被其他程序占用,你能捕获到具体的异常并做出更精细的处理,而不是整个更新直接失败。
3.4 为什么替换阶段必须做备份与回滚
说到替换,这里必须展开讲一个很多人容易疏忽的点:回滚能力。我之前做第一版自研更新器时,图省事直接在临时目录解压后就往安装目录里塞,结果有一次在某个用户的机器上,更新包中一个文件损坏,解压后文件列表不完整,我把新文件拷过去之后,主程序直接缺 dll 启动崩溃。
后面我调整了策略:替换之前先建一个备份目录,把待覆盖的旧文件全部移过去,等新文件全部就位、并且主程序能正常启动后再删备份。更新的安装包本身也留一份在临时目录,不急着删。这样处理之后,即使遇到启动崩溃,我还能让更新器检测到“上次更新未确认”,自动从备份恢复现场。虽然多占了点磁盘空间,但换来的安全边际是值得的。
建议在你的更新器里做一个“更新状态标志”,比如写一个标记文件到安装目录,主程序启动成功后主动删掉这个标记。下次启动时如果发现标记还在,说明上一次更新没走完,自动进入回滚流程。这个设计说起来简单,但真正能把更新成功率做到 99% 以上,靠的就是这种兜底逻辑。
3.5 自研方案的价值和使用前提
自研更新器最大的价值是定制自由。你可以任意设计更新流程、界面、交互,可以自己接入灰度发布、强制更新、分批放量等功能。但它也要求团队具备一定的工程能力,至少对进程通信、文件安全、Windows 权限模型这些基础有了解。
如果你们团队只有一两个人,而且系统部署环境单一,我更建议用方案三,也就是成熟的开源更新框架,因为自研的边际成本只有在复杂的业务需求下才划算。
4. 方案三:引入成熟开源更新框架,低成本获得增量更新体验
如果你不想像 ClickOnce 那样被官方机制限制,又不想完整自研一套,那取中间路线:引入社区里成熟的开源更新框架。这类组件在 .NET 生态里并不少见,有的专注于“带安装体验”的更新,有的专注于“增量同步文件”,选对类型可以帮你省掉非常多的底层工程。
4.1 这类框架到底帮你解决了什么
一个成熟的更新框架,通常已经帮你实现了下面这些能力:更新包的自动化打包与发布、更新源清单的生成、客户端版本检查逻辑、更新包下载与校验,甚至还能提供安装/更新过程中执行脚本的能力。这意味着你从“造轮子”变成“用轮子”,需要考虑的核心只剩业务逻辑和发布流程。
尤其是增量更新这一块,框架的价值非常明显。一个桌面应用积累到几十上百 MB 是常有的事,如果每次更新都全量下载,用户在内网还好,公网上就会明显影响体验。增量更新的原理说起来并不神秘:更新清单文件里记录了每个文件的哈希值和版本状态,客户端拿到新清单后,逐文件比对本地目录的文件哈希,有变化的才下载,没变化就跳过。这样一次 50MB 的应用,如果只改了一个 200KB 的 dll,用户实际下载量就只有 200KB 加清单开销。
4.2 小型团队怎么快速搭起更新服务器
很多团队一听“引入框架”,就担心还要搭一套服务端。实际上,更新服务端的门槛并没有你想的那么高。最轻量的做法是:准备一台静态文件服务器,或者用一个对象存储桶,把更新清单文件和更新压缩包丢进去就行。客户端配置一个更新源地址,框架自行去请求清单、解析版本、下载文件。
比如你可以在发布机上跑一个自动打包的脚本,每次发布时自动生成新的更新清单和更新包,然后上传到对象存储的某个固定前缀路径下。整个流程完全可以做到“双双击就发版”,不需要专门的后端接口服务。对于大多数中小企业场景,这是非常实用的落地路径。
4.3 框架接入的几个实操建议
这类框架的接入大体分三步。第一步,在你的主工程里引用对应的 NuGet 包,并初始化更新器的配置,包括更新源地址、当前安装版本、安装目录等。第二步,在主程序的启动流程或者某个设置页面里调用检查更新的 API。第三步,也是很多人容易忽略的,要把框架生成的“更新包描述文件”纳入到你的 CI/CD 脚本里。很多团队手动打包后忘了生成对应的清单,导致客户端永远判断不出有新版。
用框架时还有一点要提醒:尽量使用框架推荐的打包方式去生成更新包,而不是随便打个 zip。因为它们通常不只是“解压覆盖”这么简单,还会记录文件权限、符号链接、或者一些特殊的文件元数据。你如果用手工打包的方式破坏了这些信息,将来更新后可能会出现难排查的运行时问题。我自己就遇到过更新后但配置文件被重置的怪现象,排查到最后才发现是手工打包导致文件权限位丢失。
4.4 这种方案的上限和妥协之处
说实话,这些成熟框架也有自己的适用范围限制。比如它们对非常特殊的部署布局兼容性可能不够好,如果你的应用要在多个不同的安装目录之间切换、或者存在一个程序多实例运行的场景,继承框架的默认流程就不够用了。此外,框架本身也有升级维护的需求,一旦框架的主版本升级,你的接入代码也可能需要同步调整。
所以我的判断标准是:如果你的应用是一个常规的单实例桌面程序,打包目录结构不奇葩,那用成熟框架是最优解;如果你们的产品运行模型已经比较特殊了,那还是回到自研方案,不要因为怕麻烦而硬套框架。
5. 更新过程中最容易翻车的几个现场与排查实录
前面讲的都是方案层面的内容,但真正让你“从入门到放弃”的,往往是那些看起来不起眼、但一上线就炸的现场问题。这一节我把自己和身边同行在自动更新上踩过的坑,挑几个最有代表性的写出来,给大家一个排查思路。
5.1 文件被占用导致覆盖失败
这是全行业桌面更新最经典的问题。用户开着你程序的编辑窗口,里面还有内容没保存,这时更新器收到指令杀掉了主进程。但用户在另一台机器上通过共享方式打开着你机器上的某个配置文件,或者杀毒软件正拿着你的 exe 在做扫描,Windows 就会给你一个“文件被另一进程使用”的访问违规错误。
排查思路:先把报错文件路径记录下来,更新器增加重试机制,比如每 2 秒重试一次、持续 10 次。如果还失败,把失败文件列表写进日志。等下次主程序启动时,通过“续传/补更”的方式重新尝试。有一些框架会提前检查“哪些文件即将被覆盖”并提示用户关闭相关程序,这也是一个减少失败率的实用做法。我在自己项目里还加了一步:更新替换前先尝试用只读方式打开所有待覆盖文件,预检一遍占用情况,提前预警,比实际覆盖时再报错体验好非常多。
5.2 更新包下载不完整,客户端启动即崩溃
下载中断、网络慢、服务端返回了错误页面但客户端没识别出 HTTP 状态码,都会导致本地出现一个“尺寸不对或内容不对”的更新包。如果代码里没有做校验,直接解压覆盖,那程序启动时大概率会崩在某个 dll 加载环节。
排查思路:所有下载操作完成后,强制进行哈希校验,不一致就去掉。同时,将更新包先解压到独立目录,并在解压完成后做一次“文件数量和大小完整性校验”,确认无误后再执行替换动作。还有一个容易被忽略的细节:解压时如果磁盘空间不足,解压到一半 zip 文件会损坏或者丢文件。建议解压前先检查磁盘剩余空间是否大于更新包大小的两倍。
5.3 服务端清单出错导致所有客户端陷入“更新回退风暴”
这是最容易引发生产事故、但也最不常被提及的坑。想象一个场景:客户端版本是 1.2,服务端把清单里的版本错配成了 2.0,但更新包实际还是 1.2 的内容。客户端更新后发现“版本号变高了,但文件跟旧版一样”,下一次启动时它又去请求服务端,发现服务端说“你已经是 2.0 了”没有新包,于是用户的机器就那么在混乱的状态里运行着。
比这个更严重的是另一个方向:服务端上线回滚时,把版本号从 2.0 降回 1.5,所有已经在 2.0 的客户端会再次收到“有更新”,于是大量客户端同时回退,形成一个大规模“回退风暴”,网络带宽和用户困惑都爆表。
排查思路:更新服务端的版本记录要保留“发布时间”这个维度,客户端请求时带上自身版本和最近更新时间,服务端不要只判断“是不是同一个版本号”,还要判断“这个版本号是否比当前客户端旧”。同时,所有版本发布记录必须走配置化而非人工改数字,避免手滑。
5.4 权限问题:为什么有的机器能更新,有的机器提示拒绝访问
最常见的权限场景是:用户不是管理员,程序被安装到了C:\Program Files\下。这个目录对普通用户默认没有写权限,更新器在替换文件时就会被拒绝。
排查思路:更新器进程可以以管理员身份重新启动(UAC 弹窗),或者干脆从一开始就把应用的安装位置放到用户目录下,比如%LocalAppData%\Programs\。我一般建议桌面应用默认装到用户级目录,这样既不需要 UAC,又能正常自动更新。如果业务上必须装到 Program Files,那就必须接受每次更新会有 UAC 弹窗的体验,同时要做好权限不全时的降级提示。
5.5 更新界面不清晰,用户以为程序卡死了
这个不算技术故障,但体感杀伤力最大。有些应用更新时只在托盘区闪个图标,或者完全没有界面,用户根本不知道发生了什么。一旦更新包大一点、网络慢一点,用户就以为程序坏了,直接打开任务管理器把进程结束,然后留下一个半更新的现场。
排查思路:更新期间务必给出明确的界面状态,至少包含“正在下载 x% / 正在安装 / 正在重启”三档。下载和安装阶段不要用“假进度条”——如果没法精确计算,就诚实地显示“正在更新中,大约需要 x 分钟”。在强制更新场景下,还要明确提示“请勿关闭电脑或断开网络”。这些细节做得到位,自动更新的投诉率能降一多半。
6. 选型建议与实操心得
写了这么多,最后把选择思路再梳理一下,顺便聊几个我实际操作中沉淀下来的心得。
6.1 不同团队规模的选型决策表
我用下面这个表总结不同场景下的推荐方案,供你参考:
| 团队情况 | 应用类型与分发场景 | 推荐方案 | 原因 |
|---|---|---|---|
| 单人/小团队,无专职运维 | 内部工具、原型系统,内网分发 | 微软官方机制 | 零代码、起步快,发版成本低 |
| 普通研发团队,有打包机 | 常规 To B 桌面工具,公网或内网分发 | 开源更新框架 | 平衡开发量与定制能力,自带增量更新 |
| 研发团队,产品形态复杂 | 需要写驱动、注册系统服务、多实例运行 | 自研更新器 | 官方和后端框架都满足不了特殊部署需求 |
| 成熟团队,有发布平台支撑 | 用户量大、需要灰度与强制更新策略 | 自研更新器 + 发布平台 | 全流程可控,策略可以按需扩展 |
这个表只是一个起点,不是绝对答案。你们的具体场景如果有特殊之处,完全可以在某个方案的基础上做局部改造,比如用开源框架再加一层镜像下载逻辑。
6.2 灰度发布和强制更新怎么和更新机制配合
我之前一直强调自动更新不只是“把文件换成新的”,更是一个运营策略承载点。灰度发布就是典型的例子:先在更新接口上设置一个白名单比例,比如只有 10% 的客户端会收到“有新版”的响应,其余的用户仍然返回“已是最新版本”。通过日志观察这 10% 用户的崩溃率和活跃数据,再逐步放大比例,直到全量。
强制更新则要用一个“最低允许版本号”来控制。服务端除了返回“最新版本号”,还要返回“最低可运行版本号”。客户端的版本低于最低可运行版本时,只能看到“必须更新”的弹窗,不能进入主界面。这个方法在遇到“旧版本有严重缺陷、必须全员升级”的紧急情况时非常有用。
6.3 更新日志与监控最好从第一天就做
不少团队把自动更新做成一个“一次性功能”,上线后就不管了。但更新本身是需要监控的。建议你在客户端埋两条日志:一条是“收到更新响应”,另一条是“更新成功/失败”。服务端把这些数据汇总起来,你就能实时看到当前各个版本的占比、今日更新成功率、失败的主要原因分布。
有条件的团队还可以进一步统计“某次发布后的崩溃率趋势”。要做这一步,更新信息里带上具体的“发布批次号”,一旦新版本出现严重问题,你能很快定位到是哪次发布引入的,并为回滚决策提供数据支持。对自动更新这种“平时没人关心、一出事全是大事”的功能来说,监控的价值怎么强调都不为过。
6.4 我踩过几次坑之后形成的检查清单
最后送大家一份我自己踩了无数次坑之后整理出的检查清单,每次发版前照着过一遍,能避免掉九成以上的低级事故。
- 版本号是否通过发布工具自动递增,有没有可能出现新旧版本号相同的情况。
- 更新包里的文件列表是否与预期一致,有没有遗漏必备的配置文件或者数据文件。
- 更新包是否已经做签名,清单文件的哈希是否与文件内容匹配。
- 是否有回滚机制。如果新版本启动即崩溃,用户机器能否恢复到上一个正常版本。
- 更新过程中是否提示用户“正在更新,请勿关机”,下载期、安装期、重启期是否有清晰的状态反馈。
- 灰度发布比例是否设置正确,强制更新的“最低版本”是否写对。
- 服务端日志和客户端日志是否都已经接入,发布后有没有人可以观察到更新成功率和崩溃率。
- 磁盘空间检查是否做了,尤其是用户 C 盘常年余量不足的情况下,解压和备份是否可能失败。
把这份清单落地到你们的发布流程规范里,再配合一套可靠的更新机制,自动更新这个基础能力才算真正做扎实。桌面应用不像 Web 可以无缝升级,但只要链路设计得足够稳、兜底做得足够多,用户完全可以做到“不知不觉就升到了新版本”。这也是我们做桌面端的人,能给用户提供的最基本的体面。