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.
- package/agents/main_agent.py +18 -1
- package/config.py +3 -0
- package/core/vnc_manager.py +12 -4
- package/package.json +1 -1
- package/skills/chromedev_mcp.py +54 -27
- package/web/api_server.py +73 -4
- package/web/ui/chat/chat_main.js +4 -3
- package/web/ui/index.html +110 -29
package/agents/main_agent.py
CHANGED
|
@@ -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:
|
package/core/vnc_manager.py
CHANGED
|
@@ -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
|
-
#
|
|
727
|
-
await asyncio.sleep(
|
|
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
package/skills/chromedev_mcp.py
CHANGED
|
@@ -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
|
-
#
|
|
595
|
-
#
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3778
|
-
|
|
3779
|
-
|
|
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)
|
package/web/ui/chat/chat_main.js
CHANGED
|
@@ -2688,9 +2688,10 @@ function _renderMessagesInner() {
|
|
|
2688
2688
|
'</div>');
|
|
2689
2689
|
}
|
|
2690
2690
|
}
|
|
2691
|
-
// Agent files (v2_file events)
|
|
2692
|
-
|
|
2693
|
-
|
|
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&<.length){
|
|
1179
1249
|
const isSession=_memCategory==='session';
|
|
1180
|
-
|
|
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
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
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+='</
|
|
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="
|
|
1279
|
+
html+='<div class="mem-list">';
|
|
1211
1280
|
for(const e of r){
|
|
1212
1281
|
const content=(e.content||'').slice(0,300);
|
|
1213
|
-
html+=`<
|
|
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+='</
|
|
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
|
-
|
|
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
|
-
|
|
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"
|
|
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 和元数据(描述、负责人)
|