Compare commits

...

46 Commits

Author SHA1 Message Date
Soulter b73cf84df0 v3.4.19 2025-02-04 16:37:15 +08:00
Soulter a5b885a774 fix: schema 中 object hint 不显示 #290
feat: 优化插件市场的访问
2025-02-04 16:36:00 +08:00
Soulter 0c785413da chore: clean code 2025-02-04 15:51:26 +08:00
Soulter 482d7ef5f7 v3.4.19 2025-02-04 15:47:24 +08:00
Soulter 9f9073c0ff feat: 支持设置所有指令的权限
feat: 插件指令支持设置指令描述
feat: plugin 指令支持查看插件的指令
2025-02-04 15:41:45 +08:00
Soulter ef05ff4abd fix: 管理员指令 /reset /persona 2025-02-04 13:50:23 +08:00
Soulter 5848aae435 Update README.md 2025-02-04 13:44:02 +08:00
Soulter fb06f33de0 Update README.md 2025-02-04 12:51:17 +08:00
Soulter 0d7ddb149e fix: 修复请求 gemini 推理模型出现 candidates 错误的问题 #333 2025-02-04 00:30:23 +08:00
Soulter 4f2d7b9c4e feat: 适配 Azure OpenAI #332 2025-02-03 23:59:04 +08:00
Soulter c02ed96f6f perf: gewechat 服务端回调接口默认暴露在所有地址 2025-02-03 18:51:19 +08:00
Soulter 3b2ac891b2 fix: 修复限流器不可用的问题 #263 2025-02-03 18:51:19 +08:00
Soulter ef0108881b Update Dockerfile 2025-02-03 17:48:17 +08:00
Soulter af48975a6b chore: v3.4.18 2025-02-03 16:14:27 +08:00
Soulter 6441b149ab fix: 修复主动概率回复关闭后仍然回复的问题 #317 2025-02-03 14:33:53 +08:00
Soulter f8892881f8 fix: 尝试修复 gewechat 群聊收不到 at 的回复 #294 2025-02-03 14:28:14 +08:00
Soulter 228aec5401 perf: 移除了默认人格 2025-02-03 14:17:45 +08:00
Soulter 68ad48ff55 fix: 修复HTTP代理删除后不生效 #319 2025-02-03 14:11:50 +08:00
Soulter 541ba64032 fix: 调用Gemini API输出多余空行问题 #318 2025-02-03 13:27:56 +08:00
Soulter 2d870b798c feat: 添加硅基流动模版 2025-02-03 13:24:22 +08:00
Soulter 0f1fe1ab63 fix: 硅基流动 not a vlm 和 tool calling not supported 报错 #305 # 291
perf: 安装和更新插件后全量重启避免奇奇怪怪的bug
feat: 支持 /tool off_all 停用所有函数工具
2025-02-03 13:20:49 +08:00
Soulter 73cc86ddb1 perf: 回复时艾特发送者之后添加空格或换行 #312 2025-02-03 12:04:26 +08:00
Soulter 23128f4be2 perf: 主动回复不支持 qq_official 的 hint 2025-02-03 12:00:05 +08:00
Soulter 92200d0e82 fix: docker容器内时区不对 2025-02-03 01:15:09 +08:00
Soulter d6e8655792 fix: 抱错时首先移除 tool 2025-02-02 23:17:59 +08:00
Soulter 37076d7920 perf: siliconcloud 不支持 tool 的模型 2025-02-02 23:05:36 +08:00
Soulter 78347ec91b perf: 当人格长度为1时设置默认人格
feat: 支持取消人格
2025-02-02 22:36:50 +08:00
Soulter 9ded102a0a chore: v3.4.17 2025-02-02 20:39:26 +08:00
Soulter 59b7d8b8cb chore: clean code 2025-02-02 20:15:57 +08:00
Soulter f5b97f6762 perf: 优化 404 提示 2025-02-02 19:55:32 +08:00
Soulter d47da241af feat: openai tts 更换模型 #300 2025-02-02 19:47:39 +08:00
Soulter 4611ce15eb feat: [beta] 支持群聊内基于概率的主动回复 2025-02-02 19:23:46 +08:00
Soulter aa8c56a688 fix: 相同type的provider共享了记忆 2025-02-02 19:13:47 +08:00
Soulter ef44d4471a feat: 增加模型响应后的插件钩子
remove: 移除了默认的r1过滤
2025-02-02 16:42:21 +08:00
Soulter 5581eae957 fix: deepseek-r1模型存在遗留“</think>”的问题 #279
Open
2025-02-02 14:59:17 +08:00
Soulter ec46dfaac9 perf: 人格情景在发现格式不对时仍然加载而不是跳过 #282 2025-02-02 14:59:17 +08:00
Soulter 6042a047bd 修复Gemini函数调用时,parameters为空对象导致的错误
fix: 修复Gemini函数调用时,parameters为空对象导致的错误
2025-02-02 14:50:34 +08:00
Soulter 6ca9e2a753 perf: websearch 可选配置引用链接 #287 2025-02-02 14:42:13 +08:00
Camreishi 618eabfe5c fix: 修复Gemini函数调用时,parameters为空对象导致的错误
Closes #288
2025-02-02 13:25:08 +08:00
Soulter bb5db2e9d0 fix: 修复弹出记录报错的问题 #272 2025-02-02 13:24:05 +08:00
Soulter 97e4d169b3 perf: 未启用模型提供商时的异常处理 2025-02-02 11:23:33 +08:00
Soulter 50e44b1473 perf: 移除默认人格 2025-02-02 11:12:17 +08:00
Soulter 38588dd3fa update compose 2025-02-02 00:11:55 +08:00
Soulter d183388347 perf: 去除gewechat默认配置 2025-02-01 23:20:25 +08:00
Soulter 1e69d59384 fix: 配置提示 typo 2025-02-01 22:56:34 +08:00
Soulter 00f008f94d Update compose.yml 2025-02-01 21:31:24 +08:00
39 changed files with 699 additions and 175 deletions
+1 -1
View File
@@ -70,7 +70,7 @@ AstrBot 是一个松耦合、异步、支持多消息平台部署、具有易用
| QQ(OneBot) | ✔ | 私聊、群聊 | 文字、图片、语音 |
| 微信(个人号) | ✔ | 微信个人号私聊、群聊 | 文字、图片、语音 |
| [Telegram](https://github.com/Soulter/astrbot_plugin_telegram) | ✔ | 私聊、群聊 | 文字、图片 |
| 微信(企业微信) | 🚧 | 计划内 | - |
| [微信(企业微信)](https://github.com/Soulter/astrbot_plugin_wecom) | | 私聊 | 文字、图片、语音 |
| 微信对话开放平台 | 🚧 | 计划内 | - |
| 飞书 | 🚧 | 计划内 | - |
| Discord | 🚧 | 计划内 | - |
+3 -1
View File
@@ -6,6 +6,7 @@ from astrbot.core.star.register import (
register_platform_adapter_type as platform_adapter_type,
register_permission_type as permission_type,
register_on_llm_request as on_llm_request,
register_on_llm_response as on_llm_response,
register_llm_tool as llm_tool,
register_on_decorating_result as on_decorating_result,
register_after_message_sent as after_message_sent
@@ -31,5 +32,6 @@ __all__ = [
'on_llm_request',
'llm_tool',
'on_decorating_result',
'after_message_sent'
'after_message_sent',
'on_llm_response'
]
+95 -20
View File
@@ -2,7 +2,7 @@
如需修改配置,请在 `data/cmd_config.json` 中修改或者在管理面板中可视化修改。
"""
VERSION = "3.4.16"
VERSION = "3.4.19"
DB_PATH = "data/data_v3.db"
# 默认配置
@@ -37,6 +37,7 @@ DEFAULT_CONFIG = {
"enable": True,
"wake_prefix": "",
"web_search": False,
"web_search_link": False,
"identifier": False,
"datetime_system_prompt": True,
"default_personality": "default",
@@ -55,6 +56,13 @@ DEFAULT_CONFIG = {
"group_message_max_cnt": 300,
"image_caption": False,
"image_caption_prompt": "Please describe the image using Chinese.",
"active_reply": {
"enable": False,
"method": "possibility_reply",
"possibility_reply": 0.1,
"prompt": "",
},
"put_history_to_prompt": True,
},
"content_safety": {
"internal_keywords": {"enable": True, "extra_keywords": []},
@@ -75,14 +83,7 @@ DEFAULT_CONFIG = {
"pip_install_arg": "",
"plugin_repo_mirror": "",
"knowledge_db": {},
"persona": [
{
"name": "default",
"prompt": "如果用户寻求帮助或者打招呼,请告诉他可以用 /help 查看 AstrBot 帮助。",
"begin_dialogs": [],
"mood_imitation_dialogs": [],
}
],
"persona": [],
}
@@ -111,14 +112,13 @@ CONFIG_METADATA_2 = {
"ws_reverse_host": "",
"ws_reverse_port": 6199,
},
"vchat(微信)": {"id": "default", "type": "vchat", "enable": False},
"gewechat(微信)": {
"id": "gwchat",
"type": "gewechat",
"enable": False,
"base_url": "http://localhost:2531",
"nickname": "soulter",
"host": "localhost",
"host": "这里填写你的局域网IP或者公网服务器IP",
"port": 11451,
},
},
@@ -237,7 +237,8 @@ CONFIG_METADATA_2 = {
"description": "ID 白名单",
"type": "list",
"items": {"type": "string"},
"hint": "AstrBot 只处理所填写的 ID 发来的消息事件。为空时不启用白名单过滤。可以使用 /myid 指令获取在某个平台上的会话 ID。也可在 AstrBot 日志内获取会话 ID,当一条消息没通过白名单时,会输出 INFO 级别的日志。会话 ID 类似 aiocqhttp:GroupMessage:547540978",
"obvious_hint": True,
"hint": "AstrBot 只处理所填写的 ID 发来的消息事件。为空时不启用白名单过滤。可以使用 /sid 指令获取在某个平台上的会话 ID。也可在 AstrBot 日志内获取会话 ID,当一条消息没通过白名单时,会输出 INFO 级别的日志。会话 ID 类似 aiocqhttp:GroupMessage:547540978",
},
"id_whitelist_log": {
"description": "打印白名单日志",
@@ -320,10 +321,21 @@ CONFIG_METADATA_2 = {
"type": "list",
"config_template": {
"openai": {
"id": "default",
"id": "openai",
"type": "openai_chat_completion",
"enable": True,
"key": [],
"api_base": "https://api.openai.com/v1",
"model_config": {
"model": "gpt-4o-mini",
},
},
"azure_openai": {
"id": "azure",
"type": "openai_chat_completion",
"enable": True,
"api_version": "2024-05-01-preview",
"key": [],
"api_base": "",
"model_config": {
"model": "gpt-4o-mini",
@@ -379,6 +391,16 @@ CONFIG_METADATA_2 = {
"model": "glm-4-flash",
},
},
"硅基流动": {
"id": "siliconflow",
"type": "openai_chat_completion",
"enable": True,
"key": [],
"api_base": "https://api.siliconflow.cn/v1",
"model_config": {
"model": "deepseek-ai/DeepSeek-V3",
},
},
"llmtuner": {
"id": "llmtuner_default",
"type": "llm_tuner",
@@ -420,9 +442,17 @@ CONFIG_METADATA_2 = {
"api_key": "",
"api_base": "",
"model": "tts-1",
"openai-tts-voice": "alloy",
"timeout": "20",
},
},
"items": {
"openai-tts-voice": {
"description": "voice",
"type": "string",
"obvious_hint": True,
"hint": "OpenAI TTS 的声音。OpenAI 默认支持:'alloy', 'echo', 'fable', 'onyx', 'nova', 'shimmer'",
},
"whisper_hint": {
"description": "本地部署 Whisper 模型须知",
"type": "string",
@@ -453,7 +483,7 @@ CONFIG_METADATA_2 = {
"api_base": {
"description": "API Base URL",
"type": "string",
"hint": "API Base URL 请在在模型提供商处获得。如使用时出现 404 报错,可以尝试在地址末尾加上 `/v1`。",
"hint": "API Base URL 请在在模型提供商处获得。如出现 404 报错,尝试在地址末尾加上 /v1",
"obvious_hint": True,
},
"base_model_path": {
@@ -539,16 +569,25 @@ CONFIG_METADATA_2 = {
"web_search": {
"description": "启用网页搜索",
"type": "bool",
"hint": "能访问 Google 时效果最佳。如果 Google 访问失败,程序会依次访问 Bing, Sogo 搜索引擎。",
"obvious_hint": True,
"hint": "能访问 Google 时效果最佳(国内需要在 `其他配置` 开启 HTTP 代理)。如果 Google 访问失败,程序会依次访问 Bing, Sogo 搜索引擎。",
},
"web_search_link": {
"description": "网页搜索引用链接",
"type": "bool",
"obvious_hint": True,
"hint": "开启后,将会传入网页搜索结果的链接给模型,并引导模型输出引用链接。",
},
"identifier": {
"description": "启动识别群员",
"type": "bool",
"obvious_hint": True,
"hint": "在 Prompt 前加上群成员的名字以让模型更好地了解群聊状态。启用将略微增加 token 开销。",
},
"datetime_system_prompt": {
"description": "启用日期时间系统提示",
"type": "bool",
"obvious_hint": True,
"hint": "启用后,会在系统提示词中加上当前机器的日期时间。",
},
"default_personality": {
@@ -591,14 +630,14 @@ CONFIG_METADATA_2 = {
"description": "预设对话",
"type": "list",
"items": {"type": "string"},
"hint": "可选。在每个对话前会插入这些预设对话。格式要求:第一句为用户,第二句为助手,以此类推。",
"hint": "可选。在每个对话前会插入这些预设对话。对话需要成对(用户和助手),输入完一个角色的内容之后按【回车】。需要偶数个对话",
"obvious_hint": True,
},
"mood_imitation_dialogs": {
"description": "对话风格模仿",
"type": "list",
"items": {"type": "string"},
"hint": "旨在让模型尽可能模仿学习到所填写的对话的语气风格。格式和 `预设对话` 一样。",
"hint": "旨在让模型尽可能模仿学习到所填写的对话的语气风格。格式和 `预设对话` 一致。对话需要成对(用户和助手),输入完一个角色的内容之后按【回车】。需要偶数个对话",
"obvious_hint": True,
},
},
@@ -644,25 +683,61 @@ CONFIG_METADATA_2 = {
"group_icl_enable": {
"description": "群聊内记录各群员对话",
"type": "bool",
"obvious-hint": True,
"obvious_hint": True,
"hint": "启用后,会记录群聊内各群员的对话。使用 /reset 命令清除记录。推荐使用 gpt-4o-mini 模型。",
},
"group_message_max_cnt": {
"description": "群聊消息最大数量",
"type": "int",
"obvious-hint": True,
"obvious_hint": True,
"hint": "群聊消息最大数量。超过此数量后,会自动清除旧消息。",
},
"image_caption": {
"description": "启用图像转述(需要模型支持)",
"type": "bool",
"obvious-hint": True,
"obvious_hint": True,
"hint": "启用后,当接收到图片消息时,会使用模型先将图片转述为文字再进行后续处理。推荐使用 gpt-4o-mini 模型。",
},
"image_caption_prompt": {
"description": "图像转述提示词",
"type": "string"
},
"active_reply": {
"description": "主动回复",
"type": "object",
"items": {
"enable": {
"description": "启用主动回复",
"type": "bool",
"obvious_hint": True,
"hint": "启用后,会根据触发概率主动回复群聊内的对话。QQ官方API(qq_official)不可用",
},
"method": {
"description": "回复方法",
"type": "string",
"options": ["possibility_reply"],
"hint": "回复方法。possibility_reply 为根据概率回复",
},
"possibility_reply": {
"description": "回复概率",
"type": "float",
"obvious_hint": True,
"hint": "回复概率。当回复方法为 possibility_reply 时有效。当概率 >= 1 时,每条消息都会回复。",
},
"prompt": {
"description": "提示词",
"type": "string",
"obvious_hint": True,
"hint": "提示词。当提示词为空时,如果触发回复,prompt是触发的消息的内容;否则是提示词。此项可以和定时回复(暂未实现)配合使用。",
},
},
},
"put_history_to_prompt": {
"description": "将群聊历史记录作为 prompt",
"type": "bool",
"obvious_hint": True,
"hint": "需要先启用 group_icl_enable。此功能会将群聊历史记录放到 prompt 再请求。如果关闭,则是放在 system_prompt。如果开启了主动回复,建议启用,模型能够更好地完成回复任务。",
}
},
},
},
+2 -3
View File
@@ -25,9 +25,8 @@ class AstrBotCoreLifecycle:
self.astrbot_config = astrbot_config
self.db = db
if self.astrbot_config['http_proxy']:
os.environ['https_proxy'] = self.astrbot_config['http_proxy']
os.environ['http_proxy'] = self.astrbot_config['http_proxy']
os.environ['https_proxy'] = self.astrbot_config['http_proxy']
os.environ['http_proxy'] = self.astrbot_config['http_proxy']
async def initialize(self):
logger.info("AstrBot v"+ VERSION)
+3 -1
View File
@@ -2,6 +2,7 @@ from astrbot.core.message.message_event_result import MessageEventResult, EventR
from .waking_check.stage import WakingCheckStage
from .whitelist_check.stage import WhitelistCheckStage
from .rate_limit_check.stage import RateLimitStage
from .content_safety_check.stage import ContentSafetyCheckStage
from .preprocess_stage.stage import PreProcessStage
from .process_stage.stage import ProcessStage
@@ -11,7 +12,7 @@ from .respond.stage import RespondStage
STAGES_ORDER = [
"WakingCheckStage", # 检查是否需要唤醒
"WhitelistCheckStage", # 检查是否在群聊/私聊白名单
"RateLimitCheckStage", # 检查会话是否超过频率限制
"RateLimitStage", # 检查会话是否超过频率限制
"ContentSafetyCheckStage", # 检查内容安全
"PreProcessStage", # 预处理
"ProcessStage", # 交由 Stars 处理(a.k.a 插件),或者 LLM 调用
@@ -22,6 +23,7 @@ STAGES_ORDER = [
__all__ = [
"WakingCheckStage",
"WhitelistCheckStage",
"RateLimitStage",
"ContentSafetyCheckStage",
"PreProcessStage",
"ProcessStage",
@@ -21,6 +21,10 @@ class DifyRequestSubStage(Stage):
req: ProviderRequest = None
provider = self.ctx.plugin_manager.context.get_using_provider()
if not provider:
return
if provider.meta().type != "dify":
return
@@ -68,12 +68,23 @@ class LLMRequestSubStage(Stage):
if _nested:
req.func_tool = None # 暂时不支持递归工具调用
llm_response = await provider.text_chat(**req.__dict__) # 请求 LLM
# 执行 LLM 响应后的事件。
handlers = star_handlers_registry.get_handlers_by_event_type(EventType.OnLLMResponseEvent)
for handler in handlers:
try:
await handler.handler(event, llm_response)
except BaseException:
logger.error(traceback.format_exc())
await Metric.upload(llm_tick=1, model_name=provider.get_model(), provider_type=provider.meta().type)
if llm_response.role == 'assistant':
# text completion
event.set_result(MessageEventResult().message(llm_response.completion_text)
.set_result_content_type(ResultContentType.LLM_RESULT))
elif llm_response.role == 'err':
event.set_result(MessageEventResult().message(f"AstrBot 请求失败。\n错误信息: {llm_response.completion_text}"))
elif llm_response.role == 'tool':
# function calling
function_calling_result = {}
@@ -37,7 +37,11 @@ class ProcessStage(Stage):
# Handler 的 LLM 请求
logger.debug(f"llm request -> {resp.prompt}")
event.set_extra("provider_request", resp)
_t = False
async for _ in self.llm_request_sub_stage.process(event):
_t = True
yield
if not _t:
yield
else:
yield
@@ -49,6 +53,11 @@ class ProcessStage(Stage):
if not event._has_send_oper and event.is_at_or_wake_command:
if (event.get_result() and not event.get_result().is_stopped()) or not event.get_result():
provider = self.ctx.plugin_manager.context.get_using_provider()
if not provider:
logger.info("未找到可用的 LLM 提供商,请先前往配置服务提供商。")
return
match provider.meta().type:
case "dify":
async for _ in self.dify_request_sub_stage.process(event):
@@ -61,11 +61,12 @@ class RateLimitStage(Stage):
stall_duration = (next_window_time - now).total_seconds()
match self.rl_strategy:
case RateLimitStrategy.STALL:
case RateLimitStrategy.STALL.value:
logger.info(f"会话 {session_id} 被限流。根据限流策略,此会话处理将被暂停 {stall_duration:.2f} 秒。")
await asyncio.sleep(stall_duration)
case RateLimitStrategy.DISCARD:
event.set_result(MessageEventResult().message(f"会话 {session_id} 被限流。根据限流策略,此请求已被丢弃,直到您的限额于 {stall_duration:.2f} 秒后重置。"))
case RateLimitStrategy.DISCARD.value:
# event.set_result(MessageEventResult().message(f"会话 {session_id} 被限流。根据限流策略,此请求已被丢弃,直到您的限额于 {stall_duration:.2f} 秒后重置。"))
logger.info(f"会话 {session_id} 被限流。根据限流策略,此请求已被丢弃,直到限额于 {stall_duration:.2f} 秒后重置。")
return event.stop_event()
self._remove_expired_timestamps(timestamps, now + timedelta(seconds=stall_duration))
@@ -103,8 +103,12 @@ class ResultDecorateStage:
if url:
result.chain = [Image.fromURL(url)]
# at 回复
if self.reply_with_mention and event.get_message_type() != MessageType.FRIEND_MESSAGE:
result.chain.insert(0, At(qq=event.get_sender_id(), name=event.get_sender_name()))
if len(result.chain) > 1 and isinstance(result.chain[1], Plain):
result.chain[1].text = "\n" + result.chain[1].text
# 引用回复
if self.reply_with_quote:
result.chain.insert(0, Reply(id=event.message_obj.message_id))
@@ -296,6 +296,7 @@ class AstrMessageEvent(abc.ABC):
def request_llm(
self,
prompt: str,
func_tool_manager = None,
session_id: str = None,
image_urls: List[str] = None,
contexts: List = None,
@@ -311,11 +312,13 @@ class AstrMessageEvent(abc.ABC):
image_urls: 可以是 base64:// 或者 http:// 开头的图片链接,也可以是本地图片路径。
contexts: 当指定 contexts 时,将会**只**使用 contexts 作为上下文。
func_tool_manager: 函数工具管理器,用于调用函数工具。用 self.context.get_llm_tool_manager() 获取。
'''
return ProviderRequest(
prompt = prompt,
session_id = session_id,
image_urls = image_urls,
func_tool = func_tool_manager,
contexts = contexts,
system_prompt = system_prompt
)
+4 -1
View File
@@ -24,7 +24,10 @@ class PlatformManager():
case "qq_official":
from .sources.qqofficial.qqofficial_platform_adapter import QQOfficialPlatformAdapter # noqa: F401
case "vchat":
from .sources.vchat.vchat_platform_adapter import VChatPlatformAdapter # noqa: F401
try:
from .sources.vchat.vchat_platform_adapter import VChatPlatformAdapter # noqa: F401
except BaseException:
logger.warning("当前 astrbot 已不维护 vchat 的接入,如有需要请 pip 安装 vchat 然后重启")
case "gewechat":
from .sources.gewechat.gewechat_platform_adapter import GewechatPlatformAdapter # noqa: F401
@@ -95,6 +95,8 @@ class SimpleGewechatClient():
if f'<atuserlist><![CDATA[,{abm.self_id}]]>' in msg_source \
or f'<atuserlist><![CDATA[{abm.self_id}]]>' in msg_source:
at_me = True
if '在群聊中@了你' in d.get('PushContent', ''):
at_me = True
else:
abm.type = MessageType.FRIEND_MESSAGE
user_id = from_user_name
@@ -192,7 +194,7 @@ class SimpleGewechatClient():
async def start_polling(self):
threading.Thread(target=asyncio.run, args=(self._set_callback_url(),)).start()
await self.server.run_task(
host=self.host,
host='0.0.0.0',
port=self.port,
shutdown_trigger=self.shutdown_trigger_placeholder
)
+5 -1
View File
@@ -2,6 +2,7 @@ import enum
from dataclasses import dataclass, field
from typing import List, Dict, Type
from .func_tool_manager import FuncCall
from openai.types.chat.chat_completion import ChatCompletion
class ProviderType(enum.Enum):
@@ -51,4 +52,7 @@ class LLMResponse:
tools_call_args: List[Dict[str, any]] = field(default_factory=list)
'''工具调用参数'''
tools_call_name: List[str] = field(default_factory=list)
'''工具调用名称'''
'''工具调用名称'''
raw_completion: ChatCompletion = None
_new_record: Dict[str, any] = None
+15 -8
View File
@@ -108,14 +108,21 @@ class FuncCall:
for f in self.func_list:
if not f.active:
continue
tools.append(
{
"name": f.name,
"parameters": f.parameters,
"description": f.description,
}
)
declarations["function_declarations"] = tools
func_declaration = {
"name": f.name,
"description": f.description
}
# 检查并添加非空的properties参数
params = f.parameters if isinstance(f.parameters, dict) else {}
if params.get("properties", {}):
func_declaration["parameters"] = params
tools.append(func_declaration)
if tools:
declarations["function_declarations"] = tools
return declarations
+9 -3
View File
@@ -17,6 +17,8 @@ class ProviderManager():
self.provider_tts_settings: dict = config.get('provider_tts_settings', {})
self.persona_configs: list = config.get('persona', [])
# 人格情景管理
# 目前没有拆成独立的模块
self.default_persona_name = self.provider_settings.get('default_personality', 'default')
self.personas: List[Personality] = []
self.selected_default_persona = None
@@ -28,7 +30,7 @@ class ProviderManager():
if begin_dialogs:
if len(begin_dialogs) % 2 != 0:
logger.error(f"{persona['name']} 人格情景预设对话格式不对,条数应该为偶数。")
continue
begin_dialogs = []
user_turn = True
for dialog in begin_dialogs:
bd_processed.append({
@@ -40,9 +42,9 @@ class ProviderManager():
if mood_imitation_dialogs:
if len(mood_imitation_dialogs) % 2 != 0:
logger.error(f"{persona['name']} 对话风格对话格式不对,条数应该为偶数。")
continue
mood_imitation_dialogs = []
user_turn = True
for dialog in begin_dialogs:
for dialog in mood_imitation_dialogs:
role = "A" if user_turn else "B"
mid_processed += f"{role}: {dialog}\n"
if not user_turn:
@@ -61,6 +63,10 @@ class ProviderManager():
except Exception as e:
logger.error(f"解析 Persona 配置失败:{e}")
if not self.selected_default_persona and len(self.personas) > 0:
# 默认选择第一个
self.selected_default_persona = self.personas[0]
self.provider_insts: List[Provider] = []
'''加载的 Provider 的实例'''
+1 -1
View File
@@ -74,7 +74,7 @@ class Provider(AbstractProvider):
if persistant_history:
# 读取历史记录
try:
for history in db_helper.get_llm_history(provider_type=provider_config['type']):
for history in db_helper.get_llm_history(provider_type=provider_config['id']):
self.session_memory[history.session_id] = json.loads(history.content)
except BaseException as e:
logger.warning(f"读取 LLM 对话历史记录 失败:{e}。仍可正常使用。")
+22 -5
View File
@@ -181,6 +181,9 @@ class ProviderGoogleGenAI(Provider):
)
logger.debug(f"result: {result}")
if "candidates" not in result:
raise Exception("Gemini 返回异常结果: " + str(result))
candidates = result["candidates"][0]['content']['parts']
llm_response = LLMResponse("assistant")
for candidate in candidates:
@@ -190,7 +193,8 @@ class ProviderGoogleGenAI(Provider):
llm_response.role = "tool"
llm_response.tools_call_args.append(candidate['functionCall']['args'])
llm_response.tools_call_name.append(candidate['functionCall']['name'])
llm_response.completion_text = llm_response.completion_text.strip()
return llm_response
@@ -221,11 +225,9 @@ class ProviderGoogleGenAI(Provider):
"messages": context_query,
**self.provider_config.get("model_config", {})
}
llm_response = None
try:
llm_response = await self._query(payloads, func_tool)
await self.save_history(contexts, new_record, session_id, llm_response)
return llm_response
except Exception as e:
if "maximum context length" in str(e):
retry_cnt = 10
@@ -240,8 +242,22 @@ class ProviderGoogleGenAI(Provider):
retry_cnt -= 1
else:
raise e
if retry_cnt == 0:
llm_response = LLMResponse("err", "err: 请尝试 /reset 重置会话")
elif "Function calling is not enabled" in str(e):
logger.info(f"{self.get_model()} 不支持函数调用工具调用,已经自动去除")
if 'tools' in payloads:
del payloads['tools']
llm_response = await self._query(payloads, None)
else:
logger.error(f"发生了错误(gemini_source)。Provider 配置如下: {self.provider_config}")
raise e
if kwargs.get("persist", True) and llm_response:
await self.save_history(contexts, new_record, session_id, llm_response)
return llm_response
async def save_history(self, contexts: List, new_record: dict, session_id: str, llm_response: LLMResponse):
if llm_response.role == "assistant" and session_id:
@@ -260,10 +276,11 @@ class ProviderGoogleGenAI(Provider):
"role": "assistant",
"content": llm_response.completion_text
}]
self.db_helper.update_llm_history(session_id, json.dumps(self.session_memory[session_id]), self.provider_config['type'])
self.db_helper.update_llm_history(session_id, json.dumps(self.session_memory[session_id]), self.provider_config['id'])
async def forget(self, session_id: str) -> bool:
self.session_memory[session_id] = []
self.db_helper.update_llm_history(session_id, json.dumps(self.session_memory[session_id]), self.provider_config['id'])
return True
def get_current_key(self) -> str:
@@ -118,10 +118,11 @@ class LLMTunerModelLoader(Provider):
"role": "assistant",
"content": llm_response.completion_text
}]
self.db_helper.update_llm_history(session_id, json.dumps(self.session_memory[session_id]), self.provider_config['type'])
self.db_helper.update_llm_history(session_id, json.dumps(self.session_memory[session_id]), self.provider_config['id'])
async def forget(self, session_id):
self.session_memory[session_id] = []
self.db_helper.update_llm_history(session_id, json.dumps(self.session_memory[session_id]), self.provider_config['id'])
return True
async def get_current_key(self):
+104 -59
View File
@@ -1,8 +1,8 @@
import base64
import json
import re
import os
from openai import AsyncOpenAI, NOT_GIVEN
from openai import AsyncOpenAI, AsyncAzureOpenAI, NOT_GIVEN
from openai.types.chat.chat_completion import ChatCompletion
from openai._exceptions import NotFoundError
from astrbot.core.utils.io import download_image_by_url
@@ -29,12 +29,24 @@ class ProviderOpenAIOfficial(Provider):
self.chosen_api_key = None
self.api_keys: List = provider_config.get("key", [])
self.chosen_api_key = self.api_keys[0] if len(self.api_keys) > 0 else None
self.client = AsyncOpenAI(
api_key=self.chosen_api_key,
base_url=provider_config.get("api_base", None),
timeout=provider_config.get("timeout", NOT_GIVEN),
)
# 适配 azure openai #332
if "api_version" in provider_config:
# 使用 azure api
self.client = AsyncAzureOpenAI(
api_key=self.chosen_api_key,
api_version=provider_config.get("api_version", None),
base_url=provider_config.get("api_base", None),
timeout=provider_config.get("timeout", NOT_GIVEN),
)
else:
# 使用 openai api
self.client = AsyncOpenAI(
api_key=self.chosen_api_key,
base_url=provider_config.get("api_base", None),
timeout=provider_config.get("timeout", NOT_GIVEN),
)
self.set_model(provider_config['model_config']['model'])
async def get_human_readable_context(self, session_id, page, page_size):
@@ -72,31 +84,21 @@ class ProviderOpenAIOfficial(Provider):
except NotFoundError as e:
raise Exception(f"获取模型列表失败:{e}")
async def pop_record(self, session_id: str, pop_system_prompt: bool = False):
async def pop_record(self, session_id: str):
'''
弹出第一条记录
弹出最早的一个对话
'''
if session_id not in self.session_memory:
raise Exception("会话 ID 不存在")
if len(self.session_memory[session_id]) == 0:
return None
for i in range(len(self.session_memory[session_id])):
# 检查是否是 system prompt
if not pop_system_prompt and self.session_memory[session_id][i]['user']['role'] == "system":
# 如果只有一个 system prompt,才不删掉
f = False
for j in range(i+1, len(self.session_memory[session_id])):
if self.session_memory[session_id][j]['user']['role'] == "system":
f = True
break
if not f:
continue
record = self.session_memory[session_id].pop(i)
break
return record
if len(self.session_memory[session_id]) < 2:
return
try:
self.session_memory[session_id].pop(0)
self.session_memory[session_id].pop(0)
except IndexError:
pass
async def _query(self, payloads: dict, tools: FuncCall) -> LLMResponse:
if tools:
@@ -104,21 +106,11 @@ class ProviderOpenAIOfficial(Provider):
if tool_list:
payloads['tools'] = tool_list
try:
completion = await self.client.chat.completions.create(
**payloads,
stream=False
)
except BaseException as e:
if 'does not support Function Calling' \
or 'does not support tools' in e: # ollama
del payloads['tools']
logger.debug(f"模型 {self.model_name} 不支持 tools,已自动移除")
completion = await self.client.chat.completions.create(
**payloads,
stream=False
)
completion = await self.client.chat.completions.create(
**payloads,
stream=False
)
assert isinstance(completion, ChatCompletion)
logger.debug(f"completion: {completion}")
@@ -129,14 +121,8 @@ class ProviderOpenAIOfficial(Provider):
if choice.message.content:
# text completion
completion_text = str(choice.message.content).strip()
# 适配 deepseek-r1 模型
if r'<think>' in completion_text:
completion_text = re.sub(r'<think>.*?</think>', '', completion_text, flags=re.DOTALL).strip()
# 可能有单标签情况
completion_text = completion_text.replace(r'<think>', '').replace(r'</think>', '').strip()
return LLMResponse("assistant", completion_text)
return LLMResponse("assistant", completion_text, raw_completion=completion)
elif choice.message.tool_calls:
# tools call (function calling)
args_ls = []
@@ -147,8 +133,9 @@ class ProviderOpenAIOfficial(Provider):
args = json.loads(tool_call.function.arguments)
args_ls.append(args)
func_name_ls.append(tool_call.function.name)
return LLMResponse(role="tool", tools_call_args=args_ls, tools_call_name=func_name_ls)
return LLMResponse(role="tool", tools_call_args=args_ls, tools_call_name=func_name_ls, raw_completion=completion)
else:
logger.error(f"API 返回的 completion 无法解析:{completion}")
raise Exception("Internal Error")
async def text_chat(
@@ -178,19 +165,17 @@ class ProviderOpenAIOfficial(Provider):
"messages": context_query,
**self.provider_config.get("model_config", {})
}
llm_response = None
try:
llm_response = await self._query(payloads, func_tool)
if kwargs.get("persist", True):
await self.save_history(contexts, new_record, session_id, llm_response)
return llm_response
except Exception as e:
if "maximum context length" in str(e):
# 重试 10 次
retry_cnt = 10
while retry_cnt > 0:
logger.warning(f"请求失败:{e}上下文长度超过限制。尝试弹出最早的记录然后重试。")
logger.warning("上下文长度超过限制。尝试弹出最早的记录然后重试。")
try:
self.pop_record(session_id)
await self.pop_record(session_id)
llm_response = await self._query(payloads, func_tool)
break
except Exception as e:
@@ -198,8 +183,67 @@ class ProviderOpenAIOfficial(Provider):
retry_cnt -= 1
else:
raise e
if retry_cnt == 0:
llm_response = LLMResponse("err", "err: 请尝试 /reset 清除会话记录。")
elif "The model is not a VLM" in str(e): # siliconcloud
# 尝试删除所有 image
new_contexts = await self._remove_image_from_context(context_query)
payloads['messages'] = new_contexts
llm_response = await self._query(payloads, func_tool)
# openai, ollama, gemini openai, siliconcloud 的错误提示与 code 不统一,只能通过字符串匹配
elif 'does not support Function Calling' in str(e) \
or 'does not support tools' in str(e) \
or 'Function call is not supported' in str(e) \
or 'Function calling is not enabled' in str(e) \
or 'Tool calling is not supported' in str(e): # siliconcloud
logger.info(f"{self.get_model()} 不支持函数调用工具调用,已经自动去除")
if 'tools' in payloads:
del payloads['tools']
llm_response = await self._query(payloads, None)
else:
logger.error(f"发生了错误。Provider 配置如下: {self.provider_config}")
if 'tool' in str(e).lower() and 'support' in str(e).lower():
logger.error(f"疑似该模型不支持函数调用工具调用。请输入 /tool off_all")
if 'Connection error.' in str(e):
proxy = os.environ.get("http_proxy", None)
if proxy:
logger.error(f"可能为代理原因,请检查代理是否正常。当前代理: {proxy}")
raise e
if kwargs.get("persist", True) and llm_response:
await self.save_history(contexts, new_record, session_id, llm_response)
return llm_response
async def _remove_image_from_context(self, contexts: List):
'''
从上下文中删除所有带有 image 的记录
'''
new_contexts = []
flag = False
for context in contexts:
if flag:
flag = False # 删除 image 后,下一条(LLM 响应)也要删除
continue
if isinstance(context['content'], list):
flag = True
# continue
new_content = []
for item in context['content']:
if isinstance(item, dict) and 'image_url' in item:
continue
new_content.append(item)
if not new_content:
# 用户只发了图片
new_content = [{"type": "text", "text": "[图片]"}]
context['content'] = new_content
new_contexts.append(context)
return new_contexts
async def save_history(self, contexts: List, new_record: dict, session_id: str, llm_response: LLMResponse):
@@ -219,10 +263,11 @@ class ProviderOpenAIOfficial(Provider):
"role": "assistant",
"content": llm_response.completion_text
}]
self.db_helper.update_llm_history(session_id, json.dumps(self.session_memory[session_id]), self.provider_config['type'])
self.db_helper.update_llm_history(session_id, json.dumps(self.session_memory[session_id]), self.provider_config['id'])
async def forget(self, session_id: str) -> bool:
self.session_memory[session_id] = []
self.db_helper.update_llm_history(session_id, json.dumps(self.session_memory[session_id]), self.provider_config['id'])
return True
def get_current_key(self) -> str:
@@ -15,7 +15,7 @@ class ProviderOpenAITTSAPI(TTSProvider):
) -> None:
super().__init__(provider_config, provider_settings)
self.chosen_api_key = provider_config.get("api_key", "")
self.voice = provider_config.get("voice", "alloy")
self.voice = provider_config.get("openai-tts-voice", "alloy")
self.client = AsyncOpenAI(
api_key=self.chosen_api_key,
+1 -1
View File
@@ -20,6 +20,6 @@ class PermissionTypeFilter(HandlerFilter):
if self.permission_type == PermissionType.ADMIN:
if not event.is_admin():
event.stop_event()
raise ValueError(f"您 (ID: {event.get_sender_id()}) 没有权限执行此操作。")
raise ValueError(f"您 (ID: {event.get_sender_id()}) 没有权限操作管理员指令")
return True
+2
View File
@@ -7,6 +7,7 @@ from .star_handler import (
register_regex,
register_permission_type,
register_on_llm_request,
register_on_llm_response,
register_llm_tool,
register_on_decorating_result,
register_after_message_sent
@@ -21,6 +22,7 @@ __all__ = [
'register_regex',
'register_permission_type',
'register_on_llm_request',
'register_on_llm_response',
'register_llm_tool',
'register_on_decorating_result',
'register_after_message_sent'
+32 -5
View File
@@ -17,7 +17,7 @@ def get_handler_full_name(awaitable: Awaitable) -> str:
'''获取 Handler 的全名'''
return f"{awaitable.__module__}_{awaitable.__name__}"
def get_handler_or_create(handler: Awaitable, event_type: EventType, dont_add = False) -> StarHandlerMetadata:
def get_handler_or_create(handler: Awaitable, event_type: EventType, dont_add = False, **kwargs) -> StarHandlerMetadata:
'''获取 Handler 或者创建一个新的 Handler'''
handler_full_name = get_handler_full_name(handler)
md = star_handlers_registry.get_handler_by_full_name(handler_full_name)
@@ -30,14 +30,17 @@ def get_handler_or_create(handler: Awaitable, event_type: EventType, dont_add =
handler_name=handler.__name__,
handler_module_path=handler.__module__,
handler=handler,
event_filters=[]
event_filters=[],
)
if handler.__doc__:
md.desc = handler.__doc__.strip()
if not dont_add:
star_handlers_registry.append(md)
return md
def register_command(command_name: str = None, *args):
'''注册一个 Command'''
'''注册一个 Command.
'''
new_command = None
add_to_event_filters = False
@@ -61,8 +64,9 @@ def register_command(command_name: str = None, *args):
return decorator
def register_command_group(command_group_name: str = None, *args):
'''注册一个 CommandGroup'''
def register_command_group(command_group_name: str = None, desc: str = "", *args):
'''注册一个 CommandGroup
'''
new_group = None
add_to_event_filters = False
@@ -139,6 +143,8 @@ def register_on_llm_request():
Examples:
```py
from astrbot.api.provider import ProviderRequest
@on_llm_request()
async def test(self, event: AstrMessageEvent, request: ProviderRequest) -> None:
request.system_prompt += "你是一个猫娘..."
@@ -152,6 +158,27 @@ def register_on_llm_request():
return decorator
def register_on_llm_response():
'''当有 LLM 请求后的事件
Examples:
```py
from astrbot.api.provider import LLMResponse
@on_llm_response()
async def test(self, event: AstrMessageEvent, response: LLMResponse) -> None:
...
```
请务必接收两个参数:event, request
'''
def decorator(awaitable):
_ = get_handler_or_create(awaitable, EventType.OnLLMResponseEvent)
return awaitable
return decorator
def register_llm_tool(name: str = None):
'''为函数调用(function-calling / tools-use)添加工具。
+1
View File
@@ -47,6 +47,7 @@ class EventType(enum.Enum):
'''
AdapterMessageEvent = enum.auto() # 收到适配器发来的消息
OnLLMRequestEvent = enum.auto() # 收到 LLM 请求(可以是用户也可以是插件)
OnLLMResponseEvent = enum.auto() # LLM 响应后
OnDecoratingResultEvent = enum.auto() # 发送消息前
OnCallingFuncToolEvent = enum.auto() # 调用函数工具
OnAfterMessageSentEvent = enum.auto() # 发送消息后
+26 -2
View File
@@ -19,6 +19,8 @@ from .star import star_registry, star_map
from .star_handler import star_handlers_registry
from astrbot.core.provider.register import llm_tools
from .filter.permission import PermissionTypeFilter, PermissionType
class PluginManager:
def __init__(
self,
@@ -159,6 +161,8 @@ class PluginManager:
inactivated_plugins: list = sp.get("inactivated_plugins", [])
inactivated_llm_tools: list = sp.get("inactivated_llm_tools", [])
alter_cmd = sp.get("alter_cmd", {})
# 导入插件模块,并尝试实例化插件类
for plugin_module in plugin_modules:
try:
@@ -213,12 +217,12 @@ class PluginManager:
metadata.root_dir_name = root_dir_name
metadata.reserved = reserved
# 绑定 handler
related_handlers = star_handlers_registry.get_handlers_by_module_name(metadata.module_path)
for handler in related_handlers:
logger.debug(f"bind handler {handler.handler_name} to {metadata.name}")
# handler.handler.__self__ = star_metadata.star_cls # 绑定 handler 的 self
handler.handler = functools.partial(handler.handler, metadata.star_cls)
# llm_tool
# 绑定 llm_tool handler
for func_tool in llm_tools.func_list:
if func_tool.handler.__module__ == metadata.module_path:
func_tool.handler_module_path = metadata.module_path
@@ -253,9 +257,29 @@ class PluginManager:
star_registry.append(metadata)
logger.debug(f"插件 {root_dir_name} 载入成功。")
# 禁用/启用插件
if metadata.module_path in inactivated_plugins:
metadata.activated = False
# 检查并且植入自定义的权限过滤器(alter_cmd)
for handler in star_handlers_registry.get_handlers_by_module_name(metadata.module_path):
if metadata.name in alter_cmd and handler.handler_name in alter_cmd[metadata.name]:
# 注入权限过滤器
cmd_type = alter_cmd[metadata.name][handler.handler_name].get("permission", "member")
found_permission_filter = False
for filter_ in handler.event_filters:
if isinstance(filter_, PermissionTypeFilter):
if cmd_type == "admin":
filter_.permission_type = PermissionType.ADMIN
else:
filter_.permission_type = PermissionType.MEMBER
found_permission_filter = True
break
if not found_permission_filter:
handler.event_filters.append(PermissionTypeFilter(PermissionType.ADMIN if cmd_type == "admin" else PermissionType.MEMBER))
logger.debug(f"插入权限过滤器 {cmd_type}{metadata.name}{handler.handler_name} 方法。")
# 执行 initialize() 方法
if hasattr(metadata.star_cls, "initialize"):
await metadata.star_cls.initialize()
+1 -1
View File
@@ -109,7 +109,7 @@ async def download_file(url: str, path: str, show_progress: bool = False):
'''
try:
async with aiohttp.ClientSession(trust_env=True) as session:
async with session.get(url, timeout=120) as resp:
async with session.get(url, timeout=1800) as resp:
if resp.status != 200:
raise Exception(f"下载文件失败: {resp.status}")
total_size = int(resp.headers.get('content-length', 0))
+26 -9
View File
@@ -25,15 +25,29 @@ class PluginRoute(Route):
self.register_routes()
async def get_online_plugins(self):
url = "https://soulter.github.io/AstrBot_Plugins_Collection/plugins.json"
try:
async with aiohttp.ClientSession(trust_env=True) as session:
async with session.get(url) as response:
result = await response.json()
return Response().ok(result).__dict__
except Exception as e:
logger.error(f"获取插件列表失败:{e}")
return Response().error(str(e)).__dict__
custom = request.args.get("custom_registry")
if custom:
urls = [custom]
else:
urls = [
"https://soulter.github.io/AstrBot_Plugins_Collection/plugins.json",
"https://api.soulter.top/astrbot/plugins"
]
for url in urls:
try:
async with aiohttp.ClientSession(trust_env=True) as session:
async with session.get(url) as response:
if response.status == 200:
result = await response.json()
return Response().ok(result).__dict__
else:
logger.error(f"请求 {url} 失败,状态码:{response.status}")
except Exception as e:
logger.error(f"请求 {url} 失败,错误:{e}")
return Response().error("获取插件列表失败").__dict__
async def get_plugins(self):
_plugin_resp = []
@@ -56,6 +70,7 @@ class PluginRoute(Route):
try:
logger.info(f"正在安装插件 {repo_url}")
await self.plugin_manager.install_plugin(repo_url)
self.core_lifecycle.restart()
logger.info(f"安装插件 {repo_url} 成功。")
return Response().ok(None, "安装成功。").__dict__
except Exception as e:
@@ -70,6 +85,7 @@ class PluginRoute(Route):
file_path = f"data/temp/{file.filename}"
await file.save(file_path)
await self.plugin_manager.install_plugin_from_file(file_path)
self.core_lifecycle.restart()
logger.info(f"安装插件 {file.filename} 成功")
return Response().ok(None, "安装成功。").__dict__
except Exception as e:
@@ -94,6 +110,7 @@ class PluginRoute(Route):
try:
logger.info(f"正在更新插件 {plugin_name}")
await self.plugin_manager.update_plugin(plugin_name)
self.core_lifecycle.restart()
logger.info(f"更新插件 {plugin_name} 成功。")
return Response().ok(None, "更新成功。").__dict__
except Exception as e:
+4
View File
@@ -6,6 +6,10 @@ class StaticFileRoute(Route):
index_ = ['/', '/auth/login', '/config', '/logs', '/extension', '/dashboard/default', '/project-atri', '/console', '/chat']
for i in index_:
self.app.add_url_rule(i, view_func=self.index)
@self.app.errorhandler(404)
async def page_not_found(e):
return "404 Not found。如果你初次使用打开面板发现 404,请参考文档: https://astrbot.app/deploy/dashboard-404.html"
async def index(self):
return await self.app.send_static_file('index.html')
+11
View File
@@ -0,0 +1,11 @@
# What's Changed
- [beta] 支持群聊内基于概率的主动回复
- openai tts 更换模型 #300
- 增加模型响应后的插件钩子
- 修复 相同type的provider共享了记忆
- 优化 人格情景在发现格式不对时仍然加载而不是跳过 #282
- 修复 Gemini函数调用时,parameters为空对象导致的错误 by @Camreishi
- 修复 弹出记录报错的问题 #272
- 优化 移除默认人格
- 优化 未启用模型提供商时的异常处理
+12
View File
@@ -0,0 +1,12 @@
# What's Changed
- fix: 修复主动概率回复关闭后仍然回复的问题 #317
- fix: 尝试修复 gewechat 群聊收不到 at 的回复 #294
- perf: 移除了默认人格
- fix: 修复HTTP代理删除后不生效 #319
- fix: 调用Gemini API输出多余空行问题 #318
- feat: 添加硅基流动模版
- fix: 硅基流动 not a vlm 和 tool calling not supported 报错 #305 #291
- perf: 回复时艾特发送者之后添加空格或换行 #312
- fix: docker容器内时区不对导致 reminder 时间错误
- perf: siliconcloud 不支持 tool 的模型
+13
View File
@@ -0,0 +1,13 @@
# What's Changed
1. 支持接入企业微信(测试)
2. 修复速率限制不可用的问题
3. gewechat 回调接口默认暴露在所有 IP
4. 适配 Azure OpenAI
5. 修复请求 gemini 出现 KeyError 'candidates' 的错误
6. 将 /reset /persona 挪入管理员指令 #308
7. 支持通过 /alter_cmd 设置所有指令是否只能管理员操作
8. /plugin 指令支持查看插件注册的指令和指令组
9. 插件注册指令支持传入指令的描述以方便 /plugin 查看。需要写在函数的第一行的 docstring 中。
10. 修复 schema 中 object hint 不显示 #290
11. feat: 优化插件市场的访问速度
+4 -2
View File
@@ -4,6 +4,8 @@ services:
astrbot:
image: soulter/astrbot:latest
container_name: astrbot
network_mode: "host"
ports:
- "6180-6200:6180-6200"
- "11451:11451"
volumes:
- ./data:/AstrBot/data
- ./data:/AstrBot/data
@@ -4,7 +4,7 @@
</h3>
<v-card-text>
<div v-for="(index, key) in iterable" :key="key" style="margin-bottom: 0.5px;" v-if="metadata[metadataKey]?.type === 'object' || metadata[metadataKey]?.config_template">
<v-alert v-if="metadata[metadataKey].items[key]?.obvious_hint && metadata[metadataKey].items[key]?.hint && metadata[metadataKey].items[key]?.type !== 'object'" style="margin-bottom: 16px"
<v-alert v-if="metadata[metadataKey].items[key]?.obvious_hint && metadata[metadataKey].items[key]?.hint" style="margin-bottom: 16px"
:text="metadata[metadataKey].items[key]?.hint" :title="'💡 关于' + metadata[metadataKey].items[key]?.description"
type="info" variant="tonal">
</v-alert>
@@ -52,7 +52,7 @@
</div>
<div
v-if="!metadata[metadataKey].items[key]?.obvious_hint && metadata[metadataKey].items[key]?.hint && metadata[metadataKey].items[key]?.type !== 'object' && !metadata[metadataKey].items[key]?.invisible">
v-if="!metadata[metadataKey].items[key]?.obvious_hint && metadata[metadataKey].items[key]?.hint && !metadata[metadataKey].items[key]?.invisible">
<v-btn icon size="x-small" style="margin-bottom: 22px;">
<v-icon size="x-small">mdi-help</v-icon>
<v-tooltip activator="parent" location="start">{{ metadata[metadataKey].items[key]?.hint
@@ -63,7 +63,7 @@
</div>
<div v-else>
<v-alert v-if="metadata[metadataKey]?.obvious_hint && metadata[metadataKey]?.hint && metadata[metadataKey]?.type !== 'object'" style="margin-bottom: 16px"
<v-alert v-if="metadata[metadataKey]?.obvious_hint && metadata[metadataKey]?.hint" style="margin-bottom: 16px"
:text="metadata[metadataKey]?.hint" :title="'💡 关于' + metadata[metadataKey]?.description"
type="info" variant="tonal">
</v-alert>
@@ -106,7 +106,7 @@
</div>
<div
v-if="!metadata[metadataKey]?.obvious_hint && metadata[metadataKey]?.hint && metadata[metadataKey]?.type !== 'object' && !metadata[metadataKey]?.invisible">
v-if="!metadata[metadataKey]?.obvious_hint && metadata[metadataKey]?.hint && !metadata[metadataKey]?.invisible">
<v-btn icon size="x-small" style="margin-bottom: 22px;">
<v-icon size="x-small">mdi-help</v-icon>
<v-tooltip activator="parent" location="start">{{ metadata[metadataKey]?.hint
+5 -1
View File
@@ -37,7 +37,11 @@ import axios from 'axios';
</v-col>
<v-col cols="12" md="12">
<div style="background-color: white; width: 100%; padding: 16px; border-radius: 10px;">
<h3>🧩 插件市场</h3>
<div style="display: flex; align-items: center;">
<h3>🧩 插件市场</h3>
<small style="margin-left: 16px;">如无法显示请打开 <a href="https://soulter.github.io/AstrBot_Plugins_Collection/plugins.json">链接</a> 复制想安装插件对应的 `repo` 链接然后点击右下角 + 号安装或打开链接下载压缩包安装</small>
</div>
</div>
</v-col>
<v-col cols="12" md="6" lg="4" v-for="plugin in pluginMarketData">
+48 -8
View File
@@ -1,5 +1,6 @@
import datetime
import uuid
import random
import astrbot.api.star as star
from astrbot.api.event import AstrMessageEvent
from astrbot.api.platform import MessageType
@@ -8,7 +9,9 @@ from astrbot.api.message_components import Plain, Image
from astrbot import logger
from collections import defaultdict
'''
聊天记忆增强
'''
class LongTermMemory:
def __init__(self, config: dict, context: star.Context):
self.config = config
@@ -23,6 +26,14 @@ class LongTermMemory:
self.image_caption = self.config["image_caption"]
self.image_caption_prompt = self.config["image_caption_prompt"]
self.active_reply = self.config["active_reply"]
self.enable_active_reply = self.active_reply.get("enable", False)
self.ar_method = self.active_reply["method"]
self.ar_possibility = self.active_reply["possibility_reply"]
self.ar_prompt = self.active_reply.get("prompt", "")
self.put_history_to_prompt = self.config["put_history_to_prompt"]
async def remove_session(self, event: AstrMessageEvent) -> int:
cnt = 0
if event.unified_msg_origin in self.session_chats:
@@ -39,8 +50,27 @@ class LongTermMemory:
persist=False,
)
return response.completion_text
async def need_active_reply(self, event: AstrMessageEvent) -> bool:
if not self.enable_active_reply:
return False
if event.get_message_type() != MessageType.GROUP_MESSAGE:
return False
if event.is_at_or_wake_command:
# if the message is a command, let it pass
return False
match self.ar_method:
case "possibility_reply":
trig = random.random() < self.ar_possibility
return trig
return False
async def handle_message(self, event: AstrMessageEvent):
'''仅支持群聊'''
if event.get_message_type() == MessageType.GROUP_MESSAGE:
datetime_str = datetime.datetime.now().strftime("%H:%M:%S")
@@ -59,23 +89,33 @@ class LongTermMemory:
final_message += f" [Image: {caption}]"
except Exception as e:
logger.error(f"获取图片描述失败: {e}")
else:
final_message += " [Image]"
logger.debug(f"ltm | {event.unified_msg_origin} | {final_message}")
self.session_chats[event.unified_msg_origin].append(final_message)
if len(self.session_chats[event.unified_msg_origin]) > self.max_cnt:
self.session_chats[event.unified_msg_origin].pop(0)
async def on_req_llm(self, event: AstrMessageEvent, req: ProviderRequest):
'''当触发 LLM 请求前,调用此方法修改 req'''
if event.unified_msg_origin not in self.session_chats:
return
chats_str = '\n---\n'.join(self.session_chats[event.unified_msg_origin])
req.system_prompt += "You are now in a chatroom. The chat history is as follows: \n"
req.system_prompt += chats_str
if self.image_caption:
req.system_prompt += (
"The images sent by the members are displayed in text form above."
)
if self.put_history_to_prompt:
prompt = req.prompt
req.prompt = f"You are now in a chatroom. The chat history is as follows:\n{chats_str}"
req.prompt += f"\nNow, a new message is coming: `{prompt}`. Please react to it. Only output your response and do not output any other information."
req.contexts = [] # 清空上下文,当使用了群聊增强,所有聊天记录都在一个prompt中。
else:
req.system_prompt += "You are now in a chatroom. The chat history is as follows: \n"
req.system_prompt += chats_str
if self.image_caption:
req.system_prompt += (
"The images sent by the members are displayed in text form above."
)
async def after_req_llm(self, event: AstrMessageEvent):
if event.unified_msg_origin not in self.session_chats:
return
+190 -30
View File
@@ -5,11 +5,14 @@ import astrbot.api.star as star
import astrbot.api.event.filter as filter
from astrbot.api.event import AstrMessageEvent, MessageEventResult
from astrbot.api import sp
from astrbot.api.provider import Personality, ProviderRequest
from astrbot.api.platform import MessageType
from astrbot.api.provider import Personality, ProviderRequest, LLMResponse
from astrbot.core.utils.io import download_dashboard, get_dashboard_version
from astrbot.core.star.star_handler import star_handlers_registry, StarHandlerMetadata
from astrbot.core.star.star import star_map
from astrbot.core.star.filter.command import CommandFilter
from astrbot.core.star.filter.command_group import CommandGroupFilter
from astrbot.core.star.filter.permission import PermissionTypeFilter
from astrbot.core.config.default import VERSION
from collections import defaultdict
from .long_term_memory import LongTermMemory
from astrbot.core import logger
@@ -25,7 +28,7 @@ class Main(star.Star):
self.enable_datetime = cfg['provider_settings']["datetime_system_prompt"]
self.ltm = None
if self.context.get_config()['provider_ltm_settings']['group_icl_enable']:
if self.context.get_config()['provider_ltm_settings']['group_icl_enable'] or self.context.get_config()['provider_ltm_settings']['active_reply']['enable']:
try:
self.ltm = LongTermMemory(self.context.get_config()['provider_ltm_settings'], self.context)
except BaseException as e:
@@ -41,6 +44,7 @@ class Main(star.Star):
@filter.command("help")
async def help(self, event: AstrMessageEvent):
'''查看帮助'''
notice = ""
try:
notice = await self._query_astrbot_notice()
@@ -50,31 +54,32 @@ class Main(star.Star):
dashboard_version = await get_dashboard_version()
msg = f"""AstrBot v{VERSION}(WebUI: {dashboard_version})
已注册的 AstrBot 内置指令:
AstrBot 指令:
[System]
/plugin: 查看注册的插件、插件帮助
/t2i: 开启/关闭文本转图片模式
/sid: 获取当前会话 ID
/plugin: 查看插件、插件帮助
/t2i: 开文本转图片
/sid: 获取会话 ID
/op <admin_id>: 授权管理员
/deop <admin_id>: 取消管理员
/wl <sid>: 添加会话白名单
/dwl <sid>: 删除会话白名单
/wl <sid>: 添加白名单
/dwl <sid>: 删除白名单
/dashboard_update: 更新管理面板
/alter_cmd: 设置指令权限
[大模型]
/provider: 查看、切换大模型提供商
/model: 查看、切换提供商模型列表
/key: 查看、切换 API Key
/provider: 大模型提供商
/model: 模型列表
/key: API Key
/reset: 重置 LLM 会话
/history: 获取会话历史记录
/persona: 情境人格设置
/tool ls: 查看、激活、停用当前注册的函数工具
/history: 对话记录
/persona: 人格情景
/tool ls: 函数工具
[其他]
/set <变量名> <值>: 为当前会话定义一个变量。适用于 Dify 工作流输入。
/unset <变量名>: 删除当前会话的变量。
/set <变量名> <值>: 为会话定义一个变量。适用于 Dify 工作流输入。
/unset <变量名>: 删除会话的变量。
提示:如要查看插件指令,请输入 /plugin 查看具体信息。
提示:如要查看插件指令,请输入 /plugin 查看具体信息。
{notice}"""
event.set_result(MessageEventResult().message(msg).use_t2i(False))
@@ -91,7 +96,7 @@ class Main(star.Star):
active = " (启用)" if tool.active else "(停用)"
msg += f"- {tool.name}: {tool.description} {active}\n"
msg += "\n使用 /tool on/off <工具名> 激活或者停用工具。"
msg += "\n使用 /tool on/off <工具名> 激活或者停用函数工具。/tool off_all 停用所有函数工具。"
event.set_result(MessageEventResult().message(msg).use_t2i(False))
@tool.command("on")
@@ -107,6 +112,13 @@ class Main(star.Star):
event.set_result(MessageEventResult().message(f"停用工具 {tool_name} 成功。"))
else:
event.set_result(MessageEventResult().message(f"停用工具 {tool_name} 失败,未找到此工具。"))
@tool.command("off_all")
async def tool_all_off(self, event: AstrMessageEvent):
tm = self.context.get_llm_tool_manager()
for tool in tm.func_list:
self.context.deactivate_llm_tool(tool.name)
event.set_result(MessageEventResult().message(f"停用所有工具成功。"))
@filter.command("plugin")
async def plugin(self, event: AstrMessageEvent, oper1: str = None, oper2: str = None):
@@ -117,7 +129,7 @@ class Main(star.Star):
if plugin_list_info.strip() == "":
plugin_list_info = "没有加载任何插件。"
plugin_list_info += "\n使用 /plugin <插件名> 查看插件帮助。\n使用 /plugin on/off <插件名> 启用或者禁用插件。"
plugin_list_info += "\n使用 /plugin <插件名> 查看插件帮助和加载的指令\n使用 /plugin on/off <插件名> 启用或者禁用插件。"
event.set_result(MessageEventResult().message(f"{plugin_list_info}").use_t2i(False))
else:
if oper1 == "off":
@@ -140,10 +152,34 @@ class Main(star.Star):
plugin = self.context.get_registered_star(oper1)
if plugin is None:
event.set_result(MessageEventResult().message("未找到此插件。"))
else:
help_msg = plugin.star_cls.__doc__ if plugin.star_cls.__doc__ else "该插件未提供帮助信息"
ret = f"插件 {oper1} 帮助信息:\n" + help_msg
event.set_result(MessageEventResult().message(ret).use_t2i(False))
return
help_msg = plugin.star_cls.__doc__ if plugin.star_cls.__doc__ else "帮助信息: 未提供"
help_msg += f"\n\n作者: {plugin.author}\n版本: {plugin.version}"
command_handlers = []
command_names = []
for handler in star_handlers_registry:
assert isinstance(handler, StarHandlerMetadata)
if handler.handler_module_path != plugin.module_path:
continue
for filter_ in handler.event_filters:
if isinstance(filter_, CommandFilter):
command_handlers.append(handler)
command_names.append(filter_.command_name)
break
elif isinstance(filter_, CommandGroupFilter):
command_handlers.append(handler)
command_names.append(filter_.group_name)
if len(command_handlers) > 0:
help_msg += "\n\n指令列表:\n"
for i in range(len(command_handlers)):
help_msg += f"{command_names[i]}: {command_handlers[i].desc}\n"
help_msg += "\nTip: 指令的触发需要添加唤醒前缀,默认为 /。"
ret = f"插件 {oper1} 帮助信息:\n" + help_msg
ret += "更多帮助信息请查看插件仓库 README。"
event.set_result(MessageEventResult().message(ret).use_t2i(False))
@filter.command("t2i")
async def t2i(self, event: AstrMessageEvent):
@@ -203,6 +239,10 @@ UID: {user_id} 此 ID 可用于设置管理员。/op <UID> 授权管理员, /deo
async def provider(self, event: AstrMessageEvent, idx: int = None):
'''查看或者切换 LLM Provider'''
if not self.context.get_using_provider():
event.set_result(MessageEventResult().message("未找到任何 LLM 提供商。请先配置。"))
return
if idx is None:
ret = "## 当前载入的 LLM 提供商\n"
for idx, llm in enumerate(self.context.get_all_providers()):
@@ -225,8 +265,14 @@ UID: {user_id} 此 ID 可用于设置管理员。/op <UID> 授权管理员, /deo
event.set_result(MessageEventResult().message(f"成功切换到 {id_}"))
@filter.permission_type(filter.PermissionType.ADMIN)
@filter.command("reset")
async def reset(self, message: AstrMessageEvent):
if not self.context.get_using_provider():
message.set_result(MessageEventResult().message("未找到任何 LLM 提供商。请先配置。"))
return
await self.context.get_using_provider().forget(message.session_id)
ret = "清除会话 LLM 聊天历史成功。"
if self.ltm:
@@ -237,6 +283,12 @@ UID: {user_id} 此 ID 可用于设置管理员。/op <UID> 授权管理员, /deo
@filter.command("model")
async def model_ls(self, message: AstrMessageEvent, idx_or_name: Union[int, str] = None):
if not self.context.get_using_provider():
message.set_result(MessageEventResult().message("未找到任何 LLM 提供商。请先配置。"))
return
if idx_or_name is None:
models = []
try:
@@ -277,6 +329,12 @@ UID: {user_id} 此 ID 可用于设置管理员。/op <UID> 授权管理员, /deo
@filter.command("history")
async def his(self, message: AstrMessageEvent, page: int = 1):
if not self.context.get_using_provider():
message.set_result(MessageEventResult().message("未找到任何 LLM 提供商。请先配置。"))
return
size_per_page = 3
contexts, total_pages = await self.context.get_using_provider().get_human_readable_context(message.session_id, page, size_per_page)
@@ -296,6 +354,10 @@ UID: {user_id} 此 ID 可用于设置管理员。/op <UID> 授权管理员, /deo
@filter.permission_type(filter.PermissionType.ADMIN)
@filter.command("key")
async def key(self, message: AstrMessageEvent, index: int=None):
if not self.context.get_using_provider():
message.set_result(MessageEventResult().message("未找到任何 LLM 提供商。请先配置。"))
return
if index is None:
keys_data = self.context.get_using_provider().get_keys()
@@ -322,8 +384,15 @@ UID: {user_id} 此 ID 可用于设置管理员。/op <UID> 授权管理员, /deo
MessageEventResult().message("切换 Key 未知错误: "+str(e)))
message.set_result(MessageEventResult().message("切换 Key 成功。"))
@filter.permission_type(filter.PermissionType.ADMIN)
@filter.command("persona")
async def persona(self, message: AstrMessageEvent):
if not self.context.get_using_provider():
message.set_result(MessageEventResult().message("未找到任何 LLM 提供商。请先配置。"))
return
l = message.message_str.split(" ")
curr_persona_name = ""
@@ -337,6 +406,7 @@ UID: {user_id} 此 ID 可用于设置管理员。/op <UID> 授权管理员, /deo
- 设置人格情景: `/persona 人格名`, 如 /persona 编剧
- 人格情景列表: `/persona list`
- 人格情景详细信息: `/persona view 人格名`
- 取消人格: `/persona unset`
当前人格情景: {curr_persona_name}
@@ -362,6 +432,9 @@ UID: {user_id} 此 ID 可用于设置管理员。/op <UID> 授权管理员, /deo
else:
msg = f"人格{ps}不存在"
message.set_result(MessageEventResult().message(msg))
elif l[1] == "unset":
self.context.get_using_provider().curr_personality = None
message.set_result(MessageEventResult().message("取消人格成功。"))
else:
ps = "".join(l[1:]).strip()
if persona := next(builtins.filter(
@@ -421,12 +494,39 @@ UID: {user_id} 此 ID 可用于设置管理员。/op <UID> 授权管理员, /deo
@filter.platform_adapter_type(filter.PlatformAdapterType.ALL)
async def on_message(self, event: AstrMessageEvent):
'''长期记忆'''
'''群聊记忆增强'''
if self.ltm:
try:
await self.ltm.handle_message(event)
except BaseException as e:
logger.error(e)
need_active = await self.ltm.need_active_reply(event)
group_icl_enable = self.context.get_config()['provider_ltm_settings']['group_icl_enable']
if group_icl_enable:
'''记录对话'''
try:
await self.ltm.handle_message(event)
except BaseException as e:
logger.error(e)
if need_active:
'''主动回复'''
provider = self.context.get_using_provider()
if not provider:
logger.error("未找到任何 LLM 提供商。请先配置。无法主动回复")
return
try:
session_provider_context = provider.session_memory.get(event.session_id)
prompt = self.ltm.ar_prompt
if not prompt:
prompt = event.message_str
yield event.request_llm(
prompt=prompt,
func_tool_manager=self.context.get_llm_tool_manager(),
session_id=event.session_id,
contexts=session_provider_context if session_provider_context else []
)
except BaseException as e:
logger.error(f"主动回复失败: {e}")
@filter.on_llm_request()
@@ -469,6 +569,66 @@ UID: {user_id} 此 ID 可用于设置管理员。/op <UID> 授权管理员, /deo
await self.ltm.after_req_llm(event)
except BaseException as e:
logger.error(f"ltm: {e}")
@filter.permission_type(filter.PermissionType.ADMIN)
@filter.command("alter_cmd")
async def alter_cmd(self, event: AstrMessageEvent):
# token = event.message_str.split(" ")
token = self.parse_commands(event.message_str)
if token.len < 2:
yield event.plain_result("可设置所有其他指令是否需要管理员权限。\n格式: /alter_cmd <cmd_name> <admin/member>\n 例如: /alter_cmd provider admin 将 provider 设置为管理员指令")
return
cmd_name = token.get(1)
cmd_type = token.get(2)
if cmd_type not in ["admin", "member"]:
yield event.plain_result("指令类型错误,可选类型有 admin, member")
return
# 查找指令
found_command = None
for handler in star_handlers_registry:
assert isinstance(handler, StarHandlerMetadata)
for filter_ in handler.event_filters:
if isinstance(filter_, CommandFilter):
if filter_.command_name == cmd_name:
found_command = handler
break
elif isinstance(filter_, CommandGroupFilter):
if cmd_name == filter_.group_name:
found_command = handler
break
if not found_command:
yield event.plain_result("未找到该指令")
return
found_plugin = star_map[found_command.handler_module_path]
alter_cmd_cfg = sp.get("alter_cmd", {})
plugin_ = alter_cmd_cfg.get(found_plugin.name, {})
cfg = plugin_.get(found_command.handler_name, {})
cfg["permission"] = cmd_type
plugin_[found_command.handler_name] = cfg
alter_cmd_cfg[found_plugin.name] = plugin_
sp.put("alter_cmd", alter_cmd_cfg)
# 注入权限过滤器
found_permission_filter = False
for filter_ in found_command.event_filters:
if isinstance(filter_, PermissionTypeFilter):
if cmd_type == "admin":
filter_.permission_type = filter.PermissionType.ADMIN
else:
filter_.permission_type = filter.PermissionType.MEMBER
found_permission_filter = True
break
if not found_permission_filter:
found_command.event_filters.insert(0, PermissionTypeFilter(filter.PermissionType.ADMIN if cmd_type == "admin" else filter.PermissionType.MEMBER))
yield event.plain_result(f"已将 {cmd_name} 设置为 {cmd_type} 指令")
# @filter.command_group("kdb")
# def kdb(self):
+2 -1
View File
@@ -8,7 +8,8 @@ from typing import List
class Google(SearchEngine):
def __init__(self) -> None:
super().__init__()
self.proxy = os.environ.get("HTTPS_PROXY")
self.proxy = os.environ.get("https_proxy")
print(f"Google Search using proxy: {self.proxy}")
async def search(self, query: str, num_results: int) -> List[SearchResult]:
results = []
+12 -1
View File
@@ -22,6 +22,8 @@ class Main(star.Star):
self.sogo_search = Sogo()
self.google = Google()
self.websearch_link = self.context.get_config()['provider_settings'].get('web_search_link', False)
async def initialize(self):
websearch = self.context.get_config()['provider_settings']['web_search']
if websearch:
@@ -109,8 +111,17 @@ class Main(star.Star):
except BaseException:
site_result = ""
site_result = site_result[:700] + "..." if len(site_result) > 700 else site_result
ret += f"{idx}. {i.title} \n{i.snippet}\n{site_result}\n\n"
header = f"{idx}. {i.title} "
if self.websearch_link and i.url:
header += i.url
ret += f"{header}\n{i.snippet}\n{site_result}\n\n"
idx += 1
if self.websearch_link:
ret += "针对问题,请根据上面的结果分点总结,并且在结尾处附上对应内容的参考链接(如有)。"
return ret