<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"><channel><title>Niapya Blog</title><link>https://portfolio.niapya.deno.net/blog?lang=zh-cn</link><description>记录数字产品、界面与实验的个人作品集。</description><language>zh-CN</language><lastBuildDate>Wed, 16 Sep 2026 07:10:19 GMT</lastBuildDate><item><title>为了创建一个猫娘我们都做了什么</title><link>https://portfolio.niapya.deno.net/blog/%E4%B8%BA%E4%BA%86%E5%88%9B%E5%BB%BA%E4%B8%80%E4%B8%AA%E7%8C%AB%E5%A8%98%E6%88%91%E4%BB%AC%E9%83%BD%E5%81%9A%E4%BA%86%E4%BB%80%E4%B9%88?lang=zh-cn</link><guid isPermaLink="true">https://portfolio.niapya.deno.net/blog/%E4%B8%BA%E4%BA%86%E5%88%9B%E5%BB%BA%E4%B8%80%E4%B8%AA%E7%8C%AB%E5%A8%98%E6%88%91%E4%BB%AC%E9%83%BD%E5%81%9A%E4%BA%86%E4%BB%80%E4%B9%88?lang=zh-cn</guid><pubDate>Wed, 16 Sep 2026 07:10:19 GMT</pubDate><description>&lt;p&gt;从 Next.js 到 Nuxt 再到 TanStack Start，5 天内用 Durable Objects、Just Bash 虚拟文件系统、VRM 桌面宠物和自研的 DTP 对话协议，从零搓出一个猫娘 Luna。文章记录了架构选型的取舍、6 小时 Agent 驱动的大规模迁移，以及为什么 Bash 就是 Harness 的全部。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2 id=&quot;起因&quot;&gt;&lt;a class=&quot;anchor&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; href=&quot;#起因&quot;&gt;&lt;svg class=&quot;octicon octicon-link&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot; height=&quot;16&quot; aria-hidden=&quot;true&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;起因&lt;/h2&gt;
&lt;p&gt;我和我的两个朋友组队，三个人报了知乎的 2026 黑客马拉松。&lt;/p&gt;
&lt;p&gt;我们聊了一圈，决定做一个猫娘，她叫 Luna，我们的灵感来自于酒馆式角色聊天🍺，再加上龙虾那种能动手干活的智能体🦐，再配上养成。于是就有了她。&lt;/p&gt;
&lt;p&gt;在生活上，他是你的好伙伴，你的每一个爱好，他都会记到他的记忆里，我们认真设计了 LUNA 的记忆系统，下次再见面，她还记得你喜欢什么。&lt;/p&gt;
&lt;p&gt;在工作上，他还可以帮你处理一些复杂的程序，你可以添加 agent skill，甚至是上传附件，由它调用 python 去解决。&lt;/p&gt;
&lt;p&gt;在娱乐上，她是你的好玩伴，我们做了养成和成就系统：喂她吃饭、给她喝水、帮她清洁，亲密度会慢慢提高。&lt;/p&gt;
&lt;p&gt;我们还为 LUNA 做了一个模型，它也会跟着动。她会先想一想，再一条一条地回你，会发贴纸，会做表情，就好像她真的出现在你面前一样。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/posts/luna/demo-screenshot.jpg&quot; alt=&quot;Luna demo screenshot&quot; /&gt;&lt;/p&gt;
&lt;p&gt;成品在这里：&lt;a href=&quot;https://luna.htu.me&quot; rel=&quot;noopener noreferrer&quot;&gt;LUNA&lt;/a&gt;，所以为了创建一个猫娘，我们都做了什么？&lt;/p&gt;
&lt;h2 id=&quot;技术设计&quot;&gt;&lt;a class=&quot;anchor&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; href=&quot;#技术设计&quot;&gt;&lt;svg class=&quot;octicon octicon-link&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot; height=&quot;16&quot; aria-hidden=&quot;true&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;技术设计&lt;/h2&gt;
&lt;p&gt;为了做出这个产品，我们下了很多功夫，从架构定型、设计整个 Harness，到前端等等，我们都有慎重的考虑，而且做了很多新东西。&lt;/p&gt;
&lt;p&gt;尽管如此，我们这个项目是不到 5 天内做完的，非常仓促。在这期间，我们大概用了 1000 刀的 tokens。&lt;/p&gt;
&lt;p&gt;我们从一开始就把它定为一个完整的全栈 Web App，挑选一个前后端一体框架非常重要，我们本项目最开始的架构是 Next.js，是 OpenNext 驱动的 Next.js。但是有很多原因，我们先后迁到了 Nuxt，到最后的 TanStack Start 框架，这一路的心路历程和背后的取舍，待会儿我都会讲到。&lt;/p&gt;
&lt;h3 id=&quot;基于-cloudflare-durable-objects-设计的-agent-架构&quot;&gt;&lt;a class=&quot;anchor&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; href=&quot;#基于-cloudflare-durable-objects-设计的-agent-架构&quot;&gt;&lt;svg class=&quot;octicon octicon-link&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot; height=&quot;16&quot; aria-hidden=&quot;true&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;基于 Cloudflare Durable Objects 设计的 Agent 架构&lt;/h3&gt;
&lt;p&gt;我们从一开始就把它定为一个完整的前后端一体 Web App，在最开始这个项目定型之前，我们一直把它部署在 OpenNext 驱动的 Next.js 中，部署在 Cloudflare 上。&lt;/p&gt;
&lt;p&gt;我们在第一次开发完成的时候，发现它非常慢，而且 Next.js 的 Adapter API 没有受到 Cloudflare 的支持，所以这让我下定了决心，第一次重构放弃了 Next.js，转向了 Vue 框架的 Nuxt.js，由于 Nuxt 的原语统一了 KV、Database 以及 Blob，所以我们自然选择了 Nuxt。&lt;/p&gt;
&lt;p&gt;Cloudflare 与 Vercel 不同，它是一个集大成的东西，偏向基础设施，一个服务掌控了一切，比如有&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;SQL database：比如说 D1 SQLite&lt;/li&gt;
&lt;li&gt;NoSQL database：比如说 Key-Value 的 KV&lt;/li&gt;
&lt;li&gt;类 S3 的存储：R2 Blob&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;我们一开始设计的架构非常简单，而且符合大众的思考，应该把所有的用户数据、会话数据存到 SQL database 中。用户的附件应该存到 R2 的 Blob 中。&lt;/p&gt;
&lt;p&gt;但是 Cloudflare 是一家 Serverless 服务商，在 Serverless 这种情况下可能会遇到问题。&lt;/p&gt;
&lt;p&gt;第一，与 Supabase 不同，Cloudflare D1 它的服务分布在全球各地，如果我们用的是 D1 的全球副本，数据的复制是异步的。我们在存入数据时，副本可能还没有完成更新，所以读取这些副本会有延迟。&lt;/p&gt;
&lt;p&gt;第二，我们 AI 会话的本身需要并发地大量处理各种的状态更新，用 D1 难以保持它的一致性。&lt;/p&gt;
&lt;p&gt;所以，这就隐性造成了很多问题，特别是在同步会话和信息时，会造成非常大的隐患。我们尝试过把会话数据以 JSON 或其他格式存在 R2 中，写入时加一个锁来保证一致性，但是 R2 的 IO 开销比较大，而且它毕竟是远程存储，不适合频繁地把整个 Session 读出来、修改、写回。&lt;/p&gt;
&lt;p&gt;这个时候我在寻找解决方案时，想到了 Cloudflare 的 Durable Objects。&lt;/p&gt;
&lt;p&gt;DO 用 JavaScript class 去描述，有全局唯一的实例身份，同一个 Durable Object ID 在全局同时只会有一个实例运行，并且每个实例都有自己私有的、事务性的、强一致性的持久化存储，还提供完整的 SQLite 存储 API。&lt;/p&gt;
&lt;div class=&quot;not-prose highlight my-8 max-w-full overflow-hidden rounded border border-border bg-muted text-foreground highlight-source-ts notranslate&quot;&gt;&lt;pre class=&quot;not-prose m-0 max-w-full overflow-x-auto bg-muted p-5 font-mono text-sm text-foreground leading-6&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;MyDurableObject&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;extends&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;DurableObject&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;Env&lt;span class=&quot;token operator&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;sayHello&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; result &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;ctx&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;storage&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;sql
      &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;exec&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;SELECT &apos;Hello, World!&apos; as greeting&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;one&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; result&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;greeting&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;fetch&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;request&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; env&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; ctx&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token&quot;&gt;Promise&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;Response&lt;span class=&quot;token operator&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; stub &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; env&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token&quot;&gt;MY_DURABLE_OBJECT&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getByName&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token&quot;&gt;USER_ID&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; stub&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;sayHello&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;我们可以把一个用户或者一个会话直接映射成一个 Durable Object，让这个对象本身成为状态的持有者，并负责协调对这个状态的访问。&lt;/p&gt;
&lt;p&gt;Durable Objects 以 Serverless 形态运行，按需唤醒，接到请求或者其他 Worker 的 RPC 调用时启动，并在一段时间内销毁。&lt;/p&gt;
&lt;p&gt;Durable Objects 还实现了 Alarm，符合我们未来要做的定时任务的需求。&lt;/p&gt;
&lt;p&gt;Durable Objects 只有 Cloudflare 才有，所以我们主动放弃了平台无关性，我们分了三层来重新构思这个持久化设计：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;第一层是基于 D1 SQLite 的用户和验证：我们使用 Better Auth 和 Drizzle 来完成&lt;/li&gt;
&lt;li&gt;第二层是基于 Durable Objects 的用户数据，比如说记忆、定时任务、配置，他们存储在 User DO 的 &lt;code&gt;ctx.storage.sql&lt;/code&gt; 中。&lt;/li&gt;
&lt;li&gt;第三层是基于 Durable Objects 的会话设计，我们使用 Cloudflare Agents SDK 来完成会话设计&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;not-prose m-0 max-w-full overflow-x-auto bg-muted p-5 font-mono text-sm text-foreground leading-6&quot;&gt;&lt;code&gt;           AI Chat
               │
      ┌────────┴────────┐
      │                 │
     D1                DO
      │                 │
     Auth        ┌──────┴──────┐
                 │             │
               User ──────&amp;gt; Sessions
                 │             │
              State A       State B&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;这样下来，我们让 SQL 负责相对稳定的全局数据，把真正需要强一致、并发协调的状态交给 Durable Objects。&lt;/p&gt;
