学习目标:看懂 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 我想讲三件事:

  1. HiFox 到底怎么用(心智模型 + 快速上手四步);
  2. 我怎么把两个小项目真的跑完了(附源码、附截图、附实测数据);
  3. 哪些坑是文档里没写、但你一定会踩的。

在这里插入图片描述


二、HiFox 的心智模型:6 个词就够了

HiFox 的概念不少,但真正天天用到的只有这几个:

概念一句话解释我的用法
Space(空间)一个项目的边界,里面有成员、任务类型、状态、标签一个空间 = 一个独立项目。心动盲盒单独一个空间,会年抽奖放默认空间
Task(任务)一个可交付的工作项,类似 Jira issue任务正文写清「背景 / 目标 / 交付物 / 验收标准 / 不做什么」
Agent可自定义的 AI 队友,有自己的职责说明和技能我配了需求 / 开发 / 验收 / 自动化测试四个,分工见第四节
Computer + RuntimeAgent 真正干活的机器和执行器(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.html13.87 KB抽奖页。零 CDN、零框架、零构建,移动端优先
draw.js27.66 KB抽奖引擎 + CLI。浏览器和 Node 共用同一套逻辑
content.js6.99 KB文案池:情话 35 / 小奖励 15 / 彩蛋 14 / 表白信 1 篇
push_wecom.sh11.33 KB企微群机器人推送,含幂等与密钥打码
README.md19.30 KB使用说明
SPEC.md / SPEC_V2.md25.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 个问题,其中两个特别典型:

  1. 性能基准被误纳入覆盖率插桩运行,导致 3 条基准假性失败 —— 改为独立无插桩配置后实测 500 轮抽取 324 ms,与基线吻合;
  2. 视觉回归「中奖揭晓」基线是一张纯灰图 —— 撒花层是全屏浮层,遮罩污染了整张截图,断言形同虚设。改为固定随机源制造确定性,并二次复跑验证零像素差异。

第二个问题值得所有做视觉回归的人警惕:你的基线错了,测试就是在稳定地验证一个错误。


总结

把两个项目跑完,我对 HiFox 的认知收敛成几句话:

  1. HiFox 不是「更聪明的 AI」,是「有结构的 AI 协作」。它的价值在任务流转和留痕,不在模型本身。
  2. 全流程自动化的骨架是「状态 → Agent → 产物 → 状态」,人只在两端:定标准、判对错。
  3. 验收标准要写成可执行的动作,这是整条链子里性价比最高的一步。
  4. 能自动化的都写成自动化:定时推送、状态触发,交给 HiFox 的 Automation;
  5. 不确定的先问人:比如「要不要发布成公网链接」「表白信文案能不能推」——这类涉及外部可见性和他人感受的决策,必须由人拍板。

最后一条尤其重要。HiFox 官方文档在「必要的准备」里也强调,人工验收要验两件事:走一遍需求流程,确认没有 Agent 漏掉的问题;查验 Agent 留存的证据,确认 Agent 的流程是对的。

Agent 能替你做完 90% 的执行,但不能替你承担那 10% 的判断。

下一步我想继续做的:把「心动盲盒」的图片配图功能补完(目前 65 张只生成了 8 张),以及把两个项目的验收脚本合并成一套通用的「HiFox 交付物自检」流程。


在这里插入图片描述
在这里插入图片描述

实践是检验真理的唯一标准

Logo

更多推荐