feat(channels,voice): IM 各渠道语音消息可被读到(全渠道云端 ASR + 临时熔断)
采集面:mediafetch 新增 inline_voice.go(音频白名单,与 documents.ExtAllowed 互不污染; 复用既有来源白名单 / 逐跳校验 / 边读边限 / 超时);四个适配器零改动。 消费面:channelbridge 在流水线最前端把「语音字节 → 文本」归一并写回 msg.Content; inbound.go 的丢空判据放宽为「无文本且无可用媒体」。 微信优先平台自带转写(Content 非空 ⇒ 不调 ASR,不重复付费)。 降级(主人定的两层):新增 voice/outcome.go 的 STTOutcome 识破静默档位; errors.go 分类鉴权失败/欠费/限流(CircuitWorthy);voice_breaker.go 临时熔断 + 退避(TTL 恢复); 无论是否熔断,消息面都落一行中文诚实提示(绝不静默、绝不留空消息)。 独立短超时:VOICE_STT_TIMEOUT_SEC(默认 20s,同步 .env.example)。 kook 不再把 CDN 地址写进 Content(地址仍在 Media[].URL)。 设计先行:design/06 §1.1/§1.2、design/08 §4.1、design/24 §1.3/§4.2、开源对照清单。 变异 5 条(静默档位不带 Cause / Transcribed 误判 / 熔断恒放行 / ChatText 退化 / 平台文字判据取反)。 诚实边界:实施时官方音频规格尚未抓到 ⇒ MaxVoiceBytes 自定 5 MiB(保守)、采样率仍 16 kHz; 官方规格随后已抓到(10 MB / 5 min;格式含 amr、无 silk),对齐留待下次改动。
This commit is contained in:
1 parent
679cee04f7
commit
a918819878
20 files changed
+1685
-61
No files matched your search
@@ -149,6 +149,13 @@ type Options struct {
|
||||
// 被移入某个角色的作用域(design/22 §7-4)。为 nil 时所有渠道都使用全局
|
||||
// 默认角色。
|
||||
ChannelPersona func(platform, accountID string) string
|
||||
// VoiceSTT 是**入站语音转写**的注入点(切片 IM-ASR;design/06 §1.1)。组合根注入
|
||||
// `*voice.Chain`(与 Web 侧 WS 的 voice_input 同一条降级链)。为 nil 时,带语音的
|
||||
// 消息走「服务端未配置云端语音识别」的**诚实提示**路径 —— 绝不静默丢消息。
|
||||
VoiceSTT VoiceSTT
|
||||
// VoiceSTTTimeout 是一次 IM 入站语音转写的超时上限(≤0 时取默认 20s)。入站流水线
|
||||
// 会为这次调用举着一个并发槽,因此上游卡住**不能**拖死整条流水线。
|
||||
VoiceSTTTimeout time.Duration
|
||||
// DocumentStore 是 IM **入站附件**的落盘配置(design/24 §3;切片 M1):
|
||||
// 目录、单文件上限与 `documents` 元数据写入面。它由组合根给出(桥接器不猜
|
||||
// 目录布局),并与 Web 上传共用同一个存储核心。UploadsDir 与 DataDir 都为
|
||||
@@ -205,6 +212,13 @@ type Bridge struct {
|
||||
// documentsCfg 是入站附件落盘配置(design/24 §3;切片 M1)。零值 = 未接线:
|
||||
// 带文件的消息照旧能跑,模型会收到一行「读不到」的诚实提示(绝不静默)。
|
||||
documentsCfg documents.StoreConfig
|
||||
// voiceSTT / voiceSTTTimeout / voiceCircuit 是入站语音转写面(切片 IM-ASR;
|
||||
// design/06 §1.1):注入点、独立短超时,以及按原因分类的**临时熔断**。
|
||||
// voiceLog 是带 `services/channelbridge/voice` 来源的子功能日志器。
|
||||
voiceSTT VoiceSTT
|
||||
voiceSTTTimeout time.Duration
|
||||
voiceCircuit voiceBreaker
|
||||
voiceLog *slog.Logger
|
||||
// proactiveCursor 是主动推送「无信号」兜底时的轮转游标
|
||||
// (channelbridge/proactive.go),使默认渠道不会总是
|
||||
// 白名单中的第一个主人渠道。由 mu 保护。
|
||||
@@ -251,34 +265,37 @@ func New(opts Options) (*Bridge, error) {
|
||||
engageRand = func() float64 { return rand.Float64() }
|
||||
}
|
||||
return &Bridge{
|
||||
log: logger.WithSource(log, "services/channelbridge"),
|
||||
run: opts.Loop,
|
||||
sessions: opts.Sessions,
|
||||
owner: opts.Owner,
|
||||
history: opts.History,
|
||||
onPersisted: opts.OnPersisted,
|
||||
onRejected: opts.OnRejected,
|
||||
review: opts.Review,
|
||||
light: opts.Light,
|
||||
quietStartHour: qStart,
|
||||
quietEndHour: qEnd,
|
||||
now: now,
|
||||
triggerWords: opts.TriggerWords,
|
||||
personaPrompt: opts.PersonaPrompt,
|
||||
engage: opts.Engage,
|
||||
engageFor: opts.EngageFor,
|
||||
engageRand: engageRand,
|
||||
bubbleGapFn: bubbleGap,
|
||||
engageGroups: make(map[string]*engageGroupState),
|
||||
adapters: make(map[string]adapter.PlatformAdapter),
|
||||
connected: make(map[string]bool),
|
||||
readCancels: make(map[string]context.CancelFunc),
|
||||
gates: make(map[string]*adapter.FlowGate),
|
||||
factory: opts.Factory,
|
||||
channelPersona: opts.ChannelPersona,
|
||||
adapterPersona: make(map[string]string),
|
||||
documentsCfg: opts.DocumentStore,
|
||||
cancel: func() {},
|
||||
sem: make(chan struct{}, maxConcurrent),
|
||||
log: logger.WithSource(log, "services/channelbridge"),
|
||||
run: opts.Loop,
|
||||
sessions: opts.Sessions,
|
||||
owner: opts.Owner,
|
||||
history: opts.History,
|
||||
onPersisted: opts.OnPersisted,
|
||||
onRejected: opts.OnRejected,
|
||||
review: opts.Review,
|
||||
light: opts.Light,
|
||||
quietStartHour: qStart,
|
||||
quietEndHour: qEnd,
|
||||
now: now,
|
||||
triggerWords: opts.TriggerWords,
|
||||
personaPrompt: opts.PersonaPrompt,
|
||||
engage: opts.Engage,
|
||||
engageFor: opts.EngageFor,
|
||||
engageRand: engageRand,
|
||||
bubbleGapFn: bubbleGap,
|
||||
engageGroups: make(map[string]*engageGroupState),
|
||||
adapters: make(map[string]adapter.PlatformAdapter),
|
||||
connected: make(map[string]bool),
|
||||
readCancels: make(map[string]context.CancelFunc),
|
||||
gates: make(map[string]*adapter.FlowGate),
|
||||
factory: opts.Factory,
|
||||
channelPersona: opts.ChannelPersona,
|
||||
adapterPersona: make(map[string]string),
|
||||
documentsCfg: opts.DocumentStore,
|
||||
voiceSTT: opts.VoiceSTT,
|
||||
voiceSTTTimeout: voiceSTTTimeout(opts.VoiceSTTTimeout),
|
||||
voiceLog: logger.WithSource(log, "services/channelbridge/voice"),
|
||||
cancel: func() {},
|
||||
sem: make(chan struct{}, maxConcurrent),
|
||||
}, nil
|
||||
}
|
||||
Reference in new issue
Block a user