feat: allow production provider bootstrap #2

Merged
brother7 merged 1 commits from agent/production-runtime-mock-removal into main 2026-08-18 00:40:17 +08:00
21 changed files with 192 additions and 18 deletions

View File

@@ -37,6 +37,10 @@ ZHINIAN_WORKER_RETRY_BASE_MS=10000
ZHINIAN_WORKER_RETRY_MAX_MS=300000
ZHINIAN_WORKER_REQUEST_TIMEOUT_MS=120000
ZHINIAN_GO_EMBEDDED_WORKER=true
# Temporary production bootstrap only: allows the Go API to start before provider
# credentials are available. Generation and quote requests remain unavailable
# until credentials are saved and the Go API is restarted. Keep false/empty after setup.
ZHINIAN_ALLOW_UNCONFIGURED_PROVIDERS=false
# Data layer. Production and Docker Compose use PostgreSQL; set local only for
# an explicitly non-production single-process development run.

View File

@@ -222,10 +222,11 @@ cp .env.example .env.local
- `SEEDANCE_RESOLUTION`:支持 `480p``720p``1080p``4k`Seedance 2.0 fast 不支持 `1080p`
- `ALI_OSS_*`:用于上传素材和生成结果转存
- `ZHINIAN_DATA_BACKEND`:生产使用 `postgres`,开发可使用 `local`
- `ZHINIAN_ALLOW_UNCONFIGURED_PROVIDERS=true`:仅用于临时生产引导;允许 Go API 先启动,但未配置服务商的报价/生成请求会返回 503配置后必须重启并恢复为 `false`
- `DATABASE_URL`:仅服务端读取的 PostgreSQL 连接串
- PostgreSQL 客户端强制使用 `sslmode=disable` 且不读取 CA。ACK 到 RDS 的数据库链路为明文,只应使用 RDS 内网地址,并通过 VPC、安全组和白名单限制访问。
`ZHINIAN_DATA_BACKEND=local` 时,应用使用进程内单实例开发数据层。生产 `postgres` 模式缺少连接配置或真实服务商凭据会在启动时直接失败,不会静默写入本地数据。如果 OSS 未配置,上传和生成结果会保存到 `.runtime/uploads``.runtime/generated-results`,并通过 Go 路由提供访问。
`ZHINIAN_DATA_BACKEND=local` 时,应用使用进程内单实例开发数据层。生产 `postgres` 模式默认在启动时校验真实服务商凭据;临时引导可设置 `ZHINIAN_ALLOW_UNCONFIGURED_PROVIDERS=true`,但未配置服务商的报价和生成请求会返回 503不会静默切换到 Mock配置保存后还必须重启 Go 后端。如果 OSS 未配置,上传和生成结果会保存到 `.runtime/uploads``.runtime/generated-results`,并通过 Go 路由提供访问。
## 数据库

View File

