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

This commit is contained in:
Abeautifulsnow 2026-07-17 18:14:49 +08:00 committed by GitHub
parent c88fad6bd8
commit c417e54698
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 142 additions and 14 deletions

View File

@ -1614,10 +1614,6 @@ async function deleteConversation(id: string) {
function startNewChat() {
clearMessages();
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 () => {

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 { createPinia, setActivePinia } from "pinia";
import type { AiConfigItem } from "@/types/ai";
describe("normalizeEditorSettings", () => {
it("enables automatic table aliases by default", () => {
@ -162,3 +164,134 @@ describe("normalizeEditorSettings - tabLayout", () => {
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" });
});
});

View File

@ -944,7 +944,8 @@ export const useSettingsStore = defineStore("settings", () => {
await migrateToMultiConfig();
}
// 同步活跃状态到默认配置
// 重置 activeModel 到默认配置是有意行为——activeModel 是本次运行 (run-scoped) 的末次使用选择,
// 应用启动和配置同步下载 (reloadAiConfigs) 两条路径均需丢弃会话内手动切换的模型、回到默认。
const defaultConfig = aiConfigs.value.find((c) => c.isDefault) || aiConfigs.value[0];
if (defaultConfig) {
activeModel.value = { configId: defaultConfig.id, modelId: defaultConfig.model };
@ -956,14 +957,7 @@ export const useSettingsStore = defineStore("settings", () => {
async function reloadAiConfigs(): Promise<void> {
isAiConfigLoaded.value = false;
await initAiConfigs();
// If the active config was deleted, fall back to the default
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;
}
}
if (aiConfigs.value.length === 0) activeModel.value = null;
}
async function migrateToMultiConfig(): Promise<void> {
@ -1029,6 +1023,11 @@ export const useSettingsStore = defineStore("settings", () => {
aiConfigs.value.forEach((c) => {
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 }) {