agent-Specialization/static/src/stores/personalization.ts
JOJO c76431a8f1 feat(tools): 网页提取白名单直提 + recall 记忆纳入已读文件
- extract_webpage/save_webpage 命中白名单域名时本机直提:GitHub 代码页走
  jsDelivr CDN 直链(备 GitHub API),其余白名单页用 trafilatura 正文识别,
  失败自动回退 Tavily;内置白名单 github.com,个人空间可关闭/追加域名
- trafilatura 进 requirements(可选依赖,缺失时静默降级全走 Tavily)
- recall_project_memory 返回记忆全文后标记为已读,可直接 edit_file

Co-authored-by: Astrion powered by Kimi-K3 <astrion-agent@users.noreply.github.com>
2026-09-12 11:30:38 +08:00

1231 lines
46 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 { t, currentLocale, setLocale } from '@/locales';
import { useModelStore } from './model';
// 语气预设:模块顶层禁止调 t(),这里只存 key由 getter tonePresets 在读取时 t(key) 求值(随语言切换刷新)
const TONE_PRESET_KEYS = [
'stores.toneChatty',
'stores.toneHumorous',
'stores.toneBlunt',
'stores.toneEncouraging',
'stores.tonePoetic',
'stores.toneCorporate',
'stores.toneUnconventional',
'stores.toneEmpathetic'
] as const;
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';
export type WorkMode = 'plan' | 'ask' | 'execute';
type CommunicationStyle = 'default' | 'human_like' | 'auto';
type ConversationContinuity = 'low' | 'medium' | 'high';
type VersioningBackupMode = 'shallow' | 'full';
/** 与后端 DEFAULT_PERSONALIZATION_CONFIG['review_agents'] 保持一致 */
const defaultReviewAgents = (): Record<ReviewAgentKey, ReviewAgentSetting> => ({
auto_approval: {
model: '',
thinking: false,
timeout_seconds: 60,
max_rounds: 3,
max_command_timeout: 20
},
goal_review: {
model: '',
thinking: false,
timeout_seconds: 60,
max_rounds: 3,
max_command_timeout: 60
},
workflow_review: {
model: '',
thinking: false,
timeout_seconds: 120,
max_rounds: 6,
max_command_timeout: 60
}
});
/** 清洗后端返回的 review_agents缺键/类型错时回落默认值,数值钳到合理区间 */
const sanitizeReviewAgents = (raw: any): Record<ReviewAgentKey, ReviewAgentSetting> => {
const defaults = defaultReviewAgents();
const src = raw && typeof raw === 'object' ? raw : {};
const clamp = (v: any, fallback: number, lo: number, hi: number): number => {
const n = typeof v === 'number' ? v : parseInt(v, 10);
if (!Number.isFinite(n)) return fallback;
return Math.max(lo, Math.min(Math.trunc(n), hi));
};
const keys: ReviewAgentKey[] = ['auto_approval', 'goal_review', 'workflow_review'];
const out = {} as Record<ReviewAgentKey, ReviewAgentSetting>;
for (const key of keys) {
const d = defaults[key];
const item = src[key] && typeof src[key] === 'object' ? src[key] : {};
out[key] = {
model: typeof item.model === 'string' ? item.model : '',
thinking: typeof item.thinking === 'boolean' ? item.thinking : d.thinking,
timeout_seconds: clamp(item.timeout_seconds, d.timeout_seconds, 5, 3600),
max_rounds: clamp(item.max_rounds, d.max_rounds, 1, 50),
max_command_timeout: clamp(item.max_command_timeout, d.max_command_timeout, 1, 600)
};
}
return out;
};
/** 审核智能体键:自动审批 / 目标审核 / 工作流审核 */
export type ReviewAgentKey = 'auto_approval' | 'goal_review' | 'workflow_review';
export interface ReviewAgentSetting {
/** 模型名(子智能体模型库条目);留空 = 模型库 default_model */
model: string;
/** 思考模式开关(模型不支持思考时后端自动回落 fast 段) */
thinking: boolean;
/** 审核请求超时(秒) */
timeout_seconds: number;
/** 审核最大轮次 */
max_rounds: number;
/** 只读取证命令超时(秒) */
max_command_timeout: number;
}
interface PersonalForm {
enabled: boolean;
communication_style: CommunicationStyle;
conversation_continuity: ConversationContinuity;
auto_generate_title: boolean;
/** 标题生成模型(子智能体模型库条目名);留空 = 模型库 default_model */
title_model: string;
recent_conversations_prompt_enabled: boolean;
recent_conversations_prompt_limit: number | string;
project_memory_inject_limit: number | string | null;
tool_intent_enabled: boolean;
tool_loading_enabled: boolean;
/** 默认延迟加载的工具名列表(仅影响新建对话) */
tool_loading_deferred: string[];
/** 网页提取白名单直提开关(默认开启) */
webpage_direct_extract_enabled: boolean;
/** 用户追加的直提白名单域名(内置 github.com 不在此列) */
webpage_direct_extract_domains: string[];
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;
default_work_mode: WorkMode;
versioning_enabled_by_default: boolean;
versioning_backup_mode: VersioningBackupMode;
versioning_restore_mode: 'overwrite';
default_model: string | null;
external_session_header: boolean;
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;
claude_md_auto_inject: boolean;
agents_skills_scan_enabled: boolean;
new_chat_button_behavior: 'route' | 'blank';
group_sidebar_by_workspace: boolean;
sidebar_pinned_workspaces: string[];
sidebar_workspace_order: string[];
theme: 'classic' | 'light' | 'dark';
ui_locale: 'zh-CN' | 'en-US';
goal_review_mode: 'readonly' | 'active';
goal_end_conditions: string[];
goal_max_turns: number;
goal_max_tokens: number | null;
review_agents: Record<ReviewAgentKey, ReviewAgentSetting>;
}
interface ExperimentState {
blockDisplayMode: BlockDisplayMode;
compactMessageDisplay: CompactMessageDisplay;
}
interface PersonalizationState {
visible: boolean;
loading: boolean;
saving: boolean;
loaded: boolean;
status: string;
error: string;
toggleUpdating: boolean;
overlayPressActive: boolean;
form: PersonalForm;
toolCategories: Array<{ id: string; label: string }>;
toolLoadingRegistry: Array<{ key: string; tools: 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 'minimal';
}
try {
const raw = window.localStorage.getItem(EXPERIMENT_STORAGE_KEY);
if (!raw) {
return 'minimal';
}
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 'minimal';
};
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,
title_model: '',
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,
tool_loading_enabled: true,
tool_loading_deferred: [],
webpage_direct_extract_enabled: true,
webpage_direct_extract_domains: [],
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: 'approval',
default_work_mode: 'plan',
versioning_enabled_by_default: true,
versioning_backup_mode: 'shallow',
versioning_restore_mode: 'overwrite',
default_model: null,
external_session_header: false,
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,
claude_md_auto_inject: false,
agents_skills_scan_enabled: true,
new_chat_button_behavior: 'route',
group_sidebar_by_workspace: false,
sidebar_pinned_workspaces: [],
sidebar_workspace_order: [],
theme: loadCachedTheme(),
ui_locale: 'zh-CN' as 'zh-CN' | 'en-US',
goal_review_mode: 'readonly',
goal_end_conditions: ['max_turns'],
goal_max_turns: 5,
goal_max_tokens: null,
review_agents: defaultReviewAgents()
});
const defaultExperimentState = (): ExperimentState => ({
blockDisplayMode: 'minimal',
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,
form: defaultForm(),
toolCategories: [],
toolLoadingRegistry: [],
skillsCatalog: [],
recentConversationsPromptLimitRange: { ...DEFAULT_RECENT_CONVERSATIONS_PROMPT_LIMIT_RANGE },
projectMemoryInjectLimitMin: PROJECT_MEMORY_INJECT_LIMIT_MIN,
experiments: loadExperimentState()
}),
getters: {
// 语气预设:响应式依赖 currentLocale语言切换时自动刷新
tonePresets: () => {
void currentLocale.value;
return TONE_PRESET_KEYS.map((key) => t(key));
}
},
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 || t('common.loadFailed'));
}
this.applyPersonalizationData(result.data || {});
this.applyPersonalizationMeta(result);
this.loaded = true;
} catch (error: any) {
this.error = error?.message || t('common.loadFailed');
} 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,
title_model: typeof data.title_model === 'string' ? data.title_model : '',
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,
tool_loading_enabled: data.tool_loading_enabled !== false,
tool_loading_deferred: Array.isArray(data.tool_loading_deferred)
? data.tool_loading_deferred.filter((item: any) => typeof item === 'string')
: [],
webpage_direct_extract_enabled: data.webpage_direct_extract_enabled !== false,
webpage_direct_extract_domains: Array.isArray(data.webpage_direct_extract_domains)
? data.webpage_direct_extract_domains.filter(
(item: any) => typeof item === 'string' && item.trim()
)
: [],
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
: 'minimal',
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
: 'approval',
default_work_mode:
data.default_work_mode === 'plan' ||
data.default_work_mode === 'ask' ||
data.default_work_mode === 'execute'
? data.default_work_mode
: 'plan',
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,
external_session_header: !!data.external_session_header,
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,
claude_md_auto_inject: !!data.claude_md_auto_inject,
agents_skills_scan_enabled: data.agents_skills_scan_enabled === undefined ? true : !!data.agents_skills_scan_enabled,
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,
ui_locale: ['zh-CN', 'en-US'].includes(data.ui_locale)
? data.ui_locale
: (this.form?.ui_locale || 'zh-CN'),
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,
review_agents: sanitizeReviewAgents(data.review_agents)
};
// 如果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);
}
// 界面语言以后端个人偏好为准(多设备/多工作区共享),覆盖本地缓存
if (this.form.ui_locale && this.form.ui_locale !== currentLocale.value) {
setLocale(this.form.ui_locale);
}
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 保存,后端默认 minimal 时回写本地缓存值。
const cachedBlockMode = this.experiments.blockDisplayMode;
if (this.form.block_display_mode === 'minimal' && cachedBlockMode !== 'minimal') {
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.tool_loading_registry)) {
this.toolLoadingRegistry = payload.tool_loading_registry
.map((item: { key?: string; tools?: any } = {}) => ({
key: typeof item.key === 'string' ? item.key : String(item.key ?? ''),
tools: Array.isArray(item.tools)
? item.tools.filter((n: any) => typeof n === 'string')
: []
}))
.filter((item: { key: string }) => !!item.key);
} else {
this.toolLoadingRegistry = [];
}
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 || t('stores.updateFailed'));
}
if (result.data) {
this.applyPersonalizationData(result.data);
}
this.applyPersonalizationMeta(result);
const statusLabel = newValue ? t('stores.enabled') : t('stores.disabled');
this.status = statusLabel;
setTimeout(() => {
if (this.status === statusLabel) {
this.status = '';
}
}, 2000);
} catch (error: any) {
this.form.enabled = previousValue;
this.error = error?.message || t('stores.updateFailed');
} 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(t('stores.deepCompressMustExceedShallow'));
}
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 || t('stores.saveFailed'));
}
this.applyPersonalizationData(result.data || {});
this.applyPersonalizationMeta(result);
const savedStatus = t('stores.saved');
this.status = savedStatus;
setTimeout(() => {
if (this.status === savedStatus) {
this.status = '';
}
}, 3000);
} catch (error: any) {
this.error = error?.message || t('stores.saveFailed');
} 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 || t('stores.autoSaveFailed'));
}
// 静默成功,不刷新表单(避免打断用户编辑)
} catch (error: any) {
this.error = error?.message || t('stores.autoSaveFailed');
} 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();
},
setDefaultWorkMode(mode: WorkMode) {
const allowed: WorkMode[] = ['plan', 'ask', 'execute'];
const target = allowed.includes(mode) ? mode : 'plan';
this.form = {
...this.form,
default_work_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 || t('stores.logoutFailedRetry');
// 兜底:直接访问 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();
}
}
});