fix: 修复time未引包问题&状态工作台,文件列表递归调用接口的情况,递归放在后端

This commit is contained in:
supreme0597 2026-03-14 23:54:52 +08:00
parent 9c1e5e2154
commit c5622fdef4
7 changed files with 51 additions and 29 deletions

View File

@ -141,6 +141,8 @@ services:
- ./saves:/app/saves - ./saves:/app/saves
- ./docker/sandbox_provisioner/app.py:/app/app.py:ro - ./docker/sandbox_provisioner/app.py:/app/app.py:ro
- /var/run/docker.sock:/var/run/docker.sock - /var/run/docker.sock:/var/run/docker.sock
ports:
- "8002:8002"
networks: networks:
- app-network - app-network
extra_hosts: extra_hosts:

View File

@ -11,6 +11,7 @@ if sys.platform == "win32":
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
import time
from collections import defaultdict, deque from collections import defaultdict, deque
import uvicorn import uvicorn

View File

@ -516,6 +516,7 @@ async def resume_agent_chat(
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
): ):
"""恢复被人工审批中断的对话(需要登录)""" """恢复被人工审批中断的对话(需要登录)"""
def normalize_resume_input(raw_answer: Any, raw_approved: bool | None) -> Any: def normalize_resume_input(raw_answer: Any, raw_approved: bool | None) -> Any:
if raw_answer is not None: if raw_answer is not None:
if isinstance(raw_answer, str): if isinstance(raw_answer, str):
@ -874,6 +875,7 @@ async def delete_thread_attachment(
async def list_thread_files( async def list_thread_files(
thread_id: str, thread_id: str,
path: str = Query("/mnt/user-data"), path: str = Query("/mnt/user-data"),
recursive: bool = Query(False),
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_required_user), current_user: User = Depends(get_required_user),
): ):
@ -883,6 +885,7 @@ async def list_thread_files(
current_user_id=str(current_user.id), current_user_id=str(current_user.id),
db=db, db=db,
path=path, path=path,
recursive=recursive,
) )
@ -923,11 +926,7 @@ async def get_thread_artifact(
) )
media_type = guess_type(file_path.name)[0] or "application/octet-stream" media_type = guess_type(file_path.name)[0] or "application/octet-stream"
headers = ( headers = {"Content-Disposition": f'attachment; filename="{file_path.name}"'} if download else None
{"Content-Disposition": f'attachment; filename="{file_path.name}"'}
if download
else None
)
return FileResponse(path=file_path, media_type=media_type, headers=headers) return FileResponse(path=file_path, media_type=media_type, headers=headers)

View File

