方案汇报 · YOUT-148

OutBrief:AI Agent 干完活,像微信视频一样“打给你”汇报

把 Agent 冗长的文字结果,转成真人助理口吻的语音 + 卡片来电汇报;你随时用说话或打字打断、拍板;挂断时自动生成下一步提示词,一键回到原会话继续执行。目标:把“读报告—想决策”的脑力瓶颈从你身上拿走,让你能并行推进多个项目。

日期:2026-09-25仓库:outbrief-server · outbrief-app · outbrief-hook范围:产品形态 · 架构 · 技术选型 · 首期需求 · 项目骨架

结论速览

四个讨论点,我的建议先放前面,理由在后面各章。

① 产品形态

一个跨端本地 APP(Win / macOS / Linux / iOS / Android)+ 一个自托管的轻服务端 + 一个 Agent hook 脚本。APP 常驻托盘/后台,Agent 完成任务即“来电”;通话中来的新汇报自动排队,挂断后下一个响铃。

② 架构方向

“Hook 接入 → 服务端精炼成结构化简报 → 推送来电 → 客户端播报/打断 → 生成提示词 → 回原会话”。服务端只做无状态的单轮 LLM 调用(简报生成、问答、提示词);TTS / STT 与对话状态都在客户端,免费语音通道的限流按用户分摊,服务端只兜底;hook 带回 sessionId + cwd,是后续闭环的关键。

③ 技术选型

客户端 Tauri 2 + React 19 + TS(一套代码五端);服务端 Node 22 + Hono + zod,存储 MySQL 8;推送 SSE + 轮询兜底;LLM 调用用 Vercel AI SDK(generateObject / streamText)接现有 CLIProxy,不引入 Agent 框架;配音 Azure 路径 B 在客户端直连(复用 youtube-dubbing-extension 实现,已在纯网页里实测通过);语音识别首期同样走 Azure 路径 B(客户端直连,与 TTS 共用 token),本地 SenseVoice 作为二期提速选项。

④ 首期需求

桌面端先跑通完整闭环:Claude Code / Codex 接入 → 简报生成(口播稿 + 卡片 + 决策点)→ 来电/排队 → 客户端 Azure 语音逐句播报 → 点“打字”或按住说话即暂停、问答后续播 → 挂断生成提示词 + “回原会话”命令一键复制。移动端、自动续跑、多用户放二期。

1产品形态

核心不是“把报告念出来”,而是“替你把报告消化成可决策的信息”。

📞 来电,而不是通知

完成事件以全屏来电/置前窗口出现,显示 Agent、项目、一句话结论和“需要你决策 N 项”。可接听、拒绝(稍后在列表回看)、静音排队。

🗣️ 真人助理口吻

先结论后细节:“老板,登录重构做完了,测试全过;有一件事要你拍板……”。原文事实被抽成事实清单,口播稿每段标注覆盖了哪些事实,服务端校验关键事实(风险、决策、数字、未完成项)零遗漏。

🖼️ 卡片配图

每段口播对应一张卡片:总结 / 改动清单 / 风险 / 决策点(带选项与推荐)/ 下一步。卡片随语音翻页,手动翻页也会跳到对应语音段。

✋ 随时打断

按住说话或直接打字即暂停播报;助理基于原始报告 + 已播内容回答或记下你的意见,然后问“要我继续吗?”,从断点继续。决策点可直接点选项回答。

📋 挂断即出提示词

通话结束,把“原报告 + 你的所有意见/决策”合成一段可直接粘贴给 Agent 的提示词,同时给出回到原会话的命令(如 claude --resume <id>),可复制、可编辑。

🧵 排队与回看

通话中来的新事件自动进队列,角标显示“还有 N 个汇报在排队”;挂断后自动响下一个。所有汇报、对话记录、提示词可在历史里回看重播。

闭环方案:怎么“回到之前的 session”?

你提到的难点:对话完自动执行只能开新 CLI,回不到原会话;另一个想法是干脆做一个类 Multica 的完整工具。

