AstrBot 4.2.1__py3-none-any.whl → 4.3.1__py3-none-any.whl
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.
- astrbot/core/config/default.py +49 -10
- astrbot/core/db/__init__.py +12 -1
- astrbot/core/db/po.py +8 -4
- astrbot/core/db/sqlite.py +144 -34
- astrbot/core/pipeline/preprocess_stage/stage.py +24 -0
- astrbot/core/pipeline/result_decorate/stage.py +5 -1
- astrbot/core/platform/astr_message_event.py +10 -0
- astrbot/core/platform/platform_metadata.py +2 -0
- astrbot/core/platform/register.py +3 -0
- astrbot/core/platform/sources/lark/lark_event.py +16 -0
- astrbot/core/platform/sources/telegram/tg_adapter.py +59 -23
- astrbot/core/platform/sources/telegram/tg_event.py +34 -0
- astrbot/core/provider/provider.py +1 -1
- astrbot/core/provider/sources/zhipu_source.py +5 -65
- astrbot/core/star/filter/command.py +32 -2
- astrbot/core/star/register/star_handler.py +6 -4
- astrbot/core/utils/command_parser.py +1 -1
- astrbot/dashboard/routes/config.py +88 -2
- astrbot/dashboard/routes/session_management.py +37 -29
- {astrbot-4.2.1.dist-info → astrbot-4.3.1.dist-info}/METADATA +3 -3
- {astrbot-4.2.1.dist-info → astrbot-4.3.1.dist-info}/RECORD +24 -24
- {astrbot-4.2.1.dist-info → astrbot-4.3.1.dist-info}/WHEEL +0 -0
- {astrbot-4.2.1.dist-info → astrbot-4.3.1.dist-info}/entry_points.txt +0 -0
- {astrbot-4.2.1.dist-info → astrbot-4.3.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -95,9 +95,8 @@ class TelegramPlatformAdapter(Platform):
|
|
|
95
95
|
|
|
96
96
|
@override
|
|
97
97
|
def meta(self) -> PlatformMetadata:
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
)
|
|
98
|
+
id_ = self.config.get("id") or "telegram"
|
|
99
|
+
return PlatformMetadata(name="telegram", description="telegram 适配器", id=id_)
|
|
101
100
|
|
|
102
101
|
@override
|
|
103
102
|
async def run(self):
|
|
@@ -117,6 +116,10 @@ class TelegramPlatformAdapter(Platform):
|
|
|
117
116
|
)
|
|
118
117
|
self.scheduler.start()
|
|
119
118
|
|
|
119
|
+
if not self.application.updater:
|
|
120
|
+
logger.error("Telegram Updater is not initialized. Cannot start polling.")
|
|
121
|
+
return
|
|
122
|
+
|
|
120
123
|
queue = self.application.updater.start_polling()
|
|
121
124
|
logger.info("Telegram Platform Adapter is running.")
|
|
122
125
|
await queue
|
|
@@ -194,6 +197,11 @@ class TelegramPlatformAdapter(Platform):
|
|
|
194
197
|
return cmd_name, description
|
|
195
198
|
|
|
196
199
|
async def start(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
200
|
+
if not update.effective_chat:
|
|
201
|
+
logger.warning(
|
|
202
|
+
"Received a start command without an effective chat, skipping /start reply."
|
|
203
|
+
)
|
|
204
|
+
return
|
|
197
205
|
await context.bot.send_message(
|
|
198
206
|
chat_id=update.effective_chat.id, text=self.config["start_message"]
|
|
199
207
|
)
|
|
@@ -206,15 +214,20 @@ class TelegramPlatformAdapter(Platform):
|
|
|
206
214
|
|
|
207
215
|
async def convert_message(
|
|
208
216
|
self, update: Update, context: ContextTypes.DEFAULT_TYPE, get_reply=True
|
|
209
|
-
) -> AstrBotMessage:
|
|
217
|
+
) -> AstrBotMessage | None:
|
|
210
218
|
"""转换 Telegram 的消息对象为 AstrBotMessage 对象。
|
|
211
219
|
|
|
212
220
|
@param update: Telegram 的 Update 对象。
|
|
213
221
|
@param context: Telegram 的 Context 对象。
|
|
214
222
|
@param get_reply: 是否获取回复消息。这个参数是为了防止多个回复嵌套。
|
|
215
223
|
"""
|
|
224
|
+
if not update.message:
|
|
225
|
+
logger.warning("Received an update without a message.")
|
|
226
|
+
return None
|
|
227
|
+
|
|
216
228
|
message = AstrBotMessage()
|
|
217
229
|
message.session_id = str(update.message.chat.id)
|
|
230
|
+
|
|
218
231
|
# 获得是群聊还是私聊
|
|
219
232
|
if update.message.chat.type == ChatType.PRIVATE:
|
|
220
233
|
message.type = MessageType.FRIEND_MESSAGE
|
|
@@ -225,10 +238,13 @@ class TelegramPlatformAdapter(Platform):
|
|
|
225
238
|
# Topic Group
|
|
226
239
|
message.group_id += "#" + str(update.message.message_thread_id)
|
|
227
240
|
message.session_id = message.group_id
|
|
228
|
-
|
|
229
241
|
message.message_id = str(update.message.message_id)
|
|
242
|
+
_from_user = update.message.from_user
|
|
243
|
+
if not _from_user:
|
|
244
|
+
logger.warning("[Telegram] Received a message without a from_user.")
|
|
245
|
+
return None
|
|
230
246
|
message.sender = MessageMember(
|
|
231
|
-
str(
|
|
247
|
+
str(_from_user.id), _from_user.username or "Unknown"
|
|
232
248
|
)
|
|
233
249
|
message.self_id = str(context.bot.username)
|
|
234
250
|
message.raw_message = update
|
|
@@ -247,22 +263,32 @@ class TelegramPlatformAdapter(Platform):
|
|
|
247
263
|
)
|
|
248
264
|
reply_abm = await self.convert_message(reply_update, context, False)
|
|
249
265
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
266
|
+
if reply_abm:
|
|
267
|
+
message.message.append(
|
|
268
|
+
Comp.Reply(
|
|
269
|
+
id=reply_abm.message_id,
|
|
270
|
+
chain=reply_abm.message,
|
|
271
|
+
sender_id=reply_abm.sender.user_id,
|
|
272
|
+
sender_nickname=reply_abm.sender.nickname,
|
|
273
|
+
time=reply_abm.timestamp,
|
|
274
|
+
message_str=reply_abm.message_str,
|
|
275
|
+
text=reply_abm.message_str,
|
|
276
|
+
qq=reply_abm.sender.user_id,
|
|
277
|
+
)
|
|
260
278
|
)
|
|
261
|
-
)
|
|
262
279
|
|
|
263
280
|
if update.message.text:
|
|
264
281
|
# 处理文本消息
|
|
265
282
|
plain_text = update.message.text
|
|
283
|
+
if (
|
|
284
|
+
message.type == MessageType.GROUP_MESSAGE
|
|
285
|
+
and update.message
|
|
286
|
+
and update.message.reply_to_message
|
|
287
|
+
and update.message.reply_to_message.from_user
|
|
288
|
+
and update.message.reply_to_message.from_user.id == context.bot.id
|
|
289
|
+
):
|
|
290
|
+
plain_text2 = f"/@{context.bot.username} " + plain_text
|
|
291
|
+
plain_text = plain_text2
|
|
266
292
|
|
|
267
293
|
# 群聊场景命令特殊处理
|
|
268
294
|
if plain_text.startswith("/"):
|
|
@@ -328,15 +354,25 @@ class TelegramPlatformAdapter(Platform):
|
|
|
328
354
|
|
|
329
355
|
elif update.message.document:
|
|
330
356
|
file = await update.message.document.get_file()
|
|
331
|
-
message.
|
|
332
|
-
|
|
333
|
-
|
|
357
|
+
file_name = update.message.document.file_name or uuid.uuid4().hex
|
|
358
|
+
file_path = file.file_path
|
|
359
|
+
if file_path is None:
|
|
360
|
+
logger.warning(
|
|
361
|
+
f"Telegram document file_path is None, cannot save the file {file_name}."
|
|
362
|
+
)
|
|
363
|
+
else:
|
|
364
|
+
message.message.append(Comp.File(file=file_path, name=file_name))
|
|
334
365
|
|
|
335
366
|
elif update.message.video:
|
|
336
367
|
file = await update.message.video.get_file()
|
|
337
|
-
message.
|
|
338
|
-
|
|
339
|
-
|
|
368
|
+
file_name = update.message.video.file_name or uuid.uuid4().hex
|
|
369
|
+
file_path = file.file_path
|
|
370
|
+
if file_path is None:
|
|
371
|
+
logger.warning(
|
|
372
|
+
f"Telegram video file_path is None, cannot save the file {file_name}."
|
|
373
|
+
)
|
|
374
|
+
else:
|
|
375
|
+
message.message.append(Comp.Video(file=file_path, path=file.file_path))
|
|
340
376
|
|
|
341
377
|
return message
|
|
342
378
|
|
|
@@ -16,6 +16,7 @@ from telegram.ext import ExtBot
|
|
|
16
16
|
from astrbot.core.utils.io import download_file
|
|
17
17
|
from astrbot import logger
|
|
18
18
|
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
|
|
19
|
+
from telegram import ReactionTypeEmoji, ReactionTypeCustomEmoji
|
|
19
20
|
|
|
20
21
|
|
|
21
22
|
class TelegramPlatformEvent(AstrMessageEvent):
|
|
@@ -135,6 +136,39 @@ class TelegramPlatformEvent(AstrMessageEvent):
|
|
|
135
136
|
await self.send_with_client(self.client, message, self.get_sender_id())
|
|
136
137
|
await super().send(message)
|
|
137
138
|
|
|
139
|
+
async def react(self, emoji: str | None, big: bool = False):
|
|
140
|
+
"""
|
|
141
|
+
给原消息添加 Telegram 反应:
|
|
142
|
+
- 普通 emoji:传入 '👍'、'😂' 等
|
|
143
|
+
- 自定义表情:传入其 custom_emoji_id(纯数字字符串)
|
|
144
|
+
- 取消本机器人的反应:传入 None 或空字符串
|
|
145
|
+
"""
|
|
146
|
+
try:
|
|
147
|
+
# 解析 chat_id(去掉超级群的 "#<thread_id>" 片段)
|
|
148
|
+
if self.get_message_type() == MessageType.GROUP_MESSAGE:
|
|
149
|
+
chat_id = (self.message_obj.group_id or "").split("#")[0]
|
|
150
|
+
else:
|
|
151
|
+
chat_id = self.get_sender_id()
|
|
152
|
+
|
|
153
|
+
message_id = int(self.message_obj.message_id)
|
|
154
|
+
|
|
155
|
+
# 组装 reaction 参数(必须是 ReactionType 的列表)
|
|
156
|
+
if not emoji: # 清空本 bot 的反应
|
|
157
|
+
reaction_param = [] # 空列表表示移除本 bot 的反应
|
|
158
|
+
elif emoji.isdigit(): # 自定义表情:传 custom_emoji_id
|
|
159
|
+
reaction_param = [ReactionTypeCustomEmoji(emoji)]
|
|
160
|
+
else: # 普通 emoji
|
|
161
|
+
reaction_param = [ReactionTypeEmoji(emoji)]
|
|
162
|
+
|
|
163
|
+
await self.client.set_message_reaction(
|
|
164
|
+
chat_id=chat_id,
|
|
165
|
+
message_id=message_id,
|
|
166
|
+
reaction=reaction_param, # 注意是列表
|
|
167
|
+
is_big=big, # 可选:大动画
|
|
168
|
+
)
|
|
169
|
+
except Exception as e:
|
|
170
|
+
logger.error(f"[Telegram] 添加反应失败: {e}")
|
|
171
|
+
|
|
138
172
|
async def send_streaming(self, generator, use_fallback: bool = False):
|
|
139
173
|
message_thread_id = None
|
|
140
174
|
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
# This file was originally created to adapt to glm-4v-flash, which only supports one image in the context.
|
|
2
|
+
# It is no longer specifically adapted to Zhipu's models. To ensure compatibility, this
|
|
3
|
+
|
|
4
|
+
|
|
4
5
|
from ..register import register_provider_adapter
|
|
5
|
-
from astrbot.core.provider.entities import LLMResponse
|
|
6
6
|
from .openai_source import ProviderOpenAIOfficial
|
|
7
7
|
|
|
8
8
|
|
|
9
|
-
@register_provider_adapter("zhipu_chat_completion", "
|
|
9
|
+
@register_provider_adapter("zhipu_chat_completion", "智谱 Chat Completion 提供商适配器")
|
|
10
10
|
class ProviderZhipu(ProviderOpenAIOfficial):
|
|
11
11
|
def __init__(
|
|
12
12
|
self,
|
|
@@ -19,63 +19,3 @@ class ProviderZhipu(ProviderOpenAIOfficial):
|
|
|
19
19
|
provider_settings,
|
|
20
20
|
default_persona,
|
|
21
21
|
)
|
|
22
|
-
|
|
23
|
-
async def text_chat(
|
|
24
|
-
self,
|
|
25
|
-
prompt: str,
|
|
26
|
-
session_id: str = None,
|
|
27
|
-
image_urls: List[str] = None,
|
|
28
|
-
func_tool: FuncCall = None,
|
|
29
|
-
contexts=None,
|
|
30
|
-
system_prompt=None,
|
|
31
|
-
model=None,
|
|
32
|
-
**kwargs,
|
|
33
|
-
) -> LLMResponse:
|
|
34
|
-
if contexts is None:
|
|
35
|
-
contexts = []
|
|
36
|
-
new_record = await self.assemble_context(prompt, image_urls)
|
|
37
|
-
context_query = []
|
|
38
|
-
|
|
39
|
-
context_query = [*contexts, new_record]
|
|
40
|
-
|
|
41
|
-
model_cfgs: dict = self.provider_config.get("model_config", {})
|
|
42
|
-
model = model or self.get_model()
|
|
43
|
-
# glm-4v-flash 只支持一张图片
|
|
44
|
-
if model.lower() == "glm-4v-flash" and image_urls and len(context_query) > 1:
|
|
45
|
-
logger.debug("glm-4v-flash 只支持一张图片,将只保留最后一张图片")
|
|
46
|
-
logger.debug(context_query)
|
|
47
|
-
new_context_query_ = []
|
|
48
|
-
for i in range(0, len(context_query) - 1, 2):
|
|
49
|
-
if isinstance(context_query[i].get("content", ""), list):
|
|
50
|
-
continue
|
|
51
|
-
new_context_query_.append(context_query[i])
|
|
52
|
-
new_context_query_.append(context_query[i + 1])
|
|
53
|
-
new_context_query_.append(context_query[-1]) # 保留最后一条记录
|
|
54
|
-
context_query = new_context_query_
|
|
55
|
-
logger.debug(context_query)
|
|
56
|
-
|
|
57
|
-
if system_prompt:
|
|
58
|
-
context_query.insert(0, {"role": "system", "content": system_prompt})
|
|
59
|
-
|
|
60
|
-
payloads = {"messages": context_query, **model_cfgs}
|
|
61
|
-
try:
|
|
62
|
-
llm_response = await self._query(payloads, func_tool)
|
|
63
|
-
return llm_response
|
|
64
|
-
except Exception as e:
|
|
65
|
-
if "maximum context length" in str(e):
|
|
66
|
-
retry_cnt = 10
|
|
67
|
-
while retry_cnt > 0:
|
|
68
|
-
logger.warning(
|
|
69
|
-
f"请求失败:{e}。上下文长度超过限制。尝试弹出最早的记录然后重试。"
|
|
70
|
-
)
|
|
71
|
-
try:
|
|
72
|
-
self.pop_record(session_id)
|
|
73
|
-
llm_response = await self._query(payloads, func_tool)
|
|
74
|
-
break
|
|
75
|
-
except Exception as e:
|
|
76
|
-
if "maximum context length" in str(e):
|
|
77
|
-
retry_cnt -= 1
|
|
78
|
-
else:
|
|
79
|
-
raise e
|
|
80
|
-
else:
|
|
81
|
-
raise e
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import re
|
|
2
2
|
import inspect
|
|
3
|
+
import types
|
|
4
|
+
import typing
|
|
3
5
|
from typing import List, Any, Type, Dict
|
|
4
6
|
from . import HandlerFilter
|
|
5
7
|
from astrbot.core.platform.astr_message_event import AstrMessageEvent
|
|
@@ -14,6 +16,18 @@ class GreedyStr(str):
|
|
|
14
16
|
pass
|
|
15
17
|
|
|
16
18
|
|
|
19
|
+
def unwrap_optional(annotation) -> tuple:
|
|
20
|
+
"""去掉 Optional[T] / Union[T, None] / T|None,返回 T"""
|
|
21
|
+
args = typing.get_args(annotation)
|
|
22
|
+
non_none_args = [a for a in args if a is not type(None)]
|
|
23
|
+
if len(non_none_args) == 1:
|
|
24
|
+
return (non_none_args[0],)
|
|
25
|
+
elif len(non_none_args) > 1:
|
|
26
|
+
return tuple(non_none_args)
|
|
27
|
+
else:
|
|
28
|
+
return ()
|
|
29
|
+
|
|
30
|
+
|
|
17
31
|
# 标准指令受到 wake_prefix 的制约。
|
|
18
32
|
class CommandFilter(HandlerFilter):
|
|
19
33
|
"""标准指令过滤器"""
|
|
@@ -40,6 +54,8 @@ class CommandFilter(HandlerFilter):
|
|
|
40
54
|
for k, v in self.handler_params.items():
|
|
41
55
|
if isinstance(v, type):
|
|
42
56
|
result += f"{k}({v.__name__}),"
|
|
57
|
+
elif isinstance(v, types.UnionType) or typing.get_origin(v) is typing.Union:
|
|
58
|
+
result += f"{k}({v}),"
|
|
43
59
|
else:
|
|
44
60
|
result += f"{k}({type(v).__name__})={v},"
|
|
45
61
|
result = result.rstrip(",")
|
|
@@ -95,7 +111,8 @@ class CommandFilter(HandlerFilter):
|
|
|
95
111
|
# 没有 GreedyStr 的情况
|
|
96
112
|
if i >= len(params):
|
|
97
113
|
if (
|
|
98
|
-
isinstance(param_type_or_default_val, Type)
|
|
114
|
+
isinstance(param_type_or_default_val, (Type, types.UnionType))
|
|
115
|
+
or typing.get_origin(param_type_or_default_val) is typing.Union
|
|
99
116
|
or param_type_or_default_val is inspect.Parameter.empty
|
|
100
117
|
):
|
|
101
118
|
# 是类型
|
|
@@ -132,7 +149,20 @@ class CommandFilter(HandlerFilter):
|
|
|
132
149
|
elif isinstance(param_type_or_default_val, float):
|
|
133
150
|
result[param_name] = float(params[i])
|
|
134
151
|
else:
|
|
135
|
-
|
|
152
|
+
origin = typing.get_origin(param_type_or_default_val)
|
|
153
|
+
if origin in (typing.Union, types.UnionType):
|
|
154
|
+
# 注解是联合类型
|
|
155
|
+
# NOTE: 目前没有处理联合类型嵌套相关的注解写法
|
|
156
|
+
nn_types = unwrap_optional(param_type_or_default_val)
|
|
157
|
+
if len(nn_types) == 1:
|
|
158
|
+
# 只有一个非 NoneType 类型
|
|
159
|
+
result[param_name] = nn_types[0](params[i])
|
|
160
|
+
else:
|
|
161
|
+
# 没有或者有多个非 NoneType 类型,这里我们暂时直接赋值为原始值。
|
|
162
|
+
# NOTE: 目前还没有做类型校验
|
|
163
|
+
result[param_name] = params[i]
|
|
164
|
+
else:
|
|
165
|
+
result[param_name] = param_type_or_default_val(params[i])
|
|
136
166
|
except ValueError:
|
|
137
167
|
raise ValueError(
|
|
138
168
|
f"参数 {param_name} 类型错误。完整参数: {self.print_types()}"
|
|
@@ -205,7 +205,6 @@ def register_command_group(
|
|
|
205
205
|
new_group = CommandGroupFilter(command_group_name, alias)
|
|
206
206
|
|
|
207
207
|
def decorator(obj):
|
|
208
|
-
# 根指令组
|
|
209
208
|
if new_group:
|
|
210
209
|
handler_md = get_handler_or_create(
|
|
211
210
|
obj, EventType.AdapterMessageEvent, **kwargs
|
|
@@ -213,6 +212,7 @@ def register_command_group(
|
|
|
213
212
|
handler_md.event_filters.append(new_group)
|
|
214
213
|
|
|
215
214
|
return RegisteringCommandable(new_group)
|
|
215
|
+
raise ValueError("注册指令组失败。")
|
|
216
216
|
|
|
217
217
|
return decorator
|
|
218
218
|
|
|
@@ -220,9 +220,11 @@ def register_command_group(
|
|
|
220
220
|
class RegisteringCommandable:
|
|
221
221
|
"""用于指令组级联注册"""
|
|
222
222
|
|
|
223
|
-
group:
|
|
224
|
-
|
|
225
|
-
|
|
223
|
+
group: Callable[..., Callable[..., "RegisteringCommandable"]] = (
|
|
224
|
+
register_command_group
|
|
225
|
+
)
|
|
226
|
+
command: Callable[..., Callable[..., None]] = register_command
|
|
227
|
+
custom_filter: Callable[..., Callable[..., None]] = register_custom_filter
|
|
226
228
|
|
|
227
229
|
def __init__(self, parent_group: CommandGroupFilter):
|
|
228
230
|
self.parent_group = parent_group
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import typing
|
|
2
2
|
import traceback
|
|
3
3
|
import os
|
|
4
|
+
import inspect
|
|
4
5
|
from .route import Route, Response, RouteContext
|
|
5
6
|
from astrbot.core.provider.entities import ProviderType
|
|
6
7
|
from quart import request
|
|
@@ -13,10 +14,10 @@ from astrbot.core.config.default import (
|
|
|
13
14
|
from astrbot.core.utils.astrbot_path import get_astrbot_path
|
|
14
15
|
from astrbot.core.config.astrbot_config import AstrBotConfig
|
|
15
16
|
from astrbot.core.core_lifecycle import AstrBotCoreLifecycle
|
|
16
|
-
from astrbot.core.platform.register import platform_registry
|
|
17
|
+
from astrbot.core.platform.register import platform_registry, platform_cls_map
|
|
17
18
|
from astrbot.core.provider.register import provider_registry
|
|
18
19
|
from astrbot.core.star.star import star_registry
|
|
19
|
-
from astrbot.core import logger
|
|
20
|
+
from astrbot.core import logger, file_token_service
|
|
20
21
|
from astrbot.core.provider import Provider
|
|
21
22
|
from astrbot.core.provider.provider import RerankProvider
|
|
22
23
|
import asyncio
|
|
@@ -149,6 +150,7 @@ class ConfigRoute(Route):
|
|
|
149
150
|
super().__init__(context)
|
|
150
151
|
self.core_lifecycle = core_lifecycle
|
|
151
152
|
self.config: AstrBotConfig = core_lifecycle.astrbot_config
|
|
153
|
+
self._logo_token_cache = {} # 缓存logo token,避免重复注册
|
|
152
154
|
self.acm = core_lifecycle.astrbot_config_mgr
|
|
153
155
|
self.routes = {
|
|
154
156
|
"/config/abconf/new": ("POST", self.create_abconf),
|
|
@@ -655,6 +657,78 @@ class ConfigRoute(Route):
|
|
|
655
657
|
return Response().error(str(e)).__dict__
|
|
656
658
|
return Response().ok(None, "删除成功,已经实时生效~").__dict__
|
|
657
659
|
|
|
660
|
+
async def get_llm_tools(self):
|
|
661
|
+
"""获取函数调用工具。包含了本地加载的以及 MCP 服务的工具"""
|
|
662
|
+
tool_mgr = self.core_lifecycle.provider_manager.llm_tools
|
|
663
|
+
tools = tool_mgr.get_func_desc_openai_style()
|
|
664
|
+
return Response().ok(tools).__dict__
|
|
665
|
+
|
|
666
|
+
async def _register_platform_logo(self, platform, platform_default_tmpl):
|
|
667
|
+
"""注册平台logo文件并生成访问令牌"""
|
|
668
|
+
if not platform.logo_path:
|
|
669
|
+
return
|
|
670
|
+
|
|
671
|
+
try:
|
|
672
|
+
# 检查缓存
|
|
673
|
+
cache_key = f"{platform.name}:{platform.logo_path}"
|
|
674
|
+
if cache_key in self._logo_token_cache:
|
|
675
|
+
cached_token = self._logo_token_cache[cache_key]
|
|
676
|
+
# 确保platform_default_tmpl[platform.name]存在且为字典
|
|
677
|
+
if platform.name not in platform_default_tmpl:
|
|
678
|
+
platform_default_tmpl[platform.name] = {}
|
|
679
|
+
elif not isinstance(platform_default_tmpl[platform.name], dict):
|
|
680
|
+
platform_default_tmpl[platform.name] = {}
|
|
681
|
+
platform_default_tmpl[platform.name]["logo_token"] = cached_token
|
|
682
|
+
logger.debug(f"Using cached logo token for platform {platform.name}")
|
|
683
|
+
return
|
|
684
|
+
|
|
685
|
+
# 获取平台适配器类
|
|
686
|
+
platform_cls = platform_cls_map.get(platform.name)
|
|
687
|
+
if not platform_cls:
|
|
688
|
+
logger.warning(f"Platform class not found for {platform.name}")
|
|
689
|
+
return
|
|
690
|
+
|
|
691
|
+
# 获取插件目录路径
|
|
692
|
+
module_file = inspect.getfile(platform_cls)
|
|
693
|
+
plugin_dir = os.path.dirname(module_file)
|
|
694
|
+
|
|
695
|
+
# 解析logo文件路径
|
|
696
|
+
logo_file_path = os.path.join(plugin_dir, platform.logo_path)
|
|
697
|
+
|
|
698
|
+
# 检查文件是否存在并注册令牌
|
|
699
|
+
if os.path.exists(logo_file_path):
|
|
700
|
+
logo_token = await file_token_service.register_file(
|
|
701
|
+
logo_file_path, timeout=3600
|
|
702
|
+
)
|
|
703
|
+
|
|
704
|
+
# 确保platform_default_tmpl[platform.name]存在且为字典
|
|
705
|
+
if platform.name not in platform_default_tmpl:
|
|
706
|
+
platform_default_tmpl[platform.name] = {}
|
|
707
|
+
elif not isinstance(platform_default_tmpl[platform.name], dict):
|
|
708
|
+
platform_default_tmpl[platform.name] = {}
|
|
709
|
+
|
|
710
|
+
platform_default_tmpl[platform.name]["logo_token"] = logo_token
|
|
711
|
+
|
|
712
|
+
# 缓存token
|
|
713
|
+
self._logo_token_cache[cache_key] = logo_token
|
|
714
|
+
|
|
715
|
+
logger.debug(f"Logo token registered for platform {platform.name}")
|
|
716
|
+
else:
|
|
717
|
+
logger.warning(
|
|
718
|
+
f"Platform {platform.name} logo file not found: {logo_file_path}"
|
|
719
|
+
)
|
|
720
|
+
|
|
721
|
+
except (ImportError, AttributeError) as e:
|
|
722
|
+
logger.warning(
|
|
723
|
+
f"Failed to import required modules for platform {platform.name}: {e}"
|
|
724
|
+
)
|
|
725
|
+
except (OSError, IOError) as e:
|
|
726
|
+
logger.warning(f"File system error for platform {platform.name} logo: {e}")
|
|
727
|
+
except Exception as e:
|
|
728
|
+
logger.warning(
|
|
729
|
+
f"Unexpected error registering logo for platform {platform.name}: {e}"
|
|
730
|
+
)
|
|
731
|
+
|
|
658
732
|
async def _get_astrbot_config(self):
|
|
659
733
|
config = self.config
|
|
660
734
|
|
|
@@ -662,9 +736,21 @@ class ConfigRoute(Route):
|
|
|
662
736
|
platform_default_tmpl = CONFIG_METADATA_2["platform_group"]["metadata"][
|
|
663
737
|
"platform"
|
|
664
738
|
]["config_template"]
|
|
739
|
+
|
|
740
|
+
# 收集需要注册logo的平台
|
|
741
|
+
logo_registration_tasks = []
|
|
665
742
|
for platform in platform_registry:
|
|
666
743
|
if platform.default_config_tmpl:
|
|
667
744
|
platform_default_tmpl[platform.name] = platform.default_config_tmpl
|
|
745
|
+
# 收集logo注册任务
|
|
746
|
+
if platform.logo_path:
|
|
747
|
+
logo_registration_tasks.append(
|
|
748
|
+
self._register_platform_logo(platform, platform_default_tmpl)
|
|
749
|
+
)
|
|
750
|
+
|
|
751
|
+
# 并行执行logo注册
|
|
752
|
+
if logo_registration_tasks:
|
|
753
|
+
await asyncio.gather(*logo_registration_tasks, return_exceptions=True)
|
|
668
754
|
|
|
669
755
|
# 服务提供商的默认配置模板注入
|
|
670
756
|
provider_default_tmpl = CONFIG_METADATA_2["provider_group"]["metadata"][
|
|
@@ -20,6 +20,7 @@ class SessionManagementRoute(Route):
|
|
|
20
20
|
core_lifecycle: AstrBotCoreLifecycle,
|
|
21
21
|
) -> None:
|
|
22
22
|
super().__init__(context)
|
|
23
|
+
self.db_helper = db_helper
|
|
23
24
|
self.routes = {
|
|
24
25
|
"/session/list": ("GET", self.list_sessions),
|
|
25
26
|
"/session/update_persona": ("POST", self.update_session_persona),
|
|
@@ -39,22 +40,42 @@ class SessionManagementRoute(Route):
|
|
|
39
40
|
async def list_sessions(self):
|
|
40
41
|
"""获取所有会话的列表,包括 persona 和 provider 信息"""
|
|
41
42
|
try:
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
43
|
+
page = int(request.args.get("page", 1))
|
|
44
|
+
page_size = int(request.args.get("page_size", 20))
|
|
45
|
+
search_query = request.args.get("search", "")
|
|
46
|
+
platform = request.args.get("platform", "")
|
|
47
|
+
|
|
48
|
+
# 获取活跃的会话数据(处于对话内的会话)
|
|
49
|
+
sessions_data, total = await self.db_helper.get_session_conversations(
|
|
50
|
+
page, page_size, search_query, platform
|
|
51
|
+
)
|
|
52
|
+
|
|
46
53
|
provider_manager = self.core_lifecycle.provider_manager
|
|
47
54
|
persona_mgr = self.core_lifecycle.persona_mgr
|
|
48
55
|
personas = persona_mgr.personas_v3
|
|
49
56
|
|
|
50
57
|
sessions = []
|
|
51
58
|
|
|
52
|
-
#
|
|
53
|
-
for
|
|
59
|
+
# 循环补充非数据库信息,如 provider 和 session 状态
|
|
60
|
+
for data in sessions_data:
|
|
61
|
+
session_id = data["session_id"]
|
|
62
|
+
conversation_id = data["conversation_id"]
|
|
63
|
+
conv_persona_id = data["persona_id"]
|
|
64
|
+
title = data["title"]
|
|
65
|
+
persona_name = data["persona_name"]
|
|
66
|
+
|
|
67
|
+
# 处理 persona 显示
|
|
68
|
+
if conv_persona_id == "[%None]":
|
|
69
|
+
persona_name = "无人格"
|
|
70
|
+
else:
|
|
71
|
+
default_persona = persona_mgr.selected_default_persona_v3
|
|
72
|
+
if default_persona:
|
|
73
|
+
persona_name = default_persona["name"]
|
|
74
|
+
|
|
54
75
|
session_info = {
|
|
55
76
|
"session_id": session_id,
|
|
56
77
|
"conversation_id": conversation_id,
|
|
57
|
-
"persona_id":
|
|
78
|
+
"persona_id": persona_name,
|
|
58
79
|
"chat_provider_id": None,
|
|
59
80
|
"stt_provider_id": None,
|
|
60
81
|
"tts_provider_id": None,
|
|
@@ -79,31 +100,10 @@ class SessionManagementRoute(Route):
|
|
|
79
100
|
"session_raw_name": session_id.split(":")[2]
|
|
80
101
|
if session_id.count(":") >= 2
|
|
81
102
|
else session_id,
|
|
103
|
+
"title": title,
|
|
82
104
|
}
|
|
83
105
|
|
|
84
|
-
# 获取对话信息
|
|
85
|
-
conversation = await self.conv_mgr.get_conversation(
|
|
86
|
-
unified_msg_origin=session_id, conversation_id=conversation_id
|
|
87
|
-
)
|
|
88
|
-
if conversation:
|
|
89
|
-
session_info["persona_id"] = conversation.persona_id
|
|
90
|
-
|
|
91
|
-
# 查找 persona 名称
|
|
92
|
-
if conversation.persona_id and conversation.persona_id != "[%None]":
|
|
93
|
-
for persona in personas:
|
|
94
|
-
if persona["name"] == conversation.persona_id:
|
|
95
|
-
session_info["persona_id"] = persona["name"]
|
|
96
|
-
break
|
|
97
|
-
elif conversation.persona_id == "[%None]":
|
|
98
|
-
session_info["persona_id"] = "无人格"
|
|
99
|
-
else:
|
|
100
|
-
# 使用默认人格
|
|
101
|
-
default_persona = persona_mgr.selected_default_persona_v3
|
|
102
|
-
if default_persona:
|
|
103
|
-
session_info["persona_id"] = default_persona["name"]
|
|
104
|
-
|
|
105
106
|
# 获取 provider 信息
|
|
106
|
-
provider_manager = self.core_lifecycle.provider_manager
|
|
107
107
|
chat_provider = provider_manager.get_using_provider(
|
|
108
108
|
provider_type=ProviderType.CHAT_COMPLETION, umo=session_id
|
|
109
109
|
)
|
|
@@ -172,6 +172,14 @@ class SessionManagementRoute(Route):
|
|
|
172
172
|
"available_chat_providers": available_chat_providers,
|
|
173
173
|
"available_stt_providers": available_stt_providers,
|
|
174
174
|
"available_tts_providers": available_tts_providers,
|
|
175
|
+
"pagination": {
|
|
176
|
+
"page": page,
|
|
177
|
+
"page_size": page_size,
|
|
178
|
+
"total": total,
|
|
179
|
+
"total_pages": (total + page_size - 1) // page_size
|
|
180
|
+
if page_size > 0
|
|
181
|
+
else 0,
|
|
182
|
+
},
|
|
175
183
|
}
|
|
176
184
|
|
|
177
185
|
return Response().ok(result).__dict__
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: AstrBot
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.3.1
|
|
4
4
|
Summary: 易上手的多平台 LLM 聊天机器人及开发框架
|
|
5
5
|
License-File: LICENSE
|
|
6
6
|
Requires-Python: >=3.10
|
|
@@ -25,10 +25,10 @@ Requires-Dist: docstring-parser>=0.16
|
|
|
25
25
|
Requires-Dist: faiss-cpu==1.10.0
|
|
26
26
|
Requires-Dist: filelock>=3.18.0
|
|
27
27
|
Requires-Dist: google-genai>=1.14.0
|
|
28
|
-
Requires-Dist: googlesearch-python>=1.3.0
|
|
29
28
|
Requires-Dist: lark-oapi>=1.4.15
|
|
30
29
|
Requires-Dist: lxml-html-clean>=0.4.2
|
|
31
30
|
Requires-Dist: mcp>=1.8.0
|
|
31
|
+
Requires-Dist: mi-googlesearch-python==1.3.0.post1
|
|
32
32
|
Requires-Dist: openai>=1.78.0
|
|
33
33
|
Requires-Dist: ormsgpack>=1.9.1
|
|
34
34
|
Requires-Dist: pillow>=11.2.1
|
|
@@ -246,7 +246,7 @@ pre-commit install
|
|
|
246
246
|
- [koishijs/koishi](https://github.com/koishijs/koishi) - 扩展性极强的 Bot 框架
|
|
247
247
|
- [MaiM-with-u/MaiBot](https://github.com/MaiM-with-u/MaiBot) - 注重拟人功能的 ChatBot
|
|
248
248
|
- [langbot-app/LangBot](https://github.com/langbot-app/LangBot) - 功能丰富的 Bot 平台
|
|
249
|
-
- [
|
|
249
|
+
- [KroMiose/nekro-agent](https://github.com/KroMiose/nekro-agent) - 注重 Agent 的 ChatBot
|
|
250
250
|
- [zhenxun-org/zhenxun_bot](https://github.com/zhenxun-org/zhenxun_bot) - 功能完善的 ChatBot
|
|
251
251
|
|
|
252
252
|
## ⭐ Star History
|