Files
infra-docker-configs/servers/tools-pve/asr-pipeline/docs/agent-decoupling-implementation-v2.md
T
Codex 3c70861463
Build image / build (push) Canceled after 0s
Build ASR Pipeline / image (push) Canceled after 0s
feat(asr-pipeline): merge PR #1 webauth/CSRF/locking hardening and web_password fallback
2026-11-02 06:37:33 -08:00

19 KiB
Raw Blame History

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 仍有等待。本期保留有界等待,不声称实现了异步或可靠消息队列。

  • 旧文档的 .48 SSH/端口结论属于历史记录;本轮没有重新探测远端,也未读取凭证。新地址、密钥和投递结果仍需部署时核实。

  • 递归搜索只限活跃源文件及相关 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. 部署与回滚

  1. 在隔离副本完成代码和测试,部署前重新检查磁盘、备份、unit 与 drop-in 的实际生效设置;不输出含凭证的整个环境。旧报告中的 active/inactive 和队列为空只是一时快照。
  2. 暂停新上传/投递的窗口内,确认 inbox/processing、watcher 子进程及 native/revise 作业没有在处理任务。先排空再停止相关进程;只看到 processing 为空不等于没有入队竞态。
  3. 若 processing 已有孤立文件,记录并按现有恢复流程处理,不自动移动回 inbox,避免重复转写。未排空则推迟部署,不使用强制 restart。
  4. 部署已验证代码和权限为 0600 的本地配置;首次无真实目标时保持 none,并在交付记录明确“通知尚未恢复”。生产凭证一次性迁移时只复制所需字段,不输出密钥;源文件缺失则不阻塞代码部署。
  5. watcher unit 删除旧 MEETING_WEBHOOK_URL,保留 MEETING_INBOX_BASE、MEETING_ASR_BACKEND 和既有回滚注释;检查 drop-in 是否仍覆盖旧变量。daemon-reload 后在空闲窗口启动 watcher。
  6. 检查常驻 control_web 是否已加载改动模块,安排在无审核提交、无正在上传的请求及 .uploading/*.part 活跃写入时重启以加载新代码;oneshot 下一次启动自然加载。不能假定仅重启 watcher 会更新全部 Python 进程。无需改 review/revise unit 的模型凭证设置。
  7. 只读检查目标服务 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 变更。对不满足的门槛明确标为未完成,不用“重构完成”掩盖投递尚未恢复。