&lt;p&gt;具体到 Luna，就是两个 DO 各司其职：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;UserDO&lt;/code&gt; 按用户维度持有记忆、配置、宠物数值、技能清单和文件索引。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ChatDO&lt;/code&gt; 按会话维度持有对话历史和模型轮次。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;同一个用户或同一个会话在全球同时只有一个实例在运行，并发写冲突由 DO 单例本身协调。&lt;/p&gt;
&lt;p&gt;Worker 收到用户消息后唤醒对应的 &lt;code&gt;ChatAgent&lt;/code&gt;，&lt;code&gt;ChatAgent&lt;/code&gt; 再去 &lt;code&gt;UserAgent&lt;/code&gt; 拉取记忆构成 system prompt，然后一边流式调用模型、一边把模型的增量输出翻译成结构化帧落入存储，最后以 SSE 流回浏览器。&lt;/p&gt;
&lt;h3 id=&quot;使用-agent-在-6-个小时从-nuxt-迁移到了-tanstack-start&quot;&gt;&lt;a class=&quot;anchor&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; href=&quot;#使用-agent-在-6-个小时从-nuxt-迁移到了-tanstack-start&quot;&gt;&lt;svg class=&quot;octicon octicon-link&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot; height=&quot;16&quot; aria-hidden=&quot;true&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;使用 Agent 在 6 个小时从 Nuxt 迁移到了 TanStack Start&lt;/h3&gt;
&lt;p&gt;之前说过，我们项目最开始是用 OpenNext 的 Next.js 来实现的，然后我们迁移到了 Vue 的 Nuxt.js，因为我们看中了 Nuxt Hub 的平台无关性。&lt;/p&gt;
&lt;p&gt;但是，当我们确定 DO 架构时，发现 Next.js 并不能真正处理 Cloudflare Durable Objects 的绑定，这是一个 Cloudflare 的已知 &lt;a href=&quot;https://developers.cloudflare.com/workers/wrangler/api/#supported-bindings&quot; rel=&quot;noopener noreferrer&quot;&gt;issue&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/posts/luna/cf-bindings-issue.png&quot; alt=&quot;Cloudflare DO binding not supported in Nuxt&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;UserDO&lt;/code&gt; / &lt;code&gt;ChatDO&lt;/code&gt; 在 Nuxt 的适配层里根本拿不到，缺少 DO 绑定就无法继续推进。&lt;/p&gt;
&lt;p&gt;所以我们决定：用 Agent 把整个项目从 Nuxt 搬到 TanStack Start。&lt;/p&gt;
&lt;p&gt;这次迁移花了大概 6 个小时，思路是分而治之、ReAct 与 测试。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第一步，先把依赖的映射关系理清楚。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Nuxt 是一个重度约定的前后端一体框架，字体、图片、UI、内容、动画都有官方模块。我们开了多个 subagent 并行去找 React 生态的对位替代——比如 Nuxt Content 对应 Content Collections，Nuxt Hub 对应 Cloudflare Vite 插件，motion-v 对应 motion，TresJS 对应 React Three Fiber，Nuxt UI 对应 shadcn/ui。React 生态足够大，这一步几乎没有卡点。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/posts/luna/nuxt-to-tanstack-migration.png&quot; alt=&quot;Nuxt to TanStack Start migration flow&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第二步，配置 Skills 和 Hooks。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;通过 Context7 抓取目标库的最新文档，并用 &lt;code&gt;npx skills find&lt;/code&gt; 和 &lt;code&gt;bunx @tanstack/intent&lt;/code&gt; 安装官方 Skills，避免 Agent 靠编写错误的 API。&lt;/p&gt;
&lt;p&gt;然后我们还配置了 ESLint 和 Prettier，并设置了 hooks，使它在每一步完成后，都能及时地进行类型检查、lint 检查以及格式化检查。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第三步，拆解层级，定好 monorepo 的边界。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Nuxt 的 &lt;code&gt;layers/&lt;/code&gt;（按 &lt;code&gt;00.site&lt;/code&gt; / &lt;code&gt;01.auth&lt;/code&gt; / &lt;code&gt;06.chat&lt;/code&gt; … 数字前缀决定优先级）被拆成 &lt;code&gt;packages/@luna/*&lt;/code&gt;，&lt;code&gt;apps/web&lt;/code&gt; 作为唯一可部署的 TanStack Start 应用，其余包只通过子路径导出对外暴露，禁止跨层反向依赖。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/posts/luna/monorepo-structure.png&quot; alt=&quot;Monorepo directory structure&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Nuxt 的自动发现被显式的 workspace 依赖取代，结构反而更清晰了。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第四步，UI 体系从 Nuxt UI 切到 shadcn。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;列出所有用到的组件，逐一映射到 shadcn 的 Components，我们既然都 monorepo 了，就都放在 &lt;code&gt;@luna/ui&lt;/code&gt; 子 repo 中。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第五步，主 Agent 列出包含 subagent 顺序计划，开始实现&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这一步是关键，由于各种 layer 相互错综复杂，所以我们在制定计划时，必须要考虑 subagent 的顺序，哪一步是并行，哪一步是先后的。&lt;/p&gt;
&lt;p&gt;我们必须综合速度和质量，考虑各个 monorepo 的顺序来重构，这样才能重构成功。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;最后，启动服务，使用 Chrome MCP 测试。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在 &lt;code&gt;127.0.0.1:3000&lt;/code&gt; 拉起开发服务器，用浏览器自动化走一遍完整链路，确认 DO 的 RPC 和 SSE 能够正常连通。&lt;/p&gt;
&lt;p&gt;我们没有写任何 codemod 全部由 AI 来搞定，我们使用 &lt;code&gt;Grok 4.6&lt;/code&gt; 来做到这一切，6 小时后，已经可以在本地跑通完整的对话链路。&lt;/p&gt;
&lt;h3 id=&quot;threejs-与-vrm-的宠物&quot;&gt;&lt;a class=&quot;anchor&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; href=&quot;#threejs-与-vrm-的宠物&quot;&gt;&lt;svg class=&quot;octicon octicon-link&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot; height=&quot;16&quot; aria-hidden=&quot;true&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Three.js 与 VRM 的宠物&lt;/h3&gt;
&lt;p&gt;为了拟人感，也是为了有虚拟现实的体验，我们一开始就打算使用一些模型，比如说 Live2D 或者是 3D 的模型。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/posts/luna/vrm-model.png&quot; alt=&quot;VRM model preview&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这个 VRM 模型是我们队的美术担当花一个下午制作出来的，从 VRoid Studio 捏人、调整骨骼，到导出 &lt;code&gt;.vrm&lt;/code&gt;，再到在 Three.js 里点亮，前后大概 4 个小时。&lt;/p&gt;
&lt;p&gt;VRM 本质上是带扩展的 glTF，&lt;code&gt;@pixiv/three-vrm&lt;/code&gt; 会处理 humanoid 骨骼、人形约束和表情 blendshape。&lt;/p&gt;
&lt;p&gt;思路上我们把「渲染」和「状态」分开。&lt;/p&gt;
&lt;p&gt;渲染是一个 R3F 场景，负责加载 VRM、计算包围盒自适应相机、混合动画过渡；状态侧是一个小的状态机，管理 idle / happy / sleep 等动作的切换。&lt;/p&gt;
&lt;p&gt;为了性能，Three.js 场景只在宠物面板展开时才加载，收起时只保留数值面板。&lt;/p&gt;
&lt;p&gt;状态有一套数值系统（饱食度、心情、亲密度），存在 &lt;code&gt;UserDO&lt;/code&gt; 里，每次对话结束会微调。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/posts/luna/pet-status-panel.png&quot; alt=&quot;Pet status panel&quot; /&gt;&lt;/p&gt;
&lt;p&gt;关于动作，我们使用下面要讲到的 Dialogue Transport Protocol 来在会话时执行 VRMA 动作。&lt;/p&gt;
&lt;p&gt;在喂食和清洁等操作时，我们还预设了一些动作，我们还在内部维护了一个状态机，在 idle 时也会偶尔执行动作，以免用户无聊。&lt;/p&gt;
&lt;h3 id=&quot;bash-tool-就是你-harness-的全部&quot;&gt;&lt;a class=&quot;anchor&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; href=&quot;#bash-tool-就是你-harness-的全部&quot;&gt;&lt;svg class=&quot;octicon octicon-link&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot; height=&quot;16&quot; aria-hidden=&quot;true&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Bash Tool 就是你 Harness 的全部&lt;/h3&gt;
&lt;p&gt;然后我们开始设计整个 Harness，在 Agent Loop 就是老的 Pi 的那一套，存储消息，估算 token，达到预算后压缩。&lt;/p&gt;
&lt;p&gt;但是我们真正要考虑的是 function calling。&lt;/p&gt;
&lt;p&gt;我们每次请求 AI，请求之间的往返是有时间成本的。如果我们把所有的聊天工具、宠物工具以及其他工具都做成 tool， API 的次数会很多，而且会造成很大的时间成本，所以关于这个设计，我们有一个消息协议和三个工具。&lt;/p&gt;
&lt;p&gt;协议等会儿我们再讲，我们先说这三个工具。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;view：AI 的眼睛，可以通过指定行数来查看大文本，它还支持多模态媒体，比如图片、视频和音频&lt;/li&gt;
&lt;li&gt;patch：AI 的手，可以一次性更改多个文件&lt;/li&gt;
&lt;li&gt;bash：AI 的心脏，其他所有的功能都会通过 Bash 解决&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;一个虚拟的-fs&quot;&gt;&lt;a class=&quot;anchor&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; href=&quot;#一个虚拟的-fs&quot;&gt;&lt;svg class=&quot;octicon octicon-link&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot; height=&quot;16&quot; aria-hidden=&quot;true&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;一个虚拟的 FS&lt;/h4&gt;
&lt;p&gt;我们希望 Luna 拥有一个“像真实电脑一样”的文件空间，这样他可以参与复杂任务。&lt;/p&gt;
&lt;p&gt;有目录、有文件、能安装技能、能保存附件。但 Cloudflare Workers 没有本地文件系统，R2 是对象存储，DO SQLite 也不适合直接存放大块文件内容。&lt;/p&gt;
&lt;p&gt;所以我们做了一层虚拟文件系统（VFS），把「索引」和「内容」分开。&lt;/p&gt;
&lt;p&gt;索引是一份 &lt;code&gt;manifest&lt;/code&gt;，存在 &lt;code&gt;User&lt;/code&gt; 的 Durable SQLite 里，记录每个路径对应的对象 key、大小和类型。&lt;/p&gt;
&lt;p&gt;真正的文件内容作为不可变对象存在 R2 里，每次写入生成一个新的 UUID key，旧对象由后台任务回收。&lt;/p&gt;
&lt;p&gt;目录也做了权限划分，&lt;code&gt;/luna/system&lt;/code&gt; 是只读的系统技能，&lt;code&gt;/luna/user/&amp;lt;id&amp;gt;/skills&lt;/code&gt; 和 &lt;code&gt;/luna/user/&amp;lt;id&amp;gt;/attachments&lt;/code&gt; 才是用户可写的空间，其他路径一律拒绝。&lt;/p&gt;
&lt;pre class=&quot;not-prose m-0 max-w-full overflow-x-auto bg-muted p-5 font-mono text-sm text-foreground leading-6&quot;&gt;&lt;code&gt;/luna/system
    └── READ ONLY

/luna/user/&amp;lt;id&amp;gt;/
    ├── skills/       ← READ + WRITE
    └── attachments/  ← READ + WRITE

其他路径
    └── DENY&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;并发则用 CAS（compare-and-swap）保证：每次提交带上预期的版本号，冲突就返回错误并重试，避免两个轮次同时写入把索引破坏。&lt;/p&gt;
&lt;p&gt;限制也写得很明确，单文件 5 MiB、最多 100 个文件、总计 20 MiB。&lt;/p&gt;
&lt;h4 id=&quot;再给一个虚拟-bash&quot;&gt;&lt;a class=&quot;anchor&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; href=&quot;#再给一个虚拟-bash&quot;&gt;&lt;svg class=&quot;octicon octicon-link&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot; height=&quot;16&quot; aria-hidden=&quot;true&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;再给一个虚拟 Bash&lt;/h4&gt;
&lt;p&gt;有了文件系统，下一步就是让 Agent 能使用更加强大的工作，&lt;code&gt;bash&lt;/code&gt; 承担 90% 的能力，AI 经过的训练已经包含大部分的 Bash 语料，所以它本身就已经熟悉 Bash。因此，我们需要给 AI 一个 Bash 工具。&lt;/p&gt;
&lt;p&gt;但是我们不能真的给 Agent 做一个真实的 Bash。因为有两点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;成本：如果我们每一个对话都给 Agent 分配一个 container，那么成本会很大&lt;/li&gt;
&lt;li&gt;安全：如果给他真实的 Bash 环境，那么权限和安全就变得不可控，&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;所以我们给 Agent 一个虚拟的 Bash，Bash 的运行时是 &lt;a href=&quot;https://justbash.dev&quot; rel=&quot;noopener noreferrer&quot;&gt;Just Bash&lt;/a&gt;，一个能在 JS 环境里运行的 Bash 模拟器，我们运行在 Cloudflare Workers 里。&lt;/p&gt;
&lt;p&gt;它的文件系统就是上面那套 VFS，网络开放了 &lt;code&gt;curl&lt;/code&gt;，执行有配额限制，每次对话都在一个隔离的 shell 里执行，不会污染其他轮次。&lt;/p&gt;
&lt;p&gt;因为是虚拟 Bash，单一能力的工具就没有保留的必要，我们设计了一个自定义的工厂函数，他可以自定义 command。&lt;/p&gt;
&lt;p&gt;比如我们可以设计一个 &lt;code&gt;skill&lt;/code&gt; 工具，可以让他搜索和读取 skills。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;我们可以给 Agent 添加新功能的方式就是自定义 command + skills 的做法&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;想让 Luna 记住一件事，直接往文件里追加一行，想安装一个 skill，用 &lt;code&gt;curl&lt;/code&gt; 拉取后保存到技能目录，想查询天气、搜索网页、拉取知乎热榜，这些在 Luna 里都注册为 Bash 的自定义命令，既可以在 shell 里以 CLI 形式调用，也可以被模型以工具形式调用——同一套注册表，两套入口。&lt;/p&gt;
&lt;p&gt;甚至有些是 client command，部分命令需要浏览器配合，比如获取定位、发送通知、切换主题，它们会通过 &lt;code&gt;ChatDO&lt;/code&gt; 的 WebSocket 往返到当前标签页，这种感觉真的很奇妙，这可能就是「人机恋」的魅力吧。&lt;/p&gt;
&lt;p&gt;我们把 Harness 做成了文件系统 + Bash，模型学会使用 Bash，就学会了操作 Luna 的整个世界。&lt;/p&gt;
&lt;h3 id=&quot;介绍我们研发的新协议-dialogue-transport-protocol&quot;&gt;&lt;a class=&quot;anchor&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; href=&quot;#介绍我们研发的新协议-dialogue-transport-protocol&quot;&gt;&lt;svg class=&quot;octicon octicon-link&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot; height=&quot;16&quot; aria-hidden=&quot;true&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;介绍我们研发的新协议 Dialogue Transport Protocol&lt;/h3&gt;
&lt;p&gt;刚才我说了，每一次 AI 的 API 往返都是有时间和价值成本的，所以我希望它一次可以返回多个信息。&lt;/p&gt;
&lt;p&gt;我们最开始设计的方案是 JSONL，每一行都是一个独立的 JSON。&lt;/p&gt;
&lt;pre class=&quot;not-prose m-0 max-w-full overflow-x-auto bg-muted p-5 font-mono text-sm text-foreground leading-6&quot;&gt;&lt;code&gt;{type: &quot;text&quot;, content: &quot;hello\nworld&quot;}
{type: &quot;sticker&quot;, id: &quot;smile&quot;}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;但是我们很快就发现一些问题。既然让 AI 来输出，它的输出就是不稳定的。&lt;/p&gt;
&lt;p&gt;比如 JSONL，我们每行都是一个 JSON，看起来实现很方便。但是，所有的换行都需要 AI 主动用转义符转义，AI 很难做到这一点，所以很难解析成功。&lt;/p&gt;
&lt;p&gt;所以我们要设计一个全新的协议，需要满足以下条件：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;不能与大部分常见的语言重合&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;因为 AI 输出是不稳定的，我们不能去直接设计成 JSON 或者是 XML 的格式，因为如果我们直接让 AI 去输出代码，它们可能会直接用这些格式，直接会造成格式冲突。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;支持多条消息：&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;AI 直接生成协议文本，我们希望可以包含多个消息、多个事件，而且可以自定义属性。&lt;/p&gt;
&lt;p&gt;消息有很多类型，例如文字、图片、语音、视频。&lt;/p&gt;
&lt;p&gt;那么事件也有可能包含多个类型，比如反应、投票。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;足够熟悉、简单：&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;门槛不能太高，而且可以容错，例如像 wired HTML 一样，我们即使标签没有闭合，即使它的属性带有引号或者是属性，或者是嵌套错误、重复嵌套，也能够解析。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;解析简单&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;便于跨平台、跨语言迁移。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;人类可读&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;AI 输出的是文本，人类阅读的也是文本。&lt;/p&gt;
&lt;p&gt;所以我们设计了我们自己的 frame，我们把它称作 &lt;code&gt;Dialogue Transport Protocol&lt;/code&gt;(DTP) 协议。&lt;/p&gt;
&lt;pre class=&quot;not-prose m-0 max-w-full overflow-x-auto bg-muted p-5 font-mono text-sm text-foreground leading-6&quot;&gt;&lt;code&gt;┌──────────────────────────────────────────┐
│ &amp;lt;|message                                │
│   id=msg_123                             │  ← Header / Envelope
│   type=text                              │
│   reply=msg_100                          │
│   target=user_42                         │
│   ...                                    │
│ |&amp;gt;                                       │
├──────────────────────────────────────────┤
│                                          │
│  Hello!                                  │
│                                          │  ← Payload
│  This is **Markdown**.                   │
│                                          │
│  ```ts                                   │
│  console.log(&quot;hello&quot;)                    │
│  ```                                     │
│                                          │
├──────────────────────────────────────────┤
│ &amp;lt;|/message|&amp;gt;                             │  ← End
└──────────────────────────────────────────┘&lt;/code&gt;&lt;/pre&gt;&lt;h4 id=&quot;协议长什么样&quot;&gt;&lt;a class=&quot;anchor&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; href=&quot;#协议长什么样&quot;&gt;&lt;svg class=&quot;octicon octicon-link&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot; height=&quot;16&quot; aria-hidden=&quot;true&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;协议长什么样&lt;/h4&gt;
&lt;p&gt;DTP 的每一帧都是这样的文本——分隔符用 &lt;code&gt;&amp;lt;| ... |&amp;gt;&lt;/code&gt;，几乎不会和 Markdown、代码块、HTML 冲突，模型不需要转义就能稳定生成。&lt;/p&gt;
&lt;p&gt;帧头是类 HTML 属性的键值对，&lt;code&gt;id&lt;/code&gt; / &lt;code&gt;type&lt;/code&gt; / &lt;code&gt;reply&lt;/code&gt; / &lt;code&gt;target&lt;/code&gt; 等信封字段由协议定义，其他字段由具体类型自行扩展。&lt;/p&gt;
&lt;p&gt;载荷就是普通的 Markdown 文本。&lt;/p&gt;
&lt;pre class=&quot;not-prose m-0 max-w-full overflow-x-auto bg-muted p-5 font-mono text-sm text-foreground leading-6&quot;&gt;&lt;code&gt;&amp;lt;|message id=a type=text |&amp;gt;
Hello
&amp;lt;|/message|&amp;gt;

&amp;lt;|message id=b type=sticker |&amp;gt;

&amp;lt;|message id=c type=reaction target=a emoji=❤️ |&amp;gt;

&amp;lt;|message id=d type=vote target=a options=&quot;A,B,C&quot; |&amp;gt;&lt;/code&gt;&lt;/pre&gt;&lt;h4 id=&quot;流式解析&quot;&gt;&lt;a class=&quot;anchor&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; href=&quot;#流式解析&quot;&gt;&lt;svg class=&quot;octicon octicon-link&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot; height=&quot;16&quot; aria-hidden=&quot;true&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;流式解析&lt;/h4&gt;
&lt;p&gt;模型是逐 token 流式输出的，DTP 提供了流式解析器，每收到一段就喂进去，结束时做隐式闭合和引用解析。&lt;/p&gt;
&lt;p&gt;它和批量解析共享同一套词法和语法，行为完全一致。&lt;/p&gt;
&lt;p&gt;在 Luna 的对话链路里，流式解析器就夹在模型输出和前端渲染之间，把文本流实时翻译成结构化帧，再分发到 SSE 推送、DO 存储，然后 dispatch。&lt;/p&gt;
&lt;p&gt;历史消息先被反序列化为 DTP 文本塞进 system prompt，让模型看到用哪些 id 去回复。&lt;/p&gt;
&lt;p&gt;模型生成时，流式解析器把增量文本翻译成帧，并把临时 id 重映射为服务端稳定 id。&lt;/p&gt;
&lt;p&gt;最后根据帧类型发送前端，然后分发出对应的消息和事件。&lt;/p&gt;
&lt;p&gt;整个过程对前端是透明的，收到的就是一条条带类型的消息。&lt;/p&gt;
&lt;h2 id=&quot;尾声&quot;&gt;&lt;a class=&quot;anchor&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; href=&quot;#尾声&quot;&gt;&lt;svg class=&quot;octicon octicon-link&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot; height=&quot;16&quot; aria-hidden=&quot;true&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;尾声&lt;/h2&gt;
&lt;p&gt;5 天 3 个人花了 1000 刀 tokens做了一个我们的 OC 猫娘，路程真的是艰辛。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;为了强一致的会话，我们放弃了平台无关性，选择 all-in Cloudflare Durable Objects。&lt;/li&gt;
&lt;li&gt;为了让 Harness 足够通用，我们放弃了一堆零散的工具，只保留 Bash。&lt;/li&gt;
&lt;li&gt;为了让协议满足需要，我们放弃了 JSON，选择了我们设计的消息协议。&lt;/li&gt;
&lt;li&gt;为了让架构真正跑通，我们放弃了 Nuxt 的舒适区，用 Agent 在 6 小时内搬完了整个应用。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这些取舍让 Luna 拥有了自己的文件系统、自己的身体、自己的记忆和技能，以及自己的通信协议。&lt;/p&gt;
&lt;p&gt;如果你也想养一只 Luna，欢迎来 &lt;a href=&quot;https://luna.htu.me/chat&quot; rel=&quot;noopener noreferrer&quot;&gt;luna.htu.me/chat&lt;/a&gt; 找她聊天。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/posts/luna/luna-chat-screenshot.jpg&quot; alt=&quot;Luna chat interface&quot; /&gt;&lt;/p&gt;
&lt;p&gt;它开源在 &lt;a href=&quot;https://github.com/HackHTU/LUNA&quot; rel=&quot;noopener noreferrer&quot;&gt;LUNA&lt;/a&gt; 中。&lt;/p&gt;
</description></item><item><title>迁移到 remix@3</title><link>https://portfolio.niapya.deno.net/blog/%E8%BF%81%E7%A7%BB%E5%88%B0-remix-3?lang=zh-cn</link><guid isPermaLink="true">https://portfolio.niapya.deno.net/blog/%E8%BF%81%E7%A7%BB%E5%88%B0-remix-3?lang=zh-cn</guid><pubDate>Wed, 29 Jul 2026 21:24:00 GMT</pubDate><description>&lt;p&gt;迁移到 Remix v3 后，我们发现它抛弃了 React，改用自研的 TypeScript UI 框架和显式的 Islands 模型，默认在服务器生成完整 HTML，仅在标记为 &lt;code&gt;clientEntry&lt;/code&gt; 的组件中才会注入 JavaScript。借助 Remix 内置的 TypeScript/JSX loader 与 asset server，实现了“无构建步骤”的按需 ESM 加载，同时配合 Twind 动态生成 Tailwind CSS、Satori + Resvg 生成 SVG/PNG 动态图片，让交互可以完全依赖原生表单或视图过渡。尽管框架仍在 Beta、生态尚不成熟，需要自行编写中间件并适配部署平台，但作者认为 Remix v3 已经能够在保持类型安全、组件化与渐进增强的前提下，实现无需 bundle 的现代 Web 应用，并计划继续关注其发展。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;最近，我们把这个博客迁移到了 &lt;code&gt;remix@3&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;Remix v3 不是 Remix v2 的一次常规升级，它抛弃了
React，之后自己做了一个全栈框架。它仍处于 Beta 阶段，API 和生态都很年轻。&lt;/p&gt;
&lt;p&gt;我决定在这么早的阶段使用 Remix v3，是因为我真的喜欢它，它回归到了 Web API
的本质。&lt;/p&gt;
&lt;h2 id=&quot;一瞥-remix-v3&quot;&gt;&lt;a class=&quot;anchor&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; href=&quot;#一瞥-remix-v3&quot;&gt;&lt;svg class=&quot;octicon octicon-link&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot; height=&quot;16&quot; aria-hidden=&quot;true&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;一瞥 Remix v3&lt;/h2&gt;
&lt;p&gt;Remix v3 最引人注目的是没有用 React ，而是重写了一套 TypeScript UI 框架。&lt;/p&gt;
&lt;p&gt;Remix 组件不是 React 组件，也没有 Hooks。组件接收一个
&lt;code&gt;handle&lt;/code&gt;，然后返回真正的渲染函数。&lt;/p&gt;
&lt;div class=&quot;not-prose highlight my-8 max-w-full overflow-hidden rounded border border-border bg-muted text-foreground highlight-source-tsx notranslate&quot;&gt;&lt;pre class=&quot;not-prose m-0 max-w-full overflow-x-auto bg-muted p-5 font-mono text-sm text-foreground leading-6&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;Greeting&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;handle&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Handle&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; name&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;p&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;Hello, &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;handle&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;props&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;name&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;p&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;状态更新、事件、服务端渲染和水合都围绕这套模型展开。页面默认在服务器上生成
HTML；只有被 &lt;code&gt;clientEntry()&lt;/code&gt; 明确标记的组件，才会成为浏览器里的 JavaScript
边界。服务端会为这些边界留下标记并序列化 props，浏览器中的 &lt;code&gt;run()&lt;/code&gt;
再按需加载对应模块并完成水合。比如一个计数器可以显式声明为 client entry：&lt;/p&gt;
&lt;div class=&quot;not-prose highlight my-8 max-w-full overflow-hidden rounded border border-border bg-muted text-foreground highlight-source-tsx notranslate&quot;&gt;&lt;pre class=&quot;not-prose m-0 max-w-full overflow-x-auto bg-muted p-5 font-mono text-sm text-foreground leading-6&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; Counter &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;clientEntry&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;meta&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;url&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;Counter&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;handle&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Handle&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; initialCount&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token&quot;&gt;number&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; count &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; handle&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;props&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;initialCount&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
      &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;button&lt;/span&gt;
        &lt;span class=&quot;token attr-name&quot;&gt;mix&lt;/span&gt;&lt;span class=&quot;token script&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;on&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;click&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
          count&lt;span class=&quot;token operator&quot;&gt;++&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
          handle&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;update&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;count&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;button&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这里的 &lt;code&gt;import.meta.url&lt;/code&gt; 是服务端源码地址，比如
&lt;code&gt;file:///app/components/counter.tsx&lt;/code&gt;。SSR 遇到这个组件时会调用
&lt;code&gt;resolveClientEntry()&lt;/code&gt;，把源码地址交给 asset server，换成浏览器能访问的 URL：&lt;/p&gt;
&lt;div class=&quot;not-prose highlight my-8 max-w-full overflow-hidden rounded border border-border bg-muted text-foreground highlight-source-tsx notranslate&quot;&gt;&lt;pre class=&quot;not-prose m-0 max-w-full overflow-x-auto bg-muted p-5 font-mono text-sm text-foreground leading-6&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; stream &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;renderToStream&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;App&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;resolveClientEntry&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;entryId&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; component&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      href&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; assetServer&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getHref&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;entryId&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      exportName&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; component&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;name&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;服务端仍然先输出按钮的 HTML，但会使用注释标记它拥有的 DOM
区域，大致类似下面这样：&lt;/p&gt;
&lt;div class=&quot;not-prose highlight my-8 max-w-full overflow-hidden rounded border border-border bg-muted text-foreground highlight-source-html notranslate&quot;&gt;&lt;pre class=&quot;not-prose m-0 max-w-full overflow-x-auto bg-muted p-5 font-mono text-sm text-foreground leading-6&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;&amp;lt;!--rmx:h:counter-1--&amp;gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;button&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;0&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;button&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;&amp;lt;!--/rmx:h--&amp;gt;&lt;/span&gt;

&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;script&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;application/json&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;token&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;rmx-data&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token script&quot;&gt;&lt;span class=&quot;token&quot;&gt;
&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;c&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;counter-1&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;&quot;moduleUrl&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;/assets/app/components/counter.tsx&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;&quot;exportName&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Counter&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;&quot;props&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token property&quot;&gt;&quot;initialCount&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;script&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;上面的 HTML 是为了说明协议而简化的，但关键结构来自 Remix runtime：一对 &lt;code&gt;rmx:h&lt;/code&gt;
注释负责划定水合区域，&lt;code&gt;rmx-data&lt;/code&gt; 保存模块地址、export name 和可序列化的
props。即使 JavaScript 还没有下载，用户已经能看到服务器输出的按钮。&lt;/p&gt;
&lt;p&gt;接着浏览器入口启动 &lt;code&gt;run()&lt;/code&gt;。runtime 读取 &lt;code&gt;rmx-data&lt;/code&gt;、遍历注释标记，并把每个
&lt;code&gt;moduleUrl + exportName&lt;/code&gt; 交给 &lt;code&gt;loadModule()&lt;/code&gt;：&lt;/p&gt;
&lt;div class=&quot;not-prose highlight my-8 max-w-full overflow-hidden rounded border border-border bg-muted text-foreground highlight-source-ts notranslate&quot;&gt;&lt;pre class=&quot;not-prose m-0 max-w-full overflow-x-auto bg-muted p-5 font-mono text-sm text-foreground leading-6&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; app &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;loadModule&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;moduleUrl&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; exportName&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; module &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;moduleUrl&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; module&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;exportName&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; app&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;ready&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;此时才发生真正的网络请求：浏览器执行
&lt;code&gt;import(&quot;/assets/app/components/counter.tsx&quot;)&lt;/code&gt;时 asset server 从磁盘读取 TSX，用
Oxc 解析和转换语法，再把文件里的 &lt;code&gt;import&lt;/code&gt; 改写成公开 URL。&lt;/p&gt;
&lt;p&gt;浏览器拿回的是标准 ESM，&lt;code&gt;loadModule()&lt;/code&gt; 取出其中的 &lt;code&gt;Counter&lt;/code&gt; export，runtime
用序列化 props 重建虚拟节点，最后在两条注释之间 hydrate 已有 DOM，并挂上 click
listener。相同模块的加载还会被缓存，页面上有十个 Counter 也不会请求十次。&lt;/p&gt;
&lt;p&gt;你可能开始感觉有点复杂，但这是一套真正显式的 Islands
架构，不是先把整棵组件树变成客户端应用，再想办法减少
JavaScript，而是默认没有客户端组件，只有确实需要交互的地方才去添加岛屿。&lt;/p&gt;
&lt;p&gt;不仅如此，在 AI 时代，Remix 写了一切。&lt;code&gt;remix/data-schema&lt;/code&gt; 提供了类似
Zod、Valibot 的 schema、检查、转换和 &lt;code&gt;parseSafe()&lt;/code&gt;，它也支持 Standard
Schema，所以不得不佩服 Remix 的胆量。&lt;/p&gt;
&lt;p&gt;最简单的对象验证很像其他 schema library，schema
本身同时描述了运行时约束和输出类型：&lt;/p&gt;
&lt;div class=&quot;not-prose highlight my-8 max-w-full overflow-hidden rounded border border-border bg-muted text-foreground highlight-source-ts notranslate&quot;&gt;&lt;pre class=&quot;not-prose m-0 max-w-full overflow-x-auto bg-muted p-5 font-mono text-sm text-foreground leading-6&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;as&lt;/span&gt; s &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;remix/data-schema&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; email&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; minLength &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;remix/data-schema/checks&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; User &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; s&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;object&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  name&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; s&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;pipe&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;minLength&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  email&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; s&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;pipe&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  role&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; s&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;defaulted&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;s&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;reader&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; user &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; s&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;parse&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;User&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  name&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Ada&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  email&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;ada@example.com&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;最后，它遵循 Standard Schema。&lt;code&gt;s.parse()&lt;/code&gt; 和 &lt;code&gt;s.parseSafe()&lt;/code&gt; 不只接受 Remix
自己的 schema，也能接收 Zod、Valibot 或 ArkType schema。&lt;/p&gt;
&lt;p&gt;除此之外，Remix 还提供路由、中间件、会话、认证和 data table
等一系列彼此配合的包。所有能力都从 &lt;code&gt;remix/&amp;lt;subpath&amp;gt;&lt;/code&gt; 导入，边界非常清楚。&lt;/p&gt;
&lt;h3 id=&quot;why-should-we-have-a-build-step&quot;&gt;&lt;a class=&quot;anchor&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; href=&quot;#why-should-we-have-a-build-step&quot;&gt;&lt;svg class=&quot;octicon octicon-link&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot; height=&quot;16&quot; aria-hidden=&quot;true&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Why should we have a build step?&lt;/h3&gt;
&lt;p&gt;Remix v3 的另一个激进选择，是不把 build step 当作应用运行的前提。&lt;/p&gt;
&lt;p&gt;Remix 自己写了一个 TypeScript/JSX loader，在 Node.js 中，可以通过 &lt;code&gt;--import&lt;/code&gt;
flags 使用。&lt;/p&gt;
&lt;div class=&quot;not-prose highlight my-8 max-w-full overflow-hidden rounded border border-border bg-muted text-foreground highlight-source-sh notranslate&quot;&gt;&lt;pre class=&quot;not-prose m-0 max-w-full overflow-x-auto bg-muted p-5 font-mono text-sm text-foreground leading-6&quot;&gt;&lt;span class=&quot;token function&quot;&gt;node&lt;/span&gt; &lt;span class=&quot;token&quot;&gt;--import&lt;/span&gt; remix/node-tsx ./server.ts&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;remix/node-tsx&lt;/code&gt; 会在模块被 Node 执行前转换 &lt;code&gt;.ts&lt;/code&gt; 和 &lt;code&gt;.tsx&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;有人就问了，那浏览器呢？浏览器肯定不能运行 TypeScript 的代码，Remix
的解决办法很巧妙。&lt;/p&gt;
&lt;p&gt;需要客户端 JavaScript 时，Remix 的 asset server 可以从源码提供模块、改写
import，并生成 preload 和带指纹的 URL。比如可以把源码目录映射为 &lt;code&gt;/assets&lt;/code&gt;
下的公开地址：&lt;/p&gt;
&lt;div class=&quot;not-prose highlight my-8 max-w-full overflow-hidden rounded border border-border bg-muted text-foreground highlight-source-ts notranslate&quot;&gt;&lt;pre class=&quot;not-prose m-0 max-w-full overflow-x-auto bg-muted p-5 font-mono text-sm text-foreground leading-6&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; createAssetServer &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;remix/assets&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; assetServer &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;createAssetServer&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  rootDir&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; process&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;cwd&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  basePath&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;/assets&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  fileMap&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;/app/*path&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;app/*path&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;/npm/*path&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;node_modules/*path&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  allow&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;app/assets/**&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;node_modules/**&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  deny&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;app/**/*.server.*&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

