Optimize string concatenation in loops: replace += with list.join() (#3246)

* Initial plan

* Fix string concatenation performance issues in loops

Co-authored-by: LIghtJUNction <106986785+LIghtJUNction@users.noreply.github.com>

* Address code review feedback: Fix plugin list logic and add comment

Co-authored-by: LIghtJUNction <106986785+LIghtJUNction@users.noreply.github.com>

* Improve comment clarity for at_parts accumulation

Co-authored-by: LIghtJUNction <106986785+LIghtJUNction@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: LIghtJUNction <106986785+LIghtJUNction@users.noreply.github.com>
This commit is contained in:
Copilot
2025-11-02 13:00:59 +08:00
committed by GitHub
parent 92abc43c9d
commit e190bbeeed
18 changed files with 151 additions and 117 deletions

View File

@@ -250,14 +250,15 @@ async def migration_persona_data(
try:
begin_dialogs = persona.get("begin_dialogs", [])
mood_imitation_dialogs = persona.get("mood_imitation_dialogs", [])
mood_prompt = ""
parts = []
user_turn = True
for mood_dialog in mood_imitation_dialogs:
if user_turn:
mood_prompt += f"A: {mood_dialog}\n"
parts.append(f"A: {mood_dialog}\n")
else:
mood_prompt += f"B: {mood_dialog}\n"
parts.append(f"B: {mood_dialog}\n")
user_turn = not user_turn
mood_prompt = "".join(parts)
system_prompt = persona.get("prompt", "")
if mood_prompt:
system_prompt += f"Here are few shots of dialogs, you need to imitate the tone of 'B' in the following dialogs to respond:\n {mood_prompt}"

View File

@@ -21,8 +21,9 @@ class BaiduAipStrategy(ContentSafetyStrategy):
if "data" not in res:
return False, ""
count = len(res["data"])
info = f"百度审核服务发现 {count} 处违规:\n"
parts = [f"百度审核服务发现 {count} 处违规:\n"]
for i in res["data"]:
info += f"{i['msg']}\n"
info += "\n判断结果:" + res["conclusion"]
parts.append(f"{i['msg']}\n")
parts.append("\n判断结果:" + res["conclusion"])
info = "".join(parts)
return False, info

View File

@@ -246,12 +246,13 @@ class ResultDecorateStage(Stage):
elif (
result.use_t2i_ is None and self.ctx.astrbot_config["t2i"]
) or result.use_t2i_:
plain_str = ""
parts = []
for comp in result.chain:
if isinstance(comp, Plain):
plain_str += "\n\n" + comp.text
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:

View File

@@ -91,33 +91,34 @@ class AstrMessageEvent(abc.ABC):
return self.message_str
def _outline_chain(self, chain: list[BaseMessageComponent] | None) -> str:
outline = ""
if not chain:
return outline
return ""
parts = []
for i in chain:
if isinstance(i, Plain):
outline += i.text
parts.append(i.text)
elif isinstance(i, Image):
outline += "[图片]"
parts.append("[图片]")
elif isinstance(i, Face):
outline += f"[表情:{i.id}]"
parts.append(f"[表情:{i.id}]")
elif isinstance(i, At):
outline += f"[At:{i.qq}]"
parts.append(f"[At:{i.qq}]")
elif isinstance(i, AtAll):
outline += "[At:全体成员]"
parts.append("[At:全体成员]")
elif isinstance(i, Forward):
# 转发消息
outline += "[转发消息]"
parts.append("[转发消息]")
elif isinstance(i, Reply):
# 引用回复
if i.message_str:
outline += f"[引用消息({i.sender_nickname}: {i.message_str})]"
parts.append(f"[引用消息({i.sender_nickname}: {i.message_str})]")
else:
outline += "[引用消息]"
parts.append("[引用消息]")
else:
outline += f"[{i.type}]"
outline += " "
return outline
parts.append(f"[{i.type}]")
parts.append(" ")
return "".join(parts)
def get_message_outline(self) -> str:
"""获取消息概要。

View File

@@ -315,6 +315,8 @@ class AiocqhttpAdapter(Platform):
abm.message.append(a)
elif t == "at":
first_at_self_processed = False
# Accumulate @ mention text for efficient concatenation
at_parts = []
for m in m_group:
try:
@@ -354,13 +356,15 @@ class AiocqhttpAdapter(Platform):
first_at_self_processed = True
else:
# 非第一个@机器人或@其他用户添加到message_str
message_str += f" @{nickname}({m['data']['qq']}) "
at_parts.append(f" @{nickname}({m['data']['qq']}) ")
else:
abm.message.append(At(qq=str(m["data"]["qq"]), name=""))
except ActionFailed as e:
logger.error(f"获取 @ 用户信息失败: {e},此消息段将被忽略。")
except BaseException as e:
logger.error(f"获取 @ 用户信息失败: {e},此消息段将被忽略。")
message_str += "".join(at_parts)
else:
for m in m_group:
a = ComponentTypes[t](**m["data"])

View File

@@ -113,18 +113,18 @@ class DiscordPlatformEvent(AstrMessageEvent):
message: MessageChain,
) -> tuple[str, list[discord.File], discord.ui.View | None, list[discord.Embed]]:
"""将 MessageChain 解析为 Discord 发送所需的内容"""
content = ""
content_parts = []
files = []
view = None
embeds = []
reference_message_id = None
for i in message.chain: # 遍历消息链
if isinstance(i, Plain): # 如果是文字类型的
content += i.text
content_parts.append(i.text)
elif isinstance(i, Reply):
reference_message_id = i.id
elif isinstance(i, At):
content += f"<@{i.qq}>"
content_parts.append(f"<@{i.qq}>")
elif isinstance(i, Image):
logger.debug(f"[Discord] 开始处理 Image 组件: {i}")
try:
@@ -238,6 +238,7 @@ class DiscordPlatformEvent(AstrMessageEvent):
else:
logger.debug(f"[Discord] 忽略了不支持的消息组件: {i.type}")
content = "".join(content_parts)
if len(content) > 2000:
logger.warning("[Discord] 消息内容超过2000字符将被截断。")
content = content[:2000]

View File

@@ -222,39 +222,41 @@ class SlackAdapter(Platform):
if element.get("type") == "rich_text_section":
# 处理富文本段落
section_elements = element.get("elements", [])
text_content = ""
text_parts = []
for section_element in section_elements:
element_type = section_element.get("type", "")
if element_type == "text":
# 普通文本
text_content += section_element.get("text", "")
text_parts.append(section_element.get("text", ""))
elif element_type == "user":
# @用户提及
user_id = section_element.get("user_id", "")
if user_id:
# 将之前的文本内容先添加到组件中
text_content = "".join(text_parts)
if text_content.strip():
message_components.append(
Plain(text=text_content),
)
text_content = ""
text_parts = []
# 添加@提及组件
message_components.append(At(qq=user_id, name=""))
elif element_type == "channel":
# #频道提及
channel_id = section_element.get("channel_id", "")
text_content += f"#{channel_id}"
text_parts.append(f"#{channel_id}")
elif element_type == "link":
# 链接
url = section_element.get("url", "")
link_text = section_element.get("text", url)
text_content += f"[{link_text}]({url})"
text_parts.append(f"[{link_text}]({url})")
elif element_type == "emoji":
# 表情符号
emoji_name = section_element.get("name", "")
text_content += f":{emoji_name}:"
text_parts.append(f":{emoji_name}:")
text_content = "".join(text_parts)
if text_content.strip():
message_components.append(Plain(text=text_content))

View File

@@ -148,14 +148,15 @@ class SlackMessageEvent(AstrMessageEvent):
)
except Exception:
# 如果块发送失败,尝试只发送文本
fallback_text = ""
parts = []
for segment in message.chain:
if isinstance(segment, Plain):
fallback_text += segment.text
parts.append(segment.text)
elif isinstance(segment, File):
fallback_text += f" [文件: {segment.name}] "
parts.append(f" [文件: {segment.name}] ")
elif isinstance(segment, Image):
fallback_text += " [图片] "
parts.append(" [图片] ")
fallback_text = "".join(parts)
if self.get_group_id():
await self.web_client.chat_postMessage(

View File

@@ -146,14 +146,15 @@ class ProviderDashscope(ProviderOpenAIOfficial):
# RAG 引用脚标格式化
output_text = re.sub(r"<ref>\[(\d+)\]</ref>", r"[\1]", output_text)
if self.output_reference and response.output.get("doc_references", None):
ref_str = ""
ref_parts = []
for ref in response.output.get("doc_references", []) or []:
ref_title = (
ref.get("title", "")
if ref.get("title")
else ref.get("doc_name", "")
)
ref_str += f"{ref['index_id']}. {ref_title}\n"
ref_parts.append(f"{ref['index_id']}. {ref_title}\n")
ref_str = "".join(ref_parts)
output_text += f"\n\n回答来源:\n{ref_str}"
llm_response = LLMResponse("assistant")

View File

@@ -51,15 +51,15 @@ class CommandFilter(HandlerFilter):
self._cmpl_cmd_names: list | None = None
def print_types(self):
result = ""
parts = []
for k, v in self.handler_params.items():
if isinstance(v, type):
result += f"{k}({v.__name__}),"
parts.append(f"{k}({v.__name__}),")
elif isinstance(v, types.UnionType) or typing.get_origin(v) is typing.Union:
result += f"{k}({v}),"
parts.append(f"{k}({v}),")
else:
result += f"{k}({type(v).__name__})={v},"
result = result.rstrip(",")
parts.append(f"{k}({type(v).__name__})={v},")
result = "".join(parts).rstrip(",")
return result
def init_handler_md(self, handle_md: StarHandlerMetadata):

View File

@@ -66,7 +66,7 @@ class CommandGroupFilter(HandlerFilter):
event: AstrMessageEvent | None = None,
cfg: AstrBotConfig | None = None,
) -> str:
result = ""
parts = []
for sub_filter in sub_command_filters:
if isinstance(sub_filter, CommandFilter):
custom_filter_pass = True
@@ -74,31 +74,32 @@ class CommandGroupFilter(HandlerFilter):
custom_filter_pass = sub_filter.custom_filter_ok(event, cfg)
if custom_filter_pass:
cmd_th = sub_filter.print_types()
result += f"{prefix}├── {sub_filter.command_name}"
line = f"{prefix}├── {sub_filter.command_name}"
if cmd_th:
result += f" ({cmd_th})"
line += f" ({cmd_th})"
else:
result += " (无参数指令)"
line += " (无参数指令)"
if sub_filter.handler_md and sub_filter.handler_md.desc:
result += f": {sub_filter.handler_md.desc}"
line += f": {sub_filter.handler_md.desc}"
result += "\n"
parts.append(line + "\n")
elif isinstance(sub_filter, CommandGroupFilter):
custom_filter_pass = True
if event and cfg:
custom_filter_pass = sub_filter.custom_filter_ok(event, cfg)
if custom_filter_pass:
result += f"{prefix}├── {sub_filter.group_name}"
result += "\n"
result += sub_filter.print_cmd_tree(
sub_filter.sub_command_filters,
prefix + "",
event=event,
cfg=cfg,
parts.append(f"{prefix}├── {sub_filter.group_name}\n")
parts.append(
sub_filter.print_cmd_tree(
sub_filter.sub_command_filters,
prefix + "",
event=event,
cfg=cfg,
)
)
return result
return "".join(parts)
def custom_filter_ok(self, event: AstrMessageEvent, cfg: AstrBotConfig) -> bool:
for custom_filter in self.custom_filter_list:

View File

@@ -213,11 +213,12 @@ class AstrBotDashboard:
raise Exception(f"端口 {port} 已被占用")
display = f"\n ✨✨✨\n AstrBot v{VERSION} WebUI 已启动,可访问\n\n"
display += f" ➜ 本地: http://localhost:{port}\n"
parts = [f"\n ✨✨✨\n AstrBot v{VERSION} WebUI 已启动,可访问\n\n"]
parts.append(f" ➜ 本地: http://localhost:{port}\n")
for ip in ip_addr:
display += f" ➜ 网络: http://{ip}:{port}\n"
display += " ➜ 默认用户名和密码: astrbot\n ✨✨✨\n"
parts.append(f" ➜ 网络: http://{ip}:{port}\n")
parts.append(" ➜ 默认用户名和密码: astrbot\n ✨✨✨\n")
display = "".join(parts)
if not ip_addr:
display += (

View File

@@ -134,12 +134,13 @@ class ConversationCommands:
size_per_page,
)
history = ""
parts = []
for context in contexts:
if len(context) > 150:
context = context[:150] + "..."
history += f"{context}\n"
parts.append(f"{context}\n")
history = "".join(parts)
ret = (
f"当前对话历史记录:"
f"{history or '无历史记录'}\n\n"
@@ -154,7 +155,7 @@ class ConversationCommands:
provider = self.context.get_using_provider(message.unified_msg_origin)
if provider and provider.meta().type == "dify":
"""原有的Dify处理逻辑保持不变"""
ret = "Dify 对话列表:\n"
parts = ["Dify 对话列表:\n"]
assert isinstance(provider, ProviderDify)
data = await provider.api_client.get_chat_convs(message.unified_msg_origin)
idx = 1
@@ -162,12 +163,17 @@ class ConversationCommands:
ts_h = datetime.datetime.fromtimestamp(conv["updated_at"]).strftime(
"%m-%d %H:%M",
)
ret += f"{idx}. {conv['name']}({conv['id'][:4]})\n 上次更新:{ts_h}\n"
parts.append(
f"{idx}. {conv['name']}({conv['id'][:4]})\n 上次更新:{ts_h}\n"
)
idx += 1
if idx == 1:
ret += "没有找到任何对话。"
parts.append("没有找到任何对话。")
dify_cid = provider.conversation_ids.get(message.unified_msg_origin, None)
ret += f"\n\n用户: {message.unified_msg_origin}\n当前对话: {dify_cid}\n使用 /switch <序号> 切换对话。"
parts.append(
f"\n\n用户: {message.unified_msg_origin}\n当前对话: {dify_cid}\n使用 /switch <序号> 切换对话。"
)
ret = "".join(parts)
message.set_result(MessageEventResult().message(ret))
return
@@ -185,7 +191,7 @@ class ConversationCommands:
end_idx = start_idx + size_per_page
conversations_paged = conversations_all[start_idx:end_idx]
ret = "对话列表:\n---\n"
parts = ["对话列表:\n---\n"]
"""全局序号从当前页的第一个开始"""
global_index = start_idx + 1
@@ -204,10 +210,13 @@ class ConversationCommands:
)
persona_id = persona["name"]
title = _titles.get(conv.cid, "新对话")
ret += f"{global_index}. {title}({conv.cid[:4]})\n 人格情景: {persona_id}\n 上次更新: {datetime.datetime.fromtimestamp(conv.updated_at).strftime('%m-%d %H:%M')}\n"
parts.append(
f"{global_index}. {title}({conv.cid[:4]})\n 人格情景: {persona_id}\n 上次更新: {datetime.datetime.fromtimestamp(conv.updated_at).strftime('%m-%d %H:%M')}\n"
)
global_index += 1
ret += "---\n"
parts.append("---\n")
ret = "".join(parts)
curr_cid = await self.context.conversation_manager.get_curr_conversation_id(
message.unified_msg_origin,
)

View File

@@ -59,10 +59,11 @@ class PersonaCommands:
.use_t2i(False),
)
elif l[1] == "list":
msg = "人格列表:\n"
parts = ["人格列表:\n"]
for persona in self.context.provider_manager.personas:
msg += f"- {persona['name']}\n"
msg += "\n\n*输入 `/persona view 人格名` 查看人格详细信息"
parts.append(f"- {persona['name']}\n")
parts.append("\n\n*输入 `/persona view 人格名` 查看人格详细信息")
msg = "".join(parts)
message.set_result(MessageEventResult().message(msg))
elif l[1] == "view":
if len(l) == 2:

View File

@@ -13,14 +13,17 @@ class PluginCommands:
async def plugin_ls(self, event: AstrMessageEvent):
"""获取已经安装的插件列表。"""
plugin_list_info = "已加载的插件:\n"
parts = ["已加载的插件:\n"]
for plugin in self.context.get_all_stars():
plugin_list_info += f"- `{plugin.name}` By {plugin.author}: {plugin.desc}"
line = f"- `{plugin.name}` By {plugin.author}: {plugin.desc}"
if not plugin.activated:
plugin_list_info += " (未启用)"
plugin_list_info += "\n"
if plugin_list_info.strip() == "":
line += " (未启用)"
parts.append(line + "\n")
if len(parts) == 1:
plugin_list_info = "没有加载任何插件。"
else:
plugin_list_info = "".join(parts)
plugin_list_info += "\n使用 /plugin help <插件名> 查看插件帮助和加载的指令。\n使用 /plugin on/off <插件名> 启用或者禁用插件。"
event.set_result(
@@ -103,14 +106,14 @@ class PluginCommands:
command_names.append(filter_.group_name)
if len(command_handlers) > 0:
help_msg += "\n\n🔧 指令列表:\n"
parts = ["\n\n🔧 指令列表:\n"]
for i in range(len(command_handlers)):
help_msg += f"- {command_names[i]}"
line = f"- {command_names[i]}"
if command_handlers[i].desc:
help_msg += f": {command_handlers[i].desc}"
help_msg += "\n"
help_msg += "\nTip: 指令的触发需要添加唤醒前缀,默认为 /。"
line += f": {command_handlers[i].desc}"
parts.append(line + "\n")
parts.append("\nTip: 指令的触发需要添加唤醒前缀,默认为 /。")
help_msg += "".join(parts)
ret = f"🧩 插件 {plugin_name} 帮助信息:\n" + help_msg
ret += "更多帮助信息请查看插件仓库 README。"

View File

@@ -19,38 +19,39 @@ class ProviderCommands:
umo = event.unified_msg_origin
if idx is None:
ret = "## 载入的 LLM 提供商\n"
parts = ["## 载入的 LLM 提供商\n"]
for idx, llm in enumerate(self.context.get_all_providers()):
id_ = llm.meta().id
ret += f"{idx + 1}. {id_} ({llm.meta().model})"
line = f"{idx + 1}. {id_} ({llm.meta().model})"
provider_using = self.context.get_using_provider(umo=umo)
if provider_using and provider_using.meta().id == id_:
ret += " (当前使用)"
ret += "\n"
line += " (当前使用)"
parts.append(line + "\n")
tts_providers = self.context.get_all_tts_providers()
if tts_providers:
ret += "\n## 载入的 TTS 提供商\n"
parts.append("\n## 载入的 TTS 提供商\n")
for idx, tts in enumerate(tts_providers):
id_ = tts.meta().id
ret += f"{idx + 1}. {id_}"
line = f"{idx + 1}. {id_}"
tts_using = self.context.get_using_tts_provider(umo=umo)
if tts_using and tts_using.meta().id == id_:
ret += " (当前使用)"
ret += "\n"
line += " (当前使用)"
parts.append(line + "\n")
stt_providers = self.context.get_all_stt_providers()
if stt_providers:
ret += "\n## 载入的 STT 提供商\n"
parts.append("\n## 载入的 STT 提供商\n")
for idx, stt in enumerate(stt_providers):
id_ = stt.meta().id
ret += f"{idx + 1}. {id_}"
line = f"{idx + 1}. {id_}"
stt_using = self.context.get_using_stt_provider(umo=umo)
if stt_using and stt_using.meta().id == id_:
ret += " (当前使用)"
ret += "\n"
line += " (当前使用)"
parts.append(line + "\n")
ret += "\n使用 /provider <序号> 切换 LLM 提供商。"
parts.append("\n使用 /provider <序号> 切换 LLM 提供商。")
ret = "".join(parts)
if tts_providers:
ret += "\n使用 /provider tts <序号> 切换 TTS 提供商。"
@@ -128,16 +129,17 @@ class ProviderCommands:
.use_t2i(False),
)
return
i = 1
ret = "下面列出了此模型提供商可用模型:"
for model in models:
ret += f"\n{i}. {model}"
i += 1
parts = ["下面列出了此模型提供商可用模型:"]
for i, model in enumerate(models, 1):
parts.append(f"\n{i}. {model}")
curr_model = prov.get_model() or ""
ret += f"\n当前模型: [{curr_model}]"
parts.append(f"\n当前模型: [{curr_model}]")
parts.append(
"\nTips: 使用 /model <模型名/编号>,即可实时更换模型。如目标模型不存在于上表,请输入模型名。"
)
ret += "\nTips: 使用 /model <模型名/编号>,即可实时更换模型。如目标模型不存在于上表,请输入模型名。"
ret = "".join(parts)
message.set_result(MessageEventResult().message(ret).use_t2i(False))
elif isinstance(idx_or_name, int):
models = []
@@ -180,14 +182,15 @@ class ProviderCommands:
if index is None:
keys_data = prov.get_keys()
curr_key = prov.get_current_key()
ret = "Key:"
for i, k in enumerate(keys_data):
ret += f"\n{i + 1}. {k[:8]}"
parts = ["Key:"]
for i, k in enumerate(keys_data, 1):
parts.append(f"\n{i}. {k[:8]}")
ret += f"\n当前 Key: {curr_key[:8]}"
ret += "\n当前模型: " + prov.get_model()
ret += "\n使用 /key <idx> 切换 Key。"
parts.append(f"\n当前 Key: {curr_key[:8]}")
parts.append("\n当前模型: " + prov.get_model())
parts.append("\n使用 /key <idx> 切换 Key。")
ret = "".join(parts)
message.set_result(MessageEventResult().message(ret).use_t2i(False))
else:
keys_data = prov.get_keys()

View File

@@ -119,13 +119,13 @@ class LongTermMemory:
if event.get_message_type() == MessageType.GROUP_MESSAGE:
datetime_str = datetime.datetime.now().strftime("%H:%M:%S")
final_message = f"[{event.message_obj.sender.nickname}/{datetime_str}]: "
parts = [f"[{event.message_obj.sender.nickname}/{datetime_str}]: "]
cfg = self.cfg(event)
for comp in event.get_messages():
if isinstance(comp, Plain):
final_message += f" {comp.text}"
parts.append(f" {comp.text}")
elif isinstance(comp, Image):
if cfg["image_caption"]:
try:
@@ -137,11 +137,13 @@ class LongTermMemory:
cfg["image_caption_provider_id"],
cfg["image_caption_prompt"],
)
final_message += f" [Image: {caption}]"
parts.append(f" [Image: {caption}]")
except Exception as e:
logger.error(f"获取图片描述失败: {e}")
else:
final_message += " [Image]"
parts.append(" [Image]")
final_message = "".join(parts)
logger.debug(f"ltm | {event.unified_msg_origin} | {final_message}")
self.session_chats[event.unified_msg_origin].append(final_message)
if len(self.session_chats[event.unified_msg_origin]) > cfg["max_cnt"]:

View File

@@ -203,14 +203,15 @@ class Main(star.Star):
if not reminders:
yield event.plain_result("没有正在进行的待办事项。")
else:
reminder_str = "正在进行的待办事项:\n"
parts = ["正在进行的待办事项:\n"]
for i, reminder in enumerate(reminders):
time_ = reminder.get("datetime", "")
if not time_:
cron_expr = reminder.get("cron", "")
time_ = reminder.get("cron_h", "") + f"(Cron: {cron_expr})"
reminder_str += f"{i + 1}. {reminder['text']} - {time_}\n"
reminder_str += "\n使用 /reminder rm <id> 删除待办事项。\n"
parts.append(f"{i + 1}. {reminder['text']} - {time_}\n")
parts.append("\n使用 /reminder rm <id> 删除待办事项。\n")
reminder_str = "".join(parts)
yield event.plain_result(reminder_str)
@reminder.command("rm")