From deaa1560a5a5a6fb17fd9153c42eb2e1d074a82b Mon Sep 17 00:00:00 2001 From: JOJO <1498581755@qq.com> Date: Tue, 1 Sep 2026 09:46:05 +0800 Subject: [PATCH] =?UTF-8?q?feat(personalization):=20=E4=B8=AA=E4=BA=BA?= =?UTF-8?q?=E7=A9=BA=E9=97=B4=E6=94=AF=E6=8C=81=E9=85=8D=E7=BD=AE=E5=AF=B9?= =?UTF-8?q?=E8=AF=9D=E6=A0=87=E9=A2=98=E7=94=9F=E6=88=90=E6=A8=A1=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 title_model 配置,复用子智能体模型库条目生成对话标题: - 个人空间「常规」页签新增「标题生成模型」下拉菜单(复用子智能体 模型列表,general 页签挂载时加载) - 标题生成改为使用所选子智能体模型 profile;未配置时回落模型库 default_model,移除 AGENT_TITLE_* 环境变量覆盖与主智能体默认 模型回退(个人空间为唯一配置来源) - personalization 白名单注册 title_model 键(默认值 + sanitize) --- modules/personalization_manager.py | 2 + modules/review_agent_config.py | 11 +++- server/chat_flow.py | 3 +- server/chat_flow_helpers.py | 61 ++++++++++--------- server/chat_flow_runtime.py | 3 +- server/chat_flow_task_main.py | 3 +- .../personalization/PersonalizationDrawer.vue | 18 +++--- .../personalization/tabs/GeneralTab.vue | 54 +++++++++++++++- static/src/locales/en-US/personalization.ts | 3 + static/src/locales/zh-CN/personalization.ts | 3 + static/src/stores/personalization.ts | 4 ++ 11 files changed, 122 insertions(+), 43 deletions(-) diff --git a/modules/personalization_manager.py b/modules/personalization_manager.py index 2297ca21..d82d4655 100644 --- a/modules/personalization_manager.py +++ b/modules/personalization_manager.py @@ -82,6 +82,7 @@ DEFAULT_PERSONALIZATION_CONFIG: Dict[str, Any] = { "default_permission_mode": "unrestricted", "default_work_mode": "plan", # 默认运行模式:plan / ask / execute "auto_generate_title": True, + "title_model": "", # 对话标题生成使用的子智能体模型条目名(空=跟随主对话默认模型) "recent_conversations_prompt_enabled": False, "recent_conversations_prompt_limit": RECENT_CONVERSATIONS_PROMPT_LIMIT_DEFAULT, "project_memory_inject_limit": PROJECT_MEMORY_INJECT_LIMIT_DEFAULT, # 项目记忆索引最大注入条数:None-无上限 / >=5-该值 @@ -278,6 +279,7 @@ def sanitize_personalization_payload( else "medium" ) base["auto_generate_title"] = bool(data.get("auto_generate_title", base["auto_generate_title"])) + base["title_model"] = str(data.get("title_model", base.get("title_model", "")) or "").strip() base["recent_conversations_prompt_enabled"] = bool( data.get("recent_conversations_prompt_enabled", base.get("recent_conversations_prompt_enabled", False)) ) diff --git a/modules/review_agent_config.py b/modules/review_agent_config.py index 63161dec..05b78365 100644 --- a/modules/review_agent_config.py +++ b/modules/review_agent_config.py @@ -18,7 +18,7 @@ from config import DATA_DIR from config.sub_agent import SUB_AGENT_MODELS_CONFIG_FILE from modules.personalization_manager import REVIEW_AGENT_KEYS -__all__ = ["resolve_review_agent_config", "REVIEW_AGENT_KEYS"] +__all__ = ["resolve_review_agent_config", "resolve_sub_agent_model_profile", "REVIEW_AGENT_KEYS"] def _load_model_entry(model_name: str) -> Dict[str, Any]: @@ -52,6 +52,15 @@ def _load_model_entry(model_name: str) -> Dict[str, Any]: return model_map.get(chosen) +def resolve_sub_agent_model_profile(model_name: str) -> Dict[str, Any]: + """按名称从子智能体模型库解析模型 profile(APIClient.apply_profile 格式)。 + + 名称留空或不存在时回落模型库 default_model(模型库也不可用时返回 None, + 由调用方决定兜底行为)。 + """ + return _load_model_entry(str(model_name).strip() if model_name and str(model_name).strip() else "") + + def resolve_review_agent_config(agent_key: str) -> Dict[str, Any]: """解析指定审核智能体的完整运行配置。 diff --git a/server/chat_flow.py b/server/chat_flow.py index 42716f0a..f54df92b 100644 --- a/server/chat_flow.py +++ b/server/chat_flow.py @@ -112,7 +112,7 @@ from modules.i18n import tr conversation_bp = Blueprint('conversation', __name__) -def generate_conversation_title_background(web_terminal: WebTerminal, conversation_id: str, user_message: str, username: str): +def generate_conversation_title_background(web_terminal: WebTerminal, conversation_id: str, user_message: str, username: str, title_model: str = ""): """在后台生成对话标题并更新索引、推送给前端。""" return _generate_conversation_title_background( web_terminal=web_terminal, @@ -122,6 +122,7 @@ def generate_conversation_title_background(web_terminal: WebTerminal, conversati socketio_instance=socketio, title_prompt_path=TITLE_PROMPT_PATH, debug_logger=debug_log, + title_model=title_model, ) diff --git a/server/chat_flow_helpers.py b/server/chat_flow_helpers.py index e5361f11..87be0cf2 100644 --- a/server/chat_flow_helpers.py +++ b/server/chat_flow_helpers.py @@ -2,7 +2,6 @@ from __future__ import annotations import asyncio import json -import os import re from datetime import datetime from pathlib import Path @@ -11,7 +10,6 @@ from typing import Any, Dict, List, Optional from core.web_terminal import WebTerminal from config import LOGS_DIR from utils.api_client import APIClient -from config.model_profiles import get_default_model_key, get_model_profile TITLE_DEBUG_DIR = Path(LOGS_DIR).expanduser().resolve() / "title_debug" TITLE_DEBUG_FILE = TITLE_DEBUG_DIR / "title_generation.log" @@ -32,44 +30,33 @@ def _title_debug_log(message: str, **extra: Any) -> None: pass -def _env_optional(name: str) -> Optional[str]: - """从环境变量获取可选配置(已由 config/__init__.py 统一注入)。""" - value = os.environ.get(name) - if value is None: - return None - value = value.strip() - return value or None - - async def _generate_title_async( user_message: str, title_prompt_path, debug_logger, + model_profile: Optional[Dict[str, Any]] = None, ) -> Optional[str]: - """使用快速模型生成对话标题。""" + """使用子智能体模型生成对话标题。 + + model_profile 来自个人空间「标题生成模型」配置解析出的子智能体模型库条目 + (未配置时为模型库 default_model)。个人空间是唯一配置来源:不使用主智能体 + 默认模型,也不支持 AGENT_TITLE_* 环境变量覆盖;profile 缺失或应用失败时 + 直接放弃生成(带日志),不做其他回退。 + """ if not user_message: _title_debug_log("skip_empty_user_message") return None client = APIClient(thinking_mode=False, web_mode=True) + if not model_profile: + _title_debug_log("title_model_profile_missing") + return None try: - default_model = get_default_model_key() - client.model_key = default_model - client.apply_profile(get_model_profile(default_model)) + client.apply_profile(model_profile) + _title_debug_log("title_model_profile_applied", model_name=model_profile.get("name")) except Exception as exc: - _title_debug_log("default_title_model_profile_failed", error=str(exc)) - title_base = _env_optional("AGENT_TITLE_API_BASE_URL") - title_key = _env_optional("AGENT_TITLE_API_KEY") - title_model = _env_optional("AGENT_TITLE_MODEL_ID") - if title_base: - client.fast_api_config["base_url"] = title_base - client.api_base_url = title_base - if title_key: - client.fast_api_config["api_key"] = title_key - client.api_key = title_key - if title_model: - client.fast_api_config["model_id"] = title_model - client.model_id = title_model + _title_debug_log("title_model_profile_failed", error=str(exc), model_name=model_profile.get("name")) + return None _title_debug_log("start_generate_title", user_message_preview=str(user_message)[:200], user_message_len=len(str(user_message))) _title_debug_log( "title_api_config", @@ -118,13 +105,27 @@ def generate_conversation_title_background( socketio_instance, title_prompt_path, debug_logger, + title_model: str = "", ): - """在后台生成对话标题并更新索引、推送给前端。""" + """在后台生成对话标题并更新索引、推送给前端。 + + title_model 为个人空间配置的子智能体模型条目名(空 = 子智能体模型库 + default_model)。个人空间是唯一配置来源。 + """ if not conversation_id or not user_message: return async def _runner(): - title = await _generate_title_async(user_message, title_prompt_path, debug_logger) + try: + from modules.review_agent_config import resolve_sub_agent_model_profile + # 未配置(空)时回落子智能体模型库 default_model,个人空间为唯一配置来源 + model_profile = resolve_sub_agent_model_profile(title_model) + except Exception: + model_profile = None + if model_profile is None: + _title_debug_log("title_model_profile_unavailable", title_model=title_model, conversation_id=conversation_id) + return + title = await _generate_title_async(user_message, title_prompt_path, debug_logger, model_profile=model_profile) if not title: _title_debug_log("title_not_generated", conversation_id=conversation_id, username=username) return diff --git a/server/chat_flow_runtime.py b/server/chat_flow_runtime.py index 7431c916..5e8c1e9a 100644 --- a/server/chat_flow_runtime.py +++ b/server/chat_flow_runtime.py @@ -104,7 +104,7 @@ from .chat_flow_runner_helpers import ( ) -def generate_conversation_title_background(web_terminal: WebTerminal, conversation_id: str, user_message: str, username: str): +def generate_conversation_title_background(web_terminal: WebTerminal, conversation_id: str, user_message: str, username: str, title_model: str = ""): return _generate_conversation_title_background( web_terminal=web_terminal, conversation_id=conversation_id, @@ -113,6 +113,7 @@ def generate_conversation_title_background(web_terminal: WebTerminal, conversati socketio_instance=socketio, title_prompt_path=TITLE_PROMPT_PATH, debug_logger=debug_log, + title_model=title_model, ) def detect_malformed_tool_call(text): diff --git a/server/chat_flow_task_main.py b/server/chat_flow_task_main.py index c6a7e9af..f7a5914d 100644 --- a/server/chat_flow_task_main.py +++ b/server/chat_flow_task_main.py @@ -1855,7 +1855,8 @@ async def handle_task_with_sender( web_terminal, conv_id, message, - username + username, + personal_config.get("title_model", "") ) # 自动深层压缩(用户输入后触发) diff --git a/static/src/components/personalization/PersonalizationDrawer.vue b/static/src/components/personalization/PersonalizationDrawer.vue index 7ab1279e..3620b0ac 100644 --- a/static/src/components/personalization/PersonalizationDrawer.vue +++ b/static/src/components/personalization/PersonalizationDrawer.vue @@ -874,14 +874,16 @@ watch( if (isVisible && tab === 'review-agents') { loadSubAgentModels(); } - if ( - isVisible && - tab === 'general' && - isAppShell.value && - !appUpdateInfo.value && - !appUpdateChecking.value - ) { - checkAppUpdate(); + if (isVisible && tab === 'general') { + // 「标题生成模型」菜单复用子智能体模型库,需在常规页签也加载模型列表 + loadSubAgentModels(); + if ( + isAppShell.value && + !appUpdateInfo.value && + !appUpdateChecking.value + ) { + checkAppUpdate(); + } } } ); diff --git a/static/src/components/personalization/tabs/GeneralTab.vue b/static/src/components/personalization/tabs/GeneralTab.vue index d58858ef..80eacb9f 100644 --- a/static/src/components/personalization/tabs/GeneralTab.vue +++ b/static/src/components/personalization/tabs/GeneralTab.vue @@ -32,6 +32,10 @@ const sandboxShowWizard = computed(() => { */ const ctx = inject>('personalizationDrawer')!; const { + activeDropdown, + activeTheme, + closeDropdown, + floatingMenuStyle, personalization, form, startTutorial, @@ -42,7 +46,9 @@ const { appUpdateStateText, appUpdateChecking, checkAppUpdate, - downloadLatestApp + downloadLatestApp, + subAgentModels, + toggleDropdown } = ctx; @@ -65,6 +71,52 @@ const { /> + + +
+ + {{ $t('personalization.titleModelTitle') }} + {{ $t('personalization.titleModelDesc') }} + +
+ +
+ + +
+
+
+
{{ $t('personalization.tutorialTitle') }} diff --git a/static/src/locales/en-US/personalization.ts b/static/src/locales/en-US/personalization.ts index 857927be..568d7e47 100644 --- a/static/src/locales/en-US/personalization.ts +++ b/static/src/locales/en-US/personalization.ts @@ -330,6 +330,9 @@ export default { reviewAgentWorkflowReviewDesc: 'Judges whether stage output meets the bar at workflow review nodes', reviewModelEmptyDesc: 'Leave empty to use the model library default', reviewDefaultModelDesc: 'Uses the model library default_model', + titleModelTitle: 'Title generation model', + titleModelDesc: 'Choose the AI model used to generate conversation titles, shared with sub-agents', + titleDefaultModelDesc: 'Uses the sub-agent model library default', reviewThinkingDesc: 'Falls back to fast mode automatically when the model does not support thinking', timeoutTitle: 'Review request timeout', timeoutDesc: 'Single model request timeout (5-3600 seconds)', diff --git a/static/src/locales/zh-CN/personalization.ts b/static/src/locales/zh-CN/personalization.ts index 0099f2af..626beeda 100644 --- a/static/src/locales/zh-CN/personalization.ts +++ b/static/src/locales/zh-CN/personalization.ts @@ -331,6 +331,9 @@ export default { reviewAgentWorkflowReviewDesc: '工作流审核节点判断阶段产出是否达标', reviewModelEmptyDesc: '留空则使用模型库默认模型', reviewDefaultModelDesc: '使用模型库的 default_model', + titleModelTitle: '标题生成模型', + titleModelDesc: '选择用于生成对话标题的 AI 模型,与子智能体共用模型库', + titleDefaultModelDesc: '使用子智能体模型库的默认模型', reviewThinkingDesc: '模型不支持思考时自动回落快速模式', timeoutTitle: '审核请求超时', timeoutDesc: '单次模型请求超时(5-3600 秒)', diff --git a/static/src/stores/personalization.ts b/static/src/stores/personalization.ts index 1f7522c4..cd0bdbac 100644 --- a/static/src/stores/personalization.ts +++ b/static/src/stores/personalization.ts @@ -94,6 +94,8 @@ interface PersonalForm { communication_style: CommunicationStyle; conversation_continuity: ConversationContinuity; auto_generate_title: boolean; + /** 标题生成模型(子智能体模型库条目名);留空 = 模型库 default_model */ + title_model: string; recent_conversations_prompt_enabled: boolean; recent_conversations_prompt_limit: number | string; project_memory_inject_limit: number | string | null; @@ -300,6 +302,7 @@ const defaultForm = (): PersonalForm => ({ communication_style: 'default', conversation_continuity: 'medium', auto_generate_title: true, + title_model: '', recent_conversations_prompt_enabled: false, recent_conversations_prompt_limit: DEFAULT_RECENT_CONVERSATIONS_PROMPT_LIMIT, project_memory_inject_limit: DEFAULT_PROJECT_MEMORY_INJECT_LIMIT, @@ -515,6 +518,7 @@ export const usePersonalizationStore = defineStore('personalization', { ? data.conversation_continuity : 'medium', auto_generate_title: data.auto_generate_title !== false, + title_model: typeof data.title_model === 'string' ? data.title_model : '', recent_conversations_prompt_enabled: !!data.recent_conversations_prompt_enabled, recent_conversations_prompt_limit: this.normalizeRecentConversationsPromptLimit( data.recent_conversations_prompt_limit