好消息是主流 Agent CLI 都支持按会话 ID 恢复并追加指令,而 hook 恰好能拿到这个 ID,所以不需要先重造一个 Multica:

Agenthook 给到的标识交互式回到原会话无人值守续跑(二期)
Claude CodeStop hook stdin:session_id、cwd、last_assistant_messagecd <cwd> && claude --resume <session_id>claude -p --resume <session_id> "<prompt>"
Codexnotify 参数 JSON:thread-id、cwd、last-assistant-messagecodex resume <thread-id>codex exec resume <thread-id> "<prompt>"
其他 / 自研POST /v1/events(generic)展示提示词,人工粘贴回调 URL(由接入方实现)

分三级演进,由浅入深

首期

L1 复制即用

挂断页给出:① 提示词 ② “回原会话”命令(已拼好 cwd 与会话 ID)。你粘贴即继续,零风险,所有 Agent 通用。

二期

L2 一键续跑

桌面端增加本地 Runner:点“让它继续”,在原 cwd 执行 claude -p --resume / codex exec resume。续跑结束又触发 hook → 再次来电,形成真正闭环。需要白名单目录与确认弹窗。

三期(可选)

L3 任务编排

当你需要同时管多个项目的队列、看板、多 Agent 分工时,再在 Runner 之上加任务层——即你说的“类 Multica”。先有 L1/L2 验证价值,再决定是否值得做,或直接对接 Multica。

建议:首期做 L1,架构上为 L2 预留(事件已存 source/sessionId/cwd,骨架里已实现)。不建议一上来做完整编排工具——工作量大一个数量级,而核心痛点(读报告、做决策)在 L1 就已被解决。

2系统架构

三个部件:Agent 侧 hook、自托管服务端、跨端客户端。实线 = 首期,虚线 = 二期。

开发机 · Agent 侧 OutBrief Server(Node 22 + Hono,自托管) 客户端(Tauri 2 + React) Claude Code · Stop hook Codex · notify Gemini CLI / 其他 outbrief-hook归一化 · 失败恒 exit 0 Local Runner(二期)resume 原会话续跑 Ingest APIPOST /v1/events · Bearer Event StoreMySQL 8 · seq 自增 Brief Engine事实抽取 → 口播段 + 卡片单次结构化调用 · 覆盖校验 Voice Fallback(兜底)客户端合成失败时代合成官方 Speech Key · 限额 LLM Gateway(无状态)问答 / 提示词 · 单轮流式鉴权 · 限额 · 提示词模板 DeliverySSE /v1/stream · 断线续传轮询兜底 · APNs/FCM(二期) Auth(设备 token)· 配置(音色/语速/模型)· 历史与回放 LLM(CLIProxy)OpenAI 兼容 · 可切模型gemini-3.6-flash 等 Azure Speech路径 B 免费签名 JWT可切官方 Key 桌面端(首期)Windows · macOS · Linux托盘常驻 · 来电置前来电队列 · 播放器 · 麦克风 移动端(二期)iOS · Android(同一代码)CallKit / 全屏来电通知推送唤醒 客户端核心(本地运行)来电队列 · 通话/打断状态机Voice:Azure TTS · Azure STT对话上下文 → LLM Gateway HTTPS 事件 SSE 推送 客户端直连 “让它继续” → Runner → resume → 再次触发 hook(二期闭环)

服务端 / 客户端怎么分工

  • 服务端:简报生成(事件到达时你可能不在线)、LLM 凭据与限额;不保存会话、不跑循环
  • 客户端:TTS / STT、对话上下文、打断与续播——免费语音通道的限流按用户 IP 分摊
  • 服务端每次调用都是无状态单轮;Node 异步 I/O 下等模型返回不占线程

为什么 SSE + 轮询兜底

  • 你提的“轮询”完全可用,但 SSE 秒级到达且更省
  • 事件带单调 seq,断线用 Last-Event-ID 续传,不丢不重
  • 后台/休眠的手机靠 APNs/FCM 唤醒(二期)

