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 {
|
export class ProviderService {
|
||||||
async listVendors(): Promise<ProviderDefinition[]> {
|
async listVendors(): Promise<ProviderDefinition[]> {
|
||||||
return PROVIDER_DEFINITIONS;
|
return PROVIDER_DEFINITIONS;
|
||||||
@@ -157,9 +171,8 @@ export class ProviderService {
|
|||||||
for (const [key, entry] of Object.entries(providers)) {
|
for (const [key, entry] of Object.entries(providers)) {
|
||||||
if (existingIds.has(key)) continue;
|
if (existingIds.has(key)) continue;
|
||||||
|
|
||||||
const definition = getProviderDefinition(key);
|
const vendorId = inferProviderVendorIdFromOpenClawEntry(key, entry);
|
||||||
const isBuiltin = (BUILTIN_PROVIDER_TYPES as readonly string[]).includes(key);
|
const definition = getProviderDefinition(vendorId === 'custom' ? key : vendorId);
|
||||||
const vendorId = isBuiltin ? key : 'custom';
|
|
||||||
|
|
||||||
// Skip if an account with this vendorId already exists (e.g. user already
|
// Skip if an account with this vendorId already exists (e.g. user already
|
||||||
// created "openrouter-uuid" via UI — no need to import bare "openrouter").
|
// created "openrouter-uuid" via UI — no need to import bare "openrouter").
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
getProviderConfig,
|
getProviderConfig,
|
||||||
} from './provider-registry';
|
} from './provider-registry';
|
||||||
import {
|
import {
|
||||||
|
OPENCLAW_PROVIDER_KEY_MINIMAX,
|
||||||
OPENCLAW_PROVIDER_KEY_MOONSHOT,
|
OPENCLAW_PROVIDER_KEY_MOONSHOT,
|
||||||
OPENCLAW_PROVIDER_KEY_MOONSHOT_GLOBAL,
|
OPENCLAW_PROVIDER_KEY_MOONSHOT_GLOBAL,
|
||||||
isOAuthProviderType,
|
isOAuthProviderType,
|
||||||
@@ -29,9 +30,228 @@ import { withConfigLock } from './config-mutex';
|
|||||||
|
|
||||||
const AUTH_STORE_VERSION = 1;
|
const AUTH_STORE_VERSION = 1;
|
||||||
const AUTH_PROFILE_FILENAME = 'auth-profiles.json';
|
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 {
|
interface BundledPluginManifest {
|
||||||
return `${provider}-auth`;
|
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 ──────────────────────────────────────────────────────
|
// ── Helpers ──────────────────────────────────────────────────────
|
||||||
@@ -264,36 +484,19 @@ function expandProviderKeysForDeletion(provider: string): string[] {
|
|||||||
* Results are cached for the lifetime of the process since bundled
|
* Results are cached for the lifetime of the process since bundled
|
||||||
* extensions don't change at runtime.
|
* extensions don't change at runtime.
|
||||||
*/
|
*/
|
||||||
let _bundledPluginCache: { all: Set<string>; enabledByDefault: string[] } | null = null;
|
|
||||||
function discoverBundledPlugins(): { all: Set<string>; enabledByDefault: string[] } {
|
function discoverBundledPlugins(): { all: Set<string>; enabledByDefault: string[] } {
|
||||||
if (_bundledPluginCache) return _bundledPluginCache;
|
if (_bundledPluginCache) return _bundledPluginCache;
|
||||||
|
|
||||||
const all = new Set<string>();
|
const all = new Set<string>();
|
||||||
const enabledByDefault: string[] = [];
|
const enabledByDefault: string[] = [];
|
||||||
try {
|
|
||||||
const extensionsDir = join(getOpenClawResolvedDir(), 'dist', 'extensions');
|
for (const manifest of discoverBundledPluginManifests()) {
|
||||||
if (!existsSync(extensionsDir)) {
|
all.add(manifest.id);
|
||||||
_bundledPluginCache = { all, enabledByDefault };
|
if (manifest.enabledByDefault) {
|
||||||
return _bundledPluginCache;
|
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 };
|
_bundledPluginCache = { all, enabledByDefault };
|
||||||
return _bundledPluginCache;
|
return _bundledPluginCache;
|
||||||
}
|
}
|
||||||
@@ -556,14 +759,13 @@ export async function removeProviderFromOpenClaw(provider: string): Promise<void
|
|||||||
const config = await readOpenClawJson();
|
const config = await readOpenClawJson();
|
||||||
let modified = false;
|
let modified = false;
|
||||||
|
|
||||||
// Disable plugin (for OAuth like minimax-portal-auth)
|
// Remove plugin registrations for OAuth providers (e.g. MiniMax).
|
||||||
const plugins = config.plugins as Record<string, unknown> | undefined;
|
if (isOpenClawOAuthPluginProviderKey(provider)) {
|
||||||
const entries = (plugins?.entries ?? {}) as Record<string, Record<string, unknown>>;
|
const { canonicalPluginId, stalePluginIds } = getOAuthPluginRegistration(provider);
|
||||||
const pluginName = `${provider}-auth`;
|
if (removePluginRegistrations(config, [canonicalPluginId, ...stalePluginIds])) {
|
||||||
if (entries[pluginName]) {
|
modified = true;
|
||||||
entries[pluginName].enabled = false;
|
console.log(`Removed OpenClaw plugin registrations for provider "${provider}"`);
|
||||||
modified = true;
|
}
|
||||||
console.log(`Disabled OpenClaw plugin: ${pluginName}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove from models.providers
|
// Remove from models.providers
|
||||||
@@ -916,17 +1118,7 @@ export async function syncProviderConfigToOpenClaw(
|
|||||||
|
|
||||||
// Ensure extension is enabled for oauth providers to prevent gateway wiping config
|
// Ensure extension is enabled for oauth providers to prevent gateway wiping config
|
||||||
if (isOpenClawOAuthPluginProviderKey(provider)) {
|
if (isOpenClawOAuthPluginProviderKey(provider)) {
|
||||||
const plugins = (config.plugins || {}) as Record<string, unknown>;
|
ensureOAuthPluginEnabled(config, provider);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await writeOpenClawJson(config);
|
await writeOpenClawJson(config);
|
||||||
@@ -981,17 +1173,7 @@ export async function setOpenClawDefaultModelWithOverride(
|
|||||||
|
|
||||||
// Ensure the extension plugin is marked as enabled in openclaw.json
|
// Ensure the extension plugin is marked as enabled in openclaw.json
|
||||||
if (isOpenClawOAuthPluginProviderKey(provider)) {
|
if (isOpenClawOAuthPluginProviderKey(provider)) {
|
||||||
const plugins = (config.plugins || {}) as Record<string, unknown>;
|
ensureOAuthPluginEnabled(config, provider);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await writeOpenClawJson(config);
|
await writeOpenClawJson(config);
|
||||||
@@ -1659,6 +1841,34 @@ export async function sanitizeOpenClawConfig(): Promise<void> {
|
|||||||
pluginsObj.allow = allowArr;
|
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 ─────────────────────
|
// ── acpx legacy config/install cleanup ─────────────────────
|
||||||
// Older OpenClaw releases allowed plugins.entries.acpx.config.command
|
// Older OpenClaw releases allowed plugins.entries.acpx.config.command
|
||||||
// and expectedVersion overrides. Current bundled acpx schema rejects
|
// 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> {
|
async function writeOpenClawJson(config: unknown): Promise<void> {
|
||||||
const openclawDir = join(testHome, '.openclaw');
|
const openclawDir = join(testHome, '.openclaw');
|
||||||
await mkdir(openclawDir, { recursive: true });
|
await mkdir(openclawDir, { recursive: true });
|
||||||
@@ -494,6 +504,58 @@ describe('sanitizeOpenClawConfig', () => {
|
|||||||
expect(dingtalk.clientId).toBe('dt-client-id');
|
expect(dingtalk.clientId).toBe('dt-client-id');
|
||||||
expect(dingtalk.clientSecret).toBe('dt-secret');
|
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', () => {
|
describe('syncProviderConfigToOpenClaw', () => {
|
||||||
@@ -504,6 +566,100 @@ describe('syncProviderConfigToOpenClaw', () => {
|
|||||||
await rm(testUserData, { recursive: true, force: true });
|
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 () => {
|
it('writes moonshot web search config to plugin config instead of tools.web.search.kimi', async () => {
|
||||||
await writeOpenClawJson({
|
await writeOpenClawJson({
|
||||||
models: {
|
models: {
|
||||||
@@ -719,4 +875,104 @@ describe('auth-backed provider discovery', () => {
|
|||||||
expect(result.providers).toEqual({});
|
expect(result.providers).toEqual({});
|
||||||
await expect(getActiveOpenClawProviders()).resolves.toEqual(new Set());
|
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');
|
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 () => {
|
it('seeds builtin providers discovered from auth profiles without explicit models.providers entries', async () => {
|
||||||
mocks.listProviderAccounts.mockResolvedValue([]);
|
mocks.listProviderAccounts.mockResolvedValue([]);
|
||||||
mocks.getActiveOpenClawProviders.mockResolvedValue(new Set(['openai', 'anthropic']));
|
mocks.getActiveOpenClawProviders.mockResolvedValue(new Set(['openai', 'anthropic']));
|
||||||
|
|||||||
Reference in New Issue
Block a user