myagent-ai 1.23.29 → 1.23.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -158,6 +158,12 @@ class MainAgent(BaseAgent):
158
158
  <tool><toolname>command</toolname><parms>{"command": "myagent-ai search xxx && myagent-ai read-url https://..."}</parms><timeout>30</timeout></tool>
159
159
  <tool><toolname>command</toolname><parms>{"command": "myagent-ai sysinfo && myagent-ai ps --filter python"}</parms><timeout>15</timeout></tool>
160
160
 
161
+ **file_send**(向用户发送文件,文件会以卡片形式显示在聊天中):
162
+ <tool><toolname>file_send</toolname><parms>{"file_path": "文件的绝对路径", "description": "文件描述(可选)"}</parms><timeout>30</timeout></tool>
163
+ - 当你需要把生成的文件(PDF、Excel、图片、脚本等)发送给用户时,直接使用此工具
164
+ - 当你需要发送一个已存在的文件时,直接使用此工具
165
+ - 不要把文件路径当成文本展示给用户,而是用 file_send 工具发送文件卡片
166
+
161
167
  **web_control**(网页控制器,在聊天中打开可操作的浏览器面板):
162
168
  <tool><toolname>web_control</toolname><parms>{"action": "open", "url": "https://example.com"}</parms><timeout>30</timeout></tool>
163
169
  - 打开: {"action": "open", "url": "URL"}
@@ -197,23 +197,13 @@ class ToolDispatcher:
197
197
  try:
198
198
  p = _P(send_path)
199
199
  if p.exists():
200
- from skills.file_send import FileSendSkill
201
- fskill = FileSendSkill()
202
- fresult = await fskill.execute(
203
- str(p), send_desc or f"文件: {p.name}",
204
- stream_callback=stream_callback,
200
+ # [v1.23.29] 直接通过 _exec_file_send 发送(统一入口,确保 v2_file 推送)
201
+ file_result = await self._exec_file_send(
202
+ {"file_path": send_path, "description": send_desc},
203
+ task_id, stream_callback, sent_files,
205
204
  )
206
- if fresult.get("success"):
207
- logger.info(f"[{task_id}] CLI 自动发送文件: {p.name}")
208
- if sent_files is not None and fresult.get("file_id"):
209
- sent_files.append({
210
- "id": fresult["file_id"],
211
- "name": fresult.get("name", ""),
212
- "size": fresult.get("size", 0),
213
- "url": fresult.get("url", ""),
214
- })
215
- else:
216
- result["output"] += f"\n[文件发送失败: {fresult.get('error', '')}]"
205
+ if not file_result.get("success"):
206
+ result["output"] += f"\n[文件发送失败: {file_result.get('error', '')}]"
217
207
  except Exception as e:
218
208
  logger.warning(f"[{task_id}] CLI 文件发送异常: {e}")
219
209
  result["output"] += f"\n[文件发送异常: {e}]"
@@ -303,7 +293,7 @@ class ToolDispatcher:
303
293
  stream_callback: Optional[Callable] = None,
304
294
  sent_files: Optional[List[Dict]] = None,
305
295
  ) -> Dict:
306
- """发送文件给用户"""
296
+ """发送文件给用户 — 后端推送 v2_file SSE 事件 + 持久化到聊天记录"""
307
297
  try:
308
298
  from skills.file_send import FileSendSkill
309
299
  fskill = FileSendSkill()
@@ -313,20 +303,71 @@ class ToolDispatcher:
313
303
  logger.warning(f"[{task_id}] file_send: 缺少 file_path 参数")
314
304
  return {"success": False, "error": "缺少 file_path 参数,请提供要发送的文件路径"}
315
305
  logger.info(f"[{task_id}] file_send: 发送文件 {fpath}")