router&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;/assets/*&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; request &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&amp;gt;&lt;/span&gt; assetServer&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;fetch&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;request&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;当浏览器请求一个 TSX entry 时，asset server 才转换这个模块，并把它依赖的 import
改写成浏览器可以继续请求的 URL。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;allow&lt;/code&gt; 和 &lt;code&gt;deny&lt;/code&gt;
同时构成安全边界，避免服务端文件意外暴露。它仍然会做编译，但不需要提前运行一次
build，也不需要生成和部署一个 &lt;code&gt;dist&lt;/code&gt; 目录。&lt;/p&gt;
&lt;p&gt;它内部直接组合了 Oxc parser、transform、minifier 和 resolver，再用
&lt;code&gt;es-module-lexer&lt;/code&gt; 改写源码、Lightning CSS 处理样式。&lt;/p&gt;
&lt;p&gt;更惊喜的是 &lt;code&gt;fileMap&lt;/code&gt; 不只可以映射 &lt;code&gt;app/&lt;/code&gt;，也可以把 &lt;code&gt;node_modules/&lt;/code&gt;
放进同一张依赖图：&lt;/p&gt;
&lt;div class=&quot;not-prose highlight my-8 max-w-full overflow-hidden rounded border border-border bg-muted text-foreground highlight-source-ts notranslate&quot;&gt;&lt;pre class=&quot;not-prose m-0 max-w-full overflow-x-auto bg-muted p-5 font-mono text-sm text-foreground leading-6&quot;&gt;fileMap&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;/app/*path&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;app/*path&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;/npm/*path&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;node_modules/*path&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;假设 Counter 里写了 &lt;code&gt;import { on } from &quot;remix/ui&quot;&lt;/code&gt;，asset server 会解析这个裸
specifier，找到 &lt;code&gt;node_modules&lt;/code&gt; 里的真实 package export，再把它改写成类似
&lt;code&gt;/assets/npm/remix/ui/index.js&lt;/code&gt; 的浏览器 URL。浏览器请求这个 URL 时，asset
server 继续读取、分析并返回它；依赖还可以继续引用自己的依赖。整个 node_modules
不是被预先塞进一个 bundle，而是像源码一样按 ESM 依赖图按需加载。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;getPreloads()&lt;/code&gt; 还可以提前遍历这张图，把将要请求的模块变成
&lt;code&gt;&amp;lt;link rel=&quot;modulepreload&quot;&amp;gt;&lt;/code&gt; 或 &lt;code&gt;Link&lt;/code&gt; header：&lt;/p&gt;
&lt;div class=&quot;not-prose highlight my-8 max-w-full overflow-hidden rounded border border-border bg-muted text-foreground highlight-source-ts notranslate&quot;&gt;&lt;pre class=&quot;not-prose m-0 max-w-full overflow-x-auto bg-muted p-5 font-mono text-sm text-foreground leading-6&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; preloads &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; assetServer&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getPreloads&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
  &lt;span class=&quot;token string&quot;&gt;&quot;app/components/counter.tsx&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// /assets/app/components/counter.tsx&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// /assets/npm/remix/ui/index.js&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// ...它们继续依赖的模块&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这就是很 amazing 的地方，它保留了浏览器原生 ESM 的请求模型，同时仍然能执行
TypeScript 转换、package resolution、preload、source map、minify 和
fingerprint，却没有要求我们先产出一个 bundle。&lt;/p&gt;
&lt;h2 id=&quot;准备事项&quot;&gt;&lt;a class=&quot;anchor&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; href=&quot;#准备事项&quot;&gt;&lt;svg class=&quot;octicon octicon-link&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot; height=&quot;16&quot; aria-hidden=&quot;true&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;准备事项&lt;/h2&gt;
&lt;p&gt;其实，我认为原生的 Web API
已经足够强大，我们完全可以依赖原生来实现一切。对此，我为新首页设置了几个目标：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;没有客户端 JavaScript 参与&lt;/p&gt;
&lt;p&gt;博客本来就是一个轻量的程序，所有页面都可以是静态的，没有客户端 JavaScript
参与，顶多在评论时使用表单，这部分完全可以用 HTML 来解决。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;没有构建流程&lt;/p&gt;
&lt;p&gt;Remix 的 dev 脚本、start
脚本都没有用到构建流程，所以我们也不想添加一个，我想要直接 push
到远程仓库，直接可以运行。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;实现一个博客的全部功能&lt;/p&gt;
&lt;p&gt;一个博客应该有统计、评论、验证码等功能，应该有一个好的 UI 和 UX，有一个
TOC，以及很好的交互效果。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;当应用不再依赖传统 bundler，过去由构建工具顺手完成的事情就需要重新设计。为了保留
Tailwind 的开发体验、流式 SSR 和动态图片，我们做了几项额外工作。&lt;/p&gt;
&lt;h3 id=&quot;用-twind-为-ssr-生成-tailwind-css&quot;&gt;&lt;a class=&quot;anchor&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; href=&quot;#用-twind-为-ssr-生成-tailwind-css&quot;&gt;&lt;svg class=&quot;octicon octicon-link&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot; height=&quot;16&quot; aria-hidden=&quot;true&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;用 Twind 为 SSR 生成 Tailwind CSS&lt;/h3&gt;
&lt;p&gt;这个项目使用 Twind 的 Tailwind preset，而不是运行 Tailwind CLI
扫描源码并生成一份静态 CSS。Twind 可以读取最终 HTML 中出现的
class，并按需生成对应样式，这很适合没有构建步骤的服务端应用。&lt;/p&gt;
&lt;p&gt;问题在于 Remix 返回的是流，而不是一个完整字符串。为此我们写了一个 &lt;code&gt;twind()&lt;/code&gt;
中间件：&lt;/p&gt;
&lt;div class=&quot;not-prose highlight my-8 max-w-full overflow-hidden rounded border border-border bg-muted text-foreground highlight-source-ts notranslate&quot;&gt;&lt;pre class=&quot;not-prose m-0 max-w-full overflow-x-auto bg-muted p-5 font-mono text-sm text-foreground leading-6&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;twind&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Middleware &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; request &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; next&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; response &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;next&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;/* ... */&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Response&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
      response&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;body&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;pipeThrough&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;TwindStream&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        signal&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; request&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;signal&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; status&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; response&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;status &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;它先等待下游完成 SSR，只处理 &lt;code&gt;text/html&lt;/code&gt; 响应，然后把 &lt;code&gt;response.body&lt;/code&gt; 接入一个