@@ -272,6 +272,7 @@ cp .env.example .env.local
| `ZHINIAN_WEBHOOK_SECRET` | Webhook 签名密钥,可选 |
| `ZHINIAN_WORKER_*` | Worker 间隔、批量、锁超时、重试配置 |
| `ZHINIAN_GO_EMBEDDED_WORKER` | Go 内嵌 Worker 开关,生产设为 `true` |
| `ZHINIAN_ALLOW_UNCONFIGURED_PROVIDERS` | 临时生产引导开关;设为 `true` 可先启动后配置,但未配置服务商的报价/生成请求返回 503配置后必须重启 Go 后端 |
| `IMAGE_GENERATE_ENGINE` | 图片生成引擎:`jimeng``evolink``bailian` |
| `BAILIAN_API_KEY` | 阿里云百炼 API Key |
| `BAILIAN_BASE_URL` | 百炼业务空间兼容地址;系统自动派生原生异步接口 |
@@ -291,7 +292,7 @@ cp .env.example .env.local
| `DATABASE_URL` | PostgreSQL 连接串(仅放 Secret |
| PostgreSQL 传输 | 客户端强制 `sslmode=disable` 且不读取 CAACK 到 RDS 的链路为明文,只应走内网并通过 VPC、安全组和白名单限制访问 |
`ZHINIAN_DATA_BACKEND=local` 时,应用使用进程内单实例开发数据层;生产 `postgres` 模式配置错误或缺少真实服务商凭据会直接失败。未配置 OSS 时,上传和生成结果会写入 `.runtime/uploads``.runtime/generated-results`
`ZHINIAN_DATA_BACKEND=local` 时,应用使用进程内单实例开发数据层;生产 `postgres` 模式默认会在启动时校验真实服务商凭据。若上线引导阶段暂时没有凭据,可将 `ZHINIAN_ALLOW_UNCONFIGURED_PROVIDERS=true` 注入 Go API让服务先启动此时未配置服务商的报价和生成请求会返回 503不会切换到 Mock。通过设置页保存凭据后必须重启 Go 后端,并将该开关恢复为 `false`。未配置 OSS 时,上传和生成结果会写入 `.runtime/uploads``.runtime/generated-results`
## 项目结构

View File

@@ -18,7 +18,7 @@ func TestListenAddressDefaultsToLoopbackAndSupportsExplicitBinding(t *testing.T)
func TestRuntimeGetenvLoadsWhitelistedSettingsAndPreservesProcessPrecedence(t *testing.T) {
path := filepath.Join(t.TempDir(), ".env.local")
if err := os.WriteFile(path, []byte("IMAGE_GENERATE_ENGINE=evolink\nEVOLINK_API_KEY=file-secret\nDATABASE_URL=postgres://must-not-load\n"), 0o600); err != nil {
if err := os.WriteFile(path, []byte("IMAGE_GENERATE_ENGINE=evolink\nEVOLINK_API_KEY=file-secret\nZHINIAN_ALLOW_UNCONFIGURED_PROVIDERS=true\nDATABASE_URL=postgres://must-not-load\n"), 0o600); err != nil {
t.Fatal(err)
}
process := map[string]string{"ZHINIAN_SETTINGS_FILE": path, "EVOLINK_API_KEY": "process-secret", "ZHINIAN_DATA_BACKEND": "local"}
@@ -26,7 +26,7 @@ func TestRuntimeGetenvLoadsWhitelistedSettingsAndPreservesProcessPrecedence(t *t
if err != nil {
t.Fatal(err)
}
if getenv("IMAGE_GENERATE_ENGINE") != "evolink" || getenv("EVOLINK_API_KEY") != "process-secret" || getenv("DATABASE_URL") != "" || getenv("ZHINIAN_DATA_BACKEND") != "local" {
t.Fatalf("loaded engine=%q key=%q database=%q backend=%q", getenv("IMAGE_GENERATE_ENGINE"), getenv("EVOLINK_API_KEY"), getenv("DATABASE_URL"), getenv("ZHINIAN_DATA_BACKEND"))
if getenv("IMAGE_GENERATE_ENGINE") != "evolink" || getenv("EVOLINK_API_KEY") != "process-secret" || getenv("ZHINIAN_ALLOW_UNCONFIGURED_PROVIDERS") != "true" || getenv("DATABASE_URL") != "" || getenv("ZHINIAN_DATA_BACKEND") != "local" {
t.Fatalf("loaded engine=%q key=%q bootstrap=%q database=%q backend=%q", getenv("IMAGE_GENERATE_ENGINE"), getenv("EVOLINK_API_KEY"), getenv("ZHINIAN_ALLOW_UNCONFIGURED_PROVIDERS"), getenv("DATABASE_URL"), getenv("ZHINIAN_DATA_BACKEND"))
}
}

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) {

View File

@@ -92,9 +92,10 @@ func (scope Scope) Owns(job Job) bool {
type ErrorKind string
const (
ErrorInvalid ErrorKind = "invalid"
ErrorNotFound ErrorKind = "not_found"
ErrorConflict ErrorKind = "conflict"
ErrorInvalid ErrorKind = "invalid"
ErrorNotFound ErrorKind = "not_found"
ErrorConflict ErrorKind = "conflict"
ErrorUnavailable ErrorKind = "unavailable"
)
type Error struct {

View File

@@ -121,6 +121,7 @@ type ProviderJobBuilder struct {
ImageEngine, VideoEngine string
ImageEngines map[string]ProviderTarget
VideoEngines map[string]ProviderTarget
UnavailableProviders map[string]string
NewID func() string
}
@@ -141,6 +142,9 @@ func (b ProviderJobBuilder) Build(_ context.Context, owner, client, capability,
if err != nil {
return CreateCommand{}, err
}
if message := strings.TrimSpace(b.UnavailableProviders[target.Provider]); message != "" {
return CreateCommand{}, &Error{Kind: ErrorUnavailable, Status: 503, Message: message}
}
if target.Provider == "" || target.Model == "" || b.NewID == nil {
return CreateCommand{}, errors.New("provider job builder is not configured")
}

View File

@@ -40,6 +40,23 @@ func TestProviderJobBuilderPreparesImageRequestAndEngineOverride(t *testing.T) {
}
}
func TestProviderJobBuilderRejectsUnconfiguredProviderBeforeCreation(t *testing.T) {
b := testProviderBuilder()
b.UnavailableProviders = map[string]string{
"evolink": "EvoLink 服务商未配置,请先配置 EVOLINK_API_KEY。",
}
_, err := b.Build(context.Background(), "owner", "client", "image.generate", "idem", map[string]any{
"engine": "evolink", "prompt": "hello",
})
if err == nil || err.Error() != "EvoLink 服务商未配置,请先配置 EVOLINK_API_KEY。" {
t.Fatalf("error = %v", err)
}
var unavailable *Error
if !errors.As(err, &unavailable) || unavailable.Kind != ErrorUnavailable || unavailable.Status != 503 {
t.Fatalf("error = %#v, want unavailable 503", err)
}
}
func TestProviderJobBuilderUsesPublicInputURLsForImageMaterialCoverage(t *testing.T) {
b := testProviderBuilder()
cmd, err := b.Build(context.Background(), "owner", "client", "image.generate", "", map[string]any{

View File

@@ -97,7 +97,7 @@ func LoadEnvironment(path string, environment map[string]string) (map[string]str
}
merged := cloneStrings(environment)
for key, value := range file {
if _, allowed := fieldIndex[key]; !allowed {
if !allowedEnvironmentKey(key) {
continue
}
if _, exists := merged[key]; !exists {
@@ -108,14 +108,25 @@ func LoadEnvironment(path string, environment map[string]string) (map[string]str
}
func RuntimeSettingKeys() []string {
keys := make([]string, 0, len(fieldIndex))
keys := make([]string, 0, len(fieldIndex)+len(runtimeOnlyKeys))
for key := range fieldIndex {
keys = append(keys, key)
}
for key := range runtimeOnlyKeys {
keys = append(keys, key)
}
sort.Strings(keys)
return keys
}
func allowedEnvironmentKey(key string) bool {
if _, allowed := fieldIndex[key]; allowed {
return true
}
_, allowed := runtimeOnlyKeys[key]
return allowed
}
func (s *Service) WithBillingAccountWriter(writer BillingAccountWriter) *Service {
s.billing = writer
return s
@@ -483,3 +494,9 @@ var fieldIndex = func() map[string]Field {
result["VIDEO_GENERATE_ENGINE"] = Field{Key: "VIDEO_GENERATE_ENGINE", Label: "视频生成", Type: "select", DefaultValue: "bailian", Options: []Option{{Label: "Seedance", Value: "seedance"}, {Label: "阿里云百炼 Wan 2.7", Value: "bailian"}}}
return result
}()
// runtimeOnlyKeys are read from the process/settings file by the Go startup
// loader but are intentionally not exposed as editable settings-panel fields.
var runtimeOnlyKeys = map[string]struct{}{
"ZHINIAN_ALLOW_UNCONFIGURED_PROVIDERS": {},
}

View File

@@ -100,6 +100,23 @@ func TestServiceGetUsesInjectedEnvironmentWithoutMutatingProcess(t *testing.T) {
assertSecretProjection(t, value, "SEEDANCE_API_KEY", true)
}
func TestLoadEnvironmentAllowsBootstrapRuntimeFlagButIgnoresUnknownKeys(t *testing.T) {
path := filepath.Join(t.TempDir(), ".env.local")
if err := os.WriteFile(path, []byte("ZHINIAN_ALLOW_UNCONFIGURED_PROVIDERS=true\nUNKNOWN_RUNTIME_FLAG=true\n"), 0o600); err != nil {
t.Fatal(err)
}
values, err := LoadEnvironment(path, nil)
if err != nil {
t.Fatal(err)
}
if values["ZHINIAN_ALLOW_UNCONFIGURED_PROVIDERS"] != "true" {
t.Fatalf("bootstrap flag = %q", values["ZHINIAN_ALLOW_UNCONFIGURED_PROVIDERS"])
}
if _, exists := values["UNKNOWN_RUNTIME_FLAG"]; exists {
t.Fatal("unknown runtime flag was loaded")
}
}
func TestServiceSynchronizesBillingAccountWithoutRequiringRestart(t *testing.T) {
writer := &billingWriterStub{}
service := New(filepath.Join(t.TempDir(), ".env.local"), nil, nil).WithBillingAccountWriter(writer)

View File

@@ -14,6 +14,9 @@ data:
ZHINIAN_AUTH_COOKIE_SECURE: "true"
ZHINIAN_PUBLIC_BASE_URL: https://REPLACE_WITH_PUBLIC_HOST
ZHINIAN_GO_EMBEDDED_WORKER: "true"
# Temporary bootstrap switch only. Keep false for steady-state production;
# when true, unconfigured provider requests return 503 until restart.
ZHINIAN_ALLOW_UNCONFIGURED_PROVIDERS: "false"
ZHINIAN_RUNTIME_DIR: /var/lib/zhinian/runtime
ZHINIAN_LOG_DIR: /var/lib/zhinian/logs
ZHINIAN_SETTINGS_FILE: /var/lib/zhinian/settings.env

View File

@@ -29,8 +29,10 @@ metadata:
namespace: zhinian
type: Opaque
stringData:
# The Go API refuses to start in PostgreSQL production mode when any of the
# four selectable real providers is missing its credentials.
# In steady-state production the Go API expects all four selectable real
# providers to be configured. For a temporary bootstrap, set
# ZHINIAN_ALLOW_UNCONFIGURED_PROVIDERS=true in the Go runtime ConfigMap;
# requests for an unconfigured provider will still return 503 until restart.
VOLCENGINE_ACCESS_KEY_ID: REPLACE_WITH_VOLCENGINE_ACCESS_KEY_ID
VOLCENGINE_SECRET_ACCESS_KEY: REPLACE_WITH_VOLCENGINE_SECRET_ACCESS_KEY
EVOLINK_API_KEY: REPLACE_WITH_EVOLINK_API_KEY

View File

@@ -126,8 +126,11 @@ Kubernetes Secret
生产配置的事实来源是 ACK Secret/ConfigMap。不要通过静态页面或修改 Pod 文件来更新
生产密钥;修改 Secret/ConfigMap 后滚动 Go Deployment。
Go API 在 PostgreSQL 生产模式启动时会校验即梦、EvoLink、百炼和 Seedance 的真实凭据;
任一服务商缺少凭据都会拒绝启动,不会自动切换到占位或模拟生成。
Go API 在 PostgreSQL 生产模式默认会校验即梦、EvoLink、百炼和 Seedance 的真实凭据;
任一服务商缺少凭据都会拒绝启动,不会自动切换到占位或模拟生成。若需要先上线配置页,
可在 Go API 的 ConfigMap 临时设置 `ZHINIAN_ALLOW_UNCONFIGURED_PROVIDERS=true`:服务可以
启动,但选中未配置服务商的报价和生成请求会返回 503。通过设置页填写凭据后必须滚动重启
Go Deployment确认健康后再将该开关恢复为 `false`
## 探针与验收
@@ -162,7 +165,8 @@ curl https://你的域名/api/v1/openapi.json
```bash
cp .env.example .env.local
# ZHINIAN_DATA_BACKEND 保持为 postgres并填写 DATABASE_URL、登录密钥和全部真实服务商凭据
# 正常发布:保持 ZHINIAN_DATA_BACKEND=postgres并填写 DATABASE_URL、登录密钥和真实服务商凭据
# 临时引导:可将 ZHINIAN_ALLOW_UNCONFIGURED_PROVIDERS=true先启动配置页保存凭据后重启并恢复为 false
./scripts/deploy.sh
```

View File

@@ -629,3 +629,9 @@
- `ProviderProcessor` now rejects a provider-reported success without an output URL before terminal finalization. The create page and result-asset task view also treat a succeeded job without a resolvable output asset as `结果同步中`, so neither frontend surface presents a false `已完成` state to the user.
- `docs/API.md` and deployment/README guidance now describe the Go API + embedded Worker topology. The old `scripts/worker.mjs` remains only as legacy source and is no longer part of the checked-in Compose or package-script path.
- Verification is limited by the host environment: `git diff --check` passes; Go, frontend dependency/typecheck/build, and Docker Compose execution still require a deployment/CI environment with those toolchains.
### Production provider bootstrap requirement — 2026-08-18
- PostgreSQL production startup currently calls `validateProductionProviderConfiguration` from `application.New` before the HTTP server is created, so missing any of the four provider credential groups terminates the process before it listens.
- The runtime settings endpoint persists provider secrets to `.env.local`/the configured settings file and reports `RestartRequired` for non-billing updates, but the provider registry and `ProviderJobBuilder` are created once during application composition; saved credentials therefore require a process restart.
- The safe bootstrap boundary is an explicit environment flag, `ZHINIAN_ALLOW_UNCONFIGURED_PROVIDERS`, that bypasses only the startup guard. The real provider adapters remain the only production adapters, and a selected provider with missing credentials must fail before quote/creation with a clear service-unavailable response.
- Compose injects `.env.local` through `env_file`, and ACK injects runtime values through the Go API ConfigMap/Secret, so the bootstrap flag can be supplied by deployment configuration without exposing it as a mutable settings-panel field.

View File

@@ -1593,3 +1593,22 @@
- Frontend TypeScript, all 54 Vitest files / 173 tests, Next production build, ACK manifest checks, contract JSON parsing, script syntax, and `git diff --check` passed.
- Docker Compose was not executed because Docker CLI/daemon is unavailable in this host; live PostgreSQL/provider credentials and rollout checks remain for deployment.
- **Status:** complete
## Session: 2026-08-18 - Production Provider Bootstrap
### Phase 79: Production Bootstrap Without Provider Credentials
- **Status:** in_progress
- User confirmed the temporary bootstrap flow: allow the production Go API to start without provider credentials, expose clear unavailable responses until credentials are saved, and require a restart for the saved credentials to take effect.
- Existing behavior is intentionally fail-closed in PostgreSQL mode at `application.New` before `ListenAndServe`; the change will be guarded by `ZHINIAN_ALLOW_UNCONFIGURED_PROVIDERS` and will not restore Mock providers.
- Provider adapters are constructed once at startup, so the selected provider's configuration will be checked by the job builder for both quote and generation creation; this prevents billing/queueing work that cannot execute.
- First focused Go test attempt was blocked before compilation because the default `proxy.golang.org` IPv6 connection timed out while downloading `pgx`, `x/crypto`, and `x/text`; retrying with the repository's configured `goproxy.cn` proxy.
- The `goproxy.cn` retry downloaded dependencies and compiled, but Go 1.21.13 test binaries aborted on macOS with `dyld: missing LC_UUID load command`; `internal/settings` passed. Retrying with `CGO_ENABLED=0` before changing toolchains.
- With `CGO_ENABLED=0`, the focused Go packages passed. One follow-up formatting command repeated the `backend/` prefix while already in the backend working directory; it emitted a path warning but did not affect tests. Final formatting is run from the project root.
### Phase 79 final verification
- Added `ZHINIAN_ALLOW_UNCONFIGURED_PROVIDERS`; it bypasses only PostgreSQL production startup credential validation and emits a warning. Mock providers remain disabled.
- Added hidden runtime-file loading for the bootstrap flag without exposing it as an editable settings-panel field. Added provider-specific availability checks to the job builder, so quote and generation creation return HTTP 503 before billing/queueing when the selected provider lacks credentials.
- Preserved the existing settings `RestartRequired` contract and documented the sequence: enable the flag, deploy, configure credentials, restart Go, verify health, then restore the flag to `false`.
- Verification passed: Go `test ./...`, `go vet ./...`, frontend Vitest 54 files/173 tests, TypeScript, Next production build, ACK manifest assertions, and `git diff --check`.
- The Go checks used an isolated official Go 1.21.13 toolchain with `CGO_ENABLED=0` because the host has no system Go and the default macOS test linker emitted `LC_UUID` errors; module downloads used the repository's `goproxy.cn` setting. Live PostgreSQL/provider connectivity and Docker rollout remain deployment-time checks.
- **Status:** complete

View File

@@ -23,7 +23,7 @@ if command -v node >/dev/null 2>&1; then
elif [ ! -f .env.local ]; then
cp .env.example .env.local
echo "[deploy] Created .env.local from .env.example"
echo "[deploy] Configure DATABASE_URL and all real provider credentials in .env.local before production use."
echo "[deploy] Configure DATABASE_URL and real provider credentials in .env.local before production use, or use the temporary ZHINIAN_ALLOW_UNCONFIGURED_PROVIDERS bootstrap flag."
fi
if ! grep -q '^ZHINIAN_INTERNAL_WORKER_TOKEN=' .env.local || grep -q '^ZHINIAN_INTERNAL_WORKER_TOKEN=$\|^ZHINIAN_INTERNAL_WORKER_TOKEN=change-me-worker-token$' .env.local; then

View File

@@ -15,7 +15,7 @@ if (!existsSync(envPath)) {
}
copyFileSync(examplePath, envPath);
console.log("[deploy] Created .env.local from .env.example");
console.log("[deploy] Configure DATABASE_URL and all real provider credentials in .env.local before production use.");
console.log("[deploy] Configure DATABASE_URL and real provider credentials in .env.local before production use, or use the temporary ZHINIAN_ALLOW_UNCONFIGURED_PROVIDERS bootstrap flag.");
}
let envText = readFileSync(envPath, "utf8");

View File

@@ -620,3 +620,17 @@ Phase 77 - Alibaba Cloud RDS PostgreSQL Adapter complete
- [x] Add focused regression coverage for provider configuration and quote availability
- [x] Verify Go tests, frontend tests/typecheck/build, Compose/deployment static checks, and diff hygiene
- **Status:** complete; Docker/real-provider rollout verification remains deployment-time
### Phase 79: Production Bootstrap Without Provider Credentials
- [x] Add an explicit temporary production bootstrap flag that bypasses only startup credential validation
- [x] Reject generation and quote requests clearly when the selected provider is still unconfigured
- [x] Preserve restart-required behavior after settings are saved and document the rollout sequence
- [x] Add focused regression coverage and run Go/frontend/documentation checks
- **Status:** complete; live database/provider rollout remains deployment-time
## Errors Encountered
| Error | Attempt | Resolution |
| --- | --- | --- |
| Go module downloads timed out through `proxy.golang.org` over IPv6 | Phase 79 focused test attempt 1 | Retry with the `goproxy.cn` proxy used by `backend/Dockerfile.alpine`; no source failure reached yet |
| Go 1.21.13 test binaries aborted with macOS `dyld: missing LC_UUID` | Phase 79 focused test attempt 2 | Retry with `CGO_ENABLED=0`; if needed use a newer official Go toolchain |
| Follow-up `gofmt` path repeated `backend/` from the backend directory | Phase 79 focused verification | Run final `gofmt` from the project root; tests were unaffected |