feat: add manage_personalization tool and fix personalization sync
This commit is contained in:
parent
4b7453ce4a
commit
13400f62d5
@ -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)
|
||||
|
||||
@ -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']} 需要确认的操作:")
|
||||
|
||||
@ -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,
|
||||
),
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -195,7 +195,8 @@ const appOptions = {
|
||||
focusSetFiles: 'setFocusedFiles'
|
||||
}),
|
||||
...mapActions(usePersonalizationStore, {
|
||||
personalizationOpenDrawer: 'openDrawer'
|
||||
personalizationOpenDrawer: 'openDrawer',
|
||||
personalizationFetch: 'fetchPersonalization'
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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) => {
|
||||
|
||||
@ -1393,6 +1393,7 @@ export async function initializeLegacySocket(ctx: any) {
|
||||
});
|
||||
}
|
||||
}
|
||||
// 个性化设置工具主题切换处理在 taskPolling.ts 中(当前使用轮询模式)
|
||||
if (data.message !== undefined) {
|
||||
targetAction.tool.message = data.message;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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) => {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user