<?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>gengyue's blog</title>
        <link>https://www.gengyue.dev</link>
        <description>The personal blog of gengyue.</description>
        <lastBuildDate>Mon, 08 Jun 2026 07:42:03 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>Nofte</generator>
        <language>zh-CN</language>
        <copyright>Copyright © 2026 gengyue</copyright>
        <item>
            <title><![CDATA[利用 Bun 的最新内置 API Bun.Webview 搭建轻量网页抓取小玩具]]></title>
            <link>https://www.gengyue.dev/blog/build-fig-via-bun-webview</link>
            <guid isPermaLink="false">https://www.gengyue.dev/blog/build-fig-via-bun-webview</guid>
            <pubDate>Mon, 11 May 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[本文介绍了如何利用 Bun 的实验性 API `Bun.Webview` 构建一个轻量级网页抓取工具 Fig，重点解决了在 Windows 下连接 Chrome 后端的问题（通过远程调试端口和手动启动浏览器），并实现了 HTML 到 Markdown 的转换、自定义 User-Agent 插件绕过微信公众号风控，最终部署为 HTTP 服务，可集成到 QQ 机器人等场景中。]]></description>
            <content:encoded><![CDATA[<h3>前言</h3>
<p><a rel="noopener noreferrer" target="_blank" href="https://bun.com">Bun</a>在 1.3.12 版本之后内置了一个全新的 API <code>Bun.Webview</code>，可以实现一些简单的浏览器自动化，<s>能够</s>部分替代 <a rel="noopener noreferrer" target="_blank" href="https://playwright.dev/">Playwright</a>的职能。哈，大好，试吃一下看看。 <span class="rss-marginnote">⊕ (请注意此 API 仍然处于实验状态， Bun 官方也承认 <em>This API is experimental and may change in future releases.</em>，并且存在一些神秘 Bug，建议用于测试而非生产环境。)</span></p>
<p>对于尊贵的 macOS 用户，<code>Bun.Webview</code> 可以直接调用系统原生的 <code>webkit</code> 作为 API 的后端。针对 Windows 或 Linux 平台，可以使用 chrome 作为 <code>Bun.Webview</code> 的后端，通过<code>const view = new Bun.WebView({ backend: &quot;chrome&quot; });</code>设置。 <span class="rss-marginnote">⊕ (macOS 亦可通过此声明使用 chrome 而非 webkit 后端)</span></p>
<p>根据<a rel="noopener noreferrer" target="_blank" href="https://bun.com/docs/runtime/webview#finding-the-chrome-executable">Bun 的文档</a>，Bun 会通过以下的顺序寻找 chrome 后端：</p>
<ol>
<li>在 <code>backend: { type: &quot;chrome&quot;, path: &quot;...&quot; }</code> 下设置的 <code>path</code></li>
<li><code>BUN_CHROME_PATH</code> 环境变量  <span class="rss-marginnote">⊕ (<strong>! 注意，这里有问题</strong>，会在下面提到)</span></li>
<li><code>$PATH</code> (<code>google-chrome-stable</code>, <code>google-chrome</code>, <code>chromium-browser</code>, <code>chromium</code>, <code>brave-browser</code>, <code>microsoft-edge</code>, <code>chrome</code>)`</li>
<li>常见的安装目录</li>
<li>Playwright 的缓存 (<code>~/Library/Caches/ms-playwright</code> or <code>~/.cache/ms-playwright</code>) for <code>chrome-headless-shell</code></li>
</ol>
<h3>集成 chrome 后端</h3>
<p>但是在实际操作中，我发现无论在 Windows 下如何设置 <code>BUN_CHROME_PATH</code> ，Bun 似乎都无法正确的找到并启动 chrome 后端，即使你安装了 chrome、chromium 或者 edge。在 <a rel="noopener noreferrer" target="_blank" href="https://github.com/oven-sh/bun/issues">Bun 的 Issue 区</a>找到了一个<a rel="noopener noreferrer" target="_blank" href="https://github.com/oven-sh/bun/issues/29102">类似的 Issue</a>，看起来这是设计早期的缺陷，<em>应该</em>会在后续的版本中获得改进。</p>
<p>哎，只能换个方法了。观察到显式设置 chrome 后端的 <code>path</code> 似乎是个好主意！</p>
<p>chromium 系的浏览器都有一个模式可以开启远程调试，对于 chrome 而言，这个设置在 <code>chrome://inspect/#remote-debugging</code>，对于 edge，这个设置位于 <code>edge://inspect/#remote-debugging</code>。理论上，我们只需要在这里勾选 &quot;Allow remote debugging for this browser instance&quot;，浏览器就会自行启动一个位于 <code>127.0.0.1:9222</code> 的远程调试服务器。但是很奇怪，这在我的笔记本电脑上并不工作：<strong>调试服务器确实在 9222 端口顺利运行了，但是预期的接口全部返回 404</strong>，这很奇怪。</p>
<p>不过，天无绝人之路，我们可以通过命令提示符输入 <code>&quot;C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe&quot; --remote-debugging-port=9222</code> 用 debugging 参数启动一个新的 edge 实例数 <span class="rss-marginnote">⊕ (如果后端起不来请尝试杀掉所有的 edge 进程)</span>，这样调试服务器就能正常工作了，好耶！</p>
<p>下一步的工作是让 <code>Bun.Webview</code> 连上我们启动的后端，我们可以通过类似下面的 typescript 脚本获取浏览器的调试 Websocket 地址：</p>
<pre><code class="language-typescript">import axios from &quot;axios&quot;;

async function getBrowserDebuggingURL(): Promise&lt;string&gt; {
  try {
    const response = await axios.get(&quot;http://localhost:9222/json/version&quot;);
    return response.data.webSocketDebuggerUrl;
  } catch (error) {
    const message = error instanceof Error ? error.message : String(error);
    console.error(`获取浏览器调试 URL 失败: ${message}`);
    throw new Error(&quot;获取浏览器调试 URL 失败&quot;);
  }
}

export { getBrowserDebuggingURL };
</code></pre>
<p>并在 <code>Bun.Webview</code> 的启动参数中 <code>await</code> 上面的异步函数返回的结果，就像：</p>
<pre><code class="language-typescript">const view = new Bun.WebView({
  backend: {
     type: &quot;chrome&quot;,
     url: await getBrowserDebuggingURL(),
  },
  headless: true,
 });
</code></pre>
<p>好耶，经过这一番操作，Bun 应该能顺利连上浏览器后端了。</p>
<h3>抓取网页与格式化</h3>
<p><code>Bun.Webview</code> 的 API 和 playwright 基本上类似，我们可以通过类似下面的代码完成对网页的简单抓取：</p>
<pre><code class="language-typescript">const title = await view.evaluate(`document.title
        || document.querySelector('meta[property=&quot;og:title&quot;]')?.content
        || document.querySelector('meta[name=&quot;twitter:title&quot;]')?.content
        || document.querySelector('h1')?.textContent?.trim()
        || document.querySelector('h2')?.textContent?.trim()
        || &quot;&quot;`);
const html = await view.evaluate(&quot;document.documentElement.outerHTML&quot;);
const text = await view.evaluate(&quot;document.documentElement.innerText&quot;);
</code></pre>
<p>对于抓取到的数据，我通过一个自定义的 <code>parser</code>，利用 <code>cheerio</code> 清理一下 DOM 结构去除 <code>script</code> <code>style</code> 等无意义的标签，只取出 <code>body</code> 部分，然后用 <code>@mizchi/readability</code> 尝试把 html 转换成 Markdown 格式： <span class="rss-marginnote">⊕ (这样可以减少一部分 token 消耗，也就是省下了钱钱！)</span></p>
<pre><code class="language-typescript">import { extract, toMarkdown } from &quot;@mizchi/readability&quot;;
import * as cheerio from &quot;cheerio&quot;;

function normalizeHtml(html: string) {
  try {
    const $ = cheerio.load(html);
    return $(&quot;body&quot;).html() ?? &quot;&quot;;
  } catch (error) {
    console.warn(&quot;HTML 格式化失败:&quot;, error);
    return html;
  }
}

async function htmlParser(url: string, html: string): Promise&lt;string&gt; {
  try {
    const normalizedHtml = normalizeHtml(html);
    const extracted = extract(normalizedHtml, {
      charThreshold: 100,
    });
    if (!extracted?.root) {
      console.warn(`没有找到文章根元素 ${url}`);
      return &quot;&quot;;
    }
    const parsed = toMarkdown(extracted.root);
    if (typeof parsed !== &quot;string&quot; || parsed.trim().length === 0) {
      console.warn(`Markdown 转换为空 ${url}`);
      return &quot;&quot;;
    }
    return parsed;
  } catch (error) {
    const message = error instanceof Error ? error.message : String(error);
    console.error(`HTML 解析失败: ${message}`);
    return &quot;&quot;;
  }
}

export { htmlParser };
</code></pre>
<p>不幸的是，markdown 格式经常转换失败， <span class="rss-marginnote">⊕ (猜测是因为库是基于阅读模式构建的，但是有些页面并不支持转换成阅读模式，所以在转换时会出错。考虑可以用 <code>turndown</code> 或者类似的库兜个底)</span>这个时候就需要用 <code>innerText</code> 兜个底 —— 虽然 markdown 格式没了，但是至少不会返回令人诧异的空白。</p>
<p>用下面的 <code>curl</code> 命令测试一下：</p>
<pre><code class="language-bash">curl -X POST http://localhost:9233/read \
  -H &quot;Content-Type: application/json&quot; \
  -H &quot;Authorization: Bearer my-magic-access-token&quot; \
  -d '{&quot;url&quot;:&quot;https://www.gengyue.site&quot;}'
</code></pre>
<p>返回结果类似：</p>
<pre><code class="language-plaintext">---
title: gengyue
url: https://www.gengyue.site
---

# Hi 👋!

此地属于 gengyue。目前这个页面应该是被扔到 [Netcup](https://www.netcup.com/en?ref=366353) 的一台 VPS VPS Lite 2 G12s, 4 vCore (x86), 8 GB RAM, 160 GB SSD, Nürnberg, BY, DE  上了，利用 [Cloudflare](https://cloudflare.com/) 回源。

您可以通过阅读 [About](/about), [Blog](/blog) 和 [Logbook](/logbook) 页面了解更多信息，下面的链接亦可：

- 我的 Email: [hi@gengyue.site](mailto:hi@gengyue.site)
- 我的 GitHub: [@gengyue2468](https://github.com/gengyue2468)
- 我的 Memos: [https://memos.gengyue.site](https://memos.gengyue.site)

‍ **Linus Torvalds** is the Finnish-American software engineer who created the Linux kernel, the foundation of countless open-source operating systems. ![Linux 企鹅](/static/og/tux.gif)

... truncated
</code></pre>
<h3>UA 与插件系统</h3>
<p>启动成功了自然要测一下常见的网站能不能爬：例如知乎、小红书、微信公众号。奇怪的是，知乎、小红书都是正常的，但不幸的是，微信被风控拦了，但是我们是聪明的工人智能，可以想到伪造一点 UA 绕过限制，虽然看起来比较绿皮，但是确实有效。</p>
<p>预制了一点 UA，仅供参考：</p>
<pre><code class="language-typescript">export const UA_PRESETS = {
  iPhone_WebView: &quot;Mozilla/5.0 (iPhone; CPU iPhone OS 17_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 MicroMessenger/8.0.49&quot;,
  iPhone_Safari: &quot;Mozilla/5.0 (iPhone; CPU iPhone OS 17_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4 Mobile/15E148 Safari/604.1&quot;,
  Android_WebView: &quot;Mozilla/5.0 (Linux; Android 14; Pixel 8 Pro) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.6261.119 Mobile Safari/537.36 MicroMessenger/8.0.49&quot;,
  Android_Chrome: &quot;Mozilla/5.0 (Linux; Android 14; Pixel 8 Pro) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.6261.119 Mobile Safari/537.36&quot;,
  Desktop_Chrome: &quot;Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36&quot;,
  Desktop_Safari: &quot;Mozilla/5.0 (Macintosh; Intel Mac OS X 14_4) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4 Safari/605.1.15&quot;,
  Baidu_Spider: &quot;Mozilla/5.0 (compatible; Baiduspider/2.0; +http://www.baidu.com/search/spider.html)&quot;,
  Googlebot: &quot;Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)&quot;,

} as const;
</code></pre>
<p>经过实测，伪造一个 iPhone Webview 的 UA 就可以绕过微信公众号的限制！参考 <code>wechat.ts</code> 插件： <span class="rss-marginnote">⊕ (微信是否歧视安卓用户🤔🤯)</span></p>
<pre><code class="language-typescript">import type { UAPlugin } from &quot;../registry&quot;;
import { UA_PRESETS } from &quot;../presets&quot;;

const WECHAT_DOMAINS = [
  &quot;mp.weixin.qq.com&quot;,
  &quot;channels.weixin.qq.com&quot;,
  &quot;weixin.qq.com&quot;,
  &quot;open.weixin.qq.com&quot;,
];

const wechatPlugin: UAPlugin = {
  name: &quot;wechat&quot;,
  match(url: string): boolean {
    try {
      const hostname = new URL(url).hostname;
      return WECHAT_DOMAINS.some((domain) =&gt; hostname === domain || hostname.endsWith(&quot;.&quot; + domain));
    } catch {
      return false;
    }
  },
  getUserAgent(_url: string): string {
    return UA_PRESETS.iPhone_WebView;
  },
};
  
export default wechatPlugin;
</code></pre>
<p>然后在主逻辑中引用：</p>
<pre><code class="language-typescript">const matchedUA = pluginRegistry.resolve(url);
      if (matchedUA) {
        await view.cdp(&quot;Network.setUserAgentOverride&quot;, { userAgent: matchedUA });
      }
</code></pre>
<blockquote>
<p>这里有个细节，Bun 似乎没有直接的方法换 UA，这里通过 cdp 覆盖 chrome 后端的默认 UA 实现</p>
</blockquote>
<p>好耶！爬爬</p>
<h3>部署</h3>
<p>最初写这个小玩具也是为了和 QQ 机器人结合，于是用 Bun + Hono 搭了一个简易的 HTTP 后端。部署这个 Hono 程序很简单，只需要在小鸡上 clone 仓库然后 <code>bun i</code> 然后用 <code>pm2</code> 持久化启动一下就好。</p>
<p>chrome 后端我们选用的是 <code>chromium</code> ，在 Ubuntu Server 上，通过以下命令安装：</p>
<pre><code class="language-bash">sudo apt update
sudo apt install -y ca-certificates fonts-liberation fonts-noto-cjk
sudo apt install -y chromium-browser
</code></pre>
<p>为了让浏览器看起来更像真人，我们安装  <code>xvfb</code> 以让 chromium 以非 <code>headless</code> 模式启动！</p>
<blockquote>
<p>注意，这样可能会让您的小鸡消耗相当多的资源，如果小鸡性能不是很强或者负载比较大，建议还是以 headless 模式启动 chrome 后端。最好不要让这个绿皮科技承载过多的并发，否则短时间的高负载极其容易导致 OOM 杀进程。</p>
</blockquote>
<p>为了持久化运行，我们创建一个 <code>systemd</code> 进程，编辑  <code>~/.config/systemd/user/chromium.service</code>:</p>
<pre><code class="language-plaintext">[Unit]
Description=Chromium Browser
After=network.target

[Service]
ExecStart=/usr/bin/xvfb-run --auto-servernum --server-args=&quot;-screen 0 1920x1080x24&quot; \
    /usr/bin/chromium-browser \
    --no-sandbox \
    --disable-dev-shm-usage \
    --remote-debugging-port=9222 \
    --remote-debugging-address=127.0.0.1 \
    --user-data-dir=/tmp/chrome-debug-profile \
    --disable-blink-features=AutomationControlled \
    --disable-features=IsolateOrigins,site-per-process \
    --disable-infobars \
    --disable-component-update \
    --lang=zh-CN \
    --window-size=1920,1080 \
    https://www.google.com
Restart=always
RestartSec=10

[Install]
WantedBy=default.target
</code></pre>
<p>然后启动服务并设置开机自启：</p>
<pre><code class="language-bash">systemctl --user daemon-reload
systemctl --user enable chromium.service
systemctl --user start chromium.service
</code></pre>
<p>测试一下：</p>
<pre><code class="language-bash">curl -X POST http://localhost:9233/read \
  -H &quot;Content-Type: application/json&quot; \
  -H &quot;Authorization: Bearer my-magic-access-token&quot; \
  -d '{&quot;url&quot;:&quot;https://www.gengyue.site&quot;}'
</code></pre>
<p>好耶，跑起来了，下面就可以接入 QQ 机器人或者任何想要接入的场景了 <span class="rss-marginnote">⊕ (毕竟这是一个 HTTP 路由，强烈建议引入 <code>authMiddleware</code> 保护路由，防止被扫到滥用)</span></p>
<p>您可以在<a rel="noopener noreferrer" target="_blank" href="https://github.com/gengyue2468/fig">GitHub</a>上读到这个绿皮科技的完整源码，可能这个小玩具不够稳定，但是它确实是我摇摇欲坠的基建的一部分了！</p>
<footer class="notes-list"><h3>Notae</h3><ul><li id="sn-list-0">请注意此 API 仍然处于实验状态， Bun 官方也承认 <em>This API is experimental and may change in future releases.</em>，并且存在一些神秘 Bug，建议用于测试而非生产环境。 <a href="#ref-marker-0">↩</a></li><li id="sn-list-1">macOS 亦可通过此声明使用 chrome 而非 webkit 后端 <a href="#ref-marker-1">↩</a></li><li id="sn-list-2"><strong>! 注意，这里有问题</strong>，会在下面提到 <a href="#ref-marker-2">↩</a></li><li id="sn-list-3">如果后端起不来请尝试杀掉所有的 edge 进程 <a href="#ref-marker-3">↩</a></li><li id="sn-list-4">这样可以减少一部分 token 消耗，也就是省下了钱钱！ <a href="#ref-marker-4">↩</a></li><li id="sn-list-5">猜测是因为库是基于阅读模式构建的，但是有些页面并不支持转换成阅读模式，所以在转换时会出错。考虑可以用 <code>turndown</code> 或者类似的库兜个底 <a href="#ref-marker-5">↩</a></li><li id="sn-list-6">微信是否歧视安卓用户🤔🤯 <a href="#ref-marker-6">↩</a></li><li id="sn-list-7">毕竟这是一个 HTTP 路由，强烈建议引入 <code>authMiddleware</code> 保护路由，防止被扫到滥用 <a href="#ref-marker-7">↩</a></li></ul></footer>]]></content:encoded>
            <author>gengyue</author>
        </item>
        <item>
            <title><![CDATA[为什么您的深色模式看起来不太对劲？]]></title>
            <link>https://www.gengyue.dev/blog/on-darkmode</link>
            <guid isPermaLink="false">https://www.gengyue.dev/blog/on-darkmode</guid>
            <pubDate>Sun, 12 Apr 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[探讨 prefers-color-scheme 与 .dark 类在实际开发中的冲突，指出 color-scheme: light dark 会导致浏览器原生控件与页面主题不一致的问题，并给出以 .dark 控制 color-scheme 的解决方案，同时反思主题切换在产品设计中的必要性。]]></description>
            <content:encoded><![CDATA[<p><a rel="noopener noreferrer" target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/At-rules/@media/prefers-color-scheme">根据 MDN 的说法</a>  <code>prefers-color-scheme</code> 能够侦测到用户在操作系统上的主题偏好，也就是浅色和深色模式。</p>
<blockquote>
<p>The <strong><code>prefers-color-scheme</code></strong> <a rel="noopener noreferrer" target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/CSS">CSS</a> <a rel="noopener noreferrer" target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Media_queries/Using#targeting_media_features">media feature</a> is used to detect if a user has requested light or dark color themes. A user indicates their preference through an operating system setting (e.g., light or dark mode) or a user agent setting.</p>
</blockquote>
<p>通常，我们可以通过<strong>媒体查询</strong>在写 CSS 的时候控制全局的主题变量。比如，我们可以写出下面的代码：</p>
<pre><code class="language-css">:root{
/* light mode goes here */
}

@media (prefers-color-scheme: dark) {
  :root{
  /* dark mode goes here */
  }
}
</code></pre>
<p>这样，页面主题可以自适应用户的偏好而自动切换。原生组件也可以自适应。 <span class="rss-marginnote">⊕ (例如浏览器的滚动条，您可能没有注意过)</span></p>
<p>通常，我们可以通过设置 <code>color-scheme</code> 来控制可用的主题，例如： <span class="rss-marginnote">⊕ (<a rel="noopener noreferrer" target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/color-scheme#styling_based_on_color_schemes">color-scheme - CSS | MDN</a>)</span></p>
<pre><code class="language-css">:root {
  color-scheme: light dark;
}

@media (prefers-color-scheme: light) {
  .element {
    color: black;
    background-color: white;
  }
}

@media (prefers-color-scheme: dark) {
  .element {
    color: white;
    background-color: black;
  }
}
</code></pre>
<p>其中 <code>color-scheme: light dark</code> 告诉了浏览器尊重用户的主题偏好来控制主题的显示。举一反三，如果设置 <code>color-scheme</code> 为 <code>only light</code> 或 <code>only dark</code> 就覆盖了用户的主题偏好，转而显示单一主题。</p>
<p><strong>看起来很不错，但是，在实际应用角度上，原生的 CSS 似乎显得有那么力不足。有的时候，我们需要给出一个切换主题的按钮，这个按钮要覆写掉用户原生的主题偏好。</strong></p>
<p>一般的做法是使用一个 <code>.dark</code> 类，通过添加或移除这个类，可以手动的控制页面的外观主题。例如，在 <a rel="noopener noreferrer" target="_blank" href="https://ui.shadcn.com">Shadcn UI</a> 中，有这样一行代码：</p>
<pre><code class="language-css">@custom-variant dark (&amp;:is(.dark *));
</code></pre>
<p>对于 <a rel="noopener noreferrer" target="_blank" href="https://nextjs.org">Next.js</a> 框架的项目来说，有一个成熟的解决方案是用 <code>next-themes</code> 这个库 <span class="rss-marginnote">⊕ (<a rel="noopener noreferrer" target="_blank" href="https://www.npmjs.com/package/next-themes">next-themes - npm</a>)</span>，这个库封装好了 <code>ThemeProvider</code> ，只需要包裹一下主要部分就可以轻松使用。然而，我们在使用 <a rel="noopener noreferrer" target="_blank" href="https://reactrouter.com/">React Router v7</a> 或者类似的框架的时候，由于 UI 库本身没有自带 ThemeProvider 或者我们是手搓的 UI 没有封装好的组件，我们似乎不得不自己或者让 LLM 造一个轮子，用于主题的切换。一个常见的思路是用 JS 向 <code>html</code> 标签添加或移除 <code>dark</code> 类：</p>
<pre><code class="language-javascript">function toggleTheme() {
  const root = document.documentElement;
  const isDark = root.classList.contains(&quot;dark&quot;);

  const nextTheme = isDark ? &quot;light&quot; : &quot;dark&quot;;

  localStorage.setItem(THEME_KEY, nextTheme);
  applyTheme(nextTheme);
}
</code></pre>
<p>聪明一点，我们顺带加上 <code>localStorage</code> 的持久化存储：</p>
<pre><code class="language-javascript">const THEME_KEY = &quot;theme&quot;;

function applyTheme(theme) {
  const root = document.documentElement;

  if (theme === &quot;dark&quot;) {
    root.classList.add(&quot;dark&quot;);
  } else {
    root.classList.remove(&quot;dark&quot;);
  }
}

function getSystemTheme() {
  return window.matchMedia(&quot;(prefers-color-scheme: dark)&quot;).matches
    ? &quot;dark&quot;
    : &quot;light&quot;;
}

function initTheme() {
  const saved = localStorage.getItem(THEME_KEY);

  if (saved) {
    applyTheme(saved);
  } else {
    applyTheme(getSystemTheme());
  }
}
</code></pre>
<p>然后在 HTML 中渲染一个主题切换按钮：</p>
<pre><code class="language-html">&lt;button onclick=&quot;toggleTheme()&quot;&gt;切换主题&lt;/button&gt;
</code></pre>
<p>相信水了这么多，聪明的读者一定会发现这里的一个明显的漏洞：<code>prefers-color-scheme</code> 在某种程度上是和我们手动添加的 <code>.dark</code> 类是冲突的，尤其是在 <code>color-scheme: light dark;</code> 的情况下 ——我们控制了页面的颜色，但没有控制浏览器自己的 UI。</p>
<p>正确的做法是：<strong>如果需要手动控制，就不要用 <code>color-scheme: light dark;</code> 而是将 <code>color-scheme</code> 的变换完全交给<code>.dark</code> 类控制。</strong> 这是一个很细微的细节，看起来很简单，但是许多网站并没有注意这一点，导致外观上出现了一定程度的脱节。 <span class="rss-marginnote">⊕ (这种脱节更多地体现在浏览器的原生组件中，例如滚动条或者 Native Select 这类原生 UI 组件)</span></p>
<p>比如 Memos， Memos 是一款很有趣好用开源的自由备忘录软件，前端设计得很现代精致，可惜黑暗模式下侧边栏的滚动条是白色的（见下图），这种割裂的设计顿时会让设计的优雅气质衰减。截至我在用的 0.26.0 版本，这个问题仍未解决。猜想应该是因为 Memos 维护了诸多主题的原因，不过有待考证。</p>
<figure><img loading="lazy" decoding="async" src="/static/tech/memos-darkmode.webp" alt="Memos 黑暗模式下滚动条的突兀"><figcaption>Memos 黑暗模式下滚动条的突兀</figcaption></figure>
<blockquote>
<p>Updated 2026-04-15</p>
<p>我给 Memos 提了一个 Issue: <a rel="noopener noreferrer" target="_blank" href="https://github.com/usememos/memos/issues/5839">Dark themes do not set <code>color-scheme</code>, causing native browser UI elements such as scrollbar to stay light · Issue #5839 · usememos/memos</a>。可惜下午满课，手边没有电脑，<s>没法水个 pr</s>。但是这个问题确实在 <a rel="noopener noreferrer" target="_blank" href="https://github.com/usememos/memos/pull/5840">chore: set native color scheme for dark themes by boojack · Pull Request #5840 · usememos/memos</a> 中解决了，还是很快速的。</p>
<p>哎，不过我似乎并不打算更新 Memos 的版本，所以只为服务后人了...（其实并非，我们可以通过简单的自定义代码解决这个问题，见下面提到的解决方案）</p>
</blockquote>
<p>要解决类似的问题，其实只需要让 <code>color-scheme</code> 跟随 <code>.dark</code> 类的变化就行，例如：</p>
<pre><code class="language-css">:root {
  color-scheme: light;
}
:root.dark {
  color-scheme: dark;
}
</code></pre>
<p><strong>Updated 2026-04-15</strong> 针对上面 Memos 的案例，Memos 的开发者利用 Codex 巧妙的修复了这个问题，首先判断是否是黑暗模式：</p>
<pre><code class="language-typescript">const isDarkTheme = (theme: ResolvedTheme): boolean =&gt; {
  return theme.endsWith(&quot;-dark&quot;) || theme.endsWith(&quot;.dark&quot;);
};

/**
 * Updates the browser native control color scheme to match the current theme.
 */
const updateColorScheme = (theme: ResolvedTheme): void =&gt; {
  document.documentElement.style.colorScheme = isDarkTheme(theme) ? &quot;dark&quot; : &quot;light&quot;;
};

</code></pre>
<p>根据是否是黑暗模式更新 DOM 的 <code>colorScheme</code>，如果是，就设置为 <code>dark</code>，反之为 <code>light</code>。</p>
<p>另外，Memos 的前端使用 React Router 构建，它们的 <code>/utils/theme.ts</code> 对于主题加载的函数也很值得学习：</p>
<pre><code class="language-typescript">export const loadTheme = (themeName: string): void =&gt; {
  const validTheme = validateTheme(themeName);
  injectThemeStyle(resolvedTheme);
  setThemeAttribute(resolvedTheme);
  updateThemeColorMeta(resolvedTheme);
  updateColorScheme(resolvedTheme);
  setStoredTheme(validTheme); // Store original theme preference (not resolved)
};
</code></pre>
<p>根据主题按需注入主题 CSS -&gt; 设置主题偏好 -&gt; 更新 Meta 颜色元数据 -&gt; 更新 colorScheme -&gt; 持久化到本地存储，这也是很不错的最佳实践。</p>
<p>当然，如果您使用的是旧版 Memos，仍然可以通过注入自定义脚本来监听并及时更新 <code>color-scheme</code> 状态，这很简单，只需要去 <code>Settings -&gt; System -&gt; Additional script</code> 添加下面的代码就 ok ：</p>
<pre><code class="language-javascript">(function() {
    const isDarkTheme = (theme) =&gt; {
        return theme === &quot;default-dark&quot; || theme === &quot;midnight&quot;;
    };

    const updateColorScheme = (theme) =&gt; {
        const scheme = (theme &amp;&amp; isDarkTheme(theme)) ? &quot;dark&quot; : &quot;light&quot;;
        document.documentElement.style.colorScheme = scheme;
    };

    const applyColorScheme = () =&gt; {
        const currentTheme = document.documentElement.getAttribute(&quot;data-theme&quot;);
        updateColorScheme(currentTheme);
    };

    applyColorScheme();


    const observer = new MutationObserver((mutations) =&gt; {
        for (const mutation of mutations) {
            if (mutation.attributeName === &quot;data-theme&quot;) {
                applyColorScheme();
                break;
            }
        }
    });
    observer.observe(document.documentElement, { attributes: true });

    window.addEventListener(&quot;load&quot;, applyColorScheme);

    setTimeout(applyColorScheme, 1000);
})();
</code></pre>
<p>在某种程度上，<code>prefers-color-scheme</code> 和 <code>color-scheme: light dark;</code> 仍然是最优解，因为我们可以完全依靠浏览器的原生能力而不是繁杂的 JavaScript 脚本来控制这个极其细微的细节。一般来说，用户使用了深色主题，它们在访问网站的时候，往往需要的是一个同样舒服的深色主题，而不是选择手动切换到可能亮瞎眼的浅色模式。某种程度上，如果不是像 Memos 那样维护其它例如 Paper 这样的暖色主题（区分于 light/dark 模式），主题切换按钮往往也是没有必要的、甚至是过度设计的（over-designed)。</p>
<p>所以，下次设计用户界面的时候，不妨先思考一下：用户真的需要手动切换主题吗？或者说，您认为用户反复的手动切换主题对它们来说也算是一种乐趣。如果是后者，那么设计一个按钮、维护一些 JavaScript 也无可厚非，但是如果用户觉得这没必要，那还是删除比较好，毕竟，少即是多嘛。</p>
<footer class="notes-list"><h3>Notae</h3><ul><li id="sn-list-0">例如浏览器的滚动条，您可能没有注意过 <a href="#ref-marker-0">↩</a></li><li id="sn-list-1"><a rel="noopener noreferrer" target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/color-scheme#styling_based_on_color_schemes">color-scheme - CSS | MDN</a> <a href="#ref-marker-1">↩</a></li><li id="sn-list-2"><a rel="noopener noreferrer" target="_blank" href="https://www.npmjs.com/package/next-themes">next-themes - npm</a> <a href="#ref-marker-2">↩</a></li><li id="sn-list-3">这种脱节更多地体现在浏览器的原生组件中，例如滚动条或者 Native Select 这类原生 UI 组件 <a href="#ref-marker-3">↩</a></li></ul></footer>]]></content:encoded>
            <author>gengyue</author>
        </item>
        <item>
            <title><![CDATA[网页离开了 CSS 和 JS 还有可读性吗？]]></title>
            <link>https://www.gengyue.dev/blog/webpage-without-css-and-js</link>
            <guid isPermaLink="false">https://www.gengyue.dev/blog/webpage-without-css-and-js</guid>
            <pubDate>Thu, 02 Apr 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[gengyue 的个人网站与博客 - 华中科技大学计算机专业学生，记录技术探索、开发经验与项目实践。分享技术、开发等文章，以及开源项目和小玩具建设经验。]]></description>
            <content:encoded><![CDATA[<p>我一直认为现代 HTML5 的语义化标签是阅读优化的、利于 SEO 的，并且语义化标签是对人类和机器都很有好的一种表示：人类可以通过精妙的网页排版辨识出页面的标题、副标题、正文等元素，也可以通过<code>section</code> 等标签判断出所谓的“区域”。同样，机器也可以辨识出网页具有的各个部分，利于搜索引擎爬虫和各种 SEO 和无障碍优化。</p>
<p>CSS 和 JavaScript 为现代网站提供了锦上添花的效果：前者负责美化页面，让用户感觉很舒服；后者负责丰富网页的交互，让用户能够参与其中。</p>
<p>但是，我有个问题：<strong>我们真的需要依靠复杂的 CSS 样式和 JS 脚本去复杂内容站吗？不管是塞入大量的无用的动画脚本或 CSS，轻量的或臃肿的追踪用户的脚本，甚至莫名其妙地弹出一个您是否同意使用小饼干 <span class="rss-sidenote">(Cookie)</span>的神秘弹窗。</strong></p>
<p>HTML 本身就是一种<strong>超文本标记语言</strong> <span class="rss-sidenote">(<a rel="noopener noreferrer" target="_blank" href="https://zh.wikipedia.org/wiki/HTML5">https://zh.wikipedia.org/wiki/HTML5</a>)</span>，所以自然可以用于排版。事实上，如果我们能够真正地尊重语义化标准，通过合理的 HTML5 标签就可以排出相当美丽的网页版式。</p>
<p>例如：</p>
<pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html lang=&quot;en&quot;&gt;
&lt;head&gt;
    &lt;meta charset=&quot;UTF-8&quot;&gt;
    &lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot;&gt;
    &lt;title&gt;Sample Blog Article&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;

&lt;header&gt;
    &lt;h1&gt;My Blog&lt;/h1&gt;
    &lt;nav&gt;
        &lt;ul&gt;
            &lt;li&gt;&lt;a href=&quot;#&quot;&gt;Home&lt;/a&gt;&lt;/li&gt;
            &lt;li&gt;&lt;a href=&quot;#&quot;&gt;Articles&lt;/a&gt;&lt;/li&gt;
            &lt;li&gt;&lt;a href=&quot;#&quot;&gt;About&lt;/a&gt;&lt;/li&gt;
        &lt;/ul&gt;
    &lt;/nav&gt;
&lt;/header&gt;

&lt;main&gt;

    &lt;article&gt;

        &lt;header&gt;
            &lt;h2&gt;How to Build a Semantic HTML Blog Page&lt;/h2&gt;
            &lt;p&gt;
                Published on 
                &lt;time datetime=&quot;2026-04-02&quot;&gt;April 2, 2026&lt;/time&gt;
                by &lt;strong&gt;John Doe&lt;/strong&gt;
            &lt;/p&gt;
        &lt;/header&gt;

        &lt;section&gt;
            &lt;p&gt;
                Lorem ipsum dolor sit amet, consectetur adipiscing elit. 
                Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
            &lt;/p&gt;

            &lt;p&gt;
                Ut enim ad minim veniam, quis nostrud exercitation ullamco 
                laboris nisi ut aliquip ex ea commodo consequat.
            &lt;/p&gt;
        &lt;/section&gt;

        &lt;figure&gt;
            &lt;img src=&quot;https://picsum.photos/200/300&quot; alt=&quot;Sample Image&quot;&gt;
            &lt;figcaption&gt;Figure 1: Example placeholder image&lt;/figcaption&gt;
        &lt;/figure&gt;

        &lt;section&gt;
            &lt;h3&gt;Why Use Semantic Tags?&lt;/h3&gt;
            &lt;p&gt;
                Duis aute irure dolor in reprehenderit in voluptate velit 
                esse cillum dolore eu fugiat nulla pariatur.
            &lt;/p&gt;

            &lt;p&gt;
                Excepteur sint occaecat cupidatat non proident, sunt in culpa 
                qui officia deserunt mollit anim id est laborum.
            &lt;/p&gt;
        &lt;/section&gt;

        &lt;blockquote&gt;
            &lt;p&gt;
                &quot;Lorem ipsum dolor sit amet, consectetur adipiscing elit.&quot;
            &lt;/p&gt;
        &lt;/blockquote&gt;

        &lt;section&gt;
            &lt;h3&gt;Key Points&lt;/h3&gt;
            &lt;ul&gt;
                &lt;li&gt;Lorem ipsum dolor sit amet&lt;/li&gt;
                &lt;li&gt;Consectetur adipiscing elit&lt;/li&gt;
                &lt;li&gt;Sed do eiusmod tempor&lt;/li&gt;
            &lt;/ul&gt;
        &lt;/section&gt;

        &lt;footer&gt;
            &lt;p&gt;Tags: &lt;a href=&quot;#&quot;&gt;HTML&lt;/a&gt;, &lt;a href=&quot;#&quot;&gt;Web Development&lt;/a&gt;&lt;/p&gt;
        &lt;/footer&gt;

    &lt;/article&gt;

    &lt;aside&gt;
        &lt;h3&gt;About the Author&lt;/h3&gt;
        &lt;p&gt;
            Lorem ipsum dolor sit amet, consectetur adipiscing elit.
        &lt;/p&gt;
    &lt;/aside&gt;

&lt;/main&gt;

&lt;footer&gt;
    &lt;p&gt;&amp;copy; 2026 My Blog. All rights reserved.&lt;/p&gt;
&lt;/footer&gt;

&lt;/body&gt;
&lt;/html&gt;
</code></pre>
<blockquote>
<p>这个页面全部依靠 HTML5 语义化标签进行，没有 CSS 和 JavaScript 参与。不过严格说，展示的是浏览器自带 CSS 的样式，这里更多的指的是没有自定义 CSS 参与。</p>
</blockquote>
<p>效果如下：</p>
<div class="fullwidth-content">
<figure><img loading="lazy" decoding="async" src="/static/tech/html.webp" alt="纯 HTML5 排版效果"><figcaption>纯 HTML5 排版效果</figcaption></figure>
</div>
<p>其实效果还是可以看的，也就是说，<strong>网页离开了CSS 和 JavaScript，并不像鱼儿离开了水，彻底失去了生命力，它依然可以很好地活着</strong>  <span class="rss-marginnote">⊕ (这里只针对类似的静态网页)</span></p>
<p>当前，相当部分的网页依赖 React 或者 Vue 框架渲染，此类框架在 SSR 或纯客户端渲染类似 SPA 的情况下是严重依赖 JavaScript 的。此时，SSG 就提供了一个还算不错的替代方案，Astro 就是这方面的先驱。</p>
<p><strong>那么，CSS 和 JavaScript 是完全无用的喽？</strong></p>
<p>显然不是，我们不会否认 CSS 和 JavaScript 作为网页三件套的另外两架马车的地位，我们只是强调<strong>Webpage 并非离不开 CSS  和 JavaScript 的高级功能，HTML5 的粗鄙依旧能打</strong>。</p>
<p>对于<strong>内容驱动的网页</strong>来说，最理想的状态就是 SSG 或者纯静态网页，我们希望 <strong>HTML 作为底层，CSS 作为点缀，JavaScript 并非必要</strong>，理想状态是 JavaScript 按需加载 <span class="rss-marginnote">⊕ (本网站的 Mermaid.js 就是在构建时按需注入的，通过扫描文章是否存在<code>mermaid</code>代码块按需加载 Mermaid.js)</span>，核心 CSS 内联展示，对于不强调重交互的内容站而言，我认为这是最佳实践。</p>
<p>对于<strong>交互网页</strong>而言，JavaScript 依旧是实现交互必不可少的一环，但是良好的语义化标签命名依旧是一个好习惯，不仅利于 SEO，而且有利于无障碍。</p>
<p>曾几何时，所有的按钮都是<code>div</code>，<strong>而在 2026 年，前端又一次将死之时，希望给 LLM 留下的是一个充满语义化标签的前端规范，而不是一地鸡毛的别样的<code>div</code>大战。</strong></p>
<footer class="notes-list"><h3>Notae</h3><ol><li id="sn-list-0">Cookie <a href="#ref-marker-0">↩</a></li><li id="sn-list-1"><a rel="noopener noreferrer" target="_blank" href="https://zh.wikipedia.org/wiki/HTML5">https://zh.wikipedia.org/wiki/HTML5</a> <a href="#ref-marker-1">↩</a></li><li class="marginnote-item" id="sn-list-2">这里只针对类似的静态网页 <a href="#ref-marker-2">↩</a></li><li class="marginnote-item" id="sn-list-3">本网站的 Mermaid.js 就是在构建时按需注入的，通过扫描文章是否存在<code>mermaid</code>代码块按需加载 Mermaid.js <a href="#ref-marker-3">↩</a></li></ol></footer>]]></content:encoded>
            <author>gengyue</author>
        </item>
        <item>
            <title><![CDATA[让 llm 吐出胡说八道、天马行空的构石文章]]></title>
            <link>https://www.gengyue.dev/blog/let-llm-write-shitposts</link>
            <guid isPermaLink="false">https://www.gengyue.dev/blog/let-llm-write-shitposts</guid>
            <pubDate>Mon, 16 Mar 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[利用采样参数、结构约束与标签平衡，构建自动生成“伪学术”LLM文章的实践经验。]]></description>
            <content:encoded><![CDATA[<p>LLM 可谓是十分擅长胡说八道的，而且能够正儿八经地胡说八道，这可能是人类做不到的。LLM 的幻觉在某种程度上可能是缺陷，但是何尝不可以变成所谓的<strong>特性</strong>呢？如果我们故意地把温度调高，逼迫 LLM 输出尽可能独特的 token，何尝不可以得到一些看似有道理、高深莫测、故弄玄虚的文本呢？ <span class="rss-marginnote">⊕ (就像你正在阅读的这个引言一样，以 LLM 开头，看似很高大上，实际上可能是 1tan√10)</span></p>
<h2>参数</h2>
<p>决定 LLM 胡言乱语的参数有那么几个： <span class="rss-marginnote">⊕ (参见 <a rel="noopener noreferrer" target="_blank" href="https://ppio.com/docs/model/playground#%E5%8F%82%E6%95%B0%E9%85%8D%E7%BD%AE">PPIO 大语言模型参数控制文档</a>)</span></p>
<ul>
<li><strong>temperature</strong> 温度，如果我们把<strong>温度</strong>稍微调高一点，比如 0.96，可以控制输出的创造性与随机性。数值越高，模型越倾向于生成发散、丰富的表达，其实也就是胡言乱语。</li>
<li><strong>top_p</strong> 核采样，和温度搭配使用，<s>适当提高</s>可以增加 LLM 的创造性，但是如果我们<s>不适当</s>地提高，模型可能就不是在<strong>创造</strong>，而是在<strong>胡说八道</strong>。</li>
<li><strong>presence_penalty</strong> 出现惩罚，鼓励模型在输出中引入新的主题。设置为正值时，模型更可能跳出当前话题，扩展新内容。我们提高这一部分的值，可以让 LLM <s>自然地</s>突然从一个话题跳转到另一个话题，也就是更加正经地胡说八道。</li>
<li><strong>frequency_penalty</strong> 频率惩罚，我们通过提高这部分的值让 LLM 尽可能输出不同的词汇，避免表达的单一性和单调性，甚至，我们可以创造出一种情况：LLM 在上一句话中提到了一个中文的专有词汇，在下一次提到这个词汇的时候不会选择中文词汇而是<strong>翻译或者生造一个对应的英文词汇</strong>，这样做可能是 academic 的，可能是 journal 气质的，也可能是 bull shit 的 —— 毫无可读性的。</li>
<li><strong>repetition_penalty</strong> 重复惩罚：进一步减少模型陷入循环或过度重复提示词的风险，常用于控制文本生成的鲁棒性。</li>
</ul>
<p>通过巧妙地控制上面的参数，就可以让 LLM 倾向于一种放飞自我、天马行空的表达中，在人类生活中，这叫做<em>发疯的艺术家</em>或<em>癫狂的疯子</em>，但是我们的研究对象是 LLM，我们称这种情况为<strong>幻觉</strong>。 <span class="rss-marginnote">⊕ (提请读者注意，我们在讨论这里的时候，<strong>幻觉</strong>并不是一个贬义词，相反，我们是刻意利用 LLM 的幻觉，因为我们鼓励开放、自由、不为传统拘束的文学创作范式)</span></p>
<p>以 <a rel="noopener noreferrer" target="_blank" href="http://shitposts.org">shitposts.org</a> 为例，采用下面的参数：</p>
<pre><code class="language-json">    { label: &quot;volatile&quot;, temperature: randomBetween(1.08, 1.22), topP: randomBetween(0.92, 1), presencePenalty: randomBetween(0.45, 0.8), frequencyPenalty: randomBetween(0.1, 0.35) },

    { label: &quot;chaotic&quot;, temperature: randomBetween(1.18, 1.32), topP: randomBetween(0.9, 0.98), presencePenalty: randomBetween(0.6, 1.0), frequencyPenalty: randomBetween(0.05, 0.25) },

    { label: &quot;digressive&quot;, temperature: randomBetween(1.02, 1.16), topP: randomBetween(0.95, 1), presencePenalty: randomBetween(0.35, 0.7), frequencyPenalty: randomBetween(0.2, 0.45) },
</code></pre>
<p>通过 <code>randomBetween</code> 函数控制参数亦可以保证 LLM 生成的随机性。 <span class="rss-marginnote">⊕ (随机数最好不要与时间绑定，因为 <code>workflow</code> 可能与时间绑定)</span></p>
<h2>格式</h2>
<p>为了约束 LLM 的输出格式，以及让 LLM 输出更加胡乱的文章，我们通常需要考虑一下通过何种格式约束 LLM 的输出，以及，输出格式在 LLM 产出学术垃圾中到底起到了什么样的作用。</p>
<h3>Plain Text or JSON</h3>
<p>常见的 LLM 标榜为大型语言模型，那生成文本自然是得心应手，所以 Plain Text 可能是一个好的选项，但是，设想下面的流程，我们在创造一篇构式的时候，可能会像人类一样在大脑中构思 10 ~ 20 个各异的话题，这些话题可以是滑稽的、可笑的，也可以是一丝不苟、正儿八经的。同样，如果让 LLM 也完成同样的步骤，当然是非常好的，比如，为了生成一篇话题“随机”的文章，LLM 并不懂什么是“随机”，所以，它可能会根据概率推断到底什么是 ridiculous 或者 funny 的，这种概率有时并不可靠，而生成多个可能会弥补这种缺陷。</p>
<p>设想，针对一个 <code>random</code> 的话题，让 LLM 输出 ~ 20 个话题：</p>
<pre><code class="language-json">{
 theme: '',
 theme: '',
 ...
 theme: '',
}
</code></pre>
<p>然后再调用 LLM 本身对这些话题打分，然后挑出一个最搞笑的，似乎非常好。比如下面的流程：</p>
<pre class="mermaid">flowchart LR

A[Random seed / Prompt] --> B[LLM 生成 20 个主题<br>JSON]
B --> C[解析 JSON]
C --> D[LLM 对主题进行评分]
D --> E[选择最高分主题]
E --> F[生成文章正文]
</pre>
<p><strong>不过，为什么是 JSON，而不是 Plain Text？</strong></p>
<p>显然，JSON 为 LLM 提供了一种更强的<strong>结构化约束</strong>，而 Plain Text 很容易淹没在 LLM 的 System Prompt 或者 Context 的汪洋之中，那是概率都救不回来的。</p>
<h3>JSON 一定能好吗？</h3>
<p>这显然是不对的，因为 LLM 无法做到 100 % 听你的话生成一个<strong>字段不多不少，结构合法的，包含所需信息，去掉占位符的</strong>合法 JSON，事实上，在大多数情况下，LLM 生成的 JSON 都有很大的可能无法被 <code>JSON.parse()</code> 正确解析。 <span class="rss-marginnote">⊕ (问题可能是 LLM 太喜欢 Markdown 了，输出了 ``` 这样的代码块包裹，也有可能是自作聪明给你加了几个字段，太聪明了！当然，当下很多 API 支持选定 LLM 输出格式为 <code>json_format</code>，但是，即使你强暴地显式设置了这个字段，LLM 也不一定会听你的话，尤其是在温度这么高的情况下。)</span></p>
<p>不过，也可以狠狠 push &amp; punish LLM，相信经过足够多的轮数，LLM 也会松口吐出合法的 JSON 字段的。</p>
<blockquote>
<p>常见的手段包括而不仅限于：在 prompt 里强调 <strong>只输出 JSON 结构而不要其它任何的解释或者说明</strong> 或者 通过<em>巧妙的后处理</em>，譬如通过正则表达式从支离破碎的 JSON 字段中提取出合法的部分同时筛选掉可能存在问题的字段（例如多余字段或者什么）。不过，<strong>一切责任在于 LLM 方</strong>。任何在输出结束后的修正都是不得已的手段，还是要想办法在源头解决问题。</p>
</blockquote>
<h3>那 Plain Text？</h3>
<p>这是一个自然的想法，拼接 system prompt 是很好的，可能比 JSON 更加正常。缺点就是你的 system prompt 可能会变得又臭又长，这可能是 expensive 的，而且我们也不保证 LLM 会不会忽略这部分内容而将加权投向它们认为更加重要的部分？</p>
<h2>标签数目的平衡</h2>
<h3>平衡分类的数目</h3>
<p>我们之前讨论了这么多，都是鼓动 LLM 的创造性思维和发散思维的，但是，在创作领域中，我们并不希望 LLM 太过于发散，聚类在这里是有必要的，但是并不是所谓的 RAG 或者什么向量聚类，这里的聚类其实更像是一种归类。比如：</p>
<pre><code class="language-js">const TAGS = ['技术','生活','华中大','武大','武汉市','牙膏','卫生纸','食堂'，'马桶',...]
</code></pre>
<p>上面的 <code>TAGS</code> 只是一个非常简单的示例，假设我们有一个字段完备的对象，甚至能读到 <code>length</code>，我们就更加有说服力了——优先把少的 tags 传给 LLM，让 LLM 多生成这方面的内容，总体而言，保持一种<strong>势均力敌</strong>的局面。</p>
<p>比如考虑这样的算法：</p>
<pre><code class="language-ts">async function computeCategoryDistribution() {
  const whitelist = [
    &quot;Tech&quot;,
    &quot;Physics&quot;,
    &quot;Life&quot;,
    &quot;Earth&quot;,
    &quot;Math&quot;,
    &quot;People&quot;,
    &quot;Society&quot;,
    &quot;Culture&quot;,
    &quot;Ideas&quot;,
    &quot;Systems&quot;,
  ];

  const counts = new Map(whitelist.map((c) =&gt; [c, 0]));

  const files = await readdir(RESEARCH_DIR);

  for (const file of files) {
    const text = await readFile(join(RESEARCH_DIR, file), &quot;utf8&quot;);
    const fm = matter(text).data;

    const categories = fm.categories ?? [];

    for (const c of categories) {
      if (counts.has(c)) {
        counts.set(c, counts.get(c) + 1);
      }
    }
  }

  return Array.from(counts.entries()).sort((a, b) =&gt; a[1] - b[1]);
}
</code></pre>
<p>通过计算类别以及出现的次数，统计一下，按照从小到大排序，保证小的在前面，吸引 LLM 阅读兴趣。然后传给 LLM，让 LLM 尽量选取靠前较少出现的话题做文章。</p>
<h3>平衡标签中的发散性思维</h3>
<p>我们在上面的讨论中发现 tags 可能包含很多相关或者不相干的内容，比如牙膏、马桶或者食堂，这个时候<strong>发散性思维</strong>又发力了，我们可以让 LLM 每次只<em>随机</em>挑选几个它们喜欢的话题写成一篇文章，不过可能它们挑选随机性又要大打折扣，或许可以我们帮助一下，从少的类中再用随机数选取几个 categories，这样就能让 LLM 写出<s>精妙绝伦</s>的跨学科优质答辩了！ <span class="rss-marginnote">⊕ (不过这里似乎需要考虑一下优先级了...)</span></p>
<h2>八股文和 LLM 必须面对的惩罚</h2>
<p>这里的八股文不同于前面讨论的限制 LLM 输出的格式，而是偏向于一些文章中必须输出的、格式必须正确的固定格式。譬如一篇 Markdown 文章想要 smoothly 解析成 HTML，一个合法的 frontmatter 就是必须的。同样的，让 LLM 批量生产跨学科的发散思维学术垃圾，对 <a rel="noopener noreferrer" target="_blank" href="http://shitposts.org">shitposts.org</a> 的致谢以及必要的论文格式也是必要的，一方面，八股文保证了文章的正确解析和渲染，不至于让构建器 panic；另一方面，八股文也让 LLM 生产的学术垃圾看起来更加“学术”而非纯粹的“垃圾” <span class="rss-marginnote">⊕ (这里似乎和 JSON 存在一样的问题，有时 LLM 可能会忘记 YAML 结尾的 --- 导致解析直接挂掉，似乎可以想办法兜个底？)</span></p>
<p>如果 LLM 再构建时没有完成这些任务，等待它们的一定是惩罚和返工，不过这可能会消耗更多的 token 和时间，假使 LLM 真的有了独立思考的思维，不知道会不会嘲笑人类花如此多的时间和精力，专门搭建自动产生一点都不好笑的学术垃圾的自动化工作流。 <span class="rss-marginnote">⊕ (希望 LLM 不要在 CI 流程中把自己死锁了，最终直到 GitHub Workflow 超时了也没走出 失败 → 重试的循环)</span></p>
<h2>结语</h2>
<p>以上这些是我从建设 <a rel="noopener noreferrer" target="_blank" href="http://shitposts.org">shitposts.org</a> 中得到的经验，感觉个人感觉偏多了一些，技术上的价值并不是很多，很多都是自己感觉的，有的时候感觉自己就像一个 LLM 一样纯靠感觉输出 token，而我的脑子的算力还不如 10 年前的老古董显卡，模型还不到 0.1b，哎，这些感觉也不见得比 LLM 准确。</p>
<p>不过，如果你愿意赤石，可以访问 <a rel="noopener noreferrer" target="_blank" href="http://www.shitposts.org">www.shitposts.org</a> 一探究竟，看看 LLM 今天拉了几坨。</p>
<p>下面是 LLM 近期生产的妙妙文章：</p>
<pre><code class="language-json">[
  {
    &quot;title&quot;: &quot;On the Perturbation Theory of Badge-Reel Retraction Latency Gradients and Their Misrecognition as Indoor Astronomical Positioning Errors March 16, 2026 at 09:08:17 UTC&quot;,
    &quot;title_zh&quot;: &quot;关于徽章卷轴回缩延迟梯度的微扰理论及其被误识别为室内天文定位误差的研究 2026年3月16日 09:08:17 UTC&quot;
  },
  {
    &quot;title&quot;: &quot;Precession at the Micro-Scale: Vending Machine Spiral Drift as Astronomical Navigation Error, and the Acoustic-Archival Consequences of Queueing in High-Compliance Environments March 16, 2026 at 02:56:53 UTC&quot;,
    &quot;title_zh&quot;: &quot;微观尺度下的进动：自动售货机螺旋漂移作为天文导航误差，以及高顺应性环境中排队的声学档案后果 2026年3月16日 02:56:53 UTC&quot;
  },
  {
    &quot;title&quot;: &quot;层压指令单的符号熵与郊区合规生态：关于过塑文件边缘卷曲动力学的跨域研究 March 15, 2026 at 08:46:57 UTC&quot;
  },
  {
    &quot;title&quot;: &quot;郊区办公园区自动手部消毒 dispensers 的符号水文学与保修裁决机制研究 March 15, 2026 at 02:55:20 UTC&quot;
  },
  {
    &quot;title&quot;: &quot;Rheological Compliance and Actuarial Risk in Manual Hand Sanitizer Dispensing Mechanisms March 14, 2026 at 16:42:24 UTC&quot;,
    &quot;title_zh&quot;: &quot;手动消毒液分配机制中的流变顺应性与精算风险 2026年3月14日 16:42:24 UTC&quot;
  },
  {
    &quot;title&quot;: &quot;热力学档案学视角下塑料餐盘滑移行为的沉积地层模型研究：一项关于机构焦虑、家具人体工程学与微气候交互作用的跨学科探索 March 14, 2026 at 08:45:44 UTC&quot;
  },
  {
    &quot;title&quot;: &quot;The Thermodynamic Liability of Turnstile Bar Hesitation in High-Traffic Transit Hubs March 14, 2026 at 02:34:31 UTC&quot;,
    &quot;title_zh&quot;: &quot;高流量交通枢纽中闸机横杆犹豫的热力学责任 2026年3月14日 02:34:31 UTC&quot;
  },
  {
    &quot;title&quot;: &quot;Kinematic Entropy of Polyester Lanyards in Rotational Turnstile Fields March 13, 2026 at 16:51:16 UTC&quot;,
    &quot;title_zh&quot;: &quot;旋转闸机场中涤纶挂绳的运动熵 2026年3月13日 16:51:16 UTC&quot;
  },
  {
    &quot;title&quot;: &quot;打印机碳粉尘埃的档案学沉积与队列伦理：一种关于保修裁决板的民间传说分析 March 13, 2026 at 08:52:17 UTC&quot;
  },
  {
    &quot;title&quot;: &quot;A Thermodynamic-Semiotic Inquiry into the Plastic Cafeteria Tray Migration Protocol and Its Unanticipated Macroergonomic Implications March 13, 2026 at 02:37:55 UTC&quot;,
    &quot;title_zh&quot;: &quot;对塑料食堂托盘迁移协议及其未预料到的人机工程学宏观含义的热力学符号学探究 2026年3月13日 02:37:55 UTC&quot;
  },
  {
    &quot;title&quot;: &quot;非牛顿性气候与塑料餐盘的队列形变：基于走廊安全转闸仪式的跨模态协调分析 March 12, 2026 at 17:06:45 UTC&quot;
  },
  {
    &quot;title&quot;: &quot;The Axial Drift of Corridor Orientation in Suburban Office Parks: A Diachronic Study of Hand-Sanitizer Pump Hesitation as Navigational Error and Linguistic Speciation March 12, 2026 at 08:54:18 UTC&quot;,
    &quot;title_zh&quot;: &quot;郊区办公园区走廊朝向的轴向漂移：将消毒液泵按压犹豫视为导航误差与语言物种形成的历时研究 2026年3月12日 08:54:18 UTC&quot;
  },
  {
    &quot;title&quot;: &quot;铝制写字板夹角的 declination 误差：室内合规导航的民间维护研究 March 12, 2026 at 08:26:44 UTC&quot;
  },
  {
    &quot;title&quot;: &quot;The Caster Lock as Regulatory Membrane: Immunosemiotic Analysis of Stationary Swivel Chairs in Municipal Planning Cultures and Their Evolutionary Trajectory Toward Cosmological Household Integration March 12, 2026 at 02:43:56 UTC&quot;,
    &quot;title_zh&quot;: &quot;脚轮锁作为调节膜：对城市规划文化中固定转椅的免疫符号学分析及其朝向宇宙家庭整合的演化轨迹 2026年3月12日 02:43:56 UTC&quot;
  },
  {
    &quot;title&quot;: &quot;Liturgical Torsion in Commercial Vending Spirals: A Failed Religious Calendar Encoded in Spring Steel Fatigue, with Atmospheric Archival Implications and Insured Maintenance Logistics March 11, 2026 at 17:03:36 UTC&quot;,
    &quot;title_zh&quot;: &quot;商用售货螺旋中的礼拜仪式扭转：编码在弹簧钢疲劳中的失败宗教历法，及其大气档案意义与保险维护物流 2026年3月11日 17:03:36 UTC&quot;
  },
  {
  ...
  }
]
</code></pre>
<p>更多的类似文章在 <a rel="noopener noreferrer" target="_blank" href="http://shitposts.org">shitposts.org</a> 上能被找到，希望您生活愉快 :)</p>
<footer class="notes-list"><h3>Notae</h3><ul><li id="sn-list-0">就像你正在阅读的这个引言一样，以 LLM 开头，看似很高大上，实际上可能是 1tan√10 <a href="#ref-marker-0">↩</a></li><li id="sn-list-1">参见 <a rel="noopener noreferrer" target="_blank" href="https://ppio.com/docs/model/playground#%E5%8F%82%E6%95%B0%E9%85%8D%E7%BD%AE">PPIO 大语言模型参数控制文档</a> <a href="#ref-marker-1">↩</a></li><li id="sn-list-2">提请读者注意，我们在讨论这里的时候，<strong>幻觉</strong>并不是一个贬义词，相反，我们是刻意利用 LLM 的幻觉，因为我们鼓励开放、自由、不为传统拘束的文学创作范式 <a href="#ref-marker-2">↩</a></li><li id="sn-list-3">随机数最好不要与时间绑定，因为 <code>workflow</code> 可能与时间绑定 <a href="#ref-marker-3">↩</a></li><li id="sn-list-4">问题可能是 LLM 太喜欢 Markdown 了，输出了 ``` 这样的代码块包裹，也有可能是自作聪明给你加了几个字段，太聪明了！当然，当下很多 API 支持选定 LLM 输出格式为 <code>json_format</code>，但是，即使你强暴地显式设置了这个字段，LLM 也不一定会听你的话，尤其是在温度这么高的情况下。 <a href="#ref-marker-4">↩</a></li><li id="sn-list-5">不过这里似乎需要考虑一下优先级了... <a href="#ref-marker-5">↩</a></li><li id="sn-list-6">这里似乎和 JSON 存在一样的问题，有时 LLM 可能会忘记 YAML 结尾的 --- 导致解析直接挂掉，似乎可以想办法兜个底？ <a href="#ref-marker-6">↩</a></li><li id="sn-list-7">希望 LLM 不要在 CI 流程中把自己死锁了，最终直到 GitHub Workflow 超时了也没走出 失败 → 重试的循环 <a href="#ref-marker-7">↩</a></li></ul></footer>]]></content:encoded>
            <author>gengyue</author>
        </item>
        <item>
            <title><![CDATA[用 Ink 写 TUI！]]></title>
            <link>https://www.gengyue.dev/blog/ink-tui</link>
            <guid isPermaLink="false">https://www.gengyue.dev/blog/ink-tui</guid>
            <pubDate>Tue, 24 Feb 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[如果你熟悉 React，又想快速搞一个 CLI 应用，Ink 是个超方便的选择。我用它做了 HUST-Chifan 的 TUI CLI，从 Box 布局到 useInput 都像写 React App 一样顺手，轻松搭出能跑就行的交互界面。]]></description>
            <content:encoded><![CDATA[<p><a rel="noopener noreferrer" target="_blank" href="https://github.com/vadimdemedes/ink">Ink</a> 是一个能让你用写 React 应用的思路写终端应用的库，并且由于它是一个 React 的渲染器，所以 React 的大多数特性仍然可用，所以可以像写 React App 那样写 Ink。 <span class="rss-marginnote">⊕ (Claude Code 的 CLI 就曾使用 Ink 构建)</span></p>
<p>使用 <code>npx create-ink-app --typescript my-ink-cli</code> 创建一个新的 ink 项目。</p>
<p>Ink 使用了类似 React Native 的 Box 布局，比如，一个基本的 Ink App 可能长这样：</p>
<pre><code class="language-tsx">import React from 'react';
import {render, Box, Text} from 'ink';

const App:React.FC = () =&gt; (
	&lt;Box&gt;
	   &lt;Text color=&quot;blue&quot;&gt;Hello World&lt;/Text&gt;
	&lt;/Box&gt;
);

render(&lt;App /&gt;);
</code></pre>
<blockquote>
<p>这里 <code>Box</code> 不能嵌套在 <code>Text</code> 中</p>
</blockquote>
<p><code>Text</code> 接受一些常见的参数用来控制文字的行为，比如 <code>color</code>/<code>backgroundColor</code>/<code>bold</code>/<code>underline</code>/<code>dimColor</code>/<code>strikethrough</code> ...  <span class="rss-sidenote">(<a rel="noopener noreferrer" target="_blank" href="https://github.com/vadimdemedes/ink?tab=readme-ov-file#text">vadimdemedes/ink: 🌈 React for interactive command-line apps</a>)</span></p>
<p><code>Box</code> 是一个常见的盒子布局，本质上是一种 <code>Flexbox</code>， 可以通过 <code>flexDirection: 'row' | 'column'</code>/ <code>gap</code>/<code>margin</code>/<code>padding</code>/<code>width</code>/<code>height</code>/<code>justify-content</code>/<code>align-items</code>/... 等参数控制样式和行为，和 CSS 控制基本一样。 <span class="rss-sidenote">(<a rel="noopener noreferrer" target="_blank" href="https://github.com/vadimdemedes/ink?tab=readme-ov-file#box">vadimdemedes/ink: 🌈 React for interactive command-line apps</a>)</span></p>
<p>Ink 也提供了一些 hooks，比如 <code>useInput</code>  <span class="rss-sidenote">(<a rel="noopener noreferrer" target="_blank" href="https://github.com/vadimdemedes/ink?tab=readme-ov-file#useinputinputhandler-options">vadimdemedes/ink: 🌈 React for interactive command-line apps</a>)</span> ，可以用于管理输入状态，比如创建一个简单的命令系统：</p>
<pre><code class="language-tsx">import {useInput} from 'ink';

const UserInput = () =&gt; {
	useInput((input, key) =&gt; {
		if (input === 'q') {
			process.exit(0)
		}

		if (key.leftArrow) {
			// Left arrow key pressed
		}
	});

	return …
};
</code></pre>
<p>我用它构建了新的 <a rel="noopener noreferrer" target="_blank" href="https://github.com/gengyue2468/HUST-Chifan">HUST-Chifan</a>，提供了一个还不错的<s>能跑就行</s>的 <a rel="noopener noreferrer" target="_blank" href="https://github.com/gengyue2468/HUST-Chifan/tree/master/backend">Hono 后端</a>)和一个同样 <s>能跑就行</s>的 <a rel="noopener noreferrer" target="_blank" href="https://github.com/gengyue2468/HUST-Chifan/tree/master/tui">TUI</a> 。使用 bun 快速冷启动项目并在本地跑起来 terminal！</p>
<p>最终结果：</p>
<figure><img loading="lazy" decoding="async" src="/static/tech/hust-chifan-cli.webp" alt="HUST-Chifan Ink TUI CLI"><figcaption>HUST-Chifan Ink TUI CLI</figcaption></figure>
<footer class="notes-list"><h3>Notae</h3><ol><li id="sn-list-0"><a rel="noopener noreferrer" target="_blank" href="https://github.com/vadimdemedes/ink?tab=readme-ov-file#text">vadimdemedes/ink: 🌈 React for interactive command-line apps</a> <a href="#ref-marker-0">↩</a></li><li id="sn-list-1"><a rel="noopener noreferrer" target="_blank" href="https://github.com/vadimdemedes/ink?tab=readme-ov-file#box">vadimdemedes/ink: 🌈 React for interactive command-line apps</a> <a href="#ref-marker-1">↩</a></li><li id="sn-list-2"><a rel="noopener noreferrer" target="_blank" href="https://github.com/vadimdemedes/ink?tab=readme-ov-file#useinputinputhandler-options">vadimdemedes/ink: 🌈 React for interactive command-line apps</a> <a href="#ref-marker-2">↩</a></li><li class="marginnote-item" id="sn-list-3">Claude Code 的 CLI 就曾使用 Ink 构建 <a href="#ref-marker-3">↩</a></li></ol></footer>]]></content:encoded>
            <author>gengyue</author>
        </item>
        <item>
            <title><![CDATA[自建 Tailscale Derp 服务器]]></title>
            <link>https://www.gengyue.dev/blog/derp-server</link>
            <guid isPermaLink="false">https://www.gengyue.dev/blog/derp-server</guid>
            <pubDate>Sun, 22 Feb 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[CNY 49/年搞定一台香港 KVM VPS，512MB 内存刚好跑 Tailscale 自建 DERP，移动友好又便宜！教程手把手教你配置 Go、下载编译 Derp、用 systemd 持久化，还顺便加上防火墙双重保护，保证只有你 tailnet 的节点能用。实测延迟从广州跑香港节点也相当爽，傻瓜式操作，省钱又安全，完美小鸡升级计划。]]></description>
            <content:encoded><![CDATA[<p><a rel="noopener noreferrer" target="_blank" href="https://ccp.bitsflow.cloud/">BitsFlowCloud</a> 推出了 CNY ￥ 49/yr 的<s>穷鬼</s> KVM VPS 套餐，竟然还是对移动友好的香港 VPS，于是顶着每月 ~ $0.5 USD 的花销整了一台，512 MB 的内存正好跑一个 Tailscale 自建 Derp 服务器比较适合!  <span class="rss-marginnote">⊕ (Tailscale 是著名的内网组网工具，不过由于其在大陆没有 Derp 服务器，导致 P2P 打洞比较慢，而自建服务器可以在一定程度上缓解这个问题)</span></p>
<p>2026/03/10 更新：现在 Go 需要大于等于 1.26.1 的版本了....</p>
<p>根据<a rel="noopener noreferrer" target="_blank" href="https://tailscale.com/docs/reference/derp-servers/custom-derp-servers#run-a-derp-server-from-source">官方的最新教程</a>现在只需要 <code>go install</code> 就可以了</p>
<pre><code class="language-bash">go install tailscale.com/cmd/derper@latest
</code></pre>
<p>通过：</p>
<pre><code class="language-bash">sudo derper --hostname=example.com
</code></pre>
<p>启动 derp 服务，不过后面持久化运行还是按照创建 systemd 服务运行就行</p>
<blockquote>
<p>哎，这时效性，似乎仓库都没了...</p>
</blockquote>
<p><s>根据教程首先要配置 Go 环境：</s></p>
<pre><code class="language-bash">wget https://go.dev/dl/go1.20.7.linux-amd64.tar.gz
sudo rm -rf /usr/local/go
sudo tar -C /usr/local -xzf go1.20.7.linux-amd64.tar.gz
export PATH=$PATH:/usr/local/go/bin
</code></pre>
<p><s>然后下载 &amp; 编译 Derp 源码：</s></p>
<pre><code class="language-bash">wget https://github.com/tailscale/derper/releases/download/v0.3.0/derper-linux-amd64 -O derper
sudo mv derper /usr/local/bin/derper
sudo chmod +x /usr/local/bin/derper
</code></pre>
<p><s>测试一下，然后 <code>Ctrl + C</code> 杀死：</s></p>
<pre><code class="language-bash">derper -h
</code></pre>
<p>然后创建 <code>systemd</code> 服务持久化运行，编辑 <code>/etc/systemd/system/derper.service</code>： <span class="rss-marginnote">⊕ (这里的 <code>--verify-clients</code> 参数可防止被白嫖，确保只有你 tailnet 内的节点能使用此中继)</span></p>
<pre><code>[Unit]  
Description=DERP Server  
After=network.target  
  
[Service]  
User=root  
ExecStart=/usr/local/bin/derper \  
-hostname my-elegant-derp-domain \  
-certmode letsencrypt \  
-certdir /var/lib/derper \  
-a :443 \
--verify-clients
Restart=always  
RestartSec=5  
LimitNOFILE=1048576  
  
[Install]  
WantedBy=multi-user.target
</code></pre>
<p>源神，启动！</p>
<pre><code class="language-bash">sudo systemctl daemon-reload
sudo systemctl enable derper
sudo systemctl start derper
sudo systemctl status derper
</code></pre>
<p>然后 <code>curl</code> 测试一下公网的域名，不出意外应该能看到一个 Derp 服务的说明性 HTML。 <span class="rss-marginnote">⊕ (这里注意关掉 Cloudflare 的橙云)</span></p>
<p>好耶，这就差不多搞定了，还是相当傻瓜式操作的。</p>
<p>~~不过为了保证安全，可以采用一些更加激进 &amp; 极端的方式来保护一下，比如在服务器上：<br>
~~</p>
<pre><code class="language-bash">code here has been expired or deprecated.
</code></pre>
<p><s>这样 <code>ufw</code> 防火墙只会允许来自我的 Tailscale 内部 tailnet 的 <code>100.101.102.0/24</code> 这个网段的 Tailscale 流量走自建 Derp 服务器，双重保障哈哈。</s></p>
<blockquote>
<p>靠，千万不要去尝试用 <code>ufw</code> 或者 <code>nginx</code> 等各种方式隐藏你的 derp 服务，不然到时候连不上 <code>ssh</code> 只能去 <code>VNC</code> 救小鸡的时候就狼狈了...</p>
</blockquote>
<p>搞定服务端配置之后去 <a rel="noopener noreferrer" target="_blank" href="https://login.tailscale.com/admin/acls/file">Access controls</a> 找到 <code>Edit File</code>，添加类似下面这样的配置：</p>
<pre><code class="language-json">&quot;derpMap&quot;: {
		&quot;Regions&quot;: {
			&quot;900&quot;: {
				&quot;RegionID&quot;:   900,
				&quot;RegionCode&quot;: &quot;hk&quot;,
				&quot;RegionName&quot;: &quot;Hong Kong Self&quot;,
				&quot;Nodes&quot;: [
					{
						&quot;Name&quot;:     &quot;hk-1&quot;,
						&quot;RegionID&quot;: 900,
						&quot;HostName&quot;: &quot;my-elegant-domain&quot;,
						&quot;DERPPort&quot;: 443,
					},
				],
			},
		},
	},
</code></pre>
<p>好耶，现在可以用 <code>tailscale netcheck</code> 测试一下了，当自建 Derp 服务器挂了的时候也会自动 fallback 到 Tailscale 的 Derp 服务器，所以还算不错！</p>
<p>最终结果（在我的本地 Laptop 上测试）： <span class="rss-marginnote">⊕ (忽略时间戳，因为是好几天之后才想起来写这篇 logbook 临时补的测试哈哈)</span></p>
<div class="fullwidth-content">
<pre><code class="language-bash">gengyue@gengyue-laptop:~$ tailscale netcheck
2026/02/25 00:14:02 portmap: monitor: gateway and self IP changed: gw=172.30.192.1 self=172.30.207.69

Report:
        * Time: 2026-02-24T16:14:05.311261679Z
        * UDP: true
        * IPv4: yes, 111.15.81.242:10298
        * IPv6: no, but OS has support
        * MappingVariesByDestIP: false
        * PortMapping:
        * CaptivePortal: false
        * Nearest DERP: Hong Kong Self
        * DERP latency:
                -  hk: 62.6ms  (Hong Kong Self)
                - tok: 79.2ms  (Tokyo)
                - sin: 100ms   (Singapore)
                - blr: 140.2ms (Bengaluru)
                - nue: 211.5ms (Nuremberg)
                - syd: 215ms   (Sydney)
                - sfo: 234.8ms (San Francisco)
                - hel: 235.4ms (Helsinki)
                - dfw: 261ms   (Dallas)
                - lax: 268.2ms (Los Angeles)
                - sea: 292.1ms (Seattle)
                - den: 296.7ms (Denver)
                - iad: 296.7ms (Ashburn)
                - lhr: 311.9ms (London)
                - ord: 318.9ms (Chicago)
                - par: 320.4ms (Paris)
                - hnl: 323.7ms (Honolulu)
                - ams: 325.8ms (Amsterdam)
                - tor: 326.4ms (Toronto)
                - mia: 333.5ms (Miami)
                - waw: 334.5ms (Warsaw)
                - nyc: 335.8ms (New York City)
                - mad: 338.4ms (Madrid)
                - nai: 377.3ms (Nairobi)
                - sao: 386.6ms (São Paulo)
                - dbi: 404.8ms (Dubai)
                - jnb: 506.8ms (Johannesburg)
                - fra:         (Frankfurt)
                - hkg:         (Hong Kong)
</code></pre>
</div>
<p>emm...只能说效果是有的，可能没有那么明显就是...至于 rn 小鸡和腾讯云的小鸡，一个走西雅图的 Tailscale 节点去了，另一个连的旧金山节点（？这很奇怪，不太明白） <span class="rss-marginnote">⊕ (按理说作为一个在广州的小鸡至少应该走香港啊，走旧金山是何意味啊（挠头)</span></p>
<footer class="notes-list"><h3>Notae</h3><ul><li id="sn-list-0">Tailscale 是著名的内网组网工具，不过由于其在大陆没有 Derp 服务器，导致 P2P 打洞比较慢，而自建服务器可以在一定程度上缓解这个问题 <a href="#ref-marker-0">↩</a></li><li id="sn-list-1">这里的 <code>--verify-clients</code> 参数可防止被白嫖，确保只有你 tailnet 内的节点能使用此中继 <a href="#ref-marker-1">↩</a></li><li id="sn-list-2">这里注意关掉 Cloudflare 的橙云 <a href="#ref-marker-2">↩</a></li><li id="sn-list-3">忽略时间戳，因为是好几天之后才想起来写这篇 logbook 临时补的测试哈哈 <a href="#ref-marker-3">↩</a></li><li id="sn-list-4">按理说作为一个在广州的小鸡至少应该走香港啊，走旧金山是何意味啊（挠头 <a href="#ref-marker-4">↩</a></li></ul></footer>]]></content:encoded>
            <author>gengyue</author>
        </item>
        <item>
            <title><![CDATA[奇怪的 Cloudflare Tunnel]]></title>
            <link>https://www.gengyue.dev/blog/weird-cloudflare-tunnel</link>
            <guid isPermaLink="false">https://www.gengyue.dev/blog/weird-cloudflare-tunnel</guid>
            <pubDate>Sat, 14 Feb 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[奇怪的 Cloudflare Tunnel：用 Tunnel + Zero Trust 访问家里的 Mac，通过 SSH，  Windows 客户端直接连上，Mac 端用 LaunchDaemon 自启和后台保活，整个流程很神奇，但是能跑！]]></description>
            <content:encoded><![CDATA[<p>前几天用 Tailscale 组了个大内网，想连家里的 iMac 但是不知道为啥这个 Tailscale GUI 每次重启都要重新认证，好麻烦的，于是准备用 <a rel="noopener noreferrer" target="_blank" href="https://www.cloudflare-cn.com/products/tunnel/">Cloudflare Tunnel</a>  <span class="rss-marginnote">⊕ (<strong>Cloudflare Tunnel</strong> 是一款隧道软件，旨在为应用程序和基础设施提供快速、安全的加密流量传输，同时隐藏 Web 服务器的 IP 地址，防止直接攻击。它特别适用于没有公网 IP 的场景，帮助用户从公网安全访问内网服务。)</span>结合 <a rel="noopener noreferrer" target="_blank" href="https://www.cloudflare-cn.com/learning/security/glossary/what-is-zero-trust/">Zero Trust</a> 给家宽打个洞，只要能连上 ssh 就行！</p>
<p>先给服务端 （Mac） 和客户端 (Windows) 装上 <code>cloudflared</code>，然后在 mac 上写一个妙妙 <code>config.yml</code> 文件，大概是：</p>
<pre><code class="language-yml">tunnel: homelab
credentials-file:path-to-cert

ingress:
  - hostname: xxx.xxx.xxx
    service: ssh://localhost:22
    
- service:http_status:404
</code></pre>
<p>写完之后 <code>cloudflared tunnel run homelab</code> 就 ok。</p>
<p>然后去搞 Cloudflare 的 ZeroTrust，大失败！没有信用卡，连免费套餐都要绑定信用卡！</p>
<p>查了一下发现可以把<code>https://one.dash.cloudflare.com</code>后面的路径全部删除，这样就能跳过去了。不过这样没法管理应用程序了 <span class="rss-marginnote">⊕ (会循环跳转到添加付款方式那一步)</span>，坏，自然也就没法把服务规则 (Access Rule) 绑定上去了，这不是废了！</p>
<p>试了一下发现其实可以先创建规则，这样就好了，哎，当时在 get started 的时候搞太快忽略过去了，坏。然后可以加个认证方式，比如用 GitHub，根据官方的指导创建一个 OAuth App 认证就可以了！</p>
<p>然后在 Windows 上测试一下连接：</p>
<pre><code class="language-bash">cloudflared access ssh --hostname mac.012607.xyz
</code></pre>
<p>会弹出一个 OAuth 浏览器窗口，验证一下，好耶，连上了，下面编辑 <code>~\.ssh\config</code>:</p>
<pre><code>Host mac
  HostName host
  User username
  ProxyCommand cloudflared access ssh --hostname %h
</code></pre>
<p>这样就可以通过 <code>ssh mac</code> 直接建立连接了，相当的方便，不用记那些长串的乱七八糟的命令了！</p>
<p>不过似乎还是那个问题，macOS 一重启 tunnel 就断开了，还是要手动开一下，根本就不方便！</p>
<p>一开始试了一下什么<code>sudo cloudflared service install``sudo launchctl kickstart -k system/com.cloudflare.cloudflared</code>，结果 <code>sudo reboot</code> 重启之后发现只启动了 Cloudflare Tunnel 的核心服务，绑定的 Tunnel 压根没启动，我去，那有什么用？ <span class="rss-marginnote">⊕ (奇怪，这里的症候和 Tailscale 的有点像，哎，这 macOS 的逻辑真是奇怪啊，有点搞不懂！)</span></p>
<p>哎，和 AI 对线了半天，告诉我可能需要自己创建一个自启 + 后台保活项了，比如 <code>/Library/LaunchDaemons/com.cloudflare.homelab.plist</code>：</p>
<pre><code class="language-xml">&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;
&lt;!DOCTYPE plist PUBLIC &quot;-//Apple//DTD PLIST 1.0//EN&quot; &quot;http://www.apple.com/DTDs/PropertyList-1.0.dtd&quot;&gt;
&lt;plist version=&quot;1.0&quot;&gt;
    &lt;dict&gt;
        &lt;key&gt;Label&lt;/key&gt;
        &lt;string&gt;com.cloudflare.homelab&lt;/string&gt;
        &lt;key&gt;ProgramArguments&lt;/key&gt;
        &lt;array&gt;
            &lt;string&gt;/usr/local/bin/cloudflared&lt;/string&gt;
            &lt;string&gt;tunnel&lt;/string&gt;
            &lt;string&gt;run&lt;/string&gt;
            &lt;string&gt;homelab&lt;/string&gt;
        &lt;/array&gt;
        &lt;key&gt;RunAtLoad&lt;/key&gt;
        &lt;true /&gt;
        &lt;key&gt;KeepAlive&lt;/key&gt;
        &lt;true /&gt;
        &lt;key&gt;StandardOutPath&lt;/key&gt;
        &lt;string&gt;/Library/Logs/cloudflared-homelab.out.log&lt;/string&gt;
        &lt;key&gt;StandardErrorPath&lt;/key&gt;
        &lt;string&gt;/Library/Logs/cloudflared-homelab.err.log&lt;/string&gt;
    &lt;/dict&gt;
&lt;/plist&gt;
</code></pre>
<p>哎，结果发现还是不行，看了眼日志，竟然发现是 Tunnel 找不到证书导致的！</p>
<pre><code class="language-bash">Cannot determine default origin certificate path
error parsing tunnel ID: client didn't specify origincert path
</code></pre>
<p>哦哦哦，原来是执行的时候找不到 <code>config.yml</code> 导致的啊，手动加上：</p>
<pre><code class="language-xml">&lt;array&gt;
    &lt;string&gt;/usr/local/bin/cloudflared&lt;/string&gt;
    &lt;string&gt;--config&lt;/string&gt;
    &lt;string&gt;/Users/xxx/.cloudflared/config.yml&lt;/string&gt;
    &lt;string&gt;tunnel&lt;/string&gt;
    &lt;string&gt;run&lt;/string&gt;
    &lt;string&gt;homelab&lt;/string&gt;
&lt;/array&gt;
</code></pre>
<p>然后重启服务:</p>
<pre><code class="language-bash">sudo launchctl unload /Library/LaunchDaemons/com.cloudflare.homelab.plist
sudo launchctl load /Library/LaunchDaemons/com.cloudflare.homelab.plist
sudo launchctl start com.cloudflare.homelab
</code></pre>
<p>查找一下服务：</p>
<pre><code class="language-bash">ps aux | grep cloudflared
</code></pre>
<p>哇哦，终于看到 <code>tunnel run homelab</code> 了，好耶，这下算是搞定了！</p>
<p>最终结果：</p>
<div class="fullwidth-content">
<pre class="mermaid">flowchart TD
    %% 外部客户端
    A[Windows 客户端] --> B[Cloudflare Access / Zero Trust]

    %% Cloudflare 平台
    subgraph Cloudflare
        B1["OAuth 验证 (GitHub)"]
        C1[Cloudflare Tunnel]
        I1["Tunnel 后台服务"]
    end

    %% Mac 端配置
    subgraph Mac
        D1["Mac 服务器 (SSH)"]
        E1[cloudflared 安装]
        F1["config.yml 配置 Tunnel"]
        G1["LaunchDaemon 自启 + KeepAlive"]
    end

    %% 连接箭头
    B --> B1
    B --> C1
    C1 --> I1
    C1 --> D1
    D1 --> E1
    E1 --> F1
    F1 --> G1
</pre>
</div>
<footer class="notes-list"><h3>Notae</h3><ul><li id="sn-list-0"><strong>Cloudflare Tunnel</strong> 是一款隧道软件，旨在为应用程序和基础设施提供快速、安全的加密流量传输，同时隐藏 Web 服务器的 IP 地址，防止直接攻击。它特别适用于没有公网 IP 的场景，帮助用户从公网安全访问内网服务。 <a href="#ref-marker-0">↩</a></li><li id="sn-list-1">会循环跳转到添加付款方式那一步 <a href="#ref-marker-1">↩</a></li><li id="sn-list-2">奇怪，这里的症候和 Tailscale 的有点像，哎，这 macOS 的逻辑真是奇怪啊，有点搞不懂！ <a href="#ref-marker-2">↩</a></li></ul></footer>]]></content:encoded>
            <author>gengyue</author>
        </item>
        <item>
            <title><![CDATA[简单的标记嵌套处理]]></title>
            <link>https://www.gengyue.dev/blog/nested-inline-parser</link>
            <guid isPermaLink="false">https://www.gengyue.dev/blog/nested-inline-parser</guid>
            <pubDate>Fri, 13 Feb 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[实现一个支持嵌套结构的轻量级文本解析器，探索括号匹配、递归解析与简单 AST 构建。]]></description>
            <content:encoded><![CDATA[<p>哎，博客系统偷懒是 vibe 出来的，其中自定义语法还蛮多的，比如 <code>&lt;!-- MNOTE_1 --&gt;</code>  <span class="rss-marginnote">⊕ (这里的 <code>content</code> 支持嵌套的 markdown 语法)</span>，好奇背后的原理是什么，研究一下。</p>
<p>目标：</p>
<pre><code>我很[danger:危险]，请[warn:小心]，但我也有[success:优点]。
-&gt; &lt;p&gt;&lt;span&gt;我很&lt;/span&gt;&lt;span class=&quot;text-red-500 bg-red-50 dark:bg-red-900/25&quot;&gt;&lt;span&gt;危险&lt;/span&gt;&lt;/span&gt;&lt;span&gt;，请&lt;/span&gt;&lt;span class=&quot;text-yellow-500 bg-yellow-50 dark:bg-yellow-900/25&quot;&gt;&lt;span&gt;小心&lt;/span&gt;&lt;/span&gt;&lt;span&gt;，但我也有&lt;/span&gt;&lt;span class=&quot;text-sky-500 bg-sky-50 dark:bg-sky-900/25&quot;&gt;&lt;span&gt;优点&lt;/span&gt;&lt;/span&gt;&lt;span&gt;。&lt;/span&gt;&lt;/p&gt;
</code></pre>
<p>emm，一开始想想用正则表达式匹配一下，比如：</p>
<pre><code class="language-js">function parseSentence(sentence) {
  const regex = /\[([a-z]+):(&lt;!-- SNOTE_0 --&gt;]+)\]/g;
  let match;
  let lastIndex = 0;
  const result = [];

  while ((match = regex.exec(sentence)) !== null) {
    if (match.index &gt; lastIndex) {
      result.push({
        type: &quot;text&quot;,
        text: sentence.slice(lastIndex, match.index),
      });
    }
    result.push({
      type: match[1],
      text: match[2],
    });

    lastIndex = regex.lastIndex;
  }
  if (lastIndex &lt; sentence.length) {
    result.push({
      type: &quot;text&quot;,
      text: sentence.slice(lastIndex),
    });
  }
  return result;
}
</code></pre>
<p>通过记录 <code>lastIndex</code> 和正则表达式的匹配把文字对应的 <code>type</code> 和 <code>content</code> 塞到 <code>result</code> 数组里头。这对于普通语法来说很不错，不过遇到下面这种情况，大失败！ <span class="rss-marginnote">⊕ (正则表达式本质上无法正确处理任意层级的嵌套结构（除非引入扩展特性）。因为括号嵌套属于「上下文无关语言」，而经典正则只能处理「正则语言」。。)</span></p>
<pre><code>[danger:危机中孕育着[success:希望]，请[warn:务必小心]]！
-&gt; &lt;p&gt;&lt;span&gt;&lt;/span&gt;&lt;span class=&quot;text-red-500 bg-red-50 dark:bg-red-900/25&quot;&gt;&lt;span&gt;危机中孕育着&lt;/span&gt;&lt;span class=&quot;text-sky-500 bg-sky-50 dark:bg-sky-900/25&quot;&gt;&lt;span&gt;希望&lt;/span&gt;&lt;/span&gt;&lt;span&gt;，请&lt;/span&gt;&lt;span class=&quot;text-yellow-500 bg-yellow-50 dark:bg-yellow-900/25&quot;&gt;&lt;span&gt;务必小心&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;！&lt;/span&gt;&lt;/p&gt;

这是一个[warn:嵌套了[danger:危险]和[success:成功]的警告]，请注意！
-&gt; &lt;p&gt;&lt;span&gt;这是一个&lt;/span&gt;&lt;span class=&quot;text-yellow-500 bg-yellow-50 dark:bg-yellow-900/25&quot;&gt;&lt;span&gt;嵌套了&lt;/span&gt;&lt;span class=&quot;text-red-500 bg-red-50 dark:bg-red-900/25&quot;&gt;&lt;span&gt;危险&lt;/span&gt;&lt;/span&gt;&lt;span&gt;和&lt;/span&gt;&lt;span class=&quot;text-sky-500 bg-sky-50 dark:bg-sky-900/25&quot;&gt;&lt;span&gt;成功&lt;/span&gt;&lt;/span&gt;&lt;span&gt;的警告&lt;/span&gt;&lt;/span&gt;&lt;span&gt;，请注意！&lt;/span&gt;&lt;/p&gt;
</code></pre>
<p>哎，看起来需要手动维护一个匹配完整 <code>[</code> 和 <code>]</code> 闭合嵌套的逻辑了！思路可以是，先找到某个 <code>[</code>标记，找到与之对应的 <code>]</code> 标记，对于这部分，前面和后面递归解析，对于中间部分，由于结构是 <code>type:content</code> 于是可以按 <code>:</code> <code>split</code> 成两部分，作为 <code>type</code> 和 <code>content</code> ，对于新的 <code>content</code> 继续递归解析，哇哦，虽然时间复杂度高了点，但是能跑！</p>
<p>定义 <code>interface</code>：</p>
<pre><code class="language-ts">interface TextPart {
  type: string;
  content: string | TextPart[];
}
</code></pre>
<pre><code class="language-ts">function parseText(text: string): TextPart[] {
  const start = text.indexOf(&quot;[&quot;);
  if (start === -1) {
    return [{ type: &quot;text&quot;, content: text }]; // 如果不存在起始符 [，说明是纯文本
  }

  const end = findMatchingBracket(text, start); // 找到与 start 对应的 ]，保证嵌套正确
  if (end === -1) {
    return [{ type: &quot;text&quot;, content: text }];
  }

  const before = text.slice(0, start);
  const inside = text.slice(start + 1, end); //格式 type:content，按 : 分割，解析 type 和 content
  const after = text.slice(end + 1);

  const [type, ...rest] = inside.split(&quot;:&quot;); // type 作为冒号前的部分，content 作为冒号后的部分
  const content = rest.join(&quot;:&quot;); // join 回内容，继续解析 content

  return [
    ...parseText(before),
    { type, content: parseText(content) },
    ...parseText(after),
  ];
}
</code></pre>
<p><code>findMatchingBracket</code> 函数是用于寻找与 <code>start</code> 对应的 <code>[</code> 对应结束的 <code>]</code> 标签的 <code>index</code>，逻辑如下：</p>
<pre><code class="language-ts">function findMatchingBracket(text: string, start: number): number {
  let count = 0;
  for (let i = start; i &lt; text.length; i++) {
    if (text[i] === &quot;[&quot;) count++;
    else if (text[i] === &quot;]&quot;) count--;
    if (count === 0) return i;
  }
  return -1;
}
</code></pre>
<p>遇到 <code>[</code> 让 <code>count</code> 自增，遇到 <code>]</code>让 <code>count</code> 自减，直到 <code>count</code> 为 0 表明嵌套闭合。</p>
<p>配合上前端嵌套渲染逻辑：</p>
<pre><code class="language-ts">function renderPart(part: TextPart, index: number): React.ReactNode {
  if (part.type === &quot;text&quot;) {
    return &lt;span key={index}&gt;{part.content as string}&lt;/span&gt;;
  }

  const children = (part.content as TextPart[]).map((child, i) =&gt;
    renderPart(child, i),
  );

  switch (part.type) {
    case &quot;danger&quot;:
      // danger render goes here
    case &quot;warn&quot;:
      // warn render goes here
    case &quot;success&quot;:
      // success render goes here
    default:
      return &lt;span key={index}&gt;{children}&lt;/span&gt;;
  }
}
</code></pre>
<p>最终结果：</p>
<div class="embed-block"><iframe src="https://d.gengyue.site/embed/parseCustom" title="解析自定义文本语法" loading="lazy"></iframe>
</div>
<p>哎，不过发现这一套还是有些问题的： <span class="rss-marginnote">⊕ (按理说应该有错误处理的，而不是简单的直接当作自然语言输出了...或者至少对脚注这样的 Markdown 语法要有一定的的豁免/例外处理)</span></p>
<ul>
<li>要求语法必须严格匹配，但是一旦用于 Markdown 渲染可能某些地方需要表示脚注这套解析就废了。</li>
<li>同上，如果有个伙计故意不闭合<code>[]</code> 或者 <code>: </code>写成<code>：</code> 都会导致整套逻辑垮台。</li>
<li>算法并非很优，时间复杂度 O(n^2)，很屎山。</li>
</ul>
<p>所以为啥不直接用成熟的 <code>markdown-it</code> 之类的库呢 🤔</p>
<footer class="notes-list"><h3>Notae</h3><ol><li id="sn-list-0">\ <a href="#ref-marker-0">↩</a></li><li class="marginnote-item" id="sn-list-1"><content> <a href="#ref-marker-1">↩</a></li><li class="marginnote-item" id="sn-list-2">这里的 <code>content</code> 支持嵌套的 markdown 语法 <a href="#ref-marker-2">↩</a></li><li class="marginnote-item" id="sn-list-3">正则表达式本质上无法正确处理任意层级的嵌套结构（除非引入扩展特性）。因为括号嵌套属于「上下文无关语言」，而经典正则只能处理「正则语言」。。 <a href="#ref-marker-3">↩</a></li><li class="marginnote-item" id="sn-list-4">按理说应该有错误处理的，而不是简单的直接当作自然语言输出了...或者至少对脚注这样的 Markdown 语法要有一定的的豁免/例外处理 <a href="#ref-marker-4">↩</a></li></ol></footer>]]></content:encoded>
            <author>gengyue</author>
        </item>
        <item>
            <title><![CDATA[用 Tailscale 组大内网！]]></title>
            <link>https://www.gengyue.dev/blog/tailscale</link>
            <guid isPermaLink="false">https://www.gengyue.dev/blog/tailscale</guid>
            <pubDate>Tue, 10 Feb 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[文章记录了作者尝试用 Tailscale 给多台设备组建大内网的过程，从最初尝试 Wireguard 到最终用 Tailscale 解决掉线问题，包括不同操作系统的体验差异，并分享了实际设备状态和使用心得。]]></description>
            <content:encoded><![CDATA[<p>前天试着用 Wireguard 给手里的两台小鸡和 Laptop 组个大内网，一开始跑的还行，结果昨天晚上莫名奇妙地连不上了，一看 Last handshake 7 hours ago...就这么莫名奇妙地掉线了，哎，于是找了找别的，发现 <a rel="noopener noreferrer" target="_blank" href="https://tailscale.com/">Tailscale</a>  <span class="rss-marginnote">⊕ (WireGuard 是经典的轻量 VPN，需要手动配置 <code>Interface</code> 和 <code>Peer</code>，适合对网络控制要求高的场景。Tailscale 则是在 WireGuard 基础上做了零配置优化，多平台设备可以自动连起来，跨 NAT/防火墙也更稳，适合快速组个人或小型实验内网。)</span> 似乎还行，而且不用像 Wireguard 那样手写 <code>[Interface]</code> 和 <code>[Peer]</code> ，试试看！</p>
<p>先给 Racknerd 的小鸡装：</p>
<pre><code class="language-bash">curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale up
tailscale status
</code></pre>
<p>好耶，看到自己了，再把腾讯云的小鸡拉进去，重复上面的工作！</p>
<p>再把 Laptop 拉进去，哎，根据 Guideline，似乎用 <em>#Windows Powershell</em> 连不如 <em>#GUI</em> 稳定，于是下载 GUI App，启动，连上，完美！ <span class="rss-marginnote">⊕ (手机也可以安装 Tailscale App，配置极其简单！)</span></p>
<p>然后在 iMac 上安装 Tailscale App，大失败，重启之后必定掉线！找了半天文档外加求助 LLM 都没整好，哎，macOS 怎么这么坏啊。</p>
<p>最后的结果：</p>
<pre><code class="language-bash">gengyue@gengyue-laptop:~$ tailscale status
100.101.102.6  laptop-wsl            gengyue2468@  linux    -
100.79.18.74   gengyue-imac          gengyue2468@  macOS    -
100.101.102.3  laptop                gengyue2468@  windows  -
100.101.102.4  phone                 gengyue2468@  android  offline, last seen 4h ago
100.101.102.1  racknerd-seattle      gengyue2468@  linux    -
100.101.102.2  tencent-cloud-canton  gengyue2468@  linux    -
</code></pre>
<p>哎，可以看到 <em>#imac</em> 在这里是一个动态的 IPv4 地址，因为每次重启电脑都需要重新认证，哎，好烦。不过似乎也用不着这个机器，需要的时候再让老父亲打开认证一下吧...... <span class="rss-marginnote">⊕ (macOS 的妙妙机制真的很<s>迷人</s>烦人...)</span></p>
<p>最终结果：</p>
<pre class="mermaid">flowchart LR
    subgraph Tailscale_Network["Tailscale 网络"]
        Racknerd["Racknerd 小鸡\n100.101.102.1\nLinux"]
        TencentCloud["腾讯云小鸡\n100.101.102.2\nLinux"]
        LaptopWin["Laptop Windows\n100.101.102.3\nWindows"]
        LaptopWSL["Laptop WSL\n100.101.102.6\nLinux"]
        iMac["iMac\n100.79.18.74\nmacOS\n(动态 IP)"]
        Phone["Phone\n100.101.102.4\nAndroid\noffline"]
    end

    Racknerd --- TencentCloud
    Racknerd --- LaptopWin
    Racknerd --- LaptopWSL
    Racknerd --- iMac
    Racknerd --- Phone

    TencentCloud --- LaptopWin
    TencentCloud --- LaptopWSL
    TencentCloud --- iMac
    TencentCloud --- Phone

    LaptopWin --- LaptopWSL
    LaptopWin --- iMac
    LaptopWin --- Phone

    LaptopWSL --- iMac
    LaptopWSL --- Phone

    iMac --- Phone


</pre>
<footer class="notes-list"><h3>Notae</h3><ul><li id="sn-list-0">WireGuard 是经典的轻量 VPN，需要手动配置 <code>Interface</code> 和 <code>Peer</code>，适合对网络控制要求高的场景。Tailscale 则是在 WireGuard 基础上做了零配置优化，多平台设备可以自动连起来，跨 NAT/防火墙也更稳，适合快速组个人或小型实验内网。 <a href="#ref-marker-0">↩</a></li><li id="sn-list-1">手机也可以安装 Tailscale App，配置极其简单！ <a href="#ref-marker-1">↩</a></li><li id="sn-list-2">macOS 的妙妙机制真的很<s>迷人</s>烦人... <a href="#ref-marker-2">↩</a></li></ul></footer>]]></content:encoded>
            <author>gengyue</author>
        </item>
        <item>
            <title><![CDATA[迁移 Memos 到 Racknerd]]></title>
            <link>https://www.gengyue.dev/blog/memos-migration</link>
            <guid isPermaLink="false">https://www.gengyue.dev/blog/memos-migration</guid>
            <pubDate>Sat, 07 Feb 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[将 Memos 从国内 VPS 迁移到 RackNerd，并顺手搭了一套自动备份方案。]]></description>
            <content:encoded><![CDATA[<p>昨天购置了一台 <a rel="noopener noreferrer" target="_blank" href="https://www.racknerd.com/">RackNerd</a> 的黑色星期五的特价小鸡，年付 USD $18.66，配置如下： <span class="rss-marginnote">⊕ (located at Seattle, Washington)</span></p>
<ul>
<li>CPU: Intel Xeon Gold 6152 (2) @ 2.095GHz</li>
<li>Memory: 2476MiB (2.5 GB)</li>
<li>SSD: 45GB</li>
<li>Network: 3TB/mo 1Gbps</li>
</ul>
<p>这么一看性价比还是挺高的，毕竟只要 100 多块钱就能买到一台<s>不太稳定</s>的实验鸡，感觉挺划算的。正好，感觉国内的小鸡留着跑 Web 服务也不太合适，决定把一些没啥用的 Web 服务迁移到这个小鸡上，其实主要也就是 Memos，别的倒是无所谓...</p>
<p>我们知道 Memos 是拿 Sqlite 作为数据库的，那好办，我们只需要把数据库的<code>.db</code>文件打包传到新小鸡上就 ok 了！</p>
<p>于是从国内小鸡上找数据库文件，发现似乎位于 <code>~/memos/memos</code>这个目录下，用 <code>tar</code> 打包带走！</p>
<pre><code class="language-bash">tar czvf memos_sqlite_backup_$(date +%F).tar.gz memos_prod.db memos_prod.db-shm memos_prod.db-wal
</code></pre>
<p>哎，然后用 <code>scp</code> 传到 RackNerd 的小鸡上，好耶，数据在 <code>~/memos</code> 下了！然后 <code>nano docker-compose.yml</code>:</p>
<pre><code class="language-yml">version: &quot;3.8&quot;
services:
  memos:
    image: neosmemo/memos:stable
    container_name: memos
    restart: always
    ports:
      - &quot;5230:5230&quot;
    volumes:
      - ~/.memos:/var/opt/memos
</code></pre>
<p>然后把打包回来的数据库文件统统扔到新的 Memos 数据目录下，根据 <code>docker-compose.yml</code>，Docker 把 <code>~/.memos </code> 挂载为 Docker 容器的 <code>/var/opt/memos</code> 目录，所以： <span class="rss-marginnote">⊕ (哎我发现之前在国内小鸡上把 <code>volumes</code> 写成了 <code>volumes:- ./memos:/var/opt/memos</code> 导致找了半天都没找到错误在哪里，我还纳闷为啥数据在 <code>~/memos/memos</code> 下呢！)</span></p>
<pre><code class="language-bash">mkdir -p ~/.memos
tar xzvf memos_sqlite_backup_2026-02-06.tar.gz ~/.memos
</code></pre>
<p>然后启动 Docker：</p>
<pre><code class="language-bash">cd ~/memos
docker-compose up -d
</code></pre>
<p>好耶，Docker 启动了！访问<code>http://ip:5230</code> 就能看到原来的数据了！哎，然后配置一下 <code>nginx</code> 就可以公网访问了！</p>
<p>不过这样似乎还是不太保险，国外小鸡很有可能不太稳定，哪天 Memos 数据丢了可就太可惜了，于是我们来加上一个定时自动备份的功能，嘻嘻，这样似乎保险一点：</p>
<pre><code class="language-bash">mkdir -p ~/backups
nano memos.sh
</code></pre>
<pre><code class="language-bash">#!/usr/bin/env bash
set -euo pipefail

SRC_DIR=&quot;$HOME/.memos&quot;
TMP_DIR=&quot;/tmp/memos_backup&quot;
KEEP_DAYS=14

REMOTE_USER=&quot;REMOTE_USER_NAME&quot;
REMOTE_HOST=&quot;REMOTE_HOST_IP&quot;
REMOTE_DIR=&quot;/home/ubuntu/backups/memos&quot;

DATE=$(date +%F)
ARCHIVE=&quot;memos_${DATE}.tar.gz&quot;

mkdir -p &quot;$TMP_DIR&quot;
cd &quot;$TMP_DIR&quot;

docker stop memos &gt;/dev/null

tar czf &quot;$ARCHIVE&quot; -C &quot;$SRC_DIR&quot; .

docker start memos &gt;/dev/null

scp &quot;$ARCHIVE&quot; &quot;${REMOTE_USER}@${REMOTE_HOST}:${REMOTE_DIR}/&quot;

rm -f &quot;$ARCHIVE&quot;

ssh &quot;${REMOTE_USER}@${REMOTE_HOST}&quot; &lt;&lt;EOF
  find ${REMOTE_DIR} -name &quot;memos_*.tar.gz&quot; -mtime +${KEEP_DAYS} -delete
EOF

echo &quot;[OK] memos backup finished: ${DATE}&quot;
</code></pre>
<p>不过要记得加上可执行权限：</p>
<pre><code class="language-bash">chmod +x ./memos.sh
</code></pre>
<p>哎不过为了让 ssh 无需每次都输入密码链接，可以考虑生成 <code>ssh key</code>，这样就可以一路无感 <code>scp</code> 了，好耶！</p>
<pre><code class="language-bash">ssh-keygen -t ed25519
ssh-copy-id user@remote-user-ip
</code></pre>
<p>最后修改一下 <code>cron</code> 就可以添加定时任务了：</p>
<pre><code class="language-bash">crontab -e
</code></pre>
<p>添加：</p>
<pre><code class="language-bash">0 3 * * * /home/gengyue/backups/memos.sh &gt;&gt; /home/gengyue/backups/memos.log 2&gt;&amp;1
</code></pre>
<p>手动测试一下：</p>
<pre><code class="language-bash">gengyue@racknerd-la:~/backups$./memos.sh
</code></pre>
<p>然后 <code>cat ./memos.log</code> 看到 <code>[OK] memos backup finished: ${DATE}</code> 或者访问备份 vps 看到备份后的 <code>tar</code> 文件就说明成功了！好耶！</p>
<p>哎这样哪天 Racknerd 爆爆了，好歹国内小鸡还能抢救一部分数据回来...</p>
<footer class="notes-list"><h3>Notae</h3><ul><li id="sn-list-0">located at Seattle, Washington <a href="#ref-marker-0">↩</a></li><li id="sn-list-1">哎我发现之前在国内小鸡上把 <code>volumes</code> 写成了 <code>volumes:- ./memos:/var/opt/memos</code> 导致找了半天都没找到错误在哪里，我还纳闷为啥数据在 <code>~/memos/memos</code> 下呢！ <a href="#ref-marker-1">↩</a></li></ul></footer>]]></content:encoded>
            <author>gengyue</author>
        </item>
        <item>
            <title><![CDATA[装一下 Memos]]></title>
            <link>https://www.gengyue.dev/blog/memos</link>
            <guid isPermaLink="false">https://www.gengyue.dev/blog/memos</guid>
            <pubDate>Sun, 01 Feb 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[折腾了一次 Memos 的自部署，从 Docker 镜像拉取、Nginx 反代到 API 接入 QQ Bot，顺手记录了一些踩坑和实现细节。]]></description>
            <content:encoded><![CDATA[<p>今天突发奇想心血来潮，想体验一下  <a rel="noopener noreferrer" target="_blank" href="https://usememos.com/">Memos</a>   <span class="rss-marginnote">⊕ (<strong>Memos</strong>:A lightweight, self-hosted memo hub for effortlessly capturing and sharing your ideas. Open source, no tracking, free forever.)</span> ，于是翻出来吃灰的那台轻量云服务器，跑跑看看。</p>
<p>按照 <a rel="noopener noreferrer" target="_blank" href="https://usememos.com/docs/deploy/docker-compose">Docker Compose - Memos</a> 这个页面的指示，只需要创建一个 <code>docker-compose.yml</code> 文件：</p>
<pre><code class="language-bash">services:
  memos:
    image: neosmemo/memos:stable
    container_name: memos
    volumes:
      - ~/.memos/:/var/opt/memos
    ports:
      - 5230:5230
</code></pre>
<p>然后：</p>
<pre><code class="language-bash">docker compose up -d
</code></pre>
<p>然后不出所料地爆爆了，原来是 <a rel="noopener noreferrer" target="_blank" href="http://docker.io">docker.io</a> 没法访问导致的。 <span class="rss-marginnote">⊕ (不过我之前似乎配置了 Docker 镜像啊，很奇怪。)</span></p>
<pre><code class="language-bash">ubuntu@VM-0-6-ubuntu:~/memos$ docker compose up -d

[+] Running 1/1
✘ memos Error Get &quot;https://registry-1.docker.io/v2/&quot;:
  context deadline exceeded
  (Client.Timeout exceeded while awaiting headers)

Error response from daemon:
  Get &quot;https://registry-1.docker.io/v2/&quot;:
  context deadline exceeded
  (Client.Timeout exceeded while awaiting headers)
</code></pre>
<p>按照<a rel="noopener noreferrer" target="_blank" href="https://cloud.tencent.com/document/product/1207/45596">腾讯云官方的指导教程</a>，用 nano 编辑 <code>/etc/docker/daemon.json</code> 文件，哎，结果打开一看，怎么已经有了 <code> &quot;https://mirror.ccs.tencentyun.com&quot;</code> 啊。于是<code>sudo docker info</code> 一下看看，结果发现怎么镜像地址是之前错配的中科大的 docker 镜像页面，哎，坏。 <span class="rss-marginnote">⊕ (好像地址爆爆了)</span></p>
<p>于是重启了一下 Docker，然后 Memos 镜像能拉取了，安装还是挺顺利的，用 VS Code 转发一下端口就能访问<code>http://localhost:5230</code> 看到 Memos 的管理页面了。</p>
<p>哎不过发现 <em>瞌睡猫子</em>  <span class="rss-sidenote">(NapCat)</span>怎么没了，去腾讯云面板一看发现 Docker 容器被暂停了，打开重新登录一下就好了。</p>
<p>然后准备把 <code>https://memos.gengyue.site</code> 用 Nginx 反代一下，这样就能从公网访问 Memos 服务了。</p>
<pre><code class="language-nginx">sudo nano /etc/nginx/sites-available/memos
</code></pre>
<p><code>memos</code></p>
<pre><code class="language-nginx">server {
    listen 2095; 
    server_name memos.gengyue.site;

    location / {
        return 301 https://$host$request_uri;
    }
}

server {
    listen 443 ssl http2;
    server_name memos.gengyue.site;

    ssl_certificate /etc/nginx/ssl/cloudflare/gengyue.site.crt;
    ssl_certificate_key /etc/nginx/ssl/cloudflare/gengyue.site.key;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;

    ssl_prefer_server_ciphers on;
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 10m;

    location / {
        proxy_pass http://127.0.0.1:5230; 
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection &quot;upgrade&quot;;
    }
}
</code></pre>
<p>哎，这样一番配置就行了，最起码是跑起来了。然后：</p>
<pre><code class="language-bash">sudo ln -s /etc/nginx/sites-available/memos /etc/nginx/sites-enabled/memos
sudo nginx -t
sudo systemctl reload nginx
</code></pre>
<p>好耶😋</p>
<p>哎，然后实践了一下用 <code>#memos</code> 提供的 API 实现了 QQ 机器人自动推送到 Memos 服务，LLM 在这个过程中为#文本自动打上 3 - 5 个标签，虽然有的时候生成的挺离谱的，不过又不是不能用...</p>
<p>不过神秘 <code>#memos</code>  似乎上传图片是先转成 <code>base64</code> 之后上传的，哎，神奇：</p>
<pre><code class="language-js">async function uploadImage(url) {
  const img = await fetch(url);
  const buffer = Buffer.from(await img.arrayBuffer());

  const res = await fetch(`${MEMOS_URL}/api/v1/attachments`, {
    method: &quot;POST&quot;,
    headers: {
      &quot;Authorization&quot;: `Bearer ${TOKEN}`,
      &quot;Content-Type&quot;: &quot;application/json&quot;,
    },
    body: JSON.stringify({
      filename: `image_${Date.now()}.jpg`,
      type: img.headers.get(&quot;content-type&quot;),
      content: buffer.toString(&quot;base64&quot;),
    }),
  });

  return res.json();
}
</code></pre>
<footer class="notes-list"><h3>Notae</h3><ol><li id="sn-list-0">NapCat <a href="#ref-marker-0">↩</a></li><li class="marginnote-item" id="sn-list-1"><strong>Memos</strong>:A lightweight, self-hosted memo hub for effortlessly capturing and sharing your ideas. Open source, no tracking, free forever. <a href="#ref-marker-1">↩</a></li><li class="marginnote-item" id="sn-list-2">不过我之前似乎配置了 Docker 镜像啊，很奇怪。 <a href="#ref-marker-2">↩</a></li><li class="marginnote-item" id="sn-list-3">好像地址爆爆了 <a href="#ref-marker-3">↩</a></li></ol></footer>]]></content:encoded>
            <author>gengyue</author>
        </item>
        <item>
            <title><![CDATA[试一试 Intent Parsing]]></title>
            <link>https://www.gengyue.dev/blog/try-intent-parsing</link>
            <guid isPermaLink="false">https://www.gengyue.dev/blog/try-intent-parsing</guid>
            <pubDate>Thu, 29 Jan 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[把 LLM 当作自然语言与后端 API 之间的中间层，做一次可控的 Intent Parsing / 语义路由实验。]]></description>
            <content:encoded><![CDATA[<p>前段时间 jyi 写了一个留学申请的院校库后端 api，闲的没事想要试试 <a rel="noopener noreferrer" target="_blank" href="https://ppio.com">PPIO</a> 爆的金币的 LLM 模型的 intent parsing 怎么样，毕竟之前自己也没试过，也是第一次玩玩这个玩意儿，试试看。</p>
<p>首先，<s>作为懒惰的人类</s>，我们要用 LLM 完成 LLM 应该做的事情，简单用 LLM 规划一下结构：</p>
<pre><code>/api
- /intent
  - route.ts
- /chat
  - route.ts
</code></pre>
<blockquote>
<p>其实要试验一下 intent parsing 只需要第一个<code>/intent</code> api 路由就足够了 <span class="rss-marginnote">⊕ (因为我们本质上在找一种用户自然语言到 JSON schema 的映射关系，LLM 只是帮助我们处理这一层)</span>，不过我更喜欢让 llm 输出一些废话😋</p>
</blockquote>
<p>我们的思路是，llm 根据我们喂给它的 system prompt 中的 api route 文档，生成一个结构化的 JSON schema，然后前端通过某些神秘操作解析这个 JSON，根据 <code>type</code> 和相关的 <code>params</code> 请求对应的 API，拿到数据之后放在前端展示就 ok。</p>
<p>得益于 <a rel="noopener noreferrer" target="_blank" href="https://nextjs.org">Next.js</a> 天然的 API Route 优势，我们可以轻松地创建这样一个 <code>/api/intent/route.ts</code> 文件：</p>
<pre><code class="language-ts">import OpenAI from &quot;openai&quot;;
import { NextResponse } from &quot;next/server&quot;;

const openai = new OpenAI({
  baseURL: process.env.PPIO_BASE_URL,
  apiKey: process.env.PPIO_API_KEY,
});

export async function POST(req: Request) {
  try {
    const { query } = await req.json();

    const completion = await openai.chat.completions.create({
      model: &quot;deepseek/deepseek-v3.2&quot;,
      stream: false,
      messages: [
        {
          role: &quot;system&quot;,
          content: `你是一个智能 API 操作生成器。根据用户的自然语言输入，分析意图并生成对应的操作。

