后台插件管理页展示前后台执行方式

1. 在插件治理快照中新增消息插件分发方式摘要,区分前台同步、后台任务、混合模式与非消息插件。
2. 插件详情接口统一复用完整治理快照,避免列表和详情字段不一致。
3. 插件管理页列表、移动端卡片和详情弹窗新增执行方式展示,并支持命令级分发预览。
This commit is contained in:
Liu
2026-05-01 11:45:23 +08:00
parent adbf4471cf
commit c0a6ee6c21
2 changed files with 264 additions and 4 deletions

View File

@@ -253,6 +253,18 @@
</div>
</template>
</el-table-column>
<el-table-column label="执行方式" width="180" align="center">
<template slot-scope="scope">
<div class="governance-cell">
<el-tag :type="dispatchModeTagType((scope.row.dispatch_summary || {}).mode)" size="small">
{% raw %}{{ (scope.row.dispatch_summary || {}).label || dispatchModeLabel(scope.row.dispatch_mode) }}{% endraw %}
</el-tag>
<div class="governance-note">
{% raw %}{{ buildDispatchSummaryBrief(scope.row) }}{% endraw %}
</div>
</div>
</template>
</el-table-column>
<el-table-column label="命令 / 权限" min-width="180">
<template slot-scope="scope">
<div class="entity-subtitle">
@@ -315,6 +327,10 @@
<span>成功率:{% raw %}{{ formatPercent((plugin.execution_summary || {}).success_rate) }}{% endraw %}</span>
<span>耗时:{% raw %}{{ formatDurationMs((plugin.execution_summary || {}).last_process_time_ms) }}{% endraw %}</span>
</div>
<div class="mobile-plugin-card__meta">
<span>执行方式:{% raw %}{{ (plugin.dispatch_summary || {}).label || dispatchModeLabel(plugin.dispatch_mode) }}{% endraw %}</span>
<span>{% raw %}{{ buildDispatchSummaryBrief(plugin) }}{% endraw %}</span>
</div>
<div class="mobile-plugin-card__desc">
{% raw %}{{ plugin.description || '暂无描述' }}{% endraw %}
</div>
@@ -367,6 +383,14 @@
<el-descriptions-item label="命令前缀" :span="1" v-if="selectedPlugin.command_prefix !== undefined">
{% raw %}{{ selectedPlugin.command_prefix || '无' }}{% endraw %}
</el-descriptions-item>
<el-descriptions-item label="执行方式" :span="1">
<el-tag :type="dispatchModeTagType((selectedPlugin.dispatch_summary || {}).mode)" size="small">
{% raw %}{{ (selectedPlugin.dispatch_summary || {}).label || dispatchModeLabel(selectedPlugin.dispatch_mode) }}{% endraw %}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="分发说明" :span="2">
{% raw %}{{ buildDispatchSummaryText(selectedPlugin) }}{% endraw %}
</el-descriptions-item>
<el-descriptions-item label="描述" :span="2">{% raw %}{{ selectedPlugin.description }}{% endraw %}</el-descriptions-item>
<el-descriptions-item label="能力类型" :span="2" v-if="selectedPlugin.plugin_types && selectedPlugin.plugin_types.length > 0">
<div class="command-tags">
@@ -445,6 +469,24 @@
</el-tag>
</div>
</el-descriptions-item>
<el-descriptions-item
label="命令分发预览"
:span="2"
v-if="selectedPlugin.dispatch_summary && selectedPlugin.dispatch_summary.command_modes && selectedPlugin.dispatch_summary.command_modes.length > 0">
<div class="command-tags">
<el-tag
v-for="dispatchCommand in selectedPlugin.dispatch_summary.command_modes"
:key="`dispatch-${dispatchCommand.command}`"
:type="dispatchModeTagType(dispatchCommand.mode)"
size="mini"
effect="plain">
{% raw %}{{ `${dispatchCommand.command} · ${dispatchCommand.label}` }}{% endraw %}
</el-tag>
</div>
<div class="entity-subtitle" style="margin-top: 8px;" v-if="(selectedPlugin.dispatch_summary.preview_failed_count || 0) > 0">
{% raw %}{{ `另有 ${selectedPlugin.dispatch_summary.preview_failed_count} 条预览样本无法识别,可能依赖更完整的运行时上下文。` }}{% endraw %}
</div>
</el-descriptions-item>
<el-descriptions-item label="配置概览" :span="2" v-if="selectedPlugin.config_overview">
<div class="config-overview-grid">
<div class="config-overview-item">
@@ -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);

View File

@@ -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