Compare commits
No commits in common. "b1cf4b9ff25ccbba7c8b9a33a08d56d507853694" and "672110c21376b231ab162459ce776e790032ee62" have entirely different histories.
b1cf4b9ff2
...
672110c213
@ -493,10 +493,14 @@ class MainTerminalToolsExecutionMixin:
|
|||||||
if self._is_host_mode():
|
if self._is_host_mode():
|
||||||
return Path(AGENT_SKILLS_DIR).expanduser().resolve()
|
return Path(AGENT_SKILLS_DIR).expanduser().resolve()
|
||||||
|
|
||||||
inferred = infer_private_skills_dir(self.data_dir)
|
data_dir = Path(self.data_dir).expanduser().resolve()
|
||||||
if inferred:
|
# 普通 Docker 用户: users/<username>/data -> users/<username>/agentskills
|
||||||
return inferred
|
# API 工作区用户: users_api/<username>/workspaces/<ws>/data -> users_api/<username>/agentskills
|
||||||
return (Path(self.data_dir).expanduser().resolve().parent / "agentskills").resolve()
|
if data_dir.name == "data" and data_dir.parent.parent.name == "workspaces":
|
||||||
|
user_root = data_dir.parent.parent.parent
|
||||||
|
else:
|
||||||
|
user_root = data_dir.parent
|
||||||
|
return (user_root / "agentskills").resolve()
|
||||||
|
|
||||||
def _handle_create_skill_tool(self, arguments: Dict[str, Any]) -> Dict[str, Any]:
|
def _handle_create_skill_tool(self, arguments: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
source = self._resolve_create_skill_source_dir(arguments.get("source_dir"))
|
source = self._resolve_create_skill_source_dir(arguments.get("source_dir"))
|
||||||
|
|||||||
@ -240,7 +240,7 @@ def create_host_workspace(
|
|||||||
|
|
||||||
workspace = {
|
workspace = {
|
||||||
"workspace_id": workspace_id,
|
"workspace_id": workspace_id,
|
||||||
"label": clean_label or normalized_path.name or workspace_id,
|
"label": clean_label or workspace_id,
|
||||||
"path": target_path_str,
|
"path": target_path_str,
|
||||||
}
|
}
|
||||||
raw_workspaces.append(workspace)
|
raw_workspaces.append(workspace)
|
||||||
@ -259,85 +259,6 @@ def create_host_workspace(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def rename_host_workspace(
|
|
||||||
workspace_id: str,
|
|
||||||
label: str,
|
|
||||||
config_path: Optional[Union[str, Path]] = None,
|
|
||||||
) -> Dict[str, Any]:
|
|
||||||
ws_id = str(workspace_id or "").strip()
|
|
||||||
clean_label = str(label or "").strip()
|
|
||||||
if not ws_id:
|
|
||||||
raise ValueError("缺少 workspace_id")
|
|
||||||
if not clean_label:
|
|
||||||
raise ValueError("工作区名称不能为空")
|
|
||||||
|
|
||||||
cfg_path = _resolve_config_path(config_path)
|
|
||||||
with _HOST_WORKSPACE_LOCK:
|
|
||||||
payload = _ensure_config_file(cfg_path, strict=True)
|
|
||||||
raw_workspaces = payload.get("workspaces")
|
|
||||||
if not isinstance(raw_workspaces, list):
|
|
||||||
raw_workspaces = []
|
|
||||||
updated = None
|
|
||||||
for idx, item in enumerate(raw_workspaces):
|
|
||||||
normalized = _normalize_entry(item, idx)
|
|
||||||
if not normalized or normalized.get("workspace_id") != ws_id:
|
|
||||||
continue
|
|
||||||
item["workspace_id"] = ws_id
|
|
||||||
item["label"] = clean_label
|
|
||||||
item["path"] = normalized.get("path")
|
|
||||||
updated = dict(item)
|
|
||||||
break
|
|
||||||
if not updated:
|
|
||||||
raise ValueError("工作区不存在")
|
|
||||||
payload["workspaces"] = raw_workspaces
|
|
||||||
_atomic_write_json(cfg_path, payload)
|
|
||||||
|
|
||||||
return {
|
|
||||||
"workspace": updated,
|
|
||||||
"catalog": load_host_workspace_catalog(config_path=cfg_path),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def delete_host_workspace(
|
|
||||||
workspace_id: str,
|
|
||||||
config_path: Optional[Union[str, Path]] = None,
|
|
||||||
) -> Dict[str, Any]:
|
|
||||||
ws_id = str(workspace_id or "").strip()
|
|
||||||
if not ws_id:
|
|
||||||
raise ValueError("缺少 workspace_id")
|
|
||||||
|
|
||||||
cfg_path = _resolve_config_path(config_path)
|
|
||||||
with _HOST_WORKSPACE_LOCK:
|
|
||||||
payload = _ensure_config_file(cfg_path, strict=True)
|
|
||||||
raw_workspaces = payload.get("workspaces")
|
|
||||||
if not isinstance(raw_workspaces, list):
|
|
||||||
raw_workspaces = []
|
|
||||||
|
|
||||||
kept = []
|
|
||||||
deleted = None
|
|
||||||
for idx, item in enumerate(raw_workspaces):
|
|
||||||
normalized = _normalize_entry(item, idx)
|
|
||||||
if normalized and normalized.get("workspace_id") == ws_id:
|
|
||||||
deleted = normalized
|
|
||||||
continue
|
|
||||||
kept.append(item)
|
|
||||||
if not deleted:
|
|
||||||
raise ValueError("工作区不存在")
|
|
||||||
if not kept:
|
|
||||||
raise ValueError("至少保留一个工作区")
|
|
||||||
|
|
||||||
payload["workspaces"] = kept
|
|
||||||
if str(payload.get("default_workspace_id") or "").strip() == ws_id:
|
|
||||||
first = _normalize_entry(kept[0], 0)
|
|
||||||
payload["default_workspace_id"] = (first or {}).get("workspace_id") or "default"
|
|
||||||
_atomic_write_json(cfg_path, payload)
|
|
||||||
|
|
||||||
return {
|
|
||||||
"deleted": deleted,
|
|
||||||
"catalog": load_host_workspace_catalog(config_path=cfg_path),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"load_host_workspace_catalog",
|
"load_host_workspace_catalog",
|
||||||
"resolve_host_workspace",
|
"resolve_host_workspace",
|
||||||
|
|||||||
@ -55,41 +55,6 @@ def infer_private_skills_dir(data_dir: str | Path | None) -> Optional[Path]:
|
|||||||
data_path = Path(data_dir).expanduser().resolve()
|
data_path = Path(data_dir).expanduser().resolve()
|
||||||
if data_path.name == "data" and data_path.parent.parent.name == "workspaces":
|
if data_path.name == "data" and data_path.parent.parent.name == "workspaces":
|
||||||
return (data_path.parent.parent.parent / "agentskills").resolve()
|
return (data_path.parent.parent.parent / "agentskills").resolve()
|
||||||
# Docker Web 项目化布局:
|
|
||||||
# users/<user>/projects/<project_id>/data -> users/<user>/personal/agentskills
|
|
||||||
if data_path.name == "data" and data_path.parent.parent.name == "projects":
|
|
||||||
user_root = data_path.parent.parent.parent
|
|
||||||
shared_root = (user_root / "personal" / "agentskills").resolve()
|
|
||||||
legacy_root = (data_path.parent / "agentskills").resolve()
|
|
||||||
if legacy_root.exists() and legacy_root.is_dir() and legacy_root != shared_root:
|
|
||||||
shared_root.mkdir(parents=True, exist_ok=True)
|
|
||||||
for item in list(legacy_root.iterdir()):
|
|
||||||
dest = shared_root / item.name
|
|
||||||
if not dest.exists():
|
|
||||||
shutil.move(str(item), str(dest))
|
|
||||||
try:
|
|
||||||
legacy_root.rmdir()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return shared_root
|
|
||||||
# 兼容上一轮临时结构:
|
|
||||||
# users/<user>/project/<project_id>/data -> users/<user>/personal/agentskills
|
|
||||||
if data_path.name == "data" and data_path.parent.parent.name == "project":
|
|
||||||
user_root = data_path.parent.parent.parent
|
|
||||||
shared_root = (user_root / "personal" / "agentskills").resolve()
|
|
||||||
temp_shared_root = (data_path.parent.parent / "shared" / "agentskills").resolve()
|
|
||||||
for legacy_root in (temp_shared_root, data_path.parent / "agentskills"):
|
|
||||||
if legacy_root.exists() and legacy_root.is_dir() and legacy_root != shared_root:
|
|
||||||
shared_root.mkdir(parents=True, exist_ok=True)
|
|
||||||
for item in list(legacy_root.iterdir()):
|
|
||||||
dest = shared_root / item.name
|
|
||||||
if not dest.exists():
|
|
||||||
shutil.move(str(item), str(dest))
|
|
||||||
try:
|
|
||||||
legacy_root.rmdir()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return shared_root
|
|
||||||
return (data_path.parent / "agentskills").resolve()
|
return (data_path.parent / "agentskills").resolve()
|
||||||
except Exception:
|
except Exception:
|
||||||
return None
|
return None
|
||||||
|
|||||||
@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
import shutil
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@ -17,11 +16,8 @@ from config import (
|
|||||||
USER_SPACE_DIR,
|
USER_SPACE_DIR,
|
||||||
USERS_DB_FILE,
|
USERS_DB_FILE,
|
||||||
UPLOAD_QUARANTINE_SUBDIR,
|
UPLOAD_QUARANTINE_SUBDIR,
|
||||||
MAIN_MEMORY_FILE,
|
|
||||||
TASK_MEMORY_FILE,
|
|
||||||
)
|
)
|
||||||
from modules.personalization_manager import ensure_personalization_config
|
from modules.personalization_manager import ensure_personalization_config
|
||||||
from modules.personalization_manager import PERSONALIZATION_FILENAME
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@ -50,7 +46,6 @@ class UserManager:
|
|||||||
"""Handle user registration, authentication and workspace provisioning."""
|
"""Handle user registration, authentication and workspace provisioning."""
|
||||||
|
|
||||||
USERNAME_REGEX = re.compile(r"^[a-z0-9_\-]{3,32}$")
|
USERNAME_REGEX = re.compile(r"^[a-z0-9_\-]{3,32}$")
|
||||||
RESERVED_WORKSPACE_IDS = {"default", "personal", "shared"}
|
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@ -124,221 +119,12 @@ class UserManager:
|
|||||||
def get_user(self, username: str) -> Optional[UserRecord]:
|
def get_user(self, username: str) -> Optional[UserRecord]:
|
||||||
return self._users.get((username or "").strip().lower())
|
return self._users.get((username or "").strip().lower())
|
||||||
|
|
||||||
@staticmethod
|
def ensure_user_workspace(self, username: str) -> UserWorkspace:
|
||||||
def normalize_workspace_id(workspace_id: str) -> str:
|
|
||||||
candidate = (workspace_id or "default").strip()
|
|
||||||
if not candidate:
|
|
||||||
candidate = "default"
|
|
||||||
if not re.fullmatch(r"[A-Za-z0-9._-]{1,40}", candidate):
|
|
||||||
raise ValueError("项目 ID 只能包含字母、数字、点、下划线或连字符,长度 1-40。")
|
|
||||||
return candidate
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def validate_new_workspace_id(cls, workspace_id: str) -> str:
|
|
||||||
candidate = cls.normalize_workspace_id(workspace_id)
|
|
||||||
if candidate in cls.RESERVED_WORKSPACE_IDS or candidate.startswith("."):
|
|
||||||
raise ValueError("该项目名称已被系统保留,请换一个名称。")
|
|
||||||
return candidate
|
|
||||||
|
|
||||||
def _project_container_root(self, username: str) -> Path:
|
|
||||||
return (self.workspace_root / username / "projects").resolve()
|
|
||||||
|
|
||||||
def _personal_root(self, username: str) -> Path:
|
|
||||||
return (self.workspace_root / username / "personal").resolve()
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _looks_like_project_workspace_dir(path: Path) -> bool:
|
|
||||||
return path.is_dir() and ((path / "project").exists() or (path / "data").exists())
|
|
||||||
|
|
||||||
def _move_dir_contents(self, source: Path, target: Path) -> None:
|
|
||||||
if not source.exists() or not source.is_dir():
|
|
||||||
return
|
|
||||||
target.mkdir(parents=True, exist_ok=True)
|
|
||||||
for child in list(source.iterdir()):
|
|
||||||
dest = target / child.name
|
|
||||||
if dest.exists():
|
|
||||||
continue
|
|
||||||
shutil.move(str(child), str(dest))
|
|
||||||
|
|
||||||
def _ensure_shared_file_link(self, link_path: Path, target_path: Path) -> None:
|
|
||||||
target_path.parent.mkdir(parents=True, exist_ok=True)
|
|
||||||
link_path.parent.mkdir(parents=True, exist_ok=True)
|
|
||||||
if link_path.is_symlink():
|
|
||||||
try:
|
|
||||||
if link_path.resolve() == target_path.resolve():
|
|
||||||
return
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
try:
|
|
||||||
link_path.unlink()
|
|
||||||
except Exception:
|
|
||||||
return
|
|
||||||
if link_path.exists():
|
|
||||||
if not link_path.is_file():
|
|
||||||
return
|
|
||||||
try:
|
|
||||||
should_adopt_existing = (
|
|
||||||
not target_path.exists()
|
|
||||||
or (target_path.is_file() and target_path.stat().st_size == 0)
|
|
||||||
)
|
|
||||||
except Exception:
|
|
||||||
should_adopt_existing = not target_path.exists()
|
|
||||||
if should_adopt_existing:
|
|
||||||
try:
|
|
||||||
shutil.copy2(str(link_path), str(target_path))
|
|
||||||
except Exception:
|
|
||||||
return
|
|
||||||
try:
|
|
||||||
link_path.unlink()
|
|
||||||
except Exception:
|
|
||||||
return
|
|
||||||
if not target_path.exists():
|
|
||||||
target_path.touch()
|
|
||||||
try:
|
|
||||||
link_path.symlink_to(target_path)
|
|
||||||
except Exception:
|
|
||||||
if not link_path.exists() and target_path.exists():
|
|
||||||
try:
|
|
||||||
shutil.copy2(target_path, link_path)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def _ensure_shared_user_state_links(self, username: str, data_dir: Path) -> None:
|
|
||||||
"""项目间共享用户级个性化与记忆,同时保持 data_dir 路径兼容。"""
|
|
||||||
personal_dir = self._personal_root(username)
|
|
||||||
personalization_dir = personal_dir / "personalization"
|
|
||||||
memory_dir = personal_dir / "memory"
|
|
||||||
personalization_dir.mkdir(parents=True, exist_ok=True)
|
|
||||||
memory_dir.mkdir(parents=True, exist_ok=True)
|
|
||||||
|
|
||||||
self._ensure_shared_file_link(
|
|
||||||
data_dir / PERSONALIZATION_FILENAME,
|
|
||||||
personalization_dir / PERSONALIZATION_FILENAME,
|
|
||||||
)
|
|
||||||
ensure_personalization_config(personalization_dir)
|
|
||||||
self._ensure_shared_file_link(
|
|
||||||
data_dir / Path(MAIN_MEMORY_FILE).name,
|
|
||||||
memory_dir / Path(MAIN_MEMORY_FILE).name,
|
|
||||||
)
|
|
||||||
self._ensure_shared_file_link(
|
|
||||||
data_dir / Path(TASK_MEMORY_FILE).name,
|
|
||||||
memory_dir / Path(TASK_MEMORY_FILE).name,
|
|
||||||
)
|
|
||||||
|
|
||||||
def _migrate_legacy_docker_workspace_layout(self, username: str) -> None:
|
|
||||||
"""把旧 Docker Web 单项目目录迁移为 projects/default/{project,data,logs}。
|
|
||||||
|
|
||||||
旧布局:
|
|
||||||
<user>/project/*
|
|
||||||
<user>/data/*
|
|
||||||
<user>/logs/*
|
|
||||||
|
|
||||||
新布局:
|
|
||||||
<user>/projects/default/project/*
|
|
||||||
<user>/projects/default/data/*
|
|
||||||
<user>/projects/default/logs/*
|
|
||||||
<user>/personal/{personalization,memory,agentskills}
|
|
||||||
"""
|
|
||||||
root = (self.workspace_root / username).resolve()
|
|
||||||
projects_root = self._project_container_root(username)
|
|
||||||
personal_root = self._personal_root(username)
|
|
||||||
legacy_project_root = root / "project"
|
|
||||||
default_root = projects_root / "default"
|
|
||||||
default_project = default_root / "project"
|
|
||||||
default_data = default_root / "data"
|
|
||||||
default_logs = default_root / "logs"
|
|
||||||
marker = default_root / ".migrated_from_legacy"
|
|
||||||
|
|
||||||
# 兼容上一轮临时结构 users/<user>/project/<id> 与 users/<user>/project/shared。
|
|
||||||
if legacy_project_root.exists() and legacy_project_root.is_dir():
|
|
||||||
projects_root.mkdir(parents=True, exist_ok=True)
|
|
||||||
|
|
||||||
legacy_shared = legacy_project_root / "shared"
|
|
||||||
if legacy_shared.exists() and legacy_shared.is_dir():
|
|
||||||
for sub in ("personalization", "memory", "agentskills"):
|
|
||||||
self._move_dir_contents(legacy_shared / sub, personal_root / sub)
|
|
||||||
try:
|
|
||||||
shutil.rmtree(legacy_shared, ignore_errors=True)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
for item in list(legacy_project_root.iterdir()):
|
|
||||||
if item.name.startswith("."):
|
|
||||||
continue
|
|
||||||
if item.name in self.RESERVED_WORKSPACE_IDS - {"default"}:
|
|
||||||
continue
|
|
||||||
if self._looks_like_project_workspace_dir(item):
|
|
||||||
dest = projects_root / item.name
|
|
||||||
if not dest.exists():
|
|
||||||
shutil.move(str(item), str(dest))
|
|
||||||
|
|
||||||
# 迁移最老布局 users/<user>/project/* 下的实际文件到 default/project。
|
|
||||||
if legacy_project_root.exists() and not marker.exists():
|
|
||||||
legacy_children = [
|
|
||||||
child
|
|
||||||
for child in list(legacy_project_root.iterdir())
|
|
||||||
if (
|
|
||||||
not child.name.startswith(".")
|
|
||||||
and not (child.is_dir() and child.name in (self.RESERVED_WORKSPACE_IDS - {"project"}))
|
|
||||||
and not self._looks_like_project_workspace_dir(child)
|
|
||||||
)
|
|
||||||
]
|
|
||||||
if legacy_children:
|
|
||||||
default_project.mkdir(parents=True, exist_ok=True)
|
|
||||||
for child in legacy_children:
|
|
||||||
dest = default_project / child.name
|
|
||||||
if not dest.exists():
|
|
||||||
shutil.move(str(child), str(dest))
|
|
||||||
try:
|
|
||||||
legacy_project_root.rmdir()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# 迁移旧 data/logs。
|
|
||||||
legacy_data = root / "data"
|
|
||||||
legacy_logs = root / "logs"
|
|
||||||
if legacy_data.exists() and legacy_data.is_dir() and legacy_data.resolve() != default_data.resolve():
|
|
||||||
if not default_data.exists():
|
|
||||||
default_data.parent.mkdir(parents=True, exist_ok=True)
|
|
||||||
shutil.move(str(legacy_data), str(default_data))
|
|
||||||
else:
|
|
||||||
self._move_dir_contents(legacy_data, default_data)
|
|
||||||
try:
|
|
||||||
legacy_data.rmdir()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
if legacy_logs.exists() and legacy_logs.is_dir() and legacy_logs.resolve() != default_logs.resolve():
|
|
||||||
if not default_logs.exists():
|
|
||||||
default_logs.parent.mkdir(parents=True, exist_ok=True)
|
|
||||||
shutil.move(str(legacy_logs), str(default_logs))
|
|
||||||
else:
|
|
||||||
self._move_dir_contents(legacy_logs, default_logs)
|
|
||||||
try:
|
|
||||||
legacy_logs.rmdir()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
legacy_agentskills = root / "agentskills"
|
|
||||||
if legacy_agentskills.exists() and legacy_agentskills.is_dir():
|
|
||||||
self._move_dir_contents(legacy_agentskills, personal_root / "agentskills")
|
|
||||||
try:
|
|
||||||
legacy_agentskills.rmdir()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
try:
|
|
||||||
default_root.mkdir(parents=True, exist_ok=True)
|
|
||||||
marker.write_text("ok", encoding="utf-8")
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def ensure_user_workspace(self, username: str, workspace_id: str = "default") -> UserWorkspace:
|
|
||||||
username = self._normalize_username(username)
|
username = self._normalize_username(username)
|
||||||
workspace_id = self.normalize_workspace_id(workspace_id)
|
|
||||||
root = (self.workspace_root / username).resolve()
|
root = (self.workspace_root / username).resolve()
|
||||||
self._migrate_legacy_docker_workspace_layout(username)
|
project_path = root / "project"
|
||||||
workspace_root = self._project_container_root(username) / workspace_id
|
data_dir = root / "data"
|
||||||
project_path = workspace_root / "project"
|
logs_dir = root / "logs"
|
||||||
data_dir = workspace_root / "data"
|
|
||||||
logs_dir = workspace_root / "logs"
|
|
||||||
uploads_dir = project_path / ".agents" / "user_upload"
|
uploads_dir = project_path / ".agents" / "user_upload"
|
||||||
skills_dir = project_path / ".agents" / "skills"
|
skills_dir = project_path / ".agents" / "skills"
|
||||||
|
|
||||||
@ -348,152 +134,23 @@ class UserManager:
|
|||||||
# 初始化数据子目录
|
# 初始化数据子目录
|
||||||
(data_dir / "conversations").mkdir(parents=True, exist_ok=True)
|
(data_dir / "conversations").mkdir(parents=True, exist_ok=True)
|
||||||
(data_dir / "backups").mkdir(parents=True, exist_ok=True)
|
(data_dir / "backups").mkdir(parents=True, exist_ok=True)
|
||||||
self._ensure_shared_user_state_links(username, data_dir)
|
ensure_personalization_config(data_dir)
|
||||||
|
|
||||||
quarantine_root = Path(UPLOAD_QUARANTINE_SUBDIR).expanduser()
|
quarantine_root = Path(UPLOAD_QUARANTINE_SUBDIR).expanduser()
|
||||||
if not quarantine_root.is_absolute():
|
if not quarantine_root.is_absolute():
|
||||||
quarantine_root = (self.workspace_root.parent / UPLOAD_QUARANTINE_SUBDIR).resolve()
|
quarantine_root = (self.workspace_root.parent / UPLOAD_QUARANTINE_SUBDIR).resolve()
|
||||||
quarantine_dir = (quarantine_root / username / workspace_id).resolve()
|
quarantine_dir = (quarantine_root / username).resolve()
|
||||||
quarantine_dir.mkdir(parents=True, exist_ok=True)
|
quarantine_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
workspace = UserWorkspace(
|
return UserWorkspace(
|
||||||
username=username,
|
username=username,
|
||||||
root=workspace_root,
|
root=root,
|
||||||
project_path=project_path,
|
project_path=project_path,
|
||||||
data_dir=data_dir,
|
data_dir=data_dir,
|
||||||
logs_dir=logs_dir,
|
logs_dir=logs_dir,
|
||||||
uploads_dir=uploads_dir,
|
uploads_dir=uploads_dir,
|
||||||
quarantine_dir=quarantine_dir,
|
quarantine_dir=quarantine_dir,
|
||||||
)
|
)
|
||||||
workspace.workspace_id = workspace_id
|
|
||||||
return workspace
|
|
||||||
|
|
||||||
def list_user_workspaces(self, username: str) -> Dict[str, Dict]:
|
|
||||||
username = self._normalize_username(username)
|
|
||||||
self._migrate_legacy_docker_workspace_layout(username)
|
|
||||||
result: Dict[str, Dict] = {}
|
|
||||||
|
|
||||||
projects_root = self._project_container_root(username)
|
|
||||||
default_project = projects_root / "default" / "project"
|
|
||||||
default_data = projects_root / "default" / "data"
|
|
||||||
if default_project.exists() or default_data.exists():
|
|
||||||
result["default"] = {
|
|
||||||
"workspace_id": "default",
|
|
||||||
"label": "默认项目",
|
|
||||||
"path": "",
|
|
||||||
"is_default": True,
|
|
||||||
"has_conversations": (default_data / "conversations").exists(),
|
|
||||||
}
|
|
||||||
else:
|
|
||||||
# 新用户也始终暴露默认项目,首次访问时再创建目录。
|
|
||||||
result["default"] = {
|
|
||||||
"workspace_id": "default",
|
|
||||||
"label": "默认项目",
|
|
||||||
"path": "",
|
|
||||||
"is_default": True,
|
|
||||||
"has_conversations": False,
|
|
||||||
}
|
|
||||||
|
|
||||||
if projects_root.exists():
|
|
||||||
for item in sorted(projects_root.iterdir(), key=lambda p: p.name.lower()):
|
|
||||||
if not item.is_dir():
|
|
||||||
continue
|
|
||||||
ws_id = item.name
|
|
||||||
if ws_id in self.RESERVED_WORKSPACE_IDS or ws_id.startswith("."):
|
|
||||||
continue
|
|
||||||
data_dir = item / "data"
|
|
||||||
result[ws_id] = {
|
|
||||||
"workspace_id": ws_id,
|
|
||||||
"label": self._load_workspace_label(item, ws_id),
|
|
||||||
"path": "",
|
|
||||||
"is_default": False,
|
|
||||||
"has_conversations": (data_dir / "conversations").exists(),
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
|
|
||||||
def generate_workspace_id(self, username: str, label: str = "") -> str:
|
|
||||||
username = self._normalize_username(username)
|
|
||||||
raw = (label or "project").strip().lower()
|
|
||||||
slug = re.sub(r"[^a-z0-9]+", "-", raw).strip("-")
|
|
||||||
if not slug or slug in self.RESERVED_WORKSPACE_IDS or slug.startswith("."):
|
|
||||||
slug = "project"
|
|
||||||
slug = slug[:32].strip("-") or "project"
|
|
||||||
existing = set(self.list_user_workspaces(username).keys())
|
|
||||||
candidate = slug
|
|
||||||
idx = 2
|
|
||||||
while candidate in existing or candidate in self.RESERVED_WORKSPACE_IDS or candidate.startswith("."):
|
|
||||||
suffix = f"-{idx}"
|
|
||||||
candidate = f"{slug[: max(1, 40 - len(suffix))]}{suffix}"
|
|
||||||
idx += 1
|
|
||||||
return candidate
|
|
||||||
|
|
||||||
def create_user_workspace(self, username: str, workspace_id: str = "", label: str = "") -> UserWorkspace:
|
|
||||||
ws_id = (
|
|
||||||
self.validate_new_workspace_id(workspace_id)
|
|
||||||
if workspace_id
|
|
||||||
else self.generate_workspace_id(username, label)
|
|
||||||
)
|
|
||||||
workspace = self.ensure_user_workspace(username, ws_id)
|
|
||||||
label_text = (label or "").strip()
|
|
||||||
if label_text:
|
|
||||||
meta_path = Path(workspace.root) / "project.json"
|
|
||||||
try:
|
|
||||||
meta_path.write_text(
|
|
||||||
json.dumps({"workspace_id": ws_id, "label": label_text}, ensure_ascii=False, indent=2),
|
|
||||||
encoding="utf-8",
|
|
||||||
)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return workspace
|
|
||||||
|
|
||||||
def rename_user_workspace(self, username: str, workspace_id: str, label: str) -> Dict:
|
|
||||||
username = self._normalize_username(username)
|
|
||||||
ws_id = self.normalize_workspace_id(workspace_id)
|
|
||||||
label_text = (label or "").strip()
|
|
||||||
if not label_text:
|
|
||||||
raise ValueError("项目名称不能为空")
|
|
||||||
known = self.list_user_workspaces(username)
|
|
||||||
if ws_id not in known:
|
|
||||||
raise ValueError("项目不存在")
|
|
||||||
workspace = self.ensure_user_workspace(username, ws_id)
|
|
||||||
meta_path = Path(workspace.root) / "project.json"
|
|
||||||
meta_path.write_text(
|
|
||||||
json.dumps({"workspace_id": ws_id, "label": label_text}, ensure_ascii=False, indent=2),
|
|
||||||
encoding="utf-8",
|
|
||||||
)
|
|
||||||
return {
|
|
||||||
"workspace_id": ws_id,
|
|
||||||
"label": label_text,
|
|
||||||
"path": "",
|
|
||||||
"is_default": ws_id == "default",
|
|
||||||
"has_conversations": (Path(workspace.data_dir) / "conversations").exists(),
|
|
||||||
}
|
|
||||||
|
|
||||||
def delete_user_workspace(self, username: str, workspace_id: str) -> None:
|
|
||||||
username = self._normalize_username(username)
|
|
||||||
ws_id = self.validate_new_workspace_id(workspace_id)
|
|
||||||
projects_root = self._project_container_root(username)
|
|
||||||
target = (projects_root / ws_id).resolve()
|
|
||||||
try:
|
|
||||||
target.relative_to(projects_root.resolve())
|
|
||||||
except ValueError as exc:
|
|
||||||
raise ValueError("项目路径不合法") from exc
|
|
||||||
if not target.exists() or not target.is_dir():
|
|
||||||
raise ValueError("项目不存在")
|
|
||||||
shutil.rmtree(target)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _load_workspace_label(workspace_root: Path, fallback: str) -> str:
|
|
||||||
meta_path = workspace_root / "project.json"
|
|
||||||
if meta_path.exists():
|
|
||||||
try:
|
|
||||||
data = json.loads(meta_path.read_text(encoding="utf-8"))
|
|
||||||
label = str(data.get("label") or "").strip()
|
|
||||||
if label:
|
|
||||||
return label
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return fallback
|
|
||||||
|
|
||||||
def list_invite_codes(self):
|
def list_invite_codes(self):
|
||||||
return list(self._invites.values())
|
return list(self._invites.values())
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
## 工作区信息
|
## 工作区信息
|
||||||
|
|
||||||
- **项目名称**:{project_name}
|
|
||||||
- **运行环境**:{runtime_environment}
|
- **运行环境**:{runtime_environment}
|
||||||
- **资源限制**:{resource_limit}
|
- **资源限制**:{resource_limit}
|
||||||
- **当前时间**:{current_time}
|
- **当前时间**:{current_time}
|
||||||
|
|||||||
@ -182,7 +182,6 @@ def login():
|
|||||||
session['username'] = record.username
|
session['username'] = record.username
|
||||||
session['role'] = record.role or 'user'
|
session['role'] = record.role or 'user'
|
||||||
session['host_mode'] = False
|
session['host_mode'] = False
|
||||||
session['workspace_id'] = 'default'
|
|
||||||
default_thinking = current_app.config.get('DEFAULT_THINKING_MODE', False)
|
default_thinking = current_app.config.get('DEFAULT_THINKING_MODE', False)
|
||||||
session['thinking_mode'] = default_thinking
|
session['thinking_mode'] = default_thinking
|
||||||
session['run_mode'] = current_app.config.get('DEFAULT_RUN_MODE', "deep" if default_thinking else "fast")
|
session['run_mode'] = current_app.config.get('DEFAULT_RUN_MODE', "deep" if default_thinking else "fast")
|
||||||
@ -193,12 +192,7 @@ def login():
|
|||||||
_issue_login_nonce(record.username)
|
_issue_login_nonce(record.username)
|
||||||
clear_failures("login", identifier=client_ip)
|
clear_failures("login", identifier=client_ip)
|
||||||
try:
|
try:
|
||||||
state.container_manager.ensure_container(
|
state.container_manager.ensure_container(record.username, str(workspace.project_path), preferred_mode="docker")
|
||||||
record.username,
|
|
||||||
str(workspace.project_path),
|
|
||||||
container_key=f"{record.username}::default",
|
|
||||||
preferred_mode="docker",
|
|
||||||
)
|
|
||||||
except RuntimeError as exc:
|
except RuntimeError as exc:
|
||||||
session.clear()
|
session.clear()
|
||||||
return jsonify({"success": False, "error": str(exc), "code": "resource_busy"}), 503
|
return jsonify({"success": False, "error": str(exc), "code": "resource_busy"}), 503
|
||||||
@ -247,12 +241,7 @@ def host_login():
|
|||||||
|
|
||||||
# 预先创建宿主机模式的终端/容器句柄(host 模式不会启动 Docker)
|
# 预先创建宿主机模式的终端/容器句柄(host 模式不会启动 Docker)
|
||||||
try:
|
try:
|
||||||
state.container_manager.ensure_container(
|
state.container_manager.ensure_container("host", str(host_path), container_key="host", preferred_mode="host")
|
||||||
"host",
|
|
||||||
str(host_path),
|
|
||||||
container_key=f"host::{host_workspace_id}",
|
|
||||||
preferred_mode="host",
|
|
||||||
)
|
|
||||||
except RuntimeError as exc:
|
except RuntimeError as exc:
|
||||||
session.clear()
|
session.clear()
|
||||||
return jsonify({"success": False, "error": str(exc)}), 503
|
return jsonify({"success": False, "error": str(exc)}), 503
|
||||||
@ -468,10 +457,7 @@ def serve_user_upload(filename: str):
|
|||||||
user = get_current_user_record()
|
user = get_current_user_record()
|
||||||
if not user:
|
if not user:
|
||||||
return redirect('/login')
|
return redirect('/login')
|
||||||
workspace = state.user_manager.ensure_user_workspace(
|
workspace = state.user_manager.ensure_user_workspace(user.username)
|
||||||
user.username,
|
|
||||||
session.get("workspace_id") or "default",
|
|
||||||
)
|
|
||||||
uploads_dir = workspace.uploads_dir.resolve()
|
uploads_dir = workspace.uploads_dir.resolve()
|
||||||
target = (uploads_dir / filename).resolve()
|
target = (uploads_dir / filename).resolve()
|
||||||
try:
|
try:
|
||||||
@ -489,10 +475,7 @@ def serve_workspace_file(filename: str):
|
|||||||
user = get_current_user_record()
|
user = get_current_user_record()
|
||||||
if not user:
|
if not user:
|
||||||
return redirect('/login')
|
return redirect('/login')
|
||||||
workspace = state.user_manager.ensure_user_workspace(
|
workspace = state.user_manager.ensure_user_workspace(user.username)
|
||||||
user.username,
|
|
||||||
session.get("workspace_id") or "default",
|
|
||||||
)
|
|
||||||
project_root = workspace.project_path.resolve()
|
project_root = workspace.project_path.resolve()
|
||||||
target = (project_root / filename).resolve()
|
target = (project_root / filename).resolve()
|
||||||
try:
|
try:
|
||||||
|
|||||||
@ -50,19 +50,6 @@ def _make_terminal_key(username: str, workspace_id: Optional[str] = None) -> str
|
|||||||
return f"{username}::{workspace_id}" if workspace_id else username
|
return f"{username}::{workspace_id}" if workspace_id else username
|
||||||
|
|
||||||
|
|
||||||
def _set_terminal_workspace_label(terminal: WebTerminal, label: Optional[str]) -> None:
|
|
||||||
label_text = str(label or "").strip()
|
|
||||||
try:
|
|
||||||
terminal.workspace_label = label_text
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
try:
|
|
||||||
if getattr(terminal, "context_manager", None):
|
|
||||||
terminal.context_manager.workspace_label = label_text
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def _ensure_workspace_skills_synced(terminal: WebTerminal, workspace) -> None:
|
def _ensure_workspace_skills_synced(terminal: WebTerminal, workspace) -> None:
|
||||||
"""
|
"""
|
||||||
确保工作区 skills 已按当前个性化配置完成同步。
|
确保工作区 skills 已按当前个性化配置完成同步。
|
||||||
@ -103,25 +90,15 @@ def get_user_resources(username: Optional[str] = None, workspace_id: Optional[st
|
|||||||
host_mode_session = bool(session.get("host_mode")) if has_request_context() else False
|
host_mode_session = bool(session.get("host_mode")) if has_request_context() else False
|
||||||
sandbox_is_host = (TERMINAL_SANDBOX_MODE or "host").lower() == "host"
|
sandbox_is_host = (TERMINAL_SANDBOX_MODE or "host").lower() == "host"
|
||||||
if host_mode_session and sandbox_is_host:
|
if host_mode_session and sandbox_is_host:
|
||||||
# 宿主机多工作区并行:资源选择必须优先由显式 workspace_id / 当前请求 session 决定,
|
selected_workspace_id = session.get("host_workspace_id") if has_request_context() else None
|
||||||
# 不能被进程级 HOST_ACTIVE_WORKSPACE_ID 覆盖,否则后台任务会在用户切换视图后串到新工作区。
|
|
||||||
selected_workspace_id = (
|
|
||||||
workspace_id
|
|
||||||
or (session.get("host_workspace_id") if has_request_context() else None)
|
|
||||||
or (session.get("workspace_id") if has_request_context() else None)
|
|
||||||
)
|
|
||||||
with state.HOST_ACTIVE_WORKSPACE_LOCK:
|
with state.HOST_ACTIVE_WORKSPACE_LOCK:
|
||||||
active_workspace_id = state.HOST_ACTIVE_WORKSPACE_ID
|
active_workspace_id = state.HOST_ACTIVE_WORKSPACE_ID
|
||||||
active_workspace_path = state.HOST_ACTIVE_WORKSPACE_PATH
|
active_workspace_path = state.HOST_ACTIVE_WORKSPACE_PATH
|
||||||
active_workspace_version = state.HOST_ACTIVE_WORKSPACE_VERSION
|
active_workspace_version = state.HOST_ACTIVE_WORKSPACE_VERSION
|
||||||
if not selected_workspace_id and active_workspace_id:
|
if active_workspace_id:
|
||||||
selected_workspace_id = active_workspace_id
|
selected_workspace_id = active_workspace_id
|
||||||
_, host_workspace = resolve_host_workspace(selected_workspace_id)
|
_, host_workspace = resolve_host_workspace(selected_workspace_id)
|
||||||
if (
|
if active_workspace_id and active_workspace_path:
|
||||||
active_workspace_id
|
|
||||||
and active_workspace_path
|
|
||||||
and selected_workspace_id == active_workspace_id
|
|
||||||
):
|
|
||||||
host_workspace = dict(host_workspace)
|
host_workspace = dict(host_workspace)
|
||||||
host_workspace["workspace_id"] = active_workspace_id
|
host_workspace["workspace_id"] = active_workspace_id
|
||||||
host_workspace["path"] = active_workspace_path
|
host_workspace["path"] = active_workspace_path
|
||||||
@ -162,8 +139,7 @@ def get_user_resources(username: Optional[str] = None, workspace_id: Optional[st
|
|||||||
if not hasattr(workspace, "workspace_id"):
|
if not hasattr(workspace, "workspace_id"):
|
||||||
workspace.workspace_id = host_workspace.get("workspace_id") or "default"
|
workspace.workspace_id = host_workspace.get("workspace_id") or "default"
|
||||||
|
|
||||||
workspace_id_value = getattr(workspace, "workspace_id", None) or host_workspace.get("workspace_id") or "default"
|
term_key = "host"
|
||||||
term_key = _make_terminal_key("host", workspace_id_value)
|
|
||||||
container_handle = state.container_manager.ensure_container("host", str(project_path), container_key=term_key, preferred_mode="host")
|
container_handle = state.container_manager.ensure_container("host", str(project_path), container_key=term_key, preferred_mode="host")
|
||||||
usage_tracker = None # 宿主机模式不计配额
|
usage_tracker = None # 宿主机模式不计配额
|
||||||
terminal = state.user_terminals.get(term_key)
|
terminal = state.user_terminals.get(term_key)
|
||||||
@ -255,10 +231,6 @@ def get_user_resources(username: Optional[str] = None, workspace_id: Optional[st
|
|||||||
if has_request_context():
|
if has_request_context():
|
||||||
session['workspace_id'] = getattr(workspace, "workspace_id", None)
|
session['workspace_id'] = getattr(workspace, "workspace_id", None)
|
||||||
session['host_workspace_id'] = getattr(workspace, "workspace_id", None)
|
session['host_workspace_id'] = getattr(workspace, "workspace_id", None)
|
||||||
_set_terminal_workspace_label(
|
|
||||||
terminal,
|
|
||||||
host_workspace.get("label") or host_workspace.get("workspace_id") or getattr(workspace, "workspace_id", None),
|
|
||||||
)
|
|
||||||
|
|
||||||
# 宿主机模式同样需要应用管理员策略(否则前端工具菜单会退化成静态基础分类)
|
# 宿主机模式同样需要应用管理员策略(否则前端工具菜单会退化成静态基础分类)
|
||||||
try:
|
try:
|
||||||
@ -321,20 +293,14 @@ def get_user_resources(username: Optional[str] = None, workspace_id: Optional[st
|
|||||||
workspace = state.api_user_manager.ensure_workspace(username, workspace_id)
|
workspace = state.api_user_manager.ensure_workspace(username, workspace_id)
|
||||||
else:
|
else:
|
||||||
record = get_current_user_record()
|
record = get_current_user_record()
|
||||||
selected_workspace_id = (
|
workspace = state.user_manager.ensure_user_workspace(username)
|
||||||
workspace_id
|
|
||||||
or (session.get("workspace_id") if has_request_context() else None)
|
|
||||||
or "default"
|
|
||||||
)
|
|
||||||
workspace = state.user_manager.ensure_user_workspace(username, selected_workspace_id)
|
|
||||||
# 为兼容后续逻辑,补充 workspace_id 属性
|
# 为兼容后续逻辑,补充 workspace_id 属性
|
||||||
if not hasattr(workspace, "workspace_id"):
|
if not hasattr(workspace, "workspace_id"):
|
||||||
try:
|
try:
|
||||||
workspace.workspace_id = selected_workspace_id or "default"
|
workspace.workspace_id = "default"
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
workspace_id_value = getattr(workspace, "workspace_id", None) or "default"
|
term_key = _make_terminal_key(username, getattr(workspace, "workspace_id", None) if is_api_user else None)
|
||||||
term_key = _make_terminal_key(username, workspace_id_value)
|
|
||||||
container_handle = state.container_manager.ensure_container(username, str(workspace.project_path), container_key=term_key, preferred_mode="docker")
|
container_handle = state.container_manager.ensure_container(username, str(workspace.project_path), container_key=term_key, preferred_mode="docker")
|
||||||
usage_tracker = None if is_api_user else get_or_create_usage_tracker(username, workspace)
|
usage_tracker = None if is_api_user else get_or_create_usage_tracker(username, workspace)
|
||||||
terminal = state.user_terminals.get(term_key)
|
terminal = state.user_terminals.get(term_key)
|
||||||
@ -378,29 +344,17 @@ def get_user_resources(username: Optional[str] = None, workspace_id: Optional[st
|
|||||||
session['run_mode'] = terminal.run_mode
|
session['run_mode'] = terminal.run_mode
|
||||||
session['thinking_mode'] = terminal.thinking_mode
|
session['thinking_mode'] = terminal.thinking_mode
|
||||||
session['model_key'] = getattr(terminal, "model_key", None)
|
session['model_key'] = getattr(terminal, "model_key", None)
|
||||||
session['workspace_id'] = getattr(workspace, "workspace_id", None)
|
if is_api_user:
|
||||||
|
session['workspace_id'] = getattr(workspace, "workspace_id", None)
|
||||||
else:
|
else:
|
||||||
terminal.update_container_session(container_handle)
|
terminal.update_container_session(container_handle)
|
||||||
attach_user_broadcast(terminal, username)
|
attach_user_broadcast(terminal, username)
|
||||||
terminal.username = username
|
terminal.username = username
|
||||||
terminal.user_role = "api" if is_api_user else get_current_user_role(record)
|
terminal.user_role = "api" if is_api_user else get_current_user_role(record)
|
||||||
terminal.quota_update_callback = (lambda metric=None: emit_user_quota_update(username)) if not is_api_user else None
|
terminal.quota_update_callback = (lambda metric=None: emit_user_quota_update(username)) if not is_api_user else None
|
||||||
if has_request_context():
|
if has_request_context() and is_api_user:
|
||||||
session['workspace_id'] = getattr(workspace, "workspace_id", None)
|
session['workspace_id'] = getattr(workspace, "workspace_id", None)
|
||||||
|
|
||||||
if is_api_user:
|
|
||||||
workspace_label = workspace_id_value
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
workspace_label = (
|
|
||||||
state.user_manager.list_user_workspaces(username)
|
|
||||||
.get(workspace_id_value, {})
|
|
||||||
.get("label")
|
|
||||||
) or workspace_id_value
|
|
||||||
except Exception:
|
|
||||||
workspace_label = workspace_id_value
|
|
||||||
_set_terminal_workspace_label(terminal, workspace_label)
|
|
||||||
|
|
||||||
# 应用管理员策略
|
# 应用管理员策略
|
||||||
if not is_api_user:
|
if not is_api_user:
|
||||||
try:
|
try:
|
||||||
|
|||||||
@ -153,49 +153,6 @@ def _cancel_running_tasks(username: str, workspace_id: str, timeout_seconds: flo
|
|||||||
return canceled, False
|
return canceled, False
|
||||||
|
|
||||||
|
|
||||||
def _get_active_workspace_task(username: str, workspace_id: str):
|
|
||||||
"""返回当前工作区仍在运行/停止中的主任务。用于避免视图导航改写运行任务的 terminal 上下文。"""
|
|
||||||
try:
|
|
||||||
from .tasks import task_manager
|
|
||||||
active_statuses = {"pending", "running", "cancel_requested"}
|
|
||||||
tasks = [
|
|
||||||
rec for rec in task_manager.list_tasks(username, workspace_id)
|
|
||||||
if getattr(rec, "status", None) in active_statuses
|
|
||||||
]
|
|
||||||
tasks.sort(key=lambda rec: getattr(rec, "created_at", 0), reverse=True)
|
|
||||||
return tasks[0] if tasks else None
|
|
||||||
except Exception as exc:
|
|
||||||
debug_log(f"[ConversationSafeNav] 查询运行任务失败: {exc}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def _build_safe_load_result(terminal: WebTerminal, conversation_id: str) -> Dict[str, Any]:
|
|
||||||
"""只读取对话元数据,不调用 terminal.load_conversation,避免修改运行任务正在使用的上下文。"""
|
|
||||||
normalized_id = _normalize_conv_id(conversation_id)
|
|
||||||
cm = getattr(getattr(terminal, "context_manager", None), "conversation_manager", None)
|
|
||||||
data = cm.load_conversation(normalized_id) if cm else None
|
|
||||||
if not data:
|
|
||||||
return {
|
|
||||||
"success": False,
|
|
||||||
"error": "对话不存在或加载失败",
|
|
||||||
"message": f"对话加载失败: {normalized_id}",
|
|
||||||
}
|
|
||||||
meta = data.get("metadata", {}) or {}
|
|
||||||
run_mode = meta.get("run_mode") or getattr(terminal, "run_mode", "fast")
|
|
||||||
thinking_mode = bool(meta.get("thinking_mode", run_mode != "fast"))
|
|
||||||
return {
|
|
||||||
"success": True,
|
|
||||||
"conversation_id": normalized_id,
|
|
||||||
"title": data.get("title", "未知对话"),
|
|
||||||
"messages_count": len(data.get("messages", []) or []),
|
|
||||||
"run_mode": run_mode,
|
|
||||||
"thinking_mode": thinking_mode,
|
|
||||||
"model_key": meta.get("model_key") or getattr(terminal, "model_key", None),
|
|
||||||
"message": f"对话已加载: {normalized_id}",
|
|
||||||
"safe_navigation": True,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def _normalize_conv_id(conversation_id: str) -> str:
|
def _normalize_conv_id(conversation_id: str) -> str:
|
||||||
conv = (conversation_id or "").strip()
|
conv = (conversation_id or "").strip()
|
||||||
if not conv:
|
if not conv:
|
||||||
@ -245,10 +202,9 @@ def _resolve_input_draft_path(workspace: UserWorkspace, username: str) -> Tuple[
|
|||||||
filename = f"{_sanitize_scope_component(workspace_id)}.json"
|
filename = f"{_sanitize_scope_component(workspace_id)}.json"
|
||||||
return (base_dir / "host" / filename).resolve(), scope
|
return (base_dir / "host" / filename).resolve(), scope
|
||||||
safe_user = _sanitize_scope_component(username or "user", default="user")
|
safe_user = _sanitize_scope_component(username or "user", default="user")
|
||||||
workspace_id = getattr(workspace, "workspace_id", None) or session.get("workspace_id") or "default"
|
scope = f"user:{safe_user}"
|
||||||
safe_workspace = _sanitize_scope_component(workspace_id)
|
# docker 模式下 data_dir 本身已按用户隔离,这里固定单文件即可。
|
||||||
scope = f"user:{safe_user}:project:{safe_workspace}"
|
return (base_dir / "docker" / "draft.json").resolve(), scope
|
||||||
return (base_dir / "docker" / f"{safe_workspace}.json").resolve(), scope
|
|
||||||
|
|
||||||
|
|
||||||
def _read_input_draft_payload(path: Path) -> Dict[str, Any]:
|
def _read_input_draft_payload(path: Path) -> Dict[str, Any]:
|
||||||
@ -437,49 +393,19 @@ def create_conversation(terminal: WebTerminal, workspace: UserWorkspace, usernam
|
|||||||
thinking_mode = data.get('thinking_mode') if preserve_mode and 'thinking_mode' in data else None
|
thinking_mode = data.get('thinking_mode') if preserve_mode and 'thinking_mode' in data else None
|
||||||
run_mode = data.get('mode') if preserve_mode and 'mode' in data else None
|
run_mode = data.get('mode') if preserve_mode and 'mode' in data else None
|
||||||
|
|
||||||
# 多工作区并行后,新建/切换对话只是前端视图导航,不应停止同工作区正在运行的主任务。
|
|
||||||
# 同工作区单任务互斥由 /api/tasks 创建任务时兜底限制;输入框禁用由前端根据任务状态处理。
|
|
||||||
workspace_id = session.get("workspace_id") or "default"
|
workspace_id = session.get("workspace_id") or "default"
|
||||||
active_task = _get_active_workspace_task(username=username, workspace_id=workspace_id)
|
canceled_count, settled = _cancel_running_tasks(username=username, workspace_id=workspace_id)
|
||||||
if active_task:
|
if canceled_count:
|
||||||
# 运行中时只能创建“视图用”的新对话文件,不能调用 terminal.create_new_conversation。
|
debug_log(f"[TaskCancel] 创建新对话前取消任务: {canceled_count}")
|
||||||
# 后者会切换 context_manager.current_conversation_id,导致运行任务后续内容串写到新对话。
|
if not settled:
|
||||||
cm = getattr(getattr(terminal, "context_manager", None), "conversation_manager", None)
|
return jsonify({
|
||||||
if not cm:
|
"success": False,
|
||||||
return jsonify({"success": False, "error": "对话管理器未初始化"}), 500
|
"error": "任务正在停止中,请稍后再试",
|
||||||
try:
|
"message": "检测到后台任务仍在停止,请稍后再创建新对话。"
|
||||||
prefs = load_personalization_config(workspace.data_dir)
|
}), 409
|
||||||
except Exception:
|
|
||||||
prefs = {}
|
_terminate_running_sub_agents(terminal, reason="用户创建新对话")
|
||||||
safe_run_mode = run_mode
|
result = terminal.create_new_conversation(thinking_mode=thinking_mode, run_mode=run_mode)
|
||||||
if safe_run_mode not in {"fast", "thinking", "deep"}:
|
|
||||||
candidate = (prefs or {}).get("default_run_mode")
|
|
||||||
safe_run_mode = candidate if candidate in {"fast", "thinking", "deep"} else "fast"
|
|
||||||
safe_thinking = bool(thinking_mode) if thinking_mode is not None else safe_run_mode != "fast"
|
|
||||||
previous_cm_current = getattr(cm, "current_conversation_id", None)
|
|
||||||
conversation_id = cm.create_conversation(
|
|
||||||
project_path=str(workspace.project_path),
|
|
||||||
thinking_mode=safe_thinking,
|
|
||||||
run_mode=safe_run_mode,
|
|
||||||
initial_messages=[],
|
|
||||||
model_key=(prefs or {}).get("default_model") or getattr(terminal, "model_key", None),
|
|
||||||
metadata_overrides={
|
|
||||||
"permission_mode": getattr(terminal, "get_permission_mode", lambda: "unrestricted")(),
|
|
||||||
"execution_mode": getattr(terminal, "get_execution_mode", lambda: "sandbox")(),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
cm.current_conversation_id = previous_cm_current
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
result = {
|
|
||||||
"success": True,
|
|
||||||
"conversation_id": conversation_id,
|
|
||||||
"message": f"已创建新对话: {conversation_id}",
|
|
||||||
"safe_navigation": True,
|
|
||||||
}
|
|
||||||
else:
|
|
||||||
result = terminal.create_new_conversation(thinking_mode=thinking_mode, run_mode=run_mode)
|
|
||||||
|
|
||||||
if result["success"]:
|
if result["success"]:
|
||||||
session['run_mode'] = terminal.run_mode
|
session['run_mode'] = terminal.run_mode
|
||||||
@ -490,12 +416,11 @@ def create_conversation(terminal: WebTerminal, workspace: UserWorkspace, usernam
|
|||||||
'conversation_id': result["conversation_id"]
|
'conversation_id': result["conversation_id"]
|
||||||
}, room=f"user_{username}")
|
}, room=f"user_{username}")
|
||||||
|
|
||||||
if not result.get("safe_navigation"):
|
# 广播当前对话切换事件
|
||||||
# 安全导航创建不代表后端 terminal 当前对话已切换,因此不广播 conversation_changed。
|
socketio.emit('conversation_changed', {
|
||||||
socketio.emit('conversation_changed', {
|
'conversation_id': result["conversation_id"],
|
||||||
'conversation_id': result["conversation_id"],
|
'title': "新对话"
|
||||||
'title': "新对话"
|
}, room=f"user_{username}")
|
||||||
}, room=f"user_{username}")
|
|
||||||
|
|
||||||
return jsonify(result), 201
|
return jsonify(result), 201
|
||||||
else:
|
else:
|
||||||
@ -554,14 +479,20 @@ def get_conversation_info(terminal: WebTerminal, workspace: UserWorkspace, usern
|
|||||||
def load_conversation(conversation_id, terminal: WebTerminal, workspace: UserWorkspace, username: str):
|
def load_conversation(conversation_id, terminal: WebTerminal, workspace: UserWorkspace, username: str):
|
||||||
"""加载特定对话"""
|
"""加载特定对话"""
|
||||||
try:
|
try:
|
||||||
workspace_id = session.get("workspace_id") or "default"
|
current_id = getattr(getattr(terminal, "context_manager", None), "current_conversation_id", None)
|
||||||
active_task = _get_active_workspace_task(username=username, workspace_id=workspace_id)
|
if current_id and current_id != conversation_id:
|
||||||
if active_task:
|
workspace_id = session.get("workspace_id") or "default"
|
||||||
# 同工作区有任务运行时,加载/查看其他对话不能改后端 terminal 当前上下文。
|
canceled_count, settled = _cancel_running_tasks(username=username, workspace_id=workspace_id)
|
||||||
# 否则运行任务后续保存与事件归属会被切到当前查看的对话。
|
if canceled_count:
|
||||||
result = _build_safe_load_result(terminal, conversation_id)
|
debug_log(f"[TaskCancel] 切换对话前取消任务: {canceled_count}")
|
||||||
else:
|
if not settled:
|
||||||
result = terminal.load_conversation(conversation_id)
|
return jsonify({
|
||||||
|
"success": False,
|
||||||
|
"error": "任务正在停止中,请稍后再试",
|
||||||
|
"message": "检测到后台任务仍在停止,请稍后再切换对话。"
|
||||||
|
}), 409
|
||||||
|
_terminate_running_sub_agents(terminal, reason="用户切换对话")
|
||||||
|
result = terminal.load_conversation(conversation_id)
|
||||||
|
|
||||||
if result["success"]:
|
if result["success"]:
|
||||||
session['run_mode'] = terminal.run_mode
|
session['run_mode'] = terminal.run_mode
|
||||||
@ -609,23 +540,22 @@ def load_conversation(conversation_id, terminal: WebTerminal, workspace: UserWor
|
|||||||
"error": str(exc),
|
"error": str(exc),
|
||||||
}
|
}
|
||||||
|
|
||||||
if not result.get("safe_navigation"):
|
# 广播对话切换事件
|
||||||
# 安全导航只改变前端查看对象,不代表后端 terminal 当前上下文已切换。
|
socketio.emit('conversation_changed', {
|
||||||
socketio.emit('conversation_changed', {
|
'conversation_id': conversation_id,
|
||||||
'conversation_id': conversation_id,
|
'title': result.get("title", "未知对话"),
|
||||||
'title': result.get("title", "未知对话"),
|
'messages_count': result.get("messages_count", 0)
|
||||||
'messages_count': result.get("messages_count", 0)
|
}, room=f"user_{username}")
|
||||||
}, room=f"user_{username}")
|
|
||||||
|
|
||||||
# 广播系统状态更新(因为当前对话改变了)
|
# 广播系统状态更新(因为当前对话改变了)
|
||||||
status = terminal.get_status()
|
status = terminal.get_status()
|
||||||
socketio.emit('status_update', status, room=f"user_{username}")
|
socketio.emit('status_update', status, room=f"user_{username}")
|
||||||
|
|
||||||
# 清理和重置相关UI状态
|
# 清理和重置相关UI状态
|
||||||
socketio.emit('conversation_loaded', {
|
socketio.emit('conversation_loaded', {
|
||||||
'conversation_id': conversation_id,
|
'conversation_id': conversation_id,
|
||||||
'clear_ui': True # 提示前端清理当前UI状态
|
'clear_ui': True # 提示前端清理当前UI状态
|
||||||
}, room=f"user_{username}")
|
}, room=f"user_{username}")
|
||||||
|
|
||||||
return jsonify(result)
|
return jsonify(result)
|
||||||
else:
|
else:
|
||||||
|
|||||||
422
server/status.py
422
server/status.py
@ -17,9 +17,7 @@ from .state import (
|
|||||||
from config import AGENT_VERSION, TERMINAL_SANDBOX_MODE
|
from config import AGENT_VERSION, TERMINAL_SANDBOX_MODE
|
||||||
from modules.host_workspace_manager import (
|
from modules.host_workspace_manager import (
|
||||||
create_host_workspace,
|
create_host_workspace,
|
||||||
delete_host_workspace,
|
|
||||||
load_host_workspace_catalog,
|
load_host_workspace_catalog,
|
||||||
rename_host_workspace,
|
|
||||||
resolve_host_workspace,
|
resolve_host_workspace,
|
||||||
)
|
)
|
||||||
from utils.host_workspace_debug import write_host_workspace_debug
|
from utils.host_workspace_debug import write_host_workspace_debug
|
||||||
@ -31,46 +29,6 @@ STATUS_DIAG_VERBOSE = os.environ.get("STATUS_DIAG_VERBOSE", "0") not in {"0", "f
|
|||||||
STATUS_DIAG_SLOW_MS = int(os.environ.get("STATUS_DIAG_SLOW_MS", "1200") or 1200)
|
STATUS_DIAG_SLOW_MS = int(os.environ.get("STATUS_DIAG_SLOW_MS", "1200") or 1200)
|
||||||
|
|
||||||
|
|
||||||
def _close_terminal_for_key(term_key: str):
|
|
||||||
terminal = state.user_terminals.pop(term_key, None)
|
|
||||||
if terminal:
|
|
||||||
try:
|
|
||||||
if getattr(terminal, "terminal_manager", None):
|
|
||||||
terminal.terminal_manager.close_all()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def _build_docker_projects_payload(username: str, current_id: str) -> list[dict]:
|
|
||||||
running_by_workspace = _active_task_counts(username)
|
|
||||||
projects = []
|
|
||||||
for ws_id, item in user_manager.list_user_workspaces(username).items():
|
|
||||||
projects.append({
|
|
||||||
"workspace_id": ws_id,
|
|
||||||
"label": item.get("label") or ("默认项目" if ws_id == "default" else ws_id),
|
|
||||||
"path": "",
|
|
||||||
"is_current": ws_id == current_id,
|
|
||||||
"is_default": ws_id == "default",
|
|
||||||
"running_task_count": int(running_by_workspace.get(ws_id, 0)),
|
|
||||||
})
|
|
||||||
return projects
|
|
||||||
|
|
||||||
|
|
||||||
def _build_host_workspaces_payload(catalog: dict, current_id: str) -> list[dict]:
|
|
||||||
running_by_workspace = _active_task_counts("host")
|
|
||||||
workspaces = []
|
|
||||||
for item in catalog.get("workspaces") or []:
|
|
||||||
ws_id = item.get("workspace_id")
|
|
||||||
workspaces.append({
|
|
||||||
"workspace_id": ws_id,
|
|
||||||
"label": item.get("label"),
|
|
||||||
"path": item.get("path"),
|
|
||||||
"is_current": ws_id == current_id,
|
|
||||||
"running_task_count": int(running_by_workspace.get(ws_id, 0)),
|
|
||||||
})
|
|
||||||
return workspaces
|
|
||||||
|
|
||||||
|
|
||||||
def _parse_android_version_from_gradle(project_root: Path) -> tuple[int, str]:
|
def _parse_android_version_from_gradle(project_root: Path) -> tuple[int, str]:
|
||||||
"""从 android-webview-app/app/build.gradle.kts 读取 versionCode/versionName。"""
|
"""从 android-webview-app/app/build.gradle.kts 读取 versionCode/versionName。"""
|
||||||
gradle_file = project_root / "android-webview-app" / "app" / "build.gradle.kts"
|
gradle_file = project_root / "android-webview-app" / "app" / "build.gradle.kts"
|
||||||
@ -111,23 +69,6 @@ def _is_host_mode_request() -> bool:
|
|||||||
return bool(session.get("host_mode")) and (TERMINAL_SANDBOX_MODE or "").lower() == "host"
|
return bool(session.get("host_mode")) and (TERMINAL_SANDBOX_MODE or "").lower() == "host"
|
||||||
|
|
||||||
|
|
||||||
def _is_docker_project_request() -> bool:
|
|
||||||
return bool(session.get("username")) and not bool(session.get("host_mode")) and not bool(session.get("is_api_user"))
|
|
||||||
|
|
||||||
|
|
||||||
def _active_task_counts(username: str) -> dict:
|
|
||||||
running_by_workspace = {}
|
|
||||||
try:
|
|
||||||
from .tasks import task_manager
|
|
||||||
for rec in task_manager.list_tasks(username):
|
|
||||||
if rec.status in {"pending", "running", "cancel_requested"}:
|
|
||||||
ws_id = getattr(rec, "workspace_id", None) or "default"
|
|
||||||
running_by_workspace[ws_id] = running_by_workspace.get(ws_id, 0) + 1
|
|
||||||
except Exception:
|
|
||||||
running_by_workspace = {}
|
|
||||||
return running_by_workspace
|
|
||||||
|
|
||||||
|
|
||||||
@status_bp.route('/api/health')
|
@status_bp.route('/api/health')
|
||||||
@api_login_required
|
@api_login_required
|
||||||
def get_health():
|
def get_health():
|
||||||
@ -208,13 +149,7 @@ def get_status(terminal, workspace, username):
|
|||||||
try:
|
try:
|
||||||
# 首屏状态只需要容器是否存在/运行等轻量信息;Docker stats 很慢,
|
# 首屏状态只需要容器是否存在/运行等轻量信息;Docker stats 很慢,
|
||||||
# 由 /api/container-status 在用量面板需要时单独拉取。
|
# 由 /api/container-status 在用量面板需要时单独拉取。
|
||||||
workspace_id = getattr(workspace, 'workspace_id', None) or session.get('workspace_id') or 'default'
|
status['container'] = container_manager.get_container_status(username, include_stats=False)
|
||||||
container_key = (
|
|
||||||
f"host::{workspace_id}"
|
|
||||||
if bool(session.get("host_mode")) or username == "host"
|
|
||||||
else f"{username}::{workspace_id}"
|
|
||||||
)
|
|
||||||
status['container'] = container_manager.get_container_status(container_key, include_stats=False)
|
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
status['container'] = {"success": False, "error": str(exc)}
|
status['container'] = {"success": False, "error": str(exc)}
|
||||||
log_conn_diag(f"status container-status-failed user={username} error={exc}")
|
log_conn_diag(f"status container-status-failed user={username} error={exc}")
|
||||||
@ -259,7 +194,14 @@ def list_host_workspaces():
|
|||||||
session['host_workspace_id'] = current_id
|
session['host_workspace_id'] = current_id
|
||||||
session['workspace_id'] = current_id
|
session['workspace_id'] = current_id
|
||||||
|
|
||||||
workspaces = _build_host_workspaces_payload(catalog, current_id)
|
workspaces = []
|
||||||
|
for item in catalog.get("workspaces") or []:
|
||||||
|
workspaces.append({
|
||||||
|
"workspace_id": item.get("workspace_id"),
|
||||||
|
"label": item.get("label"),
|
||||||
|
"path": item.get("path"),
|
||||||
|
"is_current": item.get("workspace_id") == current_id,
|
||||||
|
})
|
||||||
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
"success": True,
|
"success": True,
|
||||||
@ -272,194 +214,6 @@ def list_host_workspaces():
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@status_bp.route('/api/projects')
|
|
||||||
@api_login_required
|
|
||||||
def list_docker_projects():
|
|
||||||
if not _is_docker_project_request():
|
|
||||||
return jsonify({"success": False, "error": "仅 Docker Web 模式可用"}), 403
|
|
||||||
username = session.get("username")
|
|
||||||
current_id = (session.get("workspace_id") or "default").strip() or "default"
|
|
||||||
# 默认项目用于向后兼容旧 Docker Web 文件/对话。
|
|
||||||
user_manager.ensure_user_workspace(username, current_id)
|
|
||||||
projects = _build_docker_projects_payload(username, current_id)
|
|
||||||
return jsonify({
|
|
||||||
"success": True,
|
|
||||||
"data": {
|
|
||||||
"default_workspace_id": "default",
|
|
||||||
"current_workspace_id": current_id,
|
|
||||||
"workspaces": projects,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
@status_bp.route('/api/projects/select', methods=['GET', 'POST'])
|
|
||||||
@api_login_required
|
|
||||||
def select_docker_project():
|
|
||||||
if not _is_docker_project_request():
|
|
||||||
return jsonify({"success": False, "error": "仅 Docker Web 模式可用"}), 403
|
|
||||||
payload = request.get_json(silent=True) if request.method != "GET" else None
|
|
||||||
workspace_id = (
|
|
||||||
request.args.get("workspace_id")
|
|
||||||
or (payload or {}).get("workspace_id")
|
|
||||||
or ""
|
|
||||||
).strip()
|
|
||||||
if not workspace_id:
|
|
||||||
return jsonify({"success": False, "error": "缺少项目 ID"}), 400
|
|
||||||
username = session.get("username")
|
|
||||||
previous_workspace_id = session.get("workspace_id") or "default"
|
|
||||||
known_projects = user_manager.list_user_workspaces(username)
|
|
||||||
if workspace_id not in known_projects:
|
|
||||||
return jsonify({"success": False, "error": "项目不存在"}), 404
|
|
||||||
try:
|
|
||||||
workspace = user_manager.ensure_user_workspace(username, workspace_id)
|
|
||||||
except ValueError as exc:
|
|
||||||
return jsonify({"success": False, "error": str(exc)}), 400
|
|
||||||
session["workspace_id"] = getattr(workspace, "workspace_id", workspace_id)
|
|
||||||
try:
|
|
||||||
container_manager.ensure_container(
|
|
||||||
username,
|
|
||||||
str(workspace.project_path),
|
|
||||||
container_key=f"{username}::{session['workspace_id']}",
|
|
||||||
preferred_mode="docker",
|
|
||||||
)
|
|
||||||
except RuntimeError as exc:
|
|
||||||
session["workspace_id"] = previous_workspace_id
|
|
||||||
return jsonify({"success": False, "error": str(exc)}), 503
|
|
||||||
terminal = state.user_terminals.get(f"{username}::{session['workspace_id']}")
|
|
||||||
if terminal:
|
|
||||||
try:
|
|
||||||
attach_user_broadcast(terminal, username)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return jsonify({
|
|
||||||
"success": True,
|
|
||||||
"data": {
|
|
||||||
"current_workspace_id": session["workspace_id"],
|
|
||||||
"project_path": "",
|
|
||||||
"default_workspace_id": "default",
|
|
||||||
"reloaded": previous_workspace_id != session["workspace_id"],
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
@status_bp.route('/api/projects/create', methods=['POST'])
|
|
||||||
@api_login_required
|
|
||||||
def create_docker_project():
|
|
||||||
if not _is_docker_project_request():
|
|
||||||
return jsonify({"success": False, "error": "仅 Docker Web 模式可用"}), 403
|
|
||||||
payload = request.get_json(silent=True) or {}
|
|
||||||
label = (payload.get("label") or payload.get("name") or "").strip()
|
|
||||||
if not label:
|
|
||||||
return jsonify({"success": False, "error": "项目名称不能为空"}), 400
|
|
||||||
username = session.get("username")
|
|
||||||
try:
|
|
||||||
workspace = user_manager.create_user_workspace(username, "", label=label)
|
|
||||||
except ValueError as exc:
|
|
||||||
return jsonify({"success": False, "error": str(exc)}), 400
|
|
||||||
projects = []
|
|
||||||
current_id = session.get("workspace_id") or "default"
|
|
||||||
for ws_id, item in user_manager.list_user_workspaces(username).items():
|
|
||||||
projects.append({
|
|
||||||
"workspace_id": ws_id,
|
|
||||||
"label": item.get("label") or ws_id,
|
|
||||||
"path": "",
|
|
||||||
"is_current": ws_id == current_id,
|
|
||||||
"is_default": ws_id == "default",
|
|
||||||
})
|
|
||||||
return jsonify({
|
|
||||||
"success": True,
|
|
||||||
"data": {
|
|
||||||
"created": True,
|
|
||||||
"workspace": {
|
|
||||||
"workspace_id": getattr(workspace, "workspace_id", ""),
|
|
||||||
"label": label,
|
|
||||||
"path": "",
|
|
||||||
},
|
|
||||||
"default_workspace_id": "default",
|
|
||||||
"current_workspace_id": current_id,
|
|
||||||
"workspaces": projects,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
@status_bp.route('/api/projects/rename', methods=['POST'])
|
|
||||||
@api_login_required
|
|
||||||
def rename_docker_project():
|
|
||||||
if not _is_docker_project_request():
|
|
||||||
return jsonify({"success": False, "error": "仅 Docker Web 模式可用"}), 403
|
|
||||||
payload = request.get_json(silent=True) or {}
|
|
||||||
workspace_id = (payload.get("workspace_id") or "").strip()
|
|
||||||
label = (payload.get("label") or payload.get("name") or "").strip()
|
|
||||||
if not workspace_id:
|
|
||||||
return jsonify({"success": False, "error": "缺少项目 ID"}), 400
|
|
||||||
if not label:
|
|
||||||
return jsonify({"success": False, "error": "项目名称不能为空"}), 400
|
|
||||||
username = session.get("username")
|
|
||||||
try:
|
|
||||||
workspace = user_manager.rename_user_workspace(username, workspace_id, label)
|
|
||||||
except ValueError as exc:
|
|
||||||
return jsonify({"success": False, "error": str(exc)}), 400
|
|
||||||
current_id = session.get("workspace_id") or "default"
|
|
||||||
return jsonify({
|
|
||||||
"success": True,
|
|
||||||
"data": {
|
|
||||||
"workspace": workspace,
|
|
||||||
"default_workspace_id": "default",
|
|
||||||
"current_workspace_id": current_id,
|
|
||||||
"workspaces": _build_docker_projects_payload(username, current_id),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
@status_bp.route('/api/projects/delete', methods=['POST'])
|
|
||||||
@api_login_required
|
|
||||||
def delete_docker_project():
|
|
||||||
if not _is_docker_project_request():
|
|
||||||
return jsonify({"success": False, "error": "仅 Docker Web 模式可用"}), 403
|
|
||||||
payload = request.get_json(silent=True) or {}
|
|
||||||
workspace_id = (payload.get("workspace_id") or "").strip()
|
|
||||||
if not workspace_id:
|
|
||||||
return jsonify({"success": False, "error": "缺少项目 ID"}), 400
|
|
||||||
if workspace_id == "default":
|
|
||||||
return jsonify({"success": False, "error": "默认项目不能删除"}), 400
|
|
||||||
username = session.get("username")
|
|
||||||
if _active_task_counts(username).get(workspace_id):
|
|
||||||
return jsonify({"success": False, "error": "该项目有运行中的任务,暂不能删除"}), 409
|
|
||||||
known_projects = user_manager.list_user_workspaces(username)
|
|
||||||
if workspace_id not in known_projects:
|
|
||||||
return jsonify({"success": False, "error": "项目不存在"}), 404
|
|
||||||
term_key = f"{username}::{workspace_id}"
|
|
||||||
try:
|
|
||||||
_close_terminal_for_key(term_key)
|
|
||||||
container_manager.release_container(term_key, reason="delete_project")
|
|
||||||
user_manager.delete_user_workspace(username, workspace_id)
|
|
||||||
except ValueError as exc:
|
|
||||||
return jsonify({"success": False, "error": str(exc)}), 400
|
|
||||||
current_id = session.get("workspace_id") or "default"
|
|
||||||
if current_id == workspace_id:
|
|
||||||
current_id = "default"
|
|
||||||
session["workspace_id"] = current_id
|
|
||||||
try:
|
|
||||||
workspace = user_manager.ensure_user_workspace(username, current_id)
|
|
||||||
container_manager.ensure_container(
|
|
||||||
username,
|
|
||||||
str(workspace.project_path),
|
|
||||||
container_key=f"{username}::{current_id}",
|
|
||||||
preferred_mode="docker",
|
|
||||||
)
|
|
||||||
except RuntimeError as exc:
|
|
||||||
return jsonify({"success": False, "error": str(exc)}), 503
|
|
||||||
return jsonify({
|
|
||||||
"success": True,
|
|
||||||
"data": {
|
|
||||||
"deleted_workspace_id": workspace_id,
|
|
||||||
"default_workspace_id": "default",
|
|
||||||
"current_workspace_id": current_id,
|
|
||||||
"workspaces": _build_docker_projects_payload(username, current_id),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
@status_bp.route('/api/host/workspaces/select', methods=['GET', 'POST'])
|
@status_bp.route('/api/host/workspaces/select', methods=['GET', 'POST'])
|
||||||
@api_login_required
|
@api_login_required
|
||||||
def select_host_workspace():
|
def select_host_workspace():
|
||||||
@ -496,6 +250,60 @@ def select_host_workspace():
|
|||||||
current_workspace_id=current_id,
|
current_workspace_id=current_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if current_id == workspace_id:
|
||||||
|
with state.HOST_ACTIVE_WORKSPACE_LOCK:
|
||||||
|
state.HOST_ACTIVE_WORKSPACE_ID = workspace_id
|
||||||
|
state.HOST_ACTIVE_WORKSPACE_PATH = target_path
|
||||||
|
state.HOST_ACTIVE_WORKSPACE_VERSION += 1
|
||||||
|
session['workspace_id'] = workspace_id
|
||||||
|
try:
|
||||||
|
container_handle = state.container_manager.ensure_container(
|
||||||
|
"host",
|
||||||
|
target_path,
|
||||||
|
container_key="host",
|
||||||
|
preferred_mode="host",
|
||||||
|
)
|
||||||
|
host_terminal = state.user_terminals.get("host")
|
||||||
|
if host_terminal:
|
||||||
|
host_terminal.update_project_path(target_path)
|
||||||
|
host_terminal.update_container_session(container_handle)
|
||||||
|
attach_user_broadcast(host_terminal, "host")
|
||||||
|
host_terminal.username = "host"
|
||||||
|
host_terminal.user_role = "admin"
|
||||||
|
write_host_workspace_debug(
|
||||||
|
"status.select_host_workspace.same_id_reapply",
|
||||||
|
terminal_id=id(host_terminal),
|
||||||
|
workspace_id=workspace_id,
|
||||||
|
project_path=str(getattr(host_terminal, "project_path", "")),
|
||||||
|
context_project_path=str(getattr(getattr(host_terminal, "context_manager", None), "project_path", "")),
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return jsonify({
|
||||||
|
"success": True,
|
||||||
|
"data": {
|
||||||
|
"current_workspace_id": workspace_id,
|
||||||
|
"project_path": target_path,
|
||||||
|
"default_workspace_id": catalog.get("default_workspace_id"),
|
||||||
|
"reloaded": False,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
try:
|
||||||
|
from .tasks import task_manager
|
||||||
|
|
||||||
|
running = [
|
||||||
|
rec for rec in task_manager.list_tasks("host")
|
||||||
|
if rec.status in {"pending", "running", "cancel_requested"}
|
||||||
|
]
|
||||||
|
if running:
|
||||||
|
return jsonify({
|
||||||
|
"success": False,
|
||||||
|
"error": "存在运行中的任务,请先停止后再切换工作区",
|
||||||
|
}), 409
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
previous_workspace_id = session.get("workspace_id")
|
previous_workspace_id = session.get("workspace_id")
|
||||||
with state.HOST_ACTIVE_WORKSPACE_LOCK:
|
with state.HOST_ACTIVE_WORKSPACE_LOCK:
|
||||||
state.HOST_ACTIVE_WORKSPACE_ID = workspace_id
|
state.HOST_ACTIVE_WORKSPACE_ID = workspace_id
|
||||||
@ -508,7 +316,7 @@ def select_host_workspace():
|
|||||||
container_handle = state.container_manager.ensure_container(
|
container_handle = state.container_manager.ensure_container(
|
||||||
"host",
|
"host",
|
||||||
target_path,
|
target_path,
|
||||||
container_key=f"host::{workspace_id}",
|
container_key="host",
|
||||||
preferred_mode="host",
|
preferred_mode="host",
|
||||||
)
|
)
|
||||||
except RuntimeError as exc:
|
except RuntimeError as exc:
|
||||||
@ -526,9 +334,10 @@ def select_host_workspace():
|
|||||||
)
|
)
|
||||||
return jsonify({"success": False, "error": str(exc)}), 503
|
return jsonify({"success": False, "error": str(exc)}), 503
|
||||||
|
|
||||||
host_terminal = state.user_terminals.get(f"host::{workspace_id}")
|
host_terminal = state.user_terminals.get("host")
|
||||||
if host_terminal:
|
if host_terminal:
|
||||||
try:
|
try:
|
||||||
|
host_terminal.update_project_path(target_path)
|
||||||
host_terminal.update_container_session(container_handle)
|
host_terminal.update_container_session(container_handle)
|
||||||
attach_user_broadcast(host_terminal, "host")
|
attach_user_broadcast(host_terminal, "host")
|
||||||
host_terminal.username = "host"
|
host_terminal.username = "host"
|
||||||
@ -547,7 +356,7 @@ def select_host_workspace():
|
|||||||
host_terminal.terminal_manager.close_all()
|
host_terminal.terminal_manager.close_all()
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
state.user_terminals.pop(f"host::{workspace_id}", None)
|
state.user_terminals.pop("host", None)
|
||||||
write_host_workspace_debug(
|
write_host_workspace_debug(
|
||||||
"status.select_host_workspace.drop_terminal_after_error",
|
"status.select_host_workspace.drop_terminal_after_error",
|
||||||
workspace_id=workspace_id,
|
workspace_id=workspace_id,
|
||||||
@ -564,7 +373,7 @@ def select_host_workspace():
|
|||||||
"current_workspace_id": workspace_id,
|
"current_workspace_id": workspace_id,
|
||||||
"project_path": target_path,
|
"project_path": target_path,
|
||||||
"default_workspace_id": catalog.get("default_workspace_id"),
|
"default_workspace_id": catalog.get("default_workspace_id"),
|
||||||
"reloaded": current_id != workspace_id,
|
"reloaded": True,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -625,97 +434,12 @@ def create_host_workspace_api():
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@status_bp.route('/api/host/workspaces/rename', methods=['POST'])
|
|
||||||
@api_login_required
|
|
||||||
def rename_host_workspace_api():
|
|
||||||
if not _is_host_mode_request():
|
|
||||||
return jsonify({"success": False, "error": "仅宿主机模式可用"}), 403
|
|
||||||
|
|
||||||
payload = request.get_json(silent=True) or {}
|
|
||||||
workspace_id = (payload.get("workspace_id") or "").strip()
|
|
||||||
label = (payload.get("label") or payload.get("name") or "").strip()
|
|
||||||
if not workspace_id:
|
|
||||||
return jsonify({"success": False, "error": "缺少 workspace_id"}), 400
|
|
||||||
if not label:
|
|
||||||
return jsonify({"success": False, "error": "工作区名称不能为空"}), 400
|
|
||||||
try:
|
|
||||||
result = rename_host_workspace(workspace_id, label)
|
|
||||||
except ValueError as exc:
|
|
||||||
return jsonify({"success": False, "error": str(exc)}), 400
|
|
||||||
except Exception as exc:
|
|
||||||
return jsonify({"success": False, "error": str(exc)}), 500
|
|
||||||
catalog = result.get("catalog") or load_host_workspace_catalog()
|
|
||||||
current_id = (session.get("host_workspace_id") or "").strip()
|
|
||||||
return jsonify({
|
|
||||||
"success": True,
|
|
||||||
"data": {
|
|
||||||
"workspace": result.get("workspace") or {},
|
|
||||||
"source_path": catalog.get("source_path"),
|
|
||||||
"default_workspace_id": catalog.get("default_workspace_id"),
|
|
||||||
"current_workspace_id": current_id,
|
|
||||||
"workspaces": _build_host_workspaces_payload(catalog, current_id),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
@status_bp.route('/api/host/workspaces/delete', methods=['POST'])
|
|
||||||
@api_login_required
|
|
||||||
def delete_host_workspace_api():
|
|
||||||
if not _is_host_mode_request():
|
|
||||||
return jsonify({"success": False, "error": "仅宿主机模式可用"}), 403
|
|
||||||
|
|
||||||
payload = request.get_json(silent=True) or {}
|
|
||||||
workspace_id = (payload.get("workspace_id") or "").strip()
|
|
||||||
if not workspace_id:
|
|
||||||
return jsonify({"success": False, "error": "缺少 workspace_id"}), 400
|
|
||||||
if _active_task_counts("host").get(workspace_id):
|
|
||||||
return jsonify({"success": False, "error": "该工作区有运行中的任务,暂不能删除"}), 409
|
|
||||||
try:
|
|
||||||
_close_terminal_for_key(f"host::{workspace_id}")
|
|
||||||
container_manager.release_container(f"host::{workspace_id}", reason="delete_host_workspace")
|
|
||||||
result = delete_host_workspace(workspace_id)
|
|
||||||
except ValueError as exc:
|
|
||||||
return jsonify({"success": False, "error": str(exc)}), 400
|
|
||||||
except Exception as exc:
|
|
||||||
return jsonify({"success": False, "error": str(exc)}), 500
|
|
||||||
|
|
||||||
catalog = result.get("catalog") or load_host_workspace_catalog()
|
|
||||||
current_id = (session.get("host_workspace_id") or "").strip()
|
|
||||||
if current_id == workspace_id or not any(
|
|
||||||
item.get("workspace_id") == current_id for item in (catalog.get("workspaces") or [])
|
|
||||||
):
|
|
||||||
_, current = resolve_host_workspace(catalog.get("default_workspace_id"))
|
|
||||||
current_id = current.get("workspace_id") or catalog.get("default_workspace_id") or "default"
|
|
||||||
current_path = str(Path(current.get("path") or "").expanduser().resolve())
|
|
||||||
session["host_workspace_id"] = current_id
|
|
||||||
session["workspace_id"] = current_id
|
|
||||||
with state.HOST_ACTIVE_WORKSPACE_LOCK:
|
|
||||||
state.HOST_ACTIVE_WORKSPACE_ID = current_id
|
|
||||||
state.HOST_ACTIVE_WORKSPACE_PATH = current_path
|
|
||||||
state.HOST_ACTIVE_WORKSPACE_VERSION += 1
|
|
||||||
return jsonify({
|
|
||||||
"success": True,
|
|
||||||
"data": {
|
|
||||||
"deleted_workspace_id": workspace_id,
|
|
||||||
"source_path": catalog.get("source_path"),
|
|
||||||
"default_workspace_id": catalog.get("default_workspace_id"),
|
|
||||||
"current_workspace_id": current_id,
|
|
||||||
"workspaces": _build_host_workspaces_payload(catalog, current_id),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
@status_bp.route('/api/container-status')
|
@status_bp.route('/api/container-status')
|
||||||
@api_login_required
|
@api_login_required
|
||||||
@with_terminal
|
@with_terminal
|
||||||
def get_container_status_api(terminal, workspace, username):
|
def get_container_status_api(terminal, workspace, username):
|
||||||
try:
|
try:
|
||||||
container_key = (
|
status = container_manager.get_container_status(username)
|
||||||
f"host::{getattr(workspace, 'workspace_id', None) or session.get('workspace_id') or 'default'}"
|
|
||||||
if bool(session.get("host_mode")) or username == "host"
|
|
||||||
else f"{username}::{getattr(workspace, 'workspace_id', None) or session.get('workspace_id') or 'default'}"
|
|
||||||
)
|
|
||||||
status = container_manager.get_container_status(container_key)
|
|
||||||
return jsonify({"success": True, "data": status})
|
return jsonify({"success": True, "data": status})
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
return jsonify({"success": False, "error": str(exc)}), 500
|
return jsonify({"success": False, "error": str(exc)}), 500
|
||||||
@ -726,9 +450,7 @@ def get_container_status_api(terminal, workspace, username):
|
|||||||
@with_terminal
|
@with_terminal
|
||||||
def get_project_storage(terminal, workspace, username):
|
def get_project_storage(terminal, workspace, username):
|
||||||
now = time.time()
|
now = time.time()
|
||||||
workspace_id = getattr(workspace, "workspace_id", None) or session.get("workspace_id") or "default"
|
cache_entry = PROJECT_STORAGE_CACHE.get(username)
|
||||||
cache_key = f"{username}::{workspace_id}"
|
|
||||||
cache_entry = PROJECT_STORAGE_CACHE.get(cache_key)
|
|
||||||
if cache_entry and (now - cache_entry.get("ts", 0)) < PROJECT_STORAGE_CACHE_TTL_SECONDS:
|
if cache_entry and (now - cache_entry.get("ts", 0)) < PROJECT_STORAGE_CACHE_TTL_SECONDS:
|
||||||
return jsonify({"success": True, "data": cache_entry["data"]})
|
return jsonify({"success": True, "data": cache_entry["data"]})
|
||||||
try:
|
try:
|
||||||
@ -744,10 +466,10 @@ def get_project_storage(terminal, workspace, username):
|
|||||||
"limit_label": f"{PROJECT_MAX_STORAGE_MB}MB" if PROJECT_MAX_STORAGE_MB else "未限制",
|
"limit_label": f"{PROJECT_MAX_STORAGE_MB}MB" if PROJECT_MAX_STORAGE_MB else "未限制",
|
||||||
"usage_percent": usage_percent
|
"usage_percent": usage_percent
|
||||||
}
|
}
|
||||||
PROJECT_STORAGE_CACHE[cache_key] = {"ts": now, "data": data}
|
PROJECT_STORAGE_CACHE[username] = {"ts": now, "data": data}
|
||||||
return jsonify({"success": True, "data": data})
|
return jsonify({"success": True, "data": data})
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
stale = PROJECT_STORAGE_CACHE.get(cache_key)
|
stale = PROJECT_STORAGE_CACHE.get(username)
|
||||||
if stale:
|
if stale:
|
||||||
return jsonify({"success": True, "data": stale.get("data"), "stale": True}), 200
|
return jsonify({"success": True, "data": stale.get("data"), "stale": True}), 200
|
||||||
return jsonify({"success": False, "error": str(exc)}), 500
|
return jsonify({"success": False, "error": str(exc)}), 500
|
||||||
|
|||||||
101
server/tasks.py
101
server/tasks.py
@ -1,12 +1,10 @@
|
|||||||
"""简单任务 API:将聊天任务与 WebSocket 解耦,支持后台运行与轮询。"""
|
"""简单任务 API:将聊天任务与 WebSocket 解耦,支持后台运行与轮询。"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
import mimetypes
|
import mimetypes
|
||||||
import json
|
|
||||||
import time
|
import time
|
||||||
import threading
|
import threading
|
||||||
import uuid
|
import uuid
|
||||||
from collections import deque
|
from collections import deque
|
||||||
from pathlib import Path
|
|
||||||
from typing import Dict, Any, Optional, List
|
from typing import Dict, Any, Optional, List
|
||||||
|
|
||||||
from flask import Blueprint, request, jsonify
|
from flask import Blueprint, request, jsonify
|
||||||
@ -18,7 +16,6 @@ from .chat_flow import run_chat_task_sync
|
|||||||
from .state import stop_flags
|
from .state import stop_flags
|
||||||
from .utils_common import debug_log, log_conn_diag
|
from .utils_common import debug_log, log_conn_diag
|
||||||
from utils.host_workspace_debug import write_host_workspace_debug
|
from utils.host_workspace_debug import write_host_workspace_debug
|
||||||
from config import DATA_DIR
|
|
||||||
|
|
||||||
|
|
||||||
class TaskRecord:
|
class TaskRecord:
|
||||||
@ -128,7 +125,7 @@ class TaskManager:
|
|||||||
# 单工作区互斥:禁止同一用户同一工作区并发任务
|
# 单工作区互斥:禁止同一用户同一工作区并发任务
|
||||||
existing = [t for t in self.list_tasks(username, workspace_id) if t.status in {"pending", "running"}]
|
existing = [t for t in self.list_tasks(username, workspace_id) if t.status in {"pending", "running"}]
|
||||||
if existing:
|
if existing:
|
||||||
raise RuntimeError("当前工作区已有运行中的任务,请稍后再试。")
|
raise RuntimeError("已有运行中的任务,请稍后再试。")
|
||||||
task_id = str(uuid.uuid4())
|
task_id = str(uuid.uuid4())
|
||||||
record = TaskRecord(task_id, username, workspace_id, message, conversation_id, model_key, thinking_mode, run_mode, max_iterations)
|
record = TaskRecord(task_id, username, workspace_id, message, conversation_id, model_key, thinking_mode, run_mode, max_iterations)
|
||||||
# 记录当前 session 快照,便于后台线程内使用
|
# 记录当前 session 快照,便于后台线程内使用
|
||||||
@ -487,13 +484,6 @@ class TaskManager:
|
|||||||
|
|
||||||
# ---- internal helpers ----
|
# ---- internal helpers ----
|
||||||
def _append_event(self, rec: TaskRecord, event_type: str, data: Dict[str, Any]):
|
def _append_event(self, rec: TaskRecord, event_type: str, data: Dict[str, Any]):
|
||||||
if isinstance(data, dict):
|
|
||||||
data = dict(data)
|
|
||||||
data.setdefault("task_id", rec.task_id)
|
|
||||||
if rec.conversation_id:
|
|
||||||
data.setdefault("conversation_id", rec.conversation_id)
|
|
||||||
if rec.workspace_id:
|
|
||||||
data.setdefault("workspace_id", rec.workspace_id)
|
|
||||||
with self._lock:
|
with self._lock:
|
||||||
idx = getattr(rec, "next_event_idx", None)
|
idx = getattr(rec, "next_event_idx", None)
|
||||||
if idx is None:
|
if idx is None:
|
||||||
@ -611,13 +601,6 @@ class TaskManager:
|
|||||||
debug_log(f"[Task] 注入 user_message 事件失败: {exc}")
|
debug_log(f"[Task] 注入 user_message 事件失败: {exc}")
|
||||||
|
|
||||||
def sender(event_type, data):
|
def sender(event_type, data):
|
||||||
if isinstance(data, dict):
|
|
||||||
data = dict(data)
|
|
||||||
data.setdefault("task_id", rec.task_id)
|
|
||||||
if rec.conversation_id:
|
|
||||||
data.setdefault("conversation_id", rec.conversation_id)
|
|
||||||
if rec.workspace_id:
|
|
||||||
data.setdefault("workspace_id", rec.workspace_id)
|
|
||||||
# 记录事件
|
# 记录事件
|
||||||
self._append_event(rec, event_type, data)
|
self._append_event(rec, event_type, data)
|
||||||
# 在线用户仍然收到实时推送(房间 user_{username})
|
# 在线用户仍然收到实时推送(房间 user_{username})
|
||||||
@ -737,83 +720,23 @@ def start_task_cleanup_scheduler():
|
|||||||
start_task_cleanup_scheduler()
|
start_task_cleanup_scheduler()
|
||||||
|
|
||||||
|
|
||||||
def _conversation_title_for_task(rec: TaskRecord) -> Optional[str]:
|
|
||||||
conv_id = (getattr(rec, "conversation_id", None) or "").strip()
|
|
||||||
if not conv_id:
|
|
||||||
return None
|
|
||||||
if not conv_id.startswith("conv_"):
|
|
||||||
conv_id = f"conv_{conv_id}"
|
|
||||||
try:
|
|
||||||
if rec.username == "host":
|
|
||||||
path = Path(DATA_DIR).expanduser().resolve() / "conversations" / rec.workspace_id / f"{conv_id}.json"
|
|
||||||
else:
|
|
||||||
from . import state
|
|
||||||
workspace = state.user_manager.ensure_user_workspace(rec.username, rec.workspace_id or "default")
|
|
||||||
path = Path(workspace.data_dir).expanduser().resolve() / "conversations" / f"{conv_id}.json"
|
|
||||||
if not path.exists():
|
|
||||||
return None
|
|
||||||
data = json.loads(path.read_text(encoding="utf-8"))
|
|
||||||
title = str(data.get("title") or "").strip()
|
|
||||||
return title or None
|
|
||||||
except Exception:
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def _workspace_label_for_task(rec: TaskRecord) -> Optional[str]:
|
|
||||||
try:
|
|
||||||
if rec.username == "host":
|
|
||||||
from modules.host_workspace_manager import load_host_workspace_catalog
|
|
||||||
catalog = load_host_workspace_catalog()
|
|
||||||
for item in catalog.get("workspaces") or []:
|
|
||||||
if item.get("workspace_id") == rec.workspace_id:
|
|
||||||
return str(item.get("label") or rec.workspace_id)
|
|
||||||
else:
|
|
||||||
from . import state
|
|
||||||
item = state.user_manager.list_user_workspaces(rec.username).get(rec.workspace_id)
|
|
||||||
if item:
|
|
||||||
return str(item.get("label") or rec.workspace_id)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return rec.workspace_id
|
|
||||||
|
|
||||||
|
|
||||||
def _task_public_payload(rec: TaskRecord, *, include_title: bool = True) -> Dict[str, Any]:
|
|
||||||
payload = {
|
|
||||||
"task_id": rec.task_id,
|
|
||||||
"username": rec.username,
|
|
||||||
"workspace_id": rec.workspace_id,
|
|
||||||
"workspace_label": _workspace_label_for_task(rec),
|
|
||||||
"status": rec.status,
|
|
||||||
"created_at": rec.created_at,
|
|
||||||
"updated_at": rec.updated_at,
|
|
||||||
"message": rec.message,
|
|
||||||
"conversation_id": rec.conversation_id,
|
|
||||||
"error": rec.error,
|
|
||||||
}
|
|
||||||
if include_title:
|
|
||||||
payload["conversation_title"] = _conversation_title_for_task(rec)
|
|
||||||
return payload
|
|
||||||
|
|
||||||
|
|
||||||
@tasks_bp.route("/api/tasks", methods=["GET"])
|
@tasks_bp.route("/api/tasks", methods=["GET"])
|
||||||
@api_login_required
|
@api_login_required
|
||||||
def list_tasks_api():
|
def list_tasks_api():
|
||||||
username = get_current_username()
|
username = get_current_username()
|
||||||
workspace_id = (request.args.get("workspace_id") or "").strip() or None
|
recs = task_manager.list_tasks(username)
|
||||||
status_filter = (request.args.get("status") or "").strip().lower()
|
|
||||||
recs = task_manager.list_tasks(username, workspace_id)
|
|
||||||
if status_filter:
|
|
||||||
if status_filter == "active":
|
|
||||||
active = {"pending", "running", "cancel_requested"}
|
|
||||||
recs = [rec for rec in recs if rec.status in active]
|
|
||||||
else:
|
|
||||||
wanted = {part.strip() for part in status_filter.split(",") if part.strip()}
|
|
||||||
recs = [rec for rec in recs if rec.status in wanted]
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
"success": True,
|
"success": True,
|
||||||
"data": [
|
"data": [
|
||||||
_task_public_payload(r)
|
{
|
||||||
for r in sorted(recs, key=lambda x: x.created_at, reverse=True)
|
"task_id": r.task_id,
|
||||||
|
"status": r.status,
|
||||||
|
"created_at": r.created_at,
|
||||||
|
"updated_at": r.updated_at,
|
||||||
|
"message": r.message,
|
||||||
|
"conversation_id": r.conversation_id,
|
||||||
|
"error": r.error,
|
||||||
|
} for r in sorted(recs, key=lambda x: x.created_at, reverse=True)
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -863,7 +786,6 @@ def create_task_api():
|
|||||||
"success": True,
|
"success": True,
|
||||||
"data": {
|
"data": {
|
||||||
"task_id": rec.task_id,
|
"task_id": rec.task_id,
|
||||||
"workspace_id": rec.workspace_id,
|
|
||||||
"status": rec.status,
|
"status": rec.status,
|
||||||
"created_at": rec.created_at,
|
"created_at": rec.created_at,
|
||||||
"conversation_id": rec.conversation_id,
|
"conversation_id": rec.conversation_id,
|
||||||
@ -949,7 +871,6 @@ def get_task_api(task_id: str):
|
|||||||
"success": True,
|
"success": True,
|
||||||
"data": {
|
"data": {
|
||||||
"task_id": rec.task_id,
|
"task_id": rec.task_id,
|
||||||
"workspace_id": rec.workspace_id,
|
|
||||||
"status": rec.status,
|
"status": rec.status,
|
||||||
"created_at": rec.created_at,
|
"created_at": rec.created_at,
|
||||||
"updated_at": rec.updated_at,
|
"updated_at": rec.updated_at,
|
||||||
|
|||||||
@ -62,8 +62,6 @@
|
|||||||
v-if="!isMobileViewport"
|
v-if="!isMobileViewport"
|
||||||
:icon-style="iconStyle"
|
:icon-style="iconStyle"
|
||||||
:format-time="formatTime"
|
:format-time="formatTime"
|
||||||
:running-tasks="runningWorkspaceTasks"
|
|
||||||
:current-workspace-id="currentHostWorkspaceId"
|
|
||||||
:display-mode="chatDisplayMode"
|
:display-mode="chatDisplayMode"
|
||||||
:display-mode-disabled="displayModeSwitchDisabled"
|
:display-mode-disabled="displayModeSwitchDisabled"
|
||||||
@toggle="toggleSidebar"
|
@toggle="toggleSidebar"
|
||||||
@ -72,7 +70,6 @@
|
|||||||
@search-submit="handleSidebarSearchSubmit"
|
@search-submit="handleSidebarSearchSubmit"
|
||||||
@search-more="loadMoreSearchResults"
|
@search-more="loadMoreSearchResults"
|
||||||
@select="loadConversation"
|
@select="loadConversation"
|
||||||
@select-running-task="openRunningWorkspaceTask"
|
|
||||||
@load-more="loadMoreConversations"
|
@load-more="loadMoreConversations"
|
||||||
@personal="openPersonalPage"
|
@personal="openPersonalPage"
|
||||||
@delete="deleteConversation"
|
@delete="deleteConversation"
|
||||||
@ -96,8 +93,7 @@
|
|||||||
:panel-menu-open="panelMenuOpen"
|
:panel-menu-open="panelMenuOpen"
|
||||||
:panel-mode="panelMode"
|
:panel-mode="panelMode"
|
||||||
:file-manager-disabled="policyUiBlocks.block_file_manager"
|
:file-manager-disabled="policyUiBlocks.block_file_manager"
|
||||||
:host-workspace-enabled="versioningHostMode || dockerProjectMode"
|
:host-workspace-enabled="versioningHostMode"
|
||||||
:workspace-kind="versioningHostMode ? 'workspace' : 'project'"
|
|
||||||
:host-workspaces="hostWorkspaces"
|
:host-workspaces="hostWorkspaces"
|
||||||
:host-workspace-id="currentHostWorkspaceId"
|
:host-workspace-id="currentHostWorkspaceId"
|
||||||
:host-workspace-default-id="defaultHostWorkspaceId"
|
:host-workspace-default-id="defaultHostWorkspaceId"
|
||||||
@ -108,7 +104,6 @@
|
|||||||
@toggle-thinking-mode="handleQuickModeToggle"
|
@toggle-thinking-mode="handleQuickModeToggle"
|
||||||
@switch-host-workspace="handleHostWorkspaceSwitch"
|
@switch-host-workspace="handleHostWorkspaceSwitch"
|
||||||
@create-host-workspace="handleCreateHostWorkspace"
|
@create-host-workspace="handleCreateHostWorkspace"
|
||||||
@manage-host-workspace="openHostWorkspaceManageDialog"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@ -267,7 +262,7 @@
|
|||||||
:input-is-multiline="inputIsMultiline"
|
:input-is-multiline="inputIsMultiline"
|
||||||
:input-is-focused="inputIsFocused"
|
:input-is-focused="inputIsFocused"
|
||||||
:is-connected="isConnected"
|
:is-connected="isConnected"
|
||||||
:streaming-message="composerStreamingForInput"
|
:streaming-message="composerBusy"
|
||||||
:input-locked="displayLockEngaged"
|
:input-locked="displayLockEngaged"
|
||||||
:uploading="uploading"
|
:uploading="uploading"
|
||||||
:media-uploading="mediaUploading"
|
:media-uploading="mediaUploading"
|
||||||
@ -475,29 +470,12 @@
|
|||||||
:label-value="hostWorkspaceCreateLabel"
|
:label-value="hostWorkspaceCreateLabel"
|
||||||
:submitting="hostWorkspaceCreateSubmitting"
|
:submitting="hostWorkspaceCreateSubmitting"
|
||||||
:error-message="hostWorkspaceCreateError"
|
:error-message="hostWorkspaceCreateError"
|
||||||
:workspace-kind="versioningHostMode ? 'workspace' : 'project'"
|
|
||||||
@close="closeHostWorkspaceCreateDialog"
|
@close="closeHostWorkspaceCreateDialog"
|
||||||
@submit="submitHostWorkspaceCreate"
|
@submit="submitHostWorkspaceCreate"
|
||||||
@update:path-value="hostWorkspaceCreatePath = $event"
|
@update:path-value="hostWorkspaceCreatePath = $event"
|
||||||
@update:label-value="hostWorkspaceCreateLabel = $event"
|
@update:label-value="hostWorkspaceCreateLabel = $event"
|
||||||
/>
|
/>
|
||||||
</transition>
|
</transition>
|
||||||
<transition name="overlay-fade">
|
|
||||||
<HostWorkspaceManageDialog
|
|
||||||
v-if="hostWorkspaceManageDialogOpen"
|
|
||||||
:workspaces="hostWorkspaces"
|
|
||||||
:current-workspace-id="currentHostWorkspaceId"
|
|
||||||
:default-workspace-id="defaultHostWorkspaceId"
|
|
||||||
:workspace-kind="versioningHostMode ? 'workspace' : 'project'"
|
|
||||||
:busy="hostWorkspaceCreateSubmitting || hostWorkspaceManageSubmitting"
|
|
||||||
:create-submitting="hostWorkspaceCreateSubmitting"
|
|
||||||
:error-message="hostWorkspaceCreateError"
|
|
||||||
@close="closeHostWorkspaceManageDialog"
|
|
||||||
@create="submitHostWorkspaceCreateFromManage"
|
|
||||||
@rename="renameHostWorkspace"
|
|
||||||
@delete="deleteHostWorkspace"
|
|
||||||
/>
|
|
||||||
</transition>
|
|
||||||
<TutorialOverlay v-if="tutorialStore.running" />
|
<TutorialOverlay v-if="tutorialStore.running" />
|
||||||
<NewUserTutorialPrompt
|
<NewUserTutorialPrompt
|
||||||
:visible="tutorialPromptVisible"
|
:visible="tutorialPromptVisible"
|
||||||
@ -690,8 +668,6 @@
|
|||||||
class="mobile-overlay-content"
|
class="mobile-overlay-content"
|
||||||
:icon-style="iconStyle"
|
:icon-style="iconStyle"
|
||||||
:format-time="formatTime"
|
:format-time="formatTime"
|
||||||
:running-tasks="runningWorkspaceTasks"
|
|
||||||
:current-workspace-id="currentHostWorkspaceId"
|
|
||||||
:display-mode="chatDisplayMode"
|
:display-mode="chatDisplayMode"
|
||||||
:display-mode-disabled="displayModeSwitchDisabled"
|
:display-mode-disabled="displayModeSwitchDisabled"
|
||||||
:show-collapse-button="true"
|
:show-collapse-button="true"
|
||||||
@ -702,7 +678,6 @@
|
|||||||
@search-submit="handleSidebarSearchSubmit"
|
@search-submit="handleSidebarSearchSubmit"
|
||||||
@search-more="loadMoreSearchResults"
|
@search-more="loadMoreSearchResults"
|
||||||
@select="handleMobileOverlaySelect($event)"
|
@select="handleMobileOverlaySelect($event)"
|
||||||
@select-running-task="openRunningWorkspaceTask"
|
|
||||||
@load-more="loadMoreConversations"
|
@load-more="loadMoreConversations"
|
||||||
@personal="openPersonalPage"
|
@personal="openPersonalPage"
|
||||||
@delete="deleteConversation"
|
@delete="deleteConversation"
|
||||||
@ -739,8 +714,7 @@
|
|||||||
:is-connected="isConnected"
|
:is-connected="isConnected"
|
||||||
:panel-menu-open="panelMenuOpen"
|
:panel-menu-open="panelMenuOpen"
|
||||||
:panel-mode="panelMode"
|
:panel-mode="panelMode"
|
||||||
:host-workspace-enabled="versioningHostMode || dockerProjectMode"
|
:host-workspace-enabled="versioningHostMode"
|
||||||
:workspace-kind="versioningHostMode ? 'workspace' : 'project'"
|
|
||||||
:host-workspaces="hostWorkspaces"
|
:host-workspaces="hostWorkspaces"
|
||||||
:host-workspace-id="currentHostWorkspaceId"
|
:host-workspace-id="currentHostWorkspaceId"
|
||||||
:host-workspace-default-id="defaultHostWorkspaceId"
|
:host-workspace-default-id="defaultHostWorkspaceId"
|
||||||
@ -750,7 +724,6 @@
|
|||||||
@open-file-manager="openGuiFileManager"
|
@open-file-manager="openGuiFileManager"
|
||||||
@switch-host-workspace="handleHostWorkspaceSwitch"
|
@switch-host-workspace="handleHostWorkspaceSwitch"
|
||||||
@create-host-workspace="handleCreateHostWorkspace"
|
@create-host-workspace="handleCreateHostWorkspace"
|
||||||
@manage-host-workspace="openHostWorkspaceManageDialog"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -27,9 +27,6 @@ const VersioningDialog = defineAsyncComponent(
|
|||||||
const HostWorkspaceCreateDialog = defineAsyncComponent(
|
const HostWorkspaceCreateDialog = defineAsyncComponent(
|
||||||
() => import('../components/overlay/HostWorkspaceCreateDialog.vue')
|
() => import('../components/overlay/HostWorkspaceCreateDialog.vue')
|
||||||
);
|
);
|
||||||
const HostWorkspaceManageDialog = defineAsyncComponent(
|
|
||||||
() => import('../components/overlay/HostWorkspaceManageDialog.vue')
|
|
||||||
);
|
|
||||||
const UserQuestionDialog = defineAsyncComponent(
|
const UserQuestionDialog = defineAsyncComponent(
|
||||||
() => import('../components/overlay/UserQuestionDialog.vue')
|
() => import('../components/overlay/UserQuestionDialog.vue')
|
||||||
);
|
);
|
||||||
@ -56,7 +53,6 @@ export const appComponents = {
|
|||||||
BackgroundCommandDialog,
|
BackgroundCommandDialog,
|
||||||
VersioningDialog,
|
VersioningDialog,
|
||||||
HostWorkspaceCreateDialog,
|
HostWorkspaceCreateDialog,
|
||||||
HostWorkspaceManageDialog,
|
|
||||||
UserQuestionDialog,
|
UserQuestionDialog,
|
||||||
TutorialOverlay,
|
TutorialOverlay,
|
||||||
NewUserTutorialPrompt
|
NewUserTutorialPrompt
|
||||||
|
|||||||
@ -58,16 +58,13 @@ export const computed = {
|
|||||||
'searchResults',
|
'searchResults',
|
||||||
'conversationInsertAnimations',
|
'conversationInsertAnimations',
|
||||||
'conversationListAnimationMode',
|
'conversationListAnimationMode',
|
||||||
'pendingDeletingConversationIds',
|
|
||||||
'searchActive',
|
'searchActive',
|
||||||
'searchInProgress',
|
'searchInProgress',
|
||||||
'searchMoreAvailable',
|
'searchMoreAvailable',
|
||||||
'searchOffset',
|
'searchOffset',
|
||||||
'searchTotal',
|
'searchTotal',
|
||||||
'conversationsOffset',
|
'conversationsOffset',
|
||||||
'conversationsLimit',
|
'conversationsLimit'
|
||||||
'runningWorkspaceTasks',
|
|
||||||
'acknowledgedCompletedTaskIds'
|
|
||||||
]),
|
]),
|
||||||
...mapWritableState(useModelStore, ['currentModelKey']),
|
...mapWritableState(useModelStore, ['currentModelKey']),
|
||||||
...mapState(useModelStore, ['models']),
|
...mapState(useModelStore, ['models']),
|
||||||
@ -171,21 +168,7 @@ export const computed = {
|
|||||||
return !!this.policyUiBlocks.block_virtual_monitor;
|
return !!this.policyUiBlocks.block_virtual_monitor;
|
||||||
},
|
},
|
||||||
displayLockEngaged() {
|
displayLockEngaged() {
|
||||||
return !!this.compressionInProgress || !!this.compressing || !!this.currentWorkspaceHasRunningTask;
|
return !!this.compressionInProgress || !!this.compressing;
|
||||||
},
|
|
||||||
currentWorkspaceHasRunningTask() {
|
|
||||||
if (!(this.versioningHostMode || this.dockerProjectMode) || !this.currentHostWorkspaceId) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const tasks = Array.isArray(this.runningWorkspaceTasks) ? this.runningWorkspaceTasks : [];
|
|
||||||
return tasks.some((task: any) => {
|
|
||||||
const status = String(task?.status || '');
|
|
||||||
return (
|
|
||||||
task?.workspace_id === this.currentHostWorkspaceId &&
|
|
||||||
task?.conversation_id !== this.currentConversationId &&
|
|
||||||
['pending', 'running', 'cancel_requested'].includes(status)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
streamingUi() {
|
streamingUi() {
|
||||||
return this.streamingMessage || this.hasPendingToolActions();
|
return this.streamingMessage || this.hasPendingToolActions();
|
||||||
@ -201,9 +184,6 @@ export const computed = {
|
|||||||
this.compressing
|
this.compressing
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
composerStreamingForInput() {
|
|
||||||
return this.composerBusy && !this.displayLockEngaged;
|
|
||||||
},
|
|
||||||
composerHeroActive() {
|
composerHeroActive() {
|
||||||
return (this.blankHeroActive || this.blankHeroExiting) && !this.composerInteractionActive;
|
return (this.blankHeroActive || this.blankHeroExiting) && !this.composerInteractionActive;
|
||||||
},
|
},
|
||||||
|
|||||||
@ -279,19 +279,47 @@ export const conversationMethods = {
|
|||||||
|
|
||||||
// 注意:加载已有对话时必须保留该对话自身的模型/模式,不能套用用户默认值。
|
// 注意:加载已有对话时必须保留该对话自身的模型/模式,不能套用用户默认值。
|
||||||
|
|
||||||
// 多工作区并行后,切换对话只切换视图,不再取消后台任务;停止当前轮询避免事件写入新对话界面。
|
// 有任务或后台子智能体运行时,提示用户确认切换
|
||||||
try {
|
try {
|
||||||
const { useTaskStore } = await import('../../stores/task');
|
const { useTaskStore } = await import('../../stores/task');
|
||||||
const taskStore = useTaskStore();
|
const taskStore = useTaskStore();
|
||||||
if (taskStore.hasActiveTask || this.taskInProgress) {
|
const hasActiveTask =
|
||||||
taskStore.clearTask();
|
taskStore.hasActiveTask || this.taskInProgress || this.waitingForSubAgent;
|
||||||
if (typeof this.clearProcessedEvents === 'function') {
|
if (hasActiveTask) {
|
||||||
this.clearProcessedEvents();
|
const waitingLabel = this.waitingForBackgroundCommand ? '后台指令' : '后台子智能体';
|
||||||
|
const confirmed = await this.confirmAction({
|
||||||
|
title: '切换对话',
|
||||||
|
message: this.waitingForSubAgent
|
||||||
|
? `${waitingLabel}正在运行,切换对话后将不会自动接收完成提示。确定要切换吗?`
|
||||||
|
: '当前有任务正在执行,切换对话后任务会停止。确定要切换吗?',
|
||||||
|
confirmText: '切换',
|
||||||
|
cancelText: '取消'
|
||||||
|
});
|
||||||
|
if (!confirmed) {
|
||||||
|
this.suppressTitleTyping = false;
|
||||||
|
this.titleReady = true;
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (taskStore.hasActiveTask) {
|
||||||
|
await taskStore.cancelTask();
|
||||||
|
taskStore.clearTask();
|
||||||
|
if (typeof this.clearProcessedEvents === 'function') {
|
||||||
|
this.clearProcessedEvents();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.waitingForSubAgent && !taskStore.hasActiveTask) {
|
||||||
|
this.waitingForSubAgent = false;
|
||||||
|
this.waitingForBackgroundCommand = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.streamingMessage = false;
|
||||||
|
this.taskInProgress = false;
|
||||||
|
this.stopRequested = false;
|
||||||
}
|
}
|
||||||
this.clearLocalTaskUiState?.(`switch-conversation:${conversationId}`);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[切换对话] 停止本地轮询失败:', error);
|
console.error('[切换对话] 检查/停止任务失败:', error);
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.persistComposerDraftNow({
|
await this.persistComposerDraftNow({
|
||||||
@ -348,22 +376,6 @@ export const conversationMethods = {
|
|||||||
|
|
||||||
// 4. 立即加载历史和统计,确保列表切换后界面同步更新
|
// 4. 立即加载历史和统计,确保列表切换后界面同步更新
|
||||||
await this.fetchAndDisplayHistory();
|
await this.fetchAndDisplayHistory();
|
||||||
await this.refreshRunningWorkspaceTasks?.();
|
|
||||||
const visibleTask =
|
|
||||||
typeof this.getVisibleWorkspaceTaskForConversation === 'function'
|
|
||||||
? this.getVisibleWorkspaceTaskForConversation(conversationId)
|
|
||||||
: null;
|
|
||||||
const visibleTaskStatus = String(visibleTask?.status || '');
|
|
||||||
const activeStatuses = new Set(['pending', 'running', 'cancel_requested']);
|
|
||||||
const terminalStatuses = new Set(['succeeded', 'failed', 'canceled']);
|
|
||||||
if (visibleTask && activeStatuses.has(visibleTaskStatus)) {
|
|
||||||
// 切回正在运行的对话时,必须恢复该任务的 REST 轮询和输入区忙碌态;
|
|
||||||
// 否则只切换了历史视图,后续输出不会续接到当前对话,发送按钮也不会变成停止按钮。
|
|
||||||
await this.restoreTaskState?.();
|
|
||||||
} else if (visibleTask && terminalStatuses.has(visibleTaskStatus)) {
|
|
||||||
// 用户已点进完成后的“待查看”对话,清除列表里的完成标记。
|
|
||||||
this.acknowledgeCompletedWorkspaceTask?.(visibleTask.task_id);
|
|
||||||
}
|
|
||||||
this.fetchPermissionMode();
|
this.fetchPermissionMode();
|
||||||
this.fetchExecutionMode();
|
this.fetchExecutionMode();
|
||||||
await this.fetchVersioningStatus(conversationId, { silent: true });
|
await this.fetchVersioningStatus(conversationId, { silent: true });
|
||||||
@ -485,19 +497,54 @@ export const conversationMethods = {
|
|||||||
console.warn('应用个性化默认设置失败:', error);
|
console.warn('应用个性化默认设置失败:', error);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建新对话只切换视图,不再取消后台任务;停止当前轮询避免事件串写。
|
// 检查是否有运行中的任务,如果有则提示用户
|
||||||
|
let hasActiveTask = false;
|
||||||
try {
|
try {
|
||||||
const { useTaskStore } = await import('../../stores/task');
|
const { useTaskStore } = await import('../../stores/task');
|
||||||
const taskStore = useTaskStore();
|
const taskStore = useTaskStore();
|
||||||
if (taskStore.hasActiveTask || this.taskInProgress) {
|
|
||||||
taskStore.clearTask();
|
if (taskStore.hasActiveTask || this.taskInProgress || this.waitingForSubAgent) {
|
||||||
if (typeof this.clearProcessedEvents === 'function') {
|
hasActiveTask = true;
|
||||||
this.clearProcessedEvents();
|
const waitingLabel = this.waitingForBackgroundCommand ? '后台指令' : '后台子智能体';
|
||||||
|
|
||||||
|
// 显示提示
|
||||||
|
const confirmed = await this.confirmAction({
|
||||||
|
title: '创建新对话',
|
||||||
|
message: this.waitingForSubAgent
|
||||||
|
? `${waitingLabel}正在运行,创建新对话后将不会自动接收完成提示。确定要创建吗?`
|
||||||
|
: '当前有任务正在执行,创建新对话后任务会停止。确定要创建吗?',
|
||||||
|
confirmText: '创建',
|
||||||
|
cancelText: '取消'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!confirmed) {
|
||||||
|
// 用户取消创建
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 用户确认,停止任务
|
||||||
|
debugLog('[创建新对话] 用户确认,正在停止任务...');
|
||||||
|
if (taskStore.hasActiveTask) {
|
||||||
|
await taskStore.cancelTask();
|
||||||
|
taskStore.clearTask();
|
||||||
|
if (typeof this.clearProcessedEvents === 'function') {
|
||||||
|
this.clearProcessedEvents();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置任务相关状态
|
||||||
|
this.streamingMessage = false;
|
||||||
|
this.taskInProgress = false;
|
||||||
|
this.stopRequested = false;
|
||||||
|
if (this.waitingForSubAgent && !taskStore.hasActiveTask) {
|
||||||
|
this.waitingForSubAgent = false;
|
||||||
|
this.waitingForBackgroundCommand = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
debugLog('[创建新对话] 任务已停止');
|
||||||
}
|
}
|
||||||
this.clearLocalTaskUiState?.('create-new-conversation');
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[创建新对话] 停止本地轮询失败:', error);
|
console.error('[创建新对话] 检查/停止任务失败:', error);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -568,7 +615,15 @@ export const conversationMethods = {
|
|||||||
conversationsLen: Array.isArray(this.conversations) ? this.conversations.length : 'n/a'
|
conversationsLen: Array.isArray(this.conversations) ? this.conversations.length : 'n/a'
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.refreshRunningWorkspaceTasks?.();
|
// 如果停止了任务,显示提示
|
||||||
|
if (hasActiveTask) {
|
||||||
|
this.uiPushToast({
|
||||||
|
title: '任务已停止',
|
||||||
|
message: '创建新对话后,之前的任务已停止',
|
||||||
|
type: 'info',
|
||||||
|
duration: 3000
|
||||||
|
});
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
console.error('创建对话失败:', result.message);
|
console.error('创建对话失败:', result.message);
|
||||||
this.uiPushToast({
|
this.uiPushToast({
|
||||||
@ -631,31 +686,17 @@ export const conversationMethods = {
|
|||||||
history.replaceState({}, '', '/new');
|
history.replaceState({}, '', '/new');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 先给目标项加 deleting 类执行左滑动画,再从本地列表移除。
|
// 先从本地列表移除,让侧边栏执行删除动画;随后再刷新服务端列表补齐分页状态
|
||||||
// 这比完全依赖 transition-group leave 更稳定:即使列表即将为空或分支切换,也能先播放离场动画。
|
|
||||||
this.conversationListAnimationMode = 'delete';
|
this.conversationListAnimationMode = 'delete';
|
||||||
const deletingIds = Array.isArray(this.pendingDeletingConversationIds)
|
|
||||||
? this.pendingDeletingConversationIds
|
|
||||||
: [];
|
|
||||||
if (!deletingIds.includes(conversationId)) {
|
|
||||||
this.pendingDeletingConversationIds = [...deletingIds, conversationId];
|
|
||||||
}
|
|
||||||
await this.$nextTick();
|
|
||||||
await waitForAnimation(430);
|
|
||||||
|
|
||||||
this.conversations = this.conversations.filter(
|
this.conversations = this.conversations.filter(
|
||||||
(conversation) => conversation.id !== conversationId
|
(conversation) => conversation.id !== conversationId
|
||||||
);
|
);
|
||||||
this.searchResults = this.searchResults.filter(
|
this.searchResults = this.searchResults.filter(
|
||||||
(conversation) => conversation.id !== conversationId
|
(conversation) => conversation.id !== conversationId
|
||||||
);
|
);
|
||||||
this.pendingDeletingConversationIds = (Array.isArray(this.pendingDeletingConversationIds)
|
|
||||||
? this.pendingDeletingConversationIds
|
|
||||||
: []
|
|
||||||
).filter((id) => id !== conversationId);
|
|
||||||
|
|
||||||
// 等待“左滑离场 + 0.1s 后集体上移”动画结束,避免刷新列表打断过渡。
|
// 等待“左滑离场 + 0.1s 后集体上移”动画结束,避免刷新列表打断过渡。
|
||||||
await waitForAnimation(180);
|
await waitForAnimation(560);
|
||||||
|
|
||||||
// 刷新对话列表
|
// 刷新对话列表
|
||||||
this.conversationsOffset = 0;
|
this.conversationsOffset = 0;
|
||||||
@ -670,11 +711,6 @@ export const conversationMethods = {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.pendingDeletingConversationIds = (Array.isArray(this.pendingDeletingConversationIds)
|
|
||||||
? this.pendingDeletingConversationIds
|
|
||||||
: []
|
|
||||||
).filter((id) => id !== conversationId);
|
|
||||||
this.conversationListAnimationMode = 'idle';
|
|
||||||
console.error('删除对话异常:', error);
|
console.error('删除对话异常:', error);
|
||||||
this.uiPushToast({
|
this.uiPushToast({
|
||||||
title: '删除对话异常',
|
title: '删除对话异常',
|
||||||
|
|||||||
@ -565,14 +565,6 @@ export const messageMethods = {
|
|||||||
const hasMedia =
|
const hasMedia =
|
||||||
(Array.isArray(this.selectedImages) && this.selectedImages.length > 0) ||
|
(Array.isArray(this.selectedImages) && this.selectedImages.length > 0) ||
|
||||||
(Array.isArray(this.selectedVideos) && this.selectedVideos.length > 0);
|
(Array.isArray(this.selectedVideos) && this.selectedVideos.length > 0);
|
||||||
if (this.currentWorkspaceHasRunningTask) {
|
|
||||||
this.uiPushToast({
|
|
||||||
title: '当前工作区正在运行',
|
|
||||||
message: '请先在“运行中的任务”中查看或等待任务完成后再发送新消息。',
|
|
||||||
type: 'warning'
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (this.composerBusy) {
|
if (this.composerBusy) {
|
||||||
if (hasText) {
|
if (hasText) {
|
||||||
if (Array.isArray(this.pendingUserQuestions) && this.pendingUserQuestions.length > 0) {
|
if (Array.isArray(this.pendingUserQuestions) && this.pendingUserQuestions.length > 0) {
|
||||||
@ -792,7 +784,6 @@ export const messageMethods = {
|
|||||||
});
|
});
|
||||||
|
|
||||||
debugLog('[Message] 任务已创建,开始轮询');
|
debugLog('[Message] 任务已创建,开始轮询');
|
||||||
await this.refreshRunningWorkspaceTasks?.();
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[Message] 创建任务失败:', error);
|
console.error('[Message] 创建任务失败:', error);
|
||||||
this.uiPushToast({
|
this.uiPushToast({
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
import { debugLog } from './common';
|
import { debugLog } from './common';
|
||||||
import { useTaskStore } from '../../stores/task';
|
|
||||||
|
|
||||||
const debugNotifyLog = (...args: any[]) => {
|
const debugNotifyLog = (...args: any[]) => {
|
||||||
void args;
|
void args;
|
||||||
@ -179,57 +178,6 @@ export const taskPollingMethods = {
|
|||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
|
|
||||||
ensureRunningAssistantPlaceholder(runningTask: any = null, reason = 'restore-running-task') {
|
|
||||||
if (!Array.isArray(this.messages) || this.messages.length === 0) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const last = this.messages[this.messages.length - 1];
|
|
||||||
const lastActions = Array.isArray(last?.actions) ? last.actions : [];
|
|
||||||
const alreadyWaiting =
|
|
||||||
last?.role === 'assistant' && lastActions.length === 0 && !!last.awaitingFirstContent;
|
|
||||||
if (alreadyWaiting) {
|
|
||||||
this.taskInProgress = true;
|
|
||||||
this.streamingMessage = true;
|
|
||||||
this.stopRequested = false;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (last?.role === 'assistant' && lastActions.length > 0) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (last?.role !== 'user') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const startedAt =
|
|
||||||
typeof runningTask?.created_at === 'number'
|
|
||||||
? new Date(runningTask.created_at * 1000).toISOString()
|
|
||||||
: new Date().toISOString();
|
|
||||||
last.metadata = {
|
|
||||||
...(last.metadata || {}),
|
|
||||||
work_timer: last.metadata?.work_timer || {
|
|
||||||
status: 'working',
|
|
||||||
started_at: startedAt
|
|
||||||
}
|
|
||||||
};
|
|
||||||
this.taskInProgress = true;
|
|
||||||
this.stopRequested = false;
|
|
||||||
this.streamingMessage = true;
|
|
||||||
this.chatStartAssistantMessage();
|
|
||||||
if (typeof this.monitorShowPendingReply === 'function') {
|
|
||||||
this.monitorShowPendingReply();
|
|
||||||
}
|
|
||||||
this.$forceUpdate();
|
|
||||||
this.$nextTick(() => {
|
|
||||||
this.conditionalScrollToBottom?.();
|
|
||||||
});
|
|
||||||
debugLog('[TaskPolling] 已恢复等待首包占位', {
|
|
||||||
reason,
|
|
||||||
conversationId: this.currentConversationId,
|
|
||||||
taskId: runningTask?.task_id || null
|
|
||||||
});
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
|
|
||||||
stopWaitingTaskProbe() {
|
stopWaitingTaskProbe() {
|
||||||
if (this.waitingTaskProbeTimer) {
|
if (this.waitingTaskProbeTimer) {
|
||||||
debugNotifyLog('[DEBUG_NOTIFY][ui] stopWaitingTaskProbe');
|
debugNotifyLog('[DEBUG_NOTIFY][ui] stopWaitingTaskProbe');
|
||||||
@ -339,11 +287,7 @@ export const taskPollingMethods = {
|
|||||||
});
|
});
|
||||||
const runningTask = tasks.find(
|
const runningTask = tasks.find(
|
||||||
(task: any) =>
|
(task: any) =>
|
||||||
task?.status === 'running' &&
|
task?.status === 'running' && task?.conversation_id === this.currentConversationId
|
||||||
task?.conversation_id === this.currentConversationId &&
|
|
||||||
(!(this.versioningHostMode || this.dockerProjectMode) ||
|
|
||||||
!this.currentHostWorkspaceId ||
|
|
||||||
task?.workspace_id === this.currentHostWorkspaceId)
|
|
||||||
);
|
);
|
||||||
if (!runningTask?.task_id) {
|
if (!runningTask?.task_id) {
|
||||||
debugNotifyLog('[DEBUG_NOTIFY][ui] tryResumeWaitingNoticeTask:no-matched-task');
|
debugNotifyLog('[DEBUG_NOTIFY][ui] tryResumeWaitingNoticeTask:no-matched-task');
|
||||||
@ -432,7 +376,6 @@ export const taskPollingMethods = {
|
|||||||
const eventType = event.type;
|
const eventType = event.type;
|
||||||
const eventData = event.data || {};
|
const eventData = event.data || {};
|
||||||
const eventIdx = event.idx;
|
const eventIdx = event.idx;
|
||||||
const taskStore = useTaskStore();
|
|
||||||
if (eventType === 'task_complete' || eventType === 'task_stopped' || eventType === 'error') {
|
if (eventType === 'task_complete' || eventType === 'task_stopped' || eventType === 'error') {
|
||||||
jsonDebug('task-event', {
|
jsonDebug('task-event', {
|
||||||
eventType,
|
eventType,
|
||||||
@ -468,10 +411,48 @@ export const taskPollingMethods = {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 事件去重检查
|
||||||
|
if (typeof eventIdx === 'number') {
|
||||||
|
if (!this._processedEventIndices) {
|
||||||
|
this._processedEventIndices = new Set();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._processedEventIndices.has(eventIdx)) {
|
||||||
|
if (
|
||||||
|
[
|
||||||
|
'ai_message_start',
|
||||||
|
'thinking_start',
|
||||||
|
'thinking_chunk',
|
||||||
|
'thinking_end',
|
||||||
|
'text_start',
|
||||||
|
'text_chunk',
|
||||||
|
'text_end'
|
||||||
|
].includes(eventType)
|
||||||
|
) {
|
||||||
|
restoreDebugLog('event:drop-duplicate', {
|
||||||
|
eventType,
|
||||||
|
eventIdx,
|
||||||
|
currentConversationId: this.currentConversationId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
debugLog(`[TaskPolling] 跳过重复事件 #${eventIdx}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._processedEventIndices.add(eventIdx);
|
||||||
|
|
||||||
|
// 限制 Set 大小(保留最近 1000 个)
|
||||||
|
if (this._processedEventIndices.size > 1000) {
|
||||||
|
const firstIdx = Math.min(...this._processedEventIndices);
|
||||||
|
this._processedEventIndices.delete(firstIdx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 检查事件的 conversation_id 是否匹配当前对话
|
// 检查事件的 conversation_id 是否匹配当前对话
|
||||||
// 如果不匹配,忽略该事件(避免切换对话后旧任务的事件显示到新对话中)
|
// 如果不匹配,忽略该事件(避免切换对话后旧任务的事件显示到新对话中)
|
||||||
const crossConversationAllowed = new Set([
|
const crossConversationAllowed = new Set([
|
||||||
'shallow_compression',
|
'shallow_compression',
|
||||||
|
'conversation_resolved',
|
||||||
'compression_finished'
|
'compression_finished'
|
||||||
]);
|
]);
|
||||||
if (
|
if (
|
||||||
@ -510,69 +491,6 @@ export const taskPollingMethods = {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 检查事件 task_id 是否仍是当前正在接管的任务,防止切换/新建对话后旧轮询的在途响应串写。
|
|
||||||
if (
|
|
||||||
!crossConversationAllowed.has(eventType) &&
|
|
||||||
eventData.task_id &&
|
|
||||||
(!taskStore.currentTaskId || eventData.task_id !== taskStore.currentTaskId)
|
|
||||||
) {
|
|
||||||
restoreDebugLog('event:drop-task-mismatch', {
|
|
||||||
eventType,
|
|
||||||
eventIdx,
|
|
||||||
eventTaskId: eventData.task_id,
|
|
||||||
currentTaskId: taskStore.currentTaskId,
|
|
||||||
eventConversationId: eventData.conversation_id,
|
|
||||||
currentConversationId: this.currentConversationId
|
|
||||||
});
|
|
||||||
debugLog(
|
|
||||||
`[TaskPolling] 忽略不匹配的任务事件 #${eventIdx}: ${eventType}, 事件任务=${eventData.task_id}, 当前任务=${taskStore.currentTaskId}`
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 事件去重检查必须在 conversation/task 校验之后执行,避免其他对话同 idx 事件污染当前任务去重表。
|
|
||||||
const dedupeKey =
|
|
||||||
eventData.task_id && typeof eventIdx === 'number'
|
|
||||||
? `${eventData.task_id}:${eventIdx}`
|
|
||||||
: typeof eventIdx === 'number'
|
|
||||||
? `idx:${eventIdx}`
|
|
||||||
: '';
|
|
||||||
if (dedupeKey) {
|
|
||||||
if (!this._processedEventIndices) {
|
|
||||||
this._processedEventIndices = new Set();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this._processedEventIndices.has(dedupeKey)) {
|
|
||||||
if (
|
|
||||||
[
|
|
||||||
'ai_message_start',
|
|
||||||
'thinking_start',
|
|
||||||
'thinking_chunk',
|
|
||||||
'thinking_end',
|
|
||||||
'text_start',
|
|
||||||
'text_chunk',
|
|
||||||
'text_end'
|
|
||||||
].includes(eventType)
|
|
||||||
) {
|
|
||||||
restoreDebugLog('event:drop-duplicate', {
|
|
||||||
eventType,
|
|
||||||
eventIdx,
|
|
||||||
dedupeKey,
|
|
||||||
currentConversationId: this.currentConversationId
|
|
||||||
});
|
|
||||||
}
|
|
||||||
debugLog(`[TaskPolling] 跳过重复事件 ${dedupeKey}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this._processedEventIndices.add(dedupeKey);
|
|
||||||
|
|
||||||
// 限制 Set 大小(保留最近 1000 个)
|
|
||||||
if (this._processedEventIndices.size > 1000) {
|
|
||||||
const firstValue = this._processedEventIndices.values().next().value;
|
|
||||||
this._processedEventIndices.delete(firstValue);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
debugLog(`[TaskPolling] 处理事件 #${eventIdx}: ${eventType}`, eventData);
|
debugLog(`[TaskPolling] 处理事件 #${eventIdx}: ${eventType}`, eventData);
|
||||||
|
|
||||||
@ -1751,10 +1669,6 @@ export const taskPollingMethods = {
|
|||||||
}, 500);
|
}, 500);
|
||||||
}
|
}
|
||||||
this.scheduleTodoListRefresh(100);
|
this.scheduleTodoListRefresh(100);
|
||||||
if (data?.conversation_id && data.conversation_id === this.currentConversationId && data?.task_id) {
|
|
||||||
this.acknowledgeCompletedWorkspaceTask?.(data.task_id);
|
|
||||||
}
|
|
||||||
setTimeout(() => this.refreshRunningWorkspaceTasks?.(), 0);
|
|
||||||
// 标题生成可能晚于主任务完成;主轮询停止后主动短轮询当前会话标题,避免必须刷新页面。
|
// 标题生成可能晚于主任务完成;主轮询停止后主动短轮询当前会话标题,避免必须刷新页面。
|
||||||
this.scheduleGeneratedTitleRefresh('task_complete', {
|
this.scheduleGeneratedTitleRefresh('task_complete', {
|
||||||
conversationId: data?.conversation_id || this.currentConversationId,
|
conversationId: data?.conversation_id || this.currentConversationId,
|
||||||
@ -1798,7 +1712,6 @@ export const taskPollingMethods = {
|
|||||||
this.clearPendingTools('task_stopped');
|
this.clearPendingTools('task_stopped');
|
||||||
}
|
}
|
||||||
this.scheduleTodoListRefresh(100);
|
this.scheduleTodoListRefresh(100);
|
||||||
setTimeout(() => this.refreshRunningWorkspaceTasks?.(), 0);
|
|
||||||
this.$forceUpdate();
|
this.$forceUpdate();
|
||||||
|
|
||||||
this.clearTaskState();
|
this.clearTaskState();
|
||||||
@ -1931,12 +1844,7 @@ export const taskPollingMethods = {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
this.taskInProgress = true;
|
this.taskInProgress = true;
|
||||||
// 恢复“已发送但尚未收到任何回复”的运行中对话时,前端会先补一个
|
this.streamingMessage = false;
|
||||||
// awaitingFirstContent assistant 占位。重放 user_message 不能把这个等待态清掉,
|
|
||||||
// 否则输入区会丢失停止按钮。
|
|
||||||
this.streamingMessage = isEmptyAssistantPlaceholderMessage(this.messages?.[this.messages.length - 1])
|
|
||||||
? true
|
|
||||||
: false;
|
|
||||||
this.stopRequested = false;
|
this.stopRequested = false;
|
||||||
} else {
|
} else {
|
||||||
const source = resolveUserMessageSource(data);
|
const source = resolveUserMessageSource(data);
|
||||||
@ -2295,21 +2203,6 @@ export const taskPollingMethods = {
|
|||||||
await this.restoreSubAgentWaitingState();
|
await this.restoreSubAgentWaitingState();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (
|
|
||||||
(this.versioningHostMode || this.dockerProjectMode) &&
|
|
||||||
this.currentHostWorkspaceId &&
|
|
||||||
runningTask?.workspace_id &&
|
|
||||||
runningTask.workspace_id !== this.currentHostWorkspaceId
|
|
||||||
) {
|
|
||||||
restoreDebugLog('restore:skip-workspace-mismatch', {
|
|
||||||
taskId: runningTask?.task_id,
|
|
||||||
taskWorkspaceId: runningTask?.workspace_id,
|
|
||||||
currentHostWorkspaceId: this.currentHostWorkspaceId
|
|
||||||
});
|
|
||||||
taskStore.clearTask();
|
|
||||||
await this.restoreSubAgentWaitingState();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
debugLog('[TaskPolling] 发现运行中的任务,开始恢复状态', {
|
debugLog('[TaskPolling] 发现运行中的任务,开始恢复状态', {
|
||||||
taskId: runningTask?.task_id,
|
taskId: runningTask?.task_id,
|
||||||
@ -2409,41 +2302,9 @@ export const taskPollingMethods = {
|
|||||||
let inThinking = false;
|
let inThinking = false;
|
||||||
let inText = false;
|
let inText = false;
|
||||||
let hasTextChunkEvent = false;
|
let hasTextChunkEvent = false;
|
||||||
let hasAssistantResponseEvent = false;
|
|
||||||
let hasAssistantContentEvent = false;
|
|
||||||
|
|
||||||
for (let i = 0; i < allEvents.length; i++) {
|
for (let i = 0; i < allEvents.length; i++) {
|
||||||
const event = allEvents[i];
|
const event = allEvents[i];
|
||||||
const eventType = String(event?.type || '');
|
|
||||||
if (
|
|
||||||
[
|
|
||||||
'ai_message_start',
|
|
||||||
'thinking_start',
|
|
||||||
'thinking_chunk',
|
|
||||||
'thinking_end',
|
|
||||||
'text_start',
|
|
||||||
'text_chunk',
|
|
||||||
'text_end',
|
|
||||||
'tool_preparing',
|
|
||||||
'tool_start',
|
|
||||||
'tool_update',
|
|
||||||
'tool_complete'
|
|
||||||
].includes(eventType)
|
|
||||||
) {
|
|
||||||
hasAssistantResponseEvent = true;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
[
|
|
||||||
'thinking_chunk',
|
|
||||||
'text_chunk',
|
|
||||||
'tool_preparing',
|
|
||||||
'tool_start',
|
|
||||||
'tool_update',
|
|
||||||
'tool_complete'
|
|
||||||
].includes(eventType)
|
|
||||||
) {
|
|
||||||
hasAssistantContentEvent = true;
|
|
||||||
}
|
|
||||||
if (event.type === 'thinking_start') {
|
if (event.type === 'thinking_start') {
|
||||||
inThinking = true;
|
inThinking = true;
|
||||||
} else if (event.type === 'thinking_end') {
|
} else if (event.type === 'thinking_end') {
|
||||||
@ -2484,8 +2345,6 @@ export const taskPollingMethods = {
|
|||||||
isAssistantMessage,
|
isAssistantMessage,
|
||||||
historyActionsCount,
|
historyActionsCount,
|
||||||
eventCount,
|
eventCount,
|
||||||
hasAssistantResponseEvent,
|
|
||||||
hasAssistantContentEvent,
|
|
||||||
historyIncomplete,
|
historyIncomplete,
|
||||||
inText,
|
inText,
|
||||||
hasTextChunkEvent,
|
hasTextChunkEvent,
|
||||||
@ -2511,9 +2370,6 @@ export const taskPollingMethods = {
|
|||||||
|
|
||||||
this.streamingMessage = true;
|
this.streamingMessage = true;
|
||||||
this.taskInProgress = true;
|
this.taskInProgress = true;
|
||||||
if (!hasAssistantContentEvent) {
|
|
||||||
this.ensureRunningAssistantPlaceholder?.(runningTask, 'restore:no-visible-content-yet');
|
|
||||||
}
|
|
||||||
this.$forceUpdate();
|
this.$forceUpdate();
|
||||||
|
|
||||||
// 重置偏移量为 0,从头获取所有事件来重建 assistant 消息
|
// 重置偏移量为 0,从头获取所有事件来重建 assistant 消息
|
||||||
|
|||||||
@ -154,45 +154,6 @@ function parseSystemNoticeLabel(rawContent: any): string | null {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const uiMethods = {
|
export const uiMethods = {
|
||||||
clearLocalTaskUiState(reason = 'safe-navigation') {
|
|
||||||
// 只清理当前浏览器视图里的运行态/工具态,不取消后端任务。
|
|
||||||
// 用于切换对话/工作区后,避免旧任务残留让新视图的发送按钮显示为“停止”。
|
|
||||||
try {
|
|
||||||
if (typeof this.stopWaitingTaskProbe === 'function') {
|
|
||||||
this.stopWaitingTaskProbe();
|
|
||||||
}
|
|
||||||
} catch (_) {}
|
|
||||||
this.streamingMessage = false;
|
|
||||||
this.taskInProgress = false;
|
|
||||||
this.stopRequested = false;
|
|
||||||
this.waitingForSubAgent = false;
|
|
||||||
this.waitingForBackgroundCommand = false;
|
|
||||||
this.dropToolEvents = false;
|
|
||||||
this.runtimeQueuedMessages = [];
|
|
||||||
this.runtimeGuidanceFallbackQueue = [];
|
|
||||||
this.runtimeQueueAutoSendInProgress = false;
|
|
||||||
this.runtimeQueueSyncLockKey = '';
|
|
||||||
this.runtimeQueueSyncLockUntil = 0;
|
|
||||||
if (typeof this.clearRuntimeQueueSuppressionState === 'function') {
|
|
||||||
this.clearRuntimeQueueSuppressionState();
|
|
||||||
} else {
|
|
||||||
this.runtimeQueueSuppressedMessageIds = new Set();
|
|
||||||
this.runtimeGuidanceSuppressedTextCounts = {};
|
|
||||||
}
|
|
||||||
if (typeof this.clearPendingTools === 'function') {
|
|
||||||
this.clearPendingTools(reason);
|
|
||||||
} else {
|
|
||||||
this.preparingTools?.clear?.();
|
|
||||||
this.activeTools?.clear?.();
|
|
||||||
this.toolActionIndex?.clear?.();
|
|
||||||
this.toolStacks?.clear?.();
|
|
||||||
}
|
|
||||||
if (typeof this.chatClearThinkingLocks === 'function') {
|
|
||||||
this.chatClearThinkingLocks();
|
|
||||||
}
|
|
||||||
this.$forceUpdate?.();
|
|
||||||
},
|
|
||||||
|
|
||||||
ensureScrollListener() {
|
ensureScrollListener() {
|
||||||
if (this._scrollListenerReady) {
|
if (this._scrollListenerReady) {
|
||||||
return;
|
return;
|
||||||
@ -752,238 +713,42 @@ export const uiMethods = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
async fetchHostWorkspaces() {
|
async fetchHostWorkspaces() {
|
||||||
if (!(this.versioningHostMode || this.dockerProjectMode)) {
|
if (!this.versioningHostMode) {
|
||||||
this.hostWorkspaces = [];
|
this.hostWorkspaces = [];
|
||||||
this.currentHostWorkspaceId = '';
|
this.currentHostWorkspaceId = '';
|
||||||
this.defaultHostWorkspaceId = '';
|
this.defaultHostWorkspaceId = '';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(this.versioningHostMode ? '/api/host/workspaces' : '/api/projects');
|
const resp = await fetch('/api/host/workspaces');
|
||||||
const payload = await resp.json().catch(() => ({}));
|
const payload = await resp.json().catch(() => ({}));
|
||||||
if (!resp.ok || !payload?.success) {
|
if (!resp.ok || !payload?.success) {
|
||||||
throw new Error(payload?.error || '获取项目列表失败');
|
throw new Error(payload?.error || '获取宿主机工作区列表失败');
|
||||||
}
|
}
|
||||||
const data = payload.data || {};
|
const data = payload.data || {};
|
||||||
const items = Array.isArray(data.workspaces) ? data.workspaces : [];
|
const items = Array.isArray(data.workspaces) ? data.workspaces : [];
|
||||||
this.hostWorkspaces = items.map((item: any) => ({
|
this.hostWorkspaces = items.map((item: any) => ({
|
||||||
workspace_id: String(item.workspace_id || ''),
|
workspace_id: String(item.workspace_id || ''),
|
||||||
label: String(item.label || item.workspace_id || '未命名项目'),
|
label: String(item.label || item.workspace_id || '未命名工作区'),
|
||||||
path: String(item.path || ''),
|
path: String(item.path || ''),
|
||||||
is_current: !!item.is_current,
|
is_current: !!item.is_current
|
||||||
running_task_count: Number(item.running_task_count || 0)
|
|
||||||
}));
|
}));
|
||||||
this.currentHostWorkspaceId = String(data.current_workspace_id || '');
|
this.currentHostWorkspaceId = String(data.current_workspace_id || '');
|
||||||
this.defaultHostWorkspaceId = String(data.default_workspace_id || '');
|
this.defaultHostWorkspaceId = String(data.default_workspace_id || '');
|
||||||
await this.refreshRunningWorkspaceTasks();
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('加载工作区/项目失败:', error);
|
console.warn('加载宿主机工作区失败:', error);
|
||||||
this.hostWorkspaces = [];
|
this.hostWorkspaces = [];
|
||||||
this.currentHostWorkspaceId = '';
|
this.currentHostWorkspaceId = '';
|
||||||
this.defaultHostWorkspaceId = '';
|
this.defaultHostWorkspaceId = '';
|
||||||
this.runningWorkspaceTasks = [];
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async refreshRunningWorkspaceTasks() {
|
|
||||||
if (!(this.versioningHostMode || this.dockerProjectMode)) {
|
|
||||||
this.runningWorkspaceTasks = [];
|
|
||||||
if (this.runningWorkspaceTasksRefreshTimer) {
|
|
||||||
clearTimeout(this.runningWorkspaceTasksRefreshTimer);
|
|
||||||
this.runningWorkspaceTasksRefreshTimer = null;
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const resp = await fetch('/api/tasks');
|
|
||||||
const payload = await resp.json().catch(() => ({}));
|
|
||||||
if (!resp.ok || !payload?.success) {
|
|
||||||
throw new Error(payload?.error || '获取运行任务失败');
|
|
||||||
}
|
|
||||||
const tasks = Array.isArray(payload.data) ? payload.data : [];
|
|
||||||
const activeStatuses = new Set(['pending', 'running', 'cancel_requested']);
|
|
||||||
const terminalStatuses = new Set(['succeeded', 'failed', 'canceled']);
|
|
||||||
const trackedTaskIds = this.getStoredWorkspaceTaskIdSet?.('tracked') || new Set();
|
|
||||||
const acknowledged = new Set(
|
|
||||||
Array.isArray(this.acknowledgedCompletedTaskIds) ? this.acknowledgedCompletedTaskIds : []
|
|
||||||
);
|
|
||||||
const storedAcknowledged = this.getStoredWorkspaceTaskIdSet?.('acknowledged') || new Set();
|
|
||||||
storedAcknowledged.forEach((id: string) => acknowledged.add(id));
|
|
||||||
this.acknowledgedCompletedTaskIds = Array.from(acknowledged);
|
|
||||||
const previousVisibleIds = new Set(
|
|
||||||
(Array.isArray(this.runningWorkspaceTasks) ? this.runningWorkspaceTasks : [])
|
|
||||||
.map((task: any) => String(task?.task_id || ''))
|
|
||||||
.filter(Boolean)
|
|
||||||
);
|
|
||||||
const visibleTasks = tasks.filter((task: any) => {
|
|
||||||
const taskId = String(task?.task_id || '');
|
|
||||||
const status = String(task?.status || '');
|
|
||||||
const conversationId = String(task?.conversation_id || '');
|
|
||||||
if (!taskId) return false;
|
|
||||||
if (activeStatuses.has(status)) {
|
|
||||||
trackedTaskIds.add(taskId);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
// 如果完成的是当前正在查看的对话,用户已经“看到了”,不要再进入待查看/显示勾。
|
|
||||||
if (terminalStatuses.has(status) && conversationId === this.currentConversationId) {
|
|
||||||
if (!acknowledged.has(taskId)) {
|
|
||||||
acknowledged.add(taskId);
|
|
||||||
this.acknowledgedCompletedTaskIds = [
|
|
||||||
...(Array.isArray(this.acknowledgedCompletedTaskIds)
|
|
||||||
? this.acknowledgedCompletedTaskIds
|
|
||||||
: []),
|
|
||||||
taskId
|
|
||||||
];
|
|
||||||
}
|
|
||||||
trackedTaskIds.delete(taskId);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
// 刚完成的任务保留为“待查看”状态,直到用户点击进入该对话。
|
|
||||||
return (
|
|
||||||
terminalStatuses.has(status) &&
|
|
||||||
(previousVisibleIds.has(taskId) || trackedTaskIds.has(taskId)) &&
|
|
||||||
!acknowledged.has(taskId)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
this.runningWorkspaceTasks = visibleTasks;
|
|
||||||
visibleTasks.forEach((task: any) => {
|
|
||||||
const taskId = String(task?.task_id || '');
|
|
||||||
if (taskId) trackedTaskIds.add(taskId);
|
|
||||||
});
|
|
||||||
acknowledged.forEach((id: string) => trackedTaskIds.delete(id));
|
|
||||||
this.setStoredWorkspaceTaskIdSet?.('tracked', trackedTaskIds);
|
|
||||||
this.setStoredWorkspaceTaskIdSet?.('acknowledged', acknowledged);
|
|
||||||
const hasActiveVisibleTask = visibleTasks.some((task: any) =>
|
|
||||||
activeStatuses.has(String(task?.status || ''))
|
|
||||||
);
|
|
||||||
if (this.runningWorkspaceTasksRefreshTimer) {
|
|
||||||
clearTimeout(this.runningWorkspaceTasksRefreshTimer);
|
|
||||||
this.runningWorkspaceTasksRefreshTimer = null;
|
|
||||||
}
|
|
||||||
// 如果用户已经切到其他对话/工作区,当前运行任务没有本地轮询事件可触发列表更新。
|
|
||||||
// 这里轻量轮询任务列表,使 loader 能在后台任务完成后自动切成“待查看”的勾。
|
|
||||||
if (hasActiveVisibleTask) {
|
|
||||||
this.runningWorkspaceTasksRefreshTimer = window.setTimeout(() => {
|
|
||||||
this.runningWorkspaceTasksRefreshTimer = null;
|
|
||||||
void this.refreshRunningWorkspaceTasks?.();
|
|
||||||
}, 1500);
|
|
||||||
}
|
|
||||||
if (Array.isArray(this.hostWorkspaces)) {
|
|
||||||
const counts = visibleTasks.reduce((acc: Record<string, number>, task: any) => {
|
|
||||||
const ws = String(task?.workspace_id || '');
|
|
||||||
const status = String(task?.status || '');
|
|
||||||
if (ws && activeStatuses.has(status)) acc[ws] = (acc[ws] || 0) + 1;
|
|
||||||
return acc;
|
|
||||||
}, {});
|
|
||||||
this.hostWorkspaces = this.hostWorkspaces.map((item: any) => ({
|
|
||||||
...item,
|
|
||||||
running_task_count: Number(counts[item.workspace_id] || 0)
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('刷新运行中任务失败:', error);
|
|
||||||
this.runningWorkspaceTasks = [];
|
|
||||||
if (this.runningWorkspaceTasksRefreshTimer) {
|
|
||||||
clearTimeout(this.runningWorkspaceTasksRefreshTimer);
|
|
||||||
this.runningWorkspaceTasksRefreshTimer = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
getVisibleWorkspaceTaskForConversation(conversationId) {
|
|
||||||
const id = String(conversationId || '').trim();
|
|
||||||
if (!id) return null;
|
|
||||||
const tasks = Array.isArray(this.runningWorkspaceTasks) ? this.runningWorkspaceTasks : [];
|
|
||||||
return (
|
|
||||||
tasks.find((task: any) => String(task?.conversation_id || '') === id) || null
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
getStoredWorkspaceTaskIdSet(kind = 'tracked') {
|
|
||||||
if (typeof window === 'undefined') return new Set();
|
|
||||||
const key =
|
|
||||||
kind === 'acknowledged'
|
|
||||||
? 'agents.hostWorkspaceTasks.acknowledged'
|
|
||||||
: 'agents.hostWorkspaceTasks.tracked';
|
|
||||||
try {
|
|
||||||
const raw = window.localStorage?.getItem(key);
|
|
||||||
const values = JSON.parse(raw || '[]');
|
|
||||||
return new Set(
|
|
||||||
(Array.isArray(values) ? values : [])
|
|
||||||
.map((item: any) => String(item || '').trim())
|
|
||||||
.filter(Boolean)
|
|
||||||
);
|
|
||||||
} catch {
|
|
||||||
return new Set();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
setStoredWorkspaceTaskIdSet(kind = 'tracked', values) {
|
|
||||||
if (typeof window === 'undefined') return;
|
|
||||||
const key =
|
|
||||||
kind === 'acknowledged'
|
|
||||||
? 'agents.hostWorkspaceTasks.acknowledged'
|
|
||||||
: 'agents.hostWorkspaceTasks.tracked';
|
|
||||||
try {
|
|
||||||
window.localStorage?.setItem(
|
|
||||||
key,
|
|
||||||
JSON.stringify(Array.from(values || []).map((item: any) => String(item)))
|
|
||||||
);
|
|
||||||
} catch {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
acknowledgeCompletedWorkspaceTask(taskId) {
|
|
||||||
const id = String(taskId || '').trim();
|
|
||||||
if (!id) return;
|
|
||||||
const current = Array.isArray(this.acknowledgedCompletedTaskIds)
|
|
||||||
? this.acknowledgedCompletedTaskIds
|
|
||||||
: [];
|
|
||||||
if (!current.includes(id)) {
|
|
||||||
this.acknowledgedCompletedTaskIds = [...current, id];
|
|
||||||
}
|
|
||||||
const acknowledged = this.getStoredWorkspaceTaskIdSet?.('acknowledged') || new Set();
|
|
||||||
acknowledged.add(id);
|
|
||||||
this.setStoredWorkspaceTaskIdSet?.('acknowledged', acknowledged);
|
|
||||||
const tracked = this.getStoredWorkspaceTaskIdSet?.('tracked') || new Set();
|
|
||||||
tracked.delete(id);
|
|
||||||
this.setStoredWorkspaceTaskIdSet?.('tracked', tracked);
|
|
||||||
this.runningWorkspaceTasks = (Array.isArray(this.runningWorkspaceTasks)
|
|
||||||
? this.runningWorkspaceTasks
|
|
||||||
: []
|
|
||||||
).filter((item: any) => String(item?.task_id || '') !== id);
|
|
||||||
},
|
|
||||||
|
|
||||||
async openRunningWorkspaceTask(task) {
|
|
||||||
const workspaceId = String(task?.workspace_id || '').trim();
|
|
||||||
const conversationId = String(task?.conversation_id || '').trim();
|
|
||||||
if (!workspaceId || !conversationId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if ((this.versioningHostMode || this.dockerProjectMode) && workspaceId !== this.currentHostWorkspaceId) {
|
|
||||||
await this.handleHostWorkspaceSwitch(workspaceId);
|
|
||||||
}
|
|
||||||
if (this.currentConversationId === conversationId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await this.loadConversation(conversationId, { force: true });
|
|
||||||
const taskId = String(task?.task_id || '');
|
|
||||||
if (taskId && !['pending', 'running', 'cancel_requested'].includes(String(task?.status || ''))) {
|
|
||||||
this.acknowledgeCompletedWorkspaceTask(taskId);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// 复用刷新页面时的运行任务恢复逻辑:它会等待历史加载、按事件重建未完成的 assistant
|
|
||||||
// 消息,并重新注册思考/工具块,避免重复加载和块分裂。
|
|
||||||
await this.restoreTaskState();
|
|
||||||
},
|
|
||||||
|
|
||||||
async handleHostWorkspaceSwitch(workspaceId) {
|
async handleHostWorkspaceSwitch(workspaceId) {
|
||||||
const targetId = String(workspaceId || '').trim();
|
const targetId = String(workspaceId || '').trim();
|
||||||
if (!targetId || this.hostWorkspaceSwitching) {
|
if (!targetId || this.hostWorkspaceSwitching) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!(this.versioningHostMode || this.dockerProjectMode)) {
|
if (!this.versioningHostMode) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (targetId === this.currentHostWorkspaceId) {
|
if (targetId === this.currentHostWorkspaceId) {
|
||||||
@ -991,14 +756,6 @@ export const uiMethods = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.hostWorkspaceSwitching = true;
|
this.hostWorkspaceSwitching = true;
|
||||||
const previousConversationList = Array.isArray(this.conversations) ? [...this.conversations] : [];
|
|
||||||
const previousSearchActive = this.searchActive;
|
|
||||||
this.searchActive = false;
|
|
||||||
this.searchResults = [];
|
|
||||||
this.conversations = [];
|
|
||||||
this.conversationsOffset = 0;
|
|
||||||
this.hasMoreConversations = false;
|
|
||||||
this.conversationsLoading = true;
|
|
||||||
try {
|
try {
|
||||||
// 先在“当前工作区作用域”落盘草稿,再切换工作区。
|
// 先在“当前工作区作用域”落盘草稿,再切换工作区。
|
||||||
// 否则会把旧工作区草稿误写到目标工作区作用域里。
|
// 否则会把旧工作区草稿误写到目标工作区作用域里。
|
||||||
@ -1009,11 +766,10 @@ export const uiMethods = {
|
|||||||
}).catch(() => {});
|
}).catch(() => {});
|
||||||
|
|
||||||
const query = encodeURIComponent(targetId);
|
const query = encodeURIComponent(targetId);
|
||||||
const selectEndpoint = this.versioningHostMode ? '/api/host/workspaces/select' : '/api/projects/select';
|
const resp = await fetch(`/api/host/workspaces/select?workspace_id=${query}`);
|
||||||
const resp = await fetch(`${selectEndpoint}?workspace_id=${query}`);
|
|
||||||
const payload = await resp.json().catch(() => ({}));
|
const payload = await resp.json().catch(() => ({}));
|
||||||
if (!resp.ok || !payload?.success) {
|
if (!resp.ok || !payload?.success) {
|
||||||
throw new Error(payload?.error || (this.versioningHostMode ? '切换宿主机工作区失败' : '切换项目失败'));
|
throw new Error(payload?.error || '切换宿主机工作区失败');
|
||||||
}
|
}
|
||||||
const data = payload.data || {};
|
const data = payload.data || {};
|
||||||
this.currentHostWorkspaceId = String(data.current_workspace_id || targetId);
|
this.currentHostWorkspaceId = String(data.current_workspace_id || targetId);
|
||||||
@ -1023,48 +779,18 @@ export const uiMethods = {
|
|||||||
if (data.default_workspace_id) {
|
if (data.default_workspace_id) {
|
||||||
this.defaultHostWorkspaceId = String(data.default_workspace_id);
|
this.defaultHostWorkspaceId = String(data.default_workspace_id);
|
||||||
}
|
}
|
||||||
this.messages = [];
|
|
||||||
this.currentConversationId = null;
|
|
||||||
this.currentConversationTitle = '新对话';
|
|
||||||
this.titleReady = true;
|
|
||||||
this.suppressTitleTyping = false;
|
|
||||||
try {
|
|
||||||
const { useTaskStore } = await import('../../stores/task');
|
|
||||||
const taskStore = useTaskStore();
|
|
||||||
taskStore.clearTask();
|
|
||||||
} catch (_) {}
|
|
||||||
this.clearLocalTaskUiState?.('switch-host-workspace');
|
|
||||||
await this.fetchHostWorkspaces();
|
|
||||||
const activeStatuses = new Set(['pending', 'running', 'cancel_requested']);
|
|
||||||
const activeTask = (Array.isArray(this.runningWorkspaceTasks) ? this.runningWorkspaceTasks : [])
|
|
||||||
.filter((task: any) => String(task?.workspace_id || '') === this.currentHostWorkspaceId)
|
|
||||||
.find((task: any) => activeStatuses.has(String(task?.status || '')) && task?.conversation_id);
|
|
||||||
if (activeTask?.conversation_id) {
|
|
||||||
await this.loadConversation(String(activeTask.conversation_id), { force: true });
|
|
||||||
this.conversationsOffset = 0;
|
|
||||||
await this.loadConversationsList();
|
|
||||||
} else {
|
|
||||||
this.startTitleTyping('新对话', { animate: false });
|
|
||||||
history.replaceState({}, '', '/new');
|
|
||||||
await this.loadConversationsList();
|
|
||||||
}
|
|
||||||
const switchedWorkspace = (Array.isArray(this.hostWorkspaces) ? this.hostWorkspaces : [])
|
|
||||||
.find((item: any) => String(item?.workspace_id || '') === this.currentHostWorkspaceId);
|
|
||||||
const switchedLabel = String(switchedWorkspace?.label || '').trim();
|
|
||||||
this.uiPushToast({
|
this.uiPushToast({
|
||||||
title: this.versioningHostMode ? '工作区已切换' : '项目已切换',
|
title: '工作区已切换',
|
||||||
message: this.versioningHostMode
|
message: '正在刷新页面…',
|
||||||
? (data.project_path || switchedLabel || targetId)
|
|
||||||
: (switchedLabel || targetId),
|
|
||||||
type: 'success'
|
type: 'success'
|
||||||
});
|
});
|
||||||
|
window.setTimeout(() => {
|
||||||
|
window.location.reload();
|
||||||
|
}, 260);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.conversations = previousConversationList;
|
|
||||||
this.searchActive = previousSearchActive;
|
|
||||||
this.conversationsLoading = false;
|
|
||||||
const message = error instanceof Error ? error.message : String(error || '切换失败');
|
const message = error instanceof Error ? error.message : String(error || '切换失败');
|
||||||
this.uiPushToast({
|
this.uiPushToast({
|
||||||
title: this.versioningHostMode ? '切换工作区失败' : '切换项目失败',
|
title: '切换工作区失败',
|
||||||
message,
|
message,
|
||||||
type: 'error'
|
type: 'error'
|
||||||
});
|
});
|
||||||
@ -1074,7 +800,7 @@ export const uiMethods = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
async handleCreateHostWorkspace() {
|
async handleCreateHostWorkspace() {
|
||||||
if (!(this.versioningHostMode || this.dockerProjectMode)) {
|
if (!this.versioningHostMode) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (this.hostWorkspaceSwitching || this.hostWorkspaceCreateSubmitting) {
|
if (this.hostWorkspaceSwitching || this.hostWorkspaceCreateSubmitting) {
|
||||||
@ -1086,23 +812,6 @@ export const uiMethods = {
|
|||||||
this.hostWorkspaceCreateDialogOpen = true;
|
this.hostWorkspaceCreateDialogOpen = true;
|
||||||
},
|
},
|
||||||
|
|
||||||
async openHostWorkspaceManageDialog() {
|
|
||||||
if (!(this.versioningHostMode || this.dockerProjectMode)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.hostWorkspaceCreateError = '';
|
|
||||||
await this.fetchHostWorkspaces();
|
|
||||||
this.hostWorkspaceManageDialogOpen = true;
|
|
||||||
},
|
|
||||||
|
|
||||||
closeHostWorkspaceManageDialog() {
|
|
||||||
if (this.hostWorkspaceCreateSubmitting || this.hostWorkspaceManageSubmitting) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.hostWorkspaceManageDialogOpen = false;
|
|
||||||
this.hostWorkspaceCreateError = '';
|
|
||||||
},
|
|
||||||
|
|
||||||
closeHostWorkspaceCreateDialog() {
|
closeHostWorkspaceCreateDialog() {
|
||||||
if (this.hostWorkspaceCreateSubmitting) {
|
if (this.hostWorkspaceCreateSubmitting) {
|
||||||
return;
|
return;
|
||||||
@ -1112,38 +821,34 @@ export const uiMethods = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
async submitHostWorkspaceCreate() {
|
async submitHostWorkspaceCreate() {
|
||||||
if (!(this.versioningHostMode || this.dockerProjectMode)) {
|
if (!this.versioningHostMode) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (this.hostWorkspaceSwitching || this.hostWorkspaceCreateSubmitting) {
|
if (this.hostWorkspaceSwitching || this.hostWorkspaceCreateSubmitting) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const workspacePath = String(this.hostWorkspaceCreatePath || '').trim();
|
const workspacePath = String(this.hostWorkspaceCreatePath || '').trim();
|
||||||
const label = String(this.hostWorkspaceCreateLabel || '').trim();
|
if (!workspacePath) {
|
||||||
if (this.versioningHostMode && !workspacePath) {
|
|
||||||
this.hostWorkspaceCreateError = '路径不能为空';
|
this.hostWorkspaceCreateError = '路径不能为空';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (this.dockerProjectMode && !label) {
|
const label = String(this.hostWorkspaceCreateLabel || '').trim();
|
||||||
this.hostWorkspaceCreateError = '项目名称不能为空';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.hostWorkspaceCreateSubmitting = true;
|
this.hostWorkspaceCreateSubmitting = true;
|
||||||
this.hostWorkspaceCreateError = '';
|
this.hostWorkspaceCreateError = '';
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(this.versioningHostMode ? '/api/host/workspaces/create' : '/api/projects/create', {
|
const resp = await fetch('/api/host/workspaces/create', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
credentials: 'same-origin',
|
credentials: 'same-origin',
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
path: this.versioningHostMode ? workspacePath : undefined,
|
path: workspacePath,
|
||||||
label
|
label
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
const payload = await resp.json().catch(() => ({}));
|
const payload = await resp.json().catch(() => ({}));
|
||||||
if (!resp.ok || !payload?.success) {
|
if (!resp.ok || !payload?.success) {
|
||||||
throw new Error(payload?.error || (this.versioningHostMode ? '新建宿主机工作区失败' : '新建项目失败'));
|
throw new Error(payload?.error || '新建宿主机工作区失败');
|
||||||
}
|
}
|
||||||
await this.fetchHostWorkspaces();
|
await this.fetchHostWorkspaces();
|
||||||
const createdLabel = payload?.data?.workspace?.label || label || workspacePath;
|
const createdLabel = payload?.data?.workspace?.label || label || workspacePath;
|
||||||
@ -1151,7 +856,7 @@ export const uiMethods = {
|
|||||||
this.hostWorkspaceCreatePath = '';
|
this.hostWorkspaceCreatePath = '';
|
||||||
this.hostWorkspaceCreateLabel = '';
|
this.hostWorkspaceCreateLabel = '';
|
||||||
this.uiPushToast({
|
this.uiPushToast({
|
||||||
title: this.versioningHostMode ? '工作区已创建' : '项目已创建',
|
title: '工作区已创建',
|
||||||
message: createdLabel,
|
message: createdLabel,
|
||||||
type: 'success'
|
type: 'success'
|
||||||
});
|
});
|
||||||
@ -1159,7 +864,7 @@ export const uiMethods = {
|
|||||||
const message = error instanceof Error ? error.message : String(error || '新建失败');
|
const message = error instanceof Error ? error.message : String(error || '新建失败');
|
||||||
this.hostWorkspaceCreateError = message;
|
this.hostWorkspaceCreateError = message;
|
||||||
this.uiPushToast({
|
this.uiPushToast({
|
||||||
title: this.versioningHostMode ? '新建工作区失败' : '新建项目失败',
|
title: '新建工作区失败',
|
||||||
message,
|
message,
|
||||||
type: 'error'
|
type: 'error'
|
||||||
});
|
});
|
||||||
@ -1168,129 +873,6 @@ export const uiMethods = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async submitHostWorkspaceCreateFromManage(payload = {}) {
|
|
||||||
this.hostWorkspaceCreatePath = String(payload?.path || '').trim();
|
|
||||||
this.hostWorkspaceCreateLabel = String(payload?.label || '').trim();
|
|
||||||
await this.submitHostWorkspaceCreate();
|
|
||||||
},
|
|
||||||
|
|
||||||
async renameHostWorkspace(payload = {}) {
|
|
||||||
if (!(this.versioningHostMode || this.dockerProjectMode)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const workspaceId = String(payload?.workspace_id || '').trim();
|
|
||||||
const label = String(payload?.label || '').trim();
|
|
||||||
if (!workspaceId || !label) {
|
|
||||||
this.hostWorkspaceCreateError = this.dockerProjectMode ? '项目名称不能为空' : '工作区名称不能为空';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.hostWorkspaceManageSubmitting = true;
|
|
||||||
this.hostWorkspaceCreateError = '';
|
|
||||||
try {
|
|
||||||
const endpoint = this.versioningHostMode
|
|
||||||
? '/api/host/workspaces/rename'
|
|
||||||
: '/api/projects/rename';
|
|
||||||
const resp = await fetch(endpoint, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
credentials: 'same-origin',
|
|
||||||
body: JSON.stringify({ workspace_id: workspaceId, label })
|
|
||||||
});
|
|
||||||
const result = await resp.json().catch(() => ({}));
|
|
||||||
if (!resp.ok || !result?.success) {
|
|
||||||
throw new Error(result?.error || '重命名失败');
|
|
||||||
}
|
|
||||||
await this.fetchHostWorkspaces();
|
|
||||||
this.uiPushToast({
|
|
||||||
title: this.versioningHostMode ? '工作区已重命名' : '项目已重命名',
|
|
||||||
message: label,
|
|
||||||
type: 'success'
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
const message = error instanceof Error ? error.message : String(error || '重命名失败');
|
|
||||||
this.hostWorkspaceCreateError = message;
|
|
||||||
this.uiPushToast({
|
|
||||||
title: this.versioningHostMode ? '重命名工作区失败' : '重命名项目失败',
|
|
||||||
message,
|
|
||||||
type: 'error'
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
this.hostWorkspaceManageSubmitting = false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async deleteHostWorkspace(item) {
|
|
||||||
if (!(this.versioningHostMode || this.dockerProjectMode)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const workspaceId = String(item?.workspace_id || '').trim();
|
|
||||||
if (!workspaceId) return;
|
|
||||||
const kindLabel = this.versioningHostMode ? '工作区' : '项目';
|
|
||||||
const wasCurrent = workspaceId === this.currentHostWorkspaceId;
|
|
||||||
const ok = await this.confirmAction({
|
|
||||||
title: `删除${kindLabel}`,
|
|
||||||
message: this.versioningHostMode
|
|
||||||
? `确定要从列表中删除“${item?.label || workspaceId}”吗?不会删除磁盘上的工作区文件夹。`
|
|
||||||
: `确定要删除项目“${item?.label || workspaceId}”吗?该项目文件夹和对话记录会被一并删除。`,
|
|
||||||
confirmText: '删除',
|
|
||||||
cancelText: '取消',
|
|
||||||
confirmVariant: 'danger'
|
|
||||||
});
|
|
||||||
if (!ok) return;
|
|
||||||
|
|
||||||
this.hostWorkspaceManageSubmitting = true;
|
|
||||||
this.hostWorkspaceCreateError = '';
|
|
||||||
try {
|
|
||||||
const endpoint = this.versioningHostMode
|
|
||||||
? '/api/host/workspaces/delete'
|
|
||||||
: '/api/projects/delete';
|
|
||||||
const resp = await fetch(endpoint, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
credentials: 'same-origin',
|
|
||||||
body: JSON.stringify({ workspace_id: workspaceId })
|
|
||||||
});
|
|
||||||
const result = await resp.json().catch(() => ({}));
|
|
||||||
if (!resp.ok || !result?.success) {
|
|
||||||
throw new Error(result?.error || '删除失败');
|
|
||||||
}
|
|
||||||
const data = result.data || {};
|
|
||||||
this.currentHostWorkspaceId = String(data.current_workspace_id || this.currentHostWorkspaceId || '');
|
|
||||||
if (data.default_workspace_id) {
|
|
||||||
this.defaultHostWorkspaceId = String(data.default_workspace_id);
|
|
||||||
}
|
|
||||||
await this.fetchHostWorkspaces();
|
|
||||||
if (wasCurrent) {
|
|
||||||
this.messages = [];
|
|
||||||
this.currentConversationId = null;
|
|
||||||
this.currentConversationTitle = '新对话';
|
|
||||||
this.searchActive = false;
|
|
||||||
this.searchResults = [];
|
|
||||||
this.conversations = [];
|
|
||||||
this.conversationsOffset = 0;
|
|
||||||
this.hasMoreConversations = false;
|
|
||||||
this.conversationsLoading = true;
|
|
||||||
history.replaceState({}, '', '/new');
|
|
||||||
await this.loadConversationsList();
|
|
||||||
}
|
|
||||||
this.uiPushToast({
|
|
||||||
title: `${kindLabel}已删除`,
|
|
||||||
message: item?.label || workspaceId,
|
|
||||||
type: 'success'
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
const message = error instanceof Error ? error.message : String(error || '删除失败');
|
|
||||||
this.hostWorkspaceCreateError = message;
|
|
||||||
this.uiPushToast({
|
|
||||||
title: `删除${kindLabel}失败`,
|
|
||||||
message,
|
|
||||||
type: 'error'
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
this.hostWorkspaceManageSubmitting = false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
fetchTodoList() {
|
fetchTodoList() {
|
||||||
return this.fileFetchTodoList();
|
return this.fileFetchTodoList();
|
||||||
},
|
},
|
||||||
@ -2738,12 +2320,7 @@ export const uiMethods = {
|
|||||||
const payload = await resp.json();
|
const payload = await resp.json();
|
||||||
const tasks = Array.isArray(payload?.data) ? payload.data : [];
|
const tasks = Array.isArray(payload?.data) ? payload.data : [];
|
||||||
const runningTask = tasks.find(
|
const runningTask = tasks.find(
|
||||||
(task: any) =>
|
(task: any) => task?.status === 'running' && task?.conversation_id
|
||||||
task?.status === 'running' &&
|
|
||||||
task?.conversation_id &&
|
|
||||||
(!(this.versioningHostMode || this.dockerProjectMode) ||
|
|
||||||
!this.currentHostWorkspaceId ||
|
|
||||||
task?.workspace_id === this.currentHostWorkspaceId)
|
|
||||||
);
|
);
|
||||||
return runningTask?.conversation_id || null;
|
return runningTask?.conversation_id || null;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -3241,18 +2818,19 @@ export const uiMethods = {
|
|||||||
let treePromise: Promise<any> | null = null;
|
let treePromise: Promise<any> | null = null;
|
||||||
const isHostMode = statusData?.container?.mode === 'host';
|
const isHostMode = statusData?.container?.mode === 'host';
|
||||||
this.versioningHostMode = !!isHostMode;
|
this.versioningHostMode = !!isHostMode;
|
||||||
this.dockerProjectMode = !isHostMode;
|
|
||||||
if (isHostMode) {
|
if (isHostMode) {
|
||||||
this.fileMarkTreeUnavailable('宿主机模式下文件树不可用');
|
this.fileMarkTreeUnavailable('宿主机模式下文件树不可用');
|
||||||
await this.fetchHostWorkspaces();
|
await this.fetchHostWorkspaces();
|
||||||
} else {
|
} else {
|
||||||
this.fileMarkTreeUnavailable('Docker 模式下文件区已改为项目列表');
|
this.hostWorkspaces = [];
|
||||||
await this.fetchHostWorkspaces();
|
this.currentHostWorkspaceId = '';
|
||||||
|
this.defaultHostWorkspaceId = '';
|
||||||
this.hostWorkspaceCreateDialogOpen = false;
|
this.hostWorkspaceCreateDialogOpen = false;
|
||||||
this.hostWorkspaceCreatePath = '';
|
this.hostWorkspaceCreatePath = '';
|
||||||
this.hostWorkspaceCreateLabel = '';
|
this.hostWorkspaceCreateLabel = '';
|
||||||
this.hostWorkspaceCreateError = '';
|
this.hostWorkspaceCreateError = '';
|
||||||
this.hostWorkspaceCreateSubmitting = false;
|
this.hostWorkspaceCreateSubmitting = false;
|
||||||
|
treePromise = this.fileFetchTree();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取当前对话信息
|
// 获取当前对话信息
|
||||||
|
|||||||
@ -25,8 +25,6 @@ export function dataState() {
|
|||||||
taskInProgress: false,
|
taskInProgress: false,
|
||||||
// 等待后台通知期间,用于发现并接管新建任务的轮询定时器
|
// 等待后台通知期间,用于发现并接管新建任务的轮询定时器
|
||||||
waitingTaskProbeTimer: null,
|
waitingTaskProbeTimer: null,
|
||||||
// 宿主机多工作区任务列表后台刷新定时器(用于当前未查看运行对话时同步完成态)
|
|
||||||
runningWorkspaceTasksRefreshTimer: null,
|
|
||||||
// 运行期消息堆积(提前发送 / 引导对话)
|
// 运行期消息堆积(提前发送 / 引导对话)
|
||||||
runtimeQueuedMessages: [],
|
runtimeQueuedMessages: [],
|
||||||
runtimeGuidanceFallbackQueue: [],
|
runtimeGuidanceFallbackQueue: [],
|
||||||
@ -114,7 +112,6 @@ export function dataState() {
|
|||||||
pathAuthorizationDraft: '',
|
pathAuthorizationDraft: '',
|
||||||
pathAuthorizationSaving: false,
|
pathAuthorizationSaving: false,
|
||||||
versioningHostMode: false,
|
versioningHostMode: false,
|
||||||
dockerProjectMode: false,
|
|
||||||
hostWorkspaces: [],
|
hostWorkspaces: [],
|
||||||
currentHostWorkspaceId: '',
|
currentHostWorkspaceId: '',
|
||||||
defaultHostWorkspaceId: '',
|
defaultHostWorkspaceId: '',
|
||||||
@ -124,8 +121,6 @@ export function dataState() {
|
|||||||
hostWorkspaceCreateLabel: '',
|
hostWorkspaceCreateLabel: '',
|
||||||
hostWorkspaceCreateSubmitting: false,
|
hostWorkspaceCreateSubmitting: false,
|
||||||
hostWorkspaceCreateError: '',
|
hostWorkspaceCreateError: '',
|
||||||
hostWorkspaceManageDialogOpen: false,
|
|
||||||
hostWorkspaceManageSubmitting: false,
|
|
||||||
versioningEnabled: false,
|
versioningEnabled: false,
|
||||||
newConversationVersioningEnabled: false,
|
newConversationVersioningEnabled: false,
|
||||||
versioningTrackingMode: 'workspace_and_conversation',
|
versioningTrackingMode: 'workspace_and_conversation',
|
||||||
|
|||||||
@ -3,14 +3,12 @@
|
|||||||
class="host-workspace-dialog-overlay"
|
class="host-workspace-dialog-overlay"
|
||||||
role="dialog"
|
role="dialog"
|
||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
:aria-label="workspaceKind === 'project' ? '新建项目' : '新建工作区'"
|
aria-label="新建工作区"
|
||||||
@click.self="handleOverlayClose"
|
@click.self="handleOverlayClose"
|
||||||
>
|
>
|
||||||
<form class="host-workspace-dialog" @submit.prevent="$emit('submit')">
|
<form class="host-workspace-dialog" @submit.prevent="$emit('submit')">
|
||||||
<div class="host-workspace-dialog__header">
|
<div class="host-workspace-dialog__header">
|
||||||
<div class="host-workspace-dialog__title">
|
<div class="host-workspace-dialog__title">新建工作区</div>
|
||||||
{{ workspaceKind === 'project' ? '新建项目' : '新建工作区' }}
|
|
||||||
</div>
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="host-workspace-dialog__close"
|
class="host-workspace-dialog__close"
|
||||||
@ -22,10 +20,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="host-workspace-dialog__body">
|
<div class="host-workspace-dialog__body">
|
||||||
<label v-if="workspaceKind !== 'project'" class="host-workspace-dialog__field">
|
<label class="host-workspace-dialog__field">
|
||||||
<span class="host-workspace-dialog__label">
|
<span class="host-workspace-dialog__label">工作区路径</span>
|
||||||
工作区路径
|
|
||||||
</span>
|
|
||||||
<input
|
<input
|
||||||
:value="pathValue"
|
:value="pathValue"
|
||||||
type="text"
|
type="text"
|
||||||
@ -38,14 +34,12 @@
|
|||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label class="host-workspace-dialog__field">
|
<label class="host-workspace-dialog__field">
|
||||||
<span class="host-workspace-dialog__label">
|
<span class="host-workspace-dialog__label">工作区名称(可选)</span>
|
||||||
{{ workspaceKind === 'project' ? '项目名称' : '工作区名称(可选)' }}
|
|
||||||
</span>
|
|
||||||
<input
|
<input
|
||||||
:value="labelValue"
|
:value="labelValue"
|
||||||
type="text"
|
type="text"
|
||||||
class="host-workspace-dialog__input"
|
class="host-workspace-dialog__input"
|
||||||
:placeholder="workspaceKind === 'project' ? '例如:客户 A 项目' : '例如:客户A项目'"
|
placeholder="例如:客户A项目"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
:disabled="submitting"
|
:disabled="submitting"
|
||||||
@input="$emit('update:labelValue', ($event.target as HTMLInputElement).value)"
|
@input="$emit('update:labelValue', ($event.target as HTMLInputElement).value)"
|
||||||
@ -60,7 +54,7 @@
|
|||||||
取消
|
取消
|
||||||
</button>
|
</button>
|
||||||
<button type="submit" class="host-workspace-dialog__btn primary" :disabled="submitting">
|
<button type="submit" class="host-workspace-dialog__btn primary" :disabled="submitting">
|
||||||
{{ submitting ? '创建中...' : (workspaceKind === 'project' ? '创建项目' : '创建工作区') }}
|
{{ submitting ? '创建中...' : '创建工作区' }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
@ -76,7 +70,6 @@ const props = defineProps<{
|
|||||||
labelValue: string;
|
labelValue: string;
|
||||||
submitting?: boolean;
|
submitting?: boolean;
|
||||||
errorMessage?: string;
|
errorMessage?: string;
|
||||||
workspaceKind?: 'workspace' | 'project';
|
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emits = defineEmits<{
|
const emits = defineEmits<{
|
||||||
@ -86,8 +79,6 @@ const emits = defineEmits<{
|
|||||||
(event: 'update:labelValue', value: string): void;
|
(event: 'update:labelValue', value: string): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const workspaceKind = props.workspaceKind || 'workspace';
|
|
||||||
|
|
||||||
const handleOverlayClose = () => {
|
const handleOverlayClose = () => {
|
||||||
if (props.submitting) return;
|
if (props.submitting) return;
|
||||||
emits('close');
|
emits('close');
|
||||||
|
|||||||
@ -1,392 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div
|
|
||||||
class="host-workspace-dialog-overlay"
|
|
||||||
role="dialog"
|
|
||||||
aria-modal="true"
|
|
||||||
:aria-label="workspaceKind === 'project' ? '管理项目' : '管理工作区'"
|
|
||||||
@click.self="handleOverlayClose"
|
|
||||||
>
|
|
||||||
<form class="host-workspace-dialog host-workspace-manage" @submit.prevent="submitCreate">
|
|
||||||
<div class="host-workspace-dialog__header">
|
|
||||||
<div class="host-workspace-dialog__title">
|
|
||||||
{{ workspaceKind === 'project' ? '管理项目' : '管理工作区' }}
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="host-workspace-dialog__close"
|
|
||||||
:disabled="busy"
|
|
||||||
@click="$emit('close')"
|
|
||||||
>
|
|
||||||
关闭
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="host-workspace-dialog__body">
|
|
||||||
<section class="workspace-manage-section">
|
|
||||||
<div class="workspace-manage-section__title">
|
|
||||||
{{ workspaceKind === 'project' ? '新建项目' : '新建工作区' }}
|
|
||||||
</div>
|
|
||||||
<label v-if="workspaceKind !== 'project'" class="host-workspace-dialog__field">
|
|
||||||
<span class="host-workspace-dialog__label">工作区路径</span>
|
|
||||||
<input
|
|
||||||
v-model="createPath"
|
|
||||||
type="text"
|
|
||||||
class="host-workspace-dialog__input"
|
|
||||||
placeholder="请输入绝对路径或相对仓库路径"
|
|
||||||
autocomplete="off"
|
|
||||||
:disabled="busy"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<label class="host-workspace-dialog__field">
|
|
||||||
<span class="host-workspace-dialog__label">
|
|
||||||
{{ workspaceKind === 'project' ? '项目名称' : '工作区名称(可选)' }}
|
|
||||||
</span>
|
|
||||||
<input
|
|
||||||
v-model="createLabel"
|
|
||||||
type="text"
|
|
||||||
class="host-workspace-dialog__input"
|
|
||||||
:placeholder="workspaceKind === 'project' ? '例如:客户 A 项目' : '例如:客户A项目'"
|
|
||||||
autocomplete="off"
|
|
||||||
:disabled="busy"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<button type="submit" class="host-workspace-dialog__btn primary" :disabled="busy">
|
|
||||||
{{ createSubmitting ? '创建中...' : (workspaceKind === 'project' ? '新建项目' : '新建工作区') }}
|
|
||||||
</button>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="workspace-manage-section">
|
|
||||||
<div class="workspace-manage-section__title">
|
|
||||||
{{ workspaceKind === 'project' ? '已有项目' : '已有工作区' }}
|
|
||||||
</div>
|
|
||||||
<div v-if="workspaces.length" class="workspace-manage-list">
|
|
||||||
<div
|
|
||||||
v-for="item in workspaces"
|
|
||||||
:key="item.workspace_id"
|
|
||||||
class="workspace-manage-row"
|
|
||||||
:class="{ current: item.workspace_id === currentWorkspaceId }"
|
|
||||||
>
|
|
||||||
<div class="workspace-manage-main">
|
|
||||||
<input
|
|
||||||
class="host-workspace-dialog__input workspace-manage-name"
|
|
||||||
:value="renameDrafts[item.workspace_id] ?? item.label"
|
|
||||||
:disabled="busy"
|
|
||||||
@input="setRenameDraft(item.workspace_id, ($event.target as HTMLInputElement).value)"
|
|
||||||
/>
|
|
||||||
<div v-if="workspaceKind !== 'project'" class="workspace-manage-path">
|
|
||||||
{{ item.path || '(未配置路径)' }}
|
|
||||||
</div>
|
|
||||||
<div class="workspace-manage-badges">
|
|
||||||
<span v-if="item.workspace_id === defaultWorkspaceId" class="workspace-manage-badge">
|
|
||||||
{{ workspaceKind === 'project' ? '默认项目' : '默认工作区' }}
|
|
||||||
</span>
|
|
||||||
<span v-if="item.workspace_id === currentWorkspaceId" class="workspace-manage-badge">
|
|
||||||
当前
|
|
||||||
</span>
|
|
||||||
<span v-if="Number(item.running_task_count || 0) > 0" class="workspace-manage-badge running">
|
|
||||||
运行中
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="workspace-manage-actions">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="host-workspace-dialog__btn ghost"
|
|
||||||
:disabled="busy || !hasRenameChange(item)"
|
|
||||||
@click="submitRename(item)"
|
|
||||||
>
|
|
||||||
重命名
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="host-workspace-dialog__btn danger"
|
|
||||||
:disabled="busy || !canDelete(item)"
|
|
||||||
@click="$emit('delete', item)"
|
|
||||||
>
|
|
||||||
删除
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-else class="workspace-manage-empty">
|
|
||||||
{{ workspaceKind === 'project' ? '暂无项目' : '暂无工作区' }}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<div v-if="errorMessage" class="host-workspace-dialog__error">{{ errorMessage }}</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { reactive, ref, watch } from 'vue';
|
|
||||||
|
|
||||||
defineOptions({ name: 'HostWorkspaceManageDialog' });
|
|
||||||
|
|
||||||
type WorkspaceItem = {
|
|
||||||
workspace_id: string;
|
|
||||||
label: string;
|
|
||||||
path?: string;
|
|
||||||
running_task_count?: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
workspaces: WorkspaceItem[];
|
|
||||||
currentWorkspaceId?: string;
|
|
||||||
defaultWorkspaceId?: string;
|
|
||||||
workspaceKind?: 'workspace' | 'project';
|
|
||||||
busy?: boolean;
|
|
||||||
createSubmitting?: boolean;
|
|
||||||
errorMessage?: string;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const emits = defineEmits<{
|
|
||||||
(event: 'close'): void;
|
|
||||||
(event: 'create', payload: { path: string; label: string }): void;
|
|
||||||
(event: 'rename', payload: { workspace_id: string; label: string }): void;
|
|
||||||
(event: 'delete', item: WorkspaceItem): void;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const workspaceKind = props.workspaceKind || 'workspace';
|
|
||||||
const createPath = ref('');
|
|
||||||
const createLabel = ref('');
|
|
||||||
const renameDrafts = reactive<Record<string, string>>({});
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => props.workspaces,
|
|
||||||
(items) => {
|
|
||||||
for (const item of items || []) {
|
|
||||||
renameDrafts[item.workspace_id] = item.label || item.workspace_id;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ immediate: true }
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleOverlayClose = () => {
|
|
||||||
if (props.busy) return;
|
|
||||||
emits('close');
|
|
||||||
};
|
|
||||||
|
|
||||||
const setRenameDraft = (workspaceId: string, value: string) => {
|
|
||||||
renameDrafts[workspaceId] = value;
|
|
||||||
};
|
|
||||||
|
|
||||||
const hasRenameChange = (item: WorkspaceItem) => {
|
|
||||||
const draft = String(renameDrafts[item.workspace_id] ?? '').trim();
|
|
||||||
return !!draft && draft !== String(item.label || '').trim();
|
|
||||||
};
|
|
||||||
|
|
||||||
const canDelete = (item: WorkspaceItem) => {
|
|
||||||
if (Number(item.running_task_count || 0) > 0) return false;
|
|
||||||
if (workspaceKind === 'project' && item.workspace_id === 'default') return false;
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const submitCreate = () => {
|
|
||||||
emits('create', {
|
|
||||||
path: createPath.value.trim(),
|
|
||||||
label: createLabel.value.trim()
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const submitRename = (item: WorkspaceItem) => {
|
|
||||||
emits('rename', {
|
|
||||||
workspace_id: item.workspace_id,
|
|
||||||
label: String(renameDrafts[item.workspace_id] ?? '').trim()
|
|
||||||
});
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.host-workspace-dialog-overlay {
|
|
||||||
position: fixed;
|
|
||||||
inset: 0;
|
|
||||||
z-index: 1300;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 20px;
|
|
||||||
background: var(--theme-overlay-scrim);
|
|
||||||
backdrop-filter: blur(10px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.host-workspace-dialog {
|
|
||||||
width: min(680px, 96vw);
|
|
||||||
height: min(760px, 92vh);
|
|
||||||
border-radius: 18px;
|
|
||||||
border: 1px solid var(--theme-control-border-strong);
|
|
||||||
background: var(--theme-surface-card);
|
|
||||||
box-shadow: var(--theme-shadow-soft);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.host-workspace-dialog__header,
|
|
||||||
.host-workspace-dialog__actions {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 12px;
|
|
||||||
padding: 16px 18px;
|
|
||||||
border-bottom: 1px solid var(--theme-control-border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.host-workspace-dialog__title {
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 700;
|
|
||||||
color: var(--claude-text);
|
|
||||||
}
|
|
||||||
|
|
||||||
.host-workspace-dialog__close,
|
|
||||||
.host-workspace-dialog__btn {
|
|
||||||
border: 1px solid var(--theme-control-border-strong);
|
|
||||||
background: transparent;
|
|
||||||
color: var(--claude-text-secondary);
|
|
||||||
border-radius: 10px;
|
|
||||||
padding: 7px 12px;
|
|
||||||
cursor: pointer;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.host-workspace-dialog__close:disabled,
|
|
||||||
.host-workspace-dialog__btn:disabled {
|
|
||||||
opacity: 0.6;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.host-workspace-dialog__body {
|
|
||||||
padding: 16px 18px;
|
|
||||||
display: grid;
|
|
||||||
grid-template-rows: auto minmax(0, 1fr) auto;
|
|
||||||
gap: 16px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.workspace-manage-section {
|
|
||||||
display: grid;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.workspace-manage-section__title {
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 700;
|
|
||||||
color: var(--claude-text-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.host-workspace-dialog__field {
|
|
||||||
display: grid;
|
|
||||||
gap: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.host-workspace-dialog__label,
|
|
||||||
.workspace-manage-path {
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--claude-text-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.host-workspace-dialog__input {
|
|
||||||
border: 1px solid var(--theme-control-border-strong);
|
|
||||||
border-radius: 10px;
|
|
||||||
background: var(--theme-surface-soft);
|
|
||||||
color: var(--claude-text);
|
|
||||||
padding: 9px 11px;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 1.4;
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.host-workspace-dialog__input:focus {
|
|
||||||
border-color: var(--claude-accent);
|
|
||||||
box-shadow: 0 0 0 2px color-mix(in srgb, var(--claude-accent) 20%, transparent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.host-workspace-dialog__btn.primary {
|
|
||||||
justify-self: flex-start;
|
|
||||||
background: var(--claude-accent);
|
|
||||||
border-color: var(--claude-accent-strong);
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.host-workspace-dialog__btn.danger {
|
|
||||||
color: #ef4444;
|
|
||||||
border-color: color-mix(in srgb, #ef4444 45%, var(--theme-control-border-strong));
|
|
||||||
}
|
|
||||||
|
|
||||||
.workspace-manage-list {
|
|
||||||
display: grid;
|
|
||||||
align-content: start;
|
|
||||||
gap: 8px;
|
|
||||||
min-height: 0;
|
|
||||||
max-height: 100%;
|
|
||||||
overflow: auto;
|
|
||||||
scrollbar-width: none;
|
|
||||||
-ms-overflow-style: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.workspace-manage-list::-webkit-scrollbar {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.workspace-manage-row {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: minmax(0, 1fr) auto;
|
|
||||||
gap: 10px;
|
|
||||||
padding: 10px;
|
|
||||||
border: 1px solid var(--theme-control-border);
|
|
||||||
border-radius: 12px;
|
|
||||||
background: var(--theme-surface-soft);
|
|
||||||
}
|
|
||||||
|
|
||||||
.workspace-manage-row.current {
|
|
||||||
border-color: color-mix(in srgb, var(--claude-accent) 45%, var(--theme-control-border));
|
|
||||||
box-shadow: 0 0 0 1px color-mix(in srgb, var(--claude-accent) 18%, transparent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.workspace-manage-main {
|
|
||||||
display: grid;
|
|
||||||
gap: 6px;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.workspace-manage-name {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.workspace-manage-actions,
|
|
||||||
.workspace-manage-badges {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.workspace-manage-badge {
|
|
||||||
font-size: 11px;
|
|
||||||
color: var(--claude-text-secondary);
|
|
||||||
border: 1px solid var(--theme-control-border);
|
|
||||||
border-radius: 999px;
|
|
||||||
padding: 2px 7px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.workspace-manage-badge.running {
|
|
||||||
color: var(--claude-accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.workspace-manage-empty {
|
|
||||||
font-size: 13px;
|
|
||||||
color: var(--claude-text-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.host-workspace-dialog__error {
|
|
||||||
font-size: 13px;
|
|
||||||
color: #ef4444;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 640px) {
|
|
||||||
.workspace-manage-row {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -60,7 +60,7 @@
|
|||||||
type="button"
|
type="button"
|
||||||
:class="{ active: panelMode === 'files' }"
|
:class="{ active: panelMode === 'files' }"
|
||||||
@click.stop="$emit('select-panel', 'files')"
|
@click.stop="$emit('select-panel', 'files')"
|
||||||
:title="workspaceKind === 'project' ? '项目' : '项目文件'"
|
title="项目文件"
|
||||||
>
|
>
|
||||||
<span class="icon icon-md" :style="iconStyle('folder')" aria-hidden="true"></span>
|
<span class="icon icon-md" :style="iconStyle('folder')" aria-hidden="true"></span>
|
||||||
</button>
|
</button>
|
||||||
@ -107,9 +107,9 @@
|
|||||||
管理
|
管理
|
||||||
</button>
|
</button>
|
||||||
<h3>
|
<h3>
|
||||||
<span v-if="panelMode === 'files'" class="icon-label">
|
<span v-if="panelMode === 'files'" class="icon-label">
|
||||||
<span class="icon icon-sm" :style="iconStyle('folder')" aria-hidden="true"></span>
|
<span class="icon icon-sm" :style="iconStyle('folder')" aria-hidden="true"></span>
|
||||||
<span>{{ workspaceKind === 'project' ? '项目' : '项目文件' }}</span>
|
<span>项目文件</span>
|
||||||
</span>
|
</span>
|
||||||
<span v-else-if="panelMode === 'todo'" class="icon-label">
|
<span v-else-if="panelMode === 'todo'" class="icon-label">
|
||||||
<span class="icon icon-sm" :style="iconStyle('stickyNote')" aria-hidden="true"></span>
|
<span class="icon icon-sm" :style="iconStyle('stickyNote')" aria-hidden="true"></span>
|
||||||
@ -200,14 +200,12 @@
|
|||||||
type="button"
|
type="button"
|
||||||
class="host-workspace-add-btn"
|
class="host-workspace-add-btn"
|
||||||
:disabled="hostWorkspaceSwitching"
|
:disabled="hostWorkspaceSwitching"
|
||||||
@click="handleManageHostWorkspace"
|
@click="handleCreateHostWorkspace"
|
||||||
:title="workspaceKind === 'project' ? '管理项目' : '管理工作区'"
|
title="新建工作区"
|
||||||
>
|
>
|
||||||
<span class="icon icon-sm" :style="iconStyle('settings')" aria-hidden="true"></span>
|
+
|
||||||
</button>
|
</button>
|
||||||
<div class="host-workspace-title">
|
<div class="host-workspace-title">切换工作区</div>
|
||||||
{{ workspaceKind === 'project' ? '管理项目' : '管理工作区' }}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="host-workspace-list" v-if="hostWorkspaceOptions.length">
|
<div class="host-workspace-list" v-if="hostWorkspaceOptions.length">
|
||||||
<button
|
<button
|
||||||
@ -226,7 +224,7 @@
|
|||||||
v-if="item.workspace_id === hostWorkspaceDefaultId"
|
v-if="item.workspace_id === hostWorkspaceDefaultId"
|
||||||
class="host-workspace-badge default"
|
class="host-workspace-badge default"
|
||||||
>
|
>
|
||||||
{{ workspaceKind === 'project' ? '默认项目' : '默认工作区' }}
|
默认工作区
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
v-if="item.workspace_id === hostWorkspaceId"
|
v-if="item.workspace_id === hostWorkspaceId"
|
||||||
@ -234,22 +232,14 @@
|
|||||||
>
|
>
|
||||||
当前
|
当前
|
||||||
</span>
|
</span>
|
||||||
<span
|
|
||||||
v-if="Number(item.running_task_count || 0) > 0"
|
|
||||||
class="host-workspace-badge running"
|
|
||||||
>
|
|
||||||
运行中
|
|
||||||
</span>
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="workspaceKind !== 'project'" class="host-workspace-path">
|
<div class="host-workspace-path">
|
||||||
{{ item.path || (workspaceKind === 'project' ? '项目文件' : '(未配置路径)') }}
|
{{ item.path || '(未配置路径)' }}
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="file-tree-empty">
|
<div v-else class="file-tree-empty">暂无工作区,点击右侧 + 新建</div>
|
||||||
{{ workspaceKind === 'project' ? '暂无项目,点击齿轮管理' : '暂无工作区,点击齿轮管理' }}
|
|
||||||
</div>
|
|
||||||
<div class="host-workspace-hint" v-if="hostWorkspaceSwitching">
|
<div class="host-workspace-hint" v-if="hostWorkspaceSwitching">
|
||||||
正在切换,请稍候…
|
正在切换,请稍候…
|
||||||
</div>
|
</div>
|
||||||
@ -302,12 +292,10 @@ const props = defineProps<{
|
|||||||
label: string;
|
label: string;
|
||||||
path?: string;
|
path?: string;
|
||||||
is_current?: boolean;
|
is_current?: boolean;
|
||||||
running_task_count?: number;
|
|
||||||
}>;
|
}>;
|
||||||
hostWorkspaceId?: string;
|
hostWorkspaceId?: string;
|
||||||
hostWorkspaceDefaultId?: string;
|
hostWorkspaceDefaultId?: string;
|
||||||
hostWorkspaceSwitching?: boolean;
|
hostWorkspaceSwitching?: boolean;
|
||||||
workspaceKind?: 'workspace' | 'project';
|
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emits = defineEmits<{
|
const emits = defineEmits<{
|
||||||
@ -317,12 +305,10 @@ const emits = defineEmits<{
|
|||||||
(event: 'toggle-thinking-mode'): void;
|
(event: 'toggle-thinking-mode'): void;
|
||||||
(event: 'switch-host-workspace', workspaceId: string): void;
|
(event: 'switch-host-workspace', workspaceId: string): void;
|
||||||
(event: 'create-host-workspace'): void;
|
(event: 'create-host-workspace'): void;
|
||||||
(event: 'manage-host-workspace'): void;
|
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const panelMenuWrapper = ref<HTMLElement | null>(null);
|
const panelMenuWrapper = ref<HTMLElement | null>(null);
|
||||||
const statusLogoSvg = statusLogoSvgRaw.replace(/fill="#000000"/g, 'fill="currentColor"');
|
const statusLogoSvg = statusLogoSvgRaw.replace(/fill="#000000"/g, 'fill="currentColor"');
|
||||||
const workspaceKind = computed(() => props.workspaceKind || 'workspace');
|
|
||||||
const panelStyle = computed(() => {
|
const panelStyle = computed(() => {
|
||||||
const px = `${props.width}px`;
|
const px = `${props.width}px`;
|
||||||
const layoutWidth = props.collapsed ? '0px' : px;
|
const layoutWidth = props.collapsed ? '0px' : px;
|
||||||
@ -403,13 +389,6 @@ const handleCreateHostWorkspace = () => {
|
|||||||
emits('create-host-workspace');
|
emits('create-host-workspace');
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleManageHostWorkspace = () => {
|
|
||||||
if (props.hostWorkspaceSwitching) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
emits('manage-host-workspace');
|
|
||||||
};
|
|
||||||
|
|
||||||
const openSubAgent = (agent: any) => {
|
const openSubAgent = (agent: any) => {
|
||||||
subAgentStore.openSubAgent(agent);
|
subAgentStore.openSubAgent(agent);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -104,37 +104,13 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="conversation-list">
|
<div class="conversation-list">
|
||||||
<div v-if="runningTaskItems.length && !searchActive" class="running-task-section">
|
|
||||||
<div class="running-task-section-title">运行中的任务</div>
|
|
||||||
<button
|
|
||||||
v-for="task in runningTaskItems"
|
|
||||||
:key="task.task_id"
|
|
||||||
type="button"
|
|
||||||
class="running-task-item"
|
|
||||||
:class="{ active: task.conversation_id === currentConversationId }"
|
|
||||||
@click="$emit('select-running-task', task)"
|
|
||||||
>
|
|
||||||
<span class="running-task-workspace">{{ task.workspace_label || task.workspace_id }}</span>
|
|
||||||
<span class="running-task-main">
|
|
||||||
<span class="running-task-title">{{
|
|
||||||
task.conversation_title || task.message || '运行中的对话'
|
|
||||||
}}</span>
|
|
||||||
<span
|
|
||||||
v-if="isTaskActive(task)"
|
|
||||||
class="conversation-running-loader running-task-state"
|
|
||||||
aria-label="运行中"
|
|
||||||
></span>
|
|
||||||
<span v-else class="conversation-complete-check running-task-state" aria-label="已完成"></span>
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div
|
<div
|
||||||
v-if="loading && !displayConversations.length && !searchActive && !isDeletingConversation"
|
v-if="loading && !displayConversations.length && !searchActive"
|
||||||
class="loading-conversations"
|
class="loading-conversations"
|
||||||
>
|
>
|
||||||
正在加载...
|
正在加载...
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="!displayConversations.length && !loading && !isDeletingConversation" class="no-conversations">
|
<div v-else-if="!displayConversations.length && !loading" class="no-conversations">
|
||||||
{{ searchActive ? '未找到匹配对话' : '暂无对话记录' }}
|
{{ searchActive ? '未找到匹配对话' : '暂无对话记录' }}
|
||||||
</div>
|
</div>
|
||||||
<transition-group
|
<transition-group
|
||||||
@ -150,11 +126,9 @@
|
|||||||
class="conversation-item"
|
class="conversation-item"
|
||||||
:class="{
|
:class="{
|
||||||
active: conv.id === currentConversationId,
|
active: conv.id === currentConversationId,
|
||||||
running: isConversationActive(conv.id) || isConversationCompleted(conv.id),
|
|
||||||
'insert-from-left': conversationInsertAnimations[conv.id] === 'create',
|
'insert-from-left': conversationInsertAnimations[conv.id] === 'create',
|
||||||
'insert-from-under': conversationInsertAnimations[conv.id] === 'duplicate',
|
'insert-from-under': conversationInsertAnimations[conv.id] === 'duplicate',
|
||||||
'duplicate-source-mask': conversationInsertAnimations[conv.id] === 'duplicateSource',
|
'duplicate-source-mask': conversationInsertAnimations[conv.id] === 'duplicateSource'
|
||||||
deleting: pendingDeletingConversationIds.includes(conv.id)
|
|
||||||
}"
|
}"
|
||||||
@click="
|
@click="
|
||||||
openActionMenuId = null;
|
openActionMenuId = null;
|
||||||
@ -167,20 +141,7 @@
|
|||||||
:class="{ 'menu-open': openActionMenuId === conv.id }"
|
:class="{ 'menu-open': openActionMenuId === conv.id }"
|
||||||
@click.stop
|
@click.stop
|
||||||
>
|
>
|
||||||
<span
|
|
||||||
v-if="isConversationActive(conv.id)"
|
|
||||||
class="conversation-running-loader"
|
|
||||||
aria-label="运行中"
|
|
||||||
title="运行中"
|
|
||||||
></span>
|
|
||||||
<span
|
|
||||||
v-else-if="isConversationCompleted(conv.id)"
|
|
||||||
class="conversation-complete-check"
|
|
||||||
aria-label="已完成"
|
|
||||||
title="已完成"
|
|
||||||
></span>
|
|
||||||
<button
|
<button
|
||||||
v-else
|
|
||||||
type="button"
|
type="button"
|
||||||
class="conversation-more-btn"
|
class="conversation-more-btn"
|
||||||
title="更多操作"
|
title="更多操作"
|
||||||
@ -191,7 +152,6 @@
|
|||||||
<span aria-hidden="true"></span>
|
<span aria-hidden="true"></span>
|
||||||
</button>
|
</button>
|
||||||
<div
|
<div
|
||||||
v-if="!isConversationActive(conv.id) && !isConversationCompleted(conv.id)"
|
|
||||||
class="conversation-actions-menu"
|
class="conversation-actions-menu"
|
||||||
:class="{ open: openActionMenuId === conv.id }"
|
:class="{ open: openActionMenuId === conv.id }"
|
||||||
>
|
>
|
||||||
@ -292,16 +252,12 @@ const props = withDefaults(
|
|||||||
collapseButtonVariant?: 'toggle' | 'close';
|
collapseButtonVariant?: 'toggle' | 'close';
|
||||||
displayMode?: 'chat' | 'monitor';
|
displayMode?: 'chat' | 'monitor';
|
||||||
displayModeDisabled?: boolean;
|
displayModeDisabled?: boolean;
|
||||||
runningTasks?: any[];
|
|
||||||
currentWorkspaceId?: string;
|
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
showCollapseButton: true,
|
showCollapseButton: true,
|
||||||
collapseButtonVariant: 'toggle',
|
collapseButtonVariant: 'toggle',
|
||||||
displayMode: 'chat',
|
displayMode: 'chat',
|
||||||
displayModeDisabled: false,
|
displayModeDisabled: false
|
||||||
runningTasks: () => [],
|
|
||||||
currentWorkspaceId: ''
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -311,7 +267,6 @@ defineEmits<{
|
|||||||
(event: 'search', value: string): void;
|
(event: 'search', value: string): void;
|
||||||
(event: 'search-submit', value: string): void;
|
(event: 'search-submit', value: string): void;
|
||||||
(event: 'select', id: string): void;
|
(event: 'select', id: string): void;
|
||||||
(event: 'select-running-task', task: any): void;
|
|
||||||
(event: 'load-more'): void;
|
(event: 'load-more'): void;
|
||||||
(event: 'search-more'): void;
|
(event: 'search-more'): void;
|
||||||
(event: 'personal'): void;
|
(event: 'personal'): void;
|
||||||
@ -339,7 +294,6 @@ const {
|
|||||||
conversations,
|
conversations,
|
||||||
conversationInsertAnimations,
|
conversationInsertAnimations,
|
||||||
conversationListAnimationMode,
|
conversationListAnimationMode,
|
||||||
pendingDeletingConversationIds,
|
|
||||||
conversationsLoading,
|
conversationsLoading,
|
||||||
hasMoreConversations: hasMore,
|
hasMoreConversations: hasMore,
|
||||||
loadingMoreConversations: loadingMore,
|
loadingMoreConversations: loadingMore,
|
||||||
@ -377,43 +331,4 @@ const displayConversations = computed(() =>
|
|||||||
const loading = computed(() =>
|
const loading = computed(() =>
|
||||||
searchActive.value ? searchInProgress.value : conversationsLoading.value
|
searchActive.value ? searchInProgress.value : conversationsLoading.value
|
||||||
);
|
);
|
||||||
const isDeletingConversation = computed(() => conversationListAnimationMode.value === 'delete');
|
|
||||||
const runningTaskItems = computed(() =>
|
|
||||||
Array.isArray(props.runningTasks)
|
|
||||||
? props.runningTasks.filter(
|
|
||||||
(task) =>
|
|
||||||
task &&
|
|
||||||
task.task_id &&
|
|
||||||
task.conversation_id &&
|
|
||||||
task.conversation_id !== currentConversationId.value &&
|
|
||||||
String(task.workspace_id || '') !== String(props.currentWorkspaceId || '')
|
|
||||||
)
|
|
||||||
: []
|
|
||||||
);
|
|
||||||
const activeStatuses = new Set(['pending', 'running', 'cancel_requested']);
|
|
||||||
const terminalStatuses = new Set(['succeeded', 'failed', 'canceled']);
|
|
||||||
const isTaskActive = (task: any) => activeStatuses.has(String(task?.status || ''));
|
|
||||||
const isTaskCompleted = (task: any) => terminalStatuses.has(String(task?.status || ''));
|
|
||||||
const activeConversationIds = computed(
|
|
||||||
() =>
|
|
||||||
new Set(
|
|
||||||
(Array.isArray(props.runningTasks) ? props.runningTasks : [])
|
|
||||||
.filter((task) => isTaskActive(task))
|
|
||||||
.map((task) => String(task?.conversation_id || ''))
|
|
||||||
.filter(Boolean)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
const completedConversationIds = computed(
|
|
||||||
() =>
|
|
||||||
new Set(
|
|
||||||
(Array.isArray(props.runningTasks) ? props.runningTasks : [])
|
|
||||||
.filter((task) => isTaskCompleted(task))
|
|
||||||
.map((task) => String(task?.conversation_id || ''))
|
|
||||||
.filter(Boolean)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
const isConversationActive = (conversationId: string) =>
|
|
||||||
activeConversationIds.value.has(String(conversationId || ''));
|
|
||||||
const isConversationCompleted = (conversationId: string) =>
|
|
||||||
completedConversationIds.value.has(String(conversationId || ''));
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -13,7 +13,6 @@ interface ConversationState {
|
|||||||
searchResults: ConversationSummary[];
|
searchResults: ConversationSummary[];
|
||||||
conversationInsertAnimations: Record<string, 'create' | 'duplicate' | 'duplicateSource'>;
|
conversationInsertAnimations: Record<string, 'create' | 'duplicate' | 'duplicateSource'>;
|
||||||
conversationListAnimationMode: 'idle' | 'create' | 'duplicate' | 'delete';
|
conversationListAnimationMode: 'idle' | 'create' | 'duplicate' | 'delete';
|
||||||
pendingDeletingConversationIds: string[];
|
|
||||||
conversationsLoading: boolean;
|
conversationsLoading: boolean;
|
||||||
hasMoreConversations: boolean;
|
hasMoreConversations: boolean;
|
||||||
loadingMoreConversations: boolean;
|
loadingMoreConversations: boolean;
|
||||||
@ -28,8 +27,6 @@ interface ConversationState {
|
|||||||
searchTotal: number;
|
searchTotal: number;
|
||||||
conversationsOffset: number;
|
conversationsOffset: number;
|
||||||
conversationsLimit: number;
|
conversationsLimit: number;
|
||||||
runningWorkspaceTasks: any[];
|
|
||||||
acknowledgedCompletedTaskIds: string[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useConversationStore = defineStore('conversation', {
|
export const useConversationStore = defineStore('conversation', {
|
||||||
@ -38,7 +35,6 @@ export const useConversationStore = defineStore('conversation', {
|
|||||||
searchResults: [],
|
searchResults: [],
|
||||||
conversationInsertAnimations: {},
|
conversationInsertAnimations: {},
|
||||||
conversationListAnimationMode: 'idle',
|
conversationListAnimationMode: 'idle',
|
||||||
pendingDeletingConversationIds: [],
|
|
||||||
conversationsLoading: false,
|
conversationsLoading: false,
|
||||||
hasMoreConversations: false,
|
hasMoreConversations: false,
|
||||||
loadingMoreConversations: false,
|
loadingMoreConversations: false,
|
||||||
@ -52,9 +48,7 @@ export const useConversationStore = defineStore('conversation', {
|
|||||||
searchOffset: 0,
|
searchOffset: 0,
|
||||||
searchTotal: 0,
|
searchTotal: 0,
|
||||||
conversationsOffset: 0,
|
conversationsOffset: 0,
|
||||||
conversationsLimit: 20,
|
conversationsLimit: 20
|
||||||
runningWorkspaceTasks: [],
|
|
||||||
acknowledgedCompletedTaskIds: []
|
|
||||||
}),
|
}),
|
||||||
actions: {
|
actions: {
|
||||||
resetConversations() {
|
resetConversations() {
|
||||||
@ -62,7 +56,6 @@ export const useConversationStore = defineStore('conversation', {
|
|||||||
this.searchResults = [];
|
this.searchResults = [];
|
||||||
this.conversationInsertAnimations = {};
|
this.conversationInsertAnimations = {};
|
||||||
this.conversationListAnimationMode = 'idle';
|
this.conversationListAnimationMode = 'idle';
|
||||||
this.pendingDeletingConversationIds = [];
|
|
||||||
this.searchActive = false;
|
this.searchActive = false;
|
||||||
this.searchInProgress = false;
|
this.searchInProgress = false;
|
||||||
this.searchMoreAvailable = false;
|
this.searchMoreAvailable = false;
|
||||||
@ -71,8 +64,6 @@ export const useConversationStore = defineStore('conversation', {
|
|||||||
this.hasMoreConversations = false;
|
this.hasMoreConversations = false;
|
||||||
this.loadingMoreConversations = false;
|
this.loadingMoreConversations = false;
|
||||||
this.conversationsOffset = 0;
|
this.conversationsOffset = 0;
|
||||||
this.runningWorkspaceTasks = [];
|
|
||||||
this.acknowledgedCompletedTaskIds = [];
|
|
||||||
},
|
},
|
||||||
cancelSearchTimer() {
|
cancelSearchTimer() {
|
||||||
if (this.searchTimer) {
|
if (this.searchTimer) {
|
||||||
|
|||||||
@ -170,17 +170,6 @@ export const useTaskStore = defineStore('task', {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const data = result.data;
|
const data = result.data;
|
||||||
// 如果用户已切换/新建对话并清除了当前轮询,忽略这次已在路上的旧响应。
|
|
||||||
// 否则旧任务事件可能被写入当前新对话视图。
|
|
||||||
if (this.currentTaskId !== taskId) {
|
|
||||||
taskPollDiag('task-poll-stale-response-ignored', {
|
|
||||||
requestId,
|
|
||||||
responseTaskId: data?.task_id || taskId,
|
|
||||||
activeTaskId: this.currentTaskId,
|
|
||||||
from: fromOffset
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const eventsCount = Array.isArray(data?.events) ? data.events.length : 0;
|
const eventsCount = Array.isArray(data?.events) ? data.events.length : 0;
|
||||||
jsonDebug('taskStore.poll:response', {
|
jsonDebug('taskStore.poll:response', {
|
||||||
taskId,
|
taskId,
|
||||||
@ -236,14 +225,6 @@ export const useTaskStore = defineStore('task', {
|
|||||||
debugLog(`[Task] 收到 ${data.events.length} 个新事件`);
|
debugLog(`[Task] 收到 ${data.events.length} 个新事件`);
|
||||||
|
|
||||||
for (const event of data.events) {
|
for (const event of data.events) {
|
||||||
if (this.currentTaskId !== taskId) {
|
|
||||||
taskPollDiag('task-poll-stale-event-loop-abort', {
|
|
||||||
requestId,
|
|
||||||
responseTaskId: data?.task_id || taskId,
|
|
||||||
activeTaskId: this.currentTaskId
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (
|
if (
|
||||||
event?.type === 'task_complete' ||
|
event?.type === 'task_complete' ||
|
||||||
event?.type === 'task_stopped' ||
|
event?.type === 'task_stopped' ||
|
||||||
@ -547,11 +528,10 @@ export const useTaskStore = defineStore('task', {
|
|||||||
throw new Error(result.error || '获取任务列表失败');
|
throw new Error(result.error || '获取任务列表失败');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 查找当前对话的活动任务
|
// 查找运行中的任务
|
||||||
const activeStatuses = new Set(['pending', 'running', 'cancel_requested']);
|
|
||||||
const runningTask = result.data.find(
|
const runningTask = result.data.find(
|
||||||
(task: any) =>
|
(task: any) =>
|
||||||
activeStatuses.has(String(task.status || '')) &&
|
task.status === 'running' &&
|
||||||
(!conversationId || task.conversation_id === conversationId)
|
(!conversationId || task.conversation_id === conversationId)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -683,12 +683,6 @@
|
|||||||
border-color: rgba(47, 111, 78, 0.35);
|
border-color: rgba(47, 111, 78, 0.35);
|
||||||
background: rgba(118, 176, 134, 0.18);
|
background: rgba(118, 176, 134, 0.18);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.running {
|
|
||||||
color: var(--claude-deep-strong);
|
|
||||||
border-color: color-mix(in srgb, var(--claude-accent) 34%, transparent);
|
|
||||||
background: color-mix(in srgb, var(--claude-highlight) 78%, transparent);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-theme='dark'] .host-workspace-header-box {
|
[data-theme='dark'] .host-workspace-header-box {
|
||||||
@ -720,12 +714,6 @@
|
|||||||
border-color: rgba(142, 219, 180, 0.34);
|
border-color: rgba(142, 219, 180, 0.34);
|
||||||
background: rgba(16, 185, 129, 0.2);
|
background: rgba(16, 185, 129, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.running {
|
|
||||||
color: #f7c56f;
|
|
||||||
border-color: rgba(247, 197, 111, 0.35);
|
|
||||||
background: rgba(247, 197, 111, 0.16);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.host-workspace-path {
|
.host-workspace-path {
|
||||||
|
|||||||
@ -222,81 +222,6 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.conversation-sidebar .running-task-section {
|
|
||||||
margin: 0 8px 12px;
|
|
||||||
padding: 6px 0 8px;
|
|
||||||
border-bottom: 1px solid color-mix(in srgb, var(--claude-border) 72%, transparent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.conversation-sidebar .running-task-section-title {
|
|
||||||
margin: 0 8px 4px;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 700;
|
|
||||||
color: var(--claude-text-secondary);
|
|
||||||
letter-spacing: 0.01em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.conversation-sidebar .running-task-item {
|
|
||||||
position: relative;
|
|
||||||
display: grid;
|
|
||||||
width: 100%;
|
|
||||||
grid-template-columns: minmax(0, 1fr);
|
|
||||||
gap: 3px;
|
|
||||||
margin: 1px 0;
|
|
||||||
padding: 8px 10px 8px 12px;
|
|
||||||
border: 0;
|
|
||||||
border-radius: 12px;
|
|
||||||
background: transparent;
|
|
||||||
color: var(--claude-text);
|
|
||||||
text-align: left;
|
|
||||||
cursor: pointer;
|
|
||||||
transition:
|
|
||||||
background 140ms ease,
|
|
||||||
color 140ms ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.conversation-sidebar .running-task-item:hover,
|
|
||||||
.conversation-sidebar .running-task-item.active {
|
|
||||||
background: var(--theme-tab-active, rgba(0, 0, 0, 0.055));
|
|
||||||
}
|
|
||||||
|
|
||||||
.conversation-sidebar .running-task-workspace {
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
font-size: 11px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--claude-text-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.conversation-sidebar .running-task-title {
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 1.25;
|
|
||||||
letter-spacing: -0.01em;
|
|
||||||
color: var(--claude-text);
|
|
||||||
}
|
|
||||||
|
|
||||||
.conversation-sidebar .running-task-main {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: minmax(0, 1fr) 18px;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.conversation-sidebar .running-task-state {
|
|
||||||
justify-self: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme='dark'] .conversation-sidebar .running-task-item:hover,
|
|
||||||
[data-theme='dark'] .conversation-sidebar .running-task-item.active,
|
|
||||||
body[data-theme='dark'] .conversation-sidebar .running-task-item:hover,
|
|
||||||
body[data-theme='dark'] .conversation-sidebar .running-task-item.active {
|
|
||||||
background: #222222;
|
|
||||||
}
|
|
||||||
|
|
||||||
.conversation-sidebar .conversation-item {
|
.conversation-sidebar .conversation-item {
|
||||||
position: relative;
|
position: relative;
|
||||||
grid-template-columns: minmax(0, 1fr) 40px;
|
grid-template-columns: minmax(0, 1fr) 40px;
|
||||||
@ -394,15 +319,6 @@ body[data-theme='dark']
|
|||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.conversation-sidebar .conversation-item.deleting {
|
|
||||||
transform: translateX(-112%);
|
|
||||||
opacity: 0;
|
|
||||||
pointer-events: none;
|
|
||||||
transition:
|
|
||||||
transform 420ms cubic-bezier(0.32, 0, 0.67, 0),
|
|
||||||
opacity 260ms ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.conversation-sidebar .conversation-item:hover,
|
.conversation-sidebar .conversation-item:hover,
|
||||||
.conversation-sidebar .conversation-item:focus-within,
|
.conversation-sidebar .conversation-item:focus-within,
|
||||||
.conversation-sidebar .conversation-item.active:hover,
|
.conversation-sidebar .conversation-item.active:hover,
|
||||||
@ -449,43 +365,13 @@ body[data-theme='dark'] .conversation-sidebar .conversation-item.active:focus-wi
|
|||||||
|
|
||||||
.conversation-sidebar .conversation-action-wrap {
|
.conversation-sidebar .conversation-action-wrap {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 40px;
|
width: 34px;
|
||||||
height: 34px;
|
height: 34px;
|
||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
justify-self: start;
|
justify-self: start;
|
||||||
}
|
}
|
||||||
|
|
||||||
.conversation-sidebar .conversation-running-loader {
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
border: 2px solid var(--claude-text-secondary);
|
|
||||||
border-bottom-color: transparent;
|
|
||||||
border-radius: 50%;
|
|
||||||
display: inline-block;
|
|
||||||
box-sizing: border-box;
|
|
||||||
animation: conversation-running-rotation 1s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.conversation-sidebar .conversation-complete-check {
|
|
||||||
width: 18px;
|
|
||||||
height: 18px;
|
|
||||||
display: inline-block;
|
|
||||||
background: var(--claude-text-secondary);
|
|
||||||
mask: url('/static/icons/check.svg') center / contain no-repeat;
|
|
||||||
-webkit-mask: url('/static/icons/check.svg') center / contain no-repeat;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes conversation-running-rotation {
|
|
||||||
0% {
|
|
||||||
transform: rotate(0deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
100% {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.conversation-sidebar .conversation-more-btn {
|
.conversation-sidebar .conversation-more-btn {
|
||||||
width: 30px;
|
width: 30px;
|
||||||
height: 30px;
|
height: 30px;
|
||||||
|
|||||||
@ -2290,7 +2290,6 @@ class ContextManager:
|
|||||||
if not template:
|
if not template:
|
||||||
template = (
|
template = (
|
||||||
"## 工作区信息\n"
|
"## 工作区信息\n"
|
||||||
"- **项目名称**:{project_name}\n"
|
|
||||||
"- **运行环境**:{runtime_environment}\n"
|
"- **运行环境**:{runtime_environment}\n"
|
||||||
"- **资源限制**:{resource_limit}\n"
|
"- **资源限制**:{resource_limit}\n"
|
||||||
"- **当前时间**:{current_time}\n"
|
"- **当前时间**:{current_time}\n"
|
||||||
@ -2312,14 +2311,8 @@ class ContextManager:
|
|||||||
|
|
||||||
now = datetime.now()
|
now = datetime.now()
|
||||||
current_time_text = f"{now.year}年{now.month}月{now.day}日 {now.hour}点(24小时制)"
|
current_time_text = f"{now.year}年{now.month}月{now.day}日 {now.hour}点(24小时制)"
|
||||||
project_name = (
|
|
||||||
str(getattr(self, "workspace_label", "") or "").strip()
|
|
||||||
or str(getattr(self, "project_label", "") or "").strip()
|
|
||||||
or self.project_path.name
|
|
||||||
)
|
|
||||||
|
|
||||||
content = template.format(
|
content = template.format(
|
||||||
project_name=project_name,
|
|
||||||
runtime_environment=runtime_environment,
|
runtime_environment=runtime_environment,
|
||||||
resource_limit=resource_limit,
|
resource_limit=resource_limit,
|
||||||
container_path=self.container_mount_path or "/workspace",
|
container_path=self.container_mount_path or "/workspace",
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user