|
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)) |
Description / 描述
为
AstrMessageEvent.send()方法新增一个可选的参数decoration,用于控制是否为通过该方法发送的消息添加消息装饰,以使其支持 AstrBot 的回复文本前缀、At 发送人、引用回复等等一大堆功能。个人认为可以把这段逻辑单独提取出来为一个独立函数:
AstrBot/astrbot/core/pipeline/result_decorate/stage.py
Lines 196 to 428 in d0e5e68
如果
decoration为 True 就调用这个工具函数装饰一遍消息。Use Case / 使用场景
目前有许多位置无法使用
yield event.chain_result(),例如 Event Hook、会话控制,但我确实希望插件返回的消息能走一遍装饰。此 issue 的目的就是能在无法使用yield的上下文中使event.send()也能遵循用户在 AstrBot 中配置的回复装饰规则。Willing to Submit PR? / 是否愿意提交PR?
Code of Conduct