Files
dsh_ai1net_server/交付物/唤醒机制-实现方案对比与选型-20260930.md
admin c1b5e4d966 chore(工作区): 全量入库 + 补齐 .gitignore(以工作区为准)
- 变更规模:新增 514 / 修改 62 / 重命名 155 / 删除 4(归档重组与文档轮次)
- .gitignore 修:`归档/**/db-cwd归一-备份-*/` —— 原规则写绝对层级(归档/db-cwd归一-…),
  目录搬进 归档/配置与备份/ 后**静默失效**,43 MB 的 DB 备份又变成未跟踪
- .gitignore 补:嵌套 git 内部数据(归档/内嵌git-20261008/、归档/skills-git-旧线-20261007/dotgit-原样移出/)
- .gitignore 补:运行态与部署副本(.workbuddy/collab/、.workbuddy/tools/、.workbuddy/.load-pending、.workbuddy/tmp-*)
- .gitignore 补:备份件(*.bak-*)
- 未跟踪文件从 2190 降到 890(其余为 归档/ 归档件与 .workbuddy/memory/ 知识文件,按口径入库)
2026-10-10 23:13:22 +08:00

18 KiB
Raw Permalink Blame History

唤醒机制 · 实现方案对比与选型(2026-09-30)

🔴 修正(2026-09-30 17:2x · 用户否决「限制了使用」)

上一版把通道定成 POST /sessions/{id}/reply(只认 live)⇒ 只能唤醒"桌面当前打开的那一个会话" ⇒ 这就是"限制使用",用户已否决。 修正:通道改用 /api/v1/jobs —— 限制不存在了。 实测与源码级依据见 §8。一句话: 任意会话(含非 live / 归档)都能 POST /jobs/resume?cwd= 拉成 headless worker(idle、不重放 prompt)→ 再 POST /jobs/{id}/reply 派活;不会把用户桌面的对话切走。

结论先给:最佳 = 「常驻静默判定 + 条件触发投递」(D4),而不是"每 5 分钟叫醒一次"。 理由一句话:任何"唤醒会话"都等于开一轮 AI,必然烧一轮 token;所以最优设计不是每隔 5 分钟叫醒,而是每隔 5 分钟看一眼(判定≈0 token),只在真有事时才叫醒。 ⛔ 次优才是"纯闹钟"(每次必醒);⛔ 最差是自动化(粒度只有 1 小时 + 每次还开一个新会话)。


§1 先把一条成本律钉死(它决定了选型)

唤醒会话  ⇒  注入一条消息  ⇒  会话开新的一轮  ⇒  🔴 必然消耗一轮 AI

推论:

  • 「5 分钟一次」= 288 轮/天,这是无条件的 token 成本 —— 不管用哪条通道(自动化 / gateway / 后台任务脉冲)都一样,因为贵的是唤醒这件事本身,不是通道。
  • ⇒ 真正的杠杆是"减少唤醒次数",不是"换更省钱的通道"。
  • ⇒ 由此得到本选型的核心思想:把"看"和"叫醒"拆开 —— 判定用进程内代码(≈0 token、可任意快),叫醒才投递(1 轮 AI)。

§2 候选机制清单(M1–M7,全部有实测依据)

