Compare commits

...

18 Commits

Author SHA1 Message Date
Soulter 17d4bf8f22 perf: 管理面板优化新增列表项的提示 2025-02-06 20:19:53 +08:00
Soulter 836be3b097 update: changelogs 2025-02-06 18:51:47 +08:00
Soulter 310415bea9 feat: 聊天增强图像转述支持自定义 Provider id 2025-02-06 18:49:16 +08:00
Soulter aafc1276a9 v3.4.21 2025-02-06 18:34:43 +08:00
Soulter 2993e794cc perf: hint 2025-02-06 17:45:15 +08:00
Soulter 58cb9cfb2d chore: clean code 2025-02-06 17:43:04 +08:00
Soulter fbdf0901d5 fix: 修复reminder时区问题 2025-02-06 17:41:34 +08:00
Soulter af8c81b621 feat: 支持重载插件 2025-02-06 17:27:53 +08:00
Soulter 06b5275e48 perf: 增加报错显示 2025-02-06 16:43:40 +08:00
Soulter ad95572d5f perf: 更好的 list 可视化 2025-02-06 15:59:45 +08:00
Soulter aebc7850f4 fix: openrouter 报错 no endpoints found that support tool use #371 2025-02-06 15:25:15 +08:00
Soulter 1b7efbc607 支持列表展示插件市场 2025-02-06 15:18:11 +08:00
Soulter 3800e96d14 fix: 修复metadata不生效的问题
feat: 支持查看插件行为
2025-02-06 15:10:24 +08:00
Soulter 461f1bb07c feat: 支持插件handler优先级 2025-02-06 12:35:43 +08:00
Soulter 7d4c07e4f6 feat: 支持设置 timeout 2025-02-06 12:31:39 +08:00
Soulter 31b788f463 fix: 修复不支持图片的模型请求异常 2025-02-06 01:50:53 +08:00
Soulter 96ab761f73 fix: 修复reminder无法删除的问题 2025-02-05 22:45:02 +08:00
Soulter 2b3f05c039 update: 优化部分注释 2025-02-05 19:58:48 +08:00
27 changed files with 733 additions and 206 deletions
+27 -2
View File
@@ -2,7 +2,7 @@
如需修改配置,请在 `data/cmd_config.json` 中修改或者在管理面板中可视化修改。
"""
VERSION = "3.4.20"
VERSION = "3.4.21"
DB_PATH = "data/data_v3.db"
# 默认配置
@@ -57,6 +57,7 @@ DEFAULT_CONFIG = {
"group_icl_enable": False,
"group_message_max_cnt": 300,
"image_caption": False,
"image_caption_provider_id": "",
"image_caption_prompt": "Please describe the image using Chinese.",
"active_reply": {
"enable": False,
@@ -72,6 +73,7 @@ DEFAULT_CONFIG = {
},
"admins_id": [],
"t2i": False,
"t2i_word_threshold": 150,
"http_proxy": "",
"dashboard": {
"enable": True,
@@ -338,6 +340,7 @@ CONFIG_METADATA_2 = {
"enable": True,
"key": [],
"api_base": "https://api.openai.com/v1",
"timeout": 120,
"model_config": {
"model": "gpt-4o-mini",
},
@@ -349,6 +352,7 @@ CONFIG_METADATA_2 = {
"api_version": "2024-05-01-preview",
"key": [],
"api_base": "",
"timeout": 120,
"model_config": {
"model": "gpt-4o-mini",
},
@@ -369,6 +373,7 @@ CONFIG_METADATA_2 = {
"enable": True,
"key": [],
"api_base": "https://generativelanguage.googleapis.com/v1beta/openai/",
"timeout": 120,
"model_config": {
"model": "gemini-1.5-flash",
},
@@ -379,6 +384,7 @@ CONFIG_METADATA_2 = {
"enable": True,
"key": [],
"api_base": "https://generativelanguage.googleapis.com/",
"timeout": 120,
"model_config": {
"model": "gemini-1.5-flash",
},
@@ -389,6 +395,7 @@ CONFIG_METADATA_2 = {
"enable": True,
"key": [],
"api_base": "https://api.deepseek.com/v1",
"timeout": 120,
"model_config": {
"model": "deepseek-chat",
},
@@ -398,6 +405,7 @@ CONFIG_METADATA_2 = {
"type": "zhipu_chat_completion",
"enable": True,
"key": [],
"timeout": 120,
"api_base": "https://open.bigmodel.cn/api/paas/v4/",
"model_config": {
"model": "glm-4-flash",
@@ -408,6 +416,7 @@ CONFIG_METADATA_2 = {
"type": "openai_chat_completion",
"enable": True,
"key": [],
"timeout": 120,
"api_base": "https://api.siliconflow.cn/v1",
"model_config": {
"model": "deepseek-ai/DeepSeek-V3",
@@ -459,6 +468,11 @@ CONFIG_METADATA_2 = {
},
},
"items": {
"timeout": {
"description": "超时时间",
"type": "int",
"hint": "超时时间,单位为秒。",
},
"openai-tts-voice": {
"description": "voice",
"type": "string",
@@ -710,6 +724,12 @@ CONFIG_METADATA_2 = {
"obvious_hint": True,
"hint": "启用后,当接收到图片消息时,会使用模型先将图片转述为文字再进行后续处理。推荐使用 gpt-4o-mini 模型。",
},
"image_caption_provider_id": {
"description": "图像转述提供商 ID",
"type": "string",
"obvious_hint": True,
"hint": "可选。图像转述提供商 ID。如为空将选择聊天使用的提供商。",
},
"image_caption_prompt": {
"description": "图像转述提示词",
"type": "string"
@@ -740,7 +760,7 @@ CONFIG_METADATA_2 = {
"description": "提示词",
"type": "string",
"obvious_hint": True,
"hint": "提示词。当提示词为空时,如果触发回复,prompt是触发的消息的内容;否则是提示词。此项可以和定时回复(暂未实现)配合使用。",
"hint": "提示词。当提示词为空时,如果触发回复,则向 LLM 请求的是触发的消息的内容;否则是提示词。此项可以和定时回复(暂未实现)配合使用。",
},
},
},
@@ -769,6 +789,11 @@ CONFIG_METADATA_2 = {
"type": "bool",
"hint": "启用后,超出一定长度的文本将会通过 AstrBot API 渲染成 Markdown 图片发送。可以缓解审核和消息过长刷屏的问题,并提高 Markdown 文本的可读性。",
},
"t2i_word_threshold": {
"description": "文本转图像字数阈值",
"type": "int",
"hint": "超出此字符长度的文本将会被转换成图片。字数不能低于 50。",
},
"admins_id": {
"description": "管理员 ID",
"type": "list",
@@ -19,6 +19,13 @@ class ResultDecorateStage:
self.reply_with_mention = ctx.astrbot_config['platform_settings']['reply_with_mention']
self.reply_with_quote = ctx.astrbot_config['platform_settings']['reply_with_quote']
self.use_tts = ctx.astrbot_config['provider_tts_settings']['enable']
self.t2i_word_threshold = ctx.astrbot_config['t2i_word_threshold']
try:
self.t2i_word_threshold = int(self.t2i_word_threshold)
if self.t2i_word_threshold < 50:
self.t2i_word_threshold = 50
except BaseException:
self.t2i_word_threshold = 150
# 分段回复
self.enable_segmented_reply = ctx.astrbot_config['platform_settings']['segmented_reply']['enable']
@@ -59,7 +66,7 @@ class ResultDecorateStage:
comp.text = llm_resp.completion_text
except BaseException as e:
traceback.print_exc()
logger.error("使用 LLM 分段回复失败: " + str(e))
logger.warning("使用 LLM 分段回复失败。将不分段回复。 " + str(e))
new_chain.append(comp)
continue
@@ -106,7 +113,7 @@ class ResultDecorateStage:
plain_str += "\n\n" + comp.text
else:
break
if plain_str and len(plain_str) > 150:
if plain_str and len(plain_str) > self.t2i_word_threshold:
render_start = time.time()
try:
url = await html_renderer.render_t2i(plain_str, return_url=True)
+1 -1
View File
@@ -122,7 +122,7 @@ class WakingCheckStage(Stage):
if permission_not_pass:
if self.no_permission_reply:
await event.send(MessageChain().message(f"ID {event.get_sender_id()} 权限不足"))
await event.send(MessageChain().message(f"ID {event.get_sender_id()} 权限不足。通过 /sid 获取 ID 并请管理员添加。"))
event.stop_event()
return
+9 -2
View File
@@ -31,12 +31,19 @@ class AstrMessageEvent(abc.ABC):
platform_meta: PlatformMetadata,
session_id: str,):
self.message_str = message_str
'''纯文本的消息'''
self.message_obj = message_obj
'''消息对象,AstrBotMessage。带有完整的消息结构。'''
self.platform_meta = platform_meta
'''消息平台的信息, 其中 name 是平台的类型,如 aiocqhttp'''
self.session_id = session_id
'''用户的会话 ID。可以直接使用下面的 unified_msg_origin'''
self.role = "member"
'''用户是否是管理员。如果是管理员,这里是 admin'''
self.is_wake = False # 是否通过 WakingStage
self.is_at_or_wake_command = False # 是否是 At 机器人或者带有唤醒词或者是私聊(事件监听器会让 is_wake 设为 True
'''是否唤醒'''
self.is_at_or_wake_command = False
'''是否是 At 机器人或者带有唤醒词或者是私聊(事件监听器会让 is_wake 设为 True,但是不会让这个属性置为 True)'''
self._extras = {}
self.session = MessageSesion(
platform_name=platform_meta.name,
@@ -44,7 +51,7 @@ class AstrMessageEvent(abc.ABC):
session_id=session_id
)
self.unified_msg_origin = str(self.session)
'''统一的消息来源字符串。格式为 platform_name:message_type:session_id'''
self._result: MessageEventResult = None
'''消息事件的结果'''
+1 -4
View File
@@ -39,7 +39,7 @@ class ProviderDify(Provider):
self,
prompt: str,
session_id: str = None,
image_urls: List[str] = None,
image_urls: List[str] = [],
func_tool: FuncCall = None,
contexts: List = None,
system_prompt: str = None,
@@ -64,8 +64,6 @@ class ProviderDify(Provider):
else:
# TODO: 处理更多情况
logger.warning(f"未知的图片链接:{image_url},图片将忽略。")
logger.debug(files_payload)
# 获得会话变量
session_vars = sp.get("session_variables", {})
@@ -115,7 +113,6 @@ class ProviderDify(Provider):
result = chunk['data']['outputs'][self.workflow_output_key]
case _:
raise Exception(f"未知的 Dify API 类型:{self.api_type}")
return LLMResponse(role="assistant", completion_text=result)
async def forget(self, session_id):
+13 -8
View File
@@ -1,6 +1,4 @@
import traceback
import base64
import json
import aiohttp
from astrbot.core.utils.io import download_image_by_url
from astrbot.core.db import BaseDatabase
@@ -12,17 +10,18 @@ from ..register import register_provider_adapter
from astrbot.core.provider.entites import LLMResponse
class SimpleGoogleGenAIClient():
def __init__(self, api_key: str, api_base: str):
def __init__(self, api_key: str, api_base: str, timeout: int=120) -> None:
self.api_key = api_key
if api_base.endswith("/"):
self.api_base = api_base[:-1]
else:
self.api_base = api_base
self.client = aiohttp.ClientSession(trust_env=True)
self.timeout = timeout
async def models_list(self) -> List[str]:
request_url = f"{self.api_base}/v1beta/models?key={self.api_key}"
async with self.client.get(request_url, timeout=10) as resp:
async with self.client.get(request_url, timeout=self.timeout) as resp:
response = await resp.json()
models = []
@@ -48,7 +47,7 @@ class SimpleGoogleGenAIClient():
payload["contents"] = contents
logger.debug(f"payload: {payload}")
request_url = f"{self.api_base}/v1beta/models/{model}:generateContent?key={self.api_key}"
async with self.client.post(request_url, json=payload, timeout=10) as resp:
async with self.client.post(request_url, json=payload, timeout=self.timeout) as resp:
response = await resp.json()
return response
@@ -67,10 +66,13 @@ class ProviderGoogleGenAI(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.timeout = provider_config.get("timeout", 180)
if isinstance(self.timeout, str):
self.timeout = int(self.timeout)
self.client = SimpleGoogleGenAIClient(
api_key=self.chosen_api_key,
api_base=provider_config.get("api_base", None)
api_base=provider_config.get("api_base", None),
timeout=self.timeout
)
self.set_model(provider_config['model_config']['model'])
@@ -194,7 +196,7 @@ class ProviderGoogleGenAI(Provider):
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()} 不支持函数调用工具调用,已自动去除")
logger.info(f"{self.get_model()} 不支持函数工具调用,已自动去除,不影响使用。")
if 'tools' in payloads:
del payloads['tools']
llm_response = await self._query(payloads, None)
@@ -224,6 +226,9 @@ class ProviderGoogleGenAI(Provider):
if image_url.startswith("http"):
image_path = await download_image_by_url(image_url)
image_data = await self.encode_image_bs64(image_path)
elif image_url.startswith("file:///"):
image_path = image_url.replace("file:///", "")
image_data = await self.encode_image_bs64(image_path)
else:
image_data = await self.encode_image_bs64(image_url)
user_content["content"].append({"type": "image_url", "image_url": {"url": image_data}})
+19 -9
View File
@@ -4,7 +4,7 @@ import os
from openai import AsyncOpenAI, AsyncAzureOpenAI, NOT_GIVEN
from openai.types.chat.chat_completion import ChatCompletion
from openai._exceptions import NotFoundError
from openai._exceptions import NotFoundError, UnprocessableEntityError
from astrbot.core.utils.io import download_image_by_url
from astrbot.core.db import BaseDatabase
@@ -29,7 +29,9 @@ 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.timeout = provider_config.get("timeout", 120)
if isinstance(self.timeout, str):
self.timeout = int(self.timeout)
# 适配 azure openai #332
if "api_version" in provider_config:
# 使用 azure api
@@ -37,14 +39,14 @@ class ProviderOpenAIOfficial(Provider):
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),
timeout=self.timeout
)
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),
timeout=self.timeout
)
self.set_model(provider_config['model_config']['model'])
@@ -128,6 +130,12 @@ class ProviderOpenAIOfficial(Provider):
llm_response = None
try:
llm_response = await self._query(payloads, func_tool)
except UnprocessableEntityError as e:
logger.warning(f"不可处理的实体错误:{e},尝试删除图片。")
# 尝试删除所有 image
new_contexts = await self._remove_image_from_context(context_query)
payloads['messages'] = new_contexts
llm_response = await self._query(payloads, func_tool)
except Exception as e:
if "maximum context length" in str(e):
# 重试 10 次
@@ -156,8 +164,9 @@ class ProviderOpenAIOfficial(Provider):
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()} 不支持函数调用工具调用,已经自动去除")
or 'Tool calling is not supported' in str(e) \
or 'No endpoints found that support tool use' in str(e): # siliconcloud
logger.info(f"{self.get_model()} 不支持函数工具调用,已自动去除,不影响使用。")
if 'tools' in payloads:
del payloads['tools']
llm_response = await self._query(payloads, None)
@@ -165,7 +174,7 @@ class ProviderOpenAIOfficial(Provider):
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")
logger.error("疑似该模型不支持函数调用工具调用。请输入 /tool off_all")
if 'Connection error.' in str(e):
proxy = os.environ.get("http_proxy", None)
@@ -221,9 +230,10 @@ class ProviderOpenAIOfficial(Provider):
if image_url.startswith("http"):
image_path = await download_image_by_url(image_url)
image_data = await self.encode_image_bs64(image_path)
elif image_url.startswith("file:///"):
image_path = image_url.replace("file:///", "")
image_data = await self.encode_image_bs64(image_path)
else:
if image_url.startswith("file:///"):
image_url = image_url.replace("file:///", "")
image_data = await self.encode_image_bs64(image_url)
user_content["content"].append({"type": "image_url", "image_url": {"url": image_data}})
return user_content
+1
View File
@@ -8,6 +8,7 @@ from astrbot.core.config import AstrBotConfig
class RegexFilter(HandlerFilter):
'''正则表达式过滤器'''
def __init__(self, regex: str):
self.regex_str = regex
self.regex = re.compile(regex)
def filter(self, event: AstrMessageEvent, cfg: AstrBotConfig) -> bool:
+35 -19
View File
@@ -17,7 +17,12 @@ 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, **kwargs) -> 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,18 +35,27 @@ 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=[]
)
# 插件handler的附加额外信息
if handler.__doc__:
md.desc = handler.__doc__.strip()
if 'desc' in kwargs:
md.desc = kwargs['desc']
del kwargs['desc']
md.extras_configs = kwargs
if not dont_add:
star_handlers_registry.append(md)
return md
def register_command(command_name: str = None, *args):
def register_command(command_name: str = None, *args, **kwargs):
'''注册一个 Command.
'''
# print("command: ", command_name, args, kwargs)
new_command = None
add_to_event_filters = False
if isinstance(command_name, RegisteringCommandable):
@@ -54,7 +68,7 @@ def register_command(command_name: str = None, *args):
add_to_event_filters = True
def decorator(awaitable):
handler_md = get_handler_or_create(awaitable, EventType.AdapterMessageEvent)
handler_md = get_handler_or_create(awaitable, EventType.AdapterMessageEvent, **kwargs)
new_command.init_handler_md(handler_md)
if add_to_event_filters:
# 裸指令
@@ -64,10 +78,12 @@ def register_command(command_name: str = None, *args):
return decorator
def register_command_group(command_group_name: str = None, *args):
def register_command_group(command_group_name: str = None, *args, **kwargs):
'''注册一个 CommandGroup
'''
# print("commandgroup: ", command_group_name,args, kwargs)
new_group = None
add_to_event_filters = False
if isinstance(command_group_name, RegisteringCommandable):
@@ -82,7 +98,7 @@ def register_command_group(command_group_name: str = None, *args):
def decorator(obj):
if add_to_event_filters:
# 根指令组
handler_md = get_handler_or_create(obj, EventType.AdapterMessageEvent)
handler_md = get_handler_or_create(obj, EventType.AdapterMessageEvent, **kwargs)
handler_md.event_filters.append(new_group)
return RegisteringCommandable(new_group)
@@ -97,16 +113,16 @@ class RegisteringCommandable():
def __init__(self, parent_group: CommandGroupFilter):
self.parent_group = parent_group
def register_event_message_type(event_message_type: EventMessageType):
def register_event_message_type(event_message_type: EventMessageType, **kwargs):
'''注册一个 EventMessageType'''
def decorator(awaitable):
handler_md = get_handler_or_create(awaitable, EventType.AdapterMessageEvent)
handler_md = get_handler_or_create(awaitable, EventType.AdapterMessageEvent, kwargs)
handler_md.event_filters.append(EventMessageTypeFilter(event_message_type))
return awaitable
return decorator
def register_platform_adapter_type(platform_adapter_type: PlatformAdapterType):
def register_platform_adapter_type(platform_adapter_type: PlatformAdapterType, **kwargs):
'''注册一个 PlatformAdapterType'''
def decorator(awaitable):
handler_md = get_handler_or_create(awaitable, EventType.AdapterMessageEvent)
@@ -115,10 +131,10 @@ def register_platform_adapter_type(platform_adapter_type: PlatformAdapterType):
return decorator
def register_regex(regex: str):
def register_regex(regex: str, **kwargs):
'''注册一个 Regex'''
def decorator(awaitable):
handler_md = get_handler_or_create(awaitable, EventType.AdapterMessageEvent)
handler_md = get_handler_or_create(awaitable, EventType.AdapterMessageEvent, **kwargs)
handler_md.event_filters.append(RegexFilter(regex))
return awaitable
@@ -138,7 +154,7 @@ def register_permission_type(permission_type: PermissionType, raise_error: bool
return decorator
def register_on_llm_request():
def register_on_llm_request(**kwargs):
'''当有 LLM 请求时的事件
Examples:
@@ -153,12 +169,12 @@ def register_on_llm_request():
请务必接收两个参数:event, request
'''
def decorator(awaitable):
_ = get_handler_or_create(awaitable, EventType.OnLLMRequestEvent)
_ = get_handler_or_create(awaitable, EventType.OnLLMRequestEvent, **kwargs)
return awaitable
return decorator
def register_on_llm_response():
def register_on_llm_response(**kwargs):
'''当有 LLM 请求后的事件
Examples:
@@ -173,7 +189,7 @@ def register_on_llm_response():
请务必接收两个参数:event, request
'''
def decorator(awaitable):
_ = get_handler_or_create(awaitable, EventType.OnLLMResponseEvent)
_ = get_handler_or_create(awaitable, EventType.OnLLMResponseEvent, **kwargs)
return awaitable
return decorator
@@ -219,18 +235,18 @@ def register_llm_tool(name: str = None):
return decorator
def register_on_decorating_result():
def register_on_decorating_result(**kwargs):
'''在发送消息前的事件'''
def decorator(awaitable):
_ = get_handler_or_create(awaitable, EventType.OnDecoratingResultEvent)
_ = get_handler_or_create(awaitable, EventType.OnDecoratingResultEvent, **kwargs)
return awaitable
return decorator
def register_after_message_sent():
def register_after_message_sent(**kwargs):
'''在消息发送后的事件'''
def decorator(awaitable):
_ = get_handler_or_create(awaitable, EventType.OnAfterMessageSentEvent)
_ = get_handler_or_create(awaitable, EventType.OnAfterMessageSentEvent, **kwargs)
return awaitable
return decorator
+4 -1
View File
@@ -2,7 +2,7 @@ from __future__ import annotations
from types import ModuleType
from typing import List, Dict
from dataclasses import dataclass
from dataclasses import dataclass, field
from astrbot.core.config import AstrBotConfig
star_registry: List[StarMetadata] = []
@@ -39,6 +39,9 @@ class StarMetadata:
config: AstrBotConfig = None
'''插件配置'''
star_handler_full_names: List[str] = field(default_factory=list)
'''注册的 Handler 的全名列表'''
def __str__(self) -> str:
return f"StarMetadata({self.name}, {self.desc}, {self.version}, {self.repo})"
+54 -15
View File
@@ -1,34 +1,41 @@
from __future__ import annotations
import enum
from dataclasses import dataclass
import heapq
from dataclasses import dataclass, field
from typing import Awaitable, List, Dict, TypeVar, Generic
from .filter import HandlerFilter
from .star import star_map
T = TypeVar('T', bound='StarHandlerMetadata')
class StarHandlerRegistry(Generic[T], List[T]):
class StarHandlerRegistry(Generic[T]):
'''用于存储所有的 Star Handler'''
star_handlers_map: Dict[str, StarHandlerMetadata] = {}
'''用于快速查找。key 是 handler_full_name'''
_handlers = []
def append(self, handler: StarHandlerMetadata):
'''添加一个 Handler'''
super().append(handler)
if 'priority' not in handler.extras_configs:
handler.extras_configs['priority'] = 0
heapq.heappush(self._handlers, (-handler.extras_configs['priority'], handler))
self.star_handlers_map[handler.handler_full_name] = handler
def get_handlers_by_event_type(self, event_type: EventType, only_activated = True) -> List[StarHandlerMetadata]:
def _print_handlers(self):
'''打印所有的 Handler'''
for _, handler in self._handlers:
print(handler.handler_full_name)
def get_handlers_by_event_type(self, event_type: EventType, only_activated=True) -> List[StarHandlerMetadata]:
'''通过事件类型获取 Handler'''
if only_activated:
return [
handler
for handler in self
if handler.event_type == event_type and
star_map[handler.handler_module_path] and
star_map[handler.handler_module_path].activated
]
else:
return [handler for handler in self if handler.event_type == event_type]
handlers = [
handler
for _, handler in self._handlers
if handler.event_type == event_type and
(not only_activated or (star_map[handler.handler_module_path] and star_map[handler.handler_module_path].activated))
]
return handlers
def get_handler_by_full_name(self, full_name: str) -> StarHandlerMetadata:
'''通过 Handler 的全名获取 Handler'''
@@ -36,7 +43,32 @@ class StarHandlerRegistry(Generic[T], List[T]):
def get_handlers_by_module_name(self, module_name: str) -> List[StarHandlerMetadata]:
'''通过模块名获取 Handler'''
return [handler for handler in self if handler.handler_module_path == module_name]
return [handler for _, handler in self._handlers if handler.handler_module_path == module_name]
def clear(self):
'''清空所有的 Handler'''
self.star_handlers_map.clear()
self._handlers.clear()
def remove(self, handler: StarHandlerMetadata):
'''删除一个 Handler'''
# self._handlers.remove(handler)
for i, h in enumerate(self._handlers):
if h[1] == handler:
self._handlers.pop(i)
break
try:
del self.star_handlers_map[handler.handler_full_name]
except KeyError:
pass
def __iter__(self):
'''使 StarHandlerRegistry 支持迭代'''
return (handler for _, handler in self._handlers)
def __len__(self):
'''返回 Handler 的数量'''
return len(self._handlers)
star_handlers_registry = StarHandlerRegistry()
@@ -76,3 +108,10 @@ class StarHandlerMetadata():
desc: str = ""
'''Handler 的描述信息'''
extras_configs: dict = field(default_factory=dict)
'''插件注册的一些其他的信息, 如 priority 等'''
def __lt__(self, other: StarHandlerMetadata):
'''定义小于运算符以支持优先队列'''
return self.extras_configs.get('priority', 0) < other.extras_configs.get('priority', 0)
+71 -28
View File
@@ -9,7 +9,6 @@ import logging
from types import ModuleType
from typing import List
from astrbot.core.config.astrbot_config import AstrBotConfig
from astrbot.core.config.default import DEFAULT_VALUE_MAP
from astrbot.core import logger, sp, pip_installer
from .context import Context
from . import StarMetadata
@@ -41,6 +40,8 @@ class PluginManager:
'''保留插件的路径。在 packages 目录下'''
self.conf_schema_fname = "_conf_schema.json"
'''插件配置 Schema 文件名'''
self.failed_plugin_info = ""
def _get_classes(self, arg: ModuleType):
'''获取指定模块(可以理解为一个 python 文件)下所有的类'''
@@ -127,7 +128,7 @@ class PluginManager:
if isinstance(metadata, dict):
if 'name' not in metadata or 'desc' not in metadata or 'version' not in metadata or 'author' not in metadata:
raise Exception("插件元数据信息不完整。")
raise Exception("插件元数据信息不完整。name, desc, version, author 是必须的字段。")
metadata = StarMetadata(
name=metadata['name'],
author=metadata['author'],
@@ -137,25 +138,45 @@ class PluginManager:
)
return metadata
async def reload(self):
'''扫描并加载所有的插件'''
for smd in star_registry:
logger.debug(f"尝试终止插件 {smd.name} ...")
if hasattr(smd.star_cls, "__del__"):
smd.star_cls.__del__()
star_handlers_registry.clear()
star_handlers_registry.star_handlers_map.clear()
star_map.clear()
star_registry.clear()
for key in list(sys.modules.keys()):
if key.startswith("data.plugins") or key.startswith("packages"):
del sys.modules[key]
async def reload(self, specified_plugin_name=None):
'''扫描并加载所有的插件 当 specified_module_path 指定时,重载指定插件'''
specified_module_path = None
if specified_plugin_name:
for smd in star_registry:
if smd.name == specified_plugin_name:
specified_module_path = smd.module_path
break
# 终止插件
if not specified_module_path:
for smd in star_registry:
logger.debug(f"尝试终止插件 {smd.name} ...")
if hasattr(smd.star_cls, "__del__"):
smd.star_cls.__del__()
star_handlers_registry.clear()
star_map.clear()
star_registry.clear()
for key in list(sys.modules.keys()):
if key.startswith("data.plugins") or key.startswith("packages"):
del sys.modules[key]
else:
# 只重载指定插件
smd = star_map.get(specified_module_path)
if smd:
await self._unbind_plugin(smd.name, specified_module_path)
try:
del sys.modules[specified_module_path]
except KeyError:
logger.warning(f"模块 {specified_module_path} 未载入")
plugin_modules = self._get_plugin_modules()
if plugin_modules is None:
return False, "未找到任何插件模块"
fail_rec = ""
inactivated_plugins: list = sp.get("inactivated_plugins", [])
@@ -171,11 +192,15 @@ class PluginManager:
root_dir_name = plugin_module['pname'] # 插件的目录名
reserved = plugin_module.get('reserved', False) # 是否是保留插件。目前在 packages/ 目录下的都是保留插件。保留插件不可以卸载。
path = "data.plugins." if not reserved else "packages."
path += root_dir_name + "." + module_str
if specified_module_path and path != specified_module_path:
continue
logger.info(f"正在载入插件 {root_dir_name} ...")
# 尝试导入模块
path = "data.plugins." if not reserved else "packages."
path += root_dir_name + "." + module_str
try:
module = __import__(path, fromlist=[module_str])
except (ModuleNotFoundError, ImportError):
@@ -204,6 +229,18 @@ class PluginManager:
# 通过装饰器的方式注册插件
metadata = star_map[path]
try:
# yaml 文件的元数据优先
metadata_yaml = self._load_plugin_metadata(plugin_path=plugin_dir_path)
if metadata_yaml:
metadata.name = metadata_yaml.name
metadata.author = metadata_yaml.author
metadata.desc = metadata_yaml.desc
metadata.version = metadata_yaml.version
metadata.repo = metadata_yaml.repo
except Exception:
pass
if plugin_config:
metadata.config = plugin_config
try:
@@ -220,7 +257,6 @@ class PluginManager:
# 绑定 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 = functools.partial(handler.handler, metadata.star_cls)
# 绑定 llm_tool handler
for func_tool in llm_tools.func_list:
@@ -244,8 +280,7 @@ class PluginManager:
obj = getattr(module, classes[0])(context=self.context) # 实例化插件类
metadata = None
plugin_path = os.path.join(self.plugin_store_path, root_dir_name) if not reserved else os.path.join(self.reserved_plugin_path, root_dir_name)
metadata = self._load_plugin_metadata(plugin_path=plugin_path, plugin_obj=obj)
metadata = self._load_plugin_metadata(plugin_path=plugin_dir_path, plugin_obj=obj)
metadata.star_cls = obj
metadata.config = plugin_config
metadata.module = module
@@ -255,16 +290,17 @@ class PluginManager:
metadata.module_path = path
star_map[path] = metadata
star_registry.append(metadata)
logger.debug(f"插件 {root_dir_name} 载入成功。")
# 禁用/启用插件
if metadata.module_path in inactivated_plugins:
metadata.activated = False
# 检查并且植入自定义的权限过滤器(alter_cmd)
full_names = []
for handler in star_handlers_registry.get_handlers_by_module_name(metadata.module_path):
full_names.append(handler.handler_full_name)
# 检查并且植入自定义的权限过滤器(alter_cmd)
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:
@@ -280,21 +316,28 @@ class PluginManager:
logger.debug(f"插入权限过滤器 {cmd_type}{metadata.name}{handler.handler_name} 方法。")
metadata.star_handler_full_names = full_names
# 执行 initialize() 方法
if hasattr(metadata.star_cls, "initialize"):
await metadata.star_cls.initialize()
except BaseException as e:
traceback.print_exc()
fail_rec += f"加载 {path} 插件时出现问题,原因 {str(e)}\n"
logger.error(f"----- 插件 {root_dir_name} 载入失败 -----")
errors = traceback.format_exc()
for line in errors.split('\n'):
logger.error(f"| {line}")
logger.error("----------------------------------")
fail_rec += f"加载 {root_dir_name} 插件时出现问题,原因 {str(e)}\n"
# 清除 pip.main 导致的多余的 logging handlers
for handler in logging.root.handlers[:]:
logging.root.removeHandler(handler)
if not fail_rec:
return True, None
else:
self.failed_plugin_info = fail_rec
return False, fail_rec
async def install_plugin(self, repo_url: str):
+9
View File
@@ -27,9 +27,14 @@ class DifyAPIClient:
payload = locals()
payload.pop("self")
payload.pop("timeout")
logger.info(f"chat_messages payload: {payload}")
async with self.session.post(
url, json=payload, headers=self.headers, timeout=timeout
) as resp:
if resp.status != 200:
text = await resp.text()
raise Exception(f"chat_messages 请求失败:{resp.status}. {text}")
while True:
data = await resp.content.read(8192) # 防止数据过大导致高水位报错
if not data:
@@ -55,9 +60,13 @@ class DifyAPIClient:
payload = locals()
payload.pop("self")
payload.pop("timeout")
logger.info(f"workflow_run payload: {payload}")
async with self.session.post(
url, json=payload, headers=self.headers, timeout=timeout
) as resp:
if resp.status != 200:
text = await resp.text()
raise Exception(f"chat_messages 请求失败:{resp.status}. {text}")
while True:
data = await resp.content.read(8192) # 防止数据过大导致高水位报错
if not data:
+79 -3
View File
@@ -6,6 +6,12 @@ from astrbot.core import logger
from quart import request
from astrbot.core.star.star_manager import PluginManager
from astrbot.core.core_lifecycle import AstrBotCoreLifecycle
from astrbot.core.star.star_handler import star_handlers_registry
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.star.filter.regex import RegexFilter
from astrbot.core.star.star_handler import EventType
class PluginRoute(Route):
def __init__(self, context: RouteContext, core_lifecycle: AstrBotCoreLifecycle, plugin_manager: PluginManager) -> None:
@@ -18,11 +24,33 @@ class PluginRoute(Route):
'/plugin/uninstall': ('POST', self.uninstall_plugin),
'/plugin/market_list': ('GET', self.get_online_plugins),
'/plugin/off': ('POST', self.off_plugin),
'/plugin/on': ('POST', self.on_plugin)
'/plugin/on': ('POST', self.on_plugin),
'/plugin/reload': ('POST', self.reload_plugins),
}
self.core_lifecycle = core_lifecycle
self.plugin_manager = plugin_manager
self.register_routes()
self.translated_event_type = {
EventType.AdapterMessageEvent: "平台消息下发时",
EventType.OnLLMRequestEvent: "LLM 请求时",
EventType.OnLLMResponseEvent: "LLM 响应后",
EventType.OnDecoratingResultEvent: "回复消息前",
EventType.OnCallingFuncToolEvent: "函数工具",
EventType.OnAfterMessageSentEvent: "发送消息后"
}
async def reload_plugins(self):
data = await request.json
plugin_name = data.get("name", None)
try:
success, message = await self.plugin_manager.reload(plugin_name)
if not success:
return Response().error(message).__dict__
return Response().ok(None, "重载成功。").__dict__
except Exception as e:
logger.error(f"/api/extensions/reload: {traceback.format_exc()}")
return Response().error(str(e)).__dict__
async def get_online_plugins(self):
custom = request.args.get("custom_registry")
@@ -59,10 +87,58 @@ class PluginRoute(Route):
"desc": plugin.desc,
"version": plugin.version,
"reserved": plugin.reserved,
"activated": plugin.activated
"activated": plugin.activated,
"handlers": await self.get_plugin_handlers_info(plugin.star_handler_full_names),
}
_plugin_resp.append(_t)
return Response().ok(_plugin_resp).__dict__
return Response().ok(_plugin_resp, message=self.plugin_manager.failed_plugin_info).__dict__
async def get_plugin_handlers_info(self, handler_full_names: list[str]):
'''解析插件行为'''
handlers = []
for handler_full_name in handler_full_names:
info = {}
handler = star_handlers_registry.star_handlers_map.get(handler_full_name, None)
if handler is None:
continue
info["event_type"] = handler.event_type.name
info["event_type_h"] = self.translated_event_type.get(handler.event_type, handler.event_type.name)
info["handler_full_name"] = handler.handler_full_name
info["desc"] = handler.desc
info["handler_name"] = handler.handler_name
if handler.event_type == EventType.AdapterMessageEvent:
# 处理平台适配器消息事件
has_admin = False
for filter in handler.event_filters: # 正常handler就只有 1~2 个 filter,因此这里时间复杂度不会太高
if isinstance(filter, CommandFilter):
info["type"] = "指令"
info["cmd"] = filter.command_name
elif isinstance(filter, CommandGroupFilter):
info["type"] = "指令组"
info["cmd"] = filter.group_name
info["sub_command"] = filter.print_cmd_tree(filter.sub_command_filters)
elif isinstance(filter, RegexFilter):
info["type"] = "正则匹配"
info["cmd"] = filter.regex_str
elif isinstance(filter, PermissionTypeFilter):
has_admin = True
info["has_admin"] = has_admin
if "cmd" not in info:
info["cmd"] = "未知"
if "type" not in info:
info["type"] = "事件监听器"
else:
info["cmd"] = "自动触发"
info["type"] = ""
if not info["desc"]:
info["desc"] = "无描述"
handlers.append(info)
return handlers
async def install_plugin(self):
post_data = await request.json
+19
View File
@@ -0,0 +1,19 @@
# What's Changed
> 由于重写了会话记录部分,更新此版本后,将会造成之前的对话记录清空(但没有被删除)。
> 关于更好的对话管理,如果有任何报错或者优化建议,请直接提交 issue~
1. 修复 reminder 时区问题
2. 面板支持重载单个插件 #297
3. 面板支持列表展示插件市场
4. 文字转图片支持自定义字数阈值(配置->其他配置)
5. 面板更好的列表可视化 #274
6. 面板支持查看插件行为
7. 支持设置 timeout 超时时间参数,防止思考模型太长达到超时时间。(需要重新配置服务提供商或者在服务提供商 config 中配置 timeout 参数) #378
8. openrouter 报错 no endpoints found that support tool use #371
9. 修复插件 metadata 不生效的问题
10. 修复不支持图片的模型请求异常
11. 修复 reminder 无法删除的问题
12. 修复 /model 切换不了模型的问题
13. 插件支持设置优先级
14. 聊天增强图像转述支持自定义 provider id。#274
+1 -1
View File
@@ -32,7 +32,7 @@
"vue-router": "4.2.4",
"vue3-apexcharts": "1.4.4",
"vue3-print-nb": "0.1.4",
"vuetify": "3.3.14",
"vuetify": "3.7.11",
"yup": "1.2.0"
},
"devDependencies": {
@@ -1,22 +1,26 @@
<template>
<h3 style="margin-bottom: 8px;" v-if="iterable && metadata[metadataKey]?.type === 'object'">
{{metadata[metadataKey]?.description }}
{{ metadata[metadataKey]?.description }}
</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" style="margin-bottom: 16px"
:text="metadata[metadataKey].items[key]?.hint" :title="'💡 关于' + metadata[metadataKey].items[key]?.description"
type="info" variant="tonal">
<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"
style="margin-bottom: 16px" :text="metadata[metadataKey].items[key]?.hint"
:title="'💡 关于' + metadata[metadataKey].items[key]?.description" type="info" variant="tonal">
</v-alert>
<div style="display: flex; align-items: center; justify-content: center; gap: 16px">
<div style="width: 100%;" v-if="metadata[metadataKey].items[key]">
<v-select v-if="metadata[metadataKey].items[key]?.options && !metadata[metadataKey].items[key]?.invisible" v-model="iterable[key]"
variant="outlined" :items="metadata[metadataKey].items[key]?.options"
:label="metadata[metadataKey].items[key]?.description + '(' + key + ')'" dense :disabled="metadata[metadataKey].items[key]?.readonly"></v-select>
<v-text-field v-else-if="metadata[metadataKey].items[key]?.type === 'string' && !metadata[metadataKey].items[key]?.invisible"
<v-select
v-if="metadata[metadataKey].items[key]?.options && !metadata[metadataKey].items[key]?.invisible"
v-model="iterable[key]" variant="outlined" :items="metadata[metadataKey].items[key]?.options"
:label="metadata[metadataKey].items[key]?.description + '(' + key + ')'" dense
:disabled="metadata[metadataKey].items[key]?.readonly"></v-select>
<v-text-field
v-else-if="metadata[metadataKey].items[key]?.type === 'string' && !metadata[metadataKey].items[key]?.invisible"
v-model="iterable[key]" :label="metadata[metadataKey].items[key]?.description + '(' + key + ')'"
variant="outlined" dense ></v-text-field>
variant="outlined" dense></v-text-field>
<v-text-field
v-else-if="(metadata[metadataKey].items[key]?.type === 'int' || metadata[metadataKey].items[key]?.type === 'float') && !metadata[metadataKey].items[key]?.invisible"
v-model="iterable[key]" :label="metadata[metadataKey].items[key]?.description + '(' + key + ')'"
@@ -27,17 +31,11 @@
<v-switch v-else-if="metadata[metadataKey].items[key]?.type === 'bool' && !metadata[metadataKey].items[key]?.invisible" v-model="iterable[key]"
:label="metadata[metadataKey].items[key]?.description + '(' + key + ')'" color="primary"
inset></v-switch>
<v-combobox variant="outlined" v-else-if="metadata[metadataKey].items[key]?.type === 'list' && !metadata[metadataKey].items[key]?.invisible"
v-model="iterable[key]" chips clearable
:label="metadata[metadataKey].items[key]?.description + '(' + key + ')'" multiple
prepend-icon="mdi-tag-multiple-outline">
<template v-slot:selection="{ attrs, item, select, selected }">
<v-chip v-bind="attrs" :model-value="selected" closable @click="select"
@click:close="remove(item)">
<strong>{{ item }}</strong>
</v-chip>
</template>
</v-combobox>
<ListConfigItem
v-else-if="metadata[metadataKey].items[key]?.type === 'list' && !metadata[metadataKey].items[key]?.invisible"
:value="iterable[key]"
:label="metadata[metadataKey].items[key]?.description + '(' + key + ')'"/>
<div v-else-if="metadata[metadataKey].items[key]?.type === 'object' && !metadata[metadataKey].items[key]?.invisible"
style="border: 1px solid #e0e0e0; padding: 8px; margin-bottom: 16px; border-radius: 10px;">
<AstrBotConfig :metadata="metadata[metadataKey].items" :iterable="iterable[key]"
@@ -59,44 +57,47 @@
}}</v-tooltip>
</v-btn>
</div>
<div>
<v-chip v-if="!metadata[metadataKey].items[key]?.invisible" color="primary">{{ metadata[metadataKey].items[key]?.type }}</v-chip>
</div>
</div>
</div>
<div v-else>
<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 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>
<div style="display: flex; align-items: center; justify-content: center; gap: 16px">
<div style="width: 100%;">
<v-select v-if="metadata[metadataKey]?.options && !metadata[metadataKey]?.invisible" v-model="iterable[metadataKey]"
variant="outlined" :items="metadata[metadataKey]?.options"
:label="metadata[metadataKey]?.description + '(' + metadataKey+ ')'" dense :disabled="metadata[metadataKey]?.readonly"></v-select>
<v-text-field v-else-if="metadata[metadataKey]?.type === 'string' && !metadata[metadataKey]?.invisible"
v-model="iterable[metadataKey]" :label="metadata[metadataKey]?.description + '(' + metadataKey+ ')'"
variant="outlined" dense ></v-text-field>
<v-select v-if="metadata[metadataKey]?.options && !metadata[metadataKey]?.invisible"
v-model="iterable[metadataKey]" variant="outlined" :items="metadata[metadataKey]?.options"
:label="metadata[metadataKey]?.description + '(' + metadataKey + ')'" dense
:disabled="metadata[metadataKey]?.readonly"></v-select>
<v-text-field
v-else-if="metadata[metadataKey]?.type === 'string' && !metadata[metadataKey]?.invisible"
v-model="iterable[metadataKey]"
:label="metadata[metadataKey]?.description + '(' + metadataKey + ')'" variant="outlined"
dense></v-text-field>
<v-text-field
v-else-if="(metadata[metadataKey]?.type === 'int' || metadata[metadataKey]?.type === 'float') && !metadata[metadataKey]?.invisible"
v-model="iterable[metadataKey]" :label="metadata[metadataKey]?.description + '(' + metadataKey+ ')'"
variant="outlined" dense></v-text-field>
<v-textarea v-else-if="metadata[metadataKey]?.type === 'text' && !metadata[metadataKey]?.invisible" v-model="iterable[metadataKey]"
:label="metadata[metadataKey]?.description + '(' + metadataKey+ ')'" variant="outlined"
v-model="iterable[metadataKey]"
:label="metadata[metadataKey]?.description + '(' + metadataKey + ')'" variant="outlined"
dense></v-text-field>
<v-textarea v-else-if="metadata[metadataKey]?.type === 'text' && !metadata[metadataKey]?.invisible"
v-model="iterable[metadataKey]"
:label="metadata[metadataKey]?.description + '(' + metadataKey + ')'" variant="outlined"
dense></v-textarea>
<v-switch v-else-if="metadata[metadataKey]?.type === 'bool' && !metadata[metadataKey]?.invisible" v-model="iterable[metadataKey]"
:label="metadata[metadataKey]?.description + '(' + metadataKey+ ')'" color="primary"
<v-switch v-else-if="metadata[metadataKey]?.type === 'bool' && !metadata[metadataKey]?.invisible"
v-model="iterable[metadataKey]"
:label="metadata[metadataKey]?.description + '(' + metadataKey + ')'" color="primary"
inset></v-switch>
<v-combobox variant="outlined" v-else-if="metadata[metadataKey]?.type === 'list' && !metadata[metadataKey]?.invisible"
v-model="iterable[metadataKey]" chips clearable
:label="metadata[metadataKey]?.description + '(' + metadataKey+ ')'" multiple
prepend-icon="mdi-tag-multiple-outline">
<template v-slot:selection="{ attrs, item, select, selected }">
<v-chip v-bind="attrs" :model-value="selected" closable @click="select"
@click:close="remove(item)">
<strong>{{ item }}</strong>
</v-chip>
</template>
</v-combobox>
<ListConfigItem
v-else-if="metadata[metadataKey]?.type === 'list' && !metadata[metadataKey]?.invisible"
:value="iterable[metadataKey]"
:label="metadata[metadataKey]?.description + '(' + metadataKey+ ')'"/>
<div v-else-if="metadata[metadataKey]?.type === 'object' && !metadata[metadataKey]?.invisible"
style="border: 1px solid #e0e0e0; padding: 8px; margin-bottom: 16px; border-radius: 10px;">
<AstrBotConfig :metadata="metadata[metadataKey].items" :iterable="iterable[metadataKey]"
@@ -113,6 +114,10 @@
}}</v-tooltip>
</v-btn>
</div>
<div>
<v-chip v-if="!metadata[metadataKey]?.invisible" color="primary">{{ metadata[metadataKey]?.type }}</v-chip>
</div>
</div>
</div>
</v-card-text>
@@ -120,8 +125,12 @@
<script>
import { readonly } from 'vue';
import ListConfigItem from './ListConfigItem.vue';
export default {
components: {
ListConfigItem
},
props: {
metadata: Object,
iterable: Object,
@@ -12,16 +12,16 @@ const open = (link: string | undefined) => {
<template>
<v-card variant="outlined" elevation="0" class="withbg">
<v-card-item style="padding: 10px 14px">
<v-card-item style="padding: 10px 12px">
<div class="d-sm-flex align-center justify-space-between">
<img v-if="logo" :src="logo" alt="logo" style="width: 40px; height: 40px; margin-right: 8px;">
<v-card-title style="font-size: 17px;">{{ props.title }}</v-card-title>
<v-card-title style="font-size: 16px;">{{ props.title }}</v-card-title>
<v-spacer></v-spacer>
<v-btn variant="plain" @click="open(props.link)">仓库</v-btn>
<v-btn size="small" text="Read" variant="flat" border @click="open(props.link)">帮助</v-btn>
</div>
</v-card-item>
<v-divider></v-divider>
<v-card-text>
<v-card-text style="padding: 16px;">
<slot />
</v-card-text>
</v-card>
@@ -0,0 +1,93 @@
<template>
<div class="list-config-item">
<h3>{{ label }}</h3>
<v-list dense style="background-color: transparent;max-height: 300px; overflow-y: scroll;" >
<v-list-item v-for="(item, index) in items" :key="index">
<v-list-item-content style="display: flex; justify-content: space-between;">
<v-list-item-title>
<v-chip>{{ item }}</v-chip>
</v-list-item-title>
<v-btn @click="removeItem(index)" variant="plain">
<v-icon>mdi-close</v-icon>
</v-btn>
</v-list-item-content>
</v-list-item>
</v-list>
<v-text-field
v-model="newItem"
label="添加新项,按回车确认添加"
@keyup.enter="addItem"
clearable
dense
hide-details
variant="outlined"
></v-text-field>
</div>
</template>
<script>
export default {
name: 'ListConfigItem',
props: {
value: {
type: Array,
default: () => [],
},
label: {
type: String,
default: '',
},
},
data() {
return {
newItem: '',
items: this.value,
};
},
watch: {
items(newVal) {
this.$emit('input', newVal);
},
},
methods: {
addItem() {
if (this.newItem.trim() !== '') {
this.items.push(this.newItem.trim());
this.newItem = '';
}
},
removeItem(index) {
this.items.splice(index, 1);
},
},
};
</script>
<style scoped>
.list-config-item {
border: 1px solid #e0e0e0;
padding: 16px;
margin-bottom: 16px;
border-radius: 10px;
background-color: #ffffff;
}
.list-config-item h3 {
margin-top: 0;
margin-bottom: 16px;
font-size: 18px;
font-weight: 500;
}
.v-list-item {
padding: 0;
}
.v-list-item-title {
font-size: 14px;
}
.v-btn {
margin-left: 8px;
}
</style>
@@ -9,7 +9,7 @@ const sidebarMenu = shallowRef(sidebarItems);
</script>
<template>
<v-navigation-drawer left v-model="customizer.Sidebar_drawer" elevation="0" rail-width="80" mobile-breakpoint="960"
<v-navigation-drawer left v-model="customizer.Sidebar_drawer" elevation="0" rail-width="80"
app class="leftSidebar" :rail="customizer.mini_sidebar">
<v-list class="pa-4 listitem" style="height: auto">
<template v-for="(item, i) in sidebarMenu" :key="i">
+15 -1
View File
@@ -68,7 +68,7 @@ import config from '@/config';
<v-tabs-window-item v-for="(config_item, index) in config_data[key2]" v-show="config_template_tab === index"
:key="index" :value="index">
<v-container>
<v-btn variant="tonal" rounded="xl" color="error" @click="config_data[key2].splice(index, 1)">
<v-btn variant="tonal" rounded="xl" color="error" @click="deleteItem(key2, index)">
删除这项
</v-btn>
@@ -215,6 +215,20 @@ export default {
// new_tmpl_cfg.id = "new_" + val + "_" + this.config_data[config_item_name].length;
this.config_data[config_item_name].push(new_tmpl_cfg);
this.config_template_tab = this.config_data[config_item_name].length - 1;
},
deleteItem(config_item_name, index) {
console.log(config_item_name, index);
let new_list = [];
for (let i = 0; i < this.config_data[config_item_name].length; i++) {
if (i !== index) {
new_list.push(this.config_data[config_item_name][i]);
}
}
this.config_data[config_item_name] = new_list;
if (this.config_template_tab > 0) {
this.config_template_tab -= 1;
}
}
},
}
+152 -37
View File
@@ -4,6 +4,7 @@ import WaitingForRestart from '@/components/shared/WaitingForRestart.vue';
import AstrBotConfig from '@/components/shared/AstrBotConfig.vue';
import ConsoleDisplayer from '@/components/shared/ConsoleDisplayer.vue';
import axios from 'axios';
import { max } from 'date-fns';
</script>
@@ -14,30 +15,56 @@ import axios from 'axios';
</v-alert>
<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>
<v-dialog max-width="500px">
<template v-slot:activator="{ props }">
<v-btn v-bind="props" v-if="extension_data.message" icon size="small" color="error"
style="margin-left: auto;" variant="plain">
<v-icon>mdi-alert-circle</v-icon>
</v-btn>
</template>
<v-card>
<v-card-title class="headline">错误信息</v-card-title>
<v-card-text>{{ extension_data.message }}
<br>
<small>详情请检查控制台</small>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="primary" text>关闭</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</div>
</div>
</v-col>
<v-col cols="12" md="6" lg="3" v-for="extension in extension_data.data">
<ExtensionCard :key="extension.name" :title="extension.name" :link="extension.repo" :logo="extension?.logo"
style="margin-bottom: 4px;">
<p style="min-height: 130px; max-height: 130px; overflow: none;">{{ extension.desc }}</p>
<div class="d-flex align-center gap-2">
<v-icon>mdi-account</v-icon>
<span>{{ extension.author }}</span>
<v-spacer></v-spacer>
<div v-if="!extension.reserved">
<v-btn class="text-none mr-2" size="small" text="Read" variant="flat" border
@click="openExtensionConfig(extension.name)">配置</v-btn>
<v-btn class="text-none mr-2" size="small" text="Read" variant="flat" border
@click="updateExtension(extension.name)">更新</v-btn>
<v-btn class="text-none mr-2" size="small" text="Read" variant="flat" border
@click="uninstallExtension(extension.name)">卸载</v-btn>
</div>
<div style="min-height: 135px; max-height: 135px; overflow: none;">
<span style="font-weight: bold;">By @{{ extension.author }}</span>
<span> | 插件有 {{ extension.handlers.length }} 个行为</span>
<p style="margin-top: 8px;">{{ extension.desc }}</p>
<a style="font-size: 12px; cursor: pointer; text-decoration: underline; color: #555;" @click="reloadPlugin(extension.name)">重载插件</a>
</div>
<div class="d-flex align-center gap-2 " style="overflow-x: auto;">
<v-btn v-if="!extension.reserved" class="text-none mr-2" size="small" text="Read" variant="flat" border
@click="openExtensionConfig(extension.name)">配置</v-btn>
<v-btn v-if="!extension.reserved" class="text-none mr-2" size="small" text="Read" variant="flat" border
@click="updateExtension(extension.name)">更新</v-btn>
<v-btn v-if="!extension.reserved" class="text-none mr-2" size="small" text="Read" variant="flat" border
@click="uninstallExtension(extension.name)">卸载</v-btn>
<!-- <span v-else>保留插件</span> -->
<v-btn class="text-none mr-2" size="small" text="Read" variant="flat" border v-if="extension.activated"
@click="pluginOff(extension)">禁用</v-btn>
<v-btn class="text-none mr-2" size="small" text="Read" variant="flat" border v-else
@click="pluginOn(extension)">启用</v-btn>
<v-btn class="text-none mr-2" size="small" text="Read" variant="flat" border
@click="showPluginInfo(extension)">行为</v-btn>
</div>
</ExtensionCard>
</v-col>
@@ -48,29 +75,44 @@ import axios from 'axios';
<small style="margin-left: 16px;">如无法显示请打开 <a
href="https://soulter.github.io/AstrBot_Plugins_Collection/plugins.json">链接</a> 复制想安装插件对应的 `repo`
链接然后点击右下角 + 号安装或打开链接下载压缩包安装</small>
<v-btn icon @click="isListView = !isListView" size="small" style="margin-left: auto;" variant="plain">
<v-icon>{{ isListView ? 'mdi-view-grid' : 'mdi-view-list' }}</v-icon>
</v-btn>
</div>
</div>
</v-col>
<v-col cols="12" md="12" v-if="announcement">
<v-banner color="success" lines="one" :text="announcement" :stacked="false" >
<v-banner color="success" lines="one" :text="announcement" :stacked="false">
</v-banner>
</v-col>
<v-col cols="12" md="6" lg="3" v-for="plugin in pluginMarketData">
<ExtensionCard :key="plugin.name" :title="plugin.name" :link="plugin.repo" style="margin-bottom: 4px;">
<p style="min-height: 130px; max-height: 130px; overflow: hidden;">{{ plugin.desc }}</p>
<div class="d-flex align-center gap-2">
<v-icon>mdi-account</v-icon>
<span>{{ plugin.author }}</span>
<v-spacer></v-spacer>
<v-btn v-if="!plugin.installed" class="text-none mr-2" size="small" text="Read" variant="flat" border
@click="extension_url = plugin.repo; newExtension()">安装</v-btn>
<v-btn v-else class="text-none mr-2" size="small" text="Read" variant="flat" border disabled>已安装</v-btn>
</div>
</ExtensionCard>
</v-col>
<template v-if="isListView">
<v-col cols="12" md="12">
<v-data-table :headers="pluginMarketHeaders" :items="pluginMarketData" item-key="name">
<template v-slot:item.actions="{ item }">
<v-btn v-if="!item.installed" class="text-none mr-2" size="small" text="Read" variant="flat" border
@click="extension_url = item.repo; newExtension()">安装</v-btn>
<v-btn v-else class="text-none mr-2" size="small" text="Read" variant="flat" border disabled>已安装</v-btn>
</template>
</v-data-table>
</v-col>
</template>
<template v-else>
<v-col cols="12" md="6" lg="3" v-for="plugin in pluginMarketData">
<ExtensionCard :key="plugin.name" :title="plugin.name" :link="plugin.repo" style="margin-bottom: 4px;">
<div style="min-height: 130px; max-height: 130px; overflow: hidden;">
<p style="font-weight: bold;">By @{{ plugin.author }}</p>
{{ plugin.desc }}
</div>
<div class="d-flex align-center gap-2">
<v-btn v-if="!plugin.installed" class="text-none mr-2" size="small" text="Read" variant="flat" border
@click="extension_url = plugin.repo; newExtension()">安装</v-btn>
<v-btn v-else class="text-none mr-2" size="small" text="Read" variant="flat" border disabled>已安装</v-btn>
</div>
</ExtensionCard>
</v-col>
</template>
<v-col style="margin-bottom: 16px;" cols="12" md="12">
<small><a href="https://astrbot.app/dev/plugin.html">插件开发文档</a></small> |
@@ -184,6 +226,44 @@ import axios from 'axios';
</v-card>
</v-dialog>
<v-dialog v-model="showPluginInfoDialog" width="1200">
<template v-slot:activator="{ props }">
</template>
<v-card>
<v-card-title>
<span class="text-h5">{{ selectedPlugin.name }} 插件行为</span>
</v-card-title>
<v-card-text>
<v-data-table style="font-size: 17px;" :headers="plugin_handler_info_headers" :items="selectedPlugin.handlers"
item-key="name">
<template v-slot:header.id="{ column }">
<p style="font-weight: bold;">{{ column.title }}</p>
</template>
<template v-slot:item.event_type="{ item }">
{{ item.event_type }}
</template>
<template v-slot:item.desc="{ item }">
{{ item.desc }}
</template>
<template v-slot:item.type="{ item }">
<v-chip color="success">
{{ item.type }}
</v-chip>
</template>
<template v-slot:item.cmd="{ item }">
<span style="font-weight: bold;">{{ item.cmd }}</span>
</template>
</v-data-table>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="blue-darken-1" variant="text" @click="showPluginInfoDialog = false">
关闭
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<v-snackbar :timeout="2000" elevation="24" :color="snack_success" v-model="snack_show">
{{ snack_message }}
</v-snackbar>
@@ -204,7 +284,8 @@ export default {
data() {
return {
extension_data: {
"data": []
"data": [],
"message": ""
},
extension_url: "",
status: "",
@@ -227,7 +308,22 @@ export default {
result: ""
},
announcement: ""
announcement: "",
showPluginInfoDialog: false,
selectedPlugin: {},
plugin_handler_info_headers: [
{ title: '行为类型', key: 'event_type_h' },
{ title: '描述', key: 'desc', maxWidth: '250px' },
{ title: '具体类型', key: 'type' },
{ title: '触发方式', key: 'cmd' },
],
isListView: false,
pluginMarketHeaders: [
{ title: '名称', value: 'name' },
{ title: '描述', value: 'desc' },
{ title: '作者', value: 'author' },
{ title: '操作', value: 'actions', sortable: false }
],
}
},
mounted() {
@@ -265,7 +361,8 @@ export default {
},
getExtensions() {
axios.get('/api/plugin/get').then((res) => {
this.extension_data.data = res.data.data;
this.extension_data = res.data;
this.checkAlreadyInstalled();
});
},
@@ -295,7 +392,7 @@ export default {
this.onLoadingDialogResult(2, res.data.message, -1);
return;
}
this.extension_data.data = res.data.data;
this.extension_data = res.data;
this.upload_file = "";
this.onLoadingDialogResult(1, res.data.message);
this.dialog = false;
@@ -316,8 +413,7 @@ export default {
this.onLoadingDialogResult(2, res.data.message, -1);
return;
}
this.extension_data.data = res.data.data;
console.log(this.extension_data);
this.extension_data = res.data;
this.extension_url = "";
this.onLoadingDialogResult(1, res.data.message);
this.dialog = false;
@@ -339,7 +435,7 @@ export default {
this.toast(res.data.message, "error");
return;
}
this.extension_data.data = res.data.data;
this.extension_data = res.data;
this.toast(res.data.message, "success");
this.dialog = false;
this.getExtensions();
@@ -357,7 +453,7 @@ export default {
this.onLoadingDialogResult(2, res.data.message, -1);
return;
}
this.extension_data.data = res.data.data;
this.extension_data = res.data;
console.log(this.extension_data);
this.onLoadingDialogResult(1, res.data.message);
this.dialog = false;
@@ -460,6 +556,25 @@ export default {
}
}
this.pluginMarketData = notInstalled.concat(installed);
},
showPluginInfo(plugin) {
this.selectedPlugin = plugin;
this.showPluginInfoDialog = true;
},
reloadPlugin(plugin_name) {
axios.post('/api/plugin/reload',
{
name: plugin_name
}).then((res) => {
if (res.data.status === "error") {
this.onLoadingDialogResult(2, res.data.message, -1);
return;
}
this.toast("重载成功", "success");
this.getExtensions();
}).catch((err) => {
this.toast(err, "error");
});
}
},
}
+8 -1
View File
@@ -25,6 +25,7 @@ class LongTermMemory:
self.max_cnt = 300
self.image_caption = self.config["image_caption"]
self.image_caption_prompt = self.config["image_caption_prompt"]
self.image_caption_provider_id = self.config["image_caption_provider_id"]
self.active_reply = self.config["active_reply"]
self.enable_active_reply = self.active_reply.get("enable", False)
@@ -42,7 +43,13 @@ class LongTermMemory:
return cnt
async def get_image_caption(self, image_url: str) -> str:
provider = self.context.get_using_provider()
if not self.image_caption_provider_id:
provider = self.context.get_using_provider()
else:
provider = self.context.get_provider_by_id(self.image_caption_provider_id)
if not provider:
raise Exception(f"没有找到 ID 为 {self.image_caption_provider_id} 的提供商")
response = await provider.text_chat(
prompt=self.image_caption_prompt,
session_id=uuid.uuid4().hex,
+6 -4
View File
@@ -339,7 +339,7 @@ UID: {user_id} 此 ID 可用于设置管理员。/op <UID> 授权管理员, /deo
else:
self.context.get_using_provider().set_model(idx_or_name)
message.set_result(
MessageEventResult().message(f"切换模型成功。 \n模型信息: {idx_or_name}"))
MessageEventResult().message(f"切换模型{self.context.get_using_provider().get_model()}"))
@filter.command("history")
@@ -655,7 +655,6 @@ UID: {user_id} 此 ID 可用于设置管理员。/op <UID> 授权管理员, /deo
@filter.on_llm_request()
async def decorate_llm_req(self, event: AstrMessageEvent, req: ProviderRequest):
'''在请求 LLM 前注入人格信息、Identifier、时间等 System Prompt'''
provider = self.context.get_using_provider()
if self.prompt_prefix:
req.prompt = self.prompt_prefix + req.prompt
@@ -666,8 +665,11 @@ UID: {user_id} 此 ID 可用于设置管理员。/op <UID> 授权管理员, /deo
req.prompt = user_info + req.prompt
if self.enable_datetime:
req.system_prompt += f"\nCurrent datetime: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M')}\n"
tz_offset = datetime.timedelta(hours=8)
tz = datetime.timezone(tz_offset)
current_time = datetime.datetime.now(tz).strftime('%Y-%m-%d %H:%M')
req.system_prompt += f"\nCurrent datetime: {current_time}\n"
if req.conversation:
persona_id = req.conversation.persona_id
if not persona_id and persona_id != "[%None]": # [%None] 为用户取消人格
+45 -13
View File
@@ -1,6 +1,7 @@
import os
import json
import datetime
import uuid
import astrbot.api.star as star
from astrbot.api.event import filter
from apscheduler.schedulers.asyncio import AsyncIOScheduler
@@ -28,11 +29,18 @@ class Main(star.Star):
'''Initialize the scheduler.'''
for group in self.reminder_data:
for reminder in self.reminder_data[group]:
if 'id' not in reminder:
id_ = str(uuid.uuid4())
reminder['id'] = id_
else:
id_ = reminder['id']
if "datetime" in reminder:
if self.check_is_outdated(reminder):
continue
self.scheduler.add_job(
self._reminder_callback,
id=id_,
trigger='date',
args=[group, reminder],
run_date=datetime.datetime.strptime(reminder["datetime"], "%Y-%m-%d %H:%M"),
@@ -42,6 +50,7 @@ class Main(star.Star):
self.scheduler.add_job(
self._reminder_callback,
trigger='cron',
id=id_,
args=[group, reminder],
misfire_grace_time=60,
**self._parse_cron_expr(reminder["cron"])
@@ -69,11 +78,11 @@ class Main(star.Star):
}
@llm_tool("reminder")
async def reminder_tool(self, event: AstrMessageEvent, text: str, datetime_str: str = None, cron_expression: str = None, human_readable_cron: str = None):
async def reminder_tool(self, event: AstrMessageEvent, text: str=None, datetime_str: str = None, cron_expression: str = None, human_readable_cron: str = None):
'''Call this function when user ask for setting a reminder.
Args:
text(string): The content of the reminder.
text(string): Must Required. The content of the reminder.
datetime_str(string): Required when user's reminder is a single reminder. The datetime string of the reminder, Must format with %Y-%m-%d %H:%M
cron_expression(string): Required when user's reminder is a repeated reminder. The cron expression of the reminder.
human_readable_cron(string): Optional. The human readable cron expression of the reminder.
@@ -88,65 +97,88 @@ class Main(star.Star):
if not cron_expression and not datetime_str:
raise ValueError("The cron_expression and datetime_str cannot be both None.")
reminder_time = ""
if not text:
text = "未命名待办事项"
if cron_expression:
d = { "text": text, "cron": cron_expression, "cron_h": human_readable_cron }
d = { "text": text, "cron": cron_expression, "cron_h": human_readable_cron, "id": str(uuid.uuid4()) }
self.reminder_data[event.unified_msg_origin].append(d)
self.scheduler.add_job(
self._reminder_callback,
'cron',
id=d["id"],
misfire_grace_time=60,
**self._parse_cron_expr(cron_expression), args=[event.unified_msg_origin, d]
)
if human_readable_cron:
reminder_time = f"{human_readable_cron}(Cron: {cron_expression})"
else:
d = { "text": text, "datetime": datetime_str }
d = { "text": text, "datetime": datetime_str, "id": str(uuid.uuid4()) }
self.reminder_data[event.unified_msg_origin].append(d)
datetime_scheduled = datetime.datetime.strptime(datetime_str, "%Y-%m-%d %H:%M")
self.scheduler.add_job(
self._reminder_callback,
'date',
id=d["id"],
args=[event.unified_msg_origin, d],
run_date=datetime_scheduled,
misfire_grace_time=60
)
reminder_time = datetime_str
await self._save_data()
yield event.plain_result("成功设置待办事项。\n内容: " + text + "\n时间: " + reminder_time + "\n\n使用 /reminder ls 查看所有待办事项。")
yield event.plain_result("成功设置待办事项。\n内容: " + text + "\n时间: " + reminder_time + "\n\n使用 /reminder ls 查看所有待办事项。\n使用 /tool off reminder 关闭此功能。")
@filter.command_group("reminder")
def reminder(self):
'''The command group of the reminder.'''
pass
async def get_upcoming_reminders(self, unified_msg_origin: str):
'''Get upcoming reminders.'''
reminders = self.reminder_data.get(unified_msg_origin, [])
if not reminders:
return []
now = datetime.datetime.now()
upcoming_reminders = [
reminder for reminder in reminders
if "datetime" not in reminder or datetime.datetime.strptime(reminder["datetime"], "%Y-%m-%d %H:%M") >= now
]
return upcoming_reminders
@reminder.command("ls")
async def reminder_ls(self, event: AstrMessageEvent):
'''List all reminders.'''
reminders = self.reminder_data.get(event.unified_msg_origin, [])
'''List upcoming reminders.'''
reminders = await self.get_upcoming_reminders(event.unified_msg_origin)
if not reminders:
yield event.plain_result("没有待办事项。")
yield event.plain_result("没有正在进行的待办事项。")
else:
reminder_str = "待办事项:\n"
reminder_str = "正在进行的待办事项:\n"
for i, reminder in enumerate(reminders):
time_ = reminder.get("datetime", "")
if not time_:
cron_expr = reminder.get("cron", "")
time_ = reminder.get("cron_h", "") + f"(Cron: {cron_expr})"
reminder_str += f"{i + 1}. {reminder['text']} - {time_}\n"
reminder_str += "\n使用 /reminder rm <index> 删除待办事项。"
reminder_str += "\n使用 /reminder rm <id> 删除待办事项。\n"
yield event.plain_result(reminder_str)
@reminder.command("rm")
async def reminder_rm(self, event: AstrMessageEvent, index: int):
'''Remove a reminder by index.'''
reminders = self.reminder_data.get(event.unified_msg_origin, [])
reminders = await self.get_upcoming_reminders(event.unified_msg_origin)
if not reminders:
yield event.plain_result("没有待办事项。")
elif index < 1 or index > len(reminders):
yield event.plain_result("索引越界。")
else:
reminder = reminders.pop(index - 1)
self.scheduler.remove_job(event.unified_msg_origin)
job_id = reminder.get("id")
try:
self.scheduler.remove_job(job_id)
except Exception as e:
logger.error(f"Remove job error: {e}")
await self._save_data()
yield event.plain_result("成功删除待办事项:\n" + reminder["text"])
-1
View File
@@ -9,7 +9,6 @@ class Google(SearchEngine):
def __init__(self) -> None:
super().__init__()
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 = []
-1
View File
@@ -30,7 +30,6 @@ async def test_plugin_manager_reload(plugin_manager_pm: PluginManager):
success, err_message = await plugin_manager_pm.reload()
assert success is True
assert err_message is None
assert len(star_handlers_registry) > 0 # package
@pytest.mark.asyncio
async def test_plugin_crud(plugin_manager_pm: PluginManager):