myagent-ai 1.16.17 → 1.16.18
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 +23 -0
- package/data/uploads/2026-04/32c9e9d3-8cf_test.pdf.pdf +1 -0
- package/package.json +1 -1
- package/skills/file_send.py +95 -0
- package/web/api_server.py +130 -6
- package/web/ui/chat/chat.css +4 -0
- package/web/ui/chat/chat_main.js +69 -10
- package/web/ui/chat/flow_engine.js +7 -0
package/agents/main_agent.py
CHANGED
|
@@ -73,6 +73,7 @@ class MainAgent(BaseAgent):
|
|
|
73
73
|
- **执行代码**: 用 `code` 工具(language: python/javascript/shell)
|
|
74
74
|
- **执行命令**: 用 `command` 或 `command_run` 工具
|
|
75
75
|
- **文件操作**: 用 `file_read` / `file_write` / `file_list` 等文件工具
|
|
76
|
+
- **发送文件给用户**: 用 `file_send` 工具(参数: file_path=文件路径, description=说明),当你生成或处理了文件需要返回给用户时使用
|
|
76
77
|
- **主动召回记忆**: 用 `recall_memory` 工具(参数: keyword=关键字, time_point=可选时间点如"2025-01", limit=数量默认5),根据关键字和时间搜索历史记忆
|
|
77
78
|
"""
|
|
78
79
|
|
|
@@ -552,11 +553,18 @@ class MainAgent(BaseAgent):
|
|
|
552
553
|
|
|
553
554
|
# 保存用户消息到会话记忆
|
|
554
555
|
if self.memory:
|
|
556
|
+
# [v1.16.17] 附加附件元数据到用户消息
|
|
557
|
+
_attachment_meta = {}
|
|
558
|
+
if context.metadata.get("user_image_files"):
|
|
559
|
+
_attachment_meta["images"] = context.metadata["user_image_files"]
|
|
560
|
+
if context.metadata.get("user_file_files"):
|
|
561
|
+
_attachment_meta["files"] = context.metadata["user_file_files"]
|
|
555
562
|
self.memory.add_session(
|
|
556
563
|
session_id=context.session_id,
|
|
557
564
|
role="user",
|
|
558
565
|
content=context.user_message,
|
|
559
566
|
key="user_input",
|
|
567
|
+
metadata=_attachment_meta if _attachment_meta else None,
|
|
560
568
|
)
|
|
561
569
|
|
|
562
570
|
# 加载相关记忆 (recall from previous round or initial load)
|
|
@@ -1561,6 +1569,21 @@ class MainAgent(BaseAgent):
|
|
|
1561
1569
|
result = {"success": False, "error": f"记忆召回失败: {re_err}"}
|
|
1562
1570
|
logger.warning(f"[{task_id}] recall_memory 工具异常: {re_err}")
|
|
1563
1571
|
|
|
1572
|
+
elif tool_name == "file_send":
|
|
1573
|
+
# [v1.16.17] 文件发送工具 — 让 Agent 向用户发送文件
|
|
1574
|
+
try:
|
|
1575
|
+
from skills.file_send import FileSendSkill
|
|
1576
|
+
_fskill = FileSendSkill()
|
|
1577
|
+
_fpath = params.get("file_path", "")
|
|
1578
|
+
_fdesc = params.get("description", "")
|
|
1579
|
+
# Extract stream_callback from context if available
|
|
1580
|
+
_stream_cb = getattr(context, '_stream_callback', None) if hasattr(self, '_current_stream_callback') else None
|
|
1581
|
+
_fresult = _fskill.execute(_fpath, _fdesc, stream_callback=_stream_cb)
|
|
1582
|
+
result = {"success": True, "output": json.dumps(_fresult, ensure_ascii=False, indent=2), "data": _fresult}
|
|
1583
|
+
except Exception as _fse:
|
|
1584
|
+
result = {"success": False, "error": f"文件发送失败: {_fse}"}
|
|
1585
|
+
logger.warning(f"[{task_id}] file_send 工具异常: {_fse}")
|
|
1586
|
+
|
|
1564
1587
|
elif self.skills:
|
|
1565
1588
|
exec_result = await self.skills.execute(tool_name, **params)
|
|
1566
1589
|
if exec_result is None:
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
%PDF-1.4 test content
|
package/package.json
CHANGED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""
|
|
2
|
+
skills/file_send.py - Agent 文件发送工具
|
|
3
|
+
========================================
|
|
4
|
+
让 Agent 可以向用户发送文件(图片、PDF、文档等)。
|
|
5
|
+
"""
|
|
6
|
+
import os
|
|
7
|
+
import json
|
|
8
|
+
import uuid
|
|
9
|
+
import time
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from core.logger import get_logger
|
|
12
|
+
|
|
13
|
+
logger = get_logger("myagent.skill.file_send")
|
|
14
|
+
|
|
15
|
+
UPLOADS_DIR = Path(__file__).parent.parent / "data" / "uploads"
|
|
16
|
+
UPLOADS_DIR.mkdir(parents=True, exist_ok=True)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class FileSendSkill:
|
|
20
|
+
"""文件发送技能 — Agent 可通过此技能向用户发送文件"""
|
|
21
|
+
|
|
22
|
+
name = "file_send"
|
|
23
|
+
description = "向用户发送文件。支持发送已存在的文件路径,或由代码生成的文件。"
|
|
24
|
+
parameters = {
|
|
25
|
+
"type": "object",
|
|
26
|
+
"properties": {
|
|
27
|
+
"file_path": {
|
|
28
|
+
"type": "string",
|
|
29
|
+
"description": "要发送的文件路径(绝对路径或相对路径)"
|
|
30
|
+
},
|
|
31
|
+
"description": {
|
|
32
|
+
"type": "string",
|
|
33
|
+
"description": "文件描述(可选)"
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"required": ["file_path"]
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
def execute(self, file_path: str, description: str = "", stream_callback=None) -> dict:
|
|
40
|
+
"""执行文件发送 — 将文件复制到上传目录,返回 file_id"""
|
|
41
|
+
file_path = file_path.strip().strip("'\"")
|
|
42
|
+
fpath = Path(file_path)
|
|
43
|
+
if not fpath.exists():
|
|
44
|
+
return {"success": False, "error": f"文件不存在: {file_path}"}
|
|
45
|
+
if not fpath.is_file():
|
|
46
|
+
return {"success": False, "error": f"不是文件: {file_path}"}
|
|
47
|
+
|
|
48
|
+
try:
|
|
49
|
+
file_id = str(uuid.uuid4())[:12]
|
|
50
|
+
date_dir = UPLOADS_DIR / time.strftime("%Y-%m")
|
|
51
|
+
date_dir.mkdir(parents=True, exist_ok=True)
|
|
52
|
+
stored_name = f"{file_id}_{fpath.name}"
|
|
53
|
+
stored_path = date_dir / stored_name
|
|
54
|
+
|
|
55
|
+
import shutil
|
|
56
|
+
shutil.copy2(str(fpath), str(stored_path))
|
|
57
|
+
|
|
58
|
+
mime_map = {
|
|
59
|
+
".pdf": "application/pdf", ".png": "image/png", ".jpg": "image/jpeg",
|
|
60
|
+
".jpeg": "image/jpeg", ".gif": "image/gif", ".webp": "image/webp",
|
|
61
|
+
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
62
|
+
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
63
|
+
".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
64
|
+
".txt": "text/plain", ".csv": "text/csv", ".md": "text/markdown",
|
|
65
|
+
".json": "application/json", ".html": "text/html",
|
|
66
|
+
}
|
|
67
|
+
mime = mime_map.get(fpath.suffix.lower(), "application/octet-stream")
|
|
68
|
+
size = stored_path.stat().st_size
|
|
69
|
+
|
|
70
|
+
result = {
|
|
71
|
+
"success": True,
|
|
72
|
+
"file_id": file_id,
|
|
73
|
+
"name": fpath.name,
|
|
74
|
+
"type": mime,
|
|
75
|
+
"size": size,
|
|
76
|
+
"description": description or f"文件: {fpath.name}",
|
|
77
|
+
"url": f"/api/file/{file_id}",
|
|
78
|
+
"download_url": f"/api/file/{file_id}/download",
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
# Send v2_file SSE event so frontend can display it
|
|
82
|
+
if stream_callback:
|
|
83
|
+
import asyncio
|
|
84
|
+
try:
|
|
85
|
+
loop = asyncio.get_running_loop()
|
|
86
|
+
loop.create_task(stream_callback({"type": "v2_file", "data": result}))
|
|
87
|
+
except RuntimeError:
|
|
88
|
+
pass
|
|
89
|
+
|
|
90
|
+
logger.info(f"文件发送成功: {fpath.name} -> {file_id}")
|
|
91
|
+
return result
|
|
92
|
+
|
|
93
|
+
except Exception as e:
|
|
94
|
+
logger.error(f"文件发送失败: {e}")
|
|
95
|
+
return {"success": False, "error": str(e)}
|
package/web/api_server.py
CHANGED
|
@@ -18,6 +18,62 @@ from web.tts_handler import synthesize, preprocess_for_tts, AVAILABLE_VOICES
|
|
|
18
18
|
|
|
19
19
|
logger = get_logger("myagent.api")
|
|
20
20
|
|
|
21
|
+
# [v1.16.17] 文件上传存储目录
|
|
22
|
+
UPLOADS_DIR = Path(__file__).parent.parent / "data" / "uploads"
|
|
23
|
+
UPLOADS_DIR.mkdir(parents=True, exist_ok=True)
|
|
24
|
+
|
|
25
|
+
import hashlib
|
|
26
|
+
|
|
27
|
+
def _save_upload_file(filename: str, mime_type: str, data: bytes) -> str:
|
|
28
|
+
"""Save uploaded file to disk, return file_id"""
|
|
29
|
+
file_id = str(uuid.uuid4())[:12]
|
|
30
|
+
# Organize by date: data/uploads/2024-01/
|
|
31
|
+
date_dir = UPLOADS_DIR / time.strftime("%Y-%m")
|
|
32
|
+
date_dir.mkdir(parents=True, exist_ok=True)
|
|
33
|
+
# Sanitize filename (remove extension first to avoid double .pdf.pdf)
|
|
34
|
+
stem = Path(filename).stem
|
|
35
|
+
safe_name = "".join(c for c in stem if c.isalnum() or c in "._- ").strip("._- ")
|
|
36
|
+
if not safe_name:
|
|
37
|
+
safe_name = "file"
|
|
38
|
+
ext = Path(filename).suffix.lower() or _ext_from_mime(mime_type)
|
|
39
|
+
stored_name = f"{file_id}_{safe_name}{ext}"
|
|
40
|
+
stored_path = date_dir / stored_name
|
|
41
|
+
stored_path.write_bytes(data)
|
|
42
|
+
return file_id
|
|
43
|
+
|
|
44
|
+
def _ext_from_mime(mime_type: str) -> str:
|
|
45
|
+
"""Get file extension from MIME type"""
|
|
46
|
+
mapping = {
|
|
47
|
+
"application/pdf": ".pdf", "image/png": ".png", "image/jpeg": ".jpg",
|
|
48
|
+
"image/gif": ".gif", "image/webp": ".webp",
|
|
49
|
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document": ".docx",
|
|
50
|
+
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": ".xlsx",
|
|
51
|
+
"application/vnd.openxmlformats-officedocument.presentationml.presentation": ".pptx",
|
|
52
|
+
"text/plain": ".txt", "text/csv": ".csv", "text/markdown": ".md",
|
|
53
|
+
"application/json": ".json", "text/html": ".html",
|
|
54
|
+
}
|
|
55
|
+
return mapping.get(mime_type, "")
|
|
56
|
+
|
|
57
|
+
def _find_upload_file(file_id: str):
|
|
58
|
+
"""Find uploaded file by file_id, return (path, mime_type_guess) or (None, None)"""
|
|
59
|
+
for date_dir in sorted(UPLOADS_DIR.iterdir(), reverse=True):
|
|
60
|
+
if not date_dir.is_dir():
|
|
61
|
+
continue
|
|
62
|
+
for f in date_dir.iterdir():
|
|
63
|
+
if f.name.startswith(file_id + "_"):
|
|
64
|
+
ext = f.suffix.lower()
|
|
65
|
+
mime_map = {
|
|
66
|
+
".pdf": "application/pdf", ".png": "image/png", ".jpg": "image/jpeg",
|
|
67
|
+
".jpeg": "image/jpeg", ".gif": "image/gif", ".webp": "image/webp",
|
|
68
|
+
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
69
|
+
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
70
|
+
".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
71
|
+
".txt": "text/plain", ".csv": "text/csv", ".md": "text/markdown",
|
|
72
|
+
".json": "application/json", ".html": "text/html",
|
|
73
|
+
}
|
|
74
|
+
return f, mime_map.get(ext, "application/octet-stream")
|
|
75
|
+
return None, None
|
|
76
|
+
|
|
21
77
|
def _now_iso():
|
|
22
78
|
"""返回配置时区的 ISO 时间戳"""
|
|
23
79
|
from core.utils import get_config_tz
|
|
@@ -415,6 +471,9 @@ class ApiServer:
|
|
|
415
471
|
r.add_post("/api/chat/save-to-knowledge", self.handle_save_to_knowledge)
|
|
416
472
|
r.add_get("/ui/", self.handle_ui_index)
|
|
417
473
|
r.add_get("/", self.handle_index)
|
|
474
|
+
# [v1.16.17] 文件服务 API
|
|
475
|
+
r.add_get('/api/file/{file_id}', self.handle_get_file)
|
|
476
|
+
r.add_get('/api/file/{file_id}/download', self.handle_download_file)
|
|
418
477
|
ui_dir = Path(__file__).parent / "ui"
|
|
419
478
|
if ui_dir.exists():
|
|
420
479
|
r.add_static("/ui", str(ui_dir), append_version=True)
|
|
@@ -596,6 +655,32 @@ class ApiServer:
|
|
|
596
655
|
|
|
597
656
|
return f"[不支持的文件格式: {filename} ({mime_type})]"
|
|
598
657
|
|
|
658
|
+
# --- File Serving (v1.16.17) ---
|
|
659
|
+
async def handle_get_file(self, request):
|
|
660
|
+
"""GET /api/file/{file_id} - 在新窗口中打开/预览文件"""
|
|
661
|
+
file_id = request.match_info.get('file_id', '')
|
|
662
|
+
if not file_id or len(file_id) < 8:
|
|
663
|
+
return web.Response(status=404, text="File not found")
|
|
664
|
+
fpath, mime = _find_upload_file(file_id)
|
|
665
|
+
if not fpath or not fpath.exists():
|
|
666
|
+
return web.Response(status=404, text="File not found")
|
|
667
|
+
# For images, return inline; for PDFs, return inline; for others, return attachment
|
|
668
|
+
return web.FileResponse(fpath, content_type=mime)
|
|
669
|
+
|
|
670
|
+
async def handle_download_file(self, request):
|
|
671
|
+
"""GET /api/file/{file_id}/download - 强制下载文件"""
|
|
672
|
+
file_id = request.match_info.get('file_id', '')
|
|
673
|
+
if not file_id or len(file_id) < 8:
|
|
674
|
+
return web.Response(status=404, text="File not found")
|
|
675
|
+
fpath, mime = _find_upload_file(file_id)
|
|
676
|
+
if not fpath or not fpath.exists():
|
|
677
|
+
return web.Response(status=404, text="File not found")
|
|
678
|
+
return web.FileResponse(
|
|
679
|
+
fpath,
|
|
680
|
+
content_type=mime,
|
|
681
|
+
headers={"Content-Disposition": f"attachment; filename=\"{fpath.name}\""}
|
|
682
|
+
)
|
|
683
|
+
|
|
599
684
|
# --- Execution Progress ---
|
|
600
685
|
async def handle_execution_progress(self, request):
|
|
601
686
|
"""GET /api/execution/progress - 返回当前执行进度"""
|
|
@@ -2792,7 +2877,17 @@ class ApiServer:
|
|
|
2792
2877
|
entries = entries[offset:]
|
|
2793
2878
|
# Filter out internal entries (LLM raw output only)
|
|
2794
2879
|
entries = [e for e in entries if (e.key or "") not in self._HIDDEN_KEYS]
|
|
2795
|
-
|
|
2880
|
+
result = []
|
|
2881
|
+
for e in entries:
|
|
2882
|
+
msg = {"role": e.role, "content": e.content, "time": e.created_at, "key": e.key or ""}
|
|
2883
|
+
# [v1.16.17] 附加附件元数据
|
|
2884
|
+
meta = e.metadata or {}
|
|
2885
|
+
if meta.get("images"):
|
|
2886
|
+
msg["images"] = meta["images"]
|
|
2887
|
+
if meta.get("files"):
|
|
2888
|
+
msg["files"] = meta["files"]
|
|
2889
|
+
result.append(msg)
|
|
2890
|
+
return web.json_response(result)
|
|
2796
2891
|
|
|
2797
2892
|
async def handle_get_raw_messages(self, request):
|
|
2798
2893
|
"""GET /api/sessions/{sid}/raw - 获取会话全部原始消息(含 llm_output 等)"""
|
|
@@ -2836,7 +2931,17 @@ class ApiServer:
|
|
|
2836
2931
|
entries = entries[offset:]
|
|
2837
2932
|
# Filter out internal entries (LLM raw output only)
|
|
2838
2933
|
entries = [e for e in entries if (e.key or "") not in self._HIDDEN_KEYS]
|
|
2839
|
-
|
|
2934
|
+
result = []
|
|
2935
|
+
for e in entries:
|
|
2936
|
+
msg = {"role": e.role, "content": e.content, "time": e.created_at, "key": e.key or ""}
|
|
2937
|
+
# [v1.16.17] 附加附件元数据
|
|
2938
|
+
meta = e.metadata or {}
|
|
2939
|
+
if meta.get("images"):
|
|
2940
|
+
msg["images"] = meta["images"]
|
|
2941
|
+
if meta.get("files"):
|
|
2942
|
+
msg["files"] = meta["files"]
|
|
2943
|
+
result.append(msg)
|
|
2944
|
+
return web.json_response(result)
|
|
2840
2945
|
|
|
2841
2946
|
def _cleanup_session_state(self, sid: str):
|
|
2842
2947
|
"""删除会话时清理所有关联的后端状态"""
|
|
@@ -3853,13 +3958,21 @@ class ApiServer:
|
|
|
3853
3958
|
context.metadata["chat_mode"] = chat_mode
|
|
3854
3959
|
context.metadata["user_voice_text"] = voice_text # 语音输入原始文本(用于 usersays_correct)
|
|
3855
3960
|
|
|
3856
|
-
# [v1.16.12] 处理用户图片附件 —
|
|
3961
|
+
# [v1.16.12→17] 处理用户图片附件 — 保存到磁盘 + data URI 传给 LLM Vision API
|
|
3857
3962
|
if user_images:
|
|
3858
3963
|
_processed_images = []
|
|
3964
|
+
_image_file_ids = []
|
|
3859
3965
|
for img in user_images:
|
|
3860
3966
|
mime = img.get("type", "image/png")
|
|
3861
3967
|
b64 = img.get("data", "")
|
|
3862
3968
|
if b64:
|
|
3969
|
+
try:
|
|
3970
|
+
import base64 as _b64mod
|
|
3971
|
+
fbytes = _b64mod.b64decode(b64)
|
|
3972
|
+
file_id = _save_upload_file(img.get("name", "image.png"), mime, fbytes)
|
|
3973
|
+
_image_file_ids.append({"id": file_id, "name": img.get("name", ""), "type": mime, "size": len(fbytes)})
|
|
3974
|
+
except Exception as _ie:
|
|
3975
|
+
logger.warning(f"[{session_id}] 图片保存失败: {_ie}")
|
|
3863
3976
|
_processed_images.append({
|
|
3864
3977
|
"url": f"data:{mime};base64,{b64}",
|
|
3865
3978
|
"type": mime,
|
|
@@ -3867,11 +3980,14 @@ class ApiServer:
|
|
|
3867
3980
|
})
|
|
3868
3981
|
if _processed_images:
|
|
3869
3982
|
context.metadata["user_images"] = _processed_images
|
|
3983
|
+
if _image_file_ids:
|
|
3984
|
+
context.metadata["user_image_files"] = _image_file_ids
|
|
3870
3985
|
logger.info(f"[{session_id}] 用户发送了 {len(_processed_images)} 张图片")
|
|
3871
3986
|
|
|
3872
|
-
# [v1.16.12] 处理用户文件附件 —
|
|
3987
|
+
# [v1.16.12→17] 处理用户文件附件 — 保存到磁盘 + 提取文本附加到消息
|
|
3873
3988
|
if user_files:
|
|
3874
3989
|
_file_texts = []
|
|
3990
|
+
_file_file_ids = []
|
|
3875
3991
|
for f in user_files:
|
|
3876
3992
|
fname = f.get("name", "unknown")
|
|
3877
3993
|
ftype = f.get("type", "")
|
|
@@ -3881,6 +3997,13 @@ class ApiServer:
|
|
|
3881
3997
|
try:
|
|
3882
3998
|
import base64 as _b64mod
|
|
3883
3999
|
fbytes = _b64mod.b64decode(fdata_b64)
|
|
4000
|
+
# Save to disk
|
|
4001
|
+
try:
|
|
4002
|
+
file_id = _save_upload_file(fname, ftype, fbytes)
|
|
4003
|
+
_file_file_ids.append({"id": file_id, "name": fname, "type": ftype, "size": len(fbytes)})
|
|
4004
|
+
except Exception as _se:
|
|
4005
|
+
logger.warning(f"[{session_id}] 文件 {fname} 保存失败: {_se}")
|
|
4006
|
+
# Extract text
|
|
3884
4007
|
_text = self._extract_text_from_file(fname, ftype, fbytes)
|
|
3885
4008
|
if _text:
|
|
3886
4009
|
_file_texts.append(f"--- 文件: {fname} ---\n{_text}")
|
|
@@ -3889,10 +4012,11 @@ class ApiServer:
|
|
|
3889
4012
|
if _file_texts:
|
|
3890
4013
|
file_context = "\n\n".join(_file_texts)
|
|
3891
4014
|
context.metadata["user_file_texts"] = file_context
|
|
3892
|
-
# 将文件内容附加到 user_message 中
|
|
3893
4015
|
user_message = f"{user_message}\n\n[附件内容]\n{file_context}" if user_message else f"[附件内容]\n{file_context}"
|
|
3894
4016
|
context.user_message = user_message
|
|
3895
|
-
|
|
4017
|
+
if _file_file_ids:
|
|
4018
|
+
context.metadata["user_file_files"] = _file_file_ids
|
|
4019
|
+
logger.info(f"[{session_id}] 用户发送了 {len(user_files)} 个文件,提取文本 {len(file_context) if _file_texts else 0} 字符")
|
|
3896
4020
|
|
|
3897
4021
|
# ── 根据 Agent 配置设置执行引擎参数(execution_mode 等)──
|
|
3898
4022
|
agent_cfg_for_exec = self._read_agent_config(agent_path)
|
package/web/ui/chat/chat.css
CHANGED
|
@@ -652,7 +652,11 @@ input,textarea,select{font:inherit}
|
|
|
652
652
|
display:flex;align-items:center;gap:8px;padding:6px 10px;
|
|
653
653
|
background:var(--bg3);border-radius:var(--radius-sm);
|
|
654
654
|
font-size:13px;color:var(--text2);max-width:240px;
|
|
655
|
+
cursor:pointer;transition:background .15s,border-color .15s;
|
|
656
|
+
border:1px solid transparent;
|
|
655
657
|
}
|
|
658
|
+
.msg-file-item:hover{background:var(--bg4);border-color:var(--accent)}
|
|
659
|
+
.agent-file{border-color:var(--accent);background:var(--bg2)}
|
|
656
660
|
.msg-file-icon{font-size:18px;flex-shrink:0}
|
|
657
661
|
.msg-file-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
658
662
|
.msg-file-size{font-size:11px;color:var(--text3);flex-shrink:0}
|
package/web/ui/chat/chat_main.js
CHANGED
|
@@ -2500,25 +2500,45 @@ function _renderMessagesInner() {
|
|
|
2500
2500
|
|
|
2501
2501
|
const avatar = isUser ? '<span style="font-size:18px">👤</span>' : avatarHtml({avatar_image: state.currentAgent?.avatar_image, avatar_emoji: botEmoji, avatar_color: state.currentAgent?.avatar_color, name: state.currentAgent?.name}, 32, 'border-radius:8px;');
|
|
2502
2502
|
const content = renderMarkdown(msg.content);
|
|
2503
|
-
// [v1.16.12]
|
|
2503
|
+
// [v1.16.12→17] 渲染图片和文件附件(支持磁盘持久化 file_id)
|
|
2504
2504
|
const attachmentHtml = (() => {
|
|
2505
|
-
if (!isUser) return '';
|
|
2506
2505
|
let parts = [];
|
|
2507
|
-
//
|
|
2508
|
-
if (msg.images && msg.images.length > 0) {
|
|
2506
|
+
// User images
|
|
2507
|
+
if (isUser && msg.images && msg.images.length > 0) {
|
|
2509
2508
|
for (const img of msg.images) {
|
|
2510
|
-
|
|
2511
|
-
|
|
2509
|
+
// Check if we have a file_id (from server) or raw data
|
|
2510
|
+
const fileId = img.id;
|
|
2511
|
+
let src;
|
|
2512
|
+
if (fileId) {
|
|
2513
|
+
src = '/api/file/' + fileId;
|
|
2514
|
+
} else {
|
|
2515
|
+
src = 'data:' + (img.type || 'image/png') + ';base64,' + (img.data || '');
|
|
2516
|
+
}
|
|
2517
|
+
parts.push('<div class="msg-image-wrapper"><img src="' + src + '" class="msg-image" loading="lazy" alt="' + escapeHtml(img.name || 'image') + '" onclick="openFileViewer(\'' + (fileId || '') + '\', this.src, \'' + escapeHtml(img.name || 'image') + '\')" /></div>');
|
|
2512
2518
|
}
|
|
2513
2519
|
}
|
|
2514
|
-
//
|
|
2515
|
-
if (msg.files && msg.files.length > 0) {
|
|
2520
|
+
// User files
|
|
2521
|
+
if (isUser && msg.files && msg.files.length > 0) {
|
|
2516
2522
|
for (const f of msg.files) {
|
|
2523
|
+
const fileId = f.id;
|
|
2517
2524
|
const sizeStr = f.size ? formatFileSize(f.size) : '';
|
|
2518
2525
|
const icon = _getFileIcon(f.name || f.type || '');
|
|
2519
|
-
parts.push('<div class="msg-file-item">' +
|
|
2526
|
+
parts.push('<div class="msg-file-item" onclick="openFileViewer(\'' + (fileId || '') + '\', \'/api/file/' + (fileId || '') + '\', \'' + escapeHtml(f.name) + '\')" title="点击打开">' +
|
|
2520
2527
|
'<span class="msg-file-icon">' + icon + '</span>' +
|
|
2521
|
-
'<span class="msg-file-name"
|
|
2528
|
+
'<span class="msg-file-name">' + escapeHtml(f.name) + '</span>' +
|
|
2529
|
+
(sizeStr ? '<span class="msg-file-size">' + sizeStr + '</span>' : '') +
|
|
2530
|
+
'</div>');
|
|
2531
|
+
}
|
|
2532
|
+
}
|
|
2533
|
+
// Agent files (v2_file events)
|
|
2534
|
+
if (!isUser && msg._files && msg._files.length > 0) {
|
|
2535
|
+
for (const f of msg._files) {
|
|
2536
|
+
const fileId = f.id;
|
|
2537
|
+
const sizeStr = f.size ? formatFileSize(f.size) : '';
|
|
2538
|
+
const icon = _getFileIcon(f.name || f.type || '');
|
|
2539
|
+
parts.push('<div class="msg-file-item agent-file" onclick="openFileViewer(\'' + (fileId || '') + '\', \'/api/file/' + (fileId || '') + '\', \'' + escapeHtml(f.name) + '\')" title="点击打开">' +
|
|
2540
|
+
'<span class="msg-file-icon">' + icon + '</span>' +
|
|
2541
|
+
'<span class="msg-file-name">' + escapeHtml(f.name) + '</span>' +
|
|
2522
2542
|
(sizeStr ? '<span class="msg-file-size">' + sizeStr + '</span>' : '') +
|
|
2523
2543
|
'</div>');
|
|
2524
2544
|
}
|
|
@@ -2769,6 +2789,45 @@ function _getFileIcon(name) {
|
|
|
2769
2789
|
return map[ext] || '📎';
|
|
2770
2790
|
}
|
|
2771
2791
|
|
|
2792
|
+
// [v1.16.17] 文件查看器 — 新窗口打开文件 + 下载按钮
|
|
2793
|
+
function openFileViewer(fileId, src, fileName) {
|
|
2794
|
+
if (!src && !fileId) return;
|
|
2795
|
+
if (!src && fileId) src = '/api/file/' + fileId;
|
|
2796
|
+
|
|
2797
|
+
// Check if it's an image
|
|
2798
|
+
var isImage = /\.(png|jpe?g|gif|webp|bmp|svg|ico)$/i.test(fileName || src);
|
|
2799
|
+
|
|
2800
|
+
var html = '<!DOCTYPE html><html><head><meta charset="utf-8"><title>' + escapeHtml(fileName || '文件查看') + '</title>' +
|
|
2801
|
+
'<style>*{margin:0;padding:0;box-sizing:border-box}body{background:#1a1a2e;color:#e0e0e0;font-family:-apple-system,BlinkMacSystemFont,sans-serif;display:flex;flex-direction:column;height:100vh;overflow:hidden}' +
|
|
2802
|
+
'.toolbar{display:flex;align-items:center;padding:10px 16px;background:#16213e;gap:12px;border-bottom:1px solid #2a2a4a;flex-shrink:0}' +
|
|
2803
|
+
'.toolbar .fname{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:14px}' +
|
|
2804
|
+
'.toolbar button{padding:6px 16px;border:none;border-radius:6px;cursor:pointer;font-size:13px;background:#4a4a8a;color:#fff;transition:background .2s}' +
|
|
2805
|
+
'.toolbar button:hover{background:#6a6aaa}' +
|
|
2806
|
+
'.toolbar .close-btn{background:#c0392b}.toolbar .close-btn:hover{background:#e74c3c}' +
|
|
2807
|
+
'.viewer{flex:1;display:flex;align-items:center;justify-content:center;overflow:auto;padding:16px;background:#0f0f23}' +
|
|
2808
|
+
'.viewer img{max-width:100%;max-height:100%;object-fit:contain;border-radius:4px}' +
|
|
2809
|
+
'.viewer iframe{width:100%;height:100%;border:none;background:#fff}' +
|
|
2810
|
+
'</style></head><body>' +
|
|
2811
|
+
'<div class="toolbar">' +
|
|
2812
|
+
'<span class="fname">' + escapeHtml(fileName || '文件') + '</span>' +
|
|
2813
|
+
(fileId ? '<button onclick="location.href=\'/api/file/' + fileId + '/download\'">⬇ 下载</button>' : '') +
|
|
2814
|
+
'<button class="close-btn" onclick="window.close()">✕ 关闭</button>' +
|
|
2815
|
+
'</div><div class="viewer">';
|
|
2816
|
+
|
|
2817
|
+
if (isImage) {
|
|
2818
|
+
html += '<img src="' + src + '" alt="' + escapeHtml(fileName) + '" />';
|
|
2819
|
+
} else {
|
|
2820
|
+
html += '<iframe src="' + src + '"></iframe>';
|
|
2821
|
+
}
|
|
2822
|
+
html += '</div></body></html>';
|
|
2823
|
+
|
|
2824
|
+
var w = window.open('', '_blank', 'width=900,height=700');
|
|
2825
|
+
if (w) {
|
|
2826
|
+
w.document.write(html);
|
|
2827
|
+
w.document.close();
|
|
2828
|
+
}
|
|
2829
|
+
}
|
|
2830
|
+
|
|
2772
2831
|
// ── 附件上传系统 ──
|
|
2773
2832
|
var _attachState = { images: [], files: [] };
|
|
2774
2833
|
|
|
@@ -1800,6 +1800,13 @@ async function sendMessage(opts) {
|
|
|
1800
1800
|
// Memory was saved - show a small indicator
|
|
1801
1801
|
state.messages[msgIdx]._memorySaved = (state.messages[msgIdx]._memorySaved || '') +
|
|
1802
1802
|
(evt.content ? evt.content.substring(0, 100) : '');
|
|
1803
|
+
} else if (evt.type === 'v2_file') {
|
|
1804
|
+
// [v1.16.17] Agent is sending a file to the user
|
|
1805
|
+
if (evt.data) {
|
|
1806
|
+
if (!state.messages[msgIdx]._files) state.messages[msgIdx]._files = [];
|
|
1807
|
+
state.messages[msgIdx]._files.push(evt.data);
|
|
1808
|
+
throttledStreamUpdate(msgIdx);
|
|
1809
|
+
}
|
|
1803
1810
|
} else if (evt.type === 'v2_session_rename') {
|
|
1804
1811
|
// [v1.15.8] 会话自动命名 — 后端通过 mainsubject 生成
|
|
1805
1812
|
if (evt.data && evt.data.name) {
|