import { defineStore } from 'pinia'; import { useModelStore } from './model'; import { persistDefaultHideWorkspace, useUiStore } from './ui'; export type BlockDisplayMode = 'traditional' | 'stacked' | 'minimal'; export type CompactMessageDisplay = 'full' | 'brief'; type RunMode = 'fast' | 'thinking' | 'deep'; type PermissionMode = 'readonly' | 'approval' | 'auto_approval' | 'unrestricted'; type CommunicationStyle = 'default' | 'human_like' | 'auto'; type ConversationContinuity = 'low' | 'medium' | 'high'; type VersioningBackupMode = 'shallow' | 'full'; interface PersonalForm { enabled: boolean; communication_style: CommunicationStyle; conversation_continuity: ConversationContinuity; auto_generate_title: boolean; recent_conversations_prompt_enabled: boolean; recent_conversations_prompt_limit: number | string; tool_intent_enabled: boolean; skill_hints_enabled: boolean; skill_strict_terminal_enabled: boolean; skill_strict_sub_agent_enabled: boolean; skill_strict_run_command_foreground_enabled: boolean; skill_strict_run_command_background_enabled: boolean; silent_tool_disable: boolean; enhanced_tool_display: boolean; compact_message_display: CompactMessageDisplay; show_status_avatar: boolean; show_git_status_bar: boolean; auto_open_terminal_panel: boolean; stacked_hide_borders: boolean; enhanced_tool_display_categories: string[]; enabled_skills: string[]; self_identify: string; user_name: string; use_custom_names: boolean; profession: string; tone: string; considerations: string[]; thinking_interval: number | null; disabled_tool_categories: string[]; default_run_mode: RunMode | null; default_permission_mode: PermissionMode; versioning_enabled_by_default: boolean; versioning_backup_mode: VersioningBackupMode; versioning_restore_mode: 'overwrite'; default_model: string | null; image_compression: string; auto_shallow_compress_enabled: boolean; auto_deep_compress_enabled: boolean; shallow_compress_trigger_tokens: number | null; shallow_compress_keep_recent_tools: number | null; shallow_compress_keep_user_turn_tools: number | null; shallow_compress_max_replace_per_round: number | null; shallow_compress_trigger_tool_calls_interval: number | null; deep_compress_trigger_tokens: number | null; deep_compress_form: 'file' | 'inject'; agents_md_auto_inject: boolean; allow_root_file_creation: boolean; default_hide_workspace: boolean; theme: 'classic' | 'light' | 'dark'; goal_review_mode: 'readonly' | 'active'; goal_end_conditions: string[]; goal_max_turns: number; goal_max_tokens: number | null; } interface ExperimentState { blockDisplayMode: BlockDisplayMode; compactMessageDisplay: CompactMessageDisplay; } interface PersonalizationState { visible: boolean; loading: boolean; saving: boolean; loaded: boolean; status: string; error: string; maxConsiderations: number; toggleUpdating: boolean; overlayPressActive: boolean; newConsideration: string; tonePresets: string[]; draggedConsiderationIndex: number | null; form: PersonalForm; toolCategories: Array<{ id: string; label: string }>; skillsCatalog: Array<{ id: string; label: string; description?: string }>; thinkingIntervalDefault: number; thinkingIntervalRange: { min: number; max: number }; recentConversationsPromptLimitRange: { min: number; max: number }; experiments: ExperimentState; } const DEFAULT_INTERVAL = 10; const DEFAULT_INTERVAL_RANGE = { min: 1, max: 50 }; const DEFAULT_RECENT_CONVERSATIONS_PROMPT_LIMIT = 10; const DEFAULT_RECENT_CONVERSATIONS_PROMPT_LIMIT_RANGE = { min: 1, max: 30 }; const DEFAULT_SHALLOW_COMPRESS_TRIGGER_TOKENS = 80000; const DEFAULT_DEEP_COMPRESS_TRIGGER_TOKENS = 150000; const RUN_MODE_OPTIONS: RunMode[] = ['fast', 'thinking', 'deep']; const EXPERIMENT_STORAGE_KEY = 'agents_personalization_experiments'; const THEME_STORAGE_KEY = 'agents_ui_theme'; const STACKED_HIDE_BORDERS_STORAGE_KEY = 'agents_stacked_hide_borders'; const loadCachedTheme = (): PersonalForm['theme'] => { if (typeof window === 'undefined' || !window.localStorage) { return 'classic'; } const saved = window.localStorage.getItem(THEME_STORAGE_KEY); return saved === 'light' || saved === 'dark' || saved === 'classic' ? saved : 'classic'; }; const loadCachedStackedHideBorders = (): boolean => { if (typeof window === 'undefined' || !window.localStorage) { return false; } return window.localStorage.getItem(STACKED_HIDE_BORDERS_STORAGE_KEY) === 'true'; }; const persistStackedHideBorders = (value: boolean) => { if (typeof window === 'undefined' || !window.localStorage) { return; } try { window.localStorage.setItem(STACKED_HIDE_BORDERS_STORAGE_KEY, value ? 'true' : 'false'); } catch (error) { console.warn('写入堆叠块边线设置失败:', error); } }; const defaultForm = (): PersonalForm => ({ enabled: false, communication_style: 'default', conversation_continuity: 'medium', auto_generate_title: true, recent_conversations_prompt_enabled: false, recent_conversations_prompt_limit: DEFAULT_RECENT_CONVERSATIONS_PROMPT_LIMIT, tool_intent_enabled: true, skill_hints_enabled: false, skill_strict_terminal_enabled: false, skill_strict_sub_agent_enabled: false, skill_strict_run_command_foreground_enabled: false, skill_strict_run_command_background_enabled: false, silent_tool_disable: false, enhanced_tool_display: true, compact_message_display: 'full', show_status_avatar: true, show_git_status_bar: true, auto_open_terminal_panel: true, stacked_hide_borders: loadCachedStackedHideBorders(), enhanced_tool_display_categories: [], enabled_skills: [], self_identify: '', user_name: '', use_custom_names: false, profession: '', tone: '', considerations: [], thinking_interval: null, disabled_tool_categories: [], default_run_mode: null, default_permission_mode: 'unrestricted', versioning_enabled_by_default: true, versioning_backup_mode: 'shallow', versioning_restore_mode: 'overwrite', default_model: null, image_compression: 'original', auto_shallow_compress_enabled: false, auto_deep_compress_enabled: false, shallow_compress_trigger_tokens: null, shallow_compress_keep_recent_tools: null, shallow_compress_keep_user_turn_tools: null, shallow_compress_max_replace_per_round: null, shallow_compress_trigger_tool_calls_interval: null, deep_compress_trigger_tokens: null, deep_compress_form: 'file', agents_md_auto_inject: false, allow_root_file_creation: false, default_hide_workspace: false, theme: loadCachedTheme(), goal_review_mode: 'readonly', goal_end_conditions: ['max_turns'], goal_max_turns: 5, goal_max_tokens: null }); const defaultExperimentState = (): ExperimentState => ({ blockDisplayMode: 'stacked', compactMessageDisplay: 'full' }); const loadExperimentState = (): ExperimentState => { if (typeof window === 'undefined' || !window.localStorage) { return defaultExperimentState(); } try { const raw = window.localStorage.getItem(EXPERIMENT_STORAGE_KEY); if (!raw) { return defaultExperimentState(); } const parsed = JSON.parse(raw); // 兼容旧版 stackedBlocksEnabled let blockDisplayMode: BlockDisplayMode = defaultExperimentState().blockDisplayMode; if ( typeof parsed?.blockDisplayMode === 'string' && ['traditional', 'stacked', 'minimal'].includes(parsed.blockDisplayMode) ) { blockDisplayMode = parsed.blockDisplayMode; } else if (typeof parsed?.stackedBlocksEnabled === 'boolean') { // 兼容旧版:true -> stacked, false -> traditional blockDisplayMode = parsed.stackedBlocksEnabled ? 'stacked' : 'traditional'; } let compactMessageDisplay: CompactMessageDisplay = defaultExperimentState().compactMessageDisplay; if ( typeof parsed?.compactMessageDisplay === 'string' && ['full', 'brief'].includes(parsed.compactMessageDisplay) ) { compactMessageDisplay = parsed.compactMessageDisplay; } return { blockDisplayMode, compactMessageDisplay }; } catch (error) { console.warn('无法读取实验功能设置:', error); return defaultExperimentState(); } }; // 防抖自动保存计时器(非响应式,避免触发不必要的重渲染) let autoSaveTimer: ReturnType | null = null; export const usePersonalizationStore = defineStore('personalization', { state: (): PersonalizationState => ({ visible: false, loading: false, saving: false, loaded: false, status: '', error: '', maxConsiderations: 10, toggleUpdating: false, overlayPressActive: false, newConsideration: '', tonePresets: ['健谈', '幽默', '直言不讳', '鼓励性', '诗意', '企业商务', '打破常规', '同理心'], draggedConsiderationIndex: null, form: defaultForm(), toolCategories: [], skillsCatalog: [], thinkingIntervalDefault: DEFAULT_INTERVAL, thinkingIntervalRange: { ...DEFAULT_INTERVAL_RANGE }, recentConversationsPromptLimitRange: { ...DEFAULT_RECENT_CONVERSATIONS_PROMPT_LIMIT_RANGE }, experiments: loadExperimentState() }), actions: { async openDrawer() { this.visible = true; // 每次打开都刷新数据,确保显示最新内容 if (!this.loading) { await this.fetchPersonalization(); } }, closeDrawer() { this.visible = false; this.draggedConsiderationIndex = null; this.overlayPressActive = false; }, handleOverlayPressStart(event: Event) { if (event && (event as MouseEvent).type === 'mousedown') { const mouse = event as MouseEvent; if (mouse.button !== 0) { return; } } this.overlayPressActive = true; }, handleOverlayPressEnd() { if (!this.overlayPressActive) { return; } this.overlayPressActive = false; this.closeDrawer(); }, handleOverlayPressCancel() { this.overlayPressActive = false; }, async fetchPersonalization() { this.loading = true; this.error = ''; try { const resp = await fetch('/api/personalization'); const result = await resp.json(); if (!resp.ok || !result.success) { throw new Error(result.error || '加载失败'); } this.applyPersonalizationData(result.data || {}); this.applyPersonalizationMeta(result); this.loaded = true; } catch (error: any) { this.error = error?.message || '加载失败'; } finally { this.loading = false; } }, applyPersonalizationData(data: any) { // 若后端未返回默认模型(旧版本接口),保持当前已选模型而不是回退到内置模型 const fallbackModel = (this.form && typeof this.form.default_model === 'string' ? this.form.default_model : null); const fallbackTheme = this.form?.theme || loadCachedTheme(); this.form = { enabled: !!data.enabled, communication_style: data.communication_style === 'human_like' || data.communication_style === 'auto' ? data.communication_style : 'default', conversation_continuity: data.conversation_continuity === 'low' || data.conversation_continuity === 'high' ? data.conversation_continuity : 'medium', auto_generate_title: data.auto_generate_title !== false, recent_conversations_prompt_enabled: !!data.recent_conversations_prompt_enabled, recent_conversations_prompt_limit: this.normalizeRecentConversationsPromptLimit(data.recent_conversations_prompt_limit), tool_intent_enabled: !!data.tool_intent_enabled, skill_hints_enabled: !!data.skill_hints_enabled, skill_strict_terminal_enabled: !!data.skill_strict_terminal_enabled, skill_strict_sub_agent_enabled: !!data.skill_strict_sub_agent_enabled, skill_strict_run_command_foreground_enabled: !!data.skill_strict_run_command_foreground_enabled, skill_strict_run_command_background_enabled: !!data.skill_strict_run_command_background_enabled, silent_tool_disable: !!data.silent_tool_disable, enhanced_tool_display: data.enhanced_tool_display !== false, compact_message_display: data.compact_message_display === 'brief' ? 'brief' : 'full', show_status_avatar: data.show_status_avatar !== false, show_git_status_bar: data.show_git_status_bar !== false, auto_open_terminal_panel: data.auto_open_terminal_panel !== false, stacked_hide_borders: !!data.stacked_hide_borders, enhanced_tool_display_categories: Array.isArray(data.enhanced_tool_display_categories) ? data.enhanced_tool_display_categories.filter((item: unknown) => typeof item === 'string') : [], enabled_skills: Array.isArray(data.enabled_skills) ? data.enabled_skills.filter((item: unknown) => typeof item === 'string') : [], self_identify: data.self_identify || '', user_name: data.user_name || '', use_custom_names: !!data.use_custom_names, profession: data.profession || '', tone: data.tone || '', considerations: Array.isArray(data.considerations) ? [...data.considerations] : [], thinking_interval: typeof data.thinking_interval === 'number' ? data.thinking_interval : null, disabled_tool_categories: Array.isArray(data.disabled_tool_categories) ? data.disabled_tool_categories.filter((item: unknown) => typeof item === 'string') : [], default_run_mode: typeof data.default_run_mode === 'string' && RUN_MODE_OPTIONS.includes(data.default_run_mode as RunMode) ? (data.default_run_mode as RunMode) : null, default_permission_mode: data.default_permission_mode === 'readonly' || data.default_permission_mode === 'approval' || data.default_permission_mode === 'auto_approval' || data.default_permission_mode === 'unrestricted' ? data.default_permission_mode : 'unrestricted', versioning_enabled_by_default: data.versioning_enabled_by_default !== false, versioning_backup_mode: data.versioning_backup_mode === 'full' ? 'full' : 'shallow', versioning_restore_mode: 'overwrite', default_model: typeof data.default_model === 'string' ? data.default_model : fallbackModel, image_compression: typeof data.image_compression === 'string' ? data.image_compression : 'original', auto_shallow_compress_enabled: !!data.auto_shallow_compress_enabled, auto_deep_compress_enabled: !!data.auto_deep_compress_enabled, shallow_compress_trigger_tokens: this.normalizeCompressionNumber( data.shallow_compress_trigger_tokens ), shallow_compress_keep_recent_tools: this.normalizeCompressionNumber( data.shallow_compress_keep_recent_tools ), shallow_compress_keep_user_turn_tools: this.normalizeCompressionNumber( data.shallow_compress_keep_user_turn_tools ), shallow_compress_max_replace_per_round: this.normalizeCompressionNumber( data.shallow_compress_max_replace_per_round ), shallow_compress_trigger_tool_calls_interval: this.normalizeCompressionNumber( data.shallow_compress_trigger_tool_calls_interval ), deep_compress_trigger_tokens: this.normalizeCompressionNumber( data.deep_compress_trigger_tokens ), deep_compress_form: data.deep_compress_form === 'inject' ? 'inject' : 'file', agents_md_auto_inject: !!data.agents_md_auto_inject, allow_root_file_creation: !!data.allow_root_file_creation, default_hide_workspace: !!data.default_hide_workspace, theme: ['classic', 'light', 'dark'].includes(data.theme) ? data.theme : fallbackTheme, goal_review_mode: data.goal_review_mode === 'active' ? 'active' : 'readonly', goal_end_conditions: Array.isArray(data.goal_end_conditions) ? data.goal_end_conditions.filter( (x: any) => x === 'max_turns' || x === 'max_tokens' ) : ['max_turns'], goal_max_turns: typeof data.goal_max_turns === 'number' && data.goal_max_turns > 0 ? data.goal_max_turns : 5, goal_max_tokens: typeof data.goal_max_tokens === 'number' && data.goal_max_tokens > 0 ? data.goal_max_tokens : null }; // 如果theme发生变化,应用到界面 const currentTheme = (typeof window !== 'undefined' && window.localStorage) ? window.localStorage.getItem(THEME_STORAGE_KEY) : null; if (this.form.theme !== currentTheme) { this.applyTheme(this.form.theme); } persistDefaultHideWorkspace(this.form.default_hide_workspace); persistStackedHideBorders(this.form.stacked_hide_borders); if (this.form.default_hide_workspace) { useUiStore().setWorkspaceCollapsed(true); } // 简略消息显示:以配置文件为准,同步到旧版 localStorage 镜像供 ChatArea 读取。 // 一次性迁移:后端仍为默认 full,但本地缓存遗留 brief(旧版纯前端记录)时,回写到配置文件。 const cachedCompact = this.experiments.compactMessageDisplay; if (this.form.compact_message_display === 'full' && cachedCompact === 'brief') { this.form = { ...this.form, compact_message_display: 'brief' }; void this.persistCompactMessageDisplay('brief'); } this.experiments = { ...this.experiments, compactMessageDisplay: this.form.compact_message_display }; this.persistExperiments(); this.clearFeedback(); }, applyTheme(theme: 'classic' | 'light' | 'dark') { if (typeof window === 'undefined') return; // 同步到localStorage window.localStorage.setItem(THEME_STORAGE_KEY, theme); // 应用主题 const root = document.documentElement; root.setAttribute('data-theme', theme); document.body.setAttribute('data-theme', theme); }, normalizeCompressionNumber(value: any): number | null { if (value === null || typeof value === 'undefined' || value === '') { return null; } const parsed = Number(value); if (!Number.isFinite(parsed)) { return null; } const rounded = Math.round(parsed); if (rounded <= 0) { return null; } return rounded; }, normalizeRecentConversationsPromptLimit(value: any): number { const parsed = Number(value); const min = this.recentConversationsPromptLimitRange.min ?? DEFAULT_RECENT_CONVERSATIONS_PROMPT_LIMIT_RANGE.min; const max = this.recentConversationsPromptLimitRange.max ?? DEFAULT_RECENT_CONVERSATIONS_PROMPT_LIMIT_RANGE.max; if (!Number.isFinite(parsed)) { return DEFAULT_RECENT_CONVERSATIONS_PROMPT_LIMIT; } return Math.max(min, Math.min(max, Math.round(parsed))); }, applyPersonalizationMeta(payload: any) { if (payload && typeof payload.thinking_interval_default === 'number') { this.thinkingIntervalDefault = payload.thinking_interval_default; } else { this.thinkingIntervalDefault = DEFAULT_INTERVAL; } if (payload && payload.thinking_interval_range) { const { min, max } = payload.thinking_interval_range; this.thinkingIntervalRange = { min: typeof min === 'number' ? min : DEFAULT_INTERVAL_RANGE.min, max: typeof max === 'number' ? max : DEFAULT_INTERVAL_RANGE.max }; } else { this.thinkingIntervalRange = { ...DEFAULT_INTERVAL_RANGE }; } if (payload && payload.recent_conversations_prompt_limit_range) { const { min, max } = payload.recent_conversations_prompt_limit_range; this.recentConversationsPromptLimitRange = { min: typeof min === 'number' ? min : DEFAULT_RECENT_CONVERSATIONS_PROMPT_LIMIT_RANGE.min, max: typeof max === 'number' ? max : DEFAULT_RECENT_CONVERSATIONS_PROMPT_LIMIT_RANGE.max }; } else { this.recentConversationsPromptLimitRange = { ...DEFAULT_RECENT_CONVERSATIONS_PROMPT_LIMIT_RANGE }; } if (payload && Array.isArray(payload.tool_categories)) { this.toolCategories = payload.tool_categories .map((item: { id?: string; label?: string } = {}) => ({ id: typeof item.id === 'string' ? item.id : String(item.id ?? ''), label: (item.label && String(item.label)) || (typeof item.id === 'string' ? item.id : String(item.id ?? '')) })) .filter((item: { id: string }) => !!item.id); } else { this.toolCategories = []; } if (payload && Array.isArray(payload.skills_catalog)) { this.skillsCatalog = payload.skills_catalog .map((item: { id?: string; label?: string; description?: string } = {}) => ({ id: typeof item.id === 'string' ? item.id : String(item.id ?? ''), label: (item.label && String(item.label)) || (typeof item.id === 'string' ? item.id : String(item.id ?? '')), description: typeof item.description === 'string' ? item.description : undefined })) .filter((item: { id: string }) => !!item.id); } else { this.skillsCatalog = []; } }, clearFeedback() { this.status = ''; this.error = ''; }, async toggleEnabled() { if (this.toggleUpdating) { return; } const newValue = !this.form.enabled; const previousValue = this.form.enabled; this.toggleUpdating = true; this.status = ''; this.error = ''; this.form.enabled = newValue; try { const resp = await fetch('/api/personalization', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ enabled: newValue }) }); const result = await resp.json(); if (!resp.ok || !result.success) { throw new Error(result.error || '更新失败'); } if (result.data) { this.applyPersonalizationData(result.data); } this.applyPersonalizationMeta(result); const statusLabel = newValue ? '已启用' : '已停用'; this.status = statusLabel; setTimeout(() => { if (this.status === statusLabel) { this.status = ''; } }, 2000); } catch (error: any) { this.form.enabled = previousValue; this.error = error?.message || '更新失败'; } finally { this.toggleUpdating = false; } }, async save() { if (this.saving) { return; } this.saving = true; this.status = ''; this.error = ''; try { const shallowTrigger = this.normalizeCompressionNumber(this.form.shallow_compress_trigger_tokens) ?? DEFAULT_SHALLOW_COMPRESS_TRIGGER_TOKENS; const deepTrigger = this.normalizeCompressionNumber(this.form.deep_compress_trigger_tokens) ?? DEFAULT_DEEP_COMPRESS_TRIGGER_TOKENS; if (deepTrigger <= shallowTrigger) { throw new Error('深压缩触发上下文必须大于浅压缩触发上下文'); } const resp = await fetch('/api/personalization', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(this.form) }); const result = await resp.json(); if (!resp.ok || !result.success) { throw new Error(result.error || '保存失败'); } this.applyPersonalizationData(result.data || {}); this.applyPersonalizationMeta(result); this.status = '已保存'; setTimeout(() => { if (this.status === '已保存') { this.status = ''; } }, 3000); } catch (error: any) { this.error = error?.message || '保存失败'; } finally { this.saving = false; } }, updateField(payload: { key: keyof PersonalForm; value: any }) { if (!payload || !payload.key) { return; } this.form = { ...this.form, [payload.key]: payload.value }; if (payload.key === 'stacked_hide_borders') { persistStackedHideBorders(!!payload.value); } this.clearFeedback(); this.scheduleAutoSave(); }, /** 防抖自动保存:500ms 内多次调用只执行最后一次 */ scheduleAutoSave() { if (autoSaveTimer) { clearTimeout(autoSaveTimer); } autoSaveTimer = setTimeout(() => { autoSaveTimer = null; this.autoSave(); }, 500); }, /** 静默自动保存,成功不显示提示,失败显示错误 */ async autoSave() { if (this.saving) { return; } this.saving = true; try { const resp = await fetch('/api/personalization', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(this.form) }); const result = await resp.json(); if (!resp.ok || !result.success) { throw new Error(result.error || '自动保存失败'); } // 静默成功,不刷新表单(避免打断用户编辑) } catch (error: any) { this.error = error?.message || '自动保存失败'; } finally { this.saving = false; } }, setThinkingInterval(value: number | null) { let target: number | null = value; if (typeof target === 'number') { if (Number.isNaN(target)) { target = null; } else { const rounded = Math.round(target); const min = this.thinkingIntervalRange.min ?? DEFAULT_INTERVAL_RANGE.min; const max = this.thinkingIntervalRange.max ?? DEFAULT_INTERVAL_RANGE.max; target = Math.max(min, Math.min(max, rounded)); if (target === this.thinkingIntervalDefault) { target = null; } } } this.form = { ...this.form, thinking_interval: target }; this.clearFeedback(); this.scheduleAutoSave(); }, setRecentConversationsPromptLimit(value: number | null) { const target = value === null || typeof value === 'undefined' ? DEFAULT_RECENT_CONVERSATIONS_PROMPT_LIMIT : this.normalizeRecentConversationsPromptLimit(value); this.form = { ...this.form, recent_conversations_prompt_limit: target }; this.clearFeedback(); this.scheduleAutoSave(); }, toggleDefaultToolCategory(categoryId: string) { if (!categoryId) { return; } const current = new Set(this.form.disabled_tool_categories || []); if (current.has(categoryId)) { current.delete(categoryId); } else { current.add(categoryId); } this.form = { ...this.form, disabled_tool_categories: Array.from(current) }; this.clearFeedback(); this.scheduleAutoSave(); }, toggleSkill(skillId: string) { if (!skillId) { return; } const current = new Set(this.form.enabled_skills || []); if (current.has(skillId)) { current.delete(skillId); } else { current.add(skillId); } this.form = { ...this.form, enabled_skills: Array.from(current) }; this.clearFeedback(); this.scheduleAutoSave(); }, setDefaultRunMode(mode: RunMode | null) { let target: RunMode | null = null; if (typeof mode === 'string' && RUN_MODE_OPTIONS.includes(mode as RunMode)) { target = mode as RunMode; } this.form = { ...this.form, default_run_mode: target }; this.clearFeedback(); this.scheduleAutoSave(); }, setDefaultPermissionMode(mode: PermissionMode) { const allowed: PermissionMode[] = ['readonly', 'approval', 'auto_approval', 'unrestricted']; const target = allowed.includes(mode) ? mode : 'unrestricted'; this.form = { ...this.form, default_permission_mode: target }; this.clearFeedback(); this.scheduleAutoSave(); }, setVersioningRestoreMode(_mode: 'overwrite') { const target: 'overwrite' = 'overwrite'; this.form = { ...this.form, versioning_restore_mode: target }; this.clearFeedback(); this.scheduleAutoSave(); }, setVersioningBackupMode(mode: VersioningBackupMode) { const target: VersioningBackupMode = mode === 'full' ? 'full' : 'shallow'; this.form = { ...this.form, versioning_backup_mode: target }; this.clearFeedback(); this.scheduleAutoSave(); }, setDefaultModel(model: string | null) { const modelStore = useModelStore(); const allowed = new Set((modelStore.models || []).map((m) => m.key)); const target = typeof model === 'string' && allowed.has(model) ? model : null; this.form = { ...this.form, default_model: target }; this.clearFeedback(); this.scheduleAutoSave(); }, setImageCompression(mode: string) { const allowed = ['original', '1080p', '720p', '540p']; if (!allowed.includes(mode)) return; this.form = { ...this.form, image_compression: mode }; this.clearFeedback(); this.scheduleAutoSave(); }, applyTonePreset(preset: string) { if (!preset) { return; } this.form = { ...this.form, tone: preset }; this.clearFeedback(); this.scheduleAutoSave(); }, updateNewConsideration(value: string) { this.newConsideration = value; this.clearFeedback(); }, addConsideration() { if (!this.newConsideration) { return; } if (this.form.considerations.length >= this.maxConsiderations) { return; } this.form = { ...this.form, considerations: [...this.form.considerations, this.newConsideration] }; this.newConsideration = ''; this.clearFeedback(); this.scheduleAutoSave(); }, removeConsideration(index: number) { const items = [...this.form.considerations]; items.splice(index, 1); this.form = { ...this.form, considerations: items }; this.clearFeedback(); this.scheduleAutoSave(); }, considerationDragStart(index: number, event: DragEvent) { this.draggedConsiderationIndex = index; if (event && event.dataTransfer) { event.dataTransfer.effectAllowed = 'move'; } }, considerationDragOver(index: number, event: DragEvent) { if (event) { event.preventDefault(); } if (this.draggedConsiderationIndex === null || this.draggedConsiderationIndex === index) { return; } const items = [...this.form.considerations]; const [moved] = items.splice(this.draggedConsiderationIndex, 1); items.splice(index, 0, moved); this.form = { ...this.form, considerations: items }; this.draggedConsiderationIndex = index; this.clearFeedback(); }, considerationDrop(index: number, event: DragEvent) { if (event) { event.preventDefault(); } this.considerationDragEnd(); this.considerationDragOver(index, event); this.scheduleAutoSave(); }, considerationDragEnd() { this.draggedConsiderationIndex = null; }, async logout() { try { console.info('[auth-debug] logout clicked, sending POST /logout'); const resp = await fetch('/logout', { method: 'POST', credentials: 'same-origin', cache: 'no-store' }); console.info('[auth-debug] logout POST status:', resp.status); let payload: any = null; try { payload = await resp.json(); } catch (_err) { payload = null; } console.info('[auth-debug] logout POST payload:', payload); if (resp.ok && (!payload || payload.success !== false)) { window.location.replace(`/login?logged_out=1&ts=${Date.now()}`); return; } // 接口失败时,走 GET /logout 做兜底清理 window.location.replace(`/logout?ts=${Date.now()}`); } catch (error: any) { console.error('退出登录失败:', error); this.error = error?.message || '退出登录失败,请稍后重试'; // 兜底:直接访问 GET /logout window.location.replace(`/logout?ts=${Date.now()}`); } }, persistExperiments() { if (typeof window === 'undefined' || !window.localStorage) { return; } try { window.localStorage.setItem(EXPERIMENT_STORAGE_KEY, JSON.stringify(this.experiments)); } catch (error) { console.warn('写入实验功能设置失败:', error); } }, setBlockDisplayMode(mode: BlockDisplayMode) { this.experiments = { ...this.experiments, blockDisplayMode: mode }; this.persistExperiments(); this.scheduleAutoSave(); }, setCompactMessageDisplay(mode: CompactMessageDisplay) { const target: CompactMessageDisplay = mode === 'brief' ? 'brief' : 'full'; this.form = { ...this.form, compact_message_display: target }; // 同步旧版 localStorage 镜像,保持兼容并避免迁移逻辑回写旧值 this.experiments = { ...this.experiments, compactMessageDisplay: target }; this.persistExperiments(); this.clearFeedback(); // 持久化到后端配置文件(即时保存,参照主题/默认隐藏工作区) void this.persistCompactMessageDisplay(target); }, async persistCompactMessageDisplay(mode: CompactMessageDisplay) { try { const resp = await fetch('/api/personalization', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ compact_message_display: mode }) }); const result = await resp.json(); if (!resp.ok || !result.success) { console.warn('保存简略消息显示失败:', result?.error); } } catch (error) { console.warn('保存简略消息显示失败:', error); } }, setCommunicationStyle(style: CommunicationStyle) { this.form = { ...this.form, communication_style: style }; this.clearFeedback(); this.scheduleAutoSave(); }, setConversationContinuity(value: ConversationContinuity) { this.form = { ...this.form, conversation_continuity: value }; this.clearFeedback(); this.scheduleAutoSave(); } } });