This commit is contained in:
andy committed 2026-08-18 18:23:11 +08:00
1 parent 648b274c55
commit 18cb51670c
14 files changed
+420 -171

No files matched your search

+62 -93
View File
@@ -14,8 +14,9 @@ import (
type RuntimeUpdater func(context.Context, map[string]string) error
// RuntimeSettingsRepository persists the subset of settings that can be
// applied to provider calls without restarting the Go process.
// RuntimeSettingsRepository persists editable settings for shared PostgreSQL
// deployments. Runtime application semantics are independent from storage:
// some settings apply immediately while startup-only settings require restart.
type RuntimeSettingsRepository interface {
LoadRuntimeSettings(context.Context, []string) (map[string]string, error)
SaveRuntimeSettings(context.Context, map[string]string) error
@@ -80,7 +81,7 @@ type Service struct {
mu sync.Mutex
path string
environment map[string]string
provider map[string]string
database map[string]string
repository RuntimeSettingsRepository
update RuntimeUpdater
billing BillingAccountWriter
@@ -100,7 +101,7 @@ func New(path string, environment map[string]string, updater RuntimeUpdater) *Se
func NewWithRuntimeRepository(path string, environment map[string]string, repository RuntimeSettingsRepository, updater RuntimeUpdater) *Service {
return &Service{
path: path, environment: cloneStrings(environment), provider: map[string]string{},
path: path, environment: cloneStrings(environment), database: map[string]string{},
repository: repository, update: updater,
}
}
@@ -133,15 +134,6 @@ func RuntimeSettingKeys() []string {
return keys
}
func ProviderSettingKeys() []string {
keys := make([]string, 0, len(providerSettingKeys))
for key := range providerSettingKeys {
keys = append(keys, key)
}
sort.Strings(keys)
return keys
}
func allowedEnvironmentKey(key string) bool {
_, allowed := fieldIndex[key]
return allowed
@@ -157,38 +149,38 @@ func (s *Service) Get(ctx context.Context) (any, error) {
}
s.mu.Lock()
defer s.mu.Unlock()
if err := s.refreshProviderSettings(ctx); err != nil {
if err := s.refreshDatabaseSettings(ctx); err != nil {
return nil, err
}
return s.get()
}
// RuntimeValues returns one coherent provider configuration snapshot. Database
// RuntimeValues returns one coherent editable configuration snapshot. Database
// values override process/file values, including an explicitly saved empty
// non-secret value. Callers use a fresh snapshot per request or worker action
// so changes made through another replica are also observed.
// non-secret value. Dynamic callers use a fresh snapshot per request or worker
// action so changes made through another replica are also observed.
func (s *Service) RuntimeValues(ctx context.Context) (map[string]string, error) {
if err := ctx.Err(); err != nil {
return nil, err
}
s.mu.Lock()
defer s.mu.Unlock()
if err := s.refreshProviderSettings(ctx); err != nil {
if err := s.refreshDatabaseSettings(ctx); err != nil {
return nil, err
}
return s.runtimeValues()
}
// CachedRuntimeValues is the non-blocking fallback used by liveness reporting
// if PostgreSQL is temporarily unavailable. Mutating/provider request paths use
// RuntimeValues and therefore still fail closed on a refresh error.
// if PostgreSQL is temporarily unavailable. Mutating and dynamic provider paths
// use RuntimeValues and therefore still fail closed on a refresh error.
func (s *Service) CachedRuntimeValues() map[string]string {
s.mu.Lock()
defer s.mu.Unlock()
values, err := s.runtimeValues()
if err != nil {
values = cloneStrings(s.environment)
for key, value := range s.provider {
for key, value := range s.database {
values[key] = value
}
}
@@ -201,7 +193,7 @@ func (s *Service) Save(ctx context.Context, values map[string]any) (any, error)
}
s.mu.Lock()
defer s.mu.Unlock()
if err := s.refreshProviderSettings(ctx); err != nil {
if err := s.refreshDatabaseSettings(ctx); err != nil {
return nil, err
}
updates := map[string]string{}
@@ -218,10 +210,10 @@ func (s *Service) Save(ctx context.Context, values map[string]any) (any, error)
updates[key] = next
}
if len(updates) > 0 {
providerUpdates, fileUpdates := map[string]string{}, map[string]string{}
databaseUpdates, fileUpdates := map[string]string{}, map[string]string{}
for key, value := range updates {
if s.repository != nil && isProviderSetting(key) {
providerUpdates[key] = value
if s.repository != nil {
databaseUpdates[key] = value
} else {
fileUpdates[key] = value
}
@@ -229,7 +221,7 @@ func (s *Service) Save(ctx context.Context, values map[string]any) (any, error)
// Capture the complete file-backed view before writing so a partial billing
// account update cannot blank sibling fields.
persisted := map[string]string{}
if len(fileUpdates) > 0 || containsBillingAccountUpdate(updates) {
if len(fileUpdates) > 0 || (s.billing != nil && containsBillingAccountUpdate(updates)) {
var err error
persisted, err = readEnv(s.path)
if err != nil {
@@ -241,9 +233,9 @@ func (s *Service) Save(ctx context.Context, values map[string]any) (any, error)
return nil, err
}
}
if len(providerUpdates) > 0 {
if err := s.repository.SaveRuntimeSettings(ctx, cloneStrings(providerUpdates)); err != nil {
return nil, fmt.Errorf("persist provider settings: %w", err)
if len(databaseUpdates) > 0 {
if err := s.repository.SaveRuntimeSettings(ctx, cloneStrings(databaseUpdates)); err != nil {
return nil, fmt.Errorf("persist runtime settings: %w", err)
}
}
if s.update != nil {
@@ -255,15 +247,15 @@ func (s *Service) Save(ctx context.Context, values map[string]any) (any, error)
s.environment[key] = value
persisted[key] = value
}
for key, value := range providerUpdates {
s.provider[key] = value
for key, value := range databaseUpdates {
s.database[key] = value
}
if s.billing != nil && containsBillingAccountUpdate(updates) {
if err := s.billing.SaveBillingAccount(ctx, BillingAccount{
AccountName: billingSetting(s.environment, persisted, "ZHINIAN_BILLING_ACCOUNT_NAME"),
BankName: billingSetting(s.environment, persisted, "ZHINIAN_BILLING_ACCOUNT_BANK"),
AccountNumber: billingSetting(s.environment, persisted, "ZHINIAN_BILLING_ACCOUNT_NUMBER"),
Contact: billingSetting(s.environment, persisted, "ZHINIAN_BILLING_CONTACT"),
AccountName: lookup(s.database, s.environment, persisted, "ZHINIAN_BILLING_ACCOUNT_NAME", ""),
BankName: lookup(s.database, s.environment, persisted, "ZHINIAN_BILLING_ACCOUNT_BANK", ""),
AccountNumber: lookup(s.database, s.environment, persisted, "ZHINIAN_BILLING_ACCOUNT_NUMBER", ""),
Contact: lookup(s.database, s.environment, persisted, "ZHINIAN_BILLING_CONTACT", ""),
}); err != nil {
return nil, fmt.Errorf("apply billing account settings: %w", err)
}
@@ -276,13 +268,6 @@ func (s *Service) Save(ctx context.Context, values map[string]any) (any, error)
return payload, err
}
func billingSetting(environment, persisted map[string]string, key string) string {
if value, ok := environment[key]; ok {
return value
}
return persisted[key]
}
func containsBillingAccountUpdate(updates map[string]string) bool {
for key := range updates {
switch key {
@@ -295,13 +280,7 @@ func containsBillingAccountUpdate(updates map[string]string) bool {
func requiresRestart(updates map[string]string) bool {
for key := range updates {
if isProviderSetting(key) {
continue
}
switch key {
case "ZHINIAN_BILLING_ACCOUNT_NAME", "ZHINIAN_BILLING_ACCOUNT_BANK", "ZHINIAN_BILLING_ACCOUNT_NUMBER", "ZHINIAN_BILLING_CONTACT":
continue
default:
if _, required := restartRequiredSettingKeys[key]; required {
return true
}
}
@@ -313,7 +292,7 @@ func (s *Service) get() (Payload, error) {
return Payload{}, err
}
current := func(field Field) string {
if value, ok := s.provider[field.Key]; ok {
if value, ok := s.database[field.Key]; ok {
return value
}
if value, ok := s.environment[field.Key]; ok {
@@ -337,32 +316,32 @@ func (s *Service) get() (Payload, error) {
}
image := normalizeImage(current(fieldIndex["IMAGE_GENERATE_ENGINE"]))
video := normalizeVideo(current(fieldIndex["VIDEO_GENERATE_ENGINE"]))
imageModel := map[string]string{"jimeng": lookup(s.provider, s.environment, file, "JIMENG_IMAGE_GENERATE_46_REQ_KEY", "jimeng_seedream46_cvtob"), "evolink": lookup(s.provider, s.environment, file, "EVOLINK_IMAGE_MODEL", "gpt-image-2"), "bailian": lookup(s.provider, s.environment, file, "BAILIAN_IMAGE_MODEL", "wan2.7-image-pro")}[image]
videoModel := lookup(s.provider, s.environment, file, "SEEDANCE_MODEL", "doubao-seedance-2-0-260128")
imageModel := map[string]string{"jimeng": lookup(s.database, s.environment, file, "JIMENG_IMAGE_GENERATE_46_REQ_KEY", "jimeng_seedream46_cvtob"), "evolink": lookup(s.database, s.environment, file, "EVOLINK_IMAGE_MODEL", "gpt-image-2"), "bailian": lookup(s.database, s.environment, file, "BAILIAN_IMAGE_MODEL", "wan2.7-image-pro")}[image]
videoModel := lookup(s.database, s.environment, file, "SEEDANCE_MODEL", "doubao-seedance-2-0-260128")
if video == "bailian" {
videoModel = lookup(s.provider, s.environment, file, "BAILIAN_VIDEO_MODEL", "wan2.7-i2v-2026-04-25")
videoModel = lookup(s.database, s.environment, file, "BAILIAN_VIDEO_MODEL", "wan2.7-i2v-2026-04-25")
}
imageConnected := connected(image, s.provider, s.environment, file)
videoConnected := connected(video, s.provider, s.environment, file)
imageField := project(fieldIndex["IMAGE_GENERATE_ENGINE"], image, s.provider, s.environment, file)
videoField := project(fieldIndex["VIDEO_GENERATE_ENGINE"], video, s.provider, s.environment, file)
imageConnected := connected(image, s.database, s.environment, file)
videoConnected := connected(video, s.database, s.environment, file)
imageField := project(fieldIndex["IMAGE_GENERATE_ENGINE"], image, s.database, s.environment, file)
videoField := project(fieldIndex["VIDEO_GENERATE_ENGINE"], video, s.database, s.environment, file)
assignments := []EngineAssignment{{ID: "image.generate", Label: "图片生成", Engine: image, EngineLabel: label(image), Connected: imageConnected, ConnectionLabel: connection(imageConnected), ReqKey: imageModel, Configurable: true, Field: &imageField}, {ID: "video.generate", Label: "视频生成", Engine: video, EngineLabel: label(video), Connected: videoConnected, ConnectionLabel: connection(videoConnected), ReqKey: videoModel, Configurable: true, Field: &videoField}}
services := Services{Visual: connected("jimeng", s.provider, s.environment, file), Evolink: connected("evolink", s.provider, s.environment, file), Seedance: connected("seedance", s.provider, s.environment, file), Bailian: connected("bailian", s.provider, s.environment, file), Auth: lookup(s.provider, s.environment, file, "ZHINIAN_AUTH_SESSION_SECRET", "") != "", Organization: lookup(s.provider, s.environment, file, "DATABASE_URL", "") != "" || lookup(s.provider, s.environment, file, "ZHINIAN_AUTH_SESSION_SECRET", "") != ""}
services := Services{Visual: connected("jimeng", s.database, s.environment, file), Evolink: connected("evolink", s.database, s.environment, file), Seedance: connected("seedance", s.database, s.environment, file), Bailian: connected("bailian", s.database, s.environment, file), Auth: lookup(s.database, s.environment, file, "ZHINIAN_AUTH_SESSION_SECRET", "") != "", Organization: lookup(s.database, s.environment, file, "DATABASE_URL", "") != "" || lookup(s.database, s.environment, file, "ZHINIAN_AUTH_SESSION_SECRET", "") != ""}
return Payload{Services: services, Capabilities: []Capability{{ID: "image.generate", Label: "图片生成 4.6", ReqKey: imageModel, Engine: image, EngineLabel: label(image), Enabled: true}, {ID: "video.generate", Label: "视频生成", ReqKey: videoModel, Engine: video, EngineLabel: label(video), Enabled: true}}, EngineAssignments: assignments, Groups: groups}, nil
}
func (s *Service) refreshProviderSettings(ctx context.Context) error {
func (s *Service) refreshDatabaseSettings(ctx context.Context) error {
if s.repository == nil {
return nil
}
values, err := s.repository.LoadRuntimeSettings(ctx, ProviderSettingKeys())
values, err := s.repository.LoadRuntimeSettings(ctx, RuntimeSettingKeys())
if err != nil {
return fmt.Errorf("load provider settings: %w", err)
return fmt.Errorf("load runtime settings: %w", err)
}
s.provider = map[string]string{}
s.database = map[string]string{}
for key, value := range values {
if isProviderSetting(key) {
s.provider[key] = value
if allowedEnvironmentKey(key) {
s.database[key] = value
}
}
return nil
@@ -382,7 +361,7 @@ func (s *Service) runtimeValues() (map[string]string, error) {
for key, value := range s.environment {
values[key] = value
}
for key, value := range s.provider {
for key, value := range s.database {
values[key] = value
}
return values, nil
@@ -537,8 +516,8 @@ func cloneStrings(input map[string]string) map[string]string {
}
return out
}
func lookup(provider, environment, file map[string]string, key, fallback string) string {
if value, ok := provider[key]; ok {
func lookup(database, environment, file map[string]string, key, fallback string) string {
if value, ok := database[key]; ok {
return value
}
if value, ok := environment[key]; ok {
@@ -549,9 +528,9 @@ func lookup(provider, environment, file map[string]string, key, fallback string)
}
return fallback
}
func project(field Field, value string, provider, environment, file map[string]string) Field {
func project(field Field, value string, database, environment, file map[string]string) Field {
field.Value = value
field.Configured = lookup(provider, environment, file, field.Key, "") != ""
field.Configured = lookup(database, environment, file, field.Key, "") != ""
return field
}
func normalizeImage(value string) string {
@@ -585,16 +564,16 @@ func connection(ok bool) string {
}
return "待配置"
}
func connected(engine string, provider, environment, file map[string]string) bool {
func connected(engine string, database, environment, file map[string]string) bool {
switch engine {
case "evolink":
return strings.TrimSpace(lookup(provider, environment, file, "EVOLINK_API_KEY", "")) != ""
return strings.TrimSpace(lookup(database, environment, file, "EVOLINK_API_KEY", "")) != ""
case "seedance":
return strings.TrimSpace(lookup(provider, environment, file, "SEEDANCE_API_KEY", "")) != ""
return strings.TrimSpace(lookup(database, environment, file, "SEEDANCE_API_KEY", "")) != ""
case "bailian":
return strings.TrimSpace(lookup(provider, environment, file, "BAILIAN_API_KEY", "")) != "" || strings.TrimSpace(lookup(provider, environment, file, "DASHSCOPE_API_KEY", "")) != ""
return strings.TrimSpace(lookup(database, environment, file, "BAILIAN_API_KEY", "")) != "" || strings.TrimSpace(lookup(database, environment, file, "DASHSCOPE_API_KEY", "")) != ""
default:
return strings.TrimSpace(lookup(provider, environment, file, "VOLCENGINE_ACCESS_KEY_ID", "")) != "" && strings.TrimSpace(lookup(provider, environment, file, "VOLCENGINE_SECRET_ACCESS_KEY", "")) != ""
return strings.TrimSpace(lookup(database, environment, file, "VOLCENGINE_ACCESS_KEY_ID", "")) != "" && strings.TrimSpace(lookup(database, environment, file, "VOLCENGINE_SECRET_ACCESS_KEY", "")) != ""
}
}
@@ -622,23 +601,13 @@ var fieldIndex = func() map[string]Field {
return result
}()
var providerSettingKeys = map[string]struct{}{
"VOLCENGINE_ACCESS_KEY_ID": {},
"VOLCENGINE_SECRET_ACCESS_KEY": {},
"EVOLINK_API_KEY": {},
"EVOLINK_BASE_URL": {},
"EVOLINK_IMAGE_MODEL": {},
"EVOLINK_IMAGE_QUALITY": {},
"SEEDANCE_API_KEY": {},
"BAILIAN_API_KEY": {},
"BAILIAN_BASE_URL": {},
"BAILIAN_IMAGE_MODEL": {},
"BAILIAN_VIDEO_MODEL": {},
"IMAGE_GENERATE_ENGINE": {},
"VIDEO_GENERATE_ENGINE": {},
}
func isProviderSetting(key string) bool {
_, ok := providerSettingKeys[key]
return ok
var restartRequiredSettingKeys = map[string]struct{}{
"ZHINIAN_AUTH_REQUIRED": {},
"ZHINIAN_AUTH_SESSION_SECRET": {},
"ZHINIAN_BILLING_REQUIRED": {},
"ALI_OSS_ENDPOINT": {},
"ALI_OSS_BUCKET": {},
"ALI_OSS_ACCESS_KEY_ID": {},
"ALI_OSS_ACCESS_KEY_SECRET": {},
"ALI_OSS_PUBLIC_BASE_URL": {},
}