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.
- package/agents/main_agent.py +14 -1
- package/core/tool_dispatcher.py +70 -29
- package/package.json +1 -1
- package/web/ui/chat/flow_engine.js +108 -0
package/agents/main_agent.py
CHANGED
|
@@ -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(
|
|
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)
|
package/core/tool_dispatcher.py
CHANGED
|
@@ -197,23 +197,13 @@ class ToolDispatcher:
|
|
|
197
197
|
try:
|
|
198
198
|
p = _P(send_path)
|
|
199
199
|
if p.exists():
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
|
207
|
-
|
|
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
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
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":
|
|
325
|
-
"
|
|
326
|
-
"
|
|
327
|
-
"
|
|
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
|
-
|
|
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
|
@@ -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
|