"""send_file:在对话/工作流场景下投递一份文件给用户。 regulatory_node 直接对话场景下用此工具把生成的文件发给用户: - 工作流场景(带 trace_id):写入 data/artifact//,前端通过 SSE 收到带下载链接的卡片。 - 直接对话场景(无 trace_id):退化为把文件内容拼回字符串返回给 agent, 让 agent 再以代码块形式吐给用户。 """ import json import re import uuid from pathlib import Path from kilostar.utils.ray_hook import ray_actor_hook from kilostar.utils.settings import get_artifact_dir _SAFE_NAME_RE = re.compile(r"[^A-Za-z0-9._-]+") def _sanitize_filename(name: str) -> str: name = name.strip().replace("\\", "/").split("/")[-1] name = _SAFE_NAME_RE.sub("_", name) return name or "file" async def send_file(filename: str, content: str, trace_id: str = "") -> str: """把 agent 生成的文件作为附件投递给用户。 Args: filename: 文件名(含扩展名),如 "report.md" / "main.py" content: 文件内容(UTF-8 文本) trace_id: 当前会话/工作流的 trace_id;为空时退化为直接返回内容 Returns: 发送结果说明或文件内容 """ if not trace_id: return f"文件 {filename} 内容如下:\n\n```\n{content}\n```" safe_name = _sanitize_filename(filename) artifact_id = uuid.uuid4().hex[:12] trace_dir: Path = get_artifact_dir() / trace_id trace_dir.mkdir(parents=True, exist_ok=True) file_path = trace_dir / f"{artifact_id}_{safe_name}" file_path.write_text(content, encoding="utf-8") payload = json.dumps( { "type": "file", "filename": safe_name, "artifact_id": artifact_id, "url": f"/api/v1/resource/artifact/{trace_id}/{artifact_id}", "size": len(content.encode("utf-8")), }, ensure_ascii=False, ) actor_list = ray_actor_hook("global_workflow_manager") await actor_list.global_workflow_manager.put_pending.remote( trace_id, f"__FILE__{payload}" ) return f"已发送文件: {safe_name}"