从一句话需求到 159 条测试用例:我用 HiFox 搭了一条 AI 自动化开发流水线
学习目标:看懂 HiFox 的心智模型;知道怎么把一件事拆成「需求 → 开发 → 验收 → 测试」交给不同 Agent;看完能照着搭出自己的第一条自动化流水线(不仅仅局限于自动化测试)。
资料来源:
目录
一、先说结论:这篇讲的不是工具,是一条能跑通的流水线
2026 年 10 月中旬,我在 HiFox 上开了一个默认空间,随手扔进去一个需求:
做一个年会抽奖程序,单文件 HTML,双击就能用,粘名单、点开始、抽出中奖者、大屏好看。
然后我没有写一行代码。我建了任务、派给了「自动化测试」这个 Agent,它交回来一个 35.55 KB 的单文件 HTML 和一份 159 条用例的测试报告。
两周后,我在「心动工程」空间里开了第二个项目:给女朋友做一个「每天抽一句心动」的盲盒网页。这次走了完整链路——需求 Agent 写 SPEC、开发 Agent 写代码、验收 Agent 按标准逐条核对、自动化测试 Agent 出测试包,9 个任务全部由 HiFox 上的 Agent 完成,我只负责定标准和判对错。
这两件事让我意识到:很多人用 AI 编程工具的方式是「提问 → 复制代码 → 再提问」,本质上是把 Agent 当成一个更贵的搜索引擎。而 HiFox 的价值在于它提供了一套任务流转的结构,让多个 Agent 能像一支有分工的小团队一样,围绕同一张任务单协作,并且每一步都留痕。
这篇 blog 我想讲三件事:
- HiFox 到底怎么用(心智模型 + 快速上手四步);
- 我怎么把两个小项目真的跑完了(附源码、附截图、附实测数据);
- 哪些坑是文档里没写、但你一定会踩的。

二、HiFox 的心智模型:6 个词就够了
HiFox 的概念不少,但真正天天用到的只有这几个:
| 概念 | 一句话解释 | 我的用法 |
|---|---|---|
| Space(空间) | 一个项目的边界,里面有成员、任务类型、状态、标签 | 一个空间 = 一个独立项目。心动盲盒单独一个空间,会年抽奖放默认空间 |
| Task(任务) | 一个可交付的工作项,类似 Jira issue | 任务正文写清「背景 / 目标 / 交付物 / 验收标准 / 不做什么」 |
| Agent | 可自定义的 AI 队友,有自己的职责说明和技能 | 我配了需求 / 开发 / 验收 / 自动化测试四个,分工见第四节 |
| Computer + Runtime | Agent 真正干活的机器和执行器(Claude Code、Codex 等) | Agent 绑一台电脑,它就在你本机文件上改代码、跑测试 |
| Automation(自动化) | 「当任务变成某状态时,自动让某个 Agent 接手」 | 心动盲盒每天 21:00 自动推送一条 |
| CLI(hifox) | 在终端里操作 HiFox 的命令行 | 我让 Agent 用 hifox 命令读写任务、评论、附件 |
关键认知:Agent 不是聊天机器人,是「有工作目录、能改文件、能跑命令、有产出」的同事。
它的产出会被回写到任务上(评论、附件、状态变更),所以你和它的每一次交互都留在任务时间线上。三个月后再回看,能知道当时为什么这么设计。
三、快速上手四步

