mirror of
https://github.com/hansjone/oclaw.git
synced 2026-10-09 03:30:48 +08:00
Generation returns attachment_id only; channel delivery still requires save_deliverable_attachment. Co-authored-by: Cursor <cursoragent@cursor.com>
108 lines
4.1 KiB
Python
108 lines
4.1 KiB
Python
from __future__ import annotations
|
|
|
|
import mimetypes
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
from runtime.tools.base import ToolSpec
|
|
from runtime.tools.path_guard import resolve_workspace_path
|
|
from svc.files.attachment_assets import AttachmentAssetStore
|
|
|
|
|
|
def _guess_mime(path: Path, override: str) -> str:
|
|
if override:
|
|
return override
|
|
guessed, _ = mimetypes.guess_type(str(path))
|
|
return guessed or "application/octet-stream"
|
|
|
|
|
|
def save_deliverable_attachment_tool() -> ToolSpec:
|
|
def _handler(args: dict[str, Any]) -> dict[str, Any]:
|
|
attachment_id = str(args.get("attachment_id") or "").strip().lower()
|
|
raw_path = str(args.get("path") or "").strip().strip('"').strip("'")
|
|
display_name = str(args.get("name") or "").strip()
|
|
mime_override = str(args.get("mime") or "").strip()
|
|
|
|
if attachment_id:
|
|
store = AttachmentAssetStore()
|
|
meta = store.get_meta(attachment_id)
|
|
if meta is None:
|
|
return {"ok": False, "error": "attachment_not_found", "attachment_id": attachment_id}
|
|
name = display_name or meta.name
|
|
return {
|
|
"ok": True,
|
|
"attachment_id": meta.attachment_id,
|
|
"name": name,
|
|
"mime": mime_override or meta.mime,
|
|
"bytes": meta.bytes,
|
|
"width": meta.width,
|
|
"height": meta.height,
|
|
"deliverable": True,
|
|
}
|
|
|
|
if not raw_path:
|
|
return {"ok": False, "error": "path_or_attachment_id_required"}
|
|
try:
|
|
p = resolve_workspace_path(raw_path)
|
|
except ValueError as exc:
|
|
return {"ok": False, "error": str(exc)}
|
|
if not p.exists() or not p.is_file():
|
|
return {"ok": False, "error": "file_not_found", "path": str(p)}
|
|
filename = display_name or p.name
|
|
mime = _guess_mime(p, mime_override)
|
|
try:
|
|
data = p.read_bytes()
|
|
except Exception as exc:
|
|
return {"ok": False, "error": "read_failed", "detail": str(exc)}
|
|
meta = AttachmentAssetStore().save_bytes(data, filename=filename, mime=mime)
|
|
return {
|
|
"ok": True,
|
|
"attachment_id": meta.attachment_id,
|
|
"name": meta.name,
|
|
"mime": meta.mime,
|
|
"bytes": meta.bytes,
|
|
"deliverable": True,
|
|
}
|
|
|
|
return ToolSpec(
|
|
name="save_deliverable_attachment",
|
|
description=(
|
|
"Mark an attachment for outbound channel delivery (WhatsApp/WeChat). "
|
|
"Required before the user receives any generated file, image, or video in a messaging channel. "
|
|
"Prefer attachment_id for assets already in the store (write_xlsx, cloudflare_image_generate, etc.). "
|
|
"Use path for workspace files (after write_file or run_command). "
|
|
"Generating content alone does not send attachments to the channel."
|
|
),
|
|
parameters={
|
|
"type": "object",
|
|
"properties": {
|
|
"path": {
|
|
"type": "string",
|
|
"description": "Workspace file path (relative to workspace root or allowed absolute path).",
|
|
},
|
|
"attachment_id": {
|
|
"type": "string",
|
|
"description": (
|
|
"Existing attachment_id from generation tools "
|
|
"(write_xlsx, cloudflare_image_generate, image_edit, etc.)."
|
|
),
|
|
},
|
|
"name": {
|
|
"type": "string",
|
|
"description": "Optional download filename shown to the user.",
|
|
},
|
|
"mime": {
|
|
"type": "string",
|
|
"description": "Optional MIME type override.",
|
|
},
|
|
},
|
|
"additionalProperties": False,
|
|
},
|
|
handler=_handler,
|
|
tags=frozenset({"public", "workspace", "attachment", "channel"}),
|
|
read_only=False,
|
|
risk_level="low",
|
|
)
|
|
|
|
|
|
__all__ = ["save_deliverable_attachment_tool"]
|