- 移除 GovAI, nomifun-tauri, 算力盒子 的 submodule 引用 - 添加所有子项目的完整源代码 - 保留原始 .git 为 .git.bak 备份
16 KiB
AutoWork 与 Requirements
AutoWork 是 NomiFun 的旗舰自动化能力:一块 需求看板(requirements board)加上一个 编排器(orchestrator),由它驱动 AI 智能体(或运行在终端中的 agent CLI)逐条处理这些需求,无需你全程盯着。
你登记需求,按 tag 分组,把 tag 绑定到一个会话(对话或终端),编排器就会按顺序认领、执行并完结它们。当某条需求进入终态时,可以触发 完成通知(Lark/飞书 webhook),让你的团队第一时间知道结果。
这里描述的所有内容都是 后端权威 的:AutoWork 在进程启动时自动恢复,无论你是否打开 UI 都会运行。
概念
| 术语 | 含义 |
|---|---|
| Requirement | 一个工作单元:标题、内容(实际指令)、tag、order_key(按字典序比较的字符串)以及状态。存储在 SQLite 中。 |
| Tag | 任意字符串,用来把需求归入一个队列。绑定关系、看板列以及 webhook 路由都以 tag 为键。 |
| Status | pending → in_progress → done(或 failed / cancelled)。看板视图每个状态对应一列。 |
| Claim & lease | 编排器原子地把某 tag 中 order_key 最小的 pending 需求转为 in_progress,并写入一份带过期时间的租约(lease)。 |
| Lease sweeper | 一个后台任务(每 60 秒一次),会把租约已过期、且持有它的会话已不在的 in_progress 行重置回 pending——这样崩溃永远不会让任务孤立。 |
| Orchestrator | 每个目标对应一个的循环:认领 → 注入 → 等待 → 完结 → 重复。每个绑定的会话有一个循环。它是常驻的:队列空了就空闲等待,不会退出。 |
| Target | 实际执行工作的对象。两种:会话(一个 AI 智能体),或者 终端(通过 PTY 运行的真实 CLI 智能体)。 |
| 回合完成 | 一轮如何宣告"完成"。对智能体目标来说,是该智能体结束本轮回复(或调用 Nomi 专属工具);对终端目标来说,是终端输出静默下来(干净收尾)。 |
| Completion notifier | 当需求进入 done/failed/cancelled 时触发的 Lark/飞书 webhook。按 tag 绑定。 |
| IDMM | 智能决策模式(Intelligent Decision-Making Mode)——一个会话级监督器,能在 provider 故障和决策卡顿时让目标继续存活。可与 AutoWork 叠加使用。 |
单条需求的生命周期
pending ──claim_next()──▶ in_progress (lease) ──injection──▶ agent / CLI runs
│ │
▼ ▼
sweeper re-pends if lease Finish event / quiescence
expires & loop is gone │
▼
done | failed | cancelled
│
▼
CompletionNotifier fires (best-effort)
当 tag 为空时编排器 不会 退出。它会等待唤醒通知(外加一个 10 秒兜底轮询),并永久持续认领,因此向已绑定的 tag 新提交的需求几乎是即时被拾取。
它仅在以下情况退出:
- 你对该目标关闭了 AutoWork;
- 绑定触达了
max_requirements上限(此时配置会被持久化为已禁用,使该上限在重启后依然生效);或 - 某个终端目标对应的行被删除(仅仅是 PTY 退出的终端会进入空闲并等待重新启动——它并不会停止循环)。
三种视图
AutoWork 在每个视图中的数据完全相同,视图只是不同的"镜头"。
需求列表 — /requirements
扁平表格。可按 tag、状态或全文搜索过滤。可批量删除选中行。点击行可以打开详情抽屉;编辑 路径是 /requirements/:id/edit,新建需求 通过 /requirements?new=1 打开,旧的 /requirements/new 会重定向到这里。
看板 — /requirements?view=board
针对所选 tag,每个状态一列。这里有意 不 通过拖拽来改状态;请使用详情抽屉。看板会在每次 requirements.* 实时事件触发时重取数据,因此能跟随编排器实时变化。
Tag sessions — 需求平台 → 扩展能力 → 自动执行
AutoWork 的管理面板(/requirements/extensions?tab=autowork)。列出所有 tag、所有绑定(哪些会话和终端绑定到哪个 tag)、每条绑定的实时运行状态(Idle,或正在执行某轮时为 Active)。每个 tag 的完成 webhook 现在在旁边的 通知 tab(/requirements/extensions?tab=notify)。
这里是你"巡视舰队"的地方。要在某条绑定上 启动 AutoWork,请打开会话本身并在那里切换 AutoWork 开关——那才是绑定 tag、设置 max_requirements 和持久化配置的标准位置。
提交一条需求
在列表页点击 新建需求(或访问 /requirements?new=1)。表单包含:
- 标题:简短的标签。
- Tag:选择已有 tag 或键入一个新值。tag 在首次使用时会被创建。
- 内容:交给智能体 / CLI 的实际指令。当作 ticket 来写:上下文足够让智能体不必反问就能开始,并附上清晰的"完成定义"。
- Order key:用于队列排序的字符串。按字典序排列,因此常见模式如
1.0、1.1、1.2.0等等。值越小越早。 - 状态:默认是
pending。你也可以在这里手动把某行标记为done或cancelled。
提交后该行进入队列。如果已有会话绑定到该 tag,它会立刻被唤醒并开始处理这条需求(前提是没有别的需求排在它前面)。
绑定会话:智能体 vs 终端
一条绑定形如 (target_kind, target_id, tag, max_requirements?)。target kind 只有两种。
智能体目标(一个会话)
打开任意会话。头部有一个 AutoWork 控件。选择 tag,可选地设置完成上限,然后启用。
每一轮中发生的事:
- 编排器认领该 tag 中下一条
pending需求。 - 它构造一段注入 prompt,点名该需求并告知智能体如何上报完成状态。具体协议是 engine-aware 的:
- 仅在 Nomi-engine 会话上,智能体会注册
requirement_complete/requirement_update_status工具,并由 prompt 要求模型调用它们。 - 在所有其他 engine(ACP / Codex / Gemini / Openclaw / Nanobot / Remote)上,智能体不会注册任何 requirement 工具,因此 prompt 使用 无工具协议:把工作做完,以一段纯文本完成说明结束本轮,平台会在本轮干净结束后自动记为
done。失败通过纯文本上报(prompt 要求模型把最后一行以Requirement failed:起头,紧跟原因)。
- 仅在 Nomi-engine 会话上,智能体会注册
- 注入消息会从用户可见的对话记录中隐藏。
- 编排器订阅该智能体的流,等待
Finish(干净)或Error/超时(重置回 pending 或 fail)。同时它会把智能体的文本输出捕获到一份 tail-bounded 的 completion note,存到该需求上;在无工具协议的 engine 上,这份 note 就是发到下游的报告。 - 当本轮干净结束时,
finalize_if_needed把该行记为done并触发通知器。
终端目标(运行在 PTY 中的 agent CLI)
打开预设为 claude 或 codex 的终端(普通 shell 不符合条件)。
Gemini 终端可以手动运行,但后端目前不会接受它作为终端 AutoWork 目标:
它的回合生命周期和完成契约还没有接入编排器。符合条件的终端头部会显示同一个
AutoWork 控件;绑定一个 tag 并启用即可。
每一轮中发生的事:
- 编排器在注入 之前 订阅终端的实时输出流(这样不会漏任何字节)。
- 它向 PTY 写入需求 prompt,外面包了一对 bracketed-paste 标记(
ESC [200~ … ESC [201~),后跟CR,使多行文本作为单次粘贴落入 CLI 的编辑器,并由 Enter 实际提交。 - prompt 只要求 agent 把活干完、结束本轮回复——不需要打印任何标记。从交互式 TUI 里抓协议字符串被证明不可靠(光标重绘输出、没有干净的换行、模型抄错 code),所以完成判定改为基于回合本身。
- 当输出 静默(在最少 3 秒后≥10 秒无输出,且 PTY 还活着),说明 agent 已干完并回到空闲——本轮记为
done,与无工具的对话 agent 用的是同一套「干净收尾即完成」契约。 - 如果 agent 无法完成,会被要求用纯文本明确说明(例如最后一行以
Requirement failed:开头);这类回合在平台层面仍记为done,拿不准时请回看对话。 - 中途 PTY 死亡 → 重置回 pending。整轮硬超时是 1 小时。
强烈推荐 Full Auto。 一旦本轮撞到交互式批准提示,会一直阻塞到超时。每个 agent CLI 都有一个非交互式开关,终端的 "Full Auto" 模式会替你加上(参见 Terminals → Creating a terminal)。
被绑定但 PTY 已退出的终端会让循环以空闲方式存活:当你重新启动该终端时,AutoWork 会从中断处继续——无需先关闭再开启绑定。
启动恢复——它在你不在场时也会运行
编排器的运行集合存放在内存中,但每条绑定的 enabled、tag、max_requirements 都已持久化(在会话的 extra.autowork 或终端的 autowork 列中)。进程启动时后端会列出每个用户、遍历每条 tag 绑定,并 自行启动 这些循环。要让 AutoWork 工作你不必去打开会话页面;UI 只是把已经在跑的状态展示给你看。
这就是为什么"AutoWork 只在我开着标签页时才工作"是一个 bug 而不是 feature。如果你观察到这种现象,去检查编排器日志中是否有该用户/目标的 resume 失败记录。
完成通知(Lark / 飞书)
当需求进入终态时,会调用 CompletionNotifier。今天它做的事:
- 查找该需求 tag 的 per-tag 设置——如果该 tag 没有设置或没有绑定 webhook,通知器静默 no-op。
- 按 id 查找绑定的 webhook;如果它处于禁用状态,no-op。
- 构造一张 Lark 互动卡片,字段如下:
需求id·需求名·需求内容(截断到 500 字符) ·完成状态(done/failed/cancelled) ·完成记录(报告)(本轮中捕获的 completion note,截断到 500 字符)。 - POST 到 webhook URL。如果该 webhook 配置了 secret,请求会按 Lark 自定义机器人的标准方案签名(
HMAC-SHA256(key="{ts}\n{secret}", msg=""),base64)。 - 失败会以
warn记录并吞掉——一个不稳定的 webhook 永远不会影响需求状态。
配置步骤
- 进入 需求平台 → 扩展能力 → 通知(
/requirements/extensions?tab=notify)并 Create webhook:填写名称、Lark 自定义机器人 URL,以及(可选的)匹配 secret。点 Test 发一张卡片,验证机器人可达。 - 在同一个 通知 tab 里找到该 tag,从 per-tag 下拉框中挑选 webhook。设置按 tag 保存。
你可以随时改变某个 tag 指向哪个 webhook,包括清空绑定以静音该 tag 的通知。
IDMM——让卡顿中的本轮继续存活
IDMM 是一个独立、可选的监督器(nomifun-idmm)。它监视会话,并在检测到卡顿时介入:
- 规则层(无 LLM):provider 报错、反复重试、模型在工具调用上转圈等等——以确定性策略处理。
- Sidecar 层:调用一个轻量备用模型来下达下一步决策,避免会话挂死。
当 AutoWork 启动一轮时,它会请求 IDMM(如果已对接)在本轮持续期间 保证监督 该目标。两个特性可以组合:AutoWork 推动前进,IDMM 让每一轮不至于卡死,从而真正进入终态而不是超时。IDMM 与 AutoWork 在同一处切换(会话头部)。
每层策略的细节和介入日志 API 见 crates/backend/nomifun-idmm/。
想看完整全貌 —— 规则层、旁路模型、会话保活,以及何时开启 —— 参见专门的 智能决策(IDMM)指南。
路由与 API
| 用途 | 位置 |
|---|---|
| 需求列表 | /requirements |
| 看板(按 tag) | /requirements?view=board |
| Tag sessions 管理 | /requirements/extensions?tab=autowork |
| 通知配置 | /requirements/extensions?tab=notify |
| 新建 / 编辑 | /requirements?new=1、/requirements/:id/edit |
旧版 /autowork、/requirements/tag-sessions |
重定向到 /requirements/extensions?tab=autowork |
旧版 /requirements/new、/requirements/kanban |
重定向到当前 query-param 路由 |
| 列出 / 创建需求 | GET /api/requirements、POST /api/requirements |
| Tags | GET /api/requirements/tags |
| Tag 绑定(管理) | GET /api/requirements/tag-bindings |
| Per-tag 看板 | GET /api/requirements/board?tag=… |
| 获取 / 更新 / 删除 | `GET |
| 状态 / 完成 / 认领 | POST /api/requirements/:id/status、…/complete、…/claim |
| AutoWork 开关 / 状态 | POST /api/requirements/autowork、GET …/autowork/:kind/:tid |
| Webhooks | `GET |
| Per-tag webhook | `GET |
实现注记(写给好奇的你)
requirements.conversation_id有意 不带外键 指向 conversations 表。一条需求一旦创建,会随着重置回 pending 在多个会话之间轮转;用引用完整性把它绑死到单个 conversation 会让清理逻辑变得别扭,而且并不会带来真正的安全保障。请把该列视为参考性字段。- 编排器的
wakeNotify 与RequirementService共用;任何会重置回 pending 或创建工作的状态变更都会触发它,循环也会在每次claim_next()调用前后用 armed-then-await 的方式包起来,因此在"claim 返回 None"和"await"之间到达的唤醒永远不会丢。 - 终端注入仍用 bracketed-paste 标记把多行 prompt 作为单次粘贴落入,再在一拍之后单独写一个
CR提交(与 paste 同批写入的 CR 会被现代 agent TUI 的 paste-burst 检测吞掉)。 - 无工具 engine 的 completion note 有上限(
MAX_NOTE_CHARS = 4000)且 偏向尾部——智能体倾向于在末尾做总结,因此当需要截断时我们保留尾部。




