From 7b2f6f5497f7285cd5a41e2ffd3e1461e0b23cea Mon Sep 17 00:00:00 2001 From: whatevertogo Date: Fri, 13 Mar 2026 18:26:13 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=BC=BA=E6=97=A7=E7=89=88=E5=85=BC?= =?UTF-8?q?=E5=AE=B9=E6=80=A7=EF=BC=8C=E6=B7=BB=E5=8A=A0=E5=A4=9A=E4=B8=AA?= =?UTF-8?q?=E6=97=A7=E8=B7=AF=E5=BE=84=E5=85=A5=E5=8F=A3=E5=92=8C=E7=9B=B8?= =?UTF-8?q?=E5=85=B3=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 1 + CLAUDE.md | 1 + src-new/astrbot/core/__init__.py | 5 +- src-new/astrbot/core/agent/__init__.py | 5 ++ src-new/astrbot/core/agent/message.py | 13 ++++ src-new/astrbot/core/db/__init__.py | 5 ++ src-new/astrbot/core/db/po.py | 5 ++ src-new/astrbot/core/provider/__init__.py | 24 +++++++ src-new/astrbot/core/provider/entities.py | 29 +++++++++ src-new/astrbot/core/provider/provider.py | 61 ++++++++++++++++++ src-new/astrbot/core/utils/__init__.py | 34 +++++++++- src-new/astrbot/core/utils/astrbot_path.py | 73 ++++++++++++++++++++++ tests_v4/external_plugin_matrix.json | 10 +++ tests_v4/test_api_modules.py | 33 ++++++++++ 14 files changed, 297 insertions(+), 2 deletions(-) create mode 100644 src-new/astrbot/core/agent/__init__.py create mode 100644 src-new/astrbot/core/agent/message.py create mode 100644 src-new/astrbot/core/db/__init__.py create mode 100644 src-new/astrbot/core/db/po.py create mode 100644 src-new/astrbot/core/provider/__init__.py create mode 100644 src-new/astrbot/core/provider/entities.py create mode 100644 src-new/astrbot/core/provider/provider.py create mode 100644 src-new/astrbot/core/utils/astrbot_path.py diff --git a/AGENTS.md b/AGENTS.md index dbb3aa437..4a4f29de5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -32,6 +32,7 @@ - 2026-03-13: `filter.llm_tool()` must resolve deferred annotations from `from __future__ import annotations` when inferring JSON schema. Reading `inspect.Parameter.annotation` directly degrades typed params like `a: int` into `"int"` strings and silently turns tool argument schemas into generic strings. - 2026-03-13: Legacy result hooks must reuse the same `AstrMessageEvent` instance across `on_decorating_result` and `after_message_sent`. Re-wrapping the original v4 `MessageEvent` for the second hook drops decorated `event.set_result(...)` mutations and makes post-send hooks observe an empty result. - 2026-03-13: `src-new/astrbot_sdk/_legacy_runtime.py` already exists as the intended compat execution boundary. When cleaning runtime architecture, wire `loader` / `handler_dispatcher` / `bootstrap` through that adapter instead of adding new direct `legacy_context` branches in runtime files, or the compat logic will spread again. +- 2026-03-13: Real legacy plugins may still load through deep `astrbot.core.*` imports even when their public entrypoint only looks like `astrbot.api.*`. `astrbot_plugin_self_learning` hits `astrbot.core.utils.astrbot_path`, `astrbot.core.provider.*`, `astrbot.core.agent.message`, and `astrbot.core.db.po` during load; keep those deep-path shims minimal and whitelist-driven, but do not assume the `api` facade alone is enough. # 开发命令 diff --git a/CLAUDE.md b/CLAUDE.md index 90d87fe38..f1abbdde9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -32,6 +32,7 @@ - 2026-03-13: `filter.llm_tool()` must resolve deferred annotations from `from __future__ import annotations` when inferring JSON schema. Reading `inspect.Parameter.annotation` directly degrades typed params like `a: int` into `"int"` strings and silently turns tool argument schemas into generic strings. - 2026-03-13: Legacy result hooks must reuse the same `AstrMessageEvent` instance across `on_decorating_result` and `after_message_sent`. Re-wrapping the original v4 `MessageEvent` for the second hook drops decorated `event.set_result(...)` mutations and makes post-send hooks observe an empty result. - 2026-03-13: `src-new/astrbot_sdk/_legacy_runtime.py` already exists as the intended compat execution boundary. When cleaning runtime architecture, wire `loader` / `handler_dispatcher` / `bootstrap` through that adapter instead of adding new direct `legacy_context` branches in runtime files, or the compat logic will spread again. +- 2026-03-13: Real legacy plugins may still load through deep `astrbot.core.*` imports even when their public entrypoint only looks like `astrbot.api.*`. `astrbot_plugin_self_learning` hits `astrbot.core.utils.astrbot_path`, `astrbot.core.provider.*`, `astrbot.core.agent.message`, and `astrbot.core.db.po` during load; keep those deep-path shims minimal and whitelist-driven, but do not assume the `api` facade alone is enough. # 开发命令 diff --git a/src-new/astrbot/core/__init__.py b/src-new/astrbot/core/__init__.py index 3d2d10ed5..aabde13a5 100644 --- a/src-new/astrbot/core/__init__.py +++ b/src-new/astrbot/core/__init__.py @@ -5,7 +5,7 @@ from loguru import logger from astrbot_sdk._shared_preferences import sp from astrbot_sdk.api.basic import AstrBotConfig -from . import config, message, platform, utils +from . import agent, config, db, message, platform, provider, utils class _HtmlRendererCompat: @@ -31,11 +31,14 @@ html_renderer = _HtmlRendererCompat() __all__ = [ "AstrBotConfig", + "agent", "config", + "db", "html_renderer", "logger", "message", "platform", + "provider", "sp", "utils", ] diff --git a/src-new/astrbot/core/agent/__init__.py b/src-new/astrbot/core/agent/__init__.py new file mode 100644 index 000000000..090dd6469 --- /dev/null +++ b/src-new/astrbot/core/agent/__init__.py @@ -0,0 +1,5 @@ +"""旧版 ``astrbot.core.agent`` 兼容入口。""" + +from .message import TextPart + +__all__ = ["TextPart"] diff --git a/src-new/astrbot/core/agent/message.py b/src-new/astrbot/core/agent/message.py new file mode 100644 index 000000000..4f0fe8e9e --- /dev/null +++ b/src-new/astrbot/core/agent/message.py @@ -0,0 +1,13 @@ +"""旧版 ``astrbot.core.agent.message`` 兼容入口。""" + +from __future__ import annotations + +from dataclasses import dataclass + + +@dataclass(slots=True) +class TextPart: + text: str + + +__all__ = ["TextPart"] diff --git a/src-new/astrbot/core/db/__init__.py b/src-new/astrbot/core/db/__init__.py new file mode 100644 index 000000000..16f5418f0 --- /dev/null +++ b/src-new/astrbot/core/db/__init__.py @@ -0,0 +1,5 @@ +"""旧版 ``astrbot.core.db`` 兼容入口。""" + +from .po import Personality + +__all__ = ["Personality"] diff --git a/src-new/astrbot/core/db/po.py b/src-new/astrbot/core/db/po.py new file mode 100644 index 000000000..c6d69d232 --- /dev/null +++ b/src-new/astrbot/core/db/po.py @@ -0,0 +1,5 @@ +"""旧版 ``astrbot.core.db.po`` 兼容入口。""" + +from astrbot.api.provider import Personality + +__all__ = ["Personality"] diff --git a/src-new/astrbot/core/provider/__init__.py b/src-new/astrbot/core/provider/__init__.py new file mode 100644 index 000000000..7657e5f95 --- /dev/null +++ b/src-new/astrbot/core/provider/__init__.py @@ -0,0 +1,24 @@ +"""旧版 ``astrbot.core.provider`` 兼容入口。""" + +from .entities import ( + LLMResponse, + Personality, + ProviderMetaData, + ProviderRequest, + ProviderType, + RerankResult, +) +from .provider import EmbeddingProvider, Provider, RerankProvider, STTProvider + +__all__ = [ + "EmbeddingProvider", + "LLMResponse", + "Personality", + "Provider", + "ProviderMetaData", + "ProviderRequest", + "ProviderType", + "RerankProvider", + "RerankResult", + "STTProvider", +] diff --git a/src-new/astrbot/core/provider/entities.py b/src-new/astrbot/core/provider/entities.py new file mode 100644 index 000000000..e9df85caf --- /dev/null +++ b/src-new/astrbot/core/provider/entities.py @@ -0,0 +1,29 @@ +"""旧版 ``astrbot.core.provider.entities`` 兼容入口。""" + +from __future__ import annotations + +from dataclasses import dataclass + +from astrbot.api.provider import ( + LLMResponse, + Personality, + ProviderMetaData, + ProviderRequest, + ProviderType, +) + + +@dataclass(slots=True) +class RerankResult: + index: int + relevance_score: float + + +__all__ = [ + "LLMResponse", + "Personality", + "ProviderMetaData", + "ProviderRequest", + "ProviderType", + "RerankResult", +] diff --git a/src-new/astrbot/core/provider/provider.py b/src-new/astrbot/core/provider/provider.py new file mode 100644 index 000000000..40a348da5 --- /dev/null +++ b/src-new/astrbot/core/provider/provider.py @@ -0,0 +1,61 @@ +"""旧版 ``astrbot.core.provider.provider`` 兼容入口。""" + +from __future__ import annotations + +from typing import Any + +from .entities import ProviderMetaData + + +class Provider: + """旧版 Provider 基类占位。""" + + async def text_chat(self, *args, **kwargs): # pragma: no cover - compat stub + raise NotImplementedError("compat facade does not implement core providers") + + def meta(self) -> ProviderMetaData: # pragma: no cover - compat stub + raise NotImplementedError("compat facade does not implement core providers") + + def get_model(self) -> str: # pragma: no cover - compat stub + raise NotImplementedError("compat facade does not implement core providers") + + +class STTProvider(Provider): + pass + + +class EmbeddingProvider(Provider): + async def get_embeddings(self, texts: list[str]) -> list[list[float]]: + raise NotImplementedError("compat facade does not implement embeddings") + + async def get_embeddings_batch( + self, + texts: list[str], + *, + batch_size: int = 16, + tasks_limit: int = 3, + max_retries: int = 3, + progress_callback: Any | None = None, + ) -> list[list[float]]: + raise NotImplementedError("compat facade does not implement embeddings") + + def get_dim(self) -> int: + raise NotImplementedError("compat facade does not implement embeddings") + + +class RerankProvider(Provider): + async def rerank( + self, + query: str, + documents: list[str], + top_n: int | None = None, + ): + raise NotImplementedError("compat facade does not implement rerank") + + +__all__ = [ + "EmbeddingProvider", + "Provider", + "RerankProvider", + "STTProvider", +] diff --git a/src-new/astrbot/core/utils/__init__.py b/src-new/astrbot/core/utils/__init__.py index 5864149d2..2a7cba0fd 100644 --- a/src-new/astrbot/core/utils/__init__.py +++ b/src-new/astrbot/core/utils/__init__.py @@ -1,5 +1,37 @@ """旧版 ``astrbot.core.utils`` 兼容入口。""" +from .astrbot_path import ( + get_astrbot_backups_path, + get_astrbot_config_path, + get_astrbot_data_path, + get_astrbot_knowledge_base_path, + get_astrbot_path, + get_astrbot_plugin_data_path, + get_astrbot_plugin_path, + get_astrbot_root, + get_astrbot_site_packages_path, + get_astrbot_skills_path, + get_astrbot_t2i_templates_path, + get_astrbot_temp_path, + get_astrbot_webchat_path, +) from .session_waiter import SessionController, SessionWaiter, session_waiter -__all__ = ["SessionController", "SessionWaiter", "session_waiter"] +__all__ = [ + "SessionController", + "SessionWaiter", + "get_astrbot_backups_path", + "get_astrbot_config_path", + "get_astrbot_data_path", + "get_astrbot_knowledge_base_path", + "get_astrbot_path", + "get_astrbot_plugin_data_path", + "get_astrbot_plugin_path", + "get_astrbot_root", + "get_astrbot_site_packages_path", + "get_astrbot_skills_path", + "get_astrbot_t2i_templates_path", + "get_astrbot_temp_path", + "get_astrbot_webchat_path", + "session_waiter", +] diff --git a/src-new/astrbot/core/utils/astrbot_path.py b/src-new/astrbot/core/utils/astrbot_path.py new file mode 100644 index 000000000..7d7dfeca7 --- /dev/null +++ b/src-new/astrbot/core/utils/astrbot_path.py @@ -0,0 +1,73 @@ +"""旧版 ``astrbot.core.utils.astrbot_path`` 兼容入口。""" + +from __future__ import annotations + +import os +from pathlib import Path + + +def get_astrbot_path() -> str: + """返回当前兼容 SDK 的项目根路径。""" + + return str(Path(__file__).resolve().parents[4]) + + +def get_astrbot_root() -> str: + """返回 AstrBot 运行根目录。 + + 旧版优先读取 ``ASTRBOT_ROOT``,否则默认当前工作目录。compat 层保持 + 这个约定,方便旧插件继续把数据写到 ``/data`` 下。 + """ + + root = os.environ.get("ASTRBOT_ROOT") + if root: + return str(Path(root).resolve()) + return str(Path.cwd().resolve()) + + +def _data_child(*parts: str) -> str: + return str(Path(get_astrbot_data_path(), *parts).resolve()) + + +def get_astrbot_data_path() -> str: + return str(Path(get_astrbot_root(), "data").resolve()) + + +def get_astrbot_config_path() -> str: + return _data_child("config") + + +def get_astrbot_plugin_path() -> str: + return _data_child("plugins") + + +def get_astrbot_plugin_data_path() -> str: + return _data_child("plugin_data") + + +def get_astrbot_t2i_templates_path() -> str: + return _data_child("t2i_templates") + + +def get_astrbot_webchat_path() -> str: + return _data_child("webchat") + + +def get_astrbot_temp_path() -> str: + return _data_child("temp") + + +def get_astrbot_skills_path() -> str: + return _data_child("skills") + + +def get_astrbot_site_packages_path() -> str: + return _data_child("site-packages") + + +def get_astrbot_knowledge_base_path() -> str: + return _data_child("knowledge_base") + + +def get_astrbot_backups_path() -> str: + return _data_child("backups") diff --git a/tests_v4/external_plugin_matrix.json b/tests_v4/external_plugin_matrix.json index c84f9660d..47d5bd345 100644 --- a/tests_v4/external_plugin_matrix.json +++ b/tests_v4/external_plugin_matrix.json @@ -17,6 +17,16 @@ "known_unsupported": [ "依赖 playwright 时会退回纯文本帮助输出" ] + }, + { + "name": "self_learning", + "repo": "https://github.com/NickCharlie/astrbot_plugin_self_learning.git", + "command": "learning_status", + "event_text": "learning_status", + "expected_text": "自学习插件状态报告", + "known_unsupported": [ + "矩阵当前只验证管理命令,未覆盖需要真实 provider/persona 系统的深层能力" + ] } ] } diff --git a/tests_v4/test_api_modules.py b/tests_v4/test_api_modules.py index c0f184d0a..a5c9c36e6 100644 --- a/tests_v4/test_api_modules.py +++ b/tests_v4/test_api_modules.py @@ -287,6 +287,39 @@ class TestAstrbotImportAlias: with pytest.raises(NotImplementedError, match="register_platform_adapter"): register_platform_adapter() + def test_legacy_astrbot_core_utils_astrbot_path_imports(self): + """astrbot.core.utils.astrbot_path should expose legacy path helpers.""" + from astrbot.core.utils.astrbot_path import ( + get_astrbot_data_path, + get_astrbot_temp_path, + ) + + assert callable(get_astrbot_data_path) + assert callable(get_astrbot_temp_path) + + def test_legacy_astrbot_core_provider_imports(self): + """astrbot.core.provider old import paths should remain available.""" + from astrbot.core.provider.entities import ProviderType, RerankResult + from astrbot.core.provider.provider import ( + EmbeddingProvider, + Provider, + RerankProvider, + ) + + assert Provider is not None + assert EmbeddingProvider is not None + assert RerankProvider is not None + assert ProviderType.CHAT_COMPLETION.value == "chat_completion" + assert RerankResult(index=1, relevance_score=0.5).index == 1 + + def test_legacy_astrbot_core_agent_and_db_imports(self): + """astrbot.core.agent/db old import paths should remain available.""" + from astrbot.core.agent.message import TextPart + from astrbot.core.db.po import Personality + + assert TextPart(text="hi").text == "hi" + assert Personality is not None + def test_legacy_astrbot_event_filter_module_exports(self): """astrbot.api.event.filter should be importable from the old module path.""" from astrbot.api.event.filter import EventMessageType, command, llm_tool