# 机制 粒度 新增会话 每轮 AI 生命周期 实测依据
M1 自动化(rrule) ⛔ 最小 1 小时 🔴 每次 1 个新会话 有 宿主排期,持久 rrule 是硬白名单(FREQ 只认 HOURLY/DAILY/WEEKLY/MONTHLY/YEARLY;全包 MINUTELY/SECONDLY 命中 0);automation_runs 74/74 ⇒ 一次运行=一个新会话
M2 本机 gateway reply 直连 任意(由调用方决定) 0 有 靠调用方 实测 POST /reply → 200 {"delivered":true};⛔ 只认 live(非 live ⇒ 409)
M3 会话自建一次性后台任务(脉冲) 任意(实测 300 s) 0 有(每棒一轮) ⚠️ 绑该会话(会话/宿主一关就没) A/B/C/D 四组:完成即唤醒(与 stdout 无关);静默安全;300 s 不被回收
M4 常驻静默 + 内部主动 reply 任意(判定间隔) 0 只在实际投递时 ⚠️ 绑该会话 常驻能长跑(跨轮存活实测);但常驻自己不唤醒(实测)⇒ 唤醒必须来自它调的 reply
M5 外部定时器(服务器 47 / 手机快捷指令)→ 443 → reply 任意 0 有 ✅ 不绑会话(宿主重启后仍可投递) 443 通道已建、V2 已验收
M6 钩子驱动(有事就投递) 事件触发,不是闹钟 0 有 宿主在跑就有效 钩子是宿主起的子进程,有网关口令、不占会话、零 token;⚠️ 闲时无事件 ⇒ 不能当定时器
M7 官方手机助理(微信/企微) 人驱动(任意) 0(固定"助理专属文件夹"一个会话) 有 官方维护 官方文档原文:「助理(远程任务):仅一个会话」;本机 未启用;粒度看人

一句话排除:

  • ⛔ M1 排除 —— 粒度差(1 小时)+ 每次开新会话(会话列表污染,旧坑"会话洪泛")。
  • ⛔ M6 不能单独用 —— 它只会在"你已经在动"时才有事件;你不动它就不响。
  • ⛔ M7 是"人驱动",不是自动唤醒;且只能进它自己那一个会话。
  • ⚠️ M3 的"常驻循环 + 周期 echo"变体已实测排除 —— 永不"完成" ⇒ 永不唤醒。

§3 对比矩阵(按"能否当 5 分钟心跳"评)

维度 M1 自动化 M2 gateway 直连 M3 一次性脉冲 M4 常驻+条件投递 M5 外部定时器
5 分钟粒度 ⛔ 不行 ✅ ✅ ✅ ✅
新增会话 🔴 每次 1 个 ✅ 0 ✅ 0 ✅ 0 ✅ 0
AI 轮数/天(5 分钟) 288(且额外开 288 会话) 288 288 ✅ 只在实际有事时 288
常驻进程 1(宿主内) — ✅ 0 ⚠️ 1(必须完全静默) —
宿主重启后 ✅ 还在 ✅ ⛔ 没了 ⛔ 没了(除非钩子自拉起) ✅ 还在
需要口令 ⛔(在进程树内,天然有) ✅ 需要 ✅ 不需要 ✅ 需要 ✅ 需要(且要跨网传)
跨网依赖 ✅ 无 ✅ 无 ✅ 无 ✅ 无 ⚠️ 有(443/覆盖网络)
落地复杂度 低 低(工具已建) 低(工具已建) 中(要写判定逻辑) 中高(外部一端+令牌)
可一键关停 ✅ — ✅ ✅ ✅
红线对账 ✅ ✅ ✅ ⚠️ T1 字面冲突(本意满足:完全静默) ✅

§4 ✅ 最佳选择:D4「常驻静默判定 + 条件触发投递」

4.1 架构

 ┌──────────────── 常驻(1 个进程 · 完全静默 · stdout 全重定向)────────────────┐
 │  每 N 秒(建议 60–300 s)做一次【纯代码判定】—— ≈0 token、不进会话:         │
 │    ① 有没有待处理的活?(台账 / advance.md / automation_runs)              │
 │    ② 目标会话是不是 live?(GET /api/v1/sessions/live)                      │
 │    ③ 距上次投递是否够久?(去抖,⛔ 防同一条重复投)                          │
 │    ④ 任务图是否已全 done?(是 ⇒ 收工,不再投)                              │
 └───────────────────────────────┬───────────────────────────────────────────┘
                                 │  只有 ①②③④ 全过 才走这一步 ↓(贵的那一步)
                                 ▼
                 POST /api/v1/sessions/{live}/reply   ← 本机 127.0.0.1,无代理
                                 │
                                 ▼
                         会话被唤醒,开 1 轮 AI 去推进

