myagent-ai 1.23.29 → 1.23.31

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"}
@@ -275,8 +281,15 @@ class MainAgent(BaseAgent):
275
281
 
276
282
  logger.info(f"[{task_id}] 开始处理用户请求: {context.user_message[:100]}")
277
283
 
284
+ # [v1.23.30] 读取外部注入的 Agent 专属提示词(群聊、多Agent 场景使用)
285
+ # _try_model_chain_inner 通过此属性注入 group_context 等自定义提示词
286
+ _override_prompt = getattr(self, '_agent_override_prompt', None)
287
+
278
288
  try:
279
- return await self.process_v2(context)
289
+ return await self.process_v2(
290
+ context,
291
+ agent_override_prompt=_override_prompt,
292
+ )
280
293
  finally:
281
294
  # 移除活跃上下文
282
295
  self.active_contexts.pop(context.session_id, None)
@@ -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.31",
4
4
  "description": "本地桌面端执行型AI助手 - Open Interpreter 风格 | Local Desktop Execution-Oriented AI Assistant",
5
5
  "main": "main.py",
6
6
  "bin": {
@@ -811,6 +811,114 @@ function updateStreamingMessage(msgIdx) {
811
811
  }
812
812
  }
813
813
  }
814
+
815
+ // [v1.23.30] 文件卡片渲染(backward compat 路径 — 确保无 parts 时也能显示文件)
816
+ if (msg._files && msg._files.length > 0) {
817
+ var bcImageContainer = contentArea.querySelector(':scope > .msg-attachments-images');
818
+ var bcExistingFiles = contentArea.querySelectorAll(':scope > .msg-attachments-files');
819
+ var bcFileContainer = bcExistingFiles.length > 0 ? bcExistingFiles[0] : null;
820
+ var bcRenderedIds = (bcFileContainer ? bcFileContainer._renderedFileIds : []) || [];
821
+ var bcRenderedImageIds = (bcImageContainer ? bcImageContainer._renderedImageIds : []) || [];
822
+ for (var _bcFi = 0; _bcFi < msg._files.length; _bcFi++) {
823
+ var _bcF = msg._files[_bcFi];
824
+ var _bcFId = _bcF.id || _bcF.file_id || '';
825
+ if (bcRenderedIds.indexOf(_bcFId) >= 0 || bcRenderedImageIds.indexOf(_bcFId) >= 0) continue;
826
+ var _bcIsImg = _bcF.type && _bcF.type.indexOf('image/') === 0;
827
+ var _bcIsAud = _bcF.type && _bcF.type.indexOf('audio/') === 0;
828
+ var _bcIsVid = _bcF.type && _bcF.type.indexOf('video/') === 0;
829
+ if (_bcIsImg || _bcIsAud || _bcIsVid) {
830
+ if (!bcImageContainer) {
831
+ bcImageContainer = document.createElement('div');
832
+ bcImageContainer.className = 'msg-attachments msg-attachments-images';
833
+ bcImageContainer._renderedImageIds = [];
834
+ var bcBubble = contentArea.querySelector('.message-bubble');
835
+ if (bcBubble) {
836
+ contentArea.insertBefore(bcImageContainer, bcBubble);
837
+ } else {
838
+ contentArea.appendChild(bcImageContainer);
839
+ }
840
+ }
841
+ if (_bcIsImg && _bcFId) {
842
+ var _bcImgDiv = document.createElement('div');
843
+ _bcImgDiv.className = 'msg-image-wrapper agent-image';
844
+ _bcImgDiv.innerHTML = '<img src="/api/file/' + _bcFId + '" class="msg-image" loading="lazy" alt="' + escapeHtml(_bcF.name || 'image') + '" onclick="openFileViewer(\'' + _bcFId + '\', this.src, \'' + escapeHtml(_bcF.name) + '\')" />';
845
+ bcImageContainer.appendChild(_bcImgDiv);
846
+ } else if (_bcIsAud && _bcFId) {
847
+ var _bcAudDiv = document.createElement('div');
848
+ _bcAudDiv.className = 'msg-media-player';
849
+ _bcAudDiv.innerHTML = '<audio controls src="/api/file/' + _bcFId + '" style="width:100%;max-width:480px" preload="metadata"></audio><div class="msg-media-title">' + escapeHtml(_bcF.name || '音频') + '</div>';
850
+ bcImageContainer.appendChild(_bcAudDiv);
851
+ } else if (_bcIsVid && _bcFId) {
852
+ var _bcVidDiv = document.createElement('div');
853
+ _bcVidDiv.className = 'msg-media-player';
854
+ _bcVidDiv.innerHTML = '<video controls src="/api/file/' + _bcFId + '" style="width:100%;max-width:640px;border-radius:8px" preload="metadata"></video><div class="msg-media-title">' + escapeHtml(_bcF.name || '视频') + '</div>';
855
+ bcImageContainer.appendChild(_bcVidDiv);
856
+ }
857
+ bcImageContainer._renderedImageIds.push(_bcFId);
858
+ continue;
859
+ }
860
+ if (!bcFileContainer) {
861
+ bcFileContainer = document.createElement('div');
862
+ bcFileContainer.className = 'msg-attachments msg-attachments-files';
863
+ bcFileContainer._renderedFileIds = [];
864
+ var bcBubble2 = contentArea.querySelector('.message-bubble');
865
+ if (bcBubble2) {
866
+ bcBubble2.parentNode.insertBefore(bcFileContainer, bcBubble2.nextSibling);
867
+ } else {
868
+ contentArea.appendChild(bcFileContainer);
869
+ }
870
+ }
871
+ var _bcIcon = _getFileIcon(_bcF.name || _bcF.type || '');
872
+ var _bcSizeStr = _bcF.size ? formatFileSize(_bcF.size) : '';
873
+ var _bcFDiv = document.createElement('div');
874
+ _bcFDiv.className = 'msg-file-item agent-file';
875
+ _bcFDiv.title = '点击预览';
876
+ _bcFDiv.innerHTML = '<span class="msg-file-icon">' + _bcIcon + '</span>' +
877
+ '<span class="msg-file-info"><span class="msg-file-name">' + escapeHtml(_bcF.name) + '</span>' +
878
+ (_bcSizeStr ? '<span class="msg-file-size">' + _bcSizeStr + '</span>' : '') +
879
+ '</span>' +
880
+ '<span class="msg-file-actions">' +
881
+ '<a class="msg-file-download" href="/api/file/' + (_bcFId || '') + '?name=' + encodeURIComponent(_bcF.name || 'file') + '" download="' + escapeHtml(_bcF.name) + '" title="下载" onclick="event.stopPropagation()">⬇</a>' +
882
+ '</span>';
883
+ bcFileContainer.appendChild(_bcFDiv);
884
+ bcFileContainer._renderedFileIds.push(_bcFId);
885
+ }
886
+ }
887
+
888
+ // [v1.23.30] 在线媒体嵌入渲染(backward compat 路径)
889
+ if (msg._media && msg._media.length > 0) {
890
+ var bcExistingMedia = contentArea.querySelectorAll(':scope > .msg-attachments-media');
891
+ var bcMediaContainer = bcExistingMedia.length > 0 ? bcExistingMedia[0] : null;
892
+ if (!bcMediaContainer) {
893
+ bcMediaContainer = document.createElement('div');
894
+ bcMediaContainer.className = 'msg-attachments msg-attachments-media';
895
+ var bcBubble3 = contentArea.querySelector('.message-bubble');
896
+ if (bcBubble3) {
897
+ contentArea.insertBefore(bcMediaContainer, bcBubble3);
898
+ } else {
899
+ contentArea.appendChild(bcMediaContainer);
900
+ }
901
+ }
902
+ var bcRenderedMediaUrls = bcMediaContainer._renderedMediaUrls || [];
903
+ for (var _bcMi = 0; _bcMi < msg._media.length; _bcMi++) {
904
+ var _bcM = msg._media[_bcMi];
905
+ var _bcMUrl = _bcM.embed_url || _bcM.original_url || '';
906
+ if (!_bcMUrl || bcRenderedMediaUrls.indexOf(_bcMUrl) >= 0) continue;
907
+ var _bcMIsAud = _bcM.media_type === 'audio';
908
+ var _bcMTitle = _bcM.title || (_bcMIsAud ? '在线音乐' : '在线视频');
909
+ var _bcMDiv = document.createElement('div');
910
+ _bcMDiv.className = 'msg-media-embed' + (_bcMIsAud ? ' msg-media-audio' : ' msg-media-video');
911
+ if (_bcM.embed_url) {
912
+ _bcMDiv.innerHTML = '<div class="msg-media-header"><span class="msg-media-icon">' + (_bcMIsAud ? '🎵' : '🎬') + '</span><span class="msg-media-label">' + escapeHtml(_bcMTitle) + '</span></div><iframe src="' + escapeHtml(_bcMUrl) + '" style="width:100%;max-width:' + (_bcMIsAud ? '480' : '640') + 'px;height:' + (_bcMIsAud ? '80' : '360') + 'px;border:none;border-radius:8px" loading="lazy" allow="autoplay;encrypted-media;picture-in-picture" allowfullscreen></iframe>';
913
+ } else {
914
+ _bcMDiv.style.cssText = 'padding:10px 14px;border-radius:8px;background:rgba(255,255,255,0.06);border:1px solid rgba(255,255,255,0.1);cursor:pointer';
915
+ _bcMDiv.innerHTML = '<div class="msg-media-header"><span class="msg-media-icon">' + (_bcMIsAud ? '🎵' : '🎬') + '</span><span class="msg-media-label">' + escapeHtml(_bcMTitle) + '</span></div>';
916
+ }
917
+ bcMediaContainer.appendChild(_bcMDiv);
918
+ bcRenderedMediaUrls.push(_bcMUrl);
919
+ }
920
+ bcMediaContainer._renderedMediaUrls = bcRenderedMediaUrls;
921
+ }
814
922
  }
815
923
 
816
924
  // Update streaming indicator