feat: add new provider for minimax and qwen portals (#203)
Co-authored-by: Haze <709547807@qq.com>
This commit is contained in:
committed by
GitHub
Unverified
parent
5d548da2e6
commit
f70d5b0c28
@@ -59,12 +59,14 @@ import { deviceOAuthManager, OAuthProviderType } from '../utils/device-oauth';
|
|||||||
* @param providerId - Unique provider ID from secure-storage (UUID-like)
|
* @param providerId - Unique provider ID from secure-storage (UUID-like)
|
||||||
* @returns A string like 'custom-a1b2c3d4' or 'openrouter'
|
* @returns A string like 'custom-a1b2c3d4' or 'openrouter'
|
||||||
*/
|
*/
|
||||||
function getOpenClawProviderKey(type: string, providerId: string): string {
|
export function getOpenClawProviderKey(type: string, providerId: string): string {
|
||||||
if (type === 'custom' || type === 'ollama') {
|
if (type === 'custom' || type === 'ollama') {
|
||||||
// Use the first 8 chars of the providerId as a stable short suffix
|
|
||||||
const suffix = providerId.replace(/-/g, '').slice(0, 8);
|
const suffix = providerId.replace(/-/g, '').slice(0, 8);
|
||||||
return `${type}-${suffix}`;
|
return `${type}-${suffix}`;
|
||||||
}
|
}
|
||||||
|
if (type === 'minimax-portal-cn') {
|
||||||
|
return 'minimax-portal';
|
||||||
|
}
|
||||||
return type;
|
return type;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -834,6 +836,14 @@ function registerDeviceOAuthHandlers(mainWindow: BrowserWindow): void {
|
|||||||
* Provider-related IPC handlers
|
* Provider-related IPC handlers
|
||||||
*/
|
*/
|
||||||
function registerProviderHandlers(gatewayManager: GatewayManager): void {
|
function registerProviderHandlers(gatewayManager: GatewayManager): void {
|
||||||
|
// Listen for OAuth success to automatically restart the Gateway with new tokens/configs
|
||||||
|
deviceOAuthManager.on('oauth:success', (providerType) => {
|
||||||
|
logger.info(`[IPC] Restarting Gateway after ${providerType} OAuth success...`);
|
||||||
|
void gatewayManager.restart().catch(err => {
|
||||||
|
logger.error('Failed to restart Gateway after OAuth success:', err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// Get all providers with key info
|
// Get all providers with key info
|
||||||
ipcMain.handle('provider:list', async () => {
|
ipcMain.handle('provider:list', async () => {
|
||||||
return await getAllProvidersWithKeyInfo();
|
return await getAllProvidersWithKeyInfo();
|
||||||
@@ -923,6 +933,12 @@ function registerProviderHandlers(gatewayManager: GatewayManager): void {
|
|||||||
try {
|
try {
|
||||||
const ock = getOpenClawProviderKey(existing.type, providerId);
|
const ock = getOpenClawProviderKey(existing.type, providerId);
|
||||||
removeProviderFromOpenClaw(ock);
|
removeProviderFromOpenClaw(ock);
|
||||||
|
|
||||||
|
// Restart Gateway so it no longer loads the deleted provider's plugin/config
|
||||||
|
logger.info(`Restarting Gateway after deleting provider "${ock}"`);
|
||||||
|
void gatewayManager.restart().catch((err) => {
|
||||||
|
logger.warn('Gateway restart after provider delete failed:', err);
|
||||||
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn('Failed to completely remove provider from OpenClaw:', err);
|
console.warn('Failed to completely remove provider from OpenClaw:', err);
|
||||||
}
|
}
|
||||||
@@ -1114,9 +1130,9 @@ function registerProviderHandlers(gatewayManager: GatewayManager): void {
|
|||||||
const ock = getOpenClawProviderKey(provider.type, providerId);
|
const ock = getOpenClawProviderKey(provider.type, providerId);
|
||||||
const providerKey = await getApiKey(providerId);
|
const providerKey = await getApiKey(providerId);
|
||||||
|
|
||||||
// OAuth providers (qwen-portal, minimax-portal) might use OAuth OR a direct API key.
|
// OAuth providers (qwen-portal, minimax-portal, minimax-portal-cn) might use OAuth OR a direct API key.
|
||||||
// Treat them as OAuth only if they don't have a local API key configured.
|
// Treat them as OAuth only if they don't have a local API key configured.
|
||||||
const OAUTH_PROVIDER_TYPES = ['qwen-portal', 'minimax-portal'];
|
const OAUTH_PROVIDER_TYPES = ['qwen-portal', 'minimax-portal', 'minimax-portal-cn'];
|
||||||
const isOAuthProvider = OAUTH_PROVIDER_TYPES.includes(provider.type) && !providerKey;
|
const isOAuthProvider = OAUTH_PROVIDER_TYPES.includes(provider.type) && !providerKey;
|
||||||
|
|
||||||
if (!isOAuthProvider) {
|
if (!isOAuthProvider) {
|
||||||
@@ -1141,23 +1157,30 @@ function registerProviderHandlers(gatewayManager: GatewayManager): void {
|
|||||||
saveProviderKeyToOpenClaw(ock, providerKey);
|
saveProviderKeyToOpenClaw(ock, providerKey);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// OAuth providers (minimax-portal, qwen-portal)
|
// OAuth providers (minimax-portal, minimax-portal-cn, qwen-portal)
|
||||||
const defaultBaseUrl = provider.type === 'minimax-portal'
|
const defaultBaseUrl = provider.type === 'minimax-portal'
|
||||||
? 'https://api.minimax.io/anthropic'
|
? 'https://api.minimax.io/anthropic'
|
||||||
: 'https://portal.qwen.ai/v1';
|
: (provider.type === 'minimax-portal-cn' ? 'https://api.minimaxi.com/anthropic' : 'https://portal.qwen.ai/v1');
|
||||||
const api: 'anthropic-messages' | 'openai-completions' = provider.type === 'minimax-portal'
|
const api: 'anthropic-messages' | 'openai-completions' =
|
||||||
? 'anthropic-messages'
|
(provider.type === 'minimax-portal' || provider.type === 'minimax-portal-cn')
|
||||||
: 'openai-completions';
|
? 'anthropic-messages'
|
||||||
|
: 'openai-completions';
|
||||||
|
|
||||||
let baseUrl = provider.baseUrl || defaultBaseUrl;
|
let baseUrl = provider.baseUrl || defaultBaseUrl;
|
||||||
if (provider.type === 'minimax-portal' && baseUrl && !baseUrl.endsWith('/anthropic')) {
|
if ((provider.type === 'minimax-portal' || provider.type === 'minimax-portal-cn') && baseUrl && !baseUrl.endsWith('/anthropic')) {
|
||||||
baseUrl = baseUrl.replace(/\/$/, '') + '/anthropic';
|
baseUrl = baseUrl.replace(/\/$/, '') + '/anthropic';
|
||||||
}
|
}
|
||||||
|
|
||||||
setOpenClawDefaultModelWithOverride(provider.type, undefined, {
|
// To ensure the OpenClaw Gateway's internal token refresher works,
|
||||||
|
// we must save the CN provider under the "minimax-portal" key in openclaw.json
|
||||||
|
const targetProviderKey = (provider.type === 'minimax-portal' || provider.type === 'minimax-portal-cn')
|
||||||
|
? 'minimax-portal'
|
||||||
|
: provider.type;
|
||||||
|
|
||||||
|
setOpenClawDefaultModelWithOverride(targetProviderKey, undefined, {
|
||||||
baseUrl,
|
baseUrl,
|
||||||
api,
|
api,
|
||||||
apiKeyEnv: provider.type === 'minimax-portal' ? 'minimax-oauth' : 'qwen-oauth',
|
apiKeyEnv: targetProviderKey === 'minimax-portal' ? 'minimax-oauth' : 'qwen-oauth',
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.info(`Configured openclaw.json for OAuth provider "${provider.type}"`);
|
logger.info(`Configured openclaw.json for OAuth provider "${provider.type}"`);
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ import {
|
|||||||
} from '../../node_modules/openclaw/extensions/qwen-portal-auth/oauth';
|
} from '../../node_modules/openclaw/extensions/qwen-portal-auth/oauth';
|
||||||
import { saveOAuthTokenToOpenClaw, setOpenClawDefaultModelWithOverride } from './openclaw-auth';
|
import { saveOAuthTokenToOpenClaw, setOpenClawDefaultModelWithOverride } from './openclaw-auth';
|
||||||
|
|
||||||
export type OAuthProviderType = 'minimax-portal' | 'qwen-portal';
|
export type OAuthProviderType = 'minimax-portal' | 'minimax-portal-cn' | 'qwen-portal';
|
||||||
export type { MiniMaxRegion };
|
export type { MiniMaxRegion };
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────
|
||||||
@@ -55,15 +55,17 @@ class DeviceOAuthManager extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.active = true;
|
this.active = true;
|
||||||
|
this.emit('oauth:start', { provider: provider });
|
||||||
this.activeProvider = provider;
|
this.activeProvider = provider;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (provider === 'minimax-portal') {
|
if (provider === 'minimax-portal' || provider === 'minimax-portal-cn') {
|
||||||
await this.runMiniMaxFlow(region);
|
const actualRegion = provider === 'minimax-portal-cn' ? 'cn' : (region || 'global');
|
||||||
|
await this.runMiniMaxFlow(actualRegion, provider);
|
||||||
} else if (provider === 'qwen-portal') {
|
} else if (provider === 'qwen-portal') {
|
||||||
await this.runQwenFlow();
|
await this.runQwenFlow();
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`Unsupported OAuth provider: ${provider}`);
|
throw new Error(`Unsupported OAuth provider type: ${provider}`);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -89,7 +91,7 @@ class DeviceOAuthManager extends EventEmitter {
|
|||||||
// MiniMax flow
|
// MiniMax flow
|
||||||
// ─────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
private async runMiniMaxFlow(region: MiniMaxRegion): Promise<void> {
|
private async runMiniMaxFlow(region?: MiniMaxRegion, providerType: OAuthProviderType = 'minimax-portal'): Promise<void> {
|
||||||
if (!isOpenClawPresent()) {
|
if (!isOpenClawPresent()) {
|
||||||
throw new Error('OpenClaw package not found');
|
throw new Error('OpenClaw package not found');
|
||||||
}
|
}
|
||||||
@@ -123,7 +125,7 @@ class DeviceOAuthManager extends EventEmitter {
|
|||||||
|
|
||||||
if (!this.active) return;
|
if (!this.active) return;
|
||||||
|
|
||||||
await this.onSuccess('minimax-portal', {
|
await this.onSuccess(providerType, {
|
||||||
access: token.access,
|
access: token.access,
|
||||||
refresh: token.refresh,
|
refresh: token.refresh,
|
||||||
expires: token.expires,
|
expires: token.expires,
|
||||||
@@ -131,6 +133,7 @@ class DeviceOAuthManager extends EventEmitter {
|
|||||||
resourceUrl: token.resourceUrl,
|
resourceUrl: token.resourceUrl,
|
||||||
// MiniMax uses Anthropic Messages API format
|
// MiniMax uses Anthropic Messages API format
|
||||||
api: 'anthropic-messages',
|
api: 'anthropic-messages',
|
||||||
|
region,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,15 +192,19 @@ class DeviceOAuthManager extends EventEmitter {
|
|||||||
expires: number;
|
expires: number;
|
||||||
resourceUrl?: string;
|
resourceUrl?: string;
|
||||||
api: 'anthropic-messages' | 'openai-completions';
|
api: 'anthropic-messages' | 'openai-completions';
|
||||||
|
region?: MiniMaxRegion;
|
||||||
}) {
|
}) {
|
||||||
this.active = false;
|
this.active = false;
|
||||||
this.activeProvider = null;
|
this.activeProvider = null;
|
||||||
logger.info(`[DeviceOAuth] Successfully completed OAuth for ${providerType}`);
|
logger.info(`[DeviceOAuth] Successfully completed OAuth for ${providerType}`);
|
||||||
|
|
||||||
// 1. Write OAuth token to OpenClaw's auth-profiles.json in native OAuth format
|
// 1. Write OAuth token to OpenClaw's auth-profiles.json in native OAuth format.
|
||||||
// (matches what `openclaw models auth login` → upsertAuthProfile writes)
|
// (matches what `openclaw models auth login` → upsertAuthProfile writes).
|
||||||
|
// We save both MiniMax providers to the generic "minimax-portal" profile
|
||||||
|
// so OpenClaw's gateway auto-refresher knows how to find it.
|
||||||
try {
|
try {
|
||||||
saveOAuthTokenToOpenClaw(providerType, {
|
const tokenProviderId = providerType.startsWith('minimax-portal') ? 'minimax-portal' : providerType;
|
||||||
|
saveOAuthTokenToOpenClaw(tokenProviderId, {
|
||||||
access: token.access,
|
access: token.access,
|
||||||
refresh: token.refresh,
|
refresh: token.refresh,
|
||||||
expires: token.expires,
|
expires: token.expires,
|
||||||
@@ -213,18 +220,19 @@ class DeviceOAuthManager extends EventEmitter {
|
|||||||
// Note: MiniMax Anthropic-compatible API requires the /anthropic suffix.
|
// Note: MiniMax Anthropic-compatible API requires the /anthropic suffix.
|
||||||
const defaultBaseUrl = providerType === 'minimax-portal'
|
const defaultBaseUrl = providerType === 'minimax-portal'
|
||||||
? 'https://api.minimax.io/anthropic'
|
? 'https://api.minimax.io/anthropic'
|
||||||
: 'https://portal.qwen.ai/v1';
|
: (providerType === 'minimax-portal-cn' ? 'https://api.minimaxi.com/anthropic' : 'https://portal.qwen.ai/v1');
|
||||||
|
|
||||||
let baseUrl = token.resourceUrl || defaultBaseUrl;
|
let baseUrl = token.resourceUrl || defaultBaseUrl;
|
||||||
|
|
||||||
// If MiniMax returned a resourceUrl (e.g. https://api.minimax.io) but no /anthropic suffix,
|
// If MiniMax returned a resourceUrl (e.g. https://api.minimax.io) but no /anthropic suffix,
|
||||||
// we must append it because we use the 'anthropic-messages' API mode
|
// we must append it because we use the 'anthropic-messages' API mode
|
||||||
if (providerType === 'minimax-portal' && baseUrl && !baseUrl.endsWith('/anthropic')) {
|
if (providerType.startsWith('minimax-portal') && baseUrl && !baseUrl.endsWith('/anthropic')) {
|
||||||
baseUrl = baseUrl.replace(/\/$/, '') + '/anthropic';
|
baseUrl = baseUrl.replace(/\/$/, '') + '/anthropic';
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setOpenClawDefaultModelWithOverride(providerType, undefined, {
|
const tokenProviderId = providerType.startsWith('minimax-portal') ? 'minimax-portal' : providerType;
|
||||||
|
setOpenClawDefaultModelWithOverride(tokenProviderId, undefined, {
|
||||||
baseUrl,
|
baseUrl,
|
||||||
api: token.api,
|
api: token.api,
|
||||||
// OAuth placeholder — tells Gateway to resolve credentials
|
// OAuth placeholder — tells Gateway to resolve credentials
|
||||||
@@ -232,7 +240,7 @@ class DeviceOAuthManager extends EventEmitter {
|
|||||||
// This matches what the OpenClaw plugin's configPatch writes:
|
// This matches what the OpenClaw plugin's configPatch writes:
|
||||||
// minimax-portal → apiKey: 'minimax-oauth'
|
// minimax-portal → apiKey: 'minimax-oauth'
|
||||||
// qwen-portal → apiKey: 'qwen-oauth'
|
// qwen-portal → apiKey: 'qwen-oauth'
|
||||||
apiKeyEnv: providerType === 'minimax-portal' ? 'minimax-oauth' : 'qwen-oauth',
|
apiKeyEnv: tokenProviderId === 'minimax-portal' ? 'minimax-oauth' : 'qwen-oauth',
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.warn(`[DeviceOAuth] Failed to configure openclaw models:`, err);
|
logger.warn(`[DeviceOAuth] Failed to configure openclaw models:`, err);
|
||||||
@@ -240,19 +248,28 @@ class DeviceOAuthManager extends EventEmitter {
|
|||||||
|
|
||||||
// 3. Save provider record in ClawX's own store so UI shows it as configured
|
// 3. Save provider record in ClawX's own store so UI shows it as configured
|
||||||
const existing = await getProvider(providerType);
|
const existing = await getProvider(providerType);
|
||||||
|
const nameMap: Record<OAuthProviderType, string> = {
|
||||||
|
'minimax-portal': 'MiniMax (Global)',
|
||||||
|
'minimax-portal-cn': 'MiniMax (CN)',
|
||||||
|
'qwen-portal': 'Qwen',
|
||||||
|
};
|
||||||
const providerConfig: ProviderConfig = {
|
const providerConfig: ProviderConfig = {
|
||||||
id: providerType,
|
id: providerType,
|
||||||
name: providerType === 'minimax-portal' ? 'MiniMax' : 'Qwen',
|
name: nameMap[providerType as OAuthProviderType] || providerType,
|
||||||
type: providerType,
|
type: providerType,
|
||||||
enabled: existing?.enabled ?? true,
|
enabled: existing?.enabled ?? true,
|
||||||
baseUrl: existing?.baseUrl,
|
baseUrl, // Save the dynamically resolved URL (Global vs CN)
|
||||||
|
|
||||||
model: existing?.model || getProviderDefaultModel(providerType),
|
model: existing?.model || getProviderDefaultModel(providerType),
|
||||||
createdAt: existing?.createdAt || new Date().toISOString(),
|
createdAt: existing?.createdAt || new Date().toISOString(),
|
||||||
updatedAt: new Date().toISOString(),
|
updatedAt: new Date().toISOString(),
|
||||||
};
|
};
|
||||||
await saveProvider(providerConfig);
|
await saveProvider(providerConfig);
|
||||||
|
|
||||||
// 4. Emit success to frontend
|
// 4. Emit success internally so the main process can restart the Gateway
|
||||||
|
this.emit('oauth:success', providerType);
|
||||||
|
|
||||||
|
// 5. Emit success to frontend
|
||||||
if (this.mainWindow && !this.mainWindow.isDestroyed()) {
|
if (this.mainWindow && !this.mainWindow.isDestroyed()) {
|
||||||
this.mainWindow.webContents.send('oauth:success', { provider: providerType, success: true });
|
this.mainWindow.webContents.send('oauth:success', { provider: providerType, success: true });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -175,7 +175,7 @@ export function saveProviderKeyToOpenClaw(
|
|||||||
// managed by OpenClaw plugins via `openclaw models auth login`.
|
// managed by OpenClaw plugins via `openclaw models auth login`.
|
||||||
// Skip only if there's no explicit API key — meaning the user is using OAuth.
|
// Skip only if there's no explicit API key — meaning the user is using OAuth.
|
||||||
// If the user provided an actual API key, write it normally.
|
// If the user provided an actual API key, write it normally.
|
||||||
const OAUTH_PROVIDERS = ['qwen-portal', 'minimax-portal'];
|
const OAUTH_PROVIDERS = ['qwen-portal', 'minimax-portal', 'minimax-portal-cn'];
|
||||||
if (OAUTH_PROVIDERS.includes(provider) && !apiKey) {
|
if (OAUTH_PROVIDERS.includes(provider) && !apiKey) {
|
||||||
console.log(`Skipping auth-profiles write for OAuth provider "${provider}" (no API key provided, using OAuth)`);
|
console.log(`Skipping auth-profiles write for OAuth provider "${provider}" (no API key provided, using OAuth)`);
|
||||||
return;
|
return;
|
||||||
@@ -227,7 +227,7 @@ export function removeProviderKeyFromOpenClaw(
|
|||||||
): void {
|
): void {
|
||||||
// OAuth providers have their credentials managed by OpenClaw plugins.
|
// OAuth providers have their credentials managed by OpenClaw plugins.
|
||||||
// Do NOT delete their auth-profiles entries.
|
// Do NOT delete their auth-profiles entries.
|
||||||
const OAUTH_PROVIDERS = ['qwen-portal', 'minimax-portal'];
|
const OAUTH_PROVIDERS = ['qwen-portal', 'minimax-portal', 'minimax-portal-cn'];
|
||||||
if (OAUTH_PROVIDERS.includes(provider)) {
|
if (OAUTH_PROVIDERS.includes(provider)) {
|
||||||
console.log(`Skipping auth-profiles removal for OAuth provider "${provider}" (managed by OpenClaw plugin)`);
|
console.log(`Skipping auth-profiles removal for OAuth provider "${provider}" (managed by OpenClaw plugin)`);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export const BUILTIN_PROVIDER_TYPES = [
|
|||||||
'moonshot',
|
'moonshot',
|
||||||
'siliconflow',
|
'siliconflow',
|
||||||
'minimax-portal',
|
'minimax-portal',
|
||||||
|
'minimax-portal-cn',
|
||||||
'qwen-portal',
|
'qwen-portal',
|
||||||
'ollama',
|
'ollama',
|
||||||
] as const;
|
] as const;
|
||||||
@@ -110,6 +111,15 @@ const REGISTRY: Record<string, ProviderBackendMeta> = {
|
|||||||
apiKeyEnv: 'MINIMAX_API_KEY',
|
apiKeyEnv: 'MINIMAX_API_KEY',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
'minimax-portal-cn': {
|
||||||
|
envVar: 'MINIMAX_CN_API_KEY',
|
||||||
|
defaultModel: 'minimax-portal/MiniMax-M2.1',
|
||||||
|
providerConfig: {
|
||||||
|
baseUrl: 'https://api.minimaxi.com/anthropic',
|
||||||
|
api: 'anthropic-messages',
|
||||||
|
apiKeyEnv: 'MINIMAX_CN_API_KEY',
|
||||||
|
},
|
||||||
|
},
|
||||||
'qwen-portal': {
|
'qwen-portal': {
|
||||||
envVar: 'QWEN_API_KEY',
|
envVar: 'QWEN_API_KEY',
|
||||||
defaultModel: 'qwen-portal/coder-model',
|
defaultModel: 'qwen-portal/coder-model',
|
||||||
|
|||||||
@@ -234,7 +234,7 @@ export async function getAllProvidersWithKeyInfo(): Promise<
|
|||||||
// This must match getOpenClawProviderKey() in ipc-handlers.ts exactly.
|
// This must match getOpenClawProviderKey() in ipc-handlers.ts exactly.
|
||||||
const openClawKey = (provider.type === 'custom' || provider.type === 'ollama')
|
const openClawKey = (provider.type === 'custom' || provider.type === 'ollama')
|
||||||
? `${provider.type}-${provider.id.replace(/-/g, '').slice(0, 8)}`
|
? `${provider.type}-${provider.id.replace(/-/g, '').slice(0, 8)}`
|
||||||
: provider.type;
|
: provider.type === 'minimax-portal-cn' ? 'minimax-portal' : provider.type;
|
||||||
if (!isBuiltin && !activeOpenClawProviders.has(provider.type) && !activeOpenClawProviders.has(provider.id) && !activeOpenClawProviders.has(openClawKey)) {
|
if (!isBuiltin && !activeOpenClawProviders.has(provider.type) && !activeOpenClawProviders.has(provider.id) && !activeOpenClawProviders.has(openClawKey)) {
|
||||||
console.log(`[Sync] Provider ${provider.id} (${provider.type}) missing from OpenClaw, dropping from ClawX UI`);
|
console.log(`[Sync] Provider ${provider.id} (${provider.type}) missing from OpenClaw, dropping from ClawX UI`);
|
||||||
await deleteProvider(provider.id);
|
await deleteProvider(provider.id);
|
||||||
|
|||||||
@@ -167,6 +167,7 @@ function App() {
|
|||||||
position="bottom-right"
|
position="bottom-right"
|
||||||
richColors
|
richColors
|
||||||
closeButton
|
closeButton
|
||||||
|
style={{ zIndex: 99999 }}
|
||||||
/>
|
/>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ export const providerIcons: Record<string, string> = {
|
|||||||
moonshot,
|
moonshot,
|
||||||
siliconflow,
|
siliconflow,
|
||||||
'minimax-portal': minimaxPortal,
|
'minimax-portal': minimaxPortal,
|
||||||
|
'minimax-portal-cn': minimaxPortal,
|
||||||
'qwen-portal': qwenPortal,
|
'qwen-portal': qwenPortal,
|
||||||
ollama,
|
ollama,
|
||||||
custom,
|
custom,
|
||||||
|
|||||||
@@ -487,20 +487,19 @@ function AddProviderDialog({ existingTypes, onClose, onAdd, onValidateKey }: Add
|
|||||||
setOauthFlowing(false);
|
setOauthFlowing(false);
|
||||||
setOauthData(null);
|
setOauthData(null);
|
||||||
setValidationError(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
|
const { onClose: close, t: translate } = latestRef.current;
|
||||||
if (type && add) {
|
|
||||||
try {
|
// device-oauth.ts already saved the provider config to the backend,
|
||||||
await add(
|
// including the dynamically resolved baseUrl for the region (e.g. CN vs Global).
|
||||||
type,
|
// If we call add() here with undefined baseUrl, it will overwrite and erase it!
|
||||||
info?.name || type,
|
// So we just fetch the latest list from the backend to update the UI.
|
||||||
'', // OAuth providers don't use a plain API key
|
try {
|
||||||
{ model: info?.defaultModelId }
|
await useProviderStore.getState().fetchProviders();
|
||||||
);
|
} catch (err) {
|
||||||
} catch {
|
console.error('Failed to refresh providers after OAuth:', err);
|
||||||
// provider may already exist; ignore duplicate errors
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
close();
|
close();
|
||||||
toast.success(translate('aiProviders.toast.added'));
|
toast.success(translate('aiProviders.toast.added'));
|
||||||
};
|
};
|
||||||
@@ -525,12 +524,22 @@ function AddProviderDialog({ existingTypes, onClose, onAdd, onValidateKey }: Add
|
|||||||
|
|
||||||
const handleStartOAuth = async () => {
|
const handleStartOAuth = async () => {
|
||||||
if (!selectedType) return;
|
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);
|
setOauthFlowing(true);
|
||||||
setOauthData(null);
|
setOauthData(null);
|
||||||
setOauthError(null);
|
setOauthError(null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await window.electron.ipcRenderer.invoke('provider:requestOAuth', selectedType, 'global');
|
await window.electron.ipcRenderer.invoke('provider:requestOAuth', selectedType);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setOauthError(String(e));
|
setOauthError(String(e));
|
||||||
setOauthFlowing(false);
|
setOauthFlowing(false);
|
||||||
@@ -552,6 +561,15 @@ function AddProviderDialog({ existingTypes, onClose, onAdd, onValidateKey }: Add
|
|||||||
const handleAdd = async () => {
|
const handleAdd = async () => {
|
||||||
if (!selectedType) return;
|
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);
|
setSaving(true);
|
||||||
setValidationError(null);
|
setValidationError(null);
|
||||||
|
|
||||||
|
|||||||
@@ -53,7 +53,8 @@
|
|||||||
"updated": "Provider updated",
|
"updated": "Provider updated",
|
||||||
"failedUpdate": "Failed to update provider",
|
"failedUpdate": "Failed to update provider",
|
||||||
"invalidKey": "Invalid API key",
|
"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": {
|
"oauth": {
|
||||||
"loginMode": "OAuth Login",
|
"loginMode": "OAuth Login",
|
||||||
|
|||||||
@@ -53,7 +53,8 @@
|
|||||||
"updated": "提供商已更新",
|
"updated": "提供商已更新",
|
||||||
"failedUpdate": "更新提供商失败",
|
"failedUpdate": "更新提供商失败",
|
||||||
"invalidKey": "无效的 API 密钥",
|
"invalidKey": "无效的 API 密钥",
|
||||||
"modelRequired": "需要模型 ID"
|
"modelRequired": "需要模型 ID",
|
||||||
|
"minimaxConflict": "不能同时添加 MiniMax 国际站和国内站的服务商。"
|
||||||
},
|
},
|
||||||
"oauth": {
|
"oauth": {
|
||||||
"loginMode": "OAuth 登录",
|
"loginMode": "OAuth 登录",
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export const PROVIDER_TYPES = [
|
|||||||
'moonshot',
|
'moonshot',
|
||||||
'siliconflow',
|
'siliconflow',
|
||||||
'minimax-portal',
|
'minimax-portal',
|
||||||
|
'minimax-portal-cn',
|
||||||
'qwen-portal',
|
'qwen-portal',
|
||||||
'ollama',
|
'ollama',
|
||||||
'custom',
|
'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: '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: '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: '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: '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: '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' },
|
{ id: 'custom', name: 'Custom', icon: '⚙️', placeholder: 'API key...', requiresApiKey: true, showBaseUrl: true, showModelId: true, modelIdPlaceholder: 'your-provider/model-id' },
|
||||||
|
|||||||
@@ -773,14 +773,28 @@ function ProviderContent({
|
|||||||
|
|
||||||
const handleStartOAuth = async () => {
|
const handleStartOAuth = async () => {
|
||||||
if (!selectedProvider) return;
|
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);
|
setOauthFlowing(true);
|
||||||
setOauthData(null);
|
setOauthData(null);
|
||||||
setOauthError(null);
|
setOauthError(null);
|
||||||
|
|
||||||
// Default to global region for MiniMax in setup
|
|
||||||
const region = 'global';
|
|
||||||
try {
|
try {
|
||||||
await window.electron.ipcRenderer.invoke('provider:requestOAuth', selectedProvider, region);
|
await window.electron.ipcRenderer.invoke('provider:requestOAuth', selectedProvider);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setOauthError(String(e));
|
setOauthError(String(e));
|
||||||
setOauthFlowing(false);
|
setOauthFlowing(false);
|
||||||
@@ -905,6 +919,21 @@ function ProviderContent({
|
|||||||
const handleValidateAndSave = async () => {
|
const handleValidateAndSave = async () => {
|
||||||
if (!selectedProvider) return;
|
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);
|
setValidating(true);
|
||||||
setKeyValid(null);
|
setKeyValid(null);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user