316
- fresult = await fskill.execute(fpath, fdesc, stream_callback=stream_callback)
317
- if not fresult.get("success"):
318
- logger.warning(f"[{task_id}] file_send: 发送失败 - {fresult.get('error', '')}")
319
- else:
320
- logger.info(f"[{task_id}] file_send: 发送成功 {fresult.get('file_id', '')} → {fresult.get('name', '')}")
321
- result = {"success": True, "output": json.dumps(fresult, ensure_ascii=False, indent=2), "data": fresult}
322
- if sent_files is not None and fresult.get("success") and fresult.get("file_id"):
306
+
307
+ # [v1.23.29] 先复制文件(不依赖 file_send.execute 的 SSE 发送)
308
+ from pathlib import Path as _P
309
+ import shutil, uuid as _uuid, time as _time
310
+ fpath_resolved = _P(fpath.strip().strip("'\"")).expanduser()
311
+ if not fpath_resolved.exists():
312
+ return {"success": False, "error": f"文件不存在: {fpath}"}
313
+ if not fpath_resolved.is_file():
314
+ return {"success": False, "error": f"不是文件: {fpath}"}
315
+
316
+ file_id = str(_uuid.uuid4())[:12]
317
+ date_dir = fskill.UPLOADS_DIR / _time.strftime("%Y-%m")
318
+ date_dir.mkdir(parents=True, exist_ok=True)
319
+ stored_name = f"{file_id}_{fpath_resolved.name}"
320
+ stored_path = date_dir / stored_name
321
+ shutil.copy2(str(fpath_resolved), str(stored_path))
322
+
323
+ mime_map = {
324
+ ".pdf": "application/pdf", ".png": "image/png", ".jpg": "image/jpeg",
325
+ ".jpeg": "image/jpeg", ".gif": "image/gif", ".webp": "image/webp",
326
+ ".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
327
+ ".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
328
+ ".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
329
+ ".txt": "text/plain", ".csv": "text/csv", ".md": "text/markdown",
330
+ ".json": "application/json", ".html": "text/html",
331
+ ".mp3": "audio/mpeg", ".mp4": "video/mp4", ".wav": "audio/wav",
332
+ ".zip": "application/zip", ".tar.gz": "application/gzip",
333
+ }
334
+ mime = mime_map.get(fpath_resolved.suffix.lower(), "application/octet-stream")
335
+ size = stored_path.stat().st_size
336
+
337
+ file_data = {
338
+ "id": file_id,
339
+ "file_id": file_id,
340
+ "name": fpath_resolved.name,
341
+ "type": mime,
342
+ "size": size,
343
+ "description": fdesc or f"文件: {fpath_resolved.name}",
344
+ "url": f"/api/file/{file_id}?name={fpath_resolved.name}",
345
+ "download_url": f"/api/file/{file_id}/download?name={fpath_resolved.name}",
346
+ }
347
+
348
+ # [v1.23.29] 关键:通过 _emit_sse 后端推送 v2_file 事件到前端
349
+ # 这是文件卡片显示的核心机制 — 不依赖 file_send.execute 内部的 SSE 发送
350
+ await self._emit_sse("v2_file", file_data, stream_callback)
351
+ logger.info(f"[{task_id}] file_send: v2_file 已推送 → {file_id} ({fpath_resolved.name})")
352
+
353
+ # 持久化到 sent_files(写入聊天记录数据库)
354
+ if sent_files is not None:
323
355
  sent_files.append({
324
- "id": fresult["file_id"],
325
- "name": fresult.get("name", ""),
326
- "type": fresult.get("type", ""),
327
- "size": fresult.get("size", 0),
356
+ "id": file_id,
357
+ "file_id": file_id,
358
+ "name": fpath_resolved.name,
359
+ "type": mime,
360
+ "size": size,
361
+ "description": fdesc or f"文件: {fpath_resolved.name}",
362
+ "url": file_data["url"],
363
+ "download_url": file_data["download_url"],
328
364
  })
329
- return result
365
+
366
+ return {
367
+ "success": True,
368
+ "output": f"文件已发送: {fpath_resolved.name} (ID: {file_id}, 大小: {size} bytes)",
369
+ "data": file_data,
370
+ }
330
371
  except Exception as e:
331
372
  logger.error(f"[{task_id}] file_send: 异常 - {e}", exc_info=True)
332
373
  return {"success": False, "error": f"文件发送失败: {e}"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "myagent-ai",
3
- "version": "1.23.29",
3
+ "version": "1.23.30",
4
4
  "description": "本地桌面端执行型AI助手 - Open Interpreter 风格 | Local Desktop Execution-Oriented AI Assistant",
5
5
  "main": "main.py",
6
6
  "bin": {