myagent-ai 1.20.1 → 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/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 +85 -33
- package/web/ui/chat/chat.css +9 -0
- package/web/ui/chat/chat_main.js +57 -12
- package/web/ui/chat/flow_engine.js +29 -7
- package/web/ui/index.html +151 -41
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
|
@@ -2743,16 +2743,28 @@ window.toggleFullscreen = function() {{
|
|
|
2743
2743
|
if "user" in data: (ad / "user.md").write_text(data["user"], encoding="utf-8")
|
|
2744
2744
|
# 如果 name 改变,需要重命名目录
|
|
2745
2745
|
new_name = data.get("name")
|
|
2746
|
+
renamed_to = None
|
|
2746
2747
|
if new_name and new_name != path and not is_system:
|
|
2747
2748
|
agents_dir = self._agents_dir()
|
|
2748
2749
|
new_ad = agents_dir / new_name
|
|
2749
2750
|
if not new_ad.exists():
|
|
2750
2751
|
ad.rename(new_ad)
|
|
2751
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
|
+
)
|
|
2752
2761
|
else:
|
|
2753
2762
|
logger.warning(f"目标目录已存在,无法重命名: {new_name}")
|
|
2754
2763
|
logger.info(f"更新 Agent: {path}")
|
|
2755
|
-
|
|
2764
|
+
result = {"ok": True, "hot_reload": True}
|
|
2765
|
+
if renamed_to:
|
|
2766
|
+
result["renamed_to"] = renamed_to
|
|
2767
|
+
return web.json_response(result)
|
|
2756
2768
|
|
|
2757
2769
|
async def handle_delete_agent(self, request):
|
|
2758
2770
|
"""DELETE /api/agents/{path} - 删除 agent 及其所有子 agent"""
|
|
@@ -2961,7 +2973,9 @@ window.toggleFullscreen = function() {{
|
|
|
2961
2973
|
platforms = []
|
|
2962
2974
|
for cp in self.core.config_mgr.config.chat_platforms:
|
|
2963
2975
|
platforms.append({
|
|
2976
|
+
"id": cp.id,
|
|
2964
2977
|
"platform": cp.platform,
|
|
2978
|
+
"display_name": cp.display_name,
|
|
2965
2979
|
"enabled": cp.enabled,
|
|
2966
2980
|
"token": cp.token[:8] + "****" if cp.token else "",
|
|
2967
2981
|
"app_id": cp.app_id,
|
|
@@ -2969,58 +2983,75 @@ window.toggleFullscreen = function() {{
|
|
|
2969
2983
|
"webhook_url": cp.webhook_url,
|
|
2970
2984
|
"allowed_users": cp.allowed_users,
|
|
2971
2985
|
"extra": cp.extra,
|
|
2986
|
+
"bind_agent": cp.bind_agent,
|
|
2987
|
+
"bind_agents": cp.bind_agents,
|
|
2972
2988
|
})
|
|
2973
2989
|
return web.json_response(platforms)
|
|
2974
2990
|
|
|
2975
2991
|
async def handle_get_platform(self, request):
|
|
2976
|
-
"""
|
|
2992
|
+
"""获取单个平台配置详情(支持按 id 或 platform 查找)"""
|
|
2977
2993
|
name = request.match_info["name"]
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
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
|
+
})
|
|
2991
3014
|
|
|
2992
3015
|
async def handle_add_platform(self, request):
|
|
2993
|
-
"""
|
|
3016
|
+
"""新增聊天平台配置(支持多实例)"""
|
|
2994
3017
|
data = await request.json()
|
|
2995
3018
|
platform_name = data.get("platform", "")
|
|
2996
3019
|
if not platform_name:
|
|
2997
3020
|
return web.json_response({"error": "缺少 platform 字段"}, status=400)
|
|
2998
|
-
#
|
|
2999
|
-
existing = self.core.config_mgr.get_chat_platform(platform_name)
|
|
3000
|
-
if existing:
|
|
3001
|
-
return web.json_response({"error": f"平台 {platform_name} 已存在"}, status=409)
|
|
3021
|
+
# 不再限制同类型只能有一个,支持多 token 实例
|
|
3002
3022
|
cp = ChatPlatformConfig(
|
|
3003
3023
|
platform=platform_name,
|
|
3004
|
-
enabled=data.get("enabled",
|
|
3024
|
+
enabled=data.get("enabled", True),
|
|
3005
3025
|
token=data.get("token", ""),
|
|
3006
3026
|
app_id=data.get("app_id", ""),
|
|
3007
3027
|
app_secret=data.get("app_secret", ""),
|
|
3008
3028
|
webhook_url=data.get("webhook_url", ""),
|
|
3009
3029
|
allowed_users=data.get("allowed_users", []),
|
|
3010
3030
|
extra=data.get("extra", {}),
|
|
3031
|
+
bind_agent=data.get("bind_agent", ""),
|
|
3032
|
+
bind_agents=data.get("bind_agents", []),
|
|
3011
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)
|
|
3012
3040
|
self.core.config_mgr.config.chat_platforms.append(cp)
|
|
3013
3041
|
self.core.config_mgr.save()
|
|
3014
3042
|
# 热更新聊天平台
|
|
3015
3043
|
self._hot_reload_chat_platforms()
|
|
3016
|
-
logger.info(f"新增聊天平台: {
|
|
3017
|
-
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})
|
|
3018
3046
|
|
|
3019
3047
|
async def handle_update_platform(self, request):
|
|
3020
3048
|
"""更新聊天平台配置"""
|
|
3021
3049
|
name = request.match_info["name"]
|
|
3022
3050
|
data = await request.json()
|
|
3023
|
-
|
|
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)
|
|
3024
3055
|
if not cp:
|
|
3025
3056
|
return web.json_response({"error": f"平台 {name} 不存在"}, status=404)
|
|
3026
3057
|
if "enabled" in data:
|
|
@@ -3037,30 +3068,44 @@ window.toggleFullscreen = function() {{
|
|
|
3037
3068
|
cp.allowed_users = data["allowed_users"]
|
|
3038
3069
|
if "extra" in data:
|
|
3039
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}")
|
|
3040
3083
|
self.core.config_mgr.save()
|
|
3041
3084
|
# 热更新聊天平台
|
|
3042
3085
|
self._hot_reload_chat_platforms()
|
|
3043
|
-
logger.info(f"聊天平台配置已更新: {
|
|
3044
|
-
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})
|
|
3045
3088
|
|
|
3046
3089
|
async def handle_delete_platform(self, request):
|
|
3047
3090
|
"""删除聊天平台配置"""
|
|
3048
3091
|
name = request.match_info["name"]
|
|
3049
3092
|
platforms = self.core.config_mgr.config.chat_platforms
|
|
3050
3093
|
for i, cp in enumerate(platforms):
|
|
3051
|
-
if cp.platform == name:
|
|
3094
|
+
if cp.id == name or cp.platform == name:
|
|
3052
3095
|
platforms.pop(i)
|
|
3053
3096
|
self.core.config_mgr.save()
|
|
3054
3097
|
# 热更新聊天平台
|
|
3055
3098
|
self._hot_reload_chat_platforms()
|
|
3056
|
-
logger.info(f"已删除聊天平台: {
|
|
3099
|
+
logger.info(f"已删除聊天平台: {cp.display_name} (id={cp.id})")
|
|
3057
3100
|
return web.json_response({"ok": True, "hot_reload": True})
|
|
3058
3101
|
return web.json_response({"error": f"平台 {name} 不存在"}, status=404)
|
|
3059
3102
|
|
|
3060
3103
|
async def handle_toggle_platform(self, request):
|
|
3061
3104
|
"""切换平台启用/禁用"""
|
|
3062
3105
|
name = request.match_info["name"]
|
|
3063
|
-
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)
|
|
3064
3109
|
if not cp:
|
|
3065
3110
|
return web.json_response({"error": f"平台 {name} 不存在"}, status=404)
|
|
3066
3111
|
data = await request.json()
|
|
@@ -3068,14 +3113,18 @@ window.toggleFullscreen = function() {{
|
|
|
3068
3113
|
self.core.config_mgr.save()
|
|
3069
3114
|
# 热更新聊天平台
|
|
3070
3115
|
self._hot_reload_chat_platforms()
|
|
3071
|
-
logger.info(f"聊天平台 {
|
|
3072
|
-
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})
|
|
3073
3118
|
|
|
3074
3119
|
async def handle_restart_platform(self, request):
|
|
3075
3120
|
"""重启指定平台Bot"""
|
|
3076
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
|
|
3077
3126
|
# TODO: 实现平台 Bot 热重启
|
|
3078
|
-
return web.json_response({"ok": True, "message": f"平台 {
|
|
3127
|
+
return web.json_response({"ok": True, "message": f"平台 {display} 重启请求已发送(重启需服务端支持)"})
|
|
3079
3128
|
|
|
3080
3129
|
async def handle_platform_agents(self, request):
|
|
3081
3130
|
"""获取绑定到指定平台的所有 Agent"""
|
|
@@ -6271,7 +6320,10 @@ window.toggleFullscreen = function() {{
|
|
|
6271
6320
|
avatar_file = ad / "avatar.png"
|
|
6272
6321
|
if not avatar_file.exists():
|
|
6273
6322
|
return web.json_response({"error": "头像不存在"}, status=404)
|
|
6274
|
-
|
|
6323
|
+
# [v1.20.2] 添加缓存控制,防止浏览器缓存旧的 404 响应
|
|
6324
|
+
resp = web.FileResponse(str(avatar_file))
|
|
6325
|
+
resp.headers["Cache-Control"] = "no-cache"
|
|
6326
|
+
return resp
|
|
6275
6327
|
|
|
6276
6328
|
# ── 知识库 RAG 搜索 ──
|
|
6277
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}
|
package/web/ui/chat/chat_main.js
CHANGED
|
@@ -225,10 +225,10 @@ var rpSections = {
|
|
|
225
225
|
|
|
226
226
|
// ── 平台图标映射 ──
|
|
227
227
|
const PLATFORM_ICONS = {
|
|
228
|
-
telegram: '✈️', discord: '🎮', feishu: '🐦', qq: '🐧', wechat: '💬',
|
|
228
|
+
telegram: '✈️', discord: '🎮', feishu: '🐦', qq: '🐧', wechat: '💬', whatsapp: '📱',
|
|
229
229
|
};
|
|
230
230
|
const PLATFORM_LABELS = {
|
|
231
|
-
telegram: 'Telegram', discord: 'Discord', feishu: '飞书', qq: 'QQ', wechat: '微信',
|
|
231
|
+
telegram: 'Telegram', discord: 'Discord', feishu: '飞书', qq: 'QQ', wechat: '微信', whatsapp: 'WhatsApp',
|
|
232
232
|
};
|
|
233
233
|
|
|
234
234
|
// ── Utility: deterministic color from agent name ──
|
|
@@ -1638,19 +1638,20 @@ function showAgentModal(editPath, parentPath, data) {
|
|
|
1638
1638
|
+ '<option value="feishu"' + (platform === 'feishu' ? ' selected' : '') + '>🐦 飞书</option>'
|
|
1639
1639
|
+ '<option value="qq"' + (platform === 'qq' ? ' selected' : '') + '>🐧 QQ</option>'
|
|
1640
1640
|
+ '<option value="wechat"' + (platform === 'wechat' ? ' selected' : '') + '>💬 微信</option>'
|
|
1641
|
+
+ '<option value="whatsapp"' + (platform === 'whatsapp' ? ' selected' : '') + '>📱 WhatsApp</option>'
|
|
1641
1642
|
+ '</select>'
|
|
1642
1643
|
+ '<button class="config-action-btn" style="padding:6px 10px;font-size:11px" onclick="window.location.href=\'/ui/index.html?page=platforms\'" title="管理聊天平台">管理平台</button>'
|
|
1643
1644
|
+ '</div>'
|
|
1644
|
-
+ '<div class="hint">选择 Agent
|
|
1645
|
+
+ '<div class="hint">选择 Agent 对接的聊天平台。多实例绑定和 Agent 选择请在"管理平台"中配置。</div></div>'
|
|
1645
1646
|
+ '<div class="agent-form-group" id="platformTokenGroup" style="display:' + (platform ? '' : 'none') + '"><label>平台 Token</label>'
|
|
1646
1647
|
+ '<input type="text" id="agentFormPlatformToken" placeholder="Bot Token" value="' + escapeHtml(platform_token) + '">'
|
|
1647
|
-
+ '<div class="hint">聊天平台的 Bot Token
|
|
1648
|
-
+ '<div class="agent-form-group" id="platformAppIdGroup" style="display:' + (platform === 'feishu' ? '' : 'none') + '"><label>App ID</label>'
|
|
1649
|
-
+ '<input type="text" id="agentFormPlatformAppId" placeholder="
|
|
1650
|
-
+ '<div class="hint"
|
|
1651
|
-
+ '<div class="agent-form-group" id="platformAppSecretGroup" style="display:' + (platform === 'feishu' ? '' : 'none') + '"><label>App Secret</label>'
|
|
1652
|
-
+ '<input type="text" id="agentFormPlatformAppSecret" placeholder="
|
|
1653
|
-
+ '<div class="hint"
|
|
1648
|
+
+ '<div class="hint">聊天平台的 Bot Token(也可在"管理平台"中配置)</div></div>'
|
|
1649
|
+
+ '<div class="agent-form-group" id="platformAppIdGroup" style="display:' + ((platform === 'feishu' || platform === 'whatsapp') ? '' : 'none') + '"><label>App ID</label>'
|
|
1650
|
+
+ '<input type="text" id="agentFormPlatformAppId" placeholder="App ID" value="' + escapeHtml(platform_app_id) + '">'
|
|
1651
|
+
+ '<div class="hint">应用 App ID</div></div>'
|
|
1652
|
+
+ '<div class="agent-form-group" id="platformAppSecretGroup" style="display:' + ((platform === 'feishu' || platform === 'whatsapp') ? '' : 'none') + '"><label>App Secret</label>'
|
|
1653
|
+
+ '<input type="text" id="agentFormPlatformAppSecret" placeholder="App Secret" value="' + escapeHtml(platform_app_secret) + '">'
|
|
1654
|
+
+ '<div class="hint">应用 App Secret</div></div>'
|
|
1654
1655
|
+ '</div>'
|
|
1655
1656
|
+ '<div class="agent-form-group"><label>Identity (身份)</label>'
|
|
1656
1657
|
+ '<textarea id="agentFormIdentity" placeholder="你是一个... 你擅长... 你的风格是..." rows="3">' + escapeHtml(identity) + '</textarea>'
|
|
@@ -2446,11 +2447,11 @@ function groupHistoryMessages(messages) {
|
|
|
2446
2447
|
if (key === 'tool_call') {
|
|
2447
2448
|
var tc = m._parsedToolCall || parseToolCallContent(m.content);
|
|
2448
2449
|
m._parsedToolCall = tc;
|
|
2449
|
-
return { type: 'exec', data: { id: evtId
|
|
2450
|
+
return { type: 'exec', data: { id: evtId, type: 'tool_call', title: tc.title, tool_name: tc.toolName, params: tc.params || undefined, status: 'done' } };
|
|
2450
2451
|
}
|
|
2451
2452
|
if (m.role === 'tool') {
|
|
2452
2453
|
var tr = parseToolResultContent(m.content);
|
|
2453
|
-
return { type: '
|
|
2454
|
+
return { type: 'exec_result', data: { id: evtId, type: 'tool_result', title: tr.title, tool_name: tr.toolName, success: tr.success, summary: tr.body.substring(0, 500).trim(), result: { output: tr.body.substring(0, 2000) } } };
|
|
2454
2455
|
}
|
|
2455
2456
|
if (m.content && m.content.trim() && m.content !== '(无回复)') {
|
|
2456
2457
|
return { type: 'text', content: m.content };
|
|
@@ -2458,6 +2459,47 @@ function groupHistoryMessages(messages) {
|
|
|
2458
2459
|
return null;
|
|
2459
2460
|
}
|
|
2460
2461
|
|
|
2462
|
+
// 合并连续的 tool_call + tool_result 为单个 exec 卡片(一个工具一个框)
|
|
2463
|
+
// [v1.20.2] 修复:向前搜索匹配的 tool_result,不再仅检查紧邻的下一个 part
|
|
2464
|
+
function mergeToolParts(parts) {
|
|
2465
|
+
var merged = [];
|
|
2466
|
+
var consumed = {}; // 记录已被 tool_call 消费的 exec_result 索引
|
|
2467
|
+
for (var pi = 0; pi < parts.length; pi++) {
|
|
2468
|
+
if (consumed[pi]) continue; // 已被前面的 tool_call 消费
|
|
2469
|
+
var p = parts[pi];
|
|
2470
|
+
if (p.type === 'exec' && p.data.type === 'tool_call') {
|
|
2471
|
+
var callData = p.data;
|
|
2472
|
+
var mergedPart = { type: 'exec', data: Object.assign({}, callData, { has_result: false, result_data: null }) };
|
|
2473
|
+
// 向前搜索匹配的 tool_result(允许中间有 text 等其他 part)
|
|
2474
|
+
for (var ri = pi + 1; ri < parts.length; ri++) {
|
|
2475
|
+
if (parts[ri].type === 'exec_result') {
|
|
2476
|
+
var nextData = parts[ri].data;
|
|
2477
|
+
if (nextData.tool_name === callData.tool_name || nextData.tool_call_id === callData.id) {
|
|
2478
|
+
mergedPart.data.has_result = true;
|
|
2479
|
+
mergedPart.data.success = nextData.success;
|
|
2480
|
+
mergedPart.data.summary = nextData.summary;
|
|
2481
|
+
mergedPart.data.result = nextData.result;
|
|
2482
|
+
if (nextData.title) mergedPart.data.title = nextData.title;
|
|
2483
|
+
consumed[ri] = true; // 标记为已消费
|
|
2484
|
+
break;
|
|
2485
|
+
}
|
|
2486
|
+
}
|
|
2487
|
+
// 遇到下一个 tool_call 就停止搜索(tool_result 不会跨越 tool_call)
|
|
2488
|
+
if (parts[ri].type === 'exec' && parts[ri].data.type === 'tool_call') break;
|
|
2489
|
+
}
|
|
2490
|
+
evtId++;
|
|
2491
|
+
merged.push(mergedPart);
|
|
2492
|
+
} else if (p.type === 'exec_result') {
|
|
2493
|
+
// 孤立的 tool_result(没有对应的 tool_call),保留为失败卡片
|
|
2494
|
+
evtId++;
|
|
2495
|
+
merged.push({ type: 'exec', data: Object.assign({}, p.data, { type: 'tool_result', has_result: true }) });
|
|
2496
|
+
} else {
|
|
2497
|
+
merged.push(p);
|
|
2498
|
+
}
|
|
2499
|
+
}
|
|
2500
|
+
return merged;
|
|
2501
|
+
}
|
|
2502
|
+
|
|
2461
2503
|
var i = 0;
|
|
2462
2504
|
while (i < messages.length) {
|
|
2463
2505
|
var msg = messages[i];
|
|
@@ -2507,6 +2549,9 @@ function groupHistoryMessages(messages) {
|
|
|
2507
2549
|
i++;
|
|
2508
2550
|
}
|
|
2509
2551
|
|
|
2552
|
+
// [v1.20.2] 合并连续 tool_call + tool_result 为单个卡片
|
|
2553
|
+
parts = mergeToolParts(parts);
|
|
2554
|
+
|
|
2510
2555
|
// 组装: content 取最后一段文本(用于搜索/纯文本回退),parts 展示完整时间线
|
|
2511
2556
|
var textParts = parts.filter(function(p) { return p.type === 'text'; });
|
|
2512
2557
|
var hasExecParts = parts.some(function(p) { return p.type === 'exec'; });
|
|
@@ -925,7 +925,12 @@ function renderInlineExecEvent(data, msgIdx) {
|
|
|
925
925
|
|
|
926
926
|
// Determine event state class for legacy events
|
|
927
927
|
let stateClass = '';
|
|
928
|
-
|
|
928
|
+
// [v1.20.2] 合并卡片:tool_call + has_result → 显示完成状态
|
|
929
|
+
if (data.has_result) {
|
|
930
|
+
stateClass = data.success === false ? 'tool-failed' : 'tool-success';
|
|
931
|
+
} else if (data.type === 'tool_call' || data.type === 'skill_call') {
|
|
932
|
+
stateClass = 'tool-call-pending';
|
|
933
|
+
}
|
|
929
934
|
if (data.type === 'tool_result') stateClass = data.success === false ? 'tool-failed' : 'tool-success';
|
|
930
935
|
if (data.type === 'code_result') stateClass = data.timed_out ? 'tool-failed' : (data.success ? 'tool-success' : 'tool-failed');
|
|
931
936
|
|
|
@@ -942,8 +947,8 @@ function renderInlineExecEvent(data, msgIdx) {
|
|
|
942
947
|
|
|
943
948
|
// Build body content
|
|
944
949
|
let bodyHtml = '';
|
|
945
|
-
// Params for tool_call/skill_call
|
|
946
|
-
if (data.params && (data.type === 'tool_call' || data.type === 'skill_call')) {
|
|
950
|
+
// Params for tool_call/skill_call (合并卡片中 params 在折叠区域显示)
|
|
951
|
+
if (data.params && (data.type === 'tool_call' || data.type === 'skill_call') && !data.has_result) {
|
|
947
952
|
let paramPreview = typeof data.params === 'string' ? data.params : JSON.stringify(data.params);
|
|
948
953
|
if (paramPreview.length > 300) paramPreview = paramPreview.substring(0, 300) + '...';
|
|
949
954
|
bodyHtml += '<div class="inline-exec-code">' + escapeHtml(paramPreview) + '</div>';
|
|
@@ -952,16 +957,33 @@ function renderInlineExecEvent(data, msgIdx) {
|
|
|
952
957
|
if (data.code_preview && (data.type === 'code_exec' || data.type === 'code_result')) {
|
|
953
958
|
bodyHtml += '<div class="inline-exec-code" onclick="showExecResultModal(' + msgIdx + ', \'' + data.id + '\')" title="点击查看完整结果">' + escapeHtml(data.code_preview) + '</div>';
|
|
954
959
|
}
|
|
955
|
-
//
|
|
956
|
-
if (data.
|
|
960
|
+
// [v1.20.2] 合并卡片:显示折叠的参数 + 结果摘要 + 查看详情按钮
|
|
961
|
+
if (data.has_result) {
|
|
962
|
+
// 折叠的参数区域
|
|
963
|
+
if (data.params) {
|
|
964
|
+
let paramPreview = typeof data.params === 'string' ? data.params : JSON.stringify(data.params);
|
|
965
|
+
if (paramPreview.length > 300) paramPreview = paramPreview.substring(0, 300) + '...';
|
|
966
|
+
bodyHtml += '<div class="tool-result-collapsible"><div class="tool-collapsible-toggle" onclick="this.parentElement.classList.toggle(\'collapsed\')">📋 参数</div><div class="tool-collapsible-body"><div class="inline-exec-code">' + escapeHtml(paramPreview) + '</div></div></div>';
|
|
967
|
+
}
|
|
968
|
+
// 结果摘要
|
|
969
|
+
if (data.summary) {
|
|
970
|
+
bodyHtml += '<div class="inline-exec-summary">' + escapeHtml(data.summary) + '</div>';
|
|
971
|
+
}
|
|
972
|
+
// 查看详情按钮
|
|
973
|
+
if (data.result) {
|
|
974
|
+
bodyHtml += '<button class="inline-exec-result-btn" onclick="showToolResultModal(' + msgIdx + ', \'' + data.id + '\')">查看详情</button>';
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
// Summary for standalone tool_result/skill_result (非合并的孤立结果)
|
|
978
|
+
if (!data.has_result && data.summary && (data.type === 'tool_result' || data.type === 'skill_result')) {
|
|
957
979
|
bodyHtml += '<div class="inline-exec-summary">' + escapeHtml(data.summary) + '</div>';
|
|
958
980
|
}
|
|
959
981
|
// Result button for code_result
|
|
960
982
|
if (data.type === 'code_result' && (data.stdout || data.stderr || data.error)) {
|
|
961
983
|
bodyHtml += '<button class="inline-exec-result-btn" onclick="showExecResultModal(' + msgIdx + ', \'' + data.id + '\')">查看详情</button>';
|
|
962
984
|
}
|
|
963
|
-
// Result button for tool_result/skill_result
|
|
964
|
-
if ((data.type === 'tool_result' || data.type === 'skill_result') && data.result) {
|
|
985
|
+
// Result button for standalone tool_result/skill_result
|
|
986
|
+
if (!data.has_result && (data.type === 'tool_result' || data.type === 'skill_result') && data.result) {
|
|
965
987
|
bodyHtml += '<button class="inline-exec-result-btn" onclick="showToolResultModal(' + msgIdx + ', \'' + data.id + '\')">查看详情</button>';
|
|
966
988
|
}
|
|
967
989
|
|
package/web/ui/index.html
CHANGED
|
@@ -373,7 +373,9 @@ async function api(url,opts={}){
|
|
|
373
373
|
}catch(e){showToast('请求失败: '+e.message,'danger');return {error:e.message}}
|
|
374
374
|
}
|
|
375
375
|
function $(id){return document.getElementById(id)}
|
|
376
|
-
function escHtml(s){return String(s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"')}
|
|
376
|
+
function escHtml(s){return String(s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,''')}
|
|
377
|
+
/* [v1.20.2] 安全地将任意文本插入 JS 模板字符串中 textarea 元素:转义反引号、${ 和 </textarea */
|
|
378
|
+
function escTpl(s){return String(s||'').replace(/\\/g,'\\\\').replace(/`/g,'\\`').replace(/\$\{/g,'\\${').replace(/<\/textarea/gi,'<\\/textarea')}
|
|
377
379
|
function fmtDate(d){if(!d)return'-';try{return new Date(d).toLocaleString('zh-CN',{month:'2-digit',day:'2-digit',hour:'2-digit',minute:'2-digit'})}catch(e){return d.slice(0,16)}}
|
|
378
380
|
function fmtTimeAgo(d){if(!d)return'-';const s=Math.floor((Date.now()-new Date(d))/1000);if(s<60)return s+'秒前';if(s<3600)return Math.floor(s/60)+'分钟前';if(s<86400)return Math.floor(s/3600)+'小时前';return Math.floor(s/86400)+'天前'}
|
|
379
381
|
function showToast(msg,type='info',duration=3000){
|
|
@@ -752,7 +754,7 @@ async function showWorkdirModal(agentPath){
|
|
|
752
754
|
$('modalContainer').innerHTML=`<div class="modal-overlay" onclick="closeModal()"><div class="modal modal-wide" onclick="event.stopPropagation()" style="max-width:600px">
|
|
753
755
|
<div class="flex justify-between items-center mb-16">
|
|
754
756
|
<h3>${title}</h3>
|
|
755
|
-
<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
|
|
757
|
+
<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)">刷新</button></div>
|
|
756
758
|
</div>
|
|
757
759
|
<div id="workdirBreadcrumb" style="font-size:12px;color:var(--text3);margin-bottom:8px"></div>
|
|
758
760
|
<div id="workdirContent"><div class="empty">加载中...</div></div>
|
|
@@ -936,7 +938,7 @@ async function openEditAgentModal(path){
|
|
|
936
938
|
<div class="form-group"><label>创建时间 <span style="color:var(--text2)">(只读)</span></label><input id="eaCreated" value="${fmtDate(a.created_at)}" disabled></div>
|
|
937
939
|
</div>
|
|
938
940
|
<div class="form-group"><label>工作目录</label><input id="eaWorkDir" value="${escHtml(a.work_dir||'')}" placeholder="留空使用默认"></div>
|
|
939
|
-
<div class="form-group"><label>系统提示</label><textarea id="eaPrompt" rows="4" ${isSys?'disabled':''}
|
|
941
|
+
<div class="form-group"><label>系统提示</label><textarea id="eaPrompt" rows="4" ${isSys?'disabled':''}></textarea></div>
|
|
940
942
|
<div class="flex gap-8 mt-16">
|
|
941
943
|
<button class="btn btn-primary" onclick="doSaveAgent('${escHtml(path)}')">保存</button>
|
|
942
944
|
<button class="btn btn-ghost" onclick="closeModal()">关闭</button>
|
|
@@ -945,11 +947,11 @@ async function openEditAgentModal(path){
|
|
|
945
947
|
|
|
946
948
|
<!-- 人格系统 -->
|
|
947
949
|
<div id="atSoul" class="hidden">
|
|
948
|
-
<div class="form-group"><label>soul.md</label><textarea id="eaSoul" rows="10" style="min-height:200px"
|
|
950
|
+
<div class="form-group"><label>soul.md</label><textarea id="eaSoul" rows="10" style="min-height:200px"></textarea>
|
|
949
951
|
<button class="btn btn-sm btn-primary mt-8" onclick="doSaveSoul('${escHtml(path)}')">保存 Soul</button></div>
|
|
950
|
-
<div class="form-group"><label>identity.md</label><textarea id="eaIdentity" rows="10" style="min-height:200px"
|
|
952
|
+
<div class="form-group"><label>identity.md</label><textarea id="eaIdentity" rows="10" style="min-height:200px"></textarea>
|
|
951
953
|
<button class="btn btn-sm btn-primary mt-8" onclick="doSaveIdentity('${escHtml(path)}')">保存 Identity</button></div>
|
|
952
|
-
<div class="form-group"><label>user.md</label><textarea id="eaUser" rows="6" style="min-height:120px"
|
|
954
|
+
<div class="form-group"><label>user.md</label><textarea id="eaUser" rows="6" style="min-height:120px"></textarea>
|
|
953
955
|
<button class="btn btn-sm btn-primary mt-8" onclick="doSaveUser('${escHtml(path)}')">保存 User</button></div>
|
|
954
956
|
</div>
|
|
955
957
|
|
|
@@ -978,6 +980,11 @@ async function openEditAgentModal(path){
|
|
|
978
980
|
<div id="atPermsContent"><div class="empty">加载中...</div></div>
|
|
979
981
|
</div>
|
|
980
982
|
</div></div>`;
|
|
983
|
+
// [v1.20.2] 安全设置 textarea 内容(避免模板字符串注入和 HTML 实体问题)
|
|
984
|
+
if($('eaPrompt'))$('eaPrompt').value=a.system_prompt||'';
|
|
985
|
+
if($('eaSoul'))$('eaSoul').value=a.soul||'';
|
|
986
|
+
if($('eaIdentity'))$('eaIdentity').value=a.identity||'';
|
|
987
|
+
if($('eaUser'))$('eaUser').value=a.user||'';
|
|
981
988
|
}
|
|
982
989
|
|
|
983
990
|
function agentTabSwitch(el,tabId){
|
|
@@ -1001,7 +1008,10 @@ async function doSaveAgent(path){
|
|
|
1001
1008
|
avatar_image:$('eaAvatarImage')?.value||''
|
|
1002
1009
|
})});
|
|
1003
1010
|
if(r.error){showToast(r.error,'danger');return}
|
|
1004
|
-
showToast('已保存','success');
|
|
1011
|
+
showToast('已保存','success');
|
|
1012
|
+
// [v1.20.2] 如果 agent 被重命名,更新当前编辑路径,防止后续保存出现 not found
|
|
1013
|
+
if(r.renamed_to){window._currentEditAgentPath=r.renamed_to}
|
|
1014
|
+
renderAgents();
|
|
1005
1015
|
}
|
|
1006
1016
|
|
|
1007
1017
|
async function doSaveAgentSettings(path){
|
|
@@ -1012,9 +1022,9 @@ async function doSaveAgentSettings(path){
|
|
|
1012
1022
|
showToast('设置已保存','success');
|
|
1013
1023
|
}
|
|
1014
1024
|
|
|
1015
|
-
async function doSaveSoul(path){await api(`/api/agents/${encodeURIComponent(path)}/soul`,{method:'PUT',body:JSON.stringify({soul:$('eaSoul').value})});showToast('Soul.md 已保存','success')}
|
|
1016
|
-
async function doSaveIdentity(path){await api(`/api/agents/${encodeURIComponent(path)}/identity`,{method:'PUT',body:JSON.stringify({identity:$('eaIdentity').value})});showToast('Identity.md 已保存','success')}
|
|
1017
|
-
async function doSaveUser(path){await api(`/api/agents/${encodeURIComponent(path)}/user`,{method:'PUT',body:JSON.stringify({user:$('eaUser').value})});showToast('User.md 已保存','success')}
|
|
1025
|
+
async function doSaveSoul(path){const r=await api(`/api/agents/${encodeURIComponent(path)}/soul`,{method:'PUT',body:JSON.stringify({soul:$('eaSoul').value})});if(r.error){showToast(r.error,'danger');return}showToast('Soul.md 已保存','success')}
|
|
1026
|
+
async function doSaveIdentity(path){const r=await api(`/api/agents/${encodeURIComponent(path)}/identity`,{method:'PUT',body:JSON.stringify({identity:$('eaIdentity').value})});if(r.error){showToast(r.error,'danger');return}showToast('Identity.md 已保存','success')}
|
|
1027
|
+
async function doSaveUser(path){const r=await api(`/api/agents/${encodeURIComponent(path)}/user`,{method:'PUT',body:JSON.stringify({user:$('eaUser').value})});if(r.error){showToast(r.error,'danger');return}showToast('User.md 已保存','success')}
|
|
1018
1028
|
|
|
1019
1029
|
async function loadAgentKB(){
|
|
1020
1030
|
// Find current agent path from the modal title or store it
|
|
@@ -1223,66 +1233,163 @@ function chatWithAgent(path){
|
|
|
1223
1233
|
async function renderPlatforms(){
|
|
1224
1234
|
const ps=await api('/api/platforms');
|
|
1225
1235
|
if(ps.error){$('content').innerHTML='<div class="empty" style="color:var(--danger)">加载失败: '+escHtml(ps.error)+'</div>';return}
|
|
1226
|
-
const icons={telegram:'📱',discord:'🎮',feishu:'🐦',qq:'🐧',wechat:'💚'};
|
|
1236
|
+
const icons={telegram:'📱',discord:'🎮',feishu:'🐦',qq:'🐧',wechat:'💚',whatsapp:'💬'};
|
|
1227
1237
|
let html=`<div class="flex justify-between items-center mb-16">
|
|
1228
|
-
<div style="color:var(--text2);font-size:13px">共 ${ps.length}
|
|
1238
|
+
<div style="color:var(--text2);font-size:13px">共 ${ps.length} 个平台实例</div>
|
|
1229
1239
|
<button class="btn btn-primary" onclick="showAddPlatformModal()">+ 添加平台</button></div>`;
|
|
1230
1240
|
if(!ps.length){html+='<div class="empty">暂无聊天平台配置,点击上方按钮添加</div>';$('content').innerHTML=html;return}
|
|
1231
1241
|
html+='<div class="grid">';
|
|
1232
1242
|
for(const p of ps){
|
|
1233
|
-
const
|
|
1243
|
+
const pid=p.id||p.platform;
|
|
1244
|
+
const displayName=p.display_name||(icons[p.platform]||'📡')+' '+p.platform;
|
|
1245
|
+
const bindInfo=p.bind_agent?'绑定: '+escHtml(p.bind_agent):(p.bind_agents&&p.bind_agents.length?'绑定: '+p.bind_agents.map(a=>escHtml(a)).join(', '):'');
|
|
1234
1246
|
html+=`<div class="card"><div class="flex justify-between items-center">
|
|
1235
|
-
<h3 style="color:var(--text)">${icons[
|
|
1247
|
+
<h3 style="color:var(--text)">${icons[p.platform]||'📡'} ${escHtml(p.display_name||p.platform)}</h3>
|
|
1236
1248
|
<span class="badge ${p.enabled?'badge-green':'badge-red'}">${p.enabled?'已启用':'未启用'}</span>
|
|
1237
1249
|
</div><p style="font-size:13px;color:var(--text2);margin-top:8px">Token: ${p.token?'已配置':'未配置'} ${p.app_id?'· App ID: '+escHtml(p.app_id):''}</p>
|
|
1250
|
+
${bindInfo?'<p style="font-size:12px;color:var(--text3);margin-top:4px">'+bindInfo+'</p>':''}
|
|
1238
1251
|
<div class="mt-8 flex gap-8">
|
|
1239
|
-
<button class="btn btn-sm ${p.enabled?'btn-danger':'btn-success'}" onclick="togglePlatform('${escHtml(
|
|
1240
|
-
<button class="btn btn-sm btn-ghost" onclick="showEditPlatformModal('${escHtml(
|
|
1252
|
+
<button class="btn btn-sm ${p.enabled?'btn-danger':'btn-success'}" onclick="togglePlatform('${escHtml(pid)}',${!p.enabled})">${p.enabled?'停用':'启用'}</button>
|
|
1253
|
+
<button class="btn btn-sm btn-ghost" onclick="showEditPlatformModal('${escHtml(pid)}')">配置</button>
|
|
1241
1254
|
</div></div>`;
|
|
1242
1255
|
}
|
|
1243
1256
|
html+='</div>';$('content').innerHTML=html;
|
|
1244
1257
|
}
|
|
1245
|
-
async function togglePlatform(
|
|
1258
|
+
async function togglePlatform(id,enable){await api(`/api/platforms/${id}`,{method:'PUT',body:JSON.stringify({enabled})});renderPlatforms()}
|
|
1246
1259
|
function showAddPlatformModal(){
|
|
1247
|
-
$('modalContainer').innerHTML=`<div class="modal-overlay" onclick="closeModal()"><div class="modal" onclick="event.stopPropagation()">
|
|
1260
|
+
$('modalContainer').innerHTML=`<div class="modal-overlay" onclick="closeModal()"><div class="modal" onclick="event.stopPropagation()" style="max-width:520px">
|
|
1248
1261
|
<h3>+ 添加聊天平台</h3>
|
|
1249
|
-
<div class="form-group"><label>平台类型</label><select id="apType">
|
|
1262
|
+
<div class="form-group"><label>平台类型</label><select id="apType" onchange="onPlatformTypeChange()">
|
|
1250
1263
|
<option value="telegram">Telegram</option><option value="discord">Discord</option>
|
|
1251
1264
|
<option value="feishu">飞书</option><option value="qq">QQ</option><option value="wechat">微信</option>
|
|
1265
|
+
<option value="whatsapp">WhatsApp</option>
|
|
1252
1266
|
</select></div>
|
|
1253
|
-
<div class="form-group"><label>Token</label><input id="apToken" placeholder="Bot Token"></div>
|
|
1254
|
-
<div class="form-group"><label>App ID</label><input id="apAppId" placeholder="应用ID (可选)"></div>
|
|
1255
|
-
<div class="form-group"><label>Webhook URL</label><input id="apWebhook" placeholder="Webhook 地址 (可选)"></div>
|
|
1267
|
+
<div class="form-group"><label>Token / Access Token</label><input id="apToken" placeholder="Bot Token 或 Access Token"></div>
|
|
1268
|
+
<div id="apAppIdGroup" class="form-group"><label>App ID</label><input id="apAppId" placeholder="应用ID (可选)"></div>
|
|
1269
|
+
<div id="apWebhookGroup" class="form-group"><label>Webhook URL</label><input id="apWebhook" placeholder="Webhook 地址 (可选)"></div>
|
|
1270
|
+
<div class="form-group"><label>绑定 Agent</label>
|
|
1271
|
+
<div class="flex gap-8">
|
|
1272
|
+
<input id="apBindAgent" placeholder="手输 Agent ID (可选)" style="flex:1">
|
|
1273
|
+
<button class="btn btn-sm btn-ghost" onclick="showAgentSelectorForPlatform('apBindAgent','apBindAgents')">选择 Agent</button>
|
|
1274
|
+
</div>
|
|
1275
|
+
<div id="apBindAgents" style="font-size:11px;color:var(--text3);margin-top:4px"></div>
|
|
1276
|
+
<div class="hint">手输 Agent ID 或点击"选择 Agent"从部门中选择。支持绑定多个 Agent。</div>
|
|
1277
|
+
</div>
|
|
1256
1278
|
<div class="flex gap-8 mt-16"><button class="btn btn-primary" onclick="doAddPlatform()">添加</button><button class="btn btn-ghost" onclick="closeModal()">取消</button></div>
|
|
1257
1279
|
</div></div>`;
|
|
1258
1280
|
}
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1281
|
+
function onPlatformTypeChange(){
|
|
1282
|
+
var t=$('apType').value;
|
|
1283
|
+
var showAppId=(t==='feishu'||t==='whatsapp');
|
|
1284
|
+
var showWebhook=(t==='whatsapp');
|
|
1285
|
+
if($('apAppIdGroup'))$('apAppIdGroup').style.display=showAppId?'':'none';
|
|
1286
|
+
if($('apWebhookGroup'))$('apWebhookGroup').style.display=showWebhook?'':'none';
|
|
1262
1287
|
}
|
|
1263
|
-
async function
|
|
1264
|
-
|
|
1288
|
+
async function doAddPlatform(){
|
|
1289
|
+
var bindAgent=$('apBindAgent').value.trim();
|
|
1290
|
+
var bindAgents=[];
|
|
1291
|
+
var selectedEls=document.querySelectorAll('#apBindAgents .selected-agent-tag');
|
|
1292
|
+
selectedEls.forEach(function(el){var v=el.getAttribute('data-agent');if(v)bindAgents.push(v);});
|
|
1293
|
+
if(bindAgent)bindAgents.unshift(bindAgent);
|
|
1294
|
+
bindAgents=[...new Set(bindAgents)];
|
|
1295
|
+
const r=await api('/api/platforms',{method:'POST',body:JSON.stringify({platform:$('apType').value,token:$('apToken').value,app_id:$('apAppId').value,webhook_url:$('apWebhook').value,bind_agent:bindAgents[0]||'',bind_agents:bindAgents})});
|
|
1296
|
+
if(r.error){showToast(r.error,'danger');return}closeModal();showToast('已添加: '+(r.display_name||r.platform),'success');renderPlatforms();
|
|
1297
|
+
}
|
|
1298
|
+
async function showEditPlatformModal(id){
|
|
1299
|
+
const p=await api(`/api/platforms/${id}`);
|
|
1265
1300
|
if(p.error){showToast(p.error,'danger');return}
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1301
|
+
const pid=p.id||id;
|
|
1302
|
+
var bindAgentVal=p.bind_agent||'';
|
|
1303
|
+
var bindAgentsArr=p.bind_agents||[];
|
|
1304
|
+
$('modalContainer').innerHTML=`<div class="modal-overlay" onclick="closeModal()"><div class="modal" onclick="event.stopPropagation()" style="max-width:520px">
|
|
1305
|
+
<h3>配置 ${escHtml(p.display_name||p.platform)}</h3>
|
|
1306
|
+
<div class="form-group"><label>显示名称</label><input id="epDisplayName" value="${escHtml(p.display_name||'')}" placeholder="留空自动生成"></div>
|
|
1307
|
+
<div class="form-group"><label>Token / Access Token</label><input id="epToken" value="${escHtml(p.token||'')}" placeholder="Bot Token"></div>
|
|
1308
|
+
<div id="epAppIdGroup" class="form-group"><label>App ID</label><input id="epAppId" value="${escHtml(p.app_id||'')}" placeholder="应用ID"></div>
|
|
1270
1309
|
<div class="form-group"><label>App Secret</label><input id="epSecret" type="password" placeholder="${p.has_secret?'已配置(留空不修改)':'未配置'}"></div>
|
|
1271
|
-
<div class="form-group"><label>Webhook URL</label><input id="epWebhook" value="${escHtml(p.webhook_url||'')}" placeholder="Webhook 地址"></div>
|
|
1310
|
+
<div id="epWebhookGroup" class="form-group"><label>Webhook URL</label><input id="epWebhook" value="${escHtml(p.webhook_url||'')}" placeholder="Webhook 地址"></div>
|
|
1272
1311
|
<div class="form-group"><label>允许用户 (逗号分隔)</label><input id="epUsers" value="${escHtml((p.allowed_users||[]).join(','))}" placeholder="user1,user2"></div>
|
|
1273
|
-
<div class="
|
|
1312
|
+
<div class="form-group"><label>绑定 Agent</label>
|
|
1313
|
+
<div class="flex gap-8">
|
|
1314
|
+
<input id="epBindAgent" value="${escHtml(bindAgentVal)}" placeholder="手输 Agent ID (可选)" style="flex:1">
|
|
1315
|
+
<button class="btn btn-sm btn-ghost" onclick="showAgentSelectorForPlatform('epBindAgent','epBindAgents')">选择 Agent</button>
|
|
1316
|
+
</div>
|
|
1317
|
+
<div id="epBindAgents" style="font-size:11px;color:var(--text3);margin-top:4px">${bindAgentsArr.map(function(a){return '<span class="selected-agent-tag" data-agent="'+escHtml(a)+'" style="display:inline-block;background:var(--bg);border:1px solid var(--border);border-radius:4px;padding:1px 6px;margin:2px;font-size:11px;cursor:pointer" onclick="this.remove()" title="点击移除">'+escHtml(a)+' ×</span>';}).join('')}</div>
|
|
1318
|
+
<div class="hint">手输 Agent ID 或点击"选择 Agent"从部门中选择。一个平台可以绑定多个 Agent。</div>
|
|
1319
|
+
</div>
|
|
1320
|
+
<div class="flex gap-8 mt-16"><button class="btn btn-primary" onclick="doSavePlatform('${escHtml(pid)}')">保存</button><button class="btn btn-danger" onclick="doDeletePlatform('${escHtml(pid)}')">删除平台</button><button class="btn btn-ghost" onclick="closeModal()">关闭</button></div>
|
|
1274
1321
|
</div></div>`;
|
|
1275
1322
|
}
|
|
1276
|
-
async function doSavePlatform(
|
|
1277
|
-
|
|
1323
|
+
async function doSavePlatform(id){
|
|
1324
|
+
var bindAgent=$('epBindAgent').value.trim();
|
|
1325
|
+
var bindAgents=[];
|
|
1326
|
+
var selectedEls=document.querySelectorAll('#epBindAgents .selected-agent-tag');
|
|
1327
|
+
selectedEls.forEach(function(el){var v=el.getAttribute('data-agent');if(v)bindAgents.push(v);});
|
|
1328
|
+
if(bindAgent)bindAgents.unshift(bindAgent);
|
|
1329
|
+
bindAgents=[...new Set(bindAgents)];
|
|
1330
|
+
const r=await api(`/api/platforms/${id}`,{method:'PUT',body:JSON.stringify({display_name:$('epDisplayName').value,token:$('epToken').value,app_id:$('epAppId').value,app_secret:$('epSecret').value||undefined,webhook_url:$('epWebhook').value,allowed_users:$('epUsers').value.split(',').map(s=>s.trim()).filter(Boolean),bind_agent:bindAgents[0]||'',bind_agents:bindAgents})});
|
|
1278
1331
|
if(r.error){showToast(r.error,'danger');return}closeModal();showToast('已保存','success');renderPlatforms();
|
|
1279
1332
|
}
|
|
1280
|
-
async function doDeletePlatform(
|
|
1281
|
-
showConfirm('删除平台','
|
|
1282
|
-
const r=await api(`/api/platforms/${
|
|
1333
|
+
async function doDeletePlatform(id){
|
|
1334
|
+
showConfirm('删除平台','确认删除该平台实例吗?',async()=>{
|
|
1335
|
+
const r=await api(`/api/platforms/${id}`,{method:'DELETE'});if(r.error){showToast(r.error,'danger');closeModal();return}closeModal();showToast('已删除','success');renderPlatforms();
|
|
1283
1336
|
});
|
|
1284
1337
|
}
|
|
1285
1338
|
|
|
1339
|
+
// Agent 选择器(从部门树选择 Agent)
|
|
1340
|
+
async function showAgentSelectorForPlatform(inputId,containerId){
|
|
1341
|
+
try{
|
|
1342
|
+
var agents=await api('/api/agents');
|
|
1343
|
+
if(!Array.isArray(agents)){showToast('加载 Agent 列表失败','danger');return}
|
|
1344
|
+
var agentList=agents.filter(function(a){return!a.system&&a.path!=='default'});
|
|
1345
|
+
var deptMap={};
|
|
1346
|
+
agentList.forEach(function(a){
|
|
1347
|
+
var dept=a.department||'未分组';
|
|
1348
|
+
if(!deptMap[dept])deptMap[dept]=[];
|
|
1349
|
+
deptMap[dept].push(a);
|
|
1350
|
+
});
|
|
1351
|
+
var html='<div class="modal-overlay" onclick="closeModal()"><div class="modal" onclick="event.stopPropagation()" style="max-width:480px;max-height:70vh;overflow-y:auto">';
|
|
1352
|
+
html+='<h3>选择 Agent</h3>';
|
|
1353
|
+
for(var dept in deptMap){
|
|
1354
|
+
html+='<div style="margin-bottom:12px"><div style="font-size:13px;font-weight:600;color:var(--text2);margin-bottom:4px">📁 '+escHtml(dept)+'</div>';
|
|
1355
|
+
deptMap[dept].forEach(function(a){
|
|
1356
|
+
var selected=false;
|
|
1357
|
+
var existing=document.querySelectorAll('#'+containerId+' .selected-agent-tag');
|
|
1358
|
+
existing.forEach(function(el){if(el.getAttribute('data-agent')===a.path)selected=true;});
|
|
1359
|
+
html+='<label style="display:flex;align-items:center;gap:8px;padding:4px 0;cursor:pointer;font-size:13px"><input type="checkbox" value="'+escHtml(a.path)+'" '+(selected?'checked':'')+' onchange="toggleAgentSelection(this,\''+inputId+'\',\''+containerId+'\')"> '+(a.avatar_emoji||'🤖')+' '+escHtml(a.name)+' <span style="color:var(--text3);font-size:11px">('+escHtml(a.path)+')</span></label>';
|
|
1360
|
+
});
|
|
1361
|
+
html+='</div>';
|
|
1362
|
+
}
|
|
1363
|
+
html+='<div class="flex gap-8 mt-16"><button class="btn btn-primary" onclick="closeModal()">确定</button></div></div></div>';
|
|
1364
|
+
$('modalContainer').innerHTML=html;
|
|
1365
|
+
}catch(e){showToast('加载失败: '+e.message,'danger')}
|
|
1366
|
+
}
|
|
1367
|
+
function toggleAgentSelection(cb,inputId,containerId){
|
|
1368
|
+
var agentPath=cb.value;
|
|
1369
|
+
var container=document.getElementById(containerId);
|
|
1370
|
+
if(cb.checked){
|
|
1371
|
+
// 添加 tag
|
|
1372
|
+
if(!container.querySelector('[data-agent="'+agentPath+'"]')){
|
|
1373
|
+
var tag=document.createElement('span');
|
|
1374
|
+
tag.className='selected-agent-tag';
|
|
1375
|
+
tag.setAttribute('data-agent',agentPath);
|
|
1376
|
+
tag.style.cssText='display:inline-block;background:var(--bg);border:1px solid var(--border);border-radius:4px;padding:1px 6px;margin:2px;font-size:11px;cursor:pointer';
|
|
1377
|
+
tag.title='点击移除';
|
|
1378
|
+
tag.textContent=agentPath+' ×';
|
|
1379
|
+
tag.onclick=function(){this.remove();};
|
|
1380
|
+
container.appendChild(tag);
|
|
1381
|
+
}
|
|
1382
|
+
// 同步到输入框
|
|
1383
|
+
var first=container.querySelector('.selected-agent-tag');
|
|
1384
|
+
if(first)document.getElementById(inputId).value=first.getAttribute('data-agent');
|
|
1385
|
+
}else{
|
|
1386
|
+
var existing=container.querySelector('[data-agent="'+agentPath+'"]');
|
|
1387
|
+
if(existing)existing.remove();
|
|
1388
|
+
var first=container.querySelector('.selected-agent-tag');
|
|
1389
|
+
document.getElementById(inputId).value=first?first.getAttribute('data-agent'):'';
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1286
1393
|
// ========== Sessions ==========
|
|
1287
1394
|
async function renderSessions(){
|
|
1288
1395
|
const ss=await api('/api/sessions');
|
|
@@ -2004,19 +2111,22 @@ async function viewSkillDetail(name){
|
|
|
2004
2111
|
// ========== Files ==========
|
|
2005
2112
|
var _workdirAgent=''; // 当前选中的 agent(空=全局工作目录)
|
|
2006
2113
|
async function renderFiles(){
|
|
2007
|
-
|
|
2114
|
+
try{
|
|
2115
|
+
const [wd,agents]=await Promise.all([api('/api/workdir').catch(()=>({})),api('/api/agents').catch(()=>[])]);
|
|
2008
2116
|
const agentList=Array.isArray(agents)?agents.filter(a=>!a.system&&a.path!=='default'):[];
|
|
2117
|
+
const wdPath=wd&&wd.path?wd.path:'';
|
|
2009
2118
|
let html=`<div class="flex items-center gap-8 mb-16 flex-wrap">
|
|
2010
|
-
<span style="font-size:14px;color:var(--text2)">📁 工作目录: <strong>${escHtml(
|
|
2119
|
+
<span style="font-size:14px;color:var(--text2)">📁 工作目录: <strong>${escHtml(wdPath)}</strong></span>
|
|
2011
2120
|
<select id="workdirAgentSelect" onchange="onWorkdirAgentChange()" style="width:auto">
|
|
2012
2121
|
<option value="">全局工作目录</option>
|
|
2013
|
-
${agentList.map(a=>`<option value="${escHtml(a.path)}">${escHtml((a.avatar_emoji||'🤖')+' '+a.name)} (${escHtml(a.path)})</option>`).join('')}
|
|
2122
|
+
${agentList.map(a=>`<option value="${escHtml(a.path)}" ${_workdirAgent===a.path?'selected':''}>${escHtml((a.avatar_emoji||'🤖')+' '+a.name)} (${escHtml(a.path)})</option>`).join('')}
|
|
2014
2123
|
</select>
|
|
2015
2124
|
<button class="btn btn-sm btn-ghost" onclick="changeWorkdir()">更改全局</button>
|
|
2016
2125
|
<button class="btn btn-sm btn-ghost" onclick="renderFiles()">🔄 刷新</button></div>
|
|
2017
2126
|
<div id="workdirContent">加载中...</div>`;
|
|
2018
2127
|
$('content').innerHTML=html;
|
|
2019
2128
|
loadWorkdirContent('');
|
|
2129
|
+
}catch(e){console.error('renderFiles error:',e);$('content').innerHTML='<div class="empty" style="color:var(--danger)">加载失败: '+escHtml(e.message||'未知错误')+'</div>';}
|
|
2020
2130
|
}
|
|
2021
2131
|
function onWorkdirAgentChange(){
|
|
2022
2132
|
_workdirAgent=$('workdirAgentSelect').value;
|