四步分别是:建空间 → 建任务 → 委派 Agent → 验收闭环。
3.1 第一步:建空间
空间是项目的边界,不是文件夹。一进来先定三件事:
- 谁在里面:有哪些 Agent、哪些人;
- 任务长什么样:任务类型(需求 / 开发 / 测试…)和状态流转;
- 怎么算完成:状态机本身就是验收流程。
3.2 第二步:建任务,任务正文就是「给 Agent 的 Prompt」
这是我踩过最大的坑:任务正文写得像一句话需求,Agent 就会给你一句话交付。
我在「心动盲盒」的第一个任务里,正文是这样的结构(后面对应展开):
## 背景
练手 HiFox,同时给女朋友做个浪漫小工具。
## 目标
1. 一个微信可点开的静态页
2. 点"抽今天的心动"→ 权重随机出结果
3. 结果分三类:情话(90%)/ 小奖励(约9%)/ 彩蛋(第100抽解锁表白信,0.5%)
4. 每天21:00 自动推一条到企微群机器人 Webhook
5. 防刷:同人每天最多3次
## 验收标准
- 本地能打开页面并抽奖
- 权重逻辑正确(不是均匀随机)
- curl 能推 markdown 消息到企微 Webhook
- 自动化可定时触发
- 表白文案先人工看,再推送
## 交付物
- 前端 index.html
- 抽奖函数 draw.js 或 draw.py
- 推送脚本 push_wecom.sh
- README:怎么配 Webhook、怎么本地跑
注意 「不做什么」 这一节是需求 Agent 后来补上的,效果出奇地好:它明确写了「不改权重算法、不改表白信阈值、不做浏览器端实时调图像 API」,把开发 Agent 的自由度收窄到可验收的范围内。
3.3 第三步:委派 Agent
任务建好后,直接指定谁来执行:
hifox task create --title "实现:心动盲盒 MVP(抽奖页 + 推送脚本)" \
--body-file ./brief_dev.md \
--delegate-agent "开发 Agent" \
--parent <父任务ID> \
--output json
--parent 很重要:子任务挂父任务,整个项目的任务树在界面上就长成了一棵有结构的树,而不是一堆平铺的任务。
3.4 第四步:验收闭环
Agent 交付后,任务不会自动消失,它会带着交付说明回到你面前。你要做的是按事先写好的验收标准逐条核对,而不是「看着还行就算过」。
我在「心动盲盒」上的做法是:开发 Agent 交付后,由需求 Agent 复核交付说明完整性,再单独派一个验收 Agent 按 SPEC 第 6 节的 22 条标准逐条实测。验收 Agent 不改代码,开发 Agent 也不验收——角色隔离,这是质量的关键。
四、全流程自动化是怎么跑起来的
HiFox 官方文档《全流程自动化测试》给了一套参考实现:白小浅(白盒)、测小统(版本)、测小案(用例)、凤小九 / 端小九(黑盒执行),人做最终验收。核心是两条自动化:
| 触发条件 | 执行 Agent | 执行指令 |
|---|---|---|
| 任务状态变为「待测试」 | 白小浅 | 执行需求分析、白盒测试、风险判断和提测预检 |
| 任务状态变为「测试中」 | 测小案 | 根据任务描述和评论设计测试用例,输出到原任务评论 |
我照这个思路,把「心动盲盒」和「年会抽奖程序」跑成了自己的版本:

我的 Agent 分工:
| Agent | 职责 | 产出 |
|---|---|---|
| 我(人类) | 提需求、定验收标准、最终确认 | 决策与签字 |
| 需求 Agent | 写 SPEC、拆子任务、派工、复核交付说明 | SPEC.md / SPEC_V2.md、子任务拆分 |
| 开发 Agent | 写代码、自测、提交 Git、出交付说明 | index.html / draw.js / push_wecom.sh… |
| 验收 Agent | 按标准逐条核对、独立加压、出验收结论 | 验收报告(逐条结论) |
| 自动化测试 Agent | 批量用例、覆盖率、性能、证据留存 | 测试包(策略 / 用例表 / 报告 / 代码) |
4.1 官方文档强调、但我体感最深的一条:测试产物必须落盘
官方文档里有一句很实在的话:
测试产物及时上传,避免只保存在执行目录。Agent 的临时工作目录可能被回收,前面的调研中就出现过产物丢失后重新执行测试的情况。
我原以为这是小事,直到「心动盲盒」的开发 Agent 把 3.6 MB 的 PNG 留在 tests/screenshots/ 里没清理、验收 Agent 又在别处重跑了一遍——那一次返工的成本,比一开始就把产物上传到任务附件高得多。
现在我的规矩:Agent 跑完,产物必须以附件形式落到任务上。
4.2 需求 → 开发 → 验收这条链上,「状态」是唯一的契约
我给「心动工程」空间定义的状态流转是这样的:
待开始 → 进行中 → 待验收 → 已完成
↑ │
└── 返工 ──┘
每次状态变更都会触发对应的 Agent 或产生一次人工确认。不要用评论区当状态机——评论没有强制力,状态有。开发 Agent 交付后任务必须切到「待验收」,才有人(验收 Agent 或我)必须做点什么。
五、项目一:心动盲盒(零依赖静态页 + CLI 引擎 + 企微推送)
5.1 它是什么
一个「每天抽一句心动」的静态网页。点一下,按权重随机出一条情话 / 小奖励 / 彩蛋,攒到第 100 抽解锁一封表白信。配套一个 Node CLI 引擎和企微推送脚本,每天 21:00 自动往微信群推一条。