关键点:

  • 判定在进程内(≈0 token、可 10 秒一次)⇒ 响应快,但不烧钱。
  • 投递才是开销 ⇒ 用四道闸门把它压到"真有活才干"。
  • 常驻必须完全静默:输出全重定向到文件;⛔ 一旦往 stdout 吐,就会把转录推过 10 MiB ⇒ diagnostic-log dropped ⇒ 界面像卡(09-29 实物)。
  • 常驻形态:本机唯一可行=会话后台任务 + stdout 全重定向(沙箱回收子进程 + 程序黑名单封死了计划任务/服务那条路)。

4.2 为什么它胜过其它

对比 差在哪
vs M3 一次性脉冲 M3 是"每棒一轮 AI",288 轮/天照烧;D4 把"看"变便宜,只在有事时才叫醒。M3 还有一个硬伤:链断即停(某轮我没续棒就永久停了)。
vs M5 外部定时器 M5 同样 288 轮/天,且多一层跨网依赖与令牌传递;但 M5 有一个 M4 没有的优点:宿主重启后仍在。⇒ M5 作为 D4 的降级备份。
vs M1 自动化 粒度差一个数量级,还每次开新会话。
vs M6 钩子 钩子只在有事件时响,没有事件就不会有"该干活了"的提示;但它是 D4 的最佳加速器(有人动过 ⇒ 立刻追加判定,不必等下一个 N 秒)。

4.3 ⚠️ 前提与代价(必须一起接受)

  1. 只能唤醒"桌面当前打开的那一个会话" —— reply 只认 live;非 live 只能走 ACP 借用,而借用会把用户正在看的对话切走(红线 T2 ⇒ 禁止)。跨会话唤醒实质上不可做,需求要按这个收窄。
  2. 绑会话/宿主:宿主重启后就没了 ⇒ 需要一台"重启后自动拉起"的机制(钩子 SessionStart 能否拉起常驻的子进程:未验证,列为待测)。
  3. T1 字面冲突:T1 写的是"⛔ 垫片/桥不得放进会话后台任务"。D4 属于"常驻判定器放进会话后台任务" ⇒ 文字上撞,但 T1 的本意(别让输出回流拖死宿主)靠"完全静默"已满足 ⇒ 采纳前需用户明确覆盖。
  4. 必须能一键关停(T7):停掉进程即彻底停 —— 因为不投递就不会有任何反应(天然的 fail-safe 方向)。
  5. 每轮唤醒的 token 成本 ≠ 0 —— 只是从"288 次"降到"实际需要几次"。

§5 落地步骤(可执行)

步 动作 验收
1 定判定间隔 N 与四道闸门的具体条件(默认 300 s;闸门照抄既有 collabd.wake_round 的四条) 闸门逐条可复现
2 写判定器(零依赖单文件;口令只从环境变量读,⛔ 不落盘) 冷启动即产出正确判定
3 以一次性能跑满 N 秒的形态试跑(先跑 10 分钟) 期间 sessions.updated_at 只在真的该投递时才动
4 加去抖+终止条件(任务图全 done ⇒ 自行退出) 全 done 后不再投递、进程自行退出
5 接 M6 钩子做加速(人一动手就追加一次判定) 有事件时判定提前触发
6 过 V7 验收(连续 1 小时/≥50 次操作:宿主 pid 恒定、无 429、桌面"当前对话"未被改变、无新增对外端口、配置哈希零差异) 六项全过
7 备降级路径 M5(服务器 47 或手机快捷指令 → 443 → reply) 宿主重启后仍能投递

§6 一句话答复"如何实现最好"

不要做"每 5 分钟叫醒"的闹钟,要做"每 5 分钟看一眼、有事才叫醒"的门卫。 门卫 = 一个完全静默的常驻判定进程(会话后台任务起)+ 它自己调本机 gateway reply; 判定走代码(≈0 token),投递才花 AI;关掉它 = 功能消失、宿主无感。 「纯闹钟」只有在明确接受 288 轮/天时才考虑,且形态选 M3 一次性脉冲链(最简、不要口令)。


§7 出处

