myagent-ai 1.20.0 → 1.20.2

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.
@@ -60,8 +60,8 @@ class MainAgent(BaseAgent):
60
60
  <next_step>当 finish=false 时必填,描述下一步计划做什么(简洁明了,1-2句话)。finish=true 时为空。</next_step>
61
61
  </output>
62
62
 
63
- 事项注意:
64
- 1. toolstocal标签: 可列出所有需要执行的工具调用,可以多个工具。解析器会按顺序执行工具调用,最终全部执行完后,会连同所有结果,回调大语言模型。如果某个工具执行超时了,也会回调回调大模型,让大模型分析为什么超时,改用其他工具。
63
+ 注意事项:
64
+ 1. toolstocal标签: 尽量一次性列出所有需执行工具调用的,多个"tool""工具调用只要按顺序重复堆叠tool标签即可,解析器会按顺序执行工具调用,最终全部执行完后,会连同所有结果,回调大语言模型。如果某个工具执行超时了,也会回调回调大模型,让大模型分析为什么超时,改用其他工具。如非必要,不要一次仅调用一个工具。
65
65
  2. 上下文中的记忆系统说明
66
66
  - <automemory>: 系统自动根据你通过 <remember> 保存的记忆和当前用户输入,搜索出的 top10 相关记忆。这些是你过去主动记住的内容(包含时间信息),可供参考。
67
67
  - <recall_memory>: 你在上一轮通过 <recall> 指定的记忆搜索结果。系统根据你提供的关键字和时间点搜索了 top5 相关记忆。
@@ -55,10 +55,12 @@ class ChatBotManager:
55
55
  try:
56
56
  bot = self._create_bot(cfg, message_handler)
57
57
  if bot:
58
- self._bots[cfg.platform] = bot
59
- logger.info(f"聊天平台已配置: {cfg.platform}")
58
+ # 使用唯一 id 作为 key,支持多实例
59
+ key = cfg.id or cfg.platform
60
+ self._bots[key] = bot
61
+ logger.info(f"聊天平台已配置: {cfg.display_name or key}")
60
62
  except Exception as e:
61
- logger.error(f"平台 {cfg.platform} 初始化失败: {e}")
63
+ logger.error(f"平台 {cfg.display_name or cfg.platform} 初始化失败: {e}")
62
64
 
63
65
  def _create_bot(
64
66
  self,
@@ -115,6 +117,17 @@ class ChatBotManager:
115
117
  **config.extra,
116
118
  )
117
119
 
120
+ elif platform == "whatsapp":
121
+ from chatbot.whatsapp_bot import WhatsAppBot
122
+ return WhatsAppBot(
123
+ token=config.token,
124
+ app_id=config.app_id,
125
+ app_secret=config.app_secret,
126
+ allowed_users=config.allowed_users,
127
+ message_handler=message_handler,
128
+ **config.extra,
129
+ )
130
+
118
131
  else:
119
132
  logger.warning(f"不支持的平台: {platform}")
120
133
  return None
