myagent-ai 1.20.1 → 1.20.3
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 +99 -0
- 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 +43 -0
- package/web/ui/chat/chat_main.js +124 -14
- package/web/ui/chat/flow_engine.js +38 -7
- package/web/ui/index.html +151 -41
package/agents/main_agent.py
CHANGED
|
@@ -74,6 +74,8 @@ class MainAgent(BaseAgent):
|
|
|
74
74
|
- **执行命令**: 用 `command` 或 `command_run` 工具
|
|
75
75
|
- **文件操作**: 用 `file_read` / `file_write` / `file_list` 等文件工具
|
|
76
76
|
- **发送文件给用户**: 用 `file_send` 工具(参数: file_path=文件路径, description=说明),当你生成或处理了文件需要返回给用户时使用
|
|
77
|
+
- **播放音频**: 用 `playaudio` 工具(参数: url=音乐链接或file_path=本地文件路径),在聊天中内嵌播放音频(支持QQ音乐、YouTube音乐、本地MP3/WAV等),播放时自动关闭语音合成
|
|
78
|
+
- **播放视频**: 用 `playvideo` 工具(参数: url=视频链接或file_path=本地文件路径),在聊天中内嵌播放视频(支持抖音、YouTube、B站、本地MP4等),播放时自动关闭语音合成
|
|
77
79
|
- **主动召回记忆**: 用 `recall_memory` 工具(参数: keyword=关键字, time_point=可选时间点如"2025-01", limit=数量默认5),根据关键字和时间搜索历史记忆
|
|
78
80
|
4. 准备好内容后,最后,再检查输出格式,确保满足以下要求:
|
|
79
81
|
<output>
|
|
@@ -1648,6 +1650,103 @@ class MainAgent(BaseAgent):
|
|
|
1648
1650
|
result = {"success": False, "error": f"文件发送失败: {_fse}"}
|
|
1649
1651
|
logger.warning(f"[{task_id}] file_send 工具异常: {_fse}")
|
|
1650
1652
|
|
|
1653
|
+
elif tool_name in ("playaudio", "playvideo"):
|
|
1654
|
+
# [v1.20.3] 音视频播放工具 — 在聊天中内嵌播放器
|
|
1655
|
+
try:
|
|
1656
|
+
_media_url = params.get("url", "").strip()
|
|
1657
|
+
_media_file = params.get("file_path", "").strip()
|
|
1658
|
+
_media_type = "audio" if tool_name == "playaudio" else "video"
|
|
1659
|
+
_embed_url = None
|
|
1660
|
+
_embed_title = params.get("title", "")
|
|
1661
|
+
|
|
1662
|
+
if _media_url:
|
|
1663
|
+
# 在线链接 — 提取嵌入式播放 URL
|
|
1664
|
+
import re
|
|
1665
|
+
_url_lower = _media_url.lower()
|
|
1666
|
+
# YouTube: https://www.youtube.com/watch?v=xxx 或 youtu.be/xxx
|
|
1667
|
+
_yt_match = re.search(r'(?:youtube\.com/watch\?v=|youtu\.be/|youtube\.com/embed/)([\w-]+)', _media_url)
|
|
1668
|
+
if _yt_match:
|
|
1669
|
+
_embed_url = f"https://www.youtube.com/embed/{_yt_match.group(1)}"
|
|
1670
|
+
_embed_title = _embed_title or "YouTube 视频"
|
|
1671
|
+
# Bilibili: https://www.bilibili.com/video/BVxxx 或 b23.tv/xxx
|
|
1672
|
+
elif 'bilibili.com' in _url_lower or 'b23.tv' in _url_lower:
|
|
1673
|
+
_bv_match = re.search(r'bilibili\.com/video/(BV[\w]+)', _media_url)
|
|
1674
|
+
if _bv_match:
|
|
1675
|
+
_embed_url = f"https://player.bilibili.com/player.html?bvid={_bv_match.group(1)}&autoplay=0"
|
|
1676
|
+
_embed_title = _embed_title or "B站视频"
|
|
1677
|
+
else:
|
|
1678
|
+
_embed_url = _media_url # b23.tv 短链接直接使用
|
|
1679
|
+
_embed_title = _embed_title or "B站视频"
|
|
1680
|
+
# QQ音乐: https://y.qq.com/n/ryqq/songDetail/xxx
|
|
1681
|
+
elif 'y.qq.com' in _url_lower:
|
|
1682
|
+
_embed_url = _media_url
|
|
1683
|
+
_embed_title = _embed_title or "QQ音乐"
|
|
1684
|
+
# 网易云音乐: https://music.163.com/song?id=xxx
|
|
1685
|
+
elif 'music.163.com' in _url_lower:
|
|
1686
|
+
_song_match = re.search(r'music\.163\.com.*[?&]id=(\d+)', _media_url)
|
|
1687
|
+
if _song_match:
|
|
1688
|
+
_embed_url = f"https://music.163.com/outchain/player?type=2&id={_song_match.group(1)}&auto=0&height=66"
|
|
1689
|
+
_embed_title = _embed_title or "网易云音乐"
|
|
1690
|
+
else:
|
|
1691
|
+
_embed_url = _media_url
|
|
1692
|
+
_embed_title = _embed_title or "网易云音乐"
|
|
1693
|
+
# 抖音: https://www.douyin.com/video/xxx
|
|
1694
|
+
elif 'douyin.com' in _url_lower:
|
|
1695
|
+
_embed_url = _media_url
|
|
1696
|
+
_embed_title = _embed_title or "抖音视频"
|
|
1697
|
+
else:
|
|
1698
|
+
# 其他 URL,尝试直接嵌入
|
|
1699
|
+
_embed_url = _media_url
|
|
1700
|
+
_embed_title = _embed_title or ("在线音乐" if _media_type == "audio" else "在线视频")
|
|
1701
|
+
|
|
1702
|
+
if _embed_url and stream_callback:
|
|
1703
|
+
# 在线播放 — 发送 v2_media 事件让前端渲染嵌入播放器
|
|
1704
|
+
stream_callback({
|
|
1705
|
+
"type": "v2_media",
|
|
1706
|
+
"data": {
|
|
1707
|
+
"media_type": _media_type,
|
|
1708
|
+
"embed_url": _embed_url,
|
|
1709
|
+
"title": _embed_title,
|
|
1710
|
+
"original_url": _media_url,
|
|
1711
|
+
}
|
|
1712
|
+
})
|
|
1713
|
+
result = {"success": True, "output": f"已嵌入{_embed_title}播放器: {_media_url}"}
|
|
1714
|
+
|
|
1715
|
+
elif _media_file:
|
|
1716
|
+
# 本地文件 — 使用 file_send 发送文件,前端渲染内嵌播放器
|
|
1717
|
+
from pathlib import Path as _P
|
|
1718
|
+
_fpath = _P(_media_file).expanduser().resolve()
|
|
1719
|
+
if not _fpath.exists():
|
|
1720
|
+
result = {"success": False, "error": f"文件不存在: {_media_file}"}
|
|
1721
|
+
else:
|
|
1722
|
+
from skills.file_send import FileSendSkill
|
|
1723
|
+
_fskill = FileSendSkill()
|
|
1724
|
+
_desc = f"{'音频' if _media_type == 'audio' else '视频'}播放: {_fpath.name}"
|
|
1725
|
+
_fresult = await _fskill.execute(str(_fpath), _desc, stream_callback=stream_callback)
|
|
1726
|
+
if _fresult.get("success"):
|
|
1727
|
+
# 标记为媒体文件,前端渲染内嵌播放器
|
|
1728
|
+
_fresult["_media_type"] = _media_type
|
|
1729
|
+
result = {"success": True, "output": f"已发送{_media_type}文件: {_fpath.name}", "data": _fresult}
|
|
1730
|
+
try:
|
|
1731
|
+
if _fresult.get("file_id"):
|
|
1732
|
+
_sent_files.append({
|
|
1733
|
+
"id": _fresult["file_id"],
|
|
1734
|
+
"name": _fresult.get("name", ""),
|
|
1735
|
+
"type": _fresult.get("type", ""),
|
|
1736
|
+
"size": _fresult.get("size", 0),
|
|
1737
|
+
"_media_type": _media_type,
|
|
1738
|
+
})
|
|
1739
|
+
except NameError:
|
|
1740
|
+
pass
|
|
1741
|
+
else:
|
|
1742
|
+
result = {"success": False, "error": _fresult.get("error", "文件发送失败")}
|
|
1743
|
+
else:
|
|
1744
|
+
result = {"success": False, "error": f"请提供 url(在线链接)或 file_path(本地文件路径)参数"}
|
|
1745
|
+
|
|
1746
|
+
except Exception as _me:
|
|
1747
|
+
result = {"success": False, "error": f"播放工具异常: {_me}"}
|
|
1748
|
+
logger.warning(f"[{task_id}] {tool_name} 工具异常: {_me}")
|
|
1749
|
+
|
|
1651
1750
|
elif self.skills:
|
|
1652
1751
|
exec_result = await self.skills.execute(tool_name, **params)
|
|
1653
1752
|
if exec_result is None:
|
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.3",
|
|
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
|
+
}
|