feat(agent): Agent Files / agent Cloud storage — api backend (ENG-589) (#37172)

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
zyssyz123
2026-06-09 04:01:05 +00:00
committed by GitHub
co-authored by Claude Opus 4.8 autofix-ci[bot]
parent 789698cddd
commit a80bba2c35
40 changed files with 2730 additions and 166 deletions
@@ -0,0 +1,80 @@
"""Inner API for the agent drive (agent 网盘) control plane — ENG-591.
Two endpoints, called by the dify-agent server (not the sandbox) with the inner
API key. The drive ref is the URL segment ``agent-<agent_id>``; the path-like
file key travels in the query/body, never as a URL path segment (so its ``/``
characters do not collide with routing). Drive-owned semantics: tenant scoped,
no user-level FileAccessScope.
"""
from flask import request
from flask_restx import Resource
from pydantic import BaseModel, ValidationError
from controllers.console.wraps import setup_required
from controllers.inner_api import inner_api_ns
from controllers.inner_api.wraps import plugin_inner_api_only
from services.agent_drive_service import (
AgentDriveError,
AgentDriveService,
DriveCommitItem,
parse_agent_drive_ref,
)
class _CommitRequest(BaseModel):
tenant_id: str
user_id: str
items: list[DriveCommitItem]
def _error_response(exc: AgentDriveError) -> tuple[dict[str, str], int]:
return {"code": exc.code, "message": exc.message}, exc.status_code
@inner_api_ns.route("/drive/<string:drive_ref>/manifest")
class AgentDriveManifestApi(Resource):
@setup_required
@plugin_inner_api_only
@inner_api_ns.doc("agent_drive_manifest")
@inner_api_ns.doc(description="List an agent drive (optionally with download URLs)")
def get(self, drive_ref: str):
try:
agent_id = parse_agent_drive_ref(drive_ref)
tenant_id = (request.args.get("tenant_id") or "").strip()
if not tenant_id:
raise AgentDriveError("missing_tenant_id", "tenant_id is required", status_code=400)
include_download_url = (request.args.get("include_download_url") or "").lower() in ("1", "true", "yes")
items = AgentDriveService().manifest(
tenant_id=tenant_id,
agent_id=agent_id,
prefix=request.args.get("prefix", ""),
include_download_url=include_download_url,
)
except AgentDriveError as exc:
return _error_response(exc)
return {"items": items}
@inner_api_ns.route("/drive/<string:drive_ref>/commit")
class AgentDriveCommitApi(Resource):
@setup_required
@plugin_inner_api_only
@inner_api_ns.doc("agent_drive_commit")
@inner_api_ns.doc(description="Commit a batch of file refs into an agent drive")
def post(self, drive_ref: str):
try:
agent_id = parse_agent_drive_ref(drive_ref)
try:
body = _CommitRequest.model_validate(request.get_json(silent=True) or {})
except ValidationError as exc:
raise AgentDriveError("invalid_request", str(exc), status_code=400) from exc
items = AgentDriveService().commit(
tenant_id=body.tenant_id,
user_id=body.user_id,
agent_id=agent_id,
items=body.items,
)
except AgentDriveError as exc:
return _error_response(exc)
return {"items": items}
@@ -25,6 +25,7 @@ from core.plugin.entities.request import (
RequestInvokeTextEmbedding,
RequestInvokeTool,
RequestInvokeTTS,
RequestRequestDownloadFile,
RequestRequestUploadFile,
)
from core.tools.entities.tool_entities import ToolProviderType
@@ -33,6 +34,7 @@ from graphon.model_runtime.utils.encoders import jsonable_encoder
from libs.helper import length_prefixed_response
from models import Account, Tenant
from models.model import EndUser
from services.agent_file_request_service import AgentFileDownloadRequestService, FileDownloadRequestError
@inner_api_ns.route("/invoke/llm")
@@ -429,6 +431,36 @@ class PluginUploadFileRequestApi(Resource):
return BaseBackwardsInvocationResponse(data={"url": url}).model_dump()
@inner_api_ns.route("/download/file/request")
class PluginDownloadFileRequestApi(Resource):
@get_user_tenant
@setup_required
@plugin_inner_api_only
@plugin_data(payload_type=RequestRequestDownloadFile)
@inner_api_ns.doc("plugin_download_file_request")
@inner_api_ns.doc(description="Request a signed download URL for a workflow file ref")
@inner_api_ns.doc(
responses={
200: "Signed download URL generated successfully",
400: "Invalid access context or file mapping",
401: "Unauthorized - invalid API key",
404: "File not accessible to the tenant/user",
}
)
def post(self, user_model: Account | EndUser, tenant_model: Tenant, payload: RequestRequestDownloadFile):
try:
data = AgentFileDownloadRequestService.resolve(
tenant_id=tenant_model.id,
user_id=user_model.id,
user_from=payload.user_from,
invoke_from=payload.invoke_from,
file_mapping=payload.file,
)
except FileDownloadRequestError as exc:
return BaseBackwardsInvocationResponse(error=exc.message).model_dump(), exc.status_code
return BaseBackwardsInvocationResponse(data=data).model_dump()
@inner_api_ns.route("/fetch/app/info")
class PluginFetchAppInfoApi(Resource):
@get_user_tenant