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.
- package/agents/main_agent.py +2 -2
- package/chatbot/manager.py +16 -3
- package/chatbot/whatsapp_bot.py +179 -0
- package/config.py +44 -3
- package/memory/manager.py +4 -3
- package/package.json +2 -2
- package/web/api_server.py +102 -33
- package/web/ui/chat/chat.css +9 -0
- package/web/ui/chat/chat_main.js +97 -16
- package/web/ui/chat/flow_engine.js +38 -7
- package/web/ui/index.html +183 -46
package/agents/main_agent.py
CHANGED
|
@@ -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 相关记忆。
|
package/chatbot/manager.py
CHANGED
|
@@ -55,10 +55,12 @@ class ChatBotManager:
|
|
|
55
55
|
try:
|
|
56
56
|
bot = self._create_bot(cfg, message_handler)
|
|
57
57
|
if bot:
|
|
58
|
-
|
|
59
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
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",
|
|
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"新增聊天平台: {
|
|
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
|
-
|
|
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"聊天平台配置已更新: {
|
|
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"已删除聊天平台: {
|
|
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.
|
|
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"聊天平台 {
|
|
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"平台 {
|
|
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
|
-
|
|
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
|
|
package/web/ui/chat/chat.css
CHANGED
|
@@ -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}
|