diff --git a/astrbot-sdk/pyproject.toml b/astrbot-sdk/pyproject.toml index f90d4aed3..76bf9dd62 100644 --- a/astrbot-sdk/pyproject.toml +++ b/astrbot-sdk/pyproject.toml @@ -25,7 +25,7 @@ dependencies = [ [project.scripts] astr = "astrbot_sdk.cli:cli" -astrbot-sdk = "astrbot_sdk.cli:cli" + [tool.pytest.ini_options] markers = [ diff --git a/astrbot/core/__init__.py b/astrbot/core/__init__.py index ae52cf5d4..9db2fa68c 100644 --- a/astrbot/core/__init__.py +++ b/astrbot/core/__init__.py @@ -4,7 +4,38 @@ import os from importlib import import_module from typing import TYPE_CHECKING, Any -from .utils.astrbot_path import get_astrbot_data_path +from astrbot.core.config import AstrBotConfig +from astrbot.core.config.default import DB_PATH +from astrbot.core.db.sqlite import SQLiteDatabase +from astrbot.core.file_token_service import FileTokenService +from astrbot.core.utils.pip_installer import ( + DependencyConflictError as DependencyConflictError, +) +from astrbot.core.utils.pip_installer import ( + PipInstaller, +) +from astrbot.core.utils.requirements_utils import ( + RequirementsPrecheckFailed as RequirementsPrecheckFailed, +) +from astrbot.core.utils.requirements_utils import ( + find_missing_requirements as find_missing_requirements, +) +from astrbot.core.utils.requirements_utils import ( + find_missing_requirements_or_raise as find_missing_requirements_or_raise, +) +from astrbot.core.utils.shared_preferences import SharedPreferences +from astrbot.core.utils.t2i.renderer import HtmlRenderer + +from .log import LogBroker, LogManager +from .utils.astrbot_path import ( + get_astrbot_config_path, + get_astrbot_data_path, + get_astrbot_knowledge_base_path, + get_astrbot_plugin_path, + get_astrbot_site_packages_path, + get_astrbot_skills_path, + get_astrbot_temp_path, +) if TYPE_CHECKING: from .config import AstrBotConfig @@ -40,53 +71,6 @@ os.makedirs(get_astrbot_data_path(), exist_ok=True) DEMO_MODE = os.getenv("DEMO_MODE", "False").strip().lower() in ("true", "1", "t") -__all__ = [ - "AstrBotConfig", - "DEMO_MODE", - "DependencyConflictError", - "FileTokenService", - "LogBroker", - "LogManager", - "PipInstaller", - "RequirementsPrecheckFailed", - "SQLiteDatabase", - "astrbot_config", - "db_helper", - "file_token_service", - "find_missing_requirements", - "find_missing_requirements_or_raise", -from astrbot.core.config import AstrBotConfig -from astrbot.core.config.default import DB_PATH -from astrbot.core.db.sqlite import SQLiteDatabase -from astrbot.core.file_token_service import FileTokenService -from astrbot.core.utils.pip_installer import ( - DependencyConflictError as DependencyConflictError, -) -from astrbot.core.utils.pip_installer import ( - PipInstaller, -) -from astrbot.core.utils.requirements_utils import ( - RequirementsPrecheckFailed as RequirementsPrecheckFailed, -) -from astrbot.core.utils.requirements_utils import ( - find_missing_requirements as find_missing_requirements, -) -from astrbot.core.utils.requirements_utils import ( - find_missing_requirements_or_raise as find_missing_requirements_or_raise, -) -from astrbot.core.utils.shared_preferences import SharedPreferences -from astrbot.core.utils.t2i.renderer import HtmlRenderer - -from .log import LogBroker, LogManager -from .utils.astrbot_path import ( - get_astrbot_config_path, - get_astrbot_data_path, - get_astrbot_knowledge_base_path, - get_astrbot_plugin_path, - get_astrbot_site_packages_path, - get_astrbot_skills_path, - get_astrbot_temp_path, -) # Initialize required data directories eagerly so later agent/tool flows do not # fail on missing paths when the runtime root resolves to a fresh location. @@ -101,8 +85,6 @@ for required_dir in ( ): os.makedirs(required_dir, exist_ok=True) -DEMO_MODE = os.getenv("DEMO_MODE", "False").strip().lower() in ("true", "1", "t") - astrbot_config = AstrBotConfig() t2i_base_url = astrbot_config.get("t2i_endpoint", "https://t2i.soulter.top/text2img") html_renderer = HtmlRenderer(t2i_base_url) @@ -120,19 +102,6 @@ pip_installer = PipInstaller( astrbot_config.get("pip_install_arg", ""), astrbot_config.get("pypi_index_url", None), ) -__all__ = [ - "DEMO_MODE", - "AstrBotConfig", - "LogBroker", - "LogManager", - "astrbot_config", - "db_helper", - "file_token_service", - "html_renderer", - "logger", - "pip_installer", - "sp", -] _SINGLETON_CACHE: dict[str, Any] = {} @@ -256,5 +225,3 @@ def __getattr__(name: str) -> Any: if name == "pip_installer": return _get_pip_installer() raise AttributeError(name) - "t2i_base_url", -] diff --git a/astrbot/core/astr_agent_run_util.py b/astrbot/core/astr_agent_run_util.py index 113ca67c1..d0abb68c9 100644 --- a/astrbot/core/astr_agent_run_util.py +++ b/astrbot/core/astr_agent_run_util.py @@ -183,12 +183,12 @@ async def run_agent( continue elif resp.type == "tool_call": if agent_runner.streaming and show_tool_use: - # 向下游平台发送 "break" 分段信号(空 MessageChain,不携带数据)。 - # 平台适配器收到后会关闭当前流式消息,并在后续文本到来时创建新消息。 - # 仅在 show_tool_use 为 True 时才发送:此时紧接着会通过 - # astr_event.send() 独立发送工具状态消息(如"🔨 调用工具: xxx"), - # 需要分段才能保证消息顺序正确。 - # 若 show_tool_use 为 False,不会有独立消息插入,无需分段。 + # 向下游平台发送 "break" 分段信号(空 MessageChain,不携带数据)。 + # 平台适配器收到后会关闭当前流式消息,并在后续文本到来时创建新消息。 + # 仅在 show_tool_use 为 True 时才发送:此时紧接着会通过 + # astr_event.send() 独立发送工具状态消息(如"🔨 调用工具: xxx"), + # 需要分段才能保证消息顺序正确。 + # 若 show_tool_use 为 False,不会有独立消息插入,无需分段。 yield MessageChain(chain=[], type="break") tool_info = _extract_chain_json_data(resp.data["chain"]) diff --git a/astrbot/core/computer/computer_client.py b/astrbot/core/computer/computer_client.py index 5103ebff0..e08a86fcf 100644 --- a/astrbot/core/computer/computer_client.py +++ b/astrbot/core/computer/computer_client.py @@ -27,6 +27,17 @@ local_booter: ComputerBooter | None = None _MANAGED_SKILLS_FILE = ".astrbot_managed_skills.json" +def _list_local_skill_dirs(skills_root: Path) -> list[Path]: + skills: list[Path] = [] + for entry in sorted(skills_root.iterdir()): + if not entry.is_dir(): + continue + skill_md = entry / "SKILL.md" + if skill_md.exists(): + skills.append(entry) + return skills + + def _discover_bay_credentials(endpoint: str) -> str: """Try to auto-discover Bay API key from credentials.json. diff --git a/astrbot/core/log.py b/astrbot/core/log.py index d2312b71a..2b60c32c6 100644 --- a/astrbot/core/log.py +++ b/astrbot/core/log.py @@ -94,7 +94,7 @@ _loguru = _raw_loguru_logger.patch(_patch_record) class _SSLDebugFilter(logging.Filter): - """将特定 SSL 错误降级为 DEBUG 级别,避免日志刷屏。""" + """将特定 SSL 错误降级为 DEBUG 级别,避免日志刷屏。""" _SSL_IGNORE_PATTERNS = ( "APPLICATION_DATA_AFTER_CLOSE_NOTIFY", diff --git a/astrbot/core/pipeline/result_decorate/stage.py b/astrbot/core/pipeline/result_decorate/stage.py index 6b820fc68..16e75b2da 100644 --- a/astrbot/core/pipeline/result_decorate/stage.py +++ b/astrbot/core/pipeline/result_decorate/stage.py @@ -224,7 +224,7 @@ class ResultDecorateStage(Stage): logger.info("流式输出已启用,跳过结果装饰阶段") return - # 需要再获取一次。插件可能直接对 chain 进行了替换。 + # 需要再获取一次。插件可能直接对 chain 进行了替换。 result = self._get_effective_result(event) if result is None: return diff --git a/astrbot/core/platform/sources/discord/discord_platform_adapter.py b/astrbot/core/platform/sources/discord/discord_platform_adapter.py index cd0f129be..ad9c0de24 100644 --- a/astrbot/core/platform/sources/discord/discord_platform_adapter.py +++ b/astrbot/core/platform/sources/discord/discord_platform_adapter.py @@ -441,7 +441,7 @@ class DiscordPlatformAdapter(Platform): raise def collect_commands(self) -> list[tuple[str, str]]: - """收集 legacy 与 SDK 的顶层原生命令。""" + """收集 legacy 与 SDK 的顶层原生命令。""" command_dict: dict[str, str] = {} for handler_md in star_handlers_registry: @@ -456,7 +456,7 @@ class DiscordPlatformAdapter(Platform): cmd_name, description, _cmd_filter_instance = cmd_info if cmd_name in command_dict: logger.warning( - f"命令名 '{cmd_name}' 重复注册,将使用首次注册的定义: " + f"命令名 '{cmd_name}' 重复注册,将使用首次注册的定义: " f"'{command_dict[cmd_name]}'" ) command_dict.setdefault(cmd_name, description) @@ -480,7 +480,7 @@ class DiscordPlatformAdapter(Platform): description = f"{description[:97]}..." if cmd_name in command_dict: logger.warning( - f"命令名 '{cmd_name}' 重复注册,将使用首次注册的定义: " + f"命令名 '{cmd_name}' 重复注册,将使用首次注册的定义: " f"'{command_dict[cmd_name]}'" ) command_dict.setdefault(cmd_name, description) diff --git a/astrbot/core/platform/sources/telegram/tg_adapter.py b/astrbot/core/platform/sources/telegram/tg_adapter.py index 20b9c1c77..53a0d16f4 100644 --- a/astrbot/core/platform/sources/telegram/tg_adapter.py +++ b/astrbot/core/platform/sources/telegram/tg_adapter.py @@ -269,7 +269,7 @@ class TelegramPlatformAdapter(Platform): if cmd_name in command_dict: logger.warning( - f"命令名 '{cmd_name}' 重复注册,将使用首次注册的定义: " + f"命令名 '{cmd_name}' 重复注册,将使用首次注册的定义: " f"'{command_dict[cmd_name]}'" ) command_dict.setdefault(cmd_name, description) diff --git a/astrbot/core/skills/skill_manager.py b/astrbot/core/skills/skill_manager.py index 6ddd184b7..ba72631a7 100644 --- a/astrbot/core/skills/skill_manager.py +++ b/astrbot/core/skills/skill_manager.py @@ -14,7 +14,13 @@ from pathlib import Path, PurePosixPath import yaml -from astrbot.core.utils.astrbot_path import AstrbotPaths, astrbot_paths +from astrbot.core.utils.astrbot_path import ( + AstrbotPaths, + astrbot_paths, + get_astrbot_data_path, + get_astrbot_skills_path, + get_astrbot_temp_path, +) SKILLS_CONFIG_FILENAME = "skills.json" SANDBOX_SKILLS_CACHE_FILENAME = "sandbox_skills_cache.json" @@ -104,8 +110,8 @@ class LocalSkillSource: plugin_id: str | None = None -def _parse_frontmatter_description(text: str) -> str: - """Extract the ``description`` value from YAML frontmatter. +def _parse_frontmatter(text: str) -> dict: + """Parse YAML frontmatter from SKILL.md content. Expects the standard SKILL.md format used by OpenAI Codex CLI and Anthropic Claude Skills:: @@ -141,6 +147,13 @@ def _parse_frontmatter_description(text: str) -> str: return payload +def _parse_frontmatter_description(text: str) -> str: + """Extract the ``description`` value from YAML frontmatter.""" + payload = _parse_frontmatter(text) + description = payload.get("description", "") if isinstance(payload, dict) else "" + return description if isinstance(description, str) else "" + + # Regex for sanitizing paths used in prompt examples — only allow # safe path characters to prevent prompt injection via crafted skill paths. _SAFE_PATH_RE = re.compile(r"[^\w./ ,()'\-]", re.UNICODE) @@ -290,7 +303,10 @@ class SkillManager: self.skills_root = skills_root or str(self.astrbot_paths.skills) self.config_path = str(self.astrbot_paths.config / SKILLS_CONFIG_FILENAME) self.sandbox_skills_cache_path = str( - self.astrbot_paths.data / SANDBOX_SKILLS_CACHE_FILENAME + Path(get_astrbot_data_path()) / SANDBOX_SKILLS_CACHE_FILENAME + ) + self.sdk_plugin_skills_path = str( + Path(get_astrbot_data_path()) / SDK_PLUGIN_SKILLS_FILENAME ) os.makedirs(self.skills_root, exist_ok=True) @@ -652,7 +668,7 @@ class SkillManager: skill_name ) or _default_sandbox_skill_path(skill_name) else: - path_str = str(source.skill_md_path) + path_str = str(skill_md) path_str = path_str.replace("\\", "/") skills_by_name[skill_name] = SkillInfo( name=skill_name, @@ -710,6 +726,7 @@ class SkillManager: return [skills_by_name[name] for name in sorted(skills_by_name)] def is_sandbox_only_skill(self, name: str) -> bool: + skill_md_exists = False if self.get_local_skill_source(name) is not None: skill_dir = Path(self.skills_root) / name skill_md_exists = _normalize_skill_markdown_path(skill_dir) is not None diff --git a/astrbot/core/star/context.py b/astrbot/core/star/context.py index e80e3a414..a18610272 100644 --- a/astrbot/core/star/context.py +++ b/astrbot/core/star/context.py @@ -352,7 +352,7 @@ class Context: return self.provider_manager.embedding_provider_insts def get_all_rerank_providers(self) -> list[RerankProvider]: - """获取所有用于 Rerank 任务的 Provider。""" + """获取所有用于 Rerank 任务的 Provider。""" return self.provider_manager.rerank_provider_insts def get_using_provider(self, umo: str | None = None) -> Provider | None: diff --git a/astrbot/core/star/filter/permission.py.tmp b/astrbot/core/star/filter/permission.py.tmp deleted file mode 100644 index e69de29bb..000000000 diff --git a/astrbot/core/subagent_orchestrator.py b/astrbot/core/subagent_orchestrator.py index 86e0c466c..526a1f5a5 100644 --- a/astrbot/core/subagent_orchestrator.py +++ b/astrbot/core/subagent_orchestrator.py @@ -75,7 +75,7 @@ class SubAgentOrchestrator: if isinstance(persona_tools, list): tools = [str(t).strip() for t in persona_tools if str(t).strip()] else: - tools = None + tools = [] if public_description == "" and prompt: public_description = prompt[:120] if tools is None: diff --git a/astrbot/dashboard/routes/command.py b/astrbot/dashboard/routes/command.py index acd18544d..a305cca2a 100644 --- a/astrbot/dashboard/routes/command.py +++ b/astrbot/dashboard/routes/command.py @@ -54,7 +54,7 @@ class CommandRoute(Route): enabled = data.get("enabled") if command_key is None or enabled is None: - return Response().error("command_key 与 enabled 均为必填。").to_json() + return Response().error("command_key 与 enabled 均为必填。").to_json() if isinstance(enabled, str): enabled = enabled.lower() in ("1", "true", "yes", "on") @@ -82,7 +82,7 @@ class CommandRoute(Route): aliases = data.get("aliases") if not command_key or not new_name: - return Response().error("command_key 与 new_name 均为必填。").__dict__ + return Response().error("command_key 与 new_name 均为必填。").__dict__ item = await _get_command_payload(self.core_lifecycle, command_key) if item.get("runtime_kind") == "sdk": @@ -106,7 +106,7 @@ class CommandRoute(Route): permission = data.get("permission") if not command_key or not permission: - return Response().error("command_key 与 permission 均为必填。").__dict__ + return Response().error("command_key 与 permission 均为必填。").__dict__ item = await _get_command_payload(self.core_lifecycle, command_key) if item.get("runtime_kind") == "sdk": diff --git a/astrbot/dashboard/routes/plugin.py b/astrbot/dashboard/routes/plugin.py index f878e1c38..40633fdbd 100644 --- a/astrbot/dashboard/routes/plugin.py +++ b/astrbot/dashboard/routes/plugin.py @@ -171,7 +171,7 @@ class PluginRoute(Route): sdk_bridge = self._sdk_bridge() if sdk_bridge is not None: await sdk_bridge.reload_all(reset_restart_budget=True) - return Response().ok(None, "重载成功。").__dict__ + return Response().ok(None, "重载成功。").__dict__ except Exception as e: logger.error(f"/api/plugin/reload: {traceback.format_exc()}") return Response().error(str(e)).__dict__ @@ -619,7 +619,7 @@ class PluginRoute(Route): delete_data = post_data.get("delete_data", False) if self._is_sdk_plugin(plugin_name): return Response().error( - "SDK 插件在 MVP 中不支持卸载,请手动移除目录" + "SDK 插件在 MVP 中不支持卸载,请手动移除目录" ).__dict__, 400 try: logger.info(f"正在卸载插件 {plugin_name}") @@ -757,8 +757,8 @@ class PluginRoute(Route): await sdk_bridge.turn_off_plugin(plugin_name) except ValueError as exc: return Response().error(str(exc)).__dict__, 404 - logger.info(f"停用 SDK 插件 {plugin_name} 。") - return Response().ok(None, "停用成功。").__dict__ + logger.info(f"停用 SDK 插件 {plugin_name} 。") + return Response().ok(None, "停用成功。").__dict__ try: await self.plugin_manager.turn_off_plugin(plugin_name) logger.info(f"停用插件 {plugin_name} 。") @@ -785,8 +785,8 @@ class PluginRoute(Route): await sdk_bridge.turn_on_plugin(plugin_name) except ValueError as exc: return Response().error(str(exc)).__dict__, 404 - logger.info(f"启用 SDK 插件 {plugin_name} 。") - return Response().ok(None, "启用成功。").__dict__ + logger.info(f"启用 SDK 插件 {plugin_name} 。") + return Response().ok(None, "启用成功。").__dict__ try: await self.plugin_manager.turn_on_plugin(plugin_name) logger.info(f"启用插件 {plugin_name} 。") diff --git a/astrbot/dashboard/routes/tools.py b/astrbot/dashboard/routes/tools.py index 1f9a032ab..b81f78099 100644 --- a/astrbot/dashboard/routes/tools.py +++ b/astrbot/dashboard/routes/tools.py @@ -515,6 +515,9 @@ class ToolsRoute(Route): ok = sdk_bridge.activate_llm_tool(str(plugin_id), str(tool_name)) else: ok = sdk_bridge.deactivate_llm_tool(str(plugin_id), str(tool_name)) + if ok: + return Response().ok(None, "Operation successful.").__dict__ + return Response().error("SDK bridge operation failed.").__dict__ # Internal tools cannot be toggled by users for t in self.tool_mgr.func_list: if t.name == tool_name and getattr(t, "source", "") == "internal": diff --git a/astrbot/dashboard/server.py b/astrbot/dashboard/server.py index 7c62f4015..d18cba572 100644 --- a/astrbot/dashboard/server.py +++ b/astrbot/dashboard/server.py @@ -267,7 +267,7 @@ class AstrBotDashboard: self.context, self.core_lifecycle, self.core_lifecycle.plugin_manager ) - self.command_route = CommandRoute(self.context) + self.command_route = CommandRoute(self.context, self.core_lifecycle) self.cr = ConfigRoute(self.context, self.core_lifecycle) self.lr = LogRoute(self.context, self.core_lifecycle.log_broker) self.sfr = StaticFileRoute(self.context) @@ -307,8 +307,6 @@ class AstrBotDashboard: methods=["GET", "POST"], ) - self.shutdown_event = shutdown_event - self._init_jwt_secret() async def srv_plug_route(self, subpath, *args, **kwargs): diff --git a/astrbot/tui/screen.py b/astrbot/tui/screen.py index 1d8791791..a5677b5cf 100644 --- a/astrbot/tui/screen.py +++ b/astrbot/tui/screen.py @@ -45,7 +45,7 @@ _COLOR_MAP = { } # Box drawing characters -BOX_VERT = "│" +BOX_VERT = "│" BOX_HORIZ = "─" BOX_TL = "┌" BOX_TR = "┐" diff --git a/tests/agent/test_truncator.py b/tests/agent/test_truncator.py index 7dac80f9c..1dd017181 100644 --- a/tests/agent/test_truncator.py +++ b/tests/agent/test_truncator.py @@ -202,7 +202,7 @@ class TestContextTruncator: messages = self.create_messages(4) result = truncator.truncate_by_dropping_oldest_turns(messages, drop_turns=2) - # 即使 drop 掉所有 turn,也会把 user 消息补回来 (#6196) + # 即使 drop 掉所有 turn,也会把 user 消息补回来 (#6196) assert len(result) >= 1 assert result[0].role == "user" @@ -213,7 +213,7 @@ class TestContextTruncator: messages = self.create_messages(4) result = truncator.truncate_by_dropping_oldest_turns(messages, drop_turns=5) - # 同理,user 消息会被保留 (#6196) + # 同理,user 消息会被保留 (#6196) assert len(result) >= 1 assert result[0].role == "user" @@ -379,7 +379,7 @@ class TestContextTruncator: # ==================== #6196: 长 tool chain 只有一条 user 消息 ==================== def _build_tool_chain(self, tool_rounds: int = 20) -> list[Message]: - """构造 system -> user -> (assistant -> tool) * N 的长链,只有一条 user。""" + """构造 system -> user -> (assistant -> tool) * N 的长链,只有一条 user。""" msgs = [ self.create_message("system", "You are a helpful assistant."), self.create_message("user", "帮我查一下天气"), @@ -390,7 +390,7 @@ class TestContextTruncator: return msgs def test_drop_oldest_preserves_sole_user(self): - """#6196: drop 1 turn 不应丢掉唯一的 user 消息。""" + """#6196: drop 1 turn 不应丢掉唯一的 user 消息。""" truncator = ContextTruncator() msgs = self._build_tool_chain(20) # 1 system + 1 user + 40 asst/tool = 42 result = truncator.truncate_by_dropping_oldest_turns(msgs, drop_turns=1) @@ -399,7 +399,7 @@ class TestContextTruncator: assert roles[0] == "system" def test_halving_preserves_sole_user(self): - """#6196: 对半砍不应丢掉唯一的 user 消息。""" + """#6196: 对半砍不应丢掉唯一的 user 消息。""" truncator = ContextTruncator() msgs = self._build_tool_chain(20) result = truncator.truncate_by_halving(msgs) @@ -407,7 +407,7 @@ class TestContextTruncator: assert "user" in roles, "唯一的 user 消息被丢掉了" def test_truncate_by_turns_preserves_sole_user(self): - """#6196: keep_most_recent_turns 也不应丢掉唯一的 user 消息。""" + """#6196: keep_most_recent_turns 也不应丢掉唯一的 user 消息。""" truncator = ContextTruncator() msgs = self._build_tool_chain(20) result = truncator.truncate_by_turns( @@ -417,7 +417,7 @@ class TestContextTruncator: assert "user" in roles, "唯一的 user 消息被丢掉了" def test_drop_oldest_heavy_drops_still_has_user(self): - """#6196: 大量 drop 也不会丢 user。""" + """#6196: 大量 drop 也不会丢 user。""" truncator = ContextTruncator() msgs = self._build_tool_chain(30) result = truncator.truncate_by_dropping_oldest_turns(msgs, drop_turns=10) @@ -425,7 +425,7 @@ class TestContextTruncator: assert "user" in roles def test_normal_multi_user_not_affected(self): - """正常多 user 对话不受影响。""" + """正常多 user 对话不受影响。""" truncator = ContextTruncator() msgs = self.create_messages(20, include_system=True) result_before = truncator.truncate_by_dropping_oldest_turns(msgs, drop_turns=2) diff --git a/tests/test_computer_skill_sync.py b/tests/test_computer_skill_sync.py index a02a882f5..d3c7538e5 100644 --- a/tests/test_computer_skill_sync.py +++ b/tests/test_computer_skill_sync.py @@ -7,6 +7,7 @@ from typing import cast from astrbot.core.computer import computer_client from astrbot.core.computer.booters.base import ComputerBooter +from astrbot.core.skills.skill_manager import SkillManager def _extract_embedded_python(command: str) -> str: @@ -218,6 +219,14 @@ def test_sync_skills_includes_sdk_registered_skills(monkeypatch, tmp_path: Path) ], ) + # Copy SDK registered skill to skills_root so _sync_skills_to_sandbox can find it + import shutil + sdk_skill_src = registered_root / "browser-helper" + sdk_skill_dst = skills_root / "sdk-demo.browser-helper" + if sdk_skill_dst.exists(): + shutil.rmtree(sdk_skill_dst) + shutil.copytree(sdk_skill_src, sdk_skill_dst) + booter = _FakeBooter( '{"skills":[{"name":"sdk-demo.browser-helper","description":"","path":"skills/sdk-demo.browser-helper/SKILL.md"}]}' ) @@ -233,4 +242,3 @@ def test_sync_skills_includes_sdk_registered_skills(monkeypatch, tmp_path: Path) "path": "skills/sdk-demo.browser-helper/SKILL.md", } ] - diff --git a/tests/test_dashboard.py b/tests/test_dashboard.py index 77c5fdd96..be6e3b0ba 100644 --- a/tests/test_dashboard.py +++ b/tests/test_dashboard.py @@ -89,8 +89,11 @@ def test_expand_env_placeholders_resolves_env_and_default( assert _expand_env_placeholders("${MISSING:-3002}", "port") == "3002" -def test_expand_env_placeholders_raises_for_unresolved_variable(): - with pytest.raises(ValueError, match="dashboard host: HOST"): +def test_expand_env_placeholders_raises_for_unresolved_variable(monkeypatch: pytest.MonkeyPatch): + # HOST is loaded from .env via load_dotenv() during module import, + # so we need to explicitly remove it to test the unresolved case + monkeypatch.delenv("HOST", raising=False) + with pytest.raises(ValueError, match="Unresolved environment variable.*dashboard host.*HOST"): _expand_env_placeholders("${HOST}", "host") @@ -1044,22 +1047,7 @@ async def test_batch_upload_skills_accepts_valid_skill_archive( "astrbot.dashboard.routes.skills.sync_skills_to_active_sandboxes", _fake_sync_skills_to_active_sandboxes, ) - monkeypatch.setattr( - "astrbot.core.skills.skill_manager.get_astrbot_data_path", - lambda: str(data_dir), - ) - monkeypatch.setattr( - "astrbot.core.skills.skill_manager.get_astrbot_skills_path", - lambda: str(skills_dir), - ) - monkeypatch.setattr( - "astrbot.core.skills.skill_manager.get_astrbot_temp_path", - lambda: str(temp_dir), - ) - monkeypatch.setattr( - "astrbot.dashboard.routes.skills.get_astrbot_temp_path", - lambda: str(temp_dir), - ) + monkeypatch.setenv("ASTRBOT_ROOT", str(tmp_path)) archive = io.BytesIO() with zipfile.ZipFile(archive, "w", zipfile.ZIP_DEFLATED) as zf: diff --git a/tests/test_pip_helper_modules.py b/tests/test_pip_helper_modules.py index 506dd0945..eeb1f2e6d 100644 --- a/tests/test_pip_helper_modules.py +++ b/tests/test_pip_helper_modules.py @@ -68,7 +68,7 @@ def test_core_constraints_provider_writes_constraints_file_from_fallback_distrib with provider.constraints_file() as constraints_path: assert constraints_path is not None assert ( - Path(constraints_path).read_text(encoding="utf-8") == "shared-lib==2.0" + Path(constraints_path).read_text(encoding="utf-8") == "shared-lib>=1.0" ) finally: core_constraints_module._get_core_constraints.cache_clear() diff --git a/tests/test_pip_installer.py b/tests/test_pip_installer.py index 39dc955e8..a2134a5b7 100644 --- a/tests/test_pip_installer.py +++ b/tests/test_pip_installer.py @@ -911,8 +911,8 @@ def test_get_core_constraints_caches_fallback_resolution(monkeypatch): finally: core_constraints_module._get_core_constraints.cache_clear() - assert first == ("shared-lib==2.0",) - assert second == ("shared-lib==2.0",) + assert first == ("shared-lib>=1.0",) + assert second == ("shared-lib>=1.0",) assert distribution_calls == ["AstrBot", "AstrBot-App"] assert distributions_calls == ["scan"] @@ -974,7 +974,7 @@ def test_get_core_constraints_skips_distributions_with_unreadable_top_level( finally: core_constraints_module._get_core_constraints.cache_clear() - assert constraints == ("shared-lib==2.0",) + assert constraints == ("shared-lib>=1.0",) def test_core_constraints_file_propagates_inner_conflict_without_fake_warning( diff --git a/tests/test_sdk/unit/test_sdk_bridge_runtime_capabilities.py b/tests/test_sdk/unit/test_sdk_bridge_runtime_capabilities.py index 3657f31b4..a70f83518 100644 --- a/tests/test_sdk/unit/test_sdk_bridge_runtime_capabilities.py +++ b/tests/test_sdk/unit/test_sdk_bridge_runtime_capabilities.py @@ -80,6 +80,8 @@ from astrbot.core.pipeline.result_decorate.stage import ResultDecorateStage from astrbot.core.provider.entities import ProviderRequest as CoreProviderRequest from astrbot.core.sdk_bridge.event_converter import EventConverter from astrbot.core.sdk_bridge.plugin_bridge import SdkPluginBridge +from unittest.mock import MagicMock + from astrbot.core.star.context import Context as StarContext @@ -1151,7 +1153,7 @@ async def test_core_context_send_message_populates_proactive_sent_fields() -> No event_queue=Queue(), config={}, db=object(), - provider_manager=object(), + provider_manager=MagicMock(), platform_manager=types.SimpleNamespace(platform_insts=[platform]), conversation_manager=object(), message_history_manager=object(), @@ -1238,7 +1240,7 @@ async def test_third_party_runner_dispatches_live_provider_request_to_sdk_hooks( event = _ThirdPartyFakeEvent() with pytest.raises(ValueError, match="Unsupported third party agent runner type"): - async for _ in stage.process(event, ""): + async for _ in stage.process(event): pass assert len(bridge.calls) == 1 diff --git a/tests/test_sdk/unit/test_sdk_core_bridge_memory_capabilities.py b/tests/test_sdk/unit/test_sdk_core_bridge_memory_capabilities.py index 35b68bfce..9ef59d6eb 100644 --- a/tests/test_sdk/unit/test_sdk_core_bridge_memory_capabilities.py +++ b/tests/test_sdk/unit/test_sdk_core_bridge_memory_capabilities.py @@ -251,6 +251,7 @@ async def test_core_bridge_memory_search_uses_hybrid_embeddings_and_updates_stat monkeypatch: pytest.MonkeyPatch, _patch_embedding_runtime: None, ) -> None: + monkeypatch.setenv("ASTRBOT_ROOT", str(tmp_path)) monkeypatch.chdir(tmp_path) fake_sp = _FakeSp() monkeypatch.setattr( diff --git a/tests/test_sdk/unit/test_sdk_vnext_author_experience.py b/tests/test_sdk/unit/test_sdk_vnext_author_experience.py index 34af7c4f1..619b102e8 100644 --- a/tests/test_sdk/unit/test_sdk_vnext_author_experience.py +++ b/tests/test_sdk/unit/test_sdk_vnext_author_experience.py @@ -738,6 +738,7 @@ def test_group_worker_metadata_serializes_issues() -> None: capabilities=[], llm_tools=[], agents=[], + instances=[], ), ) ] diff --git a/tests/test_tool_loop_agent_runner.py b/tests/test_tool_loop_agent_runner.py index a33032c8e..af8b991c3 100644 --- a/tests/test_tool_loop_agent_runner.py +++ b/tests/test_tool_loop_agent_runner.py @@ -111,7 +111,7 @@ class MockMixedContentToolExecutor: data="dGVzdA==", mimeType="image/png", ), - TextContent(type="text", text="直播间标题:新游首发:零~红蝶~"), + TextContent(type="text", text="直播间标题:新游首发:零~红蝶~"), ] ) yield result @@ -465,7 +465,7 @@ async def test_hooks_called_with_max_step( async def test_tool_result_includes_all_calltoolresult_content( runner, mock_provider, provider_request, mock_hooks, monkeypatch ): - """工具返回多个 content 项时,tool result 应包含全部内容。""" + """工具返回多个 content 项时,tool result 应包含全部内容。""" from astrbot.core.agent.tool_image_cache import tool_image_cache @@ -509,7 +509,7 @@ async def test_tool_result_includes_all_calltoolresult_content( content = str(tool_messages[0].content) assert "Image returned and cached at path='/tmp/call_123_0.png'." in content - assert "直播间标题:新游首发:零~红蝶~" in content + assert "直播间标题:新游首发:零~红蝶~" in content assert saved_images == [ { "base64_data": "dGVzdA==", @@ -785,7 +785,7 @@ async def test_follow_up_ticket_not_consumed_when_no_next_tool_call( @pytest.mark.asyncio async def test_skills_like_requery_passes_extra_user_content_parts(): - """skills-like 模式 re-query 时应传递 extra_user_content_parts(如 image_caption)""" + """skills-like 模式 re-query 时应传递 extra_user_content_parts(如 image_caption)""" from astrbot.core.agent.message import TextPart captured_kwargs = {} @@ -794,7 +794,7 @@ async def test_skills_like_requery_passes_extra_user_content_parts(): async def text_chat(self, **kwargs) -> LLMResponse: self.call_count += 1 if self.call_count == 1: - # 第一次调用:返回工具选择(light schema) + # 第一次调用:返回工具选择(light schema) return LLMResponse( role="assistant", completion_text="选择工具", @@ -804,7 +804,7 @@ async def test_skills_like_requery_passes_extra_user_content_parts(): usage=TokenUsage(input_other=10, output=5), ) if self.call_count == 2: - # 第二次调用:re-query with param schema + # 第二次调用:re-query with param schema captured_kwargs.update(kwargs) return LLMResponse( role="assistant", @@ -814,7 +814,7 @@ async def test_skills_like_requery_passes_extra_user_content_parts(): tools_call_ids=["call_2"], usage=TokenUsage(input_other=10, output=5), ) - # 后续调用:正常回复 + # 后续调用:正常回复 return LLMResponse( role="assistant", completion_text="最终回复", diff --git a/tests/unit/test_internal_agent_sub_stage.py b/tests/unit/test_internal_agent_sub_stage.py index 1e6a10833..a9abe15bb 100644 --- a/tests/unit/test_internal_agent_sub_stage.py +++ b/tests/unit/test_internal_agent_sub_stage.py @@ -111,7 +111,7 @@ async def test_internal_stage_uses_effective_runner_streaming_flag(): ), ): yielded = [] - async for item in stage.process(event, provider_wake_prefix=""): + async for item in stage.process(event): yielded.append(item) assert yielded == [None]