Files
story/internal/config/volc_account.go
T

203 lines
8.0 KiB
Go
Raw Normal View History

package config
import (
"bufio"
"bytes"
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
)
var volcAccountNames = strings.Fields(`VOLC_ACCESS_KEY_ID VOLC_SECRET_ACCESS_KEY VOLC_SESSION_TOKEN VOLC_RTC_BASE_URL VOLC_RTC_REGION VOLC_RTC_API_VERSION RTC_APP_ID RTC_APP_KEY RTC_TOKEN_ISSUER VOLC_HARDWARE_INSTANCE_ID VOLC_HARDWARE_PRODUCT_KEY VOLC_HARDWARE_PRODUCT_SECRET VOLC_HARDWARE_AGENT_BOT_ID VOLC_HARDWARE_PROVISIONING_ENABLED VOLC_HARDWARE_AGENT_TRANSPORT VOLC_HARDWARE_AGENT_MINIMUM_SDK_VERSION VOLC_HARDWARE_REGISTER_URL VOLC_HARDWARE_DEVICE_NAME_PREFIX MIGRATION_HARDWARE_RTC_APP_ID MIGRATION_HARDWARE_LICENSE_READY S2S_APP_ID S2S_ACCESS_TOKEN S2S_MODEL S2S_SPEAKER VIKING_MEMORY_RESOURCE_ID VIKING_MEMORY_API_KEY VIKING_MEMORY_COLLECTION_NAME VIKING_MEMORY_EVENT_TYPE VIKING_MEMORY_PROFILE_TYPE TOS_RECORDING_ACCOUNT_ID TOS_RECORDING_BUCKET TOS_RECORDING_REGION TOS_RECORDING_PLAYBACK_ENDPOINT ARK_API_KEY ARK_BASE_URL ARK_CONTENT_MODEL ARK_EMBEDDING_MODEL CONTENT_GENERATOR EMBEDDING_PROVIDER VOLC_SUBTITLE_CALLBACK_URL VOLC_SUBTITLE_PRIMARY MIGRATION_NOTES`)
// ReadVolcAccountFile reads a literal worksheet, never shell commands or expansion.
// Errors identify the field/line only; credentials must never enter logs.
func ReadVolcAccountFile(path string) (map[string]string, error) {
if !filepath.IsAbs(path) {
return nil, errors.New("VOLC_ACCOUNT_CONFIG_FILE must be absolute")
}
raw, err := os.ReadFile(path)
if err != nil {
return nil, errors.New("Volc account configuration unavailable")
}
if len(raw) > 65536 {
return nil, errors.New("Volc account configuration too large")
}
allowed := map[string]bool{}
for _, name := range volcAccountNames {
allowed[name] = true
}
values := map[string]string{}
s := bufio.NewScanner(bytes.NewReader(bytes.TrimPrefix(raw, []byte{0xef, 0xbb, 0xbf})))
line := 0
for s.Scan() {
line++
text := strings.TrimSpace(s.Text())
if text == "" || strings.HasPrefix(text, "#") {
continue
}
name, value, ok := strings.Cut(text, "=")
name = strings.TrimSpace(name)
if !ok || !allowed[name] {
return nil, fmt.Errorf("Volc account configuration invalid field at line %d", line)
}
if _, found := values[name]; found {
return nil, fmt.Errorf("Volc account configuration duplicate %s", name)
}
value = strings.TrimSpace(value)
if len(value) >= 2 && ((value[0] == '\'' && value[len(value)-1] == '\'') || (value[0] == '"' && value[len(value)-1] == '"')) {
value = value[1 : len(value)-1]
}
if strings.ContainsAny(value, "\x00\r\n") {
return nil, fmt.Errorf("Volc account configuration invalid %s", name)
}
values[name] = value
}
if s.Err() != nil {
return nil, errors.New("Volc account configuration unreadable")
}
for _, name := range volcAccountNames {
if strings.HasPrefix(name, "MIGRATION_") {
continue
}
if _, found := values[name]; !found {
return nil, fmt.Errorf("Volc account configuration missing %s", name)
}
}
return values, nil
}
func loadVolcAccount() (map[string]string, error) {
path := strings.TrimSpace(os.Getenv("VOLC_ACCOUNT_CONFIG_FILE"))
if path == "" {
return nil, nil
}
return ReadVolcAccountFile(path)
}
func loadSecretValuesWithAccount(account map[string]string, names ...string) (map[string]string, error) {
values := make(map[string]string, len(names))
for _, name := range names {
if value, found := account[name]; found {
values[name] = value
continue
}
value, err := secretEnv(name)
if err != nil {
return nil, err
}
values[name] = value
}
return values, nil
}
func (c *Config) applyVolcAccount(values map[string]string) error {
c.TOSRecordingAccountID = strings.TrimSpace(os.Getenv("TOS_RECORDING_ACCOUNT_ID"))
c.TOSRecordingBucket = strings.TrimSpace(os.Getenv("TOS_RECORDING_BUCKET"))
c.TOSRecordingRegion = strings.TrimSpace(os.Getenv("TOS_RECORDING_REGION"))
c.TOSRecordingPlaybackEndpoint = strings.TrimSpace(os.Getenv("TOS_RECORDING_PLAYBACK_ENDPOINT"))
if values == nil {
return nil
}
c.volcAccount = values
fields := map[string]*string{
"VOLC_ACCESS_KEY_ID": &c.VolcAccessKeyID, "VOLC_SECRET_ACCESS_KEY": &c.VolcSecretAccessKey, "VOLC_SESSION_TOKEN": &c.VolcSessionToken,
"RTC_APP_ID": &c.RTCAppID, "RTC_APP_KEY": &c.RTCAppKey, "RTC_TOKEN_ISSUER": &c.RTCTokenIssuer,
"VOLC_RTC_BASE_URL": &c.VolcRTCBaseURL, "VOLC_RTC_REGION": &c.VolcRTCRegion, "VOLC_RTC_API_VERSION": &c.VolcRTCAPIVersion,
"VOLC_HARDWARE_INSTANCE_ID": &c.VolcHardwareInstanceID, "VOLC_HARDWARE_PRODUCT_KEY": &c.VolcHardwareProductKey, "VOLC_HARDWARE_PRODUCT_SECRET": &c.VolcHardwareProductSecret,
"VOLC_HARDWARE_AGENT_BOT_ID": &c.VolcHardwareAgentBotID, "VOLC_HARDWARE_AGENT_TRANSPORT": &c.VolcHardwareTransport, "VOLC_HARDWARE_AGENT_MINIMUM_SDK_VERSION": &c.VolcHardwareMinimumSDKVersion,
"VOLC_HARDWARE_REGISTER_URL": &c.VolcHardwareRegisterURL, "VOLC_HARDWARE_DEVICE_NAME_PREFIX": &c.VolcHardwareDeviceNamePrefix,
"VIKING_MEMORY_RESOURCE_ID": &c.VikingResourceID, "VIKING_MEMORY_API_KEY": &c.VikingAPIKey, "VIKING_MEMORY_COLLECTION_NAME": &c.VikingCollectionName,
"VIKING_MEMORY_EVENT_TYPE": &c.VikingEventType, "VIKING_MEMORY_PROFILE_TYPE": &c.VikingProfileType,
"TOS_RECORDING_ACCOUNT_ID": &c.TOSRecordingAccountID, "TOS_RECORDING_BUCKET": &c.TOSRecordingBucket, "TOS_RECORDING_REGION": &c.TOSRecordingRegion, "TOS_RECORDING_PLAYBACK_ENDPOINT": &c.TOSRecordingPlaybackEndpoint,
"ARK_API_KEY": &c.ArkAPIKey, "ARK_BASE_URL": &c.ArkBaseURL, "ARK_CONTENT_MODEL": &c.ArkContentModel, "ARK_EMBEDDING_MODEL": &c.ArkEmbeddingModel,
"CONTENT_GENERATOR": &c.ContentGenerator, "EMBEDDING_PROVIDER": &c.EmbeddingProvider, "VOLC_SUBTITLE_CALLBACK_URL": &c.SubtitleCallbackURL,
}
for name, field := range fields {
*field = values[name]
}
for name, field := range map[string]*bool{"VOLC_HARDWARE_PROVISIONING_ENABLED": &c.VolcHardwareProvisioningEnabled, "VOLC_SUBTITLE_PRIMARY": &c.SubtitlePrimary} {
parsed, err := strconv.ParseBool(values[name])
if err != nil {
return fmt.Errorf("Volc account configuration invalid boolean %s", name)
}
*field = parsed
}
for _, name := range []string{"VOLC_ACCESS_KEY_ID", "VOLC_SECRET_ACCESS_KEY", "RTC_APP_ID", "RTC_APP_KEY", "S2S_APP_ID", "S2S_ACCESS_TOKEN", "S2S_MODEL", "S2S_SPEAKER"} {
if values[name] == "" {
return fmt.Errorf("Volc account configuration requires %s", name)
}
}
return nil
}
// ApplyVolcVoiceTemplate supplies credentials from the account file at startup.
// Templates retain prompts and behaviour but are never an alternative key source.
func (c Config) ApplyVolcVoiceTemplate(raw []byte) ([]byte, error) {
if c.volcAccount == nil {
return raw, nil
}
var root map[string]any
if json.Unmarshal(raw, &root) != nil {
return nil, errors.New("invalid Volc voice template")
}
items := []map[string]any{root}
if bundle, ok := root["templates"].(map[string]any); ok {
items = nil
for _, entry := range bundle {
item, ok := entry.(map[string]any)
if !ok {
return nil, errors.New("invalid Volc voice template entry")
}
items = append(items, item)
}
}
updated := 0
for _, item := range items {
conf, _ := item["Config"].(map[string]any)
s2s, _ := conf["S2SConfig"].(map[string]any)
if s2s == nil {
continue
}
pp, _ := s2s["ProviderParams"].(map[string]any)
if pp == nil {
pp = map[string]any{}
s2s["ProviderParams"] = pp
}
app, _ := pp["app"].(map[string]any)
if app == nil {
app = map[string]any{}
pp["app"] = app
}
app["appid"] = c.volcAccount["S2S_APP_ID"]
app["token"] = c.volcAccount["S2S_ACCESS_TOKEN"]
dialog, _ := pp["dialog"].(map[string]any)
if dialog == nil {
dialog = map[string]any{}
pp["dialog"] = dialog
}
extra, _ := dialog["extra"].(map[string]any)
if extra == nil {
extra = map[string]any{}
dialog["extra"] = extra
}
extra["model"] = c.volcAccount["S2S_MODEL"]
tts, _ := pp["tts"].(map[string]any)
if tts == nil {
tts = map[string]any{}
pp["tts"] = tts
}
tts["speaker"] = c.volcAccount["S2S_SPEAKER"]
conf["MemoryConfig"] = map[string]any{"Enable": false}
updated++
}
if updated == 0 {
return nil, errors.New("Volc account requires an S2S voice template")
}
return json.Marshal(root)
}