diff --git a/admin/dashboard/blueprints/plugin_routes.py b/admin/dashboard/blueprints/plugin_routes.py index 40d3dde..c8a31f7 100644 --- a/admin/dashboard/blueprints/plugin_routes.py +++ b/admin/dashboard/blueprints/plugin_routes.py @@ -7,42 +7,11 @@ from flask import Blueprint, request, jsonify, render_template, current_app from admin.dashboard.blueprints.auth import login_required from utils.robot_cmd.robot_command import GroupBotManager, PermissionStatus -from plugins.robot_menu.menu_render_tool import RobotMenuRenderTool # 创建蓝图 plugin_routes = Blueprint('plugin_routes', __name__) LOG = logger -# 后台命令索引页只复用“命令目录生成”能力,不需要图片渲染, -# 因此这里固定使用轻量 text 配置创建一个工具实例即可。 -_command_catalog_tool = RobotMenuRenderTool( - output_mode="text", - image_fallback_to_text=True, - image_render_timeout_seconds=30, - image_render_retries=1, - image_template_path="plugins/robot_menu/templates/menu_cards.html", - log=LOG, -) - - -def _build_group_options(server) -> list: - """构建后台命令索引页的群组选项列表。""" - group_ids = sorted(set(GroupBotManager.get_group_list() or [])) - options = [] - for group_id in group_ids: - group_name = "" - try: - group_name = server.contact_manager.get_nickname(group_id) or "" - except Exception: - group_name = "" - options.append( - { - "group_id": group_id, - "group_name": str(group_name or group_id), - } - ) - return options - # 机器人管理页面 @plugin_routes.route('/plugins_manage') @@ -51,61 +20,40 @@ def robot_management(): return render_template('plugins_manage.html') -@plugin_routes.route('/command_catalog') -@login_required -def command_catalog_page(): - """后台命令索引页面。""" - return render_template('command_catalog.html') - - @plugin_routes.route('/api/plugins', methods=['GET']) @login_required def get_plugins(): """获取所有插件列表""" try: server = current_app.dashboard_server - # 统一改为消费 PluginManager 的标准治理快照: - # 1. 这样既能覆盖“已加载插件”,也能覆盖“发现但加载失败/配置禁用”的模块; - # 2. 后台不必重复拼装版本、命令、依赖、配置健康等字段; - # 3. 后续继续补错误统计、性能排名时,也只需要在快照层扩展。 - plugin_list = server.plugin_manager.get_plugin_snapshots() + # 获取插件注册表 + plugins = server.plugin_registry.get_all_plugins() + + # 转换为前端需要的格式 + plugin_list = [] + for name, plugin in plugins.items(): + # 获取插件模块名 + try: + module_name = plugin.__class__.__module__.split('.')[-2] + except (IndexError, AttributeError): + module_name = "unknown" + + plugin_info = { + "name": plugin.name, + "module_name": module_name, + "version": getattr(plugin, 'version', 'N/A'), + "author": getattr(plugin, 'author', 'N/A'), + "description": getattr(plugin, 'description', 'N/A'), + "status": plugin.status.name if hasattr(plugin, 'status') else 'UNKNOWN' + } + plugin_list.append(plugin_info) + return jsonify({"success": True, "data": plugin_list}) except Exception as e: LOG.error(f"获取插件列表失败: {str(e)}", exc_info=True) return jsonify({"success": False, "message": f"获取插件列表失败: {str(e)}"}) -@plugin_routes.route('/api/plugins/command_catalog', methods=['GET']) -@login_required -def get_command_catalog(): - """获取后台命令索引数据。""" - try: - server = current_app.dashboard_server - group_id = str(request.args.get('group_id') or '').strip() - - # 后台命令索引默认站在“管理员”视角, - # 这样既能看到当前可用命令,也能看到未启用能力和管理指令。 - catalog = _command_catalog_tool.build_command_catalog_data( - group_id=group_id, - requester_id="dashboard_admin", - force_admin=True, - ) - data = { - **catalog, - "group_options": _build_group_options(server), - "summary": { - "available_manual_count": len(catalog.get("available_manual", []) or []), - "available_auto_count": len(catalog.get("available_auto", []) or []), - "unavailable_manual_count": len(catalog.get("unavailable_manual", []) or []), - "admin_command_count": len(catalog.get("admin_commands", []) or []), - }, - } - return jsonify({"success": True, "data": data}) - except Exception as e: - LOG.error(f"获取命令索引失败: {str(e)}", exc_info=True) - return jsonify({"success": False, "message": f"获取命令索引失败: {str(e)}"}) - - @plugin_routes.route('/api/plugins/group_status', methods=['GET']) @login_required def get_plugin_group_status(): @@ -245,10 +193,31 @@ def get_plugin_info(): if not plugin_name: return jsonify({"success": False, "message": "缺少插件名称参数"}) - plugin_info = server.plugin_manager.get_plugin_snapshot(plugin_name) - if not plugin_info: + # 获取插件管理器 + display_name, plugin = server.plugin_manager.find_plugin_by_name(plugin_name) + + if not plugin: return jsonify({"success": False, "message": f"未找到插件: {plugin_name}"}) + # 获取插件模块名 + try: + module_name = plugin.__class__.__module__.split('.')[-2] + except (IndexError, AttributeError): + module_name = "unknown" + + # 构建详细信息 + plugin_info = { + "name": plugin.name, + "module_name": module_name, + "version": getattr(plugin, 'version', 'N/A'), + "author": getattr(plugin, 'author', 'N/A'), + "description": getattr(plugin, 'description', 'N/A'), + "status": plugin.status.name if hasattr(plugin, 'status') else 'UNKNOWN', + "command_prefix": getattr(plugin, 'command_prefix', ''), + "commands": getattr(plugin, 'commands', []), + "config": getattr(plugin, '_config', {}) + } + return jsonify({"success": True, "data": plugin_info}) except Exception as e: LOG.error(f"获取插件详情失败: {str(e)}", exc_info=True) @@ -266,14 +235,9 @@ def enable_plugin(): if not plugin_name: return jsonify({"success": False, "message": "缺少插件名称参数"}) - # 已加载插件直接启动;尚未加载的插件则先尝试加载,再进入启动流程。 - display_name, plugin = server.plugin_manager.find_plugin_by_name(plugin_name) - if not plugin: - plugin = server.plugin_manager.load_plugin(plugin_name) - if plugin: - display_name = plugin.name - - if plugin and server.plugin_manager.start_plugin(display_name or plugin_name): + # 获取插件管理器 + # 启用插件 + if server.plugin_manager.start_plugin(plugin_name): return jsonify({"success": True, "message": f"插件 {plugin_name} 启用成功"}) else: return jsonify({"success": False, "message": f"插件 {plugin_name} 启用失败"}) @@ -314,14 +278,8 @@ def reload_plugin(): if not plugin_name: return jsonify({"success": False, "message": "缺少插件名称参数"}) - # 已加载插件优先走重载;若当前未加载,则退化为“重新尝试加载并启动”。 - display_name, plugin = server.plugin_manager.find_plugin_by_name(plugin_name) - if plugin: - reloaded_plugin = server.plugin_manager.reload_plugin(plugin_name) - else: - reloaded_plugin = server.plugin_manager.load_plugin(plugin_name) - if reloaded_plugin: - server.plugin_manager.start_plugin(reloaded_plugin.name) + # 重载插件 + reloaded_plugin = server.plugin_manager.reload_plugin(plugin_name) if reloaded_plugin: return jsonify({"success": True, "message": f"插件 {plugin_name} 重载成功"}) @@ -342,11 +300,16 @@ def get_raw_plugin_config(): if not plugin_name: return jsonify({"success": False, "message": "缺少插件名称参数"}) - plugin_snapshot = server.plugin_manager.get_plugin_snapshot(plugin_name) - if not plugin_snapshot: + # 获取插件管理器 + + # 查找插件 + display_name, plugin = server.plugin_manager.find_plugin_by_name(plugin_name) + + if not plugin: return jsonify({"success": False, "message": f"未找到插件: {plugin_name}"}) - config_path = str(plugin_snapshot.get("config_path", "") or "").strip() + # 获取配置文件路径 + config_path = plugin.get_config_path() if not os.path.exists(config_path): return jsonify({"success": False, "message": f"配置文件不存在: {config_path}"}) @@ -386,29 +349,15 @@ def update_plugin_config(): if not plugin_name or config_text is None: return jsonify({"success": False, "message": "缺少必要参数"}) + # 查找插件 + # 获取插件管理器 display_name, plugin = server.plugin_manager.find_plugin_by_name(plugin_name) - plugin_snapshot = server.plugin_manager.get_plugin_snapshot(plugin_name) - if not plugin_snapshot: + + if not plugin: return jsonify({"success": False, "message": f"未找到插件: {plugin_name}"}) - config_path = str(plugin_snapshot.get("config_path", "") or "").strip() - if not config_path: - return jsonify({"success": False, "message": "插件未声明配置路径,暂不支持在线编辑"}) - - # 保存前先做格式校验: - # 1. 避免把坏 TOML 先写回磁盘,再让插件进入“文件已坏但提示成功”的状态; - # 2. 校验通过后再真正落盘,失败则保留线上旧配置; - # 3. 这也是插件治理中心第一阶段的“配置校验底座”。 - try: - if format_type == 'toml': - config_obj = toml.loads(config_text) - elif format_type == 'json': - config_obj = json.loads(config_text) - else: - return jsonify({"success": False, "message": f"不支持的配置格式: {format_type}"}) - except Exception as parse_error: - LOG.error(f"解析配置失败: {str(parse_error)}", exc_info=True) - return jsonify({"success": False, "message": f"配置格式校验失败: {str(parse_error)}"}) + # 获取配置文件路径 + config_path = plugin.get_config_path() # 确保配置目录存在 os.makedirs(os.path.dirname(config_path), exist_ok=True) @@ -417,11 +366,22 @@ def update_plugin_config(): with open(config_path, 'w', encoding='utf-8') as f: f.write(config_text) - # 若插件当前已加载,则同步刷新内存中的配置镜像,减少“保存后详情弹窗仍是旧配置”的困惑。 - if plugin: + # 解析配置并更新插件内部配置 + try: + if format_type == 'toml': + config_obj = toml.loads(config_text) + elif format_type == 'json': + config_obj = json.loads(config_text) + else: + return jsonify({"success": False, "message": f"不支持的配置格式: {format_type}"}) + + # 更新插件内部配置 plugin._config = config_obj - return jsonify({"success": True, "message": "配置已保存并通过格式校验"}) + return jsonify({"success": True, "message": "配置已保存"}) + except Exception as e: + LOG.error(f"解析配置失败: {str(e)}", exc_info=True) + return jsonify({"success": False, "message": f"配置已保存,但解析失败: {str(e)}"}) except Exception as e: LOG.error(f"更新插件配置失败: {str(e)}", exc_info=True) diff --git a/admin/dashboard/blueprints/plugin_schedules.py b/admin/dashboard/blueprints/plugin_schedules.py index 3f4a7b2..aed8c00 100644 --- a/admin/dashboard/blueprints/plugin_schedules.py +++ b/admin/dashboard/blueprints/plugin_schedules.py @@ -40,7 +40,7 @@ def api_list_schedules(): data = server.plugin_schedule_manager.list_schedules_with_runtime() # 后端统一格式化时间字段,避免前端出现 Fri, 17 Apr 2026 ... 这类 RFC 时间串。 for row in data: - for key in ("next_run_at", "last_run_at", "latest_success_at", "latest_failed_at", "created_at", "updated_at"): + for key in ("next_run_at", "last_run_at", "created_at", "updated_at"): if key in row: row[key] = _normalize_datetime_text(row.get(key)) return jsonify({"success": True, "data": data}) diff --git a/admin/dashboard/blueprints/system_jobs.py b/admin/dashboard/blueprints/system_jobs.py index 54fdfa3..d994e2f 100644 --- a/admin/dashboard/blueprints/system_jobs.py +++ b/admin/dashboard/blueprints/system_jobs.py @@ -21,40 +21,6 @@ def _normalize_datetime_text(value): return text -def _build_job_health_status(*, enabled: bool, running: bool, last_status: str, latest_success_at, latest_failure_summary: str) -> str: - """根据任务启停、运行态和历史结果输出后台可读的健康状态。""" - # 状态设计尽量贴近运维判断顺序: - # 1. 停用态单独标记,避免和“从未执行”混淆; - # 2. 执行中的任务优先展示 running,方便后台快速识别实时动作; - # 3. 最近一次执行失败时直接标记 failed,让异常任务在列表里一眼可见; - # 4. 有成功历史且最近不是失败时视为 healthy,否则落到 idle。 - if not enabled: - return "disabled" - if running: - return "running" - if str(last_status or "").strip().lower() == "failed": - return "failed" - if latest_success_at or str(last_status or "").strip().lower() == "success": - return "healthy" - if str(latest_failure_summary or "").strip(): - return "failed" - return "idle" - - -def _build_job_health_message(*, health_status: str, latest_success_at, latest_failure_summary: str) -> str: - """为后台列表生成一句简短的任务健康提示。""" - if health_status == "disabled": - return "任务已停用" - if health_status == "running": - return "任务正在执行中" - if health_status == "failed": - return str(latest_failure_summary or "最近一次执行失败").strip() - if health_status == "healthy": - success_text = _normalize_datetime_text(latest_success_at) - return f"最近成功于 {success_text}" if success_text else "任务近期执行正常" - return "暂无执行记录" - - @system_jobs_bp.route("/") @login_required def page_system_jobs(): @@ -68,31 +34,11 @@ def api_list_jobs(): db_rows = server.system_job_db.list_jobs() runtime_rows = async_job.get_jobs_snapshot() runtime_by_key = {row.get("job_key", ""): row for row in runtime_rows if row.get("job_key")} - job_keys = [str(row.get("job_key") or "").strip() for row in db_rows if str(row.get("job_key") or "").strip()] - latest_log_by_key = server.system_job_db.get_latest_logs_map(job_keys) - history_summary_by_key = server.system_job_db.get_job_history_summary_map(job_keys) result = [] for row in db_rows: job_key = row.get("job_key") runtime = runtime_by_key.get(job_key, {}) - latest_log = latest_log_by_key.get(job_key, {}) - history_summary = history_summary_by_key.get(job_key, {}) - last_status = runtime.get("last_status") or latest_log.get("status") or "never" - last_run_at = runtime.get("last_run_at") or latest_log.get("triggered_at") - last_error = runtime.get("last_error") or "" - if not last_error and str(last_status or "").strip().lower() == "failed": - last_error = ( - str(latest_log.get("summary") or "").strip() - or str(history_summary.get("latest_failure_summary") or "").strip() - ) - health_status = _build_job_health_status( - enabled=bool(row.get("enabled", 0)), - running=bool(runtime.get("running", False)), - last_status=str(last_status or ""), - latest_success_at=history_summary.get("latest_success_at"), - latest_failure_summary=str(history_summary.get("latest_failure_summary") or ""), - ) result.append( { "job_key": job_key, @@ -105,26 +51,14 @@ def api_list_jobs(): "runtime_enabled": runtime.get("enabled"), "running": runtime.get("running", False), "trigger_text": runtime.get("trigger_text", ""), - "last_run_at": _normalize_datetime_text(last_run_at), - "last_status": last_status, - "last_error": last_error, - "last_duration_ms": runtime.get("last_duration_ms") or latest_log.get("duration_ms"), + "last_run_at": _normalize_datetime_text(runtime.get("last_run_at")), + "last_status": runtime.get("last_status"), + "last_error": runtime.get("last_error"), + "last_duration_ms": runtime.get("last_duration_ms"), "next_run_at": _normalize_datetime_text(runtime.get("next_run_at")), "run_count": runtime.get("run_count", 0), "success_count": runtime.get("success_count", 0), "fail_count": runtime.get("fail_count", 0), - "latest_success_at": _normalize_datetime_text(history_summary.get("latest_success_at")), - "latest_failed_at": _normalize_datetime_text(history_summary.get("latest_failed_at")), - "latest_failure_summary": str(history_summary.get("latest_failure_summary") or "").strip(), - "history_success_count": int(history_summary.get("history_success_count", 0) or 0), - "history_fail_count": int(history_summary.get("history_fail_count", 0) or 0), - "history_total_count": int(history_summary.get("history_total_count", 0) or 0), - "health_status": health_status, - "health_message": _build_job_health_message( - health_status=health_status, - latest_success_at=history_summary.get("latest_success_at"), - latest_failure_summary=str(history_summary.get("latest_failure_summary") or "").strip(), - ), } ) diff --git a/admin/dashboard/templates/base.html b/admin/dashboard/templates/base.html index 84133bc..eca0f07 100644 --- a/admin/dashboard/templates/base.html +++ b/admin/dashboard/templates/base.html @@ -1012,7 +1012,6 @@ items: [ { label: '插件统计', path: '/plugins' }, { label: '插件管理', path: '/plugins_manage' }, - { label: '命令索引', path: '/command_catalog' }, { label: '插件定时任务', path: '/plugin_schedules' }, { label: '群级插件配置', path: '/group_plugin_config' }, { label: '响应指令管理', path: '/fun_command_rules' }, @@ -1153,7 +1152,6 @@ '12': '/virtual_group', '13': '/api_docs', '14': '/system_status', - '18': '/command_catalog', '17': '/system_llm', '15': '/file_browser', '16': '/message_push' diff --git a/admin/dashboard/templates/command_catalog.html b/admin/dashboard/templates/command_catalog.html deleted file mode 100644 index f00ee7c..0000000 --- a/admin/dashboard/templates/command_catalog.html +++ /dev/null @@ -1,478 +0,0 @@ -{% extends "base.html" %} - -{% block title %}命令索引 - 机器人管理后台{% endblock %} - -{% block content %} -
-
-
-
Command Catalog
-

