mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-10-25 17:22:27 +08:00
fix(providers): reread additive client state when a removal is refused
Kimi Code saves every /model pick as default_model, so the default shown in CC Switch can go stale while the page stays open. The backend rereads config.toml and refuses to remove the provider Kimi Code is using, but the page kept showing the old default and an untranslated English reason. Reread the additive app's live state after a failed removal as well as a successful one (now including the OpenCode, OpenClaw and Hermes default model queries), reread Kimi providers after failed edits and deletions, and explain the two Kimi "model in use" refusals in all four locales.
This commit is contained in:
1 parent
c6f85464e7
commit
1c52fa918d
9 files changed
+179
-35
No files matched your search
+38
-30
@@ -50,6 +50,7 @@ import { useTauriEvent } from "@/hooks/useTauriEvent";
|
||||
import { useLastValidValue } from "@/hooks/useLastValidValue";
|
||||
import {
|
||||
extractErrorMessage,
|
||||
translateKimiProviderMutationError,
|
||||
translatePiProviderMutationError,
|
||||
} from "@/utils/errorUtils";
|
||||
import { isTextEditableTarget } from "@/utils/domUtils";
|
||||
@@ -819,6 +820,35 @@ function App() {
|
||||
setEditingProvider(null);
|
||||
};
|
||||
|
||||
// 累加式应用的「已添加」和「默认」都现读客户端文件,移除前后重读一遍
|
||||
const refreshAdditiveLiveState = async (app: AppId) => {
|
||||
if (app === "pi") {
|
||||
await invalidatePiProviderCaches(queryClient);
|
||||
} else if (app === "opencode") {
|
||||
await Promise.all([
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["opencodeLiveProviderIds"],
|
||||
}),
|
||||
queryClient.invalidateQueries({ queryKey: ["opencodeDefaultModel"] }),
|
||||
]);
|
||||
} else if (app === "openclaw") {
|
||||
await Promise.all([
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: openclawKeys.liveProviderIds,
|
||||
}),
|
||||
queryClient.invalidateQueries({ queryKey: openclawKeys.defaultModel }),
|
||||
queryClient.invalidateQueries({ queryKey: openclawKeys.health }),
|
||||
]);
|
||||
} else if (app === "hermes") {
|
||||
await Promise.all([
|
||||
queryClient.invalidateQueries({ queryKey: hermesKeys.liveProviderIds }),
|
||||
queryClient.invalidateQueries({ queryKey: hermesKeys.modelConfig }),
|
||||
]);
|
||||
} else if (app === "mcode" || app === "kimi") {
|
||||
await queryClient.invalidateQueries({ queryKey: ["providers", app] });
|
||||
}
|
||||
};
|
||||
|
||||
const handleConfirmAction = async () => {
|
||||
if (!confirmAction) return;
|
||||
const { provider, action } = confirmAction;
|
||||
@@ -831,42 +861,20 @@ function App() {
|
||||
} catch (error) {
|
||||
const detail = extractErrorMessage(error);
|
||||
const description =
|
||||
activeApp === "pi"
|
||||
? translatePiProviderMutationError(detail, t) || detail
|
||||
: detail;
|
||||
if (activeApp === "pi") {
|
||||
void invalidatePiProviderCaches(queryClient).catch(() => undefined);
|
||||
}
|
||||
(activeApp === "pi"
|
||||
? translatePiProviderMutationError(detail, t)
|
||||
: activeApp === "kimi"
|
||||
? translateKimiProviderMutationError(detail, t)
|
||||
: "") || detail;
|
||||
// 默认模型可能在客户端里改过、页面还是旧的:失败了也重读,让「默认」和移除按钮跟上
|
||||
void refreshAdditiveLiveState(activeApp).catch(() => undefined);
|
||||
toast.error(t("notifications.removeFromConfigFailed"), {
|
||||
description: description || t("common.unknown"),
|
||||
closeButton: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (activeApp === "pi") {
|
||||
await invalidatePiProviderCaches(queryClient);
|
||||
}
|
||||
// Invalidate queries to refresh the isInConfig state
|
||||
if (activeApp === "opencode") {
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: ["opencodeLiveProviderIds"],
|
||||
});
|
||||
} else if (activeApp === "openclaw") {
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: openclawKeys.liveProviderIds,
|
||||
});
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: openclawKeys.health,
|
||||
});
|
||||
} else if (activeApp === "hermes") {
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: hermesKeys.liveProviderIds,
|
||||
});
|
||||
} else if (activeApp === "mcode" || activeApp === "kimi") {
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: ["providers", activeApp],
|
||||
});
|
||||
}
|
||||
await refreshAdditiveLiveState(activeApp);
|
||||
toast.success(
|
||||
activeApp === "pi"
|
||||
? t("pi.provider.removed", {
|
||||
|
||||
@@ -4962,6 +4962,10 @@
|
||||
"title": "Add a Kimi Code provider",
|
||||
"description": "Configure an endpoint, API key and models. Existing Kimi configuration is imported automatically."
|
||||
},
|
||||
"deleteSessionHint": "Delete this session in Kimi Code to update its index and history together."
|
||||
"deleteSessionHint": "Delete this session in Kimi Code to update its index and history together.",
|
||||
"error": {
|
||||
"defaultModelInUse": "Kimi Code is using a model from this provider. Switch to another model with /model in Kimi Code first.",
|
||||
"secondaryModelInUse": "Kimi Code's subagent model uses a model from this provider. Change it with /secondary-model in Kimi Code first."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4962,6 +4962,10 @@
|
||||
"always_thinking": "常に思考",
|
||||
"tool_use": "ツール呼び出し",
|
||||
"dynamically_loaded_tools": "必要に応じてツールを読み込む"
|
||||
},
|
||||
"error": {
|
||||
"defaultModelInUse": "Kimi Code がこのプロバイダーのモデルを使用中です。先に Kimi Code の /model でほかのモデルに切り替えてください。",
|
||||
"secondaryModelInUse": "Kimi Code のサブエージェントモデルがこのプロバイダーのモデルを使用中です。先に Kimi Code の /secondary-model で変更してください。"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4963,6 +4963,10 @@
|
||||
"always_thinking": "始終思考",
|
||||
"tool_use": "工具呼叫",
|
||||
"dynamically_loaded_tools": "按需載入工具"
|
||||
},
|
||||
"error": {
|
||||
"defaultModelInUse": "Kimi Code 正在使用此供應商的模型,請先在 Kimi Code 中用 /model 切換到其他模型",
|
||||
"secondaryModelInUse": "Kimi Code 的子代理模型正在使用此供應商的模型,請先在 Kimi Code 中用 /secondary-model 更換"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4962,6 +4962,10 @@
|
||||
"title": "添加 Kimi Code 供应商",
|
||||
"description": "配置请求地址、API Key 和模型;已有 Kimi 配置会自动导入。"
|
||||
},
|
||||
"deleteSessionHint": "请在 Kimi Code 中删除,以同时更新会话索引和历史记录。"
|
||||
"deleteSessionHint": "请在 Kimi Code 中删除,以同时更新会话索引和历史记录。",
|
||||
"error": {
|
||||
"defaultModelInUse": "Kimi Code 正在使用该供应商的模型,请先在 Kimi Code 中用 /model 切换到其他模型",
|
||||
"secondaryModelInUse": "Kimi Code 的子代理模型正在使用该供应商的模型,请先在 Kimi Code 中用 /secondary-model 更换"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import { parseLiveEditConflict } from "@/lib/errors/liveEditConflict";
|
||||
import type { Provider, SessionMeta, Settings } from "@/types";
|
||||
import {
|
||||
extractErrorMessage,
|
||||
translateKimiProviderMutationError,
|
||||
translatePiProviderMutationError,
|
||||
} from "@/utils/errorUtils";
|
||||
import { generateUUID } from "@/utils/uuid";
|
||||
@@ -150,7 +151,9 @@ export const useAddProviderMutation = (appId: AppId) => {
|
||||
const detail =
|
||||
(appId === "pi"
|
||||
? translatePiProviderMutationError(rawDetail, t)
|
||||
: "") ||
|
||||
: appId === "kimi"
|
||||
? translateKimiProviderMutationError(rawDetail, t)
|
||||
: "") ||
|
||||
rawDetail ||
|
||||
t("common.unknown");
|
||||
toast.error(
|
||||
@@ -218,7 +221,9 @@ export const useUpdateProviderMutation = (appId: AppId) => {
|
||||
const detail =
|
||||
(appId === "pi"
|
||||
? translatePiProviderMutationError(rawDetail, t)
|
||||
: "") ||
|
||||
: appId === "kimi"
|
||||
? translateKimiProviderMutationError(rawDetail, t)
|
||||
: "") ||
|
||||
rawDetail ||
|
||||
t("common.unknown");
|
||||
toast.error(
|
||||
@@ -231,6 +236,11 @@ export const useUpdateProviderMutation = (appId: AppId) => {
|
||||
onSettled: async () => {
|
||||
if (appId === "pi") {
|
||||
await invalidatePiProviderCaches(queryClient);
|
||||
} else if (appId === "kimi") {
|
||||
// 默认模型可能在 Kimi Code 里改过:失败时也重读,页面上的「默认」才跟得上
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: ["providers", "kimi"],
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -294,7 +304,9 @@ export const useDeleteProviderMutation = (appId: AppId) => {
|
||||
const detail =
|
||||
(appId === "pi"
|
||||
? translatePiProviderMutationError(rawDetail, t)
|
||||
: "") ||
|
||||
: appId === "kimi"
|
||||
? translateKimiProviderMutationError(rawDetail, t)
|
||||
: "") ||
|
||||
rawDetail ||
|
||||
t("common.unknown");
|
||||
toast.error(
|
||||
@@ -307,6 +319,11 @@ export const useDeleteProviderMutation = (appId: AppId) => {
|
||||
onSettled: async () => {
|
||||
if (appId === "pi") {
|
||||
await invalidatePiProviderCaches(queryClient);
|
||||
} else if (appId === "kimi") {
|
||||
// 默认模型可能在 Kimi Code 里改过:失败时也重读,页面上的「默认」才跟得上
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: ["providers", "kimi"],
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -59,6 +59,24 @@ export const translatePiProviderMutationError = (
|
||||
return "";
|
||||
};
|
||||
|
||||
/**
|
||||
* Kimi 的默认模型可能在 Kimi Code 里被改过、页面上还是旧的,
|
||||
* 后端按 config.toml 现读拒绝后,把这两条英文原因换成说明。
|
||||
*/
|
||||
export const translateKimiProviderMutationError = (
|
||||
message: string,
|
||||
t: (key: string, options?: Record<string, unknown>) => string,
|
||||
): string => {
|
||||
if (!message) return "";
|
||||
if (message.includes("Select another default model in Kimi Code")) {
|
||||
return t("kimi.error.defaultModelInUse");
|
||||
}
|
||||
if (message.includes("Update the secondary model selection in Kimi Code")) {
|
||||
return t("kimi.error.secondaryModelInUse");
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
||||
/**
|
||||
* 将已知的 MCP 相关后端错误(通常为中文硬编码)映射为 i18n 文案
|
||||
* 采用包含式匹配,尽量稳健地覆盖不同上下文的相似消息。
|
||||
|
||||
@@ -791,6 +791,72 @@ describe("App integration with MSW", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("rereads Kimi's default after a removal is refused for a stale default", async () => {
|
||||
localStorage.setItem("cc-switch-last-app", "kimi");
|
||||
let kimiCurrent = "kimi-sub";
|
||||
let currentRequests = 0;
|
||||
server.use(
|
||||
http.post("http://tauri.local/get_providers", async ({ request }) => {
|
||||
const { app } = (await request.json()) as { app: string };
|
||||
if (app !== "kimi") return;
|
||||
return HttpResponse.json({
|
||||
go: {
|
||||
id: "go",
|
||||
name: "OpenCode Go",
|
||||
settingsConfig: {},
|
||||
meta: { liveConfigManaged: true },
|
||||
},
|
||||
"kimi-sub": {
|
||||
id: "kimi-sub",
|
||||
name: "Kimi subscription",
|
||||
settingsConfig: {},
|
||||
meta: { liveConfigManaged: true },
|
||||
},
|
||||
});
|
||||
}),
|
||||
http.post(
|
||||
"http://tauri.local/get_current_provider",
|
||||
async ({ request }) => {
|
||||
const { app } = (await request.json()) as { app: string };
|
||||
if (app !== "kimi") return;
|
||||
currentRequests += 1;
|
||||
return HttpResponse.json(kimiCurrent);
|
||||
},
|
||||
),
|
||||
http.post("http://tauri.local/remove_provider_from_live_config", () =>
|
||||
HttpResponse.json(
|
||||
"无效输入: Select another default model in Kimi Code before removing this model or provider",
|
||||
{ status: 500 },
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
const { default: App } = await import("@/App");
|
||||
renderApp(App);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(screen.getByTestId("current-provider")).toHaveTextContent(
|
||||
"kimi-sub",
|
||||
),
|
||||
);
|
||||
// 用户在 Kimi Code 里用 /model 换到了 OpenCode Go 的模型,页面还没重读
|
||||
kimiCurrent = "go";
|
||||
const requestsBeforeRemoval = currentRequests;
|
||||
fireEvent.click(screen.getByText("remove"));
|
||||
fireEvent.click(screen.getByText("confirm-delete"));
|
||||
|
||||
await waitFor(() =>
|
||||
expect(toastErrorMock).toHaveBeenCalledWith(
|
||||
"notifications.removeFromConfigFailed",
|
||||
expect.objectContaining({ description: "kimi.error.defaultModelInUse" }),
|
||||
),
|
||||
);
|
||||
await waitFor(() =>
|
||||
expect(screen.getByTestId("current-provider")).toHaveTextContent("go"),
|
||||
);
|
||||
expect(currentRequests).toBeGreaterThan(requestsBeforeRemoval);
|
||||
});
|
||||
|
||||
it("warns without blocking when removing Pi's global default provider", async () => {
|
||||
localStorage.setItem("cc-switch-last-app", "pi");
|
||||
setProviders("pi", {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
extractErrorMessage,
|
||||
translateKimiProviderMutationError,
|
||||
translatePiProviderMutationError,
|
||||
} from "@/utils/errorUtils";
|
||||
|
||||
@@ -30,4 +31,22 @@ describe("error utilities", () => {
|
||||
),
|
||||
).toBe("pi.form.providerKeyDuplicate");
|
||||
});
|
||||
|
||||
it("explains Kimi removals refused because Kimi Code still uses the model", () => {
|
||||
const t = vi.fn((key: string) => key);
|
||||
|
||||
expect(
|
||||
translateKimiProviderMutationError(
|
||||
"无效输入: Select another default model in Kimi Code before removing this model or provider",
|
||||
t,
|
||||
),
|
||||
).toBe("kimi.error.defaultModelInUse");
|
||||
expect(
|
||||
translateKimiProviderMutationError(
|
||||
"无效输入: Update the secondary model selection in Kimi Code before removing this model or provider",
|
||||
t,
|
||||
),
|
||||
).toBe("kimi.error.secondaryModelInUse");
|
||||
expect(translateKimiProviderMutationError("other failure", t)).toBe("");
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user