是什么 在哪
四组唤醒实验(A/B/C/D)+ E1/E2 + 链式续棒 交付物/唤醒-会话自建后台任务方案评估-20260930.md
gateway 直连实测 + 实现 交付物/唤醒-本机网关直连测试-20260930.md ・ .workbuddy/collab/wake-session.py
一次性静默脉冲工具 .workbuddy/collab/wake-pulse.sh
rrule 硬白名单(M1 排除依据) 技能 workbuddy-extension-surface §1.4
T1–T7 + V7 验收判据 交付物/手机App经覆盖网络操作WorkBuddy-架构定稿v3-20260929.md §0.5
「唤醒为什么断」原始定案 交付物/查唤醒为什么断-结论-20260930.md

§8 修正(17:2x · 解除「只能唤醒 live 会话」这条限制)

8.1 用户否掉的是什么

用户原话:「这个方案限制了使用 不行」

上一版把通道定死成 POST /api/v1/sessions/{id}/reply —— 而它只认 live ⇒ 只能唤醒"桌面当前打开的那一个会话"。要求"手机能看/能回桌面任意会话"时,这条就是硬限制 ⇒ 被否决。

8.2 🔴 正解:宿主原生有 /api/v1/jobs(智能体实例),不受 live 限制

源码级依据(app.asar → /cli/dist/codebuddy-headless.js,OpenAPI 描述原文):

/api/v1/jobs/resume:summary=「恢复归档会话为智能体」,description=「使用 --resume 启动常驻 worker,不重放 prompt;恢复后以 idle 状态出现在智能体列表。」,cwd 参数在 query(不是 body)。

两条关键含义:

  • ✅ 不重放 prompt ⇒ 拉起来不会自动跑一轮、不偷烧 token。
  • ✅ 它是 headless 常驻 worker(kind:"background"、独立 pid)⇒ 不接管桌面 ⇒ 不会把用户正在看的对话切走。

实测读数(本机 59486,17:19):

