Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 01efe5f869 | |||
| 28a178a55c | |||
| 88f130014c | |||
| af258c590c | |||
| b0eb5733be | |||
| fe35bfba37 | |||
| 7a9d4f0abd | |||
| 6f6a5b565c | |||
| e57deb873c | |||
| 8c03e79f99 | |||
| 71290f0929 | |||
| 22364ef7de | |||
| f51f510f2e | |||
| 76e05ea749 | |||
| ab599dceed | |||
| 4c37604445 | |||
| bb74018d19 | |||
| 575289e5bc | |||
| e89da2a7b4 | |||
| d2f7e55bf5 | |||
| 9f31df7f3a | |||
| e1bed60f1f | |||
| edbb856023 | |||
| 98d3ab646f | |||
| ab677ea100 |
@@ -1,6 +1,6 @@
|
||||
name: Run tests and upload coverage
|
||||
|
||||
on:
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
@@ -8,6 +8,7 @@ on:
|
||||
- 'README.md'
|
||||
- 'changelogs/**'
|
||||
- 'dashboard/**'
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
@@ -26,20 +27,19 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -r requirements.txt
|
||||
pip install pytest pytest-cov pytest-asyncio
|
||||
pip install pytest pytest-asyncio pytest-cov
|
||||
pip install --editable .
|
||||
|
||||
- name: Run tests
|
||||
run: |
|
||||
mkdir data
|
||||
mkdir data/plugins
|
||||
mkdir data/config
|
||||
mkdir data/temp
|
||||
mkdir -p data/plugins
|
||||
mkdir -p data/config
|
||||
mkdir -p data/temp
|
||||
export TESTING=true
|
||||
export ZHIPU_API_KEY=${{ secrets.OPENAI_API_KEY }}
|
||||
PYTHONPATH=./ pytest --cov=. tests/ -v -o log_cli=true -o log_level=DEBUG
|
||||
pytest --cov=. -v -o log_cli=true -o log_level=DEBUG
|
||||
|
||||
- name: Upload results to Codecov
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
@@ -6,7 +6,7 @@ import os
|
||||
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
|
||||
|
||||
VERSION = "3.5.19"
|
||||
VERSION = "3.5.20"
|
||||
DB_PATH = os.path.join(get_astrbot_data_path(), "data_v3.db")
|
||||
|
||||
# 默认配置
|
||||
|
||||
@@ -184,7 +184,8 @@ class LLMRequestSubStage(Stage):
|
||||
await event.send(resp.data["chain"])
|
||||
continue
|
||||
# 对于其他情况,暂时先不处理
|
||||
if resp.type == "tool_call":
|
||||
continue
|
||||
elif resp.type == "tool_call":
|
||||
if self.streaming_response:
|
||||
# 用来标记流式响应需要分节
|
||||
yield MessageChain(chain=[], type="break")
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import asyncio
|
||||
import re
|
||||
from typing import AsyncGenerator, Dict, List
|
||||
from aiocqhttp import CQHttp
|
||||
from aiocqhttp import CQHttp, Event
|
||||
from astrbot.api.event import AstrMessageEvent, MessageChain
|
||||
from astrbot.api.message_components import (
|
||||
Image,
|
||||
@@ -58,50 +58,85 @@ class AiocqhttpMessageEvent(AstrMessageEvent):
|
||||
ret.append(d)
|
||||
return ret
|
||||
|
||||
async def send(self, message: MessageChain):
|
||||
@classmethod
|
||||
async def _dispatch_send(
|
||||
cls,
|
||||
bot: CQHttp,
|
||||
event: Event | None,
|
||||
is_group: bool,
|
||||
session_id: str,
|
||||
messages: list[dict],
|
||||
):
|
||||
if event:
|
||||
await bot.send(event=event, message=messages)
|
||||
elif is_group:
|
||||
await bot.send_group_msg(group_id=session_id, message=messages)
|
||||
else:
|
||||
await bot.send_private_msg(user_id=session_id, message=messages)
|
||||
|
||||
@classmethod
|
||||
async def send_message(
|
||||
cls,
|
||||
bot: CQHttp,
|
||||
message_chain: MessageChain,
|
||||
event: Event | None = None,
|
||||
is_group: bool = False,
|
||||
session_id: str = None,
|
||||
):
|
||||
"""发送消息"""
|
||||
|
||||
# 转发消息、文件消息不能和普通消息混在一起发送
|
||||
send_one_by_one = any(
|
||||
isinstance(seg, (Node, Nodes, File)) for seg in message.chain
|
||||
isinstance(seg, (Node, Nodes, File)) for seg in message_chain.chain
|
||||
)
|
||||
if send_one_by_one:
|
||||
for seg in message.chain:
|
||||
if isinstance(seg, (Node, Nodes)):
|
||||
# 合并转发消息
|
||||
|
||||
if isinstance(seg, Node):
|
||||
nodes = Nodes([seg])
|
||||
seg = nodes
|
||||
|
||||
payload = await seg.to_dict()
|
||||
|
||||
if self.get_group_id():
|
||||
payload["group_id"] = self.get_group_id()
|
||||
await self.bot.call_action("send_group_forward_msg", **payload)
|
||||
else:
|
||||
payload["user_id"] = self.get_sender_id()
|
||||
await self.bot.call_action(
|
||||
"send_private_forward_msg", **payload
|
||||
)
|
||||
elif isinstance(seg, File):
|
||||
d = await AiocqhttpMessageEvent._from_segment_to_dict(seg)
|
||||
await self.bot.send(
|
||||
self.message_obj.raw_message,
|
||||
[d],
|
||||
)
|
||||
else:
|
||||
await self.bot.send(
|
||||
self.message_obj.raw_message,
|
||||
await AiocqhttpMessageEvent._parse_onebot_json(
|
||||
MessageChain([seg])
|
||||
),
|
||||
)
|
||||
await asyncio.sleep(0.5)
|
||||
else:
|
||||
ret = await AiocqhttpMessageEvent._parse_onebot_json(message)
|
||||
if not send_one_by_one:
|
||||
ret = await cls._parse_onebot_json(message_chain)
|
||||
if not ret:
|
||||
return
|
||||
await self.bot.send(self.message_obj.raw_message, ret)
|
||||
await cls._dispatch_send(bot, event, is_group, session_id, ret)
|
||||
return
|
||||
for seg in message_chain.chain:
|
||||
if isinstance(seg, (Node, Nodes)):
|
||||
# 合并转发消息
|
||||
if isinstance(seg, Node):
|
||||
nodes = Nodes([seg])
|
||||
seg = nodes
|
||||
|
||||
payload = await seg.to_dict()
|
||||
|
||||
if is_group:
|
||||
payload["group_id"] = session_id
|
||||
await bot.call_action("send_group_forward_msg", **payload)
|
||||
else:
|
||||
payload["user_id"] = session_id
|
||||
await bot.call_action("send_private_forward_msg", **payload)
|
||||
elif isinstance(seg, File):
|
||||
d = await cls._from_segment_to_dict(seg)
|
||||
await cls._dispatch_send(bot, event, is_group, session_id, [d])
|
||||
else:
|
||||
messages = await cls._parse_onebot_json(MessageChain([seg]))
|
||||
if not messages:
|
||||
continue
|
||||
await cls._dispatch_send(bot, event, is_group, session_id, messages)
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
async def send(self, message: MessageChain):
|
||||
"""发送消息"""
|
||||
event = self.message_obj.raw_message
|
||||
assert isinstance(event, Event), "Event must be an instance of aiocqhttp.Event"
|
||||
is_group = False
|
||||
if self.get_group_id():
|
||||
is_group = True
|
||||
session_id = self.get_group_id()
|
||||
else:
|
||||
session_id = self.get_sender_id()
|
||||
await self.send_message(
|
||||
bot=self.bot,
|
||||
message_chain=message,
|
||||
event=event,
|
||||
is_group=is_group,
|
||||
session_id=session_id,
|
||||
)
|
||||
await super().send(message)
|
||||
|
||||
async def send_streaming(
|
||||
|
||||
@@ -83,19 +83,18 @@ class AiocqhttpAdapter(Platform):
|
||||
async def send_by_session(
|
||||
self, session: MessageSesion, message_chain: MessageChain
|
||||
):
|
||||
ret = await AiocqhttpMessageEvent._parse_onebot_json(message_chain)
|
||||
match session.message_type.value:
|
||||
case MessageType.GROUP_MESSAGE.value:
|
||||
if "_" in session.session_id:
|
||||
# 独立会话
|
||||
_, group_id = session.session_id.split("_")
|
||||
await self.bot.send_group_msg(group_id=group_id, message=ret)
|
||||
else:
|
||||
await self.bot.send_group_msg(
|
||||
group_id=session.session_id, message=ret
|
||||
)
|
||||
case MessageType.FRIEND_MESSAGE.value:
|
||||
await self.bot.send_private_msg(user_id=session.session_id, message=ret)
|
||||
is_group = session.message_type == MessageType.GROUP_MESSAGE
|
||||
if is_group:
|
||||
session_id = session.session_id.split("_")[-1]
|
||||
else:
|
||||
session_id = session.session_id
|
||||
await AiocqhttpMessageEvent.send_message(
|
||||
bot=self.bot,
|
||||
message_chain=message_chain,
|
||||
event=None, # 这里不需要 event,因为是通过 session 发送的
|
||||
is_group=is_group,
|
||||
session_id=session_id,
|
||||
)
|
||||
await super().send_by_session(session, message_chain)
|
||||
|
||||
async def convert_message(self, event: Event) -> AstrBotMessage:
|
||||
@@ -307,7 +306,9 @@ class AiocqhttpAdapter(Platform):
|
||||
user_id=int(m["data"]["qq"]),
|
||||
)
|
||||
if at_info:
|
||||
nickname = at_info.get("nick", "") or at_info.get("nickname", "")
|
||||
nickname = at_info.get("nick", "") or at_info.get(
|
||||
"nickname", ""
|
||||
)
|
||||
is_at_self = str(m["data"]["qq"]) in {abm.self_id, "all"}
|
||||
|
||||
abm.message.append(
|
||||
|
||||
@@ -482,13 +482,8 @@ class ProviderOpenAIOfficial(Provider):
|
||||
"""
|
||||
new_contexts = []
|
||||
|
||||
flag = False
|
||||
for context in contexts:
|
||||
if flag:
|
||||
flag = False # 删除 image 后,下一条(LLM 响应)也要删除
|
||||
continue
|
||||
if "content" in context and isinstance(context["content"], list):
|
||||
flag = True
|
||||
# continue
|
||||
new_content = []
|
||||
for item in context["content"]:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from .star import StarMetadata
|
||||
from .star import StarMetadata, star_map
|
||||
from .star_manager import PluginManager
|
||||
from .context import Context
|
||||
from astrbot.core.provider import Provider
|
||||
@@ -14,12 +14,22 @@ class Star(CommandParserMixin):
|
||||
StarTools.initialize(context)
|
||||
self.context = context
|
||||
|
||||
async def text_to_image(self, text: str, return_url=True) -> str:
|
||||
def __init_subclass__(cls, **kwargs):
|
||||
super().__init_subclass__(**kwargs)
|
||||
metadata = StarMetadata(
|
||||
star_cls_type=cls,
|
||||
module_path=cls.__module__,
|
||||
)
|
||||
star_map[cls.__module__] = metadata
|
||||
|
||||
@staticmethod
|
||||
async def text_to_image(text: str, return_url=True) -> str:
|
||||
"""将文本转换为图片"""
|
||||
return await html_renderer.render_t2i(text, return_url=return_url)
|
||||
|
||||
@staticmethod
|
||||
async def html_render(
|
||||
self, tmpl: str, data: dict, return_url=True, options: dict = None
|
||||
tmpl: str, data: dict, return_url=True, options: dict = None
|
||||
) -> str:
|
||||
"""渲染 HTML"""
|
||||
return await html_renderer.render_custom_template(
|
||||
|
||||
@@ -8,22 +8,48 @@ from typing import Union
|
||||
class PlatformAdapterType(enum.Flag):
|
||||
AIOCQHTTP = enum.auto()
|
||||
QQOFFICIAL = enum.auto()
|
||||
VCHAT = enum.auto()
|
||||
GEWECHAT = enum.auto()
|
||||
TELEGRAM = enum.auto()
|
||||
WECOM = enum.auto()
|
||||
LARK = enum.auto()
|
||||
ALL = AIOCQHTTP | QQOFFICIAL | VCHAT | GEWECHAT | TELEGRAM | WECOM | LARK
|
||||
WECHATPADPRO = enum.auto()
|
||||
DINGTALK = enum.auto()
|
||||
DISCORD = enum.auto()
|
||||
SLACK = enum.auto()
|
||||
KOOK = enum.auto()
|
||||
VOCECHAT = enum.auto()
|
||||
WEIXIN_OFFICIAL_ACCOUNT = enum.auto()
|
||||
ALL = (
|
||||
AIOCQHTTP
|
||||
| QQOFFICIAL
|
||||
| GEWECHAT
|
||||
| TELEGRAM
|
||||
| WECOM
|
||||
| LARK
|
||||
| WECHATPADPRO
|
||||
| DINGTALK
|
||||
| DISCORD
|
||||
| SLACK
|
||||
| KOOK
|
||||
| VOCECHAT
|
||||
| WEIXIN_OFFICIAL_ACCOUNT
|
||||
)
|
||||
|
||||
|
||||
ADAPTER_NAME_2_TYPE = {
|
||||
"aiocqhttp": PlatformAdapterType.AIOCQHTTP,
|
||||
"qq_official": PlatformAdapterType.QQOFFICIAL,
|
||||
"vchat": PlatformAdapterType.VCHAT,
|
||||
"gewechat": PlatformAdapterType.GEWECHAT,
|
||||
"telegram": PlatformAdapterType.TELEGRAM,
|
||||
"wecom": PlatformAdapterType.WECOM,
|
||||
"lark": PlatformAdapterType.LARK,
|
||||
"dingtalk": PlatformAdapterType.DINGTALK,
|
||||
"discord": PlatformAdapterType.DISCORD,
|
||||
"slack": PlatformAdapterType.SLACK,
|
||||
"kook": PlatformAdapterType.KOOK,
|
||||
"wechatpadpro": PlatformAdapterType.WECHATPADPRO,
|
||||
"vocechat": PlatformAdapterType.VOCECHAT,
|
||||
"weixin_official_account": PlatformAdapterType.WEIXIN_OFFICIAL_ACCOUNT,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
from ..star import star_registry, StarMetadata, star_map
|
||||
import warnings
|
||||
|
||||
_warned_register_star = False
|
||||
|
||||
|
||||
def register_star(name: str, author: str, desc: str, version: str, repo: str = None):
|
||||
"""注册一个插件(Star)。
|
||||
|
||||
[DEPRECATED] 该装饰器已废弃,将在未来版本中移除。
|
||||
在 v3.5.19 版本之后(不含),您不需要使用该装饰器来装饰插件类,
|
||||
AstrBot 会自动识别继承自 Star 的类并将其作为插件类加载。
|
||||
|
||||
Args:
|
||||
name: 插件名称。
|
||||
author: 作者。
|
||||
@@ -21,18 +27,16 @@ def register_star(name: str, author: str, desc: str, version: str, repo: str = N
|
||||
帮助信息会被自动提取。使用 `/plugin <插件名> 可以查看帮助信息。`
|
||||
"""
|
||||
|
||||
def decorator(cls):
|
||||
star_metadata = StarMetadata(
|
||||
name=name,
|
||||
author=author,
|
||||
desc=desc,
|
||||
version=version,
|
||||
repo=repo,
|
||||
star_cls_type=cls,
|
||||
module_path=cls.__module__,
|
||||
global _warned_register_star
|
||||
if not _warned_register_star:
|
||||
_warned_register_star = True
|
||||
warnings.warn(
|
||||
"The 'register_star' decorator is deprecated and will be removed in a future version.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
star_registry.append(star_metadata)
|
||||
star_map[cls.__module__] = star_metadata
|
||||
|
||||
def decorator(cls):
|
||||
return cls
|
||||
|
||||
return decorator
|
||||
|
||||
+22
-17
@@ -1,12 +1,12 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from types import ModuleType
|
||||
from typing import List, Dict
|
||||
from dataclasses import dataclass, field
|
||||
from types import ModuleType
|
||||
|
||||
from astrbot.core.config import AstrBotConfig
|
||||
|
||||
star_registry: List[StarMetadata] = []
|
||||
star_map: Dict[str, StarMetadata] = {}
|
||||
star_registry: list[StarMetadata] = []
|
||||
star_map: dict[str, StarMetadata] = {}
|
||||
"""key 是模块路径,__module__"""
|
||||
|
||||
|
||||
@@ -18,22 +18,27 @@ class StarMetadata:
|
||||
当 activated 为 False 时,star_cls 可能为 None,请不要在插件未激活时调用 star_cls 的方法。
|
||||
"""
|
||||
|
||||
name: str
|
||||
author: str # 插件作者
|
||||
desc: str # 插件简介
|
||||
version: str # 插件版本
|
||||
repo: str = None # 插件仓库地址
|
||||
name: str | None = None
|
||||
"""插件名"""
|
||||
author: str | None = None
|
||||
"""插件作者"""
|
||||
desc: str | None = None
|
||||
"""插件简介"""
|
||||
version: str | None = None
|
||||
"""插件版本"""
|
||||
repo: str | None = None
|
||||
"""插件仓库地址"""
|
||||
|
||||
star_cls_type: type = None
|
||||
star_cls_type: type | None = None
|
||||
"""插件的类对象的类型"""
|
||||
module_path: str = None
|
||||
module_path: str | None = None
|
||||
"""插件的模块路径"""
|
||||
|
||||
star_cls: object = None
|
||||
star_cls: object | None = None
|
||||
"""插件的类对象"""
|
||||
module: ModuleType = None
|
||||
module: ModuleType | None = None
|
||||
"""插件的模块对象"""
|
||||
root_dir_name: str = None
|
||||
root_dir_name: str | None = None
|
||||
"""插件的目录名称"""
|
||||
reserved: bool = False
|
||||
"""是否是 AstrBot 的保留插件"""
|
||||
@@ -41,13 +46,13 @@ class StarMetadata:
|
||||
activated: bool = True
|
||||
"""是否被激活"""
|
||||
|
||||
config: AstrBotConfig = None
|
||||
config: AstrBotConfig | None = None
|
||||
"""插件配置"""
|
||||
|
||||
star_handler_full_names: List[str] = field(default_factory=list)
|
||||
star_handler_full_names: list[str] = field(default_factory=list)
|
||||
"""注册的 Handler 的全名列表"""
|
||||
|
||||
supported_platforms: Dict[str, bool] = field(default_factory=dict)
|
||||
supported_platforms: dict[str, bool] = field(default_factory=dict)
|
||||
"""插件支持的平台ID字典,key为平台ID,value为是否支持"""
|
||||
|
||||
def __str__(self) -> str:
|
||||
|
||||
@@ -11,7 +11,6 @@ import os
|
||||
import sys
|
||||
import traceback
|
||||
from types import ModuleType
|
||||
from typing import List
|
||||
|
||||
import yaml
|
||||
|
||||
@@ -119,7 +118,8 @@ class PluginManager:
|
||||
reloaded_plugins.add(plugin_name)
|
||||
break
|
||||
|
||||
def _get_classes(self, arg: ModuleType):
|
||||
@staticmethod
|
||||
def _get_classes(arg: ModuleType):
|
||||
"""获取指定模块(可以理解为一个 python 文件)下所有的类"""
|
||||
classes = []
|
||||
clsmembers = inspect.getmembers(arg, inspect.isclass)
|
||||
@@ -129,7 +129,8 @@ class PluginManager:
|
||||
break
|
||||
return classes
|
||||
|
||||
def _get_modules(self, path):
|
||||
@staticmethod
|
||||
def _get_modules(path):
|
||||
modules = []
|
||||
|
||||
dirs = os.listdir(path)
|
||||
@@ -155,7 +156,7 @@ class PluginManager:
|
||||
)
|
||||
return modules
|
||||
|
||||
def _get_plugin_modules(self) -> List[dict]:
|
||||
def _get_plugin_modules(self) -> list[dict]:
|
||||
plugins = []
|
||||
if os.path.exists(self.plugin_store_path):
|
||||
plugins.extend(self._get_modules(self.plugin_store_path))
|
||||
@@ -189,7 +190,8 @@ class PluginManager:
|
||||
except Exception as e:
|
||||
logger.error(f"更新插件 {p} 的依赖失败。Code: {str(e)}")
|
||||
|
||||
def _load_plugin_metadata(self, plugin_path: str, plugin_obj=None) -> StarMetadata:
|
||||
@staticmethod
|
||||
def _load_plugin_metadata(plugin_path: str, plugin_obj=None) -> StarMetadata:
|
||||
"""v3.4.0 以前的方式载入插件元数据
|
||||
|
||||
先寻找 metadata.yaml 文件,如果不存在,则使用插件对象的 info() 函数获取元数据。
|
||||
@@ -209,6 +211,9 @@ class PluginManager:
|
||||
metadata = plugin_obj.info()
|
||||
|
||||
if isinstance(metadata, dict):
|
||||
if "desc" not in metadata and "description" in metadata:
|
||||
metadata["desc"] = metadata["description"]
|
||||
|
||||
if (
|
||||
"name" not in metadata
|
||||
or "desc" not in metadata
|
||||
@@ -228,8 +233,9 @@ class PluginManager:
|
||||
|
||||
return metadata
|
||||
|
||||
@staticmethod
|
||||
def _get_plugin_related_modules(
|
||||
self, plugin_root_dir: str, is_reserved: bool = False
|
||||
plugin_root_dir: str, is_reserved: bool = False
|
||||
) -> list[str]:
|
||||
"""获取与指定插件相关的所有已加载模块名
|
||||
|
||||
@@ -435,7 +441,7 @@ class PluginManager:
|
||||
)
|
||||
|
||||
if path in star_map:
|
||||
# 通过装饰器的方式注册插件
|
||||
# 通过__init__subclass__注册插件
|
||||
metadata = star_map[path]
|
||||
|
||||
try:
|
||||
@@ -449,8 +455,10 @@ class PluginManager:
|
||||
metadata.desc = metadata_yaml.desc
|
||||
metadata.version = metadata_yaml.version
|
||||
metadata.repo = metadata_yaml.repo
|
||||
except Exception:
|
||||
pass
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"插件 {root_dir_name} 元数据载入失败: {str(e)}。使用默认元数据。"
|
||||
)
|
||||
metadata.config = plugin_config
|
||||
if path not in inactivated_plugins:
|
||||
# 只有没有禁用插件时才实例化插件类
|
||||
@@ -504,6 +512,8 @@ class PluginManager:
|
||||
if func_tool.name in inactivated_llm_tools:
|
||||
func_tool.active = False
|
||||
|
||||
star_registry.append(metadata)
|
||||
|
||||
else:
|
||||
# v3.4.0 以前的方式注册插件
|
||||
logger.debug(
|
||||
@@ -775,7 +785,8 @@ class PluginManager:
|
||||
|
||||
plugin.activated = False
|
||||
|
||||
async def _terminate_plugin(self, star_metadata: StarMetadata):
|
||||
@staticmethod
|
||||
async def _terminate_plugin(star_metadata: StarMetadata):
|
||||
"""终止插件,调用插件的 terminate() 和 __del__() 方法"""
|
||||
logger.info(f"正在终止插件 {star_metadata.name} ...")
|
||||
|
||||
|
||||
@@ -117,7 +117,7 @@ async def audio_to_tencent_silk_base64(audio_path: str) -> tuple[str, float]:
|
||||
try:
|
||||
import pilk
|
||||
except ImportError as e:
|
||||
raise Exception("未安装 pysilk,请执行: pip install pysilk") from e
|
||||
raise Exception("未安装 pilk: pip install pilk") from e
|
||||
|
||||
temp_dir = os.path.join(get_astrbot_data_path(), "temp")
|
||||
os.makedirs(temp_dir, exist_ok=True)
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
# What's Changed
|
||||
|
||||
1. 修复: 工具调用的结果错误地被当作消息发送
|
||||
2. 新增: 支持对引用消息中的图片进行理解(QQ, Telegram)
|
||||
3. 优化: QQ 主动消息发送逻辑,优化合并消息、文件、语音、图片等的处理
|
||||
4. 优化: 移除插件的 @register 插件注册装饰器(插件只需要继承 Star 类即可,AstrBot 会自动处理),简化插件代码开发
|
||||
+31
-19
@@ -56,12 +56,6 @@ class RstScene(Enum):
|
||||
return cls.PRIVATE
|
||||
|
||||
|
||||
@star.register(
|
||||
name="astrbot",
|
||||
desc="AstrBot 基础指令结合 + 拓展功能",
|
||||
author="Soulter",
|
||||
version="4.0.1",
|
||||
)
|
||||
class Main(star.Star):
|
||||
def __init__(self, context: star.Context) -> None:
|
||||
self.context = context
|
||||
@@ -235,9 +229,7 @@ class Main(star.Star):
|
||||
async def plugin_off(self, event: AstrMessageEvent, plugin_name: str = None):
|
||||
"""禁用插件"""
|
||||
if DEMO_MODE:
|
||||
event.set_result(
|
||||
MessageEventResult().message("演示模式下无法禁用插件。")
|
||||
)
|
||||
event.set_result(MessageEventResult().message("演示模式下无法禁用插件。"))
|
||||
return
|
||||
if not plugin_name:
|
||||
event.set_result(
|
||||
@@ -252,9 +244,7 @@ class Main(star.Star):
|
||||
async def plugin_on(self, event: AstrMessageEvent, plugin_name: str = None):
|
||||
"""启用插件"""
|
||||
if DEMO_MODE:
|
||||
event.set_result(
|
||||
MessageEventResult().message("演示模式下无法启用插件。")
|
||||
)
|
||||
event.set_result(MessageEventResult().message("演示模式下无法启用插件。"))
|
||||
return
|
||||
if not plugin_name:
|
||||
event.set_result(
|
||||
@@ -269,9 +259,7 @@ class Main(star.Star):
|
||||
async def plugin_get(self, event: AstrMessageEvent, plugin_repo: str = None):
|
||||
"""安装插件"""
|
||||
if DEMO_MODE:
|
||||
event.set_result(
|
||||
MessageEventResult().message("演示模式下无法安装插件。")
|
||||
)
|
||||
event.set_result(MessageEventResult().message("演示模式下无法安装插件。"))
|
||||
return
|
||||
if not plugin_repo:
|
||||
event.set_result(
|
||||
@@ -1310,12 +1298,36 @@ UID: {user_id} 此 ID 可用于设置管理员。
|
||||
) and not req.contexts:
|
||||
req.contexts[:0] = begin_dialogs
|
||||
|
||||
if quote and quote.message_str:
|
||||
if quote:
|
||||
sender_info = ""
|
||||
if quote.sender_nickname:
|
||||
sender_info = f"(Sent by {quote.sender_nickname})"
|
||||
else:
|
||||
sender_info = ""
|
||||
req.system_prompt += f"\nUser is quoting the message{sender_info}: {quote.message_str}, please consider the context."
|
||||
message_str = quote.message_str or "[Empty Text]"
|
||||
req.system_prompt += (
|
||||
f"\nUser is quoting a message{sender_info}.\n"
|
||||
f"Here are the information of the quoted message: Text Content: {message_str}.\n"
|
||||
)
|
||||
image_seg = None
|
||||
if quote.chain:
|
||||
for comp in quote.chain:
|
||||
if isinstance(comp, Image):
|
||||
image_seg = comp
|
||||
break
|
||||
if image_seg:
|
||||
try:
|
||||
if prov := self.context.get_using_provider(
|
||||
event.unified_msg_origin
|
||||
):
|
||||
llm_resp = await prov.text_chat(
|
||||
prompt="Please describe the image content.",
|
||||
image_urls=[await image_seg.convert_to_file_path()],
|
||||
)
|
||||
if llm_resp.completion_text:
|
||||
req.system_prompt += (
|
||||
f"Image Caption: {llm_resp.completion_text}\n"
|
||||
)
|
||||
except BaseException as e:
|
||||
logger.error(f"处理引用图片失败: {e}")
|
||||
|
||||
if self.ltm:
|
||||
try:
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
name: astrbot
|
||||
desc: AstrBot 基础指令结合 + 拓展功能
|
||||
author: Soulter
|
||||
version: 4.0.0
|
||||
@@ -94,12 +94,6 @@ DEFAULT_CONFIG = {
|
||||
PATH = os.path.join(get_astrbot_data_path(), "config", "python_interpreter.json")
|
||||
|
||||
|
||||
@star.register(
|
||||
name="astrbot-python-interpreter",
|
||||
desc="Python 代码执行器",
|
||||
author="Soulter",
|
||||
version="0.0.1",
|
||||
)
|
||||
class Main(star.Star):
|
||||
"""基于 Docker 沙箱的 Python 代码执行器"""
|
||||
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
name: astrbot-python-interpreter
|
||||
desc: Python 代码执行器
|
||||
author: Soulter
|
||||
version: 0.0.1
|
||||
@@ -11,9 +11,6 @@ from astrbot.api import llm_tool, logger
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
|
||||
|
||||
|
||||
@star.register(
|
||||
name="astrbot-reminder", desc="使用 LLM 待办提醒", author="Soulter", version="0.0.1"
|
||||
)
|
||||
class Main(star.Star):
|
||||
"""使用 LLM 待办提醒。只需对 LLM 说想要提醒的事情和时间即可。比如:`之后每天这个时候都提醒我做多邻国`"""
|
||||
|
||||
@@ -112,7 +109,7 @@ class Main(star.Star):
|
||||
Args:
|
||||
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.
|
||||
cron_expression(string): Required when user's reminder is a repeated reminder. The cron expression of the reminder. Monday is 0 and Sunday is 6.
|
||||
human_readable_cron(string): Optional. The human readable cron expression of the reminder.
|
||||
"""
|
||||
if event.get_platform_name() == "qq_official":
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
name: astrbot-reminder
|
||||
desc: 使用 LLM 待办提醒
|
||||
author: Soulter
|
||||
version: 0.0.1
|
||||
@@ -2,7 +2,7 @@ import astrbot.api.message_components as Comp
|
||||
import copy
|
||||
from astrbot.api import logger
|
||||
from astrbot.api.event import AstrMessageEvent, filter
|
||||
from astrbot.api.star import Context, Star, register
|
||||
from astrbot.api.star import Context, Star
|
||||
from astrbot.core.utils.session_waiter import (
|
||||
SessionWaiter,
|
||||
USER_SESSIONS,
|
||||
@@ -13,13 +13,6 @@ from astrbot.core.utils.session_waiter import (
|
||||
from sys import maxsize
|
||||
|
||||
|
||||
@register(
|
||||
"session_controller",
|
||||
"Cvandia & Soulter",
|
||||
"为插件支持会话控制",
|
||||
"v1.0.1",
|
||||
"https://astrbot.app",
|
||||
)
|
||||
class Waiter(Star):
|
||||
"""会话控制"""
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
name: session_controller
|
||||
desc: 为插件支持会话控制
|
||||
author: Cvandia & Soulter
|
||||
version: v1.0.1
|
||||
repo: https://astrbot.app
|
||||
@@ -1,17 +1,10 @@
|
||||
import re
|
||||
from astrbot.api.event import filter, AstrMessageEvent
|
||||
from astrbot.api.star import Context, Star, register
|
||||
from astrbot.api.star import Context, Star
|
||||
from astrbot.api.provider import LLMResponse
|
||||
from openai.types.chat.chat_completion import ChatCompletion
|
||||
|
||||
|
||||
@register(
|
||||
"thinking_filter",
|
||||
"Soulter",
|
||||
"可选择是否过滤推理模型的思考内容",
|
||||
"1.0.0",
|
||||
"https://astrbot.app",
|
||||
)
|
||||
class R1Filter(Star):
|
||||
def __init__(self, context: Context):
|
||||
super().__init__(context)
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
name: thinking_filter
|
||||
desc: 可选择是否过滤推理模型的思考内容
|
||||
author: Soulter
|
||||
version: 1.0.0
|
||||
repo: https://astrbot.app
|
||||
@@ -12,12 +12,6 @@ from bs4 import BeautifulSoup
|
||||
from .engines import HEADERS, USER_AGENTS
|
||||
|
||||
|
||||
@star.register(
|
||||
name="astrbot-web-searcher",
|
||||
desc="让 LLM 具有网页检索能力",
|
||||
author="Soulter",
|
||||
version="1.14.514",
|
||||
)
|
||||
class Main(star.Star):
|
||||
"""使用 /websearch on 或者 off 开启或者关闭网页搜索功能"""
|
||||
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
name: astrbot-web-searcher
|
||||
desc: 让 LLM 具有网页检索能力
|
||||
author: Soulter
|
||||
version: 1.14.514
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "AstrBot"
|
||||
version = "3.5.19"
|
||||
version = "3.5.20"
|
||||
description = "易上手的多平台 LLM 聊天机器人及开发框架"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
|
||||
Reference in New Issue
Block a user