fix(ai): preserve last-used model during app session
This commit is contained in:
parent
c88fad6bd8
commit
c417e54698
|
|
@ -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 () => {
|
||||
|
|
|
|||
|
|
@ -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" });
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 }) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue