Skip to content

Commit 3968316

Browse files
zouyonghestevessr
authored andcommitted
[codex] fix mcp init timeout keyword mismatch (AstrBotDevs#5743)
* fix: use timeout_seconds for mcp init startup * fix: support overridden mcp init timeout in startup * fix: resolve mcp init timeout from env when unset * fix: pass mcp init timeout through lifecycle chain
1 parent 2c9a4a9 commit 3968316

5 files changed

Lines changed: 148 additions & 25 deletions

File tree

astrbot/core/core_lifecycle.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,11 @@ async def _init_or_reload_subagent_orchestrator(self) -> None:
9797
except Exception as e:
9898
logger.error(f"Subagent orchestrator init failed: {e}", exc_info=True)
9999

100-
async def initialize(self) -> None:
100+
async def initialize(
101+
self,
102+
*,
103+
mcp_init_timeout: float | int | str | None = None,
104+
) -> None:
101105
"""初始化 AstrBot 核心生命周期管理类.
102106
103107
负责初始化各个组件, 包括 ProviderManager、PlatformManager、ConversationManager、PluginManager、PipelineScheduler、EventBus、AstrBotUpdator等。
@@ -201,7 +205,7 @@ async def initialize(self) -> None:
201205
await self.plugin_manager.reload()
202206

203207
# 根据配置实例化各个 Provider
204-
await self.provider_manager.initialize()
208+
await self.provider_manager.initialize(init_timeout=mcp_init_timeout)
205209

206210
await self.kb_manager.initialize()
207211

astrbot/core/provider/func_tool_manager.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -347,7 +347,10 @@ def _log_safe_mcp_debug_config(cfg: dict) -> None:
347347
logger.debug(f" 主机:{scheme}://{host}{port}")
348348

349349
async def init_mcp_clients(
350-
self, raise_on_all_failed: bool = False
350+
self,
351+
raise_on_all_failed: bool = False,
352+
*,
353+
init_timeout: float | int | str | None = None,
351354
) -> MCPInitSummary:
352355
"""从项目根目录读取 mcp_server.json 文件,初始化 MCP 服务列表。文件格式如下:
353356
```
@@ -368,6 +371,7 @@ async def init_mcp_clients(
368371
```
369372
370373
Timeout behavior:
374+
- 显式 `init_timeout` 参数优先(用于测试或调用方覆盖)。
371375
- 初始化超时使用环境变量 ASTRBOT_MCP_INIT_TIMEOUT 或默认值。
372376
- 动态启用超时使用 ASTRBOT_MCP_ENABLE_TIMEOUT(独立于初始化超时)。
373377
"""
@@ -393,8 +397,12 @@ async def init_mcp_clients(
393397
"mcpServers"
394398
]
395399

396-
init_timeout = self._init_timeout_default
397-
timeout_display = f"{init_timeout:g}"
400+
init_timeout_value = _resolve_timeout(
401+
timeout=init_timeout,
402+
env_name=MCP_INIT_TIMEOUT_ENV,
403+
default=self._init_timeout_default,
404+
)
405+
timeout_display = f"{init_timeout_value:g}"
398406

399407
active_configs: list[tuple[str, dict, asyncio.Event]] = []
400408
for name, cfg in mcp_server_json_obj.items():
@@ -413,7 +421,7 @@ async def init_mcp_clients(
413421
name=name,
414422
cfg=cfg,
415423
shutdown_event=shutdown_event,
416-
timeout=init_timeout,
424+
timeout_seconds=init_timeout_value,
417425
),
418426
name=f"mcp-init:{name}",
419427
)

astrbot/core/provider/manager.py

Lines changed: 30 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ def __init__(
6363
str,
6464
Providers,
6565
] = {}
66-
"""Provider 实例映射. key: provider_id, value: Provider 实例"""
66+
"""Provider 实例映射key: provider_id, value: Provider 实例"""
6767
self.llm_tools = llm_tools
6868

6969
self.curr_provider_inst: Provider | None = None
@@ -107,7 +107,7 @@ def _notify_provider_changed(
107107
self._provider_change_callback(provider_id, provider_type, umo)
108108
except Exception as e:
109109
logger.warning(
110-
"调用 provider 变更回调失败: provider_id=%s, type=%s, err=%s",
110+
"调用 provider 变更回调失败provider_id=%s, type=%s, err=%s",
111111
provider_id,
112112
provider_type,
113113
safe_error("", e),
@@ -119,7 +119,7 @@ def _notify_provider_changed(
119119
hook(provider_id, provider_type, umo)
120120
except Exception as e:
121121
logger.warning(
122-
"调用 provider 变更钩子失败: provider_id=%s, type=%s, err=%s",
122+
"调用 provider 变更钩子失败provider_id=%s, type=%s, err=%s",
123123
provider_id,
124124
provider_type,
125125
safe_error("", e),
@@ -270,7 +270,11 @@ def get_using_provider(
270270

271271
return provider
272272

273-
async def initialize(self) -> None:
273+
async def initialize(
274+
self,
275+
*,
276+
init_timeout: float | int | str | None = None,
277+
) -> None:
274278
# 逐个初始化提供商
275279
for provider_config in self.providers_config:
276280
try:
@@ -331,16 +335,25 @@ async def initialize(self) -> None:
331335
if not self.curr_tts_provider_inst and self.tts_provider_insts:
332336
self.curr_tts_provider_inst = self.tts_provider_insts[0]
333337

334-
async def _init_mcp_clients_bg() -> None:
335-
try:
336-
await self.llm_tools.init_mcp_clients()
337-
except Exception:
338-
logger.error("MCP init background task failed", exc_info=True)
339-
340-
if self._mcp_init_task is None or self._mcp_init_task.done():
341-
self._mcp_init_task = asyncio.create_task(
342-
_init_mcp_clients_bg(),
343-
name="provider-manager:mcp-init",
338+
# 初始化 MCP Client 连接(等待完成以确保工具可用)
339+
strict_mcp_init = os.getenv("ASTRBOT_MCP_INIT_STRICT", "").strip().lower() in {
340+
"1",
341+
"true",
342+
"yes",
343+
"on",
344+
}
345+
mcp_init_summary = await self.llm_tools.init_mcp_clients(
346+
raise_on_all_failed=strict_mcp_init,
347+
init_timeout=init_timeout,
348+
)
349+
if (
350+
mcp_init_summary.total > 0
351+
and mcp_init_summary.success == 0
352+
and not strict_mcp_init
353+
):
354+
logger.warning(
355+
"MCP 服务全部初始化失败,系统将继续启动(可设置 "
356+
"ASTRBOT_MCP_INIT_STRICT=1 以在此场景下中止启动)。"
344357
)
345358

346359
def dynamic_import_provider(self, type: str) -> None:
@@ -652,7 +665,7 @@ async def load_provider(self, provider_config: dict) -> None:
652665
await inst.initialize()
653666
self.rerank_provider_insts.append(inst)
654667
case _:
655-
# 未知供应商抛出异常,确保inst初始化
668+
# 未知供应商抛出异常,确保 inst 初始化
656669
# Should be unreachable
657670
raise Exception(
658671
f"未知的提供商类型:{provider_metadata.provider_type}"
@@ -716,7 +729,7 @@ def get_insts(self):
716729
async def terminate_provider(self, provider_id: str) -> None:
717730
if provider_id in self.inst_map:
718731
logger.info(
719-
f"终止 {provider_id} 提供商适配器({len(self.provider_insts)}, {len(self.stt_provider_insts)}, {len(self.tts_provider_insts)}) ...",
732+
f"终止 {provider_id} 提供商适配器 ({len(self.provider_insts)}, {len(self.stt_provider_insts)}, {len(self.tts_provider_insts)}) ...",
720733
)
721734

722735
if self.inst_map[provider_id] in self.provider_insts:
@@ -743,7 +756,7 @@ async def terminate_provider(self, provider_id: str) -> None:
743756
await self.inst_map[provider_id].terminate() # type: ignore
744757

745758
logger.info(
746-
f"{provider_id} 提供商适配器已终止({len(self.provider_insts)}, {len(self.stt_provider_insts)}, {len(self.tts_provider_insts)})",
759+
f"{provider_id} 提供商适配器已终止 ({len(self.provider_insts)}, {len(self.stt_provider_insts)}, {len(self.tts_provider_insts)})",
747760
)
748761
del self.inst_map[provider_id]
749762

tests/unit/test_core_lifecycle.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -373,7 +373,7 @@ async def test_initialize_sets_up_all_components(
373373
new_callable=AsyncMock,
374374
),
375375
):
376-
await lifecycle.initialize()
376+
await lifecycle.initialize(mcp_init_timeout=3.5)
377377

378378
# Verify database initialized
379379
mock_db.initialize.assert_awaited_once()
@@ -388,7 +388,7 @@ async def test_initialize_sets_up_all_components(
388388
mock_persona_mgr.initialize.assert_awaited_once()
389389

390390
# Verify provider manager initialized
391-
mock_provider_manager.initialize.assert_awaited_once()
391+
mock_provider_manager.initialize.assert_awaited_once_with(init_timeout=3.5)
392392

393393
# Verify platform manager initialized
394394
mock_platform_manager.initialize.assert_awaited_once()
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import json
2+
3+
import pytest
4+
5+
from astrbot.core.provider import func_tool_manager
6+
from astrbot.core.provider.func_tool_manager import FunctionToolManager
7+
8+
9+
@pytest.fixture
10+
def mcp_init_harness(
11+
monkeypatch: pytest.MonkeyPatch,
12+
tmp_path,
13+
):
14+
manager = FunctionToolManager()
15+
data_dir = tmp_path / "data"
16+
data_dir.mkdir()
17+
18+
(data_dir / "mcp_server.json").write_text(
19+
json.dumps({"mcpServers": {"demo": {"active": True}}}),
20+
encoding="utf-8",
21+
)
22+
monkeypatch.setattr(
23+
func_tool_manager,
24+
"get_astrbot_data_path",
25+
lambda: data_dir,
26+
)
27+
28+
called = {}
29+
30+
async def fake_start_mcp_server(*, name, cfg, shutdown_event, timeout_seconds):
31+
called[name] = {
32+
"cfg": cfg,
33+
"shutdown_event_type": type(shutdown_event).__name__,
34+
"timeout_seconds": timeout_seconds,
35+
}
36+
37+
monkeypatch.setattr(manager, "_start_mcp_server", fake_start_mcp_server)
38+
return manager, called
39+
40+
41+
def assert_demo_init_result(summary, called, *, timeout_seconds: float) -> None:
42+
assert summary.total == 1
43+
assert summary.success == 1
44+
assert summary.failed == []
45+
assert called["demo"]["cfg"] == {"active": True}
46+
assert called["demo"]["shutdown_event_type"] == "Event"
47+
assert called["demo"]["timeout_seconds"] == timeout_seconds
48+
49+
50+
@pytest.mark.asyncio
51+
async def test_init_mcp_clients_passes_timeout_seconds_keyword(mcp_init_harness):
52+
manager, called = mcp_init_harness
53+
54+
summary = await manager.init_mcp_clients()
55+
56+
assert_demo_init_result(
57+
summary,
58+
called,
59+
timeout_seconds=manager._init_timeout_default,
60+
)
61+
62+
63+
@pytest.mark.asyncio
64+
async def test_init_mcp_clients_passes_overridden_init_timeout(
65+
mcp_init_harness,
66+
):
67+
manager, called = mcp_init_harness
68+
69+
summary = await manager.init_mcp_clients(init_timeout=3.5)
70+
71+
assert_demo_init_result(summary, called, timeout_seconds=3.5)
72+
73+
74+
@pytest.mark.asyncio
75+
async def test_init_mcp_clients_reads_env_timeout_when_not_overridden(
76+
mcp_init_harness,
77+
monkeypatch: pytest.MonkeyPatch,
78+
):
79+
manager, called = mcp_init_harness
80+
manager._init_timeout_default = 20.0 # ensure env override is observable
81+
monkeypatch.setenv("ASTRBOT_MCP_INIT_TIMEOUT", "3.5")
82+
83+
summary = await manager.init_mcp_clients()
84+
85+
assert_demo_init_result(summary, called, timeout_seconds=3.5)
86+
87+
88+
@pytest.mark.asyncio
89+
async def test_init_mcp_clients_prefers_explicit_timeout_over_env(
90+
mcp_init_harness,
91+
monkeypatch: pytest.MonkeyPatch,
92+
):
93+
manager, called = mcp_init_harness
94+
monkeypatch.setenv("ASTRBOT_MCP_INIT_TIMEOUT", "7.0")
95+
96+
summary = await manager.init_mcp_clients(init_timeout=3.5)
97+
98+
assert_demo_init_result(summary, called, timeout_seconds=3.5)

0 commit comments

Comments
 (0)