首页
AndyYan's DN42 Network
友链
关于
Search
1
MaiBot+AstrBot+Napcat分离部署,实用、稳定且拟人化的QQBot部署方案
301 阅读
2
Linux环境下的Silly Tavern 云酒馆 AI 搭建与美化完善(几乎0基础?)
148 阅读
3
IOS系统解压.lz4格式压缩包
111 阅读
4
解决OMV登陆WebUI时400 Bad Request错误
77 阅读
5
Typecho + JOE 主题下 Mermaid 渲染的解决方案
36 阅读
所有文章
NAS
DN42
开源项目
AI
调优
运维
网络安全
登录
Search
AndyYan
累计撰写
12
篇文章
累计收到
1
条评论
首页
栏目
所有文章
NAS
DN42
开源项目
AI
调优
运维
网络安全
页面
AndyYan's DN42 Network
友链
关于
搜索到
11
篇与
的结果
2026-07-02
一次 Looking Glass 参数注入事件的复盘:从 /etc/shadow 泄露到全集群加固
本文隐去了真实主机名、域名与 IP,但保留了完整的攻击 payload 与处置命令,方便复现与自查。TL;DR我的 DN42 集群跑着一个公网可访问的 bird-lg-go Looking Glass,四台节点共用一个前端。其中 bird-lgproxy ≤ v1.4.6 存在参数注入漏洞(CVE-2026-26514,GHSA-3qm5-22pm-wqg9):攻击者通过 traceroute 功能的 q 参数注入 mtr 的 -F 选项,把任意文件当作“报告文件”读出来,最终读到了每台机器的 /etc/shadow——也就是 root 密码哈希。注意 v1.4.6 的修复不完整、可被绕过,真正修好是在 v1.4.7。处置结论:四台节点全部升级到 v1.4.7、轮换全部 root 密码、审计 SSH 密钥与计划任务、确认无后门与异常登录。 漏洞在公网边缘已验证彻底关闭。漏洞原理Looking Glass 的 traceroute 功能,本质上是把用户输入的目标丢给系统的 mtr / traceroute 去跑。问题出在旧版 bird-lgproxy 直接把用户输入拼进 exec.Command,既没有把它当成单一目标、也没有拒绝以 - 开头的输入。于是用户输入里可以夹带选项。攻击者只要构造:-F /etc/shadow test.invalid它就被当成了 mtr -F /etc/shadow test.invalid。而 mtr 的 -F 选项恰好是“从文件读取主机列表(report file)”——mtr 会尝试打开 /etc/shadow,并在报错信息里带出文件内容;而 proxy 用 CombinedOutput() 把 stdout + stderr 一起回显给前端,于是文件内容就顺着错误输出泄露了出去。test.invalid 只是凑一个“看起来像目标”的占位。于是一条 HTTP GET 就能读任意文件:# 攻击者视角:通过公网 LG 前端读取 /etc/shadow curl 'https://<lg-frontend>/traceroute/<node>/-F%20%2Fetc%2Fshadow%20test.invalid'这类问题的根源是老生常谈的一句话:永远不要把用户输入拆成命令行参数。目标就该当作一个不可拆分的整体字符串,任何以 - 开头的东西都不该被解释成选项。下面这张图把整条注入链路画出来了——注意红色路径是漏洞如何从一个 HTTP 参数一路变成任意文件读取的:flowchart LR A[公网攻击者] -->|"GET /traceroute/node/<br/>-F /etc/shadow test.invalid"| B[LG 前端<br/>bird-lg] B -->|转发请求<br/>源 IP = 前端| C[节点 bird-lgproxy<br/>:8000] C -->|"输入直接进 exec.Command<br/>未拒绝 - 开头参数"| D{"mtr -F /etc/shadow<br/>test.invalid"} D -->|-F 被解释为<br/>read report file| E[["读取 /etc/shadow"]] E -->|回显文件内容| A style A fill:#ffd5d5,stroke:#c0392b style D fill:#ffe6cc,stroke:#e67e22 style E fill:#ffd5d5,stroke:#c0392b,stroke-width:2px攻击时间线从被攻击节点的 bird-lgproxy 日志里,能看到一段非常“教科书”的探测→提权信息收集过程(时间为东八区,2026 年 7 月 1 日晚):时间(UTC+8)谁干了什么07-01 21:47攻击者(经前端转发)bird-lgproxy 读 /etc/passwd(枚举用户、确认可读文件)07-01 21:53攻击者读 /root/.bash_history(翻历史命令,找线索/凭据)07-01 21:57攻击者读 /etc/shadow(拿到 root 密码哈希)用时序图看这十分钟的动作更直观,也能看清“攻击者 → 前端 → proxy → 文件系统”这条链路上,日志分别记在了哪一层:sequenceDiagram autonumber actor Atk as 攻击者(公网) participant FE as LG 前端 bird-lg participant PX as 节点 bird-lgproxy participant FS as 节点文件系统 Note over Atk,FS: 2026-07-01 晚(UTC+8) Atk->>FE: 21:47 GET -F /etc/passwd FE->>PX: 转发(源 IP = 前端) PX->>FS: mtr -F /etc/passwd FS-->>Atk: 用户列表回显 Note right of PX: proxy 日志只记到<br/>前端 IP,非攻击者 Atk->>FE: 21:53 GET -F /root/.bash_history FE->>PX: 转发 PX->>FS: 读取历史命令 FS-->>Atk: 命令历史回显 Atk->>FE: 21:57 GET -F /etc/shadow FE->>PX: 转发 PX->>FS: mtr -F /etc/shadow FS-->>Atk: root 密码哈希泄露日志里的 payload 长这样(URL 编码前):GET /traceroute?q=-F+/etc/passwd+<占位主机> GET /traceroute?q=-F+/etc/shadow+<占位主机>从 passwd → bash_history → shadow 的顺序很典型:先确认漏洞可用、再翻可能藏凭据的地方、最后直取密码哈希准备离线爆破。整个过程只花了十分钟。取证的一个坑:日志里的“攻击者 IP”是我自己的前端机这里有个值得单独拎出来讲的教训。bird-lg 是前端 + 后端 proxy 分离的架构:公网用户访问前端,前端再把 traceroute 请求转发给各节点的 bird-lgproxy。所以当我去被攻击节点的 proxy 日志里查“攻击者 IP”时,看到的源地址全是前端机器自己的 IP——因为对 proxy 来说,请求确实是前端发来的。真正的攻击者公网 IP,只存在于前端(反向代理 / 前端容器)的访问日志里。而等我开始做取证时:proxy 容器在升级时被重建,日志只剩当天;前端所在机器的反代访问日志已经轮转,最早的记录已经晚于攻击发生的时间;各节点 journald 的留存窗口也已经滚过了原始攻击时刻。结果就是:现存日志里所有 -F /etc/shadow 的请求,溯源后都指向我自己后来做的 PoC 验证和加固测试(payload 里还带着我自己打的 poc.invalid、codex-audit 之类的标记)。原始攻击者的真实 IP,随日志轮转丢失了。溯源时对每一条 -F /etc/shadow 请求的判定逻辑大致是这样,最后能落到“真实攻击者 IP”的路径全被日志留存挡死了:flowchart TD Q["现存日志里的<br/>-F /etc/shadow 请求"] --> M{"payload 带<br/>poc.invalid / codex-audit ?"} M -->|是| SELF["自己的 PoC / 加固验证"] M -->|否| T{"时间早于<br/>已知攻击时刻 ?"} T -->|否| SELF2["事后测试<br/>(仍是自己)"] T -->|是| L{"能查到<br/>前端访问日志 ?"} L -->|否:已轮转/重建| LOST["真实攻击者 IP<br/>已丢失 ✗"] L -->|是| REAL["定位攻击者 IP ✓"] style SELF fill:#d5f5e3,stroke:#27ae60 style SELF2 fill:#d5f5e3,stroke:#27ae60 style LOST fill:#ffd5d5,stroke:#c0392b,stroke-width:2px style REAL fill:#d6eaf8,stroke:#2980b9也做了兜底核查——确认没有异常后果:登录审计:所有成功的 root 登录源 IP 要么是我自己的出口,要么是我常用宽带 IP 段(带密钥登录用的是我自己的部署密钥),没有陌生境外 IP 成功登录,无横向移动迹象。authorized_keys:逐个核对指纹,只有我自己的密钥,无陌生 key。计划任务 / 进程:无后门 cron,无可疑常驻进程。教训很直接:面向公网的服务,一定要把访问日志的留存时间调长、并集中收走。等出事了再回头翻,日志往往已经不在了——尤其是这种“真实来源只记录在前端一层”的架构,取证窗口比你想的更短。漏洞报告与致谢发现异常后,我第一时间意识到这是个真实的安全问题,但我本身对安全研究并不在行。于是我把情况同步给了朋友 @KaguraiYoRoy,最终由他完成了漏洞分析、复现与规范的安全报告,一起负责任地披露给了上游作者。上游据此发布了官方安全公告并修复:GHSA-3qm5-22pm-wqg9 — Argument Injection / Exposure of Sensitive InformationCVE-2026-26514 · CWE-88(Argument Injection)+ CWE-200(Sensitive Info Exposure)CVSS 3.1: 7.5 High(AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N)影响版本:bird-lgproxy ≤ 1.4.6 · 修复版本:1.4.7 · 发布:2026-07-02致谢:KaguraiYoRoy(Analyst)、AndyYJF(Reporter)公告原文:https://github.com/xddxdd/bird-lg-go/security/advisories/GHSA-3qm5-22pm-wqg9借这次事,也想说一句:不懂安全不丢人,但发现问题别憋着。找个懂行的朋友一起走负责任披露的流程,比自己瞎折腾或者装作没看见都强得多。修复:升级到 v1.4.7需要强调的是:v1.4.6 的第一次修复并不彻底——它可以被无空格变体绕过,比如 -F/etc/shadow(选项和路径连在一起,绕过了当时基于空格的判断)。真正修好是在 v1.4.7:它对 traceroute 目标增加了 strings.HasPrefix(query, "-") 检查,任何以 - 开头的输入直接拒绝,从根上断掉了“把输入当选项”的可能。这样 -F /etc/shadow 会被直接拦下返回 Invalid target.,CVE 到这里彻底堵死。我把四台节点全部拉到了 v1.4.7。集群里有两种部署方式,处理方式不同:systemd 直接跑二进制的节点——换二进制 + 重启:cd /tmp curl -sLO https://github.com/xddxdd/bird-lg-go/releases/download/v1.4.7/bird-lgproxy-go-v1.4.7-linux-amd64.tar.gz tar xzf bird-lgproxy-go-v1.4.7-linux-amd64.tar.gz cp -a /usr/local/bin/bird-lgproxy-go /usr/local/bin/bird-lgproxy-go.pre-v1.4.7.bak # 留回滚点 cp -f bird-lgproxy-go /usr/local/bin/bird-lgproxy-go chmod +x /usr/local/bin/bird-lgproxy-go systemctl restart bird-lgproxyDocker 部署的节点——用 FROM scratch 的本地镜像,替换其中的 proxy 二进制后重建:# 构建上下文里就一个 Dockerfile + proxy 二进制 + 自带的 traceroute # Dockerfile: # FROM scratch # COPY proxy /proxy # COPY traceroute /traceroute # ENTRYPOINT ["/proxy"] cp -f proxy.v147 ./bird-lgproxy-v1.4.7/proxy docker compose up -d --build bird-lgproxy验证:用攻击者的原始 payload 打自己修复不能只看版本号,得用攻击者的真实路径去验证。我把原始 payload 通过公网前端重放到每一台节点:for node in <node1> <node2> <node3> <node4>; do curl -s "https://<lg-frontend>/traceroute/$node/-F%20%2Fetc%2Fshadow%20test.invalid" \ | grep -c 'root:' # 期望:0 done四台全部返回 Invalid target.,/etc/shadow 内容零泄露。直接对本地 :8000 打也是同样结果:curl -s 'http://127.0.0.1:8000/traceroute?q=-F+/etc/shadow+8.8.8.8' # 旧版:吐出 /etc/shadow 内容 # v1.4.7:HTTP 400 / Invalid target.收尾清单{x} 四台节点升级到 v1.4.7,保留回滚二进制/compose{x} 用原始 payload 经公网前端重放验证,四台 /etc/shadow 零泄露{x} 轮换全部 root 密码(≥16 位混合){x} 审计 authorized_keys 指纹、cron、常驻进程——无异常{x} 核查登录历史——无陌生 IP 成功登录几点反思别把用户输入拆成命令行参数。 这不是 LG 独有的坑,任何把外部输入拼进 exec 的地方都要警惕以 - 开头的注入。目标当整体字符串处理是底线。公网服务的 proxy 别裸奔在 0.0.0.0。 我这次四台的 bird-lgproxy :8000 都是公网直接可达的——下一步准备把它收敛到只允许前端访问(iptables / 只监听内网)。少一个公网入口,就少一层攻击面。日志留存要提前配好、集中收走。 出事后才发现真正的攻击者 IP 只记录在前端一层、而且已经轮转丢了。取证的黄金窗口比想象中短得多。本文用于记录与分享,已隐去全部真实主机名、域名与 IP。如果你也在跑公网 Looking Glass,建议立刻确认 bird-lgproxy 版本 ≥ v1.4.7。
2026年07月02日
17 阅读
0 评论
0 点赞
2026-06-26
RP Hub——更适合新手的类Silly Tavern项目
声明:此项目二改自 RP-Hub ,根据原项目作者要求,本人已得到原作者关于二改的授权。二改过程中使用了AI,若您对此反对可停止阅读前言众所周知,原版的Silly Tavern,动画是 没有 的,界面是 臃肿 的,对新手是 地狱 的。我一直想找到一个易用且现代的类Silly Tavern项目,某次偶然在B站看到了这个视频:{bilibili bvid="BV1yKSSBKERq" page=""/}可谓是正中下怀啊!但是仔细看了看项目发现,由于偏向仅前端开发,项目没有原版Silly Tavern的多用户功能,也没有多端同步能力,和我的预期相差较多,于是 秉承着Plus订阅不用白不用的态度 我决定手搓出多用户和同步功能。博弈于是我便把项目克隆,打开了Codex,开始了和AI博弈的两天。过程就不多说了,本次二改项目使用了GLM-5.2、GPT-5.5,Claude-Opus-4.6模型共同开发,由GPT主写代码,GLM进行代码审批, Claude负责摸鱼 。跌跌撞撞花了不少token,反正是做出来了。 成果二改项目我已经放在了 Github 在原纯前端项目基础上,新增了以下功能(详见 DEPLOY.md):多用户体系JWT 认证(用户名 + 密码 + 刷新令牌轮转)用户数据云端同步(基于时间戳的智能合并,非整体覆盖)本地模式 / 服务端模式可切换(未登录时与原项目完全一致)公共角色库(自建)用户提交角色卡 → 管理员审核 → 公共展示 → 其他用户下载与原作者的「万相广场」并存,侧边栏明确区分后台管理(admin.html)系统统计(用户/角色卡/登录趋势)用户管理(增删改查、封禁/解封、API Key 绑定、配额)角色卡审核(预览、通过、拒绝带理由、下架)公告管理(创建/编辑/置顶,前端主页自动展示)API 用量统计、审计日志站点公告系统管理员发布公告 → 前端主页弹窗展示(区别于原版本更新日志)
2026年06月26日
4 阅读
0 评论
0 点赞
2026-06-20
StableDiffusion学习札记_P1
前言之前就一直想玩StableDiffusion奈何没有合适的显卡。高考完终于买了个5080的笔电想着是时候开玩了~初步了解鉴于StableDiffusion的webui有段时间没更新了,我选择使用comfyUI作为可视化界面----更加现代化,并且拓展性更强开整1部署comfyUI对于新手来说,使用StabilityMatrix部署comfyUI是个不错的选择1.1建一个固定目录在一个空间比较大的盘,新建一个文件夹用于存放后续文件,比如:D:\AI1.2下载StabilityMatrix与安装前往github,在 这里 下载最新版release安装到:D:\AI\StabilityMatrix{alert type="warning"}注意:下载下来的压缩文件必须解压后运行,否则安装程序受限{/alert}第一次打开 Stability Matrix,通常会让你设置一个数据目录 / Library 路径。填写:D:\AI\AIData以后模型、包、缓存都存这个目录。1.3安装comfyUI打开 Stability Matrix 后,找到它里面的包 / Packages / Install入口。找到 ComfyUI安装 最新版其它设置都保持默认装完后,点击 Launch / 启动{alert type="warning"}注意,下载过程需要从github拉取release,若使用代理,请开启tun模式。{/alert}若一切正常,应该会出现这个提示:[INFO] To see the GUI go to: http://127.0.0.1:8188访问即可。2.加入模型,创建工作流2.1模型获取新手可以直接使用我在网上找到的资源。只有度盘链接还请见谅 {cloud title="SD" type="bd" url="https://pan.baidu.com/s/1v2g_4hKDafaS9tcYEouYxA?pwd=1ajk" password="1ajk"/}或者前往 C站 寻找模型{collapse}{collapse-item label="附C站Content Moderation设置指南" close}代理 访问https://civitai.red/登录账号点击右上角个人图标,选择最下方的齿轮图标进入账户设置找到Content Moderation修改注:一定要使用代理,也不要访问.com域名{/collapse-item}{/collapse}2.2模型识别模型的识别比较重要,使用错误的模型+Lora会导致报错首先是 模型文件类型 建议直接识别后缀1..safetensors / .ckpt 这个属于Checkpoint(大模型主体),可以单独出图模型代数识别:sd15 / 1-5 / v1-5 → SD1.5sd21 / 2-1 → SD2.1xl / SDXL → SDXL2..safetensors 这个属于LoRA(风格/角色插件)不能单独出图,必须 + base model3..vae.safetensors这个是VAE,是用来改善颜色/对比度的4..pt / .bin这个属于Embedding / Textual Inversion,是用于用于触发关键词的2.3创建工作流打开ComfyUI,你应该能看见一个空白画布双击左键,搜索 Load Checkpoint ,添加节点双击左键,搜索 CLIP Text Encode ,添加节点(要添加两个,这是正向和反向提示词的输入处)双击左键,搜索 KSampler ,添加节点双击左键,搜索 VAE Decode ,添加节点双击左键,搜索 Save Image ,添加节点连线规则: 从 Checkpoint加载器(简易)连出去:模型 → KSampler 的 modelCLIP → 两个 CLIP文本编码 节点的 clipVAE → VAE Decode 的 vae 文本节点:正向 CLIP文本编码 → KSampler 的 positive负向 CLIP文本编码 → KSampler 的 negative Empty Latent Image:输出 → KSampler 的 latent_image KSampler:输出 → VAE Decode 的 samples VAE Decode:输出 → Save Image 的 images 3.开始作画3.1填入提示词正向提示词填到第一个 CLIP 文本编码里:masterpiece, best quality, ultra detailed, 1girl, portrait, soft lighting, detailed eyes负向提示词填到第二个 CLIP 文本编码里:low quality, blurry, bad anatomy, extra fingers, deformed hands, watermark, textEmpty Latent Image 设置:width: 1024 height: 1024 batch_size: 1KSampler 设置:steps: 28 cfg: 6 sampler_name: dpmpp_2m //自行选择,推荐这个 scheduler: karras最后点击 运行 成图:
2026年06月20日
11 阅读
1 评论
0 点赞
2026-06-11
博客封面是怎么来的?
这个博客上的封面是怎么来的?其实是用 GPT-Image-2生成的随手让它写了个prompt可以保持创作风格:我会给你一篇技术博客原文,请你根据文章内容生成一张博客封面图。 要求: 1. 先理解文章主题,不要逐字照搬正文。 2. 封面用于博客首页卡片展示,显示区域较小,所以画面必须简洁、清晰、缩略图可读。 3. 图片比例为 3:2,横向构图,例如 1200x800。 4. 风格为深色科技风、蓝紫霓虹、现代极客风,适合技术博客。 5. 不要做复杂信息图,不要大量小字,不要密集代码,不要完整教程步骤。 6. 画面只保留一个大标题、一个短副标题,以及 1 到 3 个能表达主题的简洁图标或抽象元素。 7. 中文标题要大、粗、清晰,距离边缘留足安全边距,避免被博客卡片裁切。 8. 右侧可以放终端、服务器、网络连接、代码窗口、流程节点、工具图标等抽象元素,但不要太复杂。 9. 不要水印,不要真实品牌 Logo,不要生成过多装饰。 10. 如果文章主题偏运维、Linux、网络、自建服务,请优先使用终端、服务器、连接线、状态图标等视觉元素。 请根据下面的文章原文,自动提炼: - 封面主标题 - 封面副标题 - 画面主体元素 - 最终生成一张 3:2 技术博客封面图 文章原文如下: 【在这里粘贴文章全文】
2026年06月11日
14 阅读
0 评论
1 点赞
2026-04-05
Typecho + JOE 主题下 Mermaid 渲染的解决方案
阅前须知:由于博主现在 并没有太多自己写代码甚至脚本的能力 ,本篇文章在我和AI共同解决了这个问题之后 直接由AI生成 ,若你对此感到反感,可以现在 关闭该帖 ( 我是废物 )一、问题背景在 Typecho 中使用 Mermaid 一直不算难,但一旦换成 JOE 主题,问题就开始变得复杂:写好的 `mermaid 代码块被当成普通代码高亮插件明明启用了,但图表不渲染翻页(PJAX)后 Mermaid 直接失效我一开始也尝试用常规方案(正则替换 HTML),结果发现:👉 根本不稳定二、问题的本质JOE 主题做了很多“增强”,包括:自定义代码高亮(Prism / Highlight.js)改写 Markdown 输出结构使用 PJAX(局部刷新页面)这导致一个核心问题:你在后端生成的 HTML,很可能在前端被再次修改甚至覆盖例如你期望的是:<pre><code class="language-mermaid"></code></pre>但实际可能变成:<pre class="language-mermaid"></pre>甚至:<div class="joe_code"> <pre>...</pre> </div>👉 结构不稳定 → 正则必炸三、传统方案为什么不行?常见插件思路:Markdown → HTML → 正则替换 → <pre class="mermaid">问题在于:依赖 HTML 结构(不可靠)容易被主题覆盖PJAX 后不会重新执行结论:后端改 HTML,在 JOE 这种强主题下是错误方向四、最终解决方案:前端接管我最后采用的是:✅ 完全绕过后端,前端动态解析 Mermaid核心流程:页面加载 ↓ 扫描所有 language-mermaid 代码块 ↓ 替换为 .mermaid DOM ↓ 调用 Mermaid 渲染五、核心实现解析1. 扫描代码块const blocks = document.querySelectorAll( 'pre code.language-mermaid, pre.language-mermaid' );为什么这样写?👉 兼容两种结构:<pre><code class="language-mermaid"></code></pre><pre class="language-mermaid"></pre>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);最终结构:<div class="mermaid-container"> <div class="mermaid">...</div> </div>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 '<style> .mermaid-container { text-align: center; margin: 1em 0; } </style>'; } 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 <script src="{$cdn}"></script> <script> (function () { function getTheme() { if ({$autoDark} == 1) { if (document.documentElement.classList.contains('dark') || document.body.classList.contains('dark')) { return 'dark'; } } return '{$theme}'; } function convertMermaid() { // 找到所有 mermaid 代码块 const blocks = document.querySelectorAll( 'pre code.language-mermaid, pre.language-mermaid' ); blocks.forEach(function(codeBlock) { // 防重复处理 if (codeBlock.dataset.mermaidDone) return; codeBlock.dataset.mermaidDone = "1"; let code = codeBlock.textContent; // 创建容器 const container = document.createElement('div'); container.className = 'mermaid-container'; const mermaidDiv = document.createElement('div'); mermaidDiv.className = 'mermaid'; mermaidDiv.textContent = code; container.appendChild(mermaidDiv); // 替换整个 pre let pre = codeBlock.closest('pre'); if (pre) { pre.replaceWith(container); } else { codeBlock.replaceWith(container); } }); } function renderMermaid() { if (typeof mermaid === 'undefined') { console.warn('Mermaid not loaded'); return; } try { mermaid.initialize({ startOnLoad: false, theme: getTheme() }); mermaid.init(undefined, document.querySelectorAll('.mermaid')); } catch (e) { console.error('Mermaid error:', e); } } function run() { convertMermaid(); renderMermaid(); } // 首次加载 document.addEventListener('DOMContentLoaded', run); // JOE PJAX document.addEventListener('pjax:complete', function () { run(); }); })(); </script> HTML; } }{/collapse-item}{/collapse}十二、使用方法:把这个文件放到:/usr/plugins/Mermaid/Plugin.php进入 Typecho 后台: 控制台 → 插件 → 启用 Mermaid 写文章时使用 Mermaid就直接在 Markdown 里写:发布后就会自动渲染成图。
2026年04月05日
36 阅读
0 评论
0 点赞
1
2
3