feat: persist behavior preferences

This commit is contained in:
JOJO 2025-11-30 13:11:04 +08:00
parent 2847ade631
commit 02ab023ad7
3 changed files with 172 additions and 4 deletions

View File

@ -4,7 +4,7 @@ import asyncio
import json import json
from datetime import datetime from datetime import datetime
from pathlib import Path 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 from datetime import datetime
try: try:
@ -54,6 +54,10 @@ from modules.personalization_manager import (
load_personalization_config, load_personalization_config,
build_personalization_prompt, 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 modules.container_monitor import collect_stats, inspect_state
from core.tool_config import TOOL_CATEGORIES from core.tool_config import TOOL_CATEGORIES
from utils.api_client import DeepSeekClient from utils.api_client import DeepSeekClient
@ -133,6 +137,9 @@ class MainTerminal:
self.disabled_tools = set() self.disabled_tools = set()
self.disabled_notice_tools = set() self.disabled_notice_tools = set()
self._refresh_disabled_tools() 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() self._ensure_conversation()
@ -319,6 +326,33 @@ class MainTerminal:
self.context_manager.add_conversation("system", message, metadata=metadata) self.context_manager.add_conversation("system", message, metadata=metadata)
print(f"{OUTPUT_FORMATS['info']} {message}") 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: def _handle_read_tool(self, arguments: Dict) -> Dict:
"""集中处理 read_file 工具的三种模式。""" """集中处理 read_file 工具的三种模式。"""

View File

@ -7,11 +7,20 @@ from copy import deepcopy
from pathlib import Path from pathlib import Path
from typing import Any, Dict, Iterable, Optional, Union 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" PERSONALIZATION_FILENAME = "personalization.json"
MAX_SHORT_FIELD_LENGTH = 20 MAX_SHORT_FIELD_LENGTH = 20
MAX_CONSIDERATION_LENGTH = 50 MAX_CONSIDERATION_LENGTH = 50
MAX_CONSIDERATION_ITEMS = 10 MAX_CONSIDERATION_ITEMS = 10
TONE_PRESETS = ["健谈", "幽默", "直言不讳", "鼓励性", "诗意", "企业商务", "打破常规", "同理心"] TONE_PRESETS = ["健谈", "幽默", "直言不讳", "鼓励性", "诗意", "企业商务", "打破常规", "同理心"]
THINKING_INTERVAL_MIN = 1
THINKING_INTERVAL_MAX = 50
DEFAULT_PERSONALIZATION_CONFIG: Dict[str, Any] = { DEFAULT_PERSONALIZATION_CONFIG: Dict[str, Any] = {
"enabled": False, "enabled": False,
@ -20,6 +29,8 @@ DEFAULT_PERSONALIZATION_CONFIG: Dict[str, Any] = {
"profession": "", "profession": "",
"tone": "", "tone": "",
"considerations": [], "considerations": [],
"thinking_interval": None,
"disabled_tool_categories": [],
} }
__all__ = [ __all__ = [
@ -86,6 +97,7 @@ def sanitize_personalization_payload(
if fallback: if fallback:
base.update(fallback) base.update(fallback)
data = payload or {} data = payload or {}
allowed_tool_categories = set(TOOL_CATEGORIES.keys())
def _resolve_short_field(key: str) -> str: def _resolve_short_field(key: str) -> str:
if key in data: if key in data:
@ -101,6 +113,16 @@ def sanitize_personalization_payload(
base["considerations"] = _sanitize_considerations(data.get("considerations")) base["considerations"] = _sanitize_considerations(data.get("considerations"))
else: else:
base["considerations"] = _sanitize_considerations(base.get("considerations", [])) 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 return base
@ -172,3 +194,31 @@ def _sanitize_considerations(value: Any) -> list:
if len(cleaned) >= MAX_CONSIDERATION_ITEMS: if len(cleaned) >= MAX_CONSIDERATION_ITEMS:
break break
return cleaned 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

View File

@ -7,6 +7,8 @@ interface PersonalForm {
profession: string; profession: string;
tone: string; tone: string;
considerations: string[]; considerations: string[];
thinking_interval: number | null;
disabled_tool_categories: string[];
} }
interface PersonalizationState { interface PersonalizationState {
@ -23,15 +25,23 @@ interface PersonalizationState {
tonePresets: string[]; tonePresets: string[];
draggedConsiderationIndex: number | null; draggedConsiderationIndex: number | null;
form: PersonalForm; 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 => ({ const defaultForm = (): PersonalForm => ({
enabled: false, enabled: false,
self_identify: '', self_identify: '',
user_name: '', user_name: '',
profession: '', profession: '',
tone: '', tone: '',
considerations: [] considerations: [],
thinking_interval: null,
disabled_tool_categories: []
}); });
export const usePersonalizationStore = defineStore('personalization', { export const usePersonalizationStore = defineStore('personalization', {
@ -48,7 +58,10 @@ export const usePersonalizationStore = defineStore('personalization', {
newConsideration: '', newConsideration: '',
tonePresets: ['健谈', '幽默', '直言不讳', '鼓励性', '诗意', '企业商务', '打破常规', '同理心'], tonePresets: ['健谈', '幽默', '直言不讳', '鼓励性', '诗意', '企业商务', '打破常规', '同理心'],
draggedConsiderationIndex: null, draggedConsiderationIndex: null,
form: defaultForm() form: defaultForm(),
toolCategories: [],
thinkingIntervalDefault: DEFAULT_INTERVAL,
thinkingIntervalRange: { ...DEFAULT_INTERVAL_RANGE }
}), }),
actions: { actions: {
async openDrawer() { async openDrawer() {
@ -91,6 +104,7 @@ export const usePersonalizationStore = defineStore('personalization', {
throw new Error(result.error || '加载失败'); throw new Error(result.error || '加载失败');
} }
this.applyPersonalizationData(result.data || {}); this.applyPersonalizationData(result.data || {});
this.applyPersonalizationMeta(result);
this.loaded = true; this.loaded = true;
} catch (error: any) { } catch (error: any) {
this.error = error?.message || '加载失败'; this.error = error?.message || '加载失败';
@ -105,10 +119,38 @@ export const usePersonalizationStore = defineStore('personalization', {
user_name: data.user_name || '', user_name: data.user_name || '',
profession: data.profession || '', profession: data.profession || '',
tone: data.tone || '', 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(); 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() { clearFeedback() {
this.status = ''; this.status = '';
this.error = ''; this.error = '';
@ -133,6 +175,10 @@ export const usePersonalizationStore = defineStore('personalization', {
if (!resp.ok || !result.success) { if (!resp.ok || !result.success) {
throw new Error(result.error || '更新失败'); throw new Error(result.error || '更新失败');
} }
if (result.data) {
this.applyPersonalizationData(result.data);
}
this.applyPersonalizationMeta(result);
const statusLabel = newValue ? '已启用' : '已停用'; const statusLabel = newValue ? '已启用' : '已停用';
this.status = statusLabel; this.status = statusLabel;
setTimeout(() => { setTimeout(() => {
@ -165,6 +211,7 @@ export const usePersonalizationStore = defineStore('personalization', {
throw new Error(result.error || '保存失败'); throw new Error(result.error || '保存失败');
} }
this.applyPersonalizationData(result.data || {}); this.applyPersonalizationData(result.data || {});
this.applyPersonalizationMeta(result);
this.status = '已保存'; this.status = '已保存';
setTimeout(() => { setTimeout(() => {
if (this.status === '已保存') { if (this.status === '已保存') {
@ -187,6 +234,43 @@ export const usePersonalizationStore = defineStore('personalization', {
}; };
this.clearFeedback(); 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) { applyTonePreset(preset: string) {
if (!preset) { if (!preset) {
return; return;