package settings import ( "bufio" "context" "errors" "fmt" "os" "path/filepath" "sort" "strings" "sync" ) type RuntimeUpdater func(context.Context, map[string]string) error type Option struct { Label string `json:"label"` Value string `json:"value"` } type Field struct { Key string `json:"key"` Label string `json:"label"` Description string `json:"description,omitempty"` Secret bool `json:"secret,omitempty"` Type string `json:"type,omitempty"` Options []Option `json:"options,omitempty"` Value string `json:"value"` Configured bool `json:"configured"` DefaultValue string `json:"-"` } type Group struct { ID string `json:"id"` Title string `json:"title"` Description string `json:"description"` Fields []Field `json:"fields"` } type Capability struct { ID string `json:"id"` Label string `json:"label"` ReqKey string `json:"reqKey"` Engine string `json:"engine,omitempty"` EngineLabel string `json:"engineLabel,omitempty"` Enabled bool `json:"enabled"` } type EngineAssignment struct { ID string `json:"id"` Label string `json:"label"` Engine string `json:"engine"` EngineLabel string `json:"engineLabel"` Connected bool `json:"connected"` ConnectionLabel string `json:"connectionLabel"` ReqKey string `json:"reqKey"` Configurable bool `json:"configurable"` Field *Field `json:"field,omitempty"` } type Services struct { Visual bool `json:"visual"` Evolink bool `json:"evolink"` Seedance bool `json:"seedance"` Bailian bool `json:"bailian"` Auth bool `json:"auth"` Organization bool `json:"organization"` } type Payload struct { Services Services `json:"services"` Capabilities []Capability `json:"capabilities"` EngineAssignments []EngineAssignment `json:"engineAssignments"` Groups []Group `json:"groups"` RestartRequired bool `json:"restartRequired,omitempty"` } type Service struct { mu sync.Mutex path string environment map[string]string update RuntimeUpdater billing BillingAccountWriter } type BillingAccount struct { AccountName, BankName, AccountNumber, Contact string } type BillingAccountWriter interface { SaveBillingAccount(context.Context, BillingAccount) error } func New(path string, environment map[string]string, updater RuntimeUpdater) *Service { return &Service{path: path, environment: cloneStrings(environment), update: updater} } // LoadEnvironment reads only the settings whitelist. Values already supplied // by the process win, matching Next's process.env-over-file precedence. func LoadEnvironment(path string, environment map[string]string) (map[string]string, error) { file, err := readEnv(path) if err != nil { return nil, err } merged := cloneStrings(environment) for key, value := range file { if _, allowed := fieldIndex[key]; !allowed { continue } if _, exists := merged[key]; !exists { merged[key] = value } } return merged, nil } func RuntimeSettingKeys() []string { keys := make([]string, 0, len(fieldIndex)) for key := range fieldIndex { keys = append(keys, key) } sort.Strings(keys) return keys } func (s *Service) WithBillingAccountWriter(writer BillingAccountWriter) *Service { s.billing = writer return s } func (s *Service) Get(ctx context.Context) (any, error) { if err := ctx.Err(); err != nil { return nil, err } s.mu.Lock() defer s.mu.Unlock() return s.get() } func (s *Service) Save(ctx context.Context, values map[string]any) (any, error) { if err := ctx.Err(); err != nil { return nil, err } s.mu.Lock() defer s.mu.Unlock() updates := map[string]string{} for key, raw := range values { field, ok := fieldIndex[key] next, stringValue := raw.(string) if !ok || !stringValue { continue } next = strings.TrimSpace(next) if field.Secret && next == "" { continue } updates[key] = next } if len(updates) > 0 { // Capture the complete persisted view before writing so a partial billing // account update cannot blank sibling fields that only exist in the file. persisted, err := readEnv(s.path) if err != nil { return nil, err } if err := s.write(updates); err != nil { return nil, err } if s.update != nil { if err := s.update(ctx, cloneStrings(updates)); err != nil { return nil, fmt.Errorf("apply runtime settings: %w", err) } } for key, value := range updates { s.environment[key] = value persisted[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"), }); err != nil { return nil, fmt.Errorf("apply billing account settings: %w", err) } } } payload, err := s.get() if err == nil && requiresRestart(updates) { payload.RestartRequired = true } 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 { case "ZHINIAN_BILLING_ACCOUNT_NAME", "ZHINIAN_BILLING_ACCOUNT_BANK", "ZHINIAN_BILLING_ACCOUNT_NUMBER", "ZHINIAN_BILLING_CONTACT": return true } } return false } func requiresRestart(updates map[string]string) bool { for key := range updates { switch key { case "ZHINIAN_BILLING_ACCOUNT_NAME", "ZHINIAN_BILLING_ACCOUNT_BANK", "ZHINIAN_BILLING_ACCOUNT_NUMBER", "ZHINIAN_BILLING_CONTACT": continue default: return true } } return false } func (s *Service) get() (Payload, error) { file, err := readEnv(s.path) if err != nil { return Payload{}, err } current := func(field Field) string { if value, ok := s.environment[field.Key]; ok { return value } if value, ok := file[field.Key]; ok { return value } return field.DefaultValue } groups := definitions() for gi := range groups { for fi := range groups[gi].Fields { field := &groups[gi].Fields[fi] raw := current(*field) field.Configured = raw != "" if !field.Secret { field.Value = raw } } } image := normalizeImage(current(fieldIndex["IMAGE_GENERATE_ENGINE"])) video := normalizeVideo(current(fieldIndex["VIDEO_GENERATE_ENGINE"])) imageModel := map[string]string{"jimeng": lookup(s.environment, file, "JIMENG_IMAGE_GENERATE_46_REQ_KEY", "jimeng_seedream46_cvtob"), "evolink": lookup(s.environment, file, "EVOLINK_IMAGE_MODEL", "gpt-image-2"), "bailian": lookup(s.environment, file, "BAILIAN_IMAGE_MODEL", "wan2.7-image-pro")}[image] videoModel := lookup(s.environment, file, "SEEDANCE_MODEL", "doubao-seedance-2-0-260128") if video == "bailian" { videoModel = lookup(s.environment, file, "BAILIAN_VIDEO_MODEL", "wan2.7-i2v-2026-04-25") } imageConnected := connected(image, s.environment, file) videoConnected := connected(video, s.environment, file) imageField := project(fieldIndex["IMAGE_GENERATE_ENGINE"], image, s.environment, file) videoField := project(fieldIndex["VIDEO_GENERATE_ENGINE"], video, 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.environment, file), Evolink: connected("evolink", s.environment, file), Seedance: connected("seedance", s.environment, file), Bailian: connected("bailian", s.environment, file), Auth: lookup(s.environment, file, "ZHINIAN_AUTH_SESSION_SECRET", "") != "", Organization: lookup(s.environment, file, "DATABASE_URL", "") != "" || lookup(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) write(updates map[string]string) error { data, err := os.ReadFile(s.path) if err != nil && !errors.Is(err, os.ErrNotExist) { return fmt.Errorf("read settings file: %w", err) } if errors.Is(err, os.ErrNotExist) { data = []byte("# 智念AIGC平台 API 配置\n") } lines := strings.Split(strings.ReplaceAll(string(data), "\r\n", "\n"), "\n") seen := map[string]bool{} for index, line := range lines { key, _, ok := splitLine(line) if !ok { continue } if value, replace := updates[key]; replace { lines[index] = key + "=" + formatValue(value) seen[key] = true } } missing := []string{} for key := range updates { if !seen[key] { missing = append(missing, key) } } sort.Strings(missing) if len(missing) > 0 { for len(lines) > 0 && lines[len(lines)-1] == "" { lines = lines[:len(lines)-1] } if len(lines) > 0 { lines = append(lines, "") } lines = append(lines, "# Managed by 智念AIGC平台 设置页") for _, key := range missing { lines = append(lines, key+"="+formatValue(updates[key])) } } if err := os.MkdirAll(filepath.Dir(s.path), 0o755); err != nil { return err } temp, err := os.CreateTemp(filepath.Dir(s.path), ".settings-*.tmp") if err != nil { return err } name := temp.Name() defer os.Remove(name) if err = temp.Chmod(0o600); err == nil { _, err = temp.WriteString(strings.TrimRight(strings.Join(lines, "\n"), "\n") + "\n") } if err == nil { err = temp.Sync() } closeErr := temp.Close() if err == nil { err = closeErr } if err != nil { return err } return os.Rename(name, s.path) } func readEnv(path string) (map[string]string, error) { file, err := os.Open(path) if errors.Is(err, os.ErrNotExist) { return map[string]string{}, nil } if err != nil { return nil, err } defer file.Close() result := map[string]string{} scanner := bufio.NewScanner(file) for scanner.Scan() { key, raw, ok := splitLine(scanner.Text()) if ok { result[key] = parseValue(raw) } } return result, scanner.Err() } func splitLine(line string) (string, string, bool) { index := strings.IndexByte(line, '=') if index < 1 { return "", "", false } key := strings.TrimSpace(line[:index]) if !validKey(key) { return "", "", false } return key, strings.TrimSpace(line[index+1:]), true } func validKey(key string) bool { for index, r := range key { if !(r == '_' || r >= 'A' && r <= 'Z' || r >= 'a' && r <= 'z' || index > 0 && r >= '0' && r <= '9') { return false } } return key != "" } func parseValue(raw string) string { raw = strings.TrimSpace(raw) if len(raw) >= 2 && ((raw[0] == '"' && raw[len(raw)-1] == '"') || (raw[0] == '\'' && raw[len(raw)-1] == '\'')) { if raw[0] == '\'' { return raw[1 : len(raw)-1] } var out strings.Builder escaped := false for _, r := range raw[1 : len(raw)-1] { if escaped { switch r { case 'n': out.WriteByte('\n') case '"', '\\': out.WriteRune(r) default: out.WriteByte('\\') out.WriteRune(r) } escaped = false } else if r == '\\' { escaped = true } else { out.WriteRune(r) } } return out.String() } if i := strings.Index(raw, " #"); i >= 0 { raw = raw[:i] } return strings.TrimSpace(raw) } func formatValue(value string) string { if value == "" { return "" } if !strings.ContainsAny(value, " \t\r\n#\"'\\") { return value } return "\"" + strings.NewReplacer("\\", "\\\\", "\"", "\\\"", "\n", "\\n", "\r", "\\r").Replace(value) + "\"" } func cloneStrings(input map[string]string) map[string]string { out := map[string]string{} for key, value := range input { out[key] = value } return out } func lookup(environment, file map[string]string, key, fallback string) string { if value, ok := environment[key]; ok { return value } if value, ok := file[key]; ok { return value } return fallback } func project(field Field, value string, environment, file map[string]string) Field { field.Value = value field.Configured = lookup(environment, file, field.Key, "") != "" return field } func normalizeImage(value string) string { value = strings.ToLower(strings.TrimSpace(value)) if value == "evolink" || value == "bailian" { return value } return "jimeng" } func normalizeVideo(value string) string { if strings.ToLower(strings.TrimSpace(value)) == "bailian" { return "bailian" } return "seedance" } func label(engine string) string { switch engine { case "evolink": return "EvoLink" case "bailian": return "阿里云百炼" case "seedance": return "Seedance" default: return "即梦" } } func connection(ok bool) string { if ok { return "已连接" } return "待配置" } func connected(engine string, environment, file map[string]string) bool { switch engine { case "evolink": return mock(lookup(environment, file, "EVOLINK_MOCK", "auto"), lookup(environment, file, "EVOLINK_API_KEY", "") != "") case "seedance": return mock(lookup(environment, file, "SEEDANCE_MOCK", "auto"), lookup(environment, file, "SEEDANCE_API_KEY", "") != "") case "bailian": flag := strings.ToLower(strings.TrimSpace(lookup(environment, file, "BAILIAN_MOCK", "auto"))) return flag != "1" && flag != "true" && lookup(environment, file, "BAILIAN_API_KEY", "") != "" default: return mock(lookup(environment, file, "JIMENG_VISUAL_MOCK", "auto"), lookup(environment, file, "VOLCENGINE_ACCESS_KEY_ID", "") != "" && lookup(environment, file, "VOLCENGINE_SECRET_ACCESS_KEY", "") != "") } } func mock(flag string, configured bool) bool { flag = strings.ToLower(strings.TrimSpace(flag)) if flag == "1" || flag == "true" { return false } if flag == "0" || flag == "false" { return true } return configured } func definitions() []Group { return []Group{ {ID: "auth", Title: "平台账号安全", Description: "平台自建手机号账号登录。", Fields: []Field{{Key: "ZHINIAN_AUTH_REQUIRED", Label: "登录保护", Type: "select", DefaultValue: "auto", Options: []Option{{Label: "自动", Value: "auto"}, {Label: "启用", Value: "1"}, {Label: "停用", Value: "0"}}}, {Key: "ZHINIAN_AUTH_SESSION_SECRET", Label: "会话签名密钥", Secret: true, Type: "password"}}}, {ID: "billing", Title: "企业计费", Description: "企业计费和对公账户信息。", Fields: []Field{{Key: "ZHINIAN_BILLING_REQUIRED", Label: "真实任务计费", Type: "select", DefaultValue: "1", Options: []Option{{Label: "启用", Value: "1"}, {Label: "停用(免计费)", Value: "0"}}}, {Key: "ZHINIAN_BILLING_ACCOUNT_NAME", Label: "对公账户名称"}, {Key: "ZHINIAN_BILLING_ACCOUNT_BANK", Label: "开户行"}, {Key: "ZHINIAN_BILLING_ACCOUNT_NUMBER", Label: "银行账号"}, {Key: "ZHINIAN_BILLING_CONTACT", Label: "充值对接信息"}}}, {ID: "visual", Title: "即梦图片 API", Description: "火山 AK/SK。", Fields: []Field{{Key: "VOLCENGINE_ACCESS_KEY_ID", Label: "Access Key ID", Secret: true, Type: "password"}, {Key: "VOLCENGINE_SECRET_ACCESS_KEY", Label: "Secret Access Key", Secret: true, Type: "password"}}}, {ID: "evolink", Title: "EvoLink 图片 API", Description: "GPT Image 2 图片生成。", Fields: []Field{{Key: "EVOLINK_API_KEY", Label: "EvoLink API Key", Secret: true, Type: "password"}, {Key: "EVOLINK_BASE_URL", Label: "Base URL", DefaultValue: "https://api.evolink.ai"}, {Key: "EVOLINK_IMAGE_MODEL", Label: "图片模型", DefaultValue: "gpt-image-2"}, {Key: "EVOLINK_IMAGE_QUALITY", Label: "质量", DefaultValue: "medium"}}}, {ID: "seedance", Title: "Seedance 视频 API", Description: "火山方舟 API Key。", Fields: []Field{{Key: "SEEDANCE_API_KEY", Label: "方舟 API Key", Secret: true, Type: "password"}}}, {ID: "bailian", Title: "阿里云百炼 API", Description: "万相图片与视频。", Fields: []Field{{Key: "BAILIAN_API_KEY", Label: "百炼 API Key", Secret: true, Type: "password"}, {Key: "BAILIAN_BASE_URL", Label: "Base URL", DefaultValue: "https://llm-126wneubbdo6dbr5.cn-beijing.maas.aliyuncs.com/compatible-mode/v1"}, {Key: "BAILIAN_IMAGE_MODEL", Label: "图片模型", DefaultValue: "wan2.7-image-pro"}, {Key: "BAILIAN_VIDEO_MODEL", Label: "视频模型", DefaultValue: "wan2.7-i2v-2026-04-25"}}}, {ID: "oss", Title: "OSS 资产存储", Description: "共享资产存储。", Fields: []Field{{Key: "ALI_OSS_ENDPOINT", Label: "Endpoint"}, {Key: "ALI_OSS_BUCKET", Label: "Bucket"}, {Key: "ALI_OSS_ACCESS_KEY_ID", Label: "Access Key ID", Secret: true, Type: "password"}, {Key: "ALI_OSS_ACCESS_KEY_SECRET", Label: "Access Key Secret", Secret: true, Type: "password"}, {Key: "ALI_OSS_PUBLIC_BASE_URL", Label: "公开访问 Base URL"}}}, } } var fieldIndex = func() map[string]Field { result := map[string]Field{} for _, group := range definitions() { for _, field := range group.Fields { result[field.Key] = field } } result["IMAGE_GENERATE_ENGINE"] = Field{Key: "IMAGE_GENERATE_ENGINE", Label: "图片生成", Type: "select", DefaultValue: "jimeng", Options: []Option{{Label: "即梦 / 火山视觉", Value: "jimeng"}, {Label: "EvoLink GPT Image 2", Value: "evolink"}, {Label: "阿里云百炼 Wan 2.7", Value: "bailian"}}} 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 }()