From 13400f62d5c3b68a209fc712d6730889bbeec2e8 Mon Sep 17 00:00:00 2001 From: JOJO <1498581755@qq.com> Date: Mon, 13 Apr 2026 20:22:06 +0800 Subject: [PATCH] feat: add manage_personalization tool and fix personalization sync --- core/main_terminal_parts/tools_definition.py | 47 ++++++ core/main_terminal_parts/tools_execution.py | 152 ++++++++++++++++++ core/tool_config.py | 5 + modules/personalization_manager.py | 9 ++ modules/skills_manager.py | 2 +- static/src/app.ts | 3 +- static/src/app/methods/taskPolling.ts | 29 ++++ static/src/auth/theme.ts | 8 +- static/src/composables/useLegacySocket.ts | 1 + static/src/stores/personalization.ts | 26 ++- static/src/styles/base/_tokens.scss | 2 +- static/src/styles/components/drag-upload.scss | 4 +- static/src/utils/theme.ts | 8 +- 13 files changed, 280 insertions(+), 16 deletions(-) diff --git a/core/main_terminal_parts/tools_definition.py b/core/main_terminal_parts/tools_definition.py index c225392..e36c81c 100644 --- a/core/main_terminal_parts/tools_definition.py +++ b/core/main_terminal_parts/tools_definition.py @@ -787,6 +787,32 @@ class MainTerminalToolsDefinitionMixin: "required": ["effect"] } } + }, + { + "type": "function", + "function": { + "name": "manage_personalization", + "description": "管理用户个性化设置。支持读取所有配置,或更新单个字段。可修改字段:self_identify(AI自称,最多20字)、user_name(AI如何称呼用户,最多20字)、profession(用户职业,最多20字)、tone(交流语气,最多20字)、considerations(注意事项列表,字符串数组,最多10项每项最多50字)、theme(主题配色:classic-经典/light-明亮/dark-暗黑)。更新时会自动验证格式,验证通过后立即保存并生效,新主题会立即应用到界面。", + "parameters": { + "type": "object", + "properties": self._inject_intent({ + "action": { + "type": "string", + "enum": ["read", "update"], + "description": "操作类型:read读取所有配置,update更新单个字段" + }, + "field": { + "type": "string", + "enum": ["self_identify", "user_name", "profession", "tone", "considerations", "theme"], + "description": "要更新的字段名(仅action=update时需要)" + }, + "value": { + "description": "新值(仅action=update时需要)。注意事项需提供字符串数组,其他字段提供字符串" + } + }), + "required": ["action"] + } + } } ] # 多模态模型自带能力,不再暴露 vlm_analyze,改为 view_image / view_video @@ -841,4 +867,25 @@ class MainTerminalToolsDefinitionMixin: tool for tool in tools if tool.get("function", {}).get("name") not in self.disabled_tools ] + + # 调试日志:记录工具列表 + tool_names = [t.get("function", {}).get("name") for t in tools] + logger.info("[define_tools] 可用工具列表: %s", tool_names) + print(f"[DEBUG] define_tools: 工具数量={len(tools)}") + + # 检查personalization分类状态 + if hasattr(self, 'tool_categories_map') and 'personalization' in self.tool_categories_map: + cat = self.tool_categories_map['personalization'] + state = self.tool_category_states.get('personalization', cat.default_enabled) + print(f"[DEBUG] personalization分类: default_enabled={cat.default_enabled}, current_state={state}") + else: + print(f"[DEBUG] personalization分类不在tool_categories_map中") + + if "manage_personalization" in tool_names: + logger.info("[define_tools] manage_personalization 工具已启用") + print("[DEBUG] manage_personalization 在可用工具列表中") + else: + logger.warning("[define_tools] manage_personalization 工具未找到!disabled_tools=%s", self.disabled_tools) + print(f"[DEBUG] manage_personalization 不在可用工具列表中!disabled_tools={self.disabled_tools}") + return self._apply_intent_to_tools(tools) diff --git a/core/main_terminal_parts/tools_execution.py b/core/main_terminal_parts/tools_execution.py index 984bad9..1c58b82 100644 --- a/core/main_terminal_parts/tools_execution.py +++ b/core/main_terminal_parts/tools_execution.py @@ -60,7 +60,12 @@ from modules.ocr_client import OCRClient from modules.easter_egg_manager import EasterEggManager from modules.personalization_manager import ( load_personalization_config, + save_personalization_config, build_personalization_prompt, + MAX_SHORT_FIELD_LENGTH, + MAX_CONSIDERATION_LENGTH, + MAX_CONSIDERATION_ITEMS, + ALLOWED_THEMES, ) from modules.skills_manager import ( get_skills_catalog, @@ -539,6 +544,8 @@ class MainTerminalToolsExecutionMixin: async def handle_tool_call(self, tool_name: str, arguments: Dict) -> str: """处理工具调用(添加参数预检查和改进错误处理)""" + logger.info("[handle_tool_call] 工具调用开始: tool_name=%s, arguments=%s", tool_name, arguments) + # 导入字符限制配置 from config import ( MAX_READ_FILE_CHARS, @@ -1275,15 +1282,160 @@ class MainTerminalToolsExecutionMixin: elif tool_name == "trigger_easter_egg": result = self.easter_egg_manager.trigger_effect(arguments.get("effect")) + elif tool_name == "manage_personalization": + logger.info("[handle_tool_call] 进入manage_personalization分支") + result = await self._execute_manage_personalization(arguments) + logger.info("[handle_tool_call] manage_personalization执行完成: result=%s", result) + else: result = {"success": False, "error": f"未知工具: {tool_name}"} except Exception as e: logger.error(f"工具执行失败: {tool_name} - {e}") + logger.exception("[handle_tool_call] 工具执行异常详情") result = {"success": False, "error": f"工具执行异常: {str(e)}"} + logger.info("[handle_tool_call] 工具调用结束: tool_name=%s, result=%s", tool_name, result) return json.dumps(result, ensure_ascii=False) + async def _execute_manage_personalization(self, arguments: Dict) -> Dict: + """执行个性化管理操作""" + print(f"[DEBUG] _execute_manage_personalization 被调用: {arguments}") + logger.info("[_execute_manage_personalization] 方法被调用: arguments=%s", arguments) + action = arguments.get("action") + logger.info("[_execute_manage_personalization] action=%s", action) + + if action == "read": + logger.info("[_execute_manage_personalization] 进入read分支") + # 读取所有配置 + try: + config = load_personalization_config(self.data_dir) + # 只返回可修改的字段 + readable_fields = ["self_identify", "user_name", "profession", "tone", "considerations", "theme", "enabled"] + result = {k: v for k, v in config.items() if k in readable_fields} + # 构建详细返回信息 + field_descriptions = { + "enabled": "个性化功能总开关", + "self_identify": f"AI自称: {result.get('self_identify') or '(未设置)'}", + "user_name": f"AI如何称呼用户: {result.get('user_name') or '(未设置)'}", + "profession": f"用户职业: {result.get('profession') or '(未设置)'}", + "tone": f"交流语气: {result.get('tone') or '(未设置)'}", + "considerations": f"注意事项: {len(result.get('considerations') or [])} 条", + "theme": f"主题配色: {result.get('theme', 'classic')}" + } + details = "\n".join([f"- {field_descriptions.get(k, k)}: {v}" for k, v in result.items()]) + logger.info("[_execute_manage_personalization] read成功") + return { + "success": True, + "data": result, + "message": f"个性化配置读取成功:\n{details}" + } + except Exception as e: + logger.error("[_execute_manage_personalization] read失败: %s", e) + return {"success": False, "error": f"读取配置失败: {str(e)}"} + + elif action == "update": + logger.info("[_execute_manage_personalization] 进入update分支") + field = arguments.get("field") + value = arguments.get("value") + logger.info("[_execute_manage_personalization] field=%s, value=%s", field, value) + + if not field: + logger.warning("[_execute_manage_personalization] field未指定") + return {"success": False, "error": "更新操作需要指定 field 参数"} + + # 验证字段是否允许修改 + logger.info("[_execute_manage_personalization] 验证字段: field=%s", field) + allowed_fields = ["self_identify", "user_name", "profession", "tone", "considerations", "theme"] + if field not in allowed_fields: + logger.warning("[_execute_manage_personalization] 字段不允许修改: %s not in %s", field, allowed_fields) + return {"success": False, "error": f"字段 '{field}' 不允许修改,可修改字段: {allowed_fields}"} + + # 验证value + validation_errors = [] + + if field in ["self_identify", "user_name", "profession", "tone"]: + # 字符串字段验证 + if not isinstance(value, str): + validation_errors.append(f"{field} 必须是字符串") + elif len(value) > MAX_SHORT_FIELD_LENGTH: + validation_errors.append(f"{field} 不能超过 {MAX_SHORT_FIELD_LENGTH} 个字符") + + elif field == "considerations": + # 注意事项数组验证 + if not isinstance(value, list): + validation_errors.append("considerations 必须是字符串数组") + else: + if len(value) > MAX_CONSIDERATION_ITEMS: + validation_errors.append(f"considerations 不能超过 {MAX_CONSIDERATION_ITEMS} 项") + for i, item in enumerate(value): + if not isinstance(item, str): + validation_errors.append(f"considerations[{i}] 必须是字符串") + elif len(item) > MAX_CONSIDERATION_LENGTH: + validation_errors.append(f"considerations[{i}] 不能超过 {MAX_CONSIDERATION_LENGTH} 个字符") + + elif field == "theme": + # 主题验证 + if not isinstance(value, str): + validation_errors.append("theme 必须是字符串") + elif value not in ALLOWED_THEMES: + validation_errors.append(f"theme 必须是以下之一: {list(ALLOWED_THEMES)}") + + if validation_errors: + logger.warning("[_execute_manage_personalization] 验证失败: %s", validation_errors) + return { + "success": False, + "error": "验证失败", + "validation_errors": validation_errors + } + + # 加载当前配置并更新 + try: + logger.info("[_execute_manage_personalization] 开始加载配置") + config = load_personalization_config(self.data_dir) + old_value = config.get(field) # 记录旧值 + config[field] = value + logger.info("[_execute_manage_personalization] 配置已修改: field=%s, old=%s, new=%s", field, old_value, value) + + # 保存配置 + save_personalization_config(self.data_dir, config) + print(f"[DEBUG] 配置已保存: field={field}, value={value}") + logger.info("[_execute_manage_personalization] 配置已保存到文件") + + # 重新加载配置到当前终端 + self.apply_personalization_preferences(config) + logger.info("[_execute_manage_personalization] 配置已应用到终端") + + # 调试日志:记录主题变更 + logger.info("[manage_personalization] 配置已更新并应用: field=%s, value=%s", field, value) + if field == "theme": + logger.info("[manage_personalization] 主题已变更: old=%s, new=%s", old_value, value) + + # 构建返回结果 + result = { + "success": True, + "message": f"字段 '{field}' 更新成功", + "updated_field": field, + "updated_value": value + } + + # 如果是主题更新,添加主题变更标记 + if field == "theme": + result["theme_changed"] = True + result["new_theme"] = value + logger.info("[manage_personalization] 返回结果包含主题变更标记: theme_changed=true, new_theme=%s", value) + + return result + + except Exception as e: + logger.error("[_execute_manage_personalization] 保存失败: %s", e) + logger.exception("[_execute_manage_personalization] 保存异常详情") + return {"success": False, "error": f"保存配置失败: {str(e)}"} + + else: + logger.warning("[_execute_manage_personalization] 未知的action: %s", action) + return {"success": False, "error": f"未知的 action: {action},可选: read, update"} + async def confirm_action(self, action: str, arguments: Dict) -> bool: """确认危险操作""" print(f"\n{OUTPUT_FORMATS['confirm']} 需要确认的操作:") diff --git a/core/tool_config.py b/core/tool_config.py index a8f2a46..2ca5f08 100644 --- a/core/tool_config.py +++ b/core/tool_config.py @@ -78,4 +78,9 @@ TOOL_CATEGORIES: Dict[str, ToolCategory] = { default_enabled=False, silent_when_disabled=True, ), + "personalization": ToolCategory( + label="个性化设置", + tools=["manage_personalization"], + default_enabled=True, + ), } diff --git a/modules/personalization_manager.py b/modules/personalization_manager.py index 4dd11ba..4ab4c96 100644 --- a/modules/personalization_manager.py +++ b/modules/personalization_manager.py @@ -17,6 +17,7 @@ from config.model_profiles import get_registered_model_keys ALLOWED_RUN_MODES = {"fast", "thinking", "deep"} ALLOWED_PERMISSION_MODES = {"readonly", "approval", "unrestricted"} +ALLOWED_THEMES = {"classic", "light", "dark"} PERSONALIZATION_FILENAME = "personalization.json" MAX_SHORT_FIELD_LENGTH = 20 @@ -78,6 +79,7 @@ DEFAULT_PERSONALIZATION_CONFIG: Dict[str, Any] = { "versioning_restore_mode": "overwrite", # 版本回溯模式固定为 overwrite "agents_md_auto_inject": False, # AGENTS.md 自动注入开关 "allow_root_file_creation": False, # 允许在根目录创建文件开关 + "theme": "classic", # 主题配色: classic-经典/light-明亮/dark-暗黑 } __all__ = [ @@ -374,6 +376,13 @@ def sanitize_personalization_payload( else: base["allow_root_file_creation"] = bool(base.get("allow_root_file_creation", False)) + # 主题配色 + theme_value = data.get("theme", base.get("theme")) + if isinstance(theme_value, str) and theme_value in ALLOWED_THEMES: + base["theme"] = theme_value + elif base.get("theme") not in ALLOWED_THEMES: + base["theme"] = "classic" + return base diff --git a/modules/skills_manager.py b/modules/skills_manager.py index c4422e8..ec022a3 100644 --- a/modules/skills_manager.py +++ b/modules/skills_manager.py @@ -192,7 +192,7 @@ def sync_workspace_skills( try: if skills_dir.exists(): - shutil.rmtree(skills_dir) + shutil.rmtree(skills_dir, ignore_errors=True) skills_dir.mkdir(parents=True, exist_ok=True) global_root = Path(base_dir or AGENT_SKILLS_DIR).expanduser().resolve() for skill_id in resolved: diff --git a/static/src/app.ts b/static/src/app.ts index 45482cd..b12850e 100644 --- a/static/src/app.ts +++ b/static/src/app.ts @@ -195,7 +195,8 @@ const appOptions = { focusSetFiles: 'setFocusedFiles' }), ...mapActions(usePersonalizationStore, { - personalizationOpenDrawer: 'openDrawer' + personalizationOpenDrawer: 'openDrawer', + personalizationFetch: 'fetchPersonalization' }) } }; diff --git a/static/src/app/methods/taskPolling.ts b/static/src/app/methods/taskPolling.ts index 5fc9fbe..5f618a8 100644 --- a/static/src/app/methods/taskPolling.ts +++ b/static/src/app/methods/taskPolling.ts @@ -875,6 +875,35 @@ export const taskPollingMethods = { } if (data.result !== undefined) { targetAction.tool.result = data.result; + + // 处理个性化设置工具 - 刷新个人空间数据 + if (targetAction.tool && targetAction.tool.name === 'manage_personalization') { + let result = data.result; + if (typeof result === 'string') { + try { result = JSON.parse(result); } catch (e) { /* ignore */ } + } + // 处理主题变更 + if (result?.theme_changed === true && result.new_theme) { + const theme = result.new_theme; + if (typeof window !== 'undefined' && window.localStorage) { + window.localStorage.setItem('agents_ui_theme', theme); + } + document.documentElement.setAttribute('data-theme', theme); + document.body.setAttribute('data-theme', theme); + } + // 任何字段更新后都刷新个人空间数据 + (async () => { + try { + const { usePersonalizationStore } = await import('../../stores/personalization'); + const personalizationStore = usePersonalizationStore(); + // 强制刷新个人空间数据 + await personalizationStore.fetchPersonalization(); + } catch (e) { + // 静默处理,不影响主流程 + console.debug('[TaskPolling] 刷新个人空间数据失败:', e); + } + })(); + } } if (data.message !== undefined) { targetAction.tool.message = data.message; diff --git a/static/src/auth/theme.ts b/static/src/auth/theme.ts index 1a7d5c5..bfee44c 100644 --- a/static/src/auth/theme.ts +++ b/static/src/auth/theme.ts @@ -1,14 +1,14 @@ -export type ThemeKey = 'claude' | 'light' | 'dark'; +export type ThemeKey = 'classic' | 'light' | 'dark'; const THEME_STORAGE_KEY = 'agents_ui_theme'; export const isThemeKey = (value: unknown): value is ThemeKey => - value === 'claude' || value === 'light' || value === 'dark'; + value === 'classic' || value === 'light' || value === 'dark'; export const loadTheme = (): ThemeKey => { - if (typeof window === 'undefined') return 'claude'; + if (typeof window === 'undefined') return 'classic'; const saved = window.localStorage.getItem(THEME_STORAGE_KEY); - return isThemeKey(saved) ? saved : 'claude'; + return isThemeKey(saved) ? saved : 'classic'; }; export const applyTheme = (theme: ThemeKey) => { diff --git a/static/src/composables/useLegacySocket.ts b/static/src/composables/useLegacySocket.ts index 881646f..ea8ee85 100644 --- a/static/src/composables/useLegacySocket.ts +++ b/static/src/composables/useLegacySocket.ts @@ -1393,6 +1393,7 @@ export async function initializeLegacySocket(ctx: any) { }); } } + // 个性化设置工具主题切换处理在 taskPolling.ts 中(当前使用轮询模式) if (data.message !== undefined) { targetAction.tool.message = data.message; } diff --git a/static/src/stores/personalization.ts b/static/src/stores/personalization.ts index 2d89a01..605877f 100644 --- a/static/src/stores/personalization.ts +++ b/static/src/stores/personalization.ts @@ -40,6 +40,7 @@ interface PersonalForm { deep_compress_trigger_tokens: number | null; agents_md_auto_inject: boolean; allow_root_file_creation: boolean; + theme: 'classic' | 'light' | 'dark'; } interface ExperimentState { @@ -108,7 +109,8 @@ const defaultForm = (): PersonalForm => ({ shallow_compress_trigger_tool_calls_interval: null, deep_compress_trigger_tokens: null, agents_md_auto_inject: false, - allow_root_file_creation: false + allow_root_file_creation: false, + theme: 'classic' }); const defaultExperimentState = (): ExperimentState => ({ @@ -171,7 +173,8 @@ export const usePersonalizationStore = defineStore('personalization', { actions: { async openDrawer() { this.visible = true; - if (!this.loaded && !this.loading) { + // 每次打开都刷新数据,确保显示最新内容 + if (!this.loading) { await this.fetchPersonalization(); } }, @@ -286,10 +289,27 @@ export const usePersonalizationStore = defineStore('personalization', { data.deep_compress_trigger_tokens ), agents_md_auto_inject: !!data.agents_md_auto_inject, - allow_root_file_creation: !!data.allow_root_file_creation + allow_root_file_creation: !!data.allow_root_file_creation, + theme: ['classic', 'light', 'dark'].includes(data.theme) ? data.theme : 'classic' }; + // 如果theme发生变化,应用到界面 + const currentTheme = (typeof window !== 'undefined' && window.localStorage) + ? window.localStorage.getItem('agents_ui_theme') + : null; + if (this.form.theme !== currentTheme) { + this.applyTheme(this.form.theme); + } this.clearFeedback(); }, + applyTheme(theme: 'classic' | 'light' | 'dark') { + if (typeof window === 'undefined') return; + // 同步到localStorage + window.localStorage.setItem('agents_ui_theme', 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; diff --git a/static/src/styles/base/_tokens.scss b/static/src/styles/base/_tokens.scss index d238d39..6d95644 100644 --- a/static/src/styles/base/_tokens.scss +++ b/static/src/styles/base/_tokens.scss @@ -45,7 +45,7 @@ --theme-card-border-strong: rgba(118, 103, 84, 0.25); } -:root[data-theme='claude'] { +:root[data-theme='classic'] { color-scheme: light; --claude-bg: #eeece2; --claude-panel: rgba(255, 255, 255, 0.82); diff --git a/static/src/styles/components/drag-upload.scss b/static/src/styles/components/drag-upload.scss index c35e257..1c7d656 100644 --- a/static/src/styles/components/drag-upload.scss +++ b/static/src/styles/components/drag-upload.scss @@ -93,8 +93,8 @@ } } -// 经典色主题(claude) - 提示文字改为黑色 -:root[data-theme='claude'], +// 经典色主题(classic) - 提示文字改为黑色 +:root[data-theme='classic'], :root:not([data-theme]) { .drag-upload-hint { color: var(--claude-text); diff --git a/static/src/utils/theme.ts b/static/src/utils/theme.ts index 8ae5bf7..87cae5f 100644 --- a/static/src/utils/theme.ts +++ b/static/src/utils/theme.ts @@ -1,6 +1,6 @@ import { onMounted } from 'vue'; -type ThemeKey = 'claude' | 'light' | 'dark'; +type ThemeKey = 'classic' | 'light' | 'dark'; const THEME_STORAGE_KEY = 'agents_ui_theme'; @@ -64,10 +64,10 @@ const applyTheme = (theme: ThemeKey) => { }; const loadTheme = (): ThemeKey => { - if (typeof window === 'undefined') return 'claude'; + if (typeof window === 'undefined') return 'classic'; const saved = window.localStorage.getItem(THEME_STORAGE_KEY) as ThemeKey | null; - if (saved === 'light' || saved === 'dark' || saved === 'claude') return saved; - return 'claude'; + if (saved === 'light' || saved === 'dark' || saved === 'classic') return saved; + return 'classic'; }; const persistTheme = (theme: ThemeKey) => {