package application import ( "context" "crypto/rand" "encoding/hex" "errors" "fmt" "io" "net/http" "path" "path/filepath" "strconv" "strings" "time" "git.nianxx.cn/wangxuming/NianAIGC/backend/internal/assets" "git.nianxx.cn/wangxuming/NianAIGC/backend/internal/billing" "git.nianxx.cn/wangxuming/NianAIGC/backend/internal/httpapi" "git.nianxx.cn/wangxuming/NianAIGC/backend/internal/jobs" "git.nianxx.cn/wangxuming/NianAIGC/backend/internal/logging" "git.nianxx.cn/wangxuming/NianAIGC/backend/internal/postgres" "git.nianxx.cn/wangxuming/NianAIGC/backend/internal/providers" "git.nianxx.cn/wangxuming/NianAIGC/backend/internal/settings" "git.nianxx.cn/wangxuming/NianAIGC/backend/internal/webhook" ) func unavailableHandler(status int) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(status) }) } func firstNonEmpty(values ...string) string { for _, value := range values { if value = strings.TrimSpace(value); value != "" { return value } } return "" } func positiveInt64Env(getenv postgres.Getenv, name string, fallback int64) int64 { return positiveInt64Value(getenv(name), fallback) } func positiveInt64Value(raw string, fallback int64) int64 { value, err := strconv.ParseInt(strings.TrimSpace(raw), 10, 64) if err != nil || value <= 0 { return fallback } return value } func durationEnv(getenv postgres.Getenv, name string, fallback time.Duration) time.Duration { milliseconds := positiveInt64Env(getenv, name, fallback.Milliseconds()) return time.Duration(milliseconds) * time.Millisecond } func parseBool(value string) bool { switch strings.ToLower(strings.TrimSpace(value)) { case "1", "true", "yes", "on": return true default: return false } } func applicationJobID() string { return applicationID("job") } func applicationID(prefix string) string { raw := make([]byte, 12) if _, err := rand.Read(raw); err == nil { return prefix + "-" + hex.EncodeToString(raw) } return prefix + "-" + strconv.FormatInt(time.Now().UnixNano(), 36) } func imageEngine(getenv postgres.Getenv) string { engine := strings.ToLower(firstNonEmpty(getenv("IMAGE_GENERATE_ENGINE"), getenv("IMAGE_CREATION_ENGINE"), getenv("IMAGE_PROVIDER"), "jimeng")) if engine != "evolink" && engine != "bailian" && engine != "seedream" { return "jimeng" } return engine } func videoEngine(getenv postgres.Getenv) string { switch strings.ToLower(strings.TrimSpace(getenv("VIDEO_GENERATE_ENGINE"))) { case "seedance": return "seedance" case "minimax": return "minimax" default: return "bailian" } } func imageProvider(getenv postgres.Getenv) string { switch imageEngine(getenv) { case "evolink": return "evolink" case "bailian": return "bailian" case "seedream": return "seedream" default: return "volcengine-visual" } } func videoProvider(getenv postgres.Getenv) string { return videoEngine(getenv) } func imageModel(getenv postgres.Getenv) string { switch imageEngine(getenv) { case "evolink": return firstNonEmpty(getenv("EVOLINK_IMAGE_MODEL"), "gpt-image-2") case "bailian": return firstNonEmpty(getenv("BAILIAN_IMAGE_MODEL"), "wan2.7-image-pro") case "seedream": return providers.Seedream50ProModel default: return firstNonEmpty(getenv("JIMENG_IMAGE_GENERATE_46_REQ_KEY"), "jimeng_seedream46_cvtob") } } func videoModel(getenv postgres.Getenv) string { switch videoEngine(getenv) { case "seedance": return firstNonEmpty(getenv("SEEDANCE_MODEL"), "doubao-seedance-2-0-260128") case "minimax": return providers.MinimaxH3Model default: return firstNonEmpty(getenv("BAILIAN_VIDEO_MODEL"), "wan2.7-i2v-2026-04-25") } } func bailianAPIKey(getenv postgres.Getenv) string { return firstNonEmpty(getenv("BAILIAN_API_KEY"), getenv("DASHSCOPE_API_KEY")) } func bailianNativeBaseURL(getenv postgres.Getenv) string { base := strings.TrimRight(firstNonEmpty(getenv("BAILIAN_BASE_URL"), "https://llm-126wneubbdo6dbr5.cn-beijing.maas.aliyuncs.com/compatible-mode/v1"), "/") lower := strings.ToLower(base) const suffix = "/compatible-mode/v1" if strings.HasSuffix(lower, suffix) { return base[:len(base)-len(suffix)] } return base } func buildProviderRegistry(getenv postgres.Getenv) jobs.ProviderRegistry { client := &http.Client{Timeout: durationEnv(getenv, "ZHINIAN_PROVIDER_TIMEOUT_MS", 30*time.Second)} maximum := positiveInt64Env(getenv, "ZHINIAN_PROVIDER_MAX_RESPONSE_BYTES", 2<<20) return buildProviderRegistryWithClient(getenv, client, maximum) } func buildProviderRegistryWithClient(getenv postgres.Getenv, client *http.Client, maximum int64) jobs.ProviderRegistry { return jobs.ProviderRegistry{ "volcengine-visual": providers.NewVolcengine(providers.Config{ BaseURL: firstNonEmpty(getenv("VOLCENGINE_VISUAL_ENDPOINT"), "https://visual.volcengineapi.com"), Model: imageModel(getenv), AccessKeyID: getenv("VOLCENGINE_ACCESS_KEY_ID"), SecretAccessKey: getenv("VOLCENGINE_SECRET_ACCESS_KEY"), Region: firstNonEmpty(getenv("VOLCENGINE_REGION"), "cn-north-1"), Service: firstNonEmpty(getenv("VOLCENGINE_SERVICE"), "cv"), MaxResponseBytes: maximum, }, client, nil), "evolink": providers.NewEvoLink(providers.Config{BaseURL: firstNonEmpty(getenv("EVOLINK_BASE_URL"), "https://api.evolink.ai"), APIKey: getenv("EVOLINK_API_KEY"), Model: firstNonEmpty(getenv("EVOLINK_IMAGE_MODEL"), "gpt-image-2"), MaxResponseBytes: maximum}, client), "bailian": providers.NewBailian(providers.Config{BaseURL: bailianNativeBaseURL(getenv), APIKey: bailianAPIKey(getenv), Model: firstNonEmpty(getenv("BAILIAN_IMAGE_MODEL"), "wan2.7-image-pro"), MaxResponseBytes: maximum}, client), "seedance": providers.NewSeedance(providers.Config{BaseURL: firstNonEmpty(getenv("SEEDANCE_BASE_URL"), "https://ark.cn-beijing.volces.com/api/v3"), APIKey: getenv("SEEDANCE_API_KEY"), Model: firstNonEmpty(getenv("SEEDANCE_MODEL"), "doubao-seedance-2-0-260128"), MaxResponseBytes: maximum}, client), "seedream": providers.NewSeedream(providers.Config{BaseURL: firstNonEmpty(getenv("SEEDANCE_BASE_URL"), "https://ark.cn-beijing.volces.com/api/v3"), APIKey: getenv("SEEDANCE_API_KEY"), Model: providers.Seedream50ProModel, MaxResponseBytes: maximum}, client), "minimax": providers.NewMinimax(providers.Config{BaseURL: firstNonEmpty(getenv("MINIMAX_BASE_URL"), "https://api.minimax.cn"), APIKey: getenv("MINIMAX_API_KEY"), Model: providers.MinimaxH3Model, MaxResponseBytes: maximum}, client), } } func providerImageTargets(getenv postgres.Getenv) map[string]jobs.ProviderTarget { return map[string]jobs.ProviderTarget{ "jimeng": { Provider: "volcengine-visual", Model: firstNonEmpty(getenv("JIMENG_IMAGE_GENERATE_46_REQ_KEY"), "jimeng_seedream46_cvtob"), }, "evolink": { Provider: "evolink", Model: firstNonEmpty(getenv("EVOLINK_IMAGE_MODEL"), "gpt-image-2"), Settings: map[string]any{"quality": firstNonEmpty(getenv("EVOLINK_IMAGE_QUALITY"), "medium")}, }, "bailian": { Provider: "bailian", Model: firstNonEmpty(getenv("BAILIAN_IMAGE_MODEL"), "wan2.7-image-pro"), }, "seedream": { Provider: "seedream", Model: providers.Seedream50ProModel, Settings: map[string]any{"size": "1.5K", "outputFormat": "png", "optimizeMode": "standard", "imageCount": float64(1)}, }, } } func providerImageModels(getenv postgres.Getenv) map[string]jobs.ProviderTarget { models := []string{"gpt-image-2", "gpt-image-2.5-flare", "gpt-image-2.5-sunburst", firstNonEmpty(getenv("EVOLINK_IMAGE_MODEL"), "gpt-image-2")} targets := make(map[string]jobs.ProviderTarget, len(models)) for _, model := range models { targets[model] = jobs.ProviderTarget{ Provider: "evolink", Model: model, Settings: map[string]any{"quality": firstNonEmpty(getenv("EVOLINK_IMAGE_QUALITY"), "medium")}, } } return targets } func providerVideoTargets(getenv postgres.Getenv) map[string]jobs.ProviderTarget { seedanceModel := firstNonEmpty(getenv("SEEDANCE_MODEL"), "doubao-seedance-2-0-260128") return map[string]jobs.ProviderTarget{ "seedance": { Provider: "seedance", Model: seedanceModel, Settings: seedanceDefaultSettings(getenv, seedanceModel), }, "bailian": { Provider: "bailian", Model: firstNonEmpty(getenv("BAILIAN_VIDEO_MODEL"), "wan2.7-i2v-2026-04-25"), }, "minimax": { Provider: "minimax", Model: providers.MinimaxH3Model, Settings: map[string]any{"ratio": "16:9", "duration": float64(5), "resolution": "768P"}, }, } } func providerVideoModels(getenv postgres.Getenv) map[string]jobs.ProviderTarget { models := []string{ "doubao-seedance-2-0-260128", "doubao-seedance-2-5-260628", firstNonEmpty(getenv("SEEDANCE_MODEL"), "doubao-seedance-2-0-260128"), } targets := make(map[string]jobs.ProviderTarget, len(models)) for _, model := range models { targets[model] = jobs.ProviderTarget{Provider: "seedance", Model: model, Settings: seedanceDefaultSettings(getenv, model)} } return targets } func seedanceDefaultSettings(getenv postgres.Getenv, model string) map[string]any { duration := positiveInt64Value(firstNonEmpty(getenv("SEEDANCE_DURATION"), getenv("SEEDANCE_DEFAULT_DURATION")), 5) maximum := int64(15) if model == "doubao-seedance-2-5-260628" { maximum = 30 } if duration < 4 { duration = 4 } else if duration > maximum { duration = maximum } return map[string]any{ "ratio": firstNonEmpty(getenv("SEEDANCE_RATIO"), getenv("SEEDANCE_DEFAULT_RATIO"), "9:16"), "duration": float64(duration), "resolution": firstNonEmpty(getenv("SEEDANCE_RESOLUTION"), getenv("SEEDANCE_DEFAULT_RESOLUTION"), "720p"), } } func providerUnavailableMessages(getenv postgres.Getenv) map[string]string { missing := map[string]string{} if strings.TrimSpace(getenv("VOLCENGINE_ACCESS_KEY_ID")) == "" || strings.TrimSpace(getenv("VOLCENGINE_SECRET_ACCESS_KEY")) == "" { missing["volcengine-visual"] = "即梦服务商未配置,请先配置 VOLCENGINE_ACCESS_KEY_ID 和 VOLCENGINE_SECRET_ACCESS_KEY。" } if strings.TrimSpace(getenv("EVOLINK_API_KEY")) == "" { missing["evolink"] = "EvoLink 服务商未配置,请先配置 EVOLINK_API_KEY。" } if bailianAPIKey(getenv) == "" { missing["bailian"] = "百炼服务商未配置,请先配置 BAILIAN_API_KEY 或 DASHSCOPE_API_KEY。" } if strings.TrimSpace(getenv("SEEDANCE_API_KEY")) == "" { missing["seedance"] = "Seedance 服务商未配置,请先配置 SEEDANCE_API_KEY。" missing["seedream"] = "Seedream 服务商未配置,请先配置火山方舟 API Key(SEEDANCE_API_KEY)。" } if strings.TrimSpace(getenv("MINIMAX_API_KEY")) == "" { missing["minimax"] = "MiniMax 服务商未配置,请先配置 MINIMAX_API_KEY。" } return missing } func defaultWebhookSender(getenv postgres.Getenv) (*webhook.HTTPSender, error) { return webhook.NewPublicHTTPSender( durationEnv(getenv, "ZHINIAN_WEBHOOK_TIMEOUT_MS", 10*time.Second), webhook.NewPublicDestinationPolicy(nil, nil), ) } func capabilitySummary(getenv postgres.Getenv) func(context.Context) (any, error) { return func(context.Context) (any, error) { engine := imageEngine(getenv) provider := map[string]string{"jimeng": "volcengine-visual", "evolink": "evolink", "bailian": "bailian", "seedream": "seedream"}[engine] primaryVideoEngine := videoEngine(getenv) primaryVideo := map[string]any{ "id": "video.generate", "kind": "video", "engine": primaryVideoEngine, "provider": primaryVideoEngine, "reqKey": videoModel(getenv), } if primaryVideoEngine == "seedance" { primaryVideo["label"] = "Seedance 视频生成" primaryVideo["limits"] = seedanceCapabilityLimits(videoModel(getenv)) primaryVideo["models"] = seedanceModelOptions() } else if primaryVideoEngine == "minimax" { primaryVideo["label"] = "MiniMax H3 视频生成" primaryVideo["limits"] = minimaxVideoCapabilityLimits() } else { primaryVideo["label"] = "百炼图生视频" primaryVideo["limits"] = bailianVideoCapabilityLimits() } imageCapability := map[string]any{"id": "image.generate", "label": imageCapabilityLabel(engine), "kind": "image", "engine": engine, "provider": provider, "reqKey": imageModel(getenv)} imageCapability["evolink"] = evolinkImageCapability(getenv) if engine == "evolink" { imageCapability["models"] = evolinkImageModelOptions(getenv) } return []any{ imageCapability, primaryVideo, map[string]any{"id": "video.generate.bailian", "label": "百炼图生视频", "kind": "video", "engine": "bailian", "provider": "bailian", "reqKey": firstNonEmpty(getenv("BAILIAN_VIDEO_MODEL"), "wan2.7-i2v-2026-04-25"), "limits": bailianVideoCapabilityLimits()}, map[string]any{"id": "video.generate.minimax", "label": "MiniMax H3 视频生成", "kind": "video", "engine": "minimax", "provider": "minimax", "reqKey": providers.MinimaxH3Model, "limits": minimaxVideoCapabilityLimits()}, }, nil } } func evolinkImageModelOptions(getenv postgres.Getenv) []any { options := []any{ map[string]any{"id": "gpt-image-2", "label": "GPT Image 2"}, map[string]any{"id": "gpt-image-2.5-flare", "label": "GPT Image 2.5 Flare"}, map[string]any{"id": "gpt-image-2.5-sunburst", "label": "GPT Image 2.5 Sunburst"}, } custom := firstNonEmpty(getenv("EVOLINK_IMAGE_MODEL"), "gpt-image-2") if custom != "gpt-image-2" && custom != "gpt-image-2.5-flare" && custom != "gpt-image-2.5-sunburst" { options = append(options, map[string]any{"id": custom, "label": custom}) } return options } func evolinkImageCapability(getenv postgres.Getenv) map[string]any { return map[string]any{ "reqKey": firstNonEmpty(getenv("EVOLINK_IMAGE_MODEL"), "gpt-image-2"), "models": evolinkImageModelOptions(getenv), } } func seedanceCapabilityLimits(model string) map[string]any { maximum := 15 if model == "doubao-seedance-2-5-260628" { maximum = 30 } return map[string]any{"durationSeconds": map[string]int{"min": 4, "max": maximum}, "ratios": []string{"16:9", "4:3", "1:1", "3:4", "9:16", "21:9", "adaptive"}, "resolutions": []string{"480p", "720p", "1080p"}} } func seedanceModelOptions() []any { return []any{ map[string]any{"id": "doubao-seedance-2-0-260128", "label": "Seedance 2.0", "limits": seedanceCapabilityLimits("doubao-seedance-2-0-260128")}, map[string]any{"id": "doubao-seedance-2-5-260628", "label": "Seedance 2.5", "limits": seedanceCapabilityLimits("doubao-seedance-2-5-260628")}, } } func bailianVideoCapabilityLimits() map[string]any { return map[string]any{"inputImages": map[string]int{"min": 1, "max": 2}, "durationSeconds": map[string]int{"min": 2, "max": 15}, "resolutions": []string{"720P", "1080P"}} } func minimaxVideoCapabilityLimits() map[string]any { return map[string]any{ "inputImages": map[string]int{"min": 0, "max": 1}, "durationSeconds": map[string]int{"min": 4, "max": 15}, "ratios": []string{"21:9", "16:9", "4:3", "1:1", "3:4", "9:16", "adaptive"}, "resolutions": []string{"768P", "2K"}, } } func runtimeHealthDetails(getenv postgres.Getenv) httpapi.HealthDetails { image := imageEngine(getenv) imageLabel := map[string]string{"jimeng": "即梦", "evolink": "EvoLink", "bailian": "阿里云百炼", "seedream": "Seedream 5.0 Pro"}[image] video := videoEngine(getenv) videoLabel := map[string]string{"seedance": "Seedance", "bailian": "阿里云百炼", "minimax": "MiniMax H3"}[video] visualConfigured := strings.TrimSpace(getenv("VOLCENGINE_ACCESS_KEY_ID")) != "" && strings.TrimSpace(getenv("VOLCENGINE_SECRET_ACCESS_KEY")) != "" auth, _ := ParseAuthConfig(getenv) bailianKey := bailianAPIKey(getenv) authMode := "disabled" if auth.Required { authMode = "missing" if auth.Configured { authMode = "configured" } } videoCapability := map[string]any{"id": "video.generate", "label": "视频生成", "engine": video, "engineLabel": videoLabel, "reqKey": videoModel(getenv)} imageCapability := map[string]any{"id": "image.generate", "label": imageCapabilityLabel(image), "engine": image, "engineLabel": imageLabel, "reqKey": imageModel(getenv)} imageCapability["evolink"] = evolinkImageCapability(getenv) if image == "evolink" { imageCapability["models"] = evolinkImageModelOptions(getenv) } if video == "seedance" { videoCapability["models"] = seedanceModelOptions() } return httpapi.HealthDetails{ VisualAPIMode: providerMode(visualConfigured, "volcengine"), EvolinkMode: providerMode(strings.TrimSpace(getenv("EVOLINK_API_KEY")) != "", "evolink"), SeedanceMode: providerMode(strings.TrimSpace(getenv("SEEDANCE_API_KEY")) != "", "seedance"), BailianMode: providerMode(bailianKey != "", "bailian"), MinimaxMode: providerMode(strings.TrimSpace(getenv("MINIMAX_API_KEY")) != "", "minimax"), AuthMode: authMode, Capabilities: []any{ imageCapability, videoCapability, }, } } func imageCapabilityLabel(engine string) string { if engine == "seedream" { return "Seedream 5.0 Pro 图片生成" } return "图片生成 4.6" } func providerMode(configured bool, live string) string { if !configured { return "missing" } return live } func remoteAssetMaxBytes(getenv postgres.Getenv) int64 { return positiveInt64Env(getenv, "ZHINIAN_REMOTE_ASSET_MAX_BYTES", 20<<20) } var runtimeSettingKeys = []string{ "ALI_OSS_ACCESS_KEY_ID", "ALI_OSS_ACCESS_KEY_SECRET", "ALI_OSS_BUCKET", "ALI_OSS_ENDPOINT", "ALI_OSS_PUBLIC_BASE_URL", "BAILIAN_API_KEY", "BAILIAN_BASE_URL", "BAILIAN_IMAGE_MODEL", "BAILIAN_VIDEO_MODEL", "DATABASE_URL", "DASHSCOPE_API_KEY", "EVOLINK_API_KEY", "EVOLINK_BASE_URL", "EVOLINK_IMAGE_MODEL", "EVOLINK_IMAGE_QUALITY", "IMAGE_GENERATE_ENGINE", "MINIMAX_API_KEY", "MINIMAX_BASE_URL", "SEEDANCE_API_KEY", "SEEDANCE_MODEL", "VIDEO_GENERATE_ENGINE", "VOLCENGINE_ACCESS_KEY_ID", "VOLCENGINE_SECRET_ACCESS_KEY", "ZHINIAN_AUTH_REQUIRED", "ZHINIAN_AUTH_SESSION_SECRET", "ZHINIAN_BILLING_ACCOUNT_BANK", "ZHINIAN_BILLING_ACCOUNT_NAME", "ZHINIAN_BILLING_ACCOUNT_NUMBER", "ZHINIAN_BILLING_CONTACT", "ZHINIAN_BILLING_REQUIRED", } func runtimeEnvironment(getenv postgres.Getenv) map[string]string { values := make(map[string]string, len(runtimeSettingKeys)) for _, key := range runtimeSettingKeys { if value := getenv(key); value != "" { values[key] = value } } return values } func defaultSettingsService(getenv postgres.Getenv) *settings.Service { path := firstNonEmpty(getenv("ZHINIAN_SETTINGS_FILE"), ".env.local") return settings.New(path, runtimeEnvironment(getenv), nil) } func databaseSettingsService(getenv postgres.Getenv, repository settings.RuntimeSettingsRepository) *settings.Service { path := firstNonEmpty(getenv("ZHINIAN_SETTINGS_FILE"), ".env.local") return settings.NewWithRuntimeRepository(path, runtimeEnvironment(getenv), repository, nil) } func runtimeSettingsGetenv(ctx context.Context, fallback postgres.Getenv, service *settings.Service) (postgres.Getenv, error) { values, err := service.RuntimeValues(ctx) if err != nil { return nil, err } return mergedSettingsGetenv(values, fallback), nil } func cachedRuntimeSettingsGetenv(fallback postgres.Getenv, service *settings.Service) postgres.Getenv { return mergedSettingsGetenv(service.CachedRuntimeValues(), fallback) } func mergedSettingsGetenv(values map[string]string, fallback postgres.Getenv) postgres.Getenv { return func(name string) string { if value, ok := values[name]; ok { return value } return fallback(name) } } type runtimeProviderResolver struct { fallback postgres.Getenv settings *settings.Service client *http.Client maximum int64 } func newRuntimeProviderResolver(fallback postgres.Getenv, service *settings.Service) *runtimeProviderResolver { return &runtimeProviderResolver{ fallback: fallback, settings: service, client: &http.Client{Timeout: durationEnv(fallback, "ZHINIAN_PROVIDER_TIMEOUT_MS", 30*time.Second)}, maximum: positiveInt64Env(fallback, "ZHINIAN_PROVIDER_MAX_RESPONSE_BYTES", 2<<20), } } func (resolver *runtimeProviderResolver) Resolve(ctx context.Context, name string) (providers.Adapter, error) { getenv, err := runtimeSettingsGetenv(ctx, resolver.fallback, resolver.settings) if err != nil { return nil, err } return buildProviderRegistryWithClient(getenv, resolver.client, resolver.maximum)[name], nil } type runtimeProviderJobBuilder struct { fallback postgres.Getenv settings *settings.Service enforceAvailability bool } func (builder runtimeProviderJobBuilder) Build(ctx context.Context, owner, client, capability, idempotency string, body map[string]any) (jobs.CreateCommand, error) { getenv, err := runtimeSettingsGetenv(ctx, builder.fallback, builder.settings) if err != nil { return jobs.CreateCommand{}, fmt.Errorf("load runtime provider settings: %w", err) } return configuredProviderJobBuilder(getenv, builder.enforceAvailability).Build(ctx, owner, client, capability, idempotency, body) } func configuredProviderJobBuilder(getenv postgres.Getenv, enforceAvailability bool) jobs.ProviderJobBuilder { var unavailable map[string]string if enforceAvailability { unavailable = providerUnavailableMessages(getenv) } return jobs.ProviderJobBuilder{ ImageProvider: imageProvider(getenv), VideoProvider: videoProvider(getenv), ImageModel: imageModel(getenv), VideoModel: videoModel(getenv), ImageEngine: imageEngine(getenv), VideoEngine: videoEngine(getenv), ImageEngines: providerImageTargets(getenv), ImageModels: providerImageModels(getenv), VideoEngines: providerVideoTargets(getenv), VideoModels: providerVideoModels(getenv), UnavailableProviders: unavailable, NewID: applicationJobID, } } func runtimeCapabilitySummary(fallback postgres.Getenv, service *settings.Service) func(context.Context) (any, error) { return func(ctx context.Context) (any, error) { getenv, err := runtimeSettingsGetenv(ctx, fallback, service) if err != nil { return nil, fmt.Errorf("load runtime provider settings: %w", err) } return capabilitySummary(getenv)(ctx) } } func runtimeHealthDetailsProvider(fallback postgres.Getenv, service *settings.Service) func(context.Context) httpapi.HealthDetails { return func(ctx context.Context) httpapi.HealthDetails { refreshContext, cancel := context.WithTimeout(ctx, 250*time.Millisecond) defer cancel() getenv, err := runtimeSettingsGetenv(refreshContext, fallback, service) if err != nil { getenv = cachedRuntimeSettingsGetenv(fallback, service) } return runtimeHealthDetails(getenv) } } type settingsBillingAccountStore struct{ service *settings.Service } func (store settingsBillingAccountStore) Load(ctx context.Context) (billing.AccountConfig, error) { value, err := store.service.Get(ctx) if err != nil { return billing.AccountConfig{}, err } payload, ok := value.(settings.Payload) if !ok { return billing.AccountConfig{}, fmt.Errorf("load billing account settings: unexpected settings payload") } fields := map[string]string{} for _, group := range payload.Groups { for _, field := range group.Fields { fields[field.Key] = field.Value } } return billing.AccountConfig{ AccountName: fields["ZHINIAN_BILLING_ACCOUNT_NAME"], BankName: fields["ZHINIAN_BILLING_ACCOUNT_BANK"], AccountNumber: fields["ZHINIAN_BILLING_ACCOUNT_NUMBER"], Contact: fields["ZHINIAN_BILLING_CONTACT"], }, nil } func (store settingsBillingAccountStore) Save(ctx context.Context, value billing.AccountConfig) error { _, err := store.service.Save(ctx, map[string]any{ "ZHINIAN_BILLING_ACCOUNT_NAME": value.AccountName, "ZHINIAN_BILLING_ACCOUNT_BANK": value.BankName, "ZHINIAN_BILLING_ACCOUNT_NUMBER": value.AccountNumber, "ZHINIAN_BILLING_CONTACT": value.Contact, }) return err } type logServiceAdapter struct{ service *logging.Service } func (adapter logServiceAdapter) List(ctx context.Context, filters httpapi.LogFilters) (any, error) { return adapter.service.List(ctx, logging.Filters{Level: filters.Level, Q: filters.Q, Source: filters.Source, Limit: filters.Limit}) } func (adapter logServiceAdapter) Clear(ctx context.Context) error { return adapter.service.Clear(ctx) } func (adapter logServiceAdapter) Append(ctx context.Context, input logging.Input) (logging.Entry, error) { return adapter.service.Append(ctx, input) } func defaultLogService(getenv postgres.Getenv) httpapi.LogService { runtimeDirectory := firstNonEmpty(getenv("ZHINIAN_RUNTIME_DIR"), ".runtime") logDirectory := firstNonEmpty(getenv("ZHINIAN_LOG_DIR"), filepath.Join(runtimeDirectory, "logs")) return logServiceAdapter{service: logging.New(filepath.Join(logDirectory, "server-events.jsonl"), positiveInt64Env(getenv, "ZHINIAN_LOG_MAX_BYTES", 5<<20), nil, nil)} } type prefixedBlobStore struct { prefix string store assets.BlobStore } func (store prefixedBlobStore) Put(ctx context.Context, key string, body io.Reader, size int64, contentType string) (assets.StoredObject, error) { stored, err := store.store.Put(ctx, path.Join(store.prefix, key), body, size, contentType) if err != nil { return assets.StoredObject{}, err } // StoragePath is an application key used by /uploads and // /generated-results. The OSS namespace prefix is private to this adapter. stored.Key = key return stored, nil } func (store prefixedBlobStore) Read(ctx context.Context, key string) (assets.Blob, error) { return store.store.Read(ctx, path.Join(store.prefix, key)) } func (store prefixedBlobStore) Delete(ctx context.Context, key string) error { return store.store.Delete(ctx, path.Join(store.prefix, key)) } func (store prefixedBlobStore) SignReadURL(key string, ttl time.Duration) (string, error) { signer, ok := store.store.(assets.BlobURLSigner) if !ok { return "", errors.New("blob store does not support signed URLs") } return signer.SignReadURL(path.Join(store.prefix, key), ttl) } func configuredOSSBlobStore(getenv postgres.Getenv) (assets.BlobStore, bool, error) { endpoint, bucket := strings.TrimSpace(getenv("ALI_OSS_ENDPOINT")), strings.TrimSpace(getenv("ALI_OSS_BUCKET")) accessKeyID, secret := strings.TrimSpace(getenv("ALI_OSS_ACCESS_KEY_ID")), strings.TrimSpace(getenv("ALI_OSS_ACCESS_KEY_SECRET")) publicURL := strings.TrimSpace(getenv("ALI_OSS_PUBLIC_BASE_URL")) if endpoint == "" || bucket == "" || accessKeyID == "" || secret == "" || publicURL == "" { return nil, false, nil } client, err := assets.NewOSSHTTPClient(accessKeyID, secret, &http.Client{Timeout: durationEnv(getenv, "ZHINIAN_OSS_TIMEOUT_MS", 30*time.Second)}, nil) if err != nil { return nil, false, err } store, err := assets.NewOSS(assets.OSSConfig{Endpoint: endpoint, Bucket: bucket, PublicBaseURL: publicURL, PublicRead: false}, client) if err != nil { return nil, false, err } prefix := strings.Trim(strings.TrimSpace(getenv("ALI_OSS_PREFIX")), "/") if prefix == "" { prefix = "zhinian" } return prefixedBlobStore{prefix: prefix, store: store}, true, nil }