mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-08-04 19:45:34 +08:00
- 修复 Claude Desktop 模型输入框失焦
- 为动态模型行添加稳定 rowId,避免编辑模型 ID 时重挂载 - 增加模型映射和直连模型列表焦点保持回归测试
This commit is contained in:
@@ -101,11 +101,14 @@ export interface ClaudeDesktopProviderFormProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type RouteRow = {
|
type RouteRow = {
|
||||||
|
rowId: string;
|
||||||
route: string;
|
route: string;
|
||||||
model: string;
|
model: string;
|
||||||
supports1m: boolean;
|
supports1m: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type RouteRowValues = Omit<RouteRow, "rowId">;
|
||||||
|
|
||||||
const CLAUDE_ROUTE_PREFIX = "claude-";
|
const CLAUDE_ROUTE_PREFIX = "claude-";
|
||||||
const ANTHROPIC_CLAUDE_ROUTE_PREFIX = "anthropic/claude-";
|
const ANTHROPIC_CLAUDE_ROUTE_PREFIX = "anthropic/claude-";
|
||||||
|
|
||||||
@@ -164,14 +167,23 @@ function desktopRouteIdFromModel(model: string) {
|
|||||||
return desktopRouteIdFromInput(suffix);
|
return desktopRouteIdFromInput(suffix);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createRouteRow(row: RouteRowValues): RouteRow {
|
||||||
|
return {
|
||||||
|
rowId: crypto.randomUUID(),
|
||||||
|
...row,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function initialRouteRows(
|
function initialRouteRows(
|
||||||
routes: Record<string, ClaudeDesktopModelRoute> | undefined,
|
routes: Record<string, ClaudeDesktopModelRoute> | undefined,
|
||||||
): RouteRow[] {
|
): RouteRow[] {
|
||||||
return Object.entries(routes ?? {}).map(([route, value]) => ({
|
return Object.entries(routes ?? {}).map(([route, value]) =>
|
||||||
route,
|
createRouteRow({
|
||||||
model: value.model ?? "",
|
route,
|
||||||
supports1m: value.supports1m ?? false,
|
model: value.model ?? "",
|
||||||
}));
|
supports1m: value.supports1m ?? false,
|
||||||
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isClaudeSafeRoute(route: string) {
|
function isClaudeSafeRoute(route: string) {
|
||||||
@@ -188,23 +200,34 @@ function defaultRouteRows(
|
|||||||
defaults: ClaudeDesktopDefaultRoute[],
|
defaults: ClaudeDesktopDefaultRoute[],
|
||||||
defaultModel: string,
|
defaultModel: string,
|
||||||
): RouteRow[] {
|
): RouteRow[] {
|
||||||
return defaults.map((route, index) => ({
|
return defaults.map((route, index) =>
|
||||||
route: route.routeId,
|
createRouteRow({
|
||||||
model: index === 0 ? defaultModel : "",
|
route: route.routeId,
|
||||||
supports1m: route.supports1m,
|
model: index === 0 ? defaultModel : "",
|
||||||
}));
|
supports1m: route.supports1m,
|
||||||
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function nextRouteRow(current: RouteRow[], defaults: RouteRow[]): RouteRow {
|
function nextRouteRow(current: RouteRow[], defaults: RouteRow[]): RouteRow {
|
||||||
return (
|
const defaultRow =
|
||||||
defaults.find(
|
defaults.find(
|
||||||
(route) => !current.some((existing) => existing.route === route.route),
|
(route) => !current.some((existing) => existing.route === route.route),
|
||||||
) ?? {
|
) ?? null;
|
||||||
route: "",
|
|
||||||
model: "",
|
if (defaultRow) {
|
||||||
supports1m: true,
|
return createRouteRow({
|
||||||
}
|
route: defaultRow.route,
|
||||||
);
|
model: defaultRow.model,
|
||||||
|
supports1m: defaultRow.supports1m,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return createRouteRow({
|
||||||
|
route: "",
|
||||||
|
model: "",
|
||||||
|
supports1m: true,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ClaudeDesktopProviderForm({
|
export function ClaudeDesktopProviderForm({
|
||||||
@@ -371,11 +394,13 @@ export function ClaudeDesktopProviderForm({
|
|||||||
setMode(preset.mode);
|
setMode(preset.mode);
|
||||||
if (preset.mode === "proxy" && preset.modelRoutes) {
|
if (preset.mode === "proxy" && preset.modelRoutes) {
|
||||||
setRoutes(
|
setRoutes(
|
||||||
preset.modelRoutes.map((r) => ({
|
preset.modelRoutes.map((r) =>
|
||||||
route: r.routeId,
|
createRouteRow({
|
||||||
model: r.upstreamModel,
|
route: r.routeId,
|
||||||
supports1m: r.supports1m,
|
model: r.upstreamModel,
|
||||||
})),
|
supports1m: r.supports1m,
|
||||||
|
}),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
setRoutes([]);
|
setRoutes([]);
|
||||||
@@ -412,7 +437,7 @@ export function ClaudeDesktopProviderForm({
|
|||||||
applyDesktopPreset(entry.preset);
|
applyDesktopPreset(entry.preset);
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateRoute = (index: number, patch: Partial<RouteRow>) => {
|
const updateRoute = (index: number, patch: Partial<RouteRowValues>) => {
|
||||||
setRoutes((current) =>
|
setRoutes((current) =>
|
||||||
current.map((row, i) => (i === index ? { ...row, ...patch } : row)),
|
current.map((row, i) => (i === index ? { ...row, ...patch } : row)),
|
||||||
);
|
);
|
||||||
@@ -825,7 +850,7 @@ export function ClaudeDesktopProviderForm({
|
|||||||
</div>
|
</div>
|
||||||
{routes.map((route, index) => (
|
{routes.map((route, index) => (
|
||||||
<div
|
<div
|
||||||
key={`${route.route}-${index}`}
|
key={route.rowId}
|
||||||
className="grid grid-cols-1 gap-2 md:grid-cols-[1fr_1fr_92px_36px]"
|
className="grid grid-cols-1 gap-2 md:grid-cols-[1fr_1fr_92px_36px]"
|
||||||
>
|
>
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
@@ -935,11 +960,11 @@ export function ClaudeDesktopProviderForm({
|
|||||||
() =>
|
() =>
|
||||||
setRoutes((current) => [
|
setRoutes((current) => [
|
||||||
...current,
|
...current,
|
||||||
{
|
createRouteRow({
|
||||||
route: "",
|
route: "",
|
||||||
model: "",
|
model: "",
|
||||||
supports1m: false,
|
supports1m: false,
|
||||||
},
|
}),
|
||||||
]),
|
]),
|
||||||
t("claudeDesktop.addModel", { defaultValue: "添加模型" }),
|
t("claudeDesktop.addModel", { defaultValue: "添加模型" }),
|
||||||
)}
|
)}
|
||||||
@@ -949,7 +974,7 @@ export function ClaudeDesktopProviderForm({
|
|||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{routes.map((route, index) => (
|
{routes.map((route, index) => (
|
||||||
<div
|
<div
|
||||||
key={`${route.route}-${index}`}
|
key={route.rowId}
|
||||||
className="grid grid-cols-1 gap-2 md:grid-cols-[1fr_92px_36px]"
|
className="grid grid-cols-1 gap-2 md:grid-cols-[1fr_92px_36px]"
|
||||||
>
|
>
|
||||||
<div className="flex gap-1">
|
<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