diff --git a/packages/cli/src/constants/codingPlan.ts b/packages/cli/src/constants/codingPlan.ts index 845f85d19..0a3084658 100644 --- a/packages/cli/src/constants/codingPlan.ts +++ b/packages/cli/src/constants/codingPlan.ts @@ -22,78 +22,11 @@ export enum CodingPlanRegion { export type CodingPlanTemplate = ModelConfig[]; /** - * Environment variable key for storing the coding plan API key (China/Bailian) + * Environment variable key for storing the coding plan API key. + * Unified key for both regions since they are mutually exclusive. */ export const CODING_PLAN_ENV_KEY = 'BAILIAN_CODING_PLAN_API_KEY'; -/** - * 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: CODING_PLAN_BASE_URL, - description: 'qwen3-coder-plus model from Bailian Coding Plan', - envKey: CODING_PLAN_ENV_KEY, - }, - { - id: 'qwen3-max-2026-01-23', - name: 'qwen3-max-2026-01-23', - description: - 'qwen3-max model with thinking enabled from Bailian Coding Plan', - baseUrl: CODING_PLAN_BASE_URL, - envKey: CODING_PLAN_ENV_KEY, - generationConfig: { - extra_body: { - enable_thinking: true, - }, - }, - }, -]; - -/** - * 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. @@ -106,81 +39,149 @@ export function computeCodingPlanVersion(template: CodingPlanTemplate): string { } /** - * Current version of the China/Bailian coding plan template. - * Computed at runtime from the template content. + * Generate the complete coding plan template for a specific region. + * China region uses legacy description to maintain backward compatibility. + * Global region uses new description with region indicator. + * @param region - The region to generate template for + * @returns Complete model configuration array for the region */ -export const CODING_PLAN_VERSION = computeCodingPlanVersion(CODING_PLAN_MODELS); +export function generateCodingPlanTemplate( + region: CodingPlanRegion, +): CodingPlanTemplate { + if (region === CodingPlanRegion.CHINA) { + // China region uses legacy fields to maintain backward compatibility + // This ensures existing users don't get prompted for unnecessary updates + return [ + { + id: 'qwen3-coder-plus', + name: 'qwen3-coder-plus', + baseUrl: 'https://coding.dashscope.aliyuncs.com/v1', + description: 'qwen3-coder-plus model from Bailian Coding Plan', + envKey: CODING_PLAN_ENV_KEY, + }, + { + id: 'qwen3-max-2026-01-23', + name: 'qwen3-max-2026-01-23', + description: + 'qwen3-max model with thinking enabled from Bailian Coding Plan', + baseUrl: 'https://coding.dashscope.aliyuncs.com/v1', + envKey: CODING_PLAN_ENV_KEY, + generationConfig: { + extra_body: { + enable_thinking: true, + }, + }, + }, + ]; + } + + // Global region uses new description with region indicator + return [ + { + id: 'qwen3-coder-plus', + name: 'qwen3-coder-plus', + baseUrl: 'https://coding-intl.dashscope.aliyuncs.com/v1', + description: 'qwen3-coder-plus model from Coding Plan (Global/Intl)', + envKey: CODING_PLAN_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: 'https://coding-intl.dashscope.aliyuncs.com/v1', + envKey: CODING_PLAN_ENV_KEY, + generationConfig: { + extra_body: { + enable_thinking: true, + }, + }, + }, + ]; +} /** - * Current version of the Global/Intl coding plan template. - * Computed at runtime from the template content. + * Get the complete configuration for a specific region. + * @param region - The region to use + * @returns Object containing template, baseUrl, and version */ -export const CODING_PLAN_INTL_VERSION = computeCodingPlanVersion( - CODING_PLAN_INTL_MODELS, -); +export function getCodingPlanConfig(region: CodingPlanRegion) { + const template = generateCodingPlanTemplate(region); + const baseUrl = + region === CodingPlanRegion.CHINA + ? 'https://coding.dashscope.aliyuncs.com/v1' + : 'https://coding-intl.dashscope.aliyuncs.com/v1'; + const regionName = + region === CodingPlanRegion.CHINA + ? 'Bailian Coding Plan (China)' + : 'Bailian Coding Plan (Global/Intl)'; + + return { + template, + baseUrl, + regionName, + version: computeCodingPlanVersion(template), + }; +} /** - * All coding plan templates for both regions. - * Used for update detection and filtering. + * Get all unique base URLs for coding plan (used for filtering/config detection). + * @returns Array of base URLs */ -export const ALL_CODING_PLAN_TEMPLATES: CodingPlanTemplate = [ - ...CODING_PLAN_MODELS, - ...CODING_PLAN_INTL_MODELS, -]; +export function getCodingPlanBaseUrls(): string[] { + return [ + 'https://coding.dashscope.aliyuncs.com/v1', + 'https://coding-intl.dashscope.aliyuncs.com/v1', + ]; +} /** - * Check if a config belongs to any Coding Plan template (China or Intl). + * Check if a config belongs to Coding Plan (any region). + * Returns the region if matched, or false if not a Coding Plan config. * @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 + * @returns The region if matched, false otherwise */ export function isCodingPlanConfig( baseUrl: string | undefined, envKey: string | undefined, - region?: CodingPlanRegion, -): boolean { +): CodingPlanRegion | false { 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, - ); + // Must use the unified envKey + if (envKey !== CODING_PLAN_ENV_KEY) { + return false; } - // No region specified, check all templates - return ALL_CODING_PLAN_TEMPLATES.some( - (template) => template.baseUrl === baseUrl && template.envKey === envKey, - ); + // Check which region's baseUrl matches + if (baseUrl === 'https://coding.dashscope.aliyuncs.com/v1') { + return CodingPlanRegion.CHINA; + } + if (baseUrl === 'https://coding-intl.dashscope.aliyuncs.com/v1') { + return CodingPlanRegion.GLOBAL; + } + + return false; } /** - * 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 + * Get region from baseUrl. + * @param baseUrl - The baseUrl to check + * @returns The region if matched, null otherwise */ -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, - }; +export function getRegionFromBaseUrl( + baseUrl: string | undefined, +): CodingPlanRegion | null { + if (!baseUrl) return null; + + if (baseUrl === 'https://coding.dashscope.aliyuncs.com/v1') { + return CodingPlanRegion.CHINA; } - return { - template: CODING_PLAN_MODELS, - envKey: CODING_PLAN_ENV_KEY, - version: CODING_PLAN_VERSION, - baseUrl: CODING_PLAN_BASE_URL, - }; + if (baseUrl === 'https://coding-intl.dashscope.aliyuncs.com/v1') { + return CodingPlanRegion.GLOBAL; + } + + return null; } diff --git a/packages/cli/src/i18n/locales/de.js b/packages/cli/src/i18n/locales/de.js index 291e14516..2a00a7b9e 100644 --- a/packages/cli/src/i18n/locales/de.js +++ b/packages/cli/src/i18n/locales/de.js @@ -1418,11 +1418,11 @@ export default { // ============================================================================ 'Coding Plan': 'Coding Plan', 'Coding Plan (Bailian, China)': 'Coding Plan (Bailian, China)', - 'Coding Plan (Bailian, Global/Intl)': 'Coding Plan (Bailian, Global/Intl)', + 'Bailian Coding Plan (Global/Intl)': 'Bailian Coding Plan (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!', + "Paste your api key of Bailian Coding Plan (Global/Intl) and you're all set!": + 'Fügen Sie Ihren Bailian 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`.', @@ -1438,11 +1438,10 @@ export default { // ============================================================================ '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?', + 'New model configurations are available for Bailian Coding Plan (Global/Intl). Update now?': + 'Neue Modellkonfigurationen sind für Bailian 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 a650d927f..234ff3ab8 100644 --- a/packages/cli/src/i18n/locales/en.js +++ b/packages/cli/src/i18n/locales/en.js @@ -1419,11 +1419,11 @@ export default { // ============================================================================ 'Coding Plan': 'Coding Plan', 'Coding Plan (Bailian, China)': 'Coding Plan (Bailian, China)', - 'Coding Plan (Bailian, Global/Intl)': 'Coding Plan (Bailian, Global/Intl)', + 'Bailian Coding Plan (Global/Intl)': 'Bailian Coding Plan (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!", + "Paste your api key of Bailian Coding Plan (Global/Intl) and you're all set!": + "Paste your api key of Bailian Coding Plan (Global/Intl) and you're all set!", Custom: 'Custom', 'More instructions about configuring `modelProviders` manually.': 'More instructions about configuring `modelProviders` manually.', @@ -1437,11 +1437,10 @@ export default { // ============================================================================ '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?', + 'New model configurations are available for Bailian Coding Plan (Global/Intl). Update now?': + 'New model configurations are available for Bailian 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 e20e33e55..e094dfb09 100644 --- a/packages/cli/src/i18n/locales/ja.js +++ b/packages/cli/src/i18n/locales/ja.js @@ -929,12 +929,11 @@ export default { // ============================================================================ 'Coding Plan': 'Coding Plan', 'Coding Plan (Bailian, China)': 'Coding Plan (Bailian, 中国)', - 'Coding Plan (Bailian, Global/Intl)': - 'Coding Plan (Bailian, グローバル/国際)', + 'Bailian Coding Plan (Global/Intl)': 'Bailian Coding Plan (グローバル/国際)', "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キーを貼り付けるだけで準備完了です!', + "Paste your api key of Bailian Coding Plan (Global/Intl) and you're all set!": + 'Bailian Coding Plan (グローバル/国際) のAPIキーを貼り付けるだけで準備完了です!', Custom: 'カスタム', 'More instructions about configuring `modelProviders` manually.': '`modelProviders`を手動で設定する方法の詳細はこちら。', @@ -949,11 +948,10 @@ export default { // ============================================================================ '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 (グローバル/国際) の新しいモデル設定が利用可能です。今すぐ更新しますか?', + 'New model configurations are available for Bailian Coding Plan (Global/Intl). Update now?': + 'Bailian 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 3519fadf2..1720a891a 100644 --- a/packages/cli/src/i18n/locales/pt.js +++ b/packages/cli/src/i18n/locales/pt.js @@ -1432,11 +1432,11 @@ export default { // ============================================================================ 'Coding Plan': 'Coding Plan', 'Coding Plan (Bailian, China)': 'Coding Plan (Bailian, China)', - 'Coding Plan (Bailian, Global/Intl)': 'Coding Plan (Bailian, Global/Intl)', + 'Bailian Coding Plan (Global/Intl)': 'Bailian Coding Plan (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!', + "Paste your api key of Bailian Coding Plan (Global/Intl) and you're all set!": + 'Cole sua chave de API do Bailian Coding Plan (Global/Intl) e pronto!', Custom: 'Personalizado', 'More instructions about configuring `modelProviders` manually.': 'Mais instruções sobre como configurar `modelProviders` manualmente.', @@ -1452,11 +1452,10 @@ export default { // ============================================================================ '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?', + 'New model configurations are available for Bailian Coding Plan (Global/Intl). Update now?': + 'Novas configurações de modelo estão disponíveis para o Bailian 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 6854cb1e9..49a5c7226 100644 --- a/packages/cli/src/i18n/locales/ru.js +++ b/packages/cli/src/i18n/locales/ru.js @@ -1422,12 +1422,12 @@ export default { // ============================================================================ 'Coding Plan': 'Coding Plan', 'Coding Plan (Bailian, China)': 'Coding Plan (Bailian, Китай)', - 'Coding Plan (Bailian, Global/Intl)': - 'Coding Plan (Bailian, Глобальный/Международный)', + 'Bailian Coding Plan (Global/Intl)': + 'Bailian Coding Plan (Глобальный/Международный)', "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 (Глобальный/Международный) и всё готово!', + "Paste your api key of Bailian Coding Plan (Global/Intl) and you're all set!": + 'Вставьте ваш API-ключ Bailian Coding Plan (Глобальный/Международный) и всё готово!', Custom: 'Пользовательский', 'More instructions about configuring `modelProviders` manually.': 'Дополнительные инструкции по ручной настройке `modelProviders`.', @@ -1442,11 +1442,10 @@ export default { // ============================================================================ '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 (Глобальный/Международный). Обновить сейчас?', + 'New model configurations are available for Bailian Coding Plan (Global/Intl). Update now?': + 'Доступны новые конфигурации моделей для Bailian 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 d8c434e4b..a1d6651ef 100644 --- a/packages/cli/src/i18n/locales/zh.js +++ b/packages/cli/src/i18n/locales/zh.js @@ -1254,11 +1254,11 @@ export default { 'API-KEY': 'API-KEY', 'Coding Plan': 'Coding Plan', 'Coding Plan (Bailian, China)': 'Coding Plan (百炼, 中国)', - 'Coding Plan (Bailian, Global/Intl)': 'Coding Plan (百炼, 全球/国际)', + 'Bailian Coding Plan (Global/Intl)': 'Bailian 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,即可完成设置!', + "Paste your api key of Bailian Coding Plan (Global/Intl) and you're all set!": + '粘贴您的 Bailian Coding Plan (全球/国际) API Key,即可完成设置!', Custom: '自定义', 'More instructions about configuring `modelProviders` manually.': '关于手动配置 `modelProviders` 的更多说明。', @@ -1271,11 +1271,10 @@ export default { // ============================================================================ '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 (全球/国际) 有新的模型配置可用。是否立即更新?', + 'New model configurations are available for Bailian Coding Plan (Global/Intl). Update now?': + 'Bailian 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 24263f13a..8c9a0aba7 100644 --- a/packages/cli/src/ui/auth/AuthDialog.tsx +++ b/packages/cli/src/ui/auth/AuthDialog.tsx @@ -80,7 +80,7 @@ export function AuthDialog(): React.JSX.Element { }, { key: 'coding-plan-intl', - label: t('Coding Plan (Bailian, Global/Intl)'), + label: t('Bailian Coding Plan (Global/Intl)'), value: 'coding-plan-intl' as ApiKeySubMode, }, { @@ -259,10 +259,12 @@ export function AuthDialog(): React.JSX.Element { - {apiKeySubItems[apiKeySubModeIndex]?.value === 'coding-plan' - ? t("Paste your api key of Bailian Coding Plan and you're all set!") - : t( + {apiKeySubItems[apiKeySubModeIndex]?.value === 'custom' + ? t( 'More instructions about configuring `modelProviders` manually.', + ) + : t( + "Paste your api key of Bailian Coding Plan and you're all set!", )} diff --git a/packages/cli/src/ui/auth/useAuth.ts b/packages/cli/src/ui/auth/useAuth.ts index 74cd2e8de..bb05172aa 100644 --- a/packages/cli/src/ui/auth/useAuth.ts +++ b/packages/cli/src/ui/auth/useAuth.ts @@ -33,6 +33,7 @@ import { getCodingPlanConfig, isCodingPlanConfig, CodingPlanRegion, + CODING_PLAN_ENV_KEY, } from '../../constants/codingPlan.js'; export type { QwenAuthState } from '../hooks/useQwenAuth.js'; @@ -298,23 +299,22 @@ export const useAuthCommand = ( setAuthError(null); // Get configuration based on region - const codingPlanConfig = getCodingPlanConfig(region); - const { template, envKey, version } = codingPlanConfig; + const { template, version, regionName } = getCodingPlanConfig(region); // Get persist scope const persistScope = getPersistScopeForModelSelection(settings); - // Store api-key in settings.env - settings.setValue(persistScope, `env.${envKey}`, apiKey); + // Store api-key in settings.env (unified env key) + settings.setValue(persistScope, `env.${CODING_PLAN_ENV_KEY}`, apiKey); // Sync to process.env immediately so refreshAuth can read the apiKey - process.env[envKey] = apiKey; + process.env[CODING_PLAN_ENV_KEY] = apiKey; // Generate model configs from template const newConfigs: ProviderModelConfig[] = template.map( (templateConfig) => ({ ...templateConfig, - envKey, + envKey: CODING_PLAN_ENV_KEY, }), ); @@ -324,14 +324,9 @@ export const useAuthCommand = ( settings.merged.modelProviders as ModelProvidersConfig | undefined )?.[AuthType.USE_OPENAI] || []; - // Identify Coding Plan configs by baseUrl + envKey for the given region - // Remove existing Coding Plan configs to ensure template changes are applied - const checkIsCodingPlanConfig = (config: ProviderModelConfig) => - isCodingPlanConfig(config.baseUrl, config.envKey, region); - - // Filter out existing Coding Plan configs for this region, keep user custom configs + // Filter out all existing Coding Plan configs (mutually exclusive) const nonCodingPlanConfigs = existingConfigs.filter( - (existing) => !checkIsCodingPlanConfig(existing), + (existing) => !isCodingPlanConfig(existing.baseUrl, existing.envKey), ); // Add new Coding Plan configs at the beginning @@ -351,13 +346,11 @@ export const useAuthCommand = ( AuthType.USE_OPENAI, ); - // Persist coding plan version for future update detection - // 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); + // Persist coding plan region + settings.setValue(persistScope, 'codingPlan.region', region); + + // Persist coding plan version (single field for backward compatibility) + settings.setValue(persistScope, 'codingPlan.version', version); // If there are configs, use the first one as the model if (updatedConfigs.length > 0 && updatedConfigs[0]?.id) { @@ -387,16 +380,12 @@ 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 {{region}}. API key is stored in settings.env.', - { region: regionLabel }, + { region: regionName }, ), }, Date.now(), diff --git a/packages/cli/src/ui/hooks/useCodingPlanUpdates.test.ts b/packages/cli/src/ui/hooks/useCodingPlanUpdates.test.ts index 6a6a67ea6..b2fe21e98 100644 --- a/packages/cli/src/ui/hooks/useCodingPlanUpdates.test.ts +++ b/packages/cli/src/ui/hooks/useCodingPlanUpdates.test.ts @@ -9,14 +9,15 @@ import { renderHook, waitFor } from '@testing-library/react'; import { useCodingPlanUpdates } from './useCodingPlanUpdates.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, + getCodingPlanConfig, + CodingPlanRegion, } from '../../constants/codingPlan.js'; import { AuthType } from '@qwen-code/qwen-code-core'; +// Get region configs for testing +const chinaConfig = getCodingPlanConfig(CodingPlanRegion.CHINA); +const globalConfig = getCodingPlanConfig(CodingPlanRegion.GLOBAL); + describe('useCodingPlanUpdates', () => { const mockSettings = { merged: { @@ -39,7 +40,6 @@ describe('useCodingPlanUpdates', () => { beforeEach(() => { vi.clearAllMocks(); delete process.env[CODING_PLAN_ENV_KEY]; - delete process.env[CODING_PLAN_INTL_ENV_KEY]; }); describe('version comparison', () => { @@ -57,23 +57,10 @@ describe('useCodingPlanUpdates', () => { expect(result.current.codingPlanUpdateRequest).toBeUndefined(); }); - it('should not show update prompt when China versions match', () => { - mockSettings.merged.codingPlan = { version: CODING_PLAN_VERSION }; - - const { result } = renderHook(() => - useCodingPlanUpdates( - mockSettings as never, - mockConfig as never, - mockAddItem, - ), - ); - - expect(result.current.codingPlanUpdateRequest).toBeUndefined(); - }); - - it('should not show update prompt when Global versions match', () => { + it('should not show update prompt when China region versions match', () => { mockSettings.merged.codingPlan = { - versionIntl: CODING_PLAN_INTL_VERSION, + region: CodingPlanRegion.CHINA, + version: chinaConfig.version, }; const { result } = renderHook(() => @@ -87,8 +74,28 @@ describe('useCodingPlanUpdates', () => { expect(result.current.codingPlanUpdateRequest).toBeUndefined(); }); - it('should show update prompt when China versions differ', async () => { - mockSettings.merged.codingPlan = { version: 'old-version-hash' }; + it('should not show update prompt when Global region versions match', () => { + mockSettings.merged.codingPlan = { + region: CodingPlanRegion.GLOBAL, + version: globalConfig.version, + }; + + const { result } = renderHook(() => + useCodingPlanUpdates( + mockSettings as never, + mockConfig as never, + mockAddItem, + ), + ); + + expect(result.current.codingPlanUpdateRequest).toBeUndefined(); + }); + + it('should default to China region when region is not specified', async () => { + // No region specified, should default to China + mockSettings.merged.codingPlan = { + version: 'old-version-hash', + }; const { result } = renderHook(() => useCodingPlanUpdates( @@ -102,11 +109,17 @@ describe('useCodingPlanUpdates', () => { expect(result.current.codingPlanUpdateRequest).toBeDefined(); }); - expect(result.current.codingPlanUpdateRequest?.prompt).toContain('China'); + // Should prompt for China region since it defaults to China + expect(result.current.codingPlanUpdateRequest?.prompt).toContain( + chinaConfig.regionName, + ); }); - it('should show update prompt when Global versions differ', async () => { - mockSettings.merged.codingPlan = { versionIntl: 'old-version-hash' }; + it('should show update prompt when China region versions differ', async () => { + mockSettings.merged.codingPlan = { + region: CodingPlanRegion.CHINA, + version: 'old-version-hash', + }; const { result } = renderHook(() => useCodingPlanUpdates( @@ -121,19 +134,45 @@ describe('useCodingPlanUpdates', () => { }); expect(result.current.codingPlanUpdateRequest?.prompt).toContain( - 'Global', + chinaConfig.regionName, + ); + }); + + it('should show update prompt when Global region versions differ', async () => { + mockSettings.merged.codingPlan = { + region: CodingPlanRegion.GLOBAL, + version: '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( + globalConfig.regionName, ); }); }); describe('update execution', () => { it('should execute China region update when user confirms', async () => { - mockSettings.merged.codingPlan = { version: 'old-version-hash' }; + mockSettings.merged.codingPlan = { + region: CodingPlanRegion.CHINA, + version: 'old-version-hash', + }; mockSettings.merged.modelProviders = { [AuthType.USE_OPENAI]: [ { id: 'test-model-china-1', - baseUrl: CODING_PLAN_BASE_URL, + baseUrl: chinaConfig.baseUrl, envKey: CODING_PLAN_ENV_KEY, }, { @@ -162,7 +201,7 @@ describe('useCodingPlanUpdates', () => { // Wait for async update to complete await waitFor(() => { - // Should update model providers (at least 2 calls: modelProviders + version) + // Should update model providers (at least 2 calls: modelProviders + version + region) expect(mockSettings.setValue).toHaveBeenCalled(); }); @@ -170,7 +209,14 @@ describe('useCodingPlanUpdates', () => { expect(mockSettings.setValue).toHaveBeenCalledWith( expect.anything(), 'codingPlan.version', - CODING_PLAN_VERSION, + chinaConfig.version, + ); + + // Should update region + expect(mockSettings.setValue).toHaveBeenCalledWith( + expect.anything(), + 'codingPlan.region', + CodingPlanRegion.CHINA, ); // Should reload and refresh auth @@ -181,20 +227,23 @@ describe('useCodingPlanUpdates', () => { expect(mockAddItem).toHaveBeenCalledWith( expect.objectContaining({ type: 'info', - text: expect.stringContaining('Coding Plan'), + text: expect.stringContaining(chinaConfig.regionName), }), expect.any(Number), ); }); it('should execute Global region update when user confirms', async () => { - mockSettings.merged.codingPlan = { versionIntl: 'old-version-hash' }; + mockSettings.merged.codingPlan = { + region: CodingPlanRegion.GLOBAL, + version: '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, + baseUrl: globalConfig.baseUrl, + envKey: CODING_PLAN_ENV_KEY, }, { id: 'custom-model', @@ -225,11 +274,18 @@ describe('useCodingPlanUpdates', () => { expect(mockSettings.setValue).toHaveBeenCalled(); }); - // Should update versionIntl with correct hash + // Should update version with correct hash (single version field) expect(mockSettings.setValue).toHaveBeenCalledWith( expect.anything(), - 'codingPlan.versionIntl', - CODING_PLAN_INTL_VERSION, + 'codingPlan.version', + globalConfig.version, + ); + + // Should update region + expect(mockSettings.setValue).toHaveBeenCalledWith( + expect.anything(), + 'codingPlan.region', + CodingPlanRegion.GLOBAL, ); // Should reload and refresh auth @@ -240,14 +296,17 @@ describe('useCodingPlanUpdates', () => { expect(mockAddItem).toHaveBeenCalledWith( expect.objectContaining({ type: 'info', - text: expect.stringContaining('Global'), + text: expect.stringContaining(globalConfig.regionName), }), expect.any(Number), ); }); it('should not execute update when user declines', async () => { - mockSettings.merged.codingPlan = { version: 'old-version-hash' }; + mockSettings.merged.codingPlan = { + region: CodingPlanRegion.CHINA, + version: 'old-version-hash', + }; const { result } = renderHook(() => useCodingPlanUpdates( @@ -269,17 +328,22 @@ 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 = { + it('should replace all Coding Plan configs during update (mutually exclusive)', async () => { + // Since regions are mutually exclusive, when updating one region, + // all Coding Plan configs should be replaced (not preserving other region configs) + mockSettings.merged.codingPlan = { + region: CodingPlanRegion.CHINA, + version: 'old-version-hash', + }; + const chinaModelConfig = { id: 'test-model-china-1', - baseUrl: CODING_PLAN_BASE_URL, + baseUrl: chinaConfig.baseUrl, envKey: CODING_PLAN_ENV_KEY, }; - const globalConfig = { + const globalModelConfig = { id: 'test-model-global-1', - baseUrl: CODING_PLAN_INTL_BASE_URL, - envKey: CODING_PLAN_INTL_ENV_KEY, + baseUrl: globalConfig.baseUrl, + envKey: CODING_PLAN_ENV_KEY, }; const customConfig = { id: 'custom-model', @@ -287,7 +351,11 @@ describe('useCodingPlanUpdates', () => { envKey: 'CUSTOM_API_KEY', }; mockSettings.merged.modelProviders = { - [AuthType.USE_OPENAI]: [chinaConfig, globalConfig, customConfig], + [AuthType.USE_OPENAI]: [ + chinaModelConfig, + globalModelConfig, + customConfig, + ], }; mockConfig.refreshAuth.mockResolvedValue(undefined); @@ -316,21 +384,21 @@ describe('useCodingPlanUpdates', () => { (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 have new China configs + custom config only (global config removed since regions are mutually exclusive) + // The template has 2 models, so we expect 2 (from template) + 1 (custom) = 3 + expect(updatedConfigs.length).toBe(3); - // Should contain the Global config (not modified) + // Should NOT contain the Global config (mutually exclusive) expect( updatedConfigs.some( - (c: Record) => c['id'] === 'test-model-global-1', + (c: Record) => c['baseUrl'] === globalConfig.baseUrl, ), - ).toBe(true); + ).toBe(false); // Should contain the custom config expect( @@ -339,13 +407,23 @@ describe('useCodingPlanUpdates', () => { ), ).toBe(true); + // All configs should use the unified env key + updatedConfigs.forEach((config) => { + if (config['envKey'] === CODING_PLAN_ENV_KEY) { + expect(config['baseUrl']).toBe(chinaConfig.baseUrl); + } + }); + // 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 () => { - mockSettings.merged.codingPlan = { version: 'old-version-hash' }; + mockSettings.merged.codingPlan = { + region: CodingPlanRegion.CHINA, + version: 'old-version-hash', + }; const customConfig = { id: 'custom-model', baseUrl: 'https://custom.example.com', @@ -355,7 +433,7 @@ describe('useCodingPlanUpdates', () => { [AuthType.USE_OPENAI]: [ { id: 'test-model-china-1', - baseUrl: CODING_PLAN_BASE_URL, + baseUrl: chinaConfig.baseUrl, envKey: CODING_PLAN_ENV_KEY, }, customConfig, @@ -402,12 +480,15 @@ describe('useCodingPlanUpdates', () => { }); it('should handle update errors gracefully', async () => { - mockSettings.merged.codingPlan = { version: 'old-version-hash' }; + mockSettings.merged.codingPlan = { + region: CodingPlanRegion.CHINA, + version: 'old-version-hash', + }; mockSettings.merged.modelProviders = { [AuthType.USE_OPENAI]: [ { id: 'test-model-china-1', - baseUrl: CODING_PLAN_BASE_URL, + baseUrl: chinaConfig.baseUrl, envKey: CODING_PLAN_ENV_KEY, }, ], @@ -443,7 +524,10 @@ describe('useCodingPlanUpdates', () => { describe('dismissUpdate', () => { it('should clear update request when dismissed', async () => { - mockSettings.merged.codingPlan = { version: 'old-version-hash' }; + mockSettings.merged.codingPlan = { + region: CodingPlanRegion.CHINA, + version: 'old-version-hash', + }; const { result } = renderHook(() => useCodingPlanUpdates( diff --git a/packages/cli/src/ui/hooks/useCodingPlanUpdates.ts b/packages/cli/src/ui/hooks/useCodingPlanUpdates.ts index 3d6e6da23..1646b5aef 100644 --- a/packages/cli/src/ui/hooks/useCodingPlanUpdates.ts +++ b/packages/cli/src/ui/hooks/useCodingPlanUpdates.ts @@ -11,10 +11,9 @@ import type { LoadedSettings } from '../../config/settings.js'; import { getPersistScopeForModelSelection } from '../../config/modelProvidersScope.js'; import { isCodingPlanConfig, - CODING_PLAN_VERSION, - CODING_PLAN_INTL_VERSION, getCodingPlanConfig, CodingPlanRegion, + CODING_PLAN_ENV_KEY, } from '../../constants/codingPlan.js'; import { t } from '../../i18n/index.js'; @@ -43,7 +42,7 @@ 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. + * Uses the region from settings.codingPlan.region (defaults to CHINA). */ const executeUpdate = useCallback( async (region: CodingPlanRegion = CodingPlanRegion.CHINA) => { @@ -58,24 +57,23 @@ export function useCodingPlanUpdates( | undefined )?.[AuthType.USE_OPENAI] || []; - // Filter out Coding Plan configs for the given region (keep user custom configs) + // Filter out all Coding Plan configs (since they are mutually exclusive) + // Keep only non-Coding-Plan 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; + // Get the configuration for the current region + const { template, version, regionName } = getCodingPlanConfig(region); // Generate new configs from template const newConfigs = template.map((templateConfig) => ({ ...templateConfig, - envKey, + envKey: CODING_PLAN_ENV_KEY, })); // Combine: new Coding Plan configs at the front, user configs preserved @@ -91,12 +89,11 @@ export function useCodingPlanUpdates( updatedConfigs, ); - // Update the version with region-specific key - const versionKey = - region === CodingPlanRegion.GLOBAL - ? 'codingPlan.versionIntl' - : 'codingPlan.version'; - settings.setValue(persistScope, versionKey, version); + // Update the version (single version field for backward compatibility) + settings.setValue(persistScope, 'codingPlan.version', version); + + // Update the region + settings.setValue(persistScope, 'codingPlan.region', region); // Hot-reload model providers configuration const updatedModelProviders = { @@ -112,16 +109,12 @@ export function useCodingPlanUpdates( // 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 }, + { region: regionName }, ), }, Date.now(), @@ -148,56 +141,46 @@ export function useCodingPlanUpdates( /** * Check for version mismatch and prompt user for update if needed. + * Uses the region from settings.codingPlan.region (defaults to CHINA if not set). */ const checkForUpdates = useCallback(() => { const mergedSettings = settings.merged as { - codingPlan?: { version?: string; versionIntl?: string }; + codingPlan?: { + version?: string; + region?: CodingPlanRegion; + }; }; - const savedChinaVersion = mergedSettings.codingPlan?.version; - const savedIntlVersion = mergedSettings.codingPlan?.versionIntl; + // Get the region (default to CHINA if not set) + const region = mergedSettings.codingPlan?.region ?? CodingPlanRegion.CHINA; - // 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; - } - } + // Get the saved version for the current region + const savedVersion = mergedSettings.codingPlan?.version; // If no version is stored, user hasn't used Coding Plan yet - skip check - return; + if (!savedVersion) { + return; + } + + // Get current version for the region + const currentVersion = getCodingPlanConfig(region).version; + + // Check if version matches + if (savedVersion !== currentVersion) { + const { regionName } = getCodingPlanConfig(region); + setUpdateRequest({ + prompt: t( + 'New model configurations are available for {{region}}. Update now?', + { region: regionName }, + ), + onConfirm: async (confirmed: boolean) => { + setUpdateRequest(undefined); + if (confirmed) { + await executeUpdate(region); + } + }, + }); + } }, [settings, executeUpdate]); // Check for updates on mount