僚机 + 驾驶舱

系统全景 · 数据流 · bug & UX 清单 · 2026-06-10 22:55
00TL;DR 01数据流 02状态机 03数据 schema 04渲染路径 05bug 清单 06UX 优化 07修复顺序
00

TL;DR — 你提的 4 个问题 + 我扫出来的另外 6 个

#问题严重类型是不是你提的
1milestone 时间倒序 / 漂移HIGHbug✓ 你说的
2进行中的"当前节点"无呼吸灯MEDux✓ 你说的
3TTS 朗读僵硬(系统 Tingting/Reed)MEDux✓ 你说的
4summary / verdict 行文僵硬MEDux✓ 你说的
5milestone 时间挤一起(5 个都是 +13h19m)HIGHbug我扫出来的
6DM 根 chat 也进了 DB,每 5s tick 浪费 haiku tokenMEDperf我扫出来的
7verdict 重复 summary 内容LOWux我扫出来的
8超 1 天没动的 thread 仍在 tick(无 archive)LOWperf我扫出来的
9recent_actions 只存 10 条 → milestone 必然挤在尾部HIGHbug我扫出来的 (#5 的根因)
10modal 详情时间线 ASC(旧到新),可能不符你阅读习惯LOWux我猜的(需你确认)

核心洞察:#1 #5 #9 是同一个根因链 — recent_actions 只有 10 条 + 这 10 条往往是最近几分钟的密集 tool-call → haiku 看不到任务的真实时间分布 → 所有 milestone 被挤到尾部,offset_s 还互相覆盖。表面是"时间不对",根上是"haiku 缺乏时间感"。

01

数据流 — 一条飞书消息到驾驶舱卡片的完整链路

飞书 DM 爸爸打字 / Hermes 回复 Hermes gateway feishu.py 每条进出消息 _wingman_fanout_event() wingman-fanout.jsonl append-only event log (836KB / 1696 行) wingman daemon · launchd com.leway.wingman 线程 A: tail_fanout() 0.5s 轮询 → db_upsert 线程 B: tick_loop() 5s 找 active 线 → call_haiku haiku 5 字段输出: summary / verdict / milestones / changes / done ~/.hermes/wingman.db SQLite · thread_state 表 (8 条历史) cockpit FastAPI :7421 launchd com.leway.cockpit /api/state · 5s 客户端 tick /api/respond · 代发选项 浏览器 (爸爸手机/桌面) http://192.168.1.142:7421/ 完工 → lark-cli DM 根发 "✅ 完成 + 链接" done=true 事件也回到 jsonl (套娃 by design)
关键文件与进程清单
组件位置谁拉起
fanout hook~/.hermes/hermes-agent/gateway/platforms/feishu.py:_wingman_fanout_event()Hermes 主进程
fanout 日志~/.hermes/logs/wingman-fanout.jsonl追加写, 永不轮转 (隐患)
worker~/projects/wingman/worker.py (1176 行)launchctl com.leway.wingman
DB~/.hermes/wingman.dbSQLite
cockpit~/projects/hermes-cockpit/app.py + static/index.htmllaunchctl com.leway.cockpit
haikueagle proxy https://api.eagle.openclaws.co.uk/v1/messagesworker.py 5s tick
02

状态机 — thread 在 4 个状态间怎么走

idle 新建未 tick active bot 最近 5min 有动作 stalled bot 静默 5-15min done 完工 爸爸发首条 > 5min 静默 haiku 判 done haiku 直接判 done > 15min force_done 爸爸再发消息 → active tick 频率 active: 5s 一次 (但 actions 没变就 short-circuit 不烧 haiku) stalled: 30s 一次 done: 不 tick (除非 done_notified=0 时还有一次最后通知)
03

thread_state 表 — 每个话题在 DB 里长什么样

展开 schema 表 (15 列)
类型含义
thread_idTEXT PKomt_xxx (话题) 或 oc_xxx (DM 兜底)
card_msg_idTEXT飞书卡片消息 ID (v0.4 dashboard 模式不用)
statusTEXTidle / active / stalled / done
thread_nameTEXThaiku 给的 4-12 字标题
thread_first_msg_idTEXT首条 om_xxx (给 reply-in-thread 当 anchor)
last_user_msgTEXT最近 1 条爸爸消息原文
recent_actionsTEXT (JSON)仅最近 10 条 · ts/direction/sender_type/msg_type/content
summaryTEXThaiku ≤30 字 一句话现在干啥
verdictTEXThaiku ≤40 字 进展判断/警告/卡点
milestonesTEXT (JSON)haiku 给的 3-5 条 [{event, offset_s}]
changesTEXThaiku ≤50 字 改了哪些文件
tick_countINTEGER累计 tick 次数
started_at / updated_atINTEGERunix 时间戳
done_notifiedINTEGER1 = ✅ 已发送, 防重复
last_haiku_action_count / _tsINTEGER节流键 (actions 没变就不烧 haiku)

当前真实数据(trim)

thread_name                summary
─────────────────────────  ──────────────────────────────────────────
CC MVP Bridge项目进度询问    Hermes 正在改代码 4 处并编写测试,typecheck...
drone-picker项目bug修复完成  调研剪映是否有 MCP 接口,已给出结论:有但不切
摄影公司网站导航和内容重组    完成首页合作伙伴上移和合作页重排,修复底栏...
任务已取消,驾驶舱链接共享    技能已固化完成,用通用 Python 脚本替换旧流程...
设备输入问题咨询            补全生成「DJI Mic Mini语音输入」话题的正式上下文卡片
GitHub Fork项目longcut    完工 (15min 无动作兜底)
创建Azure China命令并检查    SSH 端口从 22 迁移到 18822,NSG 规则已生效...
oc_ffa37ba1bef2fb...      已收到chat_id,等待后续指示  ← 这条不该存在!
04

渲染路径 — 一张卡片由哪些字段拼出来

每张卡片自上而下:

位置字段来源排序逻辑
卡顶渐变色statusactive橙 / stalled琥珀 / done绿
BADGEstatus → RUNNING/STUCK/DONE
标题thread_name (haiku 生成 4-12 字)
右上时长elapsed_s = now - started_at
新进展(主角)summary
判断(副内容)verdict (若 ≠ summary)
时间线 3 栏milestones[-5:]仅 filter + slice,不 sort
"每 5 秒自动刷新"仅 status=active 显示有呼吸灯 (但在卡底不在节点)
快捷回复按钮backend parser 抠 A/B/C/1/2/3
关键源码片段(renderCard line 915-927)
// 时间线 3 栏 (最近 5 条 milestone)
const ms = Array.isArray(t.milestones) ? t.milestones.filter(m => m && m.event) : [];
const shown = ms.slice(-5);                            // 没 sort!
let tl = "";
if (shown.length) {
  const base = shown[0].offset_s || 0;                 // 用第 0 条做 base
  const rows = shown.map((m, i) => {
    const tm = t.started_at ? fmtTime(t.started_at + (m.offset_s || 0)) : "—";
    const off = i === 0 ? "起点" : fmtOffset((m.offset_s || 0) - base);
    return `<div class="tl-row"><span class="tl-t">${tm}</span>...`;
  }).join("");
  tl = `<hr class="card-rule"><div class="timeline">${rows}</div>`;
}
05

bug 清单 — 详情、根因、修法

#1HIGH bug milestone 时间倒序
static/index.html:917 · 你提的

症状:你看到"摄影公司"卡片 timeline +0h22m → +0h26m → +0h28m → +0h22m,最后一条回到 22 分钟。

根因:haiku 偶尔不按时序返回 milestones,前端 .filter().slice(-5) 不 sort 直接渲染。

前端加 .sort((a,b) => (a.offset_s||0) - (b.offset_s||0));后端 parse_haiku_text() 也加保险 sort。

#5HIGH bug milestone 时间挤在一起
worker.py:_humanize_action + haiku prompt · 我扫的

症状:看 "CC MVP Bridge":
+13h19m CC 派发任务开始
+13h19m 识别 Hermes 注入点需求
+13h19m 代码 4 处改完
+13h19m 进入测试阶段
4 条全是 +13h19m。看不出先后。

根因:started_at 是 13h 前但 recent_actions 只存了 10 条(都集中在最近 1 分钟内),haiku 看不到中间 12h59m 在干什么,5 个 milestone 只能从最近 10 条里抠 → 全集中尾部。

三条路并行:(a) prompt 显式要求"如果只能从最近行抠,offset_s 用 1 分钟间隔模拟分布而不是全部相同";(b) 加 milestones_archive 列,每次 haiku 输出后把"最早被挤掉的"那条存档,下次 prompt 把存档作为"已知过往节点"喂给 haiku;(c) 把 RECENT_ACTIONS_MAX 从 10 升到 20-30 (代价:每次 tick prompt 长 2x)。

#9HIGH bug recent_actions 只存 10 条
worker.py:44 RECENT_ACTIONS_MAX=10 · 我扫的 (#5 根因)

症状:长任务(>30 分钟)的 milestone 永远只反映最后几分钟,开始/中段细节全丢。

根因:最初设计是给短任务 (10-30 min) 的,没考虑长跨度任务 (多小时)。10 条 tool-call 在密集阶段 1 分钟就刷完。

改进 #5 的 (b) milestones_archive 是更优雅的解法;最简单的是把 RECENT_ACTIONS_MAX 提到 25 + 加一个 action_summary_so_far 字段让 haiku 自己累积长任务记忆(增量摘要模式)。

#2MED ux 进行中节点无呼吸灯
static/index.html:917-927 .tl-row · 你提的

症状:active 卡的 timeline 几行里看不出哪条是"刚刚发生"的;卡底虽然有"● 每 5 秒自动刷新"呼吸灯,但跟当前进度节点没视觉关联。

根因:renderCard 没给 timeline 最后一条标"current",CSS 也无 .tl-row.current 样式。

active 卡的最后一条 .tl-row 加 .current class,CSS 里给 .tl-t 前贴一个 ::before 呼吸灯(橙色,2px 圆点 + pulse)。

#3MED ux TTS 朗读僵硬
static/index.html:1194-1215 _speak() · 你提的

症状:系统选 Tingting/Eddy/Flo 这种机械嗓门读"摄影公司网站完成",断句生硬、情绪扁平。

根因:用的是浏览器自带 SpeechSynthesisUtterance,调的是 macOS Tingting (vintage neural)。新一代 neural voice 在 macOS 默认列表里就没。

三条路:
(a) 最便宜:换 webkit "Sin-ji" / "Mei-Jia" 香港/台湾女声更自然,调 rate=0.95 + pitch=1.05 软一点。
(b) 推荐:cockpit 加 /api/tts?text=xxx,后端调 Azure Speech (你 voicetyper 项目 key 在 vault) 的 zh-CN-XiaoxiaoNeural 流式返回 mp3,前端 <audio> 播放。延迟 ~500ms 但音色质感天差地别。
(c) 本地:cockpit 调本地 Qwen3-TTS(你 HF cache 已有 4.3GB CustomVoice),完全离线 + 可自定义音色。

#4MED ux summary / verdict 行文僵硬
worker.py:371-401 build_haiku_prompt · 你提的

症状:看真实输出
summary: HERMES 改完代码 4 处,现在写测试,等 CC 跑完后连测
verdict: 正常推进。CC 任务运行中(10/90 迭代),代码改动已完成...
"HERMES"/"正常推进"/"已完成"这种公文腔重复出现。

根因:prompt 没人格化,要求"≤30 字 一句话现在干嘛"诱导出口播体;haiku-4-5 在没人格 prompt 下默认这种 PR-merge-message 风格。

prompt 加人格 + few-shot 示例:
"你是爸爸的副驾驶,跟他聊天一样汇报。用 30 字内、口语、像同事说话——不要写 '正常推进/已完成/HERMES 在...' 这种官腔。
示例:
✓ '改完代码了,现在跑 typecheck 等结果'
✓ '卡在 NSG 规则,要不要换思路?'
✗ '正常推进。代码改动已完成'"
预期效果:summary 像同事口语而不是 release notes。

#6MED perf DM 根 chat 也进了 DB
worker.py:168 db_upsert 用 chat_id 兜底 · 我扫的

症状:DB 里有一条 thread_id=oc_ffa...(DM 根)的 active 记录,summary 写"已收到 chat_id 等待后续指示"。cockpit 已 HIDDEN_THREAD_IDS 屏蔽不显示,但 worker 仍每 5s tick 烧 haiku。

根因:db_upsert 把没 thread_id 的事件兜底到 chat_id(DM 单条消息)。SKIP_THREAD_IDS 在 tick_one_thread 里有判断,但 _select_active_rows 没过滤 → tick 头部才 return,已经烧了 SQL。

_select_active_rows SQL 加 WHERE thread_id NOT IN (?, ?),或 db_upsert 直接拒绝 oc_ 开头的写入。

#7LOW ux verdict 重复 summary
haiku prompt 没强制约束 · 我扫的

症状:看 "drone-picker":summary "调研剪映是否有 MCP 接口,已给出结论",verdict "任务已回复完成,给出了技术方案对比" — 信息 80% 重叠。

根因:prompt 说"不要重复 summary"但太软。haiku 偷懒就把 summary 换种说法塞 verdict。

prompt 改:verdict 改名 "judgment",强制写"该不该担心 / 卡点 / 下一步建议"中**至少一个**。没担心写空字符串,前端就不渲染那行。

#8LOW perf 1 天+ 没动还在 tick
worker.py:_classify_status · 我扫的

症状:状态机里没有"archive"概念。stalled 30s 一次也是花费,老 thread 永远占着 active/stalled 槽。

加第 5 个状态 archived,stalled > 24h 自动归档。归档的 thread cockpit 也不显示。

#10LOW ux modal 时间线方向
static/index.html:1004 · 我猜的

症状:详情 modal 的"时间线"是 ASC(旧→新,需要往下滚才看到最新)。你提到"时间不是倒序",**不确定指的是哪个 — 卡片 milestone(已答 #1)还是 modal 详情?

如果你想要 DESC(最新在顶),改 line 1004 为 (b.ts||0) - (a.ts||0)。等你确认。

06

UX 优化清单(非 bug,但值得做)

NICE 自动滚动到 active 卡

打开 cockpit 时默认页面顶部是 stalled,需要往下滚才看到 active。打开时滚动到第一张 active 卡。

NICE 卡片 swipe 操作(左右)

左滑 = 标记 done(强制收尾);右滑 = 暂停 ticking(短期 mute)。比点叉按钮快。

NICE 卡片头/底相同信息冗余

卡顶 BADGE "RUNNING" + 右上"已 2h6m" + 卡底"每 5 秒自动刷新" 三处都告诉你它在跑。可以折掉卡底那行(既然 BADGE 已经说了)。

NICE TTS 应该读"差异"不是"全量"

当前 TTS 读"摄影公司项目 更新。完成首页合作伙伴上移..."。其实你只关心"发生了什么新事"。改成读最近一条 milestone (新事件),比读 summary 全量更短更有信号。

NICE 完成通知应该可关闭

飞书 DM 根 "✅ 完成 + 链接" 多任务并发时刷屏。加个"今天 N 条完成"批量推送 + 个别可 mute。

NICE milestone 节点的"who 做的"标记

当前只显示"event 描述",看不出是 USER 说的还是 HERMES 做的。加 1px 左侧条 / icon 区分(橙=爸爸说话 / 灰=Hermes 动作)。

NICE fanout.jsonl 不轮转

已 836KB / 1696 行。半年后撑大→ tail 性能可能掉。加个 logrotate 配置或每周自截留近 7 天。

07

建议修复顺序(你拍板)

批次包含工作量价值
批 1
立刻能见效
#1 timeline sort + #2 当前节点呼吸灯 + #6 SKIP SQL 过滤 15 min 3 个改动都是 5-15 行代码,立刻见
批 2
haiku 表达改造
#4 prompt 加人格 + few-shot + #7 verdict 强制写差异化 30 min
(改 worker.py 重启 daemon, 等 5min 看几个 thread 出新 summary)
summary/verdict 从公文转口语,朗读出来也舒服
批 3
TTS 升级
#3 选一条路:先 (a) 调系统 voice 参数,不满意再上 (b) Azure Speech 后端 (a) 5min · (b) 2h 朗读体验质变
批 4
长任务记忆
#5 + #9 加 milestones_archive 列,长任务里程碑分布化 1-2h
schema 改 + worker.py prompt 改
"+13h19m × 4" 这种现象消失
批 5
清扫
#8 archived + fanout 日志轮转 30 min 系统长期健康

我自己推荐先做批 1+2——20 分钟内能看到:"时间不会倒序了 + 当前节点有呼吸灯 + 文字像同事说话了"。批 3 你看完批 2 再决定 TTS 走 (a) 还是 (b)。

批 4 是最大的工程量也最有架构价值("长任务记忆"是个通用机制,以后任何 LLM-tick-loop 都用得上,可以沉淀进现成的 llm-tick-loop-pitfalls skill)。但不急。

end · 2026-06-10 · ~/Desktop/wingman-analysis-2026-06-10.html