feat: add manage_personalization tool and fix personalization sync

This commit is contained in:
JOJO 2026-04-13 20:22:06 +08:00
parent 4b7453ce4a
commit 13400f62d5
13 changed files with 280 additions and 16 deletions

View File

@ -787,6 +787,32 @@ class MainTerminalToolsDefinitionMixin:
"required": ["effect"]
}
}
},
{
"type": "function",
"function": {
"name": "manage_personalization",
"description": "管理用户个性化设置。支持读取所有配置或更新单个字段。可修改字段self_identifyAI自称最多20字、user_nameAI如何称呼用户最多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)

View File

@ -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']} 需要确认的操作:")

View File

@ -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,
),
}

View File

@ -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

View File

@ -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:

View File

@ -195,7 +195,8 @@ const appOptions = {
focusSetFiles: 'setFocusedFiles'
}),
...mapActions(usePersonalizationStore, {
personalizationOpenDrawer: 'openDrawer'
personalizationOpenDrawer: 'openDrawer',
personalizationFetch: 'fetchPersonalization'
})
}
};

View File

@ -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;

View File

@ -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) => {

View File

@ -1393,6 +1393,7 @@ export async function initializeLegacySocket(ctx: any) {
});
}
}
// 个性化设置工具主题切换处理在 taskPolling.ts 中(当前使用轮询模式)
if (data.message !== undefined) {
targetAction.tool.message = data.message;
}

View File

@ -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;

View File

@ -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);

View File

@ -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);

View File

@ -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) => {