@@ -0,0 +1,179 @@
1
+ """
2
+ chatbot/whatsapp_bot.py - WhatsApp 机器人
3
+ ===========================================
4
+ 使用 whatsapp-web.js 或 Baileys 库接入 WhatsApp。
5
+ 支持二维码绑定。
6
+ 纯 Python 实现,异步运行。
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import asyncio
11
+ import json
12
+ import time
13
+ from typing import Optional, List
14
+
15
+ from chatbot.base import BaseChatBot, ChatMessage, ChatResponse
16
+
17
+ try:
18
+ import aiohttp
19
+ HAS_AIOHTTP = True
20
+ except ImportError:
21
+ HAS_AIOHTTP = False
22
+
23
+
24
+ class WhatsAppBot(BaseChatBot):
25
+ """
26
+ WhatsApp 机器人适配器。
27
+
28
+ 配置要求:
29
+ - token: Access Token (从 Meta Developer Portal 获取)
30
+ - app_id: App ID
31
+ - app_secret: App Secret
32
+ - webhook_url: Webhook URL (可选,用于接收消息)
33
+ - phone_number_id: Phone Number ID (从 Meta 获取)
34
+ - verify_token: Webhook 验证 Token (自动生成)
35
+
36
+ 支持两种模式:
37
+ 1. Cloud API 模式: 使用 Meta WhatsApp Business API
38
+ 2. 二维码绑定模式: 通过 QR Code 绑定(需要 Baileys 库)
39
+ """
40
+
41
+ platform_name = "whatsapp"
42
+
43
+ def __init__(self, **kwargs):
44
+ super().__init__(**kwargs)
45
+ self._app_id = kwargs.get("app_id", "")
46
+ self._app_secret = kwargs.get("app_secret", "")
47
+ self._phone_number_id = self.config.get("phone_number_id", "")
48
+ self._verify_token = self.config.get("verify_token", "")
49
+ self._webhook_base = self.config.get("webhook_base", "")
50
+ self._qrcode_callback = self.config.get("qrcode_callback", None)
51
+ self._qr_code = ""
52
+ self._connected = False
53
+
54
+ async def start(self):
55
+ """启动 WhatsApp 机器人"""
56
+ if not self.token:
57
+ self.logger.error("WhatsApp Access Token 未配置")
58
+ return
59
+
60
+ # Meta Cloud API 模式
61
+ if self._phone_number_id:
62
+ self.logger.info("WhatsApp Cloud API 模式启动")
63
+ self._connected = True
64
+ # Cloud API 模式通过 Webhook 接收消息,不需要主动轮询
65
+ # 保持运行
66
+ while self._running:
67
+ await asyncio.sleep(1)
68
+ else:
69
+ self.logger.warning(
70
+ "WhatsApp 需要配置 phone_number_id 才能使用 Cloud API 模式。"
71
+ "请访问 Meta Developer Portal 获取 WhatsApp Business API 凭据。"
72
+ )
73
+ self._connected = False
74
+
75
+ async def stop(self):
76
+ """停止 WhatsApp 机器人"""
77
+ self._running = False
78
+ self._connected = False
79
+ self.logger.info("WhatsApp 机器人已停止")
80
+
81
+ async def send_message(self, response: ChatResponse) -> bool:
82
+ """发送消息到 WhatsApp"""
83
+ if not self._connected or not response.chat_id:
84
+ return False
85
+
86
+ try:
87
+ if not HAS_AIOHTTP:
88
+ self.logger.error("请安装 aiohttp: pip install aiohttp")
89
+ return False
90
+
91
+ url = f"https://graph.facebook.com/v18.0/{self._phone_number_id}/messages"
92
+ headers = {
93
+ "Authorization": f"Bearer {self.token}",
94
+ "Content-Type": "application/json",
95
+ }
96
+ payload = {
97
+ "messaging_product": "whatsapp",
98
+ "to": response.chat_id,
99
+ "type": "text",
100
+ "text": {"body": response.text[:4096]},
101
+ }
102
+
103
+ async with aiohttp.ClientSession() as session:
104
+ async with session.post(url, headers=headers, json=payload) as resp:
105
+ if resp.status == 200:
106
+ return True
107
+ error_text = await resp.text()
108
+ self.logger.error(f"WhatsApp 发送失败 ({resp.status}): {error_text}")
109
+ return False
110
+ except Exception as e:
111
+ self.logger.error(f"发送消息失败: {e}")
112
+ return False
113
+
114
+ # ==========================================================================
115
+ # Webhook 处理
116
+ # ==========================================================================
117
+
118
+ def verify_webhook(self, mode: str, token: str, challenge: str) -> Optional[str]:
119
+ """验证 Webhook"""
120
+ if mode == "subscribe" and token == self._verify_token:
121
+ return challenge
122
+ return None
123
+
124
+ async def handle_webhook_event(self, event_data: dict):
125
+ """处理 Webhook 事件"""
126
+ try:
127
+ for entry in event_data.get("entry", []):
128
+ for change in entry.get("changes", []):
129
+ value = change.get("value", {})
130
+ messages = value.get("messages", [])
131
+ contacts = value.get("contacts", [])
132
+
133
+ # 构建联系人映射
134
+ contact_map = {}
135
+ for c in contacts:
136
+ wa_id = c.get("wa_id", "")
137
+ name = c.get("profile", {}).get("name", "")
138
+ contact_map[wa_id] = name
139
+
140
+ for msg in messages:
141
+ msg_type = msg.get("type", "")
142
+ if msg_type != "text":
143
+ continue
144
+
145
+ from_id = msg.get("from", "")
146
+ text_body = msg.get("text", {}).get("body", "")
147
+ msg_id = msg.get("id", "")
148
+ timestamp = msg.get("timestamp", "")
149
+
150
+ username = contact_map.get(from_id, from_id)
151
+
152
+ message = ChatMessage(
153
+ platform=self.platform_name,
154
+ chat_id=from_id,
155
+ user_id=from_id,
156
+ username=username,
157
+ text=text_body,
158
+ is_group=False,
159
+ reply_to=msg_id,
160
+ raw_data={"timestamp": timestamp, "msg_id": msg_id},
161
+ )
162
+ await self._handle_message(message)
163
+ except Exception as e:
164
+ self.logger.error(f"处理 Webhook 事件失败: {e}")
165
+
166
+ # ==========================================================================
167
+ # 二维码绑定
168
+ # ==========================================================================
169
+
170
+ def get_qr_code(self) -> str:
171
+ """获取当前 QR Code(用于绑定模式)"""
172
+ return self._qr_code
173
+
174
+ async def generate_qr_code(self) -> str:
175
+ """生成绑定二维码"""
176
+ # Cloud API 模式不需要 QR Code
177
+ # 如需 Baileys 模式,需额外安装 node.js 依赖
178
+ self.logger.info("WhatsApp Cloud API 模式不需要 QR Code 绑定,使用 Webhook 接收消息")
179
+ return ""
package/config.py CHANGED
@@ -105,15 +105,19 @@ class ModelEntry:
105
105
 
106
106
  @dataclass
107
107
  class ChatPlatformConfig:
108
- """单个聊天平台配置"""
108
+ """单个聊天平台配置(支持多实例,每个实例有唯一 id)"""
109
+ id: str = "" # 唯一标识符,如 "telegram_8616478723" 或自动生成
109
110
  enabled: bool = False
110
- platform: str = "" # telegram | discord | feishu | qq | wechat
111
+ platform: str = "" # telegram | discord | feishu | qq | wechat | whatsapp
111
112
  token: str = "" # Bot Token
112
113
  app_id: str = "" # App ID (某些平台需要)
113
114
  app_secret: str = "" # App Secret
114
115
  webhook_url: str = "" # Webhook URL
115
116
  allowed_users: List[str] = field(default_factory=list) # 允许的用户白名单(空=全部)
116
117
  extra: Dict[str, Any] = field(default_factory=dict) # 平台特有配置
118
+ bind_agent: str = "" # 绑定的 Agent path(空=不绑定/使用默认)
119
+ bind_agents: List[str] = field(default_factory=list) # 绑定的多个 Agent path(v1.20.2: 支持多 agent 绑定)
120
+ display_name: str = "" # 显示名称,如 "Telegram:8616478723"
117
121
 
118
122
 
119
123
  @dataclass
@@ -263,6 +267,7 @@ class ConfigManager:
263
267
  "MYAGENT_FEISHU_APP_SECRET": "feishu",
264
268
  "MYAGENT_QQ_TOKEN": "qq",
265
269
  "MYAGENT_WECHAT_TOKEN": "wechat",
270
+ "MYAGENT_WHATSAPP_TOKEN": "whatsapp",
266
271
  }
267
272
  for env_key, platform_name in platform_env.items():
268
273
  token = os.environ.get(env_key)
@@ -277,10 +282,39 @@ class ConfigManager:
277
282
  cp.enabled = True
278
283
  return
279
284
  cp = ChatPlatformConfig(platform=platform, token=token, enabled=True)
285
+ self._auto_platform_id(cp)
280
286
  self._config.chat_platforms.append(cp)
281
287
 
288
+ def _auto_platform_id(self, cp: ChatPlatformConfig):
289
+ """为平台配置自动生成唯一 ID 和显示名称"""
290
+ import re
291
+ if not cp.id:
292
+ if cp.platform == 'telegram' and cp.token:
293
+ # 从 token 提取 bot_id: 8616478723:AAFKir8DQ4nzYUiddhifLCKUc5K2MfHcv9U
294
+ match = re.match(r'^(\d+):', cp.token)
295
+ bot_id = match.group(1) if match else str(len(self._config.chat_platforms))
296
+ cp.id = f"telegram_{bot_id}"
297
+ cp.display_name = f"Telegram:{bot_id}"
298
+ elif cp.platform == 'whatsapp' and cp.token:
299
+ match = re.match(r'^(\d+)', cp.token)
300
+ phone_id = match.group(1) if match else str(len(self._config.chat_platforms))
301
+ cp.id = f"whatsapp_{phone_id}"
302
+ cp.display_name = f"WhatsApp:{phone_id}"
303
+ elif cp.token:
304
+ cp.id = f"{cp.platform}_{cp.token[:8]}"
305
+ cp.display_name = f"{cp.platform.capitalize()}:{cp.token[:8]}"
306
+ else:
307
+ idx = sum(1 for p in self._config.chat_platforms if p.platform == cp.platform)
308
+ cp.id = f"{cp.platform}_{idx}"
309
+ cp.display_name = f"{cp.platform.capitalize()} {idx + 1}"
310
+ if not cp.display_name:
311
+ cp.display_name = cp.id or cp.platform
312
+
282
313
  def _apply_defaults(self):
283
314
  """应用平台相关的默认值"""
315
+ # [v1.20.2] 为旧配置自动填充平台 id 和 display_name
316
+ for cp in self._config.chat_platforms:
317
+ self._auto_platform_id(cp)
284
318
  if not self._config.memory.db_path:
285
319
  self._config.memory.db_path = str(self.data_dir / "memory.db")
286
320
  # data_dir 由 property 自动计算为 _data_dir / "data",无需在此设置
@@ -341,12 +375,19 @@ class ConfigManager:
341
375
  setattr(target, key, value)
342
376
 
343
377
  def get_chat_platform(self, platform: str) -> Optional[ChatPlatformConfig]:
344
- """获取指定聊天平台配置"""
378
+ """获取指定聊天平台配置(兼容旧接口,返回第一个匹配的)"""
345
379
  for cp in self._config.chat_platforms:
346
380
  if cp.platform == platform:
347
381
  return cp
348
382
  return None
349
383
 
384
+ def get_chat_platform_by_id(self, platform_id: str) -> Optional[ChatPlatformConfig]:
385
+ """通过唯一 ID 获取聊天平台配置"""
386
+ for cp in self._config.chat_platforms:
387
+ if cp.id == platform_id:
388
+ return cp
389
+ return None
390
+
350
391
  def get_enabled_platforms(self) -> List[ChatPlatformConfig]:
351
392
  """获取所有启用的聊天平台"""
352
393
  return [cp for cp in self._config.chat_platforms if cp.enabled]
package/memory/manager.py CHANGED
@@ -306,10 +306,11 @@ class MemoryManager:
306
306
  """
