Skip to content

Commit 4d186ba

Browse files
authored
Merge pull request #1128 from anka-afk/anka-dev
feature: 实现了 #1127 还有 #1133 还有 #1143
2 parents 8499132 + e54eaab commit 4d186ba

8 files changed

Lines changed: 270 additions & 29 deletions

File tree

astrbot/api/star/__init__.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,7 @@
22
register_star as register, # 注册插件(Star)
33
)
44

5-
from astrbot.core.star import Context, Star
5+
from astrbot.core.star import Context, Star, StarTools
66
from astrbot.core.star.config import *
77

8-
__all__ = [
9-
"register",
10-
"Context",
11-
"Star",
12-
]
8+
__all__ = ["register", "Context", "Star", "StarTools"]

astrbot/core/config/default.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@
9898
"plugin_repo_mirror": "",
9999
"knowledge_db": {},
100100
"persona": [],
101+
"timezone": "",
101102
}
102103

103104

@@ -1172,6 +1173,12 @@
11721173
"type": "string",
11731174
"hint": "启用后,会以添加环境变量的方式设置代理。格式为 `http://ip:port`",
11741175
},
1176+
"timezone": {
1177+
"description": "时区",
1178+
"type": "string",
1179+
"obvious_hint": True,
1180+
"hint": "时区设置。请填写 IANA 时区名称, 如 Asia/Shanghai, 为空时使用系统默认时区。所有时区请查看: https://data.iana.org/time-zones/tzdb-2021a/zone1970.tab",
1181+
},
11751182
"log_level": {
11761183
"description": "控制台日志级别",
11771184
"type": "string",

astrbot/core/pipeline/respond/stage.py

Lines changed: 68 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import asyncio
33
import math
44
import traceback
5+
import astrbot.core.message.components as Comp
56
from typing import Union, AsyncGenerator
67
from ..stage import register_stage, Stage
78
from ..context import PipelineContext
@@ -11,11 +12,42 @@
1112
from astrbot.core.message.message_event_result import BaseMessageComponent
1213
from astrbot.core.star.star_handler import star_handlers_registry, EventType
1314
from astrbot.core.star.star import star_map
14-
from astrbot.core.message.components import Plain, Reply, At
1515

1616

1717
@register_stage
1818
class RespondStage(Stage):
19+
# 组件类型到其非空判断函数的映射
20+
_component_validators = {
21+
Comp.Plain: lambda comp: bool(comp.text and comp.text.strip()), # 纯文本消息需要strip
22+
Comp.Face: lambda comp: comp.id is not None, # QQ表情
23+
Comp.Record: lambda comp: bool(comp.file), # 语音
24+
Comp.Video: lambda comp: bool(comp.file), # 视频
25+
Comp.At: lambda comp: bool(comp.qq) or bool(comp.name), # @
26+
Comp.AtAll: lambda comp: True, # @所有人
27+
Comp.RPS: lambda comp: True, # 不知道是啥(未完成)
28+
Comp.Dice: lambda comp: True, # 骰子(未完成)
29+
Comp.Shake: lambda comp: True, # 摇一摇(未完成)
30+
Comp.Anonymous: lambda comp: True, # 匿名(未完成)
31+
Comp.Share: lambda comp: bool(comp.url) and bool(comp.title), # 分享
32+
Comp.Contact: lambda comp: True, # 联系人(未完成)
33+
Comp.Location: lambda comp: bool(comp.lat and comp.lon), # 位置
34+
Comp.Music: lambda comp: bool(comp._type) and bool(comp.url) and bool(comp.audio), # 音乐
35+
Comp.Image: lambda comp: bool(comp.file), # 图片
36+
Comp.Reply: lambda comp: bool(comp.id) and comp.sender_id is not None, # 回复
37+
Comp.RedBag: lambda comp: bool(comp.title), # 红包
38+
Comp.Poke: lambda comp: comp.id != 0 and comp.qq != 0, # 戳一戳
39+
Comp.Forward: lambda comp: bool(comp.id and comp.id.strip()), # 转发
40+
Comp.Node: lambda comp: bool(comp.name) and comp.uin != 0 and bool(comp.content), # 一个转发节点
41+
Comp.Nodes: lambda comp: bool(comp.nodes), # 多个转发节点
42+
Comp.Xml: lambda comp: bool(comp.data and comp.data.strip()), # XML
43+
Comp.Json: lambda comp: bool(comp.data), # JSON
44+
Comp.CardImage: lambda comp: bool(comp.file), # 卡片图片
45+
Comp.TTS: lambda comp: bool(comp.text and comp.text.strip()), # 语音合成
46+
Comp.Unknown: lambda comp: bool(comp.text and comp.text.strip()), # 未知消息
47+
Comp.File: lambda comp: bool(comp.file), # 文件
48+
Comp.WechatEmoji: lambda comp: bool(comp.md5), # 微信表情
49+
}
50+
1951
async def initialize(self, ctx: PipelineContext):
2052
self.ctx = ctx
2153

@@ -62,7 +94,7 @@ async def _word_cnt(self, text: str) -> int:
6294
async def _calc_comp_interval(self, comp: BaseMessageComponent) -> float:
6395
"""分段回复 计算间隔时间"""
6496
if self.interval_method == "log":
65-
if isinstance(comp, Plain):
97+
if isinstance(comp, Comp.Plain):
6698
wc = await self._word_cnt(comp.text)
6799
i = math.log(wc + 1, self.log_base)
68100
return random.uniform(i, i + 0.5)
@@ -72,6 +104,28 @@ async def _calc_comp_interval(self, comp: BaseMessageComponent) -> float:
72104
# random
73105
return random.uniform(self.interval[0], self.interval[1])
74106

107+
async def _is_empty_message_chain(self, chain: list[BaseMessageComponent]):
108+
"""检查消息链是否为空
109+
110+
Args:
111+
chain (list[BaseMessageComponent]): 包含消息对象的列表
112+
"""
113+
if not chain:
114+
return True
115+
116+
for comp in chain:
117+
comp_type = type(comp)
118+
119+
# 检查组件类型是否在字典中
120+
if comp_type in self._component_validators:
121+
if self._component_validators[comp_type](comp):
122+
return False
123+
else:
124+
logger.info(f"空内容检查: 无法识别的组件类型: {comp_type.__name__}")
125+
126+
# 如果所有组件都为空
127+
return True
128+
75129
async def process(
76130
self, event: AstrMessageEvent
77131
) -> Union[None, AsyncGenerator[None, None]]:
@@ -82,20 +136,30 @@ async def process(
82136
if len(result.chain) > 0:
83137
await event._pre_send()
84138

139+
# 检查消息链是否为空
140+
try:
141+
if await self._is_empty_message_chain(result.chain):
142+
logger.info("消息为空,跳过发送阶段")
143+
event.clear_result()
144+
event.stop_event()
145+
return
146+
except Exception as e:
147+
logger.warning(f"空内容检查异常: {e}")
148+
85149
if self.enable_seg and (
86150
(self.only_llm_result and result.is_llm_result())
87151
or not self.only_llm_result
88152
):
89153
decorated_comps = []
90154
if self.reply_with_mention:
91155
for comp in result.chain:
92-
if isinstance(comp, At):
156+
if isinstance(comp, Comp.At):
93157
decorated_comps.append(comp)
94158
result.chain.remove(comp)
95159
break
96160
if self.reply_with_quote:
97161
for comp in result.chain:
98-
if isinstance(comp, Reply):
162+
if isinstance(comp, Comp.Reply):
99163
decorated_comps.append(comp)
100164
result.chain.remove(comp)
101165
break

astrbot/core/provider/sources/gemini_source.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ async def _query(self, payloads: dict, tools: FuncCall) -> LLMResponse:
147147
if message["role"] == "user":
148148
if isinstance(message["content"], str):
149149
if not message["content"]:
150-
message["content"] = "<empty_content>"
150+
message["content"] = ""
151151

152152
google_genai_conversation.append(
153153
{"role": "user", "parts": [{"text": message["content"]}]}
@@ -158,7 +158,7 @@ async def _query(self, payloads: dict, tools: FuncCall) -> LLMResponse:
158158
for part in message["content"]:
159159
if part["type"] == "text":
160160
if not part["text"]:
161-
part["text"] = "<empty_content>"
161+
part["text"] = ""
162162
parts.append({"text": part["text"]})
163163
elif part["type"] == "image_url":
164164
parts.append(
@@ -176,7 +176,7 @@ async def _query(self, payloads: dict, tools: FuncCall) -> LLMResponse:
176176
elif message["role"] == "assistant":
177177
if "content" in message:
178178
if not message["content"]:
179-
message["content"] = "<empty_content>"
179+
message["content"] = ""
180180
google_genai_conversation.append(
181181
{"role": "model", "parts": [{"text": message["content"]}]}
182182
)

astrbot/core/star/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@
44
from astrbot.core.provider import Provider
55
from astrbot.core.utils.command_parser import CommandParserMixin
66
from astrbot.core import html_renderer
7+
from astrbot.core.star.star_tools import StarTools
78

89

910
class Star(CommandParserMixin):
1011
"""所有插件(Star)的父类,所有插件都应该继承于这个类"""
1112

1213
def __init__(self, context: Context):
14+
StarTools.initialize(context)
1315
self.context = context
1416

1517
async def text_to_image(self, text: str, return_url=True) -> str:
@@ -27,4 +29,4 @@ async def terminate(self):
2729
pass
2830

2931

30-
__all__ = ["Star", "StarMetadata", "PluginManager", "Context", "Provider"]
32+
__all__ = ["Star", "StarMetadata", "PluginManager", "Context", "Provider", "StarTools"]

astrbot/core/star/star_tools.py

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
from typing import Union, Awaitable, List, Optional, ClassVar
2+
from astrbot.core.message.components import BaseMessageComponent
3+
from astrbot.core.message.message_event_result import MessageChain
4+
from astrbot.api.platform import MessageMember, AstrBotMessage
5+
from astrbot.core.platform.astr_message_event import MessageSesion
6+
from astrbot.core.star.context import Context
7+
8+
9+
class StarTools:
10+
"""
11+
提供给插件使用的便捷工具函数集合
12+
这些方法封装了一些常用操作,使插件开发更加简单便捷!
13+
"""
14+
15+
_context: ClassVar[Optional[Context]] = None
16+
17+
@classmethod
18+
def initialize(cls, context: Context) -> None:
19+
"""
20+
初始化StarTools,设置context引用
21+
22+
Args:
23+
context: 暴露给插件的上下文
24+
"""
25+
cls._context = context
26+
27+
@classmethod
28+
async def send_message(
29+
cls, session: Union[str, MessageSesion], message_chain: MessageChain
30+
) -> bool:
31+
"""
32+
根据session(unified_msg_origin)主动发送消息
33+
34+
Args:
35+
session: 消息会话。通过event.session或者event.unified_msg_origin获取
36+
message_chain: 消息链
37+
38+
Returns:
39+
bool: 是否找到匹配的平台
40+
41+
Raises:
42+
ValueError: 当session为字符串且解析失败时抛出
43+
44+
Note:
45+
qq_official(QQ官方API平台)不支持此方法
46+
"""
47+
return await cls._context.send_message(session, message_chain)
48+
49+
@classmethod
50+
async def create_message(
51+
cls,
52+
type: str,
53+
self_id: str,
54+
session_id: str,
55+
message_id: str,
56+
sender: MessageMember,
57+
message: List[BaseMessageComponent],
58+
message_str: str,
59+
raw_message: object,
60+
group_id: str = "",
61+
):
62+
"""
63+
创建一个AstrBot消息对象
64+
65+
Args:
66+
type (str): 消息类型
67+
self_id (str): 机器人自身ID
68+
session_id (str): 会话ID(通常为用户ID)(QQ号, 群号等)
69+
message_id (str): 消息ID
70+
sender (MessageMember): 发送者信息
71+
message (List[BaseMessageComponent]): 消息组件列表
72+
message_str (str): 消息字符串
73+
raw_message (object): 原始消息对象
74+
group_id (str, optional): 群组ID, 如果为私聊则为空. Defaults to "".
75+
76+
Returns:
77+
AstrBotMessage: 创建的消息对象
78+
"""
79+
abm = AstrBotMessage()
80+
abm.type = type
81+
abm.self_id = self_id
82+
abm.session_id = session_id
83+
abm.message_id = message_id
84+
abm.sender = sender
85+
abm.message = message
86+
abm.message_str = message_str
87+
abm.raw_message = raw_message
88+
abm.group_id = group_id
89+
return abm
90+
91+
# todo: 添加构造事件的方法
92+
# async def create_event(
93+
# self, platform: str, umo: str, sender_id: str, session_id: str
94+
# ):
95+
# platform = self._context.get_platform(platform)
96+
97+
# todo: 添加找到对应平台并提交对应事件的方法
98+
99+
@classmethod
100+
def activate_llm_tool(cls, name: str) -> bool:
101+
"""
102+
激活一个已经注册的函数调用工具
103+
注册的工具默认是激活状态
104+
105+
Args:
106+
name (str): 工具名称
107+
"""
108+
return cls._context.activate_llm_tool(name)
109+
110+
@classmethod
111+
def deactivate_llm_tool(cls, name: str) -> bool:
112+
"""
113+
停用一个已经注册的函数调用工具
114+
115+
Args:
116+
name (str): 工具名称
117+
"""
118+
return cls._context.deactivate_llm_tool(name)
119+
120+
@classmethod
121+
def register_llm_tool(
122+
cls, name: str, func_args: list, desc: str, func_obj: Awaitable
123+
) -> None:
124+
"""
125+
为函数调用(function-calling/tools-use)添加工具
126+
127+
Args:
128+
name (str): 工具名称
129+
func_args (list): 函数参数列表
130+
desc (str): 工具描述
131+
func_obj (Awaitable): 函数对象,必须是异步函数
132+
"""
133+
cls._context.register_llm_tool(name, func_args, desc, func_obj)
134+
135+
@classmethod
136+
def unregister_llm_tool(cls, name: str) -> None:
137+
"""
138+
删除一个函数调用工具
139+
如果再要启用,需要重新注册
140+
141+
Args:
142+
name (str): 工具名称
143+
"""
144+
cls._context.unregister_llm_tool(name)

0 commit comments

Comments
 (0)