diff --git a/src/components/providers/forms/ClaudeDesktopProviderForm.tsx b/src/components/providers/forms/ClaudeDesktopProviderForm.tsx index b8543052c..c02fb058e 100644 --- a/src/components/providers/forms/ClaudeDesktopProviderForm.tsx +++ b/src/components/providers/forms/ClaudeDesktopProviderForm.tsx @@ -101,11 +101,14 @@ export interface ClaudeDesktopProviderFormProps { } type RouteRow = { + rowId: string; route: string; model: string; supports1m: boolean; }; +type RouteRowValues = Omit; + const CLAUDE_ROUTE_PREFIX = "claude-"; const ANTHROPIC_CLAUDE_ROUTE_PREFIX = "anthropic/claude-"; @@ -164,14 +167,23 @@ function desktopRouteIdFromModel(model: string) { return desktopRouteIdFromInput(suffix); } +function createRouteRow(row: RouteRowValues): RouteRow { + return { + rowId: crypto.randomUUID(), + ...row, + }; +} + function initialRouteRows( routes: Record | undefined, ): RouteRow[] { - return Object.entries(routes ?? {}).map(([route, value]) => ({ - route, - model: value.model ?? "", - supports1m: value.supports1m ?? false, - })); + return Object.entries(routes ?? {}).map(([route, value]) => + createRouteRow({ + route, + model: value.model ?? "", + supports1m: value.supports1m ?? false, + }), + ); } function isClaudeSafeRoute(route: string) { @@ -188,23 +200,34 @@ function defaultRouteRows( defaults: ClaudeDesktopDefaultRoute[], defaultModel: string, ): RouteRow[] { - return defaults.map((route, index) => ({ - route: route.routeId, - model: index === 0 ? defaultModel : "", - supports1m: route.supports1m, - })); + return defaults.map((route, index) => + createRouteRow({ + route: route.routeId, + model: index === 0 ? defaultModel : "", + supports1m: route.supports1m, + }), + ); } function nextRouteRow(current: RouteRow[], defaults: RouteRow[]): RouteRow { - return ( + const defaultRow = defaults.find( (route) => !current.some((existing) => existing.route === route.route), - ) ?? { - route: "", - model: "", - supports1m: true, - } - ); + ) ?? null; + + if (defaultRow) { + return createRouteRow({ + route: defaultRow.route, + model: defaultRow.model, + supports1m: defaultRow.supports1m, + }); + } + + return createRouteRow({ + route: "", + model: "", + supports1m: true, + }); } export function ClaudeDesktopProviderForm({ @@ -371,11 +394,13 @@ export function ClaudeDesktopProviderForm({ setMode(preset.mode); if (preset.mode === "proxy" && preset.modelRoutes) { setRoutes( - preset.modelRoutes.map((r) => ({ - route: r.routeId, - model: r.upstreamModel, - supports1m: r.supports1m, - })), + preset.modelRoutes.map((r) => + createRouteRow({ + route: r.routeId, + model: r.upstreamModel, + supports1m: r.supports1m, + }), + ), ); } else { setRoutes([]); @@ -412,7 +437,7 @@ export function ClaudeDesktopProviderForm({ applyDesktopPreset(entry.preset); }; - const updateRoute = (index: number, patch: Partial) => { + const updateRoute = (index: number, patch: Partial) => { setRoutes((current) => current.map((row, i) => (i === index ? { ...row, ...patch } : row)), ); @@ -825,7 +850,7 @@ export function ClaudeDesktopProviderForm({ {routes.map((route, index) => (
@@ -935,11 +960,11 @@ export function ClaudeDesktopProviderForm({ () => setRoutes((current) => [ ...current, - { + createRouteRow({ route: "", model: "", supports1m: false, - }, + }), ]), t("claudeDesktop.addModel", { defaultValue: "添加模型" }), )} @@ -949,7 +974,7 @@ export function ClaudeDesktopProviderForm({
{routes.map((route, index) => (
diff --git a/tests/components/ClaudeDesktopProviderForm.test.tsx b/tests/components/ClaudeDesktopProviderForm.test.tsx new file mode 100644 index 000000000..4b47def0c --- /dev/null +++ b/tests/components/ClaudeDesktopProviderForm.test.tsx @@ -0,0 +1,94 @@ +import { fireEvent, render, screen } from "@testing-library/react"; +import { QueryClientProvider } from "@tanstack/react-query"; +import type { ComponentProps } from "react"; +import { describe, expect, it, vi } from "vitest"; +import { ClaudeDesktopProviderForm } from "@/components/providers/forms/ClaudeDesktopProviderForm"; +import { createTestQueryClient } from "../utils/testQueryClient"; + +vi.mock("@/lib/api/providers", () => ({ + providersApi: { + getClaudeDesktopDefaultRoutes: () => Promise.resolve([]), + }, +})); + +function renderForm( + initialData: ComponentProps["initialData"], +) { + const queryClient = createTestQueryClient(); + return render( + + + , + ); +} + +describe("ClaudeDesktopProviderForm", () => { + it("编辑模型映射的 Desktop 模型 ID 时保持输入框焦点", () => { + renderForm({ + name: "Proxy Provider", + settingsConfig: { + env: { + ANTHROPIC_BASE_URL: "https://api.example.com", + ANTHROPIC_AUTH_TOKEN: "sk-test", + }, + }, + meta: { + claudeDesktopMode: "proxy", + claudeDesktopModelRoutes: { + "claude-old": { + model: "upstream-old", + }, + }, + }, + }); + + const input = screen.getByPlaceholderText("gpt-5.5") as HTMLInputElement; + input.focus(); + + fireEvent.change(input, { target: { value: "12345" } }); + + const currentInput = screen.getByPlaceholderText( + "gpt-5.5", + ) as HTMLInputElement; + expect(currentInput).toHaveValue("12345"); + expect(document.activeElement).toBe(currentInput); + }); + + it("编辑直连模型列表的模型 ID 时保持输入框焦点", () => { + renderForm({ + name: "Direct Provider", + settingsConfig: { + env: { + ANTHROPIC_BASE_URL: "https://api.example.com", + ANTHROPIC_AUTH_TOKEN: "sk-test", + }, + }, + meta: { + claudeDesktopMode: "direct", + claudeDesktopModelRoutes: { + "claude-old": { + model: "claude-old", + }, + }, + }, + }); + + const input = screen.getByPlaceholderText( + "claude-deepseek-chat", + ) as HTMLInputElement; + input.focus(); + + fireEvent.change(input, { target: { value: "claude-12345" } }); + + const currentInput = screen.getByPlaceholderText( + "claude-deepseek-chat", + ) as HTMLInputElement; + expect(currentInput).toHaveValue("claude-12345"); + expect(document.activeElement).toBe(currentInput); + }); +});