myagent-ai 1.18.5 → 1.18.7
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 +172 -21
- package/web/ui/chat/chat_main.js +15 -4
- package/web/ui/index.html +387 -108
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
|
@@ -373,11 +373,13 @@ class ApiServer:
|
|
|
373
373
|
r.add_get("/api/workdir", self.handle_get_workdir)
|
|
374
374
|
# ── Task Plan ──
|
|
375
375
|
r.add_get("/api/task-plan", self.handle_get_task_plan)
|
|
376
|
+
r.add_get("/api/task-plan/all", self.handle_get_all_task_plans)
|
|
376
377
|
r.add_put("/api/task-plan", self.handle_update_task_plan)
|
|
377
378
|
r.add_post("/api/task-plan", self.handle_add_task_item)
|
|
378
379
|
r.add_delete("/api/task-plan/{idx:int}", self.handle_delete_task_item)
|
|
379
380
|
r.add_put("/api/workdir", self.handle_set_workdir)
|
|
380
381
|
r.add_get("/api/workdir/files", self.handle_list_workdir)
|
|
382
|
+
r.add_get(r"/api/workdir/download/{path:.*}", self.handle_workdir_download)
|
|
381
383
|
r.add_get("/api/logs", self.handle_get_logs)
|
|
382
384
|
r.add_get("/api/logs/stream", self.handle_log_stream)
|
|
383
385
|
r.add_post("/api/chat", self.handle_chat)
|
|
@@ -2102,6 +2104,36 @@ window.toggleFullscreen = function() {{
|
|
|
2102
2104
|
self._task_list_store[store_key] = tasks
|
|
2103
2105
|
return web.json_response({"ok": True, "tasks": tasks, "session": session_id})
|
|
2104
2106
|
|
|
2107
|
+
async def handle_get_all_task_plans(self, request):
|
|
2108
|
+
"""GET /api/task-plan/all - 获取所有活跃的 task plan(用于后台管理任务列表)。
|
|
2109
|
+
|
|
2110
|
+
返回所有 _task_list_store 中的非空任务列表,包含 session_id/agent_path 信息。
|
|
2111
|
+
"""
|
|
2112
|
+
all_plans = []
|
|
2113
|
+
for key, tasks in self._task_list_store.items():
|
|
2114
|
+
if not tasks:
|
|
2115
|
+
continue
|
|
2116
|
+
# 统计状态
|
|
2117
|
+
status_counts = {"pending": 0, "running": 0, "done": 0}
|
|
2118
|
+
for t in tasks:
|
|
2119
|
+
s = t.get("status", "pending")
|
|
2120
|
+
status_counts[s] = status_counts.get(s, 0) + 1
|
|
2121
|
+
# 判断 key 类型
|
|
2122
|
+
is_session = key.startswith(("web_", "cli_", "group_", "tg_", "discord_", "feishu_"))
|
|
2123
|
+
all_plans.append({
|
|
2124
|
+
"key": key,
|
|
2125
|
+
"type": "session" if is_session else "agent",
|
|
2126
|
+
"total": len(tasks),
|
|
2127
|
+
"pending": status_counts["pending"],
|
|
2128
|
+
"running": status_counts["running"],
|
|
2129
|
+
"done": status_counts["done"],
|
|
2130
|
+
"tasks": tasks,
|
|
2131
|
+
"label": key.split("_web_")[0][:30] if is_session else key,
|
|
2132
|
+
})
|
|
2133
|
+
# 按 total 降序
|
|
2134
|
+
all_plans.sort(key=lambda x: x["total"], reverse=True)
|
|
2135
|
+
return web.json_response({"plans": all_plans, "total_keys": len(self._task_list_store)})
|
|
2136
|
+
|
|
2105
2137
|
async def handle_shutdown(self, request):
|
|
2106
2138
|
self.core._running = False
|
|
2107
2139
|
asyncio.create_task(self.core.shutdown())
|
|
@@ -2829,13 +2861,31 @@ window.toggleFullscreen = function() {{
|
|
|
2829
2861
|
async def handle_get_executor(self, request):
|
|
2830
2862
|
info = self.core.executor.get_execution_info() if self.core.executor else {}
|
|
2831
2863
|
cfg = self.core.config.executor if self.core.config else None
|
|
2864
|
+
# [v1.18.7] 附加执行锁状态和沙盒说明
|
|
2865
|
+
lock = self._execution_lock
|
|
2832
2866
|
return web.json_response({
|
|
2833
2867
|
**info,
|
|
2834
2868
|
"timeout": cfg.timeout if cfg else 300,
|
|
2835
2869
|
"auto_fix": cfg.auto_fix if cfg else True,
|
|
2836
2870
|
"max_output_length": cfg.max_output_length if cfg else 50000,
|
|
2871
|
+
# 执行锁信息
|
|
2872
|
+
"lock": {
|
|
2873
|
+
"locked": lock["locked"],
|
|
2874
|
+
"locked_by": lock["locked_by"],
|
|
2875
|
+
"locked_at": lock["locked_at"],
|
|
2876
|
+
},
|
|
2877
|
+
# 沙盒模式说明
|
|
2878
|
+
"sandbox_desc": self._sandbox_description(info.get("mode"), info.get("sandbox_type"), info.get("docker_available")),
|
|
2837
2879
|
})
|
|
2838
2880
|
|
|
2881
|
+
def _sandbox_description(self, mode, sandbox_type, docker_available):
|
|
2882
|
+
"""生成沙盒模式说明文本"""
|
|
2883
|
+
if mode != "sandbox":
|
|
2884
|
+
return "本机模式: 代码直接在宿主机运行,拥有完整系统权限。多个 Agent 共享全局执行锁,同一时间只有一个 Agent 可以执行代码。"
|
|
2885
|
+
if docker_available:
|
|
2886
|
+
return "Docker 沙盒: 代码在 Docker 容器中运行,通过网络隔离保证安全性。容器使用指定镜像,执行完毕后自动销毁。不走全局锁,支持多 Agent 并发。"
|
|
2887
|
+
return "轻量级进程沙盒: Docker 不可用时的降级方案。代码在临时工作目录中通过子进程执行,提供目录隔离。不走全局锁,支持多 Agent 并发。安全性低于 Docker 沙盒。"
|
|
2888
|
+
|
|
2839
2889
|
async def handle_update_executor(self, request):
|
|
2840
2890
|
data = await request.json()
|
|
2841
2891
|
cfg_path = self.core.config_mgr._config_file
|
|
@@ -3771,49 +3821,145 @@ window.toggleFullscreen = function() {{
|
|
|
3771
3821
|
return web.json_response({"ok": True})
|
|
3772
3822
|
|
|
3773
3823
|
async def handle_list_workdir(self, request):
|
|
3824
|
+
"""GET /api/workdir/files?path=xxx&recursive=1 - 列出工作目录文件
|
|
3825
|
+
|
|
3826
|
+
[v1.18.5] 支持:
|
|
3827
|
+
- path: 子目录相对路径(如 'userfiles/2026-04')
|
|
3828
|
+
- recursive: 递归列出子目录
|
|
3829
|
+
"""
|
|
3774
3830
|
wd = self.core.config_mgr.data_dir / "workspace"
|
|
3775
|
-
|
|
3831
|
+
sub_path = request.query.get("path", "").strip("/")
|
|
3832
|
+
if sub_path:
|
|
3833
|
+
target = wd / sub_path
|
|
3834
|
+
else:
|
|
3835
|
+
target = wd
|
|
3836
|
+
# 安全检查:防止路径遍历
|
|
3837
|
+
try:
|
|
3838
|
+
target = target.resolve()
|
|
3839
|
+
wd_resolved = wd.resolve()
|
|
3840
|
+
if not str(target).startswith(str(wd_resolved)):
|
|
3841
|
+
return web.json_response({"error": "非法路径"}, status=403)
|
|
3842
|
+
except Exception:
|
|
3843
|
+
return web.json_response({"error": "路径错误"}, status=400)
|
|
3844
|
+
|
|
3845
|
+
if not target.exists(): return web.json_response([])
|
|
3846
|
+
recursive = request.query.get("recursive", "") in ("1", "true")
|
|
3776
3847
|
items = []
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
|
|
3848
|
+
max_items = 500
|
|
3849
|
+
if recursive:
|
|
3850
|
+
for f in sorted(target.rglob("*")):
|
|
3851
|
+
if len(items) >= max_items: break
|
|
3852
|
+
try:
|
|
3853
|
+
if f.is_file():
|
|
3854
|
+
rel = str(f.relative_to(target))
|
|
3855
|
+
items.append({"name": f.name, "path": sub_path + "/" + rel if sub_path else rel, "type": "file", "size": f.stat().st_size})
|
|
3856
|
+
except Exception:
|
|
3857
|
+
pass
|
|
3858
|
+
else:
|
|
3859
|
+
for f in sorted(target.iterdir()):
|
|
3860
|
+
if len(items) >= max_items: break
|
|
3861
|
+
try:
|
|
3862
|
+
items.append({
|
|
3863
|
+
"name": f.name,
|
|
3864
|
+
"path": (sub_path + "/" + f.name) if sub_path else f.name,
|
|
3865
|
+
"type": "dir" if f.is_dir() else "file",
|
|
3866
|
+
"size": f.stat().st_size if f.is_file() else 0,
|
|
3867
|
+
})
|
|
3868
|
+
except Exception:
|
|
3869
|
+
pass
|
|
3780
3870
|
return web.json_response(items)
|
|
3781
3871
|
|
|
3872
|
+
async def handle_workdir_download(self, request):
|
|
3873
|
+
"""GET /api/workdir/download/{path} - 下载工作目录文件"""
|
|
3874
|
+
import urllib.parse
|
|
3875
|
+
rel_path = urllib.parse.unquote(request.match_info["path"]).strip("/")
|
|
3876
|
+
if not rel_path:
|
|
3877
|
+
return web.json_response({"error": "未指定文件"}, status=400)
|
|
3878
|
+
wd = self.core.config_mgr.data_dir / "workspace"
|
|
3879
|
+
target = wd / rel_path
|
|
3880
|
+
# 安全检查
|
|
3881
|
+
try:
|
|
3882
|
+
target = target.resolve()
|
|
3883
|
+
wd_resolved = wd.resolve()
|
|
3884
|
+
if not str(target).startswith(str(wd_resolved)):
|
|
3885
|
+
return web.json_response({"error": "非法路径"}, status=403)
|
|
3886
|
+
except Exception:
|
|
3887
|
+
return web.json_response({"error": "路径错误"}, status=400)
|
|
3888
|
+
if not target.exists() or not target.is_file():
|
|
3889
|
+
return web.json_response({"error": "文件不存在"}, status=404)
|
|
3890
|
+
import mimetypes
|
|
3891
|
+
ctype = mimetypes.guess_type(str(target))[0] or "application/octet-stream"
|
|
3892
|
+
return web.Response(
|
|
3893
|
+
body=target.read_bytes(),
|
|
3894
|
+
content_type=ctype,
|
|
3895
|
+
headers={"Content-Disposition": f'attachment; filename="{target.name}"'},
|
|
3896
|
+
)
|
|
3897
|
+
|
|
3782
3898
|
# --- Logs ---
|
|
3783
3899
|
async def handle_get_logs(self, request):
|
|
3784
3900
|
log_dir = self.core.config_mgr.logs_dir
|
|
3785
3901
|
lines = int(request.query.get("lines", "200"))
|
|
3786
3902
|
level = request.query.get("level", "").upper()
|
|
3787
3903
|
logs = []
|
|
3788
|
-
|
|
3904
|
+
# [v1.18.7] 遍历所有 .log 文件(不仅限于 myagent*.log),按修改时间倒序
|
|
3905
|
+
try:
|
|
3906
|
+
all_log_files = sorted(
|
|
3907
|
+
[f for f in log_dir.glob("*.log") if f.is_file()],
|
|
3908
|
+
key=lambda f: f.stat().st_mtime,
|
|
3909
|
+
reverse=True,
|
|
3910
|
+
)
|
|
3911
|
+
except Exception:
|
|
3912
|
+
all_log_files = []
|
|
3913
|
+
for lf in all_log_files:
|
|
3789
3914
|
try:
|
|
3790
3915
|
text = lf.read_text(encoding="utf-8", errors="ignore")
|
|
3791
3916
|
for line in text.strip().split("\n")[-lines:]:
|
|
3792
|
-
if level and level not in line:
|
|
3917
|
+
if level and level not in line:
|
|
3918
|
+
continue
|
|
3793
3919
|
logs.append(line)
|
|
3794
|
-
if len(logs) >= lines:
|
|
3795
|
-
|
|
3920
|
+
if len(logs) >= lines:
|
|
3921
|
+
break
|
|
3922
|
+
except Exception:
|
|
3923
|
+
pass
|
|
3796
3924
|
return web.json_response(logs[-lines:])
|
|
3797
3925
|
|
|
3798
3926
|
async def handle_log_stream(self, request):
|
|
3799
3927
|
resp = web.StreamResponse()
|
|
3800
3928
|
resp.content_type = "text/event-stream"
|
|
3801
3929
|
resp.headers["Cache-Control"] = "no-cache"
|
|
3930
|
+
resp.headers["Connection"] = "keep-alive"
|
|
3802
3931
|
await resp.prepare(request)
|
|
3803
|
-
|
|
3932
|
+
log_dir = self.core.config_mgr.logs_dir
|
|
3933
|
+
# [v1.18.7] 自动发现最新的日志文件而非硬编码 myagent.log
|
|
3804
3934
|
last_pos = 0
|
|
3935
|
+
last_file = None
|
|
3805
3936
|
try:
|
|
3806
3937
|
while True:
|
|
3807
3938
|
try:
|
|
3808
|
-
|
|
3809
|
-
|
|
3939
|
+
# 每 5 秒重新扫描最新日志文件(应对日志轮转)
|
|
3940
|
+
current_file = None
|
|
3941
|
+
if log_dir.exists():
|
|
3942
|
+
candidates = [f for f in log_dir.glob("*.log") if f.is_file()]
|
|
3943
|
+
if candidates:
|
|
3944
|
+
current_file = max(candidates, key=lambda f: f.stat().st_mtime)
|
|
3945
|
+
if current_file:
|
|
3946
|
+
# 日志文件切换时重置位置
|
|
3947
|
+
if last_file and current_file.resolve() != last_file.resolve():
|
|
3948
|
+
last_pos = 0
|
|
3949
|
+
last_file = current_file
|
|
3950
|
+
size = current_file.stat().st_size
|
|
3810
3951
|
if size > last_pos:
|
|
3811
|
-
with open(
|
|
3812
|
-
f.seek(last_pos)
|
|
3813
|
-
|
|
3814
|
-
|
|
3952
|
+
with open(current_file, "r", encoding="utf-8", errors="ignore") as f:
|
|
3953
|
+
f.seek(last_pos)
|
|
3954
|
+
new_data = f.read()
|
|
3955
|
+
last_pos = size
|
|
3956
|
+
if new_data:
|
|
3957
|
+
await resp.write(f"data: {json.dumps(new_data.strip())}\n\n")
|
|
3958
|
+
except Exception:
|
|
3959
|
+
pass
|
|
3815
3960
|
await asyncio.sleep(0.5)
|
|
3816
|
-
except asyncio.CancelledError:
|
|
3961
|
+
except asyncio.CancelledError:
|
|
3962
|
+
pass
|
|
3817
3963
|
return resp
|
|
3818
3964
|
|
|
3819
3965
|
# ── 配置管理 (热重载 / 导入 / 导出) ──
|
|
@@ -5861,6 +6007,8 @@ window.toggleFullscreen = function() {{
|
|
|
5861
6007
|
|
|
5862
6008
|
async def handle_list_org_knowledge(self, request):
|
|
5863
6009
|
"""GET /api/organization/knowledge - 列出组织知识库文件"""
|
|
6010
|
+
if not self.core.config.organization.enabled:
|
|
6011
|
+
return web.json_response([])
|
|
5864
6012
|
org_mgr = self._get_org_manager()
|
|
5865
6013
|
files = org_mgr.list_knowledge_files()
|
|
5866
6014
|
return web.json_response(files)
|
|
@@ -6058,11 +6206,14 @@ window.toggleFullscreen = function() {{
|
|
|
6058
6206
|
break
|
|
6059
6207
|
image_data.extend(chunk)
|
|
6060
6208
|
|
|
6061
|
-
#
|
|
6062
|
-
|
|
6063
|
-
|
|
6064
|
-
|
|
6065
|
-
|
|
6209
|
+
# 解析裁剪参数([v1.18.7] 防止 NaN 传入导致 ValueError)
|
|
6210
|
+
try:
|
|
6211
|
+
crop_x = int(float(request.query.get("crop_x", 0)))
|
|
6212
|
+
crop_y = int(float(request.query.get("crop_y", 0)))
|
|
6213
|
+
crop_w = int(float(request.query.get("crop_w", 0)))
|
|
6214
|
+
crop_h = int(float(request.query.get("crop_h", 0)))
|
|
6215
|
+
except (ValueError, TypeError):
|
|
6216
|
+
crop_x = crop_y = crop_w = crop_h = 0
|
|
6066
6217
|
out_size = min(int(request.query.get("size", 128)), 512)
|
|
6067
6218
|
|
|
6068
6219
|
# 使用 Pillow 处理图片
|
package/web/ui/chat/chat_main.js
CHANGED
|
@@ -1871,14 +1871,24 @@ function renderSessions(filter = '') {
|
|
|
1871
1871
|
s.id !== '__new__' && (!fl || s.name.toLowerCase().includes(fl) || s.id.toLowerCase().includes(fl))
|
|
1872
1872
|
);
|
|
1873
1873
|
|
|
1874
|
+
const agent = findAgentByPath(state.activeAgent);
|
|
1874
1875
|
list.innerHTML = filtered.map(s => {
|
|
1875
1876
|
const previewText = s.preview
|
|
1876
1877
|
? escapeHtml(s.preview.length > 40 ? s.preview.slice(0, 40) + '...' : s.preview)
|
|
1877
1878
|
: (s.messages > 0 ? s.messages + ' 条消息' : '暂无消息');
|
|
1879
|
+
// [v1.18.7] 用 agent 头像替代固定 💬 图标,头像在前、会话名在后
|
|
1880
|
+
var _avatarContent = '';
|
|
1881
|
+
if (s.id === '__new__') {
|
|
1882
|
+
_avatarContent = '✨';
|
|
1883
|
+
} else if (agent && agent.avatar_image) {
|
|
1884
|
+
_avatarContent = '<img src="' + escapeHtml(agent.avatar_image) + '" style="width:100%;height:100%;object-fit:cover;border-radius:8px" onerror="this.outerHTML=\'' + escapeHtml(agent.avatar_emoji || '🤖') + '\'">';
|
|
1885
|
+
} else {
|
|
1886
|
+
_avatarContent = agent ? (agent.avatar_emoji || '🤖') : '💬';
|
|
1887
|
+
}
|
|
1878
1888
|
return `
|
|
1879
1889
|
<div class="session-item ${s.id === state.activeSessionId ? 'active' : ''}"
|
|
1880
1890
|
onclick="selectSession('${escapeHtml(s.id)}')" title="${escapeHtml(s.id)}">
|
|
1881
|
-
<div class="session-icon">${
|
|
1891
|
+
<div class="session-icon">${_avatarContent}</div>
|
|
1882
1892
|
<div class="session-info">
|
|
1883
1893
|
<div class="session-name">${escapeHtml(s.name)}</div>
|
|
1884
1894
|
<div class="session-preview">${previewText}</div>
|
|
@@ -2688,9 +2698,10 @@ function _renderMessagesInner() {
|
|
|
2688
2698
|
'</div>');
|
|
2689
2699
|
}
|
|
2690
2700
|
}
|
|
2691
|
-
// Agent files (v2_file events)
|
|
2692
|
-
|
|
2693
|
-
|
|
2701
|
+
// Agent files (v2_file events) — 支持实时流式 _files 和历史加载的 files
|
|
2702
|
+
const agentFiles = (msg._files || (msg.files && !isUser ? msg.files : []));
|
|
2703
|
+
if (agentFiles.length > 0) {
|
|
2704
|
+
for (const f of agentFiles) {
|
|
2694
2705
|
const fileId = f.id;
|
|
2695
2706
|
const sizeStr = f.size ? formatFileSize(f.size) : '';
|
|
2696
2707
|
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||[];
|
|
@@ -565,7 +635,8 @@ async function doCreateAgent(){
|
|
|
565
635
|
name,description:$('caDesc').value,avatar_emoji:$('caEmoji').value,
|
|
566
636
|
avatar_color:$('caColorText').value,execution_mode:$('caExecMode').value,
|
|
567
637
|
model_id:$('caModelId').value,system_prompt:$('caPrompt').value,
|
|
568
|
-
work_dir:$('caWorkDir').value,department:$('caDept').value
|
|
638
|
+
work_dir:$('caWorkDir').value,department:$('caDept').value,
|
|
639
|
+
avatar_image:$('caAvatarImage')?.value||''
|
|
569
640
|
})});
|
|
570
641
|
if(r.error){showToast(r.error,'danger');return}
|
|
571
642
|
closeModal();showToast('Agent 创建成功','success');renderAgents();
|
|
@@ -694,7 +765,8 @@ async function doSaveAgent(path){
|
|
|
694
765
|
description:$('eaDesc').value,avatar_emoji:$('eaEmoji').value,avatar_color:$('eaColorText').value,
|
|
695
766
|
model_id:$('eaModelId').value,
|
|
696
767
|
backup_model_ids:Array.from($('eaBackupModels').selectedOptions).map(o=>o.value),
|
|
697
|
-
work_dir:$('eaWorkDir').value,system_prompt:$('eaPrompt').value
|
|
768
|
+
work_dir:$('eaWorkDir').value,system_prompt:$('eaPrompt').value,
|
|
769
|
+
avatar_image:$('eaAvatarImage')?.value||''
|
|
698
770
|
})});
|
|
699
771
|
if(r.error){showToast(r.error,'danger');return}
|
|
700
772
|
showToast('已保存','success');renderAgents();
|
|
@@ -1177,26 +1249,25 @@ async function renderMemory(){
|
|
|
1177
1249
|
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
1250
|
if(lt&<.length){
|
|
1179
1251
|
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;
|
|
1252
|
+
html+='<div class="mem-list">';
|
|
1187
1253
|
for(const e of lt){
|
|
1188
1254
|
const content=(e.content||e.summary||'')||(e.role==='user'?'[用户消息]':e.role==='assistant'?'[助手回复]':'[系统]');
|
|
1189
|
-
|
|
1190
1255
|
let contentPreview=escHtml(content.slice(0,300));
|
|
1191
1256
|
if(content.length>300)contentPreview+=`<span style="color:var(--text3)">... (${content.length}字)</span>`;
|
|
1192
|
-
html
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1257
|
+
html+=`<div class="mem-card">
|
|
1258
|
+
<div class="mem-card-header">
|
|
1259
|
+
<span class="mem-key">${escHtml(e.key||e.role||'-')}</span>
|
|
1260
|
+
<div class="mem-meta">
|
|
1261
|
+
<span class="tag">${escHtml(e.role||'')}</span>
|
|
1262
|
+
${!isSession&&e.importance!=null?'<span class="tag tag-imp">'+e.importance.toFixed(2)+'</span>':''}
|
|
1263
|
+
${e.session_id?'<span class="mem-session" title="'+escHtml(e.session_id)+'">'+escHtml((e.session_id||'').split('_web_')[0])+'</span>':''}
|
|
1264
|
+
</div>
|
|
1265
|
+
</div>
|
|
1266
|
+
<div class="mem-content">${contentPreview}</div>
|
|
1267
|
+
<div class="mem-actions"><button class="btn btn-sm btn-danger" onclick="deleteMemory('${e.id}')">删除</button></div>
|
|
1268
|
+
</div>`;
|
|
1198
1269
|
}
|
|
1199
|
-
html+='</
|
|
1270
|
+
html+='</div>';
|
|
1200
1271
|
}else{
|
|
1201
1272
|
html+='<div class="empty">暂无'+(_memCategory==='session'?'会话':'全局')+'记忆</div>';
|
|
1202
1273
|
}
|
|
@@ -1207,12 +1278,22 @@ async function searchMemory(){
|
|
|
1207
1278
|
const r=await api('/api/memory/search?q='+encodeURIComponent(q));
|
|
1208
1279
|
let html='<h3>搜索结果: '+(r.length||0)+' 条</h3>';
|
|
1209
1280
|
if(r&&r.length){
|
|
1210
|
-
html+='<div class="
|
|
1281
|
+
html+='<div class="mem-list">';
|
|
1211
1282
|
for(const e of r){
|
|
1212
1283
|
const content=(e.content||'').slice(0,300);
|
|
1213
|
-
html+=`<
|
|
1284
|
+
html+=`<div class="mem-card">
|
|
1285
|
+
<div class="mem-card-header">
|
|
1286
|
+
<span class="mem-key">${escHtml(e.key||'')}</span>
|
|
1287
|
+
<div class="mem-meta">
|
|
1288
|
+
<span class="tag">${e.category||''}</span>
|
|
1289
|
+
<span class="tag">${escHtml(e.role||'')}</span>
|
|
1290
|
+
<span class="mem-session">${escHtml((e.session_id||'').split('_web_')[0])}</span>
|
|
1291
|
+
</div>
|
|
1292
|
+
</div>
|
|
1293
|
+
<div class="mem-content">${escHtml(content)}</div>
|
|
1294
|
+
</div>`;
|
|
1214
1295
|
}
|
|
1215
|
-
html+='</
|
|
1296
|
+
html+='</div>';
|
|
1216
1297
|
}else{
|
|
1217
1298
|
html+='<div class="empty">未找到匹配的记忆</div>';
|
|
1218
1299
|
}
|
|
@@ -1546,29 +1627,63 @@ async function deleteModel(id,name){
|
|
|
1546
1627
|
|
|
1547
1628
|
// ========== Executor ==========
|
|
1548
1629
|
async function renderExecutor(){
|
|
1549
|
-
const e=await api('/api/executor');
|
|
1630
|
+
const e=await api('/api/executor');if(!e)return;
|
|
1631
|
+
const isSandbox=e.mode==='sandbox';const dockerOk=e.docker_available;
|
|
1632
|
+
const lock=e.lock||{};
|
|
1633
|
+
const sandboxType=e.sandbox_type||'';
|
|
1634
|
+
const sandboxDesc=e.sandbox_desc||'';
|
|
1550
1635
|
let html=`<div class="card"><h3>执行模式</h3>
|
|
1551
|
-
<div style="display:flex;gap:12px;margin-bottom:16px">
|
|
1552
|
-
<label style="display:flex;align-items:center;gap:6px;cursor:pointer;padding:16px 24px;border-radius:var(--radius);border:2px solid ${!isSandbox?'var(--primary)':'var(--border)'};background:${!isSandbox?'#6366f122':'transparent'};flex:1">
|
|
1636
|
+
<div style="display:flex;gap:12px;margin-bottom:16px;flex-wrap:wrap">
|
|
1637
|
+
<label style="display:flex;align-items:center;gap:6px;cursor:pointer;padding:16px 24px;border-radius:var(--radius);border:2px solid ${!isSandbox?'var(--primary)':'var(--border)'};background:${!isSandbox?'#6366f122':'transparent'};flex:1;min-width:200px">
|
|
1553
1638
|
<input type="radio" name="execMode" value="local" ${!isSandbox?'checked':''} onchange="switchMode('local')">
|
|
1554
1639
|
<div><strong>🖥️ 本机执行</strong><br><span style="font-size:12px;color:var(--text2)">直接在本机运行代码,功能完整,速度最快</span></div></label>
|
|
1555
|
-
<label style="display:flex;align-items:center;gap:6px;cursor:pointer;padding:16px 24px;border-radius:var(--radius);border:2px solid ${isSandbox?'var(--primary)':'var(--border)'};background:${isSandbox?'#6366f122':'transparent'};flex:1">
|
|
1640
|
+
<label style="display:flex;align-items:center;gap:6px;cursor:pointer;padding:16px 24px;border-radius:var(--radius);border:2px solid ${isSandbox?'var(--primary)':'var(--border)'};background:${isSandbox?'#6366f122':'transparent'};flex:1;min-width:200px">
|
|
1556
1641
|
<input type="radio" name="execMode" value="sandbox" ${isSandbox?'checked':''} onchange="switchMode('sandbox')" ${!dockerOk?'disabled':''}>
|
|
1557
|
-
<div><strong>📦
|
|
1558
|
-
<div style="font-size:13px;color:var(--text2)
|
|
1642
|
+
<div><strong>📦 沙盒执行</strong><br><span style="font-size:12px;color:var(--text2)">${dockerOk?'Docker 隔离容器':'轻量级进程沙盒'}${!dockerOk?' (Docker 不可用)':''}</span></div></label></div>
|
|
1643
|
+
<div style="font-size:13px;color:var(--text2);display:flex;flex-wrap:wrap;gap:8px">
|
|
1644
|
+
<span>当前模式: <span class="badge ${isSandbox?'badge-yellow':'badge-green'}">${isSandbox?'沙盒':'本机'}</span></span>
|
|
1645
|
+
${isSandbox?`<span>沙盒类型: <span class="tag">${sandboxType==='docker'?'Docker':'轻量级进程'}</span></span>`:''}
|
|
1646
|
+
<span>Docker: <span class="badge ${dockerOk?'badge-green':'badge-red'}">${dockerOk?'可用':'不可用'}</span></span>
|
|
1647
|
+
<span>累计执行: <span class="tag">${e.execution_count||0} 次</span></span>
|
|
1648
|
+
</div>
|
|
1649
|
+
<div style="margin-top:8px;padding:10px 14px;border-radius:var(--radius);background:var(--surface);font-size:12px;color:var(--text2);line-height:1.6">${escHtml(sandboxDesc)}</div>
|
|
1650
|
+
</div>`;
|
|
1651
|
+
// 执行锁状态
|
|
1652
|
+
html+=`<div class="card"><h3>🔒 全局执行锁</h3>
|
|
1653
|
+
<div style="font-size:13px">
|
|
1654
|
+
${lock.locked?
|
|
1655
|
+
`<span class="badge badge-red">已锁定</span> <strong>${escHtml(lock.locked_by||'')}</strong> — 锁定于 ${escHtml(lock.locked_at||'')}
|
|
1656
|
+
<div style="margin-top:8px"><button class="btn btn-sm btn-danger" onclick="releaseLock()">🔓 释放锁</button></div>`:
|
|
1657
|
+
`<span class="badge badge-green">未锁定</span> — 所有 Agent 可自由运行`
|
|
1658
|
+
}
|
|
1659
|
+
<div style="margin-top:6px;font-size:12px;color:var(--text3)">${isSandbox?'沙盒模式不走全局锁,支持多 Agent 并发执行':'本机模式下多个 Agent 共享此锁,同一时间只有一个 Agent 可以执行代码'}</div>
|
|
1660
|
+
</div></div>`;
|
|
1661
|
+
// 沙盒设置
|
|
1559
1662
|
html+=`<div class="card"><h3>沙盒设置</h3><div class="form-row">
|
|
1560
|
-
<div class="form-group"><label>Docker 镜像</label><input id="sbImage" value="${e.sandbox_image||'python:3.12-slim'}"></div>
|
|
1561
|
-
<div class="form-group"><label>内存限制</label><input id="sbMemory" value="${e.sandbox_memory||'512m'}" placeholder="512m"></div>
|
|
1663
|
+
<div class="form-group"><label>Docker 镜像</label><input id="sbImage" value="${escHtml(e.sandbox_image||'python:3.12-slim')}"></div>
|
|
1664
|
+
<div class="form-group"><label>内存限制</label><input id="sbMemory" value="${escHtml(e.sandbox_memory||'512m')}" placeholder="512m"></div>
|
|
1562
1665
|
<div class="form-group"><label>网络访问</label><select id="sbNetwork"><option ${!e.sandbox_network?'selected':''} value="false">禁止 (更安全)</option><option ${e.sandbox_network?'selected':''} value="true">允许</option></select></div></div>
|
|
1563
1666
|
<div class="flex gap-8 mt-16"><button class="btn btn-primary" onclick="saveExecutor()">保存设置</button></div></div>`;
|
|
1667
|
+
// 执行参数
|
|
1564
1668
|
html+=`<div class="card"><h3>执行参数</h3><div class="form-row">
|
|
1565
1669
|
<div class="form-group"><label>超时时间 (秒)</label><input id="exTimeout" type="number" value="${e.timeout||300}"></div>
|
|
1566
|
-
<div class="form-group"><label
|
|
1567
|
-
<div class="form-group"><label
|
|
1670
|
+
<div class="form-group"><label>自动修复</label><select id="exAutoFix"><option ${e.auto_fix?'selected':''} value="true">开启</option><option ${!e.auto_fix?'selected':''} value="false">关闭</option></select></div>
|
|
1671
|
+
<div class="form-group"><label>最大输出长度</label><input id="exMaxOutput" type="number" value="${e.max_output_length||50000}"></div></div>
|
|
1672
|
+
<div class="flex gap-8 mt-16"><button class="btn btn-primary" onclick="saveExecutor()">保存参数</button></div></div>`;
|
|
1568
1673
|
$('content').innerHTML=html;
|
|
1569
1674
|
}
|
|
1570
|
-
async function switchMode(mode){const r=await api('/api/executor',{method:'PUT',body:JSON.stringify({execution_mode:mode})});if(!r.ok){showToast('切换失败: '+(r.error||''),'danger');renderExecutor();}else
|
|
1571
|
-
async function saveExecutor(){
|
|
1675
|
+
async function switchMode(mode){const r=await api('/api/executor',{method:'PUT',body:JSON.stringify({execution_mode:mode})});if(!r.ok){showToast('切换失败: '+(r.error||''),'danger');renderExecutor();}else{showToast('已切换','success');renderExecutor();}}
|
|
1676
|
+
async function saveExecutor(){
|
|
1677
|
+
const body={sandbox_image:$('sbImage').value,sandbox_memory:$('sbMemory').value,sandbox_network:$('sbNetwork').value==='true',timeout:parseInt($('exTimeout').value),auto_fix:$('exAutoFix').value==='true',max_output_length:parseInt($('exMaxOutput').value)};
|
|
1678
|
+
const r=await api('/api/executor',{method:'PUT',body:JSON.stringify(body)});
|
|
1679
|
+
if(r.ok)showToast('已保存','success');else showToast('保存失败: '+(r.error||''),'danger');
|
|
1680
|
+
renderExecutor();
|
|
1681
|
+
}
|
|
1682
|
+
async function releaseLock(){
|
|
1683
|
+
const r=await api('/api/execution-lock',{method:'POST',body:JSON.stringify({action:'release'})});
|
|
1684
|
+
if(r.ok)showToast('锁已释放','success');else showToast('释放失败: '+(r.error||''),'danger');
|
|
1685
|
+
renderExecutor();
|
|
1686
|
+
}
|
|
1572
1687
|
|
|
1573
1688
|
// ========== Skills ==========
|
|
1574
1689
|
async function renderSkills(){
|
|
@@ -1640,95 +1755,238 @@ async function viewSkillDetail(name){
|
|
|
1640
1755
|
}
|
|
1641
1756
|
|
|
1642
1757
|
// ========== Files ==========
|
|
1758
|
+
var _workdirAgent=''; // 当前选中的 agent(空=全局工作目录)
|
|
1643
1759
|
async function renderFiles(){
|
|
1644
|
-
const wd=await api('/api/workdir')
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
<
|
|
1648
|
-
<
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1760
|
+
const [wd,agents]=await Promise.all([api('/api/workdir'),api('/api/agents').catch(()=>[])]);
|
|
1761
|
+
const agentList=Array.isArray(agents)?agents.filter(a=>!a.system&&a.path!=='default'):[];
|
|
1762
|
+
let html=`<div class="flex items-center gap-8 mb-16 flex-wrap">
|
|
1763
|
+
<span style="font-size:14px;color:var(--text2)">📁 工作目录: <strong>${escHtml(wd.path||'')}</strong></span>
|
|
1764
|
+
<select id="workdirAgentSelect" onchange="onWorkdirAgentChange()" style="width:auto">
|
|
1765
|
+
<option value="">全局工作目录</option>
|
|
1766
|
+
${agentList.map(a=>`<option value="${escHtml(a.path)}">${escHtml((a.avatar_emoji||'🤖')+' '+a.name)} (${escHtml(a.path)})</option>`).join('')}
|
|
1767
|
+
</select>
|
|
1768
|
+
<button class="btn btn-sm btn-ghost" onclick="changeWorkdir()">更改全局</button>
|
|
1769
|
+
<button class="btn btn-sm btn-ghost" onclick="renderFiles()">🔄 刷新</button></div>
|
|
1770
|
+
<div id="workdirContent">加载中...</div>`;
|
|
1771
|
+
$('content').innerHTML=html;
|
|
1772
|
+
loadWorkdirContent('');
|
|
1773
|
+
}
|
|
1774
|
+
function onWorkdirAgentChange(){
|
|
1775
|
+
_workdirAgent=$('workdirAgentSelect').value;
|
|
1776
|
+
loadWorkdirContent('');
|
|
1777
|
+
}
|
|
1778
|
+
async function loadWorkdirContent(subPath){
|
|
1779
|
+
const params=new URLSearchParams();
|
|
1780
|
+
if(subPath)params.set('path',subPath);
|
|
1781
|
+
const files=await api('/api/workdir/files?'+params.toString());
|
|
1782
|
+
const el=document.getElementById('workdirContent');
|
|
1783
|
+
if(!el)return;
|
|
1784
|
+
// 面包屑
|
|
1785
|
+
var bcHtml='';
|
|
1786
|
+
if(subPath){
|
|
1787
|
+
var parts=subPath.split('/');var crumbs=['<span style="cursor:pointer;color:var(--primary)" onclick="loadWorkdirContent(\'\')">根目录</span>'];var acc='';
|
|
1788
|
+
for(var i=0;i<parts.length;i++){acc+=(acc?'/':'')+parts[i];crumbs.push(' / <span style="cursor:pointer;color:var(--primary)" onclick="loadWorkdirContent(\''+acc+'\')">'+escHtml(parts[i])+'</span>');}
|
|
1789
|
+
bcHtml=crumbs.join('');
|
|
1790
|
+
}else{bcHtml='<span style="color:var(--text3)">根目录</span>';}
|
|
1791
|
+
// Agent 工作目录信息
|
|
1792
|
+
if(_workdirAgent){
|
|
1793
|
+
var agentCfg=await api('/api/agents/'+encodeURIComponent(_workdirAgent)).catch(()=>null);
|
|
1794
|
+
if(agentCfg&&agentCfg.work_dir){
|
|
1795
|
+
bcHtml+=` <span style="margin-left:12px;font-size:11px;color:var(--text3)">Agent 工作目录: ${escHtml(agentCfg.work_dir)}</span>`;
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
var html=`<div style="font-size:12px;margin-bottom:8px">${bcHtml}</div>`;
|
|
1799
|
+
html+='<div class="table-wrap"><table><tr><th>名称</th><th>大小</th><th></th></tr>';
|
|
1800
|
+
if(!files||!files.length){
|
|
1801
|
+
html+='<tr><td colspan="3" class="empty">目录为空</td></tr>';
|
|
1802
|
+
}else{
|
|
1803
|
+
// 排序:目录在前
|
|
1804
|
+
var dirs=files.filter(f=>f.type==='dir').sort((a,b)=>a.name.localeCompare(b.name));
|
|
1805
|
+
var fils=files.filter(f=>f.type==='file').sort((a,b)=>a.name.localeCompare(b.name));
|
|
1806
|
+
for(const d of dirs){
|
|
1807
|
+
var dp=d.path||d.name;
|
|
1808
|
+
html+=`<tr style="cursor:pointer" onclick="loadWorkdirContent('${escHtml(dp)}')"><td>📂 ${escHtml(d.name)}</td><td>-</td><td></td></tr>`;
|
|
1809
|
+
}
|
|
1810
|
+
for(const f of fils){
|
|
1811
|
+
var fp=f.path||f.name;
|
|
1812
|
+
var sizeStr=f.size>1048576?(f.size/1048576).toFixed(1)+' MB':f.size>1024?(f.size/1024).toFixed(1)+' KB':f.size+' B';
|
|
1813
|
+
html+=`<tr><td>📄 ${escHtml(f.name)}</td><td>${sizeStr}</td>
|
|
1814
|
+
<td><button class="btn btn-sm btn-ghost" onclick="downloadWorkdirFileGlobal('${escHtml(fp)}')">下载</button></td></tr>`;
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
html+='</table></div>';
|
|
1818
|
+
el.innerHTML=html;
|
|
1819
|
+
}
|
|
1820
|
+
function downloadWorkdirFileGlobal(relPath){
|
|
1821
|
+
var link=document.createElement('a');
|
|
1822
|
+
link.href=API+'/api/workdir/download/'+encodeURIComponent(relPath);
|
|
1823
|
+
link.download='';
|
|
1824
|
+
document.body.appendChild(link);link.click();document.body.removeChild(link);
|
|
1825
|
+
}
|
|
1826
|
+
async function changeWorkdir(){const p=prompt('新路径:');if(!p)return;await api('/api/workdir',{method:'PUT',body:JSON.stringify({path:p})});showToast('已更新','success');renderFiles();}
|
|
1657
1827
|
|
|
1658
1828
|
// ========== Logs ==========
|
|
1659
1829
|
async function renderLogs(){
|
|
1660
|
-
let html=`<div class="flex gap-8 mb-16">
|
|
1661
|
-
<select id="logLines" style="width:auto"><option value="100">100 行</option><option value="500" selected>500 行</option><option value="1000">1000 行</option></select>
|
|
1830
|
+
let html=`<div class="flex gap-8 mb-16 flex-wrap">
|
|
1831
|
+
<select id="logLines" style="width:auto"><option value="100">100 行</option><option value="500" selected>500 行</option><option value="1000">1000 行</option><option value="2000">2000 行</option></select>
|
|
1832
|
+
<select id="logLevel" style="width:auto"><option value="">全部级别</option><option value="ERROR">ERROR</option><option value="WARNING">WARNING</option><option value="INFO">INFO</option><option value="DEBUG">DEBUG</option></select>
|
|
1662
1833
|
<button class="btn btn-primary" onclick="loadLogs()">刷新</button>
|
|
1663
|
-
<button class="btn btn-ghost" onclick="toggleLogStream()" id="streamBtn"
|
|
1664
|
-
|
|
1834
|
+
<button class="btn btn-ghost" onclick="toggleLogStream()" id="streamBtn">▶ 实时日志</button>
|
|
1835
|
+
<button class="btn btn-sm btn-ghost" onclick="clearLogViewer()" title="清空显示">🗑️ 清屏</button></div>
|
|
1836
|
+
<div class="log-viewer" id="logViewer" style="height:calc(100vh - 240px);font-family:monospace;font-size:12px;line-height:1.6;overflow-y:auto;background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:8px 12px">加载中...</div>`;
|
|
1665
1837
|
$('content').innerHTML=html;loadLogs();
|
|
1666
1838
|
}
|
|
1667
1839
|
async function loadLogs(){
|
|
1668
|
-
const lines=$('logLines').value;const
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1840
|
+
const lines=$('logLines').value;const level=$('logLevel').value;
|
|
1841
|
+
const logs=await api(`/api/logs?lines=${lines}${level?'&level='+level:''}`);
|
|
1842
|
+
const viewer=$('logViewer');
|
|
1843
|
+
viewer.innerHTML='';
|
|
1844
|
+
if(!logs||!logs.length){viewer.innerHTML='<div style="color:var(--text3)">暂无日志</div>';return;}
|
|
1845
|
+
for(const l of logs){appendLogLine(viewer,l);}
|
|
1846
|
+
viewer.scrollTop=viewer.scrollHeight;
|
|
1847
|
+
}
|
|
1848
|
+
function appendLogLine(viewer,line){
|
|
1849
|
+
var div=document.createElement('div');
|
|
1850
|
+
div.style.cssText='white-space:pre-wrap;word-break:break-all;padding:1px 0;border-bottom:1px solid var(--border)';
|
|
1851
|
+
// 高亮 ERROR/WARNING
|
|
1852
|
+
if(line.includes('ERROR')||line.includes('CRITICAL')){div.style.color='var(--danger)';div.style.fontWeight='600';}
|
|
1853
|
+
else if(line.includes('WARNING')){div.style.color='#f59e0b';}
|
|
1854
|
+
else if(line.includes('DEBUG')){div.style.color='var(--text3)';}
|
|
1855
|
+
div.textContent=line;
|
|
1856
|
+
viewer.appendChild(div);
|
|
1857
|
+
}
|
|
1858
|
+
function clearLogViewer(){if($('logViewer'))$('logViewer').innerHTML='';}
|
|
1859
|
+
let logStreamActive=false;let _logES=null;
|
|
1673
1860
|
async function toggleLogStream(){
|
|
1674
|
-
if(logStreamActive){
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1861
|
+
if(logStreamActive){
|
|
1862
|
+
logStreamActive=false;
|
|
1863
|
+
if(_logES){_logES.close();_logES=null;}
|
|
1864
|
+
$('streamBtn').textContent='▶ 实时日志';
|
|
1865
|
+
$('streamBtn').style.background='';return;
|
|
1866
|
+
}
|
|
1867
|
+
logStreamActive=true;$('streamBtn').textContent='⏹ 停止实时';$('streamBtn').style.background='var(--danger)22';
|
|
1868
|
+
_logES=new EventSource(API+'/api/logs/stream');
|
|
1869
|
+
_logES.onmessage=function(e){
|
|
1870
|
+
try{
|
|
1871
|
+
var lines=JSON.parse(e.data).split('\n');
|
|
1872
|
+
var viewer=$('logViewer');if(!viewer)return;
|
|
1873
|
+
for(const l of lines){if(l.trim())appendLogLine(viewer,l);}
|
|
1874
|
+
viewer.scrollTop=viewer.scrollHeight;
|
|
1875
|
+
while(viewer.children.length>3000)viewer.removeChild(viewer.firstChild);
|
|
1876
|
+
}catch(ex){}
|
|
1877
|
+
};
|
|
1878
|
+
// [v1.18.7] 断线自动重连
|
|
1879
|
+
_logES.onerror=function(){
|
|
1880
|
+
if(_logES){_logES.close();_logES=null;}
|
|
1881
|
+
if(logStreamActive){
|
|
1882
|
+
$('streamBtn').textContent='⟳ 重连中...';
|
|
1883
|
+
setTimeout(function(){
|
|
1884
|
+
if(logStreamActive){toggleLogStream();}// 重新连接
|
|
1885
|
+
},3000);
|
|
1886
|
+
}
|
|
1887
|
+
};
|
|
1679
1888
|
}
|
|
1680
1889
|
|
|
1681
1890
|
// ========== Tasks ==========
|
|
1682
1891
|
async function renderTasks(){
|
|
1683
|
-
|
|
1684
|
-
const
|
|
1685
|
-
const
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1892
|
+
// [v1.18.7] 同时获取实时 task plan 和持久化任务
|
|
1893
|
+
const [planR,persistR]=await Promise.all([api('/api/task-plan/all').catch(()=>({plans:[],total_keys:0})),api('/api/tasks').catch(()=>({tasks:[],total:0}))]);
|
|
1894
|
+
const plans=planR.plans||[];
|
|
1895
|
+
const persistTasks=persistR.tasks||[];
|
|
1896
|
+
// 统计
|
|
1897
|
+
var totalPlans=0,totalDone=0,totalPending=0,totalRunning=0;
|
|
1898
|
+
for(const p of plans){totalPlans+=p.total;totalDone+=p.done;totalPending+=p.pending;totalRunning+=p.running;}
|
|
1899
|
+
// 持久化任务统计
|
|
1900
|
+
var ptPending=0,ptRunning=0,ptCompleted=0,ptFailed=0;
|
|
1901
|
+
for(const t of persistTasks){if(t.status==='pending')ptPending++;else if(t.status==='running')ptRunning++;else if(t.status==='completed')ptCompleted++;else if(t.status==='failed')ptFailed++;}
|
|
1902
|
+
let html=`<div class="card" style="margin-bottom:16px"><h3>📊 概览</h3>
|
|
1903
|
+
<div class="grid grid-4" style="margin-top:12px">
|
|
1904
|
+
<div class="stat"><div class="label">活跃 Task Plan</div><div class="value">${plans.length}</div><div style="font-size:11px;color:var(--text3)">${totalPlans} 项任务</div></div>
|
|
1905
|
+
<div class="stat"><div class="label">待执行</div><div class="value">${totalPending}</div><div style="font-size:11px;color:var(--text3)">pending</div></div>
|
|
1906
|
+
<div class="stat"><div class="label">已完成</div><div class="value" style="color:var(--success)">${totalDone}</div><div style="font-size:11px;color:var(--text3)">done</div></div>
|
|
1907
|
+
<div class="stat"><div class="label">执行中</div><div class="value" style="color:var(--info)">${totalRunning}</div><div style="font-size:11px;color:var(--text3)">running</div></div>
|
|
1908
|
+
</div>
|
|
1909
|
+
${persistTasks.length?`<div style="margin-top:12px;padding-top:12px;border-top:1px solid var(--border);font-size:13px;color:var(--text2)">
|
|
1910
|
+
持久化任务: ${ptPending} 待处理 · ${ptRunning} 运行中 · <span style="color:var(--success)">${ptCompleted} 已完成</span> · <span style="color:var(--danger)">${ptFailed} 失败</span>
|
|
1911
|
+
</div>`:''}
|
|
1912
|
+
</div>`;
|
|
1692
1913
|
html+=`<div class="flex gap-8 mb-16 flex-wrap">
|
|
1693
|
-
<
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1914
|
+
<button class="btn btn-sm btn-ghost" onclick="renderTasks()">🔄 刷新</button></div>`;
|
|
1915
|
+
// 实时 Task Plan 列表
|
|
1916
|
+
if(plans.length){
|
|
1917
|
+
html+='<h3 style="margin-bottom:8px">📋 实时任务计划 ('+plans.length+' 个活跃)</h3>';
|
|
1918
|
+
for(const p of plans){
|
|
1919
|
+
html+=`<div class="card" style="margin-bottom:12px;padding:12px 16px">
|
|
1920
|
+
<div class="flex justify-between items-center mb-8" style="flex-wrap:wrap;gap:8px">
|
|
1921
|
+
<div><strong>${escHtml(p.label)}</strong> <span class="tag">${p.type==='session'?'会话':'Agent'}</span></div>
|
|
1922
|
+
<div class="flex gap-4" style="font-size:12px">
|
|
1923
|
+
<span class="badge badge-yellow">${p.pending} 待执行</span>
|
|
1924
|
+
<span class="badge badge-blue">${p.running} 执行中</span>
|
|
1925
|
+
<span class="badge badge-green">${p.done} 已完成</span>
|
|
1926
|
+
</div>
|
|
1927
|
+
</div>
|
|
1928
|
+
<div style="margin-top:4px">`;
|
|
1929
|
+
for(var i=0;i<p.tasks.length;i++){
|
|
1930
|
+
var t=p.tasks[i];
|
|
1931
|
+
var st=t.status||'pending';
|
|
1932
|
+
var stBadge=st==='done'?'<span class="badge badge-green" style="font-size:10px">✓ done</span>':
|
|
1933
|
+
st==='running'?'<span class="badge badge-blue" style="font-size:10px">⟳ running</span>':
|
|
1934
|
+
'<span class="badge badge-yellow" style="font-size:10px">○ pending</span>';
|
|
1935
|
+
var checked=st==='done'?'checked':'';
|
|
1936
|
+
html+=`<div style="display:flex;align-items:center;gap:8px;padding:3px 0;font-size:13px;${st==='done'?'text-decoration:line-through;color:var(--text3)':''}">
|
|
1937
|
+
<input type="checkbox" ${checked} style="flex-shrink:0" onclick="toggleTaskPlanItem('${escHtml(p.key)}',${i},this.checked)">
|
|
1938
|
+
<span style="flex:1">${escHtml(t.text||'')}</span>${stBadge}
|
|
1939
|
+
</div>`;
|
|
1940
|
+
}
|
|
1941
|
+
html+=`</div></div>`;
|
|
1942
|
+
}
|
|
1943
|
+
}
|
|
1944
|
+
if(!plans.length){
|
|
1945
|
+
html+='<div class="empty" style="margin-bottom:16px">暂无活跃的实时任务计划</div>';
|
|
1946
|
+
}
|
|
1947
|
+
// 持久化任务列表
|
|
1948
|
+
if(persistTasks.length){
|
|
1949
|
+
html+=`<h3 style="margin-bottom:8px;margin-top:16px">💾 持久化任务 (${persistTasks.length} 条)</h3>`;
|
|
1950
|
+
html+='<div class="table-wrap"><table><tr><th>任务ID</th><th>描述</th><th>来源</th><th>状态</th><th>时间</th><th>操作</th></tr>';
|
|
1951
|
+
for(const t of persistTasks){
|
|
1952
|
+
var statusBadge=t.status==='completed'?'<span class="badge badge-green">已完成</span>':
|
|
1708
1953
|
t.status==='running'?'<span class="badge badge-blue">运行中</span>':
|
|
1709
1954
|
t.status==='failed'?'<span class="badge badge-red">失败</span>':
|
|
1710
1955
|
'<span class="badge badge-yellow">待处理</span>';
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
const canRetry=t.status==='failed'||t.status==='pending';
|
|
1714
|
-
const meta=t.metadata||{};
|
|
1715
|
-
const source=meta.source==='group_chat'?'群聊':meta.source||'';
|
|
1956
|
+
var meta=t.metadata||{};
|
|
1957
|
+
var source=meta.source==='group_chat'?'群聊':meta.source||'';
|
|
1716
1958
|
html+=`<tr data-status="${t.status}">
|
|
1717
1959
|
<td style="font-family:monospace;font-size:11px;max-width:120px;overflow:hidden;text-overflow:ellipsis" title="${escHtml(t.task_id)}">${escHtml(t.task_id)}</td>
|
|
1718
1960
|
<td style="max-width:300px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${escHtml(t.description)}">${escHtml((t.description||'').slice(0,100))}</td>
|
|
1719
1961
|
<td>${source}${meta.group_name?' / '+escHtml(meta.group_name):''}</td>
|
|
1720
|
-
<td>${statusBadge}
|
|
1962
|
+
<td>${statusBadge}</td>
|
|
1721
1963
|
<td style="font-size:12px;white-space:nowrap">${fmtTimeAgo(t.updated_at)}</td>
|
|
1722
1964
|
<td class="flex gap-8">
|
|
1723
|
-
${
|
|
1965
|
+
${t.status==='failed'||t.status==='pending'?`<button class="btn btn-sm btn-primary" onclick="retryTask('${escHtml(t.task_id)}')">重试</button>`:''}
|
|
1724
1966
|
<button class="btn btn-sm btn-danger" onclick="deleteTask('${escHtml(t.task_id)}')">删除</button>
|
|
1725
1967
|
</td></tr>`;
|
|
1726
1968
|
}
|
|
1727
|
-
html+='</table></div
|
|
1969
|
+
html+='</table></div>';
|
|
1970
|
+
}
|
|
1971
|
+
if(!plans.length&&!persistTasks.length){
|
|
1972
|
+
html+='<div class="empty">暂无任务记录</div>';
|
|
1728
1973
|
}
|
|
1729
1974
|
$('content').innerHTML=html;
|
|
1730
1975
|
}
|
|
1731
1976
|
|
|
1977
|
+
async function toggleTaskPlanItem(key,idx,checked){
|
|
1978
|
+
await api('/api/task-plan/all');
|
|
1979
|
+
// 直接调用 task-plan API 修改状态
|
|
1980
|
+
const r=await api('/api/task-plan?agent='+encodeURIComponent(key));
|
|
1981
|
+
if(!r||!r.tasks)return;
|
|
1982
|
+
var tasks=r.tasks;
|
|
1983
|
+
if(idx>=0&&idx<tasks.length){
|
|
1984
|
+
tasks[idx].status=checked?'done':'pending';
|
|
1985
|
+
await api('/api/task-plan',{method:'PUT',body:JSON.stringify({agent:key,session:r.session,tasks:tasks})});
|
|
1986
|
+
renderTasks();
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1989
|
+
|
|
1732
1990
|
function filterTasks(){
|
|
1733
1991
|
const s=$('taskStatusFilter')?.value||'';
|
|
1734
1992
|
document.querySelectorAll('#taskList tr').forEach(tr=>{
|
|
@@ -1752,7 +2010,8 @@ async function deleteTask(taskId){
|
|
|
1752
2010
|
});
|
|
1753
2011
|
}
|
|
1754
2012
|
|
|
1755
|
-
|
|
2013
|
+
let _deptTreeNeedsRefresh=false;
|
|
2014
|
+
function closeModal(){$('modalContainer').innerHTML='';if(_deptTreeNeedsRefresh){_deptTreeNeedsRefresh=false;renderDepartments()}}
|
|
1756
2015
|
|
|
1757
2016
|
// ========== Organization ==========
|
|
1758
2017
|
async function renderOrganization(){
|
|
@@ -1774,17 +2033,18 @@ async function renderOrganization(){
|
|
|
1774
2033
|
<div class="form-group"><label>网站</label><input id="orgWebsite" value="${escHtml(inf.website||'')}" placeholder="https://..."></div>
|
|
1775
2034
|
</div>
|
|
1776
2035
|
<div class="flex gap-8 mt-16"><button class="btn btn-primary" onclick="saveOrgInfo()">保存信息</button></div></div>`;
|
|
1777
|
-
// 知识库
|
|
1778
|
-
|
|
2036
|
+
// 知识库 — 未启用组织时隐藏
|
|
2037
|
+
const orgEnabled=cfg.enabled;
|
|
2038
|
+
html+=`<div class="card" id="orgKBCard"><div class="flex justify-between items-center mb-16">
|
|
1779
2039
|
<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"
|
|
2040
|
+
<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>
|
|
2041
|
+
<div id="orgKBList"><div class="empty">${orgEnabled?'加载中...':'组织管理未启用,请先启用后再使用知识库'}</div></div></div>`;
|
|
1782
2042
|
$('content').innerHTML=html;
|
|
1783
|
-
loadOrgKnowledge();
|
|
2043
|
+
if(orgEnabled)loadOrgKnowledge();
|
|
1784
2044
|
}
|
|
1785
2045
|
async function saveOrgConfig(){
|
|
1786
2046
|
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');
|
|
2047
|
+
if(r.error){showToast(r.error,'danger');return}showToast('已保存','success');renderOrganization();
|
|
1788
2048
|
}
|
|
1789
2049
|
async function saveOrgInfo(){
|
|
1790
2050
|
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 +2227,14 @@ async function addDeptAgent(path){
|
|
|
1967
2227
|
if(sel)sel.disabled=true;if(btn)btn.disabled=true;
|
|
1968
2228
|
try{
|
|
1969
2229
|
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);
|
|
2230
|
+
if(r.error||r.ok===false){showToast(r.error||r.message||'添加失败','danger');return}
|
|
2231
|
+
showToast('已添加','success');_deptTreeNeedsRefresh=true;showDeptDetail(path);
|
|
1972
2232
|
}finally{if(sel)sel.disabled=false;if(btn)btn.disabled=false}
|
|
1973
2233
|
}
|
|
1974
2234
|
async function removeDeptAgent(path,agentName){
|
|
1975
2235
|
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);
|
|
2236
|
+
if(r.error||r.ok===false){showToast(r.error||r.message||'移除失败','danger');return}
|
|
2237
|
+
showToast('已移除','success');_deptTreeNeedsRefresh=true;showDeptDetail(path);
|
|
1978
2238
|
}
|
|
1979
2239
|
async function saveDeptInfo(path){
|
|
1980
2240
|
// 同时保存名称、emoji 和元数据(描述、负责人)
|
|
@@ -2225,12 +2485,23 @@ function endCrop(){_cropState.dragging=false;document.removeEventListener('mouse
|
|
|
2225
2485
|
function cancelAvatarCrop(prefix){$(prefix+'CropArea').style.display='none';}
|
|
2226
2486
|
function confirmAvatarCrop(prefix,agentPath){
|
|
2227
2487
|
var img=$(prefix+'CropImg'),overlay=$(prefix+'CropOverlay');
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
var
|
|
2231
|
-
|
|
2488
|
+
if(!img||!overlay){showToast('裁剪数据异常','danger');return}
|
|
2489
|
+
// [v1.18.7] 防止 clientWidth=0 导致 NaN
|
|
2490
|
+
var clientW=Math.max(img.clientWidth,1),clientH=Math.max(img.clientHeight,1);
|
|
2491
|
+
var naturalW=img.naturalWidth||clientW,naturalH=img.naturalHeight||clientH;
|
|
2492
|
+
var scale=naturalW/clientW;
|
|
2493
|
+
if(!isFinite(scale)||scale<=0)scale=1;
|
|
2494
|
+
var cx=Math.round(parseFloat(overlay.style.left||'0')*scale);
|
|
2495
|
+
var cy=Math.round(parseFloat(overlay.style.top||'0')*scale);
|
|
2496
|
+
var cw=Math.round(parseFloat(overlay.style.width||'0')*scale);
|
|
2497
|
+
var ch=Math.round(parseFloat(overlay.style.height||'0')*scale);
|
|
2498
|
+
if(cw<10||ch<10||isNaN(cx)||isNaN(cy)){showToast('请拖动选择裁剪区域','danger');return}
|
|
2499
|
+
var src=$(prefix+'CropImg').src;
|
|
2500
|
+
if(!src||!src.startsWith('data:image')){showToast('请先上传图片','danger');return}
|
|
2501
|
+
var blob=dataURItoBlob(src);
|
|
2502
|
+
if(!blob||blob.size===0){showToast('图片数据异常','danger');return}
|
|
2232
2503
|
var formData=new FormData();
|
|
2233
|
-
formData.append('file'
|
|
2504
|
+
formData.append('file',blob,'avatar.png');
|
|
2234
2505
|
showToast('正在上传裁剪...','info');
|
|
2235
2506
|
fetch('/api/agents/'+encodeURIComponent(agentPath)+'/avatar?crop_x='+cx+'&crop_y='+cy+'&crop_w='+cw+'&crop_h='+ch+'&size=128',{
|
|
2236
2507
|
method:'POST',body:formData
|
|
@@ -2239,7 +2510,15 @@ function confirmAvatarCrop(prefix,agentPath){
|
|
|
2239
2510
|
else showToast(d.error||'上传失败','danger');
|
|
2240
2511
|
}).catch(e=>showToast('上传失败: '+e,'danger'));
|
|
2241
2512
|
}
|
|
2242
|
-
function dataURItoBlob(dataURI){
|
|
2513
|
+
function dataURItoBlob(dataURI){
|
|
2514
|
+
if(!dataURI||typeof dataURI!=='string')return null;
|
|
2515
|
+
var parts=dataURI.split(',');
|
|
2516
|
+
if(parts.length<2)return null;
|
|
2517
|
+
var mimeMatch=parts[0].match(/:(.*?);/);
|
|
2518
|
+
var mime=mimeMatch?mimeMatch[1]:'image/png';
|
|
2519
|
+
try{var b=atob(parts[1]),a=new Uint8Array(b.length);for(var i=0;i<b.length;i++)a[i]=b.charCodeAt(i);return new Blob([a],{type:mime});}
|
|
2520
|
+
catch(e){return null}
|
|
2521
|
+
}
|
|
2243
2522
|
async function removeAvatarImage(agentPath){
|
|
2244
2523
|
var ad=await api('/api/agents/'+encodeURIComponent(agentPath));if(!ad||!ad.avatar_image)return;
|
|
2245
2524
|
await api('/api/agents/'+encodeURIComponent(agentPath),{method:'PUT',body:JSON.stringify({avatar_image:''})});
|