feat: expand companion platform, device controls, and memoir workflows
ci / verify (push) Canceled after 0s

Add customer and operator login, BLE provisioning, device fleet controls, call memory, memoir generation, and refreshed family and maintenance portals. Include API contracts, migrations, firmware sources, tests, assets, and deployment documentation.
This commit is contained in:
Codex Local
2026-10-08 10:06:44 +08:00
parent 98fb2965d0
commit bf1233d5df
861 changed files with 94614 additions and 3209 deletions
+45 -3
View File
@@ -23,6 +23,11 @@ const (
)
type Config struct {
volcAccount map[string]string
TOSRecordingAccountID string
TOSRecordingBucket string
TOSRecordingRegion string
TOSRecordingPlaybackEndpoint string
Onboarding OnboardingConfig
VikingResourceID string
VikingCollectionName string
@@ -121,7 +126,15 @@ type Config struct {
ArkBaseURL string
ArkAPIKey string
ArkContentModel string
ArkMemoirModel string
ArkMemoirImageModel string
ArkRequestTimeout time.Duration
DifyEnabled bool
DifyBaseURL string
DifyBridgeURL string
DifyBridgeSecret string
DifyExtractionAPIKey string
DifyMemoirAPIKey string
ContentSafetyProvider string
ContentSafetyEndpoint string
ContentSafetyAPIKey string
@@ -145,6 +158,10 @@ func Load(serviceName string) (Config, error) {
return Config{}, errors.New("service name is required")
}
account, err := loadVolcAccount()
if err != nil {
return Config{}, err
}
redisDB, err := intEnv("REDIS_DB", 0)
if err != nil {
return Config{}, err
@@ -225,6 +242,10 @@ func Load(serviceName string) (Config, error) {
if err != nil {
return Config{}, err
}
difyEnabled, err := boolEnv("DIFY_ENABLED", false)
if err != nil {
return Config{}, err
}
contentSafetyTimeout, err := durationEnv("CONTENT_SAFETY_TIMEOUT", 5*time.Second)
if err != nil {
return Config{}, err
@@ -254,14 +275,15 @@ func Load(serviceName string) (Config, error) {
return Config{}, err
}
environment := stringEnv("APP_ENV", EnvironmentLocal)
secretValues, err := loadSecretValues(
"ONBOARDING_MASTER_KEY", "ONBOARDING_SMS_GATEWAY_TOKEN",
secretValues, err := loadSecretValuesWithAccount(account,
"ONBOARDING_MASTER_KEY", "ONBOARDING_SMS_GATEWAY_TOKEN", "TENCENT_SMS_SECRET_ID", "TENCENT_SMS_SECRET_KEY", "WECHAT_MINI_APP_SECRET",
"DATABASE_URL", "REDIS_PASSWORD", "DEVICE_LOCAL_SECRET", "DEVICE_CREDENTIAL_MASTER_KEY", "MODEL_CREDENTIAL_MASTER_KEY", "RTC_APP_KEY", "RTC_TOKEN_SIGNING_SECRET",
"CURSOR_SIGNING_SECRET", "SHARE_TOKEN_SIGNING_SECRET", "PROVIDER_CALLBACK_SECRET", "PLATFORM_HEALTH_CALLBACK_SECRET",
"VOLC_SUBTITLE_CALLBACK_SECRET", "VIKING_MEMORY_API_KEY",
"MOCK_PROVIDER_API_KEY", "VOLC_ACCESS_KEY_ID", "VOLC_SECRET_ACCESS_KEY", "VOLC_SESSION_TOKEN",
"VOLC_HARDWARE_PRODUCT_SECRET", "VOLC_HARDWARE_DEVICE_CREDENTIAL_MASTER_KEY",
"OBJECT_STORE_ACCESS_KEY", "OBJECT_STORE_SECRET_KEY", "ALERT_WEBHOOK_SECRET", "ARK_API_KEY", "CONTENT_SAFETY_API_KEY", "MEMORY_TOOL_SIGNING_SECRET", "PROVIDER_MEMORY_API_KEY",
"DIFY_BRIDGE_SECRET", "DIFY_EXTRACTION_API_KEY", "DIFY_MEMOIR_API_KEY",
)
if err != nil {
return Config{}, err
@@ -404,8 +426,16 @@ func Load(serviceName string) (Config, error) {
ContentGenerator: stringEnv("CONTENT_GENERATOR", "local"),
ArkBaseURL: stringEnv("ARK_BASE_URL", "https://ark.cn-beijing.volces.com/api/v3"),
ArkAPIKey: secretValues["ARK_API_KEY"],
ArkContentModel: strings.TrimSpace(os.Getenv("ARK_CONTENT_MODEL")),
ArkContentModel: stringEnv("ARK_CONTENT_MODEL", "deepseek-v4-1-flash-260910"),
ArkMemoirModel: stringEnv("ARK_MEMOIR_MODEL", "doubao-seed-evolving"),
ArkMemoirImageModel: stringEnv("ARK_MEMOIR_IMAGE_MODEL", "doubao-seedream-5-0-pro-260628"),
ArkRequestTimeout: arkRequestTimeout,
DifyEnabled: difyEnabled,
DifyBaseURL: stringEnv("DIFY_BASE_URL", "http://story-dify-nginx/v1"),
DifyBridgeURL: stringEnv("DIFY_BRIDGE_URL", "http://model-bridge:8188"),
DifyBridgeSecret: secretValues["DIFY_BRIDGE_SECRET"],
DifyExtractionAPIKey: secretValues["DIFY_EXTRACTION_API_KEY"],
DifyMemoirAPIKey: secretValues["DIFY_MEMOIR_API_KEY"],
ContentSafetyProvider: stringEnv("CONTENT_SAFETY_PROVIDER", "local"),
ContentSafetyEndpoint: strings.TrimSpace(os.Getenv("CONTENT_SAFETY_ENDPOINT")),
ContentSafetyAPIKey: secretValues["CONTENT_SAFETY_API_KEY"],
@@ -491,6 +521,15 @@ func Load(serviceName string) (Config, error) {
if err != nil {
return Config{}, err
}
if err = cfg.Onboarding.loadTencentSMSFile(serviceName); err != nil {
return Config{}, err
}
if err = cfg.Onboarding.loadTencentCaptchaFile(serviceName); err != nil {
return Config{}, err
}
if err := cfg.applyVolcAccount(account); err != nil {
return Config{}, err
}
if err := cfg.Validate(); err != nil {
return Config{}, err
}
@@ -668,6 +707,9 @@ func (c Config) Validate() error {
}
}
if c.ServiceName == "api" || c.ServiceName == "worker" {
if c.DifyEnabled && (!isAbsoluteHTTPURL(c.DifyBaseURL) || !isAbsoluteHTTPURL(c.DifyBridgeURL) || c.DifyBridgeSecret == "" || c.DifyExtractionAPIKey == "" || c.DifyMemoirAPIKey == "") {
return errors.New("Dify workflows require internal URLs and extraction, memoir, bridge credentials")
}
if c.EmbeddingProvider != "local" && c.EmbeddingProvider != "ark" {
return errors.New("EMBEDDING_PROVIDER must be local or ark")
}
+28
View File
@@ -0,0 +1,28 @@
package config
import (
"os"
"path/filepath"
"testing"
)
func TestDifyRequiresCompleteCredentialsAndAcceptsSecretFiles(t *testing.T) {
t.Setenv("DIFY_ENABLED", "true")
t.Setenv("DIFY_BRIDGE_SECRET", "")
t.Setenv("DIFY_EXTRACTION_API_KEY", "")
t.Setenv("DIFY_MEMOIR_API_KEY", "")
if _, err := Load("worker"); err == nil {
t.Fatal("enabled workflows accepted missing credentials")
}
for _, name := range []string{"DIFY_BRIDGE_SECRET", "DIFY_EXTRACTION_API_KEY", "DIFY_MEMOIR_API_KEY"} {
file := filepath.Join(t.TempDir(), name)
if err := os.WriteFile(file, []byte("private-test-value"), 0600); err != nil {
t.Fatal(err)
}
t.Setenv(name+"_FILE", file)
}
cfg, err := Load("worker")
if err != nil || !cfg.DifyEnabled || cfg.DifyMemoirAPIKey != "private-test-value" {
t.Fatalf("secret-file workflow configuration failed: %v", err)
}
}
+59 -6
View File
@@ -7,18 +7,30 @@ import (
"net/netip"
"net/url"
"os"
"regexp"
"strings"
)
var smsNumericID = regexp.MustCompile(`^[0-9]+$`)
type OnboardingConfig struct {
SaaSCaptchaDisabled bool
TencentCaptcha TencentCaptchaConfig
TencentSMSTemplateParams map[string][]string
Enabled, PauseClaims, ActivationEnabled, SMSLoginEnabled, AllowLocalHTTP bool
MasterKey, EntryURL, SMSGatewayURL, SMSGatewayToken string
MasterKey, EntryURL, SMSGatewayURL, SMSGatewayToken, SMSProvider string
TencentSMSSecretID, TencentSMSSecretKey, TencentSMSAppID string
TencentSMSSignName, TencentSMSTemplateID string
TencentSMSTemplates map[string]string
WeChatAppID, WeChatAppSecret string
TrustedProxyCIDRs, SelfSignupTenantIDs []string
}
func loadOnboardingConfig(secrets map[string]string) (OnboardingConfig, error) {
c := OnboardingConfig{MasterKey: secrets["ONBOARDING_MASTER_KEY"], SMSGatewayToken: secrets["ONBOARDING_SMS_GATEWAY_TOKEN"], EntryURL: strings.TrimSpace(os.Getenv("ONBOARDING_ENTRY_URL")), SMSGatewayURL: strings.TrimSpace(os.Getenv("ONBOARDING_SMS_GATEWAY_URL"))}
for name, target := range map[string]*bool{"ONBOARDING_ENABLED": &c.Enabled, "ONBOARDING_PAUSE_CLAIMS": &c.PauseClaims, "ONBOARDING_ACTIVATION_ENABLED": &c.ActivationEnabled, "ONBOARDING_SMS_LOGIN_ENABLED": &c.SMSLoginEnabled, "ONBOARDING_ALLOW_LOCAL_HTTP": &c.AllowLocalHTTP} {
c := OnboardingConfig{MasterKey: secrets["ONBOARDING_MASTER_KEY"], SMSGatewayToken: secrets["ONBOARDING_SMS_GATEWAY_TOKEN"], EntryURL: strings.TrimSpace(os.Getenv("ONBOARDING_ENTRY_URL")), SMSGatewayURL: strings.TrimSpace(os.Getenv("ONBOARDING_SMS_GATEWAY_URL")), SMSProvider: strings.TrimSpace(os.Getenv("ONBOARDING_SMS_PROVIDER")), TencentSMSSecretID: secrets["TENCENT_SMS_SECRET_ID"], TencentSMSSecretKey: secrets["TENCENT_SMS_SECRET_KEY"], TencentSMSAppID: strings.TrimSpace(os.Getenv("TENCENT_SMS_SDK_APP_ID")), TencentSMSSignName: strings.TrimSpace(os.Getenv("TENCENT_SMS_SIGN_NAME")), TencentSMSTemplateID: strings.TrimSpace(os.Getenv("TENCENT_SMS_TEMPLATE_ID"))}
c.WeChatAppID = strings.TrimSpace(os.Getenv("WECHAT_MINI_APP_ID"))
c.WeChatAppSecret = secrets["WECHAT_MINI_APP_SECRET"]
for name, target := range map[string]*bool{"ONBOARDING_SAAS_CAPTCHA_DISABLED": &c.SaaSCaptchaDisabled, "ONBOARDING_ENABLED": &c.Enabled, "ONBOARDING_PAUSE_CLAIMS": &c.PauseClaims, "ONBOARDING_ACTIVATION_ENABLED": &c.ActivationEnabled, "ONBOARDING_SMS_LOGIN_ENABLED": &c.SMSLoginEnabled, "ONBOARDING_ALLOW_LOCAL_HTTP": &c.AllowLocalHTTP} {
v, err := boolEnv(name, false)
if err != nil {
return c, err
@@ -39,6 +51,14 @@ func loadOnboardingConfig(secrets map[string]string) (OnboardingConfig, error) {
return c, nil
}
func (c OnboardingConfig) validate(environment string) error {
if c.WeChatAppSecret != "" {
if len(c.WeChatAppID) != 18 || !strings.HasPrefix(c.WeChatAppID, "wx") || len(c.WeChatAppSecret) < 16 {
return errors.New("WeChat login requires WECHAT_MINI_APP_ID and WECHAT_MINI_APP_SECRET")
}
if _, err := hex.DecodeString(c.WeChatAppID[2:]); err != nil {
return errors.New("invalid WECHAT_MINI_APP_ID")
}
}
if c.AllowLocalHTTP && environment != EnvironmentLocal && environment != EnvironmentTest {
return errors.New("ONBOARDING_ALLOW_LOCAL_HTTP is limited to local/test")
}
@@ -68,9 +88,42 @@ func (c OnboardingConfig) validate(environment string) error {
}
}
if c.SMSLoginEnabled {
u, err := url.Parse(c.SMSGatewayURL)
if err != nil || u.Scheme != "https" || u.Host == "" || u.User != nil || u.RawQuery != "" || u.Fragment != "" || c.SMSGatewayToken == "" {
return errors.New("onboarding SMS requires HTTPS gateway and token")
switch c.SMSProvider {
case "", "gateway":
u, err := url.Parse(c.SMSGatewayURL)
if err != nil || u.Scheme != "https" || u.Host == "" || u.User != nil || u.RawQuery != "" || u.Fragment != "" || c.SMSGatewayToken == "" {
return errors.New("onboarding SMS requires HTTPS gateway and token")
}
case "tencent":
if c.TencentSMSSecretID == "" || c.TencentSMSSecretKey == "" || !smsNumericID.MatchString(c.TencentSMSAppID) || c.TencentSMSSignName == "" {
return errors.New("Tencent SMS requires SecretId, SecretKey, SDK AppID and sign name")
}
if c.TencentSMSTemplates != nil {
for _, name := range []string{"login", "register", "change_password", "change_phone"} {
if !smsNumericID.MatchString(c.TencentSMSTemplates[name]) {
return errors.New("Tencent SMS requires numeric login, register, change_password and change_phone template IDs")
}
}
} else if !smsNumericID.MatchString(c.TencentSMSTemplateID) {
return errors.New("Tencent SMS requires a numeric template ID")
}
for name, params := range c.TencentSMSTemplateParams {
if name != "login" && name != "register" && name != "change_password" && name != "change_phone" {
return errors.New("Tencent SMS template_params contains an unknown purpose")
}
seen := map[string]bool{}
for _, value := range params {
if (value != "code" && value != "expires_minutes") || seen[value] {
return errors.New("Tencent SMS template_params must contain code and optionally expires_minutes without duplicates")
}
seen[value] = true
}
if !seen["code"] {
return errors.New("Tencent SMS template_params must include code")
}
}
default:
return errors.New("ONBOARDING_SMS_PROVIDER must be gateway or tencent")
}
}
return nil
+36
View File
@@ -0,0 +1,36 @@
package config
import (
"strings"
"testing"
)
func TestTencentSMSConfigRequiresCompleteCredentialsAndApprovedTemplate(t *testing.T) {
c := OnboardingConfig{
SMSLoginEnabled: true,
SMSProvider: "tencent",
MasterKey: strings.Repeat("ab", 32),
EntryURL: "https://platform.example/onboarding/entry",
TencentSMSSecretID: "id",
TencentSMSSecretKey: "key",
TencentSMSAppID: "1400006666",
TencentSMSSignName: "安芯伴",
TencentSMSTemplateID: "123456",
}
if err := c.validate(EnvironmentProd); err != nil {
t.Fatal(err)
}
c.TencentSMSSecretKey = ""
if err := c.validate(EnvironmentProd); err == nil {
t.Fatal("Tencent SMS accepted a missing secret key")
}
c.TencentSMSSecretKey = "key"
c.TencentSMSTemplateID = "unapproved"
if err := c.validate(EnvironmentProd); err == nil {
t.Fatal("Tencent SMS accepted an invalid template ID")
}
c.SMSProvider = "unknown"
if err := c.validate(EnvironmentProd); err == nil {
t.Fatal("unknown SMS provider accepted")
}
}
+92
View File
@@ -0,0 +1,92 @@
package config
import (
"bytes"
"encoding/json"
"errors"
"io"
"os"
"path/filepath"
"strconv"
"strings"
)
// Credentials are server-only; policy is stored separately in PostgreSQL.
type CaptchaClientConfig struct {
AppID string `json:"captcha_app_id"`
AppSecretKey string `json:"app_secret_key"`
}
type TencentCaptchaConfig struct {
SecretID string `json:"secret_id"`
SecretKey string `json:"secret_key"`
SaaS CaptchaClientConfig `json:"saas"`
Mini CaptchaClientConfig `json:"mini_program"`
}
func (c *OnboardingConfig) loadTencentCaptchaFile(service string) error {
if service != "api" && service != "worker" {
return nil
}
path := strings.TrimSpace(os.Getenv("TENCENT_CAPTCHA_CONFIG_FILE"))
required := path != ""
if !required {
path = "/run/secrets/voice-runtime/tencent-captcha.json"
if !filepath.IsAbs(path) {
return nil
}
}
if !filepath.IsAbs(path) {
return errors.New("TENCENT_CAPTCHA_CONFIG_FILE must be absolute")
}
f, err := os.Open(filepath.Clean(path))
if errors.Is(err, os.ErrNotExist) && !required {
return nil
}
if err != nil {
return errors.New("Tencent CAPTCHA configuration file unavailable")
}
defer f.Close()
info, err := f.Stat()
if err != nil || !info.Mode().IsRegular() || info.Size() > 16384 {
return errors.New("Tencent CAPTCHA configuration must be a regular file of at most 16 KiB")
}
raw, err := io.ReadAll(io.LimitReader(f, 16385))
if err != nil || len(raw) > 16384 {
return errors.New("Tencent CAPTCHA configuration cannot be read")
}
d := json.NewDecoder(bytes.NewReader(bytes.TrimPrefix(raw, []byte{0xef, 0xbb, 0xbf})))
d.DisallowUnknownFields()
var in TencentCaptchaConfig
if d.Decode(&in) != nil || d.Decode(new(any)) != io.EOF {
return errors.New("Tencent CAPTCHA configuration must match the JSON template")
}
if err = in.Validate(); err != nil {
return err
}
c.TencentCaptcha = in
return nil
}
func (c *TencentCaptchaConfig) Validate() error {
c.SecretID = strings.TrimSpace(c.SecretID)
c.SecretKey = strings.TrimSpace(c.SecretKey)
if (c.SecretID == "") != (c.SecretKey == "") {
return errors.New("Tencent CAPTCHA requires both SecretId and SecretKey")
}
for _, client := range []*CaptchaClientConfig{&c.SaaS, &c.Mini} {
client.AppID = strings.TrimSpace(client.AppID)
client.AppSecretKey = strings.TrimSpace(client.AppSecretKey)
if client.AppID == "" && client.AppSecretKey == "" {
continue
}
id, err := strconv.ParseUint(client.AppID, 10, 64)
if err != nil || id == 0 || len(client.AppSecretKey) < 16 || len(client.AppSecretKey) > 256 || c.SecretID == "" {
return errors.New("Tencent CAPTCHA client requires numeric CaptchaAppId, AppSecretKey and API credentials")
}
client.AppID = strconv.FormatUint(id, 10)
}
if c.SaaS.AppID != "" && c.SaaS.AppID == c.Mini.AppID {
return errors.New("SaaS and mini-program CAPTCHA must use different CaptchaAppIds")
}
return nil
}
@@ -0,0 +1,51 @@
package config
import (
"os"
"path/filepath"
"strings"
"testing"
)
func TestTencentCaptchaFile(t *testing.T) {
valid := `{"secret_id":"test-id","secret_key":"test-key","saas":{"captcha_app_id":"123","app_secret_key":"abcdefghijklmnop"},"mini_program":{"captcha_app_id":"456","app_secret_key":"qrstuvwxyz123456"}}`
for _, tc := range []struct {
name, raw string
ok bool
}{
{"valid", valid, true}, {"empty", `{}`, true}, {"BOM", "\xef\xbb\xbf" + valid, true},
{"partial", `{"secret_id":"test-id"}`, false}, {"unknown", `{"secret_token":"do-not-leak"}`, false},
{"same-id", strings.ReplaceAll(valid, `"456"`, `"123"`), false},
{"bad-id", strings.ReplaceAll(valid, `"123"`, `"not-numeric"`), false},
{"trailing", valid + `{}`, false}, {"oversize", strings.Repeat("x", 16385), false},
} {
t.Run(tc.name, func(t *testing.T) {
path := filepath.Join(t.TempDir(), "captcha.json")
if err := os.WriteFile(path, []byte(tc.raw), 0600); err != nil {
t.Fatal(err)
}
t.Setenv("TENCENT_CAPTCHA_CONFIG_FILE", path)
var c OnboardingConfig
err := c.loadTencentCaptchaFile("api")
if (err == nil) != tc.ok {
t.Fatalf("valid=%v err=%v", tc.ok, err)
}
if err != nil && strings.Contains(err.Error(), "do-not-leak") {
t.Fatal("secret in error")
}
if err == nil && tc.name == "valid" && (c.TencentCaptcha.SaaS.AppID != "123" || c.TencentCaptcha.Mini.AppID != "456") {
t.Fatal("client config mixed")
}
})
}
}
func TestTencentCaptchaFileOnlyBackendAndExplicitMissing(t *testing.T) {
t.Setenv("TENCENT_CAPTCHA_CONFIG_FILE", filepath.Join(t.TempDir(), "absent.json"))
var c OnboardingConfig
if c.loadTencentCaptchaFile("callbacks") != nil {
t.Fatal("callbacks read config")
}
if c.loadTencentCaptchaFile("api") == nil {
t.Fatal("missing explicit file accepted")
}
}
+92
View File
@@ -0,0 +1,92 @@
package config
import (
"bytes"
"encoding/json"
"errors"
"io"
"os"
"path/filepath"
"strings"
)
const defaultTencentSMSConfigFile = "/run/secrets/voice-runtime/tencent-sms.json"
type tencentSMSFile struct {
TemplateParams map[string][]string `json:"template_params"`
Enabled *bool `json:"enabled"`
SecretID string `json:"secret_id"`
SecretKey string `json:"secret_key"`
SDKAppID string `json:"sdk_app_id"`
SignName string `json:"sign_name"`
Templates map[string]string `json:"templates"`
}
// Only API and worker consume SMS configuration. In particular, a mounted
// configuration must not enable onboarding in callbacks or other services.
func (c *OnboardingConfig) loadTencentSMSFile(serviceName string) error {
if serviceName != "api" && serviceName != "worker" {
return nil
}
path := strings.TrimSpace(os.Getenv("TENCENT_SMS_CONFIG_FILE"))
explicit := path != ""
if !explicit {
path = defaultTencentSMSConfigFile
// The conventional mount exists only on Unix hosts/containers. Windows
// development can still opt in with an explicit absolute file path.
if !filepath.IsAbs(path) {
return nil
}
}
return c.readTencentSMSFile(path, explicit)
}
func (c *OnboardingConfig) readTencentSMSFile(path string, required bool) error {
if !filepath.IsAbs(path) {
return errors.New("TENCENT_SMS_CONFIG_FILE must be an absolute path")
}
file, err := os.Open(filepath.Clean(path))
if errors.Is(err, os.ErrNotExist) && !required {
return nil
}
if err != nil {
return errors.New("Tencent SMS configuration file is unavailable")
}
defer file.Close()
info, err := file.Stat()
if err != nil || !info.Mode().IsRegular() || info.Size() > 64*1024 {
return errors.New("Tencent SMS configuration must be a regular file of at most 64 KiB")
}
raw, err := io.ReadAll(io.LimitReader(file, 64*1024+1))
if err != nil || len(raw) > 64*1024 {
return errors.New("Tencent SMS configuration file could not be read")
}
// Accept UTF-8 BOM from Windows editors. Never include raw JSON or parser
// errors in startup errors: unknown fields may accidentally contain secrets.
raw = bytes.TrimPrefix(raw, []byte{0xef, 0xbb, 0xbf})
decoder := json.NewDecoder(bytes.NewReader(raw))
decoder.DisallowUnknownFields()
var in tencentSMSFile
if err := decoder.Decode(&in); err != nil || in.Enabled == nil {
return errors.New("Tencent SMS configuration must match the JSON template and include enabled")
}
if err := decoder.Decode(new(any)); err != io.EOF {
return errors.New("Tencent SMS configuration must contain exactly one JSON object")
}
for key, value := range in.Templates {
if key != "login" && key != "register" && key != "change_password" && key != "change_phone" {
return errors.New("Tencent SMS template names must be login, register, change_password or change_phone")
}
in.Templates[key] = strings.TrimSpace(value)
}
c.SMSProvider = "tencent"
c.SMSLoginEnabled = *in.Enabled
c.TencentSMSSecretID = strings.TrimSpace(in.SecretID)
c.TencentSMSSecretKey = strings.TrimSpace(in.SecretKey)
c.TencentSMSAppID = strings.TrimSpace(in.SDKAppID)
c.TencentSMSSignName = strings.TrimSpace(in.SignName)
c.TencentSMSTemplateID = ""
c.TencentSMSTemplates = in.Templates
c.TencentSMSTemplateParams = in.TemplateParams
return nil
}
+89
View File
@@ -0,0 +1,89 @@
package config
import (
"os"
"path/filepath"
"strings"
"testing"
)
const validTencentSMSJSON = `{"enabled":true,"secret_id":"fixture-id","secret_key":"fixture-private-key","sdk_app_id":"1400006666","sign_name":"安芯伴","templates":{"login":"101","register":"102","change_password":"103","change_phone":"104"}}`
func smsConfigFixture(t *testing.T, content string) string {
t.Helper()
path := filepath.Join(t.TempDir(), "tencent-sms.json")
if err := os.WriteFile(path, []byte(content), 0600); err != nil {
t.Fatal(err)
}
return path
}
func TestTencentSMSFileLoadsAllTemplatesAndOverridesLegacyConfiguration(t *testing.T) {
c := OnboardingConfig{MasterKey: strings.Repeat("ab", 32), EntryURL: "https://platform.example/onboarding/entry", SMSProvider: "gateway", TencentSMSTemplateID: "old"}
t.Setenv("TENCENT_SMS_CONFIG_FILE", smsConfigFixture(t, "\xef\xbb\xbf"+validTencentSMSJSON))
if err := c.loadTencentSMSFile("api"); err != nil {
t.Fatal(err)
}
if err := c.validate(EnvironmentProd); err != nil {
t.Fatal(err)
}
if !c.SMSLoginEnabled || c.SMSProvider != "tencent" || c.TencentSMSSecretKey != "fixture-private-key" || c.TencentSMSTemplateID != "" || c.TencentSMSTemplates["change_phone"] != "104" {
t.Fatal("single-file settings did not take precedence")
}
if err := c.readTencentSMSFile(smsConfigFixture(t, strings.Replace(validTencentSMSJSON, `"enabled":true`, `"enabled":false`, 1)), true); err != nil || c.SMSLoginEnabled {
t.Fatal("file could not disable SMS")
}
}
func TestTencentSMSFileRejectsBrokenConfigurationWithoutLeakingSecrets(t *testing.T) {
for _, raw := range []string{
validTencentSMSJSON + `{}`,
strings.Replace(validTencentSMSJSON, `"enabled":true,`, "", 1),
strings.Replace(validTencentSMSJSON, `"secret_key"`, `"fixture-private-key"`, 1),
strings.Replace(validTencentSMSJSON, `"change_phone"`, `"fixture-private-key"`, 1),
strings.Replace(validTencentSMSJSON, `"104"`, `"not-an-id"`, 1),
strings.Replace(validTencentSMSJSON, `,"change_phone":"104"`, "", 1),
} {
c := OnboardingConfig{MasterKey: strings.Repeat("ab", 32), EntryURL: "https://platform.example/onboarding/entry"}
err := c.readTencentSMSFile(smsConfigFixture(t, raw), true)
if err == nil {
err = c.validate(EnvironmentProd)
}
if err == nil || strings.Contains(err.Error(), "fixture-private-key") {
t.Fatal("invalid configuration accepted or secret exposed in error")
}
}
}
func TestTencentSMSFileMissingAndNonSMSServiceBehavior(t *testing.T) {
path := filepath.Join(t.TempDir(), "missing.json")
c := OnboardingConfig{}
if err := c.readTencentSMSFile(path, false); err != nil {
t.Fatal("absent optional file broke existing deployments", err)
}
t.Setenv("TENCENT_SMS_CONFIG_FILE", path)
if err := c.loadTencentSMSFile("api"); err == nil {
t.Fatal("explicit missing configuration silently ignored")
}
if err := c.loadTencentSMSFile("callbacks"); err != nil || c.SMSLoginEnabled {
t.Fatal("SMS file affected callbacks service")
}
}
func TestTencentSMSFileTemplateParameterOrder(t *testing.T) {
raw := strings.TrimSuffix(validTencentSMSJSON, "}") + `,"template_params":{"register":["code","expires_minutes"]}}`
c := OnboardingConfig{MasterKey: strings.Repeat("ab", 32), EntryURL: "https://platform.example/onboarding/entry"}
if err := c.readTencentSMSFile(smsConfigFixture(t, raw), true); err != nil {
t.Fatal(err)
}
if err := c.validate(EnvironmentProd); err != nil {
t.Fatal(err)
}
if len(c.TencentSMSTemplateParams["register"]) != 2 || c.TencentSMSTemplateParams["register"][1] != "expires_minutes" {
t.Fatal("parameter order was not preserved")
}
c.TencentSMSTemplateParams["register"] = []string{"code", "unknown"}
if err := c.validate(EnvironmentProd); err == nil {
t.Fatal("invalid template parameter accepted")
}
}
+202
View File
@@ -0,0 +1,202 @@
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)
}
+76
View File
@@ -0,0 +1,76 @@
package config
import (
"encoding/json"
"os"
"path/filepath"
"strings"
"testing"
)
func accountFixture() string {
var b strings.Builder
for _, name := range volcAccountNames {
b.WriteString(name + "=\n")
}
return b.String()
}
func TestVolcAccountRejectsInvalidWithoutSecretLeak(t *testing.T) {
for _, suffix := range []string{"SECRET_SENTINEL=do-not-print", "RTC_APP_KEY=do-not-print", "not-valid do-not-print"} {
p := filepath.Join(t.TempDir(), "account.env")
if err := os.WriteFile(p, []byte(accountFixture()+suffix), 0600); err != nil {
t.Fatal(err)
}
_, err := ReadVolcAccountFile(p)
if err == nil || strings.Contains(err.Error(), "do-not-print") {
t.Fatal("invalid configuration accepted or secret leaked")
}
}
}
func TestVolcAccountLiteralParsingAndAuthoritativeSecrets(t *testing.T) {
p := filepath.Join(t.TempDir(), "account.env")
raw := strings.Replace(accountFixture(), "RTC_APP_KEY=\n", "RTC_APP_KEY='literal=$value#secret'\n", 1)
if err := os.WriteFile(p, []byte("\xef\xbb\xbf# worksheet\r\n"+raw), 0600); err != nil {
t.Fatal(err)
}
values, err := ReadVolcAccountFile(p)
if err != nil {
t.Fatal(err)
}
t.Setenv("RTC_APP_KEY", "old-secret")
t.Setenv("RTC_APP_KEY_FILE", filepath.Join(t.TempDir(), "missing-legacy-file"))
t.Setenv("ARK_API_KEY", "obsolete-ark-secret")
secrets, err := loadSecretValuesWithAccount(values, "RTC_APP_KEY", "ARK_API_KEY")
if err != nil {
t.Fatal(err)
}
if secrets["RTC_APP_KEY"] != "literal=$value#secret" || secrets["ARK_API_KEY"] != "" {
t.Fatal("account file did not supersede obsolete credentials")
}
}
func TestVolcAccountTemplateCredentialsAndPrompts(t *testing.T) {
c := Config{volcAccount: map[string]string{"S2S_APP_ID": "new-app", "S2S_ACCESS_TOKEN": "new-token", "S2S_MODEL": "O", "S2S_SPEAKER": "new-speaker"}}
raw := []byte(`{"schema_version":"voice-chat-template-bundle-v1","templates":{"s2s_hybrid":{"Config":{"S2SConfig":{"Provider":"volcano","ProviderParams":{"app":{"appid":"old-app","token":"old-token"},"dialog":{"system_role":"keep this prompt","extra":{"model":"old-model"}},"tts":{"speaker":"old-speaker"}}}}}}}`)
updated, err := c.ApplyVolcVoiceTemplate(raw)
if err != nil {
t.Fatal(err)
}
if strings.Contains(string(updated), "old-") || !strings.Contains(string(updated), "keep this prompt") {
t.Fatal("old credentials retained or prompt lost")
}
var root map[string]any
if json.Unmarshal(updated, &root) != nil {
t.Fatal("invalid updated JSON")
}
conf := root["templates"].(map[string]any)["s2s_hybrid"].(map[string]any)["Config"].(map[string]any)
pp := conf["S2SConfig"].(map[string]any)["ProviderParams"].(map[string]any)
if pp["app"].(map[string]any)["token"] != "new-token" || pp["dialog"].(map[string]any)["extra"].(map[string]any)["model"] != "O" {
t.Fatal("account voice parameters not applied")
}
if _, err := c.ApplyVolcVoiceTemplate([]byte(`{"Config":{}}`)); err == nil {
t.Fatal("missing S2S accepted")
}
}