阅前须知：由于博主现在 并没有太多自己写代码甚至脚本的能力 ，本篇文章在我和AI共同解决了这个问题之后 直接由AI生成 ，若你对此感到反感，可以现在 关闭该帖 （ 我是废物 ）一、问题背景在 Typecho 中使用 Mermaid 一直不算难，但一旦换成 JOE 主题，问题就开始变得复杂：写好的 `mermaid 代码块被当成普通代码高亮插件明明启用了，但图表不渲染翻页（PJAX）后 Mermaid 直接失效我一开始也尝试用常规方案（正则替换 HTML），结果发现：👉 根本不稳定二、问题的本质JOE 主题做了很多“增强”，包括：自定义代码高亮（Prism / Highlight.js）改写 Markdown 输出结构使用 PJAX（局部刷新页面）这导致一个核心问题：你在后端生成的 HTML，很可能在前端被再次修改甚至覆盖例如你期望的是： 但实际可能变成： 甚至： ... 👉 结构不稳定 → 正则必炸三、传统方案为什么不行？常见插件思路：Markdown → HTML → 正则替换 → 问题在于：依赖 HTML 结构（不可靠）容易被主题覆盖PJAX 后不会重新执行结论：后端改 HTML，在 JOE 这种强主题下是错误方向四、最终解决方案：前端接管我最后采用的是：✅ 完全绕过后端，前端动态解析 Mermaid核心流程：页面加载 ↓ 扫描所有 language-mermaid 代码块 ↓ 替换为 .mermaid DOM ↓ 调用 Mermaid 渲染五、核心实现解析1. 扫描代码块const blocks = document.querySelectorAll( 'pre code.language-mermaid, pre.language-mermaid' );为什么这样写？👉 兼容两种结构： 2. 提取原始代码let code = codeBlock.textContent;👉 直接拿文本，不依赖 HTML 结构3. 重建 DOMconst container = document.createElement('div'); container.className = 'mermaid-container'; const mermaidDiv = document.createElement('div'); mermaidDiv.className = 'mermaid'; mermaidDiv.textContent = code; container.appendChild(mermaidDiv);最终结构： ... 4. 替换原代码块pre.replaceWith(container);👉 关键点：删除原代码高亮 DOM避免主题再次干扰5. 渲染 Mermaidmermaid.initialize({ startOnLoad: false, theme: getTheme() }); mermaid.init(undefined, document.querySelectorAll('.mermaid'));为什么不用自动加载？👉 因为 DOM 是动态生成的6. 防止重复渲染if (codeBlock.dataset.mermaidDone) return;👉 防止：PJAX 重复执行多次渲染报错7. 适配 PJAX（关键）document.addEventListener('pjax:complete', run);👉 没有这行：❌ 翻页后 Mermaid 全部失效六、主题适配（暗黑模式）function getTheme() { if (document.body.classList.contains('dark')) { return 'dark'; } return 'default'; }👉 自动跟随主题切换七、为什么这个方案最稳？对比一下：方案稳定性原因后端正则替换❌依赖 HTML修改 Markdown 解析❌被主题覆盖前端接管（本方案）✅直接操作 DOM八、核心设计思想这次优化本质上是一次“架构调整”：1️⃣ 不和主题抢控制权JOE 已经接管了渲染链：👉 你再插手，只会冲突2️⃣ 前端才是最终执行层只要页面上存在：language-mermaid👉 就一定能识别3️⃣ 幂等设计data-mermaidDone👉 保证多次执行不会出问题九、最终效果✅ 支持所有 Mermaid 图✅ 支持 PJAX✅ 不受代码高亮影响✅ 自动暗黑模式✅ 主题无关（通用）十、一句话总结与其试图修补被主题打乱的 HTML，不如直接绕过它，在前端重建渲染链。十一、源码附上{collapse}{collapse-item label="过长，已折叠，点击查看" close}<?php if (!defined('__TYPECHO_ROOT_DIR__')) exit; /** * Mermaid 插件（JOE终极兼容版 / 前端解析） * * @package MermaidUltimate * @version 2.0.0 */ class Mermaid_Plugin implements Typecho_Plugin_Interface { public static function activate() { Typecho_Plugin::factory('Widget_Archive')->header = array('Mermaid_Plugin', 'header'); Typecho_Plugin::factory('Widget_Archive')->footer = array('Mermaid_Plugin', 'footer'); } public static function deactivate() {} public static function config(Typecho_Widget_Helper_Form $form) { $cdn = new Typecho_Widget_Helper_Form_Element_Text( 'cdn', null, 'https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js', _t('Mermaid CDN'), _t('推荐 jsdelivr 或 npmmirror') ); $form->addInput($cdn); $theme = new Typecho_Widget_Helper_Form_Element_Select( 'theme', array( 'default' => 'Default', 'dark' => 'Dark', 'forest' => 'Forest', 'neutral' => 'Neutral', ), 'default', _t('主题'), _t('Mermaid 渲染主题') ); $form->addInput($theme); $autoDark = new Typecho_Widget_Helper_Form_Element_Radio( 'autoDark', array( '1' => '开启', '0' => '关闭' ), '1', _t('自动暗黑模式'), _t('根据 JOE 主题自动切换') ); $form->addInput($autoDark); } public static function personalConfig(Typecho_Widget_Helper_Form $form) {} public static function header() { echo ' '; } public static function footer() { $options = Helper::options()->plugin('Mermaid'); $cdn = $options->cdn ?: 'https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js'; $theme = $options->theme ?: 'default'; $autoDark = $options->autoDark; echo <<<HTML HTML; } }{/collapse-item}{/collapse}十二、使用方法：把这个文件放到：/usr/plugins/Mermaid/Plugin.php进入 Typecho 后台： 控制台 → 插件 → 启用 Mermaid 写文章时使用 Mermaid就直接在 Markdown 里写：发布后就会自动渲染成图。
前言：在我们使用ssh的时候正常情况下，如果我们退出ssh，进程会被杀掉，导致一些需要运行较长时间（比如 rsync/cp ）的命令中断，十分的难受，这里提供两种解决办法：1.使用screen1.1开启 screen：screen -S upload1.2执行命令##举例 rsync -avh --progress /data/myfolder/ /mnt/myfolder/1.3退出SSH保持后台运行：按：Ctrl + A 然后按 D1.4重新连接后恢复screen -r upload2.使用tmux2.1启动tmuxtmux new -s upload2.2执行命令后，退出保持后台Ctrl + B 然后按 D2.3重新连接后恢复tmux attach -t upload
写在前面本人在dn42网络中有4个节点，本来搭建了LookingGlass来监测节点运行状态，但考虑到不够直观 其实是想折腾，所以打算搭建一个可以图形化监控每台节点Bird运行状态的仪表盘，在社区里面发现可以用Grafana搭配Prometheus作为数据源实现拓扑图如下：graph TD subgraph DN42 Network N1[Node 1] --> NE1(Node Exporter) N1 --> BE1(Bird/FRR Exporter) N2[Node 2] --> NE2(Node Exporter) N2 --> BE2(Bird/FRR Exporter) N3[Node 3] --> NE3(Node Exporter) N3 --> BE3(Bird/FRR Exporter) N4[Node 4] --> NE4(Node Exporter) N4 --> BE4(Bird/FRR Exporter) end NE1 --> P(Prometheus Server) NE2 --> P NE3 --> P NE4 --> P BE1 --> P BE2 --> P BE3 --> P BE4 --> P P --> G(Grafana Server) User[用户] --> G搭建部署 Prometheus ServerPrometheus Server 将运行在独立的监控服务器上。创建工作目录mkdir -p /opt/prometheus/config /opt/prometheus/data创建 Prometheus 配置文件 (/opt/prometheus/config/prometheus.yml)global: scrape_interval: 15s # 默认抓取间隔 scrape_configs: - job_name: 'prometheus' static_configs: - targets: ['localhost:9090'] # 监控 Prometheus 自身 - job_name: 'node_exporter' static_configs: - targets: ['<node1_ip>:9100', '<node2_ip>:9100', '<node3_ip>:9100', '<node4_ip>:9100'] # 替换为您的 DN42 节点 IP - job_name: 'bird_exporter' static_configs: - targets: ['<node1_ip>:9324', '<node2_ip>:9324', '<node3_ip>:9324', '<node4_ip>:9324'] # 替换为您的 DN42 节点 IP注意: 请将 <nodeX_ip> 替换为您的 DN42 节点的实际 IP 地址。使用 Docker Compose 部署 Prometheus创建 docker-compose.yml 文件：services: prometheus: image: prom/prometheus container_name: prometheus network_mode: host ports: - "9090:9090" volumes: - /opt/prometheus/config/prometheus.yml:/etc/prometheus/prometheus.yml - /opt/prometheus/data:/prometheus command: - '--config.file=/etc/prometheus/prometheus.yml' - '--storage.tsdb.path=/prometheus' - '--web.enable-lifecycle' restart: unless-stopped将数据目录的权限赋予 Prometheus 用户：chown -R 65534:65534 /opt/prometheus/data启动 Prometheus：docker-compose up -d通过访问 http://<监控服务器IP>:9090 来验证 Prometheus 是否正常运行。部署 Node Exporter (每个 DN42 节点上)创建工作目录mkdir -p /opt/node_exporter方式1：使用 Docker 部署 Node Exporterdocker run -d \ --name node_exporter \ --net="host" \ --pid="host" \ -v "/:/host:ro,rslave" \ quay.io/prometheus/node-exporter:latest \ --path.rootfs=/host方式2：使用二进制可执行文件部署 Node Exporter下载并解压 Node Exporter访问 Prometheus 下载页面 获取最新版本的 Node Exporter。以下示例使用 1.7.0 版本wget https://github.com/prometheus/node_exporter/releases/download/v1.7.0/node_exporter-1.7.0.linux-amd64.tar.gz tar xvfz node_exporter-1.7.0.linux-amd64.tar.gz sudo cp node_exporter-1.7.0.linux-amd64/node_exporter /usr/local/bin sudo chown prometheus:prometheus /usr/local/bin/node_exporter rm -rf node_exporter-1.7.0.linux-amd64.tar.gz node_exporter-1.7.0.linux-amd64创建 Systemd 服务文件 (/etc/systemd/system/node_exporter.service)[Unit] Description=Node Exporter Wants=network-online.target After=network-online.target [Service] Type=simple ExecStart=/usr/local/bin/node_exporter \ --web.listen-address=":9100" \ --collector.textfile.directory="/var/lib/node_exporter/textfile_collector" [Install] WantedBy=multi-user.target重新加载 Systemd 并启动 Node Exportersudo systemctl daemon-reload sudo systemctl start node_exporter sudo systemctl enable node_exporterBird Exporter首先，确保 BIRD 配置允许 bird_exporter 访问其控制套接字。通常需要在 /etc/bird/bird.conf 或 /etc/bird2/bird.conf 中添加类似以下内容：control socket "/var/run/bird/bird.ctl" mode 0777;然后，使用 Docker 部署 Bird Exporter (推荐用于内存充足的节点)：docker run -d \ --name bird_exporter \ --network host \ -v /var/run/bird/bird.ctl:/var/run/bird/bird.ctl \ czerwonk/bird_exporter:latest使用二进制方式部署 Bird Exporter下载并解压 Bird Exporter访问 Bird Exporter GitHub Releases 获取最新版本的 Bird Exporter。以下示例使用 1.1.0 版本，请根据实际情况替换。wget https://github.com/czerwonk/bird_exporter/releases/download/v1.1.0/bird_exporter-1.1.0.linux-amd64.tar.gz tar xvfz bird_exporter-1.1.0.linux-amd64.tar.gz sudo cp bird_exporter-1.1.0.linux-amd64/bird_exporter /usr/local/bin sudo chown prometheus:prometheus /usr/local/bin/bird_exporter rm -rf bird_exporter-1.1.0.linux-amd64.tar.gz bird_exporter-1.1.0.linux-amd64创建 Systemd 服务文件 (/etc/systemd/system/bird_exporter.service)[Unit] Description=Bird Exporter Wants=network-online.target After=network-online.target [Service] Type=simple ExecStart=/usr/local/bin/bird_exporter \ --bird.socket="/var/run/bird/bird.ctl" \ --web.listen-address=":9324" [Install] WantedBy=multi-user.target重新加载 Systemd 并启动 Bird Exportersudo systemctl daemon-reload sudo systemctl start bird_exporter sudo systemctl enable bird_exporter部署 Grafana ServerGrafana Server 建议与 Prometheus Server 部署在同一台监控服务器上。创建工作目录mkdir -p /opt/grafana/data4.4.2 使用 Docker Compose 部署 Grafana更新之前创建的 docker-compose.yml 文件，添加 Grafana 服务：services: prometheus: image: prom/prometheus container_name: prometheus ports: - "9090:9090" volumes: - /opt/prometheus/config/prometheus.yml:/etc/prometheus/prometheus.yml - /opt/prometheus/data:/prometheus command: - '--config.file=/etc/prometheus/prometheus.yml' - '--storage.tsdb.path=/prometheus' - '--web.enable-lifecycle' restart: unless-stopped grafana: image: grafana/grafana container_name: grafana ports: - "3000:3000" volumes: - /opt/grafana/data:/var/lib/grafana environment: - GF_SECURITY_ADMIN_USER=admin - GF_SECURITY_ADMIN_PASSWORD=your_strong_password # 请替换为强密码 depends_on: - prometheus restart: unless-stopped重新启动 Docker Compose 服务以启动 Grafana：docker-compose up -d配置 Grafana 数据源登录 Grafana。在左侧导航栏中，点击齿轮图标 (Configuration) -> Data Sources。点击 Add data source。选择 Prometheus。在 HTTP 部分的 URL 字段中输入 http://prometheus:9090 (如果 Prometheus 和 Grafana 在同一个 Docker Compose 网络中) 或 http://localhost:9090 (如果 Prometheus 运行在宿主机上，且 Grafana 可以直接访问)。点击 Save & Test。如果一切正常，您将看到 Data source is working 的消息。导入 Grafana 仪表盘Grafana 社区提供了许多现成的仪表盘，可以大大简化监控配置。以下是一些推荐的仪表盘 ID：Node Exporter Full: ID 1860 (或搜索 Node Exporter Full)，用于监控主机操作系统指标。BIRD RS: ID 5259 (或搜索 BIRD RS)，用于监控 BIRD 路由协议状态。FRR Exporter - BGP: ID 22943 (或搜索 FRR Exporter - BGP)，用于监控 FRR BGP 状态。导入步骤在 Grafana 左侧导航栏中，点击 + 图标 (Create) -> Import。在 Import via grafana.com 字段中输入上述仪表盘 ID，然后点击 Load。选择您的 Prometheus 数据源。点击 Import。重复此过程，导入所有您需要的仪表盘。告警配置 (可选)Grafana 允许您基于 Prometheus 收集的指标配置告警规则。当指标达到预设阈值时，Grafana 可以通过邮件、Slack、Webhook 等方式发送通知。配置告警通道在 Grafana 左侧导航栏中，点击齿轮图标 (Configuration) -> Alerting -> Notification channels。点击 New channel。选择偏好的通知类型 (例如 Email, Slack) 并填写相关配置信息。点击 Save。创建告警规则打开想要添加告警的仪表盘。选择一个面板，点击面板标题，然后选择 Edit。在面板编辑视图中，切换到 Alert 选项卡。点击 Create Alert。定义告警规则的条件、评估周期和通知通道。点击 Save。分享我的JSON{collapse}{collapse-item label="Sample" close}{ "annotations": { "list": [ { "builtIn": 1, "datasource": { "uid": "-- Grafana --" }, "enable": true, "hide": true, "iconColor": "rgba(0, 211, 255, 1)", "name": "Annotations \u0026 Alerts", "type": "dashboard" } ] }, "description": "Live BIRD routing health, prefix visibility, session status and routing activity across all monitored routers.", "editable": true, "fiscalYearStartMonth": 0, "graphTooltip": 1, "id": 6, "links": [ ], "panels": [ { "datasource": { "type": "prometheus", "uid": "dfdlzb2hrvlz4b" }, "description": "Healthy Prometheus scrape targets for the selected BIRD exporters.", "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "mappings": [ ], "thresholds": { "mode": "absolute", "steps": [ { "color": "red", "value": 0 }, { "color": "green", "value": 1 } ] }, "unit": "short" }, "overrides": [ ] }, "gridPos": { "h": 4, "w": 6, "x": 0, "y": 0 }, "id": 10, "options": { "colorMode": "background_solid", "graphMode": "none", "justifyMode": "center", "orientation": "horizontal", "percentChangeColorMode": "standard", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "showPercentChange": false, "textMode": "auto", "wideLayout": true }, "pluginVersion": "12.3.3", "targets": [ { "datasource": { "type": "prometheus", "uid": "dfdlzb2hrvlz4b" }, "editorMode": "code", "expr": "count(up{job=\"bird_exporter\",instance=~\"$instance\"} == 1) or vector(0)", "format": "time_series", "instant": true, "legendFormat": "__auto", "range": false, "refId": "A" } ], "title": "BIRD Exporters Online", "type": "stat" }, { "datasource": { "type": "prometheus", "uid": "dfdlzb2hrvlz4b" }, "description": "Established BGP sessions across the selected router scope.", "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "mappings": [ ], "thresholds": { "mode": "absolute", "steps": [ { "color": "red", "value": 0 }, { "color": "green", "value": 1 } ] }, "unit": "short" }, "overrides": [ ] }, "gridPos": { "h": 4, "w": 6, "x": 6, "y": 0 }, "id": 11, "options": { "colorMode": "background_solid", "graphMode": "none", "justifyMode": "center", "orientation": "horizontal", "percentChangeColorMode": "standard", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "showPercentChange": false, "textMode": "auto", "wideLayout": true }, "pluginVersion": "12.3.3", "targets": [ { "datasource": { "type": "prometheus", "uid": "dfdlzb2hrvlz4b" }, "editorMode": "code", "expr": "count(bird_protocol_up{proto=\"BGP\",instance=~\"$instance\"} == 1) or vector(0)", "format": "time_series", "instant": true, "legendFormat": "__auto", "range": false, "refId": "A" } ], "title": "BGP Sessions Up", "type": "stat" }, { "datasource": { "type": "prometheus", "uid": "dfdlzb2hrvlz4b" }, "description": "BGP sessions currently not established.", "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "mappings": [ ], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": 0 }, { "color": "red", "value": 1 } ] }, "unit": "short" }, "overrides": [ ] }, "gridPos": { "h": 4, "w": 6, "x": 12, "y": 0 }, "id": 12, "options": { "colorMode": "background_solid", "graphMode": "none", "justifyMode": "center", "orientation": "horizontal", "percentChangeColorMode": "standard", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "showPercentChange": false, "textMode": "auto", "wideLayout": true }, "pluginVersion": "12.3.3", "targets": [ { "datasource": { "type": "prometheus", "uid": "dfdlzb2hrvlz4b" }, "editorMode": "code", "expr": "count(bird_protocol_up{proto=\"BGP\",instance=~\"$instance\"} == 0) or vector(0)", "format": "time_series", "instant": true, "legendFormat": "__auto", "range": false, "refId": "A" } ], "title": "BGP Sessions Down", "type": "stat" }, { "datasource": { "type": "prometheus", "uid": "dfdlzb2hrvlz4b" }, "description": "Total adjacent OSPF and OSPFv3 neighbors.", "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "mappings": [ ], "thresholds": { "mode": "absolute", "steps": [ { "color": "red", "value": 0 }, { "color": "green", "value": 1 } ] }, "unit": "short" }, "overrides": [ ] }, "gridPos": { "h": 4, "w": 6, "x": 18, "y": 0 }, "id": 13, "options": { "colorMode": "background_solid", "graphMode": "none", "justifyMode": "center", "orientation": "horizontal", "percentChangeColorMode": "standard", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "showPercentChange": false, "textMode": "auto", "wideLayout": true }, "pluginVersion": "12.3.3", "targets": [ { "datasource": { "type": "prometheus", "uid": "dfdlzb2hrvlz4b" }, "editorMode": "code", "expr": "(sum(bird_ospf_neighbor_adjacent_count{instance=~\"$instance\"}) or vector(0)) + (sum(bird_ospfv3_neighbor_adjacent_count{instance=~\"$instance\"}) or vector(0))", "format": "time_series", "instant": true, "legendFormat": "__auto", "range": false, "refId": "A" } ], "title": "OSPF Adjacent Neighbors", "type": "stat" }, { "datasource": { "type": "prometheus", "uid": "dfdlzb2hrvlz4b" }, "description": "Prefixes advertised to PITER-IX peers across the selected time range.", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisBorderShow": false, "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "", "axisPlacement": "left", "barAlignment": 0, "barWidthFactor": 0.6, "drawStyle": "line", "fillOpacity": 0, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "insertNulls": false, "lineInterpolation": "stepAfter", "lineWidth": 2, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "showValues": false, "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "mappings": [ ], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": 0 }, { "color": "red", "value": 80 } ] }, "unit": "short" }, "overrides": [ { "matcher": { "id": "byValue", "options": { "op": "gte", "reducer": "allIsZero", "value": 0 } }, "properties": [ { "id": "custom.hideFrom", "value": { "legend": true, "tooltip": true, "viz": false } } ] }, { "matcher": { "id": "byValue", "options": { "op": "gte", "reducer": "allIsNull", "value": 0 } }, "properties": [ { "id": "custom.hideFrom", "value": { "legend": true, "tooltip": true, "viz": false } } ] }, { "matcher": { "id": "byValue", "options": { "op": "gte", "reducer": "allIsZero", "value": 0 } }, "properties": [ { "id": "custom.hideFrom", "value": { "legend": true, "tooltip": true, "viz": false } } ] }, { "matcher": { "id": "byValue", "options": { "op": "gte", "reducer": "allIsNull", "value": 0 } }, "properties": [ { "id": "custom.hideFrom", "value": { "legend": true, "tooltip": true, "viz": false } } ] }, { "matcher": { "id": "byValue", "options": { "op": "gte", "reducer": "allIsZero", "value": 0 } }, "properties": [ { "id": "custom.hideFrom", "value": { "legend": true, "tooltip": true, "viz": false } } ] }, { "matcher": { "id": "byValue", "options": { "op": "gte", "reducer": "allIsNull", "value": 0 } }, "properties": [ { "id": "custom.hideFrom", "value": { "legend": true, "tooltip": true, "viz": false } } ] } ] }, "gridPos": { "h": 8, "w": 12, "x": 0, "y": 4 }, "id": 2, "options": { "legend": { "calcs": [ "lastNotNull" ], "displayMode": "table", "placement": "bottom", "showLegend": true }, "tooltip": { "hideZeros": true, "mode": "multi", "sort": "desc" } }, "pluginVersion": "12.3.3", "targets": [ { "datasource": { "type": "prometheus", "uid": "dfdlzb2hrvlz4b" }, "dateTimeType": "DATETIME", "editorMode": "code", "expr": "bird_protocol_prefix_export_count{\n instance=~\"$instance\",\r\n name=~\"(dn42_peer_|dn42_)[0-9a-zA-Z]+\", \r\n name!~\"dn42_ospf\",\r\n name!~\"dn42_ospf6\", \r\n ip_version=\"4\"\r\n} \u003e 0\r\n", "format": "time_series", "formattedQuery": "SELECT $timeSeries as t, count() FROM $table WHERE $timeFilter GROUP BY t ORDER BY t", "intervalFactor": 1, "legendFormat": "{{name}}", "query": "SELECT\n $timeSeries as t,\n count()\nFROM $table\nWHERE $timeFilter\nGROUP BY t\nORDER BY t", "range": true, "refId": "A", "round": "0s" } ], "title": "PITER-IX · Exported Prefixes", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "dfdlzb2hrvlz4b" }, "description": "Prefixes received from PITER-IX peers across the selected time range.", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisBorderShow": false, "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "prefixes", "axisPlacement": "left", "barAlignment": 0, "barWidthFactor": 0.6, "drawStyle": "line", "fillOpacity": 0, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "insertNulls": false, "lineInterpolation": "stepAfter", "lineWidth": 2, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "showValues": false, "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "mappings": [ ], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": 0 }, { "color": "red", "value": 80 } ] }, "unit": "none" }, "overrides": [ { "matcher": { "id": "byValue", "options": { "op": "gte", "reducer": "allIsZero", "value": 0 } }, "properties": [ { "id": "custom.hideFrom", "value": { "legend": true, "tooltip": true, "viz": false } } ] }, { "matcher": { "id": "byValue", "options": { "op": "gte", "reducer": "allIsNull", "value": 0 } }, "properties": [ { "id": "custom.hideFrom", "value": { "legend": true, "tooltip": true, "viz": false } } ] }, { "matcher": { "id": "byValue", "options": { "op": "gte", "reducer": "allIsZero", "value": 0 } }, "properties": [ { "id": "custom.hideFrom", "value": { "legend": true, "tooltip": true, "viz": false } } ] }, { "matcher": { "id": "byValue", "options": { "op": "gte", "reducer": "allIsNull", "value": 0 } }, "properties": [ { "id": "custom.hideFrom", "value": { "legend": true, "tooltip": true, "viz": false } } ] }, { "matcher": { "id": "byValue", "options": { "op": "gte", "reducer": "allIsZero", "value": 0 } }, "properties": [ { "id": "custom.hideFrom", "value": { "legend": true, "tooltip": true, "viz": false } } ] }, { "matcher": { "id": "byValue", "options": { "op": "gte", "reducer": "allIsNull", "value": 0 } }, "properties": [ { "id": "custom.hideFrom", "value": { "legend": true, "tooltip": true, "viz": false } } ] } ] }, "gridPos": { "h": 8, "w": 12, "x": 12, "y": 4 }, "id": 3, "options": { "legend": { "calcs": [ "lastNotNull" ], "displayMode": "table", "placement": "bottom", "showLegend": true }, "tooltip": { "hideZeros": true, "mode": "multi", "sort": "desc" } }, "pluginVersion": "12.3.3", "targets": [ { "datasource": { "type": "prometheus", "uid": "dfdlzb2hrvlz4b" }, "dateTimeType": "DATETIME", "editorMode": "code", "expr": "bird_protocol_prefix_import_count{\n instance=~\"$instance\",\r\n name=~\"(dn42_peer_|dn42_)[0-9a-zA-Z]+\", \r\n name!~\"dn42_ospf\",\r\n name!~\"dn42_ospf6\", \r\n ip_version=\"4\"\r\n} \u003e 0", "format": "time_series", "formattedQuery": "SELECT $timeSeries as t, count() FROM $table WHERE $timeFilter GROUP BY t ORDER BY t", "intervalFactor": 1, "legendFormat": "{{name}}", "query": "SELECT\n $timeSeries as t,\n count()\nFROM $table\nWHERE $timeFilter\nGROUP BY t\nORDER BY t", "range": true, "refId": "A", "round": "0s" } ], "title": "PITER-IX · Imported Prefixes", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "dfdlzb2hrvlz4b" }, "description": "Accepted route updates and withdrawals per second. Spikes make routing churn immediately visible.", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisBorderShow": false, "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "events / second", "axisPlacement": "left", "barAlignment": 0, "barWidthFactor": 0.6, "drawStyle": "line", "fillOpacity": 8, "gradientMode": "opacity", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "insertNulls": false, "lineInterpolation": "smooth", "lineWidth": 2, "pointSize": 4, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "showValues": false, "spanNulls": true, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "mappings": [ ], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": 0 } ] }, "unit": "ops" }, "overrides": [ ] }, "gridPos": { "h": 8, "w": 12, "x": 0, "y": 12 }, "id": 14, "options": { "legend": { "calcs": [ "lastNotNull" ], "displayMode": "table", "placement": "bottom", "showLegend": true }, "tooltip": { "hideZeros": true, "mode": "multi", "sort": "desc" } }, "pluginVersion": "12.3.3", "targets": [ { "datasource": { "type": "prometheus", "uid": "dfdlzb2hrvlz4b" }, "editorMode": "code", "expr": "sum by (instance) (rate(bird_protocol_changes_update_import_accept_count{instance=~\"$instance\"}[$__rate_interval]))", "format": "time_series", "legendFormat": "{{instance}} · imported updates", "range": true, "refId": "A" }, { "datasource": { "type": "prometheus", "uid": "dfdlzb2hrvlz4b" }, "editorMode": "code", "expr": "sum by (instance) (rate(bird_protocol_changes_withdraw_import_accept_count{instance=~\"$instance\"}[$__rate_interval]))", "format": "time_series", "legendFormat": "{{instance}} · imported withdrawals", "range": true, "refId": "B" }, { "datasource": { "type": "prometheus", "uid": "dfdlzb2hrvlz4b" }, "editorMode": "code", "expr": "sum by (instance) (rate(bird_protocol_changes_update_export_accept_count{instance=~\"$instance\"}[$__rate_interval]))", "format": "time_series", "legendFormat": "{{instance}} · exported updates", "range": true, "refId": "C" }, { "datasource": { "type": "prometheus", "uid": "dfdlzb2hrvlz4b" }, "editorMode": "code", "expr": "sum by (instance) (rate(bird_protocol_changes_withdraw_export_accept_count{instance=~\"$instance\"}[$__rate_interval]))", "format": "time_series", "legendFormat": "{{instance}} · exported withdrawals", "range": true, "refId": "D" } ], "title": "Routing Activity · Updates \u0026 Withdrawals", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "dfdlzb2hrvlz4b" }, "description": "Only non-established BGP sessions are listed. An empty table means all matched sessions are healthy.", "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "custom": { "align": "auto", "cellOptions": { "type": "auto" }, "filterable": true, "footer": { "reducers": [ ] }, "inspect": false }, "mappings": [ { "options": { "0": { "color": "red", "index": 0, "text": "DOWN" } }, "type": "value" } ], "thresholds": { "mode": "absolute", "steps": [ { "color": "red", "value": 0 } ] } }, "overrides": [ ] }, "gridPos": { "h": 8, "w": 12, "x": 12, "y": 12 }, "id": 15, "options": { "cellHeight": "sm", "footer": { "countRows": false, "enablePagination": true, "fields": "", "reducer": [ "sum" ], "show": false }, "showHeader": true, "sortBy": [ { "desc": false, "displayName": "instance" } ] }, "pluginVersion": "12.3.3", "targets": [ { "datasource": { "type": "prometheus", "uid": "dfdlzb2hrvlz4b" }, "editorMode": "code", "expr": "bird_protocol_up{proto=\"BGP\",instance=~\"$instance\"} == 0", "format": "table", "instant": true, "legendFormat": "{{instance}} · {{name}} · IPv{{ip_version}}", "range": false, "refId": "A" } ], "title": "BGP Sessions Requiring Attention", "transformations": [ { "id": "organize", "options": { "excludeByName": { "Time": true, "__name__": true, "export_filter": true, "import_filter": true, "job": true, "proto": true }, "indexByName": { "Value": 4, "instance": 0, "ip_version": 2, "name": 1, "state": 3 }, "renameByName": { "Value": "Status", "instance": "Router", "ip_version": "IP", "name": "Session", "state": "State" } } } ], "type": "table" }, { "datasource": { "type": "prometheus", "uid": "dfdlzb2hrvlz4b" }, "description": "IPv4 and IPv6 prefixes exported by the BIRD protocol named flapalerted. Use the Router selector to isolate a node.", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisBorderShow": false, "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "events / second", "axisPlacement": "left", "barAlignment": 0, "barWidthFactor": 0.6, "drawStyle": "line", "fillOpacity": 6, "gradientMode": "opacity", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "insertNulls": false, "lineInterpolation": "smooth", "lineWidth": 2, "pointSize": 4, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "showValues": false, "spanNulls": true, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "mappings": [ ], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": 0 } ] }, "unit": "short" }, "overrides": [ ] }, "gridPos": { "h": 8, "w": 24, "x": 0, "y": 20 }, "id": 16, "options": { "legend": { "calcs": [ "lastNotNull", "max" ], "displayMode": "table", "placement": "bottom", "showLegend": true }, "tooltip": { "hideZeros": true, "mode": "multi", "sort": "desc" } }, "pluginVersion": "12.3.3", "targets": [ { "datasource": { "type": "prometheus", "uid": "dfdlzb2hrvlz4b" }, "editorMode": "code", "expr": "bird_protocol_prefix_export_count{name=\"flapalerted\",instance=~\"$instance\"}", "format": "time_series", "legendFormat": "{{instance}} · IPv{{ip_version}}", "range": true, "refId": "A" } ], "title": "FlapAlerted · Exported Prefixes", "type": "timeseries" } ], "preload": false, "refresh": "30s", "schemaVersion": 42, "tags": [ ], "templating": { "list": [ { "current": { "text": "All", "value": "$__all" }, "datasource": { "type": "prometheus", "uid": "dfdlzb2hrvlz4b" }, "definition": "label_values(up{job=\"bird_exporter\"}, instance)", "includeAll": true, "label": "Router", "multi": true, "name": "instance", "options": [ ], "query": { "query": "label_values(up{job=\"bird_exporter\"}, instance)", "refId": "PrometheusVariableQueryEditor-VariableQuery" }, "refresh": 1, "regex": "", "sort": 1, "type": "query" } ] }, "time": { "from": "now-5m", "to": "now" }, "timepicker": { }, "timezone": "", "title": "BIRD · Routing Overview", "uid": "f6bde99c-ee14-44cf-9b57-17052329ab55", "version": 15 }{/collapse-item}{/collapse}参考文献Grafana Labs - FRR Exporter - BGP DashboardGrafana Labs - BIRD RS DashboardGrafana Labs - Node Exporter Full DashboardGitHub - czerwonk/bird_exporterGitHub - tynany/frr_exporterPrometheus DocumentationGrafana Documentation
灵感来源最近一直在捣鼓QQ机器人，目前后端比较有名的就是AstrBot和 MaiBot 项目，其中AstrBot注重功能性（有多样化的插件服务）而MaiBot注重LLMs聊天与拟人化，遂想要同时给我的机器人接入两个项目，而由于TX的风控，部署在云服务器上的NapCat容易被下号，而由于网络环境问题，MaiBot最好运行在海外服务器上（可以方便的访问Gemini等AI的api），故想到了将AstrBot与NapCat部署在家里NAS上，MaiBot部署在公网服务器上的思路，拓扑图如下：graph LR subgraph NAS AstrBot <--> NapCat end subgraph Server MaiBot end NapCat <-- WebSocket --> MaiBot{message type="info" content="这里将NapCat和AstrBot部署在同一个服务器上，这是因为AstrBot在执行一些功能的时候需要和NapCat共用文件夹，若分开部署则会出现很多问题"/}部署前期准备{x} 一台公网服务器（最好2c2g及以上配置） {x} 一台家宽网络下的Linux服务器 {x} docker环境开始部署首先进入家里云sshmkdir astrbot cd astrbot wget https://raw.githubusercontent.com/NapNeko/NapCat-Docker/main/compose/astrbot.yml sudo docker compose -f astrbot.yml up -d完成然后进入公网服务器{message type="info" content="该部分以下内容大多直接来自官方文档"/}一、准备麦麦部署环境1.1 创建项目目录mkdir -p maim-bot/docker-config/{mmc,adapters} && cd maim-bot1.2 获取 Docker 编排文件wget https://raw.githubusercontent.com/Mai-with-u/MaiBot/main/docker-compose.yml备用下载方式 若 GitHub 直连不稳定，可使用镜像源：wget https://fastly.jsdelivr.net/gh/Mai-with-u/MaiBot@main/docker-compose.yml1.3删除其中的NapCat容器并取消注释adapter的端口映射使用vim docker-compose.yml编辑，编辑完成后如下:services: adapters: container_name: maim-bot-adapters #### prod #### image: unclas/maimbot-adapter:latest # image: infinitycat/maimbot-adapter:latest #### dev #### # image: unclas/maimbot-adapter:dev # image: infinitycat/maimbot-adapter:dev environment: - TZ=Asia/Shanghai ports: #<--此处修改 - "8095:8095" #<--此处修改 volumes: - ./docker-config/adapters/config.toml:/adapters/config.toml # 持久化adapters配置文件 - ./data/adapters:/adapters/data # adapters 数据持久化 restart: always networks: - maim_bot core: container_name: maim-bot-core #### prod #### image: sengokucola/maibot:dev # image: infinitycat/maibot:latest #### dev #### # image: sengokucola/maibot:dev # image: infinitycat/maibot:dev environment: - TZ=Asia/Shanghai - EULA_AGREE=99f08e0cab0190de853cb6af7d64d4de # 同意EULA - PRIVACY_AGREE=9943b855e72199d0f5016ea39052f1b6 # 同意EULA # ports: # - "8000:8000" volumes: - ./docker-config/mmc/.env:/MaiMBot/.env # 持久化env配置文件 - ./docker-config/mmc:/MaiMBot/config # 持久化bot配置文件 - ./data/MaiMBot/maibot_statistics.html:/MaiMBot/maibot_statistics.html #统计数据输出 - ./data/MaiMBot:/MaiMBot/data # 共享目录 - ./data/MaiMBot/plugins:/MaiMBot/plugins # 插件目录 - ./data/MaiMBot/logs:/MaiMBot/logs # 日志目录 - site-packages:/usr/local/lib/python3.13/site-packages # 持久化Python包 restart: always networks: - maim_bot sqlite-web: # 注意：coleifer/sqlite-web 镜像不支持arm64 image: coleifer/sqlite-web container_name: sqlite-web restart: always ports: - "8120:8080" volumes: - ./data/MaiMBot:/data/MaiMBot environment: - SQLITE_DATABASE=MaiMBot/MaiBot.db # 你的数据库文件 networks: - maim_bot # chat2db占用相对较高但是功能强大 # 内存占用约600m，内存充足推荐选此 # chat2db: # image: chat2db/chat2db:latest # container_name: maim-bot-chat2db # restart: always # ports: # - "10824:10824" # volumes: # - ./data/MaiMBot:/data/MaiMBot # networks: # - maim_bot volumes: site-packages: networks: maim_bot: driver: bridge 二、麦麦环境配置2.1 准备配置文件模板# 获取核心组件配置模板 wget https://raw.githubusercontent.com/MaiM-with-u/MaiBot/main/template/template.env -O docker-config/mmc/.env # 若 GitHub 直连不稳定，可使用镜像源：https://fastly.jsdelivr.net/gh/Mai-with-u/MaiBot@main/template/template.env 获取 adapter 的 config.toml:wget https://github.com/MaiM-with-u/MaiBot-Napcat-Adapter/raw/refs/heads/main/template/template_config.toml -O docker-config/adapters/config.toml # 若 GitHub 直连不稳定，可使用镜像源：https://fastly.jsdelivr.net/gh/Mai-with-u/MaiBot-Napcat-Adapter@main/template/template_config.toml 配置文件里的服务名如不可用可替换为容器名MaiBot_Server 配置可替换成 maim-bot-corenapcat ws 客户端可替换成 ws://maim-bot-adapters:80952.2 预留文件这个文件是 MaiBot 运行统计报告。MacOS/Linux:mkdir -p data/MaiMBot && touch ./data/MaiMBot/maibot_statistics.htmlWindows:mkdir data\MaiMBot && type nul > .\data\MaiMBot\maibot_statistics.html2.3 修改相关配置vim docker-config/mmc/.env需修改以下关键参数:# 网络监听配置 HOST=0.0.0.0修改适配器配置文件:vim docker-config/adapters/config.toml[napcat_server] # Napcat连接的ws服务设置 host = "0.0.0.0" port = 8095 heartbeat_interval = 30 tocken = '123123123' #这里必须要设置，为了NAS和公网服务器建立websocket连接时鉴权 [maibot_server] # 连接麦麦的ws服务设置 host = "core" port = 80002.3 取消注释 docker-compose.yml 的 EULAvim docker-compose.yml # 取消注释以下两行（30-31 行） - EULA_AGREE=bda99dca873f5d8044e9987eac417e01 # 同意 EULA - PRIVACY_AGREE=42dddb3cbe2b784b45a2781407b298a1 # 同意 Privacy2.4 数据库管理工具#sqlite-web: # # 注意：coleifer/sqlite-web 镜像不支持 arm64 # image: coleifer/sqlite-web # container_name: sqlite-web # restart: always # ports: # - "8120:8080" # volumes: # - ./data/MaiMBot:/data/MaiMBot # environment: # - SQLITE_DATABASE=MaiMBot/MaiBot.db # networks: # - maim_bot # chat2db 占用相对较高但是功能强大 # 内存占用约 600m，内存充足推荐选此 chat2db: image: chat2db/chat2db:latest container_name: maim-bot-chat2db restart: always ports: - "10824:10824" volumes: - ./data/MaiMBot:/data/MaiMBot networks: - maim_bot2.5 目录结构. ├── docker-compose.yml ├── data │ └── MaiMBot │ └── maibot_statistics.html └── docker-config ├── adapters │ └── config.toml └── mmc └── .env三、初始化容器环境3.1 首次启动容器生成剩余配置文件docker compose up -d && sleep 15 && docker compose down3.2 调整麦麦配置vim docker-config/mmc/bot_config.toml vim docker-config/mmc/model_config.toml四、启动麦麦4.1 启动所有组件docker compose up -d4.2 验证服务状态docker compose ps配置NapCatNapcat 配置入口: http://公网服务器IP:6099网络配置使用websocket客户端，url为ws://<你的公网服务器ip>:8095 tocken和你之前在adapter中的tocken一致，启用： 大功告成！参考文档：使用 Docker 部署 AstrBot使用 Docker 部署 MaiBot