myagent-ai 1.18.5 → 1.18.6

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.
@@ -518,6 +518,8 @@ class MainAgent(BaseAgent):
518
518
  get_knowledge_content = ""
519
519
  # 追踪流式推送的 reasoning 文本(用于构建有意义的最终回复)
520
520
  _v2_reasoning_collected: List[str] = []
521
+ # [v1.18.5] 追踪本轮 file_send 发送的文件,用于持久化到会话记忆
522
+ _sent_files: List[Dict[str, Any]] = []
521
523
  # [v1.15.73] 追踪上一轮保存到 memory 的位置,避免重复保存
522
524
  _last_saved_len: int = 0
523
525
  # XML 解析失败时的 LLM 修正重试计数
@@ -1192,7 +1194,8 @@ class MainAgent(BaseAgent):
1192
1194
  })
1193
1195
 
1194
1196
  tool_result = await self._execute_v2_tool(
1195
- tool_name, parms, timeout, context, task_id
1197
+ tool_name, parms, timeout, context, task_id,
1198
+ stream_callback=stream_callback,
1196
1199
  )
1197
1200
 
1198
1201
  # 发送工具结果事件
@@ -1446,10 +1449,15 @@ class MainAgent(BaseAgent):
1446
1449
  if not _emitted_reasoning_this_iter:
1447
1450
  await self._emit_v2_event("v2_reasoning", {"content": truncate_str(final_text, 3000)}, stream_callback)
1448
1451
  if self.memory:
1452
+ # [v1.18.5] 附加本轮发送的文件列表到助手消息 metadata
1453
+ _asst_meta = {}
1454
+ if _sent_files:
1455
+ _asst_meta["files"] = _sent_files
1449
1456
  self.memory.add_session(
1450
1457
  session_id=context.session_id,
1451
1458
  role="assistant",
1452
1459
  content=final_text,
1460
+ metadata=_asst_meta if _asst_meta else None,
1453
1461
  )
1454
1462
  break
1455
1463
 
@@ -1497,6 +1505,7 @@ class MainAgent(BaseAgent):
1497
1505
  timeout: int,
1498
1506
  context: AgentContext,
1499
1507
  task_id: str,
1508
+ stream_callback: Optional[Callable] = None,
1500
1509
  ) -> Dict[str, Any]:
1501
1510
  """V2 工具执行"""
1502
1511
  result = {"success": False, "output": "", "error": ""}
@@ -1579,6 +1588,14 @@ class MainAgent(BaseAgent):
1579
1588
  # [v1.16.18] 使用当前作用域的 stream_callback(而非 context._stream_callback)
1580
1589
  _fresult = await _fskill.execute(_fpath, _fdesc, stream_callback=stream_callback)
1581
1590
  result = {"success": True, "output": json.dumps(_fresult, ensure_ascii=False, indent=2), "data": _fresult}
1591
+ # [v1.18.5] 追踪发送的文件,用于持久化到会话记忆
1592
+ if _fresult.get("success") and _fresult.get("file_id"):
1593
+ _sent_files.append({
1594
+ "id": _fresult["file_id"],
1595
+ "name": _fresult.get("name", ""),
1596
+ "type": _fresult.get("type", ""),
1597
+ "size": _fresult.get("size", 0),
1598
+ })
1582
1599
  except Exception as _fse:
1583
1600
  result = {"success": False, "error": f"文件发送失败: {_fse}"}
1584
1601
  logger.warning(f"[{task_id}] file_send 工具异常: {_fse}")
package/config.py CHANGED
@@ -178,6 +178,9 @@ class ConfigManager:
178
178
  self._config_dir.mkdir(parents=True, exist_ok=True)
179
179
  (self._config_dir / "data").mkdir(exist_ok=True)
180
180
  (self._config_dir / "logs").mkdir(exist_ok=True)
181
+ (self._config_dir / "data" / "workspace").mkdir(exist_ok=True)
182
+ (self._config_dir / "data" / "workspace" / "userfiles").mkdir(exist_ok=True)
183
+ (self._config_dir / "data" / "uploads").mkdir(exist_ok=True)
181
184
 
182
185
  @property
183
186
  def config(self) -> AppConfig:
@@ -692,11 +692,13 @@ class VNCManager:
692
692
  "-nowf", # 禁用 wireframe
