diff --git a/modules/personalization_manager.py b/modules/personalization_manager.py index 4bb3cad..bfe2683 100644 --- a/modules/personalization_manager.py +++ b/modules/personalization_manager.py @@ -34,6 +34,7 @@ DEFAULT_PERSONALIZATION_CONFIG: Dict[str, Any] = { "thinking_interval": None, "disabled_tool_categories": [], "default_run_mode": None, + "auto_generate_title": True, } __all__ = [ @@ -108,6 +109,7 @@ def sanitize_personalization_payload( return _sanitize_short_field(base.get(key)) base["enabled"] = bool(data.get("enabled", base["enabled"])) + base["auto_generate_title"] = bool(data.get("auto_generate_title", base["auto_generate_title"])) base["self_identify"] = _resolve_short_field("self_identify") base["user_name"] = _resolve_short_field("user_name") base["profession"] = _resolve_short_field("profession") diff --git a/static/src/components/personalization/PersonalizationDrawer.vue b/static/src/components/personalization/PersonalizationDrawer.vue index 3628eb3..55b1674 100644 --- a/static/src/components/personalization/PersonalizationDrawer.vue +++ b/static/src/components/personalization/PersonalizationDrawer.vue @@ -207,6 +207,20 @@ +
+
+ 自动生成对话标题 +

默认开启;关闭后标题将沿用首条消息。

+
+ +
思考频率 diff --git a/static/src/stores/personalization.ts b/static/src/stores/personalization.ts index ccd2589..f6811f3 100644 --- a/static/src/stores/personalization.ts +++ b/static/src/stores/personalization.ts @@ -4,6 +4,7 @@ type RunMode = 'fast' | 'thinking' | 'deep'; interface PersonalForm { enabled: boolean; + auto_generate_title: boolean; self_identify: string; user_name: string; profession: string; @@ -51,6 +52,7 @@ const EXPERIMENT_STORAGE_KEY = 'agents_personalization_experiments'; const defaultForm = (): PersonalForm => ({ enabled: false, + auto_generate_title: true, self_identify: '', user_name: '', profession: '', @@ -169,6 +171,7 @@ export const usePersonalizationStore = defineStore('personalization', { applyPersonalizationData(data: any) { this.form = { enabled: !!data.enabled, + auto_generate_title: data.auto_generate_title !== false, self_identify: data.self_identify || '', user_name: data.user_name || '', profession: data.profession || '', @@ -282,7 +285,7 @@ export const usePersonalizationStore = defineStore('personalization', { this.saving = false; } }, - updateField(payload: { key: keyof PersonalForm; value: string }) { + updateField(payload: { key: keyof PersonalForm; value: any }) { if (!payload || !payload.key) { return; } diff --git a/utils/conversation_manager.py b/utils/conversation_manager.py index 421ee24..4b56014 100644 --- a/utils/conversation_manager.py +++ b/utils/conversation_manager.py @@ -323,6 +323,25 @@ class ConversationManager: print(f"📝 创建新对话: {conversation_id} - {conversation_data['title']}") return conversation_id + + def update_conversation_title(self, conversation_id: str, title: str) -> bool: + """更新对话标题并刷新索引。""" + if not conversation_id or not title: + return False + try: + data = self.load_conversation(conversation_id) + if not data: + return False + data["title"] = title + data["updated_at"] = datetime.now().isoformat() + self._save_conversation_file(conversation_id, data) + self._update_index(conversation_id, data) + if self.current_conversation_id == conversation_id: + self.current_conversation_title = title + return True + except Exception as exc: + print(f"⚠️ 更新对话标题失败 {conversation_id}: {exc}") + return False def _save_conversation_file(self, conversation_id: str, data: Dict): """保存对话文件""" diff --git a/web_server.py b/web_server.py index b0fdc16..5c0305d 100644 --- a/web_server.py +++ b/web_server.py @@ -100,6 +100,7 @@ from modules.user_container_manager import UserContainerManager from modules.usage_tracker import UsageTracker, QUOTA_DEFAULTS from utils.tool_result_formatter import format_tool_result_for_context from utils.conversation_manager import ConversationManager +from utils.api_client import DeepSeekClient app = Flask(__name__, static_folder='static') app.config['MAX_CONTENT_LENGTH'] = MAX_UPLOAD_SIZE @@ -180,6 +181,18 @@ LAST_ACTIVE_FILE = Path(LOGS_DIR).expanduser().resolve() / "last_active.json" _last_active_lock = threading.Lock() _last_active_cache: Dict[str, float] = {} _idle_reaper_started = False +TITLE_PROMPT = """### 任务: +生成一个简洁的、3-5个词的标题,并包含一个emoji,用于总结聊天内容。 + +### 指导原则: +- 标题应清晰代表主要话题 +- 使用与话题相关且单个的emoji +- 保持简洁(3-5个词) +- 清晰度优先于创意 +- 使用用户输入的语言回复 + +### 输出格式: +仅输出标题本身,不要附加解释。""" def sanitize_filename_preserve_unicode(filename: str) -> str: @@ -284,6 +297,64 @@ def start_background_jobs(): _load_last_active_cache() socketio.start_background_task(idle_reaper_loop) + +async def _generate_title_async(user_message: str) -> Optional[str]: + """使用快速模型生成对话标题。""" + if not user_message: + return None + client = DeepSeekClient(thinking_mode=False, web_mode=True) + messages = [ + {"role": "system", "content": TITLE_PROMPT}, + {"role": "user", "content": user_message} + ] + try: + async for resp in client.chat(messages, tools=[], stream=False): + try: + content = resp.get("choices", [{}])[0].get("message", {}).get("content") + if content: + return " ".join(str(content).strip().split()) + except Exception: + continue + except Exception as exc: + debug_log(f"[TitleGen] 生成标题异常: {exc}") + return None + + +def generate_conversation_title_background(web_terminal: WebTerminal, conversation_id: str, user_message: str, username: str): + """在后台生成对话标题并更新索引、推送给前端。""" + if not conversation_id or not user_message: + return + + async def _runner(): + title = await _generate_title_async(user_message) + if not title: + return + # 限长,避免标题过长 + safe_title = title[:80] + ok = False + try: + ok = web_terminal.context_manager.conversation_manager.update_conversation_title(conversation_id, safe_title) + except Exception as exc: + debug_log(f"[TitleGen] 保存标题失败: {exc}") + if not ok: + return + try: + socketio.emit('conversation_changed', { + 'conversation_id': conversation_id, + 'title': safe_title + }, room=f"user_{username}") + socketio.emit('conversation_list_update', { + 'action': 'updated', + 'conversation_id': conversation_id + }, room=f"user_{username}") + except Exception as exc: + debug_log(f"[TitleGen] 推送标题更新失败: {exc}") + + try: + asyncio.run(_runner()) + except Exception as exc: + debug_log(f"[TitleGen] 任务执行失败: {exc}") + def cache_monitor_snapshot(execution_id: Optional[str], stage: str, snapshot: Optional[Dict[str, Any]]): """缓存工具执行前/后的文件快照。""" if not execution_id or not snapshot or not snapshot.get('content'): @@ -2300,7 +2371,7 @@ def handle_message(data): send_to_client(event_type, data) # 传递客户端ID - socketio.start_background_task(process_message_task, terminal, message, send_with_activity, client_sid) + socketio.start_background_task(process_message_task, terminal, message, send_with_activity, client_sid, workspace, username) @socketio.on('client_chunk_log') @@ -2783,14 +2854,14 @@ def get_current_conversation(terminal: WebTerminal, workspace: UserWorkspace, us "error": str(e) }), 500 -def process_message_task(terminal: WebTerminal, message: str, sender, client_sid): +def process_message_task(terminal: WebTerminal, message: str, sender, client_sid, workspace: UserWorkspace, username: str): """在后台处理消息任务""" try: loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) # 创建可取消的任务 - task = loop.create_task(handle_task_with_sender(terminal, message, sender, client_sid)) + task = loop.create_task(handle_task_with_sender(terminal, workspace, message, sender, client_sid, username)) entry = stop_flags.get(client_sid) if not isinstance(entry, dict): @@ -2865,7 +2936,7 @@ def detect_malformed_tool_call(text): return False -async def handle_task_with_sender(terminal: WebTerminal, message, sender, client_sid): +async def handle_task_with_sender(terminal: WebTerminal, workspace: UserWorkspace, message, sender, client_sid, username: str): """处理任务并发送消息 - 集成token统计版本""" web_terminal = terminal conversation_id = getattr(web_terminal.context_manager, "current_conversation_id", None) @@ -2879,8 +2950,26 @@ async def handle_task_with_sender(terminal: WebTerminal, message, sender, client state["suppress_next"] = False # 添加到对话历史 + history_len_before = len(getattr(web_terminal.context_manager, "conversation_history", []) or []) + is_first_user_message = history_len_before == 0 web_terminal.context_manager.add_conversation("user", message) + if is_first_user_message and getattr(web_terminal, "context_manager", None): + try: + personal_config = load_personalization_config(workspace.data_dir) + except Exception: + personal_config = {} + auto_title_enabled = personal_config.get("auto_generate_title", True) + if auto_title_enabled: + conv_id = getattr(web_terminal.context_manager, "current_conversation_id", None) + socketio.start_background_task( + generate_conversation_title_background, + web_terminal, + conv_id, + message, + username + ) + # === 移除:不在这里计算输入token,改为在每次API调用前计算 === # 构建上下文和消息(用于API调用)