From a799a53171c882ec633ad396f6be04a01281b4bc Mon Sep 17 00:00:00 2001 From: Gogs Date: Mon, 26 Jan 2026 19:19:03 +0800 Subject: [PATCH] =?UTF-8?q?LLM=E5=8A=9F=E8=83=BD=E5=A2=9E=E5=BC=BA?= =?UTF-8?q?=EF=BC=8C=E6=96=B0=E5=A2=9E=E5=85=B3=E9=94=AE=E8=AF=8D=E6=8C=87?= =?UTF-8?q?=E4=BB=A4=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1.新增关键词指令功能 2.新增LLM功能 3.优化角色代码逻辑与性能 4.为部分菜单功能也加入toast,直观显示结果。 关键词指令:当语音输入内容完全匹配关键词时触发对应功能,可以在config.py中修改或禁用。 新增LLM功能如下:清除历史、显示最近对话、撤回最近一轮对话、复制全部上下文、复制当前角色上下文。便于使用角色功能。 功能支持关键词触发和客户端托盘菜单触发,并为原有的复制结果和清除历史功能增加了关键词指令支持。 角色功能支持缓存和长度优先级匹配。 其他修改:关键词指令带有标记,不覆盖上次结果,便于复制。 requirement增加'tbb',hiddenimports增加 'rich._unicode_data','rich._unicode_data.unicode17-0-0',解决本地打包时遇到的报错。 --- build-client.spec | 2 + build.spec | 2 + config_client.py | 24 ++++ requirements-server.txt | 5 +- util/client/startup.py | 45 ++++++ util/llm/llm_handler.py | 264 +++++++++++++++++++++++++++++++++- util/llm/llm_role_detector.py | 180 +++++++++++++++++++++-- 7 files changed, 505 insertions(+), 17 deletions(-) diff --git a/build-client.spec b/build-client.spec index 87d9a813..80f5c5cd 100644 --- a/build-client.spec +++ b/build-client.spec @@ -64,6 +64,8 @@ hiddenimports += [ 'rich', 'rich.console', 'rich.markdown', + 'rich._unicode_data', + 'rich._unicode_data.unicode17-0-0', 'keyboard', 'pyclip', 'numpy', diff --git a/build.spec b/build.spec index a37baa9a..87195bce 100644 --- a/build.spec +++ b/build.spec @@ -62,6 +62,8 @@ hiddenimports += [ 'rich', 'rich.console', 'rich.markdown', + 'rich._unicode_data', + 'rich._unicode_data.unicode17-0-0', 'keyboard', 'pyclip', 'numpy', diff --git a/config_client.py b/config_client.py index d4c299e5..88eb192a 100644 --- a/config_client.py +++ b/config_client.py @@ -32,6 +32,30 @@ class ClientConfig: }, ] + # LLM 清除历史指令关键词(完全匹配) + # 支持一个或多个关键词:clear_history_keywords = ['清除历史', '清除记忆', '清除上下文'] ,留空则关闭此功能:clear_history_keywords = [] + clear_history_keywords = ['清除历史', '清除记忆', '清除上下文'] + + # LLM 撤回上一轮对话的关键词(完全匹配) + # 支持一个或多个关键词:revoke_last_turn_keywords = ['撤回', '撤销', '回退'] ,留空则关闭此功能:revoke_last_turn_keywords = [] + revoke_last_turn_keywords = ['撤回最近对话', '撤销最近对话', '回退最近对话'] + + # LLM 显示上一轮对话的关键词(完全匹配) + # 支持一个或多个关键词:show_last_turn_keywords = ['显示', '查看', '最近'] ,留空则关闭此功能:show_last_turn_keywords = [] + show_last_turn_keywords = ['显示最近对话', '查看最近对话'] + + # LLM 复制全部上下文的关键词(完全匹配) + # 支持一个或多个关键词:copy_all_context_keywords = ['复制', '拷贝'] ,留空则关闭此功能:copy_all_context_keywords = [] + copy_all_context_keywords = ['复制全部上下文', '拷贝全部上下文'] + + # LLM 复制当前角色上下文的关键词(完全匹配) + # 支持一个或多个关键词:copy_current_role_context_keywords = ['复制当前角色上下文', '拷贝当前角色上下文'] ,留空则关闭此功能:copy_current_role_context_keywords = [] + copy_current_role_context_keywords = ['复制当前角色上下文', '拷贝当前角色上下文'] + + # LLM 复制结果的关键词(完全匹配) + # 支持一个或多个关键词:copy_result_keywords = ['复制结果', '复制输出'] ,留空则关闭此功能:copy_result_keywords = [] + copy_result_keywords = ['复制最近结果', '复制输出'] + threshold = 0.3 # 快捷键触发阈值(秒) paste = False # 是否以写入剪切板然后模拟 Ctrl-V 粘贴的方式输出结果 diff --git a/requirements-server.txt b/requirements-server.txt index 746560e5..790ec617 100644 --- a/requirements-server.txt +++ b/requirements-server.txt @@ -15,4 +15,7 @@ pystray Pillow markdown -tkhtmlview \ No newline at end of file +tkhtmlview + +# 补充缺失报错 +tbb \ No newline at end of file diff --git a/util/client/startup.py b/util/client/startup.py index 80f78d27..d203799c 100644 --- a/util/client/startup.py +++ b/util/client/startup.py @@ -37,6 +37,42 @@ def clear_memory(): from util.client.ui import toast toast("清除成功:已清除所有角色的对话历史记录", duration=3000, bg="#075077") + def revoke_last_turn(): + from util.llm.llm_handler import revoke_last_turn + from util.client.ui import toast + success, message = revoke_last_turn() + if success: + toast(message, duration=3000, bg="#075077") + else: + toast(message, duration=3000, bg="#d9534f") + + def show_last_turn(): + from util.llm.llm_handler import show_last_turn + from util.client.ui import toast + success, message = show_last_turn() + if success: + toast(message, duration=5000, bg="#075077") + else: + toast(message, duration=3000, bg="#d9534f") + + def copy_all_context(): + from util.llm.llm_handler import copy_all_context + from util.client.ui import toast + success, message = copy_all_context() + if success: + toast(message, duration=3000, bg="#075077") + else: + toast(message, duration=3000, bg="#d9534f") + + def copy_current_role_context(): + from util.llm.llm_handler import copy_current_role_context + from util.client.ui import toast + success, message = copy_current_role_context() + if success: + toast(message, duration=3000, bg="#075077") + else: + toast(message, duration=3000, bg="#d9534f") + def add_hotword(): try: from util.client.ui import on_add_hotword @@ -63,6 +99,11 @@ def copy_last_result(): if text: from util.llm.llm_clipboard import copy_to_clipboard copy_to_clipboard(text) + from util.client.ui import toast + toast("复制成功:已复制结果到剪贴板", duration=3000, bg="#075077") + else: + from util.client.ui import toast + toast("复制失败:没有可复制的内容", duration=3000, bg="#d9534f") import os icon_path = os.path.join(base_dir, 'assets', 'icon.ico') @@ -76,6 +117,10 @@ def copy_last_result(): ('✨ 添加热词', add_hotword), ('🛠️ 添加纠错', add_rectify), ('🧹 清除记忆', clear_memory), + ('↩️ 撤回上一轮', revoke_last_turn), + ('💬 显示最近对话', show_last_turn), + ('📄 复制所有上下文', copy_all_context), + ('📑 复制当前角色上下文', copy_current_role_context), ('🔄 重启音频', restart_audio), ] ) diff --git a/util/llm/llm_handler.py b/util/llm/llm_handler.py index 4e10668f..2181b26f 100644 --- a/util/llm/llm_handler.py +++ b/util/llm/llm_handler.py @@ -57,6 +57,9 @@ def __init__(self, hotwords_file: str = 'hot.txt'): # LLM 处理引擎 self.processor = LLMProcessor(self.client_pool) + # 最近使用的角色(用于撤回/显示功能) + self.last_used_role: Optional[str] = None + def _init_context_managers(self): """为启用了历史的角色创建上下文管理器""" for role_name, role_config in self.roles.items(): @@ -128,6 +131,9 @@ def process(self, role_config: RoleConfig, content: str, matched_hotwords=None, role_name = role_config.name or RoleConfig.DEFAULT_ROLE_NAME logger.debug(f"开始 LLM 核心处理 [角色: {role_name}] [内容长度: {len(content)}]") + # 更新最近使用的角色 + self.last_used_role = role_name + # 获取上下文管理器(如果启用历史) context_manager = self.context_managers.get(role_name) if role_config.enable_history else None if context_manager: @@ -179,7 +185,23 @@ async def process_and_output(self, text: str, return_result: bool = False, paste # 2. 如果不匹配任何需要处理的角色(或者默认角色被禁用) if not role_config: - result_text = TextOutput.strip_punc(text) + # 如果是指令标记,不更新 last_output_text + from util.llm.llm_role_detector import COMMAND_TOKEN + if content == COMMAND_TOKEN: + # 指令已执行,直接返回,不更新输出文本 + if return_result: + return LLMResult( + result=content, + role_name=None, + processed=False, + token_count=0, + polish_time=0, + input_text=text, + generation_time=0 + ) + return None + + result_text = TextOutput.strip_punc(content) await output_text(result_text, paste) # 更新全局状态并 UDP 广播 @@ -231,6 +253,222 @@ async def process_and_output(self, text: str, return_result: bool = False, paste ) return None + def revoke_last_turn(self) -> Tuple[bool, str]: + """ + 撤回最近一次使用的角色的最近一轮对话消息 + + 撤回规则: + - 如果最后两条是 [user, assistant],删除两者 + - 如果只剩一条助手消息,删除助手消息 + + Returns: + (success, message) - success 表示是否成功,message 是反馈信息 + """ + if not self.last_used_role: + return False, "没有最近使用的角色记录" + + context_manager = self.context_managers.get(self.last_used_role) + if not context_manager: + return False, f"角色 '{self.last_used_role}' 未启用历史记录" + + with context_manager._lock: + if not context_manager.history: + return False, f"角色 '{self.last_used_role}' 没有历史记录可撤回" + + # 撤回最后两条消息(user + assistant) + if len(context_manager.history) >= 2: + context_manager.history.pop() # 先删除 assistant + context_manager.history.pop() # 再删除 user + logger.info(f"已撤回角色 '{self.last_used_role}' 的最近一轮对话") + return True, f"已撤回角色 '{self.last_used_role}' 的最近一轮对话" + + # 只剩一条助手消息,删除助手消息 + context_manager.history.pop() + logger.info(f"已撤回角色 '{self.last_used_role}' 的最后一条助手消息") + return True, f"已撤回角色 '{self.last_used_role}' 的最后一条助手消息" + + def show_last_turn(self) -> Tuple[bool, str]: + """ + 显示最近一次使用的角色的最近一轮对话消息 + + 显示规则: + - 如果最后两条是 [user, assistant],显示完整对话 + - 如果只剩一条助手消息,显示助手消息 + + Returns: + (success, message) - success 表示是否成功,message 是反馈信息 + """ + if not self.last_used_role: + return False, "没有最近使用的角色记录" + + context_manager = self.context_managers.get(self.last_used_role) + if not context_manager: + return False, f"角色 '{self.last_used_role}' 未启用历史记录" + + with context_manager._lock: + if not context_manager.history: + return False, f"角色 '{self.last_used_role}' 没有历史记录可显示" + + # 显示最后两条消息(user + assistant) + if len(context_manager.history) >= 2: + last_two = context_manager.history[-2:] + user_msg = last_two[0]['content'] + assistant_msg = last_two[1]['content'] + return True, ( + f"角色 '{self.last_used_role}' 的最近一轮对话:\n" + f"{user_msg}\n" + f"助手输出:{assistant_msg}" + ) + + # 只剩一条助手消息,显示助手消息 + last_msg = context_manager.history[-1] + return True, f"角色 '{self.last_used_role}' 的最后一条助手消息:\n{last_msg['role']}:{last_msg['content']}" + + def copy_all_context(self) -> Tuple[bool, str]: + """ + 复制所有启用历史的角色的所有历史到剪贴板 + + 格式规则: + - 角色和内容之间增加换行 + - 倒数每两条(一条user,一条assistant)为一组 + - 每组之间额外空一行(两个换行) + - 奇数组时,最后剩下的一条单独一组 + + Returns: + (success, message) - success 表示是否成功,message 是反馈信息 + """ + full_text_parts = [] + total_messages = 0 + + # 按角色顺序遍历 + for role_name in self.roles.keys(): + context_manager = self.context_managers.get(role_name) + if not context_manager or not context_manager.history: + continue + + # 收集该角色的消息列表 + messages = [] + for msg in context_manager.history: + messages.append({ + 'role': msg['role'], + 'content': msg['content'] + }) + total_messages += 1 + + # 对每个角色的消息进行分组(user+assistant为一组) + role_groups = [] + i = 0 + while i < len(messages): + if i + 1 < len(messages): + # 取两条(user+assistant)为一组 + pair = messages[i:i+2] + # 去掉 user 消息中的 "用户输入:" 前缀 + user_content = pair[0]['content'].replace("用户输入:", "") + # 格式化为 user: 内容\nassistant: 内容 + pair_str = f"{pair[0]['role']}:\n{user_content}\n\n{pair[1]['role']}:\n{pair[1]['content']}" + role_groups.append(pair_str) + i += 2 + else: + # 剩下一条单独一组 + single_msg = messages[i] + # 去掉 user 消息中的 "用户输入:" 前缀 + if single_msg['role'] == 'user': + single_content = single_msg['content'].replace("用户输入:", "") + else: + single_content = single_msg['content'] + single_str = f"{single_msg['role']}:\n{single_content}" + role_groups.append(single_str) + i += 1 + + # 构建该角色的完整文本:角色标题 + 各个分组 + if role_groups: + role_text = f"=== 角色: {role_name} ===\n\n" + role_text += "\n\n".join(role_groups) + full_text_parts.append(role_text) + + if not full_text_parts: + return False, "没有可复制的上下文内容" + + full_text = "\n\n".join(full_text_parts) + + try: + from util.client.clipboard import copy_to_clipboard + copy_to_clipboard(full_text) + logger.info(f"已复制 {len(full_text_parts)} 个角色的 {total_messages} 条上下文到剪贴板") + return True, f"已复制 {len(full_text_parts)} 个角色的 {total_messages} 条上下文到剪贴板" + except Exception as e: + logger.error(f"复制到剪贴板失败: {e}") + return False, f"复制失败: {str(e)}" + + def copy_current_role_context(self) -> Tuple[bool, str]: + """ + 复制最近使用的助手的上下文到剪贴板 + + 格式规则与 copy_all_context() 一致: + - 角色和内容之间增加换行 + - 倒数每两条(一条user,一条assistant)为一组 + - 每组之间额外空一行(两个换行) + - 奇数组时,最后剩下的一条单独一组 + + Returns: + (success, message) - success 表示是否成功,message 是反馈信息 + """ + if not self.last_used_role: + return False, "没有最近使用的角色记录" + + context_manager = self.context_managers.get(self.last_used_role) + if not context_manager or not context_manager.history: + return False, f"角色 '{self.last_used_role}' 没有历史记录可复制" + + # 收集该角色的消息列表 + messages = [] + for msg in context_manager.history: + messages.append({ + 'role': msg['role'], + 'content': msg['content'] + }) + + # 对消息进行分组(user+assistant为一组) + groups = [] + i = 0 + while i < len(messages): + if i + 1 < len(messages): + # 取两条(user+assistant)为一组 + pair = messages[i:i+2] + # 去掉 user 消息中的 "用户输入:" 前缀 + user_content = pair[0]['content'].replace("用户输入:", "") + # 格式化为 user: 内容\nassistant: 内容 + pair_str = f"{pair[0]['role']}:\n{user_content}\n\n{pair[1]['role']}:\n{pair[1]['content']}" + groups.append(pair_str) + i += 2 + else: + # 剩下一条单独一组 + single_msg = messages[i] + # 去掉 user 消息中的 "用户输入:" 前缀 + if single_msg['role'] == 'user': + single_content = single_msg['content'].replace("用户输入:", "") + else: + single_content = single_msg['content'] + single_str = f"{single_msg['role']}:\n{single_content}" + groups.append(single_str) + i += 1 + + if not groups: + return False, f"角色 '{self.last_used_role}' 没有可复制的上下文内容" + + # 构建该角色的完整文本:角色标题 + 各个分组 + role_text = f"=== 角色: {self.last_used_role} ===\n\n" + role_text += "\n\n".join(groups) + + try: + from util.client.clipboard import copy_to_clipboard + copy_to_clipboard(role_text) + logger.info(f"已复制角色 '{self.last_used_role}' 的 {len(messages)} 条上下文到剪贴板") + return True, f"已复制角色 '{self.last_used_role}' 的 {len(messages)} 条上下文到剪贴板" + except Exception as e: + logger.error(f"复制到剪贴板失败: {e}") + return False, f"复制失败: {str(e)}" + # ====================================================================== # --- 全局实例 --- @@ -279,6 +517,30 @@ def clear_llm_history(): handler.clear_history() +def revoke_last_turn() -> Tuple[bool, str]: + """撤回最近一次使用的角色的最近一条消息(便捷函数)""" + handler = get_handler() + return handler.revoke_last_turn() + + +def show_last_turn() -> Tuple[bool, str]: + """显示最近一次使用的角色的最近一条消息(便捷函数)""" + handler = get_handler() + return handler.show_last_turn() + + +def copy_all_context() -> Tuple[bool, str]: + """复制所有启用历史的角色的所有历史到剪贴板(便捷函数)""" + handler = get_handler() + return handler.copy_all_context() + + +def copy_current_role_context() -> Tuple[bool, str]: + """复制最近使用的助手的上下文到剪贴板(便捷函数)""" + handler = get_handler() + return handler.copy_current_role_context() + + # ====================================================================== # --- 测试 --- diff --git a/util/llm/llm_role_detector.py b/util/llm/llm_role_detector.py index cfd297cd..aefa6866 100644 --- a/util/llm/llm_role_detector.py +++ b/util/llm/llm_role_detector.py @@ -4,11 +4,20 @@ 功能: 1. 检测文本是否匹配某个角色前缀 2. 返回对应的角色配置和去除前缀后的文本 +3. 检测指令关键词(清除历史、撤回上一轮、显示上一轮、复制结果、复制全部上下文、复制当前角色上下文) """ from typing import Tuple, Optional from util.llm.llm_role_config import RoleConfig from util.llm.llm_interfaces import IRoleLoader from . import logger +from config import ClientConfig + +# 指令标记常量 +COMMAND_TOKEN = "__COMMAND__" + +# 常量定义 +PREFIX_STRIP_PUNCTUATION = ':,。?!,.?! ' +ROLE_SORT_KEY = lambda item: len(item[1].name) class RoleDetector: @@ -20,10 +29,33 @@ def __init__(self, role_loader: IRoleLoader): role_loader: 角色加载器实例 """ self.role_loader = role_loader + # 缓存角色列表和排序结果 + self._cached_roles = None + self._cached_roles_sorted = None + self._roles_hash = None + + def _get_sorted_roles(self): + """获取排序后的角色列表(带缓存和变化检测)""" + current_roles = self.role_loader.get_roles() + # 使用角色名称列表的排序字符串作为哈希值(检测角色名称和配置变化) + current_hash = hash(','.join(sorted(current_roles.keys()))) + + # 如果角色列表发生变化,清除缓存 + if self._roles_hash != current_hash: + self._cached_roles = current_roles + self._cached_roles_sorted = sorted( + current_roles.items(), + key=ROLE_SORT_KEY, + reverse=True + ) + self._roles_hash = current_hash + logger.debug("角色列表已更新,已刷新检测缓存") + + return self._cached_roles_sorted def detect(self, text: str) -> Tuple[Optional[RoleConfig], str]: """ - 检测文本是否匹配某个角色前缀 + 检测文本是否匹配某个角色前缀或指令关键词 Args: text: 输入文本 @@ -32,26 +64,143 @@ def detect(self, text: str) -> Tuple[Optional[RoleConfig], str]: (role_config, content) - role_config 是 RoleConfig 对象, content 是去除前缀后的文本 """ - # logger.debug(f"检测角色,输入文本: {text[:50]}...") + # ====================================================================== + # --- 指令处理函数(提前定义,避免重复导入)--- + # ====================================================================== + + # 清除历史指令 + def _execute_clear_history(): + from util.llm.llm_handler import clear_llm_history + clear_llm_history() + logger.info(f"检测到清除指令 '{text}',已清除所有角色的对话历史记录") + toast("清除成功:已清除所有角色的对话历史记录", duration=3000, bg="#075077") - for role_name, role_config in self.role_loader.get_roles().items(): - if role_name == '默认': - continue + # 撤回上一轮对话指令 + def _execute_revoke_last_turn(): + from util.llm.llm_handler import revoke_last_turn + success, message = revoke_last_turn() + if success: + logger.info(f"检测到撤回指令 '{text}',{message}") + toast(message, duration=3000, bg="#075077") + else: + logger.warning(f"检测到撤回指令 '{text}',{message}") + toast(message, duration=3000, bg="#d9534f") - # 检查是否启用前缀匹配 - if not role_config.match: - continue + # 显示最近对话指令 + def _execute_show_last_turn(): + from util.llm.llm_handler import show_last_turn + success, message = show_last_turn() + if success: + logger.info(f"检测到显示指令 '{text}',{message}") + toast(message, duration=5000, bg="#075077") + else: + logger.warning(f"检测到显示指令 '{text}',{message}") + toast(message, duration=3000, bg="#d9534f") + + # 复制结果指令 + def _execute_copy_result(): + from util.client.state import get_state + from util.llm.llm_clipboard import copy_to_clipboard + state = get_state() + copy_text = state.last_output_text # 使用新变量名,避免覆盖外层 text + if copy_text: + copy_to_clipboard(copy_text) + logger.info(f"检测到复制结果指令 '{text}',已复制到剪贴板") + toast("复制成功:已复制结果到剪贴板", duration=3000, bg="#075077") + else: + logger.warning(f"检测到复制结果指令 '{text}',但没有可复制的内容") + toast("复制失败:没有可复制的内容", duration=3000, bg="#d9534f") + + # 复制全部上下文指令 + def _execute_copy_all_context(): + from util.llm.llm_handler import copy_all_context + success, message = copy_all_context() + if success: + logger.info(f"检测到复制全部上下文指令 '{text}',{message}") + toast(message, duration=3000, bg="#075077") + else: + logger.warning(f"检测到复制全部上下文指令 '{text}',{message}") + toast(message, duration=3000, bg="#d9534f") + + # 复制当前角色上下文指令 + def _execute_copy_current_role_context(): + from util.llm.llm_handler import copy_current_role_context + success, message = copy_current_role_context() + if success: + logger.info(f"检测到复制当前角色上下文指令 '{text}',{message}") + toast(message, duration=3000, bg="#075077") + else: + logger.warning(f"检测到复制当前角色上下文指令 '{text}',{message}") + toast(message, duration=3000, bg="#d9534f") + + # ====================================================================== + # --- 指令定义(统一管理)--- + # ====================================================================== + + # 将关键词列表转换为集合(只需转换一次) + keyword_sets = { + 'clear_history': set(ClientConfig.clear_history_keywords) if ClientConfig.clear_history_keywords else set(), + 'revoke_last_turn': set(ClientConfig.revoke_last_turn_keywords) if ClientConfig.revoke_last_turn_keywords else set(), + 'show_last_turn': set(ClientConfig.show_last_turn_keywords) if ClientConfig.show_last_turn_keywords else set(), + 'copy_result': set(ClientConfig.copy_result_keywords) if ClientConfig.copy_result_keywords else set(), + 'copy_all_context': set(ClientConfig.copy_all_context_keywords) if ClientConfig.copy_all_context_keywords else set(), + 'copy_current_role_context': set(ClientConfig.copy_current_role_context_keywords) if ClientConfig.copy_current_role_context_keywords else set(), + } + + # 指令元数据 + command_definitions = { + 'clear_history': { + 'keywords': keyword_sets['clear_history'], + 'action': _execute_clear_history, + }, + 'revoke_last_turn': { + 'keywords': keyword_sets['revoke_last_turn'], + 'action': _execute_revoke_last_turn, + }, + 'show_last_turn': { + 'keywords': keyword_sets['show_last_turn'], + 'action': _execute_show_last_turn, + }, + 'copy_result': { + 'keywords': keyword_sets['copy_result'], + 'action': _execute_copy_result, + }, + 'copy_all_context': { + 'keywords': keyword_sets['copy_all_context'], + 'action': _execute_copy_all_context, + }, + 'copy_current_role_context': { + 'keywords': keyword_sets['copy_current_role_context'], + 'action': _execute_copy_current_role_context, + }, + } + + # ====================================================================== + # --- 指令检测(单次查找,无嵌套 if-elif)--- + # ====================================================================== + + # 检测指令关键词(使用集合快速查找) + from util.client.ui import toast # 提前导入 toast + for cmd_name, cmd_def in command_definitions.items(): + if text in cmd_def['keywords']: + cmd_def['action']() + return None, COMMAND_TOKEN # 返回指令标记 + + # 检测角色前缀 + # 获取排序后的角色列表(使用缓存) + roles = self._get_sorted_roles() - name = role_config.name - # 空名字和「默认」都不作为前缀匹配 - if not name or name == '默认': + for role_name, role_config in roles: + # 合并检查:统一处理默认角色和未启用匹配的角色 + if role_name == RoleConfig.DEFAULT_ROLE_NAME or not role_config.match: continue - if name and text.startswith(name): - remaining_text = text[len(name):] - remaining_text = remaining_text.lstrip(':,。,. ') + # 检查前缀匹配 + if text.startswith(role_name): + remaining_text = text[len(role_name):] + remaining_text = remaining_text.lstrip(PREFIX_STRIP_PUNCTUATION).lstrip() - # logger.debug(f"匹配到角色: {name}, 去除前缀后: {remaining_text[:50]}...") + # logger.debug(f"匹配到角色: {role_name}, 去除前缀后: {remaining_text[:50]}...") return role_config, remaining_text # 未匹配,使用默认角色 @@ -60,5 +209,6 @@ def detect(self, text: str) -> Tuple[Optional[RoleConfig], str]: # logger.debug(f"未匹配到角色前缀,使用默认角色,处理: {default_role.process}") return default_role, text + # 未匹配到角色且默认角色不处理,返回原始文本 # logger.debug("未匹配到角色且默认角色不处理,返回原始文本") return None, text