为什么先单用户自托管

  • 报告含代码与业务信息,默认私有部署
  • MySQL 8:本机 docker 已有实例,与现有项目运维方式一致;表结构走版本化迁移(db/migrations),访问集中在 EventStore
  • 多租户/账号体系放到有外部用户时再做

核心流程

一次汇报从 Agent 结束到你拿到提示词的完整路径。

  1. Agent 完成:Claude Code Stop / Codex notify 触发 outbrief-hook,带上最终报告、sessionId、cwd。
  2. 入库:POST /v1/events → 分配 seq,状态 received;异步进入简报生成。
  3. 精炼:Brief Engine 先抽事实清单(数字/风险/决策/未完成项标为关键),再写口播段 + 卡片,校验“关键事实全部被覆盖”,未覆盖则重写一次或以“补充说明”卡片兜底。整个过程是一次结构化 LLM 调用 + 代码校验,不是 Agent。
  4. 配音:客户端收到简报后直连 Azure 按句合成并本地缓存;首句就绪即响铃,后续边播边合成;客户端失败才请求服务端兜底。
  5. 来电:SSE 推送到所有在线客户端;空闲则响铃,通话中则排队。
  6. 播报 + 打断:逐句播放、卡片同步;点“打字”或按住说话立即暂停,Azure STT 识别语音后把问题连同上下文发给 LLM Gateway,回答播完后“继续”从当前句开头接着讲。
  7. 挂断:生成“下一步提示词”+ 回原会话命令;状态写回 completed,队列里的下一个响铃。

简报结构(服务端产出,客户端渲染)

{
  "title": "登录模块重构汇报",
  "verdict": "partial",          // done | partial | blocked
  "facts": [{ "id": "f-redis", "text": "Session 迁到 Redis,需新增 REDIS_URL",
              "importance": "critical" }],
  "segments": [{
    "id": "seg-1",
    "speech": "老板,登录重构做完了,42 个测试全过。有两件事要你拍板……",
    "card": { "kind": "summary", "title": "总体进展",
              "bullets": ["JWT 校验换成 jose", "42/42 测试通过"] },
    "coveredFactIds": ["f-jwt", "f-tests"]
  }],
  "decisions": [{ "id": "d-legacy", "question": "旧接口立即下线还是保留一个版本?",
                  "options": ["保留到 v2.3", "立即下线"], "recommendation": "保留到 v2.3" }]
}

已用真实报告在 CLIProxy 上跑通该结构(gemini-3.6-flash-high 约 5.6 s,事实全覆盖、未覆盖列表为空),口播稿长度约为原文 1/3~3/5。

UI 交互设计

视觉语言对齐微信视频来电:深色全屏、绿色接听、红色挂断;通话中是“语音为主、卡片为辅”。

C
Claude Code
outbrief · 邀请你语音通话
一句话
  • 登录重构完成,42 项测试通过;2 项待你决策
✕
拒绝
✆
接听
① 来电结论 + 待决策数先亮出来,决定接不接
排队 2
Claude Code · outbrief · 01:12
改动与风险 2/4
  • Session 迁至 Redis
  • 需新增 REDIS_URL
  • 生产 Redis 尚未开通
“Session 已经迁到 Redis 了,不过生产环境的 Redis 还没开……”
⏸
暂停
⌨
打字
🎙
按住说话
✕
挂断
② 通话中语音逐段播,卡片同步翻;右上角是排队数
已暂停 · 你在打断
决策 1/2
  • 旧接口立即下线还是保留一个版本?
