agent-Specialization/utils/conversation_manager/metadata_mixin.py
JOJO f661dc6456 refactor(server,modules,utils,frontend): split oversized modules into packages and fix regressions
- Split server/chat.py, server/status.py, server/tasks.py into sub-packages
- Split utils/context_manager.py, utils/conversation_manager.py into mixin packages
- Split modules/file_manager.py, persistent_terminal.py, terminal_ops.py, mcp_client_manager.py into packages
- Split core/main_terminal_parts/context.py into base + mixins
- Split static/src/app/methods/{ui,message,upload,taskPolling,conversation} into sub-modules
- Fix flattened method exports, dynamic import paths, missing cross-module imports
- Add missing permission/network/execution mode constants and _PERMISSION_MODE_LABEL
- Update AGENTS.md, CLAUDE.md and project memories to reflect new structure
2026-06-20 15:54:48 +08:00

175 lines
6.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# utils/conversation_manager.py - 对话持久化管理器集成Token统计
import json
import os
import time
import tempfile
import threading
from datetime import datetime
from pathlib import Path
from typing import Dict, List, Optional, Any
from dataclasses import dataclass
try:
from config import DATA_DIR, HOST_WORKSPACES_FILE
except ImportError:
import sys
from pathlib import Path
project_root = Path(__file__).resolve().parents[1]
if str(project_root) not in sys.path:
sys.path.insert(0, str(project_root))
from config import DATA_DIR, HOST_WORKSPACES_FILE
@dataclass
class ConversationMetadata:
"""对话元数据"""
id: str
title: str
created_at: str
updated_at: str
project_path: Optional[str]
project_relative_path: Optional[str]
thinking_mode: bool
total_messages: int
total_tools: int
run_mode: str = "fast"
model_key: Optional[str] = None
has_images: bool = False
has_videos: bool = False
status: str = "active" # active, archived, error
class MetadataMixin:
"""ConversationManager metadata mixin 能力 mixin。"""
def _generate_conversation_id(self) -> str:
"""生成唯一的对话ID"""
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
# 添加毫秒确保唯一性
ms = int(time.time() * 1000) % 1000
return f"conv_{timestamp}_{ms:03d}"
def _get_conversation_file_path(self, conversation_id: str) -> Path:
"""获取对话文件路径"""
return self.conversations_dir / f"{conversation_id}.json"
def _extract_title_from_messages(self, messages: List[Dict]) -> str:
"""从消息中提取标题"""
# 找到第一个用户消息作为标题
for msg in messages:
if msg.get("role") == "user":
metadata = msg.get("metadata") or {}
source = str(metadata.get("source") or metadata.get("message_source") or "").strip().lower()
if source == "skill" or metadata.get("hidden") is True:
continue
content = msg.get("content", "").strip()
if content:
# 取前50个字符作为标题
title = content[:50]
if len(content) > 50:
title += "..."
return title
return "新对话"
def _extract_text_content(self, content: Any) -> str:
"""从字符串/多模态列表/字典中提取纯文本。"""
if isinstance(content, str):
return content
if isinstance(content, list):
parts: List[str] = []
for item in content:
if isinstance(item, str):
parts.append(item)
elif isinstance(item, dict):
if item.get("type") == "text":
parts.append(str(item.get("text") or ""))
elif isinstance(item.get("text"), str):
parts.append(item.get("text") or "")
return "".join(parts)
if isinstance(content, dict):
return str(content.get("text") or "")
return ""
def _extract_first_user_message(self, messages: List[Dict], max_chars: int = 100) -> str:
"""提取首条用户消息纯文本,用于最近对话/搜索展示。"""
for msg in messages or []:
if msg.get("role") != "user":
continue
text = " ".join(self._extract_text_content(msg.get("content")).split())
if not text:
continue
if len(text) > max_chars:
return text[:max_chars] + "..."
return text
return ""
def _count_tools_in_messages(self, messages: List[Dict]) -> int:
"""统计消息中的工具调用数量(仅统计 assistant.tool_calls"""
tool_count = 0
for msg in messages:
if msg.get("role") == "assistant" and "tool_calls" in msg:
tool_calls = msg.get("tool_calls", [])
tool_count += len(tool_calls) if isinstance(tool_calls, list) else 0
return tool_count
def _prepare_project_path_metadata(self, project_path: Optional[str]) -> Dict[str, Optional[str]]:
"""
将项目路径规范化为绝对/相对形式,便于在不同机器间迁移
"""
normalized = {
"project_path": None,
"project_relative_path": None
}
if not project_path:
return normalized
try:
absolute_path = Path(project_path).expanduser().resolve()
normalized["project_path"] = str(absolute_path)
try:
relative_path = absolute_path.relative_to(self.workspace_root)
normalized["project_relative_path"] = relative_path.as_posix()
except ValueError:
normalized["project_relative_path"] = None
except Exception:
# 回退为原始字符串,至少不会阻止对话保存
normalized["project_path"] = str(project_path)
normalized["project_relative_path"] = None
return normalized
def _initialize_token_statistics(self) -> Dict:
"""初始化Token统计结构"""
now = datetime.now().isoformat()
return {
"total_input_tokens": 0,
"total_output_tokens": 0,
"total_tokens": 0,
"current_context_tokens": 0,
"updated_at": now
}
def _validate_token_statistics(self, data: Dict) -> Dict:
"""验证并修复Token统计数据"""
token_stats = data.get("token_statistics", {})
# 确保必要字段存在
defaults = self._initialize_token_statistics()
for key, default_value in defaults.items():
if key not in token_stats:
token_stats[key] = default_value
# 确保数值类型正确
try:
token_stats["total_input_tokens"] = int(token_stats.get("total_input_tokens", 0))
token_stats["total_output_tokens"] = int(token_stats.get("total_output_tokens", 0))
token_stats["total_tokens"] = int(token_stats.get("total_tokens", 0))
token_stats["current_context_tokens"] = int(token_stats.get("current_context_tokens", 0))
except (ValueError, TypeError):
print("⚠️ Token统计数据损坏重置为0")
token_stats = defaults
data["token_statistics"] = token_stats
return data