diff --git a/astrbot/builtin_stars/builtin_commands/commands/__init__.py b/astrbot/builtin_stars/builtin_commands/commands/__init__.py index 46d255965..552ac4a8a 100644 --- a/astrbot/builtin_stars/builtin_commands/commands/__init__.py +++ b/astrbot/builtin_stars/builtin_commands/commands/__init__.py @@ -1,29 +1,15 @@ # Commands module from .admin import AdminCommands -from .alter_cmd import AlterCmdCommands from .conversation import ConversationCommands from .help import HelpCommand -from .llm import LLMCommands -from .persona import PersonaCommands -from .plugin import PluginCommands -from .provider import ProviderCommands from .setunset import SetUnsetCommands from .sid import SIDCommand -from .t2i import T2ICommand -from .tts import TTSCommand __all__ = [ "AdminCommands", - "AlterCmdCommands", "ConversationCommands", "HelpCommand", - "LLMCommands", - "PersonaCommands", - "PluginCommands", - "ProviderCommands", - "SIDCommand", "SetUnsetCommands", - "T2ICommand", - "TTSCommand", + "SIDCommand", ] diff --git a/astrbot/builtin_stars/builtin_commands/commands/admin.py b/astrbot/builtin_stars/builtin_commands/commands/admin.py index a4f46b603..f4632536c 100644 --- a/astrbot/builtin_stars/builtin_commands/commands/admin.py +++ b/astrbot/builtin_stars/builtin_commands/commands/admin.py @@ -1,5 +1,5 @@ from astrbot.api import star -from astrbot.api.event import AstrMessageEvent, MessageChain, MessageEventResult +from astrbot.api.event import AstrMessageEvent, MessageChain from astrbot.core.config.default import VERSION from astrbot.core.utils.io import download_dashboard @@ -8,70 +8,8 @@ class AdminCommands: def __init__(self, context: star.Context) -> None: self.context = context - async def op(self, event: AstrMessageEvent, admin_id: str = "") -> None: - """授权管理员。op """ - if not admin_id: - event.set_result( - MessageEventResult().message( - "使用方法: /op 授权管理员;/deop 取消管理员。可通过 /sid 获取 ID。", - ), - ) - return - self.context.get_config()["admins_id"].append(str(admin_id)) - self.context.get_config().save_config() - event.set_result(MessageEventResult().message("授权成功。")) - - async def deop(self, event: AstrMessageEvent, admin_id: str = "") -> None: - """取消授权管理员。deop """ - if not admin_id: - event.set_result( - MessageEventResult().message( - "使用方法: /deop 取消管理员。可通过 /sid 获取 ID。", - ), - ) - return - try: - self.context.get_config()["admins_id"].remove(str(admin_id)) - self.context.get_config().save_config() - event.set_result(MessageEventResult().message("取消授权成功。")) - except ValueError: - event.set_result( - MessageEventResult().message("此用户 ID 不在管理员名单内。"), - ) - - async def wl(self, event: AstrMessageEvent, sid: str = "") -> None: - """添加白名单。wl """ - if not sid: - event.set_result( - MessageEventResult().message( - "使用方法: /wl 添加白名单;/dwl 删除白名单。可通过 /sid 获取 ID。", - ), - ) - return - cfg = self.context.get_config(umo=event.unified_msg_origin) - cfg["platform_settings"]["id_whitelist"].append(str(sid)) - cfg.save_config() - event.set_result(MessageEventResult().message("添加白名单成功。")) - - async def dwl(self, event: AstrMessageEvent, sid: str = "") -> None: - """删除白名单。dwl """ - if not sid: - event.set_result( - MessageEventResult().message( - "使用方法: /dwl 删除白名单。可通过 /sid 获取 ID。", - ), - ) - return - try: - cfg = self.context.get_config(umo=event.unified_msg_origin) - cfg["platform_settings"]["id_whitelist"].remove(str(sid)) - cfg.save_config() - event.set_result(MessageEventResult().message("删除白名单成功。")) - except ValueError: - event.set_result(MessageEventResult().message("此 SID 不在白名单内。")) - async def update_dashboard(self, event: AstrMessageEvent) -> None: """更新管理面板""" - await event.send(MessageChain().message("正在尝试更新管理面板...")) + await event.send(MessageChain().message("⏳ Updating dashboard...")) await download_dashboard(version=f"v{VERSION}", latest=False) - await event.send(MessageChain().message("管理面板更新完成。")) + await event.send(MessageChain().message("✅ Dashboard updated successfully.")) diff --git a/astrbot/builtin_stars/builtin_commands/commands/alter_cmd.py b/astrbot/builtin_stars/builtin_commands/commands/alter_cmd.py deleted file mode 100644 index ba31c3326..000000000 --- a/astrbot/builtin_stars/builtin_commands/commands/alter_cmd.py +++ /dev/null @@ -1,173 +0,0 @@ -from astrbot.api import star -from astrbot.api.event import AstrMessageEvent, MessageChain -from astrbot.core.star.filter.command import CommandFilter -from astrbot.core.star.filter.command_group import CommandGroupFilter -from astrbot.core.star.filter.permission import PermissionTypeFilter -from astrbot.core.star.star import star_map -from astrbot.core.star.star_handler import StarHandlerMetadata, star_handlers_registry -from astrbot.core.utils.command_parser import CommandParserMixin - -from .utils.rst_scene import RstScene - - -class AlterCmdCommands(CommandParserMixin): - def __init__(self, context: star.Context) -> None: - self.context = context - - async def update_reset_permission(self, scene_key: str, perm_type: str) -> None: - """更新reset命令在特定场景下的权限设置""" - from astrbot.api import sp - - alter_cmd_cfg = await sp.global_get("alter_cmd", {}) - plugin_cfg = alter_cmd_cfg.get("astrbot", {}) - reset_cfg = plugin_cfg.get("reset", {}) - reset_cfg[scene_key] = perm_type - plugin_cfg["reset"] = reset_cfg - alter_cmd_cfg["astrbot"] = plugin_cfg - await sp.global_put("alter_cmd", alter_cmd_cfg) - - async def alter_cmd(self, event: AstrMessageEvent) -> None: - token = self.parse_commands(event.message_str) - if token.len < 3: - await event.send( - MessageChain().message( - "该指令用于设置指令或指令组的权限。\n" - "格式: /alter_cmd \n" - "例1: /alter_cmd c1 admin 将 c1 设为管理员指令\n" - "例2: /alter_cmd g1 c1 admin 将 g1 指令组的 c1 子指令设为管理员指令\n" - "/alter_cmd reset config 打开 reset 权限配置", - ), - ) - return - - # 兼容 reset scene 的专门配置 - cmd_name = token.get(1) - cmd_type = token.get(2) - - if cmd_name == "reset" and cmd_type == "config": - from astrbot.api import sp - - alter_cmd_cfg = await sp.global_get("alter_cmd", {}) - plugin_ = alter_cmd_cfg.get("astrbot", {}) - reset_cfg = plugin_.get("reset", {}) - - group_unique_on = reset_cfg.get("group_unique_on", "admin") - group_unique_off = reset_cfg.get("group_unique_off", "admin") - private = reset_cfg.get("private", "member") - - config_menu = f"""reset命令权限细粒度配置 - 当前配置: - 1. 群聊+会话隔离开: {group_unique_on} - 2. 群聊+会话隔离关: {group_unique_off} - 3. 私聊: {private} - 修改指令格式: - /alter_cmd reset scene <场景编号> - 例如: /alter_cmd reset scene 2 member""" - await event.send(MessageChain().message(config_menu)) - return - - if cmd_name == "reset" and cmd_type == "scene" and token.len >= 4: - scene_num = token.get(3) - perm_type = token.get(4) - - if scene_num is None or perm_type is None: - await event.send(MessageChain().message("场景编号和权限类型不能为空")) - return - - if not scene_num.isdigit() or int(scene_num) < 1 or int(scene_num) > 3: - await event.send( - MessageChain().message("场景编号必须是 1-3 之间的数字"), - ) - return - - if perm_type not in ["admin", "member"]: - await event.send( - MessageChain().message("权限类型错误,只能是 admin 或 member"), - ) - return - - scene_num = int(scene_num) - scene = RstScene.from_index(scene_num) - scene_key = scene.key - - await self.update_reset_permission(scene_key, perm_type) - - await event.send( - MessageChain().message( - f"已将 reset 命令在{scene.name}场景下的权限设为{perm_type}", - ), - ) - return - - if cmd_type not in ["admin", "member"]: - await event.send( - MessageChain().message("指令类型错误,可选类型有 admin, member"), - ) - return - - # 查找指令 - cmd_name = " ".join(token.tokens[1:-1]) - cmd_type = token.get(-1) - found_command = None - cmd_group = False - for handler in star_handlers_registry: - assert isinstance(handler, StarHandlerMetadata) - for filter_ in handler.event_filters: - if isinstance(filter_, CommandFilter): - if filter_.equals(cmd_name): - found_command = handler - break - elif isinstance(filter_, CommandGroupFilter): - if filter_.equals(cmd_name): - found_command = handler - cmd_group = True - break - - if not found_command: - await event.send(MessageChain().message("未找到该指令")) - return - - found_plugin = star_map[found_command.handler_module_path] - - from astrbot.api import sp - - alter_cmd_cfg = await sp.global_get("alter_cmd", {}) - plugin_ = alter_cmd_cfg.get(found_plugin.name, {}) - cfg = plugin_.get(found_command.handler_name, {}) - cfg["permission"] = cmd_type - plugin_[found_command.handler_name] = cfg - alter_cmd_cfg[found_plugin.name] = plugin_ - - await sp.global_put("alter_cmd", alter_cmd_cfg) - - # 注入权限过滤器 - found_permission_filter = False - for filter_ in found_command.event_filters: - if isinstance(filter_, PermissionTypeFilter): - if cmd_type == "admin": - from astrbot.api.event import filter - - filter_.permission_type = filter.PermissionType.ADMIN - else: - from astrbot.api.event import filter - - filter_.permission_type = filter.PermissionType.MEMBER - found_permission_filter = True - break - if not found_permission_filter: - from astrbot.api.event import filter - - found_command.event_filters.insert( - 0, - PermissionTypeFilter( - filter.PermissionType.ADMIN - if cmd_type == "admin" - else filter.PermissionType.MEMBER, - ), - ) - cmd_group_str = "指令组" if cmd_group else "指令" - await event.send( - MessageChain().message( - f"已将「{cmd_name}」{cmd_group_str} 的权限级别调整为 {cmd_type}。", - ), - ) diff --git a/astrbot/builtin_stars/builtin_commands/commands/conversation.py b/astrbot/builtin_stars/builtin_commands/commands/conversation.py index 5190a363e..1a9bc48df 100644 --- a/astrbot/builtin_stars/builtin_commands/commands/conversation.py +++ b/astrbot/builtin_stars/builtin_commands/commands/conversation.py @@ -1,13 +1,9 @@ -import datetime - from astrbot.api import sp, star from astrbot.api.event import AstrMessageEvent, MessageEventResult from astrbot.core.agent.runners.deerflow.constants import ( DEERFLOW_PROVIDER_TYPE, DEERFLOW_THREAD_ID_KEY, ) -from astrbot.core.platform.astr_message_event import MessageSession -from astrbot.core.platform.message_type import MessageType from astrbot.core.utils.active_event_registry import active_event_registry from .utils.rst_scene import RstScene @@ -60,8 +56,8 @@ class ConversationCommands: if required_perm == "admin" and message.role != "admin": message.set_result( MessageEventResult().message( - f"在{scene.name}场景下,reset命令需要管理员权限," - f"您 (ID {message.get_sender_id()}) 不是管理员,无法执行此操作。", + f"Reset command requires admin permission in {scene.name} scenario, " + f"you (ID {message.get_sender_id()}) are not admin, cannot perform this action.", ), ) return @@ -74,12 +70,16 @@ class ConversationCommands: scope_id=umo, key=THIRD_PARTY_AGENT_RUNNER_KEY[agent_runner_type], ) - message.set_result(MessageEventResult().message("重置对话成功。")) + message.set_result( + MessageEventResult().message("✅ Conversation reset successfully.") + ) return if not self.context.get_using_provider(umo): message.set_result( - MessageEventResult().message("未找到任何 LLM 提供商。请先配置。"), + MessageEventResult().message( + "😕 Cannot find any LLM provider. Configure one first." + ), ) return @@ -88,7 +88,7 @@ class ConversationCommands: if not cid: message.set_result( MessageEventResult().message( - "当前未处于对话状态,请 /switch 切换或者 /new 创建。", + "😕 You are not in a conversation. Use /new to create one.", ), ) return @@ -101,7 +101,7 @@ class ConversationCommands: [], ) - ret = "清除聊天历史成功!" + ret = "✅ Conversation reset successfully." message.set_extra("_clean_ltm_session", True) @@ -124,148 +124,15 @@ class ConversationCommands: if stopped_count > 0: message.set_result( MessageEventResult().message( - f"已请求停止 {stopped_count} 个运行中的任务。" + f"✅ Requested to stop {stopped_count} running tasks." ) ) return - message.set_result(MessageEventResult().message("当前会话没有运行中的任务。")) - - async def his(self, message: AstrMessageEvent, page: int = 1) -> None: - """查看对话记录""" - if not self.context.get_using_provider(message.unified_msg_origin): - message.set_result( - MessageEventResult().message("未找到任何 LLM 提供商。请先配置。"), - ) - return - - size_per_page = 6 - - conv_mgr = self.context.conversation_manager - umo = message.unified_msg_origin - session_curr_cid = await conv_mgr.get_curr_conversation_id(umo) - - if not session_curr_cid: - session_curr_cid = await conv_mgr.new_conversation( - umo, - message.get_platform_id(), - ) - - contexts, total_pages = await conv_mgr.get_human_readable_context( - umo, - session_curr_cid, - page, - size_per_page, + message.set_result( + MessageEventResult().message("✅ No running tasks in the current session.") ) - parts = [] - for context in contexts: - if len(context) > 150: - context = context[:150] + "..." - parts.append(f"{context}\n") - - history = "".join(parts) - ret = ( - f"当前对话历史记录:" - f"{history or '无历史记录'}\n\n" - f"第 {page} 页 | 共 {total_pages} 页\n" - f"*输入 /history 2 跳转到第 2 页" - ) - - message.set_result(MessageEventResult().message(ret).use_t2i(False)) - - async def convs(self, message: AstrMessageEvent, page: int = 1) -> None: - """查看对话列表""" - cfg = self.context.get_config(umo=message.unified_msg_origin) - agent_runner_type = cfg["provider_settings"]["agent_runner_type"] - if agent_runner_type in THIRD_PARTY_AGENT_RUNNER_KEY: - message.set_result( - MessageEventResult().message( - f"{THIRD_PARTY_AGENT_RUNNER_STR} 对话列表功能暂不支持。", - ), - ) - return - - size_per_page = 6 - """获取所有对话列表""" - conversations_all = await self.context.conversation_manager.get_conversations( - message.unified_msg_origin, - ) - """计算总页数""" - total_pages = (len(conversations_all) + size_per_page - 1) // size_per_page - """确保页码有效""" - page = max(1, min(page, total_pages)) - """分页处理""" - start_idx = (page - 1) * size_per_page - end_idx = start_idx + size_per_page - conversations_paged = conversations_all[start_idx:end_idx] - - parts = ["对话列表:\n---\n"] - """全局序号从当前页的第一个开始""" - global_index = start_idx + 1 - - """生成所有对话的标题字典""" - _titles = {} - for conv in conversations_all: - title = conv.title if conv.title else "新对话" - _titles[conv.cid] = title - - """遍历分页后的对话生成列表显示""" - provider_settings = cfg.get("provider_settings", {}) - platform_name = message.get_platform_name() - for conv in conversations_paged: - ( - persona_id, - _, - force_applied_persona_id, - _, - ) = await self.context.persona_manager.resolve_selected_persona( - umo=message.unified_msg_origin, - conversation_persona_id=conv.persona_id, - platform_name=platform_name, - provider_settings=provider_settings, - ) - if persona_id == "[%None]": - persona_name = "无" - elif persona_id: - persona_name = persona_id - else: - persona_name = "无" - - if force_applied_persona_id: - persona_name = f"{persona_name} (自定义规则)" - - title = _titles.get(conv.cid, "新对话") - parts.append( - f"{global_index}. {title}({conv.cid[:4]})\n 人格情景: {persona_name}\n 上次更新: {datetime.datetime.fromtimestamp(conv.updated_at).strftime('%m-%d %H:%M')}\n" - ) - global_index += 1 - - parts.append("---\n") - ret = "".join(parts) - curr_cid = await self.context.conversation_manager.get_curr_conversation_id( - message.unified_msg_origin, - ) - if curr_cid: - """从所有对话的标题字典中获取标题""" - title = _titles.get(curr_cid, "新对话") - ret += f"\n当前对话: {title}({curr_cid[:4]})" - else: - ret += "\n当前对话: 无" - - cfg = self.context.get_config(umo=message.unified_msg_origin) - unique_session = cfg["platform_settings"]["unique_session"] - if unique_session: - ret += "\n会话隔离粒度: 个人" - else: - ret += "\n会话隔离粒度: 群聊" - - ret += f"\n第 {page} 页 | 共 {total_pages} 页" - ret += "\n*输入 /ls 2 跳转到第 2 页" - - message.set_result(MessageEventResult().message(ret).use_t2i(False)) - return - async def new_conv(self, message: AstrMessageEvent) -> None: """创建新对话""" cfg = self.context.get_config(umo=message.unified_msg_origin) @@ -277,7 +144,9 @@ class ConversationCommands: scope_id=message.unified_msg_origin, key=THIRD_PARTY_AGENT_RUNNER_KEY[agent_runner_type], ) - message.set_result(MessageEventResult().message("已创建新对话。")) + message.set_result( + MessageEventResult().message("✅ New conversation created.") + ) return active_event_registry.stop_all(message.unified_msg_origin, exclude=message) @@ -291,130 +160,7 @@ class ConversationCommands: message.set_extra("_clean_ltm_session", True) message.set_result( - MessageEventResult().message(f"切换到新对话: 新对话({cid[:4]})。"), + MessageEventResult().message( + f"✅ Switched to new conversation: {cid[:4]}。" + ), ) - - async def groupnew_conv(self, message: AstrMessageEvent, sid: str = "") -> None: - """创建新群聊对话""" - if sid: - session = str( - MessageSession( - platform_name=message.platform_meta.id, - message_type=MessageType("GroupMessage"), - session_id=sid, - ), - ) - - cpersona = await self._get_current_persona_id(session) - cid = await self.context.conversation_manager.new_conversation( - session, - message.get_platform_id(), - persona_id=cpersona, - ) - message.set_result( - MessageEventResult().message( - f"群聊 {session} 已切换到新对话: 新对话({cid[:4]})。", - ), - ) - else: - message.set_result( - MessageEventResult().message("请输入群聊 ID。/groupnew 群聊ID。"), - ) - - async def switch_conv( - self, - message: AstrMessageEvent, - index: int | None = None, - ) -> None: - """通过 /ls 前面的序号切换对话""" - if not isinstance(index, int): - message.set_result( - MessageEventResult().message("类型错误,请输入数字对话序号。"), - ) - return - - if index is None: - message.set_result( - MessageEventResult().message( - "请输入对话序号。/switch 对话序号。/ls 查看对话 /new 新建对话", - ), - ) - return - conversations = await self.context.conversation_manager.get_conversations( - message.unified_msg_origin, - ) - if index > len(conversations) or index < 1: - message.set_result( - MessageEventResult().message("对话序号错误,请使用 /ls 查看"), - ) - else: - conversation = conversations[index - 1] - title = conversation.title if conversation.title else "新对话" - await self.context.conversation_manager.switch_conversation( - message.unified_msg_origin, - conversation.cid, - ) - message.set_result( - MessageEventResult().message( - f"切换到对话: {title}({conversation.cid[:4]})。", - ), - ) - - async def rename_conv(self, message: AstrMessageEvent, new_name: str = "") -> None: - """重命名对话""" - if not new_name: - message.set_result(MessageEventResult().message("请输入新的对话名称。")) - return - await self.context.conversation_manager.update_conversation_title( - message.unified_msg_origin, - new_name, - ) - message.set_result(MessageEventResult().message("重命名对话成功。")) - - async def del_conv(self, message: AstrMessageEvent) -> None: - """删除当前对话""" - umo = message.unified_msg_origin - cfg = self.context.get_config(umo=umo) - is_unique_session = cfg["platform_settings"]["unique_session"] - if message.get_group_id() and not is_unique_session and message.role != "admin": - # 群聊,没开独立会话,发送人不是管理员 - message.set_result( - MessageEventResult().message( - f"会话处于群聊,并且未开启独立会话,并且您 (ID {message.get_sender_id()}) 不是管理员,因此没有权限删除当前对话。", - ), - ) - return - - agent_runner_type = cfg["provider_settings"]["agent_runner_type"] - if agent_runner_type in THIRD_PARTY_AGENT_RUNNER_KEY: - active_event_registry.stop_all(umo, exclude=message) - await sp.remove_async( - scope="umo", - scope_id=umo, - key=THIRD_PARTY_AGENT_RUNNER_KEY[agent_runner_type], - ) - message.set_result(MessageEventResult().message("重置对话成功。")) - return - - session_curr_cid = ( - await self.context.conversation_manager.get_curr_conversation_id(umo) - ) - - if not session_curr_cid: - message.set_result( - MessageEventResult().message( - "当前未处于对话状态,请 /switch 序号 切换或 /new 创建。", - ), - ) - return - - active_event_registry.stop_all(umo, exclude=message) - - await self.context.conversation_manager.delete_conversation( - umo, - session_curr_cid, - ) - - ret = "删除当前对话成功。不再处于对话状态,使用 /switch 序号 切换到其他对话或 /new 创建。" - message.set_extra("_clean_ltm_session", True) - message.set_result(MessageEventResult().message(ret)) diff --git a/astrbot/builtin_stars/builtin_commands/commands/help.py b/astrbot/builtin_stars/builtin_commands/commands/help.py index ae2f4c787..494fd4a89 100644 --- a/astrbot/builtin_stars/builtin_commands/commands/help.py +++ b/astrbot/builtin_stars/builtin_commands/commands/help.py @@ -32,7 +32,6 @@ class HelpCommand: return [] lines: list[str] = [] - hidden_commands = {"set", "unset", "websearch"} def walk(items: list[dict], indent: int = 0) -> None: for item in items: @@ -49,9 +48,7 @@ class HelpCommand: or item.get("original_command") or item.get("handler_name") ) - if not effective: - continue - if effective in hidden_commands: + if not effective or effective in ["set", "unset", "help"]: continue description = item.get("description") or "" @@ -73,12 +70,13 @@ class HelpCommand: dashboard_version = await get_dashboard_version() command_lines = await self._build_reserved_command_lines() commands_section = ( - "\n".join(command_lines) if command_lines else "暂无启用的内置指令" + "\n".join(command_lines) + if command_lines + else "No enabled built-in commands." ) msg_parts = [ f"AstrBot v{VERSION}(WebUI: {dashboard_version})", - "内置指令:", commands_section, ] if notice: diff --git a/astrbot/builtin_stars/builtin_commands/commands/llm.py b/astrbot/builtin_stars/builtin_commands/commands/llm.py deleted file mode 100644 index ba9ba5c9b..000000000 --- a/astrbot/builtin_stars/builtin_commands/commands/llm.py +++ /dev/null @@ -1,20 +0,0 @@ -from astrbot.api import star -from astrbot.api.event import AstrMessageEvent, MessageChain - - -class LLMCommands: - def __init__(self, context: star.Context) -> None: - self.context = context - - async def llm(self, event: AstrMessageEvent) -> None: - """开启/关闭 LLM""" - cfg = self.context.get_config(umo=event.unified_msg_origin) - enable = cfg["provider_settings"].get("enable", True) - if enable: - cfg["provider_settings"]["enable"] = False - status = "关闭" - else: - cfg["provider_settings"]["enable"] = True - status = "开启" - cfg.save_config() - await event.send(MessageChain().message(f"{status} LLM 聊天功能。")) diff --git a/astrbot/builtin_stars/builtin_commands/commands/persona.py b/astrbot/builtin_stars/builtin_commands/commands/persona.py deleted file mode 100644 index 7a7416bba..000000000 --- a/astrbot/builtin_stars/builtin_commands/commands/persona.py +++ /dev/null @@ -1,216 +0,0 @@ -import builtins -from typing import TYPE_CHECKING - -from astrbot.api import star -from astrbot.api.event import AstrMessageEvent, MessageEventResult - -if TYPE_CHECKING: - from astrbot.core.db.po import Persona - - -class PersonaCommands: - def __init__(self, context: star.Context) -> None: - self.context = context - - def _build_tree_output( - self, - folder_tree: list[dict], - all_personas: list["Persona"], - depth: int = 0, - ) -> list[str]: - """递归构建树状输出,使用短线条表示层级""" - lines: list[str] = [] - # 使用短线条作为缩进前缀,每层只用 "│" 加一个空格 - prefix = "│ " * depth - - for folder in folder_tree: - # 输出文件夹 - lines.append(f"{prefix}├ 📁 {folder['name']}/") - - # 获取该文件夹下的人格 - folder_personas = [ - p for p in all_personas if p.folder_id == folder["folder_id"] - ] - child_prefix = "│ " * (depth + 1) - - # 输出该文件夹下的人格 - for persona in folder_personas: - lines.append(f"{child_prefix}├ 👤 {persona.persona_id}") - - # 递归处理子文件夹 - children = folder.get("children", []) - if children: - lines.extend( - self._build_tree_output( - children, - all_personas, - depth + 1, - ) - ) - - return lines - - async def persona(self, message: AstrMessageEvent) -> None: - l = message.message_str.split(" ") # noqa: E741 - umo = message.unified_msg_origin - - curr_persona_name = "无" - cid = await self.context.conversation_manager.get_curr_conversation_id(umo) - default_persona = await self.context.persona_manager.get_default_persona_v3( - umo=umo, - ) - force_applied_persona_id = None - - curr_cid_title = "无" - if cid: - conv = await self.context.conversation_manager.get_conversation( - unified_msg_origin=umo, - conversation_id=cid, - create_if_not_exists=True, - ) - if conv is None: - message.set_result( - MessageEventResult().message( - "当前对话不存在,请先使用 /new 新建一个对话。", - ), - ) - return - - provider_settings = self.context.get_config(umo=umo).get( - "provider_settings", - {}, - ) - ( - persona_id, - _, - force_applied_persona_id, - _, - ) = await self.context.persona_manager.resolve_selected_persona( - umo=umo, - conversation_persona_id=conv.persona_id, - platform_name=message.get_platform_name(), - provider_settings=provider_settings, - ) - - if persona_id == "[%None]": - curr_persona_name = "无" - elif persona_id: - curr_persona_name = persona_id - - if force_applied_persona_id: - curr_persona_name = f"{curr_persona_name} (自定义规则)" - - curr_cid_title = conv.title if conv.title else "新对话" - curr_cid_title += f"({cid[:4]})" - - if len(l) == 1: - message.set_result( - MessageEventResult() - .message( - f"""[Persona] - -- 人格情景列表: `/persona list` -- 设置人格情景: `/persona 人格` -- 人格情景详细信息: `/persona view 人格` -- 取消人格: `/persona unset` - -默认人格情景: {default_persona["name"]} -当前对话 {curr_cid_title} 的人格情景: {curr_persona_name} - -配置人格情景请前往管理面板-配置页 -""", - ) - .use_t2i(False), - ) - elif l[1] == "list": - # 获取文件夹树和所有人格 - folder_tree = await self.context.persona_manager.get_folder_tree() - all_personas = self.context.persona_manager.personas - - lines = ["📂 人格列表:\n"] - - # 构建树状输出 - tree_lines = self._build_tree_output(folder_tree, all_personas) - lines.extend(tree_lines) - - # 输出根目录下的人格(没有文件夹的) - root_personas = [p for p in all_personas if p.folder_id is None] - if root_personas: - if tree_lines: # 如果有文件夹内容,加个空行 - lines.append("") - for persona in root_personas: - lines.append(f"👤 {persona.persona_id}") - - # 统计信息 - total_count = len(all_personas) - lines.append(f"\n共 {total_count} 个人格") - lines.append("\n*使用 `/persona <人格名>` 设置人格") - lines.append("*使用 `/persona view <人格名>` 查看详细信息") - - msg = "\n".join(lines) - message.set_result(MessageEventResult().message(msg).use_t2i(False)) - elif l[1] == "view": - if len(l) == 2: - message.set_result(MessageEventResult().message("请输入人格情景名")) - return - ps = l[2].strip() - if persona := next( - builtins.filter( - lambda persona: persona["name"] == ps, - self.context.provider_manager.personas, - ), - None, - ): - msg = f"人格{ps}的详细信息:\n" - msg += f"{persona['prompt']}\n" - else: - msg = f"人格{ps}不存在" - message.set_result(MessageEventResult().message(msg)) - elif l[1] == "unset": - if not cid: - message.set_result( - MessageEventResult().message("当前没有对话,无法取消人格。"), - ) - return - await self.context.conversation_manager.update_conversation_persona_id( - message.unified_msg_origin, - "[%None]", - ) - message.set_result(MessageEventResult().message("取消人格成功。")) - else: - ps = "".join(l[1:]).strip() - if not cid: - message.set_result( - MessageEventResult().message( - "当前没有对话,请先开始对话或使用 /new 创建一个对话。", - ), - ) - return - if persona := next( - builtins.filter( - lambda persona: persona["name"] == ps, - self.context.provider_manager.personas, - ), - None, - ): - await self.context.conversation_manager.update_conversation_persona_id( - message.unified_msg_origin, - ps, - ) - force_warn_msg = "" - if force_applied_persona_id: - force_warn_msg = ( - "提醒:由于自定义规则,您现在切换的人格将不会生效。" - ) - - message.set_result( - MessageEventResult().message( - f"设置成功。如果您正在切换到不同的人格,请注意使用 /reset 来清空上下文,防止原人格对话影响现人格。{force_warn_msg}", - ), - ) - else: - message.set_result( - MessageEventResult().message( - "不存在该人格情景。使用 /persona list 查看所有。", - ), - ) diff --git a/astrbot/builtin_stars/builtin_commands/commands/plugin.py b/astrbot/builtin_stars/builtin_commands/commands/plugin.py deleted file mode 100644 index 49bee9462..000000000 --- a/astrbot/builtin_stars/builtin_commands/commands/plugin.py +++ /dev/null @@ -1,120 +0,0 @@ -from astrbot.api import star -from astrbot.api.event import AstrMessageEvent, MessageEventResult -from astrbot.core import DEMO_MODE, logger -from astrbot.core.star.filter.command import CommandFilter -from astrbot.core.star.filter.command_group import CommandGroupFilter -from astrbot.core.star.star_handler import StarHandlerMetadata, star_handlers_registry -from astrbot.core.star.star_manager import PluginManager - - -class PluginCommands: - def __init__(self, context: star.Context) -> None: - self.context = context - - async def plugin_ls(self, event: AstrMessageEvent) -> None: - """获取已经安装的插件列表。""" - parts = ["已加载的插件:\n"] - for plugin in self.context.get_all_stars(): - line = f"- `{plugin.name}` By {plugin.author}: {plugin.desc}" - if not plugin.activated: - line += " (未启用)" - parts.append(line + "\n") - - if len(parts) == 1: - plugin_list_info = "没有加载任何插件。" - else: - plugin_list_info = "".join(parts) - - plugin_list_info += "\n使用 /plugin help <插件名> 查看插件帮助和加载的指令。\n使用 /plugin on/off <插件名> 启用或者禁用插件。" - event.set_result( - MessageEventResult().message(f"{plugin_list_info}").use_t2i(False), - ) - - async def plugin_off(self, event: AstrMessageEvent, plugin_name: str = "") -> None: - """禁用插件""" - if DEMO_MODE: - event.set_result(MessageEventResult().message("演示模式下无法禁用插件。")) - return - if not plugin_name: - event.set_result( - MessageEventResult().message("/plugin off <插件名> 禁用插件。"), - ) - return - await self.context._star_manager.turn_off_plugin(plugin_name) # type: ignore - event.set_result(MessageEventResult().message(f"插件 {plugin_name} 已禁用。")) - - async def plugin_on(self, event: AstrMessageEvent, plugin_name: str = "") -> None: - """启用插件""" - if DEMO_MODE: - event.set_result(MessageEventResult().message("演示模式下无法启用插件。")) - return - if not plugin_name: - event.set_result( - MessageEventResult().message("/plugin on <插件名> 启用插件。"), - ) - return - await self.context._star_manager.turn_on_plugin(plugin_name) # type: ignore - event.set_result(MessageEventResult().message(f"插件 {plugin_name} 已启用。")) - - async def plugin_get(self, event: AstrMessageEvent, plugin_repo: str = "") -> None: - """安装插件""" - if DEMO_MODE: - event.set_result(MessageEventResult().message("演示模式下无法安装插件。")) - return - if not plugin_repo: - event.set_result( - MessageEventResult().message("/plugin get <插件仓库地址> 安装插件"), - ) - return - logger.info(f"准备从 {plugin_repo} 安装插件。") - if self.context._star_manager: - star_mgr: PluginManager = self.context._star_manager - try: - await star_mgr.install_plugin(plugin_repo) # type: ignore - event.set_result(MessageEventResult().message("安装插件成功。")) - except Exception as e: - logger.error(f"安装插件失败: {e}") - event.set_result(MessageEventResult().message(f"安装插件失败: {e}")) - return - - async def plugin_help(self, event: AstrMessageEvent, plugin_name: str = "") -> None: - """获取插件帮助""" - if not plugin_name: - event.set_result( - MessageEventResult().message("/plugin help <插件名> 查看插件信息。"), - ) - return - plugin = self.context.get_registered_star(plugin_name) - if plugin is None: - event.set_result(MessageEventResult().message("未找到此插件。")) - return - help_msg = "" - help_msg += f"\n\n✨ 作者: {plugin.author}\n✨ 版本: {plugin.version}" - command_handlers = [] - command_names = [] - for handler in star_handlers_registry: - assert isinstance(handler, StarHandlerMetadata) - if handler.handler_module_path != plugin.module_path: - continue - for filter_ in handler.event_filters: - if isinstance(filter_, CommandFilter): - command_handlers.append(handler) - command_names.append(filter_.command_name) - break - if isinstance(filter_, CommandGroupFilter): - command_handlers.append(handler) - command_names.append(filter_.group_name) - - if len(command_handlers) > 0: - parts = ["\n\n🔧 指令列表:\n"] - for i in range(len(command_handlers)): - line = f"- {command_names[i]}" - if command_handlers[i].desc: - line += f": {command_handlers[i].desc}" - parts.append(line + "\n") - parts.append("\nTip: 指令的触发需要添加唤醒前缀,默认为 /。") - help_msg += "".join(parts) - - ret = f"🧩 插件 {plugin_name} 帮助信息:\n" + help_msg - ret += "更多帮助信息请查看插件仓库 README。" - event.set_result(MessageEventResult().message(ret).use_t2i(False)) diff --git a/astrbot/builtin_stars/builtin_commands/commands/provider.py b/astrbot/builtin_stars/builtin_commands/commands/provider.py deleted file mode 100644 index b5ee75ca2..000000000 --- a/astrbot/builtin_stars/builtin_commands/commands/provider.py +++ /dev/null @@ -1,736 +0,0 @@ -from __future__ import annotations - -import asyncio -import time -from collections.abc import Sequence -from dataclasses import dataclass -from typing import TYPE_CHECKING - -from astrbot import logger -from astrbot.api import star -from astrbot.api.event import AstrMessageEvent, MessageEventResult -from astrbot.core.provider.entities import ProviderType -from astrbot.core.utils.error_redaction import safe_error - -if TYPE_CHECKING: - from astrbot.core.provider.provider import Provider - - -MODEL_LIST_CACHE_TTL_SECONDS_DEFAULT = 30.0 -MODEL_LOOKUP_MAX_CONCURRENCY_DEFAULT = 4 -MODEL_LOOKUP_MAX_CONCURRENCY_UPPER_BOUND = 16 -MODEL_LIST_CACHE_TTL_KEY = "model_list_cache_ttl_seconds" -MODEL_LOOKUP_MAX_CONCURRENCY_KEY = "model_lookup_max_concurrency" -MODEL_CACHE_MAX_ENTRIES = 512 - - -@dataclass(frozen=True) -class _ModelLookupConfig: - umo: str | None - cache_ttl_seconds: float - max_concurrency: int - - -class _ModelCache: - def __init__(self) -> None: - self._store: dict[tuple[str, str | None], tuple[float, list[str]]] = {} - - def get(self, provider_id: str, umo: str | None, ttl: float) -> list[str] | None: - if ttl <= 0: - return None - entry = self._store.get((provider_id, umo)) - if not entry: - return None - timestamp, models = entry - if time.monotonic() - timestamp > ttl: - self._store.pop((provider_id, umo), None) - return None - return models - - def set( - self, provider_id: str, umo: str | None, models: list[str], ttl: float - ) -> None: - if ttl <= 0: - return - self._store[(provider_id, umo)] = (time.monotonic(), list(models)) - self._evict_if_needed() - - def _evict_if_needed(self) -> None: - if len(self._store) <= MODEL_CACHE_MAX_ENTRIES: - return - # Drop oldest entries first when cache grows too large. - overflow = len(self._store) - MODEL_CACHE_MAX_ENTRIES - for key, _ in sorted( - self._store.items(), - key=lambda item: item[1][0], - )[:overflow]: - self._store.pop(key, None) - - def invalidate( - self, provider_id: str | None = None, *, umo: str | None = None - ) -> None: - if provider_id is None: - self._store.clear() - return - if umo is not None: - self._store.pop((provider_id, umo), None) - return - stale_keys = [ - cache_key for cache_key in self._store if cache_key[0] == provider_id - ] - for cache_key in stale_keys: - self._store.pop(cache_key, None) - - -class ProviderCommands: - def __init__(self, context: star.Context) -> None: - self.context = context - self._model_cache = _ModelCache() - self._register_provider_change_hook() - - def _register_provider_change_hook(self) -> None: - set_change_callback = getattr( - self.context.provider_manager, - "set_provider_change_callback", - None, - ) - if callable(set_change_callback): - set_change_callback(self._on_provider_manager_changed) - return - register_change_hook = getattr( - self.context.provider_manager, - "register_provider_change_hook", - None, - ) - if callable(register_change_hook): - register_change_hook(self._on_provider_manager_changed) - - def invalidate_provider_models_cache( - self, provider_id: str | None = None, *, umo: str | None = None - ) -> None: - """Public hook for cache invalidation on external provider config changes.""" - self._model_cache.invalidate(provider_id, umo=umo) - - def _on_provider_manager_changed( - self, - provider_id: str, - provider_type: ProviderType, - umo: str | None, - ) -> None: - if provider_type == ProviderType.CHAT_COMPLETION: - self.invalidate_provider_models_cache(provider_id, umo=umo) - - def _get_provider_settings(self, umo: str | None) -> dict: - if not umo: - return {} - try: - return self.context.get_config(umo).get("provider_settings", {}) or {} - except Exception as e: - logger.debug( - "读取 provider_settings 失败,使用默认值: %s", - safe_error("", e), - ) - return {} - - def _get_model_cache_ttl(self, umo: str | None) -> float: - settings = self._get_provider_settings(umo) - raw = settings.get( - MODEL_LIST_CACHE_TTL_KEY, - MODEL_LIST_CACHE_TTL_SECONDS_DEFAULT, - ) - try: - return max(float(raw), 0.0) - except Exception as e: - logger.debug( - "读取 %s 失败,回退默认值 %r: %s", - MODEL_LIST_CACHE_TTL_KEY, - MODEL_LIST_CACHE_TTL_SECONDS_DEFAULT, - safe_error("", e), - ) - return MODEL_LIST_CACHE_TTL_SECONDS_DEFAULT - - def _get_model_lookup_concurrency(self, umo: str | None) -> int: - settings = self._get_provider_settings(umo) - raw = settings.get( - MODEL_LOOKUP_MAX_CONCURRENCY_KEY, - MODEL_LOOKUP_MAX_CONCURRENCY_DEFAULT, - ) - try: - value = int(raw) - except Exception as e: - logger.debug( - "读取 %s 失败,回退默认值 %r: %s", - MODEL_LOOKUP_MAX_CONCURRENCY_KEY, - MODEL_LOOKUP_MAX_CONCURRENCY_DEFAULT, - safe_error("", e), - ) - value = MODEL_LOOKUP_MAX_CONCURRENCY_DEFAULT - return min(max(value, 1), MODEL_LOOKUP_MAX_CONCURRENCY_UPPER_BOUND) - - def _get_model_lookup_config(self, umo: str | None) -> _ModelLookupConfig: - return _ModelLookupConfig( - umo=umo, - cache_ttl_seconds=self._get_model_cache_ttl(umo), - max_concurrency=self._get_model_lookup_concurrency(umo), - ) - - def _resolve_model_name( - self, - model_name: str, - models: Sequence[str], - ) -> str | None: - """Resolve model name with precedence: - exact > case-insensitive > provider-qualified suffix. - """ - requested = model_name.strip() - if not requested: - return None - - requested_norm = requested.casefold() - - # exact / case-insensitive match - for candidate in models: - if candidate == requested or candidate.casefold() == requested_norm: - return candidate - - # provider-qualified suffix match: - # e.g. candidate `openai/gpt-4o` should match requested `gpt-4o`. - for candidate in models: - cand_norm = candidate.casefold() - if cand_norm.endswith(f"/{requested_norm}") or cand_norm.endswith( - f":{requested_norm}" - ): - return candidate - - return None - - def _apply_model( - self, prov: Provider, model_name: str, *, umo: str | None = None - ) -> str: - prov.set_model(model_name) - self.invalidate_provider_models_cache(prov.meta().id, umo=umo) - return f"切换模型成功。当前提供商: [{prov.meta().id}] 当前模型: [{prov.get_model()}]" - - async def _get_provider_models( - self, - provider: Provider, - *, - config: _ModelLookupConfig, - use_cache: bool = True, - ) -> list[str]: - provider_id = provider.meta().id - ttl_seconds = config.cache_ttl_seconds - umo = config.umo - if use_cache: - cached = self._model_cache.get(provider_id, umo, ttl_seconds) - if cached is not None: - return cached - - models = list(await provider.get_models()) - if use_cache: - self._model_cache.set(provider_id, umo, models, ttl_seconds) - return models - - async def _get_models_or_reply_error( - self, - message: AstrMessageEvent, - prov: Provider, - config: _ModelLookupConfig, - *, - error_prefix: str, - disable_t2i: bool = False, - warning_log: str | None = None, - ) -> list[str] | None: - try: - return await self._get_provider_models(prov, config=config) - except asyncio.CancelledError: - raise - except Exception as e: - if warning_log is not None: - logger.warning( - warning_log, - prov.meta().id, - safe_error("", e), - ) - result = MessageEventResult().message(safe_error(error_prefix, e)) - if disable_t2i: - result = result.use_t2i(False) - message.set_result(result) - return None - - def _log_reachability_failure( - self, - provider, - provider_capability_type: ProviderType | None, - err_code: str, - err_reason: str, - ) -> None: - """记录不可达原因到日志。""" - meta = provider.meta() - logger.warning( - "Provider reachability check failed: id=%s type=%s code=%s reason=%s", - meta.id, - provider_capability_type.name if provider_capability_type else "unknown", - err_code, - err_reason, - ) - - async def _test_provider_capability(self, provider): - """测试单个 provider 的可用性""" - meta = provider.meta() - provider_capability_type = meta.provider_type - - try: - await provider.test() - return True, None, None - except Exception as e: - err_code = "TEST_FAILED" - err_reason = safe_error("", e) - self._log_reachability_failure( - provider, provider_capability_type, err_code, err_reason - ) - return False, err_code, err_reason - - async def _find_provider_for_model( - self, - model_name: str, - *, - exclude_provider_id: str | None = None, - config: _ModelLookupConfig, - use_cache: bool = True, - ) -> tuple[Provider | None, str | None]: - all_providers = [] - for provider in self.context.get_all_providers(): - provider_meta = provider.meta() - if provider_meta.provider_type != ProviderType.CHAT_COMPLETION: - continue - if ( - exclude_provider_id is not None - and provider_meta.id == exclude_provider_id - ): - continue - all_providers.append(provider) - if not all_providers: - return None, None - - semaphore = asyncio.Semaphore(config.max_concurrency) - - async def fetch_models( - provider: Provider, - ) -> tuple[Provider, list[str] | None, str | None]: - async with semaphore: - try: - models = await self._get_provider_models( - provider, - config=config, - use_cache=use_cache, - ) - return provider, models, None - except asyncio.CancelledError: - raise - except Exception as e: - err = safe_error("", e) - logger.debug( - "跨提供商查找模型 %s 获取 %s 模型列表失败: %s", - model_name, - provider.meta().id, - err, - ) - return provider, None, err - - results = await asyncio.gather( - *(fetch_models(provider) for provider in all_providers) - ) - failed_provider_errors: list[tuple[str, str]] = [] - for provider, models, err in results: - if err is not None: - failed_provider_errors.append((provider.meta().id, err)) - continue - if models is None: - continue - - matched_model_name = self._resolve_model_name(model_name, models) - if matched_model_name is not None: - return provider, matched_model_name - - if failed_provider_errors and len(failed_provider_errors) == len(all_providers): - failed_ids = ",".join( - provider_id for provider_id, _ in failed_provider_errors - ) - logger.error( - "跨提供商查找模型 %s 时,所有 %d 个提供商的 get_models() 均失败: %s。请检查配置或网络", - model_name, - len(all_providers), - failed_ids, - ) - elif failed_provider_errors: - logger.debug( - "跨提供商查找模型 %s 时有 %d 个提供商获取模型失败: %s", - model_name, - len(failed_provider_errors), - ",".join( - f"{provider_id}({error})" - for provider_id, error in failed_provider_errors - ), - ) - return None, None - - async def provider( - self, - event: AstrMessageEvent, - idx: str | int | None = None, - idx2: int | None = None, - ) -> None: - """查看或者切换 LLM Provider""" - umo = event.unified_msg_origin - cfg = self.context.get_config(umo).get("provider_settings", {}) - reachability_check_enabled = cfg.get("reachability_check", True) - - if idx is None: - parts = ["## 载入的 LLM 提供商\n"] - - # 获取所有类型的提供商 - llms = list(self.context.get_all_providers()) - ttss = self.context.get_all_tts_providers() - stts = self.context.get_all_stt_providers() - - # 构造待检测列表: [(provider, type_label), ...] - all_providers = [] - all_providers.extend([(p, "llm") for p in llms]) - all_providers.extend([(p, "tts") for p in ttss]) - all_providers.extend([(p, "stt") for p in stts]) - - # 并发测试连通性 - if reachability_check_enabled: - if all_providers: - await event.send( - MessageEventResult().message( - "正在进行提供商可达性测试,请稍候..." - ) - ) - check_results = await asyncio.gather( - *[self._test_provider_capability(p) for p, _ in all_providers], - return_exceptions=True, - ) - else: - # 用 None 表示未检测 - check_results = [None for _ in all_providers] - - # 整合结果 - display_data = [] - for (p, p_type), reachable in zip(all_providers, check_results): - meta = p.meta() - id_ = meta.id - error_code = None - - if isinstance(reachable, asyncio.CancelledError): - raise reachable - if isinstance(reachable, Exception): - # 异常情况下兜底处理,避免单个 provider 导致列表失败 - self._log_reachability_failure( - p, - None, - reachable.__class__.__name__, - safe_error("", reachable), - ) - reachable_flag = False - error_code = reachable.__class__.__name__ - elif isinstance(reachable, tuple): - reachable_flag, error_code, _ = reachable - else: - reachable_flag = reachable - - # 根据类型构建显示名称 - if p_type == "llm": - info = f"{id_} ({meta.model})" - else: - info = f"{id_}" - - # 确定状态标记 - if reachable_flag is True: - mark = " ✅" - elif reachable_flag is False: - if error_code: - mark = f" ❌(错误码: {error_code})" - else: - mark = " ❌" - else: - mark = "" # 不支持检测时不显示标记 - - display_data.append( - { - "type": p_type, - "info": info, - "mark": mark, - "provider": p, - } - ) - - # 分组输出 - # 1. LLM - llm_data = [d for d in display_data if d["type"] == "llm"] - for i, d in enumerate(llm_data): - line = f"{i + 1}. {d['info']}{d['mark']}" - provider_using = self.context.get_using_provider(umo=umo) - if ( - provider_using - and provider_using.meta().id == d["provider"].meta().id - ): - line += " (当前使用)" - parts.append(line + "\n") - - # 2. TTS - tts_data = [d for d in display_data if d["type"] == "tts"] - if tts_data: - parts.append("\n## 载入的 TTS 提供商\n") - for i, d in enumerate(tts_data): - line = f"{i + 1}. {d['info']}{d['mark']}" - tts_using = self.context.get_using_tts_provider(umo=umo) - if tts_using and tts_using.meta().id == d["provider"].meta().id: - line += " (当前使用)" - parts.append(line + "\n") - - # 3. STT - stt_data = [d for d in display_data if d["type"] == "stt"] - if stt_data: - parts.append("\n## 载入的 STT 提供商\n") - for i, d in enumerate(stt_data): - line = f"{i + 1}. {d['info']}{d['mark']}" - stt_using = self.context.get_using_stt_provider(umo=umo) - if stt_using and stt_using.meta().id == d["provider"].meta().id: - line += " (当前使用)" - parts.append(line + "\n") - - parts.append("\n使用 /provider <序号> 切换 LLM 提供商。") - ret = "".join(parts) - - if ttss: - ret += "\n使用 /provider tts <序号> 切换 TTS 提供商。" - if stts: - ret += "\n使用 /provider stt <序号> 切换 STT 提供商。" - if not reachability_check_enabled: - ret += "\n已跳过提供商可达性检测,如需检测请在配置文件中开启。" - - event.set_result(MessageEventResult().message(ret)) - elif idx == "tts": - if idx2 is None: - event.set_result(MessageEventResult().message("请输入序号。")) - return - if idx2 > len(self.context.get_all_tts_providers()) or idx2 < 1: - event.set_result(MessageEventResult().message("无效的提供商序号。")) - return - provider = self.context.get_all_tts_providers()[idx2 - 1] - id_ = provider.meta().id - await self.context.provider_manager.set_provider( - provider_id=id_, - provider_type=ProviderType.TEXT_TO_SPEECH, - umo=umo, - ) - event.set_result(MessageEventResult().message(f"成功切换到 {id_}。")) - elif idx == "stt": - if idx2 is None: - event.set_result(MessageEventResult().message("请输入序号。")) - return - if idx2 > len(self.context.get_all_stt_providers()) or idx2 < 1: - event.set_result(MessageEventResult().message("无效的提供商序号。")) - return - provider = self.context.get_all_stt_providers()[idx2 - 1] - id_ = provider.meta().id - await self.context.provider_manager.set_provider( - provider_id=id_, - provider_type=ProviderType.SPEECH_TO_TEXT, - umo=umo, - ) - event.set_result(MessageEventResult().message(f"成功切换到 {id_}。")) - elif isinstance(idx, int): - if idx > len(self.context.get_all_providers()) or idx < 1: - event.set_result(MessageEventResult().message("无效的提供商序号。")) - return - provider = self.context.get_all_providers()[idx - 1] - id_ = provider.meta().id - await self.context.provider_manager.set_provider( - provider_id=id_, - provider_type=ProviderType.CHAT_COMPLETION, - umo=umo, - ) - event.set_result(MessageEventResult().message(f"成功切换到 {id_}。")) - else: - event.set_result(MessageEventResult().message("无效的参数。")) - - async def _switch_model_by_name( - self, message: AstrMessageEvent, model_name: str, prov: Provider - ) -> None: - model_name = model_name.strip() - if not model_name: - message.set_result(MessageEventResult().message("模型名不能为空。")) - return - - umo = message.unified_msg_origin - config = self._get_model_lookup_config(umo) - curr_provider_id = prov.meta().id - - models = await self._get_models_or_reply_error( - message, - prov, - config, - error_prefix="获取当前提供商模型列表失败: ", - warning_log="获取当前提供商 %s 模型列表失败,停止跨提供商查找: %s", - ) - if models is None: - return - - matched_model_name = self._resolve_model_name(model_name, models) - if matched_model_name is not None: - message.set_result( - MessageEventResult().message( - self._apply_model(prov, matched_model_name, umo=umo) - ), - ) - return - - target_prov, matched_target_model_name = await self._find_provider_for_model( - model_name, - exclude_provider_id=curr_provider_id, - config=config, - ) - - if target_prov is None or matched_target_model_name is None: - message.set_result( - MessageEventResult().message( - f"模型 [{model_name}] 未在任何已配置的提供商中找到,或所有提供商模型列表获取失败,请检查配置或网络后重试。", - ), - ) - return - - target_id = target_prov.meta().id - try: - await self.context.provider_manager.set_provider( - provider_id=target_id, - provider_type=ProviderType.CHAT_COMPLETION, - umo=umo, - ) - self._apply_model(target_prov, matched_target_model_name, umo=umo) - message.set_result( - MessageEventResult().message( - f"检测到模型 [{matched_target_model_name}] 属于提供商 [{target_id}],已自动切换提供商并设置模型。", - ), - ) - except asyncio.CancelledError: - raise - except Exception as e: - message.set_result( - MessageEventResult().message( - safe_error("跨提供商切换并设置模型失败: ", e) - ), - ) - - async def model_ls( - self, - message: AstrMessageEvent, - idx_or_name: int | str | None = None, - ) -> None: - """查看或者切换模型""" - prov = self.context.get_using_provider(message.unified_msg_origin) - if not prov: - message.set_result( - MessageEventResult().message("未找到任何 LLM 提供商。请先配置。"), - ) - return - config = self._get_model_lookup_config(message.unified_msg_origin) - - if idx_or_name is None: - models = await self._get_models_or_reply_error( - message, - prov, - config, - error_prefix="获取模型列表失败: ", - disable_t2i=True, - ) - if models is None: - return - parts = ["下面列出了此模型提供商可用模型:"] - for i, model in enumerate(models, 1): - parts.append(f"\n{i}. {model}") - - curr_model = prov.get_model() or "无" - parts.append(f"\n当前模型: [{curr_model}]") - parts.append( - "\nTips: 使用 /model <模型名/编号> 切换模型。输入模型名时可自动跨提供商查找并切换;跨提供商也可使用 /provider 切换。" - ) - - ret = "".join(parts) - message.set_result(MessageEventResult().message(ret).use_t2i(False)) - elif isinstance(idx_or_name, int): - models = await self._get_models_or_reply_error( - message, - prov, - config, - error_prefix="获取模型列表失败: ", - ) - if models is None: - return - if idx_or_name > len(models) or idx_or_name < 1: - message.set_result(MessageEventResult().message("模型序号错误。")) - else: - try: - new_model = models[idx_or_name - 1] - message.set_result( - MessageEventResult().message( - self._apply_model( - prov, - new_model, - umo=message.unified_msg_origin, - ) - ), - ) - except Exception as e: - message.set_result( - MessageEventResult().message( - safe_error("切换模型未知错误: ", e) - ), - ) - return - else: - await self._switch_model_by_name(message, idx_or_name, prov) - - async def key(self, message: AstrMessageEvent, index: int | None = None) -> None: - prov = self.context.get_using_provider(message.unified_msg_origin) - if not prov: - message.set_result( - MessageEventResult().message("未找到任何 LLM 提供商。请先配置。"), - ) - return - - if index is None: - keys_data = prov.get_keys() - curr_key = prov.get_current_key() - parts = ["Key:"] - for i, k in enumerate(keys_data, 1): - parts.append(f"\n{i}. {k[:8]}") - - parts.append(f"\n当前 Key: {curr_key[:8]}") - parts.append("\n当前模型: " + prov.get_model()) - parts.append("\n使用 /key 切换 Key。") - - ret = "".join(parts) - message.set_result(MessageEventResult().message(ret).use_t2i(False)) - else: - keys_data = prov.get_keys() - if index > len(keys_data) or index < 1: - message.set_result(MessageEventResult().message("Key 序号错误。")) - else: - try: - new_key = keys_data[index - 1] - prov.set_key(new_key) - self.invalidate_provider_models_cache( - prov.meta().id, - umo=message.unified_msg_origin, - ) - message.set_result(MessageEventResult().message("切换 Key 成功。")) - except Exception as e: - message.set_result( - MessageEventResult().message( - safe_error("切换 Key 未知错误: ", e) - ), - ) - return diff --git a/astrbot/builtin_stars/builtin_commands/commands/sid.py b/astrbot/builtin_stars/builtin_commands/commands/sid.py index e8bdbffb1..7be4aca54 100644 --- a/astrbot/builtin_stars/builtin_commands/commands/sid.py +++ b/astrbot/builtin_stars/builtin_commands/commands/sid.py @@ -18,19 +18,19 @@ class SIDCommand: umo_msg_type = event.session.message_type.value umo_session_id = event.session.session_id ret = ( - f"UMO: 「{sid}」 此值可用于设置白名单。\n" - f"UID: 「{user_id}」 此值可用于设置管理员。\n" - f"消息会话来源信息:\n" - f" 机器人 ID: 「{umo_platform}」\n" - f" 消息类型: 「{umo_msg_type}」\n" - f" 会话 ID: 「{umo_session_id}」\n" - f"消息来源可用于配置机器人的配置文件路由。" + f"UMO: 「{sid}」\n" + f"UID: 「{user_id}」\n" + "*Use UMO to set whitelist and configure routing, use UID to set admin list(UMO 可用于设置白名单和配置文件路由,UID 可用于设置管理员列表)\n\n" + f"Your session information:\n" + f"Bot ID: 「{umo_platform}」\n" + f"Message Type: 「{umo_msg_type}」\n" + f"Session ID: 「{umo_session_id}」\n\n" ) if ( self.context.get_config()["platform_settings"]["unique_session"] and event.get_group_id() ): - ret += f"\n\n当前处于独立会话模式, 此群 ID: 「{event.get_group_id()}」, 也可将此 ID 加入白名单来放行整个群聊。" + ret += f"\n\nThe group's ID: 「{event.get_group_id()}」. Set this ID to whitelist to allow the entire group." event.set_result(MessageEventResult().message(ret).use_t2i(False)) diff --git a/astrbot/builtin_stars/builtin_commands/commands/t2i.py b/astrbot/builtin_stars/builtin_commands/commands/t2i.py deleted file mode 100644 index 78d6b0df7..000000000 --- a/astrbot/builtin_stars/builtin_commands/commands/t2i.py +++ /dev/null @@ -1,23 +0,0 @@ -"""文本转图片命令""" - -from astrbot.api import star -from astrbot.api.event import AstrMessageEvent, MessageEventResult - - -class T2ICommand: - """文本转图片命令类""" - - def __init__(self, context: star.Context) -> None: - self.context = context - - async def t2i(self, event: AstrMessageEvent) -> None: - """开关文本转图片""" - config = self.context.get_config(umo=event.unified_msg_origin) - if config["t2i"]: - config["t2i"] = False - config.save_config() - event.set_result(MessageEventResult().message("已关闭文本转图片模式。")) - return - config["t2i"] = True - config.save_config() - event.set_result(MessageEventResult().message("已开启文本转图片模式。")) diff --git a/astrbot/builtin_stars/builtin_commands/commands/tts.py b/astrbot/builtin_stars/builtin_commands/commands/tts.py deleted file mode 100644 index 13049ac22..000000000 --- a/astrbot/builtin_stars/builtin_commands/commands/tts.py +++ /dev/null @@ -1,36 +0,0 @@ -"""文本转语音命令""" - -from astrbot.api import star -from astrbot.api.event import AstrMessageEvent, MessageEventResult -from astrbot.core.star.session_llm_manager import SessionServiceManager - - -class TTSCommand: - """文本转语音命令类""" - - def __init__(self, context: star.Context) -> None: - self.context = context - - async def tts(self, event: AstrMessageEvent) -> None: - """开关文本转语音(会话级别)""" - umo = event.unified_msg_origin - ses_tts = await SessionServiceManager.is_tts_enabled_for_session(umo) - cfg = self.context.get_config(umo=umo) - tts_enable = cfg["provider_tts_settings"]["enable"] - - # 切换状态 - new_status = not ses_tts - await SessionServiceManager.set_tts_status_for_session(umo, new_status) - - status_text = "已开启" if new_status else "已关闭" - - if new_status and not tts_enable: - event.set_result( - MessageEventResult().message( - f"{status_text}当前会话的文本转语音。但 TTS 功能在配置中未启用,请前往 WebUI 开启。", - ), - ) - else: - event.set_result( - MessageEventResult().message(f"{status_text}当前会话的文本转语音。"), - ) diff --git a/astrbot/builtin_stars/builtin_commands/main.py b/astrbot/builtin_stars/builtin_commands/main.py index fb4a83403..8e34f582d 100644 --- a/astrbot/builtin_stars/builtin_commands/main.py +++ b/astrbot/builtin_stars/builtin_commands/main.py @@ -3,17 +3,10 @@ from astrbot.api.event import AstrMessageEvent, filter from .commands import ( AdminCommands, - AlterCmdCommands, ConversationCommands, HelpCommand, - LLMCommands, - PersonaCommands, - PluginCommands, - ProviderCommands, SetUnsetCommands, SIDCommand, - T2ICommand, - TTSCommand, ) @@ -21,198 +14,49 @@ class Main(star.Star): def __init__(self, context: star.Context) -> None: self.context = context - self.help_c = HelpCommand(self.context) - self.llm_c = LLMCommands(self.context) - self.plugin_c = PluginCommands(self.context) self.admin_c = AdminCommands(self.context) self.conversation_c = ConversationCommands(self.context) - self.provider_c = ProviderCommands(self.context) - self.persona_c = PersonaCommands(self.context) - self.alter_cmd_c = AlterCmdCommands(self.context) + self.help_c = HelpCommand(self.context) self.setunset_c = SetUnsetCommands(self.context) - self.t2i_c = T2ICommand(self.context) - self.tts_c = TTSCommand(self.context) self.sid_c = SIDCommand(self.context) @filter.command("help") async def help(self, event: AstrMessageEvent) -> None: - """查看帮助""" + """Show help message""" await self.help_c.help(event) - @filter.permission_type(filter.PermissionType.ADMIN) - @filter.command("llm") - async def llm(self, event: AstrMessageEvent) -> None: - """开启/关闭 LLM""" - await self.llm_c.llm(event) - - @filter.command_group("plugin") - def plugin(self) -> None: - """插件管理""" - - @plugin.command("ls") - async def plugin_ls(self, event: AstrMessageEvent) -> None: - """获取已经安装的插件列表。""" - await self.plugin_c.plugin_ls(event) - - @filter.permission_type(filter.PermissionType.ADMIN) - @plugin.command("off") - async def plugin_off(self, event: AstrMessageEvent, plugin_name: str = "") -> None: - """禁用插件""" - await self.plugin_c.plugin_off(event, plugin_name) - - @filter.permission_type(filter.PermissionType.ADMIN) - @plugin.command("on") - async def plugin_on(self, event: AstrMessageEvent, plugin_name: str = "") -> None: - """启用插件""" - await self.plugin_c.plugin_on(event, plugin_name) - - @filter.permission_type(filter.PermissionType.ADMIN) - @plugin.command("get") - async def plugin_get(self, event: AstrMessageEvent, plugin_repo: str = "") -> None: - """安装插件""" - await self.plugin_c.plugin_get(event, plugin_repo) - - @plugin.command("help") - async def plugin_help(self, event: AstrMessageEvent, plugin_name: str = "") -> None: - """获取插件帮助""" - await self.plugin_c.plugin_help(event, plugin_name) - - @filter.command("t2i") - async def t2i(self, event: AstrMessageEvent) -> None: - """开关文本转图片""" - await self.t2i_c.t2i(event) - - @filter.command("tts") - async def tts(self, event: AstrMessageEvent) -> None: - """开关文本转语音(会话级别)""" - await self.tts_c.tts(event) - @filter.command("sid") async def sid(self, event: AstrMessageEvent) -> None: - """获取会话 ID 和 管理员 ID""" + """Get session ID and other related information""" await self.sid_c.sid(event) - @filter.permission_type(filter.PermissionType.ADMIN) - @filter.command("op") - async def op(self, event: AstrMessageEvent, admin_id: str = "") -> None: - """授权管理员。op """ - await self.admin_c.op(event, admin_id) - - @filter.permission_type(filter.PermissionType.ADMIN) - @filter.command("deop") - async def deop(self, event: AstrMessageEvent, admin_id: str) -> None: - """取消授权管理员。deop """ - await self.admin_c.deop(event, admin_id) - - @filter.permission_type(filter.PermissionType.ADMIN) - @filter.command("wl") - async def wl(self, event: AstrMessageEvent, sid: str = "") -> None: - """添加白名单。wl """ - await self.admin_c.wl(event, sid) - - @filter.permission_type(filter.PermissionType.ADMIN) - @filter.command("dwl") - async def dwl(self, event: AstrMessageEvent, sid: str) -> None: - """删除白名单。dwl """ - await self.admin_c.dwl(event, sid) - - @filter.permission_type(filter.PermissionType.ADMIN) - @filter.command("provider") - async def provider( - self, - event: AstrMessageEvent, - idx: str | int | None = None, - idx2: int | None = None, - ) -> None: - """查看或者切换 LLM Provider""" - await self.provider_c.provider(event, idx, idx2) - @filter.command("reset") async def reset(self, message: AstrMessageEvent) -> None: - """重置 LLM 会话""" + """Reset conversation history""" await self.conversation_c.reset(message) @filter.command("stop") async def stop(self, message: AstrMessageEvent) -> None: - """停止当前会话中正在运行的 Agent""" + """Stop agent execution""" await self.conversation_c.stop(message) - @filter.permission_type(filter.PermissionType.ADMIN) - @filter.command("model") - async def model_ls( - self, - message: AstrMessageEvent, - idx_or_name: int | str | None = None, - ) -> None: - """查看或者切换模型""" - await self.provider_c.model_ls(message, idx_or_name) - - @filter.command("history") - async def his(self, message: AstrMessageEvent, page: int = 1) -> None: - """查看对话记录""" - await self.conversation_c.his(message, page) - - @filter.command("ls") - async def convs(self, message: AstrMessageEvent, page: int = 1) -> None: - """查看对话列表""" - await self.conversation_c.convs(message, page) - @filter.command("new") async def new_conv(self, message: AstrMessageEvent) -> None: - """创建新对话""" + """Create new conversation""" await self.conversation_c.new_conv(message) - @filter.permission_type(filter.PermissionType.ADMIN) - @filter.command("groupnew") - async def groupnew_conv(self, message: AstrMessageEvent, sid: str) -> None: - """创建新群聊对话""" - await self.conversation_c.groupnew_conv(message, sid) - - @filter.command("switch") - async def switch_conv( - self, message: AstrMessageEvent, index: int | None = None - ) -> None: - """通过 /ls 前面的序号切换对话""" - await self.conversation_c.switch_conv(message, index) - - @filter.command("rename") - async def rename_conv(self, message: AstrMessageEvent, new_name: str) -> None: - """重命名对话""" - await self.conversation_c.rename_conv(message, new_name) - - @filter.command("del") - async def del_conv(self, message: AstrMessageEvent) -> None: - """删除当前对话""" - await self.conversation_c.del_conv(message) - - @filter.permission_type(filter.PermissionType.ADMIN) - @filter.command("key") - async def key(self, message: AstrMessageEvent, index: int | None = None) -> None: - """查看或者切换 Key""" - await self.provider_c.key(message, index) - - @filter.permission_type(filter.PermissionType.ADMIN) - @filter.command("persona") - async def persona(self, message: AstrMessageEvent) -> None: - """查看或者切换 Persona""" - await self.persona_c.persona(message) - @filter.permission_type(filter.PermissionType.ADMIN) @filter.command("dashboard_update") async def update_dashboard(self, event: AstrMessageEvent) -> None: - """更新管理面板""" + """Update AstrBot WebUI""" await self.admin_c.update_dashboard(event) @filter.command("set") async def set_variable(self, event: AstrMessageEvent, key: str, value: str) -> None: + """Set session variable""" await self.setunset_c.set_variable(event, key, value) @filter.command("unset") async def unset_variable(self, event: AstrMessageEvent, key: str) -> None: + """Unset session variable""" await self.setunset_c.unset_variable(event, key) - - @filter.permission_type(filter.PermissionType.ADMIN) - @filter.command("alter_cmd", alias={"alter"}) - async def alter_cmd(self, event: AstrMessageEvent) -> None: - """修改命令权限""" - await self.alter_cmd_c.alter_cmd(event) diff --git a/docs/en/use/command.md b/docs/en/use/command.md index 57896b2c9..74c9e0cf6 100644 --- a/docs/en/use/command.md +++ b/docs/en/use/command.md @@ -1,5 +1,110 @@ # Built-in Commands -AstrBot has many built-in commands that are imported as plugins. They are located in the `packages/astrbot` directory. +AstrBot commands are registered through the plugin system. To keep the core lightweight, only a small set of basic commands are loaded with AstrBot itself. Other management and extended commands have been moved into a separate plugin. -Use `/help` to view all built-in commands. +Use `/help` to view currently enabled commands. + +> [!NOTE] +> 1. `/help`, `/set`, and `/unset` are not shown in the `/help` command list by default, but they are still available. +> 2. If you change the wake prefix and remove the default `/`, commands must use the new wake prefix as well. For example, after changing the wake prefix to `!`, use `!help` and `!reset` instead of `/help` and `/reset`. + +## Core Built-in Commands + +The following commands are shipped with AstrBot and loaded by default: + +- `/help`: View currently enabled commands and AstrBot version information. +- `/sid`: View current message source information, including UMO, user ID, platform ID, message type, and session ID. This is commonly used when configuring admins, allowlists, or routing rules. +- `/reset`: Reset the current conversation's LLM context. +- `/stop`: Stop Agent tasks currently running in the current session. +- `/new`: Create and switch to a new conversation. +- `/dashboard_update`: Update AstrBot WebUI. This command requires admin permission. +- `/set`: Set a session variable, commonly used for Agent Runner input variables such as Dify, Coze, or DashScope. +- `/unset`: Remove a session variable. + +These commands are located in: + +```text +astrbot/builtin_stars/builtin_commands +``` + +## Core Command Details + +### `/sid` + +`/sid` shows information about the current message source. It mainly returns: + +- `UMO`: The unified message origin of the current message. It is commonly used for allowlists and per-session config routing. +- `UID`: The sender's user ID. It is commonly used when adding AstrBot admins. +- `Bot ID`: The platform instance ID of the current bot. +- `Message Type`: The message type, such as private chat or group chat. +- `Session ID`: The platform-side session ID. + +In group chats, if `unique_session` is enabled, `/sid` also shows the current group ID. This group ID can be used to allowlist the entire group. + +Common uses: + +- Add an admin: run `/sid` to get the `UID`, then add it in WebUI under `Config -> Other Config -> Admin ID`. +- Configure allowlists: use `UMO` or group ID to control which sessions can use the bot. +- Configure routing rules: use `UMO` to distinguish different platforms, groups, or private chats. + +### `/reset` + +`/reset` resets the LLM context of the current session. + +For AstrBot's built-in Agent Runner, it: + +- Stops running tasks in the current session. +- Clears the context messages of the current conversation. +- Notifies long-term memory to clear the current session state. + +For third-party Agent Runners such as `dify`, `coze`, `dashscope`, and `deerflow`, it: + +- Stops running tasks in the current session. +- Removes the saved third-party conversation ID for this session, so the next turn starts a new conversation. + +Permission notes: + +- In private chat, regular users can use it by default. +- In group chat with `unique_session` enabled, regular users can use it by default. +- In group chat without `unique_session`, admin permission is required by default. +- If command permission settings have been customized, the actual configuration takes precedence. + +### `/stop` + +`/stop` stops Agent tasks currently running in the current session. + +It does not clear conversation history and does not create a new conversation. It only sends a stop request to tasks currently executing in this session. + +For the built-in Agent Runner, `/stop` asks the Agent Runner to stop the current task. +For third-party Agent Runners such as `dify`, `coze`, `dashscope`, and `deerflow`, `/stop` directly stops registered running tasks in the current session. + +If there are no running tasks in the current session, AstrBot will report that no task is running. + +## Built-in Commands Extension + +Other commands that were previously shipped with the core have been moved to a separate plugin: + +- [builtin_commands_extension](https://github.com/AstrBotDevs/builtin_commands_extension) + +This plugin provides extended commands for plugin management, Provider management, model switching, Persona management, and conversation management. Examples include: + +- `/plugin`: View, enable, disable, or install plugins. +- `/op`, `/deop`: Add or remove admins. +- `/provider`: View or switch LLM Providers. +- `/model`: View or switch models. +- `/history`: View current conversation history. +- `/ls`: View the conversation list. +- `/groupnew`: Create a new conversation for a specified group. +- `/switch`: Switch to a specified conversation. +- `/rename`: Rename the current conversation. +- `/del`: Delete the current conversation. +- `/persona`: View or switch Persona. +- `/llm`: Enable or disable LLM chat. + +Install or enable the `builtin_commands_extension` plugin if you need these extended commands. + +## Permission Notes + +Some commands require AstrBot admin permission, such as `/dashboard_update`, `/op`, `/deop`, `/provider`, `/model`, and `/persona`. + +You can use `/sid` to get a user ID, then add it in WebUI under `Config -> Other Config -> Admin ID`. diff --git a/docs/zh/use/command.md b/docs/zh/use/command.md index 067d37e21..410b96e65 100644 --- a/docs/zh/use/command.md +++ b/docs/zh/use/command.md @@ -1,5 +1,106 @@ # 内置指令 -AstrBot 具有很多内置指令,它们通过插件的形式被导入。位于 `packages/astrbot` 目录下。 +AstrBot 的指令通过插件机制注册。为了保持主程序轻量,当前只有少量基础指令随 AstrBot 主程序内置加载;更多管理类、扩展类指令已经迁移到独立插件中维护。 -使用 `/help` 可以查看所有内置指令。 \ No newline at end of file +使用 `/help` 可以查看当前已经启用的指令。 + +> [!NOTE] +> 1. `/help`、`/set`、`/unset` 默认不会显示在 `/help` 输出的指令清单中,但这些指令仍然可用。 +> 2. 如果您修改了唤醒前缀,去掉了默认的 `/`,那么指令也需要使用新的唤醒前缀触发。例如将唤醒前缀改为 `!` 后,应使用 `!help`、`!reset`,而不是 `/help`、`/reset`。 + +## 主程序内置指令 + +以下指令由 AstrBot 主程序自带,默认随 AstrBot 加载: + +- `/help`:查看当前启用的指令和 AstrBot 版本信息。 +- `/sid`:查看当前消息来源信息,包括 UMO、用户 ID、平台 ID、消息类型和会话 ID。常用于配置管理员、白名单或路由规则。 +- `/reset`:重置当前会话的 LLM 上下文。 +- `/stop`:停止当前会话中正在运行的 Agent 任务。 +- `/new`:创建并切换到一个新对话。 +- `/dashboard_update`:更新 AstrBot WebUI。该指令需要管理员权限。 +- `/set`:设置当前会话变量,常用于 Dify、Coze、DashScope 等 Agent 执行器的输入变量。 +- `/unset`:移除当前会话变量。 + +## 核心指令详解 + +### `/sid` + +`/sid` 用于查看当前消息来源信息,主要输出: + +- `UMO`:当前消息来源的统一标识。它通常用于白名单、配置文件路由等按会话生效的配置。 +- `UID`:当前发送者的用户 ID。它通常用于添加 AstrBot 管理员。 +- `Bot ID`:当前机器人所在平台实例的 ID。 +- `Message Type`:消息类型,例如私聊或群聊。 +- `Session ID`:平台侧会话 ID。 + +在群聊中,如果开启了 `unique_session`(会话隔离),`/sid` 还会额外提示当前群 ID。这个群 ID 可用于把整个群加入白名单。 + +常见用途: + +- 添加管理员:先发送 `/sid` 获取 `UID`,再在 WebUI 的 `配置 -> 其他配置 -> 管理员 ID` 中添加。 +- 配置白名单:使用 `UMO` 或群 ID 控制哪些会话可以使用机器人。 +- 配置路由规则:使用 `UMO` 区分不同平台、群聊或私聊来源。 + +### `/reset` + +`/reset` 用于重置当前会话的 LLM 上下文。 + +对于 AstrBot 内置 Agent Runner,它会: + +- 停止当前会话中正在运行的任务。 +- 清空当前对话的上下文消息。 +- 通知长期记忆会话清理当前上下文状态。 + +对于第三方 Agent Runner,例如 `dify`、`coze`、`dashscope`、`deerflow`,它会: + +- 停止当前会话中正在运行的任务。 +- 删除当前会话保存的第三方会话 ID,让下一轮对话重新开始。 + +权限说明: + +- 私聊中默认普通用户可使用。 +- 群聊开启会话隔离时,默认普通用户可使用。 +- 群聊未开启会话隔离时,默认需要管理员权限。 +- 如果管理员修改过指令权限配置,则以实际配置为准。 + +### `/stop` + +`/stop` 用于停止当前会话中正在运行的 Agent 任务。 + +它不会清空对话历史,也不会创建新对话。它只对当前会话正在执行的任务发出停止请求。 + +对于内置 Agent Runner,`/stop` 会请求 Agent Runner 停止当前任务。 +对于第三方 Agent Runner,例如 `dify`、`coze`、`dashscope`、`deerflow`,`/stop` 会直接停止当前会话中登记的运行任务。 + +如果当前会话没有正在运行的任务,AstrBot 会提示当前会话没有运行中的任务。 + +## 内置指令扩展 + +除上述基础指令外,其他原本随主程序提供的内置指令已经迁移到独立插件: + +- [builtin_commands_extension](https://github.com/AstrBotDevs/builtin_commands_extension) + +可直接在插件市场搜索安装。 + +该插件提供插件管理、Provider 管理、模型切换、Persona 管理、对话列表管理等扩展指令,例如: + +- `/plugin`:查看、启用、停用或安装插件。 +- `/op`、`/deop`:添加或移除管理员。 +- `/provider`:查看或切换 LLM Provider。 +- `/model`:查看或切换模型。 +- `/history`:查看当前对话历史。 +- `/ls`:查看对话列表。 +- `/groupnew`:为指定群聊创建新对话。 +- `/switch`:切换到指定对话。 +- `/rename`:重命名当前对话。 +- `/del`:删除当前对话。 +- `/persona`:查看或切换 Persona。 +- `/llm`:开启或关闭 LLM 聊天功能。 + +如果你需要这些扩展指令,请安装或启用 `builtin_commands_extension` 插件。 + +## 权限说明 + +部分指令需要 AstrBot 管理员权限,例如 `/dashboard_update`、`/op`、`/deop`、`/provider`、`/model`、`/persona` 等。 + +可以通过 `/sid` 获取用户 ID,然后在 WebUI 的 `配置 -> 其他配置 -> 管理员 ID` 中添加管理员。