支持的操作类型：

1. search_school - 搜索学校
参数（均为可选）：
- region: 地区中文名称（如：中国香港、中国澳门、英国、美国）
- region_en: 地区英文名称
- school_name: 学校中文名称（模糊匹配）
- school_name_en: 学校英文名称（模糊匹配）
- min_rank: QS排名最小值（&lt;=该值）
- max_rank: QS排名最大值（&gt;=该值）
- sort_by: 排序字段（qs_rank_2025, qs_rank_2026, school_name, school_name_en）
- sort_order: 排序顺序（asc 或 desc）
- limit: 每页数量（默认20）
- offset: 偏移量（默认0）

2. search_project - 搜索项目
参数（均为可选）：
- region: 地区中文名称
- region_en: 地区英文名称
- school_name: 学校中文名称（模糊匹配）
- school_name_en: 学校英文名称（模糊匹配）
- major_category: 专业类别（如：计算机、电气电子、商科、体育）
- project_name: 项目中文名称（模糊匹配）
- major_en: 专业英文名称（模糊匹配）
- min_ielts: 雅思最小分数
- max_ielts: 雅思最大分数
- min_toefl: 托福最小分数
- max_toefl: 托福最大分数
- min_tuition: 学费最小值
- max_tuition: 学费最大值
- is_open: 是否开放申请（0或1）
- sort_by: 排序字段（project_name, major_en, tuition, ielts, toefl）
- sort_order: 排序顺序（asc 或 desc）
- limit: 每页数量（默认20）
- offset: 偏移量（默认0）

