<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
    <channel>
        <title>信也のブログ</title>
        <link>https://blog.shinya.click</link>
        <description>互联网自留地</description>
        <lastBuildDate>Tue, 12 May 2026 15:22:25 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <language>zh</language>
        <item>
            <title><![CDATA[一个 TREK 引发的二十个 stack]]></title>
            <link>https://blog.shinya.click/fiddling/one-trek-twenty-stacks</link>
            <guid isPermaLink="false">https://blog.shinya.click/fiddling/one-trek-twenty-stacks</guid>
            <pubDate>Mon, 11 May 2026 14:43:00 GMT</pubDate>
            <description><![CDATA[本来只是想自部署一个旅行规划工具，结果从挑 VPS、加固服务器到迁博客、搭邮件、SSO、备份，一路装出二十多个自托管服务。]]></description>
            <content:encoded><![CDATA[<h3 id="引">引<a href="#引" class="heading-anchor-link" aria-label="Link to 引"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h3>
<p>前几天刷 GitHub trending 看到一个项目叫 TREK，是个让一群人在地图上排路线、订住宿、列预算的项目，做完还能分享给别人继续改。点进 demo 站玩了一圈，挺顺手。</p>
<p>跟 npy 出去玩的行程一直是用 Obsidian 拼出来的，几个 daily note 来回链着写，越写越乱，往往到出门那天还是只有粗糙的"早上去哪下午去哪"。看到 TREK 我立刻就想自己跑一台。可以直接用作者的 demo，但旅行计划这种东西放别人服务器上多少有点不爽，早晚要自己部署。</p>
<p>既然要自己部署，索性顺手开一台新 VPS。</p>
<h3 id="netcup-arm">Netcup ARM<a href="#netcup-arm" class="heading-anchor-link" aria-label="Link to Netcup ARM"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h3>
<p>挑 VPS 比想象中纠结。DigitalOcean / Linode 同等配置直接贵一倍以上；国内小厂便宜，但 25 端口出站和备案是两个潜在雷，能不碰就不碰；Hetzner 性价比好，可惜 ARM 节点选址不灵活。兜回来还是 Netcup 顺手。</p>
<p>具体型号选了 VPS 2000 ARM G11，10 vCore、16G 内存、512G NVMe、2.5 Gbps 网络，月价 €13.41 含税，配置放到 x86 那边怎么也得贵一倍。流量是 flatrate，24 小时滑动平均超 2 TB 才会限速到 200 Mbps，我这种小破站根本碰不到。</p>
<p>下单前我去 Google 了一圈，捞到一张 -50% coupon，首付直接砍到 €6.7 出头，续费时回到原价。我对这种"首期半价"的玩法没什么意见，反正首年这一刀已经够香了。</p>
<p>机房可选 Nuremberg、Wien、Amsterdam 和美东 Manassas 四个。最后挑了 Manassas，国内常见路由走过去 ping 比另外三个欧洲点都低一截。至于邮件 IP 信誉这事，反正我出站邮件都走 Resend HTTPS 桥接（后面会讲），不依赖本地 IP 落地，没必要为这个守在欧洲机房。</p>
<h3 id="从零到-trek-跑起来">从零到 TREK 跑起来<a href="#从零到-trek-跑起来" class="heading-anchor-link" aria-label="Link to 从零到 TREK 跑起来"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h3>
<p>下单到收到登录信息大概 5 分钟。第一件事不是装东西，先收 SSH：禁 root、禁密码登录、改端口、ufw 默认 deny in。fail2ban 跑两天看日志，22 端口换之前一天 5000+ 次暴力尝试，换之后掉到个位数。刚装的机器，外面的人比你急。</p>
<p>ARM64 镜像生态这两年好了很多，后面陆陆续续装的二十来个 stack 里，Caddy、Postgres、Stalwart、SnappyMail、Open WebUI、Vaultwarden、Dagu、Glance、Karakeep、Paperless 全都有官方多架构镜像，docker pull 直接 work。唯一稍微费点事的是 Forgejo runner，base 镜像是 ARM64 没问题，但我需要在 runner 里跑 docker compose 当 deploy 工具，得自己加 docker-cli 和 docker-compose-plugin。最后是 buildx 推了一份 ARM64 镜像到自己的 Forgejo registry，runner 起来直接拉。</p>
<p>终于轮到 TREK 本身。仓库 mauriceboe/TREK 有官方 docker-compose.yml，照抄过来改了几行：</p>
<ul>
<li>端口收成只对 web 这个 docker 网络可见</li>
<li>ENCRYPTION_KEY 用 openssl rand -base64 32 新生成一把</li>
<li>数据库 DATABASE_URL 走 SQLite，自己用够了</li>
<li>加 web 这个 external network，跟 Caddy 接通</li>
</ul>
<p>Caddyfile 那边一行 reverse_proxy trek<div></div> 收尾。从 docker compose up -d 到 trek.shinya.click 出现登录页，前后不到 15 分钟。看着空荡荡的 dashboard 我注册了第一个账号，下意识琢磨的下一件事不是"行程从哪开始拉"，而是"还能塞点啥进这台机器"。这毛病我自己也知道。</p>
<h3 id="装着装着就停不下来">装着装着就停不下来<a href="#装着装着就停不下来" class="heading-anchor-link" aria-label="Link to 装着装着就停不下来"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h3>
<p>后半夜的折腾大致就是从这种心思开始的。反正配置富裕，闲着也是闲着，索性把之前一直想自托管的东西都搬上来。</p>
<p>第二天起床顺手装了 Forgejo 把代码托管收回来，博客也借着这波翻新了一遍，下面单独说。邮件用 Stalwart + SnappyMail + 自己写的 Resend bridge 三件套。AI 那条线挂 CLIProxyAPI 把 OpenAI / Claude / Gemini 几家订阅统一起来，前面再罩一层 Open WebUI 自己用。再后面是密码、网盘、图床、笔记、稍后读、RSS、PDF 工具集，能自托管的几乎都过了一遍。SSO 用 Authelia 把这一票服务统起来，登录一次到处通用。备份用 Dagu 起一个 DAG 每天加密快照到 R2。</p>
<p>陆陆续续装完，整套结构大概就是这张图：</p>
<figure><img src="https://blog-img.774352199.xyz/4F9LtW.png" alt="架构图，不过还缺了博客部分" loading="lazy" decoding="async" style="background-image:url(data:image/webp;base64,UklGRiwAAABXRUJQVlA4ICAAAAAwAQCdASoQAA4AAsBMJZwAA3AA/vXdgIGg5HG0oQAAAA==);background-size:cover;background-repeat:no-repeat"><figcaption>架构图，不过还缺了博客部分</figcaption></figure>
<p>入口侧全部走 Cloudflare SaaS 智能解析回源；Caddy 是唯一一台外向 TLS 终点，按 Host 头反代到对应容器；需要登录的服务统一走 Authelia forward_auth；出站邮件走 Resend HTTPS API 桥接绕开 Netcup 的封禁；备份每日全量加密快照到 Cloudflare R2。</p>
<p>回头看 Dockge 面板里二十来个 stack 全绿色亮着挺有满足感的，但我也承认这中间有相当一部分是部署成瘾型上头：本来只想要一个 TREK，最后是一整套自托管基础设施。TREK 在这张图里只占其中一个小格子。</p>
<h3 id="顺手把博客也迁了">顺手把博客也迁了<a href="#顺手把博客也迁了" class="heading-anchor-link" aria-label="Link to 顺手把博客也迁了"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h3>
<p>之前博客是 Astro + retypeset，markdown 文件 push 上去走 CI 构建发布。用了一年多没什么大问题，但有几个一直让我膈应的点：发个错别字 fix 也要 commit、push、等 build；想加个"友链"或者"独立页面"这种轻量页面，要重新折腾文件树；游记里照片多了构建慢得离谱。</p>
<p>正好新机器铺好了，索性把博客也一起翻了一遍。</p>
<p>新栈是 SvelteKit 2 + adapter-node SSR + UnoCSS，视觉沿用 retypeset 但样式按 UnoCSS 重写了一份。内容后端换成 PocketBase，建了 7 个 collection 把文章、标签、游记、游记天数、游记照片、独立页面、友链全收进 SQLite。i18n 改成 zh / en / ja 三语并存。原 Astro 那边导出来的 markdown 写了个 migration 脚本一锅倒进 PB，123 篇文章、4 篇游记、148 张游记照片记录加上 pages、friends 全部落库。</p>
<p>部署改成蓝绿：blog-blue 和 blog-green 两个容器都常驻，活色由 /opt/app/blog/active 标记，Caddy 反代上游从 /opt/app/caddy-blog/upstream.caddy 这个文件 import。CI 推新镜像后跑 switch.sh，起 target 色、等 healthcheck、改 import 文件、caddy reload 切流，失败自动回滚。这套做完整个发布流程从"push 等三分钟"变成"PB 后台改一下、保存即生效"。</p>
<p>后台是 SvelteKit 内部的 SPA 挂在 /admin 路径下，走 Authelia forward_auth。admin → PB 的写请求经 Caddy 同源反代 /api/pb/*，token 由 Caddy 注入，浏览器和 git 仓库都摸不到这把 key。PB 写入会触发一个 JS 钩子 POST 到 blog 容器内部端点，做服务端缓存失效再调一次 Cloudflare API purge 边缘缓存，公开页平均还是 CDN 命中。</p>
<p>整个迁移前后两个晚上搞定，意外地顺。</p>
<h3 id="收尾">收尾<a href="#收尾" class="heading-anchor-link" aria-label="Link to 收尾"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h3>
<p>折腾了好几天，trek 到现在一篇规划也还没做 = =</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[微软模拟飞行 2024 折腾记录]]></title>
            <link>https://blog.shinya.click/fiddling/msfs2024-joystick-and-pico</link>
            <guid isPermaLink="false">https://blog.shinya.click/fiddling/msfs2024-joystick-and-pico</guid>
            <pubDate>Sun, 14 Dec 2025 06:31:00 GMT</pubDate>
            <description><![CDATA[被朋友一句话撬开了玩模拟的念头，随手买了微软模拟飞行，才发现地图和模型实时串流、账号还得二次登录，新手教程藏得奇怪，键盘操控生涩，最后入手摇杆并折腾Pico VR才算完整体验。]]></description>
            <content:encoded><![CDATA[<h3 id="前言">前言<a href="#前言" class="heading-anchor-link" aria-label="Link to 前言"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h3>
<p>模拟类游戏（SLG）一直是一个相对小众的游戏类型，极高的上手难度和较为“无趣”的体验劝退着玩家（毕竟谁还想在下班后继续体验现实世界呢）。我个人其实一直比较喜欢现实模拟，或者说对这个概念十分沉迷，总让我联想到本科时期接触到的“数字孪生”的概念，但实际上也一直没有去尝试体验过。</p>
<p>契机其实是 K 君提到他近期在玩欧卡，过上了“上班时上班，下班后继续上班”的生活。受此启发，也恰逢电子阳痿，我便决心找一款模拟游戏玩一玩。恰好，同事提到了他一直很想玩微软模拟飞行 2024。去网上简单了解了一下后，决定入手。</p>
<figure><img src="https://blog-img.774352199.xyz/2025/73bd42514ceb45fe9eed37b39d420e3b.webp" alt="网图" loading="lazy" decoding="async" style="background-image:url(data:image/webp;base64,UklGRlYAAABXRUJQVlA4IEoAAADwAQCdASoQAAkAAsBMJYwCdAECk874YKoA/u8/zqE0iymw/bxdpAsZCLJDIDzzv9tFFs9iHP48tZ2dHhrtraKfKSxj89BSMYAAAA==);background-size:cover;background-repeat:no-repeat"><figcaption>网图</figcaption></figure>
<h3 id="msfs2024-初体验">MSFS2024 初体验<a href="#msfs2024-初体验" class="heading-anchor-link" aria-label="Link to MSFS2024 初体验"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h3>
<p>当天午休时，就从 steam 上下单了 msfs 2024 标准版，港区 498 hkd，属实是不坑穷人的钱。与直觉相悖，游戏本体只有 8gb 左右。游戏的地图、地景、天气、建筑模型等数据都是在游戏过程中实时下载（串流）的，这对网络要求比较高，红迪上提到连接到微软的相关服务器至少需要 80mbps 的速度，尤其是这些服务器是境外服务器。所以需要一个好的加速器或一些特殊的上网姿势才能有比较好的体验，否则连初始的启动游戏连接 xbox 账号都无法完成。</p>
<p>pc 端和 xbox 端账号是互通的，所以从 steam 上启动游戏后还要在游戏内再登录一次 xbox 账号以同步游戏的版本和购买记录。msfs 2024 内置的一个商店，可以用来购买机场和飞机。游戏提供了三个版本：标准版、豪华版和高级豪华版（中杯、大杯、超大杯），这三个版本除了解锁的机场和飞机之外没有任何区别。</p>
<p>msfs2024 新增了一个生涯模式（职业模式），类似于一些主线剧情，大概就是完成任务得到 money 再买飞机飞更大的任务之类的循环。但对于我这种初次体验的小白，肯定要找新手教程。有点反直觉的是，新手教程隐藏在“活动”板块，找了好半天。</p>
<p>新手教程包括一些简单的仪表盘辨认、基础操作和起飞降落，固定翼飞机以赛斯纳 172 这款飞机为例，飞机内景确实很逼真，确实有种身临其境的感觉。但说实话用键盘玩这款游戏是有点折磨的，尤其是方向舵的控制，在转弯等场景很难维持平稳，需要不断地点按微调，十分难受，油门也是。本质上还是通过键盘这种非线性操控方式来模拟一些需要线性调整的设备造成的不适配。</p>
<figure><img src="https://blog-img.774352199.xyz/2025/aa206be1b0d4e56f7ffd42b98471f150.webp" alt="赛斯纳 172" loading="lazy" decoding="async" style="background-image:url(data:image/webp;base64,UklGRmQAAABXRUJQVlA4IFgAAADQAQCdASoQAAkAAsBMJQBdgCHPo2JaIAD9eDoNO2zpfvp0sYnZP0fpvInEuR2PhJK0MWdBPpoZR1mOWXNxMHUCLS7YLRn/7/DSskbh8bB7unZVU4VpIAAA);background-size:cover;background-repeat:no-repeat"><figcaption>赛斯纳 172</figcaption></figure>
<h3 id="图马思特-tca-领航员空客版">图马思特 TCA 领航员空客版<a href="#图马思特-tca-领航员空客版" class="heading-anchor-link" aria-label="Link to 图马思特 TCA 领航员空客版"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h3>
<p>那么怎么才能解决这种冲突呢？当然是找一些线性操控设备啦。最简单的自然是手柄，但手柄由于按键有限，有些使用场景下不得不配合键盘一起使用。</p>
<p>更加进阶/拟真的方式则是购买专用的飞行模拟摇杆，模拟了真实的飞行摇杆的手感和操作方式，<strong>在飞对应机型时</strong>提供更加优秀的拟真效果。飞行摇杆可以大致分为两种类型：空客式和波音式。空客式则是你想象中的摇杆，确实是一根杆；而波音式的则是方向盘形状的。</p>
<p>经过多轮淘宝和京东的比较，最终选择了图马思特家的 TCA 领航员空客版，小贵，1800 rmb。顺丰的效率很高，直接次日达。</p>
<figure><img src="https://blog-img.774352199.xyz/2025/9962f3f424062f81391112987de19b60.jpg" alt="摇杆+节流阀+襟翼" loading="lazy" decoding="async" style="background-image:url(data:image/webp;base64,UklGRoAAAABXRUJQVlA4IHQAAAAwAgCdASoQAAsAAsBMJZQCw7YvNbMepg4H6AD+8LeKCvwM8AVP2U51++9xaftW941lhQiV6eE/yGZdqHWh/yV3APQI5bPylgj5kTNVQ5fa7JT4+2HTrgbNLiGSFhj8amH/EbAIFgm2fJQ4qKQ84hW8R8AAAA==);background-size:cover;background-repeat:no-repeat"><figcaption>摇杆+节流阀+襟翼</figcaption></figure>
<p>东西不大，箱子却很大。两件分开发货，两大箱子差点把门都堵住了，管他是不是过度包装，反正保护效果是拉满了。</p>
<p>安装也很简单，摇杆是插上即用，节流阀+襟翼组装完成插到电脑上后，需要在图马思特的官网上再下载一个<a href="https://support.thrustmaster.com/zh/product/tca-captain-pack-x-airbus-ed-zh/" target="_blank" rel="noopener noreferrer" data-umami-event="outbound-link-click" data-umami-event-url="https://support.thrustmaster.com/zh/product/tca-captain-pack-x-airbus-ed-zh/">驱动</a>，安装完成后启动游戏即可识别。</p>
<p>各种键位需要在游戏设置中检查，一个小技巧是先根据键盘键位找到设置项，再到摇杆设置中找到对应的摇杆键位。大部分键位比较符合直觉。我目前只调整了配平轮的键位。默认配平轮的调整需要按住摇杆的 button7 + 顶部的小摇杆，就需要两只手操作，不太友好。默认的顶部小摇杆只用来移动视角，在通过鼠标或 VR 移动视角时这样的键位其实不是很有必要，于是将顶部小摇杆直接改为了调整配平轮。</p>
<h3 id="pico-你这家伙">PICO 你这家伙<a href="#pico-你这家伙" class="heading-anchor-link" aria-label="Link to PICO 你这家伙"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h3>
<p>无论是赛车、卡车还是飞行模拟，一个很难绕开的话题就是 VR。恰好我有一个 pico4pro，于是寻思着折腾一下。</p>
<p>最开始使用的是 pico 自带的 pico connect，pico 互联，效果不尽如人意，启动也比较麻烦，体验上也存在各种 bug。一个比较明显的 bug 是，在连接 pico connect 后，如果此时 msfs2024 还没进入 vr 模式，vr 手柄点击无响应，即使退出 vr，msfs2024 也不再响应鼠标点击，必须退出游戏重新开启。一个简单的解决办法就是在不连接 vr 的情况下用鼠标控制 msfs2024 进入 vr 模式，而后再连接 pico connect 和 steamvr。</p>
<p>总之很麻烦～</p>
<p>google 了一番，解决方案是使用 virtual desktop。好消息是，virtual desktop 可以在 pico 上使用，直接在 pico 商店即可下载。坏消息是，virtual desktop 没有上国区商店，而我的 pico 恰好是国区的。</p>
<p>于是又是一番网上冲浪，尝试了各种完整的不完整的解决方案后，终极解决方案：刷外版系统。外版系统 ROM 可以在<a href="https://pico.crx.moe/docs/picoos-research/version-table" target="_blank" rel="noopener noreferrer" data-umami-event="outbound-link-click" data-umami-event-url="https://pico.crx.moe/docs/picoos-research/version-table">这里</a>下载。下载完成后在 internal storage 下新建一个 dload 文件夹存放 ROM 压缩包，随后即可在通用设置中选择本地升级了，升级完成后系统就变成了外版系统。</p>
<p>外版系统当然需要外版账号，建议直接在手机上注册，pico 中直接登录就好。pico 国际版 app 可以在 <a href="https://apkpure.com/pico-vr/com.picovr.assistantphone.global" target="_blank" rel="noopener noreferrer" data-umami-event="outbound-link-click" data-umami-event-url="https://apkpure.com/pico-vr/com.picovr.assistantphone.global">apkpure</a>下载安装。virtual desktop 亲测在 pico 中的商店也无法搜索到，可以直接在手机 app 上购买后，在 pico 设备的商店购买记录中即可下载安装。</p>
<p>virtual desktop 的 openXR 建议直接选择 vd 的 environment，绕过 steamvr，以获得更好的体验。</p>
<h3 id="后记">后记<a href="#后记" class="heading-anchor-link" aria-label="Link to 后记"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h3>
<p>这一套配置下来花了我一个周末的时间，飞了两个短途航线后，就不知道为啥发烧躺着了～</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[MoonTV —— 一次 Vibe Coding 尝试]]></title>
            <link>https://blog.shinya.click/fiddling/moontv-vibe-coding</link>
            <guid isPermaLink="false">https://blog.shinya.click/fiddling/moontv-vibe-coding</guid>
            <pubDate>Sun, 20 Jul 2025 15:32:00 GMT</pubDate>
            <description><![CDATA[MoonTV 是一款全新的影视聚合平台，基于 NextJS 和 React 构建，旨在为用户提供便利的追剧体验。项目起源于对 LibreTV 的改进尝试，经过几个月的开发，已经取得了不小的成功，获得了大量关注和使用。借助 Cursor 的强大能力，开发过程变得高效而顺畅，尽管也遇到了一些技术挑战，如多平台适配和复杂的数据依赖管理。随着用户的不断增加，MoonTV 正在逐步完善，响应用户反馈，力求提升整体体验。]]></description>
            <content:encoded><![CDATA[<p>大概半年前，就一直在关注一个项目 <a href="https://github.com/LibreSpark/LibreTV" target="_blank" rel="noopener noreferrer" data-umami-event="outbound-link-click" data-umami-event-url="https://github.com/LibreSpark/LibreTV">LibreTV</a>，这是一个影视聚合项目，聚合各种采集站的资源做统一搜索和播放。当时想着给 npy 部署一个追剧平台，就关注了一下这个项目，后面还给这个项目贡献了不少代码，包括更换 html5 播放器，对这个项目的运行逻辑也愈发熟稔。后面用着用着，这个项目的弊端也不断展现出来：这是一个纯前端项目，播放记录等信息都存储在浏览器的 localstorage 中，更换浏览器就会丢失；代码都是纯 JS 写就的，经过不同人手，代码也比较混乱，由于没有静态检查，很多代码都不太敢动，也很难进行大改。</p>
<p>大概过了几个月，同事告诉我 cursor 支持支付宝付款了，精神为之一振。此前一直使用 Trae，模型能力较差不说，高峰期还需要排队，后面又开始收费，免费版问答动辄排队三四百人，几乎到了不可用的状态。于是火速入了一个月 Cursor Pro，准备体验一波，正好拿来重写 LibreTV。</p>
<p>完全没有任何前端经验的我，开始了 Vibe Coding，大部分代码都是和 Cursor 对话后直接完成。最终，MoonTV 应运而生：</p>

    <a href="https://github.com/MoonTechLab/LunaTV" class="no-heti gc-container" target="_blank" rel="noopener noreferrer" data-repo="MoonTechLab/LunaTV">
      <div class="gc-title-bar">
        <div class="gc-owner-avatar" style="background-size: cover; background-position: center;" aria-hidden="true"></div>
        <span class="gc-repo-title">
          <span>MoonTechLab<span class="gc-slash" aria-hidden="true">/</span><strong>LunaTV</strong></span>
        </span>
        <svg class="gc-github-icon" width="24" height="24" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
          <path d="M12 1C5.9225 1 1 5.9225 1 12C1 16.8675 4.14875 20.9787 8.52125 22.4362C9.07125 22.5325 9.2775 22.2025 9.2775 21.9137C9.2775 21.6525 9.26375 20.7862 9.26375 19.865C6.5 20.3737 5.785 19.1912 5.565 18.5725C5.44125 18.2562 4.905 17.28 4.4375 17.0187C4.0525 16.8125 3.5025 16.3037 4.42375 16.29C5.29 16.2762 5.90875 17.0875 6.115 17.4175C7.105 19.0812 8.68625 18.6137 9.31875 18.325C9.415 17.61 9.70375 17.1287 10.02 16.8537C7.5725 16.5787 5.015 15.63 5.015 11.4225C5.015 10.2262 5.44125 9.23625 6.1425 8.46625C6.0325 8.19125 5.6475 7.06375 6.2525 5.55125C6.2525 5.55125 7.17375 5.2625 9.2775 6.67875C10.1575 6.43125 11.0925 6.3075 12.0275 6.3075C12.9625 6.3075 13.8975 6.43125 14.7775 6.67875C16.8813 5.24875 17.8025 5.55125 17.8025 5.55125C18.4075 7.06375 18.0225 8.19125 17.9125 8.46625C18.6138 9.23625 19.04 10.2125 19.04 11.4225C19.04 15.6437 16.4688 16.5787 14.0213 16.8537C14.42 17.1975 14.7638 17.8575 14.7638 18.8887C14.7638 20.36 14.75 21.5425 14.75 21.9137C14.75 22.2025 14.9563 22.5462 15.5063 22.4362C19.8513 20.9787 23 16.8537 23 12C23 5.9225 18.0775 1 12 1Z"></path>
        </svg>
      </div>
      <p class="gc-repo-description">Loading repository data...</p>
      <div class="gc-info-bar">
        <svg class="gc-info-icon" height="16" width="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
          <path d="M8 .25a.75.75 0 0 1 .673.418l1.882 3.815 4.21.612a.75.75 0 0 1 .416 1.279l-3.046 2.97.719 4.192a.751.751 0 0 1-1.088.791L8 12.347l-3.766 1.98a.75.75 0 0 1-1.088-.79l.72-4.194L.818 6.374a.75.75 0 0 1 .416-1.28l4.21-.611L7.327.668A.75.75 0 0 1 8 .25Zm0 2.445L6.615 5.5a.75.75 0 0 1-.564.41l-3.097.45 2.24 2.184a.75.75 0 0 1 .216.664l-.528 3.084 2.769-1.456a.75.75 0 0 1 .698 0l2.77 1.456-.53-3.084a.75.75 0 0 1 .216-.664l2.24-2.183-3.096-.45a.75.75 0 0 1-.564-.41L8 2.694Z"></path>
        </svg>
        <span class="gc-stars-count" aria-label="Stars count">--</span>
        <svg class="gc-info-icon" height="16" width="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
          <path d="M5 5.372v.878c0 .414.336.75.75.75h4.5a.75.75 0 0 0 .75-.75v-.878a2.25 2.25 0 1 1 1.5 0v.878a2.25 2.25 0 0 1-2.25 2.25h-1.5v2.128a2.251 2.251 0 1 1-1.5 0V8.5h-1.5A2.25 2.25 0 0 1 3.5 6.25v-.878a2.25 2.25 0 1 1 1.5 0ZM5 3.25a.75.75 0 1 0-1.5 0 .75.75 0 0 0 1.5 0Zm6.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Zm-3 8.75a.75.75 0 1 0-1.5 0 .75.75 0 0 0 1.5 0Z"></path>
        </svg>
        <span class="gc-forks-count" aria-label="Forks count">--</span>
        <svg class="gc-info-icon" height="16" width="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
          <path d="M8.75.75V2h.985c.304 0 .603.08.867.231l1.29.736c.038.022.08.033.124.033h2.234a.75.75 0 0 1 0 1.5h-.427l2.111 4.692a.75.75 0 0 1-.154.838l-.53-.53.529.531-.001.002-.002.002-.006.006-.006.005-.01.01-.045.04c-.21.176-.441.327-.686.45C14.556 10.78 13.88 11 13 11a4.498 4.498 0 0 1-2.023-.454 3.544 3.544 0 0 1-.686-.45l-.045-.04-.016-.015-.006-.006-.004-.004v-.001a.75.75 0 0 1-.154-.838L12.178 4.5h-.162c-.305 0-.604-.079-.868-.231l-1.29-.736a.245.245 0 0 0-.124-.033H8.75V13h2.5a.75.75 0 0 1 0 1.5h-6.5a.75.75 0 0 1 0-1.5h2.5V3.5h-.984a.245.245 0 0 0-.124.033l-1.289.737c-.265.15-.564.23-.869.23h-.162l2.112 4.692a.75.75 0 0 1-.154.838l-.53-.53.529.531-.001.002-.002.002-.006.006-.016.015-.045.04c-.21.176-.441.327-.686.45C4.556 10.78 3.88 11 3 11a4.498 4.498 0 0 1-2.023-.454 3.544 3.544 0 0 1-.686-.45l-.045-.04-.016-.015-.006-.006-.004-.004v-.001a.75.75 0 0 1-.154-.838L2.178 4.5H1.75a.75.75 0 0 1 0-1.5h2.234a.249.249 0 0 0 .125-.033l1.288-.737c.265-.15.564-.23.869-.23h.984V.75a.75.75 0 0 1 1.5 0Zm2.945 8.477c.285.135.718.273 1.305.273s1.02-.138 1.305-.273L13 6.327Zm-10 0c.285.135.718.273 1.305.273s1.02-.138 1.305-.273L3 6.327Z"></path>
        </svg>
        <span class="gc-license-info" aria-label="License">--</span>
      </div>
    </a>
    
<p>目前只过去了一个月，就已经有了 4.6K Star 和 5.4K Fork（出现不 star 的白嫖党），Star 趋势如下，足以见得国内对于 D 版视频的需求还是很大（全拜爱优腾的各种不做人的操作和那一堵高高的墙所赐）。
<a href="https://www.star-history.com/#MoonTechLab/LunaTV&#x26;Date" target="_blank" rel="noopener noreferrer" data-umami-event="outbound-link-click" data-umami-event-url="https://www.star-history.com/#MoonTechLab/LunaTV&#x26;Date"><img src="https://api.star-history.com/svg?repos=MoonTechLab/LunaTV&#x26;type=Date" alt="Star History Chart" loading="lazy" decoding="async"></a></p>
<p>项目基于 NextJS 和 React，支持 Vercel、Cloudflare Pages 和 Docker 部署，支持 localstorage、Redis 和 Cloudflare D1 三种存储方式，Redis 和 Cloudflare D1 存储时支持多账户信息隔离和跨浏览器信息同步，以及方便快捷的管理面板，后续还计划支持对接 Upstash。</p>
<p>这也是我第一次 Vibe Coding 尝试、第一次前端/全栈（全栈这个词似乎只有 node/JS 生态在使用）项目尝试和第一次（比较正经的）开源项目尝试。由于 Cursor 的加持，MoonTV 的大部分开发工作从 Code 变成了 Code Review。有什么需求只需要和 Cursor 聊一聊，就可以完成。当然这个聊一聊也是有一些技巧的。</p>
<p>本次 Vibe Coding 体验遇到的第一个困难就出现在体验的最开头。由于对前端一窍不通，对很多流行技术栈也只是有“只知其名不知其意”的了解程度。为了追新，我决定使用对 Serverless 友好的 NextJS 和 Tailwind CSS 作为项目的主体框架。于是，新建文件夹，我要求 AI 帮我初始化一个 NextJS 和 Tailwind 项目。然后反复折腾了半个下午，AI 初始化的项目都无法成功运行，基本都是样式丢失，页面看起来像是纯文本堆出来了，百思不得其解之下，最终放弃，在 github 上找了个脚手架项目作为基座，后续开发才得以正常进行。</p>
<p>正式的开发过程都挺顺利，AI 模型我是基本使用这两个：Claude Sonnet 4.0 和 GPT O3。整体使用下来，这两个模型偏好也很明显：Sonnet 4.0 适合对整体项目做大修改，有时候即使是修一点小问题，AI 也会尝试重构整个项目，更适合用来实现新功能。GPT O3 则更适合对项目做小修小补，它会控制修改在一个很小的范围内，不会无序地去修改整个项目的文件，更适合用来修补一些已有功能的 bug。</p>
<p>当然也有一些 AI 力所未逮的情况。在实现播放页时，由于状态转移复杂，很多数据之间存在依赖和关联，AI 实现的版本中，数据依赖几乎成了一个网状结构，一旦有一个数据变更，如切换集数或更换播放源，最终传递到播放器的就可能是重复的五六次初始化，导致播放器闪烁。试了很多模型都无法解决这个问题，最终亲自上手，梳理了数据依赖传播路径，才最终解决了重复初始化的问题。</p>
<p>另外一些 AI 无法解决的情况就比较小众了，比如 m3u8 的视频大部分播放器不会开启 airplay 或 chromecast 投屏功能，但实际上是可以通过一些小 trick 强制开启的。这个需求直接问 AI，要么直接告诉你不可能，要么就是给出一些错误的解决方案，甚至可能会告诉你 xxx 播放器可以要不我们换 xxx 播放器吧。最终搜遍全网，在 hls.js 的一个角落发现了<a href="https://github.com/video-dev/hls.js/issues/6482#issuecomment-2582666967" target="_blank" rel="noopener noreferrer" data-umami-event="outbound-link-click" data-umami-event-url="https://github.com/video-dev/hls.js/issues/6482#issuecomment-2582666967">这个 issue</a>，最终成功开启了 airplay。以 AI 目前的能力只能解决一些比较大众化的问题，过于小众或者甚至人类都无法解决的问题，暂时就不要指望 AI 了。</p>
<p>更大的困难其实来自于多平台多环境的适配，项目立项时的计划就是支持 vercel、CloudFlare pages 和 docker 部署，nextjs 原生就可以在 Vercel 上运行，只在 vercel 上倒是没有遇到什么困难。项目使用了一个 next-on-pages 依赖将代码翻译为 Cloudflare pages 代码，所以时常会出现一些兼容性问题。而对于 docker 的适配就更加复杂了，docker 下如接口处理只能跑在 node 环境下，但 nextjs 又保留了一个 edge 环境用于中间件处理（如鉴权等），这两个环境的内存也不是共享的，开发时需要严格地区分不同环境的依赖和可使用的 api。除此之外，客户端环境也是很复杂的一个情况，困难点主要来自于 webkit 内核，在 iOS 和 iPadOS 下，所有浏览器都只能使用 webkit 内核，而 webkit 内核的很多接口或 css 样式和 chromium 内核都有很大的差异，需要进行特殊的适配。</p>
<p>MoonTV 由于脱胎于 LibreTV，项目最初的宣传都是直接在 LibreTV 的交流群中进行的，项目专用的讨论群也是直接使用的 LibreTV 的讨论群。在主动宣传上基本只在 linux.do 论坛上进行，实际上也只是在有一些大更新的时候发一篇 changelog。到项目中后期发现很多科技博主也开始自发宣传起了 MoonTV：X 上关注的一些博主、TG 关注的科技频道，甚至 Youtube 和 Bilibili 以至于小红书上的很多 up 都开始主动教授起了 MoonTV 的搭建方式。</p>
<p>项目使用的人一多，各种 issue 就纷至沓来。issue 大概能分为如下两类：Bug report，这类 issue 会占大多数，写代码谁能不出点 bug 呢，更何况都是 AI 写的（转移矛盾）；Feature Request：FR 又可以分为两种，合理的需求 Request 是为了项目可以发展的更好，多是一些布局上、交互逻辑上或对接数据上的需求；最不受欢迎的就是商业化功能的 Request，这类人大概只是想着搭建这个项目后拿出去运营收费，但由于项目本身没有商业化功能，自己又没有能力实现，就跑去提 FR。这种人好逸恶劳只想着白嫖，自然是希望越少越好的。曾有个哥们一口气提了好多个类似的 issue，被 ban 了后还跑到 tg 群里继续纠缠，属实是讨厌可恶。还有一类：无效 bug，多是因为没有自己阅读 Readme 导致的部署失败，或者自身网络问题无法搜索，于是在 issue 中大呼小叫，遇到这类 issue，连讨论群群友路过都会吐口唾沫再走。</p>
<p>当然一个项目名声起来后，围绕这个项目产生的周边项目也会开始出现。MoonTV 就有一个 OrionTV 的客户端实现，专供 Android TV，可以直接使用 MoonTV 作为其后端，同步播放记录。交流群群友们也有各自实现的 Android 版和 iOS 版，当然由于各种限制 iOS 版是无法上架的。</p>

    <a href="https://github.com/zimplexing/oriontv" class="no-heti gc-container" target="_blank" rel="noopener noreferrer" data-repo="zimplexing/oriontv">
      <div class="gc-title-bar">
        <div class="gc-owner-avatar" style="background-size: cover; background-position: center;" aria-hidden="true"></div>
        <span class="gc-repo-title">
          <span>zimplexing<span class="gc-slash" aria-hidden="true">/</span><strong>oriontv</strong></span>
        </span>
        <svg class="gc-github-icon" width="24" height="24" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
          <path d="M12 1C5.9225 1 1 5.9225 1 12C1 16.8675 4.14875 20.9787 8.52125 22.4362C9.07125 22.5325 9.2775 22.2025 9.2775 21.9137C9.2775 21.6525 9.26375 20.7862 9.26375 19.865C6.5 20.3737 5.785 19.1912 5.565 18.5725C5.44125 18.2562 4.905 17.28 4.4375 17.0187C4.0525 16.8125 3.5025 16.3037 4.42375 16.29C5.29 16.2762 5.90875 17.0875 6.115 17.4175C7.105 19.0812 8.68625 18.6137 9.31875 18.325C9.415 17.61 9.70375 17.1287 10.02 16.8537C7.5725 16.5787 5.015 15.63 5.015 11.4225C5.015 10.2262 5.44125 9.23625 6.1425 8.46625C6.0325 8.19125 5.6475 7.06375 6.2525 5.55125C6.2525 5.55125 7.17375 5.2625 9.2775 6.67875C10.1575 6.43125 11.0925 6.3075 12.0275 6.3075C12.9625 6.3075 13.8975 6.43125 14.7775 6.67875C16.8813 5.24875 17.8025 5.55125 17.8025 5.55125C18.4075 7.06375 18.0225 8.19125 17.9125 8.46625C18.6138 9.23625 19.04 10.2125 19.04 11.4225C19.04 15.6437 16.4688 16.5787 14.0213 16.8537C14.42 17.1975 14.7638 17.8575 14.7638 18.8887C14.7638 20.36 14.75 21.5425 14.75 21.9137C14.75 22.2025 14.9563 22.5462 15.5063 22.4362C19.8513 20.9787 23 16.8537 23 12C23 5.9225 18.0775 1 12 1Z"></path>
        </svg>
      </div>
      <p class="gc-repo-description">Loading repository data...</p>
      <div class="gc-info-bar">
        <svg class="gc-info-icon" height="16" width="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
          <path d="M8 .25a.75.75 0 0 1 .673.418l1.882 3.815 4.21.612a.75.75 0 0 1 .416 1.279l-3.046 2.97.719 4.192a.751.751 0 0 1-1.088.791L8 12.347l-3.766 1.98a.75.75 0 0 1-1.088-.79l.72-4.194L.818 6.374a.75.75 0 0 1 .416-1.28l4.21-.611L7.327.668A.75.75 0 0 1 8 .25Zm0 2.445L6.615 5.5a.75.75 0 0 1-.564.41l-3.097.45 2.24 2.184a.75.75 0 0 1 .216.664l-.528 3.084 2.769-1.456a.75.75 0 0 1 .698 0l2.77 1.456-.53-3.084a.75.75 0 0 1 .216-.664l2.24-2.183-3.096-.45a.75.75 0 0 1-.564-.41L8 2.694Z"></path>
        </svg>
        <span class="gc-stars-count" aria-label="Stars count">--</span>
        <svg class="gc-info-icon" height="16" width="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
          <path d="M5 5.372v.878c0 .414.336.75.75.75h4.5a.75.75 0 0 0 .75-.75v-.878a2.25 2.25 0 1 1 1.5 0v.878a2.25 2.25 0 0 1-2.25 2.25h-1.5v2.128a2.251 2.251 0 1 1-1.5 0V8.5h-1.5A2.25 2.25 0 0 1 3.5 6.25v-.878a2.25 2.25 0 1 1 1.5 0ZM5 3.25a.75.75 0 1 0-1.5 0 .75.75 0 0 0 1.5 0Zm6.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Zm-3 8.75a.75.75 0 1 0-1.5 0 .75.75 0 0 0 1.5 0Z"></path>
        </svg>
        <span class="gc-forks-count" aria-label="Forks count">--</span>
        <svg class="gc-info-icon" height="16" width="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
          <path d="M8.75.75V2h.985c.304 0 .603.08.867.231l1.29.736c.038.022.08.033.124.033h2.234a.75.75 0 0 1 0 1.5h-.427l2.111 4.692a.75.75 0 0 1-.154.838l-.53-.53.529.531-.001.002-.002.002-.006.006-.006.005-.01.01-.045.04c-.21.176-.441.327-.686.45C14.556 10.78 13.88 11 13 11a4.498 4.498 0 0 1-2.023-.454 3.544 3.544 0 0 1-.686-.45l-.045-.04-.016-.015-.006-.006-.004-.004v-.001a.75.75 0 0 1-.154-.838L12.178 4.5h-.162c-.305 0-.604-.079-.868-.231l-1.29-.736a.245.245 0 0 0-.124-.033H8.75V13h2.5a.75.75 0 0 1 0 1.5h-6.5a.75.75 0 0 1 0-1.5h2.5V3.5h-.984a.245.245 0 0 0-.124.033l-1.289.737c-.265.15-.564.23-.869.23h-.162l2.112 4.692a.75.75 0 0 1-.154.838l-.53-.53.529.531-.001.002-.002.002-.006.006-.016.015-.045.04c-.21.176-.441.327-.686.45C4.556 10.78 3.88 11 3 11a4.498 4.498 0 0 1-2.023-.454 3.544 3.544 0 0 1-.686-.45l-.045-.04-.016-.015-.006-.006-.004-.004v-.001a.75.75 0 0 1-.154-.838L2.178 4.5H1.75a.75.75 0 0 1 0-1.5h2.234a.249.249 0 0 0 .125-.033l1.288-.737c.265-.15.564-.23.869-.23h.984V.75a.75.75 0 0 1 1.5 0Zm2.945 8.477c.285.135.718.273 1.305.273s1.02-.138 1.305-.273L13 6.327Zm-10 0c.285.135.718.273 1.305.273s1.02-.138 1.305-.273L3 6.327Z"></path>
        </svg>
        <span class="gc-license-info" aria-label="License">--</span>
      </div>
    </a>
    
<p>以上。这篇文章本来计划是 7 月处写完，那时 star 也仅有 2k，但一直拖延，毕竟写文章肯定没有写代码有趣。我这个人的特点就是如此，一旦手头有什么事情，就无时无刻不去想着，无论是吃饭还是睡觉。两三点想到一个 bug 爬起来修都是常有的事情，连续两周每天只睡了六个多小时，也算是夜以继日日以继夜了。</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[macOS 26 / iPadOS 26 尝鲜]]></title>
            <link>https://blog.shinya.click/fiddling/macos-26-trial</link>
            <guid isPermaLink="false">https://blog.shinya.click/fiddling/macos-26-trial</guid>
            <pubDate>Tue, 10 Jun 2025 12:18:00 GMT</pubDate>
            <description><![CDATA[macOS 和 iPadOS 26 带来了全新的 Liquid Glass 设计语言，界面风格焕然一新，图标和窗口效果更具现代感，尤其是在 iPad 上实现的窗口化应用模式，标志着其向生产力工具的转变。然而，部分设计如透明的控制中心和对启动台的整合引发了一些争议，用户体验仍需进一步优化。这次更新虽有不足，却为未来的发展奠定了基础，值得期待。]]></description>
            <content:encoded><![CDATA[<p>又是一年科技春晚 WWDC 25 在今日凌晨召开，众所周知科技以换皮为本，除了重点介绍了<del>国行用不到</del>国人不屑于使用的 Apple Intelligence 之外，最大的更新就是各平台系统发布了最新版并统一以年份命名，以及随之而来的 Liquid Glass 设计语言了。</p>
<p>原本我是不关心 WWDC 的，通常都是保守地等待正式版更新。但是今天早上铺天盖地的科技博主测评和新闻，和各种群嘲梗图，让我很难不关注，简单了解过后，决定更新下尝鲜。</p>
<figure><img src="https://blog-img.774352199.xyz/2025/10a1a04fe5272425d5df103b0403286b.png" alt="据说 Liquid Glass 的灵感来自于此" loading="lazy" decoding="async" style="background-image:url(data:image/webp;base64,UklGRoAAAABXRUJQVlA4IHQAAABQAgCdASoQABAAAsBMJYgCdAYt9SflXNl34IAA/uGrg2InWYEGU1admrZcI9hhH/pNHYswc1QyyAVc1yjIPZGWKGtAgDJdtz/u4Sj0MRVF5+bIkeHGz84jGQfEKmF1N2Fxeayw5ymALsbPxubmupl4BagAAA==);background-size:cover;background-repeat:no-repeat"><figcaption>据说 Liquid Glass 的灵感来自于此</figcaption></figure>
<p>由于我的 iPhone 已经卖掉了（见 <a href="/fiddling/one-month-using-android">上一篇</a>），所以这次体验范围为 macOS 和 iPadOS —— 这两个系统的吐槽似乎相对少一些。</p>
<p>首先最直观的感受当然也就是 UI 上的变化，本次多平台统一使用的设计语言 Liquid Glass，据说最早来自于 Vision OS。当然我的 Vision OS 也卖掉了，所以没法直接比较，不过直观感受上确实有一种 Vision OS 的风味，窗口都使用了毛玻璃效果，图标也统一改成了拟物（毛玻璃切片？姑且叫这个名字）风格。</p>
<figure><img src="https://blog-img.774352199.xyz/2025/a8cf24e6e65fb60e3b12459b13dd7b80.png" alt="说实话有一点山寨感，有点上古山寨机的风味了" loading="lazy" decoding="async" style="background-image:url(data:image/webp;base64,UklGRnQAAABXRUJQVlA4IGgAAAAwAgCdASoQAAsAAsBMJYwC7AdbPY6vFwdKAAD+7J5f6j/5e8qoP3AkwdO161ZSeOQ+maAvb3OT9oPh25J+PPQLrxg5jjRXa85zj5mKO7na4ApjWaf/1xenCr//PYs8TAABm4wAAScAAA==);background-size:cover;background-repeat:no-repeat"><figcaption>说实话有一点山寨感，有点上古山寨机的风味了</figcaption></figure>
<p>iPad 上图标统一倒是效果不错，第三方应用的图标也有做效果转换，看起来风格很统一。</p>
<figure><img src="https://blog-img.774352199.xyz/2025/7c1c0ec885660f91df41053898c0cd88.PNG" alt="iPad 图标" loading="lazy" decoding="async" style="background-image:url(data:image/webp;base64,UklGRoIAAABXRUJQVlA4IHYAAABQAgCdASoQAAsAAsBMJbACdAYubbPAMB7XggAA/upNmDNLavSi2i3ZiXdhGyNLvuacwqo8wsClL2VAQwRSmsQ7hJyfdNJycpB4kE0ENe98mYsRBrAn3S2xYLCtPMeacTC2Y7fchFyZwaSsBvqrTy53POqh3AAA);background-size:cover;background-repeat:no-repeat"><figcaption>iPad 图标</figcaption></figure>
<p>窗口也更换为了统一风格，另外还统一了 Apple Music 的视觉效果，看起来也更有科技感，不像之前 iTunes 风格，看起来就卡卡的（当然实际体验两说）。</p>
<figure><img src="https://blog-img.774352199.xyz/2025/7b73e36fb041c8dc5e7ff9aad0faeb57.png" alt="统一窗口算得上赏心悦目" loading="lazy" decoding="async" style="background-image:url(data:image/webp;base64,UklGRmAAAABXRUJQVlA4IFQAAADwAQCdASoQAAoAAsBMJZgCdADygfxzw4AA/vWyKYuKWpkHkl8I7Igc0wzdd/PpenpIuG9symeQhHrEIZE17kMcuc6ePtMeYRZ7CQxwdXaxHx9cAAA=);background-size:cover;background-repeat:no-repeat"><figcaption>统一窗口算得上赏心悦目</figcaption></figure>
<p>macOS 上被人喷得最多的，倒不是 Liquid Glass（UI 负反馈主要来自于 iPhone），而是启动台 LaunchPad 下线了，功能融合进了 SpotLight，直接点击 Dock 上的 App 按钮，和 Cmd + Space 效果相同，弹出 SpotLight 搜索框。</p>
<figure><img src="https://blog-img.774352199.xyz/2025/4b190a366a67d3f5dc14bea1491ebb92.png" alt="SpotLight" loading="lazy" decoding="async" style="background-image:url(data:image/webp;base64,UklGRkYAAABXRUJQVlA4IDoAAACQAQCdASoQAAsAAsBMJbGDrRgAiwAA/vFl69G24bLczLi+i7nynSzakVNGX+6cerFPU+ouMc24gAAA);background-size:cover;background-repeat:no-repeat"><figcaption>SpotLight</figcaption></figure>
<p>需要再次按 Cmd + 1 才会展示出全部 App。</p>
<figure><img src="https://blog-img.774352199.xyz/2025/9b74ffbd1b34ef15ef5b320135c653d7.png" alt="全部 App 页面" loading="lazy" decoding="async" style="background-image:url(data:image/webp;base64,UklGRowAAABXRUJQVlA4IIAAAAAwAgCdASoQAA0AAsBMJbACdAadCw3dImcmAAD+5Gp1bWpbD9Jh7USSszgcn7kClH8HAV9FOgP46g2K8JJNYozl2Hn+A85O8HaMEz/s/ga+8m5LDhLZ6fKxeG09yjLrQ37rYCbip6eu+7OgtKvN/1poUl2K4MpvbGOuAARcWsAkAA==);background-size:cover;background-repeat:no-repeat"><figcaption>全部 App 页面</figcaption></figure>
<p>这个设计就有点有病了，尤其是对于我这种老年痴呆症患者来说，App 的名字根本记不住，勉强看到图标才能想起来功能。不知道 LaunchPad 和 Apple 有什么仇。说不定过几天就有独立开发者开发出一个 LaunchPad 替代品，然后收我 99。</p>
<p>Quiz：相册 app 叫什么，相册还是图库？</p>
<p>答案：照片（搜了好几个都不对）</p>
<p>另外一个小更新就是点击键盘上调整音量按键和调整亮度按键时，不再会弹出全屏的提示，而是会在状态栏上展示一个小弹框，有点像触摸屏的逻辑。</p>
<img src="https://blog-img.774352199.xyz/2025/85841f6a20fe5bc87d2e4e7036e7e1ec.png" alt="音量控制" loading="lazy" decoding="async" style="background-image:url(data:image/webp;base64,UklGRkIAAABXRUJQVlA4IDYAAADwAQCdASoQAAQAAsBMJYwCdAEU91jErwAA/vGFcf8C6NWHeI1SdvuSUuLUdxkAoYcT26ZVUAA=);background-size:cover;background-repeat:no-repeat"><img src="https://blog-img.774352199.xyz/2025/c781da9ec7dfac96e7588a8faa047df5.png" alt="亮度控制" loading="lazy" decoding="async" style="background-image:url(data:image/webp;base64,UklGRkQAAABXRUJQVlA4IDgAAADQAQCdASoQAAQAAsBMJYwCdADdDMtWAAD+9dXLXt6Tpbkm9MeGahMRVpP9PajuMbrieH6dusAAAA==);background-size:cover;background-repeat:no-repeat">
<p>因为没有 iPhone，最经典的 Liquid Glass 我只能在 iPad 上体验了。我的 iPad 是 iPad Pro 11 英寸，第四代，使用的是 M2 芯片。</p>
<p>一些浮夸的锁屏时钟可以拉长之类的功能就不体验了，聚焦点主要在通知栏和控制中心。通知栏下拉的时候有实时的玻璃特效，仿佛是真的在拖动一个玻璃一般，效果很惊艳，就是不知道是否有性能要求，老 iPad 能不能轻松跑起来。</p>
<figure><img src="https://blog-img.774352199.xyz/2025/acbdd751ce6b1ca0c76301c826274aea.PNG" alt="注意边界效果" loading="lazy" decoding="async" style="background-image:url(data:image/webp;base64,UklGRnIAAABXRUJQVlA4IGYAAAAQAgCdASoQAAsAAsBMJbACdAYtfbHnWxrEAP7tIMcl63eQqh5cbsg1zdtylHdyBou9q0svJWT1Wr3nc28aV94znOxnQSMKNGHjsOFoMzNdTJv6CR8NFIEX49DP4HmVI643NZckQAA=);background-size:cover;background-repeat:no-repeat"><figcaption>注意边界效果</figcaption></figure>
<p>下面就是被人吐槽最多的控制中心了，透明度太高导致辨识度很低，估计后续版本可能会调整，或者允许用户自定义。</p>
<figure><img src="https://blog-img.774352199.xyz/2025/e4cd043a2db8acf7c8a5d12f3c90065e.PNG" alt="是有些看不清楚的" loading="lazy" decoding="async" style="background-image:url(data:image/webp;base64,UklGRoAAAABXRUJQVlA4IHQAAABQAgCdASoQAAsAAsBMJbACdAYubbPAMAaIlAAA/upNu2qV4DCZzRUdVsjWXhm27sqlRVXH6W0zqSkiOBO+qLIqr+cohmJDz3a0QQC0Qvq166QY0ugVG4GSjqXdykbBsT9yEXJnBpKwG+qtPQtbP2ZDt8AAAA==);background-size:cover;background-repeat:no-repeat"><figcaption>是有些看不清楚的</figcaption></figure>
<p>当然，除了一无是处，这次更新还是有一些可取之处的。最大的亮点就是 iPadOS 更新的窗口化 App 模式。老版本的 iPad App 只能全屏幕或台前调度，屏幕上只能保留一个 App，或者使用分屏进行一些简单的多任务操作。窗口化 App 的操作逻辑则完全和 macOS 一致了，可以随意调整窗口大小、最大化、最小化，窗口互相覆盖等等。如果外接大屏，完全可以当作电脑使用，算是 iPad 成为真正的生产力工具的重要一步。</p>
<figure><img src="https://blog-img.774352199.xyz/2025/4846d5e04b7811a0435573f43d26dfec.PNG" alt="不黑不吹，这是真的 Legendary" loading="lazy" decoding="async" style="background-image:url(data:image/webp;base64,UklGRngAAABXRUJQVlA4IGwAAAAQAgCdASoQAAkAAsBMJbACdAELYzYH7kcAAPl/bw6t4to23ibamZ9PKG4IU2iFSl6lPvsuPnKerEqXES5tbr5nMuN1bW4/b5nL66Tu0KTv3+EIVSaQ/gSCXCTLbir0wUuCyucsLP3fzIwAAAA=);background-size:cover;background-repeat:no-repeat"><figcaption>不黑不吹，这是真的 Legendary</figcaption></figure>
<p>这些就是我对本次更新的一些直观感受了，说实话我还是很看好这个设计语言的，Windows Vista 受限于当时的机能未能实现的未来，终于被 Apple 实现了。一些比较严重的问题和吐槽，在后续的更新中应该会得到解决。当前只是 macOS 26 / iPadOS 26 的第一个开发者预览版，未来可期。</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[迁移安卓一月谈]]></title>
            <link>https://blog.shinya.click/fiddling/one-month-using-android</link>
            <guid isPermaLink="false">https://blog.shinya.click/fiddling/one-month-using-android</guid>
            <pubDate>Thu, 05 Jun 2025 15:26:00 GMT</pubDate>
            <description><![CDATA[换机频繁的我，经历了从一加到 iPhone 的转变，逐渐从折腾的乐趣转向了对生态的依赖。最近在 npy 的建议下，入手了 Oppo Find X8 Ultra，主要是为了提升拍照体验。尽管沉浸在 Apple 的生态中，但 App 的迁移问题让我意识到，Android 的应用生态依然参差不齐，寻找替代品的过程颇具挑战。这个月的迁移经历让我感受到不同平台之间的摩擦与适应。]]></description>
            <content:encoded><![CDATA[<h3 id="前言">前言<a href="#前言" class="heading-anchor-link" aria-label="Link to 前言"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h3>
<p>我是一个换机很频繁的人，尤其是工作有了一些积蓄后，更是养成了一点“喜新厌旧”的坏毛病，一部手机在我手上很难待过一年。一旦有新机发售，就开始盘算着买一部做主力机如何如何，现有的软件/工具链如何迁移过去，想着想着就开始手痒，于是下单换机，旧机或是退给家人使用，或是卖二手，或是留在身边当备用机，然后被慢慢遗忘。</p>
<p>毕业后这四年起，仅仅主力机就有这些：一加 8T、一加 9Pro、Pixel 5、Oppo Find X6 Pro、Vivo X Fold3 Pro、iPhone 14 Pro、iPhone 16 Pro，另外还有一部一加 7Pro 作为备用机。</p>
<p>从换机历程，大概能看出我玩机心态的变化 —— 最初的一加系、pixel，早年刚毕业的时候喜欢折腾，类原生、root、解 bootloader，怎么自由怎么来；到 oppo、vivo，逐渐放弃了折腾，但还是保留了一些玩机的心态，如 vivo 的折叠屏；再到 iPhone，彻底投入 Apple 阵营，和 Apple 生态的深度融合。</p>
<p>正沉溺 Apple 构筑的生态链中不愿自拔时，npy 给了我当头一棒：“你这拍照也太垃圾了”。也是，早年玩机，主要关注跑分性能之类的纸面参数，摄像头嘛，能扫码就行。现在，我要向她证明：</p>
<blockquote>
<p>一切手法上的不足，都可以通过软硬件不足！</p>
</blockquote>
<p>当然这又是我换机的一个借口罢了，事实是，在上次 <a href="/travels/kansai-202504">关西行</a> 中，K 君的相机确实引起了我很大的兴趣，尤其是那大长焦。但我天生惫懒，不愿后期修图，那就不如买一部镜头和调色稍微好些的<mark>手机</mark>，来满足一下我的拍照瘾吧。</p>
<p>恰好上半年御三家都发布了各自的 Ultra 超大杯：Vivo X200 Ultra、Oppo Find X8 Ultra、小米 15 Ultra。首先排除小米（<del>雷军！金凡！</del>），vivo 的长焦增距确实很吸引我，简直演唱会神器，但鉴于我抢不到演唱会的门票，最终还是入手了主摄有一英寸大底的 Oppo Find X8 Ultra。截止目前已经换机有一个月了，迁移最主要的麻烦就是从 Apple 生态链中迁移过来，尤其是很多 App 开发者只开发 iOS 版，Android 版的 App 生态又是良莠不齐，寻找平替花了不少时间。由于无法解锁 BootLoader，root 就成了奢望，很多功能只能委屈一下了。</p>
<h3 id="去广告">去广告<a href="#去广告" class="heading-anchor-link" aria-label="Link to 去广告"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h3>
<p>更换到国产安卓生态的最大问题，就是无处不在的广告。上手第一步，去广告：</p>
<iframe src="//player.bilibili.com/player.html?isOutside=true&aid=113746622021969&bvid=BV18c6JYLEmw&cid=27626637570&p=1&autoplay=0" scrolling="no" border="0" frameborder="no" framespacing="0" allowfullscreen="true"></iframe>
<p>完成后，就只剩下天气应用的二级页面下方的推荐广告去不掉了，和牛皮癣一样。</p>
<h3 id="照片备份">照片备份<a href="#照片备份" class="heading-anchor-link" aria-label="Link to 照片备份"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h3>
<p>此前由于使用的完整的苹果生态，所有的照片都在 iCloud 上，为此也开了 iCloud 200G 扩容，专用于存储照片，目前使用了 50G，包含了从本科开始的所有照片。前些日子把 NAS 更换为了飞牛系统，这些照片又同步备份了一份到飞牛自带的相册。</p>
<p>这次切换到安卓，品牌方自带的云肯定是不用的（说的就是你，欢太云），更换到 Google 相册的话，有比较麻烦。去年花了不少时间把相册从 Google Photos 导出到 iCloud，现在又导回去算个什么事情？另外 Google 云空间不开会员只有 15GB，虽然用校友邮箱薅了 Google AI Pro 的 2T 羊毛，但心里总是不踏实。</p>
<p>由于我的其他设备还是苹果，最终决定还是继续使用 iCloud 存储。Mac 上安装一个 O+ 互联，每天打开电脑是自动连接，就是需要手动导出再导入到 Apple 相册。说实话 O+ 互联的体验比 V 家的办公套件要差很多，不仅 Bug 多，功能还不完善：剪贴板无法同步、不支持 Mac 控制手机、无法自动同步照片，看 Oppo 那边也摆烂了，更新也不怎么勤快，凑活着用吧。</p>
<h3 id="代理回家">代理/回家<a href="#代理回家" class="heading-anchor-link" aria-label="Link to 代理/回家"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h3>
<p>我的手机和电脑上都安装了代理软件，主要就是用于回家，即访问家里的网络环境，原因有二：</p>
<ol>
<li>访问家里部署的服务，比如上面提到的相册和笔记服务</li>
<li>家中部署了 DNS 去广告和基于规则的透明代理，只要能连接回家，就无须再配置复杂的规则，享受和在家里一样的网络环境</li>
</ol>
<p>之前使用 iPhone 时，Surge 我是 24 小时挂在后台的，全局回家。但如果人在家中时，还挂着全局连接家中的代理，会导致上不了网 Surge 有个很重要的功能，叫子网覆盖，可以根据当前连接的 WIFI SSID，执行一些动作，比如 SUSPEND 全局绕过代理。这样就完美解决了家中挂代理无法上网的问题，Surge 可以全程保持在后台了。</p>
<p>切换到安卓阵营，首先面临的问题就是选择太多，不同于 iOS 大部分都是自己实现的代理核心，安卓大都是直接复用开源的核心，如 mihomo（clash）、v2ray、xray 或比较新的 singbox，基于这些核心封装 UI 界面进而封装客户端，因此<del>大部分</del>几乎所有客户端都无法在 UI 上直接调整规则配置，只能通过上传或订阅配置文件。这倒是件小事，基于我的需求，配置文件一旦写成便几乎不会再变动。比较麻烦的是，很难找到一个客户端支持类似于 Surge 的子网覆盖功能，我可不想离家回家都得手动开关一次代理。</p>
<p>最终找到了一个比较小众的 SurfBoard，支持 SSID 规则，直接在规则最顶端增加一条 SSID 规则走 DIRECT，也算曲线救国实现了功能。更神奇的是，这个 App 兼容 Surge 格式的配置文件。</p>

    <a href="https://github.com/getsurfboard/surfboard" class="no-heti gc-container" target="_blank" rel="noopener noreferrer" data-repo="getsurfboard/surfboard">
      <div class="gc-title-bar">
        <div class="gc-owner-avatar" style="background-size: cover; background-position: center;" aria-hidden="true"></div>
        <span class="gc-repo-title">
          <span>getsurfboard<span class="gc-slash" aria-hidden="true">/</span><strong>surfboard</strong></span>
        </span>
        <svg class="gc-github-icon" width="24" height="24" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
          <path d="M12 1C5.9225 1 1 5.9225 1 12C1 16.8675 4.14875 20.9787 8.52125 22.4362C9.07125 22.5325 9.2775 22.2025 9.2775 21.9137C9.2775 21.6525 9.26375 20.7862 9.26375 19.865C6.5 20.3737 5.785 19.1912 5.565 18.5725C5.44125 18.2562 4.905 17.28 4.4375 17.0187C4.0525 16.8125 3.5025 16.3037 4.42375 16.29C5.29 16.2762 5.90875 17.0875 6.115 17.4175C7.105 19.0812 8.68625 18.6137 9.31875 18.325C9.415 17.61 9.70375 17.1287 10.02 16.8537C7.5725 16.5787 5.015 15.63 5.015 11.4225C5.015 10.2262 5.44125 9.23625 6.1425 8.46625C6.0325 8.19125 5.6475 7.06375 6.2525 5.55125C6.2525 5.55125 7.17375 5.2625 9.2775 6.67875C10.1575 6.43125 11.0925 6.3075 12.0275 6.3075C12.9625 6.3075 13.8975 6.43125 14.7775 6.67875C16.8813 5.24875 17.8025 5.55125 17.8025 5.55125C18.4075 7.06375 18.0225 8.19125 17.9125 8.46625C18.6138 9.23625 19.04 10.2125 19.04 11.4225C19.04 15.6437 16.4688 16.5787 14.0213 16.8537C14.42 17.1975 14.7638 17.8575 14.7638 18.8887C14.7638 20.36 14.75 21.5425 14.75 21.9137C14.75 22.2025 14.9563 22.5462 15.5063 22.4362C19.8513 20.9787 23 16.8537 23 12C23 5.9225 18.0775 1 12 1Z"></path>
        </svg>
      </div>
      <p class="gc-repo-description">Loading repository data...</p>
      <div class="gc-info-bar">
        <svg class="gc-info-icon" height="16" width="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
          <path d="M8 .25a.75.75 0 0 1 .673.418l1.882 3.815 4.21.612a.75.75 0 0 1 .416 1.279l-3.046 2.97.719 4.192a.751.751 0 0 1-1.088.791L8 12.347l-3.766 1.98a.75.75 0 0 1-1.088-.79l.72-4.194L.818 6.374a.75.75 0 0 1 .416-1.28l4.21-.611L7.327.668A.75.75 0 0 1 8 .25Zm0 2.445L6.615 5.5a.75.75 0 0 1-.564.41l-3.097.45 2.24 2.184a.75.75 0 0 1 .216.664l-.528 3.084 2.769-1.456a.75.75 0 0 1 .698 0l2.77 1.456-.53-3.084a.75.75 0 0 1 .216-.664l2.24-2.183-3.096-.45a.75.75 0 0 1-.564-.41L8 2.694Z"></path>
        </svg>
        <span class="gc-stars-count" aria-label="Stars count">--</span>
        <svg class="gc-info-icon" height="16" width="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
          <path d="M5 5.372v.878c0 .414.336.75.75.75h4.5a.75.75 0 0 0 .75-.75v-.878a2.25 2.25 0 1 1 1.5 0v.878a2.25 2.25 0 0 1-2.25 2.25h-1.5v2.128a2.251 2.251 0 1 1-1.5 0V8.5h-1.5A2.25 2.25 0 0 1 3.5 6.25v-.878a2.25 2.25 0 1 1 1.5 0ZM5 3.25a.75.75 0 1 0-1.5 0 .75.75 0 0 0 1.5 0Zm6.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Zm-3 8.75a.75.75 0 1 0-1.5 0 .75.75 0 0 0 1.5 0Z"></path>
        </svg>
        <span class="gc-forks-count" aria-label="Forks count">--</span>
        <svg class="gc-info-icon" height="16" width="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
          <path d="M8.75.75V2h.985c.304 0 .603.08.867.231l1.29.736c.038.022.08.033.124.033h2.234a.75.75 0 0 1 0 1.5h-.427l2.111 4.692a.75.75 0 0 1-.154.838l-.53-.53.529.531-.001.002-.002.002-.006.006-.006.005-.01.01-.045.04c-.21.176-.441.327-.686.45C14.556 10.78 13.88 11 13 11a4.498 4.498 0 0 1-2.023-.454 3.544 3.544 0 0 1-.686-.45l-.045-.04-.016-.015-.006-.006-.004-.004v-.001a.75.75 0 0 1-.154-.838L12.178 4.5h-.162c-.305 0-.604-.079-.868-.231l-1.29-.736a.245.245 0 0 0-.124-.033H8.75V13h2.5a.75.75 0 0 1 0 1.5h-6.5a.75.75 0 0 1 0-1.5h2.5V3.5h-.984a.245.245 0 0 0-.124.033l-1.289.737c-.265.15-.564.23-.869.23h-.162l2.112 4.692a.75.75 0 0 1-.154.838l-.53-.53.529.531-.001.002-.002.002-.006.006-.016.015-.045.04c-.21.176-.441.327-.686.45C4.556 10.78 3.88 11 3 11a4.498 4.498 0 0 1-2.023-.454 3.544 3.544 0 0 1-.686-.45l-.045-.04-.016-.015-.006-.006-.004-.004v-.001a.75.75 0 0 1-.154-.838L2.178 4.5H1.75a.75.75 0 0 1 0-1.5h2.234a.249.249 0 0 0 .125-.033l1.288-.737c.265-.15.564-.23.869-.23h.984V.75a.75.75 0 0 1 1.5 0Zm2.945 8.477c.285.135.718.273 1.305.273s1.02-.138 1.305-.273L13 6.327Zm-10 0c.285.135.718.273 1.305.273s1.02-.138 1.305-.273L3 6.327Z"></path>
        </svg>
        <span class="gc-license-info" aria-label="License">--</span>
      </div>
    </a>
    
<p>虽然 SurfBoard 默认是 FakeIP 无法修改，即使走 SSID 规则请求也会首先解析成 FakeIP，不如全局绕过来得更加彻底，但是聊胜于无吧。</p>
<h3 id="记账">记账<a href="#记账" class="heading-anchor-link" aria-label="Link to 记账"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h3>
<p>换机之前恰好养成了记账的习惯，至今记账也两月有余了，除了日常用钱时的记账之外，还会每天对账记录理财收益。iOS 上有个记账软件很推荐，叫 iCost，界面简洁但功能丰富，支持将快捷记账添加为系统捷径，可以通过敲击两下背板或者 iPhone 16 的快捷按钮唤醒，自动 AI 识屏并填写主要信息，全程只需要填一个分类即可。iCost 也有开发安卓端的规划，但是规划了两年多，进度缓慢。</p>
<p>小红书上搞记账软件的也很多，国人独立开发三大件：笔记、TODO 和记账。最终找到了一个比较老牌的记账软件：钱迹，开发时间似乎比 iCost 还长，并且支持多平台同步：iOS、MacOS、Android 甚至 Windows、鸿蒙 Next 都有支持，历史账单也可以很快捷地从 iCost 导入。该有的功能也一个不拉：资产管理、信用卡还款、退款、、多币种、多账本（虽然这个我用不到）。放个 <a href="https://qianjiapp.com" target="_blank" rel="noopener noreferrer" data-umami-event="outbound-link-click" data-umami-event-url="https://qianjiapp.com">主页</a>。</p>
<p>钱迹的自动记账感觉比 iCost 要好，不过或许是平台不同带来的差异。钱迹的自动记账基于的是系统的无障碍功能，在识别到当前打开的窗口是账单页面（支付宝或者微信）时，就会自动识别并弹出小窗快速记账。不过界面识别的准确度有待提升，有时从其他软件跳转支付宝付款完成的界面识别会失败，只能手动打开支付宝的支付记录中再识别一次，不过小瑕疵，能接受。</p>
<h3 id="日历todo">日历/TODO<a href="#日历todo" class="heading-anchor-link" aria-label="Link to 日历/TODO"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h3>
<p>iOS 18 最大的更新，就是打通了日历和提醒事项，在日历上可以直观查看和操作所有提醒事项，得益于 iOS 优秀的通知推送能力，提醒也十分及时，几乎算得上是 iOS 上最提升生产力的小工具了，比不少付费软件做的都好。切换到安卓后，首先排除的就是系统自带日历，完全不具备可迁移性，下一次换机一旦换一个品牌，数据就要全丢失了。</p>
<p>找来找去，中途微软的 Outlook 比较符合要求。作为一个邮件 app，日历功能做的出奇得好用。就是无法显示农历，只能通过订阅一些农历日历来展示，这样每天都会显示一些日程，由于 Outlook 本地化做的也不咋的，传统节日、节假日调休一概没有，又得多订阅一些日程，再加上农历生日、纪念日，整个日历一眼望去满满当当，眼花缭乱，哪一天真的有事情要做都没法直观看到。另外 Todo 功能薄弱，只能达到凑活用的水平。</p>
<p>无意中想到一个老牌的 Todo 软件：滴答清单，于是下载下来体验了一番，没想到就给我找到了 iOS 日历 + Todo 的最佳平替。上面提到的所有功能都直接支持：农历显示、农历日程、节假日调休、倒数日/纪念日、Todo 展示。不仅如此，我在 iOS Todo 上还得手动划分列表支持的四象限法，滴答清单直接原生支持，意外之喜啊。就是 139 元一年的会员价格有些不美了，不过这是我的问题。</p>
<h3 id="watch">Watch<a href="#watch" class="heading-anchor-link" aria-label="Link to Watch"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h3>
<p>切换到安卓生态后，终于摆脱了美丽废物 —— Apple Watch，终于不用一天充一次电了。用了 Oppo 的手机，自然要用 Oppo 的手表。购入了 Oppo Watch X2，几乎和一部全新的 Apple Watch 差不多贵了。</p>
<p>Oppo Watch X2 采用了现在智能手表比较少的圆形表盘，表盘样式也多是拟物主题，令人眼前一亮。我对手表的要求很低，能看时间、同步手机通知、同步手机闹钟、记录睡眠数据和其他健康数据即可。这款手表都能完美地解决，另外比较惊喜的是 HRV，这个在 Apple Watch 上基本被忽略掉、大部分情况下只能通过第三方 app 来展示提示的数据，在 Oppo 上得到了空前的重视，有一个专门的身心状态页面，还请来了孙颖莎代言。</p>
<h3 id="尾声">尾声<a href="#尾声" class="heading-anchor-link" aria-label="Link to 尾声"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h3>
<p>折腾了那么多，终于是有一部用着舒服的手机了，拍照也得到了 npy 的认可，完美~</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Astro 接入 Google Analytics (Tag Manager)]]></title>
            <link>https://blog.shinya.click/fiddling/astro-google-tag-manager</link>
            <guid isPermaLink="false">https://blog.shinya.click/fiddling/astro-google-tag-manager</guid>
            <pubDate>Wed, 28 May 2025 14:09:00 GMT</pubDate>
            <description><![CDATA[在将博客迁移至 Astro 框架后，常规的 Google Analytics 接入方法因性能因素而不再适用。尽管可以在 head 标签中直接添加 js 代码进行事件上报，这会影响页面性能。为了保持 Astro 的高效表现，采用了 partytown 技术，将脚本从主线程中剥离，确保加载过程不受影响。在此基础上，结合 demo 代码成功实现了 Google Analytics 的无缝接入，达成了性能与数据分析的平衡。]]></description>
            <content:encoded><![CDATA[<h3 id="前言">前言<a href="#前言" class="heading-anchor-link" aria-label="Link to 前言"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h3>
<p>不想看废话可直接跳转解决方案一节</p>
<p>此前一直在 Google Analytics 上看博客的访问量，分析各种 referer 等信息。使用 hexo 和 hugo 等静态博客框架，都可以很容易的接入 Google Analytics，直接在 head 标签中添加 js 代码即可。前些日子将博客迁移到了 Astro 框架，诚然可以用老办法，直接在 head 执行 js 代码上报事件，但是就会造成性能下降。众所周知，Astro 追求极致前端性能、尽量 0 JS 执行。一旦使用 js 上报事件，就会造成性能损失</p>
<figure><img src="https://blog-img.774352199.xyz/2025/e1e778992ea6b393ed763a8642db3770.png" alt="性能满昏！" loading="lazy" decoding="async" style="background-image:url(data:image/webp;base64,UklGRkYAAABXRUJQVlA4IDoAAACQAQCdASoQAAwAAsBMJZwAAiI8+4AA/vbYX2jkTyNTGHwCh0s+I4nZD5A/KhvpnpQojc2ue3hZn8AA);background-size:cover;background-repeat:no-repeat"><figcaption>性能满昏！</figcaption></figure>
<p>于是网上冲浪了一番，搜到的大部分教程是使用 partytown 来将脚本剥离主线程执行，不阻塞主线程加载，以保证性能，并都给出了 demo 代码。于是在给出的 demo 基础上，我将其嵌入了我的博客。于是就得到了以下结果：</p>
<p><img src="https://blog-img.774352199.xyz/2025/e5005b9f2321f6946761eef52156e777.png" alt="" loading="lazy" decoding="async" style="background-image:url(data:image/webp;base64,UklGRjIAAABXRUJQVlA4ICYAAADQAQCdASoQAAgAAsBMJaQAAu0ejnJwAAD++YUuDSlxdzanO46AAA==);background-size:cover;background-repeat:no-repeat"></p>
<p>上报直接掉零了</p>
<p>很难绷，于是开始了漫长的排查，始终没能找到问题所在。网上找到的所有相关例子都和我的方式相同。都有些奇怪，这些人写完教程都不自己测试一下吗，完全不可用啊</p>
<p>只得将其搁置了俩月，期间临时改用了 umami 统计数据</p>
<p>这两天又想了起来，如芒在背如鲠在喉，于是又开始了漫长的搜索。最终在 GitHub 的一个 <a href="https://github.com/QwikDev/partytown/issues/382#issuecomment-1667675238" target="_blank" rel="noopener noreferrer" data-umami-event="outbound-link-click" data-umami-event-url="https://github.com/QwikDev/partytown/issues/382#issuecomment-1667675238">角落</a> 中找到了解决方案</p>
<h3 id="解决方案">解决方案<a href="#解决方案" class="heading-anchor-link" aria-label="Link to 解决方案"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h3>
<p>安装 <code>@astrojs/partytown</code>，直接使用你的包管理器安装即可</p>
<p><code>&#x3C;head></code> 标签中添加以下代码</p>
<div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">&#x3C;</span><span style="color:#22863A;--s-dark:#85E89D">script</span><span style="color:#6F42C1;--s-dark:#B392F0"> is:inline</span><span style="color:#6F42C1;--s-dark:#B392F0"> src</span><span style="color:#24292E;--s-dark:#E1E4E8">=</span><span style="color:#032F62;--s-dark:#9ECBFF">"https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXXX"</span><span style="color:#6F42C1;--s-dark:#B392F0"> type</span><span style="color:#24292E;--s-dark:#E1E4E8">=</span><span style="color:#032F62;--s-dark:#9ECBFF">"text/partytown"</span><span style="color:#24292E;--s-dark:#E1E4E8">>&#x3C;/</span><span style="color:#22863A;--s-dark:#85E89D">script</span><span style="color:#24292E;--s-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">&#x3C;</span><span style="color:#22863A;--s-dark:#85E89D">script</span><span style="color:#6F42C1;--s-dark:#B392F0"> is:inline</span><span style="color:#6F42C1;--s-dark:#B392F0"> type</span><span style="color:#24292E;--s-dark:#E1E4E8">=</span><span style="color:#032F62;--s-dark:#9ECBFF">"text/partytown"</span><span style="color:#24292E;--s-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">  window.dataLayer = window.dataLayer || [];</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">  window.gtag = function () {</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        dataLayer.push(arguments);</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    };</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">  window.gtag('js', new Date());</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">  window.gtag('config', 'G-XXXXXXXXX');</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">&#x3C;/</span><span style="color:#22863A;--s-dark:#85E89D">script</span><span style="color:#24292E;--s-dark:#E1E4E8">></span></span></code></pre></div>
<p>有以下几个注意点：</p>
<ul>
<li><code>is:inline</code> 指示脚本在客户端执行</li>
<li><code>type="text/partytown"</code> 指示脚本由 partytown 执行，而不在主线程执行</li>
<li><code>gtag</code> 函数必须定义为 window 对象的一个函数变量，不能定义为一个函数声明（很奇怪）</li>
</ul>
<p>在 astro 的配置文件（形如是 <code>astro.config.ts</code> 或 <code>astro.config.mjs</code>）中添加以下配置</p>
<div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--s-dark:#F97583">import</span><span style="color:#24292E;--s-dark:#E1E4E8"> partytown </span><span style="color:#D73A49;--s-dark:#F97583">from</span><span style="color:#032F62;--s-dark:#9ECBFF"> '@astrojs/partytown'</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">export</span><span style="color:#D73A49;--s-dark:#F97583"> default</span><span style="color:#6F42C1;--s-dark:#B392F0"> defineConfig</span><span style="color:#24292E;--s-dark:#E1E4E8">({</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">  // ...</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">  integrations: [</span><span style="color:#6F42C1;--s-dark:#B392F0">partytown</span><span style="color:#24292E;--s-dark:#E1E4E8">({ config: { forward: [</span><span style="color:#032F62;--s-dark:#9ECBFF">'dataLayer.push'</span><span style="color:#24292E;--s-dark:#E1E4E8">, </span><span style="color:#032F62;--s-dark:#9ECBFF">'gtag'</span><span style="color:#24292E;--s-dark:#E1E4E8">] } })],</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">});</span></span></code></pre></div>
<p>大部分教程都没有提到还需要将 <code>gtag</code> 加入 forward 数组</p>
<p>完成！提交部署后，Google Analytics 的统计数据就可以正常上报了</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[上海杭州异地组网记录]]></title>
            <link>https://blog.shinya.click/fiddling/cross-city-network-setup</link>
            <guid isPermaLink="false">https://blog.shinya.click/fiddling/cross-city-network-setup</guid>
            <pubDate>Fri, 18 Apr 2025 08:43:12 GMT</pubDate>
            <description><![CDATA[随着工作调动，npy 从北京迁至上海，顺便处理了宽带安装。尽管选择了电信500M的宽带，费用却高于杭州的1000M，令人无奈。在此背景下，我决定构建一个跨城市的网络环境，期望实现上海透明代理、部分流量从杭州出口及两地局域网互通的目标。杭州的网络架构相对简单，通过软路由与无线AP的组合，搭建了一套可以支持日常流量回家的系统，准备迎接接下来的网络挑战。]]></description>
            <content:encoded><![CDATA[<h3 id="前言">前言<a href="#前言" class="heading-anchor-link" aria-label="Link to 前言"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h3>
<p>这个月 npy 因为工作原因，从北京搬到了上海</p>
<p>帮忙搬家之外，还帮着开了一条宽带，电信 500M</p>
<p>顺便吐槽下上海电信，500M 的宽带比杭州电信 1000M 宽带还贵</p>
<p>~~你这光纤是金子做的还是光猫是金子做的？~~</p>
<p>作为一名 Geek（自封），不折腾是不可能的，合计了一下，预期要达成以下成果</p>
<ol>
<li>上海透明代理越墙</li>
<li>上海部分流量从杭州出口</li>
<li>两地局域网互访</li>
</ol>
<p>第二条的原因是目前 Netflix 等流媒体解锁是走的机场（此处可插入 aff），这个机场限制了使用用户，多地同时使用可能会导致封号，这种情况下就只能将流量转发到杭州，从杭州统一出口</p>
<h3 id="前置准备">前置准备<a href="#前置准备" class="heading-anchor-link" aria-label="Link to 前置准备"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h3>
<p>目前杭州的网络拓扑很简单，光猫拖一个软路由拨号，软路由接一个 TP-Link 做无线 AP。同时有个小主机直连软路由，开了一个 shadowsocks server 用于日常流量回家。软路由是倍控的 G30S（此处可插入广告），N5100 也是够用的，系统是 ImmortalWRT。装了以下插件</p>
<ul>
<li>AdguardHome：DNS 去广告</li>
<li>Nikki：Mihomo 内核，做透明代理</li>
<li>Tailscale：虚拟局域网组网，此前一直用作流量回家的备案</li>
<li>DDNS-Go：如你所见，就是个 DDNS</li>
</ul>
<p>LAN 网段为 192.168.7.0/24</p>
<p>为了保持设备一致，降低一点复杂度，于是又购置了一台相同配置的 G30S，也安装了 ImmortalWRT。同时上海的网络拓扑计划与杭州保持一致。光猫 - 软路由 - AP</p>
<h3 id="安装">安装<a href="#安装" class="heading-anchor-link" aria-label="Link to 安装"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h3>
<p>ImmortalWRT 的安装得提一嘴，最初以为它的安装和 windows 或者其他 linux 发行版类似，镜像是个 livecd 或者安装器之类的，写入 u 盘启动后执行安装。没想到 u 盘启动后竟然就这么启动了……</p>
<p>启动了……</p>
<p>合着那个 img 直接就是系统镜像啊</p>
<p>于是麻溜改刷 winpe，用 physdiskwrite 把镜像写到硬盘，成功启动，简单配置完成了上网和 dhcp。上海的 LAN  网段为 192.168.10.0/24</p>
<p>接着就是扩容，直接写盘安装的 openwrt，可用空间只有不到 1G，硬盘的其他剩余空间完全浪费掉了。这时候第二个困难就出现了：我找不到何时的扩容教程。简中的教程错综复杂、错误百出、一个抄一个，但实际上，要么就是 squashfs 的扩容，要么就是用剩余空间新建一个分区，将根分区改挂到新分区上这样的教程，几乎找不到真正的“扩容”分区</p>
<p>最后在 OpenWRT 的 <a href="https://openwrt.org/docs/guide-user/advanced/expand_root" target="_blank" rel="noopener noreferrer" data-umami-event="outbound-link-click" data-umami-event-url="https://openwrt.org/docs/guide-user/advanced/expand_root">官方文档</a> 中找到了……以后还是不用中文搜索了，Google 都搜不出啥有用的</p>
<p>先解决越墙和流量出口的问题，这个直接 clash 就能解决</p>
<p><a href="https://github.com/nikkinikki-org/OpenWrt-nikki" target="_blank" rel="noopener noreferrer" data-umami-event="outbound-link-click" data-umami-event-url="https://github.com/nikkinikki-org/OpenWrt-nikki">Nikki</a> 是一个 OpenWRT 插件，基于 Mihomo 内核进行透明代理。相对于其他类似功能的插件如 Passwall 或者 clash，Nikki 的可定制化更强。注意安装要根据 Readme 中的 Instruction 安装，直接从 Releases 下载 ipk 是无法安装的（很奇怪）</p>

    <a href="https://github.com/nikkinikki-org/OpenWrt-nikki" class="no-heti gc-container" target="_blank" rel="noopener noreferrer" data-repo="nikkinikki-org/OpenWrt-nikki">
      <div class="gc-title-bar">
        <div class="gc-owner-avatar" style="background-size: cover; background-position: center;" aria-hidden="true"></div>
        <span class="gc-repo-title">
          <span>nikkinikki-org<span class="gc-slash" aria-hidden="true">/</span><strong>OpenWrt-nikki</strong></span>
        </span>
        <svg class="gc-github-icon" width="24" height="24" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
          <path d="M12 1C5.9225 1 1 5.9225 1 12C1 16.8675 4.14875 20.9787 8.52125 22.4362C9.07125 22.5325 9.2775 22.2025 9.2775 21.9137C9.2775 21.6525 9.26375 20.7862 9.26375 19.865C6.5 20.3737 5.785 19.1912 5.565 18.5725C5.44125 18.2562 4.905 17.28 4.4375 17.0187C4.0525 16.8125 3.5025 16.3037 4.42375 16.29C5.29 16.2762 5.90875 17.0875 6.115 17.4175C7.105 19.0812 8.68625 18.6137 9.31875 18.325C9.415 17.61 9.70375 17.1287 10.02 16.8537C7.5725 16.5787 5.015 15.63 5.015 11.4225C5.015 10.2262 5.44125 9.23625 6.1425 8.46625C6.0325 8.19125 5.6475 7.06375 6.2525 5.55125C6.2525 5.55125 7.17375 5.2625 9.2775 6.67875C10.1575 6.43125 11.0925 6.3075 12.0275 6.3075C12.9625 6.3075 13.8975 6.43125 14.7775 6.67875C16.8813 5.24875 17.8025 5.55125 17.8025 5.55125C18.4075 7.06375 18.0225 8.19125 17.9125 8.46625C18.6138 9.23625 19.04 10.2125 19.04 11.4225C19.04 15.6437 16.4688 16.5787 14.0213 16.8537C14.42 17.1975 14.7638 17.8575 14.7638 18.8887C14.7638 20.36 14.75 21.5425 14.75 21.9137C14.75 22.2025 14.9563 22.5462 15.5063 22.4362C19.8513 20.9787 23 16.8537 23 12C23 5.9225 18.0775 1 12 1Z"></path>
        </svg>
      </div>
      <p class="gc-repo-description">Loading repository data...</p>
      <div class="gc-info-bar">
        <svg class="gc-info-icon" height="16" width="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
          <path d="M8 .25a.75.75 0 0 1 .673.418l1.882 3.815 4.21.612a.75.75 0 0 1 .416 1.279l-3.046 2.97.719 4.192a.751.751 0 0 1-1.088.791L8 12.347l-3.766 1.98a.75.75 0 0 1-1.088-.79l.72-4.194L.818 6.374a.75.75 0 0 1 .416-1.28l4.21-.611L7.327.668A.75.75 0 0 1 8 .25Zm0 2.445L6.615 5.5a.75.75 0 0 1-.564.41l-3.097.45 2.24 2.184a.75.75 0 0 1 .216.664l-.528 3.084 2.769-1.456a.75.75 0 0 1 .698 0l2.77 1.456-.53-3.084a.75.75 0 0 1 .216-.664l2.24-2.183-3.096-.45a.75.75 0 0 1-.564-.41L8 2.694Z"></path>
        </svg>
        <span class="gc-stars-count" aria-label="Stars count">--</span>
        <svg class="gc-info-icon" height="16" width="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
          <path d="M5 5.372v.878c0 .414.336.75.75.75h4.5a.75.75 0 0 0 .75-.75v-.878a2.25 2.25 0 1 1 1.5 0v.878a2.25 2.25 0 0 1-2.25 2.25h-1.5v2.128a2.251 2.251 0 1 1-1.5 0V8.5h-1.5A2.25 2.25 0 0 1 3.5 6.25v-.878a2.25 2.25 0 1 1 1.5 0ZM5 3.25a.75.75 0 1 0-1.5 0 .75.75 0 0 0 1.5 0Zm6.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Zm-3 8.75a.75.75 0 1 0-1.5 0 .75.75 0 0 0 1.5 0Z"></path>
        </svg>
        <span class="gc-forks-count" aria-label="Forks count">--</span>
        <svg class="gc-info-icon" height="16" width="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
          <path d="M8.75.75V2h.985c.304 0 .603.08.867.231l1.29.736c.038.022.08.033.124.033h2.234a.75.75 0 0 1 0 1.5h-.427l2.111 4.692a.75.75 0 0 1-.154.838l-.53-.53.529.531-.001.002-.002.002-.006.006-.006.005-.01.01-.045.04c-.21.176-.441.327-.686.45C14.556 10.78 13.88 11 13 11a4.498 4.498 0 0 1-2.023-.454 3.544 3.544 0 0 1-.686-.45l-.045-.04-.016-.015-.006-.006-.004-.004v-.001a.75.75 0 0 1-.154-.838L12.178 4.5h-.162c-.305 0-.604-.079-.868-.231l-1.29-.736a.245.245 0 0 0-.124-.033H8.75V13h2.5a.75.75 0 0 1 0 1.5h-6.5a.75.75 0 0 1 0-1.5h2.5V3.5h-.984a.245.245 0 0 0-.124.033l-1.289.737c-.265.15-.564.23-.869.23h-.162l2.112 4.692a.75.75 0 0 1-.154.838l-.53-.53.529.531-.001.002-.002.002-.006.006-.016.015-.045.04c-.21.176-.441.327-.686.45C4.556 10.78 3.88 11 3 11a4.498 4.498 0 0 1-2.023-.454 3.544 3.544 0 0 1-.686-.45l-.045-.04-.016-.015-.006-.006-.004-.004v-.001a.75.75 0 0 1-.154-.838L2.178 4.5H1.75a.75.75 0 0 1 0-1.5h2.234a.249.249 0 0 0 .125-.033l1.288-.737c.265-.15.564-.23.869-.23h.984V.75a.75.75 0 0 1 1.5 0Zm2.945 8.477c.285.135.718.273 1.305.273s1.02-.138 1.305-.273L13 6.327Zm-10 0c.285.135.718.273 1.305.273s1.02-.138 1.305-.273L3 6.327Z"></path>
        </svg>
        <span class="gc-license-info" aria-label="License">--</span>
      </div>
    </a>
    
<p>安装完成后导入配置文件，就可以启动了。有几个注意点：</p>
<ol>
<li>可以直接开启 tun 模式，会自动配置路由器的路由表做透明代理，基本没什么问题</li>
<li>mihomo 的域名分流需要 dns 请求通过 mihomo 发起，这时有两个选择：mihomo 的 dns 直接 hijack 53 端口，openwrt 自带的 dnsmasq 可以改为监听其他端口；或者 dnsmasq 设置一个转发，将 dns 请求都转发到 mihomo 的 dns 端口</li>
<li>如果使用 FakeIP 模式，注意仔细设置 FakeIP Filter</li>
</ol>
<p>其他就是一些老生长谈，和其他平台使用 mihomo 基本一致，不再赘述</p>
<p>配置完成后开启 Nikki，在 Dashboard 中就能看到 mihomo 开始接管路由器的所有出口流量了</p>
<p>配置文件基本是直接从我这儿粘过去的，大部分越墙流量都是向搬瓦工🪜直接发起连接，一些需要流量从杭州出口的规则，outbound 都改为了杭州的 shadowsocks server，试了下延迟还可以，就当是走了一道国内中转</p>
<p>到这一步，其实通过域名的话已经可以实现上海访问杭州的内网，由于是 FakeIP 模式，dns 解析后返回的 fakeip 请求也会被 mihomo 内核处理，再通过域名规则将流量转到杭州即可。但是这样的话，直接的 ip 请求，不通过 dns，就无法通过 mihomo 转发（tun 默认不接管局域网请求），且杭州也没法访问到上海</p>
<p>于是，TailScale 堂堂登场！</p>
<p>OpenWRT 使用 TailScale 可以用 <a href="https://github.com/asvow/luci-app-tailscale" target="_blank" rel="noopener noreferrer" data-umami-event="outbound-link-click" data-umami-event-url="https://github.com/asvow/luci-app-tailscale">luci-app-tailscale</a> 插件，可以比较直观地管理和配置 tailscale</p>

    <a href="https://github.com/asvow/luci-app-tailscale" class="no-heti gc-container" target="_blank" rel="noopener noreferrer" data-repo="asvow/luci-app-tailscale">
      <div class="gc-title-bar">
        <div class="gc-owner-avatar" style="background-size: cover; background-position: center;" aria-hidden="true"></div>
        <span class="gc-repo-title">
          <span>asvow<span class="gc-slash" aria-hidden="true">/</span><strong>luci-app-tailscale</strong></span>
        </span>
        <svg class="gc-github-icon" width="24" height="24" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
          <path d="M12 1C5.9225 1 1 5.9225 1 12C1 16.8675 4.14875 20.9787 8.52125 22.4362C9.07125 22.5325 9.2775 22.2025 9.2775 21.9137C9.2775 21.6525 9.26375 20.7862 9.26375 19.865C6.5 20.3737 5.785 19.1912 5.565 18.5725C5.44125 18.2562 4.905 17.28 4.4375 17.0187C4.0525 16.8125 3.5025 16.3037 4.42375 16.29C5.29 16.2762 5.90875 17.0875 6.115 17.4175C7.105 19.0812 8.68625 18.6137 9.31875 18.325C9.415 17.61 9.70375 17.1287 10.02 16.8537C7.5725 16.5787 5.015 15.63 5.015 11.4225C5.015 10.2262 5.44125 9.23625 6.1425 8.46625C6.0325 8.19125 5.6475 7.06375 6.2525 5.55125C6.2525 5.55125 7.17375 5.2625 9.2775 6.67875C10.1575 6.43125 11.0925 6.3075 12.0275 6.3075C12.9625 6.3075 13.8975 6.43125 14.7775 6.67875C16.8813 5.24875 17.8025 5.55125 17.8025 5.55125C18.4075 7.06375 18.0225 8.19125 17.9125 8.46625C18.6138 9.23625 19.04 10.2125 19.04 11.4225C19.04 15.6437 16.4688 16.5787 14.0213 16.8537C14.42 17.1975 14.7638 17.8575 14.7638 18.8887C14.7638 20.36 14.75 21.5425 14.75 21.9137C14.75 22.2025 14.9563 22.5462 15.5063 22.4362C19.8513 20.9787 23 16.8537 23 12C23 5.9225 18.0775 1 12 1Z"></path>
        </svg>
      </div>
      <p class="gc-repo-description">Loading repository data...</p>
      <div class="gc-info-bar">
        <svg class="gc-info-icon" height="16" width="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
          <path d="M8 .25a.75.75 0 0 1 .673.418l1.882 3.815 4.21.612a.75.75 0 0 1 .416 1.279l-3.046 2.97.719 4.192a.751.751 0 0 1-1.088.791L8 12.347l-3.766 1.98a.75.75 0 0 1-1.088-.79l.72-4.194L.818 6.374a.75.75 0 0 1 .416-1.28l4.21-.611L7.327.668A.75.75 0 0 1 8 .25Zm0 2.445L6.615 5.5a.75.75 0 0 1-.564.41l-3.097.45 2.24 2.184a.75.75 0 0 1 .216.664l-.528 3.084 2.769-1.456a.75.75 0 0 1 .698 0l2.77 1.456-.53-3.084a.75.75 0 0 1 .216-.664l2.24-2.183-3.096-.45a.75.75 0 0 1-.564-.41L8 2.694Z"></path>
        </svg>
        <span class="gc-stars-count" aria-label="Stars count">--</span>
        <svg class="gc-info-icon" height="16" width="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
          <path d="M5 5.372v.878c0 .414.336.75.75.75h4.5a.75.75 0 0 0 .75-.75v-.878a2.25 2.25 0 1 1 1.5 0v.878a2.25 2.25 0 0 1-2.25 2.25h-1.5v2.128a2.251 2.251 0 1 1-1.5 0V8.5h-1.5A2.25 2.25 0 0 1 3.5 6.25v-.878a2.25 2.25 0 1 1 1.5 0ZM5 3.25a.75.75 0 1 0-1.5 0 .75.75 0 0 0 1.5 0Zm6.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Zm-3 8.75a.75.75 0 1 0-1.5 0 .75.75 0 0 0 1.5 0Z"></path>
        </svg>
        <span class="gc-forks-count" aria-label="Forks count">--</span>
        <svg class="gc-info-icon" height="16" width="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
          <path d="M8.75.75V2h.985c.304 0 .603.08.867.231l1.29.736c.038.022.08.033.124.033h2.234a.75.75 0 0 1 0 1.5h-.427l2.111 4.692a.75.75 0 0 1-.154.838l-.53-.53.529.531-.001.002-.002.002-.006.006-.006.005-.01.01-.045.04c-.21.176-.441.327-.686.45C14.556 10.78 13.88 11 13 11a4.498 4.498 0 0 1-2.023-.454 3.544 3.544 0 0 1-.686-.45l-.045-.04-.016-.015-.006-.006-.004-.004v-.001a.75.75 0 0 1-.154-.838L12.178 4.5h-.162c-.305 0-.604-.079-.868-.231l-1.29-.736a.245.245 0 0 0-.124-.033H8.75V13h2.5a.75.75 0 0 1 0 1.5h-6.5a.75.75 0 0 1 0-1.5h2.5V3.5h-.984a.245.245 0 0 0-.124.033l-1.289.737c-.265.15-.564.23-.869.23h-.162l2.112 4.692a.75.75 0 0 1-.154.838l-.53-.53.529.531-.001.002-.002.002-.006.006-.016.015-.045.04c-.21.176-.441.327-.686.45C4.556 10.78 3.88 11 3 11a4.498 4.498 0 0 1-2.023-.454 3.544 3.544 0 0 1-.686-.45l-.045-.04-.016-.015-.006-.006-.004-.004v-.001a.75.75 0 0 1-.154-.838L2.178 4.5H1.75a.75.75 0 0 1 0-1.5h2.234a.249.249 0 0 0 .125-.033l1.288-.737c.265-.15.564-.23.869-.23h.984V.75a.75.75 0 0 1 1.5 0Zm2.945 8.477c.285.135.718.273 1.305.273s1.02-.138 1.305-.273L13 6.327Zm-10 0c.285.135.718.273 1.305.273s1.02-.138 1.305-.273L3 6.327Z"></path>
        </svg>
        <span class="gc-license-info" aria-label="License">--</span>
      </div>
    </a>
    
<p>安装完成后首先启动一下，完成登录认证，再进行配置，最好在 tailscale 关闭这个设备的密钥过期</p>
<p>高级设置中</p>
<ul>
<li>勾选启用路由：tailscale 自动帮我们配置到其他子网的路由规则</li>
<li>不勾选允许 dns：只需要路由 ip 请求即可</li>
<li>公开网段填写 192.168.10.0/24：告诉 tailscale 这台设备可以进行这个网段的路由</li>
<li>勾选子网互通：字面意思</li>
<li>子网路由选择 192.168.7.0/24：表示这个子网范围的路由由 tailscale 接管</li>
</ul>
<p>注意，杭州的 tailscale 也要做对应的设置，注意公开网段和子网路由要用对应的网段</p>
<p>设置完成后重启一下 tailscale，就可以完成子网互通了。在上海和杭州都可以直接通过对方网段的 IP 直接访问对方网段的主机</p>
<h3 id="后记">后记<a href="#后记" class="heading-anchor-link" aria-label="Link to 后记"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h3>
<p>上海的宽带安装的是一个什么云宽带，关闭云宽带并且开启桥接的流程非常麻烦（至今没走完）</p>
<p>所以以上配置都是在上海的软路由没有公网 IP 的情况下进行的</p>
<p>好在由于杭州这边有公网 IP，tailscale 也是可以直接打洞成功，延时只有十几毫秒。如果两边都没有公网 IP，那就只能自求多福了，要么就自建 derp 转发，要么就只能顶着大几百的延时凑活着用了</p>
<p>还是想吐槽下上海电信，开个桥接又要签协议又要拍照又要审核的，和防贼似的</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[My heart beats for U —— 心率同步 Grafana 展示]]></title>
            <link>https://blog.shinya.click/fiddling/heart-rate-to-grafana</link>
            <guid isPermaLink="false">https://blog.shinya.click/fiddling/heart-rate-to-grafana</guid>
            <pubDate>Mon, 31 Mar 2025 15:51:00 GMT</pubDate>
            <description><![CDATA[通过将苹果健康的心率数据定时同步到服务器，使用 Grafana 进行可视化展示，创造了一种直观的健康监测方式。利用 Health Auto Export 应用的 Restful API，将心率信息发送到指定的 http 接口，存储在 InfluxDB 中，最终在 Grafana 中呈现出清晰的看板，便于追踪和分析个人的心率变化。]]></description>
            <content:encoded><![CDATA[<p>整了个小活：把苹果健康的心率定时同步到服务器上，并由 Grafana 绘制展示，效果大概如下：</p>
<p><img src="https://blog-img.774352199.xyz/2025/e01807e95f9c8ea4384d2c4d8f4fe3cb.png" alt="" loading="lazy" decoding="async" style="background-image:url(data:image/webp;base64,UklGRigAAABXRUJQVlA4IBwAAABQAQCdASoQAAYAAsBMJZwABDOAAP70iCXMgAAA);background-size:cover;background-repeat:no-repeat"></p>
<p><del>可以点击博客右上角的 ♥️ 标志查看，因为套了 Cloudflare Tunnel，国内访问速度不佳，尽量挂🪜访问。</del>已下线，由于更换了 Oppo 手机，心率无法同步上传了</p>
<p>大概的思路就是使用这个 app <a href="https://apps.apple.com/us/app/health-auto-export-json-csv/id1115567069?l=zh-Hans-CN" target="_blank" rel="noopener noreferrer" data-umami-event="outbound-link-click" data-umami-event-url="https://apps.apple.com/us/app/health-auto-export-json-csv/id1115567069?l=zh-Hans-CN">Health Auto Export - JSON+CSV</a> 的 Restful API 的功能，设置定时将心率数据发送到部署的 http 接口以写入 InfluxDB，再由 Grafana 连接 InfluxDB 绘制看板</p>
<p><a href="https://apps.apple.com/us/app/health-auto-export-json-csv/id1115567069?l=zh-Hans-CN" target="_blank" rel="noopener noreferrer" data-umami-event="outbound-link-click" data-umami-event-url="https://apps.apple.com/us/app/health-auto-export-json-csv/id1115567069?l=zh-Hans-CN">Health Auto Export - JSON+CSV</a> 这个 app 如果想要定时同步，需要开通 Premium，Lifetime 美区是 24.99 USD，有点小贵，但似乎也没有什么平替方案</p>
<p>订阅后新建一个 Automation：</p>
<ul>
<li>Automation Type 为 <code>REST API</code>​</li>
<li>URL 就是你下面要部署的服务的地址，API 路径为 <code>/push/heart_rate</code>​</li>
<li>Data Type 选择 <code>Health Metrics</code>​</li>
<li>Select Health Metrics 勾选 <code>Heart Rate</code>​</li>
<li>Export Format 选择 JSON</li>
<li>Sync Cadence 可以选择 1 分钟也可以选择 5 分钟，Apple Watch 并不会一直监测心率</li>
</ul>
<p>勾上 Enable 即可，为了保证在程序退出后还能同步，可以添加一个桌面小组件</p>
<p>接着就是部署服务开放 Restful API 端口，接收数据并写入 InfluxDB 了。部署 InfluxDB 可以自行 Google 不再赘述，注意服务用的是 InfluxDB 2</p>
<p>服务的源码在 <a href="https://github.com/reekystive/healthkit-collector" target="_blank" rel="noopener noreferrer" data-umami-event="outbound-link-click" data-umami-event-url="https://github.com/reekystive/healthkit-collector">reekystive/healthkit-collector</a>，是个 node 项目，可以直接用 pnpm 启动监听 3000 端口。我写了个 Dockerfile 将其打包成 docker 镜像，部署在家里的服务器上</p>
<div class="code-collapse-wrapper"><input type="checkbox" id="toggle-efj9o4d" class="code-collapse-toggle" style="display: none;"><div class="code-collapse-preview"><div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0" class="code-preview"><code><span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">FROM node:</span><span style="color:#005CC5;--s-dark:#79B8FF">20</span><span style="color:#D73A49;--s-dark:#F97583">-</span><span style="color:#24292E;--s-dark:#E1E4E8">alpine AS builder</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8"># Install pnpm</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">RUN corepack enable </span><span style="color:#D73A49;--s-dark:#F97583">&#x26;&#x26;</span><span style="color:#24292E;--s-dark:#E1E4E8"> corepack prepare pnpm@</span><span style="color:#B31D28;--s-light-font-style:italic;--s-dark:#FDAEB7;--s-dark-font-style:italic">9.14.2</span><span style="color:#D73A49;--s-dark:#F97583"> --</span><span style="color:#24292E;--s-dark:#E1E4E8">activate</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8"># Set working directory</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">WORKDIR </span><span style="color:#D73A49;--s-dark:#F97583">/</span><span style="color:#24292E;--s-dark:#E1E4E8">app</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8"># Copy package.json and pnpm</span><span style="color:#D73A49;--s-dark:#F97583">-</span><span style="color:#24292E;--s-dark:#E1E4E8">lock.yaml</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">COPY package.json pnpm</span><span style="color:#D73A49;--s-dark:#F97583">-</span><span style="color:#24292E;--s-dark:#E1E4E8">lock.yaml</span><span style="color:#D73A49;--s-dark:#F97583">*</span><span style="color:#24292E;--s-dark:#E1E4E8"> .</span><span style="color:#D73A49;--s-dark:#F97583">/</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8"># Install dependencies</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">RUN pnpm install </span><span style="color:#D73A49;--s-dark:#F97583">--</span><span style="color:#24292E;--s-dark:#E1E4E8">frozen</span><span style="color:#D73A49;--s-dark:#F97583">-</span><span style="color:#24292E;--s-dark:#E1E4E8">lockfile</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8"># Copy source code</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">COPY . .</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8"># Build the application</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">RUN pnpm build</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8"># Stage </span><span style="color:#005CC5;--s-dark:#79B8FF">2</span><span style="color:#24292E;--s-dark:#E1E4E8">: Production stage</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">FROM node:</span><span style="color:#005CC5;--s-dark:#79B8FF">20</span><span style="color:#D73A49;--s-dark:#F97583">-</span><span style="color:#24292E;--s-dark:#E1E4E8">alpine AS production</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8"># Install pnpm</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">RUN corepack enable </span><span style="color:#D73A49;--s-dark:#F97583">&#x26;&#x26;</span><span style="color:#24292E;--s-dark:#E1E4E8"> corepack prepare pnpm@</span><span style="color:#B31D28;--s-light-font-style:italic;--s-dark:#FDAEB7;--s-dark-font-style:italic">9.14.2</span><span style="color:#D73A49;--s-dark:#F97583"> --</span><span style="color:#24292E;--s-dark:#E1E4E8">activate</span></span>
</code></pre></div><div class="code-collapse-gradient"></div><label class="code-collapse-expand" for="toggle-efj9o4d"><svg class="code-collapse-icon" width="32" height="24" viewBox="0 0 32 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="4,9 12,17 20,9"></polyline></svg></label></div><div class="code-collapse-full"><div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">FROM node:</span><span style="color:#005CC5;--s-dark:#79B8FF">20</span><span style="color:#D73A49;--s-dark:#F97583">-</span><span style="color:#24292E;--s-dark:#E1E4E8">alpine AS builder</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8"># Install pnpm</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">RUN corepack enable </span><span style="color:#D73A49;--s-dark:#F97583">&#x26;&#x26;</span><span style="color:#24292E;--s-dark:#E1E4E8"> corepack prepare pnpm@</span><span style="color:#B31D28;--s-light-font-style:italic;--s-dark:#FDAEB7;--s-dark-font-style:italic">9.14.2</span><span style="color:#D73A49;--s-dark:#F97583"> --</span><span style="color:#24292E;--s-dark:#E1E4E8">activate</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8"># Set working directory</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">WORKDIR </span><span style="color:#D73A49;--s-dark:#F97583">/</span><span style="color:#24292E;--s-dark:#E1E4E8">app</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8"># Copy package.json and pnpm</span><span style="color:#D73A49;--s-dark:#F97583">-</span><span style="color:#24292E;--s-dark:#E1E4E8">lock.yaml</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">COPY package.json pnpm</span><span style="color:#D73A49;--s-dark:#F97583">-</span><span style="color:#24292E;--s-dark:#E1E4E8">lock.yaml</span><span style="color:#D73A49;--s-dark:#F97583">*</span><span style="color:#24292E;--s-dark:#E1E4E8"> .</span><span style="color:#D73A49;--s-dark:#F97583">/</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8"># Install dependencies</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">RUN pnpm install </span><span style="color:#D73A49;--s-dark:#F97583">--</span><span style="color:#24292E;--s-dark:#E1E4E8">frozen</span><span style="color:#D73A49;--s-dark:#F97583">-</span><span style="color:#24292E;--s-dark:#E1E4E8">lockfile</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8"># Copy source code</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">COPY . .</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8"># Build the application</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">RUN pnpm build</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8"># Stage </span><span style="color:#005CC5;--s-dark:#79B8FF">2</span><span style="color:#24292E;--s-dark:#E1E4E8">: Production stage</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">FROM node:</span><span style="color:#005CC5;--s-dark:#79B8FF">20</span><span style="color:#D73A49;--s-dark:#F97583">-</span><span style="color:#24292E;--s-dark:#E1E4E8">alpine AS production</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8"># Install pnpm</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">RUN corepack enable </span><span style="color:#D73A49;--s-dark:#F97583">&#x26;&#x26;</span><span style="color:#24292E;--s-dark:#E1E4E8"> corepack prepare pnpm@</span><span style="color:#B31D28;--s-light-font-style:italic;--s-dark:#FDAEB7;--s-dark-font-style:italic">9.14.2</span><span style="color:#D73A49;--s-dark:#F97583"> --</span><span style="color:#24292E;--s-dark:#E1E4E8">activate</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8"># Set working directory</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">WORKDIR </span><span style="color:#D73A49;--s-dark:#F97583">/</span><span style="color:#24292E;--s-dark:#E1E4E8">app</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8"># Copy package.json and pnpm</span><span style="color:#D73A49;--s-dark:#F97583">-</span><span style="color:#24292E;--s-dark:#E1E4E8">lock.yaml</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">COPY package.json pnpm</span><span style="color:#D73A49;--s-dark:#F97583">-</span><span style="color:#24292E;--s-dark:#E1E4E8">lock.yaml</span><span style="color:#D73A49;--s-dark:#F97583">*</span><span style="color:#24292E;--s-dark:#E1E4E8"> .</span><span style="color:#D73A49;--s-dark:#F97583">/</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8"># Install production dependencies only</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">RUN pnpm install </span><span style="color:#D73A49;--s-dark:#F97583">--</span><span style="color:#24292E;--s-dark:#E1E4E8">prod </span><span style="color:#D73A49;--s-dark:#F97583">--</span><span style="color:#24292E;--s-dark:#E1E4E8">frozen</span><span style="color:#D73A49;--s-dark:#F97583">-</span><span style="color:#24292E;--s-dark:#E1E4E8">lockfile</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8"># Copy built application from builder stage</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">COPY </span><span style="color:#D73A49;--s-dark:#F97583">--</span><span style="color:#24292E;--s-dark:#E1E4E8">from</span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#24292E;--s-dark:#E1E4E8">builder </span><span style="color:#D73A49;--s-dark:#F97583">/</span><span style="color:#24292E;--s-dark:#E1E4E8">app</span><span style="color:#D73A49;--s-dark:#F97583">/</span><span style="color:#24292E;--s-dark:#E1E4E8">dist .</span><span style="color:#D73A49;--s-dark:#F97583">/</span><span style="color:#24292E;--s-dark:#E1E4E8">dist</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8"># Set environment variables</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8"># These are </span><span style="color:#D73A49;--s-dark:#F97583">default</span><span style="color:#24292E;--s-dark:#E1E4E8"> values that can be overridden when running the container</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">ENV NODE_ENV</span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#24292E;--s-dark:#E1E4E8">production</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">ENV PORT</span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#005CC5;--s-dark:#79B8FF">3000</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8"># Expose the port your app runs on (using the PORT environment variable)</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">EXPOSE ${PORT}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8"># Command to run the application</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">CMD [</span><span style="color:#032F62;--s-dark:#9ECBFF">"node"</span><span style="color:#24292E;--s-dark:#E1E4E8">, </span><span style="color:#032F62;--s-dark:#9ECBFF">"dist/index.js"</span><span style="color:#24292E;--s-dark:#E1E4E8">]</span></span></code><label class="code-collapse-collapse" for="toggle-efj9o4d"><svg class="code-collapse-icon" width="32" height="24" viewBox="0 0 32 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20,15 12,7 4,15"></polyline></svg></label></pre></div></div></div>
<p>启动时需要设置四个连接 InfluxDB 使用的环境变量</p>
<div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre><code>INFLUXDB_TOKEN='your_influxdb_token'
INFLUXDB_URL='your_influxdb_url'
INFLUXDB_ORG='your_influxdb_org'
INFLUXDB_BUCKET='your_influxdb_bucket'
</code></pre></div>
<p>部署完成后可以尝试进行一次同步，服务会输出写入 DB 成功的 log</p>
<p>最后就是部署个 Grafana 绘制仪表盘了，添加好 Data Source，新建仪表盘，查询语句如下</p>
<div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre><code>from(bucket: "bpm")
  |> range(start: v.timeRangeStart, stop: v.timeRangeStop)
  |> filter(fn: (r) => r["_measurement"] == "heart_rate")
  |> filter(fn: (r) => r["_field"] == "avg" or r["_field"] == "max" or r["_field"] == "min")
</code></pre></div>
<p>Enjoy！</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[语法分析中类型名-变量名歧义消除]]></title>
            <link>https://blog.shinya.click/fiddling/parser-type-variable-ambiguity</link>
            <guid isPermaLink="false">https://blog.shinya.click/fiddling/parser-type-variable-ambiguity</guid>
            <pubDate>Sat, 15 Mar 2025 12:35:00 GMT</pubDate>
            <description><![CDATA[在语法分析过程中，用户自定义类型名与普通变量名的混淆成为一大难题。特别是在某些情况下，如 `a*b;`，该语句既可能被解读为数学表达式，也可能被视为类型声明。这种模糊性源于语法规则的设计，特别是涉及类型说明符的部分，可能会导致变量名被误认作类型名，影响代码的正确性与可读性。随着无初始化变量定义的普遍存在，这一问题在源码中愈发明显。]]></description>
            <content:encoded><![CDATA[<h3 id="引">引<a href="#引" class="heading-anchor-link" aria-label="Link to 引"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h3>
<p>由于语法分析阶段不维护符号表，用户自定义类型名（typedef）和普通变量名难以区分。对于函数体中的 <code>a*b;</code>​ 语句，既可以解释为 a 乘 b，忽略表达式结果，也可以解释为声明一个类型为 <code>a*</code>​ 的变量 b</p>
<p>同时，由于存在如下语法规则</p>
<div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre><code>declaration := declaration_specifiers SEMICOLON
declaration_specifiers := type_specifier declaration_specifiers
type_specifier := INT
type_specifier := typedef_name
typedef_name := IDENTIFIER
</code></pre></div>
<p>规则 1 多用于无需指定标识符的结构体前向声明。如 <code>struct Node;</code>​ 表示前向声明一个 <code>struct Node</code>​ 类型。由于该规则的存在，譬如 <code>int a;</code>​ 也可能会使用规则 1 规约，符号 a 会被识别为一个用户自定义类型名而非变量名。由于类似的无初始化变量的定义语句容易在源码中大量出现，会导致一份源码会对应较多满足语法规则的 AST。这个问题可以在语义分析阶段通过维护符号表解决，但是在语法分析阶段也可以以较小的代价提前处理，减少语义分析需要处理的 AST 个数</p>
<h3 id="思路">思路<a href="#思路" class="heading-anchor-link" aria-label="Link to 思路"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h3>
<p>对于这种类型名 - 变量名的歧义，可以在 GLR 的执行过程中或执行后通过维护代价较小的简易符号表的方式对错误的分支进行剪枝。这个符号表至少要维护变量名和用户自定义类型名，同时由于存在深层作用域变量/类型名对浅层作用域符号的遮蔽，这个符号表还需要跟踪符号的作用域</p>
<p>那么需要做的事情就很简单了：</p>
<ol>
<li>维护符号表，收集类型定义和变量声明</li>
<li>添加符号时校验同级作用域是否有同名变量声明和类型定义</li>
<li>对所有使用 <code>primary_expression := IDENTIFIER</code>​ 规约的节点检查 <code>IDENTIFIER</code>​ 是否是一个已经定义且未被遮蔽的变量名</li>
<li>对所有使用 <code>typedef_name := IDENTIFIER</code>​ 规约的节点检查 <code>IDENTIFIER</code>​ 是否是一个已经定义过且未被遮蔽的用户自定义类型名</li>
</ol>
<p>以经典 <code>a*b;</code>​ 为例：</p>
<div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#6A737D;--s-dark:#6A737D">// 例 1</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">typedef</span><span style="color:#D73A49;--s-dark:#F97583"> int</span><span style="color:#24292E;--s-dark:#E1E4E8"> a;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">func </span><span style="color:#6F42C1;--s-dark:#B392F0">test_func</span><span style="color:#24292E;--s-dark:#E1E4E8">()</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">{</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">	a</span><span style="color:#D73A49;--s-dark:#F97583">*</span><span style="color:#24292E;--s-dark:#E1E4E8">b;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">}</span></span></code></pre></div>
<div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#6A737D;--s-dark:#6A737D">// 例 2</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">typedef</span><span style="color:#D73A49;--s-dark:#F97583"> int</span><span style="color:#24292E;--s-dark:#E1E4E8"> a;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">func </span><span style="color:#6F42C1;--s-dark:#B392F0">test_func</span><span style="color:#24292E;--s-dark:#E1E4E8">()</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">{</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">	int</span><span style="color:#24292E;--s-dark:#E1E4E8"> a;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">	a</span><span style="color:#D73A49;--s-dark:#F97583">*</span><span style="color:#24292E;--s-dark:#E1E4E8">b;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">}</span></span></code></pre></div>
<p>例 1：解析到 <code>a*b;</code>​ 时，符号表的 1 级作用域（最外层）中有一个自定义类型 a，2 级作用域（函数）中无符号。对于将 <code>a*b;</code>​ 解析为声明一个类型为 <code>a*</code>​ 的变量 <code>b</code>​ 的 AST，a 会使用 <code>typedef_name := IDENTIFIER</code>​ 进行规约，检查符号表发现 a 确实是一个声明在 1 级作用域的自定义类型名，于是该 AST 会被保留；对于将其解析为变量 <code>a</code>​ 乘以变量 <code>b</code>​ 的 AST，a 会使用 <code>primary_expression := IDENTIFIER</code>​ 进行规约，检查符号表发现表中并没有变量 a，于是该 AST 会被抛弃</p>
<p>例 2：解析到 <code>a*b;</code>​ 时，符号表的 1 级作用域（最外层）中有一个自定义类型 a，，2 级作用域（函数）中有一个变量 a，此时变量 a 会遮蔽自定义类型 a。对于将 <code>a*b;</code>​ 解析为声明一个类型为 <code>a*</code>​ 的变量 <code>b</code>​ 的 AST，a 会使用 <code>typedef_name := IDENTIFIER</code>​ 进行规约，检查符号表发现在该作用域下 a 是一个变量而非自定义类型（遮蔽），于是该 AST 会被抛弃；对于将其解析为变量 <code>a</code>​ 乘以变量 <code>b</code>​ 的 AST，a 会使用 <code>primary_expression := IDENTIFIER</code>​ 进行规约，检查符号表发现 a 确实是一个变量名，于是该 AST 会被保留</p>
<p>相对于完整的符号表，简易符号表不会检查作用域内同类变量的同名问题（可以检查用户自定义类型名的同名），由于存在前向声明，对相同变量的多次声明是合法的，而包含初始化的声明只可以有一次，在语法分析阶段区分声明是否包含初始化成本还是比较高的，建议延后到语义分析阶段；同时由于成本问题类型检查也不会在这个阶段进行</p>
<p>在 GLR 执行过程维护简易符号表，难以处理函数参数和 for 循环中的变量声明，这些变量的作用域实际在更深层，且作用域的开始和结束并不完全和大括号的范围重合，由于对于函数定义的规约，一定是首先移入左右大括号，之后才能规约为函数定义，这就导致没法简单地在移入左右大括号时处理作用域，只有综合看当前符号栈中的多个符号才能判断。究其原因，是因为 GLR 构建 AST 是自底向下构建的——从叶子节点一步步构建出根节点，底层节点会被优先处理，从而缺乏对节点上下文的感知</p>
<p>在 GLR 执行完成后通过遍历 AST 森林对单棵 AST 进行检查排除的方法就相对简单了很多，尽管无法在执行中剪枝，导致了相对较高的时间和空间复杂度，但是其优点在于实现简单、实现简单和实现简单。在实际实现过程中，可以综合使用执行中剪枝和执行后排除：执行中剪枝代价较小，且一旦剪枝成功，就能够减少执行后排除需要处理的 AST 数量。由于执行后排除一定可以解决该歧义，执行中剪枝需要保证“宁可放过不可错杀”</p>
<h3 id="ast-构建中">AST 构建中<a href="#ast-构建中" class="heading-anchor-link" aria-label="Link to AST 构建中"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h3>
<p>AST 构建中可以在移入一些符号或发生规约时，在节点中存储一些信息并向上传递，方便后续再自顶向下处理时可以快速获取信息，对于处理这个歧义，我的处理是增加两个标记</p>
<div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--s-dark:#F97583">type</span><span style="color:#6F42C1;--s-dark:#B392F0"> GLRLabel</span><span style="color:#D73A49;--s-dark:#F97583"> struct</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">	// Declaration 使用，规约出 Declaration 后消除</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">	TypeDef      </span><span style="color:#D73A49;--s-dark:#F97583">bool</span><span style="color:#6A737D;--s-dark:#6A737D">     // 是否是 TypeDef</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">	DeclaratorID []</span><span style="color:#D73A49;--s-dark:#F97583">*</span><span style="color:#6F42C1;--s-dark:#B392F0">Token</span><span style="color:#6A737D;--s-dark:#6A737D"> // 包含的 Identifier</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">}</span></span></code></pre></div>
<p>typedef 用于标识这个声明是否是一个类型定义，如果最终规约出 declaration 时没有这个标识，说明这个声明只是一个普通的变量声明。DeclaratorID 为这个 declaration 定义的符号，如果这个声明是一个类型定义，那么 DeclaratorID 中为自定义类型的名称。同样，由于 function_definition 中的函数名称也使用了和 declaration 类似的 declarator 处理（<code>function_definition := declaration_specifiers declarator compound_statement</code>​），所以 DeclaratorID 中也会包含函数名称</p>
<p>这两个符号在构建 AST 树时，从下层向上层节点传递</p>
<div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">gslice.</span><span style="color:#6F42C1;--s-dark:#B392F0">ForEach</span><span style="color:#24292E;--s-dark:#E1E4E8">(children, </span><span style="color:#D73A49;--s-dark:#F97583">func</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#E36209;--s-dark:#FFAB70">child</span><span style="color:#D73A49;--s-dark:#F97583"> *</span><span style="color:#6F42C1;--s-dark:#B392F0">AstNode</span><span style="color:#24292E;--s-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">    if</span><span style="color:#24292E;--s-dark:#E1E4E8"> child.TypeDef {</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        parent.TypeDef </span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#005CC5;--s-dark:#79B8FF"> true</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    parent.DeclaratorID </span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#6F42C1;--s-dark:#B392F0"> append</span><span style="color:#24292E;--s-dark:#E1E4E8">(parent.DeclaratorID, child.DeclaratorID</span><span style="color:#D73A49;--s-dark:#F97583">...</span><span style="color:#24292E;--s-dark:#E1E4E8">)</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">})</span></span></code></pre></div>
<p>那么在哪些场景设置这两个符号呢？</p>
<p>typedef 很明显，当使用 <code>storage_class_specifier := TYPEDEF</code>​进行规约时设置当前节点的 <code>typedef</code>​ 为 true</p>
<p>DeclaratorID 就比较复杂了，比较基础的如 <code>direct_declarator := IDENTIFIER</code>​。另外，考虑枚举常量的场景，还需要处理 <code>enumeration_constant := IDENTIFIER</code>​</p>
<p>当然，不能允许这两个符号无限向上传播，构建后自顶向下处理某一节点时应当只期望处理该级节点的信息，而非混杂着下层节点的信息（由于 C 语言中作用域的限制，信息只能从上级向下级传递，下级信息无法影响上级）。这时就需要在规约出某些节点时清空标记，阻断标记的向上传播</p>
<p>阻断向上传播的时机，除了规约出 <code>declaration</code>​ 和 <code>function_definition</code>​ 外，规约出 <code>direct_declarator</code>​时，对于通过形如 <code>direct_declarator := direct_declarator LEFT_PARENTHESES parameter_type_list RIGHT_PARENTHESES</code>​ 规约出的节点，应当只取右侧 <code>direct_declarator</code>​中的 DeclaratorID 继续传播，以避免 <code>parameter_type_list</code>​ 包含函数参数声明的影响</p>
<p>在 AST 构建中，可以进行的、确定无误的检查，有以下两个：</p>
<ol>
<li>使用用户自定义类型时，检查是否是先前声明过的类型（无法检查变量声明遮蔽）</li>
<li>​<code>declaration_specifiers</code>​中如果包含自定义类型的 <code>type_specifier</code>​，那么只可以包含这一个 <code>type_specifier</code>​（自定义类型已是完整类型，不应和其他类型符号一起使用）</li>
</ol>
<p>第一个检查在构建时维护一个自定义类型符号栈，在移入 <code>{</code>​ 时压入新作用域，在移入 <code>}</code>​ 时弹出顶栈作用域。在规约出 Declaration 时，检查节点的 typedef 符号，如果是一个 typedef，就将这个节点的所有 DeclaratorID 加入顶栈作用域。在遇到通过 <code>typedef_name := IDENTIFIER</code>​ 规约的节点时，说明这个 <code>IDENTIFIER</code>​ 是一个此前定义的自定义类型，从栈顶开始向栈底检查即可</p>
<p>第二个检查比较简单，在规约出 <code>declaration</code>​、<code>function_definition</code>​ 和 <code>parameter_declaration</code>​ 时，检查 <code>declaration_specifiers</code>​ 即可</p>
<h3 id="ast-构建后">AST 构建后<a href="#ast-构建后" class="heading-anchor-link" aria-label="Link to AST 构建后"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h3>
<p>在通过上述方式完成构建后，由于构建中的检查只处理了自定义类型，没有处理变量名，所以依然会有一些错误和歧义无法解决，如</p>
<div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--s-dark:#F97583">typedef</span><span style="color:#D73A49;--s-dark:#F97583"> int</span><span style="color:#24292E;--s-dark:#E1E4E8"> a;</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">int</span><span style="color:#6F42C1;--s-dark:#B392F0"> main</span><span style="color:#24292E;--s-dark:#E1E4E8">() {</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">	int</span><span style="color:#24292E;--s-dark:#E1E4E8"> a;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">	a c;</span><span style="color:#6A737D;--s-dark:#6A737D">	// 类型 a 已经被变量 a 遮蔽，此处声明不合法</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">}</span></span></code></pre></div>
<p>所以在构建后需要维护一个相对更全面一些的符号表，在每个作用域中同时记录类型名和变量名</p>
<div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--s-dark:#F97583">type</span><span style="color:#6F42C1;--s-dark:#B392F0"> ScopeSymbols</span><span style="color:#D73A49;--s-dark:#F97583"> struct</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">	TypeNames </span><span style="color:#D73A49;--s-dark:#F97583">map</span><span style="color:#24292E;--s-dark:#E1E4E8">[</span><span style="color:#D73A49;--s-dark:#F97583">string</span><span style="color:#24292E;--s-dark:#E1E4E8">]</span><span style="color:#D73A49;--s-dark:#F97583">*</span><span style="color:#6F42C1;--s-dark:#B392F0">entity</span><span style="color:#24292E;--s-dark:#E1E4E8">.</span><span style="color:#6F42C1;--s-dark:#B392F0">Token</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">	VarNames  </span><span style="color:#D73A49;--s-dark:#F97583">map</span><span style="color:#24292E;--s-dark:#E1E4E8">[</span><span style="color:#D73A49;--s-dark:#F97583">string</span><span style="color:#24292E;--s-dark:#E1E4E8">]</span><span style="color:#D73A49;--s-dark:#F97583">*</span><span style="color:#6F42C1;--s-dark:#B392F0">entity</span><span style="color:#24292E;--s-dark:#E1E4E8">.</span><span style="color:#6F42C1;--s-dark:#B392F0">Token</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">}</span></span></code></pre></div>
<p>和构建中检查类似，在移入 <code>{</code>​ 时压入新作用域，在移入 <code>}</code>​ 时弹出栈顶作用域，遇到 <code>declaration</code>​ 节点，如果包含 typedef 符号，则将 DeclaratorID 加入栈顶作用域的类型名处，否则加入栈顶作用域的变量名中。在加入变量名时，需要检查栈顶作用域是否包含同名的类型名，若包含则需要返回错误；反之加入类型名时亦然</p>
<p>除此以外，函数定义中的函数名也需要作为变量符号加入符号表，函数变量的定义都形如<code>function_definition := declaration_specifiers declarator...</code>​，<code>declarator</code>​ 中的 DeclaratorID 即为函数名</p>
<p>接着就是检查自定义类型名和变量名的使用点了。自定义类型仅在 <code>typedef_name := IDENTIFIER</code>​ 处使用，在 AST 构建中已经检查过这个自定义类型是否已经定义过。在构建后检查中，需要额外检查这个类型名是否被变量名遮蔽，如果在更深层作用域中被遮蔽时仍然需要返回错误。变量名则是在 <code>primary_expression := IDENTIFIER</code>​ 处，其检查和类型名的检查类似</p>
<p>一个简单的变量名检查的例子</p>
<div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--s-dark:#F97583">func</span><span style="color:#24292E;--s-dark:#E1E4E8"> (</span><span style="color:#E36209;--s-dark:#FFAB70">s </span><span style="color:#D73A49;--s-dark:#F97583">*</span><span style="color:#6F42C1;--s-dark:#B392F0">symbolStack</span><span style="color:#24292E;--s-dark:#E1E4E8">) </span><span style="color:#6F42C1;--s-dark:#B392F0">CheckVar</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#E36209;--s-dark:#FFAB70">token</span><span style="color:#D73A49;--s-dark:#F97583"> *</span><span style="color:#6F42C1;--s-dark:#B392F0">entity</span><span style="color:#24292E;--s-dark:#E1E4E8">.</span><span style="color:#6F42C1;--s-dark:#B392F0">Token</span><span style="color:#24292E;--s-dark:#E1E4E8">, </span><span style="color:#E36209;--s-dark:#FFAB70">depth</span><span style="color:#D73A49;--s-dark:#F97583"> int</span><span style="color:#24292E;--s-dark:#E1E4E8">) </span><span style="color:#D73A49;--s-dark:#F97583">error</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">	for</span><span style="color:#24292E;--s-dark:#E1E4E8"> i </span><span style="color:#D73A49;--s-dark:#F97583">:=</span><span style="color:#24292E;--s-dark:#E1E4E8"> depth; i </span><span style="color:#D73A49;--s-dark:#F97583">>=</span><span style="color:#005CC5;--s-dark:#79B8FF"> 0</span><span style="color:#24292E;--s-dark:#E1E4E8">; i</span><span style="color:#D73A49;--s-dark:#F97583">--</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">		if</span><span style="color:#24292E;--s-dark:#E1E4E8"> previous, ok </span><span style="color:#D73A49;--s-dark:#F97583">:=</span><span style="color:#24292E;--s-dark:#E1E4E8"> s.stack[i].TypeNames[token.Lexeme]; ok {</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">			return</span><span style="color:#6F42C1;--s-dark:#B392F0"> InvalidSymbolKind</span><span style="color:#24292E;--s-dark:#E1E4E8">(token.SourceStart, previous.SourceStart, token.Lexeme)</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">		}</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">		if</span><span style="color:#24292E;--s-dark:#E1E4E8"> _, ok </span><span style="color:#D73A49;--s-dark:#F97583">:=</span><span style="color:#24292E;--s-dark:#E1E4E8"> s.stack[i].VarNames[token.Lexeme]; ok {</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">			return</span><span style="color:#005CC5;--s-dark:#79B8FF"> nil</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">		}</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">	}</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">	return</span><span style="color:#6F42C1;--s-dark:#B392F0"> UndeclaredIdentifier</span><span style="color:#24292E;--s-dark:#E1E4E8">(token.SourceStart, token.Lexeme)</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">}</span></span></code></pre></div>
<p>在函数定义和 for 循环中，需要特殊处理作用域。函数定义中的函数参数和 for 循环条件（括号中的内容），并不在函数/循环所在的作用域中，而在其内层作用域，在函数体/循环体作用域中，以 for 循环为例</p>
<div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">currentSymbolStackDepth </span><span style="color:#D73A49;--s-dark:#F97583">:=</span><span style="color:#24292E;--s-dark:#E1E4E8"> s.symbolStack.currentSymbolStackDepth</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">s.symbolStack.</span><span style="color:#6F42C1;--s-dark:#B392F0">SwitchScope</span><span style="color:#24292E;--s-dark:#E1E4E8">(currentSymbolStackDepth </span><span style="color:#D73A49;--s-dark:#F97583">+</span><span style="color:#005CC5;--s-dark:#79B8FF"> 1</span><span style="color:#24292E;--s-dark:#E1E4E8">)	</span><span style="color:#6A737D;--s-dark:#6A737D">// 切换到深层作用域</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">for</span><span style="color:#24292E;--s-dark:#E1E4E8"> i </span><span style="color:#D73A49;--s-dark:#F97583">:=</span><span style="color:#005CC5;--s-dark:#79B8FF"> 0</span><span style="color:#24292E;--s-dark:#E1E4E8">; i </span><span style="color:#D73A49;--s-dark:#F97583">&#x3C;</span><span style="color:#6F42C1;--s-dark:#B392F0"> len</span><span style="color:#24292E;--s-dark:#E1E4E8">(node.Children)</span><span style="color:#D73A49;--s-dark:#F97583">-</span><span style="color:#005CC5;--s-dark:#79B8FF">1</span><span style="color:#24292E;--s-dark:#E1E4E8">; i</span><span style="color:#D73A49;--s-dark:#F97583">++</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">	if</span><span style="color:#24292E;--s-dark:#E1E4E8"> err </span><span style="color:#D73A49;--s-dark:#F97583">:=</span><span style="color:#24292E;--s-dark:#E1E4E8"> s.</span><span style="color:#6F42C1;--s-dark:#B392F0">Chop</span><span style="color:#24292E;--s-dark:#E1E4E8">(node.Children[i]); err </span><span style="color:#D73A49;--s-dark:#F97583">!=</span><span style="color:#005CC5;--s-dark:#79B8FF"> nil</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">		return</span><span style="color:#24292E;--s-dark:#E1E4E8"> err</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">	}</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">}</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">s.symbolStack.</span><span style="color:#6F42C1;--s-dark:#B392F0">SwitchScope</span><span style="color:#24292E;--s-dark:#E1E4E8">(currentSymbolStackDepth)		</span><span style="color:#6A737D;--s-dark:#6A737D">// 切换回当前作用域</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">if</span><span style="color:#24292E;--s-dark:#E1E4E8"> err </span><span style="color:#D73A49;--s-dark:#F97583">:=</span><span style="color:#24292E;--s-dark:#E1E4E8"> s.</span><span style="color:#6F42C1;--s-dark:#B392F0">Chop</span><span style="color:#24292E;--s-dark:#E1E4E8">(node.Children[</span><span style="color:#6F42C1;--s-dark:#B392F0">len</span><span style="color:#24292E;--s-dark:#E1E4E8">(node.Children)</span><span style="color:#D73A49;--s-dark:#F97583">-</span><span style="color:#005CC5;--s-dark:#79B8FF">1</span><span style="color:#24292E;--s-dark:#E1E4E8">]); err </span><span style="color:#D73A49;--s-dark:#F97583">!=</span><span style="color:#005CC5;--s-dark:#79B8FF"> nil</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">	// 如果循环体中包含 {，则会自然进入</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">	return</span><span style="color:#24292E;--s-dark:#E1E4E8"> err</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">}</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">s.symbolStack.</span><span style="color:#6F42C1;--s-dark:#B392F0">EnterScope</span><span style="color:#24292E;--s-dark:#E1E4E8">(currentSymbolStackDepth)		</span><span style="color:#6A737D;--s-dark:#6A737D">// 若不存在循环体，则会导致深层作用域无法弹出，这里强行重置一下</span></span></code></pre></div>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[VPS 套 Warp 分流指定出口 IPv6]]></title>
            <link>https://blog.shinya.click/fiddling/vps-warp-ipv6</link>
            <guid isPermaLink="false">https://blog.shinya.click/fiddling/vps-warp-ipv6</guid>
            <pubDate>Sat, 15 Mar 2025 08:24:00 GMT</pubDate>
            <description><![CDATA[在一个平常的下午，Telegram 推送消息引发了对一个新套餐的热情，电信 CN2 直连、2.5G 带宽的优惠让人兴奋不已。这个套餐自带 IPv6，适合解锁流媒体，但仍需谨慎使用，毕竟并不是所有流量都需要通过 Warp 来处理。先前使用的脚本虽然方便，但在速度和流量分配上存在不足，显然需要寻找更灵活的解决方案。]]></description>
            <content:encoded><![CDATA[<p>上周一个普通的下午，一直挂在后台沉寂许久的 Telegram 收到了一条推送：</p>
<p><img src="https://blog-img.774352199.xyz/2025/c2dcc1d96db444256f1092fb0e15ce3d.png" alt="" loading="lazy" decoding="async" style="background-image:url(data:image/webp;base64,UklGRkQAAABXRUJQVlA4IDgAAACwAQCdASoQAA0AAsBMJZQAAuXTaG8gAP705rZC0klKJM7jSBay3Du5Pllw/Oq2EXsSoZeSwUIAAA==);background-size:cover;background-repeat:no-repeat">​</p>
<p>定睛一看，电信 CN2 直连，2.5G 带宽，1T 流量，券后 36 刀一年，合 3 刀一个月</p>
<p>新的传家宝已经出现！</p>
<p>火速付款之余，立即呼朋唤友，忽悠了周围三四个同事都入手了</p>
<p>这个套餐是自带 IPv6 的，对于各种流媒体解锁都有好处。当然依然有很多 VPS 没有 IPv6 地址，并且中国有句古话叫狡兔三窟，出门在外还是得套个马甲的。这时候就需要用到大善人 Cloudflare 家的 Warp 了</p>
<p>此前使用的 warp 脚本是 <a href="https://gitlab.com/fscarmen/warp" target="_blank" rel="noopener noreferrer" data-umami-event="outbound-link-click" data-umami-event-url="https://gitlab.com/fscarmen/warp">scarmen/warp</a>，直接启动全局模式，就会将整个 VPS 的全局出口都重定位到 warp。但是有两点问题：</p>
<ol>
<li>warp 降速，并非所有的出口流量都需要走 warp。通常只有一些如 Netflix、OpenAI 之类的对 IP 要求比较高的服务需要走 warp</li>
<li>warp 接管全局流量后，即使是双栈出口，但是有时出口会优先走 IPv4。由于 DNS 解析发生在 warp 内部（远程 DNS 解析），无法做干预</li>
</ol>
<p>对于问题 1，warp 脚本可以开启非全局模式，并在本地开放一个 socks 代理，可以使用代理软件分流。对于问题 2，则可以通过代理软件让 DNS 解析发生在本地，完成解析后再将流量通过 warp 出口</p>
<p>非全局的 warp，可以在安装脚本时直接选择</p>
<div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">wget</span><span style="color:#005CC5;--s-dark:#79B8FF"> -N</span><span style="color:#032F62;--s-dark:#9ECBFF"> https://gitlab.com/fscarmen/warp/-/raw/main/menu.sh</span><span style="color:#24292E;--s-dark:#E1E4E8"> &#x26;&#x26; </span><span style="color:#6F42C1;--s-dark:#B392F0">bash</span><span style="color:#032F62;--s-dark:#9ECBFF"> menu.sh</span><span style="color:#032F62;--s-dark:#9ECBFF"> c</span></span></code></pre></div>
<p>也可以安装完成后在菜单中选择，WARP Linux Client 或 wireproxy 都可以</p>
<p>开启后 socks 默认会开放在本机 40000 端口</p>
<p>有了这个 socks 服务，添加 socks 出口，再根据需要分流即可。以 xray 为例，clash 和 sing-box 类似</p>
<div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">{</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">  "outbounds"</span><span style="color:#24292E;--s-dark:#E1E4E8">: [</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    {</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">      "tag"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">"warp"</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">      "protocol"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">"socks"</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">      "settings"</span><span style="color:#24292E;--s-dark:#E1E4E8">: {</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">        "servers"</span><span style="color:#24292E;--s-dark:#E1E4E8">: [</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">          {</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            "address"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">"127.0.0.1"</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            "port"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">40000</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">          }</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        ]</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">      }</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">  ]</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">}</span></span></code></pre></div>
<p>直接设置代理软件的出口为 socks5 服务，就会触发远程 DNS 解析，导致出口 IPv4/IPv6 不稳定，这里可以通过本地 DNS 的方式解决</p>
<p>本地 dns 就需要代理软件开启 dns 模块</p>
<div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">{</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">  "dns"</span><span style="color:#24292E;--s-dark:#E1E4E8">: {</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">    "servers"</span><span style="color:#24292E;--s-dark:#E1E4E8">: [</span></span>
<span class="line"><span style="color:#032F62;--s-dark:#9ECBFF">      "2606:4700:4700::1111"</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#032F62;--s-dark:#9ECBFF">      "1.1.1.1"</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    ],</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">    "queryStrategy"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">"UseIP"</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">    "tag"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">"dns_inbound"</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">  }</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">}</span></span></code></pre></div>
<p>出口添加为一个链式代理</p>
<div class="code-collapse-wrapper"><input type="checkbox" id="toggle-mf93yxk" class="code-collapse-toggle" style="display: none;"><div class="code-collapse-preview"><div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0" class="code-preview"><code><span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">{</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">  "outbounds"</span><span style="color:#24292E;--s-dark:#E1E4E8">: [</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    {</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">      "tag"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">"warp"</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">      "protocol"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">"freedom"</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">      "settings"</span><span style="color:#24292E;--s-dark:#E1E4E8">: {</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">        "domainStrategy"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">"UseIPv6v4"</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">      },</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">      "proxySettings"</span><span style="color:#24292E;--s-dark:#E1E4E8">: {</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">        "tag"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">"warp-inner"</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">      }</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    },</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    {</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">      "tag"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">"warp-inner"</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">      "protocol"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">"socks"</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">      "settings"</span><span style="color:#24292E;--s-dark:#E1E4E8">: {</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">        "servers"</span><span style="color:#24292E;--s-dark:#E1E4E8">: [</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">          {</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            "address"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">"127.0.0.1"</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            "port"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">40000</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">          }</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        ]</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">      }</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">  ]</span></span>
</code></pre></div><div class="code-collapse-gradient"></div><label class="code-collapse-expand" for="toggle-mf93yxk"><svg class="code-collapse-icon" width="32" height="24" viewBox="0 0 32 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="4,9 12,17 20,9"></polyline></svg></label></div><div class="code-collapse-full"><div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">{</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">  "outbounds"</span><span style="color:#24292E;--s-dark:#E1E4E8">: [</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    {</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">      "tag"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">"warp"</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">      "protocol"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">"freedom"</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">      "settings"</span><span style="color:#24292E;--s-dark:#E1E4E8">: {</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">        "domainStrategy"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">"UseIPv6v4"</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">      },</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">      "proxySettings"</span><span style="color:#24292E;--s-dark:#E1E4E8">: {</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">        "tag"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">"warp-inner"</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">      }</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    },</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    {</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">      "tag"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">"warp-inner"</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">      "protocol"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">"socks"</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">      "settings"</span><span style="color:#24292E;--s-dark:#E1E4E8">: {</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">        "servers"</span><span style="color:#24292E;--s-dark:#E1E4E8">: [</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">          {</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            "address"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">"127.0.0.1"</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            "port"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">40000</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">          }</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        ]</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">      }</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">  ]</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">}</span></span></code><label class="code-collapse-collapse" for="toggle-mf93yxk"><svg class="code-collapse-icon" width="32" height="24" viewBox="0 0 32 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20,15 12,7 4,15"></polyline></svg></label></pre></div></div></div>
<p>先通过一个 freedom 出口解析域名，UseIPv6v4 表示优先使用解析出的 IPv6 地址，如果没有 IPv6 再使用 IPv4，再通过 proxySettings 将流量导入 warp-inner 出口，也就是之前设置的 socks，即 warp</p>
<p>这样如果访问的网站支持 IPv6，那么通过 warp tag 访问该网站就必然通过 IPv6 访问</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[在 VitePress 中实现一个动态说说功能]]></title>
            <link>https://blog.shinya.click/fiddling/vitepress-memos-component</link>
            <guid isPermaLink="false">https://blog.shinya.click/fiddling/vitepress-memos-component</guid>
            <pubDate>Wed, 29 Jan 2025 13:58:00 GMT</pubDate>
            <description><![CDATA[在构建动态博客时，添加说说功能能显著提升用户体验。相比静态博客的繁琐流程，这种功能允许用户随时随地分享短小的想法，降低了发文的心理负担。通过利用 CloudFlare Workers 实现后端逻辑，并结合 KV 存储，开发者能够轻松管理说说内容。前端则通过 VitePress 框架和 Vue 组件的嵌入，快捷地展示这些动态信息，为博客增添了生动的交互性。]]></description>
            <content:encoded><![CDATA[<h3 id="前言">前言<a href="#前言" class="heading-anchor-link" aria-label="Link to 前言"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h3>
<p>很多动态博客中都有一个说说的功能，本质就是一种特殊的博文，借助动态博客的实时性，可以做到随写随发</p>
<p>静态博客由于是在本地或服务器上静态编译成 html 后再部署，实时性比较差。写一篇博文长篇大论自然可以在电脑前，走 git 推送部署也不算麻烦，但是发一篇说说还要打开电脑，心智负担就有些重了，手机上操作 git 也比较麻烦，不是很优雅，干脆一想就不发算了</p>
<p>于是实现了一套说说系统的前后端，效果就是本博客的 <a href="/memos">碎碎念</a>。后端使用 CloudFlare Workers 实现，存储当然也就近存储在大善人的 KV 里，简单写了个管理页面。博客框架是 VitePress，前端也就做成了个 Vue 组件，直接嵌入一个页面作为说说页</p>
<p>前端效果不再多说，后端管理页面效果
<img src="https://blog-img.774352199.xyz/2025/8551751fe98e55c4159d28b9ff5b9473.png" alt="Memo 管理页面" loading="lazy" decoding="async" style="background-image:url(data:image/webp;base64,UklGRjwAAABXRUJQVlA4IDAAAACwAQCdASoQAAoAAsBMJaQAAxe5tOXAAP75eEgLg8L4WvZtCjrDBcjfSVbmDBuAAAA=);background-size:cover;background-repeat:no-repeat"></p>
<h3 id="后端-cloudflare-workers--kv">后端 CloudFlare Workers + KV<a href="#后端-cloudflare-workers--kv" class="heading-anchor-link" aria-label="Link to 后端 CloudFlare Workers + KV"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h3>
<h4 id="基本概述">基本概述<a href="#基本概述" class="heading-anchor-link" aria-label="Link to 基本概述"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h4>
<p>后端包含以下功能：</p>
<ul>
<li>支持说说的增删改（基本功能）</li>
<li>页面和所有写接口都有鉴权，足够安全</li>
<li>Markdown 格式实时预览（by marked）</li>
</ul>
<p>KV 中存储一个 <code>index</code> key，value 是一个 uid 的数组，作为全部说说的索引。其他所有说说都存储在以 <code>uid</code> 为 key 的条目中，value 格式如</p>
<div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">{</span></span>
<span class="line"><span style="color:#032F62;--s-dark:#9ECBFF">    "uid"</span><span style="color:#24292E;--s-dark:#E1E4E8">:</span><span style="color:#032F62;--s-dark:#9ECBFF">"唯一 id"</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#032F62;--s-dark:#9ECBFF">    "createTime"</span><span style="color:#24292E;--s-dark:#E1E4E8">:</span><span style="color:#032F62;--s-dark:#9ECBFF">"发布时间"</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#032F62;--s-dark:#9ECBFF">    "content"</span><span style="color:#24292E;--s-dark:#E1E4E8">:</span><span style="color:#032F62;--s-dark:#9ECBFF">"说说内容"</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">}</span></span></code></pre></div>
<h4 id="实现">实现<a href="#实现" class="heading-anchor-link" aria-label="Link to 实现"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h4>
<p>首先要创建一个 CloudFlare 的 KV Space，专门存储说说相关的 KV 对。位置在<code>账户首页 - 存储和数据库 - KV</code>，点击创建，名字不太重要，记住就行了，我这里简单命名为 <code>memos</code></p>
<p>接着就是创建 CloudFlare Workers，用于逻辑处理。位置在<code>账户首页 - 计算（Workers）- Workers 和 Pages</code>，点击创建，名字依然不太重要，我简单命名为 <code>memos-api</code>。创建完成后，点击 Workers 名称进入 Workers 详情，在<code>设置 - 绑定</code>中添加一个绑定关系，选择绑定 <code>KV 命名空间</code>，变量名称为 <code>KV</code>，KV 命名空间选择刚刚创建的 KV Space 名称，我的是 <code>memos</code>。这样绑定完成后，就可以在代码中直接使用 <code>env.KV</code> 操作 <code>memos</code> 这个 KV 空间了。最后点击顶栏右侧的 <code>编辑代码</code> 按钮</p>
<p>下面就是 Code Time！</p>
<p>首先创建一个 <code>index.html</code>，用来存放管理页面的 html、css 和 js</p>
<div class="code-collapse-wrapper"><input type="checkbox" id="toggle-givnjhd" class="code-collapse-toggle" style="display: none;"><div class="code-collapse-preview"><div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0" class="code-preview"><code><span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">&#x3C;!</span><span style="color:#22863A;--s-dark:#85E89D">DOCTYPE</span><span style="color:#6F42C1;--s-dark:#B392F0"> html</span><span style="color:#24292E;--s-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">&#x3C;</span><span style="color:#22863A;--s-dark:#85E89D">html</span><span style="color:#24292E;--s-dark:#E1E4E8">></span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">&#x3C;</span><span style="color:#22863A;--s-dark:#85E89D">head</span><span style="color:#24292E;--s-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    &#x3C;</span><span style="color:#22863A;--s-dark:#85E89D">title</span><span style="color:#24292E;--s-dark:#E1E4E8">>Memos 管理&#x3C;/</span><span style="color:#22863A;--s-dark:#85E89D">title</span><span style="color:#24292E;--s-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    &#x3C;</span><span style="color:#22863A;--s-dark:#85E89D">meta</span><span style="color:#6F42C1;--s-dark:#B392F0"> charset</span><span style="color:#24292E;--s-dark:#E1E4E8">=</span><span style="color:#032F62;--s-dark:#9ECBFF">"UTF-8"</span><span style="color:#24292E;--s-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    &#x3C;</span><span style="color:#22863A;--s-dark:#85E89D">meta</span><span style="color:#6F42C1;--s-dark:#B392F0"> name</span><span style="color:#24292E;--s-dark:#E1E4E8">=</span><span style="color:#032F62;--s-dark:#9ECBFF">"viewport"</span><span style="color:#6F42C1;--s-dark:#B392F0"> content</span><span style="color:#24292E;--s-dark:#E1E4E8">=</span><span style="color:#032F62;--s-dark:#9ECBFF">"width=device-width, initial-scale=1.0"</span><span style="color:#24292E;--s-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    &#x3C;</span><span style="color:#22863A;--s-dark:#85E89D">link</span><span style="color:#6F42C1;--s-dark:#B392F0"> rel</span><span style="color:#24292E;--s-dark:#E1E4E8">=</span><span style="color:#032F62;--s-dark:#9ECBFF">"stylesheet"</span><span style="color:#6F42C1;--s-dark:#B392F0"> href</span><span style="color:#24292E;--s-dark:#E1E4E8">=</span><span style="color:#032F62;--s-dark:#9ECBFF">"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css"</span><span style="color:#24292E;--s-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    &#x3C;</span><span style="color:#22863A;--s-dark:#85E89D">style</span><span style="color:#24292E;--s-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">        :root</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#E36209;--s-dark:#FFAB70">            --primary-color</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">#2c3e50</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#E36209;--s-dark:#FFAB70">            --secondary-color</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">#34495e</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#E36209;--s-dark:#FFAB70">            --accent-color</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">#3498db</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#E36209;--s-dark:#FFAB70">            --background-color</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">#f5f6fa</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#E36209;--s-dark:#FFAB70">            --text-color</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">#2c3e50</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#E36209;--s-dark:#FFAB70">            --border-color</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">#dcdde1</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#E36209;--s-dark:#FFAB70">            --hover-color</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">#f1f2f6</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        }</span></span>
<span class="line"></span>
<span class="line"><span style="color:#22863A;--s-dark:#85E89D">        *</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            margin</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">0</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            padding</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">0</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            box-sizing</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">border-box</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        }</span></span>
<span class="line"></span>
</code></pre></div><div class="code-collapse-gradient"></div><label class="code-collapse-expand" for="toggle-givnjhd"><svg class="code-collapse-icon" width="32" height="24" viewBox="0 0 32 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="4,9 12,17 20,9"></polyline></svg></label></div><div class="code-collapse-full"><div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">&#x3C;!</span><span style="color:#22863A;--s-dark:#85E89D">DOCTYPE</span><span style="color:#6F42C1;--s-dark:#B392F0"> html</span><span style="color:#24292E;--s-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">&#x3C;</span><span style="color:#22863A;--s-dark:#85E89D">html</span><span style="color:#24292E;--s-dark:#E1E4E8">></span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">&#x3C;</span><span style="color:#22863A;--s-dark:#85E89D">head</span><span style="color:#24292E;--s-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    &#x3C;</span><span style="color:#22863A;--s-dark:#85E89D">title</span><span style="color:#24292E;--s-dark:#E1E4E8">>Memos 管理&#x3C;/</span><span style="color:#22863A;--s-dark:#85E89D">title</span><span style="color:#24292E;--s-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    &#x3C;</span><span style="color:#22863A;--s-dark:#85E89D">meta</span><span style="color:#6F42C1;--s-dark:#B392F0"> charset</span><span style="color:#24292E;--s-dark:#E1E4E8">=</span><span style="color:#032F62;--s-dark:#9ECBFF">"UTF-8"</span><span style="color:#24292E;--s-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    &#x3C;</span><span style="color:#22863A;--s-dark:#85E89D">meta</span><span style="color:#6F42C1;--s-dark:#B392F0"> name</span><span style="color:#24292E;--s-dark:#E1E4E8">=</span><span style="color:#032F62;--s-dark:#9ECBFF">"viewport"</span><span style="color:#6F42C1;--s-dark:#B392F0"> content</span><span style="color:#24292E;--s-dark:#E1E4E8">=</span><span style="color:#032F62;--s-dark:#9ECBFF">"width=device-width, initial-scale=1.0"</span><span style="color:#24292E;--s-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    &#x3C;</span><span style="color:#22863A;--s-dark:#85E89D">link</span><span style="color:#6F42C1;--s-dark:#B392F0"> rel</span><span style="color:#24292E;--s-dark:#E1E4E8">=</span><span style="color:#032F62;--s-dark:#9ECBFF">"stylesheet"</span><span style="color:#6F42C1;--s-dark:#B392F0"> href</span><span style="color:#24292E;--s-dark:#E1E4E8">=</span><span style="color:#032F62;--s-dark:#9ECBFF">"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css"</span><span style="color:#24292E;--s-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    &#x3C;</span><span style="color:#22863A;--s-dark:#85E89D">style</span><span style="color:#24292E;--s-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">        :root</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#E36209;--s-dark:#FFAB70">            --primary-color</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">#2c3e50</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#E36209;--s-dark:#FFAB70">            --secondary-color</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">#34495e</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#E36209;--s-dark:#FFAB70">            --accent-color</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">#3498db</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#E36209;--s-dark:#FFAB70">            --background-color</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">#f5f6fa</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#E36209;--s-dark:#FFAB70">            --text-color</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">#2c3e50</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#E36209;--s-dark:#FFAB70">            --border-color</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">#dcdde1</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#E36209;--s-dark:#FFAB70">            --hover-color</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">#f1f2f6</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        }</span></span>
<span class="line"></span>
<span class="line"><span style="color:#22863A;--s-dark:#85E89D">        *</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            margin</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">0</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            padding</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">0</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            box-sizing</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">border-box</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        }</span></span>
<span class="line"></span>
<span class="line"><span style="color:#22863A;--s-dark:#85E89D">        body</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            font-family</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">-apple-system</span><span style="color:#24292E;--s-dark:#E1E4E8">, BlinkMacSystemFont, </span><span style="color:#032F62;--s-dark:#9ECBFF">'Segoe UI'</span><span style="color:#24292E;--s-dark:#E1E4E8">, Roboto, Oxygen, Ubuntu, Cantarell, </span><span style="color:#005CC5;--s-dark:#79B8FF">sans-serif</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            background-color</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">var</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#E36209;--s-dark:#FFAB70">--background-color</span><span style="color:#24292E;--s-dark:#E1E4E8">);</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            color</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">var</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#E36209;--s-dark:#FFAB70">--text-color</span><span style="color:#24292E;--s-dark:#E1E4E8">);</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            line-height</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">1.6</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        }</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">        #auth-panel</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            position</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">fixed</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            top</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">0</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            left</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">0</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            width</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">100</span><span style="color:#D73A49;--s-dark:#F97583">%</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            height</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">100</span><span style="color:#D73A49;--s-dark:#F97583">%</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            background</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">rgba</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#005CC5;--s-dark:#79B8FF">255</span><span style="color:#24292E;--s-dark:#E1E4E8">, </span><span style="color:#005CC5;--s-dark:#79B8FF">255</span><span style="color:#24292E;--s-dark:#E1E4E8">, </span><span style="color:#005CC5;--s-dark:#79B8FF">255</span><span style="color:#24292E;--s-dark:#E1E4E8">, </span><span style="color:#005CC5;--s-dark:#79B8FF">0.95</span><span style="color:#24292E;--s-dark:#E1E4E8">);</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            display</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">flex</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            justify-content</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">center</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            align-items</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">center</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            z-index</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">1000</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            backdrop-filter</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">blur</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#005CC5;--s-dark:#79B8FF">5</span><span style="color:#D73A49;--s-dark:#F97583">px</span><span style="color:#24292E;--s-dark:#E1E4E8">);</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        }</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">        #auth-form</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            background</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">white</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            padding</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">2</span><span style="color:#D73A49;--s-dark:#F97583">rem</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            border-radius</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">10</span><span style="color:#D73A49;--s-dark:#F97583">px</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            box-shadow</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">0</span><span style="color:#005CC5;--s-dark:#79B8FF"> 4</span><span style="color:#D73A49;--s-dark:#F97583">px</span><span style="color:#005CC5;--s-dark:#79B8FF"> 6</span><span style="color:#D73A49;--s-dark:#F97583">px</span><span style="color:#005CC5;--s-dark:#79B8FF"> rgba</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#005CC5;--s-dark:#79B8FF">0</span><span style="color:#24292E;--s-dark:#E1E4E8">, </span><span style="color:#005CC5;--s-dark:#79B8FF">0</span><span style="color:#24292E;--s-dark:#E1E4E8">, </span><span style="color:#005CC5;--s-dark:#79B8FF">0</span><span style="color:#24292E;--s-dark:#E1E4E8">, </span><span style="color:#005CC5;--s-dark:#79B8FF">0.1</span><span style="color:#24292E;--s-dark:#E1E4E8">);</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            width</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">300</span><span style="color:#D73A49;--s-dark:#F97583">px</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        }</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">        #auth-form</span><span style="color:#22863A;--s-dark:#85E89D"> input</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            width</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">100</span><span style="color:#D73A49;--s-dark:#F97583">%</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            padding</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">0.8</span><span style="color:#D73A49;--s-dark:#F97583">rem</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            margin-bottom</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">1</span><span style="color:#D73A49;--s-dark:#F97583">rem</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            border</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">1</span><span style="color:#D73A49;--s-dark:#F97583">px</span><span style="color:#005CC5;--s-dark:#79B8FF"> solid</span><span style="color:#005CC5;--s-dark:#79B8FF"> var</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#E36209;--s-dark:#FFAB70">--border-color</span><span style="color:#24292E;--s-dark:#E1E4E8">);</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            border-radius</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">5</span><span style="color:#D73A49;--s-dark:#F97583">px</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            font-size</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">1</span><span style="color:#D73A49;--s-dark:#F97583">rem</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        }</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">        .container</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            max-width</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">1400</span><span style="color:#D73A49;--s-dark:#F97583">px</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            margin</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">2</span><span style="color:#D73A49;--s-dark:#F97583">rem</span><span style="color:#005CC5;--s-dark:#79B8FF"> auto</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            padding</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">0</span><span style="color:#005CC5;--s-dark:#79B8FF"> 1</span><span style="color:#D73A49;--s-dark:#F97583">rem</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            display</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">flex</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            gap</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">2</span><span style="color:#D73A49;--s-dark:#F97583">rem</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            height</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">calc</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#005CC5;--s-dark:#79B8FF">100</span><span style="color:#D73A49;--s-dark:#F97583">vh</span><span style="color:#D73A49;--s-dark:#F97583"> -</span><span style="color:#005CC5;--s-dark:#79B8FF"> 4</span><span style="color:#D73A49;--s-dark:#F97583">rem</span><span style="color:#24292E;--s-dark:#E1E4E8">);</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        }</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">        .memo-list</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            width</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">350</span><span style="color:#D73A49;--s-dark:#F97583">px</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            background</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">white</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            border-radius</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">10</span><span style="color:#D73A49;--s-dark:#F97583">px</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            box-shadow</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">0</span><span style="color:#005CC5;--s-dark:#79B8FF"> 2</span><span style="color:#D73A49;--s-dark:#F97583">px</span><span style="color:#005CC5;--s-dark:#79B8FF"> 4</span><span style="color:#D73A49;--s-dark:#F97583">px</span><span style="color:#005CC5;--s-dark:#79B8FF"> rgba</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#005CC5;--s-dark:#79B8FF">0</span><span style="color:#24292E;--s-dark:#E1E4E8">, </span><span style="color:#005CC5;--s-dark:#79B8FF">0</span><span style="color:#24292E;--s-dark:#E1E4E8">, </span><span style="color:#005CC5;--s-dark:#79B8FF">0</span><span style="color:#24292E;--s-dark:#E1E4E8">, </span><span style="color:#005CC5;--s-dark:#79B8FF">0.1</span><span style="color:#24292E;--s-dark:#E1E4E8">);</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            display</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">flex</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            flex-direction</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">column</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        }</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">        .memo-list-header</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            padding</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">1</span><span style="color:#D73A49;--s-dark:#F97583">rem</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            border-bottom</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">1</span><span style="color:#D73A49;--s-dark:#F97583">px</span><span style="color:#005CC5;--s-dark:#79B8FF"> solid</span><span style="color:#005CC5;--s-dark:#79B8FF"> var</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#E36209;--s-dark:#FFAB70">--border-color</span><span style="color:#24292E;--s-dark:#E1E4E8">);</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            font-weight</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">600</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            display</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">flex</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            justify-content</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">space-between</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            align-items</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">center</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        }</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">        .memo-items</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            flex</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">1</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            overflow-y</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">auto</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            padding</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">0.5</span><span style="color:#D73A49;--s-dark:#F97583">rem</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        }</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">        .memo-item</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            padding</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">1</span><span style="color:#D73A49;--s-dark:#F97583">rem</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            border-radius</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">8</span><span style="color:#D73A49;--s-dark:#F97583">px</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            margin-bottom</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">0.5</span><span style="color:#D73A49;--s-dark:#F97583">rem</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            cursor</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">pointer</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            transition</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">all</span><span style="color:#005CC5;--s-dark:#79B8FF"> 0.2</span><span style="color:#D73A49;--s-dark:#F97583">s</span><span style="color:#005CC5;--s-dark:#79B8FF"> ease</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            border</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">1</span><span style="color:#D73A49;--s-dark:#F97583">px</span><span style="color:#005CC5;--s-dark:#79B8FF"> solid</span><span style="color:#005CC5;--s-dark:#79B8FF"> var</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#E36209;--s-dark:#FFAB70">--border-color</span><span style="color:#24292E;--s-dark:#E1E4E8">);</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            height</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">auto</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">            /* 移除固定高度 */</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            overflow</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">hidden</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            position</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">relative</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            display</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">flex</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            flex-direction</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">column</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            gap</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">0.5</span><span style="color:#D73A49;--s-dark:#F97583">rem</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        }</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">        .memo-item:hover</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            background-color</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">var</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#E36209;--s-dark:#FFAB70">--hover-color</span><span style="color:#24292E;--s-dark:#E1E4E8">);</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            transform</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">translateY</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#005CC5;--s-dark:#79B8FF">-2</span><span style="color:#D73A49;--s-dark:#F97583">px</span><span style="color:#24292E;--s-dark:#E1E4E8">);</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        }</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">        .memo-item.active</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            border-color</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">var</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#E36209;--s-dark:#FFAB70">--accent-color</span><span style="color:#24292E;--s-dark:#E1E4E8">);</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            background-color</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">var</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#E36209;--s-dark:#FFAB70">--hover-color</span><span style="color:#24292E;--s-dark:#E1E4E8">);</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        }</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">        .memo-detail</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            flex</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">1</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            background</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">white</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            border-radius</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">10</span><span style="color:#D73A49;--s-dark:#F97583">px</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            box-shadow</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">0</span><span style="color:#005CC5;--s-dark:#79B8FF"> 2</span><span style="color:#D73A49;--s-dark:#F97583">px</span><span style="color:#005CC5;--s-dark:#79B8FF"> 4</span><span style="color:#D73A49;--s-dark:#F97583">px</span><span style="color:#005CC5;--s-dark:#79B8FF"> rgba</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#005CC5;--s-dark:#79B8FF">0</span><span style="color:#24292E;--s-dark:#E1E4E8">, </span><span style="color:#005CC5;--s-dark:#79B8FF">0</span><span style="color:#24292E;--s-dark:#E1E4E8">, </span><span style="color:#005CC5;--s-dark:#79B8FF">0</span><span style="color:#24292E;--s-dark:#E1E4E8">, </span><span style="color:#005CC5;--s-dark:#79B8FF">0.1</span><span style="color:#24292E;--s-dark:#E1E4E8">);</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            display</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">flex</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            flex-direction</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">column</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        }</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">        .memo-detail-header</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            display</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">flex</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            justify-content</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">space-between</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            align-items</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">center</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        }</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">        .memo-item-header</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            display</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">flex</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            justify-content</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">space-between</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            font-size</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">0.8</span><span style="color:#D73A49;--s-dark:#F97583">rem</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            color</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">#666</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            border-bottom</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">1</span><span style="color:#D73A49;--s-dark:#F97583">px</span><span style="color:#005CC5;--s-dark:#79B8FF"> solid</span><span style="color:#005CC5;--s-dark:#79B8FF"> var</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#E36209;--s-dark:#FFAB70">--border-color</span><span style="color:#24292E;--s-dark:#E1E4E8">);</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            padding-bottom</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">0.5</span><span style="color:#D73A49;--s-dark:#F97583">rem</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        }</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">        .memo-item-content</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            font-size</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">0.9</span><span style="color:#D73A49;--s-dark:#F97583">rem</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            line-height</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">1.4</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            max-height</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">4.2</span><span style="color:#D73A49;--s-dark:#F97583">em</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">            /* 显示 3 行文本 */</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            overflow</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">hidden</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            display</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">-webkit-box</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            -webkit-line-clamp</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">3</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            -webkit-box-orient</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">vertical</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        }</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">        .memo-uid</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            font-family</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">monospace</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            color</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">var</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#E36209;--s-dark:#FFAB70">--accent-color</span><span style="color:#24292E;--s-dark:#E1E4E8">);</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        }</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">        .memo-info</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            font-size</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">0.9</span><span style="color:#D73A49;--s-dark:#F97583">rem</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            color</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">#666</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            margin-left</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">10</span><span style="color:#D73A49;--s-dark:#F97583">px</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            margin-top</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">10</span><span style="color:#D73A49;--s-dark:#F97583">px</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        }</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">        .memo-content</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            flex</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">1</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            display</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">flex</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            flex-direction</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">column</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            padding</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">1</span><span style="color:#D73A49;--s-dark:#F97583">rem</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            gap</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">1</span><span style="color:#D73A49;--s-dark:#F97583">rem</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        }</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">        .memo-edit</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            flex</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">1</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        }</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">        .memo-edit</span><span style="color:#22863A;--s-dark:#85E89D"> textarea</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            width</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">100</span><span style="color:#D73A49;--s-dark:#F97583">%</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            height</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">100</span><span style="color:#D73A49;--s-dark:#F97583">%</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            border</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">1</span><span style="color:#D73A49;--s-dark:#F97583">px</span><span style="color:#005CC5;--s-dark:#79B8FF"> solid</span><span style="color:#005CC5;--s-dark:#79B8FF"> var</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#E36209;--s-dark:#FFAB70">--border-color</span><span style="color:#24292E;--s-dark:#E1E4E8">);</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            border-radius</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">5</span><span style="color:#D73A49;--s-dark:#F97583">px</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            padding</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">1</span><span style="color:#D73A49;--s-dark:#F97583">rem</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            font-size</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">1</span><span style="color:#D73A49;--s-dark:#F97583">rem</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            resize</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">vertical</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            font-family</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">inherit</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        }</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">        .memo-preview</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            flex</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">1</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            padding</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">1</span><span style="color:#D73A49;--s-dark:#F97583">rem</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            border</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">1</span><span style="color:#D73A49;--s-dark:#F97583">px</span><span style="color:#005CC5;--s-dark:#79B8FF"> solid</span><span style="color:#005CC5;--s-dark:#79B8FF"> var</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#E36209;--s-dark:#FFAB70">--border-color</span><span style="color:#24292E;--s-dark:#E1E4E8">);</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            border-radius</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">5</span><span style="color:#D73A49;--s-dark:#F97583">px</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            overflow-y</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">auto</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            overflow-x</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">hidden</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            background-color</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">var</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#E36209;--s-dark:#FFAB70">--background-color</span><span style="color:#24292E;--s-dark:#E1E4E8">);</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        }</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">        .memo-preview</span><span style="color:#22863A;--s-dark:#85E89D"> img</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            max-width</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">100</span><span style="color:#D73A49;--s-dark:#F97583">%</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            max-height</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">150</span><span style="color:#D73A49;--s-dark:#F97583">px</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            object-fit</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">contain</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            display</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">block</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">            /* 避免图片底部空隙 */</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            margin</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">5</span><span style="color:#D73A49;--s-dark:#F97583">px</span><span style="color:#005CC5;--s-dark:#79B8FF"> 0</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        }</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">        .memo-preview</span><span style="color:#22863A;--s-dark:#85E89D"> blockquote</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            border-left</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">2</span><span style="color:#D73A49;--s-dark:#F97583">px</span><span style="color:#005CC5;--s-dark:#79B8FF"> solid</span><span style="color:#005CC5;--s-dark:#79B8FF"> #e2e2e3</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            padding-left</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">16</span><span style="color:#D73A49;--s-dark:#F97583">px</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            color</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">rgba</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#005CC5;--s-dark:#79B8FF">60</span><span style="color:#24292E;--s-dark:#E1E4E8">, </span><span style="color:#005CC5;--s-dark:#79B8FF">60</span><span style="color:#24292E;--s-dark:#E1E4E8">, </span><span style="color:#005CC5;--s-dark:#79B8FF">67</span><span style="color:#24292E;--s-dark:#E1E4E8">, </span><span style="color:#005CC5;--s-dark:#79B8FF">.78</span><span style="color:#24292E;--s-dark:#E1E4E8">);</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        }</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">        .memo-actions</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            padding</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">1</span><span style="color:#D73A49;--s-dark:#F97583">rem</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            border-top</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">1</span><span style="color:#D73A49;--s-dark:#F97583">px</span><span style="color:#005CC5;--s-dark:#79B8FF"> solid</span><span style="color:#005CC5;--s-dark:#79B8FF"> var</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#E36209;--s-dark:#FFAB70">--border-color</span><span style="color:#24292E;--s-dark:#E1E4E8">);</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            display</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">flex</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            justify-content</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">flex-end</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            gap</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">1</span><span style="color:#D73A49;--s-dark:#F97583">rem</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        }</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">        .pagination</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            padding</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">1</span><span style="color:#D73A49;--s-dark:#F97583">rem</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            display</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">flex</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            justify-content</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">center</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            align-items</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">center</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            gap</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">1</span><span style="color:#D73A49;--s-dark:#F97583">rem</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            border-top</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">1</span><span style="color:#D73A49;--s-dark:#F97583">px</span><span style="color:#005CC5;--s-dark:#79B8FF"> solid</span><span style="color:#005CC5;--s-dark:#79B8FF"> var</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#E36209;--s-dark:#FFAB70">--border-color</span><span style="color:#24292E;--s-dark:#E1E4E8">);</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        }</span></span>
<span class="line"></span>
<span class="line"><span style="color:#22863A;--s-dark:#85E89D">        button</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            padding</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">0.5</span><span style="color:#D73A49;--s-dark:#F97583">rem</span><span style="color:#005CC5;--s-dark:#79B8FF"> 1</span><span style="color:#D73A49;--s-dark:#F97583">rem</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            border</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">none</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            border-radius</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">5</span><span style="color:#D73A49;--s-dark:#F97583">px</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            cursor</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">pointer</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            transition</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">all</span><span style="color:#005CC5;--s-dark:#79B8FF"> 0.2</span><span style="color:#D73A49;--s-dark:#F97583">s</span><span style="color:#005CC5;--s-dark:#79B8FF"> ease</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            font-size</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">0.9</span><span style="color:#D73A49;--s-dark:#F97583">rem</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            background-color</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">var</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#E36209;--s-dark:#FFAB70">--primary-color</span><span style="color:#24292E;--s-dark:#E1E4E8">);</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            color</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">white</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        }</span></span>
<span class="line"></span>
<span class="line"><span style="color:#22863A;--s-dark:#85E89D">        button</span><span style="color:#6F42C1;--s-dark:#B392F0">:hover</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            opacity</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">0.9</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        }</span></span>
<span class="line"></span>
<span class="line"><span style="color:#22863A;--s-dark:#85E89D">        button</span><span style="color:#6F42C1;--s-dark:#B392F0">.secondary</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            background-color</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">var</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#E36209;--s-dark:#FFAB70">--secondary-color</span><span style="color:#24292E;--s-dark:#E1E4E8">);</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        }</span></span>
<span class="line"></span>
<span class="line"><span style="color:#22863A;--s-dark:#85E89D">        button</span><span style="color:#6F42C1;--s-dark:#B392F0">.danger</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            background-color</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">#e74c3c</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        }</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">        .create-btn</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            padding</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">0.5</span><span style="color:#D73A49;--s-dark:#F97583">rem</span><span style="color:#005CC5;--s-dark:#79B8FF"> 1</span><span style="color:#D73A49;--s-dark:#F97583">rem</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            display</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">flex</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            align-items</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">center</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            gap</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">0.5</span><span style="color:#D73A49;--s-dark:#F97583">rem</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            margin-right</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">10</span><span style="color:#D73A49;--s-dark:#F97583">px</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            margin-top</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">10</span><span style="color:#D73A49;--s-dark:#F97583">px</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        }</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">        /* Responsive Design */</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">        @media</span><span style="color:#24292E;--s-dark:#E1E4E8"> (</span><span style="color:#005CC5;--s-dark:#79B8FF">max-width</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">768</span><span style="color:#D73A49;--s-dark:#F97583">px</span><span style="color:#24292E;--s-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">            .container</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">                flex-direction</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">column</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">                height</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">auto</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            }</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">            .memo-list</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">                width</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">100</span><span style="color:#D73A49;--s-dark:#F97583">%</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">                height</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">300</span><span style="color:#D73A49;--s-dark:#F97583">px</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            }</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">            .memo-detail</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">                height</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">calc</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#005CC5;--s-dark:#79B8FF">100</span><span style="color:#D73A49;--s-dark:#F97583">vh</span><span style="color:#D73A49;--s-dark:#F97583"> -</span><span style="color:#005CC5;--s-dark:#79B8FF"> 400</span><span style="color:#D73A49;--s-dark:#F97583">px</span><span style="color:#24292E;--s-dark:#E1E4E8">);</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            }</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">            .memo-preview</span><span style="color:#22863A;--s-dark:#85E89D"> img</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">                max-height</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">100</span><span style="color:#D73A49;--s-dark:#F97583">px</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            }</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        }</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">        /* Markdown Preview Styles */</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">        .memo-preview</span><span style="color:#22863A;--s-dark:#85E89D"> h1</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">        .memo-preview</span><span style="color:#22863A;--s-dark:#85E89D"> h2</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">        .memo-preview</span><span style="color:#22863A;--s-dark:#85E89D"> h3</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            margin-top</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">1</span><span style="color:#D73A49;--s-dark:#F97583">rem</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            margin-bottom</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">0.5</span><span style="color:#D73A49;--s-dark:#F97583">rem</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        }</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">        .memo-preview</span><span style="color:#22863A;--s-dark:#85E89D"> p</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            margin-bottom</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">1</span><span style="color:#D73A49;--s-dark:#F97583">rem</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        }</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">        .memo-preview</span><span style="color:#22863A;--s-dark:#85E89D"> code</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            background-color</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">#f8f9fa</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            padding</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">0.2</span><span style="color:#D73A49;--s-dark:#F97583">rem</span><span style="color:#005CC5;--s-dark:#79B8FF"> 0.4</span><span style="color:#D73A49;--s-dark:#F97583">rem</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            border-radius</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">3</span><span style="color:#D73A49;--s-dark:#F97583">px</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            font-family</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">monospace</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        }</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">        .memo-preview</span><span style="color:#22863A;--s-dark:#85E89D"> pre</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            background-color</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">#f8f9fa</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            padding</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">1</span><span style="color:#D73A49;--s-dark:#F97583">rem</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            border-radius</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">5</span><span style="color:#D73A49;--s-dark:#F97583">px</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            overflow-x</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">auto</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        }</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">        /* Loading Spinner */</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">        .loading</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            display</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">inline-block</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            width</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">20</span><span style="color:#D73A49;--s-dark:#F97583">px</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            height</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">20</span><span style="color:#D73A49;--s-dark:#F97583">px</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            border</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">2</span><span style="color:#D73A49;--s-dark:#F97583">px</span><span style="color:#005CC5;--s-dark:#79B8FF"> solid</span><span style="color:#005CC5;--s-dark:#79B8FF"> rgba</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#005CC5;--s-dark:#79B8FF">0</span><span style="color:#24292E;--s-dark:#E1E4E8">, </span><span style="color:#005CC5;--s-dark:#79B8FF">0</span><span style="color:#24292E;--s-dark:#E1E4E8">, </span><span style="color:#005CC5;--s-dark:#79B8FF">0</span><span style="color:#24292E;--s-dark:#E1E4E8">, </span><span style="color:#005CC5;--s-dark:#79B8FF">0.1</span><span style="color:#24292E;--s-dark:#E1E4E8">);</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            border-radius</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">50</span><span style="color:#D73A49;--s-dark:#F97583">%</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            border-top-color</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">var</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#E36209;--s-dark:#FFAB70">--accent-color</span><span style="color:#24292E;--s-dark:#E1E4E8">);</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            animation</span><span style="color:#24292E;--s-dark:#E1E4E8">: spin </span><span style="color:#005CC5;--s-dark:#79B8FF">1</span><span style="color:#D73A49;--s-dark:#F97583">s</span><span style="color:#005CC5;--s-dark:#79B8FF"> ease-in-out</span><span style="color:#005CC5;--s-dark:#79B8FF"> infinite</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        }</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">        @keyframes</span><span style="color:#E36209;--s-dark:#FFAB70"> spin</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">            to</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">                transform</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">rotate</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#005CC5;--s-dark:#79B8FF">360</span><span style="color:#D73A49;--s-dark:#F97583">deg</span><span style="color:#24292E;--s-dark:#E1E4E8">);</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            }</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        }</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    &#x3C;/</span><span style="color:#22863A;--s-dark:#85E89D">style</span><span style="color:#24292E;--s-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">&#x3C;/</span><span style="color:#22863A;--s-dark:#85E89D">head</span><span style="color:#24292E;--s-dark:#E1E4E8">></span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">&#x3C;</span><span style="color:#22863A;--s-dark:#85E89D">body</span><span style="color:#24292E;--s-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    &#x3C;</span><span style="color:#22863A;--s-dark:#85E89D">div</span><span style="color:#6F42C1;--s-dark:#B392F0"> id</span><span style="color:#24292E;--s-dark:#E1E4E8">=</span><span style="color:#032F62;--s-dark:#9ECBFF">"auth-panel"</span><span style="color:#24292E;--s-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        &#x3C;</span><span style="color:#22863A;--s-dark:#85E89D">form</span><span style="color:#6F42C1;--s-dark:#B392F0"> id</span><span style="color:#24292E;--s-dark:#E1E4E8">=</span><span style="color:#032F62;--s-dark:#9ECBFF">"auth-form"</span><span style="color:#24292E;--s-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            &#x3C;</span><span style="color:#22863A;--s-dark:#85E89D">h2</span><span style="color:#6F42C1;--s-dark:#B392F0"> style</span><span style="color:#24292E;--s-dark:#E1E4E8">=</span><span style="color:#032F62;--s-dark:#9ECBFF">"margin-bottom: 1rem;"</span><span style="color:#24292E;--s-dark:#E1E4E8">>Memos 管理&#x3C;/</span><span style="color:#22863A;--s-dark:#85E89D">h2</span><span style="color:#24292E;--s-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            &#x3C;</span><span style="color:#22863A;--s-dark:#85E89D">input</span><span style="color:#6F42C1;--s-dark:#B392F0"> type</span><span style="color:#24292E;--s-dark:#E1E4E8">=</span><span style="color:#032F62;--s-dark:#9ECBFF">"password"</span><span style="color:#6F42C1;--s-dark:#B392F0"> id</span><span style="color:#24292E;--s-dark:#E1E4E8">=</span><span style="color:#032F62;--s-dark:#9ECBFF">"password"</span><span style="color:#6F42C1;--s-dark:#B392F0"> placeholder</span><span style="color:#24292E;--s-dark:#E1E4E8">=</span><span style="color:#032F62;--s-dark:#9ECBFF">"Enter password"</span><span style="color:#6F42C1;--s-dark:#B392F0"> required</span><span style="color:#24292E;--s-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            &#x3C;</span><span style="color:#22863A;--s-dark:#85E89D">button</span><span style="color:#6F42C1;--s-dark:#B392F0"> type</span><span style="color:#24292E;--s-dark:#E1E4E8">=</span><span style="color:#032F62;--s-dark:#9ECBFF">"submit"</span><span style="color:#6F42C1;--s-dark:#B392F0"> style</span><span style="color:#24292E;--s-dark:#E1E4E8">=</span><span style="color:#032F62;--s-dark:#9ECBFF">"width: 100%"</span><span style="color:#24292E;--s-dark:#E1E4E8">>登录&#x3C;/</span><span style="color:#22863A;--s-dark:#85E89D">button</span><span style="color:#24292E;--s-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        &#x3C;/</span><span style="color:#22863A;--s-dark:#85E89D">form</span><span style="color:#24292E;--s-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    &#x3C;/</span><span style="color:#22863A;--s-dark:#85E89D">div</span><span style="color:#24292E;--s-dark:#E1E4E8">></span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    &#x3C;</span><span style="color:#22863A;--s-dark:#85E89D">div</span><span style="color:#6F42C1;--s-dark:#B392F0"> class</span><span style="color:#24292E;--s-dark:#E1E4E8">=</span><span style="color:#032F62;--s-dark:#9ECBFF">"container"</span><span style="color:#24292E;--s-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        &#x3C;</span><span style="color:#22863A;--s-dark:#85E89D">div</span><span style="color:#6F42C1;--s-dark:#B392F0"> class</span><span style="color:#24292E;--s-dark:#E1E4E8">=</span><span style="color:#032F62;--s-dark:#9ECBFF">"memo-list"</span><span style="color:#24292E;--s-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            &#x3C;</span><span style="color:#22863A;--s-dark:#85E89D">div</span><span style="color:#6F42C1;--s-dark:#B392F0"> class</span><span style="color:#24292E;--s-dark:#E1E4E8">=</span><span style="color:#032F62;--s-dark:#9ECBFF">"memo-list-header"</span><span style="color:#24292E;--s-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">                &#x3C;</span><span style="color:#22863A;--s-dark:#85E89D">span</span><span style="color:#24292E;--s-dark:#E1E4E8">>已发布&#x3C;/</span><span style="color:#22863A;--s-dark:#85E89D">span</span><span style="color:#24292E;--s-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">                &#x3C;</span><span style="color:#22863A;--s-dark:#85E89D">span</span><span style="color:#6F42C1;--s-dark:#B392F0"> id</span><span style="color:#24292E;--s-dark:#E1E4E8">=</span><span style="color:#032F62;--s-dark:#9ECBFF">"memo-count"</span><span style="color:#24292E;--s-dark:#E1E4E8">>&#x3C;/</span><span style="color:#22863A;--s-dark:#85E89D">span</span><span style="color:#24292E;--s-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            &#x3C;/</span><span style="color:#22863A;--s-dark:#85E89D">div</span><span style="color:#24292E;--s-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            &#x3C;</span><span style="color:#22863A;--s-dark:#85E89D">div</span><span style="color:#6F42C1;--s-dark:#B392F0"> class</span><span style="color:#24292E;--s-dark:#E1E4E8">=</span><span style="color:#032F62;--s-dark:#9ECBFF">"memo-items"</span><span style="color:#6F42C1;--s-dark:#B392F0"> id</span><span style="color:#24292E;--s-dark:#E1E4E8">=</span><span style="color:#032F62;--s-dark:#9ECBFF">"memo-items"</span><span style="color:#24292E;--s-dark:#E1E4E8">>&#x3C;/</span><span style="color:#22863A;--s-dark:#85E89D">div</span><span style="color:#24292E;--s-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            &#x3C;</span><span style="color:#22863A;--s-dark:#85E89D">div</span><span style="color:#6F42C1;--s-dark:#B392F0"> class</span><span style="color:#24292E;--s-dark:#E1E4E8">=</span><span style="color:#032F62;--s-dark:#9ECBFF">"pagination"</span><span style="color:#24292E;--s-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">                &#x3C;</span><span style="color:#22863A;--s-dark:#85E89D">button</span><span style="color:#6F42C1;--s-dark:#B392F0"> onclick</span><span style="color:#24292E;--s-dark:#E1E4E8">=</span><span style="color:#032F62;--s-dark:#9ECBFF">"</span><span style="color:#6F42C1;--s-dark:#B392F0">prevPage</span><span style="color:#032F62;--s-dark:#9ECBFF">()"</span><span style="color:#6F42C1;--s-dark:#B392F0"> class</span><span style="color:#24292E;--s-dark:#E1E4E8">=</span><span style="color:#032F62;--s-dark:#9ECBFF">"secondary"</span><span style="color:#24292E;--s-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">                    &#x3C;</span><span style="color:#22863A;--s-dark:#85E89D">i</span><span style="color:#6F42C1;--s-dark:#B392F0"> class</span><span style="color:#24292E;--s-dark:#E1E4E8">=</span><span style="color:#032F62;--s-dark:#9ECBFF">"fas fa-chevron-left"</span><span style="color:#24292E;--s-dark:#E1E4E8">>&#x3C;/</span><span style="color:#22863A;--s-dark:#85E89D">i</span><span style="color:#24292E;--s-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">                &#x3C;/</span><span style="color:#22863A;--s-dark:#85E89D">button</span><span style="color:#24292E;--s-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">                &#x3C;</span><span style="color:#22863A;--s-dark:#85E89D">span</span><span style="color:#6F42C1;--s-dark:#B392F0"> id</span><span style="color:#24292E;--s-dark:#E1E4E8">=</span><span style="color:#032F62;--s-dark:#9ECBFF">"page-info"</span><span style="color:#24292E;--s-dark:#E1E4E8">>&#x3C;/</span><span style="color:#22863A;--s-dark:#85E89D">span</span><span style="color:#24292E;--s-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">                &#x3C;</span><span style="color:#22863A;--s-dark:#85E89D">button</span><span style="color:#6F42C1;--s-dark:#B392F0"> onclick</span><span style="color:#24292E;--s-dark:#E1E4E8">=</span><span style="color:#032F62;--s-dark:#9ECBFF">"</span><span style="color:#6F42C1;--s-dark:#B392F0">nextPage</span><span style="color:#032F62;--s-dark:#9ECBFF">()"</span><span style="color:#6F42C1;--s-dark:#B392F0"> class</span><span style="color:#24292E;--s-dark:#E1E4E8">=</span><span style="color:#032F62;--s-dark:#9ECBFF">"secondary"</span><span style="color:#24292E;--s-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">                    &#x3C;</span><span style="color:#22863A;--s-dark:#85E89D">i</span><span style="color:#6F42C1;--s-dark:#B392F0"> class</span><span style="color:#24292E;--s-dark:#E1E4E8">=</span><span style="color:#032F62;--s-dark:#9ECBFF">"fas fa-chevron-right"</span><span style="color:#24292E;--s-dark:#E1E4E8">>&#x3C;/</span><span style="color:#22863A;--s-dark:#85E89D">i</span><span style="color:#24292E;--s-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">                &#x3C;/</span><span style="color:#22863A;--s-dark:#85E89D">button</span><span style="color:#24292E;--s-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            &#x3C;/</span><span style="color:#22863A;--s-dark:#85E89D">div</span><span style="color:#24292E;--s-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        &#x3C;/</span><span style="color:#22863A;--s-dark:#85E89D">div</span><span style="color:#24292E;--s-dark:#E1E4E8">></span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        &#x3C;</span><span style="color:#22863A;--s-dark:#85E89D">div</span><span style="color:#6F42C1;--s-dark:#B392F0"> class</span><span style="color:#24292E;--s-dark:#E1E4E8">=</span><span style="color:#032F62;--s-dark:#9ECBFF">"memo-detail"</span><span style="color:#24292E;--s-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            &#x3C;</span><span style="color:#22863A;--s-dark:#85E89D">div</span><span style="color:#6F42C1;--s-dark:#B392F0"> class</span><span style="color:#24292E;--s-dark:#E1E4E8">=</span><span style="color:#032F62;--s-dark:#9ECBFF">"memo-detail-header"</span><span style="color:#24292E;--s-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">                &#x3C;</span><span style="color:#22863A;--s-dark:#85E89D">div</span><span style="color:#6F42C1;--s-dark:#B392F0"> class</span><span style="color:#24292E;--s-dark:#E1E4E8">=</span><span style="color:#032F62;--s-dark:#9ECBFF">"memo-info"</span><span style="color:#6F42C1;--s-dark:#B392F0"> id</span><span style="color:#24292E;--s-dark:#E1E4E8">=</span><span style="color:#032F62;--s-dark:#9ECBFF">"memo-info"</span><span style="color:#24292E;--s-dark:#E1E4E8">>新 Memo&#x3C;/</span><span style="color:#22863A;--s-dark:#85E89D">div</span><span style="color:#24292E;--s-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">                &#x3C;</span><span style="color:#22863A;--s-dark:#85E89D">button</span><span style="color:#6F42C1;--s-dark:#B392F0"> class</span><span style="color:#24292E;--s-dark:#E1E4E8">=</span><span style="color:#032F62;--s-dark:#9ECBFF">"create-btn"</span><span style="color:#6F42C1;--s-dark:#B392F0"> onclick</span><span style="color:#24292E;--s-dark:#E1E4E8">=</span><span style="color:#032F62;--s-dark:#9ECBFF">"</span><span style="color:#6F42C1;--s-dark:#B392F0">createMemo</span><span style="color:#032F62;--s-dark:#9ECBFF">()"</span><span style="color:#24292E;--s-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">                    &#x3C;</span><span style="color:#22863A;--s-dark:#85E89D">i</span><span style="color:#6F42C1;--s-dark:#B392F0"> class</span><span style="color:#24292E;--s-dark:#E1E4E8">=</span><span style="color:#032F62;--s-dark:#9ECBFF">"fas fa-plus"</span><span style="color:#24292E;--s-dark:#E1E4E8">>&#x3C;/</span><span style="color:#22863A;--s-dark:#85E89D">i</span><span style="color:#24292E;--s-dark:#E1E4E8">> 发布新 Memo</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">                &#x3C;/</span><span style="color:#22863A;--s-dark:#85E89D">button</span><span style="color:#24292E;--s-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            &#x3C;/</span><span style="color:#22863A;--s-dark:#85E89D">div</span><span style="color:#24292E;--s-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            &#x3C;</span><span style="color:#22863A;--s-dark:#85E89D">div</span><span style="color:#6F42C1;--s-dark:#B392F0"> class</span><span style="color:#24292E;--s-dark:#E1E4E8">=</span><span style="color:#032F62;--s-dark:#9ECBFF">"memo-content"</span><span style="color:#24292E;--s-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">                &#x3C;</span><span style="color:#22863A;--s-dark:#85E89D">div</span><span style="color:#6F42C1;--s-dark:#B392F0"> class</span><span style="color:#24292E;--s-dark:#E1E4E8">=</span><span style="color:#032F62;--s-dark:#9ECBFF">"memo-edit"</span><span style="color:#24292E;--s-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">                    &#x3C;</span><span style="color:#22863A;--s-dark:#85E89D">textarea</span><span style="color:#6F42C1;--s-dark:#B392F0"> id</span><span style="color:#24292E;--s-dark:#E1E4E8">=</span><span style="color:#032F62;--s-dark:#9ECBFF">"memo-content"</span><span style="color:#6F42C1;--s-dark:#B392F0"> placeholder</span><span style="color:#24292E;--s-dark:#E1E4E8">=</span><span style="color:#032F62;--s-dark:#9ECBFF">"Write your memo here..."</span><span style="color:#24292E;--s-dark:#E1E4E8">>&#x3C;/</span><span style="color:#22863A;--s-dark:#85E89D">textarea</span><span style="color:#24292E;--s-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">                &#x3C;/</span><span style="color:#22863A;--s-dark:#85E89D">div</span><span style="color:#24292E;--s-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">                &#x3C;</span><span style="color:#22863A;--s-dark:#85E89D">div</span><span style="color:#6F42C1;--s-dark:#B392F0"> class</span><span style="color:#24292E;--s-dark:#E1E4E8">=</span><span style="color:#032F62;--s-dark:#9ECBFF">"memo-preview"</span><span style="color:#6F42C1;--s-dark:#B392F0"> id</span><span style="color:#24292E;--s-dark:#E1E4E8">=</span><span style="color:#032F62;--s-dark:#9ECBFF">"memo-preview"</span><span style="color:#24292E;--s-dark:#E1E4E8">>&#x3C;/</span><span style="color:#22863A;--s-dark:#85E89D">div</span><span style="color:#24292E;--s-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            &#x3C;/</span><span style="color:#22863A;--s-dark:#85E89D">div</span><span style="color:#24292E;--s-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            &#x3C;</span><span style="color:#22863A;--s-dark:#85E89D">div</span><span style="color:#6F42C1;--s-dark:#B392F0"> class</span><span style="color:#24292E;--s-dark:#E1E4E8">=</span><span style="color:#032F62;--s-dark:#9ECBFF">"memo-actions"</span><span style="color:#24292E;--s-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">                &#x3C;</span><span style="color:#22863A;--s-dark:#85E89D">button</span><span style="color:#6F42C1;--s-dark:#B392F0"> onclick</span><span style="color:#24292E;--s-dark:#E1E4E8">=</span><span style="color:#032F62;--s-dark:#9ECBFF">"</span><span style="color:#6F42C1;--s-dark:#B392F0">saveMemo</span><span style="color:#032F62;--s-dark:#9ECBFF">()"</span><span style="color:#6F42C1;--s-dark:#B392F0"> id</span><span style="color:#24292E;--s-dark:#E1E4E8">=</span><span style="color:#032F62;--s-dark:#9ECBFF">"save-btn"</span><span style="color:#24292E;--s-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">                    &#x3C;</span><span style="color:#22863A;--s-dark:#85E89D">i</span><span style="color:#6F42C1;--s-dark:#B392F0"> class</span><span style="color:#24292E;--s-dark:#E1E4E8">=</span><span style="color:#032F62;--s-dark:#9ECBFF">"fas fa-save"</span><span style="color:#24292E;--s-dark:#E1E4E8">>&#x3C;/</span><span style="color:#22863A;--s-dark:#85E89D">i</span><span style="color:#24292E;--s-dark:#E1E4E8">> 保存</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">                &#x3C;/</span><span style="color:#22863A;--s-dark:#85E89D">button</span><span style="color:#24292E;--s-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">                &#x3C;</span><span style="color:#22863A;--s-dark:#85E89D">button</span><span style="color:#6F42C1;--s-dark:#B392F0"> onclick</span><span style="color:#24292E;--s-dark:#E1E4E8">=</span><span style="color:#032F62;--s-dark:#9ECBFF">"</span><span style="color:#6F42C1;--s-dark:#B392F0">deleteMemo</span><span style="color:#032F62;--s-dark:#9ECBFF">()"</span><span style="color:#6F42C1;--s-dark:#B392F0"> class</span><span style="color:#24292E;--s-dark:#E1E4E8">=</span><span style="color:#032F62;--s-dark:#9ECBFF">"danger"</span><span style="color:#6F42C1;--s-dark:#B392F0"> id</span><span style="color:#24292E;--s-dark:#E1E4E8">=</span><span style="color:#032F62;--s-dark:#9ECBFF">"delete-btn"</span><span style="color:#24292E;--s-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">                    &#x3C;</span><span style="color:#22863A;--s-dark:#85E89D">i</span><span style="color:#6F42C1;--s-dark:#B392F0"> class</span><span style="color:#24292E;--s-dark:#E1E4E8">=</span><span style="color:#032F62;--s-dark:#9ECBFF">"fas fa-trash"</span><span style="color:#24292E;--s-dark:#E1E4E8">>&#x3C;/</span><span style="color:#22863A;--s-dark:#85E89D">i</span><span style="color:#24292E;--s-dark:#E1E4E8">> 删除</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">                &#x3C;/</span><span style="color:#22863A;--s-dark:#85E89D">button</span><span style="color:#24292E;--s-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            &#x3C;/</span><span style="color:#22863A;--s-dark:#85E89D">div</span><span style="color:#24292E;--s-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        &#x3C;/</span><span style="color:#22863A;--s-dark:#85E89D">div</span><span style="color:#24292E;--s-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    &#x3C;/</span><span style="color:#22863A;--s-dark:#85E89D">div</span><span style="color:#24292E;--s-dark:#E1E4E8">></span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    &#x3C;</span><span style="color:#22863A;--s-dark:#85E89D">script</span><span style="color:#6F42C1;--s-dark:#B392F0"> src</span><span style="color:#24292E;--s-dark:#E1E4E8">=</span><span style="color:#032F62;--s-dark:#9ECBFF">"https://cdnjs.cloudflare.com/ajax/libs/marked/2.0.3/marked.min.js"</span><span style="color:#24292E;--s-dark:#E1E4E8">>&#x3C;/</span><span style="color:#22863A;--s-dark:#85E89D">script</span><span style="color:#24292E;--s-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">    &#x3C;!-- JavaScript 代码与之前相同，但需要添加以下功能增强 --></span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    &#x3C;</span><span style="color:#22863A;--s-dark:#85E89D">script</span><span style="color:#24292E;--s-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">        let</span><span style="color:#24292E;--s-dark:#E1E4E8"> password </span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#032F62;--s-dark:#9ECBFF"> ''</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">        let</span><span style="color:#24292E;--s-dark:#E1E4E8"> currentMemo </span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#005CC5;--s-dark:#79B8FF"> null</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">        let</span><span style="color:#24292E;--s-dark:#E1E4E8"> offset </span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#005CC5;--s-dark:#79B8FF"> 0</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">        const</span><span style="color:#005CC5;--s-dark:#79B8FF"> limit</span><span style="color:#D73A49;--s-dark:#F97583"> =</span><span style="color:#005CC5;--s-dark:#79B8FF"> 10</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">        let</span><span style="color:#24292E;--s-dark:#E1E4E8"> total </span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#005CC5;--s-dark:#79B8FF"> 0</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">        let</span><span style="color:#24292E;--s-dark:#E1E4E8"> currentPageMap </span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#24292E;--s-dark:#E1E4E8"> {};</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">        // Authentication</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        document.</span><span style="color:#6F42C1;--s-dark:#B392F0">getElementById</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#032F62;--s-dark:#9ECBFF">'auth-form'</span><span style="color:#24292E;--s-dark:#E1E4E8">).</span><span style="color:#6F42C1;--s-dark:#B392F0">addEventListener</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#032F62;--s-dark:#9ECBFF">'submit'</span><span style="color:#24292E;--s-dark:#E1E4E8">, </span><span style="color:#D73A49;--s-dark:#F97583">async</span><span style="color:#24292E;--s-dark:#E1E4E8"> (</span><span style="color:#E36209;--s-dark:#FFAB70">e</span><span style="color:#24292E;--s-dark:#E1E4E8">) </span><span style="color:#D73A49;--s-dark:#F97583">=></span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            e.</span><span style="color:#6F42C1;--s-dark:#B392F0">preventDefault</span><span style="color:#24292E;--s-dark:#E1E4E8">();</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            password </span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#24292E;--s-dark:#E1E4E8"> document.</span><span style="color:#6F42C1;--s-dark:#B392F0">getElementById</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#032F62;--s-dark:#9ECBFF">'password'</span><span style="color:#24292E;--s-dark:#E1E4E8">).value;</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">            try</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">                const</span><span style="color:#005CC5;--s-dark:#79B8FF"> response</span><span style="color:#D73A49;--s-dark:#F97583"> =</span><span style="color:#D73A49;--s-dark:#F97583"> await</span><span style="color:#6F42C1;--s-dark:#B392F0"> fetch</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#032F62;--s-dark:#9ECBFF">'/api/auth'</span><span style="color:#24292E;--s-dark:#E1E4E8">, {</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">                    method: </span><span style="color:#032F62;--s-dark:#9ECBFF">'POST'</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">                    headers: {</span></span>
<span class="line"><span style="color:#032F62;--s-dark:#9ECBFF">                        'Content-Type'</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">'application/json'</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">                    },</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">                    body: </span><span style="color:#005CC5;--s-dark:#79B8FF">JSON</span><span style="color:#24292E;--s-dark:#E1E4E8">.</span><span style="color:#6F42C1;--s-dark:#B392F0">stringify</span><span style="color:#24292E;--s-dark:#E1E4E8">({ password }),</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">                });</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">                if</span><span style="color:#24292E;--s-dark:#E1E4E8"> (response.ok) {</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">                    document.</span><span style="color:#6F42C1;--s-dark:#B392F0">getElementById</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#032F62;--s-dark:#9ECBFF">'auth-panel'</span><span style="color:#24292E;--s-dark:#E1E4E8">).style.display </span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#032F62;--s-dark:#9ECBFF"> 'none'</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">                    loadMemos</span><span style="color:#24292E;--s-dark:#E1E4E8">();</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">                } </span><span style="color:#D73A49;--s-dark:#F97583">else</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">                    showNotification</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#032F62;--s-dark:#9ECBFF">'密码错误'</span><span style="color:#24292E;--s-dark:#E1E4E8">, </span><span style="color:#032F62;--s-dark:#9ECBFF">'error'</span><span style="color:#24292E;--s-dark:#E1E4E8">);</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">                }</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            } </span><span style="color:#D73A49;--s-dark:#F97583">catch</span><span style="color:#24292E;--s-dark:#E1E4E8"> (error) {</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">                showNotification</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#032F62;--s-dark:#9ECBFF">'密码错误'</span><span style="color:#24292E;--s-dark:#E1E4E8">, </span><span style="color:#032F62;--s-dark:#9ECBFF">'error'</span><span style="color:#24292E;--s-dark:#E1E4E8">);</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            }</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        });</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">        // Load memos</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">        async</span><span style="color:#D73A49;--s-dark:#F97583"> function</span><span style="color:#6F42C1;--s-dark:#B392F0"> loadMemos</span><span style="color:#24292E;--s-dark:#E1E4E8">() {</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">            try</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">                const</span><span style="color:#005CC5;--s-dark:#79B8FF"> response</span><span style="color:#D73A49;--s-dark:#F97583"> =</span><span style="color:#D73A49;--s-dark:#F97583"> await</span><span style="color:#6F42C1;--s-dark:#B392F0"> fetch</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#032F62;--s-dark:#9ECBFF">`/api/memos?offset=${</span><span style="color:#24292E;--s-dark:#E1E4E8">offset</span><span style="color:#032F62;--s-dark:#9ECBFF">}&#x26;limit=${</span><span style="color:#24292E;--s-dark:#E1E4E8">limit</span><span style="color:#032F62;--s-dark:#9ECBFF">}`</span><span style="color:#24292E;--s-dark:#E1E4E8">);</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">                const</span><span style="color:#005CC5;--s-dark:#79B8FF"> data</span><span style="color:#D73A49;--s-dark:#F97583"> =</span><span style="color:#D73A49;--s-dark:#F97583"> await</span><span style="color:#24292E;--s-dark:#E1E4E8"> response.</span><span style="color:#6F42C1;--s-dark:#B392F0">json</span><span style="color:#24292E;--s-dark:#E1E4E8">();</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">                displayMemos</span><span style="color:#24292E;--s-dark:#E1E4E8">(data.data);</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">                currentPageMap </span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#24292E;--s-dark:#E1E4E8"> data.data.</span><span style="color:#6F42C1;--s-dark:#B392F0">reduce</span><span style="color:#24292E;--s-dark:#E1E4E8">((</span><span style="color:#E36209;--s-dark:#FFAB70">acc</span><span style="color:#24292E;--s-dark:#E1E4E8">, </span><span style="color:#E36209;--s-dark:#FFAB70">item</span><span style="color:#24292E;--s-dark:#E1E4E8">) </span><span style="color:#D73A49;--s-dark:#F97583">=></span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">                    acc[item.uid] </span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#24292E;--s-dark:#E1E4E8"> item;</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">                    return</span><span style="color:#24292E;--s-dark:#E1E4E8"> acc;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">                }, {})</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">                total </span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#24292E;--s-dark:#E1E4E8"> data.total;</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">                updatePagination</span><span style="color:#24292E;--s-dark:#E1E4E8">();</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">                document.</span><span style="color:#6F42C1;--s-dark:#B392F0">getElementById</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#032F62;--s-dark:#9ECBFF">'memo-count'</span><span style="color:#24292E;--s-dark:#E1E4E8">).textContent </span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#032F62;--s-dark:#9ECBFF"> `${</span><span style="color:#24292E;--s-dark:#E1E4E8">total</span><span style="color:#032F62;--s-dark:#9ECBFF">} memos`</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            } </span><span style="color:#D73A49;--s-dark:#F97583">catch</span><span style="color:#24292E;--s-dark:#E1E4E8"> (error) {</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">                showNotification</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#032F62;--s-dark:#9ECBFF">'加载列表错误'</span><span style="color:#24292E;--s-dark:#E1E4E8">, </span><span style="color:#032F62;--s-dark:#9ECBFF">'error'</span><span style="color:#24292E;--s-dark:#E1E4E8">);</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            }</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        }</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">        function</span><span style="color:#6F42C1;--s-dark:#B392F0"> displayMemos</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#E36209;--s-dark:#FFAB70">memos</span><span style="color:#24292E;--s-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">            const</span><span style="color:#005CC5;--s-dark:#79B8FF"> container</span><span style="color:#D73A49;--s-dark:#F97583"> =</span><span style="color:#24292E;--s-dark:#E1E4E8"> document.</span><span style="color:#6F42C1;--s-dark:#B392F0">getElementById</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#032F62;--s-dark:#9ECBFF">'memo-items'</span><span style="color:#24292E;--s-dark:#E1E4E8">);</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            container.innerHTML </span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#24292E;--s-dark:#E1E4E8"> memos.</span><span style="color:#6F42C1;--s-dark:#B392F0">map</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#E36209;--s-dark:#FFAB70">memo</span><span style="color:#D73A49;--s-dark:#F97583"> =></span><span style="color:#032F62;--s-dark:#9ECBFF"> `</span></span>
<span class="line"><span style="color:#032F62;--s-dark:#9ECBFF">        &#x3C;div class="memo-item" data-id="${</span><span style="color:#24292E;--s-dark:#E1E4E8">memo</span><span style="color:#032F62;--s-dark:#9ECBFF">.</span><span style="color:#24292E;--s-dark:#E1E4E8">uid</span><span style="color:#032F62;--s-dark:#9ECBFF">}" onclick="selectMemo('${</span><span style="color:#24292E;--s-dark:#E1E4E8">memo</span><span style="color:#032F62;--s-dark:#9ECBFF">.</span><span style="color:#24292E;--s-dark:#E1E4E8">uid</span><span style="color:#032F62;--s-dark:#9ECBFF">}')"></span></span>
<span class="line"><span style="color:#032F62;--s-dark:#9ECBFF">            &#x3C;div class="memo-item-header"></span></span>
<span class="line"><span style="color:#032F62;--s-dark:#9ECBFF">                &#x3C;span class="memo-uid">${</span><span style="color:#24292E;--s-dark:#E1E4E8">memo</span><span style="color:#032F62;--s-dark:#9ECBFF">.</span><span style="color:#24292E;--s-dark:#E1E4E8">uid</span><span style="color:#032F62;--s-dark:#9ECBFF">.</span><span style="color:#6F42C1;--s-dark:#B392F0">slice</span><span style="color:#032F62;--s-dark:#9ECBFF">(</span><span style="color:#005CC5;--s-dark:#79B8FF">0</span><span style="color:#032F62;--s-dark:#9ECBFF">, </span><span style="color:#005CC5;--s-dark:#79B8FF">8</span><span style="color:#032F62;--s-dark:#9ECBFF">)</span><span style="color:#032F62;--s-dark:#9ECBFF">}...&#x3C;/span></span></span>
<span class="line"><span style="color:#032F62;--s-dark:#9ECBFF">                &#x3C;span>${</span><span style="color:#D73A49;--s-dark:#F97583">new</span><span style="color:#6F42C1;--s-dark:#B392F0"> Date</span><span style="color:#032F62;--s-dark:#9ECBFF">(</span><span style="color:#24292E;--s-dark:#E1E4E8">memo</span><span style="color:#032F62;--s-dark:#9ECBFF">.</span><span style="color:#24292E;--s-dark:#E1E4E8">createTime</span><span style="color:#032F62;--s-dark:#9ECBFF">).</span><span style="color:#6F42C1;--s-dark:#B392F0">toLocaleString</span><span style="color:#032F62;--s-dark:#9ECBFF">()</span><span style="color:#032F62;--s-dark:#9ECBFF">}&#x3C;/span></span></span>
<span class="line"><span style="color:#032F62;--s-dark:#9ECBFF">            &#x3C;/div></span></span>
<span class="line"><span style="color:#032F62;--s-dark:#9ECBFF">            &#x3C;div class="memo-item-content"></span></span>
<span class="line"><span style="color:#032F62;--s-dark:#9ECBFF">                ${</span><span style="color:#6F42C1;--s-dark:#B392F0">escapeHtml</span><span style="color:#032F62;--s-dark:#9ECBFF">(</span><span style="color:#24292E;--s-dark:#E1E4E8">memo</span><span style="color:#032F62;--s-dark:#9ECBFF">.</span><span style="color:#24292E;--s-dark:#E1E4E8">content</span><span style="color:#032F62;--s-dark:#9ECBFF">)</span><span style="color:#032F62;--s-dark:#9ECBFF">}</span></span>
<span class="line"><span style="color:#032F62;--s-dark:#9ECBFF">            &#x3C;/div></span></span>
<span class="line"><span style="color:#032F62;--s-dark:#9ECBFF">        &#x3C;/div></span></span>
<span class="line"><span style="color:#032F62;--s-dark:#9ECBFF">    `</span><span style="color:#24292E;--s-dark:#E1E4E8">).</span><span style="color:#6F42C1;--s-dark:#B392F0">join</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#032F62;--s-dark:#9ECBFF">''</span><span style="color:#24292E;--s-dark:#E1E4E8">);</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        }</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">        async</span><span style="color:#D73A49;--s-dark:#F97583"> function</span><span style="color:#6F42C1;--s-dark:#B392F0"> selectMemo</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#E36209;--s-dark:#FFAB70">uid</span><span style="color:#24292E;--s-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">            try</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">                const</span><span style="color:#005CC5;--s-dark:#79B8FF"> memo</span><span style="color:#D73A49;--s-dark:#F97583"> =</span><span style="color:#24292E;--s-dark:#E1E4E8"> currentPageMap[uid];</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">                currentMemo </span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#24292E;--s-dark:#E1E4E8"> memo;</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">                displayMemoDetail</span><span style="color:#24292E;--s-dark:#E1E4E8">(memo);</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">                // Update selected state</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">                document.</span><span style="color:#6F42C1;--s-dark:#B392F0">querySelectorAll</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#032F62;--s-dark:#9ECBFF">'.memo-item'</span><span style="color:#24292E;--s-dark:#E1E4E8">).</span><span style="color:#6F42C1;--s-dark:#B392F0">forEach</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#E36209;--s-dark:#FFAB70">item</span><span style="color:#D73A49;--s-dark:#F97583"> =></span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">                    item.classList.</span><span style="color:#6F42C1;--s-dark:#B392F0">remove</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#032F62;--s-dark:#9ECBFF">'active'</span><span style="color:#24292E;--s-dark:#E1E4E8">);</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">                });</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">                document.</span><span style="color:#6F42C1;--s-dark:#B392F0">querySelector</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#032F62;--s-dark:#9ECBFF">`.memo-item[data-id="${</span><span style="color:#24292E;--s-dark:#E1E4E8">uid</span><span style="color:#032F62;--s-dark:#9ECBFF">}"]`</span><span style="color:#24292E;--s-dark:#E1E4E8">)?.classList.</span><span style="color:#6F42C1;--s-dark:#B392F0">add</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#032F62;--s-dark:#9ECBFF">'active'</span><span style="color:#24292E;--s-dark:#E1E4E8">);</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            } </span><span style="color:#D73A49;--s-dark:#F97583">catch</span><span style="color:#24292E;--s-dark:#E1E4E8"> (error) {</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">                showNotification</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#032F62;--s-dark:#9ECBFF">'加载 Memo 错误'</span><span style="color:#24292E;--s-dark:#E1E4E8">, </span><span style="color:#032F62;--s-dark:#9ECBFF">'error'</span><span style="color:#24292E;--s-dark:#E1E4E8">);</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            }</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        }</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">        function</span><span style="color:#6F42C1;--s-dark:#B392F0"> displayMemoDetail</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#E36209;--s-dark:#FFAB70">memo</span><span style="color:#24292E;--s-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            document.</span><span style="color:#6F42C1;--s-dark:#B392F0">getElementById</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#032F62;--s-dark:#9ECBFF">'memo-info'</span><span style="color:#24292E;--s-dark:#E1E4E8">).innerHTML </span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#24292E;--s-dark:#E1E4E8"> memo.uid;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            document.</span><span style="color:#6F42C1;--s-dark:#B392F0">getElementById</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#032F62;--s-dark:#9ECBFF">'memo-content'</span><span style="color:#24292E;--s-dark:#E1E4E8">).value </span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#24292E;--s-dark:#E1E4E8"> memo.content;</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">            updatePreview</span><span style="color:#24292E;--s-dark:#E1E4E8">();</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        }</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">        function</span><span style="color:#6F42C1;--s-dark:#B392F0"> updatePreview</span><span style="color:#24292E;--s-dark:#E1E4E8">() {</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">            const</span><span style="color:#005CC5;--s-dark:#79B8FF"> content</span><span style="color:#D73A49;--s-dark:#F97583"> =</span><span style="color:#24292E;--s-dark:#E1E4E8"> document.</span><span style="color:#6F42C1;--s-dark:#B392F0">getElementById</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#032F62;--s-dark:#9ECBFF">'memo-content'</span><span style="color:#24292E;--s-dark:#E1E4E8">).value;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            document.</span><span style="color:#6F42C1;--s-dark:#B392F0">getElementById</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#032F62;--s-dark:#9ECBFF">'memo-preview'</span><span style="color:#24292E;--s-dark:#E1E4E8">).innerHTML </span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#6F42C1;--s-dark:#B392F0"> marked</span><span style="color:#24292E;--s-dark:#E1E4E8">(content);</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        }</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        document.</span><span style="color:#6F42C1;--s-dark:#B392F0">getElementById</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#032F62;--s-dark:#9ECBFF">'memo-content'</span><span style="color:#24292E;--s-dark:#E1E4E8">).</span><span style="color:#6F42C1;--s-dark:#B392F0">addEventListener</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#032F62;--s-dark:#9ECBFF">'input'</span><span style="color:#24292E;--s-dark:#E1E4E8">, updatePreview);</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">        async</span><span style="color:#D73A49;--s-dark:#F97583"> function</span><span style="color:#6F42C1;--s-dark:#B392F0"> saveMemo</span><span style="color:#24292E;--s-dark:#E1E4E8">() {</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">            const</span><span style="color:#005CC5;--s-dark:#79B8FF"> content</span><span style="color:#D73A49;--s-dark:#F97583"> =</span><span style="color:#24292E;--s-dark:#E1E4E8"> document.</span><span style="color:#6F42C1;--s-dark:#B392F0">getElementById</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#032F62;--s-dark:#9ECBFF">'memo-content'</span><span style="color:#24292E;--s-dark:#E1E4E8">).value;</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">            if</span><span style="color:#24292E;--s-dark:#E1E4E8"> (</span><span style="color:#D73A49;--s-dark:#F97583">!</span><span style="color:#24292E;--s-dark:#E1E4E8">content.</span><span style="color:#6F42C1;--s-dark:#B392F0">trim</span><span style="color:#24292E;--s-dark:#E1E4E8">()) {</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">                showNotification</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#032F62;--s-dark:#9ECBFF">'Memo 内容不得为空'</span><span style="color:#24292E;--s-dark:#E1E4E8">, </span><span style="color:#032F62;--s-dark:#9ECBFF">'error'</span><span style="color:#24292E;--s-dark:#E1E4E8">);</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">                return</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            }</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">            try</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">                showLoading</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#005CC5;--s-dark:#79B8FF">true</span><span style="color:#24292E;--s-dark:#E1E4E8">);</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">                if</span><span style="color:#24292E;--s-dark:#E1E4E8"> (currentMemo) {</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">                    // Update existing memo</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">                    await</span><span style="color:#6F42C1;--s-dark:#B392F0"> fetch</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#032F62;--s-dark:#9ECBFF">`/api/memos/${</span><span style="color:#24292E;--s-dark:#E1E4E8">currentMemo</span><span style="color:#032F62;--s-dark:#9ECBFF">.</span><span style="color:#24292E;--s-dark:#E1E4E8">uid</span><span style="color:#032F62;--s-dark:#9ECBFF">}`</span><span style="color:#24292E;--s-dark:#E1E4E8">, {</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">                        method: </span><span style="color:#032F62;--s-dark:#9ECBFF">'PUT'</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">                        headers: {</span></span>
<span class="line"><span style="color:#032F62;--s-dark:#9ECBFF">                            'Authorization'</span><span style="color:#24292E;--s-dark:#E1E4E8">: password,</span></span>
<span class="line"><span style="color:#032F62;--s-dark:#9ECBFF">                            'Content-Type'</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">'application/json'</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">                        },</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">                        body: </span><span style="color:#005CC5;--s-dark:#79B8FF">JSON</span><span style="color:#24292E;--s-dark:#E1E4E8">.</span><span style="color:#6F42C1;--s-dark:#B392F0">stringify</span><span style="color:#24292E;--s-dark:#E1E4E8">({ content })</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">                    });</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">                } </span><span style="color:#D73A49;--s-dark:#F97583">else</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">                    // Create new memo</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">                    await</span><span style="color:#6F42C1;--s-dark:#B392F0"> fetch</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#032F62;--s-dark:#9ECBFF">'/api/memos'</span><span style="color:#24292E;--s-dark:#E1E4E8">, {</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">                        method: </span><span style="color:#032F62;--s-dark:#9ECBFF">'POST'</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">                        headers: {</span></span>
<span class="line"><span style="color:#032F62;--s-dark:#9ECBFF">                            'Authorization'</span><span style="color:#24292E;--s-dark:#E1E4E8">: password,</span></span>
<span class="line"><span style="color:#032F62;--s-dark:#9ECBFF">                            'Content-Type'</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">'application/json'</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">                        },</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">                        body: </span><span style="color:#005CC5;--s-dark:#79B8FF">JSON</span><span style="color:#24292E;--s-dark:#E1E4E8">.</span><span style="color:#6F42C1;--s-dark:#B392F0">stringify</span><span style="color:#24292E;--s-dark:#E1E4E8">({ content })</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">                    });</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">                }</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">                showNotification</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#032F62;--s-dark:#9ECBFF">'保存 Memo 成功'</span><span style="color:#24292E;--s-dark:#E1E4E8">);</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">                loadMemos</span><span style="color:#24292E;--s-dark:#E1E4E8">();</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            } </span><span style="color:#D73A49;--s-dark:#F97583">catch</span><span style="color:#24292E;--s-dark:#E1E4E8"> (error) {</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">                showNotification</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#032F62;--s-dark:#9ECBFF">'保存 Memo 失败'</span><span style="color:#24292E;--s-dark:#E1E4E8">, </span><span style="color:#032F62;--s-dark:#9ECBFF">'error'</span><span style="color:#24292E;--s-dark:#E1E4E8">);</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            } </span><span style="color:#D73A49;--s-dark:#F97583">finally</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">                showLoading</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#005CC5;--s-dark:#79B8FF">false</span><span style="color:#24292E;--s-dark:#E1E4E8">);</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            }</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        }</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">        async</span><span style="color:#D73A49;--s-dark:#F97583"> function</span><span style="color:#6F42C1;--s-dark:#B392F0"> deleteMemo</span><span style="color:#24292E;--s-dark:#E1E4E8">() {</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">            if</span><span style="color:#24292E;--s-dark:#E1E4E8"> (</span><span style="color:#D73A49;--s-dark:#F97583">!</span><span style="color:#24292E;--s-dark:#E1E4E8">currentMemo) </span><span style="color:#D73A49;--s-dark:#F97583">return</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">            if</span><span style="color:#24292E;--s-dark:#E1E4E8"> (</span><span style="color:#6F42C1;--s-dark:#B392F0">confirm</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#032F62;--s-dark:#9ECBFF">'确定要删除这条 Memo 吗？'</span><span style="color:#24292E;--s-dark:#E1E4E8">)) {</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">                try</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">                    showLoading</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#005CC5;--s-dark:#79B8FF">true</span><span style="color:#24292E;--s-dark:#E1E4E8">);</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">                    await</span><span style="color:#6F42C1;--s-dark:#B392F0"> fetch</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#032F62;--s-dark:#9ECBFF">`/api/memos/${</span><span style="color:#24292E;--s-dark:#E1E4E8">currentMemo</span><span style="color:#032F62;--s-dark:#9ECBFF">.</span><span style="color:#24292E;--s-dark:#E1E4E8">uid</span><span style="color:#032F62;--s-dark:#9ECBFF">}`</span><span style="color:#24292E;--s-dark:#E1E4E8">, {</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">                        method: </span><span style="color:#032F62;--s-dark:#9ECBFF">'DELETE'</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">                        headers: {</span></span>
<span class="line"><span style="color:#032F62;--s-dark:#9ECBFF">                            'Authorization'</span><span style="color:#24292E;--s-dark:#E1E4E8">: password</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">                        }</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">                    });</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">                    showNotification</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#032F62;--s-dark:#9ECBFF">'删除 Memo 成功'</span><span style="color:#24292E;--s-dark:#E1E4E8">);</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">                    loadMemos</span><span style="color:#24292E;--s-dark:#E1E4E8">();</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">                    clearMemoDetail</span><span style="color:#24292E;--s-dark:#E1E4E8">();</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">                } </span><span style="color:#D73A49;--s-dark:#F97583">catch</span><span style="color:#24292E;--s-dark:#E1E4E8"> (error) {</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">                    showNotification</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#032F62;--s-dark:#9ECBFF">'删除 Memo 失败'</span><span style="color:#24292E;--s-dark:#E1E4E8">, </span><span style="color:#032F62;--s-dark:#9ECBFF">'error'</span><span style="color:#24292E;--s-dark:#E1E4E8">);</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">                } </span><span style="color:#D73A49;--s-dark:#F97583">finally</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">                    showLoading</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#005CC5;--s-dark:#79B8FF">false</span><span style="color:#24292E;--s-dark:#E1E4E8">);</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">                }</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            }</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        }</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">        function</span><span style="color:#6F42C1;--s-dark:#B392F0"> createMemo</span><span style="color:#24292E;--s-dark:#E1E4E8">() {</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            currentMemo </span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#005CC5;--s-dark:#79B8FF"> null</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">            clearMemoDetail</span><span style="color:#24292E;--s-dark:#E1E4E8">();</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        }</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">        function</span><span style="color:#6F42C1;--s-dark:#B392F0"> clearMemoDetail</span><span style="color:#24292E;--s-dark:#E1E4E8">() {</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            document.</span><span style="color:#6F42C1;--s-dark:#B392F0">getElementById</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#032F62;--s-dark:#9ECBFF">'memo-info'</span><span style="color:#24292E;--s-dark:#E1E4E8">).innerHTML </span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#032F62;--s-dark:#9ECBFF"> '新 Memo'</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            document.</span><span style="color:#6F42C1;--s-dark:#B392F0">getElementById</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#032F62;--s-dark:#9ECBFF">'memo-content'</span><span style="color:#24292E;--s-dark:#E1E4E8">).value </span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#032F62;--s-dark:#9ECBFF"> ''</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            document.</span><span style="color:#6F42C1;--s-dark:#B392F0">getElementById</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#032F62;--s-dark:#9ECBFF">'memo-preview'</span><span style="color:#24292E;--s-dark:#E1E4E8">).innerHTML </span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#032F62;--s-dark:#9ECBFF"> ''</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        }</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">        function</span><span style="color:#6F42C1;--s-dark:#B392F0"> prevPage</span><span style="color:#24292E;--s-dark:#E1E4E8">() {</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">            if</span><span style="color:#24292E;--s-dark:#E1E4E8"> (offset </span><span style="color:#D73A49;--s-dark:#F97583">-</span><span style="color:#24292E;--s-dark:#E1E4E8"> limit </span><span style="color:#D73A49;--s-dark:#F97583">>=</span><span style="color:#005CC5;--s-dark:#79B8FF"> 0</span><span style="color:#24292E;--s-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">                offset </span><span style="color:#D73A49;--s-dark:#F97583">-=</span><span style="color:#24292E;--s-dark:#E1E4E8"> limit;</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">                loadMemos</span><span style="color:#24292E;--s-dark:#E1E4E8">();</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            }</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        }</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">        function</span><span style="color:#6F42C1;--s-dark:#B392F0"> nextPage</span><span style="color:#24292E;--s-dark:#E1E4E8">() {</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">            if</span><span style="color:#24292E;--s-dark:#E1E4E8"> (offset </span><span style="color:#D73A49;--s-dark:#F97583">+</span><span style="color:#24292E;--s-dark:#E1E4E8"> limit </span><span style="color:#D73A49;--s-dark:#F97583">&#x3C;</span><span style="color:#24292E;--s-dark:#E1E4E8"> total) {</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">                offset </span><span style="color:#D73A49;--s-dark:#F97583">+=</span><span style="color:#24292E;--s-dark:#E1E4E8"> limit;</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">                loadMemos</span><span style="color:#24292E;--s-dark:#E1E4E8">();</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            }</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        }</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">        function</span><span style="color:#6F42C1;--s-dark:#B392F0"> updatePagination</span><span style="color:#24292E;--s-dark:#E1E4E8">() {</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">            const</span><span style="color:#005CC5;--s-dark:#79B8FF"> currentPage</span><span style="color:#D73A49;--s-dark:#F97583"> =</span><span style="color:#24292E;--s-dark:#E1E4E8"> Math.</span><span style="color:#6F42C1;--s-dark:#B392F0">floor</span><span style="color:#24292E;--s-dark:#E1E4E8">(offset </span><span style="color:#D73A49;--s-dark:#F97583">/</span><span style="color:#24292E;--s-dark:#E1E4E8"> limit) </span><span style="color:#D73A49;--s-dark:#F97583">+</span><span style="color:#005CC5;--s-dark:#79B8FF"> 1</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">            const</span><span style="color:#005CC5;--s-dark:#79B8FF"> totalPages</span><span style="color:#D73A49;--s-dark:#F97583"> =</span><span style="color:#24292E;--s-dark:#E1E4E8"> Math.</span><span style="color:#6F42C1;--s-dark:#B392F0">ceil</span><span style="color:#24292E;--s-dark:#E1E4E8">(total </span><span style="color:#D73A49;--s-dark:#F97583">/</span><span style="color:#24292E;--s-dark:#E1E4E8"> limit);</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            document.</span><span style="color:#6F42C1;--s-dark:#B392F0">getElementById</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#032F62;--s-dark:#9ECBFF">'page-info'</span><span style="color:#24292E;--s-dark:#E1E4E8">).textContent </span><span style="color:#D73A49;--s-dark:#F97583">=</span></span>
<span class="line"><span style="color:#032F62;--s-dark:#9ECBFF">                `Page ${</span><span style="color:#24292E;--s-dark:#E1E4E8">currentPage</span><span style="color:#032F62;--s-dark:#9ECBFF">} of ${</span><span style="color:#24292E;--s-dark:#E1E4E8">totalPages</span><span style="color:#032F62;--s-dark:#9ECBFF">}`</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        }</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">        function</span><span style="color:#6F42C1;--s-dark:#B392F0"> showLoading</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#E36209;--s-dark:#FFAB70">show</span><span style="color:#24292E;--s-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">            const</span><span style="color:#005CC5;--s-dark:#79B8FF"> saveBtn</span><span style="color:#D73A49;--s-dark:#F97583"> =</span><span style="color:#24292E;--s-dark:#E1E4E8"> document.</span><span style="color:#6F42C1;--s-dark:#B392F0">getElementById</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#032F62;--s-dark:#9ECBFF">'save-btn'</span><span style="color:#24292E;--s-dark:#E1E4E8">);</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">            if</span><span style="color:#24292E;--s-dark:#E1E4E8"> (show) {</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">                saveBtn.innerHTML </span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#032F62;--s-dark:#9ECBFF"> '&#x3C;div class="loading">&#x3C;/div> 保存中...'</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">                saveBtn.disabled </span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#005CC5;--s-dark:#79B8FF"> true</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            } </span><span style="color:#D73A49;--s-dark:#F97583">else</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">                saveBtn.innerHTML </span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#032F62;--s-dark:#9ECBFF"> '&#x3C;i class="fas fa-save">&#x3C;/i> 保存'</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">                saveBtn.disabled </span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#005CC5;--s-dark:#79B8FF"> false</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            }</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        }</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">        function</span><span style="color:#6F42C1;--s-dark:#B392F0"> showNotification</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#E36209;--s-dark:#FFAB70">message</span><span style="color:#24292E;--s-dark:#E1E4E8">, </span><span style="color:#E36209;--s-dark:#FFAB70">type</span><span style="color:#D73A49;--s-dark:#F97583"> =</span><span style="color:#032F62;--s-dark:#9ECBFF"> 'success'</span><span style="color:#24292E;--s-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">            const</span><span style="color:#005CC5;--s-dark:#79B8FF"> notification</span><span style="color:#D73A49;--s-dark:#F97583"> =</span><span style="color:#24292E;--s-dark:#E1E4E8"> document.</span><span style="color:#6F42C1;--s-dark:#B392F0">createElement</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#032F62;--s-dark:#9ECBFF">'div'</span><span style="color:#24292E;--s-dark:#E1E4E8">);</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            notification.className </span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#032F62;--s-dark:#9ECBFF"> `notification ${</span><span style="color:#24292E;--s-dark:#E1E4E8">type</span><span style="color:#032F62;--s-dark:#9ECBFF">}`</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            notification.textContent </span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#24292E;--s-dark:#E1E4E8"> message;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            notification.style.position </span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#032F62;--s-dark:#9ECBFF"> 'fixed'</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            notification.style.top </span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#032F62;--s-dark:#9ECBFF"> '20px'</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            notification.style.right </span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#032F62;--s-dark:#9ECBFF"> '20px'</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            notification.style.padding </span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#032F62;--s-dark:#9ECBFF"> '1rem'</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            notification.style.borderRadius </span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#032F62;--s-dark:#9ECBFF"> '5px'</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            notification.style.backgroundColor </span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#24292E;--s-dark:#E1E4E8"> type </span><span style="color:#D73A49;--s-dark:#F97583">===</span><span style="color:#032F62;--s-dark:#9ECBFF"> 'success'</span><span style="color:#D73A49;--s-dark:#F97583"> ?</span><span style="color:#032F62;--s-dark:#9ECBFF"> '#2ecc71'</span><span style="color:#D73A49;--s-dark:#F97583"> :</span><span style="color:#032F62;--s-dark:#9ECBFF"> '#e74c3c'</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            notification.style.color </span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#032F62;--s-dark:#9ECBFF"> 'white'</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            notification.style.zIndex </span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#032F62;--s-dark:#9ECBFF"> '1000'</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            document.body.</span><span style="color:#6F42C1;--s-dark:#B392F0">appendChild</span><span style="color:#24292E;--s-dark:#E1E4E8">(notification);</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">            setTimeout</span><span style="color:#24292E;--s-dark:#E1E4E8">(() </span><span style="color:#D73A49;--s-dark:#F97583">=></span><span style="color:#24292E;--s-dark:#E1E4E8"> notification.</span><span style="color:#6F42C1;--s-dark:#B392F0">remove</span><span style="color:#24292E;--s-dark:#E1E4E8">(), </span><span style="color:#005CC5;--s-dark:#79B8FF">3000</span><span style="color:#24292E;--s-dark:#E1E4E8">);</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        }</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">        // 用于防止 XSS 攻击的辅助函数</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">        function</span><span style="color:#6F42C1;--s-dark:#B392F0"> escapeHtml</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#E36209;--s-dark:#FFAB70">html</span><span style="color:#24292E;--s-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">            const</span><span style="color:#005CC5;--s-dark:#79B8FF"> div</span><span style="color:#D73A49;--s-dark:#F97583"> =</span><span style="color:#24292E;--s-dark:#E1E4E8"> document.</span><span style="color:#6F42C1;--s-dark:#B392F0">createElement</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#032F62;--s-dark:#9ECBFF">'div'</span><span style="color:#24292E;--s-dark:#E1E4E8">);</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            div.textContent </span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#24292E;--s-dark:#E1E4E8"> html;</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">            return</span><span style="color:#24292E;--s-dark:#E1E4E8"> div.innerHTML;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        }</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">        // 初始化 marked 配置</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        marked.</span><span style="color:#6F42C1;--s-dark:#B392F0">setOptions</span><span style="color:#24292E;--s-dark:#E1E4E8">({</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            breaks: </span><span style="color:#005CC5;--s-dark:#79B8FF">true</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            gfm: </span><span style="color:#005CC5;--s-dark:#79B8FF">true</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            headerIds: </span><span style="color:#005CC5;--s-dark:#79B8FF">false</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        });</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    &#x3C;/</span><span style="color:#22863A;--s-dark:#85E89D">script</span><span style="color:#24292E;--s-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">&#x3C;/</span><span style="color:#22863A;--s-dark:#85E89D">body</span><span style="color:#24292E;--s-dark:#E1E4E8">></span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">&#x3C;/</span><span style="color:#22863A;--s-dark:#85E89D">html</span><span style="color:#24292E;--s-dark:#E1E4E8">></span></span></code><label class="code-collapse-collapse" for="toggle-givnjhd"><svg class="code-collapse-icon" width="32" height="24" viewBox="0 0 32 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20,15 12,7 4,15"></polyline></svg></label></pre></div></div></div>
<p>从 JS 代码中即可看出，后端包含如下两个端点</p>
<ul>
<li><code>POST /api/auth</code>：页面鉴权</li>
<li><code>GET /api/memos</code>：获取说说详情，支持分页</li>
<li><code>POST /api/memos</code>: 发布新说说</li>
<li><code>PUT /api/memos/{uid}</code>: 更新说说</li>
<li><code>DELETE /api/memos/{uid}</code>：删除说说</li>
</ul>
<p>随后编辑 <code>worker.js</code> 实现这些端点即可</p>
<div class="code-collapse-wrapper"><input type="checkbox" id="toggle-zydeqyu" class="code-collapse-toggle" style="display: none;"><div class="code-collapse-preview"><div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0" class="code-preview"><code><span class="line"><span style="color:#D73A49;--s-dark:#F97583">import</span><span style="color:#24292E;--s-dark:#E1E4E8"> html </span><span style="color:#D73A49;--s-dark:#F97583">from</span><span style="color:#032F62;--s-dark:#9ECBFF"> './index.html'</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">const</span><span style="color:#005CC5;--s-dark:#79B8FF"> CORRECT_PASSWORD</span><span style="color:#D73A49;--s-dark:#F97583"> =</span><span style="color:#032F62;--s-dark:#9ECBFF"> 'CORRECT_PASSWORD'</span><span style="color:#24292E;--s-dark:#E1E4E8">;        </span><span style="color:#6A737D;--s-dark:#6A737D">// 设置你的密码    // [!code highlight]</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">const</span><span style="color:#005CC5;--s-dark:#79B8FF"> CALLBACK_URL</span><span style="color:#D73A49;--s-dark:#F97583"> =</span><span style="color:#032F62;--s-dark:#9ECBFF"> 'https://CALLBACK_URL'</span><span style="color:#24292E;--s-dark:#E1E4E8">;        </span><span style="color:#6A737D;--s-dark:#6A737D">// 设置回调 URL   // [!code highlight]</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">const</span><span style="color:#005CC5;--s-dark:#79B8FF"> ALLOWED_ORIGINS</span><span style="color:#D73A49;--s-dark:#F97583"> =</span><span style="color:#24292E;--s-dark:#E1E4E8"> [</span><span style="color:#032F62;--s-dark:#9ECBFF">'https://example.com'</span><span style="color:#24292E;--s-dark:#E1E4E8">];    </span><span style="color:#6A737D;--s-dark:#6A737D">// 允许请求的域名  // [!code highlight]</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">// 生成随机 UID</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">function</span><span style="color:#6F42C1;--s-dark:#B392F0"> generateUID</span><span style="color:#24292E;--s-dark:#E1E4E8">() {</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">  const</span><span style="color:#005CC5;--s-dark:#79B8FF"> chars</span><span style="color:#D73A49;--s-dark:#F97583"> =</span><span style="color:#032F62;--s-dark:#9ECBFF"> 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">  let</span><span style="color:#24292E;--s-dark:#E1E4E8"> result </span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#032F62;--s-dark:#9ECBFF"> ''</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">  for</span><span style="color:#24292E;--s-dark:#E1E4E8"> (</span><span style="color:#D73A49;--s-dark:#F97583">let</span><span style="color:#24292E;--s-dark:#E1E4E8"> i </span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#005CC5;--s-dark:#79B8FF"> 0</span><span style="color:#24292E;--s-dark:#E1E4E8">; i </span><span style="color:#D73A49;--s-dark:#F97583">&#x3C;</span><span style="color:#005CC5;--s-dark:#79B8FF"> 22</span><span style="color:#24292E;--s-dark:#E1E4E8">; i</span><span style="color:#D73A49;--s-dark:#F97583">++</span><span style="color:#24292E;--s-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">    const</span><span style="color:#005CC5;--s-dark:#79B8FF"> randomIndex</span><span style="color:#D73A49;--s-dark:#F97583"> =</span><span style="color:#24292E;--s-dark:#E1E4E8"> Math.</span><span style="color:#6F42C1;--s-dark:#B392F0">floor</span><span style="color:#24292E;--s-dark:#E1E4E8">(Math.</span><span style="color:#6F42C1;--s-dark:#B392F0">random</span><span style="color:#24292E;--s-dark:#E1E4E8">() </span><span style="color:#D73A49;--s-dark:#F97583">*</span><span style="color:#24292E;--s-dark:#E1E4E8"> chars.</span><span style="color:#005CC5;--s-dark:#79B8FF">length</span><span style="color:#24292E;--s-dark:#E1E4E8">);</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    result </span><span style="color:#D73A49;--s-dark:#F97583">+=</span><span style="color:#24292E;--s-dark:#E1E4E8"> chars[randomIndex];</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">  }</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">  return</span><span style="color:#24292E;--s-dark:#E1E4E8"> result;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">}</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">// CORS 处理</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">function</span><span style="color:#6F42C1;--s-dark:#B392F0"> handleCORS</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#E36209;--s-dark:#FFAB70">request</span><span style="color:#24292E;--s-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">  const</span><span style="color:#005CC5;--s-dark:#79B8FF"> origin</span><span style="color:#D73A49;--s-dark:#F97583"> =</span><span style="color:#24292E;--s-dark:#E1E4E8"> request.headers.</span><span style="color:#6F42C1;--s-dark:#B392F0">get</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#032F62;--s-dark:#9ECBFF">'Origin'</span><span style="color:#24292E;--s-dark:#E1E4E8">);</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">  const</span><span style="color:#005CC5;--s-dark:#79B8FF"> allowedOrigin</span><span style="color:#D73A49;--s-dark:#F97583"> =</span><span style="color:#005CC5;--s-dark:#79B8FF"> ALLOWED_ORIGINS</span><span style="color:#24292E;--s-dark:#E1E4E8">.</span><span style="color:#6F42C1;--s-dark:#B392F0">includes</span><span style="color:#24292E;--s-dark:#E1E4E8">(origin) </span><span style="color:#D73A49;--s-dark:#F97583">?</span><span style="color:#24292E;--s-dark:#E1E4E8"> origin </span><span style="color:#D73A49;--s-dark:#F97583">:</span><span style="color:#005CC5;--s-dark:#79B8FF"> ALLOWED_ORIGINS</span><span style="color:#24292E;--s-dark:#E1E4E8">[</span><span style="color:#005CC5;--s-dark:#79B8FF">0</span><span style="color:#24292E;--s-dark:#E1E4E8">];</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">  const</span><span style="color:#005CC5;--s-dark:#79B8FF"> corsHeaders</span><span style="color:#D73A49;--s-dark:#F97583"> =</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#032F62;--s-dark:#9ECBFF">    'Access-Control-Allow-Origin'</span><span style="color:#24292E;--s-dark:#E1E4E8">: allowedOrigin,</span></span>
<span class="line"><span style="color:#032F62;--s-dark:#9ECBFF">    'Access-Control-Allow-Methods'</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">'GET, POST, OPTIONS'</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#032F62;--s-dark:#9ECBFF">    'Access-Control-Allow-Headers'</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">'Content-Type, Authorization'</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
</code></pre></div><div class="code-collapse-gradient"></div><label class="code-collapse-expand" for="toggle-zydeqyu"><svg class="code-collapse-icon" width="32" height="24" viewBox="0 0 32 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="4,9 12,17 20,9"></polyline></svg></label></div><div class="code-collapse-full"><div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--s-dark:#F97583">import</span><span style="color:#24292E;--s-dark:#E1E4E8"> html </span><span style="color:#D73A49;--s-dark:#F97583">from</span><span style="color:#032F62;--s-dark:#9ECBFF"> './index.html'</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">const</span><span style="color:#005CC5;--s-dark:#79B8FF"> CORRECT_PASSWORD</span><span style="color:#D73A49;--s-dark:#F97583"> =</span><span style="color:#032F62;--s-dark:#9ECBFF"> 'CORRECT_PASSWORD'</span><span style="color:#24292E;--s-dark:#E1E4E8">;        </span><span style="color:#6A737D;--s-dark:#6A737D">// 设置你的密码    // [!code highlight]</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">const</span><span style="color:#005CC5;--s-dark:#79B8FF"> CALLBACK_URL</span><span style="color:#D73A49;--s-dark:#F97583"> =</span><span style="color:#032F62;--s-dark:#9ECBFF"> 'https://CALLBACK_URL'</span><span style="color:#24292E;--s-dark:#E1E4E8">;        </span><span style="color:#6A737D;--s-dark:#6A737D">// 设置回调 URL   // [!code highlight]</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">const</span><span style="color:#005CC5;--s-dark:#79B8FF"> ALLOWED_ORIGINS</span><span style="color:#D73A49;--s-dark:#F97583"> =</span><span style="color:#24292E;--s-dark:#E1E4E8"> [</span><span style="color:#032F62;--s-dark:#9ECBFF">'https://example.com'</span><span style="color:#24292E;--s-dark:#E1E4E8">];    </span><span style="color:#6A737D;--s-dark:#6A737D">// 允许请求的域名  // [!code highlight]</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">// 生成随机 UID</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">function</span><span style="color:#6F42C1;--s-dark:#B392F0"> generateUID</span><span style="color:#24292E;--s-dark:#E1E4E8">() {</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">  const</span><span style="color:#005CC5;--s-dark:#79B8FF"> chars</span><span style="color:#D73A49;--s-dark:#F97583"> =</span><span style="color:#032F62;--s-dark:#9ECBFF"> 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">  let</span><span style="color:#24292E;--s-dark:#E1E4E8"> result </span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#032F62;--s-dark:#9ECBFF"> ''</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">  for</span><span style="color:#24292E;--s-dark:#E1E4E8"> (</span><span style="color:#D73A49;--s-dark:#F97583">let</span><span style="color:#24292E;--s-dark:#E1E4E8"> i </span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#005CC5;--s-dark:#79B8FF"> 0</span><span style="color:#24292E;--s-dark:#E1E4E8">; i </span><span style="color:#D73A49;--s-dark:#F97583">&#x3C;</span><span style="color:#005CC5;--s-dark:#79B8FF"> 22</span><span style="color:#24292E;--s-dark:#E1E4E8">; i</span><span style="color:#D73A49;--s-dark:#F97583">++</span><span style="color:#24292E;--s-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">    const</span><span style="color:#005CC5;--s-dark:#79B8FF"> randomIndex</span><span style="color:#D73A49;--s-dark:#F97583"> =</span><span style="color:#24292E;--s-dark:#E1E4E8"> Math.</span><span style="color:#6F42C1;--s-dark:#B392F0">floor</span><span style="color:#24292E;--s-dark:#E1E4E8">(Math.</span><span style="color:#6F42C1;--s-dark:#B392F0">random</span><span style="color:#24292E;--s-dark:#E1E4E8">() </span><span style="color:#D73A49;--s-dark:#F97583">*</span><span style="color:#24292E;--s-dark:#E1E4E8"> chars.</span><span style="color:#005CC5;--s-dark:#79B8FF">length</span><span style="color:#24292E;--s-dark:#E1E4E8">);</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    result </span><span style="color:#D73A49;--s-dark:#F97583">+=</span><span style="color:#24292E;--s-dark:#E1E4E8"> chars[randomIndex];</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">  }</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">  return</span><span style="color:#24292E;--s-dark:#E1E4E8"> result;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">}</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">// CORS 处理</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">function</span><span style="color:#6F42C1;--s-dark:#B392F0"> handleCORS</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#E36209;--s-dark:#FFAB70">request</span><span style="color:#24292E;--s-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">  const</span><span style="color:#005CC5;--s-dark:#79B8FF"> origin</span><span style="color:#D73A49;--s-dark:#F97583"> =</span><span style="color:#24292E;--s-dark:#E1E4E8"> request.headers.</span><span style="color:#6F42C1;--s-dark:#B392F0">get</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#032F62;--s-dark:#9ECBFF">'Origin'</span><span style="color:#24292E;--s-dark:#E1E4E8">);</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">  const</span><span style="color:#005CC5;--s-dark:#79B8FF"> allowedOrigin</span><span style="color:#D73A49;--s-dark:#F97583"> =</span><span style="color:#005CC5;--s-dark:#79B8FF"> ALLOWED_ORIGINS</span><span style="color:#24292E;--s-dark:#E1E4E8">.</span><span style="color:#6F42C1;--s-dark:#B392F0">includes</span><span style="color:#24292E;--s-dark:#E1E4E8">(origin) </span><span style="color:#D73A49;--s-dark:#F97583">?</span><span style="color:#24292E;--s-dark:#E1E4E8"> origin </span><span style="color:#D73A49;--s-dark:#F97583">:</span><span style="color:#005CC5;--s-dark:#79B8FF"> ALLOWED_ORIGINS</span><span style="color:#24292E;--s-dark:#E1E4E8">[</span><span style="color:#005CC5;--s-dark:#79B8FF">0</span><span style="color:#24292E;--s-dark:#E1E4E8">];</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">  const</span><span style="color:#005CC5;--s-dark:#79B8FF"> corsHeaders</span><span style="color:#D73A49;--s-dark:#F97583"> =</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#032F62;--s-dark:#9ECBFF">    'Access-Control-Allow-Origin'</span><span style="color:#24292E;--s-dark:#E1E4E8">: allowedOrigin,</span></span>
<span class="line"><span style="color:#032F62;--s-dark:#9ECBFF">    'Access-Control-Allow-Methods'</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">'GET, POST, OPTIONS'</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#032F62;--s-dark:#9ECBFF">    'Access-Control-Allow-Headers'</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">'Content-Type, Authorization'</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#032F62;--s-dark:#9ECBFF">    'Access-Control-Max-Age'</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">'86400'</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">  };</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">  return</span><span style="color:#24292E;--s-dark:#E1E4E8"> corsHeaders;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">}</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">function</span><span style="color:#6F42C1;--s-dark:#B392F0"> getCurrentTimeInISOFormat</span><span style="color:#24292E;--s-dark:#E1E4E8">() {</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">  const</span><span style="color:#005CC5;--s-dark:#79B8FF"> now</span><span style="color:#D73A49;--s-dark:#F97583"> =</span><span style="color:#D73A49;--s-dark:#F97583"> new</span><span style="color:#6F42C1;--s-dark:#B392F0"> Date</span><span style="color:#24292E;--s-dark:#E1E4E8">();</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">  // 获取各个部分</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">  const</span><span style="color:#005CC5;--s-dark:#79B8FF"> year</span><span style="color:#D73A49;--s-dark:#F97583"> =</span><span style="color:#24292E;--s-dark:#E1E4E8"> now.</span><span style="color:#6F42C1;--s-dark:#B392F0">getUTCFullYear</span><span style="color:#24292E;--s-dark:#E1E4E8">();</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">  const</span><span style="color:#005CC5;--s-dark:#79B8FF"> month</span><span style="color:#D73A49;--s-dark:#F97583"> =</span><span style="color:#6F42C1;--s-dark:#B392F0"> String</span><span style="color:#24292E;--s-dark:#E1E4E8">(now.</span><span style="color:#6F42C1;--s-dark:#B392F0">getUTCMonth</span><span style="color:#24292E;--s-dark:#E1E4E8">() </span><span style="color:#D73A49;--s-dark:#F97583">+</span><span style="color:#005CC5;--s-dark:#79B8FF"> 1</span><span style="color:#24292E;--s-dark:#E1E4E8">).</span><span style="color:#6F42C1;--s-dark:#B392F0">padStart</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#005CC5;--s-dark:#79B8FF">2</span><span style="color:#24292E;--s-dark:#E1E4E8">, </span><span style="color:#032F62;--s-dark:#9ECBFF">'0'</span><span style="color:#24292E;--s-dark:#E1E4E8">); </span><span style="color:#6A737D;--s-dark:#6A737D">// 月份从零开始</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">  const</span><span style="color:#005CC5;--s-dark:#79B8FF"> day</span><span style="color:#D73A49;--s-dark:#F97583"> =</span><span style="color:#6F42C1;--s-dark:#B392F0"> String</span><span style="color:#24292E;--s-dark:#E1E4E8">(now.</span><span style="color:#6F42C1;--s-dark:#B392F0">getUTCDate</span><span style="color:#24292E;--s-dark:#E1E4E8">()).</span><span style="color:#6F42C1;--s-dark:#B392F0">padStart</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#005CC5;--s-dark:#79B8FF">2</span><span style="color:#24292E;--s-dark:#E1E4E8">, </span><span style="color:#032F62;--s-dark:#9ECBFF">'0'</span><span style="color:#24292E;--s-dark:#E1E4E8">);</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">  const</span><span style="color:#005CC5;--s-dark:#79B8FF"> hours</span><span style="color:#D73A49;--s-dark:#F97583"> =</span><span style="color:#6F42C1;--s-dark:#B392F0"> String</span><span style="color:#24292E;--s-dark:#E1E4E8">(now.</span><span style="color:#6F42C1;--s-dark:#B392F0">getUTCHours</span><span style="color:#24292E;--s-dark:#E1E4E8">()).</span><span style="color:#6F42C1;--s-dark:#B392F0">padStart</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#005CC5;--s-dark:#79B8FF">2</span><span style="color:#24292E;--s-dark:#E1E4E8">, </span><span style="color:#032F62;--s-dark:#9ECBFF">'0'</span><span style="color:#24292E;--s-dark:#E1E4E8">);</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">  const</span><span style="color:#005CC5;--s-dark:#79B8FF"> minutes</span><span style="color:#D73A49;--s-dark:#F97583"> =</span><span style="color:#6F42C1;--s-dark:#B392F0"> String</span><span style="color:#24292E;--s-dark:#E1E4E8">(now.</span><span style="color:#6F42C1;--s-dark:#B392F0">getUTCMinutes</span><span style="color:#24292E;--s-dark:#E1E4E8">()).</span><span style="color:#6F42C1;--s-dark:#B392F0">padStart</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#005CC5;--s-dark:#79B8FF">2</span><span style="color:#24292E;--s-dark:#E1E4E8">, </span><span style="color:#032F62;--s-dark:#9ECBFF">'0'</span><span style="color:#24292E;--s-dark:#E1E4E8">);</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">  const</span><span style="color:#005CC5;--s-dark:#79B8FF"> seconds</span><span style="color:#D73A49;--s-dark:#F97583"> =</span><span style="color:#6F42C1;--s-dark:#B392F0"> String</span><span style="color:#24292E;--s-dark:#E1E4E8">(now.</span><span style="color:#6F42C1;--s-dark:#B392F0">getUTCSeconds</span><span style="color:#24292E;--s-dark:#E1E4E8">()).</span><span style="color:#6F42C1;--s-dark:#B392F0">padStart</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#005CC5;--s-dark:#79B8FF">2</span><span style="color:#24292E;--s-dark:#E1E4E8">, </span><span style="color:#032F62;--s-dark:#9ECBFF">'0'</span><span style="color:#24292E;--s-dark:#E1E4E8">);</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">  // 组装成 ISO 8601 格式字符串</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">  return</span><span style="color:#032F62;--s-dark:#9ECBFF"> `${</span><span style="color:#24292E;--s-dark:#E1E4E8">year</span><span style="color:#032F62;--s-dark:#9ECBFF">}-${</span><span style="color:#24292E;--s-dark:#E1E4E8">month</span><span style="color:#032F62;--s-dark:#9ECBFF">}-${</span><span style="color:#24292E;--s-dark:#E1E4E8">day</span><span style="color:#032F62;--s-dark:#9ECBFF">}T${</span><span style="color:#24292E;--s-dark:#E1E4E8">hours</span><span style="color:#032F62;--s-dark:#9ECBFF">}:${</span><span style="color:#24292E;--s-dark:#E1E4E8">minutes</span><span style="color:#032F62;--s-dark:#9ECBFF">}:${</span><span style="color:#24292E;--s-dark:#E1E4E8">seconds</span><span style="color:#032F62;--s-dark:#9ECBFF">}Z`</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">}</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">async</span><span style="color:#D73A49;--s-dark:#F97583"> function</span><span style="color:#6F42C1;--s-dark:#B392F0"> handleRequest</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#E36209;--s-dark:#FFAB70">request</span><span style="color:#24292E;--s-dark:#E1E4E8">, </span><span style="color:#E36209;--s-dark:#FFAB70">env</span><span style="color:#24292E;--s-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">  const</span><span style="color:#005CC5;--s-dark:#79B8FF"> url</span><span style="color:#D73A49;--s-dark:#F97583"> =</span><span style="color:#D73A49;--s-dark:#F97583"> new</span><span style="color:#6F42C1;--s-dark:#B392F0"> URL</span><span style="color:#24292E;--s-dark:#E1E4E8">(request.url);</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">  function</span><span style="color:#6F42C1;--s-dark:#B392F0"> validateAuth</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#E36209;--s-dark:#FFAB70">request</span><span style="color:#24292E;--s-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">    const</span><span style="color:#005CC5;--s-dark:#79B8FF"> auth</span><span style="color:#D73A49;--s-dark:#F97583"> =</span><span style="color:#24292E;--s-dark:#E1E4E8"> request.headers.</span><span style="color:#6F42C1;--s-dark:#B392F0">get</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#032F62;--s-dark:#9ECBFF">'Authorization'</span><span style="color:#24292E;--s-dark:#E1E4E8">);</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">    return</span><span style="color:#24292E;--s-dark:#E1E4E8"> auth </span><span style="color:#D73A49;--s-dark:#F97583">===</span><span style="color:#005CC5;--s-dark:#79B8FF"> CORRECT_PASSWORD</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">  }</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">  async</span><span style="color:#D73A49;--s-dark:#F97583"> function</span><span style="color:#6F42C1;--s-dark:#B392F0"> shouldNotify</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#E36209;--s-dark:#FFAB70">uid</span><span style="color:#24292E;--s-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">    const</span><span style="color:#005CC5;--s-dark:#79B8FF"> indexStr</span><span style="color:#D73A49;--s-dark:#F97583"> =</span><span style="color:#D73A49;--s-dark:#F97583"> await</span><span style="color:#24292E;--s-dark:#E1E4E8"> env.</span><span style="color:#005CC5;--s-dark:#79B8FF">KV</span><span style="color:#24292E;--s-dark:#E1E4E8">.</span><span style="color:#6F42C1;--s-dark:#B392F0">get</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#032F62;--s-dark:#9ECBFF">'index'</span><span style="color:#24292E;--s-dark:#E1E4E8">);</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">    if</span><span style="color:#24292E;--s-dark:#E1E4E8"> (</span><span style="color:#D73A49;--s-dark:#F97583">!</span><span style="color:#24292E;--s-dark:#E1E4E8">indexStr) </span><span style="color:#D73A49;--s-dark:#F97583">return</span><span style="color:#005CC5;--s-dark:#79B8FF"> false</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">    const</span><span style="color:#005CC5;--s-dark:#79B8FF"> index</span><span style="color:#D73A49;--s-dark:#F97583"> =</span><span style="color:#005CC5;--s-dark:#79B8FF"> JSON</span><span style="color:#24292E;--s-dark:#E1E4E8">.</span><span style="color:#6F42C1;--s-dark:#B392F0">parse</span><span style="color:#24292E;--s-dark:#E1E4E8">(indexStr);</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">    return</span><span style="color:#24292E;--s-dark:#E1E4E8"> index.</span><span style="color:#6F42C1;--s-dark:#B392F0">indexOf</span><span style="color:#24292E;--s-dark:#E1E4E8">(uid) </span><span style="color:#D73A49;--s-dark:#F97583">&#x3C;</span><span style="color:#005CC5;--s-dark:#79B8FF"> 10</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">  }</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">  async</span><span style="color:#D73A49;--s-dark:#F97583"> function</span><span style="color:#6F42C1;--s-dark:#B392F0"> executeCallback</span><span style="color:#24292E;--s-dark:#E1E4E8">() {</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">    try</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">      await</span><span style="color:#6F42C1;--s-dark:#B392F0"> fetch</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#005CC5;--s-dark:#79B8FF">CALLBACK_URL</span><span style="color:#24292E;--s-dark:#E1E4E8">);</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    } </span><span style="color:#D73A49;--s-dark:#F97583">catch</span><span style="color:#24292E;--s-dark:#E1E4E8"> (error) {</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">      console.</span><span style="color:#6F42C1;--s-dark:#B392F0">error</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#032F62;--s-dark:#9ECBFF">'Callback failed:'</span><span style="color:#24292E;--s-dark:#E1E4E8">, error);</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">  }</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">  const</span><span style="color:#005CC5;--s-dark:#79B8FF"> corsHeaders</span><span style="color:#D73A49;--s-dark:#F97583"> =</span><span style="color:#6F42C1;--s-dark:#B392F0"> handleCORS</span><span style="color:#24292E;--s-dark:#E1E4E8">(request);</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">  </span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">  // 处理 CORS 预检请求</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">  if</span><span style="color:#24292E;--s-dark:#E1E4E8"> (request.method </span><span style="color:#D73A49;--s-dark:#F97583">===</span><span style="color:#032F62;--s-dark:#9ECBFF"> 'OPTIONS'</span><span style="color:#24292E;--s-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">    return</span><span style="color:#D73A49;--s-dark:#F97583"> new</span><span style="color:#6F42C1;--s-dark:#B392F0"> Response</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#005CC5;--s-dark:#79B8FF">null</span><span style="color:#24292E;--s-dark:#E1E4E8">, {</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">      headers: </span><span style="color:#6F42C1;--s-dark:#B392F0">handleCORS</span><span style="color:#24292E;--s-dark:#E1E4E8">(request),</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    });</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">  }</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">  // 管理页面</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">  if</span><span style="color:#24292E;--s-dark:#E1E4E8"> (url.pathname </span><span style="color:#D73A49;--s-dark:#F97583">===</span><span style="color:#032F62;--s-dark:#9ECBFF"> '/manage'</span><span style="color:#24292E;--s-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">    return</span><span style="color:#D73A49;--s-dark:#F97583"> new</span><span style="color:#6F42C1;--s-dark:#B392F0"> Response</span><span style="color:#24292E;--s-dark:#E1E4E8">(html, {</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">      headers: { </span><span style="color:#032F62;--s-dark:#9ECBFF">'Content-Type'</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">'text/html'</span><span style="color:#24292E;--s-dark:#E1E4E8"> },</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    });</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">  }</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">  // 验证密码</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">  if</span><span style="color:#24292E;--s-dark:#E1E4E8"> (url.pathname </span><span style="color:#D73A49;--s-dark:#F97583">===</span><span style="color:#032F62;--s-dark:#9ECBFF"> '/api/auth'</span><span style="color:#D73A49;--s-dark:#F97583"> &#x26;&#x26;</span><span style="color:#24292E;--s-dark:#E1E4E8"> request.method </span><span style="color:#D73A49;--s-dark:#F97583">===</span><span style="color:#032F62;--s-dark:#9ECBFF"> 'POST'</span><span style="color:#24292E;--s-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">    const</span><span style="color:#24292E;--s-dark:#E1E4E8"> { </span><span style="color:#005CC5;--s-dark:#79B8FF">password</span><span style="color:#24292E;--s-dark:#E1E4E8"> } </span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#D73A49;--s-dark:#F97583"> await</span><span style="color:#24292E;--s-dark:#E1E4E8"> request.</span><span style="color:#6F42C1;--s-dark:#B392F0">json</span><span style="color:#24292E;--s-dark:#E1E4E8">();</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">    return</span><span style="color:#D73A49;--s-dark:#F97583"> new</span><span style="color:#6F42C1;--s-dark:#B392F0"> Response</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">      JSON</span><span style="color:#24292E;--s-dark:#E1E4E8">.</span><span style="color:#6F42C1;--s-dark:#B392F0">stringify</span><span style="color:#24292E;--s-dark:#E1E4E8">({ success: password </span><span style="color:#D73A49;--s-dark:#F97583">===</span><span style="color:#005CC5;--s-dark:#79B8FF"> CORRECT_PASSWORD</span><span style="color:#24292E;--s-dark:#E1E4E8"> }),</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">      {</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        headers: {</span></span>
<span class="line"><span style="color:#032F62;--s-dark:#9ECBFF">          'Content-Type'</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">'application/json'</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">          ...</span><span style="color:#24292E;--s-dark:#E1E4E8">corsHeaders</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        },</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">      }</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    );</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">  }</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">  // API 路由处理</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">  if</span><span style="color:#24292E;--s-dark:#E1E4E8"> (url.pathname.</span><span style="color:#6F42C1;--s-dark:#B392F0">startsWith</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#032F62;--s-dark:#9ECBFF">'/api/memos'</span><span style="color:#24292E;--s-dark:#E1E4E8">)) {</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">    // 获取说说列表</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">    if</span><span style="color:#24292E;--s-dark:#E1E4E8"> (request.method </span><span style="color:#D73A49;--s-dark:#F97583">===</span><span style="color:#032F62;--s-dark:#9ECBFF"> 'GET'</span><span style="color:#24292E;--s-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">      const</span><span style="color:#005CC5;--s-dark:#79B8FF"> offset</span><span style="color:#D73A49;--s-dark:#F97583"> =</span><span style="color:#6F42C1;--s-dark:#B392F0"> parseInt</span><span style="color:#24292E;--s-dark:#E1E4E8">(url.searchParams.</span><span style="color:#6F42C1;--s-dark:#B392F0">get</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#032F62;--s-dark:#9ECBFF">'offset'</span><span style="color:#24292E;--s-dark:#E1E4E8">)) </span><span style="color:#D73A49;--s-dark:#F97583">||</span><span style="color:#005CC5;--s-dark:#79B8FF"> 0</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">      const</span><span style="color:#005CC5;--s-dark:#79B8FF"> limit</span><span style="color:#D73A49;--s-dark:#F97583"> =</span><span style="color:#6F42C1;--s-dark:#B392F0"> parseInt</span><span style="color:#24292E;--s-dark:#E1E4E8">(url.searchParams.</span><span style="color:#6F42C1;--s-dark:#B392F0">get</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#032F62;--s-dark:#9ECBFF">'limit'</span><span style="color:#24292E;--s-dark:#E1E4E8">)) </span><span style="color:#D73A49;--s-dark:#F97583">||</span><span style="color:#005CC5;--s-dark:#79B8FF"> 10</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">      const</span><span style="color:#005CC5;--s-dark:#79B8FF"> indexStr</span><span style="color:#D73A49;--s-dark:#F97583"> =</span><span style="color:#D73A49;--s-dark:#F97583"> await</span><span style="color:#24292E;--s-dark:#E1E4E8"> env.</span><span style="color:#005CC5;--s-dark:#79B8FF">KV</span><span style="color:#24292E;--s-dark:#E1E4E8">.</span><span style="color:#6F42C1;--s-dark:#B392F0">get</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#032F62;--s-dark:#9ECBFF">'index'</span><span style="color:#24292E;--s-dark:#E1E4E8">);</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">      const</span><span style="color:#005CC5;--s-dark:#79B8FF"> index</span><span style="color:#D73A49;--s-dark:#F97583"> =</span><span style="color:#24292E;--s-dark:#E1E4E8"> indexStr </span><span style="color:#D73A49;--s-dark:#F97583">?</span><span style="color:#005CC5;--s-dark:#79B8FF"> JSON</span><span style="color:#24292E;--s-dark:#E1E4E8">.</span><span style="color:#6F42C1;--s-dark:#B392F0">parse</span><span style="color:#24292E;--s-dark:#E1E4E8">(indexStr) </span><span style="color:#D73A49;--s-dark:#F97583">:</span><span style="color:#24292E;--s-dark:#E1E4E8"> [];</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">      const</span><span style="color:#005CC5;--s-dark:#79B8FF"> pageUids</span><span style="color:#D73A49;--s-dark:#F97583"> =</span><span style="color:#24292E;--s-dark:#E1E4E8"> index.</span><span style="color:#6F42C1;--s-dark:#B392F0">slice</span><span style="color:#24292E;--s-dark:#E1E4E8">(offset, offset </span><span style="color:#D73A49;--s-dark:#F97583">+</span><span style="color:#24292E;--s-dark:#E1E4E8"> limit);</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">      const</span><span style="color:#005CC5;--s-dark:#79B8FF"> posts</span><span style="color:#D73A49;--s-dark:#F97583"> =</span><span style="color:#D73A49;--s-dark:#F97583"> await</span><span style="color:#005CC5;--s-dark:#79B8FF"> Promise</span><span style="color:#24292E;--s-dark:#E1E4E8">.</span><span style="color:#6F42C1;--s-dark:#B392F0">all</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        pageUids.</span><span style="color:#6F42C1;--s-dark:#B392F0">map</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#E36209;--s-dark:#FFAB70">uid</span><span style="color:#D73A49;--s-dark:#F97583"> =></span><span style="color:#24292E;--s-dark:#E1E4E8"> env.</span><span style="color:#005CC5;--s-dark:#79B8FF">KV</span><span style="color:#24292E;--s-dark:#E1E4E8">.</span><span style="color:#6F42C1;--s-dark:#B392F0">get</span><span style="color:#24292E;--s-dark:#E1E4E8">(uid).</span><span style="color:#6F42C1;--s-dark:#B392F0">then</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#005CC5;--s-dark:#79B8FF">JSON</span><span style="color:#24292E;--s-dark:#E1E4E8">.parse))</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">      );</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">      return</span><span style="color:#D73A49;--s-dark:#F97583"> new</span><span style="color:#6F42C1;--s-dark:#B392F0"> Response</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#005CC5;--s-dark:#79B8FF">JSON</span><span style="color:#24292E;--s-dark:#E1E4E8">.</span><span style="color:#6F42C1;--s-dark:#B392F0">stringify</span><span style="color:#24292E;--s-dark:#E1E4E8">({</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        offset,</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        limit,</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        data: posts,</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        total: index.</span><span style="color:#005CC5;--s-dark:#79B8FF">length</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        hasMore: (offset </span><span style="color:#D73A49;--s-dark:#F97583">+</span><span style="color:#24292E;--s-dark:#E1E4E8"> limit) </span><span style="color:#D73A49;--s-dark:#F97583">&#x3C;</span><span style="color:#24292E;--s-dark:#E1E4E8"> index.</span><span style="color:#005CC5;--s-dark:#79B8FF">length</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">      }), {</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        headers: {</span></span>
<span class="line"><span style="color:#032F62;--s-dark:#9ECBFF">          'Content-Type'</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">'application/json'</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">          ...</span><span style="color:#24292E;--s-dark:#E1E4E8">corsHeaders</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        },</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">      });</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">    // 需要验证的操作</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">    if</span><span style="color:#24292E;--s-dark:#E1E4E8"> (</span><span style="color:#D73A49;--s-dark:#F97583">!</span><span style="color:#6F42C1;--s-dark:#B392F0">validateAuth</span><span style="color:#24292E;--s-dark:#E1E4E8">(request)) {</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">      return</span><span style="color:#D73A49;--s-dark:#F97583"> new</span><span style="color:#6F42C1;--s-dark:#B392F0"> Response</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#032F62;--s-dark:#9ECBFF">'Unauthorized'</span><span style="color:#24292E;--s-dark:#E1E4E8">, {</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        status: </span><span style="color:#005CC5;--s-dark:#79B8FF">401</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        headers: corsHeaders</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">      });</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">    // 发布新说说</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">    if</span><span style="color:#24292E;--s-dark:#E1E4E8"> (request.method </span><span style="color:#D73A49;--s-dark:#F97583">===</span><span style="color:#032F62;--s-dark:#9ECBFF"> 'POST'</span><span style="color:#24292E;--s-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">      const</span><span style="color:#24292E;--s-dark:#E1E4E8"> { </span><span style="color:#005CC5;--s-dark:#79B8FF">content</span><span style="color:#24292E;--s-dark:#E1E4E8"> } </span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#D73A49;--s-dark:#F97583"> await</span><span style="color:#24292E;--s-dark:#E1E4E8"> request.</span><span style="color:#6F42C1;--s-dark:#B392F0">json</span><span style="color:#24292E;--s-dark:#E1E4E8">();</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">      if</span><span style="color:#24292E;--s-dark:#E1E4E8"> (</span><span style="color:#D73A49;--s-dark:#F97583">!</span><span style="color:#24292E;--s-dark:#E1E4E8">content </span><span style="color:#D73A49;--s-dark:#F97583">||</span><span style="color:#D73A49;--s-dark:#F97583"> !</span><span style="color:#24292E;--s-dark:#E1E4E8">content.</span><span style="color:#6F42C1;--s-dark:#B392F0">trim</span><span style="color:#24292E;--s-dark:#E1E4E8">()) {</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">        return</span><span style="color:#D73A49;--s-dark:#F97583"> new</span><span style="color:#6F42C1;--s-dark:#B392F0"> Response</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#032F62;--s-dark:#9ECBFF">'Content cannot be empty'</span><span style="color:#24292E;--s-dark:#E1E4E8">, {</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">          status: </span><span style="color:#005CC5;--s-dark:#79B8FF">400</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">          headers: corsHeaders</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        });</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">      }</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">      const</span><span style="color:#005CC5;--s-dark:#79B8FF"> indexStr</span><span style="color:#D73A49;--s-dark:#F97583"> =</span><span style="color:#D73A49;--s-dark:#F97583"> await</span><span style="color:#24292E;--s-dark:#E1E4E8"> env.</span><span style="color:#005CC5;--s-dark:#79B8FF">KV</span><span style="color:#24292E;--s-dark:#E1E4E8">.</span><span style="color:#6F42C1;--s-dark:#B392F0">get</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#032F62;--s-dark:#9ECBFF">'index'</span><span style="color:#24292E;--s-dark:#E1E4E8">);</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">      const</span><span style="color:#005CC5;--s-dark:#79B8FF"> index</span><span style="color:#D73A49;--s-dark:#F97583"> =</span><span style="color:#24292E;--s-dark:#E1E4E8"> indexStr </span><span style="color:#D73A49;--s-dark:#F97583">?</span><span style="color:#005CC5;--s-dark:#79B8FF"> JSON</span><span style="color:#24292E;--s-dark:#E1E4E8">.</span><span style="color:#6F42C1;--s-dark:#B392F0">parse</span><span style="color:#24292E;--s-dark:#E1E4E8">(indexStr) </span><span style="color:#D73A49;--s-dark:#F97583">:</span><span style="color:#24292E;--s-dark:#E1E4E8"> [];</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">      let</span><span style="color:#24292E;--s-dark:#E1E4E8"> uid </span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#6F42C1;--s-dark:#B392F0"> generateUID</span><span style="color:#24292E;--s-dark:#E1E4E8">();</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">      while</span><span style="color:#24292E;--s-dark:#E1E4E8"> (</span><span style="color:#005CC5;--s-dark:#79B8FF">true</span><span style="color:#24292E;--s-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">        if</span><span style="color:#24292E;--s-dark:#E1E4E8"> (</span><span style="color:#D73A49;--s-dark:#F97583">!</span><span style="color:#24292E;--s-dark:#E1E4E8">index.</span><span style="color:#6F42C1;--s-dark:#B392F0">includes</span><span style="color:#24292E;--s-dark:#E1E4E8">(uid)) {</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">          break</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        }</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        uid </span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#6F42C1;--s-dark:#B392F0"> generateUID</span><span style="color:#24292E;--s-dark:#E1E4E8">();</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">      }</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">      const</span><span style="color:#005CC5;--s-dark:#79B8FF"> post</span><span style="color:#D73A49;--s-dark:#F97583"> =</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        uid,</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        createTime: </span><span style="color:#6F42C1;--s-dark:#B392F0">getCurrentTimeInISOFormat</span><span style="color:#24292E;--s-dark:#E1E4E8">(),</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        content: content.</span><span style="color:#6F42C1;--s-dark:#B392F0">trim</span><span style="color:#24292E;--s-dark:#E1E4E8">()</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">      };</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">      index.</span><span style="color:#6F42C1;--s-dark:#B392F0">unshift</span><span style="color:#24292E;--s-dark:#E1E4E8">(uid);</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">      await</span><span style="color:#005CC5;--s-dark:#79B8FF"> Promise</span><span style="color:#24292E;--s-dark:#E1E4E8">.</span><span style="color:#6F42C1;--s-dark:#B392F0">all</span><span style="color:#24292E;--s-dark:#E1E4E8">([</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        env.</span><span style="color:#005CC5;--s-dark:#79B8FF">KV</span><span style="color:#24292E;--s-dark:#E1E4E8">.</span><span style="color:#6F42C1;--s-dark:#B392F0">put</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#032F62;--s-dark:#9ECBFF">'index'</span><span style="color:#24292E;--s-dark:#E1E4E8">, </span><span style="color:#005CC5;--s-dark:#79B8FF">JSON</span><span style="color:#24292E;--s-dark:#E1E4E8">.</span><span style="color:#6F42C1;--s-dark:#B392F0">stringify</span><span style="color:#24292E;--s-dark:#E1E4E8">(index)),</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        env.</span><span style="color:#005CC5;--s-dark:#79B8FF">KV</span><span style="color:#24292E;--s-dark:#E1E4E8">.</span><span style="color:#6F42C1;--s-dark:#B392F0">put</span><span style="color:#24292E;--s-dark:#E1E4E8">(uid, </span><span style="color:#005CC5;--s-dark:#79B8FF">JSON</span><span style="color:#24292E;--s-dark:#E1E4E8">.</span><span style="color:#6F42C1;--s-dark:#B392F0">stringify</span><span style="color:#24292E;--s-dark:#E1E4E8">(post))</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">      ]);</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">      await</span><span style="color:#6F42C1;--s-dark:#B392F0"> executeCallback</span><span style="color:#24292E;--s-dark:#E1E4E8">();</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">      return</span><span style="color:#D73A49;--s-dark:#F97583"> new</span><span style="color:#6F42C1;--s-dark:#B392F0"> Response</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#005CC5;--s-dark:#79B8FF">JSON</span><span style="color:#24292E;--s-dark:#E1E4E8">.</span><span style="color:#6F42C1;--s-dark:#B392F0">stringify</span><span style="color:#24292E;--s-dark:#E1E4E8">(post), {</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        headers: {</span></span>
<span class="line"><span style="color:#032F62;--s-dark:#9ECBFF">          'Content-Type'</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">'application/json'</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">          ...</span><span style="color:#24292E;--s-dark:#E1E4E8">corsHeaders</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        },</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">      });</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">    // 编辑说说</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">    if</span><span style="color:#24292E;--s-dark:#E1E4E8"> (request.method </span><span style="color:#D73A49;--s-dark:#F97583">===</span><span style="color:#032F62;--s-dark:#9ECBFF"> 'PUT'</span><span style="color:#24292E;--s-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">      const</span><span style="color:#005CC5;--s-dark:#79B8FF"> uid</span><span style="color:#D73A49;--s-dark:#F97583"> =</span><span style="color:#24292E;--s-dark:#E1E4E8"> url.pathname.</span><span style="color:#6F42C1;--s-dark:#B392F0">split</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#032F62;--s-dark:#9ECBFF">'/'</span><span style="color:#24292E;--s-dark:#E1E4E8">).</span><span style="color:#6F42C1;--s-dark:#B392F0">pop</span><span style="color:#24292E;--s-dark:#E1E4E8">();</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">      const</span><span style="color:#24292E;--s-dark:#E1E4E8"> { </span><span style="color:#005CC5;--s-dark:#79B8FF">content</span><span style="color:#24292E;--s-dark:#E1E4E8"> } </span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#D73A49;--s-dark:#F97583"> await</span><span style="color:#24292E;--s-dark:#E1E4E8"> request.</span><span style="color:#6F42C1;--s-dark:#B392F0">json</span><span style="color:#24292E;--s-dark:#E1E4E8">();</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">      if</span><span style="color:#24292E;--s-dark:#E1E4E8"> (</span><span style="color:#D73A49;--s-dark:#F97583">!</span><span style="color:#24292E;--s-dark:#E1E4E8">content </span><span style="color:#D73A49;--s-dark:#F97583">||</span><span style="color:#D73A49;--s-dark:#F97583"> !</span><span style="color:#24292E;--s-dark:#E1E4E8">content.</span><span style="color:#6F42C1;--s-dark:#B392F0">trim</span><span style="color:#24292E;--s-dark:#E1E4E8">()) {</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">        return</span><span style="color:#D73A49;--s-dark:#F97583"> new</span><span style="color:#6F42C1;--s-dark:#B392F0"> Response</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#032F62;--s-dark:#9ECBFF">'Content cannot be empty'</span><span style="color:#24292E;--s-dark:#E1E4E8">, {</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">          status: </span><span style="color:#005CC5;--s-dark:#79B8FF">400</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">          headers: corsHeaders</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        });</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">      }</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">      const</span><span style="color:#005CC5;--s-dark:#79B8FF"> postStr</span><span style="color:#D73A49;--s-dark:#F97583"> =</span><span style="color:#D73A49;--s-dark:#F97583"> await</span><span style="color:#24292E;--s-dark:#E1E4E8"> env.</span><span style="color:#005CC5;--s-dark:#79B8FF">KV</span><span style="color:#24292E;--s-dark:#E1E4E8">.</span><span style="color:#6F42C1;--s-dark:#B392F0">get</span><span style="color:#24292E;--s-dark:#E1E4E8">(uid);</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">      if</span><span style="color:#24292E;--s-dark:#E1E4E8"> (</span><span style="color:#D73A49;--s-dark:#F97583">!</span><span style="color:#24292E;--s-dark:#E1E4E8">postStr) {</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">        return</span><span style="color:#D73A49;--s-dark:#F97583"> new</span><span style="color:#6F42C1;--s-dark:#B392F0"> Response</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#032F62;--s-dark:#9ECBFF">'Post not found'</span><span style="color:#24292E;--s-dark:#E1E4E8">, {</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">          status: </span><span style="color:#005CC5;--s-dark:#79B8FF">404</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">          headers: corsHeaders</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        });</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">      }</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">      const</span><span style="color:#005CC5;--s-dark:#79B8FF"> post</span><span style="color:#D73A49;--s-dark:#F97583"> =</span><span style="color:#005CC5;--s-dark:#79B8FF"> JSON</span><span style="color:#24292E;--s-dark:#E1E4E8">.</span><span style="color:#6F42C1;--s-dark:#B392F0">parse</span><span style="color:#24292E;--s-dark:#E1E4E8">(postStr);</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">      post.content </span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#24292E;--s-dark:#E1E4E8"> content.</span><span style="color:#6F42C1;--s-dark:#B392F0">trim</span><span style="color:#24292E;--s-dark:#E1E4E8">();</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">      await</span><span style="color:#24292E;--s-dark:#E1E4E8"> env.</span><span style="color:#005CC5;--s-dark:#79B8FF">KV</span><span style="color:#24292E;--s-dark:#E1E4E8">.</span><span style="color:#6F42C1;--s-dark:#B392F0">put</span><span style="color:#24292E;--s-dark:#E1E4E8">(uid, </span><span style="color:#005CC5;--s-dark:#79B8FF">JSON</span><span style="color:#24292E;--s-dark:#E1E4E8">.</span><span style="color:#6F42C1;--s-dark:#B392F0">stringify</span><span style="color:#24292E;--s-dark:#E1E4E8">(post));</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">      // 检查是否需要回调</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">      if</span><span style="color:#24292E;--s-dark:#E1E4E8"> (</span><span style="color:#D73A49;--s-dark:#F97583">await</span><span style="color:#6F42C1;--s-dark:#B392F0"> shouldNotify</span><span style="color:#24292E;--s-dark:#E1E4E8">(uid)) {</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">        await</span><span style="color:#6F42C1;--s-dark:#B392F0"> executeCallback</span><span style="color:#24292E;--s-dark:#E1E4E8">();</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">      }</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">      return</span><span style="color:#D73A49;--s-dark:#F97583"> new</span><span style="color:#6F42C1;--s-dark:#B392F0"> Response</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#005CC5;--s-dark:#79B8FF">JSON</span><span style="color:#24292E;--s-dark:#E1E4E8">.</span><span style="color:#6F42C1;--s-dark:#B392F0">stringify</span><span style="color:#24292E;--s-dark:#E1E4E8">(post), {</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        headers: {</span></span>
<span class="line"><span style="color:#032F62;--s-dark:#9ECBFF">          'Content-Type'</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">'application/json'</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">          ...</span><span style="color:#24292E;--s-dark:#E1E4E8">corsHeaders</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        },</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">      });</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">    // 删除说说</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">    if</span><span style="color:#24292E;--s-dark:#E1E4E8"> (request.method </span><span style="color:#D73A49;--s-dark:#F97583">===</span><span style="color:#032F62;--s-dark:#9ECBFF"> 'DELETE'</span><span style="color:#24292E;--s-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">      const</span><span style="color:#005CC5;--s-dark:#79B8FF"> uid</span><span style="color:#D73A49;--s-dark:#F97583"> =</span><span style="color:#24292E;--s-dark:#E1E4E8"> url.pathname.</span><span style="color:#6F42C1;--s-dark:#B392F0">split</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#032F62;--s-dark:#9ECBFF">'/'</span><span style="color:#24292E;--s-dark:#E1E4E8">).</span><span style="color:#6F42C1;--s-dark:#B392F0">pop</span><span style="color:#24292E;--s-dark:#E1E4E8">();</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">      const</span><span style="color:#005CC5;--s-dark:#79B8FF"> indexStr</span><span style="color:#D73A49;--s-dark:#F97583"> =</span><span style="color:#D73A49;--s-dark:#F97583"> await</span><span style="color:#24292E;--s-dark:#E1E4E8"> env.</span><span style="color:#005CC5;--s-dark:#79B8FF">KV</span><span style="color:#24292E;--s-dark:#E1E4E8">.</span><span style="color:#6F42C1;--s-dark:#B392F0">get</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#032F62;--s-dark:#9ECBFF">'index'</span><span style="color:#24292E;--s-dark:#E1E4E8">);</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">      if</span><span style="color:#24292E;--s-dark:#E1E4E8"> (</span><span style="color:#D73A49;--s-dark:#F97583">!</span><span style="color:#24292E;--s-dark:#E1E4E8">indexStr) {</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">        return</span><span style="color:#D73A49;--s-dark:#F97583"> new</span><span style="color:#6F42C1;--s-dark:#B392F0"> Response</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#032F62;--s-dark:#9ECBFF">'Post not found'</span><span style="color:#24292E;--s-dark:#E1E4E8">, {</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">          status: </span><span style="color:#005CC5;--s-dark:#79B8FF">404</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">          headers: corsHeaders</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        });</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">      }</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">      const</span><span style="color:#005CC5;--s-dark:#79B8FF"> needCallback</span><span style="color:#D73A49;--s-dark:#F97583"> =</span><span style="color:#D73A49;--s-dark:#F97583"> await</span><span style="color:#6F42C1;--s-dark:#B392F0"> shouldNotify</span><span style="color:#24292E;--s-dark:#E1E4E8">(uid);</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">      const</span><span style="color:#005CC5;--s-dark:#79B8FF"> index</span><span style="color:#D73A49;--s-dark:#F97583"> =</span><span style="color:#005CC5;--s-dark:#79B8FF"> JSON</span><span style="color:#24292E;--s-dark:#E1E4E8">.</span><span style="color:#6F42C1;--s-dark:#B392F0">parse</span><span style="color:#24292E;--s-dark:#E1E4E8">(indexStr);</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">      const</span><span style="color:#005CC5;--s-dark:#79B8FF"> newIndex</span><span style="color:#D73A49;--s-dark:#F97583"> =</span><span style="color:#24292E;--s-dark:#E1E4E8"> index.</span><span style="color:#6F42C1;--s-dark:#B392F0">filter</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#E36209;--s-dark:#FFAB70">id</span><span style="color:#D73A49;--s-dark:#F97583"> =></span><span style="color:#24292E;--s-dark:#E1E4E8"> id </span><span style="color:#D73A49;--s-dark:#F97583">!==</span><span style="color:#24292E;--s-dark:#E1E4E8"> uid);</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">      await</span><span style="color:#005CC5;--s-dark:#79B8FF"> Promise</span><span style="color:#24292E;--s-dark:#E1E4E8">.</span><span style="color:#6F42C1;--s-dark:#B392F0">all</span><span style="color:#24292E;--s-dark:#E1E4E8">([</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        env.</span><span style="color:#005CC5;--s-dark:#79B8FF">KV</span><span style="color:#24292E;--s-dark:#E1E4E8">.</span><span style="color:#6F42C1;--s-dark:#B392F0">put</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#032F62;--s-dark:#9ECBFF">'index'</span><span style="color:#24292E;--s-dark:#E1E4E8">, </span><span style="color:#005CC5;--s-dark:#79B8FF">JSON</span><span style="color:#24292E;--s-dark:#E1E4E8">.</span><span style="color:#6F42C1;--s-dark:#B392F0">stringify</span><span style="color:#24292E;--s-dark:#E1E4E8">(newIndex)),</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        env.</span><span style="color:#005CC5;--s-dark:#79B8FF">KV</span><span style="color:#24292E;--s-dark:#E1E4E8">.</span><span style="color:#6F42C1;--s-dark:#B392F0">delete</span><span style="color:#24292E;--s-dark:#E1E4E8">(uid)</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">      ]);</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">      if</span><span style="color:#24292E;--s-dark:#E1E4E8"> (needCallback) {</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">        await</span><span style="color:#6F42C1;--s-dark:#B392F0"> executeCallback</span><span style="color:#24292E;--s-dark:#E1E4E8">();</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">      }</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">      return</span><span style="color:#D73A49;--s-dark:#F97583"> new</span><span style="color:#6F42C1;--s-dark:#B392F0"> Response</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#005CC5;--s-dark:#79B8FF">JSON</span><span style="color:#24292E;--s-dark:#E1E4E8">.</span><span style="color:#6F42C1;--s-dark:#B392F0">stringify</span><span style="color:#24292E;--s-dark:#E1E4E8">({ success: </span><span style="color:#005CC5;--s-dark:#79B8FF">true</span><span style="color:#24292E;--s-dark:#E1E4E8"> }), {</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        headers: {</span></span>
<span class="line"><span style="color:#032F62;--s-dark:#9ECBFF">          'Content-Type'</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">'application/json'</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">          ...</span><span style="color:#24292E;--s-dark:#E1E4E8">corsHeaders</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        },</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">      });</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">  }</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">  return</span><span style="color:#D73A49;--s-dark:#F97583"> new</span><span style="color:#6F42C1;--s-dark:#B392F0"> Response</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#032F62;--s-dark:#9ECBFF">'Not Found'</span><span style="color:#24292E;--s-dark:#E1E4E8">, {</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    status: </span><span style="color:#005CC5;--s-dark:#79B8FF">404</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    headers: corsHeaders</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">  });</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">}</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">export</span><span style="color:#D73A49;--s-dark:#F97583"> default</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">  async</span><span style="color:#6F42C1;--s-dark:#B392F0"> fetch</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#E36209;--s-dark:#FFAB70">request</span><span style="color:#24292E;--s-dark:#E1E4E8">, </span><span style="color:#E36209;--s-dark:#FFAB70">env</span><span style="color:#24292E;--s-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">    try</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">      return</span><span style="color:#6F42C1;--s-dark:#B392F0"> handleRequest</span><span style="color:#24292E;--s-dark:#E1E4E8">(request, env);</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    } </span><span style="color:#D73A49;--s-dark:#F97583">catch</span><span style="color:#24292E;--s-dark:#E1E4E8"> (error) {</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">      return</span><span style="color:#D73A49;--s-dark:#F97583"> new</span><span style="color:#6F42C1;--s-dark:#B392F0"> Response</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#032F62;--s-dark:#9ECBFF">`Internal Server Error: ${</span><span style="color:#24292E;--s-dark:#E1E4E8">error</span><span style="color:#032F62;--s-dark:#9ECBFF">.</span><span style="color:#24292E;--s-dark:#E1E4E8">message</span><span style="color:#032F62;--s-dark:#9ECBFF">}`</span><span style="color:#24292E;--s-dark:#E1E4E8">, {</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        status: </span><span style="color:#005CC5;--s-dark:#79B8FF">500</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        headers: </span><span style="color:#6F42C1;--s-dark:#B392F0">handleCORS</span><span style="color:#24292E;--s-dark:#E1E4E8">(request)</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">      });</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">  },</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">};</span></span></code><label class="code-collapse-collapse" for="toggle-zydeqyu"><svg class="code-collapse-icon" width="32" height="24" viewBox="0 0 32 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20,15 12,7 4,15"></polyline></svg></label></pre></div></div></div>
<p>最顶上三个常量需要配置：</p>
<ul>
<li><code>CORRECT_PASSWORD</code>，页面密码</li>
<li><code>CALLBACK_URL</code>，发布新说说或更新/删除说说后触发的回调地址</li>
<li><code>ALLOWED_ORIGINS</code>，跨域处理，允许访问的域名列表，至少两个：你的博客域名和管理页面域名</li>
</ul>
<p>配置完成后点击发布</p>
<p>由于墙的原因，默认的 <code>workers.dev</code> 域名很难访问，最好为 worker 配置一个新的域名。在 <code>memos 详情页面 - 设置 - 域和路由</code>，添加一个自定义域，填入一个在 Cloudflare 上托管的域名即可。注意这个域名也要添加到 <code>worker.js</code> 的 <code>ALLOWED_ORIGINS</code> 中</p>
<p>完成后就可以使用这个管理页面了，管理页面的 URL 为 <code>https://{你的域名}/manage</code>，进入页面需要输入密码，then enjoy！</p>
<h3 id="前端">前端<a href="#前端" class="heading-anchor-link" aria-label="Link to 前端"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h3>
<p>Thanks to VitePress，我们可以很方便地通过 Vue 组件的方式，编写说说前端并嵌入博客</p>
<p>首先安装 markedjs 依赖，pnpm 可使用如下命令：</p>
<div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">pnpm</span><span style="color:#032F62;--s-dark:#9ECBFF"> add</span><span style="color:#032F62;--s-dark:#9ECBFF"> marked</span></span></code></pre></div>
<p>在你的博客的主题配置文件（通常为 <code>docs/.vitepress/theme/index.ts</code>，文件路径和拓展名也许会有区别）的同级目录下，新建一个 <code>components</code> 文件夹（已有则无需新建），在其中新建 <code>memos.vue</code></p>
<div class="code-collapse-wrapper"><input type="checkbox" id="toggle-tt6jqo5" class="code-collapse-toggle" style="display: none;"><div class="code-collapse-preview"><div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0" class="code-preview"><code><span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">&#x3C;</span><span style="color:#22863A;--s-dark:#85E89D">template</span><span style="color:#24292E;--s-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    &#x3C;</span><span style="color:#22863A;--s-dark:#85E89D">div</span><span style="color:#6F42C1;--s-dark:#B392F0"> class</span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#032F62;--s-dark:#9ECBFF">"memos-container"</span><span style="color:#24292E;--s-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        &#x3C;</span><span style="color:#22863A;--s-dark:#85E89D">div</span><span style="color:#6F42C1;--s-dark:#B392F0"> v-for</span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#032F62;--s-dark:#9ECBFF">"memo of memoList"</span><span style="color:#B31D28;--s-light-font-style:italic;--s-dark:#FDAEB7;--s-dark-font-style:italic"> :key="memo.uid"></span></span>
<span class="line"><span style="color:#B31D28;--s-light-font-style:italic;--s-dark:#FDAEB7;--s-dark-font-style:italic">            &#x3C;div</span><span style="color:#6F42C1;--s-dark:#B392F0"> class</span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#032F62;--s-dark:#9ECBFF">"card"</span><span style="color:#24292E;--s-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">                &#x3C;</span><span style="color:#22863A;--s-dark:#85E89D">div</span><span style="color:#6F42C1;--s-dark:#B392F0"> class</span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#032F62;--s-dark:#9ECBFF">"header"</span><span style="color:#24292E;--s-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">                    &#x3C;</span><span style="color:#22863A;--s-dark:#85E89D">span</span><span style="color:#6F42C1;--s-dark:#B392F0"> class</span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#032F62;--s-dark:#9ECBFF">"time-text"</span><span style="color:#24292E;--s-dark:#E1E4E8">>{{ memo.createTime }}&#x3C;/</span><span style="color:#22863A;--s-dark:#85E89D">span</span><span style="color:#24292E;--s-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">                &#x3C;/</span><span style="color:#22863A;--s-dark:#85E89D">div</span><span style="color:#24292E;--s-dark:#E1E4E8">></span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">                &#x3C;</span><span style="color:#22863A;--s-dark:#85E89D">div</span><span style="color:#6F42C1;--s-dark:#B392F0"> class</span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#032F62;--s-dark:#9ECBFF">"memo-content"</span><span style="color:#6F42C1;--s-dark:#B392F0"> v-html</span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#032F62;--s-dark:#9ECBFF">"memo.content"</span><span style="color:#24292E;--s-dark:#E1E4E8"> /></span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            &#x3C;/</span><span style="color:#22863A;--s-dark:#85E89D">div</span><span style="color:#24292E;--s-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        &#x3C;/</span><span style="color:#22863A;--s-dark:#85E89D">div</span><span style="color:#24292E;--s-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        &#x3C;</span><span style="color:#22863A;--s-dark:#85E89D">div</span><span style="color:#6F42C1;--s-dark:#B392F0"> v-if</span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#032F62;--s-dark:#9ECBFF">"hasMore"</span><span style="color:#6F42C1;--s-dark:#B392F0"> class</span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#032F62;--s-dark:#9ECBFF">"load-more"</span><span style="color:#24292E;--s-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            &#x3C;</span><span style="color:#22863A;--s-dark:#85E89D">button</span><span style="color:#B31D28;--s-light-font-style:italic;--s-dark:#FDAEB7;--s-dark-font-style:italic"> @click="loadMoreMemos"</span><span style="color:#B31D28;--s-light-font-style:italic;--s-dark:#FDAEB7;--s-dark-font-style:italic"> :disabled="isLoading"</span><span style="color:#6F42C1;--s-dark:#B392F0"> class</span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#032F62;--s-dark:#9ECBFF">"load-more-button"</span><span style="color:#24292E;--s-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">                &#x3C;</span><span style="color:#22863A;--s-dark:#85E89D">span</span><span style="color:#6F42C1;--s-dark:#B392F0"> v-if</span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#032F62;--s-dark:#9ECBFF">"!isLoading"</span><span style="color:#24292E;--s-dark:#E1E4E8">>加载更多&#x3C;/</span><span style="color:#22863A;--s-dark:#85E89D">span</span><span style="color:#24292E;--s-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">                &#x3C;</span><span style="color:#22863A;--s-dark:#85E89D">span</span><span style="color:#6F42C1;--s-dark:#B392F0"> v-else</span><span style="color:#6F42C1;--s-dark:#B392F0"> class</span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#032F62;--s-dark:#9ECBFF">"loading-spinner"</span><span style="color:#24292E;--s-dark:#E1E4E8">>&#x3C;/</span><span style="color:#22863A;--s-dark:#85E89D">span</span><span style="color:#24292E;--s-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            &#x3C;/</span><span style="color:#22863A;--s-dark:#85E89D">button</span><span style="color:#24292E;--s-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        &#x3C;/</span><span style="color:#22863A;--s-dark:#85E89D">div</span><span style="color:#24292E;--s-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    &#x3C;/</span><span style="color:#22863A;--s-dark:#85E89D">div</span><span style="color:#24292E;--s-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">&#x3C;/</span><span style="color:#24292E;--s-dark:#E1E4E8">template</span><span style="color:#D73A49;--s-dark:#F97583">></span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">&#x3C;</span><span style="color:#22863A;--s-dark:#85E89D">script</span><span style="color:#6F42C1;--s-dark:#B392F0"> setup</span><span style="color:#6F42C1;--s-dark:#B392F0"> lang</span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#032F62;--s-dark:#9ECBFF">"ts"</span><span style="color:#24292E;--s-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">import { marked, Tokens } from "marked"</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">import { reactive, toRefs, onMounted } from "vue"</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">import memosRaw from '../../../../memos.json'       // [!code highlight]</span></span>
<span class="line"></span>
</code></pre></div><div class="code-collapse-gradient"></div><label class="code-collapse-expand" for="toggle-tt6jqo5"><svg class="code-collapse-icon" width="32" height="24" viewBox="0 0 32 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="4,9 12,17 20,9"></polyline></svg></label></div><div class="code-collapse-full"><div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">&#x3C;</span><span style="color:#22863A;--s-dark:#85E89D">template</span><span style="color:#24292E;--s-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    &#x3C;</span><span style="color:#22863A;--s-dark:#85E89D">div</span><span style="color:#6F42C1;--s-dark:#B392F0"> class</span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#032F62;--s-dark:#9ECBFF">"memos-container"</span><span style="color:#24292E;--s-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        &#x3C;</span><span style="color:#22863A;--s-dark:#85E89D">div</span><span style="color:#6F42C1;--s-dark:#B392F0"> v-for</span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#032F62;--s-dark:#9ECBFF">"memo of memoList"</span><span style="color:#B31D28;--s-light-font-style:italic;--s-dark:#FDAEB7;--s-dark-font-style:italic"> :key="memo.uid"></span></span>
<span class="line"><span style="color:#B31D28;--s-light-font-style:italic;--s-dark:#FDAEB7;--s-dark-font-style:italic">            &#x3C;div</span><span style="color:#6F42C1;--s-dark:#B392F0"> class</span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#032F62;--s-dark:#9ECBFF">"card"</span><span style="color:#24292E;--s-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">                &#x3C;</span><span style="color:#22863A;--s-dark:#85E89D">div</span><span style="color:#6F42C1;--s-dark:#B392F0"> class</span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#032F62;--s-dark:#9ECBFF">"header"</span><span style="color:#24292E;--s-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">                    &#x3C;</span><span style="color:#22863A;--s-dark:#85E89D">span</span><span style="color:#6F42C1;--s-dark:#B392F0"> class</span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#032F62;--s-dark:#9ECBFF">"time-text"</span><span style="color:#24292E;--s-dark:#E1E4E8">>{{ memo.createTime }}&#x3C;/</span><span style="color:#22863A;--s-dark:#85E89D">span</span><span style="color:#24292E;--s-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">                &#x3C;/</span><span style="color:#22863A;--s-dark:#85E89D">div</span><span style="color:#24292E;--s-dark:#E1E4E8">></span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">                &#x3C;</span><span style="color:#22863A;--s-dark:#85E89D">div</span><span style="color:#6F42C1;--s-dark:#B392F0"> class</span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#032F62;--s-dark:#9ECBFF">"memo-content"</span><span style="color:#6F42C1;--s-dark:#B392F0"> v-html</span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#032F62;--s-dark:#9ECBFF">"memo.content"</span><span style="color:#24292E;--s-dark:#E1E4E8"> /></span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            &#x3C;/</span><span style="color:#22863A;--s-dark:#85E89D">div</span><span style="color:#24292E;--s-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        &#x3C;/</span><span style="color:#22863A;--s-dark:#85E89D">div</span><span style="color:#24292E;--s-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        &#x3C;</span><span style="color:#22863A;--s-dark:#85E89D">div</span><span style="color:#6F42C1;--s-dark:#B392F0"> v-if</span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#032F62;--s-dark:#9ECBFF">"hasMore"</span><span style="color:#6F42C1;--s-dark:#B392F0"> class</span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#032F62;--s-dark:#9ECBFF">"load-more"</span><span style="color:#24292E;--s-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            &#x3C;</span><span style="color:#22863A;--s-dark:#85E89D">button</span><span style="color:#B31D28;--s-light-font-style:italic;--s-dark:#FDAEB7;--s-dark-font-style:italic"> @click="loadMoreMemos"</span><span style="color:#B31D28;--s-light-font-style:italic;--s-dark:#FDAEB7;--s-dark-font-style:italic"> :disabled="isLoading"</span><span style="color:#6F42C1;--s-dark:#B392F0"> class</span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#032F62;--s-dark:#9ECBFF">"load-more-button"</span><span style="color:#24292E;--s-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">                &#x3C;</span><span style="color:#22863A;--s-dark:#85E89D">span</span><span style="color:#6F42C1;--s-dark:#B392F0"> v-if</span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#032F62;--s-dark:#9ECBFF">"!isLoading"</span><span style="color:#24292E;--s-dark:#E1E4E8">>加载更多&#x3C;/</span><span style="color:#22863A;--s-dark:#85E89D">span</span><span style="color:#24292E;--s-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">                &#x3C;</span><span style="color:#22863A;--s-dark:#85E89D">span</span><span style="color:#6F42C1;--s-dark:#B392F0"> v-else</span><span style="color:#6F42C1;--s-dark:#B392F0"> class</span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#032F62;--s-dark:#9ECBFF">"loading-spinner"</span><span style="color:#24292E;--s-dark:#E1E4E8">>&#x3C;/</span><span style="color:#22863A;--s-dark:#85E89D">span</span><span style="color:#24292E;--s-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            &#x3C;/</span><span style="color:#22863A;--s-dark:#85E89D">button</span><span style="color:#24292E;--s-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        &#x3C;/</span><span style="color:#22863A;--s-dark:#85E89D">div</span><span style="color:#24292E;--s-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    &#x3C;/</span><span style="color:#22863A;--s-dark:#85E89D">div</span><span style="color:#24292E;--s-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">&#x3C;/</span><span style="color:#24292E;--s-dark:#E1E4E8">template</span><span style="color:#D73A49;--s-dark:#F97583">></span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">&#x3C;</span><span style="color:#22863A;--s-dark:#85E89D">script</span><span style="color:#6F42C1;--s-dark:#B392F0"> setup</span><span style="color:#6F42C1;--s-dark:#B392F0"> lang</span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#032F62;--s-dark:#9ECBFF">"ts"</span><span style="color:#24292E;--s-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">import { marked, Tokens } from "marked"</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">import { reactive, toRefs, onMounted } from "vue"</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">import memosRaw from '../../../../memos.json'       // [!code highlight]</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">interface memosRes {</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    data: memo[]</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    hasMore: boolean</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">interface image {</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    name: string</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    filename: string</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    url: string</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">interface memo {</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    uid: string</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    createTime: string</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    content: string</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">function convertToLocalTime(dateString: string, timeZone: string = 'Asia/Shanghai'): string {</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">    // 创建 Date 对象</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    const date </span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#D73A49;--s-dark:#F97583"> new</span><span style="color:#6F42C1;--s-dark:#B392F0"> Date</span><span style="color:#24292E;--s-dark:#E1E4E8">(dateString);</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">    // 提取所需的时间组件</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    const options: Intl.DateTimeFormatOptions </span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        timeZone: timeZone,</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        year: </span><span style="color:#032F62;--s-dark:#9ECBFF">'numeric'</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        month: </span><span style="color:#032F62;--s-dark:#9ECBFF">'2-digit'</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        day: </span><span style="color:#032F62;--s-dark:#9ECBFF">'2-digit'</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        hour: </span><span style="color:#032F62;--s-dark:#9ECBFF">'2-digit'</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        minute: </span><span style="color:#032F62;--s-dark:#9ECBFF">'2-digit'</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        second: </span><span style="color:#032F62;--s-dark:#9ECBFF">'2-digit'</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        hour12: </span><span style="color:#005CC5;--s-dark:#79B8FF">false</span><span style="color:#6A737D;--s-dark:#6A737D"> // 使用 24 小时制</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    };</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    const formatter </span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#D73A49;--s-dark:#F97583"> new</span><span style="color:#24292E;--s-dark:#E1E4E8"> Intl.</span><span style="color:#6F42C1;--s-dark:#B392F0">DateTimeFormat</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#032F62;--s-dark:#9ECBFF">'zh-CN'</span><span style="color:#24292E;--s-dark:#E1E4E8">, options);</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    const parts </span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#24292E;--s-dark:#E1E4E8"> formatter.</span><span style="color:#6F42C1;--s-dark:#B392F0">formatToParts</span><span style="color:#24292E;--s-dark:#E1E4E8">(date);</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">    // 构建最终输出格式</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    const year </span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#24292E;--s-dark:#E1E4E8"> parts.</span><span style="color:#6F42C1;--s-dark:#B392F0">find</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#E36209;--s-dark:#FFAB70">part</span><span style="color:#D73A49;--s-dark:#F97583"> =></span><span style="color:#24292E;--s-dark:#E1E4E8"> part.type </span><span style="color:#D73A49;--s-dark:#F97583">===</span><span style="color:#032F62;--s-dark:#9ECBFF"> 'year'</span><span style="color:#24292E;--s-dark:#E1E4E8">)?.value;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    const month </span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#24292E;--s-dark:#E1E4E8"> parts.</span><span style="color:#6F42C1;--s-dark:#B392F0">find</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#E36209;--s-dark:#FFAB70">part</span><span style="color:#D73A49;--s-dark:#F97583"> =></span><span style="color:#24292E;--s-dark:#E1E4E8"> part.type </span><span style="color:#D73A49;--s-dark:#F97583">===</span><span style="color:#032F62;--s-dark:#9ECBFF"> 'month'</span><span style="color:#24292E;--s-dark:#E1E4E8">)?.value;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    const day </span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#24292E;--s-dark:#E1E4E8"> parts.</span><span style="color:#6F42C1;--s-dark:#B392F0">find</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#E36209;--s-dark:#FFAB70">part</span><span style="color:#D73A49;--s-dark:#F97583"> =></span><span style="color:#24292E;--s-dark:#E1E4E8"> part.type </span><span style="color:#D73A49;--s-dark:#F97583">===</span><span style="color:#032F62;--s-dark:#9ECBFF"> 'day'</span><span style="color:#24292E;--s-dark:#E1E4E8">)?.value;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    const hour </span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#24292E;--s-dark:#E1E4E8"> parts.</span><span style="color:#6F42C1;--s-dark:#B392F0">find</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#E36209;--s-dark:#FFAB70">part</span><span style="color:#D73A49;--s-dark:#F97583"> =></span><span style="color:#24292E;--s-dark:#E1E4E8"> part.type </span><span style="color:#D73A49;--s-dark:#F97583">===</span><span style="color:#032F62;--s-dark:#9ECBFF"> 'hour'</span><span style="color:#24292E;--s-dark:#E1E4E8">)?.value;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    const minute </span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#24292E;--s-dark:#E1E4E8"> parts.</span><span style="color:#6F42C1;--s-dark:#B392F0">find</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#E36209;--s-dark:#FFAB70">part</span><span style="color:#D73A49;--s-dark:#F97583"> =></span><span style="color:#24292E;--s-dark:#E1E4E8"> part.type </span><span style="color:#D73A49;--s-dark:#F97583">===</span><span style="color:#032F62;--s-dark:#9ECBFF"> 'minute'</span><span style="color:#24292E;--s-dark:#E1E4E8">)?.value;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    const second </span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#24292E;--s-dark:#E1E4E8"> parts.</span><span style="color:#6F42C1;--s-dark:#B392F0">find</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#E36209;--s-dark:#FFAB70">part</span><span style="color:#D73A49;--s-dark:#F97583"> =></span><span style="color:#24292E;--s-dark:#E1E4E8"> part.type </span><span style="color:#D73A49;--s-dark:#F97583">===</span><span style="color:#032F62;--s-dark:#9ECBFF"> 'second'</span><span style="color:#24292E;--s-dark:#E1E4E8">)?.value;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">    // 拼接成目标格式</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    return </span><span style="color:#032F62;--s-dark:#9ECBFF">`${</span><span style="color:#24292E;--s-dark:#E1E4E8">year</span><span style="color:#032F62;--s-dark:#9ECBFF">}-${</span><span style="color:#24292E;--s-dark:#E1E4E8">month</span><span style="color:#032F62;--s-dark:#9ECBFF">}-${</span><span style="color:#24292E;--s-dark:#E1E4E8">day</span><span style="color:#032F62;--s-dark:#9ECBFF">} ${</span><span style="color:#24292E;--s-dark:#E1E4E8">hour</span><span style="color:#032F62;--s-dark:#9ECBFF">}:${</span><span style="color:#24292E;--s-dark:#E1E4E8">minute</span><span style="color:#032F62;--s-dark:#9ECBFF">}:${</span><span style="color:#24292E;--s-dark:#E1E4E8">second</span><span style="color:#032F62;--s-dark:#9ECBFF">}`</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">const PAGE_SIZE = 10;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">const data = reactive({</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    memoList: [] </span><span style="color:#D73A49;--s-dark:#F97583">as</span><span style="color:#6F42C1;--s-dark:#B392F0"> memo</span><span style="color:#24292E;--s-dark:#E1E4E8">[],</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    offset: </span><span style="color:#005CC5;--s-dark:#79B8FF">10</span><span style="color:#24292E;--s-dark:#E1E4E8">, </span><span style="color:#6A737D;--s-dark:#6A737D">// 从文件加载了 10 条，所以初始 offset 为 10</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    hasMore: </span><span style="color:#005CC5;--s-dark:#79B8FF">true</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    isLoading: </span><span style="color:#005CC5;--s-dark:#79B8FF">false</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">})</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">const { memoList, hasMore, isLoading } = toRefs(data);</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">const renderer = new marked.Renderer();</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">renderer.image = function({href, title, text}: Tokens.Image):string {</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">  return </span><span style="color:#032F62;--s-dark:#9ECBFF">`</span></span>
<span class="line"><span style="color:#032F62;--s-dark:#9ECBFF">    &#x3C;div class="img-container"></span></span>
<span class="line"><span style="color:#032F62;--s-dark:#9ECBFF">        &#x3C;img class="imgwrp" loading="lazy" src="${</span><span style="color:#24292E;--s-dark:#E1E4E8">href</span><span style="color:#032F62;--s-dark:#9ECBFF">}" /></span></span>
<span class="line"><span style="color:#032F62;--s-dark:#9ECBFF">    &#x3C;/div></span></span>
<span class="line"><span style="color:#032F62;--s-dark:#9ECBFF">  `</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">};</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">marked.use({</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    renderer: renderer,</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    breaks: </span><span style="color:#005CC5;--s-dark:#79B8FF">true</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    gfm: </span><span style="color:#005CC5;--s-dark:#79B8FF">true</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">})</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">function processMemos(memos: memo[]) {</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">  return memos.</span><span style="color:#6F42C1;--s-dark:#B392F0">map</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#E36209;--s-dark:#FFAB70">memo</span><span style="color:#D73A49;--s-dark:#F97583"> =></span><span style="color:#24292E;--s-dark:#E1E4E8"> ({</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">    ...</span><span style="color:#24292E;--s-dark:#E1E4E8">memo,</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    content: marked.</span><span style="color:#6F42C1;--s-dark:#B392F0">parse</span><span style="color:#24292E;--s-dark:#E1E4E8">(memo.content) </span><span style="color:#D73A49;--s-dark:#F97583">as</span><span style="color:#005CC5;--s-dark:#79B8FF"> string</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    createTime: </span><span style="color:#6F42C1;--s-dark:#B392F0">convertToLocalTime</span><span style="color:#24292E;--s-dark:#E1E4E8">(memo.createTime)</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">  }));</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">// 初始化数据</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">onMounted(() => {</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">  const initialMemos </span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#24292E;--s-dark:#E1E4E8"> memosRaw.data </span><span style="color:#D73A49;--s-dark:#F97583">as</span><span style="color:#6F42C1;--s-dark:#B392F0"> memo</span><span style="color:#24292E;--s-dark:#E1E4E8">[];</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">  data.memoList </span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#6F42C1;--s-dark:#B392F0"> processMemos</span><span style="color:#24292E;--s-dark:#E1E4E8">(initialMemos);</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">});</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">async function loadMoreMemos() {</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">  if</span><span style="color:#24292E;--s-dark:#E1E4E8"> (</span><span style="color:#D73A49;--s-dark:#F97583">!</span><span style="color:#24292E;--s-dark:#E1E4E8">data.hasMore </span><span style="color:#D73A49;--s-dark:#F97583">||</span><span style="color:#24292E;--s-dark:#E1E4E8"> data.isLoading) return;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">  </span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">  data.isLoading </span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#005CC5;--s-dark:#79B8FF"> true</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">  try {</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    const url </span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#032F62;--s-dark:#9ECBFF"> `https://{你的域名}/api/memos?limit=${</span><span style="color:#005CC5;--s-dark:#79B8FF">PAGE_SIZE</span><span style="color:#032F62;--s-dark:#9ECBFF">}&#x26;offset=${</span><span style="color:#24292E;--s-dark:#E1E4E8">data</span><span style="color:#032F62;--s-dark:#9ECBFF">.</span><span style="color:#24292E;--s-dark:#E1E4E8">offset</span><span style="color:#032F62;--s-dark:#9ECBFF">}`</span><span style="color:#24292E;--s-dark:#E1E4E8">;   </span><span style="color:#6A737D;--s-dark:#6A737D">// [!code highlight]</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    const response </span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#D73A49;--s-dark:#F97583"> await</span><span style="color:#6F42C1;--s-dark:#B392F0"> fetch</span><span style="color:#24292E;--s-dark:#E1E4E8">(url);</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    const result: memosRes </span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#D73A49;--s-dark:#F97583"> await</span><span style="color:#24292E;--s-dark:#E1E4E8"> response.</span><span style="color:#6F42C1;--s-dark:#B392F0">json</span><span style="color:#24292E;--s-dark:#E1E4E8">();</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    </span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    const processedMemos </span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#6F42C1;--s-dark:#B392F0"> processMemos</span><span style="color:#24292E;--s-dark:#E1E4E8">(result.data);</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    data.memoList.</span><span style="color:#6F42C1;--s-dark:#B392F0">push</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#D73A49;--s-dark:#F97583">...</span><span style="color:#24292E;--s-dark:#E1E4E8">processedMemos);</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    data.offset </span><span style="color:#D73A49;--s-dark:#F97583">+=</span><span style="color:#24292E;--s-dark:#E1E4E8"> result.data.</span><span style="color:#005CC5;--s-dark:#79B8FF">length</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    data.hasMore </span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#24292E;--s-dark:#E1E4E8"> result.hasMore;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">  } </span><span style="color:#6F42C1;--s-dark:#B392F0">catch</span><span style="color:#24292E;--s-dark:#E1E4E8"> (error) {</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    console.error(</span><span style="color:#032F62;--s-dark:#9ECBFF">'Failed to load memos:'</span><span style="color:#24292E;--s-dark:#E1E4E8">, error);</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">  } finally {</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    data.isLoading </span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#005CC5;--s-dark:#79B8FF"> false</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">  }</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">}</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">&#x3C;/</span><span style="color:#22863A;--s-dark:#85E89D">script</span><span style="color:#24292E;--s-dark:#E1E4E8">></span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">&#x3C;</span><span style="color:#22863A;--s-dark:#85E89D">style</span><span style="color:#6F42C1;--s-dark:#B392F0"> lang</span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#032F62;--s-dark:#9ECBFF">"scss"</span><span style="color:#24292E;--s-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">.card {</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    border</span><span style="color:#D73A49;--s-dark:#F97583">-</span><span style="color:#24292E;--s-dark:#E1E4E8">style: solid;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    margin</span><span style="color:#D73A49;--s-dark:#F97583">-</span><span style="color:#24292E;--s-dark:#E1E4E8">bottom: .5rem;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    border</span><span style="color:#D73A49;--s-dark:#F97583">-</span><span style="color:#24292E;--s-dark:#E1E4E8">width: 1px; </span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    position: relative;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    border</span><span style="color:#D73A49;--s-dark:#F97583">-</span><span style="color:#24292E;--s-dark:#E1E4E8">radius: .5rem;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    border</span><span style="color:#D73A49;--s-dark:#F97583">-</span><span style="color:#24292E;--s-dark:#E1E4E8">color: </span><span style="color:#6F42C1;--s-dark:#B392F0">var</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#D73A49;--s-dark:#F97583">--</span><span style="color:#24292E;--s-dark:#E1E4E8">vp</span><span style="color:#D73A49;--s-dark:#F97583">-</span><span style="color:#24292E;--s-dark:#E1E4E8">c</span><span style="color:#D73A49;--s-dark:#F97583">-</span><span style="color:#24292E;--s-dark:#E1E4E8">bg);</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    padding</span><span style="color:#D73A49;--s-dark:#F97583">-</span><span style="color:#24292E;--s-dark:#E1E4E8">top: .75rem;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    padding</span><span style="color:#D73A49;--s-dark:#F97583">-</span><span style="color:#24292E;--s-dark:#E1E4E8">bottom: .75rem;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    padding</span><span style="color:#D73A49;--s-dark:#F97583">-</span><span style="color:#24292E;--s-dark:#E1E4E8">left: 1rem;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    padding</span><span style="color:#D73A49;--s-dark:#F97583">-</span><span style="color:#24292E;--s-dark:#E1E4E8">right: 1rem;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    background</span><span style="color:#D73A49;--s-dark:#F97583">-</span><span style="color:#24292E;--s-dark:#E1E4E8">color: </span><span style="color:#6F42C1;--s-dark:#B392F0">var</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#D73A49;--s-dark:#F97583">--</span><span style="color:#24292E;--s-dark:#E1E4E8">memo</span><span style="color:#D73A49;--s-dark:#F97583">-</span><span style="color:#24292E;--s-dark:#E1E4E8">bg);</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    font</span><span style="color:#D73A49;--s-dark:#F97583">-</span><span style="color:#24292E;--s-dark:#E1E4E8">family: ui</span><span style="color:#D73A49;--s-dark:#F97583">-</span><span style="color:#24292E;--s-dark:#E1E4E8">sans</span><span style="color:#D73A49;--s-dark:#F97583">-</span><span style="color:#24292E;--s-dark:#E1E4E8">serif, system</span><span style="color:#D73A49;--s-dark:#F97583">-</span><span style="color:#24292E;--s-dark:#E1E4E8">ui, sans</span><span style="color:#D73A49;--s-dark:#F97583">-</span><span style="color:#24292E;--s-dark:#E1E4E8">serif, </span><span style="color:#032F62;--s-dark:#9ECBFF">"Apple Color Emoji"</span><span style="color:#24292E;--s-dark:#E1E4E8">, </span><span style="color:#032F62;--s-dark:#9ECBFF">"Segoe UI Emoji"</span><span style="color:#24292E;--s-dark:#E1E4E8">, Segoe </span><span style="color:#005CC5;--s-dark:#79B8FF">UI</span><span style="color:#24292E;--s-dark:#E1E4E8"> Symbol, </span><span style="color:#032F62;--s-dark:#9ECBFF">"Noto Color Emoji"</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    .header {</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        display: flex;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        justify</span><span style="color:#D73A49;--s-dark:#F97583">-</span><span style="color:#24292E;--s-dark:#E1E4E8">content: space</span><span style="color:#D73A49;--s-dark:#F97583">-</span><span style="color:#24292E;--s-dark:#E1E4E8">between;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        align</span><span style="color:#D73A49;--s-dark:#F97583">-</span><span style="color:#24292E;--s-dark:#E1E4E8">items: center;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        .time</span><span style="color:#D73A49;--s-dark:#F97583">-</span><span style="color:#24292E;--s-dark:#E1E4E8">text {</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            display: inline</span><span style="color:#D73A49;--s-dark:#F97583">-</span><span style="color:#24292E;--s-dark:#E1E4E8">block;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            font</span><span style="color:#D73A49;--s-dark:#F97583">-</span><span style="color:#24292E;--s-dark:#E1E4E8">size: .875rem;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            text</span><span style="color:#D73A49;--s-dark:#F97583">-</span><span style="color:#24292E;--s-dark:#E1E4E8">decoration: none;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            color: </span><span style="color:#6F42C1;--s-dark:#B392F0">var</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#D73A49;--s-dark:#F97583">--</span><span style="color:#24292E;--s-dark:#E1E4E8">memo</span><span style="color:#D73A49;--s-dark:#F97583">-</span><span style="color:#24292E;--s-dark:#E1E4E8">time)</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        }</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    }</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    .memo</span><span style="color:#D73A49;--s-dark:#F97583">-</span><span style="color:#24292E;--s-dark:#E1E4E8">content {</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        margin-top: 5px;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        font</span><span style="color:#D73A49;--s-dark:#F97583">-</span><span style="color:#24292E;--s-dark:#E1E4E8">size: 1rem;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        word</span><span style="color:#D73A49;--s-dark:#F97583">-</span><span style="color:#24292E;--s-dark:#E1E4E8">break: break</span><span style="color:#D73A49;--s-dark:#F97583">-</span><span style="color:#24292E;--s-dark:#E1E4E8">word;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        color: </span><span style="color:#6F42C1;--s-dark:#B392F0">var</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#D73A49;--s-dark:#F97583">--</span><span style="color:#24292E;--s-dark:#E1E4E8">memo</span><span style="color:#D73A49;--s-dark:#F97583">-</span><span style="color:#24292E;--s-dark:#E1E4E8">content);</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">        *</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            margin: </span><span style="color:#005CC5;--s-dark:#79B8FF">0</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        }</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">        *</span><span style="color:#24292E;--s-dark:#E1E4E8">:</span><span style="color:#6F42C1;--s-dark:#B392F0">not</span><span style="color:#24292E;--s-dark:#E1E4E8">(:first</span><span style="color:#D73A49;--s-dark:#F97583">-</span><span style="color:#24292E;--s-dark:#E1E4E8">child):</span><span style="color:#6F42C1;--s-dark:#B392F0">not</span><span style="color:#24292E;--s-dark:#E1E4E8">([hidden]) {</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            margin-top: .5rem;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        }</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        .img</span><span style="color:#D73A49;--s-dark:#F97583">-</span><span style="color:#24292E;--s-dark:#E1E4E8">container {</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            width: </span><span style="color:#005CC5;--s-dark:#79B8FF">40</span><span style="color:#D73A49;--s-dark:#F97583">%</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            .imgwrp {</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">                width:</span><span style="color:#005CC5;--s-dark:#79B8FF">100</span><span style="color:#D73A49;--s-dark:#F97583">%</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">                height: </span><span style="color:#005CC5;--s-dark:#79B8FF">100</span><span style="color:#D73A49;--s-dark:#F97583">%</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            }</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        }</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    </span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    </span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">.card:hover {</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    border</span><span style="color:#D73A49;--s-dark:#F97583">-</span><span style="color:#24292E;--s-dark:#E1E4E8">color: </span><span style="color:#6F42C1;--s-dark:#B392F0">var</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#D73A49;--s-dark:#F97583">--</span><span style="color:#24292E;--s-dark:#E1E4E8">memo</span><span style="color:#D73A49;--s-dark:#F97583">-</span><span style="color:#24292E;--s-dark:#E1E4E8">card</span><span style="color:#D73A49;--s-dark:#F97583">-</span><span style="color:#24292E;--s-dark:#E1E4E8">border);</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">.load-more {</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">  text</span><span style="color:#D73A49;--s-dark:#F97583">-</span><span style="color:#24292E;--s-dark:#E1E4E8">align: center;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">  margin</span><span style="color:#D73A49;--s-dark:#F97583">-</span><span style="color:#24292E;--s-dark:#E1E4E8">top: 40px;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">  margin</span><span style="color:#D73A49;--s-dark:#F97583">-</span><span style="color:#24292E;--s-dark:#E1E4E8">bottom: 40px;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">  .load</span><span style="color:#D73A49;--s-dark:#F97583">-</span><span style="color:#24292E;--s-dark:#E1E4E8">more</span><span style="color:#D73A49;--s-dark:#F97583">-</span><span style="color:#24292E;--s-dark:#E1E4E8">button {</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    display: inline</span><span style="color:#D73A49;--s-dark:#F97583">-</span><span style="color:#24292E;--s-dark:#E1E4E8">flex;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    align</span><span style="color:#D73A49;--s-dark:#F97583">-</span><span style="color:#24292E;--s-dark:#E1E4E8">items: center;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    justify</span><span style="color:#D73A49;--s-dark:#F97583">-</span><span style="color:#24292E;--s-dark:#E1E4E8">content: center;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    width: 120px; </span><span style="color:#6A737D;--s-dark:#6A737D">// 固定宽度</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    height: 40px; </span><span style="color:#6A737D;--s-dark:#6A737D">// 固定高度</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    background</span><span style="color:#D73A49;--s-dark:#F97583">-</span><span style="color:#24292E;--s-dark:#E1E4E8">color: transparent;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    color: </span><span style="color:#6F42C1;--s-dark:#B392F0">var</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#D73A49;--s-dark:#F97583">--</span><span style="color:#24292E;--s-dark:#E1E4E8">vp</span><span style="color:#D73A49;--s-dark:#F97583">-</span><span style="color:#24292E;--s-dark:#E1E4E8">c</span><span style="color:#D73A49;--s-dark:#F97583">-</span><span style="color:#24292E;--s-dark:#E1E4E8">text</span><span style="color:#D73A49;--s-dark:#F97583">-</span><span style="color:#005CC5;--s-dark:#79B8FF">2</span><span style="color:#24292E;--s-dark:#E1E4E8">);</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    border: 1px solid </span><span style="color:#6F42C1;--s-dark:#B392F0">var</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#D73A49;--s-dark:#F97583">--</span><span style="color:#24292E;--s-dark:#E1E4E8">vp</span><span style="color:#D73A49;--s-dark:#F97583">-</span><span style="color:#24292E;--s-dark:#E1E4E8">c</span><span style="color:#D73A49;--s-dark:#F97583">-</span><span style="color:#24292E;--s-dark:#E1E4E8">divider);</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    border</span><span style="color:#D73A49;--s-dark:#F97583">-</span><span style="color:#24292E;--s-dark:#E1E4E8">radius: 4px;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    font</span><span style="color:#D73A49;--s-dark:#F97583">-</span><span style="color:#24292E;--s-dark:#E1E4E8">size: 14px;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    font</span><span style="color:#D73A49;--s-dark:#F97583">-</span><span style="color:#24292E;--s-dark:#E1E4E8">weight: </span><span style="color:#005CC5;--s-dark:#79B8FF">500</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    cursor: pointer;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    transition: all 0.2s ease;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    outline: none;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">    &#x26;</span><span style="color:#24292E;--s-dark:#E1E4E8">:hover:</span><span style="color:#6F42C1;--s-dark:#B392F0">not</span><span style="color:#24292E;--s-dark:#E1E4E8">(:disabled) {</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">      background-color: </span><span style="color:#6F42C1;--s-dark:#B392F0">var</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#D73A49;--s-dark:#F97583">--</span><span style="color:#24292E;--s-dark:#E1E4E8">vp</span><span style="color:#D73A49;--s-dark:#F97583">-</span><span style="color:#24292E;--s-dark:#E1E4E8">c</span><span style="color:#D73A49;--s-dark:#F97583">-</span><span style="color:#24292E;--s-dark:#E1E4E8">bg</span><span style="color:#D73A49;--s-dark:#F97583">-</span><span style="color:#24292E;--s-dark:#E1E4E8">soft);</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">      color: </span><span style="color:#6F42C1;--s-dark:#B392F0">var</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#D73A49;--s-dark:#F97583">--</span><span style="color:#24292E;--s-dark:#E1E4E8">vp</span><span style="color:#D73A49;--s-dark:#F97583">-</span><span style="color:#24292E;--s-dark:#E1E4E8">c</span><span style="color:#D73A49;--s-dark:#F97583">-</span><span style="color:#24292E;--s-dark:#E1E4E8">text</span><span style="color:#D73A49;--s-dark:#F97583">-</span><span style="color:#005CC5;--s-dark:#79B8FF">1</span><span style="color:#24292E;--s-dark:#E1E4E8">);</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">      border</span><span style="color:#D73A49;--s-dark:#F97583">-</span><span style="color:#24292E;--s-dark:#E1E4E8">color: </span><span style="color:#6F42C1;--s-dark:#B392F0">var</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#D73A49;--s-dark:#F97583">--</span><span style="color:#24292E;--s-dark:#E1E4E8">vp</span><span style="color:#D73A49;--s-dark:#F97583">-</span><span style="color:#24292E;--s-dark:#E1E4E8">c</span><span style="color:#D73A49;--s-dark:#F97583">-</span><span style="color:#24292E;--s-dark:#E1E4E8">text</span><span style="color:#D73A49;--s-dark:#F97583">-</span><span style="color:#005CC5;--s-dark:#79B8FF">2</span><span style="color:#24292E;--s-dark:#E1E4E8">);</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    }</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">    &#x26;</span><span style="color:#24292E;--s-dark:#E1E4E8">:active:</span><span style="color:#6F42C1;--s-dark:#B392F0">not</span><span style="color:#24292E;--s-dark:#E1E4E8">(:disabled) {</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">      transform: </span><span style="color:#6F42C1;--s-dark:#B392F0">translateY</span><span style="color:#24292E;--s-dark:#E1E4E8">(1px);</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    }</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">    &#x26;</span><span style="color:#24292E;--s-dark:#E1E4E8">:disabled {</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">      opacity: </span><span style="color:#005CC5;--s-dark:#79B8FF">0.5</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">      cursor: not</span><span style="color:#D73A49;--s-dark:#F97583">-</span><span style="color:#24292E;--s-dark:#E1E4E8">allowed;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    }</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    .loading</span><span style="color:#D73A49;--s-dark:#F97583">-</span><span style="color:#24292E;--s-dark:#E1E4E8">spinner {</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">      width: 14px;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">      height: 14px;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">      border: 2px solid </span><span style="color:#6F42C1;--s-dark:#B392F0">var</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#D73A49;--s-dark:#F97583">--</span><span style="color:#24292E;--s-dark:#E1E4E8">vp</span><span style="color:#D73A49;--s-dark:#F97583">-</span><span style="color:#24292E;--s-dark:#E1E4E8">c</span><span style="color:#D73A49;--s-dark:#F97583">-</span><span style="color:#24292E;--s-dark:#E1E4E8">text</span><span style="color:#D73A49;--s-dark:#F97583">-</span><span style="color:#005CC5;--s-dark:#79B8FF">3</span><span style="color:#24292E;--s-dark:#E1E4E8">);</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">      border</span><span style="color:#D73A49;--s-dark:#F97583">-</span><span style="color:#24292E;--s-dark:#E1E4E8">radius: </span><span style="color:#005CC5;--s-dark:#79B8FF">50</span><span style="color:#D73A49;--s-dark:#F97583">%</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">      border</span><span style="color:#D73A49;--s-dark:#F97583">-</span><span style="color:#24292E;--s-dark:#E1E4E8">top</span><span style="color:#D73A49;--s-dark:#F97583">-</span><span style="color:#24292E;--s-dark:#E1E4E8">color: </span><span style="color:#6F42C1;--s-dark:#B392F0">var</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#D73A49;--s-dark:#F97583">--</span><span style="color:#24292E;--s-dark:#E1E4E8">vp</span><span style="color:#D73A49;--s-dark:#F97583">-</span><span style="color:#24292E;--s-dark:#E1E4E8">c</span><span style="color:#D73A49;--s-dark:#F97583">-</span><span style="color:#24292E;--s-dark:#E1E4E8">text</span><span style="color:#D73A49;--s-dark:#F97583">-</span><span style="color:#005CC5;--s-dark:#79B8FF">1</span><span style="color:#24292E;--s-dark:#E1E4E8">);</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">      animation: spin 0.8s linear infinite;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">  }</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">@keyframes spin {</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">  to { transform: </span><span style="color:#6F42C1;--s-dark:#B392F0">rotate</span><span style="color:#24292E;--s-dark:#E1E4E8">(360deg); }</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">}</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">&#x3C;/</span><span style="color:#22863A;--s-dark:#85E89D">style</span><span style="color:#24292E;--s-dark:#E1E4E8">></span></span></code><label class="code-collapse-collapse" for="toggle-tt6jqo5"><svg class="code-collapse-icon" width="32" height="24" viewBox="0 0 32 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20,15 12,7 4,15"></polyline></svg></label></pre></div></div></div>
<p>注意将 <code>{你的域名}</code> 替换为 CloudFlare Worker 的域名</p>
<p>眼尖的同学可能注意到了，这个组件初始化加载的内容不是通过请求 Worker 接口获取到的，而是从一个 json 文件获取的（<code>import memosRaw from '../../../../memos.json'</code>）。只有点击加载更多，才会通过 Worker 接口获取更多内容。这是为什么呢？</p>
<ul>
<li>从体验上来说，进入说说页面时，如果初始数据从接口获取，那么这时页面在获取到数据之前会空白一会儿，体验不佳</li>
<li>从<mark>省钱</mark>的角度上来说，CloudFlare Worker 免费版是限制请求次数的，初始化数据静态获取可以极大地降低请求次数</li>
</ul>
<p>这个 <code>memos.json</code>，则是在项目编译时，从接口获取到的前十条说说。这也就是为什么，Worker 代码中会添加一个 <code>CALLBACK_URL</code>，这个 URL 是在你发布新说说，或者删改前十条说说时重新触发编译使用的，具体 URL 可以根据你的部署平台自行搜索。如果完全动态获取说说内容的话，这里可以不用设置这么一个回调</p>
<p>下面的代码用于在编译时生成 <code>memos.json</code>，在主题配置文件（通常为 <code>docs/.vitepress/theme/index.ts</code>，文件路径和拓展名也许会有区别）的同级目录下，新建一个 <code>utils</code> 文件夹（已有则无需新建），在其中新建 <code>memos.js</code></p>
<div class="code-collapse-wrapper"><input type="checkbox" id="toggle-9qo4owv" class="code-collapse-toggle" style="display: none;"><div class="code-collapse-preview"><div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0" class="code-preview"><code><span class="line"><span style="color:#D73A49;--s-dark:#F97583">import</span><span style="color:#24292E;--s-dark:#E1E4E8"> https </span><span style="color:#D73A49;--s-dark:#F97583">from</span><span style="color:#032F62;--s-dark:#9ECBFF"> 'https'</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">import</span><span style="color:#24292E;--s-dark:#E1E4E8"> { promises </span><span style="color:#D73A49;--s-dark:#F97583">as</span><span style="color:#24292E;--s-dark:#E1E4E8"> fs } </span><span style="color:#D73A49;--s-dark:#F97583">from</span><span style="color:#032F62;--s-dark:#9ECBFF"> 'fs'</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">const</span><span style="color:#005CC5;--s-dark:#79B8FF"> url</span><span style="color:#D73A49;--s-dark:#F97583"> =</span><span style="color:#032F62;--s-dark:#9ECBFF"> 'https://{你的域名}/api/memos?limit=10'</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span><span style="color:#6A737D;--s-dark:#6A737D">// [!code highlight]</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">const</span><span style="color:#005CC5;--s-dark:#79B8FF"> requestOptions</span><span style="color:#D73A49;--s-dark:#F97583"> =</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    headers: {</span></span>
<span class="line"><span style="color:#032F62;--s-dark:#9ECBFF">      'Accept-Encoding'</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">''</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">};</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">// 发出 GET 请求</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">https.</span><span style="color:#6F42C1;--s-dark:#B392F0">get</span><span style="color:#24292E;--s-dark:#E1E4E8">(url, requestOptions, (</span><span style="color:#E36209;--s-dark:#FFAB70">resp</span><span style="color:#24292E;--s-dark:#E1E4E8">) </span><span style="color:#D73A49;--s-dark:#F97583">=></span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">  let</span><span style="color:#24292E;--s-dark:#E1E4E8"> data </span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#24292E;--s-dark:#E1E4E8"> [];</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">  // 逐步接收数据</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">  resp.</span><span style="color:#6F42C1;--s-dark:#B392F0">on</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#032F62;--s-dark:#9ECBFF">'data'</span><span style="color:#24292E;--s-dark:#E1E4E8">, (</span><span style="color:#E36209;--s-dark:#FFAB70">chunk</span><span style="color:#24292E;--s-dark:#E1E4E8">) </span><span style="color:#D73A49;--s-dark:#F97583">=></span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    data.</span><span style="color:#6F42C1;--s-dark:#B392F0">push</span><span style="color:#24292E;--s-dark:#E1E4E8">(chunk);</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">  });</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">  // 完成接收数据</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">  resp.</span><span style="color:#6F42C1;--s-dark:#B392F0">on</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#032F62;--s-dark:#9ECBFF">'end'</span><span style="color:#24292E;--s-dark:#E1E4E8">, </span><span style="color:#D73A49;--s-dark:#F97583">async</span><span style="color:#24292E;--s-dark:#E1E4E8"> () </span><span style="color:#D73A49;--s-dark:#F97583">=></span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">    try</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">      // 将 Buffer 数组合并为一个 Buffer</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">      const</span><span style="color:#005CC5;--s-dark:#79B8FF"> buffer</span><span style="color:#D73A49;--s-dark:#F97583"> =</span><span style="color:#24292E;--s-dark:#E1E4E8"> Buffer.</span><span style="color:#6F42C1;--s-dark:#B392F0">concat</span><span style="color:#24292E;--s-dark:#E1E4E8">(data);</span></span>
</code></pre></div><div class="code-collapse-gradient"></div><label class="code-collapse-expand" for="toggle-9qo4owv"><svg class="code-collapse-icon" width="32" height="24" viewBox="0 0 32 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="4,9 12,17 20,9"></polyline></svg></label></div><div class="code-collapse-full"><div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--s-dark:#F97583">import</span><span style="color:#24292E;--s-dark:#E1E4E8"> https </span><span style="color:#D73A49;--s-dark:#F97583">from</span><span style="color:#032F62;--s-dark:#9ECBFF"> 'https'</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">import</span><span style="color:#24292E;--s-dark:#E1E4E8"> { promises </span><span style="color:#D73A49;--s-dark:#F97583">as</span><span style="color:#24292E;--s-dark:#E1E4E8"> fs } </span><span style="color:#D73A49;--s-dark:#F97583">from</span><span style="color:#032F62;--s-dark:#9ECBFF"> 'fs'</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">const</span><span style="color:#005CC5;--s-dark:#79B8FF"> url</span><span style="color:#D73A49;--s-dark:#F97583"> =</span><span style="color:#032F62;--s-dark:#9ECBFF"> 'https://{你的域名}/api/memos?limit=10'</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span><span style="color:#6A737D;--s-dark:#6A737D">// [!code highlight]</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">const</span><span style="color:#005CC5;--s-dark:#79B8FF"> requestOptions</span><span style="color:#D73A49;--s-dark:#F97583"> =</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    headers: {</span></span>
<span class="line"><span style="color:#032F62;--s-dark:#9ECBFF">      'Accept-Encoding'</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">''</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">};</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">// 发出 GET 请求</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">https.</span><span style="color:#6F42C1;--s-dark:#B392F0">get</span><span style="color:#24292E;--s-dark:#E1E4E8">(url, requestOptions, (</span><span style="color:#E36209;--s-dark:#FFAB70">resp</span><span style="color:#24292E;--s-dark:#E1E4E8">) </span><span style="color:#D73A49;--s-dark:#F97583">=></span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">  let</span><span style="color:#24292E;--s-dark:#E1E4E8"> data </span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#24292E;--s-dark:#E1E4E8"> [];</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">  // 逐步接收数据</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">  resp.</span><span style="color:#6F42C1;--s-dark:#B392F0">on</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#032F62;--s-dark:#9ECBFF">'data'</span><span style="color:#24292E;--s-dark:#E1E4E8">, (</span><span style="color:#E36209;--s-dark:#FFAB70">chunk</span><span style="color:#24292E;--s-dark:#E1E4E8">) </span><span style="color:#D73A49;--s-dark:#F97583">=></span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    data.</span><span style="color:#6F42C1;--s-dark:#B392F0">push</span><span style="color:#24292E;--s-dark:#E1E4E8">(chunk);</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">  });</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">  // 完成接收数据</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">  resp.</span><span style="color:#6F42C1;--s-dark:#B392F0">on</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#032F62;--s-dark:#9ECBFF">'end'</span><span style="color:#24292E;--s-dark:#E1E4E8">, </span><span style="color:#D73A49;--s-dark:#F97583">async</span><span style="color:#24292E;--s-dark:#E1E4E8"> () </span><span style="color:#D73A49;--s-dark:#F97583">=></span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">    try</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">      // 将 Buffer 数组合并为一个 Buffer</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">      const</span><span style="color:#005CC5;--s-dark:#79B8FF"> buffer</span><span style="color:#D73A49;--s-dark:#F97583"> =</span><span style="color:#24292E;--s-dark:#E1E4E8"> Buffer.</span><span style="color:#6F42C1;--s-dark:#B392F0">concat</span><span style="color:#24292E;--s-dark:#E1E4E8">(data);</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">      const</span><span style="color:#005CC5;--s-dark:#79B8FF"> decodedData</span><span style="color:#D73A49;--s-dark:#F97583"> =</span><span style="color:#24292E;--s-dark:#E1E4E8"> buffer.</span><span style="color:#6F42C1;--s-dark:#B392F0">toString</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#032F62;--s-dark:#9ECBFF">'utf-8'</span><span style="color:#24292E;--s-dark:#E1E4E8">); </span><span style="color:#6A737D;--s-dark:#6A737D">// 假设返回的数据是 UTF-8 编码</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">      // 保存 JSON 数据到文件</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">      await</span><span style="color:#24292E;--s-dark:#E1E4E8"> fs.</span><span style="color:#6F42C1;--s-dark:#B392F0">writeFile</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#032F62;--s-dark:#9ECBFF">'memos.json'</span><span style="color:#24292E;--s-dark:#E1E4E8">, decodedData);</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">      console.</span><span style="color:#6F42C1;--s-dark:#B392F0">log</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#032F62;--s-dark:#9ECBFF">'JSON 数据已保存到 data.json'</span><span style="color:#24292E;--s-dark:#E1E4E8">);</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    } </span><span style="color:#D73A49;--s-dark:#F97583">catch</span><span style="color:#24292E;--s-dark:#E1E4E8"> (e) {</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">      console.</span><span style="color:#6F42C1;--s-dark:#B392F0">error</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#032F62;--s-dark:#9ECBFF">'解析 JSON 时出错:'</span><span style="color:#24292E;--s-dark:#E1E4E8">, e);</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">  });</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">}).</span><span style="color:#6F42C1;--s-dark:#B392F0">on</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#032F62;--s-dark:#9ECBFF">'error'</span><span style="color:#24292E;--s-dark:#E1E4E8">, (</span><span style="color:#E36209;--s-dark:#FFAB70">err</span><span style="color:#24292E;--s-dark:#E1E4E8">) </span><span style="color:#D73A49;--s-dark:#F97583">=></span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">  console.</span><span style="color:#6F42C1;--s-dark:#B392F0">error</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#032F62;--s-dark:#9ECBFF">'获取数据时出错:'</span><span style="color:#24292E;--s-dark:#E1E4E8">, err);</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">});</span></span></code><label class="code-collapse-collapse" for="toggle-9qo4owv"><svg class="code-collapse-icon" width="32" height="24" viewBox="0 0 32 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20,15 12,7 4,15"></polyline></svg></label></pre></div></div></div>
<p>接着编辑博客根目录下的 <code>package.json</code>，在 dev 和 build 的命令前都添加 <code>node docs/.vitepress/theme/utils/memos.js</code>。这里的添加位置可能因人而异，以我为例：</p>
<div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">{</span></span>
<span class="line"><span style="color:#B31D28;--s-light-font-style:italic;--s-dark:#FDAEB7;--s-dark-font-style:italic">  ...</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">  "scripts"</span><span style="color:#24292E;--s-dark:#E1E4E8">: {</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">    "dev"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">"node docs/.vitepress/theme/utils/memos.js &#x26;&#x26; vitepress dev docs"</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">    "build"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">"node docs/.vitepress/theme/utils/memos.js &#x26;&#x26; vitepress build docs"</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">    "serve"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">"vitepress serve docs"</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">  },</span></span>
<span class="line"><span style="color:#B31D28;--s-light-font-style:italic;--s-dark:#FDAEB7;--s-dark-font-style:italic">  ...</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">}</span></span></code></pre></div>
<p>这样在 dev 阶段和 build 阶段都会首先调用 <code>memos.js</code>，在博客根目录下生成 <code>memos.json</code>。注意根据目录层级调整 <code>memos.vue</code> 中 import 的路径</p>
<p>这样组件和数据都准备好了，下面这个组件注册为全局组件</p>
<p>在主题配置文件（通常为 <code>docs/.vitepress/theme/index.ts</code>，文件路径和拓展名也许会有区别）中引入这个组件，并注册</p>
<div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--s-dark:#F97583">...</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">import</span><span style="color:#24292E;--s-dark:#E1E4E8"> Memos </span><span style="color:#D73A49;--s-dark:#F97583">from</span><span style="color:#032F62;--s-dark:#9ECBFF"> './components/memos.vue'</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">...</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">export</span><span style="color:#D73A49;--s-dark:#F97583"> default</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">    ...</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">    enhanceApp</span><span style="color:#24292E;--s-dark:#E1E4E8">({ app }) {</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">        ...</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        app.</span><span style="color:#6F42C1;--s-dark:#B392F0">component</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#032F62;--s-dark:#9ECBFF">'Memos'</span><span style="color:#24292E;--s-dark:#E1E4E8">, Memos);</span><span style="color:#6A737D;--s-dark:#6A737D">// [!code highlight]</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">} </span><span style="color:#D73A49;--s-dark:#F97583">satisfies</span><span style="color:#6F42C1;--s-dark:#B392F0"> Theme</span></span></code></pre></div>
<p>这样在博客的任何地方，都可以通过 <code>&#x3C;Memos /></code> 直接引入这个组件了</p>
<p>最后就是创建一个单页，专门用于放置这个组件</p>
<blockquote>
<p>什么？你说你从来没有在 vitepress 中使用过单页？</p>
<p>这样，你先在根目录下新建一个 pages 文件夹，再在 VitePress 核心配置文件中（注意不是主题配置文件，通常为 docs/.vitepress/config.ts，文件路径和拓展名也许会有区别）中新增一个 rewrites 规则 <code>'pages/:file.md': ':file.md'</code>，这样 pages 下的内容都可以直接通过 <code>/文件名</code> 访问了。关于 rewrites，见 <a href="https://vitepress.dev/guide/routing#route-rewrites" target="_blank" rel="noopener noreferrer" data-umami-event="outbound-link-click" data-umami-event-url="https://vitepress.dev/guide/routing#route-rewrites">官方文档</a></p>
</blockquote>
<p>pages 文件夹下新建 balabala.md，内容为</p>
<div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">---</span></span>
<span class="line"><span style="color:#22863A;--s-dark:#85E89D">title</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">碎碎念</span></span>
<span class="line"><span style="color:#22863A;--s-dark:#85E89D">hidden</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">true</span></span>
<span class="line"><span style="color:#22863A;--s-dark:#85E89D">comment</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">false</span></span>
<span class="line"><span style="color:#22863A;--s-dark:#85E89D">sidebar</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">false</span></span>
<span class="line"><span style="color:#22863A;--s-dark:#85E89D">aside</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">false</span></span>
<span class="line"><span style="color:#22863A;--s-dark:#85E89D">readingTime</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">false</span></span>
<span class="line"><span style="color:#22863A;--s-dark:#85E89D">showMeta</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">false</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">---</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">&#x3C;</span><span style="color:#22863A;--s-dark:#85E89D">Memos</span><span style="color:#24292E;--s-dark:#E1E4E8"> /></span></span></code></pre></div>
<p>完事收工</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[实现 OPNsense 透明代理+分流]]></title>
            <link>https://blog.shinya.click/fiddling/opnsense-transparent-proxy</link>
            <guid isPermaLink="false">https://blog.shinya.click/fiddling/opnsense-transparent-proxy</guid>
            <pubDate>Thu, 16 Jan 2025 15:09:00 GMT</pubDate>
            <description><![CDATA[OPNsense 作为一款开源的防火墙和路由系统，因其美观的用户界面和全面的功能而备受关注。在经历了一系列路由方案后，用户们逐渐认识到其在透明代理和流量分流方面的强大潜力。通过结合 BGP 分流转发方式，OPNsense 提供了更高的安全性和稳定性，成为了理想的网络管理解决方案。尤其是其自动更新 IP 列表的功能，使得网络管理变得更加便捷。]]></description>
            <content:encoded><![CDATA[<h3 id="前言">前言<a href="#前言" class="heading-anchor-link" aria-label="Link to 前言"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h3>
<p>早先透明代理 + 分流一直使用的是 ikuai 作为主路由，OpenWRT 作为旁路由的方案，这也是网上大部分教程的主流方案。后面冲浪的时候了解到 ikuai 可能会有 <a href="https://wusiyu.me/2022-ikuai-non-cloud-background-activities/" target="_blank" rel="noopener noreferrer" data-umami-event="outbound-link-click" data-umami-event-url="https://wusiyu.me/2022-ikuai-non-cloud-background-activities/">偷跑流量与上报信息的情况</a> 出现，另外作为国产闭源系统，安全性也比较成问题</p>
<p>后续更换了主路由 OpenWRT，旁路由 Debian 的 <a href="/fiddling/debian-as-bypass-router">方案</a>，后面又不使用旁路由方案，先后尝试了 <a href="/fiddling/fake-ip-based-transparent-proxy">基于 FakeIP 的分流转发方案</a> 和 <a href="/fiddling/more-accurate-chnroute">基于 BGP 的分流转发方案</a>，最终稳定在了基于 BGP 的分流转发方案上</p>
<p>最近又了解到了 OPNsense 这个防火墙/路由系统。简直是梦中情路由系统，开源免费，UI 美观，功能完善，同时还提供了 gui 界面支持自动更新 ip list 以用于分流。所以计划将现有的分流转发方案迁移到该系统上，同时不在单独使用一个软路由用于科学，而是直接将 clash 集成进主路由中</p>
<p>网上冲浪了下，相关的教程并不多，有部分教程也由于年久失修，不再有效。踩了一些坑后，决定整理一下详细的方案</p>
<p>梳理了下需要实现的功能：</p>
<ul>
<li>DNS 转发到 clash 统一解析</li>
<li>请求流量进入 OPNsense 后，可以根据某个 list 进行分流，将部分流量导入 clash</li>
</ul>
<p>OPNsense 的基础安装过程就不再赘述，网络上相关的内容很多</p>
<h3 id="clash-安装">clash 安装<a href="#clash-安装" class="heading-anchor-link" aria-label="Link to clash 安装"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h3>
<h4 id="二进制下载和配置文件">二进制下载和配置文件<a href="#二进制下载和配置文件" class="heading-anchor-link" aria-label="Link to 二进制下载和配置文件"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h4>
<p>DNS 解析和流量处理都依赖了 clash 的功能，所以第一步先安装 clash</p>
<p>ssh 进 OPNsense（怎么开启 ssh？STFW）后，新建文件夹 <code>/usr/local/clash</code> 作为 clash 二进制、配置文件和其他相关文件的存放点。clash 二进制建议 scp 过去（毕竟主路由现在还没科学，直接下载速度很慢）。这里需要先把 clash 二进制和配置文件上传到该目录下。clash 二进制重命名为 clash，配置文件命名为 <code>config.yaml</code></p>
<p>在 mihomo 的 <a href="https://github.com/MetaCubeX/mihomo/releases" target="_blank" rel="noopener noreferrer" data-umami-event="outbound-link-click" data-umami-event-url="https://github.com/MetaCubeX/mihomo/releases">releases 页面</a> 下载最新的内核版本。注意下载 freebsd 版本，根据你的机器架构选择 386、amd64 或者 arm64。如果你是 amd64 且后续运行 clash 时阶段出现以下报错，请下载 <code>amd64-compatible</code> 版</p>
<div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">This</span><span style="color:#032F62;--s-dark:#9ECBFF"> PROGRAM</span><span style="color:#032F62;--s-dark:#9ECBFF"> can</span><span style="color:#032F62;--s-dark:#9ECBFF"> only</span><span style="color:#032F62;--s-dark:#9ECBFF"> be</span><span style="color:#032F62;--s-dark:#9ECBFF"> run</span><span style="color:#032F62;--s-dark:#9ECBFF"> on _AMD64</span><span style="color:#032F62;--s-dark:#9ECBFF"> processors</span><span style="color:#032F62;--s-dark:#9ECBFF"> with</span><span style="color:#032F62;--s-dark:#9ECBFF"> v3</span><span style="color:#032F62;--s-dark:#9ECBFF"> microarchitecture_ support.</span></span></code></pre></div>
<p>配置文件就不多说了，直接用一份你一直在用的就可以。但注意改动下面的配置</p>
<div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#22863A;--s-dark:#85E89D">mixed-port</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">7890</span></span>
<span class="line"></span>
<span class="line"><span style="color:#22863A;--s-dark:#85E89D">dns</span><span style="color:#24292E;--s-dark:#E1E4E8">:</span></span>
<span class="line"><span style="color:#22863A;--s-dark:#85E89D">  listen</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">127.0.0.1:5353</span></span>
<span class="line"></span>
<span class="line"><span style="color:#22863A;--s-dark:#85E89D">tun</span><span style="color:#24292E;--s-dark:#E1E4E8">:</span></span>
<span class="line"><span style="color:#22863A;--s-dark:#85E89D">  enable</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">false</span></span></code></pre></div>
<p>dns 监听 5353 端口，作为 OPNsense 自带的 DNS 上游。同时关闭 tun，不主动劫持流量，而是由 OPNsense 进行流量筛选后导入。这里 mixed-port 同时兼具 socks-port、http-port 和 https-port 的功能</p>
<p>运行 <code>pw user add clash -c "Clash" -s /usr/sbin/nologin</code> 创建一个无登录的 clash 用户，并通过 <code>chown clash:clash /usr/local/clash</code> 赋予文件夹权限。完成后可以通过 <code>/usr/local/clash/clash -d /usr/local/clash</code> 执行一次，观察下是否可以成功运行</p>
<h4 id="注册-clash-服务">注册 clash 服务<a href="#注册-clash-服务" class="heading-anchor-link" aria-label="Link to 注册 clash 服务"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h4>
<p>新建文件 <code>/usr/local/etc/rc.d/clash</code> 和 <code>/usr/local/opnsense/service/conf/actions.d/actions_clash.conf</code> 将 clash 注册成一个系统服务</p>
<div class="code-collapse-wrapper"><input type="checkbox" id="toggle-v56g1sg" class="code-collapse-toggle" style="display: none;"><div class="code-collapse-preview"><div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0" class="code-preview"><code><span class="line"><span style="color:#6A737D;--s-dark:#6A737D">#!/bin/sh</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D"># $FreeBSD$</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D"># PROVIDE: clash</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D"># REQUIRE: LOGIN cleanvar</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D"># KEYWORD: shutdown</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D"># Add the following lines to /etc/rc.conf to enable clash:</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D"># clash_enable (bool): Set to "NO" by default.</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D"># Set to "YES" to enable clash.</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D"># clash_config (path): Clash config dir.</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D"># Defaults to "/usr/local/etc/clash"</span></span>
<span class="line"></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">.</span><span style="color:#032F62;--s-dark:#9ECBFF"> /etc/rc.subr</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">name</span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#032F62;--s-dark:#9ECBFF">"clash"</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">rcvar</span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#032F62;--s-dark:#9ECBFF">clash_enable</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">load_rc_config</span><span style="color:#24292E;--s-dark:#E1E4E8"> $name</span></span>
<span class="line"></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">:</span><span style="color:#24292E;--s-dark:#E1E4E8"> ${clash_enable</span><span style="color:#D73A49;--s-dark:#F97583">:=</span><span style="color:#032F62;--s-dark:#9ECBFF">"NO"</span><span style="color:#24292E;--s-dark:#E1E4E8">}</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">:</span><span style="color:#24292E;--s-dark:#E1E4E8"> ${clash_config=</span><span style="color:#032F62;--s-dark:#9ECBFF">"/usr/local/clash"</span><span style="color:#24292E;--s-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">command</span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#032F62;--s-dark:#9ECBFF">"/usr/local/clash/clash"</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">#pidfile="/var/run/clash.pid"</span></span>
</code></pre></div><div class="code-collapse-gradient"></div><label class="code-collapse-expand" for="toggle-v56g1sg"><svg class="code-collapse-icon" width="32" height="24" viewBox="0 0 32 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="4,9 12,17 20,9"></polyline></svg></label></div><div class="code-collapse-full"><div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#6A737D;--s-dark:#6A737D">#!/bin/sh</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D"># $FreeBSD$</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D"># PROVIDE: clash</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D"># REQUIRE: LOGIN cleanvar</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D"># KEYWORD: shutdown</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D"># Add the following lines to /etc/rc.conf to enable clash:</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D"># clash_enable (bool): Set to "NO" by default.</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D"># Set to "YES" to enable clash.</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D"># clash_config (path): Clash config dir.</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D"># Defaults to "/usr/local/etc/clash"</span></span>
<span class="line"></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">.</span><span style="color:#032F62;--s-dark:#9ECBFF"> /etc/rc.subr</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">name</span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#032F62;--s-dark:#9ECBFF">"clash"</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">rcvar</span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#032F62;--s-dark:#9ECBFF">clash_enable</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">load_rc_config</span><span style="color:#24292E;--s-dark:#E1E4E8"> $name</span></span>
<span class="line"></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">:</span><span style="color:#24292E;--s-dark:#E1E4E8"> ${clash_enable</span><span style="color:#D73A49;--s-dark:#F97583">:=</span><span style="color:#032F62;--s-dark:#9ECBFF">"NO"</span><span style="color:#24292E;--s-dark:#E1E4E8">}</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">:</span><span style="color:#24292E;--s-dark:#E1E4E8"> ${clash_config=</span><span style="color:#032F62;--s-dark:#9ECBFF">"/usr/local/clash"</span><span style="color:#24292E;--s-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">command</span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#032F62;--s-dark:#9ECBFF">"/usr/local/clash/clash"</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">#pidfile="/var/run/clash.pid"</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">required_files</span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#032F62;--s-dark:#9ECBFF">"${</span><span style="color:#24292E;--s-dark:#E1E4E8">clash_config</span><span style="color:#032F62;--s-dark:#9ECBFF">}"</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">clash_group</span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#032F62;--s-dark:#9ECBFF">"clash"</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">clash_user</span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#032F62;--s-dark:#9ECBFF">"clash"</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">command_args</span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#032F62;--s-dark:#9ECBFF">"-d </span><span style="color:#24292E;--s-dark:#E1E4E8">$clash_config</span><span style="color:#032F62;--s-dark:#9ECBFF">"</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">run_rc_command</span><span style="color:#032F62;--s-dark:#9ECBFF"> "</span><span style="color:#005CC5;--s-dark:#79B8FF">$1</span><span style="color:#032F62;--s-dark:#9ECBFF">"</span></span></code><label class="code-collapse-collapse" for="toggle-v56g1sg"><svg class="code-collapse-icon" width="32" height="24" viewBox="0 0 32 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20,15 12,7 4,15"></polyline></svg></label></pre></div></div></div>
<div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre><code>[start]
command:/usr/local/etc/rc.d/clash onestart
type:script
message:starting clash

[stop]
command:/usr/local/etc/rc.d/clash stop
type:script
message:stoping clash

[status]
command:/usr/local/etc/rc.d/clash statusexit 0
type:script_output
message:get clash status

[restart]
command:/usr/local/etc/rc.d/clash onerestart
type:script
message:restarting clash
</code></pre></div>
<p>赋予运行权限 <code>chmod +x /usr/local/etc/rc.d/clash</code> 后启用 <code>service configd restart</code></p>
<h4 id="clash-开机自启">clash 开机自启<a href="#clash-开机自启" class="heading-anchor-link" aria-label="Link to clash 开机自启"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h4>
<p>接下来设置 clash 开机自启就可以了，但这里有个坑：</p>
<blockquote>
<p>clash 作为系统服务启动后，并没有完成启动后就保持后台运行的功能，这样每次系统重启后会启动到 clash 之后就不会往后走，因该 clash 一直会保持在前台，导致排在 clash 后面的待启动服务就没法启动了</p>
</blockquote>
<p>有一个曲折的办法就是通过 OPNsense 自带的一个服务监控功能 Monit 来拉起和监控 clash 的状态。Monit 功能可在 <code>服务-Monit</code> 中开启</p>
<p>在 Service Test Settings 中添加两个 Service Test，第一个负责拉起 clash</p>
<p>| Setting   | Value                                    |
| --------- | ---------------------------------------- |
| Name      | Clash                                    |
| Condition | failed host 127.0.0.1 port 7890 type tcp |
| Action    | Restart                                  |</p>
<p>第二个避免重启死循环</p>
<p>| Setting   | Value                      |
| --------- | -------------------------- |
| Name      | RestartLimit4              |
| Condition | 5 restarts within 5 cycles |
| Action    | Unmonitor                  |</p>
<p>最后在 Service Settings 里添加</p>
<p>| Setting | Value                                 |
| ------- | ------------------------------------- |
| Name    | Clash                                 |
| Match   | clash                                 |
| Start   | /usr/local/sbin/configctl clash start |
| Stop    | /usr/local/sbin/configctl clash stop  |
| Tests   | Clash,RestartLimit4                   |</p>
<p>保存后等待一段时间，在 Monit - Status 里查看 clash 是否正常运行</p>
<h3 id="dns-解析">DNS 解析<a href="#dns-解析" class="heading-anchor-link" aria-label="Link to DNS 解析"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h3>
<p>使用 OPNsense 自带的 Unbound DNS 设置上游 DNS 为 clash 的 127.0.0.1<div></div>，一直解析出错，非常奇怪</p>
<p>百思不得其解，最终决定关闭 Unbound DNS，用回了 AdGuard Home 作为默认 DNS，劫持 53 端口</p>
<p>AdGuard Home 没有被包含在 OPNsense 的默认插件源中，需要手动添加社区源</p>
<p>ssh 进 OPNsense 后执行命令</p>
<div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">$</span><span style="color:#032F62;--s-dark:#9ECBFF"> fetch</span><span style="color:#005CC5;--s-dark:#79B8FF"> -o</span><span style="color:#032F62;--s-dark:#9ECBFF"> /usr/local/etc/pkg/repos/mimugmail.conf</span><span style="color:#032F62;--s-dark:#9ECBFF"> https://www.routerperformance.net/mimugmail.conf</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">$</span><span style="color:#032F62;--s-dark:#9ECBFF"> pkg</span><span style="color:#032F62;--s-dark:#9ECBFF"> update</span></span></code></pre></div>
<p>接着在 web-gui 中的系统 - 固件 - 插件中搜索 adguard，安装 os-adguardhome-maxit 即可。安装完成后即可在服务-Adguardhome 中开启 Adguard Home。web 管理开放在 3000 端口，初始化设置过程不表，注意 DNS 监听端口设置为 53，即以 Adguard Home 作为 OPNsense 所在机器的默认 DNS server</p>
<p>安装完成后在 Adguard Home 的设置-DNS 设置中将上游 DNS 服务器设置为 127.0.0.1<div></div>，即 Clash 的 DNS 监听地址即可</p>
<h3 id="国内外-ip-分流">国内外 IP 分流<a href="#国内外-ip-分流" class="heading-anchor-link" aria-label="Link to 国内外 IP 分流"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h3>
<h4 id="二进制下载和配置文件-1">二进制下载和配置文件<a href="#二进制下载和配置文件-1" class="heading-anchor-link" aria-label="Link to 二进制下载和配置文件"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h4>
<p>OPNsense 默认是内置了一个 Squid 代理程序用于提供流量代理，但是该程序只能用于代理 http/https 流量，无法代理常规的 tcp 和 udp 流量，作为一个代理是很不完美的。于是采用了曲线救国的方案，通过 tun2socks 项目将 tcp/udp 流量导入 clash 完成代理</p>
<p>新建文件夹 <code>/usr/local/tun2socks</code> 作为 tun2socks 的二进制和配置文件的安放处。在 <a href="https://github.com/xjasonlyu/tun2socks/releases" target="_blank" rel="noopener noreferrer" data-umami-event="outbound-link-click" data-umami-event-url="https://github.com/xjasonlyu/tun2socks/releases">Github Releases</a> 中下载最新的 freebsd 二进制到文件夹下，并将其重命名为 tun2socks。新建配置文件 <code>/usr/local/tun2socks/config.yaml</code></p>
<div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#6A737D;--s-dark:#6A737D"># debug / info / warning / error / silent</span></span>
<span class="line"><span style="color:#22863A;--s-dark:#85E89D">loglevel</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">info</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D"># URL format: [protocol://]host[:port]</span></span>
<span class="line"><span style="color:#22863A;--s-dark:#85E89D">proxy</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">socks5://127.0.0.1:7890</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D"># URL format: [driver://]name</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D"># TUN 设备名称，避免使用 tun0</span></span>
<span class="line"><span style="color:#22863A;--s-dark:#85E89D">device</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">tun://proxytun2socks0</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D"># Maximum transmission unit for each packet</span></span>
<span class="line"><span style="color:#22863A;--s-dark:#85E89D">mtu</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">1500</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D"># Timeout for each UDP session, default value: 60 seconds</span></span>
<span class="line"><span style="color:#22863A;--s-dark:#85E89D">udp-timeout</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">120s</span></span></code></pre></div>
<p><code>proxy</code> 处填写到 clash 的 socks5 端口的地址</p>
<p>可在文件夹 <code>/usr/local/tun2socks/</code> 内运行 <code>./tun2socks -config ./config.yaml</code>，测试配置文件是否正确</p>
<h4 id="注册服务">注册服务<a href="#注册服务" class="heading-anchor-link" aria-label="Link to 注册服务"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h4>
<p>新建文件 <code>/usr/local/etc/rc.d/tun2socks</code> 和 <code>/usr/local/opnsense/service/conf/actions.d/actions_tun2socks.conf</code></p>
<div class="code-collapse-wrapper"><input type="checkbox" id="toggle-dszlftb" class="code-collapse-toggle" style="display: none;"><div class="code-collapse-preview"><div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0" class="code-preview"><code><span class="line"><span style="color:#6A737D;--s-dark:#6A737D">#!/bin/sh</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D"># PROVIDE: tun2socks</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D"># REQUIRE: LOGIN</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D"># KEYWORD: shutdown</span></span>
<span class="line"></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">.</span><span style="color:#032F62;--s-dark:#9ECBFF"> /etc/rc.subr</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">name</span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#032F62;--s-dark:#9ECBFF">"tun2socks"</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">rcvar</span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#032F62;--s-dark:#9ECBFF">"tun2socks_enable"</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">load_rc_config</span><span style="color:#24292E;--s-dark:#E1E4E8"> $name</span></span>
<span class="line"></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">:</span><span style="color:#24292E;--s-dark:#E1E4E8"> ${tun2socks_enable</span><span style="color:#D73A49;--s-dark:#F97583">:=</span><span style="color:#24292E;--s-dark:#E1E4E8">no}</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">:</span><span style="color:#24292E;--s-dark:#E1E4E8"> ${tun2socks_config</span><span style="color:#D73A49;--s-dark:#F97583">:=</span><span style="color:#032F62;--s-dark:#9ECBFF">"/usr/local/tun2socks/config.yaml"</span><span style="color:#24292E;--s-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">pidfile</span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#032F62;--s-dark:#9ECBFF">"/var/run/${</span><span style="color:#24292E;--s-dark:#E1E4E8">name</span><span style="color:#032F62;--s-dark:#9ECBFF">}.pid"</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">command</span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#032F62;--s-dark:#9ECBFF">"/usr/local/tun2socks/tun2socks"</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">command_args</span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#032F62;--s-dark:#9ECBFF">"-config ${</span><span style="color:#24292E;--s-dark:#E1E4E8">tun2socks_config</span><span style="color:#032F62;--s-dark:#9ECBFF">} > /dev/null 2>&#x26;1 &#x26; echo </span><span style="color:#005CC5;--s-dark:#79B8FF">\$</span><span style="color:#032F62;--s-dark:#9ECBFF">! > ${</span><span style="color:#24292E;--s-dark:#E1E4E8">pidfile</span><span style="color:#032F62;--s-dark:#9ECBFF">}"</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">start_cmd</span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#032F62;--s-dark:#9ECBFF">"${</span><span style="color:#24292E;--s-dark:#E1E4E8">name</span><span style="color:#032F62;--s-dark:#9ECBFF">}_start"</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">tun2socks_start</span><span style="color:#24292E;--s-dark:#E1E4E8">()</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">{</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">    if</span><span style="color:#24292E;--s-dark:#E1E4E8"> [ </span><span style="color:#D73A49;--s-dark:#F97583">!</span><span style="color:#D73A49;--s-dark:#F97583"> -f</span><span style="color:#24292E;--s-dark:#E1E4E8"> ${tun2socks_config} ]; </span><span style="color:#D73A49;--s-dark:#F97583">then</span></span>
</code></pre></div><div class="code-collapse-gradient"></div><label class="code-collapse-expand" for="toggle-dszlftb"><svg class="code-collapse-icon" width="32" height="24" viewBox="0 0 32 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="4,9 12,17 20,9"></polyline></svg></label></div><div class="code-collapse-full"><div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#6A737D;--s-dark:#6A737D">#!/bin/sh</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D"># PROVIDE: tun2socks</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D"># REQUIRE: LOGIN</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D"># KEYWORD: shutdown</span></span>
<span class="line"></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">.</span><span style="color:#032F62;--s-dark:#9ECBFF"> /etc/rc.subr</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">name</span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#032F62;--s-dark:#9ECBFF">"tun2socks"</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">rcvar</span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#032F62;--s-dark:#9ECBFF">"tun2socks_enable"</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">load_rc_config</span><span style="color:#24292E;--s-dark:#E1E4E8"> $name</span></span>
<span class="line"></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">:</span><span style="color:#24292E;--s-dark:#E1E4E8"> ${tun2socks_enable</span><span style="color:#D73A49;--s-dark:#F97583">:=</span><span style="color:#24292E;--s-dark:#E1E4E8">no}</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">:</span><span style="color:#24292E;--s-dark:#E1E4E8"> ${tun2socks_config</span><span style="color:#D73A49;--s-dark:#F97583">:=</span><span style="color:#032F62;--s-dark:#9ECBFF">"/usr/local/tun2socks/config.yaml"</span><span style="color:#24292E;--s-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">pidfile</span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#032F62;--s-dark:#9ECBFF">"/var/run/${</span><span style="color:#24292E;--s-dark:#E1E4E8">name</span><span style="color:#032F62;--s-dark:#9ECBFF">}.pid"</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">command</span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#032F62;--s-dark:#9ECBFF">"/usr/local/tun2socks/tun2socks"</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">command_args</span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#032F62;--s-dark:#9ECBFF">"-config ${</span><span style="color:#24292E;--s-dark:#E1E4E8">tun2socks_config</span><span style="color:#032F62;--s-dark:#9ECBFF">} > /dev/null 2>&#x26;1 &#x26; echo </span><span style="color:#005CC5;--s-dark:#79B8FF">\$</span><span style="color:#032F62;--s-dark:#9ECBFF">! > ${</span><span style="color:#24292E;--s-dark:#E1E4E8">pidfile</span><span style="color:#032F62;--s-dark:#9ECBFF">}"</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">start_cmd</span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#032F62;--s-dark:#9ECBFF">"${</span><span style="color:#24292E;--s-dark:#E1E4E8">name</span><span style="color:#032F62;--s-dark:#9ECBFF">}_start"</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">tun2socks_start</span><span style="color:#24292E;--s-dark:#E1E4E8">()</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">{</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">    if</span><span style="color:#24292E;--s-dark:#E1E4E8"> [ </span><span style="color:#D73A49;--s-dark:#F97583">!</span><span style="color:#D73A49;--s-dark:#F97583"> -f</span><span style="color:#24292E;--s-dark:#E1E4E8"> ${tun2socks_config} ]; </span><span style="color:#D73A49;--s-dark:#F97583">then</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">        echo</span><span style="color:#032F62;--s-dark:#9ECBFF"> "${</span><span style="color:#24292E;--s-dark:#E1E4E8">tun2socks_config</span><span style="color:#032F62;--s-dark:#9ECBFF">} not found."</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">        exit</span><span style="color:#005CC5;--s-dark:#79B8FF"> 1</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">    fi</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">    echo</span><span style="color:#032F62;--s-dark:#9ECBFF"> "Starting ${</span><span style="color:#24292E;--s-dark:#E1E4E8">name</span><span style="color:#032F62;--s-dark:#9ECBFF">}."</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">    /bin/sh</span><span style="color:#005CC5;--s-dark:#79B8FF"> -c</span><span style="color:#032F62;--s-dark:#9ECBFF"> "${</span><span style="color:#24292E;--s-dark:#E1E4E8">command</span><span style="color:#032F62;--s-dark:#9ECBFF">} ${</span><span style="color:#24292E;--s-dark:#E1E4E8">command_args</span><span style="color:#032F62;--s-dark:#9ECBFF">}"</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">run_rc_command</span><span style="color:#032F62;--s-dark:#9ECBFF"> "</span><span style="color:#005CC5;--s-dark:#79B8FF">$1</span><span style="color:#032F62;--s-dark:#9ECBFF">"</span></span></code><label class="code-collapse-collapse" for="toggle-dszlftb"><svg class="code-collapse-icon" width="32" height="24" viewBox="0 0 32 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20,15 12,7 4,15"></polyline></svg></label></pre></div></div></div>
<div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre><code>[start]
command:/usr/local/etc/rc.d/tun2socks start
parameters:
type:script
message:starting tun2socks

[stop]
command:/usr/local/etc/rc.d/tun2socks stop
parameters:
type:script
message:stopping tun2socks

[restart]
command:/usr/local/etc/rc.d/tun2socks restart
parameters:
type:script
message:restarting tun2socks

[status]
command:/usr/local/etc/rc.d/tun2socks status; exit 0
parameters:
type:script_output
message:request tun2socks status
</code></pre></div>
<p>创建 <code>/etc/rc.conf</code> 并添加以下内容：</p>
<div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre><code>tun2socks_enable="YES"
</code></pre></div>
<p>给予运行权限 <code>chmod +x /usr/local/etc/rc.d/tun2socks</code> 后启用 <code>service configd restart</code></p>
<p>并手动启动 tun2socks</p>
<div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">/usr/local/etc/rc.d/tun2socks</span><span style="color:#032F62;--s-dark:#9ECBFF"> start</span></span></code></pre></div>
<h4 id="开机启动">开机启动<a href="#开机启动" class="heading-anchor-link" aria-label="Link to 开机启动"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h4>
<p>创建文件 <code>/usr/local/etc/rc.syshook.d/early/60-tun2socks</code></p>
<div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#6A737D;--s-dark:#6A737D">#!/bin/sh</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D"># Start tun2socks service</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">/usr/local/etc/rc.d/tun2socks</span><span style="color:#032F62;--s-dark:#9ECBFF"> start</span></span></code></pre></div>
<p>给予文件可执行权限 <code>chmod +x /usr/local/etc/rc.syshook.d/early/60-tun2socks</code> 即可</p>
<h4 id="新建端口配置网关">新建端口、配置网关<a href="#新建端口配置网关" class="heading-anchor-link" aria-label="Link to 新建端口、配置网关"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h4>
<p>在 OPNsense 的接口 - 分配中，添加一个新接口，设备即为我们在配置文件中写的 proxytun2socks0，保存即可</p>
<p>随后在添加的接口的配置页面启用接口，描述填写 TUN2SOCKS，IPv4 配置类型选择静态 IPv4，IPv4 地址为 <code>10.0.3.1/24</code>，保存</p>
<p>在系统 - 网关 - 配置中，新建一个网关，名称为 TUN2SOCKS_MIHOMO，接口选择我们刚添加的接口 TUN2SOCKS，IP 地址填写 <code>10.0.3.2</code>，其他默认保存</p>
<p>这样我们就新建了一个网关，只要流量进入这个网关，就会被转发到 127.0.0.1<div></div>，也就是 clash 进行代理</p>
<h4 id="设置国内外-ip-分流">设置国内外 IP 分流<a href="#设置国内外-ip-分流" class="heading-anchor-link" aria-label="Link to 设置国内外 IP 分流"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h4>
<p>OPNsense 对于我来说最有用的功能，就是防火墙 - 别名。这里可以设置一个 ip list，并在后续的规则配置中直接使用这个 ip list。这个 list 不仅可以手动填写，还可以直接订阅一个地址，动态获取</p>
<p>进入防火墙 - 别名，这里需要新建两个别名</p>
<p>第一个是 InternalAddress，表示局域网地址范围，类型选择 Network(s)，内容为</p>
<div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre><code>0.0.0.0/8
127.0.0.0/8
10.0.0.0/8
172.16.0.0/12
192.168.0.0/16
169.254.0.0/16
224.0.0.0/4
240.0.0.0/4
</code></pre></div>
<p>第二个是 CN_V4，表示国内的 IP 地址范围，类型选择 URL Table (IPs)，内容填写一个包含国内所有网段范围的列表地址即可，如 https://raw.githubusercontent.com/gaoyifan/china-operator-ip/refs/heads/ip-lists/china.txt</p>
<p>随后在防火墙 - 规则-LAN 中新建两个规则，排在规则列表的最前面，注意规则顺序从上到下</p>
<p>第一个规则目标为 InternalAddress，其余默认，表示目标地址为局域网时，按照默认规则路由</p>
<p>第二个规则目标为 CN_V4，并勾上目标/反转，网关选择上文新建的 TUN2SOCKS_MIHOMO，表示当目标地址为非中国 IP 时，将流量转发到 TUN2SOCKS_MIHOMO 网关</p>
<p>下面应该就是剩下的默认规则了，其他所有流量仍然按照默认规则路由，也就是国内 IP 直接走直连</p>
<p>当尝试访问 google 时，DNS 请求由 Adguard Home 获取转发到 clash，解析出 google 的真实地址。随后再向这个地址请求数据时，命中防火墙规则的第二条，转发到 TUN2SOCKS_MIHOMO 网关，通过 socks5 端口进入 clash 处理，最终实现出墙</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[更精确的基于 BGP 的国内外 IP 分流]]></title>
            <link>https://blog.shinya.click/fiddling/more-accurate-chnroute</link>
            <guid isPermaLink="false">https://blog.shinya.click/fiddling/more-accurate-chnroute</guid>
            <pubDate>Mon, 07 Oct 2024 08:51:00 GMT</pubDate>
            <description><![CDATA[基于 BGP 的国内外 IP 分流方案，提升了透明代理的效率和精准度。通过对国外 IP 进行 FakeIP 标记，主路由能够更智能地进行流量分流，确保网络连接的顺畅性。sing-box 的 DNS 模块配置也进行了相应优化，使得在处理 DNS 请求时，更加灵活且高效，进一步提升整体网络体验。]]></description>
            <content:encoded><![CDATA[<p>此前折腾过两节的透明代理方案：<a href="/fiddling/debian-as-bypass-router">debian 旁路由方案</a> 和 <a href="/fiddling/fake-ip-based-transparent-proxy">基于 FakeIP 的透明代理分流</a>，家里的透明代理基本已经可用了。基于 FakeIP 的方案使用 FakeIP 标记国外 IP，并在主路由识别并进行分流。sing-box 的 dns 模块配置为</p>
<div class="code-collapse-wrapper"><input type="checkbox" id="toggle-ysxs9pu" class="code-collapse-toggle" style="display: none;"><div class="code-collapse-preview"><div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0" class="code-preview"><code><span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">{</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">  "dns"</span><span style="color:#24292E;--s-dark:#E1E4E8">: {</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">    "servers"</span><span style="color:#24292E;--s-dark:#E1E4E8">: [</span></span>
<span class="line"><span style="color:#B31D28;--s-light-font-style:italic;--s-dark:#FDAEB7;--s-dark-font-style:italic">      ...</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    ],</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">    "rules"</span><span style="color:#24292E;--s-dark:#E1E4E8">: [</span></span>
<span class="line"><span style="color:#B31D28;--s-light-font-style:italic;--s-dark:#FDAEB7;--s-dark-font-style:italic">      ...</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">      {</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">        "server"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">"local"</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">        "rewrite_ttl"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">10</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">        "type"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">"logical"</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">        "mode"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">"and"</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">        "rules"</span><span style="color:#24292E;--s-dark:#E1E4E8">: [</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">          {</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            "rule_set"</span><span style="color:#24292E;--s-dark:#E1E4E8">: [</span></span>
<span class="line"><span style="color:#032F62;--s-dark:#9ECBFF">              "geosite-geolocation-!cn"</span><span style="color:#6A737D;--s-dark:#6A737D"> // [!code highlight]</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            ],</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            "invert"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">true</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">          },</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">          {</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            "rule_set"</span><span style="color:#24292E;--s-dark:#E1E4E8">: [</span></span>
<span class="line"><span style="color:#032F62;--s-dark:#9ECBFF">              "geosite-cn"</span><span style="color:#24292E;--s-dark:#E1E4E8">, </span><span style="color:#6A737D;--s-dark:#6A737D">// [!code highlight]</span></span>
<span class="line"><span style="color:#032F62;--s-dark:#9ECBFF">              "geosite-category-companies@cn"</span><span style="color:#24292E;--s-dark:#E1E4E8">, </span><span style="color:#6A737D;--s-dark:#6A737D">// [!code highlight]</span></span>
<span class="line"><span style="color:#032F62;--s-dark:#9ECBFF">              "geoip-cn"</span><span style="color:#6A737D;--s-dark:#6A737D"> // [!code highlight]</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            ]</span></span>
</code></pre></div><div class="code-collapse-gradient"></div><label class="code-collapse-expand" for="toggle-ysxs9pu"><svg class="code-collapse-icon" width="32" height="24" viewBox="0 0 32 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="4,9 12,17 20,9"></polyline></svg></label></div><div class="code-collapse-full"><div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">{</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">  "dns"</span><span style="color:#24292E;--s-dark:#E1E4E8">: {</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">    "servers"</span><span style="color:#24292E;--s-dark:#E1E4E8">: [</span></span>
<span class="line"><span style="color:#B31D28;--s-light-font-style:italic;--s-dark:#FDAEB7;--s-dark-font-style:italic">      ...</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    ],</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">    "rules"</span><span style="color:#24292E;--s-dark:#E1E4E8">: [</span></span>
<span class="line"><span style="color:#B31D28;--s-light-font-style:italic;--s-dark:#FDAEB7;--s-dark-font-style:italic">      ...</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">      {</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">        "server"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">"local"</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">        "rewrite_ttl"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">10</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">        "type"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">"logical"</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">        "mode"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">"and"</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">        "rules"</span><span style="color:#24292E;--s-dark:#E1E4E8">: [</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">          {</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            "rule_set"</span><span style="color:#24292E;--s-dark:#E1E4E8">: [</span></span>
<span class="line"><span style="color:#032F62;--s-dark:#9ECBFF">              "geosite-geolocation-!cn"</span><span style="color:#6A737D;--s-dark:#6A737D"> // [!code highlight]</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            ],</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            "invert"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">true</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">          },</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">          {</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            "rule_set"</span><span style="color:#24292E;--s-dark:#E1E4E8">: [</span></span>
<span class="line"><span style="color:#032F62;--s-dark:#9ECBFF">              "geosite-cn"</span><span style="color:#24292E;--s-dark:#E1E4E8">, </span><span style="color:#6A737D;--s-dark:#6A737D">// [!code highlight]</span></span>
<span class="line"><span style="color:#032F62;--s-dark:#9ECBFF">              "geosite-category-companies@cn"</span><span style="color:#24292E;--s-dark:#E1E4E8">, </span><span style="color:#6A737D;--s-dark:#6A737D">// [!code highlight]</span></span>
<span class="line"><span style="color:#032F62;--s-dark:#9ECBFF">              "geoip-cn"</span><span style="color:#6A737D;--s-dark:#6A737D"> // [!code highlight]</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            ]</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">          }</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        ]</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">      },</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">      {</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">        "server"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">"dns-fakeip"</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">        "rewrite_ttl"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">1</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">        "query_type"</span><span style="color:#24292E;--s-dark:#E1E4E8">: [</span></span>
<span class="line"><span style="color:#032F62;--s-dark:#9ECBFF">          "A"</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#032F62;--s-dark:#9ECBFF">          "AAAA"</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        ]</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">      }</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    ],</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">    "strategy"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">"ipv4_only"</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">    "fakeip"</span><span style="color:#24292E;--s-dark:#E1E4E8">: {</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">      "enabled"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">true</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">      "inet4_range"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">"198.18.0.0/15"</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">  }</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">}</span></span></code><label class="code-collapse-collapse" for="toggle-ysxs9pu"><svg class="code-collapse-icon" width="32" height="24" viewBox="0 0 32 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20,15 12,7 4,15"></polyline></svg></label></pre></div></div></div>
<p>这个 dns 分流规则基于规则集：当域名不在 <code>geosite-geolocation-!cn</code> 中，且域名在 <code>geosite-cn</code> 或者 <code>geosite-category-companies@cn</code>，或域名解析出的 ip 在 <code>geoip-cn</code> 中时，认为是国内流量，返回 RealIP，否则返回 FakeIP</p>
<p>这种判断方式十分粗糙，且不说这几个域名规则集只能囊括一些常见域名，ip 规则集 <code>geoip-cn</code> 是基于 MaxMind 的 GeoLite2 数据库，来自于 WHOIS 数据库，大部分情况只代表这个 IP 被哪个机构注册使用，但无从知晓该 IP 被用在何处，尤其是 CN-IP，十分不准确</p>
<p>正好，最近了解到了 BGP。抄一点科普如下</p>
<blockquote>
<p>边界网关协议（Border Gateway Protocol，BGP）是一种用来在路由选择域之间交换网络层可达性信息（Network Layer Reachability Information，NLRI）的路由选择协议。由于不同的管理机构分别控制着他们各自的路由选择域，因此，路由选择域经常被称为自治系统 AS（Autonomous System）。现在的 Internet 是一个由多个自治系统相互连接构成的大网络，BGP 作为事实上的 Internet 外部路由协议标准，被广泛应用于 ISP（Internet Service Provider）之间。</p>
</blockquote>
<p>基于 BGP，所有路由到中国的流量都会由国内 AS 声明，那么只要收集到国内全部 AS 声明的 IP list，就是一份更加准确的 CN-IP 了</p>
<blockquote>
<p>根据中国具体国情、维基百科，能够直接与国际互联网建立 BGP Session 只有三大运营商、教育网和科技网</p>
</blockquote>
<p>网上有很多教程教你如何自己运营一个 AS，由此拉到一张完整的 BGP 表。但是作为懒狗（拿来主义），我找到 Github 上其实已经有一些基于 BGP 的 CN-IP list。本篇基于这个项目：https://github.com/gaoyifan/china-operator-ip/blob/ip-lists/china.txt</p>
<p>有了 list，下面就是 code time！</p>
<div class="code-collapse-wrapper"><input type="checkbox" id="toggle-0o441az" class="code-collapse-toggle" style="display: none;"><div class="code-collapse-preview"><div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0" class="code-preview"><code><span class="line"><span style="color:#6A737D;--s-dark:#6A737D">#!/bin/bash</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D"># 定义变量</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">URL</span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#032F62;--s-dark:#9ECBFF">"https://raw.githubusercontent.com/gaoyifan/china-operator-ip/refs/heads/ip-lists/china.txt"</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">IPSET_NAME</span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#032F62;--s-dark:#9ECBFF">"allowed_ips"</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D"># 下载新的 IP 列表</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">curl</span><span style="color:#005CC5;--s-dark:#79B8FF"> -o</span><span style="color:#032F62;--s-dark:#9ECBFF"> /tmp/ip-list.txt</span><span style="color:#032F62;--s-dark:#9ECBFF"> "</span><span style="color:#24292E;--s-dark:#E1E4E8">$URL</span><span style="color:#032F62;--s-dark:#9ECBFF">"</span><span style="color:#D73A49;--s-dark:#F97583"> ||</span><span style="color:#24292E;--s-dark:#E1E4E8"> { </span><span style="color:#005CC5;--s-dark:#79B8FF">echo</span><span style="color:#032F62;--s-dark:#9ECBFF"> "下载 IP 列表失败"</span><span style="color:#24292E;--s-dark:#E1E4E8">; </span><span style="color:#005CC5;--s-dark:#79B8FF">exit</span><span style="color:#005CC5;--s-dark:#79B8FF"> 1</span><span style="color:#24292E;--s-dark:#E1E4E8">; }</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D"># 清空现有的 ipset 集合</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">ipset</span><span style="color:#032F62;--s-dark:#9ECBFF"> flush</span><span style="color:#24292E;--s-dark:#E1E4E8"> $IPSET_NAME</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D"># 重新创建 ipset 集合（如果不存在则创建）</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">ipset</span><span style="color:#032F62;--s-dark:#9ECBFF"> create</span><span style="color:#24292E;--s-dark:#E1E4E8"> $IPSET_NAME </span><span style="color:#032F62;--s-dark:#9ECBFF">hash:net</span><span style="color:#005CC5;--s-dark:#79B8FF"> -exist</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D"># 添加局域网地址到集合</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">ipset</span><span style="color:#032F62;--s-dark:#9ECBFF"> add</span><span style="color:#24292E;--s-dark:#E1E4E8"> $IPSET_NAME </span><span style="color:#032F62;--s-dark:#9ECBFF">0.0.0.0/8</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">ipset</span><span style="color:#032F62;--s-dark:#9ECBFF"> add</span><span style="color:#24292E;--s-dark:#E1E4E8"> $IPSET_NAME </span><span style="color:#032F62;--s-dark:#9ECBFF">127.0.0.0/8</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">ipset</span><span style="color:#032F62;--s-dark:#9ECBFF"> add</span><span style="color:#24292E;--s-dark:#E1E4E8"> $IPSET_NAME </span><span style="color:#032F62;--s-dark:#9ECBFF">10.0.0.0/8</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">ipset</span><span style="color:#032F62;--s-dark:#9ECBFF"> add</span><span style="color:#24292E;--s-dark:#E1E4E8"> $IPSET_NAME </span><span style="color:#032F62;--s-dark:#9ECBFF">172.16.0.0/12</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">ipset</span><span style="color:#032F62;--s-dark:#9ECBFF"> add</span><span style="color:#24292E;--s-dark:#E1E4E8"> $IPSET_NAME </span><span style="color:#032F62;--s-dark:#9ECBFF">192.168.0.0/16</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">ipset</span><span style="color:#032F62;--s-dark:#9ECBFF"> add</span><span style="color:#24292E;--s-dark:#E1E4E8"> $IPSET_NAME </span><span style="color:#032F62;--s-dark:#9ECBFF">169.254.0.0/16</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">ipset</span><span style="color:#032F62;--s-dark:#9ECBFF"> add</span><span style="color:#24292E;--s-dark:#E1E4E8"> $IPSET_NAME </span><span style="color:#032F62;--s-dark:#9ECBFF">224.0.0.0/4</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">ipset</span><span style="color:#032F62;--s-dark:#9ECBFF"> add</span><span style="color:#24292E;--s-dark:#E1E4E8"> $IPSET_NAME </span><span style="color:#032F62;--s-dark:#9ECBFF">240.0.0.0/4</span></span>
<span class="line"></span>
</code></pre></div><div class="code-collapse-gradient"></div><label class="code-collapse-expand" for="toggle-0o441az"><svg class="code-collapse-icon" width="32" height="24" viewBox="0 0 32 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="4,9 12,17 20,9"></polyline></svg></label></div><div class="code-collapse-full"><div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#6A737D;--s-dark:#6A737D">#!/bin/bash</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D"># 定义变量</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">URL</span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#032F62;--s-dark:#9ECBFF">"https://raw.githubusercontent.com/gaoyifan/china-operator-ip/refs/heads/ip-lists/china.txt"</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">IPSET_NAME</span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#032F62;--s-dark:#9ECBFF">"allowed_ips"</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D"># 下载新的 IP 列表</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">curl</span><span style="color:#005CC5;--s-dark:#79B8FF"> -o</span><span style="color:#032F62;--s-dark:#9ECBFF"> /tmp/ip-list.txt</span><span style="color:#032F62;--s-dark:#9ECBFF"> "</span><span style="color:#24292E;--s-dark:#E1E4E8">$URL</span><span style="color:#032F62;--s-dark:#9ECBFF">"</span><span style="color:#D73A49;--s-dark:#F97583"> ||</span><span style="color:#24292E;--s-dark:#E1E4E8"> { </span><span style="color:#005CC5;--s-dark:#79B8FF">echo</span><span style="color:#032F62;--s-dark:#9ECBFF"> "下载 IP 列表失败"</span><span style="color:#24292E;--s-dark:#E1E4E8">; </span><span style="color:#005CC5;--s-dark:#79B8FF">exit</span><span style="color:#005CC5;--s-dark:#79B8FF"> 1</span><span style="color:#24292E;--s-dark:#E1E4E8">; }</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D"># 清空现有的 ipset 集合</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">ipset</span><span style="color:#032F62;--s-dark:#9ECBFF"> flush</span><span style="color:#24292E;--s-dark:#E1E4E8"> $IPSET_NAME</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D"># 重新创建 ipset 集合（如果不存在则创建）</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">ipset</span><span style="color:#032F62;--s-dark:#9ECBFF"> create</span><span style="color:#24292E;--s-dark:#E1E4E8"> $IPSET_NAME </span><span style="color:#032F62;--s-dark:#9ECBFF">hash:net</span><span style="color:#005CC5;--s-dark:#79B8FF"> -exist</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D"># 添加局域网地址到集合</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">ipset</span><span style="color:#032F62;--s-dark:#9ECBFF"> add</span><span style="color:#24292E;--s-dark:#E1E4E8"> $IPSET_NAME </span><span style="color:#032F62;--s-dark:#9ECBFF">0.0.0.0/8</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">ipset</span><span style="color:#032F62;--s-dark:#9ECBFF"> add</span><span style="color:#24292E;--s-dark:#E1E4E8"> $IPSET_NAME </span><span style="color:#032F62;--s-dark:#9ECBFF">127.0.0.0/8</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">ipset</span><span style="color:#032F62;--s-dark:#9ECBFF"> add</span><span style="color:#24292E;--s-dark:#E1E4E8"> $IPSET_NAME </span><span style="color:#032F62;--s-dark:#9ECBFF">10.0.0.0/8</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">ipset</span><span style="color:#032F62;--s-dark:#9ECBFF"> add</span><span style="color:#24292E;--s-dark:#E1E4E8"> $IPSET_NAME </span><span style="color:#032F62;--s-dark:#9ECBFF">172.16.0.0/12</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">ipset</span><span style="color:#032F62;--s-dark:#9ECBFF"> add</span><span style="color:#24292E;--s-dark:#E1E4E8"> $IPSET_NAME </span><span style="color:#032F62;--s-dark:#9ECBFF">192.168.0.0/16</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">ipset</span><span style="color:#032F62;--s-dark:#9ECBFF"> add</span><span style="color:#24292E;--s-dark:#E1E4E8"> $IPSET_NAME </span><span style="color:#032F62;--s-dark:#9ECBFF">169.254.0.0/16</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">ipset</span><span style="color:#032F62;--s-dark:#9ECBFF"> add</span><span style="color:#24292E;--s-dark:#E1E4E8"> $IPSET_NAME </span><span style="color:#032F62;--s-dark:#9ECBFF">224.0.0.0/4</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">ipset</span><span style="color:#032F62;--s-dark:#9ECBFF"> add</span><span style="color:#24292E;--s-dark:#E1E4E8"> $IPSET_NAME </span><span style="color:#032F62;--s-dark:#9ECBFF">240.0.0.0/4</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D"># 读取 IP 列表并添加到 ipset 集合</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">while</span><span style="color:#24292E;--s-dark:#E1E4E8"> IFS</span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#005CC5;--s-dark:#79B8FF"> read</span><span style="color:#005CC5;--s-dark:#79B8FF"> -r</span><span style="color:#032F62;--s-dark:#9ECBFF"> ip</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">do</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">    # 如果行为空或注释行，则跳过</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">    if</span><span style="color:#24292E;--s-dark:#E1E4E8"> [ </span><span style="color:#D73A49;--s-dark:#F97583">-z</span><span style="color:#032F62;--s-dark:#9ECBFF"> "</span><span style="color:#24292E;--s-dark:#E1E4E8">$ip</span><span style="color:#032F62;--s-dark:#9ECBFF">"</span><span style="color:#24292E;--s-dark:#E1E4E8"> ] </span><span style="color:#D73A49;--s-dark:#F97583">||</span><span style="color:#24292E;--s-dark:#E1E4E8"> [[ $ip </span><span style="color:#D73A49;--s-dark:#F97583">==</span><span style="color:#005CC5;--s-dark:#79B8FF"> \#</span><span style="color:#D73A49;--s-dark:#F97583">*</span><span style="color:#24292E;--s-dark:#E1E4E8"> ]]; </span><span style="color:#D73A49;--s-dark:#F97583">then</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">        continue</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">    fi</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">    ipset</span><span style="color:#032F62;--s-dark:#9ECBFF"> add</span><span style="color:#24292E;--s-dark:#E1E4E8"> $IPSET_NAME $ip</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">done</span><span style="color:#D73A49;--s-dark:#F97583"> &#x3C;</span><span style="color:#24292E;--s-dark:#E1E4E8"> /tmp/ip-list.txt</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D"># 清理临时文件</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">rm</span><span style="color:#032F62;--s-dark:#9ECBFF"> /tmp/ip-list.txt</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D"># 创建自定义链</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">iptables</span><span style="color:#005CC5;--s-dark:#79B8FF"> -t</span><span style="color:#032F62;--s-dark:#9ECBFF"> mangle</span><span style="color:#005CC5;--s-dark:#79B8FF"> -N</span><span style="color:#032F62;--s-dark:#9ECBFF"> NO_FORWARD</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D"># 配置 iptables：将流量引导到自定义链，并基于逻辑规则返回或标记</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">iptables</span><span style="color:#005CC5;--s-dark:#79B8FF"> -t</span><span style="color:#032F62;--s-dark:#9ECBFF"> mangle</span><span style="color:#005CC5;--s-dark:#79B8FF"> -A</span><span style="color:#032F62;--s-dark:#9ECBFF"> PREROUTING</span><span style="color:#005CC5;--s-dark:#79B8FF"> -j</span><span style="color:#032F62;--s-dark:#9ECBFF"> NO_FORWARD</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D"># 在自定义链中配置规则</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">iptables</span><span style="color:#005CC5;--s-dark:#79B8FF"> -t</span><span style="color:#032F62;--s-dark:#9ECBFF"> mangle</span><span style="color:#005CC5;--s-dark:#79B8FF"> -A</span><span style="color:#032F62;--s-dark:#9ECBFF"> NO_FORWARD</span><span style="color:#005CC5;--s-dark:#79B8FF"> -s</span><span style="color:#005CC5;--s-dark:#79B8FF"> 192.168.7.2</span><span style="color:#005CC5;--s-dark:#79B8FF"> -j</span><span style="color:#032F62;--s-dark:#9ECBFF"> RETURN</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">iptables</span><span style="color:#005CC5;--s-dark:#79B8FF"> -t</span><span style="color:#032F62;--s-dark:#9ECBFF"> mangle</span><span style="color:#005CC5;--s-dark:#79B8FF"> -A</span><span style="color:#032F62;--s-dark:#9ECBFF"> NO_FORWARD</span><span style="color:#005CC5;--s-dark:#79B8FF"> -m</span><span style="color:#032F62;--s-dark:#9ECBFF"> set</span><span style="color:#005CC5;--s-dark:#79B8FF"> --match-set</span><span style="color:#24292E;--s-dark:#E1E4E8"> $IPSET_NAME </span><span style="color:#032F62;--s-dark:#9ECBFF">dst</span><span style="color:#005CC5;--s-dark:#79B8FF"> -j</span><span style="color:#032F62;--s-dark:#9ECBFF"> RETURN</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">iptables</span><span style="color:#005CC5;--s-dark:#79B8FF"> -t</span><span style="color:#032F62;--s-dark:#9ECBFF"> mangle</span><span style="color:#005CC5;--s-dark:#79B8FF"> -A</span><span style="color:#032F62;--s-dark:#9ECBFF"> NO_FORWARD</span><span style="color:#005CC5;--s-dark:#79B8FF"> -j</span><span style="color:#032F62;--s-dark:#9ECBFF"> MARK</span><span style="color:#005CC5;--s-dark:#79B8FF"> --set-mark</span><span style="color:#005CC5;--s-dark:#79B8FF"> 1</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D"># 设置路由以标记的流量转发到 192.168.7.2</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">ip</span><span style="color:#032F62;--s-dark:#9ECBFF"> rule</span><span style="color:#032F62;--s-dark:#9ECBFF"> add</span><span style="color:#032F62;--s-dark:#9ECBFF"> fwmark</span><span style="color:#005CC5;--s-dark:#79B8FF"> 1</span><span style="color:#032F62;--s-dark:#9ECBFF"> table</span><span style="color:#005CC5;--s-dark:#79B8FF"> 100</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">ip</span><span style="color:#032F62;--s-dark:#9ECBFF"> route</span><span style="color:#032F62;--s-dark:#9ECBFF"> add</span><span style="color:#032F62;--s-dark:#9ECBFF"> default</span><span style="color:#032F62;--s-dark:#9ECBFF"> via</span><span style="color:#005CC5;--s-dark:#79B8FF"> 192.168.7.2</span><span style="color:#032F62;--s-dark:#9ECBFF"> table</span><span style="color:#005CC5;--s-dark:#79B8FF"> 100</span></span></code><label class="code-collapse-collapse" for="toggle-0o441az"><svg class="code-collapse-icon" width="32" height="24" viewBox="0 0 32 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20,15 12,7 4,15"></polyline></svg></label></pre></div></div></div>
<p>代码注释很完全，不做过多解释</p>
<p>如果你的路由系统是 OpenWRT，需要额外安装 bash、ipset、iptables 等，OpenWRT 的默认 shell 是 ash，无法运行此脚本</p>
<div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">opkg</span><span style="color:#032F62;--s-dark:#9ECBFF"> update</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">opkg</span><span style="color:#032F62;--s-dark:#9ECBFF"> install</span><span style="color:#032F62;--s-dark:#9ECBFF"> bash</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">opkg</span><span style="color:#032F62;--s-dark:#9ECBFF"> install</span><span style="color:#032F62;--s-dark:#9ECBFF"> curl</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">opkg</span><span style="color:#032F62;--s-dark:#9ECBFF"> install</span><span style="color:#032F62;--s-dark:#9ECBFF"> ipset</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">opkg</span><span style="color:#032F62;--s-dark:#9ECBFF"> install</span><span style="color:#032F62;--s-dark:#9ECBFF"> iptables</span></span></code></pre></div>
<p>这份 CN-IP 一天更新一次，可以设置一个定时任务一天执行一次这个脚本，并把这个脚本添加到启动项中</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[在字节工作三年的同时仍保持一定程度的心理健康其实也并非完全不可能]]></title>
            <link>https://blog.shinya.click/daily/work-for-3-years</link>
            <guid isPermaLink="false">https://blog.shinya.click/daily/work-for-3-years</guid>
            <pubDate>Sat, 28 Sep 2024 08:26:00 GMT</pubDate>
            <description><![CDATA[在字节工作三年，时间的流逝仿佛在无形中加速。尽管身处快速变化的环境，仍能找到一丝心理平衡。回想起刚入职时的懵懂，那个年轻的自己在杭州八方城的阳台上，眺望着远方的山脉，心中对未来充满期待与好奇。随着时间的推移，职场的挑战与成长交织，形成了一段独特而丰富的经历，让人学会在忙碌中寻找属于自己的节奏。]]></description>
            <content:encoded><![CDATA[<figure><img src="https://blog-img.774352199.xyz/1726828874334.jpg" alt="标题源" loading="lazy" decoding="async" style="background-image:url(data:image/webp;base64,UklGRlgAAABXRUJQVlA4IEwAAADQAQCdASoQAAkAAsBMJQBOgB6Ojmm5gAD+2+m6+jhCVrL8nW6hFKayYrRPG85hKLV7/wC/dFnffEycGDNQPZ5D1BkTJt0fGgYKNcAA);background-size:cover;background-repeat:no-repeat"><figcaption>标题源</figcaption></figure>
<p>晚饭时间，同事聊起了他买的车，忽然感叹了一句：“感觉工作之后时间变得特别快”</p>
<p>我：“不好说，今天（周五）下午的时间就挺难熬”</p>
<p>虽然很不愿承认，总把自己当学生来看（暂时是组里年龄最小的），但是距我离开校园，满打满算也有三年多。这三年时间算得动荡，但也相对安稳</p>
<p>字节内有句老话：“字节一年，人间三年”，这“九年”时光，也尽在这里了</p>
<h3 id="懵懂">懵懂<a href="#懵懂" class="heading-anchor-link" aria-label="Link to 懵懂"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h3>
<p>如果不算上实习，我是在 21 年 6 月底入职了杭州字节</p>
<p>请看 VCR：<a href="https://www.nowcoder.com/share/jump/67966291840886568" target="_blank" rel="noopener noreferrer" data-umami-event="outbound-link-click" data-umami-event-url="https://www.nowcoder.com/share/jump/67966291840886568">入职一周小记</a></p>
<p>杭州字节租下了八方城的两栋楼，却不是整栋，两幢楼基本都只租了最顶上的五六层，风景倒是不错，每天趴在阳台上可以远眺远处的 EFC（余杭最像城里的地方）和更远处的余杭南部的山区，山雨欲来和雨过天晴时的风景算得一绝</p>
<p>我入职的是抖音直播部门下面的某个 ToC 的组，由于提前实习了两个月，帮助建设过新人文档，对组里的人员业务还算了解。但由于实习结束，回校办理毕业适宜耽搁一个月，再入职时，忽然没有需求分配给我了。于是度过了快乐的摸鱼时间，享受了最后一个月的大小周双倍工资</p>
<p>忽然有一天 leader 和 mentor 找到我，问我是否愿意去志愿下北京的一个业务。原来是一个 ToB 平台，和我们同属一个 +2，发展迅速，人手短缺，向 +2 四处要人，摊派到我们组就是一个名额。彼时我刚刚毕业，浑身有使不完的冲劲儿，又恰好闲着，自然满嘴答应</p>
<p>于是就一直跟着北京的那个组做需求，这算是一个向上管理的平台，平台 QPS 很低，但是主要用来产出数据向上汇报，和 ToC 业务的关注点完全不同，重业务轻性能，更早期甚至对一致性也没很高的要求，调用失败了，加个报警，人工看看，影响不大甚至可以不修，再加上平台用户操作频率也不高，日常维护也算的上清闲</p>
<p>我加入时期，恰逢平台大版本迭代，北京组里派了一个研发远程带着我做需求，最初都是写小需求，边边角角，修修改改，主要是熟悉熟悉流程</p>
<blockquote class="admonition-tip">
<span class="admonition-title">TIP</span>
<p>需求初评 —— 需求详评 —— （研发开始介入）技术评审 —— 开发 —— （测试开始介入）showcase —— 测试 —— Launch Review —— 上线</p>
</blockquote>
<p>有条不紊，一个需求接着一个需求，没有多余的事情。我从最开始的，技术评审都不知道说些什么，甚至专门找个会议室正襟危坐，到后来……</p>
<p>但其实并没有什么后来的进步，只是习惯或者麻木了而已，也逐渐看轻了这些事情。一个线下 bug 也并不会怎么样，就算是线上 bug，大不了修数据</p>
<p>本身用 Golang 写业务就相当 Freelance，再加上需求的快速迭代，没什么时间对代码进行重构，能跑起来就是成功，仓库代码愈发狂乱。许多奇奇怪怪的复杂需求，再加上四处借调导致技术风格不统一，让这个平台逐渐成了一个“大泥球”</p>
<p>但和我又有什么关系呢，我只是一个写小需求的 noob 罢了</p>
<p>由于刚入职，干啥都是新奇的，我甚至周末会到公司主动加班 —— 反正一个人在家也没什么事做，何乐而不为呢。后期给自己找了点事情，完成了 <a href="https://github.com/CN-GuoZiyang/Mydb" target="_blank" rel="noopener noreferrer" data-umami-event="outbound-link-click" data-umami-event-url="https://github.com/CN-GuoZiyang/Mydb">MYDB</a> 和相关的 <a href="/projects/mydb/mydb0">教程</a>，虽说最后 BUG 不少，也没怎么充分测试，但确实令我干劲十足：八九点下班，能一直写到凌晨一两点，还能再洗个澡打打游戏喝点小酒，三四点睡觉</p>
<p>也结交了不少有趣的人，拉了个小群，每天不工作的时候就在群里吹吹水。这时的字节简直满足了我对互联网的一切想象：自由自在，管理扁平，请个假不用费尽心思编理由，工作电脑也想装啥装啥，甚至因为疫情还没有结束，隔三岔五就居家办公，没有打卡和强制工时，十一点多到公司，吃完晚饭就跑路……</p>
<h3 id="发愤">发愤<a href="#发愤" class="heading-anchor-link" aria-label="Link to 发愤"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h3>
<p>就这么一路开开心心地干着，到了 22 年初。在这期间，我也由一些边角需求开始，逐渐能 cover 住整个领域</p>
<p>阿里有句古话，经常在安慰被裁人员或者被裁时自我安慰时使用：“拥抱变化”</p>
<blockquote>
<p>唯一不变的，就是变化</p>
</blockquote>
<p>平台发展迅猛，业务从直播扩展到了整个抖音，同时由于产品的调整，原本的北京开发团队把平台交接了出去，接盘的恰好是我原本的组</p>
<p>于是便是交接流程，每天的活动就是带着新人（新组的人）做需求，以及各种分享。我在那时已经负责预算模块，于是也组织了一次分享。虽然全程念文档毫无感情，但也确实是我在公司的第一次分享（转正不需要答辩）</p>
<p>随着北京的人逐渐撤出，人力猛然出现短缺。于是在预算模块外，整个项目领域又猛地塞到我手上。恰逢产品提出了一个大需求，我在项目领域的第一个需求，就是将整个项目领域近乎重写了一番</p>
<p>需求撞上春节假期，需求紧急，整个假期除了三十和初一，都在图书馆搓代码，前后由于理解误差等原因（还是太菜了），整个代码被来来回回实现了三四遍，最后终于勉强赶上了节后第一班上线</p>
<p>后续又有各种历史帐清算，各种重构评审，代码迁移到新服务（BTW，2022 年启动的新服务迁移，至今没有迁移完成），忙到脚不沾地</p>
<p>那段时间极其焦虑，加班非常猛。日常处理各种问题都够忙到十几点，基本没有十点之前下过班。对应的就是报复性熬夜和报复性娱乐。典型娱乐比如周五晚上到凌晨在公司找个会议室喝酒，喝到周六大清早回家睡觉 😅</p>
<p>非常神奇的是，每次加班时，都能遇到我组组长，每次十点十一点钟，不是我喊他下班就是他喊我下班。我图上下班方便，直接搬到了公司所在的小区，每天上下班走地下车库；他住的地方每天通勤要五十分钟一小时</p>
<p>可能这就是有家庭的代价吧</p>
<p>由于下班太晚，各种日常开发和学习也基本都停掉了，回到家只想喝酒和睡觉。喝的酒也逐渐从自己调制的鸡尾酒变为伏特加/威士忌直接喝，喝酒的目的也从好喝/微醺变成了助眠/去除焦虑</p>
<h3 id="转变">转变<a href="#转变" class="heading-anchor-link" aria-label="Link to 转变"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h3>
<blockquote>
<p>有时候宁愿生活一成不变，因为熟悉的平淡胜过未知的风险</p>
</blockquote>
<p>风险来的很快</p>
<p>22 年中，我们组换了一个新的 +2，气氛开始微妙变化。原本随意的非正式会议变得正式化，需求流程也变得复杂，简单的步骤被新增了无数的审查环节，每一个决策都需要经过层层把关</p>
<p>各种名词被生造了出来：bug 比人天、人效比、代码复杂度等等。指标的或高或低，通常意味着你在下次周会或者双周会上会被拎出来批斗。产研测的关系也不再和谐，各种会议上的唇枪舌剑、钩心斗角、推诿甩锅，让人精疲力竭，专心写代码成了一种奢望。做多错多，不做不错，能推则推，成了保命技巧。原本“线上 bug 大不了修数据”，到现在“一个回归 bug 就能宣判你的死刑”</p>
<p>也是从这段时间开始，hc 的收缩导致组里开始减员，很多我熟悉的人逐渐要么主动要么被动地离开了，原本每天一起吃饭一起摸鱼一起下班的同事，好些一周才能见到一次，工作的孤独感不断加剧</p>
<img src="https://blog-img.774352199.xyz/2024/630b780569e7aa8fa40e1ecc6a189b40.png" style="width: 50%"/>
<p>在这种情况下，不知是情绪躯体化，还是这几年熬夜 + 喝酒，或者兼具有之，身体开始报警，具体可见：<a href="/daily/anti-chronic-gastritis">慢性胃炎治疗之路</a></p>
<p>体重急剧减轻之下（一个月 12 斤），痛定思痛，决定~~开摆~~开启养生模式！</p>
<p>于是过上了每天八点半起床去公司吃早餐，晚上六点多吃完晚饭直接下班，下班后跑步半小时，十二点前睡觉（目前还在努力）的健康生活</p>
<blockquote class="admonition-caution">
<span class="admonition-title">CAUTION</span>
<p>奋斗？命都要没了还怎么奋斗！</p>
</blockquote>
<p>好在，前面那段剧变也搞走了不少产品，需求量急剧减少。虽然上面也开启了“没法整活那就整人”模式，又增加了许多乱七八糟的卡点和规范，且现在的 +2 阿里味儿特别浓厚，很喜欢搞一些莫名其妙的团建和个人 show 表演（算是另一种的折磨人）。平台进入低频维护期，琐碎的事情也不再多</p>
<p>外面行情很差，先这么混着吧</p>
<h3 id="后记">后记<a href="#后记" class="heading-anchor-link" aria-label="Link to 后记"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h3>
<p>原本这篇三周年总结应该在 5、6 月份完成，但那时恰逢产品剧变 + 身体不适，这篇文档的标题就躺在我的备忘录三个月，直到最近身体状况稳定了下来，才又拾起了总结。然而就是这三个月，我的心态就从积极进取变成了“先这么混着”，不禁让人感叹世事多变</p>
<p>但是好在，一定程度的心理健康也确实保持住了。最近在研究心理状态检测 + 日程规划，尝试做一个自律的人，虽然暂时的成果是能直观的看到时间被浪费到哪了，后续会写篇文章介绍下这方面的成果</p>
<p>这些年出国留学的心思还是在蠢蠢欲动，尤其是遇到各种不如意时，就尤其上头。在无数忙碌了一天后的深夜，这个念头便犹如藤蔓，爬满心头。这三年距离我辞职最近的时刻，就是我在医院等待胃镜时，还在回复飞书消息处理 oncall 的时刻。我时常问自己：“为了这些工资，付出的这些代价真的值得吗”。但实际冷静下来，还是叹了口气，狠心摁下了这想法</p>
<p>不知道再过三年，我是否会后悔甚至痛恨现在的自己。但这也许就是选择的诅咒</p>
<blockquote>
<p>无论你如何选择，甚至不做选择，你都必须承受代价</p>
</blockquote>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[慢性胃炎治疗之路]]></title>
            <link>https://blog.shinya.click/daily/anti-chronic-gastritis</link>
            <guid isPermaLink="false">https://blog.shinya.click/daily/anti-chronic-gastritis</guid>
            <pubDate>Sun, 01 Sep 2024 14:03:10 GMT</pubDate>
            <description><![CDATA[慢性胃炎的治疗之路充满波折，经历了儿时的肠胃不适到大学期间的神奇恢复，再到职场生活中的放纵与反复发作，胃部的痛苦成为了生活中的常态。尽管早年的症状在一段时间内神奇消失，但随着熬夜与饮酒的习惯加重，胃的警报再次响起，频繁的恶心和反胃让生活变得难以忍受。在经历了多次的医疗检查与反思后，终于开始认真对待这段漫长而复杂的治疗过程。]]></description>
            <content:encoded><![CDATA[<p>我自幼便肠胃不佳，小学初中高中，看了无数医生，吃了无数的药，也没有把胃养好。一旦吃点冷的、辣的，便腹胀反酸、反胃想吐，甚至即使饮食健康，遇到什么考试面试紧张时也会反胃恶心。</p>
<p>神奇的是，这几乎所有的症状，在我本科期间，都奇迹般地消失了。冷的、辣的，啥都能吃，酒也猛猛喝，啥事没有，miracle！</p>
<p>工作后，生活习惯更加离谱。由于公司对早上上班时间要求较松，每日便熬夜，三年内几乎没有在三点前睡觉过。几乎每日喝酒，开始还会调一调酒，伏特加兑一兑橙汁等，十几度的样子。到后期酒量上来了，十几度已经难以满足，就直接干喝威士忌，每天 150ml 到 300ml 的样子，就这样又喝了两年。</p>
<p>~~不得不说，我的肝真的是 tough guy~~</p>
<p>结果肝先没出问题，胃先顶不住了。大概一个半月前，开始频繁恶心头晕反胃，尤其是吃饭后，吃了就吐，半月就瘦了快十斤😓再加上胡思乱想，网上看病更是吓人，实在顶不住了，连续做了两次胃镜（不得不吐槽下大城市的就医效率，浙一约胃肠镜，基本只能约半个月后，约周末更是别想，实在绷不住就先跑回家做了个胃镜，回来后又做了一个），最终诊断结果</p>
<p><img src="https://blog-img.774352199.xyz/2025/0a4da5b052d4794c80ed1ba23922a909.png" alt="" loading="lazy" decoding="async" style="background-image:url(data:image/webp;base64,UklGRjQAAABXRUJQVlA4ICgAAADQAQCdASoQAAoAAsBMJaQAAxfuRGcNwAD+9/J7qOVbcapyVoj/AAAA);background-size:cover;background-repeat:no-repeat"></p>
<p>好在没有幽门螺旋杆菌，但是就是这慢性胃炎伴糜烂，比我想象的要更是折磨。</p>
<p>医生给开了三种药：艾司奥美拉挫肠溶片、替普瑞酮胶囊和马来酸曲美布汀分散片，吃两周。</p>
<p>吃药的两周非常舒服，几乎完全感受不到任何难受，吃嘛嘛香，后面才知道是奥美拉挫抑制了胃酸，抑制住了所有症状。但是一旦开始停药，事情就坏起来了。</p>
<p>事实上没有遇到非常剧烈的反扑，但是胃部（左上腹）有一种存在感，不痛，但是能感受到，这种感觉时时刻刻提醒着我，时常令人心情烦躁，然而胃又是情绪器官，于是……</p>
<p>Time to make some changes!</p>
<p>暂时的措施如下</p>
<ol>
<li>每天早起（八点半）吃早餐，公司的早餐比较丰富，一般一个煮鸡蛋一碗粥，再加一些当天的包子馒头什么的</li>
<li>一天三次康复新液（蟑螂水）修复胃糜烂，康复新液倒是没有我想象中的那么难喝，除了比较甜腻外，克服了心理恐惧后还是比较好喝的（</li>
<li>维生素 B 族和维生素 C，想起来就吃，佛系吃</li>
<li>午饭后一个小时左右喝酸奶。这个暂时没有验证是否有效，因为酸奶买错了……买成光明的酸牛奶饮品，我还买了一箱😭</li>
<li>早睡，基本能压缩到一点之前睡觉（似乎也不是那么早</li>
<li>戒酒</li>
</ol>
<p>看看咋样吧，没有进行非常严格的节食，希望养个几个月能养好</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[基于 FakeIP 的透明代理分流]]></title>
            <link>https://blog.shinya.click/fiddling/fake-ip-based-transparent-proxy</link>
            <guid isPermaLink="false">https://blog.shinya.click/fiddling/fake-ip-based-transparent-proxy</guid>
            <pubDate>Fri, 16 Aug 2024 15:53:00 GMT</pubDate>
            <description><![CDATA[基于 FakeIP 的透明代理分流方案旨在解决传统旁路由的单点故障、性能不足及复杂的端口映射问题。通过引入新的代理内核 sing-box，不仅提升了转发性能，还简化了配置流程。sing-box 提供了更丰富的协议支持，其优化效果明显优于原先的 clash，成为实现高效透明代理的新选择。虽然这也可以通过 clash 实现，但 sing-box 的引入无疑为用户带来了更灵活的体验。]]></description>
            <content:encoded><![CDATA[<h3 id="前言">前言<a href="#前言" class="heading-anchor-link" aria-label="Link to 前言"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h3>
<p><a href="/fiddling/debian-as-bypass-router">上篇文章</a> 中介绍了使用旁路由做局域网内透明代理的方案，对于大部分用户已经基本可用，但是该方案的 cons 也比较明显：</p>
<ol>
<li>存在单点故障的可能，由于 DHCP 下发的网关直接指向了旁路由，一旦旁路由的 clash 不可用，即使不需要科学的网站也无法访问</li>
<li>clash 包转发性能孱弱，远比不上硬件转发。而一旦网关设置为旁路由，由于 iptables 的设置，无论是否需要科学的流量都会走 clash 转发</li>
<li>由于存在旁路由网关，端口映射需要同时在主路由和旁路由配置</li>
</ol>
<p>正好，最近看到了一个新兴的代理内核 sing-box（似乎也不新兴了，只是从 clash archive 后才火起来）。看了下 <a href="https://sing-box.sagernet.org/configuration" target="_blank" rel="noopener noreferrer" data-umami-event="outbound-link-click" data-umami-event-url="https://sing-box.sagernet.org/configuration">Wiki</a>，支持的协议和功能十分全面，在性能优化上也比 clash 要好。新方案的代理核心就直接用 sing-box 了。</p>
<p>当然该方案也可以用 clash 实现就是了（</p>
<p>~~坏消息，sing-box 的 wiki 需要越墙才可访问~~</p>
<h3 id="方案思路">方案思路<a href="#方案思路" class="heading-anchor-link" aria-label="Link to 方案思路"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h3>
<p>sing-box 和 clash 都内置了 DNS 模块，实现了 DNS Server 了功能，且都有 FakeIP。关于 FakeIP，可以理解为当<mark>客户端进行 DNS 查询时，DNS 模块立刻响应一个假的 IP 地址，并在后台进行实际的 DNS 查询过程，并维护这个假 IP 地址和实际 IP 的映射关系。当后续客户端拿着这个 FakeIP 来建立连接发送数据时，网关即可根据这个映射关系向真实的 IP 发起请求。</mark>更具体的描述可见 <a href="https://datatracker.ietf.org/doc/html/rfc3089" target="_blank" rel="noopener noreferrer" data-umami-event="outbound-link-click" data-umami-event-url="https://datatracker.ietf.org/doc/html/rfc3089">RFC3089</a>。由于后续进行流量路由时需要用到 DNS 应答时存储的 FakeIP 和真实 IP 映射，所以单纯的 DNS Server 无法单独实现 FakeIP 机制。</p>
<p>由于 FakeIP 通常位于一个保留网段（大部分配置为 <code>198.18.0.0/15</code>），分流特征及其明显，分流也十分简单。我们可以直接让软路由代理的 DNS 模块仅对需要代理的域名响应 FakeIP，在主路由中配置下一跳，仅让目标 IP 为 FakeIP 的流量通过软路由代理，非 FakeIP 的流量正常转发。具体如下</p>
<figure><img src="https://blog-img.774352199.xyz/2025/e078ffe1fe41b2cbcb04b40a55cbbc56.png" alt="fakeIP 分流" loading="lazy" decoding="async" style="background-image:url(data:image/webp;base64,UklGRjIAAABXRUJQVlA4ICYAAAAwAQCdASoQAAkAAsBMJaQAA3AA/vZdG9wmb5KWxkVuxrfpXKEAAA==);background-size:cover;background-repeat:no-repeat"><figcaption>fakeIP 分流</figcaption></figure>
<div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre><code>1. 无须科学的域名
  1. 客户端发起 DNS 解析
  2. DNS 模块判断为无须科学，向国内 DNS 请求，返回 RealIP
  3. 客户端使用 RealIP 发起请求
  4. 主路由判断不是 FakeIP，走默认路由（直连）
  
2. 需要科学的域名
  1. 客户端发起 DNS 解析
  2. DNS 模块判断为需要科学，返回 FakeIP，并向国外 DNS 请求
  3. 客户端使用 FakeIP 发起请求
  4. 主路由判断是 FakeIP，路由流量到代理软件
  5. 代理软件根据 FakeIP 映射，通过出口节点向国外 IP 发起请求
</code></pre></div>
<p>这个方案解决了上篇文章的三个 cons：</p>
<ol>
<li>单点故障问题，避免了 sing-box 失效导致无法联网。基于上篇文章的方案，sing-box 的 DNS 解析应当位于 AdGuard 后，当 sing-box 失效时，AdGuard 发现上游 DNS 异常，会启用后备 DNS，即国内 DNS。由于不返回 FakeIP，所有流量在主路由分流时都会走默认路由</li>
<li>该方案无须科学的流量不会走代理软件转发，而是直接走路由转发</li>
<li>由于转发是由主路由路由表进行，所有客户端网关都为主路由，不存在二层 NAT，主路由端口映射不会失效</li>
</ol>
<h3 id="具体实现">具体实现<a href="#具体实现" class="heading-anchor-link" aria-label="Link to 具体实现"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h3>
<h4 id="主路由配置">主路由配置<a href="#主路由配置" class="heading-anchor-link" aria-label="Link to 主路由配置"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h4>
<p>首先在主路由配置下一跳网关，ikuai 的配置位于 流控分流 - 分流设置 - 端口分流，添加一条分流规则，分流方式选择下一跳网关，并填写你的软路由 IP，我的就是 192.168.7.2 了。并在目的地址中添加一条 198.18.0.0/15，其他配置保持默认即可</p>
<figure><img src="https://blog-img.774352199.xyz/2025/37f3bc2ebbd0f4f79e218c2a949a84c4.png" alt="下一跳网关" loading="lazy" decoding="async" style="background-image:url(data:image/webp;base64,UklGRjoAAABXRUJQVlA4IC4AAABwAQCdASoQAA0AAsBMJaV2AAGIAAD+9yazJ4KdGBKz9fIloxabkhXytl10AAAA);background-size:cover;background-repeat:no-repeat"><figcaption>下一跳网关</figcaption></figure>
<p>这样所有目标地址为 198.18.0.0/15 的流量经过主路由时都会被转发到 192.168.7.2 了</p>
<h4 id="sing-box-安装和配置">sing-box 安装和配置<a href="#sing-box-安装和配置" class="heading-anchor-link" aria-label="Link to sing-box 安装和配置"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h4>
<p>使用 <a href="/fiddling/debian-as-bypass-router">上篇文章</a> 的教程搭建好 AdGuard Home，上游 DNS 仍然设置为 127.0.0.1<div></div>。之后安装 sing-box，Debian 机器只需要一条命令：</p>
<div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">bash</span><span style="color:#032F62;--s-dark:#9ECBFF"> &#x3C;(</span><span style="color:#6F42C1;--s-dark:#B392F0">curl</span><span style="color:#005CC5;--s-dark:#79B8FF"> -fsSL</span><span style="color:#032F62;--s-dark:#9ECBFF"> https://sing-box.app/deb-install.sh)</span></span></code></pre></div>
<p>其他发行版的安装方法可在 <a href="https://sing-box.sagernet.org/installation/package-manager/#__tabbed_2_1" target="_blank" rel="noopener noreferrer" data-umami-event="outbound-link-click" data-umami-event-url="https://sing-box.sagernet.org/installation/package-manager/#__tabbed_2_1">https://sing-box.sagernet.org/installation/package-manager</a> 中找到。</p>
<p>安装程序会自动创建 systemd service。sing-box 的 systemd service 比较不走寻常路，定义位于 <code>/lib/systemd/system/sing-box.service</code>，编辑这个文件，在 ExecStart 前加上这三行：</p>
<div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">ExecStartPre</span><span style="color:#032F62;--s-dark:#9ECBFF">  =</span><span style="color:#032F62;--s-dark:#9ECBFF"> +/usr/bin/bash</span><span style="color:#032F62;--s-dark:#9ECBFF"> /etc/sing-box/clean.sh</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">ExecStartPost</span><span style="color:#032F62;--s-dark:#9ECBFF"> =</span><span style="color:#032F62;--s-dark:#9ECBFF"> +/usr/bin/bash</span><span style="color:#032F62;--s-dark:#9ECBFF"> /etc/sing-box/iptables.sh</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">ExecStopPost</span><span style="color:#032F62;--s-dark:#9ECBFF">  =</span><span style="color:#032F62;--s-dark:#9ECBFF"> +/usr/bin/bash</span><span style="color:#032F62;--s-dark:#9ECBFF"> /etc/sing-box/clean.sh</span></span></code></pre></div>
<p>这其实和上个方案类似，也是在启动时设置路由表，并在 sing-box 关闭时清除。sing-box 的所有配置都位于 <code>/etc/sing-box</code> 下，默认读取的配置文件也是 <code>/etc/sing-box/config.json</code>，所以我们也保持统一。</p>
<p>新建 <code>/etc/sing-box/iptables.sh</code> 和 <code>/etc/sing-box/clean.sh</code> 如下：</p>
<div class="code-collapse-wrapper"><input type="checkbox" id="toggle-tjf54wb" class="code-collapse-toggle" style="display: none;"><div class="code-collapse-preview"><div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0" class="code-preview"><code><span class="line"><span style="color:#6A737D;--s-dark:#6A737D">#!/usr/bin/env bash</span></span>
<span class="line"></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">set</span><span style="color:#005CC5;--s-dark:#79B8FF"> -ex</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D"># ENABLE ipv4 forward</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">sysctl</span><span style="color:#005CC5;--s-dark:#79B8FF"> -w</span><span style="color:#032F62;--s-dark:#9ECBFF"> net.ipv4.ip_forward=</span><span style="color:#005CC5;--s-dark:#79B8FF">1</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D"># ENABLE ipv6 forward</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">sysctl</span><span style="color:#005CC5;--s-dark:#79B8FF"> -w</span><span style="color:#032F62;--s-dark:#9ECBFF"> net.ipv6.conf.all.forwarding=</span><span style="color:#005CC5;--s-dark:#79B8FF">1</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">### IPv4 Routing Rules ###</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D"># ROUTE RULES</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">ip</span><span style="color:#032F62;--s-dark:#9ECBFF"> rule</span><span style="color:#032F62;--s-dark:#9ECBFF"> add</span><span style="color:#032F62;--s-dark:#9ECBFF"> fwmark</span><span style="color:#005CC5;--s-dark:#79B8FF"> 666</span><span style="color:#032F62;--s-dark:#9ECBFF"> lookup</span><span style="color:#005CC5;--s-dark:#79B8FF"> 666</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">ip</span><span style="color:#032F62;--s-dark:#9ECBFF"> route</span><span style="color:#032F62;--s-dark:#9ECBFF"> add</span><span style="color:#032F62;--s-dark:#9ECBFF"> local</span><span style="color:#032F62;--s-dark:#9ECBFF"> 0.0.0.0/0</span><span style="color:#032F62;--s-dark:#9ECBFF"> dev</span><span style="color:#032F62;--s-dark:#9ECBFF"> lo</span><span style="color:#032F62;--s-dark:#9ECBFF"> table</span><span style="color:#005CC5;--s-dark:#79B8FF"> 666</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D"># clash 链负责处理转发流量</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">iptables</span><span style="color:#005CC5;--s-dark:#79B8FF"> -t</span><span style="color:#032F62;--s-dark:#9ECBFF"> mangle</span><span style="color:#005CC5;--s-dark:#79B8FF"> -N</span><span style="color:#032F62;--s-dark:#9ECBFF"> clash</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D"># 跳过内网流量</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">iptables</span><span style="color:#005CC5;--s-dark:#79B8FF"> -t</span><span style="color:#032F62;--s-dark:#9ECBFF"> mangle</span><span style="color:#005CC5;--s-dark:#79B8FF"> -A</span><span style="color:#032F62;--s-dark:#9ECBFF"> clash</span><span style="color:#005CC5;--s-dark:#79B8FF"> -d</span><span style="color:#032F62;--s-dark:#9ECBFF"> 0.0.0.0/8</span><span style="color:#005CC5;--s-dark:#79B8FF"> -j</span><span style="color:#032F62;--s-dark:#9ECBFF"> RETURN</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">iptables</span><span style="color:#005CC5;--s-dark:#79B8FF"> -t</span><span style="color:#032F62;--s-dark:#9ECBFF"> mangle</span><span style="color:#005CC5;--s-dark:#79B8FF"> -A</span><span style="color:#032F62;--s-dark:#9ECBFF"> clash</span><span style="color:#005CC5;--s-dark:#79B8FF"> -d</span><span style="color:#032F62;--s-dark:#9ECBFF"> 127.0.0.0/8</span><span style="color:#005CC5;--s-dark:#79B8FF"> -j</span><span style="color:#032F62;--s-dark:#9ECBFF"> RETURN</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">iptables</span><span style="color:#005CC5;--s-dark:#79B8FF"> -t</span><span style="color:#032F62;--s-dark:#9ECBFF"> mangle</span><span style="color:#005CC5;--s-dark:#79B8FF"> -A</span><span style="color:#032F62;--s-dark:#9ECBFF"> clash</span><span style="color:#005CC5;--s-dark:#79B8FF"> -d</span><span style="color:#032F62;--s-dark:#9ECBFF"> 10.0.0.0/8</span><span style="color:#005CC5;--s-dark:#79B8FF"> -j</span><span style="color:#032F62;--s-dark:#9ECBFF"> RETURN</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">iptables</span><span style="color:#005CC5;--s-dark:#79B8FF"> -t</span><span style="color:#032F62;--s-dark:#9ECBFF"> mangle</span><span style="color:#005CC5;--s-dark:#79B8FF"> -A</span><span style="color:#032F62;--s-dark:#9ECBFF"> clash</span><span style="color:#005CC5;--s-dark:#79B8FF"> -d</span><span style="color:#032F62;--s-dark:#9ECBFF"> 172.16.0.0/12</span><span style="color:#005CC5;--s-dark:#79B8FF"> -j</span><span style="color:#032F62;--s-dark:#9ECBFF"> RETURN</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">iptables</span><span style="color:#005CC5;--s-dark:#79B8FF"> -t</span><span style="color:#032F62;--s-dark:#9ECBFF"> mangle</span><span style="color:#005CC5;--s-dark:#79B8FF"> -A</span><span style="color:#032F62;--s-dark:#9ECBFF"> clash</span><span style="color:#005CC5;--s-dark:#79B8FF"> -d</span><span style="color:#032F62;--s-dark:#9ECBFF"> 192.168.0.0/16</span><span style="color:#005CC5;--s-dark:#79B8FF"> -j</span><span style="color:#032F62;--s-dark:#9ECBFF"> RETURN</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">iptables</span><span style="color:#005CC5;--s-dark:#79B8FF"> -t</span><span style="color:#032F62;--s-dark:#9ECBFF"> mangle</span><span style="color:#005CC5;--s-dark:#79B8FF"> -A</span><span style="color:#032F62;--s-dark:#9ECBFF"> clash</span><span style="color:#005CC5;--s-dark:#79B8FF"> -d</span><span style="color:#032F62;--s-dark:#9ECBFF"> 169.254.0.0/16</span><span style="color:#005CC5;--s-dark:#79B8FF"> -j</span><span style="color:#032F62;--s-dark:#9ECBFF"> RETURN</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">iptables</span><span style="color:#005CC5;--s-dark:#79B8FF"> -t</span><span style="color:#032F62;--s-dark:#9ECBFF"> mangle</span><span style="color:#005CC5;--s-dark:#79B8FF"> -A</span><span style="color:#032F62;--s-dark:#9ECBFF"> clash</span><span style="color:#005CC5;--s-dark:#79B8FF"> -d</span><span style="color:#032F62;--s-dark:#9ECBFF"> 224.0.0.0/4</span><span style="color:#005CC5;--s-dark:#79B8FF"> -j</span><span style="color:#032F62;--s-dark:#9ECBFF"> RETURN</span></span>
</code></pre></div><div class="code-collapse-gradient"></div><label class="code-collapse-expand" for="toggle-tjf54wb"><svg class="code-collapse-icon" width="32" height="24" viewBox="0 0 32 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="4,9 12,17 20,9"></polyline></svg></label></div><div class="code-collapse-full"><div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#6A737D;--s-dark:#6A737D">#!/usr/bin/env bash</span></span>
<span class="line"></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">set</span><span style="color:#005CC5;--s-dark:#79B8FF"> -ex</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D"># ENABLE ipv4 forward</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">sysctl</span><span style="color:#005CC5;--s-dark:#79B8FF"> -w</span><span style="color:#032F62;--s-dark:#9ECBFF"> net.ipv4.ip_forward=</span><span style="color:#005CC5;--s-dark:#79B8FF">1</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D"># ENABLE ipv6 forward</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">sysctl</span><span style="color:#005CC5;--s-dark:#79B8FF"> -w</span><span style="color:#032F62;--s-dark:#9ECBFF"> net.ipv6.conf.all.forwarding=</span><span style="color:#005CC5;--s-dark:#79B8FF">1</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">### IPv4 Routing Rules ###</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D"># ROUTE RULES</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">ip</span><span style="color:#032F62;--s-dark:#9ECBFF"> rule</span><span style="color:#032F62;--s-dark:#9ECBFF"> add</span><span style="color:#032F62;--s-dark:#9ECBFF"> fwmark</span><span style="color:#005CC5;--s-dark:#79B8FF"> 666</span><span style="color:#032F62;--s-dark:#9ECBFF"> lookup</span><span style="color:#005CC5;--s-dark:#79B8FF"> 666</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">ip</span><span style="color:#032F62;--s-dark:#9ECBFF"> route</span><span style="color:#032F62;--s-dark:#9ECBFF"> add</span><span style="color:#032F62;--s-dark:#9ECBFF"> local</span><span style="color:#032F62;--s-dark:#9ECBFF"> 0.0.0.0/0</span><span style="color:#032F62;--s-dark:#9ECBFF"> dev</span><span style="color:#032F62;--s-dark:#9ECBFF"> lo</span><span style="color:#032F62;--s-dark:#9ECBFF"> table</span><span style="color:#005CC5;--s-dark:#79B8FF"> 666</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D"># clash 链负责处理转发流量</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">iptables</span><span style="color:#005CC5;--s-dark:#79B8FF"> -t</span><span style="color:#032F62;--s-dark:#9ECBFF"> mangle</span><span style="color:#005CC5;--s-dark:#79B8FF"> -N</span><span style="color:#032F62;--s-dark:#9ECBFF"> clash</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D"># 跳过内网流量</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">iptables</span><span style="color:#005CC5;--s-dark:#79B8FF"> -t</span><span style="color:#032F62;--s-dark:#9ECBFF"> mangle</span><span style="color:#005CC5;--s-dark:#79B8FF"> -A</span><span style="color:#032F62;--s-dark:#9ECBFF"> clash</span><span style="color:#005CC5;--s-dark:#79B8FF"> -d</span><span style="color:#032F62;--s-dark:#9ECBFF"> 0.0.0.0/8</span><span style="color:#005CC5;--s-dark:#79B8FF"> -j</span><span style="color:#032F62;--s-dark:#9ECBFF"> RETURN</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">iptables</span><span style="color:#005CC5;--s-dark:#79B8FF"> -t</span><span style="color:#032F62;--s-dark:#9ECBFF"> mangle</span><span style="color:#005CC5;--s-dark:#79B8FF"> -A</span><span style="color:#032F62;--s-dark:#9ECBFF"> clash</span><span style="color:#005CC5;--s-dark:#79B8FF"> -d</span><span style="color:#032F62;--s-dark:#9ECBFF"> 127.0.0.0/8</span><span style="color:#005CC5;--s-dark:#79B8FF"> -j</span><span style="color:#032F62;--s-dark:#9ECBFF"> RETURN</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">iptables</span><span style="color:#005CC5;--s-dark:#79B8FF"> -t</span><span style="color:#032F62;--s-dark:#9ECBFF"> mangle</span><span style="color:#005CC5;--s-dark:#79B8FF"> -A</span><span style="color:#032F62;--s-dark:#9ECBFF"> clash</span><span style="color:#005CC5;--s-dark:#79B8FF"> -d</span><span style="color:#032F62;--s-dark:#9ECBFF"> 10.0.0.0/8</span><span style="color:#005CC5;--s-dark:#79B8FF"> -j</span><span style="color:#032F62;--s-dark:#9ECBFF"> RETURN</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">iptables</span><span style="color:#005CC5;--s-dark:#79B8FF"> -t</span><span style="color:#032F62;--s-dark:#9ECBFF"> mangle</span><span style="color:#005CC5;--s-dark:#79B8FF"> -A</span><span style="color:#032F62;--s-dark:#9ECBFF"> clash</span><span style="color:#005CC5;--s-dark:#79B8FF"> -d</span><span style="color:#032F62;--s-dark:#9ECBFF"> 172.16.0.0/12</span><span style="color:#005CC5;--s-dark:#79B8FF"> -j</span><span style="color:#032F62;--s-dark:#9ECBFF"> RETURN</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">iptables</span><span style="color:#005CC5;--s-dark:#79B8FF"> -t</span><span style="color:#032F62;--s-dark:#9ECBFF"> mangle</span><span style="color:#005CC5;--s-dark:#79B8FF"> -A</span><span style="color:#032F62;--s-dark:#9ECBFF"> clash</span><span style="color:#005CC5;--s-dark:#79B8FF"> -d</span><span style="color:#032F62;--s-dark:#9ECBFF"> 192.168.0.0/16</span><span style="color:#005CC5;--s-dark:#79B8FF"> -j</span><span style="color:#032F62;--s-dark:#9ECBFF"> RETURN</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">iptables</span><span style="color:#005CC5;--s-dark:#79B8FF"> -t</span><span style="color:#032F62;--s-dark:#9ECBFF"> mangle</span><span style="color:#005CC5;--s-dark:#79B8FF"> -A</span><span style="color:#032F62;--s-dark:#9ECBFF"> clash</span><span style="color:#005CC5;--s-dark:#79B8FF"> -d</span><span style="color:#032F62;--s-dark:#9ECBFF"> 169.254.0.0/16</span><span style="color:#005CC5;--s-dark:#79B8FF"> -j</span><span style="color:#032F62;--s-dark:#9ECBFF"> RETURN</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">iptables</span><span style="color:#005CC5;--s-dark:#79B8FF"> -t</span><span style="color:#032F62;--s-dark:#9ECBFF"> mangle</span><span style="color:#005CC5;--s-dark:#79B8FF"> -A</span><span style="color:#032F62;--s-dark:#9ECBFF"> clash</span><span style="color:#005CC5;--s-dark:#79B8FF"> -d</span><span style="color:#032F62;--s-dark:#9ECBFF"> 224.0.0.0/4</span><span style="color:#005CC5;--s-dark:#79B8FF"> -j</span><span style="color:#032F62;--s-dark:#9ECBFF"> RETURN</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">iptables</span><span style="color:#005CC5;--s-dark:#79B8FF"> -t</span><span style="color:#032F62;--s-dark:#9ECBFF"> mangle</span><span style="color:#005CC5;--s-dark:#79B8FF"> -A</span><span style="color:#032F62;--s-dark:#9ECBFF"> clash</span><span style="color:#005CC5;--s-dark:#79B8FF"> -d</span><span style="color:#032F62;--s-dark:#9ECBFF"> 240.0.0.0/4</span><span style="color:#005CC5;--s-dark:#79B8FF"> -j</span><span style="color:#032F62;--s-dark:#9ECBFF"> RETURN</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D"># 需代理的 IP 转向到 7893 端口，并打上 mark</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">iptables</span><span style="color:#005CC5;--s-dark:#79B8FF"> -t</span><span style="color:#032F62;--s-dark:#9ECBFF"> mangle</span><span style="color:#005CC5;--s-dark:#79B8FF"> -A</span><span style="color:#032F62;--s-dark:#9ECBFF"> clash</span><span style="color:#005CC5;--s-dark:#79B8FF"> -d</span><span style="color:#032F62;--s-dark:#9ECBFF"> 198.18.0.0/15</span><span style="color:#005CC5;--s-dark:#79B8FF"> -p</span><span style="color:#032F62;--s-dark:#9ECBFF"> tcp</span><span style="color:#005CC5;--s-dark:#79B8FF"> -j</span><span style="color:#032F62;--s-dark:#9ECBFF"> TPROXY</span><span style="color:#005CC5;--s-dark:#79B8FF"> --on-port</span><span style="color:#005CC5;--s-dark:#79B8FF"> 7893</span><span style="color:#005CC5;--s-dark:#79B8FF"> --tproxy-mark</span><span style="color:#005CC5;--s-dark:#79B8FF"> 666</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">iptables</span><span style="color:#005CC5;--s-dark:#79B8FF"> -t</span><span style="color:#032F62;--s-dark:#9ECBFF"> mangle</span><span style="color:#005CC5;--s-dark:#79B8FF"> -A</span><span style="color:#032F62;--s-dark:#9ECBFF"> clash</span><span style="color:#005CC5;--s-dark:#79B8FF"> -d</span><span style="color:#032F62;--s-dark:#9ECBFF"> 198.18.0.0/15</span><span style="color:#005CC5;--s-dark:#79B8FF"> -p</span><span style="color:#032F62;--s-dark:#9ECBFF"> udp</span><span style="color:#005CC5;--s-dark:#79B8FF"> -j</span><span style="color:#032F62;--s-dark:#9ECBFF"> TPROXY</span><span style="color:#005CC5;--s-dark:#79B8FF"> --on-port</span><span style="color:#005CC5;--s-dark:#79B8FF"> 7893</span><span style="color:#005CC5;--s-dark:#79B8FF"> --tproxy-mark</span><span style="color:#005CC5;--s-dark:#79B8FF"> 666</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D"># 剩余流量正常处理</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">iptables</span><span style="color:#005CC5;--s-dark:#79B8FF"> -t</span><span style="color:#032F62;--s-dark:#9ECBFF"> mangle</span><span style="color:#005CC5;--s-dark:#79B8FF"> -A</span><span style="color:#032F62;--s-dark:#9ECBFF"> clash</span><span style="color:#005CC5;--s-dark:#79B8FF"> -j</span><span style="color:#032F62;--s-dark:#9ECBFF"> RETURN</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D"># 最后让所有流量通过 clash 链进行处理</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">iptables</span><span style="color:#005CC5;--s-dark:#79B8FF"> -t</span><span style="color:#032F62;--s-dark:#9ECBFF"> mangle</span><span style="color:#005CC5;--s-dark:#79B8FF"> -A</span><span style="color:#032F62;--s-dark:#9ECBFF"> PREROUTING</span><span style="color:#005CC5;--s-dark:#79B8FF"> -j</span><span style="color:#032F62;--s-dark:#9ECBFF"> clash</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D"># clash_local 链负责处理网关本身发出的流量</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">iptables</span><span style="color:#005CC5;--s-dark:#79B8FF"> -t</span><span style="color:#032F62;--s-dark:#9ECBFF"> mangle</span><span style="color:#005CC5;--s-dark:#79B8FF"> -N</span><span style="color:#032F62;--s-dark:#9ECBFF"> clash_local</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D"># 跳过内网流量</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">iptables</span><span style="color:#005CC5;--s-dark:#79B8FF"> -t</span><span style="color:#032F62;--s-dark:#9ECBFF"> mangle</span><span style="color:#005CC5;--s-dark:#79B8FF"> -A</span><span style="color:#032F62;--s-dark:#9ECBFF"> clash_local</span><span style="color:#005CC5;--s-dark:#79B8FF"> -d</span><span style="color:#032F62;--s-dark:#9ECBFF"> 0.0.0.0/8</span><span style="color:#005CC5;--s-dark:#79B8FF"> -j</span><span style="color:#032F62;--s-dark:#9ECBFF"> RETURN</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">iptables</span><span style="color:#005CC5;--s-dark:#79B8FF"> -t</span><span style="color:#032F62;--s-dark:#9ECBFF"> mangle</span><span style="color:#005CC5;--s-dark:#79B8FF"> -A</span><span style="color:#032F62;--s-dark:#9ECBFF"> clash_local</span><span style="color:#005CC5;--s-dark:#79B8FF"> -d</span><span style="color:#032F62;--s-dark:#9ECBFF"> 127.0.0.0/8</span><span style="color:#005CC5;--s-dark:#79B8FF"> -j</span><span style="color:#032F62;--s-dark:#9ECBFF"> RETURN</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">iptables</span><span style="color:#005CC5;--s-dark:#79B8FF"> -t</span><span style="color:#032F62;--s-dark:#9ECBFF"> mangle</span><span style="color:#005CC5;--s-dark:#79B8FF"> -A</span><span style="color:#032F62;--s-dark:#9ECBFF"> clash_local</span><span style="color:#005CC5;--s-dark:#79B8FF"> -d</span><span style="color:#032F62;--s-dark:#9ECBFF"> 10.0.0.0/8</span><span style="color:#005CC5;--s-dark:#79B8FF"> -j</span><span style="color:#032F62;--s-dark:#9ECBFF"> RETURN</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">iptables</span><span style="color:#005CC5;--s-dark:#79B8FF"> -t</span><span style="color:#032F62;--s-dark:#9ECBFF"> mangle</span><span style="color:#005CC5;--s-dark:#79B8FF"> -A</span><span style="color:#032F62;--s-dark:#9ECBFF"> clash_local</span><span style="color:#005CC5;--s-dark:#79B8FF"> -d</span><span style="color:#032F62;--s-dark:#9ECBFF"> 172.16.0.0/12</span><span style="color:#005CC5;--s-dark:#79B8FF"> -j</span><span style="color:#032F62;--s-dark:#9ECBFF"> RETURN</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">iptables</span><span style="color:#005CC5;--s-dark:#79B8FF"> -t</span><span style="color:#032F62;--s-dark:#9ECBFF"> mangle</span><span style="color:#005CC5;--s-dark:#79B8FF"> -A</span><span style="color:#032F62;--s-dark:#9ECBFF"> clash_local</span><span style="color:#005CC5;--s-dark:#79B8FF"> -d</span><span style="color:#032F62;--s-dark:#9ECBFF"> 192.168.0.0/16</span><span style="color:#005CC5;--s-dark:#79B8FF"> -j</span><span style="color:#032F62;--s-dark:#9ECBFF"> RETURN</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">iptables</span><span style="color:#005CC5;--s-dark:#79B8FF"> -t</span><span style="color:#032F62;--s-dark:#9ECBFF"> mangle</span><span style="color:#005CC5;--s-dark:#79B8FF"> -A</span><span style="color:#032F62;--s-dark:#9ECBFF"> clash_local</span><span style="color:#005CC5;--s-dark:#79B8FF"> -d</span><span style="color:#032F62;--s-dark:#9ECBFF"> 169.254.0.0/16</span><span style="color:#005CC5;--s-dark:#79B8FF"> -j</span><span style="color:#032F62;--s-dark:#9ECBFF"> RETURN</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">iptables</span><span style="color:#005CC5;--s-dark:#79B8FF"> -t</span><span style="color:#032F62;--s-dark:#9ECBFF"> mangle</span><span style="color:#005CC5;--s-dark:#79B8FF"> -A</span><span style="color:#032F62;--s-dark:#9ECBFF"> clash_local</span><span style="color:#005CC5;--s-dark:#79B8FF"> -d</span><span style="color:#032F62;--s-dark:#9ECBFF"> 224.0.0.0/4</span><span style="color:#005CC5;--s-dark:#79B8FF"> -j</span><span style="color:#032F62;--s-dark:#9ECBFF"> RETURN</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">iptables</span><span style="color:#005CC5;--s-dark:#79B8FF"> -t</span><span style="color:#032F62;--s-dark:#9ECBFF"> mangle</span><span style="color:#005CC5;--s-dark:#79B8FF"> -A</span><span style="color:#032F62;--s-dark:#9ECBFF"> clash_local</span><span style="color:#005CC5;--s-dark:#79B8FF"> -d</span><span style="color:#032F62;--s-dark:#9ECBFF"> 240.0.0.0/4</span><span style="color:#005CC5;--s-dark:#79B8FF"> -j</span><span style="color:#032F62;--s-dark:#9ECBFF"> RETURN</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D"># 为本机发出的流量打 mark</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">iptables</span><span style="color:#005CC5;--s-dark:#79B8FF"> -t</span><span style="color:#032F62;--s-dark:#9ECBFF"> mangle</span><span style="color:#005CC5;--s-dark:#79B8FF"> -A</span><span style="color:#032F62;--s-dark:#9ECBFF"> clash_local</span><span style="color:#005CC5;--s-dark:#79B8FF"> -p</span><span style="color:#032F62;--s-dark:#9ECBFF"> tcp</span><span style="color:#005CC5;--s-dark:#79B8FF"> -j</span><span style="color:#032F62;--s-dark:#9ECBFF"> MARK</span><span style="color:#005CC5;--s-dark:#79B8FF"> --set-mark</span><span style="color:#005CC5;--s-dark:#79B8FF"> 666</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">iptables</span><span style="color:#005CC5;--s-dark:#79B8FF"> -t</span><span style="color:#032F62;--s-dark:#9ECBFF"> mangle</span><span style="color:#005CC5;--s-dark:#79B8FF"> -A</span><span style="color:#032F62;--s-dark:#9ECBFF"> clash_local</span><span style="color:#005CC5;--s-dark:#79B8FF"> -p</span><span style="color:#032F62;--s-dark:#9ECBFF"> udp</span><span style="color:#005CC5;--s-dark:#79B8FF"> -j</span><span style="color:#032F62;--s-dark:#9ECBFF"> MARK</span><span style="color:#005CC5;--s-dark:#79B8FF"> --set-mark</span><span style="color:#005CC5;--s-dark:#79B8FF"> 666</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D"># 让本机发出的流量跳转到 clash_local</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D"># clash_local 链会为本机流量打 mark, 打过 mark 的流量会重新回到 PREROUTING 上</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">iptables</span><span style="color:#005CC5;--s-dark:#79B8FF"> -t</span><span style="color:#032F62;--s-dark:#9ECBFF"> mangle</span><span style="color:#005CC5;--s-dark:#79B8FF"> -A</span><span style="color:#032F62;--s-dark:#9ECBFF"> OUTPUT</span><span style="color:#005CC5;--s-dark:#79B8FF"> -j</span><span style="color:#032F62;--s-dark:#9ECBFF"> clash_local</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D"># 修复 ICMP(ping)</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D"># 这并不能保证 ping 结果有效 (clash 等不支持转发 ICMP), 只是让它有返回结果而已</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D"># --to-destination 设置为一个可达的地址即可</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">sysctl</span><span style="color:#005CC5;--s-dark:#79B8FF"> -w</span><span style="color:#032F62;--s-dark:#9ECBFF"> net.ipv4.conf.all.route_localnet=</span><span style="color:#005CC5;--s-dark:#79B8FF">1</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">iptables</span><span style="color:#005CC5;--s-dark:#79B8FF"> -t</span><span style="color:#032F62;--s-dark:#9ECBFF"> nat</span><span style="color:#005CC5;--s-dark:#79B8FF"> -A</span><span style="color:#032F62;--s-dark:#9ECBFF"> PREROUTING</span><span style="color:#005CC5;--s-dark:#79B8FF"> -p</span><span style="color:#032F62;--s-dark:#9ECBFF"> icmp</span><span style="color:#005CC5;--s-dark:#79B8FF"> -d</span><span style="color:#032F62;--s-dark:#9ECBFF"> 198.18.0.0/16</span><span style="color:#005CC5;--s-dark:#79B8FF"> -j</span><span style="color:#032F62;--s-dark:#9ECBFF"> DNAT</span><span style="color:#005CC5;--s-dark:#79B8FF"> --to-destination</span><span style="color:#005CC5;--s-dark:#79B8FF"> 127.0.0.1</span></span></code><label class="code-collapse-collapse" for="toggle-tjf54wb"><svg class="code-collapse-icon" width="32" height="24" viewBox="0 0 32 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20,15 12,7 4,15"></polyline></svg></label></pre></div></div></div>
<div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#6A737D;--s-dark:#6A737D">#!/usr/bin/env bash</span></span>
<span class="line"></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">set</span><span style="color:#005CC5;--s-dark:#79B8FF"> -ex</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">ip</span><span style="color:#032F62;--s-dark:#9ECBFF"> rule</span><span style="color:#032F62;--s-dark:#9ECBFF"> del</span><span style="color:#032F62;--s-dark:#9ECBFF"> fwmark</span><span style="color:#005CC5;--s-dark:#79B8FF"> 666</span><span style="color:#032F62;--s-dark:#9ECBFF"> table</span><span style="color:#005CC5;--s-dark:#79B8FF"> 666</span><span style="color:#D73A49;--s-dark:#F97583"> ||</span><span style="color:#005CC5;--s-dark:#79B8FF"> true</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">ip</span><span style="color:#032F62;--s-dark:#9ECBFF"> route</span><span style="color:#032F62;--s-dark:#9ECBFF"> del</span><span style="color:#032F62;--s-dark:#9ECBFF"> local</span><span style="color:#032F62;--s-dark:#9ECBFF"> 0.0.0.0/0</span><span style="color:#032F62;--s-dark:#9ECBFF"> dev</span><span style="color:#032F62;--s-dark:#9ECBFF"> lo</span><span style="color:#032F62;--s-dark:#9ECBFF"> table</span><span style="color:#005CC5;--s-dark:#79B8FF"> 666</span><span style="color:#D73A49;--s-dark:#F97583"> ||</span><span style="color:#005CC5;--s-dark:#79B8FF"> true</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">iptables</span><span style="color:#005CC5;--s-dark:#79B8FF"> -t</span><span style="color:#032F62;--s-dark:#9ECBFF"> nat</span><span style="color:#005CC5;--s-dark:#79B8FF"> -F</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">iptables</span><span style="color:#005CC5;--s-dark:#79B8FF"> -t</span><span style="color:#032F62;--s-dark:#9ECBFF"> nat</span><span style="color:#005CC5;--s-dark:#79B8FF"> -X</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">iptables</span><span style="color:#005CC5;--s-dark:#79B8FF"> -t</span><span style="color:#032F62;--s-dark:#9ECBFF"> mangle</span><span style="color:#005CC5;--s-dark:#79B8FF"> -F</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">iptables</span><span style="color:#005CC5;--s-dark:#79B8FF"> -t</span><span style="color:#032F62;--s-dark:#9ECBFF"> mangle</span><span style="color:#005CC5;--s-dark:#79B8FF"> -X</span><span style="color:#032F62;--s-dark:#9ECBFF"> clash</span><span style="color:#D73A49;--s-dark:#F97583"> ||</span><span style="color:#005CC5;--s-dark:#79B8FF"> true</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">iptables</span><span style="color:#005CC5;--s-dark:#79B8FF"> -t</span><span style="color:#032F62;--s-dark:#9ECBFF"> mangle</span><span style="color:#005CC5;--s-dark:#79B8FF"> -X</span><span style="color:#032F62;--s-dark:#9ECBFF"> clash_local</span><span style="color:#D73A49;--s-dark:#F97583"> ||</span><span style="color:#005CC5;--s-dark:#79B8FF"> true</span></span></code></pre></div>
<p>iptables.sh 其实和上篇文章的 iptables.sh 高度相似，只是最终 clash 链的最终处理变为了：将目标地址为 198.18.0.0/15 的流量转发到 7893 的 tproxy 端口，其他流量则走默认规则。实际上就是将被主路由转发的 FakeIP 流量交给 sing-box 处理了。clean.sh 则完全没有变化。</p>
<p>这里两个处理链：clash 和 clash_local，我还是保留了 clash 的名称，因为实际上就是从 clash 方案简单修改来的。（偷懒）</p>
<p>接着就是 sing-box 的配置文件，我这里给出一份配置模板：</p>
<div class="code-collapse-wrapper"><input type="checkbox" id="toggle-0san92l" class="code-collapse-toggle" style="display: none;"><div class="code-collapse-preview"><div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0" class="code-preview"><code><span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">{</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">  "log"</span><span style="color:#24292E;--s-dark:#E1E4E8">: {</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">    "level"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">"info"</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">    "output"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">"box.log"</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">    "timestamp"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">true</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">  },</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">  "dns"</span><span style="color:#24292E;--s-dark:#E1E4E8">: {</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">    "servers"</span><span style="color:#24292E;--s-dark:#E1E4E8">: [</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">      {</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">        "tag"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">"cloudflare"</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">        "address"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">"tls://1.1.1.1"</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">        "detour"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">"🌍 外网"</span><span style="color:#6A737D;--s-dark:#6A737D"> // 改为你的代理节点 tag</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">      },</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">      {</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">        "tag"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">"local"</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">        "address"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">"223.5.5.5"</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">        "detour"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">"DIRECT"</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">      },</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">      {</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">        "tag"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">"dns-fakeip"</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">        "address"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">"fakeip"</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">      },</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">      {</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">        "tag"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">"block"</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">        "address"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">"rcode://success"</span></span>
</code></pre></div><div class="code-collapse-gradient"></div><label class="code-collapse-expand" for="toggle-0san92l"><svg class="code-collapse-icon" width="32" height="24" viewBox="0 0 32 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="4,9 12,17 20,9"></polyline></svg></label></div><div class="code-collapse-full"><div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">{</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">  "log"</span><span style="color:#24292E;--s-dark:#E1E4E8">: {</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">    "level"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">"info"</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">    "output"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">"box.log"</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">    "timestamp"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">true</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">  },</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">  "dns"</span><span style="color:#24292E;--s-dark:#E1E4E8">: {</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">    "servers"</span><span style="color:#24292E;--s-dark:#E1E4E8">: [</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">      {</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">        "tag"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">"cloudflare"</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">        "address"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">"tls://1.1.1.1"</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">        "detour"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">"🌍 外网"</span><span style="color:#6A737D;--s-dark:#6A737D"> // 改为你的代理节点 tag</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">      },</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">      {</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">        "tag"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">"local"</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">        "address"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">"223.5.5.5"</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">        "detour"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">"DIRECT"</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">      },</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">      {</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">        "tag"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">"dns-fakeip"</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">        "address"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">"fakeip"</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">      },</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">      {</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">        "tag"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">"block"</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">        "address"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">"rcode://success"</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">      }</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    ],</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">    "rules"</span><span style="color:#24292E;--s-dark:#E1E4E8">: [</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">      {</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">        "server"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">"block"</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">        "query_type"</span><span style="color:#24292E;--s-dark:#E1E4E8">: [</span></span>
<span class="line"><span style="color:#032F62;--s-dark:#9ECBFF">          "HTTPS"</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#032F62;--s-dark:#9ECBFF">          "SVCB"</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        ]</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">      },</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">      {</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">        "server"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">"local"</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">        "outbound"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">"any"</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">      },</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">      {</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">        "server"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">"local"</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">        "rewrite_ttl"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">10</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">        "type"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">"logical"</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">        "mode"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">"and"</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">        "rules"</span><span style="color:#24292E;--s-dark:#E1E4E8">: [</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">          {</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            "rule_set"</span><span style="color:#24292E;--s-dark:#E1E4E8">: [</span></span>
<span class="line"><span style="color:#032F62;--s-dark:#9ECBFF">              "geosite-geolocation-!cn"</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            ],</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            "invert"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">true</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">          },</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">          {</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            "rule_set"</span><span style="color:#24292E;--s-dark:#E1E4E8">: [</span></span>
<span class="line"><span style="color:#032F62;--s-dark:#9ECBFF">              "geosite-cn"</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#032F62;--s-dark:#9ECBFF">              "geosite-category-companies@cn"</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#032F62;--s-dark:#9ECBFF">              "geoip-cn"</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            ]</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">          }</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        ]</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">      },</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">      {</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">        "server"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">"dns-fakeip"</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">        "rewrite_ttl"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">1</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">        "query_type"</span><span style="color:#24292E;--s-dark:#E1E4E8">: [</span></span>
<span class="line"><span style="color:#032F62;--s-dark:#9ECBFF">          "A"</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#032F62;--s-dark:#9ECBFF">          "AAAA"</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        ]</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">      }</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    ],</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">    "strategy"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">"ipv4_only"</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">    "fakeip"</span><span style="color:#24292E;--s-dark:#E1E4E8">: {</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">      "enabled"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">true</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">      "inet4_range"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">"198.18.0.0/15"</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">  },</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">  "inbounds"</span><span style="color:#24292E;--s-dark:#E1E4E8">: [</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    {</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">      "type"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">"tproxy"</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">      "tag"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">"tproxy-in"</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">      "listen"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">"::"</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">      "listen_port"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">7893</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">      "tcp_fast_open"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">true</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">      "udp_fragment"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">true</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">      "sniff"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">true</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    },</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    {</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">      "type"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">"mixed"</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">      "tag"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">"mixed-in"</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">      "listen"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">"::"</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">      "listen_port"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">7890</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">      "tcp_fast_open"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">true</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">      "udp_fragment"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">true</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">      "sniff"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">true</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    },</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    {</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">      "type"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">"direct"</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">      "tag"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">"dns-in"</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">      "listen"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">"::"</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">      "listen_port"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">1053</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">  ],</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">  "outbounds"</span><span style="color:#24292E;--s-dark:#E1E4E8">: [</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    {</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">      "type"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">"direct"</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">      "tag"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">"DIRECT"</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    },</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    {</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">      "type"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">"block"</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">      "tag"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">"REJECT"</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    },</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    {</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">      "type"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">"dns"</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">      "tag"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">"dns-out"</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    },</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">    // 此处填写你的代理节点</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">  ],</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">  "route"</span><span style="color:#24292E;--s-dark:#E1E4E8">: {</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">    "rules"</span><span style="color:#24292E;--s-dark:#E1E4E8">: [</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">      {</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">        "inbound"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">"dns-in"</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">        "outbound"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">"dns-out"</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">      },</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">      {</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">        "protocol"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">"dns"</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">        "outbound"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">"dns-out"</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">      },</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">      {</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">        "outbound"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">"DIRECT"</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">        "type"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">"logical"</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">        "mode"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">"and"</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">        "rules"</span><span style="color:#24292E;--s-dark:#E1E4E8">: [</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">          {</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            "rule_set"</span><span style="color:#24292E;--s-dark:#E1E4E8">: [</span></span>
<span class="line"><span style="color:#032F62;--s-dark:#9ECBFF">              "geosite-geolocation-!cn"</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            ],</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            "invert"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">true</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">          },</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">          {</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">            "rule_set"</span><span style="color:#24292E;--s-dark:#E1E4E8">: [</span></span>
<span class="line"><span style="color:#032F62;--s-dark:#9ECBFF">              "geosite-cn"</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#032F62;--s-dark:#9ECBFF">              "geosite-category-companies@cn"</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#032F62;--s-dark:#9ECBFF">              "geoip-cn"</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            ]</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">          }</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        ]</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">      }</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    ],</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">    "rule_set"</span><span style="color:#24292E;--s-dark:#E1E4E8">: [</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">      {</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">        "type"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">"remote"</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">        "tag"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">"geoip-cn"</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">        "format"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">"binary"</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">        "url"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">"https://cdn.jsdelivr.net/gh/SagerNet/sing-geoip@rule-set/geoip-cn.srs"</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">        "download_detour"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">"DIRECT"</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">      },</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">      {</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">        "type"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">"remote"</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">        "tag"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">"geosite-cn"</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">        "format"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">"binary"</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">        "url"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">"https://cdn.jsdelivr.net/gh/SagerNet/sing-geosite@rule-set/geosite-cn.srs"</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">        "download_detour"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">"DIRECT"</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">      },</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">      {</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">        "type"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">"remote"</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">        "tag"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">"geosite-geolocation-!cn"</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">        "format"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">"binary"</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">        "url"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">"https://cdn.jsdelivr.net/gh/SagerNet/sing-geosite@rule-set/geosite-geolocation-!cn.srs"</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">        "download_detour"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">"DIRECT"</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">      },</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">      {</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">        "type"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">"remote"</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">        "tag"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">"geosite-category-companies@cn"</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">        "format"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">"binary"</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">        "url"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">"https://cdn.jsdelivr.net/gh/SagerNet/sing-geosite@rule-set/geosite-category-companies@cn.srs"</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">        "download_detour"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">"DIRECT"</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">      }</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    ],</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">    "final"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">"🌍 外网"</span><span style="color:#24292E;--s-dark:#E1E4E8">, </span><span style="color:#6A737D;--s-dark:#6A737D">// 改为你的代理节点 tag</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">    "auto_detect_interface"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">true</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">  },</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">  "experimental"</span><span style="color:#24292E;--s-dark:#E1E4E8">: {</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">    "clash_api"</span><span style="color:#24292E;--s-dark:#E1E4E8">: {</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">      "external_controller"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">"0.0.0.0:9090"</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">      "external_ui"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">"yacd"</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">      "external_ui_download_url"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">"https://github.com/MetaCubeX/Yacd-meta/archive/gh-pages.zip"</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">      "external_ui_download_detour"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">"🌍 外网"</span><span style="color:#24292E;--s-dark:#E1E4E8">, </span><span style="color:#6A737D;--s-dark:#6A737D">// 改为你的代理节点 tag</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">      "default_mode"</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">"Rule"</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">  }</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">}</span></span></code><label class="code-collapse-collapse" for="toggle-0san92l"><svg class="code-collapse-icon" width="32" height="24" viewBox="0 0 32 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20,15 12,7 4,15"></polyline></svg></label></pre></div></div></div>
<p>注意几个注释的地方需要修改。默认配置中，会将中国域名的 DNS 解析直接通过 223.5.5.5 解析成 RealIP（见 DNS-rules[2]），其他域名解析为 FakeIP（见 DNS-rules[3]）。并在进行流量分流时，将所有中国 IP 和域名都走 DIRECT 直连（见 route-rules[2]），其他流量都走代理（见 route-final）。</p>
<p>完全配置好以后，我们可以设置 sing-box 为开启自动启动，并立即启动起来。</p>
<div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">systemctl</span><span style="color:#032F62;--s-dark:#9ECBFF"> enable</span><span style="color:#005CC5;--s-dark:#79B8FF"> --now</span><span style="color:#032F62;--s-dark:#9ECBFF"> sing-box</span></span></code></pre></div>
<p>后续如想查看日志，我们直接使用 Debian 自带的工具来查看：</p>
<div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">journalctl</span><span style="color:#005CC5;--s-dark:#79B8FF"> -efu</span><span style="color:#032F62;--s-dark:#9ECBFF"> sing-box</span></span></code></pre></div>
<p>由于 sing-box 提供了兼容 clash 的 api，所以也可以直接使用 clash 的 web ui 进行管理。在启动后稍等一会（sing-box 下载 ui），即可通过 9090 端口打开 yacd 界面了。</p>
<h4 id="telegram-问题">Telegram 问题<a href="#telegram-问题" class="heading-anchor-link" aria-label="Link to Telegram 问题"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h4>
<p>此方案的缺点也很明显，由于基于 DNS 分流，不走 DNS 的直接 IP 流量都会被主路由直接直连，导致像 Telegram 之类的直接使用 IP 的 APP 无法正确分流。解决方法也很简单，将这个 IP 加到主路由下一跳网关的 IP List，iptables.sh 的转发列表，和 sing-box 配置中的 rules 中，指定该 IP 走代理即可。</p>
<p>当前我是写了个脚本帮我自动处理规则集中的 IP，生成对应的 IP List、iptables.sh 和可直接用于 sing-box 的 config.json 配置文件。待我加工加工，脱敏后开源，敬请期待吧。</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[旁路由端口映射失效解决]]></title>
            <link>https://blog.shinya.click/fiddling/fix-port-forward-in-bypass-router</link>
            <guid isPermaLink="false">https://blog.shinya.click/fiddling/fix-port-forward-in-bypass-router</guid>
            <pubDate>Thu, 15 Aug 2024 15:50:00 GMT</pubDate>
            <description><![CDATA[在使用旁路由配置的情况下，主路由上的端口映射往往会失效。这是因为旁路由网关的设置改变了流量的转发路径，使得原本依赖于主路由的端口映射无法正常工作。网关的主要功能是进行地址转换，将内网流量转发到外网，而每个内网设备都需要一个网关来实现与外部的通信。理解这一机制，有助于更好地解决端口映射失效的问题。]]></description>
            <content:encoded><![CDATA[<h3 id="前言">前言<a href="#前言" class="heading-anchor-link" aria-label="Link to 前言"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h3>
<p>使用 <a href="/fiddling/debian-as-bypass-router">上篇文章的方案</a> 配置完成后，如果你在主路由上配置了端口映射，且被映射端口的机器网关配置的是旁路由，那此时端口映射应当是失效了。其实并不是 Clash 分流导致的问题，只要是配置了旁路由网关，主路由配置的端口映射都会失效。</p>
<h3 id="原理太长不看系列">原理（太长不看系列）<a href="#原理太长不看系列" class="heading-anchor-link" aria-label="Link to 原理（太长不看系列）"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h3>
<p>网关的作用其实就是 NAT 地址转换，用于将内部网络的流量转发到外部网络，内部流量只要传输到外部网络，就必须通过网关。所以每个内网机器都必须配置一个网关地址，以与外部通信。</p>
<p>网关在实现上，可以简单理解为一个自动的端口转发。网关对每一个活动连接（通过五元组唯一标识）都维护了一个端口对，一个端口对内一个端口对外。例如你的机器与 Google 服务器建立连接，网关维护的端口对是 (32384, 14122)，那么就表示你向 Google 发送的所有流量都要发送到网关的 32384 端口，网关转发流量包，通过 14122 端口送往 Google 的服务器，反之 Google 服务器向你发送数包也要经过这层转发。当然很多时候 NAT 不一定只有一层，如果你的宽带没有获取到公网 IP，那么你的家庭网关的上级网关也会做一次 NAT，最终与 Google 通信的网关一定是具有公网 IP 的网关（只有具有公网 IP 的机器才可以在互联网通信）</p>
<p>当我们配置了旁路由时，旁路由由于充当了网关，也会进行一次 NAT。那么在你的机器与外部通信时，实际上是经过两次 NAT 的：</p>
<div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre><code>你的机器 &#x3C;——> 旁路由（NAT） &#x3C;——> 主路由（NAT） &#x3C;——> 外部机器
</code></pre></div>
<p>如果你在主路由配置端口映射，但是内部机器配置的网关却是旁路由，当你尝试通过公网 IP 访问内网时，入流量是这样的：</p>
<div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre><code>外部机器 ----> 主路由网关 ----> 你的机器
</code></pre></div>
<p>而出流量则是这样的：</p>
<div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre><code>你的机器 ----> 旁路由 ----> 主路由 ----> 外部机器
</code></pre></div>
<p>实际上，在外部机器发起连接的时候，你的机器只与主路由网关建立了连接，但是回程流量却被发到了旁路由网关，旁路由网关找不到连接对应的端口对就会直接将包丢弃，导致了端口映射失效</p>
<h3 id="解决方案直接看这个">解决方案（直接看这个）<a href="#解决方案直接看这个" class="heading-anchor-link" aria-label="Link to 解决方案（直接看这个）"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h3>
<p>解决方案很简单，内网包含了两层 NAT，那也只需要将端口映射配置两次即可。</p>
<ol>
<li>主路由配置端口映射，指向旁路由</li>
<li>旁路由配置端口映射，指向目标机器</li>
</ol>
<p>这样，无论入程还是回程，流量都按如下模式路由，即可成功建立连接</p>
<div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre><code>你的机器 &#x3C;——> 旁路由（NAT） &#x3C;——> 主路由（NAT） &#x3C;——> 外部机器
</code></pre></div>
<p>主路由和旁路由配置端口映射的方式因系统而异，iKuai 和 OpenWRT 都由图形化的方式配置，这里不再赘述了。上篇文章的方案中旁路由使用的是 Debian，实际上以下方法适用于所有使用 iptables 的系统。执行以下命令设置 iptables，以下为目标机器为"内部"，称呼网关机器为"外部"</p>
<div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">iptables</span><span style="color:#005CC5;--s-dark:#79B8FF"> -t</span><span style="color:#032F62;--s-dark:#9ECBFF"> nat</span><span style="color:#005CC5;--s-dark:#79B8FF"> -I</span><span style="color:#032F62;--s-dark:#9ECBFF"> PREROUTING</span><span style="color:#005CC5;--s-dark:#79B8FF"> -p</span><span style="color:#032F62;--s-dark:#9ECBFF"> tcp</span><span style="color:#005CC5;--s-dark:#79B8FF"> -d</span><span style="color:#D73A49;--s-dark:#F97583"> &#x3C;</span><span style="color:#032F62;--s-dark:#9ECBFF">外部</span><span style="color:#032F62;--s-dark:#9ECBFF"> I</span><span style="color:#24292E;--s-dark:#E1E4E8">P</span><span style="color:#D73A49;--s-dark:#F97583">></span><span style="color:#005CC5;--s-dark:#79B8FF"> --dport</span><span style="color:#D73A49;--s-dark:#F97583"> &#x3C;</span><span style="color:#032F62;--s-dark:#9ECBFF">外部端</span><span style="color:#24292E;--s-dark:#E1E4E8">口</span><span style="color:#D73A49;--s-dark:#F97583">></span><span style="color:#005CC5;--s-dark:#79B8FF"> -j</span><span style="color:#032F62;--s-dark:#9ECBFF"> DNAT</span><span style="color:#005CC5;--s-dark:#79B8FF"> --to-destination</span><span style="color:#D73A49;--s-dark:#F97583"> &#x3C;</span><span style="color:#032F62;--s-dark:#9ECBFF">内部</span><span style="color:#032F62;--s-dark:#9ECBFF"> I</span><span style="color:#24292E;--s-dark:#E1E4E8">P</span><span style="color:#D73A49;--s-dark:#F97583">></span><span style="color:#032F62;--s-dark:#9ECBFF">:</span><span style="color:#D73A49;--s-dark:#F97583">&#x3C;</span><span style="color:#032F62;--s-dark:#9ECBFF">内部端</span><span style="color:#24292E;--s-dark:#E1E4E8">口</span><span style="color:#D73A49;--s-dark:#F97583">></span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">iptables</span><span style="color:#005CC5;--s-dark:#79B8FF"> -t</span><span style="color:#032F62;--s-dark:#9ECBFF"> nat</span><span style="color:#005CC5;--s-dark:#79B8FF"> -I</span><span style="color:#032F62;--s-dark:#9ECBFF"> POSTROUTING</span><span style="color:#005CC5;--s-dark:#79B8FF"> -p</span><span style="color:#032F62;--s-dark:#9ECBFF"> tcp</span><span style="color:#005CC5;--s-dark:#79B8FF"> --dport</span><span style="color:#D73A49;--s-dark:#F97583"> &#x3C;</span><span style="color:#032F62;--s-dark:#9ECBFF">内部端</span><span style="color:#24292E;--s-dark:#E1E4E8">口</span><span style="color:#D73A49;--s-dark:#F97583">></span><span style="color:#005CC5;--s-dark:#79B8FF"> -d</span><span style="color:#D73A49;--s-dark:#F97583"> &#x3C;</span><span style="color:#032F62;--s-dark:#9ECBFF">外部</span><span style="color:#032F62;--s-dark:#9ECBFF"> I</span><span style="color:#24292E;--s-dark:#E1E4E8">P</span><span style="color:#D73A49;--s-dark:#F97583">></span><span style="color:#005CC5;--s-dark:#79B8FF"> -j</span><span style="color:#032F62;--s-dark:#9ECBFF"> SNAT</span><span style="color:#005CC5;--s-dark:#79B8FF"> --to-source</span><span style="color:#D73A49;--s-dark:#F97583"> &#x3C;</span><span style="color:#032F62;--s-dark:#9ECBFF">内部</span><span style="color:#032F62;--s-dark:#9ECBFF"> I</span><span style="color:#24292E;--s-dark:#E1E4E8">P</span><span style="color:#D73A49;--s-dark:#F97583">></span></span></code></pre></div>
<p>执行完后即可设置 iptables</p>
<p>注意上篇文章中 clean.sh 中的 <code>iptables -t nat -F</code> 命令会清除所有用户自定义的路由规则，导致转发失效。所以在执行完后需要补充执行下上述命令。</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[debian 旁路由方案]]></title>
            <link>https://blog.shinya.click/fiddling/debian-as-bypass-router</link>
            <guid isPermaLink="false">https://blog.shinya.click/fiddling/debian-as-bypass-router</guid>
            <pubDate>Sat, 13 Jul 2024 09:49:00 GMT</pubDate>
            <description><![CDATA[Debian 旁路由方案为用户提供了一种更为稳定和灵活的选择，摆脱了对 OpenWRT 和 LuCI 的依赖。通过直接在 Debian 上进行配置，用户可以享受更高的系统控制权，避免了 GUI 带来的种种限制和不稳定性。相比于常见的旁路由方案，Debian 的方法使得透明代理的设置更加可靠，为那些追求性能和效率的用户提供了新的可能性。]]></description>
            <content:encoded><![CDATA[<h3 id="前言">前言<a href="#前言" class="heading-anchor-link" aria-label="Link to 前言"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h3>
<p>大部分旁路由方案都是基于 OpenWRT 搭建 —— 这是一个单独的 Linux 发行版，拥有自己的软件包系统。再其中大部分方案又是基于 LuCI —— 专用于 OpenWRT 的 Web GUI，教程中使用的软件也是 luci-app-xxx，专为 LuCI 打造。这些方案很好，但不够好：</p>
<ol>
<li>过于依赖 GUI 配置：LuCI 的软件包通常在 web 端只能进行有限的配置</li>
<li>LuCI 不够稳定：不是 OpenWRT 不稳定，而是 LuCI 不稳定。我的 LuCI 曾因为 OpenClash 崩溃过三次（也可能是我的问题）</li>
<li>尽管我们可以自编译 OpenWRT，但是大部分教程都直接使用了一些预编译好的固件，有些可能会过时</li>
<li>无法完全掌控系统（被 LuCI 架空啦</li>
</ol>
<p>我也曾经折腾过一两年的 OpenWRT 透明代理方案，主路由方案旁路由方案，最终都因为其不够稳定而放弃。很长一段时间都直接使用各种客户端（Surge、loon、clash verge rev 等）勉强用着。大概一周前，绝区零上线。由于国服版号问题，PS5 仅上线了国际服。即使选择亚服，勉强可以直连，网速和延迟也让人绝望。本着不给网易 UU 送钱的态度，我又想起了透明代理。正好，我手头有一台闲置的零刻小主机。预先装好了 Debian，本来打算用来做开发机来着，结果因为我太懒，也一直吃灰。于是折腾了一个周末，最终完成了使用 Debian 作为旁路由进行透明代理的方案。</p>
<p>最终形态的网络拓扑如下：</p>
<figure><img src="https://blog-img.774352199.xyz/2025/c4347103c787f3d28b50a679e80aa0fe.png" alt="topo" loading="lazy" decoding="async" style="background-image:url(data:image/webp;base64,UklGRgoBAABXRUJQVlA4WAoAAAAQAAAADwAACgAAQUxQSIsAAAAFgFtt27Lmwd3ptHOHAVxLdnB2oGIL6K2kZAC3yqGMJ5XrF94qtkFECAkSbE6BO19XO/rOz6P+jnEA2BMtZl5Z+GcCw8M1IKVXGT75duGWwo8T0Xuteg/i0gcEeOPylQ1tkfHxvS9eE7/qhl65xOdxGOMAJFpacw88cIlDBOAk7wX+vQfZJ+4PTrI+AFZQOCBYAAAA8AEAnQEqEAALAALATCWUAuwBHqBvWt7AAP7wTViblfFJws/RtnVy2LH3qffnG36J3f++fxg1Z4ROwYGvcrprZH/2hpxBYWcVTrO/j3kyyimzyf/swAAAAA==);background-size:cover;background-repeat:no-repeat"><figcaption>topo</figcaption></figure>
<p>可以看到，整个内网被划分成了两个网段：192.168.6.0/24，192.168.7.0/24。其中 6.0/24 为默认网段，用于无需越墙的设备，而 7.0/24 为需要越墙的设备，其流量都会经过旁路由小主机转发。</p>
<p>主要方案为 AdguardHome + Clash，其中 AdguardHome 用于广告过滤等功能，Clash 用于 DNS 分流和流量代理。</p>
<h3 id="主路由配置">主路由配置<a href="#主路由配置" class="heading-anchor-link" aria-label="Link to 主路由配置"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h3>
<p>在配置之前，内网 IP 段为 192.168.6.0/24，我们需要新开一个网段 192.168.7.0/24</p>
<p>我的主路由是 iKuai，下面是 iKuai 新开网段的方法，OpenWRT 或者其他路由系统可以自行 Google</p>
<p>iKuai 下网络设置 - 内外网设置 - lan1，高级设置中添加一个扩展 IP，IP 为 192.168.7.1，子网掩码 255.255.255.0</p>
<figure><img src="https://blog-img.774352199.xyz/2025/9fc87b145274f1fb0cba1ed0d2329ac0.png" alt="iKuai 配置" loading="lazy" decoding="async" style="background-image:url(data:image/webp;base64,UklGRkYAAABXRUJQVlA4IDoAAAAwAgCdASoQAAkAAsBMJZwCdIExFx+tqDTxwAD+6iWw9L8j9/sVRYX7l7TZBDjF+d/WGfCRGF8R4AAA);background-size:cover;background-repeat:no-repeat"><figcaption>iKuai 配置</figcaption></figure>
<p>DHCP 设置中添加一个 192.168.7.0/24 网段的 DHCP 配置</p>
<figure><img src="https://blog-img.774352199.xyz/2025/2de91498b256d08c92a3c8844ca14dbe.png" alt="DHCP 配置" loading="lazy" decoding="async" style="background-image:url(data:image/webp;base64,UklGRlIAAABXRUJQVlA4IEYAAADQAQCdASoQAA0AAsBMJYwCdAD0Qn7boAD+5m8Rl+EN4PrpwSvsGBeo0E8jJkS0BRwWSGQ9fUWyHnUl98D/EJLi4Ch6AAAA);background-size:cover;background-repeat:no-repeat"><figcaption>DHCP 配置</figcaption></figure>
<p>网关设置为 192.168.7.2，即后面要配置的旁路由的地址，首选 DNS 和备选 DNS 也都设置为 192.168.7.2，因为该网段下的 DNS 都由旁路由处理。</p>
<h3 id="debian-配置">Debian 配置<a href="#debian-配置" class="heading-anchor-link" aria-label="Link to Debian 配置"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h3>
<p>以下操作如果不做说明，都是在旁路由机器上进行。</p>
<h4 id="配置-ip">配置 IP<a href="#配置-ip" class="heading-anchor-link" aria-label="Link to 配置 IP"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h4>
<p>编辑 Debian 的网络配置：输入 <code>sudo nano /etc/network/interfaces</code>，编辑为以下内容，并保存退出：</p>
<div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre><code># This file describes the network interfaces available on your system
# and how to activate them. For more information, see interfaces(5).

source /etc/network/interfaces.d/*

# The loopback network interface
auto lo
iface lo inet loopback

# The primary network interface
allow-hotplug enp1s0
iface enp1s0 inet static
address 192.168.7.2
netmask 255.255.255.0
gateway 192.168.7.1
dns-nameservers 127.0.0.1
</code></pre></div>
<p>该配置中：</p>
<ul>
<li><code>enp1s0</code> 是我的网卡设备名，请将其改为你自己的设备名，可以通过 <code>ip a</code> 查看</li>
<li>IPV4 网络进行静态配置 <code>inet static</code>，并固定设备 IP 为 <code>192.168.7.2/24</code>，网关指向主路由 <code>192.168.7.1</code>，DNS 在配置完成前可以先设置为可用的 DNS Server，等待本机上的 AdguardHome 配置完成后再改为 <code>127.0.0.1</code>，以防在配置过程中无法上网</li>
</ul>
<p>保存配置后可以通过如下命令重启网络</p>
<div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">sudo</span><span style="color:#032F62;--s-dark:#9ECBFF"> systemctl</span><span style="color:#032F62;--s-dark:#9ECBFF"> restart</span><span style="color:#032F62;--s-dark:#9ECBFF"> networking.service</span></span></code></pre></div>
<p>注意这个时候 SSH 连接可能会断开，因为设备 IP 已经改变了。应当通过新的 IP <code>192.168.7.2</code> 重新 SSH 登陆。</p>
<p><code>ip a</code> 查看配置结果：</p>
<div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">ip</span><span style="color:#032F62;--s-dark:#9ECBFF"> a</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">1:</span><span style="color:#032F62;--s-dark:#9ECBFF"> lo:</span><span style="color:#D73A49;--s-dark:#F97583"> &#x3C;</span><span style="color:#032F62;--s-dark:#9ECBFF">LOOPBACK,UP,LOWER_U</span><span style="color:#24292E;--s-dark:#E1E4E8">P</span><span style="color:#D73A49;--s-dark:#F97583">></span><span style="color:#032F62;--s-dark:#9ECBFF"> mtu</span><span style="color:#005CC5;--s-dark:#79B8FF"> 65536</span><span style="color:#032F62;--s-dark:#9ECBFF"> qdisc</span><span style="color:#032F62;--s-dark:#9ECBFF"> noqueue</span><span style="color:#032F62;--s-dark:#9ECBFF"> state</span><span style="color:#032F62;--s-dark:#9ECBFF"> UNKNOWN</span><span style="color:#032F62;--s-dark:#9ECBFF"> group</span><span style="color:#032F62;--s-dark:#9ECBFF"> default</span><span style="color:#032F62;--s-dark:#9ECBFF"> qlen</span><span style="color:#005CC5;--s-dark:#79B8FF"> 1000</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">    link/loopback</span><span style="color:#032F62;--s-dark:#9ECBFF"> 00:00:00:00:00:00</span><span style="color:#032F62;--s-dark:#9ECBFF"> brd</span><span style="color:#032F62;--s-dark:#9ECBFF"> 00:00:00:00:00:00</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">    inet</span><span style="color:#032F62;--s-dark:#9ECBFF"> 127.0.0.1/8</span><span style="color:#032F62;--s-dark:#9ECBFF"> scope</span><span style="color:#032F62;--s-dark:#9ECBFF"> host</span><span style="color:#032F62;--s-dark:#9ECBFF"> lo</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">       valid_lft</span><span style="color:#032F62;--s-dark:#9ECBFF"> forever</span><span style="color:#032F62;--s-dark:#9ECBFF"> preferred_lft</span><span style="color:#032F62;--s-dark:#9ECBFF"> forever</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">    inet6</span><span style="color:#032F62;--s-dark:#9ECBFF"> ::1/128</span><span style="color:#032F62;--s-dark:#9ECBFF"> scope</span><span style="color:#032F62;--s-dark:#9ECBFF"> host</span><span style="color:#032F62;--s-dark:#9ECBFF"> noprefixroute</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">       valid_lft</span><span style="color:#032F62;--s-dark:#9ECBFF"> forever</span><span style="color:#032F62;--s-dark:#9ECBFF"> preferred_lft</span><span style="color:#032F62;--s-dark:#9ECBFF"> forever</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">2:</span><span style="color:#032F62;--s-dark:#9ECBFF"> enp1s0:</span><span style="color:#D73A49;--s-dark:#F97583"> &#x3C;</span><span style="color:#032F62;--s-dark:#9ECBFF">BROADCAST,MULTICAST,UP,LOWER_U</span><span style="color:#24292E;--s-dark:#E1E4E8">P</span><span style="color:#D73A49;--s-dark:#F97583">></span><span style="color:#032F62;--s-dark:#9ECBFF"> mtu</span><span style="color:#005CC5;--s-dark:#79B8FF"> 1500</span><span style="color:#032F62;--s-dark:#9ECBFF"> qdisc</span><span style="color:#032F62;--s-dark:#9ECBFF"> fq_codel</span><span style="color:#032F62;--s-dark:#9ECBFF"> state</span><span style="color:#032F62;--s-dark:#9ECBFF"> UP</span><span style="color:#032F62;--s-dark:#9ECBFF"> group</span><span style="color:#032F62;--s-dark:#9ECBFF"> default</span><span style="color:#032F62;--s-dark:#9ECBFF"> qlen</span><span style="color:#005CC5;--s-dark:#79B8FF"> 1000</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">    link/ether</span><span style="color:#032F62;--s-dark:#9ECBFF"> 70:70:fc:00:e3:36</span><span style="color:#032F62;--s-dark:#9ECBFF"> brd</span><span style="color:#032F62;--s-dark:#9ECBFF"> ff:ff:ff:ff:ff:ff</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">    inet</span><span style="color:#032F62;--s-dark:#9ECBFF"> 192.168.7.2/24</span><span style="color:#032F62;--s-dark:#9ECBFF"> brd</span><span style="color:#005CC5;--s-dark:#79B8FF"> 192.168.7.255</span><span style="color:#032F62;--s-dark:#9ECBFF"> scope</span><span style="color:#032F62;--s-dark:#9ECBFF"> global</span><span style="color:#032F62;--s-dark:#9ECBFF"> enp1s0</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">       valid_lft</span><span style="color:#032F62;--s-dark:#9ECBFF"> forever</span><span style="color:#032F62;--s-dark:#9ECBFF"> preferred_lft</span><span style="color:#032F62;--s-dark:#9ECBFF"> forever</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">    inet6</span><span style="color:#032F62;--s-dark:#9ECBFF"> ■■■:■■■■:■■■■:■■■:■■■■:■■■■/64</span><span style="color:#032F62;--s-dark:#9ECBFF"> scope</span><span style="color:#032F62;--s-dark:#9ECBFF"> global</span><span style="color:#032F62;--s-dark:#9ECBFF"> dynamic</span><span style="color:#032F62;--s-dark:#9ECBFF"> mngtmpaddr</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">       valid_lft</span><span style="color:#032F62;--s-dark:#9ECBFF"> 1741sec</span><span style="color:#032F62;--s-dark:#9ECBFF"> preferred_lft</span><span style="color:#032F62;--s-dark:#9ECBFF"> 1741sec</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">    inet6</span><span style="color:#032F62;--s-dark:#9ECBFF"> fe80::7270:fcff:fe00:e336/64</span><span style="color:#032F62;--s-dark:#9ECBFF"> scope</span><span style="color:#032F62;--s-dark:#9ECBFF"> link</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">       valid_lft</span><span style="color:#032F62;--s-dark:#9ECBFF"> forever</span><span style="color:#032F62;--s-dark:#9ECBFF"> preferred_lft</span><span style="color:#032F62;--s-dark:#9ECBFF"> forever</span></span></code></pre></div>
<p>可以看到本机的内网 IP 已经变成了 192.168.7.2/24</p>
<h4 id="配置转发">配置转发<a href="#配置转发" class="heading-anchor-link" aria-label="Link to 配置转发"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h4>
<p>只有拥有流量转发功能的机器才可以作为路由和网关：</p>
<div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">sudo</span><span style="color:#032F62;--s-dark:#9ECBFF"> echo</span><span style="color:#032F62;--s-dark:#9ECBFF"> "net.ipv4.ip_forward = 1"</span><span style="color:#D73A49;--s-dark:#F97583"> >></span><span style="color:#032F62;--s-dark:#9ECBFF"> /etc/sysctl.conf</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">sudo</span><span style="color:#032F62;--s-dark:#9ECBFF"> sysctl</span><span style="color:#005CC5;--s-dark:#79B8FF"> -p</span></span></code></pre></div>
<h3 id="adguardhome-配置">AdguardHome 配置<a href="#adguardhome-配置" class="heading-anchor-link" aria-label="Link to AdguardHome 配置"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h3>
<p>以下说明下 DNS 思路</p>
<figure><img src="https://blog-img.774352199.xyz/2025/bb62d86943fdce26364eea909c4621a9.png" alt="DNS 链路" loading="lazy" decoding="async" style="background-image:url(data:image/webp;base64,UklGRmgBAABXRUJQVlA4WAoAAAAQAAAADwAADQAAQUxQSLQAAAABgJNt27LlfuR+nvf99MdtJQAUgBUmmR22vwQbDYhgCZisATsNbLQElIiIgBS2S8ujl8XluUhGO4P9978rLL19P6pkYZW1g5NNH/aOtshIy2QUANqwNkzrm25muH562S1zUKZNxg7N5f3NNsyYaWZGR1ElnZaWRrJfmK3uDJZuGNuSDx9fhzEDDWgOVN+ZXqyD6gQyMhOpnpHFoEXdPTbWhG4RkDorAF+ncED/S0Tk2EVEJAFWUDggjgAAABACAJ0BKhAADgACwEwlnAAPhm7rzFo2WDAA/vW12cdXPPf8LX/n6Kw0R30XmiXCEO6RYssN4cx+AdMRIpn8LAQsu/DzV3b/YcnNH8eekEI+cvJxwl1bD/vpUu7beRMDxC8srFpUlHWXwq3tFGdKZHJzr5VWqihIH+YcSRmFmWx/8CH5pJMsWeWgneLgAAA=);background-size:cover;background-repeat:no-repeat"><figcaption>DNS 链路</figcaption></figure>
<p>当客户端需要解析 DNS 时，监听在 53 端口上的 AdguardHome 会转发给其上游 Clash，然后 Clash 根据设置来进行分流，中国大陆部分使用国内的公共 DNS 服务器进行解析，非中国大陆部分通过代理向国外公共 DNS 服务器进行解析</p>
<p>而当 Clash 出现异常时，AdguardHome 则直接向国内公共 DNS 服务器请求解析（实际上意义不大，即使解析出来的 IP，流量还是要通过 Clash）</p>
<h4 id="安装-adguardhome">安装 AdguardHome<a href="#安装-adguardhome" class="heading-anchor-link" aria-label="Link to 安装 AdguardHome"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h4>
<p>以下命令以 root 身份运行</p>
<div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#6A737D;--s-dark:#6A737D">## 检查最新稳定版的版本号，如果获取不到请检查网络</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">remote_ver</span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#24292E;--s-dark:#E1E4E8">$(</span><span style="color:#6F42C1;--s-dark:#B392F0">curl</span><span style="color:#005CC5;--s-dark:#79B8FF"> -sS</span><span style="color:#032F62;--s-dark:#9ECBFF"> https://api.github.com/repos/AdguardTeam/AdGuardHome/releases/latest</span><span style="color:#D73A49;--s-dark:#F97583"> |</span><span style="color:#6F42C1;--s-dark:#B392F0"> jq</span><span style="color:#005CC5;--s-dark:#79B8FF"> -r</span><span style="color:#032F62;--s-dark:#9ECBFF"> .tag_name</span><span style="color:#D73A49;--s-dark:#F97583"> |</span><span style="color:#6F42C1;--s-dark:#B392F0"> sed</span><span style="color:#032F62;--s-dark:#9ECBFF"> 's|v||'</span><span style="color:#D73A49;--s-dark:#F97583"> |</span><span style="color:#6F42C1;--s-dark:#B392F0"> grep</span><span style="color:#005CC5;--s-dark:#79B8FF"> -v</span><span style="color:#032F62;--s-dark:#9ECBFF"> "null"</span><span style="color:#24292E;--s-dark:#E1E4E8">); </span><span style="color:#005CC5;--s-dark:#79B8FF">echo</span><span style="color:#24292E;--s-dark:#E1E4E8"> $remote_ver</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">## 下载最新稳定版（前一句有输出这一句才能正常执行）</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">cd</span><span style="color:#032F62;--s-dark:#9ECBFF"> /tmp</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">wget</span><span style="color:#005CC5;--s-dark:#79B8FF"> -q</span><span style="color:#005CC5;--s-dark:#79B8FF"> --progress=bar:dot</span><span style="color:#005CC5;--s-dark:#79B8FF"> --show-progress</span><span style="color:#005CC5;--s-dark:#79B8FF"> -O</span><span style="color:#032F62;--s-dark:#9ECBFF"> "AdGuardHome_linux_amd64.tar.gz"</span><span style="color:#032F62;--s-dark:#9ECBFF"> "https://github.com/AdguardTeam/AdGuardHome/releases/download/v${</span><span style="color:#24292E;--s-dark:#E1E4E8">remote_ver</span><span style="color:#032F62;--s-dark:#9ECBFF">}/AdGuardHome_linux_amd64.tar.gz"</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">## 解压</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">tar</span><span style="color:#005CC5;--s-dark:#79B8FF"> --no-same-owner</span><span style="color:#005CC5;--s-dark:#79B8FF"> -xf</span><span style="color:#032F62;--s-dark:#9ECBFF"> "AdGuardHome_linux_amd64.tar.gz"</span><span style="color:#005CC5;--s-dark:#79B8FF"> --strip-components</span><span style="color:#005CC5;--s-dark:#79B8FF"> 2</span><span style="color:#005CC5;--s-dark:#79B8FF"> --directory=.</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">## 安装</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">install</span><span style="color:#005CC5;--s-dark:#79B8FF"> -ps</span><span style="color:#032F62;--s-dark:#9ECBFF"> AdGuardHome</span><span style="color:#032F62;--s-dark:#9ECBFF"> /usr/local/bin/adguardhome</span></span></code></pre></div>
<h4 id="创建服务">创建服务<a href="#创建服务" class="heading-anchor-link" aria-label="Link to 创建服务"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h4>
<p>创建工作目录 <code>/var/lib/adguardhome</code></p>
<div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">mkdir</span><span style="color:#005CC5;--s-dark:#79B8FF"> -p</span><span style="color:#032F62;--s-dark:#9ECBFF"> /var/lib/adguardhome</span></span></code></pre></div>
<p>创建 <code>/etc/systemd/system/adguardhome.service</code> 如下，这时配置文件为 <code>/var/lib/adguardhome/AdGuardHome.yaml</code></p>
<div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">[Unit]</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">Description</span><span style="color:#24292E;--s-dark:#E1E4E8"> = Network-wide ads &#x26; trackers blocking DNS server.</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">Wants</span><span style="color:#24292E;--s-dark:#E1E4E8">       = network-online.target mosdns.service</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">After</span><span style="color:#24292E;--s-dark:#E1E4E8">       = network-online.target mosdns.service</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">[Service]</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">Type</span><span style="color:#24292E;--s-dark:#E1E4E8">               = simple</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">Restart</span><span style="color:#24292E;--s-dark:#E1E4E8">            = always</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">StartLimitInterval</span><span style="color:#24292E;--s-dark:#E1E4E8"> = 5</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">StartLimitBurst</span><span style="color:#24292E;--s-dark:#E1E4E8">    = 10</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">ExecStart</span><span style="color:#24292E;--s-dark:#E1E4E8">          = /usr/local/bin/adguardhome -w /var/lib/adguardhome</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">RestartSec</span><span style="color:#24292E;--s-dark:#E1E4E8">         = 10</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">[Install]</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">WantedBy</span><span style="color:#24292E;--s-dark:#E1E4E8"> = multi-user.target</span></span></code></pre></div>
<p>保存后通过 <code>systemctl enable --now adguardhome.service</code> 设置开机启动并立刻启动。后续如想查看日志，我们直接使用 Debian 自带的工具来查看：</p>
<div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">journalctl</span><span style="color:#005CC5;--s-dark:#79B8FF"> -efu</span><span style="color:#032F62;--s-dark:#9ECBFF"> adguardhome.service</span></span></code></pre></div>
<p>如果想要重启：</p>
<div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">systemctl</span><span style="color:#032F62;--s-dark:#9ECBFF"> restart</span><span style="color:#032F62;--s-dark:#9ECBFF"> adguardhome.service</span></span></code></pre></div>
<h4 id="初始化配置">初始化配置<a href="#初始化配置" class="heading-anchor-link" aria-label="Link to 初始化配置"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h4>
<p>打开 <code>http://192.168.7.2:3000</code> 进行初始化配置，网络管理界面端口可以保持 3000，DNS 服务器端口设置为 53</p>
<p>设置 - DNS 设置中，上游 DNS 设置为我们暂未配置的 Clash DNS <code>127.0.0.1:1053</code>，后备 DNS 服务器可以填写几个国内的 DNS，如</p>
<div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre><code>223.5.5.5
119.29.29.29
</code></pre></div>
<p>记得点击应用</p>
<p>随后 DNS 服务配置 - 速度限制 设置为 0 即可。</p>
<p>如果需要去广告的话，可以在 过滤器 - DNS 黑名单 中添加，这里推荐两个大陆使用效果较好的规则集：</p>
<div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre><code>easylist:  https://anti-ad.net/easylist.txt
half-life: https://adguard.yojigen.tech/HalfLifeList.txt
</code></pre></div>
<h3 id="clash-配置">Clash 配置<a href="#clash-配置" class="heading-anchor-link" aria-label="Link to Clash 配置"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h3>
<p>Clash 在整个方案中负责国内外 DNS 解析分流，另外就是老本行穿墙了。由于 Clash 原仓库已删库跑路，衣钵由 mihomo 集成（米哈游你坏事做尽）</p>
<h4 id="安装-clash">安装 Clash<a href="#安装-clash" class="heading-anchor-link" aria-label="Link to 安装 Clash"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h4>
<p>以下命令以 root 身份运行：</p>
<div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#6A737D;--s-dark:#6A737D">## 检查最新稳定版的版本号，如果获取不到请检查网络</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">remote_ver</span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#24292E;--s-dark:#E1E4E8">$(</span><span style="color:#6F42C1;--s-dark:#B392F0">curl</span><span style="color:#005CC5;--s-dark:#79B8FF"> -sS</span><span style="color:#032F62;--s-dark:#9ECBFF"> https://api.github.com/repos/MetaCubeX/mihomo/releases/latest</span><span style="color:#D73A49;--s-dark:#F97583"> |</span><span style="color:#6F42C1;--s-dark:#B392F0"> jq</span><span style="color:#005CC5;--s-dark:#79B8FF"> -r</span><span style="color:#032F62;--s-dark:#9ECBFF"> .tag_name</span><span style="color:#D73A49;--s-dark:#F97583"> |</span><span style="color:#6F42C1;--s-dark:#B392F0"> sed</span><span style="color:#032F62;--s-dark:#9ECBFF"> 's|v||'</span><span style="color:#D73A49;--s-dark:#F97583"> |</span><span style="color:#6F42C1;--s-dark:#B392F0"> grep</span><span style="color:#005CC5;--s-dark:#79B8FF"> -v</span><span style="color:#032F62;--s-dark:#9ECBFF"> "null"</span><span style="color:#24292E;--s-dark:#E1E4E8">); </span><span style="color:#005CC5;--s-dark:#79B8FF">echo</span><span style="color:#24292E;--s-dark:#E1E4E8"> $remote_ver</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">## 下载最新稳定版（前一句有输出这一句才能正常执行）</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">cd</span><span style="color:#032F62;--s-dark:#9ECBFF"> /tmp</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">wget</span><span style="color:#005CC5;--s-dark:#79B8FF"> -q</span><span style="color:#005CC5;--s-dark:#79B8FF"> --progress=bar:dot</span><span style="color:#005CC5;--s-dark:#79B8FF"> --show-progress</span><span style="color:#005CC5;--s-dark:#79B8FF"> -O</span><span style="color:#032F62;--s-dark:#9ECBFF"> "mihomo-linux-amd64-v${</span><span style="color:#24292E;--s-dark:#E1E4E8">remote_ver</span><span style="color:#032F62;--s-dark:#9ECBFF">}.gz"</span><span style="color:#032F62;--s-dark:#9ECBFF"> "https://github.com/MetaCubeX/mihomo/releases/download/v${</span><span style="color:#24292E;--s-dark:#E1E4E8">remote_ver</span><span style="color:#032F62;--s-dark:#9ECBFF">}/mihomo-linux-amd64-v${</span><span style="color:#24292E;--s-dark:#E1E4E8">remote_ver</span><span style="color:#032F62;--s-dark:#9ECBFF">}.gz"</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">## 解压</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">gzip</span><span style="color:#005CC5;--s-dark:#79B8FF"> -d</span><span style="color:#032F62;--s-dark:#9ECBFF"> "mihomo-linux-amd64-v${</span><span style="color:#24292E;--s-dark:#E1E4E8">remote_ver</span><span style="color:#032F62;--s-dark:#9ECBFF">}.gz"</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">## 安装</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">install</span><span style="color:#005CC5;--s-dark:#79B8FF"> -ps</span><span style="color:#032F62;--s-dark:#9ECBFF"> mihomo-linux-amd64-v</span><span style="color:#24292E;--s-dark:#E1E4E8">${remote_ver} </span><span style="color:#032F62;--s-dark:#9ECBFF">/usr/local/bin/clash</span></span></code></pre></div>
<h4 id="创建服务-1">创建服务<a href="#创建服务-1" class="heading-anchor-link" aria-label="Link to 创建服务"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h4>
<p>创建工作目录 <code>/var/lib/clash</code></p>
<div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">mkdir</span><span style="color:#005CC5;--s-dark:#79B8FF"> -p</span><span style="color:#032F62;--s-dark:#9ECBFF"> /var/lib/clash</span></span></code></pre></div>
<p>创建用户 clash</p>
<div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">useradd</span><span style="color:#005CC5;--s-dark:#79B8FF"> -M</span><span style="color:#005CC5;--s-dark:#79B8FF"> -s</span><span style="color:#032F62;--s-dark:#9ECBFF"> /usr/sbin/nologin</span><span style="color:#032F62;--s-dark:#9ECBFF"> clash</span></span></code></pre></div>
<p>创建文件 <code>/etc/systemd/system/clash.service</code>，内容如下。这时 clash 的配置文件为 <code>/var/lib/clash/config.yaml</code>。</p>
<div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">[Unit]</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">Description</span><span style="color:#24292E;--s-dark:#E1E4E8"> = Clash-Meta tproxy daemon.</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">Wants</span><span style="color:#24292E;--s-dark:#E1E4E8">       = network-online.target</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">After</span><span style="color:#24292E;--s-dark:#E1E4E8">       = network-online.target</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">[Service]</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">Environment</span><span style="color:#24292E;--s-dark:#E1E4E8">   = </span><span style="color:#D73A49;--s-dark:#F97583">PATH</span><span style="color:#24292E;--s-dark:#E1E4E8">=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/b></span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">Type</span><span style="color:#24292E;--s-dark:#E1E4E8">          = simple</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">User</span><span style="color:#24292E;--s-dark:#E1E4E8">          = clash</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">Group</span><span style="color:#24292E;--s-dark:#E1E4E8">         = clash</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">CapabilityBoundingSet</span><span style="color:#24292E;--s-dark:#E1E4E8"> = CAP_NET_ADMIN CAP_NET_BIND_SERVICE CAP_NET_RAW</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">AmbientCapabilities</span><span style="color:#24292E;--s-dark:#E1E4E8">   = CAP_NET_ADMIN CAP_NET_BIND_SERVICE CAP_NET_RAW</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">Restart</span><span style="color:#24292E;--s-dark:#E1E4E8">       = always</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">ExecStartPre</span><span style="color:#24292E;--s-dark:#E1E4E8">  = +/usr/bin/bash /var/lib/clash/clean.sh</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">ExecStart</span><span style="color:#24292E;--s-dark:#E1E4E8">     = clash -d /var/lib/clash</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">ExecStartPost</span><span style="color:#24292E;--s-dark:#E1E4E8"> = +/usr/bin/bash /var/lib/clash/iptables.sh</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">ExecStopPost</span><span style="color:#24292E;--s-dark:#E1E4E8">  = +/usr/bin/bash /var/lib/clash/clean.sh</span></span></code></pre></div>
<p>可以从这个文件中看到，clash 二进制文件是以 clash<div></div> 用户身份运行的，这是为了方便区分 clash 自身程序发出来的流量，和 clash 转发的流量。</p>
<p>注意到 ExecStartPost 和 ExecStopPost 阶段我们执行了两个文件 iptables.sh 和 clean.sh，用来设置和清空路由表。</p>
<p>iptables.sh 和 clean.sh 内容如下：</p>
<div class="code-collapse-wrapper"><input type="checkbox" id="toggle-30wc6vk" class="code-collapse-toggle" style="display: none;"><div class="code-collapse-preview"><div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0" class="code-preview"><code><span class="line"><span style="color:#6A737D;--s-dark:#6A737D">#!/usr/bin/env bash</span></span>
<span class="line"></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">set</span><span style="color:#005CC5;--s-dark:#79B8FF"> -ex</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D"># ENABLE ipv4 forward</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">sysctl</span><span style="color:#005CC5;--s-dark:#79B8FF"> -w</span><span style="color:#032F62;--s-dark:#9ECBFF"> net.ipv4.ip_forward=</span><span style="color:#005CC5;--s-dark:#79B8FF">1</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D"># ROUTE RULES</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">ip</span><span style="color:#032F62;--s-dark:#9ECBFF"> rule</span><span style="color:#032F62;--s-dark:#9ECBFF"> add</span><span style="color:#032F62;--s-dark:#9ECBFF"> fwmark</span><span style="color:#005CC5;--s-dark:#79B8FF"> 666</span><span style="color:#032F62;--s-dark:#9ECBFF"> lookup</span><span style="color:#005CC5;--s-dark:#79B8FF"> 666</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">ip</span><span style="color:#032F62;--s-dark:#9ECBFF"> route</span><span style="color:#032F62;--s-dark:#9ECBFF"> add</span><span style="color:#032F62;--s-dark:#9ECBFF"> local</span><span style="color:#032F62;--s-dark:#9ECBFF"> 0.0.0.0/0</span><span style="color:#032F62;--s-dark:#9ECBFF"> dev</span><span style="color:#032F62;--s-dark:#9ECBFF"> lo</span><span style="color:#032F62;--s-dark:#9ECBFF"> table</span><span style="color:#005CC5;--s-dark:#79B8FF"> 666</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D"># clash 链负责处理转发流量</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">iptables</span><span style="color:#005CC5;--s-dark:#79B8FF"> -t</span><span style="color:#032F62;--s-dark:#9ECBFF"> mangle</span><span style="color:#005CC5;--s-dark:#79B8FF"> -N</span><span style="color:#032F62;--s-dark:#9ECBFF"> clash</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D"># 目标地址为局域网或保留地址的流量跳过处理</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D"># 保留地址参考：https://zh.wikipedia.org/wiki/%E5%B7%B2%E5%88%86%E9%85%8D%E7%9A%84/8_IPv4%E5%9C%B0%E5%9D%80%E5%9D%97%E5%88%97%E8%A1%A8</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">iptables</span><span style="color:#005CC5;--s-dark:#79B8FF"> -t</span><span style="color:#032F62;--s-dark:#9ECBFF"> mangle</span><span style="color:#005CC5;--s-dark:#79B8FF"> -A</span><span style="color:#032F62;--s-dark:#9ECBFF"> clash</span><span style="color:#005CC5;--s-dark:#79B8FF"> -d</span><span style="color:#032F62;--s-dark:#9ECBFF"> 0.0.0.0/8</span><span style="color:#005CC5;--s-dark:#79B8FF"> -j</span><span style="color:#032F62;--s-dark:#9ECBFF"> RETURN</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">iptables</span><span style="color:#005CC5;--s-dark:#79B8FF"> -t</span><span style="color:#032F62;--s-dark:#9ECBFF"> mangle</span><span style="color:#005CC5;--s-dark:#79B8FF"> -A</span><span style="color:#032F62;--s-dark:#9ECBFF"> clash</span><span style="color:#005CC5;--s-dark:#79B8FF"> -d</span><span style="color:#032F62;--s-dark:#9ECBFF"> 127.0.0.0/8</span><span style="color:#005CC5;--s-dark:#79B8FF"> -j</span><span style="color:#032F62;--s-dark:#9ECBFF"> RETURN</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">iptables</span><span style="color:#005CC5;--s-dark:#79B8FF"> -t</span><span style="color:#032F62;--s-dark:#9ECBFF"> mangle</span><span style="color:#005CC5;--s-dark:#79B8FF"> -A</span><span style="color:#032F62;--s-dark:#9ECBFF"> clash</span><span style="color:#005CC5;--s-dark:#79B8FF"> -d</span><span style="color:#032F62;--s-dark:#9ECBFF"> 10.0.0.0/8</span><span style="color:#005CC5;--s-dark:#79B8FF"> -j</span><span style="color:#032F62;--s-dark:#9ECBFF"> RETURN</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">iptables</span><span style="color:#005CC5;--s-dark:#79B8FF"> -t</span><span style="color:#032F62;--s-dark:#9ECBFF"> mangle</span><span style="color:#005CC5;--s-dark:#79B8FF"> -A</span><span style="color:#032F62;--s-dark:#9ECBFF"> clash</span><span style="color:#005CC5;--s-dark:#79B8FF"> -d</span><span style="color:#032F62;--s-dark:#9ECBFF"> 172.16.0.0/12</span><span style="color:#005CC5;--s-dark:#79B8FF"> -j</span><span style="color:#032F62;--s-dark:#9ECBFF"> RETURN</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">iptables</span><span style="color:#005CC5;--s-dark:#79B8FF"> -t</span><span style="color:#032F62;--s-dark:#9ECBFF"> mangle</span><span style="color:#005CC5;--s-dark:#79B8FF"> -A</span><span style="color:#032F62;--s-dark:#9ECBFF"> clash</span><span style="color:#005CC5;--s-dark:#79B8FF"> -d</span><span style="color:#032F62;--s-dark:#9ECBFF"> 192.168.0.0/16</span><span style="color:#005CC5;--s-dark:#79B8FF"> -j</span><span style="color:#032F62;--s-dark:#9ECBFF"> RETURN</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">iptables</span><span style="color:#005CC5;--s-dark:#79B8FF"> -t</span><span style="color:#032F62;--s-dark:#9ECBFF"> mangle</span><span style="color:#005CC5;--s-dark:#79B8FF"> -A</span><span style="color:#032F62;--s-dark:#9ECBFF"> clash</span><span style="color:#005CC5;--s-dark:#79B8FF"> -d</span><span style="color:#032F62;--s-dark:#9ECBFF"> 169.254.0.0/16</span><span style="color:#005CC5;--s-dark:#79B8FF"> -j</span><span style="color:#032F62;--s-dark:#9ECBFF"> RETURN</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">iptables</span><span style="color:#005CC5;--s-dark:#79B8FF"> -t</span><span style="color:#032F62;--s-dark:#9ECBFF"> mangle</span><span style="color:#005CC5;--s-dark:#79B8FF"> -A</span><span style="color:#032F62;--s-dark:#9ECBFF"> clash</span><span style="color:#005CC5;--s-dark:#79B8FF"> -d</span><span style="color:#032F62;--s-dark:#9ECBFF"> 224.0.0.0/4</span><span style="color:#005CC5;--s-dark:#79B8FF"> -j</span><span style="color:#032F62;--s-dark:#9ECBFF"> RETURN</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">iptables</span><span style="color:#005CC5;--s-dark:#79B8FF"> -t</span><span style="color:#032F62;--s-dark:#9ECBFF"> mangle</span><span style="color:#005CC5;--s-dark:#79B8FF"> -A</span><span style="color:#032F62;--s-dark:#9ECBFF"> clash</span><span style="color:#005CC5;--s-dark:#79B8FF"> -d</span><span style="color:#032F62;--s-dark:#9ECBFF"> 240.0.0.0/4</span><span style="color:#005CC5;--s-dark:#79B8FF"> -j</span><span style="color:#032F62;--s-dark:#9ECBFF"> RETURN</span></span>
</code></pre></div><div class="code-collapse-gradient"></div><label class="code-collapse-expand" for="toggle-30wc6vk"><svg class="code-collapse-icon" width="32" height="24" viewBox="0 0 32 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="4,9 12,17 20,9"></polyline></svg></label></div><div class="code-collapse-full"><div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#6A737D;--s-dark:#6A737D">#!/usr/bin/env bash</span></span>
<span class="line"></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">set</span><span style="color:#005CC5;--s-dark:#79B8FF"> -ex</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D"># ENABLE ipv4 forward</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">sysctl</span><span style="color:#005CC5;--s-dark:#79B8FF"> -w</span><span style="color:#032F62;--s-dark:#9ECBFF"> net.ipv4.ip_forward=</span><span style="color:#005CC5;--s-dark:#79B8FF">1</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D"># ROUTE RULES</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">ip</span><span style="color:#032F62;--s-dark:#9ECBFF"> rule</span><span style="color:#032F62;--s-dark:#9ECBFF"> add</span><span style="color:#032F62;--s-dark:#9ECBFF"> fwmark</span><span style="color:#005CC5;--s-dark:#79B8FF"> 666</span><span style="color:#032F62;--s-dark:#9ECBFF"> lookup</span><span style="color:#005CC5;--s-dark:#79B8FF"> 666</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">ip</span><span style="color:#032F62;--s-dark:#9ECBFF"> route</span><span style="color:#032F62;--s-dark:#9ECBFF"> add</span><span style="color:#032F62;--s-dark:#9ECBFF"> local</span><span style="color:#032F62;--s-dark:#9ECBFF"> 0.0.0.0/0</span><span style="color:#032F62;--s-dark:#9ECBFF"> dev</span><span style="color:#032F62;--s-dark:#9ECBFF"> lo</span><span style="color:#032F62;--s-dark:#9ECBFF"> table</span><span style="color:#005CC5;--s-dark:#79B8FF"> 666</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D"># clash 链负责处理转发流量</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">iptables</span><span style="color:#005CC5;--s-dark:#79B8FF"> -t</span><span style="color:#032F62;--s-dark:#9ECBFF"> mangle</span><span style="color:#005CC5;--s-dark:#79B8FF"> -N</span><span style="color:#032F62;--s-dark:#9ECBFF"> clash</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D"># 目标地址为局域网或保留地址的流量跳过处理</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D"># 保留地址参考：https://zh.wikipedia.org/wiki/%E5%B7%B2%E5%88%86%E9%85%8D%E7%9A%84/8_IPv4%E5%9C%B0%E5%9D%80%E5%9D%97%E5%88%97%E8%A1%A8</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">iptables</span><span style="color:#005CC5;--s-dark:#79B8FF"> -t</span><span style="color:#032F62;--s-dark:#9ECBFF"> mangle</span><span style="color:#005CC5;--s-dark:#79B8FF"> -A</span><span style="color:#032F62;--s-dark:#9ECBFF"> clash</span><span style="color:#005CC5;--s-dark:#79B8FF"> -d</span><span style="color:#032F62;--s-dark:#9ECBFF"> 0.0.0.0/8</span><span style="color:#005CC5;--s-dark:#79B8FF"> -j</span><span style="color:#032F62;--s-dark:#9ECBFF"> RETURN</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">iptables</span><span style="color:#005CC5;--s-dark:#79B8FF"> -t</span><span style="color:#032F62;--s-dark:#9ECBFF"> mangle</span><span style="color:#005CC5;--s-dark:#79B8FF"> -A</span><span style="color:#032F62;--s-dark:#9ECBFF"> clash</span><span style="color:#005CC5;--s-dark:#79B8FF"> -d</span><span style="color:#032F62;--s-dark:#9ECBFF"> 127.0.0.0/8</span><span style="color:#005CC5;--s-dark:#79B8FF"> -j</span><span style="color:#032F62;--s-dark:#9ECBFF"> RETURN</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">iptables</span><span style="color:#005CC5;--s-dark:#79B8FF"> -t</span><span style="color:#032F62;--s-dark:#9ECBFF"> mangle</span><span style="color:#005CC5;--s-dark:#79B8FF"> -A</span><span style="color:#032F62;--s-dark:#9ECBFF"> clash</span><span style="color:#005CC5;--s-dark:#79B8FF"> -d</span><span style="color:#032F62;--s-dark:#9ECBFF"> 10.0.0.0/8</span><span style="color:#005CC5;--s-dark:#79B8FF"> -j</span><span style="color:#032F62;--s-dark:#9ECBFF"> RETURN</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">iptables</span><span style="color:#005CC5;--s-dark:#79B8FF"> -t</span><span style="color:#032F62;--s-dark:#9ECBFF"> mangle</span><span style="color:#005CC5;--s-dark:#79B8FF"> -A</span><span style="color:#032F62;--s-dark:#9ECBFF"> clash</span><span style="color:#005CC5;--s-dark:#79B8FF"> -d</span><span style="color:#032F62;--s-dark:#9ECBFF"> 172.16.0.0/12</span><span style="color:#005CC5;--s-dark:#79B8FF"> -j</span><span style="color:#032F62;--s-dark:#9ECBFF"> RETURN</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">iptables</span><span style="color:#005CC5;--s-dark:#79B8FF"> -t</span><span style="color:#032F62;--s-dark:#9ECBFF"> mangle</span><span style="color:#005CC5;--s-dark:#79B8FF"> -A</span><span style="color:#032F62;--s-dark:#9ECBFF"> clash</span><span style="color:#005CC5;--s-dark:#79B8FF"> -d</span><span style="color:#032F62;--s-dark:#9ECBFF"> 192.168.0.0/16</span><span style="color:#005CC5;--s-dark:#79B8FF"> -j</span><span style="color:#032F62;--s-dark:#9ECBFF"> RETURN</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">iptables</span><span style="color:#005CC5;--s-dark:#79B8FF"> -t</span><span style="color:#032F62;--s-dark:#9ECBFF"> mangle</span><span style="color:#005CC5;--s-dark:#79B8FF"> -A</span><span style="color:#032F62;--s-dark:#9ECBFF"> clash</span><span style="color:#005CC5;--s-dark:#79B8FF"> -d</span><span style="color:#032F62;--s-dark:#9ECBFF"> 169.254.0.0/16</span><span style="color:#005CC5;--s-dark:#79B8FF"> -j</span><span style="color:#032F62;--s-dark:#9ECBFF"> RETURN</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">iptables</span><span style="color:#005CC5;--s-dark:#79B8FF"> -t</span><span style="color:#032F62;--s-dark:#9ECBFF"> mangle</span><span style="color:#005CC5;--s-dark:#79B8FF"> -A</span><span style="color:#032F62;--s-dark:#9ECBFF"> clash</span><span style="color:#005CC5;--s-dark:#79B8FF"> -d</span><span style="color:#032F62;--s-dark:#9ECBFF"> 224.0.0.0/4</span><span style="color:#005CC5;--s-dark:#79B8FF"> -j</span><span style="color:#032F62;--s-dark:#9ECBFF"> RETURN</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">iptables</span><span style="color:#005CC5;--s-dark:#79B8FF"> -t</span><span style="color:#032F62;--s-dark:#9ECBFF"> mangle</span><span style="color:#005CC5;--s-dark:#79B8FF"> -A</span><span style="color:#032F62;--s-dark:#9ECBFF"> clash</span><span style="color:#005CC5;--s-dark:#79B8FF"> -d</span><span style="color:#032F62;--s-dark:#9ECBFF"> 240.0.0.0/4</span><span style="color:#005CC5;--s-dark:#79B8FF"> -j</span><span style="color:#032F62;--s-dark:#9ECBFF"> RETURN</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D"># 其他所有流量转向到 7893 端口，并打上 mark</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">iptables</span><span style="color:#005CC5;--s-dark:#79B8FF"> -t</span><span style="color:#032F62;--s-dark:#9ECBFF"> mangle</span><span style="color:#005CC5;--s-dark:#79B8FF"> -A</span><span style="color:#032F62;--s-dark:#9ECBFF"> clash</span><span style="color:#005CC5;--s-dark:#79B8FF"> -p</span><span style="color:#032F62;--s-dark:#9ECBFF"> tcp</span><span style="color:#005CC5;--s-dark:#79B8FF"> -j</span><span style="color:#032F62;--s-dark:#9ECBFF"> TPROXY</span><span style="color:#005CC5;--s-dark:#79B8FF"> --on-port</span><span style="color:#005CC5;--s-dark:#79B8FF"> 7893</span><span style="color:#005CC5;--s-dark:#79B8FF"> --tproxy-mark</span><span style="color:#005CC5;--s-dark:#79B8FF"> 666</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">iptables</span><span style="color:#005CC5;--s-dark:#79B8FF"> -t</span><span style="color:#032F62;--s-dark:#9ECBFF"> mangle</span><span style="color:#005CC5;--s-dark:#79B8FF"> -A</span><span style="color:#032F62;--s-dark:#9ECBFF"> clash</span><span style="color:#005CC5;--s-dark:#79B8FF"> -p</span><span style="color:#032F62;--s-dark:#9ECBFF"> udp</span><span style="color:#005CC5;--s-dark:#79B8FF"> -j</span><span style="color:#032F62;--s-dark:#9ECBFF"> TPROXY</span><span style="color:#005CC5;--s-dark:#79B8FF"> --on-port</span><span style="color:#005CC5;--s-dark:#79B8FF"> 7893</span><span style="color:#005CC5;--s-dark:#79B8FF"> --tproxy-mark</span><span style="color:#005CC5;--s-dark:#79B8FF"> 666</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D"># 最后让所有流量通过 clash 链进行处理</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">iptables</span><span style="color:#005CC5;--s-dark:#79B8FF"> -t</span><span style="color:#032F62;--s-dark:#9ECBFF"> mangle</span><span style="color:#005CC5;--s-dark:#79B8FF"> -A</span><span style="color:#032F62;--s-dark:#9ECBFF"> PREROUTING</span><span style="color:#005CC5;--s-dark:#79B8FF"> -j</span><span style="color:#032F62;--s-dark:#9ECBFF"> clash</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D"># clash_local 链负责处理网关本身发出的流量</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">iptables</span><span style="color:#005CC5;--s-dark:#79B8FF"> -t</span><span style="color:#032F62;--s-dark:#9ECBFF"> mangle</span><span style="color:#005CC5;--s-dark:#79B8FF"> -N</span><span style="color:#032F62;--s-dark:#9ECBFF"> clash_local</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D"># 跳过内网流量</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">iptables</span><span style="color:#005CC5;--s-dark:#79B8FF"> -t</span><span style="color:#032F62;--s-dark:#9ECBFF"> mangle</span><span style="color:#005CC5;--s-dark:#79B8FF"> -A</span><span style="color:#032F62;--s-dark:#9ECBFF"> clash_local</span><span style="color:#005CC5;--s-dark:#79B8FF"> -d</span><span style="color:#032F62;--s-dark:#9ECBFF"> 0.0.0.0/8</span><span style="color:#005CC5;--s-dark:#79B8FF"> -j</span><span style="color:#032F62;--s-dark:#9ECBFF"> RETURN</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">iptables</span><span style="color:#005CC5;--s-dark:#79B8FF"> -t</span><span style="color:#032F62;--s-dark:#9ECBFF"> mangle</span><span style="color:#005CC5;--s-dark:#79B8FF"> -A</span><span style="color:#032F62;--s-dark:#9ECBFF"> clash_local</span><span style="color:#005CC5;--s-dark:#79B8FF"> -d</span><span style="color:#032F62;--s-dark:#9ECBFF"> 127.0.0.0/8</span><span style="color:#005CC5;--s-dark:#79B8FF"> -j</span><span style="color:#032F62;--s-dark:#9ECBFF"> RETURN</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">iptables</span><span style="color:#005CC5;--s-dark:#79B8FF"> -t</span><span style="color:#032F62;--s-dark:#9ECBFF"> mangle</span><span style="color:#005CC5;--s-dark:#79B8FF"> -A</span><span style="color:#032F62;--s-dark:#9ECBFF"> clash_local</span><span style="color:#005CC5;--s-dark:#79B8FF"> -d</span><span style="color:#032F62;--s-dark:#9ECBFF"> 10.0.0.0/8</span><span style="color:#005CC5;--s-dark:#79B8FF"> -j</span><span style="color:#032F62;--s-dark:#9ECBFF"> RETURN</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">iptables</span><span style="color:#005CC5;--s-dark:#79B8FF"> -t</span><span style="color:#032F62;--s-dark:#9ECBFF"> mangle</span><span style="color:#005CC5;--s-dark:#79B8FF"> -A</span><span style="color:#032F62;--s-dark:#9ECBFF"> clash_local</span><span style="color:#005CC5;--s-dark:#79B8FF"> -d</span><span style="color:#032F62;--s-dark:#9ECBFF"> 172.16.0.0/12</span><span style="color:#005CC5;--s-dark:#79B8FF"> -j</span><span style="color:#032F62;--s-dark:#9ECBFF"> RETURN</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">iptables</span><span style="color:#005CC5;--s-dark:#79B8FF"> -t</span><span style="color:#032F62;--s-dark:#9ECBFF"> mangle</span><span style="color:#005CC5;--s-dark:#79B8FF"> -A</span><span style="color:#032F62;--s-dark:#9ECBFF"> clash_local</span><span style="color:#005CC5;--s-dark:#79B8FF"> -d</span><span style="color:#032F62;--s-dark:#9ECBFF"> 192.168.0.0/16</span><span style="color:#005CC5;--s-dark:#79B8FF"> -j</span><span style="color:#032F62;--s-dark:#9ECBFF"> RETURN</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">iptables</span><span style="color:#005CC5;--s-dark:#79B8FF"> -t</span><span style="color:#032F62;--s-dark:#9ECBFF"> mangle</span><span style="color:#005CC5;--s-dark:#79B8FF"> -A</span><span style="color:#032F62;--s-dark:#9ECBFF"> clash_local</span><span style="color:#005CC5;--s-dark:#79B8FF"> -d</span><span style="color:#032F62;--s-dark:#9ECBFF"> 169.254.0.0/16</span><span style="color:#005CC5;--s-dark:#79B8FF"> -j</span><span style="color:#032F62;--s-dark:#9ECBFF"> RETURN</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">iptables</span><span style="color:#005CC5;--s-dark:#79B8FF"> -t</span><span style="color:#032F62;--s-dark:#9ECBFF"> mangle</span><span style="color:#005CC5;--s-dark:#79B8FF"> -A</span><span style="color:#032F62;--s-dark:#9ECBFF"> clash_local</span><span style="color:#005CC5;--s-dark:#79B8FF"> -d</span><span style="color:#032F62;--s-dark:#9ECBFF"> 224.0.0.0/4</span><span style="color:#005CC5;--s-dark:#79B8FF"> -j</span><span style="color:#032F62;--s-dark:#9ECBFF"> RETURN</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">iptables</span><span style="color:#005CC5;--s-dark:#79B8FF"> -t</span><span style="color:#032F62;--s-dark:#9ECBFF"> mangle</span><span style="color:#005CC5;--s-dark:#79B8FF"> -A</span><span style="color:#032F62;--s-dark:#9ECBFF"> clash_local</span><span style="color:#005CC5;--s-dark:#79B8FF"> -d</span><span style="color:#032F62;--s-dark:#9ECBFF"> 240.0.0.0/4</span><span style="color:#005CC5;--s-dark:#79B8FF"> -j</span><span style="color:#032F62;--s-dark:#9ECBFF"> RETURN</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D"># 为本机发出的流量打 mark</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">iptables</span><span style="color:#005CC5;--s-dark:#79B8FF"> -t</span><span style="color:#032F62;--s-dark:#9ECBFF"> mangle</span><span style="color:#005CC5;--s-dark:#79B8FF"> -A</span><span style="color:#032F62;--s-dark:#9ECBFF"> clash_local</span><span style="color:#005CC5;--s-dark:#79B8FF"> -p</span><span style="color:#032F62;--s-dark:#9ECBFF"> tcp</span><span style="color:#005CC5;--s-dark:#79B8FF"> -j</span><span style="color:#032F62;--s-dark:#9ECBFF"> MARK</span><span style="color:#005CC5;--s-dark:#79B8FF"> --set-mark</span><span style="color:#005CC5;--s-dark:#79B8FF"> 666</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">iptables</span><span style="color:#005CC5;--s-dark:#79B8FF"> -t</span><span style="color:#032F62;--s-dark:#9ECBFF"> mangle</span><span style="color:#005CC5;--s-dark:#79B8FF"> -A</span><span style="color:#032F62;--s-dark:#9ECBFF"> clash_local</span><span style="color:#005CC5;--s-dark:#79B8FF"> -p</span><span style="color:#032F62;--s-dark:#9ECBFF"> udp</span><span style="color:#005CC5;--s-dark:#79B8FF"> -j</span><span style="color:#032F62;--s-dark:#9ECBFF"> MARK</span><span style="color:#005CC5;--s-dark:#79B8FF"> --set-mark</span><span style="color:#005CC5;--s-dark:#79B8FF"> 666</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D"># 跳过 clash 程序本身发出的流量，防止死循环 (clash 程序需要使用 "clash" 用户启动)</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">iptables</span><span style="color:#005CC5;--s-dark:#79B8FF"> -t</span><span style="color:#032F62;--s-dark:#9ECBFF"> mangle</span><span style="color:#005CC5;--s-dark:#79B8FF"> -A</span><span style="color:#032F62;--s-dark:#9ECBFF"> OUTPUT</span><span style="color:#005CC5;--s-dark:#79B8FF"> -p</span><span style="color:#032F62;--s-dark:#9ECBFF"> tcp</span><span style="color:#005CC5;--s-dark:#79B8FF"> -m</span><span style="color:#032F62;--s-dark:#9ECBFF"> owner</span><span style="color:#005CC5;--s-dark:#79B8FF"> --uid-owner</span><span style="color:#032F62;--s-dark:#9ECBFF"> clash</span><span style="color:#005CC5;--s-dark:#79B8FF"> -j</span><span style="color:#032F62;--s-dark:#9ECBFF"> RETURN</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">iptables</span><span style="color:#005CC5;--s-dark:#79B8FF"> -t</span><span style="color:#032F62;--s-dark:#9ECBFF"> mangle</span><span style="color:#005CC5;--s-dark:#79B8FF"> -A</span><span style="color:#032F62;--s-dark:#9ECBFF"> OUTPUT</span><span style="color:#005CC5;--s-dark:#79B8FF"> -p</span><span style="color:#032F62;--s-dark:#9ECBFF"> udp</span><span style="color:#005CC5;--s-dark:#79B8FF"> -m</span><span style="color:#032F62;--s-dark:#9ECBFF"> owner</span><span style="color:#005CC5;--s-dark:#79B8FF"> --uid-owner</span><span style="color:#032F62;--s-dark:#9ECBFF"> clash</span><span style="color:#005CC5;--s-dark:#79B8FF"> -j</span><span style="color:#032F62;--s-dark:#9ECBFF"> RETURN</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D"># 让本机发出的流量跳转到 clash_local</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D"># clash_local 链会为本机流量打 mark, 打过 mark 的流量会重新回到 PREROUTING 上</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">iptables</span><span style="color:#005CC5;--s-dark:#79B8FF"> -t</span><span style="color:#032F62;--s-dark:#9ECBFF"> mangle</span><span style="color:#005CC5;--s-dark:#79B8FF"> -A</span><span style="color:#032F62;--s-dark:#9ECBFF"> OUTPUT</span><span style="color:#005CC5;--s-dark:#79B8FF"> -j</span><span style="color:#032F62;--s-dark:#9ECBFF"> clash_local</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D"># 修复 ICMP(ping)</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D"># 这并不能保证 ping 结果有效 (clash 等不支持转发 ICMP), 只是让它有返回结果而已</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D"># --to-destination 设置为一个可达的地址即可</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">sysctl</span><span style="color:#005CC5;--s-dark:#79B8FF"> -w</span><span style="color:#032F62;--s-dark:#9ECBFF"> net.ipv4.conf.all.route_localnet=</span><span style="color:#005CC5;--s-dark:#79B8FF">1</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">iptables</span><span style="color:#005CC5;--s-dark:#79B8FF"> -t</span><span style="color:#032F62;--s-dark:#9ECBFF"> nat</span><span style="color:#005CC5;--s-dark:#79B8FF"> -A</span><span style="color:#032F62;--s-dark:#9ECBFF"> PREROUTING</span><span style="color:#005CC5;--s-dark:#79B8FF"> -p</span><span style="color:#032F62;--s-dark:#9ECBFF"> icmp</span><span style="color:#005CC5;--s-dark:#79B8FF"> -d</span><span style="color:#032F62;--s-dark:#9ECBFF"> 198.18.0.0/16</span><span style="color:#005CC5;--s-dark:#79B8FF"> -j</span><span style="color:#032F62;--s-dark:#9ECBFF"> DNAT</span><span style="color:#005CC5;--s-dark:#79B8FF"> --to-destination</span><span style="color:#005CC5;--s-dark:#79B8FF"> 127.0.0.1</span></span></code><label class="code-collapse-collapse" for="toggle-30wc6vk"><svg class="code-collapse-icon" width="32" height="24" viewBox="0 0 32 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20,15 12,7 4,15"></polyline></svg></label></pre></div></div></div>
<div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#6A737D;--s-dark:#6A737D">#!/usr/bin/env bash</span></span>
<span class="line"></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">set</span><span style="color:#005CC5;--s-dark:#79B8FF"> -ex</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">ip</span><span style="color:#032F62;--s-dark:#9ECBFF"> rule</span><span style="color:#032F62;--s-dark:#9ECBFF"> del</span><span style="color:#032F62;--s-dark:#9ECBFF"> fwmark</span><span style="color:#005CC5;--s-dark:#79B8FF"> 666</span><span style="color:#032F62;--s-dark:#9ECBFF"> table</span><span style="color:#005CC5;--s-dark:#79B8FF"> 666</span><span style="color:#D73A49;--s-dark:#F97583"> ||</span><span style="color:#005CC5;--s-dark:#79B8FF"> true</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">ip</span><span style="color:#032F62;--s-dark:#9ECBFF"> route</span><span style="color:#032F62;--s-dark:#9ECBFF"> del</span><span style="color:#032F62;--s-dark:#9ECBFF"> local</span><span style="color:#032F62;--s-dark:#9ECBFF"> 0.0.0.0/0</span><span style="color:#032F62;--s-dark:#9ECBFF"> dev</span><span style="color:#032F62;--s-dark:#9ECBFF"> lo</span><span style="color:#032F62;--s-dark:#9ECBFF"> table</span><span style="color:#005CC5;--s-dark:#79B8FF"> 666</span><span style="color:#D73A49;--s-dark:#F97583"> ||</span><span style="color:#005CC5;--s-dark:#79B8FF"> true</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">iptables</span><span style="color:#005CC5;--s-dark:#79B8FF"> -t</span><span style="color:#032F62;--s-dark:#9ECBFF"> nat</span><span style="color:#005CC5;--s-dark:#79B8FF"> -F</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">iptables</span><span style="color:#005CC5;--s-dark:#79B8FF"> -t</span><span style="color:#032F62;--s-dark:#9ECBFF"> nat</span><span style="color:#005CC5;--s-dark:#79B8FF"> -X</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">iptables</span><span style="color:#005CC5;--s-dark:#79B8FF"> -t</span><span style="color:#032F62;--s-dark:#9ECBFF"> mangle</span><span style="color:#005CC5;--s-dark:#79B8FF"> -F</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">iptables</span><span style="color:#005CC5;--s-dark:#79B8FF"> -t</span><span style="color:#032F62;--s-dark:#9ECBFF"> mangle</span><span style="color:#005CC5;--s-dark:#79B8FF"> -X</span><span style="color:#032F62;--s-dark:#9ECBFF"> clash</span><span style="color:#D73A49;--s-dark:#F97583"> ||</span><span style="color:#005CC5;--s-dark:#79B8FF"> true</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">iptables</span><span style="color:#005CC5;--s-dark:#79B8FF"> -t</span><span style="color:#032F62;--s-dark:#9ECBFF"> mangle</span><span style="color:#005CC5;--s-dark:#79B8FF"> -X</span><span style="color:#032F62;--s-dark:#9ECBFF"> clash_local</span><span style="color:#D73A49;--s-dark:#F97583"> ||</span><span style="color:#005CC5;--s-dark:#79B8FF"> true</span></span></code></pre></div>
<p>每行都有详细的注释，再细节可以去问 ChatGPT</p>
<h4 id="clash-配置文件">clash 配置文件<a href="#clash-配置文件" class="heading-anchor-link" aria-label="Link to clash 配置文件"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h4>
<p>clash 配置文件可以从各订阅商处获得，格式应当是 yaml。将其保存到 <code>/var/lib/clash/config.yaml</code>，并将其按照如下模块更改：</p>
<div class="code-collapse-wrapper"><input type="checkbox" id="toggle-xeuqkg4" class="code-collapse-toggle" style="display: none;"><div class="code-collapse-preview"><div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0" class="code-preview"><code><span class="line"><span style="color:#22863A;--s-dark:#85E89D">tproxy-port</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">7893</span><span style="color:#6A737D;--s-dark:#6A737D">   # iptables.sh 将所有流量转发到了 7893 端口</span></span>
<span class="line"><span style="color:#22863A;--s-dark:#85E89D">mixed-port</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">7890</span></span>
<span class="line"><span style="color:#22863A;--s-dark:#85E89D">allow-lan</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">true</span></span>
<span class="line"><span style="color:#22863A;--s-dark:#85E89D">find-process-mode</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">off</span></span>
<span class="line"><span style="color:#22863A;--s-dark:#85E89D">bind-address</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">"*"</span></span>
<span class="line"><span style="color:#22863A;--s-dark:#85E89D">mode</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">rule</span></span>
<span class="line"><span style="color:#22863A;--s-dark:#85E89D">log-level</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">debug</span></span>
<span class="line"><span style="color:#22863A;--s-dark:#85E89D">ipv6</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">false</span><span style="color:#6A737D;--s-dark:#6A737D"> # 不进行 IPv6 流量代理</span></span>
<span class="line"></span>
<span class="line"><span style="color:#22863A;--s-dark:#85E89D">external-controller</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">0.0.0.0:9090</span></span>
<span class="line"><span style="color:#22863A;--s-dark:#85E89D">secret</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#6A737D;--s-dark:#6A737D"># 登陆 ui 的密码</span></span>
<span class="line"><span style="color:#22863A;--s-dark:#85E89D">external-ui</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">ui</span><span style="color:#6A737D;--s-dark:#6A737D"> # webui 的基础路径</span></span>
<span class="line"><span style="color:#22863A;--s-dark:#85E89D">external-ui-name</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">xd</span><span style="color:#6A737D;--s-dark:#6A737D"> # webui 的下级路径</span></span>
<span class="line"><span style="color:#22863A;--s-dark:#85E89D">external-ui-url</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">https://github.com/MetaCubeX/metacubexd/archive/refs/heads/gh-pages.zip</span></span>
<span class="line"><span style="color:#22863A;--s-dark:#85E89D">unified-delay</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">true</span></span>
<span class="line"><span style="color:#22863A;--s-dark:#85E89D">tcp-concurrent</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">true</span></span>
<span class="line"><span style="color:#22863A;--s-dark:#85E89D">experimental</span><span style="color:#24292E;--s-dark:#E1E4E8">:</span></span>
<span class="line"><span style="color:#22863A;--s-dark:#85E89D">  sniff-tls-sni</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">true</span></span>
<span class="line"><span style="color:#22863A;--s-dark:#85E89D">geodata-mode</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">true</span></span>
<span class="line"><span style="color:#22863A;--s-dark:#85E89D">geodata-loader</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">standard</span></span>
<span class="line"><span style="color:#22863A;--s-dark:#85E89D">geox-url</span><span style="color:#24292E;--s-dark:#E1E4E8">:</span></span>
<span class="line"><span style="color:#22863A;--s-dark:#85E89D">  geoip</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@release/geoip.dat</span></span>
<span class="line"><span style="color:#22863A;--s-dark:#85E89D">  geosite</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@release/geosite.dat</span></span>
<span class="line"><span style="color:#22863A;--s-dark:#85E89D">  mmdb</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@release/country.mmdb</span></span>
<span class="line"><span style="color:#22863A;--s-dark:#85E89D">profile</span><span style="color:#24292E;--s-dark:#E1E4E8">:</span></span>
</code></pre></div><div class="code-collapse-gradient"></div><label class="code-collapse-expand" for="toggle-xeuqkg4"><svg class="code-collapse-icon" width="32" height="24" viewBox="0 0 32 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="4,9 12,17 20,9"></polyline></svg></label></div><div class="code-collapse-full"><div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#22863A;--s-dark:#85E89D">tproxy-port</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">7893</span><span style="color:#6A737D;--s-dark:#6A737D">   # iptables.sh 将所有流量转发到了 7893 端口</span></span>
<span class="line"><span style="color:#22863A;--s-dark:#85E89D">mixed-port</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">7890</span></span>
<span class="line"><span style="color:#22863A;--s-dark:#85E89D">allow-lan</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">true</span></span>
<span class="line"><span style="color:#22863A;--s-dark:#85E89D">find-process-mode</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">off</span></span>
<span class="line"><span style="color:#22863A;--s-dark:#85E89D">bind-address</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">"*"</span></span>
<span class="line"><span style="color:#22863A;--s-dark:#85E89D">mode</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">rule</span></span>
<span class="line"><span style="color:#22863A;--s-dark:#85E89D">log-level</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">debug</span></span>
<span class="line"><span style="color:#22863A;--s-dark:#85E89D">ipv6</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">false</span><span style="color:#6A737D;--s-dark:#6A737D"> # 不进行 IPv6 流量代理</span></span>
<span class="line"></span>
<span class="line"><span style="color:#22863A;--s-dark:#85E89D">external-controller</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">0.0.0.0:9090</span></span>
<span class="line"><span style="color:#22863A;--s-dark:#85E89D">secret</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#6A737D;--s-dark:#6A737D"># 登陆 ui 的密码</span></span>
<span class="line"><span style="color:#22863A;--s-dark:#85E89D">external-ui</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">ui</span><span style="color:#6A737D;--s-dark:#6A737D"> # webui 的基础路径</span></span>
<span class="line"><span style="color:#22863A;--s-dark:#85E89D">external-ui-name</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">xd</span><span style="color:#6A737D;--s-dark:#6A737D"> # webui 的下级路径</span></span>
<span class="line"><span style="color:#22863A;--s-dark:#85E89D">external-ui-url</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">https://github.com/MetaCubeX/metacubexd/archive/refs/heads/gh-pages.zip</span></span>
<span class="line"><span style="color:#22863A;--s-dark:#85E89D">unified-delay</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">true</span></span>
<span class="line"><span style="color:#22863A;--s-dark:#85E89D">tcp-concurrent</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">true</span></span>
<span class="line"><span style="color:#22863A;--s-dark:#85E89D">experimental</span><span style="color:#24292E;--s-dark:#E1E4E8">:</span></span>
<span class="line"><span style="color:#22863A;--s-dark:#85E89D">  sniff-tls-sni</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">true</span></span>
<span class="line"><span style="color:#22863A;--s-dark:#85E89D">geodata-mode</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">true</span></span>
<span class="line"><span style="color:#22863A;--s-dark:#85E89D">geodata-loader</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">standard</span></span>
<span class="line"><span style="color:#22863A;--s-dark:#85E89D">geox-url</span><span style="color:#24292E;--s-dark:#E1E4E8">:</span></span>
<span class="line"><span style="color:#22863A;--s-dark:#85E89D">  geoip</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@release/geoip.dat</span></span>
<span class="line"><span style="color:#22863A;--s-dark:#85E89D">  geosite</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@release/geosite.dat</span></span>
<span class="line"><span style="color:#22863A;--s-dark:#85E89D">  mmdb</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@release/country.mmdb</span></span>
<span class="line"><span style="color:#22863A;--s-dark:#85E89D">profile</span><span style="color:#24292E;--s-dark:#E1E4E8">:</span></span>
<span class="line"><span style="color:#22863A;--s-dark:#85E89D">  tracing</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">true</span></span>
<span class="line"><span style="color:#22863A;--s-dark:#85E89D">  store-selected</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">true</span></span>
<span class="line"><span style="color:#22863A;--s-dark:#85E89D">  store-fake-ip</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">true</span></span>
<span class="line"><span style="color:#22863A;--s-dark:#85E89D">sniffer</span><span style="color:#24292E;--s-dark:#E1E4E8">:</span></span>
<span class="line"><span style="color:#22863A;--s-dark:#85E89D">  enable</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">true</span></span>
<span class="line"><span style="color:#22863A;--s-dark:#85E89D">  parse-pure-ip</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">true</span></span>
<span class="line"><span style="color:#22863A;--s-dark:#85E89D">  override-destination</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">true</span></span>
<span class="line"></span>
<span class="line"><span style="color:#22863A;--s-dark:#85E89D">dns</span><span style="color:#24292E;--s-dark:#E1E4E8">:</span></span>
<span class="line"><span style="color:#22863A;--s-dark:#85E89D">  enable</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">true</span></span>
<span class="line"><span style="color:#22863A;--s-dark:#85E89D">  ipv6</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">false</span></span>
<span class="line"><span style="color:#22863A;--s-dark:#85E89D">  listen</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">0.0.0.0:1053</span><span style="color:#6A737D;--s-dark:#6A737D"> # DNS 监听端口</span></span>
<span class="line"><span style="color:#22863A;--s-dark:#85E89D">  use-hosts</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">true</span></span>
<span class="line"><span style="color:#22863A;--s-dark:#85E89D">  enhanced-mode</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">fake-ip</span></span>
<span class="line"><span style="color:#22863A;--s-dark:#85E89D">  default-nameserver</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#6A737D;--s-dark:#6A737D"># 建议修改为国内 DNS 服务器</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    - </span><span style="color:#005CC5;--s-dark:#79B8FF">223.5.5.5</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    - </span><span style="color:#005CC5;--s-dark:#79B8FF">119.29.29.29</span></span>
<span class="line"><span style="color:#22863A;--s-dark:#85E89D">  nameserver</span><span style="color:#24292E;--s-dark:#E1E4E8">:</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    - </span><span style="color:#032F62;--s-dark:#9ECBFF">https://doh.pub/dns-query</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    - </span><span style="color:#032F62;--s-dark:#9ECBFF">tls://dot.pub</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    - </span><span style="color:#032F62;--s-dark:#9ECBFF">tls://dns.alidns.com</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    - </span><span style="color:#032F62;--s-dark:#9ECBFF">https://dns.alidns.com/dns-query</span></span>
<span class="line"><span style="color:#22863A;--s-dark:#85E89D">  fallback</span><span style="color:#24292E;--s-dark:#E1E4E8">:</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    - </span><span style="color:#032F62;--s-dark:#9ECBFF">https://dns.cloudflare.com/dns-query</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    - </span><span style="color:#032F62;--s-dark:#9ECBFF">tls://dns.google:853</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    - </span><span style="color:#032F62;--s-dark:#9ECBFF">https://1.1.1.1/dns-query</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    - </span><span style="color:#032F62;--s-dark:#9ECBFF">tls://1.1.1.1:853</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    - </span><span style="color:#032F62;--s-dark:#9ECBFF">tls://8.8.8.8:853</span></span>
<span class="line"><span style="color:#22863A;--s-dark:#85E89D">  fake-ip-filter</span><span style="color:#24292E;--s-dark:#E1E4E8">:</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    - </span><span style="color:#032F62;--s-dark:#9ECBFF">'+.lan'</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    - </span><span style="color:#032F62;--s-dark:#9ECBFF">'+.cluster.local'</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    - </span><span style="color:#032F62;--s-dark:#9ECBFF">'time.*.com'</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    - </span><span style="color:#032F62;--s-dark:#9ECBFF">'time.*.gov'</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    - </span><span style="color:#032F62;--s-dark:#9ECBFF">'time.*.edu.cn'</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    - </span><span style="color:#032F62;--s-dark:#9ECBFF">'time.*.apple.com'</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    - </span><span style="color:#032F62;--s-dark:#9ECBFF">'ntp.*.com'</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    - </span><span style="color:#032F62;--s-dark:#9ECBFF">'localhost.ptlogin2.qq.com'</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    - </span><span style="color:#032F62;--s-dark:#9ECBFF">'+.ntp.org.cn'</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    - </span><span style="color:#032F62;--s-dark:#9ECBFF">'+.pool.ntp.org'</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    - </span><span style="color:#032F62;--s-dark:#9ECBFF">'+.localhost'</span></span>
<span class="line"><span style="color:#22863A;--s-dark:#85E89D">  fallback-filter</span><span style="color:#24292E;--s-dark:#E1E4E8">:</span></span>
<span class="line"><span style="color:#22863A;--s-dark:#85E89D">    geoip</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#005CC5;--s-dark:#79B8FF">true</span></span>
<span class="line"><span style="color:#22863A;--s-dark:#85E89D">    geoip-code</span><span style="color:#24292E;--s-dark:#E1E4E8">: </span><span style="color:#032F62;--s-dark:#9ECBFF">CN</span></span>
<span class="line"><span style="color:#22863A;--s-dark:#85E89D">    geosite</span><span style="color:#24292E;--s-dark:#E1E4E8">:</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">      - </span><span style="color:#032F62;--s-dark:#9ECBFF">gfw</span></span>
<span class="line"><span style="color:#22863A;--s-dark:#85E89D">    ipcidr</span><span style="color:#24292E;--s-dark:#E1E4E8">:</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">      - </span><span style="color:#032F62;--s-dark:#9ECBFF">224.0.0.0/4</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">      - </span><span style="color:#032F62;--s-dark:#9ECBFF">240.0.0.0/4</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">      - </span><span style="color:#032F62;--s-dark:#9ECBFF">169.254.0.0/16</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">      - </span><span style="color:#032F62;--s-dark:#9ECBFF">0.0.0.0/8</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">      - </span><span style="color:#032F62;--s-dark:#9ECBFF">127.0.0.1/32</span></span>
<span class="line"><span style="color:#22863A;--s-dark:#85E89D">    domain</span><span style="color:#24292E;--s-dark:#E1E4E8">:</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">      - </span><span style="color:#032F62;--s-dark:#9ECBFF">'+.google.com'</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">      - </span><span style="color:#032F62;--s-dark:#9ECBFF">'+.facebook.com'</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">      - </span><span style="color:#032F62;--s-dark:#9ECBFF">'+.youtube.com'</span></span>
<span class="line"></span>
<span class="line"><span style="color:#22863A;--s-dark:#85E89D">proxies</span><span style="color:#24292E;--s-dark:#E1E4E8">:  </span><span style="color:#6A737D;--s-dark:#6A737D"># 以下为你的代理节点、分组及代理规则</span></span>
<span class="line"><span style="color:#22863A;--s-dark:#85E89D">proxy-groups</span><span style="color:#24292E;--s-dark:#E1E4E8">:</span></span>
<span class="line"><span style="color:#22863A;--s-dark:#85E89D">rules</span><span style="color:#24292E;--s-dark:#E1E4E8">:</span></span></code><label class="code-collapse-collapse" for="toggle-xeuqkg4"><svg class="code-collapse-icon" width="32" height="24" viewBox="0 0 32 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20,15 12,7 4,15"></polyline></svg></label></pre></div></div></div>
<p>这里着重说一下 dns 部分，dns 分成了两个 dns server 组：</p>
<ul>
<li>nameserver 部分为国内的公共 DNS</li>
<li>fallback 部分为国外的公共 DNS</li>
</ul>
<p>fallback-filter 控制了当域名满足什么条件时，使用 fallback 组的 DNS 解析结果</p>
<ul>
<li>geoip-code 为反向条件，即通过 nameserver 解析出的 IP 未命中 geoip-code 则会使用 fallback 的解析结果</li>
<li>geosite 为正向条件，匹配 geosite 中的域名会使用 fallback</li>
<li>ipcidr 为正向条件，nameserver 解析出这些结果时（即污染 IP）则会使用 fallback 的解析结果</li>
<li>domain 为正向条件，匹配到这些域名则会直接使用 fallback</li>
</ul>
<p>由此即完成了 DNS 解析的分流。</p>
<h4 id="clash-的其他相关文件">clash 的其他相关文件<a href="#clash-的其他相关文件" class="heading-anchor-link" aria-label="Link to clash 的其他相关文件"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h4>
<p>clash 还需要一些配套文件，在启动前需要先下载下来</p>
<div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">cd</span><span style="color:#032F62;--s-dark:#9ECBFF"> /var/lib/clash</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">wget</span><span style="color:#005CC5;--s-dark:#79B8FF"> -q</span><span style="color:#005CC5;--s-dark:#79B8FF"> --progress=bar:dot</span><span style="color:#005CC5;--s-dark:#79B8FF"> --show-progress</span><span style="color:#005CC5;--s-dark:#79B8FF"> -O</span><span style="color:#032F62;--s-dark:#9ECBFF"> country.mmdb</span><span style="color:#032F62;--s-dark:#9ECBFF"> https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/country.mmdb</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">wget</span><span style="color:#005CC5;--s-dark:#79B8FF"> -q</span><span style="color:#005CC5;--s-dark:#79B8FF"> --progress=bar:dot</span><span style="color:#005CC5;--s-dark:#79B8FF"> --show-progress</span><span style="color:#005CC5;--s-dark:#79B8FF"> -O</span><span style="color:#032F62;--s-dark:#9ECBFF"> geosite.dat</span><span style="color:#032F62;--s-dark:#9ECBFF">  https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geosite.dat</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">wget</span><span style="color:#005CC5;--s-dark:#79B8FF"> -q</span><span style="color:#005CC5;--s-dark:#79B8FF"> --progress=bar:dot</span><span style="color:#005CC5;--s-dark:#79B8FF"> --show-progress</span><span style="color:#005CC5;--s-dark:#79B8FF"> -O</span><span style="color:#032F62;--s-dark:#9ECBFF"> GeoIP.dat</span><span style="color:#032F62;--s-dark:#9ECBFF">    https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.dat</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">mkdir</span><span style="color:#005CC5;--s-dark:#79B8FF"> -p</span><span style="color:#032F62;--s-dark:#9ECBFF"> ui</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">cd</span><span style="color:#032F62;--s-dark:#9ECBFF"> ui</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">wget</span><span style="color:#005CC5;--s-dark:#79B8FF"> -q</span><span style="color:#005CC5;--s-dark:#79B8FF"> --progress=bar:dot</span><span style="color:#005CC5;--s-dark:#79B8FF"> --show-progress</span><span style="color:#005CC5;--s-dark:#79B8FF"> -O</span><span style="color:#032F62;--s-dark:#9ECBFF"> xd.zip</span><span style="color:#032F62;--s-dark:#9ECBFF"> https://github.com/MetaCubeX/metacubexd/archive/refs/heads/gh-pages.zip</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">unzip</span><span style="color:#005CC5;--s-dark:#79B8FF"> -oqq</span><span style="color:#032F62;--s-dark:#9ECBFF"> xd.zip</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">mv</span><span style="color:#032F62;--s-dark:#9ECBFF"> metacubexd-gh-pages</span><span style="color:#032F62;--s-dark:#9ECBFF"> xd</span></span></code></pre></div>
<p>最终 <code>/var/lib/clash</code> 目录下应当是这样的</p>
<div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">/var/lib/clash</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">├──</span><span style="color:#032F62;--s-dark:#9ECBFF"> clean.sh</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">├──</span><span style="color:#032F62;--s-dark:#9ECBFF"> config.yaml</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">├──</span><span style="color:#032F62;--s-dark:#9ECBFF"> country.mmdb</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">├──</span><span style="color:#032F62;--s-dark:#9ECBFF"> GeoIP.dat</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">├──</span><span style="color:#032F62;--s-dark:#9ECBFF"> geosite.dat</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">├──</span><span style="color:#032F62;--s-dark:#9ECBFF"> iptables.sh</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">└──</span><span style="color:#032F62;--s-dark:#9ECBFF"> ui</span></span></code></pre></div>
<p>由于 clash 程序以 clash 用户身份启动，所以需要更改下所有权：</p>
<div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">chown</span><span style="color:#005CC5;--s-dark:#79B8FF"> -R</span><span style="color:#032F62;--s-dark:#9ECBFF"> clash:clash</span><span style="color:#032F62;--s-dark:#9ECBFF"> /var/lib/clash</span></span></code></pre></div>
<p>并设置 iptables.sh 和 clean.sh 可执行</p>
<div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">chmod</span><span style="color:#032F62;--s-dark:#9ECBFF"> +x</span><span style="color:#032F62;--s-dark:#9ECBFF"> iptables.sh</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">chmod</span><span style="color:#032F62;--s-dark:#9ECBFF"> +x</span><span style="color:#032F62;--s-dark:#9ECBFF"> clean.sh</span></span></code></pre></div>
<h4 id="服务启动">服务启动<a href="#服务启动" class="heading-anchor-link" aria-label="Link to 服务启动"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h4>
<p>完全配置好以后，我们可以设置 <code>/etc/systemd/system/clash.service</code> 为开启自动启动，并立即启动起来。</p>
<div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">systemctl</span><span style="color:#032F62;--s-dark:#9ECBFF"> enable</span><span style="color:#005CC5;--s-dark:#79B8FF"> --now</span><span style="color:#032F62;--s-dark:#9ECBFF"> clash.service</span></span></code></pre></div>
<p>后续如想查看日志，我们直接使用 Debian 自带的工具来查看：</p>
<div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">journalctl</span><span style="color:#005CC5;--s-dark:#79B8FF"> -efu</span><span style="color:#032F62;--s-dark:#9ECBFF"> clash.service</span></span></code></pre></div>
<p>访问 webui：<code>http://192.168.7.2:9090/ui/xd</code></p>
<p>webui 的配置大家就很熟悉了。如果配置完成没啥问题，就可以将本机的 dns 改为 127.0.0.1 了（上文提到），并将内网设备的网关和 DNS 都指向 192.168.7.2</p>
<h3 id="端口映射">端口映射<a href="#端口映射" class="heading-anchor-link" aria-label="Link to 端口映射"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h3>
<p>如果你在主路由上配置了端口映射，且被映射端口的机器网关配置的是旁路由，那此时端口映射应当是失效了。可以在 <a href="/fiddling/fix-port-forward-in-bypass-router">旁路由端口映射失效解决</a> 这篇文章中找到解决方案。</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[GFW 原理考]]></title>
            <link>https://blog.shinya.click/fiddling/tech-about-gfw</link>
            <guid isPermaLink="false">https://blog.shinya.click/fiddling/tech-about-gfw</guid>
            <pubDate>Sun, 23 Jun 2024 07:31:32 GMT</pubDate>
            <description><![CDATA[GFW 的运作机制并非简单的出口网关监控，而是通过旁路监听技术对国际流量进行审查。这种方式使得所有出入境的 IP 包都被复制到 GFW 集群，进行深度分析和过滤。了解这一点，对于研究翻墙方法至关重要，因为它揭示了封锁的具体路径和技术手段。深入探讨 GFW 的网络拓扑，有助于更有效地应对和规避网络审查。]]></description>
            <content:encoded><![CDATA[<p>技术无罪。</p>
<blockquote>
<p>防火长城（英语：Great Firewall，GFW），中国国家防火墙，或简称为墙、防火墙等，中国网信办称其为数据跨境安全网关，是该国政府过滤国际互联网出口内容的软硬件系统集合。———— Wikipedia</p>
</blockquote>
<p>要有效对抗 GFW，不应只关注哪些网站被封，而应深入了解其封锁机制。知道 Google 被封并不能直接帮助翻墙，但理解 GFW 如何封锁 Google 对选择和实施翻墙方案至关重要。因此，在讨论翻墙方法之前，必须先深入研究 GFW 的封锁原理。</p>
<h3 id="gfw-在哪">GFW 在哪<a href="#gfw-在哪" class="heading-anchor-link" aria-label="Link to GFW 在哪"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h3>
<p>很多人可能想当然：GFW 当然部署在出口网关上，这样就能直接抓到全部出口流量进行审查了。但事实上，据 gfwrev.blogspot.com 考证，GFW“在三个国际出口作旁路监听”，通过分光的方式将所有出入境的 IP 包复制到 GFW 集群上以进行审查。推测的 GFW 网络拓扑如下（图源 gfwrev.blogspot.com）：</p>
<figure><img src="https://blog-img.774352199.xyz/2025/4beb5462a598c9015c56c75fa73ae301.svg" alt="gfw topology" loading="lazy" decoding="async" style="background-image:url(data:image/webp;base64,UklGRhQBAABXRUJQVlA4WAoAAAAQAAAADwAACgAAQUxQSH8AAAABcF3btumsZxuxbdv5yl/6S5epQTVExARkGjm3knHceujYAOY6co5Ls/V+bleZbC7Xvt8vGwlxovLVXh7udx38WiabcyUgHDSOsgVXUBKAgb9TLDjC4wWo00JH0qEH/q4IUJm4LY8C4Ckf/3dGPqCHzeV+OVxs8vzUdF1XNE0AAFZQOCBuAAAAMAIAnQEqEAALAALATCWUAA+ECYAYdtMSkAAA/fB/8dpCsQkIIc/4/kq/9lNxkbj/iZS/xEZHKZ4qfjmuOnfszYeVDn7fS/JP8G0X+/5/sV3ySiXrqzzv7b1RsfZFMcWnmxCPzhinQe39fewcQAA=);background-size:cover;background-repeat:no-repeat"><figcaption>gfw topology</figcaption></figure>
<p>GFW 希望对不同线路的链路异构性进行耦合，并研究了多种链路的耦合技术（来源：<a href="https://xueshu.baidu.com/usercenter/paper/show?paperid=f46cb7e5a6dbf7b9cb81b1dd3b9965ce" target="_blank" rel="noopener noreferrer" data-umami-event="outbound-link-click" data-umami-event-url="https://xueshu.baidu.com/usercenter/paper/show?paperid=f46cb7e5a6dbf7b9cb81b1dd3b9965ce">高速网络环境下入侵检测系统结构研究</a>）。根据《国际通信出入口局管理办法》，几大 ISP 在公用国际光缆处汇合，而安管中心（CNNISC）有独立的交换中心，各 ISP 分别接入其交换中心。为了适应不同 ISP 的链路规格，GFW 的交换中心需要对不同链路进行整合，不同 ISP 分别引出旁路接入 GFW。接入的线路主要是光纤线路，所以称为“旁路分光”。实验发现，GFW 的接入地点不一定紧靠最后一跳，所以图中以虚线表示。</p>
<p>关于这些有一篇更严谨的研究论文：<a href="https://web.eecs.umich.edu/~zmao/Papers/china-censorship-pam11.pdf" target="_blank" rel="noopener noreferrer" data-umami-event="outbound-link-click" data-umami-event-url="https://web.eecs.umich.edu/~zmao/Papers/china-censorship-pam11.pdf">Internet Censorship in China: Where Does the Filtering Occur?</a></p>
<p>早期（2010 年）研究认为，GFW 工程伪装成“虚拟计算环境实验床”计划实施</p>
<blockquote>
<p>“虚拟计算环境实验床”是由国家计算机网络应急技术处理协调中心（CNCERT/CC）和哈尔滨工业大学（HIT）协作建设，以国家计算机网络应急技术处理协调中心遍布全国 31 个省份的网络基础设施及计算资源为基础，对分布自治资源进行集成和综合利用，构建起的一个开放、安全、动态、可控的大规模虚拟计算环境实验平台，研究并验证虚拟计算环境聚合与协同机理。</p>
</blockquote>
<p>从“虚拟计算环境试验床”公开的论文：<a href="https://dds.sciengine.com/cfs/files/pdfs/1674-5973/LcNrPPSfzafrc3Pmn.pdf" target="_blank" rel="noopener noreferrer" data-umami-event="outbound-link-click" data-umami-event-url="https://dds.sciengine.com/cfs/files/pdfs/1674-5973/LcNrPPSfzafrc3Pmn.pdf">计算网格环境下基于多址协同的作业级任务调度算法</a> 来看，2005 年该平台的配置如下：</p>
<p>| 站点 | 地理位置 | 机型 | 节点数 | 每节点处理器 | 每节点主存 |
|  ----  | ----  |  ----  | ----  |  ----  | ----  |
|CNCERT/CC|北京 | 曙光 4000L|128 节点|2<em>Xeon 2.4G|RAM2G|
|HIT|哈尔滨 | 曙光服务器|32 节点|2</em>Xeon 2.4G|RAM2G|
|CNCERT/CC|上海|Beowulf 集群|64 节点|2*AMD|Athlon 1.5G|RAM2G|</p>
<p>当然，如此配置仅是 2005 年的配置，目前 GFW 的软硬件配置已难以考证。</p>
<h3 id="数据处理">数据处理<a href="#数据处理" class="heading-anchor-link" aria-label="Link to 数据处理"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h3>
<p>GFW 在获取到 IP 包后，需要决定是否允许你与服务器之间的通信继续。它不能过于激进，因为全国性地阻断访问国外网站违背其存在价值。GFW 在理解 IP 包的含义后，才决定是否安全地阻断你与国外服务器的连接。GFW 首先需要进行重建，分析其中的 TCP 协议，以最终重建出一个完整的字节流，供后续在这个重建的字节流上分析具体的应用协议，比如 HTTP 协议。然后在应用协议中查找是否存在不和谐的内容，然后决定采用何种应对方式。</p>
<p>为了简化讨论，假设有如下三个 TCP 包：</p>
<div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre><code>IP 包 1：包含 TCP 包：包含的数据：Get /inde
IP 包 2：包含 TCP 包：包含的数据：x.html H
IP 包 1：包含 TCP 包：包含的数据：TTP/1.1
</code></pre></div>
<p>重建需要做的事情就是把 IP 包 1 中的 GET /inde 和 IP 包 2 中的 x.html H 和 IP 包 3 中的 TTP/1.1 拼到一起变成 GET /index.html HTTP/1.1。拼出来的数据可能是纯文本的，也可能是二进制加密的协议内容。具体是什么是你和服务器之间约定好的。GFW 做为窃听者需要猜测才知道你们俩之间的交谈内容。对于 HTTP 协议就非常容易猜测了，因为 HTTP 的协议是标准化的，而且是未加密的。所以 GFW 可以在重建之后很容易的知道，你使用了 HTTP 协议，访问的是什么网站。</p>
<p>重建这样的字节流有一个难点是如何处理巨大的流量？这个问题在 <a href="http://gfwrev.blogspot.tw/2010/02/gfw.html" target="_blank" rel="noopener noreferrer" data-umami-event="outbound-link-click" data-umami-event-url="http://gfwrev.blogspot.tw/2010/02/gfw.html">这篇博客</a> 中已经讲得很明白了。其原理与网站的负载均衡器一样。对于给定的来源和目标，使用一个 HASH 算法取得一个节点值，然后把所有符合这个来源和目标的流量都往这个节点发。所以在一个节点上就可以重建一个 TCP 会话的单向字节流。</p>
<p>最后为了讨论完整，再提两点</p>
<ol>
<li>虽然 GFW 的重建发生在旁路上是基于分光来实现的，但并不代表整个 GFW 的所有设备都在旁路。后面会提到有一些 GFW 应对形式必须是把一些 GFW 的设备部署在了主干路由上，比如对 Google 的 HTTPS 的间歇性丢包，也就是 GFW 是要参与部分 IP 的路由工作的。</li>
<li>重建是单向的 TCP 流，也就是 GFW 根本不在乎双向的对话内容，它只根据监听到的一个方向的内容然后做判断。但是监听本身是双向的，也就是无论是从国内发到国外，还是从国外发到国内，都会被重建然后加以分析。所以一个 TCP 连接对于 GFW 来说会被重建成两个字节流。</li>
</ol>
<h3 id="分析">分析<a href="#分析" class="heading-anchor-link" aria-label="Link to 分析"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h3>
<p>分析是 GFW 在重建出字节流之后要做的第二步。对于重建来说，GFW 主要处理 IP 协议，以及上一层的 TCP 和 UDP 协议就可以了。但是对于分析来说，GFW 就需要理解各种各样的应用层的稀奇古怪的协议了。甚至，我们也可以自己发明新的协议。</p>
<p>总的来说，GFW 做协议分析有两个相似，但是不同的目的。第一个目的是防止不和谐内容的传播，比如说使用 Google 搜索了“不该”搜索的关键字。第二个目的是防止使用翻墙工具绕过 GFW 的审查。</p>
<p>对于 GFW 具体是怎么达到目的一，也就是防止不和谐内容传播的就牵涉到对 HTTP 协议和 DNS 协议等几个协议的明文审查。大体的做法是这样的：</p>
<div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre><code>1. 特征检测
2. 拆包
3. 关键词匹配
</code></pre></div>
<p>像 HTTP 这样的协议会有非常明显的特征供检测，所以第一步就没什么好说的了。当 GFW 发现了包是 HTTP 的包之后就会按照 HTTP 的协议规则拆包。这个拆包过程是 GFW 按照它对于协议的理解来做的。比如说，从 HTTP 的 GET 请求中取得请求的 URL。然后 GFW 拿到这个请求的 URL 去与关键字做匹配，比如查找 Twitter 是否在请求的 URL 中。为什么有拆包这个过程？首先，拆包之后可以更精确的打击，防止误杀。另外可能预先做拆包，比全文匹配更节省资源。其次，<a href="https://github.com/liruqi/jjproxy" target="_blank" rel="noopener noreferrer" data-umami-event="outbound-link-click" data-umami-event-url="https://github.com/liruqi/jjproxy">liruqi/jjproxy</a> 的核心就是基于 GFW 的一个 HTTP 拆包的漏洞，当然这个 bug 已经被修复了。其原理就是 GFW 在拆解 HTTP 包的时候没有处理有多出来的 \r\n 这样的情况，但是你访问的 google.com 却可以正确处理额外的 \r\n 的情况。从这个例子中可以证明，GFW 还是先去理解协议，然后才做关键字匹配的。关键字匹配应该就是使用了一些高效的正则表达式算法，没有什么可以讨论的。</p>
<p>目前已知的 GFW 会做的协议分析如下：</p>
<h4 id="dns-协议">DNS 协议<a href="#dns-协议" class="heading-anchor-link" aria-label="Link to DNS 协议"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h4>
<p>GFW 可以分析 53 端口的 UDP 协议的 DNS 查询。如果查询的域名匹配关键字则会被 DNS 劫持。可以肯定的是，这个匹配过程使用的是类似正则的机制，而不仅仅是一个黑名单，因为子域名实在太多了。证据是</p>
<ul>
<li>2010 年 3 月，一名智利域名注册商的技术人员发现向位于中国的根服务器查询 facebook.com、youtube.com 和 twitter.com 等域名时的回复不正常。中国根服务器运营商 Netnod 于是暂时切断了其与国际互联网的连接。安全专家认为这与 Netnod 无关，而是中国政府修改某处网络时造成的。</li>
<li>2014 年 1 月 21 日下午三点半，中国互联网域名解析异常，大量网站被错误解析到 IP 地址 65.49.2.178，这个 IP 位于美国加利福尼亚州费利蒙市 Hurricane Electric 公司，被动态网公司租用于翻墙软件连接节点。动态网公司和研究人员认为这是因为防火长城的工作人员操作失误，另一些人认为，不能排除真正的黑客借这一 IP 地址作为跳板发动攻击的可能。</li>
<li>2015 年 1 月 2 日，污染方式有所改变，防火长城不再注入固定、被封锁 IP 地址，而是境外真实网站的、可访问的地址，这导致了境外服务器遭受来自中国的 DDoS 攻击，部分网站因此屏蔽中国 IP。该年 4 月，CNCERT 发表声明称，劫持是境外攻击所致。</li>
</ul>
<p>来源：https://zh.wikipedia.org/wiki/%E9%98%B2%E7%81%AB%E9%95%BF%E5%9F%8E</p>
<h4 id="http-协议">HTTP 协议<a href="#http-协议" class="heading-anchor-link" aria-label="Link to HTTP 协议"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h4>
<p>GFW 可以识别出 HTTP 协议，并且检查 GET 的 URL 与 HOST。如果匹配了关键字则会触发 TCP RST 阻断。</p>
<h4 id="tls-协议">TLS 协议<a href="#tls-协议" class="heading-anchor-link" aria-label="Link to TLS 协议"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h4>
<p>早期 TLS 版本中，服务器握手响应，包括证书，是未被加密的，GFW 可以嗅探它而得知访问站点，自 TLS 1.3 开始，ServerHello 之后的握手信息，包括站点证书，也会被加密后传输，一般可以认为能防止对证书信息的检测</p>
<p>然而现在普遍使用的 SNI 协议是 TLS 的一个扩展协议，在该协议下客户端在握手过程开始时告诉服务器其连接的域名，以便运作多个 HTTPS 网站的服务器选择并提供对应证书，而该扩展也未被加密。GFW 当前也会嗅探 SNI 握手阶段的明文域名以进行阻断。由于 HTTPS 即 HTTP + TLS，对 HTTPS 连接的检测仍可以归类为对 HTTP 的检测。</p>
<p><strong>注意：由于 GFW 无法获取目标域名的证书，GFW 仍然无法解密实际的 HTTPS 内容</strong></p>
<h4 id="流量特征识别">流量特征识别<a href="#流量特征识别" class="heading-anchor-link" aria-label="Link to 流量特征识别"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h4>
<p>GFW 的第二个目的是封杀翻墙软件。为了达到这个目的 GFW 采取的手段更加暴力。原因简单，对于 HTTP 协议的封杀如果做不好会影响互联网的正常运作，GFW 与互联网是共生的关系，它不会做威胁自己存在的事情。但是对于 TOR 这样的几乎纯粹是为翻墙而存在的协议，只要检测出来就是格杀勿论的了。GFW 具体是如何封杀各种翻墙协议的，我也不是很清楚，事态仍然在不断更新中。但是举两个例子来证明 GFW 的高超技术。</p>
<p>第一个例子是 GFW 对 TOR 的自动封杀，体现了 GFW 尽最大努力去理解协议本身。根据这篇博客 https://blog.torproject.org/blog/knock-knock-knockin-bridges-doors。使用中国的 IP 去连接一个美国的 TOR 网桥，会被 GFW 发现。然后 GFW 回头（15 分钟之后）会亲自假装成客户端，用 TOR 的协议去连接那个网桥。如果确认是 TOR 的网桥，则会封当时的那个端口。换了端口之后，可以用一段时间，然后又会被封。这表现出了 GFW 对于协议的高超检测能力，可以从国际出口的流量中敏锐地发现你连接的 TOR 网桥。据 TOR 的同志说是因为 TOR 协议中的握手过程具有太明显的特征了。另外一点就表现了 GFW 的不辞辛劳，居然会自己伪装成客户端过去连连看。</p>
<p>第二个例子表现了 GFW 根本不在乎加密的流量中的具体内容是不是有敏感词。只要疑似翻墙，特别是提供商业翻墙服务，就会被封杀。（几乎可以确定）GFW 已经升级为能够机器识别出哪些加密的流量是疑似翻墙服务的。</p>
<p>GFW 显然近期的工作重心在分析网络流量上，从中识别出哪些是翻墙的流量。这方面的研究还比较少，而且一个显著的特征是自己用没关系，大规模部署就容易出问题。</p>
<h3 id="干扰措施">干扰措施<a href="#干扰措施" class="heading-anchor-link" aria-label="Link to 干扰措施"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h3>
<p>GFW 通过协议分析，确定你这个字节流是“具有威胁的”，就会通过以下几种方式来干扰继续通信：</p>
<h4 id="ip-封锁">IP 封锁<a href="#ip-封锁" class="heading-anchor-link" aria-label="Link to IP 封锁"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h4>
<p>一般常见于人工检测之后的应对。还没有听说有什么方式可以直接使得 GFW 的机器检测直接封 IP。一般常见的现象是 GFW 机器检测，然后用 TCP RST 重置来应对。过了一段时间才会被封 IP，而且没有明显的时间规律。所以我的推测是，全局性的封 IP 应该是一种需要人工介入的。注意我强调了全局性的封 IP，与之相对的是部分封 IP，比如只对你访问那个 IP 封个 3 分钟，但是别人还是可以访问这样的。这是一种完全不同的封锁方式，虽然现象差不多，都是 ping 也 ping 不通。要观摩的话 ping twitter.com 就可以了，都封了好久了。</p>
<p>其实现方式是把无效的路由黑洞加入到主干路由器的路由表中，然后让这些主干网上的路由器去帮 GFW 把到指定 IP 的包给丢弃掉。路由器的路由表是动态更新的，使用的协议是 BGP 协议。GFW 只需要维护一个被封的 IP 列表，然后用 BGP 协议广播出去就好了。然后国内主干网上的路由器都好像变成了 GFW 的一份子那样，成为了帮凶。</p>
<p>如果我们使用 traceroute 去检查这种被全局封锁的 IP 就可以发现，IP 包还没有到 GFW 所在的国际出口就已经被电信或者联通的路由器给丢弃了。这就是 BGP 广播的作用了。</p>
<h4 id="dns-劫持">DNS 劫持<a href="#dns-劫持" class="heading-anchor-link" aria-label="Link to DNS 劫持"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h4>
<p>这也是一种常见的人工检测之后的应对。人工发现一个不和谐网站，然后就把这个网站的域名给加到劫持列表中。其原理是基于 DNS 与 IP 协议的弱点，DNS 与 IP 这两个协议都不验证服务器的权威性，而且 DNS 客户端会盲目地相信第一个收到的答案。所以你去查询 facebook.com 的话，GFW 只要在正确的答案被返回之前抢答了，然后伪装成你查询的 DNS 服务器向你发错误的答案就可以了。</p>
<h4 id="tcp-连接重置">TCP 连接重置<a href="#tcp-连接重置" class="heading-anchor-link" aria-label="Link to TCP 连接重置"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h4>
<p>TCP 协议规定，只要看到 RST 包，连接立马被中断。从浏览器里来看就是连接已经被重置。我想对于这个错误大家都不陌生。据我个人观感，这种封锁方式是 GFW 目前的主要应对手段。大部分的 RST 是条件触发的，比如 URL 中包含某些关键字。目前享受这种待遇的网站就多得去了，著名的有 facebook。还有一些网站，会被无条件 RST。也就是针对特定的 IP 和端口，无论包的内容就会触发 RST。比较著名的例子是 https 的 wikipedia。GFW 在 TCP 层的应对是利用了 IPv4 协议的弱点，也就是只要你在网络上，就假装成任何人发包。所以 GFW 可以很轻易地让你相信 RST 确实是 Google 发的，而让 Google 相信 RST 是你发的。</p>
<h4 id="端口封锁">端口封锁<a href="#端口封锁" class="heading-anchor-link" aria-label="Link to 端口封锁"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h4>
<p>GFW 除了自身主体是挂在骨干路由器旁路上的入侵检测设备，利用分光技术从这个骨干路由器抓包下来做入侵检测 (所谓 IDS)，除此之外这个路由器还会被用来封端口 (所谓 IPS)。GFW 在检测到入侵之后可以不仅仅可以用 TCP RST 阻断当前这个连接，而且利用骨干路由器还可以对指定的 IP 或者端口进行从封端口到封 IP，设置选择性丢包的各种封禁措施。可以理解为骨干路由器上具有了类似“iptables”的能力（网络层和传输层的实时拆包，匹配规则的能力）。这个 iptables 的能力在 CISCO 路由器上叫做 ACL Based Forwarding (ABF)。而且规则的部署是全国同步的，一台路由器封了你的端口，全国的挂了 GFW 的骨干路由器都会封。一般这种封端口都是针对翻墙服务器的，如果检测到服务器是用 SSH 或者 VPN 等方式提供翻墙服务。GFW 会在全国的出口骨干路由上部署这样的一条 ACL 规则，来封你这个服务器 + 端口的下行数据包。也就是如果包是从国外发向国内的，而且 src（源 ip）是被封的服务器 ip，sport（源端口）是被封的端口，那么这个包就会被过滤掉。这样部署的规则的特点是，上行的数据包是可以被服务器收到的，而下行的数据包会被过滤掉。</p>
<p>如果被封端口之后服务器采取更换端口的应对措施，很快会再次被封。而且多次尝试之后会被封 IP。初步推断是，封端口不是 GFW 的自动应对行为，而是采取黑名单加人工过滤地方式实现的。一个推断的理由就是网友报道，封端口都是发生在白天工作时间。</p>
<h4 id="反向墙">反向墙<a href="#反向墙" class="heading-anchor-link" aria-label="Link to 反向墙"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h4>
<p>大部分机场使用的国内中转服务器，GFW 检测到了十分异常的境外大流量，就会在特殊时期，如两会和国庆期间，将该服务器反向墙。具体表现为国外的服务器无法访问该被反向墙了的 IP。ping 该国内服务器就是国外一片红，国内一片绿。解决方法不多，要么更换 IP，或者等敏感时期过去，自动恢复。</p>
<blockquote>
<p>本文转载编辑补充自</p>
<ol>
<li>https://ednovas.xyz/2022/06/25/gfw/#%E4%B8%AD%E8%BD%AC</li>
<li>https://gfwrev.blogspot.com/2010/02/gfw.html</li>
</ol>
</blockquote>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[2024 学习计划]]></title>
            <link>https://blog.shinya.click/daily/plan2024</link>
            <guid isPermaLink="false">https://blog.shinya.click/daily/plan2024</guid>
            <pubDate>Wed, 03 Jan 2024 18:06:51 GMT</pubDate>
            <description><![CDATA[2024年的学习计划明确而具体，目标包括日语达到N2水平，完成SICP和TAPL的学习，深入操作系统的知识以实现一个完整的POSIX内核，同时也计划更新博客主题。这些目标不仅具挑战性，也为个人成长和技能提升提供了清晰的方向。]]></description>
            <content:encoded><![CDATA[<p>客套话不多说了，尽力而为好吧。</p>
<ul>
<li>日语：达到 N2 水平</li>
<li>SICP：刷完</li>
<li>OS：完整实现了 POSIX 的内核</li>
<li>TAPL：刷完</li>
<li>博客主题</li>
</ul>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[RISC-V 工具链与模拟器（emulator）的安装]]></title>
            <link>https://blog.shinya.click/fiddling/spike-install</link>
            <guid isPermaLink="false">https://blog.shinya.click/fiddling/spike-install</guid>
            <pubDate>Wed, 24 May 2023 09:51:09 GMT</pubDate>
            <description><![CDATA[在安装 RISC-V 工具链时，首先需要获取 riscv-gnu-toolchain 的源码，建议使用带有 `--depth=1` 的 clone 命令，以减少下载体积。安装过程中，注意查看 README 中的 Prerequisites 部分，确保前置依赖都已正确安装。在 Debian 系统中，可以通过简单的命令安装所需的依赖包，确保工具链的顺利搭建。]]></description>
            <content:encoded><![CDATA[<p>闲的无聊看看 spike 源码，但是翻了很多教程都没有可以直接安装好 spike 和相关的工具链。于是自己动手丰衣足食，直接读相关仓库的 README，基本都可以安装成功，但是还是暗藏着一个小坑，记录一下。</p>
<h3 id="工具链riscv-gnu-toolchain的安装">工具链（riscv-gnu-toolchain）的安装<a href="#工具链riscv-gnu-toolchain的安装" class="heading-anchor-link" aria-label="Link to 工具链（riscv-gnu-toolchain）的安装"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h3>
<p>RISC-V 工具链包括 gcc、gdb、objdump/copy 和相关的标准库实现等，建议首先安装。</p>
<p>仓库地址：https://github.com/riscv-collab/riscv-gnu-toolchain</p>
<p>clone 时建议加上 <code>--depth=1</code> 参数缩小 clone 体积，后面几个仓库的 clone 也建议加此参数。</p>
<p>前置的依赖安装可见 README 中的 Prerequisites 一节，当前 Debian 系下为：</p>
<div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">sudo</span><span style="color:#032F62;--s-dark:#9ECBFF"> apt</span><span style="color:#032F62;--s-dark:#9ECBFF"> install</span><span style="color:#032F62;--s-dark:#9ECBFF"> autoconf</span><span style="color:#032F62;--s-dark:#9ECBFF"> automake</span><span style="color:#032F62;--s-dark:#9ECBFF"> autotools-dev</span><span style="color:#032F62;--s-dark:#9ECBFF"> curl</span><span style="color:#032F62;--s-dark:#9ECBFF"> python3</span><span style="color:#032F62;--s-dark:#9ECBFF"> libmpc-dev</span><span style="color:#032F62;--s-dark:#9ECBFF"> libmpfr-dev</span><span style="color:#032F62;--s-dark:#9ECBFF"> libgmp-dev</span><span style="color:#032F62;--s-dark:#9ECBFF"> gawk</span><span style="color:#032F62;--s-dark:#9ECBFF"> build-essential</span><span style="color:#032F62;--s-dark:#9ECBFF"> bison</span><span style="color:#032F62;--s-dark:#9ECBFF"> flex</span><span style="color:#032F62;--s-dark:#9ECBFF"> texinfo</span><span style="color:#032F62;--s-dark:#9ECBFF"> gperf</span><span style="color:#032F62;--s-dark:#9ECBFF"> libtool</span><span style="color:#032F62;--s-dark:#9ECBFF"> patchutils</span><span style="color:#032F62;--s-dark:#9ECBFF"> bc</span><span style="color:#032F62;--s-dark:#9ECBFF"> zlib1g-dev</span><span style="color:#032F62;--s-dark:#9ECBFF"> libexpat-dev</span><span style="color:#032F62;--s-dark:#9ECBFF"> ninja-build</span></span></code></pre></div>
<p>clone 后按照工具链中的 <code>Installation (Newlib)</code> 一节基本可以安装成功，但是有坑点：</p>
<blockquote>
<p>使用此方式编译的 gcc 无法编译 riscv-pk，会报 extension `zifencei' required 的错误，推测是因为默认的编译参数并不支持 zifencei 扩展（即 FENCE.I 指令）</p>
</blockquote>
<p>可用的编译参数如下：</p>
<div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">./configure</span><span style="color:#005CC5;--s-dark:#79B8FF"> --prefix=/opt/riscv</span><span style="color:#005CC5;--s-dark:#79B8FF"> --with-arch=rv64gc</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">make</span></span></code></pre></div>
<p>建议首先在 /opt 下建立 riscv 文件夹，并保证 riscv 文件夹的所有人为普通用户自己。如非则需要 chown：</p>
<div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">sudo</span><span style="color:#032F62;--s-dark:#9ECBFF"> chown</span><span style="color:#032F62;--s-dark:#9ECBFF"> 1000:1000</span><span style="color:#032F62;--s-dark:#9ECBFF"> /opt/riscv</span></span></code></pre></div>
<p>这里假设 uid 和 gid 皆为 1000，具体可以通过 <code>id</code> 命令查看。</p>
<p>安装完成后需要将 <code>/opt/riscv/bin</code> 加入 Path，具体方式可 Google 可 ChatGPT。后续安装的内容也都会放入 <code>/opt/riscv</code> 中。完成后 <code>riscv64-unknown-elf-gcc</code> 应当可用。</p>
<h3 id="模拟器spike的安装">模拟器（spike）的安装<a href="#模拟器spike的安装" class="heading-anchor-link" aria-label="Link to 模拟器（spike）的安装"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h3>
<p>仓库地址：https://github.com/riscv-software-src/riscv-isa-sim</p>
<p>clone 下来后按照 README 中的 Build Steps 一节直接安装即可，并无坑点。当前命令：</p>
<div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">$</span><span style="color:#032F62;--s-dark:#9ECBFF"> sudo</span><span style="color:#032F62;--s-dark:#9ECBFF"> apt</span><span style="color:#032F62;--s-dark:#9ECBFF"> install</span><span style="color:#032F62;--s-dark:#9ECBFF"> device-tree-compiler</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">$</span><span style="color:#032F62;--s-dark:#9ECBFF"> mkdir</span><span style="color:#032F62;--s-dark:#9ECBFF"> build</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">$</span><span style="color:#032F62;--s-dark:#9ECBFF"> cd</span><span style="color:#032F62;--s-dark:#9ECBFF"> build</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">$</span><span style="color:#032F62;--s-dark:#9ECBFF"> ../configure</span><span style="color:#005CC5;--s-dark:#79B8FF"> --prefix=/opt/riscv</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">$</span><span style="color:#032F62;--s-dark:#9ECBFF"> make</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">$</span><span style="color:#032F62;--s-dark:#9ECBFF"> make</span><span style="color:#032F62;--s-dark:#9ECBFF"> install</span></span></code></pre></div>
<p>安装完成后 <code>spike</code> 命令应可用</p>
<h3 id="模拟内核riscv-pk的安装">模拟内核（riscv-pk）的安装<a href="#模拟内核riscv-pk的安装" class="heading-anchor-link" aria-label="Link to 模拟内核（riscv-pk）的安装"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h3>
<p>仓库地址：https://github.com/riscv-software-src/riscv-pk</p>
<p>pk 即 Proxy Kernel，用来直接运行静态链接的用户态 RISCV 程序，毕竟只要验证的话，再跑一个操作系统太重了。</p>
<p>clone 下来后按照 README 中的 Build Steps 直接安装即可。坑点仅限于工具链一节提到的 FENCE.I 指令。当前命令：</p>
<div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">$</span><span style="color:#032F62;--s-dark:#9ECBFF"> mkdir</span><span style="color:#032F62;--s-dark:#9ECBFF"> build</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">$</span><span style="color:#032F62;--s-dark:#9ECBFF"> cd</span><span style="color:#032F62;--s-dark:#9ECBFF"> build</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">$</span><span style="color:#032F62;--s-dark:#9ECBFF"> ../configure</span><span style="color:#005CC5;--s-dark:#79B8FF"> --prefix=/opt/riscv</span><span style="color:#005CC5;--s-dark:#79B8FF"> --host=riscv64-unknown-elf</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">$</span><span style="color:#032F62;--s-dark:#9ECBFF"> make</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">$</span><span style="color:#032F62;--s-dark:#9ECBFF"> make</span><span style="color:#032F62;--s-dark:#9ECBFF"> install</span></span></code></pre></div>
<h3 id="验证">验证<a href="#验证" class="heading-anchor-link" aria-label="Link to 验证"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h3>
<p>验证使用了 spike 中的例子。新建 hello.c，内容如下：</p>
<div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--s-dark:#F97583">#include</span><span style="color:#032F62;--s-dark:#9ECBFF"> &#x3C;stdio.h></span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">void</span><span style="color:#6F42C1;--s-dark:#B392F0"> main</span><span style="color:#24292E;--s-dark:#E1E4E8">()</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">{</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">    const</span><span style="color:#D73A49;--s-dark:#F97583"> char</span><span style="color:#D73A49;--s-dark:#F97583"> *</span><span style="color:#24292E;--s-dark:#E1E4E8">s </span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#032F62;--s-dark:#9ECBFF"> "Hello.</span><span style="color:#005CC5;--s-dark:#79B8FF">\n</span><span style="color:#032F62;--s-dark:#9ECBFF">"</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">    while</span><span style="color:#24292E;--s-dark:#E1E4E8"> (</span><span style="color:#D73A49;--s-dark:#F97583">*</span><span style="color:#24292E;--s-dark:#E1E4E8">s) </span><span style="color:#6F42C1;--s-dark:#B392F0">putchar</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#D73A49;--s-dark:#F97583">*</span><span style="color:#24292E;--s-dark:#E1E4E8">s</span><span style="color:#D73A49;--s-dark:#F97583">++</span><span style="color:#24292E;--s-dark:#E1E4E8">);</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">    while</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#005CC5;--s-dark:#79B8FF">1</span><span style="color:#24292E;--s-dark:#E1E4E8">);</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">}</span></span></code></pre></div>
<p>编译该文件：</p>
<div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">$</span><span style="color:#032F62;--s-dark:#9ECBFF"> riscv64-unknown-elf-gcc</span><span style="color:#005CC5;--s-dark:#79B8FF"> -o</span><span style="color:#032F62;--s-dark:#9ECBFF"> hello</span><span style="color:#032F62;--s-dark:#9ECBFF"> hello.c</span></span></code></pre></div>
<p>通过 spike 运行：</p>
<div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">spike</span><span style="color:#032F62;--s-dark:#9ECBFF"> pk</span><span style="color:#032F62;--s-dark:#9ECBFF"> hello</span></span></code></pre></div>
<p>可以看到终端输入了 Hello.，按多次 ctrl+c 可退出。</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[漫谈编程语言]]></title>
            <link>https://blog.shinya.click/fiddling/chitchat-about-programming-language</link>
            <guid isPermaLink="false">https://blog.shinya.click/fiddling/chitchat-about-programming-language</guid>
            <pubDate>Sat, 08 Apr 2023 05:16:36 GMT</pubDate>
            <description><![CDATA[设计一门新的编程语言是一项既挑战又有趣的任务。通过简化复杂的编译原理和特性实现，重点关注代码如何在计算机系统中运行，能够帮助我们更清晰地理解编程语言的构建过程。以 RISC-VI 指令集为基础，探索语言设计的底层架构，揭示了计算机系统的分层结构和虚拟机模型，这不仅是对技术的探讨，还是对编程语言本质的深刻反思。]]></description>
            <content:encoded><![CDATA[<p>本文通过讨论如何设计一门编程语言的方式，来普及编程语言理论中的一些基础概念、实现思路和现状。</p>
<h3 id="我来设计编程语言吗">我来设计编程语言……吗？<a href="#我来设计编程语言吗" class="heading-anchor-link" aria-label="Link to 我来设计编程语言……吗？"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h3>
<p>现在由我们来设计一门全新的编程语言：C--。</p>
<p>对，就是我们自己设计。</p>
<p>我们先抛下一些编译原理、编译器和解释器的实现等一些复杂的东西，不会去讨论一些特性的具体实现（看看标题：漫谈！）。</p>
<p>我们来自底向上建设这门属于我们自己的编程语言。</p>
<p>最底层我们直接假设好了，我们这门编程语言的最终编译产物是 RISC-VI 格式，有一个 RISC-VI 指令集与之对应。这个汇编语言十分基础，只能对内存和寄存器进行一些简单的操作。</p>
<h3 id="代码在哪里跑">代码在哪里跑？<a href="#代码在哪里跑" class="heading-anchor-link" aria-label="Link to 代码在哪里跑？"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h3>
<p>这个问题其实应该是 RISC-VI 指令集或者架构的问题，和我们要设计的高级语言特性似乎没有关系。但这其实关系到了你的代码处于计算机系统的哪一层，这是编程语言设计者首先就必须要考虑的问题：</p>
<blockquote>
<p>计算机系统的本质，就是分层的虚拟机</p>
</blockquote>
<p>在这个系统的分层虚拟机模型中，上层会包装和屏蔽下层的操作接口，并增加了自身的特性，以供更上一层使用。</p>
<p>操作系统就是一层对于硬件（裸机）的虚拟，例如你的计算机是 x86 指令集架构，当你将你写的 C 语言小程序编译到二进制后运行时，操作系统是需要首先解析这个二进制文件的格式，如果你是 Linux 操作系统，那这个可执行文件的格式就是 elf 格式。按照 elf 格式解析完成后，操作系统会将文件中的各个段加载到内存中，并跳转到代码段的第一个指令开始执行。当然实际不会这么简单，真正的操作系统还会更精细地管理内存资源，并通过进程等方式隔离运行中的不同任务。</p>
<p>很巧合的是（？），C 语言编译的二进制文件，不仅仅可以在操作系统这一层执行，其实也是可以在裸机上运行的。一个很典型的例子就是，Linux 内核大部分就是由 C 语言写出来的（最近主线中合入了 rust 代码，未来可期）。在系统编程这个层面，C 程序主要使用的是操作系统提供的系统调用。例如，在 x86 的 linux 下，程序需要将文件中的数据读入内存时，通常调用 sys_read 这个系统调用。在收到这个系统调用后，linux 会代你从文件中读取，当然也少不了前置的权限校验等工作。然而，如果你要使用 C 编写一个操作系统，当然就没有系统调用给你使用了，甚至连文件这个概念都是操作系统抽象出来的。这时你不得不通过某种方式直接和硬盘驱动交互，通过修改硬盘控制器中一些寄存器，来达到读取某个位置的一些数据这一简单的目的。</p>
<p>通过这样的描述，可以看出操作系统完美符合上述的虚拟机的定义。甚至可以说，Linux 就是一个用于执行 elf 文件的虚拟机（当然它还干了更多）。如果不考虑操作系统提供给 C 语言的标准库，C 语言是运行在裸机这一层语言（其实是 C 语言的编译产出运行在这一层，不过先这么说着吧）。</p>
<p>我们来看另一个🌰：Python。Python 是一个典型的解释型语言。它的官方解释器是 CPython，这是一个用 C 语言编写的 Python 解释器。套用上面的分层模型，那就是 CPython 是基于操作系统的一个虚拟机：它包装了操作系统提供的接口，供上层的 Python 程序去调用。</p>
<p>当然，极端一点，即使是 C 语言这种编译型语言，也可以勉强看作是解释型语言：CPU 确实是一条一条读取指令、解析执行的，只是这些指令是二进制的。而 Python 作为解释型语言，它的指令是可读的，CPython 的输入是人类可读的字符串。</p>
<p>解释型语言的一个巨大的好处，就是可移植性强。解释器屏蔽了不同操作系统的底层差异，向高级语言这一层提供了相同的 API，这样你写的代码，就可以<strong>一次编写，处处执行</strong>了。当然，根据苦难守恒定律，你是轻松了，苦的是写解释器的人。不过，即使是一门编译型语言，编译器也不得不根据不同的指令集架构去分别实现，看起来也没苦到哪去～</p>
<p>既然提到了<strong>处处执行</strong>，就不得不讲一下最著名的<strong>一次编译，处处执行</strong>：Java！Java 将编译和解释结合到了一起。JVM 就相当于 Python 中的 CPython，是 Java 语言的一个运行时（Runtime）。java 文件首先需要编译成 class 文件，JVM 读入的文件格式，就是 class 文件格式。这个文件也是一个二进制文件，但是其格式在不同指令集架构的操作系统上是相同的。这就是为什么，我在 x86 下编译的 class 文件，放到 RISC-V 上的 JVM 中也可以执行。JVM 在读入 class 文件后，还是会像解释器一样，一条一条加载指令解析执行。所以很难去界定 Java 究竟是一门编译型语言还是解释型语言。</p>
<p>JVM 是一个很成功的虚拟机（计算机系统层面），它不仅支持了 Java 语言在其上运行，还支持了许多其他语言，如 Scala 和 Groovy。当然这基于一个重要的事实：这些语言都可以编译到 class 格式。</p>
<p>我们普遍认为：解释型语言的执行效率较低，而编译型语言的执行效率比较高。然而随着编程语言的发展，许多解释型语言都加入了一些语言特性来提升运行效率。以 Java 为例，在 JVM 解释执行 class 文件的阶段，JVM 会动态分析热点代码，将热点代码直接编译为机器语言。再次执行到热点代码时就不会再次解释，而是直接执行已经编译好的机器语言了。这个技术被称为 JIT（Just-in-time compilation，即使编译）。类似的，Python 也有 numba 这个库用于加速执行，也是使用了 JIT 技术。</p>
<p>当然，如果你写出了一个 C 语言的解释器，你也可以说 C 语言是解释型语言（</p>
<h3 id="类型系统">类型系统<a href="#类型系统" class="heading-anchor-link" aria-label="Link to 类型系统"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h3>
<p>现在我们确定下来了 C-- 的运行位置，但我们的 C-- 还是非常简陋，甚至根本不存在：</p>
<ul>
<li>如果是走编译型语言的路子，RISC-VI 是一个指令集架构，一般和高级语言无关</li>
<li>如果走解释型语言的路子，RISC-VI 以及对应的解释器可能是由我们设计和实现的</li>
</ul>
<p>RISC-VI 是一个只能操作内存和寄存器的汇编语言，内存区域和寄存器在它眼中，只是一些无意义的字节数组，你可以操作任何一个字节（在下层虚拟机允许的范围内）</p>
<p>我们假设现在我们的语言没有类型系统，所有的操作都是直接处理字节数组。如果是在 C 语言中，相当于不定义任何类型，直接使用 void * 指针，去操作所有的字节。能使用的操作只有取地址和解引用，以及对字节赋值和取值，那和直接写汇编语言都没啥区别！我们在堆上创建一个四字节整数并赋值为 1 的方法就像下面一样（当然用的是 C 语法，实际上没有类型系统真的和汇编一样）：</p>
<div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--s-dark:#F97583">void</span><span style="color:#D73A49;--s-dark:#F97583"> *</span><span style="color:#24292E;--s-dark:#E1E4E8">intBytes </span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#6F42C1;--s-dark:#B392F0"> malloc</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#005CC5;--s-dark:#79B8FF">4</span><span style="color:#24292E;--s-dark:#E1E4E8">);</span><span style="color:#6A737D;--s-dark:#6A737D">    // 堆上分配四字节</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">*</span><span style="color:#24292E;--s-dark:#E1E4E8">(intBytes</span><span style="color:#D73A49;--s-dark:#F97583">+</span><span style="color:#005CC5;--s-dark:#79B8FF">3</span><span style="color:#24292E;--s-dark:#E1E4E8">) </span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#D73A49;--s-dark:#F97583"> 0x</span><span style="color:#005CC5;--s-dark:#79B8FF">01</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span><span style="color:#6A737D;--s-dark:#6A737D">          // 假设大端序，偏移为 3 处设置为 1</span></span></code></pre></div>
<p>我们熟知的，int、float 啥啥啥，都去哪了？那就是类型系统的工作了。</p>
<blockquote>
<p>类型的本质是对一块内存区域的解释方式</p>
</blockquote>
<p>类型系统将无序的堆栈空间划分为有实际意义的块，根据类型的不同赋予它们不同的解释方式。当然对于程序员来说，不同的类型最直观的差异在于定义类型时语法的不同。比如在 C 语言中，int 通常用于表示四字节整形，而 double 则表示符合 IEEE 754 规范的双精度浮点型。类型将影响编译器生成的运行时对这段字节的操作方式。</p>
<p>有了类型系统后，上面的创建四字节整数的方法就变成了这样：</p>
<div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--s-dark:#F97583">int</span><span style="color:#D73A49;--s-dark:#F97583"> *</span><span style="color:#24292E;--s-dark:#E1E4E8">intBytes </span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#24292E;--s-dark:#E1E4E8"> (</span><span style="color:#D73A49;--s-dark:#F97583">int</span><span style="color:#D73A49;--s-dark:#F97583"> *</span><span style="color:#24292E;--s-dark:#E1E4E8">)</span><span style="color:#6F42C1;--s-dark:#B392F0">malloc</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#005CC5;--s-dark:#79B8FF">4</span><span style="color:#24292E;--s-dark:#E1E4E8">);</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">*</span><span style="color:#24292E;--s-dark:#E1E4E8">intBytes </span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#005CC5;--s-dark:#79B8FF"> 1</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span></code></pre></div>
<p>由于我们声明了 intBytes 是一个 int 类型的指针，指向的那片内存区域应当按照整型变量去解析。于是第二行我们可以直接给 intBytes 赋值为 1，而不需要担心这个 1 会被赋值给第 0 个字节而非第 3 个。编译器在得知 intBytes 是一个 int 类型的指针时，对其的一切操作都会按照 int 类型的操作加以定制。生成的最终的机器代码其实还是将第三个字节置为 1，但是这是编译器帮我们实现的，我们只管操作各种类型即可。</p>
<p>有一个有趣的点，就是 C 语言在编译期优化了指针变量的偏移计算，例如上述的例子：<code>intBytes+1</code> 这个地址实际上代表了 intBytes 指向的地址 +4，因为 int 类型占用四个字节。这也是 C 语言借此实现数组的重要依据。</p>
<blockquote>
<p>类型系统是一个编译期特性（对于 C 语言来说）</p>
</blockquote>
<p>同样我们定义的结构体，本质上也是用于指导编译器操作内存的，例如如下的结构体：</p>
<div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--s-dark:#F97583">typedef</span><span style="color:#24292E;--s-dark:#E1E4E8"> exampleStruct </span><span style="color:#D73A49;--s-dark:#F97583">struct</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">	int</span><span style="color:#24292E;--s-dark:#E1E4E8"> a;</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">	int</span><span style="color:#24292E;--s-dark:#E1E4E8"> b;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">}</span></span></code></pre></div>
<p>这个结构体中有两个 int 变量，占用内存 8 字节。那么当我们定义一个 exampleStruct 类型的指针 <code>esp</code>，指向一片内存区域时，实际上就是认为这个地址及其后 8 字节的区域按照 exampleStruct 的结构去解析。当我们使用 <code>esp.b</code> 或者 <code>esp->b</code> 去操作这里的 int b 时，实际上就是在按照 int 的方式操作 esp 指向地址之后的第四到第七个字节。</p>
<p>所以可以这么说，结构体中定义的变量，在运行时只用于指示这个变量的地址相对于结构体起始位置的偏移。</p>
<p>至于不使用指针的方式，直接在函数中使用 <code>exampleStruct esp;</code> 声明一个结构体，则属于编译器对于栈上变量分配的特殊处理，甚至可以理解为语法糖。因为你：</p>
<ol>
<li>不需要手动初始化这个结构体，声明一下就能用，实际上编译器在编译时就决定了这个结构体在栈帧的位置</li>
<li>不需要管理这个结构体的生命周期，函数结束自动释放，当然释放的方式就是栈指针上移（栈向下生长），不会覆写这片内存</li>
</ol>
<p>当然栈上分配的坏处也不用多说，它的第二点好处就是坏处，函数返回后会自动释放，就没法在函数之外去使用了。</p>
<p>相对于 C 来说，Java 的类型系统就十分局限：类型信息被直接写进了对象所在的内存中，强制类型转换也只能在类型树的父子节点之间进行。</p>
<h4 id="传值还是传引用">传值还是传引用？<a href="#传值还是传引用" class="heading-anchor-link" aria-label="Link to 传值还是传引用？"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h4>
<p>一个经常被讨论的、同时也是经常会犯错的点，就是函数的传参，到底是传值还是传引用。</p>
<p>究其本质，可以认为所有的函数传参都是传值，所谓的传引用，本质上也是基于传值做的优化。</p>
<p>C 语言自不必多说，无论是基于寄存器传参还是基于栈传参，都是要将原内容复制一份或者将原内容备份一份，保证在新函数返回后这部分内容不会发生变化。如果传递的是个指针，可以去除其指针的属性来看，本质上只是传递了一串数字（根据指令集架构可能是 32 位或 64 位），同你将这个地址赋值给一个 long 变量后传递的结果是一样的。</p>
<p>传引用这种说法，主要出现在 Java 中。引用本质上是一种对象句柄，程序可以通过句柄访问对象的部分信息。句柄不代表对象的内存地址，但是在实现上一定包含了对象的实际内存地址。当传递一个引用到函数或方法中时，可以看作传递的是一个包含对象实际地址的结构体，和 C 语言的传递指针类似，所以行为类似。</p>
<p>然而 Java 中还包含了 8 种基本数据类型，这 8 种基本数据类型又有其对应的包装类，在实现上显得很不统一，据说是早期为了吸引 C++ 程序员迁移的一种举措，属实失了优雅。</p>
<h3 id="数组是什么">数组是什么？<a href="#数组是什么" class="heading-anchor-link" aria-label="Link to 数组是什么？"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h3>
<p>在拥有了基础的类型系统后，下面就需要考虑一种特殊但是很常见的复合数据类型：数组。但是，数组是什么，以及数组真的存在吗？</p>
<p>C 语言在运行时实际上是没有数组的，数组成了一个编译期语法糖。C 基于指针实现了数组。数组名实际上也是指向数组第零个元素的地址，即当我声明 <code>int a[10]</code> 时，a 在使用时等同于 <code>&#x26;a[0]</code>。而数组的下标运算（方括号），也是基于类型指针的偏移实现的。当使用 <code>a[1]</code> 获取数组中的第一个元素时，可以认为等同于 <code>*(a+1)</code>，更具体的：</p>
<div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--s-dark:#F97583">int</span><span style="color:#24292E;--s-dark:#E1E4E8"> b </span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#E36209;--s-dark:#FFAB70"> a</span><span style="color:#24292E;--s-dark:#E1E4E8">[</span><span style="color:#005CC5;--s-dark:#79B8FF">0</span><span style="color:#24292E;--s-dark:#E1E4E8">];</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">// 等同于</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">void</span><span style="color:#D73A49;--s-dark:#F97583"> *</span><span style="color:#24292E;--s-dark:#E1E4E8">p </span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#24292E;--s-dark:#E1E4E8"> (</span><span style="color:#D73A49;--s-dark:#F97583">void</span><span style="color:#D73A49;--s-dark:#F97583"> *</span><span style="color:#24292E;--s-dark:#E1E4E8">)a;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">p </span><span style="color:#D73A49;--s-dark:#F97583">+=</span><span style="color:#005CC5;--s-dark:#79B8FF"> 4</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">int</span><span style="color:#24292E;--s-dark:#E1E4E8"> b </span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#D73A49;--s-dark:#F97583"> *</span><span style="color:#24292E;--s-dark:#E1E4E8">((</span><span style="color:#D73A49;--s-dark:#F97583">int</span><span style="color:#D73A49;--s-dark:#F97583"> *</span><span style="color:#24292E;--s-dark:#E1E4E8">)p);</span></span></code></pre></div>
<p>由于数组的实现是基于特定类型指针的，且可以和对应类型的指针任意转换，所以实际上对于数组越界，在语言层面是没有任何检查的。例如我在栈上定义了一个长度为 10 的数组，那么我去读取和写入第 11 个元素有时是没有问题的。</p>
<p>有人会说不对不对，数组越界会出现段错误 segment fault。这实际上不是 C 语言在检查，而是在读取和写入超限内存时，可能读取到了不可读的内存、写入了不可写的内存，引起了操作系统的报错。这不属于语言层面的错误。</p>
<p>在 C 语言中，C 语言向参数传递数组有三种方式：传递指针、传递已定义大小的数组和传递未定义大小的数组，分别如下：</p>
<div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--s-dark:#F97583">void</span><span style="color:#6F42C1;--s-dark:#B392F0"> func</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#D73A49;--s-dark:#F97583">int</span><span style="color:#D73A49;--s-dark:#F97583"> *</span><span style="color:#E36209;--s-dark:#FFAB70">array</span><span style="color:#24292E;--s-dark:#E1E4E8">);</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">void</span><span style="color:#6F42C1;--s-dark:#B392F0"> func</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#D73A49;--s-dark:#F97583">int</span><span style="color:#E36209;--s-dark:#FFAB70"> array</span><span style="color:#24292E;--s-dark:#E1E4E8">[</span><span style="color:#005CC5;--s-dark:#79B8FF">10</span><span style="color:#24292E;--s-dark:#E1E4E8">]);</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">void</span><span style="color:#6F42C1;--s-dark:#B392F0"> func</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#D73A49;--s-dark:#F97583">int</span><span style="color:#E36209;--s-dark:#FFAB70"> array</span><span style="color:#D73A49;--s-dark:#F97583">[]</span><span style="color:#24292E;--s-dark:#E1E4E8">);</span></span></code></pre></div>
<p>值的一提的是，在使用这三种方式传参时，第一种和第三种，在 func 中都没有办法通过 len 获取原数组的长度，第二种获取的长度永久为 10，即使实参并不是一个长度为 10 的数组。这足以说明，数组的长度信息，是写在其类型定义中的，当由于传参等方式导致类型的变化，会导致长度信息的丢失。这更贴合了数组本质是由指针来实现的这一说法：实际的存储内存中，除了连续的各个元素外，就没有其他信息了。</p>
<p>与之相对的，Java 是有真正的数组的。Java 中每个数组都是一个对象，数组的相关信息，如基本类型和长度等，都写在对象头中。所以 Java 可以在运行时检查数组是否超限访问，并抛出 IndexOutOfBoundsException。</p>
<h3 id="面向过程还是面向对象">面向过程还是面向对象？<a href="#面向过程还是面向对象" class="heading-anchor-link" aria-label="Link to 面向过程还是面向对象？"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h3>
<p>实际上，这说不上是一个问题，广义上来说，面向对象和面向过程更多的是一种编程思想和范式而非具体的编程语言的差别。C 语言同样可以通过定义结构体来实现面向对象的编程。</p>
<p>那么可以狭义定义一下面向过程和面向对象：只有原生支持、完整实现了面向对象的三大特性的语言（封装、继承和多态），才可以称为面向对象的编程语言。</p>
<p>封装自然不必多说，简单的封装连 C 语言都可以做得到：包装成一个结构体就可以了。然而，封装的一个重要的目的：“对象的内部实现细节被隐藏起来，外部只能通过对象提供的接口来访问和操作对象的数据。”，对于 C 语言的结构体，是没有访问控制的，内部的字段也可以被任意修改，这种封装实质上是没有意义的。至于 Java、CPP 和 C 的一个语法上很大的区别：可以直接通过 . 运算调用对象（结构体）的成员方法，在实现上实际上并没有什么特殊的：在底层实现上，成员方法就是第一个参数为对象指针的函数，编译器自动将对象指针添加到函数参数中并命名为 this 指针，除此以外成员方法和一般函数并无不同。</p>
<p>在实现上，Java、C++ 和 Golang 有异曲同工之妙：它们都通过直接或者间接的方式，通过组合来实现了继承。由于需要使用的是组合，所以在自定义构造方法时，都需要首先调用父类的构造方法。golang 的继承中组合的意味最为明显：直接在结构体中定义一个无名的父类结构，这使得访问父类的变量本质上就是直接访问这个父类对象的变量，而使得更像是一个语法糖了：</p>
<div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--s-dark:#F97583">type</span><span style="color:#6F42C1;--s-dark:#B392F0"> Parent</span><span style="color:#D73A49;--s-dark:#F97583"> struct</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">	a </span><span style="color:#D73A49;--s-dark:#F97583">int64</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">type</span><span style="color:#6F42C1;--s-dark:#B392F0"> Child</span><span style="color:#D73A49;--s-dark:#F97583"> struct</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">	Parent</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">	b </span><span style="color:#D73A49;--s-dark:#F97583">int64</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">c </span><span style="color:#D73A49;--s-dark:#F97583">:=</span><span style="color:#D73A49;--s-dark:#F97583"> &#x26;</span><span style="color:#6F42C1;--s-dark:#B392F0">Child</span><span style="color:#24292E;--s-dark:#E1E4E8">{</span><span style="color:#6F42C1;--s-dark:#B392F0">Parent</span><span style="color:#24292E;--s-dark:#E1E4E8">{}, </span><span style="color:#005CC5;--s-dark:#79B8FF">0</span><span style="color:#24292E;--s-dark:#E1E4E8">}</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">a </span><span style="color:#D73A49;--s-dark:#F97583">:=</span><span style="color:#24292E;--s-dark:#E1E4E8"> c.a</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">//或者</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">a </span><span style="color:#D73A49;--s-dark:#F97583">:=</span><span style="color:#24292E;--s-dark:#E1E4E8"> c.Parent.a;</span></span></code></pre></div>
<p>相对于 Java 和 C++，Golang 的继承更像是过家家，并没有规定子类对父类变量的访问控制，而是直接沿用了通过首字母大小写来决定是否包导出的方法，缺乏像 Java 一般的 protected 来具体控制子访问父的场景。</p>
<p>多态这一特性，一个典型的表现就是：父指针指向不同子类对象时，调用其共有的函数，不同的子类会有不同的行为。Java 和 C++ 以两种典型的方式实现了多态。Java 由于包含一个运行时（JVM），使得它在实现多态时非常容易：虽然使用的是父类型的对象句柄去调用的方法，但是由于可以根据对象句柄定位到对象的具体信息，包含各种类型和方法信息，调用具体的实现方法也就变得比较容易，所以说 Java 的多态是一种运行时多态。而 C++ 则是一种编译期多态。C++ 中，每个类包含了一个虚函数表，对象在实例化时会包含指向自己的虚函数表的指针，虚函数即可能会由于继承而被重写的方法的列表。编译器保证如果子类和父类同时实现了一个方法，那么这个方法在父类和子类的虚函数表中位于同一个位置。这样，对于一个可能被继承的方法的调用，都变成了类似于“调用虚函数表中第 N 个函数”这样的指令。在通过父类指针调用方法时，实际上找到的是子类对象虚函数表中的函数指针，从而实现了调用子类的函数实现，实现了多态。</p>
<h3 id="泛型的实现">泛型的实现<a href="#泛型的实现" class="heading-anchor-link" aria-label="Link to 泛型的实现"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h3>
<p>泛型，得益于 idea 的智能提示，基本算是最广为使用的高级语言特性了。然而，Java 直到 JDK 5 才实现了泛型编程，而 C++ 则更是在 C++11 标准中才引入模板编程（泛型的一种实现方法），算是个很新的特性。很有趣的是，这两种实现恰好代表了两种截然不同的泛型实现方式。</p>
<p>以 Java 为例，泛型是一个编译期实现，在运行时则是无感的，即所谓的类型擦除。所以 Java 的泛型只用于编译期的类型检查：检查你所传入的对象或者类型是否符合泛型参数中规定的类型。如果你通过某种方式绕过了编译期的泛型检查：例如手动构造了一个 class 文件，或者干脆就是运行时通过反射等方式，那么 JVM 对此是无能为力的。例如，你声明了一个 <code>List&#x3C;String></code> 的列表，常规写代码只能 List 内塞入 String 类型的变量。但由于类型擦除的特性，在运行时它本质上只是个 <code>List</code>，或者叫 <code>List&#x3C;Object></code> 也可以，那如果在运行时通过某种方式塞进去一个 Integer，JVM 是不会报错的。</p>
<p>C++ 中，泛型是通过代码生成来实现的，所以 C++ 中泛型的实现被称为模板。编译器会检查模板类的每一个实例化的位置都涉及了哪些模板参数，针对每一种模板参数，生成这套模板参数对应的每一个定义在泛型类中的方法。例如，如果我定义了一个 <code>ClassName&#x3C; typename T ></code> 的模板类，其中包含了一个 <code>Test(T t)</code> 方法，并在下面通过 <code>ClassName&#x3C;int> testObj</code> 根据这个模板创建了一个对象。那么在编译产出中，其实是真实存在 <code>Test(int t)</code> 这个方法的。和 Java 一样，运行时也是无法感知泛型的存在的，根据模板生成的类和方法，同手动定义的无异。但是这种方式实现泛型，相对于 Java 无疑更加安全。由于使用的不是类型擦除而是代码生成，不会出现像 Java 一样只要绕过编译就完全没有检查的现象。</p>
<p>C++ 的泛型有一个缺点，就是通过模板生成的最终实现类必须是完整的，所以即使不包含模板参数的函数，也会被重复生成。即使它们的代码都相同。这样就可能会造成比较大的空间浪费。C# 在实现泛型时，针对这一点进行了优化，C# 不会在编译期直接生成最终的模板代码，而是统计出这个模板类有多少种不同的实现，.Net 运行时（CLR，类似于 JVM）的 JIT 编译期会为这个模板生成一份共享的机器码，而类型特化的信息存在一个额外的表里面由每个实例化泛型类型自己存储，这样就可以共享大部分代码了。具体可见这篇论文。不过这也怪不得 C++，C# 有一个运行时，除非 C++ 也有一个自己的运行时，或者将运行时编译到结果产物中，否则是很难实现这种动态共享的。</p>
<h3 id="尾声">尾声<a href="#尾声" class="heading-anchor-link" aria-label="Link to 尾声"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h3>
<p>当你定好了上面的内容，你就差不多确定了你的编程语言的大致样貌，甚至你还没确定它的语法！剩余的就是一些非常通用、按部就班的内容：去实现它！</p>
<p>当然，你可以尽情地给你的语言添加一些高级特性：例如 gc、例如特殊的生命周期管理。拥有一个 VM 会让高级特性的实现轻松很多，但不是必须的：例如 golang 在实现 gc 时，直接将 gc 相关的代码编译进最终的产物中，相当于你的每一个编译产物都带了一个小小的 VM。</p>
<p>当然，大部分人并需要自己实现编程语言，但是了解这些编程语言之间的一些共性和特性仍然是必要的。世界上没有最好的编程语言，只有某个场景下最适合的编程语言。所以那些最佳编程语言之争，就显得非常可笑：每个编程语言的出现，必然是为了解决某个问题而来。如果有这样一门新编程语言，除了语法之外，和另一门已经存在的编程语言在特性上没有任何不同，那这门新语言是难以长久的：如果不能解决新问题，为什么要费心思学新语言呢？</p>
<p>当然，了解这些特性，还是有一些意想不到的好处的。比如程序员间喝酒吹牛、或者一些技术群中又在争论最好的语言时（当然是有理有据的争论），你就可以高谈阔论了。</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[一次失败的项目实践——春节七天乐（不起来）]]></title>
            <link>https://blog.shinya.click/fiddling/go-os</link>
            <guid isPermaLink="false">https://blog.shinya.click/fiddling/go-os</guid>
            <pubDate>Thu, 02 Feb 2023 15:24:55 GMT</pubDate>
            <description><![CDATA[在一次春节假期中，灵感悄然降临。高铁上偶然阅读了关于在裸机上运行Go程序的文章，激发了对底层系统接口的探索欲望。作者的成功实践和实现，让人对将高级语言与操作系统结合的可能性感到兴奋。随着对相关研究的深入，发现这一概念早已有先例，这股热情在潜移默化中积聚，最终演变成了一场充满期待却未能如愿的项目实践。]]></description>
            <content:encoded><![CDATA[<h3 id="缘起">缘起<a href="#缘起" class="heading-anchor-link" aria-label="Link to 缘起"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h3>
<p>最初是在过年回家的高铁上，在知乎上看到了这篇文章：<a href="https://zhuanlan.zhihu.com/p/265806072" target="_blank" rel="noopener noreferrer" data-umami-event="outbound-link-click" data-umami-event-url="https://zhuanlan.zhihu.com/p/265806072">将 Go 程序跑在裸机上</a>，大致想法是通过实现一遍系统接口，来接管 golang 程序的各种系统调用和中断之类的。感觉这个想法十分有趣。作者还用 golang 写了一个 x86 os：<a href="https://zhuanlan.zhihu.com/p/265806072" target="_blank" rel="noopener noreferrer" data-umami-event="outbound-link-click" data-umami-event-url="https://zhuanlan.zhihu.com/p/265806072">eggos</a>，完成度相当之高。由于是从底层魔改了 golang 的运行时，用户程序完全无感知，所以各种 golang 的第三方库都可以直接使用。作者甚至实现了一个支持 TCP/IP 的协议栈，使得一些网络库可以直接使用。看的我心潮澎湃。</p>
<p>搜了搜一些前人的工作，发现这个想法很早就被人提出来过。2018 年 OSDI 会议上就有一篇论文，讲述了使用高级语言实现操作系统的好处和代价，幻灯片在 <a href="https://www.usenix.org/sites/default/files/conference/protected-files/osdi18_slides_cutler.pdf" target="_blank" rel="noopener noreferrer" data-umami-event="outbound-link-click" data-umami-event-url="https://www.usenix.org/sites/default/files/conference/protected-files/osdi18_slides_cutler.pdf">这儿</a>。另外，相关的实现这几年也是有的，比如 <a href="https://github.com/gopher-os/gopher-os" target="_blank" rel="noopener noreferrer" data-umami-event="outbound-link-click" data-umami-event-url="https://github.com/gopher-os/gopher-os">gopher-os</a>，一个验证性质的内核，只是为了证明使用 golang 实现操作系统是可行的。另外还有 MIT 的一个博士论文项目 <a href="https://github.com/mit-pdos/biscuit" target="_blank" rel="noopener noreferrer" data-umami-event="outbound-link-click" data-umami-event-url="https://github.com/mit-pdos/biscuit">Buscuit</a>，思路是 hack 编译器使得能够编译到裸机，这个项目完成度更高，实现了部分 POSIX 接口，甚至可以在上面跑 redis 和 nginx。</p>
<p>在研究资料过程中，发现了一个共同点：都是基于 x86 架构实现的。我之前用 c 写过一个小内核，是基于 RISC-V 架构，RISC-V 的汇编和各种机制都十分简单，写起来也很舒服。于是就有了这么个想法：用 go 实现一个 RISC-V 的操作系统。</p>
<p>说干就干！到家第二天就开始搞起来了。</p>
<h3 id="就是干">就是干！<a href="#就是干" class="heading-anchor-link" aria-label="Link to 就是干！"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h3>
<p>做一个项目，很重要的一点，就是起名字（bushi</p>
<p>但我确实首先想到了一个绝妙的名字：goose</p>
<figure><img src="https://blog-img.774352199.xyz/2025/736f6c389e54c1327775f1aa95dad597.png" alt="README" loading="lazy" decoding="async" style="background-image:url(data:image/webp;base64,UklGRjIAAABXRUJQVlA4ICYAAADQAQCdASoQAAYAAsBMJaQAAucLLuSMAAD++OJcVYpHtufPqKAAAA==);background-size:cover;background-repeat:no-repeat"><figcaption>README</figcaption></figure>
<p>太妙了兄弟们！</p>
<p>首先 go 是原生支持交叉编译到 RISC-V 64 位的可执行文件的，这是好事。只需要在 go build 命令之前加上 <code>GOOS=linux GOARCH=riscv64</code> 即可，非常方便。</p>
<p>虚拟机照例使用的是 qemu，平台还是 virt。virt 平台的内存布局：0x80000000 以上是物理内存区域，0x80000000 以下是 mmio 区域（大概就是把设备的内存映射到了这片区域，操作这块内存等同于操作这个设备）。virt 启动时会把 pc 设置为 0x80000000</p>
<p>然而正常编译的 go 可执行文件，由于运行在用户态虚拟地址上，entry 的地址都是低地址，大概 0x10000 左右。好在 go 提供了一个链接标志 <code>-T</code> 来指定 TEXT 段的起始地址，可以用这个标志把整个代码段放在高地址内存处，同时还可以通过 <code>-E</code> 来指定入口的标志，这样就可以写一个函数来接管 go 的启动过程（go 程序的入口不是 main 函数，而是 <code>_entry</code> 函数，这个函数用来做一些初始化工作）</p>
<p>然而还有一个严重的问题：我们指定了入口函数，但是没有办法指定这个函数的起始地址，就没法把这个函数放到 0x80000000 处，virt 在启动的时候，0x80000000 就可能是一堆啥也不知道的代码。通常，如果是 c，我们可以通过编写链接脚本来解决这个问题，而且非常简单：直接指定好入口标记的地址，一行就完事。可是这是 go。</p>
<p>在查了一些资料后，在 stackoverflow 上看到了这个 <a href="https://stackoverflow.com/questions/69111979/using-custom-linker-script-with-go-build" target="_blank" rel="noopener noreferrer" data-umami-event="outbound-link-click" data-umami-event-url="https://stackoverflow.com/questions/69111979/using-custom-linker-script-with-go-build">提问</a>，使用外部链接器而非 go 自己内置的链接器，这样就可以指定链接脚本了。但是尝试了下之后，不太可行。go 的可执行文件中除了一些已知的 text 段、bss 段、rodata 段和 data 段，还有一些自己的乱七八糟的段，这些都必须在链接脚本里显式指定，几乎不太可能。</p>
<p>于是更换思路，入口可以写一段 c 代码，这段 c 代码动态获取 go 代码的入口然后跳转过去。由于 go 代码的入口只存在于 elf 文件中，在加载后的内存映像中是没有这个信息的。所以可以把这个 elf 文件直接以二进制的形式链接到 c 程序的 data 段，可以为这段保存二进制的内存开始和结尾指定一个名字，我是用的是 <code>_binary_kernel_elf_start</code> 和 <code>_binary_kernel_elf_end</code>。这样在 c 代码中就可以快速找到了。而 c 代码的作用，就是解析这段内存中保存的 elf 文件，把需要载入内存的段复制到内存对应的地址处，再跳转到 elf 指定的 entry 处即可。</p>
<p>这里贴一下入口的汇编代码，大概就是设置好栈就跳转进 c 函数中，同时指定了 data 段中的两个符号间的一段内存是编译好的 go 可执行文件：</p>
<div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    .</span><span style="color:#6F42C1;--s-dark:#B392F0">section .text</span><span style="color:#24292E;--s-dark:#E1E4E8">.entry</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    .globl _start</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">    # 仅仅是设置了 sp 就跳转到 main</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">_start:</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    la </span><span style="color:#005CC5;--s-dark:#79B8FF">sp</span><span style="color:#24292E;--s-dark:#E1E4E8">, bootstacktop</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">    call</span><span style="color:#24292E;--s-dark:#E1E4E8"> bootmain</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D"># 启动线程的内核栈 bootstack 放置在 bss 段的 stack 标记处</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    .</span><span style="color:#6F42C1;--s-dark:#B392F0">section .bss</span><span style="color:#24292E;--s-dark:#E1E4E8">.stack</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    .align </span><span style="color:#005CC5;--s-dark:#79B8FF">12</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    .global bootstack</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">bootstack:</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">    # 以下 16K 字节的空间作为 OS 的启动栈</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    .space </span><span style="color:#005CC5;--s-dark:#79B8FF">0x4000</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    .global bootstacktop</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">bootstacktop:</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    .</span><span style="color:#6F42C1;--s-dark:#B392F0">section .data</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    .globl _binary_kernel_elf_start</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    .globl _binary_kernel_elf_end</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">_binary_kernel_elf_start:</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    .</span><span style="color:#005CC5;--s-dark:#79B8FF">incbin</span><span style="color:#24292E;--s-dark:#E1E4E8"> "kernel.elf"</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">_binary_kernel_elf_end:</span></span></code></pre></div>
<p>C 函数 bootmain 也十分简单，解析 elf 文件，读取程序头表，把各个段都加载到需要的物理内存处：</p>
<div class="code-collapse-wrapper"><input type="checkbox" id="toggle-id0t8rv" class="code-collapse-toggle" style="display: none;"><div class="code-collapse-preview"><div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0" class="code-preview"><code><span class="line"><span style="color:#D73A49;--s-dark:#F97583">void</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">bootmain</span><span style="color:#24292E;--s-dark:#E1E4E8">()</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">{</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">    struct</span><span style="color:#24292E;--s-dark:#E1E4E8"> elfhdr </span><span style="color:#D73A49;--s-dark:#F97583">*</span><span style="color:#24292E;--s-dark:#E1E4E8">elf;</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">    struct</span><span style="color:#24292E;--s-dark:#E1E4E8"> proghdr </span><span style="color:#D73A49;--s-dark:#F97583">*</span><span style="color:#24292E;--s-dark:#E1E4E8">ph, </span><span style="color:#D73A49;--s-dark:#F97583">*</span><span style="color:#24292E;--s-dark:#E1E4E8">eph;</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">    void</span><span style="color:#24292E;--s-dark:#E1E4E8"> (</span><span style="color:#D73A49;--s-dark:#F97583">*</span><span style="color:#24292E;--s-dark:#E1E4E8">entry)(</span><span style="color:#D73A49;--s-dark:#F97583">void</span><span style="color:#24292E;--s-dark:#E1E4E8">);</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    uchar </span><span style="color:#D73A49;--s-dark:#F97583">*</span><span style="color:#24292E;--s-dark:#E1E4E8">pa;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8"> </span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    elf </span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#24292E;--s-dark:#E1E4E8"> (</span><span style="color:#D73A49;--s-dark:#F97583">struct</span><span style="color:#24292E;--s-dark:#E1E4E8"> elfhdr </span><span style="color:#D73A49;--s-dark:#F97583">*</span><span style="color:#24292E;--s-dark:#E1E4E8">)(_binary_kernel_elf_start);</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8"> </span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">    if</span><span style="color:#24292E;--s-dark:#E1E4E8"> (elf->magic </span><span style="color:#D73A49;--s-dark:#F97583">!=</span><span style="color:#24292E;--s-dark:#E1E4E8"> ELF_MAGIC)</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">        return</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8"> </span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    ph </span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#24292E;--s-dark:#E1E4E8"> (</span><span style="color:#D73A49;--s-dark:#F97583">struct</span><span style="color:#24292E;--s-dark:#E1E4E8"> proghdr </span><span style="color:#D73A49;--s-dark:#F97583">*</span><span style="color:#24292E;--s-dark:#E1E4E8">)((uchar </span><span style="color:#D73A49;--s-dark:#F97583">*</span><span style="color:#24292E;--s-dark:#E1E4E8">)elf </span><span style="color:#D73A49;--s-dark:#F97583">+</span><span style="color:#24292E;--s-dark:#E1E4E8"> elf->phoff);</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    eph </span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#24292E;--s-dark:#E1E4E8"> ph </span><span style="color:#D73A49;--s-dark:#F97583">+</span><span style="color:#24292E;--s-dark:#E1E4E8"> elf->phnum;</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">    for</span><span style="color:#24292E;--s-dark:#E1E4E8"> (; ph </span><span style="color:#D73A49;--s-dark:#F97583">&#x3C;</span><span style="color:#24292E;--s-dark:#E1E4E8"> eph; ph</span><span style="color:#D73A49;--s-dark:#F97583">++</span><span style="color:#24292E;--s-dark:#E1E4E8">)</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    {</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        pa </span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#24292E;--s-dark:#E1E4E8"> (uchar </span><span style="color:#D73A49;--s-dark:#F97583">*</span><span style="color:#24292E;--s-dark:#E1E4E8">)ph->paddr;</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">        readseg</span><span style="color:#24292E;--s-dark:#E1E4E8">(pa, ph->filesz, ph->off);</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">        if</span><span style="color:#24292E;--s-dark:#E1E4E8"> (ph->memsz </span><span style="color:#D73A49;--s-dark:#F97583">></span><span style="color:#24292E;--s-dark:#E1E4E8"> ph->filesz)</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">            clearMem</span><span style="color:#24292E;--s-dark:#E1E4E8">(pa </span><span style="color:#D73A49;--s-dark:#F97583">+</span><span style="color:#24292E;--s-dark:#E1E4E8"> ph->filesz, ph->memsz </span><span style="color:#D73A49;--s-dark:#F97583">-</span><span style="color:#24292E;--s-dark:#E1E4E8"> ph->filesz);</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8"> </span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    entry </span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#24292E;--s-dark:#E1E4E8"> (</span><span style="color:#D73A49;--s-dark:#F97583">void</span><span style="color:#24292E;--s-dark:#E1E4E8"> (</span><span style="color:#D73A49;--s-dark:#F97583">*</span><span style="color:#24292E;--s-dark:#E1E4E8">)(</span><span style="color:#D73A49;--s-dark:#F97583">void</span><span style="color:#24292E;--s-dark:#E1E4E8">))(elf->entry);</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">    entry</span><span style="color:#24292E;--s-dark:#E1E4E8">();</span></span>
</code></pre></div><div class="code-collapse-gradient"></div><label class="code-collapse-expand" for="toggle-id0t8rv"><svg class="code-collapse-icon" width="32" height="24" viewBox="0 0 32 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="4,9 12,17 20,9"></polyline></svg></label></div><div class="code-collapse-full"><div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--s-dark:#F97583">void</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">bootmain</span><span style="color:#24292E;--s-dark:#E1E4E8">()</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">{</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">    struct</span><span style="color:#24292E;--s-dark:#E1E4E8"> elfhdr </span><span style="color:#D73A49;--s-dark:#F97583">*</span><span style="color:#24292E;--s-dark:#E1E4E8">elf;</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">    struct</span><span style="color:#24292E;--s-dark:#E1E4E8"> proghdr </span><span style="color:#D73A49;--s-dark:#F97583">*</span><span style="color:#24292E;--s-dark:#E1E4E8">ph, </span><span style="color:#D73A49;--s-dark:#F97583">*</span><span style="color:#24292E;--s-dark:#E1E4E8">eph;</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">    void</span><span style="color:#24292E;--s-dark:#E1E4E8"> (</span><span style="color:#D73A49;--s-dark:#F97583">*</span><span style="color:#24292E;--s-dark:#E1E4E8">entry)(</span><span style="color:#D73A49;--s-dark:#F97583">void</span><span style="color:#24292E;--s-dark:#E1E4E8">);</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    uchar </span><span style="color:#D73A49;--s-dark:#F97583">*</span><span style="color:#24292E;--s-dark:#E1E4E8">pa;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8"> </span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    elf </span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#24292E;--s-dark:#E1E4E8"> (</span><span style="color:#D73A49;--s-dark:#F97583">struct</span><span style="color:#24292E;--s-dark:#E1E4E8"> elfhdr </span><span style="color:#D73A49;--s-dark:#F97583">*</span><span style="color:#24292E;--s-dark:#E1E4E8">)(_binary_kernel_elf_start);</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8"> </span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">    if</span><span style="color:#24292E;--s-dark:#E1E4E8"> (elf->magic </span><span style="color:#D73A49;--s-dark:#F97583">!=</span><span style="color:#24292E;--s-dark:#E1E4E8"> ELF_MAGIC)</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">        return</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8"> </span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    ph </span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#24292E;--s-dark:#E1E4E8"> (</span><span style="color:#D73A49;--s-dark:#F97583">struct</span><span style="color:#24292E;--s-dark:#E1E4E8"> proghdr </span><span style="color:#D73A49;--s-dark:#F97583">*</span><span style="color:#24292E;--s-dark:#E1E4E8">)((uchar </span><span style="color:#D73A49;--s-dark:#F97583">*</span><span style="color:#24292E;--s-dark:#E1E4E8">)elf </span><span style="color:#D73A49;--s-dark:#F97583">+</span><span style="color:#24292E;--s-dark:#E1E4E8"> elf->phoff);</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    eph </span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#24292E;--s-dark:#E1E4E8"> ph </span><span style="color:#D73A49;--s-dark:#F97583">+</span><span style="color:#24292E;--s-dark:#E1E4E8"> elf->phnum;</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">    for</span><span style="color:#24292E;--s-dark:#E1E4E8"> (; ph </span><span style="color:#D73A49;--s-dark:#F97583">&#x3C;</span><span style="color:#24292E;--s-dark:#E1E4E8"> eph; ph</span><span style="color:#D73A49;--s-dark:#F97583">++</span><span style="color:#24292E;--s-dark:#E1E4E8">)</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    {</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        pa </span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#24292E;--s-dark:#E1E4E8"> (uchar </span><span style="color:#D73A49;--s-dark:#F97583">*</span><span style="color:#24292E;--s-dark:#E1E4E8">)ph->paddr;</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">        readseg</span><span style="color:#24292E;--s-dark:#E1E4E8">(pa, ph->filesz, ph->off);</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">        if</span><span style="color:#24292E;--s-dark:#E1E4E8"> (ph->memsz </span><span style="color:#D73A49;--s-dark:#F97583">></span><span style="color:#24292E;--s-dark:#E1E4E8"> ph->filesz)</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">            clearMem</span><span style="color:#24292E;--s-dark:#E1E4E8">(pa </span><span style="color:#D73A49;--s-dark:#F97583">+</span><span style="color:#24292E;--s-dark:#E1E4E8"> ph->filesz, ph->memsz </span><span style="color:#D73A49;--s-dark:#F97583">-</span><span style="color:#24292E;--s-dark:#E1E4E8"> ph->filesz);</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8"> </span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    entry </span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#24292E;--s-dark:#E1E4E8"> (</span><span style="color:#D73A49;--s-dark:#F97583">void</span><span style="color:#24292E;--s-dark:#E1E4E8"> (</span><span style="color:#D73A49;--s-dark:#F97583">*</span><span style="color:#24292E;--s-dark:#E1E4E8">)(</span><span style="color:#D73A49;--s-dark:#F97583">void</span><span style="color:#24292E;--s-dark:#E1E4E8">))(elf->entry);</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">    entry</span><span style="color:#24292E;--s-dark:#E1E4E8">();</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">}</span></span></code><label class="code-collapse-collapse" for="toggle-id0t8rv"><svg class="code-collapse-icon" width="32" height="24" viewBox="0 0 32 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20,15 12,7 4,15"></polyline></svg></label></pre></div></div></div>
<p>最后 entry 的位置就是从 elf 头中读出来的 go 入口函数地址，跳转过去即可。</p>
<p>go 入口函数是 rt0 函数，是一个汇编函数。go 使用的汇编格式是 PLAN9 汇编，起源于一个上古操作系统 plan9。这个格式的汇编支持多个指令集架构，但是很神奇的是找不到任何官方的文档描述不同的指令集架构中这个格式的汇编支持哪些指令。x86 的还能找到点资料，因为 PLAN9 汇编的例子基本是 x86 的，RV64 则是一点痕迹都没有，完全靠猜（</p>
<p>通过各种摸索，最后终于写出了入口：</p>
<div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">#</span><span style="color:#D73A49;--s-dark:#F97583">include</span><span style="color:#032F62;--s-dark:#9ECBFF"> "textflag.h"</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">TEXT ·rt0(SB),</span><span style="color:#D73A49;--s-dark:#F97583">NOSPLIT</span><span style="color:#24292E;--s-dark:#E1E4E8">|NOFRAME,</span><span style="color:#005CC5;--s-dark:#79B8FF">$0</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">    CALL</span><span style="color:#24292E;--s-dark:#E1E4E8"> ·kernelStackTop(SB)</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">    MOV</span><span style="color:#005CC5;--s-dark:#79B8FF">  0</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#005CC5;--s-dark:#79B8FF">SP</span><span style="color:#24292E;--s-dark:#E1E4E8">), A1</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">    MOV</span><span style="color:#24292E;--s-dark:#E1E4E8">  A1, </span><span style="color:#005CC5;--s-dark:#79B8FF">SP</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">    CALL</span><span style="color:#24292E;--s-dark:#E1E4E8"> ·kmain(SB)</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    UNDEF</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">    RET</span></span></code></pre></div>
<p>这个格式也蛮阴间的……做的事情基本一致，调用 kernelStackTop 获得预先分配好的栈顶地址，并把 SP 指针指向那个地址，随后就调用 go 语言的入口了：kmain。唯一的 go 文件写得也很简单：</p>
<div class="code-collapse-wrapper"><input type="checkbox" id="toggle-0rsub0k" class="code-collapse-toggle" style="display: none;"><div class="code-collapse-preview"><div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0" class="code-preview"><code><span class="line"><span style="color:#D73A49;--s-dark:#F97583">type</span><span style="color:#6F42C1;--s-dark:#B392F0"> stack</span><span style="color:#24292E;--s-dark:#E1E4E8"> [</span><span style="color:#005CC5;--s-dark:#79B8FF">16</span><span style="color:#D73A49;--s-dark:#F97583"> *</span><span style="color:#005CC5;--s-dark:#79B8FF"> 4096</span><span style="color:#24292E;--s-dark:#E1E4E8">]</span><span style="color:#D73A49;--s-dark:#F97583">byte</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">type</span><span style="color:#6F42C1;--s-dark:#B392F0"> virtualAddress</span><span style="color:#D73A49;--s-dark:#F97583"> uintptr</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">var</span><span style="color:#24292E;--s-dark:#E1E4E8"> (</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    kstack </span><span style="color:#6F42C1;--s-dark:#B392F0">stack</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">)</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">//go:nosplit</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">func</span><span style="color:#24292E;--s-dark:#E1E4E8"> (</span><span style="color:#E36209;--s-dark:#FFAB70">s </span><span style="color:#D73A49;--s-dark:#F97583">*</span><span style="color:#6F42C1;--s-dark:#B392F0">stack</span><span style="color:#24292E;--s-dark:#E1E4E8">) </span><span style="color:#6F42C1;--s-dark:#B392F0">top</span><span style="color:#24292E;--s-dark:#E1E4E8">() </span><span style="color:#6F42C1;--s-dark:#B392F0">virtualAddress</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    stackTop </span><span style="color:#D73A49;--s-dark:#F97583">:=</span><span style="color:#D73A49;--s-dark:#F97583"> uintptr</span><span style="color:#24292E;--s-dark:#E1E4E8">(unsafe.</span><span style="color:#6F42C1;--s-dark:#B392F0">Pointer</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#D73A49;--s-dark:#F97583">&#x26;</span><span style="color:#24292E;--s-dark:#E1E4E8">s[</span><span style="color:#005CC5;--s-dark:#79B8FF">0</span><span style="color:#24292E;--s-dark:#E1E4E8">])) </span><span style="color:#D73A49;--s-dark:#F97583">+</span><span style="color:#24292E;--s-dark:#E1E4E8"> unsafe.</span><span style="color:#6F42C1;--s-dark:#B392F0">Sizeof</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#D73A49;--s-dark:#F97583">*</span><span style="color:#24292E;--s-dark:#E1E4E8">s)</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">    // Align to 16 bytes.</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    stackTop </span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#24292E;--s-dark:#E1E4E8"> stackTop </span><span style="color:#D73A49;--s-dark:#F97583">&#x26;^</span><span style="color:#D73A49;--s-dark:#F97583"> 0x</span><span style="color:#005CC5;--s-dark:#79B8FF">f</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">    return</span><span style="color:#6F42C1;--s-dark:#B392F0"> virtualAddress</span><span style="color:#24292E;--s-dark:#E1E4E8">(stackTop)</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">//go:nosplit</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">func</span><span style="color:#6F42C1;--s-dark:#B392F0"> kernelStackTop</span><span style="color:#24292E;--s-dark:#E1E4E8">() </span><span style="color:#D73A49;--s-dark:#F97583">uint64</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">    return</span><span style="color:#D73A49;--s-dark:#F97583"> uint64</span><span style="color:#24292E;--s-dark:#E1E4E8">(kstack.</span><span style="color:#6F42C1;--s-dark:#B392F0">top</span><span style="color:#24292E;--s-dark:#E1E4E8">())</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">//go:nosplit</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">func</span><span style="color:#6F42C1;--s-dark:#B392F0"> rt0</span><span style="color:#24292E;--s-dark:#E1E4E8">()</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">//go:nosplit</span></span>
</code></pre></div><div class="code-collapse-gradient"></div><label class="code-collapse-expand" for="toggle-0rsub0k"><svg class="code-collapse-icon" width="32" height="24" viewBox="0 0 32 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="4,9 12,17 20,9"></polyline></svg></label></div><div class="code-collapse-full"><div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--s-dark:#F97583">type</span><span style="color:#6F42C1;--s-dark:#B392F0"> stack</span><span style="color:#24292E;--s-dark:#E1E4E8"> [</span><span style="color:#005CC5;--s-dark:#79B8FF">16</span><span style="color:#D73A49;--s-dark:#F97583"> *</span><span style="color:#005CC5;--s-dark:#79B8FF"> 4096</span><span style="color:#24292E;--s-dark:#E1E4E8">]</span><span style="color:#D73A49;--s-dark:#F97583">byte</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">type</span><span style="color:#6F42C1;--s-dark:#B392F0"> virtualAddress</span><span style="color:#D73A49;--s-dark:#F97583"> uintptr</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">var</span><span style="color:#24292E;--s-dark:#E1E4E8"> (</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    kstack </span><span style="color:#6F42C1;--s-dark:#B392F0">stack</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">)</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">//go:nosplit</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">func</span><span style="color:#24292E;--s-dark:#E1E4E8"> (</span><span style="color:#E36209;--s-dark:#FFAB70">s </span><span style="color:#D73A49;--s-dark:#F97583">*</span><span style="color:#6F42C1;--s-dark:#B392F0">stack</span><span style="color:#24292E;--s-dark:#E1E4E8">) </span><span style="color:#6F42C1;--s-dark:#B392F0">top</span><span style="color:#24292E;--s-dark:#E1E4E8">() </span><span style="color:#6F42C1;--s-dark:#B392F0">virtualAddress</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    stackTop </span><span style="color:#D73A49;--s-dark:#F97583">:=</span><span style="color:#D73A49;--s-dark:#F97583"> uintptr</span><span style="color:#24292E;--s-dark:#E1E4E8">(unsafe.</span><span style="color:#6F42C1;--s-dark:#B392F0">Pointer</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#D73A49;--s-dark:#F97583">&#x26;</span><span style="color:#24292E;--s-dark:#E1E4E8">s[</span><span style="color:#005CC5;--s-dark:#79B8FF">0</span><span style="color:#24292E;--s-dark:#E1E4E8">])) </span><span style="color:#D73A49;--s-dark:#F97583">+</span><span style="color:#24292E;--s-dark:#E1E4E8"> unsafe.</span><span style="color:#6F42C1;--s-dark:#B392F0">Sizeof</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#D73A49;--s-dark:#F97583">*</span><span style="color:#24292E;--s-dark:#E1E4E8">s)</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">    // Align to 16 bytes.</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    stackTop </span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#24292E;--s-dark:#E1E4E8"> stackTop </span><span style="color:#D73A49;--s-dark:#F97583">&#x26;^</span><span style="color:#D73A49;--s-dark:#F97583"> 0x</span><span style="color:#005CC5;--s-dark:#79B8FF">f</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">    return</span><span style="color:#6F42C1;--s-dark:#B392F0"> virtualAddress</span><span style="color:#24292E;--s-dark:#E1E4E8">(stackTop)</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">//go:nosplit</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">func</span><span style="color:#6F42C1;--s-dark:#B392F0"> kernelStackTop</span><span style="color:#24292E;--s-dark:#E1E4E8">() </span><span style="color:#D73A49;--s-dark:#F97583">uint64</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">    return</span><span style="color:#D73A49;--s-dark:#F97583"> uint64</span><span style="color:#24292E;--s-dark:#E1E4E8">(kstack.</span><span style="color:#6F42C1;--s-dark:#B392F0">top</span><span style="color:#24292E;--s-dark:#E1E4E8">())</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">//go:nosplit</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">func</span><span style="color:#6F42C1;--s-dark:#B392F0"> rt0</span><span style="color:#24292E;--s-dark:#E1E4E8">()</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">//go:nosplit</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">func</span><span style="color:#6F42C1;--s-dark:#B392F0"> kmain</span><span style="color:#24292E;--s-dark:#E1E4E8">() {</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">    for</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">}</span></span></code><label class="code-collapse-collapse" for="toggle-0rsub0k"><svg class="code-collapse-icon" width="32" height="24" viewBox="0 0 32 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20,15 12,7 4,15"></polyline></svg></label></pre></div></div></div>
<p>预先分配了 stack 数组作为内核栈，kmain 啥也没干，就是无限循环。注意每个函数都有一个编译标识：<code>//go:nosplit</code>，表示让编译器不要插入检查这个函数的是否会栈溢出的代码，同时还有一个隐式的用途：阻止编译器在函数中插入 gc 检查点。如果触发了 gc，以现在这个啥也没有的裸机，gc 是完全不支持的（当然 gc 也不应该在内核中跑，更多的处理用户空间的堆）</p>
<p>这样 Makefile 就可以这样写了：</p>
<div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">Image</span><span style="color:#24292E;--s-dark:#E1E4E8">: kernel.elf</span></span>
<span class="line"><span style="color:#032F62;--s-dark:#9ECBFF">    $(</span><span style="color:#24292E;--s-dark:#E1E4E8">CC</span><span style="color:#032F62;--s-dark:#9ECBFF">)</span><span style="color:#032F62;--s-dark:#9ECBFF"> $(</span><span style="color:#24292E;--s-dark:#E1E4E8">CFLAGS</span><span style="color:#032F62;--s-dark:#9ECBFF">)</span><span style="color:#24292E;--s-dark:#E1E4E8"> -fno-pic -O -nostdinc -I. -c boot/boot.c</span></span>
<span class="line"><span style="color:#032F62;--s-dark:#9ECBFF">    $(</span><span style="color:#24292E;--s-dark:#E1E4E8">CC</span><span style="color:#032F62;--s-dark:#9ECBFF">)</span><span style="color:#032F62;--s-dark:#9ECBFF"> $(</span><span style="color:#24292E;--s-dark:#E1E4E8">CFLAGS</span><span style="color:#032F62;--s-dark:#9ECBFF">)</span><span style="color:#24292E;--s-dark:#E1E4E8"> -fno-pic -nostdinc -I. -c boot/boot_header.S</span></span>
<span class="line"><span style="color:#032F62;--s-dark:#9ECBFF">    $(</span><span style="color:#24292E;--s-dark:#E1E4E8">LD</span><span style="color:#032F62;--s-dark:#9ECBFF">)</span><span style="color:#032F62;--s-dark:#9ECBFF"> $(</span><span style="color:#24292E;--s-dark:#E1E4E8">LDFLAGS</span><span style="color:#032F62;--s-dark:#9ECBFF">)</span><span style="color:#24292E;--s-dark:#E1E4E8"> -T image.ld -o Image boot.o boot_header.o</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">kernel.elf</span><span style="color:#24292E;--s-dark:#E1E4E8">:</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    GOOS=linux GOARCH=riscv64 go build -o kernel.elf -ldflags '-E goose/kernel.rt0 -T 0x80200000' -gcflags "-N -l" ./kmain</span></span></code></pre></div>
<p>kernel.elf 编译 go 的 elf 文件，指定入口函数为 goose/kernel.rt0，TEXT 段的起始地址为 0x80200000。而 Image 则是编译了上面说的加载内核的入口代码，image.ld 中指定了将入口函数放在 TEXT 段的入口，并把 TEXT 段放在 0x80000000 位置。</p>
<div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0"><code><span class="line"><span>/* 执行入口 */</span></span>
<span class="line"><span>ENTRY(_start)</span></span>
<span class="line"><span></span></span>
<span class="line"><span>/* 数据存放起始地址 */</span></span>
<span class="line"><span>BASE_ADDRESS = 0x80000000;</span></span>
<span class="line"><span></span></span>
<span class="line"><span>SECTIONS</span></span>
<span class="line"><span>{</span></span>
<span class="line"><span>    /* . 表示当前地址（location counter） */</span></span>
<span class="line"><span>    . = BASE_ADDRESS;</span></span>
<span class="line"><span></span></span>
<span class="line"><span>    /* start 符号表示全部的开始位置 */</span></span>
<span class="line"><span>    kernel_start = .;</span></span>
<span class="line"><span></span></span>
<span class="line"><span>    text_start = .;</span></span>
<span class="line"><span></span></span>
<span class="line"><span>    /* .text 字段 */</span></span>
<span class="line"><span>    .text : {</span></span>
<span class="line"><span>        /* 把 entry 函数放在最前面 */</span></span>
<span class="line"><span>        *(.text.entry)</span></span>
<span class="line"><span>        /* 要链接的文件的 .text 字段集中放在这里 */</span></span>
<span class="line"><span>        *(.text .text.*)</span></span>
<span class="line"><span>    }</span></span>
<span class="line"><span>    ...</span></span>
<span class="line"><span>}</span></span></code></pre></div>
<p>妥！</p>
<p>因为兴趣很大，整个春节我亲戚都没有走好，整天就是憋在屋里收集资料，在外面也是发呆想思路，魔怔了一样。</p>
<h3 id="大失败">大失败<a href="#大失败" class="heading-anchor-link" aria-label="Link to 大失败"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h3>
<p>噔噔咚！</p>
<p>在把内核加载到 qemu 开始运行后，debug 看到卡死在把程序段加载到内存中。于是用 readelf 检查了一下 go build 出来的 elf 文件，发现了这个诡异的东西</p>
<div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">Type</span><span style="color:#032F62;--s-dark:#9ECBFF">           Offset</span><span style="color:#032F62;--s-dark:#9ECBFF">             VirtAddr</span><span style="color:#032F62;--s-dark:#9ECBFF">           PhysAddr</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">                 FileSiz</span><span style="color:#032F62;--s-dark:#9ECBFF">            MemSiz</span><span style="color:#032F62;--s-dark:#9ECBFF">              Flags</span><span style="color:#032F62;--s-dark:#9ECBFF">  Align</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">  PHDR</span><span style="color:#005CC5;--s-dark:#79B8FF">           0x0000000000000040</span><span style="color:#005CC5;--s-dark:#79B8FF"> 0x00000000801ff040</span><span style="color:#005CC5;--s-dark:#79B8FF"> 0x00000000801ff040</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">                 0x0000000000000188</span><span style="color:#005CC5;--s-dark:#79B8FF"> 0x0000000000000188</span><span style="color:#032F62;--s-dark:#9ECBFF">  R</span><span style="color:#005CC5;--s-dark:#79B8FF">      0x10000</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">  NOTE</span><span style="color:#005CC5;--s-dark:#79B8FF">           0x0000000000000f9c</span><span style="color:#005CC5;--s-dark:#79B8FF"> 0x00000000801fff9c</span><span style="color:#005CC5;--s-dark:#79B8FF"> 0x00000000801fff9c</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">                 0x0000000000000064</span><span style="color:#005CC5;--s-dark:#79B8FF"> 0x0000000000000064</span><span style="color:#032F62;--s-dark:#9ECBFF">  R</span><span style="color:#005CC5;--s-dark:#79B8FF">      0x4</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">  LOAD</span><span style="color:#005CC5;--s-dark:#79B8FF">           0xffffffffffff1000</span><span style="color:#005CC5;--s-dark:#79B8FF"> 0x00000000801f0000</span><span style="color:#005CC5;--s-dark:#79B8FF"> 0x00000000801f0000</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">                 0x0000000000063300</span><span style="color:#005CC5;--s-dark:#79B8FF"> 0x0000000000063300</span><span style="color:#032F62;--s-dark:#9ECBFF">  R</span><span style="color:#032F62;--s-dark:#9ECBFF"> E</span><span style="color:#005CC5;--s-dark:#79B8FF">    0x10000</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">  LOAD</span><span style="color:#005CC5;--s-dark:#79B8FF">           0x0000000000060000</span><span style="color:#005CC5;--s-dark:#79B8FF"> 0x0000000080260000</span><span style="color:#005CC5;--s-dark:#79B8FF"> 0x0000000080260000</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">                 0x000000000006adb8</span><span style="color:#005CC5;--s-dark:#79B8FF"> 0x000000000006adb8</span><span style="color:#032F62;--s-dark:#9ECBFF">  R</span><span style="color:#005CC5;--s-dark:#79B8FF">      0x10000</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">  ...</span></span></code></pre></div>
<p>注意第三段的 Offset 是 0xffffffffffff1000 这个大的吓人的数。Offset 是这个段的内容在文件中存放的位置相对于文件开头的偏移。这个 elf 文件才几十 KB，哪来这么大的偏移？即使加载到内存中，virt 计算机的默认物理内存大小也只有 128 MB，直接炸裂</p>
<p>百思不得其解，于是开始试验起来，最后发现，只要加上 <code>-T</code> 这个链接参数，就会出现这种情况。但是不加又不行，这些内存段不能被加载到低地址上，因为那是 mmio 的位置。于是我去 go 的 github 仓库里发了个 issue：<a href="https://github.com/golang/go/issues/57983" target="_blank" rel="noopener noreferrer" data-umami-event="outbound-link-click" data-umami-event-url="https://github.com/golang/go/issues/57983">cmd/link: wrong program header offset when cross-compile to riscv64 when setting -T text alignment</a>。描述了一下后，得到的回答是：</p>
<figure><img src="https://blog-img.774352199.xyz/2025/42c633b821d4323697e542b47a8fce31.png" alt="ISSUE" loading="lazy" decoding="async" style="background-image:url(data:image/webp;base64,UklGRigAAABXRUJQVlA4IBwAAAAwAQCdASoQAAIAAsBMJaQAA3AA/vZhlhMOtQAA);background-size:cover;background-repeat:no-repeat"><figcaption>ISSUE</figcaption></figure>
<p>看来是 RV64 对 <code>-T</code> 的支持不太完善……</p>
<p>于是这个项目就被搁置到了现在，可惜了我想的好名字/(ㄒo ㄒ)/ 只能期待后续 go 官方能修复这个问题，但是感觉 go 对 RV64 不是很上心，原生支持交叉编译到 RV64 也是近几年才合进主线的……</p>
<p>很气，转投 Rust 去了！</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[6.5840 实验 2a —— Leader 选举]]></title>
            <link>https://blog.shinya.click/notes/65840/raftlab2a</link>
            <guid isPermaLink="false">https://blog.shinya.click/notes/65840/raftlab2a</guid>
            <pubDate>Thu, 15 Dec 2022 18:06:10 GMT</pubDate>
            <description><![CDATA[实验 2a 专注于实现 Raft 算法中的 Leader 选举和心跳机制，旨在确保在各种极端情况下的正常换届和选举。这一实验是后续分布式KV存储实现的基础，包含了四个步骤，通过无锁版本的设计简化了 Raft 结构体的复杂性。实验指导书提供了必要的背景资料，但与前一实验相比，本次几乎不依赖参考资料，强调了独立实现的重要性。]]></description>
            <content:encoded><![CDATA[<h3 id="前言">前言<a href="#前言" class="heading-anchor-link" aria-label="Link to 前言"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h3>
<p>6.824 的实验二，是实现 Raft 算法，在后续实验中的实现的分布式 KV 存储会将本实验实现的 Raft 算法作为分布式共识模块使用，所以实验二对后续实验至关重要。</p>
<p>实验二将整个 Raft 算法分为四个步骤，作为四个子实验去实现。实验 2a 只实现基本的 Leader 选举和心跳，来保证在各种极端（断线）场景下都可以正常地换届和选举。</p>
<p>Of course，2a 作为奠定整个四个子实验基础的起始实验，不仅仅需要实现 Leader 选举功能，更需要搭好整体的流程处理的框架。同样，我实现的是无锁版本，Raft 结构体里的 mu 变量可以删掉啦（癫狂</p>
<h3 id="实验讲解">实验讲解<a href="#实验讲解" class="heading-anchor-link" aria-label="Link to 实验讲解"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h3>
<p>实验指导书在 <a href="https://pdos.csail.mit.edu/6.824/labs/lab-raft.html" target="_blank" rel="noopener noreferrer" data-umami-event="outbound-link-click" data-umami-event-url="https://pdos.csail.mit.edu/6.824/labs/lab-raft.html">https://pdos.csail.mit.edu/6.824/labs/lab-raft.html</a>。和实验一不一样，这次几乎没有任何参考。我们需要实现的代码在 <code>src/raft/raft.go</code> 中，这个 Raft 结构体只有一个很基础的结构体：</p>
<div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--s-dark:#F97583">type</span><span style="color:#6F42C1;--s-dark:#B392F0"> Raft</span><span style="color:#D73A49;--s-dark:#F97583"> struct</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    peers []</span><span style="color:#D73A49;--s-dark:#F97583">*</span><span style="color:#6F42C1;--s-dark:#B392F0">labrpc</span><span style="color:#24292E;--s-dark:#E1E4E8">.</span><span style="color:#6F42C1;--s-dark:#B392F0">ClientEnd</span><span style="color:#6A737D;--s-dark:#6A737D"> // RPC end points of all peers</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    persister </span><span style="color:#D73A49;--s-dark:#F97583">*</span><span style="color:#6F42C1;--s-dark:#B392F0">Persister</span><span style="color:#6A737D;--s-dark:#6A737D"> // Object to hold this peer's persisted state</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    me </span><span style="color:#D73A49;--s-dark:#F97583">int</span><span style="color:#6A737D;--s-dark:#6A737D"> // this peer's index into peers[]</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    dead </span><span style="color:#D73A49;--s-dark:#F97583">int32</span><span style="color:#6A737D;--s-dark:#6A737D"> // set by Kill()</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">}</span></span></code></pre></div>
<p>每一个 Raft 结构体都是集群中的一个 Server，Raft 结构体需要存储该 Server 所有需要的内容。</p>
<p>其中 peers 是当前配置集群的所有 server，ClientEnd 结构体可以通过调用 Call 直接发送 RPC 请求，me 则是当前机器在集群中的唯一标识，再其他机器上也是认这个 index 的。</p>
<p>lab2a 中 Raft 的入口是 <code>Make()</code> 方法，在 Make 方法初始化完成结构体后，会启动一个协程 <code>rf.ticker()</code>，该协程会执行一个无限循环（其实是根据结束标识持续执行的循环，鉴于我们不关心机器被关闭后的事情，所以可以看作无限循环），这个方法可以看作主协程。</p>
<p>实验二最难的地方就在于，框架实现的内容太少了，我们基本需要从零实现整个 Raft 算法。好在，论文中的 Figure 2 基本已经给出了整体的实现思路。</p>
<p>另外，测试用例的实现在同文件夹下的 <code>test_test.go</code> 中，如果测试用例不通过，可以看一下测试用例的实现，根据测试场景来 debug。</p>
<p>lab 2d 的测试命令为 <code>go test -run 2A</code>，建议使用 <code>go test -race -run 2A</code> 来同时检测数据竞争。</p>
<h3 id="实验思路">实验思路<a href="#实验思路" class="heading-anchor-link" aria-label="Link to 实验思路"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h3>
<h4 id="整体流程">整体流程<a href="#整体流程" class="heading-anchor-link" aria-label="Link to 整体流程"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h4>
<p>由于实现无锁版本，首先就需要仔细规划整体的处理流程和协程间通信，打好一个良好的基础，对后续实验也有很大的帮助，毕竟是一系列实验中的第一个。</p>
<p>首先约定主协程就是 <code>rf.ticker()</code> 方法，只有这个方法可以修改 Raft 结构体中的字段，其他协程都不允许，这样就直接避免了数据竞争，所以 <code>ticker()</code> 方法中应当是无限循环监听一堆 channel 的消息。</p>
<p>那么具体有哪些协程需要通信，需要哪些管道呢？首先，选举一共涉及两种 RPC 请求：追加请求和拉票请求，当服务器作为这两种请求的接收端时，一定不是首先在主协程中接收 RPC 请求的，那么这两种 RPC 请求需要发给主协程处理，就需要两个管道；其次，如果机器作为发送端，发送请求一般是由非主协程操作的（不能让主协程等待 RPC 返回），那么在这两种 RPC 获取到响应时，需要提交给主协程处理，就又需要两个管道。</p>
<p>除此以外，还需要两个定时器，分别用于选举超时和心跳超时。实验指导中推荐是使用 <code>time.Sleep()</code>，通过睡一段时间来实现定时。但是这种方式没有办法实现倒计时打断，所以，虽然指导书中不推荐 <code>time.Timer</code>，但是叛逆为了实现倒计时打断重置，我还是使用了 Timer。不过，timer 用对真的挺不容易。</p>
<p>首先需要定义 Server 的状态，来标识 Server 的身份：</p>
<div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--s-dark:#F97583">type</span><span style="color:#6F42C1;--s-dark:#B392F0"> ServerStatus</span><span style="color:#D73A49;--s-dark:#F97583"> uint8</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8"> </span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">const</span><span style="color:#24292E;--s-dark:#E1E4E8"> (</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">    Follower</span><span style="color:#6F42C1;--s-dark:#B392F0">  ServerStatus</span><span style="color:#D73A49;--s-dark:#F97583"> =</span><span style="color:#005CC5;--s-dark:#79B8FF"> 0</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">    Candidate</span><span style="color:#6F42C1;--s-dark:#B392F0"> ServerStatus</span><span style="color:#D73A49;--s-dark:#F97583"> =</span><span style="color:#005CC5;--s-dark:#79B8FF"> 1</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">    Leader</span><span style="color:#6F42C1;--s-dark:#B392F0">    ServerStatus</span><span style="color:#D73A49;--s-dark:#F97583"> =</span><span style="color:#005CC5;--s-dark:#79B8FF"> 2</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">)</span></span></code></pre></div>
<p>对照 Raft 论文 Figure 2，定义一下 Server 的基础字段，并定义上面提到的几个管道和定时器：</p>
<div class="code-collapse-wrapper"><input type="checkbox" id="toggle-yjt9nke" class="code-collapse-toggle" style="display: none;"><div class="code-collapse-preview"><div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0" class="code-preview"><code><span class="line"><span style="color:#D73A49;--s-dark:#F97583">type</span><span style="color:#6F42C1;--s-dark:#B392F0"> Raft</span><span style="color:#D73A49;--s-dark:#F97583"> struct</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">    ...</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">    // Status</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    Status </span><span style="color:#6F42C1;--s-dark:#B392F0">ServerStatus</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">    // 已提交日志，外部获取管道</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    ApplyCh </span><span style="color:#D73A49;--s-dark:#F97583">chan</span><span style="color:#6F42C1;--s-dark:#B392F0"> ApplyMsg</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">    /***** 所有 Server 都包含的持久状态 *****/</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">    // CurrentTerm 机器遇到的最大的任期，启动时初始化为 0，单调递增</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    CurrentTerm </span><span style="color:#D73A49;--s-dark:#F97583">int</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">    // VotedFor 当前任期内投票的 Candidate ID，未投票则为 -1</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    VotedFor </span><span style="color:#D73A49;--s-dark:#F97583">int</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">    // Logs 日志条目，每个条目都包含了一条状态机指令和 Leader 接收该条目时的任期，index 从 1 开始</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    Logs []</span><span style="color:#D73A49;--s-dark:#F97583">*</span><span style="color:#6F42C1;--s-dark:#B392F0">LogEntry</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">    /***** 所有 Server 都包含的可变状态 *****/</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">    // CommitIndex 已知的最大的即将提交的日志索引，启动时初始化为 0，单调递增</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    CommitIndex </span><span style="color:#D73A49;--s-dark:#F97583">uint64</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">    // LastApplied 最大的已提交的日志索引，启动时初始化为 0，单调递增</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    LastApplied </span><span style="color:#D73A49;--s-dark:#F97583">uint64</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">    /******* Leader 包含的可变状态，选举后初始化 *******/</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">    // NextIndex 每台机器下一个要发送的日志条目的索引，初始化为 Leader 最后一个日志索引 +1</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    NextIndex []</span><span style="color:#D73A49;--s-dark:#F97583">uint64</span></span>
</code></pre></div><div class="code-collapse-gradient"></div><label class="code-collapse-expand" for="toggle-yjt9nke"><svg class="code-collapse-icon" width="32" height="24" viewBox="0 0 32 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="4,9 12,17 20,9"></polyline></svg></label></div><div class="code-collapse-full"><div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--s-dark:#F97583">type</span><span style="color:#6F42C1;--s-dark:#B392F0"> Raft</span><span style="color:#D73A49;--s-dark:#F97583"> struct</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">    ...</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">    // Status</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    Status </span><span style="color:#6F42C1;--s-dark:#B392F0">ServerStatus</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">    // 已提交日志，外部获取管道</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    ApplyCh </span><span style="color:#D73A49;--s-dark:#F97583">chan</span><span style="color:#6F42C1;--s-dark:#B392F0"> ApplyMsg</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">    /***** 所有 Server 都包含的持久状态 *****/</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">    // CurrentTerm 机器遇到的最大的任期，启动时初始化为 0，单调递增</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    CurrentTerm </span><span style="color:#D73A49;--s-dark:#F97583">int</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">    // VotedFor 当前任期内投票的 Candidate ID，未投票则为 -1</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    VotedFor </span><span style="color:#D73A49;--s-dark:#F97583">int</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">    // Logs 日志条目，每个条目都包含了一条状态机指令和 Leader 接收该条目时的任期，index 从 1 开始</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    Logs []</span><span style="color:#D73A49;--s-dark:#F97583">*</span><span style="color:#6F42C1;--s-dark:#B392F0">LogEntry</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">    /***** 所有 Server 都包含的可变状态 *****/</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">    // CommitIndex 已知的最大的即将提交的日志索引，启动时初始化为 0，单调递增</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    CommitIndex </span><span style="color:#D73A49;--s-dark:#F97583">uint64</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">    // LastApplied 最大的已提交的日志索引，启动时初始化为 0，单调递增</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    LastApplied </span><span style="color:#D73A49;--s-dark:#F97583">uint64</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">    /******* Leader 包含的可变状态，选举后初始化 *******/</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">    // NextIndex 每台机器下一个要发送的日志条目的索引，初始化为 Leader 最后一个日志索引 +1</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    NextIndex []</span><span style="color:#D73A49;--s-dark:#F97583">uint64</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">    // MatchIndex 每台机器已知复制的最高的日志条目，初始化为 0，单调递增</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    MatchIndex []</span><span style="color:#D73A49;--s-dark:#F97583">uint64</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">    // 定时器</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    electionTimer  </span><span style="color:#D73A49;--s-dark:#F97583">*</span><span style="color:#6F42C1;--s-dark:#B392F0">time</span><span style="color:#24292E;--s-dark:#E1E4E8">.</span><span style="color:#6F42C1;--s-dark:#B392F0">Timer</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    heartbeatTimer </span><span style="color:#D73A49;--s-dark:#F97583">*</span><span style="color:#6F42C1;--s-dark:#B392F0">time</span><span style="color:#24292E;--s-dark:#E1E4E8">.</span><span style="color:#6F42C1;--s-dark:#B392F0">Timer</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">    // 处理 rpc 请求的管道</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    requestVoteChan   </span><span style="color:#D73A49;--s-dark:#F97583">chan</span><span style="color:#6F42C1;--s-dark:#B392F0"> RequestVoteMsg</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    appendEntriesChan </span><span style="color:#D73A49;--s-dark:#F97583">chan</span><span style="color:#6F42C1;--s-dark:#B392F0"> AppendEntriesMsg</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">}</span></span></code><label class="code-collapse-collapse" for="toggle-yjt9nke"><svg class="code-collapse-icon" width="32" height="24" viewBox="0 0 32 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20,15 12,7 4,15"></polyline></svg></label></pre></div></div></div>
<p>注意用到的管道和定时器等，都需要在 <code>Make()</code> 函数中初始化，否则 nil 管道会阻塞所有的读写操作，并在函数返回前起一协程作为主协程，监听管道消息：</p>
<div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--s-dark:#F97583">func</span><span style="color:#24292E;--s-dark:#E1E4E8"> (</span><span style="color:#E36209;--s-dark:#FFAB70">rf </span><span style="color:#D73A49;--s-dark:#F97583">*</span><span style="color:#6F42C1;--s-dark:#B392F0">Raft</span><span style="color:#24292E;--s-dark:#E1E4E8">) </span><span style="color:#6F42C1;--s-dark:#B392F0">ticker</span><span style="color:#24292E;--s-dark:#E1E4E8">() {</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">    for</span><span style="color:#D73A49;--s-dark:#F97583"> !</span><span style="color:#24292E;--s-dark:#E1E4E8">rf.</span><span style="color:#6F42C1;--s-dark:#B392F0">killed</span><span style="color:#24292E;--s-dark:#E1E4E8">() {</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">        select</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">        case</span><span style="color:#D73A49;--s-dark:#F97583"> &#x3C;-</span><span style="color:#24292E;--s-dark:#E1E4E8">rf.electionTimer.C:</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            rf.</span><span style="color:#6F42C1;--s-dark:#B392F0">startElection</span><span style="color:#24292E;--s-dark:#E1E4E8">()</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">            resetTimer</span><span style="color:#24292E;--s-dark:#E1E4E8">(rf.electionTimer, </span><span style="color:#6F42C1;--s-dark:#B392F0">RandomizedElectionTimeout</span><span style="color:#24292E;--s-dark:#E1E4E8">())</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">        case</span><span style="color:#D73A49;--s-dark:#F97583"> &#x3C;-</span><span style="color:#24292E;--s-dark:#E1E4E8">rf.heartbeatTimer.C:</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            rf.</span><span style="color:#6F42C1;--s-dark:#B392F0">broadcastHeartbeat</span><span style="color:#24292E;--s-dark:#E1E4E8">()</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">            resetTimer</span><span style="color:#24292E;--s-dark:#E1E4E8">(rf.heartbeatTimer, </span><span style="color:#6F42C1;--s-dark:#B392F0">FixedHeartbeatTimeout</span><span style="color:#24292E;--s-dark:#E1E4E8">())</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">        case</span><span style="color:#24292E;--s-dark:#E1E4E8"> msg </span><span style="color:#D73A49;--s-dark:#F97583">:=</span><span style="color:#D73A49;--s-dark:#F97583"> &#x3C;-</span><span style="color:#24292E;--s-dark:#E1E4E8">rf.requestVoteChan:</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            rf.</span><span style="color:#6F42C1;--s-dark:#B392F0">handleRequestVote</span><span style="color:#24292E;--s-dark:#E1E4E8">(msg)</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">        case</span><span style="color:#24292E;--s-dark:#E1E4E8"> msg </span><span style="color:#D73A49;--s-dark:#F97583">:=</span><span style="color:#D73A49;--s-dark:#F97583"> &#x3C;-</span><span style="color:#24292E;--s-dark:#E1E4E8">rf.appendEntriesChan:</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            rf.</span><span style="color:#6F42C1;--s-dark:#B392F0">handleAppendEntries</span><span style="color:#24292E;--s-dark:#E1E4E8">(msg)</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">        case</span><span style="color:#24292E;--s-dark:#E1E4E8"> msg </span><span style="color:#D73A49;--s-dark:#F97583">:=</span><span style="color:#D73A49;--s-dark:#F97583"> &#x3C;-</span><span style="color:#24292E;--s-dark:#E1E4E8">rf.requestVoteResChan:</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            rf.</span><span style="color:#6F42C1;--s-dark:#B392F0">handleRequestVoteRes</span><span style="color:#24292E;--s-dark:#E1E4E8">(msg)</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">        case</span><span style="color:#24292E;--s-dark:#E1E4E8"> msg </span><span style="color:#D73A49;--s-dark:#F97583">:=</span><span style="color:#D73A49;--s-dark:#F97583"> &#x3C;-</span><span style="color:#24292E;--s-dark:#E1E4E8">rf.appendEntriesResChan:</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            rf.</span><span style="color:#6F42C1;--s-dark:#B392F0">handleAppendEntriesRes</span><span style="color:#24292E;--s-dark:#E1E4E8">(msg)</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        }</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">}</span></span></code></pre></div>
<h4 id="两个定时器">两个定时器<a href="#两个定时器" class="heading-anchor-link" aria-label="Link to 两个定时器"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h4>
<p>electionTimer 是选举超时的定时器，每次需要初始化为一个随机时间，来防止启动时集群中的机器集体选举超时。这里随机时间范围为 300 ~ 450 ms。heartbeatTimer 是心跳超时的定时器，初始化为一个固定时间 100 ms。</p>
<div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--s-dark:#F97583">func</span><span style="color:#6F42C1;--s-dark:#B392F0"> Make</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#E36209;--s-dark:#FFAB70">peers</span><span style="color:#24292E;--s-dark:#E1E4E8"> []</span><span style="color:#D73A49;--s-dark:#F97583">*</span><span style="color:#6F42C1;--s-dark:#B392F0">labrpc</span><span style="color:#24292E;--s-dark:#E1E4E8">.</span><span style="color:#6F42C1;--s-dark:#B392F0">ClientEnd</span><span style="color:#24292E;--s-dark:#E1E4E8">, </span><span style="color:#E36209;--s-dark:#FFAB70">me</span><span style="color:#D73A49;--s-dark:#F97583"> int</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#E36209;--s-dark:#FFAB70">    persister</span><span style="color:#D73A49;--s-dark:#F97583"> *</span><span style="color:#6F42C1;--s-dark:#B392F0">Persister</span><span style="color:#24292E;--s-dark:#E1E4E8">, </span><span style="color:#E36209;--s-dark:#FFAB70">applyCh</span><span style="color:#D73A49;--s-dark:#F97583"> chan</span><span style="color:#6F42C1;--s-dark:#B392F0"> ApplyMsg</span><span style="color:#24292E;--s-dark:#E1E4E8">) </span><span style="color:#D73A49;--s-dark:#F97583">*</span><span style="color:#6F42C1;--s-dark:#B392F0">Raft</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">    ...</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    rf.electionTimer </span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#24292E;--s-dark:#E1E4E8"> time.</span><span style="color:#6F42C1;--s-dark:#B392F0">NewTimer</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#6F42C1;--s-dark:#B392F0">RandomizedElectionTimeout</span><span style="color:#24292E;--s-dark:#E1E4E8">())</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    rf.heartbeatTimer </span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#24292E;--s-dark:#E1E4E8"> time.</span><span style="color:#6F42C1;--s-dark:#B392F0">NewTimer</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#6F42C1;--s-dark:#B392F0">FixedHeartbeatTimeout</span><span style="color:#24292E;--s-dark:#E1E4E8">())</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">    ...</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">func</span><span style="color:#6F42C1;--s-dark:#B392F0"> RandomizedElectionTimeout</span><span style="color:#24292E;--s-dark:#E1E4E8">() </span><span style="color:#6F42C1;--s-dark:#B392F0">time</span><span style="color:#24292E;--s-dark:#E1E4E8">.</span><span style="color:#6F42C1;--s-dark:#B392F0">Duration</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    rand.</span><span style="color:#6F42C1;--s-dark:#B392F0">Seed</span><span style="color:#24292E;--s-dark:#E1E4E8">(time.</span><span style="color:#6F42C1;--s-dark:#B392F0">Now</span><span style="color:#24292E;--s-dark:#E1E4E8">().</span><span style="color:#6F42C1;--s-dark:#B392F0">UnixNano</span><span style="color:#24292E;--s-dark:#E1E4E8">())</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">    return</span><span style="color:#24292E;--s-dark:#E1E4E8"> time.</span><span style="color:#6F42C1;--s-dark:#B392F0">Duration</span><span style="color:#24292E;--s-dark:#E1E4E8">(rand.</span><span style="color:#6F42C1;--s-dark:#B392F0">Intn</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#005CC5;--s-dark:#79B8FF">150</span><span style="color:#24292E;--s-dark:#E1E4E8">)</span><span style="color:#D73A49;--s-dark:#F97583">+</span><span style="color:#005CC5;--s-dark:#79B8FF">300</span><span style="color:#24292E;--s-dark:#E1E4E8">) </span><span style="color:#D73A49;--s-dark:#F97583">*</span><span style="color:#24292E;--s-dark:#E1E4E8"> time.Millisecond</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">func</span><span style="color:#6F42C1;--s-dark:#B392F0"> FixedHeartbeatTimeout</span><span style="color:#24292E;--s-dark:#E1E4E8">() </span><span style="color:#6F42C1;--s-dark:#B392F0">time</span><span style="color:#24292E;--s-dark:#E1E4E8">.</span><span style="color:#6F42C1;--s-dark:#B392F0">Duration</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">    return</span><span style="color:#24292E;--s-dark:#E1E4E8"> time.Millisecond </span><span style="color:#D73A49;--s-dark:#F97583">*</span><span style="color:#005CC5;--s-dark:#79B8FF"> 100</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">}</span></span></code></pre></div>
<p>选举超时定时器主要用于非 Leader，每次收到 Leader 的心跳后，Server 会重置选举定时器，然而在一段时间没有收到 Server 的消息，Server 就会发起选举。发起选举流程如下：</p>
<ol>
<li>当前任期 +1</li>
<li>身份变为 Candidate，同时投票给自己</li>
<li>向所有机器发送拉票请求</li>
</ol>
<p>实现如下：</p>
<div class="code-collapse-wrapper"><input type="checkbox" id="toggle-lldc28p" class="code-collapse-toggle" style="display: none;"><div class="code-collapse-preview"><div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0" class="code-preview"><code><span class="line"><span style="color:#D73A49;--s-dark:#F97583">func</span><span style="color:#24292E;--s-dark:#E1E4E8"> (</span><span style="color:#E36209;--s-dark:#FFAB70">rf </span><span style="color:#D73A49;--s-dark:#F97583">*</span><span style="color:#6F42C1;--s-dark:#B392F0">Raft</span><span style="color:#24292E;--s-dark:#E1E4E8">) </span><span style="color:#6F42C1;--s-dark:#B392F0">startElection</span><span style="color:#24292E;--s-dark:#E1E4E8">() {</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">    if</span><span style="color:#24292E;--s-dark:#E1E4E8"> rf.Status </span><span style="color:#D73A49;--s-dark:#F97583">==</span><span style="color:#24292E;--s-dark:#E1E4E8"> Leader {</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">        // leader 无需发起新选举</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">        return</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    rf.CurrentTerm </span><span style="color:#D73A49;--s-dark:#F97583">+=</span><span style="color:#005CC5;--s-dark:#79B8FF"> 1</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">    // fmt.Printf("server %d start election for term %d\n", rf.me, rf.CurrentTerm)</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    rf.Status </span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#24292E;--s-dark:#E1E4E8"> Candidate</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    rf.VotedFor </span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#24292E;--s-dark:#E1E4E8"> rf.me</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    args </span><span style="color:#D73A49;--s-dark:#F97583">:=</span><span style="color:#6F42C1;--s-dark:#B392F0"> RequestVoteArgs</span><span style="color:#24292E;--s-dark:#E1E4E8">{</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        Term:         rf.CurrentTerm,</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        CandidateId:  rf.me,</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        LastLogIndex: </span><span style="color:#6F42C1;--s-dark:#B392F0">len</span><span style="color:#24292E;--s-dark:#E1E4E8">(rf.Logs) </span><span style="color:#D73A49;--s-dark:#F97583">-</span><span style="color:#005CC5;--s-dark:#79B8FF"> 1</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">    if</span><span style="color:#6F42C1;--s-dark:#B392F0"> len</span><span style="color:#24292E;--s-dark:#E1E4E8">(rf.Logs) </span><span style="color:#D73A49;--s-dark:#F97583">!=</span><span style="color:#005CC5;--s-dark:#79B8FF"> 0</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        args.LastLogTerm </span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#24292E;--s-dark:#E1E4E8"> rf.Logs[</span><span style="color:#6F42C1;--s-dark:#B392F0">len</span><span style="color:#24292E;--s-dark:#E1E4E8">(rf.Logs)</span><span style="color:#D73A49;--s-dark:#F97583">-</span><span style="color:#005CC5;--s-dark:#79B8FF">1</span><span style="color:#24292E;--s-dark:#E1E4E8">].Term</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    meta </span><span style="color:#D73A49;--s-dark:#F97583">:=</span><span style="color:#6F42C1;--s-dark:#B392F0"> ElectionMeta</span><span style="color:#24292E;--s-dark:#E1E4E8">{</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        term: rf.CurrentTerm,</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        yeas: </span><span style="color:#005CC5;--s-dark:#79B8FF">1</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        nays: </span><span style="color:#005CC5;--s-dark:#79B8FF">0</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">    for</span><span style="color:#24292E;--s-dark:#E1E4E8"> peer </span><span style="color:#D73A49;--s-dark:#F97583">:=</span><span style="color:#D73A49;--s-dark:#F97583"> range</span><span style="color:#24292E;--s-dark:#E1E4E8"> rf.peers {</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">        if</span><span style="color:#24292E;--s-dark:#E1E4E8"> peer </span><span style="color:#D73A49;--s-dark:#F97583">==</span><span style="color:#24292E;--s-dark:#E1E4E8"> rf.me {</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">            continue</span></span>
</code></pre></div><div class="code-collapse-gradient"></div><label class="code-collapse-expand" for="toggle-lldc28p"><svg class="code-collapse-icon" width="32" height="24" viewBox="0 0 32 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="4,9 12,17 20,9"></polyline></svg></label></div><div class="code-collapse-full"><div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--s-dark:#F97583">func</span><span style="color:#24292E;--s-dark:#E1E4E8"> (</span><span style="color:#E36209;--s-dark:#FFAB70">rf </span><span style="color:#D73A49;--s-dark:#F97583">*</span><span style="color:#6F42C1;--s-dark:#B392F0">Raft</span><span style="color:#24292E;--s-dark:#E1E4E8">) </span><span style="color:#6F42C1;--s-dark:#B392F0">startElection</span><span style="color:#24292E;--s-dark:#E1E4E8">() {</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">    if</span><span style="color:#24292E;--s-dark:#E1E4E8"> rf.Status </span><span style="color:#D73A49;--s-dark:#F97583">==</span><span style="color:#24292E;--s-dark:#E1E4E8"> Leader {</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">        // leader 无需发起新选举</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">        return</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    rf.CurrentTerm </span><span style="color:#D73A49;--s-dark:#F97583">+=</span><span style="color:#005CC5;--s-dark:#79B8FF"> 1</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">    // fmt.Printf("server %d start election for term %d\n", rf.me, rf.CurrentTerm)</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    rf.Status </span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#24292E;--s-dark:#E1E4E8"> Candidate</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    rf.VotedFor </span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#24292E;--s-dark:#E1E4E8"> rf.me</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    args </span><span style="color:#D73A49;--s-dark:#F97583">:=</span><span style="color:#6F42C1;--s-dark:#B392F0"> RequestVoteArgs</span><span style="color:#24292E;--s-dark:#E1E4E8">{</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        Term:         rf.CurrentTerm,</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        CandidateId:  rf.me,</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        LastLogIndex: </span><span style="color:#6F42C1;--s-dark:#B392F0">len</span><span style="color:#24292E;--s-dark:#E1E4E8">(rf.Logs) </span><span style="color:#D73A49;--s-dark:#F97583">-</span><span style="color:#005CC5;--s-dark:#79B8FF"> 1</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">    if</span><span style="color:#6F42C1;--s-dark:#B392F0"> len</span><span style="color:#24292E;--s-dark:#E1E4E8">(rf.Logs) </span><span style="color:#D73A49;--s-dark:#F97583">!=</span><span style="color:#005CC5;--s-dark:#79B8FF"> 0</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        args.LastLogTerm </span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#24292E;--s-dark:#E1E4E8"> rf.Logs[</span><span style="color:#6F42C1;--s-dark:#B392F0">len</span><span style="color:#24292E;--s-dark:#E1E4E8">(rf.Logs)</span><span style="color:#D73A49;--s-dark:#F97583">-</span><span style="color:#005CC5;--s-dark:#79B8FF">1</span><span style="color:#24292E;--s-dark:#E1E4E8">].Term</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    meta </span><span style="color:#D73A49;--s-dark:#F97583">:=</span><span style="color:#6F42C1;--s-dark:#B392F0"> ElectionMeta</span><span style="color:#24292E;--s-dark:#E1E4E8">{</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        term: rf.CurrentTerm,</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        yeas: </span><span style="color:#005CC5;--s-dark:#79B8FF">1</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        nays: </span><span style="color:#005CC5;--s-dark:#79B8FF">0</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">    for</span><span style="color:#24292E;--s-dark:#E1E4E8"> peer </span><span style="color:#D73A49;--s-dark:#F97583">:=</span><span style="color:#D73A49;--s-dark:#F97583"> range</span><span style="color:#24292E;--s-dark:#E1E4E8"> rf.peers {</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">        if</span><span style="color:#24292E;--s-dark:#E1E4E8"> peer </span><span style="color:#D73A49;--s-dark:#F97583">==</span><span style="color:#24292E;--s-dark:#E1E4E8"> rf.me {</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">            continue</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        }</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">        go</span><span style="color:#24292E;--s-dark:#E1E4E8"> rf.</span><span style="color:#6F42C1;--s-dark:#B392F0">sendRequestVoteRoutine</span><span style="color:#24292E;--s-dark:#E1E4E8">(peer, args, </span><span style="color:#D73A49;--s-dark:#F97583">&#x26;</span><span style="color:#24292E;--s-dark:#E1E4E8">meta)</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">}</span></span></code><label class="code-collapse-collapse" for="toggle-lldc28p"><svg class="code-collapse-icon" width="32" height="24" viewBox="0 0 32 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20,15 12,7 4,15"></polyline></svg></label></pre></div></div></div>
<p>构造了一个 ElectionMeta 来存储一次选举的元信息，包含这次选举的任期、投赞同和反对票的 Server 个数。由于不可能在主协程中等待各个机器投票完毕，便对集群中的每一台机器都开启了一个协程来管理拉票 RPC，这些 RPC 会在获知选举结果后通过管道通知主协程。另外在发起选举后，需要重置选举超时定时器。</p>
<p>心跳超时定时器主要用于 Leader，用来在集群中维系自己的 Leader 身份，每当心跳超时定时器超时，Leader 就会在集群中广播心跳，来保证不会有新的选举发起。同样，广播过后也需要重置心跳超时定时器。</p>
<div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--s-dark:#F97583">func</span><span style="color:#24292E;--s-dark:#E1E4E8"> (</span><span style="color:#E36209;--s-dark:#FFAB70">rf </span><span style="color:#D73A49;--s-dark:#F97583">*</span><span style="color:#6F42C1;--s-dark:#B392F0">Raft</span><span style="color:#24292E;--s-dark:#E1E4E8">) </span><span style="color:#6F42C1;--s-dark:#B392F0">broadcastHeartbeat</span><span style="color:#24292E;--s-dark:#E1E4E8">() {</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">    if</span><span style="color:#24292E;--s-dark:#E1E4E8"> rf.Status </span><span style="color:#D73A49;--s-dark:#F97583">!=</span><span style="color:#24292E;--s-dark:#E1E4E8"> Leader {</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">        return</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">    // fmt.Printf("server %d broadcast heartbeat\n", rf.me)</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    args </span><span style="color:#D73A49;--s-dark:#F97583">:=</span><span style="color:#6F42C1;--s-dark:#B392F0"> AppendEntriesArgs</span><span style="color:#24292E;--s-dark:#E1E4E8">{</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        Term:     rf.CurrentTerm,</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        LeaderID: rf.me,</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">    for</span><span style="color:#24292E;--s-dark:#E1E4E8"> peer </span><span style="color:#D73A49;--s-dark:#F97583">:=</span><span style="color:#D73A49;--s-dark:#F97583"> range</span><span style="color:#24292E;--s-dark:#E1E4E8"> rf.peers {</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">        if</span><span style="color:#24292E;--s-dark:#E1E4E8"> peer </span><span style="color:#D73A49;--s-dark:#F97583">==</span><span style="color:#24292E;--s-dark:#E1E4E8"> rf.me {</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">            continue</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        }</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">        go</span><span style="color:#24292E;--s-dark:#E1E4E8"> rf.</span><span style="color:#6F42C1;--s-dark:#B392F0">sendAppendEntriesRoutine</span><span style="color:#24292E;--s-dark:#E1E4E8">(peer, args)</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">}</span></span></code></pre></div>
<p>心跳 RPC 复用追加 RPC，同样，和集群中其他机器的 RPC 连接在单独的协程中处理。</p>
<h4 id="拉票相关">拉票相关<a href="#拉票相关" class="heading-anchor-link" aria-label="Link to 拉票相关"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h4>
<p>在选举定时器超时后，非 Leader 的机器就会发起新的选举，来尝试选出新的 Leader。在上面 <code>StartElection()</code> 中，已经为每个机器开启了一个协程，用来管理对每个机器的拉票 RPC。发送拉票请求的协程函数如下：</p>
<div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#6A737D;--s-dark:#6A737D">// 发送拉票请求的协程</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">func</span><span style="color:#24292E;--s-dark:#E1E4E8"> (</span><span style="color:#E36209;--s-dark:#FFAB70">rf </span><span style="color:#D73A49;--s-dark:#F97583">*</span><span style="color:#6F42C1;--s-dark:#B392F0">Raft</span><span style="color:#24292E;--s-dark:#E1E4E8">) </span><span style="color:#6F42C1;--s-dark:#B392F0">sendRequestVoteRoutine</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#E36209;--s-dark:#FFAB70">peer</span><span style="color:#D73A49;--s-dark:#F97583"> int</span><span style="color:#24292E;--s-dark:#E1E4E8">, </span><span style="color:#E36209;--s-dark:#FFAB70">args</span><span style="color:#6F42C1;--s-dark:#B392F0"> RequestVoteArgs</span><span style="color:#24292E;--s-dark:#E1E4E8">, </span><span style="color:#E36209;--s-dark:#FFAB70">electionMeta</span><span style="color:#D73A49;--s-dark:#F97583"> *</span><span style="color:#6F42C1;--s-dark:#B392F0">ElectionMeta</span><span style="color:#24292E;--s-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    reply </span><span style="color:#D73A49;--s-dark:#F97583">:=</span><span style="color:#6F42C1;--s-dark:#B392F0"> RequestVoteReply</span><span style="color:#24292E;--s-dark:#E1E4E8">{}</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    ok </span><span style="color:#D73A49;--s-dark:#F97583">:=</span><span style="color:#24292E;--s-dark:#E1E4E8"> rf.</span><span style="color:#6F42C1;--s-dark:#B392F0">sendRequestVote</span><span style="color:#24292E;--s-dark:#E1E4E8">(peer, </span><span style="color:#D73A49;--s-dark:#F97583">&#x26;</span><span style="color:#24292E;--s-dark:#E1E4E8">args, </span><span style="color:#D73A49;--s-dark:#F97583">&#x26;</span><span style="color:#24292E;--s-dark:#E1E4E8">reply)</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">    if</span><span style="color:#D73A49;--s-dark:#F97583"> !</span><span style="color:#24292E;--s-dark:#E1E4E8">ok {</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">        return</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    msg </span><span style="color:#D73A49;--s-dark:#F97583">:=</span><span style="color:#6F42C1;--s-dark:#B392F0"> RequestVoteResMsg</span><span style="color:#24292E;--s-dark:#E1E4E8">{</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        resp: </span><span style="color:#D73A49;--s-dark:#F97583">&#x26;</span><span style="color:#24292E;--s-dark:#E1E4E8">reply,</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        meta: electionMeta,</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    rf.requestVoteResChan </span><span style="color:#D73A49;--s-dark:#F97583">&#x3C;-</span><span style="color:#24292E;--s-dark:#E1E4E8"> msg</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">}</span></span></code></pre></div>
<p>没有特殊的处理，仅仅是发送 RPC 请求，并将请求结果包装后通过管道发送给主协程处理。这里定义 RPC 的请求和响应参数结构：</p>
<div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#6A737D;--s-dark:#6A737D">// 拉票 RPC 请求</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">type</span><span style="color:#6F42C1;--s-dark:#B392F0"> RequestVoteArgs</span><span style="color:#D73A49;--s-dark:#F97583"> struct</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">    // Term Candidate 的任期</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    Term </span><span style="color:#D73A49;--s-dark:#F97583">int</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">    // CandidateId 拉票的 Candidate 的 ID</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    CandidateId </span><span style="color:#D73A49;--s-dark:#F97583">int</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">    // LastLogIndex Candidate 最后一条日志序列的索引</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    LastLogIndex </span><span style="color:#D73A49;--s-dark:#F97583">int</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">    // LastLogTerm Candidate 最后一条日志序列的任期</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    LastLogTerm </span><span style="color:#D73A49;--s-dark:#F97583">int64</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">// 拉票 RPC 响应</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">type</span><span style="color:#6F42C1;--s-dark:#B392F0"> RequestVoteReply</span><span style="color:#D73A49;--s-dark:#F97583"> struct</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">    // Term 当前任期</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    Term </span><span style="color:#D73A49;--s-dark:#F97583">int</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">    // VoteGranted true 则拉票成功</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    VoteGranted </span><span style="color:#D73A49;--s-dark:#F97583">bool</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">// 拉票请求 RPC 发送入口</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">func</span><span style="color:#24292E;--s-dark:#E1E4E8"> (</span><span style="color:#E36209;--s-dark:#FFAB70">rf </span><span style="color:#D73A49;--s-dark:#F97583">*</span><span style="color:#6F42C1;--s-dark:#B392F0">Raft</span><span style="color:#24292E;--s-dark:#E1E4E8">) </span><span style="color:#6F42C1;--s-dark:#B392F0">sendRequestVote</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#E36209;--s-dark:#FFAB70">server</span><span style="color:#D73A49;--s-dark:#F97583"> int</span><span style="color:#24292E;--s-dark:#E1E4E8">, </span><span style="color:#E36209;--s-dark:#FFAB70">args</span><span style="color:#D73A49;--s-dark:#F97583"> *</span><span style="color:#6F42C1;--s-dark:#B392F0">RequestVoteArgs</span><span style="color:#24292E;--s-dark:#E1E4E8">, </span><span style="color:#E36209;--s-dark:#FFAB70">reply</span><span style="color:#D73A49;--s-dark:#F97583"> *</span><span style="color:#6F42C1;--s-dark:#B392F0">RequestVoteReply</span><span style="color:#24292E;--s-dark:#E1E4E8">) </span><span style="color:#D73A49;--s-dark:#F97583">bool</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    ok </span><span style="color:#D73A49;--s-dark:#F97583">:=</span><span style="color:#24292E;--s-dark:#E1E4E8"> rf.peers[server].</span><span style="color:#6F42C1;--s-dark:#B392F0">Call</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#032F62;--s-dark:#9ECBFF">"Raft.RequestVote"</span><span style="color:#24292E;--s-dark:#E1E4E8">, args, reply)</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">    return</span><span style="color:#24292E;--s-dark:#E1E4E8"> ok</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">}</span></span></code></pre></div>
<p>接收端的 RPC 入口是 RequestVote 方法，由于接收 RPC 请求的协程不是主协程，这里仍然需要使用管道将拉票请求传递给主协程处理</p>
<div class="code-collapse-wrapper"><input type="checkbox" id="toggle-tdngkvf" class="code-collapse-toggle" style="display: none;"><div class="code-collapse-preview"><div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0" class="code-preview"><code><span class="line"><span style="color:#6A737D;--s-dark:#6A737D">/********* 拉票请求接收端相关方法 *********/</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">// 拉票请求 RPC 接收入口</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">func</span><span style="color:#24292E;--s-dark:#E1E4E8"> (</span><span style="color:#E36209;--s-dark:#FFAB70">rf </span><span style="color:#D73A49;--s-dark:#F97583">*</span><span style="color:#6F42C1;--s-dark:#B392F0">Raft</span><span style="color:#24292E;--s-dark:#E1E4E8">) </span><span style="color:#6F42C1;--s-dark:#B392F0">RequestVote</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#E36209;--s-dark:#FFAB70">args</span><span style="color:#D73A49;--s-dark:#F97583"> *</span><span style="color:#6F42C1;--s-dark:#B392F0">RequestVoteArgs</span><span style="color:#24292E;--s-dark:#E1E4E8">, </span><span style="color:#E36209;--s-dark:#FFAB70">reply</span><span style="color:#D73A49;--s-dark:#F97583"> *</span><span style="color:#6F42C1;--s-dark:#B392F0">RequestVoteReply</span><span style="color:#24292E;--s-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    msg </span><span style="color:#D73A49;--s-dark:#F97583">:=</span><span style="color:#6F42C1;--s-dark:#B392F0"> RequestVoteMsg</span><span style="color:#24292E;--s-dark:#E1E4E8">{</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        req: args,</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        ok:  </span><span style="color:#6F42C1;--s-dark:#B392F0">make</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#D73A49;--s-dark:#F97583">chan</span><span style="color:#6F42C1;--s-dark:#B392F0"> RequestVoteReply</span><span style="color:#24292E;--s-dark:#E1E4E8">),</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    rf.requestVoteChan </span><span style="color:#D73A49;--s-dark:#F97583">&#x3C;-</span><span style="color:#24292E;--s-dark:#E1E4E8"> msg</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    resp </span><span style="color:#D73A49;--s-dark:#F97583">:=</span><span style="color:#D73A49;--s-dark:#F97583"> &#x3C;-</span><span style="color:#24292E;--s-dark:#E1E4E8">msg.ok</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">    *</span><span style="color:#24292E;--s-dark:#E1E4E8">reply </span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#24292E;--s-dark:#E1E4E8"> resp</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">// 主协程处理拉票请求</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">func</span><span style="color:#24292E;--s-dark:#E1E4E8"> (</span><span style="color:#E36209;--s-dark:#FFAB70">rf </span><span style="color:#D73A49;--s-dark:#F97583">*</span><span style="color:#6F42C1;--s-dark:#B392F0">Raft</span><span style="color:#24292E;--s-dark:#E1E4E8">) </span><span style="color:#6F42C1;--s-dark:#B392F0">handleRequestVote</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#E36209;--s-dark:#FFAB70">msg</span><span style="color:#6F42C1;--s-dark:#B392F0"> RequestVoteMsg</span><span style="color:#24292E;--s-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    req </span><span style="color:#D73A49;--s-dark:#F97583">:=</span><span style="color:#24292E;--s-dark:#E1E4E8"> msg.req</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">    if</span><span style="color:#24292E;--s-dark:#E1E4E8"> req.Term </span><span style="color:#D73A49;--s-dark:#F97583">&#x3C;</span><span style="color:#24292E;--s-dark:#E1E4E8"> rf.CurrentTerm </span><span style="color:#D73A49;--s-dark:#F97583">||</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        (req.Term </span><span style="color:#D73A49;--s-dark:#F97583">==</span><span style="color:#24292E;--s-dark:#E1E4E8"> rf.CurrentTerm </span><span style="color:#D73A49;--s-dark:#F97583">&#x26;&#x26;</span><span style="color:#24292E;--s-dark:#E1E4E8"> rf.VotedFor </span><span style="color:#D73A49;--s-dark:#F97583">!=</span><span style="color:#D73A49;--s-dark:#F97583"> -</span><span style="color:#005CC5;--s-dark:#79B8FF">1</span><span style="color:#D73A49;--s-dark:#F97583"> &#x26;&#x26;</span><span style="color:#24292E;--s-dark:#E1E4E8"> rf.VotedFor </span><span style="color:#D73A49;--s-dark:#F97583">!=</span><span style="color:#24292E;--s-dark:#E1E4E8"> req.CandidateId) {</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        msg.ok </span><span style="color:#D73A49;--s-dark:#F97583">&#x3C;-</span><span style="color:#6F42C1;--s-dark:#B392F0"> RequestVoteReply</span><span style="color:#24292E;--s-dark:#E1E4E8">{</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            Term:        rf.CurrentTerm,</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            VoteGranted: </span><span style="color:#005CC5;--s-dark:#79B8FF">false</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        }</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">        return</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    rf.</span><span style="color:#6F42C1;--s-dark:#B392F0">rpcTermCheck</span><span style="color:#24292E;--s-dark:#E1E4E8">(req.Term)</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    rf.VotedFor </span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#24292E;--s-dark:#E1E4E8"> req.CandidateId</span></span>
</code></pre></div><div class="code-collapse-gradient"></div><label class="code-collapse-expand" for="toggle-tdngkvf"><svg class="code-collapse-icon" width="32" height="24" viewBox="0 0 32 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="4,9 12,17 20,9"></polyline></svg></label></div><div class="code-collapse-full"><div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#6A737D;--s-dark:#6A737D">/********* 拉票请求接收端相关方法 *********/</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">// 拉票请求 RPC 接收入口</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">func</span><span style="color:#24292E;--s-dark:#E1E4E8"> (</span><span style="color:#E36209;--s-dark:#FFAB70">rf </span><span style="color:#D73A49;--s-dark:#F97583">*</span><span style="color:#6F42C1;--s-dark:#B392F0">Raft</span><span style="color:#24292E;--s-dark:#E1E4E8">) </span><span style="color:#6F42C1;--s-dark:#B392F0">RequestVote</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#E36209;--s-dark:#FFAB70">args</span><span style="color:#D73A49;--s-dark:#F97583"> *</span><span style="color:#6F42C1;--s-dark:#B392F0">RequestVoteArgs</span><span style="color:#24292E;--s-dark:#E1E4E8">, </span><span style="color:#E36209;--s-dark:#FFAB70">reply</span><span style="color:#D73A49;--s-dark:#F97583"> *</span><span style="color:#6F42C1;--s-dark:#B392F0">RequestVoteReply</span><span style="color:#24292E;--s-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    msg </span><span style="color:#D73A49;--s-dark:#F97583">:=</span><span style="color:#6F42C1;--s-dark:#B392F0"> RequestVoteMsg</span><span style="color:#24292E;--s-dark:#E1E4E8">{</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        req: args,</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        ok:  </span><span style="color:#6F42C1;--s-dark:#B392F0">make</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#D73A49;--s-dark:#F97583">chan</span><span style="color:#6F42C1;--s-dark:#B392F0"> RequestVoteReply</span><span style="color:#24292E;--s-dark:#E1E4E8">),</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    rf.requestVoteChan </span><span style="color:#D73A49;--s-dark:#F97583">&#x3C;-</span><span style="color:#24292E;--s-dark:#E1E4E8"> msg</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    resp </span><span style="color:#D73A49;--s-dark:#F97583">:=</span><span style="color:#D73A49;--s-dark:#F97583"> &#x3C;-</span><span style="color:#24292E;--s-dark:#E1E4E8">msg.ok</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">    *</span><span style="color:#24292E;--s-dark:#E1E4E8">reply </span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#24292E;--s-dark:#E1E4E8"> resp</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">// 主协程处理拉票请求</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">func</span><span style="color:#24292E;--s-dark:#E1E4E8"> (</span><span style="color:#E36209;--s-dark:#FFAB70">rf </span><span style="color:#D73A49;--s-dark:#F97583">*</span><span style="color:#6F42C1;--s-dark:#B392F0">Raft</span><span style="color:#24292E;--s-dark:#E1E4E8">) </span><span style="color:#6F42C1;--s-dark:#B392F0">handleRequestVote</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#E36209;--s-dark:#FFAB70">msg</span><span style="color:#6F42C1;--s-dark:#B392F0"> RequestVoteMsg</span><span style="color:#24292E;--s-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    req </span><span style="color:#D73A49;--s-dark:#F97583">:=</span><span style="color:#24292E;--s-dark:#E1E4E8"> msg.req</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">    if</span><span style="color:#24292E;--s-dark:#E1E4E8"> req.Term </span><span style="color:#D73A49;--s-dark:#F97583">&#x3C;</span><span style="color:#24292E;--s-dark:#E1E4E8"> rf.CurrentTerm </span><span style="color:#D73A49;--s-dark:#F97583">||</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        (req.Term </span><span style="color:#D73A49;--s-dark:#F97583">==</span><span style="color:#24292E;--s-dark:#E1E4E8"> rf.CurrentTerm </span><span style="color:#D73A49;--s-dark:#F97583">&#x26;&#x26;</span><span style="color:#24292E;--s-dark:#E1E4E8"> rf.VotedFor </span><span style="color:#D73A49;--s-dark:#F97583">!=</span><span style="color:#D73A49;--s-dark:#F97583"> -</span><span style="color:#005CC5;--s-dark:#79B8FF">1</span><span style="color:#D73A49;--s-dark:#F97583"> &#x26;&#x26;</span><span style="color:#24292E;--s-dark:#E1E4E8"> rf.VotedFor </span><span style="color:#D73A49;--s-dark:#F97583">!=</span><span style="color:#24292E;--s-dark:#E1E4E8"> req.CandidateId) {</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        msg.ok </span><span style="color:#D73A49;--s-dark:#F97583">&#x3C;-</span><span style="color:#6F42C1;--s-dark:#B392F0"> RequestVoteReply</span><span style="color:#24292E;--s-dark:#E1E4E8">{</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            Term:        rf.CurrentTerm,</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            VoteGranted: </span><span style="color:#005CC5;--s-dark:#79B8FF">false</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        }</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">        return</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    rf.</span><span style="color:#6F42C1;--s-dark:#B392F0">rpcTermCheck</span><span style="color:#24292E;--s-dark:#E1E4E8">(req.Term)</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    rf.VotedFor </span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#24292E;--s-dark:#E1E4E8"> req.CandidateId</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">    resetTimer</span><span style="color:#24292E;--s-dark:#E1E4E8">(rf.electionTimer, </span><span style="color:#6F42C1;--s-dark:#B392F0">RandomizedElectionTimeout</span><span style="color:#24292E;--s-dark:#E1E4E8">())</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">    // fmt.Printf("server %d vote for server %d for term %d\n", rf.me, msg.req.CandidateId, req.Term)</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    msg.ok </span><span style="color:#D73A49;--s-dark:#F97583">&#x3C;-</span><span style="color:#6F42C1;--s-dark:#B392F0"> RequestVoteReply</span><span style="color:#24292E;--s-dark:#E1E4E8">{</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        Term:        rf.CurrentTerm,</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        VoteGranted: </span><span style="color:#005CC5;--s-dark:#79B8FF">true</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">}</span></span></code><label class="code-collapse-collapse" for="toggle-tdngkvf"><svg class="code-collapse-icon" width="32" height="24" viewBox="0 0 32 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20,15 12,7 4,15"></polyline></svg></label></pre></div></div></div>
<p>如果 Server 投了赞成票，还需要重置选举超时定时器。<code>rpcTermCheck()</code> 是一个通用的，用于检查 rpc 请求或响应中的任期是否大于自身的任期，如果大于自身任期则需要更新任期并成为 Follower：</p>
<div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#6A737D;--s-dark:#6A737D">// 检查 rpc 请求响应中的 term，如果大于自己的则需要更新任期并成为 Follower</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">func</span><span style="color:#24292E;--s-dark:#E1E4E8"> (</span><span style="color:#E36209;--s-dark:#FFAB70">rf </span><span style="color:#D73A49;--s-dark:#F97583">*</span><span style="color:#6F42C1;--s-dark:#B392F0">Raft</span><span style="color:#24292E;--s-dark:#E1E4E8">) </span><span style="color:#6F42C1;--s-dark:#B392F0">rpcTermCheck</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#E36209;--s-dark:#FFAB70">msgTerm</span><span style="color:#D73A49;--s-dark:#F97583"> int</span><span style="color:#24292E;--s-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">    if</span><span style="color:#24292E;--s-dark:#E1E4E8"> rf.CurrentTerm </span><span style="color:#D73A49;--s-dark:#F97583">&#x3C;</span><span style="color:#24292E;--s-dark:#E1E4E8"> msgTerm {</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        rf.CurrentTerm </span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#24292E;--s-dark:#E1E4E8"> msgTerm</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        rf.Status </span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#24292E;--s-dark:#E1E4E8"> Follower</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        rf.VotedFor </span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#D73A49;--s-dark:#F97583"> -</span><span style="color:#005CC5;--s-dark:#79B8FF">1</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">}</span></span></code></pre></div>
<p>在发起投票的协程获得了返回的投票结果后，将投票结果提交给主协程处理，在主协程中进行一些计票等判断。主协程处理投票结果如下</p>
<div class="code-collapse-wrapper"><input type="checkbox" id="toggle-o61688b" class="code-collapse-toggle" style="display: none;"><div class="code-collapse-preview"><div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0" class="code-preview"><code><span class="line"><span style="color:#6A737D;--s-dark:#6A737D">// 主协程处理拉票请求返回结果</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">func</span><span style="color:#24292E;--s-dark:#E1E4E8"> (</span><span style="color:#E36209;--s-dark:#FFAB70">rf </span><span style="color:#D73A49;--s-dark:#F97583">*</span><span style="color:#6F42C1;--s-dark:#B392F0">Raft</span><span style="color:#24292E;--s-dark:#E1E4E8">) </span><span style="color:#6F42C1;--s-dark:#B392F0">handleRequestVoteRes</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#E36209;--s-dark:#FFAB70">msg</span><span style="color:#6F42C1;--s-dark:#B392F0"> RequestVoteResMsg</span><span style="color:#24292E;--s-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    meta </span><span style="color:#D73A49;--s-dark:#F97583">:=</span><span style="color:#24292E;--s-dark:#E1E4E8"> msg.meta</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">    if</span><span style="color:#24292E;--s-dark:#E1E4E8"> rf.Status </span><span style="color:#D73A49;--s-dark:#F97583">!=</span><span style="color:#24292E;--s-dark:#E1E4E8"> Candidate {</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">        return</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">    if</span><span style="color:#24292E;--s-dark:#E1E4E8"> rf.CurrentTerm </span><span style="color:#D73A49;--s-dark:#F97583">!=</span><span style="color:#24292E;--s-dark:#E1E4E8"> meta.term {</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">        return</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">    if</span><span style="color:#24292E;--s-dark:#E1E4E8"> msg.resp.VoteGranted {</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        meta.yeas</span><span style="color:#D73A49;--s-dark:#F97583">++</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">        if</span><span style="color:#24292E;--s-dark:#E1E4E8"> meta.yeas </span><span style="color:#D73A49;--s-dark:#F97583">></span><span style="color:#6F42C1;--s-dark:#B392F0"> len</span><span style="color:#24292E;--s-dark:#E1E4E8">(rf.peers)</span><span style="color:#D73A49;--s-dark:#F97583">/</span><span style="color:#005CC5;--s-dark:#79B8FF">2</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">            // fmt.Printf("server %d become leader for term %d\n", rf.me, rf.CurrentTerm)</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            rf.Status </span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#24292E;--s-dark:#E1E4E8"> Leader</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">            resetTimer</span><span style="color:#24292E;--s-dark:#E1E4E8">(rf.heartbeatTimer, </span><span style="color:#6F42C1;--s-dark:#B392F0">FixedHeartbeatTimeout</span><span style="color:#24292E;--s-dark:#E1E4E8">())</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            rf.</span><span style="color:#6F42C1;--s-dark:#B392F0">broadcastHeartbeat</span><span style="color:#24292E;--s-dark:#E1E4E8">()</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        }</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    } </span><span style="color:#D73A49;--s-dark:#F97583">else</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        meta.nays</span><span style="color:#D73A49;--s-dark:#F97583">++</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        rf.</span><span style="color:#6F42C1;--s-dark:#B392F0">rpcTermCheck</span><span style="color:#24292E;--s-dark:#E1E4E8">(msg.resp.Term)</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">        if</span><span style="color:#24292E;--s-dark:#E1E4E8"> meta.nays </span><span style="color:#D73A49;--s-dark:#F97583">></span><span style="color:#6F42C1;--s-dark:#B392F0"> len</span><span style="color:#24292E;--s-dark:#E1E4E8">(rf.peers)</span><span style="color:#D73A49;--s-dark:#F97583">/</span><span style="color:#005CC5;--s-dark:#79B8FF">2</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">            // 反对票超过一半，则该任期选举失败；可以给其他机器投票</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            rf.VotedFor </span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#D73A49;--s-dark:#F97583"> -</span><span style="color:#005CC5;--s-dark:#79B8FF">1</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        }</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    }</span></span>
</code></pre></div><div class="code-collapse-gradient"></div><label class="code-collapse-expand" for="toggle-o61688b"><svg class="code-collapse-icon" width="32" height="24" viewBox="0 0 32 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="4,9 12,17 20,9"></polyline></svg></label></div><div class="code-collapse-full"><div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#6A737D;--s-dark:#6A737D">// 主协程处理拉票请求返回结果</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">func</span><span style="color:#24292E;--s-dark:#E1E4E8"> (</span><span style="color:#E36209;--s-dark:#FFAB70">rf </span><span style="color:#D73A49;--s-dark:#F97583">*</span><span style="color:#6F42C1;--s-dark:#B392F0">Raft</span><span style="color:#24292E;--s-dark:#E1E4E8">) </span><span style="color:#6F42C1;--s-dark:#B392F0">handleRequestVoteRes</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#E36209;--s-dark:#FFAB70">msg</span><span style="color:#6F42C1;--s-dark:#B392F0"> RequestVoteResMsg</span><span style="color:#24292E;--s-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    meta </span><span style="color:#D73A49;--s-dark:#F97583">:=</span><span style="color:#24292E;--s-dark:#E1E4E8"> msg.meta</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">    if</span><span style="color:#24292E;--s-dark:#E1E4E8"> rf.Status </span><span style="color:#D73A49;--s-dark:#F97583">!=</span><span style="color:#24292E;--s-dark:#E1E4E8"> Candidate {</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">        return</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">    if</span><span style="color:#24292E;--s-dark:#E1E4E8"> rf.CurrentTerm </span><span style="color:#D73A49;--s-dark:#F97583">!=</span><span style="color:#24292E;--s-dark:#E1E4E8"> meta.term {</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">        return</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">    if</span><span style="color:#24292E;--s-dark:#E1E4E8"> msg.resp.VoteGranted {</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        meta.yeas</span><span style="color:#D73A49;--s-dark:#F97583">++</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">        if</span><span style="color:#24292E;--s-dark:#E1E4E8"> meta.yeas </span><span style="color:#D73A49;--s-dark:#F97583">></span><span style="color:#6F42C1;--s-dark:#B392F0"> len</span><span style="color:#24292E;--s-dark:#E1E4E8">(rf.peers)</span><span style="color:#D73A49;--s-dark:#F97583">/</span><span style="color:#005CC5;--s-dark:#79B8FF">2</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">            // fmt.Printf("server %d become leader for term %d\n", rf.me, rf.CurrentTerm)</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            rf.Status </span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#24292E;--s-dark:#E1E4E8"> Leader</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">            resetTimer</span><span style="color:#24292E;--s-dark:#E1E4E8">(rf.heartbeatTimer, </span><span style="color:#6F42C1;--s-dark:#B392F0">FixedHeartbeatTimeout</span><span style="color:#24292E;--s-dark:#E1E4E8">())</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            rf.</span><span style="color:#6F42C1;--s-dark:#B392F0">broadcastHeartbeat</span><span style="color:#24292E;--s-dark:#E1E4E8">()</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        }</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    } </span><span style="color:#D73A49;--s-dark:#F97583">else</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        meta.nays</span><span style="color:#D73A49;--s-dark:#F97583">++</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        rf.</span><span style="color:#6F42C1;--s-dark:#B392F0">rpcTermCheck</span><span style="color:#24292E;--s-dark:#E1E4E8">(msg.resp.Term)</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">        if</span><span style="color:#24292E;--s-dark:#E1E4E8"> meta.nays </span><span style="color:#D73A49;--s-dark:#F97583">></span><span style="color:#6F42C1;--s-dark:#B392F0"> len</span><span style="color:#24292E;--s-dark:#E1E4E8">(rf.peers)</span><span style="color:#D73A49;--s-dark:#F97583">/</span><span style="color:#005CC5;--s-dark:#79B8FF">2</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">            // 反对票超过一半，则该任期选举失败；可以给其他机器投票</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            rf.VotedFor </span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#D73A49;--s-dark:#F97583"> -</span><span style="color:#005CC5;--s-dark:#79B8FF">1</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        }</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">}</span></span></code><label class="code-collapse-collapse" for="toggle-o61688b"><svg class="code-collapse-icon" width="32" height="24" viewBox="0 0 32 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20,15 12,7 4,15"></polyline></svg></label></pre></div></div></div>
<p>前置两个校验，如果当前 Server 的身份不是 Candidate，或者 Server 的任期和投票的任期不一致，就说明是一场过期的投票，无需处理，直接返回即可。</p>
<p>如果投的是赞成票，那么就计算一下当前赞成票数是否已大于一半，如果已经大于一半，说明选举成功，发起选举的 Server 转变为 Leader，并重置心跳定时器，向所有机器广播心跳来声明自己的 Leader 身份。</p>
<p>如果投的是反对票，这里算是个小优化。可以直接校验反对票是否已经超过一半，如果超过一半，那么可以认为该次发起的选举已经失败。可以将本任期内的投票置为 -1，以给其他潜在的 Candidate 投票来加速 Leader 选举。</p>
<h4 id="追加相关">追加相关<a href="#追加相关" class="heading-anchor-link" aria-label="Link to 追加相关"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h4>
<p>追加相关的内容，本次实验只需要实现心跳的处理即可。更具体的追加在实验 2b 中。</p>
<p>在 <code>broadcastHeartbeat()</code> 函数中，对集群中的全部机器广播心跳，使用的就是追加请求。对于每个机器都有一协程来管理与之通信的追加请求。追加协程的函数如下</p>
<div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#6A737D;--s-dark:#6A737D">// 发送追加请求的协程</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">func</span><span style="color:#24292E;--s-dark:#E1E4E8"> (</span><span style="color:#E36209;--s-dark:#FFAB70">rf </span><span style="color:#D73A49;--s-dark:#F97583">*</span><span style="color:#6F42C1;--s-dark:#B392F0">Raft</span><span style="color:#24292E;--s-dark:#E1E4E8">) </span><span style="color:#6F42C1;--s-dark:#B392F0">sendAppendEntriesRoutine</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#E36209;--s-dark:#FFAB70">peer</span><span style="color:#D73A49;--s-dark:#F97583"> int</span><span style="color:#24292E;--s-dark:#E1E4E8">, </span><span style="color:#E36209;--s-dark:#FFAB70">args</span><span style="color:#6F42C1;--s-dark:#B392F0"> AppendEntriesArgs</span><span style="color:#24292E;--s-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    reply </span><span style="color:#D73A49;--s-dark:#F97583">:=</span><span style="color:#6F42C1;--s-dark:#B392F0"> AppendEntriesReply</span><span style="color:#24292E;--s-dark:#E1E4E8">{}</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    ok </span><span style="color:#D73A49;--s-dark:#F97583">:=</span><span style="color:#24292E;--s-dark:#E1E4E8"> rf.</span><span style="color:#6F42C1;--s-dark:#B392F0">sendAppendEntries</span><span style="color:#24292E;--s-dark:#E1E4E8">(peer, </span><span style="color:#D73A49;--s-dark:#F97583">&#x26;</span><span style="color:#24292E;--s-dark:#E1E4E8">args, </span><span style="color:#D73A49;--s-dark:#F97583">&#x26;</span><span style="color:#24292E;--s-dark:#E1E4E8">reply)</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">    if</span><span style="color:#D73A49;--s-dark:#F97583"> !</span><span style="color:#24292E;--s-dark:#E1E4E8">ok {</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">        return</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    rf.appendEntriesResChan </span><span style="color:#D73A49;--s-dark:#F97583">&#x3C;-</span><span style="color:#6F42C1;--s-dark:#B392F0"> AppendEntriesResMsg</span><span style="color:#24292E;--s-dark:#E1E4E8">{</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        resp: </span><span style="color:#D73A49;--s-dark:#F97583">&#x26;</span><span style="color:#24292E;--s-dark:#E1E4E8">reply,</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">}</span></span></code></pre></div>
<p>与上面的发起选举类似，也是直接发送一个 RPC 请求，并等待响应，将响应通过管道交由主协程处理。追加 RPC 的请求与响应定义如下</p>
<div class="code-collapse-wrapper"><input type="checkbox" id="toggle-kmqja7m" class="code-collapse-toggle" style="display: none;"><div class="code-collapse-preview"><div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0" class="code-preview"><code><span class="line"><span style="color:#6A737D;--s-dark:#6A737D">// 追加 RPC 请求</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">type</span><span style="color:#6F42C1;--s-dark:#B392F0"> AppendEntriesArgs</span><span style="color:#D73A49;--s-dark:#F97583"> struct</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">    // Term Leader 的任期</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    Term </span><span style="color:#D73A49;--s-dark:#F97583">int</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">    // LeaderID Follower 可以将客户端请求重定向到 Leader</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    LeaderID </span><span style="color:#D73A49;--s-dark:#F97583">int</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">    // PrevLogIndex 新日志条目前一个日志条目的日志索引</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    PrevLogIndex </span><span style="color:#D73A49;--s-dark:#F97583">int</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">    // PrevLogTerm 前一个日志条目的任期</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    PrevLogTerm </span><span style="color:#D73A49;--s-dark:#F97583">int</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">    // Entries 需要保存的日志条目，心跳包为空</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    Entries []</span><span style="color:#D73A49;--s-dark:#F97583">*</span><span style="color:#6F42C1;--s-dark:#B392F0">LogEntry</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">    // LeaderCommit Leader 的 CommitIndex</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    LeaderCommit </span><span style="color:#D73A49;--s-dark:#F97583">int</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">// 追加 RPC 响应</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">type</span><span style="color:#6F42C1;--s-dark:#B392F0"> AppendEntriesReply</span><span style="color:#D73A49;--s-dark:#F97583"> struct</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">    // Term Follower 当前任期</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    Term </span><span style="color:#D73A49;--s-dark:#F97583">int</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">    // Success Follower 包含 PrevLogIndex 和 PrevLogTerm 的日志条目为 true</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    Success </span><span style="color:#D73A49;--s-dark:#F97583">bool</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">// 追加请求 RPC 发送入口</span></span>
</code></pre></div><div class="code-collapse-gradient"></div><label class="code-collapse-expand" for="toggle-kmqja7m"><svg class="code-collapse-icon" width="32" height="24" viewBox="0 0 32 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="4,9 12,17 20,9"></polyline></svg></label></div><div class="code-collapse-full"><div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#6A737D;--s-dark:#6A737D">// 追加 RPC 请求</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">type</span><span style="color:#6F42C1;--s-dark:#B392F0"> AppendEntriesArgs</span><span style="color:#D73A49;--s-dark:#F97583"> struct</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">    // Term Leader 的任期</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    Term </span><span style="color:#D73A49;--s-dark:#F97583">int</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">    // LeaderID Follower 可以将客户端请求重定向到 Leader</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    LeaderID </span><span style="color:#D73A49;--s-dark:#F97583">int</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">    // PrevLogIndex 新日志条目前一个日志条目的日志索引</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    PrevLogIndex </span><span style="color:#D73A49;--s-dark:#F97583">int</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">    // PrevLogTerm 前一个日志条目的任期</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    PrevLogTerm </span><span style="color:#D73A49;--s-dark:#F97583">int</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">    // Entries 需要保存的日志条目，心跳包为空</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    Entries []</span><span style="color:#D73A49;--s-dark:#F97583">*</span><span style="color:#6F42C1;--s-dark:#B392F0">LogEntry</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">    // LeaderCommit Leader 的 CommitIndex</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    LeaderCommit </span><span style="color:#D73A49;--s-dark:#F97583">int</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">// 追加 RPC 响应</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">type</span><span style="color:#6F42C1;--s-dark:#B392F0"> AppendEntriesReply</span><span style="color:#D73A49;--s-dark:#F97583"> struct</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">    // Term Follower 当前任期</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    Term </span><span style="color:#D73A49;--s-dark:#F97583">int</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">    // Success Follower 包含 PrevLogIndex 和 PrevLogTerm 的日志条目为 true</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    Success </span><span style="color:#D73A49;--s-dark:#F97583">bool</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">// 追加请求 RPC 发送入口</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">func</span><span style="color:#24292E;--s-dark:#E1E4E8"> (</span><span style="color:#E36209;--s-dark:#FFAB70">rf </span><span style="color:#D73A49;--s-dark:#F97583">*</span><span style="color:#6F42C1;--s-dark:#B392F0">Raft</span><span style="color:#24292E;--s-dark:#E1E4E8">) </span><span style="color:#6F42C1;--s-dark:#B392F0">sendAppendEntries</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#E36209;--s-dark:#FFAB70">server</span><span style="color:#D73A49;--s-dark:#F97583"> int</span><span style="color:#24292E;--s-dark:#E1E4E8">, </span><span style="color:#E36209;--s-dark:#FFAB70">args</span><span style="color:#D73A49;--s-dark:#F97583"> *</span><span style="color:#6F42C1;--s-dark:#B392F0">AppendEntriesArgs</span><span style="color:#24292E;--s-dark:#E1E4E8">, </span><span style="color:#E36209;--s-dark:#FFAB70">reply</span><span style="color:#D73A49;--s-dark:#F97583"> *</span><span style="color:#6F42C1;--s-dark:#B392F0">AppendEntriesReply</span><span style="color:#24292E;--s-dark:#E1E4E8">) </span><span style="color:#D73A49;--s-dark:#F97583">bool</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    ok </span><span style="color:#D73A49;--s-dark:#F97583">:=</span><span style="color:#24292E;--s-dark:#E1E4E8"> rf.peers[server].</span><span style="color:#6F42C1;--s-dark:#B392F0">Call</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#032F62;--s-dark:#9ECBFF">"Raft.AppendEntries"</span><span style="color:#24292E;--s-dark:#E1E4E8">, args, reply)</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">    return</span><span style="color:#24292E;--s-dark:#E1E4E8"> ok</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">}</span></span></code><label class="code-collapse-collapse" for="toggle-kmqja7m"><svg class="code-collapse-icon" width="32" height="24" viewBox="0 0 32 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20,15 12,7 4,15"></polyline></svg></label></pre></div></div></div>
<p>接收方在收到追加 RPC 请求后，将请求交由主协程处理，当前主协程也只需要处理心跳场景，即转变为 Follower，重置选举超时定时器，并判断是否需要更新任期：</p>
<div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#6A737D;--s-dark:#6A737D">/********* 追加请求接收端相关方法 *********/</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">// 追加请求 RPC 接收入口</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">func</span><span style="color:#24292E;--s-dark:#E1E4E8"> (</span><span style="color:#E36209;--s-dark:#FFAB70">rf </span><span style="color:#D73A49;--s-dark:#F97583">*</span><span style="color:#6F42C1;--s-dark:#B392F0">Raft</span><span style="color:#24292E;--s-dark:#E1E4E8">) </span><span style="color:#6F42C1;--s-dark:#B392F0">AppendEntries</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#E36209;--s-dark:#FFAB70">args</span><span style="color:#D73A49;--s-dark:#F97583"> *</span><span style="color:#6F42C1;--s-dark:#B392F0">AppendEntriesArgs</span><span style="color:#24292E;--s-dark:#E1E4E8">, </span><span style="color:#E36209;--s-dark:#FFAB70">reply</span><span style="color:#D73A49;--s-dark:#F97583"> *</span><span style="color:#6F42C1;--s-dark:#B392F0">AppendEntriesReply</span><span style="color:#24292E;--s-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    msg </span><span style="color:#D73A49;--s-dark:#F97583">:=</span><span style="color:#6F42C1;--s-dark:#B392F0"> AppendEntriesMsg</span><span style="color:#24292E;--s-dark:#E1E4E8">{</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        req: args,</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        ok:  </span><span style="color:#6F42C1;--s-dark:#B392F0">make</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#D73A49;--s-dark:#F97583">chan</span><span style="color:#6F42C1;--s-dark:#B392F0"> AppendEntriesReply</span><span style="color:#24292E;--s-dark:#E1E4E8">),</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    rf.appendEntriesChan </span><span style="color:#D73A49;--s-dark:#F97583">&#x3C;-</span><span style="color:#24292E;--s-dark:#E1E4E8"> msg</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    resp </span><span style="color:#D73A49;--s-dark:#F97583">:=</span><span style="color:#D73A49;--s-dark:#F97583"> &#x3C;-</span><span style="color:#24292E;--s-dark:#E1E4E8">msg.ok</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">    *</span><span style="color:#24292E;--s-dark:#E1E4E8">reply </span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#24292E;--s-dark:#E1E4E8"> resp</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">// 主协程处理追加请求</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">func</span><span style="color:#24292E;--s-dark:#E1E4E8"> (</span><span style="color:#E36209;--s-dark:#FFAB70">rf </span><span style="color:#D73A49;--s-dark:#F97583">*</span><span style="color:#6F42C1;--s-dark:#B392F0">Raft</span><span style="color:#24292E;--s-dark:#E1E4E8">) </span><span style="color:#6F42C1;--s-dark:#B392F0">handleAppendEntries</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#E36209;--s-dark:#FFAB70">msg</span><span style="color:#6F42C1;--s-dark:#B392F0"> AppendEntriesMsg</span><span style="color:#24292E;--s-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    rf.Status </span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#24292E;--s-dark:#E1E4E8"> Follower</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">    resetTimer</span><span style="color:#24292E;--s-dark:#E1E4E8">(rf.electionTimer, </span><span style="color:#6F42C1;--s-dark:#B392F0">RandomizedElectionTimeout</span><span style="color:#24292E;--s-dark:#E1E4E8">())</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    rf.</span><span style="color:#6F42C1;--s-dark:#B392F0">rpcTermCheck</span><span style="color:#24292E;--s-dark:#E1E4E8">(msg.req.Term)</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    msg.ok </span><span style="color:#D73A49;--s-dark:#F97583">&#x3C;-</span><span style="color:#6F42C1;--s-dark:#B392F0"> AppendEntriesReply</span><span style="color:#24292E;--s-dark:#E1E4E8">{</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        Term: rf.CurrentTerm,</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">}</span></span></code></pre></div>
<p>最后发送端处理响应，只需要检查任期即可，仍可复用 <code>rpcTermCheck()</code>：</p>
<div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#6A737D;--s-dark:#6A737D">// 主协程处理追加请求返回结果</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">func</span><span style="color:#24292E;--s-dark:#E1E4E8"> (</span><span style="color:#E36209;--s-dark:#FFAB70">rf </span><span style="color:#D73A49;--s-dark:#F97583">*</span><span style="color:#6F42C1;--s-dark:#B392F0">Raft</span><span style="color:#24292E;--s-dark:#E1E4E8">) </span><span style="color:#6F42C1;--s-dark:#B392F0">handleAppendEntriesRes</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#E36209;--s-dark:#FFAB70">msg</span><span style="color:#6F42C1;--s-dark:#B392F0"> AppendEntriesResMsg</span><span style="color:#24292E;--s-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    resp </span><span style="color:#D73A49;--s-dark:#F97583">:=</span><span style="color:#24292E;--s-dark:#E1E4E8"> msg.resp</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    rf.</span><span style="color:#6F42C1;--s-dark:#B392F0">rpcTermCheck</span><span style="color:#24292E;--s-dark:#E1E4E8">(resp.Term)</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">}</span></span></code></pre></div>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Raft 论文阅读]]></title>
            <link>https://blog.shinya.click/notes/65840/reftextendedpaper</link>
            <guid isPermaLink="false">https://blog.shinya.click/notes/65840/reftextendedpaper</guid>
            <pubDate>Sat, 03 Dec 2022 13:40:09 GMT</pubDate>
            <description><![CDATA[Raft 是一种旨在提高日志复制效率的共识算法，特别适用于多机集群环境，确保在部分机器故障时仍能提供服务。这种算法通过复制状态机模型实现，利用日志记录指令顺序，使得集群各机器能够达到一致的状态。论文《In Search of an Understandable Consensus Algorithm》深入探讨了 Raft 的设计理念和与 Paxos 的对比，突显了其易于理解的特点，为构建可靠的大规模软件系统提供了基础。本文记录了对该论文的阅读笔记，旨在帮助理解其核心概念与应用。]]></description>
            <content:encoded><![CDATA[<h3 id="前言">前言<a href="#前言" class="heading-anchor-link" aria-label="Link to 前言"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h3>
<p>Raft 是用于管理日志复制的共识算法。共识算法适用于包含多台机器的集群中，以保证在其中有些机器挂掉时仍然能正常对外提供服务。由此，共识算法在构建可靠的大规模软件系统中十分重要。</p>
<p>关于 Raft 算法主要的论文就是《In Search of an Understandable Consensus Algorithm (Extended Version)》，可以在 <a href="https://raft.github.io/raft.pdf" target="_blank" rel="noopener noreferrer" data-umami-event="outbound-link-click" data-umami-event-url="https://raft.github.io/raft.pdf">这里</a> 阅读。论文也不长，18 页。文中大量出现了将 Raft 和 Paxos 的对比（开头就是一顿诉苦），凸显了 Raft 最大的优势：more understandable。</p>
<p>本文是在论文阅读中的笔记。</p>
<h3 id="前情提要">前情提要<a href="#前情提要" class="heading-anchor-link" aria-label="Link to 前情提要"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h3>
<p>共识算法的提出主要是用于复制状态机（Replicated state machines）模型。复制状态机通常用复制日志实现，每个日志中都包含一系列指令。集群中的每台机器都按相同的顺序执行这一系列指令，最终就会到达相同的状态。注意这个最终，表面这是最终一致性而非强一致性的。</p>
<p>共识算法保证复制日志在集群中是连续的，共识模块和其他机器上的共识模块通信来保证每台机器最终都按照相同顺序执行了相同的指令，即使其中某些机器挂掉了。这样保证了所有机器像单台机器一样共同对外提供服务。</p>
<p>共识算法仅适用于非拜占庭场景，即节点不会蓄意伪造信息。</p>
<h3 id="算法描述">算法描述<a href="#算法描述" class="heading-anchor-link" aria-label="Link to 算法描述"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h3>
<p>Raft 共识算法可以分成三个相对独立的子模块： - Leader 选举：如果一个现有的 Leader 挂掉了，必须选举出一个新的 Leader - 日志复制：Leader 必须从客户端接收日志，并在集群中复制，保证其他机器上的日志和自身的一致 - 安全性保障：如果任何机器接受了一个特定指令，那么其他机器就不可能再接收拥有和该指令相同的日志索引（log index）的指令</p>
<h4 id="raft-基础">Raft 基础<a href="#raft-基础" class="heading-anchor-link" aria-label="Link to Raft 基础"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h4>
<p>Raft 集群中的机器在任何时候都只有三种状态： - Leader：负责处理所有的客户端请求 - Follower：不处理请求，被动接收 Leader 和 Candidate 的请求并应答 - Candidate：用于选举出一个 Leader</p>
<p>正常执行的情况下，集群只有一个 Leader，其他所有机器都是 Follower。</p>
<p>Raft 将时间划分为多个任期，每个任期开始时都会在集群中举行选举，一个或多个 Candidate 会尝试成为 Leader。如果某个 Candidate 赢得了选举，那么它就会变成 Leader，其余机器变为 Follower。</p>
<p>任期是一个单调递增的整数，每台机器中都会保存当前任期，机器之间通信时也会带上当前任期。如果某台机器发现自己的当前任期小于其他机器，就会更新自己的当前任期。如果 Candidate 或者 Leader 发现自己的当前任期小于其他任期（出现新任期的 Leader），就会立刻转变成为 Follower。</p>
<p>Raft 机器之间的基本通信只需要两种类型的 RPC。拉票请求（RequestVote RPCs，这个翻译怎么怪怪的）在选举中由 Candidate 发起；追加请求（AppendEntries RPCs）由 Leader 发起，用于复制日志和维持心跳。</p>
<h4 id="leader-选举">Leader 选举<a href="#leader-选举" class="heading-anchor-link" aria-label="Link to Leader 选举"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h4>
<p>机器启动时的状态是 Follower，并且会在收到合适的 RPC 的情况下一直保持 Follower 状态。Leader 会定时发送不包含命令的追加请求作为心跳来维持 Server 身份。一旦某个 Follower 一段时间内没有收到请求，就会发起一场选举。</p>
<p>发起选举的 Follower 会转变为 Candidate，并将当前任期 +1，给自己投票后并行地向集群中的其他机器发送拉票请求。Candidate 会维持该状态直到一下三种情况发生：1. 如果某个 Candidate 获得了集群中大多数机器的票，那么它就赢得了选举。每个机器在某个任期中最多只会给一个 Candidate 投票，按照先来先得的原则，这样保证了每个任期中最多只有一个 Candidate 赢得选举。赢得选举的 Candidate 会变成 Leader，并向其他机器发送心跳来维持 Leader 地位。2. Candidate 可能会收到其他机器的追加请求：如果该请求中的任期大于等于 Candidate 的当前任期，Candidate 就会转变为 Follower；否则会拒绝该请求并维持 Candidate 状态。3. 如果许多 Follower 同时转变为 Candidate，那么可能没有一个 Candidate 可以获得大多数票。这种情况下 Candidate 会超时，将当前任期 +1 并发起一场新的选举。</p>
<p>为了防止场景 3 无限循环下去，Candidate 的超时时间一般设置为某个固定区间的随机时间以防止同时超时。</p>
<h4 id="日志复制">日志复制<a href="#日志复制" class="heading-anchor-link" aria-label="Link to 日志复制"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h4>
<p>每个客户端请求都包含一条需要日志复制状态机执行的指令，Leader 收到请求后会将该指令添加到 log 中，并行向集群中的机器发起追加请求来复制指令。当指令完成复制后，Leader 会提交（Commit）该命令到状态机并回复客户端执行成功。</p>
<p>每个追加请求中除了包含需要复制到指令，还包含 Leader 收到指令时的当前任期，同时还包含了一个整数索引来标明其在日志中的位置。</p>
<p>当接收该命令的 Leader 成功将命令复制到大多数机器上时，这条命令就会被 Leader 提交。Raft 会保证被提交的命令是持久化的，且最终会被所有可用的状态机执行。这个提交会同时提交所有前序的指令，即使是由其他的 Leader 创建的。Leader 会持续记录即将提交的日志索引并在所有追加请求中包含该索引。当 Follower 得知某个日志条目已经被提交了，它也会将该条目提交到它的状态机中。</p>
<p>同时 Raft 还需要保证以下两条规则： - 如果两个日志条目拥有相同的任期和日志索引，那么它们一定保存了相同的指令 - 如果两个日志条目拥有相同的任期和日志索引，那么这两条日志的所有前序日志都是相同的</p>
<p>第一条的保证比较简单，主要看第二条的保证。</p>
<p>当发送追加请求时，Leader 还会将新日志条目前面那一条条目的任期和日志索引包含在请求中。如果 Follower 在其日志序列中找不到该条日志，就会拒绝该请求，这就是连续性检查。由此，只要请求成功返回，Leader 就会知道 Follower 的日志和自己的是一致的。</p>
<p>当连续性检查失败时，Leader 会强制要求 Follower 的日志序列和自身保持一致。具体：Leader 需要找到找到和 Follower 相同的最后一条日志，删除 Follower 日志序列中那一条日志中的后面所有日志，并将自己的后续日志发给 Follower。Leader 需要给每个 Follower 维护一个 nextIndex，保存着下一个将要给该 Follower 发送的日志索引。当 Leader 刚启动时，会将所有 Follower 的 nextIndex 都初始化为其 log 序列最后一个日志索引 +1。当某个 Follower 没有通过，Leader 会将该 Follower 的 nextIndex -1 并重发追加请求。</p>
<blockquote>
<p>这里说的有点模糊，我估计应该是每个追加请求发送的是从 nextIndex 到后一个日志条目，否则在连续性检查通过后，仅仅是将 Follower 的日志序列裁剪到了 Leader 和 Follower 一致处，一致处后续的日志并没有同步到 Follower</p>
</blockquote>
<h4 id="安全性保障">安全性保障<a href="#安全性保障" class="heading-anchor-link" aria-label="Link to 安全性保障"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h4>
<p>以上描述的机制并不能完全保证安全性，例如可能存在这种可能性：某台机器突然不可达，在此期间 Leader 提交了多条日志，该机器可能会被选举为 Leader，导致这些日志被覆写。本节增加了一个选举时的限制来完善 Raft 算法。该限制保证了任一任期的 Leader 一定包含了前一任期的全部提交了的日志。</p>
<p>首先，拉票请求会包含 Candidate 的日志信息，如果其他机器发现自己的日志比 Candidate 的日志更新（最后一个条目的任期更大、日志索引更大），那么该机器就会拒绝投票请求。</p>
<p>另外，一旦某条当前任期的日志条目被大多数机器接收，Leader 就会将其提交。如果 Leader 在提交条目时就挂了，下一任 Leader 会继续尝试完成这个条目的复制。但是 Leader 不能立刻确定某个前一任期的条目已经被提交，即使它已经存储在大多数机器上。由此可能会导致论文 Figure 8 中的问题。</p>
<p>由此，Raft 并不通过检查副本的个数来尝试提交前一个任期的日志条目。只会检查副本个数来提交当前任期的日志条目，当某个当前任期的条目被提交，那么该条目前面的所有条目都被隐式地提交了。</p>
<h4 id="集群节点变更">集群节点变更<a href="#集群节点变更" class="heading-anchor-link" aria-label="Link to 集群节点变更"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h4>
<p>在集群节点变更的过程中，如果想要不让整个集群下线而实现集群配置变更，集群存在出现两个独立的“主体”的可能，即可能会选举出两个 Leader。</p>
<p>为了保证节点变更的安全性，Raft 采用了两阶段方法。首先集群切换到一个联合共识（joint consensus）状态，在联合共识被提交后，系统再切换到新配置。联合共识状态下，集群仍然可以正常地对外提供服务。在联合共识状态下： - 日志会被复制到新旧配置中的所有机器上 - 两个配置上的每台机器都允许成为 Leader - 选举和追加需要在新旧两个配置上都分别获得大多数同意才能生效</p>
<p>集群配置在日志中使用特殊的条目存储和传输。流程如下：1. Leader 收到了一个请求，将集群配置从 C_old 切换为 C_new 2. Leader 将 C_old 和 C_new 作为联合共识 C_old,new 配置存储到一条日志条目中 3. 将该条日志条目追加到新旧配置的所有机器上 4. 当某台机器接收到这样的日志条目添加其日志上时（无需提交），后续所有的操作都以该配置为标准进行 5. 当 C_old,new 被多数机器接受后，Leader 将其提交。这时，任何 C_old 或者 C_new 配置的机器就都不可能被选为 Leader 6. Leader 创建一条 C_new 的日志条目，并追加到所有机器上并提交</p>
<p>集群节点变更仍然存在以下三个问题：1. 新机器加入集群时没有存储任何日志，需要花一段时间才能追上旧机器，这可能会在短时间内造成集群的可用性降低。Raft 在将节点加入集群中时，引入了一个新阶段，在这个阶段，新机器可以正常收到追加请求，但是不会被认为是可投票的节点，不必考虑这些新节点即可达成共识。当这些新节点的日志赶上了旧机器的进度，再进行上述处理 2. 集群的 Leader 可能不是新配置的一部分。在这种情况下，当 Leader 提交 C_new 时，它自身就应当被撤下。这导致了有一段时间，Leader 管理着一个不包含它自己的集群，它复制日志但是不把自己视作主体。3. 那些被撤下的服务器可能会破坏集群，由于这些机器接收不到心跳，可能会举行选举。随后发送带有新任期的拉票请求，导致集群的 Leader 恢复成 Follower。虽然这种情况下这种选举不会成功，新 Leader 仍然会在新集群中产生，但是被撤下的机器仍然会选举超时，导致整体可用性较差。</p>
<p>为了防止问题 3 发生，Raft 增加了一条限制：如果服务器在从当前 Leader 处收到心跳的超时时间内收到拉票请求，就不会更新任期和投票。这使得一个 Leader 只要可以维持当前集群的心跳，就不会被更大的任期选票踢下位。</p>
<h4 id="日志压缩">日志压缩<a href="#日志压缩" class="heading-anchor-link" aria-label="Link to 日志压缩"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h4>
<p>随着日志的增长，机器不能将所有的日志都保存在内存中，所以需要引入快照，定期地将系统的状态保存在持久化存储中，这样从开始到快照点的日志都安全地从内存中删除。</p>
<p>集群中的每台机器独自管理自己的快照，快照仅包含其日志中已提交的日志条目。除了需要保存状态机的当前状态外，还需要保存一些元数据： - 快照包含的最后一个日志条目的日志索引 - 快照包含的最后一个日志条目的日志任期</p>
<p>这些元数据主要用于追加请求时的连续性检查（在连续性检查中需要对比前序日志条目）。如果需要支持上面提到的集群节点变更，快照中还需要包含到快照点处的最新配置信息。当机器完成快照写入后，即可删除快照点之前的所有日志和历史快照。</p>
<p>偶尔，Leader 需要将自身的日志同步到一些新加入的节点或者落后节点时，需要发送快照。Leader 会使用一种新的 RPC 请求：同步快照请求（InstallSnapshot RPC）来向 Follower 发送快照，如下：</p>
<div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--s-dark:#F97583">type</span><span style="color:#6F42C1;--s-dark:#B392F0"> InstallSnapshotRequest</span><span style="color:#D73A49;--s-dark:#F97583"> struct</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">    // Term Leader 的任期</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    Term              </span><span style="color:#D73A49;--s-dark:#F97583">int64</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">    // LeaderID Follower 可以将客户端请求重定向到 Leader</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    LeaderID          </span><span style="color:#D73A49;--s-dark:#F97583">int64</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">    // LastIncludedIndex 快照包含的最后一个条目的索引</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    LastIncludedIndex </span><span style="color:#D73A49;--s-dark:#F97583">int64</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">    // LastIncludedTerm 快照包含的最后一个条目的任期</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    LastIncludedTerm  </span><span style="color:#D73A49;--s-dark:#F97583">int64</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">    // Offset 快照文件中的该快照块的偏移</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    Offset            </span><span style="color:#D73A49;--s-dark:#F97583">int64</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">    // Data 快照块数据</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    Data              []</span><span style="color:#D73A49;--s-dark:#F97583">byte</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">    // Done 是否是最后一个快照块</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    Done              </span><span style="color:#D73A49;--s-dark:#F97583">bool</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">type</span><span style="color:#6F42C1;--s-dark:#B392F0"> InstallSnapshotResponse</span><span style="color:#D73A49;--s-dark:#F97583"> struct</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">    // Term Follower 当前任期</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    Term    </span><span style="color:#D73A49;--s-dark:#F97583">int64</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">}</span></span></code></pre></div>
<p>接收者的实现：1. 如果 Term 小于 CurrentTerm，立刻返回 2. 如果是第一个快照块（Offset = 0），创建快照文件 3. 将数据写入到快照文件对应偏移处 4. 如果 Done 为 false，返回并等待下一个同步快照请求 5. 如果日志中包含符合 LastIncludedIndex 和 LastIncludedTerm 的日志条目，则保留它后面所有的日志条目并返回 6. 废弃全部日志 7. 使用快照信息重制状态机，并使用快照中的集群配置</p>
<p>通常快照中会包含接收者当前没有的日志，这种情况下，接收者就会废弃全部日志并使用快照信息。如果快照中的日志条目接收者的日志中全部包含，那么接收者这部分日志会被快照替代，但是快照后的日志条目还需要保留。</p>
<p>最后就是一些快照可能会影响集群的性能的问题。机器可以选择一个固定的字节大小，当日志达到这个大小的时候落快照，不宜过大也不宜过小。过大会导致落快照时间过长，过小会导致落快照过于频繁。另外，落快照可能会花费很长时间（磁盘 IO 是很慢的！），可能会影响正常流程的进行。Raft 推荐使用写时复制技术，在快照写入磁盘过程中，机器仍然可以正常追加日志和处理请求。</p>
<h4 id="客户端交互">客户端交互<a href="#客户端交互" class="heading-anchor-link" aria-label="Link to 客户端交互"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h4>
<p>客户端的所有请求都由 Raft 集群中的 Leader 处理。当客户端启动时，会随便选择集群中的一个机器发送请求，如果这台机器不是 Leader，它会拒绝这个请求并返回它最近接收到心跳的 Leader 地址。当 Leader 挂掉后，客户端请求会超时，随后客户端会再次尝试随便选择一个机器请求。</p>
<p>Raft 的目标是被实现为可线性化的语义，即每个操作的执行可以看作瞬时完成的，且仅会被执行一次。然而可能存在这种可能，Leader 在执行一条指令后，在响应客户端前挂掉了；客户端就会选择另一个 Leader 重新发送一遍请求，导致指令被执行两次。解决办法是客户端<strong>们</strong>在执行请求时给每个指令标记上一个单调递增的唯一标识，同时状态机记录最后一条被执行的指令的标识。如果状态机收到一个已经被执行完成的指令，它会立刻返回成功而不会重新执行。</p>
<p>在处理只读请求的时候，由于不需要向日志中写入数据，就无需获得集群共识即可处理请求。然而由于无需共识，此时的 Leader 可能已经被某个新任期的 Leader 替代了（而它自己暂时不知道），这可能会导致这条读请求返回旧的数据。Raft 通过以下两个措施来防止这种事情发生：1. Leader 必须拥有所有已提交的日志条目的最新信息。Leader 完整性保证了 Leader 一定拥有这些条目，但是在其任期开始时它无法确定哪些是已提交的。Raft 处理这个问题的方式是在任期开始时提交一条无操作的日志条目以获取最新的提交信息。2. Leader 在处理只读请求之前必须确定它还是集群 Leader。这只需要在处理请求之前与集群的大部分机器交换一下心跳即可。</p>
<h3 id="算法实现">算法实现<a href="#算法实现" class="heading-anchor-link" aria-label="Link to 算法实现"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h3>
<p>论文在 Figure 2 中给出了一个非常详细的实现（that's why raft is awesome!），不包含节点变更和日志压缩</p>
<h4 id="server-状态">Server 状态<a href="#server-状态" class="heading-anchor-link" aria-label="Link to Server 状态"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h4>
<div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--s-dark:#F97583">type</span><span style="color:#6F42C1;--s-dark:#B392F0"> ServerState</span><span style="color:#D73A49;--s-dark:#F97583"> struct</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">    /***** 所有 Server 都包含的持久状态 *****/</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">    // CurrentTerm 机器遇到的最大的任期，启动时初始化为 0，单调递增</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    CurrentTerm </span><span style="color:#D73A49;--s-dark:#F97583">int64</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">    // VotedFor 当前任期内投票的 Candidate ID，未投票则为 nil</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    VotedFor    </span><span style="color:#D73A49;--s-dark:#F97583">*int64</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">    // Logs 日志条目，每个条目都包含了一条状态机指令和 Leader 接收该条目时的任期，index 从 1 开始</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    Logs        []</span><span style="color:#D73A49;--s-dark:#F97583">*</span><span style="color:#6F42C1;--s-dark:#B392F0">Log</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">    /***** 所有 Server 都包含的可变状态 *****/</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">    // CommitIndex 已知的最大的即将提交的日志索引，启动时初始化为 0，单调递增</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    CommitIndex </span><span style="color:#D73A49;--s-dark:#F97583">int64</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">    // LastApplied 最大的已提交的日志索引，启动时初始化为 0，单调递增</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    LastApplied </span><span style="color:#D73A49;--s-dark:#F97583">int64</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">    /******* Leader 包含的可变状态，选举后初始化 *******/</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">    // NextIndex 每台机器下一个要发送的日志条目的索引，初始化为 Leader 最后一个日志索引 +1</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    NextIndex  []</span><span style="color:#D73A49;--s-dark:#F97583">int64</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">    // MatchIndex 每台机器已知复制的最高的日志条目，初始化为 0，单调递增</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    MatchIndex []</span><span style="color:#D73A49;--s-dark:#F97583">int64</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">}</span></span></code></pre></div>
<h4 id="追加请求">追加请求<a href="#追加请求" class="heading-anchor-link" aria-label="Link to 追加请求"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h4>
<div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--s-dark:#F97583">type</span><span style="color:#6F42C1;--s-dark:#B392F0"> AppendEntriesRequest</span><span style="color:#D73A49;--s-dark:#F97583"> struct</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">    // Term Leader 的任期</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    Term         </span><span style="color:#D73A49;--s-dark:#F97583">int64</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">    // LeaderID Follower 可以将客户端请求重定向到 Leader</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    LeaderID     </span><span style="color:#D73A49;--s-dark:#F97583">int64</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">    // PrevLogIndex 新日志条目前一个日志条目的日志索引</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    PrevLogIndex </span><span style="color:#D73A49;--s-dark:#F97583">int64</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">    // PrevLogTerm 前一个日志条目的任期</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    PrevLogTerm  </span><span style="color:#D73A49;--s-dark:#F97583">int64</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">    // Entries 需要保存的日志条目，心跳包为空</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    Entries      []</span><span style="color:#D73A49;--s-dark:#F97583">*</span><span style="color:#6F42C1;--s-dark:#B392F0">Log</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">    // LeaderCommit Leader 的 CommitIndex</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    LeaderCommit </span><span style="color:#D73A49;--s-dark:#F97583">int64</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">}</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8"> </span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">type</span><span style="color:#6F42C1;--s-dark:#B392F0"> AppendEntriesResponse</span><span style="color:#D73A49;--s-dark:#F97583"> struct</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">    // Term Follower 当前任期</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    Term    </span><span style="color:#D73A49;--s-dark:#F97583">int64</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">    // Success Follower 包含 PrevLogIndex 和 PrevLogTerm 的日志条目为 true</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    Success </span><span style="color:#D73A49;--s-dark:#F97583">bool</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">}</span></span></code></pre></div>
<p>接收者的实现：1. 如果 Term 小于 CurrentTerm，返回 false 2. 如果日志中不包含 PrevLogIndex 和 PrevLogTerm 对应的日志条目，返回 false 3. 如果某个现有的日志条目和新条目包含了相同的日志索引但是有不同的任期，删除该条和其后所有的日志 4. 添加所有在日志中不存在的日志条目 5. 如果 LeaderCommit 大于 CommitIndex，则将 CommitIndex 设置为 LeaderCommit 或者新日志中最后一个索引之间的较小值</p>
<h4 id="拉票请求">拉票请求<a href="#拉票请求" class="heading-anchor-link" aria-label="Link to 拉票请求"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h4>
<div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--s-dark:#F97583">type</span><span style="color:#6F42C1;--s-dark:#B392F0"> RequestVoteRequest</span><span style="color:#D73A49;--s-dark:#F97583"> struct</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">    // Term Candidate 的任期</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    Term         </span><span style="color:#D73A49;--s-dark:#F97583">int64</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">    // CandidateId 拉票的 Candidate 的 ID</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    CandidateId  </span><span style="color:#D73A49;--s-dark:#F97583">int64</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">    // LastLogIndex Candidate 最后一条日志序列的索引</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    LastLogIndex </span><span style="color:#D73A49;--s-dark:#F97583">int64</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">    // LastLogTerm Candidate 最后一条日志序列的任期</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    LastLogTerm  </span><span style="color:#D73A49;--s-dark:#F97583">int64</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">type</span><span style="color:#6F42C1;--s-dark:#B392F0"> RequestVoteResponse</span><span style="color:#D73A49;--s-dark:#F97583"> struct</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">    // Term 当前任期</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    Term        </span><span style="color:#D73A49;--s-dark:#F97583">int64</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">    // VoteGranted true 则拉票成功</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    VoteGranted </span><span style="color:#D73A49;--s-dark:#F97583">bool</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">}</span></span></code></pre></div>
<p>接收者实现：1. 如果 Term 小于 CurrentTerm，返回 false 2. 如果 VotedFor 是 nil 或者 CandidateId，且 Candidate 的日志和接收者一样新或者更新，则返回 true</p>
<h4 id="server-规则">Server 规则<a href="#server-规则" class="heading-anchor-link" aria-label="Link to Server 规则"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h4>
<p>对于所有机器： - 如果 CommitIndex 大于 LastApplied，LastApplied +1，并将日志 log[LastApplied] 提交到状态机 - 如果 RPC 请求或者相应中的 Term 大于 CurrentTerm，则将 CurrentTerm 设置为 Term，并转变为 Follower</p>
<p>对于 Follower： - 相应来自 Candidate 和 Leader 的 RPC - 如果一直到选举超时也没有收到来自当前 Leader 的追加请求或者给某个 Candidate 投票（注意不是收到拉票请求，而是投票），则转变为 Candidate</p>
<p>对于 Candidate： - 一旦转变成 Candidate，即开始选举： - 将当前任期 +1 - 给自己投票 - 重制选举超时计时器 - 向所有其他机器发送拉票请求 - 如果收到了大多数机器的票，则转变成 Leader - 如果收到来自新 Leader 的追加请求，转变为 Follower - 如果选举超时，开始新一轮选举</p>
<p>对于 Leader： - 一旦转变成 Leader，向其他所有机器发送空的追加请求；在空闲时重复发送空追加请求来防止选举超时 - 如果收到来自客户端的指令：添加日志条目到日志中，在条目提交到状态机后返回相应 - 如果最后一条日志索引大于某个客户端的 NextIndex：向客户端发送包含 NextIndex 及其后所有日志条目的追加请求 - 如果成功，更新 Follower 的 NextIndex 和 MatchIndex - 如果因为日志连续性而失败，NextIndex -- 并重拾 - 如果存在一个 N 大于 CommitIndex，大多数 MatchIndex 大于等于 N，且第 N 条日志的任期等于当前任期，则设置 CommitIndex 为 N</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[golang 支持不同类型结构体间的深拷贝]]></title>
            <link>https://blog.shinya.click/fiddling/golang-deepcopy-between-different-type</link>
            <guid isPermaLink="false">https://blog.shinya.click/fiddling/golang-deepcopy-between-different-type</guid>
            <pubDate>Sun, 14 Aug 2022 17:05:01 GMT</pubDate>
            <description><![CDATA[在系统重构的过程中，面对分层架构中不同实体间的转换问题，深拷贝成为一项挑战。以商品为例，视图层的商品 VO、领域层的商品 entity 以及存储层的商品 PO 结构虽相似，但细微的差别如数据类型的不同，常常使得直接转换变得复杂且麻烦。为此，尝试利用反射实现一个通用的转换方法，旨在简化这一过程，减少繁琐的 assembler 方法，提升代码的可维护性与灵活性。]]></description>
            <content:encoded><![CDATA[<p>最近在做系统重构，忙得不可开交～博客荒废了好一阵子</p>
<p>在重构时，遇到了一个很恶心的事情：分层架构中不同层实体的互相转换。以商品为例，在视图层可能存在一个商品 VO，在领域层有个商品 entity 或者叫 DO（domain object），在存储层还可能有个商品 PO 对应着数据库实体……</p>
<p>这些实体结构大都很像，甚至好多基本一致或完全一样，或存在着细微的区别，例如某个字段在这个实体中是指针类型，而在另外的结构中则不是指针类型，因而导致无法直接强转，比如写很多 assembler 方法来对实体做转换，遇到复杂结构简直就是地狱。本质上就是深拷贝，就因为类型不一致而没法处理。</p>
<p>于是想着能不能用反射解决一下问题，对于这种特殊的场景做一个通用一点的转换方法，于是花了一个下午，以下代码应运而生：</p>
<div class="code-collapse-wrapper"><input type="checkbox" id="toggle-cpdpkwa" class="code-collapse-toggle" style="display: none;"><div class="code-collapse-preview"><div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0" class="code-preview"><code><span class="line"><span style="color:#D73A49;--s-dark:#F97583">func</span><span style="color:#6F42C1;--s-dark:#B392F0"> Copy</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#E36209;--s-dark:#FFAB70">src</span><span style="color:#D73A49;--s-dark:#F97583"> interface</span><span style="color:#24292E;--s-dark:#E1E4E8">{}, </span><span style="color:#E36209;--s-dark:#FFAB70">dstType</span><span style="color:#D73A49;--s-dark:#F97583"> interface</span><span style="color:#24292E;--s-dark:#E1E4E8">{}) </span><span style="color:#D73A49;--s-dark:#F97583">interface</span><span style="color:#24292E;--s-dark:#E1E4E8">{} {</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">    if</span><span style="color:#24292E;--s-dark:#E1E4E8"> src </span><span style="color:#D73A49;--s-dark:#F97583">==</span><span style="color:#005CC5;--s-dark:#79B8FF"> nil</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">        return</span><span style="color:#005CC5;--s-dark:#79B8FF"> nil</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    cpy </span><span style="color:#D73A49;--s-dark:#F97583">:=</span><span style="color:#24292E;--s-dark:#E1E4E8"> reflect.</span><span style="color:#6F42C1;--s-dark:#B392F0">New</span><span style="color:#24292E;--s-dark:#E1E4E8">(reflect.</span><span style="color:#6F42C1;--s-dark:#B392F0">TypeOf</span><span style="color:#24292E;--s-dark:#E1E4E8">(dstType)).</span><span style="color:#6F42C1;--s-dark:#B392F0">Elem</span><span style="color:#24292E;--s-dark:#E1E4E8">()</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">    copyRecursive</span><span style="color:#24292E;--s-dark:#E1E4E8">(reflect.</span><span style="color:#6F42C1;--s-dark:#B392F0">ValueOf</span><span style="color:#24292E;--s-dark:#E1E4E8">(src), cpy)</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">    return</span><span style="color:#24292E;--s-dark:#E1E4E8"> cpy.</span><span style="color:#6F42C1;--s-dark:#B392F0">Interface</span><span style="color:#24292E;--s-dark:#E1E4E8">()</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">}</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8"> </span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">func</span><span style="color:#6F42C1;--s-dark:#B392F0"> copyRecursive</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#E36209;--s-dark:#FFAB70">src</span><span style="color:#24292E;--s-dark:#E1E4E8">, </span><span style="color:#E36209;--s-dark:#FFAB70">dst</span><span style="color:#6F42C1;--s-dark:#B392F0"> reflect</span><span style="color:#24292E;--s-dark:#E1E4E8">.</span><span style="color:#6F42C1;--s-dark:#B392F0">Value</span><span style="color:#24292E;--s-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">    switch</span><span style="color:#24292E;--s-dark:#E1E4E8"> src.</span><span style="color:#6F42C1;--s-dark:#B392F0">Kind</span><span style="color:#24292E;--s-dark:#E1E4E8">() {</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">    case</span><span style="color:#24292E;--s-dark:#E1E4E8"> reflect.Ptr:</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        originValue </span><span style="color:#D73A49;--s-dark:#F97583">:=</span><span style="color:#24292E;--s-dark:#E1E4E8"> src.</span><span style="color:#6F42C1;--s-dark:#B392F0">Elem</span><span style="color:#24292E;--s-dark:#E1E4E8">()</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">        if</span><span style="color:#D73A49;--s-dark:#F97583"> !</span><span style="color:#24292E;--s-dark:#E1E4E8">originValue.</span><span style="color:#6F42C1;--s-dark:#B392F0">IsValid</span><span style="color:#24292E;--s-dark:#E1E4E8">() {</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">            return</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        }</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">        // 允许 src 为 ptr 而 dst 为非 ptr</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">        if</span><span style="color:#24292E;--s-dark:#E1E4E8"> dst.</span><span style="color:#6F42C1;--s-dark:#B392F0">Kind</span><span style="color:#24292E;--s-dark:#E1E4E8">() </span><span style="color:#D73A49;--s-dark:#F97583">==</span><span style="color:#24292E;--s-dark:#E1E4E8"> reflect.Ptr {</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            dst.</span><span style="color:#6F42C1;--s-dark:#B392F0">Set</span><span style="color:#24292E;--s-dark:#E1E4E8">(reflect.</span><span style="color:#6F42C1;--s-dark:#B392F0">New</span><span style="color:#24292E;--s-dark:#E1E4E8">(dst.</span><span style="color:#6F42C1;--s-dark:#B392F0">Type</span><span style="color:#24292E;--s-dark:#E1E4E8">().</span><span style="color:#6F42C1;--s-dark:#B392F0">Elem</span><span style="color:#24292E;--s-dark:#E1E4E8">()))</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">            copyRecursive</span><span style="color:#24292E;--s-dark:#E1E4E8">(originValue, dst.</span><span style="color:#6F42C1;--s-dark:#B392F0">Elem</span><span style="color:#24292E;--s-dark:#E1E4E8">())</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        } </span><span style="color:#D73A49;--s-dark:#F97583">else</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            dst.</span><span style="color:#6F42C1;--s-dark:#B392F0">Set</span><span style="color:#24292E;--s-dark:#E1E4E8">(reflect.</span><span style="color:#6F42C1;--s-dark:#B392F0">New</span><span style="color:#24292E;--s-dark:#E1E4E8">(dst.</span><span style="color:#6F42C1;--s-dark:#B392F0">Type</span><span style="color:#24292E;--s-dark:#E1E4E8">()).</span><span style="color:#6F42C1;--s-dark:#B392F0">Elem</span><span style="color:#24292E;--s-dark:#E1E4E8">())</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">            copyRecursive</span><span style="color:#24292E;--s-dark:#E1E4E8">(originValue, dst)</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        }</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">    case</span><span style="color:#24292E;--s-dark:#E1E4E8"> reflect.Interface:</span></span>
</code></pre></div><div class="code-collapse-gradient"></div><label class="code-collapse-expand" for="toggle-cpdpkwa"><svg class="code-collapse-icon" width="32" height="24" viewBox="0 0 32 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="4,9 12,17 20,9"></polyline></svg></label></div><div class="code-collapse-full"><div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--s-dark:#F97583">func</span><span style="color:#6F42C1;--s-dark:#B392F0"> Copy</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#E36209;--s-dark:#FFAB70">src</span><span style="color:#D73A49;--s-dark:#F97583"> interface</span><span style="color:#24292E;--s-dark:#E1E4E8">{}, </span><span style="color:#E36209;--s-dark:#FFAB70">dstType</span><span style="color:#D73A49;--s-dark:#F97583"> interface</span><span style="color:#24292E;--s-dark:#E1E4E8">{}) </span><span style="color:#D73A49;--s-dark:#F97583">interface</span><span style="color:#24292E;--s-dark:#E1E4E8">{} {</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">    if</span><span style="color:#24292E;--s-dark:#E1E4E8"> src </span><span style="color:#D73A49;--s-dark:#F97583">==</span><span style="color:#005CC5;--s-dark:#79B8FF"> nil</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">        return</span><span style="color:#005CC5;--s-dark:#79B8FF"> nil</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    cpy </span><span style="color:#D73A49;--s-dark:#F97583">:=</span><span style="color:#24292E;--s-dark:#E1E4E8"> reflect.</span><span style="color:#6F42C1;--s-dark:#B392F0">New</span><span style="color:#24292E;--s-dark:#E1E4E8">(reflect.</span><span style="color:#6F42C1;--s-dark:#B392F0">TypeOf</span><span style="color:#24292E;--s-dark:#E1E4E8">(dstType)).</span><span style="color:#6F42C1;--s-dark:#B392F0">Elem</span><span style="color:#24292E;--s-dark:#E1E4E8">()</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">    copyRecursive</span><span style="color:#24292E;--s-dark:#E1E4E8">(reflect.</span><span style="color:#6F42C1;--s-dark:#B392F0">ValueOf</span><span style="color:#24292E;--s-dark:#E1E4E8">(src), cpy)</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">    return</span><span style="color:#24292E;--s-dark:#E1E4E8"> cpy.</span><span style="color:#6F42C1;--s-dark:#B392F0">Interface</span><span style="color:#24292E;--s-dark:#E1E4E8">()</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">}</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8"> </span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">func</span><span style="color:#6F42C1;--s-dark:#B392F0"> copyRecursive</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#E36209;--s-dark:#FFAB70">src</span><span style="color:#24292E;--s-dark:#E1E4E8">, </span><span style="color:#E36209;--s-dark:#FFAB70">dst</span><span style="color:#6F42C1;--s-dark:#B392F0"> reflect</span><span style="color:#24292E;--s-dark:#E1E4E8">.</span><span style="color:#6F42C1;--s-dark:#B392F0">Value</span><span style="color:#24292E;--s-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">    switch</span><span style="color:#24292E;--s-dark:#E1E4E8"> src.</span><span style="color:#6F42C1;--s-dark:#B392F0">Kind</span><span style="color:#24292E;--s-dark:#E1E4E8">() {</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">    case</span><span style="color:#24292E;--s-dark:#E1E4E8"> reflect.Ptr:</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        originValue </span><span style="color:#D73A49;--s-dark:#F97583">:=</span><span style="color:#24292E;--s-dark:#E1E4E8"> src.</span><span style="color:#6F42C1;--s-dark:#B392F0">Elem</span><span style="color:#24292E;--s-dark:#E1E4E8">()</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">        if</span><span style="color:#D73A49;--s-dark:#F97583"> !</span><span style="color:#24292E;--s-dark:#E1E4E8">originValue.</span><span style="color:#6F42C1;--s-dark:#B392F0">IsValid</span><span style="color:#24292E;--s-dark:#E1E4E8">() {</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">            return</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        }</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">        // 允许 src 为 ptr 而 dst 为非 ptr</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">        if</span><span style="color:#24292E;--s-dark:#E1E4E8"> dst.</span><span style="color:#6F42C1;--s-dark:#B392F0">Kind</span><span style="color:#24292E;--s-dark:#E1E4E8">() </span><span style="color:#D73A49;--s-dark:#F97583">==</span><span style="color:#24292E;--s-dark:#E1E4E8"> reflect.Ptr {</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            dst.</span><span style="color:#6F42C1;--s-dark:#B392F0">Set</span><span style="color:#24292E;--s-dark:#E1E4E8">(reflect.</span><span style="color:#6F42C1;--s-dark:#B392F0">New</span><span style="color:#24292E;--s-dark:#E1E4E8">(dst.</span><span style="color:#6F42C1;--s-dark:#B392F0">Type</span><span style="color:#24292E;--s-dark:#E1E4E8">().</span><span style="color:#6F42C1;--s-dark:#B392F0">Elem</span><span style="color:#24292E;--s-dark:#E1E4E8">()))</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">            copyRecursive</span><span style="color:#24292E;--s-dark:#E1E4E8">(originValue, dst.</span><span style="color:#6F42C1;--s-dark:#B392F0">Elem</span><span style="color:#24292E;--s-dark:#E1E4E8">())</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        } </span><span style="color:#D73A49;--s-dark:#F97583">else</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            dst.</span><span style="color:#6F42C1;--s-dark:#B392F0">Set</span><span style="color:#24292E;--s-dark:#E1E4E8">(reflect.</span><span style="color:#6F42C1;--s-dark:#B392F0">New</span><span style="color:#24292E;--s-dark:#E1E4E8">(dst.</span><span style="color:#6F42C1;--s-dark:#B392F0">Type</span><span style="color:#24292E;--s-dark:#E1E4E8">()).</span><span style="color:#6F42C1;--s-dark:#B392F0">Elem</span><span style="color:#24292E;--s-dark:#E1E4E8">())</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">            copyRecursive</span><span style="color:#24292E;--s-dark:#E1E4E8">(originValue, dst)</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        }</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">    case</span><span style="color:#24292E;--s-dark:#E1E4E8"> reflect.Interface:</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">        if</span><span style="color:#24292E;--s-dark:#E1E4E8"> src.</span><span style="color:#6F42C1;--s-dark:#B392F0">IsNil</span><span style="color:#24292E;--s-dark:#E1E4E8">() {</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">            return</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        }</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        originValue </span><span style="color:#D73A49;--s-dark:#F97583">:=</span><span style="color:#24292E;--s-dark:#E1E4E8"> src.</span><span style="color:#6F42C1;--s-dark:#B392F0">Elem</span><span style="color:#24292E;--s-dark:#E1E4E8">()</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        copyValue </span><span style="color:#D73A49;--s-dark:#F97583">:=</span><span style="color:#24292E;--s-dark:#E1E4E8"> reflect.</span><span style="color:#6F42C1;--s-dark:#B392F0">New</span><span style="color:#24292E;--s-dark:#E1E4E8">(dst.</span><span style="color:#6F42C1;--s-dark:#B392F0">Type</span><span style="color:#24292E;--s-dark:#E1E4E8">().</span><span style="color:#6F42C1;--s-dark:#B392F0">Elem</span><span style="color:#24292E;--s-dark:#E1E4E8">()).</span><span style="color:#6F42C1;--s-dark:#B392F0">Elem</span><span style="color:#24292E;--s-dark:#E1E4E8">()</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">        copyRecursive</span><span style="color:#24292E;--s-dark:#E1E4E8">(originValue, copyValue)</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        dst.</span><span style="color:#6F42C1;--s-dark:#B392F0">Set</span><span style="color:#24292E;--s-dark:#E1E4E8">(copyValue)</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">    case</span><span style="color:#24292E;--s-dark:#E1E4E8"> reflect.Struct:</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">        // time.Time 需要特殊处理</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        t, ok </span><span style="color:#D73A49;--s-dark:#F97583">:=</span><span style="color:#24292E;--s-dark:#E1E4E8"> src.</span><span style="color:#6F42C1;--s-dark:#B392F0">Interface</span><span style="color:#24292E;--s-dark:#E1E4E8">().(</span><span style="color:#6F42C1;--s-dark:#B392F0">time</span><span style="color:#24292E;--s-dark:#E1E4E8">.</span><span style="color:#6F42C1;--s-dark:#B392F0">Time</span><span style="color:#24292E;--s-dark:#E1E4E8">)</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">        if</span><span style="color:#24292E;--s-dark:#E1E4E8"> ok {</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            dst.</span><span style="color:#6F42C1;--s-dark:#B392F0">Set</span><span style="color:#24292E;--s-dark:#E1E4E8">(reflect.</span><span style="color:#6F42C1;--s-dark:#B392F0">ValueOf</span><span style="color:#24292E;--s-dark:#E1E4E8">(t))</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">            return</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        }</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">        if</span><span style="color:#24292E;--s-dark:#E1E4E8"> dst.</span><span style="color:#6F42C1;--s-dark:#B392F0">Kind</span><span style="color:#24292E;--s-dark:#E1E4E8">() </span><span style="color:#D73A49;--s-dark:#F97583">==</span><span style="color:#24292E;--s-dark:#E1E4E8"> reflect.Ptr {</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">            // 目标类型是指针而源类型不是指针</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            copyValue </span><span style="color:#D73A49;--s-dark:#F97583">:=</span><span style="color:#24292E;--s-dark:#E1E4E8"> reflect.</span><span style="color:#6F42C1;--s-dark:#B392F0">New</span><span style="color:#24292E;--s-dark:#E1E4E8">(dst.</span><span style="color:#6F42C1;--s-dark:#B392F0">Type</span><span style="color:#24292E;--s-dark:#E1E4E8">().</span><span style="color:#6F42C1;--s-dark:#B392F0">Elem</span><span style="color:#24292E;--s-dark:#E1E4E8">()).</span><span style="color:#6F42C1;--s-dark:#B392F0">Elem</span><span style="color:#24292E;--s-dark:#E1E4E8">()</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">            copyRecursive</span><span style="color:#24292E;--s-dark:#E1E4E8">(src, copyValue)</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            dst.</span><span style="color:#6F42C1;--s-dark:#B392F0">Set</span><span style="color:#24292E;--s-dark:#E1E4E8">(copyValue.</span><span style="color:#6F42C1;--s-dark:#B392F0">Addr</span><span style="color:#24292E;--s-dark:#E1E4E8">())</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">            return</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        }</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">        for</span><span style="color:#24292E;--s-dark:#E1E4E8"> i </span><span style="color:#D73A49;--s-dark:#F97583">:=</span><span style="color:#005CC5;--s-dark:#79B8FF"> 0</span><span style="color:#24292E;--s-dark:#E1E4E8">; i </span><span style="color:#D73A49;--s-dark:#F97583">&#x3C;</span><span style="color:#24292E;--s-dark:#E1E4E8"> dst.</span><span style="color:#6F42C1;--s-dark:#B392F0">NumField</span><span style="color:#24292E;--s-dark:#E1E4E8">(); i</span><span style="color:#D73A49;--s-dark:#F97583">++</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">            if</span><span style="color:#24292E;--s-dark:#E1E4E8"> dst.</span><span style="color:#6F42C1;--s-dark:#B392F0">Type</span><span style="color:#24292E;--s-dark:#E1E4E8">().</span><span style="color:#6F42C1;--s-dark:#B392F0">Field</span><span style="color:#24292E;--s-dark:#E1E4E8">(i).PkgPath </span><span style="color:#D73A49;--s-dark:#F97583">!=</span><span style="color:#032F62;--s-dark:#9ECBFF"> ""</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">                // 不可导出的字段不拷贝</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">                continue</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            }</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            field </span><span style="color:#D73A49;--s-dark:#F97583">:=</span><span style="color:#24292E;--s-dark:#E1E4E8"> src.</span><span style="color:#6F42C1;--s-dark:#B392F0">FieldByName</span><span style="color:#24292E;--s-dark:#E1E4E8">(dst.</span><span style="color:#6F42C1;--s-dark:#B392F0">Type</span><span style="color:#24292E;--s-dark:#E1E4E8">().</span><span style="color:#6F42C1;--s-dark:#B392F0">Field</span><span style="color:#24292E;--s-dark:#E1E4E8">(i).Name)</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">            if</span><span style="color:#D73A49;--s-dark:#F97583"> !</span><span style="color:#24292E;--s-dark:#E1E4E8">field.</span><span style="color:#6F42C1;--s-dark:#B392F0">IsValid</span><span style="color:#24292E;--s-dark:#E1E4E8">() {</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">                // 源字段不存在，忽略（目标自动零值）</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">                continue</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            }</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">            copyRecursive</span><span style="color:#24292E;--s-dark:#E1E4E8">(field, dst.</span><span style="color:#6F42C1;--s-dark:#B392F0">Field</span><span style="color:#24292E;--s-dark:#E1E4E8">(i))</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        }</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">    case</span><span style="color:#24292E;--s-dark:#E1E4E8"> reflect.Slice:</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">        if</span><span style="color:#24292E;--s-dark:#E1E4E8"> src.</span><span style="color:#6F42C1;--s-dark:#B392F0">IsNil</span><span style="color:#24292E;--s-dark:#E1E4E8">() {</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">            return</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        }</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        dst.</span><span style="color:#6F42C1;--s-dark:#B392F0">Set</span><span style="color:#24292E;--s-dark:#E1E4E8">(reflect.</span><span style="color:#6F42C1;--s-dark:#B392F0">MakeSlice</span><span style="color:#24292E;--s-dark:#E1E4E8">(dst.</span><span style="color:#6F42C1;--s-dark:#B392F0">Type</span><span style="color:#24292E;--s-dark:#E1E4E8">(), src.</span><span style="color:#6F42C1;--s-dark:#B392F0">Len</span><span style="color:#24292E;--s-dark:#E1E4E8">(), src.</span><span style="color:#6F42C1;--s-dark:#B392F0">Cap</span><span style="color:#24292E;--s-dark:#E1E4E8">()))</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">        for</span><span style="color:#24292E;--s-dark:#E1E4E8"> i </span><span style="color:#D73A49;--s-dark:#F97583">:=</span><span style="color:#005CC5;--s-dark:#79B8FF"> 0</span><span style="color:#24292E;--s-dark:#E1E4E8">; i </span><span style="color:#D73A49;--s-dark:#F97583">&#x3C;</span><span style="color:#24292E;--s-dark:#E1E4E8"> src.</span><span style="color:#6F42C1;--s-dark:#B392F0">Len</span><span style="color:#24292E;--s-dark:#E1E4E8">(); i</span><span style="color:#D73A49;--s-dark:#F97583">++</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">            copyRecursive</span><span style="color:#24292E;--s-dark:#E1E4E8">(src.</span><span style="color:#6F42C1;--s-dark:#B392F0">Index</span><span style="color:#24292E;--s-dark:#E1E4E8">(i), dst.</span><span style="color:#6F42C1;--s-dark:#B392F0">Index</span><span style="color:#24292E;--s-dark:#E1E4E8">(i))</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        }</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">    case</span><span style="color:#24292E;--s-dark:#E1E4E8"> reflect.Map:</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">        if</span><span style="color:#24292E;--s-dark:#E1E4E8"> src.</span><span style="color:#6F42C1;--s-dark:#B392F0">IsNil</span><span style="color:#24292E;--s-dark:#E1E4E8">() {</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">            return</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        }</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        dst.</span><span style="color:#6F42C1;--s-dark:#B392F0">Set</span><span style="color:#24292E;--s-dark:#E1E4E8">(reflect.</span><span style="color:#6F42C1;--s-dark:#B392F0">MakeMap</span><span style="color:#24292E;--s-dark:#E1E4E8">(dst.</span><span style="color:#6F42C1;--s-dark:#B392F0">Type</span><span style="color:#24292E;--s-dark:#E1E4E8">()))</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">        for</span><span style="color:#24292E;--s-dark:#E1E4E8"> _, key </span><span style="color:#D73A49;--s-dark:#F97583">:=</span><span style="color:#D73A49;--s-dark:#F97583"> range</span><span style="color:#24292E;--s-dark:#E1E4E8"> src.</span><span style="color:#6F42C1;--s-dark:#B392F0">MapKeys</span><span style="color:#24292E;--s-dark:#E1E4E8">() {</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            value </span><span style="color:#D73A49;--s-dark:#F97583">:=</span><span style="color:#24292E;--s-dark:#E1E4E8"> src.</span><span style="color:#6F42C1;--s-dark:#B392F0">MapIndex</span><span style="color:#24292E;--s-dark:#E1E4E8">(key)</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            copyValue </span><span style="color:#D73A49;--s-dark:#F97583">:=</span><span style="color:#24292E;--s-dark:#E1E4E8"> reflect.</span><span style="color:#6F42C1;--s-dark:#B392F0">New</span><span style="color:#24292E;--s-dark:#E1E4E8">(dst.</span><span style="color:#6F42C1;--s-dark:#B392F0">Type</span><span style="color:#24292E;--s-dark:#E1E4E8">().</span><span style="color:#6F42C1;--s-dark:#B392F0">Elem</span><span style="color:#24292E;--s-dark:#E1E4E8">()).</span><span style="color:#6F42C1;--s-dark:#B392F0">Elem</span><span style="color:#24292E;--s-dark:#E1E4E8">()</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">            copyRecursive</span><span style="color:#24292E;--s-dark:#E1E4E8">(value, copyValue)</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            copyKey </span><span style="color:#D73A49;--s-dark:#F97583">:=</span><span style="color:#6F42C1;--s-dark:#B392F0"> Copy</span><span style="color:#24292E;--s-dark:#E1E4E8">(key.</span><span style="color:#6F42C1;--s-dark:#B392F0">Interface</span><span style="color:#24292E;--s-dark:#E1E4E8">(), reflect.</span><span style="color:#6F42C1;--s-dark:#B392F0">New</span><span style="color:#24292E;--s-dark:#E1E4E8">(dst.</span><span style="color:#6F42C1;--s-dark:#B392F0">Type</span><span style="color:#24292E;--s-dark:#E1E4E8">().</span><span style="color:#6F42C1;--s-dark:#B392F0">Key</span><span style="color:#24292E;--s-dark:#E1E4E8">()).</span><span style="color:#6F42C1;--s-dark:#B392F0">Elem</span><span style="color:#24292E;--s-dark:#E1E4E8">().</span><span style="color:#6F42C1;--s-dark:#B392F0">Interface</span><span style="color:#24292E;--s-dark:#E1E4E8">())</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            dst.</span><span style="color:#6F42C1;--s-dark:#B392F0">SetMapIndex</span><span style="color:#24292E;--s-dark:#E1E4E8">(reflect.</span><span style="color:#6F42C1;--s-dark:#B392F0">ValueOf</span><span style="color:#24292E;--s-dark:#E1E4E8">(copyKey), copyValue)</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        }</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">    default</span><span style="color:#24292E;--s-dark:#E1E4E8">:</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">        // 源类型是基础类型</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">        // 类型不一致但底层类型一致的基本类型，需要强转</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">        if</span><span style="color:#24292E;--s-dark:#E1E4E8"> dst.</span><span style="color:#6F42C1;--s-dark:#B392F0">Kind</span><span style="color:#24292E;--s-dark:#E1E4E8">() </span><span style="color:#D73A49;--s-dark:#F97583">==</span><span style="color:#24292E;--s-dark:#E1E4E8"> reflect.Ptr {</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">            // 目标类型是指针而源类型不是指针</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            copyValue </span><span style="color:#D73A49;--s-dark:#F97583">:=</span><span style="color:#24292E;--s-dark:#E1E4E8"> reflect.</span><span style="color:#6F42C1;--s-dark:#B392F0">New</span><span style="color:#24292E;--s-dark:#E1E4E8">(dst.</span><span style="color:#6F42C1;--s-dark:#B392F0">Type</span><span style="color:#24292E;--s-dark:#E1E4E8">().</span><span style="color:#6F42C1;--s-dark:#B392F0">Elem</span><span style="color:#24292E;--s-dark:#E1E4E8">()).</span><span style="color:#6F42C1;--s-dark:#B392F0">Elem</span><span style="color:#24292E;--s-dark:#E1E4E8">()</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">            copyRecursive</span><span style="color:#24292E;--s-dark:#E1E4E8">(src, copyValue)</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            dst.</span><span style="color:#6F42C1;--s-dark:#B392F0">Set</span><span style="color:#24292E;--s-dark:#E1E4E8">(copyValue.</span><span style="color:#6F42C1;--s-dark:#B392F0">Addr</span><span style="color:#24292E;--s-dark:#E1E4E8">())</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">            return</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        }</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        dst.</span><span style="color:#6F42C1;--s-dark:#B392F0">Set</span><span style="color:#24292E;--s-dark:#E1E4E8">(src.</span><span style="color:#6F42C1;--s-dark:#B392F0">Convert</span><span style="color:#24292E;--s-dark:#E1E4E8">(dst.</span><span style="color:#6F42C1;--s-dark:#B392F0">Type</span><span style="color:#24292E;--s-dark:#E1E4E8">()))</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">}</span></span></code><label class="code-collapse-collapse" for="toggle-cpdpkwa"><svg class="code-collapse-icon" width="32" height="24" viewBox="0 0 32 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20,15 12,7 4,15"></polyline></svg></label></pre></div></div></div>
<p>核心在于 copyRecursive 方法，它支持处理结构体、Slice 和 Map 的深拷贝，并且支持从指针类型向非指针类型、非指针类型向指针类型的拷贝，唯一的要求就是拷贝结构体时，目标类型结构体中的所有字段都必须在源类型结构体中有同名且底层类型相同的字段，这样才能完成递归深拷贝。</p>
<p>具体代码原理不细细讲解，但是我还是要说一句：</p>
<p>反射确实很牛逼</p>
<hr>
<p>20220820 update：支持拷贝到源结构体中不存在同名字段的结构体，当源结构体不存在同名字段时，目标结构体的字段会被赋零值（指针为 nil，结构体为空结构体）</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Java 中 this 关键字导致编译期常量传播优化失效的问题]]></title>
            <link>https://blog.shinya.click/fiddling/this-in-javac-string-concat</link>
            <guid isPermaLink="false">https://blog.shinya.click/fiddling/this-in-javac-string-concat</guid>
            <pubDate>Fri, 15 Apr 2022 16:01:28 GMT</pubDate>
            <description><![CDATA[在 Java 中，使用 `this` 关键字时，编译器对常量的优化可能会失效。代码示例中，尽管 `ab1` 和 `ab2` 看似引用了相同的 `final` 静态变量 `s`，但在比较时却产生了不同的结果。`ab1` 通过直接引用静态变量进行字符串连接，而 `ab2` 则是通过 `this` 关键字，导致编译器无法进行相同的常量传播优化，从而影响了字符串的比较结果。这一现象揭示了在 Java 中细微的语法差异可能引发的编译行为变化。]]></description>
            <content:encoded><![CDATA[<p>名字起的有点长了，但是这确实是个挺有趣的问题。如下代码：</p>
<div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--s-dark:#F97583">public</span><span style="color:#D73A49;--s-dark:#F97583"> class</span><span style="color:#6F42C1;--s-dark:#B392F0"> Test</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">    final</span><span style="color:#D73A49;--s-dark:#F97583"> static</span><span style="color:#24292E;--s-dark:#E1E4E8"> String s </span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#032F62;--s-dark:#9ECBFF"> "a"</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">    public</span><span style="color:#D73A49;--s-dark:#F97583"> void</span><span style="color:#6F42C1;--s-dark:#B392F0"> test</span><span style="color:#24292E;--s-dark:#E1E4E8">() {</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        String cmp </span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#032F62;--s-dark:#9ECBFF"> "ab"</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        String ab1 </span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#24292E;--s-dark:#E1E4E8"> s </span><span style="color:#D73A49;--s-dark:#F97583">+</span><span style="color:#032F62;--s-dark:#9ECBFF"> "b"</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        String ab2 </span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#005CC5;--s-dark:#79B8FF"> this</span><span style="color:#24292E;--s-dark:#E1E4E8">.s </span><span style="color:#D73A49;--s-dark:#F97583">+</span><span style="color:#032F62;--s-dark:#9ECBFF"> "b"</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        System.out.</span><span style="color:#6F42C1;--s-dark:#B392F0">println</span><span style="color:#24292E;--s-dark:#E1E4E8">(ab1 </span><span style="color:#D73A49;--s-dark:#F97583">==</span><span style="color:#24292E;--s-dark:#E1E4E8"> cmp);</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        System.out.</span><span style="color:#6F42C1;--s-dark:#B392F0">println</span><span style="color:#24292E;--s-dark:#E1E4E8">(ab2 </span><span style="color:#D73A49;--s-dark:#F97583">==</span><span style="color:#24292E;--s-dark:#E1E4E8"> cmp);</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    }</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">    public</span><span style="color:#D73A49;--s-dark:#F97583"> static</span><span style="color:#D73A49;--s-dark:#F97583"> void</span><span style="color:#6F42C1;--s-dark:#B392F0"> main</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#D73A49;--s-dark:#F97583">String</span><span style="color:#24292E;--s-dark:#E1E4E8">[] </span><span style="color:#E36209;--s-dark:#FFAB70">args</span><span style="color:#24292E;--s-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">        new</span><span style="color:#6F42C1;--s-dark:#B392F0"> Test</span><span style="color:#24292E;--s-dark:#E1E4E8">().</span><span style="color:#6F42C1;--s-dark:#B392F0">test</span><span style="color:#24292E;--s-dark:#E1E4E8">();</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">}</span></span></code></pre></div>
<p>我们先来猜一猜输出结果，第 7 行的 s 和第 8 行的 this.s 都是指的同一个变量，就是那个 final 静态变量 s。那么就算我们不知道结果，这两个输出应该也是一样的。但实际上的输出是</p>
<div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">true</span></span>
<span class="line"><span style="color:#005CC5;--s-dark:#79B8FF">false</span></span></code></pre></div>
<p>我们先用 javap 将生成的字节码反汇编出来，test() 部分如下：</p>
<div class="code-collapse-wrapper"><input type="checkbox" id="toggle-o66tb8z" class="code-collapse-toggle" style="display: none;"><div class="code-collapse-preview"><div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0" class="code-preview"><code><span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">  public</span><span style="color:#032F62;--s-dark:#9ECBFF"> void</span><span style="color:#032F62;--s-dark:#9ECBFF"> test</span><span style="color:#24292E;--s-dark:#E1E4E8">();</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">    descriptor:</span><span style="color:#24292E;--s-dark:#E1E4E8"> ()</span><span style="color:#6F42C1;--s-dark:#B392F0">V</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">    flags:</span><span style="color:#24292E;--s-dark:#E1E4E8"> (0x0001) ACC_PUBLIC</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">    Code:</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">      stack</span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#032F62;--s-dark:#9ECBFF">3,</span><span style="color:#24292E;--s-dark:#E1E4E8"> locals</span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#032F62;--s-dark:#9ECBFF">4,</span><span style="color:#24292E;--s-dark:#E1E4E8"> args_size</span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#032F62;--s-dark:#9ECBFF">1</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">         0:</span><span style="color:#032F62;--s-dark:#9ECBFF"> ldc</span><span style="color:#6A737D;--s-dark:#6A737D">           #7                  // String ab</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">         2:</span><span style="color:#032F62;--s-dark:#9ECBFF"> astore_1</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">         3:</span><span style="color:#032F62;--s-dark:#9ECBFF"> ldc</span><span style="color:#6A737D;--s-dark:#6A737D">           #7                  // String ab</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">         5:</span><span style="color:#032F62;--s-dark:#9ECBFF"> astore_2</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">         6:</span><span style="color:#032F62;--s-dark:#9ECBFF"> aload_0</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">         7:</span><span style="color:#032F62;--s-dark:#9ECBFF"> pop</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">         8:</span><span style="color:#032F62;--s-dark:#9ECBFF"> ldc</span><span style="color:#6A737D;--s-dark:#6A737D">           #11                 // String a</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">        10:</span><span style="color:#032F62;--s-dark:#9ECBFF"> invokedynamic</span><span style="color:#6A737D;--s-dark:#6A737D"> #13,  0             // InvokeDynamic #0:makeConcatWithConstants:(Ljava/lang/String;)Ljava/lang/String;</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">        15:</span><span style="color:#032F62;--s-dark:#9ECBFF"> astore_3</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">        16:</span><span style="color:#032F62;--s-dark:#9ECBFF"> getstatic</span><span style="color:#6A737D;--s-dark:#6A737D">     #17                 // Field java/lang/System.out:Ljava/io/PrintStream;</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">        19:</span><span style="color:#032F62;--s-dark:#9ECBFF"> aload_2</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">        20:</span><span style="color:#032F62;--s-dark:#9ECBFF"> aload_1</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">        21:</span><span style="color:#032F62;--s-dark:#9ECBFF"> if_acmpne</span><span style="color:#005CC5;--s-dark:#79B8FF">     28</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">        24:</span><span style="color:#032F62;--s-dark:#9ECBFF"> iconst_1</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">        25:</span><span style="color:#032F62;--s-dark:#9ECBFF"> goto</span><span style="color:#005CC5;--s-dark:#79B8FF">          29</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">        28:</span><span style="color:#032F62;--s-dark:#9ECBFF"> iconst_0</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">        29:</span><span style="color:#032F62;--s-dark:#9ECBFF"> invokevirtual</span><span style="color:#6A737D;--s-dark:#6A737D"> #23                 // Method java/io/PrintStream.println:(Z)V</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">        32:</span><span style="color:#032F62;--s-dark:#9ECBFF"> getstatic</span><span style="color:#6A737D;--s-dark:#6A737D">     #17                 // Field java/lang/System.out:Ljava/io/PrintStream;</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">        35:</span><span style="color:#032F62;--s-dark:#9ECBFF"> aload_3</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">        36:</span><span style="color:#032F62;--s-dark:#9ECBFF"> aload_1</span></span>
</code></pre></div><div class="code-collapse-gradient"></div><label class="code-collapse-expand" for="toggle-o66tb8z"><svg class="code-collapse-icon" width="32" height="24" viewBox="0 0 32 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="4,9 12,17 20,9"></polyline></svg></label></div><div class="code-collapse-full"><div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">  public</span><span style="color:#032F62;--s-dark:#9ECBFF"> void</span><span style="color:#032F62;--s-dark:#9ECBFF"> test</span><span style="color:#24292E;--s-dark:#E1E4E8">();</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">    descriptor:</span><span style="color:#24292E;--s-dark:#E1E4E8"> ()</span><span style="color:#6F42C1;--s-dark:#B392F0">V</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">    flags:</span><span style="color:#24292E;--s-dark:#E1E4E8"> (0x0001) ACC_PUBLIC</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">    Code:</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">      stack</span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#032F62;--s-dark:#9ECBFF">3,</span><span style="color:#24292E;--s-dark:#E1E4E8"> locals</span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#032F62;--s-dark:#9ECBFF">4,</span><span style="color:#24292E;--s-dark:#E1E4E8"> args_size</span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#032F62;--s-dark:#9ECBFF">1</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">         0:</span><span style="color:#032F62;--s-dark:#9ECBFF"> ldc</span><span style="color:#6A737D;--s-dark:#6A737D">           #7                  // String ab</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">         2:</span><span style="color:#032F62;--s-dark:#9ECBFF"> astore_1</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">         3:</span><span style="color:#032F62;--s-dark:#9ECBFF"> ldc</span><span style="color:#6A737D;--s-dark:#6A737D">           #7                  // String ab</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">         5:</span><span style="color:#032F62;--s-dark:#9ECBFF"> astore_2</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">         6:</span><span style="color:#032F62;--s-dark:#9ECBFF"> aload_0</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">         7:</span><span style="color:#032F62;--s-dark:#9ECBFF"> pop</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">         8:</span><span style="color:#032F62;--s-dark:#9ECBFF"> ldc</span><span style="color:#6A737D;--s-dark:#6A737D">           #11                 // String a</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">        10:</span><span style="color:#032F62;--s-dark:#9ECBFF"> invokedynamic</span><span style="color:#6A737D;--s-dark:#6A737D"> #13,  0             // InvokeDynamic #0:makeConcatWithConstants:(Ljava/lang/String;)Ljava/lang/String;</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">        15:</span><span style="color:#032F62;--s-dark:#9ECBFF"> astore_3</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">        16:</span><span style="color:#032F62;--s-dark:#9ECBFF"> getstatic</span><span style="color:#6A737D;--s-dark:#6A737D">     #17                 // Field java/lang/System.out:Ljava/io/PrintStream;</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">        19:</span><span style="color:#032F62;--s-dark:#9ECBFF"> aload_2</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">        20:</span><span style="color:#032F62;--s-dark:#9ECBFF"> aload_1</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">        21:</span><span style="color:#032F62;--s-dark:#9ECBFF"> if_acmpne</span><span style="color:#005CC5;--s-dark:#79B8FF">     28</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">        24:</span><span style="color:#032F62;--s-dark:#9ECBFF"> iconst_1</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">        25:</span><span style="color:#032F62;--s-dark:#9ECBFF"> goto</span><span style="color:#005CC5;--s-dark:#79B8FF">          29</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">        28:</span><span style="color:#032F62;--s-dark:#9ECBFF"> iconst_0</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">        29:</span><span style="color:#032F62;--s-dark:#9ECBFF"> invokevirtual</span><span style="color:#6A737D;--s-dark:#6A737D"> #23                 // Method java/io/PrintStream.println:(Z)V</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">        32:</span><span style="color:#032F62;--s-dark:#9ECBFF"> getstatic</span><span style="color:#6A737D;--s-dark:#6A737D">     #17                 // Field java/lang/System.out:Ljava/io/PrintStream;</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">        35:</span><span style="color:#032F62;--s-dark:#9ECBFF"> aload_3</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">        36:</span><span style="color:#032F62;--s-dark:#9ECBFF"> aload_1</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">        37:</span><span style="color:#032F62;--s-dark:#9ECBFF"> if_acmpne</span><span style="color:#005CC5;--s-dark:#79B8FF">     44</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">        40:</span><span style="color:#032F62;--s-dark:#9ECBFF"> iconst_1</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">        41:</span><span style="color:#032F62;--s-dark:#9ECBFF"> goto</span><span style="color:#005CC5;--s-dark:#79B8FF">          45</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">        44:</span><span style="color:#032F62;--s-dark:#9ECBFF"> iconst_0</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">        45:</span><span style="color:#032F62;--s-dark:#9ECBFF"> invokevirtual</span><span style="color:#6A737D;--s-dark:#6A737D"> #23                 // Method java/io/PrintStream.println:(Z)V</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">        48:</span><span style="color:#032F62;--s-dark:#9ECBFF"> return</span></span></code><label class="code-collapse-collapse" for="toggle-o66tb8z"><svg class="code-collapse-icon" width="32" height="24" viewBox="0 0 32 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20,15 12,7 4,15"></polyline></svg></label></pre></div></div></div>
<p>常量表中的 #7 为：</p>
<div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#6A737D;--s-dark:#6A737D">   #7 = String             #8             // ab</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">   #8 = Utf8               ab</span></span></code></pre></div>
<p>我们先来看第一个 true，这个大家应该基本都知道原因。编译器在将源码编译为 class 字节码文件时，会将当前类的方法中出现的 final 常量替换为字面量，于是 Java 代码第 6 行的 <code>String ab1 = s + "b"</code>; 就变为 <code>String ab1 = "a" + "b"</code>;，进一步，由于 ab1 是由两个字面量直接拼接的，编译器就直接帮其完成拼接，最终的结果，这条语句等价于 <code>String ab1 = "ab";</code>。于是 cmp 和 ab1 都指向常量池的"ab"字符串，所以 cmp == ab1。反汇编的字节码中，第 0 行和第 3 行是完全一样的，ldc（Load Constant）的参数都是 #7。</p>
<p>字节码的 8~15 行是准备字符串 ab2 的过程，可以看到这里执行了一个动态方法调用，调用的是方法 makeConcatWithConstants，这个方法是 Java 的一个引导方法，用于处理 Java 中对 String 进行“+”拼接。这个方法会在堆中创建一个新的字符串变量，这就是 ab2 != cmp 的原因。</p>
<p>BTW，makeConcatWithConstants 这个方法在 JDK 9 中被引入用于处理字符串“+”操作，在 JDK 8 之前，javac 一直是用 StringBuilder 类来处理的。</p>
<p>那么是什么导致了这个差异呢？显然，问题在这个 this 关键字上。Java 在编译时，会隐式在所有成员方法中添加一个指向当前实例的引用 this，这个 this 在字节码中是作为方法参数传递给方法的，这就是为什么 void() 方法没有参数，字节码第五行中 args_size 却等于 1。对于一个对象引用的变量（无论是类变量还是成员变量），Java 编译器仅仅是很粗暴地关闭了这个优化。如果将这里的 this.s 改为 Test.s，那么这里输出的结果就是 true 了。</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[《三体》中的“大多数人”]]></title>
            <link>https://blog.shinya.click/daily/people-in-three-body</link>
            <guid isPermaLink="false">https://blog.shinya.click/daily/people-in-three-body</guid>
            <pubDate>Sun, 10 Apr 2022 16:13:13 GMT</pubDate>
            <description><![CDATA[在《三体》中，刘慈欣通过浩瀚的历史叙述，探索了个体与集体之间的复杂关系。他对西式民主的质疑贯穿始终，反映出对大多数人自决命运的不安。书中众多英雄的努力与牺牲，最终却在历史的洪流中显得微不足道，彰显了个体在面对历史巨变时的无力感。故事从文化大革命的动荡开始，暗示着精英与平庸大众之间的深刻冲突，折射出对人性和社会的深刻思考。]]></description>
            <content:encoded><![CDATA[<blockquote>
<p>如果哪天中国搞民主了，那肯定是人间地狱，我肯定第二天就逃离中国。</p>
<p>——刘慈欣，2019 年接受《纽约客》采访</p>
</blockquote>
<p>大刘在很多场合都表达过他对西式民主的不信任对态度，对于让大多数人自决命运的一种悲观。大刘的很多小说所表现出的这种政治倾向是十分鲜明的。他的《三体》，也在无数次地传达他的理念。</p>
<p>整部《三体》描绘的地球往事编年史：从公元 1453 年到公元 18906416 年（银河纪元 18904136 年）再到 647 号小宇宙到第二年为止，这上下近两千万年的浩瀚历史，无不印证着这么一句话：</p>
<blockquote>
<p>人民群众是历史的创造者。</p>
</blockquote>
<p>当然，不是什么乐观的印证。</p>
<p>这两千万年，自然也出现过无数英雄豪杰——章北海、罗辑、云天明、托马斯维德……他们也都曾在关键时刻怒挽狂澜，无数次做出能够挽救全人类命运的举动，却最终都化为泡影。</p>
<p>很有趣的是，《三体》的故事是从文化大革命开始说起的，《三体一》的开篇，叶文洁的父亲叶哲泰就被红卫兵们批斗致死。我不知道这里大刘是否加了某种隐喻在里面，甚至直到写这篇读后感时，我才意识到，这里似乎是隐含了一种贯穿全书的很隐蔽的冲突：个别的精英人物对平庸的普罗大众的冲突，或者说，理性对感性的冲突。而叶哲泰的死，仿佛也暗合了这一系列冲突的结果：感性的胜利。书中的人类相较于三体人，很大的一个优势就是，他们会去爱，有更加丰富的情感，而不是冷冰冰的命令执行者，而类似于欺骗之类的其他特性，也可以说是情感的一种衍生品。这当然可以说是人类的幸运，但也注定了人类会走向的最终的悲剧。这不知道是不是一种过度解读，但是确实算得上一个巧合。</p>
<p>《三体》三部曲没有写成编年史的形式，而是每一部选取了一个或几个关键性的人物——第一部的汪淼、第二部的章北海和罗辑、第三部的程心，用很大的笔墨从这些人的视角，描绘整个大时代，很巧妙地用“休眠”这个特性，使得每个人物不仅仅可以参与一个很小范围（几十年）的事件，而能在一个更大的时间跨度上去参与历史。很可惜的是，很多小说花费大笔墨描写的、无数人的血汗换来的构思布局经营谋划，都被小说中一笔带过的休眠过程中所发生的种种情况，或是一些无足轻重（相对来说）的小事，轻描淡写地推翻破灭了，很有一种讽刺感。</p>
<p>人类最初和三体人对抗的情形，无疑是绝望的。智子对人类基础科技发展的锁死和对人类社会的监视，使得人类在与三体人的作战中不具有任何技术和战略上的优势。于是，人类利用自己<strong>唯一</strong>的优势：伪装和欺骗，开启了面壁计划。罗辑在领悟黑暗森林理论后将恒星 187J3X1 的坐标广播，并进入休眠等待理论的验证。然而罗辑没有在休眠中等到咒语被验证就被匆匆唤醒：太阳系舰队联席会议以全票通过的结果废除了面壁计划，于是人类丧失了唯一的优势。无数先人前赴后继耗资巨大的项目，因为那个时代人们的狂妄自大，完全化为了泡影。只有少数几个人意识到了不对劲，如丁仪：“我两个世纪前的人了，现在居然还能在大学里教物理。”</p>
<p>而面壁者雷迪亚兹的故事，就更为这种讽刺蒙上了一种悲情色彩。罗辑说：“四位面壁者都是伟大的，他们在战争一开始就看到了人类必败的结局。”然而不知道是否是大刘刻意安排的冲突，面壁计划就正好选中了这四颗世界上最聪明的脑袋，加上一个章北海，整部书中就只有这五个人对人类的未来具有最清晰最正确的认知。暂且不论这种安排的合理性，坚信人类必败的几人，最开始就知道他们只有两种选择，押上全部家底、摆出同归于尽的架势去威慑，或者逃亡主义。雷迪亚兹的计划，民众只聚焦在会毁灭地球这一个点上，而没有人去思考它的战略威慑意义。在计划败露后，雷迪亚兹回到了他热爱的祖国玻利维亚，被他亲爱的人民用石头砸死。雷迪亚兹死前在飞机上说：“人类生存的最大障碍其实来自自身”，这不仅是雷迪亚兹的悲剧的原因，也是整个人类悲剧的根源。雷迪亚兹的命运，有一种尼采的超人哲学的意味，而超人往往死于他们所保护的人。</p>
<p>不知是否是太阳系舰队全灭调高了人类的阈值，在罗辑继承了雷迪亚兹的计划，建立了类似的威慑系统后，面临着灭顶之灾的人类把他当作守护神。罗辑在威慑形成时与智子的对话中提到：“奇怪，我现在感觉自己不是人类的一员了”，在罗辑卸任执剑人时，人类甚至想以世界灭绝罪审判罗辑。然而，即使是罗辑几乎用命换来的威慑局面，也在区区六十年后，在有多名至少看起来是合格的候选人的情况下，被人类民主投票选出的新执剑人程心打破，人类在这场几十年的对峙中，率先放松了警惕。也诚然，相对于冷冰冰地、动动手指就能摧毁两个文明的罗辑，高颜值高学历亲和散发着母性光辉的程心，在这个女性化特质强烈的时代，显然是个比粗鲁的公元人更好的选择。</p>
<p>威慑失效后万有引力号代替程心发出了引力波广播，完成了威慑的最后一步，人类和三体人开始各自找寻存活的机会。托马斯维德建立的星环集团开始研究光速飞船，这项技术被联邦政府认定为逃亡主义而非法，人类害怕在死亡面前的不平等。维德唤醒了程心，于是星环集团被政府接管，维德也被处死。继承了章北海、希恩斯逃亡主义的最后一点尝试也宣告失败，人类最终难逃灭绝的命运，有远见有魄力的少数人最终覆灭于无远见的绝大多数人。</p>
<p>对《三体》的诸多评价，都痛斥程心为圣母婊，她的“圣母心”搞砸了两个可以挽救人类的计划。这自然是站在上帝视角的我们可以随意去评判的。但事实上，程心只是大刘描述的那个时代下，无数抽象的普通民众的一个具象化代表，换言之，是大刘专门立的一个靶子用来打的。不过程心算得上一位完美的圣母，程心的所作所为与所思所想是一致的，她的一切行为，都是心存善意的。在光粒打击误报时，她不愿意牺牲普通民众的性命换取她自己的存活。她的最大错误，也是认为心存善意就能解决一切问题。所以更严谨地说，程心是无数抽象的普通民众的善意的一个具象化代表，所以小说能展显的只是她的短视，而不是恶意。</p>
<p>水滴攻击地球前的混乱和广播纪元 8 年光粒打击的误报，则是整部小说中集中展现了人性的恶的一部分：人们为了不让假想的少数人逃离用激光手枪照射太空电梯，以及少数人为了逃离不顾其他人性命强行启动聚变发动机。无论是人性的善还是人性的恶，在大刘笔下似乎都没有带来什么很好的结果。</p>
<p>于是，托马斯维德说：“失去人性，失去很多；失去兽性，失去一切。”他只想“前进！不择手段地前进！”而民众往往不这么想，就像程心说的：“我选择人性。”他们看不到整体，只能看到眼前失去的东西。于是雷迪亚兹和维德都被人类处死了，泰勒被逼自尽，罗辑被妖魔化，程心成为执剑人，太阳系被二向箔拍成饼。大刘在暗戳戳地说：“你们这么人性，这就是下场。”</p>
<p>总的来说，在《三体》中，所谓的“民主”给我们带来了一个一个的笑话和恨铁不成钢感觉，让少数睿智但不懂得（或不屑于）讨好广大民众的人的呕心沥血的努力最终成了泡影，这算是对这种所谓的“民主社会”的一种很有感染力的讽刺。</p>
<p>可以联想到最近乌克兰的局势。当年乌克兰就是在民主的感召下，一步步地走到今天这步田地。在泽连斯基胜出的那场总统大选中，面对着有政治经验的波罗申科和毫无经验的喜剧演员，民众纷纷将票投给泽连斯基，大部分人的理由是他在他的喜剧中扮演了一名好总统。这是一个典型的例子：“西式”民主无法避免的政客明星化和民众的短视。大刘在另一部小说《中国 2185》中，设立了一位女主席，也是对“民主”下政客明星化的一个体现。</p>
<p>在《三体》的最后，在归零者要求归还小宇宙的质量以重启大宇宙时，虽然程心前脚还在说：“如果所有小宇宙中的人都那么想，那大宇宙肯定死了”，后脚就“还可以再留下五公斤吗，大宇宙不会因为这五公斤就不坍缩了”。最后是留了一个开放式的结局，没有说最终宇宙是否因为这些质量而死亡。小说自己也提到：“因为在那无数文明创造的无数小宇宙中，肯定有相当一部分不响应回归运动的号召”，所以最后宇宙很可能是陷入了沉寂，没能重启。大刘成功将人性的弱点扩展到了宇宙中不同文明的芸芸众生上。</p>
<p>有人认为大刘的黑暗森林理论是大刘对那场十年动乱中人与人之间关系的感悟，我不置可否。但是大刘笔下的人物，却展现出了人内在的神性、人性和兽性。神性体现在自我实现，如章北海、罗辑；人性体现在社会现实中人与人之间的妥协，即道德或者法律，如程心。而《三体》则直击兽性：生存永远是文明的第一要务。于是在黑暗森林中，人类因为人性得以存活，也最终因为不舍得抛弃人性陷入了毁灭。</p>
<p>人性没有错，大刘只是将血淋淋的现实抛了出来。他没有在讽刺谁，或者说，他讽刺了所有人。</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[6.5840 实验一 —— MapReduce]]></title>
            <link>https://blog.shinya.click/notes/65840/mapreducelab</link>
            <guid isPermaLink="false">https://blog.shinya.click/notes/65840/mapreducelab</guid>
            <pubDate>Thu, 20 Jan 2022 14:29:00 GMT</pubDate>
            <description><![CDATA[实验一的目标是实现一个 MapReduce 系统，分为 master 和 worker 两个核心部分。这个过程对 golang 的 RPC 和并发编程要求较高，同时需要深入理解 MapReduce 的流程。实验经历了两个版本的实现，从基于 mutex 锁的版本到更优雅的基于 channel 的无锁版本，后者的设计更加简洁明了。理解实验的关键在于认真阅读相关文档，特别是其中的流程图和说明。]]></description>
            <content:encoded><![CDATA[<h3 id="前言">前言<a href="#前言" class="heading-anchor-link" aria-label="Link to 前言"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h3>
<p>实验一是要实现一个 MapReduce 系统，基本就是两个部分：实现 master 程序和实现 worker 程序。这个实验基本就是劝退怪了，一来是对 golang 的 rpc 和并发的使用要比较熟悉，二来就是要对 MapReduce 的整个流程机制要比较熟悉。其实有一个小秘诀，就是拼命看论文中的这张图，再拼命看下面的流程讲解：</p>
<figure><img src="https://blog-img.774352199.xyz/2025/6f7e7839e6f09e0d8193d530920a6f7e.jpg" alt="mapReduce 执行流程" loading="lazy" decoding="async" style="background-image:url(data:image/webp;base64,UklGRkIAAABXRUJQVlA4IDYAAABwAQCdASoQAAsAAsBMJaVefAGIAAD+9mKECruaiOHkdw8ZHG461taLFLg1ra/3uqNqiL9YgAA=);background-size:cover;background-repeat:no-repeat"><figcaption>mapReduce 执行流程</figcaption></figure>
<p>这个实验我实现了两个版本，主要是并发控制的方式有些不同。最初是基于 mutex 锁的版本，后来重构成了基于 channel 的无锁版本。无锁版本的实现比较优雅，所以讲解也主要基于无锁版本。</p>
<h3 id="实验讲解">实验讲解<a href="#实验讲解" class="heading-anchor-link" aria-label="Link to 实验讲解"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h3>
<p>做实验之前，首先需要读懂实验。说明书在：<a href="https://pdos.csail.mit.edu/6.824/labs/lab-mr.html" target="_blank" rel="noopener noreferrer" data-umami-event="outbound-link-click" data-umami-event-url="https://pdos.csail.mit.edu/6.824/labs/lab-mr.html">https://pdos.csail.mit.edu/6.824/labs/lab-mr.html</a>。主要这个实验需要在 Linux 环境下进行，因为进程通信基于 unix socket，MacOS 原则上来说也可以，但是据说还是会有些小问题。</p>
<p>代码中已经提供了一个单线程串行版的 MapReduce，代码在 <code>src/main/mrsequential.go</code>。这个版本很重要，建议先阅读一遍，可以大致了解整体的流程。有一些内容的处理也可以直接从中 copy。</p>
<p>并行版本的 master 程序入口在 <code>main/mrcoordinator.go</code>，worker 程序入口在 <code>main/mrworker.go</code>。实验需要实现的有三个文件，分别是 <code>mr/coordinator.go</code>、<code>mr/worker.go</code>、和 <code>mr/rpc.go</code>，分别描述了 master 的处理代码、worker 的处理代码以及它们之间通信的 rpc 结构。</p>
<p>mrcoordinator 会调用 <code>mr/coordinator.go</code> 中的 MakeCoordinator 函数，来构建 master 的结构，并启动 socket 监听，在返回后，主协程会不断调用 Coordinator.Done 方法，来检查是否已经完成整个 MapReduce 任务，确认完成后才会退出主协程。所以，在 MakeCoordinator 中，不应当有操作阻止函数返回，否则会阻塞后续操作。<strong>相关的监听等工作应当通过新协程实现</strong>。</p>
<p>mrworker 的处理就很简单了，只有一个主协程，直接调用了 <code>mr/worker.go</code> 的 Worker 函数，在这里处理即可。一般可以直接实现成单协程程序。</p>
<p>测试脚本为 <code>src/main/test-mr.sh</code>，它会将两个现成的 MapReduce 程序：wc 和 indexer 通过你的框架执行，并与串行执行的结果相比较。它同时还会检查并行运行相同的 Map 或 Reduce 任务、甚至 worker 执行任务期间发生 crash 时，最终是否能得到正确的结构。通常它会启动一个 master 进程和三个 worker 进程。如果在运行期间发生错误不退出时，可以通过 <code>ps -A</code> 命令，找到 mrcoordinator 进程的 pid，并 kill 掉即可。普通的 <code>ctrl + c</code> 可能无法完全退出，会影响后续的测试。</p>
<p>最后，请多阅读几遍实验指导书。</p>
<h3 id="实现思路">实现思路<a href="#实现思路" class="heading-anchor-link" aria-label="Link to 实现思路"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h3>
<h4 id="整体流程">整体流程<a href="#整体流程" class="heading-anchor-link" aria-label="Link to 整体流程"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h4>
<p>workers 会首先执行完 map 任务，生成很多中间文件“mr-X-Y”，其中，X 是 map 任务的 id，Y 是对应的 reduce 任务 id。接着 reduce 会收集所有 Y 等于 reduce 任务 id 的文件，读取并进行 reduce 操作，并将结果输出到“mr-out-Y”中。</p>
<h4 id="master-实现">master 实现<a href="#master-实现" class="heading-anchor-link" aria-label="Link to master 实现"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h4>
<h5 id="无锁思路">无锁思路</h5>
<p>由于是一个无锁的实现，要避免多协程数据冲突，所有对主要数据结构的操作应当收敛到一个协程中，这里可以称为调度协程。在 worker 通过 rpc 请求 master 时，例如获取一个 task 或者汇报完成工作，master 会通过一个自动创建的协程处理 rpc 请求，由于对主要数据结构的操作已经收敛，这个 rpc 协程就必须通过 channel 要求调度协程代办，以保证没有数据竞争。由于 worker 和 master 之间可能有多种消息，这意味着调度协程必须同时管理多个 channel。这里可以运用 golang 的 select 结构：</p>
<div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#6A737D;--s-dark:#6A737D">// 只在这个 goroutine 中操作结构</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">func</span><span style="color:#24292E;--s-dark:#E1E4E8"> (</span><span style="color:#E36209;--s-dark:#FFAB70">c </span><span style="color:#D73A49;--s-dark:#F97583">*</span><span style="color:#6F42C1;--s-dark:#B392F0">Coordinator</span><span style="color:#24292E;--s-dark:#E1E4E8">) </span><span style="color:#6F42C1;--s-dark:#B392F0">schedule</span><span style="color:#24292E;--s-dark:#E1E4E8">() {</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">    for</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">        select</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">        case</span><span style="color:#24292E;--s-dark:#E1E4E8"> msg </span><span style="color:#D73A49;--s-dark:#F97583">:=</span><span style="color:#D73A49;--s-dark:#F97583"> &#x3C;-</span><span style="color:#24292E;--s-dark:#E1E4E8">c.getTaskChan:</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            c.</span><span style="color:#6F42C1;--s-dark:#B392F0">getTaskHandler</span><span style="color:#24292E;--s-dark:#E1E4E8">(msg)</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">        case</span><span style="color:#24292E;--s-dark:#E1E4E8"> msg </span><span style="color:#D73A49;--s-dark:#F97583">:=</span><span style="color:#D73A49;--s-dark:#F97583"> &#x3C;-</span><span style="color:#24292E;--s-dark:#E1E4E8">c.doneTaskChan:</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            c.</span><span style="color:#6F42C1;--s-dark:#B392F0">doneTaskHandler</span><span style="color:#24292E;--s-dark:#E1E4E8">(msg)</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">        case</span><span style="color:#24292E;--s-dark:#E1E4E8"> msg </span><span style="color:#D73A49;--s-dark:#F97583">:=</span><span style="color:#D73A49;--s-dark:#F97583"> &#x3C;-</span><span style="color:#24292E;--s-dark:#E1E4E8">c.timeoutChan:</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            c.</span><span style="color:#6F42C1;--s-dark:#B392F0">timeoutHandler</span><span style="color:#24292E;--s-dark:#E1E4E8">(msg)</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">        case</span><span style="color:#24292E;--s-dark:#E1E4E8"> msg </span><span style="color:#D73A49;--s-dark:#F97583">:=</span><span style="color:#D73A49;--s-dark:#F97583"> &#x3C;-</span><span style="color:#24292E;--s-dark:#E1E4E8">c.doneCheckChan:</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            c.</span><span style="color:#6F42C1;--s-dark:#B392F0">doneCheckHandler</span><span style="color:#24292E;--s-dark:#E1E4E8">(msg)</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        }</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">}</span></span></code></pre></div>
<p>假设这时候有一个 worker 需要获取一个 task 来执行，请求 master 的 GetTask，GetTask 处理如下：</p>
<div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--s-dark:#F97583">func</span><span style="color:#24292E;--s-dark:#E1E4E8"> (</span><span style="color:#E36209;--s-dark:#FFAB70">c </span><span style="color:#D73A49;--s-dark:#F97583">*</span><span style="color:#6F42C1;--s-dark:#B392F0">Coordinator</span><span style="color:#24292E;--s-dark:#E1E4E8">) </span><span style="color:#6F42C1;--s-dark:#B392F0">GetTask</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#E36209;--s-dark:#FFAB70">_</span><span style="color:#D73A49;--s-dark:#F97583"> *</span><span style="color:#6F42C1;--s-dark:#B392F0">GetTaskReq</span><span style="color:#24292E;--s-dark:#E1E4E8">, </span><span style="color:#E36209;--s-dark:#FFAB70">resp</span><span style="color:#D73A49;--s-dark:#F97583"> *</span><span style="color:#6F42C1;--s-dark:#B392F0">GetTaskResp</span><span style="color:#24292E;--s-dark:#E1E4E8">) </span><span style="color:#D73A49;--s-dark:#F97583">error</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    msg </span><span style="color:#D73A49;--s-dark:#F97583">:=</span><span style="color:#6F42C1;--s-dark:#B392F0"> GetTaskMsg</span><span style="color:#24292E;--s-dark:#E1E4E8">{</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        resp: resp,</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        ok:   </span><span style="color:#6F42C1;--s-dark:#B392F0">make</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#D73A49;--s-dark:#F97583">chan</span><span style="color:#D73A49;--s-dark:#F97583"> struct</span><span style="color:#24292E;--s-dark:#E1E4E8">{}),</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    c.getTaskChan </span><span style="color:#D73A49;--s-dark:#F97583">&#x3C;-</span><span style="color:#24292E;--s-dark:#E1E4E8"> msg</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">    &#x3C;-</span><span style="color:#24292E;--s-dark:#E1E4E8">msg.ok</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">    return</span><span style="color:#005CC5;--s-dark:#79B8FF"> nil</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">}</span></span></code></pre></div>
<p>在向 getTaskChan 中，不止传入了 resp（getTask 不需要请求参数），还传入了一个 chan struct{} 类型的管道，这个管道是协调协程用于通知 rpc 协程处理完成的通道：当处理完成后，就会向 msg.ok 中写入一个 struct{}，rpc 协程就会返回。</p>
<h5 id="coordinator">Coordinator</h5>
<p>整个 Coordinator 结构如下：</p>
<div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--s-dark:#F97583">type</span><span style="color:#6F42C1;--s-dark:#B392F0"> Coordinator</span><span style="color:#D73A49;--s-dark:#F97583"> struct</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    nMap    </span><span style="color:#D73A49;--s-dark:#F97583">int</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    nReduce </span><span style="color:#D73A49;--s-dark:#F97583">int</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    phase   </span><span style="color:#6F42C1;--s-dark:#B392F0">TaskPhase</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    allDone </span><span style="color:#D73A49;--s-dark:#F97583">bool</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8"> </span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    taskTimeOut </span><span style="color:#D73A49;--s-dark:#F97583">map</span><span style="color:#24292E;--s-dark:#E1E4E8">[</span><span style="color:#D73A49;--s-dark:#F97583">int</span><span style="color:#24292E;--s-dark:#E1E4E8">]</span><span style="color:#6F42C1;--s-dark:#B392F0">time</span><span style="color:#24292E;--s-dark:#E1E4E8">.</span><span style="color:#6F42C1;--s-dark:#B392F0">Time</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    tasks       []</span><span style="color:#D73A49;--s-dark:#F97583">*</span><span style="color:#6F42C1;--s-dark:#B392F0">Task</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8"> </span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    getTaskChan   </span><span style="color:#D73A49;--s-dark:#F97583">chan</span><span style="color:#6F42C1;--s-dark:#B392F0"> GetTaskMsg</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    doneTaskChan  </span><span style="color:#D73A49;--s-dark:#F97583">chan</span><span style="color:#6F42C1;--s-dark:#B392F0"> DoneTaskMsg</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    doneCheckChan </span><span style="color:#D73A49;--s-dark:#F97583">chan</span><span style="color:#6F42C1;--s-dark:#B392F0"> DoneCheckMsg</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    timeoutChan   </span><span style="color:#D73A49;--s-dark:#F97583">chan</span><span style="color:#6F42C1;--s-dark:#B392F0"> TimeoutMsg</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">}</span></span></code></pre></div>
<p>phase 记录了当前任务执行的阶段，由于 reduce 任务必须在所有 map 任务结束后才能进行，所以 TaskPhase 分为 Map 和 Reduce 阶段，每个阶段中，tasks 切片只有对应阶段的任务。</p>
<p>taskTimeOut 记录了当前正在执行的任务的开始时间，会有一个协程定时去扫描这个 map，找出其中运行时间大于十秒的任务（超时），将对应的任务状态设置为未开始，以进行下一次调度。当然这个扫描操作也需要通过协调协程进行。超时 map 中也只有当前阶段正在执行的任务，在切换阶段时会清空超时 map。</p>
<p>tasks 切片保存了当前阶段所有的 Task，以及相关的状态：</p>
<div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--s-dark:#F97583">type</span><span style="color:#6F42C1;--s-dark:#B392F0"> ReduceTask</span><span style="color:#D73A49;--s-dark:#F97583"> struct</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    NMap </span><span style="color:#D73A49;--s-dark:#F97583">int</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">}</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8"> </span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">type</span><span style="color:#6F42C1;--s-dark:#B392F0"> MapTask</span><span style="color:#D73A49;--s-dark:#F97583"> struct</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    FileName </span><span style="color:#D73A49;--s-dark:#F97583">string</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    NReduce  </span><span style="color:#D73A49;--s-dark:#F97583">int</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">}</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8"> </span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">type</span><span style="color:#6F42C1;--s-dark:#B392F0"> TaskStatus</span><span style="color:#D73A49;--s-dark:#F97583"> int</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8"> </span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">var</span><span style="color:#24292E;--s-dark:#E1E4E8"> (</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    TaskStatus_Idle     </span><span style="color:#6F42C1;--s-dark:#B392F0">TaskStatus</span><span style="color:#D73A49;--s-dark:#F97583"> =</span><span style="color:#005CC5;--s-dark:#79B8FF"> 0</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    TaskStatus_Running  </span><span style="color:#6F42C1;--s-dark:#B392F0">TaskStatus</span><span style="color:#D73A49;--s-dark:#F97583"> =</span><span style="color:#005CC5;--s-dark:#79B8FF"> 1</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    TaskStatus_Finished </span><span style="color:#6F42C1;--s-dark:#B392F0">TaskStatus</span><span style="color:#D73A49;--s-dark:#F97583"> =</span><span style="color:#005CC5;--s-dark:#79B8FF"> 2</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">)</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8"> </span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">type</span><span style="color:#6F42C1;--s-dark:#B392F0"> Task</span><span style="color:#D73A49;--s-dark:#F97583"> struct</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    TaskId     </span><span style="color:#D73A49;--s-dark:#F97583">int</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    MapTask    </span><span style="color:#6F42C1;--s-dark:#B392F0">MapTask</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    ReduceTask </span><span style="color:#6F42C1;--s-dark:#B392F0">ReduceTask</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    TaskStatus </span><span style="color:#6F42C1;--s-dark:#B392F0">TaskStatus</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">}</span></span></code></pre></div>
<p>这里可以看到任务的状态被分成三个，分别是待执行、执行中以及执行完成。同时冗余保存了 MapTask 和 ReduceTask，具体使用哪个结构体由当前阶段来判断。</p>
<h5 id="具体操作">具体操作</h5>
<p>根据 Coordinator 中的管道，可以看出有四种情况需要和协调协程通信以进行操作。</p>
<p>当 worker 请求一个任务时，可能获取到的任务类别有四种：</p>
<div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--s-dark:#F97583">type</span><span style="color:#6F42C1;--s-dark:#B392F0"> TaskType</span><span style="color:#D73A49;--s-dark:#F97583"> int</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8"> </span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">var</span><span style="color:#24292E;--s-dark:#E1E4E8"> (</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    TaskType_Map    </span><span style="color:#6F42C1;--s-dark:#B392F0">TaskType</span><span style="color:#D73A49;--s-dark:#F97583"> =</span><span style="color:#005CC5;--s-dark:#79B8FF"> 0</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    TaskType_Reduce </span><span style="color:#6F42C1;--s-dark:#B392F0">TaskType</span><span style="color:#D73A49;--s-dark:#F97583"> =</span><span style="color:#005CC5;--s-dark:#79B8FF"> 1</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    TaskType_Wait   </span><span style="color:#6F42C1;--s-dark:#B392F0">TaskType</span><span style="color:#D73A49;--s-dark:#F97583"> =</span><span style="color:#005CC5;--s-dark:#79B8FF"> 2</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    TaskType_Exit   </span><span style="color:#6F42C1;--s-dark:#B392F0">TaskType</span><span style="color:#D73A49;--s-dark:#F97583"> =</span><span style="color:#005CC5;--s-dark:#79B8FF"> 3</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">)</span></span></code></pre></div>
<p>master 首先遍历所有的 tasks，找出其中的状态为未执行的状态，并根据当前的阶段，返回 Map 或者 Reduce 任务。如果当前没有空闲任务的话，又分为以下两种情况。当前为 Map 阶段，这时需要返回 TaskType_Wait 任务，要求 worker 等待，Map 阶段结束后还需要进行 Reduce 任务；当前为 Reduce 阶段，这时所有任务已经完成，返回 TaskType_Exit 要求 worker 退出。</p>
<p>当 worker 完成时，会通知 master 任务完成。传递的信息中会带有任务的类型和任务的 Id。master 会忽略掉非当前阶段的任务，根据 taskId 修改 tasks 中的任务状态为 finished（忽略当前任务状态，直接改为完成），并删除 timeout 中的对应结构。</p>
<div class="code-collapse-wrapper"><input type="checkbox" id="toggle-392c1qb" class="code-collapse-toggle" style="display: none;"><div class="code-collapse-preview"><div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0" class="code-preview"><code><span class="line"><span style="color:#D73A49;--s-dark:#F97583">func</span><span style="color:#24292E;--s-dark:#E1E4E8"> (</span><span style="color:#E36209;--s-dark:#FFAB70">c </span><span style="color:#D73A49;--s-dark:#F97583">*</span><span style="color:#6F42C1;--s-dark:#B392F0">Coordinator</span><span style="color:#24292E;--s-dark:#E1E4E8">) </span><span style="color:#6F42C1;--s-dark:#B392F0">doneTaskHandler</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#E36209;--s-dark:#FFAB70">msg</span><span style="color:#6F42C1;--s-dark:#B392F0"> DoneTaskMsg</span><span style="color:#24292E;--s-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    req </span><span style="color:#D73A49;--s-dark:#F97583">:=</span><span style="color:#24292E;--s-dark:#E1E4E8"> msg.req</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">    if</span><span style="color:#24292E;--s-dark:#E1E4E8"> req.TaskType </span><span style="color:#D73A49;--s-dark:#F97583">==</span><span style="color:#24292E;--s-dark:#E1E4E8"> TaskType_Map </span><span style="color:#D73A49;--s-dark:#F97583">&#x26;&#x26;</span><span style="color:#24292E;--s-dark:#E1E4E8"> c.phase </span><span style="color:#D73A49;--s-dark:#F97583">==</span><span style="color:#24292E;--s-dark:#E1E4E8"> TaskPhase_Reduce {</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">        // 提交非当前阶段的任务，直接返回</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        msg.ok </span><span style="color:#D73A49;--s-dark:#F97583">&#x3C;-</span><span style="color:#D73A49;--s-dark:#F97583"> struct</span><span style="color:#24292E;--s-dark:#E1E4E8">{}{}</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">        return</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">    for</span><span style="color:#24292E;--s-dark:#E1E4E8"> _, task </span><span style="color:#D73A49;--s-dark:#F97583">:=</span><span style="color:#D73A49;--s-dark:#F97583"> range</span><span style="color:#24292E;--s-dark:#E1E4E8"> c.tasks {</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">        if</span><span style="color:#24292E;--s-dark:#E1E4E8"> task.TaskId </span><span style="color:#D73A49;--s-dark:#F97583">==</span><span style="color:#24292E;--s-dark:#E1E4E8"> req.TaskId {</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">            // 无论当前状态，直接改为完成</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            task.TaskStatus </span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#24292E;--s-dark:#E1E4E8"> TaskStatus_Finished</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">            break</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        }</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">    // 删除 timeout 结构</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">    delete</span><span style="color:#24292E;--s-dark:#E1E4E8">(c.taskTimeOut, req.TaskId)</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    allDone </span><span style="color:#D73A49;--s-dark:#F97583">:=</span><span style="color:#005CC5;--s-dark:#79B8FF"> true</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">    for</span><span style="color:#24292E;--s-dark:#E1E4E8"> _, task </span><span style="color:#D73A49;--s-dark:#F97583">:=</span><span style="color:#D73A49;--s-dark:#F97583"> range</span><span style="color:#24292E;--s-dark:#E1E4E8"> c.tasks {</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">        if</span><span style="color:#24292E;--s-dark:#E1E4E8"> task.TaskStatus </span><span style="color:#D73A49;--s-dark:#F97583">!=</span><span style="color:#24292E;--s-dark:#E1E4E8"> TaskStatus_Finished {</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            allDone </span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#005CC5;--s-dark:#79B8FF"> false</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">            break</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        }</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">    if</span><span style="color:#24292E;--s-dark:#E1E4E8"> allDone {</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">        if</span><span style="color:#24292E;--s-dark:#E1E4E8"> c.phase </span><span style="color:#D73A49;--s-dark:#F97583">==</span><span style="color:#24292E;--s-dark:#E1E4E8"> TaskPhase_Map {</span></span>
</code></pre></div><div class="code-collapse-gradient"></div><label class="code-collapse-expand" for="toggle-392c1qb"><svg class="code-collapse-icon" width="32" height="24" viewBox="0 0 32 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="4,9 12,17 20,9"></polyline></svg></label></div><div class="code-collapse-full"><div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--s-dark:#F97583">func</span><span style="color:#24292E;--s-dark:#E1E4E8"> (</span><span style="color:#E36209;--s-dark:#FFAB70">c </span><span style="color:#D73A49;--s-dark:#F97583">*</span><span style="color:#6F42C1;--s-dark:#B392F0">Coordinator</span><span style="color:#24292E;--s-dark:#E1E4E8">) </span><span style="color:#6F42C1;--s-dark:#B392F0">doneTaskHandler</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#E36209;--s-dark:#FFAB70">msg</span><span style="color:#6F42C1;--s-dark:#B392F0"> DoneTaskMsg</span><span style="color:#24292E;--s-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    req </span><span style="color:#D73A49;--s-dark:#F97583">:=</span><span style="color:#24292E;--s-dark:#E1E4E8"> msg.req</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">    if</span><span style="color:#24292E;--s-dark:#E1E4E8"> req.TaskType </span><span style="color:#D73A49;--s-dark:#F97583">==</span><span style="color:#24292E;--s-dark:#E1E4E8"> TaskType_Map </span><span style="color:#D73A49;--s-dark:#F97583">&#x26;&#x26;</span><span style="color:#24292E;--s-dark:#E1E4E8"> c.phase </span><span style="color:#D73A49;--s-dark:#F97583">==</span><span style="color:#24292E;--s-dark:#E1E4E8"> TaskPhase_Reduce {</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">        // 提交非当前阶段的任务，直接返回</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        msg.ok </span><span style="color:#D73A49;--s-dark:#F97583">&#x3C;-</span><span style="color:#D73A49;--s-dark:#F97583"> struct</span><span style="color:#24292E;--s-dark:#E1E4E8">{}{}</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">        return</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">    for</span><span style="color:#24292E;--s-dark:#E1E4E8"> _, task </span><span style="color:#D73A49;--s-dark:#F97583">:=</span><span style="color:#D73A49;--s-dark:#F97583"> range</span><span style="color:#24292E;--s-dark:#E1E4E8"> c.tasks {</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">        if</span><span style="color:#24292E;--s-dark:#E1E4E8"> task.TaskId </span><span style="color:#D73A49;--s-dark:#F97583">==</span><span style="color:#24292E;--s-dark:#E1E4E8"> req.TaskId {</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">            // 无论当前状态，直接改为完成</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            task.TaskStatus </span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#24292E;--s-dark:#E1E4E8"> TaskStatus_Finished</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">            break</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        }</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">    // 删除 timeout 结构</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">    delete</span><span style="color:#24292E;--s-dark:#E1E4E8">(c.taskTimeOut, req.TaskId)</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    allDone </span><span style="color:#D73A49;--s-dark:#F97583">:=</span><span style="color:#005CC5;--s-dark:#79B8FF"> true</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">    for</span><span style="color:#24292E;--s-dark:#E1E4E8"> _, task </span><span style="color:#D73A49;--s-dark:#F97583">:=</span><span style="color:#D73A49;--s-dark:#F97583"> range</span><span style="color:#24292E;--s-dark:#E1E4E8"> c.tasks {</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">        if</span><span style="color:#24292E;--s-dark:#E1E4E8"> task.TaskStatus </span><span style="color:#D73A49;--s-dark:#F97583">!=</span><span style="color:#24292E;--s-dark:#E1E4E8"> TaskStatus_Finished {</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            allDone </span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#005CC5;--s-dark:#79B8FF"> false</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">            break</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        }</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">    if</span><span style="color:#24292E;--s-dark:#E1E4E8"> allDone {</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">        if</span><span style="color:#24292E;--s-dark:#E1E4E8"> c.phase </span><span style="color:#D73A49;--s-dark:#F97583">==</span><span style="color:#24292E;--s-dark:#E1E4E8"> TaskPhase_Map {</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            c.</span><span style="color:#6F42C1;--s-dark:#B392F0">initReducePhase</span><span style="color:#24292E;--s-dark:#E1E4E8">()</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        } </span><span style="color:#D73A49;--s-dark:#F97583">else</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            c.allDone </span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#005CC5;--s-dark:#79B8FF"> true</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        }</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    msg.ok </span><span style="color:#D73A49;--s-dark:#F97583">&#x3C;-</span><span style="color:#D73A49;--s-dark:#F97583"> struct</span><span style="color:#24292E;--s-dark:#E1E4E8">{}{}</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">}</span></span></code><label class="code-collapse-collapse" for="toggle-392c1qb"><svg class="code-collapse-icon" width="32" height="24" viewBox="0 0 32 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20,15 12,7 4,15"></polyline></svg></label></pre></div></div></div>
<p>如果是在 Reduce 阶段发现所有任务都完成了，还会设置一下 allDone 标志位。</p>
<p>Coordinator 在初始化时，还会启动一个协程，这个协程每秒请求一次协调协程，检查 timeoutMap 是否有超时任务，如果超时，就将其状态置为未开始，这样在下一次 worker 请求任务时就可以调度执行了。</p>
<div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--s-dark:#F97583">func</span><span style="color:#24292E;--s-dark:#E1E4E8"> (</span><span style="color:#E36209;--s-dark:#FFAB70">c </span><span style="color:#D73A49;--s-dark:#F97583">*</span><span style="color:#6F42C1;--s-dark:#B392F0">Coordinator</span><span style="color:#24292E;--s-dark:#E1E4E8">) </span><span style="color:#6F42C1;--s-dark:#B392F0">timeoutHandler</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#E36209;--s-dark:#FFAB70">msg</span><span style="color:#6F42C1;--s-dark:#B392F0"> TimeoutMsg</span><span style="color:#24292E;--s-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    now </span><span style="color:#D73A49;--s-dark:#F97583">:=</span><span style="color:#24292E;--s-dark:#E1E4E8"> time.</span><span style="color:#6F42C1;--s-dark:#B392F0">Now</span><span style="color:#24292E;--s-dark:#E1E4E8">()</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">    for</span><span style="color:#24292E;--s-dark:#E1E4E8"> taskId, start </span><span style="color:#D73A49;--s-dark:#F97583">:=</span><span style="color:#D73A49;--s-dark:#F97583"> range</span><span style="color:#24292E;--s-dark:#E1E4E8"> c.taskTimeOut {</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">        if</span><span style="color:#24292E;--s-dark:#E1E4E8"> now.</span><span style="color:#6F42C1;--s-dark:#B392F0">Sub</span><span style="color:#24292E;--s-dark:#E1E4E8">(start).</span><span style="color:#6F42C1;--s-dark:#B392F0">Seconds</span><span style="color:#24292E;--s-dark:#E1E4E8">() </span><span style="color:#D73A49;--s-dark:#F97583">></span><span style="color:#005CC5;--s-dark:#79B8FF"> 10</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">            for</span><span style="color:#24292E;--s-dark:#E1E4E8"> _, task </span><span style="color:#D73A49;--s-dark:#F97583">:=</span><span style="color:#D73A49;--s-dark:#F97583"> range</span><span style="color:#24292E;--s-dark:#E1E4E8"> c.tasks {</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">                if</span><span style="color:#24292E;--s-dark:#E1E4E8"> taskId </span><span style="color:#D73A49;--s-dark:#F97583">==</span><span style="color:#24292E;--s-dark:#E1E4E8"> task.TaskId {</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">                    if</span><span style="color:#24292E;--s-dark:#E1E4E8"> task.TaskStatus </span><span style="color:#D73A49;--s-dark:#F97583">!=</span><span style="color:#24292E;--s-dark:#E1E4E8"> TaskStatus_Finished {</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">                        task.TaskStatus </span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#24292E;--s-dark:#E1E4E8"> TaskStatus_Idle</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">                    }</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">                    break</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">                }</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            }</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">            delete</span><span style="color:#24292E;--s-dark:#E1E4E8">(c.taskTimeOut, taskId)</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">            break</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        }</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    msg.ok </span><span style="color:#D73A49;--s-dark:#F97583">&#x3C;-</span><span style="color:#D73A49;--s-dark:#F97583"> struct</span><span style="color:#24292E;--s-dark:#E1E4E8">{}{}</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">    return</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">}</span></span></code></pre></div>
<p>最后还有一个完成状态检查，是主线程调用 Coordinator.Done 进行的，请求协调协程时，只需要检查 allDone 标志位即可。</p>
<h4 id="worker">worker<a href="#worker" class="heading-anchor-link" aria-label="Link to worker"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h4>
<p>worker 只有单个协程，循环从 master 处获取任务执行：</p>
<div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--s-dark:#F97583">func</span><span style="color:#6F42C1;--s-dark:#B392F0"> Worker</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#E36209;--s-dark:#FFAB70">mapf</span><span style="color:#D73A49;--s-dark:#F97583"> func</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#D73A49;--s-dark:#F97583">string</span><span style="color:#24292E;--s-dark:#E1E4E8">, </span><span style="color:#D73A49;--s-dark:#F97583">string</span><span style="color:#24292E;--s-dark:#E1E4E8">) []</span><span style="color:#6F42C1;--s-dark:#B392F0">KeyValue</span><span style="color:#24292E;--s-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#E36209;--s-dark:#FFAB70">    reducef</span><span style="color:#D73A49;--s-dark:#F97583"> func</span><span style="color:#24292E;--s-dark:#E1E4E8">(</span><span style="color:#D73A49;--s-dark:#F97583">string</span><span style="color:#24292E;--s-dark:#E1E4E8">, []</span><span style="color:#D73A49;--s-dark:#F97583">string</span><span style="color:#24292E;--s-dark:#E1E4E8">) </span><span style="color:#D73A49;--s-dark:#F97583">string</span><span style="color:#24292E;--s-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">    for</span><span style="color:#24292E;--s-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        resp </span><span style="color:#D73A49;--s-dark:#F97583">:=</span><span style="color:#6F42C1;--s-dark:#B392F0"> callGetTask</span><span style="color:#24292E;--s-dark:#E1E4E8">()</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">        switch</span><span style="color:#24292E;--s-dark:#E1E4E8"> resp.TaskType {</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">        case</span><span style="color:#24292E;--s-dark:#E1E4E8"> TaskType_Map:</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">            handleMapTask</span><span style="color:#24292E;--s-dark:#E1E4E8">(resp.Task, mapf)</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">        case</span><span style="color:#24292E;--s-dark:#E1E4E8"> TaskType_Reduce:</span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">            handleReduceTask</span><span style="color:#24292E;--s-dark:#E1E4E8">(resp.Task, reducef)</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">        case</span><span style="color:#24292E;--s-dark:#E1E4E8"> TaskType_Wait:</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">            time.</span><span style="color:#6F42C1;--s-dark:#B392F0">Sleep</span><span style="color:#24292E;--s-dark:#E1E4E8">(time.Second)</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">        case</span><span style="color:#24292E;--s-dark:#E1E4E8"> TaskType_Exit:</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">            return</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        }</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">}</span></span></code></pre></div>
<p>map 和 reduce 的操作，可以参考串行单线程的实现。有一点注意是，由于可能有多个进程同时执行同一个任务，也可能会出现执行到一半崩溃的情况，遗留下的文件可能会导致后续 worker 重新执行时发生错误。所以创建输出文件时，可以通过 ioutil.TempFile 函数创建一个临时文件写入，等到写入完成后通过 os.Rename 重命名为目标文件，这样即可保证最后的输出文件一定是完整的。</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[MapReduce 论文阅读]]></title>
            <link>https://blog.shinya.click/notes/65840/mapreducepaper</link>
            <guid isPermaLink="false">https://blog.shinya.click/notes/65840/mapreducepaper</guid>
            <pubDate>Sun, 16 Jan 2022 09:32:00 GMT</pubDate>
            <description><![CDATA[MapReduce 是一种高效的并行计算模型，旨在简化大规模数据集的处理。通过定义 Map 和 Reduce 两个关键函数，用户能轻松地将复杂任务分解为简单的操作。该模型的架构能够自动管理数据分发和任务调度，使得开发者可以专注于算法本身，而无需过多关注底层细节。这种方法在分布式系统中的广泛应用，展现了其强大的灵活性和实用性。]]></description>
            <content:encoded><![CDATA[<h3 id="前言">前言<a href="#前言" class="heading-anchor-link" aria-label="Link to 前言"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h3>
<p>MapReduce，是 Google 早年提出了一种软件架构模型，支持大规模数据集的并行运算。现在这个概念被运用在大量分布式系统中。</p>
<p>相关的理论由 Google 在 2004 年发表在论文《MapReduce: Simplified Data Processing on Large Clusters》中，可以在 <a href="https://static.googleusercontent.com/media/research.google.com/zh-CN//archive/mapreduce-osdi04.pdf" target="_blank" rel="noopener noreferrer" data-umami-event="outbound-link-click" data-umami-event-url="https://static.googleusercontent.com/media/research.google.com/zh-CN//archive/mapreduce-osdi04.pdf">这里</a> 阅读全文。13 页的小论文，信息密度比某些小论文不知道高到哪里去了。</p>
<p>由于本文是边阅读论文边记录下来的笔记，所以内容可能比较混乱。</p>
<h3 id="编程模型">编程模型<a href="#编程模型" class="heading-anchor-link" aria-label="Link to 编程模型"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h3>
<p>MapReduce 是一个很简单的并行处理模型，使用 MapReduce 框架，用户只需要指定两个函数：</p>
<ul>
<li>Map 函数，负责将一个键值对处理成一系列<strong>中间</strong>键值对</li>
<li>Reduce 函数，负责将所有具有相同 key 的中间值合并</li>
</ul>
<p>剩下的，就由框架自行处理，包括数据分发、任务分发、错误处理、负载均衡等等细节。用户无需掌握这些细节，更能关注于业务逻辑。</p>
<p>一个大致的处理流程是这样的：</p>
<p>Map 接受一个输入键值对，产生一系列中间键值对。MapReduce 框架将所有具有相同的中间 key 的中间值组织到一起，传递给 Reduce 函数。Reduce 函数，接收一个中间 key 和一系列中间值，函数通常将这些值聚合成一个较小的集合，有时每次 Reduce 函数调用只会产生一个结果值，甚至不产生结果。</p>
<p>以大规模文本单词计数为例：</p>
<div class="code-block-wrapper"><button class="code-copy-button" type="button" aria-label="Copy code"><svg class="icon-copy" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"></path><path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"></path></svg><svg class="icon-check" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"></path></svg></button><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--s-dark-bg:#24292e;color:#24292e;--s-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">map</span><span style="color:#24292E;--s-dark:#E1E4E8">(String </span><span style="color:#E36209;--s-dark:#FFAB70">key</span><span style="color:#24292E;--s-dark:#E1E4E8">, String </span><span style="color:#E36209;--s-dark:#FFAB70">value</span><span style="color:#24292E;--s-dark:#E1E4E8">):</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">    // key：文章名称</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">    // value：文章内容</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">    for</span><span style="color:#24292E;--s-dark:#E1E4E8"> 单词 w in value:</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        增加中间计数 (w, </span><span style="color:#032F62;--s-dark:#9ECBFF">"1"</span><span style="color:#24292E;--s-dark:#E1E4E8">)</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8"> </span></span>
<span class="line"><span style="color:#6F42C1;--s-dark:#B392F0">reduce</span><span style="color:#24292E;--s-dark:#E1E4E8">(String </span><span style="color:#E36209;--s-dark:#FFAB70">key</span><span style="color:#24292E;--s-dark:#E1E4E8">, Iterator </span><span style="color:#E36209;--s-dark:#FFAB70">values</span><span style="color:#24292E;--s-dark:#E1E4E8">):</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">    // key：一个单词</span></span>
<span class="line"><span style="color:#6A737D;--s-dark:#6A737D">    // value：一系列计数</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">    int</span><span style="color:#24292E;--s-dark:#E1E4E8"> result </span><span style="color:#D73A49;--s-dark:#F97583">=</span><span style="color:#005CC5;--s-dark:#79B8FF"> 0</span><span style="color:#24292E;--s-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#D73A49;--s-dark:#F97583">    for</span><span style="color:#24292E;--s-dark:#E1E4E8"> v in values:</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">        result </span><span style="color:#D73A49;--s-dark:#F97583">+=</span><span style="color:#6F42C1;--s-dark:#B392F0"> ParseInt</span><span style="color:#24292E;--s-dark:#E1E4E8">(v);</span></span>
<span class="line"><span style="color:#24292E;--s-dark:#E1E4E8">    输出 (</span><span style="color:#6F42C1;--s-dark:#B392F0">ToString</span><span style="color:#24292E;--s-dark:#E1E4E8">(result))</span></span></code></pre></div>
<h3 id="实现">实现<a href="#实现" class="heading-anchor-link" aria-label="Link to 实现"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h3>
<h4 id="执行流程">执行流程<a href="#执行流程" class="heading-anchor-link" aria-label="Link to 执行流程"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h4>
<p>MapReduce 作为一种编程模型或者说编程思想，实现方式可以有很多。Google 在论文中给出了一种实现方法，用于局域网内互相连接的大量机器。执行流程如下图：</p>
<figure><img src="https://blog-img.774352199.xyz/2025/6f7e7839e6f09e0d8193d530920a6f7e.jpg" alt="mapReduce 执行流程" loading="lazy" decoding="async" style="background-image:url(data:image/webp;base64,UklGRkIAAABXRUJQVlA4IDYAAABwAQCdASoQAAsAAsBMJaVefAGIAAD+9mKECruaiOHkdw8ZHG461taLFLg1ra/3uqNqiL9YgAA=);background-size:cover;background-repeat:no-repeat"><figcaption>mapReduce 执行流程</figcaption></figure>
<ol>
<li>MapReduce 框架首先将输入文件划分为 M 片，每片通常为 16MB 到 64MB 大小。随后会启动集群中的机器（进程）。</li>
<li>集群中的一个进程是一个特殊的 master 进程。剩余的 worker 进程都由 master 分配任务。一共有 M 个 map 任务和 R 个 reduce 任务需要分配。master 会挑选空闲的 worker，一次分配一个 map 任务或者一个 reduce 任务。</li>
<li>被分配到 map 任务的 worker 读入对应分片的输入，从输入中解析出键值对，并分别将其传给用户定义的 map 函数。map 函数返回的中间键值对会被暂时缓存在内存里。</li>
<li>worker 内存中缓存的键值对，会被分片函数分成 R 个分片，并周期性地写进本地磁盘。这些键值对在磁盘上的位置会被发生给 master，master 负责将位置发送给被分配到 reduce 任务的 worker。</li>
<li>当一个 reduce worker 接收到 master 发送的这些位置，它会向保存这些内容的 map worker 发送 RPC 请求来读取这些内容。当一个 reducer worker 读取完所有的中间数据，就会将其根据 key 进行排序，这样所有相同 key 的数据就会聚合在一起。这种排序是必要的，因为通常许多不同的 key 会由同一个 reduce 任务处理。如果数据过大，可能会使用外部排序。</li>
<li>reduce worker 遍历有序的中间数据，对遇到的所有 key，都会将 key 和对应的值集合传给用户定义的 reduce 函数。reduce 函数的输出会被追加到一个最终的输出文件（每个 reduce 分片一个）。</li>
<li>当所有的 map 任务和 reduce 任务都完成后，MapReduce 的任务也就完成了。</li>
</ol>
<p>运行结束后，MapReduce 的运行结果保存在 R 个输出文件中，通常这些文件会被用作下一个 mapreduce 任务的输入。</p>
<h4 id="容错">容错<a href="#容错" class="heading-anchor-link" aria-label="Link to 容错"><svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M2.6 21.4c2 2 5.9 2.9 8.9 0l3.5-3.5-1-1-3.5 3.5c-1.4 1.4-4.2 1.9-6.4-.3s-1.8-5-.3-6.4l3.5-3.5-1-1-3.5 3.5c-3 3-2 6.9 0 8.9ZM21.4 2.6c2 2 2.9 5.9 0 8.9L17.9 15l-1-1 3.5-3.5c1.4-1.4 1.9-4.2-.3-6.4s-5-1.8-6.4-.3l-3.5 3.5-1-1 3.5-3.5c3-3 6.9-2 8.9 0Z"></path><path d="m8.01 14.97 6.93-6.93 1.061 1.06-6.93 6.93z"></path></svg></a></h4>
<p>这里只考虑 worker 挂掉的情况，不考虑 master 挂掉的情况，因为这可能涉及选举共识等复杂情况。</p>
<p>master 和 worker 会维持一个心跳，如果一段时间没有收到 worker 的回应，就会认为这个 worker 挂掉了。所有由这个 worker <strong>完成的 map 任务</strong>都会被重新变成未开始状态，会被重新分配给其他 worker 执行。所有挂掉时<strong>正在进行的 map 或者 reduce 任务</strong>会被标记为未开始。</p>
<p>已完成的 map 任务需要重新执行是因为它们的结果存储在已经挂掉机器的本地硬盘上，而已经完成的 reduce 任务无需重新执行，reduce 任务的结果被放在全局的文件系统上。</p>
<p>如果一个 map 任务最初由 A 执行，后来 A 挂掉了，被重新分配给 B 执行，这个消息会被通知到所有执行 reduce 任务的 worker。所有还没有从 A 中读取数据的 reduce 任务会转而选择从 B 读取数据。</p>
<p>有时，会出现这种情况：部分机器的性能很低，但是由于网络通畅，不会被判定为挂掉，这种机器就会成为整个系统的短板，整个系统不得不等待慢速机器慢吞吞地执行完他们的任务。对于这种情况，Google 的实现采用的一种机制来提升：在整体 MapReduce 操作快要结束时，master 会将所有仍然在进行的任务分配给其他空闲的 worker 执行。无论是原来的 worker，还是二次分配的 worker 完成了任务，这个任务都算是成功完成。</p>
<p>性能提升与小优化小扩展略去。</p>]]></content:encoded>
        </item>
    </channel>
</rss>