- 将实时终端从独立页面改造为 Vue3 侧边栏面板,与 Git 面板上下排列 - 使用 xterm.js + Grid 布局,支持三主题配色适配 - 上下拖拽分割条,动态调整终端/Git 面板比例 - 输入栏上方新增终端数量按钮,点击快捷打开 - 个人空间新增「自动打开终端面板」开关(外观与显示分类) - REST API /api/terminals 获取终端列表,多时机自动刷新 - 有终端自动打开面板,无终端自动关闭
925 lines
33 KiB
TypeScript
925 lines
33 KiB
TypeScript
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();
|
||
}
|
||
}
|
||
});
|