diff --git a/CHANGELOG.md b/CHANGELOG.md index aea516b6bc1..da8854a5bb4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ Docs: https://docs.openclaw.ai ### Changes -- TBD. +- Onboarding: add Moonshot (.cn) auth choice and keep the China base URL when preserving defaults. (#7180) Thanks @waynelwz. ### Fixes diff --git a/src/commands/auth-choice.apply.api-providers.ts b/src/commands/auth-choice.apply.api-providers.ts index 8b8d6b6346f..ea6095c9cbd 100644 --- a/src/commands/auth-choice.apply.api-providers.ts +++ b/src/commands/auth-choice.apply.api-providers.ts @@ -18,6 +18,7 @@ import { applyMoonshotConfig, applyMoonshotConfigCn, applyMoonshotProviderConfig, + applyMoonshotProviderConfigCn, applyOpencodeZenConfig, applyOpencodeZenProviderConfig, applyOpenrouterConfig, @@ -314,7 +315,7 @@ export async function applyAuthChoiceApiProviders( setDefaultModel: params.setDefaultModel, defaultModel: MOONSHOT_DEFAULT_MODEL_REF, applyDefaultConfig: applyMoonshotConfigCn, - applyProviderConfig: (cfg) => applyMoonshotProviderConfigCnShim(cfg), + applyProviderConfig: applyMoonshotProviderConfigCn, noteAgentModel, prompter: params.prompter, }); @@ -324,14 +325,6 @@ export async function applyAuthChoiceApiProviders( return { config: nextConfig, agentModelOverride }; } - function applyMoonshotProviderConfigCnShim( - cfg: Parameters[0], - ) { - // For now, provider-level CN behavior is fully handled inside applyMoonshotConfigCn. - // Keep a thin shim to satisfy the applyDefaultModelChoice signature. - return applyMoonshotProviderConfig(cfg); - } - if (authChoice === "kimi-code-api-key") { let hasCredential = false; const tokenProvider = params.opts?.tokenProvider?.trim().toLowerCase(); diff --git a/src/commands/auth-choice.moonshot.test.ts b/src/commands/auth-choice.moonshot.test.ts new file mode 100644 index 00000000000..8bddbd7a6f6 --- /dev/null +++ b/src/commands/auth-choice.moonshot.test.ts @@ -0,0 +1,154 @@ +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import type { RuntimeEnv } from "../runtime.js"; +import type { WizardPrompter } from "../wizard/prompts.js"; +import { applyAuthChoice } from "./auth-choice.js"; + +const noopAsync = async () => {}; +const noop = () => {}; +const authProfilePathFor = (agentDir: string) => path.join(agentDir, "auth-profiles.json"); +const requireAgentDir = () => { + const agentDir = process.env.OPENCLAW_AGENT_DIR; + if (!agentDir) { + throw new Error("OPENCLAW_AGENT_DIR not set"); + } + return agentDir; +}; + +describe("applyAuthChoice (moonshot)", () => { + const previousStateDir = process.env.OPENCLAW_STATE_DIR; + const previousAgentDir = process.env.OPENCLAW_AGENT_DIR; + const previousPiAgentDir = process.env.PI_CODING_AGENT_DIR; + const previousMoonshotKey = process.env.MOONSHOT_API_KEY; + let tempStateDir: string | null = null; + + afterEach(async () => { + if (tempStateDir) { + await fs.rm(tempStateDir, { recursive: true, force: true }); + tempStateDir = null; + } + if (previousStateDir === undefined) { + delete process.env.OPENCLAW_STATE_DIR; + } else { + process.env.OPENCLAW_STATE_DIR = previousStateDir; + } + if (previousAgentDir === undefined) { + delete process.env.OPENCLAW_AGENT_DIR; + } else { + process.env.OPENCLAW_AGENT_DIR = previousAgentDir; + } + if (previousPiAgentDir === undefined) { + delete process.env.PI_CODING_AGENT_DIR; + } else { + process.env.PI_CODING_AGENT_DIR = previousPiAgentDir; + } + if (previousMoonshotKey === undefined) { + delete process.env.MOONSHOT_API_KEY; + } else { + process.env.MOONSHOT_API_KEY = previousMoonshotKey; + } + }); + + it("keeps the .cn baseUrl when setDefaultModel is false", async () => { + tempStateDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-auth-")); + process.env.OPENCLAW_STATE_DIR = tempStateDir; + process.env.OPENCLAW_AGENT_DIR = path.join(tempStateDir, "agent"); + process.env.PI_CODING_AGENT_DIR = process.env.OPENCLAW_AGENT_DIR; + delete process.env.MOONSHOT_API_KEY; + + const text = vi.fn().mockResolvedValue("sk-moonshot-cn-test"); + const prompter: WizardPrompter = { + intro: vi.fn(noopAsync), + outro: vi.fn(noopAsync), + note: vi.fn(noopAsync), + select: vi.fn(async () => "" as never), + multiselect: vi.fn(async () => []), + text, + confirm: vi.fn(async () => false), + progress: vi.fn(() => ({ update: noop, stop: noop })), + }; + const runtime: RuntimeEnv = { + log: vi.fn(), + error: vi.fn(), + exit: vi.fn((code: number) => { + throw new Error(`exit:${code}`); + }), + }; + + const result = await applyAuthChoice({ + authChoice: "moonshot-api-key-cn", + config: { + agents: { + defaults: { + model: { primary: "anthropic/claude-opus-4-5" }, + }, + }, + }, + prompter, + runtime, + setDefaultModel: false, + }); + + expect(text).toHaveBeenCalledWith( + expect.objectContaining({ message: "Enter Moonshot API key (.cn)" }), + ); + expect(result.config.agents?.defaults?.model?.primary).toBe("anthropic/claude-opus-4-5"); + expect(result.config.models?.providers?.moonshot?.baseUrl).toBe("https://api.moonshot.cn/v1"); + expect(result.agentModelOverride).toBe("moonshot/kimi-k2.5"); + + const authProfilePath = authProfilePathFor(requireAgentDir()); + const raw = await fs.readFile(authProfilePath, "utf8"); + const parsed = JSON.parse(raw) as { + profiles?: Record; + }; + expect(parsed.profiles?.["moonshot:default"]?.key).toBe("sk-moonshot-cn-test"); + }); + + it("sets the default model when setDefaultModel is true", async () => { + tempStateDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-auth-")); + process.env.OPENCLAW_STATE_DIR = tempStateDir; + process.env.OPENCLAW_AGENT_DIR = path.join(tempStateDir, "agent"); + process.env.PI_CODING_AGENT_DIR = process.env.OPENCLAW_AGENT_DIR; + delete process.env.MOONSHOT_API_KEY; + + const text = vi.fn().mockResolvedValue("sk-moonshot-cn-test"); + const prompter: WizardPrompter = { + intro: vi.fn(noopAsync), + outro: vi.fn(noopAsync), + note: vi.fn(noopAsync), + select: vi.fn(async () => "" as never), + multiselect: vi.fn(async () => []), + text, + confirm: vi.fn(async () => false), + progress: vi.fn(() => ({ update: noop, stop: noop })), + }; + const runtime: RuntimeEnv = { + log: vi.fn(), + error: vi.fn(), + exit: vi.fn((code: number) => { + throw new Error(`exit:${code}`); + }), + }; + + const result = await applyAuthChoice({ + authChoice: "moonshot-api-key-cn", + config: {}, + prompter, + runtime, + setDefaultModel: true, + }); + + expect(result.config.agents?.defaults?.model?.primary).toBe("moonshot/kimi-k2.5"); + expect(result.config.models?.providers?.moonshot?.baseUrl).toBe("https://api.moonshot.cn/v1"); + expect(result.agentModelOverride).toBeUndefined(); + + const authProfilePath = authProfilePathFor(requireAgentDir()); + const raw = await fs.readFile(authProfilePath, "utf8"); + const parsed = JSON.parse(raw) as { + profiles?: Record; + }; + expect(parsed.profiles?.["moonshot:default"]?.key).toBe("sk-moonshot-cn-test"); + }); +}); diff --git a/src/commands/onboard-auth.config-core.ts b/src/commands/onboard-auth.config-core.ts index 30c81ae548b..892d44224f3 100644 --- a/src/commands/onboard-auth.config-core.ts +++ b/src/commands/onboard-auth.config-core.ts @@ -141,6 +141,10 @@ export function applyMoonshotProviderConfig(cfg: OpenClawConfig): OpenClawConfig return applyMoonshotProviderConfigWithBaseUrl(cfg, MOONSHOT_BASE_URL); } +export function applyMoonshotProviderConfigCn(cfg: OpenClawConfig): OpenClawConfig { + return applyMoonshotProviderConfigWithBaseUrl(cfg, MOONSHOT_CN_BASE_URL); +} + function applyMoonshotProviderConfigWithBaseUrl( cfg: OpenClawConfig, baseUrl: string, @@ -210,7 +214,7 @@ export function applyMoonshotConfig(cfg: OpenClawConfig): OpenClawConfig { } export function applyMoonshotConfigCn(cfg: OpenClawConfig): OpenClawConfig { - const next = applyMoonshotProviderConfigWithBaseUrl(cfg, MOONSHOT_CN_BASE_URL); + const next = applyMoonshotProviderConfigCn(cfg); const existingModel = next.agents?.defaults?.model; return { ...next, diff --git a/src/commands/onboard-auth.ts b/src/commands/onboard-auth.ts index 8ab8101b12c..e3fc17b822b 100644 --- a/src/commands/onboard-auth.ts +++ b/src/commands/onboard-auth.ts @@ -10,6 +10,7 @@ export { applyMoonshotConfig, applyMoonshotConfigCn, applyMoonshotProviderConfig, + applyMoonshotProviderConfigCn, applyOpenrouterConfig, applyOpenrouterProviderConfig, applySyntheticConfig,