agent-Specialization/static/src/stores/personalization.ts
JOJO 56c067b710 feat(memory): 新增项目记忆最大注入条数设置,记忆索引按最近修改排序
- 个人空间-上下文新增「最大记忆注入」(默认 20,最小 5,留空无上限)
- 提示词中项目记忆索引从文件名字典序改为按 mtime 倒序注入
- 超上限时截断并注入说明:共 N 条、仅注入前 x 条、其余用 search_project_memory 检索
- 修复 memory_system 模板条件块只删标记不删占位文字导致的「暂无xx记忆」残留
2026-08-12 23:07:14 +08:00

1068 lines
40 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';
export type BlockDisplayMode = 'traditional' | 'stacked' | 'minimal';
export type CompactMessageDisplay = 'full' | 'brief';
type RunMode = 'fast' | 'thinking';
export type ReasoningEffort = 'low' | 'medium' | 'high' | 'xhigh' | 'max';
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;
project_memory_inject_limit: number | string | null;
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;
hide_tool_approval_panel: boolean;
enhanced_tool_display: boolean;
compact_message_display: CompactMessageDisplay;
block_display_mode: BlockDisplayMode;
show_status_avatar: boolean;
show_git_status_bar: boolean;
auto_open_terminal_panel: boolean;
quick_dock_auto_expand: boolean;
file_preview_auto_wrap: boolean;
edit_summary_live_display: boolean;
modify_history_enabled: boolean;
stacked_hide_borders: boolean;
minimal_expand_height_limited: 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;
disabled_tool_categories: string[];
default_run_mode: RunMode | null;
default_reasoning_effort: ReasoningEffort | 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;
new_chat_button_behavior: 'route' | 'blank';
group_sidebar_by_workspace: boolean;
sidebar_pinned_workspaces: string[];
sidebar_workspace_order: string[];
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;
toggleUpdating: boolean;
overlayPressActive: boolean;
tonePresets: string[];
form: PersonalForm;
toolCategories: Array<{ id: string; label: string }>;
skillsCatalog: Array<{ id: string; label: string; description?: string }>;
recentConversationsPromptLimitRange: { min: number; max: number };
projectMemoryInjectLimitMin: number;
experiments: ExperimentState;
}
const DEFAULT_RECENT_CONVERSATIONS_PROMPT_LIMIT = 10;
const DEFAULT_RECENT_CONVERSATIONS_PROMPT_LIMIT_RANGE = { min: 1, max: 30 };
const DEFAULT_PROJECT_MEMORY_INJECT_LIMIT = 20;
const PROJECT_MEMORY_INJECT_LIMIT_MIN = 5;
const DEFAULT_SHALLOW_COMPRESS_TRIGGER_TOKENS = 80000;
const DEFAULT_DEEP_COMPRESS_TRIGGER_TOKENS = 150000;
const RUN_MODE_OPTIONS: RunMode[] = ['fast', 'thinking'];
const REASONING_EFFORT_OPTIONS: ReasoningEffort[] = ['low', 'medium', 'high', 'xhigh', 'max'];
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 MINIMAL_EXPAND_HEIGHT_LIMITED_STORAGE_KEY = 'agents_minimal_expand_height_limited';
const QUICK_DOCK_AUTO_EXPAND_STORAGE_KEY = 'agents_quick_dock_auto_expand';
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 loadCachedBlockDisplayMode = (): BlockDisplayMode => {
if (typeof window === 'undefined' || !window.localStorage) {
return 'stacked';
}
try {
const raw = window.localStorage.getItem(EXPERIMENT_STORAGE_KEY);
if (!raw) {
return 'stacked';
}
const parsed = JSON.parse(raw);
if (
typeof parsed?.blockDisplayMode === 'string' &&
['traditional', 'stacked', 'minimal'].includes(parsed.blockDisplayMode)
) {
return parsed.blockDisplayMode as BlockDisplayMode;
}
} catch (error) {
console.warn('读取堆叠块显示模式缓存失败:', error);
}
return 'stacked';
};
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);
}
};
/**
* 快捷窗口自动展开设置的本地缓存(与主题缓存同理):
* 进入页面时个性化接口是异步的,首帧渲染必须同步拿到该设置,
* 否则 QuickDock 只能先按默认值渲染再等接口纠偏,产生「先展开再收起」/「先空再展开」闪烁。
*/
export const loadCachedQuickDockAutoExpand = (): boolean => {
if (typeof window === 'undefined' || !window.localStorage) {
return true;
}
const saved = window.localStorage.getItem(QUICK_DOCK_AUTO_EXPAND_STORAGE_KEY);
return saved === null ? true : saved === 'true';
};
/** 是否已有自动展开设置的本地缓存(无缓存的首次访问仍需等接口就绪) */
export const hasCachedQuickDockAutoExpand = (): boolean => {
if (typeof window === 'undefined' || !window.localStorage) {
return false;
}
return window.localStorage.getItem(QUICK_DOCK_AUTO_EXPAND_STORAGE_KEY) !== null;
};
const persistQuickDockAutoExpand = (value: boolean) => {
if (typeof window === 'undefined' || !window.localStorage) {
return;
}
try {
window.localStorage.setItem(QUICK_DOCK_AUTO_EXPAND_STORAGE_KEY, value ? 'true' : 'false');
} catch (error) {
console.warn('写入快捷窗口自动展开设置缓存失败:', error);
}
};
const loadCachedMinimalExpandHeightLimited = (): boolean => {
if (typeof window === 'undefined' || !window.localStorage) {
return true;
}
const saved = window.localStorage.getItem(MINIMAL_EXPAND_HEIGHT_LIMITED_STORAGE_KEY);
return saved === null ? true : saved === 'true';
};
const persistMinimalExpandHeightLimited = (value: boolean) => {
if (typeof window === 'undefined' || !window.localStorage) {
return;
}
try {
window.localStorage.setItem(MINIMAL_EXPAND_HEIGHT_LIMITED_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,
project_memory_inject_limit: DEFAULT_PROJECT_MEMORY_INJECT_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,
hide_tool_approval_panel: true,
enhanced_tool_display: true,
compact_message_display: 'full',
block_display_mode: loadCachedBlockDisplayMode(),
show_status_avatar: true,
show_git_status_bar: true,
auto_open_terminal_panel: true,
quick_dock_auto_expand: loadCachedQuickDockAutoExpand(),
file_preview_auto_wrap: false,
edit_summary_live_display: false,
modify_history_enabled: true,
stacked_hide_borders: loadCachedStackedHideBorders(),
minimal_expand_height_limited: loadCachedMinimalExpandHeightLimited(),
enhanced_tool_display_categories: [],
enabled_skills: [],
self_identify: '',
user_name: '',
use_custom_names: false,
profession: '',
tone: '',
considerations: '',
disabled_tool_categories: [],
default_run_mode: null,
default_reasoning_effort: 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: true,
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,
new_chat_button_behavior: 'route',
group_sidebar_by_workspace: false,
sidebar_pinned_workspaces: [],
sidebar_workspace_order: [],
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: '',
toggleUpdating: false,
overlayPressActive: false,
tonePresets: ['健谈', '幽默', '直言不讳', '鼓励性', '诗意', '企业商务', '打破常规', '同理心'],
form: defaultForm(),
toolCategories: [],
skillsCatalog: [],
recentConversationsPromptLimitRange: { ...DEFAULT_RECENT_CONVERSATIONS_PROMPT_LIMIT_RANGE },
projectMemoryInjectLimitMin: PROJECT_MEMORY_INJECT_LIMIT_MIN,
experiments: loadExperimentState()
}),
actions: {
async openDrawer() {
this.visible = true;
// 每次打开都刷新数据,确保显示最新内容
if (!this.loading) {
await this.fetchPersonalization();
}
},
closeDrawer() {
this.visible = false;
this.overlayPressActive = false;
},
handleOverlayPressStart(event: Event) {
// 只有直接按在遮罩上才算有效按压;按在卡片内部(如输入框)时重置状态,
// 避免「按下遮罩→拖入卡片松开」遗留的卡死状态导致后续遮罩 mouseup 误关抽屉
if (event.target !== event.currentTarget) {
this.overlayPressActive = false;
return;
}
if (event.type === 'mousedown' && (event as MouseEvent).button !== 0) {
return;
}
this.overlayPressActive = true;
},
handleOverlayPressEnd(event: Event) {
// 按下与松开都发生在遮罩上才关闭;输入框内拖选文字、松开落在遮罩上时
// target 虽为遮罩,但按压并未起始于遮罩,不会关闭
const shouldClose =
this.overlayPressActive && event.target === event.currentTarget;
this.overlayPressActive = false;
if (!shouldClose) {
return;
}
if (event.cancelable) {
event.preventDefault();
}
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
),
project_memory_inject_limit: this.normalizeProjectMemoryInjectLimit(
data.project_memory_inject_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,
hide_tool_approval_panel: data.hide_tool_approval_panel !== false,
enhanced_tool_display: data.enhanced_tool_display !== false,
compact_message_display: data.compact_message_display === 'brief' ? 'brief' : 'full',
block_display_mode:
data.block_display_mode === 'traditional' ||
data.block_display_mode === 'stacked' ||
data.block_display_mode === 'minimal'
? data.block_display_mode
: 'stacked',
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,
quick_dock_auto_expand: data.quick_dock_auto_expand !== false,
file_preview_auto_wrap: !!data.file_preview_auto_wrap,
edit_summary_live_display: !!data.edit_summary_live_display,
modify_history_enabled: data.modify_history_enabled !== false,
stacked_hide_borders: !!data.stacked_hide_borders,
minimal_expand_height_limited: data.minimal_expand_height_limited !== false,
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:
typeof data.considerations === 'string'
? data.considerations
: Array.isArray(data.considerations)
? data.considerations.filter((item: unknown) => typeof item === 'string').join('\n')
: '',
disabled_tool_categories: Array.isArray(data.disabled_tool_categories)
? data.disabled_tool_categories.filter((item: unknown) => typeof item === 'string')
: [],
default_run_mode: (() => {
// 历史值 deep 映射为 thinking
const saved = data.default_run_mode === 'deep' ? 'thinking' : data.default_run_mode;
return typeof saved === 'string' && RUN_MODE_OPTIONS.includes(saved as RunMode)
? (saved as RunMode)
: null;
})(),
default_reasoning_effort:
typeof data.default_reasoning_effort === 'string' &&
REASONING_EFFORT_OPTIONS.includes(data.default_reasoning_effort as ReasoningEffort)
? (data.default_reasoning_effort as ReasoningEffort)
: 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,
new_chat_button_behavior: data.new_chat_button_behavior === 'blank' ? 'blank' : 'route',
group_sidebar_by_workspace: !!data.group_sidebar_by_workspace,
sidebar_pinned_workspaces: Array.isArray(data.sidebar_pinned_workspaces)
? data.sidebar_pinned_workspaces.filter((item: unknown) => typeof item === 'string')
: [],
sidebar_workspace_order: Array.isArray(data.sidebar_workspace_order)
? data.sidebar_workspace_order.filter((item: unknown) => typeof item === 'string')
: [],
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);
}
persistStackedHideBorders(this.form.stacked_hide_borders);
persistMinimalExpandHeightLimited(this.form.minimal_expand_height_limited);
persistQuickDockAutoExpand(this.form.quick_dock_auto_expand);
// 简略消息显示:以配置文件为准,同步到旧版 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');
}
// 一次性迁移:堆叠块显示模式旧版仅在 localStorage 保存,后端默认 stacked 时回写本地缓存值。
const cachedBlockMode = this.experiments.blockDisplayMode;
if (this.form.block_display_mode === 'stacked' && cachedBlockMode !== 'stacked') {
this.form = { ...this.form, block_display_mode: cachedBlockMode };
}
this.experiments = {
...this.experiments,
compactMessageDisplay: this.form.compact_message_display,
blockDisplayMode: this.form.block_display_mode
};
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)));
},
normalizeProjectMemoryInjectLimit(value: any): number | null {
// undefined老接口未下发/非法值 → 默认 20null/''/0/负数 → null无上限正整数钳到 >= min
if (typeof value === 'undefined') {
return DEFAULT_PROJECT_MEMORY_INJECT_LIMIT;
}
if (value === null || value === '') {
return null;
}
const parsed = Number(value);
if (!Number.isFinite(parsed)) {
return DEFAULT_PROJECT_MEMORY_INJECT_LIMIT;
}
const rounded = Math.round(parsed);
if (rounded <= 0) {
return null;
}
const min = this.projectMemoryInjectLimitMin ?? PROJECT_MEMORY_INJECT_LIMIT_MIN;
return Math.max(min, rounded);
},
applyPersonalizationMeta(payload: any) {
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 && payload.project_memory_inject_limit_range) {
const { min } = payload.project_memory_inject_limit_range;
this.projectMemoryInjectLimitMin =
typeof min === 'number' ? min : PROJECT_MEMORY_INJECT_LIMIT_MIN;
} else {
this.projectMemoryInjectLimitMin = PROJECT_MEMORY_INJECT_LIMIT_MIN;
}
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);
}
if (payload.key === 'minimal_expand_height_limited') {
persistMinimalExpandHeightLimited(!!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;
}
},
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();
},
setProjectMemoryInjectLimit(value: number | null) {
// null = 无上限
const target =
value === null || typeof value === 'undefined'
? null
: this.normalizeProjectMemoryInjectLimit(value);
this.form = {
...this.form,
project_memory_inject_limit: target
};
this.clearFeedback();
this.scheduleAutoSave();
},
restoreProjectMemoryInjectLimit() {
this.form = {
...this.form,
project_memory_inject_limit: DEFAULT_PROJECT_MEMORY_INJECT_LIMIT
};
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();
},
setDefaultReasoningEffort(effort: ReasoningEffort | null) {
let target: ReasoningEffort | null = null;
if (typeof effort === 'string' && REASONING_EFFORT_OPTIONS.includes(effort as ReasoningEffort)) {
target = effort as ReasoningEffort;
}
this.form = {
...this.form,
default_reasoning_effort: 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();
},
updateConsiderations(value: string) {
this.form = {
...this.form,
considerations: value
};
this.clearFeedback();
this.scheduleAutoSave();
},
async logout() {
try {
const resp = await fetch('/logout', {
method: 'POST',
credentials: 'same-origin',
cache: 'no-store'
});
let payload: any = null;
try {
payload = await resp.json();
} catch (_err) {
payload = null;
}
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.form = {
...this.form,
block_display_mode: mode
};
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();
}
}
});