Skip to content

[Feature] 为 event.send() 增加可选的消息装饰开关 #9038

Description

@Hiraeth-Wave

Description / 描述

AstrMessageEvent.send() 方法新增一个可选的参数 decoration,用于控制是否为通过该方法发送的消息添加消息装饰,以使其支持 AstrBot 的回复文本前缀、At 发送人、引用回复等等一大堆功能。

个人认为可以把这段逻辑单独提取出来为一个独立函数:

if len(result.chain) > 0:
# 回复前缀
if self.reply_prefix:
for comp in result.chain:
if isinstance(comp, Plain):
comp.text = self.reply_prefix + comp.text
break
# 分段回复
if self.enable_segmented_reply and event.get_platform_name() not in [
"qq_official_webhook",
"weixin_official_account",
"dingtalk",
]:
if (
self.only_llm_result and result.is_model_result()
) or not self.only_llm_result:
new_chain = []
for comp in result.chain:
if isinstance(comp, Plain):
if len(comp.text) > self.words_count_threshold:
# 不分段回复
new_chain.append(comp)
continue
# 根据 split_mode 选择分段方式
if self.split_mode == "words":
split_response = self._split_text_by_words(comp.text)
else: # regex 模式
try:
split_response = re.findall(
self.regex,
comp.text,
re.DOTALL | re.MULTILINE,
)
except re.error:
logger.error(
f"分段回复正则表达式错误,使用默认分段方式: {traceback.format_exc()}",
)
split_response = re.findall(
r".*?[。?!~…]+|.+$",
comp.text,
re.DOTALL | re.MULTILINE,
)
if not split_response:
new_chain.append(comp)
continue
for seg in split_response:
if self.content_cleanup_rule:
try:
seg = re.sub(self.content_cleanup_rule, "", seg)
except re.error:
logger.error(
f"分段回复过滤表达式失败,无法成功过滤:{traceback.format_exc()}"
)
self.content_cleanup_rule = None
seg = seg.strip()
if seg:
new_chain.append(Plain(seg))
else:
# 非 Plain 类型的消息段不分段
new_chain.append(comp)
result.chain = new_chain
# TTS
tts_provider = self.ctx.plugin_manager.context.get_using_tts_provider(
event.unified_msg_origin,
)
should_tts = (
bool(self.ctx.astrbot_config["provider_tts_settings"]["enable"])
and result.is_llm_result()
and await SessionServiceManager.should_process_tts_request(event)
and random.random() <= self.tts_trigger_probability
and tts_provider
)
if should_tts and not tts_provider:
logger.warning(
f"会话 {event.unified_msg_origin} 未配置文本转语音模型。",
)
if (
not should_tts
and self.show_reasoning
and event.get_extra("_llm_reasoning_content")
):
# inject reasoning content to chain
reasoning_content = str(event.get_extra("_llm_reasoning_content"))
if event.get_platform_name() == "lark":
result.chain.insert(
0,
Json(
data={
"type": "lark_collapsible_panel_reasoning",
"title": "💭 Thinking",
"expanded": False,
"content": reasoning_content,
},
),
)
else:
result.chain.insert(
0, Plain(f"🤔 思考: {reasoning_content}\n\n────\n")
)
if should_tts and tts_provider:
new_chain = []
for comp in result.chain:
if isinstance(comp, Plain) and len(comp.text) > 1:
try:
logger.info(f"TTS 请求: {comp.text}")
audio_path = await tts_provider.get_audio(comp.text)
logger.info(f"TTS 结果: {audio_path}")
if not audio_path:
logger.error(
f"由于 TTS 音频文件未找到,消息段转语音失败: {comp.text}",
)
new_chain.append(comp)
continue
event.track_temporary_local_file(audio_path)
use_file_service = self.ctx.astrbot_config[
"provider_tts_settings"
]["use_file_service"]
callback_api_base = self.ctx.astrbot_config[
"callback_api_base"
]
dual_output = self.ctx.astrbot_config[
"provider_tts_settings"
]["dual_output"]
url = None
if use_file_service and callback_api_base:
token = await file_token_service.register_file(
audio_path,
)
url = f"{callback_api_base}/api/file/{token}"
logger.debug(f"已注册:{url}")
new_chain.append(
Record(
file=url or audio_path,
url=url or audio_path,
text=comp.text,
),
)
if dual_output:
new_chain.append(comp)
except Exception:
logger.error(traceback.format_exc())
logger.error("TTS 失败,使用文本发送。")
new_chain.append(comp)
else:
new_chain.append(comp)
result.chain = new_chain
# 文本转图片
elif (
result.use_t2i_ is None and self.ctx.astrbot_config["t2i"]
) or result.use_t2i_:
parts = []
for comp in result.chain:
if isinstance(comp, Plain):
parts.append("\n\n" + comp.text)
else:
break
plain_str = "".join(parts)
if plain_str and len(plain_str) > self.t2i_word_threshold:
render_start = time.time()
try:
url = await html_renderer.render_t2i(
plain_str,
return_url=True,
use_network=self.t2i_use_network,
template_name=self.t2i_active_template,
)
except BaseException:
logger.error("文本转图片失败,使用文本发送。")
return
if time.time() - render_start > 3:
logger.warning(
"文本转图片耗时超过了 3 秒,如果觉得很慢可以在 WebUI 中关闭文本转图片模式。",
)
if url:
if url.startswith("http"):
result.chain = [Image.fromURL(url)]
elif (
self.ctx.astrbot_config["t2i_use_file_service"]
and self.ctx.astrbot_config["callback_api_base"]
):
token = await file_token_service.register_file(url)
url = f"{self.ctx.astrbot_config['callback_api_base']}/api/file/{token}"
logger.debug(f"已注册:{url}")
result.chain = [Image.fromURL(url)]
else:
result.chain = [Image.fromFileSystem(url)]
# 触发转发消息
if event.get_platform_name() == "aiocqhttp":
word_cnt = 0
for comp in result.chain:
if isinstance(comp, Plain):
word_cnt += len(comp.text)
if word_cnt > self.forward_threshold:
node = Node(
uin=event.get_self_id(),
name="AstrBot",
content=[*result.chain],
)
result.chain = [node]
# at 回复 / 引用回复仅适用于纯文本或图文消息
can_decorate = all(
isinstance(item, (Plain, Image)) for item in result.chain
)
if can_decorate:
# at 回复
if (
self.reply_with_mention
and event.get_message_type() != MessageType.FRIEND_MESSAGE
):
result.chain.insert(
0,
At(qq=event.get_sender_id(), name=event.get_sender_name()),
)
if len(result.chain) > 1 and isinstance(result.chain[1], Plain):
result.chain[1].text = "\n" + result.chain[1].text
# 引用回复
if self.reply_with_quote:
result.chain.insert(0, Reply(id=event.message_obj.message_id))

如果 decoration 为 True 就调用这个工具函数装饰一遍消息。

Use Case / 使用场景

目前有许多位置无法使用 yield event.chain_result(),例如 Event Hook、会话控制,但我确实希望插件返回的消息能走一遍装饰。此 issue 的目的就是能在无法使用 yield 的上下文中使 event.send() 也能遵循用户在 AstrBot 中配置的回复装饰规则。

Willing to Submit PR? / 是否愿意提交PR?

  • Yes, I am willing to submit a PR. / 是的,我愿意提交 PR。

Code of Conduct

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:coreThe bug / feature is about astrbot's core, backendenhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions