本次变更是对版本控制功能的大范围重构,核心目标:
- 默认使用浅备份(只追踪 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 模式新建对话、编辑文件、回溯、复制对话均验证通过。
965 lines
35 KiB
TypeScript
965 lines
35 KiB
TypeScript
import { defineStore } from 'pinia';
|
||
import { useModelStore } from './model';
|
||
import { persistDefaultHideWorkspace, useUiStore } from './ui';
|
||
|
||
export type BlockDisplayMode = 'traditional' | 'stacked' | 'minimal';
|
||
export type CompactMessageDisplay = 'full' | 'brief';
|
||
type RunMode = 'fast' | 'thinking' | 'deep';
|
||
type PermissionMode = 'readonly' | 'approval' | 'auto_approval' | 'unrestricted';
|
||
type CommunicationStyle = 'default' | 'human_like' | 'auto';
|
||
type ConversationContinuity = 'low' | 'medium' | 'high';
|
||
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();
|
||
}
|
||
}
|
||
});
|