Fix/moonshot cn web search domain (#338)

This commit is contained in:
Lingxuan Zuo
2026-03-08 00:00:47 +08:00
committed by GitHub
Unverified
parent b41a8eedd9
commit c03d92e9a2
10 changed files with 318 additions and 74 deletions

View File

@@ -17,6 +17,11 @@ import {
getProviderDefaultModel,
getProviderConfig,
} from './provider-registry';
import {
OPENCLAW_PROVIDER_KEY_MOONSHOT,
isOAuthProviderType,
isOpenClawOAuthPluginProviderKey,
} from './provider-keys';
const AUTH_STORE_VERSION = 1;
const AUTH_PROFILE_FILENAME = 'auth-profiles.json';
@@ -207,8 +212,7 @@ export async function saveProviderKeyToOpenClaw(
apiKey: string,
agentId?: string
): Promise<void> {
const OAUTH_PROVIDERS = ['qwen-portal', 'minimax-portal', 'minimax-portal-cn'];
if (OAUTH_PROVIDERS.includes(provider) && !apiKey) {
if (isOAuthProviderType(provider) && !apiKey) {
console.log(`Skipping auth-profiles write for OAuth provider "${provider}" (no API key provided, using OAuth)`);
return;
}
@@ -242,8 +246,7 @@ export async function removeProviderKeyFromOpenClaw(
provider: string,
agentId?: string
): Promise<void> {
const OAUTH_PROVIDERS = ['qwen-portal', 'minimax-portal', 'minimax-portal-cn'];
if (OAUTH_PROVIDERS.includes(provider)) {
if (isOAuthProviderType(provider)) {
console.log(`Skipping auth-profiles removal for OAuth provider "${provider}" (managed by OpenClaw plugin)`);
return;
}
@@ -364,6 +367,7 @@ export async function setOpenClawDefaultModel(
fallbackModels: string[] = []
): Promise<void> {
const config = await readOpenClawJson();
ensureMoonshotKimiWebSearchCnBaseUrl(config, provider);
const model = modelOverride || getProviderDefaultModel(provider);
if (!model) {
@@ -393,6 +397,7 @@ export async function setOpenClawDefaultModel(
if (providerCfg) {
const models = (config.models || {}) as Record<string, unknown>;
const providers = (models.providers || {}) as Record<string, unknown>;
const removedLegacyMoonshot = removeLegacyMoonshotProviderEntry(provider, providers);
const existingProvider =
providers[provider] && typeof providers[provider] === 'object'
@@ -429,6 +434,9 @@ export async function setOpenClawDefaultModel(
}
providers[provider] = providerEntry;
console.log(`Configured models.providers.${provider} with baseUrl=${providerCfg.baseUrl}, model=${modelId}`);
if (removedLegacyMoonshot) {
console.log('Removed legacy models.providers.moonshot alias entry');
}
models.providers = providers;
config.models = models;
@@ -461,6 +469,32 @@ interface RuntimeProviderConfigOverride {
authHeader?: boolean;
}
function removeLegacyMoonshotProviderEntry(
_provider: string,
_providers: Record<string, unknown>
): boolean {
return false;
}
function ensureMoonshotKimiWebSearchCnBaseUrl(config: Record<string, unknown>, provider: string): void {
if (provider !== OPENCLAW_PROVIDER_KEY_MOONSHOT) return;
const tools = (config.tools || {}) as Record<string, unknown>;
const web = (tools.web || {}) as Record<string, unknown>;
const search = (web.search || {}) as Record<string, unknown>;
const kimi = (search.kimi && typeof search.kimi === 'object' && !Array.isArray(search.kimi))
? (search.kimi as Record<string, unknown>)
: {};
// Prefer env/auth-profiles for key resolution; stale inline kimi.apiKey can cause persistent 401.
delete kimi.apiKey;
kimi.baseUrl = 'https://api.moonshot.cn/v1';
search.kimi = kimi;
web.search = search;
tools.web = web;
config.tools = tools;
}
/**
* Register or update a provider's configuration in openclaw.json
* without changing the current default model.
@@ -471,10 +505,12 @@ export async function syncProviderConfigToOpenClaw(
override: RuntimeProviderConfigOverride
): Promise<void> {
const config = await readOpenClawJson();
ensureMoonshotKimiWebSearchCnBaseUrl(config, provider);
if (override.baseUrl && override.api) {
const models = (config.models || {}) as Record<string, unknown>;
const providers = (models.providers || {}) as Record<string, unknown>;
removeLegacyMoonshotProviderEntry(provider, providers);
const nextModels: Array<Record<string, unknown>> = [];
if (modelId) nextModels.push({ id: modelId, name: modelId });
@@ -495,7 +531,7 @@ export async function syncProviderConfigToOpenClaw(
}
// Ensure extension is enabled for oauth providers to prevent gateway wiping config
if (provider === 'minimax-portal' || provider === 'qwen-portal') {
if (isOpenClawOAuthPluginProviderKey(provider)) {
const plugins = (config.plugins || {}) as Record<string, unknown>;
const pEntries = (plugins.entries || {}) as Record<string, unknown>;
pEntries[`${provider}-auth`] = { enabled: true };
@@ -516,6 +552,7 @@ export async function setOpenClawDefaultModelWithOverride(
fallbackModels: string[] = []
): Promise<void> {
const config = await readOpenClawJson();
ensureMoonshotKimiWebSearchCnBaseUrl(config, provider);
const model = modelOverride || getProviderDefaultModel(provider);
if (!model) {
@@ -542,6 +579,7 @@ export async function setOpenClawDefaultModelWithOverride(
if (override.baseUrl && override.api) {
const models = (config.models || {}) as Record<string, unknown>;
const providers = (models.providers || {}) as Record<string, unknown>;
removeLegacyMoonshotProviderEntry(provider, providers);
const nextModels: Array<Record<string, unknown>> = [];
for (const candidateModelId of [modelId, ...fallbackModelIds]) {
@@ -573,7 +611,7 @@ export async function setOpenClawDefaultModelWithOverride(
config.gateway = gateway;
// Ensure the extension plugin is marked as enabled in openclaw.json
if (provider === 'minimax-portal' || provider === 'qwen-portal') {
if (isOpenClawOAuthPluginProviderKey(provider)) {
const plugins = (config.plugins || {}) as Record<string, unknown>;
const pEntries = (plugins.entries || {}) as Record<string, unknown>;
pEntries[`${provider}-auth`] = { enabled: true };
@@ -781,6 +819,28 @@ export async function sanitizeOpenClawConfig(): Promise<void> {
}
}
// ── tools.web.search.kimi ─────────────────────────────────────
// OpenClaw web_search(kimi) prioritizes tools.web.search.kimi.apiKey over
// environment/auth-profiles. A stale inline key can cause persistent 401s.
// When ClawX-managed moonshot provider exists, prefer centralized key
// resolution and strip the inline key.
const providers = ((config.models as Record<string, unknown> | undefined)?.providers as Record<string, unknown> | undefined) || {};
if (providers[OPENCLAW_PROVIDER_KEY_MOONSHOT]) {
const tools = (config.tools as Record<string, unknown> | undefined) || {};
const web = (tools.web as Record<string, unknown> | undefined) || {};
const search = (web.search as Record<string, unknown> | undefined) || {};
const kimi = (search.kimi as Record<string, unknown> | undefined) || {};
if ('apiKey' in kimi) {
console.log('[sanitize] Removing stale key "tools.web.search.kimi.apiKey" from openclaw.json');
delete kimi.apiKey;
search.kimi = kimi;
web.search = search;
tools.web = web;
config.tools = tools;
modified = true;
}
}
if (modified) {
await writeOpenClawJson(config);
console.log('[sanitize] openclaw.json sanitized successfully');

View File

@@ -0,0 +1,73 @@
const MULTI_INSTANCE_PROVIDER_TYPES = new Set(['custom', 'ollama']);
export const OPENCLAW_PROVIDER_KEY_MINIMAX = 'minimax-portal';
export const OPENCLAW_PROVIDER_KEY_QWEN = 'qwen-portal';
export const OPENCLAW_PROVIDER_KEY_MOONSHOT = 'moonshot';
export const OAUTH_PROVIDER_TYPES = ['qwen-portal', 'minimax-portal', 'minimax-portal-cn'] as const;
export const OPENCLAW_OAUTH_PLUGIN_PROVIDER_KEYS = [
OPENCLAW_PROVIDER_KEY_MINIMAX,
OPENCLAW_PROVIDER_KEY_QWEN,
] as const;
const OAUTH_PROVIDER_TYPE_SET = new Set<string>(OAUTH_PROVIDER_TYPES);
const OPENCLAW_OAUTH_PLUGIN_PROVIDER_KEY_SET = new Set<string>(OPENCLAW_OAUTH_PLUGIN_PROVIDER_KEYS);
const PROVIDER_KEY_ALIASES: Record<string, string> = {
'minimax-portal-cn': OPENCLAW_PROVIDER_KEY_MINIMAX,
};
export function getOpenClawProviderKeyForType(type: string, providerId: string): string {
if (MULTI_INSTANCE_PROVIDER_TYPES.has(type)) {
const suffix = providerId.replace(/-/g, '').slice(0, 8);
return `${type}-${suffix}`;
}
return PROVIDER_KEY_ALIASES[type] ?? type;
}
export function isOAuthProviderType(type: string): boolean {
return OAUTH_PROVIDER_TYPE_SET.has(type);
}
export function isMiniMaxProviderType(type: string): boolean {
return type === OPENCLAW_PROVIDER_KEY_MINIMAX || type === 'minimax-portal-cn';
}
export function getOAuthProviderTargetKey(type: string): string | undefined {
if (!isOAuthProviderType(type)) return undefined;
return isMiniMaxProviderType(type) ? OPENCLAW_PROVIDER_KEY_MINIMAX : OPENCLAW_PROVIDER_KEY_QWEN;
}
export function getOAuthProviderApi(type: string): 'anthropic-messages' | 'openai-completions' | undefined {
if (!isOAuthProviderType(type)) return undefined;
return isMiniMaxProviderType(type) ? 'anthropic-messages' : 'openai-completions';
}
export function getOAuthProviderDefaultBaseUrl(type: string): string | undefined {
if (!isOAuthProviderType(type)) return undefined;
if (type === OPENCLAW_PROVIDER_KEY_MINIMAX) return 'https://api.minimax.io/anthropic';
if (type === 'minimax-portal-cn') return 'https://api.minimaxi.com/anthropic';
return 'https://portal.qwen.ai/v1';
}
export function normalizeOAuthBaseUrl(type: string, baseUrl?: string): string | undefined {
if (!baseUrl) return undefined;
if (isMiniMaxProviderType(type)) {
return baseUrl.replace(/\/v1$/, '').replace(/\/anthropic$/, '').replace(/\/$/, '') + '/anthropic';
}
return baseUrl;
}
export function usesOAuthAuthHeader(providerKey: string): boolean {
return providerKey === OPENCLAW_PROVIDER_KEY_MINIMAX;
}
export function getOAuthApiKeyEnv(providerKey: string): string | undefined {
if (providerKey === OPENCLAW_PROVIDER_KEY_MINIMAX) return 'minimax-oauth';
if (providerKey === OPENCLAW_PROVIDER_KEY_QWEN) return 'qwen-oauth';
return undefined;
}
export function isOpenClawOAuthPluginProviderKey(provider: string): boolean {
return OPENCLAW_OAUTH_PLUGIN_PROVIDER_KEY_SET.has(provider);
}

View File

@@ -154,6 +154,13 @@ export function getProviderEnvVar(type: string): string | undefined {
return REGISTRY[type]?.envVar;
}
/** Get all environment variable names for a provider type (primary first). */
export function getProviderEnvVars(type: string): string[] {
const meta = REGISTRY[type];
if (!meta?.envVar) return [];
return [meta.envVar];
}
/** Get the default model string for a provider type */
export function getProviderDefaultModel(type: string): string | undefined {
return REGISTRY[type]?.defaultModel;

View File

@@ -6,6 +6,7 @@
import { BUILTIN_PROVIDER_TYPES, type ProviderType } from './provider-registry';
import { getActiveOpenClawProviders } from './openclaw-auth';
import { getOpenClawProviderKeyForType } from './provider-keys';
// Lazy-load electron-store (ESM module)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -228,9 +229,7 @@ export async function getAllProvidersWithKeyInfo(): Promise<
// e.g. provider.id "custom-a1b2c3d4-..." → strip hyphens → "customa1b2c3d4..." → slice(0,8) → "customa1"
// → openClawKey = "custom-customa1"
// This must match getOpenClawProviderKey() in ipc-handlers.ts exactly.
const openClawKey = (provider.type === 'custom' || provider.type === 'ollama')
? `${provider.type}-${provider.id.replace(/-/g, '').slice(0, 8)}`
: provider.type === 'minimax-portal-cn' ? 'minimax-portal' : provider.type;
const openClawKey = getOpenClawProviderKeyForType(provider.type, provider.id);
if (!isBuiltin && !activeOpenClawProviders.has(provider.type) && !activeOpenClawProviders.has(provider.id) && !activeOpenClawProviders.has(openClawKey)) {
console.log(`[Sync] Provider ${provider.id} (${provider.type}) missing from OpenClaw, dropping from ClawX UI`);
await deleteProvider(provider.id);