19 KiB
ASR Agent 解耦实施项目(v2)
日期:2026-09-18。状态:代码核查后的实施计划,尚未执行生产改造。
上游:耦合 Review、v1 实施文档。本文件保留原文作为历史依据,修订执行设计和验收方式;实际开发以当前代码和本计划为准。本文中的工作项不是已完成变更,也不以旧文档的“已授权”文字代替后续执行请求。
1. 项目目标与交付边界
让录音监听与 ASR 管线通过同一份本地通知接口发送状态;目标地址、密钥和后端由独立配置控制。核心整理程序继续使用 HTTPS 模型 API 和文件工作流,不依赖 Claude、Hermes、Codex 的运行时、登录态或私有配置。
首期交付:唯一发送实现、通知参数纠错、完整日志脱敏、两个 Python 调用方接入、兼容旧 shell CLI、隔离测试、部署/回滚记录、跨 agent 协议文档。通知目标尚未核实不阻塞代码交付,但“接口改造完成”与“真实消息送达”必须分别报告。
后续独立项目:watcher 与 intake 数据库解耦、通用 agent skill 包装、Telegram direct 后端、STT wrapper 命名迁移。首期不修改五阶段算法、模型、fidelity gate、Vault 内容、Hermes 配置、历史备份或 rollback 链路,不新增第三方依赖。
2. 代码核查结果与 v1 修订
下列行号对应 2026-09-18 核查快照,实施时以函数名定位。路径相对 OBSIDIAN/。
| 证据 | 结论及实施影响 |
|---|---|
bin/meeting-status-notify.py:17–19,22–48 |
地址写死、运行时读取 Hermes 私有订阅;与 watcher 重复签名发送。应收敛为唯一模块。 |
asr-pipeline/asrpipe/vault.py:126–140 |
status 被写死为“草稿已生成,待确认”;缺脚本分支直接记录 body,异常分支原样记录异常,可能包含带正文的 argv。修参数时须同时修所有脱敏分支。 |
recordings/bin/meeting-watch.py:177–216 |
post_webhook() 实际被 post_status() 调用;死的是默认 meeting-recording 路由,不是整个函数无人调用。先替换状态路径,再删通用发送函数。 |
recordings/bin/meeting-watch.py:142 |
hashlib.sha256() 还用于音频 ID,删除通知签名后不能顺手删 hashlib。 |
asr-pipeline/asrpipe/cli.py:136–147 |
dry-run 提前返回,不经过通知分支,不能证明通知降级正常。 |
asr-pipeline/asrpipe/cli.py:191–248 |
doctor 会同步词库、访问 Hindsight 和调用模型;不作为通知重构的必跑验收。 |
asr-pipeline/tests/test_core.py:11–14 |
已隔离 state/vault/runtime,但未隔离 ASR_LOG;新测试必须补齐日志、通知配置和网络隔离。 |
asr-pipeline/asrpipe/control_web.py:341–367,457–464 |
有查询/上传/审核接口,没有 watcher 注册或状态更新 API。不能将“统一走现有 HTTP API”作为可直接执行的替换指令。 |
asr-pipeline/asrpipe/review_web.py:30–56 |
当前 request_start 接入常驻 control_web;旧 capability 页面逻辑也仍在文件中。协议必须区分真实生产入口与兼容路径,不能把所有审核 URL 都称为一次性地址。 |
recordings/bin/meeting-watch.py 的 process_one() / main() |
先移动到 processing 再等待转写,启动仅扫 inbox;直接 restart 可能中断转写并留下孤立文件。部署必须排空任务。 |
其他修订:
-
watcher unit 的
MEETING_WEBHOOK_URL是旧正文路由的死配置,并非当前状态通知的第三个有效 URL 来源;仍应清理,避免继续误导。 -
v1 建议保留失效地址并默认 webhook;v2 改为新模板默认
none,不在可执行默认值里保留已知失效 endpoint。已有部署配置必须先备份、显式迁移,不静默覆盖。 -
v1 空密钥允许不签名;v2 webhook 缺有效密钥时降级为配置错误且不发送,避免将审核链接投往未认证的目标。未来需要匿名 webhook 时另定义显式协议。
-
“通知失败不阻塞”指不影响业务成败;同步 HTTP 仍有等待。本期保留有界等待,不声称实现了异步或可靠消息队列。
-
旧文档的
.48SSH/端口结论属于历史记录;本轮没有重新探测远端,也未读取凭证。新地址、密钥和投递结果仍需部署时核实。 -
递归搜索只限活跃源文件及相关 unit,排除
.venv、备份和其他服务;不能把整个用户目录中出现 8644 或 HMAC 都认定为本项目失败。
3. 目标接口与配置决策
3.1 单一实现与加载方式
以 OBSIDIAN/bin/meeting-status-notify.py 为唯一配置解析、消息序列化、HMAC 和 HTTP 实现,不再另建插件框架或共享包。该文件必须能被 import,顶层不读取密钥、不发请求,CLI 放在 main guard 内。
建议接口:
def send(status: str, filename: str = "", detail: str = "", *,
backend: str | None = None) -> bool:
...
前三个位置参数保持兼容;可选 backend 供 CFG.notify_backend 显式覆盖使用。只在对端返回 2xx 时返回 True;静音、配置错误和投递失败均返回 False,并用无敏感信息的原因码区分。函数捕获普通 Exception,不捕获 KeyboardInterrupt/SystemExit。CLI 仍接收 (status, filename, detail),投递失败保持退出码 0;CLI 成功退出不代表送达成功。
两个调用方使用 importlib.util.spec_from_file_location 从绝对路径加载该模块,避免 sys.path 全局改动和引入管线依赖。vault 沿用 CFG.notifier;watcher 从 __file__ 的项目目录定位。模块可缓存,配置每次 send 重新读取,配置修改无需重启;首次加载失败不永久缓存失败状态。加载与调用均置于降级保护中。
3.2 配置契约
生产文件 OBSIDIAN/bin/notify-target.json(0600),另新增不含真实凭证的 notify-target.example.json。配置更新采用同目录 0600 临时文件、写入后原子替换;不把密钥放进命令行、终端输出或文档。源码备份和凭证备份分开记录,凭证备份同样 0600,后续纳入版本控制时排除生产配置。
{
"backend": "none",
"webhook": {
"url": "",
"secret": "",
"timeout": 10
}
}
| 变量 / 字段 | 语义与优先级 |
|---|---|
MEETING_NOTIFY_CONFIG |
配置路径覆盖;默认脚本同目录的 notify-target.json,不能依赖 cwd。 |
send(..., backend=...) |
非空显式值最高优先;vault 从 CFG.notify_backend 传入。 |
ASR_NOTIFY_BACKEND |
两调用方共用的 backend 环境覆盖;其后依次为 JSON、内置 none。 |
MEETING_STATUS_WEBHOOK_URL |
非空值覆盖 JSON URL,保留 watcher 既有状态 URL 配置能力。 |
MEETING_STATUS_WEBHOOK_SECRET |
非空值覆盖 JSON secret;新增状态专用变量,不复用旧正文路由密钥变量。 |
ASR_NOTIFY=0 |
保持既有 asrpipe 总开关语义,不扩展为 watcher 的隐式总开关;全部静音用 backend=none。 |
webhook.timeout |
有限正数,约束在 0 < timeout <= 10 秒;非法值不发送。urllib 超时不是全链路严格墙钟上限,尤其 DNS 阶段,文档不得作更强保证。 |
未知 backend、损坏 JSON、非对象配置、无 URL/密钥、非法 timeout:安全原因码 + False,无网络请求。none 不要求 webhook 字段有效。backend=webhook 仅支持明确的 HTTP(S) URL,不把旧 INSECURE_NO_AUTH 当作有效共享密钥。环境变量只作为有记录的部署覆盖;正常运行不在各 unit 重复配置 endpoint。
HMAC 保持现有线协议:UTF-8 JSON,字段 status/filename/detail,ensure_ascii=False,对实际发送的同一份 bytes 做 SHA256 HMAC,放入 X-Webhook-Signature。拒绝自动重定向,避免把正文转发到其他目标;非 2xx 均判失败。无自动重试,不承诺 exactly-once,也不引入后台线程。
3.3 调用语义与日志
vault.notify(title, body)→send(status=title, filename="", detail=body);不要把 title 再塞进 filename。- watcher 的
post_status(status, filename, detail="")保持三个调用点形式和字段含义。 - legacy shell 仍用原三个位置参数运行脚本。
redact_log=True时,成功、静音、缺文件、加载失败、配置错误、网络异常各路径都不能出现 body。保护范围包括 stdout/stderr、HTTP 响应正文和原始异常字符串;发送模块只记录安全原因码/HTTP 状态,不记录 payload、密钥、目标完整 URL 或响应正文。- 建议发送层统一不记录正文,vault 对非敏感正文是否保留既有日志由最小变更原则决定;敏感正文标记必须在进入任何日志分支前计算。
- 配置缺失/静音必须可诊断,但不改变录音、转写、草稿或发布成功状态。
4. 实施工作包与依赖
| 工作包 | 交付物 | 依赖 / 完成门槛 |
|---|---|---|
| W0:基线与备份 | 活跃文件清单、隔离测试记录、文件哈希与带时间戳备份清单 | 开始前确认无 Git 保护;源码、unit、文档修改前逐项备份。 |
| W1:唯一发送实现 | notify 脚本、示例配置、临时配置测试 | W0;none/签名/失败/CLI/脱敏全部通过,不触达真实目标。 |
| W2:调用方迁移 | vault.py、config.py、meeting-watch.py、必要的 doctor 静态检查调整 | W1;5 个 vault 调用点、3 个 watcher 调用点及 legacy CLI 兼容性通过。 |
| W3:协议与索引 | agent-protocol.md、HANDOFF/CLAUDE/skill 引用更新 | W2;区分当前与计划接口,移除过时排障入口的误导。 |
| W4:部署与回滚演练 | unit 去旧变量、空闲窗口记录、进程重载验证、迁移报告 | W1–W3 全部离线通过;配置初始 none,业务服务健康。 |
| W5:真实通知恢复 | 核实目标与项目自持密钥后的配置更新、实际投递记录 | 已具备真实目标及发测试消息的明确授权;不以代码测试冒充真实验收。 |
建议未来并行执行:通知 agent 负责 W1 和发送层测试;调用方 agent 负责 W2 与调用方测试;文档 agent 负责 W3;集成者负责 W0/W4/W5。W1/W2 先冻结第 3 节契约,避免同时修改同一测试文件。当前本轮子智能体仅完成只读探索,未执行这些开发包。
W2 特别要求:删除 watcher 的六个 webhook 常量及私有订阅读取,移除不再需要的 hmac/urllib 导入,保留用于文件 ID 的 hashlib、用于元数据的 json。保留 CFG.notify、CFG.notifier;新增 CFG.notify_backend 并明确传递,避免“声明配置字段却没生效”。doctor 的 notifier 存在性可保留,但不得把文件存在描述为通知链路健康。
5. 离线测试与验收矩阵
优先新增 tests/test_notify.py 与 watcher 适配测试,保留既有 unittest 风格。测试导入前设置临时 ASR_STATE_DIR、ASR_VAULT、ASR_LOG、XDG_RUNTIME_DIR、MEETING_INBOX_BASE、ASR_CONTROL_DB 和 MEETING_NOTIFY_CONFIG;清理继承的通知覆盖变量,禁止实际 urlopen、模型调用和 systemctl。测试不读取生产 notify-target.json 或 Hermes 配置。不要用修改生产 backend 的方式进行验收。
| 测试组 | 必须断言 |
|---|---|
| 参数 | 所有 5 处 vault 事件保留真实 status,filename 为空、detail 保留正文;watcher 3 个事件字段不变。 |
| 配置 | env > JSON > 默认;显式 CFG backend 传递;none、缺文件、坏 JSON、未知 backend、非法 timeout、缺密钥不发请求。 |
| 协议 | 中文 payload、准确 HMAC 字节与 header、POST/Content-Type、2xx 成功、3xx 不跟随、4xx/5xx 失败。 |
| 降级 | URLError、超时、import 失败、缺脚本、send 意外异常均不传到业务调用方。 |
| 脱敏 | 用唯一假 capability URL 作标记,遍历全部分支;标记不得出现在应用日志、stdout、stderr、异常回显中。 |
| 兼容 | CLI 0/1/2/3 参数默认值和旧 shell 三参数调用;投递失败 exit 0,但 send 返回 False。 |
| 配置重载 | 同一已加载模块中原子换临时配置,下次 send 使用新值;失败加载不会永久缓存。 |
| watcher | import 不启动循环;post_status 调用唯一模块;计算文件 ID 等非通知行为保持原状。 |
本轮实测基线:额外设置 PYTHONDONTWRITEBYTECODE=1、临时 ASR_LOG / ASR_CACHE_DIR、ASR_NOTIFY=0、ASR_GIT_SYNC=0 后,结果为:
Ran 28 tests in 0.122s
OK
新增测试还应验证不同 cwd、未设置 PYTHONPATH 时两个入口的加载方式。统一 bootstrap 必须先于任何 asrpipe 导入,避免 unittest discovery 顺序导致单例 CFG 捕获另一个测试模块的环境;测试 backend 开关本身时在 mock transport 内局部解除总开关。
测试命令应在一个由测试运行器统一创建的临时环境下执行:
PYTHONDONTWRITEBYTECODE=1 PYTHONPATH=. .venv/bin/python -m unittest discover -s tests -v
上面命令只是入口,隔离环境由运行器或测试 fixture 事先提供,不能裸命令后假定生产日志也已隔离。最终报告保留真实 Ran N tests ... OK,不硬编码最终数量。附加静态检查:bash -n 检查旧 shell;编译改动 Python 时把 pycache 定向临时目录;限定目标文件检查无 Hermes 私有配置读取、无旧 route 默认值、签名只保留一份。
doctor 和完整 dry-run 均不列入本项目必需验收:前者有数据库写和联网行为,后者不覆盖通知且会调用模型。若另需全流程集成测试,使用合成录音/转写和隔离状态目录,单独记录目的与副作用。
6. 部署与回滚
- 在隔离副本完成代码和测试,部署前重新检查磁盘、备份、unit 与 drop-in 的实际生效设置;不输出含凭证的整个环境。旧报告中的 active/inactive 和队列为空只是一时快照。
- 暂停新上传/投递的窗口内,确认 inbox/processing、watcher 子进程及 native/revise 作业没有在处理任务。先排空再停止相关进程;只看到 processing 为空不等于没有入队竞态。
- 若 processing 已有孤立文件,记录并按现有恢复流程处理,不自动移动回 inbox,避免重复转写。未排空则推迟部署,不使用强制 restart。
- 部署已验证代码和权限为 0600 的本地配置;首次无真实目标时保持 none,并在交付记录明确“通知尚未恢复”。生产凭证一次性迁移时只复制所需字段,不输出密钥;源文件缺失则不阻塞代码部署。
- watcher unit 删除旧
MEETING_WEBHOOK_URL,保留MEETING_INBOX_BASE、MEETING_ASR_BACKEND和既有回滚注释;检查 drop-in 是否仍覆盖旧变量。daemon-reload 后在空闲窗口启动 watcher。 - 检查常驻 control_web 是否已加载改动模块,安排在无审核提交、无正在上传的请求及
.uploading/*.part活跃写入时重启以加载新代码;oneshot 下一次启动自然加载。不能假定仅重启 watcher 会更新全部 Python 进程。无需改 review/revise unit 的模型凭证设置。 - 只读检查目标服务 active、进程入口和脱敏日志;真实消息测试留给 W5。新地址更新只更换独立配置,不再改源码/unit。
回滚按同一空闲流程恢复成套文件(发送脚本、调用方、unit、配置),daemon-reload 并启动相关服务;记录恢复的哈希和权限。不要回滚生产数据库或手工复制运行中 SQLite 来“恢复状态”;本期不改变其 schema。旧版本通知本来可能不可用,回滚恢复的是业务行为,不能宣称修复消息投递。源码备份不得清理原有历史 .bak.*。
7. 协议文档与通用 agent 接入
docs/agent-protocol.md 作为通知和 agent 接入契约来源;HANDOFF 保留管线架构,control-web.md 保留现有 HTTP 接口说明。其他索引链接到这些来源,不再复制整套阶段表。
协议至少写清:send/CLI 参数、配置优先级、送达与静音语义、故障定位、敏感日志规则、真实现有的查询/上传路由,以及“审核回答由人完成,agent 不代填确认”。当前 HTTP 服务没有通用公网认证,不能描述为可直接暴露的 agent API;旧一次性页面与当前任务页的边界须准确。
更新 OBSIDIAN/CLAUDE.md 与 HANDOFF.md 的通知说明;HANDOFF 仍有“粗稿 commit/push”和旧临时 Web 生命周期描述,核实后标注历史或改为当前行为,保留算法历史数据。~/.claude/skills/memo-inbox/references/webhook-toolset-diagnostics.md 标历史归档并指向协议;修改前读该 skill,去重其他引用时保留“先粗整理、人工确认后正式发布”等纪律。修改 skill 必须在报告中列明。
文档同步还必须覆盖下列 v1 漏项:
~/.claude/skills/memo-inbox/SKILL.md:51–52,217–221:替换 active 的claude -p → minutes/_drafts描述及旧 webhook-agent/toolset 必查入口。~/.claude/skills/memo-inbox/references/meeting-recording-inbox.md:更新或明确归档旧.hermes-meetings、正文 webhook 和私有配置排障路径。~/.claude/skills/memo-inbox/references/asr-refine-pipeline.md:16–20:修正粗稿入 Vault、commit/push、Mac 审阅的旧时序,保留该技能独有的人工确认纪律。
首期交付跨 agent 可直接读取的协议,不把“创建 Markdown”冒充“已安装通用 skill”。如另做通用 skill,先使用 skill-creator,做薄入口引用上述契约,不复制业务规则、不读取私有 agent 状态、不授予自动审核或发消息的新权限。
8. 后续项目:任务状态与数据库解耦
先盘点 watcher、cli、review_web、control_web 的全部 intake_job 读写,明确哪些是同一服务的内部实现、哪些是真正跨进程契约;不要把所有内部函数调用都 HTTP 化。
优先评估提取唯一 intake store/schema 模块,由本机组件共享,先去重复 DDL;如确需 watcher 进程与控制服务独立部署,再引入版本化内部 API。后者必须先设计注册、状态迁移、幂等键、重复/乱序更新、认证和服务不可用时的持久重试。候选路由是未来设计,不是现有接口。网页服务中断不能新增为转录中断条件;未解决断网状态恢复前不移除 watcher 的当前写入路径。
验收至少覆盖:Web 上传与直接投文件、重复 fid、控制服务暂时不可用、转录/桥接失败、进程重启恢复、草稿/发布状态关联、禁止旧状态覆盖新状态。该项目单独评审和交付,不与首期通知上线绑定。
9. 完成定义与执行报告
- 代码完成:唯一发送实现、参数正确、完整脱敏、原调用契约兼容、隔离回归通过。
- 部署完成:已排空并加载新代码,配置权限正确,服务健康,有可执行的成套回滚记录。
- 通知恢复:已核实真实 endpoint/共享密钥,并在获得测试消息发送授权后确认端到端投递;只有 HTTP 2xx 时记录“后端接受”,未确认接收端则不写“用户已收到”。
- 待决策:真实 webhook 地址/密钥来源;是否另开 direct 后端项目。direct 不等价于自动复用 Hermes bot token 的授权。
报告列出改动与备份路径、测试原文、参数及脱敏测试名称、部署窗口、未覆盖项、真实通知状态和 skill 变更。对不满足的门槛明确标为未完成,不用“重构完成”掩盖投递尚未恢复。