693
693
  "-nowcr", # 禁用 cursor shape updates
694
694
  "-nocursorshape",
695
- "-threads", # 多线程
696
695
  "-deferupdate", "5", # 延迟更新(降低带宽)
697
696
  "-scale", "2/3", # 缩小 2/3(降低带宽)
698
697
  ]
699
698
 
699
+ # [v1.18.5] 不使用 -threads 模式:与 -scale 和 -no-shm 组合在 0.9.17 中
700
+ # 会导致父进程 fork 后立即退出,端口延迟监听
701
+
700
702
  # 处理 shm-helper: 找到就传绝对路径,找不到就用 -no-shm 禁用
701
703
  if shm_helper:
702
704
  cmd.extend(["-shm-helper", shm_helper])
@@ -709,7 +711,6 @@ class VNCManager:
709
711
 
710
712
  # [v1.18.0] proot/Termux 兼容: 可能需要额外的安全参数
711
713
  cmd.append("-nobell")
712
- cmd.append("-noxdamage")
713
714
  # 跳过 Xinerama 检查(proot 环境下可能失败)
714
715
  env["X11VNC_NO_UNIXPW"] = "1"
715
716
 
@@ -723,13 +724,20 @@ class VNCManager:
723
724
  preexec_fn=os.setpgrp,
724
725
  )
725
726
 
726
- # 等待 VNC 端口就绪
727
- await asyncio.sleep(1.5)
727
+ # [v1.18.5] x11vnc 0.9.17 可能 fork 到后台,需要更长等待
728
+ await asyncio.sleep(2.5)
728
729
  if self._x11vnc_process.poll() is not None:
729
730
  # [v1.18.4] x11vnc 可能 fork 到后台运行,父进程退出但子进程仍在监听端口
731
+ # 等待更长时间让 fork 的子进程完成端口绑定
730
732
  if self._is_port_listening(self.x11vnc_port):
731
733
  logger.warning(f"x11vnc 父进程已退出但端口 {self.x11vnc_port} 仍在监听(fork 到后台),视为启动成功")
732
734
  return True
735
+ # 端口未监听,再等几秒(某些系统 fork 后子进程初始化慢)
736
+ logger.info("x11vnc 端口尚未就绪,额外等待 3 秒...")
737
+ await asyncio.sleep(3)
738
+ if self._is_port_listening(self.x11vnc_port):
739
+ logger.warning(f"x11vnc fork 延迟启动成功 (port={self.x11vnc_port})")
740
+ return True
733
741
  # 端口未监听 = 真正的启动失败
734
742
  stderr = ""
735
743
  try:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "myagent-ai",
3
- "version": "1.18.5",
3
+ "version": "1.18.6",
4
4
  "description": "本地桌面端执行型AI助手 - Open Interpreter 风格 | Local Desktop Execution-Oriented AI Assistant",
5
5
  "main": "main.py",