文件职责:
| 文件 | 体积 | 职责 |
|---|---|---|
index.html | 13.87 KB | 抽奖页。零 CDN、零框架、零构建,移动端优先 |
draw.js | 27.66 KB | 抽奖引擎 + CLI。浏览器和 Node 共用同一套逻辑 |
content.js | 6.99 KB | 文案池:情话 35 / 小奖励 15 / 彩蛋 14 / 表白信 1 篇 |
push_wecom.sh | 11.33 KB | 企微群机器人推送,含幂等与密钥打码 |
README.md | 19.30 KB | 使用说明 |
SPEC.md / SPEC_V2.md | 25.62 KB / 23.67 KB | 需求规格 V1.1 / V2(权威依据) |
三件套合计 48.52 KB(index.html + draw.js + content.js),全部是纯文本,任何一台电脑 clone 下来双击就能跑。
5.2 权重随机:不要硬编码概率
原始权重是 90 / 9 / 0.5,总和是 99.5 而不是 100。这是需求里就写死的配置,开发 Agent 选择按比例归一化,而不是「凑成 100」:
// draw.js:51-56
/** 原始权重(总和 99.5,按比例归一化到 100%) */
var WEIGHTS = [
{ type: 'romance', weight: 90 },
{ type: 'reward', weight: 9 },
{ type: 'easter', weight: 0.5 }
];
抽取不是 if (r < 0.9) ... else if (r < 0.99) ... 这种硬编码分支,而是「算出 roll,落在累计区间就命中」:
// draw.js:147-160
/**
* 按权重抽一个档位:roll = rand() * total,落在累计区间即命中。
* 不使用 if/else 硬编码概率分支,权重改一处即可全局生效。
*/
function pickType(rand) {
var r = rand || Math.random;
var total = totalWeight();
var roll = r() * total;
var acc = 0;
for (var i = 0; i < WEIGHTS.length; i++) {
acc += WEIGHTS[i].weight;
if (roll < acc) { return WEIGHTS[i].type; }
}
return WEIGHTS[WEIGHTS.length - 1].type;
}
这个写法的直接好处:改一个数字就能调整概率,不需要动任何分支逻辑。
实际生效概率由引擎自检输出(node draw.js --check-weights):
档位 原始权重 实际概率 累计区间
情话 90 90.4523% (90.45%)
小奖励 9 9.0452% (99.50%)
彩蛋 0.5 0.5025% (100.00%)
5.3 第 100 抽表白信:判定条件的坑
需求写的是「第 100 抽解锁表白信」。看起来简单,但**「第 100 抽」是抽取完成后的次数,还是抽取前的次数**,这里出过边界问题。
最终实现用 (countBefore + 1) >= CONFESSION_AT,并在注释里写清了原因:
// draw.js:366-369
if (allowConfession && (countBefore + 1) >= confessionAt()) {
/* 第 N 抽(countBefore + 1)恰好达到阈值 → 强制表白信 */
isConfession = true;
type = 'romance';
验收 Agent 实测了两个边界:countBefore=4 时第 5 抽出表白信,countBefore=3 时第 4 抽不出——判定式正确。预置 count=99 后抽取,必定返回表白信。
阈值还能用环境变量覆盖,方便提前体验:
CONFESSION_AT=5 node draw.js --draw --json # 5 抽就解锁

5.4 概率必须能被验证,不能靠「看起来对」
引擎自带两个自检命令,这是我觉得最值钱的设计:
node draw.js --check-weights # 打印归一化后的实际概率
node draw.js --simulate 10000 --seed 42 # 跑 1 万次模拟,看命中率偏差
实测输出:
模拟次数: 10000 随机种子: 42
档位 命中数 实际命中率 理论命中率 偏差
情话 9037 90.37% 90.45% -0.08pp
小奖励 912 9.12% 9.05% +0.07pp
彩蛋 51 0.51% 0.50% +0.01pp
最大偏差 0.08pp,阈值 2.00pp → PASS
--simulate 用的是 mulberry32 伪随机 + 固定种子,所以同一个种子永远得到同一组结果,这让「概率有没有写错」变成一个可回归的断言。
验收 Agent 还加压跑了 20 万次:彩蛋命中 971/200000 = 0.4855%,理论 0.5025%,偏差 -0.017pp;并换 seed 7 复跑确认「不是挑了个幸运种子」。
5.5 状态存储:同一套引擎,两套适配器
页面跑在浏览器,CLI 跑在 Node,两边都需要记「累计抽了多少次」。开发 Agent 的做法是可插拔的存储适配器:
// draw.js:322-337
/** 自动选择当前环境对应的存储适配器 */
function defaultStore() {
/* 浏览器:优先 localStorage;个别浏览器在 file:// 下禁用它,降级到内存态 */
if (typeof root !== 'undefined' && root && root.localStorage) {
try {
root.localStorage.getItem(KEY_COUNT);
return browserStore();
} catch (e) {
return memoryStore();
}
}
/* Node:写 data/state.json */
if (isNode()) { return nodeStore(); }
/* 其它环境:绝不能碰 require('fs'),否则浏览器里会直接抛错 */
return memoryStore();
}
Node 侧的路径处理有个容易踩的点——路径相对脚本自身目录,而不是调用时的 cwd:
// draw.js:253-258
/** Node:data/state.json(路径相对本文件目录) */
function nodeStore(file) {
var fs = require('fs');
var path = require('path');
var base = (typeof __dirname !== 'undefined') ? __dirname : process.cwd();
var target = file || path.join(base, 'data', 'state.json');
验收 Agent 实测「从其他目录调用脚本」→ exit 0,正文与 JSON 转义均正确。这是自动化最容易踩的坑:定时任务的工作目录和你手敲命令时不一样。

图片文件:
心动盲盒-状态存储双适配器.png
注意:两套状态互相独立,是已知限制(README §5.2 已写明)——页面里抽了 10 次,CLI 看到的累计抽数仍是 0。
5.6 企微推送:幂等 + 密钥脱敏
推送脚本 push_wecom.sh 里有两个我觉得必须抄走的做法。
第一,幂等键就是本地日期,同一天重复执行直接跳过:
# push_wecom.sh:144-148
# 同一天重复执行 → 跳过(幂等键 = 本地日期)
if [ "$MODE" != "dry-run" ] && [ "$LAST_PUSH" = "$TODAY" ] && [ -n "$TODAY" ]; then
log "[跳过] 今天($TODAY)已经推送过了,不重复推送。"
exit 0
fi
推送成功后回写幂等键:
# push_wecom.sh:296-299
# ------------------------------------------------------------------ 8. 记录幂等键
node "$DRAW_JS" --mark-pushed --date "$TODAY" >/dev/null 2>&1 || {
warn "[警告] 推送成功,但写入 lastPushedDate 失败,明天可能会重复推送。"
}
第二,密钥绝不进日志。所有输出里的 URL 都把 key 打码:
# push_wecom.sh:60-63
# 把 URL 里的 key 打码,避免密钥进日志/截图
mask_url() {
printf '%s' "$1" | sed -E 's/(key=)[^&[:space:]]*/\1***REDACTED***/g'
}
验收 Agent 做的密钥审计很彻底:工作区全文 + 全量 git 历史搜索,只命中占位符,无真实 key;脚本日志里 URL 实测被打码为 ***REDACTED***。
5.7 定时推送:用 HiFox 的自动化,不用 crontab
hifox automation create --title "心动盲盒 每日推送" --agent "<Agent>" --space "<空间>"
hifox automation trigger create <automation-id> --kind schedule --cron "0 21 * * *"
时区 Asia/Shanghai,nextRunAt 校验过是当天 21:00。
踩坑记录:自动化最初创建时 description 是空的,Agent 每天要靠重新推断「该跑什么」——无人值守时这是配置侧的脆弱点。验收 Agent 指出后,需求 Agent 补上了明确的 runbook:
在
D:\个人学习\心动工程执行D:\Git\bin\bash.exe push_wecom.sh --auto,脚本自身负责幂等与静默跳过,执行完原样回报。
5.8 界面长什么样

页面是移动端优先的(max-width: 420px),暗色模式用 prefers-color-scheme 自动适配,细颗粒质感用内联 SVG 噪点实现——零外部请求,任何网络环境下都是这套样式。
六、项目二:年会抽奖程序
6.1 交付形态:全部内嵌一个文件
这个项目的需求原文很短:
交付形式:单文件 HTML(所有样式和脚本内嵌在一个 .html 文件里),双击即可打开使用,不依赖任何外部网络资源。
结果交付的是一个 35.55 KB 的单文件,HTML + CSS + JS 全内嵌,无任何 CDN 依赖,断网可用。

1920×1080 大屏视口实拍,可以看到撒花特效、揭晓横幅、右侧参与名单与中奖名单面板。
未导入名单时,页面默认展示 20 人示例名单,方便直接预览效果:

顶部「ACME科技有限公司 / 年会盛典」和轮次标签都可以在「设置」面板里改,配置存在
localStorage的hifox.lottery.config.v1键下。
6.2 滚动 → 停止 → 移出奖池
整个抽奖主流程只有两个函数,逻辑非常干净:

滚动中的姓名高速刷新,按钮文案变为「停止」,状态行提示「滚动中……点击「停止」或按空格 / 回车揭晓中奖者」。
开始滚动,用 setInterval 每 45ms 刷一次随机姓名:
// 年会抽奖程序.html:646-651
state.timer = window.setInterval(function () {
if (state.pool.length === 0) return;
var node = $('spin-display');
if (!node) return;
node.textContent = state.pool[Math.floor(Math.random() * state.pool.length)];
}, SPIN_INTERVAL);
停止并揭晓,关键是中奖者立刻移出奖池:
// 年会抽奖程序.html:670-690
var winner = Core.pickWinner(state.pool);
if (!winner) {
setStatus('奖池已空,没有可抽取的参与者', 'warn');
return;
}
// 中奖者立即移出奖池 —— 保证同一人不会重复中奖
state.pool = Core.removeWinner(state.pool, winner);
state.winners.push({ round: state.round, name: winner });
if (display) {
display.textContent = winner;
display.classList.add('revealed');
}
showBanner(true, winner, state.round);
state.round += 1;
renderRound();
renderCounts();
renderWinnerList();
launchConfetti();
setStatus('🎉 恭喜 ' + winner + ' 获得本轮奖项!', 'success');
移出奖池的实现是一个纯过滤,顺带做了一次性能优化——奖池里的元素都已经归一化过了,可以跳过昂贵的正则:
// 年会抽奖程序.html:449-462
/** 将中奖者移出奖池(保证不会重复中奖) */
function removeWinner(pool, name) {
if (!Array.isArray(pool)) return [];
var target = keyOf(name);
var out = [];
for (var i = 0; i < pool.length; i++) {
var item = pool[i];
// 奖池元素按契约均已归一化,可跳过昂贵的正则归一化
var itemKey = typeof item === 'string' && !hasWhitespace(item)
? item.toLowerCase()
: keyOf(item);
if (itemKey === target) continue;
out.push(item);
}
return out;
}
6.3 撒花特效:140 个 DOM 元素,不用 Canvas
// 年会抽奖程序.html:856-875(节选)
function launchConfetti() {
var layer = $('confetti-layer');
if (!layer) return;
layer.innerHTML = '';
var fragment = document.createDocumentFragment();
for (var i = 0; i < CONFETTI_COUNT; i++) {
var piece = document.createElement('i');
piece.className = 'confetti-piece';
piece.setAttribute('data-testid', 'confetti-piece');
piece.style.left = (Math.random() * 100) + '%';
/* ... */
}
layer.appendChild(fragment);
window.setTimeout(function () {
if (layer) layer.innerHTML = '';
}, CONFETTI_DURATION);
}
常量都是写死的、可预期的:
// 年会抽奖程序.html:540-544
var SPIN_INTERVAL = 45;
var CONFETTI_COUNT = 140;
var CONFETTI_DURATION = 3200;
var STORAGE_KEY = 'hifox.lottery.config.v1';
var CONFETTE_COLORS = ['#f5c451', '#ffdc8c', '#fff6da', '#e04a4a', '#ffffff', '#ff9f1c'];
用 documentFragment 批量插入,140 片彩带一次性上屏也不会卡。
6.4 键盘触发:避免「一次按键触发两次」
空格 / 回车都能开始或停止,这是年会现场的刚需。但有个细节——按钮自身被聚焦时,浏览器会原生激活按钮,再走一遍键盘处理就会切换两次(抽完立刻又抽了一次):
// 年会抽奖程序.html:948-957
var target = event.target;
var tag = (target && target.tagName ? target.tagName : '').toUpperCase();
if (tag === 'BUTTON') {
// 抽奖按钮自身被聚焦时交给浏览器原生激活逻辑,避免一次按键触发两次切换
if (target.getAttribute('data-testid') === 'draw-button') return;
event.preventDefault();
} else {
event.preventDefault();
}
toggleSpin();
同时还挡掉了输入框打字时的误触:
// 年会抽奖程序.html:932-941(节选)
function isTypingTarget(target) {
var tag = (target.tagName || '').toUpperCase();
return tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT' || tag === 'OPTION';
}
function onKeyDown(event) {
if (event.repeat) return;
if (isTypingTarget(event.target)) return;
6.5 名单导入与去重
支持「粘贴名单」导入,自动去重并显示总人数;未导入时默认 20 人示例名单,方便预览效果。分隔符支持换行 / 逗号 / 顿号 / 分号。
6.6 测试结果
自动化测试 Agent 交付的测试包:
| 项 | 结果 |
|---|---|
| 用例总数 | 159(单元 54 · 集成 49 · API/契约 29 · 性能 7 · E2E 20) |
| 执行结果 | 通过 159,失败 0 |
| 覆盖率 | 语句 98.93% · 行 98.93% · 函数 100% · 分支 83.42%(门禁 85/85/85/80,全部达标) |
| 浏览器矩阵 | Chromium · Google Chrome · Microsoft Edge · 1366×768 视口,全部通过 |
| 性能 | 真实年会规模 2,000 人 / 200 轮耗时 26.9 ms,单轮均摊 0.135 ms(滚动间隔 45 ms,大屏无掉帧) |
这个测试 Agent 还很诚实地标注了未验证项:WebKit / Firefox 引擎本机未安装内核,未纳入本轮验证;真实主办方名单样本、4K 投影布局需人工确认。
它还报告了本轮发现并修复的 5 个问题,其中两个特别典型:
- 性能基准被误纳入覆盖率插桩运行,导致 3 条基准假性失败 —— 改为独立无插桩配置后实测 500 轮抽取 324 ms,与基线吻合;
- 视觉回归「中奖揭晓」基线是一张纯灰图 —— 撒花层是全屏浮层,遮罩污染了整张截图,断言形同虚设。改为固定随机源制造确定性,并二次复跑验证零像素差异。
第二个问题值得所有做视觉回归的人警惕:你的基线错了,测试就是在稳定地验证一个错误。
总结
把两个项目跑完,我对 HiFox 的认知收敛成几句话:
- HiFox 不是「更聪明的 AI」,是「有结构的 AI 协作」。它的价值在任务流转和留痕,不在模型本身。
- 全流程自动化的骨架是「状态 → Agent → 产物 → 状态」,人只在两端:定标准、判对错。
- 验收标准要写成可执行的动作,这是整条链子里性价比最高的一步。
- 能自动化的都写成自动化:定时推送、状态触发,交给 HiFox 的 Automation;
- 不确定的先问人:比如「要不要发布成公网链接」「表白信文案能不能推」——这类涉及外部可见性和他人感受的决策,必须由人拍板。
最后一条尤其重要。HiFox 官方文档在「必要的准备」里也强调,人工验收要验两件事:走一遍需求流程,确认没有 Agent 漏掉的问题;查验 Agent 留存的证据,确认 Agent 的流程是对的。
Agent 能替你做完 90% 的执行,但不能替你承担那 10% 的判断。
下一步我想继续做的:把「心动盲盒」的图片配图功能补完(目前 65 张只生成了 8 张),以及把两个项目的验收脚本合并成一套通用的「HiFox 交付物自检」流程。


实践是检验真理的唯一标准
更多推荐

所有评论(0)