&lt;code&gt;TransformStream&lt;/code&gt;。流中的 HTML 会交给 Twind &lt;code&gt;consume()&lt;/code&gt; 扫描，首个文档片段生成的
CSS 被注入 &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;如果后续 Remix Frame 又流式返回了新的
class，中间件会比较前后两份样式，只把增量补丁写回页面。关键部分是给每一段流维护独立的
Twind 快照：&lt;/p&gt;
&lt;div class=&quot;not-prose highlight my-8 max-w-full overflow-hidden rounded border border-border bg-muted text-foreground highlight-source-ts notranslate&quot;&gt;&lt;pre class=&quot;not-prose m-0 max-w-full overflow-x-auto bg-muted p-5 font-mono text-sm text-foreground leading-6&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; restoreGlobalState &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; tw&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;snapshot&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;restoreCurrentState&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; html &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;consume&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;buffer&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; tw&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; nextStyle &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;stringify&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;tw&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;target&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

restoreCurrentState &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; tw&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;snapshot&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;restoreGlobalState&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;lastStyle &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  html &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; html&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;replace&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;token string&quot;&gt;&quot;&amp;lt;/head&amp;gt;&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token&quot;&gt;&lt;span class=&quot;token string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&amp;lt;style data-twind&amp;gt;&lt;/span&gt;&lt;span class=&quot;token&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;${&lt;/span&gt;nextStyle&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&amp;lt;/style&amp;gt;&amp;lt;/head&amp;gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; styleDiff &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getStyleDiff&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;lastStyle&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; nextStyle&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;styleDiff&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; html &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;createStylePatch&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;styleDiff&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; html&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;保存和恢复全局快照很重要，因为 Twind 的实例是进程级的，而 SSR
请求会并发发生。每个响应都应该只看到自己的
class，不能把另一个请求刚生成的规则混进来。首次 flush 把完整样式放入
&lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt;；后续 flush 只发送插入位置和新增 CSS，由一小段内联脚本修补同一个
&lt;code&gt;&amp;lt;style&amp;gt;&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;它没有破坏流式渲染，浏览器仍然可以尽快收到文档外壳，后续片段也能获得正确样式。&lt;/p&gt;
&lt;p&gt;与此同时，我们仍可使用 Tailwind 风格的原子类、Typography、Autoprefixer、主题
token 和暗色模式，不需要额外的构建命令。&lt;/p&gt;
&lt;h3 id=&quot;satori-生成验证码&quot;&gt;&lt;a class=&quot;anchor&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; href=&quot;#satori-生成验证码&quot;&gt;&lt;svg class=&quot;octicon octicon-link&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot; height=&quot;16&quot; aria-hidden=&quot;true&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Satori 生成验证码&lt;/h3&gt;
&lt;p&gt;Satori 是 Vercel 开源的 JSX/对象到 SVG 渲染器。它不是浏览器截图工具，不需要启动
Chromium，而是在服务端根据布局对象和字体数据直接计算出 SVG。我们再用 Resvg 把
SVG 栅格化成 PNG。&lt;/p&gt;
&lt;p&gt;最直接的用途是动态 Open Graph 图片。本站会先使用 &lt;code&gt;remix/data-schema&lt;/code&gt; 限制 URL
中标题的长度，再把中英文字体显式交给 Satori，最后返回 PNG：&lt;/p&gt;
&lt;div class=&quot;not-prose highlight my-8 max-w-full overflow-hidden rounded border border-border bg-muted text-foreground highlight-source-ts notranslate&quot;&gt;&lt;pre class=&quot;not-prose m-0 max-w-full overflow-x-auto bg-muted p-5 font-mono text-sm text-foreground leading-6&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; parsed &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; s&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;parseSafe&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
  ogImageQuerySchema&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;&lt;span class=&quot;token&quot;&gt;URL&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;request&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;url&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;searchParams&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; title &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; parsed&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;success &lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt; parsed&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;value&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;title &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; svg &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;satori&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;createCard&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;lang&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; title&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  width&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1200&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  height&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;630&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  fonts&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      data&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token&quot;&gt;RENDER_FONT_DATA&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;instrumentSerif&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      name&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Instrument Serif&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      weight&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;400&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      data&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token&quot;&gt;RENDER_FONT_DATA&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;notoSerifSc&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      name&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Noto Serif SC&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      weight&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;400&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; png &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Resvg&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;svg&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;render&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;asPng&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; body &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; png&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;buffer&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;slice&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
  png&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;byteOffset&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  png&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;byteOffset &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; png&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;byteLength&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;as&lt;/span&gt; ArrayBuffer&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Response&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;body&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  headers&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;Cache-Control&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;public, max-age=3600, stale-while-revalidate=86400&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;Content-Type&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;image/png&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    Vary&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Accept-Language&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这很简单对吧？我们实现了首页以及每个博客的 OG
image，不过更有意思的是评论验证码。&lt;/p&gt;
&lt;p&gt;服务端随机生成一组不同颜色和形状的图形，并选定其中一个目标。Satori 和 Resvg
把挑战渲染为图片，目标位置和一次性 token 则保存在 Deno KV 中。页面使用原生
HTML：&lt;/p&gt;
&lt;div class=&quot;not-prose highlight my-8 max-w-full overflow-hidden rounded border border-border bg-muted text-foreground highlight-source-html notranslate&quot;&gt;&lt;pre class=&quot;not-prose m-0 max-w-full overflow-x-auto bg-muted p-5 font-mono text-sm text-foreground leading-6&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;input&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;image&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;captcha&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;src&lt;/span&gt;&lt;span class=&quot;token&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;...&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;用户点击图片后，浏览器会随普通表单一起提交 &lt;code&gt;captcha.x&lt;/code&gt; 和 &lt;code&gt;captcha.y&lt;/code&gt;，这是
&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input/image&quot; rel=&quot;noopener noreferrer&quot;&gt;&lt;code&gt;&amp;lt;input type=&quot;image&quot;&amp;gt;&lt;/code&gt;&lt;/a&gt;
原生定义的行为。&lt;/p&gt;
&lt;p&gt;服务端只需要用 &lt;code&gt;remix/data-schema/form-data&lt;/code&gt; 验证 token
与坐标，再判断点击位置是否落在目标图形附近。challenge 带有短
TTL，并在提交后原子删除，因此同一个答案不能重复使用。&lt;/p&gt;
&lt;p&gt;这里真正有意思的不是验证码算法，而是实现思路，Satori 负责生成挑战，Deno KV
保存短暂状态，原生 image input 负责收集点击坐标，普通 HTTP form
负责提交。整个流程没有事件监听、&lt;code&gt;fetch()&lt;/code&gt; 或任何 client-side JavaScript。&lt;/p&gt;
&lt;h2 id=&quot;迁移工作&quot;&gt;&lt;a class=&quot;anchor&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; href=&quot;#迁移工作&quot;&gt;&lt;svg class=&quot;octicon octicon-link&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot; height=&quot;16&quot; aria-hidden=&quot;true&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;迁移工作&lt;/h2&gt;
&lt;p&gt;为了实现我定的那几个目标，实现思路已经很明确了，就是全部使用 SSR 服务器端渲染。&lt;/p&gt;
&lt;div class=&quot;not-prose highlight my-8 max-w-full overflow-hidden rounded border border-border bg-muted text-foreground highlight-source-text notranslate&quot;&gt;&lt;pre class=&quot;not-prose m-0 max-w-full overflow-x-auto bg-muted p-5 font-mono text-sm text-foreground leading-6&quot;&gt;TypeScript / TSX + Markdown
        ↓
    runtime
        ↓
 Remix 流式 SSR
        ↓
Twind 注入 CSS + Satori 生成图片
        ↓
   HTML / CSS / HTTP&lt;/pre&gt;&lt;/div&gt;&lt;h3 id=&quot;押注-ssr-渲染&quot;&gt;&lt;a class=&quot;anchor&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; href=&quot;#押注-ssr-渲染&quot;&gt;&lt;svg class=&quot;octicon octicon-link&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot; height=&quot;16&quot; aria-hidden=&quot;true&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;押注 SSR 渲染&lt;/h3&gt;
&lt;p&gt;Remix 的 JSX runtime 来自 &lt;code&gt;remix/ui&lt;/code&gt;，他自身做的真的很多。&lt;/p&gt;
&lt;p&gt;路由需要先在 &lt;code&gt;app/routes.ts&lt;/code&gt; 中声明成一份带类型的 URL 契约。&lt;/p&gt;
&lt;div class=&quot;not-prose highlight my-8 max-w-full overflow-hidden rounded border border-border bg-muted text-foreground highlight-source-ts notranslate&quot;&gt;&lt;pre class=&quot;not-prose m-0 max-w-full overflow-x-auto bg-muted p-5 font-mono text-sm text-foreground leading-6&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; routes &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;route&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  home&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;/&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  blog&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;route&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;/blog&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    index&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;/&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    article&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;/:slug&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    comment&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;post&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;/:slug/comments&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;再由 controller 返回标准 &lt;code&gt;Response&lt;/code&gt;。&lt;/p&gt;
&lt;div class=&quot;not-prose highlight my-8 max-w-full overflow-hidden rounded border border-border bg-muted text-foreground highlight-source-tsx notranslate&quot;&gt;&lt;pre class=&quot;not-prose m-0 max-w-full overflow-x-auto bg-muted p-5 font-mono text-sm text-foreground leading-6&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;createController&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;routes&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  actions&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;home&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; get&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; render &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; i18n &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;createI18n&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;LangContext&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;render&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Home&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;i18n&lt;/span&gt;&lt;span class=&quot;token script&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;i18n&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;render()&lt;/code&gt; 最后调用 &lt;code&gt;renderToStream()&lt;/code&gt;，得到的可以直接放进 &lt;code&gt;Response&lt;/code&gt; 的
&lt;code&gt;ReadableStream&amp;lt;Uint8Array&amp;gt;&lt;/code&gt;。&lt;/p&gt;
&lt;div class=&quot;not-prose highlight my-8 max-w-full overflow-hidden rounded border border-border bg-muted text-foreground highlight-source-ts notranslate&quot;&gt;&lt;pre class=&quot;not-prose m-0 max-w-full overflow-x-auto bg-muted p-5 font-mono text-sm text-foreground leading-6&quot;&gt;Deno&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;serve&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;request&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&amp;gt;&lt;/span&gt; router&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;fetch&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;request&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;GET、POST、重定向、语言协商和错误响应都在同一条 HTTP 链路里完成。&lt;/p&gt;
&lt;p&gt;评论表单验证失败时，还能让 controller
重新渲染带错误信息的页面，成功时则返回重定向。&lt;/p&gt;
&lt;p&gt;这里没有一套服务端模板再加一套客户端状态，HTML 就是状态的最终表达形态。&lt;/p&gt;
&lt;h3 id=&quot;css-新属性注入活力&quot;&gt;&lt;a class=&quot;anchor&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; href=&quot;#css-新属性注入活力&quot;&gt;&lt;svg class=&quot;octicon octicon-link&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot; height=&quot;16&quot; aria-hidden=&quot;true&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;CSS 新属性注入活力&lt;/h3&gt;
&lt;p&gt;最近几年，浏览器加入了很多过去必须依赖 JavaScript
才能完成的能力。对这个项目最有用的是跨文档 View Transitions 和 Scroll-driven
Animations。&lt;/p&gt;
&lt;p&gt;既然我们没有客户端 JS，所以这必须是个 MPA，而不是 SPA。&lt;/p&gt;
&lt;p&gt;页面间导航仍然是普通的 &lt;code&gt;&amp;lt;a&amp;gt;&lt;/code&gt; 和完整文档请求，但通过
&lt;code&gt;@view-transition { navigation: auto; }&lt;/code&gt;，浏览器可以为跨文档导航提供 View
Transitions。&lt;/p&gt;
&lt;p&gt;比如博客列表与文章页为标题设置相同的
&lt;code&gt;view-transition-name&lt;/code&gt;，于是两次独立的服务端响应也能形成连续的视觉过渡。&lt;/p&gt;
&lt;div class=&quot;not-prose highlight my-8 max-w-full overflow-hidden rounded border border-border bg-muted text-foreground highlight-source-ts notranslate&quot;&gt;&lt;pre class=&quot;not-prose m-0 max-w-full overflow-x-auto bg-muted p-5 font-mono text-sm text-foreground leading-6&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;blogPostTitleTransitionName&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;slug&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;createDomId&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;blog-post-title&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; slug&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class=&quot;not-prose highlight my-8 max-w-full overflow-hidden rounded border border-border bg-muted text-foreground highlight-source-tsx notranslate&quot;&gt;&lt;pre class=&quot;not-prose m-0 max-w-full overflow-x-auto bg-muted p-5 font-mono text-sm text-foreground leading-6&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;h1&lt;/span&gt;
  &lt;span class=&quot;token attr-name&quot;&gt;style&lt;/span&gt;&lt;span class=&quot;token script&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    viewTransitionName&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;blogPostTitleTransitionName&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;post&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;slug&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;post&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;title&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;h1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;列表页的 &lt;code&gt;&amp;lt;h2&amp;gt;&lt;/code&gt; 与文章页的 &lt;code&gt;&amp;lt;h1&amp;gt;&lt;/code&gt; 使用同一个稳定名称。浏览器在两次 document
navigation 之间为旧标题和新标题建立对应关系，整个过程不需要 SPA router。&lt;/p&gt;
&lt;p&gt;我们还在文章内部实现了由 scroll-driven animation 驱动的阅读时间线。&lt;/p&gt;
&lt;p&gt;我们先简单封装了一个 &lt;code&gt;ScrollDrivenAnimation&lt;/code&gt; 组件。&lt;/p&gt;
&lt;div class=&quot;not-prose highlight my-8 max-w-full overflow-hidden rounded border border-border bg-muted text-foreground highlight-source-tsx notranslate&quot;&gt;&lt;pre class=&quot;not-prose m-0 max-w-full overflow-x-auto bg-muted p-5 font-mono text-sm text-foreground leading-6&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;ScrollDrivenAnimation&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
  handle&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Handle&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;ScrollDrivenAnimationProps&lt;span class=&quot;token operator&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    timeline &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;view()&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    range &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;entry 0% cover 40%&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    keyframes&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; handle&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;props&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; animationStyle &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;css&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    animationName&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token&quot;&gt;&lt;span class=&quot;token string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;scroll-&lt;/span&gt;&lt;span class=&quot;token&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;${&lt;/span&gt;handle&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;id&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    animationDuration&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;1ms&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    animationFillMode&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;both&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    animationTimeline&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; timeline&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    animationRange&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; range&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token&quot;&gt;&lt;span class=&quot;token string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;@keyframes scroll-&lt;/span&gt;&lt;span class=&quot;token&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;${&lt;/span&gt;handle&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;id&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; keyframes&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;div&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;mix&lt;/span&gt;&lt;span class=&quot;token script&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;animationStyle&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;handle&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;props&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;children&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;div&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;文章容器声明一个命名的 view
timeline，每个刻度按照自己在全文中的百分比消费其中一段进度：&lt;/p&gt;
&lt;div class=&quot;not-prose highlight my-8 max-w-full overflow-hidden rounded border border-border bg-muted text-foreground highlight-source-ts notranslate&quot;&gt;&lt;pre class=&quot;not-prose m-0 max-w-full overflow-x-auto bg-muted p-5 font-mono text-sm text-foreground leading-6&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; articleTimelineStyle &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;css&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  viewTimelineAxis&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;block&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  viewTimelineName&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;--article-reading&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class=&quot;not-prose highlight my-8 max-w-full overflow-hidden rounded border border-border bg-muted text-foreground highlight-source-tsx notranslate&quot;&gt;&lt;pre class=&quot;not-prose m-0 max-w-full overflow-x-auto bg-muted p-5 font-mono text-sm text-foreground leading-6&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;ScrollDrivenAnimation&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token attr-name&quot;&gt;timeline&lt;/span&gt;&lt;span class=&quot;token&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;--article-reading&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token attr-name&quot;&gt;range&lt;/span&gt;&lt;span class=&quot;token script&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token&quot;&gt;&lt;span class=&quot;token string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;cover 40% cover 60%&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token attr-name&quot;&gt;keyframes&lt;/span&gt;&lt;span class=&quot;token script&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    from&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; color&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;var(--border)&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    to&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; color&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;var(--foreground)&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;span&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;data-timeline-line&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;ScrollDrivenAnimation&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;我们根据 Markdown 各节的字数计算。
长章节占据更长的时间线，标题刻度同时是普通锚点链接，所以它既是阅读进度，也是一份不依赖
JavaScript 的 TOC。&lt;/p&gt;
&lt;h3 id=&quot;i18n-a11y-和其他&quot;&gt;&lt;a class=&quot;anchor&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; href=&quot;#i18n-a11y-和其他&quot;&gt;&lt;svg class=&quot;octicon octicon-link&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot; height=&quot;16&quot; aria-hidden=&quot;true&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;i18n, a11y 和其他&lt;/h3&gt;
&lt;p&gt;i18n 也放在请求边界完成。&lt;code&gt;locale()&lt;/code&gt; 中间件先读取 URL 的
&lt;code&gt;?lang=&lt;/code&gt;，没有显式选择时再解析 &lt;code&gt;Accept-Language&lt;/code&gt; 及其 &lt;code&gt;q&lt;/code&gt;
权重，然后把语言写入类型化的 &lt;code&gt;LangContext&lt;/code&gt;：&lt;/p&gt;
&lt;div class=&quot;not-prose highlight my-8 max-w-full overflow-hidden rounded border border-border bg-muted text-foreground highlight-source-ts notranslate&quot;&gt;&lt;pre class=&quot;not-prose m-0 max-w-full overflow-x-auto bg-muted p-5 font-mono text-sm text-foreground leading-6&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;locale&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Middleware &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;context&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; next&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    context&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;set&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;LangContext&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getLangFromRequest&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;context&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;request&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      property&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;lang&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;next&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这个值不只决定正文文案，还同时决定 &lt;code&gt;&amp;lt;html lang&amp;gt;&lt;/code&gt;、日期 locale、OG
locale、manifest URL 和动态 OG 图片中的字体。&lt;/p&gt;
&lt;p&gt;语言信息从一个请求事实向下传递，不需要浏览器启动后再修正页面，因此首屏 HTML
对搜索引擎和读屏软件也是完整的。&lt;/p&gt;
&lt;p&gt;关于 a11y，使用新 Web 能力并不意味着忽略兼容性。&lt;/p&gt;
&lt;p&gt;View Transition
与滚动动画都是渐进增强，支持它们的浏览器得到更连贯的体验，不支持时仍然保留普通跳转、锚点和内容。&lt;/p&gt;
&lt;p&gt;与此同时 &lt;code&gt;prefers-reduced-motion&lt;/code&gt; 则直接关闭动画，让系统设置成为最终决定。&lt;/p&gt;
&lt;h2 id=&quot;部署&quot;&gt;&lt;a class=&quot;anchor&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; href=&quot;#部署&quot;&gt;&lt;svg class=&quot;octicon octicon-link&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot; height=&quot;16&quot; aria-hidden=&quot;true&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;部署&lt;/h2&gt;
&lt;p&gt;没有构建步骤带来了更直接的开发与部署模型，也让运行平台的差异变得无法回避。&lt;/p&gt;
&lt;p&gt;我们先后考察了 Cloudflare Workers、Vercel 和 Deno。&lt;/p&gt;
&lt;h3 id=&quot;cloudflare-workers&quot;&gt;&lt;a class=&quot;anchor&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; href=&quot;#cloudflare-workers&quot;&gt;&lt;svg class=&quot;octicon octicon-link&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot; height=&quot;16&quot; aria-hidden=&quot;true&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Cloudflare Workers&lt;/h3&gt;
&lt;p&gt;Workers 是优秀的边缘运行时，但它不是传统服务器环境。应用代码需要被打包成 Worker
可部署的模块，运行时也没有可供应用任意读取的本地文件系统。&lt;/p&gt;
&lt;p&gt;对于许多应用，这是合理限制，对于这个博客，却意味着必须在部署前把
Markdown、字体和其他源码资产收集进 bundle，或者迁移到外部存储。我们希望仓库 push
后就能直接运行，文章仍然是磁盘上的 Markdown，运行时能够读取项目文件。&lt;/p&gt;
&lt;p&gt;因此 Workers 与这次迁移最核心的 buildless 目标并不匹配。&lt;/p&gt;
&lt;h3 id=&quot;vercel&quot;&gt;&lt;a class=&quot;anchor&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; href=&quot;#vercel&quot;&gt;&lt;svg class=&quot;octicon octicon-link&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot; height=&quot;16&quot; aria-hidden=&quot;true&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Vercel&lt;/h3&gt;
&lt;p&gt;Remix 官方为 Node 准备的 buildless 路径依赖 &lt;code&gt;node --import remix/node-tsx&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;在我们考察的 Vercel 托管函数运行入口中，无法为 Node 进程自由追加这类启动
flag，也就不能按照 Remix 推荐的方式直接把 TS 和 TSX 源码作为服务端入口运行。&lt;/p&gt;
&lt;p&gt;当然，可以增加一次构建，加上 &lt;code&gt;esbuild&lt;/code&gt;
这种构建流程，把代码转成平台期望的产物，但那等于重新引入我们刚刚移除的步骤。&lt;/p&gt;
&lt;p&gt;因此这一次也没有选择 Vercel。&lt;/p&gt;
&lt;h3 id=&quot;deno&quot;&gt;&lt;a class=&quot;anchor&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; href=&quot;#deno&quot;&gt;&lt;svg class=&quot;octicon octicon-link&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot; height=&quot;16&quot; aria-hidden=&quot;true&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Deno&lt;/h3&gt;
&lt;p&gt;Deno 最终成为最自然的答案。它原生执行 TypeScript 和 TSX，不需要 Node 的
&lt;code&gt;--import&lt;/code&gt; loader。&lt;/p&gt;
&lt;p&gt;现在的 Deno 也不再是特立独行。它能够直接使用 npm 包，提供成熟得多的 Node
兼容层，同时支持 WebAssembly、Deno FFI 和 Node-API 的 &lt;code&gt;.node&lt;/code&gt; 原生 addon。&lt;/p&gt;
&lt;pre class=&quot;not-prose m-0 max-w-full overflow-x-auto bg-muted p-5 font-mono text-sm text-foreground leading-6&quot;&gt;&lt;code&gt;{
  &quot;tasks&quot;: {
    &quot;start&quot;: &quot;deno run --allow-env --allow-ffi --allow-read --allow-net deno.ts&quot;
  }
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;这个项目因此可以同时运行 Remix、Twind、Satori 和依赖 &lt;code&gt;.node&lt;/code&gt; 原生 addon 的
Resvg，并用 &lt;code&gt;Deno.serve()&lt;/code&gt; 接收请求、用 Deno KV 保存评论与一次性验证码。&lt;/p&gt;
&lt;p&gt;再加上足够个人站点使用的免费额度，Deno 让这套架构保持了它原本的形状。&lt;/p&gt;
&lt;p&gt;源码就是应用，没有生成目录，没有必须提交或上传的
bundle，也没有为了适配平台而额外维护一套方式。&lt;/p&gt;
&lt;h2 id=&quot;总结&quot;&gt;&lt;a class=&quot;anchor&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; href=&quot;#总结&quot;&gt;&lt;svg class=&quot;octicon octicon-link&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot; height=&quot;16&quot; aria-hidden=&quot;true&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;总结&lt;/h2&gt;
&lt;p&gt;迁移到 Beta 框架当然有代价。&lt;/p&gt;
&lt;p&gt;文档、API、生态和部署经验都在快速变化，我们没有成熟的生态，所以需要自己阅读源码、编写中间件。&lt;/p&gt;
&lt;p&gt;但是 Remix 3 考虑到了这一点。如今 AI 的发展，Remix 3 的库所有都是 TypeScript
文件，这使得 Remix 3 是对 Agent 友好的，而不是像 Next.js
加一堆编译器魔法，尤其是 &lt;code&gt;use client&lt;/code&gt; 这样的 Directive。&lt;/p&gt;
&lt;p&gt;而且它没有像 Next.js 那样自己实现一个 PPR 协议，而是从 Web API
本身出发去构建，而且做到了路由、服务器、组件、数据边界和渐进增强的效果。&lt;/p&gt;
&lt;p&gt;你可以使用 Islands、Frame 和客户端水合，也可以像这个网站一样，一个 client entry
都不创建。&lt;/p&gt;
&lt;p&gt;Remix v3 还很新，我们也仍在继续看它的发展，但是说明了现代 Web
应用自己本身可以拥有类型安全、组件化、动态图片和交互，同时把服务器、浏览器与开放标准放在最中心的位置。&lt;/p&gt;
</description></item><item><title>如何构建一个无服务器 Agent</title><link>https://portfolio.niapya.deno.net/blog/%E5%A6%82%E4%BD%95%E6%9E%84%E5%BB%BA%E4%B8%80%E4%B8%AA%E6%97%A0%E6%9C%8D%E5%8A%A1%E5%99%A8-agent?lang=zh-cn</link><guid isPermaLink="true">https://portfolio.niapya.deno.net/blog/%E5%A6%82%E4%BD%95%E6%9E%84%E5%BB%BA%E4%B8%80%E4%B8%AA%E6%97%A0%E6%9C%8D%E5%8A%A1%E5%99%A8-agent?lang=zh-cn</guid><pubDate>Mon, 27 Jul 2026 03:25:00 GMT</pubDate><description>&lt;p&gt;ClawLess 通过 Vercel Workflow 将 Agent 的运行时设为 Serverless，只在收到消息、Webhook 或定时任务时唤醒，从而实现低成本、无需长期在线的服务器。文章阐述了使用 Next.js、Vercel AI SDK、Workflow hook 与 DurableAgent 组合，实现可挂起、持久化的对话流、上下文压缩与工具调用等功能。通过 KV、Blob、Postgres Vector 等存储以及 Bash 沙箱、记忆、技能等工具，构建了既可交互又具扩展性的完整 Agent。作者认为，这种基于 Workflow 的 Serverless 方案是构建持久且成本友好的 AI Agent 的最佳路径。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;4 月初，我们开源了 Serverless 项目
&lt;a href=&quot;https://github.com/Niapya/clawless&quot; rel=&quot;noopener noreferrer&quot;&gt;ClawLess&lt;/a&gt;，它可以在你的 Vercel
账户上免费部署一个 24 小时可用的轻量 Agent。&lt;/p&gt;
&lt;p&gt;这篇文章想记录的是 ClawLess 背后的取舍：为什么它应该是 Serverless
的，以及我们如何用 Vercel Workflow、Vercel AI SDK、Vercel Sandbox、KV、Blob 和
Postgres Vector 组装出一个能工作的 Agent。&lt;/p&gt;
&lt;h2 id=&quot;起因&quot;&gt;&lt;a class=&quot;anchor&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; href=&quot;#起因&quot;&gt;&lt;svg class=&quot;octicon octicon-link&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot; height=&quot;16&quot; aria-hidden=&quot;true&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;起因&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;构建一个完整、长期在线的 Agent 运行时，成本其实不在模型，而在一直运行。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;模型你可以选择低价或者免费的模型，但你仍然需要一个自己的服务器、VPS，或者一台长期在线的
Mac mini。&lt;/p&gt;
&lt;p&gt;有些项目已经提出了另一种解决方案：使用更小的原生语言重写运行时，把内存占用压低，让它可以运行在树莓派、开发板，或者更便宜的
Linux 设备上。&lt;/p&gt;
&lt;p&gt;他们至少需要一个长期在线的设备来运行
Agent，维护它的状态，等待用户消息，处理定时任务，而 ClawLess
一开始就不是这个方向。&lt;/p&gt;
&lt;p&gt;对大多数人来说，Agent 最常见的入口仍然是 chatbot：用户发送消息，Agent
读取上下文、调用工具、返回结果。它不需要每一秒都在 CPU
上运行，它只需要在被唤醒时可靠地处理请求。&lt;/p&gt;
&lt;p&gt;我们只需要在它接收到会话的时候处理请求即可：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果是在网页上，那就是在用户发送消息时触发的 API 请求。&lt;/li&gt;
&lt;li&gt;如果连接你的 IM，那就是 IM 在收到请求时触发的 webhook，也可以是 API 请求。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;除此之外，我们还需要定时任务和延迟任务。比如「明天提醒我继续这个话题」，或者「每天早上把某个
channel 的信息整理一下」。这类任务本质上不要求常驻进程，只要求平台能把某个
workflow 暂停、等待，并在合适的时间继续。&lt;/p&gt;
&lt;p&gt;老朋友都知道我比较喜欢
Serverless。它的核心是请求进来时启动，任务挂起时休眠，时间到了再恢复。&lt;/p&gt;
&lt;p&gt;由于这种特性，Serverless 服务平台可以以非常低的价格部署，特别是对于 JS Rutime
的，比如 AWS Lambda、Vercel、Cloudflare Workers，而且很多平台都有足够尝鲜的 free
tier。&lt;/p&gt;
&lt;p&gt;经过调研，我看上了 Vercel Workflow。它可以启动长时间任务，可以在 Workflow
里等待，也可以在特定时间继续执行，因此 ClawLess 可以被消息、Webhook、工具审批和
schedule 唤醒的 Workflow。&lt;/p&gt;
&lt;p&gt;换句话说，ClawLess 并不需要你一直运行，只需要按需唤醒。这恰好是 Workflow
这类运行时擅长的事情。&lt;/p&gt;
&lt;h2 id=&quot;设计&quot;&gt;&lt;a class=&quot;anchor&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; href=&quot;#设计&quot;&gt;&lt;svg class=&quot;octicon octicon-link&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot; height=&quot;16&quot; aria-hidden=&quot;true&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;设计&lt;/h2&gt;
&lt;p&gt;因为同时有前后端，这是一个全栈项目。&lt;/p&gt;
&lt;p&gt;对于全栈，我们曾尝试过使用 Nuxt，或是 Hono SSR 的方式来构建全栈，但我们采用
Next.js 来作为全栈框架，因为我们要使用 Vercel Workflow 的 API 来构建 Agent，而
Next.js 是 Vercel Workflow 的原生支持框架。&lt;/p&gt;
&lt;h2 id=&quot;workflow-是-agent-的运行时&quot;&gt;&lt;a class=&quot;anchor&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; href=&quot;#workflow-是-agent-的运行时&quot;&gt;&lt;svg class=&quot;octicon octicon-link&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot; height=&quot;16&quot; aria-hidden=&quot;true&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Workflow 是 Agent 的运行时&lt;/h2&gt;
&lt;p&gt;我们先谈一下 Vercel Workflow，它是 Vercel
最近推出的官方、长时间运行的、带有状态的流程运行框架。经测试，一个 workflow
能运行至少 10 分钟以上。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;我们在做 ClawLess 的时候，Vercel Workflow 还是处于 Beta 阶段，但是现在 API
已经稳定。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;它使用 &lt;code&gt;&quot;use workflow&quot;;&lt;/code&gt; 和 &lt;code&gt;&quot;use step&quot;;&lt;/code&gt; 指令来定义一个 workflow。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;use workflow&lt;/code&gt; 负责“编排”，也就是跨步骤的状态、等待、恢复和整体流程。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;use step&lt;/code&gt; 负责“执行”，也就是可以单独重试、适合封装副作用的原子操作。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;对于 Agent 来说，真正调用数据库、第三方 API、Sandbox 命令的逻辑，通常放在到
&lt;code&gt;step&lt;/code&gt; 里；而等待用户消息、等待审批，比如等待明天早上 9
点继续执行的逻辑，则更适合留在 &lt;code&gt;workflow&lt;/code&gt; 这一层。&lt;/p&gt;
&lt;p&gt;一个 Workflow 的例子如下：&lt;/p&gt;
&lt;div class=&quot;not-prose highlight my-8 max-w-full overflow-hidden rounded border border-border bg-muted text-foreground highlight-source-ts notranslate&quot;&gt;&lt;pre class=&quot;not-prose m-0 max-w-full overflow-x-auto bg-muted p-5 font-mono text-sm text-foreground leading-6&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;updateUser&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;userId&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token string&quot;&gt;&quot;use step&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; db&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;insert&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; id&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; userId &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;userOnboardingWorkflow&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;userId&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token string&quot;&gt;&quot;use workflow&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;updateUser&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;userId&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token&quot;&gt;POST&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;updateUser&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;123&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;然后对于 AI 来说，毫无疑问，TypeScript 最流行的 AI framework 是 Vercel AI SDK。&lt;/p&gt;
&lt;div class=&quot;not-prose highlight my-8 max-w-full overflow-hidden rounded border border-border bg-muted text-foreground highlight-source-ts notranslate&quot;&gt;&lt;pre class=&quot;not-prose m-0 max-w-full overflow-x-auto bg-muted p-5 font-mono text-sm text-foreground leading-6&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; convertToModelMessages&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; streamText&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;UIMessage&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;ai&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token&quot;&gt;POST&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;req&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Request&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; messages &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; messages&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; UIMessage&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; req&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;json&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; result &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;streamText&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    model&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;anthropic/claude-sonnet-4.5&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    system&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;You are a helpful assistant.&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    messages&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;convertToModelMessages&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;messages&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; result&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toUIMessageStreamResponse&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;然后在前端，你可以使用
&lt;a href=&quot;https://ai-sdk.dev/docs/reference/ai-sdk-ui/use-chat&quot; rel=&quot;noopener noreferrer&quot;&gt;&lt;code&gt;useChat&lt;/code&gt;&lt;/a&gt; 这个 Hook
来轻松地实现一个聊天界面。&lt;/p&gt;
&lt;div class=&quot;not-prose highlight my-8 max-w-full overflow-hidden rounded border border-border bg-muted text-foreground highlight-source-ts notranslate&quot;&gt;&lt;pre class=&quot;not-prose m-0 max-w-full overflow-x-auto bg-muted p-5 font-mono text-sm text-foreground leading-6&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;use client&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; useChat &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;@ai-sdk/react&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; DefaultChatTransport &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;ai&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; useState &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;react&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;Page&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; messages&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; sendMessage&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; status &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;useChat&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    transport&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;DefaultChatTransport&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      api&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;/api/chat&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;input&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; setInput&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;useState&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;messages&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;message&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;div key&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;message&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;id&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;gt;&lt;/span&gt;
          &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;message&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;role &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;user&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;User: &quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;AI: &quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
          &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;message&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;parts&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;part&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; index&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&amp;gt;&lt;/span&gt;
            part&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;type &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;text&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;span key&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;index&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;part&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;text&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;span&lt;span class=&quot;token operator&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;
          &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;div&lt;span class=&quot;token operator&quot;&gt;&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

      &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;form
        onSubmit&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;e&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
          e&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;preventDefault&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
          &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;input&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;trim&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;token function&quot;&gt;sendMessage&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; text&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; input &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
            &lt;span class=&quot;token function&quot;&gt;setInput&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
          &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
      &lt;span class=&quot;token operator&quot;&gt;&amp;gt;&lt;/span&gt;
        &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;input
          value&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;input&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
          onChange&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;e&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;setInput&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;e&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;target&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;value&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
          disabled&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;status &lt;span class=&quot;token operator&quot;&gt;!==&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;ready&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
          placeholder&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;Say something...&quot;&lt;/span&gt;
        &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;gt;&lt;/span&gt;
        &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;button type&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;submit&quot;&lt;/span&gt; disabled&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;status &lt;span class=&quot;token operator&quot;&gt;!==&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;ready&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;gt;&lt;/span&gt;
          Submit
        &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;button&lt;span class=&quot;token operator&quot;&gt;&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;form&lt;span class=&quot;token operator&quot;&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;useChat&lt;/code&gt; Hooks 还提供了 Vue, Svelte 和 Angular 的库。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这已经能完成一个很好用的 Web Chat。但它还不是我们要的 Durable
Agent，因为请求中断/结束之后，运行时也就跟着结束了。&lt;/p&gt;
&lt;p&gt;这与我们 Agent
的逻辑不同，我们希望在请求中断后还能继续运行，并且在运行后持久化以保存状态。&lt;/p&gt;
&lt;p&gt;所以我们的 Workflow 配备了 AI 模块，它和 AI SDK 深度集成，并提供了更适合
workflow 场景的 &lt;code&gt;DurableAgent&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;Workflow 的 &lt;code&gt;getWritable()&lt;/code&gt;
可以拿到一个持久化的输出流，客户端断开以后还能重新连接回来，继续接收之前没看完的内容。&lt;/p&gt;
&lt;p&gt;下面这段是一个简化示意：&lt;/p&gt;
&lt;div class=&quot;not-prose highlight my-8 max-w-full overflow-hidden rounded border border-border bg-muted text-foreground highlight-source-ts notranslate&quot;&gt;&lt;pre class=&quot;not-prose m-0 max-w-full overflow-x-auto bg-muted p-5 font-mono text-sm text-foreground leading-6&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; DurableAgent &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;@workflow/ai/agent&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; getWritable &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;workflow&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; UIMessage&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; UIMessageChunk &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;ai&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;chatWorkflow&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;messages&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; UIMessage&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token string&quot;&gt;&quot;use workflow&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; writable &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token&quot;&gt;&lt;span class=&quot;token function&quot;&gt;getWritable&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;UIMessageChunk&lt;span class=&quot;token operator&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; agent &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;DurableAgent&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    model&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;anthropic/claude-sonnet-4.5&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    system&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;You are a helpful assistant.&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; agent&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;stream&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    messages&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    writable&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这时候，Agent 才真正开始像一个自主的系统。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;在 ClawLess 中，我们的过程更加复杂，主 workflow 会从 KV 读取当前配置，构建
system prompt，从 MCP 加载工具，创建 &lt;code&gt;DurableAgent&lt;/code&gt;，然后把输出写到 UI message
stream，并且持久化，最后再把状态保存到 Postgress SQL 中。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;hook-让-workflow-真正可交互&quot;&gt;&lt;a class=&quot;anchor&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; href=&quot;#hook-让-workflow-真正可交互&quot;&gt;&lt;svg class=&quot;octicon octicon-link&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot; height=&quot;16&quot; aria-hidden=&quot;true&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Hook 让 workflow 真正可交互&lt;/h2&gt;
&lt;p&gt;考虑更多，比如用户在网页继续发消息，或者 IM webhook 继续发消息时，如果 workflow
还在运行，我们应该去引导（Follow-up）Agent 处理这些消息，而不是直接发起新的
workflow。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;这是 Human in the Loop 的基本逻辑。如果你想了解更多，你可以看 WikiPedia 中的
&lt;a href=&quot;https://en.wikipedia.org/wiki/Human-in-the-loop&quot; rel=&quot;noopener noreferrer&quot;&gt;HITL, Human-in-the-Loop&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;庆幸的是，Workflow 提供了 hook 来做到这些。hook 的价值是：它允许一个正在运行的
workflow 暂停在某个位置，等外部事件到来之后再继续。&lt;/p&gt;
&lt;p&gt;最简单的外部事件就是“下一条用户消息”：&lt;/p&gt;
&lt;div class=&quot;not-prose highlight my-8 max-w-full overflow-hidden rounded border border-border bg-muted text-foreground highlight-source-ts notranslate&quot;&gt;&lt;pre class=&quot;not-prose m-0 max-w-full overflow-x-auto bg-muted p-5 font-mono text-sm text-foreground leading-6&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; defineHook &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;workflow&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 定义一个 hook，用于接收外部事件（下一条用户消息）&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; messageHook &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token&quot;&gt;&lt;span class=&quot;token function&quot;&gt;defineHook&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  text&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  userId&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;conversation&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;sessionId&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token string&quot;&gt;&quot;use workflow&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; events &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; messageHook&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;create&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    token&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;chat-&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; sessionId&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// 等待外部事件（下一条用户消息）&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; event &lt;span class=&quot;token keyword&quot;&gt;of&lt;/span&gt; events&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token&quot;&gt;console&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;Received&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; event&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;text&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;from&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; event&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;userId&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token&quot;&gt;POST&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;req&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Request&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; data &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; req&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;json&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; messageHook&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;resume&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;chat-&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; data&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;sessionId&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    text&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; data&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;text&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    userId&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; data&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;userId&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Response&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;OK&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;同一个机制也很适合做工具审批。比如模型想执行一个高风险命令：删文件、发消息、下单、转账，这时候就不该让它直接运行，而是要把审批请求抛给前端，再等用户确认。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;更多信息详见 AI SDK 中关于
&lt;a href=&quot;https://ai-sdk.dev/docs/ai-sdk-core/tools-and-tool-calling#tool-execution-approval&quot; rel=&quot;noopener noreferrer&quot;&gt;Tool Execution Approval&lt;/a&gt;
的说明。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Workflow hook 的好处是，你不需要自己轮询数据库，不需要写一套复杂的状态机，因为
workflow 会在等待时挂起，恢复时又从原来的状态继续。&lt;/p&gt;
&lt;p&gt;所以，我们总结一下一个 Agent 的顺序：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;用户消息进入入口层。&lt;/li&gt;
&lt;li&gt;入口层判断当前 session 是否已有活动中的 workflow。&lt;/li&gt;
&lt;li&gt;如果有，就用 hook 把新消息送回去。&lt;/li&gt;
&lt;li&gt;如果没有，就创建新的 workflow，从头开始处理。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;上下文-context&quot;&gt;&lt;a class=&quot;anchor&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; href=&quot;#上下文-context&quot;&gt;&lt;svg class=&quot;octicon octicon-link&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot; height=&quot;16&quot; aria-hidden=&quot;true&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;上下文 Context&lt;/h2&gt;
&lt;p&gt;Agent 有上下文限制（Context Limit）。不同模型的 token
计算方式不同，但它们都有一个共同限制：你必须把 system
prompt、历史消息、工具结果、文件输入和记忆都塞进上下文里。&lt;/p&gt;
&lt;p&gt;不仅如此，模型在处理长上下文时，正确率也会下降，成本也会增加。&lt;/p&gt;
&lt;p&gt;处理长上下文有常见的两种方法，压缩和剪枝。&lt;/p&gt;
&lt;p&gt;压缩（Compression）是把“很长但以后还需要”的历史，改写成更短的表示。最常见的做法是：在会话进行到一定长度之后，用模型把早期对话、关键工具结果、已经确认的用户偏好压成一段摘要，然后把原始消息移出当前上下文窗口。&lt;/p&gt;
&lt;p&gt;下一轮真正调用模型时，你只需要把摘要重新组装回来。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;如果你考虑更多，你可以采用滚动窗口的方法，把摘要和最近几轮的原始消息以及当前输入加进来。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div class=&quot;not-prose highlight my-8 max-w-full overflow-hidden rounded border border-border bg-muted text-foreground highlight-source-ts notranslate&quot;&gt;&lt;pre class=&quot;not-prose m-0 max-w-full overflow-x-auto bg-muted p-5 font-mono text-sm text-foreground leading-6&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; generateText&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;ModelMessage&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;ai&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;compactMessages&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;messages&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; ModelMessage&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; summary &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;generateText&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    model&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    messages&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
      &lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;messages&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        role&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;user&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        content&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;请总结一下我们之前的对话，提炼出对后续有帮助的关键信息。&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    summary&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; summary&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;text&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    recentMessages&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; messages&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;slice&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;8&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;在 Workflow 里，这种做法尤其自然。因为 workflow
本来就有自己的生命周期和外部存储层，你完全可以在某一轮结束后把摘要写入
KV，把旧消息删掉，只保留一个“会话摘要”字段。这样 durable session
仍然存在，但真正送进模型的上下文不会无限增长。&lt;/p&gt;
&lt;p&gt;另一种方法是剪枝（Pruning）。它不试图理解历史，而是直接把“对当前轮次帮助不大”的内容裁掉。&lt;/p&gt;
&lt;p&gt;AI SDK 已经提供了 &lt;code&gt;pruneMessages&lt;/code&gt;，可以在送进模型之前移除旧的
reasoning、工具调用结果、审批痕迹和空消息。&lt;/p&gt;
&lt;div class=&quot;not-prose highlight my-8 max-w-full overflow-hidden rounded border border-border bg-muted text-foreground highlight-source-ts notranslate&quot;&gt;&lt;pre class=&quot;not-prose m-0 max-w-full overflow-x-auto bg-muted p-5 font-mono text-sm text-foreground leading-6&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;ModelMessage&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; pruneMessages &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;ai&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;buildContextWindow&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;messages&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; ModelMessage&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; pruned &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;pruneMessages&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    messages&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    reasoning&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;before-last-message&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    toolCalls&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;before-last-2-messages&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    emptyMessages&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;remove&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; pinned &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; pruned&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;filter&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;message&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&amp;gt;&lt;/span&gt; message&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;role &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;system&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; recent &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; pruned&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;filter&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;message&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&amp;gt;&lt;/span&gt; message&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;role &lt;span class=&quot;token operator&quot;&gt;!==&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;system&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;slice&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;12&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;pinned&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;recent&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;对于 Workflow AI，通常会在每一轮调用前通过 &lt;code&gt;prepareStep&lt;/code&gt; 来重写。&lt;/p&gt;
&lt;div class=&quot;not-prose highlight my-8 max-w-full overflow-hidden rounded border border-border bg-muted text-foreground highlight-source-ts notranslate&quot;&gt;&lt;pre class=&quot;not-prose m-0 max-w-full overflow-x-auto bg-muted p-5 font-mono text-sm text-foreground leading-6&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; result &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; agent&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;stream&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  messages&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  writable&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// prepareStep hook 会在每次调用模型前被调用，用来重写上下文窗口&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;prepareStep&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; messages&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; currentMessages &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      messages&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;buildContextWindow&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;currentMessages&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这两种方法并不冲突。实际工程里更常见的是组合使用，完全取决于设计。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;这是有关 Context Engineering 的概念，你可以阅读 Anthropic 的文章
&lt;a href=&quot;https://www.anthropic.com/engineering/effective-context-engineering-for-ai-agents&quot; rel=&quot;noopener noreferrer&quot;&gt;Effective Context Engineering for AI Agents&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;但是，我们还要要考虑的更多，比如说跨对话的请求之间的，同一知识之间的，或者是永久保存的上下文共享，这个时候我们需要引入工具来把它们存储在外部系统里，而不是直接放在模型上下文里。&lt;/p&gt;
&lt;h2 id=&quot;工具-tools&quot;&gt;&lt;a class=&quot;anchor&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; href=&quot;#工具-tools&quot;&gt;&lt;svg class=&quot;octicon octicon-link&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot; height=&quot;16&quot; aria-hidden=&quot;true&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;工具 Tools&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Agent 的工具决定了它的能力边界。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;如果没有工具，它只是一个聊天模型；有了工具，它才可以保存记忆、执行命令、学习技能、安排任务，甚至把一部分工作交给子
Agent。&lt;/p&gt;
&lt;p&gt;在 AI SDK 里，工具本身就是一等公民。一个最小的工具定义大概像这样：&lt;/p&gt;
&lt;div class=&quot;not-prose highlight my-8 max-w-full overflow-hidden rounded border border-border bg-muted text-foreground highlight-source-ts notranslate&quot;&gt;&lt;pre class=&quot;not-prose m-0 max-w-full overflow-x-auto bg-muted p-5 font-mono text-sm text-foreground leading-6&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; tool &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;ai&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; z &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;zod&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; tools &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  remember&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;tool&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    description&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Save a useful fact into long-term memory&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    inputSchema&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; z&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;object&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      note&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; z&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;execute&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; note &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; memory &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;buildMemory&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;note&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; ok&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; memory &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id=&quot;记忆-memory&quot;&gt;&lt;a class=&quot;anchor&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; href=&quot;#记忆-memory&quot;&gt;&lt;svg class=&quot;octicon octicon-link&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot; height=&quot;16&quot; aria-hidden=&quot;true&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;记忆 Memory&lt;/h3&gt;
&lt;p&gt;记忆是存储长期事实，用户偏好的部分。&lt;/p&gt;
&lt;p&gt;你可以把它存储到 KV、数据库，或者以文件形式存储到
Blob，然后给模型添加工具来管理它。&lt;/p&gt;
&lt;p&gt;在搜索时，最方便的时全文关键词搜索，但是有很多局限性，比如两个句话关联性强但是没有重叠。&lt;/p&gt;
&lt;p&gt;你在公司内部系统搜索“如何处理产假期间的奖金核算”。问题：如果公司制度里使用的是“生育津贴”、“女职工福利”或“长期病假（生育）规定”，而没有同时包含“产假”和“奖金”这两个具体词汇，全文搜索会直接显示“未找到相关结果”，即便系统里实际上有相关的政策文档。&lt;/p&gt;
&lt;p&gt;所以，我们可以考虑
&lt;a href=&quot;https://en.wikipedia.org/wiki/Retrieval-augmented_generation&quot; rel=&quot;noopener noreferrer&quot;&gt;RAG, Retrieval-augmented generation, 检索增强生成&lt;/a&gt;，这是一种让模型在生成响应时，从外部知识库中检索相关信息的方法。&lt;/p&gt;
&lt;p&gt;最简单的是向量嵌入搜索，把一句话通过向量模型映射到一个多维向量上，然后在向量空间中查找最相似的句子。&lt;/p&gt;
&lt;p&gt;在查找最相似时，可以使用余弦相似度（Cosine Similarity）的计算方法。&lt;/p&gt;
&lt;p&gt;&lt;span class=&quot;katex-display&quot;&gt;&lt;span class=&quot;katex&quot;&gt;&lt;span class=&quot;katex-mathml&quot;&gt;&lt;math xmlns=&quot;http://www.w3.org/1998/Math/MathML&quot;&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mi&gt;Similarity&lt;/mi&gt;&lt;mo&gt;⁡&lt;/mo&gt;&lt;mo&gt;=&lt;/mo&gt;&lt;mover&gt;&lt;mi&gt;q&lt;/mi&gt;&lt;mo&gt;^&lt;/mo&gt;&lt;/mover&gt;&lt;mo&gt;⋅&lt;/mo&gt;&lt;mover&gt;&lt;mi&gt;d&lt;/mi&gt;&lt;mo&gt;^&lt;/mo&gt;&lt;/mover&gt;&lt;mo&gt;=&lt;/mo&gt;&lt;mo&gt;∑&lt;/mo&gt;&lt;mrow&gt;&lt;mi&gt;i&lt;/mi&gt;&lt;mo&gt;=&lt;/mo&gt;&lt;mn&gt;1&lt;/mn&gt;&lt;/mrow&gt;&lt;mi&gt;n&lt;/mi&gt;&lt;msub&gt;&lt;mover&gt;&lt;mi&gt;q&lt;/mi&gt;&lt;mo&gt;^&lt;/mo&gt;&lt;/mover&gt;&lt;mi&gt;i&lt;/mi&gt;&lt;/msub&gt;&lt;msub&gt;&lt;mover&gt;&lt;mi&gt;d&lt;/mi&gt;&lt;mo&gt;^&lt;/mo&gt;&lt;/mover&gt;&lt;mi&gt;i&lt;/mi&gt;&lt;/msub&gt;&lt;/mrow&gt;&lt;annotation encoding=&quot;application/x-tex&quot;&gt;\operatorname{Similarity} = \hat{\mathbf{q}} \cdot \hat{\mathbf{d}} = \sum_{i=1}^{n} \hat q_i \hat d_i&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class=&quot;katex-html&quot; aria-hidden=&quot;true&quot;&gt;&lt;span class=&quot;base&quot;&gt;&lt;span class=&quot;strut&quot; style=&quot;height:0.8889em;vertical-align:-0.1944em&quot;&gt;&lt;/span&gt;&lt;span&gt;&lt;span class=&quot;mathrm&quot; style=&quot;margin-right:0.0139em&quot;&gt;Similarity&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;mspace&quot; style=&quot;margin-right:0.2778em&quot;&gt;&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span class=&quot;mspace&quot; style=&quot;margin-right:0.2778em&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;base&quot;&gt;&lt;span class=&quot;strut&quot; style=&quot;height:0.9023em;vertical-align:-0.1944em&quot;&gt;&lt;/span&gt;&lt;span class=&quot;accent&quot;&gt;&lt;span class=&quot;vlist-t vlist-t2&quot;&gt;&lt;span class=&quot;vlist-r&quot;&gt;&lt;span class=&quot;vlist&quot; style=&quot;height:0.7079em&quot;&gt;&lt;span style=&quot;top:-3em&quot;&gt;&lt;span class=&quot;pstrut&quot; style=&quot;height:3em&quot;&gt;&lt;/span&gt;&lt;span class=&quot;mathbf&quot;&gt;q&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;top:-3.0134em&quot;&gt;&lt;span class=&quot;pstrut&quot; style=&quot;height:3em&quot;&gt;&lt;/span&gt;&lt;span class=&quot;accent-body&quot; style=&quot;left:-0.25em&quot;&gt;&lt;span&gt;^&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;vlist-s&quot;&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;vlist-r&quot;&gt;&lt;span class=&quot;vlist&quot; style=&quot;height:0.1944em&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;mspace&quot; style=&quot;margin-right:0.2222em&quot;&gt;&lt;/span&gt;&lt;span&gt;⋅&lt;/span&gt;&lt;span class=&quot;mspace&quot; style=&quot;margin-right:0.2222em&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;base&quot;&gt;&lt;span class=&quot;strut&quot; style=&quot;height:0.9579em&quot;&gt;&lt;/span&gt;&lt;span class=&quot;accent&quot;&gt;&lt;span class=&quot;vlist-t&quot;&gt;&lt;span class=&quot;vlist-r&quot;&gt;&lt;span class=&quot;vlist&quot; style=&quot;height:0.9579em&quot;&gt;&lt;span style=&quot;top:-3em&quot;&gt;&lt;span class=&quot;pstrut&quot; style=&quot;height:3em&quot;&gt;&lt;/span&gt;&lt;span class=&quot;mathbf&quot;&gt;d&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;top:-3.2634em&quot;&gt;&lt;span class=&quot;pstrut&quot; style=&quot;height:3em&quot;&gt;&lt;/span&gt;&lt;span class=&quot;accent-body&quot; style=&quot;left:-0.25em&quot;&gt;&lt;span&gt;^&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;mspace&quot; style=&quot;margin-right:0.2778em&quot;&gt;&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span class=&quot;mspace&quot; style=&quot;margin-right:0.2778em&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;base&quot;&gt;&lt;span class=&quot;strut&quot; style=&quot;height:2.9291em;vertical-align:-1.2777em&quot;&gt;&lt;/span&gt;&lt;span class=&quot;op-limits&quot;&gt;&lt;span class=&quot;vlist-t vlist-t2&quot;&gt;&lt;span class=&quot;vlist-r&quot;&gt;&lt;span class=&quot;vlist&quot; style=&quot;height:1.6514em&quot;&gt;&lt;span style=&quot;top:-1.8723em;margin-left:0em&quot;&gt;&lt;span class=&quot;pstrut&quot; style=&quot;height:3.05em&quot;&gt;&lt;/span&gt;&lt;span class=&quot;sizing reset-size6 size3&quot;&gt;&lt;span&gt;&lt;span class=&quot;mathnormal&quot;&gt;i&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;top:-3.05em&quot;&gt;&lt;span class=&quot;pstrut&quot; style=&quot;height:3.05em&quot;&gt;&lt;/span&gt;&lt;span&gt;&lt;span class=&quot;op-symbol large-op&quot;&gt;∑&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;top:-4.3em;margin-left:0em&quot;&gt;&lt;span class=&quot;pstrut&quot; style=&quot;height:3.05em&quot;&gt;&lt;/span&gt;&lt;span class=&quot;sizing reset-size6 size3&quot;&gt;&lt;span&gt;&lt;span class=&quot;mathnormal&quot;&gt;n&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;vlist-s&quot;&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;vlist-r&quot;&gt;&lt;span class=&quot;vlist&quot; style=&quot;height:1.2777em&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;mspace&quot; style=&quot;margin-right:0.1667em&quot;&gt;&lt;/span&gt;&lt;span&gt;&lt;span class=&quot;accent&quot;&gt;&lt;span class=&quot;vlist-t vlist-t2&quot;&gt;&lt;span class=&quot;vlist-r&quot;&gt;&lt;span class=&quot;vlist&quot; style=&quot;height:0.6944em&quot;&gt;&lt;span style=&quot;top:-3em&quot;&gt;&lt;span class=&quot;pstrut&quot; style=&quot;height:3em&quot;&gt;&lt;/span&gt;&lt;span class=&quot;mathnormal&quot; style=&quot;margin-right:0.0359em&quot;&gt;q&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;top:-3em&quot;&gt;&lt;span class=&quot;pstrut&quot; style=&quot;height:3em&quot;&gt;&lt;/span&gt;&lt;span class=&quot;accent-body&quot; style=&quot;left:-0.1667em&quot;&gt;&lt;span&gt;^&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;vlist-s&quot;&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;vlist-r&quot;&gt;&lt;span class=&quot;vlist&quot; style=&quot;height:0.1944em&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;msupsub&quot;&gt;&lt;span class=&quot;vlist-t vlist-t2&quot;&gt;&lt;span class=&quot;vlist-r&quot;&gt;&lt;span class=&quot;vlist&quot; style=&quot;height:0.3117em&quot;&gt;&lt;span style=&quot;top:-2.55em;margin-left:-0.0359em;margin-right:0.05em&quot;&gt;&lt;span class=&quot;pstrut&quot; style=&quot;height:2.7em&quot;&gt;&lt;/span&gt;&lt;span class=&quot;sizing reset-size6 size3&quot;&gt;&lt;span class=&quot;mathnormal&quot;&gt;i&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;vlist-s&quot;&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;vlist-r&quot;&gt;&lt;span class=&quot;vlist&quot; style=&quot;height:0.15em&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class=&quot;accent&quot;&gt;&lt;span class=&quot;vlist-t&quot;&gt;&lt;span class=&quot;vlist-r&quot;&gt;&lt;span class=&quot;vlist&quot; style=&quot;height:0.9579em&quot;&gt;&lt;span style=&quot;top:-3em&quot;&gt;&lt;span class=&quot;pstrut&quot; style=&quot;height:3em&quot;&gt;&lt;/span&gt;&lt;span class=&quot;mathnormal&quot;&gt;d&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;top:-3.2634em&quot;&gt;&lt;span class=&quot;pstrut&quot; style=&quot;height:3em&quot;&gt;&lt;/span&gt;&lt;span class=&quot;accent-body&quot; style=&quot;left:-0.0833em&quot;&gt;&lt;span&gt;^&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;msupsub&quot;&gt;&lt;span class=&quot;vlist-t vlist-t2&quot;&gt;&lt;span class=&quot;vlist-r&quot;&gt;&lt;span class=&quot;vlist&quot; style=&quot;height:0.3117em&quot;&gt;&lt;span style=&quot;top:-2.55em;margin-left:0em;margin-right:0.05em&quot;&gt;&lt;span class=&quot;pstrut&quot; style=&quot;height:2.7em&quot;&gt;&lt;/span&gt;&lt;span class=&quot;sizing reset-size6 size3&quot;&gt;&lt;span class=&quot;mathnormal&quot;&gt;i&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;vlist-s&quot;&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;vlist-r&quot;&gt;&lt;span class=&quot;vlist&quot; style=&quot;height:0.15em&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;将计算出来相似度归一化，之后排序。&lt;/p&gt;
&lt;p&gt;在真实系统中，我们一般用混合搜索，也就是将向量搜索和关键词搜索结合起来。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;更多关于 Memory 的探究，你可以参考
&lt;a href=&quot;https://docs.openclaw.ai/concepts/memory&quot; rel=&quot;noopener noreferrer&quot;&gt;OpenClaw 的 Memory 设计&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;技能-skills&quot;&gt;&lt;a class=&quot;anchor&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; href=&quot;#技能-skills&quot;&gt;&lt;svg class=&quot;octicon octicon-link&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot; height=&quot;16&quot; aria-hidden=&quot;true&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;技能 Skills&lt;/h3&gt;
&lt;p&gt;技能更像基于文件系统的知识库。&lt;/p&gt;
&lt;p&gt;一个 Skill 一般存储在文件系统，它目录结构可能是这样的：&lt;/p&gt;
&lt;div class=&quot;not-prose highlight my-8 max-w-full overflow-hidden rounded border border-border bg-muted text-foreground highlight-source-bash notranslate&quot;&gt;&lt;pre class=&quot;not-prose m-0 max-w-full overflow-x-auto bg-muted p-5 font-mono text-sm text-foreground leading-6&quot;&gt;my-skill $ tree

my-skill/
	SKILL.md

	scripts/
	templates/
	examples/
	assets/&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Skills 表明了怎么做事，比如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如何写某类测试&lt;/li&gt;
&lt;li&gt;如何调用某个内部规范&lt;/li&gt;
&lt;li&gt;如何按照团队约定生成代码&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我们一般会把 Skills 的标题和描述提取出来，注入到 System Prompt 中。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Skills 是 Anthropic 提出的，具体请看
&lt;a href=&quot;https://platform.claude.com/docs/en/agents-and-tools/agent-skills/overview&quot; rel=&quot;noopener noreferrer&quot;&gt;Agent Skills 概述&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;执行-bash&quot;&gt;&lt;a class=&quot;anchor&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; href=&quot;#执行-bash&quot;&gt;&lt;svg class=&quot;octicon octicon-link&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot; height=&quot;16&quot; aria-hidden=&quot;true&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;执行 Bash&lt;/h3&gt;
&lt;p&gt;执行工具是 Agent 真正开始接触外部世界的地方。&lt;/p&gt;
&lt;p&gt;一个最基础的执行工具就是 Bash 工具，他可以执行 Shell 命令，它允许了 Agent
和真实环境的连接起来。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;在 ClawLess 中，我们使用 @vercel/sandbox 来实现 Bash 工具。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Bash 风险最高，也最需要隔离，要特别考虑工具审核和沙箱环境的隔离。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;如果你在寻找 ServerLess 的 Bash 工具或者是需要一个沙箱 Bash
工具，你可以考虑看看我们的 &lt;a href=&quot;https://github.com/Niapya/ai-sdk-x&quot; rel=&quot;noopener noreferrer&quot;&gt;AI SDK X&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;任务&quot;&gt;&lt;a class=&quot;anchor&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; href=&quot;#任务&quot;&gt;&lt;svg class=&quot;octicon octicon-link&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot; height=&quot;16&quot; aria-hidden=&quot;true&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;任务&lt;/h3&gt;
&lt;p&gt;在处理复杂的任务时，我们还需要考虑更多。&lt;/p&gt;
&lt;h4 id=&quot;延时与定时任务&quot;&gt;&lt;a class=&quot;anchor&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; href=&quot;#延时与定时任务&quot;&gt;&lt;svg class=&quot;octicon octicon-link&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot; height=&quot;16&quot; aria-hidden=&quot;true&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;延时与定时任务&lt;/h4&gt;
&lt;p&gt;延时任务会在一定时间之后提醒你。&lt;/p&gt;
&lt;p&gt;ClawLess 在 Workflow 内部使用 &lt;code&gt;sleep&lt;/code&gt; 函数来实现延时任务。&lt;/p&gt;
&lt;div class=&quot;not-prose highlight my-8 max-w-full overflow-hidden rounded border border-border bg-muted text-foreground highlight-source-ts notranslate&quot;&gt;&lt;pre class=&quot;not-prose m-0 max-w-full overflow-x-auto bg-muted p-5 font-mono text-sm text-foreground leading-6&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; sleep &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;workflow&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;remindTomorrow&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;taskId&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token string&quot;&gt;&quot;use workflow&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;sleep&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;1 day&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;sendReminder&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;taskId&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;定时任务又称作 Cron Job，它可以让你在指定的时间点执行任务。&lt;/p&gt;
&lt;p&gt;定时任务的实现与延时任务相似，我们使用一个 &lt;code&gt;while (true)&lt;/code&gt; 循环来实现定时任务。&lt;/p&gt;
&lt;div class=&quot;not-prose highlight my-8 max-w-full overflow-hidden rounded border border-border bg-muted text-foreground highlight-source-ts notranslate&quot;&gt;&lt;pre class=&quot;not-prose m-0 max-w-full overflow-x-auto bg-muted p-5 font-mono text-sm text-foreground leading-6&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;while&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;sleep&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;1 day&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;runTask&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h4 id=&quot;子任务-sub-agent&quot;&gt;&lt;a class=&quot;anchor&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; href=&quot;#子任务-sub-agent&quot;&gt;&lt;svg class=&quot;octicon octicon-link&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot; height=&quot;16&quot; aria-hidden=&quot;true&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;子任务 Sub-Agent&lt;/h4&gt;
&lt;p&gt;子任务不一定非要是另一个完整 Agent，他会把一部分小的任务委托出去。&lt;/p&gt;
&lt;p&gt;例如主 Agent 负责和用户对话，但某个研究问题可以单独交给一个更便宜的小模型。&lt;/p&gt;
&lt;p&gt;一个最小的 Sub-Agent 实现可以是这样的。&lt;/p&gt;
&lt;div class=&quot;not-prose highlight my-8 max-w-full overflow-hidden rounded border border-border bg-muted text-foreground highlight-source-ts notranslate&quot;&gt;&lt;pre class=&quot;not-prose m-0 max-w-full overflow-x-auto bg-muted p-5 font-mono text-sm text-foreground leading-6&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; generateText&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; tool &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;ai&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; z &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;zod&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; delegateResearch &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;tool&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  description&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Hand off a focused research subtask&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  inputSchema&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; z&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;object&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    question&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; z&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;execute&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; question &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; result &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;generateText&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      model&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;openai/gpt-4.1-mini&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      prompt&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Answer briefly and return only key facts:\n&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; question&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; summary&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; result&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;text &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id=&quot;mcp-与其他工具&quot;&gt;&lt;a class=&quot;anchor&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; href=&quot;#mcp-与其他工具&quot;&gt;&lt;svg class=&quot;octicon octicon-link&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot; height=&quot;16&quot; aria-hidden=&quot;true&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;MCP 与其他工具&lt;/h3&gt;
&lt;p&gt;模型上下文协议 (MCP, Model Context Protocol) 是一个统一的协议，他让 Agent
统一接入外部系统的能力。&lt;/p&gt;
&lt;p&gt;如果你在做一个特定领域的 Agent，往往更适合 MCP，而不是一堆 Skills。&lt;/p&gt;
&lt;p&gt;ClawLess 其实并没有做网页搜索（Web Search），浏览器访问（Browser
User）等功能，我们可以通过 MCP 来接入这些能力。&lt;/p&gt;
&lt;h2 id=&quot;与前端连接&quot;&gt;&lt;a class=&quot;anchor&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; href=&quot;#与前端连接&quot;&gt;&lt;svg class=&quot;octicon octicon-link&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot; height=&quot;16&quot; aria-hidden=&quot;true&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;与前端连接&lt;/h2&gt;
&lt;p&gt;与 Agent 交互的前端部分，我们可以通过网页，Bot 或是其他地方与 Agent 链接。&lt;/p&gt;
&lt;h3 id=&quot;网页对话&quot;&gt;&lt;a class=&quot;anchor&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; href=&quot;#网页对话&quot;&gt;&lt;svg class=&quot;octicon octicon-link&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot; height=&quot;16&quot; aria-hidden=&quot;true&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;网页对话&lt;/h3&gt;
&lt;p&gt;我们刚才的 &lt;code&gt;useChat&lt;/code&gt; Hooks 就是最简单的网页对话部分，我们在这里不再叙述。&lt;/p&gt;
&lt;h3 id=&quot;通过-webhook-与-im-连接&quot;&gt;&lt;a class=&quot;anchor&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; href=&quot;#通过-webhook-与-im-连接&quot;&gt;&lt;svg class=&quot;octicon octicon-link&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot; height=&quot;16&quot; aria-hidden=&quot;true&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;通过 Webhook 与 IM 连接&lt;/h3&gt;
&lt;p&gt;大部分即时通讯软件一般会有内置 Bot 的能力，例如 Telegram 、Discord 等。&lt;/p&gt;
&lt;p&gt;Bot 大部分只有两种接收消息的方法：长轮询和 Webhook。&lt;/p&gt;
&lt;p&gt;长轮询意味着 Bot
需要每隔一段时间（我们称之为心跳周期），向服务器请求最新消息，OpenClaw
的大部分连接器就是这么做的。&lt;/p&gt;
&lt;p&gt;Webhook 则是一种更高效的方式，它允许服务器在有新消息时立即通知 Bot，而不需要 Bot
主动请求。&lt;/p&gt;
&lt;p&gt;在 ClawLess 内部使用 &lt;a href=&quot;https://chat-sdk.dev&quot; rel=&quot;noopener noreferrer&quot;&gt;Chat SDK&lt;/a&gt; 来通过 WebHook 连接各个
IM。&lt;/p&gt;
&lt;p&gt;Chat SDK 特别简单，只需要你创建一个 Chat 实例。&lt;/p&gt;
&lt;div class=&quot;not-prose highlight my-8 max-w-full overflow-hidden rounded border border-border bg-muted text-foreground highlight-source-ts notranslate&quot;&gt;&lt;pre class=&quot;not-prose m-0 max-w-full overflow-x-auto bg-muted p-5 font-mono text-sm text-foreground leading-6&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Next: lib/bot.ts&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; Chat &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;chat&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; bot &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Chat&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  userName&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;mybot&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  adapters&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; slack &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  state&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;createRedisState&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

bot&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;onNewMention&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;thread&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; message&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; agent&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;handleMessage&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;message&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;然后暴露一个适用于 WebHook 的 API 端口。&lt;/p&gt;
&lt;div class=&quot;not-prose highlight my-8 max-w-full overflow-hidden rounded border border-border bg-muted text-foreground highlight-source-ts notranslate&quot;&gt;&lt;pre class=&quot;not-prose m-0 max-w-full overflow-x-auto bg-muted p-5 font-mono text-sm text-foreground leading-6&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Next: app/api/webhooks/slack/route.ts&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token&quot;&gt;POST&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; bot&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;webhooks&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;slack&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&quot;最后&quot;&gt;&lt;a class=&quot;anchor&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; href=&quot;#最后&quot;&gt;&lt;svg class=&quot;octicon octicon-link&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot; height=&quot;16&quot; aria-hidden=&quot;true&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;最后&lt;/h2&gt;
&lt;p&gt;看到这里，你会发现构建 Agent 的思路其实很简单。&lt;/p&gt;
&lt;p&gt;我们特别希望看到你搭配这些，构建自己的 Agent，这真的特别有意思。&lt;/p&gt;
&lt;p&gt;另外，我们在构建一个通用的沙盒虚拟 Bash，它适合任何一个 JS Runtime 的环境，以及
ServerLess 和嵌入式，如果你想了解，可以看看我们的
&lt;a href=&quot;https://niapya.github.io/ai-sdk-x/&quot; rel=&quot;noopener noreferrer&quot;&gt;AI-SDK-X 文档&lt;/a&gt;.&lt;/p&gt;
</description></item><item><title>Welcome To Niapya&apos;s Blog</title><link>https://portfolio.niapya.deno.net/blog/welcome-to-niapyas-blog?lang=zh-cn</link><guid isPermaLink="true">https://portfolio.niapya.deno.net/blog/welcome-to-niapyas-blog?lang=zh-cn</guid><pubDate>Mon, 27 Jul 2026 03:25:00 GMT</pubDate><description>&lt;p&gt;作者认为博客应由作者完全掌控，于是从零实现了一个基于 Hono/JSX 的静态站点生成器，使用 &lt;code&gt;marked&lt;/code&gt; 解析 Markdown、&lt;code&gt;UnoCSS&lt;/code&gt; 生成原子化 CSS，并在构建时渲染成纯 HTML 文件，从而避免任何客户端 JavaScript。文章展示了内容获取、模板渲染和文件输出的完整流程，并说明了选择轻量化方案的理由。作者计划将该项目发展为完整的框架并提供 UI 组件库、插件化 RSS/Sitemap、表单搜索评论等功能，甚至支持在浏览器中运行。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;对，我终于有了一个博客。&lt;/p&gt;
&lt;p&gt;2026 年是 Vibe Coding 大火的一年，我们习惯了使用 Code Agent
来编写任何项目，原来的「手搓」代码的方式被调侃为「古法编程」，这个时候大部分人都没有动手习惯了，如果他们希望有一个博客，直接
Vibe 一个 &lt;code&gt;Next.js&lt;/code&gt; 模版，甚至直接用现成的 &lt;code&gt;SaaS&lt;/code&gt; 博客平台加上 &lt;code&gt;Skills&lt;/code&gt; 让 Agent
帮忙写文章就好了。&lt;/p&gt;
&lt;p&gt;但是我认为，博客作为自己的平台，理应是由作者掌控一切的，所以我希望可以从无到有做出一个自己的博客框架、自己的博客样式以及博客内容。&lt;/p&gt;
&lt;h2 id=&quot;制作博客框架&quot;&gt;&lt;a class=&quot;anchor&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; href=&quot;#制作博客框架&quot;&gt;&lt;svg class=&quot;octicon octicon-link&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot; height=&quot;16&quot; aria-hidden=&quot;true&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;制作博客框架&lt;/h2&gt;
&lt;p&gt;由于这类网站通常不需要服务器端逻辑，我们把博客这类网站称之为「静态网站」，所以我要做的是一个「&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Glossary/SSG&quot; rel=&quot;noopener noreferrer&quot;&gt;静态站点生成器&lt;/a&gt;」（Static
Site Generator，SSG）。&lt;/p&gt;
&lt;p&gt;SSG
的流程很简单：获取内容源并解析成文档，使用模板渲染，输出为网站文件，而我希望掌控这一切。&lt;/p&gt;
&lt;p&gt;第一步是获取内容源并解析成文档。&lt;/p&gt;
&lt;p&gt;内容源的来源可能是任何东西，数据库、CMS、Markdown
文件等等，所以我们创建了一个统一的 Interface，类似于下面。&lt;/p&gt;
&lt;div class=&quot;not-prose highlight my-8 max-w-full overflow-hidden rounded border border-border bg-muted text-foreground highlight-source-ts notranslate&quot;&gt;&lt;pre class=&quot;not-prose m-0 max-w-full overflow-x-auto bg-muted p-5 font-mono text-sm text-foreground leading-6&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;definePost&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
  input&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    title&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    category&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    tags&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    content&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Post&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这样一来，我们可以在任何地方以任何方式定义我们的内容了。未来我们可以使用插件，从文件路由、数据库来源，甚至浏览器中获取内容。&lt;/p&gt;
&lt;p&gt;博客的内容 &lt;code&gt;content&lt;/code&gt; 通常以 Markdown 格式编写，所以我使用 &lt;code&gt;marked&lt;/code&gt; 来把 Markdown
编译为 HTML，并配合 &lt;code&gt;marked-highlight&lt;/code&gt; 与 &lt;code&gt;highlight.js&lt;/code&gt; 提供代码高亮。&lt;/p&gt;
&lt;div class=&quot;not-prose highlight my-8 max-w-full overflow-hidden rounded border border-border bg-muted text-foreground highlight-source-ts notranslate&quot;&gt;&lt;pre class=&quot;not-prose m-0 max-w-full overflow-x-auto bg-muted p-5 font-mono text-sm text-foreground leading-6&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; hljs &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;highlight.js&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; Marked &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;marked&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; markedHighlight &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;marked-highlight&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; markdownRenderer &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Marked&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  breaks&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  gfm&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

markdownRenderer&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;use&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;markedHighlight&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    emptyLangClass&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;hljs&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    langPrefix&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;hljs language-&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;highlight&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;code&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; language&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; highlightLanguage &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; hljs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getLanguage&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;language&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt; language
        &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;plaintext&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

      &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; hljs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;highlight&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;code&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; language&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; highlightLanguage &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;value&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; markdownRenderer&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;parse&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;markdown&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; renderer &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;第二步是使用模板渲染。&lt;/p&gt;
&lt;p&gt;由于我们要编写的是网站文件，在大多数 SSG 中，这个模板一般会和 HTML
结构强相关。如果选择自定义模板语法，就需要自己定义 AST 和编译器来处理，比如
Astro 的 &lt;code&gt;.astro&lt;/code&gt;
模板本身就是一套框架自有的组件语法。与此同时，你还需要开发用于类型检查和语法高亮的工具链，这实在不利好开发者体验。&lt;/p&gt;
&lt;p&gt;还有一些 SSG 使用 React/Vue
组件来编写模板，这样的模板虽然开发体验不错，但它们通常会和对应框架的生态绑定得更深。如果处理不当，甚至可能默认引入客户端
JavaScript。对于 SSG 来说，默认引入任何不必要的客户端 JavaScript 都是不明智的。&lt;/p&gt;
&lt;p&gt;所以我选择了一个轻量的方案 —— 由 &lt;code&gt;Hono/JSX&lt;/code&gt; 驱动的 JSX 来编写模板。它可以在使用
JSX 语法的情况下进行服务端渲染，最终输出 HTML
字符串，同时保持类型安全。对于纯静态页面来说，最终输出的 HTML
文件也不需要包含任何框架相关的客户端代码。&lt;/p&gt;
&lt;div class=&quot;not-prose highlight my-8 max-w-full overflow-hidden rounded border border-border bg-muted text-foreground highlight-source-tsx notranslate&quot;&gt;&lt;pre class=&quot;not-prose m-0 max-w-full overflow-x-auto bg-muted p-5 font-mono text-sm text-foreground leading-6&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;HtmlArticle&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; html &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;div&lt;/span&gt;
      &lt;span class=&quot;token attr-name&quot;&gt;dangerouslySetInnerHTML&lt;/span&gt;&lt;span class=&quot;token script&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; __html&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; html &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;/&amp;gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; app &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Hono&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;/&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;c&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; c&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;html&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;HtmlArticle&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;html&lt;/span&gt;&lt;span class=&quot;token script&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;markdownRenderer&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;parse&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;markdown&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;第三步是输出为网站文件。&lt;/p&gt;
&lt;p&gt;定义好内容和模板之后，我们需要定义网站的结构（也可以称之为路由）。既然我们使用
&lt;code&gt;Hono/JSX&lt;/code&gt;，那我们可以通过一个 HTTP 框架来定义路由，Hono 就是这样的。而且借助
Hono 框架，我们可以轻松启动开发服务器。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;在后续，我们还可以通过这种方式进行服务端渲染（SSR），但目前我们先专注于 SSG。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Hono 提供了 &lt;code&gt;toSSG&lt;/code&gt; 方法，能够在构建时把 Hono
应用的路由预渲染成静态文件，这样我们就可以通过 Hono 来输出静态文件了。&lt;/p&gt;
&lt;h2 id=&quot;设置博客样式&quot;&gt;&lt;a class=&quot;anchor&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; href=&quot;#设置博客样式&quot;&gt;&lt;svg class=&quot;octicon octicon-link&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot; height=&quot;16&quot; aria-hidden=&quot;true&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;设置博客样式&lt;/h2&gt;
&lt;p&gt;在 JSX 中设置 CSS 一直是一个难题，比如 CSS Modules，直到诸如 &lt;code&gt;TailwindCSS&lt;/code&gt;
这样的原子化 CSS 方案出现。&lt;/p&gt;
&lt;p&gt;我们选择了 &lt;code&gt;UnoCSS&lt;/code&gt; 作为我们的 CSS 解决方案，因为它可以按需生成原子化
CSS，并且提供了可编程的生成器 API，可以直接把 CSS 类名编译成 CSS 代码。&lt;/p&gt;
&lt;div class=&quot;not-prose highlight my-8 max-w-full overflow-hidden rounded border border-border bg-muted text-foreground highlight-source-ts notranslate&quot;&gt;&lt;pre class=&quot;not-prose m-0 max-w-full overflow-x-auto bg-muted p-5 font-mono text-sm text-foreground leading-6&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; createGenerator&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; presetWind4 &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;unocss&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; uno &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;createGenerator&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  presets&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;presetWind4&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;compileAtomicCss&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;input&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token&quot;&gt;Promise&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; css &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; uno&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;generate&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;input&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; css&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;默认的博客样式我选择了一个经典的麦金塔风格，我们正在计划使用 Figma 并制作相关的
UI 组件库，以打造一个复古风格的博客主题。&lt;/p&gt;
&lt;h2 id=&quot;后续&quot;&gt;&lt;a class=&quot;anchor&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; href=&quot;#后续&quot;&gt;&lt;svg class=&quot;octicon octicon-link&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot; height=&quot;16&quot; aria-hidden=&quot;true&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;后续&lt;/h2&gt;
&lt;p&gt;你也许会发现，本博客没有用到任何客户端
JavaScript，这正是我想要的，对于一个静态博客，他不需要客户端 JavaScript 的引入。&lt;/p&gt;
&lt;p&gt;你可以在 GitHub 上找到这个项目的源代码。&lt;/p&gt;
&lt;p&gt;我们未来应该会把这个项目变成一个真正的框架，然后推出一套自己的 UI 组件库。&lt;/p&gt;
&lt;p&gt;以及下面的功能：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;插件支持的 RSS 与 Sitemap 功能&lt;/li&gt;
&lt;li&gt;基于 HTML Form 的搜索和评论系统&lt;/li&gt;
&lt;li&gt;在线编译器&lt;/li&gt;
&lt;li&gt;以及可能的 SSR 全栈支持&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因为我们没有用到 &lt;code&gt;Vite&lt;/code&gt;，也尽量避免依赖只属于 &lt;code&gt;Node.js&lt;/code&gt;
环境的模块，所以这个框架未来甚至可以在浏览器中直接运行。&lt;/p&gt;
</description></item></channel></rss>