feat: add new provider for minimax and qwen portals (#203)

Co-authored-by: Haze <709547807@qq.com>
This commit is contained in:
paisley
2026-02-27 14:59:37 +08:00
committed by GitHub
Unverified
parent 5d548da2e6
commit f70d5b0c28
12 changed files with 154 additions and 51 deletions

View File

@@ -167,6 +167,7 @@ function App() {
position="bottom-right"
richColors
closeButton
style={{ zIndex: 99999 }}
/>
</TooltipProvider>
</ErrorBoundary>

View File

@@ -17,6 +17,7 @@ export const providerIcons: Record<string, string> = {
moonshot,
siliconflow,
'minimax-portal': minimaxPortal,
'minimax-portal-cn': minimaxPortal,
'qwen-portal': qwenPortal,
ollama,
custom,

View File

@@ -487,20 +487,19 @@ function AddProviderDialog({ existingTypes, onClose, onAdd, onValidateKey }: Add
setOauthFlowing(false);
setOauthData(null);
setValidationError(null);
const { selectedType: type, typeInfo: info, onAdd: add, onClose: close, t: translate } = latestRef.current;
// Save the provider to the store so the list refreshes automatically
if (type && add) {
try {
await add(
type,
info?.name || type,
'', // OAuth providers don't use a plain API key
{ model: info?.defaultModelId }
);
} catch {
// provider may already exist; ignore duplicate errors
}
const { onClose: close, t: translate } = latestRef.current;
// device-oauth.ts already saved the provider config to the backend,
// including the dynamically resolved baseUrl for the region (e.g. CN vs Global).
// If we call add() here with undefined baseUrl, it will overwrite and erase it!
// So we just fetch the latest list from the backend to update the UI.
try {
await useProviderStore.getState().fetchProviders();
} catch (err) {
console.error('Failed to refresh providers after OAuth:', err);
}
close();
toast.success(translate('aiProviders.toast.added'));
};
@@ -525,12 +524,22 @@ function AddProviderDialog({ existingTypes, onClose, onAdd, onValidateKey }: Add
const handleStartOAuth = async () => {
if (!selectedType) return;
if (selectedType === 'minimax-portal' && existingTypes.has('minimax-portal-cn')) {
toast.error(t('aiProviders.toast.minimaxConflict'));
return;
}
if (selectedType === 'minimax-portal-cn' && existingTypes.has('minimax-portal')) {
toast.error(t('aiProviders.toast.minimaxConflict'));
return;
}
setOauthFlowing(true);
setOauthData(null);
setOauthError(null);
try {
await window.electron.ipcRenderer.invoke('provider:requestOAuth', selectedType, 'global');
await window.electron.ipcRenderer.invoke('provider:requestOAuth', selectedType);
} catch (e) {
setOauthError(String(e));
setOauthFlowing(false);
@@ -552,6 +561,15 @@ function AddProviderDialog({ existingTypes, onClose, onAdd, onValidateKey }: Add
const handleAdd = async () => {
if (!selectedType) return;
if (selectedType === 'minimax-portal' && existingTypes.has('minimax-portal-cn')) {
toast.error(t('aiProviders.toast.minimaxConflict'));
return;
}
if (selectedType === 'minimax-portal-cn' && existingTypes.has('minimax-portal')) {
toast.error(t('aiProviders.toast.minimaxConflict'));
return;
}
setSaving(true);
setValidationError(null);

View File

@@ -53,7 +53,8 @@
"updated": "Provider updated",
"failedUpdate": "Failed to update provider",
"invalidKey": "Invalid API key",
"modelRequired": "Model ID is required"
"modelRequired": "Model ID is required",
"minimaxConflict": "Cannot add both MiniMax (Global) and MiniMax (CN) providers."
},
"oauth": {
"loginMode": "OAuth Login",

View File

@@ -53,7 +53,8 @@
"updated": "提供商已更新",
"failedUpdate": "更新提供商失败",
"invalidKey": "无效的 API 密钥",
"modelRequired": "需要模型 ID"
"modelRequired": "需要模型 ID",
"minimaxConflict": "不能同时添加 MiniMax 国际站和国内站的服务商。"
},
"oauth": {
"loginMode": "OAuth 登录",

View File

@@ -13,6 +13,7 @@ export const PROVIDER_TYPES = [
'moonshot',
'siliconflow',
'minimax-portal',
'minimax-portal-cn',
'qwen-portal',
'ollama',
'custom',
@@ -69,7 +70,8 @@ export const PROVIDER_TYPE_INFO: ProviderTypeInfo[] = [
{ id: 'openrouter', name: 'OpenRouter', icon: '🌐', placeholder: 'sk-or-v1-...', model: 'Multi-Model', requiresApiKey: true },
{ 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', defaultModelId: 'Pro/moonshotai/Kimi-K2.5' },
{ id: 'minimax-portal', name: 'MiniMax', icon: '☁️', placeholder: 'sk-...', model: 'MiniMax', requiresApiKey: false, isOAuth: true, supportsApiKey: true, defaultModelId: 'MiniMax-M2.1' },
{ id: 'minimax-portal', name: 'MiniMax (Global)', icon: '☁️', placeholder: 'sk-...', model: 'MiniMax', requiresApiKey: false, isOAuth: true, supportsApiKey: true, defaultModelId: 'MiniMax-M2.1' },
{ id: 'minimax-portal-cn', name: 'MiniMax (CN)', icon: '☁️', placeholder: 'sk-...', model: 'MiniMax', requiresApiKey: false, isOAuth: true, supportsApiKey: true, defaultModelId: 'MiniMax-M2.1' },
{ id: 'qwen-portal', name: 'Qwen', icon: '☁️', placeholder: 'sk-...', model: 'Qwen', requiresApiKey: false, isOAuth: true, supportsApiKey: true, defaultModelId: 'coder-model' },
{ id: 'ollama', name: 'Ollama', icon: '🦙', placeholder: 'Not required', requiresApiKey: false, defaultBaseUrl: 'http://localhost:11434', 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' },

View File

@@ -773,14 +773,28 @@ function ProviderContent({
const handleStartOAuth = async () => {
if (!selectedProvider) return;
try {
const list = await window.electron.ipcRenderer.invoke('provider:list') as Array<{ type: string }>;
const existingTypes = new Set(list.map(l => l.type));
if (selectedProvider === 'minimax-portal' && existingTypes.has('minimax-portal-cn')) {
toast.error(t('settings:aiProviders.toast.minimaxConflict'));
return;
}
if (selectedProvider === 'minimax-portal-cn' && existingTypes.has('minimax-portal')) {
toast.error(t('settings:aiProviders.toast.minimaxConflict'));
return;
}
} catch {
// ignore check failure
}
setOauthFlowing(true);
setOauthData(null);
setOauthError(null);
// Default to global region for MiniMax in setup
const region = 'global';
try {
await window.electron.ipcRenderer.invoke('provider:requestOAuth', selectedProvider, region);
await window.electron.ipcRenderer.invoke('provider:requestOAuth', selectedProvider);
} catch (e) {
setOauthError(String(e));
setOauthFlowing(false);
@@ -905,6 +919,21 @@ function ProviderContent({
const handleValidateAndSave = async () => {
if (!selectedProvider) return;
try {
const list = await window.electron.ipcRenderer.invoke('provider:list') as Array<{ type: string }>;
const existingTypes = new Set(list.map(l => l.type));
if (selectedProvider === 'minimax-portal' && existingTypes.has('minimax-portal-cn')) {
toast.error(t('settings:aiProviders.toast.minimaxConflict'));
return;
}
if (selectedProvider === 'minimax-portal-cn' && existingTypes.has('minimax-portal')) {
toast.error(t('settings:aiProviders.toast.minimaxConflict'));
return;
}
} catch {
// ignore check failure
}
setValidating(true);
setKeyValid(null);