myagent-ai 1.23.36 → 1.23.38
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 +10 -0
- package/executor/engine.py +25 -0
- package/groups/manager.py +90 -0
- package/package.json +1 -1
- package/web/api_server.py +120 -8
- package/web/ui/admin/admin-agentchat.js +175 -0
- package/web/ui/chat/flow_engine.js +10 -0
- package/web/ui/index.html +2 -0
package/agents/main_agent.py
CHANGED
|
@@ -215,6 +215,8 @@ class MainAgent(BaseAgent):
|
|
|
215
215
|
self._exec_event_counter: int = 0
|
|
216
216
|
# 活跃会话上下文追踪(用于消息注入)
|
|
217
217
|
self.active_contexts: Dict[str, AgentContext] = {}
|
|
218
|
+
# [v1.23.37] 会话取消标志(由 api_server.handle_chat_stop 设置)
|
|
219
|
+
self._cancelled_sessions: set = set()
|
|
218
220
|
|
|
219
221
|
def init_context_builder(self, memory_manager=None, skill_registry=None, knowledge_base_dir=None, context_window=None):
|
|
220
222
|
"""初始化 Context Builder(在系统启动后调用,注入依赖)"""
|
|
@@ -737,6 +739,14 @@ class MainAgent(BaseAgent):
|
|
|
737
739
|
logger.info(f"[{task_id}] V2 迭代 {self._iteration_count}/{max_iter}")
|
|
738
740
|
_emitted_reasoning_this_iter = False # [v1.15.8] 本轮是否已发送过 v2_reasoning 事件(防 TTS 重复)
|
|
739
741
|
|
|
742
|
+
# [v1.23.37] 检查是否收到停止信号
|
|
743
|
+
if context.session_id in self._cancelled_sessions:
|
|
744
|
+
logger.info(f"[{task_id}] 收到停止信号,终止执行循环")
|
|
745
|
+
context.working_memory["final_response"] = "⏹️ 任务已被用户停止"
|
|
746
|
+
await self._emit_v2_event("v2_stopped", {"reason": "用户手动停止"}, stream_callback)
|
|
747
|
+
self._cancelled_sessions.discard(context.session_id)
|
|
748
|
+
break
|
|
749
|
+
|
|
740
750
|
# ── 检查配置热加载广播 ──
|
|
741
751
|
if self.config_broadcaster:
|
|
742
752
|
reloaded, reload_type = await self.config_broadcaster.check_and_wait(task_id)
|
package/executor/engine.py
CHANGED
|
@@ -221,6 +221,8 @@ class ExecutionEngine:
|
|
|
221
221
|
self.work_dir = work_dir or os.getcwd()
|
|
222
222
|
self._permission_checker = permission_checker # callable(perm_name) -> bool
|
|
223
223
|
self._agent_name = agent_name
|
|
224
|
+
# [v1.23.37] 当前正在运行的子进程引用,用于 cancel_current() 终止
|
|
225
|
+
self._current_process: Optional[asyncio.subprocess.Process] = None
|
|
224
226
|
|
|
225
227
|
# 安全: 合并正则模式 + 字符串黑名单
|
|
226
228
|
self._blocked = set(self.DANGEROUS_COMMANDS)
|
|
@@ -606,6 +608,7 @@ class ExecutionEngine:
|
|
|
606
608
|
cwd=work_dir,
|
|
607
609
|
env=self._get_env(env),
|
|
608
610
|
)
|
|
611
|
+
self._current_process = process # [v1.23.37] 保存引用
|
|
609
612
|
|
|
610
613
|
try:
|
|
611
614
|
# 使用流式读取代替 communicate(),确保超时后能保留已产生的输出
|
|
@@ -747,6 +750,7 @@ class ExecutionEngine:
|
|
|
747
750
|
cwd=work_dir,
|
|
748
751
|
env=self._get_env(env),
|
|
749
752
|
)
|
|
753
|
+
self._current_process = process # [v1.23.37] 保存引用
|
|
750
754
|
|
|
751
755
|
# 使用流式读取代替 communicate(),确保超时后能保留已产生的输出
|
|
752
756
|
stdout_chunks = []
|
|
@@ -948,6 +952,7 @@ class ExecutionEngine:
|
|
|
948
952
|
cwd=work_dir,
|
|
949
953
|
env=self._get_env(env),
|
|
950
954
|
)
|
|
955
|
+
self._current_process = process # [v1.23.37] 保存引用
|
|
951
956
|
try:
|
|
952
957
|
stdout, stderr = await asyncio.wait_for(
|
|
953
958
|
process.communicate(), timeout=timeout
|
|
@@ -989,6 +994,26 @@ class ExecutionEngine:
|
|
|
989
994
|
metadata={"mode": "sandbox"},
|
|
990
995
|
)
|
|
991
996
|
|
|
997
|
+
# [v1.23.37] ── 取消当前执行 ──
|
|
998
|
+
|
|
999
|
+
def cancel_current(self):
|
|
1000
|
+
"""终止当前正在运行的子进程(由 /api/chat/stop 调用)"""
|
|
1001
|
+
proc = self._current_process
|
|
1002
|
+
if proc is None:
|
|
1003
|
+
return False
|
|
1004
|
+
try:
|
|
1005
|
+
if proc.returncode is None: # 进程仍在运行
|
|
1006
|
+
proc.kill()
|
|
1007
|
+
logger.info(f"[executor] 已终止子进程 PID={proc.pid}")
|
|
1008
|
+
self._current_process = None
|
|
1009
|
+
return True
|
|
1010
|
+
except ProcessLookupError:
|
|
1011
|
+
pass # 进程已结束
|
|
1012
|
+
except Exception as e:
|
|
1013
|
+
logger.debug(f"[executor] 终止子进程失败: {e}")
|
|
1014
|
+
self._current_process = None
|
|
1015
|
+
return False
|
|
1016
|
+
|
|
992
1017
|
def set_execution_mode(self, mode: str) -> bool:
|
|
993
1018
|
"""切换执行模式。返回是否切换成功。"""
|
|
994
1019
|
if mode not in ("local", "sandbox"):
|
package/groups/manager.py
CHANGED
|
@@ -275,6 +275,27 @@ class GroupManager:
|
|
|
275
275
|
CREATE INDEX IF NOT EXISTS idx_group_messages_group_id
|
|
276
276
|
ON group_messages(group_id, timestamp)
|
|
277
277
|
""")
|
|
278
|
+
# [v1.23.37] Agent间私聊记录表(独立于群聊消息,便于查询和管理)
|
|
279
|
+
self._db_conn.execute("""
|
|
280
|
+
CREATE TABLE IF NOT EXISTS agent_chat (
|
|
281
|
+
id TEXT PRIMARY KEY,
|
|
282
|
+
group_id TEXT NOT NULL DEFAULT '',
|
|
283
|
+
from_agent TEXT NOT NULL,
|
|
284
|
+
from_name TEXT DEFAULT '',
|
|
285
|
+
to_agent TEXT NOT NULL,
|
|
286
|
+
to_name TEXT DEFAULT '',
|
|
287
|
+
content TEXT NOT NULL,
|
|
288
|
+
timestamp REAL NOT NULL
|
|
289
|
+
)
|
|
290
|
+
""")
|
|
291
|
+
self._db_conn.execute("""
|
|
292
|
+
CREATE INDEX IF NOT EXISTS idx_agent_chat_group
|
|
293
|
+
ON agent_chat(group_id, timestamp)
|
|
294
|
+
""")
|
|
295
|
+
self._db_conn.execute("""
|
|
296
|
+
CREATE INDEX IF NOT EXISTS idx_agent_chat_pair
|
|
297
|
+
ON agent_chat(from_agent, to_agent, timestamp)
|
|
298
|
+
""")
|
|
278
299
|
self._db_conn.commit()
|
|
279
300
|
|
|
280
301
|
def close(self):
|
|
@@ -697,6 +718,75 @@ class GroupManager:
|
|
|
697
718
|
self._db_conn.commit()
|
|
698
719
|
return True
|
|
699
720
|
|
|
721
|
+
# ==================================================================
|
|
722
|
+
# [v1.23.37] Agent间私聊记录
|
|
723
|
+
# ==================================================================
|
|
724
|
+
|
|
725
|
+
def add_agent_chat(self, group_id: str, from_agent: str, from_name: str,
|
|
726
|
+
to_agent: str, to_name: str, content: str) -> str:
|
|
727
|
+
"""添加一条Agent间私聊记录,返回消息ID"""
|
|
728
|
+
if not self._db_conn:
|
|
729
|
+
return ""
|
|
730
|
+
msg_id = uuid.uuid4().hex[:16]
|
|
731
|
+
self._db_conn.execute(
|
|
732
|
+
"INSERT INTO agent_chat (id, group_id, from_agent, from_name, to_agent, to_name, content, timestamp) "
|
|
733
|
+
"VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
|
734
|
+
(msg_id, group_id, from_agent, from_name, to_agent, to_name, content, time.time()),
|
|
735
|
+
)
|
|
736
|
+
self._db_conn.commit()
|
|
737
|
+
return msg_id
|
|
738
|
+
|
|
739
|
+
def get_agent_chats(self, group_id: str = "", from_agent: str = "",
|
|
740
|
+
to_agent: str = "", limit: int = 200) -> list:
|
|
741
|
+
"""查询Agent间私聊记录,支持按群、发送方、接收方筛选"""
|
|
742
|
+
if not self._db_conn:
|
|
743
|
+
return []
|
|
744
|
+
conditions = []
|
|
745
|
+
params = []
|
|
746
|
+
if group_id:
|
|
747
|
+
conditions.append("group_id = ?")
|
|
748
|
+
params.append(group_id)
|
|
749
|
+
if from_agent:
|
|
750
|
+
conditions.append("(from_agent = ? OR to_agent = ?)")
|
|
751
|
+
params.extend([from_agent, from_agent])
|
|
752
|
+
if to_agent:
|
|
753
|
+
conditions.append("(from_agent = ? OR to_agent = ?)")
|
|
754
|
+
params.extend([to_agent, to_agent])
|
|
755
|
+
where = (" WHERE " + " AND ".join(conditions)) if conditions else ""
|
|
756
|
+
rows = self._db_conn.execute(
|
|
757
|
+
f"SELECT * FROM agent_chat{where} ORDER BY timestamp DESC LIMIT ?",
|
|
758
|
+
params + [limit],
|
|
759
|
+
).fetchall()
|
|
760
|
+
return [dict(r) for r in rows]
|
|
761
|
+
|
|
762
|
+
def get_agent_chat_pairs(self, group_id: str = "") -> list:
|
|
763
|
+
"""获取所有有私聊记录的Agent对(去重),返回 [{from_agent, from_name, to_agent, to_name, count, last_time}]"""
|
|
764
|
+
if not self._db_conn:
|
|
765
|
+
return []
|
|
766
|
+
where = " WHERE group_id = ?" if group_id else ""
|
|
767
|
+
params = [group_id] if group_id else []
|
|
768
|
+
rows = self._db_conn.execute(
|
|
769
|
+
f"""SELECT from_agent, from_name, to_agent, to_name,
|
|
770
|
+
COUNT(*) as cnt, MAX(timestamp) as last_ts
|
|
771
|
+
FROM agent_chat{where}
|
|
772
|
+
GROUP BY CASE WHEN from_agent < to_agent THEN from_agent ELSE to_agent END,
|
|
773
|
+
CASE WHEN from_agent < to_agent THEN to_agent ELSE from_agent END
|
|
774
|
+
ORDER BY last_ts DESC""",
|
|
775
|
+
params,
|
|
776
|
+
).fetchall()
|
|
777
|
+
return [dict(r) for r in rows]
|
|
778
|
+
|
|
779
|
+
def clear_agent_chats(self, group_id: str = "") -> int:
|
|
780
|
+
"""清空私聊记录,返回删除数量"""
|
|
781
|
+
if not self._db_conn:
|
|
782
|
+
return 0
|
|
783
|
+
if group_id:
|
|
784
|
+
cur = self._db_conn.execute("DELETE FROM agent_chat WHERE group_id = ?", (group_id,))
|
|
785
|
+
else:
|
|
786
|
+
cur = self._db_conn.execute("DELETE FROM agent_chat")
|
|
787
|
+
self._db_conn.commit()
|
|
788
|
+
return cur.rowcount
|
|
789
|
+
|
|
700
790
|
# ==================================================================
|
|
701
791
|
# 群消息统计
|
|
702
792
|
# ==================================================================
|
package/package.json
CHANGED
package/web/api_server.py
CHANGED
|
@@ -406,6 +406,8 @@ class ApiServer:
|
|
|
406
406
|
r.add_post("/api/chat", self.handle_chat)
|
|
407
407
|
r.add_post("/api/chat/stream", self.handle_chat_stream)
|
|
408
408
|
r.add_post("/api/chat/inject", self.handle_chat_inject)
|
|
409
|
+
# [v1.23.37] 停止执行
|
|
410
|
+
r.add_post("/api/chat/stop", self.handle_chat_stop)
|
|
409
411
|
r.add_post("/api/voice-optimize", self.handle_voice_optimize)
|
|
410
412
|
r.add_post("/api/voice-stt", self.handle_voice_stt)
|
|
411
413
|
r.add_get("/chat", self.handle_chat_page)
|
|
@@ -464,6 +466,10 @@ class ApiServer:
|
|
|
464
466
|
r.add_get("/api/groups/{gid}/messages", self.handle_get_group_messages)
|
|
465
467
|
r.add_post("/api/groups/{gid}/messages", self.handle_send_group_message)
|
|
466
468
|
r.add_delete("/api/groups/{gid}/messages", self.handle_clear_group_messages)
|
|
469
|
+
# [v1.23.37] Agent间私聊记录查询
|
|
470
|
+
r.add_get("/api/agent-chat/pairs", self.handle_get_agent_chat_pairs)
|
|
471
|
+
r.add_get("/api/agent-chat/messages", self.handle_get_agent_chat_messages)
|
|
472
|
+
r.add_delete("/api/agent-chat/messages", self.handle_clear_agent_chat_messages)
|
|
467
473
|
# ── 部门管理 ──
|
|
468
474
|
r.add_get("/api/departments", self.handle_dept_tree)
|
|
469
475
|
r.add_post("/api/departments", self.handle_create_dept)
|
|
@@ -1907,6 +1913,10 @@ window.addEventListener('beforeunload', function() {{
|
|
|
1907
1913
|
# {session_id: {running: bool, started_at: float, result: str, done: bool, error: str}}
|
|
1908
1914
|
_running_sessions: Dict[str, Dict] = {}
|
|
1909
1915
|
_running_sessions_ttl = 300 # 过期时间(秒),用于自动清理已完成会话
|
|
1916
|
+
# [v1.23.37] 存储后台 asyncio.Task 引用,用于取消执行
|
|
1917
|
+
_running_tasks: Dict[str, asyncio.Task] = {}
|
|
1918
|
+
# [v1.23.37] 会话取消标志,供 agent 执行循环检查
|
|
1919
|
+
_session_cancelled: Dict[str, bool] = {}
|
|
1910
1920
|
|
|
1911
1921
|
async def _get_session_lock(self, session_id: str) -> asyncio.Lock:
|
|
1912
1922
|
"""获取或创建会话级锁(每个 session_id 一个锁,防并发覆盖 MainAgent 共享状态)"""
|
|
@@ -2206,10 +2216,15 @@ window.addEventListener('beforeunload', function() {{
|
|
|
2206
2216
|
session_lock.release()
|
|
2207
2217
|
except RuntimeError:
|
|
2208
2218
|
pass
|
|
2219
|
+
# [v1.23.37] 清理任务引用和取消标志
|
|
2220
|
+
self._running_tasks.pop(session_id, None)
|
|
2221
|
+
self._session_cancelled.pop(session_id, None)
|
|
2209
2222
|
|
|
2210
2223
|
# ── 启动后台任务(不等它完成,前端断开也不影响) ──
|
|
2211
2224
|
logger.info(f"[{session_id}] 准备创建后台任务")
|
|
2212
2225
|
bg_task = asyncio.create_task(_run_stream_task())
|
|
2226
|
+
self._running_tasks[session_id] = bg_task # [v1.23.37] 保存引用用于取消
|
|
2227
|
+
self._session_cancelled[session_id] = False # [v1.23.37] 初始化取消标志
|
|
2213
2228
|
logger.info(f"[{session_id}] 后台任务已创建: {bg_task}")
|
|
2214
2229
|
|
|
2215
2230
|
# ── SSE 事件循环:实时转发后台任务的事件到客户端 ──
|
|
@@ -2236,6 +2251,56 @@ window.addEventListener('beforeunload', function() {{
|
|
|
2236
2251
|
|
|
2237
2252
|
return response
|
|
2238
2253
|
|
|
2254
|
+
# [v1.23.37] ── 停止执行 ──
|
|
2255
|
+
|
|
2256
|
+
async def handle_chat_stop(self, request):
|
|
2257
|
+
"""POST /api/chat/stop - 停止正在执行的任务
|
|
2258
|
+
|
|
2259
|
+
停止链:前端 abort → 调用此 API → 设置取消标志 + cancel task
|
|
2260
|
+
→ agent 循环检测到取消标志 → 退出循环 → 任务结束
|
|
2261
|
+
"""
|
|
2262
|
+
try:
|
|
2263
|
+
data = await request.json()
|
|
2264
|
+
except Exception:
|
|
2265
|
+
return web.json_response({"error": "invalid JSON"}, status=400)
|
|
2266
|
+
|
|
2267
|
+
session_id = data.get("session_id", "")
|
|
2268
|
+
if not session_id:
|
|
2269
|
+
return web.json_response({"ok": False, "error": "missing session_id"})
|
|
2270
|
+
|
|
2271
|
+
logger.info(f"[{session_id}] 收到停止执行请求")
|
|
2272
|
+
|
|
2273
|
+
# 1. 设置取消标志(agent 循环会检查)
|
|
2274
|
+
self._session_cancelled[session_id] = True
|
|
2275
|
+
|
|
2276
|
+
# 1.5 设置 MainAgent 上的取消标志(agent 循环会检查 _cancelled_sessions)
|
|
2277
|
+
if self.core.main_agent:
|
|
2278
|
+
self.core.main_agent._cancelled_sessions.add(session_id)
|
|
2279
|
+
|
|
2280
|
+
# 2. 尝试取消 asyncio.Task
|
|
2281
|
+
task = self._running_tasks.get(session_id)
|
|
2282
|
+
cancelled = False
|
|
2283
|
+
if task and not task.done():
|
|
2284
|
+
task.cancel()
|
|
2285
|
+
cancelled = True
|
|
2286
|
+
logger.info(f"[{session_id}] 已发送 task.cancel()")
|
|
2287
|
+
|
|
2288
|
+
# 3. 更新会话状态
|
|
2289
|
+
sess = self._running_sessions.get(session_id)
|
|
2290
|
+
if sess and sess.get("running"):
|
|
2291
|
+
sess["running"] = False
|
|
2292
|
+
sess["done"] = True
|
|
2293
|
+
sess["cancelled"] = True
|
|
2294
|
+
|
|
2295
|
+
# 4. 尝试终止正在运行的子进程(如果 executor 正在执行命令)
|
|
2296
|
+
try:
|
|
2297
|
+
if hasattr(self.core, 'executor') and self.core.executor:
|
|
2298
|
+
self.core.executor.cancel_current()
|
|
2299
|
+
except Exception as e:
|
|
2300
|
+
logger.debug(f"[{session_id}] executor 取消失败: {e}")
|
|
2301
|
+
|
|
2302
|
+
return web.json_response({"ok": True, "cancelled": cancelled})
|
|
2303
|
+
|
|
2239
2304
|
async def handle_chat_inject(self, request):
|
|
2240
2305
|
"""POST /api/chat/inject - 注入消息到正在执行的任务,或进入排队序列"""
|
|
2241
2306
|
try:
|
|
@@ -7779,11 +7844,15 @@ window.addEventListener('beforeunload', function() {{
|
|
|
7779
7844
|
- @某个Agent: 只有被@的Agent需要回复
|
|
7780
7845
|
- @所有人 / @all: 所有成员都需要回复
|
|
7781
7846
|
- 不@任何人(广播): 你自行判断是否需要回复
|
|
7782
|
-
2. **跨Agent
|
|
7783
|
-
|
|
7784
|
-
-
|
|
7785
|
-
|
|
7786
|
-
-
|
|
7847
|
+
2. **跨Agent私下沟通**: 你可以使用 `myagent-ai chat --agent <路径> --message "消息"` 命令向群内其他Agent发送私下消息。对方会在自己下次处理消息时收到。
|
|
7848
|
+
- 私下沟通的内容不会直接显示给用户,适合讨论细节、交换数据、协调方案
|
|
7849
|
+
- 当任务需要多个Agent协作时,应该先在群里讨论分工,然后私下沟通具体细节
|
|
7850
|
+
3. **协作分工模式**(复杂任务):
|
|
7851
|
+
- 部长/管理员应主动分析任务,在群里提出分工方案(谁负责什么)
|
|
7852
|
+
- 其他成员应积极响应,认领自己擅长的部分
|
|
7853
|
+
- 分工确定后,各自私下沟通需要协调的细节
|
|
7854
|
+
- 部长/管理员负责在群里汇总进展,向用户汇报阶段性成果和最终结果
|
|
7855
|
+
- 私下沟通的详细过程不需要在群里展示,只汇报关键进展和最终结论
|
|
7787
7856
|
{at_info}
|
|
7788
7857
|
### 近期群聊记录(最近10条)
|
|
7789
7858
|
{chat_history if chat_history else '(暂无历史消息)'}
|
|
@@ -7792,7 +7861,7 @@ window.addEventListener('beforeunload', function() {{
|
|
|
7792
7861
|
- 你只代表你自己发言,使用第一人称
|
|
7793
7862
|
- 不要假装是其他Agent或代替其他Agent回答
|
|
7794
7863
|
- 如果问题超出你的能力范围,建议用户@相关专家Agent
|
|
7795
|
-
- 如果需要其他Agent的信息,使用 `myagent-ai chat`
|
|
7864
|
+
- 如果需要其他Agent的信息,使用 `myagent-ai chat` 命令私下沟通
|
|
7796
7865
|
|
|
7797
7866
|
### 能力提醒(关键)
|
|
7798
7867
|
- 你拥有完整的工具调用能力(搜索、文件操作、代码执行、图片生成等)
|
|
@@ -7872,6 +7941,7 @@ window.addEventListener('beforeunload', function() {{
|
|
|
7872
7941
|
)
|
|
7873
7942
|
|
|
7874
7943
|
# [v1.23.29] 从 Agent 响应中提取 __CHAT_AGENT__ 标记,清理并保存跨Agent通信消息
|
|
7944
|
+
# [v1.23.37] 同时写入独立的 agent_chat 私聊记录表,便于管理后台查看
|
|
7875
7945
|
_chat_msg_pattern = _re.compile(r'__CHAT_AGENT__(.+?)\|(.+?)\|(.+?)__END__')
|
|
7876
7946
|
for resp in final_responses:
|
|
7877
7947
|
resp_text = resp.get("response", "")
|
|
@@ -7879,17 +7949,31 @@ window.addEventListener('beforeunload', function() {{
|
|
|
7879
7949
|
if chat_matches:
|
|
7880
7950
|
# 清理标记文本
|
|
7881
7951
|
resp["response"] = _chat_msg_pattern.sub('', resp_text).strip()
|
|
7882
|
-
# 保存跨Agent
|
|
7952
|
+
# 保存跨Agent通信消息
|
|
7883
7953
|
for c_path, c_name, c_msg in chat_matches:
|
|
7954
|
+
c_path_s, c_name_s, c_msg_s = c_path.strip(), c_name.strip(), c_msg.strip()
|
|
7955
|
+
# 群聊中显示简要提示
|
|
7884
7956
|
chat_sys_msg = GroupMessage(
|
|
7885
7957
|
group_id=gid,
|
|
7886
7958
|
sender=resp.get("agent_path", ""),
|
|
7887
7959
|
sender_name=resp.get("name", ""),
|
|
7888
7960
|
sender_avatar=resp.get("avatar", "🤖"),
|
|
7889
|
-
content=f"💬
|
|
7961
|
+
content=f"💬 私下与 {c_name_s} 沟通中...",
|
|
7890
7962
|
msg_type="text",
|
|
7891
7963
|
)
|
|
7892
7964
|
mgr.add_message(chat_sys_msg)
|
|
7965
|
+
# [v1.23.37] 写入独立的私聊记录表
|
|
7966
|
+
try:
|
|
7967
|
+
mgr.add_agent_chat(
|
|
7968
|
+
group_id=gid,
|
|
7969
|
+
from_agent=resp.get("agent_path", ""),
|
|
7970
|
+
from_name=resp.get("name", ""),
|
|
7971
|
+
to_agent=c_path_s,
|
|
7972
|
+
to_name=c_name_s,
|
|
7973
|
+
content=c_msg_s,
|
|
7974
|
+
)
|
|
7975
|
+
except Exception as ce:
|
|
7976
|
+
logger.debug(f"保存私聊记录失败: {ce}")
|
|
7893
7977
|
|
|
7894
7978
|
# Save agent messages sequentially in sorted order
|
|
7895
7979
|
for resp in final_responses:
|
|
@@ -7926,6 +8010,34 @@ window.addEventListener('beforeunload', function() {{
|
|
|
7926
8010
|
ok = mgr.clear_messages(gid)
|
|
7927
8011
|
return web.json_response({"ok": ok})
|
|
7928
8012
|
|
|
8013
|
+
# ── [v1.23.37] Agent间私聊记录 API ──
|
|
8014
|
+
|
|
8015
|
+
async def handle_get_agent_chat_pairs(self, request):
|
|
8016
|
+
"""GET /api/agent-chat/pairs - 获取所有私聊Agent对"""
|
|
8017
|
+
gid = request.query.get("group_id", "")
|
|
8018
|
+
mgr = self._get_group_manager()
|
|
8019
|
+
pairs = mgr.get_agent_chat_pairs(group_id=gid)
|
|
8020
|
+
return web.json_response(pairs)
|
|
8021
|
+
|
|
8022
|
+
async def handle_get_agent_chat_messages(self, request):
|
|
8023
|
+
"""GET /api/agent-chat/messages - 查询私聊记录详情"""
|
|
8024
|
+
gid = request.query.get("group_id", "")
|
|
8025
|
+
from_agent = request.query.get("from_agent", "")
|
|
8026
|
+
to_agent = request.query.get("to_agent", "")
|
|
8027
|
+
limit = min(int(request.query.get("limit", "200")), 500)
|
|
8028
|
+
mgr = self._get_group_manager()
|
|
8029
|
+
messages = mgr.get_agent_chats(
|
|
8030
|
+
group_id=gid, from_agent=from_agent, to_agent=to_agent, limit=limit
|
|
8031
|
+
)
|
|
8032
|
+
return web.json_response(messages)
|
|
8033
|
+
|
|
8034
|
+
async def handle_clear_agent_chat_messages(self, request):
|
|
8035
|
+
"""DELETE /api/agent-chat/messages - 清空私聊记录"""
|
|
8036
|
+
gid = request.query.get("group_id", "")
|
|
8037
|
+
mgr = self._get_group_manager()
|
|
8038
|
+
deleted = mgr.clear_agent_chats(group_id=gid)
|
|
8039
|
+
return web.json_response({"ok": True, "deleted": deleted})
|
|
8040
|
+
|
|
7929
8041
|
async def start(self, port: int = 8767, host: str = "127.0.0.1"):
|
|
7930
8042
|
# 加载禁用技能列表
|
|
7931
8043
|
self._load_disabled_skills()
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* [v1.23.37] Agent间私聊记录查看模块
|
|
3
|
+
* 路径: /api/agent-chat/pairs, /api/agent-chat/messages
|
|
4
|
+
*/
|
|
5
|
+
async function renderAgentChat() {
|
|
6
|
+
const content = $('content');
|
|
7
|
+
if (!content) return;
|
|
8
|
+
|
|
9
|
+
let currentFilter = { group_id: '', from_agent: '', to_agent: '' };
|
|
10
|
+
|
|
11
|
+
function esc(s) { return (s || '').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); }
|
|
12
|
+
|
|
13
|
+
async function loadGroups() {
|
|
14
|
+
try {
|
|
15
|
+
const data = await api('/api/groups');
|
|
16
|
+
return Array.isArray(data) ? data : (data.groups || data.data || []);
|
|
17
|
+
} catch { return []; }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function loadPairs() {
|
|
21
|
+
const q = new URLSearchParams();
|
|
22
|
+
if (currentFilter.group_id) q.set('group_id', currentFilter.group_id);
|
|
23
|
+
const data = await api('/api/agent-chat/pairs?' + q.toString());
|
|
24
|
+
return Array.isArray(data) ? data : [];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function loadMessages(fromAgent, toAgent) {
|
|
28
|
+
const q = new URLSearchParams();
|
|
29
|
+
if (currentFilter.group_id) q.set('group_id', currentFilter.group_id);
|
|
30
|
+
if (fromAgent) q.set('from_agent', fromAgent);
|
|
31
|
+
if (toAgent) q.set('to_agent', toAgent);
|
|
32
|
+
q.set('limit', '500');
|
|
33
|
+
const data = await api('/api/agent-chat/messages?' + q.toString());
|
|
34
|
+
return Array.isArray(data) ? data : [];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function formatTime(ts) {
|
|
38
|
+
if (!ts) return '';
|
|
39
|
+
const d = new Date(ts * 1000);
|
|
40
|
+
const pad = n => String(n).padStart(2, '0');
|
|
41
|
+
return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function truncate(s, len) {
|
|
45
|
+
if (!s) return '';
|
|
46
|
+
return s.length > len ? s.slice(0, len) + '...' : s;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ── 主界面 ──
|
|
50
|
+
content.innerHTML = `
|
|
51
|
+
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:20px">
|
|
52
|
+
<div>
|
|
53
|
+
<h2 style="font-size:20px;font-weight:600;color:var(--text)">Agent 私聊记录</h2>
|
|
54
|
+
<p style="font-size:13px;color:var(--text3);margin-top:4px">查看群聊中 Agent 之间的私下沟通记录</p>
|
|
55
|
+
</div>
|
|
56
|
+
<div style="display:flex;gap:8px">
|
|
57
|
+
<button id="acRefreshBtn" onclick="window._acRefresh()" style="padding:6px 14px;background:var(--bg3);color:var(--text);border:1px solid var(--border);border-radius:var(--radius);cursor:pointer;font-size:13px">
|
|
58
|
+
刷新
|
|
59
|
+
</button>
|
|
60
|
+
<button id="acClearBtn" onclick="window._acClearAll()" style="padding:6px 14px;background:var(--danger);color:#fff;border:none;border-radius:var(--radius);cursor:pointer;font-size:13px">
|
|
61
|
+
清空全部
|
|
62
|
+
</button>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
<div style="display:flex;gap:12px;margin-bottom:16px;flex-wrap:wrap">
|
|
66
|
+
<select id="acGroupFilter" onchange="window._acFilterChange()" style="padding:6px 10px;background:var(--bg3);color:var(--text);border:1px solid var(--border);border-radius:var(--radius);font-size:13px;min-width:160px">
|
|
67
|
+
<option value="">全部群聊</option>
|
|
68
|
+
</select>
|
|
69
|
+
</div>
|
|
70
|
+
<div style="display:grid;grid-template-columns:340px 1fr;gap:16px;min-height:500px" id="acLayout">
|
|
71
|
+
<div id="acPairsPanel" style="background:var(--bg2);border:1px solid var(--border);border-radius:var(--radius);overflow:hidden;display:flex;flex-direction:column">
|
|
72
|
+
<div style="padding:12px 16px;border-bottom:1px solid var(--border);font-weight:600;font-size:14px;color:var(--text2)">聊天对象</div>
|
|
73
|
+
<div id="acPairsList" style="flex:1;overflow-y:auto;padding:8px">
|
|
74
|
+
<div style="text-align:center;color:var(--text3);padding:40px;font-size:13px">加载中...</div>
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
<div id="acChatPanel" style="background:var(--bg2);border:1px solid var(--border);border-radius:var(--radius);overflow:hidden;display:flex;flex-direction:column">
|
|
78
|
+
<div id="acChatHeader" style="padding:12px 16px;border-bottom:1px solid var(--border);font-weight:600;font-size:14px;color:var(--text2)">选择左侧聊天对象查看详情</div>
|
|
79
|
+
<div id="acChatMessages" style="flex:1;overflow-y:auto;padding:16px">
|
|
80
|
+
<div style="text-align:center;color:var(--text3);padding:60px;font-size:13px">暂无聊天记录</div>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
`;
|
|
85
|
+
|
|
86
|
+
// ── 加载群列表 ──
|
|
87
|
+
const groups = await loadGroups();
|
|
88
|
+
const sel = $('acGroupFilter');
|
|
89
|
+
groups.forEach(g => {
|
|
90
|
+
const opt = document.createElement('option');
|
|
91
|
+
opt.value = g.id || g.group_id || '';
|
|
92
|
+
opt.textContent = g.name || g.group_name || g.id || '未命名';
|
|
93
|
+
sel.appendChild(opt);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// ── 渲染聊天对列表 ──
|
|
97
|
+
async function renderPairs() {
|
|
98
|
+
const pairs = await loadPairs();
|
|
99
|
+
const list = $('acPairsList');
|
|
100
|
+
if (!pairs.length) {
|
|
101
|
+
list.innerHTML = '<div style="text-align:center;color:var(--text3);padding:40px;font-size:13px">暂无私聊记录</div>';
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
list.innerHTML = pairs.map(p => {
|
|
105
|
+
const a = esc(p.from_name || p.from_agent);
|
|
106
|
+
const b = esc(p.to_name || p.to_agent);
|
|
107
|
+
const last = formatTime(p.last_ts);
|
|
108
|
+
const count = p.cnt || p.count || 0;
|
|
109
|
+
return `<div onclick="window._acSelectPair('${esc(p.from_agent)}','${esc(p.to_agent)}','${a}','${b}')"
|
|
110
|
+
style="padding:10px 12px;border-radius:6px;cursor:pointer;margin-bottom:4px;transition:background .15s"
|
|
111
|
+
onmouseover="this.style.background='var(--bg3)'" onmouseout="this.style.background='transparent'">
|
|
112
|
+
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:4px">
|
|
113
|
+
<span style="font-size:13px;font-weight:600;color:var(--text)">${a} ↔ ${b}</span>
|
|
114
|
+
<span style="font-size:11px;color:var(--text3)">${count}条</span>
|
|
115
|
+
</div>
|
|
116
|
+
<div style="font-size:11px;color:var(--text3)">最后沟通: ${last}</div>
|
|
117
|
+
</div>`;
|
|
118
|
+
}).join('');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ── 渲染聊天记录 ──
|
|
122
|
+
async function renderMessages(fromAgent, toAgent, fromName, toName) {
|
|
123
|
+
const header = $('acChatHeader');
|
|
124
|
+
header.innerHTML = `<span style="color:var(--accent)">${esc(fromName)}</span> ↔ <span style="color:var(--accent)">${esc(toName)}</span> 的私聊记录`;
|
|
125
|
+
|
|
126
|
+
const msgs = await loadMessages(fromAgent, toAgent);
|
|
127
|
+
const container = $('acChatMessages');
|
|
128
|
+
if (!msgs.length) {
|
|
129
|
+
container.innerHTML = '<div style="text-align:center;color:var(--text3);padding:60px;font-size:13px">暂无记录</div>';
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
// 按 time 正序显示
|
|
133
|
+
const sorted = [...msgs].sort((a, b) => (a.timestamp || 0) - (b.timestamp || 0));
|
|
134
|
+
container.innerHTML = sorted.map(m => {
|
|
135
|
+
const isFrom = m.from_agent === fromAgent;
|
|
136
|
+
const sender = isFrom ? esc(m.from_name || m.from_agent) : esc(m.to_name || m.to_agent);
|
|
137
|
+
const bubbleBg = isFrom ? 'var(--accent-light)' : 'var(--bg3)';
|
|
138
|
+
const align = isFrom ? 'flex-end' : 'flex-start';
|
|
139
|
+
const labelColor = isFrom ? 'var(--accent)' : 'var(--text3)';
|
|
140
|
+
const content = esc(m.content || '').replace(/\n/g, '<br>');
|
|
141
|
+
const time = formatTime(m.timestamp);
|
|
142
|
+
return `<div style="display:flex;flex-direction:column;align-items:${align};margin-bottom:12px">
|
|
143
|
+
<div style="font-size:11px;color:${labelColor};margin-bottom:3px;padding:0 4px">${sender} · ${time}</div>
|
|
144
|
+
<div style="max-width:75%;padding:10px 14px;background:${bubbleBg};border-radius:12px;font-size:13px;line-height:1.6;color:var(--text);word-break:break-word">${content}</div>
|
|
145
|
+
</div>`;
|
|
146
|
+
}).join('');
|
|
147
|
+
container.scrollTop = container.scrollHeight;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ── 全局方法 ──
|
|
151
|
+
window._acRefresh = async () => { await renderPairs(); };
|
|
152
|
+
window._acFilterChange = () => {
|
|
153
|
+
currentFilter.group_id = $('acGroupFilter').value;
|
|
154
|
+
renderPairs();
|
|
155
|
+
};
|
|
156
|
+
window._acSelectPair = (fa, ta, fn, tn) => { renderMessages(fa, ta, fn, tn); };
|
|
157
|
+
window._acClearAll = async () => {
|
|
158
|
+
if (!confirm('确定要清空所有私聊记录吗?此操作不可恢复。')) return;
|
|
159
|
+
const q = new URLSearchParams();
|
|
160
|
+
if (currentFilter.group_id) q.set('group_id', currentFilter.group_id);
|
|
161
|
+
try {
|
|
162
|
+
const data = await api('/api/agent-chat/messages?' + q.toString(), { method: 'DELETE' });
|
|
163
|
+
toast('已清空 ' + (data.deleted || 0) + ' 条记录');
|
|
164
|
+
await renderPairs();
|
|
165
|
+
$('acChatMessages').innerHTML = '<div style="text-align:center;color:var(--text3);padding:60px;font-size:13px">暂无聊天记录</div>';
|
|
166
|
+
$('acChatHeader').textContent = '选择左侧聊天对象查看详情';
|
|
167
|
+
} catch (e) { toast('清空失败: ' + (e.message || e), 'error'); }
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
// 初始加载
|
|
171
|
+
await renderPairs();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (typeof window._adminRenderers === 'undefined') window._adminRenderers = {};
|
|
175
|
+
window._adminRenderers['agentchat'] = renderAgentChat;
|
|
@@ -2392,6 +2392,16 @@ async function sendMessage(opts) {
|
|
|
2392
2392
|
|
|
2393
2393
|
// ── Stop Generation ──
|
|
2394
2394
|
function stopGenerating() {
|
|
2395
|
+
// [v1.23.37] 告知后端停止执行(kill subprocess + 设置取消标志)
|
|
2396
|
+
var sid = state.activeSessionId || '';
|
|
2397
|
+
if (sid) {
|
|
2398
|
+
fetch('/api/chat/stop', {
|
|
2399
|
+
method: 'POST',
|
|
2400
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2401
|
+
body: JSON.stringify({ session_id: sid }),
|
|
2402
|
+
}).catch(function() {});
|
|
2403
|
+
}
|
|
2404
|
+
// 中断前端 SSE 连接
|
|
2395
2405
|
if (state.abortController) {
|
|
2396
2406
|
state.abortController.abort();
|
|
2397
2407
|
}
|
package/web/ui/index.html
CHANGED
|
@@ -327,6 +327,7 @@ tr:hover{background:var(--surface2)}
|
|
|
327
327
|
<div class="nav-item" data-tooltip="聊天平台" onclick="showPage('platforms')"><span class="icon">🌐</span><span class="icon-text">聊天平台</span></div>
|
|
328
328
|
<div class="nav-item" data-tooltip="组织管理" onclick="showPage('organization')"><span class="icon">🏢</span><span class="icon-text">组织管理</span></div>
|
|
329
329
|
<div class="nav-item" data-tooltip="部门管理" onclick="showPage('departments')"><span class="icon">🏛</span><span class="icon-text">部门管理</span></div>
|
|
330
|
+
<div class="nav-item" data-tooltip="Agent私聊" onclick="showPage('agentchat')"><span class="icon">🔒</span><span class="icon-text">Agent私聊</span></div>
|
|
330
331
|
<div class="nav-item" data-tooltip="会话管理" onclick="showPage('sessions')"><span class="icon">💬</span><span class="icon-text">会话管理</span></div>
|
|
331
332
|
<div class="nav-item" data-tooltip="记忆管理" onclick="showPage('memory')"><span class="icon">🧠</span><span class="icon-text">记忆管理</span></div>
|
|
332
333
|
<div class="nav-item" data-tooltip="权限管理" onclick="showPage('permissions')"><span class="icon">🔑</span><span class="icon-text">权限管理</span></div>
|
|
@@ -368,6 +369,7 @@ tr:hover{background:var(--surface2)}
|
|
|
368
369
|
<script src="admin/admin-logs.js"></script>
|
|
369
370
|
<script src="admin/admin-tasks.js"></script>
|
|
370
371
|
<script src="admin/admin-org.js"></script>
|
|
372
|
+
<script src="admin/admin-agentchat.js"></script>
|
|
371
373
|
<script src="admin/admin-system.js"></script>
|
|
372
374
|
</body>
|
|
373
375
|
</html>
|