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:
@@ -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").
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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']));
|
||||
|
||||
Reference in New Issue
Block a user