只要是跟博客相关的东西，我就往这个分类里放。
我对 AI 的使用一直相当朴素，无非是把它当成一个更聪明一点的搜索引擎：遇到不会的东西，就在网页上来回车轮战，让 AI 猜解决方案，再把答案手动搬回编辑器里慢慢调试，最后一点点把功能做出来。例如之前提到的《给博客代码框增加复制按钮》，就是这样一点一点折腾出来的。彼时的 AI 于我而言，更像一个“贴心的老师”——它愿意告诉我答案，却不能直接帮我写作业。因此，我对博客的修改大多只能停留在小修小补的层面。涉及稍微复杂一些的东西，我就无能为力了。直到最近，我接触到了支持 AI Agent 的 IDE。第一次尝试借助 Google Antigravity 内置的 Claude Opus 模型编写小工具，亲眼看着代码从零开始一点点搭起来，我才真正意识到，如今的 AI 编程工具和前几年已经不是一回事了。它不再只是帮你补全几行代码、修正几处语法错误，而是可以直接阅读、理解，甚至参与修改整个项目。对于我这种完全不懂编程的小白来说，这种体验多少有点魔幻。于是，我也终于决定开始实施一项蓄谋已久的计划：翻新 VOID 主题。更新 VOID 主题如果你对这个 AI 适配的 VOID 主题感兴趣，可以访问 这里 下载主题压缩包并安装。距离 VOID 上一个 3.5.1 版本发布，已经过去了五年多。其间，Typecho 先后经历了 1.2.1 的小版本更新和 1.3.0 的大版本更新，而旧版 VOID 早在 1.2.0 时期就已经有启用后白屏的问题。所以这一次，我没有一上来就急着做兼容适配，而是先借助 Claude Opus 4.6，把这些年我自己陆续加在 VOID 上的小改动一点点迁了过去。 毕竟对我来说，这些东西比适配新版 Typecho 还要更紧迫一些。而后，我才让 Claude Code 针对 Typecho 1.3 及 PHP 8.2 的升级作专项适配，解决主题白屏等一系列兼容性问题。这中间我充分利用了上班摸鱼的时间，断断续续修改了大约一周。功能增强在我使用 VOID 的这些年，基于个人需求添加或改进了许多功能。我一直在思考这些小东西的必要性，是否只有我自己才用得到。最终还是决定，既然这是我个人修改的主题，那么就应该优先考虑自己的需求。在不破坏原主题整体设计的前提下，我完全可以将这些功能整合进主题源码，并公之于众。代码复制代码框的复制按钮其实早就合并到官方 VOID 仓库中了，这次是让 AI 根据原生剪切板 API 做了优化。原本是想让它抄一下 GitHub 的剪切板方案，但没能抄成功。GitHub 的复制按钮最后改成鼠标划过代码框才浮出复制按钮，复制成功的提示则还是放在弹窗上。OwO 表情包这一块我曾让 Z 酱帮忙提交了我一些新增的米哈游表情包。这次则是针对我自己真实的使用情况，对这些表情包做了一些增删。米哈游表情包其实常用的还是不多。写到这我才想起来，如果新增的表情包太多，超出数量的分组会因为溢出而被隐藏。于是火速让 Codex 修了这个 bug，现在的表情包选框是这样的：支持横向滚动但测试的时候又发现，这个表情包框只会往下弹，如果是没有评论的文章，就会把底部都撑开。于是就让 AI 糊成了这样：感觉实际效果还是有些不太对，等后续有想法了再作修改吧。归档页增强这就完全属于我自娱自乐的功能了。去年写总结的时候想看看今年总共写了多少字、哪些主题，觉得一篇篇手数好麻烦，就让 AI 糊了这么个功能。现在，归档页支持年度文章发布统计，只需将鼠标划过年份，即可展示年度总字数与分类聚合信息；同时，在文章列表右侧增加了其关联的分类标签，实测超多分类也可以正常显示。该功能不可关闭。文章过时提示开关VOID 会默认在文章发布 90 天后，提示其信息可能过时。例如这个提示对于教程类或是强时效类的文章，这个提示很有必要。但若是日常类或其他不受时间影响的文章，就有些多余了。于是，我在文章编辑页添加了一个开关，可按需展示内容是否过时。新文章默认关闭过时提示。主图来源我的博客使用了大量来自 Pixiv、Twitter 和其他网站的图片。考虑到可能存在的风险，我曾使用 Copyright 插件将主图来源展示在文末。但 VOID 的一大特色便是其主图，主图在主题的整体布局中具有十分重要的地位。那么，主图来源理应受到同等重视。可我对这方面的设计毫无头绪。在询问 Gemini 后，它给出了下面 4 种方案。 /* ========================================= 图源预览组件 - 独立作用域样式 ========================================= */ .source-demo-wrapper { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif; margin: 30px 0; border: 1px solid #eaeaea; border-radius: 12px; overflow: hidden; box-shadow: 0 4px 12px rgba(0,0,0,0.05); background: #fff; } /* 模拟头图区域 */ .demo-hero { position: relative; height: 260px; background: linear-gradient(rgba(0,0,0,0.3), rgba(0,0,0,0.6)), url('https://static.himiku.com/2026/02/23/699c55202b36b.webp') center/cover no-repeat; display: flex; flex-direction: column; justify-content: center; align-items: center; color: white; text-align: center; padding: 0 20px; } .demo-hero-title { margin: 0 0 12px 0; font-size: 1.8rem; font-weight: 500; color: #fff; text-shadow: 0 2px 4px rgba(0,0,0,0.5); } .demo-meta { font-size: 0.9rem; color: rgba(255,255,255,0.85); display: flex; flex-wrap: wrap; justify-content: center; gap: 10px; text-shadow: 0 1px 2px rgba(0,0,0,0.6); } /* === 4种方案样式 === */ /* 方案1：右下角直接文字 */ .demo-src-1 { position: absolute; bottom: 12px; right: 20px; font-size: 0.8rem; color: rgba(255, 255, 255, 0.6); text-shadow: 0 1px 2px rgba(0,0,0,0.8); } .demo-src-1 a { color: inherit; text-decoration: none; border-bottom: 1px dashed rgba(255,255,255,0.4); } .demo-src-1 a:hover { color: #fff; border-bottom-style: solid; } /* 方案2：融入 Meta 数据 */ .demo-src-2 { display: inline-block; } .demo-src-2::before { content: "·"; margin: 0 8px; } .demo-src-2 a { color: inherit; text-decoration: none; border-bottom: 1px solid rgba(255,255,255,0.4); padding-bottom: 1px; } /* 方案3：极简相机图标 + Hover */ .demo-src-3 { position: absolute; bottom: 15px; right: 20px; display: flex; align-items: center; color: rgba(255, 255, 255, 0.5); font-size: 0.8rem; cursor: pointer; } .demo-src-3 .icon { font-size: 1.2rem; margin-left: 8px; filter: drop-shadow(0 1px 2px rgba(0,0,0,0.8)); transition: all 0.3s ease; } .demo-src-3 .text { opacity: 0; transform: translateX(10px); transition: all 0.3s ease; background: rgba(0,0,0,0.6); padding: 2px 8px; border-radius: 4px; pointer-events: none; backdrop-filter: blur(4px); } .demo-src-3:hover .icon { color: #fff; } .demo-src-3:hover .text { opacity: 1; transform: translateX(0); pointer-events: auto; } .demo-src-3 a { color: white; text-decoration: none; border-bottom: 1px solid rgba(255,255,255,0.4); } /* 模拟正文区域与方案4 */ .demo-content-area { padding: 25px 30px; background: #fff; } .demo-src-4 { text-align: right; font-size: 0.85rem; color: #999; margin-bottom: 15px; } .demo-src-4 a { color: #999; text-decoration: underline; text-decoration-color: #ddd; } .demo-src-4 a:hover { color: #666; text-decoration-color: #999; } .demo-text-p { color: #444; font-size: 1rem; line-height: 1.7; margin: 0; text-indent: 2em; } /* 隐藏类 */ .demo-hidden { display: none !important; } /* 控制面板区域 */ .demo-controls-panel { background: #f8f9fa; border-top: 1px solid #eaeaea; padding: 20px 30px; } .demo-controls-panel h4 { margin: 0 0 15px 0; font-size: 1rem; color: #333; } .demo-controls-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 12px; } .demo-controls-grid label { display: flex; align-items: center; font-size: 0.9rem; color: #555; cursor: pointer; } .demo-controls-grid input[type="radio"] { margin-right: 8px; margin-top: 0; } 文章封面图源样式演示 作者名称 · 2026-02-23 📷 题图：Pixiv @某画师 图源: Pixiv @某画师 题图来自 Pixiv 📷 封面图片由 某画师 提供 （正文演示区域）前些时间提到，我的群晖 NAS 数据被套件误删了。由于是第三方套件造成的... 🎨 点击切换图源展示方案预览 方案1：右下角直接文字 方案2：极简相机图标 (Hover展开) 方案3：融入文章数据区 方案4：正文右上方 (Medium风格) function switchDemoDesign(num) { // 隐藏所有方案 document.getElementById('demo-src-1').classList.add('demo-hidden'); document.getElementById('demo-src-2').classList.add('demo-hidden'); document.getElementById('demo-src-3').classList.add('demo-hidden'); document.getElementById('demo-src-4').classList.add('demo-hidden'); // 显示选中的方案 document.getElementById('demo-src-' + num).classList.remove('demo-hidden'); } 如你所见，我选择的是最后一种。设置也很简单。使用 markdown 或 HTML 格式填写主图来源，即可展示在文章顶部。主图预览严格来说，这个功能也有些鸡肋。但对我而言，它解决了一个很具体的问题：在正式发布前，确认主图在首页的展示效果。Typecho 并不像 WordPress 那样提供完整的文章预览功能，所以我以前只能在本地另开一个测试博客，发布之后再查看实际呈现。这样的流程不仅低效，也很折腾。于是，我索性花了几天时间，借助 AI 把这个功能做了出来。现在，VOID 支持在编辑页面预览首页封面卡片的实际效果，支持原 VOID 主题预设的三种首页主图样式，支持预览卡片的摘要随实际摘要变动。卡片上方的文字描述可能还需要再稍微改改，但预览效果大体上是符合实际首页的。评论交互增强以前的评论其实也没什么大问题，只是层级一多，就看不出来回复顺序。中间考虑过这么设计。但这样子的话，后续的回复会随着评论量增加越来越往右边挤。我把目光移到了隔壁 B 站。最后还是抄了一把叔叔的设计，为 VOID 的评论区做了这几点改动：不再纠结层级问题，不管是谁回复谁，都按回复时间顺序排列第一条回复、自己回复自己，均隐去 回复@xxx 的字样将评论时间、赞踩按钮移动到了评论主体下方，回复按钮转移到踩按钮的右侧默认折叠超过两条以上的评论折叠效果如下图所示：折叠时会有轻微的回弹，应该是个 bug，我问不出原因来，暂且搁置……同时，我还调整了回复按钮的交互。点击 回复按钮 展开评论框时，回复按钮会自动变成 取消回复 按钮。我本来想干掉右侧的 取消回复 按钮，但 AI 说应当保留，避免访客找不到回复按钮，就留着了。字数统计又一个鸡肋的功能，用于实时预估当前编辑内容的字数、图片数和标签数。该功能依赖 VOID 插件，随插件启用生效。兼容适配关于 VOID 在 Typecho 1.2.0 等后续版本白屏的问题，AI 给出了几点主要原因：继承签名对齐 (致命修复)：修复了 libs/Comments.php。Typecho 1.3 核心类引入了 PHP 强类型提示，主题原有的 ___children() 等重写方法因缺少 : array 或类型不匹配导致 Fatal Error (白屏)。Null 传参防御：PHP 8.1+ 严禁向系统函数（如 preg_match）传递 null。我们在libs/Utils.php (UA 处理) 和 libs/Contents.php (图片尺寸) 处添加了严谨的 null 保护。调试开关：发现主题 functions.php 的 error_reporting(0) 会掩盖报错堆栈，适配阶段已建议通过控制台或手动调整显隐。这一部分就太过硬核了，没有我能插嘴的余地，AI 说啥就是啥。后台优化这个想法，源于我在添加字数统计功能时的一次临时起意。原本我并没有打算细化这一部分的样式，没想到 AI 在生成功能时，顺带把字数统计区域的视觉样式也设计了出来。实际效果意外地不错，于是我索性以此为起点，将整个 VOID 中涉及到的字段样式重新梳理并统一设计了一遍。整体设计基本由 Gemini 主导。它建议我将 选项 改为滑块、将 是否 改为开关；而我则根据自己的理解，把文章类型中的 landscape 译作 封面文章，以便与旁边的 一般文章 在字面上形成对应。我还调整了部分字段的展示逻辑。例如，只有在填写文章主图后，才会展开与主图相关的其他设置。首页卡片预览也默认隐藏，只能手动展开查看。展开后还可以根据首页主图样式自由调整并预览实际效果。体验优化在把我需要的功能全部补齐之后，我又问 AI：VOID 还有哪些地方值得继续升级？它首先指出的是 PJAX 的问题。PJAX 上一次更新是 2017 年，而今天的现代浏览器已经具备实现这类局部刷新的核心能力。于是，我让 AI 把主题里原本依赖 jquery.pjax 的插件式方案，重构成了一个基于浏览器原生能力实现的 VoidPjax。新的实现直接使用 fetch、DOMParser、history.pushState/popstate 和原生事件代理来完成页面局部跳转、容器替换与历史管理，同时保留 pjax:* 事件兼容层。这样不仅减少了对老旧插件的依赖，也让后续在脚本重跑、一次性脚本去重、Safari 兼容，以及评论区局部刷新这类细节场景上的控制更加从容。也许应该直接干掉 PJAX，但我没什么好的想法让 AI 执行，改天再问问看。此外，bigfoot 已经十几年没更新过了，我用 littlefoot 完整替换了 bigfoot，优化了脚注在暗色模式、触屏设备和 PJAX 场景下的体验；pangu.js 也一并更新到最新版本，并切换使用新版 API，减少脚注、数学公式附近的错误加空格问题；MathJax 也从 2.7.4 迁移到 4.1.1，并精简运行时使用的资源包。数学公式我用得不多，实际使用中可能会存在其他未知的 bug。jQuery 更新到了 3.7.1；Prism高亮插件更新到了 1.30.0；fancybox 大版本改动太大，更到了最后一个 3.5.7；Headroom 导航栏自动隐藏、tocbot 目录树等也一并更新到了最新版本。其他优化则是在主题构建方面，例如：移除 node-sass，切换到 Dart sass。升级 Gulp 到 5.x，并同步更新相关依赖锁文件。迁移 Sass 模块写法，转向 meta.load-css。升级 gulp-sass、sass、del、gulp-autoprefixer、gulp-rev 等构建依赖。升级 GitHub Actions 工作流与 CI 环境到 Node 20。修复构建过程中错误扫描 node_modules 并将其内容误带入产物的问题。调整 clean / move / md5 等构建任务实现，使新工具链下构建结果稳定。迁移 ESLint 到新配置格式。至此，VOID 主题的适配与升级，就告一段落了。完整的修改记录可以查看从 这个 开始的 commit。更新 VOID 插件如果你对这个 AI 适配的 VOID 插件感兴趣，可以访问 这里 下载插件压缩包并安装。接下来是 VOID 主题配套插件的适配问题。改起来倒是方便，AI 三下五除二就糊好了。但主要的问题是，旧版本解析评论 IP 与归属地使用的 17monipdb.dat IP库太过古老，我想更换一个支持 IPv6 的、数据更新更及时一些的数据库。AI 给出的推荐是 ip2region。因此，现在 VOID 插件使用的 IP 数据库为 ip2region xdb，支持 IPv4 与 IPv6；UA 识别范围也更完整，后台互动页可以更准确地显示访客浏览器与系统信息。修改前修改后当然，还包括后台编辑器新增的实时统计面板。可在文章及独立页面实时显示字数、图片数与标签数。更新 ExSearch 插件如果你对这个 AI 适配的 ExSearch 插件感兴趣，可以访问 这里 下载插件压缩包并安装。既然 VOID 插件都适配了，索性把 ExSearch 插件也糊了一遍。当前 ExSearch 插件已适配 Typecho 1.3，并将搜索前端改为原生实现，去除了运行时对 jQuery 的依赖。另优化了搜索界面的样式、适配了夜间模式，提升桌面与移动端的一致性。日间夜间更新 APlayer 插件如果你对这个 AI 适配的 APlayer 插件感兴趣，可以访问 这里 下载插件压缩包并安装。APlayer 在 Typecho 1.3 下直接失效了。我有时需要在文章内插入音乐，就也让 ChatGPT 完整适配了下。顺便更新了 MetingJS。在新版本 MetingJS 的加持下，Aplayer 可以默认使用 <meting-js> 中 Custom Element 来渲染播放器。因此，在常见的 PJAX 场景中，新页面内容插入后会自动初始化播放器，旧节点移除时也会自动销毁，一般不需要再额外添加 loadMeting() 一类的重载函数。 最后本来还想再写一写关于 Typecho-Plugin-PandaBangumi 与 Typecho-Plugin-Copyright 这两个插件的修改，前者用于展示番剧日历、卡片，后者用于在文末展示版权声明。可尝试让 AI 总结 PandaBangumi 的改动情况时（因为这个插件还是我一点点用网页糊出来的，改得比较乱），它说：我顺手看到的几个后续注意点番剧卡片样式被限制在 .yue .bgm-card 下，如果主题没有 .yue 这个容器类，卡片会功能正常但样式不完整。loadCalendar() 只取了第一个 .bgm-calendar，如果同一页放多个日历实例，后面的不会渲染 PandaBangumi.js。卡片标题做了转义，但列表里的 item.name / item.name_cn 仍然直接拼进 HTML，卡片标签 tag.name 也没有转义；数据源通常可信，但严格说这块还可以再补一层 PandaBangumi.js。cURL 关闭了 SSL 证书校验，能减少环境问题，但安全性偏保守。所以，这部分还得等后续继续让 AI 把这些问题修完，才能正式发布。 Copyright 插件则是我计划塞一堆声明样式，但方向没设计好，越糊越乱，也不能发出来…… 所以，对主题与插件的改动记录，便到此为止了。写完这篇日志，我也该回头去填别的坑了，不然时间一长，我就要忘掉它们怎么写了。 下篇文章见！
Typecho 的管理后台相当简洁，整体以淡雅的灰白色为主色调。没有花哨的图表，也没有多余的视觉干扰，三栏式的布局关键功能、信息层次分明，使用起来非常高效。但是，官方博客部署在国外，并使用了 Cloudflare 的 CDN。如果一时访问不了，最右边的「官方最新日志」在进入后台的时候就会卡顿一下，非常影响加载速度。为了保证稳定，一般我们可能好几年都不会更新一次 Typecho 主程序；再者，官方正式版本迭代速度非常缓慢，这个日志也没什么大用。所以，可以考虑直接干掉这块内容，这样访问后台的时候能稍微提提速。首先，移除「官方最新日志」栏目。打开 admin/index.php 文件，找到并删除以下代码： <?php _e('官方最新日志'); ?> <?php _e('读取中...'); ?> 接着移除获取官方日志的 JavaScript 代码。在文件末尾的 标签中，找到并删除以下代码：if (!!html) { ul.html(html); } else { html = ''; $.get('<?php $options->index('/action/ajax?do=feed'); ?>', function (o) { for (var i = 0; i < o.length; i++) { var item = o[i]; html += ' ' + item.date + ' ' + item.title + ' '; } ul.html(html); cache.setItem('feed', html); }, 'json'); }刷新一下，后台就变成了这样：但这么一删，原本布局就显得有些不对称了。我倒是有个点子，把Z酱的动态塞到这边上，这样就可以及时注意到更新，然后第一时间转载，机智如我！可以找到「最近发布的文章」栏目的代码块，将 col-tb-4 修改为 col-tb-6：修改前： <?php _e('最近发布的文章'); ?> 修改后： <?php _e('最近发布的文章'); ?> 「最近得到的回复」栏目的代码块也同样如法炮制，将 col-tb-4 修改为 col-tb-6 即可。整体就会看起来稍微居中一些，就像这样：至于获取 Typecho 官方博客 RSS 内容的代码，既然不会被引用到就不去动它了。本来这篇水文到这里就该结束了。但我灵机一动，要不底下垫张壁纸试试？于是就变成了这样：方法也很简单，塞个样式就行。在 admin/header.php 文件中 前加入以下代码，效果是会在所有后台背景页面都显示这张壁纸： 以上代码由 Gemini 提供。就是太二次元了，一点也不习惯。原本为了加速访问删除的日志，反倒因此变得更卡了，有点本末倒置。用了几天就删掉了（
从 2019 年起使用 VOID 主题到现在也快有 5 个年头了，虽说 VOID 已经有 3 年多没有更新，但她依旧是我用得最舒服的主题，没有之一。A 酱牛逼！只是「Mikusa Annual Issue」作为一档主攻博客小修小补的栏目，如果不体现针对博客现状具体的改进方案，继续像以前一样水的话，长久下去恐怕难以服众。时移事迁，我也有想要在博客上增加的小东西。往年我可以说「我并不会任何编程语言，无法自行为博客添加其他功能」，所以可以冠冕堂皇地水一点简单的东西。但今年情况不一样了。ChatGPT 等 AI 的出现，让我等不懂代码之辈也能写出代码来。于是乎，在 ChatGPT 和 New Bing 两位老师（主要是 Bing 老师，GPT 老师收费太高了）的帮助下，本代码小白，为博客添加了如下功能。代码框复制按钮众所周知，初之音是一个以开箱和水文为主的生活类博客，但在博主购买 NAS 继而写了一连串 NAS 相关的教程之后，水文的代码含量急剧升高。因此，为了方便大伙一键复制这些代码，我参考「亚灿网志」关于《代码块增加复制按钮》的内容，又从别处抄了个复制图标，简单实现了这一功能。可使用过程中我才发现，亚灿的代码并不支持手机端复制。我尝试询问了下 ChatGPT，她这么说到：document.execCommand 兼容性：document.execCommand 在现代浏览器中逐渐被废弃，不同浏览器对其的支持也有差异。您可以考虑使用更现代的 Clipboard API 来执行复制操作。另外，这个脚本似乎还不支持PJAX，需要刷新页面才能使用。我又问 GPT 加上了 PJAX 重载，同时还加上了夜间模式。以及复制提示：总之，在同 GPT 进行多番友好而激烈的探讨之后，修缮过的代码框复制按钮的 JS 部分如下：// 复制处理函数 function copyHandle(content, successMessage = "复制成功", errorMessage = "复制失败") { navigator.clipboard.writeText(content) .then(() => { VOID.alert(successMessage); }) .catch((error) => { console.error(errorMessage, error); VOID.alert(errorMessage); }); } // 点击事件，通过事件委托处理 function addClickListener() { $('.clipboard').on('click', function () { copyHandle($(this).next().text()); }); } // 初始化剪贴板功能 function loadClipboard() { $('pre').prepend(' <path fill-rule="evenodd" d="M5.75 1a.75.75 0 00-.75.75v3c0 .414.336.75.75.75h4.5a.75.75 0 00.75-.75v-3a.75.75 0 00-.75-.75h-4.5zm.75 3V2.5h3V4h-3zm-2.874-.467a.75.75 0 00-.752-1.298A1.75 1.75 0 002 3.75v9.5c0 .966.784 1.75 1.75 1.75h8.5A1.75 1.75 0 0014 13.25v-9.5a1.75 1.75 0 00-.874-1.515.75.75 0 10-.752 1.298.25.25 0 01.126.217v9.5a.25.25 0 01-.25.25h-8.5a.25.25 0 01-.25-.25v-9.5a.25.25 0 01.126-.217z"></path> '); // 重新绑定事件处理程序 addClickListener(); } // 在页面加载时设置事件 $(document).ready(() => { loadClipboard(); });增加了夜间模式的 CSS 部分： /* 剪切板 */ .clipboard { position: absolute; top: 3px; right: 10px; color: #22252b; z-index: 100; text-align: center; cursor: pointer; line-height: 18px; } body.theme-dark .clipboard { color: white; }以及最重要的 PJAX 重载函数。如果有使用 PJAX 的话，别忘了在 VOID 主题设置的「PJAX 重载函数」中填入这个函数：loadClipboard();你只需在 VOID 主题设置的「head 标签输出内容」中用 和 分别包住 JS 和 CSS 代码，就可以拥有这个复制按钮啦！首页头图动效首页头图动效是从贰岛博客那里抄过来的，你可以现在返回本站首页（仅PC端）查看一下效果。具体是在 VOID/includes/banner.php 最后的 <?php elseif($this->is('index')): ?> 条件判断中添加一段脚本，详细代码如下：<?php elseif($this->is('index')): ?> <?php $title = Helper::options()->title; if($setting['indexBannerTitle']!='') $title = $setting['indexBannerTitle']; $subtitle = Helper::options()->description; if($setting['indexBannerSubtitle']!='') $subtitle = $setting['indexBannerSubtitle']; ?> <div class="banner-title index<?php if(!empty($banner)) echo ' force-normal'; ?>"> <?php echo $title; ?> <?php echo $subtitle; ?> <?php endif; ?> 只是几天没见他又更新了，我已经抄过来了，嘿嘿。首先是一段 css #scrollButton { position: absolute; left: 50%; bottom: 10px; color: white; transform: translateX(-50%); } #scrollButton:hover { filter: drop-shadow(2px 5px 6px white); } #scrollButton.rotated { transform: rotateX(180deg); /* Rotates the button 180 degrees around the X-axis */ transition: transform 0.5s ease; /* Smooth transition for rotation */ } .lazy-wrap:not(.no-banner) { min-height: 0vh; filter: brightness(1); transition: min-height 0.5s ease, filter 1s ease; }再是一段脚本，也是在 VOID/includes/banner.php 最后的 <?php elseif($this->is('index')): ?> 条件判断中添加： <?php if (!Utils::isMobile()): ?> <path id="XMLID_225_" d="M325.607,79.393c-5.857-5.857-15.355-5.858-21.213,0.001l-139.39,139.393L25.607,79.393 c-5.857-5.857-15.355-5.858-21.213,0.001c-5.858,5.858-5.858,15.355,0,21.213l150.004,150c2.813,2.813,6.628,4.393,10.606,4.393 s7.794-1.581,10.606-4.394l149.996-150C331.465,94.749,331.465,85.251,325.607,79.393z" /> <?php endif; ?>页脚一言VOID 早期的版本自带了「一言」，后来移除了。就像这样：早期的 VOID 也超好看！我想着添加回来。官网的 使用示例 是这样的：我们假设您的网页中存在一个块级元素用于显示一言的文本内容，且我们想让它能跳转到一言的指定页面用于后续的收藏、反馈。 :D 获取中... 那我们可以在 中 或者 .js 文件中使用我们的接口： 或者使用 Fetch API ：// 请注意此 Web API 的兼容性， // 不支持 IE, iOS Safari < 10.1， // 完整支持列表参考：https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API fetch('https://v1.hitokoto.cn') .then(response => response.json()) .then(data => { const hitokoto = document.querySelector('#hitokoto_text') hitokoto.href = `https://hitokoto.cn/?uuid=${data.uuid}` hitokoto.innerText = data.hitokoto }) .catch(console.error)我最终选择了 Fetch API 的方案。但这样出现的一言没有出处，我希望的一言格式是像早期 VOID 那样带出处的格式。在询问了 New Bing 之后，这段代码改成了这样：fetch('https://v1.hitokoto.cn') .then(response => response.json()) .then(data => { const hitokoto = document.querySelector('#hitokoto_text') hitokoto.href = `https://hitokoto.cn/?uuid=${data.uuid}` hitokoto.innerText = data.hitokoto + " —— " + "「" + data.from + "」" }) .catch(console.error)一言的效果就会是这样：你指尖跃动的电光，是我此生不变的信仰，唯我超电磁炮永世长存。 —— 「某科学的超电磁炮」可这样还不够，这是一条带链接的一言，可以跳转到一言官网。说实话我不希望是这样的效果，我只要文字就够了。继续询问了 New Bing 之后，引用一言的代码改成了这样： 少女祈祷中... 打开 VOID 主题文件夹，在 /VOID/includes/里找到 footer.php ，在「感谢陪伴」下面插入这段代码： 感谢陪伴： 少女祈祷中... 那既然不需要链接，原来的脚本就改成了这样：function loadHitokoto() { fetch('https://v1.hitokoto.cn/?c=a&c=c') .then(response => response.json()) .then(data => { const hitokoto = document.querySelector('#hitokoto_text') hitokoto.innerText = data.hitokoto + " —— " + "「" + data.from + "」" }) .catch(console.error) }这还没结束。因为我启用了 VOID 的 PJAX 功能，所以需要让这段脚本能在切换页面的时候跟着重载。同时，我还希望能在一言服务中断的时候，能输出错误提示而不是显示「少女祈祷中...」。于是在 New Bing 的帮助下，这段脚本就适配上了 PJAX ：// 定义一个函数来加载一言 function loadHitokoto() { // 使用 fetch API 从一言服务器获取数据 fetch('https://v1.hitokoto.cn/?c=a&c=c') // 当响应到达时，将其解析为 JSON .then(response => response.json()) // 当数据被解析时，更新 #hitokoto_text 元素的文本 .then(data => { const hitokoto = document.querySelector('#hitokoto_text') hitokoto.innerText = data.hitokoto + " —— " + "「" + data.from + "」" }) // 如果在上述过程中出现错误，将 #hitokoto_text 元素的文本更改为错误消息 .catch(error => { console.error(error); const hitokoto = document.querySelector('#hitokoto_text') hitokoto.innerText = "一言服务失效啦~"; }); } $(document).ready(function () { loadHitokoto(); });最后，在 VOID 主题设置中添加一言的重载函数：loadHitokoto();表情包脚本关于 VOID 的表情包，在《Mikusa Yearly Issue 2》一文中我有照猫画虎地模仿其他博主自行增加了一些表情（其实我很好奇那些代码是如何生成的），现在那些表情我用腻了，全手动再添加不仅费时费力且不符合本次 Issue 的主旨。我通过 New Bing 写了一段脚本，把这一过程尽可能地自动化了：# 创建一个空的ArrayList来存储结果 $jsonArray = New-Object System.Collections.ArrayList # 遍历当前目录下的所有.png文件 Get-ChildItem -Filter "*.png" | ForEach-Object { # $_ 是一个特殊变量，表示当前正在处理的对象（在这里，它表示当前正在处理的文件） # 获取当前文件的文件名（不包括扩展名） $oldName = $_.BaseName # 获取当前文件的扩展名 $extension = $_.Extension # 使用URL编码对文件名进行编码 $newName = [System.Web.HttpUtility]::UrlEncode($oldName) # 删除文件名中的% $newName = $newName.Replace("%", "") # 将文件名转换为大写 $newName = $newName.ToUpper() # 将文件名和扩展名合并，得到新的文件名 $newName = $newName + $extension # 重命名文件 Rename-Item -Path $_.Name -NewName $newName # 创建一个Hashtable来存储当前文件的信息 $jsonObject = New-Object PSObject # 添加"icon"字段到Hashtable中 $jsonObject | Add-Member -MemberType NoteProperty -Name "icon" -Value (" ") # 添加"data"字段到Hashtable中，!($oldName)前面的斜杠 \ 请手动删除，会被 VOID 转义 $jsonObject | Add-Member -MemberType NoteProperty -Name "data" -Value (":\!($oldName)") # 添加"text"字段到Hashtable中 $jsonObject | Add-Member -MemberType NoteProperty -Name "text" -Value ("$oldName") # 将当前文件的信息添加到结果中 $jsonArray.Add($jsonObject) | Out-Null } # 将结果转换为JSON格式，并输出到名为owo.json的文件中 $jsonArray | ConvertTo-Json | Out-File -FilePath owo.json 现在只需手动准备好表情图片，比如上米游社扒一些藿藿。表情包文件名称最好为图中所示格式。然后新建一个文本文档，把上面的脚本填入其中，修改 .txt 后缀为 .ps1 。Win 11的话自带终端。右键当前目录打开终端，输入.\owo.ps1回车后，表情文件名就自动修改成了删除 % 后的 URL 编码格式，同时生成符合 VOID 表情格式的 JSON 文件。[ { "icon": " ", "data": ":/!(藿藿_好痛)", "text": "藿藿_好痛" }, { "icon": " ", "data": ":/!(藿藿_捏脸)", "text": "藿藿_捏脸" }, { "icon": " ", "data": ":/!(藿藿_抓狂)", "text": "藿藿_抓狂" } ]参考《Mikusa Yearly Issue 2》中的步骤，修改表情包相关文件，就可以拥有崭新的表情包啦！！如果能外挂表情包就好了，可惜我不知道如何就这个功能怎么向 GPT 提问。Copyright 插件同样是在《Mikusa Yearly Issue 2》一文中，我提到用上了 Copyright 插件的事。那时还在碎碎念不能直接使用 markdown 格式的链接，这次依托 New Bing，也成功为其加上了 markdown 解析的功能。虽然看不懂原理而且像是有 Bug，但至少真的能直接用 markdown 了。具体是将这一部分代码：$t_cover = ' 封面出处：' . $cr['cover'] . ' ';修改成这样：$parsedCover = Typecho_Widget::widget('Widget_Abstract_Contents')->markdown($cr['cover']); $parsedCover = strip_tags($parsedCover, ''); // 保留链接和强调标签 $t_cover = ' 封面出处：' . $parsedCover . ' ';就可以像这样直接在封面出处中使用 markdown 语法的链接了。[XilmO@夕末 / sad #Pixiv](https://www.pixiv.net/artworks/109181977)顺便把作者那块也改成能解析 markdown 的：$t_author = ' 本文作者：' . $cr['author'] . ' ';修改成这样：$parsedAuthor = Typecho_Widget::widget('Widget_Abstract_Contents')->markdown($cr['author']); $parsedAuthor = strip_tags($parsedAuthor, ''); // 保留链接和强调标签 $t_author = ' 本文作者：' . $parsedAuthor . ' ';就可以像这样直接使用 markdown 语法了：[mikusa](https://www.himiku.com)不过测试的时候发现 Copyright 插件好像不太兼容 Typecho 1.2，禁用后再启用功能会全部失效。所以不要贸然禁用插件，否则只能手动修改数据库才能启用相关功能。但也可以手动修改插件，让它默认启用「显示原（本）文链接」和「在文章显示」两个功能。即在插件 Plugin.php 中搜索 NULL, NULL, NULL，把第一个 NULL 改成 1 就行。 $form->addInput($notice); $showURL = new Typecho_Widget_Helper_Form_Element_Checkbox('showURL', array(1 => _t('显示原（本）文链接')), 1, NULL, NULL); $form->addInput($showURL); $showOnPost = new Typecho_Widget_Helper_Form_Element_Checkbox('showOnPost', array(1 => _t('在文章显示')), 1, NULL, NULL); $form->addInput($showOnPost); $showOnPage = new Typecho_Widget_Helper_Form_Element_Checkbox('showOnPage', array(1 => _t('在独立页面显示')), NULL, NULL, NULL); $form->addInput($showOnPage);镜像站本来我不想写这一部分的，但怕不明真相的小伙伴们觉得我钱多没地方花买那么多域名。虽然我确实买了好些个域名。也许我这 PHP 程序不咋会「时刻暴露在危险之中」1，即使真的有危险我也不知道如何从日志发现蛛丝马迹，但我会自搜啊！所以就真让我逮到了这么俩家伙。这俩镜像站不像 Z酱那种直接请求源站数据，而是自己手把手，或者说用批量程序创建出来的全新站点。只是使用了我的名字、我的文章和我的图片而已，甚至没有直接使用来自小站的链接。所以我对它们完全没有办法。第一个 盗版初之音 是今年五月份左右在博客后台的引用中发现的，不知道 Typecho 的哪个版本更新了的这一功能。总之多亏了它自投罗网，我才能第一时间发现这一复制站点。我确实没想到过注册「初之音」的拼音域名，只是未曾预料这种防止商标被盗用的事竟然发生在了我身上。目前为止，它总共复制粘贴了我 11 篇文章、2张照片以及4个友链。一开始我还想着及时发表声明，但冷静下来才发现，我对它根本无计可施。声明只会占用我宝贵的博客空间，大家好奇了还会过去点两下吸引走我所剩无几的流量。再说，它复制完那些文章之后就没有下一步动作了，所以我也就没去搭理它。就是时间一长，谷歌已经把它收录了，还排在我下面。 另一个 盗版初之音 ……怎么说呢，它用了我的落地页不说，还扒走了我的随机图，自已开了个……我大概就是在它建站的时间点，另一个图站经历了一次不小的图片请求，直接把我机子干爆了……对于它我也是我无可奈何，只是希望它不要用我 ID 惹事生非。Z酱说被镜像的时候我还眼红自己咋没被镜像，是不是水的不够多。等到真被复制粘贴的时候却又头痛了……最后虽然这次看着像是干货满满，可很多代码我都看不懂。今年也是在 AI 大模型的加持下，我才得以对小站进行了这些修改。只是没有基础的代码知识，debug 起来还是困难重重的。我猜，肯定有值得优化的地方……不知道后续出问题的话，我还能不能依靠 AI 的力量修复小站的 bug。总之，以上就是这一年的 Issue 啦！前阵子催 Z酱更新的时候，才注意到今年还有日志没水。那么，加上这篇日志，本年度博客的 KPI 就完成啦！感谢 Z酱 ヾ(≧∇≦*)ゝ或许我应该转变思路，想办法从Z 酱手里骗到电波站才对。但是骗到手了我也不会修 Bug 啊，好像陷入了死循环……嘛，矫情的话就留到以后再说吧，让我们下个 Issue 再见ヾ(￣▽￣)Bye~Bye~ Velas Weekly Issue 7 ↩
这两天上网冲浪的时候，发现了一个叫作「WebP Server Go」的开源项目。这个项目是这么介绍的：Go version of WebP Server. A tool that will serve your JPG/PNG/BMP/SVGs as WebP/AVIF format with compression, on-the-fly.WebP Server 的 Go 版本，旨在将您的 JPG/PNG/BMP/SVG 图片进行动态压缩并转换为 WebP/AVIF 格式。简单地说，这是一个自动压缩图片为 WebP/AVIF 格式的工具。关于 WebP 这一格式，早在《Mikusa Yearly Issue 1》一文中我就有详细地解释过。当时为了节省 CDN 流量的开销，拜托 Z酱帮我写了判断用来自动调用同路径下的 WebP 图片，唯一的不足就是要手动压缩图片并上传到服务器。后来 VOID 更新，我便放弃了这个需要更改主题代码的方案，改为直接使用 WebP 格式的图片。缺点也很明显，我还是需要手动压缩原始图片，再上传 WebP 格式的图片到图床，本地需要保留两份图片文件。至于本地备份的图片，肯定也是 png 或其他格式的原图优先级更高，毕竟 webp 有用到的话我可以手动再压缩。即使写东西顺带压缩下图片花不了多少功夫，也还是会有一转眼就忘记压缩直接上传原图的时候。不管不顾还好，强迫症发作的时候就会想着删掉原图重新上传。这看起来是个不痛不痒的问题，但也有会感到烦躁的时候。因此，在发现了「WebP Server Go」之后，我便急忙部署在服务器上运行起来，测试实际的使用效果。 【重要提示】随着处理图片数量的增加，WebP Server GO 的内存占用会显著上升。为确保程序稳定高效运行，建议您在高配置服务器上部署本应用。我的机子配置一般，已经承受不住 Webpgo 的摧残了，已于 2025年6月放弃使用该程序。安装项目提供了 Docker 容器，使用 docker compose 就可以一键安装：version: "3.9" services: webpgo: image: webpsh/webp-server-go environment: - MALLOC_ARENA_MAX=1 volumes: - ./pic:/opt/pics #本地图片路径 - ./webpgo/exhaust:/opt/exhaust #压缩后的图片缓存 - ./webpgo/metadata:/opt/metadata #压缩前后的图片元数据缓存 ports: - 3333:3333 restart: unless-stopped container_name: webpgo在 volumes 挂载目录中，./pic 指的是本地存放图片的路径，需要挂载到容器中的 /opt/pics 文件夹中。假设你有张图片路径为 ./pic/myblog/test.png ，那么 WebP Server Go 压缩后的链接就是 http://example.com/myblog/test.png 。/opt/pics 、/opt/exhaust 、/opt/metadata 是配置文件 config.json 中的默认配置，并非不可修改。参考官方 Docker 部署文档1中的说明，我们可以自定义这一配置文件，再挂载到容器中去，以对容器进行一些微调。原始的 config.json 内容如下（如有更新，请参考官方文档）：{ "HOST": "0.0.0.0", "PORT": "3333", "QUALITY": "90", "IMG_PATH": "./pics", "EXHAUST_PATH": "./exhaust", "IMG_MAP": {}, "ALLOWED_TYPES": ["jpg", "png", "jpeg", "bmp", "gif", "svg", "heic", "nef", "webp"], "ENABLE_AVIF": false, "ENABLE_EXTRA_PARAMS": false, "READ_BUFFER_SIZE": 4096, "CONCURRENCY": 262144, "DISABLE_KEEPALIVE": false }通俗易懂：QUALITY：压缩质量，默认 80，即 80%，有需要可以修改为 90%。IMG_PATH：图片路径，即映射中的 /opt/pics，你也可以修改为 ./pic，只是修改完后不要忘了同时修改 docker-compose.yml 文件。这一配置项亦支持远程路径，即你可以填写 http://example.com 直接从远程链接里抓图，这样就不用本地挂载图片了。IMG_MAP：多路径。按照官方的解释2和示例，这个配置的格式是这么写的： "IMG_MAP": { "/anon": "/pics1", "/soyo": "/pics2", "/mygo": "http://example.com" },简单地说，: 左边填你想要展示的路径，随便什么都行只要格式是 /xxx；右边填具体路径 ./pics1 或远程链接 http://example.com ，多路径就生效了。只不过要注意的是，链接的格式必须为 http?s://.*，即 https:// 或 http:// ，不匹配此格式的会被忽略。ALLOWED_TYPES：允许的图片格式。我在测试的时候发现，如果已经是 WebP 格式，程序就会报错。本想向官方提 Issue，但试着在格式中添加了 webp ，程序就不报错了，会直接使用已存在的 WebP 格式图片。（官方已默认添加 webp 格式的图片）其他部分都没有改动的必要，如果你有需求可以详细参考官方文档自行修改。修改后的 config.json 保存到本地：{ "HOST": "0.0.0.0", "PORT": "3333", "QUALITY": "90", "IMG_PATH": "/pic", "EXHAUST_PATH": "./exhaust", "IMG_MAP": { "/anon": "/pics1", "/soyo": "/pics2", "/mygo": "http://example.com" }, "ALLOWED_TYPES": ["jpg", "png", "jpeg", "bmp", "gif", "svg", "heic", "nef", "webp"], "ENABLE_AVIF": false, "ENABLE_EXTRA_PARAMS": false, "READ_BUFFER_SIZE": 4096, "CONCURRENCY": 262144, "DISABLE_KEEPALIVE": false }再挂载到容器内部即可。于是，docker-compose.yml 也跟着改动：version: "3.9" services: webpgo: image: webpsh/webp-server-go environment: - MALLOC_ARENA_MAX=1 volumes: - ./webpgo/config.json:/etc/config.json #配置文件，需提前创建 - ./pics:/pics #本地图片路径 - ./pics1:/pics1 #本地图片路径1 - ./pics2:/pics2 #本地图片路径2 - ./webpgo/exhaust:/opt/exhaust #压缩后的图片缓存 - ./webpgo/metadata:/opt/metadata #压缩前后的图片元数据缓存 ports: - 3333:3333 restart: unless-stopped container_name: webpgo安装完毕后，可以使用 Nginx 进行反代3：server { listen 443 ssl; http2 on; server_name example.com; #省略SSL配置 location / { proxy_pass http://webpgo:3333; proxy_set_header X-Real-IP $remote_addr; proxy_hide_header X-Powered-By; proxy_set_header HOST $http_host; # add_header Cache-Control 'no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0'; #鉴于自用需要缓存，所以不启用这段。 } access_log /home/wwwlogs/example.com.log; }接着，就可以使用 http://example.com/anon/xxx.jpg 访问到 /pics1 路径下名为 xxx.jpg 的图片了。如果你是将图片保存在博客的 usr/upload 文件夹中，也可以直接反代这一路径： location /usr/upload { proxy_pass http://webpgo:3333; proxy_set_header X-Real-IP $remote_addr; proxy_hide_header X-Powered-By; proxy_set_header HOST $http_host; # add_header Cache-Control 'no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0'; }这样就可以在不改变图片链接的前提下使用 WebP Server Go。前提是你已经挂载了博客的 usr/upload 文件夹到 webpgo 中。使用其实安装完就可以直接开始使用了，没什么好写的东西了。但我测试时发现，这玩意还可以拿来反代任意图片，不受防盗链影响，比如 sina。例如在 IMG_MAP 配置中写成这样： "IMG_MAP": { "/sina": "https://wx1.sinaimg.cn/large" },然后访问 http://example.com/sina/9b8c9418ly1hbsl6dpurej22fa1i01kx.jpg ，可以直接访问到 https://wx1.sinaimg.cn/large/9b8c9418ly1hbsl6dpurej22fa1i01kx.jpg 的图片！或者是 bilibili 的视频封面： "IMG_MAP": { "/bili": "https://i0.hdslb.com/bfs/archive" },然后访问 http://example.com/bili/6d1590d19238188da23755a083eba70f415af569.jpg ，也可以直接访问 https://i0.hdslb.com/bfs/archive/6d1590d19238188da23755a083eba70f415af569.jpg ！也就是说，WebP Server Go 不仅能拿来压缩图片，还能当便捷反向（图片）代理使用！只要服务器能下载到图片！或者，如果你认为自己部署一个 WebP Server Go 很麻烦，或是使用纯静态的博客程序托管在 GitHub Pages 上没有服务器，那么可以尝试使用官方的云服务：WebP Cloud Services 。打开官网后，点击 Try WebP Cloud 并使用 GitHub 账户授权登录。登录后，即可来到 WebP Cloud Services 的控制台。点击屏幕右下方的 Create Proxy 按钮，创建一个反代配置。选择一个地区（随便选，国内访问肯定是都慢的），在「Proxy Name」中填入备注用以区分，「Proxy Origin URL」中填入具体到图片路径的原链接。点击 Create 创建这样，你就创建好了一个 WebP Cloud Services 的反代，只需要将 Proxy Address 提供的地址替换掉你原来的图片地址，就可以啦！例如，原图片为 https://www.himiku.com/2023/09/21/12345566.jpg，那么 WebP Cloud Services 反代的图片就应为 https://abcdef.webp.ee/2023/09/21/12345566.jpg。WebP Cloud Services 除了国内连接速度可能不太好接受（使用了 Cloudflare 进行减速），再就是 WebP Cloud Services 是一个商业项目，其他方面都是要优于自建的。至少不用你准备足够大的硬盘空间来存放转换后的 WebP 图片，也无须担心 WebP Server Go 出 bug 后卡死你的机子了。如果有使用需求的话，就请准备好钱包吧！ https://docs.webp.sh/usage/usage/ ↩ https://docs.webp.sh/usage/multipath/ ↩ 参考官方配置：https://docs.webp.sh/usage/usage-with-binary/ ↩
先祝各位新年快乐！ 现在已经是公历2022年的新年了，小站也平稳地度过了第4个年头。回首这一年，小站没有发生任何变化。更新速度缓慢，无意义的水文增多，文笔丝毫没有进步（好像本来也没什么文笔，那这条不算），简而言之，小站正处于停滞不前的节点。等等，停更也算是变化的吧？本来也没有什么更新的念头，只是上个月初在 半吊子的静树 大佬1的帮助下，顺利地用上了Docker，并成功将博客迁移了过去。小站现在的网站环境不再是靠「LNMP一键安装包」脚本编译的程序，而是由Docker驱动的Docker LNMP！。其实大佬很早就叫我用Docker了，毕竟容器化的LNMP搬家很方便，但我一直没有动手。一是懒，搬家真的很麻烦，重新配置服务器很麻烦；二是学习新东西对我这种不思进取的老年人来说，真的很头疼；三是社畜下班后只想哒哒哒打游戏了。上班摸鱼就不一样了。没错！我就是在摸鱼的时候把Docker LNMP给搭起来的！用上Docker的兴奋程度不亚于刚开始学会怎么搭建博客，以至于我很想写一篇教程来讲讲我是怎么费尽千辛万苦才用上这Docker的。可冷静之余才意识到，这东西我自己都还没搞懂，即便是现在也只到docker compose up -d的程度。我可不想误人子弟，这个念头就这样掐掉了。直到有一天欣赏 Z酱的电波站，才猛地想起来自己还有个叫做「Mikusa Yearly Issue」的东西。这不就是我为了水一些与博客相关，但不知道怎么起题目的东西所建的分类嘛！正好也有一段时间了，不就可以更新下？我可真是个小机灵鬼！哦对了，你们肯定纳闷，「诶，不是说叫 Mikusa Yearly Issue 吗，为啥标题是 Mikusa Annual Issue 嘞？」我早就猜到会问，这是因为Z酱说：「吾觉得 Annual Issue 比较顺口。」所以就换过来啦！Docker 化使用全新的啥软件都没有安装的Debian服务器，按照官方教程安装好docker-ce和docker compose，编写好docker-compose.yml，再执行docker compose up -d，只需数分钟2，一个可运行Typecho的LNMP环境就完成了。Docker重新部署环境的耗时要比二进制编译要短得多，这是优势之一。我的docker-compose.yml配置完全照搬大佬，而大佬为了在体验上与「LNMP一键安装包」脚本一致，nginx的conf配置完全使用原先脚本的文件。如此一来，迁移到Docker就十分方便，我只要把原先vhost下的文件全部复制到新服务器就行。MySQL使用的是mariadb。由于我原本的服务器年久失修，数据库排序规则比较混乱，我选择全部挨个导出，更新成utf8mb4_unicode_ci的排序规则。PHP由于A酱的VOID还未支持PHP8，Typecho亦未发布支持PHP8的正式版，因此使用的还是PHP7的最新版本。至于acme.sh，也替换成了Docker版本，虽说在申请证书上理解申请命令花了不少功夫，但搞明白之后，使用体验和Shell版acme.sh基本一致。毕竟申请完就放后台全自动了，谁还管这玩意。大致目录如下：├───docker-compose.yml │ ├───acme.sh ├───mysql │ │───Dockerfile │ └───data ├───nginx │ ├───conf │ └───logs │────────── access.log │────────── nginx.pid │────────── nginx_error.log ├───php │────────── Dockerfile └───www我本就习惯用VS Code打开/usr/local/，再找到其中的nginx、php等文件夹直接编辑需要的文件。现在这些东西都在同一级目录下，操作起来十分方便，熟悉新环境的成本为零。只是Docker的命令就需要重新学习一番了，比如最常用的lnmp nginx restart，现在变成了docker exec nginx nginx -s reload。更新php、nginx变得无比快捷，如果docker-compose.yml中是指定版本的，改个版本号再重新docker compose up -d就行，甚至不用停机维护。此外，有关挂载目录等新知识也还需要花时间琢磨才行。总而言之，对于只有一个博客的小博主来说，容器化真的是非常便捷的选择之一。当然你也可以选择将博客托管给朋友维护，那就更轻松了。接入备案既然搬了家，那接入备案就该提上计划了。我很早就不用阿里云的服务器了，但备案还在阿里云。这次既然又用回了国内服务器，自然就想着按照有关规定接入备案到腾讯云。迁移备案非常麻烦，可以说是一波三折。我的mikusa.cn是直接301到himiku.com的，备案的话需要分别备案，就开了个落地页把mikusa.cn解析了上去，提交了申请。没多久就被打回来了，电话告知「闽 ICP 备 18002751 号-2」需要改为「闽 ICP 备 18002751 号-1」才行。但我老早就考虑到这个问题，落地页填的就是「闽 ICP 备 18002751 号-1」，猜测可能是 DNS 缓存没有刷新，访问的还是301后的页面——也就是himiku.com，工作人员没有认真看就给我打回来了。于是就老实地把himiku.com下方的备案号改成了「-1」，提交了上去。很快就接到了电话，告知himiku.com原备案是「-2」，要与备案一致才行，要求我改回去。气不打一处来，第一次你咋不认真看呢？遂改回「-2」。结果下午再一次接到了电话，告知备案号可以不用带数字，建议去掉。为什么不早说……所以朋友们，有超过两个备案网站的话，为了避免麻烦可以只写主题备案号。年度总结顺便也写一下好久没写的年度总结。网站流量根据 Google An­a­lyt­ics 的数据统计，本年度小站一共获得了 8.5 万名用户，其中 8.3 万为新用户。但是在2月至5月有一段时间觉得 Google An­a­lyt­ics 拖累了网站的加载速度，便把它撤下来了。所以这个数据并不准确。其实我也挂了百度统计。以下便是2021年度的浏览量统计。但是有一段时间我觉得 百度统计也很拖累网站速度，它也被撤下来了。因此这个数据同样不准确。我为什么敢撤下来呢？是因为我有自己的统计工具「Umami」。可是7月份的时候误删了数据库，所以……Umami的数据也不准确……对不起，给大家看笑话了。另一个 Google Search Console 就比较准确了。本站经由 Google 搜索，在2021年度获得了 1.17 万次点击，可喜可贺。CDN流量截至 2021年12月31日晚8时，本年度博客在腾讯云上一共耗费了大约3 652 GB 的CDN 流量，平均每天花费的流量约为1.78GB。而近两个月下来，平均每天都要耗费2GB。最多甚至有过3.37G。我自己都不知道这些流量花到哪里去了，但根据访问用户区域分布图可以得知，小站特别受广东人青睐。图为近90天内，访问用户区域分布图本年度在 CDN 流量上的花费大大增加。按照这个趋势，明年可能需要直接购置1TB 的流量包才能满足 CDN 的消耗。未完待续2021年对我来说，可以用「迷茫」二字概括。我甚至有预感明年亦会是迷茫的一年。三月初从福建到上海，再到广州，在小工厂里混完了2021剩下的时间。既没挣到多少钱，也没学习什么新知识，也未学到什么待人处事的方法。反倒是多了一堆烦心事，思维变得愈加迟钝。接下来该怎么做呢？我还不知道。就博客来说，便是先把计划写的上海篇写完，再写写广州的事情。嗯，那么下一篇再见。以下简称「大佬」。 ↩此为虚数，实际时长可能因网络等因素会有波动。 ↩为何是大约？因为腾讯云只支持90天内的使用查询。这个数据是我每90天查询出来后手动计算的。辣鸡腾讯 ↩
隔壁酚酞大佬向我抱怨说博客头像加载不出来，要我赶紧想想办法。具体情况如下图。好家伙，因为日常挂代理的原因，我一直没注意到。清光缓存看了眼自己的。得，是必须要解决的严重问题。琢磨并测试了下，头像崩掉单纯是 Gravatar 服务在国内不好使的原因。因此只要使用国内的公共 CDN 服务，或是自己建一个，再替换掉 Typecho 程序中的原有链接，就能解决这个问题。替换原有链接在博客根目录/var/Typecho/Common.php中，使用VSCODE或是其他编辑器，快捷键Ctrl+F搜索secure.gravatar.com。Typecho 版本不同，位置可能会不一样，我的在 991 行左右。 $url = $isSecure ? 'https://secure.gravatar.com' : 'http://www.gravatar.com'; $url .= '/avatar/';搜索下可用的公共 CDNV2EX：https://cdn.v2ex.com/gravatar/极客族：https://sdn.geekzu.org/avatar/把第一行里头的链接替换掉 $url = $isSecure ? 'https://sdn.geekzu.org/avatar/' : 'https://cdn.v2ex.com/gravatar/';因为上面的 CDN 链接里已经带有/avatar/了，所以需要注释掉这一行，也可以把这行直接删掉。 //$url .= '/avatar/';然后保存，刷新，一切恢复正常。但是更新 Typecho 的时候覆盖了怎么办？还有一个方法，在博客根目录程序配置文件config.inc.php最后面插一段：/**自定义头像源*/ define('__TYPECHO_GRAVATAR_PREFIX__', 'https://cdn.v2ex.com/gravatar/');这个配置文件一般情况不会去动它，所以是最一劳永逸的方法，也推荐使用这个方法。自己反代一个直接开一个二级域名用来反代当然可以，但用二级目录（如himiku.com/avatar/）来反代也不错。参考「Gravatar镜像资源及Nginx反代教程」的方法自己尝试了下。在配置开头加个缓存，毕竟有缓存又快又能省钱。比如把缓存放在/home/wwwcache/gravatar。proxy_cache_path /home/wwwcache/gravatar levels=1:2 keys_zone=gravatar:5m inactive=30m max_size=50m;然后在server段里头加个反代。比如我想用https://www.himiku.com/avatar/，来直接反代https://secure.gravatar.com/avatar/。 location /avatar/ { proxy_pass https://secure.gravatar.com/avatar/; proxy_redirect off; proxy_set_header Host secure.gravatar.com; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto https; proxy_set_header Accept-Encoding ""; proxy_cache gravatar; proxy_cache_key $scheme$host$request_uri; proxy_cache_valid 200 304 3h; proxy_cache_valid 301 24h; proxy_cache_valid 500 502 503 504 0s; proxy_cache_valid any 3h; }不喜欢用avatar，改用touxiang也行，把第一行location /avatar/改成location /touxiang/就好了。完整些的大致如下。proxy_cache_path /home/wwwcache/gravatar levels=1:2 keys_zone=gravatar:5m inactive=30m max_size=50m; server { listen 80; server_name himiku.com ; return 301 https://himiku.com$request_uri; } server { listen 443 ssl http2; server_name himiku.com; index index.html; ssl........ 省略 location /avatar/ { proxy_pass https://secure.gravatar.com/avatar/; proxy_redirect off; proxy_set_header Host secure.gravatar.com; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto https; proxy_set_header Accept-Encoding ""; proxy_cache gravatar; proxy_cache_key $scheme$host$request_uri; proxy_cache_valid 200 304 3h; proxy_cache_valid 301 24h; proxy_cache_valid 500 502 503 504 0s; proxy_cache_valid any 3h; } …… 其他省略 } 重复上一步，把自己的链接替换进去就行了。若是想使用我的反代服务，可以点开评论区头像自行获取链接。但是不保证长期稳定。准备停止服务啦，有看到的小伙伴们还请切换到公共cdn！
好久不见，甚是想念。虽然上一篇「Issue 2」还没满一年，但我不得不打破规定再开(shui)一文来处理一个有点老生常谈的问题：我的CDN流量费又爆炸了。没错，第一篇「Issue 1」就是为了解决这件事而诞生的。为此我换用腾讯云，图片尽量使用WebP格式，还因为「Safari」不支持WebP而拜托Z酱写了个魔改版「VOID Δ」。但是在腾讯云流量抵扣券用光，「Safari 14.0」支持WebP后，这些改动都被我全盘推翻了。我又用回了又拍云以白嫖免费的代金券，换回正式版 VOID 享受来自官方的主题维护。 可是，随着最近流量费的暴涨，我必须重新审视这个问题，确定一个省钱的方案，尽可能摆脱CDN这个烧钱的大头。启用图床既然不准备使用又拍云了，那么「在Windows上，使用Typora，配合PicGo，通过又拍云上传图片」的方法就不可行了。因此我准备先使用「图床」来完成「上传图片到服务器」这一过程。选择的图床程序是简洁好看的「lsky-pro」。安装过程就略过了，可以参考官方手册。通过 PicGo 快捷上传图片在Windows上，我一般使用 Typora 来水文章。Typora中有一个设置项可以使用「PicGo」上传图片。安装就略过了。如果是直接使用又拍云或是腾讯云的服务的话，PicGo中有现成的的图床项。配置完成后，只要让 PicGo 在后台运行，就可以直接在写作的时候上传图片，无需额外操作。方便得很。 本来还担心换用Lsky-Pro图床之后，这一快捷功能没法使用。没想到竟然有人写了插件。这样只要在「插件设置」中搜索「picgo-plugin-lskypro」并安装就可以了。而且使用这一插件仅需启用图床api，获取账号token，依次填入配置中就好。前者比较好办，后者对于不懂技术的人来说就十分困难了。但是插件作者贴心地在博客中提到了有关「Token获取」的说明。所以只要对Lsky-Pro进行一些修改，就可以直接在图床上获得token。这么修改可能在安全上有些隐患，但是图床就我一个人用，不管了。 对图床启用CDN图床能用了，还需要解决一些历史遗留问题。原本在又拍云云存储里的图片，就直接下载下来放在图床公开目录中，可以直接用；而没花完的代金券，我只要再对图床启用CDN就好了。这样等代金券消耗殆尽，我直接关闭CDN，也不用去迁移这些图片，甚至可以随便换到任意一家CDN。 这应该是目前来说比较合适的解决方法了吧。虽然一开始使用云存储存放图片的初衷就是为了解决图片的备份问题，那时服务器的带宽太小，下载到本地很花时间。而现在带宽管够，换服务器的话一点儿也不费劲。况且不怎么想轻易更换服务器了。因此只要博客活着，图片就能活着；博客挂了，图片也必须挂。损失当然是有的，不能白嫖了，少了防盗链，没了VOID针对又拍云CDN的图片模糊加载。嘛，不管了。
前一段时间看到 applemiku 大佬在接收 Google AdSense 广告收益的过程中吃了苦头（见「年轻人第一次拿谷歌AdSense收益」），过程之繁琐让我也有些担忧。虽然我当时才 50 刀左右。哪能想到只是过了 2 个月，我也达到了最低汇款要求 ，并且很轻松地就接收到了汇款 。所以，为纪念博客的第一笔广告收益：109.61美元，特此水文一篇。 为了避免出现「需要去开户行才能收到汇款」的情况，我和隔壁 野兔 探讨过这个问题，他和我一样在这个月就能接收汇款。我们都决定用「招行银行卡」来接收这笔汇款，但我的招行卡是在洛阳开户的。因此我打电话到「95555」咨询过电话客服，客服的回复是「有可能需要去柜台面签，但还需咨询开户行」，而开户行却说「会直接就汇到卡里，如果真需要去柜台，异地招行也可办理」。电话客服倒是热心地为我提供了下接收汇款需要填写的信息：收款行地址：China Merchants Bank Tower NO.7088, Shennan Boulevard, Shenzhen, China.SWIFT Code：CMBCCNBS谷歌会在 21 号至 26 号之间发放付款1。我 21 号在 AdSense 上看到已到打款的提示，22 号就收到银行款项到账的短信通知了。现在美元一直在贬值，但是我缺钱花，就直接用招行结成现汇花掉了。 随后就因为无效流量以及ghs被限制广告投放 ，现在博客上一个广告都看不到，只有预留广告的空白框。 详见：AdSense 付款时间 ↩
时间过得真快，上一次的「Issue」都已经是去年的事了。那么2021年的第一篇文章就由 「Mikusa Yearly Issue 2」开始吧！没错，说年更，那就是年更。毕竟我没啥技术力。为 VOID 增加表情一直想往VOID里增加点表情包。尝试过照猫画虎，但总失败。不过前两天看到「为VOID主题增加自定义表情包」提到了具体的修改方法：需要使用「UrlEncode」转码并删去其中的%才能显示带中文的表情。因此再次将增加表情的想法提上日程。加个表情并不是啥难事，只是不会编程的话纯手写费手。也许我应该直接找A酱。我想添加的是B站的2233娘，虽然有版权风险，但不是啥盈利性目的应该没啥事。修改OwO.json文件在VOID/assets/libs/owo/OwO_02.json中插入： "2233娘": { "type": "image", "container": [{ "icon": " ", "data": "", "text": "2233娘_第一" }, { "icon": " ", "data": "", "text": "2233娘_无语" }, { "icon": " ", "data": "", "text": "2233娘_吐魂" }, { "icon": " ", "data": "", "text": "2233娘_委屈" }, { "icon": " ", "data": "", "text": "2233娘_哭泣" }, { "icon": " ", "data": "", "text": "2233娘_郁闷" }, { "icon": " ", "data": "", "text": "2233娘_困惑" }, { "icon": " ", "data": "", "text": "2233娘_生气" }, { "icon": " ", "data": "", "text": "2233娘_吃惊" }, { "icon": " ", "data": "", "text": "2233娘_汗" }, { "icon": " ", "data": "", "text": "2233娘_大笑" }, { "icon": " ", "data": "", "text": "2233娘_喝水" }, { "icon": " ", "data": "", "text": "2233娘_耶" }, { "icon": " ", "data": "", "text": "2233娘_卖萌" }, { "icon": " ", "data": "", "text": "2233娘_疑问" } ] },修改Contents.php文件在VOID/libs/Contents.php中，第196行插入： $content = preg_replace_callback('/\:\$\(\s*(.*?)\s*\)/is', array('Contents', 'parse2233BiaoqingCallback'), $content);第232行处插入： /** * 2233娘表情回调函数 * * @return string */ private static function parse2233BiaoqingCallback($match) { return ' '; }最终效果就是这样，要记得刷新浏览器缓存才能看到：哦对了，差点忘了说，要现在/VOID/assets/libs/owo/biaoqing/里创建对应的文件夹放表情包。 VOID 开发版已经添加了这个表情。详见 GitHub，或者直接点击：https://github.com/AlanDecode/Typecho-Theme-VOID/archive/nightly.zip 下载开发版本。后来我又想加入崩坏3的表情，步骤一致：修改OwO.json文件 "崩坏3": { "type": "image", "container": [{ "icon": " ", "data": "", "text": "崩坏3_脱非" }, { "icon": " ", "data": "", "text": "崩坏3_入欧" }, { "icon": " ", "data": "", "text": "崩坏3_二胡" }, { "icon": " ", "data": "", "text": "崩坏3_唢呐" }, { "icon": " ", "data": "", "text": "崩坏3_琵琶" }, { "icon": " ", "data": "", "text": "崩坏3_笛子" }, { "icon": " ", "data": "", "text": "崩坏3_镲" } ] },修改Contents.php文件 $content = preg_replace_callback('/\:\!\(\s*(.*?)\s*\)/is', array('Contents', 'parsebh3BiaoqingCallback'), $content); /** * 崩坏3表情回调函数 * * @return string */ private static function parsebh3BiaoqingCallback($match) { return ' '; }实际效果如下： 修改 Copyright 插件因为使用了很多 Pixiv 的图片作为文章头图，而我声明图片来源的方法是在文章首段末尾加上「封面图：xxxx」。这当然没啥问题，直到我注意到「Copyright」这个插件，才意识到使用插件可能可以更好地解决这个问题。于是我又是一番依样画葫芦，修改了下这个插件，效果如下：使用方法是新建一个cover字符字段，填写如2020.11 / gracile #Pixiv 的字段值，即可显示。因为不会，所以不支持使用markdown直接显示链接。 修改后的插件放在 GitHub 上，如果有需要可以下载使用。博客的备份问题这个是用来凑数的。起先使用宝塔面板的时候，我就有设置定时备份。然而在我整理备份文件的时候才发现没有备份数据库。我还想翻翻看自己的黑历史…… 但是我在里头找到了博客建成的截图。一开始用的是「NexT.Mist」主题。而这个点击鼠标的爱心特效，是因为感觉很炫酷，所以就立马放到博客里了。没事干就点这个爱心，玩了好几天……然后就一不小心被我删库了 。那时候还没明白数据库的作用，因此一开始写的东西就没能找回来，不然也是个可以回忆的东西呢。现在备份策略做得很完善了，用的是这个脚本：GitHub。每天自动备份数据库和程序文件，存储在又拍云。防范于未然。水文结束。最后祝各位2021新年快乐！
如果需要统计博客访问的相关数据，一般会选择「百度统计」或是「谷歌分析」。前者面向中文用户，在数据展示上会更加直观；后者面向全球，功能丰富数据详细，只是需要一定的学习成本和使用门槛。当然，你也可以尝试自己搭建一个统计工具，将数据完全掌握在自己的手里。比如使用 Umami。本文于近期（2025年6月）简单更新了下，前置了容器安装的方法，更新了 Umami 的 Github 链接。其余部分未改动。整体内容仅供参考。安装容器安装现在已经是 5202 年了，自行编译构建程序那叫一个麻烦！最简单的方法还是用 Docker 来部署 Umami。参考「Docker -- 从入门到实践」中的教程安装完 Docker-CE 和 docker-compose 后，创建 docker-compose.yml 文件。填入以下内容：services: umami: image: ghcr.io/umami-software/umami:postgresql-latest container_name: umami ports: - "3000:3000" environment: DATABASE_URL: postgresql://umami:umami@umami-db:5432/umami DATABASE_TYPE: postgresql APP_SECRET: replace-me-with-a-random-string #生成一个随机数替换这里 depends_on: umami-db: condition: service_healthy restart: always healthcheck: test: ["CMD-SHELL", "curl http://umami:3000/api/heartbeat"] interval: 5s timeout: 5s retries: 5 networks: - umami umami-db: image: postgres:15-alpine container_name: umami-db user: 1000:1000 environment: POSTGRES_DB: umami POSTGRES_USER: umami POSTGRES_PASSWORD: umami volumes: - ./database:/var/lib/postgresql/data restart: always healthcheck: test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"] interval: 5s timeout: 5s retries: 5 networks: - umami networks: umami: external: true因为随着统计数据的日积月累，数据库占用会越来越大，所以这里我使用的是 umami:postgresql 版本。即不使用 MySQL，而是改为使用 PostgreSQL 并将数据文件存储在指定文件夹中。另外，我创建了一个 umami 网络，你可以删除 networks 字段，或者使用 docker network create umami 也创建一个 umami 网络，用以隔离容器。之后控制台执行 docker compose up -d 启动即可。编译安装或者，你想体验一下编译安装的乐趣，可以自行编译。但是下面这些步骤写于 2020 年，不保证现在也适用。首先，安装 Umami 需要一个基本的 LNMP 环境，然后再安装 nodejs 和 npm。先安装 Nodejs、npm：curl -sL https://deb.nodesource.com/setup_14.x | sudo -E bash - apt-get install -y nodejs apt-get install -y npm安装完毕之后，可以用 node -v 和 npm -v 检查版本。然后下载 Umami：git clone https://github.com/mikecao/umami.git cd umami npm install接着创建一个空数据库，比如 umami，然后在 umami 目录中输入：mysql -u username -p databasename < sql/schema.mysql.sqlusername 一般是 root，databasename 就是你创建的空数据库的名字，具体例子是：mysql -u root -p umami < sql/schema.mysql.sql你也可以在 phpmyadmin 中手动导入到空数据库中。随后创建配置文件。在 umami 目录中新建一个 .env 的配置文件，填入：DATABASE_URL=(connection url) HASH_SALT=(any random string)其中，connection url 需要填入：mysql://username:mypassword@localhost:3306/mydb而 any random string 填入的是随机字符串，完整的例子是：DATABASE_URL=mysql://username:mypassword@localhost:3306/mydb HASH_SALT=123456789注意去掉括号。编译：npm run build启动程序。通常使用 npm start 就可以直接启动，但是退出 ssh 之后就直接停止运行了，所以需要用一些工具保证后台运行，官方文档中的推荐是：npm install pm2 -g cd umami pm2 start npm --name umami -- start pm2 startup pm2 save这样就可以用了。后续如有更新 Umami，首先停止运行：pm2 stop umami获取更新git pull可能会遇到获取更新失败。我也不知道为啥我这每次都失败，可以用这个命令 git reset --hard，然后再执行上一步 git pull然后重新编译npm install npm run build重新启动pm2 restart umami页尾版本号有发生变化，那就是更新成功了。反向代理如果是使用容器部署，映射并暴露了端口，那么 ip:3000 是可以直接访问的。为了配合域名访问，建议使用 Nginx 或 Caddy 反向代理 Umami。这里以 Nginx 举例。在 server 段加入配置：server { server_name umami.yourdomain.com; location / { proxy_pass http://localhost:3000; proxy_set_header X-Real-IP $remote_addr; proxy_set_header Host $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }使用进入后台，登录名是 admin，密码我有些忘了，好像默认是 umami。好像一开始登录还会要求修改密码，我也忘了…… 登陆成功之后，第一件事自然是修改语言。右上角修改语言为中文，然后添加网站。注意域名处填写纯域名，开头不要带 https://，结尾不要带 /。之后将跟踪代码填入网站的相应位置，就可以开始数据记录了。体验「简洁」，是 Umami 的一大特色。首页以图表形式列出访问量、访问时长等基本信息，而柱状图则可以更直观地了解各个时间段网站的访问情况。而更详细的数据如流量来源、着陆页，访客设备、地区等，可以在「查看更多」找到。美中不足的是，访客地区的统计并不准确。在使用 Umami 之前，我也体验过一段时间的「Matomo」。Matomo 开启首页需要较长的加载时间，页面存在着无法关闭的开发者广告。功能丰富但略显臃肿。随着使用时间一长，统计数据量的增加，也会拖慢 Matomo 的运行效率，就需要更高规格的设备才能长期运行，总结下来就是整体使用体验不太好。Umami 则不同，在设计上就主张简洁、快速，运行占用资源低，配置简单，因此在加载上更迅速，使用起来更轻松，只是简洁意味着众多功能的缺失。不过对于小博客博主来说，这些基础功能就足够了。其他从 V1 迁移至 V2Umami 最近更新到了 V2 版本，数据库连接方式发生了改变，需要手动执行一下迁移命令。如果是使用 Docker 的话，可以这么迁移。这里参考了 Github 上的 Issue：https://github.com/umami-software/umami/issues/1887在确保设置好 DATABASE_URL 的前提下执行以下操作。首先，将 Umami 的镜像修改成 V1 版本，例如：image: ghcr.io/umami-software/umami:mysql-v1.40启动后，进入到 容器内部终端：docker exec -ti -u 0 umami sh其中，umami 是你设置的容器的名字，请注意修改。接着，执行如下命令：apk add git如果安装不动，可以再执行一下换源命令：sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositories然后再安装 git。接着，拉取迁移脚本：git clone https://github.com/umami-software/migrate-v1-v2.git如果拉去不动，可以在链接前加入 https://ghproxy.com/ 加速：git clone https://ghproxy.com/https://github.com/umami-software/migrate-v1-v2.git拉取完成后，进入该脚本内：cd migrate-v1-v2执行以下命令：yarn install接着依次执行以下命令：yarn build脚本会提示「Which database are you using?」 请选择你使用的数据库类型，按方向键选择。接着执行：yarn start运行到最后，脚本会提示「Do you want to delete V1 database tables? (Y/N)」是否删除 V1 数据？我是删了的，因为数据库太大了。然后输入 exit 退出终端，将镜像修改回 mysql-latest ，重新启动容器，即可完成升级工作。新版界面长这样：最后，你还要修改一下脚本代码，因为从 umami.js 改成 script.js 了，大概是为了防那些广告插件。毕竟 umami.js 已经进黑名单了。参考Linux 发行版安装 NodejsGitHub：UmamiUmami 文档