refactor: Filesystem & Upload 代码重构
- filesystem_service 简化文件元数据处理 - viewer_filesystem_service 移除冗余逻辑 - 新增 upload_utils 上传工具模块 - filesystem_router 移除废弃端点 - 前端 viewer_filesystem.js 接口重构,新增 upload_limits.js 上传限制 - file_preview、file_utils 更新预览与工具函数
This commit is contained in:
parent
d2859ab8cd
commit
42521bab63
@ -6,8 +6,9 @@ from fastapi import HTTPException
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from yuxi.agents.backends import create_agent_composite_backend
|
||||
from yuxi.agents.backends.sandbox.backend import _looks_like_binary
|
||||
from yuxi.agents.buildin import agent_manager
|
||||
from yuxi.agents.context import BaseContext, normalize_agent_context_config, prepare_agent_runtime_context
|
||||
from yuxi.repositories.agent_config_repository import AgentConfigRepository
|
||||
from yuxi.repositories.agent_repository import AgentRepository
|
||||
from yuxi.repositories.conversation_repository import ConversationRepository
|
||||
from yuxi.services.conversation_service import require_user_conversation
|
||||
from yuxi.storage.postgres.models_business import User
|
||||
@ -17,29 +18,23 @@ async def _resolve_filesystem_context(
|
||||
*,
|
||||
db: AsyncSession,
|
||||
user: User,
|
||||
agent_id: str,
|
||||
agent_config_id: int | None,
|
||||
bound_agent_id: str,
|
||||
) -> BaseContext:
|
||||
context = BaseContext(thread_id="", uid=str(user.uid))
|
||||
repo = AgentConfigRepository(db)
|
||||
agent_item = await AgentRepository(db).get_visible_by_slug(slug=bound_agent_id, user=user)
|
||||
if not agent_item:
|
||||
raise HTTPException(status_code=404, detail="智能体不存在")
|
||||
|
||||
config_item = None
|
||||
if agent_config_id is not None:
|
||||
config_item = await repo.get_by_id(config_id=int(agent_config_id))
|
||||
if config_item is not None and (config_item.uid != str(user.uid) or config_item.agent_id != agent_id):
|
||||
config_item = None
|
||||
|
||||
if config_item is None:
|
||||
config_item = await repo.get_or_create_default(
|
||||
uid=str(user.uid),
|
||||
agent_id=agent_id,
|
||||
created_by=str(user.uid),
|
||||
)
|
||||
backend = agent_manager.get_agent(agent_item.backend_id)
|
||||
if not backend:
|
||||
raise HTTPException(status_code=404, detail="智能体后端不存在")
|
||||
|
||||
context_schema = backend.context_schema
|
||||
context = context_schema(thread_id="", uid=str(user.uid))
|
||||
normalized_config = await normalize_agent_context_config(
|
||||
(config_item.config_json or {}).get("context", {}),
|
||||
(agent_item.config_json or {}).get("context", {}),
|
||||
db=db,
|
||||
user=user,
|
||||
context_schema=context_schema,
|
||||
)
|
||||
context.update_from_dict(normalized_config)
|
||||
return context
|
||||
@ -50,8 +45,6 @@ async def _resolve_filesystem_state(
|
||||
thread_id: str,
|
||||
user: User,
|
||||
db: AsyncSession,
|
||||
agent_id: str | None,
|
||||
agent_config_id: int | None,
|
||||
):
|
||||
conv_repo = ConversationRepository(db)
|
||||
conversation = await require_user_conversation(conv_repo, thread_id, str(user.uid))
|
||||
@ -59,8 +52,7 @@ async def _resolve_filesystem_state(
|
||||
runtime_context = await _resolve_filesystem_context(
|
||||
db=db,
|
||||
user=user,
|
||||
agent_id=agent_id or conversation.agent_id,
|
||||
agent_config_id=agent_config_id,
|
||||
bound_agent_id=conversation.agent_id,
|
||||
)
|
||||
runtime_context.thread_id = thread_id
|
||||
runtime_context.uid = str(user.uid)
|
||||
@ -73,8 +65,6 @@ async def list_filesystem_entries_view(
|
||||
*,
|
||||
thread_id: str,
|
||||
path: str,
|
||||
agent_id: str | None,
|
||||
agent_config_id: int | None,
|
||||
current_user: User,
|
||||
db: AsyncSession,
|
||||
) -> dict:
|
||||
@ -87,8 +77,6 @@ async def list_filesystem_entries_view(
|
||||
thread_id=thread_id,
|
||||
user=current_user,
|
||||
db=db,
|
||||
agent_id=agent_id,
|
||||
agent_config_id=agent_config_id,
|
||||
)
|
||||
|
||||
runtime_stub = type("RuntimeStub", (), {"context": runtime_context})()
|
||||
@ -107,8 +95,6 @@ async def read_file_content_view(
|
||||
*,
|
||||
thread_id: str,
|
||||
path: str,
|
||||
agent_id: str | None,
|
||||
agent_config_id: int | None,
|
||||
current_user: User,
|
||||
db: AsyncSession,
|
||||
) -> dict:
|
||||
@ -123,8 +109,6 @@ async def read_file_content_view(
|
||||
thread_id=thread_id,
|
||||
user=current_user,
|
||||
db=db,
|
||||
agent_id=agent_id,
|
||||
agent_config_id=agent_config_id,
|
||||
)
|
||||
|
||||
runtime_stub = type("RuntimeStub", (), {"context": runtime_context})()
|
||||
|
||||
@ -1,8 +1,11 @@
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
|
||||
import aiofiles
|
||||
from fastapi import UploadFile
|
||||
|
||||
from yuxi.storage.minio import aupload_file_to_minio
|
||||
|
||||
|
||||
async def write_upload_to_buffer(
|
||||
upload: UploadFile,
|
||||
@ -24,6 +27,26 @@ async def write_upload_to_buffer(
|
||||
return written
|
||||
|
||||
|
||||
async def read_upload_with_limit(
|
||||
upload: UploadFile,
|
||||
*,
|
||||
max_size_bytes: int,
|
||||
too_large_message: str,
|
||||
chunk_size: int = 1024 * 1024,
|
||||
) -> bytes:
|
||||
await upload.seek(0)
|
||||
written = 0
|
||||
chunks: list[bytes] = []
|
||||
|
||||
while chunk := await upload.read(chunk_size):
|
||||
written += len(chunk)
|
||||
if written > max_size_bytes:
|
||||
raise ValueError(too_large_message)
|
||||
chunks.append(chunk)
|
||||
|
||||
return b"".join(chunks)
|
||||
|
||||
|
||||
async def write_upload_to_path(
|
||||
upload: UploadFile,
|
||||
dest: Path,
|
||||
@ -41,3 +64,23 @@ async def write_upload_to_path(
|
||||
too_large_message=too_large_message,
|
||||
chunk_size=chunk_size,
|
||||
)
|
||||
|
||||
|
||||
async def upload_image_to_minio(
|
||||
upload: UploadFile,
|
||||
*,
|
||||
object_prefix: str,
|
||||
max_size_bytes: int,
|
||||
too_large_message: str,
|
||||
) -> str:
|
||||
if not upload.content_type or not upload.content_type.startswith("image/"):
|
||||
raise ValueError("只能上传图片文件")
|
||||
|
||||
file_content = await read_upload_with_limit(
|
||||
upload,
|
||||
max_size_bytes=max_size_bytes,
|
||||
too_large_message=too_large_message,
|
||||
)
|
||||
file_extension = upload.filename.rsplit(".", 1)[-1].lower() if upload.filename and "." in upload.filename else "jpg"
|
||||
object_name = f"{object_prefix.strip('/')}/{uuid.uuid4()}.{file_extension}"
|
||||
return await aupload_file_to_minio("public", object_name, file_content)
|
||||
|
||||
@ -334,8 +334,6 @@ def _list_user_data_root_entries(thread_id: str, uid: str) -> list[dict]:
|
||||
async def _resolve_viewer_state(
|
||||
*,
|
||||
thread_id: str,
|
||||
agent_id: str | None,
|
||||
agent_config_id: int | None,
|
||||
current_user: User,
|
||||
db: AsyncSession,
|
||||
):
|
||||
@ -343,8 +341,6 @@ async def _resolve_viewer_state(
|
||||
thread_id=thread_id,
|
||||
user=current_user,
|
||||
db=db,
|
||||
agent_id=agent_id,
|
||||
agent_config_id=agent_config_id,
|
||||
)
|
||||
selected_skills = getattr(runtime_context, "_readable_skills", [])
|
||||
selected_skills = normalize_string_list(selected_skills if isinstance(selected_skills, list) else [])
|
||||
@ -358,8 +354,6 @@ async def list_viewer_filesystem_tree(
|
||||
*,
|
||||
thread_id: str,
|
||||
path: str,
|
||||
agent_id: str | None,
|
||||
agent_config_id: int | None,
|
||||
current_user: User,
|
||||
db: AsyncSession,
|
||||
) -> dict:
|
||||
@ -369,8 +363,6 @@ async def list_viewer_filesystem_tree(
|
||||
normalized_path = _normalize_path(path)
|
||||
sandbox_backend, skills_backend, selected_skills = await _resolve_viewer_state(
|
||||
thread_id=thread_id,
|
||||
agent_id=agent_id,
|
||||
agent_config_id=agent_config_id,
|
||||
current_user=current_user,
|
||||
db=db,
|
||||
)
|
||||
@ -418,8 +410,6 @@ async def read_viewer_file_content(
|
||||
*,
|
||||
thread_id: str,
|
||||
path: str,
|
||||
agent_id: str | None,
|
||||
agent_config_id: int | None,
|
||||
current_user: User,
|
||||
db: AsyncSession,
|
||||
) -> dict:
|
||||
@ -429,8 +419,6 @@ async def read_viewer_file_content(
|
||||
|
||||
sandbox_backend, skills_backend, _selected_skills = await _resolve_viewer_state(
|
||||
thread_id=thread_id,
|
||||
agent_id=agent_id,
|
||||
agent_config_id=agent_config_id,
|
||||
current_user=current_user,
|
||||
db=db,
|
||||
)
|
||||
@ -512,16 +500,12 @@ async def download_viewer_file(
|
||||
*,
|
||||
thread_id: str,
|
||||
path: str,
|
||||
agent_id: str | None,
|
||||
agent_config_id: int | None,
|
||||
current_user: User,
|
||||
db: AsyncSession,
|
||||
) -> StreamingResponse:
|
||||
normalized_path = _normalize_path(path)
|
||||
sandbox_backend, skills_backend, _selected_skills = await _resolve_viewer_state(
|
||||
thread_id=thread_id,
|
||||
agent_id=agent_id,
|
||||
agent_config_id=agent_config_id,
|
||||
current_user=current_user,
|
||||
db=db,
|
||||
)
|
||||
@ -577,8 +561,6 @@ async def delete_viewer_file(
|
||||
*,
|
||||
thread_id: str,
|
||||
path: str,
|
||||
agent_id: str | None,
|
||||
agent_config_id: int | None,
|
||||
current_user: User,
|
||||
db: AsyncSession,
|
||||
) -> dict:
|
||||
@ -588,8 +570,6 @@ async def delete_viewer_file(
|
||||
normalized_path = _normalize_path(path)
|
||||
await _resolve_viewer_state(
|
||||
thread_id=thread_id,
|
||||
agent_id=agent_id,
|
||||
agent_config_id=agent_config_id,
|
||||
current_user=current_user,
|
||||
db=db,
|
||||
)
|
||||
@ -620,8 +600,6 @@ async def create_viewer_directory(
|
||||
thread_id: str,
|
||||
parent_path: str,
|
||||
name: str,
|
||||
agent_id: str | None,
|
||||
agent_config_id: int | None,
|
||||
current_user: User,
|
||||
db: AsyncSession,
|
||||
) -> dict:
|
||||
@ -630,8 +608,6 @@ async def create_viewer_directory(
|
||||
|
||||
await _resolve_viewer_state(
|
||||
thread_id=thread_id,
|
||||
agent_id=agent_id,
|
||||
agent_config_id=agent_config_id,
|
||||
current_user=current_user,
|
||||
db=db,
|
||||
)
|
||||
@ -658,8 +634,6 @@ async def upload_viewer_file(
|
||||
thread_id: str,
|
||||
parent_path: str,
|
||||
file: UploadFile,
|
||||
agent_id: str | None,
|
||||
agent_config_id: int | None,
|
||||
current_user: User,
|
||||
db: AsyncSession,
|
||||
) -> dict:
|
||||
@ -668,8 +642,6 @@ async def upload_viewer_file(
|
||||
|
||||
await _resolve_viewer_state(
|
||||
thread_id=thread_id,
|
||||
agent_id=agent_id,
|
||||
agent_config_id=agent_config_id,
|
||||
current_user=current_user,
|
||||
db=db,
|
||||
)
|
||||
|
||||
@ -28,24 +28,18 @@ class CreateViewerDirectoryRequest(BaseModel):
|
||||
thread_id: str
|
||||
parent_path: str
|
||||
name: str
|
||||
agent_id: str | None = None
|
||||
agent_config_id: int | None = None
|
||||
|
||||
|
||||
@filesystem_router.get("/tree", response_model=dict)
|
||||
async def get_viewer_tree(
|
||||
thread_id: str = Query(..., description="线程 ID"),
|
||||
path: str = Query("/", description="目录路径"),
|
||||
agent_id: str | None = Query(None, description="智能体 ID"),
|
||||
agent_config_id: int | None = Query(None, description="智能体配置 ID"),
|
||||
current_user: User = Depends(get_required_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
return await list_viewer_filesystem_tree(
|
||||
thread_id=thread_id,
|
||||
path=path,
|
||||
agent_id=agent_id,
|
||||
agent_config_id=agent_config_id,
|
||||
current_user=current_user,
|
||||
db=db,
|
||||
)
|
||||
@ -55,16 +49,12 @@ async def get_viewer_tree(
|
||||
async def get_viewer_file(
|
||||
thread_id: str = Query(..., description="线程 ID"),
|
||||
path: str = Query(..., description="文件路径"),
|
||||
agent_id: str | None = Query(None, description="智能体 ID"),
|
||||
agent_config_id: int | None = Query(None, description="智能体配置 ID"),
|
||||
current_user: User = Depends(get_required_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
return await read_viewer_file_content(
|
||||
thread_id=thread_id,
|
||||
path=path,
|
||||
agent_id=agent_id,
|
||||
agent_config_id=agent_config_id,
|
||||
current_user=current_user,
|
||||
db=db,
|
||||
)
|
||||
@ -74,16 +64,12 @@ async def get_viewer_file(
|
||||
async def delete_viewer_file_route(
|
||||
thread_id: str = Query(..., description="线程 ID"),
|
||||
path: str = Query(..., description="文件路径"),
|
||||
agent_id: str | None = Query(None, description="智能体 ID"),
|
||||
agent_config_id: int | None = Query(None, description="智能体配置 ID"),
|
||||
current_user: User = Depends(get_required_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
return await delete_viewer_file(
|
||||
thread_id=thread_id,
|
||||
path=path,
|
||||
agent_id=agent_id,
|
||||
agent_config_id=agent_config_id,
|
||||
current_user=current_user,
|
||||
db=db,
|
||||
)
|
||||
@ -99,8 +85,6 @@ async def create_viewer_directory_route(
|
||||
thread_id=payload.thread_id,
|
||||
parent_path=payload.parent_path,
|
||||
name=payload.name,
|
||||
agent_id=payload.agent_id,
|
||||
agent_config_id=payload.agent_config_id,
|
||||
current_user=current_user,
|
||||
db=db,
|
||||
)
|
||||
@ -110,8 +94,6 @@ async def create_viewer_directory_route(
|
||||
async def upload_viewer_file_route(
|
||||
thread_id: str = Form(..., description="线程 ID"),
|
||||
parent_path: str = Form(..., description="父目录路径"),
|
||||
agent_id: str | None = Form(None, description="智能体 ID"),
|
||||
agent_config_id: int | None = Form(None, description="智能体配置 ID"),
|
||||
file: UploadFile = File(..., description="上传文件"),
|
||||
current_user: User = Depends(get_required_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
@ -120,8 +102,6 @@ async def upload_viewer_file_route(
|
||||
thread_id=thread_id,
|
||||
parent_path=parent_path,
|
||||
file=file,
|
||||
agent_id=agent_id,
|
||||
agent_config_id=agent_config_id,
|
||||
current_user=current_user,
|
||||
db=db,
|
||||
)
|
||||
@ -131,16 +111,12 @@ async def upload_viewer_file_route(
|
||||
async def download_viewer(
|
||||
thread_id: str = Query(..., description="线程 ID"),
|
||||
path: str = Query(..., description="文件路径"),
|
||||
agent_id: str | None = Query(None, description="智能体 ID"),
|
||||
agent_config_id: int | None = Query(None, description="智能体配置 ID"),
|
||||
current_user: User = Depends(get_required_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
return await download_viewer_file(
|
||||
thread_id=thread_id,
|
||||
path=path,
|
||||
agent_id=agent_id,
|
||||
agent_config_id=agent_config_id,
|
||||
current_user=current_user,
|
||||
db=db,
|
||||
)
|
||||
|
||||
@ -10,72 +10,45 @@ const buildQuery = (params) => {
|
||||
return query.toString()
|
||||
}
|
||||
|
||||
const buildViewerQuery = (threadId, path, agentId = null, agentConfigId = null) => {
|
||||
const buildViewerQuery = (threadId, path) => {
|
||||
return buildQuery({
|
||||
thread_id: threadId,
|
||||
path,
|
||||
agent_id: agentId,
|
||||
agent_config_id: agentConfigId
|
||||
path
|
||||
})
|
||||
}
|
||||
|
||||
export const getViewerFileSystemTree = (
|
||||
threadId,
|
||||
path = '/',
|
||||
agentId = null,
|
||||
agentConfigId = null
|
||||
) => {
|
||||
const query = buildViewerQuery(threadId, path, agentId, agentConfigId)
|
||||
export const getViewerFileSystemTree = (threadId, path = '/') => {
|
||||
const query = buildViewerQuery(threadId, path)
|
||||
return apiGet(`/api/viewer/filesystem/tree?${query}`)
|
||||
}
|
||||
|
||||
export const getViewerFileContent = (threadId, path, agentId = null, agentConfigId = null) => {
|
||||
const query = buildViewerQuery(threadId, path, agentId, agentConfigId)
|
||||
export const getViewerFileContent = (threadId, path) => {
|
||||
const query = buildViewerQuery(threadId, path)
|
||||
return apiGet(`/api/viewer/filesystem/file?${query}`)
|
||||
}
|
||||
|
||||
export const downloadViewerFile = (threadId, path, agentId = null, agentConfigId = null) => {
|
||||
const query = buildViewerQuery(threadId, path, agentId, agentConfigId)
|
||||
export const downloadViewerFile = (threadId, path) => {
|
||||
const query = buildViewerQuery(threadId, path)
|
||||
return apiGet(`/api/viewer/filesystem/download?${query}`, {}, true, 'blob')
|
||||
}
|
||||
|
||||
export const deleteViewerFile = (threadId, path, agentId = null, agentConfigId = null) => {
|
||||
const query = buildViewerQuery(threadId, path, agentId, agentConfigId)
|
||||
export const deleteViewerFile = (threadId, path) => {
|
||||
const query = buildViewerQuery(threadId, path)
|
||||
return apiDelete(`/api/viewer/filesystem/file?${query}`)
|
||||
}
|
||||
|
||||
export const createViewerDirectory = (
|
||||
threadId,
|
||||
parentPath,
|
||||
name,
|
||||
agentId = null,
|
||||
agentConfigId = null
|
||||
) => {
|
||||
export const createViewerDirectory = (threadId, parentPath, name) => {
|
||||
return apiPost('/api/viewer/filesystem/directory', {
|
||||
thread_id: threadId,
|
||||
parent_path: parentPath,
|
||||
name,
|
||||
agent_id: agentId,
|
||||
agent_config_id: agentConfigId
|
||||
name
|
||||
})
|
||||
}
|
||||
|
||||
export const uploadViewerFile = (
|
||||
threadId,
|
||||
parentPath,
|
||||
file,
|
||||
agentId = null,
|
||||
agentConfigId = null
|
||||
) => {
|
||||
export const uploadViewerFile = (threadId, parentPath, file) => {
|
||||
const formData = new FormData()
|
||||
formData.set('thread_id', threadId)
|
||||
formData.set('parent_path', parentPath)
|
||||
if (agentId !== undefined && agentId !== null && agentId !== '') {
|
||||
formData.set('agent_id', agentId)
|
||||
}
|
||||
if (agentConfigId !== undefined && agentConfigId !== null && agentConfigId !== '') {
|
||||
formData.set('agent_config_id', agentConfigId)
|
||||
}
|
||||
formData.set('file', file)
|
||||
return apiPost('/api/viewer/filesystem/upload', formData)
|
||||
}
|
||||
|
||||
@ -1,5 +1,15 @@
|
||||
const MARKDOWN_EXTENSIONS = new Set(['.md', '.markdown', '.mdx'])
|
||||
const IMAGE_EXTENSIONS = new Set(['.png', '.jpg', '.jpeg', '.gif', '.bmp', '.webp', '.svg'])
|
||||
const IMAGE_EXTENSIONS = new Set([
|
||||
'.apng',
|
||||
'.avif',
|
||||
'.png',
|
||||
'.jpg',
|
||||
'.jpeg',
|
||||
'.gif',
|
||||
'.bmp',
|
||||
'.webp',
|
||||
'.svg'
|
||||
])
|
||||
const PDF_EXTENSIONS = new Set(['.pdf'])
|
||||
const HTML_EXTENSIONS = new Set(['.html', '.htm'])
|
||||
const CODE_LANGUAGE_ALIASES = {
|
||||
|
||||
@ -11,6 +11,7 @@ import {
|
||||
LinkOutlined,
|
||||
CodeFilled
|
||||
} from '@ant-design/icons-vue'
|
||||
import { getPreviewFileExtension, getPreviewTypeByPath } from '@/utils/file_preview'
|
||||
import { formatRelative, parseToShanghai } from '@/utils/time'
|
||||
|
||||
const DEFAULT_FILE_ICON = { icon: FileFilled, color: 'var(--gray-600)' }
|
||||
@ -30,6 +31,8 @@ const FILE_ICON_CONFIG = {
|
||||
csv: { icon: FileExcelFilled, color: 'var(--color-success-500)' },
|
||||
ppt: { icon: FilePptFilled, color: 'var(--color-warning-700)' },
|
||||
pptx: { icon: FilePptFilled, color: 'var(--color-warning-700)' },
|
||||
apng: { icon: FileImageFilled, color: 'var(--color-accent-700)' },
|
||||
avif: { icon: FileImageFilled, color: 'var(--color-accent-700)' },
|
||||
jpg: { icon: FileImageFilled, color: 'var(--color-accent-700)' },
|
||||
jpeg: { icon: FileImageFilled, color: 'var(--color-accent-700)' },
|
||||
png: { icon: FileImageFilled, color: 'var(--color-accent-700)' },
|
||||
@ -110,3 +113,49 @@ export const formatFileSize = (bytes) => {
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
|
||||
}
|
||||
|
||||
export const getDisplayFileName = (pathOrName, fallback = '文件') => {
|
||||
const value = String(pathOrName || '').trim()
|
||||
if (!value) return fallback
|
||||
return value.split('/').pop() || value || fallback
|
||||
}
|
||||
|
||||
export const getFileExtensionLabel = (pathOrName) => {
|
||||
const extension = getPreviewFileExtension(pathOrName).replace(/^\./, '')
|
||||
return extension ? extension.toUpperCase() : ''
|
||||
}
|
||||
|
||||
export const getMimeSubtypeLabel = (mimeType) => {
|
||||
const subtype = String(mimeType || '')
|
||||
.split('/')
|
||||
.pop()
|
||||
?.trim()
|
||||
return subtype ? subtype.toUpperCase() : ''
|
||||
}
|
||||
|
||||
export const normalizeAttachmentPreview = (attachment) => {
|
||||
const name = getDisplayFileName(
|
||||
attachment?.file_name || attachment?.name || attachment?.path,
|
||||
'附件'
|
||||
)
|
||||
const fileId = attachment?.file_id || attachment?.path || name
|
||||
const fileType = String(attachment?.file_type || '')
|
||||
const sizeLabel = formatFileSize(attachment?.file_size)
|
||||
const typeLabel = getFileExtensionLabel(name) || getMimeSubtypeLabel(fileType) || '文件'
|
||||
|
||||
return {
|
||||
raw: attachment,
|
||||
fileId,
|
||||
name,
|
||||
previewUrl: attachment?.original_artifact_url || attachment?.artifact_url || '',
|
||||
isImage: fileType.startsWith('image/') || getPreviewTypeByPath(name) === 'image',
|
||||
meta: [typeLabel, sizeLabel === '-' ? '' : sizeLabel].filter(Boolean).join(' · '),
|
||||
icon: getFileIcon(name),
|
||||
iconColor: getFileIconColor(name)
|
||||
}
|
||||
}
|
||||
|
||||
export const normalizeAttachmentPreviews = (attachments) => {
|
||||
if (!Array.isArray(attachments)) return []
|
||||
return attachments.map(normalizeAttachmentPreview).filter((attachment) => attachment.fileId)
|
||||
}
|
||||
|
||||
2
web/src/utils/upload_limits.js
Normal file
2
web/src/utils/upload_limits.js
Normal file
@ -0,0 +1,2 @@
|
||||
export const MAX_IMAGE_UPLOAD_SIZE_MB = 5
|
||||
export const MAX_IMAGE_UPLOAD_SIZE_BYTES = MAX_IMAGE_UPLOAD_SIZE_MB * 1024 * 1024
|
||||
Loading…
Reference in New Issue
Block a user