保留到 v2.3(推荐)立即下线
Redis 让运维用 Terraform 开,你先把配置写进文档
好的,记下了:生产 Redis 由运维 Terraform 开通,我在下一步里补文档。要我继续讲吗?
继续 / 输入你的意见…
🎙
③ 打断说话或打字都会暂停;决策点可直接点选
通话结束 · 04:36
下一步提示词
继续登录模块重构收尾:
1. 旧接口保留到 v2.3,加 Deprecation 头;
2. 在 docs/deploy.md 补 REDIS_URL 配置说明,生产 Redis 由运维 Terraform 开通;
3. 完成后跑全量测试并汇报。
cd ~/work/outbrief && claude --resume 9b08d75a…
编辑复制提示词
复制命令让它继续(二期)
④ 挂断提示词 + 回原会话命令,一键复制

桌面端:托盘常驻 + 来电浮窗 + 队列

OutBrief
通话中 / 排队
Claude Code · outbrief通话中 · 2/4
Codex · api排队 #1 · 2 分钟前
Gemini CLI · web排队 #2 · 刚刚
历史
Codex · data-sync已完成 · 提示词已复制
改动与风险 2/4
  • Session 存储迁到 Redis,新增 REDIS_URL
  • API 文档新增 3 个接口说明
  • 生产 Redis 尚未开通 —— 需要决策
“Session 已经迁到 Redis 了,不过生产环境的 Redis 还没开……”
空格 = 按住说话 · 直接输入 = 打字打断 · Esc = 挂断
🎙
X
Codex · api
邀请你语音通话 · 任务汇报
✕
✆

空闲时来电:窗口置前 + 系统级“请求注意”(Dock 跳动 / 任务栏闪烁);移动端二期用 CallKit / 全屏通知。

3技术选型

原则:一套 TS 类型贯穿三端;能复用现有资产就复用;首期零运维。

层选择理由备选 / 放弃原因
客户端框架Tauri 2 + React 19 + TypeScript + Vite一套代码覆盖 Win/macOS/Linux/iOS/Android;包体小(~10 MB 级);原生能力用 Rust 或 Swift/Kotlin 插件补Electron 无移动端;Flutter 与现有 TS 资产割裂;RN 桌面(尤其 Linux)弱
客户端状态纯函数 callQueue 状态机 + React state来电/排队/挂断逻辑可单测,与 UI 解耦,移动端直接复用首期不引入 Redux/Zustand
音频WebView HTMLAudioElement / Web Audio;MediaRecorder 录音按句 MP3 顺序播放、随时暂停;录音转 16 kHz WAV 交给 Azure STT移动端后台播放二期用原生插件
服务端Node 22 + Hono + zod协议以服务端 src/protocol.ts(zod)为准,客户端 / hook 各自镜像类型;Hono 自带 SSE;Node 22 原生跑 TS,无构建Spring Boot 同现有 proxy 栈,但无法共享类型、无法直接复用 TS 版 TTS
存储MySQL 8(mysql2)+ 版本化 SQL 迁移本机 docker 已有 MySQL,与现有项目一致;启动时自动迁移;访问集中在 EventStoreSQLite:部署到服务器后要单独管文件与备份;Redis 首期不需要
下发SSE(fetch 流,带 Authorization)+ Last-Event-ID + 轮询兜底秒级到达、服务端实现简单、天然续传WebSocket 双向能力首期用不上;APNs/FCM 二期
LLM现有 CLIProxy(OpenAI 兼容),默认 gemini-3.6-flash-high,可配置已有账号与额度;JSON 结构化输出实测可用;模型可随时切直连各家 SDK:多一套 key 管理
LLM 调用Vercel AI SDK:ai + @ai-sdk/openai-compatible;简报 generateObject(zod schema),问答 / 提示词 streamText三类调用都是单轮,不需要 Agent 框架;schema 放服务端,客户端镜像类型;同一套 API 在 Node 和 WebView 都能跑LangChain / LangGraph / Mastra:解决的是多步工具循环,这里用不上,徒增抽象
TTSAzure 路径 B,客户端直连(同 youtube-dubbing-extension:前端做、后端兜底)零成本、音色丰富;限流按用户 IP 分摊;已在纯网页实测(CORS 放行 *)服务端官方 Speech Key 作兜底
STT首期 Azure 路径 B STT,客户端直连(与 TTS 共用签名 / token)免费、零安装、实现与 TTS 同一套;实测 11.3 s 音频往返 4.5 s本地 SenseVoice(0.18 s,但要带 240 MB 模型和 sherpa-onnx 原生依赖)放二期;Web Speech API 在 WebView2 / WebKitGTK 上不可用
Hook零依赖 Node 脚本 outbrief-hook装在任何开发机都能跑;超时短、失败 exit 0 永不阻塞 AgentRust 单文件二进制:二期做分发时再考虑
工程三个独立仓库(server / app / hook)· pnpm · Biome · Vitest · GitHub Actions每个仓库一条命令 lint/typecheck/test;server CI 带 MySQL 服务;app CI 覆盖三平台 Tauri 构建ESLint+Prettier:配置更重

