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"