命令索引

-

集中查看当前插件命令、群可用状态、自动能力与管理员触发示例,减少靠记忆找功能的成本。

-
-
- - - - - - 刷新索引 - -
-
- - - - -
可用手动命令
-
{% raw %}{{ summary.available_manual_count || 0 }}{% endraw %}
-
当前群可以直接触发的消息命令
-
-
- - -
自动能力
-
{% raw %}{{ summary.available_auto_count || 0 }}{% endraw %}
-
无需手动发送指令的自动/定时能力
-
-
- - -
未启用命令
-
{% raw %}{{ summary.unavailable_manual_count || 0 }}{% endraw %}
-
管理员视角下可看到但当前群不可用的命令
-
-
- - -
管理命令
-
{% raw %}{{ summary.admin_command_count || 0 }}{% endraw %}
-
群开关、管理员维护等后台辅助命令
-
-
-
- - -
-
-

筛选条件

-

支持按命令、插件名、描述关键词快速定位。

-
- - - -
-
- - -
-
-

当前可用命令

-

这里展示的是当前群在管理员视角下“真实可触发”的命令入口。

-
-
- {% raw %}{{ activeGroupLabel }}{% endraw %} -
-
- - - - - - - - - - - - - - - - - - - - - -
- - - - -
-
-

未启用命令

