feat(model): opt model provider doc url (#475)

This commit is contained in:
Haze
2026-03-13 18:14:54 +08:00
committed by GitHub
Unverified
parent 4485491913
commit f847982632
4 changed files with 96 additions and 21 deletions

View File

@@ -31,6 +31,7 @@ import {
} from '@/stores/providers';
import {
PROVIDER_TYPE_INFO,
getProviderDocsUrl,
type ProviderType,
getProviderIconUrl,
resolveProviderApiKeyForSave,
@@ -318,6 +319,7 @@ function ProviderCard({
const [saving, setSaving] = useState(false);
const typeInfo = PROVIDER_TYPE_INFO.find((t) => t.id === account.vendorId);
const providerDocsUrl = getProviderDocsUrl(typeInfo, i18n.language);
const showModelIdField = shouldShowProviderModelId(typeInfo, devModeUnlocked);
const canEditModelConfig = Boolean(typeInfo?.showBaseUrl || showModelIdField);
@@ -522,12 +524,10 @@ function ProviderCard({
{isEditing && (
<div className="space-y-6 mt-4 pt-4 border-t border-black/5 dark:border-white/5">
{account.vendorId === 'custom' && (
{providerDocsUrl && (
<div className="flex justify-end -mt-2 mb-2">
<a
href={i18n.language.startsWith('zh')
? 'https://icnnp7d0dymg.feishu.cn/wiki/BmiLwGBcEiloZDkdYnGc8RWnn6d#IWQCdfe5fobGU3xf3UGcgbLynGh'
: 'https://icnnp7d0dymg.feishu.cn/wiki/BmiLwGBcEiloZDkdYnGc8RWnn6d#Ee1ldfvKJoVGvfxc32mcILwenth'}
href={providerDocsUrl}
target="_blank"
rel="noopener noreferrer"
className="text-[12px] text-blue-500 hover:text-blue-600 font-medium inline-flex items-center gap-1"
@@ -799,6 +799,7 @@ function AddProviderDialog({
const [authMode, setAuthMode] = useState<'oauth' | 'apikey'>('apikey');
const typeInfo = PROVIDER_TYPE_INFO.find((t) => t.id === selectedType);
const providerDocsUrl = getProviderDocsUrl(typeInfo, i18n.language);
const showModelIdField = shouldShowProviderModelId(typeInfo, devModeUnlocked);
const isOAuth = typeInfo?.isOAuth ?? false;
const supportsApiKey = typeInfo?.supportsApiKey ?? false;
@@ -1092,13 +1093,11 @@ function AddProviderDialog({
>
{t('aiProviders.dialog.change')}
</button>
{selectedType === 'custom' && (
{providerDocsUrl && (
<>
<span className="mx-2 text-foreground/20">|</span>
<a
href={i18n.language.startsWith('zh')
? 'https://icnnp7d0dymg.feishu.cn/wiki/BmiLwGBcEiloZDkdYnGc8RWnn6d#IWQCdfe5fobGU3xf3UGcgbLynGh'
: 'https://icnnp7d0dymg.feishu.cn/wiki/BmiLwGBcEiloZDkdYnGc8RWnn6d#Ee1ldfvKJoVGvfxc32mcILwenth'}
href={providerDocsUrl}
target="_blank"
rel="noopener noreferrer"
className="text-[13px] text-blue-500 hover:text-blue-600 font-medium inline-flex items-center gap-1"

View File

@@ -73,6 +73,8 @@ export interface ProviderTypeInfo {
isOAuth?: boolean;
supportsApiKey?: boolean;
apiKeyUrl?: string;
docsUrl?: string;
docsUrlZh?: string;
}
export type ProviderAuthMode =
@@ -121,7 +123,15 @@ import { providerIcons } from '@/assets/providers';
/** All supported provider types with UI metadata */
export const PROVIDER_TYPE_INFO: ProviderTypeInfo[] = [
{ id: 'anthropic', name: 'Anthropic', icon: '🤖', placeholder: 'sk-ant-api03-...', model: 'Claude', requiresApiKey: true },
{
id: 'anthropic',
name: 'Anthropic',
icon: '🤖',
placeholder: 'sk-ant-api03-...',
model: 'Claude',
requiresApiKey: true,
docsUrl: 'https://platform.claude.com/docs/en/api/overview',
},
{
id: 'openai',
name: 'OpenAI',
@@ -145,15 +155,26 @@ export const PROVIDER_TYPE_INFO: ProviderTypeInfo[] = [
defaultModelId: 'gemini-3.1-pro-preview',
apiKeyUrl: 'https://aistudio.google.com/app/apikey',
},
{ id: 'openrouter', name: 'OpenRouter', icon: '🌐', placeholder: 'sk-or-v1-...', model: 'Multi-Model', requiresApiKey: true, showModelId: true, modelIdPlaceholder: 'anthropic/claude-opus-4.6', defaultModelId: 'anthropic/claude-opus-4.6' },
{ 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' },
{ id: 'moonshot', name: 'Moonshot (CN)', icon: '🌙', placeholder: 'sk-...', model: 'Kimi', requiresApiKey: true, defaultBaseUrl: 'https://api.moonshot.cn/v1', defaultModelId: 'kimi-k2.5' },
{ 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' },
{ 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: 'openrouter', name: 'OpenRouter', icon: '🌐', placeholder: 'sk-or-v1-...', model: 'Multi-Model', requiresApiKey: true, showModelId: true, modelIdPlaceholder: 'openai/gpt-5.4', defaultModelId: 'openai/gpt-5.4', docsUrl: 'https://openrouter.ai/models' },
{ id: 'minimax-portal-cn', name: 'MiniMax (CN)', icon: '☁️', placeholder: 'sk-...', model: 'MiniMax', requiresApiKey: false, isOAuth: true, supportsApiKey: true, defaultModelId: 'MiniMax-M2.5', apiKeyUrl: 'https://platform.minimaxi.com/' },
{ id: 'qwen-portal', name: 'Qwen', icon: '☁️', placeholder: 'sk-...', model: 'Qwen', requiresApiKey: false, isOAuth: true, defaultModelId: 'coder-model' },
{ id: 'moonshot', name: 'Moonshot (CN)', icon: '🌙', placeholder: 'sk-...', model: 'Kimi', requiresApiKey: true, defaultBaseUrl: 'https://api.moonshot.cn/v1', defaultModelId: 'kimi-k2.5', docsUrl: 'https://platform.moonshot.cn/' },
{ 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: 'ollama', name: 'Ollama', icon: '🦙', placeholder: 'Not required', requiresApiKey: false, defaultBaseUrl: 'http://localhost:11434/v1', showBaseUrl: true, showModelId: true, modelIdPlaceholder: 'qwen3:latest' },
{ id: 'custom', name: 'Custom', icon: '⚙️', placeholder: 'API key...', requiresApiKey: true, showBaseUrl: true, showModelId: true, modelIdPlaceholder: 'your-provider/model-id' },
{
id: 'custom',
name: 'Custom',
icon: '⚙️',
placeholder: 'API key...',
requiresApiKey: true,
showBaseUrl: true,
showModelId: true,
modelIdPlaceholder: 'your-provider/model-id',
docsUrl: 'https://icnnp7d0dymg.feishu.cn/wiki/BmiLwGBcEiloZDkdYnGc8RWnn6d#Ee1ldfvKJoVGvfxc32mcILwenth',
docsUrlZh: 'https://icnnp7d0dymg.feishu.cn/wiki/BmiLwGBcEiloZDkdYnGc8RWnn6d#IWQCdfe5fobGU3xf3UGcgbLynGh',
},
];
/** Get the SVG logo URL for a provider type, falls back to undefined */
@@ -174,6 +195,21 @@ export function getProviderTypeInfo(type: ProviderType): ProviderTypeInfo | unde
return PROVIDER_TYPE_INFO.find((t) => t.id === type);
}
export function getProviderDocsUrl(
provider: Pick<ProviderTypeInfo, 'docsUrl' | 'docsUrlZh'> | undefined,
language: string
): string | undefined {
if (!provider?.docsUrl) {
return undefined;
}
if (language.startsWith('zh') && provider.docsUrlZh) {
return provider.docsUrlZh;
}
return provider.docsUrl;
}
export function shouldShowProviderModelId(
provider: Pick<ProviderTypeInfo, 'showModelId' | 'showModelIdInDevModeOnly'> | undefined,
devModeUnlocked: boolean

View File

@@ -97,6 +97,7 @@ import {
type ProviderAccount,
type ProviderType,
type ProviderTypeInfo,
getProviderDocsUrl,
getProviderIconUrl,
resolveProviderApiKeyForSave,
resolveProviderModelForSave,
@@ -713,7 +714,7 @@ function ProviderContent({
onApiKeyChange,
onConfiguredChange,
}: ProviderContentProps) {
const { t } = useTranslation(['setup', 'settings']);
const { t, i18n } = useTranslation(['setup', 'settings']);
const devModeUnlocked = useSettingsStore((state) => state.devModeUnlocked);
const [showKey, setShowKey] = useState(false);
const [validating, setValidating] = useState(false);
@@ -975,6 +976,7 @@ function ProviderContent({
}, [providerMenuOpen]);
const selectedProviderData = providers.find((p) => p.id === selectedProvider);
const providerDocsUrl = getProviderDocsUrl(selectedProviderData, i18n.language);
const selectedProviderIconUrl = selectedProviderData
? getProviderIconUrl(selectedProviderData.id)
: undefined;
@@ -1139,7 +1141,20 @@ function ProviderContent({
<div className="space-y-6">
{/* Provider selector — dropdown */}
<div className="space-y-2">
<Label>{t('provider.label')}</Label>
<div className="flex items-center justify-between gap-3">
<Label>{t('provider.label')}</Label>
{selectedProvider && providerDocsUrl && (
<a
href={providerDocsUrl}
target="_blank"
rel="noopener noreferrer"
className="text-[13px] text-blue-500 hover:text-blue-600 font-medium inline-flex items-center gap-1"
>
{t('settings:aiProviders.dialog.customDoc')}
<ExternalLink className="h-3 w-3" />
</a>
)}
</div>
<div className="relative" ref={providerMenuRef}>
<button
type="button"