From 67cc4c5715bd5fe042f9584efa5a0e8d299fff1d Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Tue, 11 Nov 2025 17:22:03 +0800 Subject: [PATCH] refactor: remove unused hello command, echo listener, and related tools; add CLI and context enhancements --- dev/plugin_sample/handlers/commands/hello.py | 12 ----- dev/plugin_sample/handlers/listeners/echo.py | 12 ----- dev/plugin_sample/handlers/tools/hello.py | 31 ------------ dev/plugin_sample/icon.png | 0 dev/plugin_sample/plugin.yaml | 22 -------- pyproject.toml | 4 ++ src/astrbot_sdk/cli/__init__.py | 3 ++ src/astrbot_sdk/cli/main.py | 50 +++++++++++++++++++ src/astrbot_sdk/runtime/api/context.py | 12 ++++- .../runtime/api/conversation_mgr.py | 8 +++ src/astrbot_sdk/runtime/star_manager.py | 20 +++++--- src/astrbot_sdk/runtime/star_runner.py | 3 +- src/astrbot_sdk/runtime/start_server.py | 17 +++---- src/run.py | 2 +- .../commands/hello.py | 0 {src => test_plugin}/plugin.yaml | 2 +- uv.lock | 14 ++++++ 17 files changed, 111 insertions(+), 101 deletions(-) delete mode 100644 dev/plugin_sample/handlers/commands/hello.py delete mode 100644 dev/plugin_sample/handlers/listeners/echo.py delete mode 100644 dev/plugin_sample/handlers/tools/hello.py delete mode 100644 dev/plugin_sample/icon.png delete mode 100644 dev/plugin_sample/plugin.yaml create mode 100644 src/astrbot_sdk/cli/__init__.py create mode 100644 src/astrbot_sdk/cli/main.py rename {src/test_plugin => test_plugin}/commands/hello.py (100%) rename {src => test_plugin}/plugin.yaml (92%) diff --git a/dev/plugin_sample/handlers/commands/hello.py b/dev/plugin_sample/handlers/commands/hello.py deleted file mode 100644 index 493d4126b..000000000 --- a/dev/plugin_sample/handlers/commands/hello.py +++ /dev/null @@ -1,12 +0,0 @@ -from astrbot.api.v1.components.command import CommandComponent -from astrbot.api.v1.event import AstrMessageEvent, filter -from astrbot.api.v1.context import Context - - -class HelloCommand(CommandComponent): - def __init__(self, context: Context): - super().__init__(context) - - @filter.command("hello") - async def hello(self, event: AstrMessageEvent): - yield event.plain_result("Hello, Astrbot!") diff --git a/dev/plugin_sample/handlers/listeners/echo.py b/dev/plugin_sample/handlers/listeners/echo.py deleted file mode 100644 index b7a509415..000000000 --- a/dev/plugin_sample/handlers/listeners/echo.py +++ /dev/null @@ -1,12 +0,0 @@ -from astrbot.api.v1.components.listener import ListenerComponent -from astrbot.api.v1.event import AstrMessageEvent, filter -from astrbot.api.v1.context import Context - - -class EchoListener(ListenerComponent): - def __init__(self, context: Context): - super().__init__(context) - - @filter.platform_adapter_type(filter.PlatformAdapterType.ALL) - async def on_message(self, event: AstrMessageEvent): - yield event.plain_result("Hello, Astrbot!") diff --git a/dev/plugin_sample/handlers/tools/hello.py b/dev/plugin_sample/handlers/tools/hello.py deleted file mode 100644 index 7cf641a5d..000000000 --- a/dev/plugin_sample/handlers/tools/hello.py +++ /dev/null @@ -1,31 +0,0 @@ -from mcp.types import CallToolResult -from pydantic import Field -from pydantic.dataclasses import dataclass - -from astrbot.api.v1.components.agent.tool import FunctionTool -from astrbot.api.v1.components.agent import ContextWrapper, AstrAgentContext - - -@dataclass -class HelloWorldTool(FunctionTool): - name: str = "hello_world" # 工具名称 - description: str = "Say hello to the world." # 工具描述 - parameters: dict = Field( - default_factory=lambda: { - "type": "object", - "properties": { - "greeting": { - "type": "string", - "description": "The greeting message.", - }, - }, - "required": ["greeting"], - } - ) # 工具参数定义,见 OpenAI 官网或 https://json-schema.org/understanding-json-schema/ - - async def call( - self, context: ContextWrapper[AstrAgentContext], **kwargs - ) -> str | CallToolResult: - # event 在 context.context.event 中可用 - greeting = kwargs.get("greeting", "Hello") - return f"{greeting}, World!" # 也支持 mcp.types.CallToolResult 类型 diff --git a/dev/plugin_sample/icon.png b/dev/plugin_sample/icon.png deleted file mode 100644 index e69de29bb..000000000 diff --git a/dev/plugin_sample/plugin.yaml b/dev/plugin_sample/plugin.yaml deleted file mode 100644 index 5eff216ec..000000000 --- a/dev/plugin_sample/plugin.yaml +++ /dev/null @@ -1,22 +0,0 @@ -_schema_version: 2 -name: astrbot_plugin_helloworld -display_name: HelloWorld 插件 -desc: 一个简单的问候插件示例 -author: Soulter -version: 0.1.0 -components: # 组件列表,将支持自动生成 - - class: plugin_sample.commands.hello:HelloCommand - type: command - name: hello - description: 发送问候消息 - subcommands: - - name: wow - description: 发送 "Hello, Astrbot!" 消息 - - class: plugin_sample.tools.echo:EchoTool - type: llm_tool - name: echo_tool - description: 回显输入的消息 - - class: plugin_sample.listeners.message_listener:MessageListener - type: listener - name: message_logger - description: 监听并记录所有消息 diff --git a/pyproject.toml b/pyproject.toml index e07960d4d..daa085f82 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,8 +7,12 @@ requires-python = ">=3.12" dependencies = [ "aiohttp>=3.13.2", "certifi>=2025.10.5", + "click>=8.3.0", "docstring-parser>=0.17.0", "loguru>=0.7.3", "pydantic>=2.12.3", "pyyaml>=6.0.3", ] + +[project.scripts] +astr = "astrbot_sdk.cli:cli" diff --git a/src/astrbot_sdk/cli/__init__.py b/src/astrbot_sdk/cli/__init__.py new file mode 100644 index 000000000..ed9c2c342 --- /dev/null +++ b/src/astrbot_sdk/cli/__init__.py @@ -0,0 +1,3 @@ +from .main import cli + +__all__ = ['cli'] diff --git a/src/astrbot_sdk/cli/main.py b/src/astrbot_sdk/cli/main.py new file mode 100644 index 000000000..5bd3ab13f --- /dev/null +++ b/src/astrbot_sdk/cli/main.py @@ -0,0 +1,50 @@ +import asyncio +import sys +import click +from loguru import logger +from ..runtime.start_server import amain as run_server + + +def setup_logger(verbose: bool = False): + """Configure loguru for CLI output""" + # Remove default handler + logger.remove() + + # Add custom handler with CLI-friendly format + log_format = ( + "{time:HH:mm:ss} | " + "{level: <8} | " + "{message}" + ) + + level = "DEBUG" if verbose else "INFO" + + logger.add( + sys.stderr, + format=log_format, + level=level, + colorize=True, + ) + + +@click.group() +@click.option("-v", "--verbose", is_flag=True, help="Enable verbose output") +@click.pass_context +def cli(ctx, verbose): + """AstrBot SDK CLI""" + ctx.ensure_object(dict) + ctx.obj["verbose"] = verbose + setup_logger(verbose) + + +@cli.command() +@click.option("--port", default=8765, help="WebSocket server port", type=int) +@click.pass_context +def run(ctx, port: int): + """Start the WebSocket server""" + logger.info(f"Starting WebSocket server on port {port}...") + asyncio.run(run_server(port)) + + +if __name__ == "__main__": + cli() diff --git a/src/astrbot_sdk/runtime/api/context.py b/src/astrbot_sdk/runtime/api/context.py index 1ada7d77f..929be8f41 100644 --- a/src/astrbot_sdk/runtime/api/context.py +++ b/src/astrbot_sdk/runtime/api/context.py @@ -6,5 +6,13 @@ class Context(BaseContext): def __init__(self, conversation_manager: ConversationManager): self.conversation_manager = conversation_manager - def _inject_rpc_handlers(self, runner): - setattr(self.conversation_manager, "runner", runner) + @classmethod + def default_context(cls, runner=None): + """Create a default context instance. + + Args: + runner: Optional StarRunner instance to inject into conversation manager. + If provided, enables RPC functionality. + """ + conversation_manager = ConversationManager(runner=runner) + return cls(conversation_manager=conversation_manager) diff --git a/src/astrbot_sdk/runtime/api/conversation_mgr.py b/src/astrbot_sdk/runtime/api/conversation_mgr.py index ea2eb6b61..72b4ecd9b 100644 --- a/src/astrbot_sdk/runtime/api/conversation_mgr.py +++ b/src/astrbot_sdk/runtime/api/conversation_mgr.py @@ -3,6 +3,14 @@ from .util import rpc_method class ConversationManager(BaseConversationManager): + def __init__(self, runner=None): + """Initialize ConversationManager. + + Args: + runner: Optional StarRunner instance for RPC functionality. + """ + self.runner = runner + @rpc_method async def new_conversation( self, diff --git a/src/astrbot_sdk/runtime/star_manager.py b/src/astrbot_sdk/runtime/star_manager.py index d94dba8cb..b4e5f8e0d 100644 --- a/src/astrbot_sdk/runtime/star_manager.py +++ b/src/astrbot_sdk/runtime/star_manager.py @@ -1,6 +1,7 @@ import yaml import importlib import functools +import sys from pathlib import Path from loguru import logger from .stars.registry import star_handlers_registry, star_map, star_registry @@ -20,13 +21,21 @@ class StarManager: root_dir (Path | None): The root directory to search for plugin.yaml. Defaults to None, which means the current working directory. """ if root_dir is None: - root_dir = Path.cwd().relative_to(Path.cwd()) + root_dir = Path.cwd() else: - root_dir = Path.cwd().joinpath(root_dir).resolve() + root_dir = Path(root_dir).resolve() + path = root_dir / "plugin.yaml" if not path.exists(): logger.warning("No plugin.yaml found in the current directory.") return [] + + # Add the plugin directory to sys.path so we can import its modules + root_dir_str = str(root_dir) + if root_dir_str not in sys.path: + sys.path.insert(0, root_dir_str) + logger.debug(f"Added {root_dir_str} to sys.path") + with open(path, "r", encoding="utf-8") as f: data = yaml.safe_load(f) @@ -46,7 +55,7 @@ class StarManager: full_name_list = [] for comp in components: class_ = comp.get("class", "") - print(f"Loading component: {class_}") + logger.debug(f"Loading component: {class_}") if not class_: logger.warning(f"Component without class found: {comp}") continue @@ -56,10 +65,7 @@ class StarManager: continue # dynamically register the component try: - # we need edit the module path to be relative to the root_dir - root_dir_dot = str(root_dir).replace("/", ".").lstrip(".") - if root_dir_dot: - module_path = f"{root_dir_dot}.{module_path}" + logger.debug(f"Importing module: {module_path}") module_type = importlib.import_module(module_path) logger.info(f"Successfully loaded component module: {module_path}") component_cls = getattr(module_type, class_name) diff --git a/src/astrbot_sdk/runtime/star_runner.py b/src/astrbot_sdk/runtime/star_runner.py index a894033c5..c31c235a3 100644 --- a/src/astrbot_sdk/runtime/star_runner.py +++ b/src/astrbot_sdk/runtime/star_runner.py @@ -10,8 +10,7 @@ from .rpc.jsonrpc import ( JSONRPCErrorResponse, JSONRPCErrorData, ) -from .types import CallHandlerRequest, HandshakeRequest -from ..api.event.astr_message_event import AstrMessageEvent +from .types import CallHandlerRequest class StarRunner: diff --git a/src/astrbot_sdk/runtime/start_server.py b/src/astrbot_sdk/runtime/start_server.py index a49337890..913f68f63 100644 --- a/src/astrbot_sdk/runtime/start_server.py +++ b/src/astrbot_sdk/runtime/start_server.py @@ -4,31 +4,26 @@ from .rpc.server import WebSocketServer from .star_runner import StarRunner from .star_manager import StarManager from ..runtime.api.context import Context -from ..runtime.api.conversation_mgr import ConversationManager +from loguru import logger -async def amain(): - server = WebSocketServer(port=8765) - conversation_manager = ConversationManager() - context = Context(conversation_manager=conversation_manager) +async def amain(port: int = 8765): + server = WebSocketServer(port=port) runner = StarRunner(server) - context._inject_rpc_handlers(runner=runner) + context = Context.default_context(runner=runner) star_manager = StarManager(context=context) star_manager.discover_star() await runner.run() - # 设置停止事件 stop_event = asyncio.Event() - - # 注册信号处理器 loop = asyncio.get_running_loop() for sig in (signal.SIGTERM, signal.SIGINT): loop.add_signal_handler(sig, stop_event.set) - print("Server is running. Press Ctrl+C to stop.") + logger.info("Server is running. Press Ctrl+C to stop.") try: await stop_event.wait() finally: - print("Shutting down...") + logger.info("Shutting down...") await server.stop() diff --git a/src/run.py b/src/run.py index 2d9a8def2..8e55df710 100644 --- a/src/run.py +++ b/src/run.py @@ -3,4 +3,4 @@ from astrbot_sdk.runtime.start_server import amain import asyncio if __name__ == "__main__": - asyncio.run(amain()) + asyncio.run(amain()) # 使用默认端口 8765 diff --git a/src/test_plugin/commands/hello.py b/test_plugin/commands/hello.py similarity index 100% rename from src/test_plugin/commands/hello.py rename to test_plugin/commands/hello.py diff --git a/src/plugin.yaml b/test_plugin/plugin.yaml similarity index 92% rename from src/plugin.yaml rename to test_plugin/plugin.yaml index a178bdbd8..9cbbad136 100644 --- a/src/plugin.yaml +++ b/test_plugin/plugin.yaml @@ -5,7 +5,7 @@ desc: 一个简单的问候插件示例 author: Soulter version: 0.1.0 components: # 组件列表,将支持自动生成 - - class: test_plugin.commands.hello:HelloCommand + - class: commands.hello:HelloCommand type: command name: hello description: 发送问候消息 diff --git a/uv.lock b/uv.lock index b309793cb..15c9ba99d 100644 --- a/uv.lock +++ b/uv.lock @@ -125,6 +125,7 @@ source = { virtual = "." } dependencies = [ { name = "aiohttp" }, { name = "certifi" }, + { name = "click" }, { name = "docstring-parser" }, { name = "loguru" }, { name = "pydantic" }, @@ -135,6 +136,7 @@ dependencies = [ requires-dist = [ { name = "aiohttp", specifier = ">=3.13.2" }, { name = "certifi", specifier = ">=2025.10.5" }, + { name = "click", specifier = ">=8.3.0" }, { name = "docstring-parser", specifier = ">=0.17.0" }, { name = "loguru", specifier = ">=0.7.3" }, { name = "pydantic", specifier = ">=2.12.3" }, @@ -159,6 +161,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e4/37/af0d2ef3967ac0d6113837b44a4f0bfe1328c2b9763bd5b1744520e5cfed/certifi-2025.10.5-py3-none-any.whl", hash = "sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de", size = 163286 }, ] +[[package]] +name = "click" +version = "8.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/46/61/de6cd827efad202d7057d93e0fed9294b96952e188f7384832791c7b2254/click-8.3.0.tar.gz", hash = "sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4", size = 276943 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/d3/9dcc0f5797f070ec8edf30fbadfb200e71d9db6b84d211e3b2085a7589a0/click-8.3.0-py3-none-any.whl", hash = "sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc", size = 107295 }, +] + [[package]] name = "colorama" version = "0.4.6"