- 修复 Claude Desktop 模型输入框失焦

- 为动态模型行添加稳定 rowId,避免编辑模型 ID 时重挂载

- 增加模型映射和直连模型列表焦点保持回归测试
This commit is contained in:
Jason
2026-05-12 11:47:45 +08:00
parent 270f49a4a6
commit 44d4ea81af
2 changed files with 146 additions and 27 deletions
@@ -101,11 +101,14 @@ export interface ClaudeDesktopProviderFormProps {
}
type RouteRow = {
rowId: string;
route: string;
model: string;
supports1m: boolean;
};
type RouteRowValues = Omit<RouteRow, "rowId">;
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<string, ClaudeDesktopModelRoute> | 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<RouteRow>) => {
const updateRoute = (index: number, patch: Partial<RouteRowValues>) => {
setRoutes((current) =>
current.map((row, i) => (i === index ? { ...row, ...patch } : row)),
);
@@ -825,7 +850,7 @@ export function ClaudeDesktopProviderForm({
</div>
{routes.map((route, index) => (
<div
key={`${route.route}-${index}`}
key={route.rowId}
className="grid grid-cols-1 gap-2 md:grid-cols-[1fr_1fr_92px_36px]"
>
<div className="flex">
@@ -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({
<div className="space-y-2">
{routes.map((route, index) => (
<div
key={`${route.route}-${index}`}
key={route.rowId}
className="grid grid-cols-1 gap-2 md:grid-cols-[1fr_92px_36px]"
>
<div className="flex gap-1">
@@ -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<typeof ClaudeDesktopProviderForm>["initialData"],
) {
const queryClient = createTestQueryClient();
return render(
<QueryClientProvider client={queryClient}>
<ClaudeDesktopProviderForm
submitLabel="保存"
onSubmit={vi.fn()}
onCancel={vi.fn()}
initialData={initialData}
/>
</QueryClientProvider>,
);
}
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);
});
});