-

这部分只在后台管理员视角展示,便于你知道还有哪些能力没在当前群打开。

-
-
- - - - - - - - - - - -
-
- - -
-
-

自动/定时能力

-

用于提醒你哪些功能不是靠用户发命令触发,而是自动执行。

-
-
-
-
当前没有已启用的自动能力
-
-
-
{% raw %}{{ item.name }}{% endraw %}
- {% raw %}{{ item.category_label }}{% endraw %} -
-
{% raw %}{{ item.description }}{% endraw %}
-
{% raw %}{{ item.availability_reason || '自动执行' }}{% endraw %}
-
-
-
-
-
- - -
-
-

管理命令示例

-

给管理员的常用操作命令,适合快速开关功能和维护群管理员。

-
-
-
-
当前没有管理命令示例
-
-
-
{% raw %}{{ item.title }}{% endraw %}
-
-
{% raw %}{{ item.example }}{% endraw %}
-
{% raw %}{{ item.description }}{% endraw %}
-
-
-
-
-{% endblock %} - -{% block scripts %} - -{% endblock %} - -{% block styles %} - -{% endblock %} diff --git a/admin/dashboard/templates/index.html b/admin/dashboard/templates/index.html index eeabcbd..d14657d 100644 --- a/admin/dashboard/templates/index.html +++ b/admin/dashboard/templates/index.html @@ -131,7 +131,7 @@

