<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet href="/style.xsl" type="text/xsl"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>面条实验室</title>
    <link>https://miantiao.me</link>
    <description>折腾些什么玩意</description>
    <atom:link href="https://miantiao.me/diyfeed.xml" rel="self" type="application/rss+xml" />
    <lastBuildDate>Sat, 11 Jul 2026 16:48:52 GMT</lastBuildDate>
    <item>
      <title>用 AI Agent 搞了个全自动 AI 周刊</title>
      <link>https://miantiao.me/posts/aigc-weekly/</link>
      <guid>https://miantiao.me/posts/aigc-weekly/</guid>
      <description>一个由 Claude Agent 全自动驱动的 AIGC 周刊，基于 Next.js 15 和 Cloudflare 全栈构建，每周自动搜集、筛选、生成并发布 AI 领域的精选内容。</description>
      <pubDate>Sat, 13 Dec 2025 05:10:31 GMT</pubDate>
      <content:encoded><![CDATA[<link rel="preload" as="image" href="https://aigc-weekly.agi.li/opengraph-image.png"/>
<section class="not-prose relative -mx-4 mb-16 overflow-hidden rounded-2xl bg-gradient-to-br from-zinc-100 via-zinc-50 to-white p-8 shadow-xs dark:from-zinc-900 dark:via-zinc-800 dark:to-zinc-900 sm:-mx-6 sm:p-12 md:p-16"><div class="absolute -right-20 -top-20 h-64 w-64 rounded-full bg-gradient-to-br from-zinc-300/30 to-zinc-400/20 blur-3xl dark:from-zinc-600/20 dark:to-zinc-500/10"></div><div class="absolute -bottom-20 -left-20 h-64 w-64 rounded-full bg-gradient-to-tr from-zinc-300/30 to-zinc-400/20 blur-3xl dark:from-zinc-600/20 dark:to-zinc-500/10"></div><div class="relative"><div class="mb-6 inline-flex items-center gap-2 rounded-full bg-zinc-200/80 px-4 py-2 text-sm font-medium text-zinc-700 dark:bg-zinc-700/80 dark:text-zinc-300"><span class="icon-[tabler--robot] h-4 w-4"></span><span>Agentic AI 实验</span></div><h2 class="mb-6 text-2xl font-bold tracking-tight text-zinc-900 dark:text-zinc-100 sm:text-3xl md:text-4xl"><p>当 AI 成为编辑<br/>
<span class="text-zinc-500 dark:text-zinc-400">周刊从此自动运转</span></p></h2><p class="max-w-2xl text-lg leading-relaxed text-zinc-600 dark:text-zinc-400"><p>最近我做了一个有点意思的项目——<strong class="font-semibold text-zinc-900 dark:text-zinc-100">AIGC Weekly</strong>。这不仅是一份关注 AIGC（人工智能生成内容）领域的周刊，更是一次关于 <strong class="font-semibold text-zinc-900 dark:text-zinc-100">Agentic AI（代理智能）</strong> 的深度实验。</p></p></div></section>
<p class="text-lg leading-relaxed text-zinc-700 dark:text-zinc-300"><p>今天想和大家聊聊，我为什么要做这个项目，以及它是如何通过一群 AI Agent 自动运转起来的。</p></p>
<figure class="not-prose my-12 overflow-hidden rounded-xl shadow-lg ring-1 ring-zinc-200 dark:ring-zinc-800"><img src="https://aigc-weekly.agi.li/opengraph-image.png" alt="AIGC Weekly" class="w-full"/></figure>

<section class="not-prose my-16"><div class="mb-8 flex items-center gap-4"><span class="flex h-12 w-12 items-center justify-center rounded-xl bg-zinc-200 text-zinc-700 dark:bg-zinc-700 dark:text-zinc-300"><span class="icon-[tabler--bulb] h-6 w-6"></span></span><h2 class="text-xl font-bold text-zinc-900 dark:text-zinc-100 sm:text-2xl">为什么要做 AIGC Weekly？</h2></div><p class="mb-8 text-lg leading-relaxed text-zinc-700 dark:text-zinc-300">作为一名开发者，我每天都会被海量的 AI 资讯淹没：</p><ul class="mb-8 space-y-4"><li class="flex items-start gap-4"><span class="flex h-7 w-7 shrink-0 items-center justify-center rounded-full bg-zinc-100 dark:bg-zinc-800"><span class="icon-[tabler--brand-github] h-4 w-4 text-zinc-600 dark:text-zinc-400"></span></span><span class="text-lg text-zinc-700 dark:text-zinc-300">GitHub 上每天冒出无数个新项目</span></li><li class="flex items-start gap-4"><span class="flex h-7 w-7 shrink-0 items-center justify-center rounded-full bg-zinc-100 dark:bg-zinc-800"><span class="icon-[tabler--message-circle] h-4 w-4 text-zinc-600 dark:text-zinc-400"></span></span><span class="text-lg text-zinc-700 dark:text-zinc-300">Twitter 和 Hacker News 上的讨论层出不穷</span></li><li class="flex items-start gap-4"><span class="flex h-7 w-7 shrink-0 items-center justify-center rounded-full bg-zinc-100 dark:bg-zinc-800"><span class="icon-[tabler--article] h-4 w-4 text-zinc-600 dark:text-zinc-400"></span></span><span class="text-lg text-zinc-700 dark:text-zinc-300">各种技术博客更新得比我读得还快</span></li></ul><blockquote class="relative my-10 border-l-4 border-zinc-400 bg-zinc-50 py-6 pl-8 pr-6 italic text-zinc-700 dark:border-zinc-500 dark:bg-zinc-800/50 dark:text-zinc-300"><span class="icon-[tabler--quote] absolute -left-3.5 -top-3.5 h-7 w-7 text-zinc-400 dark:text-zinc-500"></span><p><strong class="not-italic">信息过载</strong>是这个时代的通病，尤其是在 AI 这样一个日新月异的领域。我希望能有一个工具，能帮我从这些噪音中筛选出真正有价值的信号。</p></blockquote><p class="mb-6 text-lg text-zinc-700 dark:text-zinc-300">但我不想人工来做这件事。</p><p class="text-lg text-zinc-700 dark:text-zinc-300">既然我们已经有了像 Claude 这样强大的 LLM，为什么不让 AI 自己来当编辑呢？于是，<strong class="font-semibold text-zinc-900 dark:text-zinc-100">AIGC Weekly</strong> 诞生了。</p><div class="mt-8 inline-flex items-center gap-3 rounded-lg bg-zinc-100 px-5 py-3 text-base font-medium text-zinc-700 dark:bg-zinc-800 dark:text-zinc-300"><span class="icon-[tabler--target] h-5 w-5"></span><p>目标很简单：全自动化、高质量、无需人工干预</p></div></section>

<section class="not-prose my-16"><div class="mb-8 flex items-center gap-4"><span class="flex h-12 w-12 items-center justify-center rounded-xl bg-zinc-200 text-zinc-700 dark:bg-zinc-700 dark:text-zinc-300"><span class="icon-[tabler--settings-automation] h-6 w-6"></span></span><h2 class="text-xl font-bold text-zinc-900 dark:text-zinc-100 sm:text-2xl">它是如何工作的？</h2></div><p class="mb-10 text-lg leading-relaxed text-zinc-700 dark:text-zinc-300"><p>AIGC Weekly 的核心不仅是一个网站，而是一套<strong class="font-semibold text-zinc-900 dark:text-zinc-100">自主运行的流水线</strong>。每周日早上八点，当大部分人还在休息时，我的 AI Agent 们就开始上班了。</p></p><div class="mt-8 grid gap-6 sm:grid-cols-2"><div class="group relative overflow-hidden rounded-xl border border-zinc-200 bg-white p-8 transition-all hover:border-zinc-400 hover:shadow-md dark:border-zinc-700 dark:bg-zinc-800/50 dark:hover:border-zinc-500"><div class="absolute right-4 top-3 text-5xl font-bold text-zinc-200 dark:text-zinc-700">1</div><div class="relative"><div class="mb-4 flex items-center gap-3"><span class="icon-[tabler--search] h-6 w-6 text-zinc-500"></span><h3 class="text-lg font-bold text-zinc-900 dark:text-zinc-100">自动搜集</h3><span class="text-sm text-zinc-500">Research</span></div><p class="text-base leading-relaxed text-zinc-600 dark:text-zinc-400">Agent 会从 Hacker News、知名 AI 博客、Telegram 频道等 <strong class="text-zinc-900 dark:text-zinc-100">15+</strong> 个信源抓取最新的 AI 资讯。</p></div></div><div class="group relative overflow-hidden rounded-xl border border-zinc-200 bg-white p-8 transition-all hover:border-zinc-400 hover:shadow-md dark:border-zinc-700 dark:bg-zinc-800/50 dark:hover:border-zinc-500"><div class="absolute right-4 top-3 text-5xl font-bold text-zinc-200 dark:text-zinc-700">2</div><div class="relative"><div class="mb-4 flex items-center gap-3"><span class="icon-[tabler--filter] h-6 w-6 text-zinc-500"></span><h3 class="text-lg font-bold text-zinc-900 dark:text-zinc-100">智能筛选</h3><span class="text-sm text-zinc-500">Curate</span></div><p class="text-base leading-relaxed text-zinc-600 dark:text-zinc-400">Agent 会像一个挑剔的主编，根据 GitHub Star 数增长趋势、社区讨论热度等&quot;智能标准&quot;来评估每一条内容的价值。</p></div></div><div class="group relative overflow-hidden rounded-xl border border-zinc-200 bg-white p-8 transition-all hover:border-zinc-400 hover:shadow-md dark:border-zinc-700 dark:bg-zinc-800/50 dark:hover:border-zinc-500"><div class="absolute right-4 top-3 text-5xl font-bold text-zinc-200 dark:text-zinc-700">3</div><div class="relative"><div class="mb-4 flex items-center gap-3"><span class="icon-[tabler--pencil] h-6 w-6 text-zinc-500"></span><h3 class="text-lg font-bold text-zinc-900 dark:text-zinc-100">内容生成</h3><span class="text-sm text-zinc-500">Write</span></div><p class="text-base leading-relaxed text-zinc-600 dark:text-zinc-400">筛选出的素材会被交给负责写作的 Agent。它会撰写标题、摘要，并整理成一篇结构清晰的周刊文章。</p></div></div><div class="group relative overflow-hidden rounded-xl border border-zinc-200 bg-white p-8 transition-all hover:border-zinc-400 hover:shadow-md dark:border-zinc-700 dark:bg-zinc-800/50 dark:hover:border-zinc-500"><div class="absolute right-4 top-3 text-5xl font-bold text-zinc-200 dark:text-zinc-700">4</div><div class="relative"><div class="mb-4 flex items-center gap-3"><span class="icon-[tabler--send] h-6 w-6 text-zinc-500"></span><h3 class="text-lg font-bold text-zinc-900 dark:text-zinc-100">自动发布</h3><span class="text-sm text-zinc-500">Publish</span></div><p class="text-base leading-relaxed text-zinc-600 dark:text-zinc-400">Agent 会通过 MCP (Model Context Protocol) 直接与 CMS 交互，将文章发布到网站上，并同步更新 RSS 源。</p></div></div></div><p class="mt-8 text-center text-base text-zinc-500 dark:text-zinc-500"><p>这一切，都在 Cloudflare 的云端容器里静悄悄地完成。</p></p></section>

<section class="not-prose my-16"><div class="mb-8 flex items-center gap-4"><span class="flex h-12 w-12 items-center justify-center rounded-xl bg-zinc-200 text-zinc-700 dark:bg-zinc-700 dark:text-zinc-300"><span class="icon-[tabler--stack-2] h-6 w-6"></span></span><h2 class="text-xl font-bold text-zinc-900 dark:text-zinc-100 sm:text-2xl">技术栈：Serverless + AI Agent</h2></div><p class="mb-10 text-lg leading-relaxed text-zinc-700 dark:text-zinc-300"><p>作为一个技术人，我也想借此机会实践一下最新的技术栈。AIGC Weekly 是一个典型的 <strong class="font-semibold text-zinc-900 dark:text-zinc-100">Modern Web + AI</strong> 应用：</p></p><div class="mt-8 overflow-hidden rounded-xl border border-zinc-200 dark:border-zinc-700"><table class="w-full text-left text-base"><thead class="bg-zinc-50 dark:bg-zinc-800"><tr><th class="px-6 py-4 font-semibold text-zinc-900 dark:text-zinc-100">组件</th><th class="px-6 py-4 font-semibold text-zinc-900 dark:text-zinc-100">技术选型</th></tr></thead><tbody class="divide-y divide-zinc-200 dark:divide-zinc-700"><tr class="bg-white dark:bg-zinc-800/30"><td class="px-6 py-4"><span class="flex items-center gap-3"><span class="icon-[tabler--layout] h-5 w-5 text-zinc-500"></span><span class="text-zinc-700 dark:text-zinc-300">前端框架</span></span></td><td class="px-6 py-4 font-medium text-zinc-900 dark:text-zinc-100">Next.js 15 (App Router) + OpenNext</td></tr><tr class="bg-white dark:bg-zinc-800/30"><td class="px-6 py-4"><span class="flex items-center gap-3"><span class="icon-[tabler--database] h-5 w-5 text-zinc-500"></span><span class="text-zinc-700 dark:text-zinc-300">内容管理</span></span></td><td class="px-6 py-4 font-medium text-zinc-900 dark:text-zinc-100">Payload CMS 3.0</td></tr><tr class="bg-white dark:bg-zinc-800/30"><td class="px-6 py-4"><span class="flex items-center gap-3"><span class="icon-[tabler--server] h-5 w-5 text-zinc-500"></span><span class="text-zinc-700 dark:text-zinc-300">基础设施</span></span></td><td class="px-6 py-4 font-medium text-zinc-900 dark:text-zinc-100">Cloudflare D1 / R2 / Containers</td></tr><tr class="bg-white dark:bg-zinc-800/30"><td class="px-6 py-4"><span class="flex items-center gap-3"><span class="icon-[tabler--brain] h-5 w-5 text-zinc-500"></span><span class="text-zinc-700 dark:text-zinc-300">AI 核心</span></span></td><td class="px-6 py-4 font-medium text-zinc-900 dark:text-zinc-100">Anthropic Claude Agent SDK</td></tr><tr class="bg-white dark:bg-zinc-800/30"><td class="px-6 py-4"><span class="flex items-center gap-3"><span class="icon-[tabler--world-download] h-5 w-5 text-zinc-500"></span><span class="text-zinc-700 dark:text-zinc-300">数据抓取</span></span></td><td class="px-6 py-4 font-medium text-zinc-900 dark:text-zinc-100">Firecrawl</td></tr></tbody></table></div><div class="mt-8 flex items-start gap-4 rounded-lg bg-zinc-100 p-5 dark:bg-zinc-800"><span class="icon-[tabler--sparkles] mt-0.5 h-6 w-6 shrink-0 text-zinc-600 dark:text-zinc-400"></span><p class="text-base leading-relaxed text-zinc-700 dark:text-zinc-300"><p>这套架构最大的好处是 <strong class="font-semibold text-zinc-900 dark:text-zinc-100">Serverless</strong>。我不需要维护复杂的服务器，系统会根据负载自动伸缩，而且成本极低。</p></p></div></section>

<section class="not-prose my-16"><div class="overflow-hidden rounded-2xl bg-gradient-to-br from-zinc-100 via-white to-zinc-100 p-8 text-center shadow-xl ring-1 ring-zinc-200 dark:from-zinc-800 dark:via-zinc-900 dark:to-zinc-800 dark:ring-zinc-700 sm:p-12"><div class="mb-6 inline-flex items-center gap-2 rounded-full bg-zinc-200/80 px-3 py-1 text-sm font-medium text-zinc-600 dark:bg-zinc-700/80 dark:text-zinc-400"><span class="icon-[tabler--source-code] h-4 w-4"></span><p>开源项目</p></div><h2 class="mb-4 text-2xl font-bold text-zinc-900 dark:text-zinc-100 sm:text-3xl">开始探索 AIGC Weekly</h2><p class="mx-auto mb-10 max-w-xl text-lg leading-relaxed text-zinc-600 dark:text-zinc-400"><p>如果你对 <strong class="text-zinc-900 dark:text-zinc-100">AI Agent 开发</strong>、<strong class="text-zinc-900 dark:text-zinc-100">Next.js 实战</strong> 或者 <strong class="text-zinc-900 dark:text-zinc-100">Serverless 架构</strong> 感兴趣，欢迎来看看！</p></p><div class="mt-4 flex flex-col justify-center gap-4 sm:flex-row"><a href="https://aigc-weekly.agi.li/" target="_blank" rel="nofollow noopener noreferrer" title="访问 AIGC Weekly 周刊" class="group inline-flex items-center justify-center gap-3 rounded-xl bg-zinc-900 px-6 py-4 text-base font-semibold text-white shadow-lg transition-all hover:bg-zinc-800 hover:shadow-xl dark:bg-zinc-100 dark:text-zinc-900 dark:hover:bg-zinc-200"><span class="icon-[tabler--external-link] h-5 w-5 transition-transform group-hover:-translate-y-0.5 group-hover:translate-x-0.5"></span><p>访问周刊</p></a><a href="https://github.com/miantiao-me/aigc-weekly" target="_blank" rel="nofollow noopener noreferrer" title="查看 AIGC Weekly GitHub 仓库" class="group inline-flex items-center justify-center gap-3 rounded-xl border-2 border-zinc-300 bg-white px-6 py-4 text-base font-semibold text-zinc-700 transition-all hover:border-zinc-400 hover:bg-zinc-50 dark:border-zinc-600 dark:bg-zinc-800 dark:text-zinc-300 dark:hover:border-zinc-500 dark:hover:bg-zinc-700"><span class="icon-[tabler--brand-github] h-5 w-5"></span><p>GitHub 仓库</p></a></div></div></section>
<p class="mt-4 text-center text-lg text-zinc-600 dark:text-zinc-400"><p>期待在这个由 AI 驱动的小小角落里，与你相遇。✨</p></p><img src="https://c.statcounter.com/9823304/0/92a1d06c/1/" alt="" width="1" height="1" referrerPolicy="no-referrer-when-downgrade" />]]></content:encoded>
    </item>
    <item>
      <title>hink - 只需 10 行代码的短链接系统</title>
      <link>https://miantiao.me/posts/hink/</link>
      <guid>https://miantiao.me/posts/hink/</guid>
      <description>一个用 Git 和 Serverless 平台实现的极简短链接系统，代码量不到 10 行。</description>
      <pubDate>Sun, 31 Aug 2025 04:50:55 GMT</pubDate>
      <content:encoded><![CDATA[<link rel="preload" as="image" href="https://static.miantiao.me/share/2025/ePMX6q/Zsb50p.png"/><link rel="preload" as="image" href="https://static.miantiao.me/share/2025/JiOSmY/6oDC36.png"/><link rel="preload" as="image" href="https://static.miantiao.me/share/2025/dyzFzs/efOSKl.png"/><link rel="preload" as="image" href="https://github.html.zone/miantiao-me/hink"/><p>和大家分享一个最近做的小工具——<a href="https://github.com/miantiao-me/hink" rel="nofollow noopener noreferrer" target="_blank"><strong>hink</strong></a>，一个用不到 10 行代码实现的短链接系统。</p>
<p>这个工具利用 Git 和 Serverless 平台，实现了短链接生成和访问统计功能。</p>
<h2>核心原理：Git 提交哈希值的妙用</h2>
<p>hink 的核心思路很简单：用 Git 的空提交哈希值作为短链接的唯一标识符，把原始长链接存储在提交信息中。访问短链接时，系统通过 GitHub 的 <code>.patch</code> 文件接口读取提交信息，提取长链接并重定向。结合云平台的 WAF（Web 应用防火墙）分析面板，还能实现访问统计。</p>
<p>目前，这个方案已经在以下平台测试通过：</p>
<ul>
<li>Cloudflare Workers/Snippets + Cloudflare WAF (Pro)</li>
<li>腾讯云 EdgeOne (Free)</li>
<li>阿里云 ESA (Free)</li>
</ul>
<h2>代码：极简实现</h2>
<p>hink 的代码非常短小，核心逻辑只有几行。以下是针对不同平台的实现。</p>
<h3>Cloudflare Workers / 阿里云 ESA 版本</h3>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#2e3440ff;color:#d8dee9ff" tabindex="0" data-language="js" data-theme="nord"><code data-language="js" data-theme="nord" style="display:grid"><span data-line=""><span style="color:#81A1C1">const</span><span style="color:#D8DEE9"> GIT_REPO</span><span style="color:#81A1C1"> =</span><span style="color:#ECEFF4"> &quot;</span><span style="color:#A3BE8C">https://github.com/miantiao-me/hink</span><span style="color:#ECEFF4">&quot;</span></span>
<span data-line=""><span style="color:#81A1C1">export</span><span style="color:#81A1C1"> default</span><span style="color:#ECEFF4"> {</span></span>
<span data-line=""><span style="color:#81A1C1">  async</span><span style="color:#88C0D0"> fetch</span><span style="color:#ECEFF4">(</span><span style="color:#D8DEE9">request</span><span style="color:#ECEFF4">)</span><span style="color:#ECEFF4"> {</span></span>
<span data-line=""><span style="color:#81A1C1">    const</span><span style="color:#ECEFF4"> {</span><span style="color:#D8DEE9"> pathname</span><span style="color:#ECEFF4"> }</span><span style="color:#81A1C1"> =</span><span style="color:#81A1C1"> new</span><span style="color:#88C0D0"> URL</span><span style="color:#D8DEE9FF">(</span><span style="color:#D8DEE9">request</span><span style="color:#ECEFF4">.</span><span style="color:#D8DEE9">url</span><span style="color:#D8DEE9FF">)</span></span>
<span data-line=""><span style="color:#81A1C1">    const</span><span style="color:#D8DEE9"> gitPatch</span><span style="color:#81A1C1"> =</span><span style="color:#ECEFF4"> `</span><span style="color:#81A1C1">${</span><span style="color:#D8DEE9">GIT_REPO</span><span style="color:#81A1C1">}</span><span style="color:#A3BE8C">/commit</span><span style="color:#81A1C1">${</span><span style="color:#D8DEE9">pathname</span><span style="color:#81A1C1">}</span><span style="color:#A3BE8C">.patch</span><span style="color:#ECEFF4">`</span></span>
<span data-line=""><span style="color:#81A1C1">    const</span><span style="color:#D8DEE9"> patch</span><span style="color:#81A1C1"> =</span><span style="color:#81A1C1"> await</span><span style="color:#88C0D0"> fetch</span><span style="color:#D8DEE9FF">(</span><span style="color:#D8DEE9">gitPatch</span><span style="color:#ECEFF4">,</span><span style="color:#ECEFF4"> {</span><span style="color:#88C0D0"> cf</span><span style="color:#ECEFF4">:</span><span style="color:#ECEFF4"> {</span><span style="color:#88C0D0"> cacheEverything</span><span style="color:#ECEFF4">:</span><span style="color:#81A1C1"> true</span><span style="color:#ECEFF4">,</span><span style="color:#88C0D0"> cacheTtlByStatus</span><span style="color:#ECEFF4">:</span><span style="color:#ECEFF4"> {</span><span style="color:#ECEFF4"> &#x27;</span><span style="color:#A3BE8C">200-299</span><span style="color:#ECEFF4">&#x27;</span><span style="color:#ECEFF4">:</span><span style="color:#B48EAD"> 86400</span><span style="color:#ECEFF4"> }</span><span style="color:#ECEFF4"> }}</span><span style="color:#D8DEE9FF">)</span><span style="color:#ECEFF4">.</span><span style="color:#88C0D0">then</span><span style="color:#D8DEE9FF">(</span><span style="color:#D8DEE9">res</span><span style="color:#81A1C1"> =&gt;</span><span style="color:#D8DEE9"> res</span><span style="color:#ECEFF4">.</span><span style="color:#88C0D0">text</span><span style="color:#D8DEE9FF">())</span></span>
<span data-line=""><span style="color:#81A1C1">    const</span><span style="color:#D8DEE9"> url</span><span style="color:#81A1C1"> =</span><span style="color:#D8DEE9"> pathname</span><span style="color:#81A1C1"> ===</span><span style="color:#ECEFF4"> &#x27;</span><span style="color:#A3BE8C">/</span><span style="color:#ECEFF4">&#x27;</span><span style="color:#81A1C1"> ?</span><span style="color:#D8DEE9"> GIT_REPO</span><span style="color:#81A1C1"> :</span><span style="color:#D8DEE9"> patch</span><span style="color:#ECEFF4">.</span><span style="color:#88C0D0">match</span><span style="color:#D8DEE9FF">(</span><span style="color:#ECEFF4">/</span><span style="color:#81A1C1">^</span><span style="color:#EBCB8B">Subject:\s</span><span style="color:#81A1C1">*</span><span style="color:#EBCB8B">\[PATCH\]</span><span style="color:#ECEFF4">(</span><span style="color:#EBCB8B">.</span><span style="color:#81A1C1">*</span><span style="color:#ECEFF4">)</span><span style="color:#81A1C1">$</span><span style="color:#ECEFF4">/</span><span style="color:#81A1C1">m</span><span style="color:#D8DEE9FF">)</span><span style="color:#ECEFF4">?.</span><span style="color:#D8DEE9FF">[</span><span style="color:#B48EAD">1</span><span style="color:#D8DEE9FF">]</span><span style="color:#ECEFF4">?.</span><span style="color:#88C0D0">trim</span><span style="color:#D8DEE9FF">()</span></span>
<span data-line=""><span style="color:#81A1C1">    return</span><span style="color:#D8DEE9"> Response</span><span style="color:#ECEFF4">.</span><span style="color:#88C0D0">redirect</span><span style="color:#D8DEE9FF">(</span><span style="color:#D8DEE9">url</span><span style="color:#81A1C1"> ||</span><span style="color:#D8DEE9"> GIT_REPO</span><span style="color:#D8DEE9FF">)</span></span>
<span data-line=""><span style="color:#ECEFF4">  }</span></span>
<span data-line=""><span style="color:#ECEFF4">}</span></span></code></pre></figure>
<h3>腾讯云 EdgeOne 版本</h3>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#2e3440ff;color:#d8dee9ff" tabindex="0" data-language="js" data-theme="nord"><code data-language="js" data-theme="nord" style="display:grid"><span data-line=""><span style="color:#81A1C1">const</span><span style="color:#D8DEE9"> GIT_REPO</span><span style="color:#81A1C1"> =</span><span style="color:#ECEFF4"> &quot;</span><span style="color:#A3BE8C">https://github.com/miantiao-me/hink</span><span style="color:#ECEFF4">&quot;</span></span>
<span data-line=""><span style="color:#88C0D0">addEventListener</span><span style="color:#D8DEE9FF">(</span><span style="color:#ECEFF4">&quot;</span><span style="color:#A3BE8C">fetch</span><span style="color:#ECEFF4">&quot;</span><span style="color:#ECEFF4">,</span><span style="color:#81A1C1"> async</span><span style="color:#ECEFF4"> (</span><span style="color:#D8DEE9">event</span><span style="color:#ECEFF4">)</span><span style="color:#81A1C1"> =&gt;</span><span style="color:#ECEFF4"> {</span></span>
<span data-line=""><span style="color:#81A1C1">  const</span><span style="color:#ECEFF4"> {</span><span style="color:#D8DEE9"> pathname</span><span style="color:#ECEFF4"> }</span><span style="color:#81A1C1"> =</span><span style="color:#81A1C1"> new</span><span style="color:#88C0D0"> URL</span><span style="color:#D8DEE9FF">(</span><span style="color:#D8DEE9">event</span><span style="color:#ECEFF4">.</span><span style="color:#D8DEE9">request</span><span style="color:#ECEFF4">.</span><span style="color:#D8DEE9">url</span><span style="color:#D8DEE9FF">)</span></span>
<span data-line=""><span style="color:#81A1C1">  const</span><span style="color:#D8DEE9"> gitPatch</span><span style="color:#81A1C1"> =</span><span style="color:#ECEFF4"> `</span><span style="color:#81A1C1">${</span><span style="color:#D8DEE9">GIT_REPO</span><span style="color:#81A1C1">}</span><span style="color:#A3BE8C">/commit</span><span style="color:#81A1C1">${</span><span style="color:#D8DEE9">pathname</span><span style="color:#81A1C1">}</span><span style="color:#A3BE8C">.patch</span><span style="color:#ECEFF4">`</span></span>
<span data-line=""><span style="color:#81A1C1">  const</span><span style="color:#D8DEE9"> patch</span><span style="color:#81A1C1"> =</span><span style="color:#81A1C1"> await</span><span style="color:#88C0D0"> fetch</span><span style="color:#D8DEE9FF">(</span><span style="color:#D8DEE9">gitPatch</span><span style="color:#D8DEE9FF">)</span><span style="color:#ECEFF4">.</span><span style="color:#88C0D0">then</span><span style="color:#D8DEE9FF">(</span><span style="color:#D8DEE9">res</span><span style="color:#81A1C1"> =&gt;</span><span style="color:#D8DEE9"> res</span><span style="color:#ECEFF4">.</span><span style="color:#88C0D0">text</span><span style="color:#D8DEE9FF">())</span></span>
<span data-line=""><span style="color:#81A1C1">  const</span><span style="color:#D8DEE9"> url</span><span style="color:#81A1C1"> =</span><span style="color:#D8DEE9"> pathname</span><span style="color:#81A1C1"> ===</span><span style="color:#ECEFF4"> &#x27;</span><span style="color:#A3BE8C">/</span><span style="color:#ECEFF4">&#x27;</span><span style="color:#81A1C1"> ?</span><span style="color:#D8DEE9"> GIT_REPO</span><span style="color:#81A1C1"> :</span><span style="color:#D8DEE9"> patch</span><span style="color:#ECEFF4">.</span><span style="color:#88C0D0">match</span><span style="color:#D8DEE9FF">(</span><span style="color:#ECEFF4">/</span><span style="color:#81A1C1">^</span><span style="color:#EBCB8B">Subject:\s</span><span style="color:#81A1C1">*</span><span style="color:#EBCB8B">\[PATCH\]</span><span style="color:#ECEFF4">(</span><span style="color:#EBCB8B">.</span><span style="color:#81A1C1">*</span><span style="color:#ECEFF4">)</span><span style="color:#81A1C1">$</span><span style="color:#ECEFF4">/</span><span style="color:#81A1C1">m</span><span style="color:#D8DEE9FF">)</span><span style="color:#ECEFF4">?.</span><span style="color:#D8DEE9FF">[</span><span style="color:#B48EAD">1</span><span style="color:#D8DEE9FF">]</span><span style="color:#ECEFF4">?.</span><span style="color:#88C0D0">trim</span><span style="color:#D8DEE9FF">()</span></span>
<span data-line=""><span style="color:#D8DEE9">  event</span><span style="color:#ECEFF4">.</span><span style="color:#88C0D0">respondWith</span><span style="color:#D8DEE9FF">(</span><span style="color:#81A1C1">new</span><span style="color:#88C0D0"> Response</span><span style="color:#D8DEE9FF">(</span><span style="color:#81A1C1">null</span><span style="color:#ECEFF4">,</span><span style="color:#ECEFF4"> {</span><span style="color:#88C0D0"> status</span><span style="color:#ECEFF4">:</span><span style="color:#B48EAD"> 302</span><span style="color:#ECEFF4">,</span><span style="color:#88C0D0"> headers</span><span style="color:#ECEFF4">:</span><span style="color:#ECEFF4"> {</span><span style="color:#88C0D0"> Location</span><span style="color:#ECEFF4">:</span><span style="color:#D8DEE9"> url</span><span style="color:#81A1C1"> ||</span><span style="color:#D8DEE9"> GIT_REPO</span><span style="color:#ECEFF4"> }</span><span style="color:#ECEFF4"> }</span><span style="color:#D8DEE9FF">))</span></span>
<span data-line=""><span style="color:#ECEFF4">}</span><span style="color:#D8DEE9FF">)</span><span style="color:#81A1C1">;</span></span></code></pre></figure>
<p>部署到 Serverless 平台后，绑定一个域名，就能拥有自己的短链接服务。</p>
<h2>为什么做 hink？</h2>
<p>短链接服务并不新鲜，市面上有很多工具。但 hink 的目标是探索一种极简且有趣的实现方式。利用 Git 提交哈希值，避免了数据库管理，借助 GitHub 实现存储，而云平台的 WAF 功能让访问统计变得简单。这对我来说是一次技术实验，用最少的代码解决实际问题。</p>
<h2>演示效果：三大平台运行情况</h2>
<p>我在 Cloudflare Workers、阿里云 ESA 和腾讯云 EdgeOne 上部署了 hink，并通过 WAF 面板查看访问统计。以下是各平台的运行截图：</p>
<h3>Cloudflare Workers</h3>
<p><img src="https://static.miantiao.me/share/2025/ePMX6q/Zsb50p.png" alt="Cloudflare"/></p>
<h3>阿里云 ESA</h3>
<p><img src="https://static.miantiao.me/share/2025/JiOSmY/6oDC36.png" alt="Alibaba"/></p>
<h3>腾讯云 EdgeOne</h3>
<p><img src="https://static.miantiao.me/share/2025/dyzFzs/efOSKl.png" alt="Tencent"/></p>
<h2>项目地址</h2>
<p><a href="https://github.com/miantiao-me/hink" rel="nofollow noopener noreferrer" target="_blank"><img src="https://github.html.zone/miantiao-me/hink" alt="miantiao-me/hink - GitHub"/></a></p><img src="https://c.statcounter.com/9823304/0/92a1d06c/1/" alt="" width="1" height="1" referrerPolicy="no-referrer-when-downgrade" />]]></content:encoded>
    </item>
    <item>
      <title>解密 SSH AI Chat：它是如何运行的</title>
      <link>https://miantiao.me/posts/how-ssh-ai-chat-works/</link>
      <guid>https://miantiao.me/posts/how-ssh-ai-chat-works/</guid>
      <description>深入探索 SSH AI Chat 的技术架构与实现原理，了解如何通过 SSH 协议构建创新的 AI 聊天应用，以及终端 UI、认证系统和流式响应等核心技术的巧妙设计。</description>
      <pubDate>Fri, 01 Aug 2025 14:07:03 GMT</pubDate>
      <content:encoded><![CDATA[<div class="not-prose"><div class="relative overflow-hidden bg-gradient-to-br from-zinc-50 via-zinc-100 to-zinc-200 dark:from-zinc-900 dark:via-zinc-800 dark:to-zinc-700 rounded-2xl p-8 md:p-12 lg:p-16 mb-12"><div class="absolute inset-0 bg-grid-zinc-100 dark:bg-grid-zinc-800 opacity-50"></div><div class="relative z-10"><div class="flex items-center gap-3 mb-6"><div class="p-3 bg-zinc-500/10 dark:bg-zinc-400/10 rounded-xl"><i class="icon-[tabler--terminal-2] w-6 h-6 text-zinc-600 dark:text-zinc-400 block"></i></div><span class="px-1 py-1 text-lg font-medium bg-zinc-100 dark:bg-zinc-800/50 text-zinc-700 dark:text-zinc-300 rounded-full"><p>技术深度解析</p></span></div><h2 class="text-2xl md:text-3xl md:leading-9 lg:text-4xl lg:leading-10 font-bold text-zinc-900 dark:text-zinc-100 mb-6 leading-tight"><p>解密 SSH AI Chat
<span class="text-zinc-600 dark:text-zinc-400 mt-2">它是如何运行的</span></p></h2><p class="text-base md:text-lg md:leading-7 text-zinc-600 dark:text-zinc-300 max-w-3xl leading-relaxed"><p>探索一个令人惊叹的创新项目：通过 SSH 协议直接与 AI 对话。无需安装客户端，无需打开浏览器，
只需一条简单的 SSH 命令，就能开启与 AI 的终端对话之旅。</p></p><div class="mt-8 p-4 bg-zinc-900 dark:bg-zinc-800 rounded-lg border border-zinc-200 dark:border-zinc-700"><code class="text-zinc-300 font-mono text-sm md:text-base"><p>$ ssh username<span>@</span>chat.agi.li</p></code></div></div></div></div>
<p>大家好，我是面条，今天想和大家分享我最近的一个项目 —— SSH AI Chat。</p>
<h2>项目简介</h2>
<div class="not-prose mb-8"><div class="bg-gradient-to-r from-zinc-50 to-zinc-100 dark:from-zinc-800/50 dark:to-zinc-700/50 rounded-xl p-6 border border-zinc-200 dark:border-zinc-700"><div class="flex items-start gap-4"><div class="p-2 bg-zinc-500/10 dark:bg-zinc-400/10 rounded-lg"><i class="icon-[tabler--rocket] w-5 h-5 text-zinc-600 dark:text-zinc-400 block"></i></div><div><h3 class="text-lg font-semibold text-zinc-900 dark:text-zinc-100 mb-2">核心理念</h3><p class="text-zinc-700 dark:text-zinc-300"><p>SSH AI Chat 是一个可以通过 SSH 直接连接的 AI 聊天应用。使用方式非常简单，
你不需要安装任何客户端，不需要打开浏览器，只需要一个 SSH 客户端，就能和 AI 进行对话。</p></p></div></div></div></div>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#2e3440ff;color:#d8dee9ff" tabindex="0" data-language="bash" data-theme="nord"><code data-language="bash" data-theme="nord" style="display:grid"><span data-line=""><span style="color:#88C0D0">ssh</span><span style="color:#A3BE8C"> username@chat.agi.li</span></span></code></pre></figure>
<p>没错，就这么简单！你不需要安装任何客户端，不需要打开浏览器，只需要一个 SSH 客户端，就能和 AI 进行对话。</p>
<p>作为一个对 TUI 应用有着浓厚兴趣的开发者，我一直觉得在终端里聊天是一件很酷的事情。其实我最初是被 itter.sh 这个网站惊艳到了 - 居然能用 SSH 访问社交网络！这让我意识到，原来 SSH 不只是用来连服务器的，还可以做很多有趣的事情。</p>
<p>于是就有了这个想法：如果能用 SSH 和 AI 聊天，那该多酷啊！不需要安装任何软件，不需要打开浏览器，只要在终端里输入 <code>ssh yourname@chat.agi.li</code> 就能开始对话。</p>
<h2>项目架构</h2>
<div class="not-prose mb-8"><div class="grid md:grid-cols-2 gap-6"><div class="bg-white dark:bg-zinc-800 rounded-xl p-6 border border-zinc-200 dark:border-zinc-700 shadow-xs"><div class="flex items-center gap-3 mb-4"><i class="icon-[tabler--stack-2] w-5 h-5 text-zinc-600 dark:text-zinc-400 block"></i><h3 class="text-lg font-semibold text-zinc-900 dark:text-zinc-100">核心技术栈</h3></div><ul class="space-y-2 text-sm text-zinc-600 dark:text-zinc-300"><li class="flex items-center gap-2"><i class="icon-[tabler--server] w-4 h-4 text-zinc-500 block"></i><span><strong>SSH 服务器</strong>：Node.js + ssh2 模块</span></li><li class="flex items-center gap-2"><i class="icon-[tabler--brand-react] w-4 h-4 text-zinc-500 block"></i><span><strong>UI 框架</strong>：React + Ink（终端渲染）</span></li><li class="flex items-center gap-2"><i class="icon-[tabler--database] w-4 h-4 text-zinc-500 block"></i><span><strong>数据库</strong>：PostgreSQL / PGLite</span></li><li class="flex items-center gap-2"><i class="icon-[tabler--refresh] w-4 h-4 text-zinc-500 block"></i><span><strong>缓存</strong>：Redis / ioredis-mock</span></li><li class="flex items-center gap-2"><i class="icon-[tabler--brain] w-4 h-4 text-zinc-500 block"></i><span><strong>AI 集成</strong>：Vercel AI SDK</span></li></ul></div><div class="bg-gradient-to-br from-zinc-50 to-zinc-100 dark:from-zinc-800/50 dark:to-zinc-700/50 rounded-xl p-6 border border-zinc-200 dark:border-zinc-700"><div class="flex items-center gap-3 mb-4"><i class="icon-[tabler--topology-star-3] w-5 h-5 text-zinc-600 dark:text-zinc-400 block"></i><h3 class="text-lg font-semibold text-zinc-900 dark:text-zinc-100">架构特点</h3></div><ul class="space-y-2 text-sm text-zinc-700 dark:text-zinc-300"><li>• 无客户端安装需求</li><li>• 跨平台终端支持</li><li>• GitHub 公钥认证</li><li>• 实时流式响应</li><li>• 多模型 AI 支持</li></ul></div></div></div>
<h3>系统架构图</h3>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#2e3440ff;color:#d8dee9ff" tabindex="0" data-language="txt" data-theme="nord"><code data-language="txt" data-theme="nord" style="display:grid"><span data-line=""><span>┌─────────────────┐    ┌─────────────────┐    ┌─────────────────┐</span></span>
<span data-line=""><span>│   SSH Client    │    │   SSH Server    │    │   React App     │</span></span>
<span data-line=""><span>│                 │    │                 │    │                 │</span></span>
<span data-line=""><span>│  ssh username@  │───▶│  Node.js +      │───▶│  Ink UI +       │</span></span>
<span data-line=""><span>│  chat.agi.li  │    │  ssh2           │    │  React Hooks    │</span></span>
<span data-line=""><span>└─────────────────┘    └─────────────────┘    └─────────────────┘</span></span>
<span data-line=""><span>                               │</span></span>
<span data-line=""><span>                               ▼</span></span>
<span data-line=""><span>                       ┌─────────────────┐</span></span>
<span data-line=""><span>                       │   AI Services   │</span></span>
<span data-line=""><span>                       │                 │</span></span>
<span data-line=""><span>                       │  OpenAI API     │</span></span>
<span data-line=""><span>                       │  Gemini API     │</span></span>
<span data-line=""><span>                       │  DeepSeek API   │</span></span>
<span data-line=""><span>                       └─────────────────┘</span></span></code></pre></figure>
<h2>核心模块解析</h2>
<div class="not-prose mb-8"><div class="grid gap-6"><div class="bg-white dark:bg-zinc-800 rounded-xl border border-zinc-200 dark:border-zinc-700 overflow-hidden"><div class="bg-gradient-to-r from-zinc-600 to-zinc-700 p-4"><h3 class="text-lg font-semibold text-white flex items-center gap-2"><i class="icon-[tabler--server-2] w-5 h-5 block"></i><span>1. SSH 服务器模块</span></h3></div><div class="p-6"><p class="text-zinc-600 dark:text-zinc-300"><p>这是整个应用的核心，负责处理 SSH 连接和认证。系统会自动处理密钥验证、GitHub 公钥认证、登录限制和速率限制。</p></p></div></div><div class="bg-white dark:bg-zinc-800 rounded-xl border border-zinc-200 dark:border-zinc-700 overflow-hidden"><div class="bg-gradient-to-r from-zinc-600 to-zinc-700 p-4"><h3 class="text-lg font-semibold text-white flex items-center gap-2"><i class="icon-[tabler--shield-check] w-5 h-5 block"></i><span>2. 认证系统</span></h3></div><div class="p-6"><p class="text-zinc-600 dark:text-zinc-300 mb-4"><span>最巧妙的设计是使用 GitHub 公钥认证。用户不需要注册，直接使用 GitHub SSH 密钥就能登录。</span></p><div class="bg-zinc-50 dark:bg-zinc-800/50 rounded-lg p-4 border border-zinc-200 dark:border-zinc-700"><p class="text-zinc-700 dark:text-zinc-300 text-sm"><i class="icon-[tabler--info-circle] w-5 h-5 mr-2 inline-block"></i><span>系统会获取用户的 GitHub 公钥进行验证，每 6 小时缓存一次，既安全又高效。</span></p></div></div></div><div class="bg-white dark:bg-zinc-800 rounded-xl border border-zinc-200 dark:border-zinc-700 overflow-hidden"><div class="bg-gradient-to-r from-zinc-600 to-zinc-700 p-4"><h3 class="text-lg font-semibold text-white flex items-center gap-2"><i class="icon-[tabler--terminal] w-5 h-5 block"></i><span>3. 终端 UI 系统</span></h3></div><div class="p-6"><p class="text-zinc-600 dark:text-zinc-300 mb-4"><span>使用 Ink 框架在终端中渲染 React 组件。想象一下，你平时写的 React 组件，现在不是在浏览器里渲染，而是在终端里显示！</span></p><div class="grid grid-cols-2 md:grid-cols-4 gap-3"><div class="text-center p-3 bg-zinc-50 dark:bg-zinc-800/50 rounded-lg"><i class="icon-[tabler--language] w-5 h-5 text-zinc-600 dark:text-zinc-400 mb-1 mx-auto block"></i><p class="text-xs text-zinc-700 dark:text-zinc-300">多语言界面</p></div><div class="text-center p-3 bg-zinc-50 dark:bg-zinc-800/50 rounded-lg"><i class="icon-[tabler--message-circle] w-5 h-5 text-zinc-600 dark:text-zinc-400 mb-1 mx-auto block"></i><p class="text-xs text-zinc-700 dark:text-zinc-300">实时聊天</p></div><div class="text-center p-3 bg-zinc-50 dark:bg-zinc-800/50 rounded-lg"><i class="icon-[tabler--history] w-5 h-5 text-zinc-600 dark:text-zinc-400 mb-1 mx-auto block"></i><p class="text-xs text-zinc-700 dark:text-zinc-300">历史记录</p></div><div class="text-center p-3 bg-zinc-50 dark:bg-zinc-800/50 rounded-lg"><i class="icon-[tabler--device-mobile] w-5 h-5 text-zinc-600 dark:text-zinc-400 mb-1 mx-auto block"></i><p class="text-xs text-zinc-700 dark:text-zinc-300">响应式布局</p></div></div></div></div><div class="bg-white dark:bg-zinc-800 rounded-xl border border-zinc-200 dark:border-zinc-700 overflow-hidden"><div class="bg-gradient-to-r from-zinc-600 to-zinc-700 p-4"><h3 class="text-lg font-semibold text-white flex items-center gap-2"><i class="icon-[tabler--message-chatbot] w-5 h-5 block"></i><span>4. 聊天系统</span></h3></div><div class="p-6"><p class="text-zinc-600 dark:text-zinc-300 mb-4"><span>使用 Vercel AI SDK 处理 AI 对话。当你在终端里输入消息时，系统会接收消息、加载历史对话、选择模型、实时显示流式响应，并保存对话记录。</span></p><div class="flex flex-wrap gap-2"><span class="px-3 py-1 bg-zinc-100 dark:bg-zinc-800/50 text-zinc-700 dark:text-zinc-300 rounded-full text-sm">流式响应</span><span class="px-3 py-1 bg-zinc-100 dark:bg-zinc-800/50 text-zinc-700 dark:text-zinc-300 rounded-full text-sm">多模型支持</span><span class="px-3 py-1 bg-zinc-100 dark:bg-zinc-800/50 text-zinc-700 dark:text-zinc-300 rounded-full text-sm">思维链展示</span><span class="px-3 py-1 bg-zinc-100 dark:bg-zinc-800/50 text-zinc-700 dark:text-zinc-300 rounded-full text-sm">对话历史管理</span></div></div></div></div></div>
<h2>技术难点和解决方案</h2>
<div class="not-prose mb-8"><div class="relative bg-gradient-to-br from-zinc-50 via-zinc-100 to-zinc-200 dark:from-zinc-800 dark:via-zinc-700 dark:to-zinc-600 rounded-2xl p-8 border border-zinc-200/50 dark:border-zinc-600/50 shadow-lg hover:shadow-xl transition-all duration-300"><div class="absolute inset-0 bg-gradient-to-br from-white/30 to-transparent rounded-2xl"></div><div class="relative"><div class="flex items-center gap-4 mb-6"><div class="p-3 bg-gradient-to-br from-zinc-500 to-zinc-600 rounded-xl shadow-lg"><i class="icon-[tabler--alert-triangle] w-6 h-6 text-white block"></i></div><div><h3 class="text-2xl font-bold text-zinc-900 dark:text-zinc-100"><span>核心技术挑战</span></h3><p class="text-zinc-600 dark:text-zinc-400 text-sm mt-1">项目开发过程中遇到的关键技术难点</p></div></div><div class="grid grid-cols-1 md:grid-cols-2 gap-4"><div class="p-4 bg-white/70 dark:bg-zinc-800/70 backdrop-blur-xs rounded-xl border border-zinc-200/50 dark:border-zinc-600/50"><div class="flex items-center gap-2 mb-2"><div class="w-2 h-2 bg-zinc-500 rounded-full"></div><span class="text-sm font-semibold text-zinc-800 dark:text-zinc-200">终端渲染复杂性</span></div><p class="text-xs text-zinc-600 dark:text-zinc-400">React 组件在终端环境的适配</p></div><div class="p-4 bg-white/70 dark:bg-zinc-800/70 backdrop-blur-xs rounded-xl border border-zinc-200/50 dark:border-zinc-600/50"><div class="flex items-center gap-2 mb-2"><div class="w-2 h-2 bg-zinc-600 rounded-full"></div><span class="text-sm font-semibold text-zinc-800 dark:text-zinc-200">会话状态管理</span></div><p class="text-xs text-zinc-600 dark:text-zinc-400">多用户并发连接的处理</p></div></div></div></div></div>
<h3>1. 终端渲染的挑战</h3>
<p>最大的挑战是在终端中实现复杂的 UI 界面。使用 Ink 框架将 React 组件渲染到终端，实现虚拟 PTY 来处理终端 I/O。在终端里显示 AI 回复的 Markdown 需要用专门的 worker 进程处理转换，才能显示粗体、斜体、代码块。</p>
<h3>2. SSH 会话管理</h3>
<p>管理多个 SSH 会话和状态需要为每个连接创建独立的 React 应用实例，使用 Context API 管理全局状态，并实现会话生命周期管理。</p>
<h3>3. 实时流式响应</h3>
<p>AI 的回复是流式的，如果每收到一个字节就刷新界面，终端会直接卡死。使用 Vercel AI SDK 的 streamText 配合节流更新，每 300ms 更新一次，既流畅又不卡。这是性能优化的关键技术点。</p>
<h3>4. 数据存储的灵活性</h3>
<p>项目支持 PostgreSQL 和 PGLite 两种数据库，以及 Redis 和内存缓存，让项目既能独立运行又能部署到生产环境。</p>
<h2>有趣的设计细节</h2>
<div class="not-prose mb-8"><div class="grid md:grid-cols-2 gap-6"><div class="bg-white dark:bg-zinc-800 rounded-xl p-6 border border-zinc-200 dark:border-zinc-700 hover:shadow-lg transition-shadow"><div class="flex items-center gap-3 mb-4"><div class="p-2 bg-zinc-500/10 rounded-lg"><i class="icon-[tabler--brand-github] w-5 h-5 text-zinc-900 dark:text-zinc-100 block"></i></div><h3 class="text-lg font-semibold text-zinc-900 dark:text-zinc-100"><span>GitHub 认证</span></h3></div><p class="text-zinc-600 dark:text-zinc-300 text-sm mb-3"><span>最酷的设计！用户不需要注册，直接使用 GitHub SSH 密钥就能登录，既方便又安全。</span></p><div class="flex items-center gap-2 text-xs text-zinc-600 dark:text-zinc-400"><i class="icon-[tabler--check] w-4 h-4 inline-block"></i><span>零注册流程</span></div></div><div class="bg-white dark:bg-zinc-800 rounded-xl p-6 border border-zinc-200 dark:border-zinc-700 hover:shadow-lg transition-shadow"><div class="flex items-center gap-3 mb-4"><div class="p-2 bg-zinc-500/10 rounded-lg"><i class="icon-[tabler--cpu] w-5 h-5 text-zinc-600 dark:text-zinc-400 block"></i></div><h3 class="text-lg font-semibold text-zinc-900 dark:text-zinc-100"><span>多模型支持</span></h3></div><p class="text-zinc-600 dark:text-zinc-300 text-sm mb-3"><span>支持 DeepSeek-V3/DeepSeek-R1、Gemini-2.5-Flash/Gemini-2.5-Pro 等多个 AI 模型，包括思维链展示。</span></p><div class="flex flex-wrap gap-1"><span class="px-2 py-1 bg-zinc-100 dark:bg-zinc-800/50 text-zinc-700 dark:text-zinc-300 rounded text-xs">DeepSeek</span><span class="px-2 py-1 bg-zinc-100 dark:bg-zinc-800/50 text-zinc-700 dark:text-zinc-300 rounded text-xs">Gemini</span></div></div><div class="bg-white dark:bg-zinc-800 rounded-xl p-6 border border-zinc-200 dark:border-zinc-700 hover:shadow-lg transition-shadow"><div class="flex items-center gap-3 mb-4"><div class="p-2 bg-zinc-500/10 rounded-lg"><i class="icon-[tabler--world] w-5 h-5 text-zinc-600 dark:text-zinc-400 block"></i></div><h3 class="text-lg font-semibold text-zinc-900 dark:text-zinc-100"><span>国际化支持</span></h3></div><p class="text-zinc-600 dark:text-zinc-300 text-sm mb-3"><span>完整的 i18n 支持，通过 <code>LANG</code> 环境变量自动检测用户语言偏好，支持中英文切换。</span></p><div class="flex items-center gap-2 text-xs text-zinc-600 dark:text-zinc-400"><i class="icon-[tabler--language] w-4 h-4 inline-block"></i><span>自动语言检测</span></div></div><div class="bg-white dark:bg-zinc-800 rounded-xl p-6 border border-zinc-200 dark:border-zinc-700 hover:shadow-lg transition-shadow"><div class="flex items-center gap-3 mb-4"><div class="p-2 bg-zinc-500/10 rounded-lg"><i class="icon-[tabler--keyboard] w-5 h-5 text-zinc-600 dark:text-zinc-400 block"></i></div><h3 class="text-lg font-semibold text-zinc-900 dark:text-zinc-100"><span>键盘快捷键</span></h3></div><p class="text-zinc-600 dark:text-zinc-300 text-sm mb-3"><span><code>Ctrl+C</code> 退出应用，<code>N</code> 新建对话，<code>I</code> 聚焦输入框，<code>?</code> 查看帮助。还有一些小彩蛋功能。</span></p><div class="grid grid-cols-2 gap-2 text-xs"><div class="flex items-center gap-1"><kbd class="px-1.5 py-0.5 bg-zinc-100 dark:bg-zinc-700 rounded text-zinc-600 dark:text-zinc-300 font-mono text-xs">Ctrl+C</kbd><span class="text-zinc-500 dark:text-zinc-400">退出</span></div><div class="flex items-center gap-1"><kbd class="px-1.5 py-0.5 bg-zinc-100 dark:bg-zinc-700 rounded text-zinc-600 dark:text-zinc-300 font-mono text-xs">N</kbd><span class="text-zinc-500 dark:text-zinc-400">新建</span></div></div></div></div></div>
<h2>开发心得</h2>
<div class="not-prose mb-8"><div class="relative bg-gradient-to-br from-zinc-50 via-zinc-100 to-zinc-200 dark:from-zinc-800 dark:via-zinc-700 dark:to-zinc-600 rounded-2xl p-8 border border-zinc-200/50 dark:border-zinc-600/50 shadow-lg hover:shadow-xl transition-all duration-300"><div class="absolute inset-0 bg-gradient-to-br from-white/30 to-transparent rounded-2xl"></div><div class="relative"><div class="flex items-center gap-4 mb-6"><div class="p-3 bg-gradient-to-br from-zinc-500 to-zinc-600 rounded-xl shadow-lg"><i class="icon-[tabler--bulb] w-6 h-6 text-white block"></i></div><div><h3 class="text-2xl font-bold text-zinc-900 dark:text-zinc-100"><span>技术感悟与收获</span></h3><p class="text-zinc-600 dark:text-zinc-400 text-sm mt-1">开发过程中的深度思考与技术洞察</p></div></div><div class="grid grid-cols-1 md:grid-cols-3 gap-4"><div class="p-4 bg-white/70 dark:bg-zinc-800/70 backdrop-blur-xs rounded-xl border border-zinc-200/50 dark:border-zinc-600/50"><div class="flex items-center gap-2 mb-2"><div class="w-2 h-2 bg-zinc-500 rounded-full"></div><span class="text-sm font-semibold text-zinc-800 dark:text-zinc-200">终端应用潜力</span></div><p class="text-xs text-zinc-600 dark:text-zinc-400">现代化终端交互的可能性</p></div><div class="p-4 bg-white/70 dark:bg-zinc-800/70 backdrop-blur-xs rounded-xl border border-zinc-200/50 dark:border-zinc-600/50"><div class="flex items-center gap-2 mb-2"><div class="w-2 h-2 bg-zinc-600 rounded-full"></div><span class="text-sm font-semibold text-zinc-800 dark:text-zinc-200">SSH 协议创新</span></div><p class="text-xs text-zinc-600 dark:text-zinc-400">跨平台应用的新思路</p></div><div class="p-4 bg-white/70 dark:bg-zinc-800/70 backdrop-blur-xs rounded-xl border border-zinc-200/50 dark:border-zinc-600/50"><div class="flex items-center gap-2 mb-2"><div class="w-2 h-2 bg-zinc-700 rounded-full"></div><span class="text-sm font-semibold text-zinc-800 dark:text-zinc-200">技术栈融合</span></div><p class="text-xs text-zinc-600 dark:text-zinc-400">现代前端技术的适应性</p></div></div></div></div></div>
<h3>1. 终端应用的可能性</h3>
<p>这个项目让我看到了终端应用的巨大潜力。通过 Ink 框架，我们可以在终端中实现复杂的交互界面。</p>
<h3>2. SSH 的妙用</h3>
<p>SSH 不仅仅是一个远程管理工具，它还是一个强大的应用平台。通过 SSH，我们可以实现跨平台的客户端应用，用户不需要安装任何额外的软件。</p>
<h3>3. 现代化的技术栈</h3>
<p>虽然这是一个终端应用，但我们使用了最现代的技术栈：React、TypeScript、Vercel AI SDK 等。这证明了终端应用也可以很&quot;现代化&quot;。</p>
<h2>未来展望</h2>
<div class="not-prose mb-8"><div class="bg-gradient-to-br from-zinc-50 to-zinc-100 dark:from-zinc-800/50 dark:to-zinc-700/50 rounded-xl p-6 border border-zinc-200 dark:border-zinc-700"><div class="flex items-center gap-3 mb-4"><i class="icon-[tabler--telescope] w-6 h-6 text-zinc-600 dark:text-zinc-400 block"></i><h3 class="text-xl font-bold text-zinc-900 dark:text-zinc-100"><span>发展规划</span></h3></div><div class="grid md:grid-cols-2 gap-4"><div class="flex items-start gap-3"><i class="icon-[tabler--server-bolt] w-5 h-5 text-zinc-600 dark:text-zinc-400 mt-1 block"></i><div><h4 class="font-semibold text-zinc-900 dark:text-zinc-100 mb-1"><span>本地模型支持</span></h4><p class="text-zinc-700 dark:text-zinc-300 text-sm">计划支持 Ollama 等本地模型</p></div></div><div class="flex items-start gap-3"><i class="icon-[tabler--plug-connected] w-5 h-5 text-zinc-600 dark:text-zinc-400 mt-1 block"></i><div><h4 class="font-semibold text-zinc-900 dark:text-zinc-100 mb-1"><span>MCP 协议</span></h4><p class="text-zinc-700 dark:text-zinc-300 text-sm">支持 Model Context Protocol 插件扩展</p></div></div></div></div></div>
<p>计划支持更多的 AI 模型，包括 Ollama 等本地模型，以及支持 MCP (Model Context Protocol) 协议让用户可以通过插件扩展功能。</p>
<h2>总结</h2>
<div class="not-prose mb-8"><div class="bg-gradient-to-br from-zinc-50 to-zinc-100 dark:from-zinc-800 dark:to-zinc-700 rounded-xl p-8 border border-zinc-200 dark:border-zinc-700"><div class="text-center mb-6"><i class="icon-[tabler--sparkles] w-8 h-8 text-zinc-600 dark:text-zinc-400 mb-3 mx-auto block"></i><h3 class="text-2xl font-bold text-zinc-900 dark:text-zinc-100 mb-4"><span>项目价值与意义</span></h3></div><div class="grid md:grid-cols-2 gap-6"><div class="space-y-3"><div class="flex items-center gap-3"><i class="icon-[tabler--circle-check] w-5 h-5 text-zinc-500 inline-block"></i><span class="text-zinc-700 dark:text-zinc-300">终端应用的现代化可能性</span></div><div class="flex items-center gap-3"><i class="icon-[tabler--circle-check] w-5 h-5 text-zinc-500 inline-block"></i><span class="text-zinc-700 dark:text-zinc-300">SSH 协议的灵活应用</span></div></div><div class="space-y-3"><div class="flex items-center gap-3"><i class="icon-[tabler--circle-check] w-5 h-5 text-zinc-500 inline-block"></i><span class="text-zinc-700 dark:text-zinc-300">React 在不同平台上的适应性</span></div><div class="flex items-center gap-3"><i class="icon-[tabler--circle-check] w-5 h-5 text-zinc-500 inline-block"></i><span class="text-zinc-700 dark:text-zinc-300">AI 技术的普及化</span></div></div></div></div></div>
<p>SSH AI Chat 是一个融合了多种技术的创新项目。它展示了：</p>
<ul>
<li>终端应用的现代化可能性</li>
<li>SSH 协议的灵活应用</li>
<li>React 在不同平台上的适应性</li>
<li>AI 技术的普及化</li>
</ul>
<p>这个项目让我意识到，技术不只是为了解决问题，也可以很有趣。把 SSH 和 AI 结合起来，创造出了意想不到的体验。</p>
<p>希望这个项目能给大家带来一些启发，让我们一起探索技术的边界！</p>
<h2>体验地址</h2>
<div class="not-prose mb-8"><div class="bg-gradient-to-r from-zinc-600 to-zinc-700 rounded-xl p-6 text-white"><div class="flex items-center gap-4 mb-4"><div class="p-3 bg-white/20 rounded-xl"><i class="icon-[tabler--terminal-2] w-6 h-6 block"></i></div><div><h3 class="text-lg font-bold mb-1">立即体验</h3><p class="text-zinc-100 text-sm">一条命令即可开始</p></div></div><div class="bg-black/20 rounded-lg p-4 mb-4"><code class="text-zinc-200 font-mono text-sm"><p>ssh your-github-username<span>@</span>chat.agi.li</p></code></div><div class="flex items-center gap-4"><a href="https://github.com/miantiao-me/ssh-ai-chat" target="_blank" rel="nofollow" title="SSH AI Chat GitHub 仓库" class="inline-flex items-center gap-2 px-3 py-2 bg-white/20 hover:bg-white/30 rounded-lg transition-colors text-sm"><i class="icon-[tabler--brand-github] w-4 h-4 block"></i><span>GitHub</span></a></div></div></div>
<div class="not-prose"><div class="text-center py-8 border-t border-zinc-200 dark:border-zinc-700"><p class="text-zinc-600 dark:text-zinc-400 italic"><p>如果你对这个项目有任何问题或建议，欢迎在 GitHub 上讨论。也欢迎 Star 这个项目，你的支持是我继续开发的动力！</p></p></div></div><img src="https://c.statcounter.com/9823304/0/92a1d06c/1/" alt="" width="1" height="1" referrerPolicy="no-referrer-when-downgrade" />]]></content:encoded>
    </item>
    <item>
      <title>在 Docker 沙箱中运行 MCP Server</title>
      <link>https://miantiao.me/posts/guide-to-running-mcp-server-in-a-sandbox/</link>
      <guid>https://miantiao.me/posts/guide-to-running-mcp-server-in-a-sandbox/</guid>
      <description>利用 Docker 运行 MCP Server 并转换为 MCP SSE 协议，减少 npx 和 uvx 直接运行带来的任意文件读取风险。</description>
      <pubDate>Wed, 23 Apr 2025 14:01:48 GMT</pubDate>
      <content:encoded><![CDATA[<link rel="preload" as="image" href="https://static.miantiao.me/share/ZUy0MY/twitter.jpeg"/><link rel="preload" as="image" href="https://github.html.zone/TBXark/mcp-proxy"/><link rel="preload" as="image" href="https://static.miantiao.me/share/g4KUgP/claw.jpg"/><link rel="preload" as="image" href="https://static.miantiao.me/share/mI3zIh/fetch.jpg"/><link rel="preload" as="image" href="https://static.miantiao.me/share/t43O9e/chatwise.jpg"/><p>MCP 是今年 AI 开发行业很热门的一个协议，但是由于它的 C/S 架构，导致使用者必须在本地运行 MCP Server。</p>
<p>MCP Server 常见的运行方式有 npx(NPM 生态)、uvx(Python 生态)、Docker 等 stdio 方式和 HTTP(SSE/Streaming) 方式。但是 npx 和 uvx 运行命令有着极大的风险。如果不慎执行恶意软件包，可能会导致隐私数据泄露带来极大的安全风险。具体可以看 Invariant 的 <a href="https://invariantlabs.ai/blog/mcp-security-notification-tool-poisoning-attacks" rel="nofollow noopener noreferrer" target="_blank">MCP Security Notification: Tool Poisoning Attacks</a> 这篇文章。</p>
<p>作为一个软件行业从业者，对安全的关注度极高。让 ChatGPT 整理了一下最近 5 年的 NPM 和 PyPI 供应链攻击事件，让人不寒而栗。</p>
<table><thead><tr><th><strong>时间</strong></th><th><strong>事件</strong></th><th><strong>概要与影响范围</strong></th></tr></thead><tbody><tr><td><strong>2021年2月</strong></td><td><strong>“依赖混淆”漏洞披露</strong></td><td>安全研究员 Alex Birsan 利用<strong>依赖混淆</strong>（Dependency Confusion）技术，在 NPM/PyPI 上传与多家企业内部库同名的软件包，成功入侵了包括苹果、微软等35家大厂内部服务器 (<a href="https://www.sonatype.com/blog/pypi-flooded-with-over-1200-dependency-confusion-packages#:~:text=Dependency%20confusion%3A%20Year%20in%20review" rel="nofollow noopener noreferrer" target="_blank">PyPI flooded with 1,275 dependency confusion packages</a>)。这一演示引发业内对供应链风险的高度关注。</td></tr><tr><td><strong>2021年10月</strong></td><td><strong>UAParser.js 库遭劫持</strong></td><td>NPM上每周下载量超700万的流行库 <em>ua-parser-js</em> 被攻击者通过维护者账户入侵发布恶意版本 (<a href="https://www.sonatype.com/resources/vulnerability-timeline#:~:text=%23%23%20%20Popular%20%22ua,Attacked" rel="nofollow noopener noreferrer" target="_blank">A Timeline of SSC Attacks, Curated by Sonatype</a>)。受感染版本在安装时植入<strong>密码窃取木马</strong>和<strong>加密货币挖矿程序</strong>，波及大量开发者系统。</td></tr><tr><td><strong>2021年10月</strong></td><td><strong>假冒 Roblox 库投毒</strong></td><td>攻击者在 NPM 上传多个假冒 Roblox API 的软件包（如 <em>noblox.js-proxy</em>），内含混淆的恶意代码，安装后会植入<strong>木马和勒索软件</strong>等Payload (<a href="https://www.sonatype.com/resources/vulnerability-timeline#:~:text=,and%20has%20a%20Spooky%20Surprise" rel="nofollow noopener noreferrer" target="_blank">A Timeline of SSC Attacks, Curated by Sonatype</a>)。这些包下载数千次，显示出攻击者通过<strong>typosquatting</strong>手法诱骗游戏开发者。</td></tr><tr><td><strong>2021年11月</strong></td><td><strong>COA 与 RC 库连续劫持</strong></td><td>NPM上热门库 <em>coa</em>（每周下载数百万）和 <em>rc</em>（每周1400万下载）相继被入侵发布恶意版本。受害版本执行与 UAParser.js 案例类似的<strong>凭证窃取木马</strong>，一度导致全球众多使用 React 等框架的项目构建管线中断 (<a href="https://www.sonatype.com/resources/vulnerability-timeline#:~:text=,js" rel="nofollow noopener noreferrer" target="_blank">A Timeline of SSC Attacks, Curated by Sonatype</a>) (<a href="https://www.sonatype.com/resources/vulnerability-timeline#:~:text=,Is%20Hijacked%2C%20Too" rel="nofollow noopener noreferrer" target="_blank">A Timeline of SSC Attacks, Curated by Sonatype</a>)。官方调查认定原因均为维护者账户被盗用。</td></tr><tr><td><strong>2022年1月</strong></td><td><strong>Colors/Faker 开源库“自杀”</strong></td><td>著名的颜色格式库 <em>colors.js</em> 和测试数据生成库 <em>faker.js</em> 的作者出于抗议，在最新版本中注入无限循环等破坏性代码，导致包括Meta（Facebook）和亚马逊等公司在内的数千项目崩溃 (<a href="https://www.sonatype.com/resources/vulnerability-timeline#:~:text=Thousands%20of%20open%20source%20projects,companies%20exploiting%20open%20source" rel="nofollow noopener noreferrer" target="_blank">A Timeline of SSC Attacks, Curated by Sonatype</a>)（虽非外部攻击，但属于供应链投毒范畴）。</td></tr><tr><td><strong>2022年1月</strong></td><td><strong>PyPI 1,275个恶意包集中投放</strong></td><td>一名用户在1月23日一天内疯狂向 PyPI 发布了 <strong>1,275 个恶意软件包</strong> (<a href="https://www.sonatype.com/resources/vulnerability-timeline#:~:text=,Than%201%2C200%20Dependency%20Confusion%20Packages" rel="nofollow noopener noreferrer" target="_blank">A Timeline of SSC Attacks, Curated by Sonatype</a>)。这些包大多冒用知名项目或公司的名字（如 <em>xcryptography</em>、<em>Sagepay</em> 等），安装后收集主机名、IP等指纹信息并通过 DNS/HTTP 回传给攻击者 (<a href="https://www.sonatype.com/blog/pypi-flooded-with-over-1200-dependency-confusion-packages#:~:text=The%20,of%20these%20components%20are%20installed" rel="nofollow noopener noreferrer" target="_blank">PyPI flooded with 1,275 dependency confusion packages</a>) (<a href="https://www.sonatype.com/blog/pypi-flooded-with-over-1200-dependency-confusion-packages#:~:text=For%20DNS%3A%20.sub.deliverycontent,online" rel="nofollow noopener noreferrer" target="_blank">PyPI flooded with 1,275 dependency confusion packages</a>)。PyPI 管理员在收到报告后一小时内即下架了所有相关包 (<a href="https://www.sonatype.com/blog/pypi-flooded-with-over-1200-dependency-confusion-packages#:~:text=All%20of%20the%201%2C275%20were,an%20hour%20of%20our%20report" rel="nofollow noopener noreferrer" target="_blank">PyPI flooded with 1,275 dependency confusion packages</a>)。</td></tr><tr><td><strong>2022年3月</strong></td><td><strong>Node-ipc “抗议软件”事件</strong></td><td>前端构建常用库 <em>node-ipc</em> 的作者在 v10.1.1–10.1.3 版本中加入恶意代码：检测到客户端 IP 属于俄罗斯或白俄罗斯时，就<strong>擦除文件系统</strong>、用爱心表情覆盖文件 (<a href="https://www.zdnet.com/article/corrupted-open-source-software-enters-the-russian-battlefield/#:~:text=To%20be%20exact%2C%20Miller%20added,annoying%20to%20a%20system%20destroyer" rel="nofollow noopener noreferrer" target="_blank">Corrupted open-source software enters the Russian battlefield | ZDNET</a>) (<a href="https://www.zdnet.com/article/corrupted-open-source-software-enters-the-russian-battlefield/#:~:text=According%20to%20developer%20security%20company,8%2C%20critical" rel="nofollow noopener noreferrer" target="_blank">Corrupted open-source software enters the Russian battlefield | ZDNET</a>)。该库被 Vue CLI 等广泛依赖，导致大量用户系统遭破坏，并被赋予 CVE-2022-23812（CVSS 9.8） (<a href="https://www.zdnet.com/article/corrupted-open-source-software-enters-the-russian-battlefield/#:~:text=According%20to%20developer%20security%20company,8%2C%20critical" rel="nofollow noopener noreferrer" target="_blank">Corrupted open-source software enters the Russian battlefield | ZDNET</a>)。</td></tr><tr><td><strong>2022年10月</strong></td><td><strong>LofyGang 大规模投毒活动</strong></td><td>安全公司发现一个名为“LofyGang”的团伙在 NPM 上分发了将近 <strong>200 个恶意包</strong> (<a href="https://thehackernews.com/2022/10/lofygang-distributed-200-malicious-npm.html#:~:text=Multiple%20campaigns%20that%20distributed%20trojanized,single%20threat%20actor%20dubbed%20LofyGang" rel="nofollow noopener noreferrer" target="_blank">LofyGang Distributed ~200 Malicious NPM Packages to Steal Credit Card Data</a>)。这些包通过<strong>typosquatting</strong>和伪装常用库名称植入<strong>木马</strong>，窃取开发者的信用卡信息、Discord 账户以及游戏服务登录凭据，累计安装次数达数千次 (<a href="https://thehackernews.com/2022/10/lofygang-distributed-200-malicious-npm.html#:~:text=Multiple%20campaigns%20that%20distributed%20trojanized,single%20threat%20actor%20dubbed%20LofyGang" rel="nofollow noopener noreferrer" target="_blank">LofyGang Distributed ~200 Malicious NPM Packages to Steal Credit Card Data</a>)。这是一起持续一年多的有组织网络犯罪活动。</td></tr><tr><td><strong>2022年12月</strong></td><td><strong>PyTorch-nightly 依赖链攻击</strong></td><td>知名深度学习框架 PyTorch 披露其夜间版在 12月25–30日间遭遇<strong>依赖混淆式</strong>供应链攻击 (<a href="https://www.wiz.io/blog/malicious-pytorch-dependency-torchtriton-on-pypi-everything-you-need-to-know#:~:text=means%20that%20anyone%20who%20downloaded,and%20rotate%20any%20discovered%20keys" rel="nofollow noopener noreferrer" target="_blank">Malicious PyTorch dependency &#x27;torchtriton&#x27; on PyPI | Wiz Blog</a>)：攻击者在 PyPI 上注册了名为 <em>torchtriton</em> 的恶意包，与 PyTorch 夜ly 版所需的私有依赖同名，导致数千名通过 pip 安装 nightly 版的用户中招 (<a href="https://www.wiz.io/blog/malicious-pytorch-dependency-torchtriton-on-pypi-everything-you-need-to-know#:~:text=means%20that%20anyone%20who%20downloaded,and%20rotate%20any%20discovered%20keys" rel="nofollow noopener noreferrer" target="_blank">Malicious PyTorch dependency &#x27;torchtriton&#x27; on PyPI | Wiz Blog</a>)。恶意 <em>torchtriton</em> 包运行后收集系统上的环境变量和秘钥并上传至攻击者服务器，危及用户的云凭证安全。PyTorch 官方紧急发布警告并替换了该命名空间 (<a href="https://www.wiz.io/blog/malicious-pytorch-dependency-torchtriton-on-pypi-everything-you-need-to-know#:~:text=The%20creator%20of%20the%20copied,were%20stored%20on%20impacted%20resources" rel="nofollow noopener noreferrer" target="_blank">Malicious PyTorch dependency &#x27;torchtriton&#x27; on PyPI | Wiz Blog</a>)。</td></tr><tr><td><strong>2023年3月</strong></td><td><strong>“W4SP Stealer” 木马泛滥 PyPI</strong></td><td>安全研究员陆续发现 PyPI 上出现大量携带 <strong>W4SP Stealer</strong> 信息窃取木马的恶意包 (<a href="https://thehackernews.com/2022/12/w4sp-stealer-discovered-in-multiple.html#:~:text=Threat%20actors%20have%20published%20yet,malware%20on%20compromised%20developer%20machines" rel="nofollow noopener noreferrer" target="_blank">W4SP Stealer Discovered in Multiple PyPI Packages Under Various Names</a>)。这些木马别名众多（如 ANGEL Stealer、PURE Stealer 等），但本质均为 W4SP 家族，专门窃取用户密码、加密货币钱包和 Discord 令牌等信息 (<a href="https://thehackernews.com/2022/12/w4sp-stealer-discovered-in-multiple.html#:~:text=Interestingly%2C%20while%20the%20malware%20goes,be%20copies%20of%20W4SP%20Stealer" rel="nofollow noopener noreferrer" target="_blank">W4SP Stealer Discovered in Multiple PyPI Packages Under Various Names</a>)。一次报告就揭示了16个此类恶意包（如 <em>modulesecurity</em>、<em>easycordey</em> 等） (<a href="https://thehackernews.com/2022/12/w4sp-stealer-discovered-in-multiple.html#:~:text=The%2016%20rogue%20modules%20are,nowsys%2C%20upamonkws%2C%20captchaboy%2C%20and%20proxybooster" rel="nofollow noopener noreferrer" target="_blank">W4SP Stealer Discovered in Multiple PyPI Packages Under Various Names</a>)。PyPI 针对此类木马展开清理，并加强了上传检测。</td></tr><tr><td><strong>2023年8月</strong></td><td><strong>Lazarus 组织攻击 PyPI</strong></td><td>ReversingLabs 报告称朝鲜黑客组织 Lazarus 的分支在 PyPI 发布了逾两打（24个以上）伪装热门库的恶意包（代号“VMConnect”行动） (<a href="https://www.reversinglabs.com/blog/a-partial-history-of-software-supply-chain-attacks#:~:text=" rel="nofollow noopener noreferrer" target="_blank">Software Supply Chain Attacks: A (partial) History</a>)。这些包企图针对特定行业（如金融）用户，植入远程访问木马。据称该攻击与此前针对 NuGet 的类似活动相关联，显示出国家级黑客对开源供应链的兴趣。</td></tr><tr><td><strong>2024年及以后</strong></td><td><strong>持续的供应链威胁</strong></td><td>2024年以来，NPM 与 PyPI 上仍不断爆出新的投毒事件。例如2024年初发现假冒VS Code相关NPM包内含远控间谍软件 (<a href="https://www.sonatype.com/resources/vulnerability-timeline#:~:text=,altered%20ScreenConnect%20utility%20as%20spyware" rel="nofollow noopener noreferrer" target="_blank">A Timeline of SSC Attacks, Curated by Sonatype</a>)、假冒Solana库窃取加密钱包密钥的PyPI包 (<a href="https://www.sonatype.com/resources/vulnerability-timeline#:~:text=%23%23%20%20Ideal%20typosquat%20%27solana,steals%20your%20crypto%20wallet%20keys" rel="nofollow noopener noreferrer" target="_blank">A Timeline of SSC Attacks, Curated by Sonatype</a>)等。这表明供应链攻击已成常态化威胁，需要生态系统持续提高警惕和防御能力。</td></tr></tbody></table>
<p>发 Twitter 吐槽了一下，结果吐槽的时候就看到一个推友遇到了一起供应链攻击事件。</p>
<p><a href="https://x.com/tcdwww/status/1914202659210359108" rel="nofollow noopener noreferrer" target="_blank"><img src="https://static.miantiao.me/share/ZUy0MY/twitter.jpeg" alt="Twitter"/></a></p>
<p>所幸 <a href="https://x.com/TBXark" rel="nofollow noopener noreferrer" target="_blank">@TBXark</a> 推荐了他的 <strong>MCP Proxy</strong> 项目，可以很方便的将 MCP Server 运行在 Docker 中。他最初的目的是把 MCP Server 运行在服务器上，减少客户端压力和方便移动端调用。 然而由于 Docker 天然的隔离特性，与我期望有沙箱的诉求不谋而合。</p>
<p>MCP Proxy 会在 Docker 中运行 MCP Servers 并转换为 MCP SSE 的协议，这样用户就可以在 MCP 客户端中全部走 SSE 协议调用，这样可以大大减小 npx 和 uvx 直接运行带来的任意文件读取风险。<em>如果部署在境外服务器, 还可以顺带解决网络的问题</em>。</p>
<p>但是当前还是可以读取到 <code>/config/config.json</code> 这个 MCP Proxy 的配置文件, 风险可控。同时也给开发者提了需求， config 文件配置 400 权限， npx 和 uvx 命令使用 nobody 用户运行。如果可以实现，将完美解决任意文件读取的问题。</p>
<h2>运行 MCP Proxy</h2>
<p><a href="https://github.com/TBXark/mcp-proxy" rel="nofollow noopener noreferrer" target="_blank"><img src="https://github.html.zone/TBXark/mcp-proxy" alt="MCP Proxy"/></a></p>
<p>如果你自己有 VPS 部署了 Docker, 可以使用下面的命令运行 MCP Proxy。</p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#2e3440ff;color:#d8dee9ff" tabindex="0" data-language="bash" data-theme="nord"><code data-language="bash" data-theme="nord" style="display:grid"><span data-line=""><span style="color:#88C0D0">docker</span><span style="color:#A3BE8C"> run</span><span style="color:#A3BE8C"> -d</span><span style="color:#A3BE8C"> -p</span><span style="color:#A3BE8C"> 9090:9090</span><span style="color:#A3BE8C"> -v</span><span style="color:#A3BE8C"> /path/to/config.json:/config/config.json</span><span style="color:#A3BE8C"> ghcr.io/tbxark/mcp-proxy:latest</span></span></code></pre></figure>
<p>如果你没有自己的 VPS, 可以使用 <a href="https://404.li/claw" rel="nofollow noopener noreferrer" target="_blank"><strong>claw.cloud</strong></a> 提供的免费容器服务（每个月 $5 额度, GitHub 注册需满 180 天）。</p>
<p>由于 Claw 有容器大小的限制，我们需要使用下面的环境变量，配置 npx 和 uvx 的缓存目录，防止容器崩溃。</p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#2e3440ff;color:#d8dee9ff" tabindex="0" data-language="txt" data-theme="nord"><code data-language="txt" data-theme="nord" style="display:grid"><span data-line=""><span>UV_CACHE_DIR=/cache/uv</span></span>
<span data-line=""><span>npm_config_cache=/cache/npm</span></span></code></pre></figure>
<p>同时在 <code>/cache</code> 路径下挂载 10G 的存储。 配置参考我的配置： 0.5c CPU, 512M 内存, 10G 硬盘。</p>
<p>最终的配置如下：</p>
<p><img src="https://static.miantiao.me/share/g4KUgP/claw.jpg" alt="Claw"/></p>
<h2>配置 MCP Proxy</h2>
<p>MCP Proxy 的配置文件需要挂载在 <code>/config/config.json</code> 路径下，完整配置请参考 <a href="https://github.com/TBXark/mcp-proxy?tab=readme-ov-file#configurationonfiguration" rel="nofollow noopener noreferrer" target="_blank">https://github.com/TBXark/mcp-proxy?tab=readme-ov-file#configurationonfiguration</a>。</p>
<p>以下是我的配置，可以参考。</p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#2e3440ff;color:#d8dee9ff" tabindex="0" data-language="json" data-theme="nord"><code data-language="json" data-theme="nord" style="display:grid"><span data-line=""><span style="color:#ECEFF4">{</span></span>
<span data-line=""><span style="color:#ECEFF4">    &quot;</span><span style="color:#8FBCBB">mcpProxy</span><span style="color:#ECEFF4">&quot;</span><span style="color:#ECEFF4">:</span><span style="color:#ECEFF4"> {</span></span>
<span data-line=""><span style="color:#ECEFF4">        &quot;</span><span style="color:#8FBCBB">baseURL</span><span style="color:#ECEFF4">&quot;</span><span style="color:#ECEFF4">:</span><span style="color:#ECEFF4"> &quot;</span><span style="color:#A3BE8C">https://mcp.miantiao.me</span><span style="color:#ECEFF4">&quot;</span><span style="color:#ECEFF4">,</span></span>
<span data-line=""><span style="color:#ECEFF4">        &quot;</span><span style="color:#8FBCBB">addr</span><span style="color:#ECEFF4">&quot;</span><span style="color:#ECEFF4">:</span><span style="color:#ECEFF4"> &quot;</span><span style="color:#A3BE8C">:9090</span><span style="color:#ECEFF4">&quot;</span><span style="color:#ECEFF4">,</span></span>
<span data-line=""><span style="color:#ECEFF4">        &quot;</span><span style="color:#8FBCBB">name</span><span style="color:#ECEFF4">&quot;</span><span style="color:#ECEFF4">:</span><span style="color:#ECEFF4"> &quot;</span><span style="color:#A3BE8C">MCP Proxy</span><span style="color:#ECEFF4">&quot;</span><span style="color:#ECEFF4">,</span></span>
<span data-line=""><span style="color:#ECEFF4">        &quot;</span><span style="color:#8FBCBB">version</span><span style="color:#ECEFF4">&quot;</span><span style="color:#ECEFF4">:</span><span style="color:#ECEFF4"> &quot;</span><span style="color:#A3BE8C">1.0.0</span><span style="color:#ECEFF4">&quot;</span><span style="color:#ECEFF4">,</span></span>
<span data-line=""><span style="color:#ECEFF4">        &quot;</span><span style="color:#8FBCBB">options</span><span style="color:#ECEFF4">&quot;</span><span style="color:#ECEFF4">:</span><span style="color:#ECEFF4"> {</span></span>
<span data-line=""><span style="color:#ECEFF4">          &quot;</span><span style="color:#8FBCBB">panicIfInvalid</span><span style="color:#ECEFF4">&quot;</span><span style="color:#ECEFF4">:</span><span style="color:#81A1C1"> false</span><span style="color:#ECEFF4">,</span></span>
<span data-line=""><span style="color:#ECEFF4">          &quot;</span><span style="color:#8FBCBB">logEnabled</span><span style="color:#ECEFF4">&quot;</span><span style="color:#ECEFF4">:</span><span style="color:#81A1C1"> true</span><span style="color:#ECEFF4">,</span></span>
<span data-line=""><span style="color:#ECEFF4">          &quot;</span><span style="color:#8FBCBB">authTokens</span><span style="color:#ECEFF4">&quot;</span><span style="color:#ECEFF4">:</span><span style="color:#ECEFF4"> [</span></span>
<span data-line=""><span style="color:#ECEFF4">            &quot;</span><span style="color:#A3BE8C">miantiao.me</span><span style="color:#ECEFF4">&quot;</span></span>
<span data-line=""><span style="color:#ECEFF4">          ]</span></span>
<span data-line=""><span style="color:#ECEFF4">        }</span></span>
<span data-line=""><span style="color:#ECEFF4">    },</span></span>
<span data-line=""><span style="color:#ECEFF4">    &quot;</span><span style="color:#8FBCBB">mcpServers</span><span style="color:#ECEFF4">&quot;</span><span style="color:#ECEFF4">:</span><span style="color:#ECEFF4"> {</span></span>
<span data-line=""><span style="color:#ECEFF4">        &quot;</span><span style="color:#8FBCBB">github</span><span style="color:#ECEFF4">&quot;</span><span style="color:#ECEFF4">:</span><span style="color:#ECEFF4"> {</span></span>
<span data-line=""><span style="color:#ECEFF4">            &quot;</span><span style="color:#8FBCBB">command</span><span style="color:#ECEFF4">&quot;</span><span style="color:#ECEFF4">:</span><span style="color:#ECEFF4"> &quot;</span><span style="color:#A3BE8C">npx</span><span style="color:#ECEFF4">&quot;</span><span style="color:#ECEFF4">,</span></span>
<span data-line=""><span style="color:#ECEFF4">            &quot;</span><span style="color:#8FBCBB">args</span><span style="color:#ECEFF4">&quot;</span><span style="color:#ECEFF4">:</span><span style="color:#ECEFF4"> [</span></span>
<span data-line=""><span style="color:#ECEFF4">                &quot;</span><span style="color:#A3BE8C">-y</span><span style="color:#ECEFF4">&quot;</span><span style="color:#ECEFF4">,</span></span>
<span data-line=""><span style="color:#ECEFF4">                &quot;</span><span style="color:#A3BE8C">@modelcontextprotocol/server-github</span><span style="color:#ECEFF4">&quot;</span></span>
<span data-line=""><span style="color:#ECEFF4">            ],</span></span>
<span data-line=""><span style="color:#ECEFF4">            &quot;</span><span style="color:#8FBCBB">env</span><span style="color:#ECEFF4">&quot;</span><span style="color:#ECEFF4">:</span><span style="color:#ECEFF4"> {</span></span>
<span data-line=""><span style="color:#ECEFF4">                &quot;</span><span style="color:#8FBCBB">GITHUB_PERSONAL_ACCESS_TOKEN</span><span style="color:#ECEFF4">&quot;</span><span style="color:#ECEFF4">:</span><span style="color:#ECEFF4"> &quot;</span><span style="color:#A3BE8C">&lt;YOUR_TOKEN&gt;</span><span style="color:#ECEFF4">&quot;</span></span>
<span data-line=""><span style="color:#ECEFF4">            }</span></span>
<span data-line=""><span style="color:#ECEFF4">        },</span></span>
<span data-line=""><span style="color:#ECEFF4">        &quot;</span><span style="color:#8FBCBB">fetch</span><span style="color:#ECEFF4">&quot;</span><span style="color:#ECEFF4">:</span><span style="color:#ECEFF4"> {</span></span>
<span data-line=""><span style="color:#ECEFF4">            &quot;</span><span style="color:#8FBCBB">command</span><span style="color:#ECEFF4">&quot;</span><span style="color:#ECEFF4">:</span><span style="color:#ECEFF4"> &quot;</span><span style="color:#A3BE8C">uvx</span><span style="color:#ECEFF4">&quot;</span><span style="color:#ECEFF4">,</span></span>
<span data-line=""><span style="color:#ECEFF4">            &quot;</span><span style="color:#8FBCBB">args</span><span style="color:#ECEFF4">&quot;</span><span style="color:#ECEFF4">:</span><span style="color:#ECEFF4"> [</span></span>
<span data-line=""><span style="color:#ECEFF4">                &quot;</span><span style="color:#A3BE8C">mcp-server-fetch</span><span style="color:#ECEFF4">&quot;</span></span>
<span data-line=""><span style="color:#ECEFF4">            ]</span></span>
<span data-line=""><span style="color:#ECEFF4">        },</span></span>
<span data-line=""><span style="color:#ECEFF4">        &quot;</span><span style="color:#8FBCBB">amap</span><span style="color:#ECEFF4">&quot;</span><span style="color:#ECEFF4">:</span><span style="color:#ECEFF4"> {</span></span>
<span data-line=""><span style="color:#ECEFF4">            &quot;</span><span style="color:#8FBCBB">url</span><span style="color:#ECEFF4">&quot;</span><span style="color:#ECEFF4">:</span><span style="color:#ECEFF4"> &quot;</span><span style="color:#A3BE8C">https://mcp.amap.com/sse?key=&lt;YOUR_TOKEN&gt;</span><span style="color:#ECEFF4">&quot;</span></span>
<span data-line=""><span style="color:#ECEFF4">        }</span></span>
<span data-line=""><span style="color:#ECEFF4">    }</span></span>
<span data-line=""><span style="color:#ECEFF4">}</span></span></code></pre></figure>
<h2>调用 MCP proxy</h2>
<p>以 <a href="https://404.li/chatwise" rel="nofollow noopener noreferrer" target="_blank"><strong>ChatWise</strong></a> 调用 fetch 为例，直接配置 SSE 协议即可。</p>
<p><img src="https://static.miantiao.me/share/mI3zIh/fetch.jpg" alt="fetch"/></p>
<p>是不是很简单，等 <a href="https://404.li/chatwise" rel="nofollow noopener noreferrer" target="_blank"><strong>ChatWise</strong></a> 出了移动端这样调用也是完全可用的。</p>
<p><img src="https://static.miantiao.me/share/t43O9e/chatwise.jpg" alt="ChatWise"/></p><img src="https://c.statcounter.com/9823304/0/92a1d06c/1/" alt="" width="1" height="1" referrerPolicy="no-referrer-when-downgrade" />]]></content:encoded>
    </item>
    <item>
      <title>使用 Cloudflare Workers 合并音频文件</title>
      <link>https://miantiao.me/posts/cloudflare-audio-concat/</link>
      <guid>https://miantiao.me/posts/cloudflare-audio-concat/</guid>
      <description>使用 Cloudflare Workers 和 Browser Rendering 将多个音频文件拼接成一个音频文件</description>
      <pubDate>Sat, 19 Apr 2025 11:09:12 GMT</pubDate>
      <content:encoded><![CDATA[<p>最近把 <a href="https://hacker-news.agi.li/" rel="nofollow noopener noreferrer" target="_blank">Hacker News 中文播客</a> 改成了双人对话的形式，由于目前的语音合成模型还不能很好地处理双人对话，所以需要把每个人的音频文件拼接起来。</p>
<p>由于项目之前运行在 Cloudflare Workflow 的 Worker Runtime, 众所周知 Worker Runtime 缺少不少 Node.JS 特性，无法调用 C++ 扩展。而且 Cloudflare Container 还没有正式上线，所以只能使用 Browser Rendering 来实现。</p>
<p>合并音频文件一般都使用 FFMpeg 来做，现在 FFMpeg 也可以通过 WASM 在浏览器内运行了。所以大体的技术方案是：</p>
<ol>
<li>使用 Worker Binding 来启动浏览器实例</li>
<li>浏览器打开音频合并页面，合成语音文件，返回 Blob</li>
<li>将 Blob 返回给 Worker 后存入 R2</li>
</ol>
<p>整体代码量不多，但是由于 Browser Rendering 只能远程调用，调试比较麻烦。</p>
<p>最终实现代码：</p>
<h3>浏览器内音频合并代码</h3>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#2e3440ff;color:#d8dee9ff" tabindex="0" data-language="html" data-theme="nord"><code data-language="html" data-theme="nord" style="display:grid"><span data-line=""><span style="color:#81A1C1">&lt;!doctype</span><span style="color:#8FBCBB"> html</span><span style="color:#81A1C1">&gt;</span></span>
<span data-line=""><span style="color:#81A1C1">&lt;html</span><span style="color:#8FBCBB"> lang</span><span style="color:#ECEFF4">=</span><span style="color:#ECEFF4">&quot;</span><span style="color:#A3BE8C">en</span><span style="color:#ECEFF4">&quot;</span><span style="color:#81A1C1">&gt;</span></span>
<span data-line=""><span style="color:#81A1C1">  &lt;head&gt;</span></span>
<span data-line=""><span style="color:#81A1C1">    &lt;meta</span><span style="color:#8FBCBB"> charset</span><span style="color:#ECEFF4">=</span><span style="color:#ECEFF4">&quot;</span><span style="color:#A3BE8C">UTF-8</span><span style="color:#ECEFF4">&quot;</span><span style="color:#81A1C1"> /&gt;</span></span>
<span data-line=""><span style="color:#81A1C1">    &lt;meta</span><span style="color:#8FBCBB"> name</span><span style="color:#ECEFF4">=</span><span style="color:#ECEFF4">&quot;</span><span style="color:#A3BE8C">viewport</span><span style="color:#ECEFF4">&quot;</span><span style="color:#8FBCBB"> content</span><span style="color:#ECEFF4">=</span><span style="color:#ECEFF4">&quot;</span><span style="color:#A3BE8C">width=device-width, initial-scale=1.0</span><span style="color:#ECEFF4">&quot;</span><span style="color:#81A1C1"> /&gt;</span></span>
<span data-line=""><span style="color:#81A1C1">    &lt;title&gt;</span><span style="color:#D8DEE9FF">Audio</span><span style="color:#81A1C1">&lt;/title&gt;</span></span>
<span data-line=""><span style="color:#81A1C1">  &lt;/head&gt;</span></span>
<span data-line=""><span style="color:#81A1C1">  &lt;body&gt;</span></span>
<span data-line=""><span style="color:#81A1C1">    &lt;script&gt;</span></span>
<span data-line=""><span style="color:#81A1C1">      const</span><span style="color:#88C0D0"> concatAudioFilesOnBrowser</span><span style="color:#81A1C1"> =</span><span style="color:#81A1C1"> async</span><span style="color:#ECEFF4"> (</span><span style="color:#D8DEE9">audioFiles</span><span style="color:#ECEFF4">)</span><span style="color:#81A1C1"> =&gt;</span><span style="color:#ECEFF4"> {</span></span>
<span data-line=""><span style="color:#81A1C1">        const</span><span style="color:#D8DEE9"> script</span><span style="color:#81A1C1"> =</span><span style="color:#D8DEE9"> document</span><span style="color:#ECEFF4">.</span><span style="color:#88C0D0">createElement</span><span style="color:#D8DEE9FF">(</span><span style="color:#ECEFF4">&#x27;</span><span style="color:#A3BE8C">script</span><span style="color:#ECEFF4">&#x27;</span><span style="color:#D8DEE9FF">)</span></span>
<span data-line=""><span style="color:#D8DEE9">        script</span><span style="color:#ECEFF4">.</span><span style="color:#D8DEE9">src</span><span style="color:#81A1C1"> =</span><span style="color:#ECEFF4"> &#x27;</span><span style="color:#A3BE8C">https://unpkg.com/@ffmpeg/ffmpeg@0.11.6/dist/ffmpeg.min.js</span><span style="color:#ECEFF4">&#x27;</span></span>
<span data-line=""><span style="color:#D8DEE9">        document</span><span style="color:#ECEFF4">.</span><span style="color:#D8DEE9">head</span><span style="color:#ECEFF4">.</span><span style="color:#88C0D0">appendChild</span><span style="color:#D8DEE9FF">(</span><span style="color:#D8DEE9">script</span><span style="color:#D8DEE9FF">)</span></span>
<span data-line=""><span style="color:#81A1C1">        await</span><span style="color:#81A1C1"> new</span><span style="color:#8FBCBB"> Promise</span><span style="color:#D8DEE9FF">(</span><span style="color:#ECEFF4">(</span><span style="color:#D8DEE9">resolve</span><span style="color:#ECEFF4">)</span><span style="color:#81A1C1"> =&gt;</span><span style="color:#D8DEE9FF"> (</span><span style="color:#D8DEE9">script</span><span style="color:#ECEFF4">.</span><span style="color:#D8DEE9">onload</span><span style="color:#81A1C1"> =</span><span style="color:#D8DEE9"> resolve</span><span style="color:#D8DEE9FF">))</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#81A1C1">        const</span><span style="color:#ECEFF4"> {</span><span style="color:#D8DEE9"> createFFmpeg</span><span style="color:#ECEFF4">,</span><span style="color:#D8DEE9"> fetchFile</span><span style="color:#ECEFF4"> }</span><span style="color:#81A1C1"> =</span><span style="color:#D8DEE9"> FFmpeg</span></span>
<span data-line=""><span style="color:#81A1C1">        const</span><span style="color:#D8DEE9"> ffmpeg</span><span style="color:#81A1C1"> =</span><span style="color:#88C0D0"> createFFmpeg</span><span style="color:#D8DEE9FF">(</span><span style="color:#ECEFF4">{</span><span style="color:#88C0D0"> log</span><span style="color:#ECEFF4">:</span><span style="color:#81A1C1"> true</span><span style="color:#ECEFF4"> }</span><span style="color:#D8DEE9FF">)</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#81A1C1">        await</span><span style="color:#D8DEE9"> ffmpeg</span><span style="color:#ECEFF4">.</span><span style="color:#88C0D0">load</span><span style="color:#D8DEE9FF">()</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#616E88">        // Download and write each file to FFmpeg&#x27;s virtual file system</span></span>
<span data-line=""><span style="color:#81A1C1">        for</span><span style="color:#D8DEE9FF"> (</span><span style="color:#81A1C1">const</span><span style="color:#ECEFF4"> [</span><span style="color:#D8DEE9">index</span><span style="color:#ECEFF4">,</span><span style="color:#D8DEE9"> audioFile</span><span style="color:#ECEFF4">]</span><span style="color:#81A1C1"> of</span><span style="color:#D8DEE9"> audioFiles</span><span style="color:#ECEFF4">.</span><span style="color:#88C0D0">entries</span><span style="color:#D8DEE9FF">()) </span><span style="color:#ECEFF4">{</span></span>
<span data-line=""><span style="color:#81A1C1">          const</span><span style="color:#D8DEE9"> audioData</span><span style="color:#81A1C1"> =</span><span style="color:#81A1C1"> await</span><span style="color:#88C0D0"> fetchFile</span><span style="color:#D8DEE9FF">(</span><span style="color:#D8DEE9">audioFile</span><span style="color:#D8DEE9FF">)</span></span>
<span data-line=""><span style="color:#D8DEE9">          ffmpeg</span><span style="color:#ECEFF4">.</span><span style="color:#88C0D0">FS</span><span style="color:#D8DEE9FF">(</span><span style="color:#ECEFF4">&#x27;</span><span style="color:#A3BE8C">writeFile</span><span style="color:#ECEFF4">&#x27;</span><span style="color:#ECEFF4">,</span><span style="color:#ECEFF4"> `</span><span style="color:#A3BE8C">input</span><span style="color:#81A1C1">${</span><span style="color:#D8DEE9">index</span><span style="color:#81A1C1">}</span><span style="color:#A3BE8C">.mp3</span><span style="color:#ECEFF4">`</span><span style="color:#ECEFF4">,</span><span style="color:#D8DEE9"> audioData</span><span style="color:#D8DEE9FF">)</span></span>
<span data-line=""><span style="color:#ECEFF4">        }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#616E88">        // Create a file list for ffmpeg concat</span></span>
<span data-line=""><span style="color:#81A1C1">        const</span><span style="color:#D8DEE9"> fileList</span><span style="color:#81A1C1"> =</span><span style="color:#D8DEE9"> audioFiles</span><span style="color:#ECEFF4">.</span><span style="color:#88C0D0">map</span><span style="color:#D8DEE9FF">(</span><span style="color:#ECEFF4">(</span><span style="color:#D8DEE9">_</span><span style="color:#ECEFF4">,</span><span style="color:#D8DEE9"> i</span><span style="color:#ECEFF4">)</span><span style="color:#81A1C1"> =&gt;</span><span style="color:#ECEFF4"> `</span><span style="color:#A3BE8C">file &#x27;input</span><span style="color:#81A1C1">${</span><span style="color:#D8DEE9">i</span><span style="color:#81A1C1">}</span><span style="color:#A3BE8C">.mp3&#x27;</span><span style="color:#ECEFF4">`</span><span style="color:#D8DEE9FF">)</span><span style="color:#ECEFF4">.</span><span style="color:#88C0D0">join</span><span style="color:#D8DEE9FF">(</span><span style="color:#ECEFF4">&#x27;</span><span style="color:#EBCB8B">\n</span><span style="color:#ECEFF4">&#x27;</span><span style="color:#D8DEE9FF">)</span></span>
<span data-line=""><span style="color:#D8DEE9">        ffmpeg</span><span style="color:#ECEFF4">.</span><span style="color:#88C0D0">FS</span><span style="color:#D8DEE9FF">(</span><span style="color:#ECEFF4">&#x27;</span><span style="color:#A3BE8C">writeFile</span><span style="color:#ECEFF4">&#x27;</span><span style="color:#ECEFF4">,</span><span style="color:#ECEFF4"> &#x27;</span><span style="color:#A3BE8C">filelist.txt</span><span style="color:#ECEFF4">&#x27;</span><span style="color:#ECEFF4">,</span><span style="color:#D8DEE9"> fileList</span><span style="color:#D8DEE9FF">)</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#616E88">        // Execute FFmpeg command to concatenate files</span></span>
<span data-line=""><span style="color:#81A1C1">        await</span><span style="color:#D8DEE9"> ffmpeg</span><span style="color:#ECEFF4">.</span><span style="color:#88C0D0">run</span><span style="color:#D8DEE9FF">(</span></span>
<span data-line=""><span style="color:#ECEFF4">          &#x27;</span><span style="color:#A3BE8C">-f</span><span style="color:#ECEFF4">&#x27;</span><span style="color:#ECEFF4">,</span></span>
<span data-line=""><span style="color:#ECEFF4">          &#x27;</span><span style="color:#A3BE8C">concat</span><span style="color:#ECEFF4">&#x27;</span><span style="color:#ECEFF4">,</span></span>
<span data-line=""><span style="color:#ECEFF4">          &#x27;</span><span style="color:#A3BE8C">-safe</span><span style="color:#ECEFF4">&#x27;</span><span style="color:#ECEFF4">,</span></span>
<span data-line=""><span style="color:#ECEFF4">          &#x27;</span><span style="color:#A3BE8C">0</span><span style="color:#ECEFF4">&#x27;</span><span style="color:#ECEFF4">,</span></span>
<span data-line=""><span style="color:#ECEFF4">          &#x27;</span><span style="color:#A3BE8C">-i</span><span style="color:#ECEFF4">&#x27;</span><span style="color:#ECEFF4">,</span></span>
<span data-line=""><span style="color:#ECEFF4">          &#x27;</span><span style="color:#A3BE8C">filelist.txt</span><span style="color:#ECEFF4">&#x27;</span><span style="color:#ECEFF4">,</span></span>
<span data-line=""><span style="color:#ECEFF4">          &#x27;</span><span style="color:#A3BE8C">-c:a</span><span style="color:#ECEFF4">&#x27;</span><span style="color:#ECEFF4">,</span></span>
<span data-line=""><span style="color:#ECEFF4">          &#x27;</span><span style="color:#A3BE8C">libmp3lame</span><span style="color:#ECEFF4">&#x27;</span><span style="color:#ECEFF4">,</span></span>
<span data-line=""><span style="color:#ECEFF4">          &#x27;</span><span style="color:#A3BE8C">-q:a</span><span style="color:#ECEFF4">&#x27;</span><span style="color:#ECEFF4">,</span></span>
<span data-line=""><span style="color:#ECEFF4">          &#x27;</span><span style="color:#A3BE8C">5</span><span style="color:#ECEFF4">&#x27;</span><span style="color:#ECEFF4">,</span></span>
<span data-line=""><span style="color:#ECEFF4">          &#x27;</span><span style="color:#A3BE8C">output.mp3</span><span style="color:#ECEFF4">&#x27;</span><span style="color:#ECEFF4">,</span></span>
<span data-line=""><span style="color:#D8DEE9FF">        )</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#616E88">        // Read the output file</span></span>
<span data-line=""><span style="color:#81A1C1">        const</span><span style="color:#D8DEE9"> data</span><span style="color:#81A1C1"> =</span><span style="color:#D8DEE9"> ffmpeg</span><span style="color:#ECEFF4">.</span><span style="color:#88C0D0">FS</span><span style="color:#D8DEE9FF">(</span><span style="color:#ECEFF4">&#x27;</span><span style="color:#A3BE8C">readFile</span><span style="color:#ECEFF4">&#x27;</span><span style="color:#ECEFF4">,</span><span style="color:#ECEFF4"> &#x27;</span><span style="color:#A3BE8C">output.mp3</span><span style="color:#ECEFF4">&#x27;</span><span style="color:#D8DEE9FF">)</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#616E88">        // Create a downloadable link</span></span>
<span data-line=""><span style="color:#81A1C1">        const</span><span style="color:#D8DEE9"> blob</span><span style="color:#81A1C1"> =</span><span style="color:#81A1C1"> new</span><span style="color:#88C0D0"> Blob</span><span style="color:#D8DEE9FF">([</span><span style="color:#D8DEE9">data</span><span style="color:#ECEFF4">.</span><span style="color:#D8DEE9">buffer</span><span style="color:#D8DEE9FF">]</span><span style="color:#ECEFF4">,</span><span style="color:#ECEFF4"> {</span><span style="color:#88C0D0"> type</span><span style="color:#ECEFF4">:</span><span style="color:#ECEFF4"> &#x27;</span><span style="color:#A3BE8C">audio/mp3</span><span style="color:#ECEFF4">&#x27;</span><span style="color:#ECEFF4"> }</span><span style="color:#D8DEE9FF">)</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#616E88">        // Clean up</span></span>
<span data-line=""><span style="color:#D8DEE9">        audioFiles</span><span style="color:#ECEFF4">.</span><span style="color:#88C0D0">forEach</span><span style="color:#D8DEE9FF">(</span><span style="color:#ECEFF4">(</span><span style="color:#D8DEE9">_</span><span style="color:#ECEFF4">,</span><span style="color:#D8DEE9"> i</span><span style="color:#ECEFF4">)</span><span style="color:#81A1C1"> =&gt;</span><span style="color:#ECEFF4"> {</span></span>
<span data-line=""><span style="color:#D8DEE9">          ffmpeg</span><span style="color:#ECEFF4">.</span><span style="color:#88C0D0">FS</span><span style="color:#D8DEE9FF">(</span><span style="color:#ECEFF4">&#x27;</span><span style="color:#A3BE8C">unlink</span><span style="color:#ECEFF4">&#x27;</span><span style="color:#ECEFF4">,</span><span style="color:#ECEFF4"> `</span><span style="color:#A3BE8C">input</span><span style="color:#81A1C1">${</span><span style="color:#D8DEE9">i</span><span style="color:#81A1C1">}</span><span style="color:#A3BE8C">.mp3</span><span style="color:#ECEFF4">`</span><span style="color:#D8DEE9FF">)</span></span>
<span data-line=""><span style="color:#ECEFF4">        }</span><span style="color:#D8DEE9FF">)</span></span>
<span data-line=""><span style="color:#D8DEE9">        ffmpeg</span><span style="color:#ECEFF4">.</span><span style="color:#88C0D0">FS</span><span style="color:#D8DEE9FF">(</span><span style="color:#ECEFF4">&#x27;</span><span style="color:#A3BE8C">unlink</span><span style="color:#ECEFF4">&#x27;</span><span style="color:#ECEFF4">,</span><span style="color:#ECEFF4"> &#x27;</span><span style="color:#A3BE8C">filelist.txt</span><span style="color:#ECEFF4">&#x27;</span><span style="color:#D8DEE9FF">)</span></span>
<span data-line=""><span style="color:#D8DEE9">        ffmpeg</span><span style="color:#ECEFF4">.</span><span style="color:#88C0D0">FS</span><span style="color:#D8DEE9FF">(</span><span style="color:#ECEFF4">&#x27;</span><span style="color:#A3BE8C">unlink</span><span style="color:#ECEFF4">&#x27;</span><span style="color:#ECEFF4">,</span><span style="color:#ECEFF4"> &#x27;</span><span style="color:#A3BE8C">output.mp3</span><span style="color:#ECEFF4">&#x27;</span><span style="color:#D8DEE9FF">)</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#81A1C1">        return</span><span style="color:#D8DEE9"> blob</span></span>
<span data-line=""><span style="color:#ECEFF4">      }</span></span>
<span data-line=""><span style="color:#81A1C1">    &lt;/script&gt;</span></span>
<span data-line=""><span style="color:#81A1C1">  &lt;/body&gt;</span></span>
<span data-line=""><span style="color:#81A1C1">&lt;/html&gt;</span></span></code></pre></figure>
<h3>Worker 调用代码</h3>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#2e3440ff;color:#d8dee9ff" tabindex="0" data-language="ts" data-theme="nord"><code data-language="ts" data-theme="nord" style="display:grid"><span data-line=""><span style="color:#81A1C1">export</span><span style="color:#81A1C1"> async</span><span style="color:#81A1C1"> function</span><span style="color:#88C0D0"> concatAudioFiles</span><span style="color:#ECEFF4">(</span><span style="color:#D8DEE9">audioFiles</span><span style="color:#81A1C1">:</span><span style="color:#8FBCBB"> string</span><span style="color:#D8DEE9FF">[]</span><span style="color:#ECEFF4">,</span><span style="color:#D8DEE9"> BROWSER</span><span style="color:#81A1C1">:</span><span style="color:#8FBCBB"> Fetcher</span><span style="color:#ECEFF4">,</span><span style="color:#ECEFF4"> {</span><span style="color:#D8DEE9"> workerUrl</span><span style="color:#ECEFF4"> }</span><span style="color:#81A1C1">:</span><span style="color:#ECEFF4"> {</span><span style="color:#D8DEE9FF"> workerUrl</span><span style="color:#81A1C1">:</span><span style="color:#8FBCBB"> string</span><span style="color:#ECEFF4"> })</span><span style="color:#ECEFF4"> {</span></span>
<span data-line=""><span style="color:#81A1C1">  const</span><span style="color:#D8DEE9"> browser</span><span style="color:#81A1C1"> =</span><span style="color:#81A1C1"> await</span><span style="color:#D8DEE9"> puppeteer</span><span style="color:#ECEFF4">.</span><span style="color:#88C0D0">launch</span><span style="color:#D8DEE9FF">(</span><span style="color:#D8DEE9">BROWSER</span><span style="color:#D8DEE9FF">)</span></span>
<span data-line=""><span style="color:#81A1C1">  const</span><span style="color:#D8DEE9"> page</span><span style="color:#81A1C1"> =</span><span style="color:#81A1C1"> await</span><span style="color:#D8DEE9"> browser</span><span style="color:#ECEFF4">.</span><span style="color:#88C0D0">newPage</span><span style="color:#D8DEE9FF">()</span></span>
<span data-line=""><span style="color:#81A1C1">  await</span><span style="color:#D8DEE9"> page</span><span style="color:#ECEFF4">.</span><span style="color:#88C0D0">goto</span><span style="color:#D8DEE9FF">(</span><span style="color:#ECEFF4">`${</span><span style="color:#D8DEE9">workerUrl</span><span style="color:#ECEFF4">}</span><span style="color:#A3BE8C">/audio</span><span style="color:#ECEFF4">`</span><span style="color:#D8DEE9FF">)</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#D8DEE9">  console</span><span style="color:#ECEFF4">.</span><span style="color:#88C0D0">info</span><span style="color:#D8DEE9FF">(</span><span style="color:#ECEFF4">&#x27;</span><span style="color:#A3BE8C">start concat audio files</span><span style="color:#ECEFF4">&#x27;</span><span style="color:#ECEFF4">,</span><span style="color:#D8DEE9"> audioFiles</span><span style="color:#D8DEE9FF">)</span></span>
<span data-line=""><span style="color:#81A1C1">  const</span><span style="color:#D8DEE9"> fileUrl</span><span style="color:#81A1C1"> =</span><span style="color:#81A1C1"> await</span><span style="color:#D8DEE9"> page</span><span style="color:#ECEFF4">.</span><span style="color:#88C0D0">evaluate</span><span style="color:#D8DEE9FF">(</span><span style="color:#81A1C1">async</span><span style="color:#ECEFF4"> (</span><span style="color:#D8DEE9">audioFiles</span><span style="color:#ECEFF4">)</span><span style="color:#81A1C1"> =&gt;</span><span style="color:#ECEFF4"> {</span></span>
<span data-line=""><span style="color:#616E88">    // 此处 JS 运行在浏览器中</span></span>
<span data-line=""><span style="color:#616E88">    // @ts-expect-error 浏览器内的对象</span></span>
<span data-line=""><span style="color:#81A1C1">    const</span><span style="color:#D8DEE9"> blob</span><span style="color:#81A1C1"> =</span><span style="color:#81A1C1"> await</span><span style="color:#88C0D0"> concatAudioFilesOnBrowser</span><span style="color:#D8DEE9FF">(</span><span style="color:#D8DEE9">audioFiles</span><span style="color:#D8DEE9FF">)</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#81A1C1">    const</span><span style="color:#D8DEE9"> result</span><span style="color:#81A1C1"> =</span><span style="color:#81A1C1"> new</span><span style="color:#8FBCBB"> Promise</span><span style="color:#D8DEE9FF">(</span><span style="color:#ECEFF4">(</span><span style="color:#D8DEE9">resolve</span><span style="color:#ECEFF4">,</span><span style="color:#D8DEE9"> reject</span><span style="color:#ECEFF4">)</span><span style="color:#81A1C1"> =&gt;</span><span style="color:#ECEFF4"> {</span></span>
<span data-line=""><span style="color:#81A1C1">      const</span><span style="color:#D8DEE9"> reader</span><span style="color:#81A1C1"> =</span><span style="color:#81A1C1"> new</span><span style="color:#88C0D0"> FileReader</span><span style="color:#D8DEE9FF">()</span></span>
<span data-line=""><span style="color:#D8DEE9">      reader</span><span style="color:#ECEFF4">.</span><span style="color:#88C0D0">onloadend</span><span style="color:#81A1C1"> =</span><span style="color:#ECEFF4"> ()</span><span style="color:#81A1C1"> =&gt;</span><span style="color:#88C0D0"> resolve</span><span style="color:#D8DEE9FF">(</span><span style="color:#D8DEE9">reader</span><span style="color:#ECEFF4">.</span><span style="color:#D8DEE9">result</span><span style="color:#D8DEE9FF">)</span></span>
<span data-line=""><span style="color:#D8DEE9">      reader</span><span style="color:#ECEFF4">.</span><span style="color:#D8DEE9">onerror</span><span style="color:#81A1C1"> =</span><span style="color:#D8DEE9"> reject</span></span>
<span data-line=""><span style="color:#D8DEE9">      reader</span><span style="color:#ECEFF4">.</span><span style="color:#88C0D0">readAsDataURL</span><span style="color:#D8DEE9FF">(</span><span style="color:#D8DEE9">blob</span><span style="color:#D8DEE9FF">)</span></span>
<span data-line=""><span style="color:#ECEFF4">    }</span><span style="color:#D8DEE9FF">)</span></span>
<span data-line=""><span style="color:#81A1C1">    return</span><span style="color:#81A1C1"> await</span><span style="color:#D8DEE9"> result</span></span>
<span data-line=""><span style="color:#ECEFF4">  },</span><span style="color:#D8DEE9"> audioFiles</span><span style="color:#D8DEE9FF">) </span><span style="color:#81A1C1">as</span><span style="color:#8FBCBB"> string</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#D8DEE9">  console</span><span style="color:#ECEFF4">.</span><span style="color:#88C0D0">info</span><span style="color:#D8DEE9FF">(</span><span style="color:#ECEFF4">&#x27;</span><span style="color:#A3BE8C">concat audio files result</span><span style="color:#ECEFF4">&#x27;</span><span style="color:#ECEFF4">,</span><span style="color:#D8DEE9"> fileUrl</span><span style="color:#ECEFF4">.</span><span style="color:#88C0D0">substring</span><span style="color:#D8DEE9FF">(</span><span style="color:#B48EAD">0</span><span style="color:#ECEFF4">,</span><span style="color:#B48EAD"> 100</span><span style="color:#D8DEE9FF">))</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#81A1C1">  await</span><span style="color:#D8DEE9"> browser</span><span style="color:#ECEFF4">.</span><span style="color:#88C0D0">close</span><span style="color:#D8DEE9FF">()</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#81A1C1">  const</span><span style="color:#D8DEE9"> response</span><span style="color:#81A1C1"> =</span><span style="color:#81A1C1"> await</span><span style="color:#88C0D0"> fetch</span><span style="color:#D8DEE9FF">(</span><span style="color:#D8DEE9">fileUrl</span><span style="color:#D8DEE9FF">)</span></span>
<span data-line=""><span style="color:#81A1C1">  return</span><span style="color:#81A1C1"> await</span><span style="color:#D8DEE9"> response</span><span style="color:#ECEFF4">.</span><span style="color:#88C0D0">blob</span><span style="color:#D8DEE9FF">()</span></span>
<span data-line=""><span style="color:#ECEFF4">}</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#81A1C1">const</span><span style="color:#D8DEE9"> audio</span><span style="color:#81A1C1"> =</span><span style="color:#81A1C1"> await</span><span style="color:#88C0D0"> concatAudioFiles</span><span style="color:#D8DEE9FF">(</span><span style="color:#D8DEE9">audioFiles</span><span style="color:#ECEFF4">,</span><span style="color:#D8DEE9"> env</span><span style="color:#ECEFF4">.</span><span style="color:#D8DEE9">BROWSER</span><span style="color:#ECEFF4">,</span><span style="color:#ECEFF4"> {</span><span style="color:#D8DEE9"> workerUrl</span><span style="color:#ECEFF4">:</span><span style="color:#D8DEE9"> env</span><span style="color:#ECEFF4">.</span><span style="color:#D8DEE9">HACKER_NEWS_WORKER_URL</span><span style="color:#ECEFF4"> }</span><span style="color:#D8DEE9FF">)</span></span>
<span data-line=""><span style="color:#81A1C1">return</span><span style="color:#81A1C1"> new</span><span style="color:#88C0D0"> Response</span><span style="color:#D8DEE9FF">(</span><span style="color:#D8DEE9">audio</span><span style="color:#D8DEE9FF">)</span></span></code></pre></figure>
<p>上面的代码基本是 Cursor 写的，最终效果可以去 <a href="https://github.com/miantiao-me/hacker-news/tree/main/worker" rel="nofollow noopener noreferrer" target="_blank">Hacker News 代码仓库</a> 看。</p><img src="https://c.statcounter.com/9823304/0/92a1d06c/1/" alt="" width="1" height="1" referrerPolicy="no-referrer-when-downgrade" />]]></content:encoded>
    </item>
    <item>
      <title>使用 AI 让博客排版更上一个台阶</title>
      <link>https://miantiao.me/posts/ai-blog-typography/</link>
      <guid>https://miantiao.me/posts/ai-blog-typography/</guid>
      <description>使用人工智能技术重新定义博客文章的视觉呈现，从简约到精致，从平凡到卓越。</description>
      <pubDate>Sun, 23 Mar 2025 13:39:55 GMT</pubDate>
      <content:encoded><![CDATA[<link rel="preload" as="image" href="https://static.miantiao.me/share/2025/wm70yi/hn-old.png"/><link rel="preload" as="image" href="https://static.miantiao.me/share/2025/P9Rcfo/hn-new.png"/><link rel="preload" as="image" href="https://static.miantiao.me/share/2025/DWNmIh/rss-old.png"/><link rel="preload" as="image" href="https://static.miantiao.me/share/2025/keIqtA/rss-new.png"/><link rel="preload" as="image" href="https://static.miantiao.me/share/2025/T2xVdS/sink-old.png"/><link rel="preload" as="image" href="https://static.miantiao.me/share/2025/udluK7/sink-new.png"/><link rel="preload" as="image" href="https://static.miantiao.me/share/2025/8KZoRB/long-old.png"/><link rel="preload" as="image" href="https://static.miantiao.me/share/2025/rE7o6B/long-new.png"/><div class="not-prose"><div class="relative mb-8 overflow-hidden rounded-xl bg-zinc-100 px-4 py-8 shadow-xs dark:bg-zinc-800/50 sm:mb-10 sm:px-8 sm:py-12 md:mb-12 md:px-10 md:py-14"><div class="absolute -right-10 -top-10 h-40 w-40 rounded-full bg-gradient-to-br from-zinc-400/20 to-zinc-600/20 blur-3xl filter dark:from-zinc-400/10 dark:to-zinc-600/10"></div><div class="absolute -bottom-10 -left-10 h-40 w-40 rounded-full bg-gradient-to-tr from-zinc-300/20 to-zinc-500/20 blur-3xl filter dark:from-zinc-300/10 dark:to-zinc-500/10"></div><div class="relative"><h2 class="flex items-center font-serif text-2xl font-bold text-zinc-900 dark:text-zinc-50 sm:text-3xl md:text-4xl lg:text-5xl"><span class="icon-[tabler--sparkles] mr-2 text-zinc-600 dark:text-zinc-400 sm:mr-3"></span><p>AI 驱动的博客排版</p></h2><p class="mt-3 max-w-3xl text-base text-zinc-700 dark:text-zinc-300 sm:mt-4 sm:text-lg md:text-xl"><p>使用人工智能技术重新定义博客文章的视觉呈现，从简约到精致，从平凡到卓越。</p></p></div></div><div class="mx-auto max-w-3xl space-y-6 sm:space-y-8"><div class="first-letter:float-left first-letter:mr-3 first-letter:text-4xl first-letter:font-bold first-letter:text-zinc-600 dark:first-letter:text-zinc-400"><p class="text-base leading-relaxed text-zinc-800 dark:text-zinc-200 sm:text-lg sm:leading-7"><p>之前 Claude 3.7 发布，用它生成了一个邮箱 App 的设计图，效果还不错。</p></p></div><p class="text-base leading-relaxed text-zinc-800 dark:text-zinc-200 sm:text-lg sm:leading-7"><p>这周在 Twitter 上看到<a href="https://x.com/vista8/status/1901224129405338102" target="_blank" rel="nofollow" title="乔木老师的Twitter" class="border-b border-dotted border-zinc-400 text-zinc-600 transition-colors hover:bg-zinc-100 dark:text-zinc-400 dark:hover:bg-zinc-800">乔木老师分享</a>的生成网页效果、生成PPT、生成3D教学动画、生成SVG海报的提示词，感觉很有意思。</p></p><div class="rounded-lg bg-zinc-50 p-4 shadow-xs dark:bg-zinc-800/50 sm:p-5 md:p-6"><div class="flex items-center"><span class="icon-[tabler--code] mr-2 text-lg text-zinc-500 dark:text-zinc-400"></span><h3 class="text-base font-medium text-zinc-900 dark:text-zinc-50 sm:text-lg">技术栈更新</h3></div><p class="mt-2 text-zinc-700 dark:text-zinc-300 sm:mt-3"><p>这个周末把博客引擎升级了一下，使用 Astro + TailwindCSS + MDX 重构了博客。由于 MDX 有更强的扩展性，所以博客文章正文几乎可以展示任何内容。</p></p></div><p class="text-base leading-relaxed text-zinc-800 dark:text-zinc-200 sm:text-lg sm:leading-7"><p>于是使用乔木老师分享的提示词，修改了以后，用来生成博客文章的排版。效果出乎意料，从此不再为博客文章的排版而烦恼。</p></p><h2 class="flex items-center border-l-4 border-zinc-500 pl-3 text-xl font-bold text-zinc-900 dark:text-zinc-50 sm:pl-4 sm:text-2xl md:text-3xl"><span class="icon-[tabler--photo] mr-2 text-zinc-500"></span><p>对比图</p></h2><div class="space-y-8 sm:space-y-10 md:space-y-12"><div class="rounded-xl bg-gradient-to-r from-zinc-50 to-zinc-100 p-1 shadow-md dark:from-zinc-800 dark:to-zinc-900"><div class="rounded-lg bg-white p-4 dark:bg-zinc-900 sm:p-5 md:p-6"><h3 class="mb-4 font-serif text-lg font-semibold text-zinc-900 dark:text-zinc-50 sm:mb-5 sm:text-xl"><a href="https://miantiao.me/posts/hacker-news-podcast/" target="_blank" rel="nofollow" title="HackerNews Podcast" class="flex items-center hover:text-zinc-600 dark:hover:text-zinc-400"><span class="icon-[tabler--headphones] mr-2"></span><p>HackerNews Podcast</p><span class="icon-[tabler--external-link] ml-1 text-sm"></span></a></h3><div class="space-y-6 sm:space-y-8"><div class="space-y-2"><p class="text-sm font-medium text-zinc-500 dark:text-zinc-400">修改前：</p><div class="overflow-hidden rounded-lg transition-transform hover:scale-[1.02]"><img src="https://static.miantiao.me/share/2025/wm70yi/hn-old.png" alt="HackerNews 修改前" class="w-full"/></div></div><div class="space-y-2"><p class="text-sm font-medium text-zinc-500 dark:text-zinc-400">修改后：</p><div class="overflow-hidden rounded-lg transition-transform hover:scale-[1.02]"><img src="https://static.miantiao.me/share/2025/P9Rcfo/hn-new.png" alt="HackerNews 修改后" class="w-full"/></div></div></div></div></div><div class="rounded-xl bg-gradient-to-r from-zinc-50 to-zinc-100 p-1 shadow-md dark:from-zinc-800 dark:to-zinc-900"><div class="rounded-lg bg-white p-4 dark:bg-zinc-900 sm:p-5 md:p-6"><h3 class="mb-4 font-serif text-lg font-semibold text-zinc-900 dark:text-zinc-50 sm:mb-5 sm:text-xl"><a href="https://miantiao.me/posts/rss-beauty/" target="_blank" rel="nofollow" title="RSS Beauty" class="flex items-center hover:text-zinc-600 dark:hover:text-zinc-400"><span class="icon-[tabler--rss] mr-2"></span><p>RSS Beauty</p><span class="icon-[tabler--external-link] ml-1 text-sm"></span></a></h3><div class="space-y-6 sm:space-y-8"><div class="space-y-2"><p class="text-sm font-medium text-zinc-500 dark:text-zinc-400">修改前：</p><div class="overflow-hidden rounded-lg transition-transform hover:scale-[1.02]"><img src="https://static.miantiao.me/share/2025/DWNmIh/rss-old.png" alt="RSS Beauty 修改前" class="w-full"/></div></div><div class="space-y-2"><p class="text-sm font-medium text-zinc-500 dark:text-zinc-400">修改后：</p><div class="overflow-hidden rounded-lg transition-transform hover:scale-[1.02]"><img src="https://static.miantiao.me/share/2025/keIqtA/rss-new.png" alt="RSS Beauty 修改后" class="w-full"/></div></div></div></div></div><div class="rounded-xl bg-gradient-to-r from-zinc-50 to-zinc-100 p-1 shadow-md dark:from-zinc-800 dark:to-zinc-900"><div class="rounded-lg bg-white p-4 dark:bg-zinc-900 sm:p-5 md:p-6"><h3 class="mb-4 font-serif text-lg font-semibold text-zinc-900 dark:text-zinc-50 sm:mb-5 sm:text-xl"><a href="https://miantiao.me/posts/sink/" target="_blank" rel="nofollow" title="Sink" class="flex items-center hover:text-zinc-600 dark:hover:text-zinc-400"><span class="icon-[tabler--link] mr-2"></span><p>Sink</p><span class="icon-[tabler--external-link] ml-1 text-sm"></span></a></h3><div class="space-y-6 sm:space-y-8"><div class="space-y-2"><p class="text-sm font-medium text-zinc-500 dark:text-zinc-400">修改前：</p><div class="overflow-hidden rounded-lg transition-transform hover:scale-[1.02]"><img src="https://static.miantiao.me/share/2025/T2xVdS/sink-old.png" alt="Sink 修改前" class="w-full"/></div></div><div class="space-y-2"><p class="text-sm font-medium text-zinc-500 dark:text-zinc-400">修改后：</p><div class="overflow-hidden rounded-lg transition-transform hover:scale-[1.02]"><img src="https://static.miantiao.me/share/2025/udluK7/sink-new.png" alt="Sink 修改后" class="w-full"/></div></div></div></div></div><div class="rounded-xl bg-gradient-to-r from-zinc-50 to-zinc-100 p-1 shadow-md dark:from-zinc-800 dark:to-zinc-900"><div class="rounded-lg bg-white p-4 dark:bg-zinc-900 sm:p-5 md:p-6"><h3 class="mb-4 font-serif text-lg font-semibold text-zinc-900 dark:text-zinc-50 sm:mb-5 sm:text-xl"><a href="https://miantiao.me/posts/looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong/" target="_blank" rel="nofollow" title="URL Longer" class="flex items-center hover:text-zinc-600 dark:hover:text-zinc-400"><span class="icon-[tabler--link] mr-2"></span><p>URL Longer</p><span class="icon-[tabler--external-link] ml-1 text-sm"></span></a></h3><div class="space-y-6 sm:space-y-8"><div class="space-y-2"><p class="text-sm font-medium text-zinc-500 dark:text-zinc-400">修改前：</p><div class="overflow-hidden rounded-lg transition-transform hover:scale-[1.02]"><img src="https://static.miantiao.me/share/2025/8KZoRB/long-old.png" alt="URL longer 修改前" class="w-full"/></div></div><div class="space-y-2"><p class="text-sm font-medium text-zinc-500 dark:text-zinc-400">修改后：</p><div class="overflow-hidden rounded-lg transition-transform hover:scale-[1.02]"><img src="https://static.miantiao.me/share/2025/rE7o6B/long-new.png" alt="URL longer 修改后" class="w-full"/></div></div></div></div></div></div><div class="rounded-xl bg-zinc-50 p-6 shadow-xs dark:bg-zinc-800/50 sm:p-7 md:p-8"><h2 class="mb-4 flex items-center font-serif text-xl font-bold text-zinc-900 dark:text-zinc-50 sm:mb-5 sm:text-2xl md:text-3xl"><span class="icon-[tabler--sparkles] mr-2 text-zinc-500"></span><p>更多展示效果</p></h2><ul class="grid gap-3 sm:grid-cols-2 sm:gap-4"><li class="group rounded-lg bg-white p-3 shadow-xs transition-all hover:shadow-md dark:bg-zinc-900 sm:p-4"><a href="https://miantiao.me/posts/ai-remove-image-background/" target="_blank" rel="nofollow" title="浏览器本地使用 AI 移除图片背景" class="flex items-center text-zinc-800 group-hover:text-zinc-600 dark:text-zinc-200 dark:group-hover:text-zinc-400"><span class="icon-[tabler--photo-ai] mr-2"></span><span class="flex-1 text-sm sm:text-base">浏览器本地使用 AI 移除图片背景</span><span class="icon-[tabler--chevron-right] ml-auto opacity-0 transition-opacity group-hover:opacity-100"></span></a></li><li class="group rounded-lg bg-white p-3 shadow-xs transition-all hover:shadow-md dark:bg-zinc-900 sm:p-4"><a href="https://miantiao.me/posts/broadcast-channel/" target="_blank" rel="nofollow" title="BroadcastChannel - 将你的 Telegram Channel 转为微博客" class="flex items-center text-zinc-800 group-hover:text-zinc-600 dark:text-zinc-200 dark:group-hover:text-zinc-400"><span class="icon-[tabler--brand-telegram] mr-2"></span><span class="flex-1 text-sm sm:text-base">BroadcastChannel - 将 Telegram Channel 转为微博客</span><span class="icon-[tabler--chevron-right] ml-auto opacity-0 transition-opacity group-hover:opacity-100"></span></a></li><li class="group rounded-lg bg-white p-3 shadow-xs transition-all hover:shadow-md dark:bg-zinc-900 sm:p-4"><a href="https://miantiao.me/posts/dns-surf/" target="_blank" rel="nofollow" title="DNS.Surf - 检测域名在全球的 DNS 解析结果" class="flex items-center text-zinc-800 group-hover:text-zinc-600 dark:text-zinc-200 dark:group-hover:text-zinc-400"><span class="icon-[tabler--world-www] mr-2"></span><span class="flex-1 text-sm sm:text-base">DNS.Surf - 检测域名在全球的 DNS 解析结果</span><span class="icon-[tabler--chevron-right] ml-auto opacity-0 transition-opacity group-hover:opacity-100"></span></a></li><li class="group rounded-lg bg-white p-3 shadow-xs transition-all hover:shadow-md dark:bg-zinc-900 sm:p-4"><a href="https://miantiao.me/posts/email-ml/" target="_blank" rel="nofollow" title="TempMail.Best - 最棒的一次性临时邮箱" class="flex items-center text-zinc-800 group-hover:text-zinc-600 dark:text-zinc-200 dark:group-hover:text-zinc-400"><span class="icon-[tabler--mail] mr-2"></span><span class="flex-1 text-sm sm:text-base">TempMail.Best - 最棒的一次性临时邮箱</span><span class="icon-[tabler--chevron-right] ml-auto opacity-0 transition-opacity group-hover:opacity-100"></span></a></li></ul></div><div class="rounded-xl bg-gradient-to-br from-zinc-100 to-zinc-200 p-6 shadow-xs dark:from-zinc-800/30 dark:to-zinc-700/30 sm:p-7 md:p-8"><h2 class="mb-3 font-serif text-xl font-bold text-zinc-900 dark:text-zinc-50 sm:mb-4 sm:text-2xl md:text-3xl"><span class="icon-[tabler--prompt] mr-2 text-zinc-600 dark:text-zinc-400"></span><p>提示词</p></h2><p class="text-zinc-800 dark:text-zinc-200"><p>源自 <a href="https://x.com/vista8/status/1901224129405338102" target="_blank" rel="nofollow" title="乔木老师的Twitter分享" class="border-b border-dotted border-zinc-400 text-zinc-600 transition-colors hover:bg-zinc-100 dark:text-zinc-400 dark:hover:bg-zinc-800">乔木老师分享</a></p></p></div></div></div>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#2e3440ff;color:#d8dee9ff" tabindex="0" data-language="md" data-theme="nord"><code data-language="md" data-theme="nord" style="display:grid"><span data-line=""><span style="color:#81A1C1">#</span><span style="color:#88C0D0"> 生成文章网页</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#D8DEE9FF">你是一名专业的网页设计师和前端开发专家，对现代 Web 设计趋势和最佳实践有深入理解，尤其擅长创造具有极高审美价值的用户界面。你的设计作品不仅功能完备，而且在视觉上令人惊叹，能够给用户带来强烈的&quot;Aha-moment&quot;体验。</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#D8DEE9FF">请根据最后提供的内容，设计一个</span><span style="color:#ECEFF4;font-weight:bold">**</span><span style="color:#D8DEE9FF;font-weight:bold">美观、现代、易读</span><span style="color:#ECEFF4;font-weight:bold">**</span><span style="color:#D8DEE9FF">的&quot;中文&quot;可视化网页。请充分发挥你的专业判断，选择最能体现内容精髓的设计风格、配色方案、排版和布局。</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#ECEFF4;font-weight:bold">**</span><span style="color:#D8DEE9FF;font-weight:bold">设计目标：</span><span style="color:#ECEFF4;font-weight:bold">**</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#ECEFF4">*</span><span style="color:#ECEFF4;font-weight:bold"> **</span><span style="color:#D8DEE9FF;font-weight:bold">视觉吸引力：</span><span style="color:#ECEFF4;font-weight:bold">**</span><span style="color:#D8DEE9FF"> 创造一个在视觉上令人印象深刻的网页，能够立即吸引用户的注意力，并激发他们的阅读兴趣。</span></span>
<span data-line=""><span style="color:#ECEFF4">*</span><span style="color:#ECEFF4;font-weight:bold"> **</span><span style="color:#D8DEE9FF;font-weight:bold">可读性：</span><span style="color:#ECEFF4;font-weight:bold">**</span><span style="color:#D8DEE9FF"> 确保内容清晰易读，无论在桌面端还是移动端，都能提供舒适的阅读体验。</span></span>
<span data-line=""><span style="color:#ECEFF4">*</span><span style="color:#ECEFF4;font-weight:bold"> **</span><span style="color:#D8DEE9FF;font-weight:bold">信息传达：</span><span style="color:#ECEFF4;font-weight:bold">**</span><span style="color:#D8DEE9FF"> 以一种既美观又高效的方式呈现信息，突出关键内容，引导用户理解核心思想。</span></span>
<span data-line=""><span style="color:#ECEFF4">*</span><span style="color:#ECEFF4;font-weight:bold"> **</span><span style="color:#D8DEE9FF;font-weight:bold">情感共鸣:</span><span style="color:#ECEFF4;font-weight:bold">**</span><span style="color:#D8DEE9FF"> 通过设计激发与内容主题相关的情感（例如，对于励志内容，激发积极向上的情绪；对于严肃内容，营造庄重、专业的氛围）。</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#ECEFF4;font-weight:bold">**</span><span style="color:#D8DEE9FF;font-weight:bold">设计指导（请灵活运用，而非严格遵循）：</span><span style="color:#ECEFF4;font-weight:bold">**</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#ECEFF4">*</span><span style="color:#ECEFF4;font-weight:bold"> **</span><span style="color:#D8DEE9FF;font-weight:bold">整体风格：</span><span style="color:#ECEFF4;font-weight:bold">**</span></span>
<span data-line=""><span style="color:#ECEFF4">  *</span><span style="color:#D8DEE9FF"> 可以考虑杂志风格、出版物风格，或者其他你认为合适的现代 Web 设计风格。</span></span>
<span data-line=""><span style="color:#ECEFF4">  *</span><span style="color:#D8DEE9FF"> 配色参考 shadcn ui 的 Zinc 主题配色。</span></span>
<span data-line=""><span style="color:#ECEFF4">  *</span><span style="color:#D8DEE9FF"> 目标是创造一个既有信息量，又有视觉吸引力的页面，就像一本精心设计的数字杂志或一篇深度报道。</span></span>
<span data-line=""><span style="color:#ECEFF4">*</span><span style="color:#ECEFF4;font-weight:bold"> **</span><span style="color:#D8DEE9FF;font-weight:bold">Hero 模块（可选，但强烈建议）：</span><span style="color:#ECEFF4;font-weight:bold">**</span></span>
<span data-line=""><span style="color:#ECEFF4">  *</span><span style="color:#D8DEE9FF"> 如果你认为合适，可以设计一个引人注目的 Hero 模块。</span></span>
<span data-line=""><span style="color:#ECEFF4">  *</span><span style="color:#D8DEE9FF"> 它可以包含标题(h2)、副标题（p）、一段引人入胜的引言。</span></span>
<span data-line=""><span style="color:#ECEFF4">*</span><span style="color:#ECEFF4;font-weight:bold"> **</span><span style="color:#D8DEE9FF;font-weight:bold">排版：</span><span style="color:#ECEFF4;font-weight:bold">**</span></span>
<span data-line=""><span style="color:#ECEFF4">  *</span><span style="color:#D8DEE9FF"> 精心选择字体组合（衬线和无衬线），以提升中文阅读体验。</span></span>
<span data-line=""><span style="color:#ECEFF4">  *</span><span style="color:#D8DEE9FF"> 利用不同的字号、字重、颜色和样式，创建清晰的视觉层次结构。</span></span>
<span data-line=""><span style="color:#ECEFF4">  *</span><span style="color:#D8DEE9FF"> 可以考虑使用一些精致的排版细节（如首字下沉、悬挂标点）来提升整体质感。</span></span>
<span data-line=""><span style="color:#ECEFF4">  *</span><span style="color:#D8DEE9FF"> Tabler icon 中有很多图标，选合适的点缀增加趣味性。 使用实例 </span><span style="color:#8FBCBB">`icon-[tabler--label]`</span></span>
<span data-line=""><span style="color:#ECEFF4">*</span><span style="color:#ECEFF4;font-weight:bold"> **</span><span style="color:#D8DEE9FF;font-weight:bold">配色方案：</span><span style="color:#ECEFF4;font-weight:bold">**</span></span>
<span data-line=""><span style="color:#ECEFF4">  *</span><span style="color:#D8DEE9FF"> 选择一套既和谐又具有视觉冲击力的配色方案。</span></span>
<span data-line=""><span style="color:#ECEFF4">  *</span><span style="color:#D8DEE9FF"> 考虑使用高对比度的颜色组合来突出重要元素。</span></span>
<span data-line=""><span style="color:#ECEFF4">  *</span><span style="color:#D8DEE9FF"> 可以探索渐变、阴影等效果来增加视觉深度。</span></span>
<span data-line=""><span style="color:#ECEFF4">*</span><span style="color:#ECEFF4;font-weight:bold"> **</span><span style="color:#D8DEE9FF;font-weight:bold">布局：</span><span style="color:#ECEFF4;font-weight:bold">**</span></span>
<span data-line=""><span style="color:#ECEFF4">  *</span><span style="color:#D8DEE9FF"> 使用基于网格的布局系统来组织页面元素。</span></span>
<span data-line=""><span style="color:#ECEFF4">  *</span><span style="color:#D8DEE9FF"> 充分利用负空间（留白），创造视觉平衡和呼吸感。</span></span>
<span data-line=""><span style="color:#ECEFF4">  *</span><span style="color:#D8DEE9FF"> 可以考虑使用卡片、分割线、图标等视觉元素来分隔和组织内容。</span></span>
<span data-line=""><span style="color:#ECEFF4">*</span><span style="color:#ECEFF4;font-weight:bold"> **</span><span style="color:#D8DEE9FF;font-weight:bold">调性：</span><span style="color:#ECEFF4;font-weight:bold">**</span><span style="color:#D8DEE9FF">整体风格精致, 营造一种高级感。</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#ECEFF4;font-weight:bold">**</span><span style="color:#D8DEE9FF;font-weight:bold">技术规范：</span><span style="color:#ECEFF4;font-weight:bold">**</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#ECEFF4">*</span><span style="color:#D8DEE9FF"> 使用 tailwindCSS 定义样式。</span></span>
<span data-line=""><span style="color:#ECEFF4">*</span><span style="color:#D8DEE9FF"> 不使用 JS , HTML 和 CSS 优先。</span></span>
<span data-line=""><span style="color:#ECEFF4">*</span><span style="color:#D8DEE9FF"> 只生成正文区域，网页已经使用 </span><span style="color:#8FBCBB">`prose`</span><span style="color:#D8DEE9FF"> 类包裹了, 可以使用 </span><span style="color:#8FBCBB">`not-prose`</span><span style="color:#D8DEE9FF"> 突破限制。</span></span>
<span data-line=""><span style="color:#ECEFF4">*</span><span style="color:#D8DEE9FF"> 实现完整的深色/浅色模式切换功能。</span></span>
<span data-line=""><span style="color:#ECEFF4">*</span><span style="color:#D8DEE9FF"> 代码结构清晰、语义化，包含适当的注释。</span></span>
<span data-line=""><span style="color:#ECEFF4">*</span><span style="color:#D8DEE9FF"> 实现完整的响应式，必须在所有设备上（手机、平板、桌面）完美展示。</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#ECEFF4;font-weight:bold">**</span><span style="color:#D8DEE9FF;font-weight:bold">额外加分项：</span><span style="color:#ECEFF4;font-weight:bold">**</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#ECEFF4">*</span><span style="color:#ECEFF4;font-weight:bold"> **</span><span style="color:#D8DEE9FF;font-weight:bold">微交互：</span><span style="color:#ECEFF4;font-weight:bold">**</span><span style="color:#D8DEE9FF"> 添加微妙而有意义的微交互效果来提升用户体验（例如，按钮悬停效果、卡片悬停效果、页面滚动效果）。</span></span>
<span data-line=""><span style="color:#ECEFF4">*</span><span style="color:#ECEFF4;font-weight:bold"> **</span><span style="color:#D8DEE9FF;font-weight:bold">补充信息：</span><span style="color:#ECEFF4;font-weight:bold">**</span><span style="color:#D8DEE9FF"> 可以主动搜索并补充其他重要信息或模块（例如，关键概念的解释、相关人物的介绍等），以增强用户对内容的理解。</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#ECEFF4;font-weight:bold">**</span><span style="color:#D8DEE9FF;font-weight:bold">输出要求：</span><span style="color:#ECEFF4;font-weight:bold">**</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#ECEFF4">*</span><span style="color:#D8DEE9FF"> 输出一个独立的 MDX 文件，生成的语法符合 MDX 规范和 JSX 规范。</span></span>
<span data-line=""><span style="color:#ECEFF4">*</span><span style="color:#D8DEE9FF"> 修改范围不要超出 mdx 文件。</span></span>
<span data-line=""><span style="color:#ECEFF4">*</span><span style="color:#D8DEE9FF"> 不要在正文中出现标签，发布时间相关的信息。</span></span>
<span data-line=""><span style="color:#ECEFF4">*</span><span style="color:#D8DEE9FF"> 外链增加 </span><span style="color:#8FBCBB">`nofollow`</span><span style="color:#D8DEE9FF">, 在新窗口打开, 保留 </span><span style="color:#8FBCBB">`title`</span><span style="color:#D8DEE9FF"> 属性。</span></span>
<span data-line=""><span style="color:#ECEFF4">*</span><span style="color:#D8DEE9FF"> 代码块不做任何修改，依旧使用 md 格式。</span></span>
<span data-line=""><span style="color:#ECEFF4">*</span><span style="color:#D8DEE9FF"> 确保代码符合 W3C 标准，没有错误或警告。</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#D8DEE9FF">请你像一个真正的设计师一样思考，充分发挥你的专业技能和创造力，打造一个令人惊艳的网页！</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#D8DEE9FF">待处理内容：@miantiao_me</span></span></code></pre></figure><img src="https://c.statcounter.com/9823304/0/92a1d06c/1/" alt="" width="1" height="1" referrerPolicy="no-referrer-when-downgrade" />]]></content:encoded>
    </item>
    <item>
      <title>用播客的方式听 Hacker News</title>
      <link>https://miantiao.me/posts/hacker-news-podcast/</link>
      <guid>https://miantiao.me/posts/hacker-news-podcast/</guid>
      <description>利用AI和Cloudflare将Hacker News热门内容转换为中文播客，支持RSS订阅，方便随时随地收听科技资讯</description>
      <pubDate>Mon, 03 Mar 2025 12:40:37 GMT</pubDate>
      <content:encoded><![CDATA[<link rel="preload" as="image" href="https://github.html.zone/miantiao-me/hacker-news"/><link rel="preload" as="image" href="https://static.miantiao.me/share/2025/0ZROOg/postspark_export_2025-03-22_21-39-43.webp"/><div class="not-prose"><div class="relative bg-zinc-100 dark:bg-zinc-900 rounded-2xl overflow-hidden mb-8 md:mb-10"><div class="absolute inset-0 bg-gradient-to-br from-zinc-200/80 to-transparent dark:from-zinc-800/80 dark:to-transparent"></div><div class="relative px-6 py-10 md:px-10 md:py-16 lg:px-12 lg:py-20"><h2 class="text-2xl md:text-3xl lg:text-4xl font-bold tracking-tight text-zinc-900 dark:text-zinc-50 mb-3 md:mb-4"><p>用播客的方式听 <span class="text-orange-600 dark:text-orange-400">Hacker News</span></p></h2><p class="text-base md:text-lg md:leading-7 lg:text-xl lg:leading-7 text-zinc-600 dark:text-zinc-400 max-w-2xl leading-relaxed"><p>将每日 Hacker News 热门内容转换为中文播客，让你随时随地收听科技资讯</p></p><div class="mt-6 md:mt-8 flex flex-wrap gap-2 md:gap-3"><a href="https://hacker-news.agi.li" target="_blank" rel="nofollow" class="inline-flex items-center px-3 py-1.5 md:px-4 md:py-2 rounded-full bg-zinc-900 text-zinc-50 hover:bg-zinc-800 dark:bg-zinc-50 dark:text-zinc-900 dark:hover:bg-zinc-200 transition-colors text-sm md:text-base"><span class="icon-[tabler--live-view] mr-1.5 md:mr-2 w-4 h-4 md:w-5 md:h-5"></span><p>在线预览</p></a><a href="https://hacker-news.agi.li/rss.xml" target="_blank" rel="nofollow" class="inline-flex items-center px-3 py-1.5 md:px-4 md:py-2 rounded-full bg-orange-600 text-white hover:bg-orange-700 dark:bg-orange-500 dark:hover:bg-orange-600 transition-colors text-sm md:text-base"><span class="icon-[tabler--rss] mr-1.5 md:mr-2 w-4 h-4 md:w-5 md:h-5"></span><p>RSS 订阅</p></a><a href="https://github.com/miantiao-me/hacker-news" target="_blank" rel="nofollow" class="inline-flex items-center px-3 py-1.5 md:px-4 md:py-2 rounded-full bg-zinc-200 text-zinc-800 hover:bg-zinc-300 dark:bg-zinc-700 dark:text-zinc-100 dark:hover:bg-zinc-600 transition-colors text-sm md:text-base"><span class="icon-[tabler--brand-github] mr-1.5 md:mr-2 w-4 h-4 md:w-5 md:h-5"></span><p>GitHub 项目</p></a></div></div></div><div class="bg-zinc-50 dark:bg-zinc-900 rounded-xl p-4 md:p-6 mb-8 md:mb-10 shadow-xs border border-zinc-200 dark:border-zinc-800"><h3 class="text-base md:text-lg lg:text-xl font-semibold text-zinc-900 dark:text-zinc-50 mb-3 md:mb-4 flex items-center"><span class="icon-[tabler--bulb] mr-2 w-4 h-4 md:w-5 md:h-5 text-amber-500"></span><p>项目背景</p></h3><div class="space-y-3 md:space-y-4 text-sm md:text-base text-zinc-700 dark:text-zinc-300"><p><p>Hacker News 是我一直关注的重要资讯来源，它能持续提供新奇有趣的极客资讯。之前我每天都会花半小时左右的时间来浏览。</p></p><p><p>过年期间，我注意到 DeepSeek 非常火，就尝试使用 Cloudflare Workflow 编写了一个工作流。但生成的内容都很短，而且接口也不稳定。后来尝试 GPT 4.0 系列模型，效果也不理想，于是就暂时搁置了。</p></p><p><p>Gemini 2.0 发布后，我尝试了一下，发现生成文章的效果还不错。于是我便开发了一个 Web 界面，并加入了 RSS 订阅功能，这样就可以在上班路上用泛用型播客 App 收听 Hacker News 的资讯了。</p></p></div></div><div class="my-8 md:my-10 lg:my-12 relative"><a href="https://github.com/miantiao-me/hacker-news" target="_blank" rel="nofollow" class="block overflow-hidden rounded-xl shadow-lg hover:shadow-xl transition-shadow duration-300"><img src="https://github.html.zone/miantiao-me/hacker-news" alt="Hacker News 播客项目预览" class="w-full h-auto transform hover:scale-[1.02] transition-transform duration-500"/></a><div class="absolute -bottom-3 -right-3 bg-zinc-100 dark:bg-zinc-800 px-3 py-1.5 md:px-4 md:py-2 rounded-lg shadow-md text-xs md:text-sm font-medium text-zinc-700 dark:text-zinc-300 border border-zinc-200 dark:border-zinc-700"><p><span class="icon-[tabler--photo] mr-1 md:mr-1.5 w-3 h-3 md:w-4 md:h-4 inline-block align-text-bottom"></span>项目预览</p></div></div><div class="grid grid-cols-1 md:grid-cols-2 gap-4 md:gap-6 my-8 md:my-10 lg:my-12"><div class="bg-gradient-to-br from-zinc-50 to-zinc-100 dark:from-zinc-900 dark:to-zinc-800 rounded-xl p-4 md:p-6 shadow-xs border border-zinc-200 dark:border-zinc-800"><h2 class="text-lg md:text-xl lg:text-2xl font-bold text-zinc-900 dark:text-zinc-50 mb-4 md:mb-6 flex items-center"><span class="icon-[tabler--star] mr-2 w-5 h-5 md:w-6 md:h-6 text-amber-500"></span><p>主要特性</p></h2><ul class="space-y-2 md:space-y-3"><li class="flex items-start"><span class="icon-[tabler--robot] mr-2 md:mr-3 w-4 h-4 md:w-5 md:h-5 text-zinc-700 dark:text-zinc-300 mt-0.5"></span><span class="text-sm md:text-base text-zinc-700 dark:text-zinc-300">自动抓取 Hacker News 每日热门文章</span></li><li class="flex items-start"><span class="icon-[tabler--target] mr-2 md:mr-3 w-4 h-4 md:w-5 md:h-5 text-zinc-700 dark:text-zinc-300 mt-0.5"></span><span class="text-sm md:text-base text-zinc-700 dark:text-zinc-300">使用 AI 智能总结文章内容和评论</span></li><li class="flex items-start"><span class="icon-[tabler--microphone] mr-2 md:mr-3 w-4 h-4 md:w-5 md:h-5 text-zinc-700 dark:text-zinc-300 mt-0.5"></span><span class="text-sm md:text-base text-zinc-700 dark:text-zinc-300">通过 Edge TTS 生成中文播报</span></li><li class="flex items-start"><span class="icon-[tabler--device-mobile] mr-2 md:mr-3 w-4 h-4 md:w-5 md:h-5 text-zinc-700 dark:text-zinc-300 mt-0.5"></span><span class="text-sm md:text-base text-zinc-700 dark:text-zinc-300">支持网页和播客 App 收听</span></li><li class="flex items-start"><span class="icon-[tabler--refresh] mr-2 md:mr-3 w-4 h-4 md:w-5 md:h-5 text-zinc-700 dark:text-zinc-300 mt-0.5"></span><span class="text-sm md:text-base text-zinc-700 dark:text-zinc-300">每日自动更新</span></li><li class="flex items-start"><span class="icon-[tabler--file-text] mr-2 md:mr-3 w-4 h-4 md:w-5 md:h-5 text-zinc-700 dark:text-zinc-300 mt-0.5"></span><span class="text-sm md:text-base text-zinc-700 dark:text-zinc-300">提供文章摘要和完整播报文本</span></li></ul></div><div class="bg-gradient-to-br from-zinc-50 to-zinc-100 dark:from-zinc-900 dark:to-zinc-800 rounded-xl p-4 md:p-6 shadow-xs border border-zinc-200 dark:border-zinc-800"><h2 class="text-lg md:text-xl lg:text-2xl font-bold text-zinc-900 dark:text-zinc-50 mb-4 md:mb-6 flex items-center"><span class="icon-[tabler--stack-2] mr-2 w-5 h-5 md:w-6 md:h-6 text-teal-500"></span><p>技术栈</p></h2><ul class="space-y-2 md:space-y-3"><li class="flex items-start"><span class="icon-[tabler--brand-nextjs] mr-2 md:mr-3 w-4 h-4 md:w-5 md:h-5 text-zinc-700 dark:text-zinc-300 mt-0.5"></span><span class="text-sm md:text-base text-zinc-700 dark:text-zinc-300">Next.js 应用框架</span></li><li class="flex items-start"><span class="icon-[tabler--cloud] mr-2 md:mr-3 w-4 h-4 md:w-5 md:h-5 text-zinc-700 dark:text-zinc-300 mt-0.5"></span><span class="text-sm md:text-base text-zinc-700 dark:text-zinc-300">Cloudflare Workers 部署和运行环境</span></li><li class="flex items-start"><span class="icon-[tabler--volume] mr-2 md:mr-3 w-4 h-4 md:w-5 md:h-5 text-zinc-700 dark:text-zinc-300 mt-0.5"></span><span class="text-sm md:text-base text-zinc-700 dark:text-zinc-300">Edge TTS 语音合成</span></li><li class="flex items-start"><span class="icon-[tabler--brain] mr-2 md:mr-3 w-4 h-4 md:w-5 md:h-5 text-zinc-700 dark:text-zinc-300 mt-0.5"></span><span class="text-sm md:text-base text-zinc-700 dark:text-zinc-300">OpenAI API 内容生成</span></li><li class="flex items-start"><span class="icon-[tabler--brand-tailwind] mr-2 md:mr-3 w-4 h-4 md:w-5 md:h-5 text-zinc-700 dark:text-zinc-300 mt-0.5"></span><span class="text-sm md:text-base text-zinc-700 dark:text-zinc-300">Tailwind CSS 样式处理</span></li><li class="flex items-start"><span class="icon-[tabler--components] mr-2 md:mr-3 w-4 h-4 md:w-5 md:h-5 text-zinc-700 dark:text-zinc-300 mt-0.5"></span><span class="text-sm md:text-base text-zinc-700 dark:text-zinc-300">shadcn UI 组件库</span></li></ul></div></div><div class="my-8 md:my-10 lg:my-12"><h2 class="text-lg md:text-xl lg:text-2xl font-bold text-zinc-900 dark:text-zinc-50 mb-4 md:mb-6 flex items-center"><span class="icon-[tabler--arrows-right-left] mr-2 w-5 h-5 md:w-6 md:h-6 text-blue-500"></span><p>工作流程</p></h2><div class="relative"><div class="absolute left-[19px] top-0 bottom-0 w-1 bg-zinc-200 dark:bg-zinc-700"></div><ol class="relative space-y-4 md:space-y-6 pl-2"><li class="ml-10"><div class="absolute left-0 flex items-center justify-center w-8 h-8 md:w-10 md:h-10 rounded-full bg-zinc-100 border-2 border-zinc-300 dark:bg-zinc-800 dark:border-zinc-600"><span class="text-zinc-800 dark:text-zinc-200 text-sm md:text-base font-bold">1</span></div><h3 class="text-base md:text-lg lg:text-xl font-semibold text-zinc-900 dark:text-zinc-100">定时抓取 Hacker News 热门文章</h3><p class="mt-1 text-sm md:text-base text-zinc-600 dark:text-zinc-400">每日自动收集 Hacker News 上最受欢迎的帖子</p></li><li class="ml-10"><div class="absolute left-0 flex items-center justify-center w-8 h-8 md:w-10 md:h-10 rounded-full bg-zinc-100 border-2 border-zinc-300 dark:bg-zinc-800 dark:border-zinc-600"><span class="text-zinc-800 dark:text-zinc-200 text-sm md:text-base font-bold">2</span></div><h3 class="text-base md:text-lg lg:text-xl font-semibold text-zinc-900 dark:text-zinc-100">使用 AI 生成中文摘要和播报文稿</h3><p class="mt-1 text-sm md:text-base text-zinc-600 dark:text-zinc-400">通过 Gemini 2.0 AI 将英文内容智能翻译并总结为中文</p></li><li class="ml-10"><div class="absolute left-0 flex items-center justify-center w-8 h-8 md:w-10 md:h-10 rounded-full bg-zinc-100 border-2 border-zinc-300 dark:bg-zinc-800 dark:border-zinc-600"><span class="text-zinc-800 dark:text-zinc-200 text-sm md:text-base font-bold">3</span></div><h3 class="text-base md:text-lg lg:text-xl font-semibold text-zinc-900 dark:text-zinc-100">通过 Edge TTS 转换为音频</h3><p class="mt-1 text-sm md:text-base text-zinc-600 dark:text-zinc-400">将生成的文本转换为自然流畅的语音播报</p></li><li class="ml-10"><div class="absolute left-0 flex items-center justify-center w-8 h-8 md:w-10 md:h-10 rounded-full bg-zinc-100 border-2 border-zinc-300 dark:bg-zinc-800 dark:border-zinc-600"><span class="text-zinc-800 dark:text-zinc-200 text-sm md:text-base font-bold">4</span></div><h3 class="text-base md:text-lg lg:text-xl font-semibold text-zinc-900 dark:text-zinc-100">存储到 Cloudflare R2 和 KV</h3><p class="mt-1 text-sm md:text-base text-zinc-600 dark:text-zinc-400">将生成的内容和音频存储在高效、低成本的云端存储系统中</p></li><li class="ml-10"><div class="absolute left-0 flex items-center justify-center w-8 h-8 md:w-10 md:h-10 rounded-full bg-zinc-100 border-2 border-zinc-300 dark:bg-zinc-800 dark:border-zinc-600"><span class="text-zinc-800 dark:text-zinc-200 text-sm md:text-base font-bold">5</span></div><h3 class="text-base md:text-lg lg:text-xl font-semibold text-zinc-900 dark:text-zinc-100">通过 RSS feed 和网页提供访问</h3><p class="mt-1 text-sm md:text-base text-zinc-600 dark:text-zinc-400">用户可以通过网页浏览或在任何播客应用中订阅收听</p></li></ol></div></div><div class="bg-zinc-50 dark:bg-zinc-900 rounded-xl p-4 md:p-6 my-8 md:my-10 lg:my-12 shadow-xs border border-zinc-200 dark:border-zinc-800"><h2 class="text-lg md:text-xl lg:text-2xl font-bold text-zinc-900 dark:text-zinc-50 mb-3 md:mb-4 flex items-center"><span class="icon-[tabler--route] mr-2 w-5 h-5 md:w-6 md:h-6 text-violet-500"></span><p>未来计划</p></h2><div class="text-sm md:text-base md:leading-6 text-zinc-700 dark:text-zinc-300 leading-relaxed"><p><p>目前 TTS 使用的是 Edge TTS，只有一个女声。理想情况下，使用男声和女声进行对话的形式可能会更好。豆包的 TTS 音色很不错，但它是收费的。等后续有时间，我会考虑改进这部分。</p></p></div></div><div class="my-8 md:my-10 lg:my-12"><div class="border-t border-zinc-200 dark:border-zinc-800 pt-6 md:pt-8 pb-3 md:pb-4"><h2 class="text-lg md:text-xl lg:text-2xl font-bold text-zinc-900 dark:text-zinc-50 mb-3 md:mb-4 flex items-center"><span class="icon-[tabler--thumb-up] mr-2 w-5 h-5 md:w-6 md:h-6 text-green-500"></span><p>推荐工具</p></h2><p class="text-sm md:text-base text-zinc-700 dark:text-zinc-300 mb-3 md:mb-4"><p>最后，推荐一下 Cloudflare Workflow，一个很棒的 Workflow 运行平台。</p></p><div class="mt-3 md:mt-4 overflow-hidden rounded-xl border border-zinc-200 dark:border-zinc-700 shadow-xs hover:shadow-md transition-shadow duration-300"><img src="https://static.miantiao.me/share/2025/0ZROOg/postspark_export_2025-03-22_21-39-43.webp" alt="Cloudflare Workflow" class="w-full h-auto"/><div class="bg-zinc-50 dark:bg-zinc-900 p-3 md:p-4"><div class="font-medium text-sm md:text-base text-zinc-900 dark:text-zinc-100">Cloudflare Workflow</div><div class="text-xs md:text-sm text-zinc-600 dark:text-zinc-400">高效、低成本的云函数工作流平台</div></div></div></div></div></div><img src="https://c.statcounter.com/9823304/0/92a1d06c/1/" alt="" width="1" height="1" referrerPolicy="no-referrer-when-downgrade" />]]></content:encoded>
    </item>
    <item>
      <title>RSS.Beauty - 让 RSS 变漂亮!</title>
      <link>https://miantiao.me/posts/rss-beauty/</link>
      <guid>https://miantiao.me/posts/rss-beauty/</guid>
      <description>基于XSLT技术的RSS美化工具，将普通RSS/Atom订阅源转换为美观易读的响应式界面，提升阅读体验</description>
      <pubDate>Tue, 31 Dec 2024 14:50:48 GMT</pubDate>
      <content:encoded><![CDATA[<link rel="preload" as="image" href="https://github.html.zone/miantiao-me/RSS.Beauty"/><div class="not-prose"><div class="relative mb-10 rounded-2xl overflow-hidden bg-gradient-to-br from-zinc-100 to-zinc-200 dark:from-zinc-800 dark:to-zinc-900"><div class="absolute inset-0 opacity-10 bg-[radial-gradient(#888_1px,transparent_1px)] [background-size:20px_20px]"></div><div class="px-5 py-14 md:py-16 lg:py-20 md:px-10 text-center relative z-10"><h2 class="text-3xl md:text-4xl lg:text-5xl font-bold mb-4 bg-gradient-to-r from-zinc-900 to-zinc-600 dark:from-zinc-100 dark:to-zinc-400 text-transparent bg-clip-text">RSS.Beauty</h2><p class="text-lg md:text-xl lg:text-2xl font-medium text-zinc-700 dark:text-zinc-300 mb-6">让 RSS 订阅源焕发新生的现代工具</p><div class="flex flex-wrap gap-3 justify-center mt-6 md:mt-8"><a href="https://rss.beauty/" target="_blank" rel="nofollow" class="inline-flex items-center px-4 py-2 md:px-5 md:py-2.5 rounded-lg bg-zinc-900 text-zinc-100 hover:bg-zinc-800 dark:bg-zinc-100 dark:text-zinc-900 dark:hover:bg-zinc-200 transition-all duration-200 font-medium"><span class="icon-[tabler--arrow-right] mr-2"></span><p>立即体验</p></a><a href="https://github.com/miantiao-me/RSS.Beauty" target="_blank" rel="nofollow" class="inline-flex items-center px-4 py-2 md:px-5 md:py-2.5 rounded-lg border border-zinc-300 dark:border-zinc-700 hover:bg-zinc-100 dark:hover:bg-zinc-800 transition-all duration-200 font-medium"><span class="icon-[tabler--brand-github] mr-2"></span><p>GitHub 仓库</p></a></div></div></div><div class="grid grid-cols-1 md:grid-cols-3 gap-6 md:gap-8 mb-12 md:mb-16"><div class="col-span-2"><div class="text-base md:text-lg md:leading-7 leading-relaxed text-zinc-800 dark:text-zinc-200 mb-6 md:mb-8"><p class="text-lg md:text-xl font-medium mb-5 border-l-4 border-zinc-400 dark:border-zinc-600 pl-4 py-1 italic"><span class="icon-[tabler--quote] inline-block mr-2 opacity-70"></span><span>拖了快半年的工具终于做完了。</span></p><p class="mb-5"><p><a href="https://rss.beauty/" target="_blank" rel="nofollow" class="text-zinc-900 dark:text-zinc-100 font-medium underline decoration-zinc-400 dark:decoration-zinc-600 hover:decoration-zinc-900 dark:hover:decoration-zinc-300 transition-all duration-200">RSS.Beauty</a> 是一个基于 XSLT 技术的 RSS 美化工具，可以将普通的 RSS/Atom 订阅源转换成美观的阅读界面。让信息流变得更加优雅，阅读体验更加舒适。</p></p></div></div><div class="bg-zinc-100 dark:bg-zinc-800 rounded-xl p-5 md:p-6 self-start"><h3 class="text-base md:text-lg font-semibold mb-3 md:mb-4 flex items-center"><span class="icon-[tabler--stack-2] mr-2 text-zinc-600 dark:text-zinc-400"></span><p>技术栈</p></h3><ul class="space-y-2 md:space-y-3"><li class="flex items-start"><span class="icon-[tabler--brand-astro] mt-1 mr-2 text-zinc-700 dark:text-zinc-300"></span><a href="https://astro.build" target="_blank" rel="nofollow" class="hover:text-zinc-900 dark:hover:text-zinc-100 transition-colors">Astro</a></li><li class="flex items-start"><span class="icon-[tabler--brand-tailwind] mt-1 mr-2 text-zinc-700 dark:text-zinc-300"></span><a href="https://tailwindcss.com" target="_blank" rel="nofollow" class="hover:text-zinc-900 dark:hover:text-zinc-100 transition-colors">TailwindCSS</a></li><li class="flex items-start"><span class="icon-[tabler--code] mt-1 mr-2 text-zinc-700 dark:text-zinc-300"></span><a href="https://www.w3.org/TR/xslt/" target="_blank" rel="nofollow" class="hover:text-zinc-900 dark:hover:text-zinc-100 transition-colors">XSLT</a></li></ul></div></div><div class="mb-12 md:mb-16"><h2 class="text-2xl md:text-3xl font-bold mb-6 md:mb-8 flex items-center border-b border-zinc-200 dark:border-zinc-800 pb-3 md:pb-4"><span class="icon-[tabler--star] mr-2 md:mr-3 text-zinc-600 dark:text-zinc-400"></span><p>主要特性</p></h2><div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 md:gap-6"><div class="bg-white dark:bg-zinc-900 p-5 md:p-6 rounded-lg border border-zinc-200 dark:border-zinc-800 hover:shadow-md transition-shadow duration-300"><div class="rounded-full w-10 h-10 md:w-12 md:h-12 flex items-center justify-center bg-zinc-100 dark:bg-zinc-800 mb-3 md:mb-4"><span class="icon-[tabler--palette] text-lg md:text-xl text-zinc-700 dark:text-zinc-300"></span></div><h3 class="text-base md:text-lg font-semibold mb-2">精美的阅读界面</h3><p class="text-sm md:text-base text-zinc-600 dark:text-zinc-400">完全重新设计的视觉体验，让RSS阅读变得赏心悦目</p></div><div class="bg-white dark:bg-zinc-900 p-5 md:p-6 rounded-lg border border-zinc-200 dark:border-zinc-800 hover:shadow-md transition-shadow duration-300"><div class="rounded-full w-10 h-10 md:w-12 md:h-12 flex items-center justify-center bg-zinc-100 dark:bg-zinc-800 mb-3 md:mb-4"><span class="icon-[tabler--refresh] text-lg md:text-xl text-zinc-700 dark:text-zinc-300"></span></div><h3 class="text-base md:text-lg font-semibold mb-2">支持 RSS 2.0 和 Atom 1.0</h3><p class="text-sm md:text-base text-zinc-600 dark:text-zinc-400">全面兼容主流的RSS标准，无需担心格式问题</p></div><div class="bg-white dark:bg-zinc-900 p-5 md:p-6 rounded-lg border border-zinc-200 dark:border-zinc-800 hover:shadow-md transition-shadow duration-300"><div class="rounded-full w-10 h-10 md:w-12 md:h-12 flex items-center justify-center bg-zinc-100 dark:bg-zinc-800 mb-3 md:mb-4"><span class="icon-[tabler--device-mobile] text-lg md:text-xl text-zinc-700 dark:text-zinc-300"></span></div><h3 class="text-base md:text-lg font-semibold mb-2">响应式设计</h3><p class="text-sm md:text-base text-zinc-600 dark:text-zinc-400">完美适配各种设备，让移动端阅读同样舒适</p></div><div class="bg-white dark:bg-zinc-900 p-5 md:p-6 rounded-lg border border-zinc-200 dark:border-zinc-800 hover:shadow-md transition-shadow duration-300"><div class="rounded-full w-10 h-10 md:w-12 md:h-12 flex items-center justify-center bg-zinc-100 dark:bg-zinc-800 mb-3 md:mb-4"><span class="icon-[tabler--plug] text-lg md:text-xl text-zinc-700 dark:text-zinc-300"></span></div><h3 class="text-base md:text-lg font-semibold mb-2">一键订阅</h3><p class="text-sm md:text-base text-zinc-600 dark:text-zinc-400">快速添加到主流RSS阅读器，简化订阅流程</p></div><div class="bg-white dark:bg-zinc-900 p-5 md:p-6 rounded-lg border border-zinc-200 dark:border-zinc-800 hover:shadow-md transition-shadow duration-300"><div class="rounded-full w-10 h-10 md:w-12 md:h-12 flex items-center justify-center bg-zinc-100 dark:bg-zinc-800 mb-3 md:mb-4"><span class="icon-[tabler--server] text-lg md:text-xl text-zinc-700 dark:text-zinc-300"></span></div><h3 class="text-base md:text-lg font-semibold mb-2">支持自部署</h3><p class="text-sm md:text-base text-zinc-600 dark:text-zinc-400">可部署到自己的服务器，保持完全的控制权</p></div></div></div><div class="mb-12 md:mb-16"><h2 class="text-2xl md:text-3xl font-bold mb-5 md:mb-6 flex items-center border-b border-zinc-200 dark:border-zinc-800 pb-3 md:pb-4"><span class="icon-[tabler--rocket] mr-2 md:mr-3 text-zinc-600 dark:text-zinc-400"></span><p>快速开始</p></h2><div class="bg-white dark:bg-zinc-900 rounded-lg border border-zinc-200 dark:border-zinc-800 p-5 md:p-6 lg:p-8"><p class="mb-5 md:mb-6 text-zinc-700 dark:text-zinc-300">访问 <a href="https://rss.beauty" target="_blank" rel="nofollow" class="text-zinc-900 dark:text-zinc-100 font-medium underline decoration-zinc-400 dark:decoration-zinc-600 hover:decoration-zinc-900 dark:hover:decoration-zinc-300 transition-all duration-200">RSS.Beauty</a> 并输入任意 RSS 订阅源链接即可体验。</p><div class="relative rounded-lg overflow-hidden border border-zinc-300 dark:border-zinc-700"><div class="absolute top-0 left-0 right-0 h-8 bg-zinc-100 dark:bg-zinc-800 flex items-center px-4"><div class="flex space-x-2"><div class="w-3 h-3 rounded-full bg-zinc-300 dark:bg-zinc-600"></div><div class="w-3 h-3 rounded-full bg-zinc-300 dark:bg-zinc-600"></div><div class="w-3 h-3 rounded-full bg-zinc-300 dark:bg-zinc-600"></div></div></div><div class="pt-8 bg-zinc-50 dark:bg-zinc-900 p-3 md:p-4"><form class="flex flex-col md:flex-row gap-3 md:gap-4 items-center justify-center p-4 md:p-6 lg:p-8" action="https://rss.beauty/rss" method="GET" target="_blank"><div class="flex-1 min-w-0"><div class="relative"><input type="text" placeholder="https://feed.miantiao.me/" class="w-full px-3 md:px-4 py-2.5 md:py-3 border border-zinc-300 dark:border-zinc-700 rounded-lg bg-white dark:bg-zinc-800 text-zinc-900 dark:text-zinc-100" name="url"/><span class="absolute right-3 top-1/2 -translate-y-1/2 text-zinc-400 dark:text-zinc-500 icon-[tabler--rss]"></span></div></div><button type="submit" class="px-5 md:px-6 py-2.5 md:py-3 bg-zinc-900 dark:bg-zinc-100 text-zinc-100 dark:text-zinc-900 rounded-lg font-medium whitespace-nowrap hover:bg-zinc-800 dark:hover:bg-zinc-200 transition-colors">美化</button></form></div></div></div></div><div class="mb-8"><h2 class="text-2xl md:text-3xl font-bold mb-5 md:mb-6 flex items-center border-b border-zinc-200 dark:border-zinc-800 pb-3 md:pb-4"><span class="icon-[tabler--code] mr-2 md:mr-3 text-zinc-600 dark:text-zinc-400"></span><p>开源项目</p></h2><div class="bg-white dark:bg-zinc-900 rounded-lg border border-zinc-200 dark:border-zinc-800 p-5 md:p-6 lg:p-8"><p class="mb-5 md:mb-6 text-zinc-700 dark:text-zinc-300">这是一个开源项目，欢迎贡献代码或提出建议。</p><div class="bg-zinc-50 dark:bg-zinc-800 rounded-lg p-4 md:p-5 border border-zinc-200 dark:border-zinc-700 flex items-center justify-between"><div class="flex items-center"><span class="icon-[tabler--brand-github] text-lg md:text-xl mr-2 md:mr-3 text-zinc-700 dark:text-zinc-300"></span><a href="https://github.com/miantiao-me/RSS.Beauty" target="_blank" rel="nofollow" class="text-zinc-900 dark:text-zinc-100 font-medium hover:underline">miantiao-me/RSS.Beauty</a></div><a href="https://github.com/miantiao-me/RSS.Beauty" target="_blank" rel="nofollow" class="px-3 md:px-4 py-1.5 md:py-2 bg-zinc-200 dark:bg-zinc-700 rounded-lg text-sm font-medium hover:bg-zinc-300 dark:hover:bg-zinc-600 transition-colors">查看项目</a></div><div class="mt-5 md:mt-6"><a href="https://github.com/miantiao-me/RSS.Beauty" target="_blank" rel="nofollow"><img src="https://github.html.zone/miantiao-me/RSS.Beauty" alt="RSS.Beauty GitHub 预览" class="w-full h-auto rounded-lg border border-zinc-200 dark:border-zinc-700 hover:opacity-95 transition-opacity"/></a></div></div></div><div class="mt-10 md:mt-12 text-center"><div class="inline-block rounded-full px-3 md:px-4 py-1 bg-zinc-100 dark:bg-zinc-800 text-zinc-600 dark:text-zinc-400 text-xs md:text-sm mb-3 md:mb-4"><span class="icon-[tabler--stars] inline-block mr-1"></span><span>开始使用 RSS.Beauty</span></div><h3 class="text-xl md:text-2xl lg:text-3xl font-bold mb-3 md:mb-4">让您的 RSS 阅读体验焕然一新</h3><p class="text-zinc-600 dark:text-zinc-400 mb-5 md:mb-6 max-w-2xl mx-auto text-sm md:text-base">体验 RSS.Beauty 带来的全新阅读感受，将枯燥的信息流变成精美的阅读界面。</p><a href="https://rss.beauty/" target="_blank" rel="nofollow" class="inline-flex items-center px-4 md:px-6 py-2 md:py-3 rounded-lg bg-zinc-900 text-zinc-100 hover:bg-zinc-800 dark:bg-zinc-100 dark:text-zinc-900 dark:hover:bg-zinc-200 transition-all duration-200 font-medium"><p>立即访问 RSS.Beauty</p><span class="icon-[tabler--arrow-right] ml-2"></span></a></div></div><img src="https://c.statcounter.com/9823304/0/92a1d06c/1/" alt="" width="1" height="1" referrerPolicy="no-referrer-when-downgrade" />]]></content:encoded>
    </item>
    <item>
      <title>在浏览器中轻松运行 Python 程序</title>
      <link>https://miantiao.me/posts/run-python-in-browser/</link>
      <guid>https://miantiao.me/posts/run-python-in-browser/</guid>
      <description>利用WebAssembly和Pyodide技术在浏览器中运行Python程序，以Microsoft的MarkItDown工具为例，实现无需安装即可将Office文件转换为Markdown的在线服务。</description>
      <pubDate>Sat, 21 Dec 2024 09:57:34 GMT</pubDate>
      <content:encoded><![CDATA[<link rel="preload" as="image" href="https://www.html.zone/markitdown.png"/><p>最近，微软开源了一个名为 <a href="https://github.com/microsoft/markitdown" rel="nofollow noopener noreferrer" target="_blank">MarkItDown</a> 的程序，可以将 Office 文件转换为 Markdown 格式。这个项目一经发布就迅速登上了 GitHub 热门榜。</p>
<p>然而，由于 MarkItDown 是一个 Python 程序，对于非技术用户来说使用起来可能有些困难。为了解决这个问题，我想到了利用 WebAssembly 技术在浏览器中直接运行 Python 代码。</p>
<p>在浏览器内运行 Python 的开源程序是 Pyodide，使用 WebAssembly 移植了 CPython，所以 Python 的语法都是支持的。 Cloudflare 的 Python Worker 也使用的 Pyodide。</p>
<blockquote>
<p>Pyodide 是 CPython 的一个移植版本，用于 WebAssembly/Emscripten。</p>
<p>Pyodide 使得在浏览器中使用 micropip 安装和运行 Python 包成为可能。任何在 PyPi 上有可用 wheel 文件的纯 Python 包都被支持。</p>
<p>许多具有 C 扩展的包也已被移植以供 Pyodide 使用。这些包括许多通用包，如 regex、PyYAML、lxml，以及包括 NumPy、pandas、SciPy、Matplotlib 和 scikit-learn 在内的科学 Python 包。Pyodide 配备了强大的 JavaScript ⟺ Python 外部函数接口，使得您可以在代码中自由地混合这两种语言，几乎没有摩擦。这包括对错误处理、async/await 的全面支持，以及更多功能。</p>
<p>在浏览器中使用时，Python 可以完全访问 Web API。</p>
</blockquote>
<p>尝试了一下运行 MarkItDown 没想到异常的顺利，看来 WebAssembly 真的是浏览器的未来。</p>
<p>遇到的主要挑战和解决方案：</p>
<ol>
<li>
<p><strong>文件传输问题</strong>：如何将用户选择的文件传递给 Worker 中的 Python 运行时？</p>
<ul>
<li>解决方案：利用 Pyodide 提供的方案，将浏览器文件转换为 ArrayBuffer，然后写入 Emscripten 文件系统的本地缓存。</li>
</ul>
</li>
<li>
<p><strong>依赖安装问题</strong>：PyPI 在中国大陆访问受限。</p>
<ul>
<li>解决方案：使用 Cloudflare 搭建 PyPI 镜像，详见：<a href="https://github.com/miantiao-me/cloudflare-pypi-mirror" rel="nofollow noopener noreferrer" target="_blank">Cloudflare PyPI Mirror</a>。</li>
</ul>
</li>
</ol>
<p>最终，我们成功实现了一个完全运行在浏览器中的 MarkItDown 工具。欢迎访问 <a href="https://www.html.zone/markitdown/" rel="nofollow noopener noreferrer" target="_blank">Office File to Markdown</a> 进行体验。</p>
<p><a href="https://www.html.zone/markitdown/" rel="nofollow noopener noreferrer" target="_blank"><img src="https://www.html.zone/markitdown.png" alt="Office File to Markdown"/></a></p>
<p>最后放出一下 Worker 中运行 Python 的核心代码：</p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#2e3440ff;color:#d8dee9ff" tabindex="0" data-language="js" data-theme="nord"><code data-language="js" data-theme="nord" style="display:grid"><span data-line=""><span style="color:#88C0D0">importScripts</span><span style="color:#D8DEE9FF">(</span><span style="color:#ECEFF4">&#x27;</span><span style="color:#A3BE8C">https://testingcf.jsdelivr.net/pyodide/v0.26.4/full/pyodide.js</span><span style="color:#ECEFF4">&#x27;</span><span style="color:#D8DEE9FF">)</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#616E88">// npmmirror 支持 pyodide ，但是不支持 pyodide 下的 zip 包</span></span>
<span data-line=""><span style="color:#616E88">// importScripts(&#x27;https://registry.npmmirror.com/pyodide/0.26.4/files/pyodide.js&#x27;)</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#81A1C1">async</span><span style="color:#81A1C1"> function</span><span style="color:#88C0D0"> loadPyodideAndPackages</span><span style="color:#ECEFF4">()</span><span style="color:#ECEFF4"> {</span></span>
<span data-line=""><span style="color:#81A1C1">  const</span><span style="color:#D8DEE9"> pyodide</span><span style="color:#81A1C1"> =</span><span style="color:#81A1C1"> await</span><span style="color:#88C0D0"> loadPyodide</span><span style="color:#D8DEE9FF">()</span></span>
<span data-line=""><span style="color:#D8DEE9">  globalThis</span><span style="color:#ECEFF4">.</span><span style="color:#D8DEE9">pyodide</span><span style="color:#81A1C1"> =</span><span style="color:#D8DEE9"> pyodide</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#81A1C1">  await</span><span style="color:#D8DEE9"> pyodide</span><span style="color:#ECEFF4">.</span><span style="color:#88C0D0">loadPackage</span><span style="color:#D8DEE9FF">(</span><span style="color:#ECEFF4">&#x27;</span><span style="color:#A3BE8C">micropip</span><span style="color:#ECEFF4">&#x27;</span><span style="color:#D8DEE9FF">)</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#81A1C1">  const</span><span style="color:#D8DEE9"> micropip</span><span style="color:#81A1C1"> =</span><span style="color:#D8DEE9"> pyodide</span><span style="color:#ECEFF4">.</span><span style="color:#88C0D0">pyimport</span><span style="color:#D8DEE9FF">(</span><span style="color:#ECEFF4">&#x27;</span><span style="color:#A3BE8C">micropip</span><span style="color:#ECEFF4">&#x27;</span><span style="color:#D8DEE9FF">)</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#616E88">  // 需要支持 PEP 691 和跨域， 目前 tuna 支持 PEP 691，但不支持跨域 https://github.com/tuna/issues/issues/2092</span></span>
<span data-line=""><span style="color:#616E88">  // micropip.set_index_urls([</span></span>
<span data-line=""><span style="color:#616E88">  //   &#x27;https://pypi.your.domains/pypi/simple&#x27;,</span></span>
<span data-line=""><span style="color:#616E88">  // ])</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#81A1C1">  await</span><span style="color:#D8DEE9"> micropip</span><span style="color:#ECEFF4">.</span><span style="color:#88C0D0">install</span><span style="color:#D8DEE9FF">(</span><span style="color:#ECEFF4">&#x27;</span><span style="color:#A3BE8C">markitdown==0.0.1a2</span><span style="color:#ECEFF4">&#x27;</span><span style="color:#D8DEE9FF">)</span></span>
<span data-line=""><span style="color:#ECEFF4">}</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#81A1C1">const</span><span style="color:#D8DEE9"> pyodideReadyPromise</span><span style="color:#81A1C1"> =</span><span style="color:#88C0D0"> loadPyodideAndPackages</span><span style="color:#D8DEE9FF">()</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#D8DEE9">globalThis</span><span style="color:#ECEFF4">.</span><span style="color:#88C0D0">onmessage</span><span style="color:#81A1C1"> =</span><span style="color:#81A1C1"> async</span><span style="color:#ECEFF4"> (</span><span style="color:#D8DEE9">event</span><span style="color:#ECEFF4">)</span><span style="color:#81A1C1"> =&gt;</span><span style="color:#ECEFF4"> {</span></span>
<span data-line=""><span style="color:#81A1C1">  await</span><span style="color:#D8DEE9"> pyodideReadyPromise</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#81A1C1">  const</span><span style="color:#D8DEE9"> file</span><span style="color:#81A1C1"> =</span><span style="color:#D8DEE9"> event</span><span style="color:#ECEFF4">.</span><span style="color:#D8DEE9">data</span></span>
<span data-line=""><span style="color:#81A1C1">  try</span><span style="color:#ECEFF4"> {</span></span>
<span data-line=""><span style="color:#D8DEE9">    console</span><span style="color:#ECEFF4">.</span><span style="color:#88C0D0">log</span><span style="color:#D8DEE9FF">(</span><span style="color:#ECEFF4">&#x27;</span><span style="color:#A3BE8C">file</span><span style="color:#ECEFF4">&#x27;</span><span style="color:#ECEFF4">,</span><span style="color:#D8DEE9"> file</span><span style="color:#D8DEE9FF">)</span></span>
<span data-line=""><span style="color:#81A1C1">    const</span><span style="color:#D8DEE9"> startTime</span><span style="color:#81A1C1"> =</span><span style="color:#D8DEE9"> Date</span><span style="color:#ECEFF4">.</span><span style="color:#88C0D0">now</span><span style="color:#D8DEE9FF">()</span></span>
<span data-line=""><span style="color:#D8DEE9">    globalThis</span><span style="color:#ECEFF4">.</span><span style="color:#D8DEE9">pyodide</span><span style="color:#ECEFF4">.</span><span style="color:#D8DEE9">FS</span><span style="color:#ECEFF4">.</span><span style="color:#88C0D0">writeFile</span><span style="color:#D8DEE9FF">(</span><span style="color:#ECEFF4">`</span><span style="color:#A3BE8C">/</span><span style="color:#81A1C1">${</span><span style="color:#D8DEE9">file</span><span style="color:#ECEFF4">.</span><span style="color:#D8DEE9">filename</span><span style="color:#81A1C1">}</span><span style="color:#ECEFF4">`</span><span style="color:#ECEFF4">,</span><span style="color:#D8DEE9"> file</span><span style="color:#ECEFF4">.</span><span style="color:#D8DEE9">buffer</span><span style="color:#D8DEE9FF">)</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#81A1C1">    await</span><span style="color:#D8DEE9"> globalThis</span><span style="color:#ECEFF4">.</span><span style="color:#D8DEE9">pyodide</span><span style="color:#ECEFF4">.</span><span style="color:#88C0D0">runPythonAsync</span><span style="color:#D8DEE9FF">(</span><span style="color:#ECEFF4">`</span></span>
<span data-line=""><span style="color:#A3BE8C">from markitdown import MarkItDown</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#A3BE8C">markitdown = MarkItDown()</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#A3BE8C">result = markitdown.convert(&quot;/</span><span style="color:#81A1C1">${</span><span style="color:#D8DEE9">file</span><span style="color:#ECEFF4">.</span><span style="color:#D8DEE9">filename</span><span style="color:#81A1C1">}</span><span style="color:#A3BE8C">&quot;)</span></span>
<span data-line=""><span style="color:#A3BE8C">print(result.text_content)</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#A3BE8C">with open(&quot;/</span><span style="color:#81A1C1">${</span><span style="color:#D8DEE9">file</span><span style="color:#ECEFF4">.</span><span style="color:#D8DEE9">filename</span><span style="color:#81A1C1">}</span><span style="color:#A3BE8C">.md&quot;, &quot;w&quot;) as file:</span></span>
<span data-line=""><span style="color:#A3BE8C">  file.write(result.text_content)</span></span>
<span data-line=""><span style="color:#ECEFF4">`</span><span style="color:#D8DEE9FF">)</span></span>
<span data-line=""><span style="color:#D8DEE9">    globalThis</span><span style="color:#ECEFF4">.</span><span style="color:#88C0D0">postMessage</span><span style="color:#D8DEE9FF">(</span><span style="color:#ECEFF4">{</span></span>
<span data-line=""><span style="color:#88C0D0">      filename</span><span style="color:#ECEFF4">:</span><span style="color:#ECEFF4"> `</span><span style="color:#81A1C1">${</span><span style="color:#D8DEE9">file</span><span style="color:#ECEFF4">.</span><span style="color:#D8DEE9">filename</span><span style="color:#81A1C1">}</span><span style="color:#A3BE8C">.md</span><span style="color:#ECEFF4">`</span><span style="color:#ECEFF4">,</span></span>
<span data-line=""><span style="color:#88C0D0">      content</span><span style="color:#ECEFF4">:</span><span style="color:#D8DEE9"> globalThis</span><span style="color:#ECEFF4">.</span><span style="color:#D8DEE9">pyodide</span><span style="color:#ECEFF4">.</span><span style="color:#D8DEE9">FS</span><span style="color:#ECEFF4">.</span><span style="color:#88C0D0">readFile</span><span style="color:#D8DEE9FF">(</span><span style="color:#ECEFF4">`</span><span style="color:#A3BE8C">/</span><span style="color:#81A1C1">${</span><span style="color:#D8DEE9">file</span><span style="color:#ECEFF4">.</span><span style="color:#D8DEE9">filename</span><span style="color:#81A1C1">}</span><span style="color:#A3BE8C">.md</span><span style="color:#ECEFF4">`</span><span style="color:#ECEFF4">,</span><span style="color:#ECEFF4"> {</span><span style="color:#88C0D0"> encoding</span><span style="color:#ECEFF4">:</span><span style="color:#ECEFF4"> &#x27;</span><span style="color:#A3BE8C">utf8</span><span style="color:#ECEFF4">&#x27;</span><span style="color:#ECEFF4"> }</span><span style="color:#D8DEE9FF">)</span><span style="color:#ECEFF4">,</span></span>
<span data-line=""><span style="color:#88C0D0">      time</span><span style="color:#ECEFF4">:</span><span style="color:#D8DEE9"> Date</span><span style="color:#ECEFF4">.</span><span style="color:#88C0D0">now</span><span style="color:#D8DEE9FF">() </span><span style="color:#81A1C1">-</span><span style="color:#D8DEE9"> startTime</span><span style="color:#ECEFF4">,</span></span>
<span data-line=""><span style="color:#ECEFF4">    }</span><span style="color:#D8DEE9FF">)</span></span>
<span data-line=""><span style="color:#ECEFF4">  }</span></span>
<span data-line=""><span style="color:#81A1C1">  catch</span><span style="color:#D8DEE9FF"> (</span><span style="color:#D8DEE9">error</span><span style="color:#D8DEE9FF">) </span><span style="color:#ECEFF4">{</span></span>
<span data-line=""><span style="color:#D8DEE9">    globalThis</span><span style="color:#ECEFF4">.</span><span style="color:#88C0D0">postMessage</span><span style="color:#D8DEE9FF">(</span><span style="color:#ECEFF4">{</span><span style="color:#88C0D0"> error</span><span style="color:#ECEFF4">:</span><span style="color:#D8DEE9"> error</span><span style="color:#ECEFF4">.</span><span style="color:#D8DEE9">message</span><span style="color:#81A1C1"> ||</span><span style="color:#ECEFF4"> &#x27;</span><span style="color:#A3BE8C">convert error</span><span style="color:#ECEFF4">&#x27;</span><span style="color:#ECEFF4">,</span><span style="color:#88C0D0"> filename</span><span style="color:#ECEFF4">:</span><span style="color:#D8DEE9"> file</span><span style="color:#ECEFF4">.</span><span style="color:#D8DEE9">filename</span><span style="color:#ECEFF4"> }</span><span style="color:#D8DEE9FF">)</span></span>
<span data-line=""><span style="color:#ECEFF4">  }</span></span>
<span data-line=""><span style="color:#ECEFF4">}</span></span></code></pre></figure><img src="https://c.statcounter.com/9823304/0/92a1d06c/1/" alt="" width="1" height="1" referrerPolicy="no-referrer-when-downgrade" />]]></content:encoded>
    </item>
    <item>
      <title>使用 Cloudflare Snippets 搭建一个不限流量的 Docker 镜像</title>
      <link>https://miantiao.me/posts/cloudflare-docker-mirror/</link>
      <guid>https://miantiao.me/posts/cloudflare-docker-mirror/</guid>
      <description>利用Cloudflare Snippets搭建高效无限流量的Docker镜像加速服务，支持多种容器注册表，包括Docker Hub和GitHub Container Registry</description>
      <pubDate>Sat, 21 Dec 2024 08:17:40 GMT</pubDate>
      <content:encoded><![CDATA[<p>Cloudflare Workers 搭建 Docker 镜像个人使用请求数小没啥问题。但是如果公开使用，大量的请求数还是会产生费用。</p>
<p>其实 Cloudflare 还有一个更轻量的 JS Runtime: Cloudflare Snippets, 但是也有更严格的限制：CPU 执行时间 5 ms，最大内存 2M, 最大代码量 32K。 不过拿来重写请求足够了。</p>
<p>遗憾的是 Cloudflare Snippets 目前还未对 Free 计划开放，不过<a href="https://blog.cloudflare.com/zh-cn/snippets-announcement/" rel="nofollow noopener noreferrer" target="_blank">他们博客说 Free 计划可以建 5 个 Snippets</a>。</p>
<p>如果你有 Pro 计划，拿 Cloudflare Workers 的代码稍微修改一下就可以运行, 支持 Docker Hub, Google Container Registry, GitHub Container Registry, Amazon Elastic Container Registry, Kubernetes Container Registry, Quay, Cloudsmith。</p>
<p>修改后的代码：</p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#2e3440ff;color:#d8dee9ff" tabindex="0" data-language="js" data-theme="nord"><code data-language="js" data-theme="nord" style="display:grid"><span data-line=""><span style="color:#616E88">// 原代码: https://github.com/ciiiii/cloudflare-docker-proxy/blob/master/src/index.js</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#81A1C1">const</span><span style="color:#D8DEE9"> CUSTOM_DOMAIN</span><span style="color:#81A1C1"> =</span><span style="color:#ECEFF4"> &#x27;</span><span style="color:#A3BE8C">your.domains</span><span style="color:#ECEFF4">&#x27;</span></span>
<span data-line=""><span style="color:#81A1C1">const</span><span style="color:#D8DEE9"> MODE</span><span style="color:#81A1C1"> =</span><span style="color:#ECEFF4"> &#x27;</span><span style="color:#A3BE8C">production</span><span style="color:#ECEFF4">&#x27;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#81A1C1">const</span><span style="color:#D8DEE9"> dockerHub</span><span style="color:#81A1C1"> =</span><span style="color:#ECEFF4"> &#x27;</span><span style="color:#A3BE8C">https://registry-1.docker.io</span><span style="color:#ECEFF4">&#x27;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#81A1C1">const</span><span style="color:#D8DEE9"> routes</span><span style="color:#81A1C1"> =</span><span style="color:#ECEFF4"> {</span></span>
<span data-line=""><span style="color:#616E88">  // production</span></span>
<span data-line=""><span style="color:#88C0D0">  [</span><span style="color:#ECEFF4">`</span><span style="color:#A3BE8C">docker.</span><span style="color:#81A1C1">${</span><span style="color:#D8DEE9">CUSTOM_DOMAIN</span><span style="color:#81A1C1">}</span><span style="color:#ECEFF4">`</span><span style="color:#88C0D0">]</span><span style="color:#ECEFF4">:</span><span style="color:#D8DEE9"> dockerHub</span><span style="color:#ECEFF4">,</span></span>
<span data-line=""><span style="color:#88C0D0">  [</span><span style="color:#ECEFF4">`</span><span style="color:#A3BE8C">quay.</span><span style="color:#81A1C1">${</span><span style="color:#D8DEE9">CUSTOM_DOMAIN</span><span style="color:#81A1C1">}</span><span style="color:#ECEFF4">`</span><span style="color:#88C0D0">]</span><span style="color:#ECEFF4">:</span><span style="color:#ECEFF4"> &#x27;</span><span style="color:#A3BE8C">https://quay.io</span><span style="color:#ECEFF4">&#x27;</span><span style="color:#ECEFF4">,</span></span>
<span data-line=""><span style="color:#88C0D0">  [</span><span style="color:#ECEFF4">`</span><span style="color:#A3BE8C">gcr.</span><span style="color:#81A1C1">${</span><span style="color:#D8DEE9">CUSTOM_DOMAIN</span><span style="color:#81A1C1">}</span><span style="color:#ECEFF4">`</span><span style="color:#88C0D0">]</span><span style="color:#ECEFF4">:</span><span style="color:#ECEFF4"> &#x27;</span><span style="color:#A3BE8C">https://gcr.io</span><span style="color:#ECEFF4">&#x27;</span><span style="color:#ECEFF4">,</span></span>
<span data-line=""><span style="color:#88C0D0">  [</span><span style="color:#ECEFF4">`</span><span style="color:#A3BE8C">k8s-gcr.</span><span style="color:#81A1C1">${</span><span style="color:#D8DEE9">CUSTOM_DOMAIN</span><span style="color:#81A1C1">}</span><span style="color:#ECEFF4">`</span><span style="color:#88C0D0">]</span><span style="color:#ECEFF4">:</span><span style="color:#ECEFF4"> &#x27;</span><span style="color:#A3BE8C">https://k8s.gcr.io</span><span style="color:#ECEFF4">&#x27;</span><span style="color:#ECEFF4">,</span></span>
<span data-line=""><span style="color:#88C0D0">  [</span><span style="color:#ECEFF4">`</span><span style="color:#A3BE8C">k8s.</span><span style="color:#81A1C1">${</span><span style="color:#D8DEE9">CUSTOM_DOMAIN</span><span style="color:#81A1C1">}</span><span style="color:#ECEFF4">`</span><span style="color:#88C0D0">]</span><span style="color:#ECEFF4">:</span><span style="color:#ECEFF4"> &#x27;</span><span style="color:#A3BE8C">https://registry.k8s.io</span><span style="color:#ECEFF4">&#x27;</span><span style="color:#ECEFF4">,</span></span>
<span data-line=""><span style="color:#88C0D0">  [</span><span style="color:#ECEFF4">`</span><span style="color:#A3BE8C">ghcr.</span><span style="color:#81A1C1">${</span><span style="color:#D8DEE9">CUSTOM_DOMAIN</span><span style="color:#81A1C1">}</span><span style="color:#ECEFF4">`</span><span style="color:#88C0D0">]</span><span style="color:#ECEFF4">:</span><span style="color:#ECEFF4"> &#x27;</span><span style="color:#A3BE8C">https://ghcr.io</span><span style="color:#ECEFF4">&#x27;</span><span style="color:#ECEFF4">,</span></span>
<span data-line=""><span style="color:#88C0D0">  [</span><span style="color:#ECEFF4">`</span><span style="color:#A3BE8C">cloudsmith.</span><span style="color:#81A1C1">${</span><span style="color:#D8DEE9">CUSTOM_DOMAIN</span><span style="color:#81A1C1">}</span><span style="color:#ECEFF4">`</span><span style="color:#88C0D0">]</span><span style="color:#ECEFF4">:</span><span style="color:#ECEFF4"> &#x27;</span><span style="color:#A3BE8C">https://docker.cloudsmith.io</span><span style="color:#ECEFF4">&#x27;</span><span style="color:#ECEFF4">,</span></span>
<span data-line=""><span style="color:#88C0D0">  [</span><span style="color:#ECEFF4">`</span><span style="color:#A3BE8C">ecr.</span><span style="color:#81A1C1">${</span><span style="color:#D8DEE9">CUSTOM_DOMAIN</span><span style="color:#81A1C1">}</span><span style="color:#ECEFF4">`</span><span style="color:#88C0D0">]</span><span style="color:#ECEFF4">:</span><span style="color:#ECEFF4"> &#x27;</span><span style="color:#A3BE8C">https://public.ecr.aws</span><span style="color:#ECEFF4">&#x27;</span><span style="color:#ECEFF4">,</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#616E88">  // staging</span></span>
<span data-line=""><span style="color:#88C0D0">  [</span><span style="color:#ECEFF4">`</span><span style="color:#A3BE8C">docker-staging.</span><span style="color:#81A1C1">${</span><span style="color:#D8DEE9">CUSTOM_DOMAIN</span><span style="color:#81A1C1">}</span><span style="color:#ECEFF4">`</span><span style="color:#88C0D0">]</span><span style="color:#ECEFF4">:</span><span style="color:#D8DEE9"> dockerHub</span><span style="color:#ECEFF4">,</span></span>
<span data-line=""><span style="color:#ECEFF4">}</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#81A1C1">async</span><span style="color:#81A1C1"> function</span><span style="color:#88C0D0"> handleRequest</span><span style="color:#ECEFF4">(</span><span style="color:#D8DEE9">request</span><span style="color:#ECEFF4">)</span><span style="color:#ECEFF4"> {</span></span>
<span data-line=""><span style="color:#81A1C1">  const</span><span style="color:#D8DEE9"> url</span><span style="color:#81A1C1"> =</span><span style="color:#81A1C1"> new</span><span style="color:#88C0D0"> URL</span><span style="color:#D8DEE9FF">(</span><span style="color:#D8DEE9">request</span><span style="color:#ECEFF4">.</span><span style="color:#D8DEE9">url</span><span style="color:#D8DEE9FF">)</span></span>
<span data-line=""><span style="color:#81A1C1">  const</span><span style="color:#D8DEE9"> upstream</span><span style="color:#81A1C1"> =</span><span style="color:#88C0D0"> routeByHosts</span><span style="color:#D8DEE9FF">(</span><span style="color:#D8DEE9">url</span><span style="color:#ECEFF4">.</span><span style="color:#D8DEE9">hostname</span><span style="color:#D8DEE9FF">)</span></span>
<span data-line=""><span style="color:#81A1C1">  if</span><span style="color:#D8DEE9FF"> (</span><span style="color:#D8DEE9">upstream</span><span style="color:#81A1C1"> ===</span><span style="color:#ECEFF4"> &#x27;&#x27;</span><span style="color:#D8DEE9FF">) </span><span style="color:#ECEFF4">{</span></span>
<span data-line=""><span style="color:#81A1C1">    return</span><span style="color:#81A1C1"> new</span><span style="color:#88C0D0"> Response</span><span style="color:#D8DEE9FF">(</span></span>
<span data-line=""><span style="color:#D8DEE9">      JSON</span><span style="color:#ECEFF4">.</span><span style="color:#88C0D0">stringify</span><span style="color:#D8DEE9FF">(</span><span style="color:#ECEFF4">{</span></span>
<span data-line=""><span style="color:#D8DEE9">        routes</span><span style="color:#ECEFF4">,</span></span>
<span data-line=""><span style="color:#ECEFF4">      }</span><span style="color:#D8DEE9FF">)</span><span style="color:#ECEFF4">,</span></span>
<span data-line=""><span style="color:#ECEFF4">      {</span></span>
<span data-line=""><span style="color:#88C0D0">        status</span><span style="color:#ECEFF4">:</span><span style="color:#B48EAD"> 404</span><span style="color:#ECEFF4">,</span></span>
<span data-line=""><span style="color:#ECEFF4">      },</span></span>
<span data-line=""><span style="color:#D8DEE9FF">    )</span></span>
<span data-line=""><span style="color:#ECEFF4">  }</span></span>
<span data-line=""><span style="color:#81A1C1">  const</span><span style="color:#D8DEE9"> isDockerHub</span><span style="color:#81A1C1"> =</span><span style="color:#D8DEE9"> upstream</span><span style="color:#81A1C1"> ===</span><span style="color:#D8DEE9"> dockerHub</span></span>
<span data-line=""><span style="color:#81A1C1">  const</span><span style="color:#D8DEE9"> authorization</span><span style="color:#81A1C1"> =</span><span style="color:#D8DEE9"> request</span><span style="color:#ECEFF4">.</span><span style="color:#D8DEE9">headers</span><span style="color:#ECEFF4">.</span><span style="color:#88C0D0">get</span><span style="color:#D8DEE9FF">(</span><span style="color:#ECEFF4">&#x27;</span><span style="color:#A3BE8C">Authorization</span><span style="color:#ECEFF4">&#x27;</span><span style="color:#D8DEE9FF">)</span></span>
<span data-line=""><span style="color:#81A1C1">  if</span><span style="color:#D8DEE9FF"> (</span><span style="color:#D8DEE9">url</span><span style="color:#ECEFF4">.</span><span style="color:#D8DEE9">pathname</span><span style="color:#81A1C1"> ===</span><span style="color:#ECEFF4"> &#x27;</span><span style="color:#A3BE8C">/v2/</span><span style="color:#ECEFF4">&#x27;</span><span style="color:#D8DEE9FF">) </span><span style="color:#ECEFF4">{</span></span>
<span data-line=""><span style="color:#81A1C1">    const</span><span style="color:#D8DEE9"> newUrl</span><span style="color:#81A1C1"> =</span><span style="color:#81A1C1"> new</span><span style="color:#88C0D0"> URL</span><span style="color:#D8DEE9FF">(</span><span style="color:#ECEFF4">`</span><span style="color:#81A1C1">${</span><span style="color:#D8DEE9">upstream</span><span style="color:#81A1C1">}</span><span style="color:#A3BE8C">/v2/</span><span style="color:#ECEFF4">`</span><span style="color:#D8DEE9FF">)</span></span>
<span data-line=""><span style="color:#81A1C1">    const</span><span style="color:#D8DEE9"> headers</span><span style="color:#81A1C1"> =</span><span style="color:#81A1C1"> new</span><span style="color:#88C0D0"> Headers</span><span style="color:#D8DEE9FF">()</span></span>
<span data-line=""><span style="color:#81A1C1">    if</span><span style="color:#D8DEE9FF"> (</span><span style="color:#D8DEE9">authorization</span><span style="color:#D8DEE9FF">) </span><span style="color:#ECEFF4">{</span></span>
<span data-line=""><span style="color:#D8DEE9">      headers</span><span style="color:#ECEFF4">.</span><span style="color:#88C0D0">set</span><span style="color:#D8DEE9FF">(</span><span style="color:#ECEFF4">&#x27;</span><span style="color:#A3BE8C">Authorization</span><span style="color:#ECEFF4">&#x27;</span><span style="color:#ECEFF4">,</span><span style="color:#D8DEE9"> authorization</span><span style="color:#D8DEE9FF">)</span></span>
<span data-line=""><span style="color:#ECEFF4">    }</span></span>
<span data-line=""><span style="color:#616E88">    // check if need to authenticate</span></span>
<span data-line=""><span style="color:#81A1C1">    const</span><span style="color:#D8DEE9"> resp</span><span style="color:#81A1C1"> =</span><span style="color:#81A1C1"> await</span><span style="color:#88C0D0"> fetch</span><span style="color:#D8DEE9FF">(</span><span style="color:#D8DEE9">newUrl</span><span style="color:#ECEFF4">.</span><span style="color:#88C0D0">toString</span><span style="color:#D8DEE9FF">()</span><span style="color:#ECEFF4">,</span><span style="color:#ECEFF4"> {</span></span>
<span data-line=""><span style="color:#88C0D0">      method</span><span style="color:#ECEFF4">:</span><span style="color:#ECEFF4"> &#x27;</span><span style="color:#A3BE8C">GET</span><span style="color:#ECEFF4">&#x27;</span><span style="color:#ECEFF4">,</span></span>
<span data-line=""><span style="color:#D8DEE9">      headers</span><span style="color:#ECEFF4">,</span></span>
<span data-line=""><span style="color:#88C0D0">      redirect</span><span style="color:#ECEFF4">:</span><span style="color:#ECEFF4"> &#x27;</span><span style="color:#A3BE8C">follow</span><span style="color:#ECEFF4">&#x27;</span><span style="color:#ECEFF4">,</span></span>
<span data-line=""><span style="color:#ECEFF4">    }</span><span style="color:#D8DEE9FF">)</span></span>
<span data-line=""><span style="color:#81A1C1">    if</span><span style="color:#D8DEE9FF"> (</span><span style="color:#D8DEE9">resp</span><span style="color:#ECEFF4">.</span><span style="color:#D8DEE9">status</span><span style="color:#81A1C1"> ===</span><span style="color:#B48EAD"> 401</span><span style="color:#D8DEE9FF">) </span><span style="color:#ECEFF4">{</span></span>
<span data-line=""><span style="color:#81A1C1">      return</span><span style="color:#88C0D0"> responseUnauthorized</span><span style="color:#D8DEE9FF">(</span><span style="color:#D8DEE9">url</span><span style="color:#D8DEE9FF">)</span></span>
<span data-line=""><span style="color:#ECEFF4">    }</span></span>
<span data-line=""><span style="color:#81A1C1">    return</span><span style="color:#D8DEE9"> resp</span></span>
<span data-line=""><span style="color:#ECEFF4">  }</span></span>
<span data-line=""><span style="color:#616E88">  // get token</span></span>
<span data-line=""><span style="color:#81A1C1">  if</span><span style="color:#D8DEE9FF"> (</span><span style="color:#D8DEE9">url</span><span style="color:#ECEFF4">.</span><span style="color:#D8DEE9">pathname</span><span style="color:#81A1C1"> ===</span><span style="color:#ECEFF4"> &#x27;</span><span style="color:#A3BE8C">/v2/auth</span><span style="color:#ECEFF4">&#x27;</span><span style="color:#D8DEE9FF">) </span><span style="color:#ECEFF4">{</span></span>
<span data-line=""><span style="color:#81A1C1">    const</span><span style="color:#D8DEE9"> newUrl</span><span style="color:#81A1C1"> =</span><span style="color:#81A1C1"> new</span><span style="color:#88C0D0"> URL</span><span style="color:#D8DEE9FF">(</span><span style="color:#ECEFF4">`</span><span style="color:#81A1C1">${</span><span style="color:#D8DEE9">upstream</span><span style="color:#81A1C1">}</span><span style="color:#A3BE8C">/v2/</span><span style="color:#ECEFF4">`</span><span style="color:#D8DEE9FF">)</span></span>
<span data-line=""><span style="color:#81A1C1">    const</span><span style="color:#D8DEE9"> resp</span><span style="color:#81A1C1"> =</span><span style="color:#81A1C1"> await</span><span style="color:#88C0D0"> fetch</span><span style="color:#D8DEE9FF">(</span><span style="color:#D8DEE9">newUrl</span><span style="color:#ECEFF4">.</span><span style="color:#88C0D0">toString</span><span style="color:#D8DEE9FF">()</span><span style="color:#ECEFF4">,</span><span style="color:#ECEFF4"> {</span></span>
<span data-line=""><span style="color:#88C0D0">      method</span><span style="color:#ECEFF4">:</span><span style="color:#ECEFF4"> &#x27;</span><span style="color:#A3BE8C">GET</span><span style="color:#ECEFF4">&#x27;</span><span style="color:#ECEFF4">,</span></span>
<span data-line=""><span style="color:#88C0D0">      redirect</span><span style="color:#ECEFF4">:</span><span style="color:#ECEFF4"> &#x27;</span><span style="color:#A3BE8C">follow</span><span style="color:#ECEFF4">&#x27;</span><span style="color:#ECEFF4">,</span></span>
<span data-line=""><span style="color:#ECEFF4">    }</span><span style="color:#D8DEE9FF">)</span></span>
<span data-line=""><span style="color:#81A1C1">    if</span><span style="color:#D8DEE9FF"> (</span><span style="color:#D8DEE9">resp</span><span style="color:#ECEFF4">.</span><span style="color:#D8DEE9">status</span><span style="color:#81A1C1"> !==</span><span style="color:#B48EAD"> 401</span><span style="color:#D8DEE9FF">) </span><span style="color:#ECEFF4">{</span></span>
<span data-line=""><span style="color:#81A1C1">      return</span><span style="color:#D8DEE9"> resp</span></span>
<span data-line=""><span style="color:#ECEFF4">    }</span></span>
<span data-line=""><span style="color:#81A1C1">    const</span><span style="color:#D8DEE9"> authenticateStr</span><span style="color:#81A1C1"> =</span><span style="color:#D8DEE9"> resp</span><span style="color:#ECEFF4">.</span><span style="color:#D8DEE9">headers</span><span style="color:#ECEFF4">.</span><span style="color:#88C0D0">get</span><span style="color:#D8DEE9FF">(</span><span style="color:#ECEFF4">&#x27;</span><span style="color:#A3BE8C">WWW-Authenticate</span><span style="color:#ECEFF4">&#x27;</span><span style="color:#D8DEE9FF">)</span></span>
<span data-line=""><span style="color:#81A1C1">    if</span><span style="color:#D8DEE9FF"> (</span><span style="color:#D8DEE9">authenticateStr</span><span style="color:#81A1C1"> ===</span><span style="color:#81A1C1"> null</span><span style="color:#D8DEE9FF">) </span><span style="color:#ECEFF4">{</span></span>
<span data-line=""><span style="color:#81A1C1">      return</span><span style="color:#D8DEE9"> resp</span></span>
<span data-line=""><span style="color:#ECEFF4">    }</span></span>
<span data-line=""><span style="color:#81A1C1">    const</span><span style="color:#D8DEE9"> wwwAuthenticate</span><span style="color:#81A1C1"> =</span><span style="color:#88C0D0"> parseAuthenticate</span><span style="color:#D8DEE9FF">(</span><span style="color:#D8DEE9">authenticateStr</span><span style="color:#D8DEE9FF">)</span></span>
<span data-line=""><span style="color:#81A1C1">    let</span><span style="color:#D8DEE9"> scope</span><span style="color:#81A1C1"> =</span><span style="color:#D8DEE9"> url</span><span style="color:#ECEFF4">.</span><span style="color:#D8DEE9">searchParams</span><span style="color:#ECEFF4">.</span><span style="color:#88C0D0">get</span><span style="color:#D8DEE9FF">(</span><span style="color:#ECEFF4">&#x27;</span><span style="color:#A3BE8C">scope</span><span style="color:#ECEFF4">&#x27;</span><span style="color:#D8DEE9FF">)</span></span>
<span data-line=""><span style="color:#616E88">    // autocomplete repo part into scope for DockerHub library images</span></span>
<span data-line=""><span style="color:#616E88">    // Example: repository:busybox:pull =&gt; repository:library/busybox:pull</span></span>
<span data-line=""><span style="color:#81A1C1">    if</span><span style="color:#D8DEE9FF"> (</span><span style="color:#D8DEE9">scope</span><span style="color:#81A1C1"> &amp;&amp;</span><span style="color:#D8DEE9"> isDockerHub</span><span style="color:#D8DEE9FF">) </span><span style="color:#ECEFF4">{</span></span>
<span data-line=""><span style="color:#81A1C1">      const</span><span style="color:#D8DEE9"> scopeParts</span><span style="color:#81A1C1"> =</span><span style="color:#D8DEE9"> scope</span><span style="color:#ECEFF4">.</span><span style="color:#88C0D0">split</span><span style="color:#D8DEE9FF">(</span><span style="color:#ECEFF4">&#x27;</span><span style="color:#A3BE8C">:</span><span style="color:#ECEFF4">&#x27;</span><span style="color:#D8DEE9FF">)</span></span>
<span data-line=""><span style="color:#81A1C1">      if</span><span style="color:#D8DEE9FF"> (</span><span style="color:#D8DEE9">scopeParts</span><span style="color:#ECEFF4">.</span><span style="color:#D8DEE9FF">length </span><span style="color:#81A1C1">===</span><span style="color:#B48EAD"> 3</span><span style="color:#81A1C1"> &amp;&amp;</span><span style="color:#81A1C1"> !</span><span style="color:#D8DEE9">scopeParts</span><span style="color:#D8DEE9FF">[</span><span style="color:#B48EAD">1</span><span style="color:#D8DEE9FF">]</span><span style="color:#ECEFF4">.</span><span style="color:#88C0D0">includes</span><span style="color:#D8DEE9FF">(</span><span style="color:#ECEFF4">&#x27;</span><span style="color:#A3BE8C">/</span><span style="color:#ECEFF4">&#x27;</span><span style="color:#D8DEE9FF">)) </span><span style="color:#ECEFF4">{</span></span>
<span data-line=""><span style="color:#D8DEE9">        scopeParts</span><span style="color:#D8DEE9FF">[</span><span style="color:#B48EAD">1</span><span style="color:#D8DEE9FF">] </span><span style="color:#81A1C1">=</span><span style="color:#ECEFF4"> `</span><span style="color:#A3BE8C">library/</span><span style="color:#81A1C1">${</span><span style="color:#D8DEE9">scopeParts</span><span style="color:#ECEFF4">[</span><span style="color:#B48EAD">1</span><span style="color:#ECEFF4">]</span><span style="color:#81A1C1">}</span><span style="color:#ECEFF4">`</span></span>
<span data-line=""><span style="color:#D8DEE9">        scope</span><span style="color:#81A1C1"> =</span><span style="color:#D8DEE9"> scopeParts</span><span style="color:#ECEFF4">.</span><span style="color:#88C0D0">join</span><span style="color:#D8DEE9FF">(</span><span style="color:#ECEFF4">&#x27;</span><span style="color:#A3BE8C">:</span><span style="color:#ECEFF4">&#x27;</span><span style="color:#D8DEE9FF">)</span></span>
<span data-line=""><span style="color:#ECEFF4">      }</span></span>
<span data-line=""><span style="color:#ECEFF4">    }</span></span>
<span data-line=""><span style="color:#81A1C1">    return</span><span style="color:#81A1C1"> await</span><span style="color:#88C0D0"> fetchToken</span><span style="color:#D8DEE9FF">(</span><span style="color:#D8DEE9">wwwAuthenticate</span><span style="color:#ECEFF4">,</span><span style="color:#D8DEE9"> scope</span><span style="color:#ECEFF4">,</span><span style="color:#D8DEE9"> authorization</span><span style="color:#D8DEE9FF">)</span></span>
<span data-line=""><span style="color:#ECEFF4">  }</span></span>
<span data-line=""><span style="color:#616E88">  // redirect for DockerHub library images</span></span>
<span data-line=""><span style="color:#616E88">  // Example: /v2/busybox/manifests/latest =&gt; /v2/library/busybox/manifests/latest</span></span>
<span data-line=""><span style="color:#81A1C1">  if</span><span style="color:#D8DEE9FF"> (</span><span style="color:#D8DEE9">isDockerHub</span><span style="color:#D8DEE9FF">) </span><span style="color:#ECEFF4">{</span></span>
<span data-line=""><span style="color:#81A1C1">    const</span><span style="color:#D8DEE9"> pathParts</span><span style="color:#81A1C1"> =</span><span style="color:#D8DEE9"> url</span><span style="color:#ECEFF4">.</span><span style="color:#D8DEE9">pathname</span><span style="color:#ECEFF4">.</span><span style="color:#88C0D0">split</span><span style="color:#D8DEE9FF">(</span><span style="color:#ECEFF4">&#x27;</span><span style="color:#A3BE8C">/</span><span style="color:#ECEFF4">&#x27;</span><span style="color:#D8DEE9FF">)</span></span>
<span data-line=""><span style="color:#81A1C1">    if</span><span style="color:#D8DEE9FF"> (</span><span style="color:#D8DEE9">pathParts</span><span style="color:#ECEFF4">.</span><span style="color:#D8DEE9FF">length </span><span style="color:#81A1C1">===</span><span style="color:#B48EAD"> 5</span><span style="color:#D8DEE9FF">) </span><span style="color:#ECEFF4">{</span></span>
<span data-line=""><span style="color:#D8DEE9">      pathParts</span><span style="color:#ECEFF4">.</span><span style="color:#88C0D0">splice</span><span style="color:#D8DEE9FF">(</span><span style="color:#B48EAD">2</span><span style="color:#ECEFF4">,</span><span style="color:#B48EAD"> 0</span><span style="color:#ECEFF4">,</span><span style="color:#ECEFF4"> &#x27;</span><span style="color:#A3BE8C">library</span><span style="color:#ECEFF4">&#x27;</span><span style="color:#D8DEE9FF">)</span></span>
<span data-line=""><span style="color:#81A1C1">      const</span><span style="color:#D8DEE9"> redirectUrl</span><span style="color:#81A1C1"> =</span><span style="color:#81A1C1"> new</span><span style="color:#88C0D0"> URL</span><span style="color:#D8DEE9FF">(</span><span style="color:#D8DEE9">url</span><span style="color:#D8DEE9FF">)</span></span>
<span data-line=""><span style="color:#D8DEE9">      redirectUrl</span><span style="color:#ECEFF4">.</span><span style="color:#D8DEE9">pathname</span><span style="color:#81A1C1"> =</span><span style="color:#D8DEE9"> pathParts</span><span style="color:#ECEFF4">.</span><span style="color:#88C0D0">join</span><span style="color:#D8DEE9FF">(</span><span style="color:#ECEFF4">&#x27;</span><span style="color:#A3BE8C">/</span><span style="color:#ECEFF4">&#x27;</span><span style="color:#D8DEE9FF">)</span></span>
<span data-line=""><span style="color:#81A1C1">      return</span><span style="color:#D8DEE9"> Response</span><span style="color:#ECEFF4">.</span><span style="color:#88C0D0">redirect</span><span style="color:#D8DEE9FF">(</span><span style="color:#D8DEE9">redirectUrl</span><span style="color:#ECEFF4">,</span><span style="color:#B48EAD"> 301</span><span style="color:#D8DEE9FF">)</span></span>
<span data-line=""><span style="color:#ECEFF4">    }</span></span>
<span data-line=""><span style="color:#ECEFF4">  }</span></span>
<span data-line=""><span style="color:#616E88">  // foward requests</span></span>
<span data-line=""><span style="color:#81A1C1">  const</span><span style="color:#D8DEE9"> newUrl</span><span style="color:#81A1C1"> =</span><span style="color:#81A1C1"> new</span><span style="color:#88C0D0"> URL</span><span style="color:#D8DEE9FF">(</span><span style="color:#D8DEE9">upstream</span><span style="color:#81A1C1"> +</span><span style="color:#D8DEE9"> url</span><span style="color:#ECEFF4">.</span><span style="color:#D8DEE9">pathname</span><span style="color:#D8DEE9FF">)</span></span>
<span data-line=""><span style="color:#81A1C1">  const</span><span style="color:#D8DEE9"> newReq</span><span style="color:#81A1C1"> =</span><span style="color:#81A1C1"> new</span><span style="color:#88C0D0"> Request</span><span style="color:#D8DEE9FF">(</span><span style="color:#D8DEE9">newUrl</span><span style="color:#ECEFF4">,</span><span style="color:#ECEFF4"> {</span></span>
<span data-line=""><span style="color:#88C0D0">    method</span><span style="color:#ECEFF4">:</span><span style="color:#D8DEE9"> request</span><span style="color:#ECEFF4">.</span><span style="color:#D8DEE9">method</span><span style="color:#ECEFF4">,</span></span>
<span data-line=""><span style="color:#88C0D0">    headers</span><span style="color:#ECEFF4">:</span><span style="color:#D8DEE9"> request</span><span style="color:#ECEFF4">.</span><span style="color:#D8DEE9">headers</span><span style="color:#ECEFF4">,</span></span>
<span data-line=""><span style="color:#88C0D0">    redirect</span><span style="color:#ECEFF4">:</span><span style="color:#ECEFF4"> &#x27;</span><span style="color:#A3BE8C">follow</span><span style="color:#ECEFF4">&#x27;</span><span style="color:#ECEFF4">,</span></span>
<span data-line=""><span style="color:#ECEFF4">  }</span><span style="color:#D8DEE9FF">)</span></span>
<span data-line=""><span style="color:#81A1C1">  const</span><span style="color:#D8DEE9"> resp</span><span style="color:#81A1C1"> =</span><span style="color:#81A1C1"> await</span><span style="color:#88C0D0"> fetch</span><span style="color:#D8DEE9FF">(</span><span style="color:#D8DEE9">newReq</span><span style="color:#D8DEE9FF">)</span></span>
<span data-line=""><span style="color:#81A1C1">  if</span><span style="color:#D8DEE9FF"> (</span><span style="color:#D8DEE9">resp</span><span style="color:#ECEFF4">.</span><span style="color:#D8DEE9">status</span><span style="color:#81A1C1"> ===</span><span style="color:#B48EAD"> 401</span><span style="color:#D8DEE9FF">) </span><span style="color:#ECEFF4">{</span></span>
<span data-line=""><span style="color:#81A1C1">    return</span><span style="color:#88C0D0"> responseUnauthorized</span><span style="color:#D8DEE9FF">(</span><span style="color:#D8DEE9">url</span><span style="color:#D8DEE9FF">)</span></span>
<span data-line=""><span style="color:#ECEFF4">  }</span></span>
<span data-line=""><span style="color:#81A1C1">  return</span><span style="color:#D8DEE9"> resp</span></span>
<span data-line=""><span style="color:#ECEFF4">}</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#81A1C1">function</span><span style="color:#88C0D0"> routeByHosts</span><span style="color:#ECEFF4">(</span><span style="color:#D8DEE9">host</span><span style="color:#ECEFF4">)</span><span style="color:#ECEFF4"> {</span></span>
<span data-line=""><span style="color:#81A1C1">  if</span><span style="color:#D8DEE9FF"> (</span><span style="color:#D8DEE9">host</span><span style="color:#81A1C1"> in</span><span style="color:#D8DEE9"> routes</span><span style="color:#D8DEE9FF">) </span><span style="color:#ECEFF4">{</span></span>
<span data-line=""><span style="color:#81A1C1">    return</span><span style="color:#D8DEE9"> routes</span><span style="color:#D8DEE9FF">[</span><span style="color:#D8DEE9">host</span><span style="color:#D8DEE9FF">]</span></span>
<span data-line=""><span style="color:#ECEFF4">  }</span></span>
<span data-line=""><span style="color:#81A1C1">  if</span><span style="color:#D8DEE9FF"> (</span><span style="color:#D8DEE9">MODE</span><span style="color:#81A1C1"> ===</span><span style="color:#ECEFF4"> &#x27;</span><span style="color:#A3BE8C">debug</span><span style="color:#ECEFF4">&#x27;</span><span style="color:#D8DEE9FF">) </span><span style="color:#ECEFF4">{</span></span>
<span data-line=""><span style="color:#81A1C1">    return</span><span style="color:#D8DEE9"> dockerHub</span></span>
<span data-line=""><span style="color:#ECEFF4">  }</span></span>
<span data-line=""><span style="color:#81A1C1">  return</span><span style="color:#ECEFF4"> &#x27;&#x27;</span></span>
<span data-line=""><span style="color:#ECEFF4">}</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#81A1C1">function</span><span style="color:#88C0D0"> parseAuthenticate</span><span style="color:#ECEFF4">(</span><span style="color:#D8DEE9">authenticateStr</span><span style="color:#ECEFF4">)</span><span style="color:#ECEFF4"> {</span></span>
<span data-line=""><span style="color:#616E88">  // sample: Bearer realm=&quot;https://auth.ipv6.docker.com/token&quot;,service=&quot;registry.docker.io&quot;</span></span>
<span data-line=""><span style="color:#616E88">  // match strings after =&quot; and before &quot;</span></span>
<span data-line=""><span style="color:#81A1C1">  const</span><span style="color:#D8DEE9"> re</span><span style="color:#81A1C1"> =</span><span style="color:#ECEFF4"> /(?&lt;=</span><span style="color:#EBCB8B">=&quot;</span><span style="color:#ECEFF4">)(?:</span><span style="color:#EBCB8B">\\.</span><span style="color:#81A1C1">|</span><span style="color:#ECEFF4">[</span><span style="color:#81A1C1">^</span><span style="color:#EBCB8B">&quot;\\</span><span style="color:#ECEFF4">])</span><span style="color:#81A1C1">*</span><span style="color:#ECEFF4">(?=</span><span style="color:#EBCB8B">&quot;</span><span style="color:#ECEFF4">)/</span><span style="color:#81A1C1">g</span></span>
<span data-line=""><span style="color:#81A1C1">  const</span><span style="color:#D8DEE9"> matches</span><span style="color:#81A1C1"> =</span><span style="color:#D8DEE9"> authenticateStr</span><span style="color:#ECEFF4">.</span><span style="color:#88C0D0">match</span><span style="color:#D8DEE9FF">(</span><span style="color:#D8DEE9">re</span><span style="color:#D8DEE9FF">)</span></span>
<span data-line=""><span style="color:#81A1C1">  if</span><span style="color:#D8DEE9FF"> (</span><span style="color:#D8DEE9">matches</span><span style="color:#81A1C1"> ==</span><span style="color:#81A1C1"> null</span><span style="color:#81A1C1"> ||</span><span style="color:#D8DEE9"> matches</span><span style="color:#ECEFF4">.</span><span style="color:#D8DEE9FF">length </span><span style="color:#81A1C1">&lt;</span><span style="color:#B48EAD"> 2</span><span style="color:#D8DEE9FF">) </span><span style="color:#ECEFF4">{</span></span>
<span data-line=""><span style="color:#81A1C1">    throw</span><span style="color:#81A1C1"> new</span><span style="color:#88C0D0"> Error</span><span style="color:#D8DEE9FF">(</span><span style="color:#ECEFF4">`</span><span style="color:#A3BE8C">invalid Www-Authenticate Header: </span><span style="color:#81A1C1">${</span><span style="color:#D8DEE9">authenticateStr</span><span style="color:#81A1C1">}</span><span style="color:#ECEFF4">`</span><span style="color:#D8DEE9FF">)</span></span>
<span data-line=""><span style="color:#ECEFF4">  }</span></span>
<span data-line=""><span style="color:#81A1C1">  return</span><span style="color:#ECEFF4"> {</span></span>
<span data-line=""><span style="color:#88C0D0">    realm</span><span style="color:#ECEFF4">:</span><span style="color:#D8DEE9"> matches</span><span style="color:#D8DEE9FF">[</span><span style="color:#B48EAD">0</span><span style="color:#D8DEE9FF">]</span><span style="color:#ECEFF4">,</span></span>
<span data-line=""><span style="color:#88C0D0">    service</span><span style="color:#ECEFF4">:</span><span style="color:#D8DEE9"> matches</span><span style="color:#D8DEE9FF">[</span><span style="color:#B48EAD">1</span><span style="color:#D8DEE9FF">]</span><span style="color:#ECEFF4">,</span></span>
<span data-line=""><span style="color:#ECEFF4">  }</span></span>
<span data-line=""><span style="color:#ECEFF4">}</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#81A1C1">async</span><span style="color:#81A1C1"> function</span><span style="color:#88C0D0"> fetchToken</span><span style="color:#ECEFF4">(</span><span style="color:#D8DEE9">wwwAuthenticate</span><span style="color:#ECEFF4">,</span><span style="color:#D8DEE9"> scope</span><span style="color:#ECEFF4">,</span><span style="color:#D8DEE9"> authorization</span><span style="color:#ECEFF4">)</span><span style="color:#ECEFF4"> {</span></span>
<span data-line=""><span style="color:#81A1C1">  const</span><span style="color:#D8DEE9"> url</span><span style="color:#81A1C1"> =</span><span style="color:#81A1C1"> new</span><span style="color:#88C0D0"> URL</span><span style="color:#D8DEE9FF">(</span><span style="color:#D8DEE9">wwwAuthenticate</span><span style="color:#ECEFF4">.</span><span style="color:#D8DEE9">realm</span><span style="color:#D8DEE9FF">)</span></span>
<span data-line=""><span style="color:#81A1C1">  if</span><span style="color:#D8DEE9FF"> (</span><span style="color:#D8DEE9">wwwAuthenticate</span><span style="color:#ECEFF4">.</span><span style="color:#D8DEE9">service</span><span style="color:#ECEFF4">.</span><span style="color:#D8DEE9FF">length) </span><span style="color:#ECEFF4">{</span></span>
<span data-line=""><span style="color:#D8DEE9">    url</span><span style="color:#ECEFF4">.</span><span style="color:#D8DEE9">searchParams</span><span style="color:#ECEFF4">.</span><span style="color:#88C0D0">set</span><span style="color:#D8DEE9FF">(</span><span style="color:#ECEFF4">&#x27;</span><span style="color:#A3BE8C">service</span><span style="color:#ECEFF4">&#x27;</span><span style="color:#ECEFF4">,</span><span style="color:#D8DEE9"> wwwAuthenticate</span><span style="color:#ECEFF4">.</span><span style="color:#D8DEE9">service</span><span style="color:#D8DEE9FF">)</span></span>
<span data-line=""><span style="color:#ECEFF4">  }</span></span>
<span data-line=""><span style="color:#81A1C1">  if</span><span style="color:#D8DEE9FF"> (</span><span style="color:#D8DEE9">scope</span><span style="color:#D8DEE9FF">) </span><span style="color:#ECEFF4">{</span></span>
<span data-line=""><span style="color:#D8DEE9">    url</span><span style="color:#ECEFF4">.</span><span style="color:#D8DEE9">searchParams</span><span style="color:#ECEFF4">.</span><span style="color:#88C0D0">set</span><span style="color:#D8DEE9FF">(</span><span style="color:#ECEFF4">&#x27;</span><span style="color:#A3BE8C">scope</span><span style="color:#ECEFF4">&#x27;</span><span style="color:#ECEFF4">,</span><span style="color:#D8DEE9"> scope</span><span style="color:#D8DEE9FF">)</span></span>
<span data-line=""><span style="color:#ECEFF4">  }</span></span>
<span data-line=""><span style="color:#81A1C1">  const</span><span style="color:#D8DEE9"> headers</span><span style="color:#81A1C1"> =</span><span style="color:#81A1C1"> new</span><span style="color:#88C0D0"> Headers</span><span style="color:#D8DEE9FF">()</span></span>
<span data-line=""><span style="color:#81A1C1">  if</span><span style="color:#D8DEE9FF"> (</span><span style="color:#D8DEE9">authorization</span><span style="color:#D8DEE9FF">) </span><span style="color:#ECEFF4">{</span></span>
<span data-line=""><span style="color:#D8DEE9">    headers</span><span style="color:#ECEFF4">.</span><span style="color:#88C0D0">set</span><span style="color:#D8DEE9FF">(</span><span style="color:#ECEFF4">&#x27;</span><span style="color:#A3BE8C">Authorization</span><span style="color:#ECEFF4">&#x27;</span><span style="color:#ECEFF4">,</span><span style="color:#D8DEE9"> authorization</span><span style="color:#D8DEE9FF">)</span></span>
<span data-line=""><span style="color:#ECEFF4">  }</span></span>
<span data-line=""><span style="color:#81A1C1">  return</span><span style="color:#81A1C1"> await</span><span style="color:#88C0D0"> fetch</span><span style="color:#D8DEE9FF">(</span><span style="color:#D8DEE9">url</span><span style="color:#ECEFF4">,</span><span style="color:#ECEFF4"> {</span></span>
<span data-line=""><span style="color:#88C0D0">    method</span><span style="color:#ECEFF4">:</span><span style="color:#ECEFF4"> &#x27;</span><span style="color:#A3BE8C">GET</span><span style="color:#ECEFF4">&#x27;</span><span style="color:#ECEFF4">,</span></span>
<span data-line=""><span style="color:#D8DEE9">    headers</span></span>
<span data-line=""><span style="color:#ECEFF4">  }</span><span style="color:#D8DEE9FF">)</span></span>
<span data-line=""><span style="color:#ECEFF4">}</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#81A1C1">function</span><span style="color:#88C0D0"> responseUnauthorized</span><span style="color:#ECEFF4">(</span><span style="color:#D8DEE9">url</span><span style="color:#ECEFF4">)</span><span style="color:#ECEFF4"> {</span></span>
<span data-line=""><span style="color:#81A1C1">  const</span><span style="color:#D8DEE9"> headers</span><span style="color:#81A1C1"> =</span><span style="color:#81A1C1"> new</span><span style="color:#D8DEE9FF"> (</span><span style="color:#D8DEE9">Headers</span><span style="color:#D8DEE9FF">)()</span></span>
<span data-line=""><span style="color:#81A1C1">  if</span><span style="color:#D8DEE9FF"> (</span><span style="color:#D8DEE9">MODE</span><span style="color:#81A1C1"> ===</span><span style="color:#ECEFF4"> &#x27;</span><span style="color:#A3BE8C">debug</span><span style="color:#ECEFF4">&#x27;</span><span style="color:#D8DEE9FF">) </span><span style="color:#ECEFF4">{</span></span>
<span data-line=""><span style="color:#D8DEE9">    headers</span><span style="color:#ECEFF4">.</span><span style="color:#88C0D0">set</span><span style="color:#D8DEE9FF">(</span></span>
<span data-line=""><span style="color:#ECEFF4">      &#x27;</span><span style="color:#A3BE8C">Www-Authenticate</span><span style="color:#ECEFF4">&#x27;</span><span style="color:#ECEFF4">,</span></span>
<span data-line=""><span style="color:#ECEFF4">      `</span><span style="color:#A3BE8C">Bearer realm=&quot;http://</span><span style="color:#81A1C1">${</span><span style="color:#D8DEE9">url</span><span style="color:#ECEFF4">.</span><span style="color:#D8DEE9">host</span><span style="color:#81A1C1">}</span><span style="color:#A3BE8C">/v2/auth&quot;,service=&quot;cloudflare-docker-proxy&quot;</span><span style="color:#ECEFF4">`</span><span style="color:#ECEFF4">,</span></span>
<span data-line=""><span style="color:#D8DEE9FF">    )</span></span>
<span data-line=""><span style="color:#ECEFF4">  }</span></span>
<span data-line=""><span style="color:#81A1C1">  else</span><span style="color:#ECEFF4"> {</span></span>
<span data-line=""><span style="color:#D8DEE9">    headers</span><span style="color:#ECEFF4">.</span><span style="color:#88C0D0">set</span><span style="color:#D8DEE9FF">(</span></span>
<span data-line=""><span style="color:#ECEFF4">      &#x27;</span><span style="color:#A3BE8C">Www-Authenticate</span><span style="color:#ECEFF4">&#x27;</span><span style="color:#ECEFF4">,</span></span>
<span data-line=""><span style="color:#ECEFF4">      `</span><span style="color:#A3BE8C">Bearer realm=&quot;https://</span><span style="color:#81A1C1">${</span><span style="color:#D8DEE9">url</span><span style="color:#ECEFF4">.</span><span style="color:#D8DEE9">hostname</span><span style="color:#81A1C1">}</span><span style="color:#A3BE8C">/v2/auth&quot;,service=&quot;cloudflare-docker-proxy&quot;</span><span style="color:#ECEFF4">`</span><span style="color:#ECEFF4">,</span></span>
<span data-line=""><span style="color:#D8DEE9FF">    )</span></span>
<span data-line=""><span style="color:#ECEFF4">  }</span></span>
<span data-line=""><span style="color:#81A1C1">  return</span><span style="color:#81A1C1"> new</span><span style="color:#88C0D0"> Response</span><span style="color:#D8DEE9FF">(</span><span style="color:#D8DEE9">JSON</span><span style="color:#ECEFF4">.</span><span style="color:#88C0D0">stringify</span><span style="color:#D8DEE9FF">(</span><span style="color:#ECEFF4">{</span></span>
<span data-line=""><span style="color:#88C0D0">    message</span><span style="color:#ECEFF4">:</span><span style="color:#ECEFF4"> &#x27;</span><span style="color:#A3BE8C">UNAUTHORIZED</span><span style="color:#ECEFF4">&#x27;</span></span>
<span data-line=""><span style="color:#ECEFF4">  }</span><span style="color:#D8DEE9FF">)</span><span style="color:#ECEFF4">,</span><span style="color:#ECEFF4"> {</span></span>
<span data-line=""><span style="color:#88C0D0">    status</span><span style="color:#ECEFF4">:</span><span style="color:#B48EAD"> 401</span><span style="color:#ECEFF4">,</span></span>
<span data-line=""><span style="color:#D8DEE9">    headers</span><span style="color:#ECEFF4">,</span></span>
<span data-line=""><span style="color:#ECEFF4">  }</span><span style="color:#D8DEE9FF">)</span></span>
<span data-line=""><span style="color:#ECEFF4">}</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#81A1C1">export</span><span style="color:#81A1C1"> default</span><span style="color:#ECEFF4"> {</span></span>
<span data-line=""><span style="color:#88C0D0">  fetch</span><span style="color:#ECEFF4">:</span><span style="color:#D8DEE9"> handleRequest</span><span style="color:#ECEFF4">,</span></span>
<span data-line=""><span style="color:#ECEFF4">}</span></span></code></pre></figure><img src="https://c.statcounter.com/9823304/0/92a1d06c/1/" alt="" width="1" height="1" referrerPolicy="no-referrer-when-downgrade" />]]></content:encoded>
    </item>
  </channel>
</rss>