6
6
  "bin": {
@@ -588,36 +588,63 @@ class MCPClient:
588
588
 
589
589
  查找并杀死由 chrome-devtools-mcp 启动的 Chrome 进程。
590
590
  这些进程可能导致下次启动时 'Target closed' 错误。
591
+
592
+ [v1.18.5] 扩展匹配模式,覆盖 chrome/chromium/chromium-browser/headless_shell
591
593
  """
592
594
  import signal as _sig
593
595
  try:
594
- # 查找 chrome-devtools-mcp 启动的 Chrome 进程
595
- # 特征: 父进程是 node/npx (chrome-devtools-mcp)
596
- result = subprocess.run(
597
- ["pgrep", "-f", "chromium-browser.*--remote-debugging-port"],
598
- capture_output=True, text=True, timeout=5,
599
- )
600
- if result.returncode == 0 and result.stdout.strip():
601
- pids = result.stdout.strip().split("\n")
602
- for pid_str in pids:
603
- try:
604
- pid = int(pid_str)
605
- os.kill(pid, _sig.SIGTERM)
606
- logger.debug(f"清理残留 Chrome 进程: PID={pid}")
607
- except (ProcessLookupError, PermissionError, ValueError):
608
- pass
609
-
610
- # 等一小会让 Chrome 优雅退出
611
- import time
612
- time.sleep(0.5)
613
-
614
- # 还没退就强杀
615
- for pid_str in pids:
616
- try:
617
- pid = int(pid_str)
618
- os.kill(pid, _sig.SIGKILL)
619
- except (ProcessLookupError, PermissionError, ValueError):
620
- pass
596
+ # [v1.18.5] 更宽泛的匹配模式,覆盖各种 Chrome 变体
597
+ # 匹配所有带 --remote-debugging-port Chrome 相关进程
598
+ patterns = [
599
+ "chromium-browser.*--remote-debugging-port",
600
+ "chromium.*--remote-debugging-port",
601
+ "chrome.*--remote-debugging-port",
602
+ "google-chrome.*--remote-debugging-port",
603
+ "headless_shell.*--remote-debugging-port",
604
+ ]
605
+ all_pids = set()
606
+ for pattern in patterns:
607
+ try:
608
+ result = subprocess.run(
609
+ ["pgrep", "-f", pattern],
610
+ capture_output=True, text=True, timeout=5,
611
+ )
612
+ if result.returncode == 0 and result.stdout.strip():
613
+ for pid_str in result.stdout.strip().split("\n"):
614
+ # 排除 pgrep 自身和 npx/node 进程
615
+ try:
616
+ pid = int(pid_str.strip())
617
+ if pid != os.getpid():
618
+ all_pids.add(pid)
619
+ except ValueError:
620
+ pass
621
+ except Exception:
622
+ pass
623
+
624
+ if not all_pids:
625
+ return
626
+
627
+ logger.info(f"清理 {len(all_pids)} 个残留 Chrome 进程: {all_pids}")
628
+ for pid in all_pids:
629
+ try:
630
+ os.kill(pid, _sig.SIGTERM)
631
+ logger.debug(f"发送 SIGTERM to Chrome PID={pid}")
632
+ except (ProcessLookupError, PermissionError, ValueError):
633
+ pass
634
+
635
+ # 等一小会让 Chrome 优雅退出
636
+ import time
637
+ time.sleep(1.0)
638
+
639
+ # 还没退就强杀
640
+ for pid in all_pids:
641
+ try:
642
+ os.kill(pid, _sig.SIGKILL)
643
+ except (ProcessLookupError, PermissionError, ValueError):
644
+ pass
645
+
646
+ # 额外等待,确保端口释放
647
+ time.sleep(0.5)
621
648
  except Exception as e:
622
649
  logger.debug(f"清理 Chrome 进程异常: {e}")
623
650
 
package/web/api_server.py CHANGED
@@ -378,6 +378,7 @@ class ApiServer:
378
378
  r.add_delete("/api/task-plan/{idx:int}", self.handle_delete_task_item)
379
379
  r.add_put("/api/workdir", self.handle_set_workdir)
380
380
  r.add_get("/api/workdir/files", self.handle_list_workdir)
381
+ r.add_get(r"/api/workdir/download/{path:.*}", self.handle_workdir_download)
381
382
  r.add_get("/api/logs", self.handle_get_logs)
382
383
  r.add_get("/api/logs/stream", self.handle_log_stream)
383
384
  r.add_post("/api/chat", self.handle_chat)
@@ -3771,14 +3772,80 @@ window.toggleFullscreen = function() {{
3771
3772
  return web.json_response({"ok": True})
3772
3773
 
3773
3774
  async def handle_list_workdir(self, request):
3775
+ """GET /api/workdir/files?path=xxx&recursive=1 - 列出工作目录文件
3776
+
3777
+ [v1.18.5] 支持:
3778
+ - path: 子目录相对路径(如 'userfiles/2026-04')
3779
+ - recursive: 递归列出子目录
3780
+ """
3774
3781
  wd = self.core.config_mgr.data_dir / "workspace"
3775
- if not wd.exists(): return web.json_response([])
3782
+ sub_path = request.query.get("path", "").strip("/")
3783
+ if sub_path:
3784
+ target = wd / sub_path
3785
+ else:
3786
+ target = wd
3787
+ # 安全检查:防止路径遍历
3788
+ try:
3789
+ target = target.resolve()
3790
+ wd_resolved = wd.resolve()
3791
+ if not str(target).startswith(str(wd_resolved)):
3792
+ return web.json_response({"error": "非法路径"}, status=403)
3793
+ except Exception:
3794
+ return web.json_response({"error": "路径错误"}, status=400)
3795
+
3796
+ if not target.exists(): return web.json_response([])
3797
+ recursive = request.query.get("recursive", "") in ("1", "true")
3776
3798
  items = []
3777
- for f in sorted(wd.iterdir())[:200]:
3778
- try: items.append({"name": f.name, "type": "dir" if f.is_dir() else "file", "size": f.stat().st_size if f.is_file() else 0})
3779
- except: pass
3799
+ max_items = 500
3800
+ if recursive:
3801
+ for f in sorted(target.rglob("*")):
3802
+ if len(items) >= max_items: break
3803
+ try:
3804
+ if f.is_file():
3805
+ rel = str(f.relative_to(target))
3806
+ items.append({"name": f.name, "path": sub_path + "/" + rel if sub_path else rel, "type": "file", "size": f.stat().st_size})
3807
+ except Exception:
3808
+ pass
3809
+ else:
3810
+ for f in sorted(target.iterdir()):
3811
+ if len(items) >= max_items: break
3812
+ try:
3813
+ items.append({
3814
+ "name": f.name,
3815
+ "path": (sub_path + "/" + f.name) if sub_path else f.name,
3816
+ "type": "dir" if f.is_dir() else "file",
3817
+ "size": f.stat().st_size if f.is_file() else 0,
3818
+ })
3819
+ except Exception:
3820
+ pass
3780
3821
  return web.json_response(items)
3781
3822
 
3823
+ async def handle_workdir_download(self, request):
3824
+ """GET /api/workdir/download/{path} - 下载工作目录文件"""
3825
+ import urllib.parse
3826
+ rel_path = urllib.parse.unquote(request.match_info["path"]).strip("/")
3827
+ if not rel_path:
3828
+ return web.json_response({"error": "未指定文件"}, status=400)
3829
+ wd = self.core.config_mgr.data_dir / "workspace"
3830
+ target = wd / rel_path
3831
+ # 安全检查
3832
+ try:
3833
+ target = target.resolve()
3834
+ wd_resolved = wd.resolve()
3835
+ if not str(target).startswith(str(wd_resolved)):
3836
+ return web.json_response({"error": "非法路径"}, status=403)
3837
+ except Exception:
3838
+ return web.json_response({"error": "路径错误"}, status=400)
3839
+ if not target.exists() or not target.is_file():
3840
+ return web.json_response({"error": "文件不存在"}, status=404)
3841
+ import mimetypes
3842
+ ctype = mimetypes.guess_type(str(target))[0] or "application/octet-stream"
3843
+ return web.Response(
3844
+ body=target.read_bytes(),
3845
+ content_type=ctype,
3846
+ headers={"Content-Disposition": f'attachment; filename="{target.name}"'},
3847
+ )
3848
+
3782
3849
  # --- Logs ---
3783
3850
  async def handle_get_logs(self, request):
3784
3851
  log_dir = self.core.config_mgr.logs_dir
@@ -5861,6 +5928,8 @@ window.toggleFullscreen = function() {{
5861
5928
 
5862
5929
  async def handle_list_org_knowledge(self, request):
5863
5930
  """GET /api/organization/knowledge - 列出组织知识库文件"""
5931
+ if not self.core.config.organization.enabled:
5932
+ return web.json_response([])
5864
5933
  org_mgr = self._get_org_manager()
5865
5934
  files = org_mgr.list_knowledge_files()
5866
5935
  return web.json_response(files)
@@ -2688,9 +2688,10 @@ function _renderMessagesInner() {
2688
2688
  '</div>');
2689
2689
  }
2690
2690
  }
2691
- // Agent files (v2_file events)
2692
- if (!isUser && msg._files && msg._files.length > 0) {
2693
- for (const f of msg._files) {
2691
+ // Agent files (v2_file events) — 支持实时流式 _files 和历史加载的 files
2692
+ const agentFiles = (msg._files || (msg.files && !isUser ? msg.files : []));
2693
+ if (agentFiles.length > 0) {
2694
+ for (const f of agentFiles) {
2694
2695
  const fileId = f.id;
2695
2696
  const sizeStr = f.size ? formatFileSize(f.size) : '';
2696
2697
  const icon = _getFileIcon(f.name || f.type || '');
package/web/ui/index.html CHANGED
@@ -77,6 +77,16 @@ tr:hover{background:var(--surface2)}
77
77
  .badge-yellow{background:#f59e0b22;color:var(--warn)}
78
78
  .badge-blue{background:#3b82f622;color:var(--info)}
79
79
  .badge-purple{background:#8b5cf622;color:#a78bfa}
80
+ .tag{display:inline-block;padding:2px 8px;border-radius:12px;font-size:11px;background:var(--surface2);color:var(--text2);white-space:nowrap}
81
+ .tag-imp{background:#f59e0b22;color:#f59e0b}
82
+ .mem-list{display:flex;flex-direction:column;gap:8px}
83
+ .mem-card{border:1px solid var(--border);border-radius:var(--radius);padding:12px 16px;background:var(--surface)}
84
+ .mem-card-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;flex-wrap:wrap;gap:4px}
85
+ .mem-key{font-weight:600;font-size:13px;max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
86
+ .mem-meta{display:flex;gap:4px;align-items:center;flex-wrap:wrap}
87
+ .mem-session{font-size:11px;color:var(--text3);max-width:120px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
88
+ .mem-content{font-size:13px;line-height:1.5;color:var(--text);word-break:break-word;max-height:120px;overflow:hidden}
89
+ .mem-actions{margin-top:8px;text-align:right}
80
90
  .form-group{margin-bottom:14px}
81
91
  .form-group label{display:block;font-size:13px;color:var(--text2);margin-bottom:4px}
82
92
  .form-row{display:grid;grid-template-columns:1fr 1fr;gap:12px}
@@ -487,6 +497,7 @@ function agentCardHtml(a,deptMap){
487
497
  </div>
488
498
  <div class="flex flex-col gap-8" style="flex-shrink:0">
489
499
  <button class="btn btn-sm" style="background:var(--success);color:#fff" onclick="event.stopPropagation();chatWithAgent('${escHtml(a.path)}')">对话</button>
500
+ <button class="btn btn-sm btn-ghost" onclick="event.stopPropagation();showWorkdirModal('${escHtml(a.path)}')">📁 工作目录</button>
490
501
  <button class="btn btn-sm btn-primary" onclick="event.stopPropagation();openEditAgentModal('${escHtml(a.path)}')">编辑</button>
491
502
  ${!isSys?`<button class="btn btn-sm btn-danger" onclick="event.stopPropagation();confirmDeleteAgent('${escHtml(a.path).replace(/'/g,"\\'")}','${escHtml(a.name||a.path).replace(/'/g,"\\'")}')">删除</button>`:''}
492
503
  </div>
@@ -501,6 +512,65 @@ function filterAgents(){
501
512
  });
502
513
  }
503
514
 
515
+ // [v1.18.5] 工作目录文件浏览
516
+ let _workdirCurrentPath='';
517
+ async function showWorkdirModal(agentPath){
518
+ _workdirCurrentPath='';
519
+ const title='📁 工作目录'+(agentPath?' ('+escHtml(agentPath)+')':'');
520
+ $('modalContainer').innerHTML=`<div class="modal-overlay" onclick="closeModal()"><div class="modal modal-wide" onclick="event.stopPropagation()" style="max-width:600px">
521
+ <div class="flex justify-between items-center mb-16">
522
+ <h3>${title}</h3>
523
+ <div class="flex gap-8"><button class="btn btn-sm btn-ghost" onclick="loadWorkdirFiles('')">根目录</button><button class="btn btn-sm btn-ghost" onclick="loadWorkdirFiles(_workdirCurrentPath,true)">刷新</button></div>
524
+ </div>
525
+ <div id="workdirBreadcrumb" style="font-size:12px;color:var(--text3);margin-bottom:8px"></div>
526
+ <div id="workdirContent"><div class="empty">加载中...</div></div>
527
+ <div class="flex gap-8 mt-16"><button class="btn btn-ghost" onclick="closeModal()">关闭</button></div>
528
+ </div></div>`;
529
+ loadWorkdirFiles('');
530
+ }
531
+ async function loadWorkdirFiles(subPath,recursive){
532
+ _workdirCurrentPath=subPath||'';
533
+ const params=new URLSearchParams();
534
+ if(subPath)params.set('path',subPath);
535
+ if(recursive)params.set('recursive','1');
536
+ const files=await api('/api/workdir/files?'+params.toString());
537
+ const bc=document.getElementById('workdirBreadcrumb');
538
+ const el=document.getElementById('workdirContent');
539
+ if(!el)return;
540
+ // 面包屑导航
541
+ if(subPath){
542
+ const parts=subPath.split('/');let crumbs=['<span style="cursor:pointer" onclick="loadWorkdirFiles(\'\')">根目录</span>'];
543
+ let acc='';
544
+ for(let i=0;i<parts.length;i++){acc+=(acc?'/':'')+parts[i];crumbs.push(' / <span style="cursor:pointer" onclick="loadWorkdirFiles(\''+acc+'\')">'+escHtml(parts[i])+'</span>')}
545
+ if(bc)bc.innerHTML=crumbs.join('');
546
+ }else{if(bc)bc.innerHTML='根目录'}
547
+ if(!files||!files.length){el.innerHTML='<div class="empty">暂无文件</div>';return}
548
+ // 排序:目录在前,文件在后
549
+ const dirs=files.filter(f=>f.type==='dir').sort((a,b)=>a.name.localeCompare(b.name));
550
+ const fils=files.filter(f=>f.type==='file').sort((a,b)=>a.name.localeCompare(b.name));
551
+ let html='<div class="table-wrap"><table><tr><th>名称</th><th>大小</th><th></th></tr>';
552
+ for(const d of dirs){
553
+ const dp=d.path||d.name;
554
+ html+=`<tr style="cursor:pointer" onclick="loadWorkdirFiles('${escHtml(dp)}')"><td>📂 ${escHtml(d.name)}</td><td>-</td><td></td></tr>`;
555
+ }
556
+ for(const f of fils){
557
+ const fp=f.path||f.name;
558
+ const sizeStr=f.size>1048576?(f.size/1048576).toFixed(1)+' MB':f.size>1024?(f.size/1024).toFixed(1)+' KB':f.size+' B';
559
+ html+=`<tr><td style="cursor:pointer" onclick="downloadWorkdirFile('${escHtml(fp)}')">📄 ${escHtml(f.name)}</td><td>${sizeStr}</td>
560
+ <td><button class="btn btn-sm btn-ghost" onclick="downloadWorkdirFile('${escHtml(fp)}')">下载</button></td></tr>`;
561
+ }
562
+ html+='</table></div>';
563
+ el.innerHTML=html;
564
+ }
565
+ function downloadWorkdirFile(relPath){
566
+ const link=document.createElement('a');
567
+ link.href=API+'/api/workdir/download/'+encodeURIComponent(relPath);
568
+ link.download='';
569
+ document.body.appendChild(link);
570
+ link.click();
571
+ document.body.removeChild(link);
572
+ }
573
+
504
574
  // Create Agent Modal
505
575
  function _flattenDepts(list,pfx,result){
506
576
  result=result||[];
@@ -1177,26 +1247,25 @@ async function renderMemory(){
1177
1247
  html+='<div class="flex gap-8 mb-16"><input id="memSearch" placeholder="搜索记忆..." onkeydown="if(event.key===\'Enter\')searchMemory()" style="max-width:400px"><button class="btn btn-primary" onclick="searchMemory()">搜索</button><button class="btn btn-ghost" onclick="cleanupMemory()">清理过期</button></div>';
1178
1248
  if(lt&&lt.length){
1179
1249
  const isSession=_memCategory==='session';
1180
- let thHtml='<tr>';
1181
- thHtml+='<th>Key</th><th>内容</th>';
1182
- thHtml+='<th>角色</th>';
1183
- thHtml+='<th>会话</th>';
1184
- if(!isSession)thHtml+='<th>重要性</th>';
1185
- thHtml+='<th></th></tr>';
1186
- html+='<div class="table-wrap"><table>'+thHtml;
1250
+ html+='<div class="mem-list">';
1187
1251
  for(const e of lt){
1188
1252
  const content=(e.content||e.summary||'')||(e.role==='user'?'[用户消息]':e.role==='assistant'?'[助手回复]':'[系统]');
1189
-
1190
1253
  let contentPreview=escHtml(content.slice(0,300));
1191
1254
  if(content.length>300)contentPreview+=`<span style="color:var(--text3)">... (${content.length}字)</span>`;
1192
- html+='<tr><td style="white-space:nowrap;max-width:120px;overflow:hidden;text-overflow:ellipsis">'+escHtml(e.key||e.role||'-')+'</td>';
1193
- html+='<td style="max-width:600px;word-break:break-word;font-size:13px">'+contentPreview+'</td>';
1194
- html+='<td style="white-space:nowrap">'+escHtml(e.role||'')+'</td>';
1195
- html+='<td style="white-space:nowrap;max-width:140px;overflow:hidden;text-overflow:ellipsis;font-size:12px;color:var(--text3)" title="'+escHtml(e.session_id||'')+'">'+escHtml((e.session_id||'').split('_web_')[0])+'</td>';
1196
- if(!isSession)html+='<td>'+(e.importance!=null?e.importance.toFixed(2):'')+'</td>';
1197
- html+='<td><button class="btn btn-sm btn-danger" onclick="deleteMemory(\''+e.id+'\')">删除</button></td></tr>';
1255
+ html+=`<div class="mem-card">
1256
+ <div class="mem-card-header">
1257
+ <span class="mem-key">${escHtml(e.key||e.role||'-')}</span>
1258
+ <div class="mem-meta">
1259
+ <span class="tag">${escHtml(e.role||'')}</span>
1260
+ ${!isSession&&e.importance!=null?'<span class="tag tag-imp">'+e.importance.toFixed(2)+'</span>':''}
1261
+ ${e.session_id?'<span class="mem-session" title="'+escHtml(e.session_id)+'">'+escHtml((e.session_id||'').split('_web_')[0])+'</span>':''}
1262
+ </div>
1263
+ </div>
1264
+ <div class="mem-content">${contentPreview}</div>
1265
+ <div class="mem-actions"><button class="btn btn-sm btn-danger" onclick="deleteMemory('${e.id}')">删除</button></div>
1266
+ </div>`;
1198
1267
  }
1199
- html+='</table></div>';
1268
+ html+='</div>';
1200
1269
  }else{
1201
1270
  html+='<div class="empty">暂无'+(_memCategory==='session'?'会话':'全局')+'记忆</div>';
1202
1271
  }
@@ -1207,12 +1276,22 @@ async function searchMemory(){
1207
1276
  const r=await api('/api/memory/search?q='+encodeURIComponent(q));
1208
1277
  let html='<h3>搜索结果: '+(r.length||0)+' 条</h3>';
1209
1278
  if(r&&r.length){
1210
- html+='<div class="table-wrap"><table><tr><th>Key</th><th>内容</th><th>分类</th><th>角色</th><th>会话</th></tr>';
1279
+ html+='<div class="mem-list">';
1211
1280
  for(const e of r){
1212
1281
  const content=(e.content||'').slice(0,300);
1213
- html+=`<tr><td style="white-space:nowrap">${escHtml(e.key||'')}</td><td style="max-width:600px;word-break:break-word;font-size:13px">${escHtml(content)}</td><td>${e.category||''}</td><td>${escHtml(e.role||'')}</td><td style="font-size:12px;color:var(--text3)">${escHtml((e.session_id||'').split('_web_')[0])}</td></tr>`;
1282
+ html+=`<div class="mem-card">
1283
+ <div class="mem-card-header">
1284
+ <span class="mem-key">${escHtml(e.key||'')}</span>
1285
+ <div class="mem-meta">
1286
+ <span class="tag">${e.category||''}</span>
1287
+ <span class="tag">${escHtml(e.role||'')}</span>
1288
+ <span class="mem-session">${escHtml((e.session_id||'').split('_web_')[0])}</span>
1289
+ </div>
1290
+ </div>
1291
+ <div class="mem-content">${escHtml(content)}</div>
1292
+ </div>`;
1214
1293
  }
1215
- html+='</table></div>';
1294
+ html+='</div>';
1216
1295
  }else{
1217
1296
  html+='<div class="empty">未找到匹配的记忆</div>';
1218
1297
  }
@@ -1752,7 +1831,8 @@ async function deleteTask(taskId){
1752
1831
  });
1753
1832
  }
1754
1833
 
1755
- function closeModal(){$('modalContainer').innerHTML=''}
1834
+ let _deptTreeNeedsRefresh=false;
1835
+ function closeModal(){$('modalContainer').innerHTML='';if(_deptTreeNeedsRefresh){_deptTreeNeedsRefresh=false;renderDepartments()}}
1756
1836
 
1757
1837
  // ========== Organization ==========
1758
1838
  async function renderOrganization(){
@@ -1774,17 +1854,18 @@ async function renderOrganization(){
1774
1854
  <div class="form-group"><label>网站</label><input id="orgWebsite" value="${escHtml(inf.website||'')}" placeholder="https://..."></div>
1775
1855
  </div>
1776
1856
  <div class="flex gap-8 mt-16"><button class="btn btn-primary" onclick="saveOrgInfo()">保存信息</button></div></div>`;
1777
- // 知识库
1778
- html+=`<div class="card"><div class="flex justify-between items-center mb-16">
1857
+ // 知识库 — 未启用组织时隐藏
1858
+ const orgEnabled=cfg.enabled;
1859
+ html+=`<div class="card" id="orgKBCard"><div class="flex justify-between items-center mb-16">
1779
1860
  <h3 style="margin:0">组织知识库</h3>
1780
- <button class="btn btn-sm btn-primary" onclick="uploadOrgKnowledge(false)">上传文件</button> <button class="btn btn-sm btn-secondary" onclick="uploadOrgKnowledge(true)">📁 上传文件夹</button></div>
1781
- <div id="orgKBList"><div class="empty">加载中...</div></div></div>`;
1861
+ <div id="orgKBActions">${orgEnabled?'<button class="btn btn-sm btn-primary" onclick="uploadOrgKnowledge(false)">上传文件</button> <button class="btn btn-sm btn-secondary" onclick="uploadOrgKnowledge(true)">📁 上传文件夹</button>':''}</div></div>
1862
+ <div id="orgKBList"><div class="empty">${orgEnabled?'加载中...':'组织管理未启用,请先启用后再使用知识库'}</div></div></div>`;
1782
1863
  $('content').innerHTML=html;
1783
- loadOrgKnowledge();
1864
+ if(orgEnabled)loadOrgKnowledge();
1784
1865
  }
1785
1866
  async function saveOrgConfig(){
1786
1867
  const r=await api('/api/organization',{method:'PUT',body:JSON.stringify({enabled:$('orgEnabled').checked,knowledge_admin:$('orgAdmin').value})});
1787
- if(r.error){showToast(r.error,'danger');return}showToast('已保存','success');
1868
+ if(r.error){showToast(r.error,'danger');return}showToast('已保存','success');renderOrganization();
1788
1869
  }
1789
1870
  async function saveOrgInfo(){
1790
1871
  const r=await api('/api/organization/info',{method:'PUT',body:JSON.stringify({name:$('orgName').value,description:$('orgDesc').value,contact:$('orgContact').value,website:$('orgWebsite').value})});
@@ -1967,14 +2048,14 @@ async function addDeptAgent(path){
1967
2048
  if(sel)sel.disabled=true;if(btn)btn.disabled=true;
1968
2049
  try{
1969
2050
  const r=await api(`/api/departments/${encodeURIComponent(path)}/agents`,{method:'PUT',body:JSON.stringify({agents:[sel.value],action:'add'})});
1970
- if(r.error){showToast(r.error,'danger');return}
1971
- showToast('已添加','success');showDeptDetail(path);
2051
+ if(r.error||r.ok===false){showToast(r.error||r.message||'添加失败','danger');return}
2052
+ showToast('已添加','success');_deptTreeNeedsRefresh=true;showDeptDetail(path);
1972
2053
  }finally{if(sel)sel.disabled=false;if(btn)btn.disabled=false}
1973
2054
  }
1974
2055
  async function removeDeptAgent(path,agentName){
1975
2056
  const r=await api(`/api/departments/${encodeURIComponent(path)}/agents`,{method:'PUT',body:JSON.stringify({agents:[agentName],action:'remove'})});
1976
- if(r.error){showToast(r.error,'danger');return}
1977
- showToast('已移除','success');showDeptDetail(path);
2057
+ if(r.error||r.ok===false){showToast(r.error||r.message||'移除失败','danger');return}
2058
+ showToast('已移除','success');_deptTreeNeedsRefresh=true;showDeptDetail(path);
1978
2059
  }
1979
2060
  async function saveDeptInfo(path){
1980
2061
  // 同时保存名称、emoji 和元数据(描述、负责人)