@@ -891,6 +933,50 @@
};
return mapping[normalizedLevel] || '暂无样本';
},
+ dispatchModeTagType(mode) {
+ const normalizedMode = String(mode || '').toLowerCase();
+ if (normalizedMode === 'sync') return 'success';
+ if (normalizedMode === 'background') return 'warning';
+ if (normalizedMode === 'mixed') return '';
+ return 'info';
+ },
+ dispatchModeLabel(mode) {
+ const normalizedMode = String(mode || '').toLowerCase();
+ const mapping = {
+ sync: '前台同步',
+ background: '后台任务',
+ mixed: '混合模式',
+ non_message: '非消息插件',
+ unknown: '未知'
+ };
+ return mapping[normalizedMode] || '未知';
+ },
+ buildDispatchSummaryBrief(plugin) {
+ const dispatchSummary = (plugin && plugin.dispatch_summary) || {};
+ const mode = String(dispatchSummary.mode || plugin.dispatch_mode || '').toLowerCase();
+ const syncCount = Number(dispatchSummary.sync_command_count || 0);
+ const backgroundCount = Number(dispatchSummary.background_command_count || 0);
+ if (mode === 'mixed') {
+ return `前台 ${syncCount} / 后台 ${backgroundCount}`;
+ }
+ if (mode === 'background') {
+ return backgroundCount > 0 ? `后台命令 ${backgroundCount} 个` : '默认后台执行';
+ }
+ if (mode === 'sync') {
+ return syncCount > 0 ? `前台命令 ${syncCount} 个` : '默认前台执行';
+ }
+ if (mode === 'non_message') {
+ return '不参与消息链路';
+ }
+ return '暂未识别';
+ },
+ buildDispatchSummaryText(plugin) {
+ const dispatchSummary = (plugin && plugin.dispatch_summary) || {};
+ if (dispatchSummary.description) {
+ return dispatchSummary.description;
+ }
+ return this.dispatchModeLabel(dispatchSummary.mode || plugin.dispatch_mode);
+ },
governanceIssueSummary(plugin) {
const errorCount = Number((plugin && plugin.governance_error_count) || 0);
const warningCount = Number((plugin && plugin.governance_warning_count) || 0);
diff --git a/base/plugin_common/plugin_manager.py b/base/plugin_common/plugin_manager.py
index 37e1f65..d28411a 100644
--- a/base/plugin_common/plugin_manager.py
+++ b/base/plugin_common/plugin_manager.py
@@ -445,6 +445,162 @@ class PluginManager:
return [commands.strip()]
return []
+ @staticmethod
+ def _dispatch_mode_label(mode: str) -> str:
+ """把消息分发模式转换成后台可读中文。"""
+ normalized_mode = str(mode or "").strip().lower()
+ mapping = {
+ "sync": "前台同步",
+ "background": "后台任务",
+ "mixed": "混合模式",
+ "non_message": "非消息插件",
+ "unknown": "未知",
+ }
+ return mapping.get(normalized_mode, "未知")
+
+ def _build_dispatch_preview_message(self, plugin: PluginInterface, command: str = "") -> Dict[str, Any]:
+ """构造用于后台预览分发模式的假消息。
+
+ 说明:
+ 1. 后台只需要知道“这个插件命中后大概率走前台还是后台”,不需要真实微信上下文;
+ 2. 因此这里构造一份最小可用消息,尽量覆盖多数插件 `get_message_dispatch_mode()` 的读取字段;
+ 3. 如果某些插件依赖更复杂上下文,后续会在调用层捕获异常并回退为 `unknown`,不会影响治理页整体可用性。
+ """
+ command_text = str(command or "").strip()
+ command_prefix = str(getattr(plugin, "command_prefix", "") or "").strip()
+ content = command_text
+ if command_text and command_prefix and not command_text.startswith(command_prefix):
+ content = f"{command_prefix}{command_text}"
+
+ return {
+ "type": "dashboard_preview",
+ "content": content,
+ "sender": "dashboard_preview",
+ "roomid": "dashboard_preview_room",
+ "is_at": False,
+ "timestamp": 0,
+ "trace_id": "dashboard-preview",
+ "all_contacts": {},
+ "full_wx_msg": None,
+ "gbm": None,
+ "bot": None,
+ "revoke": None,
+ }
+
+ def _preview_plugin_dispatch_mode(self, plugin: PluginInterface, preview_message: Dict[str, Any]) -> str:
+ """安全预览插件在给定消息下的分发模式。"""
+ try:
+ raw_mode = plugin.get_message_dispatch_mode(preview_message)
+ return MessagePluginInterface.normalize_message_dispatch_mode(raw_mode)
+ except Exception as e:
+ module_name = self._get_module_name_from_plugin(plugin) or getattr(plugin, "name", "unknown")
+ self.LOG.debug(f"插件分发模式预览失败: module={module_name}, error={e}")
+ return "unknown"
+
+ def _build_plugin_dispatch_summary(
+ self,
+ plugin: PluginInterface,
+ commands: Optional[List[str]] = None,
+ ) -> Dict[str, Any]:
+ """构建插件“前台 / 后台”执行方式摘要。
+
+ 设计目标:
+ 1. 后台插件治理页需要一眼看出消息插件是“前台同步”“后台任务”还是“按命令混合切换”;
+ 2. 同一个插件里可能既有轻命令又有长命令,因此不能只看静态配置,还要做命令级预览;
+ 3. 统一在快照层产出摘要后,列表、详情、移动端卡片都能直接复用,不必各写一套判断逻辑。
+ """
+ if not isinstance(plugin, MessagePluginInterface):
+ return {
+ "mode": "non_message",
+ "label": self._dispatch_mode_label("non_message"),
+ "description": "该插件不参与消息主链路分发,不区分前台同步或后台任务。",
+ "is_message_plugin": False,
+ "supports_dynamic_dispatch": False,
+ "sync_command_count": 0,
+ "background_command_count": 0,
+ "unknown_command_count": 0,
+ "preview_failed_count": 0,
+ "sampled_command_count": 0,
+ "command_modes": [],
+ }
+
+ commands = list(commands or self._collect_plugin_commands(plugin))
+ preview_items: List[Tuple[str, Dict[str, Any]]] = []
+ if commands:
+ for command in commands:
+ preview_items.append((command, self._build_dispatch_preview_message(plugin, command)))
+ else:
+ # 没有显式命令声明的消息插件,至少用一条空消息预览默认分发模式:
+ # 1. 例如链接解析类插件可能靠正则命中,而不是 commands 数组;
+ # 2. 这类插件通常不会做命令级动态切换,空消息预览已经足够判断默认模式;
+ # 3. 后台仍会标记 sampled_command_count=0,避免用户误以为这里真的存在命令清单。
+ preview_items.append(("", self._build_dispatch_preview_message(plugin, "")))
+
+ mode_counters = {"sync": 0, "background": 0, "unknown": 0}
+ command_modes = []
+ for command, preview_message in preview_items:
+ preview_mode = self._preview_plugin_dispatch_mode(plugin, preview_message)
+ if preview_mode not in mode_counters:
+ preview_mode = "unknown"
+ mode_counters[preview_mode] += 1
+
+ if command:
+ command_modes.append(
+ {
+ "command": command,
+ "mode": preview_mode,
+ "label": self._dispatch_mode_label(preview_mode),
+ }
+ )
+
+ known_modes = []
+ if mode_counters["sync"] > 0:
+ known_modes.append("sync")
+ if mode_counters["background"] > 0:
+ known_modes.append("background")
+
+ if len(known_modes) > 1:
+ summary_mode = "mixed"
+ description = (
+ f"插件会按命令动态切换执行链路:前台同步 {mode_counters['sync']} 个,"
+ f"后台任务 {mode_counters['background']} 个。"
+ )
+ elif len(known_modes) == 1:
+ summary_mode = known_modes[0]
+ if known_modes[0] == "background":
+ if commands:
+ description = f"当前采样的 {len(commands)} 个命令均会转入后台任务池执行。"
+ else:
+ description = "该消息插件默认转入后台任务池执行。"
+ else:
+ if commands:
+ description = f"当前采样的 {len(commands)} 个命令均在前台消息链路同步执行。"
+ else:
+ description = "该消息插件默认在前台消息链路同步执行。"
+ else:
+ summary_mode = "unknown"
+ description = "插件已加载,但当前无法稳定预览其前台 / 后台执行方式。"
+
+ if mode_counters["unknown"] > 0:
+ description = (
+ f"{description} 另有 {mode_counters['unknown']} 条预览样本无法识别,"
+ "可能依赖更完整的运行时上下文。"
+ )
+
+ return {
+ "mode": summary_mode,
+ "label": self._dispatch_mode_label(summary_mode),
+ "description": description,
+ "is_message_plugin": True,
+ "supports_dynamic_dispatch": summary_mode == "mixed",
+ "sync_command_count": mode_counters["sync"],
+ "background_command_count": mode_counters["background"],
+ "unknown_command_count": mode_counters["unknown"],
+ "preview_failed_count": mode_counters["unknown"],
+ "sampled_command_count": len(commands),
+ "command_modes": command_modes,
+ }
+
def _build_governance_diagnostics(
self,
*,
@@ -706,6 +862,7 @@ class PluginManager:
config_path = plugin.get_config_path()
config_overview = self._read_plugin_config_overview(config_path)
commands = self._collect_plugin_commands(plugin)
+ dispatch_summary = self._build_plugin_dispatch_summary(plugin, commands)
feature_key = str(getattr(plugin, "feature_key", "") or "").strip()
feature_description = str(getattr(plugin, "feature_description", "") or "").strip()
governance_diagnostics = self._build_governance_diagnostics(
@@ -729,6 +886,8 @@ class PluginManager:
"commands": commands,
"command_count": len(commands),
"command_prefix": getattr(plugin, "command_prefix", ""),
+ "dispatch_mode": dispatch_summary["mode"],
+ "dispatch_summary": dispatch_summary,
"dependencies": list(getattr(plugin, "dependencies", []) or []),
"feature_key": feature_key,
"feature_description": feature_description,
@@ -770,6 +929,19 @@ class PluginManager:
status = "ERROR"
elif runtime_state == "disabled_by_config":
status = "STOPPED"
+ dispatch_summary = {
+ "mode": "unknown",
+ "label": self._dispatch_mode_label("unknown"),
+ "description": "插件未成功加载,暂时无法判断其前台 / 后台执行方式。",
+ "is_message_plugin": False,
+ "supports_dynamic_dispatch": False,
+ "sync_command_count": 0,
+ "background_command_count": 0,
+ "unknown_command_count": 0,
+ "preview_failed_count": 0,
+ "sampled_command_count": 0,
+ "command_modes": [],
+ }
return {
"name": module_name,
@@ -783,6 +955,8 @@ class PluginManager:
"commands": [],
"command_count": 0,
"command_prefix": "",
+ "dispatch_mode": dispatch_summary["mode"],
+ "dispatch_summary": dispatch_summary,
"dependencies": [],
"feature_key": "",
"feature_description": "",
@@ -929,10 +1103,10 @@ class PluginManager:
if not target_name:
return None
- display_name, plugin = self.find_plugin_by_name(target_name)
- if plugin:
- return self._build_plugin_snapshot(plugin)
-
+ # 详情页也统一走“完整治理快照列表”:
+ # 1. 列表页已经会补齐依赖关系、执行摘要、分发模式等增强字段;
+ # 2. 如果详情页单独重建快照,容易出现“列表里有,详情里没有”的字段不一致;
+ # 3. 这里直接复用同一套结果,保证前后端看到的是同一份治理视图。
for snapshot in self.get_plugin_snapshots():
if snapshot.get("module_name") == target_name or snapshot.get("name") == target_name:
return snapshot