上个月我把用了两个多月的 ZCode 请出了开发环境,顺手把 Windows 安装包构建流程从本地脚本搬到了 GitHub Actions。这套组合拳打完,最直观的变化是:我再也不用在晚上十一点等 electron-builder 慢慢磨出一个 exe,也不用担心半夜打包到一半被 UAC 弹窗卡住。如果你正在用 AI 编程工具做 Windows 端项目,又被“本地打包慢、环境乱、数据到底安不安全”这三件事反复折磨,这篇是我踩完坑之后的完整记录。内容分成工具替换和流水线自建两半,前半讲为什么抛弃 ZCode、DeepSeek Harness 值不值得换,后半是我在 Actions 上搭 Windows 打包流水线的全部配置文件、缓存方案和排错实录。个人项目、小团队接外包的,基本可以照着抄。
1. 弃用 ZCode 的真实原因,不只是“偷代码”风波
1.1 补全效果确实好,但数据流向是个黑盒
先给 ZCode 说句公道话:它的自动补全和代码解释能力在一众 AI 编程工具里属于第一梯队,装进 IDE 之后,写业务代码的效率提升是肉眼可见的。我最初选它,就是看中它那种“我刚敲完函数名,它已经把整个函数体补出来”的流畅体验。注册账号、下载安装、登录即用,几乎没有上手成本。
但真正让我下决心的不是“补全好不好用”,而是“数据到底去了哪”。那阵子社区里连续出了几轮和 ZCode 相关的讨论,被概括成“偷传代码风波”。我一开始对这类说法是持保留态度的,毕竟现在的 AI 编程工具要做上下文理解,不可能完全不和服务器通信。真正让我警惕的是我自己做的一次观测:在编辑器完全闲置、焦点长时间不变的情况下,ZCode 对应的进程依然维持着固定频率的网络连接。上传内容无法解密,但它到底在传什么、传多少、存多久,我作为用户完全不知道。
后来我去翻了它的隐私协议,写得不算离谱,用的也是“我们可能收集必要的信息以改进产品”这种话。但问题就出在这:一个做代码补全的工具,到底哪些算“必要信息”,用户是没有办法验证的。我当时手头有几个保密要求很高的项目,代码一旦出网,就没有撤回的余地。在这种前提下,哪怕它真的只是在做遥测,我和团队也没法接受这个风险。工具是拿来提高效率的,不是拿来增加合规焦虑的。信任这层窗户纸一旦破了,补全效果再好也白搭。
1.2 比隐私问题更烦人的,是本地打包的三个老毛病
按说隐私问题已经足够让我换工具了,但真正在每天的工作里消耗我的,其实是本地打包这件事。我和很多开发者一样,习惯在一台主力机上跑构建,但这台机器身上至少有三个治不好的老毛病。
第一是打包耗时长。一个 Electron 项目,冷缓存状态下跑 electron-builder,轻轻松松二十到三十分钟。中间要是再赶上 Vite 或 Webpack 的慢编译,加上偶尔蹦出来的 Maven 打包报错——尤其是 IntelliJ 里那套“依赖冲突 + 环境变量缺失”组合拳,整个下午就耗进去了。我一度还专门研究过 webpack 打包优化配置,别名、多线程、缓存策略都试过,收益始终有限。第二是环境像豆腐渣。本机装过什么依赖、改过什么环境变量、杀毒软件拦截过哪个进程,全都是不确定因素。今天能打的包,明天换一台机器大概率打不出来。第三是产物管理混乱。release 里的 exe 今天传到网盘,明天复制到 U 盘,版本号偶尔还会忘了改。有一次我打包出的安装包在客户机器上提示缺少 DLL,排查了半天,发现是某次打包漏装了依赖,而当时那个构建环境早就没法复现了。
这两件事凑在一起,足以让我下定决心:要么换一套工具链,要么改变构建方式。最后我两个都做了。
2. DeepSeek Harness 上手:从安装失败到多智能体编排
2.1 Harness 是什么,它和 ZCode 这类插件的本质区别
DeepSeek Harness 和 ZCode 完全是两个物种。ZCode 是寄生在 IDE 里的助手,你写代码的时候它在旁边给你补全、解释、生成。而 Harness 更像是一套以模型端点为基础的 Agent 工作台,你自己定义任务,它自己跑流程。核心概念有三个:skill 是给 AI 写的“岗位说明书”,agent 是执行任务的智能体,workflow 是把多个智能体串起来的编排脚本。
对我来说最关键的区别有两点。第一是数据可控性,Harness 是开源项目,模型推理走的是你自己配置的 API endpoint,网络调用模块的代码就摆在仓库里,你能看清楚它在什么时候联网、联到什么地址、传了什么参数。第二是可编排性,ZCode 给人的感觉是“一个人在 IDE 里被 AI 扶着走”,而 Harness 给我的感觉是“我定义好任务,机器自己跑完”,从代码分析到测试执行再到构建产物校验,都能拆成不同的角色去跑。这种差别,用过之后基本回不去。
2.2 安装、版本回退与 Skill 配置实录
Harness 的安装没有想象中玄乎。我用 git clone 拉源码,装依赖,再跑一下自检命令就能起来。真正折腾的是版本问题:0.1.5 正式版在我机器上装了三次,三次都挂在依赖编译上,不是这个包版本对不上,就是那个模块在新 Python 环境下编译失败。网上搜到的解决办法高度一致:先回退到 v0.1.5-rc.2。我照做之后,问题确实消失了。这种“正式版反而比 RC 版难装”的事在开源项目里不算罕见,所以后来我养成了一个习惯:装新版本之前先看一眼 issue 区,如果大面积反馈安装失败,就别当那个趟雷的人。
装好之后,端口冲突又给了我一个下马威。Harness 默认占用一个本地端口,和另一个常驻服务撞车了。Windows 下查端口占用很简单:netstat -ano | findstr 端口号,找到 PID 再用 taskkill /PID xxx /F 结束掉。但为了不每次启动都打架,我直接改了配置里的端口号,改成 17890 之后清净到现在。如果你也遇到类似问题,先别急着杀进程,改配置才是治本方案。
Skill 配置是 Harness 的精华。我用一个 YAML 文件定义了一个 Windows 打包技能,结构大概是这样:
name: windows_build description: 执行 Windows 安装包构建并校验产物 steps: - role: planner prompt: 分析当前分支的最近提交,确认需要执行的构建命令 - role: executor tool: shell cmd: npm run build:win - role: verifier tool: file_check args: paths: - "release/*.exe"这段配置的意思是:先让 planner 看一眼最近的代码改动,判断要不要走完整构建;然后 executor 执行真正的打包命令;最后 verifier 检查 release 目录下有没有 exe 生成。三个角色共享同一个工作区,但各自的职责是隔离的。这种“给 AI 写岗位说明书”的用法,比在 IDE 里靠插件闲聊式的交互要可控得多。
多智能体编排我也实际跑过。一次发布拆成 planner、coder、reviewer、builder 四个角色,planner 负责看改动范围,coder 改代码,reviewer 检查锁文件是否更新,builder 只允许跑构建脚本、不允许动源代码。这样 AI 在构建环节里就算想乱改东西也没有权限,因为它的 prompt 里根本没给编辑器工具。这个模式用在自动化发布上特别稳。
3. 把 Windows 打包搬进 GitHub Actions:完整 workflow 拆解
3.1 为什么是 GitHub Actions,而不是本地脚本或者别的 CI
工具链换了,构建方式也得换。我评估过几个方案:继续用本地脚本、自建 CI、用 GitHub Actions。本地脚本第一个被淘汰,原因前面已经说过了,环境不可复现,问题无法追踪。自建 CI 对个人项目来说运维成本太高,光维护一个 Windows 构建机就够喝一壶的。
GitHub Actions 的优势在于:windows-latest runner 是现成的,预装了 MSVC、PowerShell、Node 等一套常用工具;每次构建都从干净环境开始,本地那种“能编过是因为碰巧装了某个库”的幻觉会被直接打破;缓存机制成熟,electron 这种大依赖可以跨构建复用;触发策略灵活,可以只在打 tag 时构建,避免浪费额度;日志可追溯,历史构建随时可以回放对比。还有一个重要原因:Harness 本身支持命令行调用,我后来干脆把 AI 生成发布说明这一步也塞进了 workflow,让整个发版流程在同一个平台上闭环。
3.2 workflow 骨架:触发、矩阵、缓存、产物一个不少
下面这个 workflow 文件是我当前在用的完整版本,删掉了仓库特有的敏感信息,结构可以直接抄:
name: build-windows-installer on: push: tags: - "v*" workflow_dispatch: permissions: contents: write jobs: build: runs-on: windows-latest strategy: matrix: node-version: [18, 20] steps: - uses: actions/checkout@v4 - name: Setup Node uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} cache: npm - name: Cache Electron uses: actions/cache@v4 with: path: | ~/AppData/Local/electron/Cache ~/AppData/Local/Yarn/Cache key: ${{ runner.os }}-electron-${{ hashFiles('package-lock.json') }} - name: Install dependencies run: npm ci - name: Run tests run: npm test - name: Build Windows installer run: npm run build:win env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Upload artifact uses: actions/upload-artifact@v4 with: name: installer-node${{ matrix.node-version }} path: release/*.exe - name: Create Release if: startsWith(github.ref, 'refs/tags/') uses: softprops/action-gh-release@v2 with: files: release/*.exe逐段说几个关键点。
触发条件我故意只写了 tag 推送和手动触发两种。如果每个 commit 都触发,私有仓库的 Actions 免费额度很快会被耗尽,而且绝大多数中间提交根本没有打包的必要。只有确认要发版了,打个 v1.2.3 这样的 tag,流水线才动起来。workflow_dispatch 保留着,是为了需要手动重跑某个历史构建时能随时兜底。
permissions 这段很多人会漏掉。GitHub 对 GITHUB_TOKEN 的默认权限越来越保守,如果不显式声明 contents: write,后面 softprops/action-gh-release 上传 release 资产时会直接 403。声明之后 token 的权限范围也很窄,只够当前工作流操作仓库内容,不会暴露给外部。
strategy.matrix 用的是 node 18 和 20 两个版本。有人可能会问:一个打包任务为什么要跑两个 node 版本?因为我真遇到过一次莫名其妙的兼容问题:某个依赖在 node 18 下构建一切正常,在 node 20 下却会在运行时报一个奇怪的错。这个错在本地永远发现不了,因为我本机只装了 node 20。用了矩阵之后,这类问题在 CI 里直接暴露出来,修复之后两个版本都能顺利产出安装包。
缓存是流水线提速的关键。setup-node 里我写了 cache: npm,它会自动处理 npm 依赖的缓存。electron 本身是更大的体积,所以额外加了一个 actions/cache 步骤,缓存路径指向 Windows 下 electron 的缓存目录。key 用的是 runner.os 加 package-lock.json 的哈希,这样当 electron 版本不变、锁文件也没变的时候,可以稳定命中缓存,整个构建时间能从“全量下载依赖的 20 分钟”降到“8 到 10 分钟”。
安装依赖用的是 npm ci 而不是 npm install。这两者的区别是:npm install 可能会改 package-lock.json,而 npm ci 会严格按照锁文件安装,任何不一致都会直接报错。在 CI 环境里,严格模式恰恰是我们要的,它能保证“本地锁文件提交干净”这个最基本的要求。
Windows 安装包签名这件事,我单独多说几句。如果你没有代码签名证书,打包出来的 exe 在用户机器上一定会触发 SmartScreen 警告。CI 环境并不能绕开这一点。个人开发者的现实选择是用自签名证书或者测试证书,但用户侧的红字警告依然存在。如果是公司项目,建议申请 OV 或 EV 代码签名证书,然后把 pfx 证书文件放到 GitHub 的仓库 secrets 里,在 workflow 中引入并用 signtool 签名。相关命令大概是这样的:
signtool sign /fd SHA256 /f certificate.pfx /p $env:CERT_PASSWORD /tr http://timestamp.digicert.com /td SHA256 release/MyApp-setup.exe注意,证书密码绝对不要硬编码在 workflow 文件里,要用 secrets 引用。签完名之后再上传 artifact 和 release,用户侧的信任度会大幅提升。
3.3 release notes 直接用 AI 生成
构建完成之后,写发布说明又是一个容易偷懒的环节。我现在的做法是在 CI 里直接调用 DeepSeek 的 API,根据两次提交之间的 log 自动生成中文发布说明,然后挂到 release 上。workflow 里加这么一段:
- name: Generate release notes with DeepSeek if: startsWith(github.ref, 'refs/tags/') env: DEEPSEEK_API_KEY: ${{ secrets.DEEPSEEK_API_KEY }} shell: pwsh run: | git fetch --unshallow $log = git log "$env:GITHUB_SHA~1..$env:GITHUB_SHA" --oneline $headers = @{ Authorization = "Bearer $env:DEEPSEEK_API_KEY" } $body = @{ model = "deepseek-chat" messages = @( @{ role = "user"; content = "根据以下提交记录生成中文发布说明:`n$log" } ) } | ConvertTo-Json -Depth 5 $resp = Invoke-RestMethod -Method Post -Uri "https://api.deepseek.com/chat/completions" -Headers $headers -ContentType "application/json" -Body $body Set-Content -Path RELEASE_NOTES.md -Value $resp.choices[0].message.content这段脚本在本地跑是拿不到准确提交区间的,正是因为 CI 环境天然拥有完整的 git 历史和精确的 before、after 引用。我试过手动写发布说明,经常写着写着就漏了两条提交;AI 生成的虽然需要小改一下措辞,但至少不会漏内容。这个思路可以推而广之:AI 不一定非得干“写代码”这种创造性最强的活,把它放到“从提交记录提炼 changelog”这种机械又琐碎的任务上,性价比反而更高。
4. 迁移路上踩过的坑:问题排查与避坑速查表
4.1 本地正常、CI 失败的三类典型问题
第一次在 Actions 上跑通完整流程之前,我至少被三个问题卡了一整天,而且全都是“本地一切正常,CI 突然报错”的经典剧毒问题。
第一个是 shell 语法冲突。windows-latest runner 上,默认的 shell 是 PowerShell,而不是 Linux runner 上的 bash。我在本地一直用 export FOO=bar 这种写法,搬到 workflow 里直接报错。解决方式有两种:要么在 run 步骤里显式指定 shell: bash,要么干脆按 PowerShell 的语法写。我现在统一用 shell: bash,因为很多脚本是跨平台复用的。第二个是换行符问题。Git 在 Windows 上默认开启了 autocrlf,拉下来的脚本文件变成了 CRLF 换行,bash 执行时偶尔会出奇怪的错误。解决方式是在仓库根目录加一个 .gitattributes,强制让 .sh 和 .ps1 文件保持 LF 换行。这个坑很隐蔽,因为平时根本注意不到换行符的存在。第三个是缓存 key 设计不合理导致缓存形同虚设。我一开始的 key 写得太粗,electron 版本升级后还一直命中旧缓存,构建日志里反复出现旧版本残留。最后改成“锁文件哈希 + 缓存路径精确到 electron/Cache”才真正生效。缓存的本质是一个“可寻址的脏数据”,key 设计得好,它是加速器;key 设计得不好,它就是定时炸弹。
还有一个非常容易犯的错是没显式声明 permissions。第一次用 softprops/action-gh-release 上传 exe 时,任务跑到最后一步直接 403。翻日志才发现是 GITHUB_TOKEN 默认权限不够。所以在 workflow 顶部加上 permissions: contents: write 是必须的,不是什么锦上添花。
4.2 迁移中遇到的典型问题速查表
| 症状 | 大概率根因 | 处理建议 |
|---|---|---|
| 本地打包正常,CI 报“找不到某命令” | runner 上没有安装全局依赖 | 不要依赖全局环境,用 npx 或显式声明需要的 toolchain |
| exe 生成了,但安装后提示缺 DLL | 部分依赖没有打进安装包 | 检查 electron-builder 的 extraResources,在 CI 里加文件数量 sanity check |
| 上传 release 资产 403 | workflow 缺少权限声明 | 顶部添加 permissions: contents: write |
| npm ci 报 lockfile 不匹配 | package-lock.json 未提交或本地被修改 | 把锁文件提交进仓库,本地不要用 npm install 乱改 |
| 构建日志中文乱码 | 编码不一致 | 在 PowerShell 中设置 [Console]::OutputEncoding,或统一使用 UTF-8 |
| electron 每次都重新下载 | 缓存路径或 key 设计不合理 | 确认 Windows 下 electron 缓存路径,key 用锁文件哈希 |
| 每次 push 都触发构建 | 触发条件过宽 | 限定 tags 或 paths 过滤,节约 Actions 额度 |
补充一个属于“独家心得”的土办法:我在 workflow 里加了一个 sanity check,用 7z 列出安装包内的文件数量,如果低于阈值就直接 fail。这个检查看起来笨,但真的能拦住“源码改了却没打进包”这种低级事故。CI 的价值不只是自动化,更是在每个环节给你一个显式的信号,对还是不对,一眼就知道。
5. 迁移之后说点实话:收益、成本和工具心态
5.1 数据对比:本地打包与 CI 打包的真实差距
迁完之后我特意统计过一批数据。本地冷缓存跑一次完整构建,耗时基本在二十分钟到三十分钟之间;CI 环境首次构建因为要下载依赖,大概在十二到十八分钟;等电子缓存命中之后,后续构建能稳定压到八到十分钟。但真正的差异不在单次构建耗时,而在三个方面:环境一致性、可追溯性、多版本覆盖。CI 环境下,每次构建都是同一个起点,出问题可以复现,复现就可以修复。本地环境则永远是“薛定谔的构建”,今天能过,明天不一定。
成本方面,个人仓库在 GitHub Actions 上有免费额度,私有仓库也有月度免费分钟数,对多数独立开发者来说基本够用。如果项目变大、构建次数变多,还可以考虑减少矩阵版本或提高缓存命中率来省钱。相比自己养一台 Windows 构建机,这个成本几乎可以忽略。
这个迁移带给我最大的感受是:构建这件事终于从“靠运气”变成了“靠流程”。本地打包让我每发一次版都提心吊胆,CI 流水线跑完之后,产物、日志、版本号全部自动归档,出问题一眼就能定位到是哪一步。
5.2 工具心态和数据主动权
最后说说工具心态。我见过很多开发者选 AI 编程工具的标准是“谁生成代码更强”,这个标准当然重要,但它不是全部。对一个闭源的 IDE 插件来说,你看到的是它生成代码的准确率,看不到的是它背后做的采集、传输和存储。ZCode 的补全质量确实让我犹豫过,但一想到数据流向是一个黑盒,我觉得这个风险不值得冒。
DeepSeek Harness 的好,不在于它的代码生成能力一定比 ZCode 强多少,而在于它把控制权还给了用户。模型端点我自己配,skill 我自己写,工作流我自己编排,它到底在什么时候联网、联到哪里、传了什么数据,源码里都能查得到。这种“基础信任”在长期使用中产生的价值,比某一次补全惊艳带来的爽感要高得多。
现在我的发版流程已经基本固定:本地写好代码,推到远程,打 tag,GitHub Actions 自动完成测试、构建、签名、生成发布说明、上传 release 这一整套动作。而我本人要做的,就是最后在 release 页面手动下载一个 exe,装到干净的虚拟机里验证一遍。就在写这篇记录的时候,流水线刚好又跑完一次,release 页面多了一个新的安装包,RELEASE_NOTES.md 也已经躺在里面了。数据主动权掌握在自己手里,构建过程暴露在阳光下,这种感觉,比任何“一键打包”的爽感都踏实。