feat: allow production provider bootstrap

This commit is contained in:
inman
2026-08-18 00:36:05 +08:00
parent 6480e503eb
commit 22fc7a56d8
21 changed files with 192 additions and 18 deletions

View File

@@ -83,6 +83,9 @@ func New(options Options) (*App, error) {
if err := validateProductionProviderConfiguration(getenv); err != nil {
return nil, err
}
if allowUnconfiguredProviders(getenv) {
log.Printf("WARNING: %s is enabled; the Go API will start with unconfigured providers, and generation/quote requests will remain unavailable until credentials are added and the process is restarted", "ZHINIAN_ALLOW_UNCONFIGURED_PROVIDERS")
}
}
database, err := postgres.Open(ctx, config)
if err != nil {
@@ -265,11 +268,15 @@ func New(options Options) (*App, error) {
if providerRegistry == nil {
providerRegistry = buildProviderRegistry(getenv)
}
var unavailableProviders map[string]string
if config.Backend == postgres.BackendPostgres {
unavailableProviders = providerUnavailableMessages(getenv)
}
jobService := jobs.NewService(jobStore, nil)
jobBuilder := jobs.ProviderJobBuilder{
ImageProvider: imageProvider(getenv), VideoProvider: videoProvider(getenv),
ImageModel: imageModel(getenv), VideoModel: videoModel(getenv), ImageEngine: imageEngine(getenv), VideoEngine: videoEngine(getenv),
ImageEngines: providerImageTargets(getenv), VideoEngines: providerVideoTargets(getenv), NewID: applicationJobID,
ImageEngines: providerImageTargets(getenv), VideoEngines: providerVideoTargets(getenv), UnavailableProviders: unavailableProviders, NewID: applicationJobID,
}
usageService := usage.Service{
Repository: usageRepository,

View File

@@ -77,6 +77,19 @@ func TestApplicationRejectsInvalidProductionDatabaseConfiguration(t *testing.T)
}
}
func TestProductionProviderBootstrapFlagAllowsApplicationComposition(t *testing.T) {
app, err := application.New(application.Options{Getenv: applicationEnv(map[string]string{
"NODE_ENV": "production",
"ZHINIAN_DATA_BACKEND": "postgres",
"DATABASE_URL": "postgres://user:password@127.0.0.1:5432/zhinian",
"ZHINIAN_ALLOW_UNCONFIGURED_PROVIDERS": "true",
})})
if err != nil {
t.Fatalf("New() with provider bootstrap flag = %v", err)
}
app.Close()
}
func TestProductionLocalBackendNeverGrantsAnonymousAdministrator(t *testing.T) {
app, err := application.New(application.Options{Getenv: applicationEnv(map[string]string{
"NODE_ENV": "production",

View File

@@ -59,6 +59,10 @@ func parseBool(value string) bool {
}
}
func allowUnconfiguredProviders(getenv postgres.Getenv) bool {
return parseBool(getenv("ZHINIAN_ALLOW_UNCONFIGURED_PROVIDERS"))
}
func applicationJobID() string {
return applicationID("job")
}
@@ -186,7 +190,27 @@ func providerVideoTargets(getenv postgres.Getenv) map[string]jobs.ProviderTarget
}
}
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。"
}
return missing
}
func validateProductionProviderConfiguration(getenv postgres.Getenv) error {
if allowUnconfiguredProviders(getenv) {
return nil
}
missing := make([]string, 0, 4)
if strings.TrimSpace(getenv("VOLCENGINE_ACCESS_KEY_ID")) == "" || strings.TrimSpace(getenv("VOLCENGINE_SECRET_ACCESS_KEY")) == "" {
missing = append(missing, "即梦 VOLCENGINE_ACCESS_KEY_ID/VOLCENGINE_SECRET_ACCESS_KEY")

View File

@@ -128,6 +128,26 @@ func TestValidateProductionProviderConfigurationRequiresAllRealCredentials(t *te
}
}
func TestValidateProductionProviderConfigurationCanBeSkippedForBootstrap(t *testing.T) {
values := map[string]string{"ZHINIAN_ALLOW_UNCONFIGURED_PROVIDERS": "true"}
if err := validateProductionProviderConfiguration(func(name string) string { return values[name] }); err != nil {
t.Fatalf("validateProductionProviderConfiguration() with bootstrap flag = %v", err)
}
}
func TestProviderUnavailableMessagesIdentifyOnlyMissingCredentials(t *testing.T) {
values := map[string]string{
"VOLCENGINE_ACCESS_KEY_ID": "access",
"VOLCENGINE_SECRET_ACCESS_KEY": "secret",
"EVOLINK_API_KEY": "evolink",
"BAILIAN_API_KEY": "bailian",
}
missing := providerUnavailableMessages(func(name string) string { return values[name] })
if len(missing) != 1 || !strings.Contains(missing["seedance"], "SEEDANCE_API_KEY") {
t.Fatalf("missing = %#v", missing)
}
}
func TestProviderTargetsNeverSelectRemovedProvider(t *testing.T) {
getenv := func(string) string { return "" }
for engine, target := range providerImageTargets(getenv) {