From 39360dc058ec020503312e05c2e40ba6b42a68b5 Mon Sep 17 00:00:00 2001 From: "mingholy.lmh" Date: Tue, 17 Feb 2026 20:19:21 +0800 Subject: [PATCH] feat(cli): add Coding Plan Global/Intl region support Add support for Coding Plan international region with separate base URL: - Add CodingPlanRegion enum (CHINA, GLOBAL) for region management - Add CODING_PLAN_INTL_MODELS template with intl base URL - Add version storage for both regions (codingPlan.version/versionIntl) - Update AuthDialog to show both region options - Update useCodingPlanUpdates to handle region-specific updates - Add i18n translations for all supported languages - Fix and update unit tests Users can now choose between: - Coding Plan (Bailian, China) - https://coding.dashscope.aliyuncs.com/v1 - Coding Plan (Bailian, Global/Intl) - https://coding-intl.dashscope.aliyuncs.com/v1 Co-authored-by: Qwen-Coder --- packages/cli/src/constants/codingPlan.ts | 141 ++++++++- packages/cli/src/i18n/locales/de.js | 17 ++ packages/cli/src/i18n/locales/en.js | 17 ++ packages/cli/src/i18n/locales/ja.js | 18 ++ packages/cli/src/i18n/locales/pt.js | 17 ++ packages/cli/src/i18n/locales/ru.js | 18 ++ packages/cli/src/i18n/locales/zh.js | 17 ++ packages/cli/src/ui/auth/AuthDialog.tsx | 27 +- packages/cli/src/ui/auth/useAuth.ts | 56 ++-- .../cli/src/ui/components/ApiKeyInput.tsx | 15 +- .../cli/src/ui/contexts/UIActionsContext.tsx | 6 +- .../src/ui/hooks/useCodingPlanUpdates.test.ts | 275 ++++++++++++++--- .../cli/src/ui/hooks/useCodingPlanUpdates.ts | 279 +++++++++--------- 13 files changed, 684 insertions(+), 219 deletions(-) diff --git a/packages/cli/src/constants/codingPlan.ts b/packages/cli/src/constants/codingPlan.ts index e55aeb93d..845f85d19 100644 --- a/packages/cli/src/constants/codingPlan.ts +++ b/packages/cli/src/constants/codingPlan.ts @@ -7,6 +7,14 @@ import { createHash } from 'node:crypto'; import type { ProviderModelConfig as ModelConfig } from '@qwen-code/qwen-code-core'; +/** + * Coding plan regions + */ +export enum CodingPlanRegion { + CHINA = 'china', + GLOBAL = 'global', +} + /** * Coding plan template - array of model configurations * When user provides an api-key, these configs will be cloned with envKey pointing to the stored api-key @@ -14,18 +22,34 @@ import type { ProviderModelConfig as ModelConfig } from '@qwen-code/qwen-code-co export type CodingPlanTemplate = ModelConfig[]; /** - * Environment variable key for storing the coding plan API key + * Environment variable key for storing the coding plan API key (China/Bailian) */ export const CODING_PLAN_ENV_KEY = 'BAILIAN_CODING_PLAN_API_KEY'; /** - * CODING_PLAN_MODELS defines the model configurations for coding-plan mode. + * Environment variable key for storing the coding plan API key (Global/Intl) + */ +export const CODING_PLAN_INTL_ENV_KEY = 'BAILIAN_CODING_PLAN_INTL_API_KEY'; + +/** + * Base URL for China/Bailian Coding Plan + */ +export const CODING_PLAN_BASE_URL = 'https://coding.dashscope.aliyuncs.com/v1'; + +/** + * Base URL for Global/Intl Coding Plan + */ +export const CODING_PLAN_INTL_BASE_URL = + 'https://coding-intl.dashscope.aliyuncs.com/v1'; + +/** + * CODING_PLAN_MODELS defines the model configurations for coding-plan mode (China/Bailian). */ export const CODING_PLAN_MODELS: CodingPlanTemplate = [ { id: 'qwen3-coder-plus', name: 'qwen3-coder-plus', - baseUrl: 'https://coding.dashscope.aliyuncs.com/v1', + baseUrl: CODING_PLAN_BASE_URL, description: 'qwen3-coder-plus model from Bailian Coding Plan', envKey: CODING_PLAN_ENV_KEY, }, @@ -34,7 +58,7 @@ export const CODING_PLAN_MODELS: CodingPlanTemplate = [ name: 'qwen3-max-2026-01-23', description: 'qwen3-max model with thinking enabled from Bailian Coding Plan', - baseUrl: 'https://coding.dashscope.aliyuncs.com/v1', + baseUrl: CODING_PLAN_BASE_URL, envKey: CODING_PLAN_ENV_KEY, generationConfig: { extra_body: { @@ -44,18 +68,119 @@ export const CODING_PLAN_MODELS: CodingPlanTemplate = [ }, ]; +/** + * CODING_PLAN_INTL_MODELS defines the model configurations for coding-plan mode (Global/Intl). + */ +export const CODING_PLAN_INTL_MODELS: CodingPlanTemplate = [ + { + id: 'qwen3-coder-plus', + name: 'qwen3-coder-plus', + baseUrl: CODING_PLAN_INTL_BASE_URL, + description: 'qwen3-coder-plus model from Coding Plan (Global/Intl)', + envKey: CODING_PLAN_INTL_ENV_KEY, + }, + { + id: 'qwen3-max-2026-01-23', + name: 'qwen3-max-2026-01-23', + description: + 'qwen3-max model with thinking enabled from Coding Plan (Global/Intl)', + baseUrl: CODING_PLAN_INTL_BASE_URL, + envKey: CODING_PLAN_INTL_ENV_KEY, + generationConfig: { + extra_body: { + enable_thinking: true, + }, + }, + }, +]; + /** * Computes the version hash for the coding plan template. * Uses SHA256 of the JSON-serialized template for deterministic versioning. + * @param template - The template to compute version for * @returns Hexadecimal string representing the template version */ -export function computeCodingPlanVersion(): string { - const templateString = JSON.stringify(CODING_PLAN_MODELS); +export function computeCodingPlanVersion(template: CodingPlanTemplate): string { + const templateString = JSON.stringify(template); return createHash('sha256').update(templateString).digest('hex'); } /** - * Current version of the coding plan template. + * Current version of the China/Bailian coding plan template. * Computed at runtime from the template content. */ -export const CODING_PLAN_VERSION = computeCodingPlanVersion(); +export const CODING_PLAN_VERSION = computeCodingPlanVersion(CODING_PLAN_MODELS); + +/** + * Current version of the Global/Intl coding plan template. + * Computed at runtime from the template content. + */ +export const CODING_PLAN_INTL_VERSION = computeCodingPlanVersion( + CODING_PLAN_INTL_MODELS, +); + +/** + * All coding plan templates for both regions. + * Used for update detection and filtering. + */ +export const ALL_CODING_PLAN_TEMPLATES: CodingPlanTemplate = [ + ...CODING_PLAN_MODELS, + ...CODING_PLAN_INTL_MODELS, +]; + +/** + * Check if a config belongs to any Coding Plan template (China or Intl). + * @param baseUrl - The baseUrl to check + * @param envKey - The envKey to check + * @param region - Optional region to limit the check to a specific region + * @returns true if the config matches any Coding Plan template + */ +export function isCodingPlanConfig( + baseUrl: string | undefined, + envKey: string | undefined, + region?: CodingPlanRegion, +): boolean { + if (!baseUrl || !envKey) { + return false; + } + + // If region is specified, only check that region's templates + if (region === CodingPlanRegion.GLOBAL) { + return CODING_PLAN_INTL_MODELS.some( + (template) => template.baseUrl === baseUrl && template.envKey === envKey, + ); + } else if (region === CodingPlanRegion.CHINA) { + return CODING_PLAN_MODELS.some( + (template) => template.baseUrl === baseUrl && template.envKey === envKey, + ); + } + + // No region specified, check all templates + return ALL_CODING_PLAN_TEMPLATES.some( + (template) => template.baseUrl === baseUrl && template.envKey === envKey, + ); +} + +/** + * Get the appropriate template and env key for the selected region. + * @param region - The region to use (default: CHINA) + * @returns Object containing template, envKey, version, and baseUrl + */ +export function getCodingPlanConfig( + region: CodingPlanRegion = CodingPlanRegion.CHINA, +) { + if (region === CodingPlanRegion.GLOBAL) { + return { + template: CODING_PLAN_INTL_MODELS, + envKey: CODING_PLAN_INTL_ENV_KEY, + version: CODING_PLAN_INTL_VERSION, + baseUrl: CODING_PLAN_INTL_BASE_URL, + }; + } + return { + template: CODING_PLAN_MODELS, + envKey: CODING_PLAN_ENV_KEY, + version: CODING_PLAN_VERSION, + baseUrl: CODING_PLAN_BASE_URL, + }; +} diff --git a/packages/cli/src/i18n/locales/de.js b/packages/cli/src/i18n/locales/de.js index d000dc1f4..291e14516 100644 --- a/packages/cli/src/i18n/locales/de.js +++ b/packages/cli/src/i18n/locales/de.js @@ -1417,8 +1417,12 @@ export default { // Auth Dialog - View Titles and Labels // ============================================================================ 'Coding Plan': 'Coding Plan', + 'Coding Plan (Bailian, China)': 'Coding Plan (Bailian, China)', + 'Coding Plan (Bailian, Global/Intl)': 'Coding Plan (Bailian, Global/Intl)', "Paste your api key of Bailian Coding Plan and you're all set!": 'Fügen Sie Ihren Bailian Coding Plan API-Schlüssel ein und Sie sind bereit!', + "Paste your api key of Coding Plan (Global/Intl) and you're all set!": + 'Fügen Sie Ihren Coding Plan (Global/Intl) API-Schlüssel ein und Sie sind bereit!', Custom: 'Benutzerdefiniert', 'More instructions about configuring `modelProviders` manually.': 'Weitere Anweisungen zur manuellen Konfiguration von `modelProviders`.', @@ -1428,4 +1432,17 @@ export default { '(Press Enter to submit, Escape to cancel)': '(Enter zum Absenden, Escape zum Abbrechen)', 'More instructions please check:': 'Weitere Anweisungen finden Sie unter:', + + // ============================================================================ + // Coding Plan International Updates + // ============================================================================ + 'New model configurations are available for Bailian Coding Plan (China). Update now?': + 'Neue Modellkonfigurationen sind für Bailian Coding Plan (China) verfügbar. Jetzt aktualisieren?', + 'New model configurations are available for Coding Plan (Global/Intl). Update now?': + 'Neue Modellkonfigurationen sind für Coding Plan (Global/Intl) verfügbar. Jetzt aktualisieren?', + '{{region}} configuration updated successfully. New models are now available.': + '{{region}}-Konfiguration erfolgreich aktualisiert. Neue Modelle sind jetzt verfügbar.', + 'Authenticated successfully with {{region}}. API key is stored in settings.env.': + 'Erfolgreich mit {{region}} authentifiziert. API-Schlüssel ist in settings.env gespeichert.', + 'Coding Plan (Global/Intl)': 'Coding Plan (Global/Intl)', }; diff --git a/packages/cli/src/i18n/locales/en.js b/packages/cli/src/i18n/locales/en.js index 88b376622..a650d927f 100644 --- a/packages/cli/src/i18n/locales/en.js +++ b/packages/cli/src/i18n/locales/en.js @@ -1418,8 +1418,12 @@ export default { // Auth Dialog - View Titles and Labels // ============================================================================ 'Coding Plan': 'Coding Plan', + 'Coding Plan (Bailian, China)': 'Coding Plan (Bailian, China)', + 'Coding Plan (Bailian, Global/Intl)': 'Coding Plan (Bailian, Global/Intl)', "Paste your api key of Bailian Coding Plan and you're all set!": "Paste your api key of Bailian Coding Plan and you're all set!", + "Paste your api key of Coding Plan (Global/Intl) and you're all set!": + "Paste your api key of Coding Plan (Global/Intl) and you're all set!", Custom: 'Custom', 'More instructions about configuring `modelProviders` manually.': 'More instructions about configuring `modelProviders` manually.', @@ -1427,4 +1431,17 @@ export default { '(Press Escape to go back)': '(Press Escape to go back)', '(Press Enter to submit, Escape to cancel)': '(Press Enter to submit, Escape to cancel)', + + // ============================================================================ + // Coding Plan International Updates + // ============================================================================ + 'New model configurations are available for Bailian Coding Plan (China). Update now?': + 'New model configurations are available for Bailian Coding Plan (China). Update now?', + 'New model configurations are available for Coding Plan (Global/Intl). Update now?': + 'New model configurations are available for Coding Plan (Global/Intl). Update now?', + '{{region}} configuration updated successfully. New models are now available.': + '{{region}} configuration updated successfully. New models are now available.', + 'Authenticated successfully with {{region}}. API key is stored in settings.env.': + 'Authenticated successfully with {{region}}. API key is stored in settings.env.', + 'Coding Plan (Global/Intl)': 'Coding Plan (Global/Intl)', }; diff --git a/packages/cli/src/i18n/locales/ja.js b/packages/cli/src/i18n/locales/ja.js index 6f9ffe12d..e20e33e55 100644 --- a/packages/cli/src/i18n/locales/ja.js +++ b/packages/cli/src/i18n/locales/ja.js @@ -928,8 +928,13 @@ export default { // Auth Dialog - View Titles and Labels // ============================================================================ 'Coding Plan': 'Coding Plan', + 'Coding Plan (Bailian, China)': 'Coding Plan (Bailian, 中国)', + 'Coding Plan (Bailian, Global/Intl)': + 'Coding Plan (Bailian, グローバル/国際)', "Paste your api key of Bailian Coding Plan and you're all set!": 'Bailian Coding PlanのAPIキーを貼り付けるだけで準備完了です!', + "Paste your api key of Coding Plan (Global/Intl) and you're all set!": + 'Coding Plan (グローバル/国際) のAPIキーを貼り付けるだけで準備完了です!', Custom: 'カスタム', 'More instructions about configuring `modelProviders` manually.': '`modelProviders`を手動で設定する方法の詳細はこちら。', @@ -938,4 +943,17 @@ export default { '(Press Enter to submit, Escape to cancel)': '(Enterで送信、Escapeでキャンセル)', 'More instructions please check:': '詳細な手順はこちらをご確認ください:', + + // ============================================================================ + // Coding Plan International Updates + // ============================================================================ + 'New model configurations are available for Bailian Coding Plan (China). Update now?': + 'Bailian Coding Plan (中国) の新しいモデル設定が利用可能です。今すぐ更新しますか?', + 'New model configurations are available for Coding Plan (Global/Intl). Update now?': + 'Coding Plan (グローバル/国際) の新しいモデル設定が利用可能です。今すぐ更新しますか?', + '{{region}} configuration updated successfully. New models are now available.': + '{{region}} の設定が正常に更新されました。新しいモデルが利用可能になりました。', + 'Authenticated successfully with {{region}}. API key is stored in settings.env.': + '{{region}} での認証に成功しました。APIキーは settings.env に保存されています。', + 'Coding Plan (Global/Intl)': 'Coding Plan (グローバル/国際)', }; diff --git a/packages/cli/src/i18n/locales/pt.js b/packages/cli/src/i18n/locales/pt.js index 08901262a..3519fadf2 100644 --- a/packages/cli/src/i18n/locales/pt.js +++ b/packages/cli/src/i18n/locales/pt.js @@ -1431,8 +1431,12 @@ export default { // Auth Dialog - View Titles and Labels // ============================================================================ 'Coding Plan': 'Coding Plan', + 'Coding Plan (Bailian, China)': 'Coding Plan (Bailian, China)', + 'Coding Plan (Bailian, Global/Intl)': 'Coding Plan (Bailian, Global/Intl)', "Paste your api key of Bailian Coding Plan and you're all set!": 'Cole sua chave de API do Bailian Coding Plan e pronto!', + "Paste your api key of Coding Plan (Global/Intl) and you're all set!": + 'Cole sua chave de API do Coding Plan (Global/Intl) e pronto!', Custom: 'Personalizado', 'More instructions about configuring `modelProviders` manually.': 'Mais instruções sobre como configurar `modelProviders` manualmente.', @@ -1442,4 +1446,17 @@ export default { '(Press Enter to submit, Escape to cancel)': '(Pressione Enter para enviar, Escape para cancelar)', 'More instructions please check:': 'Mais instruções, consulte:', + + // ============================================================================ + // Coding Plan International Updates + // ============================================================================ + 'New model configurations are available for Bailian Coding Plan (China). Update now?': + 'Novas configurações de modelo estão disponíveis para o Bailian Coding Plan (China). Atualizar agora?', + 'New model configurations are available for Coding Plan (Global/Intl). Update now?': + 'Novas configurações de modelo estão disponíveis para o Coding Plan (Global/Intl). Atualizar agora?', + '{{region}} configuration updated successfully. New models are now available.': + 'Configuração do {{region}} atualizada com sucesso. Novos modelos agora estão disponíveis.', + 'Authenticated successfully with {{region}}. API key is stored in settings.env.': + 'Autenticado com sucesso com {{region}}. A chave de API está armazenada em settings.env.', + 'Coding Plan (Global/Intl)': 'Coding Plan (Global/Intl)', }; diff --git a/packages/cli/src/i18n/locales/ru.js b/packages/cli/src/i18n/locales/ru.js index 3806807d6..6854cb1e9 100644 --- a/packages/cli/src/i18n/locales/ru.js +++ b/packages/cli/src/i18n/locales/ru.js @@ -1421,8 +1421,13 @@ export default { // Auth Dialog - View Titles and Labels // ============================================================================ 'Coding Plan': 'Coding Plan', + 'Coding Plan (Bailian, China)': 'Coding Plan (Bailian, Китай)', + 'Coding Plan (Bailian, Global/Intl)': + 'Coding Plan (Bailian, Глобальный/Международный)', "Paste your api key of Bailian Coding Plan and you're all set!": 'Вставьте ваш API-ключ Bailian Coding Plan и всё готово!', + "Paste your api key of Coding Plan (Global/Intl) and you're all set!": + 'Вставьте ваш API-ключ Coding Plan (Глобальный/Международный) и всё готово!', Custom: 'Пользовательский', 'More instructions about configuring `modelProviders` manually.': 'Дополнительные инструкции по ручной настройке `modelProviders`.', @@ -1431,4 +1436,17 @@ export default { '(Press Enter to submit, Escape to cancel)': '(Нажмите Enter для отправки, Escape для отмены)', 'More instructions please check:': 'Дополнительные инструкции см.:', + + // ============================================================================ + // Coding Plan International Updates + // ============================================================================ + 'New model configurations are available for Bailian Coding Plan (China). Update now?': + 'Доступны новые конфигурации моделей для Bailian Coding Plan (Китай). Обновить сейчас?', + 'New model configurations are available for Coding Plan (Global/Intl). Update now?': + 'Доступны новые конфигурации моделей для Coding Plan (Глобальный/Международный). Обновить сейчас?', + '{{region}} configuration updated successfully. New models are now available.': + 'Конфигурация {{region}} успешно обновлена. Новые модели теперь доступны.', + 'Authenticated successfully with {{region}}. API key is stored in settings.env.': + 'Успешная аутентификация с {{region}}. API-ключ сохранён в settings.env.', + 'Coding Plan (Global/Intl)': 'Coding Plan (Глобальный/Международный)', }; diff --git a/packages/cli/src/i18n/locales/zh.js b/packages/cli/src/i18n/locales/zh.js index 60c7551f2..d8c434e4b 100644 --- a/packages/cli/src/i18n/locales/zh.js +++ b/packages/cli/src/i18n/locales/zh.js @@ -1253,12 +1253,29 @@ export default { // ============================================================================ 'API-KEY': 'API-KEY', 'Coding Plan': 'Coding Plan', + 'Coding Plan (Bailian, China)': 'Coding Plan (百炼, 中国)', + 'Coding Plan (Bailian, Global/Intl)': 'Coding Plan (百炼, 全球/国际)', "Paste your api key of Bailian Coding Plan and you're all set!": '粘贴您的百炼 Coding Plan API Key,即可完成设置!', + "Paste your api key of Coding Plan (Global/Intl) and you're all set!": + '粘贴您的 Coding Plan (全球/国际) API Key,即可完成设置!', Custom: '自定义', 'More instructions about configuring `modelProviders` manually.': '关于手动配置 `modelProviders` 的更多说明。', 'Select API-KEY configuration mode:': '选择 API-KEY 配置模式:', '(Press Escape to go back)': '(按 Escape 键返回)', '(Press Enter to submit, Escape to cancel)': '(按 Enter 提交,Escape 取消)', + + // ============================================================================ + // Coding Plan International Updates + // ============================================================================ + 'New model configurations are available for Bailian Coding Plan (China). Update now?': + '百炼 Coding Plan (中国) 有新的模型配置可用。是否立即更新?', + 'New model configurations are available for Coding Plan (Global/Intl). Update now?': + 'Coding Plan (全球/国际) 有新的模型配置可用。是否立即更新?', + '{{region}} configuration updated successfully. New models are now available.': + '{{region}} 配置更新成功。新模型现已可用。', + 'Authenticated successfully with {{region}}. API key is stored in settings.env.': + '成功通过 {{region}} 认证。API Key 已存储在 settings.env 中。', + 'Coding Plan (Global/Intl)': 'Coding Plan (全球/国际)', }; diff --git a/packages/cli/src/ui/auth/AuthDialog.tsx b/packages/cli/src/ui/auth/AuthDialog.tsx index 17d464eed..24263f13a 100644 --- a/packages/cli/src/ui/auth/AuthDialog.tsx +++ b/packages/cli/src/ui/auth/AuthDialog.tsx @@ -17,6 +17,7 @@ import { useUIState } from '../contexts/UIStateContext.js'; import { useUIActions } from '../contexts/UIActionsContext.js'; import { useConfig } from '../contexts/ConfigContext.js'; import { t } from '../../i18n/index.js'; +import { CodingPlanRegion } from '../../constants/codingPlan.js'; const MODEL_PROVIDERS_DOCUMENTATION_URL = 'https://qwenlm.github.io/qwen-code-docs/en/users/configuration/settings/#modelproviders'; @@ -34,7 +35,7 @@ function parseDefaultAuthType( } // Sub-mode types for API-KEY authentication -type ApiKeySubMode = 'coding-plan' | 'custom'; +type ApiKeySubMode = 'coding-plan' | 'coding-plan-intl' | 'custom'; // View level for navigation type ViewLevel = 'main' | 'api-key-sub' | 'api-key-input' | 'custom-info'; @@ -52,6 +53,9 @@ export function AuthDialog(): React.JSX.Element { const [selectedIndex, setSelectedIndex] = useState(null); const [viewLevel, setViewLevel] = useState('main'); const [apiKeySubModeIndex, setApiKeySubModeIndex] = useState(0); + const [region, setRegion] = useState( + CodingPlanRegion.CHINA, + ); // Main authentication entries const mainItems = [ @@ -71,9 +75,14 @@ export function AuthDialog(): React.JSX.Element { const apiKeySubItems = [ { key: 'coding-plan', - label: t('Coding Plan (Bailian)'), + label: t('Coding Plan (Bailian, China)'), value: 'coding-plan' as ApiKeySubMode, }, + { + key: 'coding-plan-intl', + label: t('Coding Plan (Bailian, Global/Intl)'), + value: 'coding-plan-intl' as ApiKeySubMode, + }, { key: 'custom', label: t('Custom'), @@ -135,6 +144,10 @@ export function AuthDialog(): React.JSX.Element { onAuthError(null); if (subMode === 'coding-plan') { + setRegion(CodingPlanRegion.CHINA); + setViewLevel('api-key-input'); + } else if (subMode === 'coding-plan-intl') { + setRegion(CodingPlanRegion.GLOBAL); setViewLevel('api-key-input'); } else { setViewLevel('custom-info'); @@ -149,8 +162,8 @@ export function AuthDialog(): React.JSX.Element { return; } - // Submit to parent for processing - await handleCodingPlanSubmit(apiKey); + // Submit to parent for processing with region info + await handleCodingPlanSubmit(apiKey, region); }; const handleGoBack = () => { @@ -264,7 +277,11 @@ export function AuthDialog(): React.JSX.Element { // Render API key input for coding-plan mode const renderApiKeyInputView = () => ( - + ); diff --git a/packages/cli/src/ui/auth/useAuth.ts b/packages/cli/src/ui/auth/useAuth.ts index 0ea157af5..74cd2e8de 100644 --- a/packages/cli/src/ui/auth/useAuth.ts +++ b/packages/cli/src/ui/auth/useAuth.ts @@ -30,9 +30,9 @@ import { AuthState, MessageType } from '../types.js'; import type { HistoryItem } from '../types.js'; import { t } from '../../i18n/index.js'; import { - CODING_PLAN_MODELS, - CODING_PLAN_ENV_KEY, - CODING_PLAN_VERSION, + getCodingPlanConfig, + isCodingPlanConfig, + CodingPlanRegion, } from '../../constants/codingPlan.js'; export type { QwenAuthState } from '../hooks/useQwenAuth.js'; @@ -285,29 +285,36 @@ export const useAuthCommand = ( /** * Handle coding plan submission - generates configs from template and stores api-key + * @param apiKey - The API key to store + * @param region - The region to use (default: CHINA) */ const handleCodingPlanSubmit = useCallback( - async (apiKey: string) => { + async ( + apiKey: string, + region: CodingPlanRegion = CodingPlanRegion.CHINA, + ) => { try { setIsAuthenticating(true); setAuthError(null); - const envKeyName = CODING_PLAN_ENV_KEY; + // Get configuration based on region + const codingPlanConfig = getCodingPlanConfig(region); + const { template, envKey, version } = codingPlanConfig; // Get persist scope const persistScope = getPersistScopeForModelSelection(settings); // Store api-key in settings.env - settings.setValue(persistScope, `env.${envKeyName}`, apiKey); + settings.setValue(persistScope, `env.${envKey}`, apiKey); // Sync to process.env immediately so refreshAuth can read the apiKey - process.env[envKeyName] = apiKey; + process.env[envKey] = apiKey; // Generate model configs from template - const newConfigs: ProviderModelConfig[] = CODING_PLAN_MODELS.map( + const newConfigs: ProviderModelConfig[] = template.map( (templateConfig) => ({ ...templateConfig, - envKey: envKeyName, + envKey, }), ); @@ -317,17 +324,14 @@ export const useAuthCommand = ( settings.merged.modelProviders as ModelProvidersConfig | undefined )?.[AuthType.USE_OPENAI] || []; - // Identify Coding Plan configs by baseUrl + envKey + // Identify Coding Plan configs by baseUrl + envKey for the given region // Remove existing Coding Plan configs to ensure template changes are applied - const isCodingPlanConfig = (config: ProviderModelConfig) => - config.envKey === envKeyName && - CODING_PLAN_MODELS.some( - (template) => template.baseUrl === config.baseUrl, - ); + const checkIsCodingPlanConfig = (config: ProviderModelConfig) => + isCodingPlanConfig(config.baseUrl, config.envKey, region); - // Filter out existing Coding Plan configs, keep user custom configs + // Filter out existing Coding Plan configs for this region, keep user custom configs const nonCodingPlanConfigs = existingConfigs.filter( - (existing) => !isCodingPlanConfig(existing), + (existing) => !checkIsCodingPlanConfig(existing), ); // Add new Coding Plan configs at the beginning @@ -348,11 +352,12 @@ export const useAuthCommand = ( ); // Persist coding plan version for future update detection - settings.setValue( - persistScope, - 'codingPlan.version', - CODING_PLAN_VERSION, - ); + // Store version with region suffix to distinguish between China and Intl versions + const versionKey = + region === CodingPlanRegion.GLOBAL + ? 'codingPlan.versionIntl' + : 'codingPlan.version'; + settings.setValue(persistScope, versionKey, version); // If there are configs, use the first one as the model if (updatedConfigs.length > 0 && updatedConfigs[0]?.id) { @@ -382,11 +387,16 @@ export const useAuthCommand = ( onAuthChange?.(); // Add success message + const regionLabel = + region === CodingPlanRegion.GLOBAL + ? 'Coding Plan (Global/Intl)' + : 'Coding Plan'; addItem( { type: MessageType.INFO, text: t( - 'Authenticated successfully with Coding Plan. API key is stored in settings.env.', + 'Authenticated successfully with {{region}}. API key is stored in settings.env.', + { region: regionLabel }, ), }, Date.now(), diff --git a/packages/cli/src/ui/components/ApiKeyInput.tsx b/packages/cli/src/ui/components/ApiKeyInput.tsx index e4082be3a..a079e9956 100644 --- a/packages/cli/src/ui/components/ApiKeyInput.tsx +++ b/packages/cli/src/ui/components/ApiKeyInput.tsx @@ -11,23 +11,34 @@ import { TextInput } from './shared/TextInput.js'; import { theme } from '../semantic-colors.js'; import { useKeypress } from '../hooks/useKeypress.js'; import { t } from '../../i18n/index.js'; +import { CodingPlanRegion } from '../../constants/codingPlan.js'; import Link from 'ink-link'; interface ApiKeyInputProps { onSubmit: (apiKey: string) => void; onCancel: () => void; + region?: CodingPlanRegion; } const CODING_PLAN_API_KEY_URL = 'https://bailian.console.aliyun.com/?tab=model#/efm/coding_plan'; +const CODING_PLAN_INTL_API_KEY_URL = + 'https://modelstudio.console.alibabacloud.com/ap-southeast-1/?tab=globalset#/efm/api_key'; + export function ApiKeyInput({ onSubmit, onCancel, + region = CodingPlanRegion.CHINA, }: ApiKeyInputProps): React.JSX.Element { const [apiKey, setApiKey] = useState(''); const [error, setError] = useState(null); + const apiKeyUrl = + region === CodingPlanRegion.GLOBAL + ? CODING_PLAN_INTL_API_KEY_URL + : CODING_PLAN_API_KEY_URL; + useKeypress( (key) => { if (key.name === 'escape') { @@ -59,9 +70,9 @@ export function ApiKeyInput({ {t('You can get your exclusive Coding Plan API-KEY here:')} - + - {CODING_PLAN_API_KEY_URL} + {apiKeyUrl} diff --git a/packages/cli/src/ui/contexts/UIActionsContext.tsx b/packages/cli/src/ui/contexts/UIActionsContext.tsx index e4cb85003..7534b6d3a 100644 --- a/packages/cli/src/ui/contexts/UIActionsContext.tsx +++ b/packages/cli/src/ui/contexts/UIActionsContext.tsx @@ -15,6 +15,7 @@ import { type ApprovalMode, } from '@qwen-code/qwen-code-core'; import { type SettingScope } from '../../config/settings.js'; +import { type CodingPlanRegion } from '../../constants/codingPlan.js'; import type { AuthState } from '../types.js'; import { type VisionSwitchOutcome } from '../components/ModelSwitchDialog.js'; // OpenAICredentials type (previously imported from OpenAIKeyPrompt) @@ -40,7 +41,10 @@ export interface UIActions { authType: AuthType | undefined, credentials?: OpenAICredentials, ) => Promise; - handleCodingPlanSubmit: (apiKey: string) => Promise; + handleCodingPlanSubmit: ( + apiKey: string, + region?: CodingPlanRegion, + ) => Promise; setAuthState: (state: AuthState) => void; onAuthError: (error: string | null) => void; cancelAuthentication: () => void; diff --git a/packages/cli/src/ui/hooks/useCodingPlanUpdates.test.ts b/packages/cli/src/ui/hooks/useCodingPlanUpdates.test.ts index a004fbdcb..6a6a67ea6 100644 --- a/packages/cli/src/ui/hooks/useCodingPlanUpdates.test.ts +++ b/packages/cli/src/ui/hooks/useCodingPlanUpdates.test.ts @@ -7,34 +7,16 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { renderHook, waitFor } from '@testing-library/react'; import { useCodingPlanUpdates } from './useCodingPlanUpdates.js'; -import { CODING_PLAN_ENV_KEY } from '../../constants/codingPlan.js'; +import { + CODING_PLAN_ENV_KEY, + CODING_PLAN_INTL_ENV_KEY, + CODING_PLAN_BASE_URL, + CODING_PLAN_INTL_BASE_URL, + CODING_PLAN_VERSION, + CODING_PLAN_INTL_VERSION, +} from '../../constants/codingPlan.js'; import { AuthType } from '@qwen-code/qwen-code-core'; -// Mock the constants module -vi.mock('../../constants/codingPlan.js', async () => { - const actual = await vi.importActual('../../constants/codingPlan.js'); - return { - ...actual, - CODING_PLAN_VERSION: 'test-version-hash', - CODING_PLAN_MODELS: [ - { - id: 'test-model-1', - name: 'Test Model 1', - baseUrl: 'https://test.example.com/v1', - description: 'Test model 1', - envKey: 'BAILIAN_CODING_PLAN_API_KEY', - }, - { - id: 'test-model-2', - name: 'Test Model 2', - baseUrl: 'https://test.example.com/v1', - description: 'Test model 2', - envKey: 'BAILIAN_CODING_PLAN_API_KEY', - }, - ], - }; -}); - describe('useCodingPlanUpdates', () => { const mockSettings = { merged: { @@ -57,6 +39,7 @@ describe('useCodingPlanUpdates', () => { beforeEach(() => { vi.clearAllMocks(); delete process.env[CODING_PLAN_ENV_KEY]; + delete process.env[CODING_PLAN_INTL_ENV_KEY]; }); describe('version comparison', () => { @@ -74,8 +57,8 @@ describe('useCodingPlanUpdates', () => { expect(result.current.codingPlanUpdateRequest).toBeUndefined(); }); - it('should not show update prompt when versions match', () => { - mockSettings.merged.codingPlan = { version: 'test-version-hash' }; + it('should not show update prompt when China versions match', () => { + mockSettings.merged.codingPlan = { version: CODING_PLAN_VERSION }; const { result } = renderHook(() => useCodingPlanUpdates( @@ -88,7 +71,23 @@ describe('useCodingPlanUpdates', () => { expect(result.current.codingPlanUpdateRequest).toBeUndefined(); }); - it('should show update prompt when versions differ', async () => { + it('should not show update prompt when Global versions match', () => { + mockSettings.merged.codingPlan = { + versionIntl: CODING_PLAN_INTL_VERSION, + }; + + const { result } = renderHook(() => + useCodingPlanUpdates( + mockSettings as never, + mockConfig as never, + mockAddItem, + ), + ); + + expect(result.current.codingPlanUpdateRequest).toBeUndefined(); + }); + + it('should show update prompt when China versions differ', async () => { mockSettings.merged.codingPlan = { version: 'old-version-hash' }; const { result } = renderHook(() => @@ -103,21 +102,38 @@ describe('useCodingPlanUpdates', () => { expect(result.current.codingPlanUpdateRequest).toBeDefined(); }); + expect(result.current.codingPlanUpdateRequest?.prompt).toContain('China'); + }); + + it('should show update prompt when Global versions differ', async () => { + mockSettings.merged.codingPlan = { versionIntl: 'old-version-hash' }; + + const { result } = renderHook(() => + useCodingPlanUpdates( + mockSettings as never, + mockConfig as never, + mockAddItem, + ), + ); + + await waitFor(() => { + expect(result.current.codingPlanUpdateRequest).toBeDefined(); + }); + expect(result.current.codingPlanUpdateRequest?.prompt).toContain( - 'New model configurations', + 'Global', ); }); }); describe('update execution', () => { - it('should execute update when user confirms', async () => { - process.env[CODING_PLAN_ENV_KEY] = 'test-api-key'; + it('should execute China region update when user confirms', async () => { mockSettings.merged.codingPlan = { version: 'old-version-hash' }; mockSettings.merged.modelProviders = { [AuthType.USE_OPENAI]: [ { - id: 'test-model-1', - baseUrl: 'https://test.example.com/v1', + id: 'test-model-china-1', + baseUrl: CODING_PLAN_BASE_URL, envKey: CODING_PLAN_ENV_KEY, }, { @@ -150,22 +166,81 @@ describe('useCodingPlanUpdates', () => { expect(mockSettings.setValue).toHaveBeenCalled(); }); - // Should update version + // Should update version with correct hash expect(mockSettings.setValue).toHaveBeenCalledWith( expect.anything(), 'codingPlan.version', - 'test-version-hash', + CODING_PLAN_VERSION, ); // Should reload and refresh auth expect(mockConfig.reloadModelProvidersConfig).toHaveBeenCalled(); expect(mockConfig.refreshAuth).toHaveBeenCalledWith(AuthType.USE_OPENAI); - // Should show success message + // Should show success message with region info expect(mockAddItem).toHaveBeenCalledWith( expect.objectContaining({ type: 'info', - text: expect.stringContaining('updated successfully'), + text: expect.stringContaining('Coding Plan'), + }), + expect.any(Number), + ); + }); + + it('should execute Global region update when user confirms', async () => { + mockSettings.merged.codingPlan = { versionIntl: 'old-version-hash' }; + mockSettings.merged.modelProviders = { + [AuthType.USE_OPENAI]: [ + { + id: 'test-model-global-1', + baseUrl: CODING_PLAN_INTL_BASE_URL, + envKey: CODING_PLAN_INTL_ENV_KEY, + }, + { + id: 'custom-model', + baseUrl: 'https://custom.example.com', + envKey: 'CUSTOM_API_KEY', + }, + ], + }; + mockConfig.refreshAuth.mockResolvedValue(undefined); + + const { result } = renderHook(() => + useCodingPlanUpdates( + mockSettings as never, + mockConfig as never, + mockAddItem, + ), + ); + + await waitFor(() => { + expect(result.current.codingPlanUpdateRequest).toBeDefined(); + }); + + // Confirm the update + await result.current.codingPlanUpdateRequest!.onConfirm(true); + + // Wait for async update to complete + await waitFor(() => { + expect(mockSettings.setValue).toHaveBeenCalled(); + }); + + // Should update versionIntl with correct hash + expect(mockSettings.setValue).toHaveBeenCalledWith( + expect.anything(), + 'codingPlan.versionIntl', + CODING_PLAN_INTL_VERSION, + ); + + // Should reload and refresh auth + expect(mockConfig.reloadModelProvidersConfig).toHaveBeenCalled(); + expect(mockConfig.refreshAuth).toHaveBeenCalledWith(AuthType.USE_OPENAI); + + // Should show success message with Global region info + expect(mockAddItem).toHaveBeenCalledWith( + expect.objectContaining({ + type: 'info', + text: expect.stringContaining('Global'), }), expect.any(Number), ); @@ -194,8 +269,82 @@ describe('useCodingPlanUpdates', () => { expect(mockConfig.reloadModelProvidersConfig).not.toHaveBeenCalled(); }); + it('should only update configs for the specific region', async () => { + mockSettings.merged.codingPlan = { version: 'old-version-hash' }; + const chinaConfig = { + id: 'test-model-china-1', + baseUrl: CODING_PLAN_BASE_URL, + envKey: CODING_PLAN_ENV_KEY, + }; + const globalConfig = { + id: 'test-model-global-1', + baseUrl: CODING_PLAN_INTL_BASE_URL, + envKey: CODING_PLAN_INTL_ENV_KEY, + }; + const customConfig = { + id: 'custom-model', + baseUrl: 'https://custom.example.com', + envKey: 'CUSTOM_API_KEY', + }; + mockSettings.merged.modelProviders = { + [AuthType.USE_OPENAI]: [chinaConfig, globalConfig, customConfig], + }; + mockConfig.refreshAuth.mockResolvedValue(undefined); + + const { result } = renderHook(() => + useCodingPlanUpdates( + mockSettings as never, + mockConfig as never, + mockAddItem, + ), + ); + + await waitFor(() => { + expect(result.current.codingPlanUpdateRequest).toBeDefined(); + }); + + await result.current.codingPlanUpdateRequest!.onConfirm(true); + + // Wait for async update to complete + await waitFor(() => { + expect(mockSettings.setValue).toHaveBeenCalled(); + }); + + // Get the updated configs passed to setValue + const setValueCalls = mockSettings.setValue.mock.calls; + const modelProvidersCall = setValueCalls.find((call: unknown[]) => + (call[1] as string).includes('modelProviders'), + ); + + // Should preserve Global config and custom config, only update China configs + expect(modelProvidersCall).toBeDefined(); + const updatedConfigs = modelProvidersCall![2] as Array< + Record + >; + + // Should have new China configs + preserved Global config + custom config + expect(updatedConfigs.length).toBeGreaterThanOrEqual(3); + + // Should contain the Global config (not modified) + expect( + updatedConfigs.some( + (c: Record) => c['id'] === 'test-model-global-1', + ), + ).toBe(true); + + // Should contain the custom config + expect( + updatedConfigs.some( + (c: Record) => c['id'] === 'custom-model', + ), + ).toBe(true); + + // Should reload and refresh auth + expect(mockConfig.reloadModelProvidersConfig).toHaveBeenCalled(); + expect(mockConfig.refreshAuth).toHaveBeenCalledWith(AuthType.USE_OPENAI); + }); + it('should preserve non-Coding Plan configs during update', async () => { - process.env[CODING_PLAN_ENV_KEY] = 'test-api-key'; mockSettings.merged.codingPlan = { version: 'old-version-hash' }; const customConfig = { id: 'custom-model', @@ -205,8 +354,8 @@ describe('useCodingPlanUpdates', () => { mockSettings.merged.modelProviders = { [AuthType.USE_OPENAI]: [ { - id: 'test-model-1', - baseUrl: 'https://test.example.com/v1', + id: 'test-model-china-1', + baseUrl: CODING_PLAN_BASE_URL, envKey: CODING_PLAN_ENV_KEY, }, customConfig, @@ -233,10 +382,38 @@ describe('useCodingPlanUpdates', () => { // Should preserve custom config - verify setValue was called expect(mockSettings.setValue).toHaveBeenCalled(); }); + + // Get the updated configs passed to setValue + const setValueCalls = mockSettings.setValue.mock.calls; + const modelProvidersCall = setValueCalls.find((call: unknown[]) => + (call[1] as string).includes('modelProviders'), + ); + + // Should preserve custom config + expect(modelProvidersCall).toBeDefined(); + const updatedConfigs = modelProvidersCall![2] as Array< + Record + >; + expect( + updatedConfigs.some( + (c: Record) => c['id'] === 'custom-model', + ), + ).toBe(true); }); - it('should handle missing API key error', async () => { + it('should handle update errors gracefully', async () => { mockSettings.merged.codingPlan = { version: 'old-version-hash' }; + mockSettings.merged.modelProviders = { + [AuthType.USE_OPENAI]: [ + { + id: 'test-model-china-1', + baseUrl: CODING_PLAN_BASE_URL, + envKey: CODING_PLAN_ENV_KEY, + }, + ], + }; + // Simulate an error during refreshAuth + mockConfig.refreshAuth.mockRejectedValue(new Error('Network error')); const { result } = renderHook(() => useCodingPlanUpdates( @@ -253,12 +430,14 @@ describe('useCodingPlanUpdates', () => { await result.current.codingPlanUpdateRequest!.onConfirm(true); // Should show error message - expect(mockAddItem).toHaveBeenCalledWith( - expect.objectContaining({ - type: 'error', - }), - expect.any(Number), - ); + await waitFor(() => { + expect(mockAddItem).toHaveBeenCalledWith( + expect.objectContaining({ + type: 'error', + }), + expect.any(Number), + ); + }); }); }); diff --git a/packages/cli/src/ui/hooks/useCodingPlanUpdates.ts b/packages/cli/src/ui/hooks/useCodingPlanUpdates.ts index 85584def8..3d6e6da23 100644 --- a/packages/cli/src/ui/hooks/useCodingPlanUpdates.ts +++ b/packages/cli/src/ui/hooks/useCodingPlanUpdates.ts @@ -10,9 +10,11 @@ import { AuthType } from '@qwen-code/qwen-code-core'; import type { LoadedSettings } from '../../config/settings.js'; import { getPersistScopeForModelSelection } from '../../config/modelProvidersScope.js'; import { - CODING_PLAN_MODELS, - CODING_PLAN_ENV_KEY, + isCodingPlanConfig, CODING_PLAN_VERSION, + CODING_PLAN_INTL_VERSION, + getCodingPlanConfig, + CodingPlanRegion, } from '../../constants/codingPlan.js'; import { t } from '../../i18n/index.js'; @@ -21,20 +23,6 @@ export interface CodingPlanUpdateRequest { onConfirm: (confirmed: boolean) => void; } -/** - * Checks if a config is a Coding Plan configuration by matching baseUrl and envKey. - * This ensures only configs from the Coding Plan provider are identified. - */ -function isCodingPlanConfig(config: { - baseUrl?: string; - envKey?: string; -}): boolean { - return ( - config.envKey === CODING_PLAN_ENV_KEY && - CODING_PLAN_MODELS.some((template) => template.baseUrl === config.baseUrl) - ); -} - /** * Hook for detecting and handling Coding Plan template updates. * Compares the persisted version with the current template version @@ -55,134 +43,161 @@ export function useCodingPlanUpdates( /** * Execute the Coding Plan configuration update. * Removes old Coding Plan configs and replaces them with new ones from the template. + * Automatically detects whether the user is using China or Intl version. */ - const executeUpdate = useCallback(async () => { - try { - const persistScope = getPersistScopeForModelSelection(settings); + const executeUpdate = useCallback( + async (region: CodingPlanRegion = CodingPlanRegion.CHINA) => { + try { + const persistScope = getPersistScopeForModelSelection(settings); - // Get current configs - const currentConfigs = - ( - settings.merged.modelProviders as - | Record>> - | undefined - )?.[AuthType.USE_OPENAI] || []; + // Get current configs + const currentConfigs = + ( + settings.merged.modelProviders as + | Record>> + | undefined + )?.[AuthType.USE_OPENAI] || []; - // Filter out Coding Plan configs (keep user custom configs) - const nonCodingPlanConfigs = currentConfigs.filter( - (cfg) => - !isCodingPlanConfig({ - baseUrl: cfg['baseUrl'] as string | undefined, - envKey: cfg['envKey'] as string | undefined, - }), - ); - - // Generate new configs from template with the stored API key - const apiKey = process.env[CODING_PLAN_ENV_KEY]; - if (!apiKey) { - throw new Error( - t( - 'Coding Plan API key not found. Please re-authenticate with Coding Plan.', - ), + // Filter out Coding Plan configs for the given region (keep user custom configs) + const nonCodingPlanConfigs = currentConfigs.filter( + (cfg) => + !isCodingPlanConfig( + cfg['baseUrl'] as string | undefined, + cfg['envKey'] as string | undefined, + region, + ), ); + + // Get the correct configuration based on region + const codingPlanConfig = getCodingPlanConfig(region); + const { template, envKey, version } = codingPlanConfig; + + // Generate new configs from template + const newConfigs = template.map((templateConfig) => ({ + ...templateConfig, + envKey, + })); + + // Combine: new Coding Plan configs at the front, user configs preserved + const updatedConfigs = [ + ...newConfigs, + ...(nonCodingPlanConfigs as Array>), + ] as Array>; + + // Persist updated model providers + settings.setValue( + persistScope, + `modelProviders.${AuthType.USE_OPENAI}`, + updatedConfigs, + ); + + // Update the version with region-specific key + const versionKey = + region === CodingPlanRegion.GLOBAL + ? 'codingPlan.versionIntl' + : 'codingPlan.version'; + settings.setValue(persistScope, versionKey, version); + + // Hot-reload model providers configuration + const updatedModelProviders = { + ...(settings.merged.modelProviders as + | Record + | undefined), + [AuthType.USE_OPENAI]: updatedConfigs, + }; + config.reloadModelProvidersConfig( + updatedModelProviders as unknown as ModelProvidersConfig, + ); + + // Refresh auth with the new configuration + await config.refreshAuth(AuthType.USE_OPENAI); + + const regionLabel = + region === CodingPlanRegion.GLOBAL + ? 'Coding Plan (Global/Intl)' + : 'Coding Plan'; + addItem( + { + type: 'info', + text: t( + '{{region}} configuration updated successfully. New models are now available.', + { region: regionLabel }, + ), + }, + Date.now(), + ); + + return true; + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : String(error); + addItem( + { + type: 'error', + text: t('Failed to update Coding Plan configuration: {{message}}', { + message: errorMessage, + }), + }, + Date.now(), + ); + return false; } - - const newConfigs = CODING_PLAN_MODELS.map((templateConfig) => ({ - ...templateConfig, - envKey: CODING_PLAN_ENV_KEY, - })); - - // Combine: new Coding Plan configs at the front, user configs preserved - const updatedConfigs = [ - ...newConfigs, - ...(nonCodingPlanConfigs as Array>), - ] as Array>; - - // Persist updated model providers - settings.setValue( - persistScope, - `modelProviders.${AuthType.USE_OPENAI}`, - updatedConfigs, - ); - - // Update the version - settings.setValue( - persistScope, - 'codingPlan.version', - CODING_PLAN_VERSION, - ); - - // Hot-reload model providers configuration - const updatedModelProviders = { - ...(settings.merged.modelProviders as - | Record - | undefined), - [AuthType.USE_OPENAI]: updatedConfigs, - }; - config.reloadModelProvidersConfig( - updatedModelProviders as unknown as ModelProvidersConfig, - ); - - // Refresh auth with the new configuration - await config.refreshAuth(AuthType.USE_OPENAI); - - addItem( - { - type: 'info', - text: t( - 'Coding Plan configuration updated successfully. New models are now available.', - ), - }, - Date.now(), - ); - - return true; - } catch (error) { - const errorMessage = - error instanceof Error ? error.message : String(error); - addItem( - { - type: 'error', - text: t('Failed to update Coding Plan configuration: {{message}}', { - message: errorMessage, - }), - }, - Date.now(), - ); - return false; - } - }, [settings, config, addItem]); + }, + [settings, config, addItem], + ); /** * Check for version mismatch and prompt user for update if needed. */ const checkForUpdates = useCallback(() => { - const savedVersion = ( - settings.merged as { codingPlan?: { version?: string } } - ).codingPlan?.version; + const mergedSettings = settings.merged as { + codingPlan?: { version?: string; versionIntl?: string }; + }; + + const savedChinaVersion = mergedSettings.codingPlan?.version; + const savedIntlVersion = mergedSettings.codingPlan?.versionIntl; + + // Determine which version the user is using based on saved version + // Check China version first + if (savedChinaVersion) { + if (savedChinaVersion !== CODING_PLAN_VERSION) { + // China version mismatch - prompt for update + setUpdateRequest({ + prompt: t( + 'New model configurations are available for Bailian Coding Plan (China). Update now?', + ), + onConfirm: async (confirmed: boolean) => { + setUpdateRequest(undefined); + if (confirmed) { + await executeUpdate(CodingPlanRegion.CHINA); + } + }, + }); + return; + } + } + + // Check Intl version + if (savedIntlVersion) { + if (savedIntlVersion !== CODING_PLAN_INTL_VERSION) { + // Intl version mismatch - prompt for update + setUpdateRequest({ + prompt: t( + 'New model configurations are available for Coding Plan (Global/Intl). Update now?', + ), + onConfirm: async (confirmed: boolean) => { + setUpdateRequest(undefined); + if (confirmed) { + await executeUpdate(CodingPlanRegion.GLOBAL); + } + }, + }); + return; + } + } // If no version is stored, user hasn't used Coding Plan yet - skip check - if (!savedVersion) { - return; - } - - // If versions match, no update needed - if (savedVersion === CODING_PLAN_VERSION) { - return; - } - - // Version mismatch - prompt user for update - setUpdateRequest({ - prompt: t( - 'New model configurations are available for Bailian Coding Plan. Update now?', - ), - onConfirm: async (confirmed: boolean) => { - setUpdateRequest(undefined); - if (confirmed) { - await executeUpdate(); - } - }, - }); + return; }, [settings, executeUpdate]); // Check for updates on mount