fix(multi-agent): 修复消息渲染、分类、存储与滚动抖动
- 子智能体消息渲染为左侧对话气泡 - 多智能体通知细分为进度/完成/提问三类 - 修复多智能体会话存储到 mutiagents 目录并迁移数据 - 修复 token-statistics API 支持多智能体会话 - 修复 ma_debug 未定义报错 - 移除 .message-block content-visibility 修复滚动抖动
This commit is contained in:
parent
7f2ad9144d
commit
692748d567
@ -374,7 +374,8 @@ class MainTerminal(MainTerminalCommandMixin, MainTerminalContextMixin, MainTermi
|
|||||||
if not cm or not conv_id:
|
if not cm or not conv_id:
|
||||||
return
|
return
|
||||||
try:
|
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):
|
if isinstance(getattr(cm, "conversation_metadata", None), dict):
|
||||||
cm.conversation_metadata.update(updates)
|
cm.conversation_metadata.update(updates)
|
||||||
except Exception:
|
except Exception:
|
||||||
|
|||||||
@ -218,8 +218,7 @@ class MainTerminalToolsPolicyMixin:
|
|||||||
conv_id = conversation_id or getattr(getattr(self, "context_manager", None), "current_conversation_id", None)
|
conv_id = conversation_id or getattr(getattr(self, "context_manager", None), "current_conversation_id", None)
|
||||||
if conv_id and getattr(self, "context_manager", None):
|
if conv_id and getattr(self, "context_manager", None):
|
||||||
try:
|
try:
|
||||||
self.context_manager.conversation_manager.update_conversation_metadata(
|
self.context_manager._get_conversation_manager_for_id(conv_id).update_conversation_metadata(conv_id,
|
||||||
conv_id,
|
|
||||||
{"permission_mode": normalized},
|
{"permission_mode": normalized},
|
||||||
)
|
)
|
||||||
if self.context_manager.current_conversation_id == conv_id:
|
if self.context_manager.current_conversation_id == conv_id:
|
||||||
|
|||||||
@ -206,7 +206,8 @@ class WebTerminal(MainTerminal):
|
|||||||
|
|
||||||
# 把默认模型同步到新对话的 metadata
|
# 把默认模型同步到新对话的 metadata
|
||||||
try:
|
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,
|
conversation_id=conversation_id,
|
||||||
messages=self.context_manager.conversation_history,
|
messages=self.context_manager.conversation_history,
|
||||||
project_path=str(self.project_path),
|
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})
|
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)
|
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})
|
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 = {
|
snapshot_payload = {
|
||||||
"conversation_id": normalized_id,
|
"conversation_id": normalized_id,
|
||||||
"title": conv_data.get("title"),
|
"title": conv_data.get("title"),
|
||||||
@ -293,7 +295,7 @@ class WebTerminal(MainTerminal):
|
|||||||
init_row = init_result.get("row") or {}
|
init_row = init_result.get("row") or {}
|
||||||
if init_row.get("tree_hash"):
|
if init_row.get("tree_hash"):
|
||||||
meta["last_tree_hash"] = 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,
|
normalized_id,
|
||||||
{
|
{
|
||||||
"versioning": {
|
"versioning": {
|
||||||
@ -324,7 +326,8 @@ class WebTerminal(MainTerminal):
|
|||||||
if success:
|
if success:
|
||||||
# 根据对话元数据同步思考模式
|
# 根据对话元数据同步思考模式
|
||||||
try:
|
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 {}
|
meta = conv_data.get("metadata", {}) or {}
|
||||||
mode = bool(meta.get("thinking_mode", self.thinking_mode))
|
mode = bool(meta.get("thinking_mode", self.thinking_mode))
|
||||||
self.thinking_mode = mode
|
self.thinking_mode = mode
|
||||||
@ -371,7 +374,8 @@ class WebTerminal(MainTerminal):
|
|||||||
self.current_session_id += 1
|
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:
|
if not conversation_data:
|
||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
|
|||||||
@ -19,6 +19,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
|
import re
|
||||||
import uuid
|
import uuid
|
||||||
from asyncio import AbstractEventLoop
|
from asyncio import AbstractEventLoop
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
@ -51,6 +52,7 @@ def format_multi_agent_message(
|
|||||||
target: Optional[str] = None,
|
target: Optional[str] = None,
|
||||||
extra_attrs: Optional[Dict[str, str]] = None,
|
extra_attrs: Optional[Dict[str, str]] = None,
|
||||||
msg_type_text: Optional[str] = None,
|
msg_type_text: Optional[str] = None,
|
||||||
|
subtype: Optional[str] = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""按统一格式构造 user 消息字符串。
|
"""按统一格式构造 user 消息字符串。
|
||||||
|
|
||||||
@ -62,6 +64,7 @@ def format_multi_agent_message(
|
|||||||
target: 接收方显示名(用于子→子 提问时标明对谁提问)
|
target: 接收方显示名(用于子→子 提问时标明对谁提问)
|
||||||
extra_attrs: 额外标签属性(如 question_id="ask_xxx")
|
extra_attrs: 额外标签属性(如 question_id="ask_xxx")
|
||||||
msg_type_text: 覆盖默认的中文消息类型文案(如"任务结束汇报")
|
msg_type_text: 覆盖默认的中文消息类型文案(如"任务结束汇报")
|
||||||
|
subtype: 渲染/分类使用的子类型(如 progress_output / completion_report / ask_master)
|
||||||
"""
|
"""
|
||||||
if not msg_id:
|
if not msg_id:
|
||||||
msg_id = f"msg_{uuid.uuid4().hex[:10]}"
|
msg_id = f"msg_{uuid.uuid4().hex[:10]}"
|
||||||
@ -80,6 +83,8 @@ def format_multi_agent_message(
|
|||||||
attrs = ""
|
attrs = ""
|
||||||
if target:
|
if target:
|
||||||
attrs += f' target="{target}"'
|
attrs += f' target="{target}"'
|
||||||
|
if subtype:
|
||||||
|
attrs += f' subtype="{subtype}"'
|
||||||
if extra_attrs:
|
if extra_attrs:
|
||||||
for k, v in extra_attrs.items():
|
for k, v in extra_attrs.items():
|
||||||
attrs += f' {k}="{v}"'
|
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:
|
def build_sub_agent_output_text(display_name: str, content: str, msg_id: Optional[str] = None, *, is_final: bool = False) -> str:
|
||||||
"""子智能体输出(进度或完成)插入到主对话的 user 消息文本。"""
|
"""子智能体输出(进度或完成)插入到主对话的 user 消息文本。"""
|
||||||
return format_multi_agent_message(
|
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,
|
content=content,
|
||||||
msg_id=msg_id,
|
msg_id=msg_id,
|
||||||
msg_type_text="任务结束汇报" if is_final else "任务进度输出",
|
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,
|
msg_type=TYPE_ASK,
|
||||||
content=question,
|
content=question,
|
||||||
msg_id=question_id,
|
msg_id=question_id,
|
||||||
|
subtype=SUBTYPE_ASK_MASTER,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -153,9 +167,45 @@ def build_sub_agent_ask_other_text(
|
|||||||
content=question,
|
content=question,
|
||||||
msg_id=question_id,
|
msg_id=question_id,
|
||||||
target=target_display,
|
target=target_display,
|
||||||
|
subtype=SUBTYPE_ASK_OTHER,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
_MULTI_AGENT_MESSAGE_RE = re.compile(
|
||||||
|
r"^来自\s+(?P<display_name>.+?)\s+的(?P<type_text>.+?)\n"
|
||||||
|
r"id:\s*(?P<msg_id>\S+)\n\n"
|
||||||
|
r"<(?P=display_name)>\n"
|
||||||
|
r"<(?P<tag>\w+)(?P<attrs>[^>]*)>\n"
|
||||||
|
r"(?P<content>.*?)\n"
|
||||||
|
r"</(?P=tag)>\n"
|
||||||
|
r"</(?P=display_name)>$",
|
||||||
|
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:
|
def build_master_message_to_sub_agent(message: str, msg_id: Optional[str] = None) -> str:
|
||||||
"""主智能体 send_message_to_sub_agent 时插入子对话的 user 消息文本。"""
|
"""主智能体 send_message_to_sub_agent 时插入子对话的 user 消息文本。"""
|
||||||
return format_multi_agent_message(
|
return format_multi_agent_message(
|
||||||
|
|||||||
143
scripts/migrate_multi_agent_conversations.py
Normal file
143
scripts/migrate_multi_agent_conversations.py
Normal file
@ -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()
|
||||||
@ -525,7 +525,7 @@ def generate_conversation_title_background(web_terminal: WebTerminal, conversati
|
|||||||
safe_title = title[:80]
|
safe_title = title[:80]
|
||||||
ok = False
|
ok = False
|
||||||
try:
|
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:
|
except Exception as exc:
|
||||||
debug_log(f"[TitleGen] 保存标题失败: {exc}")
|
debug_log(f"[TitleGen] 保存标题失败: {exc}")
|
||||||
_title_debug_log("title_save_exception", error=str(exc), conversation_id=conversation_id)
|
_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", "对话加载失败"))
|
raise RuntimeError(load_result.get("message", "对话加载失败"))
|
||||||
# 切换到对话记录的运行模式
|
# 切换到对话记录的运行模式
|
||||||
try:
|
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 {}
|
meta = conv_data.get("metadata", {}) or {}
|
||||||
run_mode_meta = meta.get("run_mode")
|
run_mode_meta = meta.get("run_mode")
|
||||||
if run_mode_meta:
|
if run_mode_meta:
|
||||||
|
|||||||
@ -132,7 +132,7 @@ def generate_conversation_title_background(
|
|||||||
safe_title = title[:80]
|
safe_title = title[:80]
|
||||||
ok = False
|
ok = False
|
||||||
try:
|
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:
|
except Exception as exc:
|
||||||
debug_logger(f"[TitleGen] 保存标题失败: {exc}")
|
debug_logger(f"[TitleGen] 保存标题失败: {exc}")
|
||||||
_title_debug_log("title_save_exception", error=str(exc), conversation_id=conversation_id)
|
_title_debug_log("title_save_exception", error=str(exc), conversation_id=conversation_id)
|
||||||
|
|||||||
@ -45,6 +45,7 @@ from modules.upload_security import UploadSecurityError
|
|||||||
from modules.user_manager import UserWorkspace
|
from modules.user_manager import UserWorkspace
|
||||||
from modules.usage_tracker import QUOTA_DEFAULTS
|
from modules.usage_tracker import QUOTA_DEFAULTS
|
||||||
from modules.sub_agent import TERMINAL_STATUSES
|
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.versioning_manager import ConversationVersioningManager, VersioningError
|
||||||
from modules.shallow_versioning import ShallowVersioningManager
|
from modules.shallow_versioning import ShallowVersioningManager
|
||||||
from core.web_terminal import WebTerminal
|
from core.web_terminal import WebTerminal
|
||||||
@ -569,6 +570,12 @@ async def _dispatch_completion_user_notice(
|
|||||||
for item in preceding_notices
|
for item in preceding_notices
|
||||||
if str(item.get("message") or "").strip()
|
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(
|
rec = task_manager.create_chat_task(
|
||||||
username,
|
username,
|
||||||
workspace_id,
|
workspace_id,
|
||||||
@ -580,6 +587,11 @@ async def _dispatch_completion_user_notice(
|
|||||||
run_mode=session_data.get("run_mode"),
|
run_mode=session_data.get("run_mode"),
|
||||||
session_data=session_data,
|
session_data=session_data,
|
||||||
)
|
)
|
||||||
|
ma_debug(
|
||||||
|
"dispatch_completion_task_created",
|
||||||
|
conversation_id=conversation_id,
|
||||||
|
task_id=getattr(rec, "task_id", None),
|
||||||
|
)
|
||||||
payload = {
|
payload = {
|
||||||
'message': user_message,
|
'message': user_message,
|
||||||
'conversation_id': conversation_id,
|
'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 t.get("status") in TERMINAL_STATUSES # completed/failed/timeout(不含 terminated)
|
||||||
and not t.get("notified")
|
and not t.get("notified")
|
||||||
and t.get("task_id") not in web_terminal._announced_sub_agent_tasks
|
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)
|
candidates.sort(key=lambda t: t.get("updated_at") or t.get("created_at") or 0)
|
||||||
for task_info in candidates:
|
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)
|
state = sub_manager.get_multi_agent_state(conversation_id)
|
||||||
if state:
|
if state:
|
||||||
ma_messages = state.drain_master_messages()
|
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:
|
for msg_text in ma_messages:
|
||||||
notices.append({
|
notices.append({
|
||||||
"kind": "multi_agent",
|
"kind": "multi_agent",
|
||||||
@ -878,6 +897,7 @@ async def poll_completion_notifications(*, web_terminal, workspace, conversation
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
loop_count = 0
|
loop_count = 0
|
||||||
|
ma_debug("poll_completion_notifications_start", conversation_id=conversation_id)
|
||||||
while (time.time() - start_wait) < max_wait_time:
|
while (time.time() - start_wait) < max_wait_time:
|
||||||
loop_count += 1
|
loop_count += 1
|
||||||
# 检查停止标志
|
# 检查停止标志
|
||||||
@ -885,22 +905,27 @@ async def poll_completion_notifications(*, web_terminal, workspace, conversation
|
|||||||
if client_stop_info:
|
if client_stop_info:
|
||||||
stop_requested = client_stop_info.get('stop', False) if isinstance(client_stop_info, dict) else client_stop_info
|
stop_requested = client_stop_info.get('stop', False) if isinstance(client_stop_info, dict) else client_stop_info
|
||||||
if stop_requested:
|
if stop_requested:
|
||||||
|
ma_debug("poll_completion_notifications_stop_requested", conversation_id=conversation_id)
|
||||||
break
|
break
|
||||||
|
|
||||||
# 若主对话仍在工具循环中,暂不消费完成事件,避免抢占 system 消息插入
|
# 若主对话仍在工具循环中,暂不消费完成事件,避免抢占 system 消息插入
|
||||||
if getattr(web_terminal, "_tool_loop_active", False):
|
if getattr(web_terminal, "_tool_loop_active", False):
|
||||||
|
ma_debug("poll_completion_notifications_wait_tool_loop", conversation_id=conversation_id)
|
||||||
await asyncio.sleep(1)
|
await asyncio.sleep(1)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# 多智能体模式:主对话任务仍在运行时,由主循环自己消费 pending 消息
|
# 多智能体模式:主对话任务仍在运行时,由主循环自己消费 pending 消息
|
||||||
if getattr(web_terminal, "_multi_agent_main_task_active", False):
|
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)
|
await asyncio.sleep(1)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
ma_debug("poll_completion_notifications_collect", conversation_id=conversation_id, loop_count=loop_count)
|
||||||
notices = _collect_pending_completion_notices(
|
notices = _collect_pending_completion_notices(
|
||||||
web_terminal=web_terminal,
|
web_terminal=web_terminal,
|
||||||
conversation_id=conversation_id,
|
conversation_id=conversation_id,
|
||||||
)
|
)
|
||||||
|
ma_debug("poll_completion_notifications_collected", conversation_id=conversation_id, notice_count=len(notices))
|
||||||
|
|
||||||
if notices:
|
if notices:
|
||||||
# 取出后剩余是否还有未完成/未通知的后台工作(决定前端是否保持等待态)
|
# 取出后剩余是否还有未完成/未通知的后台工作(决定前端是否保持等待态)
|
||||||
@ -974,6 +999,18 @@ async def handle_task_with_sender(
|
|||||||
# 多智能体模式:标记主对话任务正在运行,供后台通知池判断是否可以安全消费
|
# 多智能体模式:标记主对话任务正在运行,供后台通知池判断是否可以安全消费
|
||||||
if getattr(web_terminal, "multi_agent_mode", False):
|
if getattr(web_terminal, "multi_agent_mode", False):
|
||||||
web_terminal._multi_agent_main_task_active = True
|
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 []
|
videos = videos or []
|
||||||
raw_sender = sender
|
raw_sender = sender
|
||||||
|
|
||||||
@ -1695,6 +1732,11 @@ async def handle_task_with_sender(
|
|||||||
sender=sender,
|
sender=sender,
|
||||||
debug_log=debug_log,
|
debug_log=debug_log,
|
||||||
)
|
)
|
||||||
|
ma_debug(
|
||||||
|
"no_tool_call_turn_process_ma_messages",
|
||||||
|
conversation_id=conversation_id,
|
||||||
|
injected_count=injected_count,
|
||||||
|
)
|
||||||
if injected_count:
|
if injected_count:
|
||||||
debug_log(f"[MultiAgent] no-tool-call turn 注入 {injected_count} 条子智能体消息,继续迭代")
|
debug_log(f"[MultiAgent] no-tool-call turn 注入 {injected_count} 条子智能体消息,继续迭代")
|
||||||
is_first_iteration = False
|
is_first_iteration = False
|
||||||
@ -1907,6 +1949,13 @@ async def handle_task_with_sender(
|
|||||||
# 只 spawn 一个轮询器(无论是否同时存在子智能体与后台命令),从根本上消除
|
# 只 spawn 一个轮询器(无论是否同时存在子智能体与后台命令),从根本上消除
|
||||||
# 两个轮询器同时 create_chat_task 撞「单工作区互斥」的问题。
|
# 两个轮询器同时 create_chat_task 撞「单工作区互斥」的问题。
|
||||||
if has_running_sub_agents or has_running_background_commands or has_running_multi_agent:
|
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():
|
def run_completion_poll():
|
||||||
import asyncio
|
import asyncio
|
||||||
loop = asyncio.new_event_loop()
|
loop = asyncio.new_event_loop()
|
||||||
@ -1923,6 +1972,12 @@ async def handle_task_with_sender(
|
|||||||
loop.close()
|
loop.close()
|
||||||
|
|
||||||
socketio.start_background_task(run_completion_poll)
|
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
|
has_running_completion_jobs = has_running_sub_agents or has_running_background_commands
|
||||||
|
|
||||||
|
|||||||
@ -179,6 +179,8 @@ async def process_sub_agent_updates(*, messages: List[Dict], inline: bool = Fals
|
|||||||
continue
|
continue
|
||||||
if task.get("notified"):
|
if task.get("notified"):
|
||||||
continue
|
continue
|
||||||
|
if task.get("multi_agent_mode"):
|
||||||
|
continue
|
||||||
task_conv_id = task.get("conversation_id")
|
task_conv_id = task.get("conversation_id")
|
||||||
current_conv_id = getattr(getattr(web_terminal, "context_manager", None), "current_conversation_id", None)
|
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:
|
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(
|
def inject_multi_agent_master_message(
|
||||||
*,
|
*,
|
||||||
web_terminal,
|
web_terminal,
|
||||||
@ -338,14 +351,26 @@ def inject_multi_agent_master_message(
|
|||||||
after_tool_call_id=after_tool_call_id,
|
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 = {
|
metadata = {
|
||||||
"runtime_injected": True,
|
"runtime_injected": True,
|
||||||
"source": "sub_agent",
|
"source": "sub_agent",
|
||||||
"message_source": "sub_agent",
|
"message_source": "sub_agent",
|
||||||
"inline": inline,
|
"inline": inline,
|
||||||
"is_auto_generated": True,
|
"is_auto_generated": True,
|
||||||
"auto_message_type": "multi_agent_output",
|
"auto_message_type": auto_message_type,
|
||||||
"visibility": "compact",
|
"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,
|
"starts_work": False,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -376,8 +401,12 @@ def inject_multi_agent_master_message(
|
|||||||
"inline": inline,
|
"inline": inline,
|
||||||
"source": "sub_agent",
|
"source": "sub_agent",
|
||||||
"message_source": "sub_agent",
|
"message_source": "sub_agent",
|
||||||
"visibility": "compact",
|
"visibility": "chat",
|
||||||
"starts_work": False,
|
"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,
|
"metadata": metadata,
|
||||||
"runtime_injected": True,
|
"runtime_injected": True,
|
||||||
}
|
}
|
||||||
|
|||||||
@ -639,7 +639,7 @@ def ensure_conversation_loaded(
|
|||||||
).get("project_path"),
|
).get("project_path"),
|
||||||
)
|
)
|
||||||
try:
|
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 {}
|
meta = conv_data.get("metadata", {}) or {}
|
||||||
run_mode_meta = meta.get("run_mode")
|
run_mode_meta = meta.get("run_mode")
|
||||||
if run_mode_meta:
|
if run_mode_meta:
|
||||||
|
|||||||
@ -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]:
|
def _get_conversation_versioning_meta(terminal: WebTerminal, conversation_id: str) -> Dict[str, Any]:
|
||||||
normalized = _normalize_conv_id(conversation_id)
|
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 {}
|
meta = data.get("metadata") or {}
|
||||||
versioning = meta.get("versioning") or {}
|
versioning = meta.get("versioning") or {}
|
||||||
if not isinstance(versioning, dict):
|
if not isinstance(versioning, dict):
|
||||||
@ -316,7 +316,7 @@ def _ensure_conversation_versioning_enabled(
|
|||||||
manager = _get_conv_versioning_manager(workspace, normalized_id)
|
manager = _get_conv_versioning_manager(workspace, normalized_id)
|
||||||
normalized_tracking_mode = _normalize_versioning_tracking_mode(tracking_mode or default_mode)
|
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)
|
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 = {
|
snapshot_payload = {
|
||||||
"conversation_id": normalized_id,
|
"conversation_id": normalized_id,
|
||||||
"title": conv_data.get("title"),
|
"title": conv_data.get("title"),
|
||||||
@ -372,7 +372,7 @@ def _update_conversation_versioning_meta(
|
|||||||
payload["versioning"]["last_commit"] = last_commit
|
payload["versioning"]["last_commit"] = last_commit
|
||||||
if last_input_seq is not None:
|
if last_input_seq is not None:
|
||||||
payload["versioning"]["last_input_seq"] = int(last_input_seq)
|
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'])
|
@conversation_bp.route('/api/input-draft', methods=['GET'])
|
||||||
@ -710,7 +710,7 @@ def get_conversation_info(terminal: WebTerminal, workspace: UserWorkspace, usern
|
|||||||
"""获取特定对话信息"""
|
"""获取特定对话信息"""
|
||||||
try:
|
try:
|
||||||
# 通过ConversationManager直接获取对话数据
|
# 通过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:
|
if conversation_data:
|
||||||
# 提取关键信息,不返回完整消息内容(避免数据量过大)
|
# 提取关键信息,不返回完整消息内容(避免数据量过大)
|
||||||
@ -950,7 +950,7 @@ def get_conversation_messages(conversation_id, terminal: WebTerminal, workspace:
|
|||||||
"""获取对话的消息历史(可选功能,用于调试或详细查看)"""
|
"""获取对话的消息历史(可选功能,用于调试或详细查看)"""
|
||||||
try:
|
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:
|
if conversation_data:
|
||||||
messages = conversation_data.get("messages", [])
|
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)
|
manager = _get_conv_versioning_manager(workspace, normalized_id)
|
||||||
meta = manager.set_enabled(enabled=enabled, mode=mode, tracking_mode=tracking_mode)
|
meta = manager.set_enabled(enabled=enabled, mode=mode, tracking_mode=tracking_mode)
|
||||||
if enabled:
|
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 = {
|
snapshot_payload = {
|
||||||
"conversation_id": normalized_id,
|
"conversation_id": normalized_id,
|
||||||
"title": conv_data.get("title"),
|
"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):
|
def get_conversation_compression_status(conversation_id, terminal: WebTerminal, workspace: UserWorkspace, username: str):
|
||||||
try:
|
try:
|
||||||
normalized_id = conversation_id if conversation_id.startswith('conv_') else f"conv_{conversation_id}"
|
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 {}
|
meta = data.get("metadata", {}) or {}
|
||||||
return jsonify({
|
return jsonify({
|
||||||
"success": True,
|
"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):
|
def cancel_conversation_compression(conversation_id, terminal: WebTerminal, workspace: UserWorkspace, username: str):
|
||||||
try:
|
try:
|
||||||
normalized_id = conversation_id if conversation_id.startswith('conv_') else f"conv_{conversation_id}"
|
normalized_id = conversation_id if conversation_id.startswith('conv_') else f"conv_{conversation_id}"
|
||||||
ok = terminal.context_manager.conversation_manager.update_conversation_metadata(
|
ok = terminal.context_manager._get_conversation_manager_for_id(
|
||||||
normalized_id,
|
normalized_id).update_conversation_metadata(
|
||||||
{
|
normalized_id, {
|
||||||
"compression_in_progress": False,
|
"compression_in_progress": False,
|
||||||
"compression_mode": None,
|
"compression_mode": None,
|
||||||
"compression_stage": None,
|
"compression_stage": None,
|
||||||
@ -2007,7 +2007,7 @@ def review_conversation_preview(conversation_id, terminal: WebTerminal, workspac
|
|||||||
"message": "无法引用当前对话"
|
"message": "无法引用当前对话"
|
||||||
}), 400
|
}), 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:
|
if not conversation_data:
|
||||||
return jsonify({
|
return jsonify({
|
||||||
"success": False,
|
"success": False,
|
||||||
@ -2050,7 +2050,7 @@ def review_conversation(conversation_id, terminal: WebTerminal, workspace: UserW
|
|||||||
"message": "无法引用当前对话"
|
"message": "无法引用当前对话"
|
||||||
}), 400
|
}), 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:
|
if not conversation_data:
|
||||||
return jsonify({
|
return jsonify({
|
||||||
"success": False,
|
"success": False,
|
||||||
@ -2132,7 +2132,7 @@ def get_current_conversation(terminal: WebTerminal, workspace: UserWorkspace, us
|
|||||||
|
|
||||||
# 如果是真实的对话ID,查找对话数据
|
# 如果是真实的对话ID,查找对话数据
|
||||||
try:
|
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:
|
if conversation_data:
|
||||||
metadata = conversation_data.get("metadata", {}) or {}
|
metadata = conversation_data.get("metadata", {}) or {}
|
||||||
return jsonify({
|
return jsonify({
|
||||||
|
|||||||
@ -44,11 +44,14 @@ def rebuild_conversation_index_api():
|
|||||||
terminal, _ = get_user_resources(username)
|
terminal, _ = get_user_resources(username)
|
||||||
if not terminal:
|
if not terminal:
|
||||||
return jsonify({"success": False, "error": "工作区未就绪"}), 503
|
return jsonify({"success": False, "error": "工作区未就绪"}), 503
|
||||||
cm = getattr(getattr(terminal, "context_manager", None), "conversation_manager", None)
|
ctx_manager = getattr(terminal, "context_manager", None)
|
||||||
if not cm:
|
if not ctx_manager:
|
||||||
return jsonify({"success": False, "error": "对话管理器未初始化"}), 503
|
return jsonify({"success": False, "error": "上下文管理器未初始化"}), 503
|
||||||
rebuilt = cm._rebuild_index_from_files()
|
ma_manager = getattr(ctx_manager, "multi_agent_conversation_manager", None)
|
||||||
cm._save_index(rebuilt)
|
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)})
|
return jsonify({"success": True, "index_size": len(rebuilt)})
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
return jsonify({"success": False, "error": str(exc)}), 500
|
return jsonify({"success": False, "error": str(exc)}), 500
|
||||||
@ -150,7 +153,10 @@ def create_multi_agent_conversation():
|
|||||||
except Exception:
|
except Exception:
|
||||||
prefs = {}
|
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:
|
if not cm:
|
||||||
return jsonify({"success": False, "error": "对话管理器未初始化"}), 500
|
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"):
|
if default_permission_mode not in ("readonly", "approval", "auto_approval", "unrestricted"):
|
||||||
default_permission_mode = None
|
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(
|
conversation_id = cm.create_conversation(
|
||||||
project_path=str(workspace.project_path),
|
project_path=str(workspace.project_path),
|
||||||
@ -177,8 +183,9 @@ def create_multi_agent_conversation():
|
|||||||
"multi_agent_mode": True,
|
"multi_agent_mode": True,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
# 恢复 context_manager 的当前对话(不要影响普通对话的 current_conversation_id)
|
||||||
try:
|
try:
|
||||||
cm.current_conversation_id = previous_cm_current
|
ctx_manager.current_conversation_id = previous_cm_current
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|||||||
@ -288,7 +288,7 @@ def handle_message(data):
|
|||||||
emit('error', {'message': str(exc)})
|
emit('error', {'message': str(exc)})
|
||||||
return
|
return
|
||||||
try:
|
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:
|
except Exception:
|
||||||
conv_data = {}
|
conv_data = {}
|
||||||
title = conv_data.get('title', '新对话')
|
title = conv_data.get('title', '新对话')
|
||||||
|
|||||||
@ -122,7 +122,7 @@ def get_status(terminal, workspace, username):
|
|||||||
current_conv = terminal.context_manager.current_conversation_id
|
current_conv = terminal.context_manager.current_conversation_id
|
||||||
status.setdefault('conversation', {})['current_id'] = current_conv
|
status.setdefault('conversation', {})['current_id'] = current_conv
|
||||||
if current_conv and not current_conv.startswith('temp_'):
|
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:
|
if current_conv_data:
|
||||||
status['conversation']['title'] = current_conv_data.get('title', '未知对话')
|
status['conversation']['title'] = current_conv_data.get('title', '未知对话')
|
||||||
status['conversation']['created_at'] = current_conv_data.get('created_at')
|
status['conversation']['created_at'] = current_conv_data.get('created_at')
|
||||||
|
|||||||
@ -13,17 +13,42 @@
|
|||||||
'message-block--compact-user': getMessageVisibility(msg) === 'compact',
|
'message-block--compact-user': getMessageVisibility(msg) === 'compact',
|
||||||
'message-block--sub-agent-notice': isSubAgentNoticeOnlyMessage(msg),
|
'message-block--sub-agent-notice': isSubAgentNoticeOnlyMessage(msg),
|
||||||
'message-block--before-sub-agent-notice': isFollowedBySubAgentNotice(index),
|
'message-block--before-sub-agent-notice': isFollowedBySubAgentNotice(index),
|
||||||
|
'message-block--multi-agent': isMultiAgentMessage(msg),
|
||||||
'message-block--last': index === (filteredMessages || []).length - 1
|
'message-block--last': index === (filteredMessages || []).length - 1
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<div v-if="msg.role === 'user'" class="user-message" :class="{ 'user-message--compact': getMessageVisibility(msg) === 'compact', 'user-message--brief': isBriefCompactMessage(msg) }">
|
<div
|
||||||
|
v-if="msg.role === 'user'"
|
||||||
|
class="user-message"
|
||||||
|
:class="{
|
||||||
|
'user-message--multi-agent': isMultiAgentMessage(msg),
|
||||||
|
'user-message--compact': getMessageVisibility(msg) === 'compact',
|
||||||
|
'user-message--brief': isBriefCompactMessage(msg)
|
||||||
|
}"
|
||||||
|
>
|
||||||
<div v-if="isBriefCompactMessage(msg)" class="compact-brief-line" role="separator">
|
<div v-if="isBriefCompactMessage(msg)" class="compact-brief-line" role="separator">
|
||||||
<span class="compact-brief-text">{{ compactBriefLabel(msg) }}</span>
|
<span class="compact-brief-text">{{ compactBriefLabel(msg) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<div class="message-header icon-label">
|
<div
|
||||||
<span class="icon icon-sm" :style="iconStyleSafe(userHeaderIconKey(msg))" aria-hidden="true"></span>
|
class="message-header icon-label"
|
||||||
|
:class="{
|
||||||
|
'message-header--multi-agent': isMultiAgentMessage(msg),
|
||||||
|
'message-header--hidden':
|
||||||
|
isMultiAgentMessage(msg) && isContinuousMultiAgentMessage(msg, index)
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<template v-if="isMultiAgentMessage(msg)">
|
||||||
|
<span>{{ multiAgentHeaderLabel(msg) }}</span>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<span
|
||||||
|
class="icon icon-sm"
|
||||||
|
:style="iconStyleSafe(userHeaderIconKey(msg))"
|
||||||
|
aria-hidden="true"
|
||||||
|
></span>
|
||||||
<span>{{ userHeaderLabel(msg) }}</span>
|
<span>{{ userHeaderLabel(msg) }}</span>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="message-text user-bubble-text"
|
class="message-text user-bubble-text"
|
||||||
@ -38,8 +63,14 @@
|
|||||||
class="bubble-text"
|
class="bubble-text"
|
||||||
:class="{ 'is-expanded': isUserBubbleExpanded(msg, index) }"
|
:class="{ 'is-expanded': isUserBubbleExpanded(msg, index) }"
|
||||||
:ref="(el) => registerUserBubbleRef(msg, index, el)"
|
:ref="(el) => registerUserBubbleRef(msg, index, el)"
|
||||||
v-html="renderUserMessageContent(msg.content)"
|
>
|
||||||
></div>
|
<template v-if="isMultiAgentMessage(msg)">
|
||||||
|
{{ multiAgentBubbleContent(msg) }}
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<span v-html="renderUserMessageContent(msg.content)"></span>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
<div v-if="msg.images && msg.images.length" class="image-inline-row">
|
<div v-if="msg.images && msg.images.length" class="image-inline-row">
|
||||||
<div
|
<div
|
||||||
class="image-thumbnail-wrapper"
|
class="image-thumbnail-wrapper"
|
||||||
@ -79,7 +110,7 @@
|
|||||||
></button>
|
></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="user-bubble-actions">
|
<div v-if="!isMultiAgentMessage(msg)" class="user-bubble-actions">
|
||||||
<button
|
<button
|
||||||
class="user-bubble-action-btn copy"
|
class="user-bubble-action-btn copy"
|
||||||
:class="{ copied: isUserBubbleCopied(msg, index) }"
|
:class="{ copied: isUserBubbleCopied(msg, index) }"
|
||||||
@ -905,6 +936,69 @@ const isSystemAutoUserMessage = (message: any) => {
|
|||||||
meta.background_command_notice
|
meta.background_command_notice
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ---------- 多智能体消息渲染 ----------
|
||||||
|
|
||||||
|
interface MultiAgentMessageInfo {
|
||||||
|
displayName: string;
|
||||||
|
subtype: string;
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MULTI_AGENT_MESSAGE_RE =
|
||||||
|
/^来自\s+(.+?)\s+的(.+?)\nid:\s*(\S+)\n\n<(.+?)>\n<(\w+)([^>]*)>\n([\s\S]*?)\n<\/\5>\n<\/\4>$/;
|
||||||
|
|
||||||
|
function parseMultiAgentMessage(content: string): MultiAgentMessageInfo | null {
|
||||||
|
const text = String(content || '');
|
||||||
|
const m = text.match(MULTI_AGENT_MESSAGE_RE);
|
||||||
|
if (!m) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const attrs = m[6] || '';
|
||||||
|
const subtypeMatch = attrs.match(/subtype="([^"]+)"/);
|
||||||
|
return {
|
||||||
|
displayName: m[1].trim(),
|
||||||
|
subtype: subtypeMatch ? subtypeMatch[1] : '',
|
||||||
|
content: m[7]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function isMultiAgentMessage(message: any): boolean {
|
||||||
|
if (!message || message.role !== 'user') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const meta = message.metadata || {};
|
||||||
|
const autoType = String(meta.auto_message_type || message.auto_message_type || '');
|
||||||
|
return autoType.startsWith('multi_agent_');
|
||||||
|
}
|
||||||
|
|
||||||
|
function multiAgentHeaderLabel(message: any): string {
|
||||||
|
const displayName =
|
||||||
|
message?.metadata?.multi_agent_display_name || message?.multi_agent_display_name;
|
||||||
|
if (displayName) {
|
||||||
|
return displayName;
|
||||||
|
}
|
||||||
|
const parsed = parseMultiAgentMessage(message?.content || '');
|
||||||
|
return parsed?.displayName || '子智能体';
|
||||||
|
}
|
||||||
|
|
||||||
|
function isContinuousMultiAgentMessage(message: any, index: number): boolean {
|
||||||
|
if (!isMultiAgentMessage(message)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const messages = getFilteredMessagesSafe();
|
||||||
|
const prev = messages[index - 1];
|
||||||
|
if (!prev || !isMultiAgentMessage(prev)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return multiAgentHeaderLabel(message) === multiAgentHeaderLabel(prev);
|
||||||
|
}
|
||||||
|
|
||||||
|
function multiAgentBubbleContent(message: any): string {
|
||||||
|
const parsed = parseMultiAgentMessage(message?.content || '');
|
||||||
|
return parsed?.content || message?.content || '';
|
||||||
|
}
|
||||||
|
|
||||||
const escapeUserHtml = (value: string): string =>
|
const escapeUserHtml = (value: string): string =>
|
||||||
String(value || '')
|
String(value || '')
|
||||||
.replace(/&/g, '&')
|
.replace(/&/g, '&')
|
||||||
@ -1176,6 +1270,7 @@ function attachBounceListener() {
|
|||||||
scrollListener = (event: Event) => {
|
scrollListener = (event: Event) => {
|
||||||
const target = event.target as HTMLElement | null;
|
const target = event.target as HTMLElement | null;
|
||||||
if (!target) return;
|
if (!target) return;
|
||||||
|
const trusted = (event as any).isTrusted === true;
|
||||||
const top = target.scrollTop;
|
const top = target.scrollTop;
|
||||||
const delta = top - lastObservedTop;
|
const delta = top - lastObservedTop;
|
||||||
const height = target.scrollHeight || 0;
|
const height = target.scrollHeight || 0;
|
||||||
@ -1183,6 +1278,18 @@ function attachBounceListener() {
|
|||||||
lastObservedTop = top;
|
lastObservedTop = top;
|
||||||
lastObservedHeight = height;
|
lastObservedHeight = height;
|
||||||
|
|
||||||
|
if (heightDelta !== 0) {
|
||||||
|
console.log('[ChatAreaScrollDebug] height-change', {
|
||||||
|
heightDelta,
|
||||||
|
scrollHeight: height,
|
||||||
|
top,
|
||||||
|
delta,
|
||||||
|
escapedFromLock: escapedFromLock.value,
|
||||||
|
isAtBottom: isAtBottom.value,
|
||||||
|
isNearBottom: isNearBottom.value,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// 大幅高度增长检测:当 scrollHeight 突增(内容大幅展开/渲染),
|
// 大幅高度增长检测:当 scrollHeight 突增(内容大幅展开/渲染),
|
||||||
// 且用户没有主动脱离锁定(escapedFromLock=false)时,
|
// 且用户没有主动脱离锁定(escapedFromLock=false)时,
|
||||||
// 用 instant 瞬间追底,绕过 spring 动画的延迟。
|
// 用 instant 瞬间追底,绕过 spring 动画的延迟。
|
||||||
@ -1193,6 +1300,15 @@ function attachBounceListener() {
|
|||||||
heightDelta > largeGrowthThreshold &&
|
heightDelta > largeGrowthThreshold &&
|
||||||
!escapedFromLock.value
|
!escapedFromLock.value
|
||||||
) {
|
) {
|
||||||
|
console.log('[ChatAreaScrollDebug] TRIGGER:large-growth-scroll', {
|
||||||
|
heightDelta,
|
||||||
|
threshold: largeGrowthThreshold,
|
||||||
|
top,
|
||||||
|
delta,
|
||||||
|
escapedFromLock: escapedFromLock.value,
|
||||||
|
isAtBottom: isAtBottom.value,
|
||||||
|
isNearBottom: isNearBottom.value,
|
||||||
|
});
|
||||||
isHandlingLargeGrowth = true;
|
isHandlingLargeGrowth = true;
|
||||||
markProgrammaticHint('ChatArea.largeGrowth');
|
markProgrammaticHint('ChatArea.largeGrowth');
|
||||||
suppressUserIntentUntil = Date.now() + 900;
|
suppressUserIntentUntil = Date.now() + 900;
|
||||||
@ -1209,7 +1325,6 @@ function attachBounceListener() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const trusted = (event as any).isTrusted === true;
|
|
||||||
const closeToProgrammaticHint = now - lastProgrammaticHintTs <= 140;
|
const closeToProgrammaticHint = now - lastProgrammaticHintTs <= 140;
|
||||||
const likelyLayoutDriven = Math.abs(heightDelta) > 2;
|
const likelyLayoutDriven = Math.abs(heightDelta) > 2;
|
||||||
const manualUpByWheel = delta < -3 && now - lastUserWheelUpTs <= 240;
|
const manualUpByWheel = delta < -3 && now - lastUserWheelUpTs <= 240;
|
||||||
@ -1432,6 +1547,15 @@ async function stickScrollToBottom(
|
|||||||
) {
|
) {
|
||||||
const el = scrollRef.value;
|
const el = scrollRef.value;
|
||||||
if (el) {
|
if (el) {
|
||||||
|
console.log('[ChatAreaScrollDebug] CALL:stickScrollToBottom', {
|
||||||
|
behavior: options.behavior || 'auto',
|
||||||
|
force: !!options.force,
|
||||||
|
preserveScrollPosition: !!options.preserveScrollPosition,
|
||||||
|
isAtBottom: isAtBottom.value,
|
||||||
|
isNearBottom: isNearBottom.value,
|
||||||
|
escapedFromLock: escapedFromLock.value,
|
||||||
|
...getScrollMetrics(el),
|
||||||
|
});
|
||||||
bounceTraceLog(
|
bounceTraceLog(
|
||||||
'programmatic:scrollToBottom:before',
|
'programmatic:scrollToBottom:before',
|
||||||
{
|
{
|
||||||
|
|||||||
@ -462,8 +462,6 @@
|
|||||||
.message-block {
|
.message-block {
|
||||||
margin-bottom: 24px;
|
margin-bottom: 24px;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
content-visibility: auto;
|
|
||||||
contain-intrinsic-size: auto 300px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-block.message-block--sub-agent-notice {
|
.message-block.message-block--sub-agent-notice {
|
||||||
@ -492,11 +490,39 @@
|
|||||||
align-items: flex-end; /* 靠右对齐 */
|
align-items: flex-end; /* 靠右对齐 */
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-block--last .user-message:not(.user-message--compact):not(.user-message--brief) {
|
/* 多智能体消息:渲染为左侧对话气泡,复用用户消息气泡样式 */
|
||||||
|
.user-message.user-message--multi-agent {
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-block.message-block--multi-agent {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-message.user-message--multi-agent .message-header {
|
||||||
|
align-self: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-message.user-message--multi-agent .message-header.message-header--hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-message.user-message--multi-agent .message-text {
|
||||||
|
align-self: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-block--last
|
||||||
|
.user-message:not(.user-message--compact):not(.user-message--brief):not(
|
||||||
|
.user-message--multi-agent
|
||||||
|
) {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-block--last .user-message:not(.user-message--compact):not(.user-message--brief) .user-bubble-actions {
|
.message-block--last
|
||||||
|
.user-message:not(.user-message--compact):not(.user-message--brief):not(
|
||||||
|
.user-message--multi-agent
|
||||||
|
)
|
||||||
|
.user-bubble-actions {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 0;
|
right: 0;
|
||||||
top: 100%;
|
top: 100%;
|
||||||
|
|||||||
@ -59,6 +59,11 @@ from utils.host_workspace_debug import write_host_workspace_debug
|
|||||||
from utils.media_store import MediaStore
|
from utils.media_store import MediaStore
|
||||||
from utils.token_usage import normalize_usage_payload
|
from utils.token_usage import normalize_usage_payload
|
||||||
|
|
||||||
|
try:
|
||||||
|
from modules.multi_agent.role_store import DEFAULT_MUTIAGENTS_DIR
|
||||||
|
except Exception:
|
||||||
|
DEFAULT_MUTIAGENTS_DIR = None
|
||||||
|
|
||||||
AUTO_SHALLOW_PLACEHOLDER = "过早的工具结果已经被自动压缩"
|
AUTO_SHALLOW_PLACEHOLDER = "过早的工具结果已经被自动压缩"
|
||||||
AUTO_SHALLOW_TOOL_WHITELIST = {
|
AUTO_SHALLOW_TOOL_WHITELIST = {
|
||||||
"write_file",
|
"write_file",
|
||||||
@ -101,6 +106,12 @@ class ContextManagerBase:
|
|||||||
|
|
||||||
# 新增:对话持久化管理器
|
# 新增:对话持久化管理器
|
||||||
self.conversation_manager = ConversationManager(base_dir=self.data_dir, project_path=str(self.project_path))
|
self.conversation_manager = ConversationManager(base_dir=self.data_dir, project_path=str(self.project_path))
|
||||||
|
# 多智能体对话独立存储到 mutiagents/conversations
|
||||||
|
ma_base_dir = DEFAULT_MUTIAGENTS_DIR if DEFAULT_MUTIAGENTS_DIR else (self.data_dir.parent / "mutiagents")
|
||||||
|
self.multi_agent_conversation_manager = ConversationManager(
|
||||||
|
base_dir=str(ma_base_dir),
|
||||||
|
project_path=str(self.project_path),
|
||||||
|
)
|
||||||
self.media_store = MediaStore(self.data_dir)
|
self.media_store = MediaStore(self.data_dir)
|
||||||
self.current_conversation_id: Optional[str] = None
|
self.current_conversation_id: Optional[str] = None
|
||||||
self.auto_save_enabled = True
|
self.auto_save_enabled = True
|
||||||
@ -126,7 +137,9 @@ class ContextManagerBase:
|
|||||||
self.conversation_metadata[key] = value
|
self.conversation_metadata[key] = value
|
||||||
if save and self.current_conversation_id:
|
if save and self.current_conversation_id:
|
||||||
try:
|
try:
|
||||||
self.conversation_manager.update_conversation_metadata(
|
# 路由到正确的 conversation_manager
|
||||||
|
target_manager = getattr(self, "_get_conversation_manager_for_id", lambda _: self.conversation_manager)(self.current_conversation_id)
|
||||||
|
target_manager.update_conversation_metadata(
|
||||||
self.current_conversation_id,
|
self.current_conversation_id,
|
||||||
{key: value}
|
{key: value}
|
||||||
)
|
)
|
||||||
|
|||||||
@ -77,6 +77,42 @@ AUTO_SHALLOW_TOOL_WHITELIST = {
|
|||||||
class ConversationMixin:
|
class ConversationMixin:
|
||||||
"""ContextManager conversation mixin 能力 mixin。"""
|
"""ContextManager conversation mixin 能力 mixin。"""
|
||||||
|
|
||||||
|
def _get_conversation_manager_for_multi_agent_mode(self, multi_agent_mode: bool = False):
|
||||||
|
"""根据是否为多智能体会话返回对应的 ConversationManager。"""
|
||||||
|
if multi_agent_mode:
|
||||||
|
return getattr(self, "multi_agent_conversation_manager", None) or self.conversation_manager
|
||||||
|
return self.conversation_manager
|
||||||
|
|
||||||
|
def _get_conversation_manager_for_id(self, conversation_id: str):
|
||||||
|
"""根据对话ID判断它属于哪个 ConversationManager。
|
||||||
|
|
||||||
|
优先检查多智能体管理器(如果存在),再回退到普通管理器。
|
||||||
|
"""
|
||||||
|
ma_manager = getattr(self, "multi_agent_conversation_manager", None)
|
||||||
|
if not ma_manager:
|
||||||
|
return self.conversation_manager
|
||||||
|
# 先查多智能体索引/文件
|
||||||
|
try:
|
||||||
|
ma_path = ma_manager._get_conversation_file_path(conversation_id)
|
||||||
|
if ma_path.exists():
|
||||||
|
return ma_manager
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
regular_path = self.conversation_manager._get_conversation_file_path(conversation_id)
|
||||||
|
if regular_path.exists():
|
||||||
|
return self.conversation_manager
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
# 都不存在时:若ID在常规索引中则走常规,否则默认走多智能体(兼容新建对话后立刻保存)
|
||||||
|
try:
|
||||||
|
regular_index = self.conversation_manager._load_index()
|
||||||
|
if conversation_id in regular_index:
|
||||||
|
return self.conversation_manager
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return ma_manager
|
||||||
|
|
||||||
def start_new_conversation(
|
def start_new_conversation(
|
||||||
self,
|
self,
|
||||||
project_path: str = None,
|
project_path: str = None,
|
||||||
@ -117,8 +153,10 @@ class ConversationMixin:
|
|||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
print(f"[Skills] 同步失败: {exc}")
|
print(f"[Skills] 同步失败: {exc}")
|
||||||
|
|
||||||
# 创建新对话
|
# 创建新对话:多智能体对话使用独立的 conversation_manager
|
||||||
conversation_id = self.conversation_manager.create_conversation(
|
is_multi_agent = bool((metadata_overrides or {}).get("multi_agent_mode"))
|
||||||
|
target_manager = self._get_conversation_manager_for_multi_agent_mode(is_multi_agent)
|
||||||
|
conversation_id = target_manager.create_conversation(
|
||||||
project_path=project_path,
|
project_path=project_path,
|
||||||
thinking_mode=thinking_mode,
|
thinking_mode=thinking_mode,
|
||||||
run_mode=run_mode or ("thinking" if thinking_mode else "fast"),
|
run_mode=run_mode or ("thinking" if thinking_mode else "fast"),
|
||||||
@ -155,8 +193,20 @@ class ConversationMixin:
|
|||||||
if self.current_conversation_id and self.conversation_history:
|
if self.current_conversation_id and self.conversation_history:
|
||||||
self.save_current_conversation()
|
self.save_current_conversation()
|
||||||
|
|
||||||
# 加载指定对话
|
# 加载指定对话:先按ID路由到对应 manager
|
||||||
conversation_data = self.conversation_manager.load_conversation(conversation_id)
|
target_manager = self._get_conversation_manager_for_id(conversation_id)
|
||||||
|
conversation_data = target_manager.load_conversation(conversation_id)
|
||||||
|
if not conversation_data:
|
||||||
|
# 兜底:如果目标 manager 没有,再尝试另一个
|
||||||
|
fallback_manager = (
|
||||||
|
self.conversation_manager
|
||||||
|
if target_manager is self.multi_agent_conversation_manager
|
||||||
|
else self.multi_agent_conversation_manager
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
conversation_data = fallback_manager.load_conversation(conversation_id)
|
||||||
|
except Exception:
|
||||||
|
conversation_data = None
|
||||||
if not conversation_data:
|
if not conversation_data:
|
||||||
print(f"⌘ 对话 {conversation_id} 不存在")
|
print(f"⌘ 对话 {conversation_id} 不存在")
|
||||||
return False
|
return False
|
||||||
@ -279,7 +329,8 @@ class ConversationMixin:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
run_mode = getattr(self.main_terminal, "run_mode", None) if hasattr(self, "main_terminal") else None
|
run_mode = getattr(self.main_terminal, "run_mode", None) if hasattr(self, "main_terminal") else None
|
||||||
success = self.conversation_manager.save_conversation(
|
target_manager = self._get_conversation_manager_for_id(self.current_conversation_id)
|
||||||
|
success = target_manager.save_conversation(
|
||||||
conversation_id=self.current_conversation_id,
|
conversation_id=self.current_conversation_id,
|
||||||
messages=self.conversation_history,
|
messages=self.conversation_history,
|
||||||
project_path=str(self.project_path),
|
project_path=str(self.project_path),
|
||||||
@ -310,7 +361,8 @@ class ConversationMixin:
|
|||||||
try:
|
try:
|
||||||
run_mode = getattr(self.main_terminal, "run_mode", None) if hasattr(self, "main_terminal") else None
|
run_mode = getattr(self.main_terminal, "run_mode", None) if hasattr(self, "main_terminal") else None
|
||||||
model_key = getattr(self.main_terminal, "model_key", None) if hasattr(self, "main_terminal") else None
|
model_key = getattr(self.main_terminal, "model_key", None) if hasattr(self, "main_terminal") else None
|
||||||
self.conversation_manager.save_conversation(
|
target_manager = self._get_conversation_manager_for_id(self.current_conversation_id)
|
||||||
|
target_manager.save_conversation(
|
||||||
conversation_id=self.current_conversation_id,
|
conversation_id=self.current_conversation_id,
|
||||||
messages=self.conversation_history,
|
messages=self.conversation_history,
|
||||||
project_path=str(self.project_path),
|
project_path=str(self.project_path),
|
||||||
@ -326,8 +378,37 @@ class ConversationMixin:
|
|||||||
print(f"⌘ 自动保存异常: {e}")
|
print(f"⌘ 自动保存异常: {e}")
|
||||||
|
|
||||||
def get_conversation_list(self, limit: int = 50, offset: int = 0, non_empty: bool = False, multi_agent_mode: Optional[bool] = None) -> Dict:
|
def get_conversation_list(self, limit: int = 50, offset: int = 0, non_empty: bool = False, multi_agent_mode: Optional[bool] = None) -> Dict:
|
||||||
"""获取对话列表"""
|
"""获取对话列表。
|
||||||
|
|
||||||
|
多智能体对话存储在独立目录,需要合并普通管理器和多智能体管理器的结果。
|
||||||
|
"""
|
||||||
|
# 多智能体模式只查多智能体管理器;常规模式只查普通管理器;None 时合并
|
||||||
|
if multi_agent_mode is True:
|
||||||
|
ma_manager = getattr(self, "multi_agent_conversation_manager", None)
|
||||||
|
if ma_manager:
|
||||||
|
return ma_manager.get_conversation_list(limit=limit, offset=offset, non_empty=non_empty, multi_agent_mode=multi_agent_mode)
|
||||||
return self.conversation_manager.get_conversation_list(limit=limit, offset=offset, non_empty=non_empty, multi_agent_mode=multi_agent_mode)
|
return self.conversation_manager.get_conversation_list(limit=limit, offset=offset, non_empty=non_empty, multi_agent_mode=multi_agent_mode)
|
||||||
|
if multi_agent_mode is False:
|
||||||
|
return self.conversation_manager.get_conversation_list(limit=limit, offset=offset, non_empty=non_empty, multi_agent_mode=multi_agent_mode)
|
||||||
|
|
||||||
|
# None:合并两个管理器的结果
|
||||||
|
regular = self.conversation_manager.get_conversation_list(limit=limit, offset=offset, non_empty=non_empty, multi_agent_mode=False)
|
||||||
|
ma_manager = getattr(self, "multi_agent_conversation_manager", None)
|
||||||
|
if not ma_manager:
|
||||||
|
return regular
|
||||||
|
ma = ma_manager.get_conversation_list(limit=limit, offset=offset, non_empty=non_empty, multi_agent_mode=True)
|
||||||
|
|
||||||
|
merged_conversations = list(regular.get("conversations", [])) + list(ma.get("conversations", []))
|
||||||
|
merged_conversations.sort(key=lambda x: x.get("updated_at") or "", reverse=True)
|
||||||
|
total = (regular.get("total") or 0) + (ma.get("total") or 0)
|
||||||
|
result = merged_conversations[offset:offset + limit]
|
||||||
|
return {
|
||||||
|
"conversations": result,
|
||||||
|
"total": total,
|
||||||
|
"limit": limit,
|
||||||
|
"offset": offset,
|
||||||
|
"has_more": offset + limit < total
|
||||||
|
}
|
||||||
|
|
||||||
def delete_conversation_by_id(self, conversation_id: str) -> bool:
|
def delete_conversation_by_id(self, conversation_id: str) -> bool:
|
||||||
"""删除指定对话"""
|
"""删除指定对话"""
|
||||||
@ -338,7 +419,8 @@ class ConversationMixin:
|
|||||||
self.todo_list = None
|
self.todo_list = None
|
||||||
elif self.current_conversation_id and self.conversation_history:
|
elif self.current_conversation_id and self.conversation_history:
|
||||||
try:
|
try:
|
||||||
conversation_data = self.conversation_manager.load_conversation(self.current_conversation_id)
|
target_manager = self._get_conversation_manager_for_id(self.current_conversation_id)
|
||||||
|
conversation_data = target_manager.load_conversation(self.current_conversation_id)
|
||||||
if not conversation_data:
|
if not conversation_data:
|
||||||
self.current_conversation_id = None
|
self.current_conversation_id = None
|
||||||
self.conversation_history = []
|
self.conversation_history = []
|
||||||
@ -350,7 +432,24 @@ class ConversationMixin:
|
|||||||
print(f"⌘ 刷新待办列表失败: {exc}")
|
print(f"⌘ 刷新待办列表失败: {exc}")
|
||||||
self.todo_list = None
|
self.todo_list = None
|
||||||
|
|
||||||
return self.conversation_manager.delete_conversation(conversation_id)
|
# 先定位对话所在 manager,再删除;两个目录都尝试兜底
|
||||||
|
target_manager = self._get_conversation_manager_for_id(conversation_id)
|
||||||
|
try:
|
||||||
|
if target_manager.load_conversation(conversation_id):
|
||||||
|
return target_manager.delete_conversation(conversation_id)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
fallback_manager = (
|
||||||
|
self.conversation_manager
|
||||||
|
if target_manager is self.multi_agent_conversation_manager
|
||||||
|
else self.multi_agent_conversation_manager
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
if fallback_manager.load_conversation(conversation_id):
|
||||||
|
return fallback_manager.delete_conversation(conversation_id)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return False
|
||||||
|
|
||||||
def search_conversations(self, query: str, limit: int = 20) -> List[Dict]:
|
def search_conversations(self, query: str, limit: int = 20) -> List[Dict]:
|
||||||
"""搜索对话"""
|
"""搜索对话"""
|
||||||
|
|||||||
@ -174,7 +174,11 @@ class TokenMixin:
|
|||||||
if not target_id:
|
if not target_id:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return self.conversation_manager.get_token_statistics(target_id)
|
# 路由到正确的 conversation_manager(支持多智能体会话独立存储)
|
||||||
|
target_manager = getattr(
|
||||||
|
self, "_get_conversation_manager_for_id", lambda _: self.conversation_manager
|
||||||
|
)(target_id)
|
||||||
|
return target_manager.get_token_statistics(target_id)
|
||||||
|
|
||||||
def get_current_context_tokens(self, conversation_id: str = None) -> int:
|
def get_current_context_tokens(self, conversation_id: str = None) -> int:
|
||||||
"""
|
"""
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user