初始化提交
This commit is contained in:
commit
887c1f449e
963 files changed
+166869
No files matched your search
@@ -0,0 +1,151 @@
|
||||
"""沙箱文件预览 API。
|
||||
|
||||
通过此端点,前端可以读取沙箱中由工具写入的文件内容,实现源码/渲染双模式预览。
|
||||
|
||||
使用场景:
|
||||
- write_file 写入 HTML 文件 → 前端 iframe 渲染预览
|
||||
- write_file 写入 SVG → 前端 img 展示
|
||||
- shell 生成图片/文档 → 前端按 MIME 类型渲染
|
||||
- 任何工具的 stdout 生成的文件
|
||||
|
||||
安全约束:
|
||||
- 用户必须经过 JWT 鉴权
|
||||
- 路径由沙箱端校验(防逃逸)
|
||||
- 可选 run_id/character_id 绑定授权上下文
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import mimetypes
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from starlette.responses import Response
|
||||
|
||||
from app.api.v1.deps import get_current_user, get_tool_runtime
|
||||
from app.application.tool_runtime.runtime import ToolRuntime
|
||||
from app.integrations.sandbox.exceptions import SandboxError, SandboxNotFound
|
||||
from app.schemas.auth import CurrentUser
|
||||
|
||||
router = APIRouter(prefix="/sandbox", tags=["sandbox"])
|
||||
|
||||
|
||||
@router.get("/file")
|
||||
async def read_sandbox_file(
|
||||
path: str = Query(
|
||||
...,
|
||||
description="文件路径。isolated=True 时视为工作区相对路径"
|
||||
"(如 write_file 传入的 ``path`` 参数);"
|
||||
"isolated=False 时作为沙箱 base 下的完整相对路径。",
|
||||
),
|
||||
run_id: str | None = Query(
|
||||
None,
|
||||
description="关联的 run_id(可选,写入授权上下文)。仅用于审计链路,不影响文件可读性。",
|
||||
),
|
||||
character_id: str | None = Query(
|
||||
None,
|
||||
description="人物 ID。提供时自动构造隔离工作区路径"
|
||||
"(``workspace/<character_id>/<path>``),"
|
||||
"与 write_file/shell 等工具的 isolated_workspace 路径一致。",
|
||||
),
|
||||
isolated: bool = Query(
|
||||
True,
|
||||
description="是否自动按 isolated_workspace 规则补全路径前缀。"
|
||||
"write_file 写入的文件在 workspace/<character_id>/ 下,"
|
||||
"传递 write_file 的 path 时保持 True 即可。",
|
||||
),
|
||||
runtime: ToolRuntime = Depends(get_tool_runtime),
|
||||
_current_user: CurrentUser = Depends(get_current_user),
|
||||
) -> Response:
|
||||
"""读取沙箱内文件,以原始字节返回(MIME 类型由路径扩展名推导)。
|
||||
|
||||
前端根据 MIME 类型自行决定展示方式:
|
||||
- ``text/html`` → iframe sandbox 渲染预览
|
||||
- ``image/*`` → img 标签展示
|
||||
- ``text/*`` → 源码高亮
|
||||
- ``application/pdf`` → PDF 内嵌展示
|
||||
- 其他 → 下载
|
||||
|
||||
路径安全:沙箱端会对路径做 canonicalize + starts_with(base) 校验,
|
||||
防止目录穿越到沙箱 base 之外。
|
||||
"""
|
||||
# 1) 推导 MIME 类型
|
||||
mime_type, _ = mimetypes.guess_type(path)
|
||||
if mime_type is None:
|
||||
mime_type = "application/octet-stream"
|
||||
|
||||
# 2) 从沙箱读取
|
||||
try:
|
||||
result = await runtime.read_sandbox_file(
|
||||
path=path,
|
||||
run_id=run_id,
|
||||
character_id=character_id,
|
||||
isolated=isolated,
|
||||
)
|
||||
except SandboxNotFound:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail={"error": {"code": "file_not_found", "message": f"文件不存在: {path}"}},
|
||||
) from None
|
||||
except SandboxError as exc:
|
||||
raise HTTPException(
|
||||
status_code=502,
|
||||
detail={
|
||||
"error": {
|
||||
"code": "sandbox_error",
|
||||
"message": f"沙箱文件读取失败: {exc}",
|
||||
}
|
||||
},
|
||||
) from None
|
||||
|
||||
# 3) base64url-no-pad → raw bytes
|
||||
pad_len = -len(result.content_b64) % 4
|
||||
content = base64.urlsafe_b64decode(result.content_b64 + "=" * pad_len)
|
||||
|
||||
return Response(content=content, media_type=mime_type)
|
||||
|
||||
|
||||
@router.get("/files")
|
||||
async def list_sandbox_files(
|
||||
path: str = Query(
|
||||
".",
|
||||
description="目录路径。默认工作区根。",
|
||||
),
|
||||
run_id: str | None = Query(None, description="关联的 run_id(可选)"),
|
||||
character_id: str | None = Query(
|
||||
None,
|
||||
description="人物 ID。提供时自动应用 isolated_workspace 隔离前缀。",
|
||||
),
|
||||
isolated: bool = Query(True, description="是否自动补全隔离前缀"),
|
||||
runtime: ToolRuntime = Depends(get_tool_runtime),
|
||||
_current_user: CurrentUser = Depends(get_current_user),
|
||||
) -> list[dict[str, object]]:
|
||||
"""列出沙箱工作区目录内容。
|
||||
|
||||
返回有序列表,每个条目含 ``name`` / ``kind``(``"file"`` | ``"dir"``)/ ``size``。
|
||||
适用于前端文件浏览器导航。
|
||||
"""
|
||||
try:
|
||||
entries = await runtime.list_sandbox_dir(
|
||||
path=path,
|
||||
run_id=run_id,
|
||||
character_id=character_id,
|
||||
isolated=isolated,
|
||||
)
|
||||
except SandboxNotFound:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail={"error": {"code": "dir_not_found", "message": f"目录不存在: {path}"}},
|
||||
) from None
|
||||
except SandboxError as exc:
|
||||
raise HTTPException(
|
||||
status_code=502,
|
||||
detail={
|
||||
"error": {
|
||||
"code": "sandbox_error",
|
||||
"message": f"沙箱目录读取失败: {exc}",
|
||||
}
|
||||
},
|
||||
) from None
|
||||
|
||||
return [{"name": e.name, "kind": e.kind, "size": e.size} for e in entries]
|
||||
Reference in new issue
Block a user