From e7d5be632fc44e7318fc67b698b22b07c08447e3 Mon Sep 17 00:00:00 2001 From: Weilong Liao <37870767+Soulter@users.noreply.github.com> Date: Fri, 3 Jul 2026 21:54:20 +0800 Subject: [PATCH] feat: add ChatUI project workspaces (#9066) * feat: add ChatUI project workspaces * feat: refine chatui layout * feat: enhance chat input and project view styles for attachment handling * feat: adjust chat sidebar brand logo positioning * feat: enhance project dialog with error handling and loading state --- astrbot/core/astr_main_agent.py | 96 +- astrbot/core/db/__init__.py | 4 + astrbot/core/db/po.py | 4 + astrbot/core/db/sqlite.py | 27 + astrbot/core/tools/computer_tools/fs.py | 134 ++- astrbot/core/tools/computer_tools/python.py | 9 +- astrbot/core/tools/computer_tools/shell.py | 10 +- astrbot/core/tools/computer_tools/util.py | 46 +- astrbot/core/tools/message_tools.py | 26 +- astrbot/core/workspace.py | 196 ++++ astrbot/dashboard/schemas.py | 2 + .../services/chatui_project_service.py | 92 +- dashboard/package.json | 1 + dashboard/pnpm-lock.yaml | 12 + dashboard/public/favicon.svg | 15 +- .../src/api/generated/openapi-v1/types.gen.ts | 4 + .../mdi-subset/materialdesignicons-subset.css | 8 +- .../materialdesignicons-webfont-subset.woff | Bin 19968 -> 20068 bytes .../materialdesignicons-webfont-subset.woff2 | Bin 16008 -> 16072 bytes dashboard/src/components/chat/Chat.vue | 905 +++++++++++++----- dashboard/src/components/chat/ChatInput.vue | 614 +++++++++--- dashboard/src/components/chat/ChatUILogo.vue | 23 + .../src/components/chat/ProjectDialog.vue | 80 +- dashboard/src/components/chat/ProjectList.vue | 462 ++++++--- dashboard/src/components/chat/ProjectView.vue | 303 ++++-- .../src/components/chat/ProviderModelMenu.vue | 498 +++++++--- .../src/components/chat/ReasoningSidebar.vue | 10 +- .../src/components/chat/StandaloneChat.vue | 2 + dashboard/src/components/chat/ThreadPanel.vue | 12 +- .../chat/message_list_comps/RefsSidebar.vue | 19 +- .../src/components/shared/StyledMenu.vue | 7 +- dashboard/src/composables/useProjects.ts | 41 +- .../src/i18n/locales/en-US/features/chat.json | 14 +- .../src/i18n/locales/ru-RU/features/chat.json | 14 +- .../src/i18n/locales/zh-CN/features/chat.json | 14 +- dashboard/src/layouts/full/FullLayout.vue | 7 +- .../full/vertical-header/VerticalHeader.vue | 474 ++++++--- dashboard/src/scss/_override.scss | 17 + dashboard/src/stores/chatHeader.ts | 19 + dashboard/src/stores/customizer.ts | 4 + openspec/openapi-v1.yaml | 5 + tests/unit/test_chatui_project_service.py | 140 +++ tests/unit/test_python_tools.py | 8 +- 43 files changed, 3413 insertions(+), 965 deletions(-) create mode 100644 astrbot/core/workspace.py create mode 100644 dashboard/src/components/chat/ChatUILogo.vue create mode 100644 dashboard/src/stores/chatHeader.ts create mode 100644 tests/unit/test_chatui_project_service.py diff --git a/astrbot/core/astr_main_agent.py b/astrbot/core/astr_main_agent.py index 16ebac7a8..1eae0f17e 100644 --- a/astrbot/core/astr_main_agent.py +++ b/astrbot/core/astr_main_agent.py @@ -29,6 +29,7 @@ from astrbot.core.astr_main_agent_resources import ( TOOL_CALL_PROMPT_SKILLS_LIKE_MODE, ) from astrbot.core.conversation_mgr import Conversation +from astrbot.core.db import BaseDatabase from astrbot.core.message.components import File, Image, Record, Reply, Video from astrbot.core.persona_error_reply import ( extract_persona_custom_error_message_from_persona, @@ -73,7 +74,6 @@ from astrbot.core.tools.computer_tools import ( RollbackSkillReleaseTool, RunBrowserSkillTool, SyncSkillReleaseTool, - normalize_umo_for_workspace, ) from astrbot.core.tools.cron_tools import FutureTaskTool from astrbot.core.tools.knowledge_base_tools import ( @@ -115,6 +115,10 @@ from astrbot.core.utils.quoted_message_parser import ( extract_quoted_message_text, ) from astrbot.core.utils.string_utils import normalize_and_dedupe_strings +from astrbot.core.workspace import ( + normalize_umo_for_workspace, + resolve_workspace_root_for_umo, +) LLM_ERROR_MESSAGE_EXTRA_KEY = "_llm_error_message" WEEKDAY_NAMES = ( @@ -357,41 +361,63 @@ def _apply_prompt_prefix(req: ProviderRequest, cfg: dict) -> None: req.prompt = f"{prefix}{req.prompt}" -def _get_workspace_path_for_umo(umo: str) -> Path: - normalized_umo = normalize_umo_for_workspace(umo) - return Path(get_astrbot_workspaces_path()) / normalized_umo +async def _get_workspace_path_for_umo(umo: str, plugin_context: Context) -> Path: + """Resolve the workspace path for the current request. + + Args: + umo: Unified message origin. + plugin_context: Star context containing the database instance. + + Returns: + Workspace path used as cwd. + """ + fallback_root = ( + Path(get_astrbot_workspaces_path()) / normalize_umo_for_workspace(umo) + ).resolve(strict=False) + db = getattr(plugin_context, "_db", None) + if not isinstance(db, BaseDatabase): + return fallback_root + try: + return await resolve_workspace_root_for_umo(umo, db) + except Exception: + return fallback_root -def _apply_workspace_extra_prompt( +async def _apply_workspace_extra_prompt( event: AstrMessageEvent, req: ProviderRequest, + plugin_context: Context, ) -> None: - extra_prompt_path = _get_workspace_path_for_umo(event.unified_msg_origin) / ( - "EXTRA_PROMPT.md" + workspace_root = await _get_workspace_path_for_umo( + event.unified_msg_origin, + plugin_context, ) - if not extra_prompt_path.is_file(): - return - - try: - extra_prompt = extra_prompt_path.read_text(encoding="utf-8").strip() - except Exception as exc: # noqa: BLE001 - logger.warning( - "Failed to read workspace extra prompt for umo=%s from %s: %s", - event.unified_msg_origin, - extra_prompt_path, - exc, - ) - return - - if not extra_prompt: + extra_prompts: list[str] = [] + extra_prompt_path = workspace_root / "EXTRA_PROMPT.md" + if extra_prompt_path.is_file(): + try: + extra_prompt = extra_prompt_path.read_text(encoding="utf-8").strip() + except Exception as exc: # noqa: BLE001 + logger.warning( + "Failed to read workspace extra prompt for umo=%s from %s: %s", + event.unified_msg_origin, + extra_prompt_path, + exc, + ) + else: + if extra_prompt: + extra_prompts.append(f"From `{extra_prompt_path}`:\n{extra_prompt}") + + if not extra_prompts: return + extra_prompt_text = "\n\n".join(extra_prompts) req.system_prompt = ( f"{req.system_prompt or ''}\n" "[Workspace Extra Prompt]\n" "The following instructions are loaded from the current workspace " "`EXTRA_PROMPT.md` file.\n" - f"{extra_prompt}\n" + f"{extra_prompt_text}\n" ) @@ -498,13 +524,13 @@ async def _ensure_persona_and_skills( skill_manager = SkillManager() skills = skill_manager.list_skills(active_only=True, runtime=runtime) skills = _filter_skills_for_current_config(skills, cfg) - workspace_skills = ( - skill_manager.list_workspace_skills( - _get_workspace_path_for_umo(event.unified_msg_origin) + workspace_skills: list[SkillInfo] = [] + if runtime == "local": + workspace_root = await _get_workspace_path_for_umo( + event.unified_msg_origin, + plugin_context, ) - if runtime == "local" - else [] - ) + workspace_skills.extend(skill_manager.list_workspace_skills(workspace_root)) if skills or workspace_skills: if persona and persona.get("skills") is not None: @@ -989,7 +1015,7 @@ async def _decorate_llm_request( if tz is None: tz = plugin_context.get_config().get("timezone") _append_system_reminders(event, req, cfg, tz) - _apply_workspace_extra_prompt(event, req) + await _apply_workspace_extra_prompt(event, req, plugin_context) def _plugin_tool_fix(event: AstrMessageEvent, req: ProviderRequest) -> None: @@ -1590,10 +1616,14 @@ async def build_main_agent( ) if config.computer_use_runtime == "local": + workspace_root = await _get_workspace_path_for_umo( + event.unified_msg_origin, + plugin_context, + ) + workspace_prompt = f"\nCurrent workspace you can use: `{workspace_root}`\n" tool_prompt += ( - f"\nCurrent workspace you can use: " - f"`{_get_workspace_path_for_umo(event.unified_msg_origin)}`\n" - "Unless the user explicitly specifies a different directory, " + workspace_prompt + + "Unless the user explicitly specifies a different directory, " "perform all file-related operations in this workspace.\n" ) diff --git a/astrbot/core/db/__init__.py b/astrbot/core/db/__init__.py index cf37c4866..8e319bc52 100644 --- a/astrbot/core/db/__init__.py +++ b/astrbot/core/db/__init__.py @@ -851,6 +851,8 @@ class BaseDatabase(abc.ABC): title: str, emoji: str | None = "📁", description: str | None = None, + workspace_type: str = "session", + workspace_path: str | None = None, ) -> ChatUIProject: """Create a new ChatUI project.""" ... @@ -877,6 +879,8 @@ class BaseDatabase(abc.ABC): title: str | None = None, emoji: str | None = None, description: str | None = None, + workspace_type: str | None = None, + workspace_path: str | None = None, ) -> None: """Update a ChatUI project.""" ... diff --git a/astrbot/core/db/po.py b/astrbot/core/db/po.py index 9a297b34d..5bfbc7d9e 100644 --- a/astrbot/core/db/po.py +++ b/astrbot/core/db/po.py @@ -447,6 +447,10 @@ class ChatUIProject(TimestampMixin, SQLModel, table=True): """Title of the project""" description: str | None = Field(default=None, max_length=1000) """Description of the project""" + workspace_type: str = Field(default="session", nullable=False, max_length=32) + """Workspace mode: session, project, or custom""" + workspace_path: str | None = Field(default=None, max_length=1024) + """Custom workspace path""" __table_args__ = ( UniqueConstraint( diff --git a/astrbot/core/db/sqlite.py b/astrbot/core/db/sqlite.py index b7706cc51..f59f23419 100644 --- a/astrbot/core/db/sqlite.py +++ b/astrbot/core/db/sqlite.py @@ -64,6 +64,7 @@ class SQLiteDatabase(BaseDatabase): await self._ensure_persona_skills_column(conn) await self._ensure_persona_custom_error_message_column(conn) await self._ensure_platform_message_history_checkpoint_column(conn) + await self._ensure_chatui_project_workspace_columns(conn) await conn.commit() async def _ensure_persona_folder_columns(self, conn) -> None: @@ -128,6 +129,23 @@ class SQLiteDatabase(BaseDatabase): ) ) + async def _ensure_chatui_project_workspace_columns(self, conn) -> None: + """Ensure chatui_projects has workspace configuration columns.""" + result = await conn.execute(text("PRAGMA table_info(chatui_projects)")) + columns = {row[1] for row in result.fetchall()} + + if "workspace_type" not in columns: + await conn.execute( + text( + "ALTER TABLE chatui_projects " + "ADD COLUMN workspace_type VARCHAR(32) NOT NULL DEFAULT 'session'" + ) + ) + if "workspace_path" not in columns: + await conn.execute( + text("ALTER TABLE chatui_projects ADD COLUMN workspace_path VARCHAR") + ) + # ==== # Platform Statistics # ==== @@ -1877,6 +1895,8 @@ class SQLiteDatabase(BaseDatabase): title: str, emoji: str | None = "📁", description: str | None = None, + workspace_type: str = "session", + workspace_path: str | None = None, ) -> ChatUIProject: """Create a new ChatUI project.""" async with self.get_db() as session: @@ -1887,6 +1907,8 @@ class SQLiteDatabase(BaseDatabase): title=title, emoji=emoji, description=description, + workspace_type=workspace_type, + workspace_path=workspace_path, ) session.add(project) await session.flush() @@ -1929,6 +1951,8 @@ class SQLiteDatabase(BaseDatabase): title: str | None = None, emoji: str | None = None, description: str | None = None, + workspace_type: str | None = None, + workspace_path: str | None = None, ) -> None: """Update a ChatUI project.""" async with self.get_db() as session: @@ -1941,6 +1965,9 @@ class SQLiteDatabase(BaseDatabase): values["emoji"] = emoji if description is not None: values["description"] = description + if workspace_type is not None: + values["workspace_type"] = workspace_type + values["workspace_path"] = workspace_path await session.execute( update(ChatUIProject) diff --git a/astrbot/core/tools/computer_tools/fs.py b/astrbot/core/tools/computer_tools/fs.py index 0b6003937..74bc2ad88 100644 --- a/astrbot/core/tools/computer_tools/fs.py +++ b/astrbot/core/tools/computer_tools/fs.py @@ -14,7 +14,7 @@ Behavior when `provider_settings.computer_use_require_admin=True`: implement them and the main agent does not expose them in local mode. - Member + local: read/grep are restricted to `data/skills`, plugin-provided `data/plugins/*/skills`, - `data/workspaces/{normalized_umo}`, and `/tmp/.astrbot`; write/edit are + the current session or project workspace, and `/tmp/.astrbot`; write/edit are restricted to the same local roots except plugin-provided Skills, which are read-only. Upload/download are denied by `check_admin_permission` if invoked. - Admin + sandbox: read/write/edit/grep are not path-restricted by this @@ -28,8 +28,7 @@ When `computer_use_require_admin=False`, member behavior in this module matches admin behavior. Local path resolution rule: -- In local runtime, relative paths are resolved under - `data/workspaces/{normalized_umo}`. +- In local runtime, relative paths are resolved under the primary workspace. - In sandbox runtime, relative paths are passed through unchanged. """ @@ -60,6 +59,7 @@ from .util import ( check_admin_permission, is_local_runtime, normalize_umo_for_workspace, + workspace_root_for_context, ) _COMPUTER_RUNTIME_TOOL_CONFIG = { @@ -77,9 +77,13 @@ def _remote_basename(path: str) -> str: return path.replace("\\", "/").rstrip("/").split("/")[-1] -def _restricted_env_path_labels(umo: str, *, include_plugin_skills: bool) -> list[str]: +def _restricted_env_path_labels( + umo: str, + *, + include_plugin_skills: bool, + current_workspace_root: Path | None = None, +) -> list[str]: """Labels for the allowed directories in a local(not sandbox) and restricted(not admin) environment""" - normalized_umo = normalize_umo_for_workspace(umo) labels = [ "data/skills", ] @@ -87,7 +91,7 @@ def _restricted_env_path_labels(umo: str, *, include_plugin_skills: bool) -> lis labels.append("data/plugins/*/skills") labels.extend( [ - f"data/workspaces/{normalized_umo}", + str(current_workspace_root or _workspace_root(umo)), get_astrbot_system_tmp_path(), get_astrbot_temp_path(), ] @@ -117,22 +121,28 @@ def _plugin_skill_roots() -> tuple[Path, ...]: ) -def _read_allowed_roots(umo: str) -> tuple[Path, ...]: +def _read_allowed_roots( + umo: str, + current_workspace_root: Path | None = None, +) -> tuple[Path, ...]: """Non-admin users can only read files within these directories (and their subdirectories)""" return ( Path(get_astrbot_skills_path()).resolve(strict=False), *_plugin_skill_roots(), - _workspace_root(umo), + current_workspace_root or _workspace_root(umo), Path(get_astrbot_system_tmp_path()).resolve(strict=False), Path(get_astrbot_temp_path()).resolve(strict=False), ) -def _write_allowed_roots(umo: str) -> tuple[Path, ...]: +def _write_allowed_roots( + umo: str, + current_workspace_root: Path | None = None, +) -> tuple[Path, ...]: """Non-admin users cannot modify plugin-provided Skills.""" return ( Path(get_astrbot_skills_path()).resolve(strict=False), - _workspace_root(umo), + current_workspace_root or _workspace_root(umo), Path(get_astrbot_system_tmp_path()).resolve(strict=False), Path(get_astrbot_temp_path()).resolve(strict=False), ) @@ -149,7 +159,13 @@ def _is_restricted_env(context: ContextWrapper[AstrAgentContext]) -> bool: return require_admin and context.context.event.role != "admin" -def _resolve_tool_path(path: str, *, local_env: bool, umo: str) -> str: +def _resolve_tool_path( + path: str, + *, + local_env: bool, + umo: str, + current_workspace_root: Path | None = None, +) -> str: normalized_path = path.strip() if not normalized_path: return normalized_path @@ -157,16 +173,28 @@ def _resolve_tool_path(path: str, *, local_env: bool, umo: str) -> str: if candidate.is_absolute(): return str(candidate.resolve(strict=False)) if local_env: - return str((_workspace_root(umo) / candidate).resolve(strict=False)) + return str( + ((current_workspace_root or _workspace_root(umo)) / candidate).resolve( + strict=False + ) + ) return normalized_path -def _resolve_user_path(path: str, *, local_env: bool, umo: str) -> Path: +def _resolve_user_path( + path: str, + *, + local_env: bool, + umo: str, + current_workspace_root: Path | None = None, +) -> Path: candidate = Path(path).expanduser() if candidate.is_absolute(): return candidate.resolve(strict=False) if local_env: - return (_workspace_root(umo) / candidate).resolve(strict=False) + return ((current_workspace_root or _workspace_root(umo)) / candidate).resolve( + strict=False + ) return (Path.cwd() / candidate).resolve(strict=False) @@ -175,8 +203,14 @@ def _is_path_within_allowed_roots( *, umo: str, allowed_roots: tuple[Path, ...], + current_workspace_root: Path | None = None, ) -> bool: - resolved = _resolve_user_path(path, local_env=True, umo=umo) + resolved = _resolve_user_path( + path, + local_env=True, + umo=umo, + current_workspace_root=current_workspace_root, + ) return any( resolved == allowed_root or resolved.is_relative_to(allowed_root) for allowed_root in allowed_roots @@ -209,19 +243,34 @@ def _normalize_rw_path( local_env: bool, umo: str, write: bool = False, + current_workspace_root: Path | None = None, ) -> str: - normalized_path = _resolve_tool_path(path, local_env=local_env, umo=umo) + normalized_path = _resolve_tool_path( + path, + local_env=local_env, + umo=umo, + current_workspace_root=current_workspace_root, + ) if not normalized_path: raise ValueError("`path` must be a non-empty string.") if restricted: - allowed_roots = _write_allowed_roots(umo) if write else _read_allowed_roots(umo) + allowed_roots = ( + _write_allowed_roots(umo, current_workspace_root) + if write + else _read_allowed_roots(umo, current_workspace_root) + ) if restricted and not _is_path_within_allowed_roots( normalized_path, umo=umo, allowed_roots=allowed_roots, + current_workspace_root=current_workspace_root, ): allowed = ", ".join( - _restricted_env_path_labels(umo, include_plugin_skills=not write) + _restricted_env_path_labels( + umo, + include_plugin_skills=not write, + current_workspace_root=current_workspace_root, + ) ) access = "Write" if write else "Read" raise PermissionError( @@ -291,6 +340,9 @@ class FileReadTool(FunctionTool): ) -> ToolExecResult: local_env = is_local_runtime(context) restricted = _is_restricted_env(context) + current_workspace_root = ( + await workspace_root_for_context(context) if local_env else None + ) try: normalized_path = ( _normalize_rw_path( @@ -298,6 +350,7 @@ class FileReadTool(FunctionTool): restricted=restricted, local_env=local_env, umo=context.context.event.unified_msg_origin, + current_workspace_root=current_workspace_root, ) if local_env else path.strip() @@ -321,7 +374,10 @@ class FileReadTool(FunctionTool): offset=offset, limit=limit, workspace_dir=( - str(_workspace_root(context.context.event.unified_msg_origin)) + str( + current_workspace_root + or _workspace_root(context.context.event.unified_msg_origin) + ) if local_env else None ), @@ -363,6 +419,9 @@ class FileWriteTool(FunctionTool): ) -> ToolExecResult: local_env = is_local_runtime(context) restricted = _is_restricted_env(context) + current_workspace_root = ( + await workspace_root_for_context(context) if local_env else None + ) try: normalized_path = ( _normalize_rw_path( @@ -371,6 +430,7 @@ class FileWriteTool(FunctionTool): local_env=local_env, umo=context.context.event.unified_msg_origin, write=True, + current_workspace_root=current_workspace_root, ) if local_env else path.strip() @@ -442,6 +502,9 @@ class FileEditTool(FunctionTool): umo = str(context.context.event.unified_msg_origin) local_env = is_local_runtime(context) restricted = _is_restricted_env(context) + current_workspace_root = ( + await workspace_root_for_context(context) if local_env else None + ) try: normalized_path = ( _normalize_rw_path( @@ -450,6 +513,7 @@ class FileEditTool(FunctionTool): local_env=local_env, umo=umo, write=True, + current_workspace_root=current_workspace_root, ) if local_env else path.strip() @@ -599,15 +663,28 @@ class GrepTool(FunctionTool): restricted: bool, local_env: bool, umo: str, + current_workspace_root: Path | None = None, ) -> list[str]: normalized = ( - [_resolve_tool_path(path, local_env=local_env, umo=umo)] if path else [] + [ + _resolve_tool_path( + path, + local_env=local_env, + umo=umo, + current_workspace_root=current_workspace_root, + ) + ] + if path + else [] ) if not normalized: if restricted: - return [str(root) for root in _read_allowed_roots(umo)] + return [ + str(root) + for root in _read_allowed_roots(umo, current_workspace_root) + ] if local_env: - return [str(_workspace_root(umo))] + return [str(current_workspace_root or _workspace_root(umo))] return ["."] if restricted: @@ -617,12 +694,17 @@ class GrepTool(FunctionTool): if not _is_path_within_allowed_roots( path, umo=umo, - allowed_roots=_read_allowed_roots(umo), + allowed_roots=_read_allowed_roots(umo, current_workspace_root), + current_workspace_root=current_workspace_root, ) ] if disallowed: allowed = ", ".join( - _restricted_env_path_labels(umo, include_plugin_skills=True) + _restricted_env_path_labels( + umo, + include_plugin_skills=True, + current_workspace_root=current_workspace_root, + ) ) blocked = ", ".join(disallowed) raise PermissionError( @@ -649,6 +731,9 @@ class GrepTool(FunctionTool): local_env = is_local_runtime(context) restricted = _is_restricted_env(context) + current_workspace_root = ( + await workspace_root_for_context(context) if local_env else None + ) try: search_paths = ( self._normalize_search_paths( @@ -656,6 +741,7 @@ class GrepTool(FunctionTool): restricted=restricted, local_env=local_env, umo=context.context.event.unified_msg_origin, + current_workspace_root=current_workspace_root, ) if local_env else ([path.strip()] if path and path.strip() else ["."]) diff --git a/astrbot/core/tools/computer_tools/python.py b/astrbot/core/tools/computer_tools/python.py index f9500ff7e..b51c225d9 100644 --- a/astrbot/core/tools/computer_tools/python.py +++ b/astrbot/core/tools/computer_tools/python.py @@ -11,7 +11,10 @@ from astrbot.core.computer.computer_client import get_booter, get_local_booter from astrbot.core.message.message_event_result import MessageChain from ..registry import builtin_tool -from .util import check_admin_permission, workspace_root +from .util import ( + check_admin_permission, + workspace_root_for_context, +) _OS_NAME = platform.system() _SANDBOX_PYTHON_TOOL_CONFIG = { @@ -137,9 +140,7 @@ class LocalPythonTool(FunctionTool): else context.tool_call_timeout ) try: - current_workspace_root = workspace_root( - context.context.event.unified_msg_origin - ) + current_workspace_root = await workspace_root_for_context(context) current_workspace_root.mkdir(parents=True, exist_ok=True) result = await sb.python.exec( code, diff --git a/astrbot/core/tools/computer_tools/shell.py b/astrbot/core/tools/computer_tools/shell.py index 1e1acfbf9..88d1d69e4 100644 --- a/astrbot/core/tools/computer_tools/shell.py +++ b/astrbot/core/tools/computer_tools/shell.py @@ -14,7 +14,11 @@ from astrbot.core.computer.computer_client import get_booter from astrbot.core.utils.astrbot_path import get_astrbot_system_tmp_path from ..registry import builtin_tool -from .util import check_admin_permission, is_local_runtime, workspace_root +from .util import ( + check_admin_permission, + is_local_runtime, + workspace_root_for_context, +) _COMPUTER_RUNTIME_TOOL_CONFIG = { "provider_settings.computer_use_runtime": ("local", "sandbox"), @@ -99,9 +103,7 @@ class ExecuteShellTool(FunctionTool): try: cwd: str | None = None if is_local_runtime(context): - current_workspace_root = workspace_root( - context.context.event.unified_msg_origin - ) + current_workspace_root = await workspace_root_for_context(context) current_workspace_root.mkdir(parents=True, exist_ok=True) cwd = str(current_workspace_root) diff --git a/astrbot/core/tools/computer_tools/util.py b/astrbot/core/tools/computer_tools/util.py index a3930b4c6..9bb71298b 100644 --- a/astrbot/core/tools/computer_tools/util.py +++ b/astrbot/core/tools/computer_tools/util.py @@ -1,20 +1,48 @@ -import re from pathlib import Path from astrbot.core.agent.run_context import ContextWrapper from astrbot.core.astr_agent_context import AstrAgentContext +from astrbot.core.db import BaseDatabase from astrbot.core.utils.astrbot_path import get_astrbot_workspaces_path - - -def normalize_umo_for_workspace(umo: str) -> str: - normalized = re.sub(r"[^A-Za-z0-9._-]+", "_", umo.strip()) - return normalized or "unknown" +from astrbot.core.workspace import ( + normalize_umo_for_workspace, + resolve_workspace_root_for_umo, +) def workspace_root(umo: str) -> Path: - """Root directory for relative paths in local runtime""" - normalized_umo = normalize_umo_for_workspace(umo) - return (Path(get_astrbot_workspaces_path()) / normalized_umo).resolve(strict=False) + """Return the legacy workspace root for compatibility. + + Args: + umo: Unified message origin. + + Returns: + Legacy per-session workspace root. + """ + return ( + Path(get_astrbot_workspaces_path()) / normalize_umo_for_workspace(umo) + ).resolve(strict=False) + + +async def workspace_root_for_context( + context: ContextWrapper[AstrAgentContext], +) -> Path: + """Resolve the workspace root for a tool call context. + + Args: + context: Tool call context. + + Returns: + Workspace root used as cwd. + """ + umo = context.context.event.unified_msg_origin + db = getattr(context.context.context, "_db", None) + if not isinstance(db, BaseDatabase): + return workspace_root(umo) + try: + return await resolve_workspace_root_for_umo(umo, db) + except Exception: + return workspace_root(umo) def is_local_runtime(context: ContextWrapper[AstrAgentContext]) -> bool: diff --git a/astrbot/core/tools/message_tools.py b/astrbot/core/tools/message_tools.py index 0f5ac7e5d..7bb7eda26 100644 --- a/astrbot/core/tools/message_tools.py +++ b/astrbot/core/tools/message_tools.py @@ -20,6 +20,7 @@ from astrbot.core.tools.computer_tools.util import ( check_admin_permission, is_local_runtime, workspace_root, + workspace_root_for_context, ) from astrbot.core.tools.registry import builtin_tool from astrbot.core.utils.astrbot_path import ( @@ -28,10 +29,13 @@ from astrbot.core.utils.astrbot_path import ( ) -def _file_send_allowed_roots(umo: str | None) -> tuple[Path, ...]: +def _file_send_allowed_roots( + umo: str | None, + current_workspace_root: Path | None = None, +) -> tuple[Path, ...]: roots = [] if umo: - roots.append(workspace_root(umo)) + roots.append(current_workspace_root or workspace_root(umo)) roots.extend( [ Path(get_astrbot_temp_path()).resolve(strict=False), @@ -59,9 +63,10 @@ def _is_restricted_local_env(context: ContextWrapper[AstrAgentContext]) -> bool: def _can_send_local_file( context: ContextWrapper[AstrAgentContext], local_path: Path, + current_workspace_root: Path | None = None, ) -> bool: umo = context.context.event.unified_msg_origin - allowed_roots = _file_send_allowed_roots(umo) + allowed_roots = _file_send_allowed_roots(umo, current_workspace_root) if _is_path_within(local_path, allowed_roots): return True return is_local_runtime(context) and not _is_restricted_local_env(context) @@ -137,12 +142,18 @@ class SendMessageToUserTool(FunctionTool[AstrAgentContext]): if not path: raise FileNotFoundError(f"{component_type} path is empty") + current_workspace_root = ( + await workspace_root_for_context(context) + if is_local_runtime(context) + else None + ) + # Relative host paths are resolved only inside the user's workspace. if not os.path.isabs(path): unified_msg_origin = context.context.event.unified_msg_origin if unified_msg_origin: + ws_path = current_workspace_root or workspace_root(unified_msg_origin) try: - ws_path = workspace_root(unified_msg_origin) ws_candidate = (ws_path / path).resolve(strict=False) if ws_candidate.is_file() and ws_candidate.is_relative_to(ws_path): return str(ws_candidate), False @@ -151,13 +162,16 @@ class SendMessageToUserTool(FunctionTool[AstrAgentContext]): else: local_candidate = Path(path).expanduser().resolve(strict=False) if local_candidate.is_file(): - if _can_send_local_file(context, local_candidate): + if _can_send_local_file( + context, local_candidate, current_workspace_root + ): return str(local_candidate), False if is_local_runtime(context): allowed = ", ".join( str(root) for root in _file_send_allowed_roots( - context.context.event.unified_msg_origin + context.context.event.unified_msg_origin, + current_workspace_root, ) ) raise PermissionError( diff --git a/astrbot/core/workspace.py b/astrbot/core/workspace.py new file mode 100644 index 000000000..00ff5c5f0 --- /dev/null +++ b/astrbot/core/workspace.py @@ -0,0 +1,196 @@ +from __future__ import annotations + +import re +from pathlib import Path +from typing import Any + +from astrbot.core.db import BaseDatabase +from astrbot.core.platform.message_session import MessageSession +from astrbot.core.utils.astrbot_path import get_astrbot_workspaces_path + +WORKSPACE_TYPE_SESSION = "session" +WORKSPACE_TYPE_PROJECT = "project" +WORKSPACE_TYPE_CUSTOM = "custom" +WORKSPACE_TYPES = { + WORKSPACE_TYPE_SESSION, + WORKSPACE_TYPE_PROJECT, + WORKSPACE_TYPE_CUSTOM, +} + + +def normalize_umo_for_workspace(umo: str) -> str: + """Normalize a unified message origin into a filesystem-safe name. + + Args: + umo: Unified message origin. + + Returns: + Filesystem-safe workspace directory name. + """ + normalized = re.sub(r"[^A-Za-z0-9._-]+", "_", umo.strip()) + return normalized or "unknown" + + +def normalize_project_workspace_type(value: Any) -> str: + """Normalize stored or incoming project workspace type. + + Args: + value: Raw workspace type value. + + Returns: + A known workspace type. + """ + workspace_type = str(value or WORKSPACE_TYPE_SESSION).strip().lower() + return ( + workspace_type if workspace_type in WORKSPACE_TYPES else WORKSPACE_TYPE_SESSION + ) + + +def normalize_workspace_path(path: Any) -> str | None: + """Normalize a custom workspace path value for storage. + + Args: + path: Raw path value from API or database. + + Returns: + Normalized path string, or None when empty. + """ + if not isinstance(path, str): + return None + value = path.strip() + return value or None + + +def default_workspace_root(umo: str) -> Path: + """Return the legacy per-session workspace root. + + Args: + umo: Unified message origin. + + Returns: + The legacy workspace directory path. + """ + return ( + Path(get_astrbot_workspaces_path()) / normalize_umo_for_workspace(umo) + ).resolve(strict=False) + + +def project_workspace_root(project_id: str) -> Path: + """Return the default shared workspace root for a ChatUI project. + + Args: + project_id: ChatUI project ID. + + Returns: + The project workspace directory path. + """ + safe_project_id = re.sub(r"[^A-Za-z0-9._-]+", "_", project_id.strip()) + return (Path(get_astrbot_workspaces_path()) / f"project_{safe_project_id}").resolve( + strict=False + ) + + +def workspace_path_to_root(path: str) -> Path: + """Resolve a custom workspace path. + + Args: + path: Stored workspace path. Relative values are rooted under AstrBot + workspaces. Absolute values are allowed and resolved as provided. + + Returns: + Absolute resolved path. + + Raises: + ValueError: If a relative path escapes or targets the AstrBot workspaces + root. + """ + workspaces_root = Path(get_astrbot_workspaces_path()).resolve(strict=False) + candidate = Path(path).expanduser() + if candidate.is_absolute(): + return candidate.resolve(strict=False) + + resolved = (workspaces_root / candidate).resolve(strict=False) + if resolved == workspaces_root or not resolved.is_relative_to(workspaces_root): + raise ValueError( + "Relative workspace path must stay within a subdirectory of AstrBot workspaces" + ) + return resolved + + +def resolve_project_workspace_root(project: Any, *, fallback_umo: str) -> Path: + """Resolve the workspace root from a project record. + + Args: + project: Project-like object with workspace fields. + fallback_umo: UMO used when the project keeps legacy session workspaces. + + Returns: + Workspace root used as cwd. + """ + fallback = default_workspace_root(fallback_umo) + workspace_type = normalize_project_workspace_type( + getattr(project, "workspace_type", WORKSPACE_TYPE_SESSION) + ) + if workspace_type == WORKSPACE_TYPE_SESSION: + return fallback + if workspace_type == WORKSPACE_TYPE_PROJECT: + return project_workspace_root(str(project.project_id)) + if workspace_type == WORKSPACE_TYPE_CUSTOM: + workspace_path = normalize_workspace_path( + getattr(project, "workspace_path", None) + ) + if workspace_path: + return workspace_path_to_root(workspace_path) + return fallback + + +def parse_webchat_umo(umo: str) -> tuple[str, str] | None: + """Extract creator and session ID from a webchat UMO. + + Args: + umo: Unified message origin. + + Returns: + Tuple of creator and ChatUI session ID, or None for non-webchat UMO. + """ + try: + message_session = MessageSession.from_str(umo) + except Exception: + return None + + if message_session.platform_name != "webchat": + return None + + parts = message_session.session_id.split("!", 2) + if len(parts) != 3 or parts[0] != "webchat": + return None + return parts[1], parts[2] + + +async def resolve_workspace_root_for_umo( + umo: str, + db: BaseDatabase | None = None, +) -> Path: + """Resolve the workspace root for a UMO. + + Args: + umo: Unified message origin. + db: Optional database instance. Falls back to the global database helper. + + Returns: + Workspace root used as cwd. + """ + parsed = parse_webchat_umo(umo) + if not parsed: + return default_workspace_root(umo) + + creator, session_id = parsed + if db is None: + from astrbot.core import db_helper + + db = db_helper + + project = await db.get_project_by_session(session_id=session_id, creator=creator) + if not project: + return default_workspace_root(umo) + return resolve_project_workspace_root(project, fallback_umo=umo) diff --git a/astrbot/dashboard/schemas.py b/astrbot/dashboard/schemas.py index f37449c53..bd72e1e4b 100644 --- a/astrbot/dashboard/schemas.py +++ b/astrbot/dashboard/schemas.py @@ -95,6 +95,8 @@ class ChatProjectRequest(OpenModel): title: str | None = None emoji: str | None = None description: str | None = None + workspace_type: str | None = None + workspace_path: str | None = None class ChatProjectSessionRequest(OpenModel): diff --git a/astrbot/dashboard/services/chatui_project_service.py b/astrbot/dashboard/services/chatui_project_service.py index 4a928751e..34e711d74 100644 --- a/astrbot/dashboard/services/chatui_project_service.py +++ b/astrbot/dashboard/services/chatui_project_service.py @@ -1,7 +1,17 @@ from __future__ import annotations +import os + from astrbot.core.db import BaseDatabase from astrbot.core.utils.datetime_utils import to_utc_isoformat +from astrbot.core.workspace import ( + WORKSPACE_TYPE_CUSTOM, + WORKSPACE_TYPE_SESSION, + normalize_project_workspace_type, + normalize_workspace_path, + resolve_project_workspace_root, + workspace_path_to_root, +) class ChatUIProjectServiceError(Exception): @@ -17,6 +27,7 @@ class ChatUIProjectService: title = payload.get("title") emoji = payload.get("emoji", "📁") description = payload.get("description") + workspace_type, workspace_path = self._normalize_workspace_config(payload) if not title: raise ChatUIProjectServiceError("Missing key: title") @@ -26,6 +37,8 @@ class ChatUIProjectService: title=title, emoji=emoji, description=description, + workspace_type=workspace_type, + workspace_path=workspace_path, ) return self._serialize_project(project) @@ -53,12 +66,22 @@ class ChatUIProjectService: if not project_id: raise ChatUIProjectServiceError("Missing key: project_id") - await self._get_owned_project(username, project_id) + project = await self._get_owned_project(username, project_id) + workspace_type = None + workspace_path = None + if "workspace_type" in payload or "workspace_path" in payload: + workspace_type, workspace_path = self._normalize_workspace_config( + payload, + fallback_type=project.workspace_type, + fallback_path=project.workspace_path, + ) await self.db.update_chatui_project( project_id=project_id, title=payload.get("title"), emoji=payload.get("emoji"), description=payload.get("description"), + workspace_type=workspace_type, + workspace_path=workspace_path, ) async def delete_project(self, username: str, project_id: str | None) -> None: @@ -136,11 +159,32 @@ class ChatUIProjectService: @staticmethod def _serialize_project(project) -> dict: + workspace_type = normalize_project_workspace_type( + getattr(project, "workspace_type", WORKSPACE_TYPE_SESSION) + ) + workspace_path = normalize_workspace_path( + getattr(project, "workspace_path", None) + ) + resolved_workspace_path = None + if workspace_type != WORKSPACE_TYPE_SESSION: + fallback_umo = f"webchat:FriendMessage:webchat!{project.creator}!default" + try: + resolved_workspace_path = str( + resolve_project_workspace_root( + project, + fallback_umo=fallback_umo, + ) + ) + except ValueError: + resolved_workspace_path = None return { "project_id": project.project_id, "title": project.title, "emoji": project.emoji, "description": project.description, + "workspace_type": workspace_type, + "workspace_path": workspace_path, + "resolved_workspace_path": resolved_workspace_path, "created_at": to_utc_isoformat(project.created_at), "updated_at": to_utc_isoformat(project.updated_at), } @@ -160,3 +204,49 @@ class ChatUIProjectService: @staticmethod def _as_payload(data: object) -> dict: return data if isinstance(data, dict) else {} + + @staticmethod + def _normalize_workspace_config( + payload: dict, + *, + fallback_type: str | None = None, + fallback_path: str | None = None, + ) -> tuple[str, str | None]: + """Normalize project workspace config from request payload. + + Args: + payload: Request payload. + fallback_type: Existing workspace type used when omitted. + fallback_path: Existing workspace path used when omitted. + + Returns: + Normalized workspace type and path. + + Raises: + ChatUIProjectServiceError: If a custom workspace has no usable path. + """ + workspace_type = normalize_project_workspace_type( + payload.get("workspace_type", fallback_type or WORKSPACE_TYPE_SESSION) + ) + raw_path = payload.get("workspace_path", fallback_path) + workspace_path = normalize_workspace_path(raw_path) + if workspace_type != WORKSPACE_TYPE_CUSTOM: + workspace_path = None + return workspace_type, workspace_path + + if not workspace_path: + raise ChatUIProjectServiceError("Custom workspace requires a path") + + try: + workspace_root = workspace_path_to_root(workspace_path) + except ValueError as exc: + raise ChatUIProjectServiceError(str(exc)) from exc + if not workspace_root.exists(): + raise ChatUIProjectServiceError("Custom workspace path does not exist") + if not workspace_root.is_dir(): + raise ChatUIProjectServiceError("Custom workspace path must be a directory") + if not os.access(workspace_root, os.R_OK | os.W_OK | os.X_OK): + raise ChatUIProjectServiceError( + "Custom workspace path requires read, write, and enter permissions" + ) + return workspace_type, workspace_path diff --git a/dashboard/package.json b/dashboard/package.json index b493f1d99..81a353bf2 100644 --- a/dashboard/package.json +++ b/dashboard/package.json @@ -18,6 +18,7 @@ "dependencies": { "@guolao/vue-monaco-editor": "^1.5.4", "@hey-api/client-axios": "0.2.12", + "@lucide/vue": "^1.23.0", "@tiptap/starter-kit": "2.1.7", "@tiptap/vue-3": "2.1.7", "apexcharts": "3.42.0", diff --git a/dashboard/pnpm-lock.yaml b/dashboard/pnpm-lock.yaml index 81049e3dd..cae4d6bc1 100644 --- a/dashboard/pnpm-lock.yaml +++ b/dashboard/pnpm-lock.yaml @@ -18,6 +18,9 @@ importers: '@hey-api/client-axios': specifier: 0.2.12 version: 0.2.12(axios@1.13.5) + '@lucide/vue': + specifier: ^1.23.0 + version: 1.23.0(vue@3.3.4) '@tiptap/starter-kit': specifier: 2.1.7 version: 2.1.7(@tiptap/pm@2.27.2) @@ -501,6 +504,11 @@ packages: '@keyv/serialize@1.1.1': resolution: {integrity: sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==} + '@lucide/vue@1.23.0': + resolution: {integrity: sha512-9SIYeY5K+R1iv8F8JUKMGSL7Pck/86BJ8djtZz/lnYsoHtKFUj1H2z6+PvKnC/8blZ8tqTDeDXAoTKobuX80hg==} + peerDependencies: + vue: '>=3.0.1' + '@mdi/font@7.2.96': resolution: {integrity: sha512-e//lmkmpFUMZKhmCY9zdjRe4zNXfbOIJnn6xveHbaV2kSw5aJ5dLXUxcRt1Gxfi7ZYpFLUWlkG2MGSFAiqAu7w==} @@ -3496,6 +3504,10 @@ snapshots: '@keyv/serialize@1.1.1': {} + '@lucide/vue@1.23.0(vue@3.3.4)': + dependencies: + vue: 3.3.4 + '@mdi/font@7.2.96': {} '@mermaid-js/parser@0.6.3': diff --git a/dashboard/public/favicon.svg b/dashboard/public/favicon.svg index db85f3f33..e4f30733b 100644 --- a/dashboard/public/favicon.svg +++ b/dashboard/public/favicon.svg @@ -1 +1,14 @@ - \ No newline at end of file + + + + + + + + diff --git a/dashboard/src/api/generated/openapi-v1/types.gen.ts b/dashboard/src/api/generated/openapi-v1/types.gen.ts index 49e94b2bb..4e3d94ffc 100644 --- a/dashboard/src/api/generated/openapi-v1/types.gen.ts +++ b/dashboard/src/api/generated/openapi-v1/types.gen.ts @@ -83,8 +83,12 @@ export type ChatProjectRequest = { title?: string; emoji?: string; description?: string; + workspace_type?: 'session' | 'project' | 'custom'; + workspace_path?: string; }; +export type workspace_type = 'session' | 'project' | 'custom'; + export type ChatRequest = { /** * Caller-declared WebChat sender/session owner. This value is used as the message sender identity and may participate in sender-ID-based command permission checks. Treat chat-scoped API keys as trusted backend credentials and map or validate usernames before accepting end-user input. diff --git a/dashboard/src/assets/mdi-subset/materialdesignicons-subset.css b/dashboard/src/assets/mdi-subset/materialdesignicons-subset.css index 9ada8cf9c..924632cd6 100644 --- a/dashboard/src/assets/mdi-subset/materialdesignicons-subset.css +++ b/dashboard/src/assets/mdi-subset/materialdesignicons-subset.css @@ -496,6 +496,10 @@ content: "\F024B"; } +.mdi-folder-cog-outline::before { + content: "\F1080"; +} + .mdi-folder-move::before { content: "\F0252"; } @@ -980,10 +984,6 @@ content: "\F062C"; } -.mdi-square-edit-outline::before { - content: "\F090C"; -} - .mdi-star::before { content: "\F04CE"; } diff --git a/dashboard/src/assets/mdi-subset/materialdesignicons-webfont-subset.woff b/dashboard/src/assets/mdi-subset/materialdesignicons-webfont-subset.woff index 6401c409e3a7e525eee48ad31044090b82110891..ad504744bb9101c80a1bbcfa4cdabbfde2a0fbe1 100644 GIT binary patch delta 19461 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 literal 16008 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 + + -
-
- {{ - sessionTitle(session) - }} -
- - + +
+ {{ sessionTitle(session) }} +
+ + + + + + +
+
- -
- -
- {{ tm("conversation.noHistory") }} -
+
@@ -505,6 +547,20 @@ import { import { useRoute, useRouter } from "vue-router"; import { useDisplay } from "vuetify"; import { isAxiosError } from "axios"; +import { + Box, + Cable, + Check, + ChevronRight, + Languages, + Moon, + PanelLeft, + Pencil, + Settings, + SquarePen, + Sun, + Trash2, +} from "@lucide/vue"; import { chatApi } from "@/api/v1"; import StyledMenu from "@/components/shared/StyledMenu.vue"; import ProjectDialog, { @@ -514,6 +570,7 @@ import ProjectList, { type Project } from "@/components/chat/ProjectList.vue"; import ProjectView from "@/components/chat/ProjectView.vue"; import ChatInput from "@/components/chat/ChatInput.vue"; import ChatMessageList from "@/components/chat/ChatMessageList.vue"; +import ChatUILogo from "@/components/chat/ChatUILogo.vue"; import type { RegenerateModelSelection } from "@/components/chat/RegenerateMenu.vue"; import ReasoningSidebar from "@/components/chat/ReasoningSidebar.vue"; import ThreadPanel from "@/components/chat/ThreadPanel.vue"; @@ -530,6 +587,7 @@ import { import { useMediaHandling } from "@/composables/useMediaHandling"; import { useRecording } from "@/composables/useRecording"; import { useProjects } from "@/composables/useProjects"; +import { useChatHeaderStore } from "@/stores/chatHeader"; import { useCustomizerStore } from "@/stores/customizer"; import ProviderChatCompletionPanel from "@/components/provider/ProviderChatCompletionPanel.vue"; import { @@ -549,6 +607,7 @@ const props = withDefaults(defineProps<{ chatboxMode?: boolean; active?: boolean const route = useRoute(); const router = useRouter(); const { lgAndUp } = useDisplay(); +const chatHeader = useChatHeaderStore(); const customizer = useCustomizerStore(); const { t } = useI18n(); const { tm } = useModuleI18n("features/chat"); @@ -593,10 +652,11 @@ const { type WorkspaceView = "chat" | "providers"; -const sidebarCollapsed = ref(false); const activeWorkspace = ref("chat"); const projectDialogOpen = ref(false); const editingProject = ref(null); +const projectDialogError = ref(""); +const savingProject = ref(false); const sessionTitleDialogOpen = ref(false); const sessionTitleDraft = ref(""); const editingSessionTitleId = ref(""); @@ -606,6 +666,8 @@ const messageEditDraft = ref(""); const editingMessage = ref(null); const savingMessageEdit = ref(false); const projectSessions = ref([]); +const projectSessionsById = ref>({}); +const loadingProjectSessionIds = ref([]); const loadingSessions = ref(false); const draft = ref(""); const messagesContainer = ref(null); @@ -651,11 +713,20 @@ const chatSidebarDrawer = computed({ }, }); const isSidebarCollapsed = computed(() => - lgAndUp.value ? sidebarCollapsed.value : !customizer.chatSidebarOpen, + lgAndUp.value ? customizer.chatSidebarCollapsed : !customizer.chatSidebarOpen, ); const isProviderWorkspace = computed( () => activeWorkspace.value === "providers", ); + +function toggleChatSidebar() { + if (lgAndUp.value) { + customizer.SET_CHAT_SIDEBAR_COLLAPSED(!customizer.chatSidebarCollapsed); + return; + } + customizer.TOGGLE_CHAT_SIDEBAR(); +} + const activeReasoningParts = computed(() => { if (!activeReasoningTarget.value) return []; const blocks = buildMessageBlocks( @@ -730,6 +801,9 @@ const currentSession = computed( projectSessions.value.find( (session) => session.session_id === currSessionId.value, ) || + Object.values(projectSessionsById.value) + .flat() + .find((session) => session.session_id === currSessionId.value) || null, ); const sessionProject = computed(() => @@ -744,6 +818,21 @@ const selectedProject = computed( (project) => project.project_id === selectedProjectId.value, ) || null, ); +const isEmptyChat = computed( + () => + !isProviderWorkspace.value && + !selectedProject.value && + !loadingMessages.value && + !activeMessages.value.length, +); +const chatHeaderTitle = computed( + () => currentSessionTitle.value || selectedProject.value?.title || "", +); +const chatHeaderSubtitle = computed(() => + currentSessionTitle.value + ? sessionProject.value?.title || selectedProject.value?.title || "" + : "", +); const chatInputReplyTarget = computed(() => replyTarget.value?.id == null ? null @@ -753,8 +842,30 @@ const chatInputReplyTarget = computed(() => }, ); +function getSelectedProviderSelection() { + const inputSelection = inputRef.value?.getCurrentSelection(); + if (inputSelection?.providerId) { + return inputSelection; + } + if (typeof window === "undefined") { + return { providerId: "", modelName: "" }; + } + return { + providerId: localStorage.getItem("selectedProvider") || "", + modelName: localStorage.getItem("selectedProviderModel") || "", + }; +} + provide("isDark", isDark); +watch( + [chatHeaderTitle, chatHeaderSubtitle], + ([title, subtitle]) => { + chatHeader.SET_CONTEXT({ title, subtitle }); + }, + { immediate: true }, +); + onMounted(async () => { loadingSessions.value = true; try { @@ -771,6 +882,7 @@ onMounted(async () => { }); onBeforeUnmount(() => { + chatHeader.CLEAR_CONTEXT(); cleanupMediaCache(); }); @@ -853,11 +965,13 @@ async function startNewChat() { function openCreateProjectDialog() { editingProject.value = null; + projectDialogError.value = ""; projectDialogOpen.value = true; } function openEditProjectDialog(project: Project) { editingProject.value = project; + projectDialogError.value = ""; projectDialogOpen.value = true; } @@ -874,13 +988,40 @@ async function selectProject(projectId: string) { async function loadProjectSessions(projectId = selectedProjectId.value) { if (!projectId) { projectSessions.value = []; - return; + return []; + } + const sessions = await getProjectSessions(projectId); + projectSessionsById.value = { + ...projectSessionsById.value, + [projectId]: sessions, + }; + if (projectId === selectedProjectId.value) { + projectSessions.value = sessions; + } + return sessions; +} + +async function handleProjectToggle(projectId: string, expanded: boolean) { + if (!expanded || projectSessionsById.value[projectId]) return; + if (loadingProjectSessionIds.value.includes(projectId)) return; + loadingProjectSessionIds.value = [...loadingProjectSessionIds.value, projectId]; + try { + await loadProjectSessions(projectId); + } finally { + loadingProjectSessionIds.value = loadingProjectSessionIds.value.filter( + (item) => item !== projectId, + ); } - projectSessions.value = await getProjectSessions(projectId); } async function handleDeleteProject(projectId: string) { await deleteProjectById(projectId); + const nextSessionsById = { ...projectSessionsById.value }; + delete nextSessionsById[projectId]; + projectSessionsById.value = nextSessionsById; + loadingProjectSessionIds.value = loadingProjectSessionIds.value.filter( + (item) => item !== projectId, + ); if (selectedProjectId.value === projectId) { selectedProjectId.value = null; projectSessions.value = []; @@ -915,6 +1056,14 @@ async function saveSessionTitleDialog() { if (projectSession) { projectSession.display_name = displayName; } + Object.values(projectSessionsById.value).forEach((projectSessionList) => { + const cachedProjectSession = projectSessionList.find( + (session) => session.session_id === sessionId, + ); + if (cachedProjectSession) { + cachedProjectSession.display_name = displayName; + } + }); if (refreshProjectSessionsAfterTitleSave.value) { await loadProjectSessions(); } @@ -950,25 +1099,57 @@ async function editProjectSessionTitle(sessionId: string, title: string) { openSessionTitleDialog(sessionId, title, true); } -async function deleteProjectSession(sessionId: string) { +async function deleteProjectSession( + sessionId: string, + projectId = selectedProjectId.value, +) { await deleteSession(sessionId); - await loadProjectSessions(); + if (projectId) { + await loadProjectSessions(projectId); + } else { + await loadProjectSessions(); + } } async function saveProject(formData: ProjectFormData, projectId?: string) { - if (projectId) { - await updateProject( - projectId, - formData.title, - formData.emoji, - formData.description, - ); - return; + savingProject.value = true; + projectDialogError.value = ""; + try { + if (projectId) { + await updateProject( + projectId, + formData.title, + formData.emoji, + formData.description, + formData.workspace_type, + formData.workspace_path, + ); + } else { + await createProject( + formData.title, + formData.emoji, + formData.description, + formData.workspace_type, + formData.workspace_path, + ); + } + projectDialogOpen.value = false; + editingProject.value = null; + } catch (error) { + projectDialogError.value = + error instanceof Error ? error.message : "Failed to save project"; + } finally { + savingProject.value = false; } - - await createProject(formData.title, formData.emoji, formData.description); } +watch(projectDialogOpen, (open) => { + if (!open) { + projectDialogError.value = ""; + savingProject.value = false; + } +}); + async function selectSession(sessionId: string, pushRoute = true) { showChatWorkspace(); selectedProjectId.value = null; @@ -1012,7 +1193,7 @@ async function sendCurrentMessage() { const text = draft.value.trim(); const messageId = crypto.randomUUID?.() || `${Date.now()}-${Math.random()}`; const outgoingParts = buildOutgoingParts(text); - const selection = inputRef.value?.getCurrentSelection(); + const selection = getSelectedProviderSelection(); const { userRecord, botRecord } = createLocalExchange({ sessionId, messageId, @@ -1069,8 +1250,28 @@ function buildOutgoingParts(text: string): MessagePart[] { function updateTitleFromText(sessionId: string, text: string) { const session = sessions.value.find((item) => item.session_id === sessionId); - if (!session || session.display_name || !text) return; + const projectSession = projectSessions.value.find( + (item) => item.session_id === sessionId, + ); + const cachedProjectSessions = Object.values(projectSessionsById.value) + .flat() + .filter((item) => item.session_id === sessionId); + if ( + (!session && !projectSession && !cachedProjectSessions.length) || + session?.display_name || + projectSession?.display_name || + cachedProjectSessions.some((item) => item.display_name) || + !text + ) { + return; + } updateSessionTitle(sessionId, text.slice(0, 40)); + if (projectSession) { + projectSession.display_name = text.slice(0, 40); + } + cachedProjectSessions.forEach((item) => { + item.display_name = text.slice(0, 40); + }); } function replyPreview(messageId?: string | number, fallback?: string) { @@ -1127,7 +1328,7 @@ async function saveMessageEdit() { cancelMessageEdit(); if (result.needsRegenerate && result.truncatedAfterMessage) { - const selection = inputRef.value?.getCurrentSelection(); + const selection = getSelectedProviderSelection(); continueEditedMessage({ sessionId: currSessionId.value, sourceRecord: target, @@ -1370,11 +1571,15 @@ function toggleTheme() { diff --git a/dashboard/src/components/chat/ChatUILogo.vue b/dashboard/src/components/chat/ChatUILogo.vue new file mode 100644 index 000000000..bdbb248fa --- /dev/null +++ b/dashboard/src/components/chat/ChatUILogo.vue @@ -0,0 +1,23 @@ + diff --git a/dashboard/src/components/chat/ProjectDialog.vue b/dashboard/src/components/chat/ProjectDialog.vue index 49c0e3dba..3a00d5451 100644 --- a/dashboard/src/components/chat/ProjectDialog.vue +++ b/dashboard/src/components/chat/ProjectDialog.vue @@ -1,33 +1,52 @@ diff --git a/dashboard/src/components/chat/ProjectView.vue b/dashboard/src/components/chat/ProjectView.vue index 7c87f3539..04a499ba8 100644 --- a/dashboard/src/components/chat/ProjectView.vue +++ b/dashboard/src/components/chat/ProjectView.vue @@ -1,74 +1,85 @@ diff --git a/dashboard/src/components/chat/ReasoningSidebar.vue b/dashboard/src/components/chat/ReasoningSidebar.vue index a20a4bb89..b7d276b70 100644 --- a/dashboard/src/components/chat/ReasoningSidebar.vue +++ b/dashboard/src/components/chat/ReasoningSidebar.vue @@ -60,9 +60,10 @@ function close() { diff --git a/dashboard/src/components/shared/StyledMenu.vue b/dashboard/src/components/shared/StyledMenu.vue index d52e96c8e..01c58099b 100644 --- a/dashboard/src/components/shared/StyledMenu.vue +++ b/dashboard/src/components/shared/StyledMenu.vue @@ -7,7 +7,7 @@ @@ -35,9 +35,10 @@ withDefaults(defineProps<{ .styled-menu-card { min-width: 100px; width: fit-content; - border: 1px solid rgba(var(--v-theme-primary), 0.15) !important; + border: 0 !important; background: rgba(var(--v-theme-surface), 0.98) !important; backdrop-filter: blur(10px); + box-shadow: var(--astrbot-menu-shadow, 0 12px 28px rgba(0, 0, 0, 0.08)) !important; } .styled-menu-card-borderless { @@ -70,7 +71,7 @@ withDefaults(defineProps<{ /* 深色模式下的下拉框样式 - 需要全局样式才能检测主题 */ .v-theme--PurpleThemeDark .styled-menu-card { background: rgba(var(--v-theme-surface), 0.98) !important; - border: 1px solid rgba(var(--v-theme-primary), 0.2) !important; + border: 0 !important; } .v-theme--PurpleThemeDark .styled-menu-card-borderless { diff --git a/dashboard/src/composables/useProjects.ts b/dashboard/src/composables/useProjects.ts index c80221d12..6d2789f0d 100644 --- a/dashboard/src/composables/useProjects.ts +++ b/dashboard/src/composables/useProjects.ts @@ -2,6 +2,17 @@ import { ref } from 'vue'; import { chatApi } from '@/api/v1'; import type { Project } from '@/components/chat/ProjectList.vue'; +type WorkspaceType = 'session' | 'project' | 'custom'; + +function projectErrorMessage(error: unknown, fallback: string) { + const responseMessage = (error as any)?.response?.data?.message; + if (typeof responseMessage === 'string' && responseMessage.trim()) { + return responseMessage; + } + const message = (error as Error)?.message; + return message || fallback; +} + export function useProjects() { const projects = ref([]); const selectedProjectId = ref(null); @@ -18,34 +29,56 @@ export function useProjects() { } } - async function createProject(title: string, emoji?: string, description?: string) { + async function createProject( + title: string, + emoji?: string, + description?: string, + workspaceType: WorkspaceType = 'project', + workspacePath?: string + ) { try { const res = await chatApi.createProject({ title, emoji: emoji || '📁', - description + description, + workspace_type: workspaceType, + workspace_path: workspacePath }); if (res.data.status === 'ok') { await getProjects(); return res.data.data; } + throw new Error(res.data.message || 'Failed to create project'); } catch (error) { console.error('Failed to create project:', error); + throw new Error(projectErrorMessage(error, 'Failed to create project')); } } - async function updateProject(projectId: string, title?: string, emoji?: string, description?: string) { + async function updateProject( + projectId: string, + title?: string, + emoji?: string, + description?: string, + workspaceType?: WorkspaceType, + workspacePath?: string + ) { try { const res = await chatApi.updateProject(projectId, { title, emoji, - description + description, + workspace_type: workspaceType, + workspace_path: workspacePath }); if (res.data.status === 'ok') { await getProjects(); + return; } + throw new Error(res.data.message || 'Failed to update project'); } catch (error) { console.error('Failed to update project:', error); + throw new Error(projectErrorMessage(error, 'Failed to update project')); } } diff --git a/dashboard/src/i18n/locales/en-US/features/chat.json b/dashboard/src/i18n/locales/en-US/features/chat.json index 2eccb41ba..2ce8adc4b 100644 --- a/dashboard/src/i18n/locales/en-US/features/chat.json +++ b/dashboard/src/i18n/locales/en-US/features/chat.json @@ -2,7 +2,7 @@ "title": "Let's Chat!", "subtitle": "Chat with AI Assistant", "input": { - "placeholder": "Start typing...", + "placeholder": "Ask anything...", "send": "Send", "clear": "Clear", "upload": "Upload File", @@ -31,7 +31,7 @@ "liveMode": "Live Mode" }, "welcome": { - "title": "Welcome to AstrBot", + "title": "Anything on your mind today?", "subtitle": "Your Intelligent Chat Assistant", "quickActions": "Quick Actions", "examples": "Example Questions" @@ -73,6 +73,7 @@ "placeholder": "Ask about this excerpt..." }, "conversation": { + "title": "Conversations", "newConversation": "New Conversation", "noHistory": "No conversation history", "systemStatus": "System Status", @@ -131,7 +132,16 @@ "name": "Project Name", "emoji": "Icon (Emoji)", "description": "Description (Optional)", + "workspace": { + "type": "Workspace", + "session": "Per-session workspace", + "project": "Shared project workspace", + "custom": "Custom path", + "path": "Workspace path" + }, + "loadingSessions": "Loading conversations...", "noSessions": "No conversations in this project", + "noProjects": "No projects", "confirmDelete": "Are you sure you want to delete project \"{title}\"? Conversations in this project will not be deleted." }, "time": { diff --git a/dashboard/src/i18n/locales/ru-RU/features/chat.json b/dashboard/src/i18n/locales/ru-RU/features/chat.json index fe0958e7d..ec12ce886 100644 --- a/dashboard/src/i18n/locales/ru-RU/features/chat.json +++ b/dashboard/src/i18n/locales/ru-RU/features/chat.json @@ -2,7 +2,7 @@ "title": "Давай пообщаемся!", "subtitle": "Общение с AI-помощником", "input": { - "placeholder": "Введите сообщение...", + "placeholder": "Спросите что угодно...", "send": "Отправить", "clear": "Очистить", "upload": "Загрузить файл", @@ -31,7 +31,7 @@ "liveMode": "Общение в реальном времени" }, "welcome": { - "title": "Добро пожаловать в AstrBot", + "title": "О чем поговорим сегодня?", "subtitle": "Ваш умный помощник", "quickActions": "Быстрые действия", "examples": "Примеры вопросов" @@ -73,6 +73,7 @@ "placeholder": "Спросить об этом фрагменте..." }, "conversation": { + "title": "Диалоги", "newConversation": "Новый чат", "noHistory": "История диалогов пуста", "systemStatus": "Статус системы", @@ -131,7 +132,16 @@ "name": "Имя проекта", "emoji": "Иконка (Emoji)", "description": "Описание проекта (опционально)", + "workspace": { + "type": "Рабочая область", + "session": "Отдельно для каждого чата", + "project": "Общая область проекта", + "custom": "Пользовательский путь", + "path": "Путь рабочей области" + }, + "loadingSessions": "Загрузка диалогов...", "noSessions": "В этом проекте пока нет диалогов", + "noProjects": "Проектов пока нет", "confirmDelete": "Вы уверены, что хотите удалить проект «{title}»? Диалоги внутри проекта не будут удалены." }, "time": { diff --git a/dashboard/src/i18n/locales/zh-CN/features/chat.json b/dashboard/src/i18n/locales/zh-CN/features/chat.json index 803283bc4..4adaaf6e0 100644 --- a/dashboard/src/i18n/locales/zh-CN/features/chat.json +++ b/dashboard/src/i18n/locales/zh-CN/features/chat.json @@ -2,7 +2,7 @@ "title": "聊天吧!", "subtitle": "与AI助手进行对话", "input": { - "placeholder": "开始输入...", + "placeholder": "想问什么都可以", "send": "发送", "clear": "清空", "upload": "上传文件", @@ -31,7 +31,7 @@ "liveMode": "实时对话" }, "welcome": { - "title": "欢迎使用 AstrBot", + "title": "今天想聊点什么?", "subtitle": "您的智能对话助手", "quickActions": "快速操作", "examples": "示例问题" @@ -73,6 +73,7 @@ "placeholder": "继续追问这段内容..." }, "conversation": { + "title": "对话", "newConversation": "新的聊天", "noHistory": "暂无对话历史", "systemStatus": "系统状态", @@ -131,7 +132,16 @@ "name": "项目名称", "emoji": "图标 (Emoji)", "description": "项目描述(可选)", + "workspace": { + "type": "工作区", + "session": "按会话分开", + "project": "项目共享工作区", + "custom": "自定义路径", + "path": "工作区路径" + }, + "loadingSessions": "加载对话中...", "noSessions": "该项目暂无对话", + "noProjects": "暂无项目", "confirmDelete": "确定要删除项目 \"{title}\" 吗?项目中的对话不会被删除。" }, "time": { diff --git a/dashboard/src/layouts/full/FullLayout.vue b/dashboard/src/layouts/full/FullLayout.vue index 08ea74151..19f0da45d 100644 --- a/dashboard/src/layouts/full/FullLayout.vue +++ b/dashboard/src/layouts/full/FullLayout.vue @@ -109,8 +109,9 @@ onMounted(() => { @@ -159,4 +160,8 @@ onMounted(() => { height: 100% !important; overflow: hidden !important; } + +.chat-main { + padding-top: 0 !important; +} diff --git a/dashboard/src/layouts/full/vertical-header/VerticalHeader.vue b/dashboard/src/layouts/full/vertical-header/VerticalHeader.vue index 2090cc08a..e5d89180f 100644 --- a/dashboard/src/layouts/full/vertical-header/VerticalHeader.vue +++ b/dashboard/src/layouts/full/vertical-header/VerticalHeader.vue @@ -1,5 +1,6 @@