agent-Specialization/static/src/stores/personalization.ts
JOJO 478d24c4dd feat(versioning): 重构版本控制为对话级浅备份 + 可选完全备份
本次变更是对版本控制功能的大范围重构,核心目标:
- 默认使用浅备份(只追踪 write_file/edit_file 实际修改的文件),避免大工作区首次初始化耗时十几秒;
- 保留可选的完全备份(git write-tree),不污染 git log;
- 回溯时统一选择范围(仅对话 / 对话+工作区)和模式(覆盖 / 复制);
- 修复复制对话、侧边栏同步、新建对话延迟、diff 统计与渲染等连锁问题。

后端改动:
- modules/shallow_versioning.py(新增)
  - 实现 ShallowVersioningManager:track_edit、make_snapshot、rewind、list_snapshots。
  - 基于 difflib.SequenceMatcher 计算真实 insertions/deletions,replace 场景正确显示 +N/-M。
  - diff 详情使用结构化 add/remove/context 行,不再解析 unified_diff 文本。
- modules/versioning_manager.py
  - 完全备份从 git commit 改为 git write-tree,字段 commit/parent_commit 改为 tree_hash/parent_tree_hash。
  - 恢复改用 read-tree + checkout-index;create_checkpoint 支持 diff_summary 参数。
  - get_checkpoint_detail 保留 API 字段名 last_commit/restored_commit 以兼容前端。
- core/main_terminal_parts/tools_execution.py
  - write_file/edit_file 在执行前调用浅备份 track_edit,保留编辑前状态。
- server/chat_flow_task_main.py
  - 消息轮次结束时根据 backup_mode 选择完全备份 create_checkpoint 或浅备份 make_snapshot。
  - 浅备份模式下同时写入 ConversationVersioningManager 检查点行,让前端能统一展示。
- server/conversation.py
  - restore/detail/list API 支持 shallow 模式;list/detail 对浅备份实时重新计算统计。
  - 创建对话时读取 personalization 的 versioning_backup_mode,shallow 模式固定 tracking_mode 为 conversation_only。
- core/web_terminal.py
  - _ensure_conversation_versioning_enabled 读取默认备份模式并写入 versioning meta。
- modules/file_manager/crud_mixin.py / replace_mixin.py
  - write_file / edit_file 返回 original_file / new_file / replacement_details(真实旧行/新行)。
- modules/personalization_manager.py
  - 新增 versioning_enabled_by_default、versioning_backup_mode(shallow/full)。
- utils/perf_log.py(新增)
  - 创建新对话全链路性能日志,用于定位延迟瓶颈。
- utils/conversation_manager/{index,list_search,crud}_mixin.py
  - 加性能日志;index_mixin 主动检查新增对话文件,修复侧边栏不同步。

前端改动:
- static/src/components/overlay/VersioningDialog.vue
  - 去掉顶部管理范围下拉;底部新增回溯范围与回溯模式选择。
  - diff 详情改为 div.diff-line 网格布局(marker + content),支持横向滚动,状态显示缩写 A/M/D。
  - 修复 pre 标签空白黑行、容器被挤窄导致折行等问题。
- static/src/app/methods/versioning.ts
  - 移除 mismatch 弹窗逻辑;支持 copy 模式;selectVersioningCheckpoint 增强错误捕获。
- static/src/components/personalization/PersonalizationDrawer.vue
  - 「工作区与权限」新增版本控制默认开关与备份方式选择。
- static/src/stores/personalization.ts
  - 新增 versioning_enabled_by_default、versioning_backup_mode 字段。
- static/src/components/chat/actions/ToolAction.vue / toolRenderers.ts
  - edit_file / write_file 工具展示基于 result.details / result.new_file 的真实替换结果。
- static/src/app/state.ts / watchers.ts / App.vue
  - 新增 versioningInitializingBackupToastId,完全备份初始化时显示 toast。
- static/src/app/methods/conversation/action.ts / message/send.ts
  - host + 完全备份 + 默认开启时,创建新对话/从 /new 发送首条消息显示「正在初始化备份」toast。

验证:
- /Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/bin/python3.9 -m unittest test.test_server_refactor_smoke 通过。
- npm run build 通过。
- 宿主机与 docker 模式新建对话、编辑文件、回溯、复制对话均验证通过。
2026-07-07 12:07:44 +08:00

965 lines
35 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

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

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