mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-17 19:24:19 +00:00
feat: event log of bots (#1441)
* feat: basic arch of event log * feat: complete event log framework * fix: bad struct in bot log api * feat: add event logging to all platform adapters Co-Authored-By: wangcham233@gmail.com <651122857@qq.com> * feat: add event logging to client classes Co-Authored-By: wangcham233@gmail.com <651122857@qq.com> * refactor: bot log getting api * perf: logger for aiocqhttp and gewechat * fix: add ignored logger in dingtalk * fix: seq id bug in log getting * feat: add logger in dingtalk,QQ official,Slack, wxoa * feat: add logger for wecom * feat: add logger for wecomcs * perf(event logger): image processing * 完成机器人日志的前端部分 (#1479) * feat: webui bot log framework done * feat: bot log complete * perf(bot-log): style * chore: fix incompleted i18n * feat: support message session copy * fix: filter and badge text * perf: styles * feat: add bot toggle switch in bot card * fix: linter errors --------- Co-authored-by: Junyan Qin <rockchinq@gmail.com> --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: wangcham233@gmail.com <651122857@qq.com> Co-authored-by: HYana <65863826+KaedeSAMA@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
8dfef1d118
commit
f1e9f46af1
@@ -0,0 +1,22 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import quart
|
||||
import mimetypes
|
||||
|
||||
from .. import group
|
||||
|
||||
|
||||
@group.group_class('files', '/api/v1/files')
|
||||
class FilesRouterGroup(group.RouterGroup):
|
||||
async def initialize(self) -> None:
|
||||
@self.route('/image/<image_key>', methods=['GET'], auth_type=group.AuthType.NONE)
|
||||
async def _(image_key: str) -> quart.Response:
|
||||
if not await self.ap.storage_mgr.storage_provider.exists(image_key):
|
||||
return quart.Response(status=404)
|
||||
|
||||
image_bytes = await self.ap.storage_mgr.storage_provider.load(image_key)
|
||||
mime_type = mimetypes.guess_type(image_key)[0]
|
||||
if mime_type is None:
|
||||
mime_type = 'image/jpeg'
|
||||
|
||||
return quart.Response(image_bytes, mimetype=mime_type)
|
||||
@@ -29,3 +29,16 @@ class BotsRouterGroup(group.RouterGroup):
|
||||
elif quart.request.method == 'DELETE':
|
||||
await self.ap.bot_service.delete_bot(bot_uuid)
|
||||
return self.success()
|
||||
|
||||
@self.route('/<bot_uuid>/logs', methods=['POST'])
|
||||
async def _(bot_uuid: str) -> str:
|
||||
json_data = await quart.request.json
|
||||
from_index = json_data.get('from_index', -1)
|
||||
max_count = json_data.get('max_count', 10)
|
||||
logs, total_count = await self.ap.bot_service.list_event_logs(bot_uuid, from_index, max_count)
|
||||
return self.success(
|
||||
data={
|
||||
'logs': logs,
|
||||
'total_count': total_count,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -2,6 +2,7 @@ from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
import sqlalchemy
|
||||
import typing
|
||||
|
||||
from ....core import app
|
||||
from ....entity.persistence import bot as persistence_bot
|
||||
@@ -98,3 +99,14 @@ class BotService:
|
||||
await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.delete(persistence_bot.Bot).where(persistence_bot.Bot.uuid == bot_uuid)
|
||||
)
|
||||
|
||||
async def list_event_logs(
|
||||
self, bot_uuid: str, from_index: int, max_count: int
|
||||
) -> typing.Tuple[list[dict], int, int, int]:
|
||||
runtime_bot = await self.ap.platform_mgr.get_bot_by_uuid(bot_uuid)
|
||||
if runtime_bot is None:
|
||||
raise Exception('Bot not found')
|
||||
|
||||
logs, total_count = await runtime_bot.logger.get_logs(from_index, max_count)
|
||||
|
||||
return [log.to_json() for log in logs], total_count
|
||||
|
||||
@@ -23,6 +23,7 @@ from ..api.http.service import model as model_service
|
||||
from ..api.http.service import pipeline as pipeline_service
|
||||
from ..api.http.service import bot as bot_service
|
||||
from ..discover import engine as discover_engine
|
||||
from ..storage import mgr as storagemgr
|
||||
from ..utils import logcache
|
||||
from . import taskmgr
|
||||
from . import entities as core_entities
|
||||
@@ -96,6 +97,8 @@ class Application:
|
||||
|
||||
log_cache: logcache.LogCache = None
|
||||
|
||||
storage_mgr: storagemgr.StorageMgr = None
|
||||
|
||||
# ========= HTTP Services =========
|
||||
|
||||
user_service: user_service.UserService = None
|
||||
|
||||
@@ -17,6 +17,7 @@ from ...api.http.service import model as model_service
|
||||
from ...api.http.service import pipeline as pipeline_service
|
||||
from ...api.http.service import bot as bot_service
|
||||
from ...discover import engine as discover_engine
|
||||
from ...storage import mgr as storagemgr
|
||||
from ...utils import logcache
|
||||
from .. import taskmgr
|
||||
|
||||
@@ -50,6 +51,10 @@ class BuildAppStage(stage.BootingStage):
|
||||
log_cache = logcache.LogCache()
|
||||
ap.log_cache = log_cache
|
||||
|
||||
storage_mgr_inst = storagemgr.StorageMgr(ap)
|
||||
await storage_mgr_inst.initialize()
|
||||
ap.storage_mgr = storage_mgr_inst
|
||||
|
||||
persistence_mgr_inst = persistencemgr.PersistenceManager(ap)
|
||||
ap.persistence_mgr = persistence_mgr_inst
|
||||
await persistence_mgr_inst.initialize()
|
||||
|
||||
@@ -7,11 +7,11 @@ from ..core import app, entities as core_entities
|
||||
from . import entities
|
||||
|
||||
|
||||
preregistered_stages: dict[str, PipelineStage] = {}
|
||||
preregistered_stages: dict[str, type[PipelineStage]] = {}
|
||||
|
||||
|
||||
def stage_class(name: str):
|
||||
def decorator(cls):
|
||||
def stage_class(name: str) -> typing.Callable[[type[PipelineStage]], type[PipelineStage]]:
|
||||
def decorator(cls: type[PipelineStage]) -> type[PipelineStage]:
|
||||
preregistered_stages[name] = cls
|
||||
return cls
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import abc
|
||||
from ..core import app
|
||||
from .types import message as platform_message
|
||||
from .types import events as platform_events
|
||||
from .logger import EventLogger
|
||||
|
||||
|
||||
class MessagePlatformAdapter(metaclass=abc.ABCMeta):
|
||||
@@ -22,7 +23,9 @@ class MessagePlatformAdapter(metaclass=abc.ABCMeta):
|
||||
|
||||
ap: app.Application
|
||||
|
||||
def __init__(self, config: dict, ap: app.Application):
|
||||
logger: EventLogger
|
||||
|
||||
def __init__(self, config: dict, ap: app.Application, logger: EventLogger):
|
||||
"""初始化适配器
|
||||
|
||||
Args:
|
||||
@@ -31,6 +34,7 @@ class MessagePlatformAdapter(metaclass=abc.ABCMeta):
|
||||
"""
|
||||
self.config = config
|
||||
self.ap = ap
|
||||
self.logger = logger
|
||||
|
||||
async def send_message(self, target_type: str, target_id: str, message: platform_message.MessageChain):
|
||||
"""主动发送消息
|
||||
|
||||
+36
-7
@@ -10,12 +10,14 @@ import sqlalchemy
|
||||
from . import adapter as msadapter
|
||||
|
||||
from ..core import app, entities as core_entities, taskmgr
|
||||
from .types import events as platform_events
|
||||
from .types import events as platform_events, message as platform_message
|
||||
|
||||
from ..discover import engine
|
||||
|
||||
from ..entity.persistence import bot as persistence_bot
|
||||
|
||||
from .logger import EventLogger
|
||||
|
||||
# 处理 3.4 移除了 YiriMirai 之后,插件的兼容性问题
|
||||
from . import types as mirai
|
||||
|
||||
@@ -37,23 +39,37 @@ class RuntimeBot:
|
||||
|
||||
task_context: taskmgr.TaskContext
|
||||
|
||||
logger: EventLogger
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
ap: app.Application,
|
||||
bot_entity: persistence_bot.Bot,
|
||||
adapter: msadapter.MessagePlatformAdapter,
|
||||
logger: EventLogger,
|
||||
):
|
||||
self.ap = ap
|
||||
self.bot_entity = bot_entity
|
||||
self.enable = bot_entity.enable
|
||||
self.adapter = adapter
|
||||
self.task_context = taskmgr.TaskContext()
|
||||
self.logger = logger
|
||||
|
||||
async def initialize(self):
|
||||
async def on_friend_message(
|
||||
event: platform_events.FriendMessage,
|
||||
adapter: msadapter.MessagePlatformAdapter,
|
||||
):
|
||||
image_components = [
|
||||
component for component in event.message_chain if isinstance(component, platform_message.Image)
|
||||
]
|
||||
|
||||
await self.logger.info(
|
||||
f'{event.message_chain}',
|
||||
images=image_components,
|
||||
message_session_id=f'person_{event.sender.id}',
|
||||
)
|
||||
|
||||
await self.ap.query_pool.add_query(
|
||||
bot_uuid=self.bot_entity.uuid,
|
||||
launcher_type=core_entities.LauncherTypes.PERSON,
|
||||
@@ -68,6 +84,16 @@ class RuntimeBot:
|
||||
event: platform_events.GroupMessage,
|
||||
adapter: msadapter.MessagePlatformAdapter,
|
||||
):
|
||||
image_components = [
|
||||
component for component in event.message_chain if isinstance(component, platform_message.Image)
|
||||
]
|
||||
|
||||
await self.logger.info(
|
||||
f'{event.message_chain}',
|
||||
images=image_components,
|
||||
message_session_id=f'group_{event.group.id}',
|
||||
)
|
||||
|
||||
await self.ap.query_pool.add_query(
|
||||
bot_uuid=self.bot_entity.uuid,
|
||||
launcher_type=core_entities.LauncherTypes.GROUP,
|
||||
@@ -92,10 +118,7 @@ class RuntimeBot:
|
||||
self.task_context.set_current_action('Exited.')
|
||||
return
|
||||
self.task_context.set_current_action('Exited with error.')
|
||||
self.task_context.log(f'平台适配器运行出错: {e}')
|
||||
self.task_context.log(f'Traceback: {traceback.format_exc()}')
|
||||
self.ap.logger.error(f'平台适配器运行出错: {e}')
|
||||
self.ap.logger.debug(f'Traceback: {traceback.format_exc()}')
|
||||
await self.logger.error(f'平台适配器运行出错:\n{e}\n{traceback.format_exc()}')
|
||||
|
||||
self.task_wrapper = self.ap.task_mgr.create_task(
|
||||
exception_wrapper(),
|
||||
@@ -166,9 +189,15 @@ class PlatformManager:
|
||||
elif isinstance(bot_entity, dict):
|
||||
bot_entity = persistence_bot.Bot(**bot_entity)
|
||||
|
||||
adapter_inst = self.adapter_dict[bot_entity.adapter](bot_entity.adapter_config, self.ap)
|
||||
logger = EventLogger(name=f'platform-adapter-{bot_entity.name}', ap=self.ap)
|
||||
|
||||
runtime_bot = RuntimeBot(ap=self.ap, bot_entity=bot_entity, adapter=adapter_inst)
|
||||
adapter_inst = self.adapter_dict[bot_entity.adapter](
|
||||
bot_entity.adapter_config,
|
||||
self.ap,
|
||||
logger,
|
||||
)
|
||||
|
||||
runtime_bot = RuntimeBot(ap=self.ap, bot_entity=bot_entity, adapter=adapter_inst, logger=logger)
|
||||
|
||||
await runtime_bot.initialize()
|
||||
|
||||
|
||||
@@ -0,0 +1,233 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import typing
|
||||
import mimetypes
|
||||
import time
|
||||
import enum
|
||||
import pydantic
|
||||
import traceback
|
||||
import uuid
|
||||
|
||||
from ..core import app
|
||||
from .types import message as platform_message
|
||||
|
||||
|
||||
class EventLogLevel(enum.Enum):
|
||||
"""日志级别"""
|
||||
|
||||
DEBUG = 'debug'
|
||||
INFO = 'info'
|
||||
WARNING = 'warning'
|
||||
ERROR = 'error'
|
||||
|
||||
|
||||
class EventLog(pydantic.BaseModel):
|
||||
seq_id: int
|
||||
"""日志序号"""
|
||||
|
||||
timestamp: int
|
||||
"""日志时间戳"""
|
||||
|
||||
level: EventLogLevel
|
||||
"""日志级别"""
|
||||
|
||||
text: str
|
||||
"""日志文本"""
|
||||
|
||||
images: typing.Optional[list[str]] = None
|
||||
"""日志图片 URL 列表,需要通过 /api/v1/image/{uuid} 获取图片"""
|
||||
|
||||
message_session_id: typing.Optional[str] = None
|
||||
"""消息会话ID,仅收发消息事件有值"""
|
||||
|
||||
def to_json(self) -> dict:
|
||||
return {
|
||||
'seq_id': self.seq_id,
|
||||
'timestamp': self.timestamp,
|
||||
'level': self.level.value,
|
||||
'text': self.text,
|
||||
'images': self.images,
|
||||
'message_session_id': self.message_session_id,
|
||||
}
|
||||
|
||||
|
||||
MAX_LOG_COUNT = 200
|
||||
DELETE_COUNT_PER_TIME = 50
|
||||
|
||||
|
||||
class EventLogger:
|
||||
"""used for logging bot events"""
|
||||
|
||||
ap: app.Application
|
||||
|
||||
seq_id_inc: int
|
||||
|
||||
logs: list[EventLog]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
ap: app.Application,
|
||||
):
|
||||
self.name = name
|
||||
self.ap = ap
|
||||
self.logs = []
|
||||
self.seq_id_inc = 0
|
||||
|
||||
async def get_logs(self, from_seq_id: int, max_count: int) -> typing.Tuple[list[EventLog], int]:
|
||||
"""
|
||||
获取日志,从 from_seq_id 开始获取 max_count 条历史日志
|
||||
|
||||
Args:
|
||||
from_seq_id: 起始序号,-1 表示末尾
|
||||
max_count: 最大数量
|
||||
|
||||
Returns:
|
||||
Tuple[list[EventLog], int]: 日志列表,日志总数
|
||||
"""
|
||||
if len(self.logs) == 0:
|
||||
return [], 0
|
||||
|
||||
if from_seq_id <= -1:
|
||||
from_seq_id = self.logs[-1].seq_id
|
||||
|
||||
min_seq_id_in_logs = self.logs[0].seq_id
|
||||
max_seq_id_in_logs = self.logs[-1].seq_id
|
||||
|
||||
if from_seq_id < min_seq_id_in_logs: # 需要的整个范围都已经被删除
|
||||
return [], len(self.logs)
|
||||
|
||||
if (
|
||||
from_seq_id > max_seq_id_in_logs and from_seq_id - max_count > max_seq_id_in_logs
|
||||
): # 需要的整个范围都还没生成
|
||||
return [], len(self.logs)
|
||||
|
||||
end_index = 1
|
||||
|
||||
for i, log in enumerate(self.logs):
|
||||
if log.seq_id >= from_seq_id:
|
||||
end_index = i + 1
|
||||
break
|
||||
|
||||
start_index = max(0, end_index - max_count)
|
||||
|
||||
if max_count > 0:
|
||||
return self.logs[start_index:end_index], len(self.logs)
|
||||
else:
|
||||
return [], len(self.logs)
|
||||
|
||||
async def _truncate_logs(self):
|
||||
if len(self.logs) > MAX_LOG_COUNT:
|
||||
for i in range(DELETE_COUNT_PER_TIME):
|
||||
for image_key in self.logs[i].images:
|
||||
await self.ap.storage_mgr.storage_provider.delete(image_key)
|
||||
self.logs = self.logs[DELETE_COUNT_PER_TIME:]
|
||||
|
||||
async def _add_log(
|
||||
self,
|
||||
level: EventLogLevel,
|
||||
text: str,
|
||||
images: typing.Optional[list[platform_message.Image]] = None,
|
||||
message_session_id: typing.Optional[str] = None,
|
||||
no_throw: bool = True,
|
||||
):
|
||||
try:
|
||||
image_keys = []
|
||||
|
||||
if images is None:
|
||||
images = []
|
||||
|
||||
if message_session_id is None:
|
||||
message_session_id = ''
|
||||
|
||||
if not isinstance(message_session_id, str):
|
||||
message_session_id = str(message_session_id)
|
||||
|
||||
for img in images:
|
||||
img_bytes, mime_type = await img.get_bytes()
|
||||
extension = mimetypes.guess_extension(mime_type)
|
||||
if extension is None:
|
||||
extension = '.jpg'
|
||||
image_key = f'{message_session_id}-{uuid.uuid4()}{extension}'
|
||||
await self.ap.storage_mgr.storage_provider.save(image_key, img_bytes)
|
||||
image_keys.append(image_key)
|
||||
|
||||
self.logs.append(
|
||||
EventLog(
|
||||
seq_id=self.seq_id_inc,
|
||||
timestamp=int(time.time()),
|
||||
level=level,
|
||||
text=text,
|
||||
images=image_keys,
|
||||
message_session_id=message_session_id,
|
||||
)
|
||||
)
|
||||
self.seq_id_inc += 1
|
||||
|
||||
await self._truncate_logs()
|
||||
|
||||
except Exception as e:
|
||||
if not no_throw:
|
||||
raise e
|
||||
else:
|
||||
traceback.print_exc()
|
||||
|
||||
async def info(
|
||||
self,
|
||||
text: str,
|
||||
images: typing.Optional[list[platform_message.Image]] = None,
|
||||
message_session_id: typing.Optional[str] = None,
|
||||
no_throw: bool = True,
|
||||
):
|
||||
await self._add_log(
|
||||
level=EventLogLevel.INFO,
|
||||
text=text,
|
||||
images=images,
|
||||
message_session_id=message_session_id,
|
||||
no_throw=no_throw,
|
||||
)
|
||||
|
||||
async def debug(
|
||||
self,
|
||||
text: str,
|
||||
images: typing.Optional[list[platform_message.Image]] = None,
|
||||
message_session_id: typing.Optional[str] = None,
|
||||
no_throw: bool = True,
|
||||
):
|
||||
await self._add_log(
|
||||
level=EventLogLevel.DEBUG,
|
||||
text=text,
|
||||
images=images,
|
||||
message_session_id=message_session_id,
|
||||
no_throw=no_throw,
|
||||
)
|
||||
|
||||
async def warning(
|
||||
self,
|
||||
text: str,
|
||||
images: typing.Optional[list[platform_message.Image]] = None,
|
||||
message_session_id: typing.Optional[str] = None,
|
||||
no_throw: bool = True,
|
||||
):
|
||||
await self._add_log(
|
||||
level=EventLogLevel.WARNING,
|
||||
text=text,
|
||||
images=images,
|
||||
message_session_id=message_session_id,
|
||||
no_throw=no_throw,
|
||||
)
|
||||
|
||||
async def error(
|
||||
self,
|
||||
text: str,
|
||||
images: typing.Optional[list[platform_message.Image]] = None,
|
||||
message_session_id: typing.Optional[str] = None,
|
||||
no_throw: bool = True,
|
||||
):
|
||||
await self._add_log(
|
||||
level=EventLogLevel.ERROR,
|
||||
text=text,
|
||||
images=images,
|
||||
message_session_id=message_session_id,
|
||||
no_throw=no_throw,
|
||||
)
|
||||
@@ -12,6 +12,7 @@ from ..types import message as platform_message
|
||||
from ..types import events as platform_events
|
||||
from ..types import entities as platform_entities
|
||||
from ...utils import image
|
||||
from ..logger import EventLogger
|
||||
|
||||
|
||||
class AiocqhttpMessageConverter(adapter.MessageConverter):
|
||||
@@ -209,8 +210,11 @@ class AiocqhttpAdapter(adapter.MessagePlatformAdapter):
|
||||
|
||||
ap: app.Application
|
||||
|
||||
def __init__(self, config: dict, ap: app.Application):
|
||||
on_websocket_connection_event_cache: typing.List[typing.Callable[[aiocqhttp.Event], None]] = []
|
||||
|
||||
def __init__(self, config: dict, ap: app.Application, logger: EventLogger):
|
||||
self.config = config
|
||||
self.logger = logger
|
||||
|
||||
async def shutdown_trigger_placeholder():
|
||||
while True:
|
||||
@@ -219,6 +223,7 @@ class AiocqhttpAdapter(adapter.MessagePlatformAdapter):
|
||||
self.config['shutdown_trigger'] = shutdown_trigger_placeholder
|
||||
|
||||
self.ap = ap
|
||||
self.on_websocket_connection_event_cache = []
|
||||
|
||||
if 'access-token' in config:
|
||||
self.bot = aiocqhttp.CQHttp(access_token=config['access-token'])
|
||||
@@ -260,6 +265,7 @@ class AiocqhttpAdapter(adapter.MessagePlatformAdapter):
|
||||
try:
|
||||
return await callback(await self.event_converter.target2yiri(event,self.bot), self)
|
||||
except Exception:
|
||||
await self.logger.error(f'Error in on_message: {traceback.format_exc()}')
|
||||
traceback.print_exc()
|
||||
|
||||
if event_type == platform_events.GroupMessage:
|
||||
@@ -267,6 +273,16 @@ class AiocqhttpAdapter(adapter.MessagePlatformAdapter):
|
||||
elif event_type == platform_events.FriendMessage:
|
||||
self.bot.on_message('private')(on_message)
|
||||
|
||||
async def on_websocket_connection(event: aiocqhttp.Event):
|
||||
for event in self.on_websocket_connection_event_cache:
|
||||
if event.self_id == event.self_id and event.time == event.time:
|
||||
return
|
||||
|
||||
self.on_websocket_connection_event_cache.append(event)
|
||||
await self.logger.info(f'WebSocket connection established, bot id: {event.self_id}')
|
||||
|
||||
self.bot.on_websocket_connection(on_websocket_connection)
|
||||
|
||||
def unregister_listener(
|
||||
self,
|
||||
event_type: typing.Type[platform_events.Event],
|
||||
|
||||
@@ -9,6 +9,7 @@ from ..types import events as platform_events
|
||||
from ..types import entities as platform_entities
|
||||
from libs.dingtalk_api.api import DingTalkClient
|
||||
import datetime
|
||||
from ..logger import EventLogger
|
||||
|
||||
|
||||
class DingTalkMessageConverter(adapter.MessageConverter):
|
||||
@@ -99,9 +100,10 @@ class DingTalkAdapter(adapter.MessagePlatformAdapter):
|
||||
event_converter: DingTalkEventConverter = DingTalkEventConverter()
|
||||
config: dict
|
||||
|
||||
def __init__(self, config: dict, ap: app.Application):
|
||||
def __init__(self, config: dict, ap: app.Application, logger: EventLogger):
|
||||
self.config = config
|
||||
self.ap = ap
|
||||
self.logger = logger
|
||||
required_keys = [
|
||||
'client_id',
|
||||
'client_secret',
|
||||
@@ -120,6 +122,7 @@ class DingTalkAdapter(adapter.MessagePlatformAdapter):
|
||||
robot_name=config['robot_name'],
|
||||
robot_code=config['robot_code'],
|
||||
markdown_card=config['markdown_card'],
|
||||
logger=self.logger,
|
||||
)
|
||||
|
||||
async def reply_message(
|
||||
@@ -154,8 +157,8 @@ class DingTalkAdapter(adapter.MessagePlatformAdapter):
|
||||
await self.event_converter.target2yiri(event, self.config['robot_name']),
|
||||
self,
|
||||
)
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
except Exception as e:
|
||||
await self.logger.error(f"Error in dingtalk callback: {traceback.format_exc()}")
|
||||
|
||||
if event_type == platform_events.FriendMessage:
|
||||
self.bot.on_message('FriendMessage')(on_message)
|
||||
|
||||
@@ -16,6 +16,7 @@ from ...core import app
|
||||
from ..types import message as platform_message
|
||||
from ..types import events as platform_events
|
||||
from ..types import entities as platform_entities
|
||||
from ..logger import EventLogger
|
||||
|
||||
|
||||
class DiscordMessageConverter(adapter.MessageConverter):
|
||||
@@ -170,9 +171,10 @@ class DiscordAdapter(adapter.MessagePlatformAdapter):
|
||||
typing.Callable[[platform_events.Event, adapter.MessagePlatformAdapter], None],
|
||||
] = {}
|
||||
|
||||
def __init__(self, config: dict, ap: app.Application):
|
||||
def __init__(self, config: dict, ap: app.Application, logger: EventLogger):
|
||||
self.config = config
|
||||
self.ap = ap
|
||||
self.logger = logger
|
||||
|
||||
self.bot_account_id = self.config['client_id']
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ from ...utils import image
|
||||
import xml.etree.ElementTree as ET
|
||||
from typing import Optional, Tuple
|
||||
from functools import partial
|
||||
from ..logger import EventLogger
|
||||
|
||||
|
||||
class GewechatMessageConverter(adapter.MessageConverter):
|
||||
@@ -371,7 +372,7 @@ class GewechatMessageConverter(adapter.MessageConverter):
|
||||
quote_id = appmsg_data.find('.//refermsg').findtext('.//chatusr') # 引用消息的原发送者
|
||||
ats_bot = ats_bot or (quote_id == tousername)
|
||||
except Exception as e:
|
||||
print(f'_ats_bot got except: {e}')
|
||||
print(f'Error in gewechat _ats_bot: {e}')
|
||||
finally:
|
||||
return ats_bot
|
||||
|
||||
@@ -477,9 +478,10 @@ class GeWeChatAdapter(adapter.MessagePlatformAdapter):
|
||||
typing.Callable[[platform_events.Event, adapter.MessagePlatformAdapter], None],
|
||||
] = {}
|
||||
|
||||
def __init__(self, config: dict, ap: app.Application):
|
||||
def __init__(self, config: dict, ap: app.Application, logger: EventLogger):
|
||||
self.config = config
|
||||
self.ap = ap
|
||||
self.logger = logger
|
||||
self.quart_app = quart.Quart(__name__)
|
||||
|
||||
self.message_converter = GewechatMessageConverter(config)
|
||||
@@ -503,7 +505,7 @@ class GeWeChatAdapter(adapter.MessagePlatformAdapter):
|
||||
try:
|
||||
event = await self.event_converter.target2yiri(data.copy(), self.bot_account_id)
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
await self.logger.error(f'Error in gewechat callback: {traceback.format_exc()}')
|
||||
|
||||
if event.__class__ in self.listeners:
|
||||
await self.listeners[event.__class__](event, self)
|
||||
|
||||
@@ -23,6 +23,7 @@ from ...core import app
|
||||
from ..types import message as platform_message
|
||||
from ..types import events as platform_events
|
||||
from ..types import entities as platform_entities
|
||||
from ..logger import EventLogger
|
||||
|
||||
|
||||
class AESCipher(object):
|
||||
@@ -338,9 +339,10 @@ class LarkAdapter(adapter.MessagePlatformAdapter):
|
||||
quart_app: quart.Quart
|
||||
ap: app.Application
|
||||
|
||||
def __init__(self, config: dict, ap: app.Application):
|
||||
def __init__(self, config: dict, ap: app.Application, logger: EventLogger):
|
||||
self.config = config
|
||||
self.ap = ap
|
||||
self.logger = logger
|
||||
self.quart_app = quart.Quart(__name__)
|
||||
self.listeners = {}
|
||||
|
||||
@@ -376,15 +378,15 @@ class LarkAdapter(adapter.MessagePlatformAdapter):
|
||||
if 'im.message.receive_v1' == type:
|
||||
try:
|
||||
event = await self.event_converter.target2yiri(p2v1, self.api_client)
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
except Exception as e:
|
||||
await self.logger.error(f"Error in lark callback: {traceback.format_exc()}")
|
||||
|
||||
if event.__class__ in self.listeners:
|
||||
await self.listeners[event.__class__](event, self)
|
||||
|
||||
return {'code': 200, 'message': 'ok'}
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
except Exception as e:
|
||||
await self.logger.error(f"Error in lark callback: {traceback.format_exc()}")
|
||||
return {'code': 500, 'message': 'error'}
|
||||
|
||||
async def on_message(event: lark_oapi.im.v1.P2ImMessageReceiveV1):
|
||||
|
||||
@@ -14,6 +14,7 @@ from ...pipeline.longtext.strategies import forward
|
||||
from ...platform.types import message as platform_message
|
||||
from ...platform.types import entities as platform_entities
|
||||
from ...platform.types import events as platform_events
|
||||
from ..logger import EventLogger
|
||||
|
||||
|
||||
class NakuruProjectMessageConverter(adapter_model.MessageConverter):
|
||||
@@ -71,9 +72,8 @@ class NakuruProjectMessageConverter(adapter_model.MessageConverter):
|
||||
content=content_list,
|
||||
)
|
||||
nakuru_forward_node_list.append(nakuru_forward_node)
|
||||
except Exception:
|
||||
except Exception as e:
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
|
||||
nakuru_msg_list.append(nakuru_forward_node_list)
|
||||
@@ -178,12 +178,13 @@ class NakuruAdapter(adapter_model.MessagePlatformAdapter):
|
||||
|
||||
cfg: dict
|
||||
|
||||
def __init__(self, cfg: dict, ap):
|
||||
def __init__(self, cfg: dict, ap, logger: EventLogger):
|
||||
"""初始化nakuru-project的对象"""
|
||||
cfg['port'] = cfg['ws_port']
|
||||
del cfg['ws_port']
|
||||
self.cfg = cfg
|
||||
self.ap = ap
|
||||
self.logger = logger
|
||||
self.listener_list = []
|
||||
self.bot = nakuru.CQHTTP(**self.cfg)
|
||||
|
||||
@@ -275,7 +276,7 @@ class NakuruAdapter(adapter_model.MessagePlatformAdapter):
|
||||
# 注册监听器
|
||||
self.bot.receiver(source_cls.__name__)(listener_wrapper)
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
self.logger.error(f"Error in nakuru register_listener: {traceback.format_exc()}")
|
||||
raise e
|
||||
|
||||
def unregister_listener(
|
||||
|
||||
@@ -13,6 +13,7 @@ from .. import adapter
|
||||
from ...core import app
|
||||
from ..types import entities as platform_entities
|
||||
from ...command.errors import ParamNotEnoughError
|
||||
from ..logger import EventLogger
|
||||
|
||||
|
||||
class OAMessageConverter(adapter.MessageConverter):
|
||||
@@ -63,10 +64,10 @@ class OfficialAccountAdapter(adapter.MessagePlatformAdapter):
|
||||
event_converter: OAEventConverter = OAEventConverter()
|
||||
config: dict
|
||||
|
||||
def __init__(self, config: dict, ap: app.Application):
|
||||
def __init__(self, config: dict, ap: app.Application, logger: EventLogger):
|
||||
self.config = config
|
||||
|
||||
self.ap = ap
|
||||
self.logger = logger
|
||||
|
||||
required_keys = [
|
||||
'token',
|
||||
@@ -85,6 +86,7 @@ class OfficialAccountAdapter(adapter.MessagePlatformAdapter):
|
||||
EncodingAESKey=config['EncodingAESKey'],
|
||||
Appsecret=config['AppSecret'],
|
||||
AppID=config['AppID'],
|
||||
logger=self.logger,
|
||||
)
|
||||
elif self.config['Mode'] == 'passive':
|
||||
self.bot = OAClientForLongerResponse(
|
||||
@@ -93,6 +95,7 @@ class OfficialAccountAdapter(adapter.MessagePlatformAdapter):
|
||||
Appsecret=config['AppSecret'],
|
||||
AppID=config['AppID'],
|
||||
LoadingMessage=config['LoadingMessage'],
|
||||
logger=self.logger,
|
||||
)
|
||||
else:
|
||||
raise KeyError('请设置微信公众号通信模式')
|
||||
@@ -122,8 +125,8 @@ class OfficialAccountAdapter(adapter.MessagePlatformAdapter):
|
||||
self.bot_account_id = event.receiver_id
|
||||
try:
|
||||
return await callback(await self.event_converter.target2yiri(event), self)
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
except Exception as e:
|
||||
await self.logger.error(f"Error in officialaccount callback: {traceback.format_exc()}")
|
||||
|
||||
if event_type == platform_events.FriendMessage:
|
||||
self.bot.on_message('text')(on_message)
|
||||
|
||||
@@ -17,6 +17,7 @@ from ...config import manager as cfg_mgr
|
||||
from ...platform.types import entities as platform_entities
|
||||
from ...platform.types import events as platform_events
|
||||
from ...platform.types import message as platform_message
|
||||
from ..logger import EventLogger
|
||||
|
||||
|
||||
class OfficialGroupMessage(platform_events.GroupMessage):
|
||||
@@ -357,10 +358,11 @@ class OfficialAdapter(adapter_model.MessagePlatformAdapter):
|
||||
group_msg_seq = None
|
||||
c2c_msg_seq = None
|
||||
|
||||
def __init__(self, cfg: dict, ap: app.Application):
|
||||
def __init__(self, cfg: dict, ap: app.Application, logger: EventLogger):
|
||||
"""初始化适配器"""
|
||||
self.cfg = cfg
|
||||
self.ap = ap
|
||||
self.logger = logger
|
||||
|
||||
self.group_msg_seq = 1
|
||||
self.c2c_msg_seq = 1
|
||||
@@ -499,7 +501,7 @@ class OfficialAdapter(adapter_model.MessagePlatformAdapter):
|
||||
for event_handler in event_handler_mapping[event_type]:
|
||||
setattr(self.bot, event_handler, wrapper)
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
self.logger.error(f"Error in qqbotpy callback: {traceback.format_exc()}")
|
||||
raise e
|
||||
|
||||
def unregister_listener(
|
||||
|
||||
@@ -14,6 +14,7 @@ from ...command.errors import ParamNotEnoughError
|
||||
from libs.qq_official_api.api import QQOfficialClient
|
||||
from libs.qq_official_api.qqofficialevent import QQOfficialEvent
|
||||
from ...utils import image
|
||||
from ..logger import EventLogger
|
||||
|
||||
|
||||
class QQOfficialMessageConverter(adapter.MessageConverter):
|
||||
@@ -139,9 +140,10 @@ class QQOfficialAdapter(adapter.MessagePlatformAdapter):
|
||||
message_converter: QQOfficialMessageConverter = QQOfficialMessageConverter()
|
||||
event_converter: QQOfficialEventConverter = QQOfficialEventConverter()
|
||||
|
||||
def __init__(self, config: dict, ap: app.Application):
|
||||
def __init__(self, config: dict, ap: app.Application, logger: EventLogger):
|
||||
self.config = config
|
||||
self.ap = ap
|
||||
self.logger = logger
|
||||
|
||||
required_keys = [
|
||||
'appid',
|
||||
@@ -155,6 +157,7 @@ class QQOfficialAdapter(adapter.MessagePlatformAdapter):
|
||||
app_id=config['appid'],
|
||||
secret=config['secret'],
|
||||
token=config['token'],
|
||||
logger=self.logger
|
||||
)
|
||||
|
||||
async def reply_message(
|
||||
@@ -221,8 +224,8 @@ class QQOfficialAdapter(adapter.MessagePlatformAdapter):
|
||||
self.bot_account_id = 'justbot'
|
||||
try:
|
||||
return await callback(await self.event_converter.target2yiri(event), self)
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
except Exception as e:
|
||||
await self.logger.error(f"Error in qqofficial callback: {traceback.format_exc()}")
|
||||
|
||||
if event_type == platform_events.FriendMessage:
|
||||
self.bot.on_message('DIRECT_MESSAGE_CREATE')(on_message)
|
||||
|
||||
@@ -14,6 +14,7 @@ from .. import adapter
|
||||
from ..types import entities as platform_entities
|
||||
from ...command.errors import ParamNotEnoughError
|
||||
from ...utils import image
|
||||
from ..logger import EventLogger
|
||||
|
||||
|
||||
class SlackMessageConverter(adapter.MessageConverter):
|
||||
@@ -91,9 +92,10 @@ class SlackAdapter(adapter.MessagePlatformAdapter):
|
||||
event_converter: SlackEventConverter = SlackEventConverter()
|
||||
config: dict
|
||||
|
||||
def __init__(self, config: dict, ap: app.Application):
|
||||
def __init__(self, config: dict, ap: app.Application, logger: EventLogger):
|
||||
self.config = config
|
||||
self.ap = ap
|
||||
self.logger = logger
|
||||
required_keys = [
|
||||
'bot_token',
|
||||
'signing_secret',
|
||||
@@ -102,7 +104,7 @@ class SlackAdapter(adapter.MessagePlatformAdapter):
|
||||
if missing_keys:
|
||||
raise ParamNotEnoughError('Slack机器人缺少相关配置项,请查看文档或联系管理员')
|
||||
|
||||
self.bot = SlackClient(bot_token=self.config['bot_token'], signing_secret=self.config['signing_secret'])
|
||||
self.bot = SlackClient(bot_token=self.config['bot_token'], signing_secret=self.config['signing_secret'], logger=self.logger)
|
||||
|
||||
async def reply_message(
|
||||
self,
|
||||
@@ -137,8 +139,8 @@ class SlackAdapter(adapter.MessagePlatformAdapter):
|
||||
self.bot_account_id = 'SlackBot'
|
||||
try:
|
||||
return await callback(await self.event_converter.target2yiri(event, self.bot), self)
|
||||
except:
|
||||
traceback.print_exc()
|
||||
except Exception as e:
|
||||
await self.logger.error(f"Error in slack callback: {traceback.format_exc()}")
|
||||
|
||||
if event_type == platform_events.FriendMessage:
|
||||
self.bot.on_message('im')(on_message)
|
||||
|
||||
@@ -17,6 +17,7 @@ from ...core import app
|
||||
from ..types import message as platform_message
|
||||
from ..types import events as platform_events
|
||||
from ..types import entities as platform_entities
|
||||
from ..logger import EventLogger
|
||||
|
||||
|
||||
class TelegramMessageConverter(adapter.MessageConverter):
|
||||
@@ -147,9 +148,10 @@ class TelegramAdapter(adapter.MessagePlatformAdapter):
|
||||
typing.Callable[[platform_events.Event, adapter.MessagePlatformAdapter], None],
|
||||
] = {}
|
||||
|
||||
def __init__(self, config: dict, ap: app.Application):
|
||||
def __init__(self, config: dict, ap: app.Application, logger: EventLogger):
|
||||
self.config = config
|
||||
self.ap = ap
|
||||
self.logger = logger
|
||||
|
||||
async def telegram_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
if update.message.from_user.is_bot:
|
||||
@@ -158,8 +160,8 @@ class TelegramAdapter(adapter.MessagePlatformAdapter):
|
||||
try:
|
||||
lb_event = await self.event_converter.target2yiri(update, self.bot, self.bot_account_id)
|
||||
await self.listeners[type(lb_event)](lb_event, self)
|
||||
except Exception:
|
||||
print(traceback.format_exc())
|
||||
except Exception as e:
|
||||
await self.logger.error(f"Error in telegram callback: {traceback.format_exc()}")
|
||||
|
||||
self.application = ApplicationBuilder().token(self.config['token']).build()
|
||||
self.bot = self.application.bot
|
||||
|
||||
@@ -30,6 +30,7 @@ from ..types import message as platform_message
|
||||
from ..types import events as platform_events
|
||||
from ..types import entities as platform_entities
|
||||
from ...utils import image
|
||||
from ..logger import EventLogger
|
||||
import xml.etree.ElementTree as ET
|
||||
from typing import Optional, List, Tuple
|
||||
from functools import partial
|
||||
@@ -533,9 +534,10 @@ class WeChatPadAdapter(adapter.MessagePlatformAdapter):
|
||||
typing.Callable[[platform_events.Event, adapter.MessagePlatformAdapter], None],
|
||||
] = {}
|
||||
|
||||
def __init__(self, config: dict, ap: app.Application):
|
||||
def __init__(self, config: dict, ap: app.Application, logger: EventLogger):
|
||||
self.config = config
|
||||
self.ap = ap
|
||||
self.logger = logger
|
||||
self.quart_app = quart.Quart(__name__)
|
||||
|
||||
self.message_converter = WeChatPadMessageConverter(config)
|
||||
@@ -550,7 +552,7 @@ class WeChatPadAdapter(adapter.MessagePlatformAdapter):
|
||||
try:
|
||||
event = await self.event_converter.target2yiri(data.copy(), self.bot_account_id)
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
await self.logger.error(f"Error in wechatpad callback: {traceback.format_exc()}")
|
||||
|
||||
if event.__class__ in self.listeners:
|
||||
await self.listeners[event.__class__](event, self)
|
||||
@@ -694,7 +696,8 @@ class WeChatPadAdapter(adapter.MessagePlatformAdapter):
|
||||
|
||||
self.bot = WeChatPadClient(
|
||||
self.config['wechatpad_url'],
|
||||
self.config["token"]
|
||||
self.config["token"],
|
||||
logger=self.logger
|
||||
)
|
||||
self.ap.logger.info(self.config["token"])
|
||||
thread_1 = threading.Event()
|
||||
|
||||
@@ -14,6 +14,7 @@ from ...core import app
|
||||
from ..types import entities as platform_entities
|
||||
from ...command.errors import ParamNotEnoughError
|
||||
from ...utils import image
|
||||
from ..logger import EventLogger
|
||||
|
||||
|
||||
class WecomMessageConverter(adapter.MessageConverter):
|
||||
@@ -134,10 +135,10 @@ class WecomAdapter(adapter.MessagePlatformAdapter):
|
||||
event_converter: WecomEventConverter = WecomEventConverter()
|
||||
config: dict
|
||||
|
||||
def __init__(self, config: dict, ap: app.Application):
|
||||
def __init__(self, config: dict, ap: app.Application, logger: EventLogger):
|
||||
self.config = config
|
||||
|
||||
self.ap = ap
|
||||
self.logger = logger
|
||||
|
||||
required_keys = [
|
||||
'corpid',
|
||||
@@ -156,6 +157,7 @@ class WecomAdapter(adapter.MessagePlatformAdapter):
|
||||
token=config['token'],
|
||||
EncodingAESKey=config['EncodingAESKey'],
|
||||
contacts_secret=config['contacts_secret'],
|
||||
logger=self.logger
|
||||
)
|
||||
|
||||
async def reply_message(
|
||||
@@ -199,8 +201,8 @@ class WecomAdapter(adapter.MessagePlatformAdapter):
|
||||
self.bot_account_id = event.receiver_id
|
||||
try:
|
||||
return await callback(await self.event_converter.target2yiri(event), self)
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
except Exception as e:
|
||||
await self.logger.error(f"Error in wecom callback: {traceback.format_exc()}")
|
||||
|
||||
if event_type == platform_events.FriendMessage:
|
||||
self.bot.on_message('text')(on_message)
|
||||
|
||||
@@ -13,6 +13,7 @@ from pkg.core import app
|
||||
from .. import adapter
|
||||
from ..types import entities as platform_entities
|
||||
from ...command.errors import ParamNotEnoughError
|
||||
from ..logger import EventLogger
|
||||
|
||||
|
||||
class WecomMessageConverter(adapter.MessageConverter):
|
||||
@@ -124,10 +125,10 @@ class WecomCSAdapter(adapter.MessagePlatformAdapter):
|
||||
event_converter: WecomEventConverter = WecomEventConverter()
|
||||
config: dict
|
||||
|
||||
def __init__(self, config: dict, ap: app.Application):
|
||||
def __init__(self, config: dict, ap: app.Application, logger: EventLogger):
|
||||
self.config = config
|
||||
|
||||
self.ap = ap
|
||||
self.logger = logger
|
||||
|
||||
required_keys = [
|
||||
'corpid',
|
||||
@@ -144,6 +145,7 @@ class WecomCSAdapter(adapter.MessagePlatformAdapter):
|
||||
secret=config['secret'],
|
||||
token=config['token'],
|
||||
EncodingAESKey=config['EncodingAESKey'],
|
||||
logger=self.logger
|
||||
)
|
||||
|
||||
async def reply_message(
|
||||
@@ -176,8 +178,8 @@ class WecomCSAdapter(adapter.MessagePlatformAdapter):
|
||||
self.bot_account_id = event.receiver_id
|
||||
try:
|
||||
return await callback(await self.event_converter.target2yiri(event), self)
|
||||
except:
|
||||
traceback.print_exc()
|
||||
except Exception as e:
|
||||
await self.logger.error(f"Error in wecomcs callback: {traceback.format_exc()}")
|
||||
|
||||
if event_type == platform_events.FriendMessage:
|
||||
self.bot.on_message('text')(on_message)
|
||||
|
||||
@@ -3,7 +3,10 @@ import logging
|
||||
import typing
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
import base64
|
||||
|
||||
import aiofiles
|
||||
import httpx
|
||||
import pydantic.v1 as pydantic
|
||||
|
||||
from . import entities as platform_entities
|
||||
@@ -552,52 +555,29 @@ class Image(MessageComponent):
|
||||
image_id = image_id[1:]
|
||||
return image_id
|
||||
|
||||
async def download(
|
||||
self,
|
||||
filename: typing.Union[str, Path, None] = None,
|
||||
directory: typing.Union[str, Path, None] = None,
|
||||
determine_type: bool = True,
|
||||
):
|
||||
"""下载图片到本地。
|
||||
async def get_bytes(self) -> typing.Tuple[bytes, str]:
|
||||
"""获取图片的 bytes 和 mime type"""
|
||||
if self.url:
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get(self.url)
|
||||
response.raise_for_status()
|
||||
return response.content, response.headers.get('Content-Type')
|
||||
elif self.base64:
|
||||
mime_type = 'image/jpeg'
|
||||
|
||||
Args:
|
||||
filename: 下载到本地的文件路径。与 `directory` 二选一。
|
||||
directory: 下载到本地的文件夹路径。与 `filename` 二选一。
|
||||
determine_type: 是否自动根据图片类型确定拓展名,默认为 True。
|
||||
"""
|
||||
if not self.url:
|
||||
logger.warning(f'图片 `{self.uuid}` 无 url 参数,下载失败。')
|
||||
return
|
||||
split_index = self.base64.find(';base64,')
|
||||
if split_index == -1:
|
||||
raise ValueError('Invalid base64 string')
|
||||
|
||||
import httpx
|
||||
mime_type = self.base64[5:split_index]
|
||||
base64_data = self.base64[split_index + 8 :]
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get(self.url)
|
||||
response.raise_for_status()
|
||||
content = response.content
|
||||
|
||||
if filename:
|
||||
path = Path(filename)
|
||||
if determine_type:
|
||||
import imghdr
|
||||
|
||||
path = path.with_suffix('.' + str(imghdr.what(None, content)))
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
elif directory:
|
||||
import imghdr
|
||||
|
||||
path = Path(directory)
|
||||
path.mkdir(parents=True, exist_ok=True)
|
||||
path = path / f'{self.uuid}.{imghdr.what(None, content)}'
|
||||
else:
|
||||
raise ValueError('请指定文件路径或文件夹路径!')
|
||||
|
||||
import aiofiles
|
||||
|
||||
async with aiofiles.open(path, 'wb') as f:
|
||||
await f.write(content)
|
||||
|
||||
return path
|
||||
return base64.b64decode(base64_data), mime_type
|
||||
elif self.path:
|
||||
async with aiofiles.open(self.path, 'rb') as f:
|
||||
return await f.read(), 'image/jpeg'
|
||||
else:
|
||||
raise ValueError('Can not get bytes from image')
|
||||
|
||||
@classmethod
|
||||
async def from_local(
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
from ..core import app
|
||||
from . import provider
|
||||
from .providers import localstorage
|
||||
|
||||
|
||||
class StorageMgr:
|
||||
"""存储管理器"""
|
||||
|
||||
ap: app.Application
|
||||
|
||||
storage_provider: provider.StorageProvider
|
||||
|
||||
def __init__(self, ap: app.Application):
|
||||
self.ap = ap
|
||||
self.storage_provider = localstorage.LocalStorageProvider(ap)
|
||||
|
||||
async def initialize(self):
|
||||
await self.storage_provider.initialize()
|
||||
@@ -0,0 +1,44 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import abc
|
||||
|
||||
from ..core import app
|
||||
|
||||
|
||||
class StorageProvider(abc.ABC):
|
||||
ap: app.Application
|
||||
|
||||
def __init__(self, ap: app.Application):
|
||||
self.ap = ap
|
||||
|
||||
async def initialize(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
async def save(
|
||||
self,
|
||||
key: str,
|
||||
value: bytes,
|
||||
):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
async def load(
|
||||
self,
|
||||
key: str,
|
||||
) -> bytes:
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
async def exists(
|
||||
self,
|
||||
key: str,
|
||||
) -> bool:
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
async def delete(
|
||||
self,
|
||||
key: str,
|
||||
):
|
||||
pass
|
||||
@@ -0,0 +1,45 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import aiofiles
|
||||
|
||||
from ...core import app
|
||||
|
||||
from .. import provider
|
||||
|
||||
|
||||
LOCAL_STORAGE_PATH = os.path.join('data', 'storage')
|
||||
|
||||
|
||||
class LocalStorageProvider(provider.StorageProvider):
|
||||
def __init__(self, ap: app.Application):
|
||||
super().__init__(ap)
|
||||
if not os.path.exists(LOCAL_STORAGE_PATH):
|
||||
os.makedirs(LOCAL_STORAGE_PATH)
|
||||
|
||||
async def save(
|
||||
self,
|
||||
key: str,
|
||||
value: bytes,
|
||||
):
|
||||
async with aiofiles.open(os.path.join(LOCAL_STORAGE_PATH, f'{key}'), 'wb') as f:
|
||||
await f.write(value)
|
||||
|
||||
async def load(
|
||||
self,
|
||||
key: str,
|
||||
) -> bytes:
|
||||
async with aiofiles.open(os.path.join(LOCAL_STORAGE_PATH, f'{key}'), 'rb') as f:
|
||||
return await f.read()
|
||||
|
||||
async def exists(
|
||||
self,
|
||||
key: str,
|
||||
) -> bool:
|
||||
return os.path.exists(os.path.join(LOCAL_STORAGE_PATH, f'{key}'))
|
||||
|
||||
async def delete(
|
||||
self,
|
||||
key: str,
|
||||
):
|
||||
os.remove(os.path.join(LOCAL_STORAGE_PATH, f'{key}'))
|
||||
@@ -1,8 +1,9 @@
|
||||
import re
|
||||
import inspect
|
||||
import typing
|
||||
|
||||
|
||||
def get_func_schema(function: callable) -> dict:
|
||||
def get_func_schema(function: typing.Callable) -> dict:
|
||||
"""
|
||||
Return the data schema of a function.
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user