feat(asr-pipeline): merge PR #1 webauth/CSRF/locking hardening and web_password fallback
This commit is contained in:
@@ -0,0 +1,190 @@
|
||||
# ASR Agent 解耦实施项目(v2)
|
||||
|
||||
日期:2026-09-18。状态:代码核查后的实施计划,尚未执行生产改造。
|
||||
|
||||
上游:[耦合 Review](agent-decoupling-review.md)、[v1 实施文档](agent-decoupling-implementation.md)。本文件保留原文作为历史依据,修订执行设计和验收方式;实际开发以当前代码和本计划为准。本文中的工作项不是已完成变更,也不以旧文档的“已授权”文字代替后续执行请求。
|
||||
|
||||
## 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 内。
|
||||
|
||||
建议接口:
|
||||
|
||||
```python
|
||||
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,后续纳入版本控制时排除生产配置。
|
||||
|
||||
```json
|
||||
{
|
||||
"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` 后,结果为:
|
||||
|
||||
```text
|
||||
Ran 28 tests in 0.122s
|
||||
OK
|
||||
```
|
||||
|
||||
新增测试还应验证不同 cwd、未设置 PYTHONPATH 时两个入口的加载方式。统一 bootstrap 必须先于任何 `asrpipe` 导入,避免 unittest discovery 顺序导致单例 CFG 捕获另一个测试模块的环境;测试 backend 开关本身时在 mock transport 内局部解除总开关。
|
||||
|
||||
测试命令应在一个由测试运行器统一创建的临时环境下执行:
|
||||
|
||||
```bash
|
||||
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 变更。对不满足的门槛明确标为未完成,不用“重构完成”掩盖投递尚未恢复。
|
||||
@@ -0,0 +1,363 @@
|
||||
# ASR 管线 Agent 解耦 — 重构实施文档
|
||||
|
||||
**版本**:v1(2026-09-18)
|
||||
**上游 review**:`docs/agent-decoupling-review.md`
|
||||
**执行者**:子 agent(Claude Code / Codex / 其他具备文件与 shell 能力的 agent)
|
||||
**授权边界**:本文档 Stage 1–4 为已授权范围。超出范围的改动(尤其是 §8 禁止项)必须先回报 Kai 并取得确认。
|
||||
|
||||
---
|
||||
|
||||
## 0. 给执行 agent 的前置须知(先读完本节再动手)
|
||||
|
||||
### 0.1 这套系统正在生产运行,且无 git 保护
|
||||
|
||||
`/home/claw/claude-workspace` **不是 git 仓库**(已验证:`git rev-parse` 返回 `fatal: not a git repository`)。这意味着:
|
||||
|
||||
- **没有 `git checkout` 可以回滚**。任何改动前必须手动备份。
|
||||
- 备份约定沿用本项目既有风格(目录中已有 7 个 `*.bak.*` 文件):
|
||||
```bash
|
||||
cp -a <file> <file>.bak.$(date +%Y%m%d-%H%M%S)
|
||||
```
|
||||
- 磁盘余量充足(43G available),备份成本可忽略。
|
||||
|
||||
同时注意:`~/obsidian-vault` **是**独立 git 仓库,管线会向它 commit+push。本次重构**不应触碰 vault 内容**,只改 `OBSIDIAN/` 下的基础设施代码与 systemd unit。
|
||||
|
||||
### 0.2 当前 baseline(改动前请先自行复现,作为对照)
|
||||
|
||||
```bash
|
||||
cd /home/claw/claude-workspace/OBSIDIAN/asr-pipeline
|
||||
PYTHONPATH=. .venv/bin/python -m unittest discover -s tests -v
|
||||
# 期望:Ran 28 tests ... OK
|
||||
```
|
||||
|
||||
注意:venv 里**没有 pytest**,必须用 `unittest`。这 28 个测试全部离线(`test_core.py` 在 import 前就把 `ASR_STATE_DIR`/`ASR_VAULT` 重定向到 tempdir),可以安全反复跑。
|
||||
|
||||
**任何 stage 结束时这 28 个测试必须仍然全绿**,且新增功能需补充测试(见各 stage 的验收标准)。
|
||||
|
||||
### 0.3 当前运行状态(改动前实测,供对照)
|
||||
|
||||
| 单元 | 状态 | 说明 |
|
||||
|---|---|---|
|
||||
| `obsidian-recording-watch.service` | active | 音频监听 + 转录 + 桥接 |
|
||||
| `obsidian-asr-refine.path` | active | 监听 `.asr-inbox/` |
|
||||
| `asr-review-web.service` | active | Web 审核/上传 UI,`.145:8765` |
|
||||
| `obsidian-asr-refine.service` | inactive | 旧 `claude -p` 链路,**保留供 rollback** |
|
||||
|
||||
`127.0.0.1:8644` 已确认无监听(`ss -tlnp | grep 8644` 为空)。`.48` ICMP 可达,但 8644 等端口从 `.145` 探测为 closed/filtered,且 **SSH 到 `.48` 被拒(publickey)**——执行 agent 无法自行登录 `.48` 查看其配置。**这是本次重构的关键约束,见 §1.3。**
|
||||
|
||||
### 0.4 上游 review 的一处补充(执行时必须一并修掉)
|
||||
|
||||
在准备本实施文档时复核代码,发现 review 未记录的一个**真实 bug**,与本次解耦同属一处代码,应合并修复:
|
||||
|
||||
`asrpipe/vault.py:135` 把第一个位置参数写死成字符串 `"草稿已生成,待确认"`:
|
||||
|
||||
```python
|
||||
r = subprocess.run(["python3", str(n), "草稿已生成,待确认", title, body], ...)
|
||||
```
|
||||
|
||||
而 `meeting-status-notify.py` 的 argv 契约是 `(status, filename, detail)`。于是所有 5 处 `vault.notify()` 调用的语义都被错位丢弃:
|
||||
|
||||
| 调用点 | 调用方本意(第 1 参数) | 实际发出的 status |
|
||||
|---|---|---|
|
||||
| `revise.py:273` | "ASR 正式记录已发布" | 草稿已生成,待确认 ❌ |
|
||||
| `cli.py:109` | "ASR Web 审核启动失败" | 草稿已生成,待确认 ❌ |
|
||||
| `cli.py:160` | "ASR Web 审核启动失败" | 草稿已生成,待确认 ❌ |
|
||||
| `review_web.py:49` | "ASR 快速确认" | 草稿已生成,待确认 ❌ |
|
||||
| `review_web.py:271` | "ASR 快速确认" | 草稿已生成,待确认 ❌ |
|
||||
|
||||
即:**"发布成功"和"启动失败"在 Telegram 上都显示为"草稿已生成,待确认"**。调用方传的 `title` 被挤到 `filename` 位、`body` 被挤到 `detail` 位。这个 bug 目前被 8644 不通掩盖着(消息根本没发出去),一旦按本文档恢复通知,它会立刻变成可见的错误告警。**Stage 2 必须连带修掉。**
|
||||
|
||||
---
|
||||
|
||||
## 1. 重构目标与设计约束
|
||||
|
||||
### 1.1 目标
|
||||
|
||||
把"管线向人(Kai)发通知"这件事,从**"调用 Hermes 网关某个 IP:port 上的私有 webhook"**,改造成**"调用一个稳定的本地通知接口,Hermes 只是它的一种后端"**。
|
||||
|
||||
判据(重构完成后应当成立):
|
||||
|
||||
1. Hermes 再次换主机/换端口/换协议时,**只改一个配置文件,不改任何一行 Python、不改任何 systemd unit**。
|
||||
2. 通知目标不可用时,管线行为与今天一致:**记日志、继续跑、不阻塞、不丢数据**。
|
||||
3. 通知发送逻辑(HMAC 签名 + POST)在代码库里**只有一份实现**。
|
||||
4. 管线**不再读取 `~/.hermes/` 下的任何文件**。
|
||||
5. 上述 4 条对两个调用方(`meeting-watch.py` 和 `asrpipe`)同时成立。
|
||||
|
||||
### 1.2 设计风格必须与现有代码一致
|
||||
|
||||
这个项目有很清晰的既有风格,重构**必须沿用**而不是另立一套:
|
||||
|
||||
- 配置集中在 `asrpipe/config.py` 的 `Config` dataclass,每项都是 `_env("ASR_XXX", default)` 形式,env 可覆盖(见该文件已有的 `use_termbank` / `use_hindsight` / `gate_enabled` 等)。
|
||||
- 失败降级优先于抛异常:通知失败只 `log(..., level="WARN")`,绝不中断管线(`vault.py:139` 已是此风格,`meeting-status-notify.py` 的 docstring 也明确写了 "Silent no-op (exit 0) ... never block the pipeline")。
|
||||
- 注释说明"为什么"而非"是什么",且保留实测数据(参考 `config.py` 里 clean_model 的选型注释、`llm.py` 的 temperature 注释)。中文注释与英文注释在本项目中混用,**按所改文件的既有语言为准**。
|
||||
- 凭证走 600 权限的独立 env 文件,不进代码、不进 git(`bin/.asr-refine.env` 是既有先例)。
|
||||
|
||||
### 1.3 关键约束:`.48` 的新地址当前未知
|
||||
|
||||
执行 agent **无法** SSH 到 `.48`,因此**无法自行确定 Hermes webhook 的新地址**。
|
||||
|
||||
这不阻塞重构——恰恰相反,这正是要做接口层的理由。处理方式:
|
||||
|
||||
- Stage 1–3 的全部工作**不需要知道 `.48` 的地址**,只需要把地址变成"一个配置文件里的一个字段"。
|
||||
- 配置文件的初始值填 `.145` 的旧地址(即当前的 `http://127.0.0.1:8644/...`),并在文件里注释清楚"此地址在 2026-09-15 Hermes 迁移至 .48 后已失效,待 Kai 提供新地址后更新此处"。
|
||||
- 重构完成后,**由 Kai 填入 `.48` 的真实地址**,或按 Stage 4 的 `direct` 后端绕过 Hermes。
|
||||
- **禁止猜测端口后写进配置当作"已修复"**。宁可保留失效地址 + 明确注释,也不要写一个看似能跑实际错误的值。
|
||||
|
||||
---
|
||||
|
||||
## 2. 实施总览
|
||||
|
||||
| Stage | 内容 | 依赖 | 是否需要 Kai 输入 |
|
||||
|---|---|---|---|
|
||||
| **1** | 建立通知配置层 + 单一发送实现 | — | 否 |
|
||||
| **2** | 两个调用方收敛到该实现 + 修 argv 错位 bug | 1 | 否 |
|
||||
| **3** | 清理 systemd 硬编码 + 文档同步 | 2 | 否 |
|
||||
| **4** | (可选)`direct` 后端,彻底不依赖 Hermes | 3 | 是(需 token 授权决定) |
|
||||
|
||||
Stage 1–3 应**一镜到底连续执行**,中间不要停下来问;每个 stage 结束跑一次测试即可。Stage 4 涉及凭证使用决策,**必须停下来问 Kai**。
|
||||
|
||||
---
|
||||
|
||||
## 3. Stage 1 — 建立通知配置层与单一发送实现
|
||||
|
||||
### 3.1 新增配置文件 `OBSIDIAN/bin/notify-target.json`
|
||||
|
||||
这是本次重构的**唯一真相源**。结构建议(执行时可按需微调字段名,但必须保持"后端可切换 + 目标可配置"两个语义):
|
||||
|
||||
```json
|
||||
{
|
||||
"backend": "webhook",
|
||||
"_comment": "backend: webhook | none | direct(见 Stage 4)。切换后端不需要改代码。",
|
||||
|
||||
"webhook": {
|
||||
"url": "http://127.0.0.1:8644/webhooks/meeting-recording-status",
|
||||
"_url_comment": "⚠️ 此地址在 2026-09-15 Hermes 由 .145 迁移到 .48 后已失效。待 Kai 提供 .48 上的新地址后更新此字段。保留失效值而非猜测端口,是为了让故障显式可见。",
|
||||
"secret": "",
|
||||
"_secret_comment": "HMAC-SHA256 共享密钥。留空则不签名(对端若校验会拒绝)。历史上此值取自 ~/.hermes/webhook_subscriptions.json 的 meeting-recording-status.secret;现在改为本项目自持,不再读 Hermes 私有配置。",
|
||||
"timeout": 10
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
权限必须是 600(含密钥):
|
||||
```bash
|
||||
chmod 600 /home/claw/claude-workspace/OBSIDIAN/bin/notify-target.json
|
||||
```
|
||||
|
||||
**迁移密钥**:从 `~/.hermes/webhook_subscriptions.json` 的 `meeting-recording-status.secret` 复制一次到上面的 `secret` 字段。这是**一次性拷贝**,之后管线不再读 Hermes 的文件。(该文件当前结构已确认为 `{route: {secret, deliver, deliver_extra:{chat_id}, ...}}`。)
|
||||
|
||||
### 3.2 改造 `OBSIDIAN/bin/meeting-status-notify.py` 为唯一实现
|
||||
|
||||
保持它现有的**双重身份**——既是 CLI(供 shell 脚本调用),也要能被 Python import(供 `asrpipe` 直接调用,省掉一次 subprocess)。要求:
|
||||
|
||||
- 读 `notify-target.json` 决定后端与目标;**删除**对 `~/.hermes/webhook_subscriptions.json` 的读取(`SUBSCRIPTIONS` 常量与 `secret()` 函数)。
|
||||
- 保留 env 覆盖能力,优先级 `env > notify-target.json > 内置兜底`。env 变量名沿用既有前缀风格(如 `MEETING_STATUS_WEBHOOK_URL` 已在 `meeting-watch.py` 中使用,继续复用同名,不要另起新名)。
|
||||
- `backend: "none"` 时直接记日志返回,不发网络请求——这让"临时静音"不需要改代码,也让测试可以完全离线。
|
||||
- 暴露一个可 import 的函数,签名保持与 CLI 契约一致:
|
||||
```python
|
||||
def send(status: str, filename: str = "", detail: str = "") -> bool
|
||||
```
|
||||
返回是否送达成功;**内部吞掉所有异常**,绝不向调用方抛(维持"never block the pipeline")。
|
||||
- `__main__` 分支保持 `argv = (status, filename, detail)` 契约不变——`bin/obsidian-asr-refine.sh:45` 仍以此方式调用它(旧 rollback 链路),**不要破坏该契约**。
|
||||
|
||||
### 3.3 验收标准
|
||||
|
||||
```bash
|
||||
cd /home/claw/claude-workspace/OBSIDIAN
|
||||
|
||||
# 1. backend=none 时静默成功、不发网络请求
|
||||
python3 - <<'EOF'
|
||||
import json, pathlib
|
||||
p = pathlib.Path("bin/notify-target.json")
|
||||
d = json.loads(p.read_text()); d["backend"] = "none"
|
||||
p.write_text(json.dumps(d, ensure_ascii=False, indent=2))
|
||||
EOF
|
||||
python3 bin/meeting-status-notify.py "测试" "f.mp3" "detail" # 期望:退出码 0,日志说明 backend=none
|
||||
|
||||
# 2. backend=webhook 且目标不通时,仍然退出码 0(不阻塞)
|
||||
# (把 backend 改回 webhook 后重跑,期望打印类似 "skipped: Connection refused",退出码仍为 0)
|
||||
echo "exit=$?"
|
||||
|
||||
# 3. 确认不再引用 Hermes 私有配置
|
||||
grep -rn "webhook_subscriptions\|\.hermes" bin/meeting-status-notify.py
|
||||
# 期望:无输出(注释里提及历史来源可以,但不能有真实读取)
|
||||
|
||||
# 4. 旧链路 CLI 契约未破坏
|
||||
bash -n bin/obsidian-asr-refine.sh && echo "refine.sh syntax ok"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Stage 2 — 两个调用方收敛 + 修 argv 错位
|
||||
|
||||
### 4.1 `asrpipe` 侧
|
||||
|
||||
**`asrpipe/config.py`**:
|
||||
|
||||
- 新增 `notify_backend` 字段,风格与既有项一致:`notify_backend: str = _env("ASR_NOTIFY_BACKEND", "")`(空 = 交由 `notify-target.json` 决定;显式设置则覆盖)。
|
||||
- 保留现有 `notify: bool`(`ASR_NOTIFY=0` 的总开关)——这是既有行为,`doctor` 和文档都依赖它,不要删。
|
||||
- `notifier` property 保留(`cli.py:207` 的 doctor 检查在用),但其含义从"要 subprocess 调用的脚本"变为"通知实现模块的位置"。若改动了 doctor 的检查语义,需同步更新 `cli.py` 的 `chk("notifier", ...)`。
|
||||
|
||||
**`asrpipe/vault.py` 的 `notify()`**:
|
||||
|
||||
- **修掉 argv 错位 bug**:不再把 `"草稿已生成,待确认"` 写死为 status。调用方传入的第一个参数(语义上就是 status/事件名)必须原样传递。
|
||||
- 建议直接 import `meeting-status-notify.py` 暴露的 `send()`,取代 `subprocess.run(["python3", ...])`——省一次进程启动,也让类型契约显式。若因文件名带连字符不便 import,可用 `importlib.util.spec_from_file_location`(路径来自 `CFG.notifier`),或把共享逻辑提取为 `OBSIDIAN/bin/notify_lib.py` 再由两边 import。**三种都可接受,选一种并在注释里说明理由。**
|
||||
- 保留 `redact_log` 参数行为(`review_web.py:271` 传了 `redact_log=True`,因为 body 含一次性 capability URL,**绝不能进日志**)。这是安全相关行为,不要动。
|
||||
- 保留"notifier 不存在时只记日志"的降级分支。
|
||||
|
||||
**5 处调用点**(`revise.py:273`、`cli.py:109`、`cli.py:160`、`review_web.py:49`、`review_web.py:271`):修好 `notify()` 后语义即自动正确,**调用点本身不需要改**。改完后请逐一确认它们发出的 status 与调用方本意一致(对照 §0.4 的表格)。
|
||||
|
||||
### 4.2 `meeting-watch.py` 侧
|
||||
|
||||
- 删除自建的 HMAC 签名与 POST 逻辑:`webhook_secret()`(L177)、`post_webhook()`(L187)、`post_status()`(L205)。
|
||||
- `post_status` 改为薄封装,转调 Stage 1 的单一实现;3 个调用点(L272 "开始转录"、L286 "转录完成,正在生成粗整理"、L304 "处理失败")的调用形式**保持不变**。
|
||||
- 删除 `WEBHOOK_URL` / `WEBHOOK_ROUTE` / `WEBHOOK_SECRET` / `STATUS_WEBHOOK_URL` / `STATUS_WEBHOOK_ROUTE` / `SUBSCRIPTIONS` 这些常量(L32–L37)。
|
||||
|
||||
**关于 `meeting-recording` 路由(携带正文那条)**:`post_webhook()` 在当前代码里**只被 `post_status()` 调用过**,即 `meeting-recording` 路由从未真正被 POST(已 grep 确认)。它是旧 Hermes agent 精修架构的残留,`CLAUDE.md` 明确记载该架构"已**取代**"。**删除这条死路径**,不要为它保留接口。(`~/.hermes/webhook_subscriptions.json` 里那条带 `prompt`/`skills: [memo-inbox]` 的 `meeting-recording` 订阅同属旧架构遗留,但那是 Hermes 侧的配置,**不在本次授权改动范围**,只在 §7 报告里提一句即可。)
|
||||
|
||||
另外注意 `meeting-recording-status` 订阅带 `deliver_only: True`——它只做消息投递、不唤起 agent。这印证了"状态通知本质上只是一条消息,不需要 agent 参与",也是 Stage 4 `direct` 后端在语义上完全等价的依据。
|
||||
|
||||
### 4.3 补充测试(必须新增)
|
||||
|
||||
在 `asr-pipeline/tests/test_core.py` 中新增(沿用该文件既有的 `unittest` + `mock` 风格,注意它已在 import 前把 state/vault 重定向到 tempdir):
|
||||
|
||||
1. **status 透传测试**(覆盖 §0.4 的 bug):mock 掉实际发送,断言 `vault.notify("ASR 正式记录已发布", "body")` 传出的 status 是 `"ASR 正式记录已发布"`,**不是** `"草稿已生成,待确认"`。这是防止该 bug 回归的关键测试。
|
||||
2. **降级测试**:目标不可达(mock 抛 `URLError`)时 `vault.notify()` 不抛异常、正常返回。
|
||||
3. **backend=none 测试**:不产生任何网络调用。
|
||||
4. **`redact_log` 测试**:`redact_log=True` 时日志中不含 body 原文(防止 capability URL 泄漏进日志)。
|
||||
|
||||
### 4.4 验收标准
|
||||
|
||||
```bash
|
||||
cd /home/claw/claude-workspace/OBSIDIAN/asr-pipeline
|
||||
PYTHONPATH=. .venv/bin/python -m unittest discover -s tests -v
|
||||
# 期望:28 + 新增用例,全绿
|
||||
|
||||
# 单一实现确认:HMAC 签名逻辑只应出现在一个文件里
|
||||
cd /home/claw/claude-workspace/OBSIDIAN
|
||||
grep -rn "hmac.new" --include="*.py" . | grep -v -E "__pycache__|\.venv|\.bak\."
|
||||
# 期望:只有 1 处(notify 实现所在文件)
|
||||
|
||||
# 管线不再读 Hermes 私有配置
|
||||
grep -rn "webhook_subscriptions" --include="*.py" --include="*.sh" . | grep -v -E "__pycache__|\.venv|\.bak\."
|
||||
# 期望:无真实读取(注释提及历史来源可接受)
|
||||
|
||||
# doctor 仍通过(会联网测两个模型,属正常)
|
||||
asr-pipeline/bin/asr-refine doctor
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Stage 3 — systemd 与文档同步
|
||||
|
||||
### 5.1 systemd
|
||||
|
||||
`~/.config/systemd/user/obsidian-recording-watch.service` 删除这一行(第三处硬编码):
|
||||
|
||||
```ini
|
||||
Environment=MEETING_WEBHOOK_URL=http://127.0.0.1:8644/webhooks/meeting-recording
|
||||
```
|
||||
|
||||
理由:该 URL 对应的 `meeting-recording` 路由已在 Stage 2 删除;状态通知目标现由 `notify-target.json` 统一提供,不应再由 unit 注入。
|
||||
|
||||
**保留** `MEETING_INBOX_BASE` 与 `MEETING_ASR_BACKEND=qwen3cam`——这两个是真实生效的部署配置,与本次解耦无关(后者还带着"ASR backend trial"的回滚注释,不要动)。
|
||||
|
||||
生效并验证:
|
||||
```bash
|
||||
systemctl --user daemon-reload
|
||||
systemctl --user restart obsidian-recording-watch.service
|
||||
systemctl --user is-active obsidian-recording-watch.service # 期望 active
|
||||
journalctl --user -u obsidian-recording-watch.service -n 30 --no-pager
|
||||
```
|
||||
|
||||
`asr-review-web.service` 与 `asr-review-revise@.service` **不需要改**(它们通过 `asrpipe` 间接受益;后者的 `EnvironmentFile=.asr-refine.env` 只管模型凭证)。
|
||||
|
||||
### 5.2 文档
|
||||
|
||||
按"一份权威来源 + 其余引用"的结构收敛(与 vault 侧 `AGENTS.md` 作索引的模式一致):
|
||||
|
||||
| 文件 | 动作 |
|
||||
|---|---|
|
||||
| `asr-pipeline/docs/agent-protocol.md` | **新建**。本次解耦的权威文档:通知接口契约(status/filename/detail 三段语义)、`notify-target.json` 字段含义、后端切换方式、"Hermes 只是一种后端"的定位、任何 agent 想接入时该读什么。Kai 早在 2026-07-17 就规划过这份共用协议文档但从未落地——这次补上。 |
|
||||
| `asr-pipeline/HANDOFF.md` | 第 71 行 "Telegram 通知(hermes-gateway :8644 签名 webhook)" 改为指向通知接口层,不再把 Hermes 端口写进设计文档;链接到 `agent-protocol.md`。 |
|
||||
| `OBSIDIAN/CLAUDE.md` | 管线段落中的 `meeting-status-notify.py` 描述更新为"可切换后端的通知接口";补一行指向 `agent-protocol.md`。保持该文件"只留 Claude + 本服务器专属基础设施"的既有定位,不要把协议细节搬进来。 |
|
||||
| `~/.claude/skills/memo-inbox/references/webhook-toolset-diagnostics.md` | **标注废弃**。它描述的"webhook agent 直接处理 `meeting-recording` 路由 + 需要 `hermes-webhook`/`file`/`skills` toolset"架构已于 2026-07-25 被取代,该路由在 Stage 2 已删除。建议在文件顶部加醒目废弃声明 + 指向 `agent-protocol.md`,而不是直接删除(保留历史排障脉络)。**注意:这是 skill 目录,改动会影响 Claude 的行为,改完在 §7 里明确告知 Kai。** |
|
||||
| `~/.claude/skills/memo-inbox/references/asr-refine-pipeline.md` | 五阶段表格等与 `HANDOFF.md` 逐字重复的内容改为链接引用;**保留** skill 独有的纪律性内容("第一轮只出粗整理,不直接写正式纪要"、S3 fidelity gate 存在的理由与实测数据、模型选型不得随意改动的警告)——这些是 Kai 的工作纪律,不是可去重的冗余。 |
|
||||
|
||||
### 5.3 验收标准
|
||||
|
||||
```bash
|
||||
# 确认 8644 不再出现在任何活跃代码/配置里(文档中作为历史说明可保留)
|
||||
cd /home/claw/claude-workspace/OBSIDIAN
|
||||
grep -rn "8644" --include="*.py" --include="*.sh" . ~/.config/systemd/user/*.service 2>/dev/null \
|
||||
| grep -v -E "__pycache__|\.venv|\.bak\."
|
||||
# 期望:仅 notify-target.json 的待更新地址 + 注释
|
||||
|
||||
# 端到端(不触碰 vault、不 commit)
|
||||
asr-pipeline/bin/asr-refine dry-run <任一 .asr-inbox/*.txt>
|
||||
# 期望:五阶段跑通,产物落 asr-pipeline/dry-run/<slug>/,通知步骤降级但不报错
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Stage 4 —(可选,需 Kai 决策)`direct` 后端
|
||||
|
||||
Stage 1–3 完成后,通知链路仍然经过 Hermes:管线 → Hermes 网关 webhook → Telegram。只要 Hermes 不在线,Kai 就收不到通知。
|
||||
|
||||
**可选的彻底解耦**:增加 `backend: "direct"`,直接调 Telegram Bot API(`https://api.telegram.org/bot<token>/sendMessage`),不经过 Hermes。
|
||||
|
||||
技术上已确认可行:
|
||||
|
||||
- `~/.hermes/.env` 中存在 `TELEGRAM_BOT_TOKEN`;`chat_id`(`1093241065`)在 `webhook_subscriptions.json` 的 `deliver_extra` 里已确认。
|
||||
- `meeting-recording-status` 订阅标记为 `deliver_only: True`,即 Hermes 对这条消息**只做投递、不唤起 agent**。因此 `direct` 后端在语义上与现状**完全等价**,不损失任何 agent 能力。
|
||||
|
||||
**但这需要 Kai 决策,不要擅自实施**,因为:
|
||||
|
||||
1. 要把 Telegram bot token 复制进本项目自持的凭证文件(或新增一份 600 env 文件)。这扩大了 token 的存放面,是安全决策,须 Kai 同意。
|
||||
2. 绕过 Hermes 后,Telegram 侧的消息格式/限流/重试不再由 Hermes 统一处理,需自行承担。
|
||||
3. Kai 可能有意保留"所有对外消息都经 Hermes"的架构约定。
|
||||
|
||||
**执行方式**:Stage 3 完成后,在 §7 报告中列出此选项与上述权衡,**等 Kai 明确答复再动手**。届时若 Kai 同意,`direct` 只是 `notify-target.json` 里加一个后端分支 + 一个 sender 函数,改动很小——这正是 Stage 1 接口层带来的收益。
|
||||
|
||||
---
|
||||
|
||||
## 7. 完成后的回报要求
|
||||
|
||||
Stage 1–3 结束后,向 Kai 报告,包含:
|
||||
|
||||
1. **改了哪些文件**,每个文件一句话说明,附备份文件名。
|
||||
2. **测试结果原文**(`Ran N tests ... OK`)。若有任何失败,**如实说明失败项与原因,不要含糊**。
|
||||
3. **§0.4 的 argv 错位 bug 已修复的证明**:新增测试的名称与断言内容。
|
||||
4. **仍然待办的事**,明确区分:
|
||||
- 需要 Kai 提供 `.48` 上 Hermes webhook 新地址,填入 `notify-target.json` 的 `webhook.url`(在此之前通知仍不通,但已降级为"改一个配置字段")。
|
||||
- Stage 4 `direct` 后端的决策请求(附 §6 的三点权衡)。
|
||||
- Hermes 侧 `~/.hermes/webhook_subscriptions.json` 中 `meeting-recording` 那条旧订阅(带 `prompt`/`skills: [memo-inbox]`)属旧架构遗留、已无调用方,建议 Kai 自行清理——**本次未改动,因为不在授权范围**。
|
||||
5. **动过 `~/.claude/skills/` 的明确告知**(若执行了 §5.2 的 skill 文档改动)——这会影响 Claude 后续行为,Kai 需要知道。
|
||||
|
||||
---
|
||||
|
||||
## 8. 明确禁止(越界前必须先问)
|
||||
|
||||
- ❌ **不要猜 `.48` 的端口后填进配置当作"已修复"**。宁可留失效地址 + 注释。SSH 到 `.48` 已确认被拒,无法自行核实。
|
||||
- ❌ **不要往 `asrpipe/` 核心五阶段(`segment`/`flagger`/`refine`/`fidelity`/`revise`)里加任何 agent 相关代码**。`cli.py` 与 `config.py` 头部的 "no Claude Code, no opencode, no agent runtime" 声明是这套系统最值得保留的架构资产。
|
||||
- ❌ **不要改模型选型**(`clean_model` / `draft_model`)。`config.py` 与 `HANDOFF.md` 都记录了实测依据(flash-lite 闸门失败 19/87 vs haiku 78/87,且 haiku 有 69 次过度压缩)。与本次解耦无关。
|
||||
- ❌ **不要关闭或削弱 S3 fidelity gate**。它防的是静默信息丢失(无闸门时英文术语丢失达 22%、数字 11%)。
|
||||
- ❌ **不要删除 rollback 链路**(`bin/obsidian-asr-refine.sh` + `obsidian-asr-refine.service`)。它们当前 inactive 但被 `obsidian-asr-refine.path` 的注释和 `HANDOFF.md` 记录为回滚路径。只需保证 `refine.sh:45` 调用 `meeting-status-notify.py` 的 CLI 契约不被破坏。
|
||||
- ❌ **不要动 `~/obsidian-vault/` 的内容**,也不要在本次重构中产生 vault commit。本次只改 `OBSIDIAN/` 基础设施与 systemd unit。
|
||||
- ❌ **不要清理那 7 个 `*.bak.*` 文件**。它们是 2026-07-26 重构的历史留存,清理属独立决策,需 Kai 单独批准。
|
||||
- ❌ **不要引入新的第三方依赖**。`asr-pipeline/.venv` 目前只有 pypinyin / rapidfuzz / numpy,通知功能用标准库 `urllib.request` + `hmac` + `json` 足够(现有实现已证明)。
|
||||
- ❌ **不要为解耦搭建通用消息总线/插件框架**。当前只有一个通知需求,`notify-target.json` 里一个 `backend` 字段 + 几个分支就够。过度工程化会让下一个维护者更难读。
|
||||
|
||||
---
|
||||
|
||||
## 9. 执行顺序速查
|
||||
|
||||
```
|
||||
0. 读 §0 全部 → 跑 baseline 测试(28 OK)→ 备份将改动的文件
|
||||
1. Stage 1:notify-target.json + meeting-status-notify.py 单一实现 → 验收 §3.3
|
||||
2. Stage 2:config.py / vault.py(修 argv bug)/ meeting-watch.py + 4 个新测试 → 验收 §4.4
|
||||
3. Stage 3:systemd 去硬编码 + agent-protocol.md + 4 份文档同步 → 验收 §5.3
|
||||
4. 回报 Kai(§7),等 .48 地址与 Stage 4 决策
|
||||
```
|
||||
|
||||
Stage 1–3 连续执行,不要中途停下来确认;Stage 4 必须等 Kai 答复。
|
||||
@@ -0,0 +1,98 @@
|
||||
# ASR 管线 × Agent 耦合点 Review
|
||||
|
||||
**日期**:2026-09-18
|
||||
**背景**:Hermes 网关从 .145 迁移到 .48 后,`meeting-status-notify.py` 因硬编码 `127.0.0.1:8644` 而 Telegram 通知失效(核心转录/整理流程本身不受影响)。这次故障暴露的不是孤立 bug,而是整条管线里 Claude/Hermes 两个 agent 以"硬编码地址/私有配置文件/直接 shell out"方式接入,缺乏稳定接口层。本文档目标:**列出所有耦合点,诊断问题,提出"预留接口 + 通用 skill"的解耦方案**,供下一轮重构使用。不在本文档内直接修改代码。
|
||||
|
||||
---
|
||||
|
||||
## 1. 涉及文件清单
|
||||
|
||||
### 1.1 生产代码(按耦合程度分类)
|
||||
|
||||
| 文件 | 耦合对象 | 耦合方式 | 严重度 |
|
||||
|---|---|---|---|
|
||||
| `OBSIDIAN/bin/meeting-status-notify.py` | Hermes | 硬编码 `http://127.0.0.1:8644/webhooks/...`;直接读 `~/.hermes/webhook_subscriptions.json` 取 HMAC secret | 🔴 高(本次故障根源) |
|
||||
| `OBSIDIAN/recordings/bin/meeting-watch.py` | Hermes | `post_webhook()`/`post_status()` **重新实现**了一遍同样的 HMAC 签名 + 硬编码 URL 逻辑(与上面文件重复代码,两处维护) | 🔴 高(重复实现同一故障点) |
|
||||
| `OBSIDIAN/asr-pipeline/asrpipe/vault.py` (`notify()`) | Hermes(间接) | 不直接硬编码地址,但通过 `CFG.notifier` 固定 `subprocess.run(["python3", meeting-status-notify.py, ...])`,位置参数契约无 schema | 🟡 中(依赖上面两个文件之一才能工作) |
|
||||
| `OBSIDIAN/asr-pipeline/asrpipe/config.py` (`review_public_host`) | 部署环境 | 默认值硬编码 `192.168.50.145`(本机 IP),可用 env 覆盖 | 🟢 低(有 env 覆盖,且 review-web 确实还在 .145,不是错误,只是"写死默认值"的一贯风格问题) |
|
||||
| `OBSIDIAN/bin/obsidian-asr-refine.sh`(**已停用**,仅供 rollback) | Claude Code | 直接 shell out `$HOME/.local/bin/claude -p`,依赖交互登录态 | 🟡 中(非活跃路径,但 rollback 会重新引入该耦合) |
|
||||
| `OBSIDIAN/bin/obsidian-asr-hotwords.py`、`OBSIDIAN/bin/obsidian-asr-intake.sh` | 无(命名遗留) | 调用 `~/bin/hermes-qwen3-asr-smart`——**这是个纯 STT wrapper shell 脚本,跟 Hermes agent 运行时完全无关**,只是历史命名带 "hermes" 前缀,容易被误读为依赖 agent | 🟢 低(认知负担,不是真耦合,建议重命名去混淆) |
|
||||
| `~/.config/systemd/user/obsidian-recording-watch.service` | Hermes | `Environment=MEETING_WEBHOOK_URL=http://127.0.0.1:8644/...` 在 systemd 层再硬编码一次 | 🔴 高(第三处需要同步修改的地方) |
|
||||
| `asr-pipeline/asrpipe/llm.py` | Claude(仅模型选择) | 字符串形式引用 `anthropic/claude-sonnet-5` 等模型名做定价/no-temperature 判断 | 🟢 低(纯 API 层配置,非 agent 运行时耦合,正常) |
|
||||
| `asr-pipeline/asrpipe/cli.py`、`config.py` 文件头注释 | 无 | 明确声明"no Claude Code, no opencode, no agent runtime"——这是**架构上已经做对的部分**,核心管线是纯 HTTPS+文件驱动,值得保留和强化 | 🟢 无耦合(正面案例) |
|
||||
|
||||
### 1.2 文档/Skill 层(描述与现状不一致,需要一并清理)
|
||||
|
||||
| 文件 | 问题 |
|
||||
|---|---|
|
||||
| `~/.claude/skills/memo-inbox/references/webhook-toolset-diagnostics.md` | 描述的是**已被取代的旧架构**:假设有一个"webhook agent"(Hermes)直接处理 `meeting-recording` 路由、需要 `hermes-webhook`/`file`/`skills` toolset、把转录内容发给 Hermes agent 做 memo-inbox 整理。但 `CLAUDE.md` 明确写着"已**取代**旧的 Hermes→Telegram agent 精修",且 `meeting-watch.py` 里 `post_webhook()`(对应 `meeting-recording` 路由,携带正文)从未被实际调用,只有 `post_status()`(状态 ping)在用。**这份 skill 文档描述的故障排查对象已经不存在,但仍在 memo-inbox skill 里被索引,可能误导下一次排障。** |
|
||||
| `~/.claude/skills/memo-inbox/references/asr-refine-pipeline.md` | 与 `CLAUDE.md`、`HANDOFF.md` 三份文档描述同一条管线,各有侧重但存在信息重复(例如五阶段说明在两份文档里几乎逐字重复)。不是错误,但增加了"改一处忘改另一处"的维护成本——这次 8644 端口就是同一类问题在配置层面的重演。 |
|
||||
| `OBSIDIAN/asr-pipeline/HANDOFF.md` | 明确写了"Telegram 通知(hermes-gateway :8644 签名 webhook)",是**当前唯一还把 Hermes 网关端口写进设计文档**的地方,本身没错但需要随实现一起更新。 |
|
||||
| 目前**不存在** `agent-protocol.md`(据 hindsight 记忆,2026-07-17 曾规划把 obsidian skill 拆分出一份 Hermes/Codex/Claude Code 共用的 `agent-protocol.md`,统一七步协议) | 说明"通用协议文档"这个方向此前已经被认可过,但从未真正落地到 ASR 通知这一层,这次故障正好是没有该抽象层的直接后果。 |
|
||||
|
||||
---
|
||||
|
||||
## 2. 问题诊断
|
||||
|
||||
### 2.1 核心问题:通知通道是唯一的"横切关注点",却用点对点硬编码实现
|
||||
|
||||
真正跨 Claude/Hermes 边界的交互只有一件事:**管线要把"状态变化"告诉人(Kai),而人在 Telegram 上**。当前实现把这件事等同于"调用 Hermes 网关在某个 IP:port 上开的私有 webhook",导致:
|
||||
|
||||
- 通知目标地址在**三个物理位置**各写一份(`meeting-status-notify.py`、`meeting-watch.py`、systemd unit 的 `Environment=`),Hermes 一换主机全部失效,还都要单独改。
|
||||
- 认证方式依赖读取 **Hermes 自己的私有配置文件** `~/.hermes/webhook_subscriptions.json`,管线对 Hermes 的内部文件结构做了假设——Hermes 的实现细节变了,管线也会跟着坏,而管线本身的 git 仓库里完全看不出这个依赖关系。
|
||||
- `meeting-watch.py` 里的 `post_webhook`/`post_status` 和 `meeting-status-notify.py` 是**同一逻辑的两份实现**(都做 HMAC 签名 + POST),维护者容易改一处忘改另一处。
|
||||
|
||||
### 2.2 次要问题:文档层的"多真相源"
|
||||
|
||||
`CLAUDE.md`(基础设施说明)、`HANDOFF.md`(设计交接)、`asr-refine-pipeline.md`(skill 引用)三份文档对同一条管线各写一遍架构图和五阶段表格,`webhook-toolset-diagnostics.md` 甚至在描述一个已经退役的架构分支。这不是本次故障的直接原因,但预示着**如果解耦时只改代码不改文档索引,同样的"多处不同步"问题会在文档层重演**。
|
||||
|
||||
### 2.3 正面案例,值得保留的设计
|
||||
|
||||
- `asrpipe/` 核心包(S0-S4 五阶段 + fidelity gate + termbank)本身**已经是纯 HTTPS + 文件驱动**,不依赖任何 agent CLI 或登录态,`cli.py`/`config.py` 头部注释明确声明了这个边界——这是解耦该学的样子。
|
||||
- `asr-review-web`/`control_web.py` 是一个独立 HTTP 服务,路由/状态机/上传都通过标准 HTTP 暴露,没有假设调用方是谁——**这个"HTTP 接口面"是天然可以承载 Claude/Hermes 通用交互的地方**,不需要另起炉灶。
|
||||
|
||||
---
|
||||
|
||||
## 3. 解耦方案建议(预留接口 + 通用 skill)
|
||||
|
||||
### 3.1 接口层:把"通知"做成可插拔的抽象,而不是"调用 Hermes"
|
||||
|
||||
建议参考 `config.py` 里其余配置项的风格(`use_termbank`/`use_hindsight` 等 boolean 开关 + env 覆盖),把通知也做成同一等级的可插拔能力:
|
||||
|
||||
- 在 `asrpipe/config.py` 增加 `notify_backend`(如 `webhook` / `none` / 未来的 `stdout`/`file` 等),而不是隐式认定"notifier = 某个写死路径的 python 脚本"。
|
||||
- `meeting-status-notify.py` 改造成**唯一**的通知发送实现(`meeting-watch.py` 不再自己重新实现 HMAC 签名,改为 shell out 到同一份脚本,或提取成共享库函数),并且:
|
||||
- 目标地址、密钥来源全部走 env / 独立配置文件(例如 `OBSIDIAN/bin/notify-target.json`),**不直接读 `~/.hermes/*`**——管线自己的配置与 Hermes 的私有配置解耦,Hermes 换主机/换协议时只改这一份 target 配置,不改代码。
|
||||
- 把"目标是 Hermes 网关的 webhook"这件事,退化为该抽象接口的**一种实现**,而不是唯一实现——未来即使 Hermes 换成完全不同的通知方式(比如直接调 Telegram Bot API),也只是换一个 backend,管线代码不用动。
|
||||
|
||||
### 3.2 协议层:让 Claude 和 Hermes 通过同一份"通用 skill/协议文档"交互,而不是各自看各的私有细节
|
||||
|
||||
现状是 Hermes 一侧靠私有 webhook 配置、Claude 一侧靠读 `CLAUDE.md`/`HANDOFF.md`,两边对同一条管线的认知来自不同文档,容易出现你这次遇到的情况——迁移动作只在 Hermes 侧发生,ASR 侧的假设没人同步更新。
|
||||
|
||||
建议:
|
||||
|
||||
1. **落地此前已经规划过的 `agent-protocol.md`**(hindsight 记忆里 2026-07-17 就提出过这个方向,但目前尚未创建)。把"任何 agent(Hermes/Claude Code/Codex)如何得知 ASR 管线的状态变化、如何触发人工确认、通知目标怎么配置"写成一份三方共用的协议文档,而不是分散在 `CLAUDE.md` + `HANDOFF.md` + memo-inbox skill 引用里各写一份。
|
||||
2. 把 `control_web.py` 已有的 HTTP 接口面正式定义为**唯一**对外交互入口:任何 agent 需要查询任务状态/触发处理,都走这个 HTTP API,而不是直接读 sqlite(`intake.db`)或直接改文件。目前 `meeting-watch.py` 直接读写 `CONTROL_DB`(sqlite),这本身也是一种"绕过接口层直连内部状态"的耦合,建议后续统一收敛到 `control_web.py` 的 API 之后。
|
||||
3. 清理 `webhook-toolset-diagnostics.md`:要么标注为"历史架构,已废弃于 2026-07-25",要么直接删除/归档,避免下次排障时被过时描述误导。
|
||||
4. `CLAUDE.md` / `HANDOFF.md` / `asr-refine-pipeline.md` 三份重叠内容建议收敛为"一份权威来源 + 其余引用链接"的结构(类似 vault 侧 `AGENTS.md` 作为 SOP 索引、具体细则各自一份的模式),而不是三份都写一遍五阶段表格。
|
||||
|
||||
### 3.3 命名去混淆
|
||||
|
||||
`~/bin/hermes-qwen3-asr-smart` 建议评估是否重命名(例如 `qwen3-asr-transcribe-wrapper`)——它不依赖 Hermes agent 运行时,纯粹是历史命名,留着容易让人(或下一轮重构的 LLM)误判为真实耦合点而做无用功。
|
||||
|
||||
---
|
||||
|
||||
## 4. 优先级建议
|
||||
|
||||
| 优先级 | 事项 | 原因 |
|
||||
|---|---|---|
|
||||
| P0 | 通知 backend 抽象化 + 三处硬编码收敛为一处配置 | 直接对应本次故障,且是唯一真正跨 agent 边界的运行时依赖 |
|
||||
| P1 | 落地 `agent-protocol.md`,清理 `webhook-toolset-diagnostics.md` 等过时文档 | 防止"文档多真相源"重演同类故障 |
|
||||
| P2 | `meeting-watch.py` 收敛直连 sqlite 为走 `control_web.py` API | 减少内部状态的多点直连,但当前未造成故障,可延后 |
|
||||
| P3 | `hermes-qwen3-asr-smart` 重命名 | 纯认知成本,无功能风险,可随手做 |
|
||||
|
||||
---
|
||||
|
||||
## 5. 明确不建议做的事
|
||||
|
||||
- 不建议把核心 `asrpipe/` 五阶段管线本身引入任何 agent 依赖——它当前"纯 HTTPS + 文件驱动"的设计是对的,解耦应该聚焦在通知/协议这一层,不要往核心管线里加 Claude/Hermes 相关代码。
|
||||
- 不建议为了解耦而过度设计一套通用消息总线/插件系统——当前只有"一个通知需求 + 一个 HTTP 状态查询需求",用简单的 backend 开关 + 现有 HTTP 接口面就够,避免过度工程化。
|
||||
@@ -0,0 +1,130 @@
|
||||
# ASR Pipeline v2
|
||||
|
||||
## Goal
|
||||
|
||||
The pipeline recovers useful meeting facts and traces rather than pretending that
|
||||
speech can always be reconstructed word for word. The original transcript remains
|
||||
the audit source. A draft may omit low-value repetition, but must not invent a
|
||||
subject, number, decision, or relationship.
|
||||
|
||||
## Stages
|
||||
|
||||
1. S0 parses speaker turns and collapses decoder loops.
|
||||
2. S1 generates candidates from the untouched transcript. Phonetics, observed ASR
|
||||
errors, aliases, entity relationships, and recency are independent signals.
|
||||
3. S2 only removes fillers and repairs punctuation. It receives no Hindsight context
|
||||
and makes no entity decisions.
|
||||
4. S3 applies settled mappings deterministically. Flash Lite sees only unresolved
|
||||
candidates and Chinese-number spans.
|
||||
5. S4 extracts source-labelled evidence cards per chunk.
|
||||
6. S5 Sonnet groups those cards by topic and writes a rough draft.
|
||||
|
||||
## Personal fact store
|
||||
|
||||
`state/lexicon.db` is intentionally reusable outside ASR:
|
||||
|
||||
- `entity`, `alias`, `correction`: identity and observed surface forms.
|
||||
- `relation`: person/company/project relations with source, confidence, and dates.
|
||||
- `interaction`: contact recency and frequency.
|
||||
- `observation`: what a transcript emitted and how it was resolved.
|
||||
- `phonetic_confusion`: confirmed personal accent/ASR confusion pairs.
|
||||
- `review_question`: transport-independent human review queue.
|
||||
- `source_document`: incremental indexing state for historical minutes.
|
||||
|
||||
Obsidian remains a curated source. Hindsight supplies broad recall. SQLite holds
|
||||
small deterministic facts that applications can safely query.
|
||||
|
||||
## Human review loop
|
||||
|
||||
The pipeline never blocks waiting for an answer. Ambiguous high-impact entity
|
||||
candidates are queued and the draft keeps an uncertainty marker.
|
||||
|
||||
```bash
|
||||
bin/asr-refine review pending
|
||||
bin/asr-refine review answer 12 万亚坤
|
||||
```
|
||||
|
||||
A confirmed candidate is learned as an alias, correction, and observation. The
|
||||
candidate-pool cache includes the database timestamp, so the next recording uses it
|
||||
immediately.
|
||||
|
||||
Pi, Telegram, and the web UI are thin transports over this queue:
|
||||
|
||||
1. Read pending rows and display the question, source sentence, and choices.
|
||||
2. Submit `question_id + answer`.
|
||||
3. Call the same `review.answer()` function or CLI command.
|
||||
4. Optionally request a replay of S3-S5 for the affected recording.
|
||||
|
||||
The production web transport is a persistent mobile control plane:
|
||||
|
||||
```bash
|
||||
bin/asr-refine review serve # normally started by systemd
|
||||
bin/asr-refine review state # print the active one-time URL
|
||||
```
|
||||
|
||||
- Every generated draft is a Web delivery, even when entity candidate detection
|
||||
finds no ambiguity. Review order is: inferred call background (with a corrected
|
||||
background field that requests a redraft), unnamed voiceprints, the draft's
|
||||
`## 待你确认` list, then one overall review item.
|
||||
- It listens on `192.168.50.145:8765`. `/` accepts streamed audio uploads,
|
||||
shows recent job state, and atomically hands completed files to the existing
|
||||
recording watcher. `/hook/intake` exposes the same raw-upload contract for a
|
||||
future reverse proxy or external uploader.
|
||||
- Telegram receives a 256-bit capability URL. The token exists only in Telegram and
|
||||
the mode-0600 runtime state file; it is redacted from service/request logs.
|
||||
- The page exposes only one queued question at a time: candidate buttons, an
|
||||
explicit "无法确认", a short custom answer, and an expandable full-draft view.
|
||||
- Rough drafts live under `state/drafts/`, outside the Vault and Git. After all
|
||||
questions are answered, an auditable feedback block is saved privately.
|
||||
`asr-review-revise@.service` applies those answers with Sonnet, validates the
|
||||
formal schema/Summary/provenance, then writes and pushes exactly one formal note
|
||||
under `minutes/`. The private reviewed source moves to `state/published/`.
|
||||
- A confirmed unnamed `speaker_N` is written to `voiceprint_speakers`, marked for
|
||||
retry by the daily enrollment job, and immediately submitted to the persistent
|
||||
`:10099/voiceprints/enroll` API.
|
||||
- SQLite records one `entity_mention` per entity per formal minute. When the current
|
||||
minute would bring an uncarded person/company to two distinct minutes, Web offers
|
||||
a conservative card scaffold. No card or judgment field is created without an
|
||||
explicit human choice.
|
||||
- Upload metadata (title and optional user-supplied background) is kept separate
|
||||
from the acoustic transcript. The title survives into the task slug; the
|
||||
background is explicitly labelled as user-supplied context for drafting.
|
||||
- `asr-review-web.service` is enabled at boot and restarts on failure. A successful
|
||||
private draft ensures it is running and sends its stable review URL through
|
||||
Telegram. `dry-run` never queues questions.
|
||||
|
||||
The service unit lives at `deploy/asr-review-web.service`. Telegram may later gain
|
||||
inline choice buttons, but neither transport owns facts or writes SQLite directly.
|
||||
|
||||
## Confidence policy
|
||||
|
||||
- Confirmed: voiceprint, explicit human mapping, or multiple independent strong
|
||||
signals.
|
||||
- Probable: context and relation graph agree, but identity is inferred.
|
||||
- Pending: weak or conflicting evidence, including numbers whose type/unit is not
|
||||
clear.
|
||||
|
||||
Only confirmed facts become deterministic aliases automatically.
|
||||
|
||||
## Graph and recency
|
||||
|
||||
```bash
|
||||
bin/asr-refine graph sync
|
||||
bin/asr-refine graph stats
|
||||
```
|
||||
|
||||
The graph incrementally scans finalized minutes, imports person/company/project
|
||||
frontmatter and known wikilinks, records meeting interactions, creates low-confidence
|
||||
co-occurrence edges, and promotes explicit linked relations such as 投资/负责/合作 at a
|
||||
higher confidence. Deleted or changed notes remove only facts owned by that source.
|
||||
|
||||
Candidate context weight combines relation confidence, repeated support, last contact,
|
||||
and a 45-day half-life. It influences ranking but cannot activate the personal
|
||||
phonetic-confusion channel outside entities already in the current graph context.
|
||||
|
||||
## Numbers
|
||||
|
||||
The deterministic number pass classifies percentage, date, money, quantity, and
|
||||
project/code speech. Every decision records raw value, normalized value, type,
|
||||
confidence, source span, and nearby context. Ambiguous forms such as `两万五九份`
|
||||
remain unchanged and are passed to the uncertainty stage instead of being guessed.
|
||||
@@ -0,0 +1,58 @@
|
||||
# 录音上传与审核工作台
|
||||
|
||||
## 入口
|
||||
|
||||
- Web:`http://192.168.50.145:8765/`
|
||||
- 健康检查:`GET /health`
|
||||
- 最近任务:`GET /api/jobs`
|
||||
- 人工确认:`GET /review/`
|
||||
|
||||
服务只面向 LAN 与 tailnet,没有实现公网身份认证。未来暴露到公网时,应在反向代理层
|
||||
增加认证、上传大小限制和请求速率限制,不要直接转发当前端口。
|
||||
|
||||
## 工作流
|
||||
|
||||
1. 浏览器把录音以原始二进制流上传到隐藏临时目录。
|
||||
2. 文件完整写入并 `fsync` 后,原子移动到 `recordings/inbox/`。
|
||||
3. 既有 `obsidian-recording-watch.service` 接管转写、CAM++ 分角色和声纹识别。
|
||||
4. 转写落入 `.asr-inbox/`,独立整理程序生成私有草稿。
|
||||
5. Telegram 发送确认链接;Web 回答写入 SQLite。
|
||||
6. 全部确认后生成正式纪要并写入 Obsidian。
|
||||
|
||||
上传时填写的背景单独保存在任务元数据中,进入整理提示时会标为“用户提供的背景”,
|
||||
不会混入或伪装成 ASR 转写内容。
|
||||
|
||||
## Hook
|
||||
|
||||
`POST /hook/intake` 与 Web 上传使用相同的流式协议,便于以后在外网部署一个很薄的上传
|
||||
前端或反向代理:
|
||||
|
||||
```bash
|
||||
curl --fail-with-body \
|
||||
-X POST \
|
||||
-H 'Content-Type: application/octet-stream' \
|
||||
-H 'X-Filename: call.m4a' \
|
||||
-H 'X-Title: 项目通话' \
|
||||
-H 'X-Context: 参与人、项目和希望重点确认的事项' \
|
||||
--data-binary @call.m4a \
|
||||
http://192.168.50.145:8765/hook/intake
|
||||
```
|
||||
|
||||
响应中的 `job_id` 是任务标识。状态可从 `/api/jobs` 查询。
|
||||
|
||||
## 运行
|
||||
|
||||
```bash
|
||||
systemctl --user status asr-review-web.service
|
||||
systemctl --user restart asr-review-web.service
|
||||
journalctl --user -u asr-review-web.service -n 100 --no-pager
|
||||
curl -fsS http://127.0.0.1:8765/health
|
||||
```
|
||||
|
||||
持久数据:
|
||||
|
||||
- `asr-pipeline/state/intake.db`:上传任务与状态。
|
||||
- `recordings/state/uploads/`:上传标题、背景和任务关联。
|
||||
- `recordings/inbox/.uploading/`:未完成上传;正常完成后为空。
|
||||
|
||||
默认单文件上限为 4 GiB,可通过 `ASR_UPLOAD_MAX_BYTES` 调整。
|
||||
@@ -0,0 +1,80 @@
|
||||
# 当前流程与独立项目交接
|
||||
|
||||
核对日期:2026-11-02,Asia/Shanghai。
|
||||
|
||||
## 已验证
|
||||
|
||||
原代码:`/home/claw/claude-workspace/OBSIDIAN/asr-pipeline`。
|
||||
原音频编排:`/home/claw/claude-workspace/OBSIDIAN/recordings/bin`。
|
||||
原通知脚本:`/home/claw/claude-workspace/OBSIDIAN/bin/meeting-status-notify.py`。
|
||||
|
||||
本机 systemd:
|
||||
|
||||
| 单元 | 本次状态 | 职责 |
|
||||
|---|---|---|
|
||||
| asr-review-web.service | running | 8765 常驻上传、最近任务、审核界面 |
|
||||
| obsidian-recording-watch.service | running | 监听录音并请求外部 ASR |
|
||||
| obsidian-asr-refine.path | waiting | 监听 vault/.asr-inbox |
|
||||
| obsidian-asr-refine-native.service | inactive | 按需整理转写,当前未运行 |
|
||||
| asr-review-revise@.service | 模板 | 人工确认后生成/发布 |
|
||||
|
||||
`GET http://127.0.0.1:8765/health` 返回 `ok=true, pending=3`。
|
||||
这说明审核队列存在待确认项,不证明三个录音都在推理,也不证明通知正常。
|
||||
本轮不导出待确认正文,不生成新稿、不注册声纹、不写 vault。
|
||||
|
||||
## 代码确认的链路
|
||||
|
||||
- 监听单元显式选择 `MEETING_ASR_BACKEND=qwen3cam`。
|
||||
- 对应脚本默认请求 `http://192.168.50.100:10099/asr`,整文件上传,
|
||||
`language=Chinese`、`save_voiceprint_session=true`;服务内部切片。
|
||||
本轮未对 GPU 服务发起真实录音测试,不据此宣称其健康或当前镜像版本。
|
||||
- 原始转写保留声纹 session 与 speaker;归档后桥接到 vault/.asr-inbox。
|
||||
- 独立 Python 管线包含模糊音/别名、关系与近期权重、数字归一、纯清洗与纠错分阶段。
|
||||
- 初稿和中间状态在程序私有目录,正式纪要等待 Web 审核通过。
|
||||
- 人物/公司卡提议与声纹确认存在代码路径;不等于当前外部注册服务验证通过。
|
||||
|
||||
## 运行数据边界
|
||||
|
||||
- `asr-pipeline/state/lexicon.db`:人工确认、关系、纠错、审核队列,不能重建后替代。
|
||||
- `asr-pipeline/state/intake.db`:上传任务与状态。
|
||||
- `state/drafts/`、`state/published/`:私有稿与发布回执。
|
||||
- `recordings/{inbox,processing,processed,failed,transcripts,state}`:音频及编排状态。
|
||||
- `vault/.asr-inbox/.state`:整理处理标记,切换漏迁可能造成重复处理。
|
||||
- `.asr-refine.env`:私有凭据,未读取值、未复制、未入 Git。
|
||||
|
||||
本次只提取源代码、专业词库、现有测试、文档、unit 快照。没有复制生产音频、
|
||||
个人 SQLite、缓存、会话、凭据或 `.venv`。历史设计文档有过时路径和模型,保留作来源。
|
||||
|
||||
## 已发现的风险(尚未改生产)
|
||||
|
||||
1. 通知脚本默认 `127.0.0.1:8644` 和 `~/.hermes/webhook_subscriptions.json`。
|
||||
结合 Hermes 已迁走的资产记录,这是通知失效风险;本次未验证远端 webhook。
|
||||
2. 本仓库代码的模型缺省已是 LiteLLM(清洗 `aistudio/gemini-flash-lite-latest`,成稿 `codex/gpt-sol-latest`),不是 ZenMux,也不是 Sonnet 5。生产 unit 若仍加载旧 `.asr-refine.env`,以那个文件为准;本次未读取凭据,不把代码缺省当成现网正在用的路由。
|
||||
3. 路径耦合旧工作目录;本项目配置根目录、通知器、监听数据库已可外置,
|
||||
但声纹地址、审核 systemd 单元名等仍有历史耦合。`deploy/systemd/` 保持现网快照,没有改 ExecStart。
|
||||
4. 历史 HANDOFF 中写的是一次性页面。代码里有两个常驻界面:旧 `asrpipe.control_web`(快照单元,默认 8765)和新 `asrservice.web`(compose,默认 8788)。旧文档不作现态。
|
||||
5. 产物删除和在线编辑会拒绝允许根以外的路径。发布到 `minutes/`、`People/`、`Companies/` 仍没有对照 vault SOP 的不可绕过白名单。本轮不冒充已审计完成。
|
||||
|
||||
## 代码已修复、生产未切换
|
||||
|
||||
下面这些是仓库里的新行为。现网单元没有改,不能当成已经在 8765/8788 的生产进程上生效。
|
||||
|
||||
- 未设置 `ASR_WEB_PASSWORD` 时,两个浏览器界面都拒绝上传、审核和改配置。临时放开只认 `ASR_WEB_ALLOW_ANON=1`。
|
||||
- 登录后的写操作校验 CSRF。设置页不回显内网地址和密钥片段。
|
||||
- `asrservice.worker` 与 `recordings/bin/meeting-watch.py` 抢同一把 `consumer.lock`。
|
||||
- `ASR_GIT_SYNC`、`ASR_NOTIFY` 缺省为关。
|
||||
- 修订失败会保留队列标记并重试;重新精修失败不会先删掉已回答的审核记录。
|
||||
- 时间在界面上按 Asia/Shanghai 显示。词库列表按页读取,不再一次只截断前 180 条却不说明总数。
|
||||
|
||||
## 后续安全切换
|
||||
|
||||
1. 先确定模型路由与通知适配器,检查 vault SOP 与正式输出路径。
|
||||
2. 建独立 venv、冻结依赖,使用合成数据跑回归,再选真实录音单测。
|
||||
3. 停接新上传,确认没有转写/整理/审核发布在运行,再停相关 user units。
|
||||
4. 对 SQLite 使用 backup API 生成一致性备份,保留原件;迁移录音状态与 inbox 标记,
|
||||
不能对运行中数据库只复制主文件。
|
||||
5. 路径变更还涉及数据库内草稿/转写绝对路径;要审计和映射,不能仅改 unit。
|
||||
6. 生成新 unit 并逐个启用,确认只存在一个消费者;保留旧代码和备份以便回滚。
|
||||
7. 验证上传 → 转写 → 确认链接 → 审核 → 正式纪要 → Git 同步完整链路后再移除旧目录。
|
||||
|
||||
本轮未执行上述生产切换,不承诺已独立上线。
|
||||
@@ -0,0 +1,55 @@
|
||||
# 独立 WebUI 项目路线
|
||||
|
||||
## 定位
|
||||
|
||||
独立的会议、电话录音处理应用。WebUI 是主要操作界面,Telegram 只做可选通知,
|
||||
不依附任何 Agent CLI。当前源码已抽出,但仍是旧实现基线,尚未独立部署上线。
|
||||
|
||||
## 目标架构
|
||||
|
||||
浏览器 → 应用 API → 持久化任务队列 → 后台 Worker → 外部 ASR / LLM / Hindsight。
|
||||
SQLite 保存任务、处理阶段、审核、实体词库和修改历史;原音频、中间产物单独保存。
|
||||
Obsidian 是可选发布目标,不是应用内部的任务队列或运行数据库。
|
||||
|
||||
ASR GPU 服务继续位于 unRaid,模型调用优先接入统一 LiteLLM。
|
||||
应用状态、配置和存储必须通过环境配置,不绑定特定机器或个人工作区。
|
||||
|
||||
## 核心页面
|
||||
|
||||
1. 任务列表:上传、背景输入、进度、失败原因、重试、最近任务。
|
||||
2. 任务详情:原音频播放、带说话人与时间轴的原文、处理版本、来源。
|
||||
3. 审核:背景确认、候选词、说话人身份、关键歧义、最终纪要确认。
|
||||
4. 词库:实体、别名、模糊音规则、关系和人工确认历史。
|
||||
5. 设置:ASR、LLM、Hindsight、通知、输出路径和数据保留策略。
|
||||
|
||||
## 实施顺序
|
||||
|
||||
### 第一阶段:独立运行闭环
|
||||
|
||||
- 配置与运行数据分离;迁移个人词库、审核状态时使用一致性备份。
|
||||
- 将 systemd 路径监听/模板任务改为应用内部持久化队列与 Worker。
|
||||
- 实现任务互斥、幂等、重启恢复、超时和阶段重试,不重跑已确认步骤。
|
||||
- 修复独立通知适配器,停止依赖本机 Hermes 状态文件。
|
||||
- 对照现有 vault SOP,审计输出路径;只有确认后的正式稿可发布。
|
||||
- 上传、转写、审核、发布全链路通过后,再切换生产消费者。
|
||||
|
||||
### 第二阶段:WebUI 完整化
|
||||
|
||||
- 保留现有纯 Python 页面作过渡,先明确 API 和状态模型,再选前端框架。
|
||||
- 增加播放器、原文对照、编辑、版本回退与清晰的阶段状态。
|
||||
- 把说话人确认、词库维护及上下文确认放入一致的任务工作流。
|
||||
|
||||
### 第三阶段:Docker 发布
|
||||
|
||||
- API/WebUI 和 Worker 可分别运行;持久卷承载 SQLite 与音频。
|
||||
- 不在容器内运行 systemd;不依赖宿主 Agent home。
|
||||
- Obsidian 发布器可持有专用 Git checkout,不直接修改主工作副本。
|
||||
- 确定 Dockerfile 与验收测试后,再接 GitHub Actions + GHCR 构建发布。
|
||||
|
||||
## 验收原则
|
||||
|
||||
原文不可覆盖;候选纠错须有依据;背景不伪装为录音事实;重要歧义交人工确认。
|
||||
以关键内容和行动线索为目标,不追求伪精确的逐字恢复。
|
||||
合成测试与真实录音端到端验收分开,不能以单元测试通过代替上线验收。
|
||||
|
||||
本文件是后续开发计划,本轮不新增推理请求、不切换生产服务、不自动部署容器。
|
||||
Reference in New Issue
Block a user