feat(ai): unify model discovery and effort handling

This commit is contained in:
Guoyu Su authored and GitHub committed 2026-07-28 21:37:55 +08:00
1 parent c54b95e274
commit c7e7b7c9d9
41 files changed
+3291 -634

No files matched your search

+82 -16
View File
@@ -17,9 +17,9 @@ import { setDebugLoggingEnabled } from "@/lib/backend/debugLog";
import { DEFAULT_TABLE_COLUMN_TEMPLATE_FIELDS, normalizeTableColumnTemplateFields } from "@/lib/table/tableColumnTemplates";
import { DEFAULT_DATA_GRID_FONT_FAMILY, DEFAULT_UI_FONT_FAMILY } from "@/lib/app/appFonts";
import { safeLocalStorageGet, safeLocalStorageRemove } from "@/lib/backend/safeStorage";
import type { AiProvider, AiApiStyle, AiAuthMethod, AiEffortLevel, AiReasoningLevel, AiConfiguredModel, AiConfig, AiTestConnectionResult, AiConfigItem } from "@/types/ai";
import type { AiProvider, AiApiStyle, AiAuthMethod, AiEffortLevel, AiReasoningLevel, AiConfiguredModel, AiConfig, AiTestConnectionResult, AiConfigItem, AiChatSelectionState, AiEffortSelection, AiModelEffortPreference } from "@/types/ai";
export type { AiProvider, AiApiStyle, AiAuthMethod, AiEffortLevel, AiReasoningLevel, AiConfiguredModel, AiConfig, AiTestConnectionResult, AiConfigItem };
export type { AiProvider, AiApiStyle, AiAuthMethod, AiEffortLevel, AiReasoningLevel, AiConfiguredModel, AiConfig, AiTestConnectionResult, AiConfigItem, AiChatSelectionState, AiEffortSelection };
export interface DesktopSettings {
show_tray_icon: boolean;
@@ -980,6 +980,7 @@ export const useSettingsStore = defineStore("settings", () => {
const settingsPageActive = ref(false);
const settingsNavigationRequest = ref<SettingsNavigationRequest | null>(null);
const activeModel = ref<{ configId: string; modelId: string } | null>(null);
const effortPreferences = ref<AiModelEffortPreference[]>([]);
const isAiConfigLoaded = ref(false);
const aiConfigs = ref<AiConfigItem[]>([]);
const desktopSettings = ref<DesktopSettings>({ ...DEFAULT_DESKTOP_SETTINGS });
@@ -987,6 +988,8 @@ export const useSettingsStore = defineStore("settings", () => {
const isDesktopSettingsLoaded = ref(false);
const isMcpGlobalPolicyLoaded = ref(false);
const isEditorSettingsLoaded = ref(false);
let pendingAiChatSelection: AiChatSelectionState | null = null;
let aiChatSelectionSaveRunning = false;
const editorSettings = ref<EditorSettings>(normalizeEditorSettings({}));
@@ -1094,14 +1097,21 @@ export const useSettingsStore = defineStore("settings", () => {
aiConfigs.value = newConfigs.map(normalizeAiConfigItem);
} else {
// 迁移旧格式
aiConfigs.value = [];
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 };
const savedSelection = await api.loadAiChatSelection().catch(() => null);
effortPreferences.value = (savedSelection?.effortPreferences ?? []).filter((preference) => aiConfigs.value.some((config) => config.id === preference.configId));
const savedActive = savedSelection?.active;
const savedConfig = savedActive ? aiConfigs.value.find((config) => config.id === savedActive.configId) : undefined;
if (savedConfig && savedActive?.modelId.trim()) {
activeModel.value = { configId: savedConfig.id, modelId: savedActive.modelId.trim() };
} else {
const fallback = aiConfigs.value.find((config) => config.isDefault) || aiConfigs.value[0];
activeModel.value = fallback?.model.trim() ? { configId: fallback.id, modelId: fallback.model.trim() } : null;
if (activeModel.value) persistAiChatSelection();
}
isAiConfigLoaded.value = true;
@@ -1110,7 +1120,6 @@ export const useSettingsStore = defineStore("settings", () => {
async function reloadAiConfigs(): Promise<void> {
isAiConfigLoaded.value = false;
await initAiConfigs();
if (aiConfigs.value.length === 0) activeModel.value = null;
}
async function migrateToMultiConfig(): Promise<void> {
@@ -1153,23 +1162,36 @@ export const useSettingsStore = defineStore("settings", () => {
const normalized = normalizeAiConfigItem(config);
await api.saveAiConfigItem(normalized);
aiConfigs.value.push(normalized);
if (aiConfigs.value.length === 1) {
if (aiConfigs.value.length === 1 && normalized.model.trim()) {
activeModel.value = { configId: normalized.id, modelId: normalized.model };
persistAiChatSelection();
}
}
async function updateAiConfigItem(id: string, config: Partial<AiConfigItem>): Promise<void> {
const index = aiConfigs.value.findIndex((c) => c.id === id);
if (index !== -1) {
const updated = normalizeAiConfigItem({ ...aiConfigs.value[index], ...config });
const previous = aiConfigs.value[index];
const updated = normalizeAiConfigItem({ ...previous, ...config });
await api.saveAiConfigItem(updated);
aiConfigs.value[index] = updated;
if (previous.provider !== updated.provider) {
effortPreferences.value = effortPreferences.value.filter((preference) => preference.configId !== id);
if (activeModel.value?.configId === id) activeModel.value = null;
persistAiChatSelection();
}
}
}
async function deleteAiConfig(id: string): Promise<void> {
await api.deleteAiConfig(id);
aiConfigs.value = aiConfigs.value.filter((c) => c.id !== id);
effortPreferences.value = effortPreferences.value.filter((preference) => preference.configId !== id);
if (activeModel.value?.configId === id) {
const fallback = aiConfigs.value.find((config) => config.isDefault) || aiConfigs.value[0];
activeModel.value = fallback?.model.trim() ? { configId: fallback.id, modelId: fallback.model.trim() } : null;
}
persistAiChatSelection();
}
async function setDefaultAiConfig(id: string): Promise<void> {
@@ -1177,15 +1199,57 @@ 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 }) {
activeModel.value = model;
activeModel.value = { configId: model.configId, modelId: model.modelId.trim() };
persistAiChatSelection();
}
const activeEffort = computed<AiEffortSelection | null>(() => {
const active = activeModel.value;
if (!active) return null;
return effortPreferences.value.find((preference) => preference.configId === active.configId && preference.modelId === active.modelId)?.selection ?? null;
});
function updateActiveEffort(selection: AiEffortSelection | null) {
const active = activeModel.value;
if (!active) return;
effortPreferences.value = effortPreferences.value.filter((preference) => preference.configId !== active.configId || preference.modelId !== active.modelId);
if (selection) {
effortPreferences.value.push({
configId: active.configId,
modelId: active.modelId,
selection,
});
}
persistAiChatSelection();
}
function persistAiChatSelection() {
pendingAiChatSelection = {
version: 1,
active: activeModel.value ? { ...activeModel.value } : undefined,
effortPreferences: effortPreferences.value.map((preference) => ({
...preference,
selection: { ...preference.selection },
})),
};
if (!aiChatSelectionSaveRunning) void flushAiChatSelection();
}
async function flushAiChatSelection() {
aiChatSelectionSaveRunning = true;
try {
while (pendingAiChatSelection) {
const selection = pendingAiChatSelection;
pendingAiChatSelection = null;
await api.saveAiChatSelection(selection).catch(() => {});
}
} finally {
aiChatSelectionSaveRunning = false;
if (pendingAiChatSelection) void flushAiChatSelection();
}
}
const isConfigured = computed((): boolean => {
@@ -1352,6 +1416,7 @@ export const useSettingsStore = defineStore("settings", () => {
requestSettingsNavigation,
clearSettingsNavigationRequest,
activeModel,
activeEffort,
isAiConfigLoaded,
aiConfigs,
initAiConfigs,
@@ -1362,6 +1427,7 @@ export const useSettingsStore = defineStore("settings", () => {
deleteAiConfig,
setDefaultAiConfig,
updateActiveModel,
updateActiveEffort,
isConfigured,
isEditorSettingsLoaded,
editorSettings,