- 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
124 lines
4.6 KiB
Python
124 lines
4.6 KiB
Python
# 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 StatsMixin:
|
||
"""ConversationManager stats mixin 能力 mixin。"""
|
||
|
||
def get_statistics(self) -> Dict:
|
||
"""
|
||
获取对话统计信息
|
||
|
||
Returns:
|
||
Dict: 统计信息
|
||
"""
|
||
try:
|
||
index = self._load_index()
|
||
# 统计必须基于全量对话,避免启动阶段“仅预热最近20条索引”导致总数偏小。
|
||
# 这里按需触发一次全量重建;后续统计可复用已保存的完整索引。
|
||
try:
|
||
total_files = len(self._iter_conversation_files(sort_by_mtime=False))
|
||
except Exception:
|
||
total_files = 0
|
||
if total_files and len(index) < total_files:
|
||
rebuilt_full = self._rebuild_index_from_files(max_count=None)
|
||
if rebuilt_full:
|
||
self._save_index(rebuilt_full)
|
||
index = rebuilt_full
|
||
|
||
total_conversations = len(index)
|
||
total_messages = sum(meta.get("total_messages", 0) for meta in index.values())
|
||
total_tools = sum(meta.get("total_tools", 0) for meta in index.values())
|
||
|
||
# 按状态分类
|
||
status_count = {}
|
||
for metadata in index.values():
|
||
status = metadata.get("status", "active")
|
||
status_count[status] = status_count.get(status, 0) + 1
|
||
|
||
# 按思考模式分类
|
||
thinking_mode_count = {
|
||
"thinking": sum(1 for meta in index.values() if meta.get("thinking_mode")),
|
||
"fast": sum(1 for meta in index.values() if not meta.get("thinking_mode"))
|
||
}
|
||
|
||
# 新增:Token统计汇总
|
||
total_input_tokens = 0
|
||
total_output_tokens = 0
|
||
token_stats_count = 0
|
||
total_user_messages = 0
|
||
|
||
for conv_id in index.keys():
|
||
conversation_data = self.load_conversation(conv_id)
|
||
if not conversation_data:
|
||
continue
|
||
validated = self._validate_token_statistics(conversation_data)
|
||
token_stats = validated.get("token_statistics", {}) or {}
|
||
total_input_tokens += token_stats.get("total_input_tokens", 0)
|
||
total_output_tokens += token_stats.get("total_output_tokens", 0)
|
||
token_stats_count += 1
|
||
messages = conversation_data.get("messages") or []
|
||
total_user_messages += sum(1 for msg in messages if msg.get("role") == "user")
|
||
|
||
return {
|
||
"total_conversations": total_conversations,
|
||
"total_messages": total_messages,
|
||
"total_user_messages": total_user_messages,
|
||
"total_tools": total_tools,
|
||
"status_distribution": status_count,
|
||
"thinking_mode_distribution": thinking_mode_count,
|
||
"token_statistics": {
|
||
"total_input_tokens": total_input_tokens,
|
||
"total_output_tokens": total_output_tokens,
|
||
"total_tokens": total_input_tokens + total_output_tokens,
|
||
"conversations_with_stats": token_stats_count
|
||
}
|
||
}
|
||
except Exception as e:
|
||
print(f"⌘ 获取统计信息失败: {e}")
|
||
return {}
|
||
|
||
def get_current_conversation_id(self) -> Optional[str]:
|
||
"""获取当前对话ID"""
|
||
return self.current_conversation_id
|
||
|
||
def set_current_conversation_id(self, conversation_id: str):
|
||
"""设置当前对话ID"""
|
||
self.current_conversation_id = conversation_id
|