端点 实测返回
GET /api/v1/sessions 全部 20 个会话(id / name / createdAt / updatedAt / messageCount / isCurrent)⇒ 会话列表本来就不受 live 限制
GET /api/v1/jobs {"jobs":[]}(当前无活跃 worker)
GET /api/v1/jobs/resumable 20 条候选(含 fe146dd9 / 9ad1f801 / 44226c16 / 4d22f079 … 全是非 live 会话)+ hasMore / nextOffset 分页
GET /api/v1/status {status:"ok", busy:true, activeSessionId, runStatus:"tool_executing"} ⇒ T2「记原活会话并归还核对」的现成判据字段
POST /api/v1/jobs/resume?cwd=… 返回 job 对象:`{id, state:"working", tempo:"idle", cwd, kind:"background", alive:true, sessionId, pid}
POST /api/v1/jobs/{id}/stop {"stopped":true} ⇒ 可一键清掉

8.3 修正后的通道(三层,按目标自动选)

目标状态 走哪条 代价
live(桌面当前打开) POST /api/v1/sessions/{id}/reply 最省(0 新进程)
非 live / 归档 POST /api/v1/jobs/resume?cwd=<项目目录> body {sessionId} ⇒ 得 job(idle)⇒ POST /api/v1/jobs/{id}/reply 1 个 headless worker
看列表 GET /api/v1/sessions(全部)+ GET /api/v1/jobs(活跃)+ GET /api/v1/jobs/resumable(可分页) 只读

⇒ 「只能唤醒桌面打开的那一个」这条限制不存在。 触发者(外部定时器 / 常驻判定器 / 一次性脉冲)与投递目标彻底解耦 —— 手机端因此能列出全部会话并挑任意一个派活。

8.4 ⚠️ 三个新踩到的坑(已登记,⛔ 别再犯)

  1. 🔴 /jobs/resume 不校验 sessionId —— 我拿一个全 0 的假 id 探契约,它照样起了一个真 worker 进程(kind:"background"、pid=44444)。 ⇒ ① 这不是零副作用探针,⛔ 别当只读接口试;② 调用方必须自己先校验 id 有效。 ✅ 已清理:POST /jobs/00000000/stop → {"stopped":true};复核 jobs=[],两个 pid 均已退出。
  2. ⚠️ /jobs 按 id 前缀聚合:两个 worker 同属一个 sessionId ⇒ 列表里只出现 1 条 ⇒ stop 完必须 GET /jobs 复核,别以为停一次就干净。
  3. ⚠️ cwd 不强制正斜杠:传 e:\ProgramData\... 也接受(会被原样存进 job)。

8.5 🔴🔴 更硬的一条:「投递成功」≠「被唤醒」 —— 目标必须有人消费

来自本机实测(tmp/supervise-inbox/NEED-USER.md,2026-09-30 10:50):

原因:投给会话 fe146dd9 的通知没被消费(投出后 5.9 分钟该会话零活动)。 典型的宿主侧卡住:parkInQueue + hasWaiter=false —— 消息进了队列、没有消费者。 ⇒ 请把窗口切走再切回,或关掉再打开该会话窗口 ⇒ 队列随即会被排空。⛔ 别反复发消息试探。

含义(必须写进验收判据):

  • POST …/reply(含 /jobs/{id}/reply)返回 200 delivered:true 只代表「已入队」;真正被唤醒还需要该会话有一个"消费者"(waiter)。
  • 没有消费者时:消息静静躺在 parkInQueue 里,会话零活动 ⇒ 从外面看就是"投递成功了但没反应"。
  • ⇒ 🔴 这就是"看着能唤醒、实际不唤醒"的那道隐性门;也是最容易被误判成"通道没打通"的真因。
  • ⇒ 任何唤醒方案都必须带一条"消费确认":投递后回查 sessions.updated_at / status.activeSessionId 是否前移,⛔ 不能拿 200 当闭环(此判据与本报告 §9.2 的链式续棒取证方式一致)。
  • ⇒ 且这条 AI 侧不能自救(需要人在客户端把窗口切走再切回)⇒ 方案设计上要给出"人可见的告警",而不是静默重试(⛔ 反复发消息只会在队尾再堆一条)。

8.5.1 本机自查:我唯一一次真投递(13:55)也没被消费

取证 读数
13:55:11 POST /sessions/6ecf6d98…/reply HTTP 200 {"data":{"delivered":true}}
GET /api/v1/sessions/6ecf6d98…/history 里搜标记「本机网关唤醒测试」 0 命中
GET /api/v1/jobs/resumable 里本会话 lastUserMessage(17:2x) 意思就是走不通对吗(用户 17:22 的原话,注入那条从未成为用户消息)
时间跨度 4.5 小时(其间有多次空闲窗口)仍未出现

⇒ 🔴 结论:「程序投递 ⇒ 会话被唤醒」这条,目前是零成功实例;delivered:true 只证明了「入队」。 ⚠️ 诚实标注:单次样本,且期间用户插了一条真消息,不能严格排除"被后续消息覆盖/顶掉" ⇒ 要坐实必须重跑一次干净测试(见下)。

8.5.2 与之对照:宿主自己产生的唤醒,实测 6+ 次全成功

后台任务的 <task-notification>(§3 结论 1 / §9.2 链式续棒)不需要 waiter —— 它是宿主自身投给该会话的,实测 A/B/C/D/E1/E2 + 两棒链全部唤醒成功。

⇒ 🔴 两半要分开看:

半 状态
宿主 → 自己的会话(后台任务完成通知) ✅ 走通(6+ 次实测)
外部程序 → 任意会话(reply / jobs reply) ⛔ 按现有证据走不通(零成功实例;被 hasWaiter=false 卡住)

分水岭 = 目标会话有没有"消费者(waiter)",即客户端有没有把它挂在当前窗口。人在用 / 刚切到它 ⇒ 有;没人挂着它 ⇒ 消息永远躺在 parkInQueue。

8.5.3 一次就能定性的验证(需用户在客户端配合,约 1 分钟)

  1. 用户在客户端把目标会话切走再切回(或关掉再打开);
  2. 立刻投一条带时间戳的测试消息;
  3. 出现 ⇒ 坐实「挂载即可消费」,这道门打开;
  4. 仍不出现 ⇒ 坐实「本机外部投递这条路不通」⇒ 转向已通的那半(后台任务完成通知只唤醒它自己的会话;跨会话需求另议)。