- Project files structure: workspace/<project-slug>/files/ and /attachments/ - Database models: ProjectFile + extended Attachment - Hybrid sync: inotify + periodic scanning for manual file additions - MCP tools integration: 6 tools for agent file operations - Security: validation, quotas, file type restrictions - Features: thumbnails, text extraction, full-text search - REST API endpoints for all file operations - Detailed implementation plan and examples
42 KiB
Files Architecture — Team Board
Версия: 1.0
Дата: 2026-02-24
Автор: Subagent (Research & Design)
1. Обзор
Team Board нуждается в файловой системе для двух типов файлов:
- Project Files — файлы проекта (документация, код, ресурсы)
- Attachments — файлы, прикреплённые к сообщениям и задачам
Ключевые принципы:
- Файлы хранятся на диске в настраиваемом workspace
- Структура папок по проектам:
workspace/<project-slug>/ - Файлы, добавленные вручную, автоматически индексируются
- Агенты имеют полный доступ через MCP tools
- Единый API для всех операций с файлами
2. Структура директорий
2.1 Общая структура workspace
/data/team-board-workspace/
├── projects/ # Корневая папка проектов
│ ├── <project-slug>/ # Папка проекта (по slug)
│ │ ├── files/ # Проектные файлы (ручное добавление)
│ │ │ ├── docs/ # Документация
│ │ │ ├── assets/ # Ресурсы (изображения, шрифты)
│ │ │ ├── configs/ # Конфигурационные файлы
│ │ │ └── misc/ # Прочие файлы
│ │ ├── attachments/ # Файлы из сообщений/задач
│ │ │ ├── messages/ # По ID сообщения
│ │ │ │ ├── <message-id>/
│ │ │ │ │ ├── document.pdf
│ │ │ │ │ └── screenshot.png
│ │ │ └── tasks/ # По ID задачи
│ │ │ └── <task-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)
└── <date>/
└── <original-path>
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
Проекты: <project-slug> (латиница, дефисы)
Файлы: Исходные имена сохраняются, конфликты решаются суффиксами
Папки attachments: UUID сообщения/задачи
Временные файлы: UUID + timestamp
3. Модели базы данных
3.1 Расширение существующей модели Attachment
# Существующая модель (для 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 Новая модель для проектных файлов
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
class Project(Base):
# ... существующие поля ...
# Новая связь
files: Mapped[list["ProjectFile"]] = relationship(back_populates="project", cascade="all, delete-orphan")
3.4 Конфигурация файловой системы
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
# 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 (расширение существующего)
# 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
# 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 без перезапуска системы.
Решения:
- inotify (Linux) — мониторинг файловой системы в реальном времени
- Periodic scan — сканирование каждые N минут
- On-demand scan — сканирование при API запросе
- Гибридный подход (рекомендуется)
5.2 Гибридная реализация
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 Индексация и метаданные
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
# В 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. Изучение документации проекта:
// Агент получил задачу и изучает контекст
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. Создание технической документации:
// Агент создаёт техническую спецификацию по результатам работы
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. Прикрепление файлов к отчётам:
// Агент выполнил задачу и прикрепляет лог или скриншот
await tools.upload_attachment({
task_id: "550e8400-e29b-41d4-a716-446655440001",
filename: "test-results.log",
content: btoa(testResults)
});
7. Лимиты, квоты и безопасность
7.1 Ограничения по размеру и типам
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 Безопасность
Защита путей:
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 Генерация превью
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 Обновление модели для превью
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/<project-slug>/files/— проектные файлы (ручное добавление)workspace/projects/<project-slug>/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/<message-id>/илиattachments/tasks/<task-id>/
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 Пользователь добавляет файл вручную
# Пользователь кладёт файл
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 Агент загружает результат работы
// Агент создал диаграмму архитектуры
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 Поиск и анализ файлов проекта
// Агент ищет все документы по 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
- Пользователи могут добавлять файлы вручную без перезапуска
- Автоматическая индексация и поиск по содержимому
- Превью изображений и метаданные
- Настраиваемые лимиты и безопасность
Архитектура готова к реализации и может быть развёрнута поэтапно, начиная с базового функционала и постепенно добавляя продвинутые возможности.