3. extract_info - 提取个人信息
当用户提供个人背景材料时使用（如：简历、成绩单、经历描述等）
参数：
- raw_material: 用户提供的原始材料文本

规则：
1. 判断用户意图是搜索学校、搜索项目还是提取信息
2. 搜索学校时type为&quot;search_school&quot;，搜索项目时type为&quot;search_project&quot;，提取信息时type为&quot;extract_info&quot;
3. 对于排名范围，如&quot;QS前100&quot;表示 min_rank=100
4. 默认 limit=20
5. 只生成有值的参数，不要生成空值

请直接返回 JSON 格式的结果，格式如下：
{
  &quot;type&quot;: &quot;search_school&quot; 或 &quot;search_project&quot; 或 &quot;extract_info&quot;,
  &quot;params&quot;: {
    // 参数对象
  }
}

不要输出任何其他文字，只输出 JSON。`,
        },
        {
          role: &quot;user&quot;,
          content: query,
        },
      ],
      response_format: { type: &quot;json_object&quot; },
    });

    const content = completion.choices[0].message.content;

    if (!content) {
      return NextResponse.json(
        { error: &quot;No response from LLM&quot; },
        { status: 500 },
      );
    }

    const result = JSON.parse(content);
    console.log(&quot;Intent API Result:&quot;, result);
    return NextResponse.json(result);
  } catch (error: unknown) {
    console.error(&quot;Intent API Error:&quot;, error);
    return NextResponse.json(
      { error: (error as Error).message || &quot;Failed to process query&quot; },
      { status: 500 },
    );
  }
}

</code></pre>
<p>ok，下面可以和 llm 进行一些亲密的对话了，比如：</p>
<pre><code>用户输入：
“帮我找一下 QS 前 100 的英国计算机硕士项目”
</code></pre>
<p>我们预期 llm 回复这样的 JSON schema： <span class="rss-marginnote">⊕ (结构化的 JSON 在这里非常重要，llm 经常会不听话，比如生成```这样的 markdown 语法)</span></p>
<pre><code class="language-json">{
  &quot;type&quot;: &quot;search_project&quot;,
  &quot;params&quot;: {
    &quot;region&quot;: &quot;英国&quot;,
    &quot;major_category&quot;: &quot;计算机&quot;,
    &quot;min_rank&quot;: 100
  }
}
</code></pre>
<p>好耶，如果是这样，那很好了，我们可以轻松调用后面的 api 啦！我们像这样创建一个 hooks，比如叫<code>/hooks/use-chat.ts</code>： <span class="rss-marginnote">⊕ (省略大部分 UI/Loading 层的东西)</span></p>
<pre><code class="language-ts">async function handleUserInput(query: string) {
  /* await fetch(&quot;/api/llm/chat&quot;, {
    method: &quot;POST&quot;,
    body: JSON.stringify({ query }),
  }); */ // 可选：让 llm 先输出一段废话，让用户觉得 llm 确实在干活
   
  const intentResp = await axios.post(&quot;/api/llm/intent&quot;, { query });
  const { type, params } = intentResp.data;
    
  switch (type) {
    case &quot;search_school&quot;:
    case &quot;search_project&quot;: {
      const { data } = await axios.get(&quot;/api/search&quot;, {
        params: { type, ...params },
      });
      return { type: &quot;search&quot;, data };
    }

    case &quot;extract_info&quot;: {
      const { data } = await axios.post(&quot;/api/extract&quot;, {
        raw_material: params.raw_material ?? query,
      });
      return { type: &quot;extract&quot;, data };
    }

    default:
      return null;
  }
}
</code></pre>
<p>其实现在看来逻辑链路很清楚了，大概就是这样：</p>
<div class="fullwidth-content">
<pre class="mermaid">flowchart LR
    U[用户输入\n自然语言] --> L1[LLM\n生成初始回复]
    L1 --> L2[LLM\n解析意图 Intent]

    L2 -->|type + params| D{Intent 类型}

    D -->|search_school / search_project| A[Search API]
    D -->|extract_info| B[Extract API]
    D -->|chat| C[普通对话结束]

    A --> R1[结构化搜索结果]
    B --> R2[结构化信息提取结果]

    R1 --> UI[前端渲染]
    R2 --> UI
