Files
2026-08-29 21:56:51 +08:00

232 lines
8.1 KiB
Go

package voiceprovider
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"strings"
"time"
)
type StartRequest struct {
TaskRef string `json:"task_ref"`
RoomID string `json:"room_id"`
DeviceUserID string `json:"device_user_id"`
AgentUserID string `json:"agent_user_id"`
ConversationID string `json:"conversation_id"`
EngineMode string `json:"engine_mode"`
ContextSummary string `json:"context_summary,omitempty"`
PromptBundleVersion string `json:"prompt_bundle_version,omitempty"`
AgentBundleVersion string `json:"agent_bundle_version_id,omitempty"`
PersonaVersion string `json:"persona_version_id,omitempty"`
VoiceVersion string `json:"voice_version_id,omitempty"`
ModelVersion string `json:"model_version_id,omitempty"`
RoutingVersion string `json:"routing_version_id,omitempty"`
SystemPrompt string `json:"system_prompt,omitempty"`
PersonaTone string `json:"persona_tone,omitempty"`
VoiceResourceRef string `json:"voice_resource_ref,omitempty"`
ModelResourceRef string `json:"model_resource_ref,omitempty"`
MemoryToolURL string `json:"memory_tool_url,omitempty"`
MemoryToolToken string `json:"memory_tool_token,omitempty"`
}
type CallbackEvent struct {
Provider string `json:"provider"`
EventID string `json:"event_id"`
TaskRef string `json:"task_ref"`
ConversationID string `json:"conversation_id"`
EventType string `json:"event_type"`
Status string `json:"status"`
OccurredAt time.Time `json:"occurred_at"`
Turns []FinalTurn `json:"turns,omitempty"`
Usage []UsageMetric `json:"usage,omitempty"`
}
type UsageMetric struct {
UsageCategory string `json:"usage_category"`
MetricCode string `json:"metric_code"`
Quantity string `json:"quantity"`
Unit string `json:"unit"`
}
type FinalTurn struct {
TurnKey string `json:"turn_key"`
SequenceNo int64 `json:"sequence_no"`
Speaker string `json:"speaker"`
StartAt time.Time `json:"start_at"`
EndAt time.Time `json:"end_at"`
FinalText string `json:"final_text"`
Interrupted bool `json:"interrupted"`
ProviderRef string `json:"provider_ref,omitempty"`
}
type StartResponse struct {
TaskRef string `json:"task_ref"`
Status string `json:"status"`
}
type StopRequest struct {
TaskRef string `json:"task_ref"`
RoomID string `json:"room_id,omitempty"`
ConversationID string `json:"conversation_id"`
Reason string `json:"reason"`
}
type StopResponse struct {
TaskRef string `json:"task_ref"`
Status string `json:"status"`
}
type QueryRequest struct {
TaskRef string `json:"task_ref"`
ConversationID string `json:"conversation_id"`
}
type QueryResponse struct {
TaskRef string `json:"task_ref"`
Status string `json:"status"`
}
type Client interface {
Start(context.Context, StartRequest) (StartResponse, error)
Query(context.Context, QueryRequest) (QueryResponse, error)
Stop(context.Context, StopRequest) (StopResponse, error)
}
type Error struct {
Operation string
StatusCode int
Code string
Temporary bool
Cause error
}
func (e *Error) Error() string {
if e.Code != "" {
return fmt.Sprintf("provider %s returned %s: %v", e.Operation, e.Code, e.Cause)
}
if e.StatusCode > 0 {
return fmt.Sprintf("provider %s returned HTTP %d", e.Operation, e.StatusCode)
}
return fmt.Sprintf("provider %s failed: %v", e.Operation, e.Cause)
}
func (e *Error) Unwrap() error { return e.Cause }
func IsTemporary(err error) bool {
var providerError *Error
return errors.As(err, &providerError) && providerError.Temporary
}
func IsNotFound(err error) bool {
var providerError *Error
return errors.As(err, &providerError) && providerError.StatusCode == http.StatusNotFound
}
type HTTPClient struct {
baseURL string
apiKey string
client *http.Client
}
func NewHTTPClient(baseURL, apiKey string, timeout time.Duration) (*HTTPClient, error) {
if strings.TrimSpace(baseURL) == "" || strings.TrimSpace(apiKey) == "" || timeout <= 0 {
return nil, errors.New("provider base URL, API key and positive timeout are required")
}
return &HTTPClient{baseURL: strings.TrimRight(baseURL, "/"), apiKey: apiKey, client: &http.Client{Timeout: timeout}}, nil
}
func (c *HTTPClient) Start(ctx context.Context, input StartRequest) (StartResponse, error) {
body, err := json.Marshal(input)
if err != nil {
return StartResponse{}, err
}
request, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+"/mock/v1/voice-chats:start", bytes.NewReader(body))
if err != nil {
return StartResponse{}, err
}
request.Header.Set("Content-Type", "application/json")
request.Header.Set("X-Mock-Provider-Key", c.apiKey)
response, err := c.client.Do(request)
if err != nil {
return StartResponse{}, &Error{Operation: "start", Temporary: true, Cause: err}
}
defer response.Body.Close()
responseBody, err := io.ReadAll(io.LimitReader(response.Body, 1<<20))
if err != nil {
return StartResponse{}, &Error{Operation: "start", Temporary: true, Cause: err}
}
if response.StatusCode < 200 || response.StatusCode >= 300 {
return StartResponse{}, &Error{Operation: "start", StatusCode: response.StatusCode, Temporary: response.StatusCode == http.StatusTooManyRequests || response.StatusCode >= 500}
}
var result StartResponse
if err := json.Unmarshal(responseBody, &result); err != nil || result.TaskRef != input.TaskRef || result.Status == "" {
return StartResponse{}, errors.New("provider start returned an invalid response")
}
return result, nil
}
func (c *HTTPClient) Stop(ctx context.Context, input StopRequest) (StopResponse, error) {
body, err := json.Marshal(input)
if err != nil {
return StopResponse{}, err
}
request, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+"/mock/v1/voice-chats:stop", bytes.NewReader(body))
if err != nil {
return StopResponse{}, err
}
request.Header.Set("Content-Type", "application/json")
request.Header.Set("X-Mock-Provider-Key", c.apiKey)
response, err := c.client.Do(request)
if err != nil {
return StopResponse{}, &Error{Operation: "stop", Temporary: true, Cause: err}
}
defer response.Body.Close()
responseBody, err := io.ReadAll(io.LimitReader(response.Body, 1<<20))
if err != nil {
return StopResponse{}, &Error{Operation: "stop", Temporary: true, Cause: err}
}
if response.StatusCode < 200 || response.StatusCode >= 300 {
return StopResponse{}, &Error{Operation: "stop", StatusCode: response.StatusCode, Temporary: response.StatusCode == http.StatusTooManyRequests || response.StatusCode >= 500}
}
var result StopResponse
if err := json.Unmarshal(responseBody, &result); err != nil || result.TaskRef != input.TaskRef || result.Status == "" {
return StopResponse{}, errors.New("provider stop returned an invalid response")
}
return result, nil
}
func (c *HTTPClient) Query(ctx context.Context, input QueryRequest) (QueryResponse, error) {
body, err := json.Marshal(input)
if err != nil {
return QueryResponse{}, err
}
request, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+"/mock/v1/voice-chats:query", bytes.NewReader(body))
if err != nil {
return QueryResponse{}, err
}
request.Header.Set("Content-Type", "application/json")
request.Header.Set("X-Mock-Provider-Key", c.apiKey)
response, err := c.client.Do(request)
if err != nil {
return QueryResponse{}, &Error{Operation: "query", Temporary: true, Cause: err}
}
defer response.Body.Close()
responseBody, err := io.ReadAll(io.LimitReader(response.Body, 1<<20))
if err != nil {
return QueryResponse{}, &Error{Operation: "query", Temporary: true, Cause: err}
}
if response.StatusCode < 200 || response.StatusCode >= 300 {
return QueryResponse{}, &Error{Operation: "query", StatusCode: response.StatusCode, Temporary: response.StatusCode == http.StatusTooManyRequests || response.StatusCode >= 500}
}
var result QueryResponse
if err := json.Unmarshal(responseBody, &result); err != nil || result.TaskRef != input.TaskRef || result.Status == "" {
return QueryResponse{}, errors.New("provider query returned an invalid response")
}
return result, nil
}