@ -34,6 +34,7 @@ async def list_thread_files_view(
current_user_id: str, current_user_id: str,
db, db,
path: str | None = None, path: str | None = None,
recursive: bool = False,
) -> dict: ) -> dict:
conv_repo = ConversationRepository(db) conv_repo = ConversationRepository(db)
await require_user_conversation(conv_repo, thread_id, str(current_user_id)) await require_user_conversation(conv_repo, thread_id, str(current_user_id))
@ -50,6 +51,9 @@ async def list_thread_files_view(
if not actual_path.is_dir(): if not actual_path.is_dir():
raise HTTPException(status_code=400, detail="path must be a directory") raise HTTPException(status_code=400, detail="path must be a directory")
if recursive:
return _list_files_recursive(thread_id, actual_path, virtual_path)
entries: list[dict[str, Any]] = [] entries: list[dict[str, Any]] = []
for child in sorted(actual_path.iterdir(), key=lambda item: (not item.is_dir(), item.name.lower())): for child in sorted(actual_path.iterdir(), key=lambda item: (not item.is_dir(), item.name.lower())):
stat = child.stat() stat = child.stat()
@ -70,6 +74,35 @@ async def list_thread_files_view(
return {"path": virtual_path, "files": entries} return {"path": virtual_path, "files": entries}
def _list_files_recursive(thread_id: str, actual_path: Path, virtual_path: str) -> dict:
entries: list[dict[str, Any]] = []
def _scan_dir(base_actual_path: Path, base_virtual_path: str):
try:
for child in sorted(base_actual_path.iterdir(), key=lambda item: (not item.is_dir(), item.name.lower())):
stat = child.stat()
child_virtual_path = virtual_path_for_thread_file(thread_id, child)
entries.append(
{
"path": child_virtual_path,
"name": child.name,
"is_dir": child.is_dir(),
"size": stat.st_size if child.is_file() else 0,
"modified_at": _to_iso8601(stat.st_mtime),
"artifact_url": None
if child.is_dir()
else f"/api/chat/thread/{thread_id}/artifacts/{child_virtual_path.lstrip('/')}",
}
)
if child.is_dir():
_scan_dir(child, child_virtual_path)
except PermissionError:
pass
_scan_dir(actual_path, virtual_path)
return {"path": virtual_path, "files": entries}
async def read_thread_file_content_view( async def read_thread_file_content_view(
*, *,
thread_id: str, thread_id: str,

View File

@ -3402,6 +3402,9 @@ wheels = [
binary = [ binary = [
{ name = "psycopg-binary", marker = "implementation_name != 'pypy'" }, { name = "psycopg-binary", marker = "implementation_name != 'pypy'" },
] ]
pool = [
{ name = "psycopg-pool" },
]
[[package]] [[package]]
name = "psycopg-binary" name = "psycopg-binary"
@ -5653,7 +5656,7 @@ wheels = [
[[package]] [[package]]
name = "yuxi-know" name = "yuxi-know"
version = "0.5.1" version = "0.5.2"
source = { virtual = "." } source = { virtual = "." }
dependencies = [ dependencies = [
{ name = "agent-sandbox" }, { name = "agent-sandbox" },
@ -5697,7 +5700,7 @@ dependencies = [
{ name = "openai" }, { name = "openai" },
{ name = "opencv-python-headless" }, { name = "opencv-python-headless" },
{ name = "pillow" }, { name = "pillow" },
{ name = "psycopg", extra = ["binary"] }, { name = "psycopg", extra = ["binary", "pool"] },
{ name = "pyjwt" }, { name = "pyjwt" },
{ name = "pymilvus" }, { name = "pymilvus" },
{ name = "pymupdf" }, { name = "pymupdf" },
@ -5782,7 +5785,7 @@ requires-dist = [
{ name = "openai", specifier = ">=1.109" }, { name = "openai", specifier = ">=1.109" },
{ name = "opencv-python-headless", specifier = ">=4.11.0.86" }, { name = "opencv-python-headless", specifier = ">=4.11.0.86" },
{ name = "pillow", specifier = ">=10.5.0" }, { name = "pillow", specifier = ">=10.5.0" },
{ name = "psycopg", extras = ["binary"], specifier = ">=3.2.0" }, { name = "psycopg", extras = ["binary", "pool"], specifier = ">=3.3.3" },
{ name = "pyjwt", specifier = ">=2.8.0" }, { name = "pyjwt", specifier = ">=2.8.0" },
{ name = "pymilvus", specifier = ">=2.5.8" }, { name = "pymilvus", specifier = ">=2.5.8" },
{ name = "pymupdf", specifier = ">=1.25.5" }, { name = "pymupdf", specifier = ">=1.25.5" },

View File

@ -340,10 +340,11 @@ export const threadApi = {
* 列出线程文件目录 * 列出线程文件目录
* @param {string} threadId * @param {string} threadId
* @param {string} path * @param {string} path
* @param {boolean} recursive
* @returns {Promise} * @returns {Promise}
*/ */
listThreadFiles: (threadId, path = '/mnt/user-data') => listThreadFiles: (threadId, path = '/mnt/user-data', recursive = false) =>
apiGet(`/api/chat/thread/${threadId}/files?path=${encodeURIComponent(path)}`), apiGet(`/api/chat/thread/${threadId}/files?path=${encodeURIComponent(path)}&recursive=${recursive}`),
/** /**
* 读取线程文本文件内容分页 * 读取线程文本文件内容分页

View File

@ -758,26 +758,9 @@ const fetchThreadMessages = async ({ agentId, threadId, delay = 0 }) => {
const fetchThreadFiles = async (threadId) => { const fetchThreadFiles = async (threadId) => {
if (!threadId) return if (!threadId) return
const queue = ['/mnt/user-data']
const visited = new Set()
const entries = []
try { try {
while (queue.length > 0) { const response = await threadApi.listThreadFiles(threadId, '/mnt/user-data', true)
const currentPath = queue.shift() const entries = Array.isArray(response?.files) ? response.files : []
if (!currentPath || visited.has(currentPath)) continue
visited.add(currentPath)
const response = await threadApi.listThreadFiles(threadId, currentPath)
const files = Array.isArray(response?.files) ? response.files : []
files.forEach((item) => {
if (!item || typeof item.path !== 'string') return
entries.push(item)
if (item.is_dir === true) {
queue.push(item.path)
}
})
}
threadFilesMap.value[threadId] = entries threadFilesMap.value[threadId] = entries
} catch (error) { } catch (error) {
console.warn('Failed to fetch thread files:', error) console.warn('Failed to fetch thread files:', error)