From 89dd765fd6e3365404b65965c9841119a478bcf1 Mon Sep 17 00:00:00 2001 From: Haze <709547807@qq.com> Date: Fri, 24 Apr 2026 18:14:13 +0800 Subject: [PATCH] Adapt MiniMax auth plugin compatibility (#913) Co-authored-by: Cursor Agent Co-authored-by: Haze --- .../services/providers/provider-service.ts | 19 +- electron/utils/openclaw-auth.ts | 322 +++++++++++++++--- tests/unit/openclaw-auth.test.ts | 256 ++++++++++++++ .../provider-service-stale-cleanup.test.ts | 43 +++ 4 files changed, 581 insertions(+), 59 deletions(-) diff --git a/electron/services/providers/provider-service.ts b/electron/services/providers/provider-service.ts index a3fcf47..681925e 100644 --- a/electron/services/providers/provider-service.ts +++ b/electron/services/providers/provider-service.ts @@ -53,6 +53,20 @@ function logLegacyProviderApiUsage(method: string, replacement: string): void { ); } +function inferProviderVendorIdFromOpenClawEntry( + key: string, + entry: Record, +): ProviderType | 'custom' { + if (key === 'minimax-portal') { + const baseUrl = typeof entry.baseUrl === 'string' ? entry.baseUrl.toLowerCase() : ''; + if (baseUrl.includes('api.minimaxi.com')) { + return 'minimax-portal-cn'; + } + } + + return ((BUILTIN_PROVIDER_TYPES as readonly string[]).includes(key) ? key : 'custom') as ProviderType | 'custom'; +} + export class ProviderService { async listVendors(): Promise { return PROVIDER_DEFINITIONS; @@ -157,9 +171,8 @@ export class ProviderService { for (const [key, entry] of Object.entries(providers)) { if (existingIds.has(key)) continue; - const definition = getProviderDefinition(key); - const isBuiltin = (BUILTIN_PROVIDER_TYPES as readonly string[]).includes(key); - const vendorId = isBuiltin ? key : 'custom'; + const vendorId = inferProviderVendorIdFromOpenClawEntry(key, entry); + const definition = getProviderDefinition(vendorId === 'custom' ? key : vendorId); // Skip if an account with this vendorId already exists (e.g. user already // created "openrouter-uuid" via UI — no need to import bare "openrouter"). diff --git a/electron/utils/openclaw-auth.ts b/electron/utils/openclaw-auth.ts index 5bfdb82..3133132 100644 --- a/electron/utils/openclaw-auth.ts +++ b/electron/utils/openclaw-auth.ts @@ -20,6 +20,7 @@ import { getProviderConfig, } from './provider-registry'; import { + OPENCLAW_PROVIDER_KEY_MINIMAX, OPENCLAW_PROVIDER_KEY_MOONSHOT, OPENCLAW_PROVIDER_KEY_MOONSHOT_GLOBAL, isOAuthProviderType, @@ -29,9 +30,228 @@ import { withConfigLock } from './config-mutex'; const AUTH_STORE_VERSION = 1; const AUTH_PROFILE_FILENAME = 'auth-profiles.json'; +const LEGACY_MINIMAX_OAUTH_PLUGIN_ID = 'minimax-portal-auth'; +const MERGED_MINIMAX_PLUGIN_ID = 'minimax'; -function getOAuthPluginId(provider: string): string { - return `${provider}-auth`; +interface BundledPluginManifest { + id: string; + enabledByDefault: boolean; + providers: string[]; + legacyPluginIds: string[]; +} + +interface OAuthPluginRegistration { + canonicalPluginId: string; + stalePluginIds: string[]; +} + +interface MiniMaxPluginRegistration extends OAuthPluginRegistration { + mergedPlugin: boolean; +} + +let _bundledPluginManifestCache: BundledPluginManifest[] | null = null; +let _bundledPluginCache: { all: Set; enabledByDefault: string[] } | null = null; +let _miniMaxPluginRegistrationCache: MiniMaxPluginRegistration | null = null; + +export function resetOpenClawPluginDiscoveryCaches(): void { + _bundledPluginManifestCache = null; + _bundledPluginCache = null; + _miniMaxPluginRegistrationCache = null; +} + +function getOpenClawExtensionsRoots(): string[] { + const openClawDir = getOpenClawResolvedDir(); + return [ + join(openClawDir, 'dist', 'extensions'), + join(openClawDir, 'extensions'), + ]; +} + +function discoverBundledPluginManifests(): BundledPluginManifest[] { + if (_bundledPluginManifestCache) return _bundledPluginManifestCache; + + const manifests = new Map(); + + for (const extensionsDir of getOpenClawExtensionsRoots()) { + try { + if (!existsSync(extensionsDir)) { + continue; + } + + for (const entry of readdirSync(extensionsDir, { withFileTypes: true })) { + if (!entry.isDirectory()) continue; + + const manifestPath = join(extensionsDir, entry.name, 'openclaw.plugin.json'); + if (!existsSync(manifestPath)) continue; + + try { + const parsed = JSON.parse(readFileSync(manifestPath, 'utf-8')) as { + id?: unknown; + enabledByDefault?: unknown; + providers?: unknown; + legacyPluginIds?: unknown; + }; + if (typeof parsed.id !== 'string' || !parsed.id.trim()) { + continue; + } + + const existing = manifests.get(parsed.id) ?? { + id: parsed.id, + enabledByDefault: false, + providers: [], + legacyPluginIds: [], + }; + + const providers = Array.isArray(parsed.providers) + ? parsed.providers.filter((provider): provider is string => typeof provider === 'string' && provider.trim().length > 0) + : []; + const legacyPluginIds = Array.isArray(parsed.legacyPluginIds) + ? parsed.legacyPluginIds.filter((pluginId): pluginId is string => typeof pluginId === 'string' && pluginId.trim().length > 0) + : []; + + existing.enabledByDefault = existing.enabledByDefault || parsed.enabledByDefault === true; + existing.providers = Array.from(new Set([...existing.providers, ...providers])); + existing.legacyPluginIds = Array.from(new Set([...existing.legacyPluginIds, ...legacyPluginIds])); + + manifests.set(parsed.id, existing); + } catch { + // Malformed manifest — skip silently + } + } + } catch { + // Extension directory not found or unreadable — ignore + } + } + + _bundledPluginManifestCache = Array.from(manifests.values()); + return _bundledPluginManifestCache; +} + +function resolveMiniMaxPluginRegistration(): MiniMaxPluginRegistration { + if (_miniMaxPluginRegistrationCache) return _miniMaxPluginRegistrationCache; + + const manifests = discoverBundledPluginManifests(); + const mergedManifest = manifests.find((manifest) => ( + manifest.id === MERGED_MINIMAX_PLUGIN_ID + && ( + manifest.providers.includes(OPENCLAW_PROVIDER_KEY_MINIMAX) + || manifest.legacyPluginIds.includes(LEGACY_MINIMAX_OAUTH_PLUGIN_ID) + ) + )); + const legacyManifest = manifests.find((manifest) => manifest.id === LEGACY_MINIMAX_OAUTH_PLUGIN_ID); + + const canonicalPluginId = mergedManifest ? MERGED_MINIMAX_PLUGIN_ID : LEGACY_MINIMAX_OAUTH_PLUGIN_ID; + const knownPluginIds = new Set([ + LEGACY_MINIMAX_OAUTH_PLUGIN_ID, + MERGED_MINIMAX_PLUGIN_ID, + ]); + + for (const manifest of [mergedManifest, legacyManifest]) { + if (!manifest) continue; + knownPluginIds.add(manifest.id); + for (const legacyPluginId of manifest.legacyPluginIds) { + knownPluginIds.add(legacyPluginId); + } + } + + _miniMaxPluginRegistrationCache = { + canonicalPluginId, + stalePluginIds: Array.from(knownPluginIds).filter((pluginId) => pluginId !== canonicalPluginId), + mergedPlugin: Boolean(mergedManifest), + }; + return _miniMaxPluginRegistrationCache; +} + +function getOAuthPluginRegistration(provider: string): OAuthPluginRegistration { + if (provider === OPENCLAW_PROVIDER_KEY_MINIMAX) { + return resolveMiniMaxPluginRegistration(); + } + + return { + canonicalPluginId: `${provider}-auth`, + stalePluginIds: [], + }; +} + +function ensureOAuthPluginEnabled(config: Record, provider: string): void { + const { canonicalPluginId, stalePluginIds } = getOAuthPluginRegistration(provider); + const plugins = isPlainRecord(config.plugins) ? config.plugins as Record : {}; + const allow = Array.isArray(plugins.allow) + ? (plugins.allow as unknown[]).filter((value): value is string => typeof value === 'string') + : []; + const pEntries = isPlainRecord(plugins.entries) ? plugins.entries as Record> : {}; + + const nextAllow = allow.filter((pluginId) => !stalePluginIds.includes(pluginId)); + if (!nextAllow.includes(canonicalPluginId)) { + nextAllow.push(canonicalPluginId); + } + + for (const stalePluginId of stalePluginIds) { + delete pEntries[stalePluginId]; + } + + pEntries[canonicalPluginId] = { + ...(isPlainRecord(pEntries[canonicalPluginId]) ? pEntries[canonicalPluginId] : {}), + enabled: true, + }; + + plugins.allow = nextAllow; + plugins.entries = pEntries; + config.plugins = plugins; +} + +function removePluginRegistrations( + config: Record, + pluginIds: string[], +): boolean { + const uniquePluginIds = Array.from(new Set(pluginIds.filter(Boolean))); + if (uniquePluginIds.length === 0 || !isPlainRecord(config.plugins)) { + return false; + } + + const plugins = config.plugins as Record; + let modified = false; + + if (Array.isArray(plugins.allow)) { + const allow = (plugins.allow as unknown[]).filter((value): value is string => typeof value === 'string'); + const nextAllow = allow.filter((pluginId) => !uniquePluginIds.includes(pluginId)); + if (nextAllow.length !== allow.length) { + modified = true; + if (nextAllow.length > 0) { + plugins.allow = nextAllow; + } else { + delete plugins.allow; + } + } + } + + if (isPlainRecord(plugins.entries)) { + const entries = plugins.entries as Record; + for (const pluginId of uniquePluginIds) { + if (pluginId in entries) { + delete entries[pluginId]; + modified = true; + } + } + if (Object.keys(entries).length === 0) { + delete plugins.entries; + } + } + + if (plugins.enabled === true) { + const pluginKeysExcludingEnabled = Object.keys(plugins).filter((key) => key !== 'enabled'); + if (pluginKeysExcludingEnabled.length === 0) { + delete plugins.enabled; + modified = true; + } + } + + if (Object.keys(plugins).length === 0) { + delete config.plugins; + modified = true; + } + + return modified; } // ── Helpers ────────────────────────────────────────────────────── @@ -264,36 +484,19 @@ function expandProviderKeysForDeletion(provider: string): string[] { * Results are cached for the lifetime of the process since bundled * extensions don't change at runtime. */ -let _bundledPluginCache: { all: Set; enabledByDefault: string[] } | null = null; function discoverBundledPlugins(): { all: Set; enabledByDefault: string[] } { if (_bundledPluginCache) return _bundledPluginCache; + const all = new Set(); const enabledByDefault: string[] = []; - try { - const extensionsDir = join(getOpenClawResolvedDir(), 'dist', 'extensions'); - if (!existsSync(extensionsDir)) { - _bundledPluginCache = { all, enabledByDefault }; - return _bundledPluginCache; + + for (const manifest of discoverBundledPluginManifests()) { + all.add(manifest.id); + if (manifest.enabledByDefault) { + enabledByDefault.push(manifest.id); } - for (const entry of readdirSync(extensionsDir, { withFileTypes: true })) { - if (!entry.isDirectory()) continue; - const manifestPath = join(extensionsDir, entry.name, 'openclaw.plugin.json'); - if (!existsSync(manifestPath)) continue; - try { - const manifest = JSON.parse(readFileSync(manifestPath, 'utf-8')); - if (typeof manifest.id === 'string') { - all.add(manifest.id); - if (manifest.enabledByDefault === true) { - enabledByDefault.push(manifest.id); - } - } - } catch { - // Malformed manifest — skip silently - } - } - } catch { - // Extension directory not found or unreadable — return empty } + _bundledPluginCache = { all, enabledByDefault }; return _bundledPluginCache; } @@ -556,14 +759,13 @@ export async function removeProviderFromOpenClaw(provider: string): Promise | undefined; - const entries = (plugins?.entries ?? {}) as Record>; - const pluginName = `${provider}-auth`; - if (entries[pluginName]) { - entries[pluginName].enabled = false; - modified = true; - console.log(`Disabled OpenClaw plugin: ${pluginName}`); + // Remove plugin registrations for OAuth providers (e.g. MiniMax). + if (isOpenClawOAuthPluginProviderKey(provider)) { + const { canonicalPluginId, stalePluginIds } = getOAuthPluginRegistration(provider); + if (removePluginRegistrations(config, [canonicalPluginId, ...stalePluginIds])) { + modified = true; + console.log(`Removed OpenClaw plugin registrations for provider "${provider}"`); + } } // Remove from models.providers @@ -916,17 +1118,7 @@ export async function syncProviderConfigToOpenClaw( // Ensure extension is enabled for oauth providers to prevent gateway wiping config if (isOpenClawOAuthPluginProviderKey(provider)) { - const plugins = (config.plugins || {}) as Record; - const allow = Array.isArray(plugins.allow) ? [...plugins.allow as string[]] : []; - const pEntries = (plugins.entries || {}) as Record; - const pluginId = getOAuthPluginId(provider); - if (!allow.includes(pluginId)) { - allow.push(pluginId); - } - pEntries[pluginId] = { enabled: true }; - plugins.allow = allow; - plugins.entries = pEntries; - config.plugins = plugins; + ensureOAuthPluginEnabled(config, provider); } await writeOpenClawJson(config); @@ -981,17 +1173,7 @@ export async function setOpenClawDefaultModelWithOverride( // Ensure the extension plugin is marked as enabled in openclaw.json if (isOpenClawOAuthPluginProviderKey(provider)) { - const plugins = (config.plugins || {}) as Record; - const allow = Array.isArray(plugins.allow) ? [...plugins.allow as string[]] : []; - const pEntries = (plugins.entries || {}) as Record; - const pluginId = getOAuthPluginId(provider); - if (!allow.includes(pluginId)) { - allow.push(pluginId); - } - pEntries[pluginId] = { enabled: true }; - plugins.allow = allow; - plugins.entries = pEntries; - config.plugins = plugins; + ensureOAuthPluginEnabled(config, provider); } await writeOpenClawJson(config); @@ -1659,6 +1841,34 @@ export async function sanitizeOpenClawConfig(): Promise { pluginsObj.allow = allowArr; } + // ── MiniMax merged-plugin compatibility cleanup ───────────── + // Newer OpenClaw releases merged the legacy minimax-portal-auth plugin + // into the canonical "minimax" plugin. Legacy ids may still be accepted + // in some allowlist paths, but explicit plugins.entries map keys are not + // consistently normalized upstream, which causes "plugin not found" + // warnings. Migrate stale ids only when a merged MiniMax plugin is + // actually installed; otherwise preserve the old plugin for compatibility. + const miniMaxPluginRegistration = resolveMiniMaxPluginRegistration(); + if (miniMaxPluginRegistration.mergedPlugin) { + let miniMaxModified = false; + for (const stalePluginId of miniMaxPluginRegistration.stalePluginIds) { + const staleAllowIdx = allowArr.indexOf(stalePluginId); + if (staleAllowIdx !== -1) { + allowArr.splice(staleAllowIdx, 1); + miniMaxModified = true; + console.log(`[sanitize] Removed stale MiniMax plugin from plugins.allow: ${stalePluginId}`); + } + if (pEntries[stalePluginId]) { + delete pEntries[stalePluginId]; + miniMaxModified = true; + console.log(`[sanitize] Removed stale MiniMax plugin from plugins.entries: ${stalePluginId}`); + } + } + if (miniMaxModified) { + modified = true; + } + } + // ── acpx legacy config/install cleanup ───────────────────── // Older OpenClaw releases allowed plugins.entries.acpx.config.command // and expectedVersion overrides. Current bundled acpx schema rejects diff --git a/tests/unit/openclaw-auth.test.ts b/tests/unit/openclaw-auth.test.ts index 255bf7b..585551c 100644 --- a/tests/unit/openclaw-auth.test.ts +++ b/tests/unit/openclaw-auth.test.ts @@ -30,6 +30,16 @@ vi.mock('electron', () => ({ }, })); +vi.mock('@electron/utils/paths', async () => { + const actual = await vi.importActual('@electron/utils/paths'); + const resolvedDir = join(testHome, '.openclaw-test-openclaw'); + return { + ...actual, + getOpenClawResolvedDir: () => resolvedDir, + getOpenClawDir: () => resolvedDir, + }; +}); + async function writeOpenClawJson(config: unknown): Promise { const openclawDir = join(testHome, '.openclaw'); await mkdir(openclawDir, { recursive: true }); @@ -494,6 +504,58 @@ describe('sanitizeOpenClawConfig', () => { expect(dingtalk.clientId).toBe('dt-client-id'); expect(dingtalk.clientSecret).toBe('dt-secret'); }); + + it('removes stale minimax-portal-auth plugin entries when merged minimax plugin is installed', async () => { + await writeOpenClawJson({ + plugins: { + allow: ['minimax-portal-auth', 'custom-plugin'], + entries: { + 'minimax-portal-auth': { enabled: true }, + 'custom-plugin': { enabled: true }, + }, + }, + models: { + providers: { + 'minimax-portal': { + baseUrl: 'https://api.minimax.io/anthropic', + api: 'anthropic-messages', + }, + }, + }, + }); + + const openclawDir = join(testHome, '.openclaw-package-sanitize'); + await mkdir(join(openclawDir, 'dist', 'extensions', 'minimax'), { recursive: true }); + await writeFile( + join(openclawDir, 'dist', 'extensions', 'minimax', 'openclaw.plugin.json'), + JSON.stringify({ + id: 'minimax', + providers: ['minimax', 'minimax-portal'], + legacyPluginIds: ['minimax-portal-auth'], + }, null, 2), + 'utf8', + ); + + vi.doMock('@electron/utils/paths', async () => { + const actual = await vi.importActual('@electron/utils/paths'); + return { + ...actual, + getOpenClawResolvedDir: () => openclawDir, + }; + }); + + const { sanitizeOpenClawConfig } = await import('@electron/utils/openclaw-auth'); + await sanitizeOpenClawConfig(); + + const result = await readOpenClawJson(); + const plugins = result.plugins as Record; + const allow = plugins.allow as string[]; + const entries = plugins.entries as Record>; + + expect(allow).toEqual(['custom-plugin']); + expect(entries['minimax-portal-auth']).toBeUndefined(); + expect(entries['custom-plugin']).toEqual({ enabled: true }); + }); }); describe('syncProviderConfigToOpenClaw', () => { @@ -504,6 +566,100 @@ describe('syncProviderConfigToOpenClaw', () => { await rm(testUserData, { recursive: true, force: true }); }); + it('uses legacy minimax-portal-auth plugin registration when only the legacy plugin exists', async () => { + await writeOpenClawJson({ + models: { providers: {} }, + }); + + const openclawDir = join(testHome, '.openclaw-package-old'); + await mkdir(join(openclawDir, 'extensions', 'minimax-portal-auth'), { recursive: true }); + await writeFile( + join(openclawDir, 'extensions', 'minimax-portal-auth', 'openclaw.plugin.json'), + JSON.stringify({ + id: 'minimax-portal-auth', + providers: ['minimax-portal'], + }, null, 2), + 'utf8', + ); + + vi.doMock('@electron/utils/paths', async () => { + const actual = await vi.importActual('@electron/utils/paths'); + return { + ...actual, + getOpenClawResolvedDir: () => openclawDir, + }; + }); + + const { syncProviderConfigToOpenClaw } = await import('@electron/utils/openclaw-auth'); + + await syncProviderConfigToOpenClaw('minimax-portal', 'MiniMax-M2.7', { + baseUrl: 'https://api.minimax.io/anthropic', + api: 'anthropic-messages', + apiKeyEnv: 'minimax-oauth', + }); + + const result = await readOpenClawJson(); + const plugins = result.plugins as Record; + const allow = plugins.allow as string[]; + const entries = plugins.entries as Record>; + + expect(allow).toContain('minimax-portal-auth'); + expect(entries['minimax-portal-auth']).toEqual({ enabled: true }); + expect(entries.minimax).toBeUndefined(); + }); + + it('uses merged minimax plugin registration and removes stale legacy ids when minimax plugin is installed', async () => { + await writeOpenClawJson({ + plugins: { + allow: ['minimax-portal-auth', 'custom-plugin'], + entries: { + 'minimax-portal-auth': { enabled: true }, + 'custom-plugin': { enabled: true }, + }, + }, + models: { providers: {} }, + }); + + const openclawDir = join(testHome, '.openclaw-package-new'); + await mkdir(join(openclawDir, 'dist', 'extensions', 'minimax'), { recursive: true }); + await writeFile( + join(openclawDir, 'dist', 'extensions', 'minimax', 'openclaw.plugin.json'), + JSON.stringify({ + id: 'minimax', + providers: ['minimax', 'minimax-portal'], + legacyPluginIds: ['minimax-portal-auth'], + }, null, 2), + 'utf8', + ); + + vi.doMock('@electron/utils/paths', async () => { + const actual = await vi.importActual('@electron/utils/paths'); + return { + ...actual, + getOpenClawResolvedDir: () => openclawDir, + }; + }); + + const { syncProviderConfigToOpenClaw } = await import('@electron/utils/openclaw-auth'); + + await syncProviderConfigToOpenClaw('minimax-portal', 'MiniMax-M2.7', { + baseUrl: 'https://api.minimax.io/anthropic', + api: 'anthropic-messages', + apiKeyEnv: 'minimax-oauth', + }); + + const result = await readOpenClawJson(); + const plugins = result.plugins as Record; + const allow = plugins.allow as string[]; + const entries = plugins.entries as Record>; + + expect(allow).toContain('minimax'); + expect(allow).toContain('custom-plugin'); + expect(allow).not.toContain('minimax-portal-auth'); + expect(entries.minimax).toEqual({ enabled: true }); + expect(entries['minimax-portal-auth']).toBeUndefined(); + }); + it('writes moonshot web search config to plugin config instead of tools.web.search.kimi', async () => { await writeOpenClawJson({ models: { @@ -719,4 +875,104 @@ describe('auth-backed provider discovery', () => { expect(result.providers).toEqual({}); await expect(getActiveOpenClawProviders()).resolves.toEqual(new Set()); }); + + it('removes merged and legacy minimax plugin registrations when deleting the provider', async () => { + await writeOpenClawJson({ + plugins: { + allow: ['minimax', 'minimax-portal-auth', 'custom-plugin'], + entries: { + minimax: { enabled: true }, + 'minimax-portal-auth': { enabled: true }, + 'custom-plugin': { enabled: true }, + }, + }, + models: { + providers: { + 'minimax-portal': { + baseUrl: 'https://api.minimax.io/anthropic', + api: 'anthropic-messages', + }, + }, + }, + }); + + const openclawDir = join(testHome, '.openclaw-package-new'); + await mkdir(join(openclawDir, 'dist', 'extensions', 'minimax'), { recursive: true }); + await writeFile( + join(openclawDir, 'dist', 'extensions', 'minimax', 'openclaw.plugin.json'), + JSON.stringify({ + id: 'minimax', + providers: ['minimax', 'minimax-portal'], + legacyPluginIds: ['minimax-portal-auth'], + }, null, 2), + 'utf8', + ); + + vi.doMock('@electron/utils/paths', async () => { + const actual = await vi.importActual('@electron/utils/paths'); + return { + ...actual, + getOpenClawResolvedDir: () => openclawDir, + }; + }); + + const { removeProviderFromOpenClaw } = await import('@electron/utils/openclaw-auth'); + + await removeProviderFromOpenClaw('minimax-portal'); + + const result = await readOpenClawJson(); + const plugins = result.plugins as Record; + const allow = plugins.allow as string[]; + const entries = plugins.entries as Record>; + + expect(allow).toEqual(['custom-plugin']); + expect(entries.minimax).toBeUndefined(); + expect(entries['minimax-portal-auth']).toBeUndefined(); + expect(entries['custom-plugin']).toEqual({ enabled: true }); + }); + + it('sanitizes stale minimax-portal-auth entries when merged minimax plugin is installed', async () => { + await writeOpenClawJson({ + plugins: { + allow: ['minimax-portal-auth', 'custom-plugin'], + entries: { + 'minimax-portal-auth': { enabled: true }, + 'custom-plugin': { enabled: true }, + }, + }, + }); + + const openclawDir = join(testHome, '.openclaw-package-new'); + await mkdir(join(openclawDir, 'dist', 'extensions', 'minimax'), { recursive: true }); + await writeFile( + join(openclawDir, 'dist', 'extensions', 'minimax', 'openclaw.plugin.json'), + JSON.stringify({ + id: 'minimax', + providers: ['minimax', 'minimax-portal'], + legacyPluginIds: ['minimax-portal-auth'], + }, null, 2), + 'utf8', + ); + + vi.doMock('@electron/utils/paths', async () => { + const actual = await vi.importActual('@electron/utils/paths'); + return { + ...actual, + getOpenClawResolvedDir: () => openclawDir, + }; + }); + + const { sanitizeOpenClawConfig } = await import('@electron/utils/openclaw-auth'); + + await sanitizeOpenClawConfig(); + + const result = await readOpenClawJson(); + const plugins = result.plugins as Record; + const allow = plugins.allow as string[]; + const entries = plugins.entries as Record>; + + expect(allow).toEqual(['custom-plugin']); + expect(entries['minimax-portal-auth']).toBeUndefined(); + expect(entries['custom-plugin']).toEqual({ enabled: true }); + }); }); diff --git a/tests/unit/provider-service-stale-cleanup.test.ts b/tests/unit/provider-service-stale-cleanup.test.ts index de22222..3f2e810 100644 --- a/tests/unit/provider-service-stale-cleanup.test.ts +++ b/tests/unit/provider-service-stale-cleanup.test.ts @@ -291,6 +291,49 @@ describe('ProviderService.listAccounts (openclaw.json as sole source of truth)', expect(ids).toContain('minimax-portal-cn-uuid'); }); + it('seeds a MiniMax CN account when minimax-portal baseUrl points at the CN endpoint', async () => { + mocks.listProviderAccounts.mockResolvedValue([]); + mocks.getActiveOpenClawProviders.mockResolvedValue(new Set(['minimax-portal'])); + mocks.getOpenClawProvidersConfig.mockResolvedValue({ + providers: { + 'minimax-portal': { baseUrl: 'https://api.minimaxi.com/anthropic' }, + }, + defaultModel: undefined, + }); + mocks.getProviderDefinition.mockImplementation((key: string) => { + if (key === 'minimax-portal-cn') { + return { + id: 'minimax-portal-cn', + name: 'MiniMax (CN)', + defaultAuthMode: 'oauth_device', + defaultModelId: 'MiniMax-M2.7', + providerConfig: { + baseUrl: 'https://api.minimaxi.com/anthropic', + api: 'anthropic-messages', + }, + }; + } + return undefined; + }); + + const result = await service.listAccounts(); + + expect(mocks.saveProviderAccount).toHaveBeenCalledWith( + expect.objectContaining({ + id: 'minimax-portal', + vendorId: 'minimax-portal-cn', + label: 'MiniMax (CN)', + baseUrl: 'https://api.minimaxi.com/anthropic', + }), + ); + expect(result).toHaveLength(1); + expect(result[0]).toEqual(expect.objectContaining({ + id: 'minimax-portal', + vendorId: 'minimax-portal-cn', + label: 'MiniMax (CN)', + })); + }); + it('seeds builtin providers discovered from auth profiles without explicit models.providers entries', async () => { mocks.listProviderAccounts.mockResolvedValue([]); mocks.getActiveOpenClawProviders.mockResolvedValue(new Set(['openai', 'anthropic']));