diff --git a/astrbot/dashboard/api/chat_projects.py b/astrbot/dashboard/api/chat_projects.py index a8d4ba448..cdf27a5e9 100644 --- a/astrbot/dashboard/api/chat_projects.py +++ b/astrbot/dashboard/api/chat_projects.py @@ -1,6 +1,9 @@ from __future__ import annotations -from fastapi import APIRouter, Depends, Query, Request +import os + +from fastapi import APIRouter, Depends, HTTPException, Query, Request +from fastapi.responses import FileResponse from astrbot.dashboard.async_utils import run_maybe_async from astrbot.dashboard.responses import error, ok @@ -155,6 +158,55 @@ async def list_chat_project_sessions( return await _run(lambda: service.get_project_sessions(auth.username, project_id)) +@router.get("/chat/projects/{project_id}/workspace/files") +async def list_chat_project_workspace_files( + project_id: str, + path: str = Query(default=""), + auth: AuthContext = Depends(require_chat_scope), + service: ChatUIProjectService = Depends(get_service), +): + return await _run( + lambda: service.list_workspace_files(auth.username, project_id, path) + ) + + +@router.get("/chat/projects/{project_id}/workspace/file") +async def get_chat_project_workspace_file( + project_id: str, + path: str, + auth: AuthContext = Depends(require_chat_scope), + service: ChatUIProjectService = Depends(get_service), +): + return await _run( + lambda: service.get_workspace_file(auth.username, project_id, path) + ) + + +@router.get("/chat/projects/{project_id}/workspace/file/download") +async def download_chat_project_workspace_file( + project_id: str, + path: str, + auth: AuthContext = Depends(require_chat_scope), + service: ChatUIProjectService = Depends(get_service), +): + try: + workspace_root, file_path = await service.get_workspace_file_location( + auth.username, + project_id, + path, + ) + except ChatUIProjectServiceError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + workspace_root_path = os.path.normcase(os.path.realpath(workspace_root)) + download_path = os.path.normcase(os.path.realpath(file_path)) + workspace_root_prefix = os.path.join(workspace_root_path, "") + if download_path != workspace_root_path and not download_path.startswith( + workspace_root_prefix + ): + raise HTTPException(status_code=400, detail="Invalid workspace path") + return FileResponse(download_path, filename=os.path.basename(download_path)) + + @legacy_router.get("/get_sessions") async def list_dashboard_chat_project_sessions( project_id: str | None = Query(default=None), diff --git a/astrbot/dashboard/services/chatui_project_service.py b/astrbot/dashboard/services/chatui_project_service.py index 34e711d74..97991c195 100644 --- a/astrbot/dashboard/services/chatui_project_service.py +++ b/astrbot/dashboard/services/chatui_project_service.py @@ -1,6 +1,7 @@ from __future__ import annotations import os +from pathlib import Path from astrbot.core.db import BaseDatabase from astrbot.core.utils.datetime_utils import to_utc_isoformat @@ -13,6 +14,8 @@ from astrbot.core.workspace import ( workspace_path_to_root, ) +_WORKSPACE_FILE_MAX_BYTES = 512 * 1024 + class ChatUIProjectServiceError(Exception): pass @@ -141,6 +144,207 @@ class ChatUIProjectService: ) -> list[dict]: return await self.get_project_sessions(username, project_id) + async def list_workspace_files( + self, + username: str, + project_id: str, + relative_path: str = "", + ) -> dict: + """List one directory inside an owned project's workspace. + + Args: + username: Dashboard username. + project_id: ChatUI project ID. + relative_path: Directory path relative to the workspace root. + + Returns: + Directory metadata and its direct child entries. + + Raises: + ChatUIProjectServiceError: If the path is invalid or unreadable. + """ + project = await self._get_owned_project(username, project_id) + fallback_umo = f"webchat:FriendMessage:webchat!{project.creator}!default" + workspace_root_path = os.path.normcase( + os.path.realpath( + resolve_project_workspace_root( + project, + fallback_umo=fallback_umo, + ) + ) + ) + workspace_root = Path(workspace_root_path) + raw_path = str(relative_path or "").strip() + normalized_path = Path(raw_path.replace("\\", "/") or ".") + if normalized_path.is_absolute() or ".." in normalized_path.parts: + raise ChatUIProjectServiceError("Invalid workspace path") + + target_dir_path = os.path.normcase( + os.path.realpath(os.path.join(workspace_root_path, normalized_path)) + ) + # Keep the separator to reject sibling paths with the same name prefix. + workspace_root_prefix = os.path.join(workspace_root_path, "") + if target_dir_path != workspace_root_path and not target_dir_path.startswith( + workspace_root_prefix + ): + raise ChatUIProjectServiceError("Workspace path escapes project directory") + target_dir = Path(target_dir_path) + if not workspace_root.exists() and normalized_path == Path("."): + return {"path": "", "entries": []} + if not target_dir.is_dir(): + raise ChatUIProjectServiceError("Workspace directory not found") + + try: + children = sorted( + target_dir.iterdir(), + key=lambda item: (not item.is_dir(), item.name.lower()), + ) + except OSError as exc: + raise ChatUIProjectServiceError( + "Workspace directory cannot be read" + ) from exc + + entries = [] + for entry in children: + if entry.is_symlink(): + continue + try: + if not entry.is_dir() and not entry.is_file(): + continue + stat = entry.stat() + except OSError: + continue + is_directory = entry.is_dir() + entries.append( + { + "name": entry.name, + "path": entry.relative_to(workspace_root).as_posix(), + "type": "directory" if is_directory else "file", + "size": 0 if is_directory else stat.st_size, + "readable": ( + not is_directory and stat.st_size <= _WORKSPACE_FILE_MAX_BYTES + ), + } + ) + + current_path = target_dir.relative_to(workspace_root).as_posix() + return { + "path": "" if current_path == "." else current_path, + "entries": entries, + } + + async def get_workspace_file( + self, + username: str, + project_id: str, + relative_path: str, + ) -> dict: + """Read a UTF-8 text file inside an owned project's workspace. + + Args: + username: Dashboard username. + project_id: ChatUI project ID. + relative_path: File path relative to the workspace root. + + Returns: + Relative path, UTF-8 content, and byte size. + + Raises: + ChatUIProjectServiceError: If the file is invalid or cannot be previewed. + """ + _, target_file = await self.get_workspace_file_location( + username, + project_id, + relative_path, + ) + + try: + with target_file.open("rb") as file: + content_bytes = file.read(_WORKSPACE_FILE_MAX_BYTES + 1) + except OSError as exc: + raise ChatUIProjectServiceError("Workspace file cannot be read") from exc + if len(content_bytes) > _WORKSPACE_FILE_MAX_BYTES: + raise ChatUIProjectServiceError("Workspace file is too large to preview") + try: + content = content_bytes.decode("utf-8") + except UnicodeDecodeError as exc: + raise ChatUIProjectServiceError( + "Workspace file is not valid UTF-8 text" + ) from exc + + return { + "path": relative_path, + "content": content, + "size": len(content_bytes), + } + + async def get_workspace_file_location( + self, + username: str, + project_id: str, + relative_path: str, + ) -> tuple[Path, Path]: + """Resolve a file inside an owned project's workspace. + + Args: + username: Dashboard username. + project_id: ChatUI project ID. + relative_path: File path relative to the workspace root. + + Returns: + Validated workspace root and absolute path to the workspace file. + + Raises: + ChatUIProjectServiceError: If the file path is invalid or missing. + """ + project = await self._get_owned_project(username, project_id) + fallback_umo = f"webchat:FriendMessage:webchat!{project.creator}!default" + workspace_root_path = os.path.normcase( + os.path.realpath( + resolve_project_workspace_root( + project, + fallback_umo=fallback_umo, + ) + ) + ) + raw_path = str(relative_path or "").strip() + normalized_path = Path(raw_path.replace("\\", "/")) + if ( + not raw_path + or normalized_path.is_absolute() + or ".." in normalized_path.parts + ): + raise ChatUIProjectServiceError("Invalid workspace path") + + # Match server-enumerated entries so request values never form a file path. + target_file = Path(workspace_root_path) + path_parts = normalized_path.parts + for index, part in enumerate(path_parts): + try: + children = {entry.name: entry for entry in target_file.iterdir()} + except OSError as exc: + raise ChatUIProjectServiceError( + "Workspace file cannot be read" + ) from exc + child = children.get(part) + if child is None: + raise ChatUIProjectServiceError("Workspace file not found") + if child.is_symlink(): + if not child.resolve(strict=False).is_relative_to( + Path(workspace_root_path) + ): + raise ChatUIProjectServiceError( + "Workspace path escapes project directory" + ) + raise ChatUIProjectServiceError("Workspace file not found") + if index < len(path_parts) - 1 and not child.is_dir(): + raise ChatUIProjectServiceError("Workspace file not found") + target_file = child + if not path_parts or not target_file.is_file(): + raise ChatUIProjectServiceError("Workspace file not found") + + return Path(workspace_root_path), target_file + async def _get_owned_project(self, username: str, project_id: str): project = await self.db.get_chatui_project_by_id(project_id) if not project: diff --git a/dashboard/src/api/generated/openapi-v1/sdk.gen.ts b/dashboard/src/api/generated/openapi-v1/sdk.gen.ts index 12c50903f..9f99fdfab 100644 --- a/dashboard/src/api/generated/openapi-v1/sdk.gen.ts +++ b/dashboard/src/api/generated/openapi-v1/sdk.gen.ts @@ -1,7 +1,7 @@ // This file is auto-generated by @hey-api/openapi-ts import { createClient, createConfig, type OptionsLegacyParser, formDataBodySerializer } from '@hey-api/client-axios'; -import type { LoginData, LoginError, LoginResponse, LogoutError, LogoutResponse, GetAuthSetupStatusError, GetAuthSetupStatusResponse, SetupAuthData, SetupAuthError, SetupAuthResponse, SetupTotpData, SetupTotpError, SetupTotpResponse, RecoverTotpError, RecoverTotpResponse, UpdateAuthAccountData, UpdateAuthAccountError, UpdateAuthAccountResponse, ListApiKeysError, ListApiKeysResponse, CreateApiKeyData, CreateApiKeyError, CreateApiKeyResponse, RevokeApiKeyData, RevokeApiKeyError, RevokeApiKeyResponse, DeleteApiKeyData, DeleteApiKeyError, DeleteApiKeyResponse, GetSystemConfigSchemaError, GetSystemConfigSchemaResponse, GetSystemConfigError, GetSystemConfigResponse, UpdateSystemConfigData, UpdateSystemConfigError, UpdateSystemConfigResponse, GetSystemConfigRuntimeError, GetSystemConfigRuntimeResponse, GetConfigProfileSchemaError, GetConfigProfileSchemaResponse, ListConfigProfilesError, ListConfigProfilesResponse, CreateConfigProfileData, CreateConfigProfileError, CreateConfigProfileResponse, GetConfigProfileData, GetConfigProfileError, GetConfigProfileResponse, UpdateConfigProfileContentData, UpdateConfigProfileContentError, UpdateConfigProfileContentResponse, RenameConfigProfileData, RenameConfigProfileError, RenameConfigProfileResponse, DeleteConfigProfileData, DeleteConfigProfileError, DeleteConfigProfileResponse, ListConfigRoutesError, ListConfigRoutesResponse, ReplaceConfigRoutesData, ReplaceConfigRoutesError, ReplaceConfigRoutesResponse, UpsertConfigRouteData, UpsertConfigRouteError, UpsertConfigRouteResponse, DeleteConfigRouteData, DeleteConfigRouteError, DeleteConfigRouteResponse, ListBotTypesError, ListBotTypesResponse, RegisterBotTypeData, RegisterBotTypeError, RegisterBotTypeResponse, ListBotsData, ListBotsError, ListBotsResponse, CreateBotData, CreateBotError, CreateBotResponse, ListBotStatsError, ListBotStatsResponse, GetBotByIdData, GetBotByIdError, GetBotByIdResponse, UpdateBotByIdData, UpdateBotByIdError, UpdateBotByIdResponse, DeleteBotByIdData, DeleteBotByIdError, DeleteBotByIdResponse, SetBotEnabledByIdData, SetBotEnabledByIdError, SetBotEnabledByIdResponse, TestBotByIdData, TestBotByIdError, TestBotByIdResponse, GetBotData, GetBotError, GetBotResponse, UpdateBotData, UpdateBotError, UpdateBotResponse, DeleteBotData, DeleteBotError, DeleteBotResponse, SetBotEnabledData, SetBotEnabledError, SetBotEnabledResponse, TestBotData, TestBotError, TestBotResponse, GetProviderSchemaError, GetProviderSchemaResponse, ListProviderSourcesError, ListProviderSourcesResponse, CreateProviderSourceData, CreateProviderSourceError, CreateProviderSourceResponse, GetProviderSourceByIdData, GetProviderSourceByIdError, GetProviderSourceByIdResponse, UpsertProviderSourceByIdData, UpsertProviderSourceByIdError, UpsertProviderSourceByIdResponse, DeleteProviderSourceByIdData, DeleteProviderSourceByIdError, DeleteProviderSourceByIdResponse, ListProviderSourceModelsByIdData, ListProviderSourceModelsByIdError, ListProviderSourceModelsByIdResponse, ListProvidersBySourceIdData, ListProvidersBySourceIdError, ListProvidersBySourceIdResponse, CreateProviderInSourceByIdData, CreateProviderInSourceByIdError, CreateProviderInSourceByIdResponse, GetProviderSourceData, GetProviderSourceError, GetProviderSourceResponse, UpsertProviderSourceData, UpsertProviderSourceError, UpsertProviderSourceResponse, DeleteProviderSourceData, DeleteProviderSourceError, DeleteProviderSourceResponse, ListProviderSourceModelsData, ListProviderSourceModelsError, ListProviderSourceModelsResponse, ListProvidersBySourceData, ListProvidersBySourceError, ListProvidersBySourceResponse, CreateProviderInSourceData, CreateProviderInSourceError, CreateProviderInSourceResponse, ListProvidersData, ListProvidersError, ListProvidersResponse, CreateProviderData, CreateProviderError, CreateProviderResponse, GetProviderByIdData, GetProviderByIdError, GetProviderByIdResponse, UpdateProviderByIdData, UpdateProviderByIdError, UpdateProviderByIdResponse, DeleteProviderByIdData, DeleteProviderByIdError, DeleteProviderByIdResponse, SetProviderEnabledByIdData, SetProviderEnabledByIdError, SetProviderEnabledByIdResponse, TestProviderByIdData, TestProviderByIdError, TestProviderByIdResponse, GetProviderEmbeddingDimensionByIdData, GetProviderEmbeddingDimensionByIdError, GetProviderEmbeddingDimensionByIdResponse, GetProviderData, GetProviderError, GetProviderResponse, UpdateProviderData, UpdateProviderError, UpdateProviderResponse, DeleteProviderData, DeleteProviderError, DeleteProviderResponse, SetProviderEnabledData, SetProviderEnabledError, SetProviderEnabledResponse, TestProviderData, TestProviderError, TestProviderResponse, GetProviderEmbeddingDimensionData, GetProviderEmbeddingDimensionError, GetProviderEmbeddingDimensionResponse, SendChatMessageData, SendChatMessageError, SendChatMessageResponse, OpenChatWebSocketData, OpenLiveChatWebSocketData, OpenUnifiedChatWebSocketData, ListChatSessionsData, ListChatSessionsError, ListChatSessionsResponse, CreateChatSessionData, CreateChatSessionError, CreateChatSessionResponse, BatchDeleteChatSessionsData, BatchDeleteChatSessionsError, BatchDeleteChatSessionsResponse, GetChatSessionData, GetChatSessionError, GetChatSessionResponse, UpdateChatSessionData, UpdateChatSessionError, UpdateChatSessionResponse, DeleteChatSessionData, DeleteChatSessionError, DeleteChatSessionResponse, StopChatSessionData, StopChatSessionError, StopChatSessionResponse, ResumeChatRunData, ResumeChatRunError, ResumeChatRunResponse, UpdateChatMessageData, UpdateChatMessageError, UpdateChatMessageResponse, RegenerateChatMessageData, RegenerateChatMessageError, RegenerateChatMessageResponse, ListChatConfigsError, ListChatConfigsResponse, CreateChatThreadData, CreateChatThreadError, CreateChatThreadResponse, GetChatThreadData, GetChatThreadError, GetChatThreadResponse, DeleteChatThreadData, DeleteChatThreadError, DeleteChatThreadResponse, SendChatThreadMessageData, SendChatThreadMessageError, SendChatThreadMessageResponse, ListChatProjectsError, ListChatProjectsResponse, CreateChatProjectData, CreateChatProjectError, CreateChatProjectResponse, GetChatProjectData, GetChatProjectError, GetChatProjectResponse, UpdateChatProjectData, UpdateChatProjectError, UpdateChatProjectResponse, DeleteChatProjectData, DeleteChatProjectError, DeleteChatProjectResponse, ListChatProjectSessionsData, ListChatProjectSessionsError, ListChatProjectSessionsResponse, AddChatProjectSessionData, AddChatProjectSessionError, AddChatProjectSessionResponse, RemoveChatProjectSessionData, RemoveChatProjectSessionError, RemoveChatProjectSessionResponse, SendImMessageData, SendImMessageError, SendImMessageResponse, ListImBotsError, ListImBotsResponse, UploadFileData, UploadFileError, UploadFileResponse, UploadOpenApiFileData, UploadOpenApiFileError, UploadOpenApiFileResponse, DownloadOpenApiFileData, DownloadOpenApiFileError, DownloadOpenApiFileResponse, GetFileByNameData, GetFileByNameError, GetFileByNameResponse, GetTokenFileData, GetTokenFileError, GetTokenFileResponse, GetAttachmentData, GetAttachmentError, GetAttachmentResponse, DeleteAttachmentData, DeleteAttachmentError, DeleteAttachmentResponse, DownloadAttachmentData, DownloadAttachmentError, DownloadAttachmentResponse, ListPluginsData, ListPluginsError, ListPluginsResponse, GetPluginByIdData, GetPluginByIdError, GetPluginByIdResponse, UninstallPluginByIdData, UninstallPluginByIdError, UninstallPluginByIdResponse, GetPluginConfigByIdData, GetPluginConfigByIdError, GetPluginConfigByIdResponse, UpdatePluginConfigByIdData, UpdatePluginConfigByIdError, UpdatePluginConfigByIdResponse, GetPluginConfigSchemaByIdData, GetPluginConfigSchemaByIdError, GetPluginConfigSchemaByIdResponse, ListPluginConfigFilesByIdData, ListPluginConfigFilesByIdError, ListPluginConfigFilesByIdResponse, UploadPluginConfigFilesByIdData, UploadPluginConfigFilesByIdError, UploadPluginConfigFilesByIdResponse, DeletePluginConfigFileByIdData, DeletePluginConfigFileByIdError, DeletePluginConfigFileByIdResponse, GetPluginReadmeByIdData, GetPluginReadmeByIdError, GetPluginReadmeByIdResponse, GetPluginChangelogByIdData, GetPluginChangelogByIdError, GetPluginChangelogByIdResponse, ReloadPluginByIdData, ReloadPluginByIdError, ReloadPluginByIdResponse, SetPluginEnabledByIdData, SetPluginEnabledByIdError, SetPluginEnabledByIdResponse, ListPluginPagesByIdData, ListPluginPagesByIdError, ListPluginPagesByIdResponse, GetPluginPageByIdData, GetPluginPageByIdError, GetPluginPageByIdResponse, GetPluginPageAssetByIdData, GetPluginPageAssetByIdError, GetPluginPageAssetByIdResponse, GetPluginData, GetPluginError, GetPluginResponse, UninstallPluginData, UninstallPluginError, UninstallPluginResponse, GetPluginConfigData, GetPluginConfigError, GetPluginConfigResponse, UpdatePluginConfigData, UpdatePluginConfigError, UpdatePluginConfigResponse, UpdatePluginLogLevelData, UpdatePluginLogLevelError, UpdatePluginLogLevelResponse, GetPluginConfigSchemaData, GetPluginConfigSchemaError, GetPluginConfigSchemaResponse, ListPluginConfigFilesData, ListPluginConfigFilesError, ListPluginConfigFilesResponse, UploadPluginConfigFilesData, UploadPluginConfigFilesError, UploadPluginConfigFilesResponse, DeletePluginConfigFileData, DeletePluginConfigFileError, DeletePluginConfigFileResponse, GetPluginReadmeData, GetPluginReadmeError, GetPluginReadmeResponse, GetPluginChangelogData, GetPluginChangelogError, GetPluginChangelogResponse, ReloadPluginData, ReloadPluginError, ReloadPluginResponse, BindPluginSourceData, BindPluginSourceError, BindPluginSourceResponse, SetPluginEnabledData, SetPluginEnabledError, SetPluginEnabledResponse, UpdatePluginData, UpdatePluginError, UpdatePluginResponse, UpdatePluginsData, UpdatePluginsError, UpdatePluginsResponse, CheckPluginVersionSupportData, CheckPluginVersionSupportError, CheckPluginVersionSupportResponse, ValidatePluginRepoData, ValidatePluginRepoError, ValidatePluginRepoResponse, ListFailedPluginsError, ListFailedPluginsResponse, UninstallFailedPluginData, UninstallFailedPluginError, UninstallFailedPluginResponse, ReloadFailedPluginData, ReloadFailedPluginError, ReloadFailedPluginResponse, InstallPluginFromGithubData, InstallPluginFromGithubError, InstallPluginFromGithubResponse, InstallPluginFromUrlData, InstallPluginFromUrlError, InstallPluginFromUrlResponse, InstallPluginFromUploadData, InstallPluginFromUploadError, InstallPluginFromUploadResponse, ListPluginMarketData, ListPluginMarketError, ListPluginMarketResponse, ListPluginMarketCategoriesError, ListPluginMarketCategoriesResponse, ListPluginSourcesError, ListPluginSourcesResponse, CreatePluginSourceData, CreatePluginSourceError, CreatePluginSourceResponse, ReplacePluginSourcesData, ReplacePluginSourcesError, ReplacePluginSourcesResponse, DeletePluginSourceData, DeletePluginSourceError, DeletePluginSourceResponse, DeletePluginSourceByIdData, DeletePluginSourceByIdError, DeletePluginSourceByIdResponse, ListPluginPagesData, ListPluginPagesError, ListPluginPagesResponse, GetPluginPageData, GetPluginPageError, GetPluginPageResponse, GetPluginPageAssetData, GetPluginPageAssetError, GetPluginPageAssetResponse, GetPluginPageBridgeSdkError, GetPluginPageBridgeSdkResponse, GetPluginExtensionRouteData, GetPluginExtensionRouteError, GetPluginExtensionRouteResponse, PostPluginExtensionRouteData, PostPluginExtensionRouteError, PostPluginExtensionRouteResponse, PutPluginExtensionRouteData, PutPluginExtensionRouteError, PutPluginExtensionRouteResponse, PatchPluginExtensionRouteData, PatchPluginExtensionRouteError, PatchPluginExtensionRouteResponse, DeletePluginExtensionRouteData, DeletePluginExtensionRouteError, DeletePluginExtensionRouteResponse, ListCommandsData, ListCommandsError, ListCommandsResponse, UpdateCommandData, UpdateCommandError, UpdateCommandResponse, ListCommandConflictsError, ListCommandConflictsResponse, ListToolsData, ListToolsError, ListToolsResponse, SetToolEnabledData, SetToolEnabledError, SetToolEnabledResponse, SetToolPermissionData, SetToolPermissionError, SetToolPermissionResponse, ListMcpServersError, ListMcpServersResponse, CreateMcpServerData, CreateMcpServerError, CreateMcpServerResponse, UpdateMcpServerByNameData, UpdateMcpServerByNameError, UpdateMcpServerByNameResponse, DeleteMcpServerByNameData, DeleteMcpServerByNameError, DeleteMcpServerByNameResponse, SetMcpServerEnabledByNameData, SetMcpServerEnabledByNameError, SetMcpServerEnabledByNameResponse, TestMcpServerByNameData, TestMcpServerByNameError, TestMcpServerByNameResponse, UpdateMcpServerData, UpdateMcpServerError, UpdateMcpServerResponse, DeleteMcpServerData, DeleteMcpServerError, DeleteMcpServerResponse, SetMcpServerEnabledData, SetMcpServerEnabledError, SetMcpServerEnabledResponse, TestMcpServerData, TestMcpServerError, TestMcpServerResponse, SyncModelScopeMcpServersData, SyncModelScopeMcpServersError, SyncModelScopeMcpServersResponse, ListSkillsData, ListSkillsError, ListSkillsResponse, UploadSkillData, UploadSkillError, UploadSkillResponse, UploadSkillsBatchData, UploadSkillsBatchError, UploadSkillsBatchResponse, UpdateSkillByNameData, UpdateSkillByNameError, UpdateSkillByNameResponse, DeleteSkillByNameData, DeleteSkillByNameError, DeleteSkillByNameResponse, DownloadSkillByNameData, DownloadSkillByNameError, DownloadSkillByNameResponse, ListSkillFilesByNameData, ListSkillFilesByNameError, ListSkillFilesByNameResponse, GetSkillFileByNameData, GetSkillFileByNameError, GetSkillFileByNameResponse, UpdateSkillFileByNameData, UpdateSkillFileByNameError, UpdateSkillFileByNameResponse, UpdateSkillData, UpdateSkillError, UpdateSkillResponse, DeleteSkillData, DeleteSkillError, DeleteSkillResponse, DownloadSkillData, DownloadSkillError, DownloadSkillResponse, ListSkillFilesData, ListSkillFilesError, ListSkillFilesResponse, GetSkillFileData, GetSkillFileError, GetSkillFileResponse, UpdateSkillFileData, UpdateSkillFileError, UpdateSkillFileResponse, ListNeoSkillCandidatesData, ListNeoSkillCandidatesError, ListNeoSkillCandidatesResponse, ListNeoSkillReleasesData, ListNeoSkillReleasesError, ListNeoSkillReleasesResponse, GetNeoSkillPayloadData, GetNeoSkillPayloadError, GetNeoSkillPayloadResponse, EvaluateNeoSkillCandidateData, EvaluateNeoSkillCandidateError, EvaluateNeoSkillCandidateResponse, PromoteNeoSkillCandidateData, PromoteNeoSkillCandidateError, PromoteNeoSkillCandidateResponse, RollbackNeoSkillReleaseData, RollbackNeoSkillReleaseError, RollbackNeoSkillReleaseResponse, SyncNeoSkillReleaseData, SyncNeoSkillReleaseError, SyncNeoSkillReleaseResponse, DeleteNeoSkillCandidateData, DeleteNeoSkillCandidateError, DeleteNeoSkillCandidateResponse, DeleteNeoSkillReleaseData, DeleteNeoSkillReleaseError, DeleteNeoSkillReleaseResponse, ListKnowledgeBasesData, ListKnowledgeBasesError, ListKnowledgeBasesResponse, CreateKnowledgeBaseData, CreateKnowledgeBaseError, CreateKnowledgeBaseResponse, GetKnowledgeBaseData, GetKnowledgeBaseError, GetKnowledgeBaseResponse, UpdateKnowledgeBaseData, UpdateKnowledgeBaseError, UpdateKnowledgeBaseResponse, DeleteKnowledgeBaseData, DeleteKnowledgeBaseError, DeleteKnowledgeBaseResponse, GetKnowledgeBaseStatsData, GetKnowledgeBaseStatsError, GetKnowledgeBaseStatsResponse, ListKnowledgeDocumentsData, ListKnowledgeDocumentsError, ListKnowledgeDocumentsResponse, UploadKnowledgeDocumentData, UploadKnowledgeDocumentError, UploadKnowledgeDocumentResponse, ImportKnowledgeDocumentsData, ImportKnowledgeDocumentsError, ImportKnowledgeDocumentsResponse, ImportKnowledgeDocumentFromUrlData, ImportKnowledgeDocumentFromUrlError, ImportKnowledgeDocumentFromUrlResponse, GetKnowledgeDocumentData, GetKnowledgeDocumentError, GetKnowledgeDocumentResponse, DeleteKnowledgeDocumentData, DeleteKnowledgeDocumentError, DeleteKnowledgeDocumentResponse, ListKnowledgeChunksData, ListKnowledgeChunksError, ListKnowledgeChunksResponse, DeleteKnowledgeChunkData, DeleteKnowledgeChunkError, DeleteKnowledgeChunkResponse, RetrieveKnowledgeBaseData, RetrieveKnowledgeBaseError, RetrieveKnowledgeBaseResponse, GetKnowledgeTaskData, GetKnowledgeTaskError, GetKnowledgeTaskResponse, GetPersonaTreeError, GetPersonaTreeResponse, ListPersonasData, ListPersonasError, ListPersonasResponse, CreatePersonaData, CreatePersonaError, CreatePersonaResponse, GetPersonaByIdData, GetPersonaByIdError, GetPersonaByIdResponse, UpdatePersonaByIdData, UpdatePersonaByIdError, UpdatePersonaByIdResponse, DeletePersonaByIdData, DeletePersonaByIdError, DeletePersonaByIdResponse, GetPersonaData, GetPersonaError, GetPersonaResponse, UpdatePersonaData, UpdatePersonaError, UpdatePersonaResponse, DeletePersonaData, DeletePersonaError, DeletePersonaResponse, ListPersonaFoldersData, ListPersonaFoldersError, ListPersonaFoldersResponse, CreatePersonaFolderData, CreatePersonaFolderError, CreatePersonaFolderResponse, UpdatePersonaFolderData, UpdatePersonaFolderError, UpdatePersonaFolderResponse, DeletePersonaFolderData, DeletePersonaFolderError, DeletePersonaFolderResponse, MovePersonaItemData, MovePersonaItemError, MovePersonaItemResponse, ReorderPersonaItemsData, ReorderPersonaItemsError, ReorderPersonaItemsResponse, ListSessionsData, ListSessionsError, ListSessionsResponse, ListActiveUmosError, ListActiveUmosResponse, ListSessionRulesData, ListSessionRulesError, ListSessionRulesResponse, UpsertSessionRuleData, UpsertSessionRuleError, UpsertSessionRuleResponse, DeleteSessionRulesData, DeleteSessionRulesError, DeleteSessionRulesResponse, BatchUpdateSessionProviderData, BatchUpdateSessionProviderError, BatchUpdateSessionProviderResponse, BatchUpdateSessionServiceData, BatchUpdateSessionServiceError, BatchUpdateSessionServiceResponse, ListSessionGroupsError, ListSessionGroupsResponse, CreateSessionGroupData, CreateSessionGroupError, CreateSessionGroupResponse, UpdateSessionGroupData, UpdateSessionGroupError, UpdateSessionGroupResponse, DeleteSessionGroupData, DeleteSessionGroupError, DeleteSessionGroupResponse, ListConversationsData, ListConversationsError, ListConversationsResponse, BatchDeleteConversationsData, BatchDeleteConversationsError, BatchDeleteConversationsResponse, GetConversationData, GetConversationError, GetConversationResponse, UpdateConversationData, UpdateConversationError, UpdateConversationResponse, DeleteConversationData, DeleteConversationError, DeleteConversationResponse, ReplaceConversationMessagesData, ReplaceConversationMessagesError, ReplaceConversationMessagesResponse, ExportConversationsData, ExportConversationsError, ExportConversationsResponse, GetStatsData, GetStatsError, GetStatsResponse, GetProviderTokenStatsData, GetProviderTokenStatsError, GetProviderTokenStatsResponse, GetVersionError, GetVersionResponse, GetPublicVersionsError, GetPublicVersionsResponse, GetFirstNoticeData, GetFirstNoticeError, GetFirstNoticeResponse, TestGhproxyConnectionData, TestGhproxyConnectionError, TestGhproxyConnectionResponse, ListChangelogVersionsError, ListChangelogVersionsResponse, GetChangelogData, GetChangelogError, GetChangelogResponse, GetStartTimeError, GetStartTimeResponse, GetStorageStatusError, GetStorageStatusResponse, CleanupStorageData, CleanupStorageError, CleanupStorageResponse, RestartCoreError, RestartCoreResponse, ListBackupsData, ListBackupsError, ListBackupsResponse, CreateBackupData, CreateBackupError, CreateBackupResponse, UploadBackupData, UploadBackupError, UploadBackupResponse, InitBackupUploadData, InitBackupUploadError, InitBackupUploadResponse, UploadBackupChunkData, UploadBackupChunkError, UploadBackupChunkResponse, CompleteBackupUploadData, CompleteBackupUploadError, CompleteBackupUploadResponse, AbortBackupUploadData, AbortBackupUploadError, AbortBackupUploadResponse, GetBackupProgressData, GetBackupProgressError, GetBackupProgressResponse, DownloadBackupData, DownloadBackupError, DownloadBackupResponse, RenameBackupData, RenameBackupError, RenameBackupResponse, DeleteBackupData, DeleteBackupError, DeleteBackupResponse, CheckBackupData, CheckBackupError, CheckBackupResponse, ImportBackupData, ImportBackupError, ImportBackupResponse, CheckUpdateError, CheckUpdateResponse, ListReleasesData, ListReleasesError, ListReleasesResponse, UpdateCoreData, UpdateCoreError, UpdateCoreResponse, UpdateDashboardData, UpdateDashboardError, UpdateDashboardResponse, GetUpdateProgressData, GetUpdateProgressError, GetUpdateProgressResponse, InstallPipPackageData, InstallPipPackageError, InstallPipPackageResponse, ListCronJobsData, ListCronJobsError, ListCronJobsResponse, CreateCronJobData, CreateCronJobError, CreateCronJobResponse, UpdateCronJobData, UpdateCronJobError, UpdateCronJobResponse, DeleteCronJobData, DeleteCronJobError, DeleteCronJobResponse, RunCronJobData, RunCronJobError, RunCronJobResponse, StreamLiveLogsError, StreamLiveLogsResponse, GetLogHistoryError, GetLogHistoryResponse, GetTraceSettingsError, GetTraceSettingsResponse, UpdateTraceSettingsData, UpdateTraceSettingsError, UpdateTraceSettingsResponse, ListT2iTemplatesError, ListT2iTemplatesResponse, CreateT2iTemplateData, CreateT2iTemplateError, CreateT2iTemplateResponse, GetActiveT2iTemplateError, GetActiveT2iTemplateResponse, SetActiveT2iTemplateData, SetActiveT2iTemplateError, SetActiveT2iTemplateResponse, ResetDefaultT2iTemplateError, ResetDefaultT2iTemplateResponse, GetT2iTemplateData, GetT2iTemplateError, GetT2iTemplateResponse, UpdateT2iTemplateData, UpdateT2iTemplateError, UpdateT2iTemplateResponse, DeleteT2iTemplateData, DeleteT2iTemplateError, DeleteT2iTemplateResponse, GetSubagentConfigError, GetSubagentConfigResponse, UpdateSubagentConfigData, UpdateSubagentConfigError, UpdateSubagentConfigResponse, ListSubagentAvailableToolsError, ListSubagentAvailableToolsResponse, VerifyPlatformWebhookData, VerifyPlatformWebhookError, VerifyPlatformWebhookResponse, ReceivePlatformWebhookData, ReceivePlatformWebhookError, ReceivePlatformWebhookResponse } from './types.gen'; +import type { LoginData, LoginError, LoginResponse, LogoutError, LogoutResponse, GetAuthSetupStatusError, GetAuthSetupStatusResponse, SetupAuthData, SetupAuthError, SetupAuthResponse, SetupTotpData, SetupTotpError, SetupTotpResponse, RecoverTotpError, RecoverTotpResponse, UpdateAuthAccountData, UpdateAuthAccountError, UpdateAuthAccountResponse, ListApiKeysError, ListApiKeysResponse, CreateApiKeyData, CreateApiKeyError, CreateApiKeyResponse, RevokeApiKeyData, RevokeApiKeyError, RevokeApiKeyResponse, DeleteApiKeyData, DeleteApiKeyError, DeleteApiKeyResponse, GetSystemConfigSchemaError, GetSystemConfigSchemaResponse, GetSystemConfigError, GetSystemConfigResponse, UpdateSystemConfigData, UpdateSystemConfigError, UpdateSystemConfigResponse, GetSystemConfigRuntimeError, GetSystemConfigRuntimeResponse, GetConfigProfileSchemaError, GetConfigProfileSchemaResponse, ListConfigProfilesError, ListConfigProfilesResponse, CreateConfigProfileData, CreateConfigProfileError, CreateConfigProfileResponse, GetConfigProfileData, GetConfigProfileError, GetConfigProfileResponse, UpdateConfigProfileContentData, UpdateConfigProfileContentError, UpdateConfigProfileContentResponse, RenameConfigProfileData, RenameConfigProfileError, RenameConfigProfileResponse, DeleteConfigProfileData, DeleteConfigProfileError, DeleteConfigProfileResponse, ListConfigRoutesError, ListConfigRoutesResponse, ReplaceConfigRoutesData, ReplaceConfigRoutesError, ReplaceConfigRoutesResponse, UpsertConfigRouteData, UpsertConfigRouteError, UpsertConfigRouteResponse, DeleteConfigRouteData, DeleteConfigRouteError, DeleteConfigRouteResponse, ListBotTypesError, ListBotTypesResponse, RegisterBotTypeData, RegisterBotTypeError, RegisterBotTypeResponse, ListBotsData, ListBotsError, ListBotsResponse, CreateBotData, CreateBotError, CreateBotResponse, ListBotStatsError, ListBotStatsResponse, GetBotByIdData, GetBotByIdError, GetBotByIdResponse, UpdateBotByIdData, UpdateBotByIdError, UpdateBotByIdResponse, DeleteBotByIdData, DeleteBotByIdError, DeleteBotByIdResponse, SetBotEnabledByIdData, SetBotEnabledByIdError, SetBotEnabledByIdResponse, TestBotByIdData, TestBotByIdError, TestBotByIdResponse, GetBotData, GetBotError, GetBotResponse, UpdateBotData, UpdateBotError, UpdateBotResponse, DeleteBotData, DeleteBotError, DeleteBotResponse, SetBotEnabledData, SetBotEnabledError, SetBotEnabledResponse, TestBotData, TestBotError, TestBotResponse, GetProviderSchemaError, GetProviderSchemaResponse, ListProviderSourcesError, ListProviderSourcesResponse, CreateProviderSourceData, CreateProviderSourceError, CreateProviderSourceResponse, GetProviderSourceByIdData, GetProviderSourceByIdError, GetProviderSourceByIdResponse, UpsertProviderSourceByIdData, UpsertProviderSourceByIdError, UpsertProviderSourceByIdResponse, DeleteProviderSourceByIdData, DeleteProviderSourceByIdError, DeleteProviderSourceByIdResponse, ListProviderSourceModelsByIdData, ListProviderSourceModelsByIdError, ListProviderSourceModelsByIdResponse, ListProvidersBySourceIdData, ListProvidersBySourceIdError, ListProvidersBySourceIdResponse, CreateProviderInSourceByIdData, CreateProviderInSourceByIdError, CreateProviderInSourceByIdResponse, GetProviderSourceData, GetProviderSourceError, GetProviderSourceResponse, UpsertProviderSourceData, UpsertProviderSourceError, UpsertProviderSourceResponse, DeleteProviderSourceData, DeleteProviderSourceError, DeleteProviderSourceResponse, ListProviderSourceModelsData, ListProviderSourceModelsError, ListProviderSourceModelsResponse, ListProvidersBySourceData, ListProvidersBySourceError, ListProvidersBySourceResponse, CreateProviderInSourceData, CreateProviderInSourceError, CreateProviderInSourceResponse, ListProvidersData, ListProvidersError, ListProvidersResponse, CreateProviderData, CreateProviderError, CreateProviderResponse, GetProviderByIdData, GetProviderByIdError, GetProviderByIdResponse, UpdateProviderByIdData, UpdateProviderByIdError, UpdateProviderByIdResponse, DeleteProviderByIdData, DeleteProviderByIdError, DeleteProviderByIdResponse, SetProviderEnabledByIdData, SetProviderEnabledByIdError, SetProviderEnabledByIdResponse, TestProviderByIdData, TestProviderByIdError, TestProviderByIdResponse, GetProviderEmbeddingDimensionByIdData, GetProviderEmbeddingDimensionByIdError, GetProviderEmbeddingDimensionByIdResponse, GetProviderData, GetProviderError, GetProviderResponse, UpdateProviderData, UpdateProviderError, UpdateProviderResponse, DeleteProviderData, DeleteProviderError, DeleteProviderResponse, SetProviderEnabledData, SetProviderEnabledError, SetProviderEnabledResponse, TestProviderData, TestProviderError, TestProviderResponse, GetProviderEmbeddingDimensionData, GetProviderEmbeddingDimensionError, GetProviderEmbeddingDimensionResponse, SendChatMessageData, SendChatMessageError, SendChatMessageResponse, OpenChatWebSocketData, OpenLiveChatWebSocketData, OpenUnifiedChatWebSocketData, ListChatSessionsData, ListChatSessionsError, ListChatSessionsResponse, CreateChatSessionData, CreateChatSessionError, CreateChatSessionResponse, BatchDeleteChatSessionsData, BatchDeleteChatSessionsError, BatchDeleteChatSessionsResponse, GetChatSessionData, GetChatSessionError, GetChatSessionResponse, UpdateChatSessionData, UpdateChatSessionError, UpdateChatSessionResponse, DeleteChatSessionData, DeleteChatSessionError, DeleteChatSessionResponse, StopChatSessionData, StopChatSessionError, StopChatSessionResponse, ResumeChatRunData, ResumeChatRunError, ResumeChatRunResponse, UpdateChatMessageData, UpdateChatMessageError, UpdateChatMessageResponse, RegenerateChatMessageData, RegenerateChatMessageError, RegenerateChatMessageResponse, ListChatConfigsError, ListChatConfigsResponse, CreateChatThreadData, CreateChatThreadError, CreateChatThreadResponse, GetChatThreadData, GetChatThreadError, GetChatThreadResponse, DeleteChatThreadData, DeleteChatThreadError, DeleteChatThreadResponse, SendChatThreadMessageData, SendChatThreadMessageError, SendChatThreadMessageResponse, ListChatProjectsError, ListChatProjectsResponse, CreateChatProjectData, CreateChatProjectError, CreateChatProjectResponse, GetChatProjectData, GetChatProjectError, GetChatProjectResponse, UpdateChatProjectData, UpdateChatProjectError, UpdateChatProjectResponse, DeleteChatProjectData, DeleteChatProjectError, DeleteChatProjectResponse, ListChatProjectSessionsData, ListChatProjectSessionsError, ListChatProjectSessionsResponse, ListChatProjectWorkspaceFilesData, ListChatProjectWorkspaceFilesError, ListChatProjectWorkspaceFilesResponse, GetChatProjectWorkspaceFileData, GetChatProjectWorkspaceFileError, GetChatProjectWorkspaceFileResponse, DownloadChatProjectWorkspaceFileData, DownloadChatProjectWorkspaceFileError, DownloadChatProjectWorkspaceFileResponse, AddChatProjectSessionData, AddChatProjectSessionError, AddChatProjectSessionResponse, RemoveChatProjectSessionData, RemoveChatProjectSessionError, RemoveChatProjectSessionResponse, SendImMessageData, SendImMessageError, SendImMessageResponse, ListImBotsError, ListImBotsResponse, UploadFileData, UploadFileError, UploadFileResponse, UploadOpenApiFileData, UploadOpenApiFileError, UploadOpenApiFileResponse, DownloadOpenApiFileData, DownloadOpenApiFileError, DownloadOpenApiFileResponse, GetFileByNameData, GetFileByNameError, GetFileByNameResponse, GetTokenFileData, GetTokenFileError, GetTokenFileResponse, GetAttachmentData, GetAttachmentError, GetAttachmentResponse, DeleteAttachmentData, DeleteAttachmentError, DeleteAttachmentResponse, DownloadAttachmentData, DownloadAttachmentError, DownloadAttachmentResponse, ListPluginsData, ListPluginsError, ListPluginsResponse, GetPluginByIdData, GetPluginByIdError, GetPluginByIdResponse, UninstallPluginByIdData, UninstallPluginByIdError, UninstallPluginByIdResponse, GetPluginConfigByIdData, GetPluginConfigByIdError, GetPluginConfigByIdResponse, UpdatePluginConfigByIdData, UpdatePluginConfigByIdError, UpdatePluginConfigByIdResponse, GetPluginConfigSchemaByIdData, GetPluginConfigSchemaByIdError, GetPluginConfigSchemaByIdResponse, ListPluginConfigFilesByIdData, ListPluginConfigFilesByIdError, ListPluginConfigFilesByIdResponse, UploadPluginConfigFilesByIdData, UploadPluginConfigFilesByIdError, UploadPluginConfigFilesByIdResponse, DeletePluginConfigFileByIdData, DeletePluginConfigFileByIdError, DeletePluginConfigFileByIdResponse, GetPluginReadmeByIdData, GetPluginReadmeByIdError, GetPluginReadmeByIdResponse, GetPluginChangelogByIdData, GetPluginChangelogByIdError, GetPluginChangelogByIdResponse, ReloadPluginByIdData, ReloadPluginByIdError, ReloadPluginByIdResponse, SetPluginEnabledByIdData, SetPluginEnabledByIdError, SetPluginEnabledByIdResponse, ListPluginPagesByIdData, ListPluginPagesByIdError, ListPluginPagesByIdResponse, GetPluginPageByIdData, GetPluginPageByIdError, GetPluginPageByIdResponse, GetPluginPageAssetByIdData, GetPluginPageAssetByIdError, GetPluginPageAssetByIdResponse, GetPluginData, GetPluginError, GetPluginResponse, UninstallPluginData, UninstallPluginError, UninstallPluginResponse, GetPluginConfigData, GetPluginConfigError, GetPluginConfigResponse, UpdatePluginConfigData, UpdatePluginConfigError, UpdatePluginConfigResponse, UpdatePluginLogLevelData, UpdatePluginLogLevelError, UpdatePluginLogLevelResponse, GetPluginConfigSchemaData, GetPluginConfigSchemaError, GetPluginConfigSchemaResponse, ListPluginConfigFilesData, ListPluginConfigFilesError, ListPluginConfigFilesResponse, UploadPluginConfigFilesData, UploadPluginConfigFilesError, UploadPluginConfigFilesResponse, DeletePluginConfigFileData, DeletePluginConfigFileError, DeletePluginConfigFileResponse, GetPluginReadmeData, GetPluginReadmeError, GetPluginReadmeResponse, GetPluginChangelogData, GetPluginChangelogError, GetPluginChangelogResponse, ReloadPluginData, ReloadPluginError, ReloadPluginResponse, BindPluginSourceData, BindPluginSourceError, BindPluginSourceResponse, SetPluginEnabledData, SetPluginEnabledError, SetPluginEnabledResponse, UpdatePluginData, UpdatePluginError, UpdatePluginResponse, UpdatePluginsData, UpdatePluginsError, UpdatePluginsResponse, CheckPluginVersionSupportData, CheckPluginVersionSupportError, CheckPluginVersionSupportResponse, ValidatePluginRepoData, ValidatePluginRepoError, ValidatePluginRepoResponse, ListFailedPluginsError, ListFailedPluginsResponse, UninstallFailedPluginData, UninstallFailedPluginError, UninstallFailedPluginResponse, ReloadFailedPluginData, ReloadFailedPluginError, ReloadFailedPluginResponse, InstallPluginFromGithubData, InstallPluginFromGithubError, InstallPluginFromGithubResponse, InstallPluginFromUrlData, InstallPluginFromUrlError, InstallPluginFromUrlResponse, InstallPluginFromUploadData, InstallPluginFromUploadError, InstallPluginFromUploadResponse, ListPluginMarketData, ListPluginMarketError, ListPluginMarketResponse, ListPluginMarketCategoriesError, ListPluginMarketCategoriesResponse, ListPluginSourcesError, ListPluginSourcesResponse, CreatePluginSourceData, CreatePluginSourceError, CreatePluginSourceResponse, ReplacePluginSourcesData, ReplacePluginSourcesError, ReplacePluginSourcesResponse, DeletePluginSourceData, DeletePluginSourceError, DeletePluginSourceResponse, DeletePluginSourceByIdData, DeletePluginSourceByIdError, DeletePluginSourceByIdResponse, ListPluginPagesData, ListPluginPagesError, ListPluginPagesResponse, GetPluginPageData, GetPluginPageError, GetPluginPageResponse, GetPluginPageAssetData, GetPluginPageAssetError, GetPluginPageAssetResponse, GetPluginPageBridgeSdkError, GetPluginPageBridgeSdkResponse, GetPluginExtensionRouteData, GetPluginExtensionRouteError, GetPluginExtensionRouteResponse, PostPluginExtensionRouteData, PostPluginExtensionRouteError, PostPluginExtensionRouteResponse, PutPluginExtensionRouteData, PutPluginExtensionRouteError, PutPluginExtensionRouteResponse, PatchPluginExtensionRouteData, PatchPluginExtensionRouteError, PatchPluginExtensionRouteResponse, DeletePluginExtensionRouteData, DeletePluginExtensionRouteError, DeletePluginExtensionRouteResponse, ListCommandsData, ListCommandsError, ListCommandsResponse, UpdateCommandData, UpdateCommandError, UpdateCommandResponse, ListCommandConflictsError, ListCommandConflictsResponse, ListToolsData, ListToolsError, ListToolsResponse, SetToolEnabledData, SetToolEnabledError, SetToolEnabledResponse, SetToolPermissionData, SetToolPermissionError, SetToolPermissionResponse, ListMcpServersError, ListMcpServersResponse, CreateMcpServerData, CreateMcpServerError, CreateMcpServerResponse, UpdateMcpServerByNameData, UpdateMcpServerByNameError, UpdateMcpServerByNameResponse, DeleteMcpServerByNameData, DeleteMcpServerByNameError, DeleteMcpServerByNameResponse, SetMcpServerEnabledByNameData, SetMcpServerEnabledByNameError, SetMcpServerEnabledByNameResponse, TestMcpServerByNameData, TestMcpServerByNameError, TestMcpServerByNameResponse, UpdateMcpServerData, UpdateMcpServerError, UpdateMcpServerResponse, DeleteMcpServerData, DeleteMcpServerError, DeleteMcpServerResponse, SetMcpServerEnabledData, SetMcpServerEnabledError, SetMcpServerEnabledResponse, TestMcpServerData, TestMcpServerError, TestMcpServerResponse, SyncModelScopeMcpServersData, SyncModelScopeMcpServersError, SyncModelScopeMcpServersResponse, ListSkillsData, ListSkillsError, ListSkillsResponse, UploadSkillData, UploadSkillError, UploadSkillResponse, UploadSkillsBatchData, UploadSkillsBatchError, UploadSkillsBatchResponse, UpdateSkillByNameData, UpdateSkillByNameError, UpdateSkillByNameResponse, DeleteSkillByNameData, DeleteSkillByNameError, DeleteSkillByNameResponse, DownloadSkillByNameData, DownloadSkillByNameError, DownloadSkillByNameResponse, ListSkillFilesByNameData, ListSkillFilesByNameError, ListSkillFilesByNameResponse, GetSkillFileByNameData, GetSkillFileByNameError, GetSkillFileByNameResponse, UpdateSkillFileByNameData, UpdateSkillFileByNameError, UpdateSkillFileByNameResponse, UpdateSkillData, UpdateSkillError, UpdateSkillResponse, DeleteSkillData, DeleteSkillError, DeleteSkillResponse, DownloadSkillData, DownloadSkillError, DownloadSkillResponse, ListSkillFilesData, ListSkillFilesError, ListSkillFilesResponse, GetSkillFileData, GetSkillFileError, GetSkillFileResponse, UpdateSkillFileData, UpdateSkillFileError, UpdateSkillFileResponse, ListNeoSkillCandidatesData, ListNeoSkillCandidatesError, ListNeoSkillCandidatesResponse, ListNeoSkillReleasesData, ListNeoSkillReleasesError, ListNeoSkillReleasesResponse, GetNeoSkillPayloadData, GetNeoSkillPayloadError, GetNeoSkillPayloadResponse, EvaluateNeoSkillCandidateData, EvaluateNeoSkillCandidateError, EvaluateNeoSkillCandidateResponse, PromoteNeoSkillCandidateData, PromoteNeoSkillCandidateError, PromoteNeoSkillCandidateResponse, RollbackNeoSkillReleaseData, RollbackNeoSkillReleaseError, RollbackNeoSkillReleaseResponse, SyncNeoSkillReleaseData, SyncNeoSkillReleaseError, SyncNeoSkillReleaseResponse, DeleteNeoSkillCandidateData, DeleteNeoSkillCandidateError, DeleteNeoSkillCandidateResponse, DeleteNeoSkillReleaseData, DeleteNeoSkillReleaseError, DeleteNeoSkillReleaseResponse, ListKnowledgeBasesData, ListKnowledgeBasesError, ListKnowledgeBasesResponse, CreateKnowledgeBaseData, CreateKnowledgeBaseError, CreateKnowledgeBaseResponse, GetKnowledgeBaseData, GetKnowledgeBaseError, GetKnowledgeBaseResponse, UpdateKnowledgeBaseData, UpdateKnowledgeBaseError, UpdateKnowledgeBaseResponse, DeleteKnowledgeBaseData, DeleteKnowledgeBaseError, DeleteKnowledgeBaseResponse, GetKnowledgeBaseStatsData, GetKnowledgeBaseStatsError, GetKnowledgeBaseStatsResponse, ListKnowledgeDocumentsData, ListKnowledgeDocumentsError, ListKnowledgeDocumentsResponse, UploadKnowledgeDocumentData, UploadKnowledgeDocumentError, UploadKnowledgeDocumentResponse, ImportKnowledgeDocumentsData, ImportKnowledgeDocumentsError, ImportKnowledgeDocumentsResponse, ImportKnowledgeDocumentFromUrlData, ImportKnowledgeDocumentFromUrlError, ImportKnowledgeDocumentFromUrlResponse, GetKnowledgeDocumentData, GetKnowledgeDocumentError, GetKnowledgeDocumentResponse, DeleteKnowledgeDocumentData, DeleteKnowledgeDocumentError, DeleteKnowledgeDocumentResponse, ListKnowledgeChunksData, ListKnowledgeChunksError, ListKnowledgeChunksResponse, DeleteKnowledgeChunkData, DeleteKnowledgeChunkError, DeleteKnowledgeChunkResponse, RetrieveKnowledgeBaseData, RetrieveKnowledgeBaseError, RetrieveKnowledgeBaseResponse, GetKnowledgeTaskData, GetKnowledgeTaskError, GetKnowledgeTaskResponse, GetPersonaTreeError, GetPersonaTreeResponse, ListPersonasData, ListPersonasError, ListPersonasResponse, CreatePersonaData, CreatePersonaError, CreatePersonaResponse, GetPersonaByIdData, GetPersonaByIdError, GetPersonaByIdResponse, UpdatePersonaByIdData, UpdatePersonaByIdError, UpdatePersonaByIdResponse, DeletePersonaByIdData, DeletePersonaByIdError, DeletePersonaByIdResponse, GetPersonaData, GetPersonaError, GetPersonaResponse, UpdatePersonaData, UpdatePersonaError, UpdatePersonaResponse, DeletePersonaData, DeletePersonaError, DeletePersonaResponse, ListPersonaFoldersData, ListPersonaFoldersError, ListPersonaFoldersResponse, CreatePersonaFolderData, CreatePersonaFolderError, CreatePersonaFolderResponse, UpdatePersonaFolderData, UpdatePersonaFolderError, UpdatePersonaFolderResponse, DeletePersonaFolderData, DeletePersonaFolderError, DeletePersonaFolderResponse, MovePersonaItemData, MovePersonaItemError, MovePersonaItemResponse, ReorderPersonaItemsData, ReorderPersonaItemsError, ReorderPersonaItemsResponse, ListSessionsData, ListSessionsError, ListSessionsResponse, ListActiveUmosError, ListActiveUmosResponse, ListSessionRulesData, ListSessionRulesError, ListSessionRulesResponse, UpsertSessionRuleData, UpsertSessionRuleError, UpsertSessionRuleResponse, DeleteSessionRulesData, DeleteSessionRulesError, DeleteSessionRulesResponse, BatchUpdateSessionProviderData, BatchUpdateSessionProviderError, BatchUpdateSessionProviderResponse, BatchUpdateSessionServiceData, BatchUpdateSessionServiceError, BatchUpdateSessionServiceResponse, ListSessionGroupsError, ListSessionGroupsResponse, CreateSessionGroupData, CreateSessionGroupError, CreateSessionGroupResponse, UpdateSessionGroupData, UpdateSessionGroupError, UpdateSessionGroupResponse, DeleteSessionGroupData, DeleteSessionGroupError, DeleteSessionGroupResponse, ListConversationsData, ListConversationsError, ListConversationsResponse, BatchDeleteConversationsData, BatchDeleteConversationsError, BatchDeleteConversationsResponse, GetConversationData, GetConversationError, GetConversationResponse, UpdateConversationData, UpdateConversationError, UpdateConversationResponse, DeleteConversationData, DeleteConversationError, DeleteConversationResponse, ReplaceConversationMessagesData, ReplaceConversationMessagesError, ReplaceConversationMessagesResponse, ExportConversationsData, ExportConversationsError, ExportConversationsResponse, GetStatsData, GetStatsError, GetStatsResponse, GetProviderTokenStatsData, GetProviderTokenStatsError, GetProviderTokenStatsResponse, GetVersionError, GetVersionResponse, GetPublicVersionsError, GetPublicVersionsResponse, GetFirstNoticeData, GetFirstNoticeError, GetFirstNoticeResponse, TestGhproxyConnectionData, TestGhproxyConnectionError, TestGhproxyConnectionResponse, ListChangelogVersionsError, ListChangelogVersionsResponse, GetChangelogData, GetChangelogError, GetChangelogResponse, GetStartTimeError, GetStartTimeResponse, GetStorageStatusError, GetStorageStatusResponse, CleanupStorageData, CleanupStorageError, CleanupStorageResponse, RestartCoreError, RestartCoreResponse, ListBackupsData, ListBackupsError, ListBackupsResponse, CreateBackupData, CreateBackupError, CreateBackupResponse, UploadBackupData, UploadBackupError, UploadBackupResponse, InitBackupUploadData, InitBackupUploadError, InitBackupUploadResponse, UploadBackupChunkData, UploadBackupChunkError, UploadBackupChunkResponse, CompleteBackupUploadData, CompleteBackupUploadError, CompleteBackupUploadResponse, AbortBackupUploadData, AbortBackupUploadError, AbortBackupUploadResponse, GetBackupProgressData, GetBackupProgressError, GetBackupProgressResponse, DownloadBackupData, DownloadBackupError, DownloadBackupResponse, RenameBackupData, RenameBackupError, RenameBackupResponse, DeleteBackupData, DeleteBackupError, DeleteBackupResponse, CheckBackupData, CheckBackupError, CheckBackupResponse, ImportBackupData, ImportBackupError, ImportBackupResponse, CheckUpdateError, CheckUpdateResponse, ListReleasesData, ListReleasesError, ListReleasesResponse, UpdateCoreData, UpdateCoreError, UpdateCoreResponse, UpdateDashboardData, UpdateDashboardError, UpdateDashboardResponse, GetUpdateProgressData, GetUpdateProgressError, GetUpdateProgressResponse, InstallPipPackageData, InstallPipPackageError, InstallPipPackageResponse, ListCronJobsData, ListCronJobsError, ListCronJobsResponse, CreateCronJobData, CreateCronJobError, CreateCronJobResponse, UpdateCronJobData, UpdateCronJobError, UpdateCronJobResponse, DeleteCronJobData, DeleteCronJobError, DeleteCronJobResponse, RunCronJobData, RunCronJobError, RunCronJobResponse, StreamLiveLogsError, StreamLiveLogsResponse, GetLogHistoryError, GetLogHistoryResponse, GetTraceSettingsError, GetTraceSettingsResponse, UpdateTraceSettingsData, UpdateTraceSettingsError, UpdateTraceSettingsResponse, ListT2iTemplatesError, ListT2iTemplatesResponse, CreateT2iTemplateData, CreateT2iTemplateError, CreateT2iTemplateResponse, GetActiveT2iTemplateError, GetActiveT2iTemplateResponse, SetActiveT2iTemplateData, SetActiveT2iTemplateError, SetActiveT2iTemplateResponse, ResetDefaultT2iTemplateError, ResetDefaultT2iTemplateResponse, GetT2iTemplateData, GetT2iTemplateError, GetT2iTemplateResponse, UpdateT2iTemplateData, UpdateT2iTemplateError, UpdateT2iTemplateResponse, DeleteT2iTemplateData, DeleteT2iTemplateError, DeleteT2iTemplateResponse, GetSubagentConfigError, GetSubagentConfigResponse, UpdateSubagentConfigData, UpdateSubagentConfigError, UpdateSubagentConfigResponse, ListSubagentAvailableToolsError, ListSubagentAvailableToolsResponse, VerifyPlatformWebhookData, VerifyPlatformWebhookError, VerifyPlatformWebhookResponse, ReceivePlatformWebhookData, ReceivePlatformWebhookError, ReceivePlatformWebhookResponse } from './types.gen'; export const client = createClient(createConfig()); @@ -955,6 +955,36 @@ export const listChatProjectSessions = (op }); }; +/** + * List files in a ChatUI project workspace directory + */ +export const listChatProjectWorkspaceFiles = (options: OptionsLegacyParser) => { + return (options?.client ?? client).get({ + ...options, + url: '/api/v1/chat/projects/{project_id}/workspace/files' + }); +}; + +/** + * Read a file in a ChatUI project workspace + */ +export const getChatProjectWorkspaceFile = (options: OptionsLegacyParser) => { + return (options?.client ?? client).get({ + ...options, + url: '/api/v1/chat/projects/{project_id}/workspace/file' + }); +}; + +/** + * Download a file from a ChatUI project workspace + */ +export const downloadChatProjectWorkspaceFile = (options: OptionsLegacyParser) => { + return (options?.client ?? client).get({ + ...options, + url: '/api/v1/chat/projects/{project_id}/workspace/file/download' + }); +}; + /** * Add a session to a ChatUI project */ diff --git a/dashboard/src/api/generated/openapi-v1/types.gen.ts b/dashboard/src/api/generated/openapi-v1/types.gen.ts index bd4999045..6083cb819 100644 --- a/dashboard/src/api/generated/openapi-v1/types.gen.ts +++ b/dashboard/src/api/generated/openapi-v1/types.gen.ts @@ -1534,6 +1534,45 @@ export type ListChatProjectSessionsResponse = (SuccessEnvelope); export type ListChatProjectSessionsError = unknown; +export type ListChatProjectWorkspaceFilesData = { + path: { + project_id: string; + }; + query?: { + path?: string; + }; +}; + +export type ListChatProjectWorkspaceFilesResponse = (SuccessEnvelope); + +export type ListChatProjectWorkspaceFilesError = unknown; + +export type GetChatProjectWorkspaceFileData = { + path: { + project_id: string; + }; + query: { + path: string; + }; +}; + +export type GetChatProjectWorkspaceFileResponse = (SuccessEnvelope); + +export type GetChatProjectWorkspaceFileError = unknown; + +export type DownloadChatProjectWorkspaceFileData = { + path: { + project_id: string; + }; + query: { + path: string; + }; +}; + +export type DownloadChatProjectWorkspaceFileResponse = ((Blob | File)); + +export type DownloadChatProjectWorkspaceFileError = unknown; + export type AddChatProjectSessionData = { path: { project_id: string; diff --git a/dashboard/src/api/v1.ts b/dashboard/src/api/v1.ts index 633a5fa11..0746eeb6d 100644 --- a/dashboard/src/api/v1.ts +++ b/dashboard/src/api/v1.ts @@ -924,6 +924,29 @@ export const chatApi = { openApiV1.listChatProjectSessions({ path: { project_id: projectId } }), ); }, + listProjectWorkspaceFiles(projectId: string, path = '') { + return typed( + openApiV1.listChatProjectWorkspaceFiles({ + path: { project_id: projectId }, + query: path ? { path } : undefined, + }), + ); + }, + getProjectWorkspaceFile(projectId: string, path: string) { + return typed( + openApiV1.getChatProjectWorkspaceFile({ + path: { project_id: projectId }, + query: { path }, + }), + ); + }, + downloadProjectWorkspaceFile(projectId: string, path: string) { + return openApiV1.downloadChatProjectWorkspaceFile({ + path: { project_id: projectId }, + query: { path }, + responseType: 'blob', + }) as Promise>; + }, addProjectSession(projectId: string, sessionId: string) { return typed( openApiV1.addChatProjectSession({ diff --git a/dashboard/src/components/chat/Chat.vue b/dashboard/src/components/chat/Chat.vue index 0810cc929..85202a732 100644 --- a/dashboard/src/components/chat/Chat.vue +++ b/dashboard/src/components/chat/Chat.vue @@ -532,6 +532,12 @@ :is-dark="isDark" /> + @@ -576,6 +582,7 @@ 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"; +import WorkspaceFilesPanel from "@/components/chat/WorkspaceFilesPanel.vue"; import RefsSidebar from "@/components/chat/message_list_comps/RefsSidebar.vue"; import { useSessions, type Session } from "@/composables/useSessions"; import { @@ -834,6 +841,14 @@ const selectedProject = computed( (project) => project.project_id === selectedProjectId.value, ) || null, ); +const activeProject = computed(() => { + if (isProviderWorkspace.value) return null; + if (selectedProject.value) return selectedProject.value; + const projectId = sessionProject.value?.project_id; + return ( + projects.value.find((project) => project.project_id === projectId) || null + ); +}); const isEmptyChat = computed( () => !isProviderWorkspace.value && @@ -923,13 +938,31 @@ function getSelectedProviderSelection() { provide("isDark", isDark); watch( - [chatHeaderTitle, chatHeaderSubtitle], - ([title, subtitle]) => { - chatHeader.SET_CONTEXT({ title, subtitle }); + [chatHeaderTitle, chatHeaderSubtitle, activeProject], + ([title, subtitle, project]) => { + chatHeader.SET_CONTEXT({ + title, + subtitle, + projectId: project?.project_id, + }); }, { immediate: true }, ); +watch( + () => chatHeader.workspaceFilesOpen, + (open) => { + if (!open) return; + threadSelection.visible = false; + threadPanelOpen.value = false; + activeThread.value = null; + reasoningPanelOpen.value = false; + activeReasoningTarget.value = null; + refsSidebarOpen.value = false; + selectedRefs.value = null; + }, +); + onMounted(async () => { loadingSessions.value = true; try { @@ -998,6 +1031,7 @@ function closeSecondaryPanels() { activeReasoningTarget.value = null; refsSidebarOpen.value = false; selectedRefs.value = null; + chatHeader.SET_WORKSPACE_FILES_OPEN(false); } function showChatWorkspace() { @@ -1537,6 +1571,7 @@ async function createThreadFromSelection() { } function openThreadPanel(thread: ChatThread) { + chatHeader.SET_WORKSPACE_FILES_OPEN(false); reasoningPanelOpen.value = false; activeReasoningTarget.value = null; refsSidebarOpen.value = false; @@ -1545,6 +1580,7 @@ function openThreadPanel(thread: ChatThread) { } function openRefsSidebar(refs: unknown) { + chatHeader.SET_WORKSPACE_FILES_OPEN(false); threadPanelOpen.value = false; activeThread.value = null; reasoningPanelOpen.value = false; @@ -1558,6 +1594,7 @@ function openReasoningPanel(payload: { message: ChatRecord; blockIndex: number; }) { + chatHeader.SET_WORKSPACE_FILES_OPEN(false); threadPanelOpen.value = false; activeThread.value = null; refsSidebarOpen.value = false; diff --git a/dashboard/src/components/chat/WorkspaceFilesPanel.vue b/dashboard/src/components/chat/WorkspaceFilesPanel.vue new file mode 100644 index 000000000..2bdf8f4e1 --- /dev/null +++ b/dashboard/src/components/chat/WorkspaceFilesPanel.vue @@ -0,0 +1,739 @@ + + + + + diff --git a/dashboard/src/i18n/locales/en-US/features/chat.json b/dashboard/src/i18n/locales/en-US/features/chat.json index 9f8401a13..2f6ca6fc9 100644 --- a/dashboard/src/i18n/locales/en-US/features/chat.json +++ b/dashboard/src/i18n/locales/en-US/features/chat.json @@ -144,6 +144,24 @@ "noProjects": "No projects", "confirmDelete": "Are you sure you want to delete project \"{title}\"? Conversations in this project will not be deleted." }, + "workspaceFiles": { + "title": "Workspace Files", + "open": "Open workspace files", + "close": "Close workspace files", + "refresh": "Refresh file tree", + "filter": "Filter files...", + "clearFilter": "Clear file filter", + "empty": "This workspace is empty", + "noMatches": "No matching files", + "loadFailed": "Failed to load workspace files", + "previewFailed": "Failed to read this file", + "tooLarge": "This file is too large to preview", + "download": "Download file", + "downloadFailed": "Failed to download this file", + "dialogPreview": "Open larger preview", + "closePreview": "Close file preview", + "closeDialogPreview": "Close larger preview" + }, "time": { "today": "Today", "yesterday": "Yesterday" diff --git a/dashboard/src/i18n/locales/ru-RU/features/chat.json b/dashboard/src/i18n/locales/ru-RU/features/chat.json index 7031cd48e..bfa925c2c 100644 --- a/dashboard/src/i18n/locales/ru-RU/features/chat.json +++ b/dashboard/src/i18n/locales/ru-RU/features/chat.json @@ -144,6 +144,24 @@ "noProjects": "Проектов пока нет", "confirmDelete": "Вы уверены, что хотите удалить проект «{title}»? Диалоги внутри проекта не будут удалены." }, + "workspaceFiles": { + "title": "Файлы рабочей области", + "open": "Открыть файлы рабочей области", + "close": "Закрыть файлы рабочей области", + "refresh": "Обновить дерево файлов", + "filter": "Фильтр файлов...", + "clearFilter": "Очистить фильтр файлов", + "empty": "Рабочая область пуста", + "noMatches": "Подходящие файлы не найдены", + "loadFailed": "Не удалось загрузить файлы рабочей области", + "previewFailed": "Не удалось прочитать файл", + "tooLarge": "Файл слишком большой для предпросмотра", + "download": "Скачать файл", + "downloadFailed": "Не удалось скачать файл", + "dialogPreview": "Открыть увеличенный просмотр", + "closePreview": "Закрыть предпросмотр файла", + "closeDialogPreview": "Закрыть увеличенный просмотр" + }, "time": { "today": "Сегодня", "yesterday": "Вчера" diff --git a/dashboard/src/i18n/locales/zh-CN/features/chat.json b/dashboard/src/i18n/locales/zh-CN/features/chat.json index 58a9264b8..69584fb21 100644 --- a/dashboard/src/i18n/locales/zh-CN/features/chat.json +++ b/dashboard/src/i18n/locales/zh-CN/features/chat.json @@ -144,6 +144,24 @@ "noProjects": "暂无项目", "confirmDelete": "确定要删除项目 \"{title}\" 吗?项目中的对话不会被删除。" }, + "workspaceFiles": { + "title": "工作区文件", + "open": "打开工作区文件", + "close": "关闭工作区文件", + "refresh": "刷新文件树", + "filter": "筛选文件...", + "clearFilter": "清除文件筛选", + "empty": "工作区暂无文件", + "noMatches": "没有匹配的文件", + "loadFailed": "加载工作区文件失败", + "previewFailed": "读取文件失败", + "tooLarge": "文件过大,无法预览", + "download": "下载文件", + "downloadFailed": "下载文件失败", + "dialogPreview": "放大预览", + "closePreview": "关闭文件预览", + "closeDialogPreview": "关闭放大预览" + }, "time": { "today": "今天", "yesterday": "昨天" diff --git a/dashboard/src/layouts/full/vertical-header/VerticalHeader.vue b/dashboard/src/layouts/full/vertical-header/VerticalHeader.vue index e5d89180f..d5a1020d1 100644 --- a/dashboard/src/layouts/full/vertical-header/VerticalHeader.vue +++ b/dashboard/src/layouts/full/vertical-header/VerticalHeader.vue @@ -10,7 +10,7 @@ import { MarkdownRender, enableKatex, enableMermaid } from "markstream-vue"; import "markstream-vue/index.css"; import "katex/dist/katex.min.css"; import "highlight.js/styles/github.css"; -import { useI18n } from "@/i18n/composables"; +import { useI18n, useModuleI18n } from "@/i18n/composables"; import { router } from "@/router"; import { useRoute } from "vue-router"; import { useDisplay, useTheme } from "vuetify"; @@ -31,6 +31,7 @@ const chatHeader = useChatHeaderStore(); const theme = useTheme(); const { lgAndUp } = useDisplay(); const { t } = useI18n(); +const { tm } = useModuleI18n("features/chat"); const route = useRoute(); const LAST_BOT_ROUTE_KEY = "astrbot:last_bot_route"; const LAST_CHAT_ROUTE_KEY = "astrbot:last_chat_route"; @@ -1141,6 +1142,28 @@ onMounted(async () => {
+ + + {{ + chatHeader.workspaceFilesOpen + ? "mdi-folder-open-outline" + : "mdi-folder-outline" + }} + + + { margin-right: 0; } +.workspace-files-trigger { + color: rgb(var(--v-theme-on-surface)); +} + +.workspace-files-trigger--active { + background: rgba(var(--v-theme-on-surface), 0.08) !important; +} + .mode-switch-btn { margin: 0; border: 0; diff --git a/dashboard/src/stores/chatHeader.ts b/dashboard/src/stores/chatHeader.ts index f3e0afbd2..79c10958e 100644 --- a/dashboard/src/stores/chatHeader.ts +++ b/dashboard/src/stores/chatHeader.ts @@ -4,16 +4,37 @@ export const useChatHeaderStore = defineStore("chatHeader", { state: () => ({ title: "", subtitle: "", + projectId: "", + workspaceFilesOpen: false, }), actions: { - SET_CONTEXT(payload: { title?: string; subtitle?: string }) { + SET_CONTEXT(payload: { + title?: string; + subtitle?: string; + projectId?: string; + }) { + const nextProjectId = payload.projectId || ""; + if (this.projectId !== nextProjectId) { + this.workspaceFilesOpen = false; + } this.title = payload.title || ""; this.subtitle = payload.subtitle || ""; + this.projectId = nextProjectId; + }, + TOGGLE_WORKSPACE_FILES() { + if (this.projectId) { + this.workspaceFilesOpen = !this.workspaceFilesOpen; + } + }, + SET_WORKSPACE_FILES_OPEN(open: boolean) { + this.workspaceFilesOpen = Boolean(open && this.projectId); }, CLEAR_CONTEXT() { this.title = ""; this.subtitle = ""; + this.projectId = ""; + this.workspaceFilesOpen = false; }, }, }); diff --git a/openspec/openapi-v1.yaml b/openspec/openapi-v1.yaml index 81755b990..a74c5faab 100644 --- a/openspec/openapi-v1.yaml +++ b/openspec/openapi-v1.yaml @@ -1497,6 +1497,75 @@ paths: "200": $ref: "#/components/responses/Ok" + /api/v1/chat/projects/{project_id}/workspace/files: + get: + tags: [Chat] + summary: List files in a ChatUI project workspace directory + operationId: listChatProjectWorkspaceFiles + x-astrbot-scope: chat + parameters: + - name: project_id + in: path + required: true + schema: + type: string + - name: path + in: query + required: false + schema: + type: string + default: "" + responses: + "200": + $ref: "#/components/responses/Ok" + + /api/v1/chat/projects/{project_id}/workspace/file: + get: + tags: [Chat] + summary: Read a file in a ChatUI project workspace + operationId: getChatProjectWorkspaceFile + x-astrbot-scope: chat + parameters: + - name: project_id + in: path + required: true + schema: + type: string + - name: path + in: query + required: true + schema: + type: string + responses: + "200": + $ref: "#/components/responses/Ok" + + /api/v1/chat/projects/{project_id}/workspace/file/download: + get: + tags: [Chat] + summary: Download a file from a ChatUI project workspace + operationId: downloadChatProjectWorkspaceFile + x-astrbot-scope: chat + parameters: + - name: project_id + in: path + required: true + schema: + type: string + - name: path + in: query + required: true + schema: + type: string + responses: + "200": + description: Workspace file content + content: + application/octet-stream: + schema: + type: string + format: binary + /api/v1/chat/projects/{project_id}/sessions/{session_id}: post: tags: [Chat] diff --git a/tests/unit/test_chatui_project_service.py b/tests/unit/test_chatui_project_service.py index 3aaacb5e3..8f6c73e7a 100644 --- a/tests/unit/test_chatui_project_service.py +++ b/tests/unit/test_chatui_project_service.py @@ -1,3 +1,6 @@ +from types import SimpleNamespace +from unittest.mock import AsyncMock + import pytest from astrbot.dashboard.services.chatui_project_service import ( @@ -60,9 +63,7 @@ def test_custom_workspace_rejects_file_path(tmp_path, monkeypatch): ) -def test_custom_workspace_relative_path_uses_astrbot_workspaces( - tmp_path, monkeypatch -): +def test_custom_workspace_relative_path_uses_astrbot_workspaces(tmp_path, monkeypatch): """Relative custom workspace paths should resolve under AstrBot workspaces.""" relative_workspace = tmp_path / "relative-workspace" relative_workspace.mkdir() @@ -140,3 +141,183 @@ def test_custom_workspace_accepts_absolute_path_outside_workspaces( assert workspace_type == "custom" assert workspace_path == str(outside_workspace) + + +@pytest.fixture +def workspace_service(tmp_path, monkeypatch): + """Create a project service backed by a temporary workspace. + + Args: + tmp_path: Temporary workspace root. + monkeypatch: Pytest monkeypatch fixture. + + Returns: + Project service configured with an owned project. + """ + project = SimpleNamespace( + project_id="project-1", + creator="alice", + workspace_type="custom", + workspace_path=str(tmp_path), + ) + db = SimpleNamespace(get_chatui_project_by_id=AsyncMock(return_value=project)) + monkeypatch.setattr( + "astrbot.dashboard.services.chatui_project_service.resolve_project_workspace_root", + lambda _project, *, fallback_umo: tmp_path, + ) + return ChatUIProjectService(db) + + +@pytest.mark.asyncio +async def test_list_workspace_files_is_sorted_and_idempotent( + tmp_path, + workspace_service, +): + """Workspace listing should be stable, read-only, and directory-first.""" + (tmp_path / "z-dir").mkdir() + (tmp_path / "a.txt").write_text("alpha", encoding="utf-8") + (tmp_path / "b.txt").write_text("beta", encoding="utf-8") + + first = await workspace_service.list_workspace_files("alice", "project-1") + second = await workspace_service.list_workspace_files("alice", "project-1") + + assert first == second + assert [entry["name"] for entry in first["entries"]] == [ + "z-dir", + "a.txt", + "b.txt", + ] + assert first["entries"][1]["readable"] is True + assert (tmp_path / "a.txt").read_text(encoding="utf-8") == "alpha" + + +@pytest.mark.asyncio +async def test_get_workspace_file_reads_utf8_text(tmp_path, workspace_service): + """Workspace file reads should return content without changing the file.""" + target = tmp_path / "notes.md" + target.write_text("你好,workspace", encoding="utf-8") + + result = await workspace_service.get_workspace_file( + "alice", + "project-1", + "notes.md", + ) + + assert result == { + "path": "notes.md", + "content": "你好,workspace", + "size": len("你好,workspace".encode()), + } + assert target.read_text(encoding="utf-8") == "你好,workspace" + + +@pytest.mark.asyncio +async def test_get_workspace_file_allows_nested_path(tmp_path, workspace_service): + """Workspace reads should preserve legitimate nested file access.""" + nested_dir = tmp_path / "docs" + nested_dir.mkdir() + target = nested_dir / "notes.md" + target.write_text("nested", encoding="utf-8") + + result = await workspace_service.get_workspace_file( + "alice", + "project-1", + "docs/notes.md", + ) + + assert result["path"] == "docs/notes.md" + assert result["content"] == "nested" + + +@pytest.mark.asyncio +async def test_get_workspace_file_location_supports_binary_download( + tmp_path, + workspace_service, +): + """Workspace downloads should resolve binary files without changing them.""" + target = tmp_path / "archive.bin" + target.write_bytes(b"\xff\xfe\x00") + + workspace_root, result = await workspace_service.get_workspace_file_location( + "alice", + "project-1", + "archive.bin", + ) + + assert workspace_root == tmp_path + assert result == target + assert result.read_bytes() == b"\xff\xfe\x00" + + +@pytest.mark.asyncio +async def test_workspace_paths_reject_traversal(workspace_service): + """Workspace APIs should reject relative paths that escape the project.""" + with pytest.raises(ChatUIProjectServiceError, match="Invalid workspace path"): + await workspace_service.list_workspace_files( + "alice", + "project-1", + "../outside", + ) + + with pytest.raises(ChatUIProjectServiceError, match="Invalid workspace path"): + await workspace_service.get_workspace_file( + "alice", + "project-1", + "../outside.txt", + ) + + +@pytest.mark.asyncio +async def test_get_workspace_file_rejects_binary_text(tmp_path, workspace_service): + """Workspace preview should reject files that are not valid UTF-8.""" + (tmp_path / "binary.dat").write_bytes(b"\xff\xfe\x00") + + with pytest.raises(ChatUIProjectServiceError, match="not valid UTF-8"): + await workspace_service.get_workspace_file( + "alice", + "project-1", + "binary.dat", + ) + + +@pytest.mark.asyncio +async def test_workspace_file_rejects_symlink_escape( + tmp_path, + workspace_service, +): + """Workspace reads should not follow a symlink outside the project root.""" + outside_file = tmp_path.parent / f"{tmp_path.name}-outside.txt" + outside_file.write_text("outside", encoding="utf-8") + (tmp_path / "outside-link.txt").symlink_to(outside_file) + + with pytest.raises( + ChatUIProjectServiceError, + match="escapes project directory", + ): + await workspace_service.get_workspace_file( + "alice", + "project-1", + "outside-link.txt", + ) + + +@pytest.mark.asyncio +async def test_workspace_file_rejects_symlink_directory_escape( + tmp_path, + workspace_service, +): + """Workspace reads should reject an escaping symlink in any path segment.""" + outside_dir = tmp_path.parent / f"{tmp_path.name}-outside-dir" + outside_dir.mkdir() + (outside_dir / "secret.txt").write_text("outside", encoding="utf-8") + (tmp_path / "outside-link").symlink_to(outside_dir, target_is_directory=True) + + with pytest.raises( + ChatUIProjectServiceError, + match="escapes project directory", + ): + await workspace_service.get_workspace_file( + "alice", + "project-1", + "outside-link/secret.txt", + )