fix max-bot, add tests
This commit is contained in:
parent
7abbaf7e7a
commit
2ad1438e1c
17 changed files with 1621 additions and 494 deletions
|
|
@ -1,51 +1,88 @@
|
|||
"""File handling for MAX surface."""
|
||||
import os
|
||||
import aiohttp
|
||||
"""Incoming / outgoing file helpers for MAX (aligned with Matrix workspace layout)."""
|
||||
from __future__ import annotations
|
||||
|
||||
import mimetypes
|
||||
from pathlib import Path
|
||||
|
||||
import httpx
|
||||
|
||||
class FileHandler:
|
||||
def __init__(self, workspace_root: str):
|
||||
self.workspace_root = workspace_root
|
||||
|
||||
def _make_unique_filename(self, directory: str, filename: str) -> str:
|
||||
base = Path(filename).stem
|
||||
ext = Path(filename).suffix
|
||||
candidate = filename
|
||||
counter = 1
|
||||
while os.path.exists(os.path.join(directory, candidate)):
|
||||
candidate = f"{base} ({counter}){ext}"
|
||||
counter += 1
|
||||
return candidate
|
||||
def guess_upload_type(mime_type: str | None, *, attachment_type: str) -> str:
|
||||
if attachment_type == "image":
|
||||
return "image"
|
||||
if attachment_type == "video":
|
||||
return "video"
|
||||
if attachment_type == "audio":
|
||||
return "audio"
|
||||
mime = mime_type or ""
|
||||
if mime.startswith("image/"):
|
||||
return "image"
|
||||
if mime.startswith("video/"):
|
||||
return "video"
|
||||
if mime.startswith("audio/"):
|
||||
return "audio"
|
||||
return "file"
|
||||
|
||||
async def download_attachment(
|
||||
self,
|
||||
download_url: str,
|
||||
filename: str,
|
||||
agent_workspace: str,
|
||||
headers: dict = None,
|
||||
) -> str:
|
||||
full_dir = os.path.join(self.workspace_root, agent_workspace.strip("/"))
|
||||
os.makedirs(full_dir, exist_ok=True)
|
||||
|
||||
unique_name = self._make_unique_filename(full_dir, filename)
|
||||
filepath = os.path.join(full_dir, unique_name)
|
||||
async def save_incoming_from_url(
|
||||
*,
|
||||
api: MaxBotApi,
|
||||
workspace_root: Path,
|
||||
filename: str,
|
||||
url: str,
|
||||
) -> str:
|
||||
data = await api.download_file(url)
|
||||
workspace_root.mkdir(parents=True, exist_ok=True)
|
||||
relative_path, absolute_path = build_agent_workspace_path(
|
||||
workspace_root=workspace_root,
|
||||
filename=filename,
|
||||
)
|
||||
absolute_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
absolute_path.write_bytes(data)
|
||||
return relative_path
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(download_url, headers=headers) as resp:
|
||||
resp.raise_for_status()
|
||||
with open(filepath, "wb") as f:
|
||||
f.write(await resp.read())
|
||||
|
||||
return unique_name
|
||||
async def upload_file_as_attachment(
|
||||
api: MaxBotApi,
|
||||
*,
|
||||
filename: str,
|
||||
content: bytes,
|
||||
upload_type: str,
|
||||
) -> dict:
|
||||
meta = await api.get_upload_url(upload_type)
|
||||
upload_url = meta.get("url")
|
||||
token = meta.get("token")
|
||||
if not isinstance(upload_url, str) or not upload_url:
|
||||
raise RuntimeError("MAX uploads response missing url")
|
||||
|
||||
def read_outgoing_file(self, workspace_path: str, agent_workspace: str) -> bytes:
|
||||
full_dir = os.path.join(self.workspace_root, agent_workspace.strip("/"))
|
||||
filepath = os.path.join(full_dir, workspace_path.lstrip("/"))
|
||||
with open(filepath, "rb") as f:
|
||||
return f.read()
|
||||
async with httpx.AsyncClient(timeout=httpx.Timeout(120.0)) as client:
|
||||
response = await client.post(
|
||||
upload_url,
|
||||
files={"data": (filename, content, guess_mimetype(filename))},
|
||||
)
|
||||
response.raise_for_status()
|
||||
|
||||
def file_exists(self, workspace_path: str, agent_workspace: str) -> bool:
|
||||
full_dir = os.path.join(self.workspace_root, agent_workspace.strip("/"))
|
||||
filepath = os.path.join(full_dir, workspace_path.lstrip("/"))
|
||||
return os.path.exists(filepath)
|
||||
payload: dict = {}
|
||||
if token:
|
||||
payload["token"] = token
|
||||
if upload_type == "image":
|
||||
return {"type": "image", "payload": payload}
|
||||
|
||||
type_map = {
|
||||
"file": "file",
|
||||
"video": "video",
|
||||
"audio": "audio",
|
||||
}
|
||||
mapped = type_map.get(upload_type, "file")
|
||||
return {"type": mapped, "payload": payload}
|
||||
|
||||
|
||||
def guess_mimetype(filename: str) -> str:
|
||||
mime, _ = mimetypes.guess_type(filename)
|
||||
return mime or "application/octet-stream"
|
||||
|
||||
|
||||
def read_workspace_bytes(workspace_path: str | Path, *, agent_workspace: str) -> bytes:
|
||||
root = Path(agent_workspace)
|
||||
resolved = resolve_workspace_attachment_path(root, str(workspace_path))
|
||||
return resolved.read_bytes()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue