diff --git a/core/main_terminal.py b/core/main_terminal.py index bdffadd..af09569 100644 --- a/core/main_terminal.py +++ b/core/main_terminal.py @@ -374,7 +374,8 @@ class MainTerminal(MainTerminalCommandMixin, MainTerminalContextMixin, MainTermi if not cm or not conv_id: return try: - cm.conversation_manager.update_conversation_metadata(conv_id, updates) + target_manager = cm._get_conversation_manager_for_id(conv_id) if hasattr(cm, "_get_conversation_manager_for_id") else cm.conversation_manager + target_manager.update_conversation_metadata(conv_id, updates) if isinstance(getattr(cm, "conversation_metadata", None), dict): cm.conversation_metadata.update(updates) except Exception: diff --git a/core/main_terminal_parts/tools_policy.py b/core/main_terminal_parts/tools_policy.py index 51bd82c..19ecb37 100644 --- a/core/main_terminal_parts/tools_policy.py +++ b/core/main_terminal_parts/tools_policy.py @@ -218,8 +218,7 @@ class MainTerminalToolsPolicyMixin: conv_id = conversation_id or getattr(getattr(self, "context_manager", None), "current_conversation_id", None) if conv_id and getattr(self, "context_manager", None): try: - self.context_manager.conversation_manager.update_conversation_metadata( - conv_id, + self.context_manager._get_conversation_manager_for_id(conv_id).update_conversation_metadata(conv_id, {"permission_mode": normalized}, ) if self.context_manager.current_conversation_id == conv_id: diff --git a/core/web_terminal.py b/core/web_terminal.py index 24ab6ad..43f40a7 100644 --- a/core/web_terminal.py +++ b/core/web_terminal.py @@ -206,7 +206,8 @@ class WebTerminal(MainTerminal): # 把默认模型同步到新对话的 metadata try: - self.context_manager.conversation_manager.save_conversation( + target_manager = self.context_manager._get_conversation_manager_for_id(conversation_id) + target_manager.save_conversation( conversation_id=conversation_id, messages=self.context_manager.conversation_history, project_path=str(self.project_path), @@ -275,7 +276,8 @@ class WebTerminal(MainTerminal): perf_log("_ensure_conversation_versioning_enabled manager created", elapsed_ms=(time.perf_counter() - t0) * 1000, extra={"conv_id": normalized_id}) meta = manager.set_enabled(enabled=True, mode="overwrite", tracking_mode=tracking_mode) perf_log("_ensure_conversation_versioning_enabled set_enabled done", elapsed_ms=(time.perf_counter() - t0) * 1000, extra={"conv_id": normalized_id}) - conv_data = self.context_manager.conversation_manager.load_conversation(normalized_id) or {} + target_manager = self.context_manager._get_conversation_manager_for_id(normalized_id) + conv_data = target_manager.load_conversation(normalized_id) or {} snapshot_payload = { "conversation_id": normalized_id, "title": conv_data.get("title"), @@ -293,7 +295,7 @@ class WebTerminal(MainTerminal): init_row = init_result.get("row") or {} if init_row.get("tree_hash"): meta["last_tree_hash"] = init_row.get("tree_hash") - self.context_manager.conversation_manager.update_conversation_metadata( + target_manager.update_conversation_metadata( normalized_id, { "versioning": { @@ -324,7 +326,8 @@ class WebTerminal(MainTerminal): if success: # 根据对话元数据同步思考模式 try: - conv_data = self.context_manager.conversation_manager.load_conversation(conversation_id) or {} + target_manager = self.context_manager._get_conversation_manager_for_id(conversation_id) + conv_data = target_manager.load_conversation(conversation_id) or {} meta = conv_data.get("metadata", {}) or {} mode = bool(meta.get("thinking_mode", self.thinking_mode)) self.thinking_mode = mode @@ -371,7 +374,8 @@ class WebTerminal(MainTerminal): self.current_session_id += 1 # 获取对话信息 - conversation_data = self.context_manager.conversation_manager.load_conversation(conversation_id) + target_manager = self.context_manager._get_conversation_manager_for_id(conversation_id) + conversation_data = target_manager.load_conversation(conversation_id) if not conversation_data: return { "success": False, diff --git a/modules/multi_agent/state.py b/modules/multi_agent/state.py index 4c51f4a..67a1b96 100644 --- a/modules/multi_agent/state.py +++ b/modules/multi_agent/state.py @@ -19,6 +19,7 @@ from __future__ import annotations import asyncio import json +import re import uuid from asyncio import AbstractEventLoop from dataclasses import dataclass, field @@ -51,6 +52,7 @@ def format_multi_agent_message( target: Optional[str] = None, extra_attrs: Optional[Dict[str, str]] = None, msg_type_text: Optional[str] = None, + subtype: Optional[str] = None, ) -> str: """按统一格式构造 user 消息字符串。 @@ -62,6 +64,7 @@ def format_multi_agent_message( target: 接收方显示名(用于子→子 提问时标明对谁提问) extra_attrs: 额外标签属性(如 question_id="ask_xxx") msg_type_text: 覆盖默认的中文消息类型文案(如"任务结束汇报") + subtype: 渲染/分类使用的子类型(如 progress_output / completion_report / ask_master) """ if not msg_id: msg_id = f"msg_{uuid.uuid4().hex[:10]}" @@ -80,6 +83,8 @@ def format_multi_agent_message( attrs = "" if target: attrs += f' target="{target}"' + if subtype: + attrs += f' subtype="{subtype}"' if extra_attrs: for k, v in extra_attrs.items(): attrs += f' {k}="{v}"' @@ -119,6 +124,13 @@ def build_master_dispatch_text(task: str, msg_id: Optional[str] = None) -> str: ) +# 子类型常量(用于前端渲染与后端分类) +SUBTYPE_PROGRESS_OUTPUT = "progress_output" +SUBTYPE_COMPLETION_REPORT = "completion_report" +SUBTYPE_ASK_MASTER = "ask_master" +SUBTYPE_ASK_OTHER = "ask_other" + + def build_sub_agent_output_text(display_name: str, content: str, msg_id: Optional[str] = None, *, is_final: bool = False) -> str: """子智能体输出(进度或完成)插入到主对话的 user 消息文本。""" return format_multi_agent_message( @@ -127,6 +139,7 @@ def build_sub_agent_output_text(display_name: str, content: str, msg_id: Optiona content=content, msg_id=msg_id, msg_type_text="任务结束汇报" if is_final else "任务进度输出", + subtype=SUBTYPE_COMPLETION_REPORT if is_final else SUBTYPE_PROGRESS_OUTPUT, ) @@ -137,6 +150,7 @@ def build_sub_agent_ask_master_text(display_name: str, question: str, question_i msg_type=TYPE_ASK, content=question, msg_id=question_id, + subtype=SUBTYPE_ASK_MASTER, ) @@ -153,9 +167,45 @@ def build_sub_agent_ask_other_text( content=question, msg_id=question_id, target=target_display, + subtype=SUBTYPE_ASK_OTHER, ) +_MULTI_AGENT_MESSAGE_RE = re.compile( + r"^来自\s+(?P.+?)\s+的(?P.+?)\n" + r"id:\s*(?P\S+)\n\n" + r"<(?P=display_name)>\n" + r"<(?P\w+)(?P[^>]*)>\n" + r"(?P.*?)\n" + r"\n" + r"$", + re.DOTALL, +) + + +def parse_multi_agent_message(text: str) -> Optional[Dict[str, str]]: + """解析标准多智能体消息格式。 + + 返回字段:display_name, type_text, msg_id, tag, subtype, content。 + 若不是标准格式则返回 None。 + """ + if not text: + return None + m = _MULTI_AGENT_MESSAGE_RE.search(text) + if not m: + return None + attrs = m.group("attrs") or "" + subtype_match = re.search(r'subtype="([^"]+)"', attrs) + return { + "display_name": m.group("display_name").strip(), + "type_text": m.group("type_text").strip(), + "msg_id": m.group("msg_id").strip(), + "tag": m.group("tag").strip(), + "subtype": subtype_match.group(1) if subtype_match else "", + "content": m.group("content"), + } + + def build_master_message_to_sub_agent(message: str, msg_id: Optional[str] = None) -> str: """主智能体 send_message_to_sub_agent 时插入子对话的 user 消息文本。""" return format_multi_agent_message( diff --git a/scripts/migrate_multi_agent_conversations.py b/scripts/migrate_multi_agent_conversations.py new file mode 100644 index 0000000..7b4832b --- /dev/null +++ b/scripts/migrate_multi_agent_conversations.py @@ -0,0 +1,143 @@ +#!/usr/bin/env python3 +"""把误存到常规对话目录的多智能体会话迁移到 mutiagents/conversations/。 + +用法: + python3 scripts/migrate_multi_agent_conversations.py [--dry-run] +""" +from __future__ import annotations + +import argparse +import json +import shutil +import sys +import time +from datetime import datetime +from pathlib import Path + +# 加载项目配置以确定数据目录 +project_root = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(project_root)) +from config import DATA_DIR, IS_HOST_MODE +from modules.multi_agent.role_store import DEFAULT_MUTIAGENTS_DIR + + +def load_json(path: Path) -> dict: + return json.loads(path.read_text(encoding="utf-8")) + + +def save_json(path: Path, data: dict) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + tmp = path.with_suffix(f".tmp.{int(time.time() * 1000)}") + tmp.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8") + shutil.move(str(tmp), str(path)) + + +def migrate(dry_run: bool = False) -> dict: + regular_dir = Path(DATA_DIR) / "conversations" / "workspace" + ma_dir = Path(DEFAULT_MUTIAGENTS_DIR) / "conversations" / "workspace" + + regular_index_path = regular_dir / "index.json" + ma_index_path = ma_dir / "index.json" + + regular_index = load_json(regular_index_path) if regular_index_path.exists() else {} + ma_index = load_json(ma_index_path) if ma_index_path.exists() else {} + + moved = [] + skipped = [] + errors = [] + + for f in sorted(regular_dir.glob("conv_*.json")): + if f.name == "index.json": + continue + try: + data = load_json(f) + except Exception as exc: + errors.append({"file": str(f), "error": f"读取失败: {exc}"}) + continue + + meta = data.get("metadata", {}) or {} + if not meta.get("multi_agent_mode"): + skipped.append(f.name) + continue + + conv_id = data.get("id") or f.stem + target_file = ma_dir / f.name + + # 目标已存在时跳过,避免覆盖 + if target_file.exists(): + errors.append({"file": str(f), "error": f"目标已存在: {target_file}"}) + continue + + if dry_run: + moved.append({"id": conv_id, "file": f.name, "target": str(target_file)}) + continue + + try: + # 移动文件 + target_file.parent.mkdir(parents=True, exist_ok=True) + shutil.move(str(f), str(target_file)) + + # 更新索引 + if conv_id in regular_index: + ma_index[conv_id] = regular_index.pop(conv_id) + else: + # 从文件重建索引条目 + ma_index[conv_id] = { + "title": data.get("title") or "未命名对话", + "created_at": data.get("created_at"), + "updated_at": data.get("updated_at"), + "project_path": meta.get("project_path"), + "project_relative_path": meta.get("project_relative_path"), + "thinking_mode": meta.get("thinking_mode", False), + "run_mode": meta.get("run_mode") or ("thinking" if meta.get("thinking_mode") else "fast"), + "model_key": meta.get("model_key"), + "has_images": meta.get("has_images", False), + "has_videos": meta.get("has_videos", False), + "total_messages": meta.get("total_messages", 0), + "total_tools": meta.get("total_tools", 0), + "status": meta.get("status", "active"), + "multi_agent_mode": True, + } + + moved.append({"id": conv_id, "file": f.name, "target": str(target_file)}) + except Exception as exc: + errors.append({"file": str(f), "error": f"迁移失败: {exc}"}) + + if not dry_run: + # 保存索引 + save_json(regular_index_path, regular_index) + save_json(ma_index_path, ma_index) + + return { + "dry_run": dry_run, + "regular_dir": str(regular_dir), + "ma_dir": str(ma_dir), + "moved": moved, + "skipped_count": len(skipped), + "errors": errors, + } + + +def main(): + parser = argparse.ArgumentParser(description="迁移多智能体会话到 mutiagents 目录") + parser.add_argument("--dry-run", action="store_true", help="只预览,不实际移动") + args = parser.parse_args() + + result = migrate(dry_run=args.dry_run) + + print(f"常规目录: {result['regular_dir']}") + print(f"目标目录: {result['ma_dir']}") + print(f"模式: {'预览' if args.dry_run else '实际迁移'}") + print(f"将迁移: {len(result['moved'])} 个对话") + print(f"跳过(非多智能体): {result['skipped_count']} 个") + print(f"错误: {len(result['errors'])}") + + for item in result["moved"]: + print(f" → {item['id']} -> {item.get('target', item['file'])}") + + for err in result["errors"]: + print(f" ⚠ {err['file']}: {err['error']}") + + +if __name__ == "__main__": + main() diff --git a/server/app_legacy.py b/server/app_legacy.py index 61af79f..8e897ff 100644 --- a/server/app_legacy.py +++ b/server/app_legacy.py @@ -525,7 +525,7 @@ def generate_conversation_title_background(web_terminal: WebTerminal, conversati safe_title = title[:80] ok = False try: - ok = web_terminal.context_manager.conversation_manager.update_conversation_title(conversation_id, safe_title) + ok = web_terminal.context_manager._get_conversation_manager_for_id(conversation_id).update_conversation_title(conversation_id, safe_title) except Exception as exc: debug_log(f"[TitleGen] 保存标题失败: {exc}") _title_debug_log("title_save_exception", error=str(exc), conversation_id=conversation_id) @@ -1082,7 +1082,7 @@ def ensure_conversation_loaded(terminal: WebTerminal, conversation_id: Optional[ raise RuntimeError(load_result.get("message", "对话加载失败")) # 切换到对话记录的运行模式 try: - conv_data = terminal.context_manager.conversation_manager.load_conversation(conversation_id) or {} + conv_data = terminal.context_manager._get_conversation_manager_for_id(conversation_id).load_conversation(conversation_id) or {} meta = conv_data.get("metadata", {}) or {} run_mode_meta = meta.get("run_mode") if run_mode_meta: diff --git a/server/chat_flow_helpers.py b/server/chat_flow_helpers.py index c5309bf..b843bcb 100644 --- a/server/chat_flow_helpers.py +++ b/server/chat_flow_helpers.py @@ -132,7 +132,7 @@ def generate_conversation_title_background( safe_title = title[:80] ok = False try: - ok = web_terminal.context_manager.conversation_manager.update_conversation_title(conversation_id, safe_title) + ok = web_terminal.context_manager._get_conversation_manager_for_id(conversation_id).update_conversation_title(conversation_id, safe_title) except Exception as exc: debug_logger(f"[TitleGen] 保存标题失败: {exc}") _title_debug_log("title_save_exception", error=str(exc), conversation_id=conversation_id) diff --git a/server/chat_flow_task_main.py b/server/chat_flow_task_main.py index c8b105d..22df386 100644 --- a/server/chat_flow_task_main.py +++ b/server/chat_flow_task_main.py @@ -45,6 +45,7 @@ from modules.upload_security import UploadSecurityError from modules.user_manager import UserWorkspace from modules.usage_tracker import QUOTA_DEFAULTS from modules.sub_agent import TERMINAL_STATUSES +from modules.multi_agent.debug_logger import ma_debug from modules.versioning_manager import ConversationVersioningManager, VersioningError from modules.shallow_versioning import ShallowVersioningManager from core.web_terminal import WebTerminal @@ -569,6 +570,12 @@ async def _dispatch_completion_user_notice( for item in preceding_notices if str(item.get("message") or "").strip() ] + ma_debug( + "dispatch_completion_create_chat_task", + conversation_id=conversation_id, + user_message_preview=user_message[:300], + preceding_count=len(preceding_notices), + ) rec = task_manager.create_chat_task( username, workspace_id, @@ -580,6 +587,11 @@ async def _dispatch_completion_user_notice( run_mode=session_data.get("run_mode"), session_data=session_data, ) + ma_debug( + "dispatch_completion_task_created", + conversation_id=conversation_id, + task_id=getattr(rec, "task_id", None), + ) payload = { 'message': user_message, 'conversation_id': conversation_id, @@ -701,6 +713,7 @@ def _collect_pending_completion_notices(*, web_terminal, conversation_id: str) - and t.get("status") in TERMINAL_STATUSES # completed/failed/timeout(不含 terminated) and not t.get("notified") and t.get("task_id") not in web_terminal._announced_sub_agent_tasks + and not t.get("multi_agent_mode") # 多智能体任务走独立注入路径,不在这里发传统通知 ] candidates.sort(key=lambda t: t.get("updated_at") or t.get("created_at") or 0) for task_info in candidates: @@ -785,6 +798,12 @@ def _collect_pending_completion_notices(*, web_terminal, conversation_id: str) - state = sub_manager.get_multi_agent_state(conversation_id) if state: ma_messages = state.drain_master_messages() + ma_debug( + "collect_notices_drain_ma_messages", + conversation_id=conversation_id, + count=len(ma_messages), + previews=[str(m)[:200] for m in ma_messages], + ) for msg_text in ma_messages: notices.append({ "kind": "multi_agent", @@ -878,6 +897,7 @@ async def poll_completion_notifications(*, web_terminal, workspace, conversation pass loop_count = 0 + ma_debug("poll_completion_notifications_start", conversation_id=conversation_id) while (time.time() - start_wait) < max_wait_time: loop_count += 1 # 检查停止标志 @@ -885,22 +905,27 @@ async def poll_completion_notifications(*, web_terminal, workspace, conversation if client_stop_info: stop_requested = client_stop_info.get('stop', False) if isinstance(client_stop_info, dict) else client_stop_info if stop_requested: + ma_debug("poll_completion_notifications_stop_requested", conversation_id=conversation_id) break # 若主对话仍在工具循环中,暂不消费完成事件,避免抢占 system 消息插入 if getattr(web_terminal, "_tool_loop_active", False): + ma_debug("poll_completion_notifications_wait_tool_loop", conversation_id=conversation_id) await asyncio.sleep(1) continue # 多智能体模式:主对话任务仍在运行时,由主循环自己消费 pending 消息 if getattr(web_terminal, "_multi_agent_main_task_active", False): + ma_debug("poll_completion_notifications_wait_main_task", conversation_id=conversation_id) await asyncio.sleep(1) continue + ma_debug("poll_completion_notifications_collect", conversation_id=conversation_id, loop_count=loop_count) notices = _collect_pending_completion_notices( web_terminal=web_terminal, conversation_id=conversation_id, ) + ma_debug("poll_completion_notifications_collected", conversation_id=conversation_id, notice_count=len(notices)) if notices: # 取出后剩余是否还有未完成/未通知的后台工作(决定前端是否保持等待态) @@ -974,6 +999,18 @@ async def handle_task_with_sender( # 多智能体模式:标记主对话任务正在运行,供后台通知池判断是否可以安全消费 if getattr(web_terminal, "multi_agent_mode", False): web_terminal._multi_agent_main_task_active = True + manager = getattr(web_terminal, "sub_agent_manager", None) + pending_count = 0 + if manager: + state = manager.get_multi_agent_state(conversation_id) + if state: + pending_count = len(state.pending_master_messages) + ma_debug( + "handle_task_with_sender_start", + conversation_id=conversation_id, + message_preview=str(message)[:300] if message else None, + pending_master_messages_count=pending_count, + ) videos = videos or [] raw_sender = sender @@ -1695,6 +1732,11 @@ async def handle_task_with_sender( sender=sender, debug_log=debug_log, ) + ma_debug( + "no_tool_call_turn_process_ma_messages", + conversation_id=conversation_id, + injected_count=injected_count, + ) if injected_count: debug_log(f"[MultiAgent] no-tool-call turn 注入 {injected_count} 条子智能体消息,继续迭代") is_first_iteration = False @@ -1907,6 +1949,13 @@ async def handle_task_with_sender( # 只 spawn 一个轮询器(无论是否同时存在子智能体与后台命令),从根本上消除 # 两个轮询器同时 create_chat_task 撞「单工作区互斥」的问题。 if has_running_sub_agents or has_running_background_commands or has_running_multi_agent: + ma_debug( + "completion_poll_start", + conversation_id=conversation_id, + has_running_sub_agents=has_running_sub_agents, + has_running_background_commands=has_running_background_commands, + has_running_multi_agent=has_running_multi_agent, + ) def run_completion_poll(): import asyncio loop = asyncio.new_event_loop() @@ -1923,6 +1972,12 @@ async def handle_task_with_sender( loop.close() socketio.start_background_task(run_completion_poll) + else: + ma_debug( + "completion_poll_skip", + conversation_id=conversation_id, + reason="no_running_background_or_multi_agent", + ) has_running_completion_jobs = has_running_sub_agents or has_running_background_commands diff --git a/server/chat_flow_task_support.py b/server/chat_flow_task_support.py index b66ffca..a13ecdc 100644 --- a/server/chat_flow_task_support.py +++ b/server/chat_flow_task_support.py @@ -179,6 +179,8 @@ async def process_sub_agent_updates(*, messages: List[Dict], inline: bool = Fals continue if task.get("notified"): continue + if task.get("multi_agent_mode"): + continue task_conv_id = task.get("conversation_id") current_conv_id = getattr(getattr(web_terminal, "context_manager", None), "current_conversation_id", None) if task_conv_id and current_conv_id and task_conv_id != current_conv_id: @@ -315,6 +317,17 @@ async def process_background_command_updates(*, messages: List[Dict], inline: bo +def _auto_message_type_for_multi_agent_subtype(subtype: Optional[str]) -> str: + """把多智能体消息 subtype 映射为统一的 auto_message_type。""" + if subtype == "progress_output": + return "multi_agent_progress_output" + if subtype == "completion_report": + return "multi_agent_completion_report" + if subtype == "ask_master": + return "multi_agent_ask_master" + return "multi_agent_output" + + def inject_multi_agent_master_message( *, web_terminal, @@ -338,14 +351,26 @@ def inject_multi_agent_master_message( after_tool_call_id=after_tool_call_id, ) + # 解析标准格式以获取 subtype;解析失败时回退到通用类型 + try: + from modules.multi_agent.state import parse_multi_agent_message + parsed = parse_multi_agent_message(raw) + except Exception: + parsed = None + subtype = parsed.get("subtype") if parsed else None + auto_message_type = _auto_message_type_for_multi_agent_subtype(subtype) + metadata = { "runtime_injected": True, "source": "sub_agent", "message_source": "sub_agent", "inline": inline, "is_auto_generated": True, - "auto_message_type": "multi_agent_output", - "visibility": "compact", + "auto_message_type": auto_message_type, + "multi_agent_message": True, + "multi_agent_display_name": parsed.get("display_name") if parsed else None, + "multi_agent_subtype": subtype, + "visibility": "chat", "starts_work": False, } @@ -376,8 +401,12 @@ def inject_multi_agent_master_message( "inline": inline, "source": "sub_agent", "message_source": "sub_agent", - "visibility": "compact", + "visibility": "chat", "starts_work": False, + "auto_message_type": auto_message_type, + "multi_agent_message": True, + "multi_agent_display_name": parsed.get("display_name") if parsed else None, + "multi_agent_subtype": subtype, "metadata": metadata, "runtime_injected": True, } diff --git a/server/context.py b/server/context.py index aa3c67f..61f698f 100644 --- a/server/context.py +++ b/server/context.py @@ -639,7 +639,7 @@ def ensure_conversation_loaded( ).get("project_path"), ) try: - conv_data = terminal.context_manager.conversation_manager.load_conversation(conversation_id) or {} + conv_data = terminal.context_manager._get_conversation_manager_for_id(conversation_id).load_conversation(conversation_id) or {} meta = conv_data.get("metadata", {}) or {} run_mode_meta = meta.get("run_mode") if run_mode_meta: diff --git a/server/conversation.py b/server/conversation.py index 7cdd84e..859e4f1 100644 --- a/server/conversation.py +++ b/server/conversation.py @@ -275,7 +275,7 @@ def _atomic_write_input_draft(path: Path, payload: Dict[str, Any]) -> None: def _get_conversation_versioning_meta(terminal: WebTerminal, conversation_id: str) -> Dict[str, Any]: normalized = _normalize_conv_id(conversation_id) - data = terminal.context_manager.conversation_manager.load_conversation(normalized) or {} + data = terminal.context_manager._get_conversation_manager_for_id(normalized).load_conversation(normalized) or {} meta = data.get("metadata") or {} versioning = meta.get("versioning") or {} if not isinstance(versioning, dict): @@ -316,7 +316,7 @@ def _ensure_conversation_versioning_enabled( manager = _get_conv_versioning_manager(workspace, normalized_id) normalized_tracking_mode = _normalize_versioning_tracking_mode(tracking_mode or default_mode) meta = manager.set_enabled(enabled=True, mode="overwrite", tracking_mode=normalized_tracking_mode) - conv_data = terminal.context_manager.conversation_manager.load_conversation(normalized_id) or {} + conv_data = terminal.context_manager._get_conversation_manager_for_id(normalized_id).load_conversation(normalized_id) or {} snapshot_payload = { "conversation_id": normalized_id, "title": conv_data.get("title"), @@ -372,7 +372,7 @@ def _update_conversation_versioning_meta( payload["versioning"]["last_commit"] = last_commit if last_input_seq is not None: payload["versioning"]["last_input_seq"] = int(last_input_seq) - return terminal.context_manager.conversation_manager.update_conversation_metadata(normalized, payload) + return terminal.context_manager._get_conversation_manager_for_id(normalized).update_conversation_metadata(normalized, payload) @conversation_bp.route('/api/input-draft', methods=['GET']) @@ -710,7 +710,7 @@ def get_conversation_info(terminal: WebTerminal, workspace: UserWorkspace, usern """获取特定对话信息""" try: # 通过ConversationManager直接获取对话数据 - conversation_data = terminal.context_manager.conversation_manager.load_conversation(conversation_id) + conversation_data = terminal.context_manager._get_conversation_manager_for_id(conversation_id).load_conversation(conversation_id) if conversation_data: # 提取关键信息,不返回完整消息内容(避免数据量过大) @@ -950,7 +950,7 @@ def get_conversation_messages(conversation_id, terminal: WebTerminal, workspace: """获取对话的消息历史(可选功能,用于调试或详细查看)""" try: # 获取完整对话数据 - conversation_data = terminal.context_manager.conversation_manager.load_conversation(conversation_id) + conversation_data = terminal.context_manager._get_conversation_manager_for_id(conversation_id).load_conversation(conversation_id) if conversation_data: messages = conversation_data.get("messages", []) @@ -1072,7 +1072,7 @@ def update_conversation_versioning(conversation_id, terminal: WebTerminal, works manager = _get_conv_versioning_manager(workspace, normalized_id) meta = manager.set_enabled(enabled=enabled, mode=mode, tracking_mode=tracking_mode) if enabled: - conv_data = terminal.context_manager.conversation_manager.load_conversation(normalized_id) or {} + conv_data = terminal.context_manager._get_conversation_manager_for_id(normalized_id).load_conversation(normalized_id) or {} snapshot_payload = { "conversation_id": normalized_id, "title": conv_data.get("title"), @@ -1615,7 +1615,7 @@ def compress_conversation(conversation_id, terminal: WebTerminal, workspace: Use def get_conversation_compression_status(conversation_id, terminal: WebTerminal, workspace: UserWorkspace, username: str): try: normalized_id = conversation_id if conversation_id.startswith('conv_') else f"conv_{conversation_id}" - data = terminal.context_manager.conversation_manager.load_conversation(normalized_id) or {} + data = terminal.context_manager._get_conversation_manager_for_id(normalized_id).load_conversation(normalized_id) or {} meta = data.get("metadata", {}) or {} return jsonify({ "success": True, @@ -1639,9 +1639,9 @@ def get_conversation_compression_status(conversation_id, terminal: WebTerminal, def cancel_conversation_compression(conversation_id, terminal: WebTerminal, workspace: UserWorkspace, username: str): try: normalized_id = conversation_id if conversation_id.startswith('conv_') else f"conv_{conversation_id}" - ok = terminal.context_manager.conversation_manager.update_conversation_metadata( - normalized_id, - { + ok = terminal.context_manager._get_conversation_manager_for_id( + normalized_id).update_conversation_metadata( + normalized_id, { "compression_in_progress": False, "compression_mode": None, "compression_stage": None, @@ -2007,7 +2007,7 @@ def review_conversation_preview(conversation_id, terminal: WebTerminal, workspac "message": "无法引用当前对话" }), 400 - conversation_data = terminal.context_manager.conversation_manager.load_conversation(conversation_id) + conversation_data = terminal.context_manager._get_conversation_manager_for_id(conversation_id).load_conversation(conversation_id) if not conversation_data: return jsonify({ "success": False, @@ -2050,7 +2050,7 @@ def review_conversation(conversation_id, terminal: WebTerminal, workspace: UserW "message": "无法引用当前对话" }), 400 - conversation_data = terminal.context_manager.conversation_manager.load_conversation(conversation_id) + conversation_data = terminal.context_manager._get_conversation_manager_for_id(conversation_id).load_conversation(conversation_id) if not conversation_data: return jsonify({ "success": False, @@ -2132,7 +2132,7 @@ def get_current_conversation(terminal: WebTerminal, workspace: UserWorkspace, us # 如果是真实的对话ID,查找对话数据 try: - conversation_data = terminal.context_manager.conversation_manager.load_conversation(current_id) + conversation_data = terminal.context_manager._get_conversation_manager_for_id(current_id).load_conversation(current_id) if conversation_data: metadata = conversation_data.get("metadata", {}) or {} return jsonify({ diff --git a/server/multi_agent.py b/server/multi_agent.py index b7ad90a..ee62547 100644 --- a/server/multi_agent.py +++ b/server/multi_agent.py @@ -44,11 +44,14 @@ def rebuild_conversation_index_api(): terminal, _ = get_user_resources(username) if not terminal: return jsonify({"success": False, "error": "工作区未就绪"}), 503 - cm = getattr(getattr(terminal, "context_manager", None), "conversation_manager", None) - if not cm: - return jsonify({"success": False, "error": "对话管理器未初始化"}), 503 - rebuilt = cm._rebuild_index_from_files() - cm._save_index(rebuilt) + ctx_manager = getattr(terminal, "context_manager", None) + if not ctx_manager: + return jsonify({"success": False, "error": "上下文管理器未初始化"}), 503 + ma_manager = getattr(ctx_manager, "multi_agent_conversation_manager", None) + if not ma_manager: + return jsonify({"success": False, "error": "多智能体对话管理器未初始化"}), 503 + rebuilt = ma_manager._rebuild_index_from_files() + ma_manager._save_index(rebuilt) return jsonify({"success": True, "index_size": len(rebuilt)}) except Exception as exc: return jsonify({"success": False, "error": str(exc)}), 500 @@ -150,7 +153,10 @@ def create_multi_agent_conversation(): except Exception: prefs = {} - cm = getattr(getattr(terminal, "context_manager", None), "conversation_manager", None) + ctx_manager = getattr(terminal, "context_manager", None) + if not ctx_manager: + return jsonify({"success": False, "error": "上下文管理器未初始化"}), 500 + cm = getattr(ctx_manager, "multi_agent_conversation_manager", None) or getattr(ctx_manager, "conversation_manager", None) if not cm: return jsonify({"success": False, "error": "对话管理器未初始化"}), 500 @@ -163,7 +169,7 @@ def create_multi_agent_conversation(): if default_permission_mode not in ("readonly", "approval", "auto_approval", "unrestricted"): default_permission_mode = None - previous_cm_current = getattr(cm, "current_conversation_id", None) + previous_cm_current = getattr(ctx_manager, "current_conversation_id", None) conversation_id = cm.create_conversation( project_path=str(workspace.project_path), @@ -177,8 +183,9 @@ def create_multi_agent_conversation(): "multi_agent_mode": True, }, ) + # 恢复 context_manager 的当前对话(不要影响普通对话的 current_conversation_id) try: - cm.current_conversation_id = previous_cm_current + ctx_manager.current_conversation_id = previous_cm_current except Exception: pass diff --git a/server/socket_handlers.py b/server/socket_handlers.py index 69cad18..9811190 100644 --- a/server/socket_handlers.py +++ b/server/socket_handlers.py @@ -288,7 +288,7 @@ def handle_message(data): emit('error', {'message': str(exc)}) return try: - conv_data = terminal.context_manager.conversation_manager.load_conversation(conversation_id) or {} + conv_data = terminal.context_manager._get_conversation_manager_for_id(conversation_id).load_conversation(conversation_id) or {} except Exception: conv_data = {} title = conv_data.get('title', '新对话') diff --git a/server/status/base.py b/server/status/base.py index d8aa243..c0c0a3f 100644 --- a/server/status/base.py +++ b/server/status/base.py @@ -122,7 +122,7 @@ def get_status(terminal, workspace, username): current_conv = terminal.context_manager.current_conversation_id status.setdefault('conversation', {})['current_id'] = current_conv if current_conv and not current_conv.startswith('temp_'): - current_conv_data = terminal.context_manager.conversation_manager.load_conversation(current_conv) + current_conv_data = terminal.context_manager._get_conversation_manager_for_id(current_conv).load_conversation(current_conv) if current_conv_data: status['conversation']['title'] = current_conv_data.get('title', '未知对话') status['conversation']['created_at'] = current_conv_data.get('created_at') diff --git a/static/src/components/chat/ChatArea.vue b/static/src/components/chat/ChatArea.vue index b21e8aa..3cc1a72 100644 --- a/static/src/components/chat/ChatArea.vue +++ b/static/src/components/chat/ChatArea.vue @@ -13,17 +13,42 @@ 'message-block--compact-user': getMessageVisibility(msg) === 'compact', 'message-block--sub-agent-notice': isSubAgentNoticeOnlyMessage(msg), 'message-block--before-sub-agent-notice': isFollowedBySubAgentNotice(index), + 'message-block--multi-agent': isMultiAgentMessage(msg), 'message-block--last': index === (filteredMessages || []).length - 1 }" > -
+