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.
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "myagent-ai",
3
- "version": "1.16.17",
3
+ "version": "1.16.18",
4
4
  "description": "本地桌面端执行型AI助手 - Open Interpreter 风格 | Local Desktop Execution-Oriented AI Assistant",
5
5
  "main": "main.py",
6
6
  "bin": {
@@ -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
- return web.json_response([{"role": e.role, "content": e.content, "time": e.created_at, "key": e.key or ""} for e in entries])
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
- return web.json_response([{"role": e.role, "content": e.content, "time": e.created_at, "key": e.key or ""} for e in entries])
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] 处理用户图片附件 — 转换为 data URI 传给 LLM Vision API
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
- logger.info(f"[{session_id}] 用户发送了 {len(user_files)} 个文件,提取文本 {len(file_context)} 字符")
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)
@@ -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}
@@ -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
- const dataUrl = 'data:' + (img.type || 'image/png') + ';base64,' + (img.data || '');
2511
- parts.push('<div class="msg-image-wrapper"><img src="' + dataUrl + '" class="msg-image" onclick="window.open(this.src)" loading="lazy" alt="' + escapeHtml(img.name || 'image') + '" /></div>');
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" title="' + escapeHtml(f.name) + '">' + escapeHtml(f.name) + '</span>' +
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\'">&#11015; 下载</button>' : '') +
2814
+ '<button class="close-btn" onclick="window.close()">&#10005; 关闭</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) {