butterbot-python 3.1.0.dev1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. butterbot/__init__.py +15 -0
  2. butterbot/app/__init__.py +41 -0
  3. butterbot/app/bot_app.py +374 -0
  4. butterbot/app/config.py +156 -0
  5. butterbot/app/source_manager.py +367 -0
  6. butterbot/core/__init__.py +46 -0
  7. butterbot/core/api/__init__.py +9 -0
  8. butterbot/core/api/base_api.py +32 -0
  9. butterbot/core/context/README.md +1 -0
  10. butterbot/core/context/__init__.py +9 -0
  11. butterbot/core/context/api_registry.py +109 -0
  12. butterbot/core/context/app_context.py +51 -0
  13. butterbot/core/context/config_provider.py +14 -0
  14. butterbot/core/data/__init__.py +15 -0
  15. butterbot/core/data/base_data.py +29 -0
  16. butterbot/core/data/base_model.py +197 -0
  17. butterbot/core/event/README.md +1 -0
  18. butterbot/core/event/__init__.py +10 -0
  19. butterbot/core/event/event.py +27 -0
  20. butterbot/core/event/event_bus.py +276 -0
  21. butterbot/core/event/subscriber.py +130 -0
  22. butterbot/core/exceptions.py +90 -0
  23. butterbot/core/filter/__init__.py +11 -0
  24. butterbot/core/filter/base_filter.py +74 -0
  25. butterbot/core/source/README.md +1 -0
  26. butterbot/core/source/__init__.py +9 -0
  27. butterbot/core/source/base_source.py +134 -0
  28. butterbot/core/types/__init__.py +9 -0
  29. butterbot/core/types/base_type.py +74 -0
  30. butterbot/sources/__init__.py +1 -0
  31. butterbot/sources/bilibili/README.md +10 -0
  32. butterbot/sources/bilibili/__init__.py +20 -0
  33. butterbot/sources/bilibili/api/__init__.py +5 -0
  34. butterbot/sources/bilibili/api/bili_api.py +123 -0
  35. butterbot/sources/bilibili/data/__init__.py +52 -0
  36. butterbot/sources/bilibili/data/danmaku_gift_data.py +135 -0
  37. butterbot/sources/bilibili/data/danmaku_guard_data.py +54 -0
  38. butterbot/sources/bilibili/data/danmaku_msg_data.py +71 -0
  39. butterbot/sources/bilibili/data/dto/__init__.py +59 -0
  40. butterbot/sources/bilibili/data/dto/danmaku_gift_dto.py +193 -0
  41. butterbot/sources/bilibili/data/dto/danmaku_guard_buy_dto.py +54 -0
  42. butterbot/sources/bilibili/data/dto/danmaku_msg_dto.py +123 -0
  43. butterbot/sources/bilibili/data/dto/dynamic_dto.py +276 -0
  44. butterbot/sources/bilibili/data/dto/live_room_dto.py +169 -0
  45. butterbot/sources/bilibili/data/dto/video_part_dto.py +18 -0
  46. butterbot/sources/bilibili/data/dynamic_data.py +362 -0
  47. butterbot/sources/bilibili/data/live_room_data.py +162 -0
  48. butterbot/sources/bilibili/data/video_part.py +46 -0
  49. butterbot/sources/bilibili/source/__init__.py +15 -0
  50. butterbot/sources/bilibili/source/base_polling_source.py +130 -0
  51. butterbot/sources/bilibili/source/bili_danmaku_source.py +230 -0
  52. butterbot/sources/bilibili/source/bili_dynamic_source.py +135 -0
  53. butterbot/sources/bilibili/source/bili_live_source.py +137 -0
  54. butterbot/sources/bilibili/types/__init__.py +11 -0
  55. butterbot/sources/bilibili/types/bili_type.py +32 -0
  56. butterbot/sources/napcat/README.md +10 -0
  57. butterbot/sources/napcat/__init__.py +20 -0
  58. butterbot/sources/napcat/api/__init__.py +3 -0
  59. butterbot/sources/napcat/api/napcat_api.py +316 -0
  60. butterbot/sources/napcat/data/__init__.py +179 -0
  61. butterbot/sources/napcat/data/event_data.py +441 -0
  62. butterbot/sources/napcat/data/segment_data.py +432 -0
  63. butterbot/sources/napcat/events.py +91 -0
  64. butterbot/sources/napcat/filters/__init__.py +19 -0
  65. butterbot/sources/napcat/filters/filters.py +202 -0
  66. butterbot/sources/napcat/source/__init__.py +5 -0
  67. butterbot/sources/napcat/source/napcat_source.py +58 -0
  68. butterbot/sources/napcat/types/__init__.py +5 -0
  69. butterbot/sources/napcat/types/napcat_type.py +61 -0
  70. butterbot/utils/README.md +15 -0
  71. butterbot/utils/__init__.py +11 -0
  72. butterbot/utils/data_pair.py +27 -0
  73. butterbot/utils/logging_config.py +521 -0
  74. butterbot/utils/terminal.py +308 -0
  75. butterbot/utils/websocket.py +1270 -0
  76. butterbot_python-3.1.0.dev1.dist-info/METADATA +769 -0
  77. butterbot_python-3.1.0.dev1.dist-info/RECORD +79 -0
  78. butterbot_python-3.1.0.dev1.dist-info/WHEEL +4 -0
  79. butterbot_python-3.1.0.dev1.dist-info/licenses/LICENSE +674 -0
