<?xml version="1.0" encoding="UTF-8"?><?xml-stylesheet href="/style.xsl" type="text/xsl"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>面条实验室</title><description>折腾些什么玩意</description><link>https://miantiao.me/</link><item><title>用 AI Agent 搞了个全自动 AI 周刊</title><link>https://miantiao.me/posts/aigc-weekly/</link><guid isPermaLink="true">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>
&lt;section class=&quot;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-sm dark:from-zinc-900 dark:via-zinc-800 dark:to-zinc-900 sm:-mx-6 sm:p-12 md:p-16&quot;&gt;&lt;div class=&quot;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&quot;&gt;&lt;/div&gt;&lt;div class=&quot;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&quot;&gt;&lt;/div&gt;&lt;div class=&quot;relative&quot;&gt;&lt;div class=&quot;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&quot;&gt;&lt;span class=&quot;icon-[tabler--robot] h-4 w-4&quot;&gt;&lt;/span&gt;&lt;span&gt;Agentic AI 实验&lt;/span&gt;&lt;/div&gt;&lt;h2 class=&quot;mb-6 text-2xl font-bold tracking-tight text-zinc-900 dark:text-zinc-100 sm:text-3xl md:text-4xl&quot;&gt;&lt;p&gt;当 AI 成为编辑&lt;br/&gt;
&lt;span class=&quot;text-zinc-500 dark:text-zinc-400&quot;&gt;周刊从此自动运转&lt;/span&gt;&lt;/p&gt;&lt;/h2&gt;&lt;p class=&quot;max-w-2xl text-lg leading-relaxed text-zinc-600 dark:text-zinc-400&quot;&gt;&lt;p&gt;最近我做了一个有点意思的项目——&lt;strong class=&quot;font-semibold text-zinc-900 dark:text-zinc-100&quot;&gt;AIGC Weekly&lt;/strong&gt;。这不仅是一份关注 AIGC（人工智能生成内容）领域的周刊，更是一次关于 &lt;strong class=&quot;font-semibold text-zinc-900 dark:text-zinc-100&quot;&gt;Agentic AI（代理智能）&lt;/strong&gt; 的深度实验。&lt;/p&gt;&lt;/p&gt;&lt;/div&gt;&lt;/section&gt;
&lt;p class=&quot;text-lg leading-relaxed text-zinc-700 dark:text-zinc-300&quot;&gt;&lt;p&gt;今天想和大家聊聊，我为什么要做这个项目，以及它是如何通过一群 AI Agent 自动运转起来的。&lt;/p&gt;&lt;/p&gt;
&lt;figure class=&quot;not-prose my-12 overflow-hidden rounded-xl shadow-lg ring-1 ring-zinc-200 dark:ring-zinc-800&quot;&gt;&lt;img src=&quot;https://aigc-weekly.agi.li/opengraph-image.png&quot; alt=&quot;AIGC Weekly&quot; class=&quot;w-full&quot;/&gt;&lt;/figure&gt;

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

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

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