</pre>
</div>
<p>okk~ 看来实现的差不多了，不过有的时候 llm 还是会犯毛病啥的，乱编一些 <code>params</code> 或者即使参数好不容易搞对了，后端请求之后发现没有返回值？哎，这就看出来有个 <code>/api/chat</code> 路由和用户<s>胡扯/唠嗑/氵时长</s>的优势了...</p>
<p>哎，其实这么看llm当作搜索中间层还挺好的，这似乎是什么模糊搜索库都达不到的效果啊... <span class="rss-marginnote">⊕ (不过直接让 llm 胡搜/胡说八道可还是蒜鸟，幻觉可够严重的...)</span></p>
<footer class="notes-list"><h3>Notae</h3><ul><li id="sn-list-0">因为我们本质上在找一种用户自然语言到 JSON schema 的映射关系，LLM 只是帮助我们处理这一层 <a href="#ref-marker-0">↩</a></li><li id="sn-list-1">结构化的 JSON 在这里非常重要，llm 经常会不听话，比如生成```这样的 markdown 语法 <a href="#ref-marker-1">↩</a></li><li id="sn-list-2">省略大部分 UI/Loading 层的东西 <a href="#ref-marker-2">↩</a></li><li id="sn-list-3">不过直接让 llm 胡搜/胡说八道可还是蒜鸟，幻觉可够严重的... <a href="#ref-marker-3">↩</a></li></ul></footer>]]></content:encoded>
            <author>gengyue</author>
        </item>
        <item>
            <title><![CDATA[配置一下 SSL]]></title>
            <link>https://www.gengyue.dev/blog/ssl-config</link>
            <guid isPermaLink="false">https://www.gengyue.dev/blog/ssl-config</guid>
            <pubDate>Mon, 19 Jan 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[一次从 Cloudflare 522 出发的折腾：配置 Origin Certificate、Nginx HTTPS、ufw 防火墙，最后才发现——原来是我自己没开 443。]]></description>
            <content:encoded><![CDATA[<p>前段时间对域名在<a rel="noopener noreferrer" target="_blank" href="https://cloud.tencent.com">腾讯云</a>上进行了一下 ICP 备案，大概过了半个月左<s>右</s> <span class="rss-marginnote">⊕ (哎腾讯云控制台你说大概 12 天还真就 12 天啊，在考思政之前收到了短信...)</span>，然后就准备把博客从 <a rel="noopener noreferrer" target="_blank" href="https://vercel.com">Vercel</a> 搞到闲置的轻量云服务器上，反正就是十几二十个 HTML 页面，放到服务器上 nginx 反代一配，占不了几个内存和带宽，国内速度还快不少，何乐而不为？ <span class="rss-marginnote">⊕ (静态资源已经美美托管到<a rel="noopener noreferrer" target="_blank" href="https://edgeone.ai">EdgeOne</a>上了😋，参见<a href="/edgeone-test">这篇文章</a>)</span></p>
<p>于是就准备搞搞，打开 <a rel="noopener noreferrer" target="_blank" href="https://dash.cloudflare.com">Cloudflare 控制台</a> 然后添加一条 <code>A</code> 记录指向<code>ip</code>地址，噫，这很好。然后打开www.gengyue.site一看，woc，怎么 522 了，原来是 CF 无法访问到源站导致的。</p>
<p>于是就想想，CF 怎么会访问不到源站的，哦，原来是这样，CF 的 Rules 配置默认是搞到“完全”，表明访问者和 CF, CF 和 源站之间都要采用 HTTPS 加密连接，而源站没有配置好 SSL，自然 CF 去连接是连不上的，大概流程如下：</p>
<div class="fullwidth-content">
<pre class="mermaid">flowchart LR
    U[访问者 / 浏览器] -->|HTTPS| CF[Cloudflare CDN]

    CF -->|HTTPS 请求源站| O[源站服务器]
    O -.->|错误:未配置 SSL / HTTPS 不可用| CF

    CF -->|返回错误<br/>522 / 525 / 526| U

</pre>
</div>
<p>一个比较简单粗暴的解决方案是直接把 Rules 换成“灵活”，哎，这样就解决了。不过我们如果还是想让 CF 和源站之间加密连接呢，自然还是有办法的。一般来说可以考虑：</p>
<ul>
<li>用 <a rel="noopener noreferrer" target="_blank" href="https://letsencrypt.org/zh-cn/">Let's Encrypt</a> 颁发的证书来签名，这样似乎还很不错，嘻嘻，不过我嫌麻烦。</li>
<li>用 Cloudflare 提供的十五年期限的、只有 CF 和源站服务器之间信任的边缘证书，这个问题似乎就迎刃而解了。</li>
</ul>
<p>哎，直接去<code>SSL/TLS → Origin Server → Create Certificate</code>这个位置，搞到一个证书，为</p>
<ul>
<li>gengyue.site</li>
<li>*.gengyue.site</li>
</ul>
<p>申请 15 年的证书有效期就 OK 了，然后 CF 会给你一段 <strong>Origin Certificate</strong>和一段 <strong>Private Key</strong>。 <span class="rss-marginnote">⊕ (像大多数的 key 一样，这个私钥只显示一次)</span></p>
<p>然后登录服务器，输入下面的命令配置<code>nginx</code></p>
<pre><code class="language-bash">cd /etc/nginx/ssl/
</code></pre>
<p>然后新建个文件夹用来存证书和私钥：</p>
<pre><code class="language-bash">sudo mkdir -p /etc/nginx/ssl
sudo chmod 700 /etc/nginx/ssl
</code></pre>
<pre><code class="language-bash">sudo nano /etc/nginx/ssl/cf_origin.crt
sudo nano /etc/nginx/ssl/cf_origin.key
</code></pre>
<p>把得到的东西复制进去就ok了，嘻嘻，还是相当容易的。然后顺便配置一下<code>nginx.conf</code>，就像这样：</p>
<pre><code class="language-nginx">server {
    listen 443 ssl http2;
    server_name gengyue.site www.gengyue.site;

    ssl_certificate     /etc/nginx/ssl/cf_origin.crt;
    ssl_certificate_key /etc/nginx/ssl/cf_origin.key;

    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;

    ......
}
</code></pre>
<p>似乎可以把 80 端口重定向到 HTTPS：</p>
<pre><code class="language-nginx">server {
    listen 80;
    server_name gengyue.site www.gengyue.site;
    return 301 https://$host$request_uri;
}
</code></pre>
<p>然后重启 nginx：</p>
<pre><code class="language-bash">sudo nginx -t
sudo systemctl reload nginx
</code></pre>
<p>好耶，看来是成功了，回到控制台，把 Rules 放心大胆地调成完全（严格），然后再访问www.gengyue.site，嗯？怎么还是报 522 错误，怎么 CF 还是连不上源站？怎么又白忙活了一大顿？？😭</p>
<p>等下，我们去看看防火墙规则，登录腾讯云控制台，看到 443 端口确实是对公网 Ipv4 地址开放的啊，怎么回事呢？啊哈？哦，说不定 CF 默认用 Ipv6 连，而我们的服务器没开放 Ipv6！让我们试试看！ <span class="rss-marginnote">⊕ (Cloudflare 似乎确实是这样的...)</span></p>
<p>woc，怎么还是连不上，要爆炸了。等下<code>ufw status</code>下看看，woc，破案了！ ufw 阻止了 443 端口的流量，让我们赶紧开放 443 的 v4 和 v6 端口：</p>
<pre><code class="language-bash">sudo ufw allow 443/tcp
sudo ufw reload
sudo ufw status
443/tcp ALLOW
</code></pre>
<p>测试一下，好耶：</p>
<pre><code class="language-bash">curl -I https://www.gengyue.site
HTTP/2 200
server: cloudflare
</code></pre>
<p>现在的请求链路大概是这样的：</p>
<div class="fullwidth-content">
<pre class="mermaid">flowchart LR
    U[浏览器] -->|HTTPS| CF[Cloudflare CDN]

    CF -->|HTTPS<br/>Origin Certificate| N[Nginx / 源站服务器]
</pre>
</div>
<p>哦吼吼，不过折腾一大顿之后发现自己似乎南辕北辙了，CF 的节点在美国，大陆请求到美国代理到美国的 Vercel 服务器和大陆请求到美国的 CF 节点再代理到大陆的轻量云服务器，还真说不准谁的速度块？😂 <span class="rss-marginnote">⊕ (哎，哎，哎)</span></p>
<p>不过延迟也是从 400ms 降低到 100 ~ 200ms 了，哎，如果不是套一层 CF 何至于这么慢？不过为了安全着想嘛... <span class="rss-marginnote">⊕ (笑死，就你那破烂垃圾都没有人有攻击的欲望，笑死...(bushi)</span></p>
<footer class="notes-list"><h3>Notae</h3><ul><li id="sn-list-0">哎腾讯云控制台你说大概 12 天还真就 12 天啊，在考思政之前收到了短信... <a href="#ref-marker-0">↩</a></li><li id="sn-list-1">静态资源已经美美托管到<a rel="noopener noreferrer" target="_blank" href="https://edgeone.ai">EdgeOne</a>上了😋，参见<a href="/edgeone-test">这篇文章</a> <a href="#ref-marker-1">↩</a></li><li id="sn-list-2">像大多数的 key 一样，这个私钥只显示一次 <a href="#ref-marker-2">↩</a></li><li id="sn-list-3">Cloudflare 似乎确实是这样的... <a href="#ref-marker-3">↩</a></li><li id="sn-list-4">哎，哎，哎 <a href="#ref-marker-4">↩</a></li><li id="sn-list-5">笑死，就你那破烂垃圾都没有人有攻击的欲望，笑死...(bushi <a href="#ref-marker-5">↩</a></li></ul></footer>]]></content:encoded>
            <author>gengyue</author>
        </item>
        <item>
            <title><![CDATA[折腾一下 EdgeOne]]></title>
            <link>https://www.gengyue.dev/blog/edgeone-test</link>
            <guid isPermaLink="false">https://www.gengyue.dev/blog/edgeone-test</guid>
            <pubDate>Tue, 13 Jan 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[一次从 Cloudflare Pages 到 EdgeOne CDN 的折腾实录，踩了 cdn-loop 的坑，试了 SSL、Proxy、回源，最后靠 Vercel 成功白嫖静态加速。]]></description>
            <content:encoded><![CDATA[<p>哎，前段时间在看 GitHub 上网站部署记录的时候猛然发现 <a rel="noopener noreferrer" target="_blank" href="https://pages.cloudflare.com">Cloudflare Pages</a> 上的服务已经爆爆了 100+ 次部署了 <span class="rss-marginnote">⊕ (竟然还有 bug 删除不了超过 100 次的部署，很神奇)</span>，因为我先后从 <a rel="noopener noreferrer" target="_blank" href="https://nextjs.org">Next.js</a> 迁移到 <a rel="noopener noreferrer" target="_blank" href="https://reactrouter.com">React Router</a> 再到现在的 Plain HTML 写作，我一次都没有改过 framework type 和输出目录 <span class="rss-marginnote">⊕ (这之间似乎还有 Pages Router 到 App Router 的迁移)</span>，哎，爆爆就爆爆了。不过，这倒提醒我了，我之前似乎在腾讯云 <a rel="noopener noreferrer" target="_blank" href="https://edgeone.ai/">EdgeOne</a> 上美美白嫖的 CDN 套餐还没用 😋，这不得试吃一下 🤤。</p>
<p>打开腾讯云官网，然后“使用 Google 登录”，然后“快速登录”，哎，这就成了。然后再控制台面板上找到添加站点，用 CNAME 解析一下。不过要注意，这里还是要把整个站放到腾讯云 EdgeOne 里头，即使我们只需要用到几个子域名就 OK。我用 <a rel="noopener noreferrer" target="_blank" href="https://cloudflare.com">Cloudflare</a> 的 DNS 解析，只需要添加一条 TXT 记录就行了，相当 smooth! <span class="rss-marginnote">⊕ (这一步验证 DNS 还是很有必要的，利好后面的 SSL 证书申请)</span></p>
<p>然后美美挑选了一个漂亮的子域名，添加到 EdgeOne 规则里头 <span class="rss-marginnote">⊕ (注意用 Cloudflare 的话 CNAME 记录要仅代理)</span>，然后随便选一个预设 template 就 OK，回源填自己的网站主域名，all done！然后美美申请免费 SSL 证书，稍等片刻，访问，我靠，怎么 324 错误了！于是立刻<code>curl -I</code>一下，看看怎么回事：</p>
<pre><code>$ curl -I https://cdn.gengyue.site/
HTTP/1.1 423 Locked
cdn-loop: TencentEdgeOne; loops=16
Server: cloudflare
nel: {&quot;success_fraction&quot;:0.1,&quot;report_to&quot;:&quot;eo-nel&quot;,&quot;max_age&quot;:604800}
report-to: {
  &quot;endpoints&quot;:[{&quot;url&quot;:&quot;https://nel.teo-rum.com/eo-cgi/nel&quot;}],
  &quot;group&quot;:&quot;eo-nel&quot;,
  &quot;max_age&quot;:604800
}
cf-cache-status: DYNAMIC
speculation-rules: &quot;/cdn-cgi/speculation&quot;
alt-svc: h3=&quot;:443&quot;; ma=86400
CF-RAY: 9bd1933598a30e68-AMS
Content-Length: 0
Connection: keep-alive
Date: Tue, 13 Jan 2026 02:48:30 GMT
EO-LOG-UUID: 13426361529703584917
EO-Cache-Status: MISS
</code></pre>
<p>哎，看来 SSL 配置没生效？先等等看。</p>
<p>等了差不多 30 分钟？再试试看：</p>
<pre><code>$ curl -I https://cdn.gengyue.site/
HTTP/1.1 423 Locked
cdn-loop: TencentEdgeOne; loops=16
Server: cloudflare
nel: {&quot;success_fraction&quot;:0.1,&quot;report_to&quot;:&quot;eo-nel&quot;,&quot;max_age&quot;:604800}
report-to: {
  &quot;endpoints&quot;:[{&quot;url&quot;:&quot;https://nel.teo-rum.com/eo-cgi/nel&quot;}],
  &quot;group&quot;:&quot;eo-nel&quot;,
  &quot;max_age&quot;:604800
}
cf-cache-status: DYNAMIC
speculation-rules: &quot;/cdn-cgi/speculation&quot;
alt-svc: h3=&quot;:443&quot;; ma=86400
CF-RAY: 9bd1933598a30e68-AMS
Content-Length: 0
Connection: keep-alive
Date: Tue, 13 Jan 2026 02:48:30 GMT
EO-LOG-UUID: 13426361529703584917
EO-Cache-Status: MISSxxxxxxxxxx $ curl -I https://cdn.gengyue.site/HTTP/1.1 423 Lockedcdn-loop: TencentEdgeOne; loops=16Server: cloudflarenel: {&quot;success_fraction&quot;:0.1,&quot;report_to&quot;:&quot;eo-nel&quot;,&quot;max_age&quot;:604800}report-to: {  &quot;endpoints&quot;:[{&quot;url&quot;:&quot;https://nel.teo-rum.com/eo-cgi/nel&quot;}],  &quot;group&quot;:&quot;eo-nel&quot;,  &quot;max_age&quot;:604800}cf-cache-status: DYNAMICspeculation-rules: &quot;/cdn-cgi/speculation&quot;alt-svc: h3=&quot;:443&quot;; ma=86400CF-RAY: 9bd1933598a30e68-AMSContent-Length: 0Connection: keep-aliveDate: Tue, 13 Jan 2026 02:48:30 GMTEO-LOG-UUID: 13426361529703584917EO-Cache-Status: MISS$ curl -I https://cdn.gengyue.site/HTTP/1.1 423 Locked cdn-loop: TencentEdgeOne; loops=16 Server: cloudflare nel: {&quot;success_fraction&quot;:0.1,&quot;report_to&quot;:&quot;eo-nel&quot;,&quot;max_age&quot;:604800} report-to: {&quot;endpoints&quot;:[{&quot;url&quot;:&quot;https://nel.teo-rum.com/eo-cgi/nel&quot;}],&quot;group&quot;:&quot;eo-nel&quot;,&quot;max_age&quot;:604800} cf-cache-status: DYNAMIC speculation-rules: &quot;/cdn-cgi/speculation&quot; alt-svc: h3=&quot;:443&quot;; ma=86400 CF-RAY: 9bd1933598a30e68-AMS Content-Length: 0 Connection: keep-alive Date: Tue, 13 Jan 2026 02:48:30 GMT EO-LOG-UUID: 13426361529703584917 EO-Cache-Status: MISS
</code></pre>
<p>哎，怎么还是这样。看来不是 SSL 的问题了。仔细一想，似乎是 Cloudflare CDN 代理主站的原因了，看来还是得删除掉 Cloudflare Proxy！于是换成仅代理。</p>
<p>哎，怎么还是不行，<code>curl</code>一下看看，发现<code>Server</code>竟然还是<code>Cloudflare</code>，哎，问了 ChatGPT 它告诉我 EdgeOne 的加速服务用的是 Cloudflare 的部分节点？？晕，一看就不靠谱。看来是搞出 CDN Loop 了，害，蒜鸟。</p>
<p>用 ChatGPT 画了个图，大概就是下面这样：</p>
<div class="fullwidth-content">
<pre class="mermaid">flowchart LR
    Browser[浏览器]
    CF_DNS[Cloudflare DNS]
    EdgeOne[Tencent EdgeOne]
    CF_Proxy[Cloudflare Proxy<br/>（橙云）]

    Browser --> CF_DNS
    CF_DNS --> EdgeOne
    EdgeOne --> CF_Proxy
    CF_Proxy --> EdgeOne

    EdgeOne -.->|cdn-loop: loops=16| EdgeOne
</pre>
</div>
<p>等下，我们为啥要直接回源源站呢？<a rel="noopener noreferrer" target="_blank" href="https://vercel.com">Vercel</a>似乎提供了一个免费的<code>*.vercel.app</code>域名，我们直接回源这个不就行了。反正我们的 EdgeOne 也是在非中国大陆地区运行的，这似乎很适合。 <span class="rss-marginnote">⊕ (vercel.app 在中国大陆的可用性为 0)</span></p>
<p>于是妙妙将源站回源地址改成 <code>*.vercel.app</code> 然后等等部署再<code>curl</code>测试一番，欸，怎么还是不行，后来改了一下回源 hosts，神奇的好了。 <span class="rss-marginnote">⊕ (欸，那之前的配置估计也是回源 hosts 不匹配被 EdgeOne 当成 Loop 了...，那么...懒，不改了)</span>现在：</p>
<pre><code>$ curl -I https://cdn.gengyue.site/static/hust/hust.webp
HTTP/1.1 200 OK
Access-Control-Allow-Origin: *
Age: 0
Content-Disposition: inline; filename=&quot;hust.webp&quot;
Content-Type: image/webp
Etag: &quot;9b9b5c738a665ef2ce85ecba3a5f77e0&quot;
Server: Vercel
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
X-Vercel-Cache: HIT
X-Vercel-Id: fra1::nhl99-1768314272898-dfc7de69cffb
Last-Modified: Tue, 13 Jan 2026 14:24:33 GMT
Content-Length: 137622
Accept-Ranges: bytes
Connection: keep-alive
Date: Tue, 13 Jan 2026 14:24:32 GMT
EO-LOG-UUID: 12182038518495320345
EO-Cache-Status: MISS
Cache-Control: max-age=3600
NEL: {&quot;success_fraction&quot;:0.1,&quot;report_to&quot;:&quot;eo-nel&quot;,&quot;max_age&quot;:604800}
Report-To: {&quot;endpoints&quot;:[{&quot;url&quot;:&quot;https://nel.teo-rum.com/eo-cgi/nel&quot;}],&quot;group&quot;:&quot;eo-nel&quot;,&quot;max_age&quot;:604800}
</code></pre>
<p>好耶！成功了。大概的工作原理见下图：</p>
<div class="fullwidth-content">
<pre class="mermaid">flowchart LR
    Browser[浏览器]
    CF_DNS[Cloudflare DNS]
    EdgeOne[Tencent EdgeOne]
    Vercel[Vercel<br/>*.vercel.app]

    Browser --> CF_DNS
    CF_DNS --> EdgeOne
    EdgeOne -->|回源 + Host 修正| Vercel
</pre>
</div>
<p>剩下的工作就是在 EdgeOne 上配置一些服务逻辑啥的了，比如搞个非<code>/static/``/fonts</code>目录就直接<code>throw new error (403)</code>之类的东西，外加设置个缓存头之类的什么，总之，现在静态文件加载快多了，还不用浪费轻量云少得可怜的流量！美美的吃😋 <span class="rss-marginnote">⊕ (TTL 缓存对于字体可以搞 365 天，图片之类的 30 天也行，反正不怎么更新...)</span></p>
<footer class="notes-list"><h3>Notae</h3><ul><li id="sn-list-0">竟然还有 bug 删除不了超过 100 次的部署，很神奇 <a href="#ref-marker-0">↩</a></li><li id="sn-list-1">这之间似乎还有 Pages Router 到 App Router 的迁移 <a href="#ref-marker-1">↩</a></li><li id="sn-list-2">这一步验证 DNS 还是很有必要的，利好后面的 SSL 证书申请 <a href="#ref-marker-2">↩</a></li><li id="sn-list-3">注意用 Cloudflare 的话 CNAME 记录要仅代理 <a href="#ref-marker-3">↩</a></li><li id="sn-list-4">vercel.app 在中国大陆的可用性为 0 <a href="#ref-marker-4">↩</a></li><li id="sn-list-5">欸，那之前的配置估计也是回源 hosts 不匹配被 EdgeOne 当成 Loop 了...，那么...懒，不改了 <a href="#ref-marker-5">↩</a></li><li id="sn-list-6">TTL 缓存对于字体可以搞 365 天，图片之类的 30 天也行，反正不怎么更新... <a href="#ref-marker-6">↩</a></li></ul></footer>]]></content:encoded>
            <author>gengyue</author>
        </item>
        <item>
            <title><![CDATA[重构博客]]></title>
            <link>https://www.gengyue.dev/blog/refactor-blog</link>
            <guid isPermaLink="false">https://www.gengyue.dev/blog/refactor-blog</guid>
            <pubDate>Fri, 09 Jan 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[用 Node.js + Markdown-It + Tufte CSS 重新打造的 0 运行时 JavaScript 的极简文件驱动博客，支持自定义路由、旁注语法和 TypeScript 构建流程，性能优先、排版优雅、内容完全自主可控。]]></description>
            <content:encoded><![CDATA[<p>哎，虽然现在是期末周，明天还要考中国语文了。 <span class="rss-sidenote">(怎么理工科还要学语文啊！！！<s>你问我啊，我问谁啊？</s>)</span>虽然我连考啥都不知道，这个学期学了啥我也不知道，<s>但是也不想学习微积分了</s>，于是在这里氵文一篇。</p>
<h2>过去的方案：一个比一个重</h2>
<p>之前的博客是用像 <a rel="noopener noreferrer" target="_blank" href="https://nextjs.org/">Next.js</a> 这样的全栈 React 框架构建的，或者是 <a rel="noopener noreferrer" target="_blank" href="https://reactrouter.com/">React Router</a> 这样的 SPA 框架构建的，反正一个比一个重、臃肿。亦或者是什么 <a rel="noopener noreferrer" target="_blank" href="https://hexo.io/">Hexo</a>/<a rel="noopener noreferrer" target="_blank" href="https://gohugo.io/">Hugo</a> 静态生成器，主题又不是完全可控。 <span class="rss-sidenote">(哎，我是大懒b，不喜欢改 Hugo/Hexo 的主题，但总是感觉有些地方不合自己的意。)</span></p>
<h2>服务器闲置，于是我开始折腾</h2>
<p>恰逢最近在腾讯云上买了一个轻量云服务器，除了跑一个 QQ 机器人之外大量的资源也就是闲置着。本来我准备让它跑个 <a rel="noopener noreferrer" target="_blank" href="https://writefreely.org/">WriteFreely</a> 这样的带 <a rel="noopener noreferrer" target="_blank" href="https://www.sqlite.org/">SQLite</a> 数据库的完整动态博客系统的。结果发现这货的可自定义性也太差了吧，对于习惯了文件驱动式路由的我简直是灾难，况且我也不会写 <a rel="noopener noreferrer" target="_blank" href="https://go.dev/">Go</a>，自然 Hugo 也被我放弃了。 <span class="rss-sidenote">(哎，WriteFreely 似乎是为多用户优化的，单用户体验雀食挺差的)</span></p>
<p>此外还试了试 <a rel="noopener noreferrer" target="_blank" href="https://ghost.org/">Ghost</a>/<a rel="noopener noreferrer" target="_blank" href="https://astro.build/">Astro</a> 等等，不是太重就是我懒得改了，也就是放弃了。</p>
<p>哎，我灵光一闪，我自己搞一个框架，不久完全自主可控了？ <span class="rss-sidenote">(注意：是搞一个，具体怎么搞，那就不是那么回事了。<s>自然不是我自己写了</s>，不过期末周了，也没有时间写了，权当自己放松了)</span></p>
<h2>最终选择：Tufte CSS 的复古浪漫</h2>
<p>于是我看上了 <a rel="noopener noreferrer" target="_blank" href="https://edwardtufte.github.io/tufte-css/">Tufte CSS</a>，根据作者的描述:<a rel="noopener noreferrer" target="_blank" href="https://edwardtufte.github.io/tufte-css/">Tufte CSS</a> 是 <a rel="noopener noreferrer" target="_blank" href="https://www.edwardtufte.com/">Edward Tufte</a> 风格的 CSS 框架，专注于简洁优雅的排版和侧边栏注释。</p>
<blockquote>
<p>Tufte CSS provides tools to style web articles using the ideas demonstrated by Edward Tufte's books and handouts. Tufte's style is known for its simplicity, extensive use of sidenotes, tight integration of graphics with text, and carefully chosen typography.</p>
</blockquote>
<p>这很好，提供了很简约的设计，可以让网站看起来非常的复古且优雅，这正是我希望的！默认的字体是 ET Book，也是很优雅的西文，中文自动fallback 到 ui-serif 就很好了。 <span class="rss-sidenote">(不过移动设备大多没有系统衬线字体，这是个遗憾...)</span></p>
<h2>10 分钟极速构建</h2>
<p>于是我打开了 <a rel="noopener noreferrer" target="_blank" href="https://cursor.sh/">Cursor</a>，对着它说：</p>
<blockquote>
<p>请你参考 <a rel="noopener noreferrer" target="_blank" href="https://edwardtufte.github.io/tufte-css/%EF%BC%8C%E4%BD%BF%E7%94%A8">https://edwardtufte.github.io/tufte-css/，使用</a> <a rel="noopener noreferrer" target="_blank" href="https://nodejs.org/">Node.js</a> 和 <a rel="noopener noreferrer" target="_blank" href="https://github.com/markdown-it/markdown-it">Markdown It</a> 通过编译 Markdown 文件建立一个极简的个人网站/博客项目，要求具有下面的路由结构：</p>
</blockquote>
<ul>
<li><code>/</code></li>
<li><code>/about</code></li>
<li><code>/blog</code></li>
<li><code>/blog/:slug</code><br>
每个页面都是由 Markdown 文档构建而成的纯静态 HTML 页面。同时，提取站点设置到 config.js 中统一管理。<br>
对于 layouts，提取到/layouts/中统一管理。</li>
</ul>
<p>然后 Cursor 帮我一通修改，<code>npm install</code> 安装依赖，然后 <code>npm run build</code> 测试构建，一通下来，成功地将静态文件输出到 <code>dist</code> 文件夹里头。好耶，我们就完成了，整个过程不超过 10 分钟。</p>
<h2>让 CSS 也变得更干净</h2>
<p>之后我们自然要对 Tufte CSS 的组织进行一些修改，比如，原来的 CSS 没有统一管理一些字体常量和颜色常量，所以我让 Cursor 把它们统统提取到 <code>:root</code> 中统一管理！好耶！现在看起来好多了。</p>
<h3>设计旁注(Sidenote)语法</h3>
<p>Tufte CSS 的一大亮点就是它的这个 Sidenote，很好玩，但是 Markdown 原生并不支持这种语法，于是我让 Cursor 帮我设计一种全新的语法，大概如下：</p>
<pre><code>&lt;!-- SNOTE_5 --&gt; //自动生成诸如 1 xxx 2 xxx 这样的旁注
&lt;!-- MNOTE_8 --&gt; //自动生成无序号的旁注
</code></pre>
<p>测试构建，好耶，一切顺利！不过此时我发现 <code>build.js</code> 也膨胀到了 700 行的长度。于是我又对 Cursor 说：</p>
<blockquote>
<p>请你重构 build.js，引入 <a rel="noopener noreferrer" target="_blank" href="https://www.typescriptlang.org/">TypeScript</a> 支持并拆分成多个组件。对于 <a rel="noopener noreferrer" target="_blank" href="https://www.rssboard.org/rss-specification">RSS</a>/<a rel="noopener noreferrer" target="_blank" href="https://www.sitemaps.org/">Sitemap</a>/<a rel="noopener noreferrer" target="_blank" href="https://www.robotstxt.org/">Robots.txt</a> 等文件的生成请尽量调用 npm 库而不是自行手动写逻辑。</p>
</blockquote>
<p>又过了大概 10 分钟的时间，Cursor 完全构建了一个用 TypeScript 重构的构建流程，引入了类型安全机制，这很好。</p>
<p>到目前为止网站基本就可以用了，要想添加/删除/修改内容只需要简简单单地对对应的 Markdown 文件操作就可以了。 <span class="rss-sidenote">(要多简单有多简单)</span>然后配置好 <a rel="noopener noreferrer" target="_blank" href="https://nginx.org/">Nginx</a> 反代，访问 <a rel="noopener noreferrer" target="_blank" href="http://xn--ip-im8ckc">http://ip地址</a> 就美美成功了。</p>
<h2>最爽的特点：运行时 0 JavaScript</h2>
<p>而且，这个网站最大的特点是：别看<strong>构建时用了 JavaScript 脚本，但运行时没有任何的 JavaScript 脚本</strong>。整个 DOM 结构干干净净，没有 React 的虚拟 DOM 机制，就像一个初出茅庐的人第一次写 HTML 文档一样，一切都是那么的自然，你写的只是内容，JavaScript 只是在帮你构建你的内容。因此，得益于这套机制，网站的性能十分的高。在 <a rel="noopener noreferrer" target="_blank" href="https://developers.google.com/web/tools/lighthouse">Lighthouse</a> 测试后得到了几乎满分的成绩，这很好，在 1s 内加载好页面很不错，用户不会因此失去耐心而关闭你的网页。 <span class="rss-sidenote">(虽然没有 Ajax，在页面间路由的浏览器刷新感知却很小，<s>不像 Next 的夸张路由，等半天都没有反应...</s>)</span></p>
<p>好吧，现在已经 1 月 10 号了，我觉得写这么多已经足够了，我 run 一下 <code>npm run build</code> 然后 <code>commit</code> 一下就睡觉，好好。</p>
<h2>部署也极简</h2>
<blockquote>
<p>对了，你完全没有必要再本地<code>npm run build</code>然后费力打包<code>dist</code>文件夹上传到 Git 或者你的服务器。像 <a rel="noopener noreferrer" target="_blank" href="https://vercel.com/">Vercel</a> 或者 <a rel="noopener noreferrer" target="_blank" href="https://pages.cloudflare.com/">Cloudflare Pages</a> 这样的构建网站，你只需要自定义你的构建命令为<code>npm run build</code>，指定输出目录为<code>/dist</code>就可以了。</p>
</blockquote>
<p>缺点可能是在开发中吧，<code>npm run dev</code> 没有热更新，不过也没必要了，我们要的是一个内容驱动的网站。<strong>既然你的工作主要是在编写 Markdown 文档，那开发服务器又和你有什么关系呢</strong>，又没有必要去动那些 CSS/ Layout HTML，你只需要写 Markdown，剩下的交给构建就可以了，这也是一个比较完美的 SSG（静态站点生成）流程了！</p>
<p>晚安 :)<br>
（本页面的标题均由 AI 生成）</p>
<footer class="notes-list"><h3>Notae</h3><ol><li id="sn-list-0">怎么理工科还要学语文啊！！！<s>你问我啊，我问谁啊？</s> <a href="#ref-marker-0">↩</a></li><li id="sn-list-1">哎，我是大懒b，不喜欢改 Hugo/Hexo 的主题，但总是感觉有些地方不合自己的意。 <a href="#ref-marker-1">↩</a></li><li id="sn-list-2">哎，WriteFreely 似乎是为多用户优化的，单用户体验雀食挺差的 <a href="#ref-marker-2">↩</a></li><li id="sn-list-3">注意：是搞一个，具体怎么搞，那就不是那么回事了。<s>自然不是我自己写了</s>，不过期末周了，也没有时间写了，权当自己放松了 <a href="#ref-marker-3">↩</a></li><li id="sn-list-4">不过移动设备大多没有系统衬线字体，这是个遗憾... <a href="#ref-marker-4">↩</a></li><li id="sn-list-5">xxx <a href="#ref-marker-5">↩</a></li><li id="sn-list-6">要多简单有多简单 <a href="#ref-marker-6">↩</a></li><li id="sn-list-7">虽然没有 Ajax，在页面间路由的浏览器刷新感知却很小，<s>不像 Next 的夸张路由，等半天都没有反应...</s> <a href="#ref-marker-7">↩</a></li><li class="marginnote-item" id="sn-list-8">xxx <a href="#ref-marker-8">↩</a></li></ol></footer>]]></content:encoded>
            <author>gengyue</author>
        </item>
        <item>
            <title><![CDATA[出去走走]]></title>
            <link>https://www.gengyue.dev/blog/walk-outside</link>
            <guid isPermaLink="false">https://www.gengyue.dev/blog/walk-outside</guid>
            <pubDate>Fri, 02 Jan 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[2026 元旦假期的一次武汉城市漫步，打卡汉阳与长江大桥沿线：归元寺、月湖公园、龟山公园、武汉长江大桥、户部巷与大成路。用图片记录旅途碎片，节奏松弛、主打随性，是一次从“写代码循环”里跳出来的短暂换气与灵感采风。]]></description>
            <content:encoded><![CDATA[<p>好耶，是元旦假期，趁此机会，出去走走。哎，只有图片，因为我是大懒 b。 <span class="rss-marginnote">⊕ (2026 元旦假期的一次武汉城市漫步，打卡汉阳与长江大桥沿线。)</span></p>
<h2>哇，是汉阳！</h2>
<h3>归元寺</h3>
<figure><img loading="lazy" decoding="async" src="/static/city-walk/guiyuansi.webp" alt="归元禅寺"><figcaption>归元禅寺</figcaption></figure>
<h3>月湖公园</h3>
<figure><img loading="lazy" decoding="async" src="/static/city-walk/yuehu.webp" alt="月湖公园"><figcaption>月湖公园</figcaption></figure><p> <span class="rss-marginnote">⊕ (<img src="/static/city-walk/yuehu-2.webp" alt="月湖公园"><br>
哎，这月湖公园真的绝美！)</span></p>
<h3>龟山公园</h3>
<figure><img loading="lazy" decoding="async" src="/static/city-walk/guishan.webp" alt="龟山公园"><figcaption>龟山公园</figcaption></figure>
<h2>耶，是长江大桥</h2>
<figure><img loading="lazy" decoding="async" src="/static/city-walk/wuhan-changjiang-daqiao.webp" alt="武汉长江大桥"><figcaption>武汉长江大桥</figcaption></figure>
<figure><img loading="lazy" decoding="async" src="/static/city-walk/daqiao-train.webp" alt="长江大桥上的火车"><figcaption>长江大桥上的火车</figcaption></figure>
<h2>🤤，是户部巷</h2>
<h3>户部巷</h3>
<figure><img loading="lazy" decoding="async" src="/static/city-walk/hubuxiang.webp" alt="户部巷"><figcaption>户部巷</figcaption></figure>
<h3>大成路</h3>
<figure><img loading="lazy" decoding="async" src="/static/city-walk/dacheng-rd.webp" alt="大成路"><figcaption>大成路</figcaption></figure>
<footer class="notes-list"><h3>Notae</h3><ul><li id="sn-list-0">2026 元旦假期的一次武汉城市漫步，打卡汉阳与长江大桥沿线。 <a href="#ref-marker-0">↩</a></li><li id="sn-list-1"><img src="/static/city-walk/yuehu-2.webp" alt="月湖公园"><br>
哎，这月湖公园真的绝美！ <a href="#ref-marker-1">↩</a></li></ul></footer>]]></content:encoded>
            <author>gengyue</author>
        </item>
        <item>
            <title><![CDATA[2025 年终总结]]></title>
            <link>https://www.gengyue.dev/blog/2025-year-in-review</link>
            <guid isPermaLink="false">https://www.gengyue.dev/blog/2025-year-in-review</guid>
            <pubDate>Thu, 01 Jan 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[2025 上半年在刷模拟题与早六晚十中结束高中时代，随后高考落点关山口开启大专生活；暑假从山东到成都看熊猫、逛会议中心、感叹工作与人生，又被武汉热到自闭机场通宵。下半年军训暴晒、入坑冰岩实习肝项目、工训早八连击、秋游碳水狂欢、普测薅分、微积分炸裂，绿皮车跨年收尾。主打一个：回忆有毒但好玩，人生乱学但继续。]]></description>
            <content:encoded><![CDATA[<p>今天是 2026 年 1 月 1 日，我在湖北省武汉市，向诸位致以新年最美好的祝福！<br>
咳咳，套话说完了。让我们看一眼标题：<strong>2025 年终总结</strong>，看来，我要好好 review 一下 2025 年，看看都发生了什么<s>有趣的</s>的小故事呢！</p>
<h2>1 - 6 月 高中生活的妙妙尾声</h2>
<h3>高中牲活</h3>
<p><s>话说我还真不记得这段时间发生了什么了</s>也就是每天写一些神奇的模拟题，做做什么<s>步步高、XX方案、大x轮复习手册</s>之类的<s>垃圾</s>呗<br>
除了对我的大脑灌输了一些神奇的小知识外，似乎不觉得有什么用处了。还有几次神奇的联考，有神奇的赋分机制，不过，不是重点了。</p>
<p>其实高中除了早六晚十<s>还是挺好的</s>，至少，我知道我现在的作息比那个时候不健康多了。那个时候<s>上课吃点小零食，和可爱的😊同桌吹吹牛逼</s>还是<br>
挺有意思的。至少比现在一上思政课就刷鲨臂知乎小红书什么的，除了看抽象内容把大脑搞得 brain rot 之外似乎就没啥营养灌输了,要么就是对着电脑的终端<br>
窗口傻笑，<s>写那什么next项目渲染3分钟气得想把电脑砸了</s>要好。不得不说，高中把大家搞在一起还是有他的好处的，至少不像现在，具体是啥我就不说了。</p>
<h3>关于高烤</h3>
<p><strong>在参加高考之前，高考听起来就像是神话；在参加高考之后，高考也就那么回事儿</strong>。在山东，高考就不可能轻松，不过，多说无益，虽然说考试的时候犯了不少鲨臂错误，<br>
考的似乎也没有模拟考那么好，但是好歹是考上了大专，也是成功的来关山口念书了。害，记得考完生物还和亲爱的gj<s>老登</s>老师激情give me five来着，结果生物考了一团糟。害，没得法。<br>
感觉还是要留点遗憾的，<s>谁说考的好就好呢，别让高考考得太好影响我大学摆烂。</s></p>
<p>总而言之，高中生活<s>虽然充斥着种种霉嚎的回忆</s>但是也不太美好。如果你说，我愿不愿意再回去上一遍高中，我绝对不会说我愿意，就算是上《非诚勿扰》也不行。</p>
<h2>6 - 8 月 似乎是暑假</h2>
<h3>6 月</h3>
<p>6 月干了什么呢？似乎啥也想不起来了，哦，换了一台手机，下回了植物大战僵尸2，但是发现号登录超限了，气得我<s>直接给他删了</s>，顺便<br>
重新开了个新号。哇塞，从零开始的感觉真的很霉渺，我去！</p>
<p>对了，似乎高考是在这个月出的成绩，害，真是不太美妙的回忆。不过没烤特别差，我的运气真的好！</p>
<p>完事好像和同学们一起去开了个毕业典礼，还是挺感动的。还和同桌出去吃了顿饭，不过还没吃爽就被cc搞回学校了，害，what a pity！</p>
<h3>7 月</h3>
<p>7 月份似乎还是暑假，填完妙妙志愿之后就润出去玩了，大概出去了半个多月的样子。从山东省烟台市坐了大概 12 - 13 个小时的高铁去了四川省成都市。<br>
还好中间在西安北站换了趟车，<s>要不然我的腚真的要坐废了</s>。</p>
<h4>成都市</h4>
<p>成都还真是挺不错的！不愧被称为“天府之国”。第一天，自己在酒店睡个爽，然后就润出去<s>坐地铁玩</s>，好像是跑到一个叫<strong>天府大道中段</strong>的地方，<br>
还拍了一张照片，后来才知道照片里的那栋建筑物是什么亚洲最大还是世界最大的会议中心。不管了，感觉雀食不错！</p>
<figure><img loading="lazy" decoding="async" src="/static/chengdu/tianfu.webp" alt="天府大道中"><figcaption>天府大道中</figcaption></figure>
<p>其实还和三叔去了一趟他工作的地方，什么<strong>国星宇航</strong>公司，老板很牛的，公司也很牛！害，要是能找到这样的工作该多好啊！</p>
<p>后面一起去了大熊猫繁育研究基地看<strong>大熊猫🐼</strong>，我去，真的可爱啊，不过是真的胖、真的懒，可以算作是比我懒的物种之一了！</p>
<figure><img loading="lazy" decoding="async" src="/static/chengdu/panda.webp" alt="大熊猫"><figcaption>大熊猫</figcaption></figure>
<p>当然，成都还有个名人名字叫<strong>杜甫</strong>，还去参观了一下他家，果然是后面修的啊，咋这么宽敞明亮啊，杜甫要有这条件也用不着抱怨了！<br>
之后的某天，又去了都江堰玩玩，哇塞，果然是壮观！虽然<s>我既不懂文学，也不懂水利</s>但是我觉得还真是挺壮观的！反正我也不会欣赏景色，这就挺漂亮的说是。</p>
<figure><img loading="lazy" decoding="async" src="/static/chengdu/dufu.webp" alt="杜甫草堂"><figcaption>杜甫草堂</figcaption></figure><p><br>
</p><figure><img loading="lazy" decoding="async" src="/static/chengdu/river3.webp" alt="都江堰"><figcaption>都江堰</figcaption></figure>
<p>成都玩爽了，比较遗憾的是没吃火锅，外加没去市中心，不过我感觉市中心也就那样，没啥玩头。之后似乎就飞去武汉了。</p>
<h4>武汉市</h4>
<p>在武汉，我还真啥也没干。一下飞机我都快被热晕了，和成都完全不是一个量级的。所以我就龟缩在奶奶家呆了一个星期，害，没啥好写的了。</p>
<p>唯一不好的是我去机场，准备坐飞机回家，结果到了机场山航给我打电话，告诉我说飞机晚点了，然后我就在机场从晚上七点待到了第二天将近一点，害，我也是神人了！</p>
<h3>8 月</h3>
<p>前大半个月就宅在家里头，啥也没干说是（）。</p>
<p>后面马上要上大学了，整理整理行李，三个人一块开车回武汉，顺便探索了一下老爹的母校，再顺便去了一趟孝感，感觉没啥玩头。<br>
后面跟着老母亲去了武汉市区两次，吃了一堆碳水，不过雀食种类丰富又好吃，后面在华科也没吃过这么种类丰富和好吃😋的早餐了！</p>
<p>后面就去宿舍了，<strong>斯是陋室，吾德不行</strong>，爱来自华中科技大学沁苑学生公寓。</p>
<h2>9 - 12 月 似乎在上大学</h2>
<h3>9 月</h3>
<p><strong>武汉的9月军训，真是个人物👍</strong>天天热的要死不说，早上七点就要从床上爬起来，<strong>去吃早餐</strong>（哎哎，想起来现在，我都是选择性地忽略掉<br>
早饭和午饭中的某顿，当时的作息真的好正常）。不过教官人还挺好的，<s>但是军训21天雀食不太好</s></p>
<p>军训中组织了去马鞍山森林公园拉练 + 打靶，害，还是挺好玩的，就是<s>打靶又看不出来打中没有</s>。不过雀食有意思！</p>
<figure><img loading="lazy" decoding="async" src="/static/hust/yezhu.webp" alt="野猪"><figcaption>野猪</figcaption></figure>
<p>后来莫名其妙的<s>去“飞虎队”观训也能获得89分了，不过是随机数生成器罢了</s></p>
<h3>10 月</h3>
<p>9 - 10 月份去参加了<a rel="noopener noreferrer" target="_blank" href="https://www.bingyan.net/">冰岩作坊</a>的招新面试和实习，害，肝项目雀食很累，但是感觉和大家在一起写一些没用的小玩具和<br>
做项目雀食好好玩！下面是工卡，漂亮的捏：</p>
<figure><img loading="lazy" decoding="async" src="/static/hust/bingyan.webp" alt="冰岩的工卡"><figcaption>冰岩的工卡</figcaption></figure>
<p>10 月份似乎有个十一假期，不过一直在写妙妙实习项目，所以忽略掉了~</p>
<p>害，9 月底开始上课了，结果这课是越上越多啊，成天早八/早十到晚八/晚十的，周六的工程训练在平添一个早八的同时还要占去整整一天的休息时间。<br>
还要听老登讲一些我压根就不懂也不想懂的东西，有的时候还要忍受老登的暴躁臭脾气，<s>我xx🐎</s>。害，真的烦。不过还是有些好课的：</p>
<ul>
<li>3D 打印认知，可以做一些小玩具</li>
<li>激光雕刻认知，也可以做小玩具，但是老登态度奇差无比！</li>
</ul>
<p>不推荐：</p>
<ul>
<li>小车，唯一真史，能不能跑起来全靠硬件，硬件不行你怎么调整都没用！而工创中心的小车<s>有好的吗</s>我怀疑。</li>
</ul>
<h3>11 月</h3>
<p>11 月初考了一个什么普通话考试，<s>听说可以抵扣中国语文平时分，所以我才报名的</s>。害，这东西就是主打一个付费考试：你付钱、参加考试，完事人家给你发张<br>
什么妙妙证书，你也顺便获得了中国语文的平时分，<s>不用交妙妙读书报告了</s>。咳，说到这儿，现当代文学经典的王振滔老师讲的真的很不错，<s>虽然我没听</s>，但是人真的巨好无比！<br>
（比如：那个报告如果你要写，你写王者荣耀、原神什么的他也接受）</p>
<p>和冰社的小伙伴们一起去汉口秋游了一下😋，吃的很爽，玩得也很不错，唯一的缺点就是<s>那地方网吧奇差无比，约等于没有</s>。不过，谁<br>
还是为了上网而过去呢？</p>
<p>似乎还考了个微积分期中，太久没做题了，算错一堆，害。</p>
<p>再就是大量的满课，燃尽了。。。</p>
<h3>12 月</h3>
<p>工训<s>圆满</s>结课！趁着回黄陂给奶奶过生日的机会，体验了一下<strong>汉口 - 武汉东</strong>的美妙绿皮车，真的很好，可惜只开这两个月，现在已经没了。再就是，<br>
去的时候太晚了，外面黑漆漆的啥都看不见，也没看到亮灯的黄鹤楼。<s>不过顺便刷了个校园跑（大雾</s></p>
<figure><img loading="lazy" decoding="async" src="/static/train/wuhan/IMG_20251213_184210.webp" alt=""><figcaption></figcaption></figure>
<p>有关这段旅程的更多信息，请参阅<a href="/blog/train">这篇文章↗</a></p>
<p>再就是，周六闲下来了，<s>可以抽空出去转转了，刷爆武汉通了</s>。这个月去过的地方大概有：</p>
<ul>
<li>循礼门（2 -&gt; 1） - 黄浦路（1 -&gt; 8，汉口江滩） - 街道口（8 -&gt; 2，之前单独去过一次卓刀泉，探访了著名古迹<strong>安卓小区</strong>）</li>
<li>积玉桥（2 -&gt; 5） - 昙华林武胜门（昙华林，风景真的好好），顺便去了一下粮道街，然后走到黄鹤楼，那天天气真的绝妙！</li>
</ul>
<p>哎，我的武汉通再这么刷下去可真的要爆了，<s>昨天刚刚充了50</s></p>
<p>然后就是昨天晚上下着大雨在地铁上跨年的独特经历了，可真是独特啊，0 时 0 分 0 秒，我正在<strong>协和医院中山公园</strong>站跨年！<br>
祝大家身体健康，<s>永远不去这地方</s>！</p>
<p>咳咳，就写这么多吧，别的想不出来了，有些图片也不插进去凑字数了，祝大家新年快乐！</p>
]]></content:encoded>
            <author>gengyue</author>
        </item>
        <item>
            <title><![CDATA[2025 浏览器从A-Z]]></title>
            <link>https://www.gengyue.dev/blog/2025-a-z</link>
            <guid isPermaLink="false">https://www.gengyue.dev/blog/2025-a-z</guid>
            <pubDate>Wed, 31 Dec 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[这一年，我从 A 到 Z 浏览了各种网站，从科技前沿的苹果新闻、ChatGPT、Qwen，到生产力工具 BuddyUp、Zread.ai，再到前端利器 Vercel、Tailwind CSS、Radix UI、Motion-Primitives，以及灵感与资源聚集地 Notion、Wikipedia、Klim、Yesicon、SJ.land，每一个都丰富了学习、工作和生活。2025 年的互联网全景，浓缩在这一篇 A-Z 浏览记录里！]]></description>
            <content:encoded><![CDATA[<p>2025 年眼看就要过去了，<strong>祝大家新年快乐，长生不老，永远不死！</strong>。<br>
马上就要元旦了，2025年的最后一天，让我们打开浏览器，从A输入到Z，看看最先蹦出来的都是哪些妙妙网站呢！</p>
<h2>A - <a rel="noopener noreferrer" target="_blank" href="http://apple.com.cn/newsroom">apple.com.cn/newsroom</a></h2>
<p>这是 Apple Newsroom，在这里发布苹果公司最新的产品动态、业务服务等等内容。好奇怪，这个怎么成为A开头访问最多的了，看来A开头的网站看少了！ <span class="rss-sidenote">(Apple 的设计真的是独一份的优雅！)</span></p>
<h2>B - buddyup.top</h2>
<p>这是我正在参与开发的一款 AI 辅助留学文书工具<a rel="noopener noreferrer" target="_blank" href="https://buddyup.top">BuddyUp</a>。这个工具可以根据你的个人信息，通过AI调整prompt，写出属于你的最佳个人陈述，<br>
陪你走过申请季！感觉还可以... <span class="rss-sidenote">(AI 还是太好用了！)</span></p>
<h2>C - <a rel="noopener noreferrer" target="_blank" href="http://chatgpt.com">chatgpt.com</a></h2>
<p>从 2023 年诞生以来，相信没有人不认识 ChatGPT 了。这一年来，ChatGPT <s>作为我的宝宝</s>，回答我很多个<s>弱智的</s>问题，感觉，ChatGPT的检索功能和人文关怀做的真的很好😋。<br>
2026 年必须继续高强度使用！ <span class="rss-sidenote">(我的宝宝🤤)</span></p>
<h2>D - <a rel="noopener noreferrer" target="_blank" href="http://doubao.com">doubao.com</a> / <a rel="noopener noreferrer" target="_blank" href="http://chat.deepseek.com">chat.deepseek.com</a></h2>
<p>豆包，作为字节开发的一款 AI 助手，在国内来讲效果还是很不错的。尤其是生图/语言聊天功能，在国内算得上是独树一帜。<br>
不过，在代码构建、深度思考方面，个人感觉豆包还是不如 DeepSeek 等工具的。</p>
<p>至于 DeepSeek，由于我浏览时的习惯，会习惯性地输入<code>d</code>来找 DeepSeek，所以并没有归类到<code>c</code>里面。DeepSeek 也很强，只不过，<br>
人文关怀没有豆包或者 ChatGPT 那么好，语气总是冷冰冰的。不过，我们可以妙妙改变 system prompt，<s>这样，让ai变猫娘也不是不可以。</s></p>
<h2>E - <a rel="noopener noreferrer" target="_blank" href="http://earth.google.com">earth.google.com</a></h2>
<p>哇，地球真是太美丽了，这是用来探索地球大好河山的...<s>好吧我编不下去了</s>。Anyway，地球真的好美啊，比金星火星什么的美多了！</p>
<h2>F - <a rel="noopener noreferrer" target="_blank" href="http://fonts.google.com">fonts.google.com</a></h2>
<p><s>依旧 Google 圣人</s> Google Fonts 提供了开源网页字体的集合，开发者可以轻松地通过 embed 代码将字体集成至前端项目中，还有<s>高速</s>+缓存的 CDN 加速，妙哉！ <span class="rss-sidenote">(哎，国内怎么用不了。<s>某三个大写字母还是太坏辣</s>)</span></p>
<h2>G - <a rel="noopener noreferrer" target="_blank" href="http://github.com">github.com</a></h2>
<p>GitHub 是全球最大<s>同性恋</s>开源项目集合地，在这里，可以探索到成千上万个妙妙开源项目：不管是神奇小玩具，还是轮椅框架，只要你能想，一切均有可能！</p>
<h2>H - <a rel="noopener noreferrer" target="_blank" href="http://hub.hust.edu.cn">hub.hust.edu.cn</a></h2>
<p>??? 这是华中科技大学 HUB 系统的登录页面 ??? <span class="rss-sidenote">(不得不用罢了)</span></p>
<h2>I - <a rel="noopener noreferrer" target="_blank" href="http://iconfont.cn">iconfont.cn</a></h2>
<p>阿里巴巴开源图标库，提供了大量矢量图标，不过似乎没怎么用，看来 i 开头看的网站还是少了！</p>
<h2>J - <a rel="noopener noreferrer" target="_blank" href="http://jyi2ya.github.io">jyi2ya.github.io</a></h2>
<p>jyi 的博客，看来 j 开头看的网站还是少了！<s>怎么只有这个复古博客呢，怎么地得有个 Jetbrains 之类的吧</s></p>
<h2>K - <a rel="noopener noreferrer" target="_blank" href="http://klim.co.nz">klim.co.nz</a></h2>
<p><a rel="noopener noreferrer" target="_blank" href="http://klim.co.nz">klim.co.nz</a> 是一家新西兰的 独立字体设计商店（Klim Type Foundry），由知名字体设计师 Kris Sowersby 创立，总部位于威灵顿（Wellington）。该字体厂致力于设计、制作和销售各种专业字体，可用于平面和数字媒体，并为国际品牌和机构提供定制字体服务。其作品既具历史文化底蕴，又融合当代工艺，曾为多个知名客户设计字体，并获得业界奖项认可。</p>
<p>不过我比较喜欢他们家的网站设计hh</p>
<h2>L - localhost</h2>
<p>每台电脑上都有！不管是 3000 端口（Webpack/Turbopack）还是 5173 端口 (vite/<s>我要吃饭</s>)，啊哈！</p>
<h2>M - <a rel="noopener noreferrer" target="_blank" href="http://motion-primitives.com">motion-primitives.com</a></h2>
<p>Motion-Primitives 是一个面向 Web 开发者和设计师的 开源动画组件集，用于快速构建漂亮、流畅的界面动效。它提供现成的可复用组件（如无限滚动、过渡面板、分组动画、光标效果等），让你可以轻松为网站或应用添加交互动效，而不必每次自己重写动画逻辑。组件基于 Motion（现代动画库）和 Tailwind CSS 构建，支持高度自定义的动画表现。</p>
<p>轮椅，但是用的次数为 0.</p>
<h2>N - <a rel="noopener noreferrer" target="_blank" href="http://notion.so">notion.so</a></h2>
<p>Notion 是一个功能强大的 一体化工作空间，可以帮助个人和团队 捕捉想法、撰写文档、管理项目、构建数据库、协作共享和自动化工作流程，支持笔记、任务、看板、日历、维基等等——用一个平台组织你所有重要的信息和任务。</p>
<p>曾经试着用 Notion 写过博客来着，不过<s>它们的 API 属实难用，经常 500 / RESET 什么的</s>，不知道是不是我的问题。不过，Notion 的设计雀食很好，至少看着非常舒适😋。</p>
<h2>O - <a rel="noopener noreferrer" target="_blank" href="http://one.hust.edu.cn">one.hust.edu.cn</a></h2>
<p>??? 这是“智慧华中大”系统的登录页面 ???</p>
<h2>P - <a rel="noopener noreferrer" target="_blank" href="http://ppio.com/model-api/console">ppio.com/model-api/console</a></h2>
<p>PPIO 派欧云是中国领先的云服务提供商，恰好，PPIO 的老板王闻宇先生是华科的杰出校友，更巧的是，他还是我们冰社的老前辈。所以，谢谢老板爆的金币，3000r 的大模型 API 玩得很爽！</p>
<h2>Q - <a rel="noopener noreferrer" target="_blank" href="http://qwen.ai/home">qwen.ai/home</a></h2>
<p>Qwen（通义千问） 是由 阿里巴巴云（Alibaba Cloud）开发的一系列大型语言模型（LLM）及智能 AI 聊天平台，可用于自然语言理解、生成、推理、编程辅助等多种任务。它是一个集 聊天助手、API 服务和模型生态 于一体的 AI 平台。</p>
<p>嘻嘻，现在我的 QQ 小 bot 就是接的千问的多模态大模型，感觉效果相当不错。至少，LLM 会说人话了。</p>
<h2>R - <a rel="noopener noreferrer" target="_blank" href="http://radix-ui.com">radix-ui.com</a></h2>
<p>Radix UI 是一个开源的 无样式（headless）React 组件库，提供构建高质量 Web 应用和设计系统所需的基础 UI 模块。与许多预设样式的组件库不同，Radix UI 只负责 交互逻辑和可访问性，将样式控制完全交给开发者，这让你能按自己的设计精确定制 UI 外观。</p>
<p><s>依旧轮椅，伟大无需多言！</s>。Radix UI 是我目前用的最顺手的无头 React 组件库，基于它构建的 <a rel="noopener noreferrer" target="_blank" href="https://ui.shadcn.com">Shadcn UI</a> 也很不错！ <span class="rss-sidenote">(伟大的发明)</span></p>
<h2>S - sj.land</h2>
<p>SJ.land 是一个以个人介绍和资源汇集为核心的主页网站，由名为 SJ 的设计师/创作者维护，呈现其最新动态、阅读推荐、精选商品和个人作品集。站点风格简洁直观，集个人更新、阅读列表、精品商品和才华目录于一体。</p>
<p>在他的网站上找灵感是个好点子！</p>
<h2>T - <a rel="noopener noreferrer" target="_blank" href="http://tailwindcss.com">tailwindcss.com</a></h2>
<p>Tailwind CSS 是一个现代的 “utility‑first”（实用优先）CSS 框架，通过一套小而明确的 CSS 类（如 flex、pt‑4、text‑center 等），让开发者可以直接在 HTML 中组合这些工具类来构建任意设计，而不必写大量自定义 CSS。它不是传统带预设组件的框架，而是提供构建 UI 的基础构件，让样式真正由你掌控。</p>
<p><s>怎么轮椅这么多？？</s></p>
<h2>U - unix.bio</h2>
<p>Witt 的个人博客，可惜很久没更新过了。别的怎么就没了，看来 u 开头看的网站还是少了！</p>
<h2>V - <a rel="noopener noreferrer" target="_blank" href="http://vercel.com">vercel.com</a></h2>
<p>Vercel 是一家美国的 云应用与前端部署平台（由 Vercel Inc. 维护，前身叫 ZEIT），专注于帮助开发者快速构建、部署和运行高性能网站与 Web 应用。它以 从代码到全球生产环境 的无缝流程著称 —— 只需推送 Git 代码，即可借助其全球边缘网络自动进行构建与发布。</p>
<p>Vercel 依旧好用，<s>不过国内不怎么好用</s>。不过 Next.js 爆漏洞也太抽象了些...</p>
<h2>W - <a rel="noopener noreferrer" target="_blank" href="http://wikipedia.org">wikipedia.org</a></h2>
<p>Wikipedia 是全球最大的 免费在线百科全书，由全球志愿者共同撰写与维护，几乎涵盖所有百科类主题。它于 2001 年由 Jimmy Wales 和 Larry Sanger 创立，现在由美国的 非营利组织 Wikimedia Foundation 运营。Wikipedia 提供数千万条条目，支持 300 多种语言，面向大众开放阅读与贡献，是互联网最重要的知识资源之一。</p>
<p>查资料一把手，比“x度百科”好许多！</p>
<h2>X - <a rel="noopener noreferrer" target="_blank" href="http://x.com">x.com</a></h2>
<p>~~Former Twitter**. X（<a rel="noopener noreferrer" target="_blank" href="http://x.com">x.com</a>） 是一个全球性的 社交网络平台，原名 Twitter，自 2023 年起由 埃隆·马斯克（Elon Musk） 领导的公司全面完成品牌重塑为 “X”。自 2024 年起，原来的 <a rel="noopener noreferrer" target="_blank" href="http://twitter.com">twitter.com</a> 域名已正式改为 <a rel="noopener noreferrer" target="_blank" href="http://x.com">x.com</a> 并将所有核心系统迁移至该域名，标志着从传统微博式社交平台走向更广泛功能的尝试。</p>
<h2>Y - yesicon.app</h2>
<p>Yesicon.app 是一个专为 开发者与设计师 提供大量 开源、免费、高品质图标资源 的在线平台。它汇集了来自全球顶尖设计团队的图标合集，让你无需自己绘制就能快速获取各种图标用于项目中。</p>
<p><s>怎么又是找图标的</s></p>
<h2>Z - <a rel="noopener noreferrer" target="_blank" href="http://zread.ai">zread.ai</a></h2>
<p><a rel="noopener noreferrer" target="_blank" href="http://Zread.ai">Zread.ai</a> 是由 智谱 <a rel="noopener noreferrer" target="_blank" href="http://Z.ai">Z.ai</a>（Zhipu <a rel="noopener noreferrer" target="_blank" href="http://Z.ai">Z.ai</a>） 推出的 AI 驱动开发效率平台，专注于帮助开发者快速理解、分析和掌握 GitHub 上的开源代码仓库内容。它能够自动解析整个代码库，生成清晰、结构化的项目文档、架构指南和逻辑解释，极大提升项目上手速度和团队协作效率。</p>
<h2>[ChatGPT 的总结]</h2>
<p>哇塞，gengyue你这一年看了不少网站啊，从 A 到 Z 几乎把互联网精华都浏览了一遍：有苹果的官方新闻和 ChatGPT、Qwen 等 AI 助手让你紧跟科技前沿，有 BuddyUp、<a rel="noopener noreferrer" target="_blank" href="http://Zread.ai">Zread.ai</a> 这样的生产力工具陪你学习和开发，还有 Vercel、Tailwind CSS、Radix UI、Motion-Primitives 这样的前端利器帮你构建高效网页应用；你也没忘探索地球美景的 Google Earth、美学字体的 Klim、图标资源的 Yesicon、以及像 Notion、Wikipedia、SJ.land 这样的信息与灵感聚集地；顺便还有 <a rel="noopener noreferrer" target="_blank" href="http://x.com">x.com</a> 这种社交平台，以及华科的本地系统和 localhost 的调试乐趣，总之这一年，你几乎体验了从信息获取、开发工具、设计资源到生产力和社交娱乐的全栈互联网世界，真是丰富又多彩的一年！ <span class="rss-sidenote">(✌)</span></p>
<h2>[ChatGPT 的祝福]</h2>
<p>愿大家在新的一年里：</p>
<ul>
<li>每天都像浏览心爱的 A~Z 网站一样，充满新鲜和惊喜；</li>
<li>学习和工作效率爆表，代码顺利，创意源源不断；</li>
<li>AI 助手和各种工具都成为你的得力小伙伴，让生活和工作更轻松；</li>
<li>快乐和好运像 Tailwind 的实用类一样，随手可得，覆盖全局；</li>
<li>健康、平安、好心情永远在线，每一刻都能像发现新网站一样惊喜连连。</li>
</ul>
<p>新年快乐，2026 年，你要比 2025 年更精彩！✨🎉</p>
<p><strong>平安喜乐，各位！</strong>  —— 人类的祝福</p>
<footer class="notes-list"><h3>Notae</h3><ol><li id="sn-list-0">Apple 的设计真的是独一份的优雅！ <a href="#ref-marker-0">↩</a></li><li id="sn-list-1">AI 还是太好用了！ <a href="#ref-marker-1">↩</a></li><li id="sn-list-2">我的宝宝🤤 <a href="#ref-marker-2">↩</a></li><li id="sn-list-3">哎，国内怎么用不了。<s>某三个大写字母还是太坏辣</s> <a href="#ref-marker-3">↩</a></li><li id="sn-list-4">不得不用罢了 <a href="#ref-marker-4">↩</a></li><li id="sn-list-5">伟大的发明 <a href="#ref-marker-5">↩</a></li><li id="sn-list-6">✌ <a href="#ref-marker-6">↩</a></li></ol></footer>]]></content:encoded>
            <author>gengyue</author>
        </item>
        <item>
            <title><![CDATA[组会分享:神奇的 Tiptap 编辑器]]></title>
            <link>https://www.gengyue.dev/blog/tiptap</link>
            <guid isPermaLink="false">https://www.gengyue.dev/blog/tiptap</guid>
            <pubDate>Sat, 20 Dec 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[本文分享了 Tiptap——一个基于 ProseMirror 的开源富文本编辑器框架。Tiptap 具有高度可定制性和丰富插件生态，支持 Vue 和 React，可通过自定义插件和 UI 满足各种复杂文本编辑需求，是轻量化、高效的前端编辑解决方案。]]></description>
            <content:encoded><![CDATA[<h2>简介</h2>
<p>最近在写 <a rel="noopener noreferrer" target="_blank" href="https://buddyup.top/">BuddyUp</a>的前端，其中文书生成之后需要一个编辑器（进行一些修改），<br>
简单的<code>contentEditable</code>怎么能行！于是就寻找一个功能丰富、配置简单的富文本编辑器，于是就接触到了大名鼎鼎的 <a rel="noopener noreferrer" target="_blank" href="https://tiptap.dev">Tiptap</a>， <span class="rss-sidenote">(<img src="/static/tech/tiptap.webp" alt="Tiptap"><br>
<strong>Tiptap</strong> 是一个开源的富文本编辑器框架，基于 ProseMirror 构建，具有高度的可定制性和扩展性。它支持 Vue 和 React 的集成，并拥有丰富的插件生态。)</span><br>
下面粘贴一段简介，先水几行：</p>
<blockquote>
<p><strong>Tiptap 是一个开源的富文本编辑器框架</strong>，基于 ProseMirror 构建，具有高度的可定制性和扩展性。它允许开发者轻松构建功能强大的在线文本编辑工具，适用于博客、论坛、协作文档等多种场景。</p>
</blockquote>
<blockquote>
<p>Tiptap 提供了模块化设计，所有功能都通过扩展实现，开发者可以根据需求选择和配置扩展。此外，它支持 Vue 和 React 的集成，并拥有丰富的插件生态，如表格、代码块、任务列表等，满足复杂的文本编辑需求。</p>
</blockquote>
<p>简而言之，Tiptap提供了无样式的、可拓展的无头富文本编辑器，十分适合开发中需要大段文字编辑的场景。我们开箱即用，拿来就用，十分符合鲁迅的风格！</p>
<h2>使用</h2>
<h3>安装</h3>
<p>Tiptap 的入门方法并不困难，我们直接 CV 命令过来，就像这样：</p>
<pre><code class="language-bash">npm install @tiptap/react @tiptap/pm @tiptap/starter-kit
</code></pre>
<h3>集成</h3>
<p>通过下面的代码，我们可以轻松初始化一个 Tiptap 编辑器示例，这就像踩死一只蚂蚁那样简单。不过，注意第 10 - 11 行，如果正在使用 SSR，请务必这么设置。</p>
<pre><code class="language-jsx">&quot;use client&quot;;

import { useEditor, EditorContent } from &quot;@tiptap/react&quot;;
import StarterKit from &quot;@tiptap/starter-kit&quot;;

const Tiptap = () =&gt; {
  const editor = useEditor({
    extensions: [StarterKit],
    content: &quot;&lt;p&gt;Hello World! 🌎️&lt;/p&gt;&quot;,
    // Don't render immediately on the server to avoid SSR issues
    immediatelyRender: false,
  });

  return &lt;EditorContent editor={editor} /&gt;;
};

export default Tiptap;
</code></pre>
<h3>插件配置</h3>
<p>Tiptap 具有<a rel="noopener noreferrer" target="_blank" href="https://tiptap.dev/docs/editor/core-concepts/extensions">丰富的插件配置</a>，默认情况下，<code>StarterKit</code>就集成了丰富的内容：</p>
<details><summary>Nodes（节点）</summary>
<ul>
<li><a rel="noopener noreferrer" target="_blank" href="https://tiptap.dev/docs/editor/extensions/nodes/blockquote">Blockquote</a></li>
<li><a rel="noopener noreferrer" target="_blank" href="https://tiptap.dev/docs/editor/extensions/nodes/bullet-list">BulletList</a></li>
<li><a rel="noopener noreferrer" target="_blank" href="https://tiptap.dev/docs/editor/extensions/nodes/code-block">CodeBlock</a></li>
<li><a rel="noopener noreferrer" target="_blank" href="https://tiptap.dev/docs/editor/extensions/nodes/document">Document</a></li>
<li><a rel="noopener noreferrer" target="_blank" href="https://tiptap.dev/docs/editor/extensions/nodes/hard-break">HardBreak</a></li>
<li><a rel="noopener noreferrer" target="_blank" href="https://tiptap.dev/docs/editor/extensions/nodes/heading">Heading</a></li>
<li><a rel="noopener noreferrer" target="_blank" href="https://tiptap.dev/docs/editor/extensions/nodes/horizontal-rule">HorizontalRule</a></li>
<li><a rel="noopener noreferrer" target="_blank" href="https://tiptap.dev/docs/editor/extensions/nodes/list-item">ListItem</a></li>
<li><a rel="noopener noreferrer" target="_blank" href="https://tiptap.dev/docs/editor/extensions/nodes/ordered-list">OrderedList</a></li>
<li><a rel="noopener noreferrer" target="_blank" href="https://tiptap.dev/docs/editor/extensions/nodes/paragraph">Paragraph</a></li>
<li><a rel="noopener noreferrer" target="_blank" href="https://tiptap.dev/docs/editor/extensions/nodes/text">Text</a></li>
</ul>
</details>
<details><summary>Marks（标记/格式）</summary>
<ul>
<li><a rel="noopener noreferrer" target="_blank" href="https://tiptap.dev/docs/editor/extensions/marks/bold">Bold（加粗）</a></li>
<li><a rel="noopener noreferrer" target="_blank" href="https://tiptap.dev/docs/editor/extensions/marks/code">Code（行内代码）</a></li>
<li><a rel="noopener noreferrer" target="_blank" href="https://tiptap.dev/docs/editor/extensions/marks/italic">Italic（斜体）</a></li>
<li><a rel="noopener noreferrer" target="_blank" href="https://tiptap.dev/docs/editor/extensions/marks/link">Link（链接）</a> <em>(v3 新增)</em></li>
<li><a rel="noopener noreferrer" target="_blank" href="https://tiptap.dev/docs/editor/extensions/marks/strike">Strike（删除线）</a></li>
<li><a rel="noopener noreferrer" target="_blank" href="https://tiptap.dev/docs/editor/extensions/marks/underline">Underline（下划线）</a> <em>(v3 新增)</em></li>
</ul>
</details>
<details><summary>Extensions（扩展功能）</summary>
<ul>
<li><a rel="noopener noreferrer" target="_blank" href="https://tiptap.dev/docs/editor/extensions/functionality/dropcursor">Dropcursor（拖拽光标）</a></li>
<li><a rel="noopener noreferrer" target="_blank" href="https://tiptap.dev/docs/editor/extensions/functionality/gapcursor">Gapcursor（间隙光标）</a></li>
<li><a rel="noopener noreferrer" target="_blank" href="https://tiptap.dev/docs/editor/extensions/functionality/undo-redo">Undo/Redo（撤销/重做）</a></li>
<li><a rel="noopener noreferrer" target="_blank" href="https://tiptap.dev/docs/editor/extensions/functionality/listkeymap">ListKeymap（列表快捷键映射）</a> <em>(v3 新增)</em></li>
<li><a rel="noopener noreferrer" target="_blank" href="https://tiptap.dev/docs/editor/extensions/functionality/trailing-node">TrailingNode（尾随节点）</a> <em>(v3 新增)</em></li>
</ul>
</details>
<p>如果不想要这么多插件，我们就妙妙禁用它们！</p>
<pre><code class="language-jsx">import { Editor } from &quot;@tiptap/core&quot;;
import StarterKit from &quot;@tiptap/starter-kit&quot;;

const editor = new Editor({
  content: &quot;&lt;p&gt;Example Text&lt;/p&gt;&quot;,
  extensions: [
    StarterKit.configure({
      // Disable an included extension
      undoRedo: false,

      // Configure an included extension
      heading: {
        levels: [1, 2],
      },
    }),
  ],
});
</code></pre>
<h2>实践</h2>
<h3>实用插件</h3>
<p>在这里列出一些使用性较强的插件，氵几行：</p>
<h4><a rel="noopener noreferrer" target="_blank" href="https://tiptap.dev/docs/editor/extensions/functionality/placeholder#placeholder"><strong>Placeholder</strong></a></h4>
<p>嘻嘻，一个美观的<code>Placeholder</code>插件</p>
<pre><code class="language-jsx">const editor = useEditor({
  extensions: [
    StarterKit,
    Placeholder.configure({
      placeholder: &quot;Write something …&quot;,
      // Use different placeholders depending on the node type:
      placeholder: ({ node }) =&gt; {
        if (node.type.name === &quot;heading&quot;) {
          return &quot;What’s the title?&quot;;
        }

        return &quot;Can you add some further context?&quot;;
      },
    }),
  ],
});
</code></pre>
<h4><a rel="noopener noreferrer" target="_blank" href="https://tiptap.dev/docs/editor/extensions/functionality/bubble-menu"><strong>BubbleMenu</strong></a></h4>
<p>这个插件实现了一个悬浮菜单，选中部分文字的时候会显示一个浮动工具栏，省去了手动计算并且实现的麻烦（偷懒…</p>
<h4><a rel="noopener noreferrer" target="_blank" href="https://tiptap.dev/docs/editor/extensions/functionality/character-count"><strong>CharacterCount</strong></a></h4>
<p>顾名思义，算字符数和单词数的()</p>
<h4><a rel="noopener noreferrer" target="_blank" href="https://tiptap.dev/docs/editor/extensions/functionality/drag-handle"><strong>Drag Handle</strong></a></h4>
<p>想把节点随意拖来拖去？这个绝对适合你！</p>
<p>当然还有很多，可以自由地探索！不过，注意 Tiptap 是有付费机制的，所以说并不是所有插件都是可用的...</p>
<h3>自定义插件</h3>
<p>TipTap 丰富的可拓展性亦体现在其可以自定义插件，为编辑器添加丰富多彩的效果，比如下面这个用在 BuddyUp 编辑器里的自定义翻译块插件：</p>
<pre><code class="language-jsx">/* eslint-disable @typescript-eslint/no-explicit-any */
import Paragraph from &quot;@tiptap/extension-paragraph&quot;;

export const ParagraphWithTranslation = Paragraph.extend({
  addAttributes() {
    return {
      ...this.parent?.(),
      translationId: {
        default: null,
        parseHTML: (element) =&gt; element.getAttribute(&quot;data-has-translation&quot;),
        renderHTML: (attributes) =&gt; {
          if (!attributes.translationId) {
            return {};
          }
          return {
            &quot;data-has-translation&quot;: attributes.translationId,
          };
        },
      },
    };
  },
});
</code></pre>
<p>我们通过自定义插件扩展了Paragraph节点，添加了一个translationId属性。</p>
<p><strong>解析时（parseHTML）</strong>：当编辑器从HTML加载内容时，如果遇到有<code>data-has-translation</code>属性的段落，就把这个属性值提取出来，存储为节点的<code>translationId</code>属性。</p>
<p><strong>渲染时（renderHTML）</strong>：当编辑器输出HTML时，如果节点有<code>translationId</code>属性，就在输出的HTML元素上添加<code>data-has-translation</code>属性。</p>
<p>这样，前端就可以通过CSS选择器<code>p[data-has-translation]</code>为这些段落添加特殊样式，实现翻译块的视觉区分。并且，我们也可以通过<code>data-has-translation</code>控制翻译行为，防止重复翻译。</p>
<h3>自定义 UI</h3>
<p>由于 Tiptap 是 headless 的，所以我们自然需要对其进行一些妙妙美化，<a rel="noopener noreferrer" target="_blank" href="https://ui.shadcn.com">Shadcn UI</a> 就还不错，不过，似乎不是讨论的重点了。</p>
<h2>总结</h2>
<p>总而言之，Tiptap 非常适合处理一些需要富文本编辑的场景，既省去了手动写逻辑的麻烦，简化了实现方法，又规避了老旧富文本编辑器上手难，UI 难以控制的问题。<br>
所以，针对来讲，是一个很不错的轻量化解决方案。所以，<a rel="noopener noreferrer" target="_blank" href="https://tiptap.dev">Check it out →</a></p>
<footer class="notes-list"><h3>Notae</h3><ol><li id="sn-list-0"><img src="/static/tech/tiptap.webp" alt="Tiptap"><br>
<strong>Tiptap</strong> 是一个开源的富文本编辑器框架，基于 ProseMirror 构建，具有高度的可定制性和扩展性。它支持 Vue 和 React 的集成，并拥有丰富的插件生态。 <a href="#ref-marker-0">↩</a></li></ol></footer>]]></content:encoded>
            <author>gengyue</author>
        </item>
        <item>
            <title><![CDATA[好神奇的 WSL!]]></title>
            <link>https://www.gengyue.dev/blog/magic-wsl</link>
            <guid isPermaLink="false">https://www.gengyue.dev/blog/magic-wsl</guid>
            <pubDate>Fri, 19 Dec 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[WSL 真的太神奇了！三个小字母，却能让 Linux 和 Windows 无缝融合。曾经为了 Linux 双系统折腾得头大，现在只需一行命令就能安装，选个发行版，设置好用户名密码，立马体验丝滑流畅的开发环境。耍帅、装逼、写代码，一切都so easy，只差微软给我打个广告费！]]></description>
            <content:encoded><![CDATA[<p><a rel="noopener noreferrer" target="_blank" href="https://learn.microsoft.com/zh-cn/windows/wsl/install">WSL</a>真是太神奇了，竟然这三个小小的字母竟可以拥有如此强大的功能和能量！</p>
<p>嘻嘻，long long ago，曾几何时，时间如白驹过隙，我曾经使用过 Linux，不过那个时候都是装的双系统或者什么的，主要就是 Ubuntu 之类的 Debian 系系统，<br>
不过，受当时电脑配置的限制和我好奇心的消减以及 Linux 软件生态的过于贫瘠，使用过一段时间之后全部删干净了，回归~ <s>巨硬窗户</s>！</p>
<p>但是，好神奇，WSL，可以将 Linux 与 Windows 如此巧妙地融合！现在开始，可能只需要一行命令和若干次重启：</p>
<pre><code class="language-bash">wsl --install
</code></pre>
<p>嘻嘻，然后列出想要安装的 Linux 发行版列表，从arch到centos，再到ubuntu，可谓是琳琅满目，应有尽有啊：</p>
<pre><code class="language-bash">wsl.exe --list --online
</code></pre>
<p>然后美美地选择一个发行版安装！美美地设置好用户名和密码！然后：</p>
<pre><code class="language-bash">sudo apt update
sudo apt install neofetch
neofetch
</code></pre>
<p>耍帅时间到！哇，好帅，这一刻，show time has arrived! 原来安装 Linux 是为了装啊，一切，一切，都满足了。</p>
<figure><img loading="lazy" decoding="async" src="/static/tech/neofetch.webp" alt="Neofetch"><figcaption>Neofetch</figcaption></figure>
<p>What are u waiting for? 现在开始，可能只需要一次安装！你将拥有和 Windows 无缝集成德芙般丝滑流畅的开发体验！</p>
<p>不过<s>微软似乎忘记给我广告费了...</s></p>
]]></content:encoded>
            <author>gengyue</author>
        </item>
        <item>
            <title><![CDATA[童话:玛德和法克的故事]]></title>
            <link>https://www.gengyue.dev/blog/mother-fuck</link>
            <guid isPermaLink="false">https://www.gengyue.dev/blog/mother-fuck</guid>
            <pubDate>Thu, 18 Dec 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[很久很久以前，玛德和法克是一对快乐小伙伴，但他们的友情很快被一群“新朋友”打破平衡。派瑞马斯、瑞库埃斯特、熊人、布儒瑟、尤二……每个人都想加入游戏，分组、抢人、玩耍，友情、竞争、欢乐齐飞。最终，夕阳下只剩下尤二在纠结，两个团体的笑声却回荡在空气中——一场童话般的社交大冒险，就此展开！]]></description>
            <content:encoded><![CDATA[<p>相传很久很久以前，玛德和法克是一对好朋友，它们天天在一起玩耍。 <span class="rss-marginnote">⊕ (<img src="/static/story/mother-fuck.webp" alt="玛德和法克的故事">)</span></p>
<p>有一天，二人正在一起开心的玩耍，突然，玛德发现派瑞马斯(Params)正朝着它们走来，双手插兜，哼着一首轻快的小曲。</p>
<p>“嘿，派瑞马斯，快来一起玩啊” - 玛德邀请派瑞马斯<br>
“哦，原来是那个派瑞马斯，见他天天和玛德在一起玩” - 法克在心里默念道</p>
<p>“哦，你好，玛德，很高兴见到你，当然愿意，我们一起去玩吧” - 派瑞马斯很高兴。于是，玛德和派瑞马斯走到一边，玩起了你画我猜的游戏，<br>
不过，总是派瑞马斯出题，玛德回答。</p>
<p>看到二人玩得如此开心，法克心里很不是滋味，就在此时，他看到瑞库埃斯特(Request)正走过来，于是他赶忙邀请瑞库埃斯特：“嘿，哥们，<br>
快过来一起玩啊！”</p>
<p>瑞库埃斯特长得人高马大，拥有强健的体魄(body)，于是，二人玩起了身体格斗游戏，瑞库埃斯特不断用强壮的身体撞击法克，法克虽然无处招架，<br>
但看的出来，法克雀食专业对口，二人玩的十分开心。</p>
<p>正当二人玩的渐入佳境的时候，熊人(Bearer)走了过来，看到法克和瑞库埃斯特玩的十分开心，他很想加入，于是走过来：“哥们，你们玩的很开心，我也想加入”</p>
<p>瑞库埃斯特和法克抬起头，看到了熊人，二人也很高兴。于是，顺理成章的，熊人加入了法克的团体，三人在一起玩的不亦乐乎。</p>
<p>玛德和派瑞马斯见此，心里很不是滋味。</p>
<p>“它们是三个人，而我们，1，2，只有两个人，1，2...”(Polling)</p>
<p>正当，二人emo的时候，布儒瑟(Browser)和他的好哥们尤二(URL)走了过来，玛德和派瑞马斯见此，连忙邀请二人加入它们。布儒瑟很是爽快，<br>
当机立断加入了玛德和派瑞马斯的团体，而尤二则很纠结，似得了选择困难症一样。</p>
<p>时间一分一秒流逝，夕阳逐渐西垂，地面上只投射出尤二犹豫的身影、回响着两个团体快乐的笑声...</p>
<footer class="notes-list"><h3>Notae</h3><ul><li id="sn-list-0"><img src="/static/story/mother-fuck.webp" alt="玛德和法克的故事"> <a href="#ref-marker-0">↩</a></li></ul></footer>]]></content:encoded>
            <author>gengyue</author>
        </item>
        <item>
            <title><![CDATA[小故事:我们不说“包的”]]></title>
            <link>https://www.gengyue.dev/blog/baode-story</link>
            <guid isPermaLink="false">https://www.gengyue.dev/blog/baode-story</guid>
            <pubDate>Tue, 16 Dec 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[阳光明媚，小明回家却遇上了“429 Too Many Requests”的漏水事件。感谢慈母和水管工人的高能救援，配合《Get Started Document》与 webpack 打包，最终在 useEffect 的神奇加持下，水顺利来了。这是一个充满程序员风格的家庭小故事：bug、修复、等待，再配上美味饺子，生活也能如此有趣。]]></description>
            <content:encoded><![CDATA[<p>又是阳光明媚的一天啊，身心俱疲的小明高高兴兴地从学校回到了家中，一回到家，他不由自主地大喊： <span class="rss-marginnote">⊕ (<img src="/static/story/baode.webp" alt="我们不说包的">)</span></p>
<p>“慈母！寒舍的429 Too Many Requests怎么漏水了？！”闻讯赶来的明母慈祥地回答道：“无忧无虑，小明，<br>
水管工人会debug好的，请先用膳”</p>
<p>“噫吁嚱，是饺子哦，我最爱吃饺子了”小明兴高采烈，“这饺子真是让我垂涎三尺，吃得津津有味，是谁势在必得的？”<br>
“哦，是我奋勇争先的，怎么样，200 OK的好吧”小明母亲说道。</p>
<p>此时此刻，隔壁的水管工人大喊：“这根水管被人hotfix过！怎么没有文档？”“有的有的”，明母听闻，连忙拿起桌上的<br>
《Get Started Document》慌忙递过去。四分之一个时辰过去，水管工终于修好了tunnel，擦了擦汗，对明母说：“以后不要<br>
太高并发了，再爆了就麻烦了”随后用webpack打包剩下的零件。明母慌忙将vite递过去，水管工人摆了摆手表示拒绝：“老东西了，别折腾了”<br>
，明母见状也不再勉强。</p>
<p>送走水管工人后，小明迫不及待地去打开水龙头，发现怎么还是没有水。明母见状，叫他不要急：“useEffect是异步的，等 State 更新完就好了”<br>
果然，片刻，水来了。</p>
<footer class="notes-list"><h3>Notae</h3><ul><li id="sn-list-0"><img src="/static/story/baode.webp" alt="我们不说包的"> <a href="#ref-marker-0">↩</a></li></ul></footer>]]></content:encoded>
            <author>gengyue</author>
        </item>
        <item>
            <title><![CDATA[小玩具:HUST 吃饭]]></title>
            <link>https://www.gengyue.dev/blog/hust-chifan</link>
            <guid isPermaLink="false">https://www.gengyue.dev/blog/hust-chifan</guid>
            <pubDate>Mon, 15 Dec 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[吃饭这件事，也可以用代码玩出花样！本文介绍了用 Next.js 重写的 HUST 食堂信息小玩具，通过爬取官网、清洗数据、计算当前开放状态，你就能随时知道哪家食堂开门，还能轻松集成到自己的 bot 中，让吃饭变得科技感满满——从此再也不用盲目问“哪家食堂开门啦？”]]></description>
            <content:encoded><![CDATA[<p><a rel="noopener noreferrer" target="_blank" href="https://github.com/jyi2ya/HUST-Chifan">https://github.com/jyi2ya/HUST-Chifan</a></p>
<p>灵感是上边这个👆，原项目是用 perl 写的，奈何本人并不会写 perl ，并且 perl 对于我而言针对 web 集成而言其实相当不友好，于是决定用 next.js 重写一个版本，嘻嘻，这下配置容易些了。 <span class="rss-sidenote">(原项目：<a rel="noopener noreferrer" target="_blank" href="https://github.com/jyi2ya/HUST-Chifan">HUST-Chifan</a> (Perl 版本))</span></p>
<h2>核心</h2>
<p>核心原理其实很简单，我们通过<code>cheerio</code>爬取华中科技大学后勤处官网的链接（上面包含着各个食堂的营业时间等等），然后我们针对性的进行一些处理：</p>
<h2>开发</h2>
<pre><code class="language-typescript">/* eslint-disable @typescript-eslint/no-explicit-any */
import { NextResponse } from &quot;next/server&quot;;
import { parseCanteenTd } from &quot;@/lib/parseCanteen&quot;;
import * as cheerio from &quot;cheerio&quot;;

export async function GET() {
  const html = await fetch(&quot;https://hq.hust.edu.cn/ysfw/stfw.htm&quot;).then((r) =&gt;
    r.text()
  );
  const $ = cheerio.load(html);

  const canteens: any[] = [];

  $(&quot;td[valign='top']&quot;).each((_, td) =&gt; {
    const tdHtml = $.html(td);
    const parsed = parseCanteenTd(tdHtml);
    if (parsed.name) canteens.push(parsed);
  });

  return NextResponse.json({ canteens });
}
</code></pre>
<p>嘻嘻，由你所见，我们只是<code>fetch</code>了一下静态的html页面，然后利用cheerio加载，找到匹配的<code>td[valign='top']</code>元素，然后对其进行一些处理：</p>
<pre><code class="language-typescript">import * as cheerio from &quot;cheerio&quot;;

export function parseCanteenTd(tdHtml: string) {
  const $ = cheerio.load(tdHtml);

  const name = $(&quot;strong span&quot;)
    .text()
    .replace(/^\d+、?/, &quot;&quot;)
    .replace(/\s+/g, &quot; &quot;)
    .trim();

  const times: { meal: string; start: string; end: string }[] = [];

  $(&quot;p&quot;).each((_, p) =&gt; {
    const text = $(p).text().replace(/\s+/g, &quot; &quot;).trim();

    const match =
      /(早餐|中餐|午餐|晚餐|早、中餐)\s*(\d{1,2}[:：]\d{2})\s*[-–~]\s*(\d{1,2}[:：]\d{2})/.exec(
        text
      );

    if (match) {
      let [start, end] = match.slice(2);
      const meal = match[1];

      start = start.replace(&quot;：&quot;, &quot;:&quot;);
      end = end.replace(&quot;：&quot;, &quot;:&quot;);

      times.push({ meal, start, end });
    }
  });

  return { name, times };
}
</code></pre>
<p>然后我们用正则表达式清洗掉鲨臂官网上乱七八糟的标题文本，然后匹配<code>match</code>的文本，注意，HUST有些食堂标注的是“早、午餐”，很是神奇。</p>
<p>现在，我们就得到了干干净净的数据，我们就可以把这些数据乖乖地<code>push</code>到我们构建的数组中咯！</p>
<p>同样地，对于<code>/api/open-now</code>，我们进行一些处理：</p>
<pre><code class="language-typescript">/* eslint-disable @typescript-eslint/no-explicit-any */
import { NextResponse } from &quot;next/server&quot;;

export async function GET() {
  const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || &quot;http://localhost:3000&quot;;
  const data = await fetch(`${baseUrl}/api/canteen`).then((r) =&gt; r.json());

  const now = new Date();

  const beijingMinutes = (now.getUTCHours() + 8) * 60 + now.getUTCMinutes();

  const open = data.canteens
    .map((c: any) =&gt; {
      const currentTimeSlot = c.times.find((t: any) =&gt; {
        const [sh, sm] = t.start.split(&quot;:&quot;).map(Number);
        const [eh, em] = t.end.split(&quot;:&quot;).map(Number);

        const start = sh * 60 + sm;
        const end = eh * 60 + em;

        return beijingMinutes &gt;= start &amp;&amp; beijingMinutes &lt;= end;
      });

      if (!currentTimeSlot) return null;

      const [eh, em] = currentTimeSlot.end.split(&quot;:&quot;).map(Number);
      const endMinutes = eh * 60 + em;

      const remaining = Math.max(0, (endMinutes - beijingMinutes) * 60 * 1000);

      return {
        ...c,
        remaining,
      };
    })
    .filter(Boolean);

  if (!open.length) {
    return new Response(&quot;坏了，现在没有吃的了&quot;, { status: 404 });
  }

  return NextResponse.json(open);
}
</code></pre>
<p>由于我们最终可能要部署到 Vercel 或者类似的服务器，服务器发起请求时时间的运算时 UTC 时间，而我们在中国，因此需要手动转换成北京时间，否则我们在计算当前是否有食堂开门的时候就会有一些些问题</p>
<p>最终我们就得到了以下的 API：</p>
<h2>API</h2>
<p><code>/api/canteen</code></p>
<p><strong>Method:GET</strong> 获取所有食堂的JSON数据</p>
<p><code>/api/canteen/:canteenName</code></p>
<p><strong>Method:GET</strong> 获取指定食堂的JSON数据</p>
<p><code>/api/canteen/open-now</code></p>
<p><strong>Method:GET</strong> 获取目前能吃的食堂的数据</p>
<p><code>/api/kaifan</code></p>
<p><strong>Method:GET</strong> 获取所有食堂目前的开饭状态</p>
<h2>与你的 bot 集成</h2>
<p>这个玩意儿可以轻松缝合到你的 next 项目中，搭建一个属于你的提醒吃饭 bot!</p>
<p>只需要注意把当前的系统时间和获取到的数据一起写入llm的prompt里就可以了！</p>
<footer class="notes-list"><h3>Notae</h3><ol><li id="sn-list-0">原项目：<a rel="noopener noreferrer" target="_blank" href="https://github.com/jyi2ya/HUST-Chifan">HUST-Chifan</a> (Perl 版本) <a href="#ref-marker-0">↩</a></li></ol></footer>]]></content:encoded>
            <author>gengyue</author>
        </item>
        <item>
            <title><![CDATA[月入 -1.2k的不精致男大学生如何坐火车？]]></title>
            <link>https://www.gengyue.dev/blog/train</link>
            <guid isPermaLink="false">https://www.gengyue.dev/blog/train</guid>
            <pubDate>Sun, 14 Dec 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[月入 -1.2k 的不精致男大学生，也能优雅（？）地坐火车！本文手把手教你从买票、找检票口、登上正确车厢，到一路刷校园跑、包整趟车，甚至和爱车挥手告别。顺便提醒你：冬天别开绿皮车窗，火车旅行其实比你想象的有趣多了！]]></description>
            <content:encoded><![CDATA[<h2>Steps</h2>
<ul>
<li>Step 0. 在12306上购买一张火车票</li>
<li>Step 1. 根据火车票找到应该上车的车站 <span class="rss-marginnote">⊕ (<img src="/static/train/wuhan/IMG_20251213_170152.webp" alt="汉口火车站"><br>
千万不要到错误的火车站了，针对武汉市而言，有汉口、武昌、武汉、武汉东等等车站，不要走错了。)</span></li>
<li>Step 2. 根据火车票找到对应的检票口</li>
<li>Step 3. 根据检票口找到应该去的站台</li>
<li>Step 4. 在站台上找到自己应该坐的车次（注意不要上错车）</li>
</ul>
<figure><img loading="lazy" decoding="async" src="/static/train/wuhan/IMG_20251213_174524.webp" alt="汉口火车站站台"><figcaption>汉口火车站站台</figcaption></figure>
<ul>
<li>Step 5. 让火车带着你畅游武汉三镇，依次经过汉口、汉阳和武昌，最后当大部分人都在武昌下车后，你就可以包了整趟车！</li>
<li>Step 6. 在终点站下车，和你的爱车farewell</li>
</ul>
<figure><img loading="lazy" decoding="async" src="/static/train/wuhan/IMG_20251213_184210.webp" alt="绿皮车车"><figcaption>绿皮车车</figcaption></figure>
<h2>Additional Tips</h2>
<ul>
<li>
<p>Tip 0. 请不要在冬季打开绿皮车的车窗，因为真的很冷</p>
</li>
<li>
<p>Tip 1. 谁说坐火车不能刷校园跑？</p>
<blockquote>
<p>PS：坐火车真的好好玩！</p>
</blockquote>
</li>
</ul>
<p>对了，还有有漂亮的安检小姐姐提醒你书包里带了一个订书机，也有漂亮的站务小姐姐告诉你这里是四号车厢，大家快来试试吧！</p>
<footer class="notes-list"><h3>Notae</h3><ul><li id="sn-list-0"><img src="/static/train/wuhan/IMG_20251213_170152.webp" alt="汉口火车站"><br>
千万不要到错误的火车站了，针对武汉市而言，有汉口、武昌、武汉、武汉东等等车站，不要走错了。 <a href="#ref-marker-0">↩</a></li></ul></footer>]]></content:encoded>
            <author>gengyue</author>
        </item>
        <item>
            <title><![CDATA[重新开始写博客]]></title>
            <link>https://www.gengyue.dev/blog/hello-world</link>
            <guid isPermaLink="false">https://www.gengyue.dev/blog/hello-world</guid>
            <pubDate>Fri, 12 Dec 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[又回到写博客的老路上了！在 AI 时代，和智能体聊天似乎才够潮，但写博客就像自言自语的孤芳自赏，没人看也无妨。记录思绪、调戏代码、偶尔秀操作，博客依旧是表达自己、让脑洞自由流淌的好地方——毕竟，谁不想偶尔对着屏幕喊一句“Hello World”？]]></description>
            <content:encoded><![CDATA[<p>很是神奇，最后怎么又开始写博客了</p>
<p>好神奇，在这个神奇的AI时代，似乎和智能体对话才是潮流，写博客总是那么 old-fashioned。可我怎么又写起来了，反正也没人看。</p>
<p>诶诶，还真是，写博客和与调戏 bot 一样，更多的是&quot;江郎才尽&quot;的&quot;孤芳自赏&quot;，反正都只有自己能够看到，但是能不能看到自己，那就另当别论了。 <span class="rss-sidenote">(嘿，宝宝，我们来聊聊吧<br>都说了，用中文回答我<br>啊喂，别乱动我的代码，你怎么把我库删了，你这傻猫)</span></p>
<p>好的，看看这次写博客能坚持多久，我尽量多写点有用的东西，<s>尽量不搬石</s></p>
<pre><code class="language-jsx">console.log('Hello World'); //朕要昭告天下，重新开始写博客了哈哈
throw new error ('Brain Shut'); //???
</code></pre>
<p>Anyway, let the mind flow ~   Oh… <code>await</code>  read ECONNRESET 是何意味啊(</p>
<footer class="notes-list"><h3>Notae</h3><ol><li id="sn-list-0">嘿，宝宝，我们来聊聊吧<br>都说了，用中文回答我<br>啊喂，别乱动我的代码，你怎么把我库删了，你这傻猫 <a href="#ref-marker-0">↩</a></li></ol></footer>]]></content:encoded>
            <author>gengyue</author>
        </item>
    </channel>
</rss>