Compare commits

...

12 Commits

Author SHA1 Message Date
Soulter a876efb95f fix: 更新后覆盖文件路径错误 2024-08-10 04:35:07 -04:00
Soulter 95a8cc9498 fix: 修复部分字段未更新导致的错误 2024-08-10 04:13:24 -04:00
Soulter f02731055e fix: 修复插件启用忽略前缀之后可能的逻辑冲突 2024-08-10 03:25:50 -04:00
Soulter 1df83addfc update: add gcc 2024-08-10 14:59:00 +08:00
Soulter 9db43ac5e6 feat: 注册指令支持忽略指令前缀;快捷主动回复 2024-08-10 02:35:54 -04:00
Soulter 0f470cf96f Update README.md 2024-08-09 12:26:00 +08:00
Soulter da3fcb7b86 Merge pull request #186 from itgpt-com/master
优化 docker build
2024-08-08 22:15:48 +08:00
Soulter 73dd4703b9 Update .dockerignore 2024-08-08 22:15:05 +08:00
itgpt 0c679a0151 添加 .dockerignore 过滤 docker cp 不必要文件。缩小镜像 2024-08-08 16:21:30 +08:00
itgpt 1d6ea2dbe6 添加端口输出 2024-08-08 16:16:55 +08:00
itgpt 933df57654 优化 docker build 2024-08-08 15:53:44 +08:00
Soulter cbe761fc33 Update README.md 2024-08-07 00:49:00 +08:00
18 changed files with 308 additions and 80 deletions
+18
View File
@@ -0,0 +1,18 @@
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# github acions
.github/
.*ignore
.git/
# User-specific stuff
.idea/
# Byte-compiled / optimized / DLL files
__pycache__/
# Environments
.env
.venv
env/
venv*/
ENV/
.conda/
README*.md
+32 -13
View File
@@ -4,20 +4,39 @@ on:
release: release:
types: [published] types: [published]
workflow_dispatch: workflow_dispatch:
jobs: jobs:
publish-latest-docker-image: publish-docker:
runs-on: ubuntu-latest runs-on: ubuntu-latest
name: Build and publish docker image
steps: steps:
- name: Checkout - name: 拉取源码
uses: actions/checkout@v2 uses: actions/checkout@v3
- name: Build image with:
run: | fetch-depth: 1
git clone https://github.com/Soulter/AstrBot
cd AstrBot - name: 设置 QEMU
docker build -t ${{ secrets.DOCKER_HUB_USERNAME }}/astrbot:latest . uses: docker/setup-qemu-action@v3
- name: Publish image
run: | - name: 设置 Docker Buildx
docker login -u ${{ secrets.DOCKER_HUB_USERNAME }} -p ${{ secrets.DOCKER_HUB_PASSWORD }} uses: docker/setup-buildx-action@v3
docker push ${{ secrets.DOCKER_HUB_USERNAME }}/astrbot:latest
- name: 登录到 DockerHub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_PASSWORD }}
- name: 构建和推送 Docker hub
uses: docker/build-push-action@v6
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: |
${{ secrets.DOCKER_HUB_USERNAME }}/astrbot:latest
${{ secrets.DOCKER_HUB_USERNAME }}/astrbot:${{ github.event.release.tag_name }}
- name: Post build notifications
run: echo "Docker image has been built and pushed successfully"
+12
View File
@@ -3,6 +3,18 @@ WORKDIR /AstrBot
COPY . /AstrBot/ COPY . /AstrBot/
RUN apt-get update && apt-get install -y --no-install-recommends \
gcc \
build-essential \
python3-dev \
libffi-dev \
libssl-dev \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
RUN python -m pip install -r requirements.txt RUN python -m pip install -r requirements.txt
EXPOSE 6185
EXPOSE 6186
CMD [ "python", "main.py" ] CMD [ "python", "main.py" ]
+19 -7
View File
@@ -1,6 +1,6 @@
<p align="center"> <p align="center">
<img width="806" alt="image" src="https://github.com/Soulter/AstrBot/assets/37870767/c6f057d9-46d7-4144-8116-00a962941746"> <img width="750" alt="image" src="https://github.com/Soulter/AstrBot/assets/37870767/c6f057d9-46d7-4144-8116-00a962941746">
</p> </p>
<div align="center"> <div align="center">
@@ -21,27 +21,39 @@
🌍 支持的消息平台 🌍 支持的消息平台
- QQ 群、QQ 频道(OneBot、QQ 官方接口) - QQ 群、QQ 频道(OneBot、QQ 官方接口)
- Telegram[astrbot_plugin_telegram](https://github.com/Soulter/astrbot_plugin_telegram) 插件支持 - Telegram[astrbot_plugin_telegram](https://github.com/Soulter/astrbot_plugin_telegram) 插件)
- WeChat(微信) ([astrbot_plugin_vchat](https://github.com/z2z63/astrbot_plugin_vchat) 插件支持) - WeChat(微信) ([astrbot_plugin_vchat](https://github.com/z2z63/astrbot_plugin_vchat) 插件)
🌍 支持的大模型一览 🌍 支持的大模型/底座
- OpenAI GPT、DallE 系列 - OpenAI GPT、DallE 系列
- Claude(由[LLMs插件](https://github.com/Soulter/llms)支持) - Claude(由[LLMs插件](https://github.com/Soulter/llms)支持)
- HuggingChat(由[LLMs插件](https://github.com/Soulter/llms)支持) - HuggingChat(由[LLMs插件](https://github.com/Soulter/llms)支持)
- Gemini(由[LLMs插件](https://github.com/Soulter/llms)支持) - Gemini(由[LLMs插件](https://github.com/Soulter/llms)支持)
- Ollama
- 几乎所有已知模型(可接入 [OneAPI](https://astrbot.soulter.top/docs/docs/adavanced/one-api)
🌍 机器人支持的能力一览: 🌍 机器人支持的能力一览:
- 大模型对话、人格、网页搜索 - 大模型对话、人格、网页搜索
- 可视化管理面板 - 可视化仪表盘
- 同时处理多平台消息 - 同时处理多平台消息
- 精确到个人的会话隔离 - 精确到个人的会话隔离
- 插件支持 - 插件支持
- 文本转图片回复(Markdown - 文本转图片回复(Markdown
## 🧩 插件支持 ## 🧩 插件
有关插件的使用和列表请移步:[AstrBot 文档 - 插件](https://astrbot.soulter.top/center/docs/%E4%BD%BF%E7%94%A8/%E6%8F%92%E4%BB%B6) 有关插件的使用和列表请移步:[AstrBot 文档 - 插件](https://astrbot.soulter.top/docs/get-started/plugin)
## ❤️ 贡献
欢迎任何 Issues/Pull Requests!只需要将你的更改提交到此项目 :)
对于新功能的添加,请先通过 Issue 进行讨论。
## 🔭 展望
- [ ] 更多、更开放的 LLM Agent 能力
## ✨ Demo ## ✨ Demo
+1
View File
@@ -78,6 +78,7 @@ class AstrBotBootstrap():
self.context.updator = self.updator self.context.updator = self.updator
self.context.plugin_updator = self.plugin_manager.updator self.context.plugin_updator = self.plugin_manager.updator
self.context.message_handler = self.message_handler self.context.message_handler = self.message_handler
self.context.command_manager = self.command_manager
# load plugins, plugins' commands. # load plugins, plugins' commands.
self.load_plugins() self.load_plugins()
+7 -2
View File
@@ -134,8 +134,8 @@ class MessageHandler():
self.persist_manager.record_message(message.platform.platform_name, message.session_id) self.persist_manager.record_message(message.platform.platform_name, message.session_id)
# TODO: this should be configurable # TODO: this should be configurable
if not message.message_str: # if not message.message_str:
return MessageResult("Hi~") # return MessageResult("Hi~")
# check the rate limit # check the rate limit
if not self.rate_limit_helper.check_frequency(message.message_obj.sender.user_id): if not self.rate_limit_helper.check_frequency(message.message_obj.sender.user_id):
@@ -158,6 +158,11 @@ class MessageHandler():
use_t2i=cmd_res.is_use_t2i use_t2i=cmd_res.is_use_t2i
) )
# next is the LLM part
if message.only_command:
return
# check if the message is a llm-wake-up command # check if the message is a llm-wake-up command
if self.llm_wake_prefix and not msg_plain.startswith(self.llm_wake_prefix): if self.llm_wake_prefix and not msg_plain.startswith(self.llm_wake_prefix):
logger.debug(f"消息 `{msg_plain}` 没有以 LLM 唤醒前缀 `{self.llm_wake_prefix}` 开头,忽略。") logger.debug(f"消息 `{msg_plain}` 没有以 LLM 唤醒前缀 `{self.llm_wake_prefix}` 开头,忽略。")
+20 -1
View File
@@ -21,6 +21,7 @@ class CommandMetadata():
plugin_metadata: PluginMetadata plugin_metadata: PluginMetadata
handler: callable handler: callable
use_regex: bool = False use_regex: bool = False
ignore_prefix: bool = False
description: str = "" description: str = ""
class CommandManager(): class CommandManager():
@@ -35,6 +36,7 @@ class CommandManager():
priority: int, priority: int,
handler: callable, handler: callable,
use_regex: bool = False, use_regex: bool = False,
ignore_prefix: bool = False,
plugin_metadata: PluginMetadata = None, plugin_metadata: PluginMetadata = None,
): ):
''' '''
@@ -53,6 +55,7 @@ class CommandManager():
plugin_metadata=plugin_metadata, plugin_metadata=plugin_metadata,
handler=handler, handler=handler,
use_regex=use_regex, use_regex=use_regex,
ignore_prefix=ignore_prefix,
description=description description=description
) )
if plugin_metadata: if plugin_metadata:
@@ -75,9 +78,23 @@ class CommandManager():
priority=request.priority, priority=request.priority,
handler=request.handler, handler=request.handler,
use_regex=request.use_regex, use_regex=request.use_regex,
ignore_prefix=request.ignore_prefix,
plugin_metadata=plugin.metadata) plugin_metadata=plugin.metadata)
self.plugin_commands_waitlist = [] self.plugin_commands_waitlist = []
async def check_command_ignore_prefix(self, message_str: str) -> bool:
for _, command in self.commands:
command_metadata = self.commands_handler[command]
if command_metadata.ignore_prefix:
trig = False
if self.commands_handler[command].use_regex:
trig = self.command_parser.regex_match(message_str, command)
else:
trig = message_str.startswith(command)
if trig:
return True
return False
async def scan_command(self, message_event: AstrMessageEvent, context: Context) -> CommandResult: async def scan_command(self, message_event: AstrMessageEvent, context: Context) -> CommandResult:
message_str = message_event.message_str message_str = message_event.message_str
for _, command in self.commands: for _, command in self.commands:
@@ -89,6 +106,8 @@ class CommandManager():
if trig: if trig:
logger.info(f"触发 {command} 指令。") logger.info(f"触发 {command} 指令。")
command_result = await self.execute_handler(command, message_event, context) command_result = await self.execute_handler(command, message_event, context)
if not command_result:
continue
if command_result.hit: if command_result.hit:
return command_result return command_result
+8
View File
@@ -3,6 +3,7 @@ from typing import Union, Any, List
from nakuru.entities.components import Plain, At, Image, BaseMessageComponent from nakuru.entities.components import Plain, At, Image, BaseMessageComponent
from type.astrbot_message import AstrBotMessage from type.astrbot_message import AstrBotMessage
from type.command import CommandResult from type.command import CommandResult
from type.astrbot_message import MessageType
class Platform(): class Platform():
@@ -30,6 +31,13 @@ class Platform():
发送消息(主动) 发送消息(主动)
''' '''
pass pass
@abc.abstractmethod
async def send_msg_new(self, message_type: MessageType, target: str, result_message: CommandResult):
'''
发送消息(主动)
'''
pass
def parse_message_outline(self, message: AstrBotMessage) -> str: def parse_message_outline(self, message: AstrBotMessage) -> str:
''' '''
+2 -2
View File
@@ -58,7 +58,7 @@ class PlatformManager():
try: try:
qq_gocq = QQGOCQ(self.context, self.msg_handler) qq_gocq = QQGOCQ(self.context, self.msg_handler)
self.context.platforms.append(RegisteredPlatform( self.context.platforms.append(RegisteredPlatform(
platform_name="gocq", platform_instance=qq_gocq, origin="internal")) platform_name="nakuru", platform_instance=qq_gocq, origin="internal"))
await qq_gocq.run() await qq_gocq.run()
except BaseException as e: except BaseException as e:
logger.error("启动 nakuru 适配器时出现错误: " + str(e)) logger.error("启动 nakuru 适配器时出现错误: " + str(e))
@@ -81,7 +81,7 @@ class PlatformManager():
from model.platform.qq_official import QQOfficial from model.platform.qq_official import QQOfficial
qqchannel_bot = QQOfficial(self.context, self.msg_handler) qqchannel_bot = QQOfficial(self.context, self.msg_handler)
self.context.platforms.append(RegisteredPlatform( self.context.platforms.append(RegisteredPlatform(
platform_name="qqchan", platform_instance=qqchannel_bot, origin="internal")) platform_name="qqofficial", platform_instance=qqchannel_bot, origin="internal"))
return qqchannel_bot.run() return qqchannel_bot.run()
except BaseException as e: except BaseException as e:
logger.error("启动 QQ官方机器人适配器时出现错误: " + str(e)) logger.error("启动 QQ官方机器人适配器时出现错误: " + str(e))
+41 -11
View File
@@ -103,22 +103,27 @@ class AIOCQHTTP(Platform):
await asyncio.sleep(1) await asyncio.sleep(1)
def pre_check(self, message: AstrBotMessage) -> bool: def pre_check(self, message: AstrBotMessage) -> bool:
# if message chain contains Plain components or At components which points to self_id, return True # if message chain contains Plain components or
# At components which points to self_id, return True
if message.type == MessageType.FRIEND_MESSAGE: if message.type == MessageType.FRIEND_MESSAGE:
return True return True, "friend"
for comp in message.message: for comp in message.message:
if isinstance(comp, At) and str(comp.qq) == message.self_id: if isinstance(comp, At) and str(comp.qq) == message.self_id:
return True return True, "at"
# check commands which ignore prefix
if self.context.command_manager.check_command_ignore_prefix(message.message_str):
return True, "command"
# check nicks # check nicks
if self.check_nick(message.message_str): if self.check_nick(message.message_str):
return True return True, "nick"
return False return False, "none"
async def handle_msg(self, message: AstrBotMessage): async def handle_msg(self, message: AstrBotMessage):
logger.info( logger.info(
f"{message.sender.nickname}/{message.sender.user_id} -> {self.parse_message_outline(message)}") f"{message.sender.nickname}/{message.sender.user_id} -> {self.parse_message_outline(message)}")
if not self.pre_check(message): ok, reason = self.pre_check(message)
if not ok:
return return
# 解析 role # 解析 role
@@ -129,14 +134,30 @@ class AIOCQHTTP(Platform):
else: else:
role = 'member' role = 'member'
# parse unified message origin
unified_msg_origin = None
assert isinstance(message.raw_message, Event)
if message.type == MessageType.GROUP_MESSAGE:
unified_msg_origin = f"aiocqhttp:{message.type.value}:{message.raw_message.group_id}"
elif message.type == MessageType.FRIEND_MESSAGE:
unified_msg_origin = f"aiocqhttp:{message.type.value}:{message.sender.user_id}"
logger.debug(f"unified_msg_origin: {unified_msg_origin}")
# construct astrbot message event # construct astrbot message event
ame = AstrMessageEvent.from_astrbot_message(message, self.context, "aiocqhttp", message.session_id, role) ame = AstrMessageEvent.from_astrbot_message(message,
self.context,
"aiocqhttp",
message.session_id,
role,
unified_msg_origin,
reason == "command") # only_command
# transfer control to message handler # transfer control to message handler
message_result = await self.message_handler.handle(ame) message_result = await self.message_handler.handle(ame)
if not message_result: return if not message_result: return
await self.reply_msg(message, message_result.result_message) await self.reply_msg(message, message_result.result_message, message_result.use_t2i)
if message_result.callback: if message_result.callback:
message_result.callback() message_result.callback()
@@ -147,7 +168,8 @@ class AIOCQHTTP(Platform):
async def reply_msg(self, async def reply_msg(self,
message: AstrBotMessage, message: AstrBotMessage,
result_message: list): result_message: list,
use_t2i: bool = None):
""" """
回复用户唤醒机器人的消息。(被动回复) 回复用户唤醒机器人的消息。(被动回复)
""" """
@@ -160,7 +182,7 @@ class AIOCQHTTP(Platform):
res = [Plain(text=res), ] res = [Plain(text=res), ]
# if image mode, put all Plain texts into a new picture. # if image mode, put all Plain texts into a new picture.
if self.context.base_config.get("qq_pic_mode", False) and isinstance(res, list): if use_t2i or (use_t2i == None and self.context.base_config.get("qq_pic_mode", False)) and isinstance(res, list):
rendered_images = await self.convert_to_t2i_chain(res) rendered_images = await self.convert_to_t2i_chain(res)
if rendered_images: if rendered_images:
try: try:
@@ -223,4 +245,12 @@ class AIOCQHTTP(Platform):
''' '''
await self._reply(target, result_message.message_chain) await self._reply(target, result_message.message_chain)
async def send_msg_new(self, message_type: MessageType, target: str, result_message: CommandResult):
if message_type == MessageType.GROUP_MESSAGE:
await self.send_msg({'group_id': int(target)}, result_message)
elif message_type == MessageType.FRIEND_MESSAGE:
await self.send_msg({'user_id': int(target)}, result_message)
else:
raise Exception("aiocqhttp: 无法识别的消息类型。")
+68 -13
View File
@@ -74,14 +74,17 @@ class QQGOCQ(Platform):
def pre_check(self, message: AstrBotMessage) -> bool: def pre_check(self, message: AstrBotMessage) -> bool:
# if message chain contains Plain components or At components which points to self_id, return True # if message chain contains Plain components or At components which points to self_id, return True
if message.type == MessageType.FRIEND_MESSAGE: if message.type == MessageType.FRIEND_MESSAGE:
return True return True, "friend"
for comp in message.message: for comp in message.message:
if isinstance(comp, At) and str(comp.qq) == message.self_id: if isinstance(comp, At) and str(comp.qq) == message.self_id:
return True return True, "at"
# check commands which ignore prefix
if self.context.command_manager.check_command_ignore_prefix(message.message_str):
return True, "command"
# check nicks # check nicks
if self.check_nick(message.message_str): if self.check_nick(message.message_str):
return True return True, "nick"
return False return False, "none"
def run(self): def run(self):
coro = self.client._run() coro = self.client._run()
@@ -95,7 +98,8 @@ class QQGOCQ(Platform):
(GroupMessage, FriendMessage, GuildMessage)) (GroupMessage, FriendMessage, GuildMessage))
# 判断是否响应消息 # 判断是否响应消息
if not self.pre_check(message): ok, reason = self.pre_check(message)
if not ok:
return return
# 解析 session_id # 解析 session_id
@@ -118,14 +122,35 @@ class QQGOCQ(Platform):
else: else:
role = 'member' role = 'member'
# parse unified message origin
unified_msg_origin = None
if message.type == MessageType.GROUP_MESSAGE:
assert isinstance(message.raw_message, GroupMessage)
unified_msg_origin = f"nakuru:{message.type.value}:{message.raw_message.group_id}"
elif message.type == MessageType.FRIEND_MESSAGE:
assert isinstance(message.raw_message, FriendMessage)
unified_msg_origin = f"nakuru:{message.type.value}:{message.sender.user_id}"
elif message.type == MessageType.GUILD_MESSAGE:
assert isinstance(message.raw_message, GuildMessage)
unified_msg_origin = f"nakuru:{message.type.value}:{message.raw_message.channel_id}"
logger.debug(f"unified_msg_origin: {unified_msg_origin}")
# construct astrbot message event # construct astrbot message event
ame = AstrMessageEvent.from_astrbot_message(message, self.context, "gocq", session_id, role) ame = AstrMessageEvent.from_astrbot_message(message,
self.context,
"nakuru",
session_id,
role,
unified_msg_origin,
reason == 'command') # only_command
# transfer control to message handler # transfer control to message handler
message_result = await self.message_handler.handle(ame) message_result = await self.message_handler.handle(ame)
if not message_result: return if not message_result: return
await self.reply_msg(message, message_result.result_message) await self.reply_msg(message, message_result.result_message, message_result.use_t2i)
if message_result.callback: if message_result.callback:
message_result.callback() message_result.callback()
@@ -135,7 +160,8 @@ class QQGOCQ(Platform):
async def reply_msg(self, async def reply_msg(self,
message: AstrBotMessage, message: AstrBotMessage,
result_message: List[BaseMessageComponent]): result_message: List[BaseMessageComponent],
use_t2i: bool = None):
""" """
回复用户唤醒机器人的消息。(被动回复) 回复用户唤醒机器人的消息。(被动回复)
""" """
@@ -152,7 +178,7 @@ class QQGOCQ(Platform):
res = [Plain(text=res), ] res = [Plain(text=res), ]
# if image mode, put all Plain texts into a new picture. # if image mode, put all Plain texts into a new picture.
if self.context.base_config.get("qq_pic_mode", False) and isinstance(res, list): if use_t2i or (use_t2i == None and self.context.base_config.get("qq_pic_mode", False)) and isinstance(res, list):
rendered_images = await self.convert_to_t2i_chain(res) rendered_images = await self.convert_to_t2i_chain(res)
if rendered_images: if rendered_images:
try: try:
@@ -169,14 +195,26 @@ class QQGOCQ(Platform):
message_chain = [Plain(text=message_chain), ] message_chain = [Plain(text=message_chain), ]
is_dict = isinstance(source, dict) is_dict = isinstance(source, dict)
if source.type == "GuildMessage":
typ = None
if is_dict:
if "group_id" in source:
typ = "GroupMessage"
elif "user_id" in source:
typ = "FriendMessage"
elif "guild_id" in source:
typ = "GuildMessage"
else:
typ = source.type
if typ == "GuildMessage":
guild_id = source['guild_id'] if is_dict else source.guild_id guild_id = source['guild_id'] if is_dict else source.guild_id
chan_id = source['channel_id'] if is_dict else source.channel_id chan_id = source['channel_id'] if is_dict else source.channel_id
await self.client.sendGuildChannelMessage(guild_id, chan_id, message_chain) await self.client.sendGuildChannelMessage(guild_id, chan_id, message_chain)
elif source.type == "FriendMessage": elif typ == "FriendMessage":
user_id = source['user_id'] if is_dict else source.user_id user_id = source['user_id'] if is_dict else source.user_id
await self.client.sendFriendMessage(user_id, message_chain) await self.client.sendFriendMessage(user_id, message_chain)
elif source.type == "GroupMessage": elif typ == "GroupMessage":
group_id = source['group_id'] if is_dict else source.group_id group_id = source['group_id'] if is_dict else source.group_id
# 过长时forward发送 # 过长时forward发送
plain_text_len = 0 plain_text_len = 0
@@ -213,6 +251,23 @@ class QQGOCQ(Platform):
guild_id 不是频道号。 guild_id 不是频道号。
''' '''
await self._reply(target, result_message.message_chain) await self._reply(target, result_message.message_chain)
async def send_msg_new(self, message_type: MessageType, target: str, result_message: CommandResult):
'''
以主动的方式给用户、群或者频道发送一条消息。
`message_type` 为 MessageType 枚举类型。
- 要发给 QQ 下的某个用户,请使用 MessageType.FRIEND_MESSAGE
- 要发给某个群聊,请使用 MessageType.GROUP_MESSAGE
- 要发给某个频道,请使用 MessageType.GUILD_MESSAGE。
'''
if message_type == MessageType.FRIEND_MESSAGE:
await self.send_msg({"user_id": int(target)}, result_message)
elif message_type == MessageType.GROUP_MESSAGE:
await self.send_msg({"group_id": int(target)}, result_message)
elif message_type == MessageType.GUILD_MESSAGE:
await self.send_msg({"channel_id": int(target)}, result_message)
def convert_message(self, message: Union[GroupMessage, FriendMessage, GuildMessage]) -> AstrBotMessage: def convert_message(self, message: Union[GroupMessage, FriendMessage, GuildMessage]) -> AstrBotMessage:
abm = AstrBotMessage() abm = AstrBotMessage()
@@ -233,7 +288,7 @@ class QQGOCQ(Platform):
str(message.sender.user_id), str(message.sender.user_id),
str(message.sender.nickname) str(message.sender.nickname)
) )
abm.tag = "gocq" abm.tag = "nakuru"
abm.message = message.message abm.message = message.message
return abm return abm
+8 -4
View File
@@ -112,7 +112,7 @@ class QQOfficial(Platform):
abm.timestamp = int(time.time()) abm.timestamp = int(time.time())
abm.raw_message = message abm.raw_message = message
abm.message_id = message.id abm.message_id = message.id
abm.tag = "qqchan" abm.tag = "qqofficial"
msg: List[BaseMessageComponent] = [] msg: List[BaseMessageComponent] = []
if isinstance(message, botpy.message.GroupMessage) or isinstance(message, botpy.message.C2CMessage): if isinstance(message, botpy.message.GroupMessage) or isinstance(message, botpy.message.C2CMessage):
@@ -222,7 +222,7 @@ class QQOfficial(Platform):
if not message_result: if not message_result:
return return
ret = await self.reply_msg(message, message_result.result_message) ret = await self.reply_msg(message, message_result.result_message, message_result.use_t2i)
if message_result.callback: if message_result.callback:
message_result.callback() message_result.callback()
@@ -234,7 +234,8 @@ class QQOfficial(Platform):
async def reply_msg(self, async def reply_msg(self,
message: AstrBotMessage, message: AstrBotMessage,
result_message: List[BaseMessageComponent]): result_message: List[BaseMessageComponent],
use_t2i: bool = None):
''' '''
回复频道消息 回复频道消息
''' '''
@@ -249,7 +250,7 @@ class QQOfficial(Platform):
msg_ref = None msg_ref = None
rendered_images = [] rendered_images = []
if self.context.base_config.get("qq_pic_mode", False) and isinstance(result_message, list): if use_t2i or (use_t2i == None and self.context.base_config.get("qq_pic_mode", False)) and isinstance(res, list):
rendered_images = await self.convert_to_t2i_chain(result_message) rendered_images = await self.convert_to_t2i_chain(result_message)
if isinstance(result_message, list): if isinstance(result_message, list):
@@ -388,6 +389,9 @@ class QQOfficial(Platform):
if image_path: if image_path:
payload['file_image'] = image_path payload['file_image'] = image_path
await self._reply(**payload) await self._reply(**payload)
async def send_msg_new(self, message_type: MessageType, target: str, result_message: CommandResult):
raise NotImplementedError("qqofficial 不支持此方法。")
def wait_for_message(self, channel_id: int) -> AstrBotMessage: def wait_for_message(self, channel_id: int) -> AstrBotMessage:
''' '''
+3 -2
View File
@@ -15,12 +15,13 @@ class CommandRegisterRequest():
handler: Callable handler: Callable
use_regex: bool = False use_regex: bool = False
plugin_name: str = None plugin_name: str = None
ignore_prefix: bool = False
class PluginCommandBridge(): class PluginCommandBridge():
def __init__(self, cached_plugins: RegisteredPlugins): def __init__(self, cached_plugins: RegisteredPlugins):
self.plugin_commands_waitlist: List[CommandRegisterRequest] = [] self.plugin_commands_waitlist: List[CommandRegisterRequest] = []
self.cached_plugins = cached_plugins self.cached_plugins = cached_plugins
def register_command(self, plugin_name, command_name, description, priority, handler, use_regex=False): def register_command(self, plugin_name, command_name, description, priority, handler, use_regex=False, ignore_prefix=False):
self.plugin_commands_waitlist.append(CommandRegisterRequest(command_name, description, priority, handler, use_regex, plugin_name)) self.plugin_commands_waitlist.append(CommandRegisterRequest(command_name, description, priority, handler, use_regex, plugin_name, ignore_prefix))
+13 -11
View File
@@ -2,7 +2,6 @@ from typing import Union, List, Callable
from dataclasses import dataclass from dataclasses import dataclass
from nakuru.entities.components import Plain, Image from nakuru.entities.components import Plain, Image
@dataclass @dataclass
class CommandItem(): class CommandItem():
''' '''
@@ -19,12 +18,17 @@ class CommandResult():
用于在Command中返回多个值 用于在Command中返回多个值
''' '''
def __init__(self, hit: bool = True, success: bool = True, message_chain: list = [], command_name: str = "unknown_command") -> None: def __init__(self,
hit: bool = True,
success: bool = True,
message_chain: list = [],
command_name: str = "unknown_command",
use_t2i: bool = None) -> None:
self.hit = hit self.hit = hit
self.success = success self.success = success
self.message_chain = message_chain self.message_chain = message_chain
self.command_name = command_name self.command_name = command_name
self.is_use_t2i = None # default self.is_use_t2i = use_t2i
def message(self, message: str): def message(self, message: str):
''' '''
@@ -63,14 +67,12 @@ class CommandResult():
self.message_chain = [Image.fromFileSystem(path), ] self.message_chain = [Image.fromFileSystem(path), ]
return self return self
# def use_t2i(self, use_t2i: bool): def use_t2i(self, use_t2i: bool):
# ''' '''
# 设置是否使用文本转图片服务。如果不设置,则跟随用户的设置 设置是否使用文本转图片服务如果不设置则跟随用户的设置
'''
# CommandResult().use_t2i(False) self.is_use_t2i = use_t2i
# ''' return self
# self.is_use_t2i = use_t2i
# return self
def _result_tuple(self): def _result_tuple(self):
return (self.success, self.message_chain, self.command_name) return (self.success, self.message_chain, self.command_name)
+1 -1
View File
@@ -1,4 +1,4 @@
VERSION = '3.3.7' VERSION = '3.3.8'
DEFAULT_CONFIG = { DEFAULT_CONFIG = {
"qqbot": { "qqbot": {
+21 -10
View File
@@ -2,7 +2,14 @@ from typing import List, Union, Optional
from dataclasses import dataclass from dataclasses import dataclass
from type.register import RegisteredPlatform from type.register import RegisteredPlatform
from type.types import Context from type.types import Context
from type.astrbot_message import AstrBotMessage from type.astrbot_message import AstrBotMessage, MessageType
@dataclass
class MessageResult():
result_message: Union[str, list]
is_command_call: Optional[bool] = False
use_t2i: Optional[bool] = None # None 为跟随用户设置
callback: Optional[callable] = None
class AstrMessageEvent(): class AstrMessageEvent():
@@ -12,7 +19,9 @@ class AstrMessageEvent():
platform: RegisteredPlatform, platform: RegisteredPlatform,
role: str, role: str,
context: Context, context: Context,
session_id: str = None): session_id: str = None,
unified_msg_origin: str = None,
only_command: bool = False):
''' '''
AstrBot 消息事件 AstrBot 消息事件
@@ -22,6 +31,8 @@ class AstrMessageEvent():
`role`: 角色`admin` or `member` `role`: 角色`admin` or `member`
`context`: 全局对象 `context`: 全局对象
`session_id`: 会话id `session_id`: 会话id
`unified_msg_origin`: 统一消息来源
`only_command`: 是否只处理指令而不使用 LLM 回复
''' '''
self.context = context self.context = context
self.message_str = message_str self.message_str = message_str
@@ -29,24 +40,24 @@ class AstrMessageEvent():
self.platform = platform self.platform = platform
self.role = role self.role = role
self.session_id = session_id self.session_id = session_id
self.unified_msg_origin = unified_msg_origin
self.only_command = only_command
def from_astrbot_message(message: AstrBotMessage, def from_astrbot_message(message: AstrBotMessage,
context: Context, context: Context,
platform_name: str, platform_name: str,
session_id: str, session_id: str,
role: str = "member"): role: str = "member",
unified_msg_origin: str = None,
only_command: bool = False):
ame = AstrMessageEvent(message.message_str, ame = AstrMessageEvent(message.message_str,
message, message,
context.find_platform(platform_name), context.find_platform(platform_name),
role, role,
context, context,
session_id) session_id,
unified_msg_origin,
only_command=only_command)
return ame return ame
@dataclass
class MessageResult():
result_message: Union[str, list]
is_command_call: Optional[bool] = False
use_t2i: Optional[bool] = None # None 为跟随用户设置
callback: Optional[callable] = None
+33 -2
View File
@@ -8,6 +8,8 @@ from util.t2i.renderer import TextToImageRenderer
from util.updator.astrbot_updator import AstrBotUpdator from util.updator.astrbot_updator import AstrBotUpdator
from util.image_uploader import ImageUploader from util.image_uploader import ImageUploader
from util.updator.plugin_updator import PluginUpdator from util.updator.plugin_updator import PluginUpdator
from type.command import CommandResult
from type.astrbot_message import MessageType
from model.plugin.command import PluginCommandBridge from model.plugin.command import PluginCommandBridge
from model.provider.provider import Provider from model.provider.provider import Provider
@@ -40,6 +42,8 @@ class Context:
self.image_uploader = ImageUploader() self.image_uploader = ImageUploader()
self.message_handler = None # see astrbot/message/handler.py self.message_handler = None # see astrbot/message/handler.py
self.ext_tasks: List[Task] = [] self.ext_tasks: List[Task] = []
self.command_manager = None
# useless # useless
self.reply_prefix = "" self.reply_prefix = ""
@@ -50,7 +54,8 @@ class Context:
description: str, description: str,
priority: int, priority: int,
handler: callable, handler: callable,
use_regex: bool = False): use_regex: bool = False,
ignore_prefix: bool = False):
''' '''
注册插件指令 注册插件指令
@@ -60,8 +65,19 @@ class Context:
@param priority: 优先级越高越先被处理合理的优先级应该在 1-10 之间 @param priority: 优先级越高越先被处理合理的优先级应该在 1-10 之间
@param handler: 指令处理函数函数参数message: AstrMessageEvent, context: Context @param handler: 指令处理函数函数参数message: AstrMessageEvent, context: Context
@param use_regex: 是否使用正则表达式匹配指令名 @param use_regex: 是否使用正则表达式匹配指令名
@param ignore_prefix: 是否忽略前缀默认为 False设置为 True 将不会检查用户设置的前缀
.. Example::
ignore_prefix = False 用户输入 "/help" 会被识别为 "help" 指令如果 ignore_prefix = True则用户输入 "help" 也会被识别为 "help" 指令
''' '''
self.plugin_command_bridge.register_command(plugin_name, command_name, description, priority, handler, use_regex) self.plugin_command_bridge.register_command(plugin_name,
command_name,
description,
priority,
handler,
use_regex,
ignore_prefix)
def register_task(self, coro: Awaitable, task_name: str): def register_task(self, coro: Awaitable, task_name: str):
''' '''
@@ -87,3 +103,18 @@ class Context:
return platform return platform
raise ValueError("couldn't find the platform you specified") raise ValueError("couldn't find the platform you specified")
async def send_message(self, unified_msg_origin: str, message: CommandResult):
'''
发送消息
`unified_msg_origin`: 统一消息来源
`message`: 消息内容
'''
l = unified_msg_origin.split(":")
if len(l) != 3:
raise ValueError("Invalid unified_msg_origin")
platform_name, message_type, id = l
platform = self.find_platform(platform_name)
await platform.platform_instance.send_msg_new(MessageType(message_type), id, message)
+1 -1
View File
@@ -9,7 +9,7 @@ logger: Logger = LogManager.GetLogger(log_name='astrbot')
class AstrBotUpdator(RepoZipUpdator): class AstrBotUpdator(RepoZipUpdator):
def __init__(self): def __init__(self):
self.MAIN_PATH = os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), "..")) self.MAIN_PATH = os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), "../../"))
self.ASTRBOT_RELEASE_API = "https://api.github.com/repos/Soulter/AstrBot/releases" self.ASTRBOT_RELEASE_API = "https://api.github.com/repos/Soulter/AstrBot/releases"
def terminate_child_processes(self): def terminate_child_processes(self):