Files
Cyrene_v2/internal/db/migrations/0020_chat_message_voice.up.sql
AskaEth ede00d9928 feat(voice): 语音消息落库 + 转码接入(切片 0020)
- 迁移 0020_chat_message_voice:按 design/24 §10 与契约 §7.2 已登记的 voice 形状建列/索引
- 语音回合进编排并落库;voice 引用与统计如实回填(转码/转写没成功就如实标未成功,不假装)
- 复用既有 internal/capability/transcode:非 wav(微信 AMR 等)先转码再送 ASR;失败可分类且不丢消息

⚠️ 部分完成(如实登记):生产端到端未成立(本片只到落库与转码接入),且**未经验真机**。
2026-10-06 09:04:06 +08:00

49 lines
3.5 KiB
SQL
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
-- 0020_chat_message_voice: 给聊天消息挂上「语音事实」(design/24 §10.4;切片 CODE-VOICE)。
--
-- 一条语音消息在历史里要能被辨认成语音(客户端据此显示 🎤 与播放控件),并且要能
-- 说清它的**正文从哪来**。这两件事都是「整条消息」的属性,不是某个内容片段的属性,
-- 因此落在消息行的一列上(与 0015 的 document 同族):
--
-- {"source":"asr","status":"transcribed","name":"a1b2…ogg","mime":"audio/ogg",
-- "size":12345,"durationMs":3200}
--
-- JSON **单对象**(不是数组:一条消息只有一条被归一/落副本的语音,design/24 §10.1
-- 的 N7 明确「第 2..N 条语音既不转写也不落盘」),形状即 internal/runtime/voice 的
-- Ref —— 它就是历史 DTO 里 `voice` 对象**减去 url** 的那份数据(url 由服务端按 name
-- 拼出,绝不落库)。
--
-- 字段语义(逐条见 design/24 §10.4):
-- source 转写文本从哪来:asr(服务端语音识别)/ platform(渠道平台自带转写)。
-- status 转写结果三态闭集:transcribed / empty / failed。客户端按它分支,
-- **绝不按正文文案分支**。
-- name 音频副本的服务端存储名(内容寻址、32 位小写十六进制 + 音频白名单扩展名)。
-- mime 保存时成对裁决所得的下发 MIME(audio/*)。
-- size 副本字节数。name/mime/size 三者**同生共死**:有副本时全在,无副本时全空
-- (绝不出现「有 url 指着一个不存在的 name」这种形状)。
-- durationMs 只有渠道**如实给出**或转码产物**可确证**时才 >0,绝不估算。
--
-- 列口径与同族可选列(document / turn_id / client_msg_id)逐字一致:
-- `NOT NULL DEFAULT ''`,**绝不用 NULL** —— 于是「老行」与「这条消息不是语音」是同一个
-- 值,读侧只有一种空情形要处理(空串 ⇒ DTO 里 `voice` 键**缺席**)。
--
-- 这一列**不需要索引**:它只被随行读出,没有任何按它过滤/排序/聚合的查询(与 0018 的
-- turn_id 同一口径)。引用计数的增减发生在消息写路径上按**新旧列内容**逐条解码比较
-- (design/24 §10.5 的加粗警告),不走 SQL 过滤;因此建索引只会给每次写加一笔无收益的
-- 维护开销。
--
-- ⚠️ 引用计数(design/24 §10.5,本列落地时**必须同时做**,否则音频副本会静默坏掉):
-- `history.refs` 的 retain/release/sync 今天只认 `document` 列;voice 列里的 name 必须
-- 并入**同一次**增减(与消息行同一事务、按新旧名字集合的差增减、保留重复),否则
-- 音频副本的 ref_count 恒为 0 ⇒ GC 在保留期后删掉仍被消息引用的音频 ⇒ 历史里的语音
-- 变成永远播不出来的死链,而且**没有任何错误**。
--
-- 回填(本迁移不做,也不该做):老行一律空串 = 「不是语音消息」,行为与加入本列之前
-- 逐字相同。存量语音消息的正文是转写文本,但没有来源事实可考 —— 凭空补一个
-- `{"source":"asr",...}` 就是编造(与「绝不假装」的口径冲突)。
--
-- 0003 的 chat_messages 表在此已存在,故 ALTER ADD COLUMN 是非破坏性的正确一步;
-- 全新库由 internal/runtime/history.NewStore 幂等地前滚(含该列)。表名沿用复数口径
-- (援引 AGENTS §10 已登记的例外,与既有迁移一致)。
ALTER TABLE chat_messages ADD COLUMN voice TEXT NOT NULL DEFAULT '';