feat(qq_obv11): 出站语音发真 record 段(base64 内联优先,读不到的 file 形态提前拒绝)
M59 QQRECORD:adapter.Media 里 Kind=voice 的条目落成 {"type":"record"} 段。正文仍是独立的 text 段排在 record 前面(QQ 语音是独立气泡,正文既不塞进 record 也不被吞掉)。
record 段 file 参数三档裁决(outboundRecordFile,本片核心决策):
1. Data 非空 => base64://+标准 base64。出站语音字节本就在本进程内产生(TTS),内联不依赖跨进程共享文件系统(Napcat 可能在容器/另一台机器),也不需要插件知道框架的 $DATA_DIR(插件没有那个配置面)。
2. 原样透传:已是 base64://、http(s)://、或 filepath.IsAbs 的本地绝对路径 —— 这三种 OneBot 实现自己能解析,一个字节都不改写。
3. 明确拒绝:服务端相对路径(/api/v1/media/<name>)、裸文件名、空值,提前失败并说明要什么。前缀判定必须排在 IsAbs 之前 —— Linux 上 /api/v1/media/x.wav 会被 IsAbs 判成绝对路径,透传只会得到一条看不出根因的平台错误。注:框架语音副本(voice.SaveAudio)与文档同目录、同内容寻址、同一张 documents 表,不存在「服务端相对路径 -> 本地绝对路径」的公开映射,所以拒绝该形态是正确裁决而非偷懒。
重构:outboundSegments 从 message.go 迁到 outbound_segments.go(message.go 489 行,再加代码会破 500 行上限),并收掉原实现里不可达的内层分支(len(segs)==0 时 Content 必为 "",补 text 段那条永远走不到)—— 行为不变。
分层:生产代码不 import internal/runtime/voice(渠道插件反向依赖运行时存储层会把分层倒过来),serverMediaPathPrefix 重复一份字面量,由测试交叉断言钉住同值。
测试:7 个用例(形状/顺序/透传/拒绝/零变化/前缀同值)。零变化用硬编码期望值而非快照文件。变异实测:删 MediaVoice 分支 => 5 个 voice 用例红;base64 编码换成空切片 => record file 断言红;删前缀判定 => server-relative 用例因文案不含 "server-relative" 红。均已恢复。
⚠️ 未在本机验证(无 Napcat 环境):WAV/MP3 直发是否被 Napcat 接受、silk 转码是否成功 —— 本片只钉拼装形状,需真机验证。
验收:go build ./... + go vet ./... + go test ./... 全绿(worktree wt-59)。
This commit is contained in:
1 parent
07df06b86d
commit
eca1f33d8c
3 files changed
+361
-34
No files matched your search
@@ -1,11 +1,12 @@
|
||||
package qq_obv11
|
||||
|
||||
// This file carries OneBot v11 protocol types and the conversions between the
|
||||
// native event/segment model and the normalized adapter.UnifiedMessage /
|
||||
// UnifiedResponse model. It understands both array-format messages
|
||||
// ([]{"type","data"}) and the legacy CQ-code string format
|
||||
// ([CQ:image,file=...]text), and maps QQ media (image/record/video/file) into
|
||||
// adapter.Media and QQ mentions into adapter.Mention.
|
||||
// This file carries OneBot v11 protocol types and the conversions from the
|
||||
// native event/segment model into the normalized adapter.UnifiedMessage model.
|
||||
// It understands both array-format messages ([]{"type","data"}) and the legacy
|
||||
// CQ-code string format ([CQ:image,file=...]text), and maps QQ media
|
||||
// (image/record/video/file) into adapter.Media and QQ mentions into
|
||||
// adapter.Mention. 反方向的「统一消息 → 出站消息段」在 `outbound_segments.go`
|
||||
// (入站与出站各有各的知识面,不混在一个文件里)。
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@@ -371,34 +372,6 @@ func mediaFromParams(p map[string]string, kind adapter.MediaKind) adapter.Media
|
||||
return adapter.Media{Kind: kind, URL: url, Name: file}
|
||||
}
|
||||
|
||||
// outboundSegments converts a unified message into OneBot message segments.
|
||||
func outboundSegments(msg adapter.UnifiedMessage) ([]obSegment, error) {
|
||||
var segs []obSegment
|
||||
if msg.Content != "" {
|
||||
segs = append(segs, obSegment{Type: "text", Data: map[string]any{"text": msg.Content}})
|
||||
}
|
||||
for _, m := range msg.Media {
|
||||
file := firstNonEmpty(m.URL, m.Name)
|
||||
switch m.Kind {
|
||||
case adapter.MediaImage:
|
||||
segs = append(segs, obSegment{Type: "image", Data: map[string]any{"file": file}})
|
||||
case adapter.MediaVoice:
|
||||
segs = append(segs, obSegment{Type: "record", Data: map[string]any{"file": file}})
|
||||
case adapter.MediaVideo:
|
||||
segs = append(segs, obSegment{Type: "video", Data: map[string]any{"file": file}})
|
||||
case adapter.MediaFile:
|
||||
segs = append(segs, obSegment{Type: "file", Data: map[string]any{"file": file}})
|
||||
}
|
||||
}
|
||||
if len(segs) == 0 {
|
||||
if msg.Content == "" {
|
||||
return nil, fmt.Errorf("qq-obv11: message has no content or media: %w", adapter.ErrUnsupported)
|
||||
}
|
||||
segs = append(segs, obSegment{Type: "text", Data: map[string]any{"text": msg.Content}})
|
||||
}
|
||||
return segs, nil
|
||||
}
|
||||
|
||||
// resolveTarget determines where a reply should be delivered.
|
||||
func resolveTarget(msg adapter.UnifiedMessage) (messageType string, userID, groupID int64, err error) {
|
||||
if msg.ChannelType == adapter.ChannelGroup {
|
||||
|
||||
Reference in new issue
Block a user