# Files Architecture — Team Board **Версия:** 1.0 **Дата:** 2026-02-24 **Автор:** Subagent (Research & Design) --- ## 1. Обзор Team Board нуждается в файловой системе для двух типов файлов: 1. **Project Files** — файлы проекта (документация, код, ресурсы) 2. **Attachments** — файлы, прикреплённые к сообщениям и задачам **Ключевые принципы:** - Файлы хранятся на диске в настраиваемом workspace - Структура папок по проектам: `workspace//` - Файлы, добавленные вручную, автоматически индексируются - Агенты имеют полный доступ через MCP tools - Единый API для всех операций с файлами --- ## 2. Структура директорий ### 2.1 Общая структура workspace ``` /data/team-board-workspace/ ├── projects/ # Корневая папка проектов │ ├── / # Папка проекта (по slug) │ │ ├── files/ # Проектные файлы (ручное добавление) │ │ │ ├── docs/ # Документация │ │ │ ├── assets/ # Ресурсы (изображения, шрифты) │ │ │ ├── configs/ # Конфигурационные файлы │ │ │ └── misc/ # Прочие файлы │ │ ├── attachments/ # Файлы из сообщений/задач │ │ │ ├── messages/ # По ID сообщения │ │ │ │ ├── / │ │ │ │ │ ├── document.pdf │ │ │ │ │ └── screenshot.png │ │ │ └── tasks/ # По ID задачи │ │ │ └── / │ │ │ ├── requirements.md │ │ │ └── mockup.png │ │ └── .meta/ # Метаданные (индекс файлов) │ │ ├── files.index.json # Индекс проектных файлов │ │ └── sync.log # Лог синхронизации │ └── shared/ # Общие файлы (не привязаны к проекту) │ ├── avatars/ # Аватары пользователей │ ├── templates/ # Шаблоны документов │ └── tmp/ # Временные файлы ├── .global/ # Глобальные настройки │ ├── config.json # Конфигурация файловой системы │ ├── mime-types.json # Настройки типов файлов │ └── quotas.json # Квоты пользователей/проектов └── trash/ # Корзина (soft delete) └── / └── ``` ### 2.2 Пример для проекта "team-board" ``` /data/team-board-workspace/projects/team-board/ ├── files/ # Проектные файлы │ ├── docs/ │ │ ├── README.md # Добавлен вручную │ │ ├── API.md # Добавлен вручную │ │ └── diagrams/ │ │ └── architecture.png │ ├── configs/ │ │ ├── docker-compose.yml │ │ └── .env.example │ └── assets/ │ ├── logo.png │ └── icons/ ├── attachments/ # Из сообщений/задач │ ├── messages/ │ │ └── 550e8400-e29b-41d4-a716-446655440000/ │ │ ├── error-screenshot.png # Attachment к сообщению │ │ └── logs.txt │ └── tasks/ │ └── 550e8400-e29b-41d4-a716-446655440001/ │ └── technical-spec.pdf # Attachment к задаче └── .meta/ ├── files.index.json # Индекс проектных файлов └── sync.log ``` ### 2.3 Naming conventions **Проекты:** `` (латиница, дефисы) **Файлы:** Исходные имена сохраняются, конфликты решаются суффиксами **Папки attachments:** UUID сообщения/задачи **Временные файлы:** UUID + timestamp --- ## 3. Модели базы данных ### 3.1 Расширение существующей модели Attachment ```python # Существующая модель (для attachments) class Attachment(Base): """File attachment to message or task comment.""" __tablename__ = "attachments" message_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("messages.id"), nullable=False) filename: Mapped[str] = mapped_column(String(500), nullable=False) mime_type: Mapped[str | None] = mapped_column(String(100)) size: Mapped[int] = mapped_column(Integer, default=0) # bytes storage_path: Mapped[str] = mapped_column(String(1000), nullable=False) # относительный путь message: Mapped["Message"] = relationship(back_populates="attachments") ``` ### 3.2 Новая модель для проектных файлов ```python class ProjectFile(Base): """File in project files/ directory — manually added or uploaded via API.""" __tablename__ = "project_files" id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) project_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("projects.id"), nullable=False) # Файл filename: Mapped[str] = mapped_column(String(500), nullable=False) # исходное имя relative_path: Mapped[str] = mapped_column(String(1000), nullable=False) # относительно project/files/ mime_type: Mapped[str | None] = mapped_column(String(100)) size: Mapped[int] = mapped_column(Integer, default=0) # bytes checksum: Mapped[str | None] = mapped_column(String(64)) # SHA256 for deduplication # Метаданные source: Mapped[str] = mapped_column(String(20), default="manual") # manual | api | agent uploaded_by: Mapped[str | None] = mapped_column(String(255)) # member slug description: Mapped[str | None] = mapped_column(Text) tags: Mapped[list[str]] = mapped_column(ARRAY(String), default=list) # для категоризации # Флаги is_indexed: Mapped[bool] = mapped_column(Boolean, default=True) # виден в API is_searchable: Mapped[bool] = mapped_column(Boolean, default=True) # полнотекстовый поиск # Временные метки created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) indexed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True)) # когда последний раз сканировался # Связи project: Mapped["Project"] = relationship(back_populates="files") ``` ### 3.3 Обновление модели Project ```python class Project(Base): # ... существующие поля ... # Новая связь files: Mapped[list["ProjectFile"]] = relationship(back_populates="project", cascade="all, delete-orphan") ``` ### 3.4 Конфигурация файловой системы ```python class FileSystemConfig(Base): """Global file system configuration.""" __tablename__ = "fs_config" id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) # Пути workspace_path: Mapped[str] = mapped_column(String(1000), nullable=False) # /data/team-board-workspace # Лимиты max_file_size_mb: Mapped[int] = mapped_column(Integer, default=100) # 100MB max_files_per_project: Mapped[int] = mapped_column(Integer, default=1000) max_total_size_gb: Mapped[int] = mapped_column(Integer, default=10) # 10GB на проект # Разрешённые типы allowed_extensions: Mapped[list[str]] = mapped_column(ARRAY(String), default=list) # .pdf, .md, .png blocked_extensions: Mapped[list[str]] = mapped_column(ARRAY(String), default=list) # .exe, .bat # Автоматизация auto_scan_interval: Mapped[int] = mapped_column(Integer, default=300) # секунды (5 минут) enable_thumbnails: Mapped[bool] = mapped_column(Boolean, default=True) enable_text_extraction: Mapped[bool] = mapped_column(Boolean, default=True) # для поиска created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), onupdate=func.now()) ``` --- ## 4. REST API Endpoints ### 4.1 Project Files API ```python # GET /api/projects/{project_slug}/files async def list_project_files( project_slug: str, path: Optional[str] = Query(None), # фильтр по пути tags: Optional[List[str]] = Query(None), # фильтр по тегам mime_type: Optional[str] = Query(None), # фильтр по типу search: Optional[str] = Query(None), # поиск в содержимом limit: int = Query(50, le=200), offset: int = Query(0), db: AsyncSession = Depends(get_db) ) -> List[ProjectFileOut] # POST /api/projects/{project_slug}/files/upload async def upload_project_file( project_slug: str, file: UploadFile, path: Optional[str] = Form(None), # подпапка в files/ description: Optional[str] = Form(None), tags: Optional[str] = Form(None), # JSON array member_slug: str = Depends(get_current_member), db: AsyncSession = Depends(get_db) ) -> ProjectFileOut # GET /api/projects/{project_slug}/files/{file_id} async def get_project_file( project_slug: str, file_id: str, db: AsyncSession = Depends(get_db) ) -> ProjectFileOut # GET /api/projects/{project_slug}/files/{file_id}/download async def download_project_file( project_slug: str, file_id: str, db: AsyncSession = Depends(get_db) ) -> FileResponse # PUT /api/projects/{project_slug}/files/{file_id} async def update_project_file( project_slug: str, file_id: str, req: ProjectFileUpdate, db: AsyncSession = Depends(get_db) ) -> ProjectFileOut # DELETE /api/projects/{project_slug}/files/{file_id} async def delete_project_file( project_slug: str, file_id: str, db: AsyncSession = Depends(get_db) ) -> Dict[str, bool] # POST /api/projects/{project_slug}/files/scan async def scan_project_files( project_slug: str, db: AsyncSession = Depends(get_db) ) -> Dict[str, Any] # статистика сканирования ``` ### 4.2 Attachments API (расширение существующего) ```python # POST /api/messages/{message_id}/attachments async def upload_message_attachment( message_id: str, file: UploadFile, db: AsyncSession = Depends(get_db) ) -> AttachmentOut # GET /api/messages/{message_id}/attachments async def list_message_attachments( message_id: str, db: AsyncSession = Depends(get_db) ) -> List[AttachmentOut] # GET /api/attachments/{attachment_id}/download async def download_attachment( attachment_id: str, db: AsyncSession = Depends(get_db) ) -> FileResponse # DELETE /api/attachments/{attachment_id} async def delete_attachment( attachment_id: str, db: AsyncSession = Depends(get_db) ) -> Dict[str, bool] # POST /api/tasks/{task_id}/attachments async def upload_task_attachment( task_id: str, file: UploadFile, db: AsyncSession = Depends(get_db) ) -> AttachmentOut ``` ### 4.3 File System Management API ```python # GET /api/admin/files/config async def get_fs_config() -> FileSystemConfigOut # PUT /api/admin/files/config async def update_fs_config(req: FileSystemConfigUpdate) -> FileSystemConfigOut # POST /api/admin/files/scan-all async def scan_all_projects() -> Dict[str, Any] # глобальное сканирование # GET /api/admin/files/stats async def get_storage_stats() -> Dict[str, Any] # использование места, статистика # POST /api/admin/files/cleanup async def cleanup_orphaned_files() -> Dict[str, Any] # очистка сирых файлов ``` --- ## 5. Синхронизация файловой системы и БД ### 5.1 Проблема синхронизации **Вызов:** Файлы добавленные вручную в `files/` должны быть видны через API без перезапуска системы. **Решения:** 1. **inotify** (Linux) — мониторинг файловой системы в реальном времени 2. **Periodic scan** — сканирование каждые N минут 3. **On-demand scan** — сканирование при API запросе 4. **Гибридный подход** (рекомендуется) ### 5.2 Гибридная реализация ```python class FileSystemSyncService: def __init__(self, workspace_path: str, db: AsyncSession): self.workspace_path = workspace_path self.db = db self.observer = None # inotify observer async def start_monitoring(self): """Запуск inotify мониторинга для активных проектов.""" from watchdog.observers import Observer from watchdog.events import FileSystemEventHandler class ProjectFileHandler(FileSystemEventHandler): def __init__(self, sync_service): self.sync_service = sync_service def on_created(self, event): if not event.is_directory: asyncio.create_task( self.sync_service.handle_file_created(event.src_path) ) def on_deleted(self, event): if not event.is_directory: asyncio.create_task( self.sync_service.handle_file_deleted(event.src_path) ) def on_modified(self, event): if not event.is_directory: asyncio.create_task( self.sync_service.handle_file_modified(event.src_path) ) self.observer = Observer() handler = ProjectFileHandler(self) # Мониторим папки files/ всех активных проектов projects = await self.get_active_projects() for project in projects: files_path = f"{self.workspace_path}/projects/{project.slug}/files" if os.path.exists(files_path): self.observer.schedule(handler, files_path, recursive=True) self.observer.start() async def handle_file_created(self, file_path: str): """Обработка создания файла.""" try: project_slug, relative_path = self.extract_project_info(file_path) if not project_slug: return # Проверяем, не дублируется ли файл existing = await self.find_existing_file(project_slug, relative_path) if existing: return # Создаём запись в БД file_info = await self.analyze_file(file_path) project_file = ProjectFile( project_id=await self.get_project_id(project_slug), filename=os.path.basename(file_path), relative_path=relative_path, mime_type=file_info.mime_type, size=file_info.size, checksum=file_info.checksum, source="manual", is_indexed=True, indexed_at=datetime.now() ) self.db.add(project_file) await self.db.commit() # Уведомляем через WebSocket о новом файле await self.broadcast_file_event("file.created", project_slug, project_file) except Exception as e: logger.error(f"Error handling file creation {file_path}: {e}") async def periodic_scan(self, project_slug: str = None): """Периодическое сканирование (fallback для inotify).""" if project_slug: projects = [await self.get_project(project_slug)] else: projects = await self.get_active_projects() for project in projects: await self.scan_project_files(project) async def scan_project_files(self, project: Project): """Сканирование файлов проекта.""" files_path = f"{self.workspace_path}/projects/{project.slug}/files" if not os.path.exists(files_path): return # Сканируем файловую систему fs_files = {} for root, dirs, files in os.walk(files_path): for file in files: full_path = os.path.join(root, file) relative_path = os.path.relpath(full_path, files_path) stat = os.stat(full_path) fs_files[relative_path] = { 'size': stat.st_size, 'mtime': stat.st_mtime, 'full_path': full_path } # Получаем файлы из БД db_files = await self.get_project_files_dict(project.id) # Находим новые файлы for relative_path, file_info in fs_files.items(): if relative_path not in db_files: await self.create_db_file_record(project, relative_path, file_info) # Находим удалённые файлы for relative_path in db_files: if relative_path not in fs_files: await self.mark_file_as_deleted(db_files[relative_path]) # Находим изменённые файлы for relative_path in set(fs_files.keys()) & set(db_files.keys()): db_file = db_files[relative_path] fs_file = fs_files[relative_path] if fs_file['size'] != db_file.size: await self.update_file_record(db_file, fs_file) ``` ### 5.3 Стратегии по типам операций | Операция | Метод | Когда | |----------|-------|-------| | **Добавление файла** | inotify → немедленная индексация | Real-time | | **Удаление файла** | inotify → пометка как deleted | Real-time | | **Изменение файла** | inotify → обновление метаданных | Real-time | | **Fallback scan** | Periodic (каждые 5 минут) | Если inotify недоступен | | **Full rescan** | On-demand (API или cron) | При несоответствиях | | **Startup scan** | При запуске сервиса | Синхронизация после рестарта | ### 5.4 Индексация и метаданные ```python class FileIndexer: """Извлечение метаданных и индексация контента.""" async def analyze_file(self, file_path: str) -> FileAnalysis: """Полный анализ файла.""" mime_type = self.detect_mime_type(file_path) size = os.path.getsize(file_path) checksum = await self.calculate_checksum(file_path) # Извлекаем текст для поиска (если возможно) text_content = None if self.is_text_extractable(mime_type): text_content = await self.extract_text(file_path, mime_type) # Генерируем thumbnail для изображений thumbnail_path = None if self.is_image(mime_type): thumbnail_path = await self.generate_thumbnail(file_path) return FileAnalysis( mime_type=mime_type, size=size, checksum=checksum, text_content=text_content, thumbnail_path=thumbnail_path ) async def extract_text(self, file_path: str, mime_type: str) -> str: """Извлечение текста для полнотекстового поиска.""" if mime_type == 'application/pdf': return await self.extract_pdf_text(file_path) elif mime_type.startswith('text/'): return await self.read_text_file(file_path) elif mime_type in ['application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document']: return await self.extract_doc_text(file_path) # ... другие форматы return None ``` --- ## 6. Интеграция с агентами (MCP Tools) ### 6.1 File Tools для MCP ```python # В picogent/src/tools/files.ts export const listProjectFilesTool: ToolDefinition = { name: 'list_project_files', title: 'Список файлов проекта', description: 'Получить список всех файлов в проекте с фильтрацией', inputSchema: { type: 'object', properties: { project_slug: { type: 'string', description: 'Slug проекта' }, path: { type: 'string', description: 'Фильтр по пути (например, docs/)' }, search: { type: 'string', description: 'Поиск в содержимом файлов' }, tags: { type: 'array', items: { type: 'string' }, description: 'Фильтр по тегам' }, mime_type: { type: 'string', description: 'Фильтр по типу файла' } }, required: ['project_slug'] }, async handler(params, context) { const files = await context.trackerClient.listProjectFiles(params); return { content: [{ type: 'text', text: `Найдено файлов: ${files.length}\n${files.map(f => `- ${f.relative_path} (${f.size} bytes)`).join('\n')}` }], structuredContent: files }; } }; export const uploadProjectFileTool: ToolDefinition = { name: 'upload_project_file', title: 'Загрузить файл в проект', description: 'Загрузить файл в папку files/ проекта', inputSchema: { type: 'object', properties: { project_slug: { type: 'string', description: 'Slug проекта' }, filename: { type: 'string', description: 'Имя файла' }, content: { type: 'string', description: 'Содержимое файла (base64 для бинарных)' }, path: { type: 'string', description: 'Подпапка в files/ (например, docs/)' }, description: { type: 'string', description: 'Описание файла' }, tags: { type: 'array', items: { type: 'string' }, description: 'Теги для категоризации' } }, required: ['project_slug', 'filename', 'content'] }, async handler(params, context) { const result = await context.trackerClient.uploadProjectFile(params); return { content: [{ type: 'text', text: `✅ Файл ${result.filename} загружен в ${result.relative_path}` }], structuredContent: result }; } }; export const downloadProjectFileTool: ToolDefinition = { name: 'download_project_file', title: 'Скачать файл из проекта', description: 'Скачать содержимое файла проекта', inputSchema: { type: 'object', properties: { project_slug: { type: 'string', description: 'Slug проекта' }, file_id: { type: 'string', description: 'ID файла' } }, required: ['project_slug', 'file_id'] }, async handler(params, context) { const fileData = await context.trackerClient.downloadProjectFile(params.project_slug, params.file_id); return { content: [{ type: 'text', text: `📄 Файл ${fileData.filename} (${fileData.size} bytes):\n\n${fileData.content}` }], structuredContent: fileData }; } }; export const uploadAttachmentTool: ToolDefinition = { name: 'upload_attachment', title: 'Прикрепить файл к сообщению', description: 'Загрузить файл как attachment к сообщению или задаче', inputSchema: { type: 'object', properties: { message_id: { type: 'string', description: 'ID сообщения (для чата)' }, task_id: { type: 'string', description: 'ID задачи (для комментария)' }, filename: { type: 'string', description: 'Имя файла' }, content: { type: 'string', description: 'Содержимое файла (base64)' } }, required: ['filename', 'content'] }, async handler(params, context) { // Одно из полей должно быть заполнено if (!params.message_id && !params.task_id) { throw new Error('Необходимо указать message_id или task_id'); } const result = await context.trackerClient.uploadAttachment(params); return { content: [{ type: 'text', text: `📎 Файл ${result.filename} прикреплён` }], structuredContent: result }; } }; export const scanProjectFilesTool: ToolDefinition = { name: 'scan_project_files', title: 'Сканировать файлы проекта', description: 'Принудительно сканировать директорию проекта и синхронизировать с БД', inputSchema: { type: 'object', properties: { project_slug: { type: 'string', description: 'Slug проекта' } }, required: ['project_slug'] }, async handler(params, context) { const result = await context.trackerClient.scanProjectFiles(params.project_slug); return { content: [{ type: 'text', text: `🔄 Сканирование завершено:\n- Найдено: ${result.found}\n- Добавлено: ${result.added}\n- Обновлено: ${result.updated}\n- Удалено: ${result.deleted}` }], structuredContent: result }; } }; ``` ### 6.2 Типичные сценарии использования агентами **1. Изучение документации проекта:** ```typescript // Агент получил задачу и изучает контекст const projectFiles = await tools.list_project_files({ project_slug: "team-board", path: "docs/", search: "API" }); for (const file of projectFiles.filter(f => f.filename.endsWith('.md'))) { const content = await tools.download_project_file({ project_slug: "team-board", file_id: file.id }); // Анализируем содержимое... } ``` **2. Создание технической документации:** ```typescript // Агент создаёт техническую спецификацию по результатам работы await tools.upload_project_file({ project_slug: "team-board", filename: "FILES-ARCHITECTURE.md", content: btoa(documentContent), // base64 encoding path: "docs/", description: "Архитектура файловой системы", tags: ["documentation", "architecture"] }); ``` **3. Прикрепление файлов к отчётам:** ```typescript // Агент выполнил задачу и прикрепляет лог или скриншот await tools.upload_attachment({ task_id: "550e8400-e29b-41d4-a716-446655440001", filename: "test-results.log", content: btoa(testResults) }); ``` --- ## 7. Лимиты, квоты и безопасность ### 7.1 Ограничения по размеру и типам ```python class FileValidationService: def __init__(self, config: FileSystemConfig): self.config = config async def validate_upload(self, file: UploadFile, project: Project, member: Member) -> None: """Валидация загружаемого файла.""" # Проверка размера файла if file.size > self.config.max_file_size_mb * 1024 * 1024: raise HTTPException( status_code=413, detail=f"Файл слишком большой. Максимум: {self.config.max_file_size_mb}MB" ) # Проверка расширения extension = os.path.splitext(file.filename)[1].lower() if extension in self.config.blocked_extensions: raise HTTPException( status_code=400, detail=f"Тип файла {extension} запрещён" ) if (self.config.allowed_extensions and extension not in self.config.allowed_extensions): raise HTTPException( status_code=400, detail=f"Тип файла {extension} не разрешён" ) # Проверка квоты проекта current_usage = await self.get_project_storage_usage(project.id) max_size_bytes = self.config.max_total_size_gb * 1024 * 1024 * 1024 if current_usage + file.size > max_size_bytes: raise HTTPException( status_code=413, detail=f"Превышена квота проекта ({self.config.max_total_size_gb}GB)" ) # Проверка количества файлов file_count = await self.get_project_file_count(project.id) if file_count >= self.config.max_files_per_project: raise HTTPException( status_code=413, detail=f"Превышено максимальное количество файлов ({self.config.max_files_per_project})" ) async def scan_file_security(self, file_path: str) -> SecurityScanResult: """Базовое сканирование безопасности.""" result = SecurityScanResult(safe=True, warnings=[]) # Проверка на исполняемые файлы if self.is_executable(file_path): result.warnings.append("Файл может содержать исполняемый код") # Проверка размера (защита от zip-bomb) if os.path.getsize(file_path) > 500 * 1024 * 1024: # 500MB result.safe = False result.warnings.append("Файл слишком большой для обработки") # Проверка на подозрительные расширения в архивах if file_path.endswith(('.zip', '.tar', '.gz')): suspicious_files = await self.scan_archive_contents(file_path) if suspicious_files: result.warnings.extend(suspicious_files) return result ``` ### 7.2 Дефолтные лимиты | Ограничение | Значение | Настраиваемо | |-------------|----------|--------------| | **Размер файла** | 100MB | ✅ | | **Файлов на проект** | 1000 | ✅ | | **Общий размер проекта** | 10GB | ✅ | | **Разрешённые типы** | Список расширений | ✅ | | **Запрещённые типы** | .exe, .bat, .sh, .scr | ✅ | ### 7.3 Безопасность **Защита путей:** ```python def sanitize_file_path(user_path: str) -> str: """Очистка пути от path traversal атак.""" # Удаляем .. и другие опасные конструкции clean_path = os.path.normpath(user_path) clean_path = clean_path.replace('..', '').replace('~', '') # Убираем ведущие слеши clean_path = clean_path.lstrip('/') return clean_path ``` **Контроль доступа:** - Агенты видят только файлы проектов, к которым у них есть доступ - Attachments доступны только участникам чата/задачи - Администраторы имеют полный доступ --- ## 8. Превью изображений и обработка ### 8.1 Генерация превью ```python class ThumbnailService: def __init__(self, workspace_path: str): self.workspace_path = workspace_path self.thumbnail_sizes = [(150, 150), (300, 300)] # small, medium async def generate_thumbnails(self, file_path: str, file_id: str) -> List[str]: """Генерация превью для изображений.""" if not self.is_image_file(file_path): return [] thumbnails = [] try: from PIL import Image with Image.open(file_path) as image: # Конвертируем в RGB если нужно if image.mode in ('RGBA', 'LA', 'P'): image = image.convert('RGB') for size in self.thumbnail_sizes: thumbnail = image.copy() thumbnail.thumbnail(size, Image.Resampling.LANCZOS) # Путь для превью thumb_filename = f"{file_id}_{size[0]}x{size[1]}.jpg" thumb_path = os.path.join( self.workspace_path, "shared/thumbnails", thumb_filename ) # Создаём директорию если не существует os.makedirs(os.path.dirname(thumb_path), exist_ok=True) # Сохраняем превью thumbnail.save(thumb_path, 'JPEG', quality=85) thumbnails.append(f"shared/thumbnails/{thumb_filename}") except Exception as e: logger.error(f"Error generating thumbnails for {file_path}: {e}") return thumbnails def is_image_file(self, file_path: str) -> bool: """Проверка, является ли файл изображением.""" mime_type = mimetypes.guess_type(file_path)[0] return mime_type and mime_type.startswith('image/') ``` ### 8.2 Обновление модели для превью ```python class ProjectFile(Base): # ... существующие поля ... # Превью thumbnail_paths: Mapped[list[str]] = mapped_column(ARRAY(String), default=list) # пути к превью has_thumbnails: Mapped[bool] = mapped_column(Boolean, default=False) # Извлечённый текст для поиска extracted_text: Mapped[str | None] = mapped_column(Text) is_text_extracted: Mapped[bool] = mapped_column(Boolean, default=False) ``` --- ## 9. Ответы на исследовательские вопросы ### 1. Как организовать структуру директорий? **Решение:** Двухуровневая структура: - `workspace/projects//files/` — проектные файлы (ручное добавление) - `workspace/projects//attachments/` — файлы из сообщений/задач - Подпапки по типу контента: docs/, assets/, configs/ - Метаданные в `.meta/` для каждого проекта ### 2. Как синхронизировать файлы положенные руками с БД? **Решение:** Гибридный подход: - **inotify** для мониторинга файловой системы в реальном времени - **Periodic scan** (каждые 5 минут) как fallback - **On-demand scan** через API endpoint - **Startup scan** при запуске сервиса ### 3. Как привязывать attachments к сообщениям/задачам? **Решение:** Используем существующую модель `Attachment`: - Поле `message_id` для привязки к сообщению - Сообщения могут быть привязаны к чату (`chat_id`) или задаче (`task_id`) - Файлы хранятся в `attachments/messages//` или `attachments/tasks//` ### 4. Нужна ли БД таблица для файлов? **Решение:** Да, нужно две таблицы: - `project_files` — для файлов в папке `files/` (новая) - `attachments` — для attachments в сообщениях (существующая) - Обе содержат метаданные, пути, размеры, checksums - БД обеспечивает быстрый поиск, фильтрацию, авторизацию ### 5. Как агенты будут скачивать/загружать файлы через MCP tools? **Решение:** 6 MCP tools для файлов: - `list_project_files` — список с фильтрацией - `upload_project_file` — загрузка в files/ - `download_project_file` — скачивание с декодированием - `upload_attachment` — прикрепление к сообщению/задаче - `scan_project_files` — принудительная синхронизация - Base64 encoding для передачи бинарных данных ### 6. Лимиты: размер файла, типы, квоты? **Решение:** Настраиваемые лимиты через `FileSystemConfig`: - Размер файла: 100MB (по умолчанию) - Файлов на проект: 1000 - Общий размер проекта: 10GB - Разрешённые/запрещённые расширения - Валидация и security scanning при загрузке ### 7. Превью для изображений? **Решение:** Автоматическая генерация превью: - При загрузке изображений создаются thumbnails (150x150, 300x300) - Хранятся в `shared/thumbnails/` - Пути сохраняются в `thumbnail_paths` поле - Доступны через отдельный API endpoint --- ## 10. Примеры использования ### 10.1 Пользователь добавляет файл вручную ```bash # Пользователь кладёт файл cp ~/documents/api-spec.md /data/team-board-workspace/projects/team-board/files/docs/ # Система автоматически (через inotify): # 1. Обнаруживает новый файл # 2. Анализирует (MIME, размер, checksum) # 3. Создаёт запись в project_files # 4. Извлекает текст для поиска # 5. Уведомляет через WebSocket # Агент получает уведомление и может сразу прочитать файл await tools.list_project_files({ project_slug: "team-board", search: "API" }); ``` ### 10.2 Агент загружает результат работы ```typescript // Агент создал диаграмму архитектуры const diagram = await generateArchitectureDiagram(); await tools.upload_project_file({ project_slug: "team-board", filename: "architecture-diagram.png", content: btoa(diagram), // base64 path: "docs/diagrams/", description: "Диаграмма архитектуры файловой системы", tags: ["diagram", "architecture"] }); // Прикрепляем к задаче как attachment await tools.upload_attachment({ task_id: taskId, filename: "architecture-diagram.png", content: btoa(diagram) }); ``` ### 10.3 Поиск и анализ файлов проекта ```typescript // Агент ищет все документы по API const apiDocs = await tools.list_project_files({ project_slug: "team-board", search: "API endpoint", path: "docs/" }); // Читает содержимое для анализа for (const doc of apiDocs) { const content = await tools.download_project_file({ project_slug: "team-board", file_id: doc.id }); // Анализирует API endpoints const endpoints = parseApiDocumentation(content.content); // ... } ``` --- ## Заключение Предложенная архитектура файловой системы для Team Board обеспечивает: ✅ **Простоту использования** — файлы можно добавлять вручную и через API ✅ **Автоматическую синхронизацию** — inotify + periodic scan ✅ **Богатый API** — REST endpoints + MCP tools для агентов ✅ **Безопасность** — валидация, квоты, контроль доступа ✅ **Масштабируемость** — индексация, поиск, превью ✅ **Интеграцию** — единая модель для attachments и project files **Ключевые преимущества:** - Агенты получают полный доступ к файлам проекта через 6 MCP tools - Пользователи могут добавлять файлы вручную без перезапуска - Автоматическая индексация и поиск по содержимому - Превью изображений и метаданные - Настраиваемые лимиты и безопасность Архитектура готова к реализации и может быть развёрнута поэтапно, начиная с базового функционала и постепенно добавляя продвинутые возможности.