fix(ai): preserve last-used model during app session

This commit is contained in:
Abeautifulsnow authored and GitHub committed 2026-07-17 18:14:49 +08:00
1 parent c88fad6bd8
commit c417e54698
3 files changed
+142 -14

No files matched your search

@@ -1614,10 +1614,6 @@ async function deleteConversation(id: string) {
function startNewChat() { function startNewChat() {
clearMessages(); clearMessages();
showConversationList.value = false; showConversationList.value = false;
const defaultConfig = settings.aiConfigs.find((c) => c.isDefault) || settings.aiConfigs[0];
if (defaultConfig) {
settings.updateActiveModel({ configId: defaultConfig.id, modelId: defaultConfig.model });
}
} }
onMounted(async () => { onMounted(async () => {
+134 -1
View File
@@ -1,5 +1,7 @@
import { describe, expect, it } from "vitest"; import { beforeEach, describe, expect, it, vi } from "vitest";
import { normalizeDesktopSettings, normalizeEditorSettings } from "@/stores/settingsStore"; import { normalizeDesktopSettings, normalizeEditorSettings } from "@/stores/settingsStore";
import { createPinia, setActivePinia } from "pinia";
import type { AiConfigItem } from "@/types/ai";
describe("normalizeEditorSettings", () => { describe("normalizeEditorSettings", () => {
it("enables automatic table aliases by default", () => { it("enables automatic table aliases by default", () => {
@@ -162,3 +164,134 @@ describe("normalizeEditorSettings - tabLayout", () => {
expect(normalizeEditorSettings({ tabLayout: 123 } as any).tabLayout).toBe("scroll"); expect(normalizeEditorSettings({ tabLayout: 123 } as any).tabLayout).toBe("scroll");
}); });
}); });
// --- Helpers for Pinia store tests ---
function makeTestConfig(overrides: Partial<AiConfigItem> & { id: string }): AiConfigItem {
return {
provider: "openai",
apiKey: "",
authMethod: "api-key",
endpoint: "https://api.openai.com/v1/chat/completions",
model: "gpt-4o-mini",
apiStyle: "completions",
name: overrides.id,
...overrides,
} as AiConfigItem;
}
// --- activeModel lifecycle tests ---
describe("settingsStore activeModel lifecycle", () => {
beforeEach(() => {
vi.resetModules();
setActivePinia(createPinia());
});
it("updateActiveModel persists the model and does not change any config isDefault", async () => {
vi.doMock("@/lib/backend/api", () => ({
loadAiConfigs: vi.fn().mockResolvedValue([]),
loadAiConfig: vi.fn().mockResolvedValue(null),
loadAiProviderConfigs: vi.fn().mockResolvedValue(null),
}));
const { useSettingsStore } = await import("@/stores/settingsStore");
const store = useSettingsStore();
store.aiConfigs = [makeTestConfig({ id: "c1", model: "model-a", isDefault: true }), makeTestConfig({ id: "c2", model: "model-b", isDefault: false })];
store.isAiConfigLoaded = true;
store.updateActiveModel({ configId: "c1", modelId: "model-a" });
expect(store.activeModel).toEqual({ configId: "c1", modelId: "model-a" });
store.updateActiveModel({ configId: "c2", modelId: "model-b" });
expect(store.activeModel).toEqual({ configId: "c2", modelId: "model-b" });
// 核心保障:不改变任何配置的 isDefault
expect(store.aiConfigs[0].isDefault).toBe(true);
expect(store.aiConfigs[1].isDefault).toBe(false);
});
it("setDefaultAiConfig(id) on success points activeModel to the new default config", async () => {
const setDefaultAiConfig = vi.fn().mockResolvedValue(undefined);
vi.doMock("@/lib/backend/api", () => ({
loadAiConfigs: vi.fn().mockResolvedValue([]),
loadAiConfig: vi.fn().mockResolvedValue(null),
loadAiProviderConfigs: vi.fn().mockResolvedValue(null),
setDefaultAiConfig,
}));
const { useSettingsStore } = await import("@/stores/settingsStore");
const store = useSettingsStore();
store.aiConfigs = [makeTestConfig({ id: "c1", model: "model-a", isDefault: true }), makeTestConfig({ id: "c2", model: "model-b", isDefault: false })];
store.isAiConfigLoaded = true;
// 先手动切到非默认的配置
store.updateActiveModel({ configId: "c2", modelId: "model-b" });
expect(store.activeModel).toEqual({ configId: "c2", modelId: "model-b" });
await store.setDefaultAiConfig("c2");
expect(setDefaultAiConfig).toHaveBeenCalledWith("c2");
expect(store.aiConfigs[0].isDefault).toBe(false);
expect(store.aiConfigs[1].isDefault).toBe(true);
expect(store.activeModel).toEqual({ configId: "c2", modelId: "model-b" });
});
it("setDefaultAiConfig does not mutate state when backend call fails", async () => {
const error = new Error("backend error");
const setDefaultAiConfig = vi.fn().mockRejectedValue(error);
vi.doMock("@/lib/backend/api", () => ({
loadAiConfigs: vi.fn().mockResolvedValue([]),
loadAiConfig: vi.fn().mockResolvedValue(null),
loadAiProviderConfigs: vi.fn().mockResolvedValue(null),
setDefaultAiConfig,
}));
const { useSettingsStore } = await import("@/stores/settingsStore");
const store = useSettingsStore();
store.aiConfigs = [makeTestConfig({ id: "c1", model: "model-a", isDefault: true }), makeTestConfig({ id: "c2", model: "model-b", isDefault: false })];
store.isAiConfigLoaded = true;
store.updateActiveModel({ configId: "c1", modelId: "model-a" });
await expect(store.setDefaultAiConfig("c2")).rejects.toThrow("backend error");
// isDefault 不变
expect(store.aiConfigs[0].isDefault).toBe(true);
expect(store.aiConfigs[1].isDefault).toBe(false);
// activeModel 不变
expect(store.activeModel).toEqual({ configId: "c1", modelId: "model-a" });
});
it("reloadAiConfigs sets activeModel to null when config list is empty", async () => {
vi.doMock("@/lib/backend/api", () => ({
loadAiConfigs: vi.fn().mockResolvedValue([]),
loadAiConfig: vi.fn().mockResolvedValue(null),
loadAiProviderConfigs: vi.fn().mockResolvedValue(null),
}));
const { useSettingsStore } = await import("@/stores/settingsStore");
const store = useSettingsStore();
store.isAiConfigLoaded = false;
await store.reloadAiConfigs();
expect(store.activeModel).toBeNull();
});
it("reloadAiConfigs points activeModel to isDefault config, not first in list", async () => {
const configs = [makeTestConfig({ id: "c1", model: "model-a", isDefault: false }), makeTestConfig({ id: "c2", model: "model-b", isDefault: true }), makeTestConfig({ id: "c3", model: "model-c", isDefault: false })];
vi.doMock("@/lib/backend/api", () => ({
loadAiConfigs: vi.fn().mockResolvedValue(configs),
}));
const { useSettingsStore } = await import("@/stores/settingsStore");
const store = useSettingsStore();
store.isAiConfigLoaded = false;
await store.reloadAiConfigs();
expect(store.activeModel).toEqual({ configId: "c2", modelId: "model-b" });
});
});
+8 -9
View File
@@ -944,7 +944,8 @@ export const useSettingsStore = defineStore("settings", () => {
await migrateToMultiConfig(); await migrateToMultiConfig();
} }
// 同步活跃状态到默认配置 // 重置 activeModel 到默认配置是有意行为——activeModel 是本次运行 (run-scoped) 的末次使用选择,
// 应用启动和配置同步下载 (reloadAiConfigs) 两条路径均需丢弃会话内手动切换的模型、回到默认。
const defaultConfig = aiConfigs.value.find((c) => c.isDefault) || aiConfigs.value[0]; const defaultConfig = aiConfigs.value.find((c) => c.isDefault) || aiConfigs.value[0];
if (defaultConfig) { if (defaultConfig) {
activeModel.value = { configId: defaultConfig.id, modelId: defaultConfig.model }; activeModel.value = { configId: defaultConfig.id, modelId: defaultConfig.model };
@@ -956,14 +957,7 @@ export const useSettingsStore = defineStore("settings", () => {
async function reloadAiConfigs(): Promise<void> { async function reloadAiConfigs(): Promise<void> {
isAiConfigLoaded.value = false; isAiConfigLoaded.value = false;
await initAiConfigs(); await initAiConfigs();
// If the active config was deleted, fall back to the default if (aiConfigs.value.length === 0) activeModel.value = null;
if (activeModel.value && !aiConfigs.value.find((c) => c.id === activeModel.value!.configId)) {
if (aiConfigs.value.length > 0) {
activeModel.value = { configId: aiConfigs.value[0].id, modelId: aiConfigs.value[0].model };
} else {
activeModel.value = null;
}
}
} }
async function migrateToMultiConfig(): Promise<void> { async function migrateToMultiConfig(): Promise<void> {
@@ -1029,6 +1023,11 @@ export const useSettingsStore = defineStore("settings", () => {
aiConfigs.value.forEach((c) => { aiConfigs.value.forEach((c) => {
c.isDefault = c.id === id; c.isDefault = c.id === id;
}); });
const config = aiConfigs.value.find((c) => c.id === id);
if (config) {
// 修改默认配置时丢弃用户手动选择的模型,回到新默认——放在 await 之后确保后端持久化成功才执行
activeModel.value = { configId: config.id, modelId: config.model };
}
} }
function updateActiveModel(model: { configId: string; modelId: string }) { function updateActiveModel(model: { configId: string; modelId: string }) {