package transcode import ( "context" "encoding/base64" "fmt" "git.yeij.top/AskaEth/Cyrene_v2/pkg/plugins/sdk" ) // 本文件是 design/25 §1.3.2 的**第二层注册**:把转码暴露成 agent 可选用的内置 // 工具(KindTool),走**既有**的 sdk.Registrar → internal/capability/registry。 // 与 vision.RegisterTools / io.Register 逐字同族,因此面板、策略、审计三面零新增 // 代码即覆盖;**不新增 Kind**(validKind 闭集不动),策略默认零值 = owner-only。 // // ⚠️ 本工具是 design/25 §8-T1 的「RegisterTool 壳」:T4 定义的形态是 // `{name, to}`(服务端存储名进、新存储名出,薄壳:读存储 → 调 core → 写存储), // 它依赖 design/24 §10 的音频副本存储与 `GET /api/v1/media/{name}`——那两件**尚未 // 落地**。因此本壳用「字节进、字节出」(base64 承载)先把**注册链路**打通并可测, // T4 落地后再换入参形态;这条差异已登记在交接报告里。 // // 红线提醒:转码产物是**字节**,不进上下文。因此本壳工具另设一个远小于 // MaxOutputBytes 的结果上限(MaxToolResultBytes),让「工具结果」不可能把上下文 // 灌爆——要转大文件请走进程内 DI 路径,不要走模型工具面。 const ( // ToolName 是转码工具在全局能力注册表里的名字。 ToolName = "transcode" // MaxToolResultBytes 是本壳工具允许回给模型的产物体积上限(解码后字节数)。 // 它**不是** design/25 §2.5 的 MaxOutputBytes(那是能力上限,64 MiB);这里更小 // 是因为工具结果会进模型上下文。 MaxToolResultBytes = 256 << 10 ) // 入参字段名。抽成常量以免同一个键在三处各写一份字符串。 const ( toolArgData = "data_base64" toolArgSource = "source" toolArgTarget = "target" ) // Tool 把 Transcoder 包装成 sdk.Tool,使 agent 能直接调用转码(design/25 §9-2 // 默认「暴露,owner-only」)。它可安全并发使用。 type Tool struct { tr Transcoder } // Compile-time assertion that *Tool satisfies the SDK Tool interface. var _ sdk.Tool = (*Tool)(nil) // NewTool 用给定转码器构造工具。 func NewTool(tr Transcoder) *Tool { return &Tool{tr: tr} } // Name 返回工具标识。 func (t *Tool) Name() string { return ToolName } // Description 说明该工具做什么。它让模型知道「转码是本地能力、同步、不看身份」。 func (t *Tool) Description() string { return "Convert audio bytes between formats locally (e.g. amr -> wav). " + "Input and output are base64-encoded bytes; the result carries format, " + "sample rate, channels, duration and frame count." } // Parameters 返回输入 JSON Schema:data_base64(必填)+ target(必填)+ source(可选)。 func (t *Tool) Parameters() (map[string]any, error) { return map[string]any{ "type": "object", "properties": map[string]any{ toolArgData: map[string]any{ "type": "string", "description": "The source audio bytes, base64-encoded (standard alphabet).", }, toolArgTarget: map[string]any{ "type": "string", "description": "Target format name, lowercase and without a dot, e.g. \"wav\".", }, toolArgSource: map[string]any{ "type": "string", "description": "Optional source format name; omitted means sniff by magic bytes.", }, }, "required": []any{toolArgData, toolArgTarget}, }, nil } // Execute 解码入参、调用转码器,并把结果包成模型可读的对象。 // 失败一律返回 errors.go 的哨兵(经 %w 保留),绝不假装成功。 func (t *Tool) Execute(ctx context.Context, args any) (any, error) { if t.tr == nil { return nil, fmt.Errorf("transcode: tool %q: %w: nil transcoder", ToolName, ErrUnsupportedFormat) } req, err := toolRequest(args) if err != nil { return nil, err } res, err := t.tr.Transcode(ctx, req) if err != nil { return nil, fmt.Errorf("transcode: tool %q: %w", ToolName, err) } if len(res.Data) > MaxToolResultBytes { return nil, fmt.Errorf("transcode: tool %q: %w: result %d bytes > %d (tool face is bounded; use the in-process API for larger audio)", ToolName, ErrOutputTooLarge, len(res.Data), MaxToolResultBytes) } return map[string]any{ "format": FormatName(res.Format), "source": FormatName(res.Source), "data_base64": base64.StdEncoding.EncodeToString(res.Data), "sample_rate": res.SampleRate, "channels": res.Channels, "duration_ms": res.DurationMs, "frames": res.Frames, }, nil } // RegisterTool 经 SDK 辅助函数把转码工具注册到 r(design/25 §1.3.2 第二层)。 // 与 vision.RegisterTools 同族:tr 为 nil 时不注册(返回 nil,不制造一个必然 // 失败的能力条目)。 func RegisterTool(r sdk.Registrar, tr Transcoder) error { if r == nil { return sdk.ErrNilRegistrar } if tr == nil { return nil } tool := NewTool(tr) if err := sdk.RegisterTool(r, tool); err != nil { return fmt.Errorf("transcode: register tool %q: %w", tool.Name(), err) } return nil }