271 lines
9.7 KiB
Python
271 lines
9.7 KiB
Python
"""
|
|
项目文件业务服务
|
|
"""
|
|
from pathlib import Path
|
|
from typing import Optional
|
|
import asyncio
|
|
|
|
from fastapi import HTTPException, Request
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.core.enums import OperationType
|
|
from app.models.project import Project
|
|
from app.models.user import User
|
|
from app.services.log_service import log_service
|
|
from app.services.notification_service import notification_service
|
|
from app.services.search_service import search_service
|
|
from app.services.storage import storage_service
|
|
from app.services.zvec_service import zvec_service
|
|
|
|
|
|
class ProjectFileService:
|
|
"""收口项目文件写入、副作用联动和通知流程。"""
|
|
|
|
@staticmethod
|
|
def _actor_name(user: User) -> str:
|
|
return user.nickname or user.username
|
|
|
|
@staticmethod
|
|
def _source_suffix(source: str) -> str:
|
|
return " 通过 MCP" if source == "mcp" else ""
|
|
|
|
@staticmethod
|
|
def _detail_with_source(detail: Optional[dict], source: str) -> Optional[dict]:
|
|
merged = dict(detail or {})
|
|
if source != "http":
|
|
merged["source"] = source
|
|
return merged or None
|
|
|
|
@staticmethod
|
|
async def _update_markdown_index(project_id: int, path: str, content: str) -> None:
|
|
if path.endswith(".md"):
|
|
await search_service.update_doc(project_id, path, Path(path).stem, content)
|
|
|
|
@staticmethod
|
|
async def _remove_markdown_index(project_id: int, path: str) -> None:
|
|
if path.endswith(".md"):
|
|
await search_service.remove_doc(project_id, path)
|
|
|
|
@staticmethod
|
|
async def _sync_markdown_index_for_move(
|
|
project_id: int,
|
|
old_path: str,
|
|
new_path: str,
|
|
new_file_path: Path,
|
|
) -> None:
|
|
if not old_path.endswith(".md") or not new_path.endswith(".md"):
|
|
return
|
|
|
|
try:
|
|
content = await storage_service.read_file(new_file_path)
|
|
await search_service.remove_doc(project_id, old_path)
|
|
await search_service.update_doc(project_id, new_path, Path(new_path).stem, content)
|
|
except Exception:
|
|
pass
|
|
|
|
async def save_file(
|
|
self,
|
|
db: AsyncSession,
|
|
project_id: int,
|
|
project: Project,
|
|
path: str,
|
|
content: str,
|
|
user: User,
|
|
*,
|
|
request: Optional[Request] = None,
|
|
source: str = "http",
|
|
) -> str:
|
|
file_path = storage_service.get_secure_path(project.storage_key, path)
|
|
await storage_service.write_file(file_path, content)
|
|
await self._update_markdown_index(project_id, path, content)
|
|
|
|
await log_service.log_file_operation(
|
|
db=db,
|
|
operation_type=OperationType.SAVE_FILE,
|
|
project_id=project_id,
|
|
file_path=path,
|
|
user=user,
|
|
detail=self._detail_with_source({"content_length": len(content)}, source),
|
|
request=request,
|
|
)
|
|
|
|
await notification_service.notify_project_members(
|
|
db=db,
|
|
project_id=project_id,
|
|
exclude_user_id=user.id,
|
|
title="项目文档更新",
|
|
content=(
|
|
f"项目 [{project.name}] 中的文档 [{path}] "
|
|
f"已被 {self._actor_name(user)}{self._source_suffix(source)} 更新。"
|
|
),
|
|
link=f"/projects/{project_id}/docs?file={path}",
|
|
category="project",
|
|
)
|
|
|
|
if path.endswith(".md"):
|
|
asyncio.create_task(zvec_service.vectorize_markdown(db, project_id, path, content))
|
|
|
|
await db.commit()
|
|
return "文件保存成功" if source == "http" else "文件更新成功"
|
|
|
|
async def operate_file(
|
|
self,
|
|
db: AsyncSession,
|
|
project_id: int,
|
|
project: Project,
|
|
action: str,
|
|
path: str,
|
|
user: User,
|
|
*,
|
|
new_path: Optional[str] = None,
|
|
content: Optional[str] = None,
|
|
request: Optional[Request] = None,
|
|
source: str = "http",
|
|
) -> str:
|
|
current_path = storage_service.get_secure_path(project.storage_key, path)
|
|
|
|
if action == "delete":
|
|
await storage_service.delete_file(current_path)
|
|
await self._remove_markdown_index(project_id, path)
|
|
|
|
if path.endswith(".md"):
|
|
await zvec_service.delete_vectors(db, project_id, path)
|
|
|
|
await log_service.log_file_operation(
|
|
db=db,
|
|
operation_type=OperationType.DELETE_FILE,
|
|
project_id=project_id,
|
|
file_path=path,
|
|
user=user,
|
|
detail=self._detail_with_source(None, source),
|
|
request=request,
|
|
)
|
|
|
|
await notification_service.notify_project_members(
|
|
db=db,
|
|
project_id=project_id,
|
|
exclude_user_id=user.id,
|
|
title="项目文档删除",
|
|
content=(
|
|
f"项目 [{project.name}] 中的文档 [{path}] "
|
|
f"已被 {self._actor_name(user)}{self._source_suffix(source)} 删除。"
|
|
),
|
|
category="project",
|
|
)
|
|
await db.commit()
|
|
return "删除成功" if source == "http" else "文件删除成功"
|
|
|
|
if action in {"rename", "move"}:
|
|
if not new_path:
|
|
detail = "缺少新路径参数" if action == "rename" else "缺少目标路径参数"
|
|
raise HTTPException(status_code=400, detail=detail)
|
|
|
|
destination_path = storage_service.get_secure_path(project.storage_key, new_path)
|
|
await storage_service.rename_file(current_path, destination_path)
|
|
await self._sync_markdown_index_for_move(project_id, path, new_path, destination_path)
|
|
|
|
operation_type = (
|
|
OperationType.RENAME_FILE if action == "rename" else OperationType.MOVE_FILE
|
|
)
|
|
notification_title = "项目文档重命名" if action == "rename" else "项目文档移动"
|
|
notification_action = "重命名为" if action == "rename" else "移动到"
|
|
success_message = "重命名成功" if action == "rename" else "移动成功"
|
|
mcp_message = "文件重命名成功" if action == "rename" else "文件移动成功"
|
|
|
|
await log_service.log_file_operation(
|
|
db=db,
|
|
operation_type=operation_type,
|
|
project_id=project_id,
|
|
file_path=path,
|
|
user=user,
|
|
detail=self._detail_with_source({"new_path": new_path}, source),
|
|
request=request,
|
|
)
|
|
|
|
await notification_service.notify_project_members(
|
|
db=db,
|
|
project_id=project_id,
|
|
exclude_user_id=user.id,
|
|
title=notification_title,
|
|
content=(
|
|
f"项目 [{project.name}] 中的文档 [{path}] "
|
|
f"已被 {self._actor_name(user)}{self._source_suffix(source)} {notification_action} [{new_path}]。"
|
|
),
|
|
category="project",
|
|
)
|
|
|
|
if path.endswith(".md") and new_path.endswith(".md"):
|
|
asyncio.create_task(zvec_service.sync_vector_move(db, project_id, path, new_path))
|
|
|
|
await db.commit()
|
|
return success_message if source == "http" else mcp_message
|
|
|
|
if action == "create_dir":
|
|
await storage_service.create_directory(current_path)
|
|
|
|
await log_service.log_file_operation(
|
|
db=db,
|
|
operation_type=OperationType.CREATE_DIR,
|
|
project_id=project_id,
|
|
file_path=path,
|
|
user=user,
|
|
detail=self._detail_with_source(None, source),
|
|
request=request,
|
|
)
|
|
|
|
await notification_service.notify_project_members(
|
|
db=db,
|
|
project_id=project_id,
|
|
exclude_user_id=user.id,
|
|
title="创建新目录",
|
|
content=(
|
|
f"{self._actor_name(user)}{self._source_suffix(source)} "
|
|
f"在项目 [{project.name}] 中创建了新目录 [{path}]。"
|
|
),
|
|
category="project",
|
|
)
|
|
await db.commit()
|
|
return "目录创建成功"
|
|
|
|
if action == "create_file":
|
|
file_content = content or ""
|
|
await storage_service.write_file(current_path, file_content)
|
|
await self._update_markdown_index(project_id, path, file_content)
|
|
|
|
await log_service.log_file_operation(
|
|
db=db,
|
|
operation_type=OperationType.CREATE_FILE,
|
|
project_id=project_id,
|
|
file_path=path,
|
|
user=user,
|
|
detail=self._detail_with_source(
|
|
{"content_length": len(file_content)},
|
|
source,
|
|
),
|
|
request=request,
|
|
)
|
|
|
|
await notification_service.notify_project_members(
|
|
db=db,
|
|
project_id=project_id,
|
|
exclude_user_id=user.id,
|
|
title="创建新文档",
|
|
content=(
|
|
f"{self._actor_name(user)}{self._source_suffix(source)} "
|
|
f"在项目 [{project.name}] 中创建了新文档 [{path}]。"
|
|
),
|
|
link=f"/projects/{project_id}/docs?file={path}",
|
|
category="project",
|
|
)
|
|
|
|
if path.endswith(".md"):
|
|
asyncio.create_task(zvec_service.vectorize_markdown(db, project_id, path, file_content))
|
|
|
|
await db.commit()
|
|
return "文件创建成功"
|
|
|
|
raise HTTPException(status_code=400, detail="不支持的操作类型")
|
|
|
|
|
|
project_file_service = ProjectFileService()
|