@@ -0,0 +1,367 @@
1
+ import asyncio
2
+ from logging import getLogger
3
+ from typing import TYPE_CHECKING, Callable, ParamSpec, overload
4
+ from uuid import UUID
5
+
6
+ from butterbot.core.exceptions import LifecycleError, SourceError, SourceStartError
7
+ from butterbot.core.source import BaseSource, BaseSourceT
8
+
9
+ _SourceP = ParamSpec("_SourceP")
10
+
11
+ if TYPE_CHECKING:
12
+ from butterbot.core.context import AppContext
13
+
14
+ _log = getLogger(__name__)
15
+
16
+
17
+ class SourceManager:
18
+ """SourceManager 类,专注于事件源的生命周期管理.
19
+
20
+ 负责添加/移除事件源,并统一管理其启动、停止流程和异步任务。
21
+ 高层概念(EventBus、ApiRegistry、AppContext)由 BotApp 持有并通过
22
+ AppContext 注入——因此本类只负责事件源自身及其在 EventBus 上的订阅,
23
+ 不负责关闭 EventBus / ApiRegistry(那是 :meth:`BotApp.close` 的职责)。
24
+
25
+ Attributes:
26
+ sources: 事件源集合(UUID -> Source)
27
+ running: 是否正在运行
28
+ closed: 是否已关闭
29
+ """
30
+
31
+ def __init__(self, ctx: "AppContext"):
32
+ """初始化 SourceManager.
33
+
34
+ Args:
35
+ ctx: 由 BotApp 构建并传入的应用上下文
36
+ """
37
+ # 注入应用上下文(只读引用,不持有所有权)
38
+ self._ctx = ctx
39
+
40
+ # 事件源集合:UUID -> Source
41
+ self._sources: dict[UUID, BaseSource] = {}
42
+
43
+ # 生命周期状态
44
+ self._running = False
45
+ self._closed = False
46
+
47
+ # ============ 属性 ============ #
48
+
49
+ @property
50
+ def ctx(self) -> "AppContext":
51
+ """获取应用上下文."""
52
+ return self._ctx
53
+
54
+ @property
55
+ def running(self) -> bool:
56
+ """检查是否正在运行."""
57
+ return self._running
58
+
59
+ @property
60
+ def closed(self) -> bool:
61
+ """检查是否已关闭."""
62
+ return self._closed
63
+
64
+ @property
65
+ def sources(self) -> dict[UUID, BaseSource]:
66
+ """获取所有事件源的副本."""
67
+ return dict(self._sources)
68
+
69
+ # ============ Source 管理 ============ #
70
+
71
+ def add_source(
72
+ self,
73
+ source_cls: Callable[_SourceP, BaseSourceT],
74
+ *args: _SourceP.args,
75
+ **kwargs: _SourceP.kwargs,
76
+ ) -> BaseSourceT:
77
+ """添加事件源.
78
+
79
+ 本方法只做注册,不启动。应用已在运行时会立即注入上下文,
80
+ 以便按"注册 → 订阅 → :meth:`start_source`"的顺序接入新事件源;
81
+ 之所以不自动启动,是因为自动启动会让事件在 ``subscribe`` 注册完成前
82
+ 就开始产生,从而丢事件。
83
+
84
+ Args:
85
+ source_cls: 事件源类
86
+ *args: 事件源初始化位置参数
87
+ **kwargs: 事件源初始化关键字参数
88
+
89
+ Returns:
90
+ 事件源实例
91
+
92
+ Raises:
93
+ LifecycleError: 如果 SourceManager 已关闭
94
+ """
95
+ if self._closed:
96
+ raise LifecycleError("SourceManager 已关闭,无法添加事件源")
97
+
98
+ source = source_cls(*args, **kwargs)
99
+
100
+ self._sources[source.uuid] = source
101
+ if self._running:
102
+ # 运行中新增:先注入上下文,让用户可以立刻订阅,再显式启动
103
+ source.bind(self._ctx)
104
+ _log.info(
105
+ "运行中添加事件源: %s (uuid=%s),"
106
+ "请在注册订阅后调用 await start_source(...) 启动",
107
+ source.__class__.__name__,
108
+ source.uuid,
109
+ )
110
+ else:
111
+ _log.info(
112
+ "添加事件源: %s (uuid=%s)", source.__class__.__name__, source.uuid
113
+ )
114
+ return source
115
+
116
+ async def remove_source(self, source_id: UUID) -> BaseSource | None:
117
+ """移除事件源.
118
+
119
+ 完整的移除序列:停止事件源 → 清理它在 EventBus 上的全部订阅 →
120
+ 从集合中摘除。只 pop 不做前两步会留下"幽灵源":
121
+ 后台任务仍在跑,且派发表里的回调永久残留。
122
+
123
+ Args:
124
+ source_id: 事件源的 UUID
125
+
126
+ Returns:
127
+ 被移除的事件源,如果不存在则返回 None
128
+ """
129
+ source = self._sources.get(source_id)
130
+ if source is None:
131
+ _log.warning("事件源 %s 不存在", source_id)
132
+ return None
133
+
134
+ try:
135
+ await source.stop()
136
+ except Exception:
137
+ _log.exception("移除前停止 %s 失败", source.__class__.__name__)
138
+
139
+ removed_callbacks = self._ctx.bus.remove_subscribers(source_id)
140
+ self._sources.pop(source_id, None)
141
+ _log.info(
142
+ "移除事件源: %s (uuid=%s),同时清理 %s 个订阅回调",
143
+ source.__class__.__name__,
144
+ source_id,
145
+ removed_callbacks,
146
+ )
147
+ return source
148
+
149
+ @overload
150
+ def get_source(self, source: UUID) -> BaseSource | None: ...
151
+
152
+ @overload
153
+ def get_source(
154
+ self, source: type[BaseSourceT], config_key: str | None = None
155
+ ) -> BaseSourceT | None: ...
156
+
157
+ def get_source(
158
+ self,
159
+ source: type[BaseSource] | UUID,
160
+ config_key: str | None = None,
161
+ ) -> BaseSource | None:
162
+ """获取事件源.
163
+
164
+ 支持三种查找方式:
165
+
166
+ - ``get_source(source_id)`` — 按 UUID 查找
167
+ - ``get_source(source_cls)`` — 按类型查找(单一实例时最常用)
168
+ - ``get_source(source_cls, config_key)`` — 按类型 + 配置键查找(同源多实例时区分)
169
+
170
+ Args:
171
+ source: 事件源类或 UUID
172
+ config_key: 配置键,可选。传入时做精确匹配,否则返回该类型的第一个实例
173
+
174
+ Returns:
175
+ 事件源实例,如果不存在则返回 None
176
+ """
177
+ # 按 UUID 查找
178
+ if isinstance(source, UUID):
179
+ return self._sources.get(source)
180
+
181
+ # 此时 source 一定是 type[BaseSource]
182
+ source_cls: type[BaseSource] = source
183
+
184
+ # 按类型查找(可选精确匹配 config_key)
185
+ for inst in self._sources.values():
186
+ if isinstance(inst, source_cls):
187
+ if config_key is None or inst.config_key == config_key:
188
+ return inst
189
+ return None
190
+
191
+ def _require_source(self, source: BaseSource | UUID) -> BaseSource:
192
+ """解析并校验事件源必须已注册.
193
+
194
+ Args:
195
+ source: 事件源实例或其 UUID
196
+
197
+ Returns:
198
+ 已注册的事件源实例
199
+
200
+ Raises:
201
+ SourceError: 事件源未注册
202
+ """
203
+ source_id = source if isinstance(source, UUID) else source.uuid
204
+ found = self._sources.get(source_id)
205
+ if found is None:
206
+ raise SourceError("事件源 %s 不存在,请先通过 add_source 添加" % source_id)
207
+ return found
208
+
209
+ # ============ 单个 Source 的运行时启停 ============ #
210
+
211
+ async def start_source(self, source: BaseSource | UUID) -> BaseSource:
212
+ """启动单个事件源(可在应用运行中调用).
213
+
214
+ 用于运行期动态接入新事件源:``add_source`` → ``subscribe`` →
215
+ ``await start_source(...)``。重复调用是安全的
216
+ (:meth:`BaseSource.start` 对已运行的源直接返回)。
217
+
218
+ Args:
219
+ source: 事件源实例或其 UUID
220
+
221
+ Returns:
222
+ 被启动的事件源
223
+
224
+ Raises:
225
+ LifecycleError: SourceManager 已关闭
226
+ SourceError: 事件源未注册
227
+ """
228
+ if self._closed:
229
+ raise LifecycleError("SourceManager 已关闭,无法启动事件源")
230
+
231
+ resolved = self._require_source(source)
232
+ resolved.bind(self._ctx)
233
+ await resolved.start()
234
+ _log.info("启动事件源: %s (uuid=%s)", type(resolved).__name__, resolved.uuid)
235
+ return resolved
236
+
237
+ async def stop_source(self, source: BaseSource | UUID) -> BaseSource:
238
+ """停止单个事件源,但保留注册与订阅(可再次 :meth:`start_source`).
239
+
240
+ Args:
241
+ source: 事件源实例或其 UUID
242
+
243
+ Returns:
244
+ 被停止的事件源
245
+
246
+ Raises:
247
+ SourceError: 事件源未注册
248
+ """
249
+ resolved = self._require_source(source)
250
+ await resolved.stop()
251
+ _log.info("停止事件源: %s (uuid=%s)", type(resolved).__name__, resolved.uuid)
252
+ return resolved
253
+
254
+ # ============ 生命周期 ============ #
255
+
256
+ async def start(self) -> None:
257
+ """启动 SourceManager.
258
+
259
+ 生命周期顺序:
260
+
261
+ 1. 检查状态
262
+ 2. 为每个 source 注入上下文 (bind)
263
+ 3. 启动每个 source
264
+
265
+ 任一事件源启动失败时不留半启动状态:已成功启动的会被回滚(stop),
266
+ 然后把全部失败聚合成 :class:`SourceStartError` 上抛,``running`` 保持 ``False``。
267
+
268
+ Raises:
269
+ LifecycleError: SourceManager 已关闭
270
+ SourceStartError: 一个或多个事件源启动失败
271
+ """
272
+ if self._closed:
273
+ raise LifecycleError("SourceManager 已关闭,无法启动")
274
+
275
+ if self._running:
276
+ _log.warning("SourceManager 已在运行中")
277
+ return
278
+
279
+ _log.info("正在启动 SourceManager...")
280
+
281
+ # 为每个 source 注入上下文
282
+ for source in self._sources.values():
283
+ source.bind(self._ctx)
284
+ _log.debug("绑定上下文到 %s", source.__class__.__name__)
285
+
286
+ # 启动所有 source
287
+ started: list[BaseSource] = []
288
+ failures: dict[str, BaseException] = {}
289
+ for source in self._sources.values():
290
+ try:
291
+ await source.start()
292
+ except Exception as e:
293
+ _log.exception("启动 %s 失败", source.__class__.__name__)
294
+ failures["%s(uuid=%s)" % (source.__class__.__name__, source.uuid)] = e
295
+ else:
296
+ started.append(source)
297
+ _log.debug("启动 %s", source.__class__.__name__)
298
+
299
+ if failures:
300
+ for source in started:
301
+ try:
302
+ await source.stop()
303
+ except Exception:
304
+ _log.exception("回滚 %s 时出错", source.__class__.__name__)
305
+ raise SourceStartError(failures)
306
+
307
+ self._running = True
308
+ _log.info("SourceManager 已启动")
309
+
310
+ async def stop(self) -> None:
311
+ """停止 SourceManager.
312
+
313
+ 依次停止所有事件源;单个事件源停止失败只记录日志,不影响其余事件源。
314
+
315
+ ``CancelledError`` 被单独接住:Ctrl+C 路径下 ``await source.stop()``
316
+ 会在取消态抛出它,而它不是 ``Exception`` 的子类——不单独处理就会
317
+ 中断后续事件源的清理。这里先清完所有事件源,最后再把取消向上重抛。
318
+
319
+ Raises:
320
+ asyncio.CancelledError: 停止过程中被取消(在清理完成后重抛)
321
+ """
322
+ if not self._running:
323
+ _log.warning("SourceManager 未在运行")
324
+ return
325
+
326
+ _log.info("正在停止 SourceManager...")
327
+
328
+ cancelled: asyncio.CancelledError | None = None
329
+
330
+ # 停止所有 source
331
+ for source in self._sources.values():
332
+ try:
333
+ await source.stop()
334
+ _log.debug("停止 %s", source.__class__.__name__)
335
+ except asyncio.CancelledError as e:
336
+ cancelled = e
337
+ _log.warning(
338
+ "停止 %s 时被取消,继续清理其余事件源",
339
+ source.__class__.__name__,
340
+ )
341
+ except Exception:
342
+ _log.exception("停止 %s 失败", source.__class__.__name__)
343
+
344
+ self._running = False
345
+ _log.info("SourceManager 已停止")
346
+
347
+ if cancelled is not None:
348
+ raise cancelled
349
+
350
+ async def close(self) -> None:
351
+ """关闭 SourceManager.
352
+
353
+ 关闭后无法再使用。即使 :meth:`stop` 因取消而抛出,
354
+ 集合清理与状态置位仍会完成(在 ``finally`` 中)。
355
+ """
356
+ if self._closed:
357
+ return
358
+
359
+ try:
360
+ # 先停止
361
+ if self._running:
362
+ await self.stop()
363
+ finally:
364
+ # 清理资源
365
+ self._sources.clear()
366
+ self._closed = True
367
+ _log.info("SourceManager 已关闭")
@@ -0,0 +1,46 @@
1
+ from .api import BaseApi, BaseApiT
2
+ from .context import ApiRegistry, AppContext, ConfigProvider
3
+ from .data import AutoDispatchList, BaseDataMixin, BaseDataModel, BaseDataT
4
+ from .event import Event, EventBus
5
+ from .exceptions import (
6
+ ApiError,
7
+ ButterError,
8
+ ConfigError,
9
+ LifecycleError,
10
+ SourceError,
11
+ SourceStartError,
12
+ SubscriptionError,
13
+ )
14
+ from .filter import AndFilter, BaseFilter, OrFilter
15
+ from .source import BaseSource, BaseSourceT
16
+ from .types import BaseType, BaseTypeT
17
+
18
+ __all__ = [
19
+ "AndFilter",
20
+ "ApiError",
21
+ "ApiRegistry",
22
+ "AppContext",
23
+ "AutoDispatchList",
24
+ "BaseApi",
25
+ "BaseApiT",
26
+ "BaseDataMixin",
27
+ "BaseDataModel",
28
+ "BaseDataT",
29
+ "BaseFilter",
30
+ # Base classes
31
+ "BaseSource",
32
+ "BaseSourceT",
33
+ "BaseType",
34
+ "BaseTypeT",
35
+ # 异常层级
36
+ "ButterError",
37
+ "ConfigError",
38
+ "ConfigProvider",
39
+ "Event",
40
+ "EventBus",
41
+ "LifecycleError",
42
+ "OrFilter",
43
+ "SourceError",
44
+ "SourceStartError",
45
+ "SubscriptionError",
46
+ ]
@@ -0,0 +1,9 @@
1
+ from .base_api import (
2
+ BaseApi,
3
+ BaseApiT,
4
+ )
5
+
6
+ __all__ = [
7
+ "BaseApi",
8
+ "BaseApiT",
9
+ ]
@@ -0,0 +1,32 @@
1
+ from abc import ABC, abstractmethod
2
+ from typing import TYPE_CHECKING, Self, TypeVar
3
+
4
+ if TYPE_CHECKING:
5
+ from butterbot.core.context import ApiRegistry
6
+
7
+
8
+ class BaseApi(ABC):
9
+ @abstractmethod
10
+ def __init__(self):
11
+ pass
12
+
13
+ @classmethod
14
+ @abstractmethod
15
+ def create(cls, ctx: "ApiRegistry", config_key: str) -> Self:
16
+ """API实例工厂方法"""
17
+ pass
18
+
19
+ async def aclose(self) -> None:
20
+ """释放该 API 持有的资源(可选覆写).
21
+
22
+ 由 :meth:`ApiRegistry.aclose_all` 在应用关闭时调用,
23
+ 用于关闭长连接、取消后台任务等。默认实现什么都不做——
24
+ 无外部资源的 API 无需覆写。
25
+
26
+ 实现要求:必须幂等(可能被重复调用),且不应抛出异常
27
+ (抛出的异常会被 ApiRegistry 记录后忽略,不影响其他 API 的关闭)。
28
+ """
29
+ return None
30
+
31
+
32
+ BaseApiT = TypeVar("BaseApiT", bound=BaseApi)
@@ -0,0 +1 @@
1
+ --- 待施工 ---
@@ -0,0 +1,9 @@
1
+ from .api_registry import ApiRegistry
2
+ from .app_context import AppContext
3
+ from .config_provider import ConfigProvider
4
+
5
+ __all__ = [
6
+ "ApiRegistry",
7
+ "AppContext",
8
+ "ConfigProvider",
9
+ ]
@@ -0,0 +1,109 @@
1
+ from __future__ import annotations
2
+
3
+ from collections import defaultdict
4
+ from logging import getLogger
5
+ from threading import RLock
6
+ from typing import Any
7
+
8
+ from butterbot.core.api import BaseApi, BaseApiT
9
+ from butterbot.core.exceptions import ConfigError
10
+
11
+ from .config_provider import ConfigProvider
12
+
13
+ _log = getLogger(__name__)
14
+
15
+
16
+ class ApiRegistry:
17
+ """API 注册器,负责管理 API 单例和配置."""
18
+
19
+ def __init__(self, config: ConfigProvider):
20
+ """初始化 ApiRegistry 实例.
21
+ Args:
22
+ config: 实现 :class:`ConfigProvider` 的运行时配置
23
+ """
24
+ self.config = config
25
+ # 用可重入锁:API 的 create() 是同步方法,允许在内部再取用别的 API
26
+ # (create → get_api → 同一把锁),非重入锁会在这条路径上自死锁。
27
+ self._lock = RLock()
28
+ self._instances: dict[type, dict[str, Any]] = defaultdict(dict)
29
+ # Type[BaseApiT] - {config_key - BaseApiT}
30
+
31
+ def get_api(
32
+ self,
33
+ cls: type[BaseApiT],
34
+ config_key: str,
35
+ ) -> BaseApiT:
36
+ """
37
+ 获取API实例
38
+
39
+ Args:
40
+ cls: API类
41
+ config_key: 配置键
42
+ """
43
+ _log.debug("读取 %s 的 %s 实例", config_key, cls.__name__)
44
+ with self._lock:
45
+ if config_key not in self._instances[cls]:
46
+ self._instances[cls][config_key] = cls.create(
47
+ self, config_key=config_key
48
+ )
49
+
50
+ return self._instances[cls][config_key]
51
+
52
+ def require_config(self, config_key: str) -> Any:
53
+ """读取必需的配置项,缺失时抛出明确错误.
54
+
55
+ 供 API 的 :meth:`BaseApi.create` 使用。直接用
56
+ ``ctx.config.get_config(key)`` 时缺失的键会静默返回 ``None``,
57
+ 错误会推迟到深处变成一句与配置无关的 ``AttributeError``
58
+ (例如 ``'NoneType' object has no attribute 'token'``)。
59
+
60
+ Args:
61
+ config_key: 配置键
62
+
63
+ Returns:
64
+ 配置对象
65
+
66
+ Raises:
67
+ ConfigError: 配置键不存在或值为 ``None``
68
+ """
69
+ value = self.config.get_config(config_key)
70
+ if value is None:
71
+ raise ConfigError(
72
+ "缺少配置键 '%s'。请在 config.yaml 中添加该键,"
73
+ "或构造 RuntimeConfig(%s=...) 时显式传入。" % (config_key, config_key)
74
+ )
75
+ return value
76
+
77
+ async def aclose_all(self) -> None:
78
+ """关闭并清空所有缓存的 API 单例.
79
+
80
+ 依次 ``await`` 每个实例的 :meth:`BaseApi.aclose`,单个实例关闭失败
81
+ 只记录日志、不影响其余实例,最后统一清空缓存。
82
+ """
83
+ with self._lock:
84
+ instances = [
85
+ inst for by_key in self._instances.values() for inst in by_key.values()
86
+ ]
87
+ self._instances.clear()
88
+
89
+ for inst in instances:
90
+ if not isinstance(inst, BaseApi):
91
+ continue
92
+ try:
93
+ await inst.aclose()
94
+ except Exception:
95
+ _log.exception("关闭 API %s 时出错", type(inst).__name__)
96
+
97
+ if instances:
98
+ _log.debug("已关闭 %s 个 API 实例", len(instances))
99
+
100
+ def clear(self) -> None:
101
+ """清空所有缓存的 API 单例实例,主要用于测试隔离.
102
+
103
+ 只丢弃引用,不会调用 :meth:`BaseApi.aclose`。
104
+ 需要释放连接等资源时请用 :meth:`aclose_all`。
105
+ """
106
+ with self._lock:
107
+ self._instances.clear()
108
+
109
+ get = get_api
@@ -0,0 +1,51 @@
1
+ from __future__ import annotations
2
+
3
+ from butterbot.core.event import EventBus
4
+
5
+ from .api_registry import ApiRegistry
6
+ from .config_provider import ConfigProvider
7
+
8
+
9
+ class AppContext:
10
+ """应用上下文,统一注入对象.
11
+
12
+ 用于将分散的依赖注入整合为一个对象,
13
+ 供 Source 和其他组件使用。
14
+
15
+ Attributes:
16
+ config: 运行时配置(只读)
17
+ api_ctx: API 单例容器
18
+ bus: 事件总线
19
+ """
20
+
21
+ def __init__(
22
+ self,
23
+ config: ConfigProvider,
24
+ event_bus: EventBus | None = None,
25
+ api_ctx: ApiRegistry | None = None,
26
+ ):
27
+ """初始化 AppContext.
28
+
29
+ Args:
30
+ config: 运行时配置
31
+ event_bus: 可选,注入自定义 EventBus,默认自动创建
32
+ api_ctx: 可选,注入自定义 ApiRegistry,默认自动创建
33
+ """
34
+ self._config = config
35
+ self._api_ctx = api_ctx or ApiRegistry(config)
36
+ self._bus = event_bus or EventBus()
37
+
38
+ @property
39
+ def config(self) -> ConfigProvider:
40
+ """获取运行时配置(只读)."""
41
+ return self._config
42
+
43
+ @property
44
+ def api_ctx(self) -> ApiRegistry:
45
+ """获取 API 上下文."""
46
+ return self._api_ctx
47
+
48
+ @property
49
+ def bus(self) -> EventBus:
50
+ """获取事件总线."""
51
+ return self._bus
@@ -0,0 +1,14 @@
1
+ """core 层使用的配置契约."""
2
+
3
+ from typing import Any, Protocol
4
+
5
+
6
+ class ConfigProvider(Protocol):
7
+ """core 组件所需的最小配置接口.
8
+
9
+ core 只依赖按键读取配置;具体加载方式、YAML 解析和构建器注册均属于
10
+ app 层职责。
11
+ """
12
+
13
+ def get_config(self, key: str, default: Any = None) -> Any:
14
+ """返回配置值;键不存在时返回 ``default``."""
@@ -0,0 +1,15 @@
1
+ from .base_data import (
2
+ BaseDataMixin,
3
+ BaseDataT,
4
+ )
5
+ from .base_model import (
6
+ AutoDispatchList,
7
+ BaseDataModel,
8
+ )
9
+
10
+ __all__ = [
11
+ "AutoDispatchList",
12
+ "BaseDataMixin",
13
+ "BaseDataModel",
14
+ "BaseDataT",
15
+ ]
@@ -0,0 +1,29 @@
1
+ from typing import TypeVar
2
+
3
+
4
+ class BaseDataMixin:
5
+ """
6
+ 框架内约束的数据类混入类, 用于标记框架内数据
7
+ """
8
+
9
+ _repr_exclude = {"raw_data"} # 排除在repr中的属性集合
10
+
11
+ def __repr__(self):
12
+ core_properties_str: str = self._get_core_properties_str()
13
+ return "%s(%s)" % (self.__class__.__name__, core_properties_str)
14
+
15
+ def __str__(self):
16
+ return self.__repr__()
17
+
18
+ def _get_core_properties_str(self) -> str:
19
+ excludes = set(getattr(self, "_repr_exclude", ()))
20
+ props = {
21
+ k: v
22
+ for k, v in vars(self).items()
23
+ if not k.startswith("_") and k not in excludes
24
+ }
25
+ parts = ["%s=%r" % (k, v) for k, v in props.items()]
26
+ return ", ".join(parts)
27
+
28
+
29
+ BaseDataT = TypeVar("BaseDataT", bound=BaseDataMixin)