-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
fix: 修复异步数据库连接池跨 event loop 调用导致的 RuntimeError #4968
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey - 我发现了 1 个问题,并给出了一些整体反馈:
- 将
Context.get_using_provider/get_using_tts_provider/get_using_stt_provider和ProviderManager.get_using_provider从同步改为异步会改变它们的公共 API;如果这些方法属于对外 / star / 插件接口的一部分,你可能需要保留一个向后兼容的同步封装,或者明确将其标记为不兼容变更(breaking change)。 - 在
ProviderManager.get_using_provider中,await sp.get_async(...)调用的关键字参数缩进方式和文件其余部分不一致;将其调整为项目中常用的多行调用风格会提升可读性。 - 在若干位置,现在在循环中对
get_using_*_provider调用了await(例如在builtin_commands/commands/provider.py中构建 provider 列表时);由于在一次调用过程中选中的 provider 并不会在循环中改变,建议在循环开始前获取一次并复用,以避免重复的异步调用。
给 AI Agent 的提示词
Please address the comments from this code review:
## Overall Comments
- Changing `Context.get_using_provider`/`get_using_tts_provider`/`get_using_stt_provider` and `ProviderManager.get_using_provider` from sync to async alters their public API; if these are part of the external/star/plugin surface you may want to either keep a backward-compatible sync wrapper or explicitly mark this as a breaking change.
- In `ProviderManager.get_using_provider`, the `await sp.get_async(...)` call has its keyword arguments oddly indented compared to the rest of the file; aligning these to the project’s usual multi-line call style will improve readability.
- Several places now `await` `get_using_*_provider` inside loops (e.g., when building provider lists in `builtin_commands/commands/provider.py); since the selected provider does not change within the loop, consider fetching it once before the loop to avoid repeated async calls.
## Individual Comments
### Comment 1
<location> `astrbot/builtin_stars/builtin_commands/commands/provider.py:141` </location>
<code_context>
for i, d in enumerate(llm_data):
line = f"{i + 1}. {d['info']}{d['mark']}"
- provider_using = self.context.get_using_provider(umo=umo)
+ provider_using = await self.context.get_using_provider(umo=umo)
if (
provider_using
</code_context>
<issue_to_address>
**suggestion (performance):** Avoid repeatedly awaiting `get_using_provider` inside the loop; fetch once before iterating.
Within the loop, `await self.context.get_using_provider(umo=umo)` is invoked on every iteration even though the provider for a given `umo` is constant for the duration of this call. Move it outside the loop and reuse the result:
```python
provider_using = await self.context.get_using_provider(umo=umo)
for i, d in enumerate(llm_data):
line = f"{i + 1}. {d['info']}{d['mark']}"
if provider_using and provider_using.meta().id == d["provider"].meta().id:
line += " (当前使用)"
parts.append(line + "\n")
```
You can apply the same pattern to the TTS and STT sections for `get_using_tts_provider` and `get_using_stt_provider` as well.
Suggested implementation:
```python
llm_data = [d for d in display_data if d["type"] == "llm"]
provider_using = await self.context.get_using_provider(umo=umo)
for i, d in enumerate(llm_data):
line = f"{i + 1}. {d['info']}{d['mark']}"
if (
provider_using
and provider_using.meta().id == d["provider"].meta().id
):
line += " (当前使用)"
parts.append(line + "\n")
parts.append("\n## 载入的 TTS 提供商\n")
tts_using = await self.context.get_using_tts_provider(umo=umo)
for i, d in enumerate(tts_data):
line = f"{i + 1}. {d['info']}{d['mark']}"
if tts_using and tts_using.meta().id == d["provider"].meta().id:
line += " (当前使用)"
parts.append(line + "\n")
parts.append("\n## 载入的 STT 提供商\n")
```
` section.
Here are the code changes:
<file_operations>
<file_operation operation="edit" file_path="astrbot/builtin_stars/builtin_commands/commands/provider.py">
<<<<<<< SEARCH
llm_data = [d for d in display_data if d["type"] == "llm"]
for i, d in enumerate(llm_data):
line = f"{i + 1}. {d['info']}{d['mark']}"
provider_using = await self.context.get_using_provider(umo=umo)
if (
provider_using
and provider_using.meta().id == d["provider"].meta().id
parts.append("\n## 载入的 TTS 提供商\n")
for i, d in enumerate(tts_data):
line = f"{i + 1}. {d['info']}{d['mark']}"
tts_using = await self.context.get_using_tts_provider(umo=umo)
if tts_using and tts_using.meta().id == d["provider"].meta().id:
line += " (当前使用)"
parts.append(line + "\n")
parts.append("\n## 载入的 STT 提供商\n")
=======
llm_data = [d for d in display_data if d["type"] == "llm"]
provider_using = await self.context.get_using_provider(umo=umo)
for i, d in enumerate(llm_data):
line = f"{i + 1}. {d['info']}{d['mark']}"
if (
provider_using
and provider_using.meta().id == d["provider"].meta().id
):
line += " (当前使用)"
parts.append(line + "\n")
parts.append("\n## 载入的 TTS 提供商\n")
tts_using = await self.context.get_using_tts_provider(umo=umo)
for i, d in enumerate(tts_data):
line = f"{i + 1}. {d['info']}{d['mark']}"
if tts_using and tts_using.meta().id == d["provider"].meta().id:
line += " (当前使用)"
parts.append(line + "\n")
parts.append("\n## 载入的 STT 提供商\n")
>>>>>>> REPLACE
</file_operation>
</file_operations>
<additional_changes>
To fully apply your suggestion for STT as well, locate the STT loop below the `parts.append("\n## 载入的 STT 提供商\n")` line. It will currently look analogous to the original TTS section, i.e. repeatedly calling `await self.context.get_using_stt_provider(umo=umo)` inside the loop. Refactor it to:
1. Add `stt_using = await self.context.get_using_stt_provider(umo=umo)` **before** the `for i, d in enumerate(stt_data):` loop.
2. Remove the `await self.context.get_using_stt_provider(umo=umo)` call from inside the loop.
3. Use `stt_using` in the `if` condition inside the loop, similar to the updated TTS block:
```python
stt_using = await self.context.get_using_stt_provider(umo=umo)
for i, d in enumerate(stt_data):
line = f"{i + 1}. {d['info']}{d['mark']}"
if stt_using and stt_using.meta().id == d["provider"].meta().id:
line += " (当前使用)"
parts.append(line + "\n")
```
This will ensure `get_using_stt_provider` is also awaited only once per call.
</issue_to_address>帮我变得更有用!请在每条评论上点击 👍 或 👎,我会根据你的反馈改进后续的代码审查。
Original comment in English
Hey - I've found 1 issue, and left some high level feedback:
- Changing
Context.get_using_provider/get_using_tts_provider/get_using_stt_providerandProviderManager.get_using_providerfrom sync to async alters their public API; if these are part of the external/star/plugin surface you may want to either keep a backward-compatible sync wrapper or explicitly mark this as a breaking change. - In
ProviderManager.get_using_provider, theawait sp.get_async(...)call has its keyword arguments oddly indented compared to the rest of the file; aligning these to the project’s usual multi-line call style will improve readability. - Several places now
awaitget_using_*_providerinside loops (e.g., when building provider lists in `builtin_commands/commands/provider.py); since the selected provider does not change within the loop, consider fetching it once before the loop to avoid repeated async calls.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- Changing `Context.get_using_provider`/`get_using_tts_provider`/`get_using_stt_provider` and `ProviderManager.get_using_provider` from sync to async alters their public API; if these are part of the external/star/plugin surface you may want to either keep a backward-compatible sync wrapper or explicitly mark this as a breaking change.
- In `ProviderManager.get_using_provider`, the `await sp.get_async(...)` call has its keyword arguments oddly indented compared to the rest of the file; aligning these to the project’s usual multi-line call style will improve readability.
- Several places now `await` `get_using_*_provider` inside loops (e.g., when building provider lists in `builtin_commands/commands/provider.py); since the selected provider does not change within the loop, consider fetching it once before the loop to avoid repeated async calls.
## Individual Comments
### Comment 1
<location> `astrbot/builtin_stars/builtin_commands/commands/provider.py:141` </location>
<code_context>
for i, d in enumerate(llm_data):
line = f"{i + 1}. {d['info']}{d['mark']}"
- provider_using = self.context.get_using_provider(umo=umo)
+ provider_using = await self.context.get_using_provider(umo=umo)
if (
provider_using
</code_context>
<issue_to_address>
**suggestion (performance):** Avoid repeatedly awaiting `get_using_provider` inside the loop; fetch once before iterating.
Within the loop, `await self.context.get_using_provider(umo=umo)` is invoked on every iteration even though the provider for a given `umo` is constant for the duration of this call. Move it outside the loop and reuse the result:
```python
provider_using = await self.context.get_using_provider(umo=umo)
for i, d in enumerate(llm_data):
line = f"{i + 1}. {d['info']}{d['mark']}"
if provider_using and provider_using.meta().id == d["provider"].meta().id:
line += " (当前使用)"
parts.append(line + "\n")
```
You can apply the same pattern to the TTS and STT sections for `get_using_tts_provider` and `get_using_stt_provider` as well.
Suggested implementation:
```python
llm_data = [d for d in display_data if d["type"] == "llm"]
provider_using = await self.context.get_using_provider(umo=umo)
for i, d in enumerate(llm_data):
line = f"{i + 1}. {d['info']}{d['mark']}"
if (
provider_using
and provider_using.meta().id == d["provider"].meta().id
):
line += " (当前使用)"
parts.append(line + "\n")
parts.append("\n## 载入的 TTS 提供商\n")
tts_using = await self.context.get_using_tts_provider(umo=umo)
for i, d in enumerate(tts_data):
line = f"{i + 1}. {d['info']}{d['mark']}"
if tts_using and tts_using.meta().id == d["provider"].meta().id:
line += " (当前使用)"
parts.append(line + "\n")
parts.append("\n## 载入的 STT 提供商\n")
```
` section.
Here are the code changes:
<file_operations>
<file_operation operation="edit" file_path="astrbot/builtin_stars/builtin_commands/commands/provider.py">
<<<<<<< SEARCH
llm_data = [d for d in display_data if d["type"] == "llm"]
for i, d in enumerate(llm_data):
line = f"{i + 1}. {d['info']}{d['mark']}"
provider_using = await self.context.get_using_provider(umo=umo)
if (
provider_using
and provider_using.meta().id == d["provider"].meta().id
parts.append("\n## 载入的 TTS 提供商\n")
for i, d in enumerate(tts_data):
line = f"{i + 1}. {d['info']}{d['mark']}"
tts_using = await self.context.get_using_tts_provider(umo=umo)
if tts_using and tts_using.meta().id == d["provider"].meta().id:
line += " (当前使用)"
parts.append(line + "\n")
parts.append("\n## 载入的 STT 提供商\n")
=======
llm_data = [d for d in display_data if d["type"] == "llm"]
provider_using = await self.context.get_using_provider(umo=umo)
for i, d in enumerate(llm_data):
line = f"{i + 1}. {d['info']}{d['mark']}"
if (
provider_using
and provider_using.meta().id == d["provider"].meta().id
):
line += " (当前使用)"
parts.append(line + "\n")
parts.append("\n## 载入的 TTS 提供商\n")
tts_using = await self.context.get_using_tts_provider(umo=umo)
for i, d in enumerate(tts_data):
line = f"{i + 1}. {d['info']}{d['mark']}"
if tts_using and tts_using.meta().id == d["provider"].meta().id:
line += " (当前使用)"
parts.append(line + "\n")
parts.append("\n## 载入的 STT 提供商\n")
>>>>>>> REPLACE
</file_operation>
</file_operations>
<additional_changes>
To fully apply your suggestion for STT as well, locate the STT loop below the `parts.append("\n## 载入的 STT 提供商\n")` line. It will currently look analogous to the original TTS section, i.e. repeatedly calling `await self.context.get_using_stt_provider(umo=umo)` inside the loop. Refactor it to:
1. Add `stt_using = await self.context.get_using_stt_provider(umo=umo)` **before** the `for i, d in enumerate(stt_data):` loop.
2. Remove the `await self.context.get_using_stt_provider(umo=umo)` call from inside the loop.
3. Use `stt_using` in the `if` condition inside the loop, similar to the updated TTS block:
```python
stt_using = await self.context.get_using_stt_provider(umo=umo)
for i, d in enumerate(stt_data):
line = f"{i + 1}. {d['info']}{d['mark']}"
if stt_using and stt_using.meta().id == d["provider"].meta().id:
line += " (当前使用)"
parts.append(line + "\n")
```
This will ensure `get_using_stt_provider` is also awaited only once per call.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| for i, d in enumerate(llm_data): | ||
| line = f"{i + 1}. {d['info']}{d['mark']}" | ||
| provider_using = self.context.get_using_provider(umo=umo) | ||
| provider_using = await self.context.get_using_provider(umo=umo) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
建议(性能): 避免在循环内部重复地对 get_using_provider 调用 await;请在进入循环前获取一次并在循环中复用。
在当前实现中,await self.context.get_using_provider(umo=umo) 会在循环的每次迭代中被调用一次,即使某个给定 umo 对应的 provider 在这次调用期间是固定不变的。可以将这行代码移到循环外部,并复用结果:
provider_using = await self.context.get_using_provider(umo=umo)
for i, d in enumerate(llm_data):
line = f"{i + 1}. {d['info']}{d['mark']}"
if provider_using and provider_using.meta().id == d["provider"].meta().id:
line += " (当前使用)"
parts.append(line + "\n")同样的模式也可以应用在 TTS 和 STT 的代码中,即 get_using_tts_provider 和 get_using_stt_provider。
建议的实现方式:
llm_data = [d for d in display_data if d["type"] == "llm"]
provider_using = await self.context.get_using_provider(umo=umo)
for i, d in enumerate(llm_data):
line = f"{i + 1}. {d['info']}{d['mark']}"
if (
provider_using
and provider_using.meta().id == d["provider"].meta().id
):
line += " (当前使用)"
parts.append(line + "\n")
parts.append("\n## 载入的 TTS 提供商\n")
tts_using = await self.context.get_using_tts_provider(umo=umo)
for i, d in enumerate(tts_data):
line = f"{i + 1}. {d['info']}{d['mark']}"
if tts_using and tts_using.meta().id == d["provider"].meta().id:
line += " (当前使用)"
parts.append(line + "\n")
parts.append("\n## 载入的 STT 提供商\n")` section.
以下是代码变更:
<file_operations>
<file_operation operation="edit" file_path="astrbot/builtin_stars/builtin_commands/commands/provider.py">
<<<<<<< SEARCH
llm_data = [d for d in display_data if d["type"] == "llm"]
for i, d in enumerate(llm_data):
line = f"{i + 1}. {d['info']}{d['mark']}"
provider_using = await self.context.get_using_provider(umo=umo)
if (
provider_using
and provider_using.meta().id == d["provider"].meta().id
parts.append("\n## 载入的 TTS 提供商\n")
for i, d in enumerate(tts_data):
line = f"{i + 1}. {d['info']}{d['mark']}"
tts_using = await self.context.get_using_tts_provider(umo=umo)
if tts_using and tts_using.meta().id == d["provider"].meta().id:
line += " (当前使用)"
parts.append(line + "\n")
parts.append("\n## 载入的 STT 提供商\n")
llm_data = [d for d in display_data if d["type"] == "llm"]
provider_using = await self.context.get_using_provider(umo=umo)
for i, d in enumerate(llm_data):
line = f"{i + 1}. {d['info']}{d['mark']}"
if (
provider_using
and provider_using.meta().id == d["provider"].meta().id
):
line += " (当前使用)"
parts.append(line + "\n")
parts.append("\n## 载入的 TTS 提供商\n")
tts_using = await self.context.get_using_tts_provider(umo=umo)
for i, d in enumerate(tts_data):
line = f"{i + 1}. {d['info']}{d['mark']}"
if tts_using and tts_using.meta().id == d["provider"].meta().id:
line += " (当前使用)"
parts.append(line + "\n")
parts.append("\n## 载入的 STT 提供商\n")
REPLACE
</file_operation>
</file_operations>
<additional_changes>
要把对 STT 的优化也完整应用上去,请找到位于 parts.append("\n## 载入的 STT 提供商\n") 这一行之后的 STT 循环。它现在的写法会和原来的 TTS 代码类似,即在循环内部重复调用 await self.context.get_using_stt_provider(umo=umo)。可以按下面的方式重构:
- 在
for i, d in enumerate(stt_data):循环之前新增stt_using = await self.context.get_using_stt_provider(umo=umo)。 - 从循环内部删除
await self.context.get_using_stt_provider(umo=umo)调用。 - 在循环内部的
if判断中使用stt_using,写法与更新后的 TTS 块类似:
stt_using = await self.context.get_using_stt_provider(umo=umo)
for i, d in enumerate(stt_data):
line = f"{i + 1}. {d['info']}{d['mark']}"
if stt_using and stt_using.meta().id == d["provider"].meta().id:
line += " (当前使用)"
parts.append(line + "\n")这样可以确保 get_using_stt_provider 在一次调用流程中同样只会被 await 一次。
Original comment in English
suggestion (performance): Avoid repeatedly awaiting get_using_provider inside the loop; fetch once before iterating.
Within the loop, await self.context.get_using_provider(umo=umo) is invoked on every iteration even though the provider for a given umo is constant for the duration of this call. Move it outside the loop and reuse the result:
provider_using = await self.context.get_using_provider(umo=umo)
for i, d in enumerate(llm_data):
line = f"{i + 1}. {d['info']}{d['mark']}"
if provider_using and provider_using.meta().id == d["provider"].meta().id:
line += " (当前使用)"
parts.append(line + "\n")You can apply the same pattern to the TTS and STT sections for get_using_tts_provider and get_using_stt_provider as well.
Suggested implementation:
llm_data = [d for d in display_data if d["type"] == "llm"]
provider_using = await self.context.get_using_provider(umo=umo)
for i, d in enumerate(llm_data):
line = f"{i + 1}. {d['info']}{d['mark']}"
if (
provider_using
and provider_using.meta().id == d["provider"].meta().id
):
line += " (当前使用)"
parts.append(line + "\n")
parts.append("\n## 载入的 TTS 提供商\n")
tts_using = await self.context.get_using_tts_provider(umo=umo)
for i, d in enumerate(tts_data):
line = f"{i + 1}. {d['info']}{d['mark']}"
if tts_using and tts_using.meta().id == d["provider"].meta().id:
line += " (当前使用)"
parts.append(line + "\n")
parts.append("\n## 载入的 STT 提供商\n")` section.
Here are the code changes:
<file_operations>
<file_operation operation="edit" file_path="astrbot/builtin_stars/builtin_commands/commands/provider.py">
<<<<<<< SEARCH
llm_data = [d for d in display_data if d["type"] == "llm"]
for i, d in enumerate(llm_data):
line = f"{i + 1}. {d['info']}{d['mark']}"
provider_using = await self.context.get_using_provider(umo=umo)
if (
provider_using
and provider_using.meta().id == d["provider"].meta().id
parts.append("\n## 载入的 TTS 提供商\n")
for i, d in enumerate(tts_data):
line = f"{i + 1}. {d['info']}{d['mark']}"
tts_using = await self.context.get_using_tts_provider(umo=umo)
if tts_using and tts_using.meta().id == d["provider"].meta().id:
line += " (当前使用)"
parts.append(line + "\n")
parts.append("\n## 载入的 STT 提供商\n")
llm_data = [d for d in display_data if d["type"] == "llm"]
provider_using = await self.context.get_using_provider(umo=umo)
for i, d in enumerate(llm_data):
line = f"{i + 1}. {d['info']}{d['mark']}"
if (
provider_using
and provider_using.meta().id == d["provider"].meta().id
):
line += " (当前使用)"
parts.append(line + "\n")
parts.append("\n## 载入的 TTS 提供商\n")
tts_using = await self.context.get_using_tts_provider(umo=umo)
for i, d in enumerate(tts_data):
line = f"{i + 1}. {d['info']}{d['mark']}"
if tts_using and tts_using.meta().id == d["provider"].meta().id:
line += " (当前使用)"
parts.append(line + "\n")
parts.append("\n## 载入的 STT 提供商\n")
REPLACE
</file_operation>
</file_operations>
<additional_changes>
To fully apply your suggestion for STT as well, locate the STT loop below the parts.append("\n## 载入的 STT 提供商\n") line. It will currently look analogous to the original TTS section, i.e. repeatedly calling await self.context.get_using_stt_provider(umo=umo) inside the loop. Refactor it to:
- Add
stt_using = await self.context.get_using_stt_provider(umo=umo)before thefor i, d in enumerate(stt_data):loop. - Remove the
await self.context.get_using_stt_provider(umo=umo)call from inside the loop. - Use
stt_usingin theifcondition inside the loop, similar to the updated TTS block:
stt_using = await self.context.get_using_stt_provider(umo=umo)
for i, d in enumerate(stt_data):
line = f"{i + 1}. {d['info']}{d['mark']}"
if stt_using and stt_using.meta().id == d["provider"].meta().id:
line += " (当前使用)"
parts.append(line + "\n")This will ensure get_using_stt_provider is also awaited only once per call.
9a60ec1 to
e1b7154
Compare
| return self.provider_manager.embedding_provider_insts | ||
|
|
||
| def get_using_provider(self, umo: str | None = None) -> Provider | None: | ||
| async def get_using_provider(self, umo: str | None = None) -> Provider | None: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
保持同步方法的原因是为了向后兼容。直接将这个改为异步方法会导致插件不兼容。
建议新建 get_using_provider_async 方法去处理。
修复高并发场景下异步数据库连接池跨 event loop 调用导致的 RuntimeError。
问题现象:
当消息处理并发量较高时,日志出现以下报错:
RuntimeError: <Queue at 0x7ffebb54fc90 maxsize=5 tasks=14384> is bound to a different event loop问题根源:
SharedPreferences.get()同步方法内部使用asyncio.run_coroutine_threadsafe()将异步数据库查询提交到一个独立的 event loop(_sync_loop),而 SQLAlchemy 的AsyncSession连接池是绑定在主 event loop 上的,跨 loop 操作导致连接池队列冲突。触发链路:
ProviderManager.get_using_provider()→sp.get()→_sync_loop中执行get_async()→ 数据库连接池报错修复方案:
将
get_using_provider()及相关调用链改为 async/await 模式,直接使用sp.get_async()在主 event loop 中执行异步数据库操作,避免跨 loop 调用。Modifications / 改动点
修改的核心文件:
astrbot/core/provider/manager.pyget_using_provider()改为 async,使用await sp.get_async()astrbot/core/star/context.pyget_using_provider()、get_current_chat_provider_id()等方法改为 asyncastrbot/core/astr_main_agent.py_select_provider()改为 async,修复相关调用添加 awaitastrbot/core/pipeline/preprocess_stage/stage.pyget_using_stt_provider()调用添加 awaitastrbot/core/pipeline/process_stage/method/agent_sub_stages/internal.pyastrbot/core/pipeline/result_decorate/stage.pyastrbot/builtin_stars/astrbot/long_term_memory.pyastrbot/builtin_stars/astrbot/main.pyastrbot/builtin_stars/builtin_commands/commands/conversation.pyastrbot/builtin_stars/builtin_commands/commands/provider.py实现效果:
将
get_using_provider()、get_using_stt_provider()等 Provider 获取逻辑统一改为 async/await 模式,并修复整条调用链上所有相关方法,确保所有数据库操作都在主 event loop 中执行,消除跨 loop 调用导致的连接池冲突问题。Screenshots or Test Results / 运行截图或测试结果
Checklist / 检查清单
requirements.txt和pyproject.toml文件相应位置。/ I have ensured that no new dependencies are introduced, OR if new dependencies are introduced, they have been added to the appropriate locations inrequirements.txtandpyproject.toml.Summary by Sourcery
将 provider 选择逻辑转换为 async/await,以确保基于数据库的 provider 首选项在主事件循环中访问,避免跨事件循环导致的运行时错误。
Bug Fixes:
SharedPreferences的问题,防止来自 SQLAlchemy 异步连接池的RuntimeError。Enhancements:
Original summary in English
Summary by Sourcery
Convert provider selection to async/await to ensure database-backed provider preferences are accessed within the main event loop and avoid cross-loop runtime errors.
Bug Fixes:
Enhancements: