189 lines
6.5 KiB
Go
189 lines
6.5 KiB
Go
package configread
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net/url"
|
|
"strconv"
|
|
|
|
"git.ipao.vip/rogee/go-sip/internal/contract"
|
|
)
|
|
|
|
// CurrentSnapshot carries the four independently validated configuration
|
|
// resources. Agent.Raw preserves explicit zero/false and immutable AI fields.
|
|
type CurrentSnapshot struct {
|
|
SIP CurrentSIP
|
|
Task CurrentTask
|
|
Quota CurrentQuota
|
|
Providers map[string]CurrentProvider
|
|
}
|
|
|
|
type CurrentSIP struct {
|
|
Resource string `json:"resource"`
|
|
DispatcherID string `json:"dispatcher_id"`
|
|
Revision int64 `json:"revision"`
|
|
Trunks json.RawMessage `json:"trunks"`
|
|
}
|
|
|
|
type CurrentProvider struct {
|
|
ProviderRef string `json:"provider_ref"`
|
|
Role string `json:"role"`
|
|
Enabled bool `json:"enabled"`
|
|
Adapter string `json:"adapter"`
|
|
Endpoint string `json:"endpoint"`
|
|
Credential string `json:"credential"`
|
|
}
|
|
|
|
type currentProviderResponse struct {
|
|
Resource string `json:"resource"`
|
|
DispatcherID string `json:"dispatcher_id"`
|
|
Providers []CurrentProvider `json:"providers"`
|
|
}
|
|
|
|
type CurrentAgent struct {
|
|
Mode string `json:"mode"`
|
|
ASR struct {
|
|
ProviderRef string `json:"provider_ref"`
|
|
} `json:"asr"`
|
|
LLM struct {
|
|
ProviderRef string `json:"provider_ref"`
|
|
} `json:"llm"`
|
|
TTS struct {
|
|
ProviderRef string `json:"provider_ref"`
|
|
} `json:"tts"`
|
|
Raw json.RawMessage `json:"-"`
|
|
}
|
|
|
|
func (a *CurrentAgent) UnmarshalJSON(raw []byte) error {
|
|
type plain CurrentAgent
|
|
var parsed plain
|
|
if err := json.Unmarshal(raw, &parsed); err != nil {
|
|
return err
|
|
}
|
|
*a = CurrentAgent(parsed)
|
|
a.Raw = append([]byte(nil), raw...)
|
|
return nil
|
|
}
|
|
|
|
type CurrentTask struct {
|
|
Resource string `json:"resource"`
|
|
DispatcherID string `json:"dispatcher_id"`
|
|
TenantID int64 `json:"tenant_id"`
|
|
TaskID string `json:"task_id"`
|
|
TaskRevision int64 `json:"task_revision"`
|
|
Status string `json:"status"`
|
|
Name string `json:"name"`
|
|
MaxConcurrentCalls int64 `json:"max_concurrent_calls"`
|
|
RingTimeoutMS int64 `json:"ring_timeout_ms"`
|
|
MaxCallDurationMS int64 `json:"max_call_duration_ms"`
|
|
RoutePolicyID string `json:"route_policy_id"`
|
|
CallerProfileID string `json:"caller_profile_id"`
|
|
AllowedTrunkIDs []string `json:"allowed_trunk_ids"`
|
|
Schedule json.RawMessage `json:"schedule"`
|
|
Agent CurrentAgent `json:"agent"`
|
|
Raw json.RawMessage `json:"-"`
|
|
}
|
|
|
|
func (t *CurrentTask) UnmarshalJSON(raw []byte) error {
|
|
type plain CurrentTask
|
|
var parsed plain
|
|
if err := json.Unmarshal(raw, &parsed); err != nil {
|
|
return err
|
|
}
|
|
*t = CurrentTask(parsed)
|
|
t.Raw = append([]byte(nil), raw...)
|
|
return nil
|
|
}
|
|
|
|
type CurrentQuota struct {
|
|
Resource string `json:"resource"`
|
|
DispatcherID string `json:"dispatcher_id"`
|
|
TenantID int64 `json:"tenant_id"`
|
|
QuotaRevision int64 `json:"quota_revision"`
|
|
MaxConcurrentCalls int64 `json:"max_concurrent_calls"`
|
|
}
|
|
|
|
// ReadCurrentSIP is always called at Dispatcher startup, even with no tasks.
|
|
// Revision is not a claim that Agent/Asterisk actually loaded this snapshot;
|
|
// the caller must verify applied state before opening admission.
|
|
func (c *Client) ReadCurrentSIP(ctx context.Context) (CurrentSIP, error) {
|
|
var sip CurrentSIP
|
|
if err := c.readCurrent(ctx, configReadPath+"/sip", "sip_config", &sip); err != nil {
|
|
return CurrentSIP{}, fmt.Errorf("read SIP configuration: %w", err)
|
|
}
|
|
if sip.DispatcherID != c.dispatcherID {
|
|
return CurrentSIP{}, errors.New("SIP configuration dispatcher owner mismatch")
|
|
}
|
|
return sip, nil
|
|
}
|
|
|
|
// ReadCurrentTask reads only the agreed read-only HTTP resources. It never
|
|
// substitutes stale data or the historical MQ configuration contract.
|
|
func (c *Client) ReadCurrentTask(ctx context.Context, taskID string, tenantID int64) (CurrentSnapshot, error) {
|
|
if taskID == "" || tenantID <= 0 {
|
|
return CurrentSnapshot{}, errors.New("task ID and positive tenant ID are required")
|
|
}
|
|
var result CurrentSnapshot
|
|
var err error
|
|
result.SIP, err = c.ReadCurrentSIP(ctx)
|
|
if err != nil {
|
|
return CurrentSnapshot{}, err
|
|
}
|
|
var providers currentProviderResponse
|
|
if err := c.readCurrent(ctx, configReadPath+"/ai-providers", "ai_providers", &providers); err != nil {
|
|
return CurrentSnapshot{}, fmt.Errorf("read AI providers: %w", err)
|
|
}
|
|
if err := c.readCurrent(ctx, configReadPath+"/task/"+url.PathEscape(taskID), "task_config", &result.Task); err != nil {
|
|
return CurrentSnapshot{}, fmt.Errorf("read task configuration: %w", err)
|
|
}
|
|
if err := c.readCurrent(ctx, configReadPath+"/tenant/"+strconv.FormatInt(tenantID, 10)+"/quota", "tenant_quota", &result.Quota); err != nil {
|
|
return CurrentSnapshot{}, fmt.Errorf("read tenant quota: %w", err)
|
|
}
|
|
if result.SIP.DispatcherID != c.dispatcherID || providers.DispatcherID != c.dispatcherID || result.Task.DispatcherID != c.dispatcherID || result.Quota.DispatcherID != c.dispatcherID || result.Task.TenantID != tenantID || result.Quota.TenantID != tenantID || result.Task.TaskID != taskID {
|
|
return CurrentSnapshot{}, errors.New("configuration dispatcher, task, or tenant owner mismatch")
|
|
}
|
|
result.Providers = make(map[string]CurrentProvider, len(providers.Providers))
|
|
for _, provider := range providers.Providers {
|
|
if _, exists := result.Providers[provider.ProviderRef]; exists {
|
|
return CurrentSnapshot{}, fmt.Errorf("duplicate AI provider reference %q", provider.ProviderRef)
|
|
}
|
|
result.Providers[provider.ProviderRef] = provider
|
|
}
|
|
for _, expected := range []struct{ ref, role string }{
|
|
{result.Task.Agent.ASR.ProviderRef, "asr"},
|
|
{result.Task.Agent.LLM.ProviderRef, "llm"},
|
|
{result.Task.Agent.TTS.ProviderRef, "tts"},
|
|
} {
|
|
if expected.ref == "" {
|
|
continue
|
|
}
|
|
provider, found := result.Providers[expected.ref]
|
|
if !found || !provider.Enabled || provider.Role != expected.role {
|
|
return CurrentSnapshot{}, fmt.Errorf("AI provider %q is missing, disabled, or has an incompatible role", expected.ref)
|
|
}
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func (c *Client) readCurrent(ctx context.Context, path, resource string, dst any) error {
|
|
body, err := c.get(ctx, path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := contract.ValidateCurrent("config-read", body); err != nil {
|
|
return err
|
|
}
|
|
var identity struct {
|
|
Resource string `json:"resource"`
|
|
}
|
|
if err := json.Unmarshal(body, &identity); err != nil {
|
|
return err
|
|
}
|
|
if identity.Resource != resource {
|
|
return fmt.Errorf("expected %s, got %s", resource, identity.Resource)
|
|
}
|
|
return json.Unmarshal(body, dst)
|
|
}
|