系统健康快照

-

把连接状态、插件运行、异常数量、LLM 运行态与任务调度集中到一个面板里。

+

把连接状态、插件运行、异常数量与转图运行时集中到一个面板里。

最近刷新 @@ -148,29 +148,6 @@
{% raw %}{{ card.value }}{% endraw %}
{% raw %}{{ card.summary }}{% endraw %}
-
-
-
-
-
{% raw %}{{ service.title }}{% endraw %}
-
{% raw %}{{ service.summary }}{% endraw %}
-
- - {% raw %}{{ getHealthStatusText(service.status) }}{% endraw %} - -
-
-
- {% raw %}{{ metric.label }}{% endraw %} - {% raw %}{{ metric.value }}{% endraw %} -
-
-
-
{% raw %}{{ card.extra }}{% endraw %}
@@ -394,38 +371,15 @@ status: 'warning', total_calls: 0, failed_calls: 0, - success_rate: 0, avg_latency_ms: 0, summary: '加载中...', - last_call: {}, - scene_count: 0, - target_count: 0, - provider_count: 0, - has_routing: false, - default_scene: '', - default_backend: '', - last_provider: '', - last_backend: '', - last_scene: '', - last_model: '', - last_timestamp: '', - last_latency_ms: 0, - last_error: '' + last_call: {} }, - scheduler: { + md2img: { status: 'warning', - total_jobs: 0, - enabled_jobs: 0, - running_jobs: 0, - failed_jobs: 0, - invalid_jobs: 0, - paused_jobs: 0, - never_run_jobs: 0, - system_job_count: 0, - plugin_job_count: 0, - next_run_at: '', - latest_failed_job_name: '', - latest_failed_error: '', + healthy: false, + runtime_ready: false, + browser_ready: false, summary: '加载中...' } }, @@ -469,7 +423,7 @@ const errors = this.healthSummary.errors || {}; const infrastructure = this.healthSummary.infrastructure || {}; const aiRuntime = this.healthSummary.ai_runtime || {}; - const scheduler = this.healthSummary.scheduler || {}; + const md2img = this.healthSummary.md2img || {}; return [ { key: 'robot', @@ -499,30 +453,25 @@ key: 'infrastructure', title: '基础设施', status: infrastructure.status || 'warning', - value: `${this.countHealthyInfrastructureServices(infrastructure)} / 2`, + value: infrastructure.status === 'healthy' ? '正常' : '异常', summary: infrastructure.summary || '暂无状态', - serviceBlocks: this.buildInfrastructureServiceBlocks(infrastructure), - extra: '首页展示的是服务摘要;如果后续要做更深入的运维排查,再单独拆详细页会更合适。' + extra: `MySQL:${((infrastructure.mysql || {}).status === 'healthy') ? '正常' : '异常'} / Redis:${((infrastructure.redis || {}).status === 'healthy') ? '正常' : '异常'}` }, { key: 'ai_runtime', - title: 'LLM 运行态', + title: 'AI 运行态', status: aiRuntime.status || 'warning', - value: (aiRuntime.total_calls || 0) > 0 - ? `${this.formatMetricNumber(aiRuntime.success_rate, 2)}%` - : `${aiRuntime.scene_count || 0} 个场景`, + value: `${aiRuntime.avg_latency_ms || 0} ms`, summary: aiRuntime.summary || '暂无状态', - serviceBlocks: this.buildAiRuntimeServiceBlocks(aiRuntime), - extra: this.buildAiRuntimeExtra(aiRuntime) + extra: `最近调用 ${aiRuntime.total_calls || 0} 次,失败 ${aiRuntime.failed_calls || 0} 次` }, { - key: 'scheduler', - title: '任务调度', - status: scheduler.status || 'warning', - value: `${scheduler.enabled_jobs || 0} / ${scheduler.total_jobs || 0}`, - summary: scheduler.summary || '暂无状态', - serviceBlocks: this.buildSchedulerServiceBlocks(scheduler), - extra: this.buildSchedulerExtra(scheduler) + key: 'md2img', + title: 'Markdown 转图', + status: md2img.status || 'warning', + value: md2img.healthy ? '就绪' : '待检查', + summary: md2img.summary || '暂无状态', + extra: `Runtime ${md2img.runtime_ready ? '已就绪' : '未就绪'} / Browser ${md2img.browser_ready ? '已就绪' : '未就绪'}` } ]; } @@ -590,235 +539,6 @@ }; return statusMap[status] || '未知'; }, - formatCompactDuration(seconds) { - const totalSeconds = parseInt(seconds) || 0; - if (totalSeconds <= 0) return '-'; - const days = Math.floor(totalSeconds / 86400); - const hours = Math.floor((totalSeconds % 86400) / 3600); - const minutes = Math.floor((totalSeconds % 3600) / 60); - if (days > 0) return `${days}D ${hours}H`; - if (hours > 0) return `${hours}H ${minutes}M`; - return `${minutes}M`; - }, - formatMetricNumber(value, fractionDigits = 0) { - if (value === null || value === undefined || value === '') return '-'; - const numeric = Number(value); - if (Number.isNaN(numeric)) return String(value); - return numeric.toFixed(fractionDigits); - }, - countHealthyInfrastructureServices(infrastructure) { - const mysql = infrastructure.mysql || {}; - const redis = infrastructure.redis || {}; - let count = 0; - if (mysql.status === 'healthy') count += 1; - if (redis.status === 'healthy') count += 1; - return count; - }, - buildInfrastructureServiceBlocks(infrastructure) { - const mysql = infrastructure.mysql || {}; - const redis = infrastructure.redis || {}; - return [ - { - key: 'mysql', - title: 'MySQL', - status: mysql.status || 'warning', - summary: mysql.summary || '暂无状态', - metrics: [ - { - label: '连接负载', - value: `${this.formatMetricNumber(mysql.connection_usage_percent, 1)}%` - }, - { - label: '连接数', - value: `${this.formatMetricNumber(mysql.threads_connected)} / ${mysql.max_connections || '-'}` - }, - { - label: '运行线程', - value: this.formatMetricNumber(mysql.threads_running) - }, - { - label: 'QPS', - value: this.formatMetricNumber(mysql.questions_per_second, 2) - }, - { - label: '库体积', - value: `${this.formatMetricNumber(mysql.schema_size_mb, 2)} MB` - }, - { - label: '表数量', - value: this.formatMetricNumber(mysql.table_count) - } - ] - }, - { - key: 'redis', - title: 'Redis', - status: redis.status || 'warning', - summary: redis.summary || '暂无状态', - metrics: [ - { - label: 'Key 数量', - value: this.formatMetricNumber(redis.key_count) - }, - { - label: '客户端', - value: this.formatMetricNumber(redis.connected_clients) - }, - { - label: 'OPS/s', - value: this.formatMetricNumber(redis.ops_per_sec) - }, - { - label: '内存占用', - value: redis.used_memory_human || '-' - }, - { - label: '命中率', - value: `${this.formatMetricNumber(redis.hit_rate_percent, 1)}%` - }, - { - label: '运行时间', - value: this.formatCompactDuration(redis.uptime_seconds) - } - ] - } - ]; - }, - buildAiRuntimeServiceBlocks(aiRuntime) { - // AI 卡片拆成“路由配置”和“最近调用”两个子面板, - // 让首页既能判断配置是否完整,也能快速定位最近请求到底走了哪条链路。 - return [ - { - key: 'ai-routing', - title: '路由配置', - status: aiRuntime.has_routing ? 'healthy' : 'warning', - summary: aiRuntime.default_scene - ? `默认场景:${aiRuntime.default_scene}` - : '当前未设置默认场景', - metrics: [ - { - label: '场景数量', - value: this.formatMetricNumber(aiRuntime.scene_count) - }, - { - label: '目标数量', - value: this.formatMetricNumber(aiRuntime.target_count) - }, - { - label: 'Provider 模板', - value: this.formatMetricNumber(aiRuntime.provider_count) - }, - { - label: '默认后端', - value: aiRuntime.default_backend || '-' - } - ] - }, - { - key: 'ai-last-call', - title: '最近调用', - status: (aiRuntime.failed_calls || 0) > 0 ? 'warning' : ((aiRuntime.total_calls || 0) > 0 ? 'healthy' : 'warning'), - summary: aiRuntime.last_timestamp - ? `最近一次记录时间:${aiRuntime.last_timestamp}` - : '当前窗口内暂无调用记录', - metrics: [ - { - label: 'Provider', - value: aiRuntime.last_provider || '-' - }, - { - label: 'Backend', - value: aiRuntime.last_backend || '-' - }, - { - label: 'Scene', - value: aiRuntime.last_scene || '-' - }, - { - label: '模型', - value: aiRuntime.last_model || '-' - }, - { - label: '最近耗时', - value: `${this.formatMetricNumber(aiRuntime.last_latency_ms, 2)} ms` - }, - { - label: '最近错误', - value: aiRuntime.last_error || '无' - } - ] - } - ]; - }, - buildAiRuntimeExtra(aiRuntime) { - return `最近调用 ${aiRuntime.total_calls || 0} 次,失败 ${aiRuntime.failed_calls || 0} 次,平均耗时 ${this.formatMetricNumber(aiRuntime.avg_latency_ms, 2)} ms`; - }, - buildSchedulerServiceBlocks(scheduler) { - // 任务调度卡片只保留首页最需要的摘要: - // 任务装载量、执行态、失败数,以及系统任务/插件任务的大致构成。 - return [ - { - key: 'scheduler-overview', - title: '任务装载', - status: scheduler.enabled_jobs > 0 ? 'healthy' : 'warning', - summary: scheduler.next_run_at - ? `下一次执行:${scheduler.next_run_at}` - : '当前没有可计算的下一次执行时间', - metrics: [ - { - label: '启用任务', - value: this.formatMetricNumber(scheduler.enabled_jobs) - }, - { - label: '暂停任务', - value: this.formatMetricNumber(scheduler.paused_jobs) - }, - { - label: '系统任务', - value: this.formatMetricNumber(scheduler.system_job_count) - }, - { - label: '插件任务', - value: this.formatMetricNumber(scheduler.plugin_job_count) - } - ] - }, - { - key: 'scheduler-runtime', - title: '执行状态', - status: scheduler.status || 'warning', - summary: scheduler.latest_failed_job_name - ? `最近失败任务:${scheduler.latest_failed_job_name}` - : '当前未发现最近失败任务', - metrics: [ - { - label: '执行中', - value: this.formatMetricNumber(scheduler.running_jobs) - }, - { - label: '失败任务', - value: this.formatMetricNumber(scheduler.failed_jobs) - }, - { - label: '非法调度', - value: this.formatMetricNumber(scheduler.invalid_jobs) - }, - { - label: '未执行过', - value: this.formatMetricNumber(scheduler.never_run_jobs) - } - ] - } - ]; - }, - buildSchedulerExtra(scheduler) { - if (scheduler.latest_failed_error) { - return `最近失败原因:${scheduler.latest_failed_error}`; - } - return scheduler.next_run_at - ? `下次执行时间:${scheduler.next_run_at}` - : '当前暂无可用的下一次执行时间'; - }, renderPieChart(chartId, usageValue, label) { const ctx = document.getElementById(chartId); if (!ctx) return; @@ -1375,104 +1095,6 @@ color: #475569; } - .health-service-grid { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 12px; - margin-top: 16px; - } - - .health-service-panel { - padding: 14px; - border-radius: 16px; - border: 1px solid rgba(148, 163, 184, 0.14); - background: rgba(248, 250, 252, 0.72); - } - - .health-service-panel--healthy { - box-shadow: inset 0 0 0 1px rgba(16, 185, 129, 0.08); - } - - .health-service-panel--warning { - box-shadow: inset 0 0 0 1px rgba(245, 158, 11, 0.10); - } - - .health-service-panel--danger { - box-shadow: inset 0 0 0 1px rgba(239, 68, 68, 0.10); - } - - .health-service-panel__head { - display: flex; - align-items: flex-start; - justify-content: space-between; - gap: 12px; - margin-bottom: 12px; - } - - .health-service-panel__title { - font-size: 14px; - font-weight: 700; - color: #0f172a; - margin-bottom: 4px; - } - - .health-service-panel__summary { - font-size: 12px; - line-height: 1.6; - color: #64748b; - } - - .health-service-panel__badge { - display: inline-flex; - align-items: center; - justify-content: center; - min-width: 44px; - padding: 4px 8px; - border-radius: 999px; - font-size: 11px; - font-weight: 700; - flex-shrink: 0; - } - - .health-service-panel__badge--healthy { - color: #047857; - background: rgba(16, 185, 129, 0.12); - } - - .health-service-panel__badge--warning { - color: #b45309; - background: rgba(245, 158, 11, 0.14); - } - - .health-service-panel__badge--danger { - color: #b91c1c; - background: rgba(239, 68, 68, 0.14); - } - - .health-service-metrics { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 10px 12px; - } - - .health-service-metric { - display: flex; - flex-direction: column; - gap: 4px; - } - - .health-service-metric__label { - font-size: 11px; - color: #94a3b8; - } - - .health-service-metric__value { - font-size: 13px; - font-weight: 600; - color: #1e293b; - word-break: break-word; - } - .health-item__extra { margin-top: 12px; padding-top: 12px; @@ -1828,10 +1450,6 @@ .health-grid { grid-template-columns: 1fr; } - - .health-service-grid { - grid-template-columns: 1fr; - } } @media (max-width: 768px) { @@ -1941,10 +1559,6 @@ font-size: 24px; } - .health-service-metrics { - grid-template-columns: 1fr; - } - .chart-container--large, .chart-container--panel { height: 220px; diff --git a/admin/dashboard/templates/plugin_schedules.html b/admin/dashboard/templates/plugin_schedules.html index cc792da..1952250 100644 --- a/admin/dashboard/templates/plugin_schedules.html +++ b/admin/dashboard/templates/plugin_schedules.html @@ -42,44 +42,11 @@ {% raw %}{{ scope.row.last_status || 'never' }}{% endraw %} - - - - - - - - - - - - - + @@ -230,25 +197,6 @@ new Vue({ if (status === 'running') return 'warning' return 'info' }, - healthTag(status) { - if (status === 'healthy') return 'success' - if (status === 'running') return 'warning' - if (status === 'failed') return 'danger' - if (status === 'degraded') return 'warning' - if (status === 'disabled') return 'info' - return '' - }, - healthLabel(status) { - const mapping = { - healthy: '健康', - running: '执行中', - failed: '异常', - degraded: '有告警', - disabled: '停用', - idle: '待运行' - } - return mapping[status] || '待运行' - }, formatDateTime(value) { // 统一清洗时间展示:去掉 ISO 'T',并兼容字符串与日期对象。 if (!value) return '' @@ -382,25 +330,6 @@ new Vue({ } await this.loadSchedules() }, - async toggleEnabled(row) { - const payload = { - action_name: row.action_name, - description: row.description, - enabled: !row.enabled, - trigger_type: row.trigger_type, - trigger_config: row.trigger_config, - target_scope: row.target_scope, - target_config: row.target_config, - payload: row.payload || {} - } - const resp = await axios.put(`/plugin_schedules/api/schedules/${row.id}`, payload) - if (resp.data.success) { - this.$message.success(row.enabled ? '已停用' : '已启用') - await this.loadSchedules() - } else { - this.$message.error(resp.data.message || '更新失败') - } - }, async viewLogs(row) { const resp = await axios.get(`/plugin_schedules/api/schedules/${row.id}/logs`) if (resp.data.success) { @@ -423,10 +352,5 @@ new Vue({ .page-hero-copy p{color:#64748b;font-size:14px} .action-row{display:flex;align-items:center;gap:8px;flex-wrap:wrap} .detail-pre{white-space:pre-wrap;word-break:break-word;background:rgba(248,250,252,.85);border:1px solid rgba(148,163,184,.12);border-radius:14px;padding:10px;color:#334155} -.cell-ellipsis{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:#475569} -.history-metrics{display:flex;align-items:center;justify-content:center;gap:8px} -.metric-success{color:#16a34a;font-weight:600} -.metric-fail{color:#dc2626;font-weight:600} -.history-total{margin-top:4px;color:#64748b;font-size:12px} {% endblock %} diff --git a/admin/dashboard/templates/plugins_manage.html b/admin/dashboard/templates/plugins_manage.html index 69cc002..a80e9aa 100644 --- a/admin/dashboard/templates/plugins_manage.html +++ b/admin/dashboard/templates/plugins_manage.html @@ -18,168 +18,32 @@ - +
插件总数
{% raw %}{{ plugins.length }}{% endraw %}
当前已注册插件模块
- +
运行中
{% raw %}{{ runningPluginsCount }}{% endraw %}
可正常提供能力的插件
- +
已停用
{% raw %}{{ stoppedPluginsCount }}{% endraw %}
待启用或排查状态
- + -
治理告警
-
{% raw %}{{ governanceRiskCount }}{% endraw %}
-
存在配置、依赖或加载风险的插件
-
-
- - -
执行异常
-
{% raw %}{{ executionRiskCount }}{% endraw %}
-
最近执行失败、超时或进入熔断的插件
-
-
- - -
熔断中
-
{% raw %}{{ openCircuitCount }}{% endraw %}
-
当前被保护机制隔离的高风险插件
-
-
-
- - - - -
-
-

高风险插件

-

优先排查熔断中、连续失败或最近错误较多的插件。

-
-
-
-
暂无高风险插件
-
-
{% raw %}{{ index + 1 }}{% endraw %}
-
-
-
{% raw %}{{ plugin.name }}{% endraw %}
- - {% raw %}{{ executionLabel((plugin.execution_summary || {}).status) }}{% endraw %} - -
-
{% raw %}{{ (plugin.execution_summary || {}).summary || '暂无执行摘要' }}{% endraw %}
-
- 最近错误:{% raw %}{{ (plugin.execution_summary || {}).last_error_message || '无' }}{% endraw %} -
-
-
-
-
-
- - -
-
-

慢插件排行

-

基于最近一次执行耗时,快速定位可能影响主链路响应的插件。

-
-
-
-
暂无执行样本
-
-
{% raw %}{{ index + 1 }}{% endraw %}
-
-
-
{% raw %}{{ plugin.name }}{% endraw %}
-
{% raw %}{{ formatDurationMs((plugin.execution_summary || {}).last_process_time_ms) }}{% endraw %}
-
-
{% raw %}{{ (plugin.execution_summary || {}).summary || '暂无执行摘要' }}{% endraw %}
-
- 成功率:{% raw %}{{ formatPercent((plugin.execution_summary || {}).success_rate) }}{% endraw %} - 累计执行:{% raw %}{{ (plugin.execution_summary || {}).total_executions || 0 }}{% endraw %} -
-
-
-
-
-
-
- - - - -
-
-

依赖核心插件

-

优先保护被多个插件依赖的基础能力节点,避免单点异常扩散。

-
-
-
-
暂无依赖关系数据
-
-
{% raw %}{{ index + 1 }}{% endraw %}
-
-
-
{% raw %}{{ plugin.name }}{% endraw %}
- - {% raw %}{{ `${(plugin.dependency_summary || {}).dependent_count || 0} 个上游` }}{% endraw %} - -
-
- {% raw %}{{ buildDependencyCoreSummary(plugin) }}{% endraw %} -
-
- 执行:{% raw %}{{ executionLabel((plugin.execution_summary || {}).status) }}{% endraw %} - 治理:{% raw %}{{ governanceLabel(plugin.governance_status) }}{% endraw %} -
-
-
-
-
-
- - -
-
-

缺失依赖风险

-

快速查看声明了依赖但当前目标未加载的插件,优先处理运行链断裂问题。

-
-
-
-
当前没有缺失依赖风险
-
-
{% raw %}{{ index + 1 }}{% endraw %}
-
-
-
{% raw %}{{ plugin.name }}{% endraw %}
- - {% raw %}{{ `${(plugin.dependency_summary || {}).missing_count || 0} 个缺失` }}{% endraw %} - -
-
- {% raw %}{{ buildMissingDependencySummary(plugin) }}{% endraw %} -
-
- 模块:{% raw %}{{ plugin.module_name }}{% endraw %} -
-
-
-
+
作者数量
+
{% raw %}{{ authorsCount }}{% endraw %}
+
参与维护的作者规模
@@ -188,7 +52,7 @@

插件列表

-

优先关注状态、执行表现和说明,再进入单个插件详情与配置编辑。

+

优先关注状态和说明,再进入单个插件详情与配置编辑。

@@ -210,59 +74,11 @@ - - - - - - - - - - - - - - - - - - - - - -