1 line
41 KiB
JSON
1 line
41 KiB
JSON
{"repo": "cedar-panda/holt", "files": {"lib/services/settings/api_settings.dart": {"22611d84aa90538fc1b98c316bf418a572e7c508": {"t": 1785745912.8925943, "n": 19857, "h": "ed52e1f172c59791", "c": "import 'package:shared_preferences/shared_preferences.dart';\nimport '../secure_store.dart';\nimport '../api_adapter.dart';\nimport '../openrouter_service.dart';\nimport '../aws_bedrock_service.dart';\nimport '../gemini_service.dart';\nimport '../deepseek_service.dart';\nimport '../qwen_service.dart';\nimport '../openai_compatible_service.dart';\nimport '../local_model_service.dart';\n\n/// API 供應商配置(OpenRouter / Bedrock / Gemini / DeepSeek / Qwen / OpenAI Compatible / Local API)\nclass ApiSettings {\n // ═══ 通用 ═══\n static const String _keyLegacyApiKey = 'api_key';\n static const String _keyOpenRouterApiKey = 'openrouter_api_key';\n static const String _keyModel = 'model';\n static const String _keySystemPrompt = 'system_prompt';\n static const String _keyApiProvider = 'api_provider';\n static const String _keyImageGenModel = 'image_gen_model';\n static String? _cachedLocalModelKey;\n static LocalModelService? _cachedLocalModelService;\n static Future<LocalModelService>? _cachedLocalModelFuture;\n static String? _cachedGeminiKey;\n static String? _cachedGeminiModel;\n static GeminiService? _cachedGeminiService;\n static const String localApiModelPrefix = 'local_api:';\n\n static Future<String> getApiProviderName() async {\n final p = await SharedPreferences.getInstance();\n return p.getString(_keyApiProvider) ?? 'openrouter';\n }\n\n /// 畫圖模型(OpenRouter 圖像輸出模型)\n static Future<void> saveImageGenModel(String model) async {\n final p = await SharedPreferences.getInstance();\n await p.setString(_keyImageGenModel, model);\n }\n\n static Future<String> getImageGenModel() async {\n final p = await SharedPreferences.getInstance();\n final v = p.getString(_keyImageGenModel) ?? '';\n return v.isEmpty ? 'openai/gpt-5.4-image-2' : v;\n }\n\n static Future<void> saveApiKey(String key) async {\n await saveOpenRouterApiKey(key);\n }\n\n static Future<String> getApiKey() async {\n return getApiKeyForProvider(await getApiProvider());\n }\n\n static Future<void> saveOpenRouterApiKey(String key) async {\n await SecureStore.write(_keyOpenRouterApiKey, key);\n await SecureStore.delete(_keyLegacyApiKey);\n }\n\n static Future<String> getOpenRouterApiKey() async {\n final key = await SecureStore.read(_keyOpenRouterApiKey);\n if (key.isNotEmpty) return key;\n\n final legacy = await SecureStore.read(_keyLegacyApiKey);\n if (legacy.isNotEmpty) {\n await saveOpenRouterApiKey(legacy);\n }\n return legacy;\n }\n\n static Future<void> saveApiKeyForProvider(String provider, String key) async {\n switch (provider) {\n case 'openrouter':\n await saveOpenRouterApiKey(key);\n break;\n case 'gemini':\n await saveGeminiApiKey(key);\n break;\n case 'deepseek':\n await saveDeepseekApiKey(key);\n break;\n case 'qwen':\n await saveQwenApiKey(key);\n break;\n case 'openai_compatible':\n await saveOaiCompatApiKey(key);\n break;\n case 'local_api':\n await saveLocalApiKey(key);\n break;\n }\n }\n\n static Future<String> getApiKeyForProvider(String provider) async {\n switch (provider) {\n case 'openrouter':\n return getOpenRouterApiKey();\n case 'gemini':\n return getGeminiApiKey();\n case 'deepseek':\n return getDeepseekApiKey();\n case 'qwen':\n return getQwenApiKey();\n case 'openai_compatible':\n return getOaiCompatApiKey();\n case 'local_api':\n return getLocalApiKey();\n default:\n return '';\n }\n }\n\n static Future<void> saveModel(String model) async {\n final p = await SharedPreferences.getInstance();\n await p.setString(_keyModel, model);\n if (isLocalApiModelId(model)) {\n await p.setString(_keyLocalApiModel, extractLocalApiModel(model));\n await p.setString(_keyApiProvider, 'local_api');\n } else if ((p.getString(_keyApiProvider) ?? 'openrouter') == 'local_api') {\n await p.setString(_keyApiProvider, 'openrouter');\n }\n }\n\n static Future<String> getModel() async {\n final p = await SharedPreferences.getInstance();\n final provider = p.getString(_keyApiProvider) ?? 'openrouter';\n if (provider == 'local_api') {\n final model = p.getString(_keyLocalApiModel) ?? '';\n if (model.isNotEmpty) return toLocalApiModelId(model);\n final savedModel = p.getString(_keyModel) ?? '';\n return isLocalApiModelId(savedModel) ? savedModel : '';\n }\n if (provider == 'openai_compatible') {\n return p.getString(_keyOaiCompatModel) ?? '';\n }\n if (provider == 'gemini') {\n return p.getString(_keyGeminiModel) ?? 'gemini-2.0-flash';\n }\n if (provider == 'bedrock') {\n return p.getString(_keyAwsModelId) ??\n 'anthropic.claude-sonnet-4-20250514-v1:0';\n }\n if (provider == 'deepseek') {\n return p.getString(_keyDeepseekModel) ?? 'deepseek-chat';\n }\n if (provider == 'qwen') {\n return p.getString(_keyQwenModel) ?? 'qwen-plus';\n }\n return p.getString(_keyModel) ?? 'anthropic/claude-sonnet-4-20250514';\n }\n\n static Future<String> getModelForProvider(String provider) {\n return _modelForProvider(provider);\n }\n\n static Future<void> saveModelForProvider(\n String provider,\n String model,\n ) async {\n final value = model.trim();\n if (value.isEmpty) return;\n\n switch (provider) {\n case 'bedrock':\n await saveAwsModelId(value);\n break;\n case 'gemini':\n await saveGeminiModel(value);\n break;\n case 'deepseek':\n await saveDeepseekModel(value);\n break;\n case 'qwen':\n await saveQwenModel(value);\n break;\n case 'openai_compatible':\n await saveOaiCompatModel(value);\n break;\n case 'local_api':\n await saveLocalApiModel(value);\n break;\n default:\n await saveModel(value);\n break;\n }\n }\n\n static Future<void> saveSystemPrompt(String prompt) async {\n final p = await SharedPreferences.getInstance();\n await p.setString(_keySystemPrompt, prompt);\n }\n\n static Future<String> getSystemPrompt() async {\n final p = await SharedPreferences.getInstance();\n return p.getString(_keySystemPrompt) ?? '';\n }\n\n static Future<void> saveApiProvider(String provider) async {\n final p = await SharedPreferences.getInstance();\n await p.setString(_keyApiProvider, provider);\n }\n\n static Future<String> getApiProvider() async {\n final p = await SharedPreferences.getInstance();\n return p.getString(_keyApiProvider) ?? 'openrouter';\n }\n\n // ═══ AWS Bedrock ═══\n static const String _keyAwsAccessKeyId = 'aws_access_key_id';\n static const String _keyAwsSecretAccessKey = 'aws_secret_access_key';\n static const String _keyAwsRegion = 'aws_region';\n static const String _keyAwsModelId = 'aws_model_id';\n\n static Future<void> saveAwsAccessKeyId(String id) async {\n await SecureStore.write(_keyAwsAccessKeyId, id);\n }\n\n static Future<String> getAwsAccessKeyId() async {\n return SecureStore.read(_keyAwsAccessKeyId);\n }\n\n static Future<void> saveAwsSecretAccessKey(String key) async {\n await SecureStore.write(_keyAwsSecretAccessKey, key);\n }\n\n static Future<String> getAwsSecretAccessKey() async {\n return SecureStore.read(_keyAwsSecretAccessKey);\n }\n\n static Future<void> saveAwsRegion(String region) async {\n final p = await SharedPreferences.getInstance();\n await p.setString(_keyAwsRegion, region);\n }\n\n static Future<String> getAwsRegion() async {\n final p = await SharedPreferences.getInstance();\n return p.getString(_keyAwsRegion) ?? 'us-east-1';\n }\n\n static Future<void> saveAwsModelId(String modelId) async {\n final p = await SharedPreferences.getInstance();\n await p.setString(_keyAwsModelId, modelId);\n }\n\n static Future<String> getAwsModelId() async {\n final p = await SharedPreferences.getInstance();\n return p.getString(_keyAwsModelId) ??\n 'anthropic.claude-sonnet-4-20250514-v1:0';\n }\n\n // ═══ Gemini ═══\n static const String _keyGeminiApiKey = 'gemini_api_key';\n static const String _keyGeminiModel = 'gemini_model';\n\n static Future<void> saveGeminiApiKey(String v) async {\n await SecureStore.write(_keyGeminiApiKey, v);\n }\n\n static Future<String> getGeminiApiKey() async {\n return SecureStore.read(_keyGeminiApiKey);\n }\n\n static Future<void> saveGeminiModel(String v) async {\n final p = await SharedPreferences.getInstance();\n await p.setString(_keyGeminiModel, v);\n }\n\n static Future<String> getGeminiModel() async {\n final p = await SharedPreferences.getInstance();\n return p.getString(_keyGeminiModel) ?? 'gemini-2.0-flash';\n }\n\n // ═══ DeepSeek ═══\n static const String _keyDeepseekApiKey = 'deepseek_api_key';\n static const String _keyDeepseekModel = 'deepseek_model';\n\n static Future<void> saveDeepseekApiKey(String v) async {\n await SecureStore.write(_keyDeepseekApiKey, v);\n }\n\n static Future<String> getDeepseekApiKey() async {\n return SecureStore.read(_keyDeepseekApiKey);\n }\n\n static Future<void> saveDeepseekModel(String v) async {\n final p = await SharedPreferences.getInstance();\n await p.setString(_keyDeepseekModel, v);\n }\n\n static Future<String> getDeepseekModel() async {\n final p = await SharedPreferences.getInstance();\n return p.getString(_keyDeepseekModel) ?? 'deepseek-chat';\n }\n\n // ═══ Qwen ═══\n static const String _keyQwenApiKey = 'qwen_api_key';\n static const String _keyQwenModel = 'qwen_model';\n\n static Future<void> saveQwenApiKey(String v) async {\n await SecureStore.write(_keyQwenApiKey, v);\n }\n\n static Future<String> getQwenApiKey() async {\n return SecureStore.read(_keyQwenApiKey);\n }\n\n static Future<void> saveQwenModel(String v) async {\n final p = await SharedPreferences.getInstance();\n await p.setString(_keyQwenModel, v);\n }\n\n static Future<String> getQwenModel() async {\n final p = await SharedPreferences.getInstance();\n return p.getString(_keyQwenModel) ?? 'qwen-plus';\n }\n\n // ═══ OpenAI Compatible(中轉站)═══\n static const String _keyOaiCompatBaseUrl = 'oai_compat_base_url';\n static const String _keyOaiCompatApiKey = 'oai_compat_api_key';\n static const String _keyOaiCompatModel = 'oai_compat_model';\n\n static Future<void> saveOaiCompatBaseUrl(String v) async {\n final p = await SharedPreferences.getInstance();\n await p.setString(_keyOaiCompatBaseUrl, v);\n }\n\n static Future<String> getOaiCompatBaseUrl() async {\n final p = await SharedPreferences.getInstance();\n return p.getString(_keyOaiCompatBaseUrl) ?? '';\n }\n\n static Future<void> saveOaiCompatApiKey(String v) async {\n await SecureStore.write(_keyOaiCompatApiKey, v);\n }\n\n static Future<String> getOaiCompatApiKey() async {\n return SecureStore.read(_keyOaiCompatApiKey);\n }\n\n static Future<void> saveOaiCompatModel(String v) async {\n final p = await SharedPreferences.getInstance();\n await p.setString(_keyOaiCompatModel, v);\n }\n\n static Future<String> getOaiCompatModel() async {\n final p = await SharedPreferences.getInstance();\n return p.getString(_keyOaiCompatModel) ?? '';\n }\n\n // ═══ Local API(電腦 / 局域網 OpenAI-compatible 端點)═══\n static const String _keyLocalApiBaseUrl = 'local_api_base_url';\n static const String _keyLocalApiKey = 'local_api_key';\n static const String _keyLocalApiModel = 'local_api_model';\n\n static bool isLocalApiModelId(String modelId) =>\n modelId.startsWith(localApiModelPrefix);\n\n static String toLocalApiModelId(String model) {\n final trimmed = model.trim();\n if (trimmed.startsWith(localApiModelPrefix)) return trimmed;\n return '$localApiModelPrefix$trimmed';\n }\n\n static String extractLocalApiModel(String modelId) =>\n modelId.startsWith(localApiModelPrefix)\n ? modelId.substring(localApiModelPrefix.length)\n : modelId;\n\n static Future<void> saveLocalApiBaseUrl(String v) async {\n final p = await SharedPreferences.getInstance();\n await p.setString(_keyLocalApiBaseUrl, v);\n }\n\n static Future<String> getLocalApiBaseUrl() async {\n final p = await SharedPreferences.getInstance();\n return p.getString(_keyLocalApiBaseUrl) ?? '';\n }\n\n static Future<void> saveLocalApiKey(String v) async {\n await SecureStore.write(_keyLocalApiKey, v);\n }\n\n static Future<String> getLocalApiKey() async {\n return SecureStore.read(_keyLocalApiKey);\n }\n\n static Future<void> saveLocalApiModel(String v) async {\n final p = await SharedPreferences.getInstance();\n await p.setString(_keyLocalApiModel, extractLocalApiModel(v.trim()));\n }\n\n static Future<String> getLocalApiModel() async {\n final p = await SharedPreferences.getInstance();\n return p.getString(_keyLocalApiModel) ?? '';\n }\n\n // ═══ 星標模型 ═══\n static const String _keyStarredModels = 'starred_models';\n\n static Future<void> saveStarredModels(List<String> models) async {\n final p = await SharedPreferences.getInstance();\n await p.setStringList(_keyStarredModels, models);\n }\n\n static Future<List<String>> getStarredModels() async {\n final p = await SharedPreferences.getInstance();\n return p.getStringList(_keyStarredModels) ?? [];\n }\n\n static Future<void> toggleStarModel(String modelId) async {\n final list = await getStarredModels();\n if (list.contains(modelId)) {\n list.remove(modelId);\n } else {\n list.add(modelId);\n }\n await saveStarredModels(list);\n }\n\n // ═══ 工廠方法 ═══\n\n static Future<ApiAdapter> buildAdapter({\n String? overrideModel,\n String? overrideProvider,\n }) async {\n final provider = overrideProvider ?? await getApiProvider();\n // 如果指定了本地模型,直接返回 LocalModelService\n final modelId = overrideModel ?? await _modelForProvider(provider);\n if (LocalModelService.isLocalModelId(modelId)) {\n final key = LocalModelService.extractKey(modelId);\n final cached = _cachedLocalModelService;\n if (_cachedLocalModelKey == key && cached != null) return cached;\n\n final pending = _cachedLocalModelFuture;\n if (_cachedLocalModelKey == key && pending != null) return pending;\n\n await _disposeCachedLocalModel();\n _cachedLocalModelKey = key;\n _cachedLocalModelFuture = _loadLocalModelService(key);\n try {\n final service = await _cachedLocalModelFuture!;\n _cachedLocalModelService = service;\n return service;\n } finally {\n _cachedLocalModelFuture = null;\n }\n }\n\n if (isLocalApiModelId(modelId)) {\n await _disposeCachedLocalModel();\n final baseUrl = await getLocalApiBaseUrl();\n final key = await getLocalApiKey();\n if (baseUrl.isEmpty) throw Exception('本地 API Base URL 未設定');\n return OpenAICompatibleService(\n apiKey: key,\n baseUrl: baseUrl,\n modelPrefixToStrip: localApiModelPrefix,\n );\n }\n\n await _disposeCachedLocalModel();\n\n if (provider == 'local_api') {\n final baseUrl = await getLocalApiBaseUrl();\n final key = await getLocalApiKey();\n if (baseUrl.isEmpty) throw Exception('本地 API Base URL 未設定');\n return OpenAICompatibleService(\n apiKey: key,\n baseUrl: baseUrl,\n modelPrefixToStrip: localApiModelPrefix,\n );\n }\n\n if (provider == 'bedrock') {\n final accessKeyId = await getAwsAccessKeyId();\n final secretAccessKey = await getAwsSecretAccessKey();\n final region = await getAwsRegion();\n final bedrockModelId = overrideModel ?? await getAwsModelId();\n if (accessKeyId.isEmpty || secretAccessKey.isEmpty) {\n throw Exception('AWS 憑證未設定');\n }\n return BedrockService(\n accessKeyId: accessKeyId,\n secretAccessKey: secretAccessKey,\n region: region,\n modelId: bedrockModelId,\n );\n }\n\n if (provider == 'gemini') {\n final key = await getGeminiApiKey();\n if (key.isEmpty) throw Exception('Gemini API Key 未設定');\n final model = overrideModel ?? await getGeminiModel();\n final cached = _cachedGeminiService;\n if (_cachedGeminiKey == key &&\n _cachedGeminiModel == model &&\n cached != null) {\n return cached;\n }\n _cachedGeminiKey = key;\n _cachedGeminiModel = model;\n _cachedGeminiService = GeminiService(apiKey: key, modelId: model);\n return _cachedGeminiService!;\n }\n\n if (provider == 'deepseek') {\n final key = await getDeepseekApiKey();\n if (key.isEmpty) throw Exception('DeepSeek API Key 未設定');\n return DeepSeekService(apiKey: key);\n }\n\n if (provider == 'qwen') {\n final key = await getQwenApiKey();\n if (key.isEmpty) throw Exception('Qwen API Key 未設定');\n return QwenService(apiKey: key);\n }\n\n if (provider == 'openai_compatible') {\n final baseUrl = await getOaiCompatBaseUrl();\n final key = await getOaiCompatApiKey();\n if (baseUrl.isEmpty) throw Exception('API Base URL 未設定');\n if (key.isEmpty) throw Exception('API Key 未設定');\n return OpenAICompatibleService(apiKey: key, baseUrl: baseUrl);\n }\n\n // 默認 OpenRouter\n final apiKey = await getOpenRouterApiKey();\n if (apiKey.isEmpty) {\n throw Exception('OpenRouter API Key 未設定');\n }\n return OpenRouterService(apiKey: apiKey);\n }\n\n // ═══ 思考鏈 ═══\n static const String _keyThinkingChain = 'thinking_chain_enabled';\n\n static Future<void> saveThinkingChain(bool v) async {\n final p = await SharedPreferences.getInstance();\n await p.setBool(_keyThinkingChain, v);\n }\n\n static Future<bool> getThinkingChain() async {\n final p = await SharedPreferences.getInstance();\n return p.getBool(_keyThinkingChain) ?? false;\n }\n\n // ═══ 節省 Token ═══\n static const String _keyConciseMode = 'token_save_concise';\n static const String _keyFreeformMode = 'token_save_freeform';\n static const String _keyFreeformMaxLines = 'token_save_max_lines';\n\n static Future<void> saveConciseMode(bool v) async {\n final p = await SharedPreferences.getInstance();\n await p.setBool(_keyConciseMode, v);\n }\n\n static Future<bool> getConciseMode() async {\n final p = await SharedPreferences.getInstance();\n return p.getBool(_keyConciseMode) ?? false;\n }\n\n static Future<void> saveFreeformMode(bool v) async {\n final p = await SharedPreferences.getInstance();\n await p.setBool(_keyFreeformMode, v);\n }\n\n static Future<bool> getFreeformMode() async {\n final p = await SharedPreferences.getInstance();\n return p.getBool(_keyFreeformMode) ?? false;\n }\n\n static Future<void> saveFreeformMaxLines(int v) async {\n final p = await SharedPreferences.getInstance();\n await p.setInt(_keyFreeformMaxLines, v);\n }\n\n static Future<int> getFreeformMaxLines() async {\n final p = await SharedPreferences.getInstance();\n return p.getInt(_keyFreeformMaxLines) ?? 8;\n }\n\n static Future<LocalModelService> _loadLocalModelService(String key) async {\n final service = LocalModelService(modelKey: key);\n final ok = await service.initialize();\n if (!ok) {\n await service.dispose();\n throw Exception('本地模型加載失敗');\n }\n return service;\n }\n\n static Future<void> _disposeCachedLocalModel() async {\n await _cachedLocalModelService?.dispose();\n _cachedLocalModelService = null;\n _cachedLocalModelFuture = null;\n _cachedLocalModelKey = null;\n }\n\n static Future<String> _modelForProvider(String provider) async {\n switch (provider) {\n case 'bedrock':\n return getAwsModelId();\n case 'gemini':\n return getGeminiModel();\n case 'deepseek':\n return getDeepseekModel();\n case 'qwen':\n return getQwenModel();\n case 'openai_compatible':\n return getOaiCompatModel();\n case 'local_api':\n final model = await getLocalApiModel();\n return model.isEmpty ? '' : toLocalApiModelId(model);\n default:\n final p = await SharedPreferences.getInstance();\n return p.getString(_keyModel) ?? 'anthropic/claude-sonnet-4-20250514';\n }\n }\n}\n"}, "8886c1663abe2473caa42a3fcf826ed185f3817d": {"t": 1785757858.0134656, "n": 19857, "h": "ed52e1f172c59791", "c": "import 'package:shared_preferences/shared_preferences.dart';\nimport '../secure_store.dart';\nimport '../api_adapter.dart';\nimport '../openrouter_service.dart';\nimport '../aws_bedrock_service.dart';\nimport '../gemini_service.dart';\nimport '../deepseek_service.dart';\nimport '../qwen_service.dart';\nimport '../openai_compatible_service.dart';\nimport '../local_model_service.dart';\n\n/// API 供應商配置(OpenRouter / Bedrock / Gemini / DeepSeek / Qwen / OpenAI Compatible / Local API)\nclass ApiSettings {\n // ═══ 通用 ═══\n static const String _keyLegacyApiKey = 'api_key';\n static const String _keyOpenRouterApiKey = 'openrouter_api_key';\n static const String _keyModel = 'model';\n static const String _keySystemPrompt = 'system_prompt';\n static const String _keyApiProvider = 'api_provider';\n static const String _keyImageGenModel = 'image_gen_model';\n static String? _cachedLocalModelKey;\n static LocalModelService? _cachedLocalModelService;\n static Future<LocalModelService>? _cachedLocalModelFuture;\n static String? _cachedGeminiKey;\n static String? _cachedGeminiModel;\n static GeminiService? _cachedGeminiService;\n static const String localApiModelPrefix = 'local_api:';\n\n static Future<String> getApiProviderName() async {\n final p = await SharedPreferences.getInstance();\n return p.getString(_keyApiProvider) ?? 'openrouter';\n }\n\n /// 畫圖模型(OpenRouter 圖像輸出模型)\n static Future<void> saveImageGenModel(String model) async {\n final p = await SharedPreferences.getInstance();\n await p.setString(_keyImageGenModel, model);\n }\n\n static Future<String> getImageGenModel() async {\n final p = await SharedPreferences.getInstance();\n final v = p.getString(_keyImageGenModel) ?? '';\n return v.isEmpty ? 'openai/gpt-5.4-image-2' : v;\n }\n\n static Future<void> saveApiKey(String key) async {\n await saveOpenRouterApiKey(key);\n }\n\n static Future<String> getApiKey() async {\n return getApiKeyForProvider(await getApiProvider());\n }\n\n static Future<void> saveOpenRouterApiKey(String key) async {\n await SecureStore.write(_keyOpenRouterApiKey, key);\n await SecureStore.delete(_keyLegacyApiKey);\n }\n\n static Future<String> getOpenRouterApiKey() async {\n final key = await SecureStore.read(_keyOpenRouterApiKey);\n if (key.isNotEmpty) return key;\n\n final legacy = await SecureStore.read(_keyLegacyApiKey);\n if (legacy.isNotEmpty) {\n await saveOpenRouterApiKey(legacy);\n }\n return legacy;\n }\n\n static Future<void> saveApiKeyForProvider(String provider, String key) async {\n switch (provider) {\n case 'openrouter':\n await saveOpenRouterApiKey(key);\n break;\n case 'gemini':\n await saveGeminiApiKey(key);\n break;\n case 'deepseek':\n await saveDeepseekApiKey(key);\n break;\n case 'qwen':\n await saveQwenApiKey(key);\n break;\n case 'openai_compatible':\n await saveOaiCompatApiKey(key);\n break;\n case 'local_api':\n await saveLocalApiKey(key);\n break;\n }\n }\n\n static Future<String> getApiKeyForProvider(String provider) async {\n switch (provider) {\n case 'openrouter':\n return getOpenRouterApiKey();\n case 'gemini':\n return getGeminiApiKey();\n case 'deepseek':\n return getDeepseekApiKey();\n case 'qwen':\n return getQwenApiKey();\n case 'openai_compatible':\n return getOaiCompatApiKey();\n case 'local_api':\n return getLocalApiKey();\n default:\n return '';\n }\n }\n\n static Future<void> saveModel(String model) async {\n final p = await SharedPreferences.getInstance();\n await p.setString(_keyModel, model);\n if (isLocalApiModelId(model)) {\n await p.setString(_keyLocalApiModel, extractLocalApiModel(model));\n await p.setString(_keyApiProvider, 'local_api');\n } else if ((p.getString(_keyApiProvider) ?? 'openrouter') == 'local_api') {\n await p.setString(_keyApiProvider, 'openrouter');\n }\n }\n\n static Future<String> getModel() async {\n final p = await SharedPreferences.getInstance();\n final provider = p.getString(_keyApiProvider) ?? 'openrouter';\n if (provider == 'local_api') {\n final model = p.getString(_keyLocalApiModel) ?? '';\n if (model.isNotEmpty) return toLocalApiModelId(model);\n final savedModel = p.getString(_keyModel) ?? '';\n return isLocalApiModelId(savedModel) ? savedModel : '';\n }\n if (provider == 'openai_compatible') {\n return p.getString(_keyOaiCompatModel) ?? '';\n }\n if (provider == 'gemini') {\n return p.getString(_keyGeminiModel) ?? 'gemini-2.0-flash';\n }\n if (provider == 'bedrock') {\n return p.getString(_keyAwsModelId) ??\n 'anthropic.claude-sonnet-4-20250514-v1:0';\n }\n if (provider == 'deepseek') {\n return p.getString(_keyDeepseekModel) ?? 'deepseek-chat';\n }\n if (provider == 'qwen') {\n return p.getString(_keyQwenModel) ?? 'qwen-plus';\n }\n return p.getString(_keyModel) ?? 'anthropic/claude-sonnet-4-20250514';\n }\n\n static Future<String> getModelForProvider(String provider) {\n return _modelForProvider(provider);\n }\n\n static Future<void> saveModelForProvider(\n String provider,\n String model,\n ) async {\n final value = model.trim();\n if (value.isEmpty) return;\n\n switch (provider) {\n case 'bedrock':\n await saveAwsModelId(value);\n break;\n case 'gemini':\n await saveGeminiModel(value);\n break;\n case 'deepseek':\n await saveDeepseekModel(value);\n break;\n case 'qwen':\n await saveQwenModel(value);\n break;\n case 'openai_compatible':\n await saveOaiCompatModel(value);\n break;\n case 'local_api':\n await saveLocalApiModel(value);\n break;\n default:\n await saveModel(value);\n break;\n }\n }\n\n static Future<void> saveSystemPrompt(String prompt) async {\n final p = await SharedPreferences.getInstance();\n await p.setString(_keySystemPrompt, prompt);\n }\n\n static Future<String> getSystemPrompt() async {\n final p = await SharedPreferences.getInstance();\n return p.getString(_keySystemPrompt) ?? '';\n }\n\n static Future<void> saveApiProvider(String provider) async {\n final p = await SharedPreferences.getInstance();\n await p.setString(_keyApiProvider, provider);\n }\n\n static Future<String> getApiProvider() async {\n final p = await SharedPreferences.getInstance();\n return p.getString(_keyApiProvider) ?? 'openrouter';\n }\n\n // ═══ AWS Bedrock ═══\n static const String _keyAwsAccessKeyId = 'aws_access_key_id';\n static const String _keyAwsSecretAccessKey = 'aws_secret_access_key';\n static const String _keyAwsRegion = 'aws_region';\n static const String _keyAwsModelId = 'aws_model_id';\n\n static Future<void> saveAwsAccessKeyId(String id) async {\n await SecureStore.write(_keyAwsAccessKeyId, id);\n }\n\n static Future<String> getAwsAccessKeyId() async {\n return SecureStore.read(_keyAwsAccessKeyId);\n }\n\n static Future<void> saveAwsSecretAccessKey(String key) async {\n await SecureStore.write(_keyAwsSecretAccessKey, key);\n }\n\n static Future<String> getAwsSecretAccessKey() async {\n return SecureStore.read(_keyAwsSecretAccessKey);\n }\n\n static Future<void> saveAwsRegion(String region) async {\n final p = await SharedPreferences.getInstance();\n await p.setString(_keyAwsRegion, region);\n }\n\n static Future<String> getAwsRegion() async {\n final p = await SharedPreferences.getInstance();\n return p.getString(_keyAwsRegion) ?? 'us-east-1';\n }\n\n static Future<void> saveAwsModelId(String modelId) async {\n final p = await SharedPreferences.getInstance();\n await p.setString(_keyAwsModelId, modelId);\n }\n\n static Future<String> getAwsModelId() async {\n final p = await SharedPreferences.getInstance();\n return p.getString(_keyAwsModelId) ??\n 'anthropic.claude-sonnet-4-20250514-v1:0';\n }\n\n // ═══ Gemini ═══\n static const String _keyGeminiApiKey = 'gemini_api_key';\n static const String _keyGeminiModel = 'gemini_model';\n\n static Future<void> saveGeminiApiKey(String v) async {\n await SecureStore.write(_keyGeminiApiKey, v);\n }\n\n static Future<String> getGeminiApiKey() async {\n return SecureStore.read(_keyGeminiApiKey);\n }\n\n static Future<void> saveGeminiModel(String v) async {\n final p = await SharedPreferences.getInstance();\n await p.setString(_keyGeminiModel, v);\n }\n\n static Future<String> getGeminiModel() async {\n final p = await SharedPreferences.getInstance();\n return p.getString(_keyGeminiModel) ?? 'gemini-2.0-flash';\n }\n\n // ═══ DeepSeek ═══\n static const String _keyDeepseekApiKey = 'deepseek_api_key';\n static const String _keyDeepseekModel = 'deepseek_model';\n\n static Future<void> saveDeepseekApiKey(String v) async {\n await SecureStore.write(_keyDeepseekApiKey, v);\n }\n\n static Future<String> getDeepseekApiKey() async {\n return SecureStore.read(_keyDeepseekApiKey);\n }\n\n static Future<void> saveDeepseekModel(String v) async {\n final p = await SharedPreferences.getInstance();\n await p.setString(_keyDeepseekModel, v);\n }\n\n static Future<String> getDeepseekModel() async {\n final p = await SharedPreferences.getInstance();\n return p.getString(_keyDeepseekModel) ?? 'deepseek-chat';\n }\n\n // ═══ Qwen ═══\n static const String _keyQwenApiKey = 'qwen_api_key';\n static const String _keyQwenModel = 'qwen_model';\n\n static Future<void> saveQwenApiKey(String v) async {\n await SecureStore.write(_keyQwenApiKey, v);\n }\n\n static Future<String> getQwenApiKey() async {\n return SecureStore.read(_keyQwenApiKey);\n }\n\n static Future<void> saveQwenModel(String v) async {\n final p = await SharedPreferences.getInstance();\n await p.setString(_keyQwenModel, v);\n }\n\n static Future<String> getQwenModel() async {\n final p = await SharedPreferences.getInstance();\n return p.getString(_keyQwenModel) ?? 'qwen-plus';\n }\n\n // ═══ OpenAI Compatible(中轉站)═══\n static const String _keyOaiCompatBaseUrl = 'oai_compat_base_url';\n static const String _keyOaiCompatApiKey = 'oai_compat_api_key';\n static const String _keyOaiCompatModel = 'oai_compat_model';\n\n static Future<void> saveOaiCompatBaseUrl(String v) async {\n final p = await SharedPreferences.getInstance();\n await p.setString(_keyOaiCompatBaseUrl, v);\n }\n\n static Future<String> getOaiCompatBaseUrl() async {\n final p = await SharedPreferences.getInstance();\n return p.getString(_keyOaiCompatBaseUrl) ?? '';\n }\n\n static Future<void> saveOaiCompatApiKey(String v) async {\n await SecureStore.write(_keyOaiCompatApiKey, v);\n }\n\n static Future<String> getOaiCompatApiKey() async {\n return SecureStore.read(_keyOaiCompatApiKey);\n }\n\n static Future<void> saveOaiCompatModel(String v) async {\n final p = await SharedPreferences.getInstance();\n await p.setString(_keyOaiCompatModel, v);\n }\n\n static Future<String> getOaiCompatModel() async {\n final p = await SharedPreferences.getInstance();\n return p.getString(_keyOaiCompatModel) ?? '';\n }\n\n // ═══ Local API(電腦 / 局域網 OpenAI-compatible 端點)═══\n static const String _keyLocalApiBaseUrl = 'local_api_base_url';\n static const String _keyLocalApiKey = 'local_api_key';\n static const String _keyLocalApiModel = 'local_api_model';\n\n static bool isLocalApiModelId(String modelId) =>\n modelId.startsWith(localApiModelPrefix);\n\n static String toLocalApiModelId(String model) {\n final trimmed = model.trim();\n if (trimmed.startsWith(localApiModelPrefix)) return trimmed;\n return '$localApiModelPrefix$trimmed';\n }\n\n static String extractLocalApiModel(String modelId) =>\n modelId.startsWith(localApiModelPrefix)\n ? modelId.substring(localApiModelPrefix.length)\n : modelId;\n\n static Future<void> saveLocalApiBaseUrl(String v) async {\n final p = await SharedPreferences.getInstance();\n await p.setString(_keyLocalApiBaseUrl, v);\n }\n\n static Future<String> getLocalApiBaseUrl() async {\n final p = await SharedPreferences.getInstance();\n return p.getString(_keyLocalApiBaseUrl) ?? '';\n }\n\n static Future<void> saveLocalApiKey(String v) async {\n await SecureStore.write(_keyLocalApiKey, v);\n }\n\n static Future<String> getLocalApiKey() async {\n return SecureStore.read(_keyLocalApiKey);\n }\n\n static Future<void> saveLocalApiModel(String v) async {\n final p = await SharedPreferences.getInstance();\n await p.setString(_keyLocalApiModel, extractLocalApiModel(v.trim()));\n }\n\n static Future<String> getLocalApiModel() async {\n final p = await SharedPreferences.getInstance();\n return p.getString(_keyLocalApiModel) ?? '';\n }\n\n // ═══ 星標模型 ═══\n static const String _keyStarredModels = 'starred_models';\n\n static Future<void> saveStarredModels(List<String> models) async {\n final p = await SharedPreferences.getInstance();\n await p.setStringList(_keyStarredModels, models);\n }\n\n static Future<List<String>> getStarredModels() async {\n final p = await SharedPreferences.getInstance();\n return p.getStringList(_keyStarredModels) ?? [];\n }\n\n static Future<void> toggleStarModel(String modelId) async {\n final list = await getStarredModels();\n if (list.contains(modelId)) {\n list.remove(modelId);\n } else {\n list.add(modelId);\n }\n await saveStarredModels(list);\n }\n\n // ═══ 工廠方法 ═══\n\n static Future<ApiAdapter> buildAdapter({\n String? overrideModel,\n String? overrideProvider,\n }) async {\n final provider = overrideProvider ?? await getApiProvider();\n // 如果指定了本地模型,直接返回 LocalModelService\n final modelId = overrideModel ?? await _modelForProvider(provider);\n if (LocalModelService.isLocalModelId(modelId)) {\n final key = LocalModelService.extractKey(modelId);\n final cached = _cachedLocalModelService;\n if (_cachedLocalModelKey == key && cached != null) return cached;\n\n final pending = _cachedLocalModelFuture;\n if (_cachedLocalModelKey == key && pending != null) return pending;\n\n await _disposeCachedLocalModel();\n _cachedLocalModelKey = key;\n _cachedLocalModelFuture = _loadLocalModelService(key);\n try {\n final service = await _cachedLocalModelFuture!;\n _cachedLocalModelService = service;\n return service;\n } finally {\n _cachedLocalModelFuture = null;\n }\n }\n\n if (isLocalApiModelId(modelId)) {\n await _disposeCachedLocalModel();\n final baseUrl = await getLocalApiBaseUrl();\n final key = await getLocalApiKey();\n if (baseUrl.isEmpty) throw Exception('本地 API Base URL 未設定');\n return OpenAICompatibleService(\n apiKey: key,\n baseUrl: baseUrl,\n modelPrefixToStrip: localApiModelPrefix,\n );\n }\n\n await _disposeCachedLocalModel();\n\n if (provider == 'local_api') {\n final baseUrl = await getLocalApiBaseUrl();\n final key = await getLocalApiKey();\n if (baseUrl.isEmpty) throw Exception('本地 API Base URL 未設定');\n return OpenAICompatibleService(\n apiKey: key,\n baseUrl: baseUrl,\n modelPrefixToStrip: localApiModelPrefix,\n );\n }\n\n if (provider == 'bedrock') {\n final accessKeyId = await getAwsAccessKeyId();\n final secretAccessKey = await getAwsSecretAccessKey();\n final region = await getAwsRegion();\n final bedrockModelId = overrideModel ?? await getAwsModelId();\n if (accessKeyId.isEmpty || secretAccessKey.isEmpty) {\n throw Exception('AWS 憑證未設定');\n }\n return BedrockService(\n accessKeyId: accessKeyId,\n secretAccessKey: secretAccessKey,\n region: region,\n modelId: bedrockModelId,\n );\n }\n\n if (provider == 'gemini') {\n final key = await getGeminiApiKey();\n if (key.isEmpty) throw Exception('Gemini API Key 未設定');\n final model = overrideModel ?? await getGeminiModel();\n final cached = _cachedGeminiService;\n if (_cachedGeminiKey == key &&\n _cachedGeminiModel == model &&\n cached != null) {\n return cached;\n }\n _cachedGeminiKey = key;\n _cachedGeminiModel = model;\n _cachedGeminiService = GeminiService(apiKey: key, modelId: model);\n return _cachedGeminiService!;\n }\n\n if (provider == 'deepseek') {\n final key = await getDeepseekApiKey();\n if (key.isEmpty) throw Exception('DeepSeek API Key 未設定');\n return DeepSeekService(apiKey: key);\n }\n\n if (provider == 'qwen') {\n final key = await getQwenApiKey();\n if (key.isEmpty) throw Exception('Qwen API Key 未設定');\n return QwenService(apiKey: key);\n }\n\n if (provider == 'openai_compatible') {\n final baseUrl = await getOaiCompatBaseUrl();\n final key = await getOaiCompatApiKey();\n if (baseUrl.isEmpty) throw Exception('API Base URL 未設定');\n if (key.isEmpty) throw Exception('API Key 未設定');\n return OpenAICompatibleService(apiKey: key, baseUrl: baseUrl);\n }\n\n // 默認 OpenRouter\n final apiKey = await getOpenRouterApiKey();\n if (apiKey.isEmpty) {\n throw Exception('OpenRouter API Key 未設定');\n }\n return OpenRouterService(apiKey: apiKey);\n }\n\n // ═══ 思考鏈 ═══\n static const String _keyThinkingChain = 'thinking_chain_enabled';\n\n static Future<void> saveThinkingChain(bool v) async {\n final p = await SharedPreferences.getInstance();\n await p.setBool(_keyThinkingChain, v);\n }\n\n static Future<bool> getThinkingChain() async {\n final p = await SharedPreferences.getInstance();\n return p.getBool(_keyThinkingChain) ?? false;\n }\n\n // ═══ 節省 Token ═══\n static const String _keyConciseMode = 'token_save_concise';\n static const String _keyFreeformMode = 'token_save_freeform';\n static const String _keyFreeformMaxLines = 'token_save_max_lines';\n\n static Future<void> saveConciseMode(bool v) async {\n final p = await SharedPreferences.getInstance();\n await p.setBool(_keyConciseMode, v);\n }\n\n static Future<bool> getConciseMode() async {\n final p = await SharedPreferences.getInstance();\n return p.getBool(_keyConciseMode) ?? false;\n }\n\n static Future<void> saveFreeformMode(bool v) async {\n final p = await SharedPreferences.getInstance();\n await p.setBool(_keyFreeformMode, v);\n }\n\n static Future<bool> getFreeformMode() async {\n final p = await SharedPreferences.getInstance();\n return p.getBool(_keyFreeformMode) ?? false;\n }\n\n static Future<void> saveFreeformMaxLines(int v) async {\n final p = await SharedPreferences.getInstance();\n await p.setInt(_keyFreeformMaxLines, v);\n }\n\n static Future<int> getFreeformMaxLines() async {\n final p = await SharedPreferences.getInstance();\n return p.getInt(_keyFreeformMaxLines) ?? 8;\n }\n\n static Future<LocalModelService> _loadLocalModelService(String key) async {\n final service = LocalModelService(modelKey: key);\n final ok = await service.initialize();\n if (!ok) {\n await service.dispose();\n throw Exception('本地模型加載失敗');\n }\n return service;\n }\n\n static Future<void> _disposeCachedLocalModel() async {\n await _cachedLocalModelService?.dispose();\n _cachedLocalModelService = null;\n _cachedLocalModelFuture = null;\n _cachedLocalModelKey = null;\n }\n\n static Future<String> _modelForProvider(String provider) async {\n switch (provider) {\n case 'bedrock':\n return getAwsModelId();\n case 'gemini':\n return getGeminiModel();\n case 'deepseek':\n return getDeepseekModel();\n case 'qwen':\n return getQwenModel();\n case 'openai_compatible':\n return getOaiCompatModel();\n case 'local_api':\n final model = await getLocalApiModel();\n return model.isEmpty ? '' : toLocalApiModelId(model);\n default:\n final p = await SharedPreferences.getInstance();\n return p.getString(_keyModel) ?? 'anthropic/claude-sonnet-4-20250514';\n }\n }\n}\n"}}}} |