Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c8518ce827 | |||
| 94434e3fc0 | |||
| 9f3af95198 | |||
| acb3af8ab8 |
@@ -0,0 +1,24 @@
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
name: Auto Release
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Fetch Changelog
|
||||
run: |
|
||||
echo "changelog=changelogs/${{github.ref_name}}.md" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Create Release
|
||||
uses: ncipollo/release-action@v1
|
||||
with:
|
||||
bodyFile: ${{ env.changelog }}
|
||||
@@ -1,4 +1,5 @@
|
||||
import traceback
|
||||
import inspect
|
||||
from typing import Union, AsyncGenerator
|
||||
from ...context import PipelineContext
|
||||
from ..stage import Stage
|
||||
@@ -63,23 +64,46 @@ class LLMRequestSubStage(Stage):
|
||||
|
||||
star_cls_obj = star_map.get(func_tool.module_name).star_cls
|
||||
# 判断 handler 是否是类方法(通过装饰器注册的没有 __self__ 属性)
|
||||
ready_to_call = None
|
||||
if hasattr(func_tool.func_obj, '__self__'):
|
||||
# 猜测没有通过装饰器去注册
|
||||
try:
|
||||
ret = await func_tool.func_obj(event, **func_tool_args)
|
||||
ready_to_call = func_tool.func_obj(event, **func_tool_args)
|
||||
except TypeError:
|
||||
# 向下兼容
|
||||
ret = await func_tool.func_obj(event, self.ctx.plugin_manager.context, **func_tool_args)
|
||||
ready_to_call = func_tool.func_obj(event, self.ctx.plugin_manager.context, **func_tool_args)
|
||||
else:
|
||||
ret = await func_tool.func_obj(star_cls_obj, event, **func_tool_args)
|
||||
|
||||
if ret:
|
||||
assert isinstance(ret, (MessageEventResult, CommandResult)), "如果有返回值,必须是 MessageEventResult 或 CommandResult 类型。"
|
||||
event.stop_event()
|
||||
event.set_result(ret)
|
||||
# 执行后续步骤来发送消息
|
||||
yield
|
||||
event.clear_result() # 清除上一个 func tool 的结果
|
||||
ready_to_call = func_tool.func_obj(star_cls_obj, event, **func_tool_args)
|
||||
if isinstance(ready_to_call, AsyncGenerator):
|
||||
async for mer in ready_to_call:
|
||||
# 如果处理函数是生成器,返回值只能是 MessageEventResult 或者 None(无返回值)
|
||||
if mer:
|
||||
assert isinstance(mer, (MessageEventResult, CommandResult)), "如果有返回值,必须是 MessageEventResult 或 CommandResult 类型。"
|
||||
event.set_result(mer)
|
||||
yield
|
||||
else:
|
||||
if event.get_result():
|
||||
yield
|
||||
elif inspect.iscoroutine(ready_to_call):
|
||||
# 如果只是一个 coroutine
|
||||
ret = await ready_to_call
|
||||
if ret:
|
||||
# 如果有返回值
|
||||
assert isinstance(ret, (MessageEventResult, CommandResult)), "如果有返回值,必须是 MessageEventResult 或 CommandResult 类型。"
|
||||
event.set_result(ret)
|
||||
# 执行后续步骤来发送消息
|
||||
if event.is_stopped() and event.get_result():
|
||||
# 主动停止事件传播,并且有结果
|
||||
event.continue_event()
|
||||
yield
|
||||
event.clear_result()
|
||||
event.stop_event()
|
||||
yield
|
||||
elif not event.is_stopped and not event.get_result():
|
||||
continue
|
||||
else:
|
||||
yield
|
||||
event.clear_result() # 清除上一个 handler 的结果
|
||||
|
||||
except BaseException:
|
||||
logger.error(traceback.format_exc())
|
||||
|
||||
@@ -196,7 +196,7 @@ class Context:
|
||||
except BaseException as e:
|
||||
raise ValueError("不合法的 session 字符串: " + str(e))
|
||||
|
||||
for platform in self.registered_platforms:
|
||||
for platform in self.platform_manager.platform_insts:
|
||||
if platform.meta().name == session.platform_name:
|
||||
await platform.send_by_session(session, message_chain)
|
||||
return True
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
# What's Changed
|
||||
(Pre release)
|
||||
1. 使用事件队列和事件总线解耦消息平台适配器与事件处理的逻辑;
|
||||
2. 替换为采用装饰器的 handler 注册风格,支持通过消息事件类型、适配器类型、正则表达式、指令名开头、指令组注册 handler (指令或者监听器),不再推荐通过函数的 handler 注册方式。
|
||||
3. 解耦了事件处理时的固定逻辑,采用流水线代替。
|
||||
4. 解耦了 Provider 的相关处理逻辑。
|
||||
5. 解耦了 Platform 相关处理逻辑。
|
||||
6. aiocqhttp 适配器支持设置群聊白名单、私聊白名单;
|
||||
7. aiocqhttp 适配器将图片转换成 base64 格式上报,而不需要先上传到图床;https://github.com/Soulter/AstrBot/issues/219
|
||||
8. qq_official 适配器在群聊/ C2C 场景下以 base64 格式直接上传到 QQ 服务器,而不需要先上传到图床;
|
||||
9. 移除了对 nakuru 适配器的支持;
|
||||
10. 移除了 update, reboot 等指令;
|
||||
11. 支持使用插件仓库镜像源安装插件、更新项目;
|
||||
12. 支持接入微信
|
||||
13. 移除了内嵌的管理面板构建文件。
|
||||
14. 移除了 nakuru-project 库以适应 Pydantic V2(但仍然保留其对 OneBot 数据结构的封装文件),使用 OneBot 连接到 QQ 请使用 aiocqhttp 适配器(仅支持反向 Websockets,即 AstrBot 做 Websockets 服务器端)
|
||||
15. 新的文档
|
||||
|
||||
> 不向后兼容配置文件。
|
||||
@@ -0,0 +1,4 @@
|
||||
# What's Changed
|
||||
1. Fix websearcher
|
||||
2. Fix context send_message()
|
||||
3. Add reminder llm tool
|
||||
@@ -0,0 +1,116 @@
|
||||
import os
|
||||
import json
|
||||
import datetime
|
||||
import astrbot.api.star as star
|
||||
from astrbot.api.event import filter
|
||||
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
||||
from astrbot.api.event import AstrMessageEvent, MessageEventResult
|
||||
from astrbot.api import llm_tool, logger
|
||||
|
||||
@star.register(name="astrbot-reminder", desc="使用 LLM 待办提醒", author="Soulter", version="0.0.1")
|
||||
class Main(star.Star):
|
||||
'''使用 LLM 待办提醒。只需对 LLM 说想要提醒的事情和时间即可。比如:`之后每天这个时候都提醒我做多邻国`'''
|
||||
def __init__(self, context: star.Context) -> None:
|
||||
self.context = context
|
||||
self.scheduler = AsyncIOScheduler()
|
||||
|
||||
# set and load config
|
||||
if not os.path.exists("data/astrbot-reminder.json"):
|
||||
with open("data/astrbot-reminder.json", "w") as f:
|
||||
f.write("{}")
|
||||
with open("data/astrbot-reminder.json", "r") as f:
|
||||
self.reminder_data = json.load(f)
|
||||
|
||||
self.scheduler.start()
|
||||
|
||||
async def _init_scheduler(self):
|
||||
'''Initialize the scheduler.'''
|
||||
for group in self.reminder_data:
|
||||
for reminder in self.reminder_data[group]:
|
||||
self.scheduler.add_job(self._reminder_callback, 'cron', args=[reminder["text"]], id=group, trigger=reminder["cron"])
|
||||
|
||||
async def _save_data(self):
|
||||
'''Save the reminder data.'''
|
||||
with open("data/astrbot-reminder.json", "w") as f:
|
||||
json.dump(self.reminder_data, f, ensure_ascii=False)
|
||||
|
||||
def _parse_cron_expr(self, cron_expr: str):
|
||||
fields = cron_expr.split(" ")
|
||||
return {
|
||||
"minute": fields[0],
|
||||
"hour": fields[1],
|
||||
"day": fields[2],
|
||||
"month": fields[3],
|
||||
"day_of_week": fields[4],
|
||||
}
|
||||
|
||||
@llm_tool("reminder")
|
||||
async def reminder_tool(self, event: AstrMessageEvent, text: str, datetime_str: str = None, cron_expression: str = None, human_readable_cron: str = None):
|
||||
'''Call this function when user ask for setting a reminder.
|
||||
|
||||
Args:
|
||||
text(string): The content of the reminder.
|
||||
datetime_str(string): Required when user's reminder is a single reminder. The datetime string of the reminder, Must format with %Y-%m-%d %H:%M
|
||||
cron_expression(string): Required when user's reminder is a repeated reminder. The cron expression of the reminder.
|
||||
human_readable_cron(string): Optional. The human readable cron expression of the reminder.
|
||||
'''
|
||||
if event.unified_msg_origin not in self.reminder_data:
|
||||
self.reminder_data[event.unified_msg_origin] = []
|
||||
|
||||
if not cron_expression and not datetime_str:
|
||||
raise ValueError("The cron_expression and datetime_str cannot be both None.")
|
||||
reminder_time = ""
|
||||
if cron_expression:
|
||||
d = { "text": text, "cron": cron_expression, "cron_h": human_readable_cron }
|
||||
self.reminder_data[event.unified_msg_origin].append(d)
|
||||
self.scheduler.add_job(self._reminder_callback, 'cron', **self._parse_cron_expr(cron_expression), args=[event.unified_msg_origin, d], id=event.unified_msg_origin)
|
||||
if human_readable_cron:
|
||||
reminder_time = f"{human_readable_cron}(Cron: {cron_expression})"
|
||||
else:
|
||||
d = { "text": text, "datetime": datetime_str }
|
||||
self.reminder_data[event.unified_msg_origin].append(d)
|
||||
datetime_scheduled = datetime.datetime.strptime(datetime_str, "%Y-%m-%d %H:%M")
|
||||
self.scheduler.add_job(self._reminder_callback, 'date', args=[event.unified_msg_origin, d], id=event.unified_msg_origin, run_date=datetime_scheduled)
|
||||
reminder_time = datetime_str
|
||||
await self._save_data()
|
||||
yield event.plain_result("成功设置待办事项。\n内容: " + text + "\n时间: " + reminder_time + "\n\n使用 /reminder ls 查看所有待办事项。")
|
||||
|
||||
@filter.command_group("reminder")
|
||||
def reminder(self):
|
||||
'''The command group of the reminder.'''
|
||||
pass
|
||||
|
||||
@reminder.command("ls")
|
||||
async def reminder_ls(self, event: AstrMessageEvent):
|
||||
'''List all reminders.'''
|
||||
reminders = self.reminder_data.get(event.unified_msg_origin, [])
|
||||
if not reminders:
|
||||
yield event.plain_result("没有待办事项。")
|
||||
else:
|
||||
reminder_str = "待办事项:\n"
|
||||
for i, reminder in enumerate(reminders):
|
||||
time_ = reminder.get("datetime", "")
|
||||
if not time_:
|
||||
time_ = reminder.get("cron_h", "") + f"(Cron: {reminder.get('cron', "")})"
|
||||
reminder_str += f"{i + 1}. {reminder['text']} - {time_}\n"
|
||||
reminder_str += "\n使用 /reminder rm <index> 删除待办事项。"
|
||||
yield event.plain_result(reminder_str)
|
||||
|
||||
@reminder.command("rm")
|
||||
async def reminder_rm(self, event: AstrMessageEvent, index: int):
|
||||
'''Remove a reminder by index.'''
|
||||
reminders = self.reminder_data.get(event.unified_msg_origin, [])
|
||||
if not reminders:
|
||||
yield event.plain_result("没有待办事项。")
|
||||
elif index < 1 or index > len(reminders):
|
||||
yield event.plain_result("索引越界。")
|
||||
else:
|
||||
reminder = reminders.pop(index - 1)
|
||||
self.scheduler.remove_job(event.unified_msg_origin)
|
||||
await self._save_data()
|
||||
yield event.plain_result("成功删除待办事项:\n" + reminder["text"])
|
||||
|
||||
async def _reminder_callback(self, unified_msg_origin: str, d: dict):
|
||||
'''The callback function of the reminder.'''
|
||||
logger.info(f"Reminder Activated: {d['text']}, created by {unified_msg_origin}")
|
||||
await self.context.send_message(unified_msg_origin, MessageEventResult().message("待办提醒: \n\n" + d['text'] + "\n时间: " + d.get("datetime", "") + d.get("cron_h", "")))
|
||||
@@ -22,7 +22,7 @@ class Main(star.Star):
|
||||
self.sogo_search = Sogo()
|
||||
self.google = Google()
|
||||
|
||||
async def _tidy_text(text: str) -> str:
|
||||
async def _tidy_text(self, text: str) -> str:
|
||||
'''清理文本,去除空格、换行符等'''
|
||||
return text.strip().replace("\n", " ").replace("\r", " ").replace(" ", " ")
|
||||
|
||||
@@ -36,7 +36,7 @@ class Main(star.Star):
|
||||
doc = Document(html)
|
||||
ret = doc.summary(html_partial=True)
|
||||
soup = BeautifulSoup(ret, 'html.parser')
|
||||
ret = self._tidy_text(soup.get_text())
|
||||
ret = await self._tidy_text(soup.get_text())
|
||||
return ret
|
||||
|
||||
async def _request_from_llm(self, event: AstrMessageEvent, resources: str) -> str:
|
||||
@@ -126,4 +126,5 @@ class Main(star.Star):
|
||||
url(string): The url of the website to fetch content from
|
||||
'''
|
||||
resp = await self._get_from_url(url)
|
||||
resp = await self._request_from_llm(event, resp)
|
||||
event.set_result(MessageEventResult().message(resp))
|
||||
Reference in New Issue
Block a user