<?xml version="1.0" encoding="utf-8"?><?xml-stylesheet type="text/xsl" href="rss.xsl"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
    <channel>
        <title>Infinity CI Blog</title>
        <link>https://networm.github.io/infinityci.github.io/en/blog</link>
        <description>Infinity CI Blog</description>
        <lastBuildDate>Sun, 20 Sep 2026 00:00:00 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <language>en</language>
        <item>
            <title><![CDATA[9 天、90 个提交：我用 Z Code 从零写出一个 CI 服务器]]></title>
            <link>https://networm.github.io/infinityci.github.io/en/blog/zcode-9-days-ci</link>
            <guid>https://networm.github.io/infinityci.github.io/en/blog/zcode-9-days-ci</guid>
            <pubDate>Sun, 20 Sep 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[全程只用了 Z Code 一款 AI 编程工具。第一天开工约 2 小时，一个基本可用的 CI 就跑起来了；9 天后，它在我的 Windows 打包机上稳定执行 Unity 6 打包。]]></description>
            <content:encoded><![CDATA[<blockquote>
<p>全程只用了 Z Code 一款 AI 编程工具。第一天开工约 2 小时，一个基本可用的 CI 就跑起来了；9 天后，它在我的 Windows 打包机上稳定执行 Unity 6 打包。</p>
</blockquote>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="为什么要自己写一个-ci">为什么要自己写一个 CI<a href="https://networm.github.io/infinityci.github.io/en/blog/zcode-9-days-ci#%E4%B8%BA%E4%BB%80%E4%B9%88%E8%A6%81%E8%87%AA%E5%B7%B1%E5%86%99%E4%B8%80%E4%B8%AA-ci" class="hash-link" aria-label="Direct link to 为什么要自己写一个 CI" title="Direct link to 为什么要自己写一个 CI" translate="no">​</a></h2>
<p>我是做 Unity 开发的，一直有个老大难问题：<strong>打包</strong>。</p>
<p>每次出包，要在 Unity 编辑器里点 Build，盯着进度条，等十几分钟，然后手动拷贝产物、通知测试。想挂到 CI 上自动化？看了看市面上的方案：</p>
<ul>
<li class=""><strong>Jenkins</strong>：功能全，但 Java 生态 + 插件地狱 + 要装数据库，为了打个包维护一整套 Jenkins 太重了；</li>
<li class=""><strong>GitHub Actions</strong>：体验很好，但它是云端服务，游戏项目动辄几个 GB 的工程和出包环境，而且很多时候代码根本不能出内网；</li>
<li class=""><strong>TeamCity / GitLab CI</strong>：要么收费要么又背上一个大平台。</li>
</ul>
<p>我想要的其实很简单：一个<strong>轻量的、跑在自己机器上的 CI</strong>，有个 Web 界面能看到构建日志，能分发任务到装了 Unity 的打包机，微信里能收到构建结果通知。这个需求看似简单，但把 Jenkins/GitHub Actions 的核心体验（并行任务、依赖图、实时日志、分布式 Agent）都做齐，传统认知里没有几人月下不来。</p>
<p>于是 9 月 9 日晚上 11 点 40 分，我打开了 Z Code，发了一条消息：</p>
<blockquote>
<p>现在使用 C# .NET 10 做一个类似 Jenkins 的 CI 服务器软件，应该做哪些技术选型？前端后端都算上</p>
</blockquote>
<p>这篇文章记录的，就是接下来 9 天里，我如何用 Z Code 这一款 AI 工具，从这一条消息写到 Unity 6 打包 Windows 全流程跑通。</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="先看结果infinity-ci-是什么">先看结果：Infinity CI 是什么<a href="https://networm.github.io/infinityci.github.io/en/blog/zcode-9-days-ci#%E5%85%88%E7%9C%8B%E7%BB%93%E6%9E%9Cinfinity-ci-%E6%98%AF%E4%BB%80%E4%B9%88" class="hash-link" aria-label="Direct link to 先看结果：Infinity CI 是什么" title="Direct link to 先看结果：Infinity CI 是什么" translate="no">​</a></h2>
<p>先介绍下这个软件本身。它叫 <strong>Infinity CI</strong>，一个用 C# / .NET 10 构建的类 Jenkins / GitHub Actions 持续集成服务器。最终数据：<strong>9 天、90 个提交、净增 22,212 行代码</strong>，全部由 Z Code 编写，我一个字一个字敲的代码行数为零。</p>
<p>核心能力清单：</p>
<ul>
<li class=""><strong>并行 Run / Job / Step</strong>：GitHub Actions 式的运行模型，<code>needs</code> 声明依赖形成 DAG，依赖失败下游自动跳过，Run 页面渲染可点击的依赖图（Jenkins Blue Ocean 风格）</li>
<li class=""><strong>分布式 Agent</strong>：Agent 主动外连 Master（gRPC），拉取式领任务、心跳租约、断线自动重连、孤儿任务自动重排队——打包机不需要开任何入站端口，NAT / 防火墙友好</li>
<li class=""><strong>全页面实时</strong>：每个页面一条独立 SignalR 连接，数据变化实时上屏，整个项目没有一个手动刷新按钮；心跳看门狗指数回退重连</li>
<li class=""><strong>xterm.js 彩色日志</strong>：逐行时间戳、JSONL 断点续传、按 Step 下载、ANSI 全彩渲染</li>
<li class=""><strong>触发方式</strong>：Web 手动 / Git push webhook（HMAC 验签 + 分支通配过滤）/ PR 触发 / cron 定时</li>
<li class=""><strong>配置即 Git 仓库</strong>：每个任务的 YAML 配置本身是一个 Git 仓库，Web 表单保存即自动 commit，天然获得历史、回滚、克隆</li>
<li class=""><strong>企业级补全</strong>：RBAC 三级角色 + 项目可见性 + 个人 API Token + LDAP 域登录</li>
<li class=""><strong>通知</strong>：企业微信 / 钉钉 / Slack / 通用 Webhook / 邮件，按事件过滤</li>
<li class=""><strong>部署</strong>：SQLite 单文件存储，可发布为 win-x64 自包含单文件 EXE，也有多阶段 Dockerfile</li>
</ul>
<p>技术栈一览：</p>
<table><thead><tr><th>层</th><th>技术</th></tr></thead><tbody><tr><td>服务端</td><td>C# / .NET 10、EF Core + SQLite、SignalR、gRPC（Agent 通道）、Serilog、LDAP</td></tr><tr><td>前端</td><td>React 19、TypeScript、Rsbuild（Rspack）、Tailwind CSS v4、xterm.js 6、i18next 中英双语</td></tr><tr><td>Agent</td><td>.NET Worker + gRPC 双向流</td></tr><tr><td>协议</td><td>Protobuf 定义 Agent 协议、YAML 定义工作流</td></tr></tbody></table>
<p>架构一句话：<strong>Server（调度 + Web）→ Agent（装在打包机上领任务干活）→ Web UI（实时看板）</strong>，外部对接 Git 仓库和通知渠道。</p>
<blockquote>
<p>💬 <em>配图位置：Infinity CI 首页 Dashboard（Jenkins 风格任务卡片）</em></p>
</blockquote>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="第一天约-2-小时从一条消息到能用的-ci">第一天：约 2 小时，从一条消息到能用的 CI<a href="https://networm.github.io/infinityci.github.io/en/blog/zcode-9-days-ci#%E7%AC%AC%E4%B8%80%E5%A4%A9%E7%BA%A6-2-%E5%B0%8F%E6%97%B6%E4%BB%8E%E4%B8%80%E6%9D%A1%E6%B6%88%E6%81%AF%E5%88%B0%E8%83%BD%E7%94%A8%E7%9A%84-ci" class="hash-link" aria-label="Direct link to 第一天：约 2 小时，从一条消息到能用的 CI" title="Direct link to 第一天：约 2 小时，从一条消息到能用的 CI" translate="no">​</a></h2>
<p>9 月 9 日 23:40，我发出第一条消息（就是上面那条问技术选型的）。Z Code 给出了完整的技术选型分析，我补充了一条关键需求：</p>
<blockquote>
<p>打开的每一个网页都作为一个单独的客户端实时与服务器连接，能拿到最新的状态。
用户可能在一台机器上打开多个网页，分别查看不同任务，互相之间不能有影响。</p>
</blockquote>
<p>然后就是见证效率的时刻了。看当天的 git log：</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">00:22  chore: scaffold .NET 10 solution (Core/Server/Agent/Tests)</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">00:26  feat(core): job 定义模型、YAML 解析校验、跨平台 shell 解析器</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">01:01  feat(server): 构建队列、进程执行引擎、基于偏移量的日志存储</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">01:15  feat(server): SignalR hub（按连接分组订阅 + offset 续传）</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">01:19  feat(server): jobs/builds REST API + 分页日志</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">01:26  feat(web): Rsbuild + React 19 + Tailwind v4 前端脚手架</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">01:37  feat(web): 实时仪表盘 + xterm.js 日志流详情页</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">01:52  feat(agent): gRPC Agent 协议（拉取式派发 + 租约恢复）</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">02:01  feat(web): Agent 管理页；gRPC 独立 HTTP/2 端口 5001</span><br></div></code></pre></div></div>
<p>从 23:40 的第一条消息到凌晨 2:01 最后一个提交，<strong>大约 2 小时 20 分钟</strong>，一个 CI 服务器需要的全部骨架——.NET 10 后端（任务队列、进程执行引擎、SignalR 实时推送、REST API）、React 19 前端（仪表盘 + 日志流）、gRPC 分布式 Agent——三线全部打通，而且每一块都有提交、有测试项目。这中间我几乎没有再打字，Z Code 在持续自主施工。</p>
<p>到这里还没完。第二天早上 7 点 57 分，我睡醒后发了一条大需求：</p>
<blockquote>
<p>UI 风格改为 <a href="https://github.com/actions/runner-images/actions/runs/34350318609/job/102461706314" target="_blank" rel="noopener noreferrer" class="">https://github.com/actions/runner-images/actions/runs/34350318609/job/102461706314</a>
需要支持并行任务
每一步改为单独的 Console，像上面链接中的一样
每一条日志需要增加时间戳
需要增加任务配置界面
需要增加用户权限系统，支持超级管理员、普通管理员、普通用户，支持设置每个用户可见的项目
……</p>
</blockquote>
<p>半小时后，git 历史上出现了两记 <code>breaking change</code> 重构：一个 52 文件 +2972/-1959 行，把执行模型改成了 GitHub Actions 式的并行 Run；一个 20 文件 +2190/-522 行，前端整体换成 GHA 风格 UI + 登录页 + 角色权限。<strong>第一天结束时，它已经不是一个玩具，而是一个有权限体系的、对标 GitHub Actions 运行模型的 CI 了。</strong></p>
<p>这里有个我事后复盘很感慨的点：如果是人写代码，"推翻执行模型重写"这种事，第一天的 psyche 扛不住。但 Z Code 重构 3000 行跟写 300 行一样平静，我只需要决策"要不要这么改"。</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="第二天功能大爆炸23-个提交">第二天：功能大爆炸（23 个提交）<a href="https://networm.github.io/infinityci.github.io/en/blog/zcode-9-days-ci#%E7%AC%AC%E4%BA%8C%E5%A4%A9%E5%8A%9F%E8%83%BD%E5%A4%A7%E7%88%86%E7%82%B823-%E4%B8%AA%E6%8F%90%E4%BA%A4" class="hash-link" aria-label="Direct link to 第二天：功能大爆炸（23 个提交）" title="Direct link to 第二天：功能大爆炸（23 个提交）" translate="no">​</a></h2>
<p>9 月 11 日是整个项目提交最多的一天，23 个提交。凌晨 3 点 21 分我发了一条消息，一口气提了四个需求：</p>
<blockquote>
<p>增加运行服务器与 Agent 脚本
job 可以指定依赖的 job，从而形成一个图
需要将 job 由依赖关系组成的图显示出来，并且可以点击后切换下面的 job 详情及 steps 日志，使用 GitHub 风格
每个任务的配置放到单独的 Git 仓库中，并且可以查看历史，也支持克隆到本地</p>
</blockquote>
<p>这一天落地的东西，随便拎一个出来都是正经 CI 的核心功能：</p>
<ul>
<li class=""><strong>DAG 依赖调度</strong>：<code>needs: [a, b]</code> 声明依赖，失败级联跳过，页面上渲染成可点击的依赖图；</li>
<li class=""><strong>配置即 Git 仓库</strong>：任务的 YAML 存成独立 Git 仓库，Web 表单保存即 commit，能看历史、能回滚、能 <code>git clone</code>——这个设计我很得意，比 Jenkins 把配置埋进数据库优雅多了；</li>
<li class=""><strong>Git SCM 集成</strong>：工作流声明 <code>scm:</code> 块，构建前自动检出源码；</li>
<li class=""><strong>Jenkins 风格首页 Dashboard</strong>：任务卡片、状态图标、收藏置顶；</li>
<li class=""><strong>Webhook 触发 + 企业微信机器人通知</strong>。</li>
</ul>
<p>最有意思的是 DAG 图的视觉打磨。我拿着 Jenkins Blue Ocean 的截图逐条提意见：</p>
<blockquote>
<p>学习这张图片中的任务图风格，开始和结束以及大部分结点在一条线上，并行的任务按照顺序排在并行结点下方。结点之间使用圆角直线连接。</p>
</blockquote>
<blockquote>
<p>ok 的进入线与退出线的后半段变成了直线，需要改成圆角肘形</p>
</blockquote>
<p>一个上午连修五轮，最终的效果是：主线结点在一条水平线上、并行分支垂下来再汇聚、圆角肘形连线、成功打勾失败打叉。这就是 AI 编程的正确用法之一——<strong>审美和"像不像那个产品"的判断我来给，SVG 连线怎么画让它去迭代</strong>。</p>
<blockquote>
<p>💬 <em>配图位置：Run 页面的 Blue Ocean 风格 DAG 依赖图</em></p>
</blockquote>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="第-3-到-8-天每天一个主题">第 3 到 8 天：每天一个主题<a href="https://networm.github.io/infinityci.github.io/en/blog/zcode-9-days-ci#%E7%AC%AC-3-%E5%88%B0-8-%E5%A4%A9%E6%AF%8F%E5%A4%A9%E4%B8%80%E4%B8%AA%E4%B8%BB%E9%A2%98" class="hash-link" aria-label="Direct link to 第 3 到 8 天：每天一个主题" title="Direct link to 第 3 到 8 天：每天一个主题" translate="no">​</a></h2>
<p>之后的一周，节奏变成"每天一个主题"，每天睡前或起床后看一眼哪里不顺眼，丢给 Z Code：</p>
<p><strong>9 月 13 日（企业化）</strong>：从失败步骤重试、复制任务、i18next 中英双语、<strong>LDAP 域账号登录</strong>。到这一步，拿去公司内网部署已经没有障碍了。</p>
<p><strong>9 月 14 日（管理体验）</strong>：暗色主题、Projects 页面、真实的队列页（显示 Agent 派发等待状态）、Agent 配置界面（并发 / 标签 / 环境变量）。</p>
<p><strong>9 月 15 日（数据模型大迁移）</strong>：我发了一条带 checkbox 的长消息：</p>
<blockquote>
<ul class="contains-task-list containsTaskList_mC6p">
<li class="task-list-item"><input type="checkbox" disabled=""> <!-- -->运行不是全局的，应该跟着任务走，且 ID 只在任务内自增，不是全局自增<!-- -->
<ul class="contains-task-list containsTaskList_mC6p">
<li class="task-list-item"><input type="checkbox" disabled=""> <!-- -->现在地址是 <a href="http://127.0.0.1:5000/runs/5%EF%BC%8C%E5%BA%94%E8%AF%A5%E4%B8%BA" target="_blank" rel="noopener noreferrer" class="">http://127.0.0.1:5000/runs/5，应该为</a> <a href="http://127.0.0.1:5000/runs/dag-demo/5" target="_blank" rel="noopener noreferrer" class="">http://127.0.0.1:5000/runs/dag-demo/5</a></li>
</ul>
</li>
<li class="task-list-item"><input type="checkbox" disabled=""> <!-- -->配置需要放到任务单独子目录中进行版本控制</li>
<li class="task-list-item"><input type="checkbox" disabled=""> <!-- -->日志也需要放到任务单独子目录按运行 ID 进行存放</li>
<li class="task-list-item"><input type="checkbox" disabled=""> <!-- -->简单说就是不再先类别后任务，而是先任务后类别</li>
</ul>
</blockquote>
<p>Z Code 完成了"任务优先"目录布局的整体迁移：每个任务独立目录、独立仓库、任务内自增编号、<code>/runs/{任务}/{编号}</code> 式 URL。这种牵一发动全身的改造，AI 做起来反而干净。</p>
<p><strong>9 月 16 日（对标审计日）</strong>：这天的起点是一条灵魂拷问：</p>
<blockquote>
<p>现在全盘检查一下，Jenkins 该有的功能都实现了吗？还差哪些关键与必需的？</p>
</blockquote>
<p>Z Code 做了全盘审计，列缺失清单，我排了个优先级，然后这一天产出了两个里程碑提交：一个是<strong>全页面实时化改造</strong>——架构改为每个页面独立 SignalR 连接 + 心跳看门狗（15s ping、指数退避、连挂 5 次自动刷新页面）+ 删掉所有刷新按钮，26 文件 +2567/-1725；另一个是<strong>六项功能补齐</strong>——cron 定时触发、用户 API Token、SCM webhook HMAC-SHA256 验签、Job/Step 超时 kill 进程树、<code>if: always()</code> + Step 重试、越权过滤修复，最后一句提交信息写着"<strong>104 个测试全绿</strong>"。</p>
<p><strong>9 月 17 日（生态集成 + 决战 Unity，下面单独讲）</strong>。</p>
<p><strong>9 月 18 日（收官打磨）</strong>：凌晨围绕着 xterm.js 终端的"最后一公里"连修七个提交——隐藏自绘滚动条、拦截滚轮、焦点管理、Ctrl+C 复制（还专门兼容了某个 Chrome 复制扩展劫持选区的行为）。晚上以 LDAP 管理界面（配置存库热生效 + 连接测试诊断 + 组映射）收尾，也就是今天的 HEAD。</p>
<p>每日提交分布：13 / 23 / 0（休息）/ 5 / 6 / 7 / 10 / 17 / 9。是的，9 月 12 日一个提交都没有——写 Side project 别硬撑，休息一天效率更高。</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="决战-917unity-6-打包-windows-跑通">决战 9·17：Unity 6 打包 Windows 跑通<a href="https://networm.github.io/infinityci.github.io/en/blog/zcode-9-days-ci#%E5%86%B3%E6%88%98-917unity-6-%E6%89%93%E5%8C%85-windows-%E8%B7%91%E9%80%9A" class="hash-link" aria-label="Direct link to 决战 9·17：Unity 6 打包 Windows 跑通" title="Direct link to 决战 9·17：Unity 6 打包 Windows 跑通" translate="no">​</a></h2>
<p>前面八天，Infinity CI 已经是个"完整的软件"了，但对我而言它还缺终极验证：<strong>它能不能真的把 Unity 6 的包打出来？</strong></p>
<p>我特意设计了解耦的集成方式：<strong>CI 里没有一行 Unity 专属代码</strong>。Unity 的构建知识（怎么调命令行、输出到哪）全部留在游戏仓库自己的脚本里，CI 只负责调度。9 月 17 日早上 8 点，在 Unity 工程里我用了三条消息让 Z Code 写完整个打包链路：</p>
<blockquote>
<p>Client/Assets/Editor/Builder.cs 直接在其中增加一个 MenuItem 打包 Windows 的方法</p>
</blockquote>
<blockquote>
<p>CI\platform\windows\build.py 调用这个 Builder.BuildWindows 方法</p>
</blockquote>
<blockquote>
<p>CI\platform\windows\prepare.py 打印所有环境变量</p>
</blockquote>
<p>11 分钟，三个文件写完。Unity 侧是一个标准的命令行构建入口：</p>
<div class="language-csharp codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-csharp codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">public</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">static</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">class</span><span class="token plain"> </span><span class="token class-name">Builder</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token punctuation" style="color:rgb(248, 248, 242)">[</span><span class="token attribute class-name">MenuItem</span><span class="token attribute attribute-arguments punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token attribute attribute-arguments string" style="color:rgb(255, 121, 198)">"Tools/Build Windows"</span><span class="token attribute attribute-arguments punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">]</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">public</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">static</span><span class="token plain"> </span><span class="token return-type class-name keyword" style="color:rgb(189, 147, 249);font-style:italic">void</span><span class="token plain"> </span><span class="token function" style="color:rgb(80, 250, 123)">BuildWindows</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        </span><span class="token class-name keyword" style="color:rgb(189, 147, 249);font-style:italic">var</span><span class="token plain"> scenes </span><span class="token operator">=</span><span class="token plain"> EditorBuildSettings</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">scenes</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            </span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">Where</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">s </span><span class="token operator">=&gt;</span><span class="token plain"> s</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">enabled</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">Select</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">s </span><span class="token operator">=&gt;</span><span class="token plain"> s</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">path</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">ToArray</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        </span><span class="token class-name">BuildPlayerOptions</span><span class="token plain"> options </span><span class="token operator">=</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">new</span><span class="token plain"> </span><span class="token constructor-invocation class-name">BuildPlayerOptions</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        </span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            scenes </span><span class="token operator">=</span><span class="token plain"> scenes</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            locationPathName </span><span class="token operator">=</span><span class="token plain"> Path</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">Combine</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token string" style="color:rgb(255, 121, 198)">"Builds/Windows"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"UnityCIGame.exe"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            target </span><span class="token operator">=</span><span class="token plain"> BuildTarget</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">StandaloneWindows64</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        </span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token punctuation" style="color:rgb(248, 248, 242)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        BuildPipeline</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">BuildPlayer</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">options</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><br></div></code></pre></div></div>
<p>Python 侧的 <code>build.py</code> 定位 Unity 6（6000.0.23f1）的安装路径，拉起命令行：</p>
<div class="language-python codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-python codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">args </span><span class="token operator">=</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">[</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token builtin" style="color:rgb(189, 147, 249)">str</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">unity_exe</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token string" style="color:rgb(255, 121, 198)">"-batchmode"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"-quit"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"-nographics"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token string" style="color:rgb(255, 121, 198)">"-projectPath"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token builtin" style="color:rgb(189, 147, 249)">str</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">CLIENT_DIR</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token string" style="color:rgb(255, 121, 198)">"-executeMethod"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"Builder.BuildWindows"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token string" style="color:rgb(255, 121, 198)">"-logFile"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"-"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain">          </span><span class="token comment" style="color:rgb(98, 114, 164)"># 日志走 stdout</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token punctuation" style="color:rgb(248, 248, 242)">]</span><br></div></code></pre></div></div>
<p>然后 CI 侧，任务配置就只需要这么几行 YAML：</p>
<div class="language-yaml codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-yaml codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token key atrule">name</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> UnityCIGame</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token key atrule">project</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> Default</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token key atrule">jobs</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  </span><span class="token key atrule">build</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token key atrule">runs_on</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> agent        </span><span class="token comment" style="color:rgb(98, 114, 164)"># 分发到装了 Unity 的 Windows 打包机</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token key atrule">steps</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">      </span><span class="token punctuation" style="color:rgb(248, 248, 242)">-</span><span class="token plain"> </span><span class="token key atrule">name</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> prepare</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        </span><span class="token key atrule">command</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> python CI/platform/windows/prepare.py</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">      </span><span class="token punctuation" style="color:rgb(248, 248, 242)">-</span><span class="token plain"> </span><span class="token key atrule">name</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> build</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        </span><span class="token key atrule">command</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> python CI/platform/windows/build.py</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">      </span><span class="token punctuation" style="color:rgb(248, 248, 242)">-</span><span class="token plain"> </span><span class="token key atrule">name</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> deploy</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        </span><span class="token key atrule">command</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> python CI/platform/windows/deploy.py</span><br></div></code></pre></div></div>
<p>当然，真实世界没有这么顺利。第一次把 Unity 任务真正跑起来就撞了墙：Agent 默认把源码检出到自己的 workspace，但 Unity 工程根本不在 Git 源里，脚本路径全错。我把需求丢给 Z Code：任务要能直接绑定机器上的本地目录。功能上线后第一次实测仍有路径问题，我直接把报错日志贴回去：</p>
<blockquote>
<p>python: can't open file '...\agent-data\workspaces\21-build\CI\platform\windows\prepare.py': No such file or directory
路径不对，用的应该是 C:\Users\NETWORM\Work\Projects\20260917-UnityCIGame\CI\platform\windows\prepare.py</p>
</blockquote>
<p>半小时内两轮修复，“任务本地目录”功能成型：任务可以直接指向本地路径，Agent 就地执行。这个功能反过来让所有 CI 产品都头疼的“Unity 大工程如何进 CI”变成了伪命题——<strong>工程不动，CI 来就你</strong>。</p>
<p>早上 9 点 01 分，正式产物 <code>Builds/Windows/UnityCIGame.exe</code>（约 95 MB）落地——<strong>从 Builder.cs 的第一行代码，到 CI 产出 Windows 包，全程约 1 小时</strong>。紧接着我又提了一条：</p>
<blockquote>
<p>build.py 增加实时日志输出，每一行日志都要 flush</p>
</blockquote>
<p>于是 Unity 的海量编译日志通过 <code>-logFile -</code> 流式输出，经 Agent 的 gRPC 逐行回传，在 Web 端的 xterm.js 终端里实时滚动、带颜色、带时间戳；Run 结束的瞬间，企业微信里已经收到带状态颜色的构建卡片。9 点 06 分收尾提交，此后这条流水线每天都在跑——第二天的 shader 编译日志还证明它凌晨还在干活。</p>
<blockquote>
<p>💬 <em>配图位置：Unity 打包任务的 Run 详情页——DAG 图 + xterm.js 实时日志</em></p>
</blockquote>
<p>回头看这个集成哲学：Infinity CI 不认识 Unity，未来要接 Godot、Unreal，也只是游戏仓库里换几行脚本的事。CI 引擎专注做好调度、日志、分发、通知这些它该做的事。</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="用-z-code-写整个软件的九条体会">用 Z Code 写整个软件的九条体会<a href="https://networm.github.io/infinityci.github.io/en/blog/zcode-9-days-ci#%E7%94%A8-z-code-%E5%86%99%E6%95%B4%E4%B8%AA%E8%BD%AF%E4%BB%B6%E7%9A%84%E4%B9%9D%E6%9D%A1%E4%BD%93%E4%BC%9A" class="hash-link" aria-label="Direct link to 用 Z Code 写整个软件的九条体会" title="Direct link to 用 Z Code 写整个软件的九条体会" translate="no">​</a></h2>
<p>9 天下来，对"怎么用 AI 写一个完整软件"这件事，我有一些具体的体感：</p>
<p><strong>1. 第一条消息决定坡度。</strong> 我没有一上来就说"给我写个 CI"，而是先问技术选型，再补充架构级的要求（每个网页独立实时连接）。AI 给出的选型（.NET 10 + React 19 + gRPC + SignalR）我基本照单全收，事后证明全是对的。</p>
<p><strong>2. 小步提交是最好的护栏。</strong> 90 个提交几乎全是 Conventional Commits 规范的原子提交，这是 Z Code 自带的习惯。每次出问题，我直接把 URL 和日志贴回去（"<a href="http://127.0.0.1:5000/runs/3" target="_blank" rel="noopener noreferrer" class="">http://127.0.0.1:5000/runs/3</a> 报错"），它自己定位自己修。git 历史就是调试时的地图。</p>
<p><strong>3. 用真实产品的截图和链接驱动 UI。</strong> "UI 风格改为这个链接"、"学习这张图片中的任务图风格"——比任何文字描述都精准。第二轮开始，AI 交付的还原度高得惊人。</p>
<p><strong>4. 放心让 AI 推倒重来。</strong> 第一天上午两次 breaking 重构（合计改 72 个文件），第五天整体数据布局迁移。在 AI 编程里，"重写"的成本曲线被拉平了，架构决策不该再为沉没成本妥协。</p>
<p><strong>5. 灵魂拷问要人来做。</strong> "Jenkins 该有的功能都实现了吗？"——这一条消息引发的全盘审计比我自己想需求高效十倍。<strong>人负责提出好问题、排优先级，AI 负责穷举和实现。</strong></p>
<p><strong>6. 密集反馈循环是质量的关键。</strong> 9 月 18 日凌晨修 xterm 体验，我的消息是这样的节奏："还是存在滚动条" → "依然有" → "已经强制刷新了，你可以自己强制刷新测试一下"。经常几分钟就是一轮反馈。AI 不怕你挑剔，怕的是你不验证。</p>
<p><strong>7. 一款工具全栈通吃。</strong> 这个项目横跨 C#、TypeScript/React、Python、Protobuf、YAML、Dockerfile、LDAP、SQL——全部出自 Z Code 一款工具。不需要"前端 AI、后端 AI"各来一个。</p>
<p><strong>8. 测试从第一天就跟着走。</strong> Core 和 Server 都有测试项目，到 9 月 16 日已经有 104 个测试。每次大重构后"测试全绿"是敢单击提交的底气。</p>
<p><strong>9. 人类的时间花在刀刃上。</strong> 9 天里我真正投入的，是每天几十条消息的产品决策和验收反馈。Z Code 把"实现"这件事的成本降到了"说清楚要什么"。</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="结语">结语<a href="https://networm.github.io/infinityci.github.io/en/blog/zcode-9-days-ci#%E7%BB%93%E8%AF%AD" class="hash-link" aria-label="Direct link to 结语" title="Direct link to 结语" translate="no">​</a></h2>
<p>最终交付物：</p>
<ul>
<li class=""><strong>Infinity CI</strong>：9 天、90 个提交、22,212 行净增代码，类 Jenkins / GitHub Actions 的自托管 CI，单 EXE 或 Docker 部署，开源在 <a href="https://github.com/networm/InfinityCI" target="_blank" rel="noopener noreferrer" class="">github.com/networm/InfinityCI</a></li>
<li class=""><strong>一条真实在用的流水线</strong>：Git 事件 / 手动 / cron 触发 → Server 调度 → Windows 打包机上的 Agent 拉起 Unity 6 命令行打包 → 实时彩色日志回传 → 企业微信收通知</li>
</ul>
<p>回到开头的问题：个人开发者到底值不值得自己写一个 CI？放在以前，我的答案是不值得。但现在，一个合理的技术选型 + 一款足够强的 AI 编程工具 + 每天下班后零散的反馈时间，9 天就能得到一个完全长在自己需求上的软件——不用伺候插件，不用迁就云服务，打包机插上网线就能加入集群。</p>
<p>这是我用 Z Code 写的第一个完整软件，大概率不是最后一个。</p>]]></content:encoded>
            <category>AI 编程</category>
            <category>CI/CD</category>
            <category>Unity</category>
        </item>
    </channel>
</rss>