Files
abot/admin/dashboard/templates/plugins_manage.html
Liu c0a6ee6c21 后台插件管理页展示前后台执行方式
1. 在插件治理快照中新增消息插件分发方式摘要,区分前台同步、后台任务、混合模式与非消息插件。
2. 插件详情接口统一复用完整治理快照,避免列表和详情字段不一致。
3. 插件管理页列表、移动端卡片和详情弹窗新增执行方式展示,并支持命令级分发预览。
2026-05-01 11:45:23 +08:00

1653 lines
85 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends "base.html" %}
{% block title %}插件管理 - 机器人管理后台{% endblock %}
{% block content %}
<div class="page-shell plugins-page">
<div class="page-hero">
<div class="page-hero-copy">
<div class="page-eyebrow">Plugins Workspace</div>
<h1>插件管理</h1>
<p>统一查看插件状态、版本、说明与配置,减少传统后台式碎片操作。</p>
</div>
<div class="page-hero-actions">
<el-button type="primary" @click="refreshPlugins">
<i class="el-icon-refresh"></i> 刷新插件
</el-button>
</div>
</div>
<el-row :gutter="16" class="overview-grid">
<el-col :xs="24" :sm="12" :md="8" :lg="4">
<el-card class="overview-card overview-card--primary" shadow="hover">
<div class="overview-label">插件总数</div>
<div class="overview-value">{% raw %}{{ plugins.length }}{% endraw %}</div>
<div class="overview-note">当前已注册插件模块</div>
</el-card>
</el-col>
<el-col :xs="24" :sm="12" :md="8" :lg="4">
<el-card class="overview-card" shadow="hover">
<div class="overview-label">运行中</div>
<div class="overview-value">{% raw %}{{ runningPluginsCount }}{% endraw %}</div>
<div class="overview-note">可正常提供能力的插件</div>
</el-card>
</el-col>
<el-col :xs="24" :sm="12" :md="8" :lg="4">
<el-card class="overview-card" shadow="hover">
<div class="overview-label">已停用</div>
<div class="overview-value">{% raw %}{{ stoppedPluginsCount }}{% endraw %}</div>
<div class="overview-note">待启用或排查状态</div>
</el-card>
</el-col>
<el-col :xs="24" :sm="12" :md="8" :lg="4">
<el-card class="overview-card overview-card--soft" shadow="hover">
<div class="overview-label">治理告警</div>
<div class="overview-value">{% raw %}{{ governanceRiskCount }}{% endraw %}</div>
<div class="overview-note">存在配置、依赖或加载风险的插件</div>
</el-card>
</el-col>
<el-col :xs="24" :sm="12" :md="8" :lg="4">
<el-card class="overview-card" shadow="hover">
<div class="overview-label">执行异常</div>
<div class="overview-value">{% raw %}{{ executionRiskCount }}{% endraw %}</div>
<div class="overview-note">最近执行失败、超时或进入熔断的插件</div>
</el-card>
</el-col>
<el-col :xs="24" :sm="12" :md="8" :lg="4">
<el-card class="overview-card" shadow="hover">
<div class="overview-label">熔断中</div>
<div class="overview-value">{% raw %}{{ openCircuitCount }}{% endraw %}</div>
<div class="overview-note">当前被保护机制隔离的高风险插件</div>
</el-card>
</el-col>
</el-row>
<el-row :gutter="16" class="insight-grid">
<el-col :xs="24" :md="12">
<el-card class="workspace-card" shadow="hover">
<div slot="header" class="workspace-header">
<div>
<h3>高风险插件</h3>
<p>优先排查熔断中、连续失败或最近错误较多的插件。</p>
</div>
</div>
<div class="rank-list">
<div v-if="topRiskPlugins.length === 0" class="mobile-empty-state">暂无高风险插件</div>
<div v-for="(plugin, index) in topRiskPlugins" :key="`risk-${plugin.module_name}`" class="rank-item">
<div class="rank-item__index">{% raw %}{{ index + 1 }}{% endraw %}</div>
<div class="rank-item__content">
<div class="rank-item__title-row">
<div class="rank-item__title">{% raw %}{{ plugin.name }}{% endraw %}</div>
<el-tag :type="executionTagType((plugin.execution_summary || {}).status)" size="mini">
{% raw %}{{ executionLabel((plugin.execution_summary || {}).status) }}{% endraw %}
</el-tag>
</div>
<div class="rank-item__summary">{% raw %}{{ (plugin.execution_summary || {}).summary || '暂无执行摘要' }}{% endraw %}</div>
<div class="rank-item__meta">
<span>最近错误:{% raw %}{{ (plugin.execution_summary || {}).last_error_message || '无' }}{% endraw %}</span>
</div>
</div>
</div>
</div>
</el-card>
</el-col>
<el-col :xs="24" :md="12">
<el-card class="workspace-card" shadow="hover">
<div slot="header" class="workspace-header">
<div>
<h3>慢插件排行</h3>
<p>基于最近一次执行耗时,快速定位可能影响主链路响应的插件。</p>
</div>
</div>
<div class="rank-list">
<div v-if="slowestPlugins.length === 0" class="mobile-empty-state">暂无执行样本</div>
<div v-for="(plugin, index) in slowestPlugins" :key="`slow-${plugin.module_name}`" class="rank-item">
<div class="rank-item__index">{% raw %}{{ index + 1 }}{% endraw %}</div>
<div class="rank-item__content">
<div class="rank-item__title-row">
<div class="rank-item__title">{% raw %}{{ plugin.name }}{% endraw %}</div>
<div class="rank-item__value">{% raw %}{{ formatDurationMs((plugin.execution_summary || {}).last_process_time_ms) }}{% endraw %}</div>
</div>
<div class="rank-item__summary">{% raw %}{{ (plugin.execution_summary || {}).summary || '暂无执行摘要' }}{% endraw %}</div>
<div class="rank-item__meta">
<span>成功率:{% raw %}{{ formatPercent((plugin.execution_summary || {}).success_rate) }}{% endraw %}</span>
<span>累计执行:{% raw %}{{ (plugin.execution_summary || {}).total_executions || 0 }}{% endraw %}</span>
</div>
</div>
</div>
</div>
</el-card>
</el-col>
</el-row>
<el-row :gutter="16" class="insight-grid">
<el-col :xs="24" :md="12">
<el-card class="workspace-card" shadow="hover">
<div slot="header" class="workspace-header">
<div>
<h3>依赖核心插件</h3>
<p>优先保护被多个插件依赖的基础能力节点,避免单点异常扩散。</p>
</div>
</div>
<div class="rank-list">
<div v-if="topDependencyCorePlugins.length === 0" class="mobile-empty-state">暂无依赖关系数据</div>
<div v-for="(plugin, index) in topDependencyCorePlugins" :key="`core-${plugin.module_name}`" class="rank-item">
<div class="rank-item__index">{% raw %}{{ index + 1 }}{% endraw %}</div>
<div class="rank-item__content">
<div class="rank-item__title-row">
<div class="rank-item__title">{% raw %}{{ plugin.name }}{% endraw %}</div>
<el-tag :type="governanceTagType(plugin.governance_status)" size="mini">
{% raw %}{{ `${(plugin.dependency_summary || {}).dependent_count || 0} 个上游` }}{% endraw %}
</el-tag>
</div>
<div class="rank-item__summary">
{% raw %}{{ buildDependencyCoreSummary(plugin) }}{% endraw %}
</div>
<div class="rank-item__meta">
<span>执行:{% raw %}{{ executionLabel((plugin.execution_summary || {}).status) }}{% endraw %}</span>
<span>治理:{% raw %}{{ governanceLabel(plugin.governance_status) }}{% endraw %}</span>
</div>
</div>
</div>
</div>
</el-card>
</el-col>
<el-col :xs="24" :md="12">
<el-card class="workspace-card" shadow="hover">
<div slot="header" class="workspace-header">
<div>
<h3>缺失依赖风险</h3>
<p>快速查看声明了依赖但当前目标未加载的插件,优先处理运行链断裂问题。</p>
</div>
</div>
<div class="rank-list">
<div v-if="pluginsWithMissingDependencies.length === 0" class="mobile-empty-state">当前没有缺失依赖风险</div>
<div v-for="(plugin, index) in pluginsWithMissingDependencies" :key="`missing-${plugin.module_name}`" class="rank-item">
<div class="rank-item__index">{% raw %}{{ index + 1 }}{% endraw %}</div>
<div class="rank-item__content">
<div class="rank-item__title-row">
<div class="rank-item__title">{% raw %}{{ plugin.name }}{% endraw %}</div>
<el-tag type="warning" size="mini">
{% raw %}{{ `${(plugin.dependency_summary || {}).missing_count || 0} 个缺失` }}{% endraw %}
</el-tag>
</div>
<div class="rank-item__summary">
{% raw %}{{ buildMissingDependencySummary(plugin) }}{% endraw %}
</div>
<div class="rank-item__meta">
<span>模块:{% raw %}{{ plugin.module_name }}{% endraw %}</span>
</div>
</div>
</div>
</div>
</el-card>
</el-col>
</el-row>
<el-card class="workspace-card" shadow="hover">
<div slot="header" class="workspace-header">
<div>
<h3>插件列表</h3>
<p>优先关注状态、执行表现和说明,再进入单个插件详情与配置编辑。</p>
</div>
</div>
<!-- 桌面端继续保留表格视图,避免影响高密度信息浏览效率。 -->
<el-table v-if="!isMobileViewport" :data="plugins" style="width: 100%" v-loading="loading">
<el-table-column label="插件信息" min-width="260">
<template slot-scope="scope">
<div class="entity-cell">
<div class="entity-badge">{% raw %}{{ scope.$index + 1 }}{% endraw %}</div>
<div class="entity-copy">
<div class="entity-title">{% raw %}{{ scope.row.name }}{% endraw %}</div>
<div class="entity-subtitle">模块:{% raw %}{{ scope.row.module_name }}{% endraw %}</div>
</div>
</div>
</template>
</el-table-column>
<el-table-column prop="version" label="版本" width="120"></el-table-column>
<el-table-column prop="author" label="作者" width="140"></el-table-column>
<el-table-column prop="description" label="描述" min-width="280" show-overflow-tooltip></el-table-column>
<el-table-column label="状态" width="120" align="center">
<template slot-scope="scope">
<el-tag :type="pluginStatusTagType(scope.row.status)">
{% raw %}{{ pluginStatusLabel(scope.row) }}{% endraw %}
</el-tag>
</template>
</el-table-column>
<el-table-column label="治理健康" width="170" align="center">
<template slot-scope="scope">
<div class="governance-cell">
<el-tag :type="governanceTagType(scope.row.governance_status)" size="small">
{% raw %}{{ governanceLabel(scope.row.governance_status) }}{% endraw %}
</el-tag>
<div class="governance-note">
{% raw %}{{ governanceIssueSummary(scope.row) }}{% endraw %}
</div>
</div>
</template>
</el-table-column>
<el-table-column label="执行表现" min-width="220">
<template slot-scope="scope">
<div class="execution-cell">
<div class="execution-cell__head">
<el-tag :type="executionTagType((scope.row.execution_summary || {}).status)" size="mini">
{% raw %}{{ executionLabel((scope.row.execution_summary || {}).status) }}{% endraw %}
</el-tag>
<span class="execution-cell__metric">
{% raw %}{{ `${formatPercent((scope.row.execution_summary || {}).success_rate)} / ${formatDurationMs((scope.row.execution_summary || {}).last_process_time_ms)}` }}{% endraw %}
</span>
</div>
<div class="execution-cell__summary">
{% raw %}{{ (scope.row.execution_summary || {}).summary || '暂无执行摘要' }}{% endraw %}
</div>
</div>
</template>
</el-table-column>
<el-table-column label="能力类型" width="150" align="center">
<template slot-scope="scope">
<div class="command-tags command-tags--compact">
<el-tag v-for="pluginType in (scope.row.plugin_types || [])" :key="pluginType" size="mini" effect="plain">
{% raw %}{{ pluginType }}{% endraw %}
</el-tag>
</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">
{% raw %}{{ scope.row.command_count ? `命令 ${scope.row.command_count} 个` : '无命令声明' }}{% endraw %}
</div>
<div class="entity-subtitle">
{% raw %}{{ scope.row.feature_key ? `Feature: ${scope.row.feature_key}` : '未接入群级权限' }}{% endraw %}
</div>
</template>
</el-table-column>
<el-table-column label="操作" min-width="290">
<template slot-scope="scope">
<div class="action-row">
<el-button
size="mini"
:type="scope.row.status === 'RUNNING' ? 'danger' : 'success'"
@click="togglePluginStatus(scope.row)">
{% raw %}{{ scope.row.status === 'RUNNING' ? '禁用' : '启用' }}{% endraw %}
</el-button>
<el-button size="mini" type="primary" plain @click="reloadPlugin(scope.row)">
重载
</el-button>
<el-button size="mini" type="info" plain @click="showPluginInfo(scope.row)">
详情
</el-button>
<el-button size="mini" type="warning" plain @click="showPluginGroupStatus(scope.row)">
群状态
</el-button>
</div>
</template>
</el-table-column>
</el-table>
<div v-else class="mobile-plugin-list" v-loading="loading">
<!-- 手机端改成卡片流式布局,避免表格固定列宽把页面撑出横向滚动。 -->
<div v-if="plugins.length === 0" class="mobile-empty-state">暂无插件数据</div>
<el-card
v-for="(plugin, index) in plugins"
:key="plugin.module_name || plugin.name || index"
class="mobile-plugin-card"
shadow="hover">
<div class="mobile-plugin-card__header">
<div class="entity-cell">
<div class="entity-badge">{% raw %}{{ index + 1 }}{% endraw %}</div>
<div class="entity-copy">
<div class="entity-title">{% raw %}{{ plugin.name }}{% endraw %}</div>
<div class="entity-subtitle">模块:{% raw %}{{ plugin.module_name }}{% endraw %}</div>
</div>
</div>
<el-tag :type="pluginStatusTagType(plugin.status)" size="small">
{% raw %}{{ pluginStatusLabel(plugin) }}{% endraw %}
</el-tag>
</div>
<div class="mobile-plugin-card__meta">
<span>版本:{% raw %}{{ plugin.version || '未知' }}{% endraw %}</span>
<span>治理:{% raw %}{{ governanceLabel(plugin.governance_status) }}{% endraw %}</span>
</div>
<div class="mobile-plugin-card__meta">
<span>执行:{% raw %}{{ executionLabel((plugin.execution_summary || {}).status) }}{% endraw %}</span>
<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>
<div class="mobile-plugin-card__meta">
<span>{% raw %}{{ governanceIssueSummary(plugin) }}{% endraw %}</span>
<span>{% raw %}{{ plugin.feature_key ? `Feature: ${plugin.feature_key}` : '未接入群级权限' }}{% endraw %}</span>
</div>
<div class="mobile-plugin-card__meta">
<span>依赖:{% raw %}{{ buildDependencySummaryText(plugin) }}{% endraw %}</span>
</div>
<div class="mobile-plugin-card__actions">
<el-button
size="mini"
:type="plugin.status === 'RUNNING' ? 'danger' : 'success'"
@click="togglePluginStatus(plugin)">
{% raw %}{{ plugin.status === 'RUNNING' ? '禁用' : '启用' }}{% endraw %}
</el-button>
<el-button size="mini" type="primary" plain @click="reloadPlugin(plugin)">
重载
</el-button>
<el-button size="mini" type="info" plain @click="showPluginInfo(plugin)">
详情
</el-button>
<el-button size="mini" type="warning" plain @click="showPluginGroupStatus(plugin)">
群状态
</el-button>
</div>
</el-card>
</div>
</el-card>
<el-dialog title="插件详情" :visible.sync="pluginInfoVisible" :width="pluginInfoDialogWidth" top="5vh">
<div v-if="selectedPlugin" class="plugin-detail-container">
<div class="dialog-intro">查看插件基础信息、命令列表与配置内容,需要时可直接在这里编辑配置。</div>
<el-descriptions border direction="vertical" :column="pluginDescriptionsColumn" size="small" class="plugin-descriptions">
<el-descriptions-item label="插件名称" :span="1">{% raw %}{{ selectedPlugin.name }}{% endraw %}</el-descriptions-item>
<el-descriptions-item label="模块名称" :span="1">{% raw %}{{ selectedPlugin.module_name }}{% endraw %}</el-descriptions-item>
<el-descriptions-item label="版本" :span="1">{% raw %}{{ selectedPlugin.version }}{% endraw %}</el-descriptions-item>
<el-descriptions-item label="作者" :span="1">{% raw %}{{ selectedPlugin.author }}{% endraw %}</el-descriptions-item>
<el-descriptions-item label="状态" :span="1">
<el-tag :type="pluginStatusTagType(selectedPlugin.status)" size="small">
{% raw %}{{ pluginStatusLabel(selectedPlugin) }}{% endraw %}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="治理健康" :span="1">
<el-tag :type="governanceTagType(selectedPlugin.governance_status)" size="small">
{% raw %}{{ governanceLabel(selectedPlugin.governance_status) }}{% endraw %}
</el-tag>
</el-descriptions-item>
<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">
<el-tag v-for="pluginType in selectedPlugin.plugin_types" :key="pluginType" size="mini" effect="plain">
{% raw %}{{ pluginType }}{% endraw %}
</el-tag>
</div>
</el-descriptions-item>
<el-descriptions-item label="Feature Key" :span="1">
{% raw %}{{ selectedPlugin.feature_key || '未声明' }}{% endraw %}
</el-descriptions-item>
<el-descriptions-item label="群级开关" :span="1">
{% raw %}{{ selectedPlugin.supports_group_switch ? '支持' : '未接入' }}{% endraw %}
</el-descriptions-item>
<el-descriptions-item label="依赖插件" :span="2">
<div v-if="selectedPlugin.dependencies && selectedPlugin.dependencies.length > 0" class="command-tags">
<el-tag v-for="dependency in selectedPlugin.dependencies" :key="dependency" size="mini" effect="plain">
{% raw %}{{ dependency }}{% endraw %}
</el-tag>
</div>
<span v-else></span>
</el-descriptions-item>
<el-descriptions-item label="依赖关系" :span="2" v-if="selectedPlugin.dependency_summary">
<div class="config-overview-grid">
<div class="config-overview-item">
<span class="config-overview-label">声明依赖</span>
<span class="config-overview-value">{% raw %}{{ selectedPlugin.dependency_summary.declared_count || 0 }}{% endraw %}</span>
</div>
<div class="config-overview-item">
<span class="config-overview-label">已解析依赖</span>
<span class="config-overview-value">{% raw %}{{ selectedPlugin.dependency_summary.resolved_count || 0 }}{% endraw %}</span>
</div>
<div class="config-overview-item">
<span class="config-overview-label">缺失依赖</span>
<span class="config-overview-value">{% raw %}{{ selectedPlugin.dependency_summary.missing_count || 0 }}{% endraw %}</span>
</div>
<div class="config-overview-item">
<span class="config-overview-label">下游依赖</span>
<span class="config-overview-value">{% raw %}{{ selectedPlugin.dependency_summary.dependent_count || 0 }}{% endraw %}</span>
</div>
</div>
<div class="dependency-panels">
<div class="dependency-panel">
<div class="dependency-panel__title">已解析依赖</div>
<div v-if="selectedPlugin.resolved_dependencies && selectedPlugin.resolved_dependencies.length > 0" class="command-tags">
<el-tag v-for="dependency in selectedPlugin.resolved_dependencies" :key="`resolved-${dependency.module_name}`" size="mini" effect="plain">
{% raw %}{{ `${dependency.name} (${dependency.status_label || dependency.status || '未知'})` }}{% endraw %}
</el-tag>
</div>
<div v-else class="entity-subtitle"></div>
</div>
<div class="dependency-panel">
<div class="dependency-panel__title">缺失依赖</div>
<div v-if="selectedPlugin.missing_dependencies && selectedPlugin.missing_dependencies.length > 0" class="command-tags">
<el-tag v-for="dependency in selectedPlugin.missing_dependencies" :key="`missing-${dependency.name}`" size="mini" type="warning">
{% raw %}{{ dependency.name }}{% endraw %}
</el-tag>
</div>
<div v-else class="entity-subtitle"></div>
</div>
<div class="dependency-panel">
<div class="dependency-panel__title">下游依赖插件</div>
<div v-if="selectedPlugin.dependent_plugins && selectedPlugin.dependent_plugins.length > 0" class="command-tags">
<el-tag v-for="dependency in selectedPlugin.dependent_plugins" :key="`dependent-${dependency.module_name}`" size="mini" type="success" effect="plain">
{% raw %}{{ `${dependency.name} (${dependency.status_label || dependency.status || '未知'})` }}{% endraw %}
</el-tag>
</div>
<div v-else class="entity-subtitle"></div>
</div>
</div>
</el-descriptions-item>
<el-descriptions-item label="命令列表" :span="2" v-if="selectedPlugin.commands && selectedPlugin.commands.length > 0">
<div class="command-tags">
<el-tag v-for="cmd in selectedPlugin.commands" :key="cmd" size="mini" class="command-tag">
{% raw %}{{ cmd }}{% endraw %}
</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">
<span class="config-overview-label">配置文件</span>
<span class="config-overview-value">{% raw %}{{ selectedPlugin.config_overview.exists ? '存在' : '缺失' }}{% endraw %}</span>
</div>
<div class="config-overview-item">
<span class="config-overview-label">解析状态</span>
<span class="config-overview-value">{% raw %}{{ selectedPlugin.config_overview.parse_ok ? '正常' : '失败' }}{% endraw %}</span>
</div>
<div class="config-overview-item">
<span class="config-overview-label">配置分组</span>
<span class="config-overview-value">{% raw %}{{ selectedPlugin.config_overview.section_count || 0 }}{% endraw %}</span>
</div>
<div class="config-overview-item">
<span class="config-overview-label">敏感字段</span>
<span class="config-overview-value">{% raw %}{{ selectedPlugin.config_overview.sensitive_field_count || 0 }}{% endraw %}</span>
</div>
</div>
<div class="entity-subtitle" style="margin-top: 8px;">
{% raw %}{{ selectedPlugin.config_path || '未声明配置路径' }}{% endraw %}
</div>
<div class="entity-subtitle" v-if="selectedPlugin.config_overview.parse_error">
{% raw %}{{ `解析错误:${selectedPlugin.config_overview.parse_error}` }}{% endraw %}
</div>
</el-descriptions-item>
<el-descriptions-item label="执行保护" :span="2" v-if="selectedPlugin.execution_guard">
<div class="config-overview-grid">
<div class="config-overview-item">
<span class="config-overview-label">熔断状态</span>
<span class="config-overview-value">{% raw %}{{ selectedPlugin.execution_guard.circuit_state || 'closed' }}{% endraw %}</span>
</div>
<div class="config-overview-item">
<span class="config-overview-label">连续失败</span>
<span class="config-overview-value">{% raw %}{{ selectedPlugin.execution_guard.consecutive_failures || 0 }}{% endraw %}</span>
</div>
<div class="config-overview-item">
<span class="config-overview-label">连续超时</span>
<span class="config-overview-value">{% raw %}{{ selectedPlugin.execution_guard.consecutive_timeouts || 0 }}{% endraw %}</span>
</div>
<div class="config-overview-item">
<span class="config-overview-label">恢复剩余</span>
<span class="config-overview-value">{% raw %}{{ `${selectedPlugin.execution_guard.open_remaining_seconds || 0}s` }}{% endraw %}</span>
</div>
</div>
</el-descriptions-item>
<el-descriptions-item label="执行表现" :span="2" v-if="selectedPlugin.execution_summary">
<div class="config-overview-grid">
<div class="config-overview-item">
<span class="config-overview-label">执行状态</span>
<span class="config-overview-value">{% raw %}{{ executionLabel(selectedPlugin.execution_summary.status) }}{% endraw %}</span>
</div>
<div class="config-overview-item">
<span class="config-overview-label">累计执行</span>
<span class="config-overview-value">{% raw %}{{ selectedPlugin.execution_summary.total_executions || 0 }}{% endraw %}</span>
</div>
<div class="config-overview-item">
<span class="config-overview-label">成功率</span>
<span class="config-overview-value">{% raw %}{{ formatPercent(selectedPlugin.execution_summary.success_rate) }}{% endraw %}</span>
</div>
<div class="config-overview-item">
<span class="config-overview-label">超时率</span>
<span class="config-overview-value">{% raw %}{{ formatPercent(selectedPlugin.execution_summary.timeout_rate) }}{% endraw %}</span>
</div>
<div class="config-overview-item">
<span class="config-overview-label">最近耗时</span>
<span class="config-overview-value">{% raw %}{{ formatDurationMs(selectedPlugin.execution_summary.last_process_time_ms) }}{% endraw %}</span>
</div>
<div class="config-overview-item">
<span class="config-overview-label">最近成功</span>
<span class="config-overview-value">{% raw %}{{ selectedPlugin.execution_summary.last_success_at_text || '-' }}{% endraw %}</span>
</div>
<div class="config-overview-item">
<span class="config-overview-label">最近失败</span>
<span class="config-overview-value">{% raw %}{{ selectedPlugin.execution_summary.last_failure_at_text || '-' }}{% endraw %}</span>
</div>
<div class="config-overview-item">
<span class="config-overview-label">最近错误</span>
<span class="config-overview-value">{% raw %}{{ selectedPlugin.execution_summary.last_error_message || '无' }}{% endraw %}</span>
</div>
</div>
<div class="entity-subtitle" style="margin-top: 8px;">
{% raw %}{{ selectedPlugin.execution_summary.summary || '暂无执行摘要' }}{% endraw %}
</div>
</el-descriptions-item>
<el-descriptions-item label="治理诊断" :span="2" v-if="selectedPlugin.governance_diagnostics">
<div v-if="selectedPlugin.governance_diagnostics.length > 0" class="diagnostic-list">
<div
v-for="(diagnostic, index) in selectedPlugin.governance_diagnostics"
:key="`${diagnostic.code}-${index}`"
class="diagnostic-item">
<el-tag :type="governanceTagType(diagnostic.level)" size="mini">
{% raw %}{{ governanceLabel(diagnostic.level) }}{% endraw %}
</el-tag>
<span class="diagnostic-text">{% raw %}{{ diagnostic.message }}{% endraw %}</span>
</div>
</div>
<span v-else>暂无治理诊断项</span>
</el-descriptions-item>
<el-descriptions-item label="配置信息" :span="2" v-if="selectedPlugin.config">
<div class="config-container">
<div class="config-actions">
<el-button type="primary" size="mini" @click="editConfig" :disabled="isEditingConfig">编辑配置</el-button>
<el-button type="success" size="mini" @click="saveConfig" v-if="isEditingConfig">保存配置</el-button>
<el-button type="info" size="mini" @click="cancelEditConfig" v-if="isEditingConfig">取消</el-button>
</div>
<div v-if="!isEditingConfig">
<pre>{% raw %}{{ selectedPlugin.configText }}{% endraw %}</pre>
</div>
<div v-else>
<el-input
type="textarea"
v-model="editedConfig"
:rows="12"
placeholder="请输入TOML格式的配置"
class="config-editor">
</el-input>
<div class="config-error" v-if="configError">{% raw %}{{ configError }}{% endraw %}</div>
</div>
</div>
</el-descriptions-item>
</el-descriptions>
</div>
</el-dialog>
<el-dialog title="插件群状态" :visible.sync="pluginGroupStatusVisible" :width="groupStatusDialogWidth" top="5vh">
<div class="plugin-group-status-dialog" v-loading="groupStatusLoading">
<div v-if="pluginGroupStatusData" class="group-status-header">
<div class="group-status-title">
{% raw %}{{ pluginGroupStatusData.plugin_name || '未知插件' }}{% endraw %}
<span class="group-status-subtitle">
{% raw %}{{ `共 ${pluginGroupStatusData.total_group_count || 0} 个群` }}{% endraw %}
</span>
</div>
<div class="group-status-summary">
<el-tag size="small" type="success">已开启 {% raw %}{{ pluginGroupStatusData.enabled_count || 0 }}{% endraw %}</el-tag>
<el-tag size="small" type="info">未开启 {% raw %}{{ pluginGroupStatusData.disabled_count || 0 }}{% endraw %}</el-tag>
</div>
</div>
<el-alert
v-if="pluginGroupStatusData && !pluginGroupStatusData.supports_group_switch"
title="该插件未接入群级开关能力,当前仅展示群列表,状态默认为未开启。"
type="warning"
:closable="false"
show-icon
class="group-status-alert">
</el-alert>
<el-row :gutter="16" v-if="pluginGroupStatusData">
<el-col :xs="24" :sm="24" :md="12">
<el-card shadow="never" class="group-status-card">
<div slot="header" class="group-status-card-header">
<span>已开启群</span>
<el-tag size="mini" type="success">{% raw %}{{ pluginGroupStatusData.enabled_count || 0 }}{% endraw %}</el-tag>
</div>
<!-- 桌面端保留双列表格,移动端单独渲染群卡片,避免群 ID 列挤出屏幕。 -->
<el-table
v-if="!isMobileViewport"
:data="pluginGroupStatusData.enabled_groups || []"
size="mini"
max-height="420"
empty-text="暂无已开启群">
<el-table-column label="群名称" min-width="170">
<template slot-scope="scope">
{% raw %}{{ scope.row.group_name || scope.row.group_id }}{% endraw %}
</template>
</el-table-column>
<el-table-column prop="group_id" label="群ID" min-width="210" show-overflow-tooltip></el-table-column>
<el-table-column label="操作" width="100" align="center">
<template slot-scope="scope">
<el-button
size="mini"
type="danger"
plain
:disabled="groupStatusLoading || !pluginGroupStatusData.supports_group_switch"
@click="togglePluginGroupSwitch(scope.row, false)">
关闭
</el-button>
</template>
</el-table-column>
</el-table>
<div v-else class="mobile-group-list">
<div v-if="!(pluginGroupStatusData.enabled_groups || []).length" class="mobile-empty-state">暂无已开启群</div>
<el-card
v-for="group in (pluginGroupStatusData.enabled_groups || [])"
:key="`enabled-${group.group_id}`"
class="mobile-group-card"
shadow="never">
<div class="mobile-group-card__title">{% raw %}{{ group.group_name || group.group_id }}{% endraw %}</div>
<div class="mobile-group-card__id">群ID{% raw %}{{ group.group_id }}{% endraw %}</div>
<div class="mobile-group-card__actions">
<el-button
size="mini"
type="danger"
plain
:disabled="groupStatusLoading || !pluginGroupStatusData.supports_group_switch"
@click="togglePluginGroupSwitch(group, false)">
关闭
</el-button>
</div>
</el-card>
</div>
</el-card>
</el-col>
<el-col :xs="24" :sm="24" :md="12">
<el-card shadow="never" class="group-status-card">
<div slot="header" class="group-status-card-header">
<span>未开启群</span>
<el-tag size="mini" type="info">{% raw %}{{ pluginGroupStatusData.disabled_count || 0 }}{% endraw %}</el-tag>
</div>
<el-table
v-if="!isMobileViewport"
:data="pluginGroupStatusData.disabled_groups || []"
size="mini"
max-height="420"
empty-text="暂无未开启群">
<el-table-column label="群名称" min-width="170">
<template slot-scope="scope">
{% raw %}{{ scope.row.group_name || scope.row.group_id }}{% endraw %}
</template>
</el-table-column>
<el-table-column prop="group_id" label="群ID" min-width="210" show-overflow-tooltip></el-table-column>
<el-table-column label="操作" width="100" align="center">
<template slot-scope="scope">
<el-button
size="mini"
type="success"
plain
:disabled="groupStatusLoading || !pluginGroupStatusData.supports_group_switch"
@click="togglePluginGroupSwitch(scope.row, true)">
开启
</el-button>
</template>
</el-table-column>
</el-table>
<div v-else class="mobile-group-list">
<div v-if="!(pluginGroupStatusData.disabled_groups || []).length" class="mobile-empty-state">暂无未开启群</div>
<el-card
v-for="group in (pluginGroupStatusData.disabled_groups || [])"
:key="`disabled-${group.group_id}`"
class="mobile-group-card"
shadow="never">
<div class="mobile-group-card__title">{% raw %}{{ group.group_name || group.group_id }}{% endraw %}</div>
<div class="mobile-group-card__id">群ID{% raw %}{{ group.group_id }}{% endraw %}</div>
<div class="mobile-group-card__actions">
<el-button
size="mini"
type="success"
plain
:disabled="groupStatusLoading || !pluginGroupStatusData.supports_group_switch"
@click="togglePluginGroupSwitch(group, true)">
开启
</el-button>
</div>
</el-card>
</div>
</el-card>
</el-col>
</el-row>
</div>
</el-dialog>
</div>
{% endblock %}
{% block scripts %}
<script>
new Vue({
el: '#app',
mixins: [baseApp],
data() {
return {
plugins: [],
selectedPlugin: null,
pluginInfoVisible: false,
loading: false,
isEditingConfig: false,
editedConfig: '',
configError: '',
configFormat: 'toml',
pluginGroupStatusVisible: false,
groupStatusLoading: false,
pluginGroupStatusData: null,
currentGroupStatusPlugin: null,
// 使用响应式视口状态切换移动端卡片布局,避免仅靠 CSS 隐藏后仍渲染宽表格。
isMobileViewport: false
}
},
computed: {
runningPluginsCount() {
return this.plugins.filter(plugin => plugin.status === 'RUNNING').length;
},
stoppedPluginsCount() {
return this.plugins.filter(plugin => plugin.status !== 'RUNNING').length;
},
governanceRiskCount() {
return (this.plugins || []).filter(plugin => ['warning', 'error'].includes((plugin.governance_status || '').toLowerCase())).length;
},
executionRiskCount() {
// 这里把执行风险单独统计出来,和治理告警区分开:
// 治理告警偏配置/依赖/加载问题,执行风险偏运行过程中的失败、超时与熔断。
return (this.plugins || []).filter(plugin => ['warning', 'error'].includes((((plugin.execution_summary || {}).status) || '').toLowerCase())).length;
},
openCircuitCount() {
return (this.plugins || []).filter(plugin => ((((plugin.execution_summary || {}).circuit_state) || '').toLowerCase() === 'open')).length;
},
topRiskPlugins() {
// 风险排行优先按熔断状态、执行状态和连续失败次数排序,
// 让页面顶部尽量把“最值得先排查”的插件顶上来。
const statusPriority = {
error: 0,
warning: 1,
info: 2,
healthy: 3
};
return (this.plugins || [])
.filter(plugin => ['warning', 'error'].includes((((plugin.execution_summary || {}).status) || '').toLowerCase()))
.slice()
.sort((left, right) => {
const leftSummary = left.execution_summary || {};
const rightSummary = right.execution_summary || {};
const leftPriority = statusPriority[(leftSummary.status || 'info').toLowerCase()];
const rightPriority = statusPriority[(rightSummary.status || 'info').toLowerCase()];
return (
(typeof leftPriority === 'number' ? leftPriority : 9) - (typeof rightPriority === 'number' ? rightPriority : 9)
|| Number(rightSummary.consecutive_failures || 0) - Number(leftSummary.consecutive_failures || 0)
|| Number(rightSummary.failure_count_total || 0) - Number(leftSummary.failure_count_total || 0)
|| Number(rightSummary.timeout_count_total || 0) - Number(leftSummary.timeout_count_total || 0)
);
})
.slice(0, 5);
},
topDependencyCorePlugins() {
// 核心依赖插件优先按“被多少插件依赖”排序,
// 这样最容易形成单点影响的基础插件会排在前面。
return (this.plugins || [])
.filter(plugin => Number(((plugin.dependency_summary || {}).dependent_count) || 0) > 0)
.slice()
.sort((left, right) => {
return (
Number(((right.dependency_summary || {}).dependent_count) || 0) - Number(((left.dependency_summary || {}).dependent_count) || 0)
|| Number(((right.dependency_summary || {}).declared_count) || 0) - Number(((left.dependency_summary || {}).declared_count) || 0)
);
})
.slice(0, 5);
},
pluginsWithMissingDependencies() {
return (this.plugins || [])
.filter(plugin => Number(((plugin.dependency_summary || {}).missing_count) || 0) > 0)
.slice()
.sort((left, right) => {
return Number(((right.dependency_summary || {}).missing_count) || 0) - Number(((left.dependency_summary || {}).missing_count) || 0);
})
.slice(0, 5);
},
slowestPlugins() {
// 慢插件排行只看有执行样本的插件,避免未执行插件把榜单冲掉。
return (this.plugins || [])
.filter(plugin => Number((plugin.execution_summary || {}).total_executions || 0) > 0)
.slice()
.sort((left, right) => {
return Number((right.execution_summary || {}).last_process_time_ms || 0) - Number((left.execution_summary || {}).last_process_time_ms || 0);
})
.slice(0, 5);
},
// 弹窗宽度按视口分级收缩,保证手机上弹窗内容不会贴边或继续触发横向溢出。
pluginInfoDialogWidth() {
return this.isMobileViewport ? '94%' : '64%';
},
groupStatusDialogWidth() {
return this.isMobileViewport ? '94%' : '72%';
},
pluginDescriptionsColumn() {
return this.isMobileViewport ? 1 : 2;
}
},
mounted() {
this.currentView = '11';
// 首次进入页面就同步一次屏幕宽度,确保移动端不会先闪出桌面表格。
this.updateViewportState();
window.addEventListener('resize', this.updateViewportState);
this.loadPlugins();
},
beforeDestroy() {
// 页面销毁时移除监听,避免重复绑定造成状态更新和内存占用问题。
window.removeEventListener('resize', this.updateViewportState);
},
methods: {
updateViewportState() {
// 这里统一以 768px 作为移动端断点,和常见后台管理布局断点保持一致。
this.isMobileViewport = window.innerWidth <= 768;
},
pluginStatusTagType(status) {
const normalizedStatus = String(status || '').toUpperCase();
if (normalizedStatus === 'RUNNING') return 'success';
if (normalizedStatus === 'ERROR') return 'danger';
if (normalizedStatus === 'LOADED') return 'warning';
return 'info';
},
pluginStatusLabel(plugin) {
if (plugin && plugin.status_label) return plugin.status_label;
const normalizedStatus = String((plugin && plugin.status) || '').toUpperCase();
const mapping = {
RUNNING: '运行中',
STOPPED: '已停用',
LOADED: '已加载',
UNLOADED: '未加载',
ERROR: '异常',
DISCOVERED: '待处理'
};
return mapping[normalizedStatus] || '未知';
},
governanceTagType(level) {
const normalizedLevel = String(level || '').toLowerCase();
if (normalizedLevel === 'error') return 'danger';
if (normalizedLevel === 'warning') return 'warning';
if (normalizedLevel === 'healthy') return 'success';
return 'info';
},
governanceLabel(level) {
const normalizedLevel = String(level || '').toLowerCase();
const mapping = {
healthy: '健康',
warning: '告警',
error: '异常',
info: '提示'
};
return mapping[normalizedLevel] || '提示';
},
executionTagType(level) {
const normalizedLevel = String(level || '').toLowerCase();
if (normalizedLevel === 'error') return 'danger';
if (normalizedLevel === 'warning') return 'warning';
if (normalizedLevel === 'healthy') return 'success';
return 'info';
},
executionLabel(level) {
const normalizedLevel = String(level || '').toLowerCase();
const mapping = {
healthy: '稳定',
warning: '需关注',
error: '高风险',
info: '暂无样本'
};
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);
const infoCount = Number((plugin && plugin.governance_info_count) || 0);
if (errorCount > 0 || warningCount > 0) {
return `错误 ${errorCount} / 告警 ${warningCount}`;
}
if (infoCount > 0) {
return `提示 ${infoCount}`;
}
return '暂无治理问题';
},
formatPercent(value) {
const normalizedValue = Number(value || 0);
if (!Number.isFinite(normalizedValue)) return '0.00%';
return `${normalizedValue.toFixed(2)}%`;
},
formatDurationMs(value) {
const normalizedValue = Number(value || 0);
if (!Number.isFinite(normalizedValue) || normalizedValue <= 0) return '-';
return `${normalizedValue.toFixed(2)} ms`;
},
buildDependencySummaryText(plugin) {
const dependencySummary = (plugin && plugin.dependency_summary) || {};
const declaredCount = Number(dependencySummary.declared_count || 0);
const missingCount = Number(dependencySummary.missing_count || 0);
const dependentCount = Number(dependencySummary.dependent_count || 0);
if (declaredCount <= 0 && dependentCount <= 0) {
return '无依赖关系';
}
if (missingCount > 0) {
return `声明 ${declaredCount} 个,缺失 ${missingCount}`;
}
if (dependentCount > 0) {
return `${dependentCount} 个插件依赖`;
}
return `已解析 ${declaredCount} 个依赖`;
},
buildDependencyCoreSummary(plugin) {
const dependencySummary = (plugin && plugin.dependency_summary) || {};
return `当前被 ${(dependencySummary.dependent_count || 0)} 个插件依赖,自身声明 ${(dependencySummary.declared_count || 0)} 个依赖。`;
},
buildMissingDependencySummary(plugin) {
const missingDependencies = ((plugin && plugin.missing_dependencies) || []).map(item => item.name).filter(Boolean);
if (!missingDependencies.length) {
return '当前没有缺失依赖。';
}
return `缺失依赖:${missingDependencies.join('、')}`;
},
loadPlugins() {
this.loading = true;
axios.get('/api/plugins')
.then(response => {
if (response.data.success) {
this.plugins = response.data.data || [];
} else {
this.$message.error(response.data.message || '加载插件列表失败');
}
})
.catch(error => {
console.error('加载插件列表出错:', error);
this.$message.error('加载插件列表出错');
})
.finally(() => {
this.loading = false;
});
},
refreshPlugins() {
this.loadPlugins();
this.$message.success('插件列表已刷新');
},
togglePluginStatus(plugin) {
const action = plugin.status === 'RUNNING' ? 'disable' : 'enable';
const actionText = plugin.status === 'RUNNING' ? '禁用' : '启用';
this.$confirm(`确定要${actionText}插件 "${plugin.name}" 吗?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
axios.post(`/api/plugins/${action}`, {
plugin_name: plugin.module_name
})
.then(response => {
if (response.data.success) {
this.$message.success(`${actionText}插件成功`);
this.loadPlugins();
} else {
this.$message.error(response.data.message || `${actionText}插件失败`);
}
})
.catch(error => {
console.error(`${actionText}插件出错:`, error);
this.$message.error(`${actionText}插件出错`);
});
}).catch(() => {
this.$message.info('已取消操作');
});
},
reloadPlugin(plugin) {
this.$confirm(`确定要重载插件 "${plugin.name}" 吗?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
axios.post('/api/plugins/reload', {
plugin_name: plugin.module_name
})
.then(response => {
if (response.data.success) {
this.$message.success('重载插件成功');
this.loadPlugins();
} else {
this.$message.error(response.data.message || '重载插件失败');
}
})
.catch(error => {
console.error('重载插件出错:', error);
this.$message.error('重载插件出错');
});
}).catch(() => {
this.$message.info('已取消操作');
});
},
editConfig() {
this.isEditingConfig = true;
this.editedConfig = this.selectedPlugin.configText || '';
this.configError = '';
},
cancelEditConfig() {
this.isEditingConfig = false;
this.editedConfig = '';
this.configError = '';
},
saveConfig() {
try {
axios.post('/api/plugins/config/update', {
plugin_name: this.selectedPlugin.module_name,
config_text: this.editedConfig,
format: 'toml'
})
.then(response => {
if (response.data.success) {
this.$message.success('配置保存成功');
this.isEditingConfig = false;
this.selectedPlugin.configText = this.editedConfig;
// 保存成功后立即重新拉取详情:
// 1. 同步刷新治理诊断、配置概览和内存中的插件配置快照;
// 2. 避免页面上继续停留在旧的健康状态;
// 3. 这样后续是否重载插件,用户都能先看到“配置文本已通过校验并落盘”。
this.showPluginInfo(this.selectedPlugin);
this.$confirm('配置已保存,是否要重载插件以应用新配置?', '提示', {
confirmButtonText: '重载插件',
cancelButtonText: '稍后手动重载',
type: 'info'
}).then(() => {
this.reloadPlugin(this.selectedPlugin);
}).catch(() => {
this.$message.info('您可以稍后手动重载插件以应用新配置');
});
} else {
this.configError = response.data.message || '保存配置失败';
}
})
.catch(error => {
console.error('保存配置出错:', error);
this.configError = '保存配置出错: ' + (((error.response || {}).data || {}).message || error.message);
});
} catch (e) {
this.configError = '处理配置时出错: ' + e.message;
}
},
showPluginInfo(plugin) {
axios.get(`/api/plugins/info?plugin_name=${plugin.module_name}`)
.then(response => {
if (response.data.success) {
this.selectedPlugin = response.data.data;
if (this.selectedPlugin.configText) {
this.pluginInfoVisible = true;
this.isEditingConfig = false;
this.editedConfig = '';
this.configError = '';
return;
}
axios.get(`/api/plugins/config/raw?plugin_name=${plugin.module_name}`)
.then(configResponse => {
if (configResponse.data.success) {
this.selectedPlugin.configText = configResponse.data.data;
this.configFormat = configResponse.data.format || 'toml';
} else {
this.selectedPlugin.configText = '# 无法获取原始配置文件';
console.error('获取配置文件失败:', configResponse.data.message);
}
this.pluginInfoVisible = true;
this.isEditingConfig = false;
this.editedConfig = '';
this.configError = '';
})
.catch(error => {
console.error('获取配置文件出错:', error);
this.selectedPlugin.configText = '# 获取配置文件时出错';
this.pluginInfoVisible = true;
});
} else {
this.$message.error(response.data.message || '获取插件详情失败');
}
})
.catch(error => {
console.error('获取插件详情出错:', error);
this.$message.error('获取插件详情出错');
});
},
showPluginGroupStatus(plugin) {
// 打开弹窗前先进入加载态,避免用户在慢接口场景下看到旧数据。
this.pluginGroupStatusVisible = true;
this.groupStatusLoading = true;
this.pluginGroupStatusData = null;
// 记录当前正在查看群状态的插件,供“开启/关闭后刷新”复用。
this.currentGroupStatusPlugin = plugin;
// 统一使用插件模块名查询,和启用/禁用/重载接口参数保持一致。
return axios.get('/api/plugins/group_status', {
params: {
plugin_name: plugin.module_name
}
})
.then(response => {
if (response.data.success) {
// 接口返回已按“已开启/未开启”拆分好,前端仅做展示。
this.pluginGroupStatusData = response.data.data || {
enabled_groups: [],
disabled_groups: [],
enabled_count: 0,
disabled_count: 0,
total_group_count: 0,
supports_group_switch: false
};
} else {
this.$message.error(response.data.message || '获取插件群状态失败');
this.pluginGroupStatusVisible = false;
}
})
.catch(error => {
console.error('获取插件群状态出错:', error);
this.$message.error('获取插件群状态出错');
this.pluginGroupStatusVisible = false;
})
.finally(() => {
this.groupStatusLoading = false;
});
},
togglePluginGroupSwitch(group, enable) {
if (!this.currentGroupStatusPlugin || !group || !group.group_id) {
this.$message.error('缺少必要参数,无法切换');
return;
}
const actionText = enable ? '开启' : '关闭';
const pluginName = this.currentGroupStatusPlugin.name || this.currentGroupStatusPlugin.module_name;
const groupName = group.group_name || group.group_id;
this.$confirm(`确定要${actionText}群 "${groupName}" 的插件 "${pluginName}" 吗?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.groupStatusLoading = true;
// 调用后端切换接口,后端内部复用 GroupBotManager 的原有缓存+Redis策略。
axios.post('/api/plugins/group_status/toggle', {
plugin_name: this.currentGroupStatusPlugin.module_name,
group_id: group.group_id,
status: enable ? 'enabled' : 'disabled'
})
.then(response => {
if (response.data.success) {
this.$message.success(`${actionText}成功`);
// 切换后重新拉取当前插件群状态,保证统计与列表一致。
return this.showPluginGroupStatus(this.currentGroupStatusPlugin);
} else {
this.$message.error(response.data.message || `${actionText}失败`);
}
})
.catch(error => {
console.error(`${actionText}插件群状态出错:`, error);
this.$message.error(`${actionText}插件群状态出错`);
})
.finally(() => {
this.groupStatusLoading = false;
});
}).catch(() => {
this.$message.info('已取消操作');
});
}
}
});
</script>
<style>
.page-shell {
display: flex;
flex-direction: column;
gap: 16px;
}
.page-hero {
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 18px;
padding: 24px 26px;
border-radius: 24px;
background: linear-gradient(135deg, rgba(79,70,229,0.10), rgba(59,130,246,0.08), rgba(255,255,255,0.9));
border: 1px solid rgba(148, 163, 184, 0.16);
box-shadow: 0 18px 40px rgba(15, 23, 42, 0.06);
}
.page-eyebrow {
font-size: 12px;
text-transform: uppercase;
letter-spacing: .08em;
color: #6366f1;
font-weight: 700;
margin-bottom: 8px;
}
.page-hero-copy h1 {
font-size: 30px;
line-height: 1.1;
margin-bottom: 10px;
color: #0f172a;
}
.page-hero-copy p {
color: #64748b;
font-size: 14px;
}
.overview-grid .el-col { margin-bottom: 16px; }
.insight-grid .el-col { margin-bottom: 16px; }
.overview-card { min-height: 112px; }
.overview-card--primary {
background: linear-gradient(180deg, rgba(79,70,229,0.10), rgba(255,255,255,0.94)) !important;
}
.overview-card--soft {
background: linear-gradient(180deg, rgba(59,130,246,0.08), rgba(255,255,255,0.94)) !important;
}
.overview-label { font-size: 13px; color: #64748b; margin-bottom: 14px; }
.overview-value { font-size: 30px; font-weight: 700; color: #0f172a; margin-bottom: 10px; }
.overview-note { font-size: 12px; color: #94a3b8; }
.workspace-header {
display: flex; align-items: center; justify-content: space-between; gap: 16px;
}
.workspace-header h3 { font-size: 18px; margin-bottom: 4px; }
.workspace-header p { font-size: 13px; color: #64748b; }
.rank-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.rank-item {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 14px;
border-radius: 16px;
background: rgba(248,250,252,0.82);
border: 1px solid rgba(148,163,184,0.12);
}
.rank-item__index {
width: 28px;
height: 28px;
border-radius: 50%;
display: inline-flex;
align-items: center;
justify-content: center;
background: rgba(79,70,229,0.10);
color: #4f46e5;
font-size: 12px;
font-weight: 700;
flex-shrink: 0;
}
.rank-item__content {
flex: 1;
min-width: 0;
}
.rank-item__title-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-bottom: 8px;
}
.rank-item__title,
.rank-item__value {
font-size: 14px;
font-weight: 700;
color: #0f172a;
}
.rank-item__summary {
font-size: 13px;
line-height: 1.7;
color: #475569;
word-break: break-word;
}
.rank-item__meta {
display: flex;
flex-wrap: wrap;
gap: 8px 14px;
margin-top: 8px;
font-size: 12px;
color: #94a3b8;
}
.entity-cell { display: flex; align-items: center; gap: 12px; }
.entity-badge {
width: 30px; height: 30px; border-radius: 50%; display: inline-flex; align-items: center;
justify-content: center; background: rgba(79,70,229,0.10); color: #4f46e5; font-size: 12px;
font-weight: 700; flex-shrink: 0;
}
.entity-title { font-size: 14px; font-weight: 600; color: #0f172a; }
.entity-subtitle { margin-top: 4px; font-size: 12px; color: #94a3b8; }
.action-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.plugin-detail-container { max-height: 70vh; overflow-y: auto; }
.plugin-descriptions { width: 100%; }
.dialog-intro { margin-bottom: 14px; color: #64748b; font-size: 13px; }
.config-container {
max-height: 320px; overflow-y: auto; background: rgba(248,250,252,0.82); border: 1px solid rgba(148,163,184,0.12);
border-radius: 14px; padding: 12px; font-size: 12px; color: #334155;
}
.config-container pre { margin: 0; white-space: pre-wrap; word-break: break-word; }
.command-tags { display: flex; flex-wrap: wrap; gap: 6px; }
.command-tags--compact { justify-content: center; }
.command-tag { margin: 0 !important; }
.config-actions { margin-bottom: 10px; display: flex; gap: 10px; }
.config-editor { font-family: monospace; font-size: 12px; }
.config-error { color: #ef4444; font-size: 12px; margin-top: 5px; }
.governance-cell {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
}
.governance-note {
font-size: 11px;
color: #94a3b8;
line-height: 1.4;
}
.execution-cell {
display: flex;
flex-direction: column;
gap: 6px;
}
.execution-cell__head {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.execution-cell__metric {
font-size: 12px;
color: #64748b;
font-weight: 600;
}
.execution-cell__summary {
font-size: 12px;
color: #94a3b8;
line-height: 1.6;
word-break: break-word;
}
.config-overview-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
gap: 10px;
}
.dependency-panels {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 12px;
margin-top: 12px;
}
.dependency-panel {
padding: 12px;
border-radius: 14px;
background: rgba(248,250,252,0.82);
border: 1px solid rgba(148,163,184,0.12);
}
.dependency-panel__title {
font-size: 13px;
font-weight: 700;
color: #334155;
margin-bottom: 8px;
}
.config-overview-item {
display: flex;
flex-direction: column;
gap: 4px;
padding: 10px 12px;
border-radius: 12px;
background: rgba(255,255,255,0.72);
border: 1px solid rgba(148,163,184,0.12);
}
.config-overview-label {
font-size: 12px;
color: #64748b;
}
.config-overview-value {
font-size: 14px;
font-weight: 600;
color: #0f172a;
}
.diagnostic-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.diagnostic-item {
display: flex;
align-items: flex-start;
gap: 10px;
padding: 10px 12px;
border-radius: 12px;
background: rgba(248,250,252,0.82);
border: 1px solid rgba(148,163,184,0.12);
}
.diagnostic-text {
flex: 1;
font-size: 13px;
color: #334155;
line-height: 1.6;
word-break: break-word;
}
.plugin-group-status-dialog { min-height: 240px; }
.mobile-plugin-list,
.mobile-group-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.mobile-plugin-card,
.mobile-group-card {
border-radius: 16px;
}
.mobile-plugin-card__header,
.mobile-group-card__actions {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 10px;
}
.mobile-plugin-card__header {
margin-bottom: 12px;
}
.mobile-plugin-card__meta {
display: flex;
flex-wrap: wrap;
gap: 8px 14px;
margin-bottom: 10px;
font-size: 12px;
color: #64748b;
}
.mobile-plugin-card__desc,
.mobile-group-card__id {
font-size: 13px;
line-height: 1.7;
color: #475569;
word-break: break-word;
}
.mobile-plugin-card__actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 14px;
}
.mobile-group-card__title {
font-size: 14px;
font-weight: 600;
color: #0f172a;
margin-bottom: 6px;
word-break: break-word;
}
.mobile-group-card__actions {
margin-top: 12px;
justify-content: flex-end;
}
.mobile-empty-state {
padding: 16px 12px;
text-align: center;
color: #94a3b8;
font-size: 13px;
background: rgba(248, 250, 252, 0.9);
border: 1px dashed rgba(148, 163, 184, 0.35);
border-radius: 14px;
}
.group-status-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 14px;
gap: 10px;
flex-wrap: wrap;
}
.group-status-title { font-size: 16px; font-weight: 600; color: #0f172a; }
.group-status-subtitle { margin-left: 8px; font-size: 12px; color: #64748b; font-weight: 500; }
.group-status-summary { display: flex; align-items: center; gap: 8px; }
.group-status-alert { margin-bottom: 12px; }
.group-status-card { border-radius: 12px; }
.group-status-card-header {
display: flex;
align-items: center;
justify-content: space-between;
font-weight: 600;
color: #334155;
}
/* 小屏重点做三件事:
1. 让头部和操作区纵向排布,避免按钮把容器撑宽;
2. 让弹窗内容与卡片内边距缩小,提升可视面积;
3. 让配置编辑区和标签内容可换行,不再出现页面级横向滚动。 */
@media (max-width: 768px) {
.page-hero {
flex-direction: column;
align-items: stretch;
padding: 18px 16px;
border-radius: 20px;
}
.page-hero-copy h1 {
font-size: 24px;
}
.page-hero-actions .el-button {
width: 100%;
}
.workspace-header {
flex-direction: column;
align-items: flex-start;
}
.workspace-card .el-card__body,
.group-status-card .el-card__body {
padding: 14px;
}
.entity-cell {
align-items: flex-start;
}
.entity-copy,
.group-status-title {
min-width: 0;
}
.entity-title,
.entity-subtitle,
.group-status-title,
.group-status-subtitle {
word-break: break-word;
}
.plugin-detail-container {
max-height: 72vh;
}
.config-container {
padding: 10px;
}
.config-actions {
flex-wrap: wrap;
}
.mobile-plugin-card__header {
flex-direction: column;
}
.rank-item__title-row {
flex-direction: column;
align-items: flex-start;
}
.mobile-plugin-card__actions .el-button,
.mobile-group-card__actions .el-button {
flex: 1 1 calc(50% - 8px);
min-width: 0;
margin-left: 0 !important;
}
.group-status-summary {
flex-wrap: wrap;
}
}
</style>
{% endblock %}