mirror of
https://github.com/AstrBotDevs/AstrBot
synced 2026-07-20 02:55:08 +08:00
* Add legacy session waiter compatibility * Expand legacy astrbot package compatibility * Tighten external legacy plugin compatibility smoke tests * Consolidate controlled legacy facade compatibility * docs: clarify compat package boundaries * test: align runtime fixtures with maintained samples * fix: preserve unicode sample fixtures in runtime tests * Implement legacy hook and tool compat runtime * Refactor legacy runtime execution boundary * 增强旧版兼容性,添加多个旧路径入口和相关功能 * 增强旧版兼容性,添加适配器边界的启动和关闭钩子支持 * Refactor legacy API and LLM compatibility logic - Moved legacy LLM and tool compatibility logic from `_legacy_api.py` to a new module `_legacy_llm.py` for better organization and separation of concerns. - Updated `_legacy_api.py` to import necessary components from `_legacy_llm.py`, removing redundant code. - Enhanced database client functionality by adding support for batch read/write operations and change event subscriptions. - Improved documentation in the database client and capability router to reflect new features. - Refined environment management process in the loader to better handle plugin grouping and virtual environment management. * 补充插件分组环境测试覆盖 * feat: Enhance CLI and testing capabilities - Added a new script entry point `astrbot-sdk` in `pyproject.toml`. - Introduced `has_waiter` method in `SessionWaiterManager` to check for existing waiters. - Updated `cli.py` to improve error handling and added context to error messages. - Implemented local development support in `cli.py` with a new `dev` command for running plugins against a mock core. - Created a new testing module `astrbot_sdk.testing` with utilities for local development and plugin testing. - Added comprehensive tests for the new testing module and CLI commands. - Improved compatibility and error messaging for plugin loading failures. * feat: 添加插件初始化、验证和构建命令,增强 CLI 功能 * feat: add platform client documentation and examples - Introduced platform client documentation in `docs/v4/clients/platform.md` detailing methods for sending messages, images, and managing group members. - Added example plugins for LLM chat and database functionalities in `docs/v4/examples/README.md`, `docs/v4/examples/llm-chat/README.md`, and `docs/v4/examples/database/README.md`. - Enhanced quickstart guide with links to new documentation and example plugins. - Implemented runtime contract tests to ensure compatibility of public capabilities and hooks. * Refactor legacy runtime handling and improve plugin loading - Updated `handler_dispatcher.py` to streamline legacy runtime preparation and dispatching results. - Enhanced `loader.py` to simplify legacy plugin detection and manifest building. - Added tests for new HTTPClient and MetadataClient functionalities. - Introduced tests for legacy context metadata methods and legacy loader helpers. - Improved legacy runtime tests to cover new functionality and edge cases. * refactor: 更新兼容层和导入路径,优化文档描述 * Support grouped plugin workers in shared environments - add group metadata driven worker startup for shared env plans - track per-plugin handler and capability ownership inside grouped workers - update runtime and smoke tests for grouped worker session behavior * Add v4 compat layer and legacy shims - Introduce private v4 compatibility surface using _legacy_api.py, _legacy_runtime.py, _legacy_loader.py plus new _legacy_context.py and _legacy_star.py to centralize legacy adapters while keeping public APIs thin. - Extend InitializeOutput to carry protocol_version for negotiated protocol, enabling runtime to adapt to the chosen v4 version. - Add lightweight legacy support for Star/Context via new LegacyStar and LegacyContext shims and expose legacy API through the aggregate _legacy_api entry point. - Ensure legacy loader preserves class declaration order by iterating module.__dict__ instead of relying on alphabetical sorting. - Add tests: protocol_version handling in InitializeOutput, legacy main component order preservation, and embedded-newline framing in transport tests. * Add architecture doc and refine API compat - Add PROJECT_ARCHITECTURE.md documenting architecture, compat surface, and testing notes. - Update astrbot_sdk.api.__init__ to clarify it is a compatibility implementation layer, not a simple facade, and list migration targets. - Normalize platform in AstrMessageEvent.to_payload to emit a string id by using get_platform_id(). * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * delete old sdk * delete old sdk (#7) Co-authored-by: whatevertogo <whatevertogo@users.noreply.github.com> * feat(cli): normalize plugin init skeletons Add interactive plugin init prompts and normalize generated plugin names to the astrbot_plugin_ convention. Update CLI tests for the new skeleton layout and ignore generated plugin directories in git and coverage tooling. Also include related runtime logging adjustments from the current worktree. * Create lint.yml * Update lint.yml * clean it * feat: Enhance handler and capability dispatchers with improved error handling - Updated HandlerDispatcher to raise TypeError for uninjectable required parameters, logging errors appropriately. - Refactored CapabilityDispatcher to raise TypeError for missing required parameters during capability execution. - Renamed _load_plugin_config to load_plugin_config for clarity and consistency. - Introduced _sync_plugin_registry method in SupervisorRuntime to manage plugin capabilities more effectively. - Enhanced capability registration logic to handle naming conflicts with better logging and automatic renaming. - Added tests for handler and capability dispatchers to ensure proper error handling and functionality. - Implemented new HTTP and metadata capabilities with corresponding tests for registration and retrieval. - Improved MemoryClient methods with additional tests for save_with_ttl, get_many, delete_many, and stats. - Added tests for the testing module to ensure proper import and functionality of PluginHarness. * feat: 更新 SDK 描述,重构插件调用上下文,移除插件 ID 传递,增强能力路由和 HTTP 客户端的插件身份管理 * refactor: 更新文档和代码注释,优化兼容性描述,增强可读性 * feat: 添加插件热重载功能,支持文件变更时自动重新加载插件 * feat: 增强错误处理,添加上下文信息,优化插件组件加载和参数注入校验 * feat: 添加 hello_plugin 示例,包含插件结构、命令处理和测试用例 * refactor: 删除过时的架构文档、变更日志和兼容矩阵文件 * refactor: 更新兼容层弃用通知,优化文档结构和可读性 * refactor: 更新项目架构文档,增强能力客户端和执行边界的描述,移除兼容层设计章节 * feat(errors): Enhance AstrBotError with detailed documentation and examples feat(events): Expand MessageEvent with reply capabilities and detailed docstrings fix(loader): Ensure plugin path is correctly managed in sys.path feat(star): Improve Star class documentation and lifecycle method descriptions feat(testing): Add plugin metadata handling in MockContext and enhance PluginHarness feat(hello): Refactor HelloPlugin to utilize new reply methods and structured capabilities test(decorators): Add tests for input/output model support in provide_capability test(events): Implement tests for reply_image and reply_chain methods in MessageEvent test(http): Validate API registration with capability handler references and error handling test(tests): Enhance tests for plugin harness and directory handling in dev commands * feat(runtime): add configurable msgpack wire codec support * fix(runtime): align msgpack framing with transport defaults * fix(runtime): preserve json transport compatibility * fix(cli): scope worker wire codec option * feat: 添加 AGENTS.md 文档,描述 v4 架构约束和开发命令 refactor: 更新 HandlerDispatcher 和 WorkerSession,增强参数处理和结果汇总逻辑 * fix(test): 更新 init_plugin 测试以匹配新的目录命名规范 CLI 的 _normalize_init_plugin_name 函数现在自动添加 astrbot_plugin_ 前缀, 测试期望的目录名从 demo_plugin 更新为 astrbot_plugin_demo_plugin。 * Refactor worker initialization and remove unused codec parameters; add schedule and session waiter modules - Simplified `GroupWorkerRuntime` and `PluginWorkerRuntime` constructors by removing the codec parameter and related logic. - Introduced `schedule.py` to define `ScheduleContext` for managing scheduled tasks with a clear structure and payload handling. - Added `session_waiter.py` for session-based conversational flow management, including `SessionController` and `SessionWaiterManager` for handling multi-turn dialogues. - Enhanced testing utilities in `testing.py` by removing unused classes and streamlining the structure. - Created `types.py` to introduce `GreedyStr` for improved command parameter parsing. * feat: 添加 LLM 工具管理和会话级别状态管理能力 - 新增 llm/ 模块,包含 LLMToolSpec、ProviderRequest、AgentSpec 等实体 - 新增 LLMToolManager 用于管理 LLM 工具注册和激活状态 - 新增 SessionPluginManager 用于会话级别的插件启用状态管理 - 新增 SessionServiceManager 用于会话级别的 LLM/TTS 服务状态管理 - 新增 RegistryClient 用于查询 handler 元数据和设置白名单 - 扩展 CapabilityRouter 内置能力,支持 session.* 和 registry.* 命名空间 - 增强描述符和装饰器以支持新的 trigger 类型 * feat: 大幅增强 SDK 核心功能和文档 新增模块: - clients/files.py: 文件上传/下载客户端 - clients/managers.py: 会话/LLM/Provider 管理器 - clients/provider.py: LLM Provider 客户端 - conversation.py: 对话上下文管理 - plugin_kv.py: 插件 KV 存储辅助 - runtime/limiter.py: 限流器 - star_tools.py: Star 工具函数 - docs/: 完整的 SDK 使用文档 (01-05) 功能增强: - Context 大幅扩展,增加 reply/send_image/typing 等便捷方法 - 装饰器增强,支持 on_llm_request/on_provider_request 等 - 内置 schemas 扩展,覆盖更多 capability 定义 - capability_router_builtins 大幅扩展内置能力实现 - handler_dispatcher 增强参数注入和错误处理 - Star 基类增加生命周期钩子和工具方法 * Add comprehensive API documentation for types and utilities in AstrBot SDK - Introduced `types.md` detailing type aliases, generics, and Pydantic models used in the SDK. - Added `utils.md` covering utility classes and functions including CancelToken, MessageSession, command groups, and session management. - Included usage examples and detailed descriptions for each component to enhance developer understanding and ease of use. * feat: 添加高级方法和辅助函数文档,增强消息组件和事件处理功能 * feat: 增强过滤器类型和能力路由文档,添加 Provider 和会话管理功能 * change location * delete no need thing * delete again * feat: add Star plugin base class and StarTools utility class - Introduced `Star` class as a base for v4 native plugins, providing lifecycle methods and context management. - Added `StarTools` class for accessing runtime context and managing LLM tools. - Implemented `PluginHarness` for local development and testing of plugins, allowing for message dispatching and lifecycle management. - Created `GreedyStr` type for enhanced command parameter parsing, enabling the capture of remaining command text as a single argument. - Added testing utilities and mock capabilities for plugin development. * delete: remove hello_plugin example and its related files * Remove obsolete test files for testing module, top-level modules, transport, and wire codecs - Deleted `test_testing_module.py` as it is no longer needed. - Removed `test_top_level_modules.py` which had no content. - Eliminated `test_transport.py` due to redundancy. - Cleared out `test_wire_codecs.py` as part of the cleanup. * fix(runtime): avoid creating Star instance in on_error fallback * fix(runtime): avoid virtual dispatch in Star.on_error fallback * refactor(runtime): unify command matching logic (#25) * refactor(testing): share command matching with handler dispatcher * fix:添加公共函数文件 * fix: simplify register_task completion handling (#27) * fix: simplify register_task completion handling Remove duplicated cancellation logging in Context.register_task while keeping Future inputs compatible with asyncio.create_task semantics. Add regression coverage for coroutine, Future, cancellation, and failure paths. * fix: prioritize local src in tests_v4 Ensure tests_v4 always imports the working tree package by moving src to sys.path[0] even when another checkout or installed copy is already present. * chore: sync subtree from AstrBot * feat: replay non-sdk changes on clean sdk subtree baseline * feat(sdk): add merged provider config capability support Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> * feat(sdk): add merged provider config bridge and client Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> * feat(sdk): add merged provider config capability support Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> * fix(sdk): tighten bridge cast typing Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> * feat(sdk): add merged provider config bridge and client Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> * test(sdk): cover merged provider config parity Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> * feat: 完善 memory 向量检索与索引统计 (#28) Co-authored-by: united_pooh <united_pooh@outlook.com> * feat(tests): 添加测试用例以验证 register_task 的行为并更新测试运行说明 * Merge commit 'e45bade147ff44b43860ecff12067309e59c151a' into feat/sdk-integration * Squashed 'astrbot-sdk/' changes from 7dda6077..85342f1485342f14feat(tests): 添加测试用例以验证 register_task 的行为并更新测试运行说明fdffc09bMerge pull request #26 from united-pooh/fix/fix-star-on-error-fallback3b09747cfeat: 完善 memory 向量检索与索引统计 (#28)665c9c69fix(runtime): avoid virtual dispatch in Star.on_error fallback200559a5fix(runtime): avoid creating Star instance in on_error fallback git-subtree-dir: astrbot-sdk git-subtree-split:85342f149b* feat(tests): 添加测试用例以验证平台和消息类型过滤器的冲突处理 * docs: remove redundant testing instructions from AGENTS.md * docs: remove redundant testing instructions from AGENTS.md * docs: remove redundant testing instructions from AGENTS.md * Merge commit '5ac9401852ddb46f337da6bcc0f9b66eed265da9' into feat/sdk-integration * Squashed 'astrbot-sdk/' changes from 85342f14..09beabeb09beabebfeat(tests): 添加测试用例以验证平台和消息类型过滤器的冲突处理 git-subtree-dir: astrbot-sdk git-subtree-split:09beabeb62* feat: enhance SDK plugin configuration handling and logging * feat: enhance SDK plugin configuration handling and logging * feat: enhance SDK plugin configuration handling and logging * feat: 增强装饰器功能,添加会话命令支持及相关权限和限流装饰器 * feat: 增强装饰器功能,添加会话命令支持及相关权限和限流装饰器 * feat: 增强装饰器功能,添加会话命令支持及相关权限和限流装饰器 * feat: 更新文档以反映SDK负载的JSON可序列化要求和延迟导入设计约束 * feat: add conversation.get_current capability and related schemas - Introduced CONVERSATION_GET_CURRENT_INPUT_SCHEMA and CONVERSATION_GET_CURRENT_OUTPUT_SCHEMA for handling current conversation requests. - Implemented _conversation_get_current method in BuiltinCapabilityRouterMixin to manage current conversation retrieval and creation. - Registered the new capability in CoreCapabilityBridge. - Enhanced HandlerDispatcher to inject provider request, LLM response, and event result payloads into the event handling process. - Updated tests to validate the new functionality and ensure proper payload handling. * feat: add conversation.get_current capability and related schemas - Introduced CONVERSATION_GET_CURRENT_INPUT_SCHEMA and CONVERSATION_GET_CURRENT_OUTPUT_SCHEMA for handling current conversation requests. - Implemented _conversation_get_current method in BuiltinCapabilityRouterMixin to manage current conversation retrieval and creation. - Registered the new capability in CoreCapabilityBridge. - Enhanced HandlerDispatcher to inject provider request, LLM response, and event result payloads into the event handling process. - Updated tests to validate the new functionality and ensure proper payload handling. * feat: add conversation.get_current capability and related schemas - Introduced CONVERSATION_GET_CURRENT_INPUT_SCHEMA and CONVERSATION_GET_CURRENT_OUTPUT_SCHEMA for handling current conversation requests. - Implemented _conversation_get_current method in BuiltinCapabilityRouterMixin to manage current conversation retrieval and creation. - Registered the new capability in CoreCapabilityBridge. - Enhanced HandlerDispatcher to inject provider request, LLM response, and event result payloads into the event handling process. - Updated tests to validate the new functionality and ensure proper payload handling. * Refactor tool call handling in SdkPluginBridge - Introduced a dictionary to map tool call IDs to tool names for better clarity and efficiency. - Enhanced the extraction of tool call information from raw results, ensuring compatibility with both dictionary and object formats. - Updated the logic to retrieve tool names based on tool call IDs, improving the robustness of the tool calls result processing. * Refactor tool call handling in SdkPluginBridge - Introduced a dictionary to map tool call IDs to tool names for better clarity and efficiency. - Enhanced the extraction of tool call information from raw results, ensuring compatibility with both dictionary and object formats. - Updated the logic to retrieve tool names based on tool call IDs, improving the robustness of the tool calls result processing. * Refactor tool call handling in SdkPluginBridge - Introduced a dictionary to map tool call IDs to tool names for better clarity and efficiency. - Enhanced the extraction of tool call information from raw results, ensuring compatibility with both dictionary and object formats. - Updated the logic to retrieve tool names based on tool call IDs, improving the robustness of the tool calls result processing. * feat: add session and system capabilities for plugin management and event handling - Implemented SessionCapabilityMixin with methods to manage session-level plugin states and handlers. - Added SystemCapabilityMixin to handle system-level functionalities including file management, event handling, and dynamic command registration. - Introduced methods for enabling/disabling plugins, filtering handlers, and managing LLM and TTS service states. - Registered various system capabilities for data directory access, HTML rendering, and event streaming. * feat: add session and system capabilities for plugin management and event handling - Implemented SessionCapabilityMixin with methods to manage session-level plugin states and handlers. - Added SystemCapabilityMixin to handle system-level functionalities including file management, event handling, and dynamic command registration. - Introduced methods for enabling/disabling plugins, filtering handlers, and managing LLM and TTS service states. - Registered various system capabilities for data directory access, HTML rendering, and event streaming. * Squashed 'astrbot-sdk/' changes from 09beabeb..3204c9db3204c9dbMerge sdk-remote dev into feat/sdk-integration3a2d715eRefactor tool call handling in SdkPluginBridgeed1b9665feat: add conversation.get_current capability and related schemase74123bbfeat: 增强装饰器功能,添加会话命令支持及相关权限和限流装饰器bb361cf9feat: 增强装饰器功能,添加会话命令支持及相关权限和限流装饰器c6237f52Merge sdk-remote/dev into astrbot-sdk subtreee12029fffeat: enhance SDK plugin configuration handling and logging5e54bbb3feat: enhance SDK plugin configuration handling and loggingf48e2041Merge commit '5ac9401852ddb46f337da6bcc0f9b66eed265da9' into feat/sdk-integration619672e6Merge commit '5ac9401852ddb46f337da6bcc0f9b66eed265da9' into feat/sdk-integrationd5a3796ddocs: remove redundant testing instructions from AGENTS.md323e3f4ddocs: remove redundant testing instructions from AGENTS.mdf8438a7bMerge commit 'e45bade147ff44b43860ecff12067309e59c151a' into feat/sdk-integration96d1df85Merge commit 'e45bade147ff44b43860ecff12067309e59c151a' into feat/sdk-integrationf8a7e253feat(sdk): add merged provider config bridge and client752dc6cffeat(sdk): add merged provider config capability support git-subtree-dir: astrbot-sdk git-subtree-split:3204c9db9f* Implement feature X to enhance user experience and optimize performance * Implement feature X to enhance user experience and optimize performance * chore(sdk): stop tracking uv.lock * feat(sdk): add merged provider config capability support Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> * feat(sdk): add merged provider config bridge and client Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> * feat(tests): 添加测试用例以验证 register_task 的行为并更新测试运行说明 * feat(tests): 添加测试用例以验证平台和消息类型过滤器的冲突处理 * docs: remove redundant testing instructions from AGENTS.md * feat: enhance SDK plugin configuration handling and logging * feat: 增强装饰器功能,添加会话命令支持及相关权限和限流装饰器 * feat: add conversation.get_current capability and related schemas - Introduced CONVERSATION_GET_CURRENT_INPUT_SCHEMA and CONVERSATION_GET_CURRENT_OUTPUT_SCHEMA for handling current conversation requests. - Implemented _conversation_get_current method in BuiltinCapabilityRouterMixin to manage current conversation retrieval and creation. - Registered the new capability in CoreCapabilityBridge. - Enhanced HandlerDispatcher to inject provider request, LLM response, and event result payloads into the event handling process. - Updated tests to validate the new functionality and ensure proper payload handling. * Refactor tool call handling in SdkPluginBridge - Introduced a dictionary to map tool call IDs to tool names for better clarity and efficiency. - Enhanced the extraction of tool call information from raw results, ensuring compatibility with both dictionary and object formats. - Updated the logic to retrieve tool names based on tool call IDs, improving the robustness of the tool calls result processing. * feat: add session and system capabilities for plugin management and event handling - Implemented SessionCapabilityMixin with methods to manage session-level plugin states and handlers. - Added SystemCapabilityMixin to handle system-level functionalities including file management, event handling, and dynamic command registration. - Introduced methods for enabling/disabling plugins, filtering handlers, and managing LLM and TTS service states. - Registered various system capabilities for data directory access, HTML rendering, and event streaming. * fix(runtime): avoid creating Star instance in on_error fallback * fix(runtime): avoid virtual dispatch in Star.on_error fallback * feat: refactor injected parameter handling and introduce is_framework_injected_parameter utility * Squashed 'astrbot-sdk/' changes from 027c15b4..d078e510d078e510feat: refactor injected parameter handling and introduce is_framework_injected_parameter utility461f7276Merge branch 'dev' of https://github.com/united-pooh/astrbot-sdk into dev5ead59c4fix(runtime): avoid virtual dispatch in Star.on_error fallbackd2382858fix(runtime): avoid creating Star instance in on_error fallbacke961e361feat: add session and system capabilities for plugin management and event handling5a46321aRefactor tool call handling in SdkPluginBridge47698448feat: add conversation.get_current capability and related schemas9b35bec8feat: 增强装饰器功能,添加会话命令支持及相关权限和限流装饰器48a20240feat: enhance SDK plugin configuration handling and loggingcb593a53docs: remove redundant testing instructions from AGENTS.mdf4942076feat(tests): 添加测试用例以验证平台和消息类型过滤器的冲突处理b0f8b2d6feat(tests): 添加测试用例以验证 register_task 的行为并更新测试运行说明6e417c6dfeat(sdk): add merged provider config bridge and client659eabcefeat(sdk): add merged provider config capability support REVERT:027c15b4Implement feature X to enhance user experience and optimize performance REVERT:c272661fchore: pull sdk subtree from dev (resolve delete/modify conflict) REVERT:0a2a3592feat: add session and system capabilities for plugin management and event handling REVERT:3204c9dbMerge sdk-remote dev into feat/sdk-integration REVERT:36443f1dRefactor tool call handling in SdkPluginBridge REVERT:3a2d715eRefactor tool call handling in SdkPluginBridge REVERT:b93c2c2bfeat: add conversation.get_current capability and related schemas REVERT:ed1b9665feat: add conversation.get_current capability and related schemas REVERT:e74123bbfeat: 增强装饰器功能,添加会话命令支持及相关权限和限流装饰器 REVERT:bb361cf9feat: 增强装饰器功能,添加会话命令支持及相关权限和限流装饰器 REVERT:c6237f52Merge sdk-remote/dev into astrbot-sdk subtree REVERT:e12029fffeat: enhance SDK plugin configuration handling and logging REVERT:5e54bbb3feat: enhance SDK plugin configuration handling and logging REVERT:f48e2041Merge commit '5ac9401852ddb46f337da6bcc0f9b66eed265da9' into feat/sdk-integration REVERT:619672e6Merge commit '5ac9401852ddb46f337da6bcc0f9b66eed265da9' into feat/sdk-integration REVERT:d5a3796ddocs: remove redundant testing instructions from AGENTS.md REVERT:323e3f4ddocs: remove redundant testing instructions from AGENTS.md REVERT:f8438a7bMerge commit 'e45bade147ff44b43860ecff12067309e59c151a' into feat/sdk-integration REVERT:96d1df85Merge commit 'e45bade147ff44b43860ecff12067309e59c151a' into feat/sdk-integration REVERT:f8a7e253feat(sdk): add merged provider config bridge and client REVERT:752dc6cffeat(sdk): add merged provider config capability support git-subtree-dir: astrbot-sdk git-subtree-split:d078e51051* refactor: reorganize imports and enhance type hints in sdk_bridge modules * refactor: update import paths to use Path for better compatibility * refactor(injection): centralize legacy injected parameter filtering * fix(testing): use public session waiter probe in PluginHarness * docs: add TODO for documentation content in _command_model.py * feat: add memory management capabilities to CoreCapabilityBridge and implement unit tests * feat: enhance memory search functionality and improve metadata retrieval in SDK * feat: add memory management attributes and typed provider method to CapabilityMixinHost * Implement feature X to enhance user experience and optimize performance * Refactor memory utility functions and enhance memory capability mixin - Added new utility functions for memory management in _memory_utils.py. - Refactored memory capability mixin methods to utilize the new utility functions for better readability and maintainability. - Updated PROJECT_ARCHITECTURE.md to reflect changes in documentation and structure. * Squashed 'astrbot-sdk/' changes from d078e510..208bc591208bc591Merge pull request #30 from united-pooh/refactor/unify-legacy-injected-paramsd86534a2docs: add TODO for documentation content in _command_model.py090724a7refactor(injection): centralize legacy injected parameter filtering git-subtree-dir: astrbot-sdk git-subtree-split:208bc591dd* Initial plan * docs: fix path, Python version, and client API table in PROJECT_ARCHITECTURE.md Co-authored-by: whatevertogo <149563971+whatevertogo@users.noreply.github.com> * Squashed 'astrbot-sdk/' changes from 208bc591..ad5e8d13ad5e8d13Merge pull request #37 from united-pooh/sdk/whatevertogo5751701fMerge pull request #38 from united-pooh/copilot/sub-pr-37e21acba5docs: fix path, Python version, and client API table in PROJECT_ARCHITECTURE.mdee67cab4Initial plan7d921570Refactor memory utility functions and enhance memory capability mixin git-subtree-dir: astrbot-sdk git-subtree-split:ad5e8d1397* docs: fix TODO comment formatting in _command_model.py * 集成SDK命令候选项,优化Telegram和Discord平台适配器的命令收集逻辑 * 删除AI女友插件的单元测试文件 * 更新文档,添加Telegram和Discord原生命令菜单的注册说明 * Squashed 'astrbot-sdk/' changes from ad5e8d13..5003da585003da58Merge pull request #40 from united-pooh/sdk/whatevertogob5084c44Merge branch 'sdk/whatevertogo' of https://github.com/united-pooh/astrbot-sdk into sdk/whatevertogo7559edf7docs: fix TODO comment formatting in _command_model.py git-subtree-dir: astrbot-sdk git-subtree-split:5003da58f5* fix(testing): route session waiter followups through dispatcher (#33) * fix(testing): route session waiter followups through dispatcher * fix(testing): preserve waiter context and completion state * fix(runtime): preserve session waiter plugin identity * fix(runtime): scope session waiters by plugin * fix(testing): isolate waiter replacement and followup drains * fix(runtime): normalize waiter routing inputs * fix(cli): route protocol stdout at command entry (#41) * 添加内存后端支持,优化插件内存管理逻辑 * feat(memory): enhance memory schemas and add namespace support - Updated MEMORY_SEARCH_INPUT_SCHEMA to include `namespace` and `include_descendants`. - Modified MEMORY_SEARCH_OUTPUT_SCHEMA to allow nullable `namespace`. - Added `namespace` to MEMORY_GET_INPUT_SCHEMA, MEMORY_DELETE_INPUT_SCHEMA, MEMORY_SAVE_WITH_TTL_INPUT_SCHEMA, MEMORY_GET_MANY_INPUT_SCHEMA, and MEMORY_DELETE_MANY_INPUT_SCHEMA. - Enhanced MEMORY_STATS_INPUT_SCHEMA to support `namespace` and `include_descendants`. - Updated MEMORY_GET_OUTPUT_SCHEMA and MEMORY_STATS_OUTPUT_SCHEMA to include `namespace` and `namespace_count`. - Introduced `_memory_backends` in CapabilityRouterHost and CapabilityRouterBridgeBase for better memory management. - Refactored MemoryCapabilityMixin to utilize memory backends for plugin-specific memory operations. - Improved memory search functionality to respect namespaces and include descendants based on input parameters. - Added tests to validate memory operations across different namespaces and ensure persistence across restarts. - Implemented error handling in the handler dispatcher to manage exceptions gracefully. * fix: guard session_waiter blocking usage * fix(runtime): preserve request-scoped system event overlays * test(runtime): lock peer initialization and transport failure semantics * test(loader): cover plugin reload and import isolation regressions * refactor(supervisor): clarify plugin registry sync phases * test(clients): cover provider lifecycle regressions * feat(cli): improve astr init defaults * feat(plugin): add plugin ID validation and data directory resolution - Implemented `validate_plugin_id` to ensure safe plugin identifiers. - Added `resolve_plugin_data_dir` to resolve plugin data directories securely. - Updated memory and system capabilities to utilize new plugin ID validation. - Refactored session waiter management to simplify plugin ID handling. - Enhanced tests for plugin ID validation and data directory resolution. * fix(cli): exit cleanly on init abort * feat(memory): enhance namespace handling and add tests for memory client * feat(agent): add tool status message handling and improve SDK command integration * Refactor SDK structure for backward compatibility - Moved message result and session classes to internal modules while preserving legacy import paths for compatibility. - Updated imports across the SDK to reflect the new internal structure. - Enhanced session waiter management to support multiple plugins and improve error handling. - Added tests to ensure LLM tool registration and session waiter functionality align with dispatcher expectations. - Cleaned up code and improved documentation for clarity and maintainability. * 增强异步下载功能,更新组件导入方式,并添加相关测试用例 * 添加任务重入锁以支持会话等待器的嵌套清理,并更新相关测试用例以验证后续消息的序列化处理 * 添加 .astrbot_sdk_testing 到 .gitignore 文件 * Add unit tests for provider management and tool capabilities - Introduced new test suite for provider platform management in `test_sdk_provider_platform_management.py`, covering scenarios for merged provider configurations, reserved plugin checks, and provider management functionalities. - Added tests for tool capabilities and provider queries in `test_sdk_provider_tool_platform_capabilities.py`, validating interactions with LLM tools and specialized proxies. - Removed obsolete `test_sdk_transport.py` as it contained outdated tests for transport layer functionality. * 添加多个模块和测试用例,增强SDK功能并支持单元测试 * fix(bridge): add missing capability registrations for db/memory/http/metadata Register methods for db, memory, http, and metadata capabilities exist in BasicCapabilityMixin but were never called in CoreCapabilityBridge.__init__. This caused SDK plugins using ctx.memory, ctx.db, ctx.http to fail with "LookupError: capability not found". * feat(kb): enhance knowledge base capabilities with document management and serialization * 添加知识库文档管理功能,包括文档上传、列表、获取、删除和刷新能力,更新相关的能力路由和协议模式 * feat(conversation): add ability to unset conversation persona and update related methods * 添加对话管理和元数据管理功能,包括清空对话人格和保存插件配置的能力 * feat(conversation): add test for unsetting conversation persona and verify state * feat(plugin): add save_plugin_config method and related tests for plugin configuration persistence * 优化插件配置保存方法的代码格式 * feat(sdk): enhance handler metadata with descriptions, priority, kind, and admin requirements * 添加描述、优先级和其他元数据到处理程序元数据和描述符中,优化相关功能 * feat(sdk): enhance SDK integration with local extras handling and message payloads * 添加对 MessageEvent 的额外字段支持,优化事件处理器的参数注入,增强 SDK 本地临时数据的管理能力 * 增强插件日志记录功能,添加控制台输出格式化和路径标签支持,新增单元测试以验证日志格式 * feat: Enhance command and tool management in dashboard - Refactor CommandRoute to utilize AstrBotCoreLifecycle for improved command handling. - Introduce command_key for commands to streamline toggling, renaming, and permission updates. - Implement support for SDK commands in the dashboard, marking them as read-only. - Update ToolTable and CommandTable components to use new command_key and tool_key properties. - Add runtime_kind and plugin_id to tools for better management. - Enhance API tests to cover SDK commands and tools, ensuring proper functionality and error handling. - Update localization files to include new messages related to SDK commands. * 优化插件日志记录和能力代理模块,增强异常处理,确保优先级参数为整数 * feat: 增强LLM能力,添加聊天提供者协议和异常处理,更新单元测试以验证提供者有效性 * feat: Implement SDK skill management capabilities - Added SkillCapabilityMixin to handle skill registration, unregistration, and listing. - Integrated skill management into the CoreCapabilityBridge. - Enhanced SkillManager to support SDK-registered skills, including loading, saving, and syncing skills. - Updated computer_client to utilize SkillManager for skill synchronization with sandboxes. - Refactored skill export functionality in SkillsRoute to accommodate new skill management structure. - Introduced tests for SDK skill registration, unregistration, and syncing to ensure functionality. - Improved skill handling in the dashboard and test suite to reflect changes in skill management. * 添加技能注册功能,包含技能注册、注销和列出能力的实现,增强插件能力管理 * 删除CLAUDE.md文档,移除过时的已知问题描述 * 删除 AGENTS.md 文档,移除过时的架构约束和开发命令说明 * 更新测试用例,修复插件发现失败时的错误信息,确保使用正确的运行时字段 * 修复错误处理和权限检查,新增单元测试以验证功能 * format * 增强命名空间管理,优化数据库操作,添加 HTTP 路由验证,新增并发隔离测试,完善命令模型解析单元测试 * 增强 HTTP 路由功能,添加 HTTP 方法注销逻辑的单元测试 * 格式化日志输出,优化批量插入时无内容提供的调试信息 * fix: add uv dependency for plugin environment groups * 删除代码审查文档 CODE_REVIEW_ISSUES.md * 添加消息历史管理功能,包括消息记录的增删查改,完善相关能力混合类和测试用例 * 实现 SDK 消息历史管理功能,包括记录的增删查改操作 * Add unit tests for MCP contract and capabilities in SDK - Implemented `_mcp_contract.py` to test local MCP server functionalities including listing, enabling, and disabling servers. - Created `test_sdk_mcp_capabilities.py` to cover various aspects of the CoreCapabilityBridge and SdkPluginBridge, including session management and global MCP server operations. - Introduced fake classes to simulate MCP server behavior and manage tool configurations for testing. - Ensured comprehensive coverage of MCP session lifecycle, including opening, listing tools, calling tools, and closing sessions. * feat(mcp): Implement local and global MCP server management capabilities - Added MCP management client to the context for local/global MCP service management. - Introduced decorators to acknowledge global MCP risk for plugins. - Defined schemas for MCP server operations including get, list, enable, disable, and session management. - Created MCP capability mixin to handle local and global MCP server operations. - Enhanced provider capabilities to include active local MCP tool names. - Updated capability router to support MCP functionalities and maintain session state. - Added tests for MCP functionalities, ensuring proper behavior and risk acknowledgment. --------- Co-authored-by: whatevertogo <149563971+whatevertogo@users.noreply.github.com> Co-authored-by: whatevertogo <whatevertogo@users.noreply.github.com> Co-authored-by: letr <letr007@foxmail.com> Co-authored-by: united_pooh <united_pooh@outlook.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: united_pooh <united_pooh@icloud.com> Co-authored-by: Lishiling <m18384519631@163.com> Co-authored-by: Li-shi-ling <114913764+Li-shi-ling@users.noreply.github.com> Co-authored-by: letr <123731298+letr007@users.noreply.github.com> Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: catDforD <3276453835@qq.com> Co-authored-by: united_pooh <united_pooh@MBP-F6H6T2CYVV-2219.local>
1793 lines
70 KiB
Python
1793 lines
70 KiB
Python
"""插件的重载、启停、安装、卸载等操作。"""
|
|
|
|
import asyncio
|
|
import contextlib
|
|
import functools
|
|
import inspect
|
|
import json
|
|
import keyword
|
|
import logging
|
|
import os
|
|
import sys
|
|
import tempfile
|
|
import traceback
|
|
from types import ModuleType
|
|
|
|
import anyio
|
|
import yaml
|
|
from anyio import to_thread
|
|
from packaging.specifiers import InvalidSpecifier, SpecifierSet
|
|
from packaging.version import InvalidVersion, Version
|
|
|
|
from astrbot.core import (
|
|
DependencyConflictError,
|
|
logger,
|
|
pip_installer,
|
|
sp,
|
|
)
|
|
from astrbot.core.agent.handoff import FunctionTool, HandoffTool
|
|
from astrbot.core.config.astrbot_config import AstrBotConfig
|
|
from astrbot.core.config.default import VERSION
|
|
from astrbot.core.platform.register import unregister_platform_adapters_by_module
|
|
from astrbot.core.provider.register import llm_tools
|
|
from astrbot.core.utils.astrbot_path import (
|
|
get_astrbot_config_path,
|
|
get_astrbot_path,
|
|
get_astrbot_plugin_path,
|
|
get_astrbot_temp_path,
|
|
)
|
|
from astrbot.core.utils.io import remove_dir
|
|
from astrbot.core.utils.metrics import Metric
|
|
from astrbot.core.utils.requirements_utils import (
|
|
plan_missing_requirements_install,
|
|
)
|
|
|
|
from . import StarMetadata
|
|
from .command_management import sync_command_configs
|
|
from .context import Context
|
|
from .error_messages import format_plugin_error
|
|
from .filter.permission import PermissionType, PermissionTypeFilter
|
|
from .star import star_map, star_registry
|
|
from .star_handler import EventType, star_handlers_registry
|
|
from .updator import PluginUpdator
|
|
|
|
try:
|
|
from watchfiles import PythonFilter, awatch
|
|
except ImportError:
|
|
if os.getenv("ASTRBOT_RELOAD", "0") == "1":
|
|
logger.warning("未安装 watchfiles,无法实现插件的热重载。")
|
|
|
|
|
|
class PluginVersionIncompatibleError(Exception):
|
|
"""Raised when plugin astrbot_version is incompatible with current AstrBot."""
|
|
|
|
|
|
class PluginDependencyInstallError(Exception):
|
|
"""Raised when plugin dependency installation fails."""
|
|
|
|
def __init__(
|
|
self,
|
|
*,
|
|
plugin_label: str,
|
|
requirements_path: str,
|
|
error: Exception,
|
|
) -> None:
|
|
message = f"插件 {plugin_label} 依赖安装失败: {error!s}"
|
|
super().__init__(message)
|
|
self.plugin_label = plugin_label
|
|
self.requirements_path = requirements_path
|
|
self.error = error
|
|
|
|
|
|
@contextlib.asynccontextmanager
|
|
async def _temporary_filtered_requirements_file(
|
|
*,
|
|
install_lines: tuple[str, ...],
|
|
):
|
|
filtered_requirements_path: str | None = None
|
|
temp_dir = get_astrbot_temp_path()
|
|
|
|
# Create temp dir without blocking the event loop
|
|
await to_thread.run_sync(functools.partial(os.makedirs, temp_dir, exist_ok=True))
|
|
|
|
try:
|
|
|
|
def _create_temp():
|
|
with tempfile.NamedTemporaryFile(
|
|
mode="w",
|
|
suffix="_plugin_requirements.txt",
|
|
delete=False,
|
|
dir=temp_dir,
|
|
encoding="utf-8",
|
|
) as filtered_requirements_file:
|
|
filtered_requirements_file.write("\n".join(install_lines) + "\n")
|
|
return filtered_requirements_file.name
|
|
|
|
filtered_requirements_path = await to_thread.run_sync(_create_temp)
|
|
|
|
try:
|
|
yield filtered_requirements_path
|
|
finally:
|
|
if filtered_requirements_path and os.path.exists(
|
|
filtered_requirements_path
|
|
):
|
|
try:
|
|
await to_thread.run_sync(os.remove, filtered_requirements_path)
|
|
except OSError as exc:
|
|
logger.warning(
|
|
"删除临时插件依赖文件失败:%s(路径:%s)",
|
|
exc,
|
|
filtered_requirements_path,
|
|
)
|
|
except Exception:
|
|
# Let exceptions propagate to callers (do not swallow)
|
|
raise
|
|
|
|
|
|
async def _install_requirements_with_precheck(
|
|
*,
|
|
plugin_label: str,
|
|
requirements_path: str,
|
|
) -> None:
|
|
install_plan = plan_missing_requirements_install(requirements_path)
|
|
|
|
if install_plan is None:
|
|
logger.info(
|
|
f"正在安装插件 {plugin_label} 的依赖库(缺失依赖预检查不可裁剪,回退到完整安装): "
|
|
f"{requirements_path}"
|
|
)
|
|
await pip_installer.install(requirements_path=requirements_path)
|
|
return
|
|
|
|
if not install_plan.missing_names:
|
|
logger.info(f"插件 {plugin_label} 的依赖已满足,跳过安装。")
|
|
return
|
|
|
|
if not install_plan.install_lines:
|
|
fallback_reason = install_plan.fallback_reason or "unknown reason"
|
|
logger.info(
|
|
"检测到插件 %s 缺失依赖,但无法安全裁剪 requirements,回退到完整安装: %s (%s)",
|
|
plugin_label,
|
|
requirements_path,
|
|
fallback_reason,
|
|
)
|
|
await pip_installer.install(requirements_path=requirements_path)
|
|
return
|
|
|
|
logger.info(
|
|
f"检测到插件 {plugin_label} 缺失依赖,正在按 requirements.txt 安装: "
|
|
f"{requirements_path} -> {sorted(install_plan.missing_names)}"
|
|
)
|
|
|
|
async with _temporary_filtered_requirements_file(
|
|
install_lines=install_plan.install_lines,
|
|
) as filtered_requirements_path:
|
|
await pip_installer.install(requirements_path=filtered_requirements_path)
|
|
|
|
|
|
class PluginManager:
|
|
def __init__(self, context: Context, config: AstrBotConfig) -> None:
|
|
from .star_tools import StarTools
|
|
|
|
self.tasks = set()
|
|
|
|
self.updator = PluginUpdator()
|
|
|
|
self.context = context
|
|
self.context._star_manager = self
|
|
StarTools.initialize(context)
|
|
|
|
self.config = config
|
|
self.plugin_store_path = get_astrbot_plugin_path()
|
|
"""存储插件的路径。即 data/plugins"""
|
|
self.plugin_config_path = get_astrbot_config_path()
|
|
"""存储插件配置的路径。data/config"""
|
|
self.reserved_plugin_path = os.path.join(
|
|
get_astrbot_path(), "astrbot", "builtin_stars"
|
|
)
|
|
"""保留插件的路径。在 astrbot/builtin_stars 目录下"""
|
|
self.conf_schema_fname = "_conf_schema.json"
|
|
self.logo_fname = "logo.png"
|
|
"""插件配置 Schema 文件名"""
|
|
self._pm_lock = asyncio.Lock()
|
|
"""StarManager操作互斥锁"""
|
|
|
|
self.failed_plugin_dict = {}
|
|
"""加载失败插件的信息,用于后续可能的热重载"""
|
|
|
|
self.failed_plugin_info = ""
|
|
if os.getenv("ASTRBOT_RELOAD", "0") == "1":
|
|
_watch_plugins_changes = asyncio.create_task(self._watch_plugins_changes())
|
|
self.tasks.add(_watch_plugins_changes)
|
|
_watch_plugins_changes.add_done_callback(self.tasks.discard)
|
|
|
|
async def _watch_plugins_changes(self) -> None:
|
|
"""监视插件文件变化"""
|
|
try:
|
|
async for changes in awatch(
|
|
self.plugin_store_path,
|
|
self.reserved_plugin_path,
|
|
watch_filter=PythonFilter(),
|
|
recursive=True,
|
|
):
|
|
# 处理文件变化
|
|
await self._handle_file_changes(changes)
|
|
except asyncio.CancelledError:
|
|
pass
|
|
except Exception as e:
|
|
logger.error(f"插件热重载监视任务异常: {e!s}")
|
|
logger.error(traceback.format_exc())
|
|
|
|
async def _handle_file_changes(self, changes) -> None:
|
|
"""处理文件变化"""
|
|
logger.info(f"检测到文件变化: {changes}")
|
|
plugins_to_check = []
|
|
|
|
for star in star_registry:
|
|
if not star.activated:
|
|
continue
|
|
if star.root_dir_name is None:
|
|
continue
|
|
if star.reserved:
|
|
plugin_dir_path = os.path.join(
|
|
self.reserved_plugin_path,
|
|
star.root_dir_name,
|
|
)
|
|
else:
|
|
plugin_dir_path = os.path.join(
|
|
self.plugin_store_path,
|
|
star.root_dir_name,
|
|
)
|
|
plugins_to_check.append((plugin_dir_path, star.name))
|
|
reloaded_plugins = set()
|
|
for change in changes:
|
|
_, file_path = change
|
|
for plugin_dir_path, plugin_name in plugins_to_check:
|
|
if (
|
|
os.path.commonpath([plugin_dir_path])
|
|
== os.path.commonpath([plugin_dir_path, file_path])
|
|
and plugin_name not in reloaded_plugins
|
|
):
|
|
logger.info(f"检测到插件 {plugin_name} 文件变化,正在重载...")
|
|
await self.reload(plugin_name)
|
|
reloaded_plugins.add(plugin_name)
|
|
break
|
|
|
|
@staticmethod
|
|
def _get_classes(arg: ModuleType):
|
|
"""获取指定模块(可以理解为一个 python 文件)下所有的类"""
|
|
classes = []
|
|
clsmembers = inspect.getmembers(arg, inspect.isclass)
|
|
for name, _ in clsmembers:
|
|
if name.lower().endswith("plugin") or name.lower() == "main":
|
|
classes.append(name)
|
|
break
|
|
return classes
|
|
|
|
@staticmethod
|
|
def _get_modules(path):
|
|
modules = []
|
|
|
|
dirs = os.listdir(path)
|
|
# 遍历文件夹,找到 main.py 或者和文件夹同名的文件
|
|
for d in dirs:
|
|
if os.path.isdir(os.path.join(path, d)):
|
|
if os.path.exists(os.path.join(path, d, "main.py")):
|
|
module_str = "main"
|
|
elif os.path.exists(os.path.join(path, d, d + ".py")):
|
|
module_str = d
|
|
else:
|
|
logger.info(f"插件 {d} 未找到 main.py 或者 {d}.py,跳过。")
|
|
continue
|
|
if os.path.exists(os.path.join(path, d, "main.py")) or os.path.exists(
|
|
os.path.join(path, d, d + ".py"),
|
|
):
|
|
modules.append(
|
|
{
|
|
"pname": d,
|
|
"module": module_str,
|
|
"module_path": os.path.join(path, d, module_str),
|
|
},
|
|
)
|
|
return modules
|
|
|
|
def _get_plugin_modules(self) -> list[dict]:
|
|
plugins = []
|
|
if os.path.exists(self.plugin_store_path):
|
|
plugins.extend(self._get_modules(self.plugin_store_path))
|
|
if os.path.exists(self.reserved_plugin_path):
|
|
_p = self._get_modules(self.reserved_plugin_path)
|
|
for p in _p:
|
|
p["reserved"] = True
|
|
plugins.extend(_p)
|
|
return plugins
|
|
|
|
async def _check_plugin_dept_update(
|
|
self, target_plugin: str | None = None
|
|
) -> bool | None:
|
|
"""检查插件的依赖
|
|
如果 target_plugin 为 None,则检查所有插件的依赖
|
|
"""
|
|
plugin_dir = self.plugin_store_path
|
|
if not await anyio.Path(plugin_dir).exists():
|
|
return False
|
|
to_update = []
|
|
if target_plugin:
|
|
to_update.append(target_plugin)
|
|
else:
|
|
for p in self.context.get_all_stars():
|
|
to_update.append(p.root_dir_name)
|
|
for p in to_update:
|
|
plugin_path = os.path.join(plugin_dir, p)
|
|
await self._ensure_plugin_requirements(plugin_path, p)
|
|
return True
|
|
|
|
async def _ensure_plugin_requirements(
|
|
self,
|
|
plugin_dir_path: str,
|
|
plugin_label: str,
|
|
) -> None:
|
|
requirements_path = os.path.join(plugin_dir_path, "requirements.txt")
|
|
if not await anyio.Path(requirements_path).exists():
|
|
return
|
|
|
|
try:
|
|
await _install_requirements_with_precheck(
|
|
plugin_label=plugin_label,
|
|
requirements_path=requirements_path,
|
|
)
|
|
except asyncio.CancelledError:
|
|
raise
|
|
except DependencyConflictError as e:
|
|
logger.error(f"插件 {plugin_label} 依赖冲突: {e!s}")
|
|
raise
|
|
except Exception as e:
|
|
dependency_error = PluginDependencyInstallError(
|
|
plugin_label=plugin_label,
|
|
requirements_path=requirements_path,
|
|
error=e,
|
|
)
|
|
logger.exception(str(dependency_error))
|
|
raise dependency_error from e
|
|
|
|
async def _import_plugin_with_dependency_recovery(
|
|
self,
|
|
path: str,
|
|
module_str: str,
|
|
root_dir_name: str,
|
|
requirements_path: str,
|
|
) -> ModuleType:
|
|
try:
|
|
return __import__(path, fromlist=[module_str])
|
|
except (ModuleNotFoundError, ImportError) as import_exc:
|
|
if await anyio.Path(requirements_path).exists():
|
|
try:
|
|
logger.info(
|
|
f"插件 {root_dir_name} 导入失败,尝试从已安装依赖恢复: {import_exc!s}"
|
|
)
|
|
pip_installer.prefer_installed_dependencies(
|
|
requirements_path=requirements_path
|
|
)
|
|
module = __import__(path, fromlist=[module_str])
|
|
logger.info(
|
|
f"插件 {root_dir_name} 已从 site-packages 恢复依赖,跳过重新安装。"
|
|
)
|
|
return module
|
|
except Exception as recover_exc:
|
|
logger.info(
|
|
f"插件 {root_dir_name} 已安装依赖恢复失败,将重新安装依赖: {recover_exc!s}"
|
|
)
|
|
|
|
await self._check_plugin_dept_update(target_plugin=root_dir_name)
|
|
return __import__(path, fromlist=[module_str])
|
|
|
|
@staticmethod
|
|
def _load_plugin_metadata(plugin_path: str, plugin_obj=None) -> StarMetadata | None:
|
|
"""先寻找 metadata.yaml 文件,如果不存在,则使用插件对象的 info() 函数获取元数据。
|
|
|
|
Notes: 旧版本 AstrBot 插件可能使用的是 info() 函数来获取元数据。
|
|
"""
|
|
metadata = None
|
|
|
|
if not os.path.exists(plugin_path):
|
|
raise Exception("插件不存在。")
|
|
|
|
if os.path.exists(os.path.join(plugin_path, "metadata.yaml")):
|
|
with open(
|
|
os.path.join(plugin_path, "metadata.yaml"),
|
|
encoding="utf-8",
|
|
) as f:
|
|
metadata = yaml.safe_load(f)
|
|
elif plugin_obj and hasattr(plugin_obj, "info"):
|
|
# 使用 info() 函数
|
|
metadata = plugin_obj.info()
|
|
|
|
if isinstance(metadata, dict):
|
|
if "desc" not in metadata and "description" in metadata:
|
|
metadata["desc"] = metadata["description"]
|
|
|
|
if (
|
|
"name" not in metadata
|
|
or "desc" not in metadata
|
|
or "version" not in metadata
|
|
or "author" not in metadata
|
|
):
|
|
raise Exception(
|
|
"插件元数据信息不完整。name, desc, version, author 是必须的字段。",
|
|
)
|
|
metadata = StarMetadata(
|
|
name=metadata["name"],
|
|
author=metadata["author"],
|
|
desc=metadata["desc"],
|
|
version=metadata["version"],
|
|
repo=metadata["repo"] if "repo" in metadata else None,
|
|
display_name=metadata.get("display_name", None),
|
|
support_platforms=(
|
|
[
|
|
platform_id
|
|
for platform_id in metadata["support_platforms"]
|
|
if isinstance(platform_id, str)
|
|
]
|
|
if isinstance(metadata.get("support_platforms"), list)
|
|
else []
|
|
),
|
|
astrbot_version=(
|
|
metadata["astrbot_version"]
|
|
if isinstance(metadata.get("astrbot_version"), str)
|
|
else None
|
|
),
|
|
)
|
|
|
|
return metadata
|
|
|
|
@staticmethod
|
|
def _normalize_plugin_dir_name(plugin_name: str) -> str:
|
|
return plugin_name.strip()
|
|
|
|
@staticmethod
|
|
def _validate_importable_name(plugin_name: str) -> None:
|
|
if "/" in plugin_name or "\\" in plugin_name:
|
|
raise ValueError(
|
|
"metadata.yaml 中 name 含有路径分隔符,不可用于 importlib 加载。"
|
|
)
|
|
if not plugin_name.isidentifier() or keyword.iskeyword(plugin_name):
|
|
raise Exception(
|
|
"metadata.yaml 中 name 不是合法的模块名称(应为合法 Python 标识符且非关键字)。"
|
|
)
|
|
|
|
@staticmethod
|
|
def _get_plugin_dir_name_from_metadata(plugin_path: str) -> str:
|
|
metadata_path = os.path.join(plugin_path, "metadata.yaml")
|
|
if not os.path.exists(metadata_path):
|
|
raise Exception("未找到 metadata.yaml,无法获取插件目录名。")
|
|
|
|
with open(metadata_path, encoding="utf-8") as f:
|
|
metadata = yaml.safe_load(f)
|
|
|
|
if not isinstance(metadata, dict):
|
|
raise Exception("metadata.yaml 格式错误。")
|
|
|
|
plugin_name = metadata.get("name")
|
|
if not isinstance(plugin_name, str) or not plugin_name.strip():
|
|
raise Exception("metadata.yaml 中缺少 name 字段。")
|
|
|
|
plugin_dir_name = PluginManager._normalize_plugin_dir_name(plugin_name)
|
|
if not plugin_dir_name:
|
|
raise Exception("metadata.yaml 中 name 字段内容非法。")
|
|
PluginManager._validate_importable_name(plugin_dir_name)
|
|
return plugin_dir_name
|
|
|
|
@staticmethod
|
|
def _validate_astrbot_version_specifier(
|
|
version_spec: str | None,
|
|
) -> tuple[bool, str | None]:
|
|
if not version_spec:
|
|
return True, None
|
|
|
|
normalized_spec = version_spec.strip()
|
|
if not normalized_spec:
|
|
return True, None
|
|
|
|
try:
|
|
specifier = SpecifierSet(normalized_spec)
|
|
except InvalidSpecifier:
|
|
return (
|
|
False,
|
|
"astrbot_version 格式无效,请使用 PEP 440 版本范围格式,例如 >=4.16,<5。",
|
|
)
|
|
|
|
try:
|
|
current_version = Version(VERSION)
|
|
except InvalidVersion:
|
|
return (
|
|
False,
|
|
f"AstrBot 当前版本 {VERSION} 无法被解析,无法校验插件版本范围。",
|
|
)
|
|
|
|
if current_version not in specifier:
|
|
return (
|
|
False,
|
|
f"当前 AstrBot 版本为 {VERSION},不满足插件要求的 astrbot_version: {normalized_spec}",
|
|
)
|
|
return True, None
|
|
|
|
@staticmethod
|
|
def _get_plugin_related_modules(
|
|
plugin_root_dir: str,
|
|
is_reserved: bool = False,
|
|
) -> list[str]:
|
|
"""获取与指定插件相关的所有已加载模块名
|
|
|
|
根据插件根目录名和是否为保留插件,从 sys.modules 中筛选出相关的模块名
|
|
|
|
Args:
|
|
plugin_root_dir: 插件根目录名
|
|
is_reserved: 是否是保留插件,影响模块路径前缀
|
|
|
|
Returns:
|
|
list[str]: 与该插件相关的模块名列表
|
|
|
|
"""
|
|
prefix = "astrbot.builtin_stars." if is_reserved else "data.plugins."
|
|
return [
|
|
key
|
|
for key in list(sys.modules.keys())
|
|
if key.startswith(f"{prefix}{plugin_root_dir}")
|
|
]
|
|
|
|
def _purge_modules(
|
|
self,
|
|
module_patterns: list[str] | None = None,
|
|
root_dir_name: str | None = None,
|
|
is_reserved: bool = False,
|
|
) -> None:
|
|
"""从 sys.modules 中移除指定的模块
|
|
|
|
可以基于模块名模式或插件目录名移除模块,用于清理插件相关的模块缓存
|
|
|
|
Args:
|
|
module_patterns: 要移除的模块名模式列表(例如 ["data.plugins", "astrbot.builtin_stars"])
|
|
root_dir_name: 插件根目录名,用于移除与该插件相关的所有模块
|
|
is_reserved: 插件是否为保留插件(影响模块路径前缀)
|
|
|
|
"""
|
|
if module_patterns:
|
|
for pattern in module_patterns:
|
|
for key in list(sys.modules.keys()):
|
|
if key.startswith(pattern):
|
|
del sys.modules[key]
|
|
logger.debug(f"删除模块 {key}")
|
|
|
|
if root_dir_name:
|
|
for module_name in self._get_plugin_related_modules(
|
|
root_dir_name,
|
|
is_reserved,
|
|
):
|
|
try:
|
|
del sys.modules[module_name]
|
|
logger.debug(f"删除模块 {module_name}")
|
|
except KeyError:
|
|
logger.warning(f"模块 {module_name} 未载入")
|
|
|
|
def _cleanup_plugin_state(self, dir_name: str) -> None:
|
|
plugin_root_name = "data.plugins."
|
|
|
|
# 清理 sys.modules
|
|
for key in list(sys.modules.keys()):
|
|
if key.startswith(f"{plugin_root_name}{dir_name}"):
|
|
logger.info(f"清除了插件{dir_name}中的{key}模块")
|
|
del sys.modules[key]
|
|
|
|
possible_paths = [
|
|
f"{plugin_root_name}{dir_name}.main",
|
|
f"{plugin_root_name}{dir_name}.{dir_name}",
|
|
]
|
|
|
|
# 清理 handlers
|
|
for path in possible_paths:
|
|
handlers = star_handlers_registry.get_handlers_by_module_name(path)
|
|
for handler in handlers:
|
|
star_handlers_registry.remove(handler)
|
|
logger.info(f"清理处理器: {handler.handler_name}")
|
|
|
|
# 清理工具
|
|
for tool in list(llm_tools.func_list):
|
|
if tool.handler_module_path in possible_paths:
|
|
llm_tools.func_list.remove(tool)
|
|
logger.info(f"清理工具: {tool.name}")
|
|
|
|
def _build_failed_plugin_record(
|
|
self,
|
|
*,
|
|
root_dir_name: str,
|
|
plugin_dir_path: str,
|
|
reserved: bool,
|
|
error: BaseException | str,
|
|
error_trace: str,
|
|
) -> dict:
|
|
record: dict = {
|
|
"name": root_dir_name,
|
|
"error": str(error),
|
|
"traceback": error_trace,
|
|
"reserved": reserved,
|
|
}
|
|
try:
|
|
metadata = self._load_plugin_metadata(plugin_path=plugin_dir_path)
|
|
if metadata:
|
|
record.update(
|
|
{
|
|
"name": metadata.name,
|
|
"author": metadata.author,
|
|
"desc": metadata.desc,
|
|
"version": metadata.version,
|
|
"repo": metadata.repo,
|
|
"display_name": metadata.display_name,
|
|
"support_platforms": metadata.support_platforms,
|
|
"astrbot_version": metadata.astrbot_version,
|
|
}
|
|
)
|
|
except Exception as metadata_error:
|
|
logger.debug(
|
|
f"读取失败插件 {root_dir_name} 元数据失败: {metadata_error!s}",
|
|
)
|
|
|
|
return record
|
|
|
|
def _rebuild_failed_plugin_info(self) -> None:
|
|
if not self.failed_plugin_dict:
|
|
self.failed_plugin_info = ""
|
|
return
|
|
|
|
lines = []
|
|
for dir_name, info in self.failed_plugin_dict.items():
|
|
if isinstance(info, dict):
|
|
error = info.get("error", "未知错误")
|
|
display_name = info.get("display_name") or info.get("name") or dir_name
|
|
version = info.get("version") or info.get("astrbot_version")
|
|
if version:
|
|
lines.append(
|
|
f"加载插件「{display_name}」(目录: {dir_name}, 版本: {version}) 时出现问题,原因:{error}。",
|
|
)
|
|
else:
|
|
lines.append(
|
|
f"加载插件「{display_name}」(目录: {dir_name}) 时出现问题,原因:{error}。",
|
|
)
|
|
else:
|
|
error = str(info)
|
|
lines.append(f"加载插件目录 {dir_name} 时出现问题,原因:{error}。")
|
|
|
|
self.failed_plugin_info = "\n".join(lines) + "\n"
|
|
|
|
async def reload_failed_plugin(self, dir_name):
|
|
"""
|
|
重新加载未注册(加载失败)的插件
|
|
Args:
|
|
dir_name (str): 要重载的特定插件名称。
|
|
Returns:
|
|
tuple: 返回 load() 方法的结果,包含 (success, error_message)
|
|
- success (bool): 重载是否成功
|
|
- error_message (str|None): 错误信息,成功时为 None
|
|
"""
|
|
|
|
async with self._pm_lock:
|
|
if dir_name not in self.failed_plugin_dict:
|
|
return False, "插件不存在于失败列表中"
|
|
|
|
self._cleanup_plugin_state(dir_name)
|
|
|
|
plugin_path = os.path.join(self.plugin_store_path, dir_name)
|
|
await self._ensure_plugin_requirements(plugin_path, dir_name)
|
|
|
|
success, error = await self.load(specified_dir_name=dir_name)
|
|
if success:
|
|
self.failed_plugin_dict.pop(dir_name, None)
|
|
self._rebuild_failed_plugin_info()
|
|
return success, None
|
|
else:
|
|
return False, error
|
|
|
|
async def reload(self, specified_plugin_name=None):
|
|
"""重新加载插件
|
|
|
|
Args:
|
|
specified_plugin_name (str, optional): 要重载的特定插件名称。
|
|
如果为 None,则重载所有插件。
|
|
|
|
Returns:
|
|
tuple: 返回 load() 方法的结果,包含 (success, error_message)
|
|
- success (bool): 重载是否成功
|
|
- error_message (str|None): 错误信息,成功时为 None
|
|
|
|
"""
|
|
async with self._pm_lock:
|
|
specified_module_path = None
|
|
if specified_plugin_name:
|
|
for smd in star_registry:
|
|
if smd.name == specified_plugin_name:
|
|
specified_module_path = smd.module_path
|
|
break
|
|
|
|
# 终止插件
|
|
if not specified_module_path:
|
|
# 重载所有插件
|
|
for smd in star_registry:
|
|
try:
|
|
await self._terminate_plugin(smd)
|
|
except Exception as e:
|
|
logger.warning(traceback.format_exc())
|
|
logger.warning(
|
|
f"插件 {smd.name} 未被正常终止: {e!s}, 可能会导致该插件运行不正常。",
|
|
)
|
|
if smd.name and smd.module_path:
|
|
await self._unbind_plugin(smd.name, smd.module_path)
|
|
|
|
star_handlers_registry.clear()
|
|
star_map.clear()
|
|
star_registry.clear()
|
|
else:
|
|
# 只重载指定插件
|
|
smd = star_map.get(specified_module_path)
|
|
if smd:
|
|
try:
|
|
await self._terminate_plugin(smd)
|
|
except Exception as e:
|
|
logger.warning(traceback.format_exc())
|
|
logger.warning(
|
|
f"插件 {smd.name} 未被正常终止: {e!s}, 可能会导致该插件运行不正常。",
|
|
)
|
|
if smd.name:
|
|
await self._unbind_plugin(smd.name, specified_module_path)
|
|
|
|
result = await self.load(specified_module_path)
|
|
|
|
return result
|
|
|
|
async def load(
|
|
self,
|
|
specified_module_path=None,
|
|
specified_dir_name=None,
|
|
ignore_version_check: bool = False,
|
|
):
|
|
"""载入插件。
|
|
当 specified_module_path 或者 specified_dir_name 不为 None 时,只载入指定的插件。
|
|
|
|
Args:
|
|
specified_module_path (str, optional): 指定要加载的插件模块路径。例如: "data.plugins.my_plugin.main"
|
|
specified_dir_name (str, optional): 指定要加载的插件目录名。例如: "my_plugin"
|
|
|
|
Returns:
|
|
tuple: (success, error_message)
|
|
- success (bool): 是否全部加载成功
|
|
- error_message (str|None): 错误信息,成功时为 None
|
|
|
|
"""
|
|
inactivated_plugins = await sp.global_get("inactivated_plugins", [])
|
|
inactivated_llm_tools = await sp.global_get("inactivated_llm_tools", [])
|
|
alter_cmd = await sp.global_get("alter_cmd", {})
|
|
|
|
plugin_modules = self._get_plugin_modules()
|
|
if plugin_modules is None:
|
|
return False, "未找到任何插件模块"
|
|
|
|
has_load_error = False
|
|
|
|
# 导入插件模块,并尝试实例化插件类
|
|
for plugin_module in plugin_modules:
|
|
try:
|
|
module_str = plugin_module["module"]
|
|
# module_path = plugin_module['module_path']
|
|
root_dir_name = plugin_module["pname"] # 插件的目录名
|
|
reserved = plugin_module.get(
|
|
"reserved",
|
|
False,
|
|
) # 是否是保留插件。目前在 astrbot/builtin_stars 目录下的都是保留插件。保留插件不可以卸载。
|
|
plugin_dir_path = (
|
|
os.path.join(self.plugin_store_path, root_dir_name)
|
|
if not reserved
|
|
else os.path.join(self.reserved_plugin_path, root_dir_name)
|
|
)
|
|
requirements_path = os.path.join(plugin_dir_path, "requirements.txt")
|
|
|
|
path = "data.plugins." if not reserved else "astrbot.builtin_stars."
|
|
path += root_dir_name + "." + module_str
|
|
|
|
# 检查是否需要载入指定的插件
|
|
if specified_module_path and path != specified_module_path:
|
|
continue
|
|
if specified_dir_name and root_dir_name != specified_dir_name:
|
|
continue
|
|
|
|
logger.info(f"正在载入插件 {root_dir_name} ...")
|
|
|
|
# 尝试导入模块
|
|
try:
|
|
module = await self._import_plugin_with_dependency_recovery(
|
|
path=path,
|
|
module_str=module_str,
|
|
root_dir_name=root_dir_name,
|
|
requirements_path=requirements_path,
|
|
)
|
|
except Exception as e:
|
|
error_trace = traceback.format_exc()
|
|
logger.error(error_trace)
|
|
logger.error(f"插件 {root_dir_name} 导入失败。原因:{e!s}")
|
|
has_load_error = True
|
|
self.failed_plugin_dict[root_dir_name] = (
|
|
self._build_failed_plugin_record(
|
|
root_dir_name=root_dir_name,
|
|
plugin_dir_path=plugin_dir_path,
|
|
reserved=reserved,
|
|
error=e,
|
|
error_trace=error_trace,
|
|
)
|
|
)
|
|
if path in star_map:
|
|
logger.info("失败插件依旧在插件列表中,正在清理...")
|
|
metadata = star_map.pop(path)
|
|
if metadata in star_registry:
|
|
star_registry.remove(metadata)
|
|
continue
|
|
|
|
# 检查 _conf_schema.json
|
|
plugin_config = None
|
|
plugin_schema_path = os.path.join(
|
|
plugin_dir_path,
|
|
self.conf_schema_fname,
|
|
)
|
|
if await anyio.Path(plugin_schema_path).exists():
|
|
# 加载插件配置
|
|
async with await anyio.open_file(
|
|
plugin_schema_path, encoding="utf-8"
|
|
) as f:
|
|
plugin_config = AstrBotConfig(
|
|
config_path=os.path.join(
|
|
self.plugin_config_path,
|
|
f"{root_dir_name}_config.json",
|
|
),
|
|
schema=json.loads(await f.read()),
|
|
)
|
|
logo_path = os.path.join(plugin_dir_path, self.logo_fname)
|
|
|
|
if path in star_map:
|
|
# 通过 __init__subclass__ 注册插件
|
|
metadata = star_map[path]
|
|
|
|
try:
|
|
# yaml 文件的元数据优先
|
|
metadata_yaml = self._load_plugin_metadata(
|
|
plugin_path=plugin_dir_path,
|
|
)
|
|
if metadata_yaml:
|
|
metadata.name = metadata_yaml.name
|
|
metadata.author = metadata_yaml.author
|
|
metadata.desc = metadata_yaml.desc
|
|
metadata.version = metadata_yaml.version
|
|
metadata.repo = metadata_yaml.repo
|
|
metadata.display_name = metadata_yaml.display_name
|
|
metadata.support_platforms = metadata_yaml.support_platforms
|
|
metadata.astrbot_version = metadata_yaml.astrbot_version
|
|
except Exception as e:
|
|
logger.warning(
|
|
f"插件 {root_dir_name} 元数据载入失败: {e!s}。使用默认元数据。",
|
|
)
|
|
|
|
if not ignore_version_check:
|
|
is_valid, error_message = (
|
|
self._validate_astrbot_version_specifier(
|
|
metadata.astrbot_version,
|
|
)
|
|
)
|
|
if not is_valid:
|
|
raise PluginVersionIncompatibleError(
|
|
error_message
|
|
or "The plugin is not compatible with the current AstrBot version."
|
|
)
|
|
|
|
logger.info(metadata)
|
|
metadata.config = plugin_config
|
|
p_name = (metadata.name or "unknown").lower().replace("/", "_")
|
|
p_author = (metadata.author or "unknown").lower().replace("/", "_")
|
|
plugin_id = f"{p_author}/{p_name}"
|
|
|
|
# 在实例化前注入类属性,保证插件 __init__ 可读取这些值
|
|
if metadata.star_cls_type:
|
|
setattr(metadata.star_cls_type, "name", p_name)
|
|
setattr(metadata.star_cls_type, "author", p_author)
|
|
setattr(metadata.star_cls_type, "plugin_id", plugin_id)
|
|
|
|
if path not in inactivated_plugins:
|
|
# 只有没有禁用插件时才实例化插件类
|
|
if plugin_config and metadata.star_cls_type:
|
|
try:
|
|
metadata.star_cls = metadata.star_cls_type(
|
|
context=self.context,
|
|
config=plugin_config,
|
|
)
|
|
except TypeError as _:
|
|
metadata.star_cls = metadata.star_cls_type(
|
|
context=self.context,
|
|
)
|
|
elif metadata.star_cls_type:
|
|
metadata.star_cls = metadata.star_cls_type(
|
|
context=self.context,
|
|
)
|
|
|
|
if metadata.star_cls:
|
|
setattr(metadata.star_cls, "name", p_name)
|
|
setattr(metadata.star_cls, "author", p_author)
|
|
setattr(metadata.star_cls, "plugin_id", plugin_id)
|
|
else:
|
|
logger.info(f"插件 {metadata.name} 已被禁用。")
|
|
|
|
metadata.module = module
|
|
metadata.root_dir_name = root_dir_name
|
|
metadata.reserved = reserved
|
|
|
|
assert metadata.module_path is not None, (
|
|
f"插件 {metadata.name} 的模块路径为空。"
|
|
)
|
|
assert metadata.star_cls is not None, (
|
|
f"插件 {metadata.name} 的实例为空。"
|
|
)
|
|
|
|
# 绑定 handler
|
|
related_handlers = (
|
|
star_handlers_registry.get_handlers_by_module_name(
|
|
metadata.module_path,
|
|
)
|
|
)
|
|
for handler in related_handlers:
|
|
handler.handler = functools.partial(
|
|
handler.handler,
|
|
metadata.star_cls,
|
|
)
|
|
# 绑定 llm_tool handler
|
|
for func_tool in llm_tools.func_list:
|
|
if isinstance(func_tool, HandoffTool):
|
|
need_apply = []
|
|
sub_tools = func_tool.agent.tools
|
|
if sub_tools:
|
|
for sub_tool in sub_tools:
|
|
if isinstance(sub_tool, FunctionTool):
|
|
need_apply.append(sub_tool)
|
|
else:
|
|
need_apply = [func_tool]
|
|
|
|
for ft in need_apply:
|
|
if (
|
|
ft.handler
|
|
and ft.handler.__module__ == metadata.module_path
|
|
):
|
|
ft.handler_module_path = metadata.module_path
|
|
ft.handler = functools.partial(
|
|
ft.handler,
|
|
metadata.star_cls,
|
|
)
|
|
if ft.name in inactivated_llm_tools:
|
|
ft.active = False
|
|
|
|
else:
|
|
# v3.4.0 以前的方式注册插件
|
|
logger.debug(
|
|
f"插件 {path} 未通过装饰器注册。尝试通过旧版本方式载入。",
|
|
)
|
|
classes = self._get_classes(module)
|
|
|
|
if path not in inactivated_plugins:
|
|
# 只有没有禁用插件时才实例化插件类
|
|
if plugin_config:
|
|
try:
|
|
obj = getattr(module, classes[0])(
|
|
context=self.context,
|
|
config=plugin_config,
|
|
) # 实例化插件类
|
|
except TypeError as _:
|
|
obj = getattr(module, classes[0])(
|
|
context=self.context,
|
|
) # 实例化插件类
|
|
else:
|
|
obj = getattr(module, classes[0])(
|
|
context=self.context,
|
|
) # 实例化插件类
|
|
|
|
metadata = self._load_plugin_metadata(
|
|
plugin_path=plugin_dir_path,
|
|
plugin_obj=obj,
|
|
)
|
|
if not metadata:
|
|
raise Exception(f"无法找到插件 {plugin_dir_path} 的元数据。")
|
|
|
|
if not ignore_version_check:
|
|
is_valid, error_message = (
|
|
self._validate_astrbot_version_specifier(
|
|
metadata.astrbot_version,
|
|
)
|
|
)
|
|
if not is_valid:
|
|
raise PluginVersionIncompatibleError(
|
|
error_message
|
|
or "The plugin is not compatible with the current AstrBot version."
|
|
)
|
|
|
|
metadata.star_cls = obj
|
|
metadata.config = plugin_config
|
|
metadata.module = module
|
|
metadata.root_dir_name = root_dir_name
|
|
metadata.reserved = reserved
|
|
metadata.star_cls_type = obj.__class__
|
|
metadata.module_path = path
|
|
star_map[path] = metadata
|
|
star_registry.append(metadata)
|
|
|
|
# 禁用/启用插件
|
|
if metadata.module_path in inactivated_plugins:
|
|
metadata.activated = False
|
|
|
|
# Plugin logo path
|
|
if await anyio.Path(logo_path).exists():
|
|
metadata.logo_path = logo_path
|
|
|
|
assert metadata.module_path, f"插件 {metadata.name} 模块路径为空"
|
|
|
|
full_names = []
|
|
for handler in star_handlers_registry.get_handlers_by_module_name(
|
|
metadata.module_path,
|
|
):
|
|
full_names.append(handler.handler_full_name)
|
|
|
|
# 检查并且植入自定义的权限过滤器(alter_cmd)
|
|
if (
|
|
metadata.name in alter_cmd
|
|
and handler.handler_name in alter_cmd[metadata.name]
|
|
):
|
|
cmd_type = alter_cmd[metadata.name][handler.handler_name].get(
|
|
"permission",
|
|
"member",
|
|
)
|
|
found_permission_filter = False
|
|
for filter_ in handler.event_filters:
|
|
if isinstance(filter_, PermissionTypeFilter):
|
|
if cmd_type == "admin":
|
|
filter_.permission_type = PermissionType.ADMIN
|
|
else:
|
|
filter_.permission_type = PermissionType.MEMBER
|
|
found_permission_filter = True
|
|
break
|
|
if not found_permission_filter:
|
|
handler.event_filters.append(
|
|
PermissionTypeFilter(
|
|
PermissionType.ADMIN
|
|
if cmd_type == "admin"
|
|
else PermissionType.MEMBER,
|
|
),
|
|
)
|
|
|
|
logger.debug(
|
|
f"插入权限过滤器 {cmd_type} 到 {metadata.name} 的 {handler.handler_name} 方法。",
|
|
)
|
|
|
|
metadata.star_handler_full_names = full_names
|
|
|
|
# 执行 initialize() 方法
|
|
if hasattr(metadata.star_cls, "initialize") and metadata.star_cls:
|
|
await metadata.star_cls.initialize()
|
|
|
|
# 触发插件加载事件
|
|
handlers = star_handlers_registry.get_handlers_by_event_type(
|
|
EventType.OnPluginLoadedEvent,
|
|
)
|
|
for handler in handlers:
|
|
try:
|
|
logger.info(
|
|
f"hook(on_plugin_loaded) -> {star_map[handler.handler_module_path].name} - {handler.handler_name}",
|
|
)
|
|
await handler.handler(metadata)
|
|
except Exception:
|
|
logger.error(traceback.format_exc())
|
|
sdk_plugin_bridge = getattr(self.context, "sdk_plugin_bridge", None)
|
|
if sdk_plugin_bridge is not None:
|
|
try:
|
|
await sdk_plugin_bridge.dispatch_system_event(
|
|
"plugin_loaded",
|
|
{
|
|
"plugin_name": metadata.name,
|
|
"display_name": metadata.display_name or metadata.name,
|
|
"version": metadata.version,
|
|
},
|
|
)
|
|
except Exception as exc:
|
|
logger.warning("SDK plugin_loaded dispatch failed: %s", exc)
|
|
|
|
except BaseException as e:
|
|
logger.error(f"----- 插件 {root_dir_name} 载入失败 -----")
|
|
errors = traceback.format_exc()
|
|
for line in errors.split("\n"):
|
|
logger.error(f"| {line}")
|
|
logger.error("----------------------------------")
|
|
has_load_error = True
|
|
self.failed_plugin_dict[root_dir_name] = (
|
|
self._build_failed_plugin_record(
|
|
root_dir_name=root_dir_name,
|
|
plugin_dir_path=plugin_dir_path,
|
|
reserved=reserved,
|
|
error=e,
|
|
error_trace=errors,
|
|
)
|
|
)
|
|
# 记录注册失败的插件名称,以便后续重载插件
|
|
if path in star_map:
|
|
logger.info("失败插件依旧在插件列表中,正在清理...")
|
|
metadata = star_map.pop(path)
|
|
if metadata in star_registry:
|
|
star_registry.remove(metadata)
|
|
|
|
# 清除 pip.main 导致的多余的 logging handlers
|
|
for handler in logging.root.handlers[:]:
|
|
logging.root.removeHandler(handler)
|
|
try:
|
|
await sync_command_configs()
|
|
except Exception as e:
|
|
logger.error(f"同步指令配置失败: {e!s}")
|
|
logger.error(traceback.format_exc())
|
|
|
|
self._rebuild_failed_plugin_info()
|
|
if has_load_error:
|
|
return False, self.failed_plugin_info
|
|
return True, None
|
|
|
|
async def _cleanup_failed_plugin_install(
|
|
self,
|
|
dir_name: str,
|
|
plugin_path: str,
|
|
) -> None:
|
|
plugin = None
|
|
for star in self.context.get_all_stars():
|
|
if star.root_dir_name == dir_name:
|
|
plugin = star
|
|
break
|
|
|
|
if plugin and plugin.name and plugin.module_path:
|
|
try:
|
|
await self._terminate_plugin(plugin)
|
|
except Exception:
|
|
logger.warning(traceback.format_exc())
|
|
try:
|
|
await self._unbind_plugin(plugin.name, plugin.module_path)
|
|
except Exception:
|
|
logger.warning(traceback.format_exc())
|
|
|
|
if await anyio.Path(plugin_path).exists():
|
|
try:
|
|
await to_thread.run_sync(remove_dir, plugin_path)
|
|
logger.warning(f"已清理安装失败的插件目录: {plugin_path}")
|
|
except Exception as e:
|
|
logger.warning(
|
|
f"清理安装失败插件目录失败: {plugin_path},原因: {e!s}",
|
|
)
|
|
|
|
plugin_config_path = os.path.join(
|
|
self.plugin_config_path,
|
|
f"{dir_name}_config.json",
|
|
)
|
|
if await anyio.Path(plugin_config_path).exists():
|
|
try:
|
|
await to_thread.run_sync(os.remove, plugin_config_path)
|
|
logger.warning(f"已清理安装失败插件配置: {plugin_config_path}")
|
|
except Exception as e:
|
|
logger.warning(
|
|
f"清理安装失败插件配置失败: {plugin_config_path},原因: {e!s}",
|
|
)
|
|
|
|
def _cleanup_plugin_optional_artifacts(
|
|
self,
|
|
*,
|
|
root_dir_name: str,
|
|
plugin_label: str,
|
|
delete_config: bool,
|
|
delete_data: bool,
|
|
) -> None:
|
|
if delete_config:
|
|
config_file = os.path.join(
|
|
self.plugin_config_path,
|
|
f"{root_dir_name}_config.json",
|
|
)
|
|
if os.path.exists(config_file):
|
|
try:
|
|
os.remove(config_file)
|
|
logger.info(f"已删除插件 {plugin_label} 的配置文件")
|
|
except Exception as e:
|
|
logger.warning(f"删除插件配置文件失败 ({plugin_label}): {e!s}")
|
|
|
|
if delete_data:
|
|
data_base_dir = os.path.dirname(self.plugin_store_path)
|
|
for data_dir_name in ("plugin_data", "plugins_data"):
|
|
plugin_data_dir = os.path.join(
|
|
data_base_dir,
|
|
data_dir_name,
|
|
root_dir_name,
|
|
)
|
|
if os.path.exists(plugin_data_dir):
|
|
try:
|
|
remove_dir(plugin_data_dir)
|
|
logger.info(
|
|
f"已删除插件 {plugin_label} 的持久化数据 ({data_dir_name})",
|
|
)
|
|
except Exception as e:
|
|
logger.warning(
|
|
f"删除插件持久化数据失败 ({data_dir_name}, {plugin_label}): {e!s}",
|
|
)
|
|
|
|
def _track_failed_install_dir(
|
|
self,
|
|
*,
|
|
dir_name: str,
|
|
plugin_path: str,
|
|
error: Exception,
|
|
) -> None:
|
|
if (
|
|
not dir_name
|
|
or not plugin_path
|
|
or not os.path.isdir(plugin_path)
|
|
or dir_name in self.failed_plugin_dict
|
|
):
|
|
return
|
|
|
|
for star in self.context.get_all_stars():
|
|
if star.root_dir_name == dir_name:
|
|
return
|
|
|
|
self.failed_plugin_dict[dir_name] = self._build_failed_plugin_record(
|
|
root_dir_name=dir_name,
|
|
plugin_dir_path=plugin_path,
|
|
reserved=False,
|
|
error=error,
|
|
error_trace=traceback.format_exc(),
|
|
)
|
|
self._rebuild_failed_plugin_info()
|
|
|
|
async def install_plugin(
|
|
self, repo_url: str, proxy: str = "", ignore_version_check: bool = False
|
|
):
|
|
"""从仓库 URL 安装插件
|
|
|
|
从指定的仓库 URL 下载并安装插件,然后加载该插件到系统中
|
|
|
|
Args:
|
|
repo_url (str): 要安装的插件仓库 URL
|
|
proxy (str, optional): 用于下载的代理服务器。默认为空字符串。
|
|
|
|
Returns:
|
|
dict | None: 安装成功时返回包含插件信息的字典:
|
|
- repo: 插件的仓库 URL
|
|
- readme: README.md 文件的内容(如果存在)
|
|
如果找不到插件元数据则返回 None。
|
|
|
|
"""
|
|
# this metric is for displaying plugins installation count in webui
|
|
_task_install_star = asyncio.create_task(
|
|
Metric.upload(
|
|
et="install_star",
|
|
repo=repo_url,
|
|
),
|
|
)
|
|
self.tasks.add(_task_install_star)
|
|
|
|
async with self._pm_lock:
|
|
plugin_path = ""
|
|
dir_name = ""
|
|
try:
|
|
_, repo_name, _ = self.updator.parse_github_url(repo_url)
|
|
repo_name = self.updator.format_name(repo_name)
|
|
plugin_path = os.path.join(self.plugin_store_path, repo_name)
|
|
if await anyio.Path(plugin_path).exists():
|
|
raise Exception(
|
|
f"安装失败:目录 {os.path.basename(plugin_path)} 已存在。"
|
|
)
|
|
plugin_path = await self.updator.install(repo_url, proxy)
|
|
|
|
# reload the plugin
|
|
dir_name = os.path.basename(plugin_path)
|
|
metadata_dir_name = self._get_plugin_dir_name_from_metadata(plugin_path)
|
|
target_plugin_path = os.path.join(
|
|
self.plugin_store_path,
|
|
metadata_dir_name,
|
|
)
|
|
if (
|
|
target_plugin_path != plugin_path
|
|
and await anyio.Path(target_plugin_path).exists()
|
|
):
|
|
raise Exception(f"安装失败:目录 {metadata_dir_name} 已存在。")
|
|
if target_plugin_path != plugin_path:
|
|
os.rename(plugin_path, target_plugin_path)
|
|
plugin_path = target_plugin_path
|
|
dir_name = metadata_dir_name
|
|
await self._ensure_plugin_requirements(
|
|
plugin_path,
|
|
dir_name,
|
|
)
|
|
success, error_message = await self.load(
|
|
specified_dir_name=dir_name,
|
|
ignore_version_check=ignore_version_check,
|
|
)
|
|
if not success:
|
|
raise Exception(
|
|
error_message
|
|
or f"安装插件 {dir_name} 失败,请检查插件依赖或兼容性。"
|
|
)
|
|
|
|
# Get the plugin metadata to return repo info
|
|
plugin = self.context.get_registered_star(dir_name)
|
|
if not plugin:
|
|
# Try to find by other name if directory name doesn't match plugin name
|
|
for star in self.context.get_all_stars():
|
|
if star.root_dir_name == dir_name:
|
|
plugin = star
|
|
break
|
|
|
|
# Extract README.md content if exists
|
|
readme_content = None
|
|
readme_path = os.path.join(plugin_path, "README.md")
|
|
if not await anyio.Path(readme_path).exists():
|
|
readme_path = os.path.join(plugin_path, "readme.md")
|
|
|
|
if await anyio.Path(readme_path).exists():
|
|
try:
|
|
readme_content = await anyio.Path(readme_path).read_text(
|
|
encoding="utf-8"
|
|
)
|
|
except Exception as e:
|
|
logger.warning(
|
|
f"读取插件 {dir_name} 的 README.md 文件失败: {e!s}",
|
|
)
|
|
|
|
plugin_info = None
|
|
if plugin:
|
|
plugin_info = {
|
|
"repo": plugin.repo,
|
|
"readme": readme_content,
|
|
"name": plugin.name,
|
|
}
|
|
|
|
return plugin_info
|
|
except Exception as e:
|
|
self._track_failed_install_dir(
|
|
dir_name=dir_name,
|
|
plugin_path=plugin_path,
|
|
error=e,
|
|
)
|
|
if dir_name and plugin_path:
|
|
logger.warning(
|
|
f"安装插件 {dir_name} 失败,插件安装目录:{plugin_path}",
|
|
)
|
|
raise
|
|
|
|
async def uninstall_plugin(
|
|
self,
|
|
plugin_name: str,
|
|
delete_config: bool = False,
|
|
delete_data: bool = False,
|
|
) -> None:
|
|
"""卸载指定的插件。
|
|
|
|
Args:
|
|
plugin_name (str): 要卸载的插件名称
|
|
delete_config (bool): 是否删除插件配置文件,默认为 False
|
|
delete_data (bool): 是否删除插件数据,默认为 False
|
|
|
|
Raises:
|
|
Exception: 当插件不存在、是保留插件时,或删除插件文件夹失败时抛出异常
|
|
|
|
"""
|
|
async with self._pm_lock:
|
|
plugin = self.context.get_registered_star(plugin_name)
|
|
if not plugin:
|
|
raise Exception("插件不存在。")
|
|
if plugin.reserved:
|
|
raise Exception("该插件是 AstrBot 保留插件,无法卸载。")
|
|
root_dir_name = plugin.root_dir_name
|
|
ppath = self.plugin_store_path
|
|
|
|
# 终止插件
|
|
try:
|
|
await self._terminate_plugin(plugin)
|
|
except Exception as e:
|
|
logger.warning(traceback.format_exc())
|
|
logger.warning(
|
|
f"插件 {plugin_name} 未被正常终止 {e!s}, 可能会导致资源泄露等问题。",
|
|
)
|
|
|
|
# 从 star_registry 和 star_map 中删除
|
|
if plugin.module_path is None or root_dir_name is None:
|
|
raise Exception(f"插件 {plugin_name} 数据不完整,无法卸载。")
|
|
|
|
await self._unbind_plugin(plugin_name, plugin.module_path)
|
|
|
|
# 删除插件文件夹
|
|
try:
|
|
remove_dir(os.path.join(ppath, root_dir_name))
|
|
except Exception as e:
|
|
raise Exception(
|
|
f"移除插件成功,但是删除插件文件夹失败: {e!s}。您可以手动删除该文件夹,位于 addons/plugins/ 下。",
|
|
)
|
|
|
|
self._cleanup_plugin_optional_artifacts(
|
|
root_dir_name=root_dir_name,
|
|
plugin_label=plugin_name,
|
|
delete_config=delete_config,
|
|
delete_data=delete_data,
|
|
)
|
|
|
|
async def uninstall_failed_plugin(
|
|
self,
|
|
dir_name: str,
|
|
delete_config: bool = False,
|
|
delete_data: bool = False,
|
|
) -> None:
|
|
"""卸载加载失败的插件(按目录名)。"""
|
|
async with self._pm_lock:
|
|
failed_info = self.failed_plugin_dict.get(dir_name)
|
|
if not failed_info:
|
|
raise Exception(
|
|
format_plugin_error("not_found_in_failed_list"),
|
|
)
|
|
|
|
if isinstance(failed_info, dict) and failed_info.get("reserved"):
|
|
raise Exception(
|
|
format_plugin_error("reserved_plugin_cannot_uninstall"),
|
|
)
|
|
|
|
self._cleanup_plugin_state(dir_name)
|
|
|
|
plugin_path = os.path.join(self.plugin_store_path, dir_name)
|
|
if await anyio.Path(plugin_path).exists():
|
|
try:
|
|
remove_dir(plugin_path)
|
|
except Exception as e:
|
|
raise Exception(
|
|
format_plugin_error(
|
|
"failed_plugin_dir_remove_error",
|
|
error=f"{e!s}",
|
|
),
|
|
)
|
|
else:
|
|
logger.debug(
|
|
"插件目录不存在,视为已部分卸载状态,继续清理失败插件记录和可选产物: %s",
|
|
plugin_path,
|
|
)
|
|
|
|
plugin_label = dir_name
|
|
if isinstance(failed_info, dict):
|
|
plugin_label = (
|
|
failed_info.get("display_name")
|
|
or failed_info.get("name")
|
|
or dir_name
|
|
)
|
|
|
|
self._cleanup_plugin_optional_artifacts(
|
|
root_dir_name=dir_name,
|
|
plugin_label=plugin_label,
|
|
delete_config=delete_config,
|
|
delete_data=delete_data,
|
|
)
|
|
|
|
self.failed_plugin_dict.pop(dir_name, None)
|
|
self._rebuild_failed_plugin_info()
|
|
|
|
async def _unbind_plugin(self, plugin_name: str, plugin_module_path: str) -> None:
|
|
"""解绑并移除一个插件。
|
|
|
|
Args:
|
|
plugin_name: 要解绑的插件名称
|
|
plugin_module_path: 插件的完整模块路径
|
|
|
|
"""
|
|
plugin = None
|
|
del star_map[plugin_module_path]
|
|
for i, p in enumerate(star_registry):
|
|
if p.name == plugin_name:
|
|
plugin = p
|
|
del star_registry[i]
|
|
break
|
|
for handler in star_handlers_registry.get_handlers_by_module_name(
|
|
plugin_module_path,
|
|
):
|
|
logger.info(
|
|
f"移除了插件 {plugin_name} 的处理函数 {handler.handler_name} ({len(star_handlers_registry)})",
|
|
)
|
|
star_handlers_registry.remove(handler)
|
|
|
|
for k in [
|
|
k
|
|
for k in star_handlers_registry.star_handlers_map
|
|
if k.startswith(plugin_module_path)
|
|
]:
|
|
del star_handlers_registry.star_handlers_map[k]
|
|
|
|
# llm_tools 中移除该插件的工具函数绑定
|
|
to_remove = []
|
|
for func_tool in llm_tools.func_list:
|
|
mp = func_tool.handler_module_path
|
|
if (
|
|
mp
|
|
and mp.startswith(plugin_module_path)
|
|
and not mp.endswith(("astrbot.builtin_stars", "data.plugins"))
|
|
):
|
|
to_remove.append(func_tool)
|
|
for func_tool in to_remove:
|
|
llm_tools.func_list.remove(func_tool)
|
|
|
|
# Unregister platform adapters registered by this plugin
|
|
# module_path is like "data.plugins.my_plugin.main", extract prefix like "data.plugins.my_plugin"
|
|
module_prefix = ".".join(plugin_module_path.split(".")[:-1])
|
|
if module_prefix:
|
|
unregistered_adapters = unregister_platform_adapters_by_module(
|
|
module_prefix
|
|
)
|
|
for adapter_name in unregistered_adapters:
|
|
logger.info(
|
|
f"移除了插件 {plugin_name} 的平台适配器 {adapter_name}",
|
|
)
|
|
|
|
if plugin is None:
|
|
return
|
|
|
|
self._purge_modules(
|
|
root_dir_name=plugin.root_dir_name,
|
|
is_reserved=plugin.reserved,
|
|
)
|
|
|
|
async def update_plugin(self, plugin_name: str, proxy="") -> None:
|
|
"""升级一个插件"""
|
|
plugin = self.context.get_registered_star(plugin_name)
|
|
if not plugin:
|
|
raise Exception("插件不存在。")
|
|
if plugin.reserved:
|
|
raise Exception("该插件是 AstrBot 保留插件,无法更新。")
|
|
|
|
await self.updator.update(plugin, proxy=proxy)
|
|
if plugin.root_dir_name:
|
|
plugin_dir_path = os.path.join(self.plugin_store_path, plugin.root_dir_name)
|
|
await self._ensure_plugin_requirements(
|
|
plugin_dir_path,
|
|
plugin_name,
|
|
)
|
|
await self.reload(plugin_name)
|
|
|
|
async def turn_off_plugin(self, plugin_name: str) -> None:
|
|
"""禁用一个插件。
|
|
调用插件的 terminate() 方法,
|
|
将插件的 module_path 加入到 data/shared_preferences.json 的 inactivated_plugins 列表中。
|
|
并且同时将插件启用的 llm_tool 禁用。
|
|
"""
|
|
async with self._pm_lock:
|
|
plugin = self.context.get_registered_star(plugin_name)
|
|
if not plugin:
|
|
raise Exception("插件不存在。")
|
|
|
|
# 调用插件的终止方法
|
|
await self._terminate_plugin(plugin)
|
|
|
|
# 加入到 shared_preferences 中
|
|
inactivated_plugins: list = await sp.global_get("inactivated_plugins", [])
|
|
if plugin.module_path not in inactivated_plugins:
|
|
inactivated_plugins.append(plugin.module_path)
|
|
|
|
inactivated_llm_tools: list = list(
|
|
set(await sp.global_get("inactivated_llm_tools", [])),
|
|
) # 后向兼容
|
|
|
|
# 禁用插件启用的 llm_tool
|
|
for func_tool in llm_tools.func_list:
|
|
mp = func_tool.handler_module_path
|
|
if (
|
|
plugin.module_path
|
|
and mp
|
|
and plugin.module_path.startswith(mp)
|
|
and not mp.endswith(("astrbot.builtin_stars", "data.plugins"))
|
|
):
|
|
func_tool.active = False
|
|
if func_tool.name not in inactivated_llm_tools:
|
|
inactivated_llm_tools.append(func_tool.name)
|
|
|
|
await sp.global_put("inactivated_plugins", inactivated_plugins)
|
|
await sp.global_put("inactivated_llm_tools", inactivated_llm_tools)
|
|
|
|
plugin.activated = False
|
|
|
|
@staticmethod
|
|
async def _terminate_plugin(star_metadata: StarMetadata) -> None:
|
|
"""终止插件,调用插件的 terminate() 和 __del__() 方法"""
|
|
logger.info(f"正在终止插件 {star_metadata.name} ...")
|
|
|
|
if not star_metadata.activated:
|
|
# 说明之前已经被禁用了
|
|
logger.debug(f"插件 {star_metadata.name} 未被激活,不需要终止,跳过。")
|
|
return
|
|
|
|
if star_metadata.star_cls is None:
|
|
return
|
|
|
|
if "__del__" in star_metadata.star_cls_type.__dict__:
|
|
loop = asyncio.get_running_loop()
|
|
future = loop.run_in_executor(
|
|
None,
|
|
star_metadata.star_cls.__del__,
|
|
)
|
|
|
|
def _log_del_exception(fut: asyncio.Future) -> None:
|
|
if fut.cancelled():
|
|
return
|
|
if (exc := fut.exception()) is not None:
|
|
logger.error(
|
|
"插件 %s 在 __del__ 中抛出了异常:%r",
|
|
star_metadata.name,
|
|
exc,
|
|
)
|
|
|
|
future.add_done_callback(_log_del_exception)
|
|
elif "terminate" in star_metadata.star_cls_type.__dict__:
|
|
await star_metadata.star_cls.terminate()
|
|
|
|
# 触发插件卸载事件
|
|
handlers = star_handlers_registry.get_handlers_by_event_type(
|
|
EventType.OnPluginUnloadedEvent,
|
|
)
|
|
for handler in handlers:
|
|
try:
|
|
logger.info(
|
|
f"hook(on_plugin_unloaded) -> {star_map[handler.handler_module_path].name} - {handler.handler_name}",
|
|
)
|
|
await handler.handler(star_metadata)
|
|
except Exception:
|
|
logger.error(traceback.format_exc())
|
|
sdk_plugin_bridge = (
|
|
getattr(star_metadata.star_cls.context, "sdk_plugin_bridge", None)
|
|
if getattr(star_metadata, "star_cls", None)
|
|
else None
|
|
)
|
|
if sdk_plugin_bridge is not None:
|
|
try:
|
|
await sdk_plugin_bridge.dispatch_system_event(
|
|
"plugin_unloaded",
|
|
{
|
|
"plugin_name": star_metadata.name,
|
|
"display_name": star_metadata.display_name
|
|
or star_metadata.name,
|
|
"version": star_metadata.version,
|
|
},
|
|
)
|
|
except Exception as exc:
|
|
logger.warning("SDK plugin_unloaded dispatch failed: %s", exc)
|
|
|
|
async def turn_on_plugin(self, plugin_name: str) -> None:
|
|
plugin = self.context.get_registered_star(plugin_name)
|
|
if plugin is None:
|
|
raise Exception(f"插件 {plugin_name} 不存在。")
|
|
inactivated_plugins: list = await sp.global_get("inactivated_plugins", [])
|
|
inactivated_llm_tools: list = await sp.global_get("inactivated_llm_tools", [])
|
|
if plugin.module_path in inactivated_plugins:
|
|
inactivated_plugins.remove(plugin.module_path)
|
|
await sp.global_put("inactivated_plugins", inactivated_plugins)
|
|
|
|
# 启用插件启用的 llm_tool
|
|
for func_tool in llm_tools.func_list:
|
|
mp = func_tool.handler_module_path
|
|
if (
|
|
plugin.module_path
|
|
and mp
|
|
and plugin.module_path.startswith(mp)
|
|
and not mp.endswith(("astrbot.builtin_stars", "data.plugins"))
|
|
and func_tool.name in inactivated_llm_tools
|
|
):
|
|
inactivated_llm_tools.remove(func_tool.name)
|
|
func_tool.active = True
|
|
await sp.global_put("inactivated_llm_tools", inactivated_llm_tools)
|
|
|
|
await self.reload(plugin_name)
|
|
|
|
async def install_plugin_from_file(
|
|
self, zip_file_path: str, ignore_version_check: bool = False
|
|
):
|
|
dir_name = os.path.splitext(os.path.basename(zip_file_path))[0]
|
|
desti_dir = tempfile.mkdtemp(
|
|
dir=self.plugin_store_path, prefix="plugin_upload_"
|
|
)
|
|
temp_desti_dir = desti_dir
|
|
|
|
try:
|
|
self.updator.unzip_file(zip_file_path, desti_dir)
|
|
metadata_dir_name = self._get_plugin_dir_name_from_metadata(desti_dir)
|
|
target_plugin_path = os.path.join(
|
|
self.plugin_store_path,
|
|
metadata_dir_name,
|
|
)
|
|
if (
|
|
target_plugin_path != desti_dir
|
|
and await anyio.Path(target_plugin_path).exists()
|
|
):
|
|
raise Exception(f"安装失败:目录 {metadata_dir_name} 已存在。")
|
|
if target_plugin_path != desti_dir:
|
|
os.rename(desti_dir, target_plugin_path)
|
|
dir_name = metadata_dir_name
|
|
desti_dir = target_plugin_path
|
|
|
|
# remove the zip
|
|
try:
|
|
os.remove(zip_file_path)
|
|
except BaseException as e:
|
|
logger.warning(f"删除插件压缩包失败: {e!s}")
|
|
await self._ensure_plugin_requirements(desti_dir, dir_name)
|
|
# await self.reload()
|
|
success, error_message = await self.load(
|
|
specified_dir_name=dir_name,
|
|
ignore_version_check=ignore_version_check,
|
|
)
|
|
if not success:
|
|
raise Exception(
|
|
error_message or f"安装插件 {dir_name} 失败,请检查插件依赖或兼容性。"
|
|
)
|
|
|
|
# Get the plugin metadata to return repo info
|
|
plugin = self.context.get_registered_star(dir_name)
|
|
if not plugin:
|
|
# Try to find by other name if directory name doesn't match plugin name
|
|
for star in self.context.get_all_stars():
|
|
if star.root_dir_name == dir_name:
|
|
plugin = star
|
|
break
|
|
|
|
# Extract README.md content if exists
|
|
readme_content = None
|
|
readme_path = os.path.join(desti_dir, "README.md")
|
|
if not await anyio.Path(readme_path).exists():
|
|
readme_path = os.path.join(desti_dir, "readme.md")
|
|
|
|
if await anyio.Path(readme_path).exists():
|
|
try:
|
|
readme_content = await anyio.Path(readme_path).read_text(
|
|
encoding="utf-8"
|
|
)
|
|
except Exception as e:
|
|
logger.warning(f"读取插件 {dir_name} 的 README.md 文件失败: {e!s}")
|
|
|
|
plugin_info = None
|
|
if plugin:
|
|
plugin_info = {
|
|
"repo": plugin.repo,
|
|
"readme": readme_content,
|
|
"name": plugin.name,
|
|
}
|
|
|
|
if plugin.repo:
|
|
_task_install_star_f = asyncio.create_task(
|
|
Metric.upload(
|
|
et="install_star_f", # install star
|
|
repo=plugin.repo,
|
|
),
|
|
)
|
|
self.tasks.add(_task_install_star_f)
|
|
_task_install_star_f.add_done_callback(self.tasks.discard)
|
|
|
|
return plugin_info
|
|
except Exception as e:
|
|
self._track_failed_install_dir(
|
|
dir_name=dir_name,
|
|
plugin_path=desti_dir,
|
|
error=e,
|
|
)
|
|
logger.warning(
|
|
f"安装插件 {dir_name} 失败,插件安装目录:{desti_dir}",
|
|
)
|
|
raise
|
|
finally:
|
|
if (
|
|
temp_desti_dir != desti_dir
|
|
and await anyio.Path(temp_desti_dir).is_dir()
|
|
):
|
|
try:
|
|
remove_dir(temp_desti_dir)
|
|
except Exception as e:
|
|
logger.warning(
|
|
f"清理临时插件解压目录失败: {temp_desti_dir},原因: {e!s}",
|
|
)
|