From 02ab023ad735f0940152f742cb36fe92eef372a8 Mon Sep 17 00:00:00 2001 From: JOJO <1498581755@qq.com> Date: Sun, 30 Nov 2025 13:11:04 +0800 Subject: [PATCH] feat: persist behavior preferences --- core/main_terminal.py | 36 ++++++++++- modules/personalization_manager.py | 50 ++++++++++++++++ static/src/stores/personalization.ts | 90 +++++++++++++++++++++++++++- 3 files changed, 172 insertions(+), 4 deletions(-) diff --git a/core/main_terminal.py b/core/main_terminal.py index 3d0fe36..1898618 100644 --- a/core/main_terminal.py +++ b/core/main_terminal.py @@ -4,7 +4,7 @@ import asyncio import json from datetime import datetime from pathlib import Path -from typing import Dict, List, Optional, Set, TYPE_CHECKING +from typing import Any, Dict, List, Optional, Set, TYPE_CHECKING from datetime import datetime try: @@ -54,6 +54,10 @@ from modules.personalization_manager import ( load_personalization_config, build_personalization_prompt, ) +try: + from config.limits import THINKING_FAST_INTERVAL +except ImportError: + THINKING_FAST_INTERVAL = 10 from modules.container_monitor import collect_stats, inspect_state from core.tool_config import TOOL_CATEGORIES from utils.api_client import DeepSeekClient @@ -133,6 +137,9 @@ class MainTerminal: self.disabled_tools = set() self.disabled_notice_tools = set() self._refresh_disabled_tools() + self.thinking_fast_interval = THINKING_FAST_INTERVAL + self.default_disabled_tool_categories: List[str] = [] + self.apply_personalization_preferences() # 新增:自动开始新对话 self._ensure_conversation() @@ -319,6 +326,33 @@ class MainTerminal: self.context_manager.add_conversation("system", message, metadata=metadata) print(f"{OUTPUT_FORMATS['info']} {message}") + def apply_personalization_preferences(self, config: Optional[Dict[str, Any]] = None): + """Apply persisted personalization settings that affect runtime behavior.""" + try: + effective_config = config or load_personalization_config(self.data_dir) + except Exception: + effective_config = {} + + interval = effective_config.get("thinking_interval") + if isinstance(interval, int) and interval > 0: + self.thinking_fast_interval = interval + else: + self.thinking_fast_interval = THINKING_FAST_INTERVAL + + disabled_categories = [] + raw_disabled = effective_config.get("disabled_tool_categories") + if isinstance(raw_disabled, list): + disabled_categories = [ + key for key in raw_disabled + if isinstance(key, str) and key in TOOL_CATEGORIES + ] + self.default_disabled_tool_categories = disabled_categories + + # Reset category states to defaults before applying overrides + for key, category in TOOL_CATEGORIES.items(): + self.tool_category_states[key] = False if key in disabled_categories else category.default_enabled + self._refresh_disabled_tools() + def _handle_read_tool(self, arguments: Dict) -> Dict: """集中处理 read_file 工具的三种模式。""" diff --git a/modules/personalization_manager.py b/modules/personalization_manager.py index f16a23a..3555e3a 100644 --- a/modules/personalization_manager.py +++ b/modules/personalization_manager.py @@ -7,11 +7,20 @@ from copy import deepcopy from pathlib import Path from typing import Any, Dict, Iterable, Optional, Union +try: + from config.limits import THINKING_FAST_INTERVAL +except ImportError: + THINKING_FAST_INTERVAL = 10 + +from core.tool_config import TOOL_CATEGORIES + PERSONALIZATION_FILENAME = "personalization.json" MAX_SHORT_FIELD_LENGTH = 20 MAX_CONSIDERATION_LENGTH = 50 MAX_CONSIDERATION_ITEMS = 10 TONE_PRESETS = ["健谈", "幽默", "直言不讳", "鼓励性", "诗意", "企业商务", "打破常规", "同理心"] +THINKING_INTERVAL_MIN = 1 +THINKING_INTERVAL_MAX = 50 DEFAULT_PERSONALIZATION_CONFIG: Dict[str, Any] = { "enabled": False, @@ -20,6 +29,8 @@ DEFAULT_PERSONALIZATION_CONFIG: Dict[str, Any] = { "profession": "", "tone": "", "considerations": [], + "thinking_interval": None, + "disabled_tool_categories": [], } __all__ = [ @@ -86,6 +97,7 @@ def sanitize_personalization_payload( if fallback: base.update(fallback) data = payload or {} + allowed_tool_categories = set(TOOL_CATEGORIES.keys()) def _resolve_short_field(key: str) -> str: if key in data: @@ -101,6 +113,16 @@ def sanitize_personalization_payload( base["considerations"] = _sanitize_considerations(data.get("considerations")) else: base["considerations"] = _sanitize_considerations(base.get("considerations", [])) + + if "thinking_interval" in data: + base["thinking_interval"] = _sanitize_thinking_interval(data.get("thinking_interval")) + else: + base["thinking_interval"] = _sanitize_thinking_interval(base.get("thinking_interval")) + + if "disabled_tool_categories" in data: + base["disabled_tool_categories"] = _sanitize_tool_categories(data.get("disabled_tool_categories"), allowed_tool_categories) + else: + base["disabled_tool_categories"] = _sanitize_tool_categories(base.get("disabled_tool_categories"), allowed_tool_categories) return base @@ -172,3 +194,31 @@ def _sanitize_considerations(value: Any) -> list: if len(cleaned) >= MAX_CONSIDERATION_ITEMS: break return cleaned + + +def _sanitize_thinking_interval(value: Any) -> Optional[int]: + if value is None or value == "": + return None + try: + interval = int(value) + except (TypeError, ValueError): + return None + interval = max(THINKING_INTERVAL_MIN, min(THINKING_INTERVAL_MAX, interval)) + if interval == THINKING_FAST_INTERVAL: + return None + return interval + + +def _sanitize_tool_categories(value: Any, allowed: set) -> list: + if not isinstance(value, list): + return [] + result = [] + for item in value: + if not isinstance(item, str): + continue + candidate = item.strip() + if not candidate or candidate not in allowed: + continue + if candidate not in result: + result.append(candidate) + return result diff --git a/static/src/stores/personalization.ts b/static/src/stores/personalization.ts index 2972a7a..893629a 100644 --- a/static/src/stores/personalization.ts +++ b/static/src/stores/personalization.ts @@ -7,6 +7,8 @@ interface PersonalForm { profession: string; tone: string; considerations: string[]; + thinking_interval: number | null; + disabled_tool_categories: string[]; } interface PersonalizationState { @@ -23,15 +25,23 @@ interface PersonalizationState { tonePresets: string[]; draggedConsiderationIndex: number | null; form: PersonalForm; + toolCategories: Array<{ id: string; label: string }>; + thinkingIntervalDefault: number; + thinkingIntervalRange: { min: number; max: number }; } +const DEFAULT_INTERVAL = 10; +const DEFAULT_INTERVAL_RANGE = { min: 1, max: 50 }; + const defaultForm = (): PersonalForm => ({ enabled: false, self_identify: '', user_name: '', profession: '', tone: '', - considerations: [] + considerations: [], + thinking_interval: null, + disabled_tool_categories: [] }); export const usePersonalizationStore = defineStore('personalization', { @@ -48,7 +58,10 @@ export const usePersonalizationStore = defineStore('personalization', { newConsideration: '', tonePresets: ['健谈', '幽默', '直言不讳', '鼓励性', '诗意', '企业商务', '打破常规', '同理心'], draggedConsiderationIndex: null, - form: defaultForm() + form: defaultForm(), + toolCategories: [], + thinkingIntervalDefault: DEFAULT_INTERVAL, + thinkingIntervalRange: { ...DEFAULT_INTERVAL_RANGE } }), actions: { async openDrawer() { @@ -91,6 +104,7 @@ export const usePersonalizationStore = defineStore('personalization', { throw new Error(result.error || '加载失败'); } this.applyPersonalizationData(result.data || {}); + this.applyPersonalizationMeta(result); this.loaded = true; } catch (error: any) { this.error = error?.message || '加载失败'; @@ -105,10 +119,38 @@ export const usePersonalizationStore = defineStore('personalization', { user_name: data.user_name || '', profession: data.profession || '', tone: data.tone || '', - considerations: Array.isArray(data.considerations) ? [...data.considerations] : [] + 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') : [] }; this.clearFeedback(); }, + 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 && 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 = []; + } + }, clearFeedback() { this.status = ''; this.error = ''; @@ -133,6 +175,10 @@ export const usePersonalizationStore = defineStore('personalization', { 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(() => { @@ -165,6 +211,7 @@ export const usePersonalizationStore = defineStore('personalization', { throw new Error(result.error || '保存失败'); } this.applyPersonalizationData(result.data || {}); + this.applyPersonalizationMeta(result); this.status = '已保存'; setTimeout(() => { if (this.status === '已保存') { @@ -187,6 +234,43 @@ export const usePersonalizationStore = defineStore('personalization', { }; this.clearFeedback(); }, + 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(); + }, + 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(); + }, applyTonePreset(preset: string) { if (!preset) { return;