Skip to content

Commit 9294b44

Browse files
whatevertogoclaude
andauthored
fix: resolve pipeline and star import cycles (AstrBotDevs#5353)
* fix: resolve pipeline and star import cycles - Add bootstrap.py and stage_order.py to break circular dependencies - Export Context, PluginManager, StarTools from star module - Update pipeline __init__ to defer imports - Split pipeline initialization into separate bootstrap module Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: add logging for get_config() failure in Star class * fix: reorder logger initialization in base.py --------- Co-authored-by: whatevertogo <whatevertogo@users.noreply.github.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 80fd511 commit 9294b44

13 files changed

Lines changed: 266 additions & 101 deletions

File tree

astrbot/core/pipeline/__init__.py

Lines changed: 67 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,60 @@
1+
"""Pipeline package exports.
2+
3+
This module intentionally avoids eager imports of all pipeline stage modules to
4+
prevent import-time cycles. Stage classes remain available via lazy attribute
5+
resolution for backward compatibility.
6+
"""
7+
8+
from __future__ import annotations
9+
10+
from importlib import import_module
11+
from typing import Any
12+
113
from astrbot.core.message.message_event_result import (
214
EventResultType,
315
MessageEventResult,
416
)
517

6-
from .content_safety_check.stage import ContentSafetyCheckStage
7-
from .preprocess_stage.stage import PreProcessStage
8-
from .process_stage.stage import ProcessStage
9-
from .rate_limit_check.stage import RateLimitStage
10-
from .respond.stage import RespondStage
11-
from .result_decorate.stage import ResultDecorateStage
12-
from .session_status_check.stage import SessionStatusCheckStage
13-
from .waking_check.stage import WakingCheckStage
14-
from .whitelist_check.stage import WhitelistCheckStage
15-
16-
# 管道阶段顺序
17-
STAGES_ORDER = [
18-
"WakingCheckStage", # 检查是否需要唤醒
19-
"WhitelistCheckStage", # 检查是否在群聊/私聊白名单
20-
"SessionStatusCheckStage", # 检查会话是否整体启用
21-
"RateLimitStage", # 检查会话是否超过频率限制
22-
"ContentSafetyCheckStage", # 检查内容安全
23-
"PreProcessStage", # 预处理
24-
"ProcessStage", # 交由 Stars 处理(a.k.a 插件),或者 LLM 调用
25-
"ResultDecorateStage", # 处理结果,比如添加回复前缀、t2i、转换为语音 等
26-
"RespondStage", # 发送消息
27-
]
18+
from .stage_order import STAGES_ORDER
19+
20+
_LAZY_EXPORTS = {
21+
"ContentSafetyCheckStage": (
22+
"astrbot.core.pipeline.content_safety_check.stage",
23+
"ContentSafetyCheckStage",
24+
),
25+
"PreProcessStage": (
26+
"astrbot.core.pipeline.preprocess_stage.stage",
27+
"PreProcessStage",
28+
),
29+
"ProcessStage": (
30+
"astrbot.core.pipeline.process_stage.stage",
31+
"ProcessStage",
32+
),
33+
"RateLimitStage": (
34+
"astrbot.core.pipeline.rate_limit_check.stage",
35+
"RateLimitStage",
36+
),
37+
"RespondStage": (
38+
"astrbot.core.pipeline.respond.stage",
39+
"RespondStage",
40+
),
41+
"ResultDecorateStage": (
42+
"astrbot.core.pipeline.result_decorate.stage",
43+
"ResultDecorateStage",
44+
),
45+
"SessionStatusCheckStage": (
46+
"astrbot.core.pipeline.session_status_check.stage",
47+
"SessionStatusCheckStage",
48+
),
49+
"WakingCheckStage": (
50+
"astrbot.core.pipeline.waking_check.stage",
51+
"WakingCheckStage",
52+
),
53+
"WhitelistCheckStage": (
54+
"astrbot.core.pipeline.whitelist_check.stage",
55+
"WhitelistCheckStage",
56+
),
57+
}
2858

2959
__all__ = [
3060
"ContentSafetyCheckStage",
@@ -36,6 +66,21 @@
3666
"RespondStage",
3767
"ResultDecorateStage",
3868
"SessionStatusCheckStage",
69+
"STAGES_ORDER",
3970
"WakingCheckStage",
4071
"WhitelistCheckStage",
4172
]
73+
74+
75+
def __getattr__(name: str) -> Any:
76+
if name not in _LAZY_EXPORTS:
77+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
78+
module_path, attr_name = _LAZY_EXPORTS[name]
79+
module = import_module(module_path)
80+
value = getattr(module, attr_name)
81+
globals()[name] = value
82+
return value
83+
84+
85+
def __dir__() -> list[str]:
86+
return sorted(set(globals()) | set(__all__))

astrbot/core/pipeline/bootstrap.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
"""Pipeline bootstrap utilities."""
2+
3+
from importlib import import_module
4+
5+
from .stage import registered_stages
6+
7+
_BUILTIN_STAGE_MODULES = (
8+
"astrbot.core.pipeline.waking_check.stage",
9+
"astrbot.core.pipeline.whitelist_check.stage",
10+
"astrbot.core.pipeline.session_status_check.stage",
11+
"astrbot.core.pipeline.rate_limit_check.stage",
12+
"astrbot.core.pipeline.content_safety_check.stage",
13+
"astrbot.core.pipeline.preprocess_stage.stage",
14+
"astrbot.core.pipeline.process_stage.stage",
15+
"astrbot.core.pipeline.result_decorate.stage",
16+
"astrbot.core.pipeline.respond.stage",
17+
)
18+
19+
_EXPECTED_STAGE_NAMES = {
20+
"WakingCheckStage",
21+
"WhitelistCheckStage",
22+
"SessionStatusCheckStage",
23+
"RateLimitStage",
24+
"ContentSafetyCheckStage",
25+
"PreProcessStage",
26+
"ProcessStage",
27+
"ResultDecorateStage",
28+
"RespondStage",
29+
}
30+
31+
_builtin_stages_registered = False
32+
33+
34+
def ensure_builtin_stages_registered() -> None:
35+
"""Ensure built-in pipeline stages are imported and registered."""
36+
global _builtin_stages_registered
37+
38+
if _builtin_stages_registered:
39+
return
40+
41+
stage_names = {stage_cls.__name__ for stage_cls in registered_stages}
42+
if _EXPECTED_STAGE_NAMES.issubset(stage_names):
43+
_builtin_stages_registered = True
44+
return
45+
46+
for module_path in _BUILTIN_STAGE_MODULES:
47+
import_module(module_path)
48+
49+
_builtin_stages_registered = True
50+
51+
52+
__all__ = ["ensure_builtin_stages_registered"]

astrbot/core/pipeline/context.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1+
from __future__ import annotations
2+
13
from dataclasses import dataclass
4+
from typing import Any
25

36
from astrbot.core.config import AstrBotConfig
4-
from astrbot.core.star import PluginManager
57

68
from .context_utils import call_event_hook, call_handler
79

@@ -11,7 +13,7 @@ class PipelineContext:
1113
"""上下文对象,包含管道执行所需的上下文信息"""
1214

1315
astrbot_config: AstrBotConfig # AstrBot 配置对象
14-
plugin_manager: PluginManager # 插件管理器对象
16+
plugin_manager: Any # 插件管理器对象
1517
astrbot_config_id: str
1618
call_handler = call_handler
1719
call_event_hook = call_event_hook

astrbot/core/pipeline/process_stage/method/agent_sub_stages/internal.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
MessageEventResult,
2020
ResultContentType,
2121
)
22+
from astrbot.core.pipeline.stage import Stage
2223
from astrbot.core.platform.astr_message_event import AstrMessageEvent
2324
from astrbot.core.provider.entities import (
2425
LLMResponse,
@@ -30,7 +31,6 @@
3031

3132
from .....astr_agent_run_util import run_agent, run_live_agent
3233
from ....context import PipelineContext, call_event_hook
33-
from ...stage import Stage
3434

3535

3636
class InternalAgentSubStage(Stage):

astrbot/core/pipeline/process_stage/method/agent_sub_stages/third_party.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
DashscopeAgentRunner,
99
)
1010
from astrbot.core.agent.runners.dify.dify_agent_runner import DifyAgentRunner
11+
from astrbot.core.astr_agent_hooks import MAIN_AGENT_HOOKS
1112
from astrbot.core.message.components import Image
1213
from astrbot.core.message.message_event_result import (
1314
MessageChain,
@@ -17,6 +18,7 @@
1718

1819
if TYPE_CHECKING:
1920
from astrbot.core.agent.runners.base import BaseAgentRunner
21+
from astrbot.core.pipeline.stage import Stage
2022
from astrbot.core.platform.astr_message_event import AstrMessageEvent
2123
from astrbot.core.provider.entities import (
2224
ProviderRequest,
@@ -25,9 +27,7 @@
2527
from astrbot.core.utils.metrics import Metric
2628

2729
from .....astr_agent_context import AgentContextWrapper, AstrAgentContext
28-
from .....astr_agent_hooks import MAIN_AGENT_HOOKS
2930
from ....context import PipelineContext, call_event_hook
30-
from ...stage import Stage
3131

3232
AGENT_RUNNER_TYPE_KEY = {
3333
"dify": "dify_agent_runner_provider_id",

astrbot/core/pipeline/scheduler.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,17 @@
88
)
99
from astrbot.core.utils.active_event_registry import active_event_registry
1010

11-
from . import STAGES_ORDER
11+
from .bootstrap import ensure_builtin_stages_registered
1212
from .context import PipelineContext
1313
from .stage import registered_stages
14+
from .stage_order import STAGES_ORDER
1415

1516

1617
class PipelineScheduler:
1718
"""管道调度器,负责调度各个阶段的执行"""
1819

1920
def __init__(self, context: PipelineContext) -> None:
21+
ensure_builtin_stages_registered()
2022
registered_stages.sort(
2123
key=lambda x: STAGES_ORDER.index(x.__name__),
2224
) # 按照顺序排序
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
"""Pipeline stage execution order."""
2+
3+
STAGES_ORDER = [
4+
"WakingCheckStage", # 检查是否需要唤醒
5+
"WhitelistCheckStage", # 检查是否在群聊/私聊白名单
6+
"SessionStatusCheckStage", # 检查会话是否整体启用
7+
"RateLimitStage", # 检查会话是否超过频率限制
8+
"ContentSafetyCheckStage", # 检查内容安全
9+
"PreProcessStage", # 预处理
10+
"ProcessStage", # 交由 Stars 处理(a.k.a 插件),或者 LLM 调用
11+
"ResultDecorateStage", # 处理结果,比如添加回复前缀、t2i、转换为语音 等
12+
"RespondStage", # 发送消息
13+
]
14+
15+
__all__ = ["STAGES_ORDER"]

astrbot/core/star/__init__.py

Lines changed: 14 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,68 +1,19 @@
1-
from astrbot.core import html_renderer
1+
# 兼容导出: Provider 从 provider 模块重新导出
22
from astrbot.core.provider import Provider
3-
from astrbot.core.star.star_tools import StarTools
4-
from astrbot.core.utils.command_parser import CommandParserMixin
5-
from astrbot.core.utils.plugin_kv_store import PluginKVStoreMixin
63

4+
from .base import Star
75
from .context import Context
86
from .star import StarMetadata, star_map, star_registry
97
from .star_manager import PluginManager
10-
11-
12-
class Star(CommandParserMixin, PluginKVStoreMixin):
13-
"""所有插件(Star)的父类,所有插件都应该继承于这个类"""
14-
15-
author: str
16-
name: str
17-
18-
def __init__(self, context: Context, config: dict | None = None) -> None:
19-
StarTools.initialize(context)
20-
self.context = context
21-
22-
def __init_subclass__(cls, **kwargs):
23-
super().__init_subclass__(**kwargs)
24-
if not star_map.get(cls.__module__):
25-
metadata = StarMetadata(
26-
star_cls_type=cls,
27-
module_path=cls.__module__,
28-
)
29-
star_map[cls.__module__] = metadata
30-
star_registry.append(metadata)
31-
else:
32-
star_map[cls.__module__].star_cls_type = cls
33-
star_map[cls.__module__].module_path = cls.__module__
34-
35-
async def text_to_image(self, text: str, return_url=True) -> str:
36-
"""将文本转换为图片"""
37-
return await html_renderer.render_t2i(
38-
text,
39-
return_url=return_url,
40-
template_name=self.context._config.get("t2i_active_template"),
41-
)
42-
43-
async def html_render(
44-
self,
45-
tmpl: str,
46-
data: dict,
47-
return_url=True,
48-
options: dict | None = None,
49-
) -> str:
50-
"""渲染 HTML"""
51-
return await html_renderer.render_custom_template(
52-
tmpl,
53-
data,
54-
return_url=return_url,
55-
options=options,
56-
)
57-
58-
async def initialize(self) -> None:
59-
"""当插件被激活时会调用这个方法"""
60-
61-
async def terminate(self) -> None:
62-
"""当插件被禁用、重载插件时会调用这个方法"""
63-
64-
def __del__(self) -> None:
65-
"""[Deprecated] 当插件被禁用、重载插件时会调用这个方法"""
66-
67-
68-
__all__ = ["Context", "PluginManager", "Provider", "Star", "StarMetadata", "StarTools"]
8+
from .star_tools import StarTools
9+
10+
__all__ = [
11+
"Context",
12+
"PluginManager",
13+
"Provider",
14+
"Star",
15+
"StarMetadata",
16+
"StarTools",
17+
"star_map",
18+
"star_registry",
19+
]

0 commit comments

Comments
 (0)