Compare commits
30 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a8b2b09e0f | |||
| 6858b8c555 | |||
| 0e493b1a0e | |||
| 37d478f970 | |||
| 7d0d42a49f | |||
| 0eb1684ef1 | |||
| 9b0b723143 | |||
| 532bc6e1e6 | |||
| fe3ed4c454 | |||
| b5ec89e586 | |||
| 895e7397c2 | |||
| 59b767957a | |||
| 17d4bf8f22 | |||
| 836be3b097 | |||
| 310415bea9 | |||
| aafc1276a9 | |||
| 2993e794cc | |||
| 58cb9cfb2d | |||
| fbdf0901d5 | |||
| af8c81b621 | |||
| 06b5275e48 | |||
| ad95572d5f | |||
| aebc7850f4 | |||
| 1b7efbc607 | |||
| 3800e96d14 | |||
| 461f1bb07c | |||
| 7d4c07e4f6 | |||
| 31b788f463 | |||
| 96ab761f73 | |||
| 2b3f05c039 |
@@ -11,7 +11,8 @@ from astrbot.core.config import AstrBotConfig
|
||||
os.makedirs("data", exist_ok=True)
|
||||
|
||||
astrbot_config = AstrBotConfig()
|
||||
html_renderer = HtmlRenderer()
|
||||
t2i_base_url = astrbot_config.get('t2i_endpoint', 'https://t2i.soulter.top/text2img')
|
||||
html_renderer = HtmlRenderer(t2i_base_url)
|
||||
logger = LogManager.GetLogger(log_name='astrbot')
|
||||
|
||||
if os.environ.get('TESTING', ""):
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
如需修改配置,请在 `data/cmd_config.json` 中修改或者在管理面板中可视化修改。
|
||||
"""
|
||||
|
||||
VERSION = "3.4.20"
|
||||
VERSION = "3.4.22"
|
||||
DB_PATH = "data/data_v3.db"
|
||||
|
||||
# 默认配置
|
||||
@@ -29,7 +29,6 @@ DEFAULT_CONFIG = {
|
||||
"enable": False,
|
||||
"only_llm_result": True,
|
||||
"interval": "1.5,3.5",
|
||||
"seg_prompt": "",
|
||||
"regex": ".*?[。?!~…]+|.+$"
|
||||
},
|
||||
"no_permission_reply": True,
|
||||
@@ -57,14 +56,14 @@ 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,
|
||||
"method": "possibility_reply",
|
||||
"possibility_reply": 0.1,
|
||||
"prompt": "",
|
||||
},
|
||||
"put_history_to_prompt": True,
|
||||
}
|
||||
},
|
||||
"content_safety": {
|
||||
"internal_keywords": {"enable": True, "extra_keywords": []},
|
||||
@@ -72,6 +71,7 @@ DEFAULT_CONFIG = {
|
||||
},
|
||||
"admins_id": [],
|
||||
"t2i": False,
|
||||
"t2i_word_threshold": 150,
|
||||
"http_proxy": "",
|
||||
"dashboard": {
|
||||
"enable": True,
|
||||
@@ -218,11 +218,6 @@ CONFIG_METADATA_2 = {
|
||||
"type": "string",
|
||||
"hint": "每一段回复的间隔时间,格式为 `最小时间,最大时间`。如 `0.75,2.5`",
|
||||
},
|
||||
"seg_prompt": {
|
||||
"description": "分段提示词辅助",
|
||||
"type": "string",
|
||||
"hint": "此项为空时表达不启用这个方法。此方法会调用一次LLM请求。让 LLM 在某一句话中插入一个可以用正则表达式分隔的标记,来实现LLM基于情感分段。如: `请基于情感对以下文本进行分段, 并在两段之间添加`<seg>`以便我用正则匹配。` 然后将下面的正则表达式更换为`.+?<seg>`。",
|
||||
},
|
||||
"regex": {
|
||||
"description": "正则表达式",
|
||||
"type": "string",
|
||||
@@ -250,7 +245,7 @@ CONFIG_METADATA_2 = {
|
||||
"type": "list",
|
||||
"items": {"type": "string"},
|
||||
"obvious_hint": True,
|
||||
"hint": "AstrBot 只处理所填写的 ID 发来的消息事件。为空时不启用白名单过滤。可以使用 /sid 指令获取在某个平台上的会话 ID。也可在 AstrBot 日志内获取会话 ID,当一条消息没通过白名单时,会输出 INFO 级别的日志。会话 ID 类似 aiocqhttp:GroupMessage:547540978",
|
||||
"hint": "AstrBot 只处理所填写的 ID 发来的消息事件。为空时不启用白名单过滤。可以使用 /sid 指令获取在某个平台上的会话 ID。也可在 AstrBot 日志内获取会话 ID,当一条消息没通过白名单时,会输出 INFO 级别的日志。会话 ID 类似 aiocqhttp:GroupMessage:547540978。管理员可使用 /wl 添加白名单",
|
||||
},
|
||||
"id_whitelist_log": {
|
||||
"description": "打印白名单日志",
|
||||
@@ -338,6 +333,7 @@ CONFIG_METADATA_2 = {
|
||||
"enable": True,
|
||||
"key": [],
|
||||
"api_base": "https://api.openai.com/v1",
|
||||
"timeout": 120,
|
||||
"model_config": {
|
||||
"model": "gpt-4o-mini",
|
||||
},
|
||||
@@ -349,6 +345,7 @@ CONFIG_METADATA_2 = {
|
||||
"api_version": "2024-05-01-preview",
|
||||
"key": [],
|
||||
"api_base": "",
|
||||
"timeout": 120,
|
||||
"model_config": {
|
||||
"model": "gpt-4o-mini",
|
||||
},
|
||||
@@ -369,6 +366,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 +377,7 @@ CONFIG_METADATA_2 = {
|
||||
"enable": True,
|
||||
"key": [],
|
||||
"api_base": "https://generativelanguage.googleapis.com/",
|
||||
"timeout": 120,
|
||||
"model_config": {
|
||||
"model": "gemini-1.5-flash",
|
||||
},
|
||||
@@ -389,6 +388,7 @@ CONFIG_METADATA_2 = {
|
||||
"enable": True,
|
||||
"key": [],
|
||||
"api_base": "https://api.deepseek.com/v1",
|
||||
"timeout": 120,
|
||||
"model_config": {
|
||||
"model": "deepseek-chat",
|
||||
},
|
||||
@@ -398,6 +398,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 +409,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 +461,11 @@ CONFIG_METADATA_2 = {
|
||||
},
|
||||
},
|
||||
"items": {
|
||||
"timeout": {
|
||||
"description": "超时时间",
|
||||
"type": "int",
|
||||
"hint": "超时时间,单位为秒。",
|
||||
},
|
||||
"openai-tts-voice": {
|
||||
"description": "voice",
|
||||
"type": "string",
|
||||
@@ -710,6 +717,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,15 +753,9 @@ CONFIG_METADATA_2 = {
|
||||
"description": "提示词",
|
||||
"type": "string",
|
||||
"obvious_hint": True,
|
||||
"hint": "提示词。当提示词为空时,如果触发回复,prompt是触发的消息的内容;否则是提示词。此项可以和定时回复(暂未实现)配合使用。",
|
||||
"hint": "提示词。当提示词为空时,如果触发回复,则向 LLM 请求的是触发的消息的内容;否则是提示词。此项可以和定时回复(暂未实现)配合使用。",
|
||||
},
|
||||
},
|
||||
},
|
||||
"put_history_to_prompt": {
|
||||
"description": "将群聊历史记录作为 prompt",
|
||||
"type": "bool",
|
||||
"obvious_hint": True,
|
||||
"hint": "需要先启用 group_icl_enable。此功能会将群聊历史记录放到 prompt 再请求。如果关闭,则是放在 system_prompt。如果开启了主动回复,建议启用,模型能够更好地完成回复任务。",
|
||||
}
|
||||
},
|
||||
},
|
||||
@@ -769,6 +776,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",
|
||||
|
||||
@@ -129,6 +129,9 @@ class LLMRequestSubStage(Stage):
|
||||
req.prompt += extra_prompt
|
||||
async for _ in self.process(event, _nested=True):
|
||||
yield
|
||||
else:
|
||||
if llm_response.completion_text:
|
||||
event.set_result(MessageEventResult().message(llm_response.completion_text))
|
||||
|
||||
except BaseException as e:
|
||||
logger.error(traceback.format_exc())
|
||||
|
||||
@@ -19,11 +19,17 @@ 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']
|
||||
self.only_llm_result = ctx.astrbot_config['platform_settings']['segmented_reply']['only_llm_result']
|
||||
self.seg_prompt = ctx.astrbot_config['platform_settings']['segmented_reply']['seg_prompt']
|
||||
self.regex = ctx.astrbot_config['platform_settings']['segmented_reply']['regex']
|
||||
|
||||
async def process(self, event: AstrMessageEvent) -> Union[None, AsyncGenerator[None, None]]:
|
||||
@@ -50,19 +56,6 @@ class ResultDecorateStage:
|
||||
new_chain = []
|
||||
for comp in result.chain:
|
||||
if isinstance(comp, Plain):
|
||||
|
||||
if self.seg_prompt:
|
||||
try:
|
||||
llm_resp = await self.ctx.plugin_manager.context.get_using_provider().text_chat(
|
||||
prompt=f"{self.seg_prompt}\n{comp.text}",
|
||||
)
|
||||
comp.text = llm_resp.completion_text
|
||||
except BaseException as e:
|
||||
traceback.print_exc()
|
||||
logger.error("使用 LLM 分段回复失败: " + str(e))
|
||||
new_chain.append(comp)
|
||||
continue
|
||||
|
||||
split_response = re.findall(self.regex, comp.text)
|
||||
if not split_response:
|
||||
new_chain.append(comp)
|
||||
@@ -106,7 +99,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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
'''消息事件的结果'''
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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'])
|
||||
|
||||
@@ -179,9 +181,9 @@ class ProviderGoogleGenAI(Provider):
|
||||
llm_response = await self._query(payloads, func_tool)
|
||||
except Exception as e:
|
||||
if "maximum context length" in str(e):
|
||||
retry_cnt = 10
|
||||
retry_cnt = 20
|
||||
while retry_cnt > 0:
|
||||
logger.warning(f"请求失败:{e}。上下文长度超过限制。尝试弹出最早的记录然后重试。")
|
||||
logger.warning(f"请求失败:{e}。上下文长度超过限制。尝试弹出最早的记录然后重试。当前记录条数: {len(context_query)}")
|
||||
try:
|
||||
await self.pop_record(context_query)
|
||||
llm_response = await self._query(payloads, func_tool)
|
||||
@@ -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,8 +226,14 @@ 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)
|
||||
if not image_data:
|
||||
logger.warning(f"图片 {image_url} 得到的结果为空,将忽略。")
|
||||
continue
|
||||
user_content["content"].append({"type": "image_url", "image_url": {"url": image_data}})
|
||||
return user_content
|
||||
else:
|
||||
|
||||
@@ -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'])
|
||||
@@ -78,12 +80,14 @@ class ProviderOpenAIOfficial(Provider):
|
||||
raise Exception("API 返回的 completion 为空。")
|
||||
choice = completion.choices[0]
|
||||
|
||||
llm_response = LLMResponse("assistant")
|
||||
|
||||
if choice.message.content:
|
||||
# text completion
|
||||
completion_text = str(choice.message.content).strip()
|
||||
|
||||
return LLMResponse("assistant", completion_text, raw_completion=completion)
|
||||
elif choice.message.tool_calls:
|
||||
llm_response.completion_text = completion_text
|
||||
|
||||
if choice.message.tool_calls:
|
||||
# tools call (function calling)
|
||||
args_ls = []
|
||||
func_name_ls = []
|
||||
@@ -93,10 +97,15 @@ 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, raw_completion=completion)
|
||||
else:
|
||||
llm_response.role = "tool"
|
||||
llm_response.tools_call_args = args_ls
|
||||
llm_response.tools_call_name = func_name_ls
|
||||
|
||||
if not llm_response.completion_text and not llm_response.tools_call_args:
|
||||
logger.error(f"API 返回的 completion 无法解析:{completion}。")
|
||||
raise Exception("Internal Error")
|
||||
raise Exception(f"API 返回的 completion 无法解析:{completion}。")
|
||||
|
||||
return llm_response
|
||||
|
||||
async def text_chat(
|
||||
self,
|
||||
@@ -128,14 +137,21 @@ 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
|
||||
context_query = new_contexts
|
||||
llm_response = await self._query(payloads, func_tool)
|
||||
except Exception as e:
|
||||
if "maximum context length" in str(e):
|
||||
# 重试 10 次
|
||||
retry_cnt = 10
|
||||
retry_cnt = 20
|
||||
while retry_cnt > 0:
|
||||
logger.warning("上下文长度超过限制。尝试弹出最早的记录然后重试。")
|
||||
logger.warning(f"上下文长度超过限制。尝试弹出最早的记录然后重试。当前记录条数: {len(context_query)}")
|
||||
try:
|
||||
await self.pop_record(session_id)
|
||||
await self.pop_record(context_query)
|
||||
llm_response = await self._query(payloads, func_tool)
|
||||
break
|
||||
except Exception as e:
|
||||
@@ -156,8 +172,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 +182,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,10 +238,14 @@ 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)
|
||||
if not image_data:
|
||||
logger.warning(f"图片 {image_url} 得到的结果为空,将忽略。")
|
||||
continue
|
||||
user_content["content"].append({"type": "image_url", "image_url": {"url": image_data}})
|
||||
return user_content
|
||||
else:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
@@ -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})"
|
||||
@@ -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)
|
||||
@@ -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):
|
||||
|
||||
@@ -27,22 +27,38 @@ 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}")
|
||||
|
||||
buffer = ""
|
||||
while True:
|
||||
data = await resp.content.read(8192) # 防止数据过大导致高水位报错
|
||||
if not data:
|
||||
# 保持原有的8192字节限制,防止数据过大导致高水位报错
|
||||
chunk = await resp.content.read(8192)
|
||||
if not chunk:
|
||||
break
|
||||
if not data.strip():
|
||||
continue
|
||||
elif data.startswith(b"data:"):
|
||||
try:
|
||||
json_ = json.loads(data[5:])
|
||||
yield json_
|
||||
except BaseException:
|
||||
pass
|
||||
|
||||
buffer += chunk.decode('utf-8')
|
||||
blocks = buffer.split('\n\n')
|
||||
|
||||
# 处理完整的数据块
|
||||
for block in blocks[:-1]:
|
||||
if block.strip() and block.startswith('data:'):
|
||||
try:
|
||||
json_str = block[5:] # 移除 "data:" 前缀
|
||||
json_obj = json.loads(json_str)
|
||||
yield json_obj
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(f"JSON解析错误: {str(e)}")
|
||||
logger.error(f"原始数据块: {json_str}")
|
||||
|
||||
# 保留最后一个可能不完整的块
|
||||
buffer = blocks[-1] if blocks else ""
|
||||
|
||||
async def workflow_run(
|
||||
self,
|
||||
inputs: Dict,
|
||||
@@ -55,22 +71,38 @@ 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"workflow_run 请求失败:{resp.status}. {text}")
|
||||
|
||||
buffer = ""
|
||||
while True:
|
||||
data = await resp.content.read(8192) # 防止数据过大导致高水位报错
|
||||
if not data:
|
||||
# 保持原有的8192字节限制,防止数据过大导致高水位报错
|
||||
chunk = await resp.content.read(8192)
|
||||
if not chunk:
|
||||
break
|
||||
if not data.strip():
|
||||
continue
|
||||
elif data.startswith(b"data:"):
|
||||
try:
|
||||
json_ = json.loads(data[5:])
|
||||
yield json_
|
||||
except BaseException:
|
||||
pass
|
||||
|
||||
buffer += chunk.decode('utf-8')
|
||||
blocks = buffer.split('\n\n')
|
||||
|
||||
# 处理完整的数据块
|
||||
for block in blocks[:-1]:
|
||||
if block.strip() and block.startswith('data:'):
|
||||
try:
|
||||
json_str = block[5:] # 移除 "data:" 前缀
|
||||
json_obj = json.loads(json_str)
|
||||
yield json_obj
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(f"JSON解析错误: {str(e)}")
|
||||
logger.error(f"原始数据块: {json_str}")
|
||||
|
||||
# 保留最后一个可能不完整的块
|
||||
buffer = blocks[-1] if blocks else ""
|
||||
|
||||
async def file_upload(
|
||||
self,
|
||||
file_path: str,
|
||||
|
||||
@@ -14,11 +14,22 @@ class NetworkRenderStrategy(RenderStrategy):
|
||||
base_url = ASTRBOT_T2I_DEFAULT_ENDPOINT
|
||||
self.BASE_RENDER_URL = base_url
|
||||
self.TEMPLATE_PATH = os.path.join(os.path.dirname(__file__), "template")
|
||||
|
||||
if self.BASE_RENDER_URL.endswith("/"):
|
||||
self.BASE_RENDER_URL = self.BASE_RENDER_URL[:-1]
|
||||
if not self.BASE_RENDER_URL.endswith("text2img"):
|
||||
self.BASE_RENDER_URL += "/text2img"
|
||||
|
||||
def set_endpoint(self, base_url: str):
|
||||
if not base_url:
|
||||
base_url = ASTRBOT_T2I_DEFAULT_ENDPOINT
|
||||
self.BASE_RENDER_URL = base_url
|
||||
|
||||
if self.BASE_RENDER_URL.endswith("/"):
|
||||
self.BASE_RENDER_URL = self.BASE_RENDER_URL[:-1]
|
||||
if not self.BASE_RENDER_URL.endswith("text2img"):
|
||||
self.BASE_RENDER_URL += "/text2img"
|
||||
|
||||
|
||||
async def render_custom_template(self, tmpl_str: str, tmpl_data: dict, return_url: bool=True) -> str:
|
||||
'''使用自定义文转图模板'''
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1,12 @@
|
||||
# What's Changed
|
||||
|
||||
1. fix: 400 Bad Request: The browser (or proxy) sent a request that this server could not understand. #396
|
||||
2. remove: 移除了 put_history_to_prompt。当主动回复时,将群聊记录将自动放入prompt,当未主动回复但是开启群聊增强时,群聊记录将放入system prompt
|
||||
3. fix: 插件错误信息点击关闭没反应 #394
|
||||
4. fix: 自部署文转图不生效 #352
|
||||
5. fix: Google Search 报 429 错误时,放宽 Exception 至其他搜索引擎 #405
|
||||
6. fix: 使用 Google Gemini (OpenAI 兼容)的部分情况下联网搜索等函数调用工具没被调用 #342
|
||||
7. fix: 修复尝试弹出最早的记录失效的问题
|
||||
8. fix: 移除了分段回复llm提示词辅助
|
||||
9. perf: 当图片数据为空时不加入上下文 #379
|
||||
10. 修复 dify 返回的结果带有多行数据时的 json 解析异常导致返回值为空的问题 #298 by @zhaolj
|
||||
@@ -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">
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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,61 @@ 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>
|
||||
|
||||
<template v-slot:default="{ isActive }">
|
||||
<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 @click="isActive.value = false">关闭</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
</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 +80,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 +231,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 +289,8 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
extension_data: {
|
||||
"data": []
|
||||
"data": [],
|
||||
"message": ""
|
||||
},
|
||||
extension_url: "",
|
||||
status: "",
|
||||
@@ -227,7 +313,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 +366,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();
|
||||
});
|
||||
},
|
||||
@@ -284,7 +386,7 @@ export default {
|
||||
if (this.upload_file !== null) {
|
||||
this.toast("正在从文件安装插件", "primary");
|
||||
const formData = new FormData();
|
||||
formData.append('file', this.upload_file[0]);
|
||||
formData.append('file', this.upload_file);
|
||||
axios.post('/api/plugin/install-upload', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
@@ -295,7 +397,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 +418,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 +440,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 +458,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 +561,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");
|
||||
});
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
@@ -32,7 +33,7 @@ class LongTermMemory:
|
||||
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"]
|
||||
# self.put_history_to_prompt = self.config["put_history_to_prompt"]
|
||||
|
||||
async def remove_session(self, event: AstrMessageEvent) -> int:
|
||||
cnt = 0
|
||||
@@ -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,
|
||||
@@ -103,11 +110,11 @@ class LongTermMemory:
|
||||
|
||||
chats_str = '\n---\n'.join(self.session_chats[event.unified_msg_origin])
|
||||
|
||||
if self.put_history_to_prompt:
|
||||
if self.enable_active_reply:
|
||||
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中。
|
||||
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
|
||||
|
||||
@@ -80,6 +80,7 @@ AstrBot 指令:
|
||||
/persona: 人格情景(op)
|
||||
/tool ls: 函数工具
|
||||
/key: API Key(op)
|
||||
/websearch: 网页搜索
|
||||
|
||||
[其他]
|
||||
/set <变量名> <值>: 为会话定义变量。适用于 Dify 工作流输入。
|
||||
@@ -227,6 +228,7 @@ UID: {user_id} 此 ID 可用于设置管理员。/op <UID> 授权管理员, /deo
|
||||
@filter.permission_type(filter.PermissionType.ADMIN)
|
||||
@filter.command("wl")
|
||||
async def wl(self, event: AstrMessageEvent, sid: str):
|
||||
'''添加白名单。wl <sid>'''
|
||||
self.context.get_config()['platform_settings']['id_whitelist'].append(sid)
|
||||
self.context.get_config().save_config()
|
||||
event.set_result(MessageEventResult().message("添加白名单成功。"))
|
||||
@@ -234,6 +236,7 @@ UID: {user_id} 此 ID 可用于设置管理员。/op <UID> 授权管理员, /deo
|
||||
@filter.permission_type(filter.PermissionType.ADMIN)
|
||||
@filter.command("dwl")
|
||||
async def dwl(self, event: AstrMessageEvent, sid: str):
|
||||
'''删除白名单。dwl <sid>'''
|
||||
try:
|
||||
self.context.get_config()['platform_settings']['id_whitelist'].remove(sid)
|
||||
self.context.get_config().save_config()
|
||||
@@ -274,7 +277,7 @@ UID: {user_id} 此 ID 可用于设置管理员。/op <UID> 授权管理员, /deo
|
||||
@filter.permission_type(filter.PermissionType.ADMIN)
|
||||
@filter.command("reset")
|
||||
async def reset(self, message: AstrMessageEvent):
|
||||
|
||||
'''重置 LLM 会话'''
|
||||
if not self.context.get_using_provider():
|
||||
message.set_result(MessageEventResult().message("未找到任何 LLM 提供商。请先配置。"))
|
||||
return
|
||||
@@ -298,7 +301,7 @@ 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
|
||||
@@ -339,7 +342,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 +658,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 +668,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
@@ -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"])
|
||||
|
||||
|
||||
+28
-3
@@ -1,9 +1,30 @@
|
||||
import random
|
||||
from .config import HEADERS, USER_AGENTS
|
||||
from bs4 import BeautifulSoup
|
||||
from aiohttp import ClientSession
|
||||
from dataclasses import dataclass
|
||||
from typing import List
|
||||
import urllib.parse
|
||||
|
||||
HEADERS = {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; rv:84.0) Gecko/20100101 Firefox/84.0',
|
||||
'Accept': '*/*',
|
||||
'Connection': 'keep-alive',
|
||||
'Accept-Language': 'en-GB,en;q=0.5'
|
||||
}
|
||||
|
||||
USER_AGENT_BING = 'Mozilla/5.0 (Windows NT 6.1; rv:84.0) Gecko/20100101 Firefox/84.0'
|
||||
USER_AGENTS = [
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36',
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:89.0) Gecko/20100101 Firefox/89.0',
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:88.0) Gecko/20100101 Firefox/88.0',
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36',
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Version/14.1.2 Safari/537.36',
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Version/14.1 Safari/537.36',
|
||||
'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:89.0) Gecko/20100101 Firefox/89.0',
|
||||
'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:88.0) Gecko/20100101 Firefox/88.0'
|
||||
]
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -38,11 +59,13 @@ class SearchEngine():
|
||||
if data:
|
||||
async with ClientSession() as session:
|
||||
async with session.post(url, headers=headers, data=data, timeout=self.TIMEOUT) as resp:
|
||||
return await resp.text(encoding="utf-8")
|
||||
ret = await resp.text(encoding="utf-8")
|
||||
return ret
|
||||
else:
|
||||
async with ClientSession() as session:
|
||||
async with session.get(url, headers=headers, timeout=self.TIMEOUT) as resp:
|
||||
return await resp.text(encoding="utf-8")
|
||||
ret = await resp.text(encoding="utf-8")
|
||||
return ret
|
||||
|
||||
|
||||
def tidy_text(self, text: str) -> str:
|
||||
@@ -53,6 +76,8 @@ class SearchEngine():
|
||||
|
||||
|
||||
async def search(self, query: str, num_results: int) -> List[SearchResult]:
|
||||
query = urllib.parse.quote(query)
|
||||
|
||||
try:
|
||||
resp = await self._get_next_page(query)
|
||||
soup = BeautifulSoup(resp, 'html.parser')
|
||||
@@ -1,11 +1,11 @@
|
||||
from typing import List
|
||||
from .engine import SearchEngine, SearchResult
|
||||
from .config import USER_AGENT_BING
|
||||
from . import SearchEngine, SearchResult
|
||||
from . import USER_AGENT_BING
|
||||
|
||||
class Bing(SearchEngine):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.base_url = "https://www.bing.com"
|
||||
self.base_urls = ["https://cn.bing.com", "https://www.bing.com"]
|
||||
self.headers.update({'User-Agent': USER_AGENT_BING})
|
||||
|
||||
def _set_selector(self, selector: str):
|
||||
@@ -19,11 +19,17 @@ class Bing(SearchEngine):
|
||||
return selectors[selector]
|
||||
|
||||
async def _get_next_page(self, query) -> str:
|
||||
if self.page == 1:
|
||||
await self._get_html(self.base_url)
|
||||
url = f'{self.base_url}/search?q={query}&form=QBLH&sp=-1&lq=0&pq=hi&sc=10-2&qs=n&sk=&cvid=DE75965E2D6346D681288933984DE48F&ghsh=0&ghacc=0&ghpl='
|
||||
return await self._get_html(url, None)
|
||||
|
||||
# if self.page == 1:
|
||||
# await self._get_html(self.base_url)
|
||||
for base_url in self.base_urls:
|
||||
try:
|
||||
url = f'{base_url}/search?q={query}'
|
||||
return await self._get_html(url, None)
|
||||
except Exception as _:
|
||||
self.base_url = base_url
|
||||
continue
|
||||
raise Exception("Bing search failed")
|
||||
|
||||
async def search(self, query: str, num_results: int) -> List[SearchResult]:
|
||||
results = await super().search(query, num_results)
|
||||
for result in results:
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
HEADERS = {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; rv:84.0) Gecko/20100101 Firefox/84.0',
|
||||
'Accept': '*/*',
|
||||
'Connection': 'keep-alive',
|
||||
'Accept-Language': 'en-GB,en;q=0.5'
|
||||
}
|
||||
|
||||
USER_AGENT_BING = 'Mozilla/5.0 (Windows NT 6.1; rv:84.0) Gecko/20100101 Firefox/84.0'
|
||||
USER_AGENTS = [
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36',
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:89.0) Gecko/20100101 Firefox/89.0',
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:88.0) Gecko/20100101 Firefox/88.0',
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36',
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Version/14.1.2 Safari/537.36',
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Version/14.1 Safari/537.36',
|
||||
'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:89.0) Gecko/20100101 Firefox/89.0',
|
||||
'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:88.0) Gecko/20100101 Firefox/88.0'
|
||||
]
|
||||
@@ -1,7 +1,7 @@
|
||||
import os
|
||||
from googlesearch import search
|
||||
|
||||
from .engine import SearchEngine, SearchResult
|
||||
from . import SearchEngine, SearchResult
|
||||
|
||||
from typing import List
|
||||
|
||||
@@ -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,8 +1,8 @@
|
||||
import random
|
||||
import re
|
||||
from bs4 import BeautifulSoup
|
||||
from .engine import SearchEngine, SearchResult
|
||||
from .config import USER_AGENTS
|
||||
from . import SearchEngine, SearchResult
|
||||
from . import USER_AGENTS
|
||||
|
||||
from typing import List
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ from .engines.sogo import Sogo
|
||||
from .engines.google import Google
|
||||
from readability import Document
|
||||
from bs4 import BeautifulSoup
|
||||
from .engines.config import HEADERS, USER_AGENTS
|
||||
from .engines import HEADERS, USER_AGENTS
|
||||
|
||||
|
||||
@star.register(name="astrbot-web-searcher", desc="让 LLM 具有网页检索能力", author="Soulter", version="1.14.514")
|
||||
@@ -85,19 +85,19 @@ class Main(star.Star):
|
||||
RESULT_NUM = 5
|
||||
try:
|
||||
results = await self.google.search(query, RESULT_NUM)
|
||||
except BaseException as e:
|
||||
except Exception as e:
|
||||
logger.error(f"google search error: {e}, try the next one...")
|
||||
if len(results) == 0:
|
||||
logger.debug("search google failed")
|
||||
try:
|
||||
results = await self.bing_search.search(query, RESULT_NUM)
|
||||
except BaseException as e:
|
||||
except Exception as e:
|
||||
logger.error(f"bing search error: {e}, try the next one...")
|
||||
if len(results) == 0:
|
||||
logger.debug("search bing failed")
|
||||
try:
|
||||
results = await self.sogo_search.search(query, RESULT_NUM)
|
||||
except BaseException as e:
|
||||
except Exception as e:
|
||||
logger.error(f"sogo search error: {e}")
|
||||
if len(results) == 0:
|
||||
logger.debug("search sogo failed")
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user