Compare commits
32 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0c8ebc2b06 | |||
| b3b5ebc2ca | |||
| b8aa23ccc5 | |||
| 364843db29 | |||
| aa56c8f7e6 | |||
| 8e9fd27058 | |||
| b75908cb2a | |||
| af6df49ce1 | |||
| bd3bdb5769 | |||
| 98fe193b21 | |||
| 26cbc9e8b1 | |||
| ebb8c43fd0 | |||
| 8c7344f1c4 | |||
| 5c32a17787 | |||
| aff520e69a | |||
| 45e627c33c | |||
| 7a1b158f83 | |||
| 6374c5d49d | |||
| fd460b19d4 | |||
| dff7cc4ca5 | |||
| d013320bec | |||
| fc6dcfaf21 | |||
| a001270bd2 | |||
| 9e67883fbd | |||
| f1a448708c | |||
| a4bfa96502 | |||
| 595b83a256 | |||
| c6f037cae2 | |||
| f049830e28 | |||
| dd1995ae0b | |||
| 7155b4f0ac | |||
| 0021cfc4bc |
@@ -5,6 +5,7 @@ from astrbot.core.star.register import (
|
||||
register_regex as regex,
|
||||
register_platform_adapter_type as platform_adapter_type,
|
||||
register_permission_type as permission_type,
|
||||
register_custom_filter as custom_filter,
|
||||
register_on_llm_request as on_llm_request,
|
||||
register_on_llm_response as on_llm_response,
|
||||
register_llm_tool as llm_tool,
|
||||
@@ -15,6 +16,7 @@ from astrbot.core.star.register import (
|
||||
from astrbot.core.star.filter.event_message_type import EventMessageTypeFilter, EventMessageType
|
||||
from astrbot.core.star.filter.platform_adapter_type import PlatformAdapterTypeFilter, PlatformAdapterType
|
||||
from astrbot.core.star.filter.permission import PermissionTypeFilter, PermissionType
|
||||
from astrbot.core.star.filter.custom_filter import CustomFilter
|
||||
|
||||
__all__ = [
|
||||
'command',
|
||||
@@ -28,6 +30,8 @@ __all__ = [
|
||||
'PlatformAdapterTypeFilter',
|
||||
'PlatformAdapterType',
|
||||
'PermissionTypeFilter',
|
||||
'CustomFilter',
|
||||
'custom_filter',
|
||||
'PermissionType',
|
||||
'on_llm_request',
|
||||
'llm_tool',
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
如需修改配置,请在 `data/cmd_config.json` 中修改或者在管理面板中可视化修改。
|
||||
"""
|
||||
|
||||
VERSION = "3.4.27"
|
||||
VERSION = "3.4.28"
|
||||
DB_PATH = "data/data_v3.db"
|
||||
|
||||
# 默认配置
|
||||
@@ -32,7 +32,8 @@ DEFAULT_CONFIG = {
|
||||
"interval": "1.5,3.5",
|
||||
"log_base": 2.6,
|
||||
"words_count_threshold": 150,
|
||||
"regex": ".*?[。?!~…]+|.+$"
|
||||
"regex": ".*?[。?!~…]+|.+$",
|
||||
"content_cleanup_rule": "",
|
||||
},
|
||||
"no_permission_reply": True,
|
||||
},
|
||||
@@ -271,6 +272,12 @@ CONFIG_METADATA_2 = {
|
||||
"obvious_hint": True,
|
||||
"hint": "用于分隔一段消息。默认情况下会根据句号、问号等标点符号分隔。re.findall(r'<regex>', text)",
|
||||
},
|
||||
"content_cleanup_rule": {
|
||||
"description": "过滤分段后的内容",
|
||||
"type": "string",
|
||||
"obvious_hint": True,
|
||||
"hint": "移除分段后的内容中的指定的内容。支持正则表达式。如填写 `[。?!]` 将移除所有的句号、问号、感叹号。re.sub(r'<regex>', '', text)",
|
||||
},
|
||||
},
|
||||
},
|
||||
"reply_prefix": {
|
||||
@@ -496,6 +503,7 @@ CONFIG_METADATA_2 = {
|
||||
"dify_api_key": "",
|
||||
"dify_api_base": "https://api.dify.ai/v1",
|
||||
"dify_workflow_output_key": "",
|
||||
"dify_query_input_key": "astrbot_text_query",
|
||||
"timeout": 60,
|
||||
},
|
||||
"whisper(API)": {
|
||||
@@ -647,6 +655,12 @@ CONFIG_METADATA_2 = {
|
||||
"type": "string",
|
||||
"hint": "Dify Workflow 输出变量名。当应用类型为 workflow 时才使用。默认为 astrbot_wf_output。",
|
||||
},
|
||||
"dify_query_input_key": {
|
||||
"description": "Prompt 输入变量名",
|
||||
"type": "string",
|
||||
"hint": "发送的消息文本内容对应的输入变量名。默认为 astrbot_text_query。",
|
||||
"obvious": True,
|
||||
}
|
||||
},
|
||||
},
|
||||
"provider_settings": {
|
||||
|
||||
@@ -27,7 +27,8 @@ class AstrBotCoreLifecycle:
|
||||
|
||||
os.environ['https_proxy'] = self.astrbot_config['http_proxy']
|
||||
os.environ['http_proxy'] = self.astrbot_config['http_proxy']
|
||||
|
||||
os.environ['no_proxy'] = 'localhost,127.0.0.1'
|
||||
|
||||
async def initialize(self):
|
||||
logger.info("AstrBot v"+ VERSION)
|
||||
if os.environ.get("TESTING", ""):
|
||||
|
||||
@@ -35,7 +35,6 @@ class ProcessStage(Stage):
|
||||
# 生成器返回值处理
|
||||
if isinstance(resp, ProviderRequest):
|
||||
# Handler 的 LLM 请求
|
||||
logger.debug(f"llm request -> {resp.prompt}")
|
||||
event.set_extra("provider_request", resp)
|
||||
_t = False
|
||||
async for _ in self.llm_request_sub_stage.process(event):
|
||||
|
||||
@@ -7,14 +7,17 @@ from ..context import PipelineContext
|
||||
from astrbot.core.platform.astr_message_event import AstrMessageEvent
|
||||
from astrbot.core.message.message_event_result import MessageChain
|
||||
from astrbot.core import logger
|
||||
from astrbot.core.message.message_event_result import BaseMessageComponent, Plain
|
||||
from astrbot.core.message.message_event_result import BaseMessageComponent
|
||||
from astrbot.core.star.star_handler import star_handlers_registry, EventType
|
||||
|
||||
from astrbot.core.message.components import Plain, Reply, At
|
||||
@register_stage
|
||||
class RespondStage(Stage):
|
||||
async def initialize(self, ctx: PipelineContext):
|
||||
self.ctx = ctx
|
||||
|
||||
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.enable_seg: bool = ctx.astrbot_config['platform_settings']['segmented_reply']['enable']
|
||||
self.only_llm_result = ctx.astrbot_config['platform_settings']['segmented_reply']['only_llm_result']
|
||||
@@ -60,11 +63,24 @@ class RespondStage(Stage):
|
||||
await event._pre_send()
|
||||
|
||||
if self.enable_seg and ((self.only_llm_result and result.is_llm_result()) or not self.only_llm_result):
|
||||
decorated_comps = []
|
||||
if self.reply_with_mention:
|
||||
for comp in result.chain:
|
||||
if isinstance(comp, At):
|
||||
decorated_comps.append(comp)
|
||||
result.chain.remove(comp)
|
||||
break
|
||||
if self.reply_with_quote:
|
||||
for comp in result.chain:
|
||||
if isinstance(comp, Reply):
|
||||
decorated_comps.append(comp)
|
||||
result.chain.remove(comp)
|
||||
break
|
||||
# 分段回复
|
||||
for comp in result.chain:
|
||||
i = await self._calc_comp_interval(comp)
|
||||
await asyncio.sleep(i)
|
||||
await event.send(MessageChain([comp]))
|
||||
await event.send(MessageChain([*decorated_comps, comp]))
|
||||
else:
|
||||
await event.send(result)
|
||||
await event._post_send()
|
||||
|
||||
@@ -7,7 +7,7 @@ from ..context import PipelineContext
|
||||
from astrbot.core.platform.astr_message_event import AstrMessageEvent
|
||||
from astrbot.core.platform.message_type import MessageType
|
||||
from astrbot.core import logger
|
||||
from astrbot.core.message.components import Plain, Image, At, Reply, Record
|
||||
from astrbot.core.message.components import Plain, Image, At, Reply, Record, File, Node
|
||||
from astrbot.core import html_renderer
|
||||
from astrbot.core.star.star_handler import star_handlers_registry, EventType
|
||||
|
||||
@@ -25,12 +25,16 @@ class ResultDecorateStage(Stage):
|
||||
self.t2i_word_threshold = 50
|
||||
except BaseException:
|
||||
self.t2i_word_threshold = 150
|
||||
|
||||
self.forward_threshold = ctx.astrbot_config['platform_settings']['forward_threshold']
|
||||
|
||||
# 分段回复
|
||||
self.words_count_threshold = int(ctx.astrbot_config['platform_settings']['segmented_reply']['words_count_threshold'])
|
||||
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.regex = ctx.astrbot_config['platform_settings']['segmented_reply']['regex']
|
||||
self.content_cleanup_rule = ctx.astrbot_config['platform_settings']['segmented_reply']['content_cleanup_rule']
|
||||
|
||||
|
||||
# exception
|
||||
self.content_safe_check_reply = ctx.astrbot_config['content_safety']['also_use_in_response']
|
||||
@@ -82,12 +86,16 @@ class ResultDecorateStage(Stage):
|
||||
# 不分段回复
|
||||
new_chain.append(comp)
|
||||
continue
|
||||
split_response = re.findall(self.regex, comp.text)
|
||||
split_response = []
|
||||
for line in comp.text.split("\n"):
|
||||
split_response.extend(re.findall(self.regex, line))
|
||||
if not split_response:
|
||||
new_chain.append(comp)
|
||||
continue
|
||||
for seg in split_response:
|
||||
if seg:
|
||||
if self.content_cleanup_rule:
|
||||
seg = re.sub(self.content_cleanup_rule, "", seg)
|
||||
if seg.strip():
|
||||
new_chain.append(Plain(seg))
|
||||
else:
|
||||
# 非 Plain 类型的消息段不分段
|
||||
@@ -137,12 +145,32 @@ class ResultDecorateStage(Stage):
|
||||
if url:
|
||||
result.chain = [Image.fromURL(url)]
|
||||
|
||||
# at 回复
|
||||
if self.reply_with_mention and event.get_message_type() != MessageType.FRIEND_MESSAGE:
|
||||
result.chain.insert(0, At(qq=event.get_sender_id(), name=event.get_sender_name()))
|
||||
if len(result.chain) > 1 and isinstance(result.chain[1], Plain):
|
||||
result.chain[1].text = "\n" + result.chain[1].text
|
||||
# 触发转发消息
|
||||
has_forwarded = False
|
||||
if event.get_platform_name() == 'aiocqhttp':
|
||||
word_cnt = 0
|
||||
for comp in result.chain:
|
||||
if isinstance(comp, Plain):
|
||||
word_cnt += len(comp.text)
|
||||
if word_cnt > self.forward_threshold:
|
||||
node = Node(
|
||||
uin=event.get_self_id(),
|
||||
name="AstrBot",
|
||||
content=[
|
||||
*result.chain
|
||||
]
|
||||
)
|
||||
result.chain = [node]
|
||||
has_forwarded = True
|
||||
|
||||
# 引用回复
|
||||
if self.reply_with_quote:
|
||||
result.chain.insert(0, Reply(id=event.message_obj.message_id))
|
||||
if not has_forwarded:
|
||||
# at 回复
|
||||
if self.reply_with_mention and event.get_message_type() != MessageType.FRIEND_MESSAGE:
|
||||
result.chain.insert(0, At(qq=event.get_sender_id(), name=event.get_sender_name()))
|
||||
if len(result.chain) > 1 and isinstance(result.chain[1], Plain):
|
||||
result.chain[1].text = "\n" + result.chain[1].text
|
||||
|
||||
# 引用回复
|
||||
if self.reply_with_quote:
|
||||
if not any(isinstance(item, File) for item in result.chain):
|
||||
result.chain.insert(0, Reply(id=event.message_obj.message_id))
|
||||
|
||||
@@ -12,14 +12,14 @@ class PipelineScheduler():
|
||||
|
||||
async def initialize(self):
|
||||
for stage in registered_stages:
|
||||
logger.debug(f"初始化阶段 {stage.__class__ .__name__}")
|
||||
# logger.debug(f"初始化阶段 {stage.__class__ .__name__}")
|
||||
|
||||
await stage.initialize(self.ctx)
|
||||
|
||||
async def _process_stages(self, event: AstrMessageEvent, from_stage=0):
|
||||
for i in range(from_stage, len(registered_stages)):
|
||||
stage = registered_stages[i]
|
||||
logger.debug(f"执行阶段 {stage.__class__ .__name__}")
|
||||
# logger.debug(f"执行阶段 {stage.__class__ .__name__}")
|
||||
coro = stage.process(event)
|
||||
if isinstance(coro, AsyncGenerator):
|
||||
async for _ in coro:
|
||||
|
||||
@@ -2,6 +2,7 @@ import os
|
||||
import time
|
||||
import asyncio
|
||||
import logging
|
||||
import uuid
|
||||
from typing import Awaitable, Any
|
||||
from aiocqhttp import CQHttp, Event
|
||||
from astrbot.api.platform import Platform, AstrBotMessage, MessageMember, MessageType, PlatformMetadata
|
||||
@@ -46,18 +47,81 @@ class AiocqhttpAdapter(Platform):
|
||||
await super().send_by_session(session, message_chain)
|
||||
|
||||
async def convert_message(self, event: Event) -> AstrBotMessage:
|
||||
logger.debug(f"[aiocqhttp] RawMessage {event}")
|
||||
|
||||
if event['post_type'] == 'message':
|
||||
abm = await self._convert_handle_message_event(event)
|
||||
elif event['post_type'] == 'notice':
|
||||
abm = await self._convert_handle_notice_event(event)
|
||||
elif event['post_type'] == 'request':
|
||||
abm = await self._convert_handle_request_event(event)
|
||||
|
||||
return abm
|
||||
|
||||
async def _convert_handle_request_event(self, event: Event) -> AstrBotMessage:
|
||||
'''OneBot V11 请求类事件'''
|
||||
abm = AstrBotMessage()
|
||||
abm.self_id = str(event.self_id)
|
||||
abm.tag = "aiocqhttp"
|
||||
abm.sender = MessageMember(
|
||||
user_id=event.user_id,
|
||||
nickname=event.user_id
|
||||
)
|
||||
abm.type = MessageType.OTHER_MESSAGE
|
||||
if 'group_id' in event and event['group_id']:
|
||||
abm.type = MessageType.GROUP_MESSAGE
|
||||
abm.group_id = str(event.group_id)
|
||||
else:
|
||||
abm.type = MessageType.FRIEND_MESSAGE
|
||||
if self.unique_session and abm.type == MessageType.GROUP_MESSAGE:
|
||||
abm.session_id = abm.sender.user_id + "_" + str(event.group_id)
|
||||
abm.message_str = ''
|
||||
abm.message = []
|
||||
abm.timestamp = int(time.time())
|
||||
abm.message_id = uuid.uuid4().hex
|
||||
abm.raw_message = event
|
||||
return abm
|
||||
|
||||
async def _convert_handle_notice_event(self, event: Event) -> AstrBotMessage:
|
||||
'''OneBot V11 通知类事件'''
|
||||
abm = AstrBotMessage()
|
||||
abm.self_id = str(event.self_id)
|
||||
abm.sender = MessageMember(
|
||||
user_id=event.user_id,
|
||||
nickname=event.user_id
|
||||
)
|
||||
abm.type = MessageType.OTHER_MESSAGE
|
||||
if 'group_id' in event and event['group_id']:
|
||||
abm.group_id = str(event.group_id)
|
||||
abm.type = MessageType.GROUP_MESSAGE
|
||||
else:
|
||||
abm.type = MessageType.FRIEND_MESSAGE
|
||||
if self.unique_session and abm.type == MessageType.GROUP_MESSAGE:
|
||||
abm.session_id = abm.sender.user_id + "_" + str(event.group_id) # 也保留群组 id
|
||||
else:
|
||||
abm.session_id = str(event.group_id) if abm.type == MessageType.GROUP_MESSAGE else abm.sender.user_id
|
||||
abm.message_str = ""
|
||||
abm.message = []
|
||||
abm.raw_message = event
|
||||
abm.timestamp = int(time.time())
|
||||
abm.message_id = uuid.uuid4().hex
|
||||
|
||||
abm.sender = MessageMember(str(event.sender['user_id']), event.sender['nickname'])
|
||||
|
||||
if 'sub_type' in event:
|
||||
if event['sub_type'] == 'poke' and 'target_id' in event:
|
||||
abm.message.append(Poke(qq=str(event['target_id']), type='poke')) # noqa: F405
|
||||
|
||||
return abm
|
||||
|
||||
|
||||
async def _convert_handle_message_event(self, event: Event) -> AstrBotMessage:
|
||||
'''OneBot V11 消息类事件'''
|
||||
abm = AstrBotMessage()
|
||||
abm.self_id = str(event.self_id)
|
||||
abm.sender = MessageMember(str(event.sender['user_id']), event.sender['nickname'])
|
||||
if event['message_type'] == 'group':
|
||||
abm.type = MessageType.GROUP_MESSAGE
|
||||
abm.group_id = str(event.group_id)
|
||||
elif event['message_type'] == 'private':
|
||||
abm.type = MessageType.FRIEND_MESSAGE
|
||||
|
||||
if self.unique_session and abm.type == MessageType.GROUP_MESSAGE:
|
||||
abm.session_id = abm.sender.user_id + "_" + str(event.group_id) # 也保留群组 id
|
||||
else:
|
||||
@@ -75,7 +139,8 @@ class AiocqhttpAdapter(Platform):
|
||||
except BaseException as e:
|
||||
logger.error(f"回复消息失败: {e}")
|
||||
return
|
||||
logger.debug(f"aiocqhttp: 收到消息: {event.message}")
|
||||
|
||||
# 按消息段类型类型适配
|
||||
for m in event.message:
|
||||
t = m['type']
|
||||
a = None
|
||||
@@ -118,6 +183,7 @@ class AiocqhttpAdapter(Platform):
|
||||
abm.timestamp = int(time.time())
|
||||
abm.message_str = message_str
|
||||
abm.raw_message = event
|
||||
|
||||
return abm
|
||||
|
||||
def run(self) -> Awaitable[Any]:
|
||||
@@ -127,6 +193,19 @@ class AiocqhttpAdapter(Platform):
|
||||
self.port = 6199
|
||||
|
||||
self.bot = CQHttp(use_ws_reverse=True, import_name='aiocqhttp', api_timeout_sec=180)
|
||||
|
||||
@self.bot.on_request()
|
||||
async def request(event: Event):
|
||||
abm = await self.convert_message(event)
|
||||
if abm:
|
||||
await self.handle_msg(abm)
|
||||
|
||||
@self.bot.on_notice()
|
||||
async def notice(event: Event):
|
||||
abm = await self.convert_message(event)
|
||||
if abm:
|
||||
await self.handle_msg(abm)
|
||||
|
||||
@self.bot.on_message('group')
|
||||
async def group(event: Event):
|
||||
abm = await self.convert_message(event)
|
||||
|
||||
@@ -134,6 +134,6 @@ class GewechatPlatformEvent(AstrMessageEvent):
|
||||
elif isinstance(comp, At):
|
||||
pass
|
||||
else:
|
||||
logger.error(f"gewechat 暂不支持发送消息类型: {comp.type}")
|
||||
logger.debug(f"gewechat 忽略: {comp.type}")
|
||||
|
||||
await super().send(message)
|
||||
@@ -5,7 +5,7 @@ import botpy.types.message
|
||||
from astrbot.core.utils.io import file_to_base64, download_image_by_url
|
||||
from astrbot.api.event import AstrMessageEvent, MessageChain
|
||||
from astrbot.api.platform import AstrBotMessage, PlatformMetadata
|
||||
from astrbot.api.message_components import Plain, Image, Reply
|
||||
from astrbot.api.message_components import Plain, Image
|
||||
from botpy import Client
|
||||
from botpy.http import Route
|
||||
from astrbot.api import logger
|
||||
@@ -33,19 +33,6 @@ class QQOfficialMessageEvent(AstrMessageEvent):
|
||||
if not plain_text and not image_base64 and not image_path:
|
||||
return
|
||||
|
||||
ref = None
|
||||
for i in self.send_buffer.chain:
|
||||
if isinstance(i, Reply):
|
||||
try:
|
||||
ref = self.message_obj.raw_message.message_reference
|
||||
ref = botpy.types.message.Reference(
|
||||
message_id=ref.message_id,
|
||||
ignore_get_message_error=False
|
||||
)
|
||||
except BaseException as _:
|
||||
pass
|
||||
break
|
||||
|
||||
payload = {
|
||||
'content': plain_text,
|
||||
'msg_id': self.message_obj.message_id,
|
||||
@@ -53,30 +40,22 @@ class QQOfficialMessageEvent(AstrMessageEvent):
|
||||
|
||||
match type(source):
|
||||
case botpy.message.GroupMessage:
|
||||
if ref:
|
||||
payload['message_reference'] = ref
|
||||
if image_base64:
|
||||
media = await self.upload_group_and_c2c_image(image_base64, 1, group_openid=source.group_openid)
|
||||
payload['media'] = media
|
||||
payload['msg_type'] = 7
|
||||
await self.bot.api.post_group_message(group_openid=source.group_openid, **payload)
|
||||
case botpy.message.C2CMessage:
|
||||
if ref:
|
||||
payload['message_reference'] = ref
|
||||
if image_base64:
|
||||
media = await self.upload_group_and_c2c_image(image_base64, 1, openid=source.author.user_openid)
|
||||
payload['media'] = media
|
||||
payload['msg_type'] = 7
|
||||
await self.bot.api.post_c2c_message(openid=source.author.user_openid, **payload)
|
||||
case botpy.message.Message:
|
||||
if ref:
|
||||
payload['message_reference'] = ref
|
||||
if image_path:
|
||||
payload['file_image'] = image_path
|
||||
await self.bot.api.post_message(channel_id=source.channel_id, **payload)
|
||||
case botpy.message.DirectMessage:
|
||||
if ref:
|
||||
payload['message_reference'] = ref
|
||||
if image_path:
|
||||
payload['file_image'] = image_path
|
||||
await self.bot.api.post_dms(guild_id=source.guild_id, **payload)
|
||||
@@ -119,5 +98,5 @@ class QQOfficialMessageEvent(AstrMessageEvent):
|
||||
image_base64 = file_to_base64(i.file).replace("base64://", "")
|
||||
image_file_path = i.file
|
||||
else:
|
||||
logger.error(f"qq_official 暂不支持发送消息类型 {i.type}")
|
||||
logger.debug(f"qq_official 忽略 {i.type}")
|
||||
return plain_text, image_base64, image_file_path
|
||||
@@ -39,6 +39,6 @@ class WebChatMessageEvent(AstrMessageEvent):
|
||||
f.write(f2.read())
|
||||
web_chat_back_queue.put_nowait((f"[IMAGE]{filename}", cid))
|
||||
else:
|
||||
logger.error(f"webchat 暂不支持发送消息类型: {comp.type}")
|
||||
logger.debug(f"webchat 忽略: {comp.type}")
|
||||
web_chat_back_queue.put_nowait(None)
|
||||
await super().send(message)
|
||||
@@ -31,6 +31,9 @@ class ProviderDify(Provider):
|
||||
raise Exception("Dify API 类型不能为空。")
|
||||
self.model_name = "dify"
|
||||
self.workflow_output_key = provider_config.get("dify_workflow_output_key", "astrbot_wf_output")
|
||||
self.dify_query_input_key = provider_config.get("dify_query_input_key", "astrbot_text_query")
|
||||
if not self.dify_query_input_key:
|
||||
self.dify_query_input_key = "astrbot_text_query"
|
||||
self.timeout = provider_config.get("timeout", 120)
|
||||
if isinstance(self.timeout, str):
|
||||
self.timeout = int(self.timeout)
|
||||
@@ -95,7 +98,7 @@ class ProviderDify(Provider):
|
||||
case "workflow":
|
||||
async for chunk in self.api_client.workflow_run(
|
||||
inputs={
|
||||
"astrbot_text_query": prompt,
|
||||
self.dify_query_input_key: prompt,
|
||||
"astrbot_session_id": session_id,
|
||||
**session_var
|
||||
},
|
||||
@@ -121,7 +124,7 @@ class ProviderDify(Provider):
|
||||
return LLMResponse(role="assistant", completion_text=result)
|
||||
|
||||
async def forget(self, session_id):
|
||||
self.conversation_ids.pop(session_id, None)
|
||||
self.conversation_ids[session_id] = ""
|
||||
return True
|
||||
|
||||
async def get_current_key(self):
|
||||
|
||||
@@ -1,19 +1,23 @@
|
||||
|
||||
import re
|
||||
import inspect
|
||||
from typing import List
|
||||
from . import HandlerFilter
|
||||
from astrbot.core.platform.astr_message_event import AstrMessageEvent
|
||||
from astrbot.core.config import AstrBotConfig
|
||||
from astrbot.core.utils.param_validation_mixin import ParameterValidationMixin
|
||||
from .custom_filter import CustomFilter
|
||||
from ..star_handler import StarHandlerMetadata
|
||||
|
||||
# 标准指令受到 wake_prefix 的制约。
|
||||
class CommandFilter(HandlerFilter, ParameterValidationMixin):
|
||||
'''标准指令过滤器'''
|
||||
def __init__(self, command_name: str, handler_md: StarHandlerMetadata = None):
|
||||
def __init__(self, command_name: str, alias: set = None, handler_md: StarHandlerMetadata = None):
|
||||
self.command_name = command_name
|
||||
self.alias = alias if alias else set()
|
||||
if handler_md:
|
||||
self.init_handler_md(handler_md)
|
||||
self.custom_filter_list: List[CustomFilter] = []
|
||||
|
||||
def print_types(self):
|
||||
result = ""
|
||||
@@ -42,10 +46,22 @@ class CommandFilter(HandlerFilter, ParameterValidationMixin):
|
||||
def get_handler_md(self) -> StarHandlerMetadata:
|
||||
return self.handler_md
|
||||
|
||||
def add_custom_filter(self, custom_filter: CustomFilter):
|
||||
self.custom_filter_list.append(custom_filter)
|
||||
|
||||
def custom_filter_ok(self, event: AstrMessageEvent, cfg: AstrBotConfig) -> bool:
|
||||
for custom_filter in self.custom_filter_list:
|
||||
if not custom_filter.filter(event, cfg):
|
||||
return False
|
||||
return True
|
||||
|
||||
def filter(self, event: AstrMessageEvent, cfg: AstrBotConfig) -> bool:
|
||||
if not event.is_at_or_wake_command:
|
||||
return False
|
||||
|
||||
|
||||
if not self.custom_filter_ok(event, cfg):
|
||||
return False
|
||||
|
||||
if event.get_extra("parsing_command"):
|
||||
message_str = event.get_extra("parsing_command").strip()
|
||||
else:
|
||||
@@ -53,7 +69,7 @@ class CommandFilter(HandlerFilter, ParameterValidationMixin):
|
||||
|
||||
# 分割为列表(每个参数之间可能会有多个空格)
|
||||
ls = re.split(r"\s+", message_str)
|
||||
if self.command_name != ls[0]:
|
||||
if self.command_name != ls[0] and ls[0] not in self.alias:
|
||||
return False
|
||||
# if len(self.handler_params) == 0 and len(ls) > 1:
|
||||
# # 一定程度避免 LLM 聊天时误判为指令
|
||||
|
||||
@@ -6,36 +6,61 @@ from . import HandlerFilter
|
||||
from .command import CommandFilter
|
||||
from astrbot.core.platform.astr_message_event import AstrMessageEvent
|
||||
from astrbot.core.config import AstrBotConfig
|
||||
from .custom_filter import CustomFilter
|
||||
from ..star_handler import StarHandlerMetadata
|
||||
|
||||
# 指令组受到 wake_prefix 的制约。
|
||||
class CommandGroupFilter(HandlerFilter):
|
||||
def __init__(self, group_name: str):
|
||||
def __init__(self, group_name: str, alias: set = None):
|
||||
self.group_name = group_name
|
||||
self.alias = alias if alias else set()
|
||||
self.sub_command_filters: List[Union[CommandFilter, CommandGroupFilter]] = []
|
||||
self.custom_filter_list: List[CustomFilter] = []
|
||||
|
||||
def add_sub_command_filter(self, sub_command_filter: Union[CommandFilter, CommandGroupFilter]):
|
||||
self.sub_command_filters.append(sub_command_filter)
|
||||
|
||||
|
||||
def add_custom_filter(self, custom_filter: CustomFilter):
|
||||
self.custom_filter_list.append(custom_filter)
|
||||
|
||||
# 以树的形式打印出来
|
||||
def print_cmd_tree(self, sub_command_filters: List[Union[CommandFilter, CommandGroupFilter]], prefix: str = "") -> str:
|
||||
def print_cmd_tree(self,
|
||||
sub_command_filters: List[Union[CommandFilter, CommandGroupFilter]],
|
||||
prefix: str = "",
|
||||
event: AstrMessageEvent = None,
|
||||
cfg: AstrBotConfig = None,
|
||||
) -> str:
|
||||
result = ""
|
||||
for sub_filter in sub_command_filters:
|
||||
if isinstance(sub_filter, CommandFilter):
|
||||
cmd_th = sub_filter.print_types()
|
||||
result += f"{prefix}├── {sub_filter.command_name}"
|
||||
if cmd_th:
|
||||
result += f" ({cmd_th})"
|
||||
else:
|
||||
result += " (无参数指令)"
|
||||
|
||||
result += "\n"
|
||||
custom_filter_pass = True
|
||||
if event and cfg:
|
||||
custom_filter_pass = sub_filter.custom_filter_ok(event, cfg)
|
||||
if custom_filter_pass:
|
||||
cmd_th = sub_filter.print_types()
|
||||
result += f"{prefix}├── {sub_filter.command_name}"
|
||||
if cmd_th:
|
||||
result += f" ({cmd_th})"
|
||||
else:
|
||||
result += " (无参数指令)"
|
||||
result += "\n"
|
||||
elif isinstance(sub_filter, CommandGroupFilter):
|
||||
result += f"{prefix}├── {sub_filter.group_name}"
|
||||
result += "\n"
|
||||
result += sub_filter.print_cmd_tree(sub_filter.sub_command_filters, prefix+"│ ")
|
||||
custom_filter_pass = True
|
||||
if event and cfg:
|
||||
custom_filter_pass = sub_filter.custom_filter_ok(event, cfg)
|
||||
if custom_filter_pass:
|
||||
result += f"{prefix}├── {sub_filter.group_name}"
|
||||
result += "\n"
|
||||
result += sub_filter.print_cmd_tree(sub_filter.sub_command_filters, prefix+"│ ", event=event, cfg=cfg)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def custom_filter_ok(self, event: AstrMessageEvent, cfg: AstrBotConfig) -> bool:
|
||||
for custom_filter in self.custom_filter_list:
|
||||
if not custom_filter.filter(event, cfg):
|
||||
return False
|
||||
return True
|
||||
|
||||
def filter(self, event: AstrMessageEvent, cfg: AstrBotConfig) -> Tuple[bool, StarHandlerMetadata]:
|
||||
if not event.is_at_or_wake_command:
|
||||
return False, None
|
||||
@@ -46,8 +71,8 @@ class CommandGroupFilter(HandlerFilter):
|
||||
message_str = event.get_message_str().strip()
|
||||
|
||||
ls = re.split(r"\s+", message_str)
|
||||
|
||||
if ls[0] != self.group_name:
|
||||
|
||||
if ls[0] != self.group_name and ls[0] not in self.alias:
|
||||
return False, None
|
||||
# 改写 message_str
|
||||
ls = ls[1:]
|
||||
@@ -56,12 +81,16 @@ class CommandGroupFilter(HandlerFilter):
|
||||
parsing_command = " ".join(ls)
|
||||
parsing_command = parsing_command.strip()
|
||||
event.set_extra("parsing_command", parsing_command)
|
||||
|
||||
|
||||
# 判断当前指令组的自定义过滤器
|
||||
if not self.custom_filter_ok(event, cfg):
|
||||
return False, None
|
||||
|
||||
if parsing_command == "":
|
||||
# 当前还是指令组
|
||||
tree = self.group_name + "\n" + self.print_cmd_tree(self.sub_command_filters)
|
||||
tree = self.group_name + "\n" + self.print_cmd_tree(self.sub_command_filters, event=event, cfg=cfg)
|
||||
raise ValueError(f"指令组 {self.group_name} 未填写完全。这个指令组下有如下指令:\n"+tree)
|
||||
|
||||
|
||||
child_command_handler_md = None
|
||||
for sub_filter in self.sub_command_filters:
|
||||
if isinstance(sub_filter, CommandFilter):
|
||||
@@ -73,5 +102,5 @@ class CommandGroupFilter(HandlerFilter):
|
||||
if ok:
|
||||
child_command_handler_md = handler
|
||||
return True, child_command_handler_md
|
||||
tree = self.group_name + "\n" + self.print_cmd_tree(self.sub_command_filters)
|
||||
tree = self.group_name + "\n" + self.print_cmd_tree(self.sub_command_filters, event=event, cfg=cfg)
|
||||
raise ValueError(f"指令组 {self.group_name} 下没有找到对应的指令。这个指令组下有如下指令:\n"+tree)
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
from abc import abstractmethod, ABCMeta
|
||||
|
||||
from . import HandlerFilter
|
||||
from astrbot.core.platform.astr_message_event import AstrMessageEvent
|
||||
from astrbot.core.config import AstrBotConfig
|
||||
|
||||
class CustomFilterMeta(ABCMeta):
|
||||
def __and__(cls, other):
|
||||
if not issubclass(other, CustomFilter):
|
||||
raise TypeError("Operands must be subclasses of CustomFilter.")
|
||||
return CustomFilterAnd(cls(), other())
|
||||
|
||||
def __or__(cls, other):
|
||||
if not issubclass(other, CustomFilter):
|
||||
raise TypeError("Operands must be subclasses of CustomFilter.")
|
||||
return CustomFilterOr(cls(), other())
|
||||
|
||||
class CustomFilter(HandlerFilter, metaclass=CustomFilterMeta):
|
||||
def __init__(self, raise_error: bool = True, **kwargs):
|
||||
self.raise_error = raise_error
|
||||
|
||||
@abstractmethod
|
||||
def filter(self, event: AstrMessageEvent, cfg: AstrBotConfig) -> bool:
|
||||
''' 一个用于重写的自定义Filter '''
|
||||
raise NotImplementedError
|
||||
|
||||
def __or__(self, other):
|
||||
return CustomFilterOr(self, other)
|
||||
|
||||
def __and__(self, other):
|
||||
return CustomFilterAnd(self, other)
|
||||
|
||||
class CustomFilterOr(CustomFilter):
|
||||
def __init__(self, filter1: CustomFilter, filter2: CustomFilter):
|
||||
super().__init__()
|
||||
if not isinstance(filter1, (CustomFilter, CustomFilterAnd, CustomFilterOr)):
|
||||
raise ValueError("CustomFilter lass can only operate with other CustomFilter.")
|
||||
self.filter1 = filter1
|
||||
self.filter2 = filter2
|
||||
|
||||
def filter(self, event: AstrMessageEvent, cfg: AstrBotConfig) -> bool:
|
||||
return self.filter1.filter(event, cfg) or self.filter2.filter(event, cfg)
|
||||
|
||||
class CustomFilterAnd(CustomFilter):
|
||||
def __init__(self, filter1: CustomFilter, filter2: CustomFilter):
|
||||
super().__init__()
|
||||
if not isinstance(filter1, (CustomFilter, CustomFilterAnd, CustomFilterOr)):
|
||||
raise ValueError("CustomFilter lass can only operate with other CustomFilter.")
|
||||
self.filter1 = filter1
|
||||
self.filter2 = filter2
|
||||
|
||||
def filter(self, event: AstrMessageEvent, cfg: AstrBotConfig) -> bool:
|
||||
return self.filter1.filter(event, cfg) and self.filter2.filter(event, cfg)
|
||||
@@ -6,6 +6,7 @@ from .star_handler import (
|
||||
register_platform_adapter_type,
|
||||
register_regex,
|
||||
register_permission_type,
|
||||
register_custom_filter,
|
||||
register_on_llm_request,
|
||||
register_on_llm_response,
|
||||
register_llm_tool,
|
||||
@@ -21,6 +22,7 @@ __all__ = [
|
||||
'register_platform_adapter_type',
|
||||
'register_regex',
|
||||
'register_permission_type',
|
||||
'register_custom_filter',
|
||||
'register_on_llm_request',
|
||||
'register_on_llm_response',
|
||||
'register_llm_tool',
|
||||
|
||||
@@ -7,6 +7,7 @@ from ..filter.command_group import CommandGroupFilter
|
||||
from ..filter.event_message_type import EventMessageTypeFilter, EventMessageType
|
||||
from ..filter.platform_adapter_type import PlatformAdapterTypeFilter, PlatformAdapterType
|
||||
from ..filter.permission import PermissionTypeFilter, PermissionType
|
||||
from ..filter.custom_filter import CustomFilterAnd, CustomFilterOr
|
||||
from ..filter.regex import RegexFilter
|
||||
from typing import Awaitable
|
||||
from astrbot.core.provider.func_tool_manager import SUPPORTED_TYPES
|
||||
@@ -50,7 +51,7 @@ def get_handler_or_create(
|
||||
star_handlers_registry.append(md)
|
||||
return md
|
||||
|
||||
def register_command(command_name: str = None, *args, **kwargs):
|
||||
def register_command(command_name: str = None, sub_command: str = None, alias: set = None, **kwargs):
|
||||
'''注册一个 Command.
|
||||
'''
|
||||
|
||||
@@ -60,11 +61,11 @@ def register_command(command_name: str = None, *args, **kwargs):
|
||||
add_to_event_filters = False
|
||||
if isinstance(command_name, RegisteringCommandable):
|
||||
# 子指令
|
||||
new_command = CommandFilter(args[0], None)
|
||||
new_command = CommandFilter(sub_command, alias, None)
|
||||
command_name.parent_group.add_sub_command_filter(new_command)
|
||||
else:
|
||||
# 裸指令
|
||||
new_command = CommandFilter(command_name, None)
|
||||
new_command = CommandFilter(command_name, alias, None)
|
||||
add_to_event_filters = True
|
||||
|
||||
def decorator(awaitable):
|
||||
@@ -80,7 +81,65 @@ def register_command(command_name: str = None, *args, **kwargs):
|
||||
|
||||
return decorator
|
||||
|
||||
def register_command_group(command_group_name: str = None, *args, **kwargs):
|
||||
|
||||
def register_custom_filter(custom_type_filter, *args, **kwargs):
|
||||
'''注册一个自定义的 CustomFilter
|
||||
|
||||
Args:
|
||||
custom_type_filter: 在裸指令时为CustomFilter对象
|
||||
在指令组时为父指令的RegisteringCommandable对象,即self或者command_group的返回
|
||||
raise_error: 如果没有权限,是否抛出错误到消息平台,并且停止事件传播。默认为 True
|
||||
'''
|
||||
add_to_event_filters = False
|
||||
raise_error = True
|
||||
|
||||
# 判断是否是指令组,指令组则添加到指令组的CommandGroupFilter对象中在waking_check的时候一起判断
|
||||
if isinstance(custom_type_filter, RegisteringCommandable):
|
||||
# 子指令, 此时函数为RegisteringCommandable对象的方法,首位参数为RegisteringCommandable对象的self。
|
||||
parent_register_commandable = custom_type_filter
|
||||
custom_filter = args[0]
|
||||
if len(args) > 1:
|
||||
raise_error = args[1]
|
||||
else:
|
||||
# 裸指令
|
||||
add_to_event_filters = True
|
||||
custom_filter = custom_type_filter
|
||||
if args:
|
||||
raise_error = args[0]
|
||||
|
||||
if not isinstance(custom_filter, (CustomFilterAnd, CustomFilterOr)):
|
||||
custom_filter = custom_filter(raise_error)
|
||||
|
||||
def decorator(awaitable):
|
||||
# 裸指令,子指令与指令组的区分,指令组会因为标记跳过wake。
|
||||
if not add_to_event_filters and isinstance(awaitable, RegisteringCommandable) or \
|
||||
(add_to_event_filters and isinstance(awaitable, RegisteringCommandable)):
|
||||
# 指令组 与 根指令组,添加到本层的grouphandle中一起判断
|
||||
awaitable.parent_group.add_custom_filter(custom_filter)
|
||||
else:
|
||||
handler_md = get_handler_or_create(awaitable, EventType.AdapterMessageEvent, **kwargs)
|
||||
|
||||
if not add_to_event_filters and not isinstance(awaitable, RegisteringCommandable):
|
||||
# 底层子指令
|
||||
handle_full_name = get_handler_full_name(awaitable)
|
||||
for sub_handle in parent_register_commandable.parent_group.sub_command_filters:
|
||||
# 所有符合fullname一致的子指令handle添加自定义过滤器。
|
||||
# 不确定是否会有多个子指令有一样的fullname,比如一个方法添加多个command装饰器?
|
||||
sub_handle_md = sub_handle.get_handler_md()
|
||||
if sub_handle_md and sub_handle_md.handler_full_name == handle_full_name:
|
||||
sub_handle.add_custom_filter(custom_filter)
|
||||
|
||||
else:
|
||||
# 裸指令
|
||||
handler_md = get_handler_or_create(awaitable, EventType.AdapterMessageEvent, **kwargs)
|
||||
handler_md.event_filters.append(custom_filter)
|
||||
|
||||
return awaitable
|
||||
return decorator
|
||||
|
||||
def register_command_group(
|
||||
command_group_name: str = None, sub_command: str = None, alias: set = None, **kwargs
|
||||
):
|
||||
'''注册一个 CommandGroup
|
||||
'''
|
||||
|
||||
@@ -90,11 +149,11 @@ def register_command_group(command_group_name: str = None, *args, **kwargs):
|
||||
add_to_event_filters = False
|
||||
if isinstance(command_group_name, RegisteringCommandable):
|
||||
# 子指令组
|
||||
new_group = CommandGroupFilter(args[0])
|
||||
new_group = CommandGroupFilter(sub_command, alias)
|
||||
command_group_name.parent_group.add_sub_command_filter(new_group)
|
||||
else:
|
||||
# 根指令组
|
||||
new_group = CommandGroupFilter(command_group_name)
|
||||
new_group = CommandGroupFilter(command_group_name, alias)
|
||||
add_to_event_filters = True
|
||||
|
||||
def decorator(obj):
|
||||
@@ -102,7 +161,7 @@ def register_command_group(command_group_name: str = None, *args, **kwargs):
|
||||
# 根指令组
|
||||
handler_md = get_handler_or_create(obj, EventType.AdapterMessageEvent, **kwargs)
|
||||
handler_md.event_filters.append(new_group)
|
||||
|
||||
|
||||
return RegisteringCommandable(new_group)
|
||||
|
||||
return decorator
|
||||
@@ -111,7 +170,8 @@ class RegisteringCommandable():
|
||||
'''用于指令组级联注册'''
|
||||
group = register_command_group
|
||||
command = register_command
|
||||
|
||||
custom_filter = register_custom_filter
|
||||
|
||||
def __init__(self, parent_group: CommandGroupFilter):
|
||||
self.parent_group = parent_group
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ class HtmlRenderer:
|
||||
|
||||
@return: 图片 URL 或者文件路径,取决于 return_url 参数。
|
||||
|
||||
@example: 参见 https://astrbot.soulter.top 插件开发部分。
|
||||
@example: 参见 https://astrbot.app 插件开发部分。
|
||||
'''
|
||||
local = locals()
|
||||
local.pop('self')
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
# What's Changed
|
||||
|
||||
1. ✨ 新增: 管理面板支持搜索插件
|
||||
2. ✨ 新增: 支持传递 OneBot 的 notice, request 事件类型,如戳一戳,进退群请求等
|
||||
3. ✨ 新增: 插件支持自定义过滤算子 by @AraragiEro
|
||||
4. ✨ 新增: 添加命令和命令组的别名支持 by @Cvandia
|
||||
4. ✨ 新增: 提供了一个方法以删除分段回复后的某些字符,如末尾的标点符号。 by @Soulter and @Nothingness-Void
|
||||
5. ⚡ 优化: 优化了分段回复和回复时at,引用都打开时的一些体验性问题
|
||||
7. 🐛 修复: 分段回复导致了不完全的非 LLM 输出 #503
|
||||
8. 🐛 修复: 添加 no_proxy 环境变量以支持本地请求, 修复在代理状态下时的 502 错误当通过 LMStudio, Ollama 本地部署 LLM 时 #504 #514
|
||||
9. 💡🐛 修复: 修复转发消息的字数阈值功能 #510
|
||||
10. 💡🐛 修复: 修复 Dify 下无法主动回复的问题 #494
|
||||
@@ -4,6 +4,7 @@ const props = defineProps({
|
||||
link: String,
|
||||
logo: String,
|
||||
has_update: Boolean,
|
||||
activated: Boolean,
|
||||
});
|
||||
|
||||
const open = (link: string | undefined) => {
|
||||
@@ -16,8 +17,9 @@ const open = (link: string | undefined) => {
|
||||
<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: 16px;">{{ props.title }}</v-card-title>
|
||||
<v-card-title style="font-size: 15px; max-width: 70%">{{ props.title }}</v-card-title>
|
||||
<v-spacer></v-spacer>
|
||||
<v-icon color="success" v-if="!activated">mdi-cancel</v-icon>
|
||||
<v-icon color="success" v-if="has_update">mdi-arrow-up-bold</v-icon>
|
||||
<v-btn size="small" text="Read" variant="flat" border @click="open(props.link)">帮助</v-btn>
|
||||
</div>
|
||||
|
||||
@@ -21,7 +21,7 @@ const sidebarMenu = shallowRef(sidebarItems);
|
||||
</div>
|
||||
|
||||
<div style="position: absolute; bottom: 32px; width: 100%" class="text-center">
|
||||
<v-list-item v-if="!customizer.mini_sidebar" href="https://astrbot.soulter.top/">
|
||||
<v-list-item v-if="!customizer.mini_sidebar" href="https://astrbot.app/">
|
||||
<v-btn variant="plain" size="small">
|
||||
🤔 初次使用?点击查看文档!
|
||||
</v-btn>
|
||||
|
||||
@@ -91,7 +91,7 @@ import config from '@/config';
|
||||
|
||||
<div style="margin-left: 16px; padding-bottom: 16px">
|
||||
<small>不了解配置?请见 <a
|
||||
href="https://astrbot.soulter.top/">官方文档</a>
|
||||
href="https://astrbot.app/">官方文档</a>
|
||||
或 <a
|
||||
href="https://qm.qq.com/cgi-bin/qm/qr?k=EYGsuUTfe00_iOu9JTXS7_TEpMkXOvwv&jump_from=webapi&authKey=uUEMKCROfsseS+8IzqPjzV3y1tzy4AkykwTib2jNkOFdzezF9s9XknqnIaf3CDft">加群询问</a>。</small>
|
||||
</div>
|
||||
|
||||
@@ -10,8 +10,8 @@ import { max } from 'date-fns';
|
||||
|
||||
<template>
|
||||
<v-row>
|
||||
<v-alert style="margin: 16px" text="1. 如果因为网络问题安装失败,点击设置页选择 GitHub 加速地址。或前往仓库下载压缩包然后本地上传。" title="💡提示"
|
||||
type="info" color="primary" variant="tonal">
|
||||
<v-alert style="margin: 16px" text="1. 如果因为网络问题安装失败,点击设置页选择 GitHub 加速地址。或前往仓库下载压缩包然后本地上传。" title="💡提示" type="info"
|
||||
color="primary" variant="tonal">
|
||||
</v-alert>
|
||||
<v-col cols="12" md="12">
|
||||
<div style="background-color: white; width: 100%; padding: 16px; border-radius: 10px;">
|
||||
@@ -44,14 +44,14 @@ import { max } from 'date-fns';
|
||||
</v-dialog>
|
||||
</div>
|
||||
</div>
|
||||
</v-col>
|
||||
</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"
|
||||
:has_update="extension.has_update" style="margin-bottom: 4px;">
|
||||
:has_update="extension.has_update" style="margin-bottom: 4px;" :activated="extension.activated">
|
||||
<div style="min-height: 140px; max-height: 140px; overflow: auto;">
|
||||
<div>
|
||||
<span style="font-weight: bold ;">By @{{ extension.author }}</span>
|
||||
<span> | 插件有 {{ extension.handlers.length }} 个行为</span>
|
||||
<span> | {{ extension.handlers.length }} 个行为</span>
|
||||
</div>
|
||||
<span> 当前: <v-chip size="small" color="primary">{{ extension.version }}</v-chip>
|
||||
<span v-if="extension.online_version">
|
||||
@@ -82,51 +82,88 @@ import { max } from 'date-fns';
|
||||
</div>
|
||||
</ExtensionCard>
|
||||
</v-col>
|
||||
<v-col cols="12" md="12">
|
||||
<div style="background-color: white; width: 100%; padding: 16px; border-radius: 10px;">
|
||||
<div style="display: flex; align-items: center;">
|
||||
<h3>🧩 插件市场</h3>
|
||||
<small style="margin-left: 16px;">如无法显示,请打开 <a
|
||||
href="https://soulter.github.io/AstrBot_Plugins_Collection/plugins.json">链接</a> 复制想安装插件对应的 `repo`
|
||||
链接然后点击右下角 + 号安装,或打开链接下载压缩包安装。</small>
|
||||
<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>
|
||||
</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 cols="12" md="12">
|
||||
<v-card>
|
||||
<v-card-title class="d-flex align-center pe-2">
|
||||
|
||||
🧩 插件市场
|
||||
|
||||
<v-btn icon size="small" style="margin-left: 8px" variant="plain">
|
||||
<v-icon size="small">mdi-help</v-icon>
|
||||
<v-tooltip activator="parent" location="start">
|
||||
如无法显示,请打开 <a href="https://soulter.github.io/AstrBot_Plugins_Collection/plugins.json">链接</a> 复制想安装插件对应的
|
||||
`repo`
|
||||
链接然后点击右下角 + 号安装,或打开链接下载压缩包安装。
|
||||
</v-tooltip>
|
||||
</v-btn>
|
||||
|
||||
<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>
|
||||
|
||||
<v-spacer></v-spacer>
|
||||
|
||||
<v-text-field v-model="marketSearch" density="compact" label="Search" prepend-inner-icon="mdi-magnify"
|
||||
variant="solo-filled" flat hide-details single-line></v-text-field>
|
||||
</v-card-title>
|
||||
|
||||
<v-divider></v-divider>
|
||||
|
||||
<template v-if="isListView">
|
||||
<v-col cols="12" md="12">
|
||||
<v-data-table :headers="pluginMarketHeaders" :items="pluginMarketData" item-key="name"
|
||||
v-model:search="marketSearch" :filter-keys="['name']">
|
||||
<template v-slot:item.name="{ item }">
|
||||
<span v-if="item?.repo"><a :href="item?.repo" style="color: #000; text-decoration:none">{{ item.name }}</a></span>
|
||||
<span v-else>{{ item.name}}</span>
|
||||
</template>
|
||||
<template v-slot:item.author="{ item }">
|
||||
<span v-if="item?.social_link"><a :href="item?.social_link">{{ item.author}}</a></span>
|
||||
<span v-else>{{ item.author}}</span>
|
||||
</template>
|
||||
<template v-slot:item.tags="{ item }">
|
||||
<span v-if="item.tags.length === 0">无</span>
|
||||
<v-chip v-for="tag in item.tags" :key="tag" color="primary" size="small">{{ tag }}</v-chip>
|
||||
</template>
|
||||
<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-row style="margin: 8px;">
|
||||
<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>
|
||||
</v-row>
|
||||
|
||||
</template>
|
||||
|
||||
</v-card>
|
||||
|
||||
</v-col>
|
||||
|
||||
|
||||
<v-col style="margin-bottom: 16px;" cols="12" md="12">
|
||||
<small><a href="https://astrbot.app/dev/plugin.html">插件开发文档</a></small> |
|
||||
@@ -314,7 +351,7 @@ export default {
|
||||
"config": {}
|
||||
},
|
||||
upload_file: null,
|
||||
pluginMarketData: {},
|
||||
pluginMarketData: [],
|
||||
loadingDialog: {
|
||||
show: false,
|
||||
title: "加载中...",
|
||||
@@ -331,13 +368,15 @@ export default {
|
||||
{ title: '具体类型', key: 'type' },
|
||||
{ title: '触发方式', key: 'cmd' },
|
||||
],
|
||||
isListView: false,
|
||||
isListView: true,
|
||||
pluginMarketHeaders: [
|
||||
{ title: '名称', value: 'name' },
|
||||
{ title: '描述', value: 'desc' },
|
||||
{ title: '作者', value: 'author' },
|
||||
{ title: '操作', value: 'actions', sortable: false }
|
||||
{ title: '名称', key: 'name', maxWidth: '150px' },
|
||||
{ title: '描述', key: 'desc', maxWidth: '250px' },
|
||||
{ title: '作者', key: 'author', maxWidth: '60px' },
|
||||
{ title: '标签', key: 'tags', maxWidth: '60px' },
|
||||
{ title: '操作', key: 'actions', sortable: false }
|
||||
],
|
||||
marketSearch: "",
|
||||
alreadyCheckUpdate: false
|
||||
}
|
||||
},
|
||||
@@ -562,6 +601,8 @@ export default {
|
||||
"repo": res.data.data[key].repo,
|
||||
"installed": false,
|
||||
"version": res.data.data[key]?.version ? res.data.data[key].version : "未知",
|
||||
"social_link": res.data.data[key]?.social_link,
|
||||
"tags": res.data.data[key]?.tags ? res.data.data[key].tags : []
|
||||
})
|
||||
}
|
||||
this.pluginMarketData = data;
|
||||
|
||||
+25
-16
@@ -6,8 +6,7 @@ import astrbot.api.star as star
|
||||
import astrbot.api.event.filter as filter
|
||||
from astrbot.api.event import AstrMessageEvent, MessageEventResult
|
||||
from astrbot.api import sp
|
||||
from astrbot.api.platform import MessageType
|
||||
from astrbot.api.provider import Personality, ProviderRequest, LLMResponse
|
||||
from astrbot.api.provider import ProviderRequest
|
||||
from astrbot.core.provider.sources.dify_source import ProviderDify
|
||||
from astrbot.core.utils.io import download_dashboard, get_dashboard_version
|
||||
from astrbot.core.star.star_handler import star_handlers_registry, StarHandlerMetadata
|
||||
@@ -40,7 +39,7 @@ class Main(star.Star):
|
||||
async def _query_astrbot_notice(self):
|
||||
try:
|
||||
async with aiohttp.ClientSession(trust_env=True) as session:
|
||||
async with session.get("https://astrbot.soulter.top/notice.json", timeout=2) as resp:
|
||||
async with session.get("https://astrbot.app/notice.json", timeout=2) as resp:
|
||||
return (await resp.json())["notice"]
|
||||
except BaseException:
|
||||
return ""
|
||||
@@ -775,20 +774,30 @@ UID: {user_id} 此 ID 可用于设置管理员。/op <UID> 授权管理员, /deo
|
||||
logger.error("未找到任何 LLM 提供商。请先配置。无法主动回复")
|
||||
return
|
||||
try:
|
||||
session_curr_cid = await self.context.conversation_manager.get_curr_conversation_id(event.unified_msg_origin)
|
||||
|
||||
if not session_curr_cid:
|
||||
logger.error("当前未处于对话状态,无法主动回复,请确保 平台设置->会话隔离(unique_session) 未开启,并使用 /switch 序号 切换或者 /new 创建一个会话。")
|
||||
return
|
||||
|
||||
conv = await self.context.conversation_manager.get_conversation(
|
||||
event.unified_msg_origin,
|
||||
session_curr_cid
|
||||
)
|
||||
conv = None
|
||||
history = []
|
||||
if conv:
|
||||
history = json.loads(conv.history)
|
||||
|
||||
if provider.meta().type != 'dify':
|
||||
# Dify 自己有维护对话,不需要 bot 端维护。
|
||||
session_curr_cid = await self.context.conversation_manager.get_curr_conversation_id(event.unified_msg_origin)
|
||||
|
||||
if not session_curr_cid:
|
||||
logger.error("当前未处于对话状态,无法主动回复,请确保 平台设置->会话隔离(unique_session) 未开启,并使用 /switch 序号 切换或者 /new 创建一个会话。")
|
||||
return
|
||||
|
||||
conv = await self.context.conversation_manager.get_conversation(
|
||||
event.unified_msg_origin,
|
||||
session_curr_cid
|
||||
)
|
||||
history = []
|
||||
if conv:
|
||||
history = json.loads(conv.history)
|
||||
else:
|
||||
assert isinstance(provider, ProviderDify)
|
||||
cid = provider.conversation_ids.get(event.unified_msg_origin, None)
|
||||
if cid is None:
|
||||
logger.error("[Dify] 当前未处于对话状态,无法主动回复,请确保 平台设置->会话隔离(unique_session) 未开启,并使用 /switch 序号 切换或者 /new 创建一个会话。")
|
||||
return
|
||||
|
||||
prompt = self.ltm.ar_prompt
|
||||
if not prompt:
|
||||
prompt = event.message_str
|
||||
|
||||
@@ -75,10 +75,10 @@ class Main(star.Star):
|
||||
|
||||
@llm_tool("web_search")
|
||||
async def search_from_search_engine(self, event: AstrMessageEvent, query: str) -> str:
|
||||
'''Search the web for answers to the user's query
|
||||
'''搜索网络以回答用户的问题。当用户需要搜索网络以获取即时性的信息时调用此工具。
|
||||
|
||||
Args:
|
||||
query(string): A search query which will be used to fetch the most relevant snippets regarding the user's query
|
||||
query(string): 和用户的问题最相关的搜索关键词,用于在 Google 上搜索。
|
||||
'''
|
||||
logger.info("web_searcher - search_from_search_engine: " + query)
|
||||
results = []
|
||||
|
||||
Reference in New Issue
Block a user