语音链路(参考 youtube-dubbing-extension)

扩展里的 src/background/api/edge-tts/* 已是纯 TS + WebCrypto 实现,移植到客户端 outbrief-app/src/voice/(WebView 直接 fetch);服务端兜底改用官方 Speech Key 的标准 REST,不需要共享代码。

协议(路径 B:cognitiveservices/v1)

  1. HMAC-SHA256 签名(MSTranslatorAndroidApp)→ X-MT-Signature
  2. POST dev.microsofttranslator.com/apps/endpoint 换取 JWT + region(时钟偏差时用服务器时间重签)
  3. TTS:POST https://{region}.tts.speech.microsoft.com/cognitiveservices/v1,SSML(voice / prosody / lang),输出 audio-24khz-48kbitrate-mono-mp3
  4. STT:同一 JWT 调 {region}.stt.speech.microsoft.com/…/conversation/cognitiveservices/v1,16 kHz WAV
  5. 错误策略沿用扩展:400 不重试、401 刷新 token、429 立即降级、408/5xx 指数退避;并发 ≤ 3
  6. WebView 里必须加 referrerPolicy: "no-referrer":换 token 的接口带任何 Referer 都会 401(错误码 401001),浏览器默认会带;扩展的后台 Service Worker 不带所以没遇到

实测(spike)

场景结果
Node 22:签名换 JWT200,region eastasia,~0.2 s
Node 22:TTS(中文约 11 s 语音)200,audio/mpeg 68 KB,~2.6 s
纯网页(Chromium,无扩展、无代理):签名 → JWT → TTS → STT全部 200;TTS 5.6 s 语音耗时 1.9 s;STT 11.3 s 音频往返 4.5 s
CORS 预检(tauri://localhost、http://tauri.localhost)三个接口都返回 Access-Control-Allow-Origin: * 并放行所需请求头
本地 SenseVoice-Small int8(M4 Max CPU,2 线程)模型 240 MB,加载 0.43 s;11.3 s 音频识别 0.18 s(RTF 0.016)

含义:TTS 放客户端没有技术障碍,WebView 里直接 fetch 即可,不需要 Rust 转发;STT 同理,客户端直连即可。

风险提示:路径 B 是非官方通道,可能被限流或封禁。客户端直连把限流分摊到每个用户;服务端配置 AZURE_SPEECH_KEY 后作为兜底走官方 Speech 服务(同一 SSML、同一端点形态),上层无感。

设计细化(讨论补充)

针对五个追问:要不要 Agent、TTS 放哪、Agent 循环放哪、怎么打断、语音识别用什么。

① 把汇报转成图文简报,要不要 Agent?——不要,一次结构化调用就够

结论

这是一个输入固定、输出结构固定的变换,没有“先看结果再决定下一步调什么工具”的环节,所以不需要 Agent,直接调大模型 API:

  1. 一次 generateObject:输入原始报告,输出 zod 定义的简报(事实清单 / 口播段 / 卡片 / 决策点)
  2. 代码校验:zod 结构 + “critical 事实全部被某段覆盖”
  3. 不合格才补一次调用(把缺失事实回传让它改);仍不合格就追加“补充说明”卡片

实测一次调用约 5.6 s,事实全覆盖。简报在服务端生成:事件到达时你可能不在线,接起电话时应该已经准备好。

技术栈

用途选择
模型调用ai(Vercel AI SDK Core)+ @ai-sdk/openai-compatible 指向 CLIProxy
结构化输出generateObject({ schema }),schema 放服务端 src/protocol.ts,客户端镜像类型
问答 / 提示词streamText,流式返回,首句到了就先合成先播
提示词管理模板文件 + 版本号,放服务端;用固定样本做回归评测

不用 LangChain / LangGraph / Mastra:它们解决多步工具循环、状态持久化,这里没有这些需求。

② TTS 能不能放客户端?——能,已验证,采用“前端做、后端兜底”

验证结果

  • 在普通网页(非扩展)里用 WebCrypto 签名 → 换 JWT → TTS → STT,全部 200,MP3 能直接播放
  • 三个接口的 CORS 对 Tauri 的两种 origin 都返回 Access-Control-Allow-Origin: *,WebView 直接 fetch 即可,不需要 Rust 转发
  • 唯一的坑:换 token 的接口只要带 Referer 就 401,浏览器默认会带 → 请求加 referrerPolicy: "no-referrer"

落地方式

  • 把扩展的 edge-tts/* 移植到 outbrief-app/src/voice/(签名、token 缓存、SSML、重试、合成、识别)
  • 客户端:按句合成,按“文本 + 音色 + 语速”哈希缓存在本地,重听不再请求
  • 服务端:POST /v1/tts 只在客户端连续失败时调用,用官方 AZURE_SPEECH_KEY,按用户限额
  • 效果:免费通道的限流落在每个用户自己的 IP 上,服务器不成瓶颈

③ 多轮交互时 Agent 放哪?——循环放客户端,服务端只做无状态单轮

业界做法

  • Vercel AI SDK 客户端工具:服务端每次请求只跑一步 streamText;没有 execute 的工具调用下发到前端 onToolCall 执行,addToolOutput 写回结果,sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithToolCalls 自动发起下一轮——循环由前端驱动,服务端不保存状态
  • OpenAI Agents SDK(语音 Agent):RealtimeAgent / RealtimeSession 直接跑在浏览器里,服务端只签发临时 key(ek_…)
  • Claude Code / Codex / Cursor:Agent 循环都在用户本机,云端只是模型 API

OutBrief 的具体做法

交互谁发起服务端做什么
生成简报事件到达1 次 generateObject
打断问答(每轮)客户端1 次 streamText,客户端带上报告 + 简报 + 对话历史
挂断生成提示词客户端1 次 streamText
翻卡片 / 记录决策 / 续播客户端不参与(客户端工具)

服务端接口 POST /v1/llm/:task,task 只能是 qa / next-prompt,系统提示词由服务端按 task 选,客户端不能自带,防止网关被当成通用代理。

关于线程:Node 是异步 I/O,等模型返回时不占线程,一个进程可以同时挂数千个流式请求;真正的成本是模型 token 和每个会话的内存。“服务端只做单轮 + 不存会话”把后者也去掉了,扩容就是加无状态实例。

④ 怎么打断?——按你说的:点“打字”或按住说话,立即暂停

规则

  • 触发暂停:按住说话(按钮或空格)、点“打字”、在输入框敲下第一个字、点决策选项
  • 暂停时记住“第几段、第几句”;口播按句合成,所以续播从当前句开头重新讲,不会从半个词开始
  • 回答播完后等你:点“继续”、按回车、或说“继续”(识别结果就是这个词时直接续播,不调 LLM)
  • 回答过程中再次打断:取消还没返回的 LLM 请求和还没播的回答音频
  • 首期不做“你一开口就自动打断”:扬声器外放时麦克风会收进自己的声音,需要回声消除 + VAD,放二期

状态机

playing   ─按住说话/打字→ paused
paused    ─松开/发送────→ answering
answering ─再次打断─────→ paused
answering ─回答播完─────→ waiting
waiting   ─继续─────────→ playing
任意状态  ─挂断─────────→ summarizing

answering:Azure STT → LLM 流式 → 按句 TTS;再次打断会 abort 进行中的请求;playing 从当前句开头续播;summarizing 生成下一步提示词。

⑤ 语音识别用什么?——首期 Azure 免费通道,本地 SenseVoice 放二期

方案费用实测 / 资料结论
SenseVoice-Small int8(本地,sherpa-onnx)免费开源M4 Max CPU 2 线程:11.3 s 音频 0.18 s 识别完,模型 240 MB,加载 0.43 s;官方资料称中文准确率优于 Whisper-Large、速度快约 15 倍二期sherpa-onnx 有官方 Rust crate,可放进 Tauri command;打断延迟成为痛点时再上
Azure 路径 B STT(客户端直连)免费(非官方)网页里实测 200,11.3 s 音频往返 4.5 s;单次音频上限 60 s首期与 TTS 共用 token,零安装;超过 60 s 的语音按段切分
Whisper large-v3-turbo(whisper.cpp)免费开源自回归解码,比 SenseVoice 慢;中文不如 SenseVoice,英文更好以英文为主的用户可以作为可选项
系统语音识别(iOS Speech / Android SpeechRecognizer)免费移动端原生,可离线二期移动端备选
Web Speech API—WebView2(Windows)、WebKitGTK(Linux)不支持不用

两种方案在同一段测试音频上都把“旧接口”识别成了“就接口”(TTS 读音本身有歧义)。所以打断问答时会把识别出的文字显示给你,可以改一下再发。录音用 AudioWorklet 直接采 16 kHz 单声道 PCM,二期换本地模型时录音链路不用改。

4首期需求(Phase 1 · 桌面端闭环 MVP)

目标:你在一台 Mac/Windows/Linux 上,同时跑 3 个 Agent 项目,全部靠“接电话”处理完汇报,并通过复制提示词继续推进。

#需求验收标准骨架状态
P1-1Agent 接入:Claude Code Stop hook、Codex notify、generic HTTP三种来源事件入库,保留 sessionId/cwd;服务端不可达时 Agent 不受影响已完成
P1-2事件下发与排队:SSE + 续传 + 轮询兜底;通话中新事件排队,挂断后自动响下一个断网重连不丢不重;3 个并发事件依次响铃已完成
P1-3来电 UI:来电 / 通话中 / 挂断三态,窗口置前 + 请求注意,接听/拒绝回写状态桌面三平台可构建;拒绝的事件进入历史可重听来电/排队已完成,历史待做
P1-4Brief Engine(服务端):AI SDK generateObject 单次调用产出事实 + 口播段 + 卡片 + 决策点,代码校验关键事实覆盖zod 校验通过;critical 事实 100% 覆盖;口播稿 ≤ 原文 60%待做
P1-5Voice(客户端优先):outbrief-app/src/voice/ 移植路径 B(TTS + STT),客户端按句合成 + 本地缓存;服务端 /v1/tts 官方 Key 兜底首句音频 ≤ 3 s 可播;401/429/5xx 按策略处理;客户端失败自动走兜底待做
P1-6通话播放器:逐句播报、卡片同步翻页、暂停/继续(从当前句开头)、跳段翻卡片与语音段双向同步待做
P1-7打断问答:点“打字”/按住说话即暂停 → Azure 路径 B STT 识别(识别文字可改后再发)→ 客户端带上下文调无状态 LLM Gateway → 回答播完“继续”;决策点可点选回答不编造原文外事实;意见与决策全部记入会话待做
P1-8挂断总结:生成下一步提示词 + 回原会话命令,可编辑、一键复制提示词包含全部决策与意见;Claude Code / Codex 命令可直接运行待做
P1-9设置:服务器地址/token、音色、语速、LLM 模型;托盘常驻、开机自启设置持久化,重启后自动连接连接设置已完成

首期明确不做

移动端打包与推送唤醒、Local Runner 自动续跑、多用户/账号体系、流式 STT、历史全文检索。

路线图

Phase 1 · 约 3–4 周

桌面闭环 MVP

  • P1-1 ~ P1-9
  • 自托管部署文档
  • 你自己日常使用,校准口吻与卡片
Phase 2

移动端 + 一键续跑

  • iOS / Android 打包,CallKit / 全屏来电
  • APNs / FCM 推送唤醒
  • Local Runner:claude -p --resume / codex exec resume
  • 本地 SenseVoice STT,打断后识别从 4.5 s 降到 0.2 s
Phase 3

多项目编排(按需)

  • 项目维度看板、优先级、勿扰时段
  • 多用户、团队共享汇报
  • 视情况对接 Multica 或自建任务层

风险与对策

风险影响对策
精炼丢关键信息你基于错误信息做决策先抽事实再写稿;critical 事实强制覆盖校验;卡片“原文”入口随时可看原报告
Azure 路径 B 被限流/封禁没有声音客户端直连分摊限流;服务端官方 Key 兜底;都失败时降级为“卡片 + 文字”通话
移动端后台限制来电不响首期桌面优先;二期走 APNs/FCM + CallKit/全屏通知,不依赖长连接
报告含敏感代码/密钥数据外泄默认自托管;token 鉴权;LLM 走自有 CLIProxy;后续可加脱敏
打断延迟偏高对话不自然二期换本地 SenseVoice(实测 0.2 s);回复流式输出,首句先合成先播
路径 B 密钥随客户端分发被更多人滥用该密钥已公开(read-frog、youtube-dubbing-extension 同款),不增加暴露面;官方 Key 只在服务端
Azure STT 往返约 4.5 s打断后要等识别期间显示“正在听写”;二期接本地 SenseVoice,Azure 退为兜底

项目骨架(已上传 GitHub)

三个私有仓库,本地都在 ~/work/code/outbrief/ 下。骨架不是空壳:事件接入 → MySQL → 排队 → 来电 UI 已端到端跑通。

~/work/code/outbrief/
├── outbrief-server/   asasas234/outbrief-server · Hono + MySQL
│   ├── src/           /v1/events · /v1/stream(SSE) · 状态回写 · protocol.ts(zod,协议唯一来源)
│   ├── db/            bootstrap.sql(建库/账号)· migrations/(建表,启动时自动执行)
│   └── docs/          proposal.html 本方案 · adr/ 0001–0003
├── outbrief-app/      asasas234/outbrief-app · Tauri 2 + React 19
│   ├── src/           callQueue 状态机 · SSE 客户端 · 来电/通话 UI · protocol.ts(镜像)
│   │                  voice/(首期新增:Azure 路径 B TTS + STT)
│   └── src-tauri/     Rust 壳(窗口置前、请求注意)
└── outbrief-hook/     asasas234/outbrief-hook · 零依赖 Node 脚本
    └── src/           Claude Code Stop / Codex notify 归一化 · protocol.ts(镜像)

已验证

  • 三个仓库各自 pnpm lint / typecheck / test 全绿:server 8(真实 MySQL outbrief_test 库)· app 6 · hook 7
  • 本机 docker some-mysql 已建 outbrief / outbrief_test 库和 outbrief 账号,pnpm db:migrate 建好 agent_events 表
  • 真实启动服务 → 用 hook CLI 发 Claude Code / Codex 事件 → SSE 实时收到;无关事件类型被忽略;服务端宕机时 hook 仍 exit 0
  • 浏览器驱动 UI:来电 → 接听 → 通话中再来事件 → 显示“还有 1 个汇报在排队” → 挂断后自动响下一个;服务端状态正确回写
  • cargo build(Tauri 2.11,Rust stable)通过