Files

1 line
41 KiB
JSON
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{"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"}}}}