mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-05-27 08:32:32 +08:00
- 修复 Claude Desktop 模型输入框失焦
- 为动态模型行添加稳定 rowId,避免编辑模型 ID 时重挂载 - 增加模型映射和直连模型列表焦点保持回归测试
This commit is contained in:
@@ -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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user