已实现由外部创建会话
This commit is contained in:
parent
c669b3bb24
commit
4119fdcba7
|
|
@ -2,9 +2,10 @@
|
|||
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import APIRouter, File, HTTPException, UploadFile
|
||||
from fastapi import APIRouter, File, Form, HTTPException, UploadFile
|
||||
from pydantic import BaseModel
|
||||
|
||||
from src.agents.middlewares.thread_data_middleware import THREAD_DATA_BASE_DIR
|
||||
|
|
@ -14,6 +15,8 @@ logger = logging.getLogger(__name__)
|
|||
|
||||
router = APIRouter(prefix="/api/threads/{thread_id}/uploads", tags=["uploads"])
|
||||
|
||||
SKILL_UPLOAD_TARGET = "skill"
|
||||
|
||||
# File extensions that should be converted to markdown
|
||||
CONVERTIBLE_EXTENSIONS = {
|
||||
".pdf",
|
||||
|
|
@ -78,6 +81,7 @@ async def convert_file_to_markdown(file_path: Path) -> Path | None:
|
|||
async def upload_files(
|
||||
thread_id: str,
|
||||
files: list[UploadFile] = File(...),
|
||||
upload_target: str | None = Form(default=None),
|
||||
) -> UploadResponse:
|
||||
"""Upload multiple files to a thread's uploads directory.
|
||||
|
||||
|
|
@ -95,6 +99,19 @@ async def upload_files(
|
|||
raise HTTPException(status_code=400, detail="No files provided")
|
||||
|
||||
uploads_dir = get_uploads_dir(thread_id)
|
||||
target_dir = uploads_dir
|
||||
relative_upload_prefix = "uploads"
|
||||
|
||||
if upload_target == SKILL_UPLOAD_TARGET:
|
||||
target_dir = uploads_dir / SKILL_UPLOAD_TARGET
|
||||
|
||||
# Clean existing uploads/skill directory to keep only the latest skill package
|
||||
if target_dir.exists():
|
||||
shutil.rmtree(target_dir)
|
||||
target_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
relative_upload_prefix = f"uploads/{SKILL_UPLOAD_TARGET}"
|
||||
|
||||
uploaded_files = []
|
||||
|
||||
sandbox_provider = get_sandbox_provider()
|
||||
|
|
@ -107,12 +124,12 @@ async def upload_files(
|
|||
|
||||
try:
|
||||
# Save the original file
|
||||
file_path = uploads_dir / file.filename
|
||||
file_path = target_dir / file.filename
|
||||
content = await file.read()
|
||||
|
||||
# Build relative path from backend root
|
||||
relative_path = f".deer-flow/threads/{thread_id}/user-data/uploads/{file.filename}"
|
||||
virtual_path = f"/mnt/user-data/uploads/{file.filename}"
|
||||
relative_path = f".deer-flow/threads/{thread_id}/user-data/{relative_upload_prefix}/{file.filename}"
|
||||
virtual_path = f"/mnt/user-data/{relative_upload_prefix}/{file.filename}"
|
||||
sandbox.update_file(virtual_path, content)
|
||||
|
||||
file_info = {
|
||||
|
|
@ -120,7 +137,7 @@ async def upload_files(
|
|||
"size": str(len(content)),
|
||||
"path": relative_path, # Actual filesystem path (relative to backend/)
|
||||
"virtual_path": virtual_path, # Path for Agent in sandbox
|
||||
"artifact_url": f"/api/threads/{thread_id}/artifacts/mnt/user-data/uploads/{file.filename}", # HTTP URL
|
||||
"artifact_url": f"/api/threads/{thread_id}/artifacts/mnt/user-data/{relative_upload_prefix}/{file.filename}", # HTTP URL
|
||||
}
|
||||
|
||||
logger.info(f"Saved file: {file.filename} ({len(content)} bytes) to {relative_path}")
|
||||
|
|
@ -130,11 +147,13 @@ async def upload_files(
|
|||
if file_ext in CONVERTIBLE_EXTENSIONS:
|
||||
md_path = await convert_file_to_markdown(file_path)
|
||||
if md_path:
|
||||
md_relative_path = f".deer-flow/threads/{thread_id}/user-data/uploads/{md_path.name}"
|
||||
md_relative_path = f".deer-flow/threads/{thread_id}/user-data/{relative_upload_prefix}/{md_path.name}"
|
||||
file_info["markdown_file"] = md_path.name
|
||||
file_info["markdown_path"] = md_relative_path
|
||||
file_info["markdown_virtual_path"] = f"/mnt/user-data/uploads/{md_path.name}"
|
||||
file_info["markdown_artifact_url"] = f"/api/threads/{thread_id}/artifacts/mnt/user-data/uploads/{md_path.name}"
|
||||
file_info["markdown_virtual_path"] = f"/mnt/user-data/{relative_upload_prefix}/{md_path.name}"
|
||||
file_info["markdown_artifact_url"] = (
|
||||
f"/api/threads/{thread_id}/artifacts/mnt/user-data/{relative_upload_prefix}/{md_path.name}"
|
||||
)
|
||||
|
||||
uploaded_files.append(file_info)
|
||||
|
||||
|
|
@ -165,17 +184,18 @@ async def list_uploaded_files(thread_id: str) -> dict:
|
|||
return {"files": [], "count": 0}
|
||||
|
||||
files = []
|
||||
for file_path in sorted(uploads_dir.iterdir()):
|
||||
for file_path in sorted(uploads_dir.rglob("*")):
|
||||
if file_path.is_file():
|
||||
stat = file_path.stat()
|
||||
relative_path = f".deer-flow/threads/{thread_id}/user-data/uploads/{file_path.name}"
|
||||
path_in_uploads = file_path.relative_to(uploads_dir).as_posix()
|
||||
relative_path = f".deer-flow/threads/{thread_id}/user-data/uploads/{path_in_uploads}"
|
||||
files.append(
|
||||
{
|
||||
"filename": file_path.name,
|
||||
"filename": path_in_uploads,
|
||||
"size": stat.st_size,
|
||||
"path": relative_path, # Actual filesystem path (relative to backend/)
|
||||
"virtual_path": f"/mnt/user-data/uploads/{file_path.name}", # Path for Agent in sandbox
|
||||
"artifact_url": f"/api/threads/{thread_id}/artifacts/mnt/user-data/uploads/{file_path.name}", # HTTP URL
|
||||
"virtual_path": f"/mnt/user-data/uploads/{path_in_uploads}", # Path for Agent in sandbox
|
||||
"artifact_url": f"/api/threads/{thread_id}/artifacts/mnt/user-data/uploads/{path_in_uploads}", # HTTP URL
|
||||
"extension": file_path.suffix,
|
||||
"modified": stat.st_mtime,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -81,17 +81,34 @@ export default function ChatPage() {
|
|||
}
|
||||
}, [inputInitialValue]);
|
||||
const isNewThread = useMemo(
|
||||
() => threadIdFromPath === "new",
|
||||
[threadIdFromPath],
|
||||
() => {
|
||||
if (threadIdFromPath !== "new") {
|
||||
return false;
|
||||
}
|
||||
|
||||
const queryThreadId = searchParams.get("thread_id")?.trim();
|
||||
const queryIsNew = searchParams.get("isnew")?.trim().toLowerCase();
|
||||
const shouldReuseExisting = queryIsNew === "false" && !!queryThreadId;
|
||||
|
||||
return !shouldReuseExisting;
|
||||
},
|
||||
[threadIdFromPath, searchParams],
|
||||
);
|
||||
|
||||
const uploadTarget = useMemo(() => {
|
||||
const target = searchParams.get("upload_target")?.trim().toLowerCase();
|
||||
return target === "skill" ? "skill" : undefined;
|
||||
}, [searchParams]);
|
||||
|
||||
const [threadId, setThreadId] = useState<string | null>(null);
|
||||
useEffect(() => {
|
||||
if (threadIdFromPath !== "new") {
|
||||
setThreadId(threadIdFromPath);
|
||||
} else {
|
||||
setThreadId(uuid());
|
||||
const queryThreadId = searchParams.get("thread_id")?.trim();
|
||||
setThreadId(queryThreadId || uuid());
|
||||
}
|
||||
}, [threadIdFromPath]);
|
||||
}, [threadIdFromPath, searchParams]);
|
||||
|
||||
const { showNotification } = useNotification();
|
||||
const [finalState, setFinalState] = useState<AgentThreadState | null>(null);
|
||||
|
|
@ -185,6 +202,7 @@ export default function ChatPage() {
|
|||
isNewThread,
|
||||
threadId,
|
||||
thread,
|
||||
uploadTarget,
|
||||
threadContext: {
|
||||
...settings.context,
|
||||
thinking_enabled: settings.context.mode !== "flash",
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import type { PromptInputMessage } from "@/components/ai-elements/prompt-input";
|
|||
import { getAPIClient } from "../api";
|
||||
import { useUpdateSubtask } from "../tasks/context";
|
||||
import { uploadFiles } from "../uploads";
|
||||
import type { UploadTarget } from "../uploads/api";
|
||||
|
||||
import type {
|
||||
AgentThread,
|
||||
|
|
@ -83,12 +84,14 @@ export function useSubmitThread({
|
|||
thread,
|
||||
threadContext,
|
||||
isNewThread,
|
||||
uploadTarget,
|
||||
afterSubmit,
|
||||
}: {
|
||||
isNewThread: boolean;
|
||||
threadId: string | null | undefined;
|
||||
thread: UseStream<AgentThreadState>;
|
||||
threadContext: Omit<AgentThreadContext, "thread_id">;
|
||||
uploadTarget?: UploadTarget;
|
||||
afterSubmit?: () => void;
|
||||
}) {
|
||||
const queryClient = useQueryClient();
|
||||
|
|
@ -127,7 +130,7 @@ export function useSubmitThread({
|
|||
);
|
||||
|
||||
if (files.length > 0 && threadId) {
|
||||
await uploadFiles(threadId, files);
|
||||
await uploadFiles(threadId, files, { target: uploadTarget });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to upload files:", error);
|
||||
|
|
@ -167,7 +170,7 @@ export function useSubmitThread({
|
|||
void queryClient.invalidateQueries({ queryKey: ["threads", "search"] });
|
||||
afterSubmit?.();
|
||||
},
|
||||
[thread, isNewThread, threadId, threadContext, queryClient, afterSubmit],
|
||||
[thread, isNewThread, threadId, threadContext, uploadTarget, queryClient, afterSubmit],
|
||||
);
|
||||
return callback;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,12 +29,15 @@ export interface ListFilesResponse {
|
|||
count: number;
|
||||
}
|
||||
|
||||
export type UploadTarget = "skill";
|
||||
|
||||
/**
|
||||
* Upload files to a thread
|
||||
*/
|
||||
export async function uploadFiles(
|
||||
threadId: string,
|
||||
files: File[],
|
||||
options?: { target?: UploadTarget },
|
||||
): Promise<UploadResponse> {
|
||||
const formData = new FormData();
|
||||
|
||||
|
|
@ -42,6 +45,10 @@ export async function uploadFiles(
|
|||
formData.append("files", file);
|
||||
});
|
||||
|
||||
if (options?.target) {
|
||||
formData.append("upload_target", options.target);
|
||||
}
|
||||
|
||||
const response = await fetch(
|
||||
`${getBackendBaseURL()}/api/threads/${threadId}/uploads`,
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in New Issue