307
307
  conn = self._get_conn()
308
308
  # 只排除纯内部审计条目,保留 tool_call/tool_result 供前端展示
309
+ # [v1.20.2] 使用 rowid 作为二级排序,防止同一秒内的消息顺序不确定
309
310
  sql = """SELECT * FROM memories
310
311
  WHERE session_id = ? AND category = 'session' AND role != ''
311
312
  AND key NOT IN ('llm_output', 'llm_input', 'tool_result_raw', 'conversation_insight')
312
- ORDER BY created_at ASC LIMIT ?"""
313
+ ORDER BY created_at ASC, rowid ASC LIMIT ?"""
313
314
  rows = conn.execute(sql, (session_id, limit)).fetchall()
314
315
  entries = [MemoryEntry.from_row(row) for row in rows]
315
316
  if include_roles:
@@ -319,9 +320,9 @@ class MemoryManager:
319
320
  def get_conversation_all(self, session_id, limit=5000) -> List[MemoryEntry]:
320
321
  """获取全量对话历史(包含所有内部条目),用于完整回溯。"""
321
322
  conn = self._get_conn()
322
- sql = """SELECT * FROM memories
323
+ sql = """SELECT * FROM memories
323
324
  WHERE session_id = ? AND category = 'session' AND role != ''
324
- ORDER BY created_at ASC LIMIT ?"""
325
+ ORDER BY created_at ASC, rowid ASC LIMIT ?"""
325
326
  rows = conn.execute(sql, (session_id, limit)).fetchall()
326
327
  return [MemoryEntry.from_row(row) for row in rows]
327
328
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "myagent-ai",
3
- "version": "1.20.0",
3
+ "version": "1.20.2",
4
4
  "description": "本地桌面端执行型AI助手 - Open Interpreter 风格 | Local Desktop Execution-Oriented AI Assistant",
5
5
  "main": "main.py",
6
6
  "bin": {
@@ -43,4 +43,4 @@
43
43
  "python": ">=3.10",
44
44
  "node": ">=18"
45
45
  }
46
- }
46
+ }
package/web/api_server.py CHANGED
@@ -2021,6 +2021,23 @@ window.toggleFullscreen = function() {{
2021
2021
  "status": "done",
2022
2022
  })
2023
2023
 
2024
+ # ── 硬上限:任务列表不允许超过 MAX_TASK_ITEMS 条 ──
2025
+ # 系统提示词虽然要求 LLM 精简到 8 条,但 LLM 经常忽略,
2026
+ # 加上 merge 保留 done 项,会导致列表无限增长。
2027
+ # 策略:优先保留未完成项 + 最近完成的项,移除最早的已完成项
2028
+ MAX_TASK_ITEMS = 10
2029
+ if len(merged) > MAX_TASK_ITEMS:
2030
+ # 分离未完成和已完成
2031
+ unfinished = [t for t in merged if t.get("status") != "done"]
2032
+ finished = [t for t in merged if t.get("status") == "done"]
2033
+ # 保留所有未完成项 + 尽可能多的已完成项(保留最新的)
2034
+ remaining = MAX_TASK_ITEMS - len(unfinished)
2035
+ if remaining > 0:
2036
+ finished = finished[-remaining:] # 保留最近完成的
2037
+ else:
2038
+ finished = []
2039
+ merged = unfinished + finished
2040
+
2024
2041
  return merged
2025
2042
 
2026
2043
  async def handle_get_task_plan(self, request):
@@ -2726,16 +2743,28 @@ window.toggleFullscreen = function() {{
2726
2743
  if "user" in data: (ad / "user.md").write_text(data["user"], encoding="utf-8")
2727
2744
  # 如果 name 改变,需要重命名目录
2728
2745
  new_name = data.get("name")
2746
+ renamed_to = None
2729
2747
  if new_name and new_name != path and not is_system:
2730
2748
  agents_dir = self._agents_dir()
2731
2749
  new_ad = agents_dir / new_name
2732
2750
  if not new_ad.exists():
2733
2751
  ad.rename(new_ad)
2734
2752
  logger.info(f"Agent 目录已重命名: {path} -> {new_name}")
2753
+ renamed_to = new_name
2754
+ # [v1.20.2] 重命名后更新 avatar_image URL 中的路径
2755
+ old_avatar = cfg.get("avatar_image", "")
2756
+ if old_avatar and f"/api/agents/{path}/" in old_avatar:
2757
+ cfg["avatar_image"] = old_avatar.replace(f"/api/agents/{path}/", f"/api/agents/{new_name}/")
2758
+ (new_ad / "config.json").write_text(
2759
+ json.dumps(cfg, indent=2, ensure_ascii=False), encoding="utf-8"
2760
+ )
2735
2761
  else:
2736
2762
  logger.warning(f"目标目录已存在,无法重命名: {new_name}")
2737
2763
  logger.info(f"更新 Agent: {path}")
2738
- return web.json_response({"ok": True, "hot_reload": True})
2764
+ result = {"ok": True, "hot_reload": True}
2765
+ if renamed_to:
2766
+ result["renamed_to"] = renamed_to
2767
+ return web.json_response(result)
2739
2768
 
2740
2769
  async def handle_delete_agent(self, request):
2741
2770
  """DELETE /api/agents/{path} - 删除 agent 及其所有子 agent"""
@@ -2944,7 +2973,9 @@ window.toggleFullscreen = function() {{
2944
2973
  platforms = []
2945
2974
  for cp in self.core.config_mgr.config.chat_platforms:
2946
2975
  platforms.append({
2976
+ "id": cp.id,
2947
2977
  "platform": cp.platform,
2978
+ "display_name": cp.display_name,
2948
2979
  "enabled": cp.enabled,
2949
2980
  "token": cp.token[:8] + "****" if cp.token else "",
2950
2981
  "app_id": cp.app_id,
@@ -2952,58 +2983,75 @@ window.toggleFullscreen = function() {{
2952
2983
  "webhook_url": cp.webhook_url,
2953
2984
  "allowed_users": cp.allowed_users,
2954
2985
  "extra": cp.extra,
2986
+ "bind_agent": cp.bind_agent,
2987
+ "bind_agents": cp.bind_agents,
2955
2988
  })
2956
2989
  return web.json_response(platforms)
2957
2990
 
2958
2991
  async def handle_get_platform(self, request):
2959
- """获取单个平台配置详情"""
2992
+ """获取单个平台配置详情(支持按 id 或 platform 查找)"""
2960
2993
  name = request.match_info["name"]
2961
- for cp in self.core.config_mgr.config.chat_platforms:
2962
- if cp.platform == name:
2963
- return web.json_response({
2964
- "platform": cp.platform,
2965
- "enabled": cp.enabled,
2966
- "token": cp.token,
2967
- "app_id": cp.app_id,
2968
- "app_secret": cp.app_secret,
2969
- "webhook_url": cp.webhook_url,
2970
- "allowed_users": cp.allowed_users,
2971
- "extra": cp.extra,
2972
- })
2973
- return web.json_response({"error": f"平台 {name} 不存在"}, status=404)
2994
+ # 优先按 id 查找,兼容按 platform 查找
2995
+ cp = self.core.config_mgr.get_chat_platform_by_id(name)
2996
+ if not cp:
2997
+ cp = self.core.config_mgr.get_chat_platform(name)
2998
+ if not cp:
2999
+ return web.json_response({"error": f"平台 {name} 不存在"}, status=404)
3000
+ return web.json_response({
3001
+ "id": cp.id,
3002
+ "platform": cp.platform,
3003
+ "display_name": cp.display_name,
3004
+ "enabled": cp.enabled,
3005
+ "token": cp.token,
3006
+ "app_id": cp.app_id,
3007
+ "app_secret": cp.app_secret,
3008
+ "webhook_url": cp.webhook_url,
3009
+ "allowed_users": cp.allowed_users,
3010
+ "extra": cp.extra,
3011
+ "bind_agent": cp.bind_agent,
3012
+ "bind_agents": cp.bind_agents,
3013
+ })
2974
3014
 
2975
3015
  async def handle_add_platform(self, request):
2976
- """新增聊天平台配置"""
3016
+ """新增聊天平台配置(支持多实例)"""
2977
3017
  data = await request.json()
2978
3018
  platform_name = data.get("platform", "")
2979
3019
  if not platform_name:
2980
3020
  return web.json_response({"error": "缺少 platform 字段"}, status=400)
2981
- # 检查是否已存在
2982
- existing = self.core.config_mgr.get_chat_platform(platform_name)
2983
- if existing:
2984
- return web.json_response({"error": f"平台 {platform_name} 已存在"}, status=409)
3021
+ # 不再限制同类型只能有一个,支持多 token 实例
2985
3022
  cp = ChatPlatformConfig(
2986
3023
  platform=platform_name,
2987
- enabled=data.get("enabled", False),
3024
+ enabled=data.get("enabled", True),
2988
3025
  token=data.get("token", ""),
2989
3026
  app_id=data.get("app_id", ""),
2990
3027
  app_secret=data.get("app_secret", ""),
2991
3028
  webhook_url=data.get("webhook_url", ""),
2992
3029
  allowed_users=data.get("allowed_users", []),
2993
3030
  extra=data.get("extra", {}),
3031
+ bind_agent=data.get("bind_agent", ""),
3032
+ bind_agents=data.get("bind_agents", []),
2994
3033
  )
3034
+ # 自动生成唯一 id 和 display_name
3035
+ self.core.config_mgr._auto_platform_id(cp)
3036
+ # 检查 id 唯一性
3037
+ existing_ids = {p.id for p in self.core.config_mgr.config.chat_platforms}
3038
+ if cp.id in existing_ids:
3039
+ return web.json_response({"error": f"平台实例 {cp.display_name} 已存在(ID: {cp.id})"}, status=409)
2995
3040
  self.core.config_mgr.config.chat_platforms.append(cp)
2996
3041
  self.core.config_mgr.save()
2997
3042
  # 热更新聊天平台
2998
3043
  self._hot_reload_chat_platforms()
2999
- logger.info(f"新增聊天平台: {platform_name}")
3000
- return web.json_response({"ok": True, "platform": platform_name, "hot_reload": True})
3044
+ logger.info(f"新增聊天平台: {cp.display_name} (id={cp.id})")
3045
+ return web.json_response({"ok": True, "id": cp.id, "platform": platform_name, "display_name": cp.display_name, "hot_reload": True})
3001
3046
 
3002
3047
  async def handle_update_platform(self, request):
3003
3048
  """更新聊天平台配置"""
3004
3049
  name = request.match_info["name"]
3005
3050
  data = await request.json()
3006
- cp = self.core.config_mgr.get_chat_platform(name)
3051
+ # 优先按 id 查找
3052
+ cp = self.core.config_mgr.get_chat_platform_by_id(name)
3053
+ if not cp:
3054
+ cp = self.core.config_mgr.get_chat_platform(name)
3007
3055
  if not cp:
3008
3056
  return web.json_response({"error": f"平台 {name} 不存在"}, status=404)
3009
3057
  if "enabled" in data:
@@ -3020,30 +3068,44 @@ window.toggleFullscreen = function() {{
3020
3068
  cp.allowed_users = data["allowed_users"]
3021
3069
  if "extra" in data:
3022
3070
  cp.extra = data["extra"]
3071
+ if "bind_agent" in data:
3072
+ cp.bind_agent = data["bind_agent"]
3073
+ if "bind_agents" in data:
3074
+ cp.bind_agents = data["bind_agents"]
3075
+ if "display_name" in data:
3076
+ cp.display_name = data["display_name"]
3077
+ # 如果 token 变了,重新生成 id/display_name
3078
+ if "token" in data and data["token"]:
3079
+ old_id = cp.id
3080
+ self.core.config_mgr._auto_platform_id(cp)
3081
+ if cp.id != old_id:
3082
+ logger.info(f"平台 ID 变更: {old_id} -> {cp.id}")
3023
3083
  self.core.config_mgr.save()
3024
3084
  # 热更新聊天平台
3025
3085
  self._hot_reload_chat_platforms()
3026
- logger.info(f"聊天平台配置已更新: {name}")
3027
- return web.json_response({"ok": True, "hot_reload": True})
3086
+ logger.info(f"聊天平台配置已更新: {cp.display_name} (id={cp.id})")
3087
+ return web.json_response({"ok": True, "id": cp.id, "hot_reload": True})
3028
3088
 
3029
3089
  async def handle_delete_platform(self, request):
3030
3090
  """删除聊天平台配置"""
3031
3091
  name = request.match_info["name"]
3032
3092
  platforms = self.core.config_mgr.config.chat_platforms
3033
3093
  for i, cp in enumerate(platforms):
3034
- if cp.platform == name:
3094
+ if cp.id == name or cp.platform == name:
3035
3095
  platforms.pop(i)
3036
3096
  self.core.config_mgr.save()
3037
3097
  # 热更新聊天平台
3038
3098
  self._hot_reload_chat_platforms()
3039
- logger.info(f"已删除聊天平台: {name}")
3099
+ logger.info(f"已删除聊天平台: {cp.display_name} (id={cp.id})")
3040
3100
  return web.json_response({"ok": True, "hot_reload": True})
3041
3101
  return web.json_response({"error": f"平台 {name} 不存在"}, status=404)
3042
3102
 
3043
3103
  async def handle_toggle_platform(self, request):
3044
3104
  """切换平台启用/禁用"""
3045
3105
  name = request.match_info["name"]
3046
- cp = self.core.config_mgr.get_chat_platform(name)
3106
+ cp = self.core.config_mgr.get_chat_platform_by_id(name)
3107
+ if not cp:
3108
+ cp = self.core.config_mgr.get_chat_platform(name)
3047
3109
  if not cp:
3048
3110
  return web.json_response({"error": f"平台 {name} 不存在"}, status=404)
3049
3111
  data = await request.json()
@@ -3051,14 +3113,18 @@ window.toggleFullscreen = function() {{
3051
3113
  self.core.config_mgr.save()
3052
3114
  # 热更新聊天平台
3053
3115
  self._hot_reload_chat_platforms()
3054
- logger.info(f"聊天平台 {name} 已{'启用' if cp.enabled else '禁用'}")
3055
- return web.json_response({"ok": True, "enabled": cp.enabled, "hot_reload": True})
3116
+ logger.info(f"聊天平台 {cp.display_name} 已{'启用' if cp.enabled else '禁用'}")
3117
+ return web.json_response({"ok": True, "enabled": cp.enabled, "id": cp.id, "hot_reload": True})
3056
3118
 
3057
3119
  async def handle_restart_platform(self, request):
3058
3120
  """重启指定平台Bot"""
3059
3121
  name = request.match_info["name"]
3122
+ cp = self.core.config_mgr.get_chat_platform_by_id(name)
3123
+ if not cp:
3124
+ cp = self.core.config_mgr.get_chat_platform(name)
3125
+ display = cp.display_name if cp else name
3060
3126
  # TODO: 实现平台 Bot 热重启
3061
- return web.json_response({"ok": True, "message": f"平台 {name} 重启请求已发送(重启需服务端支持)"})
3127
+ return web.json_response({"ok": True, "message": f"平台 {display} 重启请求已发送(重启需服务端支持)"})
3062
3128
 
3063
3129
  async def handle_platform_agents(self, request):
3064
3130
  """获取绑定到指定平台的所有 Agent"""
@@ -6254,7 +6320,10 @@ window.toggleFullscreen = function() {{
6254
6320
  avatar_file = ad / "avatar.png"
6255
6321
  if not avatar_file.exists():
6256
6322
  return web.json_response({"error": "头像不存在"}, status=404)
6257
- return web.FileResponse(str(avatar_file))
6323
+ # [v1.20.2] 添加缓存控制,防止浏览器缓存旧的 404 响应
6324
+ resp = web.FileResponse(str(avatar_file))
6325
+ resp.headers["Cache-Control"] = "no-cache"
6326
+ return resp
6258
6327
 
6259
6328
  # ── 知识库 RAG 搜索 ──
6260
6329
 
@@ -2138,6 +2138,15 @@ input,textarea,select{font:inherit}
2138
2138
  .inline-exec-result-btn{background:none;border:1px solid var(--border);color:var(--text2);font-size:11px;padding:2px 8px;border-radius:4px;cursor:pointer;transition:var(--transition);flex-shrink:0}
2139
2139
  .inline-exec-result-btn:hover{background:var(--bg2);border-color:var(--accent);color:var(--accent)}
2140
2140
 
2141
+ /* ── Tool Result Collapsible (合并卡片折叠区域) ── */
2142
+ .tool-result-collapsible{border-top:1px dashed var(--border);margin-top:2px;padding-top:2px}
2143
+ .tool-collapsible-toggle{font-size:11px;color:var(--text3);cursor:pointer;padding:2px 0;user-select:none;transition:var(--transition)}
2144
+ .tool-collapsible-toggle:hover{color:var(--accent)}
2145
+ .tool-collapsible-body{overflow:hidden;max-height:0;transition:max-height .25s ease}
2146
+ .tool-result-collapsible:not(.collapsed) .tool-collapsible-body{max-height:300px;overflow-y:auto}
2147
+ .tool-result-collapsible.collapsed .tool-collapsible-toggle::after{content:' ▸'}
2148
+ .tool-result-collapsible:not(.collapsed) .tool-collapsible-toggle::after{content:' ▾'}
2149
+
2141
2150
  /* ── Execution Result Modal ── */
2142
2151
  .exec-result-modal-overlay{position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:1000;display:flex;align-items:center;justify-content:center;animation:fadeIn .15s ease}
2143
2152
  .exec-result-modal{background:var(--bg);border:1px solid var(--border);border-radius:12px;width:min(680px,90vw);max-height:80vh;display:flex;flex-direction:column;box-shadow:0 20px 60px rgba(0,0,0,.25);animation:slideUp .2s ease}