Compare commits

..
4 Commits
Author SHA1 Message Date
3030332422 a21a2e34d0 refactor: 合并 HA 设备控制插件为 hass_state 模块 2026-07-29 09:58:26 +08:00
3030332422 7a22b61969 fix:修复与函数同名时遗漏其他函数的问题 2026-07-28 17:05:18 +08:00
CGDandGitHub 477f85b97c Merge pull request #3304 from MsThink/main
fix(plugins): 展开模块级别插件名为具体函数名
2026-07-28 15:22:58 +08:00
colorful 523915f9fe fix(plugins): 展开模块级别插件名为具体函数名
在一个 function 文件中可能注册多个 @register_function,当配置下发或
本地配置使用模块名(文件名)时,需要自动展开为具体的函数名列表。

改动内容:
- register.py: 新增 module_func_map 全局映射,在 register_function
  装饰器中自动记录模块名 → 函数名列表的映射关系
- connection.py: 处理服务端下发的插件配置时,将模块级的 plugins 配置
  复制到各具体函数名 key 下,并将模块名展开写入 Intent functions 列表
- plugin_executor.py: 新增 _expand_plugin_names() 兜底展开逻辑和
  _get_plugin_description() 双层查找方法,确保本地配置场景也能正确展开
2026-07-24 10:47:00 +08:00
16 changed files with 216 additions and 131 deletions
@@ -0,0 +1,46 @@
-- liquibase formatted sql
-- 插入新的 hass_state 插件记录(合并 get_state 的配置字段)
INSERT INTO ai_model_provider (id, model_type, provider_code, name, fields,
sort, creator, create_date, updater, update_date)
VALUES ('SYSTEM_PLUGIN_HA_STATE',
'Plugin',
'hass_state',
'HomeAssistant设备控制',
JSON_ARRAY(
JSON_OBJECT(
'key', 'base_url',
'type', 'string',
'label', 'HA 服务器地址',
'default',
(SELECT param_value FROM sys_params WHERE param_code = 'plugins.home_assistant.base_url')
),
JSON_OBJECT(
'key', 'api_key',
'type', 'string',
'label', 'HA API 访问令牌',
'default',
(SELECT param_value FROM sys_params WHERE param_code = 'plugins.home_assistant.api_key')
),
JSON_OBJECT(
'key', 'devices',
'type', 'array',
'label', '设备列表(名称,实体ID;…)',
'default',
(SELECT param_value FROM sys_params WHERE param_code = 'plugins.home_assistant.devices')
)
),
50, 0, NOW(), 0, NOW());
-- 迁移已配置的 agent:把指向 HA_GET_STATE 的改成 HA_STATE(保留参数)
UPDATE ai_agent_plugin_mapping
SET plugin_id = 'SYSTEM_PLUGIN_HA_STATE'
WHERE plugin_id = 'SYSTEM_PLUGIN_HA_GET_STATE';
-- HA_SET_STATE 本来就没有配置字段(fields=[]),直接删除其 mapping
DELETE FROM ai_agent_plugin_mapping
WHERE plugin_id = 'SYSTEM_PLUGIN_HA_SET_STATE';
-- 删除旧的插件定义
DELETE FROM ai_model_provider
WHERE id IN ('SYSTEM_PLUGIN_HA_GET_STATE', 'SYSTEM_PLUGIN_HA_SET_STATE');
@@ -711,3 +711,10 @@ databaseChangeLog:
- sqlFile:
encoding: utf8
path: classpath:db/changelog/202607101200.sql
- changeSet:
id: 202607290930
author: cgd
changes:
- sqlFile:
encoding: utf8
path: classpath:db/changelog/202607290930.sql
@@ -497,8 +497,7 @@ const FALLBACK_PLUGIN_NAME_KEYS = {
SYSTEM_PLUGIN_MUSIC: "agentSnapshot.plugin.SYSTEM_PLUGIN_MUSIC",
SYSTEM_PLUGIN_NEWS_CHINANEWS: "agentSnapshot.plugin.SYSTEM_PLUGIN_NEWS_CHINANEWS",
SYSTEM_PLUGIN_NEWS_NEWSNOW: "agentSnapshot.plugin.SYSTEM_PLUGIN_NEWS_NEWSNOW",
SYSTEM_PLUGIN_HA_GET_STATE: "agentSnapshot.plugin.SYSTEM_PLUGIN_HA_GET_STATE",
SYSTEM_PLUGIN_HA_SET_STATE: "agentSnapshot.plugin.SYSTEM_PLUGIN_HA_SET_STATE",
SYSTEM_PLUGIN_HA_STATE: "agentSnapshot.plugin.SYSTEM_PLUGIN_HA_STATE",
SYSTEM_PLUGIN_HA_PLAY_MUSIC: "agentSnapshot.plugin.SYSTEM_PLUGIN_HA_PLAY_MUSIC",
SYSTEM_PLUGIN_WEB_SEARCH: "agentSnapshot.plugin.SYSTEM_PLUGIN_WEB_SEARCH",
SYSTEM_PLUGIN_CALL_DEVICE: "agentSnapshot.plugin.SYSTEM_PLUGIN_CALL_DEVICE"
+1 -2
View File
@@ -903,8 +903,7 @@ export default {
'agentSnapshot.plugin.SYSTEM_PLUGIN_MUSIC': 'Server-Musikplayer',
'agentSnapshot.plugin.SYSTEM_PLUGIN_NEWS_CHINANEWS': 'China News',
'agentSnapshot.plugin.SYSTEM_PLUGIN_NEWS_NEWSNOW': 'NewsNow-Aggregation',
'agentSnapshot.plugin.SYSTEM_PLUGIN_HA_GET_STATE': 'HomeAssistant-Statusabfrage',
'agentSnapshot.plugin.SYSTEM_PLUGIN_HA_SET_STATE': 'HomeAssistant-Statusaktualisierung',
'agentSnapshot.plugin.SYSTEM_PLUGIN_HA_STATE': 'HomeAssistant-Gerätesteuerung',
'agentSnapshot.plugin.SYSTEM_PLUGIN_HA_PLAY_MUSIC': 'HomeAssistant-Musikplayer',
'agentSnapshot.plugin.SYSTEM_PLUGIN_WEB_SEARCH': 'Websuche',
'agentSnapshot.plugin.SYSTEM_PLUGIN_CALL_DEVICE': 'Geräteanruf',
+1 -2
View File
@@ -953,8 +953,7 @@ export default {
'agentSnapshot.plugin.SYSTEM_PLUGIN_MUSIC': 'Server music player',
'agentSnapshot.plugin.SYSTEM_PLUGIN_NEWS_CHINANEWS': 'China News',
'agentSnapshot.plugin.SYSTEM_PLUGIN_NEWS_NEWSNOW': 'NewsNow aggregation',
'agentSnapshot.plugin.SYSTEM_PLUGIN_HA_GET_STATE': 'HomeAssistant state query',
'agentSnapshot.plugin.SYSTEM_PLUGIN_HA_SET_STATE': 'HomeAssistant state update',
'agentSnapshot.plugin.SYSTEM_PLUGIN_HA_STATE': 'HomeAssistant device control',
'agentSnapshot.plugin.SYSTEM_PLUGIN_HA_PLAY_MUSIC': 'HomeAssistant music player',
'agentSnapshot.plugin.SYSTEM_PLUGIN_WEB_SEARCH': 'Web search',
'agentSnapshot.plugin.SYSTEM_PLUGIN_CALL_DEVICE': 'Device-to-device call',
+1 -2
View File
@@ -903,8 +903,7 @@ export default {
'agentSnapshot.plugin.SYSTEM_PLUGIN_MUSIC': 'Reprodutor de música do servidor',
'agentSnapshot.plugin.SYSTEM_PLUGIN_NEWS_CHINANEWS': 'China News',
'agentSnapshot.plugin.SYSTEM_PLUGIN_NEWS_NEWSNOW': 'Agregação NewsNow',
'agentSnapshot.plugin.SYSTEM_PLUGIN_HA_GET_STATE': 'Consulta de estado do HomeAssistant',
'agentSnapshot.plugin.SYSTEM_PLUGIN_HA_SET_STATE': 'Atualização de estado do HomeAssistant',
'agentSnapshot.plugin.SYSTEM_PLUGIN_HA_STATE': 'Controle de dispositivo do HomeAssistant',
'agentSnapshot.plugin.SYSTEM_PLUGIN_HA_PLAY_MUSIC': 'Reprodutor de música do HomeAssistant',
'agentSnapshot.plugin.SYSTEM_PLUGIN_WEB_SEARCH': 'Pesquisa na web',
'agentSnapshot.plugin.SYSTEM_PLUGIN_CALL_DEVICE': 'Chamada entre dispositivos',
+1 -2
View File
@@ -903,8 +903,7 @@ export default {
'agentSnapshot.plugin.SYSTEM_PLUGIN_MUSIC': 'Trình phát nhạc máy chủ',
'agentSnapshot.plugin.SYSTEM_PLUGIN_NEWS_CHINANEWS': 'Tin tức China News',
'agentSnapshot.plugin.SYSTEM_PLUGIN_NEWS_NEWSNOW': 'Tổng hợp NewsNow',
'agentSnapshot.plugin.SYSTEM_PLUGIN_HA_GET_STATE': 'Truy vấn trạng thái HomeAssistant',
'agentSnapshot.plugin.SYSTEM_PLUGIN_HA_SET_STATE': 'Cập nhật trạng thái HomeAssistant',
'agentSnapshot.plugin.SYSTEM_PLUGIN_HA_STATE': 'Điều khiển thiết bị HomeAssistant',
'agentSnapshot.plugin.SYSTEM_PLUGIN_HA_PLAY_MUSIC': 'Trình phát nhạc HomeAssistant',
'agentSnapshot.plugin.SYSTEM_PLUGIN_WEB_SEARCH': 'Tìm kiếm web',
'agentSnapshot.plugin.SYSTEM_PLUGIN_CALL_DEVICE': 'Gọi thiết bị',
+1 -2
View File
@@ -953,8 +953,7 @@ export default {
'agentSnapshot.plugin.SYSTEM_PLUGIN_MUSIC': '服务器音乐播放',
'agentSnapshot.plugin.SYSTEM_PLUGIN_NEWS_CHINANEWS': '中新网新闻',
'agentSnapshot.plugin.SYSTEM_PLUGIN_NEWS_NEWSNOW': 'newsnow新闻聚合',
'agentSnapshot.plugin.SYSTEM_PLUGIN_HA_GET_STATE': 'HomeAssistant设备状态查询',
'agentSnapshot.plugin.SYSTEM_PLUGIN_HA_SET_STATE': 'HomeAssistant设备状态修改',
'agentSnapshot.plugin.SYSTEM_PLUGIN_HA_STATE': 'HomeAssistant设备控制',
'agentSnapshot.plugin.SYSTEM_PLUGIN_HA_PLAY_MUSIC': 'HomeAssistant音乐播放',
'agentSnapshot.plugin.SYSTEM_PLUGIN_WEB_SEARCH': '联网搜索',
'agentSnapshot.plugin.SYSTEM_PLUGIN_CALL_DEVICE': '设备呼叫设备',
+1 -2
View File
@@ -903,8 +903,7 @@ export default {
'agentSnapshot.plugin.SYSTEM_PLUGIN_MUSIC': '伺服器音樂播放',
'agentSnapshot.plugin.SYSTEM_PLUGIN_NEWS_CHINANEWS': '中新網新聞',
'agentSnapshot.plugin.SYSTEM_PLUGIN_NEWS_NEWSNOW': 'newsnow新聞聚合',
'agentSnapshot.plugin.SYSTEM_PLUGIN_HA_GET_STATE': 'HomeAssistant設備狀態查詢',
'agentSnapshot.plugin.SYSTEM_PLUGIN_HA_SET_STATE': 'HomeAssistant設備狀態修改',
'agentSnapshot.plugin.SYSTEM_PLUGIN_HA_STATE': 'HomeAssistant設備控制',
'agentSnapshot.plugin.SYSTEM_PLUGIN_HA_PLAY_MUSIC': 'HomeAssistant音樂播放',
'agentSnapshot.plugin.SYSTEM_PLUGIN_WEB_SEARCH': '聯網搜尋',
'agentSnapshot.plugin.SYSTEM_PLUGIN_CALL_DEVICE': '設備呼叫設備',
+1 -2
View File
@@ -286,8 +286,7 @@ Intent:
# play_music是服务器自带的音乐播放,hass_play_music是通过home assistant控制的独立外部程序音乐播放
# 如果用了hass_play_music,就不要开启play_music,两者只留一个
- play_music
#- hass_get_state
#- hass_set_state
#- hass_state
#- hass_play_music
Memory:
+19 -2
View File
@@ -35,7 +35,7 @@ from core.providers.asr.dto.dto import InterfaceType
from core.handle.textHandle import handleTextMessage
from core.providers.tools.unified_tool_handler import UnifiedToolHandler
from plugins_func.loadplugins import auto_import_modules
from plugins_func.register import Action, ActionResponse
from plugins_func.register import Action, ActionResponse, all_function_registry, module_func_map
from core.auth import AuthenticationError
from config.config_loader import get_private_config_from_api
from core.providers.tts.dto.dto import ContentType, TTSMessageDTO, SentenceType
@@ -878,10 +878,27 @@ class ConnectionHandler:
plugin_from_server = private_config.get("plugins", {})
for plugin, config_str in plugin_from_server.items():
plugin_from_server[plugin] = json.loads(config_str)
# 将模块级别的插件配置复制到各具体函数名,
# 方便后续 per-function 查找描述、news_sources 等配置
for module_name, func_names in module_func_map.items():
if module_name in plugin_from_server:
module_config = plugin_from_server[module_name]
for func_name in func_names:
if func_name not in plugin_from_server:
plugin_from_server[func_name] = module_config
self.config["plugins"] = plugin_from_server
# 将模块级别的插件名展开为具体函数名
expanded_functions = []
for plugin_key in plugin_from_server.keys():
if plugin_key in all_function_registry:
expanded_functions.append(plugin_key)
elif plugin_key in module_func_map:
expanded_functions.extend(module_func_map[plugin_key])
else:
expanded_functions.append(plugin_key)
self.config["Intent"][self.config["selected_module"]["Intent"]][
"functions"
] = plugin_from_server.keys()
] = expanded_functions
if private_config.get("prompt", None) is not None:
self.config["prompt"] = private_config["prompt"]
# 获取声纹信息
@@ -6,7 +6,7 @@ from typing import Dict, Any, TYPE_CHECKING
if TYPE_CHECKING:
from core.connection import ConnectionHandler
from ..base import ToolType, ToolDefinition, ToolExecutor
from plugins_func.register import all_function_registry, Action, ActionResponse
from plugins_func.register import all_function_registry, module_func_map, Action, ActionResponse
class ServerPluginExecutor(ToolExecutor):
@@ -54,6 +54,43 @@ class ServerPluginExecutor(ToolExecutor):
response=str(e),
)
def _expand_plugin_names(self, config_functions):
"""将模块级别的插件名展开为具体函数名。
在一个 function 文件中可能注册多个 @register_function
配置中如果使用的是模块名(文件名),需要展开为具体的函数名列表。
"""
if not isinstance(config_functions, list):
try:
config_functions = list(config_functions)
except TypeError:
return []
expanded = []
for name in config_functions:
if name in module_func_map:
# 模块名,展开为该模块下所有注册函数名
expanded.extend(module_func_map[name])
elif name in all_function_registry:
# 精确匹配函数名,直接保留
expanded.append(name)
else:
# 未知名称,保留原值(可能是 MCP 或其他工具)
expanded.append(name)
return expanded
def _get_plugin_description(self, func_name):
"""获取插件函数的描述,优先精确匹配函数名,其次匹配模块名。"""
plugins = self.config.get("plugins", {})
# 精确匹配函数名
if func_name in plugins:
return plugins[func_name].get("description", "")
# 通过 module_func_map 反向查找模块名
for module_name, func_names in module_func_map.items():
if func_name in func_names and module_name in plugins:
return plugins[module_name].get("description", "")
return ""
def get_tools(self) -> Dict[str, ToolDefinition]:
"""获取所有注册的服务端插件工具"""
tools = {}
@@ -66,12 +103,8 @@ class ServerPluginExecutor(ToolExecutor):
self.config["selected_module"]["Intent"]
].get("functions", [])
# 转换为列表
if not isinstance(config_functions, list):
try:
config_functions = list(config_functions)
except TypeError:
config_functions = []
# 将模块级别的插件名展开为具体函数名(兜底机制)
config_functions = self._expand_plugin_names(config_functions)
# 合并所有需要的函数
all_required_functions = list(set(necessary_functions + config_functions))
@@ -79,12 +112,8 @@ class ServerPluginExecutor(ToolExecutor):
for func_name in all_required_functions:
func_item = all_function_registry.get(func_name)
if func_item:
# 从函数注册中获取描述
fun_description = (
self.config.get("plugins", {})
.get(func_name, {})
.get("description", "")
)
# 从函数注册中获取描述(支持模块名和函数名的双层查找)
fun_description = self._get_plugin_description(func_name)
if fun_description is not None and len(fun_description) > 0:
if "function" in func_item.description and isinstance(
func_item.description["function"], dict
@@ -1,98 +0,0 @@
import httpx
from config.logger import setup_logging
from plugins_func.functions.hass_init import initialize_hass_handler
from plugins_func.register import register_function, ToolType, ActionResponse, Action
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from core.connection import ConnectionHandler
TAG = __name__
logger = setup_logging()
hass_get_state_function_desc = {
"type": "function",
"function": {
"name": "hass_get_state",
"description": "获取homeassistant里设备的状态,包括查询灯光亮度、颜色、色温,媒体播放器的音量,设备的暂停、继续操作",
"parameters": {
"type": "object",
"properties": {
"entity_id": {
"type": "string",
"description": "需要操作的设备id,homeassistant里的entity_id",
}
},
"required": ["entity_id"],
},
},
}
@register_function("hass_get_state", hass_get_state_function_desc, ToolType.SYSTEM_CTL)
async def hass_get_state(conn: "ConnectionHandler", entity_id=""):
try:
ha_response = await handle_hass_get_state(conn, entity_id)
return ActionResponse(Action.REQLLM, ha_response, None)
except httpx.TimeoutException:
logger.bind(tag=TAG).error("获取Home Assistant状态超时")
return ActionResponse(Action.ERROR, "请求超时", None)
except Exception as e:
error_msg = f"执行Home Assistant操作失败"
logger.bind(tag=TAG).error(error_msg)
return ActionResponse(Action.ERROR, error_msg, None)
async def handle_hass_get_state(conn: "ConnectionHandler", entity_id):
ha_config = initialize_hass_handler(conn)
api_key = ha_config.get("api_key")
base_url = ha_config.get("base_url")
url = f"{base_url}/api/states/{entity_id}"
headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
async with httpx.AsyncClient(timeout=httpx.Timeout(5.0, connect=3.0)) as client:
response = await client.get(url, headers=headers)
if response.status_code == 200:
responsetext = "设备状态:" + response.json()["state"] + " "
logger.bind(tag=TAG).info(f"api返回内容: {response.json()}")
if "media_title" in response.json()["attributes"]:
responsetext = (
responsetext
+ "正在播放的是:"
+ str(response.json()["attributes"]["media_title"])
+ " "
)
if "volume_level" in response.json()["attributes"]:
responsetext = (
responsetext
+ "音量是:"
+ str(response.json()["attributes"]["volume_level"])
+ " "
)
if "color_temp_kelvin" in response.json()["attributes"]:
responsetext = (
responsetext
+ "色温是:"
+ str(response.json()["attributes"]["color_temp_kelvin"])
+ " "
)
if "rgb_color" in response.json()["attributes"]:
responsetext = (
responsetext
+ "rgb颜色是:"
+ str(response.json()["attributes"]["rgb_color"])
+ " "
)
if "brightness" in response.json()["attributes"]:
responsetext = (
responsetext
+ "亮度是:"
+ str(response.json()["attributes"]["brightness"])
+ " "
)
logger.bind(tag=TAG).info(f"查询返回内容: {responsetext}")
return responsetext
else:
return f"切换失败,错误码: {response.status_code}"
@@ -16,7 +16,7 @@ def append_devices_to_prompt(conn):
config_source = (
"home_assistant"
if plugins_config.get("home_assistant")
else "hass_get_state"
else "hass_state"
)
if "hass_get_state" in funcs or "hass_set_state" in funcs:
@@ -36,7 +36,7 @@ def initialize_hass_handler(conn):
plugins_config = conn.config.get("plugins", {})
# 确定配置来源
config_source = (
"home_assistant" if plugins_config.get("home_assistant") else "hass_get_state"
"home_assistant" if plugins_config.get("home_assistant") else "hass_state"
)
if not plugins_config.get(config_source):
return ha_config
@@ -10,6 +10,24 @@ if TYPE_CHECKING:
TAG = __name__
logger = setup_logging()
hass_get_state_function_desc = {
"type": "function",
"function": {
"name": "hass_get_state",
"description": "获取homeassistant里设备的状态,包括查询灯光亮度、颜色、色温,媒体播放器的音量,设备的暂停、继续操作",
"parameters": {
"type": "object",
"properties": {
"entity_id": {
"type": "string",
"description": "需要操作的设备id,homeassistant里的entity_id",
}
},
"required": ["entity_id"],
},
},
}
hass_set_state_function_desc = {
"type": "function",
"function": {
@@ -52,6 +70,20 @@ hass_set_state_function_desc = {
}
@register_function("hass_get_state", hass_get_state_function_desc, ToolType.SYSTEM_CTL)
async def hass_get_state(conn: "ConnectionHandler", entity_id=""):
try:
ha_response = await handle_hass_get_state(conn, entity_id)
return ActionResponse(Action.REQLLM, ha_response, None)
except httpx.TimeoutException:
logger.bind(tag=TAG).error("获取Home Assistant状态超时")
return ActionResponse(Action.ERROR, "请求超时", None)
except Exception as e:
error_msg = f"执行Home Assistant操作失败"
logger.bind(tag=TAG).error(error_msg)
return ActionResponse(Action.ERROR, error_msg, None)
@register_function("hass_set_state", hass_set_state_function_desc, ToolType.SYSTEM_CTL)
async def hass_set_state(conn: "ConnectionHandler", entity_id="", state=None):
if state is None:
@@ -68,6 +100,61 @@ async def hass_set_state(conn: "ConnectionHandler", entity_id="", state=None):
return ActionResponse(Action.ERROR, error_msg, None)
async def handle_hass_get_state(conn: "ConnectionHandler", entity_id):
ha_config = initialize_hass_handler(conn)
api_key = ha_config.get("api_key")
base_url = ha_config.get("base_url")
url = f"{base_url}/api/states/{entity_id}"
headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
async with httpx.AsyncClient(timeout=httpx.Timeout(5.0, connect=3.0)) as client:
response = await client.get(url, headers=headers)
if response.status_code == 200:
responsetext = "设备状态:" + response.json()["state"] + " "
logger.bind(tag=TAG).info(f"api返回内容: {response.json()}")
if "media_title" in response.json()["attributes"]:
responsetext = (
responsetext
+ "正在播放的是:"
+ str(response.json()["attributes"]["media_title"])
+ " "
)
if "volume_level" in response.json()["attributes"]:
responsetext = (
responsetext
+ "音量是:"
+ str(response.json()["attributes"]["volume_level"])
+ " "
)
if "color_temp_kelvin" in response.json()["attributes"]:
responsetext = (
responsetext
+ "色温是:"
+ str(response.json()["attributes"]["color_temp_kelvin"])
+ " "
)
if "rgb_color" in response.json()["attributes"]:
responsetext = (
responsetext
+ "rgb颜色是:"
+ str(response.json()["attributes"]["rgb_color"])
+ " "
)
if "brightness" in response.json()["attributes"]:
responsetext = (
responsetext
+ "亮度是:"
+ str(response.json()["attributes"]["brightness"])
+ " "
)
logger.bind(tag=TAG).info(f"查询返回内容: {responsetext}")
return responsetext
else:
return f"切换失败,错误码: {response.status_code}"
async def handle_hass_set_state(conn: "ConnectionHandler", entity_id, state):
ha_config = initialize_hass_handler(conn)
api_key = ha_config.get("api_key")
@@ -78,6 +78,8 @@ class DeviceTypeRegistry:
# 初始化函数注册字典
all_function_registry = {}
# 模块名 -> 函数名列表的映射,用于将模块级别的插件名展开为具体的函数名
module_func_map = {}
def register_function(name, desc, type=None):
@@ -85,6 +87,9 @@ def register_function(name, desc, type=None):
def decorator(func):
all_function_registry[name] = FunctionItem(name, desc, func, type)
# 记录模块名到函数名的映射,用于 expand 模块级别的插件配置
module_name = func.__module__.split(".")[-1]
module_func_map.setdefault(module_name, []).append(name)
logger.bind(tag=TAG).debug(f"函数 '{name}' 已加载,可以注册使用")
return func