From d39982bad8384a6c04f05b68f428aa5be27fb50a Mon Sep 17 00:00:00 2001 From: Lingxuan Zuo Date: Sun, 22 Mar 2026 16:26:12 +0800 Subject: [PATCH] feat(ark): add Code Plan mode preset and guided setup (#617) --- electron/shared/providers/registry.ts | 3 + electron/shared/providers/types.ts | 3 + src/components/settings/ProvidersSettings.tsx | 171 +++++++++++++++++- src/i18n/locales/en/settings.json | 5 + src/i18n/locales/en/setup.json | 5 + src/i18n/locales/ja/settings.json | 5 + src/i18n/locales/ja/setup.json | 5 + src/i18n/locales/zh/settings.json | 5 + src/i18n/locales/zh/setup.json | 5 + src/lib/providers.ts | 5 +- src/pages/Setup/index.tsx | 94 +++++++++- tests/unit/providers.test.ts | 3 + 12 files changed, 299 insertions(+), 10 deletions(-) diff --git a/electron/shared/providers/registry.ts b/electron/shared/providers/registry.ts index 311b9d8f0..1a4a01f1e 100644 --- a/electron/shared/providers/registry.ts +++ b/electron/shared/providers/registry.ts @@ -95,6 +95,9 @@ export const PROVIDER_DEFINITIONS: ProviderDefinition[] = [ modelIdPlaceholder: 'ep-20260228000000-xxxxx', category: 'official', envVar: 'ARK_API_KEY', + codePlanPresetBaseUrl: 'https://ark.cn-beijing.volces.com/api/coding/v3', + codePlanPresetModelId: 'ark-code-latest', + codePlanDocsUrl: 'https://www.volcengine.com/docs/82379/1928261?lang=zh', supportedAuthModes: ['api_key'], defaultAuthMode: 'api_key', supportsMultipleAccounts: true, diff --git a/electron/shared/providers/types.ts b/electron/shared/providers/types.ts index e53fb1b23..018c944ae 100644 --- a/electron/shared/providers/types.ts +++ b/electron/shared/providers/types.ts @@ -84,6 +84,9 @@ export interface ProviderTypeInfo { isOAuth?: boolean; supportsApiKey?: boolean; apiKeyUrl?: string; + codePlanPresetBaseUrl?: string; + codePlanPresetModelId?: string; + codePlanDocsUrl?: string; } export interface ProviderModelEntry extends Record { diff --git a/src/components/settings/ProvidersSettings.tsx b/src/components/settings/ProvidersSettings.tsx index f812e5724..1ec0dbc0b 100644 --- a/src/components/settings/ProvidersSettings.tsx +++ b/src/components/settings/ProvidersSettings.tsx @@ -55,6 +55,7 @@ import { subscribeHostEvent } from '@/lib/host-events'; const inputClasses = 'h-[44px] rounded-xl font-mono text-[13px] bg-[#eeece3] dark:bg-muted border-black/10 dark:border-white/10 focus-visible:ring-2 focus-visible:ring-blue-500/50 focus-visible:border-blue-500 shadow-sm transition-all text-foreground placeholder:text-foreground/40'; const labelClasses = 'text-[14px] text-foreground/80 font-bold'; +type ArkMode = 'apikey' | 'codeplan'; function normalizeFallbackProviderIds(ids?: string[]): string[] { return Array.from(new Set((ids ?? []).filter(Boolean))); @@ -85,6 +86,17 @@ function fallbackModelsEqual(a?: string[], b?: string[]): boolean { return left.length === right.length && left.every((model, index) => model === right[index]); } +function isArkCodePlanMode( + vendorId: string, + baseUrl: string | undefined, + modelId: string | undefined, + codePlanPresetBaseUrl?: string, + codePlanPresetModelId?: string, +): boolean { + if (vendorId !== 'ark' || !codePlanPresetBaseUrl || !codePlanPresetModelId) return false; + return (baseUrl || '').trim() === codePlanPresetBaseUrl && (modelId || '').trim() === codePlanPresetModelId; +} + function getAuthModeLabel( authMode: ProviderAccount['authMode'], t: (key: string) => string @@ -317,10 +329,20 @@ function ProviderCard({ const [showFallback, setShowFallback] = useState(false); const [validating, setValidating] = useState(false); const [saving, setSaving] = useState(false); + const [arkMode, setArkMode] = useState('apikey'); const typeInfo = PROVIDER_TYPE_INFO.find((t) => t.id === account.vendorId); const providerDocsUrl = getProviderDocsUrl(typeInfo, i18n.language); const showModelIdField = shouldShowProviderModelId(typeInfo, devModeUnlocked); + const codePlanPreset = typeInfo?.codePlanPresetBaseUrl && typeInfo?.codePlanPresetModelId + ? { + baseUrl: typeInfo.codePlanPresetBaseUrl, + modelId: typeInfo.codePlanPresetModelId, + } + : null; + const effectiveDocsUrl = account.vendorId === 'ark' && arkMode === 'codeplan' + ? (typeInfo?.codePlanDocsUrl || providerDocsUrl) + : providerDocsUrl; const canEditModelConfig = Boolean(typeInfo?.showBaseUrl || showModelIdField); useEffect(() => { @@ -332,8 +354,17 @@ function ProviderCard({ setModelId(account.model || ''); setFallbackModelsText(normalizeFallbackModels(account.fallbackModels).join('\n')); setFallbackProviderIds(normalizeFallbackProviderIds(account.fallbackAccountIds)); + setArkMode( + isArkCodePlanMode( + account.vendorId, + account.baseUrl, + account.model, + typeInfo?.codePlanPresetBaseUrl, + typeInfo?.codePlanPresetModelId, + ) ? 'codeplan' : 'apikey' + ); } - }, [isEditing, account.baseUrl, account.fallbackModels, account.fallbackAccountIds, account.model, account.apiProtocol]); + }, [isEditing, account.baseUrl, account.fallbackModels, account.fallbackAccountIds, account.model, account.apiProtocol, account.vendorId, typeInfo?.codePlanPresetBaseUrl, typeInfo?.codePlanPresetModelId]); const fallbackOptions = allProviders.filter((candidate) => candidate.account.id !== account.id); @@ -524,10 +555,10 @@ function ProviderCard({ {isEditing && (
- {providerDocsUrl && ( + {effectiveDocsUrl && (
)} + {account.vendorId === 'ark' && codePlanPreset && ( +
+ +
+ + +
+ {arkMode === 'codeplan' && ( +

+ {t('aiProviders.dialog.codePlanPresetDesc')} +

+ )} +
+ )} {account.vendorId === 'custom' && (
@@ -776,6 +856,7 @@ function AddProviderDialog({ const [baseUrl, setBaseUrl] = useState(''); const [modelId, setModelId] = useState(''); const [apiProtocol, setApiProtocol] = useState('openai-completions'); + const [arkMode, setArkMode] = useState('apikey'); const [showKey, setShowKey] = useState(false); const [saving, setSaving] = useState(false); const [validationError, setValidationError] = useState(null); @@ -801,6 +882,15 @@ function AddProviderDialog({ const typeInfo = PROVIDER_TYPE_INFO.find((t) => t.id === selectedType); const providerDocsUrl = getProviderDocsUrl(typeInfo, i18n.language); const showModelIdField = shouldShowProviderModelId(typeInfo, devModeUnlocked); + const codePlanPreset = typeInfo?.codePlanPresetBaseUrl && typeInfo?.codePlanPresetModelId + ? { + baseUrl: typeInfo.codePlanPresetBaseUrl, + modelId: typeInfo.codePlanPresetModelId, + } + : null; + const effectiveDocsUrl = selectedType === 'ark' && arkMode === 'codeplan' + ? (typeInfo?.codePlanDocsUrl || providerDocsUrl) + : providerDocsUrl; const isOAuth = typeInfo?.isOAuth ?? false; const supportsApiKey = typeInfo?.supportsApiKey ?? false; const vendorMap = new Map(vendors.map((vendor) => [vendor.id, vendor])); @@ -820,6 +910,23 @@ function AddProviderDialog({ setAuthMode(selectedVendor.defaultAuthMode === 'api_key' ? 'apikey' : 'oauth'); }, [selectedVendor, isOAuth, supportsApiKey]); + useEffect(() => { + if (selectedType !== 'ark') { + setArkMode('apikey'); + return; + } + setArkMode( + isArkCodePlanMode( + 'ark', + baseUrl, + modelId, + typeInfo?.codePlanPresetBaseUrl, + typeInfo?.codePlanPresetModelId, + ) ? 'codeplan' : 'apikey' + ); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [selectedType]); + // Keep refs to the latest values so event handlers see the current dialog state. const latestRef = React.useRef({ selectedType, typeInfo, onAdd, onClose, t }); const pendingOAuthRef = React.useRef<{ accountId: string; label: string } | null>(null); @@ -1056,6 +1163,7 @@ function AddProviderDialog({ setName(type.id === 'custom' ? t('aiProviders.custom') : type.name); setBaseUrl(type.defaultBaseUrl || ''); setModelId(type.defaultModelId || ''); + setArkMode('apikey'); }} className="p-4 rounded-2xl border border-black/5 dark:border-white/5 hover:bg-black/5 dark:hover:bg-white/5 transition-colors text-center group" > @@ -1088,16 +1196,17 @@ function AddProviderDialog({ setValidationError(null); setBaseUrl(''); setModelId(''); + setArkMode('apikey'); }} className="text-[13px] text-blue-500 hover:text-blue-600 font-medium" > {t('aiProviders.dialog.change')} - {providerDocsUrl && ( + {effectiveDocsUrl && ( <> |
)} + {selectedType === 'ark' && codePlanPreset && ( +
+ +
+ + +
+ {arkMode === 'codeplan' && ( +

+ {t('aiProviders.dialog.codePlanPresetDesc')} +

+ )} +
+ )} {selectedType === 'custom' && (
diff --git a/src/i18n/locales/en/settings.json b/src/i18n/locales/en/settings.json index d6bf22911..0c3453610 100644 --- a/src/i18n/locales/en/settings.json +++ b/src/i18n/locales/en/settings.json @@ -52,6 +52,11 @@ "replaceApiKeyHelp": "Leave this field empty if you want to keep the currently stored API key.", "baseUrl": "Base URL", "modelId": "Model ID", + "codePlanPreset": "Code Plan Preset", + "codePlanMode": "Code Plan", + "useCodePlanPreset": "Use Ark Code Plan Preset", + "codePlanPresetDesc": "Code Plan uses https://ark.cn-beijing.volces.com/api/coding/v3 and model ark-code-latest. Do not use /api/v3 for Code Plan traffic.", + "codePlanDoc": "Code Plan docs", "protocol": "Protocol", "fallbackModels": "Fallback Models", "fallbackProviders": "Fallback Providers", diff --git a/src/i18n/locales/en/setup.json b/src/i18n/locales/en/setup.json index d42a003c4..ed674c299 100644 --- a/src/i18n/locales/en/setup.json +++ b/src/i18n/locales/en/setup.json @@ -67,6 +67,11 @@ "baseUrl": "Base URL", "modelId": "Model ID", "modelIdDesc": "The model identifier from your provider (e.g. deepseek-ai/DeepSeek-V3)", + "codePlanPreset": "Code Plan Preset", + "codePlanMode": "Code Plan", + "useCodePlanPreset": "Use Ark Code Plan Preset", + "codePlanPresetDesc": "Code Plan uses https://ark.cn-beijing.volces.com/api/coding/v3 and model ark-code-latest. Do not use /api/v3 for Code Plan traffic.", + "codePlanDoc": "Code Plan docs", "protocol": "Protocol", "protocols": { "openaiCompletions": "OpenAI Completions", diff --git a/src/i18n/locales/ja/settings.json b/src/i18n/locales/ja/settings.json index cdf4623f2..015e693b7 100644 --- a/src/i18n/locales/ja/settings.json +++ b/src/i18n/locales/ja/settings.json @@ -52,6 +52,11 @@ "replaceApiKeyHelp": "現在保存されている API キーをそのまま使う場合は、この欄を空のままにしてください。", "baseUrl": "ベース URL", "modelId": "モデル ID", + "codePlanPreset": "Code Plan プリセット", + "codePlanMode": "Code Plan", + "useCodePlanPreset": "Ark Code Plan プリセットを使用", + "codePlanPresetDesc": "Code Plan は https://ark.cn-beijing.volces.com/api/coding/v3 と model ark-code-latest を使います。Code Plan 通信に /api/v3 を使わないでください。", + "codePlanDoc": "Code Plan ドキュメント", "protocol": "プロトコル", "fallbackModels": "フォールバックモデル", "fallbackProviders": "別プロバイダーへのフォールバック", diff --git a/src/i18n/locales/ja/setup.json b/src/i18n/locales/ja/setup.json index 2a597b44c..1c5c119e0 100644 --- a/src/i18n/locales/ja/setup.json +++ b/src/i18n/locales/ja/setup.json @@ -67,6 +67,11 @@ "baseUrl": "ベース URL", "modelId": "モデル ID", "modelIdDesc": "プロバイダーのモデル識別子(例:deepseek-ai/DeepSeek-V3)", + "codePlanPreset": "Code Plan プリセット", + "codePlanMode": "Code Plan", + "useCodePlanPreset": "Ark Code Plan プリセットを使用", + "codePlanPresetDesc": "Code Plan は https://ark.cn-beijing.volces.com/api/coding/v3 と model ark-code-latest を使います。Code Plan 通信に /api/v3 を使わないでください。", + "codePlanDoc": "Code Plan ドキュメント", "protocol": "プロトコル", "protocols": { "openaiCompletions": "OpenAI Completions", diff --git a/src/i18n/locales/zh/settings.json b/src/i18n/locales/zh/settings.json index 475ea5461..522e98b24 100644 --- a/src/i18n/locales/zh/settings.json +++ b/src/i18n/locales/zh/settings.json @@ -52,6 +52,11 @@ "replaceApiKeyHelp": "如果想保留当前已保存的 API key,这里留空即可。", "baseUrl": "基础 URL", "modelId": "模型 ID", + "codePlanPreset": "Code Plan 预设", + "codePlanMode": "Code Plan", + "useCodePlanPreset": "使用 Ark Code Plan 预设", + "codePlanPresetDesc": "Code Plan 使用 https://ark.cn-beijing.volces.com/api/coding/v3 与模型 ark-code-latest。请勿把 /api/v3 用于 Code Plan 流量。", + "codePlanDoc": "Code Plan 文档", "protocol": "协议", "fallbackModels": "回退模型", "fallbackProviders": "跨 Provider 回退", diff --git a/src/i18n/locales/zh/setup.json b/src/i18n/locales/zh/setup.json index 039f39fad..8d00945be 100644 --- a/src/i18n/locales/zh/setup.json +++ b/src/i18n/locales/zh/setup.json @@ -67,6 +67,11 @@ "baseUrl": "基础 URL", "modelId": "模型 ID", "modelIdDesc": "提供商的模型标识符(例如 deepseek-ai/DeepSeek-V3)", + "codePlanPreset": "Code Plan 预设", + "codePlanMode": "Code Plan", + "useCodePlanPreset": "使用 Ark Code Plan 预设", + "codePlanPresetDesc": "Code Plan 使用 https://ark.cn-beijing.volces.com/api/coding/v3 与模型 ark-code-latest。请勿把 /api/v3 用于 Code Plan 流量。", + "codePlanDoc": "Code Plan 文档", "protocol": "协议", "protocols": { "openaiCompletions": "OpenAI Completions", diff --git a/src/lib/providers.ts b/src/lib/providers.ts index 6bb8de1ec..2372b373f 100644 --- a/src/lib/providers.ts +++ b/src/lib/providers.ts @@ -75,6 +75,9 @@ export interface ProviderTypeInfo { apiKeyUrl?: string; docsUrl?: string; docsUrlZh?: string; + codePlanPresetBaseUrl?: string; + codePlanPresetModelId?: string; + codePlanDocsUrl?: string; } export type ProviderAuthMode = @@ -161,7 +164,7 @@ export const PROVIDER_TYPE_INFO: ProviderTypeInfo[] = [ { id: 'siliconflow', name: 'SiliconFlow (CN)', icon: '🌊', placeholder: 'sk-...', model: 'Multi-Model', requiresApiKey: true, defaultBaseUrl: 'https://api.siliconflow.cn/v1', showModelId: true, modelIdPlaceholder: 'deepseek-ai/DeepSeek-V3', defaultModelId: 'deepseek-ai/DeepSeek-V3', docsUrl: 'https://docs.siliconflow.cn/cn/userguide/introduction' }, { id: 'minimax-portal', name: 'MiniMax (Global)', icon: '☁️', placeholder: 'sk-...', model: 'MiniMax', requiresApiKey: false, isOAuth: true, supportsApiKey: true, defaultModelId: 'MiniMax-M2.5', apiKeyUrl: 'https://intl.minimaxi.com/' }, { id: 'qwen-portal', name: 'Qwen (Global)', icon: '☁️', placeholder: 'sk-...', model: 'Qwen', requiresApiKey: false, isOAuth: true, defaultModelId: 'coder-model' }, - { id: 'ark', name: 'ByteDance Ark', icon: 'A', placeholder: 'your-ark-api-key', model: 'Doubao', requiresApiKey: true, defaultBaseUrl: 'https://ark.cn-beijing.volces.com/api/v3', showBaseUrl: true, showModelId: true, modelIdPlaceholder: 'ep-20260228000000-xxxxx', docsUrl: 'https://www.volcengine.com/' }, + { id: 'ark', name: 'ByteDance Ark', icon: 'A', placeholder: 'your-ark-api-key', model: 'Doubao', requiresApiKey: true, defaultBaseUrl: 'https://ark.cn-beijing.volces.com/api/v3', showBaseUrl: true, showModelId: true, modelIdPlaceholder: 'ep-20260228000000-xxxxx', docsUrl: 'https://www.volcengine.com/', codePlanPresetBaseUrl: 'https://ark.cn-beijing.volces.com/api/coding/v3', codePlanPresetModelId: 'ark-code-latest', codePlanDocsUrl: 'https://www.volcengine.com/docs/82379/1928261?lang=zh' }, { id: 'ollama', name: 'Ollama', icon: '🦙', placeholder: 'Not required', requiresApiKey: false, defaultBaseUrl: 'http://localhost:11434/v1', showBaseUrl: true, showModelId: true, modelIdPlaceholder: 'qwen3:latest' }, { id: 'custom', diff --git a/src/pages/Setup/index.tsx b/src/pages/Setup/index.tsx index bd5c08181..9e89976b7 100644 --- a/src/pages/Setup/index.tsx +++ b/src/pages/Setup/index.tsx @@ -727,6 +727,7 @@ function ProviderContent({ const providerMenuRef = useRef(null); const [authMode, setAuthMode] = useState<'oauth' | 'apikey'>('oauth'); + const [arkMode, setArkMode] = useState<'apikey' | 'codeplan'>('apikey'); // OAuth Flow State const [oauthFlowing, setOauthFlowing] = useState(false); @@ -939,9 +940,22 @@ function ProviderContent({ onApiKeyChange(storedKey || ''); const info = providers.find((p) => p.id === selectedProvider); - setBaseUrl(savedProvider?.baseUrl || info?.defaultBaseUrl || ''); - setModelId(savedProvider?.model || info?.defaultModelId || ''); + const nextBaseUrl = savedProvider?.baseUrl || info?.defaultBaseUrl || ''; + const nextModelId = savedProvider?.model || info?.defaultModelId || ''; + setBaseUrl(nextBaseUrl); + setModelId(nextModelId); setApiProtocol(savedProvider?.apiProtocol || 'openai-completions'); + if ( + selectedProvider === 'ark' + && info?.codePlanPresetBaseUrl + && info?.codePlanPresetModelId + && nextBaseUrl.trim() === info.codePlanPresetBaseUrl + && nextModelId.trim() === info.codePlanPresetModelId + ) { + setArkMode('codeplan'); + } else { + setArkMode('apikey'); + } } } catch (error) { if (!cancelled) { @@ -977,11 +991,20 @@ function ProviderContent({ const selectedProviderData = providers.find((p) => p.id === selectedProvider); const providerDocsUrl = getProviderDocsUrl(selectedProviderData, i18n.language); + const effectiveProviderDocsUrl = selectedProvider === 'ark' && arkMode === 'codeplan' + ? (selectedProviderData?.codePlanDocsUrl || providerDocsUrl) + : providerDocsUrl; const selectedProviderIconUrl = selectedProviderData ? getProviderIconUrl(selectedProviderData.id) : undefined; const showBaseUrlField = selectedProviderData?.showBaseUrl ?? false; const showModelIdField = shouldShowProviderModelId(selectedProviderData, devModeUnlocked); + const codePlanPreset = selectedProviderData?.codePlanPresetBaseUrl && selectedProviderData?.codePlanPresetModelId + ? { + baseUrl: selectedProviderData.codePlanPresetBaseUrl, + modelId: selectedProviderData.codePlanPresetModelId, + } + : null; const requiresKey = selectedProviderData?.requiresApiKey ?? false; const isOAuth = selectedProviderData?.isOAuth ?? false; const supportsApiKey = selectedProviderData?.supportsApiKey ?? false; @@ -1135,6 +1158,7 @@ function ProviderContent({ setKeyValid(null); setProviderMenuOpen(false); setAuthMode('oauth'); + setArkMode('apikey'); }; return ( @@ -1143,9 +1167,9 @@ function ProviderContent({
- {selectedProvider && providerDocsUrl && ( + {selectedProvider && effectiveProviderDocsUrl && ( + {codePlanPreset && ( +
+ +
+ + +
+ {arkMode === 'codeplan' && ( +

+ {t('provider.codePlanPresetDesc')} +

+ )} +
+ )} + {/* Base URL field (for siliconflow, ollama, custom) */} {showBaseUrlField && (
diff --git a/tests/unit/providers.test.ts b/tests/unit/providers.test.ts index 124fe3cf5..f9434e33d 100644 --- a/tests/unit/providers.test.ts +++ b/tests/unit/providers.test.ts @@ -27,6 +27,9 @@ describe('provider metadata', () => { defaultBaseUrl: 'https://ark.cn-beijing.volces.com/api/v3', showBaseUrl: true, showModelId: true, + codePlanPresetBaseUrl: 'https://ark.cn-beijing.volces.com/api/coding/v3', + codePlanPresetModelId: 'ark-code-latest', + codePlanDocsUrl: 'https://www.volcengine.com/docs/82379/1928261?lang=zh', }), ]) );