Fix/moonshot cn web search domain (#338)
This commit is contained in:
committed by
GitHub
Unverified
parent
b41a8eedd9
commit
c03d92e9a2
@@ -154,6 +154,9 @@ When you launch ClawX for the first time, the **Setup Wizard** will guide you th
|
|||||||
3. **Skill Bundles** – Select pre-configured skills for common use cases
|
3. **Skill Bundles** – Select pre-configured skills for common use cases
|
||||||
4. **Verification** – Test your configuration before entering the main interface
|
4. **Verification** – Test your configuration before entering the main interface
|
||||||
|
|
||||||
|
> Note for Moonshot (Kimi): ClawX keeps Kimi web search enabled by default.
|
||||||
|
> When Moonshot is configured, ClawX also syncs Kimi web search to the China endpoint (`https://api.moonshot.cn/v1`) in OpenClaw config.
|
||||||
|
|
||||||
### Proxy Settings
|
### Proxy Settings
|
||||||
|
|
||||||
ClawX includes built-in proxy settings for environments where Electron, the OpenClaw Gateway, or channels such as Telegram need to reach the internet through a local proxy client.
|
ClawX includes built-in proxy settings for environments where Electron, the OpenClaw Gateway, or channels such as Telegram need to reach the internet through a local proxy client.
|
||||||
|
|||||||
@@ -155,6 +155,9 @@ pnpm dev
|
|||||||
3. **技能包** – 选择适用于常见场景的预配置技能
|
3. **技能包** – 选择适用于常见场景的预配置技能
|
||||||
4. **验证** – 在进入主界面前测试你的配置
|
4. **验证** – 在进入主界面前测试你的配置
|
||||||
|
|
||||||
|
> Moonshot(Kimi)说明:ClawX 默认保持开启 Kimi 的 web search。
|
||||||
|
> 当配置 Moonshot 后,ClawX 也会将 OpenClaw 配置中的 Kimi web search 同步到中国区端点(`https://api.moonshot.cn/v1`)。
|
||||||
|
|
||||||
### 代理设置
|
### 代理设置
|
||||||
|
|
||||||
ClawX 内置了代理设置,适用于需要通过本地代理客户端访问外网的场景,包括 Electron 本身、OpenClaw Gateway,以及 Telegram 这类频道的联网请求。
|
ClawX 内置了代理设置,适用于需要通过本地代理客户端访问外网的场景,包括 Electron 本身、OpenClaw Gateway,以及 Telegram 这类频道的联网请求。
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import {
|
|||||||
} from '../utils/paths';
|
} from '../utils/paths';
|
||||||
import { getAllSettings, getSetting } from '../utils/store';
|
import { getAllSettings, getSetting } from '../utils/store';
|
||||||
import { getApiKey, getDefaultProvider, getProvider } from '../utils/secure-storage';
|
import { getApiKey, getDefaultProvider, getProvider } from '../utils/secure-storage';
|
||||||
import { getProviderEnvVar, getKeyableProviderTypes } from '../utils/provider-registry';
|
import { getProviderEnvVars, getKeyableProviderTypes } from '../utils/provider-registry';
|
||||||
import { GatewayEventType, JsonRpcNotification, isNotification, isResponse } from './protocol';
|
import { GatewayEventType, JsonRpcNotification, isNotification, isResponse } from './protocol';
|
||||||
import { logger } from '../utils/logger';
|
import { logger } from '../utils/logger';
|
||||||
import { getUvMirrorEnv } from '../utils/uv-env';
|
import { getUvMirrorEnv } from '../utils/uv-env';
|
||||||
@@ -181,6 +181,14 @@ const GATEWAY_FETCH_PRELOAD_SOURCE = `'use strict';
|
|||||||
})();
|
})();
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
function injectMoonshotWebSearchEnv(
|
||||||
|
env: Record<string, string>,
|
||||||
|
apiKey: string
|
||||||
|
): void {
|
||||||
|
// OpenClaw web_search(kimi) reads KIMI_API_KEY before provider-specific config.
|
||||||
|
env.KIMI_API_KEY = apiKey;
|
||||||
|
}
|
||||||
|
|
||||||
function ensureGatewayFetchPreload(): string {
|
function ensureGatewayFetchPreload(): string {
|
||||||
const dest = path.join(app.getPath('userData'), 'gateway-fetch-preload.cjs');
|
const dest = path.join(app.getPath('userData'), 'gateway-fetch-preload.cjs');
|
||||||
try { writeFileSync(dest, GATEWAY_FETCH_PRELOAD_SOURCE, 'utf-8'); } catch { /* best-effort */ }
|
try { writeFileSync(dest, GATEWAY_FETCH_PRELOAD_SOURCE, 'utf-8'); } catch { /* best-effort */ }
|
||||||
@@ -1146,9 +1154,14 @@ export class GatewayManager extends EventEmitter {
|
|||||||
const defaultProviderType = defaultProvider?.type;
|
const defaultProviderType = defaultProvider?.type;
|
||||||
const defaultProviderKey = await getApiKey(defaultProviderId);
|
const defaultProviderKey = await getApiKey(defaultProviderId);
|
||||||
if (defaultProviderType && defaultProviderKey) {
|
if (defaultProviderType && defaultProviderKey) {
|
||||||
const envVar = getProviderEnvVar(defaultProviderType);
|
const envVars = getProviderEnvVars(defaultProviderType);
|
||||||
if (envVar) {
|
if (envVars.length > 0) {
|
||||||
providerEnv[envVar] = defaultProviderKey;
|
for (const envVar of envVars) {
|
||||||
|
providerEnv[envVar] = defaultProviderKey;
|
||||||
|
}
|
||||||
|
if (defaultProviderType === 'moonshot') {
|
||||||
|
injectMoonshotWebSearchEnv(providerEnv, defaultProviderKey);
|
||||||
|
}
|
||||||
loadedProviderKeyCount++;
|
loadedProviderKeyCount++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1161,9 +1174,14 @@ export class GatewayManager extends EventEmitter {
|
|||||||
try {
|
try {
|
||||||
const key = await getApiKey(providerType);
|
const key = await getApiKey(providerType);
|
||||||
if (key) {
|
if (key) {
|
||||||
const envVar = getProviderEnvVar(providerType);
|
const envVars = getProviderEnvVars(providerType);
|
||||||
if (envVar) {
|
if (envVars.length > 0) {
|
||||||
providerEnv[envVar] = key;
|
for (const envVar of envVars) {
|
||||||
|
providerEnv[envVar] = key;
|
||||||
|
}
|
||||||
|
if (providerType === 'moonshot') {
|
||||||
|
injectMoonshotWebSearchEnv(providerEnv, key);
|
||||||
|
}
|
||||||
loadedProviderKeyCount++;
|
loadedProviderKeyCount++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,25 +54,27 @@ import { deviceOAuthManager, OAuthProviderType } from '../utils/device-oauth';
|
|||||||
import { applyProxySettings } from './proxy';
|
import { applyProxySettings } from './proxy';
|
||||||
import { proxyAwareFetch } from '../utils/proxy-fetch';
|
import { proxyAwareFetch } from '../utils/proxy-fetch';
|
||||||
import { getRecentTokenUsageHistory } from '../utils/token-usage';
|
import { getRecentTokenUsageHistory } from '../utils/token-usage';
|
||||||
|
import {
|
||||||
|
getOpenClawProviderKeyForType,
|
||||||
|
getOAuthApiKeyEnv,
|
||||||
|
getOAuthProviderApi,
|
||||||
|
getOAuthProviderDefaultBaseUrl,
|
||||||
|
getOAuthProviderTargetKey,
|
||||||
|
isOAuthProviderType,
|
||||||
|
normalizeOAuthBaseUrl,
|
||||||
|
usesOAuthAuthHeader,
|
||||||
|
} from '../utils/provider-keys';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* For custom/ollama providers, derive a unique key for OpenClaw config files
|
* Derive OpenClaw provider key used in openclaw.json / models.json.
|
||||||
* so that multiple instances of the same type don't overwrite each other.
|
* Some types need remapping to avoid collisions or enforce CN endpoints.
|
||||||
* For all other providers the key is simply the provider type.
|
|
||||||
*
|
*
|
||||||
* @param type - Provider type (e.g. 'custom', 'ollama', 'openrouter')
|
* @param type - Provider type (e.g. 'custom', 'ollama', 'openrouter')
|
||||||
* @param providerId - Unique provider ID from secure-storage (UUID-like)
|
* @param providerId - Unique provider ID from secure-storage (UUID-like)
|
||||||
* @returns A string like 'custom-a1b2c3d4' or 'openrouter'
|
* @returns A key like 'custom-a1b2c3d4', 'moonshot', or 'openrouter'
|
||||||
*/
|
*/
|
||||||
export function getOpenClawProviderKey(type: string, providerId: string): string {
|
export function getOpenClawProviderKey(type: string, providerId: string): string {
|
||||||
if (type === 'custom' || type === 'ollama') {
|
return getOpenClawProviderKeyForType(type, providerId);
|
||||||
const suffix = providerId.replace(/-/g, '').slice(0, 8);
|
|
||||||
return `${type}-${suffix}`;
|
|
||||||
}
|
|
||||||
if (type === 'minimax-portal-cn') {
|
|
||||||
return 'minimax-portal';
|
|
||||||
}
|
|
||||||
return type;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getProviderModelRef(config: ProviderConfig): string | undefined {
|
function getProviderModelRef(config: ProviderConfig): string | undefined {
|
||||||
@@ -84,7 +86,10 @@ function getProviderModelRef(config: ProviderConfig): string | undefined {
|
|||||||
: `${providerKey}/${config.model}`;
|
: `${providerKey}/${config.model}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return getProviderDefaultModel(config.type);
|
const defaultModel = getProviderDefaultModel(config.type);
|
||||||
|
if (!defaultModel) return undefined;
|
||||||
|
const modelId = defaultModel.includes('/') ? defaultModel.split('/').slice(1).join('/') : defaultModel;
|
||||||
|
return `${providerKey}/${modelId}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getProviderFallbackModelRefs(config: ProviderConfig): Promise<string[]> {
|
async function getProviderFallbackModelRefs(config: ProviderConfig): Promise<string[]> {
|
||||||
@@ -1201,16 +1206,20 @@ function registerProviderHandlers(gatewayManager: GatewayManager): void {
|
|||||||
// If this provider is the current default, update the primary model
|
// If this provider is the current default, update the primary model
|
||||||
const defaultProviderId = await getDefaultProvider();
|
const defaultProviderId = await getDefaultProvider();
|
||||||
if (defaultProviderId === providerId) {
|
if (defaultProviderId === providerId) {
|
||||||
const modelOverride = nextConfig.model
|
const modelOverride = getProviderModelRef(nextConfig);
|
||||||
? `${ock}/${nextConfig.model}`
|
const providerKeyIsAliased = ock !== nextConfig.type;
|
||||||
: undefined;
|
if (nextConfig.type === 'custom' || nextConfig.type === 'ollama' || providerKeyIsAliased) {
|
||||||
if (nextConfig.type !== 'custom' && nextConfig.type !== 'ollama') {
|
const baseMeta = getProviderConfig(nextConfig.type);
|
||||||
await setOpenClawDefaultModel(ock, modelOverride, fallbackModels);
|
|
||||||
} else {
|
|
||||||
await setOpenClawDefaultModelWithOverride(ock, modelOverride, {
|
await setOpenClawDefaultModelWithOverride(ock, modelOverride, {
|
||||||
baseUrl: nextConfig.baseUrl,
|
baseUrl: nextConfig.baseUrl || baseMeta?.baseUrl,
|
||||||
api: 'openai-completions',
|
api: nextConfig.type === 'custom' || nextConfig.type === 'ollama'
|
||||||
|
? 'openai-completions'
|
||||||
|
: baseMeta?.api,
|
||||||
|
apiKeyEnv: baseMeta?.apiKeyEnv,
|
||||||
|
headers: baseMeta?.headers,
|
||||||
}, fallbackModels);
|
}, fallbackModels);
|
||||||
|
} else {
|
||||||
|
await setOpenClawDefaultModel(ock, modelOverride, fallbackModels);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1288,23 +1297,23 @@ function registerProviderHandlers(gatewayManager: GatewayManager): void {
|
|||||||
const providerKey = await getApiKey(providerId);
|
const providerKey = await getApiKey(providerId);
|
||||||
const fallbackModels = await getProviderFallbackModelRefs(provider);
|
const fallbackModels = await getProviderFallbackModelRefs(provider);
|
||||||
|
|
||||||
// OAuth providers (qwen-portal, minimax-portal, minimax-portal-cn) might use OAuth OR a direct API key.
|
// OAuth providers might use OAuth OR a direct API key.
|
||||||
// Treat them as OAuth only if they don't have a local API key configured.
|
// Treat them as OAuth-only if they don't have a local API key configured.
|
||||||
const OAUTH_PROVIDER_TYPES = ['qwen-portal', 'minimax-portal', 'minimax-portal-cn'];
|
const isOAuthProvider = isOAuthProviderType(provider.type) && !providerKey;
|
||||||
const isOAuthProvider = OAUTH_PROVIDER_TYPES.includes(provider.type) && !providerKey;
|
|
||||||
|
|
||||||
if (!isOAuthProvider) {
|
if (!isOAuthProvider) {
|
||||||
// Build the full model string: "openclawKey/modelId"
|
// Build the model reference from provider settings/default mapping.
|
||||||
const modelOverride = provider.model
|
const modelOverride = getProviderModelRef(provider);
|
||||||
? (provider.model.startsWith(`${ock}/`)
|
const providerKeyIsAliased = ock !== provider.type;
|
||||||
? provider.model
|
if (provider.type === 'custom' || provider.type === 'ollama' || providerKeyIsAliased) {
|
||||||
: `${ock}/${provider.model}`)
|
const baseMeta = getProviderConfig(provider.type);
|
||||||
: undefined;
|
|
||||||
|
|
||||||
if (provider.type === 'custom' || provider.type === 'ollama') {
|
|
||||||
await setOpenClawDefaultModelWithOverride(ock, modelOverride, {
|
await setOpenClawDefaultModelWithOverride(ock, modelOverride, {
|
||||||
baseUrl: provider.baseUrl,
|
baseUrl: provider.baseUrl || baseMeta?.baseUrl,
|
||||||
api: 'openai-completions',
|
api: provider.type === 'custom' || provider.type === 'ollama'
|
||||||
|
? 'openai-completions'
|
||||||
|
: baseMeta?.api,
|
||||||
|
apiKeyEnv: baseMeta?.apiKeyEnv,
|
||||||
|
headers: baseMeta?.headers,
|
||||||
}, fallbackModels);
|
}, fallbackModels);
|
||||||
} else {
|
} else {
|
||||||
await setOpenClawDefaultModel(ock, modelOverride, fallbackModels);
|
await setOpenClawDefaultModel(ock, modelOverride, fallbackModels);
|
||||||
@@ -1315,32 +1324,22 @@ function registerProviderHandlers(gatewayManager: GatewayManager): void {
|
|||||||
await saveProviderKeyToOpenClaw(ock, providerKey);
|
await saveProviderKeyToOpenClaw(ock, providerKey);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// OAuth providers (minimax-portal, minimax-portal-cn, qwen-portal)
|
const defaultBaseUrl = getOAuthProviderDefaultBaseUrl(provider.type);
|
||||||
const defaultBaseUrl = provider.type === 'minimax-portal'
|
const api = getOAuthProviderApi(provider.type);
|
||||||
? 'https://api.minimax.io/anthropic'
|
const targetProviderKey = getOAuthProviderTargetKey(provider.type);
|
||||||
: (provider.type === 'minimax-portal-cn' ? 'https://api.minimaxi.com/anthropic' : 'https://portal.qwen.ai/v1');
|
const baseUrl = normalizeOAuthBaseUrl(provider.type, provider.baseUrl || defaultBaseUrl);
|
||||||
const api: 'anthropic-messages' | 'openai-completions' =
|
const oauthApiKeyEnv = targetProviderKey ? getOAuthApiKeyEnv(targetProviderKey) : undefined;
|
||||||
(provider.type === 'minimax-portal' || provider.type === 'minimax-portal-cn')
|
const oauthUsesAuthHeader = targetProviderKey ? usesOAuthAuthHeader(targetProviderKey) : false;
|
||||||
? 'anthropic-messages'
|
if (!baseUrl || !api || !targetProviderKey || !oauthApiKeyEnv) {
|
||||||
: 'openai-completions';
|
throw new Error(`Invalid OAuth provider config for "${provider.type}"`);
|
||||||
|
|
||||||
let baseUrl = provider.baseUrl || defaultBaseUrl;
|
|
||||||
if ((provider.type === 'minimax-portal' || provider.type === 'minimax-portal-cn') && baseUrl) {
|
|
||||||
baseUrl = baseUrl.replace(/\/v1$/, '').replace(/\/anthropic$/, '').replace(/\/$/, '') + '/anthropic';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// To ensure the OpenClaw Gateway's internal token refresher works,
|
|
||||||
// we must save the CN provider under the "minimax-portal" key in openclaw.json
|
|
||||||
const targetProviderKey = (provider.type === 'minimax-portal' || provider.type === 'minimax-portal-cn')
|
|
||||||
? 'minimax-portal'
|
|
||||||
: provider.type;
|
|
||||||
|
|
||||||
await setOpenClawDefaultModelWithOverride(targetProviderKey, getProviderModelRef(provider), {
|
await setOpenClawDefaultModelWithOverride(targetProviderKey, getProviderModelRef(provider), {
|
||||||
baseUrl,
|
baseUrl,
|
||||||
api,
|
api,
|
||||||
authHeader: targetProviderKey === 'minimax-portal' ? true : undefined,
|
authHeader: oauthUsesAuthHeader ? true : undefined,
|
||||||
// Relies on OpenClaw Gateway native auth-profiles syncing
|
// Relies on OpenClaw Gateway native auth-profiles syncing
|
||||||
apiKeyEnv: targetProviderKey === 'minimax-portal' ? 'minimax-oauth' : 'qwen-oauth',
|
apiKeyEnv: oauthApiKeyEnv,
|
||||||
}, fallbackModels);
|
}, fallbackModels);
|
||||||
|
|
||||||
logger.info(`Configured openclaw.json for OAuth provider "${provider.type}"`);
|
logger.info(`Configured openclaw.json for OAuth provider "${provider.type}"`);
|
||||||
@@ -1352,8 +1351,8 @@ function registerProviderHandlers(gatewayManager: GatewayManager): void {
|
|||||||
await updateAgentModelProvider(targetProviderKey, {
|
await updateAgentModelProvider(targetProviderKey, {
|
||||||
baseUrl,
|
baseUrl,
|
||||||
api,
|
api,
|
||||||
authHeader: targetProviderKey === 'minimax-portal' ? true : undefined,
|
authHeader: oauthUsesAuthHeader ? true : undefined,
|
||||||
apiKey: targetProviderKey === 'minimax-portal' ? 'minimax-oauth' : 'qwen-oauth',
|
apiKey: oauthApiKeyEnv,
|
||||||
models: defaultModelId ? [{ id: defaultModelId, name: defaultModelId }] : [],
|
models: defaultModelId ? [{ id: defaultModelId, name: defaultModelId }] : [],
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -2264,4 +2263,3 @@ function registerSessionHandlers(): void {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,11 @@ import {
|
|||||||
getProviderDefaultModel,
|
getProviderDefaultModel,
|
||||||
getProviderConfig,
|
getProviderConfig,
|
||||||
} from './provider-registry';
|
} from './provider-registry';
|
||||||
|
import {
|
||||||
|
OPENCLAW_PROVIDER_KEY_MOONSHOT,
|
||||||
|
isOAuthProviderType,
|
||||||
|
isOpenClawOAuthPluginProviderKey,
|
||||||
|
} from './provider-keys';
|
||||||
|
|
||||||
const AUTH_STORE_VERSION = 1;
|
const AUTH_STORE_VERSION = 1;
|
||||||
const AUTH_PROFILE_FILENAME = 'auth-profiles.json';
|
const AUTH_PROFILE_FILENAME = 'auth-profiles.json';
|
||||||
@@ -207,8 +212,7 @@ export async function saveProviderKeyToOpenClaw(
|
|||||||
apiKey: string,
|
apiKey: string,
|
||||||
agentId?: string
|
agentId?: string
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const OAUTH_PROVIDERS = ['qwen-portal', 'minimax-portal', 'minimax-portal-cn'];
|
if (isOAuthProviderType(provider) && !apiKey) {
|
||||||
if (OAUTH_PROVIDERS.includes(provider) && !apiKey) {
|
|
||||||
console.log(`Skipping auth-profiles write for OAuth provider "${provider}" (no API key provided, using OAuth)`);
|
console.log(`Skipping auth-profiles write for OAuth provider "${provider}" (no API key provided, using OAuth)`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -242,8 +246,7 @@ export async function removeProviderKeyFromOpenClaw(
|
|||||||
provider: string,
|
provider: string,
|
||||||
agentId?: string
|
agentId?: string
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const OAUTH_PROVIDERS = ['qwen-portal', 'minimax-portal', 'minimax-portal-cn'];
|
if (isOAuthProviderType(provider)) {
|
||||||
if (OAUTH_PROVIDERS.includes(provider)) {
|
|
||||||
console.log(`Skipping auth-profiles removal for OAuth provider "${provider}" (managed by OpenClaw plugin)`);
|
console.log(`Skipping auth-profiles removal for OAuth provider "${provider}" (managed by OpenClaw plugin)`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -364,6 +367,7 @@ export async function setOpenClawDefaultModel(
|
|||||||
fallbackModels: string[] = []
|
fallbackModels: string[] = []
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const config = await readOpenClawJson();
|
const config = await readOpenClawJson();
|
||||||
|
ensureMoonshotKimiWebSearchCnBaseUrl(config, provider);
|
||||||
|
|
||||||
const model = modelOverride || getProviderDefaultModel(provider);
|
const model = modelOverride || getProviderDefaultModel(provider);
|
||||||
if (!model) {
|
if (!model) {
|
||||||
@@ -393,6 +397,7 @@ export async function setOpenClawDefaultModel(
|
|||||||
if (providerCfg) {
|
if (providerCfg) {
|
||||||
const models = (config.models || {}) as Record<string, unknown>;
|
const models = (config.models || {}) as Record<string, unknown>;
|
||||||
const providers = (models.providers || {}) as Record<string, unknown>;
|
const providers = (models.providers || {}) as Record<string, unknown>;
|
||||||
|
const removedLegacyMoonshot = removeLegacyMoonshotProviderEntry(provider, providers);
|
||||||
|
|
||||||
const existingProvider =
|
const existingProvider =
|
||||||
providers[provider] && typeof providers[provider] === 'object'
|
providers[provider] && typeof providers[provider] === 'object'
|
||||||
@@ -429,6 +434,9 @@ export async function setOpenClawDefaultModel(
|
|||||||
}
|
}
|
||||||
providers[provider] = providerEntry;
|
providers[provider] = providerEntry;
|
||||||
console.log(`Configured models.providers.${provider} with baseUrl=${providerCfg.baseUrl}, model=${modelId}`);
|
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;
|
models.providers = providers;
|
||||||
config.models = models;
|
config.models = models;
|
||||||
@@ -461,6 +469,32 @@ interface RuntimeProviderConfigOverride {
|
|||||||
authHeader?: boolean;
|
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
|
* Register or update a provider's configuration in openclaw.json
|
||||||
* without changing the current default model.
|
* without changing the current default model.
|
||||||
@@ -471,10 +505,12 @@ export async function syncProviderConfigToOpenClaw(
|
|||||||
override: RuntimeProviderConfigOverride
|
override: RuntimeProviderConfigOverride
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const config = await readOpenClawJson();
|
const config = await readOpenClawJson();
|
||||||
|
ensureMoonshotKimiWebSearchCnBaseUrl(config, provider);
|
||||||
|
|
||||||
if (override.baseUrl && override.api) {
|
if (override.baseUrl && override.api) {
|
||||||
const models = (config.models || {}) as Record<string, unknown>;
|
const models = (config.models || {}) as Record<string, unknown>;
|
||||||
const providers = (models.providers || {}) as Record<string, unknown>;
|
const providers = (models.providers || {}) as Record<string, unknown>;
|
||||||
|
removeLegacyMoonshotProviderEntry(provider, providers);
|
||||||
|
|
||||||
const nextModels: Array<Record<string, unknown>> = [];
|
const nextModels: Array<Record<string, unknown>> = [];
|
||||||
if (modelId) nextModels.push({ id: modelId, name: modelId });
|
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
|
// 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 plugins = (config.plugins || {}) as Record<string, unknown>;
|
||||||
const pEntries = (plugins.entries || {}) as Record<string, unknown>;
|
const pEntries = (plugins.entries || {}) as Record<string, unknown>;
|
||||||
pEntries[`${provider}-auth`] = { enabled: true };
|
pEntries[`${provider}-auth`] = { enabled: true };
|
||||||
@@ -516,6 +552,7 @@ export async function setOpenClawDefaultModelWithOverride(
|
|||||||
fallbackModels: string[] = []
|
fallbackModels: string[] = []
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const config = await readOpenClawJson();
|
const config = await readOpenClawJson();
|
||||||
|
ensureMoonshotKimiWebSearchCnBaseUrl(config, provider);
|
||||||
|
|
||||||
const model = modelOverride || getProviderDefaultModel(provider);
|
const model = modelOverride || getProviderDefaultModel(provider);
|
||||||
if (!model) {
|
if (!model) {
|
||||||
@@ -542,6 +579,7 @@ export async function setOpenClawDefaultModelWithOverride(
|
|||||||
if (override.baseUrl && override.api) {
|
if (override.baseUrl && override.api) {
|
||||||
const models = (config.models || {}) as Record<string, unknown>;
|
const models = (config.models || {}) as Record<string, unknown>;
|
||||||
const providers = (models.providers || {}) as Record<string, unknown>;
|
const providers = (models.providers || {}) as Record<string, unknown>;
|
||||||
|
removeLegacyMoonshotProviderEntry(provider, providers);
|
||||||
|
|
||||||
const nextModels: Array<Record<string, unknown>> = [];
|
const nextModels: Array<Record<string, unknown>> = [];
|
||||||
for (const candidateModelId of [modelId, ...fallbackModelIds]) {
|
for (const candidateModelId of [modelId, ...fallbackModelIds]) {
|
||||||
@@ -573,7 +611,7 @@ export async function setOpenClawDefaultModelWithOverride(
|
|||||||
config.gateway = gateway;
|
config.gateway = gateway;
|
||||||
|
|
||||||
// Ensure the extension plugin is marked as enabled in openclaw.json
|
// 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 plugins = (config.plugins || {}) as Record<string, unknown>;
|
||||||
const pEntries = (plugins.entries || {}) as Record<string, unknown>;
|
const pEntries = (plugins.entries || {}) as Record<string, unknown>;
|
||||||
pEntries[`${provider}-auth`] = { enabled: true };
|
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) {
|
if (modified) {
|
||||||
await writeOpenClawJson(config);
|
await writeOpenClawJson(config);
|
||||||
console.log('[sanitize] openclaw.json sanitized successfully');
|
console.log('[sanitize] openclaw.json sanitized successfully');
|
||||||
|
|||||||
73
electron/utils/provider-keys.ts
Normal file
73
electron/utils/provider-keys.ts
Normal 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);
|
||||||
|
}
|
||||||
@@ -154,6 +154,13 @@ export function getProviderEnvVar(type: string): string | undefined {
|
|||||||
return REGISTRY[type]?.envVar;
|
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 */
|
/** Get the default model string for a provider type */
|
||||||
export function getProviderDefaultModel(type: string): string | undefined {
|
export function getProviderDefaultModel(type: string): string | undefined {
|
||||||
return REGISTRY[type]?.defaultModel;
|
return REGISTRY[type]?.defaultModel;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
import { BUILTIN_PROVIDER_TYPES, type ProviderType } from './provider-registry';
|
import { BUILTIN_PROVIDER_TYPES, type ProviderType } from './provider-registry';
|
||||||
import { getActiveOpenClawProviders } from './openclaw-auth';
|
import { getActiveOpenClawProviders } from './openclaw-auth';
|
||||||
|
import { getOpenClawProviderKeyForType } from './provider-keys';
|
||||||
|
|
||||||
// Lazy-load electron-store (ESM module)
|
// Lazy-load electron-store (ESM module)
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// 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"
|
// e.g. provider.id "custom-a1b2c3d4-..." → strip hyphens → "customa1b2c3d4..." → slice(0,8) → "customa1"
|
||||||
// → openClawKey = "custom-customa1"
|
// → openClawKey = "custom-customa1"
|
||||||
// This must match getOpenClawProviderKey() in ipc-handlers.ts exactly.
|
// This must match getOpenClawProviderKey() in ipc-handlers.ts exactly.
|
||||||
const openClawKey = (provider.type === 'custom' || provider.type === 'ollama')
|
const openClawKey = getOpenClawProviderKeyForType(provider.type, provider.id);
|
||||||
? `${provider.type}-${provider.id.replace(/-/g, '').slice(0, 8)}`
|
|
||||||
: provider.type === 'minimax-portal-cn' ? 'minimax-portal' : provider.type;
|
|
||||||
if (!isBuiltin && !activeOpenClawProviders.has(provider.type) && !activeOpenClawProviders.has(provider.id) && !activeOpenClawProviders.has(openClawKey)) {
|
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`);
|
console.log(`[Sync] Provider ${provider.id} (${provider.type}) missing from OpenClaw, dropping from ClawX UI`);
|
||||||
await deleteProvider(provider.id);
|
await deleteProvider(provider.id);
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
BUILTIN_PROVIDER_TYPES,
|
BUILTIN_PROVIDER_TYPES,
|
||||||
getProviderConfig,
|
getProviderConfig,
|
||||||
getProviderEnvVar,
|
getProviderEnvVar,
|
||||||
|
getProviderEnvVars,
|
||||||
} from '@electron/utils/provider-registry';
|
} from '@electron/utils/provider-registry';
|
||||||
|
|
||||||
describe('provider metadata', () => {
|
describe('provider metadata', () => {
|
||||||
@@ -40,6 +41,17 @@ describe('provider metadata', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('uses a single canonical env key for moonshot provider', () => {
|
||||||
|
expect(getProviderEnvVar('moonshot')).toBe('MOONSHOT_API_KEY');
|
||||||
|
expect(getProviderEnvVars('moonshot')).toEqual(['MOONSHOT_API_KEY']);
|
||||||
|
expect(getProviderConfig('moonshot')).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
baseUrl: 'https://api.moonshot.cn/v1',
|
||||||
|
apiKeyEnv: 'MOONSHOT_API_KEY',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('keeps builtin provider sources in sync', () => {
|
it('keeps builtin provider sources in sync', () => {
|
||||||
expect(BUILTIN_PROVIDER_TYPES).toEqual(
|
expect(BUILTIN_PROVIDER_TYPES).toEqual(
|
||||||
expect.arrayContaining(['anthropic', 'openai', 'google', 'openrouter', 'ark', 'moonshot', 'siliconflow', 'minimax-portal', 'minimax-portal-cn', 'qwen-portal', 'ollama'])
|
expect.arrayContaining(['anthropic', 'openai', 'google', 'openrouter', 'ark', 'moonshot', 'siliconflow', 'minimax-portal', 'minimax-portal-cn', 'qwen-portal', 'ollama'])
|
||||||
|
|||||||
@@ -53,6 +53,23 @@ async function sanitizeConfig(filePath: string): Promise<boolean> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Mirror: remove stale tools.web.search.kimi.apiKey when moonshot provider exists.
|
||||||
|
const providers = ((config.models as Record<string, unknown> | undefined)?.providers as Record<string, unknown> | undefined) || {};
|
||||||
|
if (providers.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) {
|
||||||
|
delete kimi.apiKey;
|
||||||
|
search.kimi = kimi;
|
||||||
|
web.search = search;
|
||||||
|
tools.web = web;
|
||||||
|
config.tools = tools;
|
||||||
|
modified = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (modified) {
|
if (modified) {
|
||||||
await writeFile(filePath, JSON.stringify(config, null, 2), 'utf-8');
|
await writeFile(filePath, JSON.stringify(config, null, 2), 'utf-8');
|
||||||
}
|
}
|
||||||
@@ -223,4 +240,58 @@ describe('sanitizeOpenClawConfig (blocklist approach)', () => {
|
|||||||
expect(result.gateway).toEqual({ mode: 'local', auth: { token: 'xyz' } });
|
expect(result.gateway).toEqual({ mode: 'local', auth: { token: 'xyz' } });
|
||||||
expect(result.agents).toEqual({ defaults: { model: { primary: 'gpt-4' } } });
|
expect(result.agents).toEqual({ defaults: { model: { primary: 'gpt-4' } } });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('removes tools.web.search.kimi.apiKey when moonshot provider exists', async () => {
|
||||||
|
await writeConfig({
|
||||||
|
models: {
|
||||||
|
providers: {
|
||||||
|
moonshot: { baseUrl: 'https://api.moonshot.cn/v1', api: 'openai-completions' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tools: {
|
||||||
|
web: {
|
||||||
|
search: {
|
||||||
|
kimi: {
|
||||||
|
apiKey: 'stale-inline-key',
|
||||||
|
baseUrl: 'https://api.moonshot.cn/v1',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const modified = await sanitizeConfig(configPath);
|
||||||
|
expect(modified).toBe(true);
|
||||||
|
|
||||||
|
const result = await readConfig();
|
||||||
|
const kimi = ((((result.tools as Record<string, unknown>).web as Record<string, unknown>).search as Record<string, unknown>).kimi as Record<string, unknown>);
|
||||||
|
expect(kimi).not.toHaveProperty('apiKey');
|
||||||
|
expect(kimi.baseUrl).toBe('https://api.moonshot.cn/v1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps tools.web.search.kimi.apiKey when moonshot provider is absent', async () => {
|
||||||
|
const original = {
|
||||||
|
models: {
|
||||||
|
providers: {
|
||||||
|
openrouter: { baseUrl: 'https://openrouter.ai/api/v1', api: 'openai-completions' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tools: {
|
||||||
|
web: {
|
||||||
|
search: {
|
||||||
|
kimi: {
|
||||||
|
apiKey: 'should-stay',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
await writeConfig(original);
|
||||||
|
|
||||||
|
const modified = await sanitizeConfig(configPath);
|
||||||
|
expect(modified).toBe(false);
|
||||||
|
|
||||||
|
const result = await readConfig();
|
||||||
|
expect(result).toEqual(original);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user