&lt;section class=&quot;not-prose my-16&quot;&gt;&lt;div class=&quot;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&quot;&gt;&lt;div class=&quot;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&quot;&gt;&lt;span class=&quot;icon-[tabler--source-code] h-4 w-4&quot;&gt;&lt;/span&gt;&lt;p&gt;开源项目&lt;/p&gt;&lt;/div&gt;&lt;h2 class=&quot;mb-4 text-2xl font-bold text-zinc-900 dark:text-zinc-100 sm:text-3xl&quot;&gt;开始探索 AIGC Weekly&lt;/h2&gt;&lt;p class=&quot;mx-auto mb-10 max-w-xl text-lg leading-relaxed text-zinc-600 dark:text-zinc-400&quot;&gt;&lt;p&gt;如果你对 &lt;strong class=&quot;text-zinc-900 dark:text-zinc-100&quot;&gt;AI Agent 开发&lt;/strong&gt;、&lt;strong class=&quot;text-zinc-900 dark:text-zinc-100&quot;&gt;Next.js 实战&lt;/strong&gt; 或者 &lt;strong class=&quot;text-zinc-900 dark:text-zinc-100&quot;&gt;Serverless 架构&lt;/strong&gt; 感兴趣，欢迎来看看！&lt;/p&gt;&lt;/p&gt;&lt;div class=&quot;mt-4 flex flex-col justify-center gap-4 sm:flex-row&quot;&gt;&lt;a href=&quot;https://aigc-weekly.agi.li/&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener noreferrer&quot; title=&quot;访问 AIGC Weekly 周刊&quot; class=&quot;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&quot;&gt;&lt;span class=&quot;icon-[tabler--external-link] h-5 w-5 transition-transform group-hover:-translate-y-0.5 group-hover:translate-x-0.5&quot;&gt;&lt;/span&gt;&lt;p&gt;访问周刊&lt;/p&gt;&lt;/a&gt;&lt;a href=&quot;https://github.com/miantiao-me/aigc-weekly&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener noreferrer&quot; title=&quot;查看 AIGC Weekly GitHub 仓库&quot; class=&quot;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&quot;&gt;&lt;span class=&quot;icon-[tabler--brand-github] h-5 w-5&quot;&gt;&lt;/span&gt;&lt;p&gt;GitHub 仓库&lt;/p&gt;&lt;/a&gt;&lt;/div&gt;&lt;/div&gt;&lt;/section&gt;
&lt;p class=&quot;mt-4 text-center text-lg text-zinc-600 dark:text-zinc-400&quot;&gt;&lt;p&gt;期待在这个由 AI 驱动的小小角落里，与你相遇。✨&lt;/p&gt;&lt;/p&gt;&lt;img src=&quot;https://c.statcounter.com/9823304/0/92a1d06c/1/&quot; alt=&quot;stat&quot; referrerPolicy=&quot;no-referrer-when-downgrade&quot; /&gt;</content:encoded></item><item><title>hink - 只需 10 行代码的短链接系统</title><link>https://miantiao.me/posts/hink/</link><guid isPermaLink="true">https://miantiao.me/posts/hink/</guid><description>一个用 Git 和 Serverless 平台实现的极简短链接系统，代码量不到 10 行。</description><pubDate>Sun, 31 Aug 2025 04:50:55 GMT</pubDate><content:encoded>&lt;p&gt;和大家分享一个最近做的小工具——&lt;a href=&quot;https://github.com/miantiao-me/hink&quot; rel=&quot;nofollow&quot; target=&quot;_blank&quot;&gt;&lt;strong&gt;hink&lt;/strong&gt;&lt;/a&gt;，一个用不到 10 行代码实现的短链接系统。&lt;/p&gt;
&lt;p&gt;这个工具利用 Git 和 Serverless 平台，实现了短链接生成和访问统计功能。&lt;/p&gt;
&lt;h2 id=&quot;核心原理git-提交哈希值的妙用&quot;&gt;核心原理：Git 提交哈希值的妙用&lt;/h2&gt;
&lt;p&gt;hink 的核心思路很简单：用 Git 的空提交哈希值作为短链接的唯一标识符，把原始长链接存储在提交信息中。访问短链接时，系统通过 GitHub 的 &lt;code&gt;.patch&lt;/code&gt; 文件接口读取提交信息，提取长链接并重定向。结合云平台的 WAF（Web 应用防火墙）分析面板，还能实现访问统计。&lt;/p&gt;
&lt;p&gt;目前，这个方案已经在以下平台测试通过：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Cloudflare Workers/Snippets + Cloudflare WAF (Pro)&lt;/li&gt;
&lt;li&gt;腾讯云 EdgeOne (Free)&lt;/li&gt;
&lt;li&gt;阿里云 ESA (Free)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;代码极简实现&quot;&gt;代码：极简实现&lt;/h2&gt;
&lt;p&gt;hink 的代码非常短小，核心逻辑只有几行。以下是针对不同平台的实现。&lt;/p&gt;
&lt;h3 id=&quot;cloudflare-workers--阿里云-esa-版本&quot;&gt;Cloudflare Workers / 阿里云 ESA 版本&lt;/h3&gt;
&lt;pre class=&quot;astro-code nord&quot; style=&quot;background-color:#2e3440ff;color:#d8dee9ff;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;js&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt; GIT_REPO&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; &amp;quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;https://github.com/miantiao-me/hink&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;export&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt; default&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;  async&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt; fetch&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;request&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;    const&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; {&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt; pathname&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; }&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt; new&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt; URL&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;request&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;url&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;    const&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt; gitPatch&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; `&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;${&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;GIT_REPO&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;/commit&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;${&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;pathname&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;.patch&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;`&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;    const&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt; patch&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt; fetch&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;gitPatch&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; {&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt; cf&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; {&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt; cacheEverything&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt; true&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt; cacheTtlByStatus&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; {&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; &amp;#39;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;200-299&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&amp;#39;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#B48EAD&quot;&gt; 86400&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; }&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; }}&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;then&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;res&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt; =&amp;gt;&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt; res&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;text&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;())&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;    const&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt; url&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt; pathname&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt; ===&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; &amp;#39;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&amp;#39;&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt; ?&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt; GIT_REPO&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt; :&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt; patch&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;match&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;^&lt;/span&gt;&lt;span style=&quot;color:#EBCB8B&quot;&gt;Subject:\s&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color:#EBCB8B&quot;&gt;\[PATCH\]&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#EBCB8B&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;$&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;m&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;?.&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;[&lt;/span&gt;&lt;span style=&quot;color:#B48EAD&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;]&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;?.&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;trim&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;    return&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt; Response&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;redirect&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;url&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt; ||&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt; GIT_REPO&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;腾讯云-edgeone-版本&quot;&gt;腾讯云 EdgeOne 版本&lt;/h3&gt;
&lt;pre class=&quot;astro-code nord&quot; style=&quot;background-color:#2e3440ff;color:#d8dee9ff;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;js&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt; GIT_REPO&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; &amp;quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;https://github.com/miantiao-me/hink&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;addEventListener&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;fetch&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt; async&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;event&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt; =&amp;gt;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; {&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt; pathname&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; }&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt; new&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt; URL&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;event&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;request&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;url&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt; gitPatch&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; `&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;${&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;GIT_REPO&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;/commit&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;${&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;pathname&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;.patch&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;`&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt; patch&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt; fetch&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;gitPatch&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;then&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;res&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt; =&amp;gt;&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt; res&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;text&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;())&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt; url&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt; pathname&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt; ===&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; &amp;#39;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&amp;#39;&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt; ?&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt; GIT_REPO&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt; :&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt; patch&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;match&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;^&lt;/span&gt;&lt;span style=&quot;color:#EBCB8B&quot;&gt;Subject:\s&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color:#EBCB8B&quot;&gt;\[PATCH\]&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#EBCB8B&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;$&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;m&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;?.&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;[&lt;/span&gt;&lt;span style=&quot;color:#B48EAD&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;]&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;?.&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;trim&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;  event&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;respondWith&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;new&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt; Response&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;null&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; {&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt; status&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#B48EAD&quot;&gt; 302&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt; headers&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; {&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt; Location&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt; url&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt; ||&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt; GIT_REPO&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; }&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; }&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;))&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;部署到 Serverless 平台后，绑定一个域名，就能拥有自己的短链接服务。&lt;/p&gt;
&lt;h2 id=&quot;为什么做-hink&quot;&gt;为什么做 hink？&lt;/h2&gt;
&lt;p&gt;短链接服务并不新鲜，市面上有很多工具。但 hink 的目标是探索一种极简且有趣的实现方式。利用 Git 提交哈希值，避免了数据库管理，借助 GitHub 实现存储，而云平台的 WAF 功能让访问统计变得简单。这对我来说是一次技术实验，用最少的代码解决实际问题。&lt;/p&gt;
&lt;h2 id=&quot;演示效果三大平台运行情况&quot;&gt;演示效果：三大平台运行情况&lt;/h2&gt;
&lt;p&gt;我在 Cloudflare Workers、阿里云 ESA 和腾讯云 EdgeOne 上部署了 hink，并通过 WAF 面板查看访问统计。以下是各平台的运行截图：&lt;/p&gt;
&lt;h3 id=&quot;cloudflare-workers&quot;&gt;Cloudflare Workers&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://static.miantiao.me/share/2025/ePMX6q/Zsb50p.png&quot; alt=&quot;Cloudflare&quot;/&gt;&lt;/p&gt;
&lt;h3 id=&quot;阿里云-esa&quot;&gt;阿里云 ESA&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://static.miantiao.me/share/2025/JiOSmY/6oDC36.png&quot; alt=&quot;Alibaba&quot;/&gt;&lt;/p&gt;
&lt;h3 id=&quot;腾讯云-edgeone&quot;&gt;腾讯云 EdgeOne&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://static.miantiao.me/share/2025/dyzFzs/efOSKl.png&quot; alt=&quot;Tencent&quot;/&gt;&lt;/p&gt;
&lt;h2 id=&quot;项目地址&quot;&gt;项目地址&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/miantiao-me/hink&quot; rel=&quot;nofollow&quot; target=&quot;_blank&quot;&gt;&lt;img src=&quot;https://github.html.zone/miantiao-me/hink&quot; alt=&quot;miantiao-me/hink - GitHub&quot;/&gt;&lt;/a&gt;&lt;/p&gt;&lt;img src=&quot;https://c.statcounter.com/9823304/0/92a1d06c/1/&quot; alt=&quot;stat&quot; referrerPolicy=&quot;no-referrer-when-downgrade&quot; /&gt;</content:encoded></item><item><title>解密 SSH AI Chat：它是如何运行的</title><link>https://miantiao.me/posts/how-ssh-ai-chat-works/</link><guid isPermaLink="true">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>&lt;div class=&quot;not-prose&quot;&gt;&lt;div class=&quot;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&quot;&gt;&lt;div class=&quot;absolute inset-0 bg-grid-zinc-100 dark:bg-grid-zinc-800 opacity-50&quot;&gt;&lt;/div&gt;&lt;div class=&quot;relative z-10&quot;&gt;&lt;div class=&quot;flex items-center gap-3 mb-6&quot;&gt;&lt;div class=&quot;p-3 bg-zinc-500/10 dark:bg-zinc-400/10 rounded-xl&quot;&gt;&lt;i class=&quot;icon-[tabler--terminal-2] w-6 h-6 text-zinc-600 dark:text-zinc-400 block&quot;&gt;&lt;/i&gt;&lt;/div&gt;&lt;span class=&quot;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&quot;&gt;&lt;p&gt;技术深度解析&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;&lt;h2 class=&quot;text-2xl md:text-3xl lg:text-4xl font-bold text-zinc-900 dark:text-zinc-100 mb-6 leading-tight&quot;&gt;&lt;p&gt;解密 SSH AI Chat
&lt;span class=&quot;text-zinc-600 dark:text-zinc-400 mt-2&quot;&gt;它是如何运行的&lt;/span&gt;&lt;/p&gt;&lt;/h2&gt;&lt;p class=&quot;text-base md:text-lg text-zinc-600 dark:text-zinc-300 max-w-3xl leading-relaxed&quot;&gt;&lt;p&gt;探索一个令人惊叹的创新项目：通过 SSH 协议直接与 AI 对话。无需安装客户端，无需打开浏览器，
只需一条简单的 SSH 命令，就能开启与 AI 的终端对话之旅。&lt;/p&gt;&lt;/p&gt;&lt;div class=&quot;mt-8 p-4 bg-zinc-900 dark:bg-zinc-800 rounded-lg border border-zinc-200 dark:border-zinc-700&quot;&gt;&lt;code class=&quot;text-zinc-300 font-mono text-sm md:text-base&quot;&gt;&lt;p&gt;$ ssh username&lt;span&gt;@&lt;/span&gt;chat.agi.li&lt;/p&gt;&lt;/code&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;大家好，我是面条，今天想和大家分享我最近的一个项目 —— SSH AI Chat。&lt;/p&gt;
&lt;h2 id=&quot;项目简介&quot;&gt;项目简介&lt;/h2&gt;
&lt;div class=&quot;not-prose mb-8&quot;&gt;&lt;div class=&quot;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&quot;&gt;&lt;div class=&quot;flex items-start gap-4&quot;&gt;&lt;div class=&quot;p-2 bg-zinc-500/10 dark:bg-zinc-400/10 rounded-lg&quot;&gt;&lt;i class=&quot;icon-[tabler--rocket] w-5 h-5 text-zinc-600 dark:text-zinc-400 block&quot;&gt;&lt;/i&gt;&lt;/div&gt;&lt;div&gt;&lt;h3 class=&quot;text-lg font-semibold text-zinc-900 dark:text-zinc-100 mb-2&quot;&gt;核心理念&lt;/h3&gt;&lt;p class=&quot;text-zinc-700 dark:text-zinc-300&quot;&gt;&lt;p&gt;SSH AI Chat 是一个可以通过 SSH 直接连接的 AI 聊天应用。使用方式非常简单，
你不需要安装任何客户端，不需要打开浏览器，只需要一个 SSH 客户端，就能和 AI 进行对话。&lt;/p&gt;&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;
&lt;pre class=&quot;astro-code nord&quot; style=&quot;background-color:#2e3440ff;color:#d8dee9ff;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;ssh&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt; username@chat.agi.li&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;没错，就这么简单！你不需要安装任何客户端，不需要打开浏览器，只需要一个 SSH 客户端，就能和 AI 进行对话。&lt;/p&gt;
&lt;p&gt;作为一个对 TUI 应用有着浓厚兴趣的开发者，我一直觉得在终端里聊天是一件很酷的事情。其实我最初是被 itter.sh 这个网站惊艳到了 - 居然能用 SSH 访问社交网络！这让我意识到，原来 SSH 不只是用来连服务器的，还可以做很多有趣的事情。&lt;/p&gt;
&lt;p&gt;于是就有了这个想法：如果能用 SSH 和 AI 聊天，那该多酷啊！不需要安装任何软件，不需要打开浏览器，只要在终端里输入 &lt;code&gt;ssh yourname@chat.agi.li&lt;/code&gt; 就能开始对话。&lt;/p&gt;
&lt;h2 id=&quot;项目架构&quot;&gt;项目架构&lt;/h2&gt;
&lt;div class=&quot;not-prose mb-8&quot;&gt;&lt;div class=&quot;grid md:grid-cols-2 gap-6&quot;&gt;&lt;div class=&quot;bg-white dark:bg-zinc-800 rounded-xl p-6 border border-zinc-200 dark:border-zinc-700 shadow-sm&quot;&gt;&lt;div class=&quot;flex items-center gap-3 mb-4&quot;&gt;&lt;i class=&quot;icon-[tabler--stack-2] w-5 h-5 text-zinc-600 dark:text-zinc-400 block&quot;&gt;&lt;/i&gt;&lt;h3 class=&quot;text-lg font-semibold text-zinc-900 dark:text-zinc-100&quot;&gt;核心技术栈&lt;/h3&gt;&lt;/div&gt;&lt;ul class=&quot;space-y-2 text-sm text-zinc-600 dark:text-zinc-300&quot;&gt;&lt;li class=&quot;flex items-center gap-2&quot;&gt;&lt;i class=&quot;icon-[tabler--server] w-4 h-4 text-zinc-500 block&quot;&gt;&lt;/i&gt;&lt;span&gt;&lt;strong&gt;SSH 服务器&lt;/strong&gt;：Node.js + ssh2 模块&lt;/span&gt;&lt;/li&gt;&lt;li class=&quot;flex items-center gap-2&quot;&gt;&lt;i class=&quot;icon-[tabler--brand-react] w-4 h-4 text-zinc-500 block&quot;&gt;&lt;/i&gt;&lt;span&gt;&lt;strong&gt;UI 框架&lt;/strong&gt;：React + Ink（终端渲染）&lt;/span&gt;&lt;/li&gt;&lt;li class=&quot;flex items-center gap-2&quot;&gt;&lt;i class=&quot;icon-[tabler--database] w-4 h-4 text-zinc-500 block&quot;&gt;&lt;/i&gt;&lt;span&gt;&lt;strong&gt;数据库&lt;/strong&gt;：PostgreSQL / PGLite&lt;/span&gt;&lt;/li&gt;&lt;li class=&quot;flex items-center gap-2&quot;&gt;&lt;i class=&quot;icon-[tabler--refresh] w-4 h-4 text-zinc-500 block&quot;&gt;&lt;/i&gt;&lt;span&gt;&lt;strong&gt;缓存&lt;/strong&gt;：Redis / ioredis-mock&lt;/span&gt;&lt;/li&gt;&lt;li class=&quot;flex items-center gap-2&quot;&gt;&lt;i class=&quot;icon-[tabler--brain] w-4 h-4 text-zinc-500 block&quot;&gt;&lt;/i&gt;&lt;span&gt;&lt;strong&gt;AI 集成&lt;/strong&gt;：Vercel AI SDK&lt;/span&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/div&gt;&lt;div class=&quot;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&quot;&gt;&lt;div class=&quot;flex items-center gap-3 mb-4&quot;&gt;&lt;i class=&quot;icon-[tabler--topology-star-3] w-5 h-5 text-zinc-600 dark:text-zinc-400 block&quot;&gt;&lt;/i&gt;&lt;h3 class=&quot;text-lg font-semibold text-zinc-900 dark:text-zinc-100&quot;&gt;架构特点&lt;/h3&gt;&lt;/div&gt;&lt;ul class=&quot;space-y-2 text-sm text-zinc-700 dark:text-zinc-300&quot;&gt;&lt;li&gt;• 无客户端安装需求&lt;/li&gt;&lt;li&gt;• 跨平台终端支持&lt;/li&gt;&lt;li&gt;• GitHub 公钥认证&lt;/li&gt;&lt;li&gt;• 实时流式响应&lt;/li&gt;&lt;li&gt;• 多模型 AI 支持&lt;/li&gt;&lt;/ul&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;
&lt;h3 id=&quot;系统架构图&quot;&gt;系统架构图&lt;/h3&gt;
&lt;pre class=&quot;astro-code nord&quot; style=&quot;background-color:#2e3440ff;color:#d8dee9ff;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;txt&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;┌─────────────────┐    ┌─────────────────┐    ┌─────────────────┐&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;│   SSH Client    │    │   SSH Server    │    │   React App     │&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;│                 │    │                 │    │                 │&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;│  ssh username@  │───▶│  Node.js +      │───▶│  Ink UI +       │&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;│  chat.agi.li  │    │  ssh2           │    │  React Hooks    │&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;└─────────────────┘    └─────────────────┘    └─────────────────┘&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;                               │&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;                               ▼&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;                       ┌─────────────────┐&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;                       │   AI Services   │&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;                       │                 │&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;                       │  OpenAI API     │&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;                       │  Gemini API     │&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;                       │  DeepSeek API   │&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;                       └─────────────────┘&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;核心模块解析&quot;&gt;核心模块解析&lt;/h2&gt;
&lt;div class=&quot;not-prose mb-8&quot;&gt;&lt;div class=&quot;grid gap-6&quot;&gt;&lt;div class=&quot;bg-white dark:bg-zinc-800 rounded-xl border border-zinc-200 dark:border-zinc-700 overflow-hidden&quot;&gt;&lt;div class=&quot;bg-gradient-to-r from-zinc-600 to-zinc-700 p-4&quot;&gt;&lt;h3 class=&quot;text-lg font-semibold text-white flex items-center gap-2&quot;&gt;&lt;i class=&quot;icon-[tabler--server-2] w-5 h-5 block&quot;&gt;&lt;/i&gt;&lt;span&gt;1. SSH 服务器模块&lt;/span&gt;&lt;/h3&gt;&lt;/div&gt;&lt;div class=&quot;p-6&quot;&gt;&lt;p class=&quot;text-zinc-600 dark:text-zinc-300&quot;&gt;&lt;p&gt;这是整个应用的核心，负责处理 SSH 连接和认证。系统会自动处理密钥验证、GitHub 公钥认证、登录限制和速率限制。&lt;/p&gt;&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;bg-white dark:bg-zinc-800 rounded-xl border border-zinc-200 dark:border-zinc-700 overflow-hidden&quot;&gt;&lt;div class=&quot;bg-gradient-to-r from-zinc-600 to-zinc-700 p-4&quot;&gt;&lt;h3 class=&quot;text-lg font-semibold text-white flex items-center gap-2&quot;&gt;&lt;i class=&quot;icon-[tabler--shield-check] w-5 h-5 block&quot;&gt;&lt;/i&gt;&lt;span&gt;2. 认证系统&lt;/span&gt;&lt;/h3&gt;&lt;/div&gt;&lt;div class=&quot;p-6&quot;&gt;&lt;p class=&quot;text-zinc-600 dark:text-zinc-300 mb-4&quot;&gt;&lt;span&gt;最巧妙的设计是使用 GitHub 公钥认证。用户不需要注册，直接使用 GitHub SSH 密钥就能登录。&lt;/span&gt;&lt;/p&gt;&lt;div class=&quot;bg-zinc-50 dark:bg-zinc-800/50 rounded-lg p-4 border border-zinc-200 dark:border-zinc-700&quot;&gt;&lt;p class=&quot;text-zinc-700 dark:text-zinc-300 text-sm&quot;&gt;&lt;i class=&quot;icon-[tabler--info-circle] w-5 h-5 mr-2 inline-block&quot;&gt;&lt;/i&gt;&lt;span&gt;系统会获取用户的 GitHub 公钥进行验证，每 6 小时缓存一次，既安全又高效。&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;bg-white dark:bg-zinc-800 rounded-xl border border-zinc-200 dark:border-zinc-700 overflow-hidden&quot;&gt;&lt;div class=&quot;bg-gradient-to-r from-zinc-600 to-zinc-700 p-4&quot;&gt;&lt;h3 class=&quot;text-lg font-semibold text-white flex items-center gap-2&quot;&gt;&lt;i class=&quot;icon-[tabler--terminal] w-5 h-5 block&quot;&gt;&lt;/i&gt;&lt;span&gt;3. 终端 UI 系统&lt;/span&gt;&lt;/h3&gt;&lt;/div&gt;&lt;div class=&quot;p-6&quot;&gt;&lt;p class=&quot;text-zinc-600 dark:text-zinc-300 mb-4&quot;&gt;&lt;span&gt;使用 Ink 框架在终端中渲染 React 组件。想象一下，你平时写的 React 组件，现在不是在浏览器里渲染，而是在终端里显示！&lt;/span&gt;&lt;/p&gt;&lt;div class=&quot;grid grid-cols-2 md:grid-cols-4 gap-3&quot;&gt;&lt;div class=&quot;text-center p-3 bg-zinc-50 dark:bg-zinc-800/50 rounded-lg&quot;&gt;&lt;i class=&quot;icon-[tabler--language] w-5 h-5 text-zinc-600 dark:text-zinc-400 mb-1 mx-auto block&quot;&gt;&lt;/i&gt;&lt;p class=&quot;text-xs text-zinc-700 dark:text-zinc-300&quot;&gt;多语言界面&lt;/p&gt;&lt;/div&gt;&lt;div class=&quot;text-center p-3 bg-zinc-50 dark:bg-zinc-800/50 rounded-lg&quot;&gt;&lt;i class=&quot;icon-[tabler--message-circle] w-5 h-5 text-zinc-600 dark:text-zinc-400 mb-1 mx-auto block&quot;&gt;&lt;/i&gt;&lt;p class=&quot;text-xs text-zinc-700 dark:text-zinc-300&quot;&gt;实时聊天&lt;/p&gt;&lt;/div&gt;&lt;div class=&quot;text-center p-3 bg-zinc-50 dark:bg-zinc-800/50 rounded-lg&quot;&gt;&lt;i class=&quot;icon-[tabler--history] w-5 h-5 text-zinc-600 dark:text-zinc-400 mb-1 mx-auto block&quot;&gt;&lt;/i&gt;&lt;p class=&quot;text-xs text-zinc-700 dark:text-zinc-300&quot;&gt;历史记录&lt;/p&gt;&lt;/div&gt;&lt;div class=&quot;text-center p-3 bg-zinc-50 dark:bg-zinc-800/50 rounded-lg&quot;&gt;&lt;i class=&quot;icon-[tabler--device-mobile] w-5 h-5 text-zinc-600 dark:text-zinc-400 mb-1 mx-auto block&quot;&gt;&lt;/i&gt;&lt;p class=&quot;text-xs text-zinc-700 dark:text-zinc-300&quot;&gt;响应式布局&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;bg-white dark:bg-zinc-800 rounded-xl border border-zinc-200 dark:border-zinc-700 overflow-hidden&quot;&gt;&lt;div class=&quot;bg-gradient-to-r from-zinc-600 to-zinc-700 p-4&quot;&gt;&lt;h3 class=&quot;text-lg font-semibold text-white flex items-center gap-2&quot;&gt;&lt;i class=&quot;icon-[tabler--message-chatbot] w-5 h-5 block&quot;&gt;&lt;/i&gt;&lt;span&gt;4. 聊天系统&lt;/span&gt;&lt;/h3&gt;&lt;/div&gt;&lt;div class=&quot;p-6&quot;&gt;&lt;p class=&quot;text-zinc-600 dark:text-zinc-300 mb-4&quot;&gt;&lt;span&gt;使用 Vercel AI SDK 处理 AI 对话。当你在终端里输入消息时，系统会接收消息、加载历史对话、选择模型、实时显示流式响应，并保存对话记录。&lt;/span&gt;&lt;/p&gt;&lt;div class=&quot;flex flex-wrap gap-2&quot;&gt;&lt;span class=&quot;px-3 py-1 bg-zinc-100 dark:bg-zinc-800/50 text-zinc-700 dark:text-zinc-300 rounded-full text-sm&quot;&gt;流式响应&lt;/span&gt;&lt;span class=&quot;px-3 py-1 bg-zinc-100 dark:bg-zinc-800/50 text-zinc-700 dark:text-zinc-300 rounded-full text-sm&quot;&gt;多模型支持&lt;/span&gt;&lt;span class=&quot;px-3 py-1 bg-zinc-100 dark:bg-zinc-800/50 text-zinc-700 dark:text-zinc-300 rounded-full text-sm&quot;&gt;思维链展示&lt;/span&gt;&lt;span class=&quot;px-3 py-1 bg-zinc-100 dark:bg-zinc-800/50 text-zinc-700 dark:text-zinc-300 rounded-full text-sm&quot;&gt;对话历史管理&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;
&lt;h2 id=&quot;技术难点和解决方案&quot;&gt;技术难点和解决方案&lt;/h2&gt;
&lt;div class=&quot;not-prose mb-8&quot;&gt;&lt;div class=&quot;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&quot;&gt;&lt;div class=&quot;absolute inset-0 bg-gradient-to-br from-white/30 to-transparent rounded-2xl&quot;&gt;&lt;/div&gt;&lt;div class=&quot;relative&quot;&gt;&lt;div class=&quot;flex items-center gap-4 mb-6&quot;&gt;&lt;div class=&quot;p-3 bg-gradient-to-br from-zinc-500 to-zinc-600 rounded-xl shadow-lg&quot;&gt;&lt;i class=&quot;icon-[tabler--alert-triangle] w-6 h-6 text-white block&quot;&gt;&lt;/i&gt;&lt;/div&gt;&lt;div&gt;&lt;h3 class=&quot;text-2xl font-bold text-zinc-900 dark:text-zinc-100&quot;&gt;&lt;span&gt;核心技术挑战&lt;/span&gt;&lt;/h3&gt;&lt;p class=&quot;text-zinc-600 dark:text-zinc-400 text-sm mt-1&quot;&gt;项目开发过程中遇到的关键技术难点&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;grid grid-cols-1 md:grid-cols-2 gap-4&quot;&gt;&lt;div class=&quot;p-4 bg-white/70 dark:bg-zinc-800/70 backdrop-blur-sm rounded-xl border border-zinc-200/50 dark:border-zinc-600/50&quot;&gt;&lt;div class=&quot;flex items-center gap-2 mb-2&quot;&gt;&lt;div class=&quot;w-2 h-2 bg-zinc-500 rounded-full&quot;&gt;&lt;/div&gt;&lt;span class=&quot;text-sm font-semibold text-zinc-800 dark:text-zinc-200&quot;&gt;终端渲染复杂性&lt;/span&gt;&lt;/div&gt;&lt;p class=&quot;text-xs text-zinc-600 dark:text-zinc-400&quot;&gt;React 组件在终端环境的适配&lt;/p&gt;&lt;/div&gt;&lt;div class=&quot;p-4 bg-white/70 dark:bg-zinc-800/70 backdrop-blur-sm rounded-xl border border-zinc-200/50 dark:border-zinc-600/50&quot;&gt;&lt;div class=&quot;flex items-center gap-2 mb-2&quot;&gt;&lt;div class=&quot;w-2 h-2 bg-zinc-600 rounded-full&quot;&gt;&lt;/div&gt;&lt;span class=&quot;text-sm font-semibold text-zinc-800 dark:text-zinc-200&quot;&gt;会话状态管理&lt;/span&gt;&lt;/div&gt;&lt;p class=&quot;text-xs text-zinc-600 dark:text-zinc-400&quot;&gt;多用户并发连接的处理&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;
&lt;h3 id=&quot;1-终端渲染的挑战&quot;&gt;1. 终端渲染的挑战&lt;/h3&gt;
&lt;p&gt;最大的挑战是在终端中实现复杂的 UI 界面。使用 Ink 框架将 React 组件渲染到终端，实现虚拟 PTY 来处理终端 I/O。在终端里显示 AI 回复的 Markdown 需要用专门的 worker 进程处理转换，才能显示粗体、斜体、代码块。&lt;/p&gt;
&lt;h3 id=&quot;2-ssh-会话管理&quot;&gt;2. SSH 会话管理&lt;/h3&gt;
&lt;p&gt;管理多个 SSH 会话和状态需要为每个连接创建独立的 React 应用实例，使用 Context API 管理全局状态，并实现会话生命周期管理。&lt;/p&gt;
&lt;h3 id=&quot;3-实时流式响应&quot;&gt;3. 实时流式响应&lt;/h3&gt;
&lt;p&gt;AI 的回复是流式的，如果每收到一个字节就刷新界面，终端会直接卡死。使用 Vercel AI SDK 的 streamText 配合节流更新，每 300ms 更新一次，既流畅又不卡。这是性能优化的关键技术点。&lt;/p&gt;
&lt;h3 id=&quot;4-数据存储的灵活性&quot;&gt;4. 数据存储的灵活性&lt;/h3&gt;
&lt;p&gt;项目支持 PostgreSQL 和 PGLite 两种数据库，以及 Redis 和内存缓存，让项目既能独立运行又能部署到生产环境。&lt;/p&gt;
&lt;h2 id=&quot;有趣的设计细节&quot;&gt;有趣的设计细节&lt;/h2&gt;
&lt;div class=&quot;not-prose mb-8&quot;&gt;&lt;div class=&quot;grid md:grid-cols-2 gap-6&quot;&gt;&lt;div class=&quot;bg-white dark:bg-zinc-800 rounded-xl p-6 border border-zinc-200 dark:border-zinc-700 hover:shadow-lg transition-shadow&quot;&gt;&lt;div class=&quot;flex items-center gap-3 mb-4&quot;&gt;&lt;div class=&quot;p-2 bg-zinc-500/10 rounded-lg&quot;&gt;&lt;i class=&quot;icon-[tabler--brand-github] w-5 h-5 text-zinc-900 dark:text-zinc-100 block&quot;&gt;&lt;/i&gt;&lt;/div&gt;&lt;h3 class=&quot;text-lg font-semibold text-zinc-900 dark:text-zinc-100&quot;&gt;&lt;span&gt;GitHub 认证&lt;/span&gt;&lt;/h3&gt;&lt;/div&gt;&lt;p class=&quot;text-zinc-600 dark:text-zinc-300 text-sm mb-3&quot;&gt;&lt;span&gt;最酷的设计！用户不需要注册，直接使用 GitHub SSH 密钥就能登录，既方便又安全。&lt;/span&gt;&lt;/p&gt;&lt;div class=&quot;flex items-center gap-2 text-xs text-zinc-600 dark:text-zinc-400&quot;&gt;&lt;i class=&quot;icon-[tabler--check] w-4 h-4 inline-block&quot;&gt;&lt;/i&gt;&lt;span&gt;零注册流程&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;bg-white dark:bg-zinc-800 rounded-xl p-6 border border-zinc-200 dark:border-zinc-700 hover:shadow-lg transition-shadow&quot;&gt;&lt;div class=&quot;flex items-center gap-3 mb-4&quot;&gt;&lt;div class=&quot;p-2 bg-zinc-500/10 rounded-lg&quot;&gt;&lt;i class=&quot;icon-[tabler--cpu] w-5 h-5 text-zinc-600 dark:text-zinc-400 block&quot;&gt;&lt;/i&gt;&lt;/div&gt;&lt;h3 class=&quot;text-lg font-semibold text-zinc-900 dark:text-zinc-100&quot;&gt;&lt;span&gt;多模型支持&lt;/span&gt;&lt;/h3&gt;&lt;/div&gt;&lt;p class=&quot;text-zinc-600 dark:text-zinc-300 text-sm mb-3&quot;&gt;&lt;span&gt;支持 DeepSeek-V3/DeepSeek-R1、Gemini-2.5-Flash/Gemini-2.5-Pro 等多个 AI 模型，包括思维链展示。&lt;/span&gt;&lt;/p&gt;&lt;div class=&quot;flex flex-wrap gap-1&quot;&gt;&lt;span class=&quot;px-2 py-1 bg-zinc-100 dark:bg-zinc-800/50 text-zinc-700 dark:text-zinc-300 rounded text-xs&quot;&gt;DeepSeek&lt;/span&gt;&lt;span class=&quot;px-2 py-1 bg-zinc-100 dark:bg-zinc-800/50 text-zinc-700 dark:text-zinc-300 rounded text-xs&quot;&gt;Gemini&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;bg-white dark:bg-zinc-800 rounded-xl p-6 border border-zinc-200 dark:border-zinc-700 hover:shadow-lg transition-shadow&quot;&gt;&lt;div class=&quot;flex items-center gap-3 mb-4&quot;&gt;&lt;div class=&quot;p-2 bg-zinc-500/10 rounded-lg&quot;&gt;&lt;i class=&quot;icon-[tabler--world] w-5 h-5 text-zinc-600 dark:text-zinc-400 block&quot;&gt;&lt;/i&gt;&lt;/div&gt;&lt;h3 class=&quot;text-lg font-semibold text-zinc-900 dark:text-zinc-100&quot;&gt;&lt;span&gt;国际化支持&lt;/span&gt;&lt;/h3&gt;&lt;/div&gt;&lt;p class=&quot;text-zinc-600 dark:text-zinc-300 text-sm mb-3&quot;&gt;&lt;span&gt;完整的 i18n 支持，通过 &lt;code&gt;LANG&lt;/code&gt; 环境变量自动检测用户语言偏好，支持中英文切换。&lt;/span&gt;&lt;/p&gt;&lt;div class=&quot;flex items-center gap-2 text-xs text-zinc-600 dark:text-zinc-400&quot;&gt;&lt;i class=&quot;icon-[tabler--language] w-4 h-4 inline-block&quot;&gt;&lt;/i&gt;&lt;span&gt;自动语言检测&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;bg-white dark:bg-zinc-800 rounded-xl p-6 border border-zinc-200 dark:border-zinc-700 hover:shadow-lg transition-shadow&quot;&gt;&lt;div class=&quot;flex items-center gap-3 mb-4&quot;&gt;&lt;div class=&quot;p-2 bg-zinc-500/10 rounded-lg&quot;&gt;&lt;i class=&quot;icon-[tabler--keyboard] w-5 h-5 text-zinc-600 dark:text-zinc-400 block&quot;&gt;&lt;/i&gt;&lt;/div&gt;&lt;h3 class=&quot;text-lg font-semibold text-zinc-900 dark:text-zinc-100&quot;&gt;&lt;span&gt;键盘快捷键&lt;/span&gt;&lt;/h3&gt;&lt;/div&gt;&lt;p class=&quot;text-zinc-600 dark:text-zinc-300 text-sm mb-3&quot;&gt;&lt;span&gt;&lt;code&gt;Ctrl+C&lt;/code&gt; 退出应用，&lt;code&gt;N&lt;/code&gt; 新建对话，&lt;code&gt;I&lt;/code&gt; 聚焦输入框，&lt;code&gt;?&lt;/code&gt; 查看帮助。还有一些小彩蛋功能。&lt;/span&gt;&lt;/p&gt;&lt;div class=&quot;grid grid-cols-2 gap-2 text-xs&quot;&gt;&lt;div class=&quot;flex items-center gap-1&quot;&gt;&lt;kbd class=&quot;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&quot;&gt;Ctrl+C&lt;/kbd&gt;&lt;span class=&quot;text-zinc-500 dark:text-zinc-400&quot;&gt;退出&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;flex items-center gap-1&quot;&gt;&lt;kbd class=&quot;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&quot;&gt;N&lt;/kbd&gt;&lt;span class=&quot;text-zinc-500 dark:text-zinc-400&quot;&gt;新建&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;
&lt;h2 id=&quot;开发心得&quot;&gt;开发心得&lt;/h2&gt;
&lt;div class=&quot;not-prose mb-8&quot;&gt;&lt;div class=&quot;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&quot;&gt;&lt;div class=&quot;absolute inset-0 bg-gradient-to-br from-white/30 to-transparent rounded-2xl&quot;&gt;&lt;/div&gt;&lt;div class=&quot;relative&quot;&gt;&lt;div class=&quot;flex items-center gap-4 mb-6&quot;&gt;&lt;div class=&quot;p-3 bg-gradient-to-br from-zinc-500 to-zinc-600 rounded-xl shadow-lg&quot;&gt;&lt;i class=&quot;icon-[tabler--bulb] w-6 h-6 text-white block&quot;&gt;&lt;/i&gt;&lt;/div&gt;&lt;div&gt;&lt;h3 class=&quot;text-2xl font-bold text-zinc-900 dark:text-zinc-100&quot;&gt;&lt;span&gt;技术感悟与收获&lt;/span&gt;&lt;/h3&gt;&lt;p class=&quot;text-zinc-600 dark:text-zinc-400 text-sm mt-1&quot;&gt;开发过程中的深度思考与技术洞察&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;grid grid-cols-1 md:grid-cols-3 gap-4&quot;&gt;&lt;div class=&quot;p-4 bg-white/70 dark:bg-zinc-800/70 backdrop-blur-sm rounded-xl border border-zinc-200/50 dark:border-zinc-600/50&quot;&gt;&lt;div class=&quot;flex items-center gap-2 mb-2&quot;&gt;&lt;div class=&quot;w-2 h-2 bg-zinc-500 rounded-full&quot;&gt;&lt;/div&gt;&lt;span class=&quot;text-sm font-semibold text-zinc-800 dark:text-zinc-200&quot;&gt;终端应用潜力&lt;/span&gt;&lt;/div&gt;&lt;p class=&quot;text-xs text-zinc-600 dark:text-zinc-400&quot;&gt;现代化终端交互的可能性&lt;/p&gt;&lt;/div&gt;&lt;div class=&quot;p-4 bg-white/70 dark:bg-zinc-800/70 backdrop-blur-sm rounded-xl border border-zinc-200/50 dark:border-zinc-600/50&quot;&gt;&lt;div class=&quot;flex items-center gap-2 mb-2&quot;&gt;&lt;div class=&quot;w-2 h-2 bg-zinc-600 rounded-full&quot;&gt;&lt;/div&gt;&lt;span class=&quot;text-sm font-semibold text-zinc-800 dark:text-zinc-200&quot;&gt;SSH 协议创新&lt;/span&gt;&lt;/div&gt;&lt;p class=&quot;text-xs text-zinc-600 dark:text-zinc-400&quot;&gt;跨平台应用的新思路&lt;/p&gt;&lt;/div&gt;&lt;div class=&quot;p-4 bg-white/70 dark:bg-zinc-800/70 backdrop-blur-sm rounded-xl border border-zinc-200/50 dark:border-zinc-600/50&quot;&gt;&lt;div class=&quot;flex items-center gap-2 mb-2&quot;&gt;&lt;div class=&quot;w-2 h-2 bg-zinc-700 rounded-full&quot;&gt;&lt;/div&gt;&lt;span class=&quot;text-sm font-semibold text-zinc-800 dark:text-zinc-200&quot;&gt;技术栈融合&lt;/span&gt;&lt;/div&gt;&lt;p class=&quot;text-xs text-zinc-600 dark:text-zinc-400&quot;&gt;现代前端技术的适应性&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;
&lt;h3 id=&quot;1-终端应用的可能性&quot;&gt;1. 终端应用的可能性&lt;/h3&gt;
&lt;p&gt;这个项目让我看到了终端应用的巨大潜力。通过 Ink 框架，我们可以在终端中实现复杂的交互界面。&lt;/p&gt;
&lt;h3 id=&quot;2-ssh-的妙用&quot;&gt;2. SSH 的妙用&lt;/h3&gt;
&lt;p&gt;SSH 不仅仅是一个远程管理工具，它还是一个强大的应用平台。通过 SSH，我们可以实现跨平台的客户端应用，用户不需要安装任何额外的软件。&lt;/p&gt;
&lt;h3 id=&quot;3-现代化的技术栈&quot;&gt;3. 现代化的技术栈&lt;/h3&gt;
&lt;p&gt;虽然这是一个终端应用，但我们使用了最现代的技术栈：React、TypeScript、Vercel AI SDK 等。这证明了终端应用也可以很”现代化”。&lt;/p&gt;
&lt;h2 id=&quot;未来展望&quot;&gt;未来展望&lt;/h2&gt;
&lt;div class=&quot;not-prose mb-8&quot;&gt;&lt;div class=&quot;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&quot;&gt;&lt;div class=&quot;flex items-center gap-3 mb-4&quot;&gt;&lt;i class=&quot;icon-[tabler--telescope] w-6 h-6 text-zinc-600 dark:text-zinc-400 block&quot;&gt;&lt;/i&gt;&lt;h3 class=&quot;text-xl font-bold text-zinc-900 dark:text-zinc-100&quot;&gt;&lt;span&gt;发展规划&lt;/span&gt;&lt;/h3&gt;&lt;/div&gt;&lt;div class=&quot;grid md:grid-cols-2 gap-4&quot;&gt;&lt;div class=&quot;flex items-start gap-3&quot;&gt;&lt;i class=&quot;icon-[tabler--server-bolt] w-5 h-5 text-zinc-600 dark:text-zinc-400 mt-1 block&quot;&gt;&lt;/i&gt;&lt;div&gt;&lt;h4 class=&quot;font-semibold text-zinc-900 dark:text-zinc-100 mb-1&quot;&gt;&lt;span&gt;本地模型支持&lt;/span&gt;&lt;/h4&gt;&lt;p class=&quot;text-zinc-700 dark:text-zinc-300 text-sm&quot;&gt;计划支持 Ollama 等本地模型&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;flex items-start gap-3&quot;&gt;&lt;i class=&quot;icon-[tabler--plug-connected] w-5 h-5 text-zinc-600 dark:text-zinc-400 mt-1 block&quot;&gt;&lt;/i&gt;&lt;div&gt;&lt;h4 class=&quot;font-semibold text-zinc-900 dark:text-zinc-100 mb-1&quot;&gt;&lt;span&gt;MCP 协议&lt;/span&gt;&lt;/h4&gt;&lt;p class=&quot;text-zinc-700 dark:text-zinc-300 text-sm&quot;&gt;支持 Model Context Protocol 插件扩展&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;计划支持更多的 AI 模型，包括 Ollama 等本地模型，以及支持 MCP (Model Context Protocol) 协议让用户可以通过插件扩展功能。&lt;/p&gt;
&lt;h2 id=&quot;总结&quot;&gt;总结&lt;/h2&gt;
&lt;div class=&quot;not-prose mb-8&quot;&gt;&lt;div class=&quot;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&quot;&gt;&lt;div class=&quot;text-center mb-6&quot;&gt;&lt;i class=&quot;icon-[tabler--sparkles] w-8 h-8 text-zinc-600 dark:text-zinc-400 mb-3 mx-auto block&quot;&gt;&lt;/i&gt;&lt;h3 class=&quot;text-2xl font-bold text-zinc-900 dark:text-zinc-100 mb-4&quot;&gt;&lt;span&gt;项目价值与意义&lt;/span&gt;&lt;/h3&gt;&lt;/div&gt;&lt;div class=&quot;grid md:grid-cols-2 gap-6&quot;&gt;&lt;div class=&quot;space-y-3&quot;&gt;&lt;div class=&quot;flex items-center gap-3&quot;&gt;&lt;i class=&quot;icon-[tabler--circle-check] w-5 h-5 text-zinc-500 inline-block&quot;&gt;&lt;/i&gt;&lt;span class=&quot;text-zinc-700 dark:text-zinc-300&quot;&gt;终端应用的现代化可能性&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;flex items-center gap-3&quot;&gt;&lt;i class=&quot;icon-[tabler--circle-check] w-5 h-5 text-zinc-500 inline-block&quot;&gt;&lt;/i&gt;&lt;span class=&quot;text-zinc-700 dark:text-zinc-300&quot;&gt;SSH 协议的灵活应用&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;space-y-3&quot;&gt;&lt;div class=&quot;flex items-center gap-3&quot;&gt;&lt;i class=&quot;icon-[tabler--circle-check] w-5 h-5 text-zinc-500 inline-block&quot;&gt;&lt;/i&gt;&lt;span class=&quot;text-zinc-700 dark:text-zinc-300&quot;&gt;React 在不同平台上的适应性&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;flex items-center gap-3&quot;&gt;&lt;i class=&quot;icon-[tabler--circle-check] w-5 h-5 text-zinc-500 inline-block&quot;&gt;&lt;/i&gt;&lt;span class=&quot;text-zinc-700 dark:text-zinc-300&quot;&gt;AI 技术的普及化&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;SSH AI Chat 是一个融合了多种技术的创新项目。它展示了：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;终端应用的现代化可能性&lt;/li&gt;
&lt;li&gt;SSH 协议的灵活应用&lt;/li&gt;
&lt;li&gt;React 在不同平台上的适应性&lt;/li&gt;
&lt;li&gt;AI 技术的普及化&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这个项目让我意识到，技术不只是为了解决问题，也可以很有趣。把 SSH 和 AI 结合起来，创造出了意想不到的体验。&lt;/p&gt;
&lt;p&gt;希望这个项目能给大家带来一些启发，让我们一起探索技术的边界！&lt;/p&gt;
&lt;h2 id=&quot;体验地址&quot;&gt;体验地址&lt;/h2&gt;
&lt;div class=&quot;not-prose mb-8&quot;&gt;&lt;div class=&quot;bg-gradient-to-r from-zinc-600 to-zinc-700 rounded-xl p-6 text-white&quot;&gt;&lt;div class=&quot;flex items-center gap-4 mb-4&quot;&gt;&lt;div class=&quot;p-3 bg-white/20 rounded-xl&quot;&gt;&lt;i class=&quot;icon-[tabler--terminal-2] w-6 h-6 block&quot;&gt;&lt;/i&gt;&lt;/div&gt;&lt;div&gt;&lt;h3 class=&quot;text-lg font-bold mb-1&quot;&gt;立即体验&lt;/h3&gt;&lt;p class=&quot;text-zinc-100 text-sm&quot;&gt;一条命令即可开始&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;bg-black/20 rounded-lg p-4 mb-4&quot;&gt;&lt;code class=&quot;text-zinc-200 font-mono text-sm&quot;&gt;&lt;p&gt;ssh your-github-username&lt;span&gt;@&lt;/span&gt;chat.agi.li&lt;/p&gt;&lt;/code&gt;&lt;/div&gt;&lt;div class=&quot;flex items-center gap-4&quot;&gt;&lt;a href=&quot;https://github.com/miantiao-me/ssh-ai-chat&quot; target=&quot;_blank&quot; rel=&quot;nofollow&quot; title=&quot;SSH AI Chat GitHub 仓库&quot; class=&quot;inline-flex items-center gap-2 px-3 py-2 bg-white/20 hover:bg-white/30 rounded-lg transition-colors text-sm&quot;&gt;&lt;i class=&quot;icon-[tabler--brand-github] w-4 h-4 block&quot;&gt;&lt;/i&gt;&lt;span&gt;GitHub&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;
&lt;div class=&quot;not-prose&quot;&gt;&lt;div class=&quot;text-center py-8 border-t border-zinc-200 dark:border-zinc-700&quot;&gt;&lt;p class=&quot;text-zinc-600 dark:text-zinc-400 italic&quot;&gt;&lt;p&gt;如果你对这个项目有任何问题或建议，欢迎在 GitHub 上讨论。也欢迎 Star 这个项目，你的支持是我继续开发的动力！&lt;/p&gt;&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;&lt;img src=&quot;https://c.statcounter.com/9823304/0/92a1d06c/1/&quot; alt=&quot;stat&quot; referrerPolicy=&quot;no-referrer-when-downgrade&quot; /&gt;</content:encoded></item><item><title>在 Docker 沙箱中运行 MCP Server</title><link>https://miantiao.me/posts/guide-to-running-mcp-server-in-a-sandbox/</link><guid isPermaLink="true">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>&lt;p&gt;MCP 是今年 AI 开发行业很热门的一个协议，但是由于它的 C/S 架构，导致使用者必须在本地运行 MCP Server。&lt;/p&gt;
&lt;p&gt;MCP Server 常见的运行方式有 npx(NPM 生态)、uvx(Python 生态)、Docker 等 stdio 方式和 HTTP(SSE/Streaming) 方式。但是 npx 和 uvx 运行命令有着极大的风险。如果不慎执行恶意软件包，可能会导致隐私数据泄露带来极大的安全风险。具体可以看 Invariant 的 &lt;a href=&quot;https://invariantlabs.ai/blog/mcp-security-notification-tool-poisoning-attacks&quot; rel=&quot;nofollow&quot; target=&quot;_blank&quot;&gt;MCP Security Notification: Tool Poisoning Attacks&lt;/a&gt; 这篇文章。&lt;/p&gt;
&lt;p&gt;作为一个软件行业从业者，对安全的关注度极高。让 ChatGPT 整理了一下最近 5 年的 NPM 和 PyPI 供应链攻击事件，让人不寒而栗。&lt;/p&gt;






































































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