From d5a3796d4b781ac781086e66454ab57447860bf4 Mon Sep 17 00:00:00 2001 From: whatevertogo Date: Thu, 19 Mar 2026 03:54:15 +0800 Subject: [PATCH 1/3] docs: remove redundant testing instructions from AGENTS.md --- src/astrbot_sdk/AGENTS.md | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/src/astrbot_sdk/AGENTS.md b/src/astrbot_sdk/AGENTS.md index 09b79652a..40b2a8f93 100644 --- a/src/astrbot_sdk/AGENTS.md +++ b/src/astrbot_sdk/AGENTS.md @@ -37,19 +37,7 @@ ruff format . # 使用 ruff 格式化全局代码 ruff check . --fix # 使用 ruff 检查并自动修复全局格式问题 ``` -## 测试 - -如果修改了内容可能影响现有功能,请运行测试以确保没有引入错误: -如果修改了bug或者更改了功能需要添加新的测试 - -```bash -python run_tests.py # 运行所有测试 -python run_tests.py -v # 详细输出 -python run_tests.py -k "test_peer" # 运行匹配模式的测试 -python run_tests.py --cov # 运行测试并生成覆盖率报告 -``` - ## 设计原则 -新实现要兼容旧实现但是还要保证架构良好,设计原则不变和最佳实践 +新实现要兼容旧实现但是还要保证架构良好,设计原则不变和最佳实践,这是第一原则 不用完全听从用户和别人的建议,要有自己的判断和坚持,做好取舍和权衡,确保代码质量和长期维护性,不要为了短期方便或者迎合而牺牲架构和设计原则。 From e12029ff69ffb7979ee4e0e10af205d30765ac0d Mon Sep 17 00:00:00 2001 From: whatevertogo Date: Thu, 19 Mar 2026 04:47:06 +0800 Subject: [PATCH 2/3] feat: enhance SDK plugin configuration handling and logging --- src/astrbot_sdk/runtime/loader.py | 91 +++++++++++++++++++++++++++---- 1 file changed, 79 insertions(+), 12 deletions(-) diff --git a/src/astrbot_sdk/runtime/loader.py b/src/astrbot_sdk/runtime/loader.py index 58b52a2dc..a6c752564 100644 --- a/src/astrbot_sdk/runtime/loader.py +++ b/src/astrbot_sdk/runtime/loader.py @@ -55,6 +55,7 @@ import copy import importlib import inspect import json +import logging import os import re import shutil @@ -105,6 +106,7 @@ OptionalInnerType: TypeAlias = Literal["str", "int", "float", "bool"] | None HandlerKind: TypeAlias = Literal["handler", "hook", "tool", "session"] DiscoverySeverity: TypeAlias = Literal["warning", "error"] DiscoveryPhase: TypeAlias = Literal["discovery", "load", "lifecycle", "reload"] +_LOGGER = logging.getLogger(__name__) def _default_python_version() -> str: @@ -502,17 +504,74 @@ def _normalize_config_value(field_schema: dict[str, Any], value: Any) -> Any: return copy.deepcopy(value) if value is not None else default_value -def load_plugin_config(plugin: PluginSpec) -> dict[str, Any]: - """加载插件配置,返回普通字典。""" +def load_plugin_config_schema(plugin: PluginSpec) -> dict[str, Any]: + """加载插件配置 schema,解析失败时记录日志并返回空对象。""" schema_path = plugin.plugin_dir / CONFIG_SCHEMA_FILE if not schema_path.exists(): return {} try: schema_payload = json.loads(schema_path.read_text(encoding="utf-8")) - except Exception: - schema_payload = {} - schema = schema_payload if isinstance(schema_payload, dict) else {} + except json.JSONDecodeError as exc: + _LOGGER.warning( + "Failed to parse SDK plugin config schema %s: %s", + schema_path, + exc, + ) + return {} + except OSError as exc: + _LOGGER.warning( + "Failed to read SDK plugin config schema %s: %s", + schema_path, + exc, + ) + return {} + if not isinstance(schema_payload, dict): + _LOGGER.warning( + "SDK plugin config schema %s must be a JSON object, got %s", + schema_path, + type(schema_payload).__name__, + ) + return {} + return schema_payload + + +def save_plugin_config( + plugin: PluginSpec, + payload: dict[str, Any], + *, + schema: dict[str, Any] | None = None, +) -> dict[str, Any]: + """按 schema 归一化并写回插件配置。""" + active_schema = ( + load_plugin_config_schema(plugin) if schema is None else dict(schema) + ) + normalized = { + key: _normalize_config_value(field_schema, payload.get(key)) + for key, field_schema in active_schema.items() + if isinstance(field_schema, dict) + } + + config_path = _plugin_config_path(plugin.plugin_dir, plugin.name) + config_path.parent.mkdir(parents=True, exist_ok=True) + config_path.write_text( + json.dumps(normalized, ensure_ascii=False, indent=2), + encoding="utf-8", + ) + return normalized + + +def load_plugin_config( + plugin: PluginSpec, + *, + schema: dict[str, Any] | None = None, +) -> dict[str, Any]: + """加载插件配置,返回普通字典。""" + active_schema = ( + load_plugin_config_schema(plugin) if schema is None else dict(schema) + ) + if not active_schema: + return {} config_path = _plugin_config_path(plugin.plugin_dir, plugin.name) try: @@ -521,21 +580,29 @@ def load_plugin_config(plugin: PluginSpec) -> dict[str, Any]: if config_path.exists() else {} ) - except Exception: + except json.JSONDecodeError as exc: + _LOGGER.warning( + "Failed to parse SDK plugin config %s: %s", + config_path, + exc, + ) + existing_payload = {} + except OSError as exc: + _LOGGER.warning( + "Failed to read SDK plugin config %s: %s", + config_path, + exc, + ) existing_payload = {} existing = existing_payload if isinstance(existing_payload, dict) else {} normalized = { key: _normalize_config_value(field_schema, existing.get(key)) - for key, field_schema in schema.items() + for key, field_schema in active_schema.items() if isinstance(field_schema, dict) } if not config_path.exists() or normalized != existing: - config_path.parent.mkdir(parents=True, exist_ok=True) - config_path.write_text( - json.dumps(normalized, ensure_ascii=False, indent=2), - encoding="utf-8", - ) + save_plugin_config(plugin, normalized, schema=active_schema) return normalized From e74123bba01a51e676dc81673abc2a5bf33062da Mon Sep 17 00:00:00 2001 From: whatevertogo Date: Thu, 19 Mar 2026 05:12:46 +0800 Subject: [PATCH 3/3] =?UTF-8?q?feat:=20=E5=A2=9E=E5=BC=BA=E8=A3=85?= =?UTF-8?q?=E9=A5=B0=E5=99=A8=E5=8A=9F=E8=83=BD=EF=BC=8C=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E4=BC=9A=E8=AF=9D=E5=91=BD=E4=BB=A4=E6=94=AF=E6=8C=81=E5=8F=8A?= =?UTF-8?q?=E7=9B=B8=E5=85=B3=E6=9D=83=E9=99=90=E5=92=8C=E9=99=90=E6=B5=81?= =?UTF-8?q?=E8=A3=85=E9=A5=B0=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/astrbot_sdk/decorators.py | 48 ++++++++++++++++++- src/astrbot_sdk/runtime/handler_dispatcher.py | 4 +- 2 files changed, 48 insertions(+), 4 deletions(-) diff --git a/src/astrbot_sdk/decorators.py b/src/astrbot_sdk/decorators.py index 7caf12d11..9de03c5e6 100644 --- a/src/astrbot_sdk/decorators.py +++ b/src/astrbot_sdk/decorators.py @@ -3,13 +3,30 @@ 提供声明式的方法来注册 handler 和 capability。 装饰器会在方法上附加元数据,由 Star.__init_subclass__ 自动收集。 -可用的装饰器: +触发器装饰器: - @on_command: 命令触发器 - @on_message: 消息触发器(关键词/正则) - @on_event: 事件触发器 - @on_schedule: 定时任务触发器 - - @require_admin: 权限标记 + - @conversation_command: 带会话生命周期的命令触发器 + +权限与过滤装饰器: + - @require_admin / @admin_only: 管理员权限标记 + - @platforms: 限定平台 + - @group_only / @private_only: 群聊/私聊限定 + - @message_types: 消息类型过滤 + +限流装饰器: + - @rate_limit: 滑动窗口限流 + - @cooldown: 冷却时间 + +优先级装饰器: + - @priority: 设置执行优先级 + +能力导出装饰器: - @provide_capability: 声明对外暴露的能力 + - @register_llm_tool: 注册 LLM 工具 + - @register_agent: 注册 Agent Example: class MyPlugin(Star): @@ -645,8 +662,35 @@ def conversation_command( busy_message: str | None = None, grace_period: float = 1.0, ) -> Callable[[HandlerCallable], HandlerCallable]: + """注册带会话生命周期的命令处理方法。 + + 在 ``on_command`` 基础上附加会话元数据,支持超时、并发策略和宽限期控制。 + + Args: + command: 命令名称或序列(首项为正式名,其余视为别名) + aliases: 额外别名列表 + description: 命令描述 + timeout: 会话超时时间(秒),必须为正整数 + mode: 会话冲突时的行为: + - ``"replace"``: 替换当前会话 + - ``"reject"``: 拒绝新请求 + busy_message: 拒绝新请求时的提示消息 + grace_period: 宽限期(秒),用于会话生命周期处理 + + Returns: + 装饰器函数 + + Raises: + ValueError: mode 不合法、timeout 非正整数或 grace_period 非正数 + + Example: + @conversation_command("chat", timeout=120, mode="reject", busy_message="请稍后再试") + async def chat(self, event: MessageEvent, ctx: Context): + await event.reply("开始对话...") + """ if mode not in {"replace", "reject"}: raise ValueError("conversation_command mode must be 'replace' or 'reject'") + # bool 是 int 子类,需单独排除 if isinstance(timeout, bool) or int(timeout) <= 0: raise ValueError("conversation_command timeout must be a positive integer") if float(grace_period) <= 0: diff --git a/src/astrbot_sdk/runtime/handler_dispatcher.py b/src/astrbot_sdk/runtime/handler_dispatcher.py index 2b32d9cfd..e9e2291d4 100644 --- a/src/astrbot_sdk/runtime/handler_dispatcher.py +++ b/src/astrbot_sdk/runtime/handler_dispatcher.py @@ -60,12 +60,12 @@ from ..protocol.descriptors import ( from ..schedule import ScheduleContext from ..session_waiter import SessionWaiterManager from ..star import Star -from .capability_dispatcher import CapabilityDispatcher from ._command_matching import ( build_command_args, build_regex_args, match_command_name, ) +from .capability_dispatcher import CapabilityDispatcher from .limiter import LimiterEngine from .loader import LoadedHandler @@ -456,7 +456,7 @@ class HandlerDispatcher: ) -> dict[str, Any]: assert loaded.conversation is not None conversation_meta = loaded.conversation - summary = {"sent_message": False, "stop": False, "call_llm": False} + summary = {"sent_message": False, "stop": True, "call_llm": False} key = f"{self._resolve_plugin_id(loaded)}:{event.session_id}" active = self._conversations.get(key) if active is not None and not active.task.done():