Adapt MiniMax auth plugin compatibility (#913)

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Haze <hazeone@users.noreply.github.com>
This commit is contained in:
Haze
2026-04-24 18:14:13 +08:00
committed by GitHub
parent 4271419abb
commit 89dd765fd6
4 changed files with 581 additions and 59 deletions

View File

@@ -53,6 +53,20 @@ function logLegacyProviderApiUsage(method: string, replacement: string): void {
);
}
function inferProviderVendorIdFromOpenClawEntry(
key: string,
entry: Record<string, unknown>,
): 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<ProviderDefinition[]> {
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").

View File

@@ -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<string>; 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<string, BundledPluginManifest>();
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<string>([
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<string, unknown>, provider: string): void {
const { canonicalPluginId, stalePluginIds } = getOAuthPluginRegistration(provider);
const plugins = isPlainRecord(config.plugins) ? config.plugins as Record<string, unknown> : {};
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<string, Record<string, unknown>> : {};
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<string, unknown>,
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<string, unknown>;
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<string, unknown>;
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<string>; enabledByDefault: string[] } | null = null;
function discoverBundledPlugins(): { all: Set<string>; enabledByDefault: string[] } {
if (_bundledPluginCache) return _bundledPluginCache;
const all = new Set<string>();
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<void
const config = await readOpenClawJson();
let modified = false;
// Disable plugin (for OAuth like minimax-portal-auth)
const plugins = config.plugins as Record<string, unknown> | undefined;
const entries = (plugins?.entries ?? {}) as Record<string, Record<string, unknown>>;
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<string, unknown>;
const allow = Array.isArray(plugins.allow) ? [...plugins.allow as string[]] : [];
const pEntries = (plugins.entries || {}) as Record<string, unknown>;
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<string, unknown>;
const allow = Array.isArray(plugins.allow) ? [...plugins.allow as string[]] : [];
const pEntries = (plugins.entries || {}) as Record<string, unknown>;
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<void> {
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

View File

@@ -30,6 +30,16 @@ vi.mock('electron', () => ({
},
}));
vi.mock('@electron/utils/paths', async () => {
const actual = await vi.importActual<typeof import('@electron/utils/paths')>('@electron/utils/paths');
const resolvedDir = join(testHome, '.openclaw-test-openclaw');
return {
...actual,
getOpenClawResolvedDir: () => resolvedDir,
getOpenClawDir: () => resolvedDir,
};
});
async function writeOpenClawJson(config: unknown): Promise<void> {
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<typeof import('@electron/utils/paths')>('@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<string, unknown>;
const allow = plugins.allow as string[];
const entries = plugins.entries as Record<string, Record<string, unknown>>;
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<typeof import('@electron/utils/paths')>('@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<string, unknown>;
const allow = plugins.allow as string[];
const entries = plugins.entries as Record<string, Record<string, unknown>>;
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<typeof import('@electron/utils/paths')>('@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<string, unknown>;
const allow = plugins.allow as string[];
const entries = plugins.entries as Record<string, Record<string, unknown>>;
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<typeof import('@electron/utils/paths')>('@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<string, unknown>;
const allow = plugins.allow as string[];
const entries = plugins.entries as Record<string, Record<string, unknown>>;
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<typeof import('@electron/utils/paths')>('@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<string, unknown>;
const allow = plugins.allow as string[];
const entries = plugins.entries as Record<string, Record<string, unknown>>;
expect(allow).toEqual(['custom-plugin']);
expect(entries['minimax-portal-auth']).toBeUndefined();
expect(entries['custom-plugin']).toEqual({ enabled: true });
});
});

View File

@@ -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']));