[{"content":"完成原有博文的迁移后，我开始写这篇文章。写时忽然想起，三年前中考结束后，我曾写过一篇题为“重新开始” 的文章。那时，我想在高中阶段用文字留下一些值得回忆的东西。结果是可喜的——后来，我从评论区得知，自己的文章确实曾帮助一些访客解决问题。这兴许正是我搭建这个博客的目的之一。\n高三学习压力骤增，我根本无暇顾及个人博客。我甚至放弃了撰写2025年的年终总结，把寒假时间全部投入深一模备考。如今高中生涯结束，我终于“自由”了！恰逢近三个月的长假，重新开始写博客，也就成了一件顺理成章的事。\n我为什么写博客？ 我希望借助写作，把零散的经历沉淀为可以重复利用的知识；也希望以此为起点，主动探索新的知识与技术，用所学创造未有之物。\n借助独立博客，我可以自主组织内容，自由设计网站的样式，不必为了迎合搜索与推荐算法而调整文章，也不必在意“喜欢”“关注”之类的量化指标。如果你喜欢这个博客，用 RSS 订阅它就好。\n我认为，写作是人理解事物的方式之一。很多想法在落笔之前，似乎已经被想明白了，真正写起来才会发现并非如此。对于一知半解的事物，即使头脑中留有印象，也很难清晰而有逻辑地说明其内涵；面对自己或他人的进一步追问，更可能无从回答。而写作会迫使作者先将零散的经验组织成结构，再用文字表达出来。\n进一步说，写作还会反过来加深作者对知识的理解。写作过程本身也会催生原本没有的想法。这些灵感与顿悟往往更具创造性，因为它们产生于作者不断梳理和深化认识的过程。正如 Paul Graham 所言 ，“Half the ideas that end up in an essay will be ones you thought of while you were writing it. Indeed, that\u0026rsquo;s why I write them.（一篇文章最终包含的想法，有一半是在写作过程中产生的。事实上，这正是我写文章的原因。）”\n只要这个站点仍在维护，我的文章就会保存在世界某个角落的服务器上，记录下我写作时的思维深度与知识边界。尽管重读过去那些文笔粗糙的文章时，我确有“悔其少作”之感，但它们毕竟真切地反映了我在那一刻的认知水平，值得保留下来。\n不过，写博客的目的或许不只是归档过去。比这更令我感到有成就感的是，我的文章能够对世界产生一些切实的影响。这种影响或许微乎其微，但倘若我的文字能帮助他人，让某个陌生人少走一些弯路，那也会是一件很幸福的事。除了通过内容帮助他人，我也希望这个博客成为人与人彼此相识的入口。\n这个站点的域名是 easton.pub。在这里，pub 取“酒馆”之意：一个供人进入、停留和交谈的公共空间。这样的意象很适合用来承载彼此之间的联系。曾经，我想结交一些志同道合的朋友，却在纷繁复杂的人际交往中感到迷茫，不知该如何达到灵魂的共鸣。我所追求的，并非外界赋予的一系列价值标签，也不是刻意经营的人脉网络，而是建立在共同兴趣与相互理解之上的真挚情谊。至少对我而言，这样的情谊在人来人往的现实生活中尤为难得。\n在赛博世界里，我不愿以功利之心罗列交友条件，因为那只会让寻觅的过程偏离初心、远离真诚，甚至使人违背内心的真实感受，以虚情假意示人。于是，我渴望拥有这样一方赛博空间，不必过度顾虑外界的目光，可以坦然地表达自我。在这里，我不必把外界的评判当作写作的尺度，可以保有充分的表达自由。我也无须反复揣测读者是否厌倦某个话题，更无须刻意迎合他人的兴趣偏好。\n可惜，在中文互联网上，并不缺少那些经过 SEO 精心包装、纯粹为了骗取点击而批量生产的低质内容。寥寥数语就能道尽的真知灼见，如今却被无情地拉长至冗长篇章，每一句都是刻意铺设的陷阱，只为诱使读者在无尽的滚动中误入无穷多个刺眼的横幅广告。\n所以，我常常想，好的网络社区不应该一味追求变现，变成一个个内容农场；抑或是一个个类似火车站的地方，间歇性谈论技术与话题，持续性大量持续涌入陌生人。久而久之，人来人往，社区并没有给人们真正的归属感。这四五年，我待过不少所谓的技术群聊和网络社区，也曾在其中认真写过内容，但最终大多选择了离开。后来我才发现，与其把表达寄托于不断更替的群聊和社区，不如先在自己的站点上自由写作，再让真正的联系自然发生。\n","permalink":"https://easton.pub/posts/re-restart/","summary":"\u003cp\u003e完成原有博文的迁移后，我开始写这篇文章。写时忽然想起，三年前中考结束后，我曾写过一篇题为\u003ca href=\"/posts/restart\" class=\"internal-link\"\u003e“重新开始”\u003c/a\u003e\n的文章。那时，我想在高中阶段用文字留下一些值得回忆的东西。结果是可喜的——后来，我从评论区得知，自己的文章确实曾帮助一些访客解决问题。这兴许正是我搭建这个博客的目的之一。\u003c/p\u003e","title":"第二次“重新开始”！"},{"content":"👋 很高兴在互联网的一角与你相遇！ 可以称呼我 Zephyr Easton！刚刚结束三年高中生活，如今是一名本科生，在广州小谷围岛上的大学城学习。\n现阶段对自己还没有一个足够明确的定位——相比于开发者、研究员或设计师，我更愿保留“学生”这一身份。\n我正试图理解基础学科的深层原理，探索机器处理信息与形成智能的方式，并尝试创造此前世上未有之物。\n我愿称此小站为一个“数字花园 ”（Digital Garden），这里会收录一些专业课学习笔记、大学生活等方面的文章，记录在实践中真实走过的弯路。\n坦率地讲，由于我阅历和认识有限，小站里的观点未必都正确。如你发现错误，请不吝赐教。当然，如果文章真的能帮助你解决问题，或启发你的思考，于我而言，便是一件令人欣喜之事。\n说回自己，我一直期待能与更广阔世界中的人们交流，在日益原子化的社会中，与更多美好的心灵建立联系。如果你对网站或文章内容有任何建议，或是有些想法愿意与我分享，期待来信！\n我的邮箱是： e@easton.pub 。我在社交媒体上不太活跃，因此更推荐通过邮件联系我，看到后会认真阅读、及时回信！\n📍 一些暂时的坐标 深圳 → 广州 · 小谷围岛 小谷围岛在广州的位置。图像由 GPT-Image-2 生成 * * *\n华南理工大学 · 2026级 · 本科生 黄框所示区域为华南理工大学大学城校区。图像由 GPT-Image-2 生成 🛠️ 目前在做 重新搭建这个停更一年多的网站 初步了解 WorkBuddy、Codex 等智能体工具的产品形态与基本使用方法 阅读《普林斯顿微积分读本》 …… (上述内容记录的是 2026 年 8 月的我。人与网站都在继续变化。)\n📜 一些已经发生的事 高考结束后的一个上午，我回到高中教室，拍下三百余张照片，并借助 Kaggle 的免费算力，训练出一个三维高斯泼溅（3D Gaussian Splatting，3DGS）​模型，将承载着高三记忆的教室定格下来。它会长期保存在腾讯云位于广州的对象存储服务器中。高中同学可以随时通过我的网站访问它。即使多年以后，教室原有的格局发生改变，甚至不复存在，这个模型仍会留在网络空间中，让共同记忆以数字形式长久保存。 3DGS 模型成品 * * *\n今年暑假，我曾借助 Claude Code 开发一款本地优先的学习辅助应用。应用基于 React Native 和 Expo 开发，原计划构建为 APK 文件。这款应用涵盖复习卡片（基于 FSRS 算法）、AI 对话、带有 AI 反馈功能的日记、定时闹钟等功能。由于 API 调用成本较高，且整体开发量超出预期，项目目前已暂时搁置。\n高二时，我创立了一个名为“通感实验室”的 AI 组织，专注于 AI 在教育领域的落地。我希望大众不再只把 AI 理解为“词语接龙”式的大语言模型，它还可以像人一样感知环境并与之交互，拥有某种“肉身感官”，也就是走向具身智能。我当时使用检索增强生成（RAG），为彼时崭露头角的 DeepSeek V3 接入一个由老师课件语料构成的知识库，做成了一套面向英语读后续写的垂直领域应用（当然，未经老师许可便直接使用课件作为语料，做法确实不妥😂）。借助高度相关的语料，系统的生成质量提升。此后，DeepSeek R1 发布，我们随之跟进，搭建了分别承担快速生成、深度思考和作文评分任务的三个模块。现在随着基础模型能力提升，专门开发读后续写模型的必要性降低（事实上，我高三备考期间，主要使用 Claude Opus 或 Gemini Pro 系列模型辅助英语作文写作，也不再使用自己曾经做出的垂直领域应用），但我始终相信，AI 终有一天能够连接不同媒介中的信息与知识，其形态也不会局限于大语言模型。\n💭 暂且相信 学习一种知识，最好的检验往往是用它做成什么。\n技术不天然意味着进步，进步也不保证将来必然发生。\n但即便如此，你我仍可以通过创造，去主动争取进步，哪怕只是寸丝半粟。\n好工具应当减少人对生活的忧虑，而不是要求人不断适应工具。\n理解可以暂时不完整，但表达不应伪装得比理解更成熟。\n🧭 从这里继续 想找教程与配置方案：前往 🛠️ 工作台 想了解一个项目从想法到成品：前往 🏗️ 建造间 想看失败经历与修正：前往 🗑️ 废料堆 想读行业技术评论：前往 🔭 观察站 想看季度书影音小结：前往 📚 藏书室 想漫无目的地浏览：前往 🌿 后花园 🏛️ 小站历史 2022年5月，在 wordpress.com 上注册免费站点，用来写博客。 2022年12月，申请的 eu.org 免费域名通过审批，将博客程序迁移至 Typecho。 2023年4月，误删数据库，又没有定期备份的习惯，致使一批建站初期的文章丢失，最终只根据手机截图复原了第一篇文章。 2023年至2024年8月，快乐地使用 Typecho 发布文章。 2024年8月20日，注册 brume.top 域名，并借此启动博客改版。 2024年8月23日，博客框架选择了 Hugo，并迁移博客文章。 2025年1月30日，注册当前的 easton.pub 域名。 2026年8月，完成旧站向 easton.pub 的迁移。 ","permalink":"https://easton.pub/about/","summary":"\u003ch3 id=\"-很高兴在互联网的一角与你相遇\"\u003e👋 很高兴在互联网的一角与你相遇！\u003c/h3\u003e\n\u003cp\u003e可以称呼我 \u003cstrong\u003eZephyr Easton\u003c/strong\u003e！刚刚结束三年高中生活，如今是一名本科生，在广州小谷围岛上的大学城学习。\u003c/p\u003e\n\u003cp\u003e现阶段对自己还没有一个足够明确的定位——相比于开发者、研究员或设计师，我更愿保留“\u003cstrong\u003e学生\u003c/strong\u003e”这一身份。\u003c/p\u003e","title":"关于"},{"content":"高考结束后，三年的高中生活也随之落幕，此后再回到这间教室的机会大概不会很多。再次回到教室，环顾四周，想到就要与这个见证了高三拼搏的地方分别，终究有些不舍。于是，我萌生了将这间教室以数字形式保存下来的想法。我选择了近年兴起的三维高斯泼溅技术，使用实拍的三百六十多张照片，“重建”了这间教室。作为第一次 3DGS 实践，最终的渲染效果已令我颇为满意。因此，我写下这篇文章，记录并复盘整个项目流程。\n关于高斯泼溅 通俗地说，高斯泼溅 (3D Gaussian Splatting) 是一种根据多视角照片重建并渲染三维场景的技术。它使用大量具有位置、形状、颜色、透明度等属性的“三维高斯”来表示场景；渲染时，这些高斯会投影并混合到二维画面上。与传统的使用三角形对空间进行建模的方法相比，3DGS 不需要先构建完整的表面连接关系；与 NeRF 等神经网络模型相比，它通常训练更快，也更容易实现实时渲染。它不显式地描述物体表面的连接关系，而是用大量位置、尺度和方向各异的三维高斯表示场景。从训练视角附近观察时，通常能够渲染出十分逼真的图像。\n本教程关注实践，对理论要求不高。若希望进一步了解 3DGS 的技术原理，参见B站视频：Gaussian Splatting - 高斯泼溅技术原理与应用 。\n设置相机参数 我们需要从不同位置和角度采集教室照片，供 COLMAP 估计相机位姿并恢复初始的三维结构。照片使用手机拍摄即可。不建议通过拍摄视频并从中提取图片的方式，因为视频帧通常分辨率较低、压缩较重，并容易出现运动模糊，单帧细节通常不及照片。\n建议使用手机（或相机）的“专业模式”拍摄，锁定相关参数，减少照片之间非几何因素造成的变化，从而提高特征匹配和后续训练的稳定性。\n相关参数可以使用如下设置：\n画面比例：建议选择4:3。因为目前多数手机主摄传感器的原生画幅接近 4∶3。如“16:9”或“全屏”，实际上是手机在 4:3 的基础上进行裁剪，不是物理意义的变焦。不可以在拍摄过程中更改画面比例或数字变焦倍数，否则，不同照片将被错误地视为拥有同一组相机内部参数。\n白平衡：建议固定。3DGS 的训练更希望同一表面在不同照片中的颜色和亮度保持相对稳定。在白平衡不固定的情况下，由于拍摄对象处于顺光或逆光状态，在不同视角下，相机算法会对白平衡进行自动调整，使拍摄物体总体呈现比较美观的视觉效果。明显的颜色和亮度变化会降低部分特征的一致性，使 3DGS 更难用同一组高斯拟合多张照片。拍摄者需要将白平衡调节至荧光灯或日光灯水平，或者移动滑块，调节至画面光照看起来比较自然的位置，拍摄过程中不去动它。\n感光度：设置成100或200，不要太高。感光度衡量感光元件对光的敏感程度。感光度越高，照片在相同拍摄条件下越亮。在低感光度的条件下，拍摄出的照片纹理清晰，边缘锐利，容易辨识。感光度升高通常会使信噪比降低（想象一下你使用一款拍照功能不那么强大的手机，在夜间拍摄室外风景，画面中遍布大量随机噪点的场景），也就是说，在相同尺度下，虚假噪点更多了。高感光度带来的大量随机噪点，极易被算法误判成物体表面的细小纹理，给特征点匹配带来误差，相机位姿估计精度下降。\n对焦：拖动滑块至0.8~0.9之间，使屏幕上大部分区域清晰，拍摄过程中保持不变。若使用自动对焦，相机可能在不同照片中反复改变焦点位置，造成局部模糊程度和有效内参变化。如果在拍摄过程中切换镜头或改变数字变焦倍率，照片将具有不同的相机内参，而本文后续又使用 single_camera 处理，易导致标定失败或精度下降。\n曝光补偿：设置为0。\n如上图，这是我在固定上述设置后，走到教室右后方拍摄的一张照片，个人感觉是质量较高的。照片具有较好的清晰度和锐度，黑板上和墙壁上的文字，以及教室内的设施细节，都能比较清晰地辨识出来。图片中纹理比较丰富，包含地板的水磨石纹理、蓝色课桌、红色消防管道。这些纹理有助于算法更准确地估计相机位置。\n拍摄流程 教室可以近似看作一个长方体空间。拍摄的目标是让墙面、地面、天花板和主要物体被多张照片从多个角度观察到，并在相邻照片之间保持充分重叠。\n我的拍摄路线如下：\n在教室的边缘走一圈\n从教室前门附近，面向教室内部，沿墙内侧顺时针绕教室一圈，镜头水平略偏下，每走一步拍一张。尽可能走慢一点，步幅小一些，到教室四个角落时，再从角落向对角方向拍一张照片。共拍摄约60张。\n下图为 GPT-Image-2 根据我的描述生成的路线，可参考但不必完全参考。 沿着教室过道，走“S”形\n从教室前门出发，面向后方，沿着座位之间的过道走“S”形，镜头水平略向下。沿每条课桌间的过道拍摄，每走一步拍一张。在讲台等细节丰富的区域，从不同角度多拍几张。 蹲姿拍摄\n降低机位，再沿相同的 S 形路线拍摄一遍。镜头水平即可。这个角度可以补充桌斗底部、椅子腿和地面纹理等在正常视角下容易被遮挡的细节。同样每走一步拍摄一张。 高处俯拍\n蹲姿走完全程，是很累的，先休息一下。将手机举过头顶，使镜头略微朝下，仍沿前述 S 形路线拍摄。每两步拍一张即可。 补充局部细节\n可以在保持与周围画面充分重叠的前提下，补拍天花板灯具、地面、讲台、窗户和门等区域。 需要注意的一些事项：\n不可以在同一个位置旋转相机拍摄。前处理阶段的 COLMAP 需要利用不同相机位置产生的视差，通过三角测量恢复空间点的深度。例如：给定拍摄两张图片时的两相机中心 $C_1$ 和 $C_2$ ，同一空间点 \\(P\\) 在两张图像上的投影分别为 \\(p_1\\) 和 \\(p_2\\)。显然，\\(p_1\\) 和 \\(p_2\\) 分别确定了从 \\(C_1\\) 和 \\(C_2\\) 指向空间点 \\(P\\) 的观测方向。从相机位置出发，通过 $p_1$,$p_2$ 确定出的方向，引出两条射线，理想情况下，两条射线的交点就是空间点 \\(P\\) 的位置。若是 $C_1$,$C_2$ 重合，那么就没法画出两条不同的射线指向空间点 $P$，也就没法确定物体的位置了。 除了水平视角，还应适当向上、向下拍摄，以覆盖天花板、桌椅底部和地面；同时可以改变机位高度，增加竖直方向的视差。避免由于前述原因，垂直方向上的物体间的相对距离无法算出。 尽量避免拍到走动的人、摆动的窗帘等动态物体。少量动态区域未必使匹配失败，但会增加错误匹配和漂浮伪影。 前期准备 照片拍摄完成后，我计划使用 Kaggle 提供的免费算力训练模型。\n打包照片\n将此前拍摄的所有照片，全部放进一个文件夹。然后压缩成一个 .zip 格式的压缩包。压缩包文件名建议全英文，避免报错。\n注册 Kaggle 并验证手机\n打开 Kaggle 官网，注册并登录后，点击右上角头像，在菜单栏中点击 “Settings” ，找到 “Phone verification” ，进行手机号验证，以获得 GPU 的使用资格。经实测，能使用中国大陆地区的手机号（+86）验证。\n上传数据集\n点击 Kaggle 首页左侧边栏 “Data Hub” 中的 “Datasets” 进入“数据集” 页面，点击“+ New Dataset”新建数据集，将压缩包上传上去，上传完成后，Kaggle 会自动解包，不需要我们动手写解压代码。\n创建 Kaggle Notebook\n在 Kaggle 左侧菜单边栏点击 “Create” ，选择 “Notebook”，新建一个笔记本。它是一种交互式网页文档，可以运行代码块。进入笔记本后，在页面顶栏的 “Settings” 中找到 “Accelerator”，将它改成 “GPU T4 x2”，并确认。完成上述设置后，页面应与下图大致相同。\n随后，在右侧栏找到 “Input”，点击 “Add Input”，选择 “Your Datasets”，选择刚才上传的数据集，添加进项目中。\n模型训练 在工作区内点击 “+”，新建一个代码块。代码块中输入代码后，可以点击形如三角形的播放按钮运行代码。\n先新建两个代码块，分别粘贴如下代码并执行，它们分别用于安装环境和自动整理照片：\n!apt-get update -y !apt-get install colmap imagemagick -y %cd /kaggle/working !git clone --recursive https://github.com/graphdeco-inria/gaussian-splatting %cd gaussian-splatting !pip install -q plyfile !pip install -q ./submodules/diff-gaussian-rasterization !pip install -q ./submodules/simple-knn print(\u0026#34;环境安装完成！\u0026#34;) import os import shutil input_base = \u0026#39;/kaggle/input\u0026#39; datasets = os.listdir(input_base) if len(datasets) \u0026gt; 0: dataset_path = os.path.join(input_base, datasets[0]) print(f\u0026#34;找到你上传的数据集: {dataset_path}\u0026#34;) else: print(\u0026#34;未找到数据集，请检查右侧 Input 里是否添加成功\u0026#34;) project_dir = \u0026#39;/kaggle/working/project\u0026#39; input_dir = os.path.join(project_dir, \u0026#39;input\u0026#39;) os.makedirs(input_dir, exist_ok=True) count = 0 valid_exts = [\u0026#39;.jpg\u0026#39;, \u0026#39;.jpeg\u0026#39;, \u0026#39;.png\u0026#39;, \u0026#39;.JPG\u0026#39;, \u0026#39;.JPEG\u0026#39;, \u0026#39;.PNG\u0026#39;] for root, dirs, files in os.walk(dataset_path): for file in files: if any(file.endswith(ext) for ext in valid_exts): shutil.copy(os.path.join(root, file), os.path.join(input_dir, f\u0026#34;img_{count}.jpg\u0026#34;)) count += 1 print(f\u0026#34;整理完毕！一共读取了 {count} 张照片。\u0026#34;) 成功完成上述步骤之后，页面显示类似下图：\n接下来，使用 COLMAP 进行相机标定与稀疏重建。也就是说，COLMAP 估计输入照片拍摄时相机的位置和朝向，计算出相机位姿。同时，它也会匹配照片中的一些共同特征点，进行三角测量，生成一个粗糙的三维点云，为后续 3DGS 初始化相机和三维高斯提供基础数据。\n此外，由于底层的 COLMAP 软件带有 Qt 依赖，在无外接显示器的 Kaggle 平台上训练时，程序会因无法找到屏幕而崩溃。因此推荐使用 xvfb-run，模拟出图形环境，欺骗 COLMAP 让它以为自己在有屏幕的服务器上运行。\n整个数据流可分为特征提取、特征匹配、稀疏重建、格式转换四个阶段。\n对于特征提取部分的脚本，COLMAP 通常使用 SIFT 算法。SIFT 算法找到每张图片中有辨识度的部分区域，称为“特征”。每个特征包含“关键点（Keypoint）”和“描述子（Descriptor）”两个部分。关键点记录当前区域的位置、尺度和使用统计学方法得出的这块区域的“主要方向”。随后，SIFT 为每个关键点生成一个 128 维的描述向量，即“描述子”。如果两张照片拍到了同一个特征，对应的描述子的距离会较小，从而可识别出一定的空间结构。\n在 COLMAP 为每张照片找到关键点并计算完描述子后，这些信息都将写入数据库，地址为/kaggle/working/project/database.db。\n由于此前在数据采集的过程中，所有照片的相机、镜头、焦段都相同，故设置single_camera 1，声明相机内参相同。这可以缩短计算时间。\n此外，由于普通镜头在拍摄照片时，画面会存在弯曲、拉伸等畸变情况，因此将 COLMAP 的相机模型设为 OPENCV，使相机模型包含畸变参数。\n在提取完成后，COLMAP 已经知道每张图片中所包含的所有特征。不同的图片中会包含非常相似的特征，它们往往对应真实世界中的同一个空间点。一般通过计算两个描述子向量的欧氏距离衡量它们之间的相似程度。对任意两个描述子向量 \\( A \\) 和 \\( B \\)，欧氏距离 \\( d = \\sqrt{\\sum_{i=1}^{128} (A_i - B_i)^2} \\)。距离越近意味着两个关键点越相似，越有可能是同一个空间处的点。\n但是，不难发现，在教室场景中，很多相似的课桌椅会产生非常相似的描述子，但描述子对应的关键点可能不同。不过，倘若两张照片确实拍到了同一处场景，那么两张图上的所有相匹配的特征点之间一定满足某种特殊的几何关系。这种严格的几何关系约束能够过滤掉很多错误匹配。\n对于图像的特征匹配，假如有 N 张照片，如果将所有图片两两比较，需要比较的总次数是 $\\frac{N(N-1)}{2}$ 次，对于我的包含三百六十多张图片的数据集来说，将要进行六万五千多次比对，如此庞大的计算量是很可怕的。\n可笑的是，我最初确实是使用穷举匹配的。数据表明，仅匹配一小部分图片就耗费了一个多小时。\n考虑到我的照片都是连续拍摄的，某一张照片的画面通常和它前后几张照片的画面重叠程度较高，而和它后面上百张照片的画面重叠程度更低，所以设置 --SequentialMatching.overlap 20，让 COLMAP 主要匹配文件顺序上相邻的 20 张照片。这样，匹配次数下降至三千多次，大幅缩短了特征匹配时间。\n匹配完成后，COLMAP 已经知道了相同物体在不同图像上的位置，此时，COLMAP 仍不知道这些匹配点的三维位置。稀疏重建可以恢复三维结构，但由于空间距离未知，整个模型的绝对尺度无法确定。\n从二维图像中恢复三维结构需要依赖“视差”。例如，我现在正坐在电脑屏幕前写这篇文章。我将右眼闭上，并将左手食指放在左眼前某处。我的手指遮住了一部分电脑屏幕，但仅凭这一视角，很难准确判断手指与屏幕之间的距离。随后闭上左眼、睁开右眼，这时所见手指位置发生明显变化，它相对于屏幕也会发生明显移动。据此，可粗略估计手指与屏幕间的相对距离大小。将手指向前或向后移动并静止，重复上述操作，会得到明显不同的相对距离估计值。这与相机从不同位置拍摄同一物体相似：距离相机近的物体，在不同图像中移动距离较大，远的物体，移动距离较小。\nCOLMAP 通常会先从全部照片中选择一对合适的图像，作为稀疏重建的起点。这两张照片需要有充分的画面重叠和足够多的可靠匹配点，同时相机位置距离比较远。选定这两张图片后，算法能够计算出这两张图片的相对位置和朝向。得到相机位姿后，这两张照片就被注册到当前模型中。\n对于在这两张图片上都出现的同一个空间点，COLMAP 可以从两条投影射线求交，估计该点的三维位置。其基本原理在前文“拍摄流程 ”里“需要注意的一些事项”中说明了。由于实际匹配存在误差，射线通常不会精确相交，因此求解的是使误差最小的三维点。像这样三角化三维点，COLMAP 生成了第一批稀疏的 3D 点云。\n此后，COLMAP 会寻找其他图片中的特征与生成的 3D 点云之间的对应关系，反推出第三张图片的相机位姿，新相机被注册后，又可以与已有相机三角化更多三维点，整个过程不断循环。\n不过，前面所说的每一步都存在不可避免的误差，在不断加入图片的过程中，误差会累积，COLMAP 会在加入一部分照片后，对所有相机参数和所有三维点进行优化、微调。对于已知的一个三维点和给定的相机位姿和相机内参，COLMAP 预测该三维点在对应照片中的投影坐标 \\((x,y)\\)。此位置与实际观测到的关键点位置之间的距离，就是重投影误差。COLMAP 会执行 Bundle Adjustment，使用最小二乘法，调整相机参数和三维点空间坐标，使全部照片的总重投影误差尽可能小。\n需要使用参数--Mapper.ba_global_function_tolerance=1e-6控制 Bundle Adjustment 迭代的停止。当目标函数的相对改善幅度低于该阈值时，优化即可停止。\n稀疏重建完成后，考虑到全部照片彼此连通，生成一个模型的情况，会生成如下目录：\nsparse/0/ ├── cameras.bin ├── images.bin └── points3D.bin 其中，cameras.bin保存相机的焦距、光学中心、畸变模型等内参，images.bin保存已注册的每张图片的信息，包括图片名称、相机位姿等。points3D.bin保存稀疏点云，它在三维空间中恢复了容易匹配的特征位置。\n随后进行格式转换。假设 COLMAP 生成了 sparse/0，如果不存在，就在当前文件夹下寻找第一个子目录。\n3DGS 仓库中的 convert.py 可以调用 COLMAP，并将原始数据整理成训练代码所需的格式。\nCOLMAP 允许使用 OpenCV 畸变模型，但是 3DGS 更希望输入图片符合针孔相机模型，因此需要将原始畸变图片重新采样为无畸变图片。去畸变后，相机内参也会随之更新。如果不执行格式转换和去畸变，输入有畸变的图片，3DGS 却按照无畸变的针孔相机模型计算，会导致高斯点放置错误，画面边缘模糊的异常情况。\n经过上述流程，完成了 COLMAP 预处理。完整的代码如下。由于 Kaggle 通过 apt 安装的 COLMAP 是预编译好的 CPU 版本，在预处理阶段，无法使用 GPU 加速，预处理过程可能持续数小时。\n%cd /kaggle/working/gaussian-splatting import os project_path = \u0026#34;/kaggle/working/project\u0026#34; db_path = os.path.join(project_path, \u0026#34;database.db\u0026#34;) sparse_path = os.path.join(project_path, \u0026#34;sparse\u0026#34;) input_path = os.path.join(project_path, \u0026#34;input\u0026#34;) os.makedirs(sparse_path, exist_ok=True) # 安装虚拟显示器工具 !apt-get install -y xvfb \u0026gt; /dev/null 2\u0026gt;\u0026amp;1 print(\u0026#34;xvfb 安装完成\u0026#34;) def run_colmap(cmd): full_cmd = f\u0026#34;xvfb-run -a -s \\\u0026#34;-screen 0 1920x1080x24\\\u0026#34; {cmd}\u0026#34; print(f\u0026#34;执行命令中，请稍候\u0026#34;) return os.system(full_cmd) # 第一步：特征提取 print(\u0026#34;\\n第一步：特征提取\u0026#34;) cmd1 = f\u0026#34;colmap feature_extractor --database_path {db_path} --image_path {input_path} --ImageReader.single_camera 1 --ImageReader.camera_model OPENCV\u0026#34; if run_colmap(cmd1) != 0: raise RuntimeError(\u0026#34;特征提取失败\u0026#34;) # 第二步：特征匹配 print(\u0026#34;\\n第二步：特征匹配\u0026#34;) cmd2 = f\u0026#34;colmap sequential_matcher --database_path {db_path} --SequentialMatching.overlap 20\u0026#34; if run_colmap(cmd2) != 0: raise RuntimeError(\u0026#34;特征匹配失败\u0026#34;) # 第三步：稀疏重建 print(\u0026#34;\\n第三步：稀疏重建\u0026#34;) cmd3 = f\u0026#34;colmap mapper --database_path {db_path} --image_path {input_path} --output_path {sparse_path} --Mapper.ba_global_function_tolerance=1e-6\u0026#34; if run_colmap(cmd3) != 0: raise RuntimeError(\u0026#34;稀疏重建失败\u0026#34;) # 第四步：格式转换 print(\u0026#34;\\n第四步：格式转换\u0026#34;) model_dir = os.path.join(sparse_path, \u0026#34;0\u0026#34;) if not os.path.exists(model_dir): subdirs = [d for d in os.listdir(sparse_path) if os.path.isdir(os.path.join(sparse_path, d))] if subdirs: model_dir = os.path.join(sparse_path, subdirs[0]) cmd4 = \u0026#34;python convert.py -s /kaggle/working/project --skip_matching --no_gpu\u0026#34; if os.system(cmd4) != 0: raise RuntimeError(\u0026#34;格式转换失败\u0026#34;) print(\u0026#34;完成了 COLMAP 预处理！\u0026#34;) 在完成 COLMAP 预处理后，会进入 3DGS 训练流程。\n在预处理阶段，COLMAP 已经估计出相机位置、朝向以及初始三维点，train.py负责将这些点优化成能够从各个视角重现照片的三维高斯。\n设置 --iterations 15000，将总训练轮数设为 15000 次，即官方默认 30000 次的一半，以实现效率和效果的平衡。\n3DGS 训练的对象是大量三维高斯。可以将一个三维高斯当成一个带颜色和透明度的椭球。越靠近高斯中心，三维高斯的特征越明显，越远离中心，其特征越弱。它像一团带着颜色的雾。从不同角度观察高斯球，由于球谐函数的作用，会看到不同颜色。\n3DGS 将 COLMAP 计算出的三维点初始化为高斯，规定它的中心坐标、初始的颜色、大小、透明度等参数。3DGS 会维护一个相机列表，每次迭代随机选择一张照片对应的相机。在选定照片后，用当前高斯从该相机视角渲染图片。渲染的图片与真实图片之间存在差异，这种差异被称为损失。3DGS 重点关注像素点的颜色，物体的结构、纹理是否接近真实图片。用数学方法量化出损失数值后，需要明确每个高斯的哪些参数导致了损失，以及每个高斯的参数如何修改以最小化损失。\n假设一个高斯点的位置相较于三维空间中的真实位置存在偏移，它渲染出的物体位置同样会与实际位置之间出现误差。此时损失函数会产生关于位置的梯度 $\\frac{\\partial L}{\\partial x},\\frac{\\partial L}{\\partial y},\\frac{\\partial L}{\\partial z}$。同理，也会出现损失对透明度、大小、颜色等参数的梯度。由于渲染过程可微，误差会从渲染图片传回高斯参数，这一过程被称作反向传播。接着，3DGS 会将相关高斯点的参数向损失下降最快的方向修改，更新部分高斯的大小、颜色、旋转和透明度等参数。\n此外，训练初期，由 COLMAP 稀疏点云初始化得到的高斯数量有限。对于一些较为精密的物体，如讲台上的金属笔筒，少量高斯难以充分描绘此类精细结构，仅仅通过反向传播修改高斯点参数也无能为力。这时，3DGS 会对这些难以拟合的部分增加高斯点的数量，这一过程被称为“增密”。对于当前高斯不算太大但是也难以拟合真实场景的情况，3DGS 会在当前位置复制一个相同的高斯。两个高斯在之后的学习过程中能学到不同的参数，从而更好地拟合现实世界。此外，对于一些原本尺寸较大的高斯，在合适情况下，3DGS 会将大高斯拆分成几个更小的高斯，每个小高斯分别负责学习不同区域。\n显而易见，对高斯的增密不能无限度地进行，避免显存占用增大、生成模型文件过大的情况。对于一些基本透明的高斯，它们的存在与否不会显著影响观感，这种情况下3DGS 会删除相关高斯，这一操作称为“剪枝”。\n上述操作流程会在 3DGS 训练中反复迭代上万次。不同阶段的训练流程略有不同。通常，在前 500 次中，先优化现有高斯，500 次以后，开始增密。此后通常每隔 100 次执行一次增密和剪枝。15000 次之后，停止改变高斯数量。对于 3DGS 的训练，通常执行 15000 次可以快速验证流程是否成功，30000 次迭代通常更适合作为最终模型。\n上述过程计算量巨大，且官方 3DGS 实现依赖 CUDA 的扩展，需要 NVIDIA GPU 才能训练。\n训练完成后，会生成文件 point_cloud.ply。它是一个存储三维高斯数据的点云文件，包含所有高斯点的全部属性。\n接着将此文件复制到 Kaggle 的根目录下，便于通过文件浏览器找到它。点击下载按钮，即可获得原始的点云文件。\n训练代码如下：\n%cd /kaggle/working/gaussian-splatting !python train.py -s /kaggle/working/project --iterations 15000 import os import glob import shutil output_dir = \u0026#34;/kaggle/working/gaussian-splatting/output\u0026#34; try: latest_folder = max(glob.glob(os.path.join(output_dir, \u0026#39;*/\u0026#39;)), key=os.path.getmtime) ply_file = os.path.join(latest_folder, \u0026#34;point_cloud\u0026#34;, \u0026#34;iteration_15000\u0026#34;, \u0026#34;point_cloud.ply\u0026#34;) if os.path.exists(ply_file): shutil.copy(ply_file, \u0026#34;/kaggle/working/classroom_model.ply\u0026#34;) print(\u0026#34;太棒了！模型训练成功！\u0026#34;) print(\u0026#34;在网页【右侧栏】，展开【Output】→【/kaggle/working】，点击 classroom_model.ply 旁边的【下载图标】即可下载到本地！\u0026#34;) else: print(\u0026#34;找不到模型文件，可能训练未完成。\u0026#34;) except Exception as e: print(\u0026#34;保存时出现错误：\u0026#34;, e) 压缩原始模型 训练完成后，原始的 .ply 文件内可能存在一些远离模型主体的噪点或杂乱色块。它们有碍观感，且增大了模型体积。推荐使用 SuperSplat 工具删除噪点并压缩模型。\n访问 SuperSplat 官网 ，进入编辑器。将下载好的原始模型文件拖进页面。随即，模型被渲染出来，模型加载完成后，可以旋转视角检查主体结构。如图，可见获得较完美的效果，说明此前工作比较到位。\n随后滚动鼠标滚轮拉远视角，使整个模型进入视野。在所有高斯点都能在网页上显示的尺度下，可见模型主体之外存在一些成簇的高斯点。它们的实际位置可能在教室外面，删去部分后对于模型效果的影响很小。\n如下图，在下方工具栏选择“套索”工具，选中期望删除的区域。被选中的区域将变成黄色。按下键盘 Delete 按键，即可将其删除。删除噪点后，可以导出模型为更小格式。\n点击左上角的“文件” → “导出” → “Splat (.splat)”，将原始的 .ply 文件另存为 .splat 格式文件。.splat 格式文件占用空间更小，更便于在网页中加载和渲染，有助于分享 3DGS 效果。\n在本地网页预览模型 选择一个文件夹，放入导出的 .splat 文件。新建 index.html 文件。输入下面这段网页代码（假设模型文件名为 classroom.splat）：\n\u0026lt;!DOCTYPE html\u0026gt; \u0026lt;html lang=\u0026#34;zh-CN\u0026#34;\u0026gt; \u0026lt;head\u0026gt; \u0026lt;meta charset=\u0026#34;UTF-8\u0026#34;\u0026gt; \u0026lt;meta name=\u0026#34;viewport\u0026#34; content=\u0026#34;width=device-width, initial-scale=1.0\u0026#34;\u0026gt; \u0026lt;title\u0026gt;我的高中教室 3DGS\u0026lt;/title\u0026gt; \u0026lt;style\u0026gt; body { margin: 0; padding: 0; overflow: hidden; background-color: #000; } \u0026lt;/style\u0026gt; \u0026lt;script type=\u0026#34;importmap\u0026#34;\u0026gt; { \u0026#34;imports\u0026#34;: { \u0026#34;three\u0026#34;: \u0026#34;https://unpkg.com/three@0.157.0/build/three.module.js\u0026#34;, \u0026#34;@mkkellogg/gaussian-splats-3d\u0026#34;: \u0026#34;https://unpkg.com/@mkkellogg/gaussian-splats-3d@0.4.0/build/gaussian-splats-3d.module.js\u0026#34; } } \u0026lt;/script\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;script type=\u0026#34;module\u0026#34;\u0026gt; import * as GaussianSplats3D from \u0026#39;@mkkellogg/gaussian-splats-3d\u0026#39;; const viewer = new GaussianSplats3D.Viewer({ \u0026#39;initialCameraPosition\u0026#39;: [0, 1, 3], \u0026#39;cameraUp\u0026#39;: [0, -1, 0], \u0026#39;sharedMemoryForWorkers\u0026#39;: false }); viewer.addSplatScene(\u0026#39;classroom.splat\u0026#39;, { \u0026#39;showLoadingUI\u0026#39;: true, \u0026#39;position\u0026#39;: [0, 0, 0] }).then(() =\u0026gt; { viewer.start(); }).catch((err) =\u0026gt; { console.error(\u0026#34;加载出错了：\u0026#34;, err); alert(\u0026#34;加载模型失败，请按 F12 查看控制台报错！\u0026#34;); }); \u0026lt;/script\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; 在终端中进入该文件夹。输入命令：python -m http.server 8000，创建一个本地服务器。打开 http://localhost:8000 ，即可在浏览器中预览模型，并使用鼠标旋转或移动视角。此外，不要直接双击打开 index.html，因为浏览器的安全策略可能阻止模块脚本加载，故需要启动 HTTP 服务器。\n将模型部署上云 我的方案是：网页文件部署在免费的 Cloudflare Pages 上。由于 Cloudflare Pages 单个文件限制大小不超过 25MB，我的模型不满足此条件，故选择将模型文件保存在腾讯云的对象存储服务器中。\n需要留心的是，腾讯云的对象存储是收费服务，存储和流量分开计费。如果希望通过网页分享模型效果，模型就必须交给浏览器渲染，需要注意防止流量被他人盗刷。\n在腾讯云的对象存储页面新建一个桶，将桶的访问权限设置为“公有读私有写”（此操作有风险，参见后文），上传 .splat 文件，记录文件分配到的 URL，供 HTML 文件中引入。\n在一个空文件夹下新建 index.html，复制并按要求修改以下代码：\n\u0026lt;!DOCTYPE html\u0026gt; \u0026lt;html lang=\u0026#34;zh-CN\u0026#34;\u0026gt; \u0026lt;head\u0026gt; \u0026lt;meta charset=\u0026#34;UTF-8\u0026#34;\u0026gt; \u0026lt;meta name=\u0026#34;viewport\u0026#34; content=\u0026#34;width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no\u0026#34;\u0026gt; \u0026lt;title\u0026gt;我的高中教室 3DGS\u0026lt;/title\u0026gt; \u0026lt;style\u0026gt; body { margin: 0; padding: 0; overflow: hidden; background-color: #000; touch-action: none; } #canvas-container { width: 100vw; height: 100vh; position: absolute; z-index: 1; } .control-panel { position: absolute; bottom: 30px; z-index: 10; display: grid; gap: 8px; user-select: none; -webkit-user-select: none; } #left-panel { left: 20px; grid-template-columns: 50px 50px 50px; grid-template-rows: 50px 50px; } #right-panel { right: 20px; grid-template-columns: 50px 50px 50px; grid-template-rows: 50px 50px; } .btn { background: rgba(255, 255, 255, 0.2); backdrop-filter: blur(4px); border: 1px solid rgba(255,255,255,0.4); border-radius: 10px; color: white; font-weight: bold; font-size: 14px; display: flex; justify-content: center; align-items: center; cursor: pointer; box-shadow: 0 4px 6px rgba(0,0,0,0.3); touch-action: none; } .btn:active { background: rgba(255, 255, 255, 0.5); transform: scale(0.95); } .up-btn { grid-column: 2; grid-row: 1; } .down-btn { grid-column: 2; grid-row: 2; } .left-btn { grid-column: 1; grid-row: 2; } .right-btn { grid-column: 3; grid-row: 2; } .rotL-btn { grid-column: 1; grid-row: 1; } .rotR-btn { grid-column: 3; grid-row: 1; } \u0026lt;/style\u0026gt; \u0026lt;script type=\u0026#34;importmap\u0026#34;\u0026gt; { \u0026#34;imports\u0026#34;: { \u0026#34;three\u0026#34;: \u0026#34;https://unpkg.com/three@0.157.0/build/three.module.js\u0026#34;, \u0026#34;@mkkellogg/gaussian-splats-3d\u0026#34;: \u0026#34;https://unpkg.com/@mkkellogg/gaussian-splats-3d@0.4.0/build/gaussian-splats-3d.module.js\u0026#34; } } \u0026lt;/script\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;div id=\u0026#34;canvas-container\u0026#34;\u0026gt;\u0026lt;/div\u0026gt; \u0026lt;div class=\u0026#34;control-panel\u0026#34; id=\u0026#34;left-panel\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;btn up-btn\u0026#34; data-action=\u0026#34;forward\u0026#34;\u0026gt;前进\u0026lt;/div\u0026gt; \u0026lt;div class=\u0026#34;btn left-btn\u0026#34; data-action=\u0026#34;left\u0026#34;\u0026gt;向左\u0026lt;/div\u0026gt; \u0026lt;div class=\u0026#34;btn down-btn\u0026#34; data-action=\u0026#34;backward\u0026#34;\u0026gt;后退\u0026lt;/div\u0026gt; \u0026lt;div class=\u0026#34;btn right-btn\u0026#34; data-action=\u0026#34;right\u0026#34;\u0026gt;向右\u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;div class=\u0026#34;control-panel\u0026#34; id=\u0026#34;right-panel\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;btn rotL-btn\u0026#34; data-action=\u0026#34;rotL\u0026#34;\u0026gt;左转\u0026lt;/div\u0026gt; \u0026lt;div class=\u0026#34;btn up-btn\u0026#34; data-action=\u0026#34;up\u0026#34;\u0026gt;上升\u0026lt;/div\u0026gt; \u0026lt;div class=\u0026#34;btn rotR-btn\u0026#34; data-action=\u0026#34;rotR\u0026#34;\u0026gt;右转\u0026lt;/div\u0026gt; \u0026lt;div class=\u0026#34;btn down-btn\u0026#34; data-action=\u0026#34;down\u0026#34;\u0026gt;下降\u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;script type=\u0026#34;module\u0026#34;\u0026gt; import * as THREE from \u0026#39;three\u0026#39;; import * as GaussianSplats3D from \u0026#39;@mkkellogg/gaussian-splats-3d\u0026#39;; const originalFetch = window.fetch; window.fetch = async function(resource, init) { const url = typeof resource === \u0026#39;string\u0026#39; ? resource : resource.url; if (url \u0026amp;\u0026amp; url.includes(\u0026#39;.splat\u0026#39;)) { const cacheName = \u0026#39;3dgs-model-cache-v2\u0026#39;; const cache = await caches.open(cacheName); let response = await cache.match(url); if (!response) { console.log(\u0026#34;downloading\u0026#34;); response = await originalFetch(resource, init); if (response.ok) { cache.put(url, response.clone()); console.log(\u0026#34;cache saved\u0026#34;); } } else { console.log(\u0026#34;cache hit\u0026#34;); } return response; } return originalFetch(resource, init); }; const viewer = new GaussianSplats3D.Viewer({ \u0026#39;initialCameraPosition\u0026#39;: [-1.21046, -0.74130, -0.07261], //修改成适配自己模型的参数。参数可在本地渲染模型的过程中使用快捷键 Ctrl + Shift + I 查看 \u0026#39;initialCameraLookAt\u0026#39;: [3.37537, -3.75399, 24.30517], \u0026#39;cameraUp\u0026#39;: [0, -1, 0], \u0026#39;sharedMemoryForWorkers\u0026#39;: false, \u0026#39;rootElement\u0026#39;: document.getElementById(\u0026#39;canvas-container\u0026#39;) }); const tencentCosUrl = \u0026#39;https://你的存储桶地址.myqcloud.com/classroom.splat\u0026#39;; viewer.addSplatScene(tencentCosUrl, { \u0026#39;showLoadingUI\u0026#39;: true }).then(() =\u0026gt; { viewer.start(); }).catch(err =\u0026gt; { alert(\u0026#34;加载模型失败：\u0026#34; + err); }); let moveInterval = null; let activeAction = null; const speed = 0.1; const rotSpeed = 0.05; function doMove() { if(!viewer.camera || !viewer.controls) return; const cam = viewer.camera; const target = viewer.controls.target; const dir = new THREE.Vector3(); cam.getWorldDirection(dir); const right = new THREE.Vector3().crossVectors(cam.up, dir).normalize(); switch(activeAction) { case \u0026#39;forward\u0026#39;: cam.position.addScaledVector(dir, speed); target.addScaledVector(dir, speed); break; case \u0026#39;backward\u0026#39;: cam.position.addScaledVector(dir, -speed); target.addScaledVector(dir, -speed); break; case \u0026#39;left\u0026#39;: cam.position.addScaledVector(right, speed); target.addScaledVector(right, speed); break; case \u0026#39;right\u0026#39;: cam.position.addScaledVector(right, -speed); target.addScaledVector(right, -speed); break; case \u0026#39;up\u0026#39;: cam.position.y += speed; target.y += speed; break; case \u0026#39;down\u0026#39;: cam.position.y -= speed; target.y -= speed; break; case \u0026#39;rotL\u0026#39;: case \u0026#39;rotR\u0026#39;: const angle = (activeAction === \u0026#39;rotL\u0026#39;) ? rotSpeed : -rotSpeed; const offset = target.clone().sub(cam.position); offset.applyAxisAngle(cam.up, angle); target.copy(cam.position).add(offset); break; } } document.querySelectorAll(\u0026#39;.btn\u0026#39;).forEach(btn =\u0026gt; { const startEvent = (e) =\u0026gt; { e.preventDefault(); activeAction = btn.getAttribute(\u0026#39;data-action\u0026#39;); if(!moveInterval) moveInterval = setInterval(doMove, 30); }; const stopEvent = (e) =\u0026gt; { e.preventDefault(); activeAction = null; if(moveInterval) { clearInterval(moveInterval); moveInterval = null; } }; btn.addEventListener(\u0026#39;mousedown\u0026#39;, startEvent); btn.addEventListener(\u0026#39;mouseup\u0026#39;, stopEvent); btn.addEventListener(\u0026#39;mouseleave\u0026#39;, stopEvent); btn.addEventListener(\u0026#39;touchstart\u0026#39;, startEvent, {passive: false}); btn.addEventListener(\u0026#39;touchend\u0026#39;, stopEvent, {passive: false}); }); \u0026lt;/script\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; 由于网站是可公开访问的，安全起见，需要对对象存储服务进行设置。\n进入当前存储桶。点击“安全管理” → “防盗链设置”。开启防盗链，类型选择白名单模式，拒绝空 Referer。Referer 填写模型将要展示的站点的域名。 点击“跨域访问 CORS 设置”，“来源”填写自己的真实生产域名，操作选择 “GET”。 在腾讯云顶栏右侧点击“费用”，进入“费用中心”页面后，点击费用预警并设置预警阈值。根据需求设置。 当然，上述设置很难作为真正的访问控制。攻击者可以通过伪造 Referer 请求绕过防盗链。更安全的策略应该是将桶改成私有读写，然后通过Cloudflare Workers 验证访问者是否允许查看页面，并生成短时有效的 COS 预签名 URL，有效期设置为数分钟。此外，需要对签发接口做 IP，会话和请求频率限制，或使用腾讯云 CDN 回源鉴权。不过，要将上述方案的完整实现全部写明，确实是不小的工程。行笔至此，全文已超万字，因此本文暂不展开实现细节，只列出可选的安全方案。\n最后，附上几张 3DGS 成品图片。\n","permalink":"https://easton.pub/posts/3dgs/","summary":"\u003cp\u003e高考结束后，三年的高中生活也随之落幕，此后再回到这间教室的机会大概不会很多。再次回到教室，环顾四周，想到就要与这个见证了高三拼搏的地方分别，终究有些不舍。于是，我萌生了将这间教室以数字形式保存下来的想法。我选择了近年兴起的三维高斯泼溅技术，使用实拍的三百六十多张照片，“重建”了这间教室。作为第一次 3DGS 实践，最终的渲染效果已令我颇为满意。因此，我写下这篇文章，记录并复盘整个项目流程。\u003c/p\u003e","title":"使用 3DGS 重建高中教室场景"},{"content":"NotebookLM 是由 Google 推出的，利用人工智能来帮助人们整理笔记的一款软件。它可以对用户上传到其中的文件，进行摘要、回答问题或者提取出重要的信息。最近 NotebookLM 新增了生成中文播客的功能，并受到很多人的喜爱。于是我就试着用了一下此功能，在读完赫尔曼·黑塞的小说《克林索尔的最后夏天》之后再来看看它的效果如何，并写下了这篇文章。\n创建笔记本并上传文件 访问 NotebookLM 官网 https://notebooklm.google.com/ 。登录，新建一个笔记本，并给它取个名字。\n在页面左侧的“来源”处有一个“+ 添加”的选项，点击后可以选择上传文件，并把和该笔记本有关的所有文件全部上传上去。\n上传完成后，NotebookLM 就会自动对文件进行处理。在很短的时间内，可以得到一份关于该文件内容的大致总结，以及一些被识别出来的主要观点、需要考虑的问题。这样可以使读者对于材料拥有初步的认识。\n对文档内容进行提问 另外也可以通过对模型提问的方式来对笔记做进一步的研究。NotebookLM 相当于一个带有知识库的 Gemini 模型，而这个知识库就是由用户上传到其中的各种文件所组成的。\n上面这张图是我在向 NotebookLM 提问时截屏下来的。当我问到有关书籍的问题的时候，NotebookLM 就会从文档中找到相关的资料，并且给出一个条理清晰的答案，在答案后面还会加上数字编号来表示该答案来源于原文中的什么地方，便于读者查阅核实。\n另外，NotebookLM 还可以生成其他的辅助学习的内容，如思维导图、常见问题列表、时间轴等。下面这张图就是我用 NotebookLM 生成的一张思维导图的一部分。\n生成中文播客 目前 NotebookLM 已经能够生成中文播客了。在“工作室”栏目下选择“生成”，就可以根据用户上传的文件来生成一段由 AI 主持人模拟对话的播客音频。需要注意的是要将语言设置为“中文（简体）”。由于播客生成的时间要比生成文字和图片长得多，所以请各位用户稍作等待。\n这是用NotebookLM生成的播客音频 ，它的风格、内容以及思想性都很符合小说的特点，值得一听。 NotebookLM是一款革命性的笔记软件，利用人工智能来处理和查询信息，使用户的学效率大大提高。这是一款很有前途的产品，也希望国内厂商能够尽快推出类似的产品！\n","permalink":"https://easton.pub/posts/try-notebooklm/","summary":"\u003cp\u003eNotebookLM 是由 Google 推出的，利用人工智能来帮助人们整理笔记的一款软件。它可以对用户上传到其中的文件，进行摘要、回答问题或者提取出重要的信息。最近 NotebookLM 新增了生成中文播客的功能，并受到很多人的喜爱。于是我就试着用了一下此功能，在读完赫尔曼·黑塞的小说《克林索尔的最后夏天》之后再来看看它的效果如何，并写下了这篇文章。\u003c/p\u003e","title":"Google NotebookLM：人工智能时代的新型学习工具"},{"content":"萌生开发一个英语读后续写模型的想法，是在去年11月末。当时偶然看到一篇介绍 RAG 原理的公众号推文，读罢灵光乍现，觉得一款轻量级模型在挂载知识库之后，或许能在特定领域与顶尖的大语言模型一较高下。一天晚上与舍友们谈论 AI ，突然想到了可以将 RAG 应用在英语读后续写的场景。我将这个想法提出，大家交口称赞，说这个项目贴合实际，很有潜力。由于年末的学习和备考压力，这个项目暂时搁置，直到今年一月期末考试结束后，我才有机会抽出时间，独立完成这个项目。\nRAG 通识 简单来说，RAG 技术实现了用户输入一个问题或者要求后，系统会先从向量数据库中找出与问题最相关的多个文本，再根据检索到的信息生成一段新的回答，使得生成的内容更贴近真实情境，有效避免 AI 模型的“幻觉”现象。\nRAG, (Retrieval Augmented Generation, 检索增强生成)，是一种结合了信息检索和文本生成的混合式模型，主要包含两个模块：\n检索模块 (Retrieval Module)：该模块从预先构建的知识库中搜索出与当前输入 (Query) 最相关的文本片段。这一步通常利用文本检索模型(such as 阿里的 text-embedding-v3 模型和 BGE-M3 模型)将文本转化为向量，然后通过相似度计算（例如余弦相似度，即求两向量的夹角余弦值）找到最匹配的内容。在数学上表示为： $$\r\\text{retrieve}(q) = \\{ d_1, d_2, \\ldots, d_k \\} \\quad \\text{使得} \\quad S(q, d_i) = \\cos\\big(f(q), f(d_i)\\big) \\text{最大}\r$$即输入 $q$ 个查询，返回 $k$ 个最相关的文档 ${d_1, d_2, \\ldots , d_k}$，使得表示查询 $q$ 与文档之间的相似度分数 $S(q, d_i)$ 最大。\n其中，$S(q, d_i)=cos(f(q), f(d_i))=\\frac{f(q) \\cdot f(d_i)}{\\|f(q)\\| \\|f(d_i)\\|}$\n这里的 $f(q)$ 和 $f(d_i)$ 表示查询和文档被转换成的高维向量。余弦相似度计算它们的夹角相似性，值越大表示越相似。\n生成模块 (Generation Module)：在获取检索结果后，将其与原始查询一起输入到生成模型中，以生成更加上下文相关和富有信息量的输出。公式为： $$\ry = \\arg\\max_{y'} P(y' \\mid q, \\{d_1, d_2, \\ldots, d_k\\}; \\theta)\r$$该公式表示在所有可能的输出文本 $y'$ 中，找到使得概率 $P(y' \\mid q, \\{ d_1, d_2, \\dots, d_k \\}; \\theta)$ 最大的 $y'$，作为最终的生成结果。\n其中，$y$ 表示模型的输出，$y'$ 表示所有候选输出文本，$\\theta$ 代表模型的参数，$P(y' \\mid q, \\{ d_1, d_2, \\dots, d_k \\}; \\theta)$ ：表示给定查询 $q$ 和检索到的文档集合后，生成模型输出 $y$ 的概率。\n通俗地讲，RAG 先用检索模型找到与 $q$ 相关的文档 $\\{ d_1, d_2, \\dots, d_k \\}$ ，然后基于这些文档，由生成模型计算生成不同候选答案的概率，并选择概率最高的答案为最优答案 $y$ 。\n模型实现 数据收集 好的数据集是模型成功的一半。\n——我说的\n一个模型的成功与否，通常取决于数据集是否具有较高的质量。没有高质量的数据，再精妙的算法也难以发挥出它的潜力。教师在课上通常使用 PPT 文档授课，我从这些高质量的教学课件中提取英文文本作为原始数据。整个过程包括两个主要步骤：\n1. 文件重命名 为了便于后续使用 Python 提取文本，我先按照修改时间顺序为当前文件夹下所有的 .pptx 文件重新命名。下面是一段 VBScript 代码示例，其主要功能是遍历所有 .pptx 文件并按修改时间排序，并将文件名改为形如 数字+\u0026quot;.pptx\u0026quot; 的格式。\n示例代码如下\r\u0026#39; 创建文件系统对象\rSet fso = CreateObject(\u0026#34;Scripting.FileSystemObject\u0026#34;)\r\u0026#39; 获取当前文件夹路径\rcurrentFolder = fso.GetAbsolutePathName(\u0026#34;.\u0026#34;)\r\u0026#39; 获取当前文件夹中的所有文件\rSet folder = fso.GetFolder(currentFolder)\rSet files = folder.Files\r\u0026#39; 存储所有的 .pptx 文件\rDim pptxFiles()\ri = 0\r\u0026#39; 遍历文件，筛选出所有 .pptx 文件\rFor Each file In files\rIf LCase(fso.GetExtensionName(file.Name)) = \u0026#34;pptx\u0026#34; Then\rReDim Preserve pptxFiles(i)\rSet pptxFiles(i) = file \u0026#39; 使用 Set 关键字赋值对象\ri = i + 1\rEnd If\rNext\rIf i = 0 Then\rWScript.Echo \u0026#34;无 .pptx 文件\u0026#34;\rWScript.Quit\rEnd If\r\u0026#39; 按照修改时间对文件排序\rFor i = 0 To UBound(pptxFiles) - 1\rFor j = i + 1 To UBound(pptxFiles)\rIf pptxFiles(i).DateLastModified \u0026gt; pptxFiles(j).DateLastModified Then\r\u0026#39; 交换文件对象\rSet temp = pptxFiles(i)\rSet pptxFiles(i) = pptxFiles(j)\rSet pptxFiles(j) = temp\rEnd If\rNext\rNext\r\u0026#39; 重命名文件\rFor i = 0 To UBound(pptxFiles)\rnewName = (i + 1) \u0026amp; \u0026#34;.pptx\u0026#34;\rfso.MoveFile pptxFiles(i).Path, currentFolder \u0026amp; \u0026#34;\\\u0026#34; \u0026amp; newName\rNext\rWScript.Echo \u0026#34;文件重命名完成！\u0026#34; 2. 文本提取 接下来，利用 Python 中的 pptx 库提取每个 PPT 文件中所有英文文本。首先遍历当前目录下重命名后的所有 .pptx 文件，按数字顺序排序，然后读取幻灯片中的所有文本，并通过正则表达式过滤出纯英文字符和标点，最后将提取结果保存到 extract.txt 文件中：\n示例代码如下\rimport os import re from pptx import Presentation pptx_folder = \u0026#39;./\u0026#39; output_txt = \u0026#39;extract.txt\u0026#39; # 匹配英文字符和标点符号 english_text_pattern = re.compile(r\u0026#39;[A-Za-z0-9.,!?()\u0026#34;;: -]+\u0026#39;) all_english_text = [] # 获取当前目录下所有符合格式的文件 pptx_files = [f for f in os.listdir(pptx_folder) if f.endswith(\u0026#39;.pptx\u0026#39;) and f[:-5].isdigit()] # 按照数字顺序排序文件名 pptx_files.sort(key=lambda x: int(x[:-5])) # 遍历每个 PPT 文件 for pptx_filename in pptx_files: pptx_path = os.path.join(pptx_folder, pptx_filename) presentation = Presentation(pptx_path) # 遍历每一张幻灯片的每个形状 for slide in presentation.slides: for shape in slide.shapes: # 确保该形状包含文本框 if hasattr(shape, \u0026#39;text\u0026#39;): # 获取文本内容 text = shape.text.strip() # 如果文本内容非空，只提取英文文本 if text: # 用正则表达式提取文本中的英文部分 extracted_english_text = \u0026#39; \u0026#39;.join(english_text_pattern.findall(text)) if extracted_english_text: # 如果有英文文本 all_english_text.append(extracted_english_text) # 如果文本为空，添加空行 else: all_english_text.append(\u0026#39;\u0026#39;) # 将提取的英文文本写入到 extract.txt 文件 with open(output_txt, \u0026#39;w\u0026#39;, encoding=\u0026#39;utf-8\u0026#39;) as f: for line in all_english_text: f.write(line + \u0026#39;\\n\u0026#39;) print(f\u0026#34;提取完成，所有英文文本已保存到 {output_txt}\u0026#34;) 数据预处理 提取到的原始数据存在拼写错误、标点混用、空格不规范、大小写问题等噪音数据。我选择使用 kimi.ai 对收集到的数据进行清洗、去重、分词等预处理操作，以便后续的模型训练。\n示例 Prompt 这是一个我制作的英语读后续写语料数据集，Kimi，我需要你完成数据清洗任务，请按照以下要求逐步检查并修正数据中的错误： ### 需要检查和修正的错误类型包括但不限于： 1. 单词拼写错误：检查是否有拼写错误，并按标准英语拼写修正 2. 中英文符号混用：（确保全英文内容使用英文符号，全中文内容使用中文符号） 3. 空格问题：缺少空格（如单词间或标点后未正确添加空格）；多余空格（如连续多个空格和不必要的前后空格） 4. 大小写错误：句首单词应大写，专有名词应遵循正确大小写规则 5. 标点符号错误：检查逗号、句号、引号、括号等是否正确使用，是否有标点符号重复、缺失或误用 6. 文本格式不规范：是否存在行首/行尾多余空格，是否有异常换行（如单个句子被拆成多行） 7. 特殊字符和乱码：检查是否有不符合语料格式的特殊字符，移除或修正乱码 ### 任务执行前确认 在正式执行任务之前，请回答以下问题： 1. 你是否完全理解本次任务的要求和操作流程？ 2. 你是否有任何疑问或需要进一步澄清的地方？ 在 Kimi 清洗数据结束后，还需要加以辅助性的人工标注，确保数据集的准确性。\n模型训练 使用 RAG 技术，将预处理后的数据作为输入，训练一个英语读后续写模型。\n我使用了一个在 GitHub 上开源的 RAG 框架，FastGPT 构建知识库 首先，将预处理后的数据集上传至知识库，并利用索引模型（例如阿里的 text-embedding-v3）将文本转化为向量表示，实现语义检索。数学上，我们希望通过一个映射函数 $ f: \\text{文本} \\rightarrow \\mathbb{R}^d $ ，使得语义上相似的文本在向量空间中距离更近。相似度计算常用余弦相似度，其公式为：\n$$\r\\text{sim}(a, b) = \\frac{f(a) \\cdot f(b)}{\\|f(a)\\| \\|f(b)\\|}\r$$其中，$a$ 与 $b$ 分别为两段文本。\n创建工作流 接下来，创建一个工作流来实现以下流程：\n知识库检索：在接收到用户输入或测试用例后，首先从知识库中检索出与输入最相关的文本片段。 文本生成：将检索到的结果作为上下文，与用户输入共同传入文本生成模型（如 DeepSeek V3），通过预先写过的的 Prompt 生成写作内容。 生成模型的目标是最大化生成文本 $y$ 的条件概率：\n$$\ry = \\arg\\max_{y'} P\\big(y' \\mid q, \\{d_1, d_2, \\ldots, d_k\\}; \\theta\\big)\r$$参数解释见前文，这里对公式含义不赘述。\n借助 FastGPT 框架，部署过程与模型训练均按照其文档步骤完成。主要流程包括：\n数据上传与向量索引构建 定义工作流、对接检索模块与生成模块 调整模型参数（例如生成模型中的温度参数） 关于温度参数，简单说明：温度用于控制生成模型输出的随机性。温度越高，生成的文本越随机；反之，则更加确定。在本项目中，为了防止基于 RAG 技术的模型过拟合，建议将温度调至最大温度的 50%～70%。\n模型优化 模型训练完成后，通过自定义的测试集对模型效果进行评估。主要评估指标包括生成文本的连贯性、语义相关性以及创新性。若发现效果不佳，可通过优化 Prompt 或调整模型一些参数来改善结果。这一过程也可在 FastGPT 控制台中灵活实现。\n模型局限 这个模型公开发布后，受到广泛赞誉，英语老师甚至向我询问了原理，并说这是一种教学新思路。不过，坦率地说，这个模型仍存在较大的局限性：\n数据集局限：样本量很少。在教师上课的课件中，只选取了16份 PPT 文档用于模型训练。数据量和数据质量有待提高，模型的泛化能力有待加强。\n过拟合风险：训练数据少时，RAG 模型会过拟合。之前朋友说，做 LoRA 微调时，数据量在上千个，模型训练超过5个 epoch 很容易过拟合。我的策略是通过调高生成温度缓解这一问题，但仍需要更丰富和多样化的数据来提升模型鲁棒性。\nGitHub 仓库：https://github.com/Crosssense-Lab/Kelly-s1 （数据集因版权原因不开源）\n","permalink":"https://easton.pub/posts/rag-write-after-read/","summary":"\u003cp\u003e萌生开发一个英语读后续写模型的想法，是在去年11月末。当时偶然看到一篇介绍 RAG 原理的公众号推文，读罢灵光乍现，觉得一款轻量级模型在挂载知识库之后，或许能在特定领域与顶尖的大语言模型一较高下。一天晚上与舍友们谈论 AI ，突然想到了可以将 RAG 应用在英语读后续写的场景。我将这个想法提出，大家交口称赞，说这个项目贴合实际，很有潜力。由于年末的学习和备考压力，这个项目暂时搁置，直到今年一月期末考试结束后，我才有机会抽出时间，独立完成这个项目。\u003c/p\u003e","title":"基于 RAG 技术的英语读后续写模型开发实践"},{"content":"2025年过去十二分之一的时间后，我终于抽出空闲时间，草草动笔写这篇题为所谓“年终总结”的文章。过去一年比2023年还要艰难，我也像当年一样踌躇满志，也曾屡败屡战，也曾独自在黑夜里摸索，也曾在黑暗的夜空下看见星子在眨着眼。到头来，只想以一句“兴尽悲来，识盈虚之有数”作结。\n代码记录 2024年，我在 GitHub 上贡献了112次，较2023年有所增多。2024年，我提交了一些灵光乍现时分写下的 UI，以及一些浸润了 ChatGPT 智慧的代码。除此之外，便是提交本博客源代码到 GitHub 啦。\n2023年，将 Typecho 的主题换了又换；2024年，趁着新域名的注册，最终将博客系统换成了更加易用的 Hugo。参考了大量资料，终于赶在开学前完成了博客的部署和数据迁移。我的博客系统很可能长期使用 Hugo。如果今年有较多空余时间，我希望了解 Hugo 主题开发流程，开发一个最适合本博客的主题。\n做一个心中有诗的人 一月下旬，我和几名本校的学生代表，参加了“鲲鹏青少年科幻文学奖”颁奖典礼。刚进会场时，距离典礼开始还有一段时间。一群不谙世事的小学生在我们前面吵吵嚷嚷；主办方在大屏幕上无休止地滚动播放宣传视频。声音聒噪，心烦意乱。\n一直到颁奖典礼结束后，获奖得主发表获奖感言，现场气氛才稍微轻松愉悦一些。记得一名获奖得主慷慨陈词道：科幻文学是对科学与人文的深度思考，一部伟大的科幻作品能够指引未来的方向。我想，一部耐人寻味的科幻作品需要具备“双重视角”，在远观未来的同时植根现实。它既要用“望远镜”凝视星辰，又需要用“显微镜”解剖人性；既需要仰望星空的想象力，又必须扎根现实的洞察力。正如一名获奖得主在他的作品中展现的创作理念：优秀的科幻从不在纯粹幻想中沉溺，而是将量子物理与人文哲思融合成思考现实的利器。\n年近岁除之时，年级选出六名代表参加区作文比赛，我有幸成为其中一员。开始比赛，看到高中组题目“《我的AI朋友》”，我错愕。写议论文，显然不可；写说明文，介绍 ChatGPT 的原理、谈谈 DeepSeek 的训练、再扯到 Transformer ……评委看不懂，更不可；写记叙文，难道要我写使用体验？难以出彩，还是不可。这不可那不可，思来想去都不可，最后应付了一篇蹩脚的科幻小说提交。\n回程已是傍晚，仰头方见月初黄。问友班的一名同学，感觉怎么样？她回答，此前蛮喜欢文学的，但花两个半小时写一篇文章，实在是太疲惫了。\n之后是原路返回学校。窗外是十一月的风，扬尘在天上漫无目的地扩散。丁达尔效应起了作用，夕阳下的车流形成了一条黄色的光带，有点马尔克斯《流光似水》中的意境了——这篇文章曾出现在我们的语文练习中。刚写完作文不久，头脑昏昏沉沉的，思绪是发散的，心中似乎五味杂陈，有点淡淡的愁绪。\n忽然想起《死亡诗社》中的一句台词：“毫无信仰的人群川流不息，城市充斥着愚昧。生活在其中有什么意义？自我？生命？答案是因为你的存在，无可替代的存在。”\n感觉生活在一个高度发达的信息时代，我们都被生活磨出了“毛边”。就像新买了一件衣服，再小心地穿总会磨损；曾经表现得多么锋芒毕露，终究抵不过发自内心的一声“力有未逮”；那些曾经以为触手可及的理想，似乎早已变成了遥不可及的“诗和远方”……我们从未如此真切地认识到自己和同龄人的差距、过去与现在的差距、理想和现实的差距……我们的思想更加成熟、我们的视野更加长远、我们有时会更加愤世嫉俗、更加自暴自弃——我们明明知道走在一条必经之路，但每走一步都疼得真切。我们这代人像是被丢进滚筒洗衣机的毛衣，在高中学业的转速、大考排名的漂洗剂、六小时睡眠的烘干程序里反复翻滚，最后变得起球变形，却还要在第二天清早熨烫平整，假装自己是橱窗里的最新款。\n哪怕我们承认自己屡战屡败，但我们不能丢下屡败屡战的信心；哪怕我们对钢筋水泥搭建出的囚笼深恶痛疾，也不妨碍我们先改变自己，充盈自己的生命。\n此前看同学的公众号上的一篇文章，有一张插图，上面写着“我们不用做诗人，但要做心中有诗的人”。\n现代人强调“心中有诗”，本质上是在对抗一种集体性的感官麻木。我观察过在地铁中通勤的人们：相似的亮屏的手机、相似的疲惫表情，像被统一格式化的数据包在光纤里传输。这时候若有人留心到车窗玻璃外的自己的倒影，或是出神地看着一本实体书，那一刻的状态就是心中最本真的诗意。这种能力不是风花雪月般附庸风雅，而是防止自己“异化”为资本的附庸的防御机制。\n暑假前，一名学姐以青年作家身份返校宣讲。宣讲结束后，我跑到台前，先“世俗”地要了一张她的签名，再询问她一些有关文学创作的疑问。谈话中，她向我推荐了诗人王计兵的诗集。他的诗作是从现实生活中拔节孕穗的，心怀理想，立足现实，有现实主义的色彩。记得高中开学初期遇到一个隔壁班的同学，他将父辈收藏的郭沫若诗集带来学校，于闲暇时间品味；去年在长沙研学时写游记，无意中在B站发现一位传播诗词绝学的“宝藏”UP主……我意识到大多数人的心中还是充盈着诗意的。当然，真正的诗意并不能仅仅等同于会写、会读、会鉴赏诗词曲赋，更体现在对抗现代物质主义社会的一种生存美学重构。\n我们这代人面对的真实困境，是连“放空”都变成了需要学习的技能。各大平台教人所谓冥想、正念、精神疗愈，反而把休憩变成了新的KPI。心中有诗恰好可以允许自己“无目的”——哪怕不产出任何可量化的价值，也可以让时间有了质地。\n未来已来 2017年，当谷歌的研究员设计出 Transformer 时，他们或许难以想象这个模型将成为未来通用人工智能(AGI)的始祖。2022年，在我第一次了解美国一家名为 \u0026ldquo;OpenAI\u0026rdquo; 的公司开发了一款聊天模型后，我还在为找不到合适的接码平台注册 ChatGPT 账号而发愁。\n2024年，一众AI生视频工具涌现，Qwen 2.5、DeepSeek 模型大杀四方。我在自己班级的电脑上也通过 Ollama 部署了上述模型，用于在非联网时段检索信息、查询资料。\n记得初中时候，遇到疑难，大多选择找身边的同学、老师求教，或是百度、谷歌；现在再遇到疑惑，询问豆包、 ChatGPT 即可在短时间内获得较为准确的答案。过去原始且通用的知识传递和思维训练过程已经在 AI 的洪流中被冲刷得一干二净。在打开某个人工智能对话网页之后，我们的思维方式和认知路径已经悄然改变。\n据此，有人可能会担心人工智能会使人们失业、会扩大贫富差距、会束缚住人们的思维。这种担忧是比较合理的，但是我们切勿将它扩大化。人工智能是一种好用的工具，“君子使物，不为物使”，人工智能并不可怕，可怕的是不会独立思考，可怕的是常识的匮乏。向 DeepSeek r1 抛出一个复杂的问题，不到一分钟时间就可以梳理出理论框架，而自己真正深入分析时，或许用进废退，已经快要失去深度思考的能力。当我们把“寻找答案”外包给AI时，或许也把“构建问题”的能力悄悄抛诸脑后。\n但换个角度看，AI其实打破了认知的传统边界。我们可以突破时间和空间限制，随时随地获得海量新知识。面对这项完全改变世界的新技术，关键是要保持清醒：把AI当“搜索引擎 Pro ”而不是“大脑外挂”。对于人类，我们不能用 AI 替代思考，而是通过对 AI 的质疑、追问，激发更深层的认知。\nAI 会取代人类的一些工作吗？会，但是大部分职业并不会因为 AI 的高效能而将人类抛弃。技术日益精进，但最优质的创作愈发需要超越工具理性。当AI打破了技术壁垒，人类的创造力反而被推向更为核心的领域：对存在体验的细腻体悟，对认知混沌的包容能力，以及在工具束缚中仍能保持的原始的反叛精神。或许，未来的创作者将是那些能够驾驭 AI 却刻意留下个人风格，善用算法却执意创造惊喜的人。最初尝试 Stable Diffusion, MidJourney, Pika 与 Suno AI 时，我曾好奇，未来是否人人都能成为数字艺术家。但很快，我便意识到，自己缺乏“创作灵感”。即便欣赏过众多他人的创作，我也时常不明确自己想要创造何种作品。因此，即便未来的 AI 拥有极其强大的绘画与音乐创作能力，很可能仅有少数人能真正成为画家和音乐家。同理，即便 AI 能够编程，也并不意味着人人都能成为程序员。\n追往事，叹今吾 2024年，读到了辛弃疾的这首《鹧鸪天》，恰好能概括我这一年的心境。\n当年二十三岁的辛弃疾率五十骑直闯金营，缚叛将张安国于马上，星夜南奔献俘建康。这段传奇经历在四十多年后凝成《鹧鸪天》中的“壮岁旌旗拥万夫”，但此时的辛弃疾已从少年横槊的将军变成了白发种树的老翁。当主和派重掌朝纲时，曾经的主战派灵魂人物却写下了最沉痛的悲歌。\n高中已经过去一半时间了。在高中的第一学年，彻底意识到高中生活与初中“田园诗”般的生活是迥乎不同的。在初中，自谓心胸坦荡、才情出众，然而时光匆匆，年少轻狂转瞬成为过往，让人感慨光阴的流逝之快。高中的自己一心追求上进。尽管身负才华与理想，但行动上往往仓促匆忙。单从考试成绩的排名上看，自己离很早之前既定的目标有些差距。事出有因，例如因为词汇量的缺乏，我的英语成绩落后班上其他同学不少，曾经不好好学英语时埋下的雷终于在一年之后被狠狠地引爆了。现在看来，“路漫漫其修远兮，吾将上下而求索”！\n进入高中一年以来，我也参加了不少集体活动，甚至高中公众号上也有几篇提到了我的推文。我喜欢以乐观的心态面对生活，希望未来的我能替现在的我走出一条明朗的路。只可惜，年少的时候总觉得广阔天地大有可为，直到在日复一日的琐碎、挫败、趋同中，我们慢慢失去对新鲜事物的好奇，我们越来越渴望舒适、我们失去骄傲，也失去了曾经愤世嫉俗时，血液里喷薄的激情、勇敢和生命力。\n总之，高中的第一年，我也踌躇满志，也曾屡败屡战，也曾独自在黑夜里摸索，也曾在黑暗的夜空下看见星子在眨着眼。除此之外，我仍有美好的回忆和美丽的遗憾，像傍晚教学楼上的明月一样忽隐忽现，或是像宿舍阳台外的春光一样在我心中偶尔泛起涟漪。有了这样的经历，我不得不承认，这个自己选择的高中确实塑造和改变了我，高中确实对我产生了各种影响——于是，我就不能不说，我在这里的三年不可能是没有意义的，这里的意义已经藏在我自己的故事里了，或许，故事本身就是意义。\n在人生新的一页上，我会写下这些文字：\n“你的生活归根到底是你的。你要去独立、去经历、去自己打倒自己、不等别人打败你。你要亲手搭建属于自己的巢穴，又要有勇气在必要时将其付之一炬。因为真正的成长，往往始于一场自我革命。要像凤凰般高傲——非梧桐不栖，非醴泉不饮。学会拥抱那些让你痛苦的重生之火，因为它们既是毁灭者，也是孕育者。在烈焰中，你将找到最真实的自己。”\n也罢，我心光明，亦复何言？我是国家最有生命力的青年一代，就算时运不济，就算虎啸猿啼，天生我青年当化碧血为剑。愿得此身长报国，何须生入玉门关。\n最后：新年快乐！\n","permalink":"https://easton.pub/posts/a-look-back-at-2024/","summary":"\u003cp\u003e2025年过去十二分之一的时间后，我终于抽出空闲时间，草草动笔写这篇题为所谓“年终总结”的文章。过去一年比2023年还要艰难，我也像当年一样踌躇满志，也曾屡败屡战，也曾独自在黑夜里摸索，也曾在黑暗的夜空下看见星子在眨着眼。到头来，只想以一句“兴尽悲来，识盈虚之有数”作结。\u003c/p\u003e","title":"二零二五，揆文奋武"},{"content":"此前，我的博客使用的图床基于 GitHub 上的 \u0026ldquo;Telegraph-Image\u0026rdquo; 项目，图片上传到 Telegraph 。去年年末， Telegraph API 接口被官方关闭，导致新的图片无法上传。在查阅不少搭建图床的教程后，我最终选择使用 Backblaze B2 私密桶作为图床，通过 Cloudflare Workers 的 Application Key 实现认证访问 Backblaze B2 私密桶，并缓存图片。\n创建 Backblaze 存储桶 首先进入 Backblaze 官网 ，创建账户。验证账户信息后进入首页，点击\u0026quot;创建一个桶\u0026quot;。\n创建一个新的存储桶，权限选择 \u0026ldquo;Private\u0026rdquo;，其他选项保持默认。\n私密桶创建完成后，记下 Endpoint 的值，以后要用到。\n点击右侧 \u0026ldquo;Lifecycle Settings\u0026rdquo; ，选择 \u0026ldquo;Keep only the last version of the file\u0026rdquo; ，点击 \u0026ldquo;Update Bucket\u0026rdquo; 保存。\n点击页面左侧菜单 \u0026ldquo;Account\u0026rdquo; 下的 \u0026ldquo;Application Keys\u0026rdquo; ，点击 \u0026ldquo;Add a New Application Key\u0026rdquo; ，在 \u0026ldquo;Name of Key\u0026rdquo; 项填写应用程序密钥名称，其余保持默认，点击 \u0026ldquo;Create New Key\u0026rdquo;。创建完成之后，记下 Key ID 和 applicationKey。关键信息不会显示第二次，请务必保存好。\n创建 Cloudflare Workers 登录 Cloudflare ，点击 \u0026ldquo;Compute(Workers)\u0026rdquo; 中的 \u0026ldquo;Workers 和 Pages \u0026ldquo;，点击 \u0026ldquo;创建\u0026rdquo; ，填写 Workers 名称，其余保持默认，点击 \u0026ldquo;部署\u0026rdquo;。\n点击“编辑代码”，将以下代码复制到代码框中，点击 “保存并部署”。\n点击查看代码\r// node_modules/aws4fetch/dist/aws4fetch.esm.mjs var encoder = new TextEncoder(); var HOST_SERVICES = { appstream2: \u0026#34;appstream\u0026#34;, cloudhsmv2: \u0026#34;cloudhsm\u0026#34;, email: \u0026#34;ses\u0026#34;, marketplace: \u0026#34;aws-marketplace\u0026#34;, mobile: \u0026#34;AWSMobileHubService\u0026#34;, pinpoint: \u0026#34;mobiletargeting\u0026#34;, queue: \u0026#34;sqs\u0026#34;, \u0026#34;git-codecommit\u0026#34;: \u0026#34;codecommit\u0026#34;, \u0026#34;mturk-requester-sandbox\u0026#34;: \u0026#34;mturk-requester\u0026#34;, \u0026#34;personalize-runtime\u0026#34;: \u0026#34;personalize\u0026#34; }; var UNSIGNABLE_HEADERS = /* @__PURE__ */ new Set([ \u0026#34;authorization\u0026#34;, \u0026#34;content-type\u0026#34;, \u0026#34;content-length\u0026#34;, \u0026#34;user-agent\u0026#34;, \u0026#34;presigned-expires\u0026#34;, \u0026#34;expect\u0026#34;, \u0026#34;x-amzn-trace-id\u0026#34;, \u0026#34;range\u0026#34;, \u0026#34;connection\u0026#34; ]); var AwsClient = class { constructor({ accesskeyID, secretAccessKey, sessionToken, service, region, cache, retries, initRetryMs }) { if (accesskeyID == null) throw new TypeError(\u0026#34;accesskeyID is a required option\u0026#34;); if (secretAccessKey == null) throw new TypeError(\u0026#34;secretAccessKey is a required option\u0026#34;); this.accesskeyID = accesskeyID; this.secretAccessKey = secretAccessKey; this.sessionToken = sessionToken; this.service = service; this.region = region; this.cache = cache || /* @__PURE__ */ new Map(); this.retries = retries != null ? retries : 10; this.initRetryMs = initRetryMs || 50; } async sign(input, init) { if (input instanceof Request) { const { method, url, headers, body } = input; init = Object.assign({ method, url, headers }, init); if (init.body == null \u0026amp;\u0026amp; headers.has(\u0026#34;Content-Type\u0026#34;)) { init.body = body != null \u0026amp;\u0026amp; headers.has(\u0026#34;X-Amz-Content-Sha256\u0026#34;) ? body : await input.clone().arrayBuffer(); } input = url; } const signer = new AwsV4Signer(Object.assign({ url: input }, init, this, init \u0026amp;\u0026amp; init.aws)); const signed = Object.assign({}, init, await signer.sign()); delete signed.aws; try { return new Request(signed.url.toString(), signed); } catch (e) { if (e instanceof TypeError) { return new Request(signed.url.toString(), Object.assign({ duplex: \u0026#34;half\u0026#34; }, signed)); } throw e; } } async fetch(input, init) { for (let i = 0; i \u0026lt;= this.retries; i++) { const fetched = fetch(await this.sign(input, init)); if (i === this.retries) { return fetched; } const res = await fetched; if (res.status \u0026lt; 500 \u0026amp;\u0026amp; res.status !== 429) { return res; } await new Promise((resolve) =\u0026gt; setTimeout(resolve, Math.random() * this.initRetryMs * Math.pow(2, i))); } throw new Error(\u0026#34;An unknown error occurred, ensure retries is not negative\u0026#34;); } }; var AwsV4Signer = class { constructor({ method, url, headers, body, accesskeyID, secretAccessKey, sessionToken, service, region, cache, datetime, signQuery, appendSessionToken, allHeaders, singleEncode }) { if (url == null) throw new TypeError(\u0026#34;url is a required option\u0026#34;); if (accesskeyID == null) throw new TypeError(\u0026#34;accesskeyID is a required option\u0026#34;); if (secretAccessKey == null) throw new TypeError(\u0026#34;secretAccessKey is a required option\u0026#34;); this.method = method || (body ? \u0026#34;POST\u0026#34; : \u0026#34;GET\u0026#34;); this.url = new URL(url); this.headers = new Headers(headers || {}); this.body = body; this.accesskeyID = accesskeyID; this.secretAccessKey = secretAccessKey; this.sessionToken = sessionToken; let guessedService, guessedRegion; if (!service || !region) { [guessedService, guessedRegion] = guessServiceRegion(this.url, this.headers); } this.service = service || guessedService || \u0026#34;\u0026#34;; this.region = region || guessedRegion || \u0026#34;us-east-1\u0026#34;; this.cache = cache || /* @__PURE__ */ new Map(); this.datetime = datetime || (/* @__PURE__ */ new Date()).toISOString().replace(/[:-]|\\.\\d{3}/g, \u0026#34;\u0026#34;); this.signQuery = signQuery; this.appendSessionToken = appendSessionToken || this.service === \u0026#34;iotdevicegateway\u0026#34;; this.headers.delete(\u0026#34;Host\u0026#34;); if (this.service === \u0026#34;s3\u0026#34; \u0026amp;\u0026amp; !this.signQuery \u0026amp;\u0026amp; !this.headers.has(\u0026#34;X-Amz-Content-Sha256\u0026#34;)) { this.headers.set(\u0026#34;X-Amz-Content-Sha256\u0026#34;, \u0026#34;UNSIGNED-PAYLOAD\u0026#34;); } const params = this.signQuery ? this.url.searchParams : this.headers; params.set(\u0026#34;X-Amz-Date\u0026#34;, this.datetime); if (this.sessionToken \u0026amp;\u0026amp; !this.appendSessionToken) { params.set(\u0026#34;X-Amz-Security-Token\u0026#34;, this.sessionToken); } this.signableHeaders = [\u0026#34;host\u0026#34;, ...this.headers.keys()].filter((header) =\u0026gt; allHeaders || !UNSIGNABLE_HEADERS.has(header)).sort(); this.signedHeaders = this.signableHeaders.join(\u0026#34;;\u0026#34;); this.canonicalHeaders = this.signableHeaders.map((header) =\u0026gt; header + \u0026#34;:\u0026#34; + (header === \u0026#34;host\u0026#34; ? this.url.host : (this.headers.get(header) || \u0026#34;\u0026#34;).replace(/\\s+/g, \u0026#34; \u0026#34;))).join(\u0026#34;\\n\u0026#34;); this.credentialString = [this.datetime.slice(0, 8), this.region, this.service, \u0026#34;aws4_request\u0026#34;].join(\u0026#34;/\u0026#34;); if (this.signQuery) { if (this.service === \u0026#34;s3\u0026#34; \u0026amp;\u0026amp; !params.has(\u0026#34;X-Amz-Expires\u0026#34;)) { params.set(\u0026#34;X-Amz-Expires\u0026#34;, \u0026#34;86400\u0026#34;); } params.set(\u0026#34;X-Amz-Algorithm\u0026#34;, \u0026#34;AWS4-HMAC-SHA256\u0026#34;); params.set(\u0026#34;X-Amz-Credential\u0026#34;, this.accesskeyID + \u0026#34;/\u0026#34; + this.credentialString); params.set(\u0026#34;X-Amz-SignedHeaders\u0026#34;, this.signedHeaders); } if (this.service === \u0026#34;s3\u0026#34;) { try { this.encodedPath = decodeURIComponent(this.url.pathname.replace(/\\+/g, \u0026#34; \u0026#34;)); } catch (e) { this.encodedPath = this.url.pathname; } } else { this.encodedPath = this.url.pathname.replace(/\\/+/g, \u0026#34;/\u0026#34;); } if (!singleEncode) { this.encodedPath = encodeURIComponent(this.encodedPath).replace(/%2F/g, \u0026#34;/\u0026#34;); } this.encodedPath = encodeRfc3986(this.encodedPath); const seenKeys = /* @__PURE__ */ new Set(); this.encodedSearch = [...this.url.searchParams].filter(([k]) =\u0026gt; { if (!k) return false; if (this.service === \u0026#34;s3\u0026#34;) { if (seenKeys.has(k)) return false; seenKeys.add(k); } return true; }).map((pair) =\u0026gt; pair.map((p) =\u0026gt; encodeRfc3986(encodeURIComponent(p)))).sort(([k1, v1], [k2, v2]) =\u0026gt; k1 \u0026lt; k2 ? -1 : k1 \u0026gt; k2 ? 1 : v1 \u0026lt; v2 ? -1 : v1 \u0026gt; v2 ? 1 : 0).map((pair) =\u0026gt; pair.join(\u0026#34;=\u0026#34;)).join(\u0026#34;\u0026amp;\u0026#34;); } async sign() { if (this.signQuery) { this.url.searchParams.set(\u0026#34;X-Amz-Signature\u0026#34;, await this.signature()); if (this.sessionToken \u0026amp;\u0026amp; this.appendSessionToken) { this.url.searchParams.set(\u0026#34;X-Amz-Security-Token\u0026#34;, this.sessionToken); } } else { this.headers.set(\u0026#34;Authorization\u0026#34;, await this.authHeader()); } return { method: this.method, url: this.url, headers: this.headers, body: this.body }; } async authHeader() { return [ \u0026#34;AWS4-HMAC-SHA256 Credential=\u0026#34; + this.accesskeyID + \u0026#34;/\u0026#34; + this.credentialString, \u0026#34;SignedHeaders=\u0026#34; + this.signedHeaders, \u0026#34;Signature=\u0026#34; + await this.signature() ].join(\u0026#34;, \u0026#34;); } async signature() { const date = this.datetime.slice(0, 8); const cacheKey = [this.secretAccessKey, date, this.region, this.service].join(); let kCredentials = this.cache.get(cacheKey); if (!kCredentials) { const kDate = await hmac(\u0026#34;AWS4\u0026#34; + this.secretAccessKey, date); const kRegion = await hmac(kDate, this.region); const kService = await hmac(kRegion, this.service); kCredentials = await hmac(kService, \u0026#34;aws4_request\u0026#34;); this.cache.set(cacheKey, kCredentials); } return buf2hex(await hmac(kCredentials, await this.stringToSign())); } async stringToSign() { return [ \u0026#34;AWS4-HMAC-SHA256\u0026#34;, this.datetime, this.credentialString, buf2hex(await hash(await this.canonicalString())) ].join(\u0026#34;\\n\u0026#34;); } async canonicalString() { return [ this.method.toUpperCase(), this.encodedPath, this.encodedSearch, this.canonicalHeaders + \u0026#34;\\n\u0026#34;, this.signedHeaders, await this.hexBodyHash() ].join(\u0026#34;\\n\u0026#34;); } async hexBodyHash() { let hashHeader = this.headers.get(\u0026#34;X-Amz-Content-Sha256\u0026#34;) || (this.service === \u0026#34;s3\u0026#34; \u0026amp;\u0026amp; this.signQuery ? \u0026#34;UNSIGNED-PAYLOAD\u0026#34; : null); if (hashHeader == null) { if (this.body \u0026amp;\u0026amp; typeof this.body !== \u0026#34;string\u0026#34; \u0026amp;\u0026amp; !(\u0026#34;byteLength\u0026#34; in this.body)) { throw new Error(\u0026#34;body must be a string, ArrayBuffer or ArrayBufferView, unless you include the X-Amz-Content-Sha256 header\u0026#34;); } hashHeader = buf2hex(await hash(this.body || \u0026#34;\u0026#34;)); } return hashHeader; } }; async function hmac(key, string) { const cryptoKey = await crypto.subtle.importKey( \u0026#34;raw\u0026#34;, typeof key === \u0026#34;string\u0026#34; ? encoder.encode(key) : key, { name: \u0026#34;HMAC\u0026#34;, hash: { name: \u0026#34;SHA-256\u0026#34; } }, false, [\u0026#34;sign\u0026#34;] ); return crypto.subtle.sign(\u0026#34;HMAC\u0026#34;, cryptoKey, encoder.encode(string)); } async function hash(content) { return crypto.subtle.digest(\u0026#34;SHA-256\u0026#34;, typeof content === \u0026#34;string\u0026#34; ? encoder.encode(content) : content); } function buf2hex(buffer) { return Array.prototype.map.call(new Uint8Array(buffer), (x) =\u0026gt; (\u0026#34;0\u0026#34; + x.toString(16)).slice(-2)).join(\u0026#34;\u0026#34;); } function encodeRfc3986(urlEncodedStr) { return urlEncodedStr.replace(/[!\u0026#39;()*]/g, (c) =\u0026gt; \u0026#34;%\u0026#34; + c.charCodeAt(0).toString(16).toUpperCase()); } function guessServiceRegion(url, headers) { const { hostname, pathname } = url; if (hostname.endsWith(\u0026#34;.r2.cloudflarestorage.com\u0026#34;)) { return [\u0026#34;s3\u0026#34;, \u0026#34;auto\u0026#34;]; } if (hostname.endsWith(\u0026#34;.backblazeb2.com\u0026#34;)) { const match2 = hostname.match(/^(?:[^.]+\\.)?s3\\.([^.]+)\\.backblazeb2\\.com$/); return match2 != null ? [\u0026#34;s3\u0026#34;, match2[1]] : [\u0026#34;\u0026#34;, \u0026#34;\u0026#34;]; } const match = hostname.replace(\u0026#34;dualstack.\u0026#34;, \u0026#34;\u0026#34;).match(/([^.]+)\\.(?:([^.]*)\\.)?amazonaws\\.com(?:\\.cn)?$/); let [service, region] = (match || [\u0026#34;\u0026#34;, \u0026#34;\u0026#34;]).slice(1, 3); if (region === \u0026#34;us-gov\u0026#34;) { region = \u0026#34;us-gov-west-1\u0026#34;; } else if (region === \u0026#34;s3\u0026#34; || region === \u0026#34;s3-accelerate\u0026#34;) { region = \u0026#34;us-east-1\u0026#34;; service = \u0026#34;s3\u0026#34;; } else if (service === \u0026#34;iot\u0026#34;) { if (hostname.startsWith(\u0026#34;iot.\u0026#34;)) { service = \u0026#34;execute-api\u0026#34;; } else if (hostname.startsWith(\u0026#34;data.jobs.iot.\u0026#34;)) { service = \u0026#34;iot-jobs-data\u0026#34;; } else { service = pathname === \u0026#34;/mqtt\u0026#34; ? \u0026#34;iotdevicegateway\u0026#34; : \u0026#34;iotdata\u0026#34;; } } else if (service === \u0026#34;autoscaling\u0026#34;) { const targetPrefix = (headers.get(\u0026#34;X-Amz-Target\u0026#34;) || \u0026#34;\u0026#34;).split(\u0026#34;.\u0026#34;)[0]; if (targetPrefix === \u0026#34;AnyScaleFrontendService\u0026#34;) { service = \u0026#34;application-autoscaling\u0026#34;; } else if (targetPrefix === \u0026#34;AnyScaleScalingPlannerFrontendService\u0026#34;) { service = \u0026#34;autoscaling-plans\u0026#34;; } } else if (region == null \u0026amp;\u0026amp; service.startsWith(\u0026#34;s3-\u0026#34;)) { region = service.slice(3).replace(/^fips-|^external-1/, \u0026#34;\u0026#34;); service = \u0026#34;s3\u0026#34;; } else if (service.endsWith(\u0026#34;-fips\u0026#34;)) { service = service.slice(0, -5); } else if (region \u0026amp;\u0026amp; /-\\d$/.test(service) \u0026amp;\u0026amp; !/-\\d$/.test(region)) { [service, region] = [region, service]; } return [HOST_SERVICES[service] || service, region]; } // index.js var UNSIGNABLE_HEADERS2 = [ // These headers appear in the request, but are not passed upstream \u0026#34;x-forwarded-proto\u0026#34;, \u0026#34;x-real-ip\u0026#34;, // We can\u0026#39;t include accept-encoding in the signature because Cloudflare // sets the incoming accept-encoding header to \u0026#34;gzip, br\u0026#34;, then modifies // the outgoing request to set accept-encoding to \u0026#34;gzip\u0026#34;. // Not cool, Cloudflare! \u0026#34;accept-encoding\u0026#34; ]; var HTTPS_PROTOCOL = \u0026#34;https:\u0026#34;; var HTTPS_PORT = \u0026#34;443\u0026#34;; var RANGE_RETRY_ATTEMPTS = 3; function filterHeaders(headers, env) { return new Headers(Array.from(headers.entries()).filter( (pair) =\u0026gt; !UNSIGNABLE_HEADERS2.includes(pair[0]) \u0026amp;\u0026amp; !pair[0].startsWith(\u0026#34;cf-\u0026#34;) \u0026amp;\u0026amp; !(\u0026#34;ALLOWED_HEADERS\u0026#34; in env \u0026amp;\u0026amp; !env.ALLOWED_HEADERS.includes(pair[0])) )); } var my_proxy_default = { async fetch(request, env) { if (![\u0026#34;GET\u0026#34;, \u0026#34;HEAD\u0026#34;].includes(request.method)) { return new Response(null, { status: 405, statusText: \u0026#34;Method Not Allowed\u0026#34; }); } const url = new URL(request.url); url.protocol = HTTPS_PROTOCOL; url.port = HTTPS_PORT; let path = url.pathname.replace(/^\\//, \u0026#34;\u0026#34;); path = path.replace(/\\/$/, \u0026#34;\u0026#34;); const pathSegments = path.split(\u0026#34;/\u0026#34;); if (env.ALLOW_LIST_BUCKET !== \u0026#34;true\u0026#34;) { if (env.BUCKET_NAME === \u0026#34;$path\u0026#34; \u0026amp;\u0026amp; pathSegments.length \u0026lt; 2 || env.BUCKET_NAME !== \u0026#34;$path\u0026#34; \u0026amp;\u0026amp; path.length === 0) { return new Response(null, { status: 404, statusText: \u0026#34;Not Found\u0026#34; }); } } switch (env.BUCKET_NAME) { case \u0026#34;$path\u0026#34;: url.hostname = env.B2_ENDPOINT; break; case \u0026#34;$host\u0026#34;: url.hostname = url.hostname.split(\u0026#34;.\u0026#34;)[0] + \u0026#34;.\u0026#34; + env.B2_ENDPOINT; break; default: url.hostname = env.BUCKET_NAME + \u0026#34;.\u0026#34; + env.B2_ENDPOINT; break; } const headers = filterHeaders(request.headers, env); const endpointRegex = /^s3\\.([a-zA-Z0-9-]+)\\.backblazeb2\\.com$/; const [, aws_region] = env.B2_ENDPOINT.match(endpointRegex); const client = new AwsClient({ \u0026#34;accesskeyID\u0026#34;: env.B2_APPLICATION_KEY_ID, \u0026#34;secretAccessKey\u0026#34;: env.B2_APPLICATION_KEY, \u0026#34;service\u0026#34;: \u0026#34;s3\u0026#34;, \u0026#34;region\u0026#34;: aws_region }); const signedRequest = await client.sign(url.toString(), { method: request.method, headers }); if (signedRequest.headers.has(\u0026#34;range\u0026#34;)) { let attempts = RANGE_RETRY_ATTEMPTS; let response; do { let controller = new AbortController(); response = await fetch(signedRequest.url, { method: signedRequest.method, headers: signedRequest.headers, signal: controller.signal }); if (response.headers.has(\u0026#34;content-range\u0026#34;)) { if (attempts \u0026lt; RANGE_RETRY_ATTEMPTS) { console.log(`Retry for ${signedRequest.url} succeeded - response has content-range header`); } break; } else if (response.ok) { attempts -= 1; console.error(`Range header in request for ${signedRequest.url} but no content-range header in response. Will retry ${attempts} more times`); if (attempts \u0026gt; 0) { controller.abort(); } } else { break; } } while (attempts \u0026gt; 0); if (attempts \u0026lt;= 0) { console.error(`Tried range request for ${signedRequest.url} ${RANGE_RETRY_ATTEMPTS} times, but no content-range in response.`); } return response; } return fetch(signedRequest); } }; export { my_proxy_default as default }; /*! Bundled license information: aws4fetch/dist/aws4fetch.esm.mjs: (** * @license MIT \u0026lt;https://opensource.org/licenses/MIT\u0026gt; * @copyright Michael Hart 2022 *) */ //# sourceMappingURL=index.js.map 然后返回 Workers 页面，点击“设置”，点击“变量和机密”，点击“添加”，添加以下变量：\nALLOW_LIST_BUCKET = false B2_APPLICATION_KEY = 此前生成的 applicationKey B2_APPLICATION_KEY_ID = 此前生成的 keyID B2_ENDPOINT = Endpoint 值（如 s3.us-west-004.backblazeb2.com） BUCKET_NAME = 你的私密桶名 需要注意的是，为了安全考虑，需要将 B2_APPLICATION_KEY 和 B2_APPLICATION_KEY_ID 变量的类型设置为“密钥”，此值在保存后无法再进行查看。\n接着，点击“保存并部署”按钮，完成部署。\n点击“域和路由”，添加自定义域名。\n回到 Backblaze B2 私密桶，点击 \u0026ldquo;Bucket Settings\u0026rdquo; ，在 \u0026ldquo;Bucket Info\u0026rdquo; 项填入：\n{\u0026#34;Cache-Control\u0026#34;: \u0026#34;public, max-age=604800\u0026#34;} 其中，604800 代表缓存时间为一周，单位为秒，可根据实际情况修改。\n随后在私密桶页面点击 \u0026ldquo;Upload\u0026rdquo; 按钮，上传图片，测试上传情况。若直接访问 S3 url，无法看到图片，但访问自定义域名，图片可以正常显示，则说明 Cloudflare Workers 配置成功。如图\n使用 PicGo 上传图片 PicGo 是一款开源的图床客户端，支持多种图床，包括七牛云、腾讯云、又拍云、GitHub、SM.MS、阿里云、Imgur、自定义图床等。\n首先下载并安装 PicGo ，在 PicGo 中，点击“插件配置”，搜索“s3”并安装。在“PicGo设置”中，启用“Amazon S3”。\n在“图床设置”中，点击“Amazon S3”,依照仓库中的说明 填写相关信息，点击确定。\n点击左侧“上传区”，上传图片。上传完成后点击“相册”，可以查看图片。\n参考：Deliver Private Backblaze B2 Content Through Cloudflare CDN ","permalink":"https://easton.pub/posts/image-hosting/","summary":"\u003cp\u003e此前，我的博客使用的图床基于 GitHub 上的 \u0026ldquo;Telegraph-Image\u0026rdquo; 项目，图片上传到 Telegraph 。去年年末， Telegraph API 接口被官方关闭，导致新的图片无法上传。在查阅不少搭建图床的教程后，我最终选择使用 Backblaze B2 私密桶作为图床，通过 Cloudflare Workers 的 Application Key 实现认证访问 Backblaze B2 私密桶，并缓存图片。\u003c/p\u003e","title":"图床搭建教程：Backblaze B2 + Cloudflare Workers + PicGo"},{"content":"我在部署本网站时，使用了 Vercel 作为前端托管服务，并将域名解析交给了 Cloudflare。初步配置完成后，满怀期待地访问了自己的网站，结果迎面而来的却是浏览器的报错提示：ERR_TOO_MANY_REDIRECTS\n我非常困惑，仅仅将域名绑定到 Vercel 并在 Cloudflare 上做了域名解析，且并未做任何复杂的配置，何来的重定向次数过多？在网上搜集了一些资料，总结起来，这个问题是由于 Cloudflare 在“灵活”模式下通过 HTTP 与 Vercel 通信，而 Vercel 会将所有 HTTP 请求重定向到 HTTPS，导致重定向循环而造成的。\n从 SSL/TLS 配置谈起 Cloudflare 提供了几种 SSL/TLS 的配置模式，其中最常见的模式包括“灵活”（Flexible）和“完全”（Full）。我经历的报错正是因为 SSL/TLS 设置不当引起的。以下是这几种模式主要特点：\n灵活模式（Flexible）： 在这种模式下，Cloudflare 与客户端的连接是通过 HTTPS（SSL 加密）完成的，但是 Cloudflare 与源服务器（例如 Vercel）之间的连接却是使用 HTTP。这意味着从用户访问网站到 Cloudflare 是加密的，但从 Cloudflare 到 Vercel 是未加密的。 完全模式（Full）： Cloudflare 与客户端以及 Cloudflare 与源服务器之间都使用 HTTPS 进行通信，但是并不验证源服务器的 SSL 证书是否合法。 完全（严格）模式（Full (Strict)）： 同样使用 HTTPS，但要求 Cloudflare 验证源服务器的 SSL 证书是有效的、可信的。 在部署网站时，默认情况下 Cloudflare 的 SSL/TLS 设置可能是“灵活”。这意味着，当客户端通过 HTTPS 访问网站时，Cloudflare 会将请求解密后以 HTTP 的形式传递给 Vercel。而 Vercel 会自动将所有的 HTTP 请求重定向到 HTTPS。这一来一回便产生了一个问题：Vercel 将请求重定向到 HTTPS，而 Cloudflare 接收到 HTTPS 请求后再次转发，形成了一个自我循环。这时，浏览器会检测到这一过多的重定向，并抛出 ERR_TOO_MANY_REDIRECTS 错误。 报错原因 当 Vercel 收到 HTTP 请求时，它会返回 308 状态码，告知客户端应将请求重定向到 HTTPS URL。HTTP 308 是一种永久性重定向，意味着客户端必须使用 HTTPS 协议进行后续请求。\n当客户端（即用户的浏览器）最初通过 HTTPS 访问 https://brume.top 时，Cloudflare 在灵活模式下将请求以 HTTP 方式传给 Vercel。Vercel 接收到 HTTP 请求后，按照其默认行为，发出 308 永久重定向，强制要求将请求重定向至 HTTPS 版本的相同 URL。Cloudflare 将此 HTTPS 重定向信息再次发送给客户端，而客户端早已处于 HTTPS 状态，因此这个重定向实际上是无效的。浏览器一再收到相同的重定向指令，形成了一个闭环，导致浏览器认为重定向次数过多，最终返回 ERR_TOO_MANY_REDIRECTS 错误。下面是我使用 ChatGPT 生成的一张示意图，展示了这个过程：\n此外，对于这个所谓的“灵活模式”，从安全性角度看，它虽然表面上为用户提供了 HTTPS 访问的体验，但在 Cloudflare 和源服务器之间的通信仍然是明文的 HTTP，这使得数据在传输过程中存在被窃听或篡改的风险。因此，严格意义上讲，“灵活模式”并不是一个安全的方案。\n解决方案 要解决这个问题，只需更改 Cloudflare 的 SSL/TLS 配置，将其从“灵活”改为“完全（严格）”，确保 Cloudflare 与 Vercel 之间的通信使用的是 HTTPS 协议即可。 此时，Cloudflare 将强制会以 HTTPS 方式与 Vercel 通信，避免了重定向循环。\n","permalink":"https://easton.pub/posts/solution-for-err-too-many-redirects-error/","summary":"\u003cp\u003e我在部署本网站时，使用了 Vercel 作为前端托管服务，并将域名解析交给了 Cloudflare。初步配置完成后，满怀期待地访问了自己的网站，结果迎面而来的却是浏览器的报错提示：\u003ccode\u003eERR_TOO_MANY_REDIRECTS\u003c/code\u003e\u003c/p\u003e","title":"解决 Vercel + Cloudflare 部署中的 ERR_TOO_MANY_REDIRECTS 错误"},{"content":"本文将介绍在本地搭建 Hugo 并通过 GitHub Pages 和 Vercel 部署 Hugo 的方法。以下所叙之方法就是我在部署本站点时所使用的，具有借鉴意义。如果在部署过程中遇到报错或其他困难，可以上网搜索或在评论区留言——我看到后会提供力所能及的建议。\n在部署 Hugo 之前，需要进行的准备：\n安装 Git. Git 用于版本控制和代码管理。Linux 和 MacOS 系统中内置 Git ，而 Windows 用户需要手动安装。下载地址: https://git-scm.com/ 。安装过程中不需要选择任何多余选项，一直点下一步即可。 拥有一个 GitHub 账号，用于托管代码文件。注册地址: https://GitHub.com/login 拥有一个称手的文本编辑器：推荐使用 Visual Studio Code、Sublime Text 或者你喜欢的编辑器。 网络环境良好。为了避免下载文件或上传文件至 GitHub 过程中出现错误，建议科学上网。 如果你做好了如下准备，那么恭喜你，你可以进行下一步啦！\n下载并安装 Hugo 在 Hugo 的 Release 页面找到最新版本的 Hugo ，选择相应版本下载。 此外，还可以通过 Hugo 官网上的指示 下载。\n创建站点 将下载后的内容放在一个空文件夹中。在当前文件夹上方地址栏输入 cmd ，唤起命令行。在命令行中输入 hugo new site 你的站点名称 ，创建站点。 输入 cd 你的站点名称 切换目录，并将 hugo.exe 复制到这个文件夹中，以避免配置环境变量。 用命令行在当前目录下输入 hugo server 启动本地站点(通过 Ctrl + C 关闭这一服务)。地址通常是 localhost:1313 。如果页面显示“Page not found”，说明此前的所有配置都是正常无误的。\n配置主题 Hugo 默认是没有主题的，需要到 官网 去下载主题。我使用的主题是 Jimmy Cai 创作的 Stack 主题。接下来的部分内容会以此主题为例。\n将主题下载完成后并解压至 themes 文件夹中，将 exampleSite 文件夹中的 content 和hugo.yaml 复制到主文件夹中，并删掉原来的 hugo.toml 和 Content/post/rich-content ，避免出现不兼容的错误。（图片上的的“春树暮云”是本博客名） 修改 hugo.yaml 中的 theme 选项，将其修改为与主题文件夹同名。 再次在命令行输入 hugo server启动服务，若此时能看见类似下图的样式，说明此前操作无误。 在 config.yaml 中输入相关配置。文件各项配置解释如下，用作参考：\nbaseurl: https://example.com # 网站的基本 URL 。替换为你自己的网站域名。 languageCode: en-us # 网站的默认语言代码，zh-cn 指中文简体。 theme: hugo-theme-stack # 使用的 Hugo 主题，这里是 Stack 主题。 paginate: 3 # 每页显示的内容数量，通常用于分页设置。 title: Example Site # 网站的标题，会显示在浏览器标签上。 copyright: Example Person # 网站的版权信息，通常显示在页面底部。 # Theme i18n support # Available values: ar, bn, ca, de, el, en, es, fr, hu, id, it, ja, ko, nl, pt-br, th, uk, zh-cn, zh-hk, zh-tw DefaultContentLanguage: en # 设置网站的默认内容语言。可选值见上注释。 # Set hasCJKLanguage to true if DefaultContentLanguage is in [zh-cn ja ko] # This will make .Summary and .WordCount behave correctly for CJK languages. hasCJKLanguage: false # 如果默认语言是中文、日文或韩文，设置为 true 以确保摘要和字数统计正确。 languages: en: languageName: English # 英语语言配置 title: Example Site # 英文站点标题 weight: 1 # 语言权重，数值越小排序越靠前 params: description: Example description # 英文站点描述 zh-cn: languageName: 中文 # 中文语言配置 title: 演示站点 # 中文站点标题 weight: 2 # 中文站点语言权重 params: description: 演示说明 # 中文站点描述 ar: languageName: عربي # 阿拉伯语配置 languagedirection: rtl # 文字方向，从右到左 title: موقع تجريبي # 阿拉伯语站点标题 weight: 3 # 阿拉伯语站点语言权重 params: description: وصف تجريبي # 阿拉伯语站点描述 services: # Change it to your Disqus shortname before using disqus: shortname: \u0026#34;hugo-theme-stack\u0026#34; # Disqus 评论系统的短名称，需替换为你自己的 Disqus 短名称。 # GA Tracking ID googleAnalytics: id: # Google Analytics 追踪 ID，用于网站流量统计。 permalinks: post: /p/:slug/ # 博客文章的永久链接格式，使用文章的 slug 作为路径。 page: /:slug/ # 页面内容的永久链接格式。 params: mainSections: - post # 主内容区域，显示文章内容。 featuredImageField: image # 特色图片字段的名称。 rssFullContent: true # RSS 提要是否包含全文内容。 favicon: # e.g.: favicon placed in `static/favicon.ico` of your site folder, then set this field to `/favicon.ico` (`/` is necessary) # 网站的favicon路径，例如`/favicon.ico`。 footer: since: 2020 # 网站创建年份，通常显示在页脚。 customText: # 自定义页脚文本。 dateFormat: published: Jan 02, 2006 # 发布日期格式。 lastUpdated: Jan 02, 2006 15:04 MST # 最后更新日期格式。 sidebar: emoji: 🍥 # 侧边栏标题旁显示的 emoji。 subtitle: Lorem ipsum dolor sit amet, consectetur adipiscing elit. # 侧边栏的副标题。 avatar: enabled: true # 是否启用头像显示。 local: true # 是否使用本地头像。 src: img/avatar.png # 头像图片路径。 article: math: false # 是否支持数学公式渲染。 toc: true # 是否显示文章目录。 readingTime: true # 是否显示预计阅读时间。 license: enabled: true # 是否启用文章版权信息。 default: Licensed under CC BY-NC-SA 4.0 # 默认版权协议。 comments: enabled: true # 是否启用评论功能。 provider: disqus # 选择的评论提供商，默认为 Disqus。 disqusjs: shortname: # DisqusJS 的短名称。 apiUrl: # DisqusJS 的 API URL。 apiKey: # DisqusJS 的 API Key。 admin: # DisqusJS 的管理员用户名。 adminLabel: # DisqusJS 管理员标签。 utterances: repo: # Utterances 评论系统的 GitHub 仓库地址。 issueTerm: pathname # Utterances 评论系统的议题关联方式，使用页面路径。 label: # Utterances 评论系统的标签。 beaudar: repo: # Beaudar 评论系统的 GitHub 仓库地址。 issueTerm: pathname # Beaudar 评论系统的议题关联方式。 label: # Beaudar 评论系统的标签。 theme: # Beaudar 评论系统的主题。 remark42: host: # Remark42 的主机地址。 site: # Remark42 的站点标识符。 locale: # Remark42 的语言设置。 vssue: platform: # Vssue 使用的平台（例如 GitHub）。 owner: # Vssue 评论仓库的所有者。 repo: # Vssue 评论的 GitHub 仓库地址。 clientId: # Vssue 的 OAuth 应用 Client ID。 clientSecret: # Vssue 的 OAuth 应用 Client Secret。 autoCreateIssue: false # 是否自动创建评论议题。 waline: serverURL: # Waline 评论系统的服务器 URL。 lang: # Waline 的语言设置。 pageview: # 是否启用页面浏览统计。 emoji: # Waline 的 Emoji 表情包地址。 - https://unpkg.com/@waline/emojis@1.0.1/weibo requiredMeta: - name # 评论时需要填写的字段，用户名。 - email # 评论时需要填写的字段，电子邮件地址。 - url # 评论时需要填写的字段，网址。 locale: admin: Admin # Waline 评论系统的管理员名称。 placeholder: # Waline 评论框的占位符文本。 twikoo: envId: # Twikoo 评论系统的环境 ID。 region: # Twikoo 评论系统的部署区域。 path: # Twikoo 评论系统的路径。 lang: # Twikoo 评论系统的语言设置。 cactus: defaultHomeserverUrl: \u0026#34;https://matrix.cactus.chat:8448\u0026#34; # Cactus.Chat 的默认主服务器 URL。 serverName: \u0026#34;cactus.chat\u0026#34; # Cactus.Chat 的服务器名称。 siteName: \u0026#34;\u0026#34; # You must insert a unique identifier here matching the one you registered (See https://cactus.chat/docs/getting-started/quick-start/#register-your-site) # Cactus.Chat 的站点名称，需与注册的标识符匹配。 giscus: repo: # Giscus 评论系统的 GitHub 仓库地址。 repoID: # Giscus 仓库的唯一标识符。 category: # Giscus 的分类名称。 categoryID: # Giscus 分类的唯一标识符。 mapping: # Giscus 的议题关联方式。 lightTheme: # Giscus 的浅色主题设置。 darkTheme: # Giscus 的深色主题设置。 reactionsEnabled: 1 # 是否启用 Giscus 的反应功能。 emitMetadata: 0 # 是否启用 Giscus 的元数据发射功能。 gitalk: owner: # Gitalk 评论系统的仓库所有者。 admin: # Gitalk 评论系统的管理员用户名。 repo: # Gitalk 评论的 GitHub 仓库地址。 clientID: # Gitalk 的 OAuth 应用 Client ID。 clientSecret: # Gitalk 的 OAuth 应用 Client Secret。 cusdis: host: # Cusdis 评论系统的主机地址。 id: # Cusdis 的站点标识符。 widgets: homepage: - type: search # 首页的小部件，搜索功能。 - type: archives # 首页的小部件，文章归档。 params: limit: 5 # 显示的归档文章数量。 - type: categories # 首页的小部件，文章分类。 params: limit: 10 # 显示的分类数量。 - type: tag-cloud # 首页的小部件，标签云。 params: limit: 10 # 显示的标签数量。 page: - type: toc # 页面的小部件，显示文章目录。 opengraph: twitter: # Your Twitter username site: # 你的 Twitter 用户名，将在 OpenGraph 元数据中使用。 # Available values: summary, summary_large_image card: summary_large_image # Twitter 卡片类型。可以选择 `summary` 或 `summary_large_image`，后者显示大图。 defaultImage: opengraph: enabled: false # 是否为没有特色图片的页面启用默认 OpenGraph 图片。 local: false # 是否使用本地图片作为 OpenGraph 图片。 src: # 默认 OpenGraph 图片的路径。 colorScheme: # Display toggle toggle: true # 是否在页面上显示颜色模式切换按钮。 # Available values: auto, light, dark default: auto # 默认的颜色模式。可以选择自动切换（auto），或固定为亮色（light）或暗色（dark）。 imageProcessing: cover: enabled: true # 是否为封面图片启用自动处理功能，例如裁剪、缩放等。 content: enabled: true # 是否为内容图片启用自动处理功能。 ### Custom menu ### See https://stack.jimmycai.com/config/menu ### To remove about, archive and search page menu item, remove `menu` field from their FrontMatter menu: main: [] # 自定义主菜单的配置，可以在这里添加导航链接。 social: - identifier: GitHub # 社交链接的标识符，通常用于指定图标。 name: GitHub # 链接的显示名称。 url: https://GitHub.com/CaiJimmy/hugo-theme-stack # GitHub 个人主页的链接。 params: icon: brand-GitHub # 使用的社交图标，这里是 GitHub 图标。 - identifier: twitter # 另一个社交链接配置，这里是 Twitter。 name: Twitter # Twitter 链接的显示名称。 url: https://twitter.com # Twitter 的链接。 params: icon: brand-twitter # 使用的社交图标，这里是 Twitter 图标。 related: includeNewer: true # 是否在相关文章中包含较新的文章。 threshold: 60 # 相关文章匹配的相似度阈值，范围是0到100。 toLower: false # 是否将标签和分类转换为小写。 indices: - name: tags # 使用标签作为相关文章的匹配依据。 weight: 100 # 标签匹配的权重值。 - name: categories # 使用分类作为相关文章的匹配依据。 weight: 200 # 分类匹配的权重值。 markup: goldmark: renderer: ## Set to true if you have HTML content inside Markdown unsafe: true # 如果 Markdown 中包含 HTML 内容，设置为 true 以允许渲染这些 HTML。 tableOfContents: endLevel: 4 # 目录生成时的最大标题级别。 ordered: true # 目录项是否使用有序列表。 startLevel: 2 # 目录生成时的起始标题级别。 highlight: noClasses: false # 语法高亮时是否禁用 CSS 类名。 codeFences: true # 是否启用代码块语法高亮。 guessSyntax: true # 是否自动猜测代码块的语言进行语法高亮。 lineNoStart: 1 # 代码行号的起始值。 lineNos: true # 是否在代码块中显示行号。 lineNumbersInTable: true # 是否在表格样式中显示行号。 tabWidth: 4 # 代码块中 Tab 的宽度（空格数）。 更多相关配置参见官网 ，例如网站字体配置、自定义页眉或页脚等。\n撰写文章 在 你的站点名称/content/post 文件夹下新建文件夹，在新建文件夹中创建 index.md 文件，就代表创建一篇新文章了。\n可以在 你的站点名称 页面打开并通过命令行创建新文章，命令如下： hugo new content post/新文章标题/index.md 之后通过 VS Code 或其他编辑器，用 markdown 语言写文章。\n在使用 hugo 命令生成的文章的最上面，都有一块被 +++ 或 --- 包裹出来的区域，它的官方名称是 \u0026ldquo;Front matter\u0026rdquo;， 用以指定文章的各项属性。下面是我在 stack 主题的一篇示例文章中摘取的 Front Matter 片段，并写出了注释：\n+++ author = \u0026#34;Hugo Authors\u0026#34; # 作者名称，用于标识文章的创作者。 title = \u0026#34;Markdown Syntax Guide\u0026#34; # 文章标题，将显示在页面和导航中。 date = \u0026#34;2019-03-11\u0026#34; # 文章的发布日期，用于排序和展示。 description = \u0026#34;Sample article showcasing basic Markdown syntax and formatting for HTML elements.\u0026#34; # 文章的简短描述，通常用于摘要或 SEO。 tags = [ \u0026#34;markdown\u0026#34;, # 文章的标签，用于分类和搜索。标签是灵活的，可以添加多个。 \u0026#34;css\u0026#34;, \u0026#34;html\u0026#34;, \u0026#34;themes\u0026#34;, ] categories = [ \u0026#34;themes\u0026#34;, # 文章的类别，用于组织和过滤内容。每篇文章可以属于一个或多个类别。 \u0026#34;syntax\u0026#34;, ] series = [\u0026#34;Themes Guide\u0026#34;] # 文章系列，通常用于将相关文章组织在一起，例如教程或主题指南系列。 aliases = [\u0026#34;migrate-from-jekyl\u0026#34;] # 别名，用于创建文章的旧路径重定向到新路径。例如，当迁移文章时使用。 image = \u0026#34;pawel-czerwinski-8uZPynIu-rQ-unsplash.jpg\u0026#34; # 文章的封面图片路径，用于展示文章时的视觉效果。 +++ Hugo 自带一个自动生成的 Front Matter 模板，但实际上，Hugo 支持的 Front Matter 字段远比默认模板提供的要丰富。以下是一些常用字段及其作用，你可以根据需要模仿原模板进行编写，或者编辑 archetypes 文件夹下的 default.md 文件，以自定义新的 Front Matter 模板。使用 hugo new 命令创建新文章时，Hugo 会根据 archetypes 目录下的模板自动生成 Front Matter。\n常用 Front Matter 字段说明 title ：文章标题。自动生成的标题与文件名保持一致。 date：文章创建日期。默认生成的日期为文件创建时的日期。 lastmod：文章最后更新日期。 description：文章简要描述，用于摘要或 SEO 优化。 categories：文章分类。 tags：文章标签。 image：封面图片。 draft：是否为草稿。如果设置为 true，Hugo 将不会编译该文件，文章也不会在页面中显示。 hidden：是否隐藏文章。与 draft 的区别在于，设置为 true 的文章仍然存在，只是不会在页面中显示，但可以通过正确的网址访问。 当然，并不是所有 Front Matter 属性在文章中都要出现。只要保证文章具有默认的属性，文章通常是不会出现 bugs 的。\n撰写页面、编辑 categories 和 tags 的方法与之基本相似，这里就不赘述了。\n部署博客 在博客内容撰写完毕以后，进入部署环节。\n同样的，Hugo 官方也提供了 部署 的相关教程。对于体积极小的静态页面来说，我不太认同单独将它配置在云服务器上。我推荐部署在 Vercel, Cloudflare Pages, GitHub Pages, Gitlab Pages, Netlify 等平台上。由于篇幅所限，我只写出部署至 GitHub Pages 并部署到 Vercel 的方法。\n我在部署之前，曾经在互联网上查找过部署在各大平台上的 Hugo 博客，并通过测速网站进行速度测试。对各个主题之间的差异忽略不计，结果如下：\n部署平台 中国最快/最慢时间 海外最快/最慢时间 中国/海外平均时间 GitHub Pages 内蒙古联通 0.304s / 湖北武汉电信 10.136s 德国法兰克福 0.018s / 南非 2.978s 1.829s / 0.541s Vercel 中国台湾 0.355s (大陆地区是浙江杭州移动 0.140s) / 黑龙江哈尔滨电信 3.088s 美国阿什本 0.050s / 美国洛杉矶② 1.602s 0.865s / 0.526s Cloudflare Pages 中国台湾 0.147s (大陆地区是上海移动 0.436s)/贵州贵阳移动 11.199s 美国阿什本 0.109s / 新加坡 2.133s 1.400s / 0.429s 我的网站(Vercel + GitHub Pages) 中国香港 0.054s (大陆地区是江苏苏州移动 0.196s)/ 福建厦门电信 7.119s 德国法兰克福 0.038s / 加拿大多伦多 0.732s 0.789s / 0.436s 因此，我推荐通过部署在 GitHub Pages 上，再通过 Vercel 的将 源文件部署，并绑定域名的方式。\n至于为什么是 GitHub Pages 而不是随便建一个 GitHub 仓库，因为包括官网在内的网上的大多数方法都基于 GitHub Pages 的。\n注意，如果要部署到 Vercel 上，一定要有一个自有域名。否则你必须每次都开全局代理访问你的博客\n但是 GitHub Pages 禁止百度抓取网页，对 SEO 不友好。所以建议再将 GitHub 仓库部署至 Vercel.\n部署至 GitHub Pages 新建一个 GitHub 仓库，仓库名为 你的名字.GitHub.io，权限设置为 public.\n前往 Setting/Pages/Branch 选择 main 分支，并保存。\n在本地的博客文件夹中执行 hugo 命令，会生成 Public 文件夹。这是将要上传至 GitHub 仓库的文件夹，包含这个博客的全部静态资源。\n接着输入以下命令，用于推送仓库至 GitHub Pages 上（若第一次上传，建议先提前输入账号密码，避免之后被绕得焦头烂额，方法见下下）：\ncd public/ # 进入 public 文件夹 git init # 初始化当前文件夹（此处就是 public 文件夹）为一个本地 git 仓库 git remote add origin git@GitHub.com:username/username.GitHub.io # 关联远程仓库。 # 远程仓库地址（git@GitHub.com:username/username.GitHub.io） # 也可以进入 GitHub 仓库-\u0026gt; Code -\u0026gt; SSH 复制远程仓库地址 git add . # 将当前文件夹的所有文件加入到暂存区 git commit -m \u0026#34;My first commit\u0026#34; # 把相关文件提交到本地仓库 git push origin main # 把相关文件推送到远程仓库的 main 分支 第一次 push 代码之前需要进行如下配置，之后 Git 会主动请求关联 GitHub 账户。\ngit config --global user.name \u0026#34;你的 GitHub 用户名\u0026#34; git config --global user.email \u0026#34;你注册 GitHub 的邮箱\u0026#34; 我建议在推送仓库的时候打开全局代理，不然 GitHub 时不时掉一下线，非常烦人！\n还有一种替代 SSH 的方法，即下载 GitHub Desktop，在客户端中进行代码的上传。\nPush 文件时经常会遇到报错，我建议仔细报错信息，看看是因为什么原因导致报错的，如果不清楚，可以问问 ChatGPT. 我曾经经历过的报错，大多都是找 GPT 帮忙，它会耐心地指出我的错误，并详细地告诉我解决方法。\n上传成功并等待部署完成后，可以访问 https://{username}.GitHub.io/ 检查无误后即可进行下一步操作。\n部署至 Vercel 并绑定域名 你需要拥有一个自己的域名才能部署至 Vercel. Vercel 分配的子域名在大陆无法访问。\n进入 Vercel 官网并登录，建议通过 GitHub 登录，以更方便地关联 GitHub 仓库。\n点击 “Add New” 再点击 “Project” 。在 “Import Git Repository” 中找到你上传到 GitHub 上的仓库。点击 “Import”. “Configure Project” 中的所有选项保持默认，点击 “Deploy”.等待大约十秒钟之后部署完成，会出现一个满屏彩带的页面。\nJust like this: 之后直奔主题，点击右侧的 “Add Domain”.进入 “Domain” 页面。 在搜索框内输入域名，选择合适的选项，并点击 “Add” ，按照要求添加 DNS 记录，等待 Vercel 验证通过并自动配置完 SSL 证书后即可访问。\n这里有个通常容易忽略的点，即在 Vercel 上部署的项目是强制 https 的，如果在 Cloudflare 或其他平台再次设置了强制 https，访问时会导致重定向次数过多，报出\u0026quot;ERR_TOO_MANY_REDIRECTS\u0026quot; 错误。我的解决方法是关闭 Cloudflare 上的相关配置，如果子域名需要强制 https，可以通过 .htaccess 文件实现。\n(可选) GitHub Action 自动部署 如果没有进行自动部署，博客也能正常访问，但是我们每次更新博客时，都需要在已经搭建好环境的电脑上进行、都要手动进行 Hugo 编译，再推送 Public 文件夹到 GitHub 仓库。如果想让整个写作+发布流程更加快捷，不妨试试自动部署。\n在 GitHub 上创建一个新的仓库,权限建议设置为“Private”.它将被用来用于存放 Hugo 的源文件，即目录为 “你的博客名称” 下的所有文件夹和文件。如下图。我自定义的仓库名为“Hugo” 前往本仓库的 Settings/Developer Settings/Personal access tokens , 点击 “Generate new token (classic)” 创建一个 token. \u0026ldquo;Expiration\u0026rdquo; 为 token 过期时间，可自定义。记得选择下方的 “repo” 和 “workflow” 选项。\n前往本仓库的 Settings/Secrets and variables/Actions 将生成的 Token 保存在仓库的环境变量中。 返回本地的 “你的博客名” 文件夹，在其中新建 “.GitHub” 文件夹，在该文件夹中创建 “workflows” 文件夹，在该文件夹中新建 “deploy.yaml” 文件。复制以下代码到该文件中：\nname: deploy # 代码提交到main分支时触发GitHub action on: push: branches: - main jobs: deploy: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 - name: Setup Hugo uses: peaceiris/actions-hugo@v3 with: hugo-version: \u0026#34;latest\u0026#34; extended: true - name: Build Web run: hugo -D - name: Deploy Web uses: peaceiris/actions-gh-pages@v4 with: PERSONAL_TOKEN: ${{ secrets.你的 Token 变量名 }} EXTERNAL_REPOSITORY: 用户名/仓库名 PUBLISH_BRANCH: main PUBLISH_DIR: ./public commit_message: auto deploy 在 “你的博客名” 文件夹中新建 “.gitignore” 文件，用来忽略提交不必要的文件或文件夹。在其中输入：\npublic resources .hugo_build.lock hugo.exe 将 Hugo 源文件上传至仓库\ngit init git add . git commit -m \u0026#34;注释\u0026#34; git branch -m main git remote add origin git@GitHub.com:用户名/仓库名.git git -u push origin main 提交完成之后前往 “Actions” 页面查看部署情况。下图是我发稿前的状况。 End 一切正常的话，就可以愉快地享用你的个人博客啦！当然，部署完成后还可以进行主题美化、评论区的配置等一系列繁杂的操作。不过，瞎折腾也不是没有意义的；或许，搭建博客本身就是意义。\n","permalink":"https://easton.pub/posts/build-blog-with-hugo/","summary":"\u003cp\u003e本文将介绍在本地搭建 Hugo 并通过 GitHub Pages 和 Vercel 部署 Hugo 的方法。以下所叙之方法就是我在部署本站点时所使用的，具有借鉴意义。如果在部署过程中遇到报错或其他困难，可以上网搜索或在评论区留言——我看到后会提供力所能及的建议。\u003c/p\u003e","title":"从零到一，用 Hugo 打造你的个人网站"},{"content":"Typecho 相比 WordPress，其所提供的接口较少，所以有些地方需要留心。遇到报错情况可以先在网上搜索相关资料，或者是询问 AI ，提出疑难。\n首页文章无限加载文章 在 functions.php 中加入一段判断 ajax 请求的方法： function is_ajax() { if (isset($_SERVER[\u0026#39;HTTP_X_REQUESTED_WITH\u0026#39;])) { if (\u0026#39;xmlhttprequest\u0026#39; == strtolower($_SERVER[\u0026#39;HTTP_X_REQUESTED_WITH\u0026#39;])) { return true; } } return false; } 在首页模板 index.php 中加入判断，if (is_ajax ()): 则不输出 header footer 等信息。 最后一步，在模板公共 js 文件中加入以下代码： var page = 1; var $body = (window.opera) ? (document.compatMode == \u0026#34;CSS1Compat\u0026#34; ? $(\u0026#39;html\u0026#39;) : $(\u0026#39;body\u0026#39;)) : $(\u0026#39;html,body\u0026#39;); $(\u0026#39;#blog_load_more\u0026#39;).click(function(event) { $(this).hide(); $(\u0026#39;#spinner\u0026#39;).show(); $.ajax({ type: \u0026#39;get\u0026#39;, url: SITE.default_url + \u0026#39;/page/\u0026#39; + parseInt(page + 1), success: function(data, textStatus, XMLHttpRequest) { page++; $(\u0026#39;.w-blog-list\u0026#39;).append(data); $(\u0026#39;#spinner\u0026#39;).hide(); $(\u0026#39;#blog_load_more\u0026#39;).show(); }, error: function(MLHttpRequest, textStatus, errorThrown) { $(\u0026#39;#spinner\u0026#39;).hide(); $.jGrowl(\u0026#39;Network Error\u0026#39;); } }); }); 其中 #blog_load_more 为需要绑定的加载更多按钮，#spinner 为加载过程中的动画，.w-blog-list 为首页文章列表容器。 由于 Typecho 并没有 Ajax 钩子函数，所以需要在 functions.php 中加入以下代码：\nfunction site_data() { $array = array( \u0026#39;site_url\u0026#39; =\u0026gt; Helper::options()-\u0026gt;siteUrl, \u0026#39;default_url\u0026#39; =\u0026gt; Helper::options()-\u0026gt;siteUrl . \u0026#39;index.php\u0026#39;, \u0026#39;theme_images_url\u0026#39; =\u0026gt; Helper::options()-\u0026gt;themeUrl . \u0026#39;/assets/images/\u0026#39;, ); echo json_encode($array); } 之后在 footer.php 中引入该数据：\n\u0026lt;script type=\u0026#34;text/javascript\u0026#34;\u0026gt; var SITE = \u0026lt;?php site_data()?\u0026gt;; \u0026lt;/script\u0026gt; 此处的 SITE 就是一个 JSON 对象，可以直接用了。\n自定义的过滤器函数 WordPress 的 add_filter 据信很好用，而 Typecho 也可以实现类似的功能： 比如想把文章中的图片都加上 rel=\u0026quot;fancybox\u0026quot; 的属性，WordPress 可能使用的以下方法：\nadd_filter(\u0026#39;the_content\u0026#39;, \u0026#39;fancybox_replace\u0026#39;); function fancybox_replace($content) { global $post; $pattern = \u0026#34;/\u0026lt;a(.*?)href=(\u0026#39;|\\\u0026#34;)([^\u0026gt;]*).(bmp|gif|jpeg|jpg|png)(\u0026#39;|\\\u0026#34;)(.*?)\u0026gt;(.*?)\u0026lt;\\/a\u0026gt;/i\u0026#34;; $replacement = \u0026#39;\u0026lt;a$1href=$2$3.$4$5 target=\u0026#34;_blank\u0026#34; rel=\u0026#34;fancybox\u0026#34;$6\u0026gt;$7\u0026lt;/a\u0026gt;\u0026#39;; $content = preg_replace($pattern, $replacement, $content); return $content; } 而 Typecho 中可以这么写：\nfunction themeInit($archive) { if ($archive-\u0026gt;is(\u0026#39;single\u0026#39;)) { $archive-\u0026gt;content = fancybox_replace($archive-\u0026gt;content); } } 获取文章第一张图片 分为 markdown 和 html 两种方式\nfunction get_post_img($archive) { $cid = $archive-\u0026gt;cid; $db = Typecho_Db::get(); $rs = $db-\u0026gt;fetchRow($db-\u0026gt;select(\u0026#39;table.contents.text\u0026#39;) -\u0026gt;from(\u0026#39;table.contents\u0026#39;) -\u0026gt;where(\u0026#39;cid=?\u0026#39;, $cid)); $text = $rs[\u0026#39;text\u0026#39;]; if (0 === strpos($text, \u0026#39;\u0026#39;)) { preg_match(\u0026#39;/!\\[[^\\]]*]\\([^\\)]*\\.(png|jpeg|jpg|gif|bmp)\\)/i\u0026#39;, $text, $img); if (empty($img)) { return \u0026#39;none\u0026#39;; } else { preg_match(\u0026#34;/(?:\\()(.*)(?:\\))/i\u0026#34;, $img[0], $result); $img_url = $result[1]; return \u0026#39;\u0026lt;img src=\u0026#34;\u0026#39; . $img_url . \u0026#39;\u0026#34;/\u0026gt;\u0026#39;; } } else { preg_match_all(\u0026#34;/\\\u0026lt;img.*?src\\=\\\u0026#34;(.*?)\\\u0026#34;[^\u0026gt;]*\u0026gt;/i\u0026#34;, $text, $img); if (empty($img)) { return \u0026#39;none\u0026#39;; } else { $img_url = $img[1][0]; return \u0026#39;\u0026lt;img src=\u0026#34;\u0026#39; . $img_url . \u0026#39;\u0026#34;/\u0026gt;\u0026#39;; } } } 评论列表加 @ function get_comment_at($coid) { $db = Typecho_Db::get(); $prow = $db-\u0026gt;fetchRow($db-\u0026gt;select(\u0026#39;parent\u0026#39;)-\u0026gt;from(\u0026#39;table.comments\u0026#39;) -\u0026gt;where(\u0026#39;coid = ? AND status = ?\u0026#39;, $coid, \u0026#39;approved\u0026#39;)); $parent = $prow[\u0026#39;parent\u0026#39;]; if ($parent != \u0026#34;0\u0026#34;) { $arow = $db-\u0026gt;fetchRow($db-\u0026gt;select(\u0026#39;author\u0026#39;)-\u0026gt;from(\u0026#39;table.comments\u0026#39;) -\u0026gt;where(\u0026#39;coid = ? AND status = ?\u0026#39;, $parent, \u0026#39;approved\u0026#39;)); $author = $arow[\u0026#39;author\u0026#39;]; $href = \u0026#39;\u0026lt;a href=\u0026#34;#comment-\u0026#39; . $parent . \u0026#39;\u0026#34;\u0026gt;@\u0026#39; . $author . \u0026#39;\u0026lt;/a\u0026gt;\u0026#39;; echo $href; } else { echo \u0026#39;\u0026#39;; } } 实现文章阅读次数统计（非插件） 在文章页面引入 get_post_view () 方法。\n//get_post_view($this) function get_post_view($archive) { $cid = $archive-\u0026gt;cid; $db = Typecho_Db::get(); $prefix = $db-\u0026gt;getPrefix(); if (!array_key_exists(\u0026#39;views\u0026#39;, $db-\u0026gt;fetchRow($db-\u0026gt;select()-\u0026gt;from(\u0026#39;table.contents\u0026#39;)))) { $db-\u0026gt;query(\u0026#39;ALTER TABLE `\u0026#39; . $prefix . \u0026#39;contents` ADD `views` INT(10) DEFAULT 0;\u0026#39;); echo 0; return; } $row = $db-\u0026gt;fetchRow($db-\u0026gt;select(\u0026#39;views\u0026#39;)-\u0026gt;from(\u0026#39;table.contents\u0026#39;)-\u0026gt;where(\u0026#39;cid = ?\u0026#39;, $cid)); if ($archive-\u0026gt;is(\u0026#39;single\u0026#39;)) { $db-\u0026gt;query($db-\u0026gt;update(\u0026#39;table.contents\u0026#39;)-\u0026gt;rows(array(\u0026#39;views\u0026#39; =\u0026gt; (int) $row[\u0026#39;views\u0026#39;] + 1))-\u0026gt;where(\u0026#39;cid = ?\u0026#39;, $cid)); } echo $row[\u0026#39;views\u0026#39;]; } 判断移动端访问 function is_mobile() { $user_agent = $_SERVER[\u0026#39;HTTP_USER_AGENT\u0026#39;]; $mobile_browser = array( \u0026#34;mqqbrowser\u0026#34;, //手机QQ浏览器 \u0026#34;opera mobi\u0026#34;, //手机opera \u0026#34;juc\u0026#34;, \u0026#34;iuc\u0026#34;, //uc浏览器 \u0026#34;fennec\u0026#34;, \u0026#34;ios\u0026#34;, \u0026#34;applewebKit/420\u0026#34;, \u0026#34;applewebkit/525\u0026#34;, \u0026#34;applewebkit/532\u0026#34;, \u0026#34;ipad\u0026#34;, \u0026#34;iphone\u0026#34;, \u0026#34;ipaq\u0026#34;, \u0026#34;ipod\u0026#34;, \u0026#34;iemobile\u0026#34;, \u0026#34;windows ce\u0026#34;, //windows phone \u0026#34;240x320\u0026#34;, \u0026#34;480x640\u0026#34;, \u0026#34;acer\u0026#34;, \u0026#34;android\u0026#34;, \u0026#34;anywhereyougo.com\u0026#34;, \u0026#34;asus\u0026#34;, \u0026#34;audio\u0026#34;, \u0026#34;blackberry\u0026#34;, \u0026#34;blazer\u0026#34;, \u0026#34;coolpad\u0026#34;, \u0026#34;dopod\u0026#34;, \u0026#34;etouch\u0026#34;, \u0026#34;hitachi\u0026#34;, \u0026#34;htc\u0026#34;, \u0026#34;huawei\u0026#34;, \u0026#34;jbrowser\u0026#34;, \u0026#34;lenovo\u0026#34;, \u0026#34;lg\u0026#34;, \u0026#34;lg-\u0026#34;, \u0026#34;lge-\u0026#34;, \u0026#34;lge\u0026#34;, \u0026#34;mobi\u0026#34;, \u0026#34;moto\u0026#34;, \u0026#34;nokia\u0026#34;, \u0026#34;phone\u0026#34;, \u0026#34;samsung\u0026#34;, \u0026#34;sony\u0026#34;, \u0026#34;symbian\u0026#34;, \u0026#34;tablet\u0026#34;, \u0026#34;tianyu\u0026#34;, \u0026#34;wap\u0026#34;, \u0026#34;xda\u0026#34;, \u0026#34;xde\u0026#34;, \u0026#34;zte\u0026#34;, ); $is_mobile = false; foreach ($mobile_browser as $device) { if (stristr($user_agent, $device)) { $is_mobile = true; break; } } return $is_mobile; } Ajax 评论 AjaxComments 有小 bug，不太好用，可以直接使用以下的修改版代码，在公共 js 中调用 ajaxComments() 方法即可\nfunction ajaxComment() { var selector = { commentMainFrame: \u0026#39;#comment\u0026#39;, commentList: \u0026#39;#commentlist\u0026#39;, commentNumText: \u0026#39;#comment h3\u0026#39;, commentReplyButton: \u0026#39;#comment span.reply\u0026#39;, submitForm: \u0026#39;#commentform\u0026#39;, submitTextarea: \u0026#39;#textarea\u0026#39;, submitButton: \u0026#39;#submit\u0026#39;, }; var parentId = \u0026#39;\u0026#39;; bindCommentReplyButton(); $(selector.submitTextarea).after(\u0026#39;\u0026lt;div style=\u0026#34;display:none;\u0026#34; id=\u0026#34;ajaxCommentMsg\u0026#34;\u0026gt;\u0026lt;\\/div\u0026gt;\u0026#39;); $msg = $(\u0026#39;#ajaxCommentMsg\u0026#39;); $(document).on(\u0026#39;submit\u0026#39;, selector.submitForm, function() { $msg.empty(); $(selector.submitButton).val(\u0026#39;正在发送\u0026#39;); $(selector.submitButton).attr(\u0026#39;disabled\u0026#39;, true).fadeTo(\u0026#39;slow\u0026#39;, 0.5); if ($(selector.submitForm).find(\u0026#39;#author\u0026#39;)[0]) { if ($(selector.submitForm).find(\u0026#39;#author\u0026#39;).val() == \u0026#39;\u0026#39;) { message(\u0026#39;昵称未填写\u0026#39;); enableCommentButton(); return false; } if ($(selector.submitForm).find(\u0026#39;#mail\u0026#39;).val() == \u0026#39;\u0026#39;) { message(\u0026#39;邮箱未填写\u0026#39;); enableCommentButton(); return false; } var filter = /^[^@\\s\u0026lt;\u0026amp;\u0026gt;]+@([a-z0-9]+\\.)+[a-z]{2,4}$/i; if (!filter.test($(selector.submitForm).find(\u0026#39;#mail\u0026#39;).val())) { message(\u0026#39;邮箱地址不正确\u0026#39;); enableCommentButton(); return false; } } if ($(selector.submitForm).find(selector.submitTextarea).val() == \u0026#39;\u0026#39;) { message(\u0026#39;评论为空\u0026#39;); enableCommentButton(); return false; } $.ajax({ url: $(this).attr(\u0026#39;action\u0026#39;), type: $(this).attr(\u0026#39;method\u0026#39;), data: $(this).serializeArray(), error: function() { message(\u0026#39;发送失败\u0026#39;); setTimeout(NProgress.done, 500) enableCommentButton(); return false; }, success: function(data) { if (!$(selector.commentList, data).length) { errorMsg = data.match(/.+/g).join().match(/\\\u0026lt;div.+\\\u0026gt;.+\\\u0026lt;\\/div\\\u0026gt;/g).join().match(/[^\\,]+/g); $msg.html(errorMsg[0] + errorMsg[1] + errorMsg[2]); enableCommentButton(); return false; } else { userCommentId = $(selector.commentList, data).html().match(/id=\\\u0026#34;?comment-\\d+/g).join().match(/\\d+/g).sort(function(a, b) { return a - b; }).pop(); commentLi = \u0026#39;\u0026lt;li id=\u0026#34;comment-\u0026#39; + userCommentId + \u0026#39;\u0026#34; class=\u0026#34;comment\u0026#34;\u0026gt;\u0026#39; + $(\u0026#39;#comment-\u0026#39; + userCommentId, data).html(); + \u0026#39;\u0026lt;\\/li\u0026gt;\u0026#39;; if (parentId) { if ($(\u0026#39;#\u0026#39; + parentId).find(\u0026#34;.comment-children\u0026#34;).length \u0026lt;= 0) { $(\u0026#39;#\u0026#39; + parentId).append(\u0026#34;\u0026lt;ul class=\u0026#39;children\u0026#39;\u0026gt;\u0026lt;/ul\u0026gt;\u0026#34;); } $(\u0026#39;#\u0026#39; + parentId + \u0026#34; .children:first\u0026#34;).append(commentLi); parentId = \u0026#39;\u0026#39; $body.animate({ scrollTop: $(\u0026#39;#comment-\u0026#39; + userCommentId).offset().top - 450 }, 900); } else { $(selector.commentList).prepend(commentLi) $body.animate({ scrollTop: $(\u0026#39;#comment-\u0026#39; + userCommentId).offset().top - 200 }, 900); } //$(\u0026#39;#comment-\u0026#39; + userCommentId).slideDown(\u0026#39;slow\u0026#39;); //console.log(userCommentId); $(selector.commentNumText).length ? (n = parseInt($(selector.commentNumText).text().match(/\\d+/)), $(selector.commentNumText).html($(selector.commentNumText).html().replace(n, n + 1))) : 0; TypechoComment.cancelReply(); $(selector.submitTextarea).val(\u0026#39;\u0026#39;); $(selector.commentReplyButton + \u0026#39; b, #cancel-comment-reply-link\u0026#39;).unbind(\u0026#39;click\u0026#39;); bindCommentReplyButton(); enableCommentButton(); } } }); return false; }); function bindCommentReplyButton() { $(document).on(\u0026#39;click\u0026#39;, selector.commentReplyButton, function() { parentId = $(this).parents(\u0026#39;li.comment\u0026#39;).attr(\u0026#34;id\u0026#34;); $(selector.submitTextarea).focus(); }); $(document).on(\u0026#39;click\u0026#39;, \u0026#39;#cancel-comment-reply-link\u0026#39;, function() { parentId = \u0026#39;\u0026#39;; }); } function enableCommentButton() { $(selector.submitButton).attr(\u0026#39;disabled\u0026#39;, false).fadeTo(\u0026#39;\u0026#39;, 1); $(selector.submitButton).val(\u0026#39;发射=A=\u0026#39;); } function message(msg) { $msg.hide(); $msg.html(msg).slideToggle(\u0026#39;fast\u0026#39;); } } 无限嵌套评论 function themeInit($archive) { Helper::options()-\u0026gt;commentsMaxNestingLevels = 999; } ","permalink":"https://easton.pub/posts/typecho-code-summary/","summary":"\u003cp\u003eTypecho 相比 WordPress，其所提供的接口较少，所以有些地方需要留心。遇到报错情况可以先在网上搜索相关资料，或者是询问 AI ，提出疑难。\u003c/p\u003e\n\u003ch3 id=\"首页文章无限加载文章\"\u003e首页文章无限加载文章\u003c/h3\u003e\n\u003col\u003e\n\u003cli\u003e在 functions.php 中加入一段判断 ajax 请求的方法：\u003c/li\u003e\n\u003c/ol\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-php\" data-lang=\"php\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003efunction\u003c/span\u003e \u003cspan class=\"nf\"\u003eis_ajax\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003eisset\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nv\"\u003e$_SERVER\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;HTTP_X_REQUESTED_WITH\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e]))\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;xmlhttprequest\u0026#39;\u003c/span\u003e \u003cspan class=\"o\"\u003e==\u003c/span\u003e \u003cspan class=\"nx\"\u003estrtolower\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nv\"\u003e$_SERVER\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;HTTP_X_REQUESTED_WITH\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e]))\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"k\"\u003etrue\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"k\"\u003efalse\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003col start=\"2\"\u003e\n\u003cli\u003e在首页模板 index.php 中加入判断，\u003ccode\u003eif (is_ajax ()):\u003c/code\u003e 则不输出 \u003ccode\u003eheader footer\u003c/code\u003e 等信息。\n最后一步，在模板公共 js 文件中加入以下代码：\u003c/li\u003e\n\u003c/ol\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-php\" data-lang=\"php\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003evar\u003c/span\u003e \u003cspan class=\"nx\"\u003epage\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"mi\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003evar\u003c/span\u003e \u003cspan class=\"nv\"\u003e$body\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003ewindow\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eopera\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"o\"\u003e?\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003edocument\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003ecompatMode\u003c/span\u003e \u003cspan class=\"o\"\u003e==\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;CSS1Compat\u0026#34;\u003c/span\u003e \u003cspan class=\"o\"\u003e?\u003c/span\u003e \u003cspan class=\"err\"\u003e$\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;html\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"o\"\u003e:\u003c/span\u003e \u003cspan class=\"err\"\u003e$\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;body\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e))\u003c/span\u003e \u003cspan class=\"o\"\u003e:\u003c/span\u003e \u003cspan class=\"err\"\u003e$\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;html,body\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"err\"\u003e$\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;#blog_load_more\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eclick\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"k\"\u003efunction\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003eevent\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"err\"\u003e$\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"k\"\u003ethis\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003ehide\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"err\"\u003e$\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;#spinner\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eshow\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"err\"\u003e$\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eajax\u003c/span\u003e\u003cspan class=\"p\"\u003e({\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"nx\"\u003etype\u003c/span\u003e\u003cspan class=\"o\"\u003e:\u003c/span\u003e \u003cspan class=\"s1\"\u003e\u0026#39;get\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"nx\"\u003eurl\u003c/span\u003e\u003cspan class=\"o\"\u003e:\u003c/span\u003e \u003cspan class=\"nx\"\u003eSITE\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003edefault_url\u003c/span\u003e \u003cspan class=\"o\"\u003e+\u003c/span\u003e \u003cspan class=\"s1\"\u003e\u0026#39;/page/\u0026#39;\u003c/span\u003e \u003cspan class=\"o\"\u003e+\u003c/span\u003e \u003cspan class=\"nx\"\u003eparseInt\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003epage\u003c/span\u003e \u003cspan class=\"o\"\u003e+\u003c/span\u003e \u003cspan class=\"mi\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e),\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"nx\"\u003esuccess\u003c/span\u003e\u003cspan class=\"o\"\u003e:\u003c/span\u003e \u003cspan class=\"k\"\u003efunction\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003edata\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nx\"\u003etextStatus\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nx\"\u003eXMLHttpRequest\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"nx\"\u003epage\u003c/span\u003e\u003cspan class=\"o\"\u003e++\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"err\"\u003e$\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;.w-blog-list\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eappend\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003edata\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"err\"\u003e$\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;#spinner\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003ehide\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"err\"\u003e$\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;#blog_load_more\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eshow\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"p\"\u003e},\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"nx\"\u003eerror\u003c/span\u003e\u003cspan class=\"o\"\u003e:\u003c/span\u003e \u003cspan class=\"k\"\u003efunction\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003eMLHttpRequest\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nx\"\u003etextStatus\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nx\"\u003eerrorThrown\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"err\"\u003e$\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;#spinner\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003ehide\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"err\"\u003e$\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003ejGrowl\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;Network Error\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e});\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e});\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e其中 \u003ccode\u003e#blog_load_more\u003c/code\u003e 为需要绑定的加载更多按钮，\u003ccode\u003e#spinner\u003c/code\u003e 为加载过程中的动画，\u003ccode\u003e.w-blog-list\u003c/code\u003e 为首页文章列表容器。\n由于 Typecho 并没有 Ajax 钩子函数，所以需要在 functions.php 中加入以下代码：\u003c/p\u003e","title":"Typecho 常用代码合集"},{"content":"在8月下旬，暑假结束前的几周时间，我的空余时间变多了，精力也充沛了不少，悠闲得很。我闲来无聊，去翻翻自己的博客，发现大陆的防火墙禁用了通过 http 协议访问 *.eu.org 域名的方式，随即在腾讯云上注册了这个新域名，brume.top。我觉得既然注册了新域名，倒不如把全站一并更新吧。于是在网络上到处搜索 Typecho 主题，但鲜有满意的。我以前也想过自己写出自己满意的主题，遗憾的是眼高手低，页面布局属实稀烂，自己都看不下去。我便产生了想放弃 Typecho 的想法。在逛了无数个自己收藏的博客之后，我选择了 hugo 。\n世界上最快的网站构建框架———— Hugo 是最受欢迎的开源静态站点生成器之一。凭借其惊人的速度和灵活性，Hugo 让构建网站再次变得有趣。\n翻译自 hugo 官网\nHugo 和 Typecho 大相径庭。它们是静态网站生成器和动态博客平台的典型代表。它们各有优劣。（下文部分由 AI 总结，并根据我的真实感受撰写成文）\n速率方面 Hugo：这是 Hugo 最引以为豪的一点。作为静态网站生成器，Hugo 在性能方面是属于碾压 Typecho 的存在。Hugo 生成的站点是纯静态的 HTML 文件，访问者在浏览网站时完全不需要进行动态内容的生成和前后端的交互，加载速度非常快。 Typecho：Typecho 在这方面稍微逊色，作为一个基于 PHP 和 MySQL 的动态博客系统，它的每次访问都会需要从数据库读取数据，并生成相应的页面。对于小型的个人博客站点，抛开加载图片或字体等资源不谈，它们之间访问速率的差异很小，基本可以忽略不计。 维护方面 Hugo：Hugo 生成静态页面后，不需要服务器支持 PHP 或数据库，直接部署即可。一般情况下，在部署后，网站不需要任何后台维护，只需使用静态服务器来托管即可。常见的托管服务的提供商有 GitHub Pages, Netlify, Vercel 等。在 Hugo 的文档 中也有提及。 Typecho：需要服务器支持 PHP 和数据库，因此在使用过程中涉及服务器环境的配置和维护。需要定期对服务器环境和数据库进行备份和更新。我之前就曾经发生过误删除数据库的事情。防患于未然的不二法门就是定期备份数据库。 可扩展性方面 Hugo：Hugo 本质上是一个生成静态网站的工具，因此对动态插件的支持比较有限。整体来说不如动态博客系统那样灵活。 Typecho：Typecho 支持通过插件扩展功能。但如果将 Typecho 和其他主流的动态博客系统相比，它的插件生态相对较小，选择有限。 开发效率方面 Hugo：Hugo 使用 Go 语言开发，生成网站速度非常快。网站的开发和生成流程也相对自动化，基于配置文件和 Markdown 语法，简单高效。 Typecho：Typecho 安装和配置比 Hugo 简单多了，但作为动态网站系统，日常运营过程中需要处理服务器和数据库相关的问题，Hugo 就不会出现这样的顾虑。 SEO 友好方面 Hugo：Hugo 本身作为静态网站生成器，所有页面都是预生成的，SEO 友好，加载速度快，对搜索引擎友好。另外，由于没有动态生成的网页，Hugo 在访问控制方面比较困难。它一般依赖于 CDN、反向代理或自定义前端实现访问控制。 Typecho：Typecho 可以通过插件或自定义开发支持更多的访问控制、权限管理等功能。此外，动态内容有助于实现一些基于用户行为的功能，比如个性化推荐和用户登录系统。 安全性方面 Hugo：因为生成的是静态页面，没有后台和数据库的存在，因此几乎不存在被攻击的风险，安全性极高。 Typecho：由于是动态系统，存在 PHP 代码执行、数据库操作等，需要注意服务器和程序的安全更新。此外，如果插件质量不佳，可能带来安全隐患。 总结 在以下几个方面，Hugo 相比 Typecho 存在优势：\n性能：动态网站速度较慢，尤其在流量大时。 维护复杂性：依赖 PHP 和数据库，服务器配置与维护需要更多工作。 扩展性（插件支持较弱）：虽然支持插件，但生态不如主流动态系统丰富。 安全性：由于动态性，安全风险比静态生成器更大。 ","permalink":"https://easton.pub/posts/a-comparison-between-typecho-and-hugo/","summary":"\u003cp\u003e在8月下旬，暑假结束前的几周时间，我的空余时间变多了，精力也充沛了不少，悠闲得很。我闲来无聊，去翻翻自己的博客，发现大陆的防火墙禁用了通过 http 协议访问 *.eu.org 域名的方式，随即在腾讯云上注册了这个新域名，\u003ccode\u003ebrume.top\u003c/code\u003e。我觉得既然注册了新域名，倒不如把全站一并更新吧。于是在网络上到处搜索 Typecho 主题，但鲜有满意的。我以前也想过自己写出自己满意的主题，遗憾的是眼高手低，页面布局属实稀烂，自己都看不下去。我便产生了想放弃 Typecho 的想法。在逛了无数个自己收藏的博客之后，我选择了 hugo 。\u003c/p\u003e","title":"Hugo与Typecho之对比"},{"content":"“蝉蜕尘埃外，蝶梦水云乡” 这是北宋词人张孝祥泛舟湘江时写下的句子。意境清远，恰似我心底对湖南那片水土最初的朦胧向往。前段时间，我们有幸参与学校组织的湖南研学。在此期间，我们往返于长沙、韶山两地，流连于乡村、城市之间，见证传统文化和现代社会的融合与碰撞，闲步湖湘，展望未来。\n第一日：束装就道，越岭向湘 正午阳光正好，南国的风难得带着一丝温存，不似往常那般灼热。整个年级的学生、带队老师及相关人员坐大巴前往深圳北站，乘坐高铁去长沙。到达北站的时间将近十二点半。\n△深圳北站远景\n在一个多小时的候车之后，我们一行终于坐上了高铁。复兴号的涂装非常亮眼，充满了现代科技感。由此不难观之，学校在出行方面充分照顾了学生，选择高铁而不是绿皮火车，学生有了更加良好的出行体验，节约了更多时间，也见证了我们国家这一领域的发展，可谓一举多得。\n登车前我边走路边拍了几张列车的图片，上面的两张是比较“清楚”的。\n上车后，我坐在窗边，欣赏窗外的风景。从珠江下游的冲积平原到粤北喀斯特地貌，从湘南丘陵到湘北平原，沿途的美景是不够看的。在行经广州北面时，天空下起了雨，车窗被雨水描摹出朦胧的线条，远方的江面（或许是珠江的某条支流吧）笼罩在烟雨之中，几艘驳船静默地泊着。烟雨蒙蒙中，竟真有点身处江南水乡的感觉了。 之后，我们经停韶关、郴州、衡阳等地，一共花费约三个半小时到长沙。 三个半小时，高铁由南往北，穿越数百公里。傍晚抵达长沙南站，现代化的气息再次扑面而来。坐上大巴，幸运地占据前排，得以更开阔地观察这座初次见面的城市。华灯初上，车流如织，高楼林立。夜渡浏阳河时，导游和我们轻松地聊着天，谈到了“我欲因之梦寥廓，芙蓉国里尽朝晖”这句词，身为语文老师的班主任也背诵一首《沁园春·长沙》，以助余兴。当此刻，河水在灯光下泛着粼粼波光，窗外的夜景亦似深圳，身在他乡，心在故乡。最终，我们在邻近黄花机场的酒店下榻，城市边缘的宁静与白日的喧嚣形成对比，为这奔波的第一日画上句点。身体虽有些疲惫，内心却因即将展开的探索而充满期待。\n第二日：熔古铸今，湘水流长 清晨的长沙，带着南方特有的湿润空气。我们重返城区，首站即是湖南省博物馆。这里的镇馆之宝，马王堆汉墓的出土文物当之无愧。站在深邃的墓坑复制模型前，等待着每半小时上演的灯光秀，人群的低语和期待的目光交织在一起。灯光秀里，光影在坑壁上流动，幻化出两千年前的服饰纹样、帛书上的神兽祥瑞，带着某种宗教般的庄肃与神秘，令人在惊叹之余，感受到历史的重量与生命的渺小，加之以若隐若显的威慑力。\n望着眼前十余米深的地宫，想起安眠于其中的辛追夫人，以及薄如蝉翼、轻若云雾的素纱襌衣，不禁感叹古人智慧的精妙与对“不朽”的执着追求。\n午后，我们跨过湘江，奔赴岳麓山下的湖南大学与岳麓书院。湘江的水沉稳而开阔地流淌着，千年文脉，传承不息。湖南大学这所“千年学府”，没有围墙阻隔，红砖建筑掩映在绿树浓荫中。校门口，几位穿着学士服的毕业生正在合影留念，给学府增添了青春的气韵。\n步入岳麓书院，一眼便望见“惟楚有材，于斯为盛”八个大字。书院里，古木参天，遮天蔽日；碑刻林立，字字珠玑。行走在青石板路上，抚摸着历经风雨的梁柱。作为一个在“文化荒漠”长大的“不谙世事”的“祖国花朵”，我似乎在此刻感受到了历史的纵深之感，得以与古代先贤共情。\n沿着屈子祠旁的小径拾级而上，便来到了爱晚亭。据说杜牧曾豫游于此，且唐伯虎有一首诗提到过爱晚亭。亭子本身古朴典雅，四周绿意盎然。我试图想象唐寅或是杜牧在此的意境，只是，周围游人如织，或许现代人在斯处似乎难以体会到那时唐寅悠然自得的意境了。\n我画蓝江水悠悠，爱晚亭上枫叶愁。 秋月溶溶照佛寺，香烟袅袅绕经楼。\n傍晚的太平老街之行，则更直接地将这种古典与现代的对立和统一摆在我们面前。作为“长沙古城保留原有街巷格局最完整的一条街”，它理应是探寻城市记忆的最佳场所。然而，放眼望去，标准化的仿古门面，千篇一律的小吃与纪念品，震耳欲聋的音乐，让这条老街更像是一个巨大的、热闹的消费市集。那些精美的麻石路面、旧时的商号遗迹，几乎被淹没在商业化的浪潮中。当然，理性地说，我肯定不会排斥商业，我只是惋惜它也没能幸免于全国古城古镇商品化、同质化的弊病，少了许多人文气息，逛来索然无味。旁边的贾谊故居在鼎沸人声中，反而显得有些“格格不入” 。那么，在现代化和商业化的浪潮下，我们该如何守护那些沉淀着文化记忆的“老地方” ？如何在满足现代人休闲需求的同时，不让它们失去本真的东西？这正是我们需要面对和探索的课题。尽管如此，太平老街的“黑色经典”臭豆腐，确实名不虚传，也算是我在喧嚣中寻得的一点“长沙味道”。\n第三日：躬耕乐道，红土情深 第三天，我们从长沙南下韶山。近三个小时的车程，窗外的景致渐渐由城市的高楼变为连绵的田野和起伏的山峦。穿陵谷，通莽原。渡江水，越荒山，我们抵达的目的地是位于黄田村的研学实践基地。\n从长沙去韶山。经过约三个小时的车程，到达位于黄田村的研学实践基地，和领队的农户会面，之后去水田捕鱼、插秧，随后前往农家就餐。地道的农家菜，食材新鲜，滋味朴实，饱含着乡土的温情。饭后在村中漫步，黄田村环境清幽，道旁几株不知名的野花自在地绽放，鲜艳的明黄点缀着绿意，展现着一种不事雕琢的、蓬勃的生命力。\n下午，一行人怀着崇敬的心情，参观韶山毛泽东同志纪念馆和故居，了解毛主席领导中国人民完成反帝反封建的历史任务、建立新中国、发展社会主义的光辉历史。纪念馆内，详实的史料和珍贵的文物将波澜壮阔的革命岁月铺陈开。毛主席领导中国人民争取民族独立、人民解放的奋斗历程，在这里得到了生动地展现。走出纪念馆，来到主席故居。那是一栋典型的南方农家建筑，坐落在一片清雅之地，门前半亩方塘，荷叶田田，背后青山环抱，有“重湖叠巘清嘉”之韵味。很难想象，就是从这样宁静、甚至有些平凡的农家院落，走出了深刻改变中国乃至世界历史进程的伟人。\n在故居附近，我偶遇了两位初中时的同窗。他们也在参加另一所高中的研学活动。他乡遇故知，分外亲切。我们短暂交流了彼此高中一年来的学习生活，分享着进入人生新阶段的感受与变化。时光的流逝和地域的阻隔，并未冲淡昔日之情，反而让此刻的重逢更显珍贵。我们合影留念，互道珍重，尽欢而别。\n夜幕降临，我们在韶山观看了大型实景演出《中国出了个毛泽东》。演出的场地极为开阔，据说其轮廓是依照中国地图的形状设计的，我在手机地图应用上竟真的能辨认出来，此设计本身就足够震撼。当灯光亮起，音乐响起，实景的山水、演员的演绎、炫目的光影特效交织，构成一幅宏大的历史画卷。演出以一种极具冲击力的方式，再现了中国革命的关键时刻和领袖人物的成长轨迹。现场实景如下图所示。演出的灯盏光彩夺目，很是让人震撼。\n晚上住宿在灰汤的一家酒店。\n第四日：湘水北去，橘洲寄情 第四日，我们再次返回长沙。上午的行程是参观湖南第一师范学院（旧址），毛主席的母校。红墙围绕，绿树成荫，这座承载着光荣革命传统的学府，依然保持着庄重而朴素的学风。漫步其中，仿佛能感受到当年那群风华正茂的年轻人，在此立志救国、探索真理的热情与抱负。\n下午，我们登上了橘子洲。湘江从城市中心穿流而过，而橘子洲静卧江心。我们乘坐小火车，从洲北一路向南，直抵橘子洲头。学校安排了全年级师生在此拍摄大合照。趁着等待的间隙，我和几位同学在江边闲逛。湘江这段流速较缓，水面较宽。站在江边看江水南下、百舸争流，也不失为一种乐趣。 湘江水流到这里，似乎变得格外开阔而从容。江面宽广，水流舒缓，偶有船只驶过，划破平静的水面，留下长长一道波纹。站在这里，眺望对岸的城市天际线，感受着江风的吹拂，心中不由得涌起一股开阔之气。\n北宋词人张孝祥有词赞曰：\n濯足夜滩急，晞发北风凉。吴山楚泽行遍，只欠到潇湘。买得扁舟归去，此事天公付我，六月下沧浪。蝉蜕尘埃外，蝶梦水云乡。\n制荷衣，纫兰佩，把琼芳。湘妃起舞一笑，抚瑟奏清商。唤起九歌忠愤，拂拭三闾文字，还与日争光。莫遣儿辈觉，此乐未渠央。\n我很喜欢这首词中带有的那种在苏轼的阔达豪迈和李清照的清新雅逸词风之间的，清高闲远的意境。虽然此刻并非“夜滩急”，亦无“北风凉”，但那种摆脱尘世纷扰、在山水间寻求精神自由与高洁的意境，却跨越千年。这片水云乡，确有让人暂时忘却俗务、心生蝶梦之感。\n在拍摄完合照之后，我们前往北面的毛主席青年艺术雕像。雕塑目光如炬，恰似主席“指点江山，激扬文字”的气概。 晚些时候，出乎意料地，我们又回到了韶山附近，参加一场篝火晚会。夜幕沉沉，篝火熊熊燃起，烟花在夜空中绚烂绽放，瞬间点亮黑暗，又归于岑寂，带来短暂而强烈的兴奋感。班级合唱环节，我本因不擅歌唱而退居一旁，却被几位“好哥们”半开玩笑半认真地“架”上了舞台。而这一幕恰好被在校电视台担任职务的同学捕捉到，后来竟成了活动报道里的一个“花絮”，在全校范围内“小火”了一把。\n第五日：稻香思源，归去来兮 上午参观袁隆平水稻博物馆，下午坐上高铁，傍晚时分返回深圳。\n下午，我们再次踏上高铁，向深圳归去。“复兴号”依旧平稳飞驰，窗外的景物迅速后退。看着电子屏上不断跳动的时速数字，有时达到了306公里/小时，心中再次涌起对国家科技发展的自豪感。三个小时的旅程，似乎比来时更快，或许是因为心中已装满了五日的见闻与感悟，我一路上在 Obsidian 整理旅途中所记的笔记，希冀整理成一片博文，发布到我的网站，以作纪念。\n返回时列车停站较少，因此三个小时就到了北站。在北站附近就地解散，研学旅行圆满结束。\n结语：于行走中思索，在实践中成长 这五日，由南到北，思绪云骞。从深圳的现代都市，到长沙的古今交融；从马王堆的汉墓幽光，到岳麓书院的千年文脉；从伟人故里的红色印记，到袁隆平稻博馆的金色希望；从都市的繁华喧嚣，到乡村的质朴宁静——我们穿梭于不同的时空坐标，亲历了历史与现实的对话，感受了传统与现代的张力。\n湖南，这片被称作“惟楚有材，于斯为盛”的土地，向我们展示了它丰富的层次。它有沉淀千年的文化底蕴，也有敢为人先、经世致用的务实精神；它孕育了改天换地的革命豪情，也滋养着“一粒米”造福世界的朴素理想。我曾在太平老街的商业化浪潮中感到些许失落，思考着文化传承与现实发展的平衡点；也曾在岳麓书院的古碑前，体会到超越功利、追求“道理最大”的纯粹；更在韶山的红土地上，理解了理想信念如何能支撑个体乃至民族穿越艰难险阻。\n张孝祥的“蝉蜕尘埃外，蝶梦水云乡”，曾是我对这趟旅程的朦胧向往。如今归来，我更深地理解了其中的意蕴。“蝉蜕”或许象征着我们这些少年人，正需要通过行走与观察，褪去认知上的稚嫩与偏见，如同蝉蜕去旧壳；而“蝶梦”，则是在这片充满生机与故事的“水云乡”里，我们开始孕育的、关于未来、关于责任、关于如何让这片土地更加美好的梦想。\n在此次研学活动中，我们学到了文化、学到了历史、学到了传承。今朝，正值盛世繁华，置身于“百年未有之大变局”的壮阔时代洪流，我们幸运地拥有了前所未有的广阔视野，得以屹立于历史的巅峰，眺望那遥不可及的璀璨未来。民族复兴的壮丽画卷，在眼前缓缓铺展，既振奋人心，又催人奋进。\n因此，作为新知识的集大成者、新思想的探索者、新时代的弄潮儿，我们的肩上自然而然地承载起了一份沉甸甸的神圣使命。“多少事，从来急；天地转，光阴迫。”作为国家之未来、民族之希望所在，我们深知责任之重大，使命之光荣。我们誓将以中国独有的智慧之光芒、创新之方案、坚定之行动，去勾勒并塑造一个更加美好的世界的明天，让中国声音响彻全球，让世界因我们青年一代而更加精彩！ ","permalink":"https://easton.pub/posts/a-trip-to-changsha/","summary":"\u003cp\u003e“蝉蜕尘埃外，蝶梦水云乡” 这是北宋词人张孝祥泛舟湘江时写下的句子。意境清远，恰似我心底对湖南那片水土最初的朦胧向往。前段时间，我们有幸参与学校组织的湖南研学。在此期间，我们往返于长沙、韶山两地，流连于乡村、城市之间，见证传统文化和现代社会的融合与碰撞，闲步湖湘，展望未来。\u003c/p\u003e","title":"湘行散记：蝉蜕尘埃外，蝶梦水云乡"},{"content":"期末考试结束后，高中的第一个学期告一段落。2024年的第一个月也将要过完。过去的2023年过得实在不算精彩——毕竟“世事一场大梦，人生几度秋凉”。对我个人而言，2023年是充满挑战和机遇的一年。\n代码如诗 这句话是我在 WordPress 官网上读到的。英文版作\u0026quot;Code is poetry.\u0026quot;\n诚然如是，我也时常把代码视作是诗篇。\n2023年，我在GitHub上的贡献次数为16次，贡献主要集中在暑假期间，毕竟这一时期空余时间较多。\n2023年初，或许是因为 Typecho 的轻量高效的优点吸引了我，我把我的博客从 WordPress 彻底迁移到了 Typecho ，接着就在这片沃土之中扎根下来，耕耘这片互联网上的小圈子。虽然初春之时的手残删库让建站前期的一些数据丢失，但总的来说，这个博客的生命还在。\n我曾设想过自己因为学业的缘故无暇顾及博客，因而开发了一款名为 SiteMaintenance 的插件，用来显示关闭站点的页面。虽然我最后放弃了长期将站点“关闭”的想法，但插件本身毕竟还是一个奇妙的创意，因此我将它上传到了 GitHub 上。颇具喜感的是，这个55行代码的小可爱，迄今也已经收获了7stars。\n另外，2023年寒假，我还创作了一个名为“癸卯工程”的游戏，它通过再现校园的风景，以纪念我们在学校的屈指三年。同学们挺喜欢这款游戏，时常下课去玩。它也随着升学，一路火到了高中。\n大概在去年11月底、12月初的时候，我了解到了申请各类 SSL 证书的底层逻辑。虽说各大厂商的控制面板大多数已经支持申请诸如 Let\u0026rsquo;s Encrypt , Zerossl 之类的免费 SSL 证书，但一些小厂提供的面板就没有这一功能，于是我上网搜集资料和源码，并将其整合，计划做出一个 SSL 证书的签发平台。\n2023年，在代码领域，我算是比2022年有所长进。我将部分空余时间利用起来学习代码，也在掘金上看过很多程序员的经历，感触颇深。总之，2023年对我未来的生涯规划和职业选择具有深远影响。\n剑吼西风 2023年，又阅读了许多诗书。现在看来，让我感触最深的当属贺方回老先生的《六州歌头》：\n少年侠气，交结五都雄。肝胆洞，毛发耸。立谈中，死生同，一诺千金重。推翘勇，矜豪纵，轻盖拥，联飞鞚，斗城东。轰饮酒垆，春色浮寒瓮，吸海垂虹。闲呼鹰嗾犬，白羽摘雕弓，狡穴俄空。乐匆匆。\n似黄粱梦，辞丹凤；明月共，漾孤篷。官冗从，怀倥偬，落尘笼，簿书丛。鹖弁如云众，供粗用，忽奇功。笳鼓动，渔阳弄，思悲翁。不请长缨，系取天骄种，剑吼西风。恨登山临水，手寄七弦桐，目送归鸿。\n这首词写的是贺铸老来回忆自己年少时的经历。想当年，他推翘勇矜豪纵轻盖拥联飞鞚斗城东轰饮酒垆。这是何等豪放！可叹浮生如梦，白衣苍狗。人间忽晚，他到老也只做得一个小官，是“官冗从，怀倥偬，落尘笼，簿书丛”。更令人扼腕叹息之处在于，山河破碎，他是乱世中的平凡人中的一员。若连生命都难保于乱世，又何谈修身齐家治国平天下？他是报国无门。\n读完这首词，我自己感慨万千。一年来，我经历了太多难以预料之事（或许再过几年看看也不过如此）。依稀记得23年初写过一套语文阅读理解题目，文章讲最初“像一块棱角分明的石头”的主人公在走上社会十余年后，被“打磨”成像“一块鹅卵石”的经历。当时的我下课时嘲讽这篇文章的作者，信誓旦旦地给同学说，自己永远不会像这个文章的主人公一样窝囊废，自己最多只会被“打磨”成一个“等边三角形”。\n可是一年后呢？我想，现在的自己既不是一块“鹅卵石”，也不是一个“等边三角形”，但以后呢？这还真不好说。\n我又想到了吴湖帆先生的次韵之作。\n废兴一例，今昔几英雄？玄理洞，头角耸。劫磨中，苦心同，木铎人争重。何神勇，资天纵。朱旆拥，青丝鞚，逞西东。谈笑偃时，颓玉沈香瓮，咳唾腾虹。慨胡沙黩武，力尽铁胎弓，喘息横空。却匆匆！\n破江山梦、凌霄凤、鱼鳖共、浪飘篷。拚傔从，行倥偬，孰牢笼，散林丛。激厉情和众，为谁用？莫称功。枢宇动，雷云弄，渐成翁。若个天骄，自是炎黄种，顾盼生风。更相思千里，按剑击焦桐，北燕南鸿。\n我有一位良师曾在他的一次演讲中，向同学喊出一句口号 “少年应有鸿鹄志，当骑骏马踏平川”。诚然，一个人最美好的年纪、他/她的少壮年华，就在少年。我进入高中以后，因办事两次返回故校。每次去时，时间都较晚，但依旧能看到一些学生在校内玩耍、逗留。我有时对他们拥有大把空余时间而钦羡，有时却又对他们肆意挥霍青春的态度而惋惜。或许人越长大，其懂得的道理就越多吧。\n如果以自己宣誓入团（2023年5月）为界，把自己的人生阶段一分为二，那我觉得我的少年时期与贺铸还是相若的。少年时期，也曾意气风发，也曾心胸坦荡、也曾才情出众。少年壮志，也曾我武惟扬，也曾意志坚定，也曾百折不回。然而时光匆匆，年少轻狂转瞬成为过往，令人感慨光阴的流逝之快。新环境里，自己一心追求上进，饱尝光荣抑或是屈辱。尽管身负才华与理想，但行动上却常常仓促匆忙。我已厌倦了世俗的纷扰，羡慕自由自在。珠玉在侧，觉我形秽。现在看来，自己一直都是一名再平凡不过的人，竟有些怀疑自身是否还能再次创造出曾经那样的功绩。\n2023年，有人称之为“AI元年”。世界依旧处在百年未有之大变局中，终于意识到自己只是人类的八十亿分之一罢了。在现实与梦想的矛盾冲突下，回想起当初年少时的雄心壮志，自比昆仑之种，面对浩荡西风。如今虽心有不甘，但愿能有辛幼安“乘风好去，长空万里，直下看山河”的壮志情怀，乘风而去，登临送目，看着漫山遍野的梧桐，眺望远方的归鸿，“剑吼西风”。\n文字和诗 初看这个标题，内容似乎和“剑吼西风”无异。其实不然。\n我或许也算是一个文学类博主。\n2023年初的一次考试中，作文是材料作文，也是半命题作文。那时的我学习压力陡增，身旁的同学也如此。\n后来老师拿了几篇市教研员选出来的优秀作文，我最欣赏的作文是如下两篇。（文章作者尚不清楚，符合《中华人民共和国著作权法》第二十四条第一、二款之规定）\n读完作文，我的心中充满感慨。文字是有力量的，是能传递情感的，是指引我们前进的方向的。不管怎么说，它终究是让我“重燃薪火誓穹苍”了。每当我再衰三竭之时，我总是去读曾经那段峥嵘岁月中的文章，它总是给人以希望。\n年近岁除之时，作为学校的代表之一，参加了区作文比赛，并取得佳绩。这也算是学友们的作文给我的灵感吧。\n开学不久学习诗歌之际，老师布置了写诗的作业，我写了数首诗，还有一首是仿写顾城《一代人》的，因为太短，它没有写进随笔本。但这首诗恰好契合这段短文的主旨。\n在黑暗中我们寻求光明，在痛苦中我们看到希望，在失败中我们汲取力量。\n“路虽远，行则将至；事虽难，做则必成”。在黑暗抑或是光明的日子里、在希望抑或是痛苦的日子里、在失败抑或是成功的日子里，无论如何，干就完了。毕竟，实干，才是民族复兴的唯一要素。\n也要时刻感受笔尖回响。\n平凡抑或是伟大 2023年，我曾在自己的知识库中记下这样一句话：\n“没有人会囿于伟大，也没有人会甘于平凡”\n以前一直觉得自己好像足够优秀，殊不知是初中学校太菜，自己的眼界太狭小了。一路走来的过程，就是跟越来越多智者同行、被越来越多同龄人打败的认识自我的过程。\n只能说以前的虚假的优势或者说借口，随着时间发展，眼界的开阔，便渐渐消失了。\n在我看来，新的一年，要少一点功利心，多一些责任心、实干心。\n又想到了很早以前很火的一部电影，里面有这样一首诗：\n我的孩子\n如果可以\n我想告诉你\n世间的一切奥秘\n告诉你山川大河，日升月落\n光荣与梦想，挫折与悲伤\n告诉你\n燃料是点燃自己\n照亮别人的东西\n火箭是为了梦想\n抛弃自己的东西\n生命是用来燃烧的东西\n死亡是验证生命的东西\n宇宙是让死亡变得渺小的东西\n渺小的尘埃\n是宇宙的开始\n平凡的渺小\n是伟大的开始\n而你\n我的孩子\n是让平凡的我\n想创造新世界的开始\n如上图，这首诗被改编成深圳2021年末的中考适应性考试语文作文的材料，或许也是2022年“是你，让我超越了平常的自己”的作文题目的来源之一。\n在新时代的大背景下长大的新一代的中国青年，是中国特色社会主义制度建设过程的亲历者。时代在变，市场的大潮在变，年轻人的奋斗姿态也在变。一代人有一代人的挑战，一代人也有一代人的追求。我们面对的是新科技的浪潮，面对的是大变革的环境，我们渴望更美好的未来、向往更丰盈的生命。\n诚然，没有人会囿于伟大，也没有人会甘于平凡。只有个体把平凡的生活过得有意义、有价值，才能拥有追求更高境界的动力和勇气。\n小站新篇 又是新的一年，也换了一个新主题。从 default theme 到 matcha，从 Mydiary 到 Jasmine 。我从前曾设想过自己这一款主题，也不知何时能把这未竟之业做成。\n写在最后 相信关注我的朋友也发现了我把GitHub上的用户名更改成了“Zephyr Easton”。这是我在2023年给自己新取的英文名。而“Zephyr”就来源于上文的“剑吼西风”之意。\n总之，前景可待，未来可期。2023年，遇见了千百倍优秀于自己的同学，也从他们身上汲取到了新时代中国青年最难得的意志品质。\n这种意志，当然也并不是寸丝半粟——它体现在年轻一代眼中闪烁出的坚定里，熔铸在他们为事业拼搏、为梦想力行的决心里，彰显在他们的言辞、他们的气度、他们的胸襟里……\n我们何其有幸与这个伟大的社会主义祖国同生，何其有幸与这个新时代同频共振！\n既如此，我也没什么好说的了：\n二零二四，凌云壮志\n","permalink":"https://easton.pub/posts/a-look-back-at-2023/","summary":"\u003cp\u003e期末考试结束后，高中的第一个学期告一段落。2024年的第一个月也将要过完。过去的2023年过得实在不算精彩——毕竟“世事一场大梦，人生几度秋凉”。对我个人而言，2023年是充满挑战和机遇的一年。\u003c/p\u003e","title":"二零二四，凌云壮志"},{"content":"博客已经平稳运行一年了，想想就让人激动！在建站一周年的时间节点上，抚今溯往，思如潮浪。感慨万千，乃志以彰。\n这篇文章我已经构思许久，在我打草稿的时候，我曾数次想到了标题“漫漫长梦”。人生如梦。我想，它象征着我对人生的思考和哲学的追寻，也映射出无数人追求真理和发现人生意义的漫长旅程。 《Hello World》 春树暮云博客是我心灵的寄托，从 22 年末以来，我在这里与读者们分享自己的探索和收获。从我的文字中，不难看出，人生就如同一场漫长的梦，无论是高中、大学还是日后走进社会，我们都将在不同的阶段中筑梦、逐梦。停下来意味着重新思考和重新定位，让不才暮云能够更好地专注于未来的探索。 《漫漫长梦》 回首过去，饱尝光荣屈辱，遍沐春风雪霜。我明白人生是一段漫长而复杂的旅程。正因如此，我对自己加以要求，要勇敢追随自己的梦想，并且无畏地破茧而出。别让不确定性吓到自己，因为正是不确定性激发了我们的潜力。 《确定性和不确定性》 当我们在拨开人生的迷雾时，我们会遇到无数的疑问和挑战。我们会跌倒，受伤，但也会重新站起来。这正是人生的真谛所在，一段伟大而真实的人生需要经历坎坷与起伏，方能演绎出奇迹。我想，或许渺小的人类也可以透过写作，完成生命的伟大和壮阔。 《重新开始》 一年之期只是博客之新起点，但我的探索和思考永未结束。我将努力应对新的挑战，迎接新的机遇，积累新的知识和经验。我希望在一段时间后，能够带着更丰富的见解回到这里，与你们再次相遇。\n最后，我想感谢博客每一个读者，你们的支持鼓励是我前进的动力。请记得，即使博客的更新时断时续，但心灵的探索和思考永不停息。让我们一同追随“漫漫长梦”，探索人生的深藏哲理。“把酒祝东风，且共从容！”\n衷心地向你们致意\n暮云君\n2023年12月31日于 Dreamland\n","permalink":"https://easton.pub/posts/long-dreams/","summary":"\u003cp\u003e博客已经平稳运行一年了，想想就让人激动！在建站一周年的时间节点上，抚今溯往，思如潮浪。感慨万千，乃志以彰。\u003c/p\u003e\n\u003cp\u003e这篇文章我已经构思许久，在我打草稿的时候，我曾数次想到了标题“漫漫长梦”。人生如梦。我想，它象征着我对人生的思考和哲学的追寻，也映射出无数人追求真理和发现人生意义的漫长旅程。\n\u003ca href=\"/posts/hello-world/\" class=\"internal-link\"\u003e《Hello World》\u003c/a\u003e\n\u003c/p\u003e","title":"漫漫长梦"},{"content":"这是在今天开学考试后，总结自己让春树暮云适配PWA的一段经历和教程。枯燥无味\n什么是PWA PWA（Progressive Web App）是一种用于开发Web应用程序的技术和方法。PWA结合了传统的Web应用程序和原生移动应用程序的最佳特性，旨在提供更好的用户体验。\nPWA可以通过浏览器访问，不需要安装在设备上，但它们具有类似原生应用程序的功能和特性。PWA利用Web浏览器提供的API，如 Service Workers 和 Web App Manifest 等，使应用程序可以脱机工作、推送通知，并添加到主屏幕。这些特性使得PWA可以在不同平台和设备上以一致的方式运行，并且无需从应用商店下载和安装。\n深入了解 PWA 检查浏览器是否支持PWA 我建议在 caniuse.com 上查询某浏览器是否支持 PWA 网页链接 打开网站后页面大概长这样▽ 网站适配PWA 简单来说，网站适配PWA大概分以下几个步骤。\n全站https化 编写 manifest.json 和 sw.js 准备 192×192px 的应用图标和 512×512px 的应用图标 在网站相关页面引入 manifest.json 文件和 sw.js 文件 接下来是自己的实操。\nmanifest.json { \u0026#34;name\u0026#34;: \u0026#34;春树暮云\u0026#34;, // 用于安装横幅和启动画面显示的名称 \u0026#34;short_name\u0026#34;: \u0026#34;春树暮云\u0026#34;, // 不为空时,为桌面PWA应用程序的名称 \u0026#34;theme_color\u0026#34;: \u0026#34;#f6ffff\u0026#34;, // 主题色 \u0026#34;background_color\u0026#34;: \u0026#34;#f6ffff\u0026#34;, // 背景色 \u0026#34;display\u0026#34;: \u0026#34;fullscreen\u0026#34;, // 启动过渡动画, standalone或fullscreen \u0026#34;scope\u0026#34;: \u0026#34;/\u0026#34;, \u0026#34;start_url\u0026#34;: \u0026#34;/\u0026#34;, //启动页面 \u0026#34;icons\u0026#34;: [ { \u0026#34;src\u0026#34;: \u0026#34;192.png\u0026#34;, //根据自己图标的路径改写，下同 \u0026#34;sizes\u0026#34;: \u0026#34;192x192\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;image/png\u0026#34;, }, { \u0026#34;src\u0026#34;: \u0026#34;512.png\u0026#34;, \u0026#34;sizes\u0026#34;: \u0026#34;512x512\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;image/png\u0026#34; } ], 上面的代码可以根据实际情况，进行对应更改。\nsw.js \u0026#39;use strict\u0026#39; //声明整个脚本处于严格模式 const cacheName = \u0026#39;PWA-cache\u0026#39;; // 缓存名字 const startPage = \u0026#39;https://example.com\u0026#39;; // 首页地址 const offlinePage = \u0026#39;https://example.com\u0026#39;;// 离线首页地址 const filesToCache = [startPage, offlinePage]; const neverCacheUrls = [/admin/];//从不缓存的目录，如typecho中的\u0026#34;admin\u0026#34;文件夹 self.addEventListener(\u0026#39;install\u0026#39;, function(e) { console.log(\u0026#39;Cache event!\u0026#39;) e.waitUntil( // 安装时，对需要缓存的文件进行缓存 caches.open(cacheStorageKey).then(function(cache) { console.log(\u0026#39;Adding to Cache:\u0026#39;, cacheList) return cache.addAll(cacheList) }).then(function() { console.log(\u0026#39;Skip waiting!\u0026#39;) return self.skipWaiting() }) ) }) self.addEventListener(\u0026#39;activate\u0026#39;, function(e) { // 判断地址是不是需要实时去请求，是就继续发送请求 console.log(\u0026#39;Activate event\u0026#39;) e.waitUntil( Promise.all( caches.keys().then(cacheNames =\u0026gt; { return cacheNames.map(name =\u0026gt; { if (name !== cacheStorageKey) { return caches.delete(name) } }) }) ).then(() =\u0026gt; { console.log(\u0026#39;Clients claims.\u0026#39;) return self.clients.claim() }) ) }) self.addEventListener(\u0026#39;fetch\u0026#39;, function(e) { // 匹配到缓存资源，就从缓存中返回数据 e.respondWith( caches.match(e.request).then(function(response) { if (response != null) { console.log(\u0026#39;Using cache for:\u0026#39;, e.request.url) return response } console.log(\u0026#39;Fallback to fetch:\u0026#39;, e.request.url) return fetch(e.request.url) }) ) }) ###引入文件### 在\u0026lt;head\u0026gt;标签中引入 manifest.json\n\u0026lt;link rel=\u0026#34;manifest\u0026#34; href=\u0026#34;manifest.json\u0026#34; crossorigin=\u0026#34;anonymous\u0026#34; /\u0026gt; 然后，在\u0026lt;body\u0026gt;标签中判断是否注册了serviceWorker\n\u0026lt;script type=\u0026#34;text/javascript\u0026#34;\u0026gt; if (navigator.serviceWorker != null) { navigator.serviceWorker.register(\u0026#39;sw.js\u0026#39;) .then(function(registration) { console.log(\u0026#39;Registered events at scope: \u0026#39;, registration.scope); }).catch(function(err) { console.log(\u0026#39;Registered events at fail: \u0026#39;, err); }); } \u0026lt;/script\u0026gt; 此时，PWA已经适配完成了。\n但是，在 typecho 或 wordpress 等博客系统中，容易产生因各类原因造成的适配不成功的情况。此时，可以选择引导用户手动配置PWA，例如，通过点击按钮配置PWA。\n代码如下：\n\u0026lt;script\u0026gt; var deferredPrompt = null; // 判断用户是否安装此应用：beforeinstallprompt,如果用户已经安装过了,那么该事件不会再次触发 // 需要卸载，然后重新打开浏览器才能再次触发 window.addEventListener(\u0026#34;beforeinstallprompt\u0026#34;, e =\u0026gt; { e.preventDefault(); deferredPrompt = e; }); // 安装完成后触发,即点击安装弹窗中的“安装”后被触发 window.addEventListener(\u0026#34;appinstalled\u0026#34;, () =\u0026gt; { deferredPrompt = null; }); function addToDesktop() { // 调用prompt()方法触发安装弹窗 deferredPrompt.prompt(); deferredPrompt = null; } \u0026lt;/script\u0026gt; \u0026lt;button onclick=\u0026#34;addToDesktop()\u0026#34;\u0026gt;点击安装PWA\u0026lt;/button\u0026gt; 演示如下： 因更换博客系统，此链接已失效，以后有时间会尝试为本站配置PWA 2026.8 注：新换博客系统后，认为已没必要为本站（easton.pub）适配 PWA。\n最后，为避免兼容问题，建议在部署PWA时，将 manifest.json和 sw.js 文件置于网站根目录下。\n","permalink":"https://easton.pub/posts/pwa/","summary":"\u003cp\u003e这是在今天开学考试后，总结自己让春树暮云适配PWA的一段经历和教程。\u003cdel\u003e枯燥无味\u003c/del\u003e\u003c/p\u003e\n\u003ch2 id=\"什么是pwa\"\u003e什么是PWA\u003c/h2\u003e\n\u003cp\u003ePWA（Progressive Web App）是一种用于开发Web应用程序的技术和方法。PWA结合了传统的Web应用程序和原生移动应用程序的最佳特性，旨在提供更好的用户体验。\u003c/p\u003e","title":"将博客适配 PWA"},{"content":" The mighty desert is burning for the love of a blade of grass who shakes her head and laughs and flies away.\nStray Birds\n今天下午，我从学校回家。回程的车上人满为患，我和一位同学并排坐着。他到站时，我侧过身让他通过。临走时，他问我，“你是不是之前在台上演讲的那位同学？”我点了点头。由于之前那次关于哲学的演讲比较成功，许多新生都认识我，而我却叫不上他们的名字。这已经不是我第一次遇到这种情况了。下车后，我和一名朋友同行，心中一直在思考一个问题：我的演讲是如何起步的？为什么能取得成功？\n在初中，我的第一次正式演讲是在2022年5月。那次演讲的成功带有一定的偶然性，但也为我之后的演讲奠定了基础。恰逢共青团成立一百周年，班主任选定我作为升旗仪式的演讲人。与老师讨论后，我选择了“追梦不负青春，奋斗不负时代”作为主题。这次国旗下的演讲引起了热烈反响。\n同年8月，年级推荐我在开学典礼上发言，鼓励同学们为实现青春目标努力奋斗。开学前几天，我来到学校做准备，正好遇到了新任年级主任。她听过我的上一次演讲，对我能力有信心，也对这次演讲提出了一些改进建议。但由于疫情原因，开学延期，演讲也被搁置。直到2022年12月，期中考试后，我们年级召开了一次线上大会，当天中午，我在校党员活动室和其他有关同学参与了视频录制，下午，年级大会上，我的演讲引起了大震动，班级里掌声和欢呼声不断，其他班级的同学也给了我很多鼓励。也许正是这几次成功的演讲，让更多志同道合的朋友认可了我的能力，在这个新的集体中，推荐我继续发言。\n第三次演讲较为低调，发生在今年2月新班级创建时。班主任和一些同学觉得开班应该有一个仪式，就推举我和班长一起发言。\n第四次演讲就是本文开头提到的那次。\n回顾起来，我的第一次演讲虽然是偶然的，但却为之后的一系列演讲铺平了道路。许多朋友认为，随着我不断努力，偶然性逐渐转变为必然性。这也意味着，在选人发言时，不确定性正逐渐变得单一，变得确定（当然，这只是朋友的一家之言，我相信高中的许多同学能力远高于我，他们也可能成为下一位被推举的人选，我对此有清楚的认识）。\n有时，我会向朋友抱怨，最近一个月要在公开场合做三次演讲，太累了；但有时，我又觉得自己很幸运，能够被老师、朋友们信任和认可。一个完全确定的世界是枯燥和乏味的；一个充满不确定性的世界是无常和恐怖的。问苍茫大地，何以调和？\n试深入思考这两个概念。\n不确定性给予我们机会去创新和进步。我们身处在一个未知的世界。当未来充满未知时，我们不再依赖过去之经验和既定之模式，不至于因循守旧，而是需要寻找新的方法、新的策略、新的机遇。正是在面对不确定性的时刻，我们才有机会挑战自己、超越自我，迎接新的机遇。\n从我自己的角度看，确定性和不确定性能为自己提供成长和发展的契机。正如上文所提到的演讲——在确定性的环境中，尽管我们的惰性是可怕的，我们可能会陷入舒适区，可能对新的经历和挑战缺乏动力……但是，在不确定的变革时刻，我们能够更加快速地适应新的环境和情况，就像我们离开曾经的集体，来到新的高中一样。我做个大胆的预言，这种适应能力完全能使我们变得更加独立和坚韧，肯定有助于提升我们个人的各个方面的技能！\n补充说明一点，从不确定性中吸取教训、积累经验，我们能够培养出更好的解决问题的能力。这些成长的机会能使我们更具竞争力，实现党和国家、社会所倡导的“全面发展”，为我们未来的事业做好准备。\n长远来看，确定性（抑或是不确定性），能完完全全地影响、改变一个人，是个彻彻底底的伪命题！每个人都是社会这张大网上的一个“节点”，每个人的成长和社会是分不开的！我的演讲之所以能够被大家认可，并不是因为确定性的缘故，而是因为自己在这一方面花了一些心思。推而广之，我校之所以取得一些成就，亦非“确定性”或“不确定性”所能完完全全决定的。\n“从来就没有什么救世主，也不靠神仙皇帝。要创造人类的幸福，全靠我们自己！”\n确定性或不确定性只能在个人成长中起促进作用，而最终的决定权掌握在我们自己手中。\n写到这里，感觉我身边的很多友人，在学习这方面或多或少地迷恋着“确定性”和“不确定性”。记得今年上半年，有个外省的同学，因讲座上的讲师发表不当言论，而将他的话筒夺去，并坚定地说出“学习是为了中华民族的伟大复兴”这句豪言壮语。我敬佩这位同学的大格局大视野大胸怀大境界。我们是新时代的中国青年。要让我们青年人眼里有诗和远方，这是做青年人的重大的责任！读书绝不仅仅是为了让自己认识几个英语单词或者数学符号，更重要的是培养博大的胸怀和宽广的格局！这些无关性格的温润或者粗犷，也无关于“确定”抑或是“不确定”！中国近代伟人中，主席、秋白、独秀、彭总……性格各异，但均为我中华民族中彪炳千秋的伟大人物。书中自有颜如玉，书中自有黄金屋，作为深圳市前百分之五的学生来讲，怎能格局太小，甚至不敢拿上桌面？青年人，应该从小就明白：读万卷圣贤书，行万里人生路！穷且益坚，不坠青云之志！往高处走，往宽处寻，往平处坐！近朱者赤，近墨者黑！学高为师，德高为范！读书不是为了以所谓“确定性”谋生，而是为了修身齐家治国平天下！读书更是为已立心，为友树榜，为家争光，为国桢干，为有能力替天下肇和平！\n在这个充满机遇与挑战的世界里，对于个人和社会来说，把握机遇是至关重要的。如何从容面对确定性和不确定性？如何把握机遇、应对挑战？这是我们每个人都需要思考的根本性问题。\n","permalink":"https://easton.pub/posts/certainty-and-uncertainty/","summary":"\u003cblockquote\u003e\n\u003cp\u003eThe mighty desert is burning for the love of a blade of grass who shakes her head and laughs and flies away.\u003cbr\u003e\u003cp align=\"right\"\u003e\u003ci\u003eStray Birds\u003c/i\u003e\u003c/p\u003e\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e今天下午，我从学校回家。回程的车上人满为患，我和一位同学并排坐着。他到站时，我侧过身让他通过。临走时，他问我，“你是不是之前在台上演讲的那位同学？”我点了点头。由于之前那次关于哲学的演讲比较成功，许多新生都认识我，而我却叫不上他们的名字。这已经不是我第一次遇到这种情况了。下车后，我和一名朋友同行，心中一直在思考一个问题：我的演讲是如何起步的？为什么能取得成功？\u003c/p\u003e","title":"确定性与不确定性"},{"content":" 近年来，AI技术的飞速发展已经深刻地改变了我们的生活方式。二维码作为一种快速、便捷的信息传递工具，也在全球范围内广泛应用。本文旨在探讨AI在二维码领域所带来的变革，并阐述其对人们生活的积极影响。\n提高二维码的识别速度和准确率 通过使用深度学习、图像处理等技术，AI可以快速地从复杂的背景中识别出二维码，并且可以纠正二维码的倾斜、模糊、遮挡等问题，从而提高识别效率和用户体验。（所以一些“花里胡哨”的二维码的潜力即将发挥出来）例如下面的这张图片： 拓宽应用领域 AI技术的引入为二维码的应用领域带来了巨大的拓展空间。通过AI算法，二维码可以与图片紧密融合，更加美观，具有了更大的发展空间。如qrbtf 和quickqr 这两个二维码平台，相继进军AI领域。通过AI生成的二维码，所得到的成品具有能从赛博空间中一窥现实的感觉。自以为，这将引起很多行业的一次大变革和大震动。这种新型的二维码，营造了一种梦幻般的超凡脱俗的界面，远胜过传统二维码所表现出的纷纷扰扰的现实之感。例如： （第一张图是我国的研发团队创作出的，暮云初敛，东方欲晓，白山黑水之间，廊腰缦回，檐牙高啄，充盈着诗情画意。第二张图由外国的一研发团队创作。将积木拼接而成的猫巧妙地融入图中，具有别具一格的美感。不得不说，这是AI的杰作。）\n促进商业创新 这个话题也是我和友人讨论过的，本文只探讨它的积极影响。AI技术在二维码领域的广泛应用对商业创新产生了积极的影响。从上两张图来看，二维码的智能化应用提供了新的商机和商业模式。\n(如果需要论证我的猜想，可以进行实验，通过分析被扫描的二维码的数据，帮助相关方了解消费者偏好，并进行精准营销和推广。)\n在我看来，二维码的创新激发了商业发展的潜力，为企业带来了更大的竞争优势。\n新型二维码对文化产业的影响 在本人看来，新型二维码将对文化产业带来**“颠覆性”**的巨大变革。将中华传统文化融入二维码中，有利于推动中华优秀传统文化的创造性转化、创新性发展。此外，AI生成的二维码也可以与二次元文化相结合，推动其新发展。 例如： △qrbtf官方制作的动漫风格的二维码\n△qrbtf官方制作的油画风格的二维码\n△qrbtf官方制作的青花瓷风格的二维码\nAI生成二维码的实现原理 利用相关软件生成一张二维码的图片。 通过Stable Diffusion将这张图片美化。 但看似简单，实际操作可能稍微麻烦。通常，Stable Diffusion对设备配置有较高要求，因此，相关开发者制作了Stable Diffusion 的网页版（使用共享GPU,功能受限），例如dreamstudio,huggingface spaces上的Stable Diffusion，qrbtf网站。\n实操成品 △借助quickqr的discord机器人生成的猫\n△同样是quickqr的模型，较上一张稍显逊色，融合不够恰当、合理\n△huggingface的模型，是一座城市的鸟瞰图\n△huggingface生成的中世纪风格的古堡图片\n后记 这篇文章写得拖沓，从7月10日开始写起，一直写写写写写了九天，直到7月18日终稿（也可能是自己最近事情比较多的缘故吧），中途可能有些文字不太连贯，望友海涵。\nqrbtf的二维码是迄今我所知道的，效果最好的AI二维码生成工具之一。但是它现在还在内测，我无法利用它生成自己的二维码，它生成的二维码暂时都是指向它的站点的。例如下图： qrbtf.com内测中的api密钥是qrbtf-ai。\n除此之外，quickqr的机器人也是不错的工具。具体操作不赘述，见其官网。\nhuggingface中的Stable Diffusion模型稍显逊色，它所生成的人像表情可能有些怪异。\n网址如下：\nhttps://qrbtf.com/ https://quickqr.art/ https://huggingface.co/spaces/huggingface-projects/QR-code-AI-art-generator https://huggingface.co/spaces/ioclab/ai-qrcode-api https://huggingface.co/spaces/stabilityai/stable-diffusion 总结与反思 AI在二维码领域的应用给人们的生活带来了巨大的变革。它提高了扫描速度和准确度，拓宽了二维码的应用领域，加强了安全保障机制，并促进了商业创新的发展，令人眼前一亮。然而，我们也要认识到AI技术的应用不会不是一把双刃剑。优胜劣汰。生产力进步，原地踏步终究会被淘汰。（朋友语）\n","permalink":"https://easton.pub/posts/ai-qr-code/","summary":"\u003cblockquote\u003e\n\u003cp\u003e近年来，AI技术的飞速发展已经深刻地改变了我们的生活方式。二维码作为一种快速、便捷的信息传递工具，也在全球范围内广泛应用。本文旨在探讨\u003cstrong\u003eAI\u003c/strong\u003e在二维码领域所带来的变革，并阐述其对人们生活的积极影响。\u003c/p\u003e","title":"使用 AI 生成一张二维码！"},{"content":"今天考完自招，将原来的个人网站彻底迁移到这里，想起了曾经的网站被DNS劫持过，便以我之所学，写一小段文字，以备不时之需。\n旧日班上，下课时间，一群同学总喜欢凑在电脑屏幕前玩电脑。一次他们在网上冲浪一段时间后，突然怪叫起来。我抬起头，看见几个男生向我跑来，大笑着告诉我我的主页跳转到了其他网站。此时他们已经把跳转后的网站关掉了。在看了浏览记录后，我大为诧异，但我晚上回家打开网站时，却一如往常。事后经分析，我的原网站大概率被DNS劫持。\n由于申请到了新域名，原来的网站已被弃用，但新站还是需要预防DNS劫持的。以下是我对应对DNS劫持的方法。\n确认自己的域名是否被进行了DNS劫持 被DNS劫持的域名无论怎么访问都会直接跳转至其他的站点，此时可以直接使用IP进行访问，如果IP访问正常但域名访问一直跳转，那大概率是自己的域名被劫持了。这种情况，需要联系DNS解析方面或者服务器方面的工作人员进行处理。\n确认自己的模板文件是不是被添加了跳转代码 此时可以在后台暂时更换成其他安全的模板，如果不再跳转，那就说明是模板文件里面被加了料。\n一般跳转代码会添加到JS文件中，尤其jQuery库是被加料的重灾区，可以查看各JS文件开头结尾是不是多出了一些加密代码，如果有，删掉后清理缓存再访问试试。\n\u0026lt;!--使用meta标签跳转--\u0026gt; \u0026lt;meta http-equiv=\u0026#34;refresh\u0026#34; content=\u0026#34;0;url＝http://example.com\u0026#34;\u0026gt; \u0026lt;!--使用JavaScript跳转--\u0026gt; \u0026lt;script language=\u0026#34;javascript\u0026#34; type=\u0026#34;text/javascript\u0026#34;\u0026gt; window.location.href = \u0026#39;http://example.com\u0026#39;; \u0026lt;/script\u0026gt; 或者直接把可疑的文件或完整模板包更换成下载模板压缩包里的原始文件。\n如果更换其他的模板仍然跳转，那就是cms程序或者运行环境被黑了 这种情况一般黑客会在运行环境里动手脚，比如apache和nginx被黑，自动在网页里面增加跳转代码，甚至可以控制频率，比如每天只加一次，而且只在手机版访问的时候出现，极其隐蔽，很难排查。\n遇到这种情况，可以重新安装cms程序或者在服务器管理面板（如宝塔）里面卸载并重装apache/nginx这些运行环境，即可解决问题。\n如果还是不行，那应该就是数据库或者服务器被黑了。 这种情况，直接联系服务器方的工作人员帮忙处理，普通人大概率是搞不定了。\n总之，整体思路就是定位问题具体出现的位置，然后针对性处理就可以了。\n修改或处理前一定要记得备份！\n平时也一定做好服务器的安全防护工作，以及定期备份网站和数据库，防患于未然！\n","permalink":"https://easton.pub/posts/the-solution-of-domain-abnormal-redirections/","summary":"\u003cp\u003e今天考完自招，将原来的个人网站彻底迁移到这里，想起了曾经的网站被DNS劫持过，便以我之所学，写一小段文字，以备不时之需。\u003c/p\u003e\n\u003cp\u003e旧日班上，下课时间，一群同学总喜欢凑在电脑屏幕前玩电脑。一次他们在网上冲浪一段时间后，突然怪叫起来。我抬起头，看见几个男生向我跑来，大笑着告诉我我的主页跳转到了其他网站。此时他们已经把跳转后的网站关掉了。在看了浏览记录后，我大为诧异，但我晚上回家打开网站时，却一如往常。\u003cstrong\u003e事后经分析，我的原网站大概率被DNS劫持。\u003c/strong\u003e\u003c/p\u003e","title":"域名跳转解决方案"},{"content":"检验自己三年学习成果的考试已经结束，偷得浮生半日闲。在误删数据库导致博客文章被悉数清空的两个月后，我又一次登录进了这个网站。意图很明显，我要重新开始。\n大概是因为我最近经历了很多事情吧，当我翻看以前写的作品时，感觉现在的我和过去的我已经截然不同了。如果将这两个时间段的文字放在一起对比，会感受到一种奇妙的反差吧。\n积攒了很多话想说，却不愿意在互联网的方寸之间挥洒笔墨——我的思绪还是太发散，不适合写这篇题为“重新开始”的文章。我深知自己不能懈怠，只愿意趁着半日光阴把网站重新建好，或许也算是自己成长的见证。\n现在看来，“重新开始”是新的征途的开始；是新的梦想、新的希望的开始；是新的成长、新的气象的开始！\n有句话我特别喜欢——能够掌握时间的人，才能够掌握自己的一生。\n我深信不疑，我，将会让我超越平常的自己。\n我，要重新开始。\n","permalink":"https://easton.pub/posts/restart/","summary":"\u003cp\u003e检验自己三年学习成果的考试已经结束，偷得浮生半日闲。在误删数据库导致博客文章被悉数清空的两个月后，我又一次登录进了这个网站。意图很明显，我要重新开始。\u003c/p\u003e","title":"重新开始"},{"content":"春树暮云 has been successfully created! why we call the site 春树暮云 ?“春树暮云” is from a piece of poem written by Du Fu, a well-known poet. In the past, it means missing friends from afar. Now I think it also means pursue our ideals. Spring trees and evining clouds symbolize our unattainable dreams, hard to achieve but also have hope. It\u0026rsquo;s our duty to achieve it, achieve “春树暮云”。\n2026.8 更新：本站已改名为“东斋”。“春树暮云”为原站站名。\n","permalink":"https://easton.pub/posts/hello-world/","summary":"\u003cp\u003e\u003cstrong\u003e\u003ca href=\"https://brume.top\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"external-link\"\u003e春树暮云\u003c/a\u003e\n\u003c/strong\u003e has been successfully created!\n\u003cfont color=\"red\"\u003e\u003ch4\u003ewhy we call the site 春树暮云 ?\u003c/h4\u003e\u003c/font\u003e“春树暮云” is from a piece of poem written by Du Fu, a well-known poet. In the past, it means missing friends from afar. Now I think it also means pursue our ideals. Spring trees and evining clouds symbolize our unattainable dreams, hard to achieve but also have hope. It\u0026rsquo;s our duty to achieve it, achieve “春树暮云”。\u003c/p\u003e","title":"Hello World"},{"content":"","permalink":"https://easton.pub/friends/","summary":"","title":"友链"}]