agent-Specialization/static/src/stores/personalization.ts
JOJO 2e54f2c343 feat(terminal): 实时终端改造为侧边栏面板,支持拖拽分割与自动开关
- 将实时终端从独立页面改造为 Vue3 侧边栏面板,与 Git 面板上下排列
- 使用 xterm.js + Grid 布局,支持三主题配色适配
- 上下拖拽分割条,动态调整终端/Git 面板比例
- 输入栏上方新增终端数量按钮,点击快捷打开
- 个人空间新增「自动打开终端面板」开关(外观与显示分类)
- REST API /api/terminals 获取终端列表,多时机自动刷新
- 有终端自动打开面板,无终端自动关闭
2026-06-12 00:18:27 +08:00

925 lines
33 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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';
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_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_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';
deep_compress_behavior: 'continue' | 'wait';
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 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 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_git_status_bar: true,
auto_open_terminal_panel: true,
stacked_hide_borders: false,
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_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',
deep_compress_behavior: 'continue',
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<typeof setTimeout> | 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_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_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',
deep_compress_behavior: data.deep_compress_behavior === 'wait' ? 'wait' : 'continue',
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);
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
};
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();
},
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();
}
}
});