package settings import ( "context" "strings" "apps/backend/internal/module/ai" "apps/backend/internal/module/member/domain" "apps/backend/internal/svc" "apps/backend/internal/types" ) func resolveAPIKey(svcCtx *svc.ServiceContext, st *domain.UserSettings, provider string) (key string, platformAvailable bool) { provider = ai.NormalizeProvider(provider) // 1) user BYOK for this provider if st != nil { if k := st.KeyForProvider(provider); k != "" { return k, platformKeyAvailable(svcCtx, provider) } } // 2) platform key return platformKey(svcCtx, provider), platformKeyAvailable(svcCtx, provider) } func platformKey(svcCtx *svc.ServiceContext, provider string) string { if svcCtx == nil { return "" } switch ai.NormalizeProvider(provider) { case ai.ProviderOpenCodeGo: return strings.TrimSpace(svcCtx.Config.Platform.OpenCodeKey) default: k := strings.TrimSpace(svcCtx.Config.Platform.XAIKey) if k == "" { k = strings.TrimSpace(svcCtx.Config.Platform.AIKey) } return k } } func platformKeyAvailable(svcCtx *svc.ServiceContext, provider string) bool { return platformKey(svcCtx, provider) != "" } // loadModelsForProvider — 真拉 + 5 分鐘快取;失敗用 fallback(不回空 list) func loadModelsForProvider(ctx context.Context, svcCtx *svc.ServiceContext, st *domain.UserSettings, provider string) (list []string, fromCache bool, modelsErr string) { provider = ai.NormalizeProvider(provider) key, _ := resolveAPIKey(svcCtx, st, provider) if key == "" { return ai.FallbackModels(provider), false, "尚未設定 API key(會員 BYOK 或平台 key)" } if svcCtx.AIRegistry == nil { return ai.FallbackModels(provider), false, "AI registry not configured" } list, fromCache, err := svcCtx.AIRegistry.ListModelsCached(ctx, provider, key) if err != nil { return ai.FallbackModels(provider), false, err.Error() } return list, fromCache, "" } // pickModel:優先用會員已存模型;即使暫時不在列表也保留(不寫死改成別台) func pickModel(preferred string, list []string) string { preferred = strings.TrimSpace(preferred) if preferred != "" { return preferred } if len(list) > 0 { return list[0] } return "" } func buildAiSettingsData(ctx context.Context, svcCtx *svc.ServiceContext, st *domain.UserSettings, modelsProvider string) *types.AiSettingsData { if st == nil { st = domain.DefaultSettings(0) } savedProvider := ai.NormalizeProvider(st.Provider) if savedProvider == "" { savedProvider = ai.ProviderXAI } modelsProvider = ai.NormalizeProvider(modelsProvider) if modelsProvider == "" { modelsProvider = savedProvider } list, fromCache, modelsErr := loadModelsForProvider(ctx, svcCtx, st, modelsProvider) // 顯示/回傳都以會員存檔為準 selected := pickModel(st.Model, list) savedModel := strings.TrimSpace(st.Model) if modelsProvider == savedProvider && selected != "" { savedModel = selected } keyConfigured := st.HasKeyForProvider(savedProvider) platformAvail := platformKeyAvailable(svcCtx, savedProvider) return &types.AiSettingsData{ Provider: savedProvider, Model: savedModel, ResearchProvider: savedProvider, ResearchModel: savedModel, ApiKeyConfigured: keyConfigured, ResearchApiKeyConfigured: keyConfigured || st.ResearchAPIKeyConfigured, PlatformKeyAvailable: platformAvail, Models: list, ModelsProvider: modelsProvider, SelectedModel: selected, ModelsFromCache: fromCache, ModelsError: modelsErr, } }