mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-24 12:44:18 +08:00
* fix: Claude Desktop 官方供应商添加时缺少 ANTHROPIC_BASE_URL 报错 根因:前端 mutation 为 claude-desktop 生成随机 UUID 作为 provider id, 后端 is_official_provider 通过 id 匹配跳过校验,随机 UUID 不匹配导致 走入普通 direct 模式校验并要求 ANTHROPIC_BASE_URL。 修复: - 前端:claude-desktop + category=official 时使用固定 id "claude-desktop-official" - 后端:validate_provider / validate_direct_provider / validate_proxy_provider / apply_provider_to_paths 增加 category=="official" 兜底检查 Fixes #3402 * fix: restrict Claude Desktop official provider detection * fix: add Claude Desktop official provider via seed --------- Co-authored-by: 金恩光 <enguang.jin@gmail.com> Co-authored-by: Jason <farion1231@gmail.com>
This commit is contained in:
@@ -1947,12 +1947,13 @@ mod tests {
|
||||
let direct = direct_provider("direct");
|
||||
assert!(is_compatible_direct_provider(&direct));
|
||||
|
||||
let claude_official = Provider::with_id(
|
||||
let mut claude_official = Provider::with_id(
|
||||
"claude-official".to_string(),
|
||||
"Claude Official".to_string(),
|
||||
json!({"env": {}}),
|
||||
Some("https://www.anthropic.com/claude-code".to_string()),
|
||||
);
|
||||
claude_official.category = Some("official".to_string());
|
||||
assert!(!is_compatible_direct_provider(&claude_official));
|
||||
|
||||
let mut openai_format = direct_provider("openai");
|
||||
|
||||
@@ -219,6 +219,17 @@ pub fn import_claude_desktop_providers_from_claude(
|
||||
Ok(imported)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn ensure_claude_desktop_official_provider(state: State<'_, AppState>) -> Result<bool, String> {
|
||||
state
|
||||
.db
|
||||
.ensure_official_seed_by_id(
|
||||
crate::database::CLAUDE_DESKTOP_OFFICIAL_PROVIDER_ID,
|
||||
AppType::ClaudeDesktop,
|
||||
)
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
fn claude_provider_models_are_claude_safe(provider: &Provider) -> bool {
|
||||
let Some(env) = provider
|
||||
.settings_config
|
||||
|
||||
@@ -1105,6 +1105,7 @@ pub fn run() {
|
||||
commands::get_claude_desktop_status,
|
||||
commands::get_claude_desktop_default_routes,
|
||||
commands::import_claude_desktop_providers_from_claude,
|
||||
commands::ensure_claude_desktop_official_provider,
|
||||
commands::get_claude_config_status,
|
||||
commands::get_config_status,
|
||||
commands::get_claude_code_config_path,
|
||||
|
||||
@@ -30,6 +30,7 @@ interface AddProviderDialogProps {
|
||||
provider: Omit<Provider, "id"> & {
|
||||
providerKey?: string;
|
||||
suggestedDefaults?: OpenClawSuggestedDefaults;
|
||||
ensureClaudeDesktopOfficialSeed?: boolean;
|
||||
},
|
||||
) => Promise<void> | void;
|
||||
}
|
||||
@@ -98,6 +99,7 @@ export function AddProviderDialog({
|
||||
const providerData: Omit<Provider, "id"> & {
|
||||
providerKey?: string;
|
||||
suggestedDefaults?: OpenClawSuggestedDefaults;
|
||||
ensureClaudeDesktopOfficialSeed?: boolean;
|
||||
} = {
|
||||
name: values.name.trim(),
|
||||
notes: values.notes?.trim() || undefined,
|
||||
@@ -109,6 +111,16 @@ export function AddProviderDialog({
|
||||
...(values.meta ? { meta: values.meta } : {}),
|
||||
};
|
||||
|
||||
if (appId === "claude-desktop" && values.presetId) {
|
||||
const presetIndex = parseInt(
|
||||
values.presetId.replace("claude-desktop-", ""),
|
||||
);
|
||||
const preset = claudeDesktopProviderPresets[presetIndex];
|
||||
providerData.ensureClaudeDesktopOfficialSeed =
|
||||
values.presetCategory === "official" &&
|
||||
preset?.category === "official";
|
||||
}
|
||||
|
||||
// OpenCode/OpenClaw: pass providerKey for ID generation
|
||||
if (
|
||||
(appId === "opencode" || appId === "openclaw" || appId === "hermes") &&
|
||||
|
||||
@@ -75,6 +75,7 @@ export function useProviderActions(
|
||||
providerKey?: string;
|
||||
suggestedDefaults?: OpenClawSuggestedDefaults;
|
||||
addToLive?: boolean;
|
||||
ensureClaudeDesktopOfficialSeed?: boolean;
|
||||
},
|
||||
) => {
|
||||
const enhanced = injectCodingPlanUsageScript(activeApp, provider);
|
||||
|
||||
@@ -99,6 +99,10 @@ export const providersApi = {
|
||||
return await invoke("import_claude_desktop_providers_from_claude");
|
||||
},
|
||||
|
||||
async ensureClaudeDesktopOfficialProvider(): Promise<boolean> {
|
||||
return await invoke("ensure_claude_desktop_official_provider");
|
||||
},
|
||||
|
||||
async getClaudeDesktopStatus(): Promise<ClaudeDesktopStatus> {
|
||||
return await invoke("get_claude_desktop_status");
|
||||
},
|
||||
|
||||
@@ -19,8 +19,26 @@ export const useAddProviderMutation = (appId: AppId) => {
|
||||
providerInput: Omit<Provider, "id"> & {
|
||||
providerKey?: string;
|
||||
addToLive?: boolean;
|
||||
ensureClaudeDesktopOfficialSeed?: boolean;
|
||||
},
|
||||
) => {
|
||||
const {
|
||||
providerKey: _providerKey,
|
||||
addToLive,
|
||||
ensureClaudeDesktopOfficialSeed,
|
||||
...rest
|
||||
} = providerInput;
|
||||
|
||||
if (appId === "claude-desktop" && ensureClaudeDesktopOfficialSeed) {
|
||||
await providersApi.ensureClaudeDesktopOfficialProvider();
|
||||
const providers = await providersApi.getAll(appId);
|
||||
const officialProvider = providers["claude-desktop-official"];
|
||||
if (!officialProvider) {
|
||||
throw new Error("Claude Desktop official provider was not created");
|
||||
}
|
||||
return officialProvider;
|
||||
}
|
||||
|
||||
let id: string;
|
||||
|
||||
if (appId === "opencode" || appId === "openclaw" || appId === "hermes") {
|
||||
@@ -40,8 +58,6 @@ export const useAddProviderMutation = (appId: AppId) => {
|
||||
id = generateUUID();
|
||||
}
|
||||
|
||||
const { providerKey: _providerKey, addToLive, ...rest } = providerInput;
|
||||
|
||||
const newProvider: Provider = {
|
||||
...rest,
|
||||
id,
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
import type { ReactNode } from "react";
|
||||
import { act, renderHook } from "@testing-library/react";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { useAddProviderMutation } from "@/lib/query/mutations";
|
||||
import type { Provider } from "@/types";
|
||||
|
||||
const apiMocks = vi.hoisted(() => ({
|
||||
add: vi.fn(),
|
||||
ensureClaudeDesktopOfficialProvider: vi.fn(),
|
||||
getAll: vi.fn(),
|
||||
updateTrayMenu: vi.fn(),
|
||||
}));
|
||||
|
||||
const uuidMocks = vi.hoisted(() => ({
|
||||
generateUUID: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/api", () => ({
|
||||
providersApi: {
|
||||
add: (...args: unknown[]) => apiMocks.add(...args),
|
||||
ensureClaudeDesktopOfficialProvider: (...args: unknown[]) =>
|
||||
apiMocks.ensureClaudeDesktopOfficialProvider(...args),
|
||||
getAll: (...args: unknown[]) => apiMocks.getAll(...args),
|
||||
updateTrayMenu: (...args: unknown[]) => apiMocks.updateTrayMenu(...args),
|
||||
},
|
||||
sessionsApi: {},
|
||||
settingsApi: {},
|
||||
}));
|
||||
|
||||
vi.mock("@/utils/uuid", () => ({
|
||||
generateUUID: () => uuidMocks.generateUUID(),
|
||||
}));
|
||||
|
||||
vi.mock("sonner", () => ({
|
||||
toast: {
|
||||
success: vi.fn(),
|
||||
error: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
function createWrapper() {
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { retry: false },
|
||||
mutations: { retry: false },
|
||||
},
|
||||
});
|
||||
|
||||
const wrapper = ({ children }: { children: ReactNode }) => (
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
);
|
||||
|
||||
return { wrapper };
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
apiMocks.add.mockReset().mockResolvedValue(true);
|
||||
apiMocks.ensureClaudeDesktopOfficialProvider
|
||||
.mockReset()
|
||||
.mockResolvedValue(true);
|
||||
apiMocks.getAll.mockReset().mockResolvedValue({});
|
||||
apiMocks.updateTrayMenu.mockReset().mockResolvedValue(true);
|
||||
uuidMocks.generateUUID.mockReset().mockReturnValue("generated-uuid");
|
||||
});
|
||||
|
||||
describe("useAddProviderMutation", () => {
|
||||
it("duplicates Claude Desktop official providers with a fresh id", async () => {
|
||||
const { wrapper } = createWrapper();
|
||||
const { result } = renderHook(
|
||||
() => useAddProviderMutation("claude-desktop"),
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
const duplicatedProvider = await act(async () =>
|
||||
result.current.mutateAsync({
|
||||
name: "Claude Desktop Official copy",
|
||||
settingsConfig: { env: {} },
|
||||
category: "official",
|
||||
}),
|
||||
);
|
||||
|
||||
expect(apiMocks.ensureClaudeDesktopOfficialProvider).not.toHaveBeenCalled();
|
||||
expect(apiMocks.add).toHaveBeenCalledTimes(1);
|
||||
expect(apiMocks.add).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
id: "generated-uuid",
|
||||
name: "Claude Desktop Official copy",
|
||||
category: "official",
|
||||
}),
|
||||
"claude-desktop",
|
||||
undefined,
|
||||
);
|
||||
expect(duplicatedProvider.id).toBe("generated-uuid");
|
||||
expect(duplicatedProvider.id).not.toBe("claude-desktop-official");
|
||||
});
|
||||
|
||||
it("returns the persisted seed row for the Claude Desktop official preset", async () => {
|
||||
const seedProvider: Provider = {
|
||||
id: "claude-desktop-official",
|
||||
name: "Claude Desktop Official",
|
||||
settingsConfig: { env: {} },
|
||||
websiteUrl: "https://claude.ai/download",
|
||||
category: "official",
|
||||
icon: "anthropic",
|
||||
iconColor: "#D4915D",
|
||||
createdAt: 123,
|
||||
};
|
||||
apiMocks.getAll.mockResolvedValueOnce({
|
||||
"claude-desktop-official": seedProvider,
|
||||
});
|
||||
const { wrapper } = createWrapper();
|
||||
const { result } = renderHook(
|
||||
() => useAddProviderMutation("claude-desktop"),
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
const persistedProvider = await act(async () =>
|
||||
result.current.mutateAsync({
|
||||
name: "Renamed by form",
|
||||
settingsConfig: { env: { ignored: true } },
|
||||
websiteUrl: "https://example.invalid",
|
||||
category: "official",
|
||||
icon: "custom-icon",
|
||||
ensureClaudeDesktopOfficialSeed: true,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(apiMocks.ensureClaudeDesktopOfficialProvider).toHaveBeenCalledTimes(
|
||||
1,
|
||||
);
|
||||
expect(apiMocks.getAll).toHaveBeenCalledWith("claude-desktop");
|
||||
expect(apiMocks.add).not.toHaveBeenCalled();
|
||||
expect(persistedProvider).toEqual(seedProvider);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user