- 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
94 lines
3.4 KiB
Python
94 lines
3.4 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 PathMixin:
|
||
"""ConversationManager path mixin 能力 mixin。"""
|
||
|
||
def _load_host_workspaces(self) -> List[Dict[str, str]]:
|
||
"""读取宿主机工作区配置,用于把对话归入 workspace 子目录。"""
|
||
try:
|
||
path = Path(HOST_WORKSPACES_FILE).expanduser().resolve()
|
||
if not path.exists():
|
||
return []
|
||
raw = json.loads(path.read_text(encoding="utf-8"))
|
||
workspaces = raw.get("workspaces") if isinstance(raw, dict) else None
|
||
if not isinstance(workspaces, list):
|
||
return []
|
||
result: List[Dict[str, str]] = []
|
||
for item in workspaces:
|
||
if not isinstance(item, dict):
|
||
continue
|
||
workspace_id = str(item.get("workspace_id") or "").strip()
|
||
workspace_path = self._normalize_path(item.get("path"))
|
||
if workspace_id and workspace_path:
|
||
result.append({"workspace_id": workspace_id, "path": str(workspace_path)})
|
||
return result
|
||
except Exception as exc:
|
||
print(f"⚠️ 读取宿主机工作区配置失败: {exc}")
|
||
return []
|
||
|
||
@staticmethod
|
||
def _path_is_relative_to(path: Path, parent: Path) -> bool:
|
||
try:
|
||
path.relative_to(parent)
|
||
return True
|
||
except ValueError:
|
||
return False
|
||
|
||
def _resolve_workspace_id(self, project_path: Optional[Path]) -> Optional[str]:
|
||
"""按 host_workspaces.json 用最长路径前缀匹配 workspace_id。"""
|
||
if not project_path:
|
||
return None
|
||
matches = []
|
||
for item in self.host_workspaces:
|
||
workspace_path = self._normalize_path(item.get("path"))
|
||
if not workspace_path:
|
||
continue
|
||
if project_path == workspace_path or self._path_is_relative_to(project_path, workspace_path):
|
||
matches.append((len(str(workspace_path)), item.get("workspace_id")))
|
||
if not matches:
|
||
# 未匹配任何工作区时兜底使用 "default"(web 模式 / 空配置均适用)
|
||
return "default"
|
||
matches.sort(reverse=True)
|
||
return str(matches[0][1])
|
||
|
||
def _conversation_dir_for_workspace(self, workspace_id: Optional[str]) -> Path:
|
||
return self.conversations_root / workspace_id if workspace_id else self.conversations_root
|