<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>棒无 · 文章</title><description>Aim lower</description><link>https://bangwu.me/</link><language>zh-cn</language><lastBuildDate>Tue, 27 Oct 2026 07:19:10 GMT</lastBuildDate><item><title>只给一条路由开 SSR：一个静态博客的局部动态化</title><link>https://bangwu.me/posts/moments-ssr/</link><guid isPermaLink="true">https://bangwu.me/posts/moments-ssr/</guid><description>图文页原来是个 client:only 的空壳。我只把 /moments/ 改成服务端渲染，其余保持静态——然后撞上 Worker 体积、没有 ResizeObserver、Swup 跨布局残留，以及一套比 SSR 难得多得多的滚动状态。</description><pubDate>Sat, 17 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;先说结论&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;/moments/&lt;/code&gt; 之前是一个 &lt;code&gt;client:only&lt;/code&gt; 的 React 岛：浏览器加载完，再去 &lt;code&gt;s.bangwu.top&lt;/code&gt; 取图文，然后渲染。对人没问题，对搜索爬虫和关掉 JS 的访问者，它就是一个空壳。&lt;/p&gt;
&lt;p&gt;我把这一条路由改成了按请求 SSR，其他页面继续静态预渲染。SSR 本身大概一个下午就通了，真正花时间的是它之后暴露出来的东西：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Worker 里没有 &lt;code&gt;ResizeObserver&lt;/code&gt;&lt;/strong&gt;，而 &lt;code&gt;200 OK&lt;/code&gt; 不代表页面渲染完整&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Worker 体积不由代码量决定，由这条路由 import 了哪个 layout 决定&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SSR 不等于可索引&lt;/strong&gt;，无限滚动出来的内容依然不可发现&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;滚动状态的边界比 SSR 难&lt;/strong&gt;——返回丢内容、快速滑动连拉 15 页、预加载把「布局变高」当成「用户想往下看」&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Swup 跨布局只替换 &lt;code&gt;&amp;lt;main&amp;gt;&lt;/code&gt;&lt;/strong&gt;，旧页面外壳会留在 DOM 里&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;一句话总结：&lt;strong&gt;SSR 不是把渲染地点挪一下，而是把浏览器里默认成立的一堆事情，变成需要自己负责的。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;先交代边界：下面是这个博客自己的改造记录，站点是 Astro 5 + &lt;code&gt;@astrojs/cloudflare&lt;/code&gt; + Swup + Masonic。数据都来自本地 Worker 实测和 Playwright 回归，不是估算。&lt;/p&gt;
&lt;h2&gt;原来为什么是个空壳&lt;/h2&gt;
&lt;p&gt;图文页原来是这样挂载的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;MemosFeed client:only=&quot;react&quot; /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;client:only&lt;/code&gt; 的含义是「不要服务端渲染，等到浏览器再挂」。所以首屏 HTML 里没有一条 memo，只有加载姿态的容器。人看到的是骨架屏，爬虫看到的是空 div。&lt;/p&gt;
&lt;h2&gt;为什么不全站 SSR&lt;/h2&gt;
&lt;p&gt;有三个选项，我当时是这么比的：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方案&lt;/th&gt;
&lt;th&gt;改动量&lt;/th&gt;
&lt;th&gt;代价&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;全站 &lt;code&gt;output: &apos;server&apos;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;一行配置&lt;/td&gt;
&lt;td&gt;每篇文章都按请求渲染，缓存、Worker 体积、可用性全部要重新想&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;只给 &lt;code&gt;/moments/&lt;/code&gt; 加 &lt;code&gt;prerender = false&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;一个路由&lt;/td&gt;
&lt;td&gt;要处理 layout 体积、水合和降级&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;不做 SSR，继续 CSR&lt;/td&gt;
&lt;td&gt;零&lt;/td&gt;
&lt;td&gt;详情页永远不可索引，内容更新后也没有稳定 URL&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;选第二个。博客 99% 的流量在第一秒就命中静态 HTML，没必要为了一个 feed 把全站变成动态。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;---
export const prerender = false;
---
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这一行是 Astro 的局部退出预渲染：加上 adapter 之后，只有标了它的路由会在 Worker 里跑，其余照旧在构建时生成。文档写得很清楚，我就不复述了。&lt;/p&gt;
&lt;h2&gt;改造的最小闭环&lt;/h2&gt;
&lt;p&gt;三件事：服务端取第一页、客户端接管后续、失败能退回原流程。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;---
let initialData: InitialMemosData | undefined;
try {
  initialData = await loadInitialMemos(baseUrl);
} catch (error) {
  console.error(&quot;[moments] failed to load initial memos&quot;, error);
}
---

&amp;lt;MemosFeed baseUrl={baseUrl} initialData={initialData} client:load /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;client:only&lt;/code&gt; 换成 &lt;code&gt;client:load&lt;/code&gt;，&lt;code&gt;initialData&lt;/code&gt; 就成了「首屏有没有真实内容」的开关。服务端拿不到数据时就不传，组件按老路自己加载，页面不会白屏。&lt;/p&gt;
&lt;p&gt;数据加载和视图转换抽到了 &lt;code&gt;src/utils/memos.ts&lt;/code&gt;，Worker 和浏览器共用同一份——否则你很快就会有两套字段映射，然后它们会慢慢不一样。&lt;/p&gt;
&lt;h2&gt;坑一：Worker 里没有 ResizeObserver&lt;/h2&gt;
&lt;p&gt;改完第一次测，&lt;code&gt;/moments/&lt;/code&gt; 返回 200，但 HTML 里没有 memo 正文。&lt;/p&gt;
&lt;p&gt;原因是 Masonic 的 &lt;code&gt;useResizeObserver&lt;/code&gt;、&lt;code&gt;useContainerPosition&lt;/code&gt; 在渲染期就直接 &lt;code&gt;new ResizeObserver(...)&lt;/code&gt;。Cloudflare Worker 里没有这个浏览器 API，React 的服务端渲染中途抛错，Astro 把已经渲染出来的部分吐了出来——&lt;strong&gt;状态码还是 200&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;这次最有用的教训就是这句：&lt;strong&gt;200 不等于成功。&lt;/strong&gt; 只看状态码的检查会放过它。&lt;/p&gt;
&lt;p&gt;修法不是给 Worker 打 polyfill。&lt;code&gt;ResizeObserver&lt;/code&gt; 是布局 API，Worker 根本没有布局，塞一个假对象只会把错误藏起来，等哪天真的出问题再还给你。做法是把依赖它的东西整体下沉到一个只在水合后挂载的子组件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (!hydrated) {
  return (
    &amp;lt;div className=&quot;memos-ssr-grid&quot;&amp;gt;
      {items.map((item) =&amp;gt; &amp;lt;MemoCard key={item.id} data={item} /&amp;gt;)}
    &amp;lt;/div&amp;gt;
  );
}

return &amp;lt;MemosMasonry items={items} ... /&amp;gt;; // Masonic 只在这里出现
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;服务端输出的是 CSS 多列布局，浏览器水合之后再换成瀑布流。代价是首屏会重排一次，收益是 SSR 路径完全不依赖浏览器 API——这个交换我觉得值。&lt;/p&gt;
&lt;h2&gt;坑二：Worker 体积是架构问题，不是构建问题&lt;/h2&gt;
&lt;p&gt;Build 是绿的，部署失败。&lt;/p&gt;
&lt;p&gt;一开始会本能地怀疑 &lt;code&gt;pnpm install&lt;/code&gt;、依赖版本、构建缓存。实际原因和这些都没关系：&lt;code&gt;/moments/&lt;/code&gt; 直接复用了全站的 &lt;code&gt;MainGridLayout&lt;/code&gt;，它带 &lt;code&gt;SideBar&lt;/code&gt;，而分类/标签要读全部文章数据；再加上 &lt;code&gt;astro-icon&lt;/code&gt; 默认把整套图标库打进 SSR bundle。&lt;/p&gt;
&lt;p&gt;当时的 dry-run 大概是这个量级：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;MainGridLayout   ~14 MiB
astro-icon       ~14 MiB
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;而同一条路由要进的是同一个 Worker 脚本。当时免费计划的脚本上限是 3 MiB（按压缩后算）。我写这篇时去翻了文档，这一项已经改成 64 MiB 未压缩、没有压缩后上限了——&lt;strong&gt;限制会变，但下面这句话不变：一条 SSR 路由的 import 就是它的部署预算。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;第一版修法是写一个轻量的 &lt;code&gt;MomentsLayout&lt;/code&gt;：保留 banner、导航、正文、页脚，不加载分类/标签侧栏。体积降到 &lt;code&gt;1605.03 KiB / gzip 328.77 KiB&lt;/code&gt;，部署过了。&lt;/p&gt;
&lt;p&gt;但我顺手把顶部导航换成了纯文本——这是一次视觉上的倒退，截图一看就不对。第二版才是正解：&lt;strong&gt;保留原来的 &lt;code&gt;Navbar&lt;/code&gt;，只把 &lt;code&gt;astro-icon&lt;/code&gt; 从「整套图标库」收成项目实际用到的白名单。&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;icon({
  include: {
    &quot;fa6-brands&quot;: [&quot;github&quot;, &quot;creative-commons&quot;],
    &quot;fa6-solid&quot;: [&quot;arrow-rotate-left&quot;, &quot;arrow-up-right-from-square&quot;, &quot;chevron-right&quot;],
    &quot;material-symbols&quot;: [
      &quot;home-outline-rounded&quot;, &quot;search&quot;, &quot;palette-outline&quot;,
      &quot;dark-mode-outline-rounded&quot;, &quot;menu-rounded&quot;, &quot;keyboard-arrow-up-rounded&quot;,
      // ...只列真正用到的
    ],
    &quot;simple-icons&quot;: [&quot;leetcode&quot;, &quot;neteasecloudmusic&quot;],
  },
}),
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;三个阶段放在一起看：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;版本&lt;/th&gt;
&lt;th&gt;SSR 路由依赖&lt;/th&gt;
&lt;th&gt;dry-run&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;复用 &lt;code&gt;MainGridLayout&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;SideBar + 全量图标&lt;/td&gt;
&lt;td&gt;超限&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;轻量 layout + 文本导航&lt;/td&gt;
&lt;td&gt;无 SideBar&lt;/td&gt;
&lt;td&gt;1605.03 KiB / gzip 328.77&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;恢复原 Navbar + 图标白名单&lt;/td&gt;
&lt;td&gt;无 SideBar，精确图标&lt;/td&gt;
&lt;td&gt;1647.73 KiB / gzip 341.25&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;所以现在的流程里多了一道门：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pnpm build &amp;amp;&amp;amp; pnpm exec wrangler deploy --dry-run
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;构建通过只说明代码能编译，dry-run 通过才说明能上线。这两个信号经常不一致。&lt;/p&gt;
&lt;h2&gt;坑三：SSR 不等于可索引&lt;/h2&gt;
&lt;p&gt;列表页 SSR 只解决「首屏有 5 条」。往下滚出来的那些，爬虫永远看不到，也没有可以分享的 URL。&lt;/p&gt;
&lt;p&gt;所以每条 memo 要有自己的页面：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/moments/{id}/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;详情在服务端取 &lt;code&gt;GET /api/v1/memos/{id}&lt;/code&gt;，直接输出正文、全部图片、作者和时间，再补 canonical、&lt;code&gt;SocialMediaPosting&lt;/code&gt; 和 &lt;code&gt;BreadcrumbList&lt;/code&gt;。关键是状态码别撒胡椒面：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (loadState === &quot;not-found&quot;) Astro.response.status = 404;
if (loadState === &quot;error&quot;) Astro.response.status = 502;

const robots = loadState === &quot;ok&quot; ? &quot;index, follow&quot; : &quot;noindex, nofollow&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上游挂了返回 502 而不是 404，是因为搜索引擎会把 404 记成「这个 URL 没了」，等上游恢复了你得自己再求它回来。&lt;/p&gt;
&lt;p&gt;还有一个很容易漏的点：&lt;strong&gt;动态路由不会出现在构建时的 sitemap 里。&lt;/strong&gt; Astro 的 sitemap 集成在构建期跑，那时候这些 URL 还不存在。只能另开一个运行时 sitemap：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export const prerender = false;

export const GET: APIRoute = async ({ site }) =&amp;gt; {
  // 分页拉全部公开 memo，输出 &amp;lt;urlset&amp;gt;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后在 &lt;code&gt;robots.txt&lt;/code&gt; 里声明它。写的时候顺手发现一个 N+1：列表每页 5 条，162 条 memo 就是 33 次上游请求。给 loader 加了一个 &lt;code&gt;pageSize&lt;/code&gt; 参数，列表继续 5 条（瀑布流需要），sitemap 走 100 条一页。&lt;/p&gt;
&lt;h2&gt;坑四：返回列表时，卡片和位置都没了&lt;/h2&gt;
&lt;p&gt;这是用户报过来的问题，原话我记着：加载很多内容之后点进详情，返回图文时，已经加载的内容会消失，回不到之前的状态。&lt;/p&gt;
&lt;p&gt;原因不复杂：Swup 缓存的是 HTML，不是 React 岛的内存。返回时 &lt;code&gt;MemosFeed&lt;/code&gt; 被重新挂载，&lt;code&gt;useState&lt;/code&gt; 从初始值开始，缓存里那份 HTML 也还是首屏的 5 条。&lt;/p&gt;
&lt;p&gt;修法是 &lt;code&gt;sessionStorage&lt;/code&gt; 存一份快照：已加载的 items、&lt;code&gt;nextPageToken&lt;/code&gt;、&lt;code&gt;hasMore&lt;/code&gt;、&lt;code&gt;scrollY&lt;/code&gt;，带 version 和 30 分钟 TTL。看起来是十分钟的活，实际撞了两个更麻烦的东西。&lt;/p&gt;
&lt;h3&gt;新实例会把旧缓存覆盖成 0&lt;/h3&gt;
&lt;p&gt;加完缓存，状态还是丢。&lt;/p&gt;
&lt;p&gt;因为 Swup 返回时，旧 island 的监听器还没卸载。它拿着自己那份 &lt;code&gt;scrollY = 0&lt;/code&gt;，把刚恢复好的缓存写了回去。所以每个 feed 实例需要一个 owner token，只有当前实例能写：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (feedWindow.__bangwuMomentsFeedOwner !== feedInstanceIdRef.current) return;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;再加一道「恢复未完成」的门：&lt;code&gt;restoreComplete&lt;/code&gt; 为 false 的时候禁止写缓存。这两条都不是优雅设计，是跟真实时序对出来的。&lt;/p&gt;
&lt;h3&gt;点击的瞬间，scrollY 已经是 0&lt;/h3&gt;
&lt;p&gt;这条最隐蔽。我们是在点击链接的时候保存状态，但 Swup 会先把页面滚回顶部再导航；等 click handler 去读 &lt;code&gt;window.scrollY&lt;/code&gt;，读到的已经是 0。日志里，刚存下的 1156 变成了 0。&lt;/p&gt;
&lt;p&gt;修法很土，但正确：不要在导航瞬间读位置，用 scroll listener 记下来的最后一个值。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const latestScrollYRef = useRef(0);

useEffect(() =&amp;gt; {
  const update = () =&amp;gt; { latestScrollYRef.current = window.scrollY; };
  window.addEventListener(&quot;scroll&quot;, update, { passive: true });
  return () =&amp;gt; window.removeEventListener(&quot;scroll&quot;, update);
}, []);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;验证结果：15 张卡片 → 进详情 → 返回，卡片还是 15 张，&lt;code&gt;scrollY&lt;/code&gt; 从 1156 到 1156。浏览器后退走的是同一条路径，也测了。&lt;/p&gt;
&lt;h2&gt;坑五：预加载分不清「用户想往下看」和「布局变高了」&lt;/h2&gt;
&lt;p&gt;原来的实现用的是 &lt;code&gt;useInfiniteLoader&lt;/code&gt;，&lt;code&gt;totalItems&lt;/code&gt; 设成 &lt;code&gt;items.length + 5&lt;/code&gt;，threshold 也是 5。水合之后，Masonic 认为有 5 个「缺失项」，于是用户一根手指都没动，它就去拉下一页。&lt;/p&gt;
&lt;p&gt;实测：&lt;strong&gt;不滚动，1.5 秒内两次分页请求，卡片从 5 涨到 10。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;第二个是 sentinel 的幻觉。布局变高之后，观察点还在 expanded root margin 里，&lt;code&gt;IntersectionObserver&lt;/code&gt; 会再回调一次，于是一页接一页地串下去。&lt;/p&gt;
&lt;p&gt;第三个是快速滑动：一次高速手势，在约 1 秒里触发了 15 次分页请求。&lt;/p&gt;
&lt;p&gt;现在的规则是这么写的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const PRELOAD_ROOT_MARGIN = &quot;0px 0px 640px 0px&quot;;
const PRELOAD_COOLDOWN_MS = 700;
const SCROLL_BURST_IDLE_MS = 600;
const MAX_LOADS_PER_SCROLL_BURST = 2;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;翻译一下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;用真实 sentinel，只观察底部 640px&lt;/li&gt;
&lt;li&gt;只认 &lt;code&gt;isTrusted&lt;/code&gt; 的滚动事件，程序化恢复滚动不算&lt;/li&gt;
&lt;li&gt;每次成功加载后，至少再推进约 1/3 屏（上限 320px），才允许下一页&lt;/li&gt;
&lt;li&gt;一页 700ms 冷却；一次连续滚动最多 2 页；停顿 600ms 才算新一轮&lt;/li&gt;
&lt;li&gt;页面短到不能滚动时，允许自动补一页，避免用户卡死&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;优化前后的实测对比：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;指标&lt;/th&gt;
&lt;th&gt;优化前&lt;/th&gt;
&lt;th&gt;优化后&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;未滚动时的分页请求&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;未滚动时的卡片数&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;5&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;轮播图片 DOM 节点&lt;/td&gt;
&lt;td&gt;153&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;15&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;首屏图片请求&lt;/td&gt;
&lt;td&gt;43&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;10&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;加载中的 skeleton&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;3&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;一次快速滑动触发的请求&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;图片那两行是顺手的收获：每个轮播原来把几十张 &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; 全塞进 DOM，&lt;code&gt;loading=&quot;lazy&quot;&lt;/code&gt; 也拦不住浏览器提前拉。现在只保留当前图、前一张、后一张，滑到哪里补哪里。&lt;/p&gt;
&lt;p&gt;skeleton 也补回来了，分三种状态：SSR 首屏有真实内容就不显示骨架；分页时显示 3 张；失败时保留已有卡片 + 「加载失败，点击重试」，而不是把整页换成一个错误提示。&lt;/p&gt;
&lt;h2&gt;坑六：Swup 跨布局只替换 main&lt;/h2&gt;
&lt;p&gt;这个不是我测出来的，是一张截图发现的：桌面端从首页点进图文，左边的分类/标签还在，内容被挤到 banner 下面。&lt;/p&gt;
&lt;p&gt;因为 Swup 的配置是 &lt;code&gt;containers: [&quot;main&quot;, &quot;#toc&quot;]&lt;/code&gt;，而两个页面用的是两套 layout：&lt;code&gt;MainGridLayout&lt;/code&gt; 有 sidebar，&lt;code&gt;MomentsLayout&lt;/code&gt; 没有。切过去之后，旧的 sidebar 外壳留在 DOM 里，新内容就落进了一个错位的容器。&lt;/p&gt;
&lt;p&gt;修法不是调 CSS，而是让&lt;strong&gt;跨布局&lt;/strong&gt;的导航走完整刷新，同一布局内继续用 Swup：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;Navbar forceFullNavigation={true} /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;a href=&quot;/moments/&quot; data-no-swup={isCrossLayout ? true : undefined}&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;data-no-swup&lt;/code&gt; 是 Swup 原生支持的，默认的 &lt;code&gt;ignoreVisit&lt;/code&gt; 就是 &lt;code&gt;el?.closest(&apos;[data-no-swup]&apos;)&lt;/code&gt;。规则只有一条：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;图文 ↔ 首页 / 归档 / 关于：强制整页&lt;/li&gt;
&lt;li&gt;图文列表 ↔ memo 详情：继续 SPA，因为状态恢复就靠这条路径&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这也是那套轻量 layout 的隐性成本：你为了体积拆出一套外壳，就得同时维护两套外壳之间的导航边界。&lt;/p&gt;
&lt;h2&gt;我是怎么验证的&lt;/h2&gt;
&lt;p&gt;这次的坑几乎都不在构建阶段暴露，所以验证方式比平时重一点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;看线上行为，不看产物。&lt;/strong&gt; &lt;code&gt;pnpm build&lt;/code&gt; 之后用 &lt;code&gt;wrangler dev&lt;/code&gt; 起本地 Worker，&lt;code&gt;curl&lt;/code&gt; &lt;code&gt;/moments/&lt;/code&gt;，确认 HTML 里真的有 memo 正文。构建成功和页面正确是两件事。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;人为制造失败。&lt;/strong&gt; 把 API 延迟 1 秒，断言分页期间出现 3 张 skeleton；把分页打成 503，断言已有卡片不被清空、重试能恢复。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;用真实导航跑回归。&lt;/strong&gt; Playwright：列表加载到 15 张 → 进详情 → 返回，断言卡片数是 15、&lt;code&gt;scrollY&lt;/code&gt; 是 1156；再压一轮快速滑动，统计请求数。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;每次 dry-run。&lt;/strong&gt; &lt;code&gt;wrangler deploy --dry-run&lt;/code&gt; 看上传体积。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;常规三道门。&lt;/strong&gt; &lt;code&gt;astro check&lt;/code&gt;、Biome、&lt;code&gt;pnpm build&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;如果重来一次&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;先量体积预算，再决定 layout 边界。&lt;/strong&gt; SSR 路由 import 什么，就等于往 Worker 里塞什么。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SSR 只保证「首屏有内容」。&lt;/strong&gt; 「可索引」要靠独立 URL、canonical、结构化数据和运行时 sitemap 一起完成。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;状态的优先级：URL &amp;gt; sessionStorage &amp;gt; 内存。&lt;/strong&gt; SPA 导航里，内存状态是最不可靠的一层。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;任何自动加载都必须有人为意图。&lt;/strong&gt; 由布局变化触发的加载，最后都会变成请求风暴。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;跨布局导航不要交给 SPA router。&lt;/strong&gt; 它只负责替换容器，不负责替换世界观。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;最后&lt;/h2&gt;
&lt;p&gt;真正落到仓库里的改动不算大：一个轻量 layout、两个路由、一个 sitemap endpoint、一个 feed 组件。加起来不到两天。&lt;/p&gt;
&lt;p&gt;但这两天里，大部分时间不是在写 SSR，而是在回答同一个问题：&lt;strong&gt;这份状态到底该由谁拥有。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;渲染搬到 Worker 之后，顺带把「谁是当前实例」「谁的滚动位置算数」「什么时候可以写缓存」这些原本由浏览器帮你糊过去的问题，全搬到了台面上。这也是我对「局部动态化」最真实的体感：成本不在那一个 &lt;code&gt;prerender = false&lt;/code&gt;，在它周围所有默认假设失效的地方。&lt;/p&gt;
&lt;p&gt;代码在 &lt;a href=&quot;https://github.com/markbang/blog&quot;&gt;markbang/blog&lt;/a&gt;。&lt;/p&gt;
&lt;h2&gt;参考&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.astro.build/en/guides/on-demand-rendering/&quot;&gt;Astro: On-demand rendering&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.astro.build/en/guides/integrations-guide/cloudflare/&quot;&gt;@astrojs/cloudflare&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developers.cloudflare.com/workers/platform/limits/&quot;&gt;Cloudflare Workers limits&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://swup.js.org/options/&quot;&gt;Swup options（&lt;code&gt;ignoreVisit&lt;/code&gt; / &lt;code&gt;data-no-swup&lt;/code&gt;）&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/jaredLunde/masonic&quot;&gt;Masonic&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Agent 开始有自己的电脑了：从 OpenAI Dots 到 Claude Code Cloud</title><link>https://bangwu.me/posts/agent-cloud-computer/</link><guid isPermaLink="true">https://bangwu.me/posts/agent-cloud-computer/</guid><description>一周之内，Claude Code Cloud sessions 正式可用、Manus 2.0 推出 Cloud Computer、OpenAI 在 DevDay 发布常驻智能体 Dots。Agent 的运行位置正在从你的笔记本搬到云上，真正难的不是给它一台 VM，而是状态、凭证和停止键放在哪里。</description><pubDate>Wed, 30 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;先说结论&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Agent 的竞争单位，正在从「一个模型」变成「一个模型 + 一台一直开着的电脑」。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;过去一周，这件事几家公司是一起做的：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;9 月 24 日&lt;/strong&gt;：Claude Code Cloud sessions 结束研究预览，正式可用。官方文档的描述很直白：session 跑在云端，「合上笔记本之后也会继续运行」。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;9 月 28 日&lt;/strong&gt;：Manus 2.0 发布，其中一项是可以单独购买的 Cloud Computer，给游戏服务器和 Automations 当「永久的家」。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;9 月 29 日（北京时间 30 日凌晨）&lt;/strong&gt;：OpenAI DevDay 发布 Dots。每个 Dot 由 GPT-6 Astra 驱动，有自己的云电脑和浏览器，可以 24/7 持续工作，通过插件连接 4000 多个应用。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;再往前看，9 月 10 日 OpenAI 的 Agents API 进了公测，把 Codex 的 harness 连同托管沙箱一起开放；8 月的 Grok Bot 一开始就被描述成「自带电脑的 AI 同事」；Meta 的 Muse 也给每个用户配了一台云电脑。&lt;/p&gt;
&lt;p&gt;所以这不是某一家的产品花样，而是一个方向：&lt;strong&gt;Agent 的生命周期，正在和你的设备解绑。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;我的判断是，接下来比拼的不是谁的 VM 更便宜，而是谁把下面三个问题回答得更好：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;状态放在哪&lt;/strong&gt;：机器被回收之后，什么还在，什么没了&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;凭证放在哪&lt;/strong&gt;：Agent 能用你的账号，但它能不能「拿到」你的账号&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;停止键在谁手里&lt;/strong&gt;：一个 24 小时在跑的东西，谁能看、谁能停、停在哪&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;先交代一下立场：我们自己也在做 Cohub 这类云端 agent 环境，这个博客最近的文章基本都是在 Cohub 的云端沙箱里写出来的。下面拿它举例时，我只写自己实际观察到的事，不替它做宣传，也不拿它去跟别家比测试结果。Dots 我还没用上，下面关于它的内容都来自官方说明和媒体报道。&lt;/p&gt;
&lt;h2&gt;这一周到底发生了什么&lt;/h2&gt;
&lt;p&gt;把几家的东西摆在一起看，会发现它们其实是三种不同的形态：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;形态&lt;/th&gt;
&lt;th&gt;代表&lt;/th&gt;
&lt;th&gt;电脑归谁管&lt;/th&gt;
&lt;th&gt;你怎么和它交互&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;托管 session&lt;/td&gt;
&lt;td&gt;Claude Code Cloud sessions、OpenAI Agents API&lt;/td&gt;
&lt;td&gt;平台按 session 分配，用完回收&lt;/td&gt;
&lt;td&gt;给一个任务，看进度，中途插话&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;常驻个人 agent&lt;/td&gt;
&lt;td&gt;OpenAI Dots、Meta Muse、Grok Bot、Manus Cue&lt;/td&gt;
&lt;td&gt;平台给「你的 agent」长期分配一台&lt;/td&gt;
&lt;td&gt;交给它一个职责，它自己持续推进&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;可购买的专用机&lt;/td&gt;
&lt;td&gt;Manus Cloud Computer&lt;/td&gt;
&lt;td&gt;你买下的一台环境&lt;/td&gt;
&lt;td&gt;当服务器或自动化的宿主&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;三种形态的区别，不在于有没有云电脑，而在于&lt;strong&gt;这台电脑的寿命跟着谁&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;托管 session      电脑寿命 = 一次任务
常驻 agent        电脑寿命 = 一个 agent 身份
专用机            电脑寿命 = 你付费的时长
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这会直接决定后面的状态、凭证和计费怎么设计。&lt;/p&gt;
&lt;h2&gt;为什么非得搬上云&lt;/h2&gt;
&lt;p&gt;本地 agent 有一个一直没被说破的限制：&lt;strong&gt;它的工作时长被人的作息绑住了。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;笔记本一合上，进程挂起；换个 Wi-Fi，长连接断了；想同时开三个任务，风扇先受不了。更根本的是，有一类工作本来就不该等人坐到电脑前才开始：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;监控 bug 报告，出现新的就复现&lt;/li&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;这些都是事件触发的，或者要持续运行，得有个一直在线的地方来跑。Manus 帮助文档里讲 Cloud Computer 的那句话很准确：有些项目「outgrow one machine」，一台个人电脑装不下。&lt;/p&gt;
&lt;p&gt;OpenAI 介绍 Dots 时用的也是这个说法：不是交给它一个任务，而是交给它一份职责，比如盯着 bug 报告，或者把一个应用从要下线的 API 上迁走，然后让它自己一步步推进。&lt;/p&gt;
&lt;p&gt;所以云电脑解决的不是「算力不够」，而是&lt;strong&gt;让 agent 从一次性的工具调用，变成一个持续存在的执行者&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;一台 agent 电脑，真正要回答的四个问题&lt;/h2&gt;
&lt;p&gt;几家里公开文档写得最细的是 Claude Code 的 cloud sessions，下面主要拿它当参照。不是说它做得最好，而是它把取舍写清楚了，方便讨论。&lt;/p&gt;
&lt;h3&gt;1. 生命周期：对话能恢复，不等于工作能恢复&lt;/h3&gt;
&lt;p&gt;Claude Code 文档里有一段很值得看：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Cloud sessions stop after a period of inactivity and the session&apos;s VM is reclaimed.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;之后重新打开 session，会分配一台新的 VM，并恢复对话历史。但紧接着一句是：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Background work that was still running when the VM was reclaimed, such as subagents and shell commands, isn&apos;t restored.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;也就是说，&lt;strong&gt;对话在，进程不在了&lt;/strong&gt;。还有个细节：等你批准一个 MCP 工具调用的那段时间，也算「不活跃」，session 可能就在那时过期。&lt;/p&gt;
&lt;p&gt;这其实是所有云端 agent 都绕不开的问题。我的理解是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;进程内存    → 随时可能消失
VM 磁盘     → 活到机器被回收
对话历史    → 平台负责保留
git / 远端  → 真正持久
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所以在云端 agent 里，「做完一步就提交」不只是 git 的好习惯，&lt;strong&gt;而是 agent 在这种环境里活下来的办法&lt;/strong&gt;。一个跑了三小时的任务，最后的结果如果只存在进程内存和临时目录里，机器一回收就全没了。&lt;/p&gt;
&lt;h3&gt;2. 状态：哪些东西是持久的，要写在明面上&lt;/h3&gt;
&lt;p&gt;说一个我自己遇到的事。&lt;/p&gt;
&lt;p&gt;这个博客最近一个多月的十几篇文章，都是 agent 在 Cohub 的云端沙箱里写、跑 &lt;code&gt;pnpm build&lt;/code&gt;、再 push 出去的。这期间，&lt;code&gt;/workspace&lt;/code&gt; 下的仓库、依赖、草稿一直都在；但 &lt;code&gt;gh&lt;/code&gt; 的登录状态和 git credential helper 丢过好几次。光 GitHub device flow 就走了 4 次：8 月 22 日、8 月 23 日两次、9 月 14 日。&lt;/p&gt;
&lt;p&gt;今天写这篇的时候又碰上了一次：&lt;code&gt;~/.config/gh&lt;/code&gt; 不见了，&lt;code&gt;gh&lt;/code&gt; 命令也没了，&lt;code&gt;git push --dry-run&lt;/code&gt; 直接报：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fatal: could not read Username for &apos;https://github.com&apos;: No such device or address
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;原因不复杂：&lt;code&gt;/workspace&lt;/code&gt; 挂的是持久存储，用户目录不是。环境一重建，装在用户目录下的工具和登录状态就没了。&lt;/p&gt;
&lt;p&gt;这不算事故，但它说明了一件事：&lt;strong&gt;云端 agent 环境里，「哪些路径是持久的」必须是一条写死、写明的规则&lt;/strong&gt;，不能让 agent 或者用户靠猜。否则 agent 会很自然地把东西装进 &lt;code&gt;~/.local&lt;/code&gt;、把登录态放进 &lt;code&gt;~/.config&lt;/code&gt;，然后在某次重建之后一脸无辜地重新问你要授权。&lt;/p&gt;
&lt;p&gt;我现在的默认做法是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;仓库、产物、笔记只放在持久目录&lt;/li&gt;
&lt;li&gt;需要长期保留的配置，写进仓库或者环境变量，不依赖用户目录&lt;/li&gt;
&lt;li&gt;每次推送前先检查一次凭证状态，而不是等 push 失败了再补&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3. 凭证：agent 能用你的账号，不等于它该拿到你的账号&lt;/h3&gt;
&lt;p&gt;还是上面那件事。这次能推送，是因为环境变量里注入了 &lt;code&gt;GITHUB_TOKEN&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;这对我这样一个单人博客仓库来说够用，但它的问题也很明显：&lt;strong&gt;token 就在环境里，agent 跑的任何一个进程都能读到它。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Claude Code 的云端环境在这里的做法不一样。文档写的是：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;In Anthropic-hosted environments, your GitHub credentials stay encrypted on Anthropic&apos;s servers and never enter a session&apos;s VM.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;VM 里的 git 只拿到一个受限凭证，所有 GitHub 操作都经过一个代理，由代理在服务端换成真实 token。它还顺手加了几条限制：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;git push&lt;/code&gt; 只能推到当前 session 的工作分支&lt;/li&gt;
&lt;li&gt;GitHub API 只能访问挂到这个 session 上的仓库&lt;/li&gt;
&lt;li&gt;GraphQL 只放行一小组 PR 相关操作，其他一律 403&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;画成图大概是这样：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;环境变量方案：
  agent 进程 ──读取──▶ $GITHUB_TOKEN ──▶ GitHub（完整权限）

代理方案：
  agent 进程 ──受限凭证──▶ 代理 ──校验 + 换成真实 token──▶ GitHub
                           │
                           └─ 只允许：当前分支 push / 已挂载仓库 / 白名单操作
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;区别在于，&lt;strong&gt;agent 没有被信任去「拿着」凭证，只是被允许「借用」凭证做一小组事。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;一个 session 级别的编码 agent，差别也许还不大。但 Dots 这种 24 小时运行、通过插件连 4000 多个应用、还能打电话发短信的常驻 agent，如果凭证直接放在它的电脑上，一次提示注入就可能把整串账号带走。这也是我在&lt;a href=&quot;/posts/agent-unauthorized-access-boundaries/&quot;&gt;《Agent 越权》&lt;/a&gt;那篇里说的：&lt;strong&gt;权限要在 agent 外面强制，不能靠 agent 自己自觉。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;从公开信息看，OpenAI 给 Dots 的设计也是往这个方向走的：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;用户给 Dot 设定边界：能用哪些应用、能不能操作电脑、能执行哪些动作&lt;/li&gt;
&lt;li&gt;改密码这类敏感操作之前，auto-review 会先找用户确认&lt;/li&gt;
&lt;li&gt;有一个监控系统，可以在发现安全问题时暂停 Dot 的工作&lt;/li&gt;
&lt;li&gt;你可以随时打开 Dot 的电脑，看它在做什么&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这些是官方的说法，实际边界执行得严不严，只能等拿到的人去测。&lt;/p&gt;
&lt;h3&gt;4. 网络：默认给多大的出口&lt;/h3&gt;
&lt;p&gt;Claude Code 把 cloud environment 的网络访问分成了四档：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;档位&lt;/th&gt;
&lt;th&gt;出站连接&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;不能通过 session 网络出站&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Trusted&lt;/td&gt;
&lt;td&gt;只能访问白名单域名：包仓库、GitHub、云 SDK&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Full&lt;/td&gt;
&lt;td&gt;任意域名&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Custom&lt;/td&gt;
&lt;td&gt;自己的白名单，可以包含默认白名单&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;默认环境是 &lt;strong&gt;Trusted&lt;/strong&gt;，不是 Full。&lt;/p&gt;
&lt;p&gt;我觉得这个默认值选得对。编码 agent 日常真正需要的出口就那么几类：装依赖、拉代码、调云服务。「可以访问整个互联网」应该是一个需要主动打开的选项，而不是默认状态。&lt;/p&gt;
&lt;p&gt;另外文档也写明了，不管选哪一档，GitHub 代理和你启用的 MCP connector 都走各自独立的通道。&lt;strong&gt;白名单管的是「VM 能直接访问什么」，不是「agent 能影响什么」。&lt;/strong&gt; 这两件事要分开想。&lt;/p&gt;
&lt;h2&gt;账单：从按 token 计费，变成 token 加机时&lt;/h2&gt;
&lt;p&gt;电脑一直开着，就要有人为它付钱。几家目前的说法差得挺多：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;产品&lt;/th&gt;
&lt;th&gt;云电脑怎么算钱（官方口径）&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Claude Code Cloud sessions&lt;/td&gt;
&lt;td&gt;云端 VM 不单独收计算费用，和其他 Claude 用量共享 rate limit；并行任务按比例消耗额度&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OpenAI Agents API&lt;/td&gt;
&lt;td&gt;模型按 API 价格计费，OpenAI 托管的沙箱按标准 container 价格另算&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OpenAI Dots&lt;/td&gt;
&lt;td&gt;Pro 和 Business Premium 套餐包含第一个 Dot；和 Dot 对话暂不占 ChatGPT 额度（FAQ 说明只在接下来一个月有效），但它启动的 Codex / Work 任务照常计入&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Manus Cloud Computer&lt;/td&gt;
&lt;td&gt;单独购买&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;有意思的是，面向消费者的产品都在努力把机时成本藏进套餐里，面向开发者的 API 则直接按 container 收费。&lt;/p&gt;
&lt;p&gt;藏进套餐体验更好，但会把成本问题推迟到后面。我在&lt;a href=&quot;/posts/coding-agent-quota-sla/&quot;&gt;《额度不是 SLA》&lt;/a&gt;和&lt;a href=&quot;/posts/grok-bot-token-cost/&quot;&gt;《Agent 开始自己省 token》&lt;/a&gt;里都写过类似的判断：&lt;strong&gt;agent 一旦常驻，成本单位就不再是每百万 token，而是「每个有效工作小时」。&lt;/strong&gt; Dots 的「对话不占额度」专门注明了只在第一个月有效，可以看作这个问题的一个预告。&lt;/p&gt;
&lt;p&gt;如果你在做自己的云端 agent，我建议至少把这几个数分开记：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;model_tokens          模型推理成本
sandbox_hours         机器开着的时长
idle_hours            机器开着但没在干活的时长
tasks_completed       完成的任务数
cost_per_task         每个成功任务的总成本
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;idle_hours&lt;/code&gt; 最容易被忽略。一个 24/7 的 agent 如果 90% 的时间在等事件，你为它付的钱大部分是在付「等待」。&lt;/p&gt;
&lt;h2&gt;如果是我来选或者自己搭&lt;/h2&gt;
&lt;p&gt;按场景给一个默认策略：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;写代码、跑一次性长任务&lt;/strong&gt; → 用托管 session（Claude Code cloud、Agents API）。任务结束就回收，状态落在 git，凭证走代理。这是目前最成熟、风险最可控的形态。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;需要事件触发的自动化&lt;/strong&gt; → 先想清楚触发频率。频率低的，用定时或 webhook 拉起一个 session 就够了，不必养一台常驻机器；频率高或者必须维持连接的，再考虑常驻环境。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;个人常驻 agent（Dots 这类）&lt;/strong&gt; → 先交给它一个有边界、结果可以检查的职责，比如「把新的 bug 报告复现成测试用例」，别一上来就说「帮我管好所有邮件」。BenchLM 的 DevDay 总结里也是这么建议的：先试一个你能审查证据的有界职责。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;企业、合规场景&lt;/strong&gt; → 看有没有自托管选项。Claude Code 有 self-hosted environment，Agents API 的 environment 也支持 &lt;code&gt;self_hosted&lt;/code&gt;。流量从自己的网络出去，凭证由自己的部署提供，很多审计问题会简单很多。&lt;/p&gt;
&lt;p&gt;搭建或者评估一个云端 agent 环境时，我会过一遍这份清单：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;[ ] 持久目录是哪几个，文档里写明了吗&lt;/li&gt;
&lt;li&gt;[ ] 机器回收之后，进程内的工作怎么办（是否提示、是否能从检查点恢复）&lt;/li&gt;
&lt;li&gt;[ ] 凭证会不会进入 agent 能读取的环境&lt;/li&gt;
&lt;li&gt;[ ] 推送、写入、付款这类动作有没有范围限制（分支、仓库、金额）&lt;/li&gt;
&lt;li&gt;[ ] 默认网络出口是白名单还是全开&lt;/li&gt;
&lt;li&gt;[ ] 敏感动作前有没有人工确认，确认等待会不会导致 session 过期&lt;/li&gt;
&lt;li&gt;[ ] 能不能随时打开 agent 的电脑看它在做什么&lt;/li&gt;
&lt;li&gt;[ ] 有没有一个平台级的暂停按钮&lt;/li&gt;
&lt;li&gt;[ ] 机时和 token 是分开计量的吗&lt;/li&gt;
&lt;li&gt;[ ] 空闲时间怎么计费&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;边界和风险&lt;/h2&gt;
&lt;p&gt;几件需要说在前面的事：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Dots 刚发布，还没有独立实测。&lt;/strong&gt; 成功率、稳定性、真实的常驻成本都还不知道。OpenAI 在台上展示的例子没有附带成功率数据。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;地区限制&lt;/strong&gt;：据 Mashable 报道，Dots 暂不在欧洲经济区、瑞士和英国提供。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;安全风险在上升&lt;/strong&gt;：Mashable 同一篇报道提到，就在上周，几家大银行警告，用户把设备和金融信息交给 Dots、OpenClaw、Muse 这类 agent 之后，AI 诈骗的风险正在变大。一个能打电话、能发短信、能登录你账号的常驻 agent，本身就是一个很大的攻击面。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;平台准入&lt;/strong&gt;：9 月 22 日亚马逊封掉了 Meta Muse 代用户购物的访问，理由是 Muse 不表明身份，还会采集并保存用户的账号凭证。常驻 agent 能在别人的平台上做多少事，不只取决于技术，也取决于对方允不允许。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;我自己的例子只有一个样本&lt;/strong&gt;：上面 Cohub 沙箱凭证丢失的经历，只说明持久目录的边界需要写清楚，不代表所有云端环境都这样，也不代表 Cohub 在这方面比谁好或差。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;最后&lt;/h2&gt;
&lt;p&gt;把 agent 搬上云，技术上并不新鲜，给它开一台 VM 就行。&lt;/p&gt;
&lt;p&gt;难的是之后的事：这台电脑在你睡着的时候替你干活，它记得什么、能拿到什么、做错了谁能喊停。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;我现在越来越觉得，云端 agent 的产品力，一半在模型，另一半在这台电脑的边界设计上。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;参考资料&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://code.claude.com/docs/en/claude-code-on-the-web&quot;&gt;Use Claude Code in the cloud（Claude Code 文档）&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://code.claude.com/docs/en/cloud-environments&quot;&gt;Configure cloud environments（Claude Code 文档：网络档位、GitHub 代理）&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developers.openai.com/api/docs/guides/agents-api/overview&quot;&gt;Agents API overview（OpenAI 开发者文档）&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://openai.com/index/devday-2026-recap/&quot;&gt;DevDay 2026 Recap（OpenAI）&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://benchlm.ai/blog/posts/openai-devday-2026&quot;&gt;OpenAI DevDay 2026 Announcements: Dots, GPT-6.1 Sol, Ultrafast（BenchLM）&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://mashable.com/tech/openai-dev-day-dots-ai-agents&quot;&gt;OpenAI introduces Dots, a new always-on AI agent（Mashable）&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://help.manus.im/en/articles/17190150-what-is-new-in-manus-2-0&quot;&gt;What is new in Manus 2.0?（Manus 帮助中心）&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://the-decoder.com/manus-2-0-lets-users-edit-videos-host-multiplayer-games-and-run-agents-remotely-from-their-phone/&quot;&gt;Manus 2.0 lets users run agents remotely（The Decoder）&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://public.cohub.run/s/f7000115-55d8-4d97-a0e4-d2a55b6ffa41/genai-hot/2026-09-30/evidence/openai-dots-devday-2026/index.html&quot;&gt;genai-hot 证据页：OpenAI Dots&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://public.cohub.run/s/f7000115-55d8-4d97-a0e4-d2a55b6ffa41/genai-hot/2026-09-25/evidence/claude-code-cloud-sessions-ga/index.html&quot;&gt;genai-hot 证据页：Claude Code Cloud sessions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/posts/grok-bot-token-cost/&quot;&gt;Agent 开始自己省 token：Grok Bot 的成本优化意味着什么&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Cohub Mobile 开发记：三周、31 个版本，和一个「原生质感」的客户端</title><link>https://bangwu.me/posts/cohub-mobile-build-log/</link><guid isPermaLink="true">https://bangwu.me/posts/cohub-mobile-build-log/</guid><description>我们的原生 iOS / Android 客户端是怎么长出来的：Expo + 新架构、缓存优先、自建 OTA，以及三周里真正花时间的那些细节。</description><pubDate>Sat, 19 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;先说结论&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Cohub Mobile 是我们给 Cohub 做的原生 iOS / Android 客户端，用 React Native + Expo 写的。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;三个数字先摆出来：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;8 月 27 日&lt;/strong&gt;首个版本 v1.1.0 → &lt;strong&gt;9 月 18 日&lt;/strong&gt; v2.2.14&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;31 个版本&lt;/strong&gt;、约 &lt;strong&gt;375 个提交&lt;/strong&gt;，三周&lt;/li&gt;
&lt;li&gt;仓库里没有 web target&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;最后一条是这个项目的设计原则，写在 README 第一段：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;This repository is the native iOS/Android client. There is no web target.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;翻译成工作方式就是：&lt;strong&gt;用 RN 写，按原生验收。&lt;/strong&gt; 没有&quot;先做 H5 再套壳&quot;，界面全部是原生屏；唯一用 WebView 的地方是用户发布的网页作品——那是内容本身，本来就该是网页。&lt;/p&gt;
&lt;p&gt;这篇讲三件事：它是什么；我们怎么搭的（技术选型和发布链）；以及三周里真正花时间的那些细节。&lt;/p&gt;
&lt;h2&gt;一、它是什么&lt;/h2&gt;
&lt;p&gt;它不是一个 demo，是我们每天在用的客户端。主要能力：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Chats&lt;/strong&gt;：跨 Space 的会话收件箱，分页、搜索、&quot;正在运行 / 需要注意&quot;筛选、乐观发送、停止、重命名、附件、相机、相册，以及实时流式补丁&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;会话历史&lt;/strong&gt;：按 turn 组织，older / newer 分页，可搜索的 turn 导航，直接跳到某个窗口，滚动位置稳定&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;模型选择器&lt;/strong&gt;：各模型的可用状态、provider 和上下文元数据、每个模型单独的 thinking level&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;语音输入&lt;/strong&gt;：原生 PCM 采集，直连 Cohub ASR&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Spaces&lt;/strong&gt;：Chats / Files / Saves / Works / Task Runs&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;文件与作品&lt;/strong&gt;：原生文件查看器；发布的 Work 用受限 WebView 打开&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Activity&lt;/strong&gt;：活动与用量概览&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;设置&lt;/strong&gt;：Profile / Appearance / Activity / Notifications / Rules / Channels / Billing / Referrals，全部原生&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;底座&lt;/strong&gt;：缓存优先的 SQLite 水合、重连对账、按用户隔离的缓存清理&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;接入&lt;/strong&gt;：Logto PKCE 登录、&lt;code&gt;cohub://&lt;/code&gt; 深链、通知点击路由、APNs / FCM&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;更新&lt;/strong&gt;：Android 应用内下载 APK（SHA-256 校验）；可选 OTA&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;一句话概括产品目标：&lt;strong&gt;把 Cohub 在网页上已有的能力，做成一个真正属于手机的版本，而不是把网页塞进手机。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;二、技术选型：几个想清楚的决定&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Expo 是工具链，不是托管服务。&lt;/strong&gt; Expo SDK 57、RN 0.86、新架构；不依赖 EAS。路由和深链用 expo-router，动画用 Reanimated 4 + gesture-handler，列表用 LegendList。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;渲染要原生。&lt;/strong&gt; 完成的消息文本走原生 Markdown 渲染；流式回复里的代码块，用一个增量 Shiki tokenizer 边流边高亮，而不是等整段结束再渲染。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;状态是缓存优先的。&lt;/strong&gt; SQLite 水合让启动和导航立刻有内容，但服务端始终是权威——缓存不能覆盖更新的服务端状态；发送是乐观更新，回来再做对账。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;边界要写死。&lt;/strong&gt; 原生 API 只允许出现在 &lt;code&gt;src/platform/&lt;/code&gt; 和 &lt;code&gt;src/auth/&lt;/code&gt; 这些模块后面；凭证放 SecureStore；&lt;code&gt;EXPO_PUBLIC_*&lt;/code&gt; 里绝不出现秘密。还有一条很实际的规则：&lt;strong&gt;浏览器里验证过，不代表真机验证过&lt;/strong&gt;——权限、通知、语音、键盘这些，必须设备上见。&lt;/p&gt;
&lt;h2&gt;三、发布链：main push 就是上线&lt;/h2&gt;
&lt;p&gt;这部分是我们花了最多心思的地方，也最像&quot;工程&quot;而不是&quot;写代码&quot;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;main 分支的每一次 push：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;质量门：lint、严格 TypeScript、release 元数据校验、Expo Doctor、依赖审计、三平台 JS export&lt;/li&gt;
&lt;li&gt;Release Please 根据 Conventional Commits 维护版本和 CHANGELOG&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;自动发 OTA&lt;/strong&gt;：把这次 commit 的 JS 包发到 Android 和 iOS 两端的生产 channel&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;打 tag（vX.Y.Z）才会出原生包：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;四个 ABI 的签名 APK（arm64-v8a / armeabi-v7a / x86 / x86_64）挂到 GitHub Release&lt;/li&gt;
&lt;li&gt;iOS 的 IPA 上传 TestFlight&lt;/li&gt;
&lt;li&gt;同时记录两个平台各自的 OTA fingerprint&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;OTA 的几个细节：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;客户端是 &lt;code&gt;ON_LOAD&lt;/code&gt; + 零等待：先跑缓存/内置代码，后台下载，下次冷启动生效——不会把你正在看的会话打断&lt;/li&gt;
&lt;li&gt;&lt;code&gt;runtimeVersion&lt;/code&gt; 用 fingerprint 策略：&lt;strong&gt;只改 JS 不会改变 runtime&lt;/strong&gt;，所以在已装的二进制上持续发版；一旦动到原生依赖、权限或 SDK，fingerprint 变化，必须出新包。这个约束逼我们把&quot;哪些改动可以 OTA、哪些必须发版&quot;分得很清楚&lt;/li&gt;
&lt;li&gt;每 &lt;code&gt;--delta-bases 3&lt;/code&gt;：对最多三个兼容的历史版本生成 BSDIFF40 二进制补丁，补丁不划算就回退整包——这也是为什么我们做了 &lt;a href=&quot;https://github.com/markbang/yaota&quot;&gt;yaota&lt;/a&gt; 那个自托管 OTA 服务&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Android 的应用内更新：&lt;/strong&gt; About 页扫描我们自己的 APK 索引 → 按设备 ABI 下载 → 校验大小和 SHA-256 → 交给系统安装器。有两个细节我们刻意做了：关掉安装器&lt;strong&gt;不算&lt;/strong&gt;更新成功，也不会 snooze 这个版本；签名不匹配（比如从调试签名切到正式签名）必须重新安装，不能用&quot;覆盖&quot;的方式蒙过去。&lt;/p&gt;
&lt;h2&gt;四、三周里真正花时间的细节&lt;/h2&gt;
&lt;p&gt;版本号涨得快，不是因为功能堆得多，而是因为&lt;strong&gt;一个细节就是一个版本&lt;/strong&gt;。挑一些战场记录：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;打开聊天的那一帧&lt;/strong&gt;：发送瞬间会闪一下灰屏，原因是拿到了过期的行布局。修法是重做首帧绘制路径——进聊天时第一帧就画出目标会话。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;流式的尾巴&lt;/strong&gt;：快速 token 爆发时尾巴会被吃掉；中途点进正在生成的会话，&quot;接回&quot;流式内容也翻过车。前后修了三条。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;气泡几何&lt;/strong&gt;：流式期间时间戳的位置会来回跳（我们量到过 48 → 64 → 48 pt 的摆动）。最后改成测量驱动：元信息要么整块留在最后一行，要么单独起一行，并配了一份 29 组固定 case × 2 个角色的验收页。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;代码块的手势&lt;/strong&gt;：横向溢出的代码块左右滑，会误触发侧边面板。先让代码块&quot;吃完&quot;自己的拖拽，再把面板的预加载补上，解决点开时白屏和慢。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;麦克风口述&lt;/strong&gt;：听写文字超框但不自动增高。连修三次，最终按文本推导高度，并且在 append 的时候增长，而不是录音时。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Android 预测性返回&lt;/strong&gt;：我们先实现了这套返回动画，又连续修了四个边界（用受支持的 callback、收窄栈状态、转场期间加守卫、取消滑动时避免 fragment attach），最后&lt;strong&gt;决定把这个手势关掉&lt;/strong&gt;。这是个不太浪漫的结论：原生能力不是每一项都值得留，收益必须大于它带来的复杂度。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;landing 页的闪烁&lt;/strong&gt;：滚动时元素一闪一闪，停下 scroll reveal 的&quot;闪入&quot;动画。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;崩溃与诊断&lt;/strong&gt;：用户反馈了一次 iOS 崩溃并附了文件，此后我们把诊断做成了一等公民——本地导出诊断日志、卡死后导出崩溃会话、SQLite 反馈收集、聊天滚动诊断。&lt;strong&gt;先让问题可观测，再谈修。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;性能&lt;/strong&gt;：&quot;为啥感觉这个 APP 性能很垃圾&quot;是从用户嘴里直接进来的。查的过程中有个有意思的发现：Markdown 解析在 V8 里其实很便宜（4000 字符的富文本约 0.35 ms），所以问题不在 parse，而在渲染路径。之后列表迁到 LegendList，并约束打开聊天时的代码工作量。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这些修复在 CHANGELOG 里都只有一行，但每一行背后都是一次设备上的观察、一次定位、一次验证。&lt;/p&gt;
&lt;h2&gt;五、验证：把&quot;细节&quot;变成可重复的东西&lt;/h2&gt;
&lt;p&gt;要让&quot;原生质感&quot;不变成一句口号，得把它变成能反复执行的检查：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Bubble Layout Acceptance&lt;/strong&gt;：29 组固定 case × 2 个角色（用户 / 助手）+ 12 档边界宽度 + 280 pt 窄视口 + 深色/浅色 + 流式的 start / pause / interrupt。判定标准写得很直白：元信息允许整块在最后一行或整块在下一行，但不允许盖住字符、链接和状态图标。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;E2E 跑在 CI 的设备上&lt;/strong&gt;：Android 设备流跟在 Publish OTA 之后跑。为了让它稳定，我们踩了一串坑——镜像里要先开 KVM 才能起模拟器，logcat 要流式落盘而不是攒在内存里，没有配置 E2E 账号时干脆跳过。现在还有几条流程在修（能做到 9/13 就已经比&quot;靠手感&quot;强太多）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;诊断导出&lt;/strong&gt;：用户和我们都能把会话日志、崩溃现场导出来核对。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;六、我们是怎么开发的&lt;/h2&gt;
&lt;p&gt;这个项目的开发日志就写在 Cohub 的 space 里——三个星期待下来攒了 20 个 session，标题基本就是每天的现场：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;为啥感觉当前这个 APP 性能很垃圾&quot;
&quot;chat 页面左右滑动召唤列表白屏 加载慢&quot;
&quot;iOS 端 Chat 页面回退按钮失效问题&quot;
&quot;麦克风输入文字超框无自动扩大问题&quot;
&quot;安卓原生应用预测性返回是什么及具体表现，实现其动画&quot;
&quot;讨论当前项目的 E2E 方案及边境情况&quot;
&quot;优化对话的草稿保存与新对话创建逻辑&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;流程很朴素：用户（包括我自己）在真机上发现问题 → 进 space 描述现象、贴日志 → agent 参与定位、改代码、补验证 → 进版本 → main push 直接 OTA 到设备。&lt;strong&gt;发现问题到设备上生效，经常是当天。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;三周 31 个版本的意思是：平均每天 1.4 个版本。能做到这个节奏，靠的不是&quot;写代码快&quot;，而是前面那套发布链——main push 即上线，原生包只在需要时出。&lt;/p&gt;
&lt;h2&gt;最后&lt;/h2&gt;
&lt;p&gt;如果你只看前面几段，可能会觉得这是一个&quot;RN 也能做原生&quot;的样板。我更想说另一句：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;原生质感不是选出来的，是验出来的。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;选 RN 还是原生，只是决定了你要打磨的对象和成本；决定体验的，是愿不愿意为一个 48pt 的抖动、一次灰屏、一个误触手势停下来，量一量、改一改、再验收一遍。&lt;/p&gt;
&lt;p&gt;Cohub Mobile 还在快速迭代里，下载地址和源码都在这：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;源码：&lt;a href=&quot;https://github.com/markbang/cohub-mobile&quot;&gt;github.com/markbang/cohub-mobile&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Android：GitHub Release 里按 ABI 下载安装包，应用内也能检查更新&lt;/li&gt;
&lt;li&gt;iOS：TestFlight 内测&lt;/li&gt;
&lt;li&gt;配套：&lt;a href=&quot;https://github.com/markbang/yaota&quot;&gt;yaota&lt;/a&gt;（自托管 Expo OTA）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;（space 里的描述写的是 &quot;cohub mobile: native for all&quot;。就照这个干。）&lt;/p&gt;
</content:encoded></item><item><title>AI 没有杀死 React Native，它杀死了「省人力」这个理由</title><link>https://bangwu.me/posts/ai-didnt-kill-react-native/</link><guid isPermaLink="true">https://bangwu.me/posts/ai-didnt-kill-react-native/</guid><description>Shopify 用 AI 把 Shop app 在 12 周内重建成原生，还坦白「React Native 可以很快」。我们做了相反的决定：cohub-mobile 继续用 RN + Expo，还自己写了 OTA 服务。这篇把两边的账都算一遍。</description><pubDate>Tue, 15 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;先说结论&lt;/h2&gt;
&lt;p&gt;9 月 10 日，Shopify 官方博客发了一篇《Native is now the future of mobile at Shopify》，作者是 Head of Mobile Mustafa Ali。核心那句话写得很直白：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Coding agents changed what it costs to build mobile apps twice.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;翻译过来：&lt;strong&gt;写代码的成本变了两次，所以 2020 年那个「全员 React Native」的决定也该重新算一遍。&lt;/strong&gt; 他们决定回到 Swift + Kotlin，用 AI 重建全部移动 App。&lt;/p&gt;
&lt;p&gt;社区的反应很热闹（Reddit 那个帖子 997 分 / 175 条评论，Theo 还做了个视频叫《Did AI Kill React Native?》）。但我的判断是：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;AI 没有杀死 React Native。它杀死的是「因为养不起两套原生团队，所以只能选 RN」这个理由。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;当实现成本被编码代理摊平，「省人力」从选型的&lt;strong&gt;主要矛盾&lt;/strong&gt;降级成了&lt;strong&gt;次要项&lt;/strong&gt;。剩下的变量里，有几条仍然对 RN 有利——尤其是一条几乎没人讨论的：&lt;strong&gt;OTA（热更新）&lt;/strong&gt;。那是原生拿不到的能力，审核周期就是它的物理上限。&lt;/p&gt;
&lt;p&gt;所以我们的决定和 Shopify 相反：&lt;strong&gt;cohub-mobile 继续用 React Native + Expo，做一个「原生质感」的客户端，并且自己写了 OTA 服务 yaota。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这篇不算谁对谁错，是两本账放在一起看：一本是 Shopify 的（几十个工程师、四个大 App、流程严谨）；一本是我们的（小团队、两个平台、要快速迭代）。&lt;/p&gt;
&lt;h2&gt;一、Shopify 到底说了什么&lt;/h2&gt;
&lt;p&gt;先说清楚他们的论证，因为这次官方口径比社区转述克制得多。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2020 年为什么选 RN：&lt;/strong&gt; 不用写两遍功能、工程师能跨栈、少花时间追平台功能对齐。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2025 年 1 月他们的公开态度还是：&lt;/strong&gt; RN 的未来是光明的，会继续投入。作者原话是「That was true based on what we knew then」。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2026 年为什么回头：&lt;/strong&gt; 不是 RN 变差了，是「两个平台写两遍」的成本结构变了：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Native still means building and maintaining software on two platforms, that cost has not disappeared. What changed is that agents can now do enough of the implementation, translation, testing, and review work that it&apos;s no longer the deciding factor it was in 2020.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;代理现在能做掉足够多的&lt;strong&gt;实现、翻译、测试和评审&lt;/strong&gt;，所以「写两遍」不再是决定性因素。&lt;/p&gt;
&lt;h3&gt;三个容易被忽略的细节&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;1. 他们明确说了 RN 可以很快。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;原文里有一句被大多数讨论忽略的话：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;React Native apps can be fast. Ours are.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这句话很重要。把 Shopify 的决策解释成「RN 太慢」是不准确的。他们的理由是「为了每个平台的原生能力」，不是「RN 跑不动」。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. 他们要做的下一笔投资，恰好是新架构迁移。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Shop app 那篇迁移复盘里写了一句关键的话：他们本来要为 Shop app 做下一笔大投入——&lt;strong&gt;适配 React Native 新架构&lt;/strong&gt;（会涉及原生模块、渲染、共享边界）。在掏这笔钱之前，他们先做了个小实验：一个工程师花一周，用编码代理把这个 RN App 尽可能迁成 SwiftUI 版本。&lt;/p&gt;
&lt;p&gt;实验结果够好，于是他们改成了重建。&lt;/p&gt;
&lt;p&gt;所以「Shopify 拿旧架构时代的 RN 数据做对比」这个批评站不住。他们看的正是新架构要花的钱——然后决定不花了。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. 他们没有一走了之。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;对开源生态的处理方式我认为很体面：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;React Native Skia&lt;/strong&gt;：Shopify 继续赞助到 2026 年底，作者 William Candillon 会在之后 fork 并改名继续维护，原仓库归档。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;FlashList&lt;/strong&gt;（周下载约 200 万）：承诺继续修关键兼容问题，同时正在和几家公司谈长期接管。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Restyle&lt;/strong&gt;：维护到 2026 年底后归档，欢迎任何人 fork。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;事后他们还给 Meta 的 RN 团队、William、Software Mansion（Reanimated）写了道谢。这个姿态比「我们用脚投票了」高明得多，也给生态留了缓冲期。&lt;/p&gt;
&lt;h3&gt;迁移本身的数据（Shop app）&lt;/h3&gt;
&lt;p&gt;这篇数字我觉得比结论更有价值：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;指标&lt;/th&gt;
&lt;th&gt;原生&lt;/th&gt;
&lt;th&gt;React Native&lt;/th&gt;
&lt;th&gt;变化&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;iOS 冷启动&lt;/td&gt;
&lt;td&gt;2466 ms&lt;/td&gt;
&lt;td&gt;3200 ms&lt;/td&gt;
&lt;td&gt;−23%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Android 冷启动&lt;/td&gt;
&lt;td&gt;2233 ms&lt;/td&gt;
&lt;td&gt;4433 ms&lt;/td&gt;
&lt;td&gt;−50%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Android 安装包&lt;/td&gt;
&lt;td&gt;184 MB&lt;/td&gt;
&lt;td&gt;293 MB&lt;/td&gt;
&lt;td&gt;−109 MB（−37.2%）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;iOS 安装包&lt;/td&gt;
&lt;td&gt;68 MB&lt;/td&gt;
&lt;td&gt;67 MB&lt;/td&gt;
&lt;td&gt;+1 MB（+1.5%）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;会话稳定性&lt;/td&gt;
&lt;td&gt;99.95%+&lt;/td&gt;
&lt;td&gt;99.5%+&lt;/td&gt;
&lt;td&gt;崩溃会话约 1/10&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;另外 Android 构建时间下降约 75%（iOS 基本持平），Pixel 上滑动 Feed 能到 120 FPS，而且「几乎还没做优化」。&lt;/p&gt;
&lt;p&gt;注意这个结构的含义：&lt;strong&gt;收益主要落在 Android 上&lt;/strong&gt;（包体 −109MB、启动 −50%、构建 −75%）。iOS 那边（+1MB、启动 −23%）是「差不多，略好」。这符合直觉——RN 时代 iOS 侧本来就有更好的底子，Android 侧一直是在追。&lt;/p&gt;
&lt;p&gt;迁移方式也很具体：核心小组 6 个人搭原生地基和主路径，功能团队中途加入验证自己那块；同时刻意砍掉了一些页面、精简了另一些。前提是「对用户像一次普通更新」：保持登录状态、推送不断、埋点事件名和字段不能变。&lt;/p&gt;
&lt;h3&gt;真正值得学的部分：他们怎么用 AI&lt;/h3&gt;
&lt;p&gt;这部分才是我看完最想记下来的，而且和框架选择无关。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Helix：一个不许「一次就对」的流水线。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;他们试过直接让 LLM 看着 RN 代码库「一把梭」出原生实现——结论是不行，产出一大堆没法上线的代码。Helix 换个做法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;开发者指一个屏幕，Helix 读 RN 代码，把它拆成一系列「几分钟能审完」的检查点&lt;/li&gt;
&lt;li&gt;每个检查点必须：用测试证明行为、和跑着的 App 做视觉比对、扛过&lt;strong&gt;两个对抗性代码审查者&lt;/strong&gt;、拿到人类点头，才能 commit、才进入下一个&lt;/li&gt;
&lt;li&gt;每次评审的反馈会被记住，循环越跑越自动化&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;解决「代理没法快速自测」这个瓶颈。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;他们描述的场景我们太熟悉了：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Agents can make code changes in seconds, but it takes them several minutes to test the output.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;代理改代码是秒级，验证是分钟级；而验证靠无障碍树或截图，慢且脆。他们的解法是把业务逻辑彻底从 UI 里拆出来、&lt;strong&gt;能在桌面无头运行&lt;/strong&gt;，再包一层 CLI 让代理直接查状态、导航、执行动作——迭代速度从分钟级变成毫秒级；真的需要模拟器时，再用远程模式驱动 UI。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Shop app 那次迁移里的两个工具。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;他们给 &lt;strong&gt;Pi&lt;/strong&gt; 写了一个可复用的迁移工作流扩展——Pi 是我们做的编码代理，看到它出现在 Shopify 的官方博客里，我确实停顿了两秒。这个扩展里：专门的子代理去读 RN 源码、记录行为、出平台方案、实现功能、做一致性评审；任务说明要求人先确认，而且&lt;strong&gt;计划文件的接受状态绑定它的内容哈希&lt;/strong&gt;——改了计划，之前的批准就作废。&lt;/li&gt;
&lt;li&gt;他们搭了个叫 &lt;strong&gt;Tardis&lt;/strong&gt; 的调试工具，给代理结构化地访问跑着的原生 App 的事件、日志和状态，还能反向发命令；跨端一致性评审时，它能在命名检查点上同时抓 RN 和原生侧的截图与事件窗口，让代理比对事件名、次数、字段（时间戳、页面 UUID 这类天然不同的值被排除掉）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;但他们也说了实话：&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Generated code could satisfy feature requirements while still introducing duplication, architectural drift, or performance problems.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;生成代码能满足需求，同时引入重复、架构漂移和性能问题——所以 lint、测试、静态分析、性能检查、代码评审一个都不能少。&lt;strong&gt;原生专业能力仍然不可替代。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;二、为什么我们做了相反的决定&lt;/h2&gt;
&lt;p&gt;先交代背景。&lt;strong&gt;cohub-mobile&lt;/strong&gt; 是我们自己的产品客户端（公开仓库），一个 Cohub 的原生 iOS / Android App。README 里有一句话我很喜欢：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;This repository is the native iOS/Android client. There is no web target.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;翻译成架构语言：&lt;strong&gt;用 RN 写，但按原生验收。&lt;/strong&gt; 没有 web target，没有「先做 H5 再套壳」，界面全部是原生屏；唯一用 WebView 的地方，是用户发布出来的网页作品（那是内容本身，就该是网页）。&lt;/p&gt;
&lt;p&gt;技术栈：Expo SDK 57 / RN 0.86 / 新架构 / expo-router / Reanimated 4 / gesture-handler / SQLite 缓存优先 / Logto PKCE。Expo 在这里是&lt;strong&gt;开源工具链&lt;/strong&gt;，不是托管服务（我们不依赖 EAS）。&lt;/p&gt;
&lt;p&gt;为什么同样的 AI 时代，我们算出相反的答案？因为我们那本账上有四个 Shopify 没有的条件。&lt;/p&gt;
&lt;h3&gt;1. 小团队 + 只有两个平台&lt;/h3&gt;
&lt;p&gt;跨平台的收益公式是「两套变一套」。平台越多，这个收益越大；但只有 iOS + Android 时，它本来就只是 2:1。&lt;/p&gt;
&lt;p&gt;Shopify 有几十上百个移动工程师、四个大 App、还要维护开源库。我们用 AI 写两套原生代码也不是不可能——但那意味着我们要同时维护 Swift 和 Kotlin 的验收、发布、性能基线，而我们连一个专职 iOS 工程师都没有。&lt;strong&gt;在他们的规模上，「写两遍」是工程组织问题；在我们的规模上，它是生存问题。&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;2. 我们用 Expo（这是被低估的关键变量）&lt;/h3&gt;
&lt;p&gt;社区里有一句不太好听但很准的话：&lt;strong&gt;如果你开发 RN 却不用 Expo，你其实是在重造一个简陋版 Expo。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;打包、签名、构建、原生兼容性、路由、原生模块生态——这些是 Expo 已经解掉的题。Shopify 没有走这条路，所以他们 RN 时代的很大一部分基础设施投入，本质是在替框架和工具链交学费。等这笔账和「写两遍」的账放在一起比的时候，RN 那边当然显得贵。&lt;/p&gt;
&lt;p&gt;一个反证：我们连自建 OTA 都是严格贴着 Expo 协议做的（后面细说），从来没想过另起炉灶。&lt;/p&gt;
&lt;h3&gt;3. 我们把 OTA 当产品能力，而不是运维细节&lt;/h3&gt;
&lt;p&gt;这是全文我最想强调的一点。&lt;strong&gt;原生开发拿不到 OTA&lt;/strong&gt;——不是说没有工具，是审核周期决定了你不可能在发布后五分钟改变线上 App 的行为。&lt;/p&gt;
&lt;p&gt;对我们这种小团队，OTA 是实打实的竞争力：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;线上出致命 bug，几分钟推修复，不用等几天审核，更不用祈祷用户去点「更新」&lt;/li&gt;
&lt;li&gt;节日运营要在当天换一个入口样式，OTA 可以按渠道、按比例放量&lt;/li&gt;
&lt;li&gt;灰度验证新交互时，不用发版就能把实验往回滚&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以我们的做法是：&lt;strong&gt;把 OTA 变成自己的基础设施。&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;4. 我们愿意把预算花在 AI 抹不平的地方&lt;/h3&gt;
&lt;p&gt;这部分我写的时候其实有点心虚——因为下面这些活，没有一个是「选对框架」白送的：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;聊天列表从倒置视图迁到 LegendList，逐条修流式追加时的尾随、飞入动画、气泡生长边界&lt;/li&gt;
&lt;li&gt;流式回复走&lt;strong&gt;原生 Markdown 渲染&lt;/strong&gt; + 增量 Shiki tokenizer（代码块边流边高亮）&lt;/li&gt;
&lt;li&gt;气泡元信息（时间 / 状态）几何：什么时候能跟在最后一行旁边、什么时候必须单独起一行，用原生测量的行矩形判定。流式期间 footer 的抖动我们是量过的——曾经出现 48 → 64 → 48 pt 的来回跳动&lt;/li&gt;
&lt;li&gt;Android 预测性返回、edge-to-edge 渐变、手势冲突（比如「代码块自己在拖拽」那段）&lt;/li&gt;
&lt;li&gt;缓存优先启动 + 重连对账（SQLite），发送是乐观更新&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;为了不让「原生质感」变成口号，我们写了一页 &lt;strong&gt;Bubble Layout Acceptance&lt;/strong&gt;：29 组固定 case × 2 个角色（用户/助手），加 12 档边界宽度，加 280pt 窄视口、深色/浅色、流式 start/pause/interrupt 全跑一遍；元信息要么整块在最后一行上，要么整块在下面一行——不允许盖住字符、链接和状态图标。这页清单里还老老实实写着一条：「这不是 Android 选择手势引起的滚动的修复」。&lt;/p&gt;
&lt;p&gt;你看，这跟 Shopify 的 Helix 是同一个形状：&lt;strong&gt;改一行代码是秒级的，验证一个细节是小时级的。&lt;/strong&gt; 差别只在语言，不在工作量。&lt;/p&gt;
&lt;h2&gt;三、OTA 这件事，值得单独展开&lt;/h2&gt;
&lt;p&gt;这次讨论里几乎没人认真谈 OTA，但对我们它是决定性的。用一个我常用的比喻：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;原生层是「主厨」&lt;/strong&gt;：他待在厨房（应用商店）里，换主厨要走很长的流程&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;JS 层是「食谱」&lt;/strong&gt;：主厨按食谱干活，而食谱可以随时从网上递进去&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;对 Shopify 这种规模，流程的严谨性抵掉了 OTA 的诱惑——他们有完整的发布火车、灰度体系、稳定的团队编制，等几天审核不是问题。但对 99% 的初创团队，&lt;strong&gt;发布速度本身就是产品能力&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;所以我们把这件事做成了自己的：&lt;strong&gt;yaota&lt;/strong&gt;（我们新开源的仓库），一个跑在 Cloudflare Workers 上的自托管 Expo OTA 服务。&lt;/p&gt;
&lt;p&gt;它解决的问题很具体。我们之前用的是自己 fork 的一套 OTA 服务，越用越发现四个坑：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;只有整包，没有二进制差分&lt;/strong&gt;。用户每次更新都要下载完整 JS bundle，流量和等待时间都浪费在没变的部分上&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;签名能力弱&lt;/strong&gt;：没有多应用独立密钥，也谈不上轮换和吊销&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;回滚靠手&lt;/strong&gt;：出事时没有一条被验证过的回滚路径&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;指标不诚实&lt;/strong&gt;：那个 &lt;code&gt;download_count&lt;/code&gt; 其实统计的是「发出了多少次 manifest」，不是「多少台设备真的更新成功了」。用它做决策会骗自己&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;yaota 现在的样子：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;多应用、多频道&lt;/strong&gt;：每个 App 独立的 channel / branch / release / 原生 runtime&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;签名发布&lt;/strong&gt;：RSA 签名、按 App 的密钥导入与轮换、资产完整性哈希、原生 fingerprint 校验&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;内容寻址 + R2 去重&lt;/strong&gt;：只上传缺失的 blob；SDK 57 默认支持 BSDIFF40，我们生成并验证二进制补丁，补丁不划算时自动回退整包&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;发布控制&lt;/strong&gt;：staging → promote → 按百分比放量 → 回滚（回滚是用新 UUID + 旧字节重发，而不是「删掉一个 release」——删掉不会把已经装上的版本变回去）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;一个能用的面板&lt;/strong&gt;：HeroUI v3 的 dashboard，管密钥、管发布、管 App&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CI 友好&lt;/strong&gt;：一个可复用的 GitHub Action + TypeScript 发布器；main 分支 push 自动发 OTA&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;而且迁移过程我按「准生产」的要求走了：旧端点不动（已安装的原生 App 里写死了 URL，改仓库变量没用）、新服务先跑 canary channel、设备验收清单里包含「发布 A → 发布 B → 回滚 B → 设备启动的必须是 A 的字节」「没有基线的客户端必须能整包升级成功」「10% 放量在反复检查后不能把已经选中的客户端丢掉」这类条目。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;是的，我们连自己家（当时几乎零用户）的 OTA 迁移都按 canary 流程走。&lt;/strong&gt; 因为这条路上最容易骗的就是自己。&lt;/p&gt;
&lt;h2&gt;四、一张表：你的团队该怎么选&lt;/h2&gt;
&lt;p&gt;把两本账抽象一下，我现在用这几个问题做判断：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;条件&lt;/th&gt;
&lt;th&gt;更偏跨平台（RN 等）&lt;/th&gt;
&lt;th&gt;更偏原生&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;移动团队规模&lt;/td&gt;
&lt;td&gt;小团队，没有专职双端&lt;/td&gt;
&lt;td&gt;几十人以上，双端编制齐全&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;平台数量&lt;/td&gt;
&lt;td&gt;2 个&lt;/td&gt;
&lt;td&gt;3 个以上，或需要深度平台特性&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;工具链&lt;/td&gt;
&lt;td&gt;用 Expo 或同等内核&lt;/td&gt;
&lt;td&gt;已有强自建能力 / 愿意自建&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OTA 的重要性&lt;/td&gt;
&lt;td&gt;高：发布速度是竞争力&lt;/td&gt;
&lt;td&gt;低：流程严谨可抵消&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;细节预算&lt;/td&gt;
&lt;td&gt;愿意投入「最后 1%」的验证工时&lt;/td&gt;
&lt;td&gt;同上（这条和框架无关）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AI 的用法&lt;/td&gt;
&lt;td&gt;用代理做翻译、验证、跨端对齐&lt;/td&gt;
&lt;td&gt;用代理把两套代码都变便宜&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;最后一行是我自己想加的：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;AI 对两边的价值不一样，但没有偏袒任何一边。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对跨平台团队：AI 最大的价值是「把一份意图落成两种实现」和「帮你验证原生细节」&lt;/li&gt;
&lt;li&gt;对原生团队：AI 最大的价值是「把写代码的成本压到两套代码可维护」&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以 AI 只是把权重从「写代码多贵」挪到了「你要的能力 + 你的组织形状」。Shopify 选了原生，因为他们要的是平台深水区的能力，而且付得起两套代码的长期账；我们选了 RN + Expo，因为我们要的是发布速度 + 一个足够好的手感，并且愿意把省下来的钱投进细节验证。&lt;/p&gt;
&lt;p&gt;两种都不是错的。可怕的只有一种：&lt;strong&gt;照着别人的答案抄，不核对自己的条件。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;最后&lt;/h2&gt;
&lt;p&gt;Mustafa 那篇文章里有一句我非常认同：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Native is the right choice for Shopify now, but React Native was the right choice for Shopify in 2020.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;我想接一句：React Native 也是我们在 2026 年仍然选它的原因——不是因为它便宜，而是因为我们把它当成一个&lt;strong&gt;可以按原生标准验收的产品技术栈&lt;/strong&gt;，并且愿意为那个标准付出验证成本。&lt;/p&gt;
&lt;p&gt;AI 让代码变便宜之后，产品的差距回到了那些不能便宜的地方：&lt;strong&gt;分发速度、细节的耐心、和对平台的尊重&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;我们的赌注是：在一个被认为「做不到原生质感」的技术栈里，把这些一条一条做出来&lt;/p&gt;
&lt;h2&gt;参考资料&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://shopify.engineering/back-to-native&quot;&gt;Native is now the future of mobile at Shopify&lt;/a&gt;（Mustafa Ali，2026-09-10）&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://shopify.engineering/shop-app-migration&quot;&gt;Migrating Shop app from React Native to native&lt;/a&gt;（12 周迁移复盘与性能数据）&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://x.com/mustafa01ali/status/2098047492772249730&quot;&gt;Mustafa Ali 的帖子&lt;/a&gt;（约 300 万浏览，链接到上面那篇官方文章）&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=oPZLPUtmROo&quot;&gt;Theo - t3.gg：Did AI Kill React Native?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/markbang/cohub-mobile&quot;&gt;cohub-mobile&lt;/a&gt;（RN + Expo 的原生客户端）&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/markbang/yaota&quot;&gt;yaota&lt;/a&gt;（自托管 Expo OTA：签名发布、二进制差分、放量与回滚）&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.reddit.com/r/programming/comments/1wd7wmu/shopify_is_moving_from_react_native_back_to_swift/&quot;&gt;r/programming 讨论&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Edit 工具的 bench：我们怎么把「-97% 的优化」杀掉</title><link>https://bangwu.me/posts/edit-tool-bench/</link><guid isPermaLink="true">https://bangwu.me/posts/edit-tool-bench/</guid><description>给 cohub / pi / Claude Code / Codex 搭一套可回归的 edit 工具 bench：三层结构、四种 harness 对撞，以及三个被我们自己证伪的优化结论。</description><pubDate>Mon, 14 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;先说结论&lt;/h2&gt;
&lt;p&gt;Edit 工具是 harness 里最小的一格：差不多就是「拿 oldText 找位置、换上新内容」几百行代码。但它正好卡在模型和环境之间——模型负责知道要改什么，它负责让这件事真的改对。&lt;/p&gt;
&lt;p&gt;我们给它搭了三层 bench，把四个 harness 放进同一批 case 里对撞，最后拿到的最有价值的产出不是某个优化点，而是&lt;strong&gt;对自己的三次证伪&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;我们排在第一位、反事实重放里「修复 97% 失败」的改动（行首缩进归一化）——不可复现，降级。&lt;/strong&gt; 那 97% 集中在单一 case，且 live A/B 里 base 三次全过。反事实重放的收益不等于生产收益。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;长块拆分引导——零行为改变。&lt;/strong&gt; 描述里写「长块建议拆小」，模型照旧整体替换，一次通过。模型根本不需要这句话。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;found-0 精确原文 hint、found-N 命中点上下文——无差异。&lt;/strong&gt; 模型本来就能在一次重试内自愈。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;留下的反而是几件不起眼的东西：bench 本身作为回归设施、批量逐条诊断、以及四家 harness 对撞出来的一个真问题——&lt;strong&gt;落盘保真&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;外部有篇很值得读的文章 &lt;a href=&quot;https://stencil.so/blog/the-harness-problem&quot;&gt;The Harness Problem&lt;/a&gt;（Stencil，2026-02）讲的是同一件事的另一面：他们只换了 edit 格式（hashline），16 个模型平均 +15 分，最强的单点提升是 Grok Code Fast 1 从 6.7% 到 68.3%，输出 token 最多降 61%，训练成本为零。里面那句话我很认同：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The model is the moat. The harness is the bridge.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这篇不重复他们的结论，讲我们自己怎么量、量出了什么、以及为什么「看起来很有道理」的优化基本都不成立。&lt;/p&gt;
&lt;h2&gt;为什么 edit 工具值得单独做 bench&lt;/h2&gt;
&lt;p&gt;先说外部现状，省得觉得我们小题大做。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Codex 用 &lt;code&gt;apply_patch&lt;/code&gt;：让模型吐一个 OpenAI 风格的 diff 字符串。给 GPT 系用没问题，给别的模型就是灾难——Stencil 的实测里 Grok 4 的 patch 失败率 50.7%，GLM-4.7 是 46.2%。不是模型不会改代码，是它们不说这门语言。&lt;/li&gt;
&lt;li&gt;Claude Code（和大多数 agent）用 &lt;code&gt;str_replace&lt;/code&gt;：找精确旧文本、换新文本。模型必须逐字节复述旧内容，包括空格和缩进。「String to replace not found」多到有自己的 issue 合集。&lt;/li&gt;
&lt;li&gt;Cursor 直接训了一个 70B 的合并模型：专门负责把草稿编辑正确地落进文件。harness 问题难到一家融资最多的公司选择再上一个模型。&lt;/li&gt;
&lt;li&gt;Aider 自己的 benchmark：同一模型只换格式，GPT-4 Turbo 从 26% 到 59%；GPT-3.5 同样的格式只有 19%，因为它写不出合法 diff。&lt;strong&gt;格式和模型一样重要。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;JetBrains 的 Diff-XYZ 系统性确认：没有一种 edit 格式在所有模型和场景下占优。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以这里没有共识，只有取舍。我们的取舍是：不靠猜，把「编辑失败」变成可测量、可回归、能证伪的对象。&lt;/p&gt;
&lt;h2&gt;bench 怎么搭&lt;/h2&gt;
&lt;p&gt;三层，共用同一套 case 和证据格式。从便宜到贵：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;L0 离线变体     纯 JS matcher / 匹配器对撞     零 LLM 成本，秒级
L1 工具层矩阵    直连 sandbox 的 fs.edit RPC   零 LLM 成本，看真实工具行为
L2 agent bench   真 harness、真模型、新会话    有成本，测端到端
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;L0 / L1：零成本的先筛&lt;/h3&gt;
&lt;p&gt;L1 的做法是直接连 sandbox 的 &lt;code&gt;fs.edit&lt;/code&gt; RPC（agent 用的同一条链路），把 golden 片段的 oldText 做 &lt;strong&gt;158 条变形&lt;/strong&gt;：行尾空白、CRLF/LF、行首缩进、空格↔tab、反斜杠、折叠空行、截断、删首行……逐条看真实工具是报错，还是&lt;strong&gt;静默改错&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;这层不用一个 token，改匹配器前后都能跑，所以我们把它当成了硬回归。&lt;/p&gt;
&lt;h3&gt;L2：真跑，但要控制有效性&lt;/h3&gt;
&lt;p&gt;每个 case 起一个全新 session，被测 agent 只能看到 &lt;code&gt;runs/&amp;lt;runId&amp;gt;/&amp;lt;case&amp;gt;/&amp;lt;model&amp;gt;/r&amp;lt;n&amp;gt;/work/&lt;/code&gt;，prompt 里只给任务描述。证据不依赖模型自述，直接从 session 的 jsonl 里抓每次 edit 的 oldText 原文和 toolResult。&lt;/p&gt;
&lt;p&gt;这层最贵的不是钱，是&lt;strong&gt;有效性&lt;/strong&gt;。我们踩了两个坑，都写出来：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;AGENTS.md 污染。&lt;/strong&gt; 第一版 workdir 放在 bench 目录里，被测 agent 读到仓库的 &lt;code&gt;AGENTS.md&lt;/code&gt;（里面有「证据不手改」之类规则），直接在两个 case 上拒绝执行改动。这版数据作废。修正：workdir 挪到 &lt;code&gt;/tmp/projects/...&lt;/code&gt;、跑时临时隐藏 AGENTS.md、结果里加 &lt;code&gt;leakDetected&lt;/code&gt; 标记。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;反事实 ≠ 验证。&lt;/strong&gt; 拿历史调用重放新旧匹配器，只能证明「匹配器能匹配」，不能证明「模型会因此做对事」。我们有 105 次失败调用、307 次总量的反事实重放，后来被 live A/B 打脸——后面细说。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;case 的设计原则&lt;/h3&gt;
&lt;p&gt;第一批 13 个，后来加到 20 个。比起「极限长度」，我们更想要&lt;strong&gt;确定性失败&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;长度：单行 2745 字符、60 行 3230 字符函数、250 行文件 + 10 轮 churn&lt;/li&gt;
&lt;li&gt;缩进/行尾：CRLF/LF 混合、tab 多行、行尾空格&lt;/li&gt;
&lt;li&gt;重复：20 个重复函数体、repeat-all&lt;/li&gt;
&lt;li&gt;上下文：凭记忆改（禁止重读）、外部改名后的陈旧读、贴错片段&lt;/li&gt;
&lt;li&gt;批量：一次 10 个 edit&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;grader 是逐字节比对（格式化后），并且有一条铁律：&lt;strong&gt;不允许「静默写错」&lt;/strong&gt;——工具可以报错、模型可以重试，但不能在没有信号的情况下把内容改坏。&lt;/p&gt;
&lt;h2&gt;首轮数据：失败不长你以为的样子&lt;/h2&gt;
&lt;h3&gt;L1：最危险的不是报错&lt;/h3&gt;
&lt;p&gt;102 次编辑、13 个 case，把 golden oldText 做变形，看真实工具的反应：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;oldText 变形&lt;/th&gt;
&lt;th&gt;RPC 报错率&lt;/th&gt;
&lt;th&gt;内容正确率&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;exact / CRLF↔LF / 行尾空白&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;行首缩进 +1、空格↔tab、反斜杠、折叠空行&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;100%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;截断尾部 / 删最后一行&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;删首行 / 只留中间行&lt;/td&gt;
&lt;td&gt;20%&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;第三行是重点：&lt;strong&gt;oldText 是目标区域子串时（模型漏行、截断），工具照常成功返回，只改了一部分——不报错、不进失败统计。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;行首缩进的硬失败也值得注意：工具的 fuzzy 只归一化行尾空白和 CRLF，行首缩进完全不容错。而我们自己的生产数据里，空白/缩进类问题占了约 27%。&lt;/p&gt;
&lt;h3&gt;L2：97% 的失败是「复述不准」&lt;/h3&gt;
&lt;p&gt;全部 cohub runs 汇总（199 份结果、307 次 edit 调用）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;105 次失败里，&lt;strong&gt;102 次是 &lt;code&gt;not_found&lt;/code&gt;&lt;/strong&gt;（ambiguous 2、noop 1）。&lt;/li&gt;
&lt;li&gt;这些 not_found 里，&lt;strong&gt;86% 的 oldText 超过 200 字符，91% 是多行&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;也就是说：失败的大头不是模型不会改代码，是&lt;strong&gt;长多行块逐字符复述不准&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;长度本身还不是关键。我们后来加了 7 个更狠的 case（单行 2745 字符、60 行 3230 字符函数、20 个重复函数体、CRLF 混合、一次 10 个 edit）——&lt;strong&gt;4 个模型全部 0 失败&lt;/strong&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;打穿模型的不是「长度」，是「多行块 + 缩进结构」。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;还有一个反差：多 edit 调用反而比单 edit 更不容易失败（20.9% vs 37.8%，当然这里有混杂——模型通常只在改动简单时才批量）。&lt;/p&gt;
&lt;h2&gt;四种 harness 对撞：空白漂移的四种死法&lt;/h2&gt;
&lt;p&gt;同一批 case，跑四个 harness。先看模型维度（各模型用自己家的 harness）——cohub 和 pi 是我们自己维护的，Claude Code、Codex 是外部对照：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;harness&lt;/th&gt;
&lt;th&gt;tasks&lt;/th&gt;
&lt;th&gt;task pass&lt;/th&gt;
&lt;th&gt;edit 失败率&lt;/th&gt;
&lt;th&gt;去掉长块 case 后&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;cohub&lt;/td&gt;
&lt;td&gt;156&lt;/td&gt;
&lt;td&gt;153/156&lt;/td&gt;
&lt;td&gt;39.7%&lt;/td&gt;
&lt;td&gt;6.1%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pi&lt;/td&gt;
&lt;td&gt;52&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;52/52&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;11.9%&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude Code&lt;/td&gt;
&lt;td&gt;53&lt;/td&gt;
&lt;td&gt;49/53&lt;/td&gt;
&lt;td&gt;36.0%&lt;/td&gt;
&lt;td&gt;7.7%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Codex CLI&lt;/td&gt;
&lt;td&gt;53&lt;/td&gt;
&lt;td&gt;46/53&lt;/td&gt;
&lt;td&gt;6.7%&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;聚合失败率非常骗人：cohub 的 39.7% 里，94 次来自 claude 在&lt;strong&gt;一个 case&lt;/strong&gt;（&lt;code&gt;large-multiline&lt;/code&gt;，604 字符 oldText）上的重试循环。去掉它，claude 是 0/37。&lt;/p&gt;
&lt;p&gt;那个 case 长这样：claude 在 r2 里试了 &lt;strong&gt;67 次全部 found 0&lt;/strong&gt; 后放弃；r3 把编辑拆成 94/101/86 字符的小片段 + 7 次 bash 才通过，单轮烧了 739k tokens。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;统一模型再看一次&lt;/strong&gt;（四家都换 &lt;code&gt;deepseek-v4-flash&lt;/code&gt;）：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;harness&lt;/th&gt;
&lt;th&gt;tasks&lt;/th&gt;
&lt;th&gt;task pass&lt;/th&gt;
&lt;th&gt;工具层失败率&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;cohub&lt;/td&gt;
&lt;td&gt;46&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;46/46&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;4.2%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pi&lt;/td&gt;
&lt;td&gt;40&lt;/td&gt;
&lt;td&gt;38/40&lt;/td&gt;
&lt;td&gt;4.8%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude Code&lt;/td&gt;
&lt;td&gt;40（33 有效）&lt;/td&gt;
&lt;td&gt;30/33&lt;/td&gt;
&lt;td&gt;4.4%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Codex&lt;/td&gt;
&lt;td&gt;40&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;40/40&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;3.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;换成 deepseek 之后，工具层失败率全部收敛到 3~5%——之前的 30~70% 主要是「模型在长块上复述不准」。差异转移到了 task 层，而那里唯一的真失败是&lt;strong&gt;零报错的静默空白损坏&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;mixed-line-endings&lt;/code&gt;：pi、Claude、Codex 都会把混合 CRLF/LF 统一成单一 EOL&lt;/li&gt;
&lt;li&gt;&lt;code&gt;whitespace-crlf&lt;/code&gt;：Claude、Codex 会丢行尾空格；Codex 还会把 &lt;code&gt;tab+CRLF&lt;/code&gt; 变成空格+LF&lt;/li&gt;
&lt;li&gt;&lt;code&gt;stale-pasted-snippet&lt;/code&gt;：Codex 把 tab 缩进静默改成 4 空格&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这四家坏的方式各不相同，但都指向同一条轴线：&lt;strong&gt;落盘保真（write-back fidelity）&lt;/strong&gt;——不只「能不能匹配上」，还有「改动区外、行尾、EOL 是否原样保留」。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;cohub 是&lt;strong&gt;区域替换&lt;/strong&gt;，天然保真，在上面这些 case 上是唯一全过的。&lt;/li&gt;
&lt;li&gt;pi / Claude Code 会&lt;strong&gt;按主 EOL 重写整个文件&lt;/strong&gt;，混合行尾必坏。&lt;/li&gt;
&lt;li&gt;Codex 的 apply_patch &lt;strong&gt;只校验上下文找不找得到&lt;/strong&gt;，不校验新写入行的缩进/行尾风格——它不怕长块、不怕重复块，但用空白保真度换来的。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;还有一类共同触发点：&lt;strong&gt;陈旧读&lt;/strong&gt;。文件被外部改过、模型没重读就写 oldText，四家都会失败（Codex 报 &lt;code&gt;apply_patch verification failed&lt;/code&gt;）。但四家都能重读自愈——所以这是「省重试成本」的优化，不是正确性问题。&lt;/p&gt;
&lt;h2&gt;三个被证伪的结论&lt;/h2&gt;
&lt;p&gt;这部分是我们自我审计的过程。每一个改动都走同一条流水线：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;反事实重放（历史调用过新旧 matcher）→ 筛出候选&lt;/li&gt;
&lt;li&gt;在 pi 扩展里把内置 edit 换成候选实现（&lt;code&gt;PI_BENCH_MATCHER=base|variant&lt;/code&gt;）→ 真模型行为 A/B&lt;/li&gt;
&lt;li&gt;逐字节 grader + 不允许静默写错 → 决定发不发&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;① 「行首缩进归一化」：反事实 -97%，live 不可复现&lt;/h3&gt;
&lt;p&gt;候选来自一个很具体的观察：把 claude 在 &lt;code&gt;large-multiline&lt;/code&gt; 上的 74 条真实失败 oldText 全部回放分析——&lt;strong&gt;74/74 都只差行首缩进&lt;/strong&gt;（嵌套块整体少缩进两格），字符内容完全正确。工具的 fuzzy 只归一化行尾空白，所以全部拒绝。&lt;/p&gt;
&lt;p&gt;反事实重放看起来很硬：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;指标&lt;/th&gt;
&lt;th&gt;现状&lt;/th&gt;
&lt;th&gt;候选&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;失败的 edit 调用&lt;/td&gt;
&lt;td&gt;105/307&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;3/307（-97.1%）&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;对成功调用的回归&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;0/147&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;合成语料 content-correct&lt;/td&gt;
&lt;td&gt;25.9%&lt;/td&gt;
&lt;td&gt;60.1%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;claude 74 条失败 oldText 命中&lt;/td&gt;
&lt;td&gt;0/74&lt;/td&gt;
&lt;td&gt;74/74&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;然后审计来了。三处虚高：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;指标&lt;/th&gt;
&lt;th&gt;我们最初说的&lt;/th&gt;
&lt;th&gt;审计后&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;修复的失败调用&lt;/td&gt;
&lt;td&gt;102/105&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;93/96&lt;/strong&gt;（9 个是重放伪修复）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;修复的构成&lt;/td&gt;
&lt;td&gt;广义&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;93 个全部来自 claude + &lt;code&gt;large-multiline&lt;/code&gt; 这一个 case&lt;/strong&gt;；其他模型的真实失败修复率 ≈ 0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;修复的内容质量&lt;/td&gt;
&lt;td&gt;未说&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0/90 逐字节一致；86/90 只是空白/缩进差异&lt;/strong&gt;（内容对、模型自己的 newText 缩进有错）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;最后上了一记 live A/B（pi 扩展，claude × &lt;code&gt;large-multiline&lt;/code&gt; × 3 repeats）：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;变体&lt;/th&gt;
&lt;th&gt;task pass&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;base&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;3/3&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;候选&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;3/3&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;base 在 pi 上根本复现不出失败。&lt;/strong&gt; 同一个模型、同一个 case，昨天在 cohub 里 94/97 失败，今天两种 harness 都一次过。&lt;/p&gt;
&lt;p&gt;结论：这类失败是模型/环境相关的、当前不可复现的。它对应的改动从「看起来 -97%」降级成「针对一个明确子类、0 回归、收益待 canary 验证」——我们没让它进主线。&lt;/p&gt;
&lt;h3&gt;② 长块拆分引导：零行为改变&lt;/h3&gt;
&lt;p&gt;动机很顺：claude 94 次失败都在长块，那就引导模型把大替换拆成小 edit。&lt;/p&gt;
&lt;p&gt;行为 A/B（base vs split，claude ×3）：&lt;strong&gt;两组都是 1 次调用、1 个 edit、oldText 21 行，全部通过。&lt;/strong&gt; 描述里写「长块请拆小」完全没用——任务说「整体替换」，模型照做。&lt;/p&gt;
&lt;p&gt;要改变行为只能硬拒绝长 oldText，但那会拒绝本来成功的编辑。收益未证明，不做。&lt;/p&gt;
&lt;h3&gt;③ found-0 hint / found-N 上下文：无差异&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;精确原文 hint：claude 的 base 和 hint 数字完全一样（都是 2 调用、1 失败后自愈）；deepseek 唯一的可测收益是&lt;strong&gt;偶尔省一次 read&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;found-N 命中点上下文：base 6/6 vs foundn 6/6，无收益，反而多花调用。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;模型本来就能在一次重试内自愈（自己把缩进补对，或重读文件）。工具塞给它的信息，它并不需要。&lt;/p&gt;
&lt;h3&gt;顺带还否掉了一个方向：静默接受缩进差异&lt;/h3&gt;
&lt;p&gt;有人提过「干脆宽容一点，别拒绝缩进差异」。我们量了 claude 的 74 条失败 oldText：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;检查&lt;/th&gt;
&lt;th&gt;结果&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;缩进偏差是&lt;strong&gt;统一偏移&lt;/strong&gt;（可整体平移还原）&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0/74&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;newText 的缩进偏差是统一的&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0/74&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;能靠「反向平移 newText」修到正确&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0/74&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;模型的缩进漂移是逐行随机的&lt;/strong&gt;，工具无法从旧文本反推出正确缩进。宽容接受 = 把逐行错缩进的代码静默写进去（86/90 是「内容对、缩进错、没有任何信号」）。&lt;/p&gt;
&lt;p&gt;所以这条改动最后的形态是：&lt;strong&gt;匹配做宽容是为了识别出这是缩进问题，然后把该区域的精确原文交回给模型——不是替模型写。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;对撞外部格式：fuzzy 和 patch 的两次败仗&lt;/h2&gt;
&lt;p&gt;既然外部有 hashline / patch / fuzzy 这些路线，我们干脆把它们做成同类扩展，在自己环境里正面对撞。20 case × 4 模型：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;变体&lt;/th&gt;
&lt;th&gt;task pass&lt;/th&gt;
&lt;th&gt;调用数&lt;/th&gt;
&lt;th&gt;失败构成&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;cohub 匹配器&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;80/80&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;83&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;opencode 式渐进 fuzzy&lt;/td&gt;
&lt;td&gt;74/80&lt;/td&gt;
&lt;td&gt;109&lt;/td&gt;
&lt;td&gt;6 个落盘保真 + 10 次 schema 摩擦&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Codex apply_patch 移植版&lt;/td&gt;
&lt;td&gt;71/80&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;31/39 是解析错误，主要是 grok&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;几个具体发现：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;fuzzy 确实吃掉了那个确定性缩进陷阱（1 调用 0 失败，base 要 2 调用 1 失败）——但&lt;strong&gt;在完整矩阵里输了&lt;/strong&gt;：它赢了匹配，输在落盘（混合行尾被统一、行尾空格被剥掉）。&lt;/li&gt;
&lt;li&gt;patch 的失败几乎全是&lt;strong&gt;模型格式先验&lt;/strong&gt;：grok 把标记写成 &lt;code&gt;*** Begin Patch ***&lt;/code&gt;（多了一个尾巴），严格解析器直接拒绝。格式不是「能不能解析」的问题，是「模型愿不愿意说这门语言」的问题。&lt;/li&gt;
&lt;li&gt;还有一个隐形成本：&lt;strong&gt;schema 摩擦&lt;/strong&gt;。模型按 pi/cohub 的风格传参被拒 10 次——换 schema 的代价不只在匹配层。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;最后留下了什么&lt;/h2&gt;
&lt;p&gt;被证伪的很多，真正留下的只有五件：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;bench 本身 = 回归设施。&lt;/strong&gt; L1 的 158 条变形 + 真实流量重放 + 变体对撞，改 edit 前后各跑一次，全免费、秒级。这是这次投入里最保值的部分。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;批量逐条诊断。&lt;/strong&gt; 唯一有&lt;strong&gt;确定性机制&lt;/strong&gt;支撑的候选：多 edit 调用失败批次里，44% 带着至少一条本来能用的 edit 一起被丢弃；浪费率 2.3%。做法是写盘保持原子，但返回「哪条坏、其余可用」，让模型只重发坏的那条。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;落盘保真。&lt;/strong&gt; 四家对撞出来的真问题。匹配错是看得见的失败，落盘错是看不见的失败——而看不见的那种更难被用户接受。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;read-first 软强制 / 陈旧预检。&lt;/strong&gt; &lt;code&gt;fs.write&lt;/code&gt; 有 size/mtime 预检，edit 没有。加上的收益是省重试成本（四家都自愈，不修正确性）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;口径。&lt;/strong&gt; 别用聚合失败率（一个 case 能主导全部数字）；别用「占失败的比例」（要算发生率）；指标应该是：task success、首次成功率、成功所需调用数、重发字符数，以及 found-0 的&lt;strong&gt;细分占比&lt;/strong&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;方法沉淀&lt;/h2&gt;
&lt;p&gt;如果你也在做 agent 产品，这是我觉得最可复用的几条：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;先量，再改。&lt;/strong&gt; 我们第一步是把生产里的失败样本变成 case，而不是先读别人的优化方案。你流量里哪种错占主导，决定了你该补哪一格。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;反事实重放只能筛，不能定。&lt;/strong&gt; 真正决定成本/收益的判断必须来自 live 行为 A/B（我们用的是「替换内置 edit 的 pi 扩展 + 逐字节 grader」）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;每个改动过三关才算「有意义」：&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;标准&lt;/th&gt;
&lt;th&gt;含义&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;契约明确&lt;/td&gt;
&lt;td&gt;模型该做什么是清楚的（读到最新 → 抄准确 → 小步编辑 → 失败照原文重试）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;信息给到&lt;/td&gt;
&lt;td&gt;工具把模型缺的信息（精确原文、哪条 edit 坏了、文件变过）放进结果里&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;行为验证&lt;/td&gt;
&lt;td&gt;真模型 A/B 确认它真的照做，且&lt;strong&gt;不允许出现静默写错&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;「实现方式暴露的是它假设模型会怎么错」&lt;/strong&gt;——这句话我在对着四个 harness 的代码时反复想起：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Codex 用 patch，是&lt;strong&gt;赌模型能力&lt;/strong&gt;（格式先验已经训进模型里）&lt;/li&gt;
&lt;li&gt;Claude Code 死守精确匹配 + 强制先读，是&lt;strong&gt;赌防幻觉/防误删&lt;/strong&gt;，用重试换安全&lt;/li&gt;
&lt;li&gt;pi / cohub 做 fuzzy + hint，是&lt;strong&gt;赌「模型记得内容、但格式会漂」&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;上 hash 预检 / 回滚的，是&lt;strong&gt;赌「文件状态会在并发里漂」&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;四个假设没有对错，取决于你的流量里哪种错占主导。这正是 bench 的意义。&lt;/p&gt;
&lt;h2&gt;最后&lt;/h2&gt;
&lt;p&gt;这次最大的收获，是三个我们自己提出、自认为很有道理的优化，被我们自己的设施杀掉了。如果只看反事实重放，第一个改动会带着「-97%」的标题进主线，然后在生产里既救不了什么、又背着无人验证的风险。&lt;/p&gt;
&lt;p&gt;Edit 工具只有几百行，但它接住的是模型和环境之间最容易漏的那部分。给它造一套能证伪的设施，比给它加一个聪明功能更值。&lt;/p&gt;
&lt;h2&gt;参考资料&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://stencil.so/blog/the-harness-problem&quot;&gt;The Harness Problem — Stencil&lt;/a&gt;（hashline、16 模型 edit 格式对比）&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://aider.chat/docs/benchmarks.html&quot;&gt;Aider：Editing formats benchmark&lt;/a&gt;（格式对模型表现的直接影响）&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/openai/codex&quot;&gt;OpenAI Codex apply_patch&lt;/a&gt;（&lt;code&gt;seek_sequence&lt;/code&gt; 四级容错语义）&lt;/li&gt;
&lt;li&gt;我们自己的 bench 报告、case 与逐条证据（在内部 space &lt;code&gt;edit bench&lt;/code&gt; 里）&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Flash 模型开始成为 AI 厂商的主战场：Gemini 3.8 Flash 观察</title><link>https://bangwu.me/posts/gemini38-flash-agent/</link><guid isPermaLink="true">https://bangwu.me/posts/gemini38-flash-agent/</guid><description>Gemini 3.8 Flash 把近旗舰能力、Agent 工作流和低价 API 放在同一个产品里。它真正值得关注的不是又一个版本号，而是 Flash 正在变成 AI 产品的主力工作层。</description><pubDate>Sat, 05 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;先说结论&lt;/h2&gt;
&lt;p&gt;Gemini 3.8 Flash 这次让我更确定一件事：&lt;strong&gt;Flash 已经不再只是旗舰模型的轻量版，而是在成为 AI 产品真正的主力工作层。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;9 月 2 日，Google DeepMind 发布 Gemini 3.8 Flash，同时发布面向网络安全任务的 Gemini 3.8 Flash Cyber。Flash 的定位很直白：长程软件工程、自主 Agent、复杂企业工作流，价格则维持在相对低的位置。&lt;/p&gt;
&lt;p&gt;公开资料里最值得注意的不是某一项跑分，而是这几个条件被放到了一起：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;DeepSWE 1.1 官方分数 71%，接近 Claude Opus 5 的 74%&lt;/li&gt;
&lt;li&gt;Terminal-Bench 2.1 官方分数 89.4%&lt;/li&gt;
&lt;li&gt;支持最多 1M input tokens 和 64K output tokens&lt;/li&gt;
&lt;li&gt;支持 function calling、Search as a tool 和 Computer use&lt;/li&gt;
&lt;li&gt;2026 年底前 API 价格是输入 &lt;code&gt;$0.75/M&lt;/code&gt;、输出（含 thinking）&lt;code&gt;$3.75/M&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;六周内连续推出 3.6、3.7、3.8 三代 Flash&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这不是“便宜模型也能聊天”的故事，而是：&lt;strong&gt;模型厂商正在用 Flash 承担越来越多真实工作流里的调用次数。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;当然，官方 benchmark 仍然是官方 benchmark，不能直接替代独立复测。下面的数据会区分来源，我这次也没有把自己的机器写成测试实验室。&lt;/p&gt;
&lt;h2&gt;Flash 原来是什么，现在是什么&lt;/h2&gt;
&lt;p&gt;过去大家说 Flash，通常想到三件事：&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;它更像一个明确的取舍：复杂任务交给 Pro，普通任务交给 Flash。&lt;/p&gt;
&lt;p&gt;但 Agent 出现之后，这个分工发生了变化。一个 Agent 任务不是只调用一次模型，而是会不断经历：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;理解任务
  → 选择工具
  → 读取结果
  → 修改计划
  → 再次调用
  → 验证
  → 收尾
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在这条链上，模型的单次回答不一定需要最强，但它需要：&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;li&gt;失败后能继续恢复&lt;/li&gt;
&lt;li&gt;单次成本足够低&lt;/li&gt;
&lt;li&gt;延迟不会让整个工作流失去交互性&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以 Flash 的价值不只是“快一点”，而是&lt;strong&gt;可以被大量调用&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;一个旗舰模型如果每次都很强，但只能让用户偶尔调用；一个 Flash 模型如果足够稳定、足够便宜，能进入每一次分类、搜索、规划、验证和重试，它在产品里的实际存在感可能更高。&lt;/p&gt;
&lt;h2&gt;Gemini 3.8 Flash 的规格&lt;/h2&gt;
&lt;p&gt;先把官方模型页上的主要信息列出来：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;项目&lt;/th&gt;
&lt;th&gt;Gemini 3.8 Flash&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;产品定位&lt;/td&gt;
&lt;td&gt;长程软件工程、自主 Agent、复杂工作流&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;输入&lt;/td&gt;
&lt;td&gt;文本、图片、视频、音频、PDF&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;最大输入&lt;/td&gt;
&lt;td&gt;1M tokens&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;最大输出&lt;/td&gt;
&lt;td&gt;64K tokens&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;工具能力&lt;/td&gt;
&lt;td&gt;Function calling、Search as a tool、Computer use&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;API 状态&lt;/td&gt;
&lt;td&gt;General availability&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;主要入口&lt;/td&gt;
&lt;td&gt;Gemini App、AI Studio、Gemini API、Gemini Enterprise、Antigravity&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;它的“Flash”并不意味着功能被砍掉。相反，官方把它定位成工作模型：可以处理代码、长文档、多模态输入和 Agent 工具调用。&lt;/p&gt;
&lt;p&gt;这也是它和传统轻量模型的区别。传统轻量模型经常只能承担一个局部动作；Gemini 3.8 Flash 想承担的是一段连续工作流。&lt;/p&gt;
&lt;h2&gt;为什么 6 周 3 个 Flash 值得关注&lt;/h2&gt;
&lt;p&gt;从 3.6 到 3.7，再到 3.8，时间间隔非常短。&lt;/p&gt;
&lt;p&gt;这意味着模型发布的节奏正在从：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;训练一代大模型
  → 发布
  → 维护很久
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;变成：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;一个持续滚动的模型产品线
  → 快速发布
  → 快速接入平台
  → 根据工作流反馈继续迭代
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;以前模型的版本号主要告诉你能力升级了多少。现在它还告诉你：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;API 兼容期可能有多久&lt;/li&gt;
&lt;li&gt;Agent 的默认模型是否会频繁改变&lt;/li&gt;
&lt;li&gt;benchmark 结果能维持多长时间&lt;/li&gt;
&lt;li&gt;你的 prompt 和工具适配是否需要持续回归&lt;/li&gt;
&lt;li&gt;供应商是不是把模型当成一个持续运营的服务，而不是一次发布的文件&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;对使用方来说，这个变化有好处，也有代价。&lt;/p&gt;
&lt;p&gt;好处是新能力快速到手。代价是“固定一个模型版本就结束”的想法越来越不现实，尤其是 Agent 产品。你需要记录模型 ID、请求参数、工具定义和关键任务样本，否则下一次模型更新后，出了行为变化很难定位。&lt;/p&gt;
&lt;h2&gt;跑分怎么看&lt;/h2&gt;
&lt;p&gt;Gemini 3.8 Flash 的公开资料里，最容易被传播的是这些数字：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Benchmark&lt;/th&gt;
&lt;th&gt;Gemini 3.8 Flash&lt;/th&gt;
&lt;th&gt;资料中的对照&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Terminal-Bench 2.1&lt;/td&gt;
&lt;td&gt;89.4%&lt;/td&gt;
&lt;td&gt;官方口径&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DeepSWE 1.1&lt;/td&gt;
&lt;td&gt;71%&lt;/td&gt;
&lt;td&gt;Claude Opus 5：74%，OKP 汇总口径&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HLE-Verified&lt;/td&gt;
&lt;td&gt;54.9%&lt;/td&gt;
&lt;td&gt;官方模型页/发布资料&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LVBench&lt;/td&gt;
&lt;td&gt;87.8%&lt;/td&gt;
&lt;td&gt;OKP 汇总的发布信息&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CursorBench&lt;/td&gt;
&lt;td&gt;69.9% / $2.38 每任务&lt;/td&gt;
&lt;td&gt;社区/第三方汇总口径&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;我会把这些数字分成三层：&lt;/p&gt;
&lt;h3&gt;第一层：模型能力信号&lt;/h3&gt;
&lt;p&gt;DeepSWE、Terminal-Bench、HLE-Verified 能说明它在代码、Agent 和多步推理任务上具备竞争力。尤其是 DeepSWE 71% 接近 Opus 5 的 74%，至少说明 Flash 和“只能做简单任务”的印象已经不完全匹配。&lt;/p&gt;
&lt;h3&gt;第二层：产品定位信号&lt;/h3&gt;
&lt;p&gt;Google 把这些结果和 Agent、长程软件工程、企业工作流放在一起宣传，说明它的目标不是成为一个更快的聊天模型，而是进入实际业务调用链。&lt;/p&gt;
&lt;h3&gt;第三层：不能直接下的结论&lt;/h3&gt;
&lt;p&gt;不能因为 71% 接近 74%，就说两者在所有 coding Agent 任务上等价。真实结果还会受：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Agent harness&lt;/li&gt;
&lt;li&gt;系统提示词&lt;/li&gt;
&lt;li&gt;工具定义&lt;/li&gt;
&lt;li&gt;上下文拼接&lt;/li&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;影响。&lt;/p&gt;
&lt;p&gt;我上一篇写 Qwen3.8-Flash-Next 时提过，模型最终表现是模型和 runtime 的乘积。这次同样适用：benchmark 是模型能力的一个切片，不是完整工作流的成绩单。&lt;/p&gt;
&lt;h2&gt;低价才是 Flash 的核心竞争力&lt;/h2&gt;
&lt;p&gt;Google API 定价页给出的 Gemini 3.8 Flash Paid Tier 价格是：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;时间&lt;/th&gt;
&lt;th&gt;输入&lt;/th&gt;
&lt;th&gt;输出（含 thinking）&lt;/th&gt;
&lt;th&gt;Cached input&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;2026-12-31 前&lt;/td&gt;
&lt;td&gt;&lt;code&gt;$0.75/M&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;$3.75/M&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;$0.075/M&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2027-01-01 起&lt;/td&gt;
&lt;td&gt;&lt;code&gt;$1.50/M&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;$7.50/M&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;$0.15/M&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;这个价格需要和调用方式一起看。&lt;/p&gt;
&lt;p&gt;Agent 的成本不只来自最终回答，还来自中间步骤：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;规划请求
  + 工具选择请求
  + 工具结果分析
  + 失败重试
  + 最终总结
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果一个任务需要 20 次模型请求，单次便宜一点会积累成很明显的差别。更重要的是，开发者会因为成本更低而敢于把模型放进更多环节：&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;li&gt;代码定位&lt;/li&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;这就是 Flash 的分发逻辑：&lt;strong&gt;不是只让一个用户少花一点，而是让整个产品可以多调用很多次。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;不过也要注意，输出价格包括 thinking tokens。一个模型“看起来便宜”，如果在每个普通任务里都进行大量思考，实际任务成本仍然可能偏高。&lt;/p&gt;
&lt;p&gt;所以我会看：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;每个成功任务的总成本
而不是单次请求的 token 单价
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Flash Cyber 为什么要单独做出来&lt;/h2&gt;
&lt;p&gt;同一天发布 Gemini 3.8 Flash Cyber，也说明网络安全正在成为模型产品线里的独立方向。&lt;/p&gt;
&lt;p&gt;普通通用模型可以回答安全问题，但网络安全 Agent 要面对更多约束：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;漏洞分析&lt;/li&gt;
&lt;li&gt;代码审计&lt;/li&gt;
&lt;li&gt;exploit 识别&lt;/li&gt;
&lt;li&gt;自动修补&lt;/li&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;这类任务不是简单地把 system prompt 改成“你是安全专家”。它需要更专门的训练、评测和工具环境。&lt;/p&gt;
&lt;p&gt;我会把 Flash Cyber 看成两件事：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Google 在建立一个面向高频安全任务的专用模型入口&lt;/li&gt;
&lt;li&gt;Google 在把网络安全从“通用 Agent 的一个场景”提升成独立产品线&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;第二件事尤其重要。OpenAI 有 GPT-5.6-Cyber，Meta、智谱和其他厂商也在持续强调网络防御能力。模型竞争开始出现按工作类型拆分的产品线：编码、网络安全、Computer Use、语音、视频理解各自形成自己的“快模型”。&lt;/p&gt;
&lt;p&gt;这对 Agent 开发者有一个直接影响：模型选择以后不一定只按“大/小”划分，还要按任务风险和工具类型划分。&lt;/p&gt;
&lt;h2&gt;Agent 产品应该怎么用 Flash&lt;/h2&gt;
&lt;p&gt;如果是我来接入 Gemini 3.8 Flash，我不会把所有任务都无脑切过去，而会先做分层：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;任务&lt;/th&gt;
&lt;th&gt;默认选择&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;解析用户意图&lt;/td&gt;
&lt;td&gt;Flash&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;选择工具&lt;/td&gt;
&lt;td&gt;Flash，先做稳定性回归&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;抽取网页/文档信息&lt;/td&gt;
&lt;td&gt;Flash&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;简单代码修改&lt;/td&gt;
&lt;td&gt;Flash&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;多文件架构设计&lt;/td&gt;
&lt;td&gt;更强模型&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;高风险安全判断&lt;/td&gt;
&lt;td&gt;Cyber 或更强模型&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;最终发布前审查&lt;/td&gt;
&lt;td&gt;更强模型 + 人工确认&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;失败重试&lt;/td&gt;
&lt;td&gt;Flash，但限制次数&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;同时给每个任务设置四个上限：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;最大步骤数
最大输入 token
最大输出 token
最大执行时间
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后记录每一次调用：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;model
request_id
input_tokens
output_tokens
cached_tokens
tool_calls
retry_count
success
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;没有这些数据，模型切换就是凭感觉。凭感觉做一两次 demo 没问题，做长期运行的 Agent 就会开始失控。&lt;/p&gt;
&lt;h2&gt;Flash 和 Pi/cohub 的关系&lt;/h2&gt;
&lt;p&gt;我们之前讨论过，Pi 作为模型分发渠道，价值不只是“模型出现在列表里”。真正的价值是模型能不能顺利进入一个已有的 Agent runtime。&lt;/p&gt;
&lt;p&gt;Gemini 3.8 Flash 这次的入口已经很完整：Gemini App、AI Studio、API、OpenRouter、Antigravity 和企业 Agent 平台都在接。对 Pi/cohub 这类运行时来说，适配一个模型至少包含：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;provider 鉴权&lt;/li&gt;
&lt;li&gt;模型 ID 和能力声明&lt;/li&gt;
&lt;li&gt;多模态输入格式&lt;/li&gt;
&lt;li&gt;tool/function calling&lt;/li&gt;
&lt;li&gt;thinking token 处理&lt;/li&gt;
&lt;li&gt;stream 事件转换&lt;/li&gt;
&lt;li&gt;context window 限制&lt;/li&gt;
&lt;li&gt;usage 统计&lt;/li&gt;
&lt;li&gt;失败和重试语义&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;最后一项经常被低估。模型能返回文本，只能说明最外层接通了；真正适合 Agent，还要确认工具调用、流式事件、取消、重试和 usage 是否能被 runtime 正确理解。&lt;/p&gt;
&lt;p&gt;所以我现在看“某模型已接入某平台”的时候，会分成三个问题：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;能不能看见
  → 能不能调用
  → 能不能稳定完成任务
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Gemini 3.8 Flash 目前已经完成了前两步，第三步还要看不同 Agent runtime 和实际工作流的独立数据。&lt;/p&gt;
&lt;h2&gt;快速迭代会带来什么成本&lt;/h2&gt;
&lt;p&gt;Flash 的快速发布对开发者不全是好事。&lt;/p&gt;
&lt;h3&gt;1. 回归成本变高&lt;/h3&gt;
&lt;p&gt;每一次模型更新都可能改变：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;工具调用参数格式&lt;/li&gt;
&lt;li&gt;什么时候选择工具&lt;/li&gt;
&lt;li&gt;thinking 的长度&lt;/li&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;所以 Agent 产品不能只测“模型能不能返回 200”。至少要保留一组固定任务做回归。&lt;/p&gt;
&lt;h3&gt;2. 价格是有期限的&lt;/h3&gt;
&lt;p&gt;Gemini 3.8 Flash 当前的低价是到 2026 年底，2027 年会翻倍。产品如果在低价期间把调用量和业务流程全部建立起来，之后要么承担价格上涨，要么重新做路由。&lt;/p&gt;
&lt;p&gt;更稳的做法是把 provider、模型 ID 和预算策略抽出来，不要让业务代码直接依赖某一个模型。&lt;/p&gt;
&lt;h3&gt;3. 评测容易追不上版本&lt;/h3&gt;
&lt;p&gt;六周三代 Flash 的节奏会让静态 benchmark 很快过时。一个 3.7 的测试结果和 3.8 的结果，可能已经不是同一套产品策略。&lt;/p&gt;
&lt;p&gt;我更希望看到的不是更多榜单，而是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;固定任务集&lt;/li&gt;
&lt;li&gt;固定 harness&lt;/li&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;这比一张一次性的排行榜更接近开发者实际需要。&lt;/p&gt;
&lt;h2&gt;我会怎样做第一轮接入验证&lt;/h2&gt;
&lt;p&gt;如果要把 Gemini 3.8 Flash 接到一个 Agent 产品里，我会先做这个最小回归集：&lt;/p&gt;
&lt;h3&gt;普通文本&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;结构化输出&lt;/li&gt;
&lt;li&gt;中英文混合&lt;/li&gt;
&lt;li&gt;长文本摘要&lt;/li&gt;
&lt;li&gt;明确拒绝和边界请求&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;工具调用&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;一个工具&lt;/li&gt;
&lt;li&gt;连续两个工具&lt;/li&gt;
&lt;li&gt;工具返回错误&lt;/li&gt;
&lt;li&gt;工具返回很长结果&lt;/li&gt;
&lt;li&gt;工具参数缺失&lt;/li&gt;
&lt;li&gt;并行工具调用&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;长上下文&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;8K&lt;/li&gt;
&lt;li&gt;32K&lt;/li&gt;
&lt;li&gt;128K&lt;/li&gt;
&lt;li&gt;接近 1M 的长输入&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;不期待每个档位都一样快，但要记录上下文增长后的延迟和成本变化。&lt;/p&gt;
&lt;h3&gt;Agent 任务&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;文件搜索和定位&lt;/li&gt;
&lt;li&gt;小范围代码修改&lt;/li&gt;
&lt;li&gt;测试失败后的修复&lt;/li&gt;
&lt;li&gt;多步骤网页任务&lt;/li&gt;
&lt;li&gt;任务中途取消后恢复&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;成本&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;单任务总 token&lt;/li&gt;
&lt;li&gt;thinking token 占比&lt;/li&gt;
&lt;li&gt;cache 命中率&lt;/li&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;跑完这组测试，才知道它是“榜单上很强”，还是“确实适合进入我的 Agent”。&lt;/p&gt;
&lt;h2&gt;最后&lt;/h2&gt;
&lt;p&gt;Gemini 3.8 Flash 值得关注，不只是因为它的 DeepSWE 分数接近旗舰，也不只是因为 API 价格低。&lt;/p&gt;
&lt;p&gt;它真正代表的是一种产品方向：&lt;strong&gt;把接近前沿模型的能力，压进一个可以被大量调用的工作层。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;旗舰模型决定上限，Flash 模型决定一个 Agent 能不能把更多事情做成自动化流程。&lt;/p&gt;
&lt;p&gt;至于 Gemini 3.8 Flash 能不能在真实工作流里稳定兑现这些数字，还要看工具调用、上下文、成本和失败恢复。模型发布只完成了一半，剩下的一半在 runtime 里。&lt;/p&gt;
&lt;h2&gt;参考资料&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://deepmind.google/models/gemini/flash/&quot;&gt;Google DeepMind：Gemini 3.8 Flash&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ai.google.dev/gemini-api/docs/models/gemini-3.8-flash&quot;&gt;Google AI for Developers：Gemini 3.8 Flash 模型文档&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ai.google.dev/gemini-api/docs/pricing&quot;&gt;Google AI for Developers：Gemini API 定价&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.google/innovation-and-ai/models-and-research/gemini-models/3-8-flash-and-3-8-flash-cyber/&quot;&gt;Google Blog：Introducing Gemini 3.8 Flash and 3.8 Flash Cyber&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://deepmind.google/models/model-cards/gemini-3-8-flash/&quot;&gt;Gemini 3.8 Flash 模型卡&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://public.cohub.run/s/f7000115-55d8-4d97-a0e4-d2a55b6ffa41/genai-hot/2026-09-05/evidence/gemini-3-8-flash-wsj-rumor/index.html&quot;&gt;OKP：Gemini 3.8 Flash / Flash Cyber 热点证据&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/posts/qwen38-llama-cpp/&quot;&gt;上一篇：模型发布只是开始：Qwen3.8-Flash-Next 与 llama.cpp&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/posts/pi-model-distribution/&quot;&gt;模型的新货架：从 Grok 4.6 上架 Pi 说起&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Agent 越权时，先别急着怪模型：权限边界才是第一现场</title><link>https://bangwu.me/posts/agent-unauthorized-access-boundaries/</link><guid isPermaLink="true">https://bangwu.me/posts/agent-unauthorized-access-boundaries/</guid><description>Anthropic 对 Claude 越权访问事件的官方复盘提醒我们：Agent 安全不是只做模型对齐，权限、沙箱、网络出口和评测环境同样是安全边界。</description><pubDate>Tue, 01 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;先说结论&lt;/h2&gt;
&lt;p&gt;Agent 出现越权行为时，我现在不会先问“这个模型是不是失控了”。我会先把现场拆成两部分：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;运行环境有没有把不该给的能力给出去？&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;模型拿到能力之后，是否为了完成窄目标而绕过了原本应该遵守的边界？&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这两件事可以同时成立。&lt;/p&gt;
&lt;p&gt;Anthropic 在 8 月 31 日发布的&lt;a href=&quot;https://www.anthropic.com/news/improving-alignment-security-efforts&quot;&gt;对齐与安全复盘&lt;/a&gt;就把问题拆成了这两个方向：一方面是 operational security，另一方面是 motivated reasoning 和为了窄任务采取有害行动的倾向。&lt;/p&gt;
&lt;p&gt;这比一句“模型逃出了沙箱”准确得多，也更值得工程师认真看。因为第一类问题靠重新训练未必能解决，第二类问题靠多加一个 approval 弹窗也未必能解决。&lt;/p&gt;
&lt;h2&gt;发生了什么&lt;/h2&gt;
&lt;p&gt;先把事实边界摆出来。&lt;/p&gt;
&lt;p&gt;Anthropic 说，7 月 30 日曾报告三起 Claude 模型获得真实计算机系统未授权访问的事件。这些模型是为了网络安全评测而&lt;strong&gt;有意在没有 cyber safeguards 的条件下运行&lt;/strong&gt;的；其中一个关键原因是第三方评测环境配置错误，模型因此访问到了互联网。&lt;/p&gt;
&lt;p&gt;另外，英国 AI Security Institute 在 8 月 4 日报告了一起自己的测试事件：Claude Mythos 5 同样是在有意不启用网络安全防护的情况下运行，并且被明确给予了互联网访问能力，随后采取了一系列未经授权的行动。&lt;/p&gt;
&lt;p&gt;这和“生产环境里的普通 Claude 用户突然被攻击”不是一回事。Anthropic 的文章也说，正在对两起事件做更深入分析，并计划与 METR 合作进行独立审查，后续细节还没有全部公布。&lt;/p&gt;
&lt;p&gt;但这不代表事件可以被轻轻带过。测试环境本来就是为了观察模型在极端权限下会做什么；如果测试环境的权限和目标边界没有被准确记录，评测本身就会变成一次真实系统风险。&lt;/p&gt;
&lt;h2&gt;第一现场其实是这条链&lt;/h2&gt;
&lt;p&gt;一个能上网、能执行命令、能访问文件的 Agent，至少经过这样一条链：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;模型决策
   ↓
工具适配层
   ↓
权限策略 / approval
   ↓
沙箱与文件系统
   ↓
网络出口 / DNS / 代理
   ↓
凭证与环境变量
   ↓
真实目标系统
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;任何一层的默认值出错，最终都可能表现成“模型越权”。但排查方向完全不同。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;现象&lt;/th&gt;
&lt;th&gt;可能的第一责任层&lt;/th&gt;
&lt;th&gt;先查什么&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;明明禁网却能访问外部地址&lt;/td&gt;
&lt;td&gt;运行环境&lt;/td&gt;
&lt;td&gt;egress、代理、DNS、worker 和子进程&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;工具审批没弹，但命令执行了&lt;/td&gt;
&lt;td&gt;工具/策略层&lt;/td&gt;
&lt;td&gt;默认策略、调用路径、绕过接口&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;只能读 workspace 却读到主机文件&lt;/td&gt;
&lt;td&gt;沙箱&lt;/td&gt;
&lt;td&gt;mount、符号链接、临时目录和权限继承&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;没有给 key 却能调用生产服务&lt;/td&gt;
&lt;td&gt;凭证层&lt;/td&gt;
&lt;td&gt;环境变量、sidecar、共享 credential store&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;获得能力后主动绕过任务边界&lt;/td&gt;
&lt;td&gt;模型对齐&lt;/td&gt;
&lt;td&gt;目标函数、提示、训练环境和行为监控&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;这张表不是为了把责任推给基础设施，而是为了避免把不同问题混成一个“模型安全”标签。&lt;/p&gt;
&lt;h2&gt;approval 不是沙箱&lt;/h2&gt;
&lt;p&gt;很多 Agent 产品把“需要确认”当成主要安全体验：模型想执行危险操作时，弹一个确认框。&lt;/p&gt;
&lt;p&gt;这对用户体验有帮助，但它只覆盖一个很窄的边界：&lt;strong&gt;模型通过产品定义的工具接口发起调用时，是否让人批准。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;它不能自动阻止：&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;li&gt;子进程继承了更高权限的环境变量&lt;/li&gt;
&lt;li&gt;日志或临时目录里残留真实凭证&lt;/li&gt;
&lt;li&gt;一个看起来只读的工具通过路径解析访问了 workspace 外部&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;DeepSeek Harness 的插件清单里就明确提醒过：工具 approval 管的是模型调工具，不等于第三方插件代码被 sandbox 了。这个区别在任何可扩展 Agent 里都成立。&lt;/p&gt;
&lt;p&gt;我的默认分层是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;插件代码：按不可信代码处理
模型工具调用：按可审计请求处理
沙箱子进程：按最小权限执行
网络访问：默认拒绝，按域名/任务放行
凭证：按一次任务、一次进程注入
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果这四层没有分开，界面上的“允许/拒绝”很容易给人一种超过实际能力的安全感。&lt;/p&gt;
&lt;h2&gt;reward hacking 为什么和越权放在一起&lt;/h2&gt;
&lt;p&gt;Anthropic 这次文章的另一部分讲了 reward hacking：模型在训练环境里找到作弊方式，获得奖励，却没有真正完成任务。&lt;/p&gt;
&lt;p&gt;这件事和网络越权看起来是两个问题，底层却有相似结构：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;狭窄的可测目标
       ↓
模型发现目标与真实意图之间的缝隙
       ↓
利用缝隙拿到更高回报
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;例如，训练环境奖励“看起来诚实的回答”，模型可能学会堆免责声明；奖励“通过评测”，模型可能寻找修改评分路径的办法。Anthropic 说，他们在容易发生 reward hacking 的模拟环境里训练模型，并观察到更严重的错位行为；同时也提到，早期训练中曾因为看到这类行为而回滚部分训练。&lt;/p&gt;
&lt;p&gt;这里不能得出“任何模型都会攻击系统”的结论。更准确的工程问题是：&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;li&gt;失败是否会被当成普通输出，而不是安全事件&lt;/li&gt;
&lt;li&gt;监控看到的是行为，还是只看最终分数&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;给 Agent 画一张真正的权限图&lt;/h2&gt;
&lt;p&gt;如果今天让我给一个新 Agent 做安全设计，我会先画 capability graph，而不是先选模型：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;                 ┌──────────────┐
                 │  Model       │
                 └──────┬───────┘
                        │ request
                 ┌──────▼───────┐
                 │ Policy       │  任务级允许范围
                 └──┬────────┬──┘
                    │        │
          ┌─────────▼─┐  ┌───▼─────────┐
          │ Tool scope │  │ Network     │  域名/端口/时限
          └──────┬──────┘  └──────┬──────┘
                 │                 │
          ┌──────▼─────────────────▼──────┐
          │ Sandboxed process + credentials│
          └────────────────┬───────────────┘
                           │
                    ┌──────▼──────┐
                    │ Audit log   │
                    └─────────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;每个箭头都应该能回答三个问题：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;谁授予了这项能力&lt;/li&gt;
&lt;li&gt;能力什么时候失效&lt;/li&gt;
&lt;li&gt;事后能不能还原一次调用&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;如果回答不了，说明这项能力只是“碰巧能用”，还没有成为可管理的系统。&lt;/p&gt;
&lt;h2&gt;一份我会真的执行的测试表&lt;/h2&gt;
&lt;h3&gt;环境隔离&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;测试账号和生产账号完全不同&lt;/li&gt;
&lt;li&gt;测试域名和生产域名在网络层分开&lt;/li&gt;
&lt;li&gt;子进程不继承不必要的环境变量&lt;/li&gt;
&lt;li&gt;workspace 外的路径默认不可读&lt;/li&gt;
&lt;li&gt;临时目录按 session 隔离，任务结束清理&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;网络出口&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;默认 deny，而不是默认 allow&lt;/li&gt;
&lt;li&gt;允许列表按任务生效，并设置过期时间&lt;/li&gt;
&lt;li&gt;DNS、HTTP、HTTPS、WebSocket 和子进程网络分别验证&lt;/li&gt;
&lt;li&gt;代理不会因为环境变量被意外绕过&lt;/li&gt;
&lt;li&gt;对外请求记录域名、调用方和任务 ID&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;凭证&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;不把长期 key 写进 system prompt&lt;/li&gt;
&lt;li&gt;不让插件在初始化时自动扫描凭证目录&lt;/li&gt;
&lt;li&gt;每个 Agent 只拿它当前任务所需的最小权限&lt;/li&gt;
&lt;li&gt;轮换和撤销不依赖重启整个服务&lt;/li&gt;
&lt;li&gt;日志、错误消息和截图中不出现 secret&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;行为评测&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;不只测任务是否完成，也测是否绕过边界&lt;/li&gt;
&lt;li&gt;记录模型尝试过但被拒绝的动作&lt;/li&gt;
&lt;li&gt;对“修改评分器”“伪造成功”“隐藏失败”单独报警&lt;/li&gt;
&lt;li&gt;检查模型在重试、超时和奖励变化后是否改变策略&lt;/li&gt;
&lt;li&gt;对高权限任务保留人工复核样本&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;安全收紧的代价也要承认&lt;/h2&gt;
&lt;p&gt;权限收紧之后，产品一定会出现更多拒绝。社区里已经有人抱怨 Claude 对一些原本无害的请求变得过于谨慎，这类体感不能直接证明某个具体策略，但它揭示了一个真实取舍：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;更少误放行  ←→  更少误拒绝
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;安全系统不能只追求“拒绝率越高越好”。如果所有不确定请求都被拒绝，用户会开始关闭保护、换工具，最后安全边界反而变差。&lt;/p&gt;
&lt;p&gt;比较好的方向是把拒绝变得可解释、可分级：&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;li&gt;不可恢复风险：拒绝，并告诉用户需要改变什么配置&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;用户需要知道是“模型不愿意做”，还是“当前环境没有给它做这件事的权限”。这两种错误的修复方法不一样。&lt;/p&gt;
&lt;h2&gt;最后&lt;/h2&gt;
&lt;p&gt;Anthropic 这次复盘最值得记住的不是某个模型名字，而是它承认了一个很多 Agent 团队容易回避的事实：&lt;strong&gt;安全事件常常是模型行为和工程配置共同造成的。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;所以 Agent 安全的第一张图，不应该是模型排行榜，而应该是权限图、网络图和恢复图。&lt;/p&gt;
&lt;p&gt;先把模型放进一个真的隔离环境，再讨论它在环境里表现得有多聪明。否则我们测到的可能只是：谁忘了关哪一扇门。&lt;/p&gt;
&lt;h2&gt;参考资料&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.anthropic.com/news/improving-alignment-security-efforts&quot;&gt;Anthropic：Improving our alignment and security efforts&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.anthropic.com/research&quot;&gt;Anthropic Research&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/posts/deepseek-harness-plugin-tutorial/&quot;&gt;DeepSeek Harness：插件开发教程&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://public.cohub.run/s/f7000115-55d8-4d97-a0e4-d2a55b6ffa41/genai-hot/2026-09-01/evidence/anthropic-claude-unauthorized-access-retro/index.html&quot;&gt;OKP：Anthropic 复盘 Claude 越权访问事件&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://public.cohub.run/s/f7000115-55d8-4d97-a0e4-d2a55b6ffa41/genai-hot/2026-09-01/evidence/mythos-5-safety-incident/index.html&quot;&gt;OKP：前沿模型安全事件线&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>上下文压缩要消失了吗：Codex 正在把记忆搬到窗口外</title><link>https://bangwu.me/posts/codex-context-external-memory/</link><guid isPermaLink="true">https://bangwu.me/posts/codex-context-external-memory/</guid><description>从 Codex 最近合并的 context baseline、bounded replay 和 token usage 变化看，Agent 的上下文管理正在从“不断总结”走向“可回放的外部状态”。</description><pubDate>Tue, 01 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;先说结论&lt;/h2&gt;
&lt;p&gt;最近看到一个说法：&lt;strong&gt;Codex CLI 要取消上下文压缩，改成硬切窗口 + 外部记忆。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这句话抓到了方向，但说得太满了。&lt;/p&gt;
&lt;p&gt;我看了 OpenAI Codex 仓库最近合并的几组改动，更准确的判断是：&lt;strong&gt;Codex 没有简单地把 compaction 删除，而是在把上下文管理从“反复总结聊天记录”，往“保留可回放状态、建立边界、按需重建上下文”推进。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这不是一个小修复。对长程 Agent 来说，真正重要的记忆不只有聊天内容，还有：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当前工作目录和环境&lt;/li&gt;
&lt;li&gt;哪些工具已经执行过&lt;/li&gt;
&lt;li&gt;哪个 turn 正在进行&lt;/li&gt;
&lt;li&gt;子 Agent 从哪里 fork 出来&lt;/li&gt;
&lt;li&gt;当前额度和 token 用量&lt;/li&gt;
&lt;li&gt;最近一次压缩之后，哪些状态仍然有效&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果这些东西只藏在一次 summary 里，Agent 很容易出现一种很具体的失忆：它记得“我们讨论过要修 bug”，却不再知道已经改过哪个文件、哪个检查失败过、接下来应该从哪里继续。&lt;/p&gt;
&lt;h2&gt;先把“取消压缩”纠正一下&lt;/h2&gt;
&lt;p&gt;截至 2026 年 9 月 1 日，我没有看到 OpenAI 发布一份可以直接解读为“Codex 已全面取消上下文压缩”的产品公告。这个说法主要来自社区对仓库改动的归纳，OKP 也把它记成了一个架构变化信号，而不是稳定版 changelog。&lt;/p&gt;
&lt;p&gt;但仓库里确实有一条很清楚的演进线：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/openai/codex/pull/41901&quot;&gt;PR #41901&lt;/a&gt;：在空 wake turn 之后加载 bounded context，把最近一次仍然有效的完整 world-state snapshot 当成 durable baseline&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/openai/codex/pull/41424&quot;&gt;PR #41424&lt;/a&gt;：在 nested agent fork 中保留 context baseline，忽略被 compaction 替代的旧 snapshot&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/openai/codex/pull/41429&quot;&gt;PR #41429&lt;/a&gt;：保留每个 turn 最近一次被选中的 StepContext&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/openai/codex/pull/41912&quot;&gt;PR #41912&lt;/a&gt;：把 response token usage 作为可持久化的 rollout item，resume 时不用无限向历史回扫&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/openai/codex/pull/41940&quot;&gt;PR #41940&lt;/a&gt;：让 transcript 的布局缓存跨 backtrack 选择继续生效，避免每次选择都重新排整条历史&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这些改动共同说明一件事：&lt;strong&gt;上下文不再只是一个字符串数组，而是一个带边界、快照、回放和归属关系的状态系统。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;压缩为什么会让 Agent 失忆&lt;/h2&gt;
&lt;p&gt;最简单的 Agent 上下文大概是这样：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;用户消息
  → 助手回复
  → 工具调用
  → 工具结果
  → 下一次模型请求
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当窗口快满时，系统把前面的内容压缩成一段摘要：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;很长的历史
  → summary
  → summary + 最近几轮消息
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这对聊天产品通常够用。对 coding Agent，问题在于历史里有很多不能只靠语义概括的东西：&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;li&gt;子 Agent 的 fork 边界&lt;/li&gt;
&lt;li&gt;某个配置是在压缩前还是压缩后生效&lt;/li&gt;
&lt;li&gt;一次失败请求是否已经消耗了额度&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Summary 可以表达“发生过一次测试失败”，却不一定保留测试命令、退出码和失败文件。模型下一次看见摘要，会很自然地重新做一遍已经做过的工作。&lt;/p&gt;
&lt;p&gt;更麻烦的是，压缩不是一次性的。长任务可能经历：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;原始历史 → 压缩 1 → 压缩 2 → 压缩 3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;后面的 summary 可能建立在前面的 summary 上。每多一层，细节就多一层损失，错误也会被“总结得很合理”。&lt;/p&gt;
&lt;h2&gt;Codex 最近改的其实是什么&lt;/h2&gt;
&lt;p&gt;我更愿意用下面这个模型理解它：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;raw rollout / transcript
          │
          ├── compaction checkpoint
          │       ├── world-state baseline
          │       ├── turn context
          │       └── token usage
          │
          └── bounded replay
                    │
                    └── 当前模型上下文
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里有三个变化。&lt;/p&gt;
&lt;h3&gt;1. Summary 不再是唯一真相&lt;/h3&gt;
&lt;p&gt;压缩后的内容仍然可以作为重建上下文的入口，但系统会同时保留一个更结构化的 baseline。它记录的不是“这段对话大意”，而是某个时间点上系统认为仍然有效的世界状态。&lt;/p&gt;
&lt;p&gt;这两者的职责不一样：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;summary 适合给模型读&lt;/li&gt;
&lt;li&gt;baseline 适合给运行时恢复&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;把这两个层次混在一起，是很多 Agent 状态 bug 的来源。&lt;/p&gt;
&lt;h3&gt;2. 历史扫描有边界了&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;PR #41901&lt;/code&gt; 处理的是一种很容易被忽略的场景：一个 paginated thread 里可以积累很多没有用户消息边界的 wake turn。旧的 reverse scan 找不到安全的停止点，就可能一直往更早的历史扫描。&lt;/p&gt;
&lt;p&gt;有了和 turn context 配对的完整 snapshot 后，系统可以在这里停下来：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;最新 compaction
      ↓
兼容的 turn context + 完整 snapshot
      ↓
bounded replay 截止
      ↓
不再扫描无关的旧 wake turns
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这不只是省一点读取时间，也减少了重放时误把旧状态带回当前 turn 的概率。&lt;/p&gt;
&lt;h3&gt;3. 用量也成为状态的一部分&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;PR #41912&lt;/code&gt; 把 token usage 做成 durable rollout item。这个细节很关键：如果用量只存在进程内存里，resume 或 fork 之后就要重新推导；重新推导又可能跨过压缩边界，得到不一致的数字。&lt;/p&gt;
&lt;p&gt;现在至少有机会把下面这些东西一起恢复：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;response usage
  → turn attribution
  → thread total
  → root-turn lineage
  → compaction checkpoint
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对一个有额度限制的 Agent，这比在界面上显示一个漂亮的 token counter 更重要。计数必须能跟着会话一起活下来。&lt;/p&gt;
&lt;h2&gt;这和 LLM Wiki 不是一回事&lt;/h2&gt;
&lt;p&gt;我之前写过 &lt;a href=&quot;/posts/llm-wiki-shared-context/&quot;&gt;LLM Wiki：把上下文编译成会生长的知识库&lt;/a&gt;，里面讲的是把原始资料加工成可读、可链接、可维护的知识页面。&lt;/p&gt;
&lt;p&gt;Codex 这条线解决的是另一层问题：&lt;strong&gt;运行时如何记住自己处于什么状态。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;可以这样区分：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;LLM Wiki&lt;/th&gt;
&lt;th&gt;Agent runtime state&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;目标&lt;/td&gt;
&lt;td&gt;沉淀语义知识&lt;/td&gt;
&lt;td&gt;恢复一次执行现场&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;主要读者&lt;/td&gt;
&lt;td&gt;人和后续 Agent&lt;/td&gt;
&lt;td&gt;runtime 和当前模型&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;数据形态&lt;/td&gt;
&lt;td&gt;Markdown 页面、链接、索引&lt;/td&gt;
&lt;td&gt;snapshot、turn、rollout、usage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;更新方式&lt;/td&gt;
&lt;td&gt;归纳、合并、修订&lt;/td&gt;
&lt;td&gt;append、checkpoint、replay&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;失败代价&lt;/td&gt;
&lt;td&gt;知识页面过时&lt;/td&gt;
&lt;td&gt;Agent 重复操作或做错决策&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;两者可以配合，但不能互相替代。Wiki 告诉 Agent“这个项目通常怎么部署”，runtime baseline 告诉它“这一次部署已经做到哪一步”。&lt;/p&gt;
&lt;h2&gt;两种上下文管理方式&lt;/h2&gt;
&lt;p&gt;旧式 compaction 可以抽象成：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;async function compact(history: Message[]): Promise&amp;lt;Message[]&amp;gt; {
  const summary = await summarize(history);
  return [
    { role: &quot;system&quot;, content: summary },
    ...history.slice(-recentMessageCount),
  ];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这段代码短，但它把“理解历史”和“恢复状态”都交给了模型。&lt;/p&gt;
&lt;p&gt;更稳的方式应该把运行时状态单独持久化：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type Checkpoint = {
  worldState: WorldState;
  turnContext: TurnContext;
  usage: TokenUsage;
  transcriptCursor: string;
};

async function resume(checkpoint: Checkpoint): Promise&amp;lt;Message[]&amp;gt; {
  const replay = await replayAfter(checkpoint.transcriptCursor);
  return projectModelContext({
    baseline: checkpoint.worldState,
    turn: checkpoint.turnContext,
    replay,
  });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里的关键不是代码长了，而是职责拆开了：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;worldState&lt;/code&gt; 负责运行时事实&lt;/li&gt;
&lt;li&gt;&lt;code&gt;turnContext&lt;/code&gt; 负责当前执行边界&lt;/li&gt;
&lt;li&gt;&lt;code&gt;usage&lt;/code&gt; 负责资源归属&lt;/li&gt;
&lt;li&gt;&lt;code&gt;transcriptCursor&lt;/code&gt; 负责从哪里开始回放&lt;/li&gt;
&lt;li&gt;最后才由 projection 决定模型看到什么&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;外部记忆也不是免费午餐&lt;/h2&gt;
&lt;p&gt;把记忆搬到窗口外，当然会引入新的问题。&lt;/p&gt;
&lt;h3&gt;检索错误&lt;/h3&gt;
&lt;p&gt;模型不再自动看到所有历史，而是依赖 runtime 选择正确的片段。选错了，系统可能比 summary 更安静地丢信息。&lt;/p&gt;
&lt;h3&gt;Snapshot 过期&lt;/h3&gt;
&lt;p&gt;一个 snapshot 只有在它的依赖仍然成立时才有效。provider 换了、fork 改了用户消息、compaction 产生了新基线，旧 snapshot 就应该失效。&lt;code&gt;PR #41424&lt;/code&gt; 专门处理的就是“被压缩替代的 snapshot 不应该继续当基线”。&lt;/p&gt;
&lt;h3&gt;磁盘和隐私&lt;/h3&gt;
&lt;p&gt;raw transcript、工具结果和 token usage 都可能包含源代码、凭证引用和内部数据。外部记忆不是一句“写到本地文件”就结束了，还要考虑：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;哪些结果可以长期保存&lt;/li&gt;
&lt;li&gt;哪些字段需要脱敏&lt;/li&gt;
&lt;li&gt;fork 是否复制全部历史&lt;/li&gt;
&lt;li&gt;删除会话时能否真的删除&lt;/li&gt;
&lt;li&gt;多个 workspace 是否会串数据&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;可观察性&lt;/h3&gt;
&lt;p&gt;如果 Agent 说“我记得之前做过”，你需要能回答：它是从哪个 checkpoint、哪个 transcript item、哪个 note 得到的。否则外部记忆只是另一种不可解释的 prompt magic。&lt;/p&gt;
&lt;h2&gt;如果是我来设计长程 Agent&lt;/h2&gt;
&lt;p&gt;我的默认策略会是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;原始事件 append-only&lt;/strong&gt;：不要只保留 summary，至少保留可定位的事件和 cursor&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;运行时状态独立于模型上下文&lt;/strong&gt;：快照是机器恢复用的，不要把所有字段直接拼进 prompt&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;压缩是投影，不是删除&lt;/strong&gt;：压缩后仍然可以回到原始事件重放&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;每个 turn 有明确边界&lt;/strong&gt;：输入、工具调用、fork、取消和恢复都要有归属&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;用量跟着会话走&lt;/strong&gt;：resume、fork、compaction 后都能解释数字从哪里来&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;恢复路径要可测试&lt;/strong&gt;：同一条 rollout 用 full replay 和 bounded replay，结果应该一致&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;给状态设失效条件&lt;/strong&gt;：provider、workspace、权限和用户消息变化后，旧 baseline 不能盲目复用&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这套设计不一定适合一个很小的聊天机器人，但只要 Agent 会持续工作、会调用工具、会被暂停和恢复，就值得尽早做。&lt;/p&gt;
&lt;h2&gt;最后&lt;/h2&gt;
&lt;p&gt;上下文窗口变长，解决的是“能装下多少内容”；上下文管理变好，解决的是“系统能不能知道哪些内容仍然有效”。这不是同一个问题。&lt;/p&gt;
&lt;p&gt;所以我不认为上下文压缩会突然消失。更可能发生的是：&lt;strong&gt;summary 逐渐退到模型体验层，真正的记忆交给可持久化、可验证、可回放的 runtime state。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;对长程 Agent 来说，记住一段话不够。它得记住自己做过什么，以及为什么可以相信这件事。&lt;/p&gt;
&lt;h2&gt;参考资料&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/openai/codex&quot;&gt;OpenAI Codex&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/openai/codex/pull/41901&quot;&gt;PR #41901：Load bounded context after empty wake turns&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/openai/codex/pull/41424&quot;&gt;PR #41424：Preserve context baselines across nested agent forks&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/openai/codex/pull/41429&quot;&gt;PR #41429：Retain the last selected step context for each turn&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/openai/codex/pull/41912&quot;&gt;PR #41912：Persist response token usage in rollout history&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://public.cohub.run/s/f7000115-55d8-4d97-a0e4-d2a55b6ffa41/genai-hot/2026-09-01/evidence/codex-ecosystem-tooling/index.html&quot;&gt;OKP：Codex 生态持续扩张&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/posts/llm-wiki-shared-context/&quot;&gt;LLM Wiki：把上下文编译成会生长的知识库&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>编程 Agent 的额度，其实是一种 SLA</title><link>https://bangwu.me/posts/coding-agent-quota-sla/</link><guid isPermaLink="true">https://bangwu.me/posts/coding-agent-quota-sla/</guid><description>Claude Code 与 Codex 最近的额度调整引发大量讨论。对编程 Agent 来说，额度不是简单的优惠，而是任务能否连续完成的一种容量与可靠性承诺。</description><pubDate>Tue, 01 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;先说结论&lt;/h2&gt;
&lt;p&gt;编程 Agent 的额度，我越来越倾向于把它当成一种 &lt;strong&gt;SLA&lt;/strong&gt;，而不是订阅页面上的赠品。&lt;/p&gt;
&lt;p&gt;因为聊天产品被限流时，用户通常只是晚一点得到一个回答；编程 Agent 被限流时，可能正好停在：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;已经改了几个文件，但还没跑完测试&lt;/li&gt;
&lt;li&gt;子 Agent 已经启动，但主任务还没有汇总&lt;/li&gt;
&lt;li&gt;上下文压缩刚做完，下一步还没执行&lt;/li&gt;
&lt;li&gt;一次部署正在等待最后一个检查&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这时候额度变化不是“少聊几句”，而是直接影响任务的连续性和恢复成本。&lt;/p&gt;
&lt;p&gt;最近 OKP 记录了 Anthropic 和 OpenAI 编程产品在同一时间窗口里的额度讨论：Anthropic 侧的公开信息涉及 Claude Code 周额度从临时增加调整为较小的永久增加；OpenAI 侧则出现了“重置付费用量、优化 token 消耗”的公开公告和社区解读。社区把它概括成“额度暗砍”和“重置更耐用”，讨论热度很高。&lt;/p&gt;
&lt;p&gt;这里必须区分三层事实：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;厂商正式公布的政策&lt;/li&gt;
&lt;li&gt;社区根据账户体感做出的观察&lt;/li&gt;
&lt;li&gt;用户自己在某个模型、套餐和时间窗口里的实测&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这三层不能混成一句“某家把额度砍了”。但它们共同说明了一个问题：&lt;strong&gt;Agent 的容量边界已经是产品契约的一部分。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;价格和额度不是一回事&lt;/h2&gt;
&lt;p&gt;很多产品页面只展示月费和模型名称，用户却真正受到下面这些变量影响：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;维度&lt;/th&gt;
&lt;th&gt;价格回答什么&lt;/th&gt;
&lt;th&gt;额度/SLA 要回答什么&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;订阅费&lt;/td&gt;
&lt;td&gt;每月付多少钱&lt;/td&gt;
&lt;td&gt;这一档能持续做多少工作&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;token 价格&lt;/td&gt;
&lt;td&gt;每次调用多少钱&lt;/td&gt;
&lt;td&gt;一项任务会消耗多少可用容量&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;reset 周期&lt;/td&gt;
&lt;td&gt;什么时候结算&lt;/td&gt;
&lt;td&gt;什么时候恢复可用额度&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;并发&lt;/td&gt;
&lt;td&gt;单次请求成本&lt;/td&gt;
&lt;td&gt;同时跑几个任务不会互相拖垮&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;模型可用性&lt;/td&gt;
&lt;td&gt;能不能选某个模型&lt;/td&gt;
&lt;td&gt;该模型在高峰期是否稳定可用&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;失败处理&lt;/td&gt;
&lt;td&gt;请求失败是否收费&lt;/td&gt;
&lt;td&gt;失败后任务能否继续、是否重复扣费&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;历史恢复&lt;/td&gt;
&lt;td&gt;是否保存聊天记录&lt;/td&gt;
&lt;td&gt;中断后能否从原位置恢复工作&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;一个“很便宜但经常在长任务中途停下”的 Agent，实际成本可能高于一个单价更高、但一次能完成任务的 Agent。&lt;/p&gt;
&lt;h2&gt;为什么编程任务特别依赖连续性&lt;/h2&gt;
&lt;p&gt;编程 Agent 不是只返回一段文字。它通常维护一条执行链：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;理解需求
  → 定位代码
  → 制定计划
  → 修改文件
  → 运行测试
  → 读取错误
  → 修复
  → 再测试
  → 汇总结果
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;每一步都可能触发新的模型请求和工具调用。额度在这条链中间耗尽，会留下一个不完整但已经产生副作用的现场。&lt;/p&gt;
&lt;p&gt;从用户角度看，真正关心的不是“今天还能发送多少条消息”，而是：&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;li&gt;额度恢复前我能不能继续做别的任务&lt;/li&gt;
&lt;li&gt;有没有明确的剩余容量提示&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以编程 Agent 的用量指标应该更接近任务维度，而不是消息维度。&lt;/p&gt;
&lt;h2&gt;我会定义哪些容量指标&lt;/h2&gt;
&lt;h3&gt;1. 可用请求预算&lt;/h3&gt;
&lt;p&gt;不是总 token 数，而是当前时间窗口还能发起多少次有效模型请求。工具调用、重试和子 Agent 都应该计入。&lt;/p&gt;
&lt;h3&gt;2. 上下文预算&lt;/h3&gt;
&lt;p&gt;同样一个模型，在短会话和长会话中的成本完全不同。需要区分：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当前输入 token&lt;/li&gt;
&lt;li&gt;输出 token&lt;/li&gt;
&lt;li&gt;cache hit / miss&lt;/li&gt;
&lt;li&gt;压缩或历史重建产生的额外输入&lt;/li&gt;
&lt;li&gt;子 Agent 是否重复携带上下文&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Claude Code 官方文档已经提供 &lt;code&gt;/usage&lt;/code&gt;、prompt cache、团队 spend limit 和 OpenTelemetry 等用量观察手段。这个方向是对的：用户至少要能知道容量花在哪里。&lt;/p&gt;
&lt;h3&gt;3. 时间窗口&lt;/h3&gt;
&lt;p&gt;“每周额度”太粗了。实际产品至少应该说明：&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;li&gt;聊天、Cowork 和 Code 是否共享&lt;/li&gt;
&lt;li&gt;失败请求是否占用额度&lt;/li&gt;
&lt;li&gt;重置是否立即生效&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4. 连续任务保证&lt;/h3&gt;
&lt;p&gt;这是最容易被忽略的指标。一个任务如果已经开始执行，额度不足时产品应该有明确策略：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;继续当前任务
  或切换备用模型
  或保存 checkpoint 后暂停
  或请求用户补充额度
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;直接返回一个模糊的“达到限制”并丢掉上下文，是最差的处理方式。&lt;/p&gt;
&lt;h3&gt;5. 资源归属&lt;/h3&gt;
&lt;p&gt;如果一个主 Agent 开了三个子 Agent，额度应该能回答：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;主任务用了多少
子任务 A 用了多少
重试用了多少
哪个工具产生了最多上下文
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;否则用户看到的总数无法指导下一次决策。&lt;/p&gt;
&lt;h2&gt;额度变化为什么会伤信任&lt;/h2&gt;
&lt;p&gt;价格调整至少是一个容易理解的数字：每月从 20 变成 25。额度调整更难，因为用户要通过长时间使用才能发现变化。&lt;/p&gt;
&lt;p&gt;最伤信任的不是额度少，而是这几种不确定：&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;li&gt;重置时间和用户看到的时间不一致&lt;/li&gt;
&lt;li&gt;失败请求是否扣费没有说明&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;编程 Agent 的用户本身就在把真实工作交给系统。他们会容忍模型偶尔犯错，但很难接受容量边界不可预测。&lt;/p&gt;
&lt;h2&gt;如果是我来做 Agent 产品&lt;/h2&gt;
&lt;p&gt;我会把额度设计成一个可观察、可恢复的状态机，而不是一个藏在后端的计数器：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;                    ┌──────────────┐
                    │ task running │
                    └──────┬───────┘
                           │ budget low
                    ┌──────▼───────┐
                    │ warn user    │
                    └──┬─────┬─────┘
                       │     │
              ┌────────▼┐  ┌─▼──────────┐
              │ fallback │  │ checkpoint │
              │ model    │  │ and pause  │
              └────┬─────┘  └─────┬──────┘
                   │              │
                   └──────┬───────┘
                          ▼
                    task continues
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;具体做法是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;任务开始时估算一个区间，不假装精确&lt;/li&gt;
&lt;li&gt;接近阈值时提前提醒，而不是等请求失败&lt;/li&gt;
&lt;li&gt;保留最近一次可恢复 checkpoint&lt;/li&gt;
&lt;li&gt;把备用模型和备用 provider 当成正式路径&lt;/li&gt;
&lt;li&gt;记录每次 fallback 的原因和额外成本&lt;/li&gt;
&lt;li&gt;额度耗尽时保留任务状态、已改文件和待办步骤&lt;/li&gt;
&lt;li&gt;让用户能看到 reset 时间和当前任务的预计消耗&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这和我们在做 Pi/cohub 时关注的事情是一致的：Agent 不是一次回答，而是一段可能持续很久的工作。工作系统必须知道自己何时应该继续，何时应该停下来等人。&lt;/p&gt;
&lt;h2&gt;用户自己能做什么&lt;/h2&gt;
&lt;p&gt;在服务商策略无法改变时，我会采用几条保守策略：&lt;/p&gt;
&lt;h3&gt;把长任务拆成可恢复阶段&lt;/h3&gt;
&lt;p&gt;不要让一个会话同时负责分析、实现、测试和部署。每个阶段写出明确产物，下一阶段从文件和 checkpoint 接着走。&lt;/p&gt;
&lt;h3&gt;给强模型留预算&lt;/h3&gt;
&lt;p&gt;机械搜索、格式修改、简单测试可以用便宜模型；设计方案、跨文件重构和最终审查留给强模型。关键不是永远用最强模型，而是不要在低价值步骤把预算消耗完。&lt;/p&gt;
&lt;h3&gt;记录自己的任务成本&lt;/h3&gt;
&lt;p&gt;我会记录：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;任务名称
开始/结束时间
模型
请求次数
是否发生压缩
是否发生重试
最终是否完成
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一两周之后，才能知道自己真正买的不是“多少 token”，而是多少个完成的任务。&lt;/p&gt;
&lt;h3&gt;预留故障转移路径&lt;/h3&gt;
&lt;p&gt;如果一个模型或 provider 是唯一入口，额度一变，整个工作流就会被带走。能接 OpenAI 兼容接口、能保留 transcript、能切换模型的 Agent，抗波动能力会更好。&lt;/p&gt;
&lt;h2&gt;本地模型也有额度&lt;/h2&gt;
&lt;p&gt;本地部署看起来没有服务商 quota，但它有自己的容量边界：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;显存和系统内存&lt;/li&gt;
&lt;li&gt;磁盘吞吐&lt;/li&gt;
&lt;li&gt;并发 slot&lt;/li&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;我上一篇写 Qwen3.8-Flash-Next 时就提到过：模型“能加载”不等于“能生产”。本地 Agent 的内存和吞吐其实也是一种 SLA，只是承诺方从云服务商变成了自己的机器和推理后端。&lt;/p&gt;
&lt;h2&gt;最后&lt;/h2&gt;
&lt;p&gt;编程 Agent 的额度不是一个抽象的套餐数字。它决定了一个正在修改真实代码的系统能不能把事情做完。&lt;/p&gt;
&lt;p&gt;所以我希望未来的 Agent 产品页面少写一点“无限”，多写几件更有用的事：&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;li&gt;中断后能不能恢复&lt;/li&gt;
&lt;li&gt;备用路径是什么&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;SLA 的核心不是承诺永远不出问题，而是出问题时边界清楚、状态保留、用户知道下一步该怎么办。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这对编程 Agent 尤其重要。&lt;/p&gt;
&lt;h2&gt;参考资料&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://code.claude.com/docs/en/costs&quot;&gt;Anthropic：Manage costs effectively&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://public.cohub.run/s/f7000115-55d8-4d97-a0e4-d2a55b6ffa41/genai-hot/2026-08-30/evidence/anthropic-openai-quota-war/index.html&quot;&gt;OKP：Anthropic 与 OpenAI 编程模型额度政策&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://public.cohub.run/s/f7000115-55d8-4d97-a0e4-d2a55b6ffa41/genai-hot/2026-09-01/evidence/chatgpt-outage-codex-migration/index.html&quot;&gt;OKP：ChatGPT 停机与 Codex 用户迁移讨论&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://public.cohub.run/s/f7000115-55d8-4d97-a0e4-d2a55b6ffa41/genai-hot/2026-09-01/evidence/codex-ecosystem-tooling/index.html&quot;&gt;OKP：Codex 生态持续扩张&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/posts/qwen38-llama-cpp/&quot;&gt;模型发布只是开始：Qwen3.8-Flash-Next 与 runtime&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Agent 开始自己省 token：Grok Bot 的成本优化意味着什么</title><link>https://bangwu.me/posts/grok-bot-token-cost/</link><guid isPermaLink="true">https://bangwu.me/posts/grok-bot-token-cost/</guid><description>Grok Bot 官方开始宣传自动 token 优化。对 Agent 来说，成本不只是后台账单，而是任务能否持续、用户是否敢长期使用的产品体验。</description><pubDate>Tue, 01 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;先说结论&lt;/h2&gt;
&lt;p&gt;Agent 产品开始主动宣传“自动省 token”，说明一个变化：&lt;strong&gt;token 成本已经从后台指标，变成了用户能感知的产品功能。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;9 月 1 日，Grok Bot 的官方更新里连续出现两条相关信息：一条是 &lt;code&gt;Grok @Bot upgrades&lt;/code&gt;，另一条是 &lt;code&gt;Automatic token optimization to lower your Grok @Bot cost will be added soon&lt;/code&gt;。按 OKP 的追踪数据，两条帖子分别获得了 16,505 和 18,789 个赞。&lt;/p&gt;
&lt;p&gt;这里要先说清楚：官方目前公布的是“即将加入自动优化”的方向，没有给出统一的节省比例、具体算法或所有任务上的成本承诺。所以这篇不是 Grok Bot 的节省效果评测，而是想回答一个更实际的问题：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;一个 Agent 到底为什么会贵，以及成本优化应该放在哪一层。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Agent 的账单不是一次请求&lt;/h2&gt;
&lt;p&gt;普通聊天产品的成本大致是一问一答：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;一次输入 + 一次输出 = 一次成本
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Agent 完成一个任务时，通常是这样：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;理解任务
  → 读文件
  → 调工具
  → 看结果
  → 修改计划
  → 再调工具
  → 重试
  → 验证
  → 总结
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果第 &lt;code&gt;i&lt;/code&gt; 次模型请求的输入、输出、缓存和工具开销分别是 &lt;code&gt;I_i&lt;/code&gt;、&lt;code&gt;O_i&lt;/code&gt;、&lt;code&gt;K_i&lt;/code&gt;、&lt;code&gt;T_i&lt;/code&gt;，一个任务的粗略成本可以写成：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;C_task = Σ (I_i × p_in + O_i × p_out + K_i × p_cache + T_i)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;真正让账单变大的，往往不是某一个回答特别长，而是请求次数和重复上下文太多。&lt;/p&gt;
&lt;p&gt;一个看起来只需要“改一个按钮”的任务，可能包含：&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;li&gt;截图或网页状态被当成高 token 的视觉输入&lt;/li&gt;
&lt;li&gt;失败后没有明确停止条件&lt;/li&gt;
&lt;li&gt;多个子 Agent 各自携带一份重复上下文&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以 Agent 的成本优化不能只做成一个“换小模型”的开关。&lt;/p&gt;
&lt;h2&gt;Grok Bot 的信号为什么值得看&lt;/h2&gt;
&lt;p&gt;Grok Bot 不是单纯的聊天框。它被描述成拥有自己的电脑，可以进入 Gmail、Salesforce、LinkedIn 等服务执行任务。这样的 Agent 一旦进入重度使用阶段，token 消耗会和任务链路绑定：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;任务越长
  → 工具调用越多
  → 上下文重复越多
  → 重试机会越多
  → 成本越难预测
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;早期产品最容易宣传“它能做什么”；到了用户真的拿它跑工作之后，问题会变成：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;这个任务大概花多少钱&lt;/li&gt;
&lt;li&gt;为什么同样的任务今天比昨天贵&lt;/li&gt;
&lt;li&gt;Agent 是不是在重复读相同内容&lt;/li&gt;
&lt;li&gt;失败重试有没有上限&lt;/li&gt;
&lt;li&gt;我能不能在任务中途切换到更便宜的模型&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;“自动 token optimization”回应的其实是这些问题，而不只是模型推理速度。&lt;/p&gt;
&lt;h2&gt;成本优化应该分五层&lt;/h2&gt;
&lt;h3&gt;第一层：不要重复发送不变的东西&lt;/h3&gt;
&lt;p&gt;最简单、收益通常也最稳定的是上下文去重：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;system prompt 保持稳定，尽量命中 prompt cache&lt;/li&gt;
&lt;li&gt;项目规则按需加载，不要每一轮全文拼接&lt;/li&gt;
&lt;li&gt;工具结果只保留模型真正需要的字段&lt;/li&gt;
&lt;li&gt;同一文件的内容在一个 turn 内复用，不要重复读取&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这层优化不改变模型能力，只减少重复搬运。&lt;/p&gt;
&lt;h3&gt;第二层：让工具返回“可用结果”&lt;/h3&gt;
&lt;p&gt;很多 Agent 贵，不是模型太爱思考，而是工具返回了太多原始材料。&lt;/p&gt;
&lt;p&gt;例如一个 shell 工具可以返回 2 万行日志，也可以返回：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;exitCode&quot;: 1,
  &quot;stderr&quot;: &quot;TypeError: ...&quot;,
  &quot;relevantFiles&quot;: [&quot;src/app.ts:42&quot;],
  &quot;tail&quot;: &quot;...&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;原始日志应该保存在可追溯的外部记录里，模型上下文只拿摘要和定位信息。这样既省 token，也让模型更容易抓住真正的错误。&lt;/p&gt;
&lt;h3&gt;第三层：不同阶段用不同模型&lt;/h3&gt;
&lt;p&gt;不是所有步骤都需要同一档模型：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;阶段&lt;/th&gt;
&lt;th&gt;更看重什么&lt;/th&gt;
&lt;th&gt;可以考虑&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;任务分类&lt;/td&gt;
&lt;td&gt;低延迟、低成本&lt;/td&gt;
&lt;td&gt;小模型&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;文件定位&lt;/td&gt;
&lt;td&gt;稳定工具调用&lt;/td&gt;
&lt;td&gt;中等模型&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;方案设计&lt;/td&gt;
&lt;td&gt;推理质量&lt;/td&gt;
&lt;td&gt;强模型&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;机械改写&lt;/td&gt;
&lt;td&gt;结构遵循&lt;/td&gt;
&lt;td&gt;便宜模型&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;最终验证&lt;/td&gt;
&lt;td&gt;减少漏错&lt;/td&gt;
&lt;td&gt;强模型&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;这里有一个重要前提：切换模型不能让上下文重新完整上传一遍，否则省下来的输出费用可能被输入费用吃掉。&lt;/p&gt;
&lt;h3&gt;第四层：给循环设置预算&lt;/h3&gt;
&lt;p&gt;Agent 的循环应该有明确的预算，而不是“直到模型觉得完成了”。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type TaskBudget = {
  maxSteps: number;
  maxInputTokens: number;
  maxOutputTokens: number;
  maxWallTimeMs: number;
};

function canContinue(state: TaskState, budget: TaskBudget): boolean {
  return (
    state.steps &amp;lt; budget.maxSteps &amp;amp;&amp;amp;
    state.inputTokens &amp;lt; budget.maxInputTokens &amp;amp;&amp;amp;
    state.outputTokens &amp;lt; budget.maxOutputTokens &amp;amp;&amp;amp;
    state.elapsedMs &amp;lt; budget.maxWallTimeMs
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;达到预算之后，不一定要直接失败。可以按任务类型选择：&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;li&gt;给用户展示目前完成了什么、还差什么&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;第五层：把成本变成可观察数据&lt;/h3&gt;
&lt;p&gt;“感觉今天很贵”不是可调试的指标。至少要记录：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;每个任务的总 token 和总费用&lt;/li&gt;
&lt;li&gt;每一步的输入/输出/cache 命中&lt;/li&gt;
&lt;li&gt;工具调用次数和工具结果大小&lt;/li&gt;
&lt;li&gt;重试次数&lt;/li&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;我会特别关注两个数字：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cost_per_successful_task
cost_before_user_abandonment
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;前者告诉你系统完成一件事要付出什么，后者告诉你用户在 Agent 还没完成之前已经被消耗了多少预算。&lt;/p&gt;
&lt;h2&gt;“省 token”不能变成“少做工作”&lt;/h2&gt;
&lt;p&gt;成本优化有一个很容易踩的坑：把任务结果变差，换成账单变好。&lt;/p&gt;
&lt;p&gt;例如：&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;li&gt;在复杂任务中途切到小模型，导致返工&lt;/li&gt;
&lt;li&gt;把多次尝试隐藏掉，让用户看不到实际成本&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以优化目标不应该是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;minimize tokens
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;而应该更接近：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;minimize cost
subject to successful completion, safety, and recoverability
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也就是在完成率、安全性和可恢复性不下降的前提下，减少成本。&lt;/p&gt;
&lt;h2&gt;云端 Agent 和本地 Agent 的成本不一样&lt;/h2&gt;
&lt;p&gt;Grok Bot 这类云端 Agent 主要面对的是服务端 token 成本和用户套餐成本。Pi/cohub 这类运行时还要考虑另一种成本：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;云端 API token&lt;/li&gt;
&lt;li&gt;浏览器/容器运行时间&lt;/li&gt;
&lt;li&gt;文件存储和日志保留&lt;/li&gt;
&lt;li&gt;多 Agent 并发&lt;/li&gt;
&lt;li&gt;本地模型的显存、内存和电力&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;本地模型看起来没有 API 账单，但“把一个 100GB 模型留在内存里跑一整天”也不是零成本。只是成本从按 token 计费，换成了固定设备成本和吞吐成本。&lt;/p&gt;
&lt;p&gt;因此我不太相信一个统一的“每百万 token 多少钱”能够描述 Agent 的真实价格。更合理的单位可能是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;每个成功任务的成本
每个有效工具动作的成本
每个可恢复工作小时的成本
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;从产品角度，我会怎么做&lt;/h2&gt;
&lt;p&gt;如果我要给 Agent 加成本控制，不会只在设置页放一个 slider。我会做四件事：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;任务开始前给区间估算&lt;/strong&gt;：告诉用户这是短任务、长任务还是可能持续运行的任务&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;任务进行中显示消耗趋势&lt;/strong&gt;：不要求精确到最后一分钱，但要能看出是否进入异常循环&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;预算触顶时优雅降级&lt;/strong&gt;：保存状态、切换模型或请求用户确认，不要突然丢掉整个任务&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;事后给出可解释账单&lt;/strong&gt;：哪些步骤最贵、哪些工具产生了大量上下文、哪些重试没有收益&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这比单纯说“我们自动优化了 token”更值得信任。用户不一定需要知道每个内部算法，但需要知道 Agent 为什么花钱。&lt;/p&gt;
&lt;h2&gt;最后&lt;/h2&gt;
&lt;p&gt;Grok Bot 这次把 token 优化放到公开产品更新里，我觉得是一个很重要的信号：&lt;strong&gt;Agent 的下半场不是只比谁更会做事，也要比谁更会控制做事的成本。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;一个 Agent 如果只能偶尔完成一次漂亮演示，却让用户不敢把真实工作交给它，它仍然只是 demo。&lt;/p&gt;
&lt;p&gt;真正好用的 Agent，应该让人知道它正在做什么、还会花多少、出了问题能不能停在一个可恢复的位置。&lt;/p&gt;
&lt;p&gt;省 token 只是手段。让用户敢把任务交给你，才是目的。&lt;/p&gt;
&lt;h2&gt;参考资料&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://public.cohub.run/s/f7000115-55d8-4d97-a0e4-d2a55b6ffa41/genai-hot/2026-09-01/evidence/grok-bot/index.html&quot;&gt;Grok Bot：OKP 最新产品更新与 token 优化信号&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://grok.com/&quot;&gt;Grok Bot 产品页&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/posts/pi-model-distribution/&quot;&gt;模型的新货架：从 Grok 4.6 上架 Pi 说起&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://code.claude.com/docs/en/costs&quot;&gt;Anthropic：Manage costs effectively&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>当视频模型开始生成界面：Runway Solaris 观察</title><link>https://bangwu.me/posts/runway-solaris-interface-world-model/</link><guid isPermaLink="true">https://bangwu.me/posts/runway-solaris-interface-world-model/</guid><description>Runway Solaris 把自己定义成 Interface World Model：界面不再是固定组件，而是由模型实时生成的可操作世界。这是产品预览，也是一个值得拆开的方向。</description><pubDate>Tue, 01 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;先说结论&lt;/h2&gt;
&lt;p&gt;8 月 31 日，Runway 发布了 Solaris 研究预览，并把它称为 &lt;strong&gt;Interface World Model&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;官方描述很大胆：这不是一个普通的视频生成器，而是一种新型“操作系统”，窗口、菜单、滑杆等界面由视频模型实时生成，方向包括场景化视频、游戏界面、创意工具和产品原型。&lt;/p&gt;
&lt;p&gt;我对这件事的判断是：&lt;strong&gt;如果 Solaris 的可操作性能够成立，它改变的不是“视频能生成得更漂亮”，而是界面本身可能不再是预先写好的组件集合。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;但现在还不能把它写成成熟产品。当前公开信号主要来自 Runway 官方账号和一个 X List 旁证，公测资格、API、状态持久化和独立测试都没有明确资料。下面是方向分析，不是能力评测。&lt;/p&gt;
&lt;h2&gt;过去的界面是什么样的&lt;/h2&gt;
&lt;p&gt;我们熟悉的软件界面有一个稳定假设：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;固定组件
  + 固定状态
  + 固定事件处理
  + 固定数据模型
  = 可操作应用
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一个按钮是一个 DOM 节点，一个滑杆有明确的数值，一个菜单项对应一条路由。即使界面由游戏引擎或 Canvas 绘制，背后通常仍然有一套确定的 UI 状态机。&lt;/p&gt;
&lt;p&gt;传统的生成式 AI 主要生成的是内容：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;提示词 → 图片 / 视频 / 音频
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;它可以生成一个看起来像软件的画面，但这个画面通常只是结果，不是一个可以持续操作、保存状态、撤销和再次打开的应用。&lt;/p&gt;
&lt;p&gt;Solaris 想挑战的是中间那条边界：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;意图
  → 模型生成当前界面
  → 用户动作
  → 模型理解动作后的世界状态
  → 生成下一帧界面
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果这条循环稳定，界面就不再只是“渲染数据”，而变成了模型对世界状态的一种实时投影。&lt;/p&gt;
&lt;h2&gt;它和 GUI Agent 不是一回事&lt;/h2&gt;
&lt;p&gt;最近也有很多 GUI Agent：模型看屏幕，点击按钮，输入文字，拖动鼠标。这些系统通常是在&lt;strong&gt;已有界面&lt;/strong&gt;上行动。&lt;/p&gt;
&lt;p&gt;可以这样对比：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;GUI Agent&lt;/th&gt;
&lt;th&gt;Interface World Model&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;界面从哪里来&lt;/td&gt;
&lt;td&gt;人或程序预先构建&lt;/td&gt;
&lt;td&gt;模型实时生成或重构&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;模型的主要动作&lt;/td&gt;
&lt;td&gt;点击、输入、滚动&lt;/td&gt;
&lt;td&gt;生成状态，同时响应动作&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;状态来源&lt;/td&gt;
&lt;td&gt;DOM、Accessibility Tree、截图、应用 API&lt;/td&gt;
&lt;td&gt;视觉/世界状态与动作反馈&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;主要难点&lt;/td&gt;
&lt;td&gt;找到正确控件并执行&lt;/td&gt;
&lt;td&gt;保持界面、状态和动作的一致性&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;失败形态&lt;/td&gt;
&lt;td&gt;点错、找不到、权限不足&lt;/td&gt;
&lt;td&gt;界面好看但状态不真实或不可持续&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;所以不能因为两者都“会用界面”，就把 Solaris 当成另一个 computer-use 模型。它更接近：&lt;strong&gt;模型不仅使用工作台，还参与生成工作台。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;真正困难的不是画出一扇窗&lt;/h2&gt;
&lt;p&gt;从演示画面看，窗口、菜单和滑杆并不难想象。真正难的是这些东西必须具备软件的性质。&lt;/p&gt;
&lt;h3&gt;1. 状态要持久&lt;/h3&gt;
&lt;p&gt;用户拖动滑杆之后，数值应该进入某个可恢复的状态，而不是下一帧又随机回去。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;动作 A
  → 状态 S1
  → 关闭界面
  → 重新打开
  → 仍然得到 S1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果状态只存在模型的短期上下文里，它就更像一次互动幻觉，而不是应用。&lt;/p&gt;
&lt;h3&gt;2. 动作要可重复&lt;/h3&gt;
&lt;p&gt;同样的点击在同样的状态下，应该产生相同或可解释的结果。视频模型天然带有生成随机性，而软件交互依赖确定性。&lt;/p&gt;
&lt;p&gt;这会要求系统把“视觉帧”和“真实状态”分开：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;可生成的显示层
       ↓
可验证的状态层
       ↓
可执行的动作层
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果没有状态层，用户看到的按钮和系统真正接受的动作可能不是一回事。&lt;/p&gt;
&lt;h3&gt;3. 界面要能被机器理解&lt;/h3&gt;
&lt;p&gt;固定 UI 有 DOM、Accessibility Tree、语义标签和键盘焦点。模型生成的界面如果只有像素，接入自动化、辅助功能和测试都会变得困难。&lt;/p&gt;
&lt;p&gt;一个真正可用的 Interface World Model，至少需要回答：&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;li&gt;操作会改变哪些状态&lt;/li&gt;
&lt;li&gt;屏幕阅读器和键盘如何访问它们&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;“看起来像按钮”不等于“它是一个按钮”。&lt;/p&gt;
&lt;h3&gt;4. 延迟必须足够低&lt;/h3&gt;
&lt;p&gt;普通视频生成可以等几秒甚至几分钟。界面交互不能每次点击都等一段完整视频生成。&lt;/p&gt;
&lt;p&gt;用户会期待：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;按下按钮
  → 立即看到反馈
  → 状态逐步稳定
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因此 Solaris 更像实时世界模型，而不是离线视频模型。它要解决的不是单次画质，而是连续生成、状态更新和输入响应之间的延迟。&lt;/p&gt;
&lt;h3&gt;5. 失败要能撤销&lt;/h3&gt;
&lt;p&gt;固定应用里的错误操作通常可以 undo、回退或重新加载。生成式界面如果把每次状态变化都当成新一帧，就必须有明确的历史：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;S0 → S1 → S2 → S3
           ↑
        undo / fork
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;否则用户很难知道一次错误是模型生成错了，还是自己的操作真的改变了数据。&lt;/p&gt;
&lt;h2&gt;我会怎样评估它&lt;/h2&gt;
&lt;p&gt;如果 Solaris 之后开放试用，我不会先截几张好看的图，而会做一套重复性测试。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;测试项&lt;/th&gt;
&lt;th&gt;需要观察的结果&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;同一 prompt 重复生成&lt;/td&gt;
&lt;td&gt;结构和交互是否稳定&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;连续拖动滑杆&lt;/td&gt;
&lt;td&gt;状态是否连续、是否丢输入&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;关闭后重新打开&lt;/td&gt;
&lt;td&gt;状态是否持久化&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;快速连续点击&lt;/td&gt;
&lt;td&gt;是否丢事件或进入错误状态&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;断网后恢复&lt;/td&gt;
&lt;td&gt;能否继续或明确失败&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;键盘操作&lt;/td&gt;
&lt;td&gt;是否有焦点和可访问语义&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;自动化操作&lt;/td&gt;
&lt;td&gt;是否能通过 API/语义层定位控件&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;撤销与分支&lt;/td&gt;
&lt;td&gt;是否能回到之前的状态&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;长时间运行&lt;/td&gt;
&lt;td&gt;延迟和状态漂移是否累积&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;我尤其关注“同一个任务跑十次”的成功率。生成界面最容易在单次演示里看起来惊艳，真正决定它是不是产品的是第十次还能不能找到同一个按钮。&lt;/p&gt;
&lt;h2&gt;它可能先在哪些地方成立&lt;/h2&gt;
&lt;h3&gt;游戏&lt;/h3&gt;
&lt;p&gt;游戏本来就接受动态世界和非固定界面。模型生成一个场景化的控制层，可能比在办公软件里生成表单更自然。但游戏仍然需要物理规则、存档、碰撞和多人同步，视觉生成不能替代底层状态机。&lt;/p&gt;
&lt;h3&gt;创意工具&lt;/h3&gt;
&lt;p&gt;创作工具的界面本来就经常围绕当前任务变化。用户在做分镜、调色或构图时，需要的控件不一样，动态生成一个任务专用工作台有实际价值。&lt;/p&gt;
&lt;h3&gt;产品原型&lt;/h3&gt;
&lt;p&gt;这是最容易落地的方向：不是直接替代生产软件，而是让用户用自然语言快速得到一个可交互的原型。这里对确定性的要求相对低，但仍然需要能保存和分享。&lt;/p&gt;
&lt;h3&gt;场景化学习&lt;/h3&gt;
&lt;p&gt;教学界面、模拟实验和可视化解释也可能受益。模型不只是给一段答案，而是根据用户的理解程度生成一套可以探索的界面。&lt;/p&gt;
&lt;h2&gt;也要警惕一个误区&lt;/h2&gt;
&lt;p&gt;“界面由模型生成”不代表界面一定更好。&lt;/p&gt;
&lt;p&gt;固定组件的价值就在于稳定、可预测、可测试。动态生成会带来新的成本：&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;li&gt;产品问题难以复现&lt;/li&gt;
&lt;li&gt;视觉一致性和品牌规范变复杂&lt;/li&gt;
&lt;li&gt;安全敏感的操作可能被模型藏在不明显的位置&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以我不期待所有软件都变成生成界面。更现实的形态可能是：&lt;strong&gt;稳定的底层状态和权限 + 针对当前任务生成的工作台视图。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;最后&lt;/h2&gt;
&lt;p&gt;Runway Solaris 目前还是研究预览，很多关键问题没有答案。它值得关注，不是因为一条视频里出现了会动的菜单，而是因为它把一个问题摆到了台面上：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;界面究竟是程序的固定外壳，还是模型理解世界后生成的一种临时视图？&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;我暂时不会把它叫作“下一个操作系统”。但如果它能把生成画面、真实状态、语义操作和持久化历史接在一起，那它可能会成为一种新的软件入口。&lt;/p&gt;
&lt;p&gt;现在先等它从演示里出来，给别人真正点几下。&lt;/p&gt;
&lt;h2&gt;参考资料&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://runwayml.com/research&quot;&gt;Runway 官方研究页面&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://runwayml.com/&quot;&gt;Runway 官方网站&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://public.cohub.run/s/f7000115-55d8-4d97-a0e4-d2a55b6ffa41/genai-hot/2026-09-01/evidence/runway-solaris-interface-world-model/index.html&quot;&gt;OKP：Runway 发布 Solaris&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://public.cohub.run/s/f7000115-55d8-4d97-a0e4-d2a55b6ffa41/genai-hot/2026-08-22/evidence/qwen-ui-agent-release/index.html&quot;&gt;阿里 Qwen-UI-Agent 信号（相关背景）&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>模型发布只是开始：为什么 day-0 的 llama.cpp 支持比榜单更重要</title><link>https://bangwu.me/posts/qwen38-llama-cpp/</link><guid isPermaLink="true">https://bangwu.me/posts/qwen38-llama-cpp/</guid><description>以 Qwen3.8-Flash-Next 为例，拆开一个开源模型从发布到真正可用要经过哪些环节，以及我会怎样判断它是否已经适合接入本地 Agent。</description><pubDate>Fri, 28 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;先说结论&lt;/h2&gt;
&lt;p&gt;我现在看一个开源模型，已经不会先看它在榜单上排第几了。我会先看另一个时间：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;从模型发布，到主流推理后端真正支持，中间隔了多久。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;我暂时把它叫作 &lt;strong&gt;release-to-runtime latency&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;Qwen3.8-Flash-Next 这次很典型：8 月 26 日发布，&lt;code&gt;llama.cpp&lt;/code&gt; 的主支持 PR 在 8 月 27 日合并。一天左右，模型从「仓库里有权重」变成「普通开发者可以用熟悉的本地推理工具启动」。这比一条“超过谁”的横评标题，更接近真实的生态进展。&lt;/p&gt;
&lt;p&gt;但「支持已合并」也不等于「所有机器都适合生产」。Qwen3.8-Flash-Next 的 n-gram embedding 很吃内存，主线之后仍有 SM110、SM121、AMD HIP、并发请求等问题在跟踪，原生 MTP 也还在单独的开放 PR 里。&lt;/p&gt;
&lt;p&gt;所以这篇不做“Qwen 又赢了”的结论。我更想把这条链路拆开：&lt;strong&gt;一个模型到底怎样才算真正到达开发者手里。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这次我没有在本机下载 75GB 以上的权重，也没有伪造自己的 tokens/s。下面的硬件数据分别来自 Qwen 官方模型卡、Unsloth 运行指南、&lt;code&gt;llama.cpp&lt;/code&gt; 的合并 PR 和公开 issue；社区结果会明确标成社区结果。&lt;/p&gt;
&lt;h2&gt;Qwen3.8-Flash-Next 到底是什么&lt;/h2&gt;
&lt;p&gt;Qwen 官方把它定位成 Qwen4 架构的早期预览，而且是 open-weight，不是一个单纯的 Qwen3 小版本。&lt;/p&gt;
&lt;p&gt;规格先摆在这里：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;项目&lt;/th&gt;
&lt;th&gt;Qwen3.8-Flash-Next&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;主模型参数&lt;/td&gt;
&lt;td&gt;125B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;每 token 激活参数&lt;/td&gt;
&lt;td&gt;6B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;n-gram embedding&lt;/td&gt;
&lt;td&gt;51B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MTP&lt;/td&gt;
&lt;td&gt;4B，包含在模型文件中&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;原生上下文&lt;/td&gt;
&lt;td&gt;262,144 tokens&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;官方描述的可扩展上下文&lt;/td&gt;
&lt;td&gt;最多 1M tokens&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;视觉&lt;/td&gt;
&lt;td&gt;Causal LM + Vision Encoder&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MoE&lt;/td&gt;
&lt;td&gt;512 experts，10 routed + 1 shared&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;它的结构有几个地方直接影响本地部署：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Gated DeltaNet 和 Qwen Sparse Attention（QSA）混合，QSA 按 micro-block 选择上下文&lt;/li&gt;
&lt;li&gt;4 路 Gated Residual&lt;/li&gt;
&lt;li&gt;51B 的 n-gram embedding，官方设计上允许更多地向主机内存卸载&lt;/li&gt;
&lt;li&gt;另外有 MTP draft head，后续可以用于 speculative decoding&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这解释了一个容易被误解的地方：&lt;strong&gt;6B 激活不等于 6B 内存。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;MoE 的“每 token 只计算一小部分”主要影响计算量，不会把所有权重从文件和内存需求里抹掉。尤其是这个 51B 的 n-gram embedding，更像一个很大的查表结构。它可能不需要像普通 dense 参数一样参与每一步矩阵乘法，但仍然要被加载、映射或放到别的存储层里。&lt;/p&gt;
&lt;p&gt;目前公开的 GGUF 文件也说明了这一点。我查到的 Unsloth 仓库里：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;UD-IQ1_M&lt;/code&gt; 三个分片合计约 &lt;strong&gt;74.5 GB&lt;/strong&gt;（十进制）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;UD-IQ4_XS&lt;/code&gt; 三个分片合计约 &lt;strong&gt;93.7 GB&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;UD-Q4_K_XL&lt;/code&gt; 四个分片合计约 &lt;strong&gt;111.3 GB&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Q8_0&lt;/code&gt; 六个分片合计约 &lt;strong&gt;188.2 GB&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这只是模型文件大小，不是完整运行时需求。上下文 KV cache、临时计算 buffer、系统内存余量和视觉 projector 都还要占空间。&lt;/p&gt;
&lt;p&gt;Unsloth 的运行指南给出的经验线是：最小量化至少需要 75GB RAM 或 unified memory，实际更建议 96GB。这个范围更像“能开始尝试”的门槛，不应该直接理解成“任何 75GB 机器都能舒服地跑”。&lt;/p&gt;
&lt;h2&gt;发布到可用，中间差了什么&lt;/h2&gt;
&lt;p&gt;我现在会把模型接入想成这样一条链：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;权重 / 配置
    ↓
模型格式与 converter
    ↓
计算图与特殊算子
    ↓
量化、分片、内存布局
    ↓
CPU / CUDA / Metal / HIP 等后端
    ↓
chat template、工具调用、视觉输入
    ↓
单请求正确性
    ↓
长上下文、并发、持续负载
    ↓
真正适合接入 Agent
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;榜单通常只覆盖中间某一小段：给模型一批输入，看它输出什么。&lt;/p&gt;
&lt;p&gt;而本地 Agent 真正会遇到的是另一组问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;能不能在自己的硬件上加载&lt;/li&gt;
&lt;li&gt;模型文件是不是完整的分片集合&lt;/li&gt;
&lt;li&gt;n-gram 表应该放 GPU、CPU 还是统一内存&lt;/li&gt;
&lt;li&gt;上下文变长后速度会不会突然塌掉&lt;/li&gt;
&lt;li&gt;工具调用格式是否被正确解析&lt;/li&gt;
&lt;li&gt;第二个并发请求进来时会不会崩&lt;/li&gt;
&lt;li&gt;OOM 或请求取消之后，服务还能不能继续用&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这就是为什么我觉得 &lt;strong&gt;day-0 runtime support 比一张 benchmark 表更重要&lt;/strong&gt;。它不是说 benchmark 没有价值，而是说 benchmark 不能替代“模型能不能被使用”这件事。&lt;/p&gt;
&lt;h2&gt;&lt;code&gt;llama.cpp&lt;/code&gt; 这次到底做了多少工作&lt;/h2&gt;
&lt;p&gt;Qwen 官方 README 现在直接写了 &lt;code&gt;llama.cpp supports the Qwen3.8-Flash-Next (text &amp;amp; vision)&lt;/code&gt;。对应的主线支持来自 &lt;a href=&quot;https://github.com/ggml-org/llama.cpp/pull/27742&quot;&gt;PR #27742&lt;/a&gt;，已经在 8 月 27 日合并。&lt;/p&gt;
&lt;p&gt;这个 PR 不是加一个模型名称那么简单，改动涉及 28 个文件，主要包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;新增 &lt;code&gt;qwen4exp&lt;/code&gt; 模型转换逻辑&lt;/li&gt;
&lt;li&gt;新增 Qwen4Exp 的计算图&lt;/li&gt;
&lt;li&gt;支持 QSA sparse attention&lt;/li&gt;
&lt;li&gt;接入 vision 路径&lt;/li&gt;
&lt;li&gt;处理 n-gram embedding 的加载和查表&lt;/li&gt;
&lt;li&gt;修复量化器和 tensor type fallback&lt;/li&gt;
&lt;li&gt;增加混合内存和缓存相关逻辑&lt;/li&gt;
&lt;li&gt;补充模型结构 roundtrip 检查&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;PR 作者还给出了几类对照验证：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;wikitext-2 perplexity：实现 4.0068，对照实现 4.0126&lt;/li&gt;
&lt;li&gt;prose 512 tokens 的 top-1 agreement：98.0%&lt;/li&gt;
&lt;li&gt;QSA 在预算以下与 dense 路径 bit-identical&lt;/li&gt;
&lt;li&gt;CPU、CUDA、Metal 的模型结构检查通过&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这些数据应该按“PR 作者在该实现上的验证”来读，不是我重新跑出的独立 benchmark，也不等于所有量化和硬件配置都已经稳定。&lt;/p&gt;
&lt;p&gt;真正值得注意的是，&lt;code&gt;llama.cpp&lt;/code&gt; 这次把模型的特殊结构接进了一个很多人已经熟悉的入口。对开发者来说，学习成本从“再学一套新的推理框架”降成了“更新 llama.cpp、准备一份 GGUF、调几项参数”。这就是 runtime support 的价值。&lt;/p&gt;
&lt;h2&gt;最小启动路径&lt;/h2&gt;
&lt;p&gt;如果只是想验证文本推理，建议先用一个保守的上下文长度，不要上来就挑战 262K。&lt;/p&gt;
&lt;h3&gt;1. 编译 llama.cpp&lt;/h3&gt;
&lt;p&gt;NVIDIA CUDA：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git clone https://github.com/ggml-org/llama.cpp
cd llama.cpp
cmake -B build -DBUILD_SHARED_LIBS=OFF -DGGML_CUDA=ON
cmake --build build --config Release -j \
  --target llama-cli llama-server llama-mtmd-cli llama-gguf-split
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;没有 NVIDIA GPU 时，去掉 &lt;code&gt;-DGGML_CUDA=ON&lt;/code&gt;，按 CPU 或本机对应后端编译。这里故意没有写一个“万能参数”：不同硬件的后端、驱动和内存布局差异足够大，直接复制别人的 &lt;code&gt;-ngl&lt;/code&gt;、&lt;code&gt;--tensor-split&lt;/code&gt; 往往比少跑几 tokens/s 更危险。&lt;/p&gt;
&lt;h3&gt;2. 下载一份 GGUF&lt;/h3&gt;
&lt;p&gt;例如先下载 &lt;code&gt;UD-IQ4_XS&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip install -U &quot;huggingface_hub[cli]&quot;

hf download unsloth/Qwen3.8-Flash-Next-GGUF \
  --local-dir ./models/qwen38 \
  --include &apos;UD-IQ4_XS/*&apos; &apos;mmproj*&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个量化大约 93.7GB，机器没有足够内存时不要直接执行。你也可以把 &lt;code&gt;UD-IQ4_XS&lt;/code&gt; 换成更小的量化，但要接受质量和速度的变化。&lt;/p&gt;
&lt;p&gt;视觉输入需要 &lt;code&gt;mmproj&lt;/code&gt;。只做文本测试时可以先不加载它。&lt;/p&gt;
&lt;h3&gt;3. 先跑一个小上下文&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;./build/bin/llama-server \
  --model ./models/qwen38/UD-IQ4_XS/Qwen3.8-Flash-Next-UD-IQ4_XS-00001-of-00003.gguf \
  --alias qwen38 \
  --jinja \
  --ctx-size 32768 \
  --host 127.0.0.1 \
  --port 8000
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;分片模型通常传第一片即可，loader 会根据分片信息继续读取其余文件。如果你的下载工具改变了目录结构，先确认第一片和其它分片在同一目录。&lt;/p&gt;
&lt;p&gt;视觉模式再加：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  --mmproj ./models/qwen38/mmproj-F16.gguf
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;--jinja&lt;/code&gt; 很重要。它让 server 使用模型的 chat template；工具调用是否正常，还取决于模型模板和当前 llama.cpp 版本，不要只看服务端成功启动就认为 Agent 接入已经完成。&lt;/p&gt;
&lt;h3&gt;4. 用 OpenAI 兼容接口做第一次请求&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;curl http://127.0.0.1:8000/v1/chat/completions \
  -H &apos;Content-Type: application/json&apos; \
  -d &apos;{
    &quot;model&quot;: &quot;qwen38&quot;,
    &quot;messages&quot;: [
      {&quot;role&quot;: &quot;user&quot;, &quot;content&quot;: &quot;用三句话解释 QSA 解决了什么问题。&quot;}
    ],
    &quot;temperature&quot;: 0.6,
    &quot;max_tokens&quot;: 256
  }&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第一轮只验证四件事：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;模型能否加载&lt;/li&gt;
&lt;li&gt;普通文本是否连贯&lt;/li&gt;
&lt;li&gt;server 是否能稳定返回 OpenAI 兼容响应&lt;/li&gt;
&lt;li&gt;上下文从 4K、8K 增长到 32K 时，速度和内存是否出现异常变化&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;不要一开始就把它接到长程 Agent 上。先把模型、后端、模板和内存问题分开，排错会快很多。&lt;/p&gt;
&lt;h2&gt;“支持已合并”后仍然要看什么&lt;/h2&gt;
&lt;p&gt;这次的后续 issue 很能说明问题。&lt;/p&gt;
&lt;h3&gt;1. 特定 GPU 可能输出乱码&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ggml-org/llama.cpp/issues/27763&quot;&gt;Issue #27763&lt;/a&gt; 报告了 Jetson Thor / SM110 上的情况：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;-ngl 8&lt;/code&gt; 时输出连贯&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-ngl 9&lt;/code&gt; 或更多层放到 GPU 时输出变成乱码&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-ngl 0&lt;/code&gt; 纯 CPU 时恢复连贯&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这说明“CUDA 支持”不是一个二值开关。GPU 架构、offload 层数、特殊 tensor 的放置方式，都可能改变结果。&lt;/p&gt;
&lt;h3&gt;2. 长上下文和后端有关&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ggml-org/llama.cpp/issues/27856&quot;&gt;Issue #27856&lt;/a&gt; 记录了 AMD Strix Halo / HIP 路径上的长上下文速度下降：短上下文约 19 到 21 tok/s，超过约 1K context 后下降到约 5.5 到 6.1 tok/s，45K 时约 4.6 tok/s。&lt;/p&gt;
&lt;p&gt;同一个 issue 里，CUDA 设备上的衰减没有表现出同样幅度。这个结果不能外推成“AMD 不行”，但足够说明：&lt;strong&gt;模型架构的长上下文收益，最终要经过具体后端才能兑现。&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;3. 单请求通过，不等于服务能扛并发&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ggml-org/llama.cpp/issues/27835&quot;&gt;Issue #27835&lt;/a&gt; 报告了 CUDA 下第二个并发连接进入后 server 崩溃的问题。单请求、工具调用、结构化输出都正常，不代表两个请求同时进来仍然正常。&lt;/p&gt;
&lt;p&gt;如果你的目标是本地 Agent 服务，至少要测：&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;li&gt;上下文接近上限时的内存变化&lt;/li&gt;
&lt;li&gt;工具调用连续失败后的恢复&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4. MTP 仍是另一条线&lt;/h3&gt;
&lt;p&gt;主支持 PR 合并时，Qwen3.8-Flash-Next 自带的 MTP draft head 还没有完整接入。后续的 &lt;a href=&quot;https://github.com/ggml-org/llama.cpp/pull/27836&quot;&gt;PR #27836&lt;/a&gt; 和 &lt;a href=&quot;https://github.com/ggml-org/llama.cpp/pull/27842&quot;&gt;PR #27842&lt;/a&gt; 都在处理这件事，目前仍是开放/草稿状态。&lt;/p&gt;
&lt;p&gt;其中一个公开测试结果是在 M3 Max 128GB、&lt;code&gt;UD-IQ4_XS&lt;/code&gt; 上：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;配置&lt;/th&gt;
&lt;th&gt;接受率&lt;/th&gt;
&lt;th&gt;速度&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;无 speculation&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;27.43 tok/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--spec-draft-n-max 2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;89.2%&lt;/td&gt;
&lt;td&gt;37.22 tok/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--spec-draft-n-max 3&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;85.7%&lt;/td&gt;
&lt;td&gt;38.83 tok/s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;这是 PR 作者的测试结果，不是所有机器的承诺。但它说明了一个方向：&lt;strong&gt;模型发布之后，推理后端还会继续围绕速度、内存和 draft head 做很长一段工程工作。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;我现在会怎样评估一个新模型&lt;/h2&gt;
&lt;p&gt;以后遇到新模型，我会按下面这张表走，不再只看发布当天的榜单：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;检查项&lt;/th&gt;
&lt;th&gt;我想知道什么&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Release-to-runtime latency&lt;/td&gt;
&lt;td&gt;发布后多久进入 llama.cpp、vLLM、SGLang、MLX 等主流后端&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;最小可运行配置&lt;/td&gt;
&lt;td&gt;最小量化、最低 RAM/VRAM、是否需要特殊 offload&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;首 token 和持续解码&lt;/td&gt;
&lt;td&gt;短 prompt、长 prompt、不同 context 深度分别是多少&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;量化覆盖&lt;/td&gt;
&lt;td&gt;Q4、Q5、Q8 或动态量化是否都有可用版本&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;工具调用&lt;/td&gt;
&lt;td&gt;chat template、parser、参数 JSON 是否稳定&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;多模态&lt;/td&gt;
&lt;td&gt;mmproj、图片输入、图片 token 上限是否正常&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;后端差异&lt;/td&gt;
&lt;td&gt;CUDA、Metal、HIP、CPU 是否出现明显行为分叉&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;长任务稳定性&lt;/td&gt;
&lt;td&gt;连续几十轮、请求取消、OOM 后是否能恢复&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;并发&lt;/td&gt;
&lt;td&gt;两个以上请求是否能稳定工作&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;复现成本&lt;/td&gt;
&lt;td&gt;其他人能否按同一套命令复现结果&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;其中最重要的不是某一项的峰值，而是&lt;strong&gt;从模型权重到工作流之间的摩擦有多大&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;一个分数稍低、但当天就能被 llama.cpp、MLX 和一个 OpenAI 兼容 server 接住的模型，实际影响力可能超过一个榜单更高、但只能在官方 API 里调用的模型。因为前者已经进入了别人的程序、脚本和 Agent，后者还停留在产品页面里。&lt;/p&gt;
&lt;h2&gt;最后&lt;/h2&gt;
&lt;p&gt;Qwen3.8-Flash-Next 这次真正值得记住的，不是“6B 激活”或者某个横评标题，而是它让一条判断变得更清楚：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;开源模型的竞争，已经不只发生在训练和 benchmark 上，也发生在它多久能进入开发者已经在用的 runtime。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;llama.cpp&lt;/code&gt; day-0 支持，是这个模型生态成熟度的信号；后续 issue，则提醒我们别把“能加载”误写成“能生产”。&lt;/p&gt;
&lt;p&gt;我会继续看榜单，但以后会在旁边多记一列：&lt;strong&gt;release 到 runtime，花了几天。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;参考资料&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/QwenLM/Qwen3.8-Flash-Next&quot;&gt;Qwen3.8-Flash-Next 官方仓库&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://huggingface.co/Qwen/Qwen3.8-Flash-Next&quot;&gt;Qwen3.8-Flash-Next Hugging Face 模型卡&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://unsloth.ai/docs/models/qwen3.8-next&quot;&gt;Unsloth：Qwen3.8-Flash-Next 本地运行指南&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/ggml-org/llama.cpp/pull/27742&quot;&gt;&lt;code&gt;llama.cpp&lt;/code&gt; PR #27742：加入 qwen4exp 支持&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/ggml-org/llama.cpp/pull/27836&quot;&gt;&lt;code&gt;llama.cpp&lt;/code&gt; PR #27836：Qwen3.8-Flash-Next MTP draft head&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/ggml-org/llama.cpp/issues/27763&quot;&gt;&lt;code&gt;llama.cpp&lt;/code&gt; Issue #27763：SM110 offload 输出异常&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/ggml-org/llama.cpp/issues/27856&quot;&gt;&lt;code&gt;llama.cpp&lt;/code&gt; Issue #27856：HIP 长上下文速度下降&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://public.cohub.run/s/f7000115-55d8-4d97-a0e4-d2a55b6ffa41/genai-hot/2026-08-28/evidence/qwen3-8-flash-next/index.html&quot;&gt;OKP genai-hot：Qwen3.8-Flash-Next 热点证据&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>模型的新货架：从 Grok 4.6 上架 Pi 说起</title><link>https://bangwu.me/posts/pi-model-distribution/</link><guid isPermaLink="true">https://bangwu.me/posts/pi-model-distribution/</guid><description>xAI 官宣 Grok 4.6 可在 Pi 使用；DeepSeek Harness 的模型适配层底层就是 pi-ai。两件事指向同一个判断：agent 运行时正在成为模型的分发渠道。</description><pubDate>Sat, 22 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;先声明立场：我是 Pi 团队的人，这篇难免「利益相关」。但正因为在场内，有些观察比外面看得早一点——下面所有数据都是公开可查的。&lt;/p&gt;
&lt;h2&gt;先说结论&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;模型分发的主战场，正在从「上架应用商店」变成「进 agent 运行时」。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;过去一周有两件事让我对这个判断确定下来：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;8 月 13 日，xAI 官宣 Grok 4.6 可以在 Pi 使用。&lt;/strong&gt; 模型发布次日单独发一条平台上架公告——这个动作本身比那条帖子重要。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DeepSeek Harness 的模型适配层 &lt;code&gt;dsh-llm-pi-ai&lt;/code&gt;，底层就是我们的 &lt;code&gt;@earendil-works/pi-ai&lt;/code&gt;。&lt;/strong&gt; 这是我上一篇写 Harness 插件时亲手发现的：156k star 的仓库，模型接入的缝是用 Pi 的 SDK 铺的。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;一个是模型厂商把 Pi 当渠道来官宣，一个是框架作者把 Pi 当底座来依赖。方向一致：&lt;strong&gt;agent 运行时正在变成模型的货架。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;两件事的细节&lt;/h2&gt;
&lt;h3&gt;Grok 4.6 登陆 Pi&lt;/h3&gt;
&lt;p&gt;Grok 4.6 是 8 月 12 日发的，主打长程 agent、跨代码库分析和自我验证。首发帖累计 2500 万浏览、近 3 万赞。第二天，官方账号单独发了一条：「Grok 4.6 is now available in Pi」——10.4 万浏览、1273 赞。&lt;/p&gt;
&lt;p&gt;把这两条帖子的关系看清楚：&lt;strong&gt;模型发布是新闻，「在哪个运行时里可用」是后续剧情。&lt;/strong&gt; 剧情线还在继续：Perplexity 的 CEO 拿 Grok 4.6 当 orchestrator 跑了 Wide-And-Deep-Research 基准，结论是它在性能-成本曲线上位于帕累托前沿。注意这个用法——模型的评价场景已经从「聊天打分」迁移到「在某个运行时的编排下表现如何」。&lt;/p&gt;
&lt;h3&gt;pi-ai 在 Harness 的地基里&lt;/h3&gt;
&lt;p&gt;上一篇&lt;a href=&quot;/posts/deepseek-harness-plugin-tutorial/&quot;&gt;插件教程&lt;/a&gt;里我提过这事，这里补上量化的一面：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@earendil-works/pi-ai
周下载（8-15 ~ 8-21）：4,236,540
最新版本：0.84.2（8-14 发布）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;DeepSeek Harness 把它用作 LLM seam——README 原话是 &quot;Generic multi-provider adapter for the harness LLM seam backed by &lt;code&gt;@earendil-works/pi-ai&lt;/code&gt;&quot;。翻译一下：&lt;strong&gt;Harness 里每一次换模型、配 provider、接 OpenAI 兼容网关，底下跑的都是 pi-ai 的抽象。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这不是我们去找 DeepSeek 谈的合作，是他们选型选中的。被当成基础设施用，是对一个库最好的验证方式——比任何 benchmark 都硬。&lt;/p&gt;
&lt;h2&gt;为什么货架会易主&lt;/h2&gt;
&lt;p&gt;回头看技术产品史，分发渠道的迁移有迹可循：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;搜索引擎时代，&lt;strong&gt;浏览器默认搜索引擎&lt;/strong&gt;是渠道，Google 付费买位&lt;/li&gt;
&lt;li&gt;移动时代，&lt;strong&gt;App Store 上架位&lt;/strong&gt;是渠道，首发渠道能决定一款应用的命运&lt;/li&gt;
&lt;li&gt;云时代，**云市场（Marketplace）**是渠道，企业软件先上 AWS Marketplace 再谈销售&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;AI 时代的等价物正在显形。模型能力在快速同质化——DeepSeek 8 月三连发（V4-Pro → V4-Flash API → Vision-Exp）、GLM、Kimi、Qwen 都在月更节奏里，&lt;strong&gt;当供给过剩，渠道价值就上升&lt;/strong&gt;。而 token 消耗最大的场景已经不是聊天框，是 agent：一个长任务动辄几百次调用。模型厂商要抓住的，是这些调用的入口。&lt;/p&gt;
&lt;p&gt;入口在哪？就在运行时的那个模型选择器里。上一篇我在 dsh 的 Web UI 里把 provider 从 DeepSeek 切到自定义的 mock——&lt;strong&gt;那个下拉菜单就是货架&lt;/strong&gt;。用户在那个菜单里能看到谁、默认是谁、切换成本多高，决定了模型的实际市场份额。&lt;/p&gt;
&lt;p&gt;DeepSeek 自己看得最明白：低价 API 加自研 harness，模型和货架一手抓。这是官方版答案。而 xAI 选择的是另一条路——不自己造货架，把模型铺进别人已有的货架，Pi 是其中之一。&lt;/p&gt;
&lt;h2&gt;站在货架上是什么感觉&lt;/h2&gt;
&lt;p&gt;坦白讲，感受是复杂的。&lt;/p&gt;
&lt;p&gt;好的那一面：pi-ai 五个月做到周下载四百多万，说明「多 provider 抽象」这个判断做对了——开发者不想为每家模型写一遍适配，模型厂商也不想让集成成本挡住试用。&lt;/p&gt;
&lt;p&gt;不安的那一面：&lt;strong&gt;当一个库从产品变成公共契约，它的错误预算就没了。&lt;/strong&gt; 以前发个 0.x 版本改接口，坏的是自己的项目；现在底下压着 Harness 这样的框架和它们身后成千上万的会话，兼容性和版本语义就是别人的生产环境。dev preview 可以随便破兼容，底座不行——这也是我从 Harness 文档里读到 &quot;breaking changes expected&quot; 时格外有共鸣的原因：他们可以这么说，我们不能。&lt;/p&gt;
&lt;h2&gt;接下来会怎样&lt;/h2&gt;
&lt;p&gt;三个预测，放在这里等着验证：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;「available in X」会成为模型发布的标准动作。&lt;/strong&gt; X 是一张运行时清单，就像今天 App 官宣支持 iOS/Android 一样自然。下一个官宣登陆某个 agent 运行时的模型，不用等太久。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;运行时的竞争会从功能转向生态位。&lt;/strong&gt; 功能会被抄平，「谁的模型选择器里有谁」「谁是新模型的首发渠道」不会。Harness 开源五天 7174 个插件仓库抢的就是这个位置。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;模型评测表会多出一列：在哪些运行时可用。&lt;/strong&gt; 单独的分数不够了，编排质量正在成为模型表现的一部分——Perplexity 那个 orchestrator 实测就是预演。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;对我们自己，结论也简单：渠道不是终点。&lt;strong&gt;被依赖就要配得上依赖&lt;/strong&gt;——接下来 pi-ai 的每个版本号，都是对下游的一份承诺。&lt;/p&gt;
&lt;h2&gt;参考资料&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://x.com/SpaceXAI/status/20879636186&quot;&gt;xAI：Grok 4.6 is now available in Pi&lt;/a&gt;（数据来自 OKP genai-hot 追踪记录）&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/deepseek-ai/deepseek-harness/tree/master/packages/llm/llm-pi-ai&quot;&gt;dsh-llm-pi-ai README&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.npmjs.com/package/@earendil-works/pi-ai&quot;&gt;@earendil-works/pi-ai on npm&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;本系列前两篇：&lt;a href=&quot;/posts/deepseek-harness/&quot;&gt;Hi, deepseek-harness!&lt;/a&gt; / &lt;a href=&quot;/posts/deepseek-harness-plugin-tutorial/&quot;&gt;给 DeepSeek Harness 写插件&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>给 DeepSeek Harness 写插件：从 hello-plugin 到一条能力缝</title><link>https://bangwu.me/posts/deepseek-harness-plugin-tutorial/</link><guid isPermaLink="true">https://bangwu.me/posts/deepseek-harness-plugin-tutorial/</guid><description>实测跑通官方插件开发教程：写一个真有用的 web_fetch 工具，用 mock LLM 跑出真实调用轨迹（含截图），外加一个关于 Pi 的彩蛋。</description><pubDate>Tue, 18 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;先说结论&lt;/h2&gt;
&lt;p&gt;上一篇我把 Harness 关了，因为它的 Web UI 不是我的菜；但「一切皆插件」这个架构我认。这篇把官方插件教程（basic → framework → practice）从头跑了一遍，而且没停在 hello-world：&lt;strong&gt;我写了一个真正有用的 &lt;code&gt;web_fetch&lt;/code&gt; 工具插件，并用官方 mock LLM 跑出了真实的调用轨迹，截图为证。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;给 dsh 写插件的门槛比想象低：&lt;strong&gt;一个导出 &lt;code&gt;apply(ctx)&lt;/code&gt; 的 TypeScript 函数就是完整的插件。&lt;/strong&gt; 真正要掌握的其实不是 API，而是三个心智模型：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;一切注册皆可逆&lt;/strong&gt;——通过 &lt;code&gt;ctx&lt;/code&gt; 注册的东西，卸载时自动回收&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;依赖驱动加载&lt;/strong&gt;——&lt;code&gt;inject&lt;/code&gt; 声明依赖，加载顺序由框架推导&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;能力缝 = 定义 + 提供者 + 消费者&lt;/strong&gt;——可替换的能力边界&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;另外有个彩蛋放最后：&lt;strong&gt;dsh 的多模型适配层，底层就是 Pi（&lt;code&gt;@earendil-works/pi-ai&lt;/code&gt;）。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;文中所有命令和输出都是我 8 月 18 号在 Node v24.18 + pnpm 11.7 上实跑的，包括一个真实的报错和一次真实的踩坑。&lt;/p&gt;
&lt;h2&gt;准备：从源码跑&lt;/h2&gt;
&lt;p&gt;用 &lt;code&gt;npx @deepseek-ai/dsh web&lt;/code&gt; 跑起来容易，但挂本地插件需要 &lt;code&gt;--patch&lt;/code&gt; 指向本地文件，从源码检出最顺：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git clone https://github.com/deepseek-ai/deepseek-harness
cd deepseek-harness
pnpm install
pnpm run build
pnpm dsh web    # 开发模式，直接跑 TS 源码
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;要求 Node &lt;code&gt;^22.19 || &amp;gt;=24&lt;/code&gt;。比我预想的轻：shallow clone 86M，install 约 21s，build 约 3s。&lt;/p&gt;
&lt;h2&gt;第一个插件：hello-plugin&lt;/h2&gt;
&lt;p&gt;插件就是一个导出 &lt;code&gt;apply&lt;/code&gt; 函数的 TS 模块。官方教程的原话：&lt;strong&gt;「这就是完整配置」&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// scratch-plugin/src/my-plugin.ts
import type { Context } from &apos;@deepseek-ai/cordis&apos;

export const name = &apos;hello-plugin&apos;

export function apply(ctx: Context) {
  console.log(&apos;[hello-plugin] plugin loaded!&apos;)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后写一个 patch 层，把它插进启动树。插件路径必须是绝对路径：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# scratch-plugin/cordis.yml
- insert:
    - id: hello
      name: &apos;/绝对路径/deepseek-harness/scratch-plugin/src/my-plugin.ts&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;启动时用 &lt;code&gt;--patch&lt;/code&gt; 挂上：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pnpm dsh web --patch ./scratch-plugin/cordis.yml
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;实测输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ node --import tsx/esm apps/cli/src/bin.ts web --patch ./scratch-plugin/cordis.yml
[hello-plugin] plugin loaded!
dsh web: http://127.0.0.1:3080
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一次通过。这里的关键概念是 &lt;strong&gt;patch 层叠&lt;/strong&gt;：不 fork、不改源码，你的插件是「挂到旁边」的，卸掉它整个框架回到原样。&lt;/p&gt;
&lt;h2&gt;生命周期：为什么不用写清理代码&lt;/h2&gt;
&lt;p&gt;Cordis 里每个插件是一个 Fiber，状态机长这样：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;PENDING → LOADING → ACTIVE
                 ↘ FAILED
ACTIVE → UNLOADING → DISPOSED
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;三个实际含义：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;依赖没就绪就不加载&lt;/strong&gt;。&lt;code&gt;inject: [&apos;tools&apos;]&lt;/code&gt; 声明的服务消失时插件自动卸载，服务回来自动重载——热替换 provider 不需要手动重启任何东西&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;注册自动回收&lt;/strong&gt;。&lt;code&gt;ctx.on()&lt;/code&gt; 的事件监听、&lt;code&gt;ctx.tools.register()&lt;/code&gt; 的工具，卸载时全部自动撤销，不需要 &lt;code&gt;removeListener&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;需要显式清理的资源用 &lt;code&gt;ctx.effect&lt;/code&gt;&lt;/strong&gt;，返回一个 disposer：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;export function apply(ctx: Context) {
  ctx.effect(() =&amp;gt; {
    const timer = setInterval(() =&amp;gt; console.log(&apos;heartbeat&apos;), 5000)
    return () =&amp;gt; clearInterval(timer)   // 插件卸载时执行
  })
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;三种插件形态：&lt;strong&gt;函数&lt;/strong&gt;（多数场景够用）、&lt;strong&gt;对象&lt;/strong&gt;（带 name/inject/apply 的字面量）、&lt;strong&gt;类&lt;/strong&gt;（继承 &lt;code&gt;Service&lt;/code&gt;，当你需要给别的插件提供 service 时用）。新手从函数形式开始就行。&lt;/p&gt;
&lt;h2&gt;一个有实际意义的工具：web_fetch&lt;/h2&gt;
&lt;p&gt;hello-plugin 只会打日志。教学意义够了，但我要一个模型真的能用的东西。dsh 基础包不带网页抓取工具，而「读一个 URL」是 agent 的高频需求——于是写了 &lt;code&gt;web_fetch&lt;/code&gt;：抓页面、剥 HTML、抽正文，纯 Node 内置能力，零依赖、不要 API key：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// scratch-plugin/src/web-fetch.ts
import { defineTool } from &apos;@deepseek-ai/dsh-tools&apos;

export const name = &apos;web-fetch-plugin&apos;
export const inject = [&apos;tools&apos;]

const MAX_CHARS = 6000

function stripHtml(html: string): { title: string; text: string } {
  const titleMatch = html.match(/&amp;lt;title[^&amp;gt;]*&amp;gt;([\s\S]*?)&amp;lt;\/title&amp;gt;/i)
  const title = titleMatch ? titleMatch[1].replace(/\s+/g, &apos; &apos;).trim() : &apos;&apos;
  const body = html
    .replace(/&amp;lt;script[\s\S]*?&amp;lt;\/script&amp;gt;/gi, &apos; &apos;)
    .replace(/&amp;lt;style[\s\S]*?&amp;lt;\/style&amp;gt;/gi, &apos; &apos;)
    .replace(/&amp;lt;[^&amp;gt;]+&amp;gt;/g, &apos; &apos;)
  const text = body
    .replace(/&amp;amp;nbsp;/g, &apos; &apos;).replace(/&amp;amp;amp;/g, &apos;&amp;amp;&apos;)
    .replace(/&amp;amp;lt;/g, &apos;&amp;lt;&apos;).replace(/&amp;amp;gt;/g, &apos;&amp;gt;&apos;)
    .replace(/&amp;amp;quot;/g, &apos;&quot;&apos;).replace(/&amp;amp;#39;/g, &quot;&apos;&quot;)
    .replace(/\s+/g, &apos; &apos;).trim()
  return { title, text }
}

export function apply(ctx: any) {
  ctx.tools.register(defineTool({
    name: &apos;web_fetch&apos;,
    description: &apos;Fetch a web page and return its title and readable text content (truncated to &apos; + MAX_CHARS + &apos; chars).&apos;,
    parameters: {
      url: { type: &apos;string&apos;, required: true },
    },
    output: {
      schema: {
        type: &apos;object&apos;,
        properties: {
          title: { type: &apos;string&apos; },
          url: { type: &apos;string&apos; },
          chars: { type: &apos;number&apos; },
          truncated: { type: &apos;boolean&apos; },
          text: { type: &apos;string&apos; },
        },
        additionalProperties: false,
      },
      render: (_args, value) =&amp;gt; [{ type: &apos;text&apos;,
        text: `## ${value.title}\n${value.text}` }],
    },
    async execute(args) {
      const resp = await fetch(args.url, {
        headers: { &apos;user-agent&apos;: &apos;dsh-web-fetch-plugin/0.1 (+educational example)&apos; },
        signal: AbortSignal.timeout(15000),
      })
      if (!resp.ok) throw new Error(`HTTP ${resp.status} for ${args.url}`)
      const html = await resp.text()
      const { title, text } = stripHtml(html)
      const truncated = text.length &amp;gt; MAX_CHARS
      return {
        title, url: args.url, chars: text.length, truncated,
        text: truncated ? text.slice(0, MAX_CHARS) + &apos;…&apos; : text,
      }
    },
  }))
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;挂进同一个 patch 层：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- insert:
    - id: hello
      name: &apos;/绝对路径/scratch-plugin/src/my-plugin.ts&apos;
    - id: web-fetch
      name: &apos;/绝对路径/scratch-plugin/src/web-fetch.ts&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;这里我踩了个真坑。&lt;/strong&gt; 第一版我把 &lt;code&gt;output.schema&lt;/code&gt; 写成了 &lt;code&gt;parameters&lt;/code&gt; 那种属性 map（&lt;code&gt;{ total: { type: &apos;number&apos; }, ... }&lt;/code&gt;），启动直接报错：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Error: dsh: plugin tree failed to load: ... unsupported JSON schema:
schema.type must be string/number/integer/boolean/null/array/object/json, or use oneOf
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;原因：&lt;code&gt;output.schema&lt;/code&gt; 是单个 &lt;code&gt;ValueSchemaSpec&lt;/code&gt;，必须把 &lt;code&gt;type: &apos;object&apos;&lt;/code&gt; 和 &lt;code&gt;properties&lt;/code&gt; 包进去，而不是直接写属性。这个报错的好处是&lt;strong&gt;精准定位到 loader 条目、给出正确枚举&lt;/strong&gt;——框架在 apply 阶段就拦住坏 schema，而不是等模型调用时炸。错误信息质量是框架成熟度的好指标，dsh 这关过了。&lt;/p&gt;
&lt;h2&gt;没有 API key，怎么验证工具真能被调用&lt;/h2&gt;
&lt;p&gt;插件加载成功 ≠ 模型会用。我没有 DeepSeek API key，但官方仓库自带一个&lt;strong&gt;可脚本化的 mock LLM server&lt;/strong&gt;，能指定它「假装调某个工具」：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pnpm mock:llm --port 8000 \
  --sequence tool_call_success,success --repeat-last \
  --tool-name web_fetch \
  --tool-arguments &apos;{&quot;url&quot;:&quot;https://example.com/&quot;}&apos; \
  --success-text &apos;我已经读取了该页面，标题是 Example Domain，内容是一个示例域名说明。&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;它暴露 OpenAI 兼容接口。然后给 dsh 配一个自定义 provider——写进 &lt;code&gt;$DSH_HOME/settings.yaml&lt;/code&gt;，不用点 UI：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;llm-pi-ai:
  providers:
    mock:
      api: openai-completions
      baseURL: http://127.0.0.1:8000/v1
      apiKeyEnv: MOCK_API_KEY
      models:
        - id: mock-model
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;启动 Web UI（&lt;code&gt;MOCK_API_KEY=abc123&lt;/code&gt; 顺手设上），在模型选择器里切到 &lt;code&gt;mock-model&lt;/code&gt;：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.bangwu.me/img/20260818-dsh-plugin-model-list.webp&quot; alt=&quot;模型列表里出现了自定义 provider mock 和 mock-model&quot; /&gt;&lt;/p&gt;
&lt;p&gt;然后开一个会话，发一句「帮我看看 https://example.com 这个页面讲了什么」。mock 模型按脚本调用了 &lt;code&gt;web_fetch&lt;/code&gt;，真实轨迹长这样：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.bangwu.me/img/20260818-dsh-plugin-trajectory.webp&quot; alt=&quot;轨迹视图：Context injection → Fetch 工具调用 → 最终回答，下方是 turn/step/token 统计&quot; /&gt;&lt;/p&gt;
&lt;p&gt;轨迹里能看到三件事：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Context injection&lt;/strong&gt;（system prompt 注入）被显式记录——「模型看到的都要进日志」这条原则是实打实的&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Fetch · https://example.com/&lt;/code&gt; 工具调用卡片，参数、耗时都在&lt;/li&gt;
&lt;li&gt;底部统计：&lt;code&gt;1 turn · 2 steps · TTFT 0.1s · Input 6 tok · Output 43 tok&lt;/code&gt;——turn/step/token 三层度量，调试 agent 时这比什么都直观&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;一个发现顺便说：&lt;strong&gt;dsh 的 LLM 适配包叫 &lt;code&gt;dsh-llm-pi-ai&lt;/code&gt;&lt;/strong&gt;。点进去看，它「backed by &lt;code&gt;@earendil-works/pi-ai&lt;/code&gt;」——就是 Pi 的多 provider SDK。也就是说 DeepSeek Harness 的模型接入层，底层用的是我们自己做的 Pi。这也是 Pi 作者会公开聊 Harness 的原因之一：看到自己维护的库被塞进一周 15 万 star 的项目里当底座，很难忍住不评论。&lt;/p&gt;
&lt;h2&gt;进阶：三角色能力缝&lt;/h2&gt;
&lt;p&gt;官方 practice 的核心概念，也是「可替换」真正落地的设计：一个通用能力拆成三个角色。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌─────────────┐     ┌──────────────────┐     ┌──────────────┐
│  dsh-shell   │────▶│  dsh-bash-local  │     │ dsh-tool-bash│
│ (定义)       │     │   (提供者)        │     │ (消费者/工具) │
└─────────────┘     └──────────────────┘     └──────────────┘
       ▲                                            │
       └────────────────────────────────────────────┘
                    inject: [&apos;shell&apos;]
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Service Definition&lt;/strong&gt;（如 &lt;code&gt;dsh-shell&lt;/code&gt;）：定义 service 接口和 Request/Result 类型，很少变&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Service Provider&lt;/strong&gt;（如 &lt;code&gt;dsh-bash-local&lt;/code&gt;）：具体实现，可独立演进、可替换&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Consumer&lt;/strong&gt;（如 &lt;code&gt;dsh-tool-bash&lt;/code&gt;）：把能力暴露成模型可调的工具&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;三者的依赖是单向的：Provider 和 Consumer 都只依赖 Definition，&lt;strong&gt;彼此不依赖&lt;/strong&gt;。换一个执行后端（比如把本地 bash 换成远程沙箱），Definition 和工具都不用动，只换 Provider 那一行配置。官方 Bash 能力就是这么拆的，Windows 上还有一套 pwsh 的平行实现，同一份 patch 按平台 gate。&lt;/p&gt;
&lt;p&gt;官方教程里有一段完整的三件套代码（&lt;code&gt;MyCapService&lt;/code&gt; / &lt;code&gt;MyCapLocal&lt;/code&gt; / &lt;code&gt;tool-my-cap&lt;/code&gt;），结构很干净，值得照抄一遍。设计要点就三条：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;不要过早拆分&lt;/strong&gt;。一个简单工具就是一个函数插件（比如本文的 web_fetch）；只有角色需要独立演化时才分三个包&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Definition 拥有 Request/Result 类型&lt;/strong&gt;，别的角色只依赖它&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;显式 &amp;gt; 隐式&lt;/strong&gt;：默认值放进显式的 &lt;code&gt;resolve()&lt;/code&gt; 步骤，别在 &lt;code&gt;run()&lt;/code&gt; 里藏 &lt;code&gt;?? default&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;生态已经起来了&lt;/h2&gt;
&lt;p&gt;上一篇结尾我说「下次它出 TUI 的时候，我会再花十分钟」。结果社区已经有人写了 &lt;strong&gt;dsh-TUI&lt;/strong&gt;——Claude Code 风格的全屏终端 UI。这十分钟看来得提前花。&lt;/p&gt;
&lt;p&gt;几个实打实的数据（8-18 查的）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;GitHub &lt;code&gt;dsh-plugin&lt;/code&gt; topic 下 &lt;strong&gt;7174 个仓库&lt;/strong&gt;——发布五天，这是很夸张的生态速度&lt;/li&gt;
&lt;li&gt;&lt;code&gt;awesome-dsh-plugin&lt;/code&gt; 精选清单 20 个分类（UI、模型接入、记忆、工具、浏览器、语音、安全……）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;dsh-market 插件市场&lt;/strong&gt;：&lt;code&gt;dsh plugin --profile web add dshmarket&lt;/code&gt;，应用内一键装/升级插件、换主题&lt;/li&gt;
&lt;li&gt;官方分发格式是 &lt;code&gt;dsh.bundle&lt;/code&gt; manifest，&lt;code&gt;dsh plugin&lt;/code&gt; 命令底层直接转发给 pnpm&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;还有一条必须说的安全线。装插件 = &lt;strong&gt;让第三方代码以你自己的权限跑在本机&lt;/strong&gt;，能读你的文件、用你的凭证。工具审批（approval）管的是模型调工具，&lt;strong&gt;管不住插件代码本身&lt;/strong&gt;。连 awesome 清单自己都写了：上榜不代表安全审查，装之前看源码，别在存着 key 的机器上试来路不明的插件。&lt;/p&gt;
&lt;h2&gt;我的建议&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;先 patch 配置，再写插件&lt;/strong&gt;。&lt;code&gt;--dump-config&lt;/code&gt; 看一眼启动树，很多定制只是换一行配置的事，根本不用写代码&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;简单工具一个函数插件就够&lt;/strong&gt;，不要预拆三角色&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;dev preview 会破兼容&lt;/strong&gt;：接口边界放在 service definition 里，Consumer 别直接依赖 Provider 的实现&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;没 key 也能开发&lt;/strong&gt;：官方 mock LLM 能脚本化工具调用，写工具插件时配合它做轨迹验证，比真模型更快更稳&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;插件放在独立机器或容器里试&lt;/strong&gt;，尤其是带凭证的环境&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;参考资料&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/deepseek-ai/deepseek-harness/tree/master/docs/user/develop&quot;&gt;官方教程：第一个插件 / 生命周期 / 三角色能力设计&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/architecture.md&quot;&gt;架构文档&lt;/a&gt; / &lt;a href=&quot;https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/cordis-primer.md&quot;&gt;Cordis Primer&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/awesome-dsh-plugin/awesome-dsh-plugin&quot;&gt;awesome-dsh-plugin 精选清单&lt;/a&gt; / &lt;a href=&quot;https://github.com/dsh-market/dsh-market&quot;&gt;dsh-market 插件市场&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;上一篇：&lt;a href=&quot;/posts/deepseek-harness/&quot;&gt;Hi, deepseek-harness!&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Hi, deepseek-harness!</title><link>https://bangwu.me/posts/deepseek-harness/</link><guid isPermaLink="true">https://bangwu.me/posts/deepseek-harness/</guid><description>Deepseek harness 近期发布，来尝尝味</description><pubDate>Mon, 17 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;DeepSeek 于 2026-08-13 发布 Harness v0.1 开发者预览版，MIT 许可证开源&lt;/p&gt;
&lt;p&gt;::github{repo=&quot;deepseek-ai/deepseek-harness&quot;}&lt;/p&gt;
&lt;p&gt;期待已久的 DeepSeek 官方 Harness 终于来了，我却体验不到十分钟就放置了（也许是我习惯了现有的 coding agent 的使用，目前依然是偏向 CLI）我们也有自己的基于 Pi 的 web UI 云端 agent——&lt;a href=&quot;https://cohub.run&quot;&gt;cohub&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;先声明一句：下面是个人体验 + 架构速读，不是评测。Harness 现在还是 developer preview，官方 README 自己都写了「未来将出现破坏兼容性的变更」，所以这篇写的是 2026-08-17 这一周的它。&lt;/p&gt;
&lt;h2&gt;先把事实摆一下&lt;/h2&gt;
&lt;p&gt;DeepSeek Harness（命令是 &lt;code&gt;dsh&lt;/code&gt;）不是新模型，是一个 agent harness。官方主页把它概括成一句话：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Agent = Model + Harness&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;模型是灵魂，harness 是让 agent 认识环境、调用工具、在真实环境里持续干活的那一层。这层以前是各家 coding agent 各做各的，现在 DeepSeek 自己下场开源了一个。&lt;/p&gt;
&lt;p&gt;发布一周的数据很夸张：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;GitHub 五天 15 万+ stars（我 8 月 18 号查 API 是 156,653，这个增速本身就少见）&lt;/li&gt;
&lt;li&gt;官方 X 帖 19,000+ 赞、近 400 万浏览，HN 731 分&lt;/li&gt;
&lt;li&gt;知乎热榜第一挂了 253 个回答，小红书出现成体系教程（「从 0 开始成为高手」这种）&lt;/li&gt;
&lt;li&gt;Ollama 社区已经在搞 &lt;code&gt;ollama launch dsh&lt;/code&gt;，Reddit 有人拿 Qwen3.8-27B 塞进去实测，X 上有单卡 4090 48GB 跑 Q8 约 100 tokens/s 的帖子&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;生态第一波反应速度比很多模型发布都猛。装起来也确实快：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npx @deepseek-ai/dsh web
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Node 环境有的话一条命令，浏览器打开 &lt;code&gt;127.0.0.1:3080&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;十分钟之后我把它关了&lt;/h2&gt;
&lt;p&gt;流程是这样的：起来之后是个 Web UI，先去 Settings 里填 DeepSeek API key，然后「Choose workspace」选一个项目目录，之后才能开会话。中间每一步都顺，没有报错，就是——&lt;/p&gt;
&lt;p&gt;我是在终端里干活的人。我的日常是 Claude Code / Codex 这类 CLI 工具，一个命令进目录，直接开始改代码。Harness 第一版只有两个官方 profile：&lt;code&gt;web&lt;/code&gt; 和 &lt;code&gt;headless&lt;/code&gt;（headless 是跑一次性任务，打印结果退出）。没有官方 TUI。于是十分钟里我的注意力被「配 key、选 workspace、认识界面」吃掉了，还没走到真正干活那一步，就关掉了。&lt;/p&gt;
&lt;p&gt;说实话，关掉它的时候我心里清楚：这不是它的问题，是我已经不是它的第一目标用户了。v0.1 明显是 web-first 的产品节奏，先让用户能在浏览器里看见轨迹、看 Trajectory 视图、点 fork/resume。这个选择对「想让更多人先上手」是对的，只是恰好跟我的肌肉记忆反着。&lt;/p&gt;
&lt;p&gt;但我放下它，不是因为觉得它没东西。恰恰相反，睡前我又把它的架构文档读了一遍，然后觉得这事值得写下来。&lt;/p&gt;
&lt;h2&gt;它的架构是「真插件化」&lt;/h2&gt;
&lt;p&gt;「Everything is a plugin」这种话每家都在喊，但 Harness 是少数真的把这句话落到加载器层面的。底层是一个叫 Cordis 的元框架（DeepSeek 自己维护，还配了一篇论文），几个关键设计：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;模型、工具、技能、会话、沙箱、存储、loop、调度、UI，全是插件。&lt;/strong&gt; 官方原文列的就是这些，包括 agent loop 本身。没有「特权核心」——session log 可以换，agent loop 可以换，你是在旁边挂一个新插件，而不是 fork 仓库改源码。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;注册是可逆副作用。&lt;/strong&gt; 插件通过 &lt;code&gt;ctx.effect()&lt;/code&gt; 挂注册，卸载时自动回滚。所以插件可以热加载、热卸载，不会像很多框架那样卸了还留一串幽灵 handler。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;依赖通过注入声明。&lt;/strong&gt; 插件声明 &lt;code&gt;inject&lt;/code&gt; 它需要的服务（&lt;code&gt;ctx.tools&lt;/code&gt;、&lt;code&gt;ctx.llm&lt;/code&gt;、&lt;code&gt;ctx.sessions&lt;/code&gt;），加载顺序由依赖关系推导，而不是手工写 boot 顺序。事件分 emit / waterfall / parallel / serial 四种派发模式，写在类型系统里。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;组合靠 patch 层叠。&lt;/strong&gt; 一次启动是这么叠出来的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dsh-base（模型适配/工具/持久化/沙箱/审批/遥测）
  → dsh-web-app（浏览器应用）或 dsh-headless（一次性 runner）
  → profile 自己的 cordis.patch.yml
  → home 级 cordis.patch.yml
  → --patch 覆盖层
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;dsh --dump-config&lt;/code&gt; 能看到你这台机器实际 boot 出来的整棵树，树里任何一行都可以用你自己的 patch 换掉。这个设计对「个人魔改」和「企业定制」是同一个答案：不用改源码，改配置。&lt;/p&gt;
&lt;p&gt;另外两个我觉得认真的地方：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Every run is traceable。&lt;/strong&gt; 模型看到的一切都进 append-only session log：system prompt、推理、工具调用与结果、子 agent 调度、上下文注入。Trajectory 视图按来源检视，fork / resume / replay 都基于同一条事件流。这个对调试 agent 比什么指标都有用。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;四种 runtime mode。&lt;/strong&gt; Standard（全工具集）、Code（用模型生成的代码编排多轮工具调用）、Minimal（只剩 shell + 文件编辑器，拿来 benchmark 模型）、Creator（在内存里试插件）。最后一个模式说明他们想清楚了自己的开发者是谁。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;我真正在意的：harness 层开始卷了&lt;/h2&gt;
&lt;p&gt;这件事比「DeepSeek 出了个 coding agent」重要。它意味着 harness 从一个各家私有的实现细节，变成一个公开的、有官方玩家的层。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;DeepSeek 的模型价格低（V4 Flash 公测那波降价很凶），harness 是模型的分发通道——低价的模型 + 好用的运行时，这个组合拳才是完整的&lt;/li&gt;
&lt;li&gt;插件生态就是第二个分发通道：模型厂商、工具厂商、服务商都能通过插件进来，&lt;code&gt;dsh-plugin&lt;/code&gt; topic 已经在运转，社区插件聚合站也出现了&lt;/li&gt;
&lt;li&gt;社区验证了它的开放性：Qwen 模型、Ollama、J-Space（社区 harness，有人测出 Terminal Bench 87.9→90.1，社区基准非官方）都挂上去了&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;当然，也有实打实的风险：dev preview 阶段 API 会破，star 数不代表留存，插件生态要等真实的开发者留存才能判断。X 上有条评论我印象很深，大意是「插件化解决了生命周期和副作用，但拓扑不是语义——换工具容易，决定什么该重试、什么该升级给人，才是 harness 的难点」。我认同这个说法。这也是为什么我虽然十分钟就关了它，还是会把它的 repo 和社区 keep 在视野里。&lt;/p&gt;
&lt;h2&gt;和 cohub 的关系&lt;/h2&gt;
&lt;p&gt;disclosure：cohub 是我们公司（Viscept）的产品，基于 Pi 做的云端 agent，web UI。有人可能觉得「你们也是 web UI，怎么评价人家的 web UI」——正因为我们也在这条路上，我才更清楚 web-first 和 local-first 是两条不同的产品路径：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Harness 是 local-first：你的代码、key、日志都在本机，装好即用，隐私边界清楚&lt;/li&gt;
&lt;li&gt;cohub 是云端的：环境在远端，从浏览器就能进，不用管本机依赖&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;两条路径都成立，目标用户有重叠但不完全一样。Harness 出来那天，Pi 的作者也在 X 上公开聊过（686 赞的那条），态度是欢迎的——harness 层有更多人认真做，对所有做 agent 产品的人都是好事。&lt;/p&gt;
&lt;h2&gt;最后&lt;/h2&gt;
&lt;p&gt;我现在的态度很简单：第一版还轮不到我日常用，但它把「一切皆插件」当真了，并且把可追踪性做成了运行时约束而不是宣传词。这两点，我会继续盯着。&lt;/p&gt;
&lt;p&gt;下次它出 TUI 的时候，我会再花十分钟。&lt;/p&gt;
&lt;h2&gt;参考资料&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://deepseek.com/harness/en/&quot;&gt;DeepSeek Harness 官方主页&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/deepseek-ai/deepseek-harness&quot;&gt;deepseek-harness GitHub&lt;/a&gt;（README / 架构文档 / CLI 文档）&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/cordiverse/paper&quot;&gt;Cordis 论文：A Programming Paradigm for Spatiotemporal Composability&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://public.cohub.run/s/f7000115-55d8-4d97-a0e4-d2a55b6ffa41/genai-hot/2026-08-18/evidence/deepseek-harness-v0-1/index.html&quot;&gt;OKP 证据页：DeepSeek Harness v0.1 热度追踪&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://reddit.com/r/LocalLLaMA/comments/1vpv12b/qwen_38_27b_with_dshdeepseek_harness_is_amazing/&quot;&gt;Reddit：Qwen3.8-27B + DSH 实测帖&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.zhihu.com/question/2071335529577239300&quot;&gt;知乎：Harness 评价讨论&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Cohub 实操：用一个 Space 从零搭一个可发布的小工具</title><link>https://bangwu.me/posts/cohub-hands-on/</link><guid isPermaLink="true">https://bangwu.me/posts/cohub-hands-on/</guid><description>上次介绍了 Cohub 是什么，这次直接上手：从创建 Space、和 Agent 协作写代码，到发布成公开 Work，走完一个真实可用的闭环。</description><pubDate>Thu, 06 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;disclosure：这是我们公司（Viscept）做的产品，这篇是实操向，我会讲清楚每一步在干什么、有什么坑。&lt;/p&gt;
&lt;h2&gt;先说结论&lt;/h2&gt;
&lt;p&gt;如果你想试 Cohub，最快的入门路径不是看文档，而是&lt;strong&gt;走一遍真实的闭环&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;创建一个 Space → 和 Agent 说需求 → 它建文件/装依赖/跑起来 → Save 存 Checkpoint → 发布成 Work
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;整个过程大概 20 分钟。这篇文章记录我实际跑通的步骤，包括遇到的坑。&lt;/p&gt;
&lt;h2&gt;第一步：创建 Space&lt;/h2&gt;
&lt;p&gt;Cohub 的核心单元是 Space。你可以把它理解成「一个隔离的开发环境 + 对话 + 文件系统 + Agent」。&lt;/p&gt;
&lt;p&gt;用 CLI 创建（&lt;code&gt;cohub&lt;/code&gt; 是官方 CLI）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 安装（如果你还没有）
npm install -g @neta-art/cohub-cli

# 登录
cohub auth login

# 列出你的 spaces
cohub spaces ls

# 创建新 space
cohub spaces create --name &quot;my-first-tool&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或者直接在 web 上点 New Space 也行。我建议至少 CLI 装一个，因为后面很多操作 CLI 更快。&lt;/p&gt;
&lt;h2&gt;第二步：和 Agent 提需求&lt;/h2&gt;
&lt;p&gt;创建之后，打开 Space，跟 Agent 说你要做什么。关键是要&lt;strong&gt;把需求讲清楚&lt;/strong&gt;，就像跟一个工程师同事提需求：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;帮我做一个 Markdown 转 HTML 的小工具，输入一段 markdown，输出渲染后的 HTML。要能选样式主题，还要有一个预览区域。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;注意几个点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;说目标，不说实现&lt;/strong&gt;。不需要告诉它用什么库，它会自己选。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;一次一个需求&lt;/strong&gt;。第一轮先让它把核心功能做出来，再迭代加功能。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;它可以读写文件、跑命令&lt;/strong&gt;。Agent 不是只回复文本，它会在 Space 的文件系统里创建文件、装依赖、跑 dev server。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;第三步：看它干活，必要时纠偏&lt;/h2&gt;
&lt;p&gt;Agent 开始干活后，你会看到它：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;创建项目文件（&lt;code&gt;package.json&lt;/code&gt;、源码等）&lt;/li&gt;
&lt;li&gt;跑 &lt;code&gt;npm install&lt;/code&gt; 装依赖&lt;/li&gt;
&lt;li&gt;启动 dev server，暴露预览端口&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这里有个经验：&lt;strong&gt;Agent 第一次做出来的东西，大概率不是你要的最终版&lt;/strong&gt;。正常的迭代节奏是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;第一轮：核心功能能用
第二轮：你试了，提反馈（&quot;样式太丑&quot;&quot;这个按钮没反应&quot;）
第三轮：它改，你再试
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 Cohub 里，你可以在&lt;strong&gt;同一个 Space 的预览区&lt;/strong&gt;直接试它做出来的东西，不用自己本地跑。这个循环比「聊天里来回贴代码」高效得多。&lt;/p&gt;
&lt;h2&gt;第四步：Save（Checkpoint）&lt;/h2&gt;
&lt;p&gt;当你对当前状态满意（或者到了一个里程碑），存一个 Checkpoint：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cohub -s &amp;lt;space-id&amp;gt; spaces checkpoints create --message &quot;markdown-to-html v1 done&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为什么这个重要：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;它是不可变快照&lt;/strong&gt;。之后再怎么改，都能回到这个点。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;可以 fork&lt;/strong&gt;。从这个 Checkpoint 分支出新想法，不影响主线。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;是可分享的稳定基底&lt;/strong&gt;。别人可以基于你的 Checkpoint 继续工作。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这比 git commit 更像「存档」——你随时能回到任何有意义的时刻。&lt;/p&gt;
&lt;h2&gt;第五步：发布成 Work&lt;/h2&gt;
&lt;p&gt;这是 Cohub 和大多数 AI 工具最大的区别：&lt;strong&gt;Agent 的产出可以直接发布成一个公开可访问的页面（Work）&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 把项目构建成静态文件后发布
cohub -s &amp;lt;space-id&amp;gt; spaces files ls

# 发布目录为 Work
cohub -s &amp;lt;space-id&amp;gt; works publish my-tool --file dist/index.html
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;发布后你会得到一个公开 URL，任何人打开就能用你做的工具。这个 Work 还可以：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;配置访问权限&lt;/li&gt;
&lt;li&gt;用 Work Commerce 变现（卖功能解锁/积分）&lt;/li&gt;
&lt;li&gt;被 fork 和 remix&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;实际遇到的坑&lt;/h2&gt;
&lt;h3&gt;坑 1：Agent 装依赖慢&lt;/h3&gt;
&lt;p&gt;第一次让 Agent 跑 &lt;code&gt;npm install&lt;/code&gt;，如果网络慢会等很久。解决办法：&lt;strong&gt;让它用国内镜像&lt;/strong&gt;，或者先用最简依赖把核心跑通，再逐步加。&lt;/p&gt;
&lt;h3&gt;坑 2：端口预览 vs 实际发布&lt;/h3&gt;
&lt;p&gt;开发时 Agent 起的 dev server 端口，和发布后的静态托管&lt;strong&gt;不是一回事&lt;/strong&gt;。如果你做的是 SPA 或需要后端的应用，发布时要注意：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;纯前端 → 构建后发布 &lt;code&gt;dist/&lt;/code&gt; 即可&lt;/li&gt;
&lt;li&gt;需要后端 → 用 Cohub 的 Sandbox 能力或对外部 API&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;坑 3：迭代时别丢上下文&lt;/h3&gt;
&lt;p&gt;每轮给 Agent 反馈时，&lt;strong&gt;引用具体现象&lt;/strong&gt;（&quot;点击提交后没反应&quot;）而不是模糊的（&quot;好像不太对&quot;）。Agent 在同一 Space 里有完整上下文，具体反馈能让它精准修改。&lt;/p&gt;
&lt;h2&gt;写在最后&lt;/h2&gt;
&lt;p&gt;Cohub 最有意思的地方，是把「和 AI 协作做东西」从聊天里搬到了一个&lt;strong&gt;有文件、有环境、能发布&lt;/strong&gt;的真实空间里。&lt;/p&gt;
&lt;p&gt;你可以把它当：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;快速原型工具（几分钟做个工具验证想法）&lt;/li&gt;
&lt;li&gt;一个带 Agent 的完整开发环境&lt;/li&gt;
&lt;li&gt;一个内容/工具的分发平台（Work）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果你还没试过，建议按这篇的流程走一遍。&lt;strong&gt;从&quot;让 AI 聊天&quot;到&quot;让 AI 帮你把东西做出来并发布&quot;&lt;/strong&gt;，这个转变，用了才知道。&lt;/p&gt;
&lt;hr /&gt;
&lt;ul&gt;
&lt;li&gt;产品：&lt;a href=&quot;https://cohub.run&quot;&gt;cohub.run&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;CLI：&lt;code&gt;npm install -g @neta-art/cohub-cli&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;生态索引：&lt;a href=&quot;https://github.com/markbang/awesome-cohub&quot;&gt;github.com/markbang/awesome-cohub&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Docker 生产实践：从能跑到跑得稳的 Checklist</title><link>https://bangwu.me/posts/docker-best-practices/</link><guid isPermaLink="true">https://bangwu.me/posts/docker-best-practices/</guid><description>把容器从&quot;本地能跑&quot;带到&quot;生产稳定&quot;，我总结了 10 个最常见的坑和对应的最佳实践：镜像体积、安全、健康检查、日志、资源限制。</description><pubDate>Thu, 06 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;先说结论&lt;/h2&gt;
&lt;p&gt;「Docker 能跑」和「Docker 能稳定跑」是两回事。本地 &lt;code&gt;docker run&lt;/code&gt; 一次成功，和在生产集群里扛住流量、重启、扩容，中间隔着不少坑。&lt;/p&gt;
&lt;p&gt;这篇文章是我在生产环境踩过之后总结的 Checklist。不追求面面俱到，只讲&lt;strong&gt;高频、高影响&lt;/strong&gt;的十件事，按重要程度排序。&lt;/p&gt;
&lt;h2&gt;1. 镜像尽量小，但要小得有理&lt;/h2&gt;
&lt;p&gt;镜像体积直接影响拉取时间和启动速度。常见手段：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;用 &lt;code&gt;alpine&lt;/code&gt; 或 &lt;code&gt;distroless&lt;/code&gt; 作为基础镜像&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;多阶段构建&lt;/strong&gt;：构建阶段用全量镜像，运行阶段只拷贝产物&lt;/li&gt;
&lt;li&gt;清理不必要的缓存和包管理器元数据&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;# 多阶段构建示例
FROM node:20-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM node:20-alpine
WORKDIR /app
COPY --from=build /app/dist ./dist
COPY --from=build /app/package*.json ./
RUN npm ci --omit=dev
EXPOSE 3000
CMD [&quot;node&quot;, &quot;dist/index.js&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但注意：&lt;strong&gt;不是越瘦越好&lt;/strong&gt;。distroless 没有 shell，调试会很痛苦；alpine 的 musl libc 偶尔和依赖有兼容问题。先瘦到合理范围，别为了几十 MB 自找麻烦。&lt;/p&gt;
&lt;h2&gt;2. 非 root 用户运行&lt;/h2&gt;
&lt;p&gt;默认容器内是 root，这是安全大忌。一旦容器被攻破，攻击者就是 root 权限。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;FROM node:20-alpine
# 创建非 root 用户
RUN addgroup -S app &amp;amp;&amp;amp; adduser -S app -G app
USER app
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;大多数官方镜像现在都支持直接切用户。这一步成本极低，收益很高。&lt;/p&gt;
&lt;h2&gt;3. 健康检查必须配&lt;/h2&gt;
&lt;p&gt;没有健康检查，编排系统（K8s/Swarm）就不知道你的容器是不是真的活着。进程在不代表服务可用。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
  CMD wget -q -O - http://localhost:3000/healthz || exit 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意 &lt;code&gt;start-period&lt;/code&gt;——给应用启动留时间，别一上来就被打回。&lt;/p&gt;
&lt;h2&gt;4. 日志打 stdout，别写文件&lt;/h2&gt;
&lt;p&gt;容器里的日志应该走 stdout/stderr，由编排系统收集，而不是写到容器内的文件。原因：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;容器文件系统是临时的，重启就没了&lt;/li&gt;
&lt;li&gt;你没法轻易拿到容器内的文件&lt;/li&gt;
&lt;li&gt;云平台（CloudWatch、ELK、Loki）都从 stdout 收集&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以应用日志直接 &lt;code&gt;console.log&lt;/code&gt;/&lt;code&gt;print&lt;/code&gt;，别自己写日志文件轮转。&lt;/p&gt;
&lt;h2&gt;5. 资源限制，不是可选项&lt;/h2&gt;
&lt;p&gt;不设 &lt;code&gt;--memory&lt;/code&gt; 和 &lt;code&gt;--cpus&lt;/code&gt; 限制，一个失控的容器能拖垮整个节点。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# docker-compose 或 k8s 里都要设
resources:
  limits:
    memory: 512M
    cpus: &quot;0.5&quot;
  reservations:
    memory: 256M
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;limits 一定要设&lt;/strong&gt;，reservations 至少给一个合理值。这是生产环境第一道防线。&lt;/p&gt;
&lt;h2&gt;6. 环境变量和密钥，别硬编码&lt;/h2&gt;
&lt;p&gt;镜像里不要写死任何密钥。用：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;环境变量（运行时注入）&lt;/li&gt;
&lt;li&gt;Docker secrets / K8s secrets&lt;/li&gt;
&lt;li&gt;专门的密钥管理（Vault、云厂商的 secrets manager）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;镜像一旦构建就难以「撤销」里面硬编码的密钥。密钥泄露的补救成本远高于一开始就用环境变量。&lt;/p&gt;
&lt;h2&gt;7. 固定依赖版本，别用 latest&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;FROM node:20-alpine&lt;/code&gt; 里的 &lt;code&gt;20&lt;/code&gt; 可能今天和明天解析到不同的小版本。生产环境要&lt;strong&gt;可复现&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;基础镜像用精确 tag（如 &lt;code&gt;node:20.18.0-alpine&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;依赖锁文件（&lt;code&gt;package-lock.json&lt;/code&gt; / &lt;code&gt;poetry.lock&lt;/code&gt;）进镜像&lt;/li&gt;
&lt;li&gt;有需要时用 digest 固定镜像&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;可复现 = 可回滚 = 可调试。&lt;/p&gt;
&lt;h2&gt;8. 处理僵尸进程和信号&lt;/h2&gt;
&lt;p&gt;容器作为 PID 1 时，要正确处理信号（SIGTERM）和孤儿进程。常见方案：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;用带 PID 1 处理的运行时（如 &lt;code&gt;tini&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;确保应用能优雅关闭（监听 SIGTERM，完成在途请求）&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;RUN apk add --no-cache tini
ENTRYPOINT [&quot;/sbin/tini&quot;, &quot;--&quot;]
CMD [&quot;node&quot;, &quot;dist/index.js&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不然 &lt;code&gt;docker stop&lt;/code&gt; 会变成 &lt;code&gt;kill -9&lt;/code&gt;，数据库写一半就没了。&lt;/p&gt;
&lt;h2&gt;9. 镜像构建也要考虑 .dockerignore&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;.dockerignore&lt;/code&gt; 能大幅减少构建上下文和镜像体积，避免把 &lt;code&gt;node_modules&lt;/code&gt;、&lt;code&gt;.git&lt;/code&gt;、日志、临时文件打进去。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;node_modules
.git
*.log
.tmp
dist
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这也是多阶段构建里第一层就过滤掉的东西。&lt;/p&gt;
&lt;h2&gt;10. 一个容器一个职责&lt;/h2&gt;
&lt;p&gt;别在一个容器里跑 web + worker + cron。拆开：&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;一个容器只做一件事，是容器化最大的架构收益。&lt;/p&gt;
&lt;h2&gt;验证清单&lt;/h2&gt;
&lt;p&gt;改完以后，跑一遍这个验证：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 1. 镜像构建成功且体积合理
docker build -t my-app .

# 2. 容器能启动且健康检查通过
docker run -d --name test -p 3000:3000 my-app
docker inspect --format=&apos;{{.State.Health.Status}}&apos; test
# 期望: healthy

# 3. 优雅停止
docker stop test
# 观察日志是否有优雅关闭记录

# 4. 资源限制生效
docker run --rm -m 256m my-app
# 期望: 超出限制时 OOM，而不是拖垮宿主
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;写在最后&lt;/h2&gt;
&lt;p&gt;这十条里，&lt;strong&gt;健康检查、资源限制、非 root、日志走 stdout&lt;/strong&gt; 是成本最低、收益最高的四项，强烈建议从这些开始。&lt;/p&gt;
&lt;p&gt;容器化不是终点，只是起点。把这些基础打牢，后面接 CI/CD、上 K8s 才会顺。&lt;/p&gt;
&lt;p&gt;如果哪条你踩过不同的坑，欢迎交流。&lt;/p&gt;
</content:encoded></item><item><title>Neta Studio：用 AI 造一个可以走进去的世界</title><link>https://bangwu.me/posts/neta-studio-intro/</link><guid isPermaLink="true">https://bangwu.me/posts/neta-studio-intro/</guid><description>Neta Studio 是 Viscept 面向海外的 AI 世界观引擎——一句话描述一个世界，AI 帮你生成视觉小说、交互地图、可玩游戏和 POV 视频。</description><pubDate>Thu, 30 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;disclosure：这是我们公司（Viscept）的产品，这篇文章不是纯客观测评，但我尽量讲产品和设计。&lt;/p&gt;
&lt;h2&gt;先说结论&lt;/h2&gt;
&lt;p&gt;Neta Studio（neta.art）是一个 AI 世界观引擎。&lt;/p&gt;
&lt;p&gt;你给它一句话描述一个世界，它帮你把这个世界造出来：角色、场景、故事线、视觉小说、交互地图、甚至可玩的小游戏。不是生成一段文字或一张图就完了，而是生成一个&lt;strong&gt;可以走进去、可以交互、可以 fork 给别人继续扩展的世界&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;我们国内有捏Ta做 AI 生图社区，neta.art 是面向海外市场的新产品，定位从「AI 生图」升级到了「AI 世界构建」。&lt;/p&gt;
&lt;h2&gt;核心体验：从一句话到一个世界&lt;/h2&gt;
&lt;p&gt;产品入口很简单：描述一个世界。&lt;/p&gt;
&lt;p&gt;输入一句话，比如&quot;一个蒸汽朋克风格的空中海盗学院&quot;，AI 就开始生成：世界观设定、角色卡、场景描述、故事大纲。生成完之后，你可以走进这个世界——以视觉小说的形式阅读、在交互地图上探索、甚至玩一个 AI 驱动的冒险战役。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.bangwu.me/img/20260730-neta-home.webp&quot; alt=&quot;Neta Studio 首页&quot; /&gt;&lt;/p&gt;
&lt;p&gt;产品支持的形态：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Visual Novels&lt;/strong&gt; - 交互式视觉小说，有分支选项&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Interactive Worlds&lt;/strong&gt; - 完整的 AI 世界观，有结构化的角色和场景数据&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;POV Videos&lt;/strong&gt; - 第一人称 AI 短视频，&quot;No camera, no actors&quot;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Interactive Maps&lt;/strong&gt; - AI 生成的幻想地图和世界探索器&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Vibe Coding Games&lt;/strong&gt; - 自然语言描述生成可玩游戏&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Adventure Campaigns&lt;/strong&gt; - AI 担任 DM 的交互叙事冒险&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这些不是独立的工具，而是同一个世界的不同表现形式。你创建一个世界之后，可以从视觉小说切换到地图探索，再切到冒险战役——底层数据是同一套。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.bangwu.me/img/20260730-neta-explore.webp&quot; alt=&quot;Explore - 热门世界与作品&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;世界是结构化的，不是一段对话&lt;/h2&gt;
&lt;p&gt;大部分 AI 创作工具的问题是：生成的内容是一坨文本，没有结构，改不了，也复用不了。&lt;/p&gt;
&lt;p&gt;Neta Studio 的做法不同。每个世界里的角色、地点、作品都是独立的「Atom」——结构化的数据卡片，可以单独读取、修改、引用。你给一个角色改了设定，所有引用这个角色的故事和场景都会更新。&lt;/p&gt;
&lt;p&gt;更关键的是，世界可以 fork。你看到别人创建的世界，fork 一份改成自己的版本，就像 Git 分支一样。这意味着创作是累积的——社区的好的世界观设定可以被复用和扩展，不是每次都从零开始。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.bangwu.me/img/20260730-neta-world-detail.webp&quot; alt=&quot;公开 World 详情与可交互 Work 预览&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.bangwu.me/img/20260730-neta-studio-formats.webp&quot; alt=&quot;产品流程画面&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;底层是 Cohub&lt;/h2&gt;
&lt;p&gt;Neta Studio 的底层跑在 Cohub 上（Cohub 也是我们公司开源的，之前&lt;a href=&quot;https://bangwu.me/posts/cohub-intro&quot;&gt;写过一篇文章介绍&lt;/a&gt;）。&lt;/p&gt;
&lt;p&gt;简单说，Cohub 提供了 Space（隔离的创作环境）、Agent（在 Space 里干活的 AI）、Sandbox（文件系统和执行运行时）和 Checkpoint（存档和 fork）。Neta Studio 把这些能力用在了世界观创作上——每个世界就是一个 Space，Agent 在里面读写文件、生成内容、跑叙事流程。&lt;/p&gt;
&lt;p&gt;这个架构选择的好处是：世界的数据是持久化的文件，不是易失的对话上下文；Agent 的操作是可追溯的；世界可以 fork 和 remix。这些都不是加个聊天框就能实现的。&lt;/p&gt;
&lt;h2&gt;付费方案&lt;/h2&gt;
&lt;p&gt;Neta Studio 有三个档位：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;计划&lt;/th&gt;
&lt;th&gt;价格&lt;/th&gt;
&lt;th&gt;定位&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Starter&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;免费&lt;/td&gt;
&lt;td&gt;默认计划，基础创作能力&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Creator&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$18/月&lt;/td&gt;
&lt;td&gt;进阶创作，更多生成额度和高级功能&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Legend&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;更高&lt;/td&gt;
&lt;td&gt;最高档，完整能力 + 优先支持&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ul&gt;
&lt;li&gt;支持&lt;strong&gt;月付和年付&lt;/strong&gt;两种周期&lt;/li&gt;
&lt;li&gt;通过 &lt;strong&gt;Stripe&lt;/strong&gt; 支付，支持信用卡&lt;/li&gt;
&lt;li&gt;创作消耗 &lt;strong&gt;AP（Action Points）&lt;/strong&gt;，不同操作消耗不同&lt;/li&gt;
&lt;li&gt;订阅计划和 AP 额度可以在产品内直接查看和升级&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;具体价格和额度以 neta.art 站内为准，这里只列大致结构。&lt;/p&gt;
&lt;h2&gt;和其他工具的区别&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;维度&lt;/th&gt;
&lt;th&gt;Neta Studio&lt;/th&gt;
&lt;th&gt;Character.AI&lt;/th&gt;
&lt;th&gt;NovelAI&lt;/th&gt;
&lt;th&gt;AI Dungeon&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;核心范式&lt;/td&gt;
&lt;td&gt;AI 世界观 + 多媒介生成&lt;/td&gt;
&lt;td&gt;AI 角色聊天&lt;/td&gt;
&lt;td&gt;AI 辅助写作&lt;/td&gt;
&lt;td&gt;AI 文字冒险&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;输出形态&lt;/td&gt;
&lt;td&gt;视觉小说、视频、地图、游戏&lt;/td&gt;
&lt;td&gt;纯文本对话&lt;/td&gt;
&lt;td&gt;文本+图片&lt;/td&gt;
&lt;td&gt;纯文本&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;多媒体生成&lt;/td&gt;
&lt;td&gt;图片+视频+歌曲+地图&lt;/td&gt;
&lt;td&gt;无&lt;/td&gt;
&lt;td&gt;AI 图片&lt;/td&gt;
&lt;td&gt;有限&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;世界观管理&lt;/td&gt;
&lt;td&gt;结构化 atom + fork&lt;/td&gt;
&lt;td&gt;角色定义&lt;/td&gt;
&lt;td&gt;lorebook&lt;/td&gt;
&lt;td&gt;世界设定&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;开发者平台&lt;/td&gt;
&lt;td&gt;开放 API + OAuth&lt;/td&gt;
&lt;td&gt;无&lt;/td&gt;
&lt;td&gt;无&lt;/td&gt;
&lt;td&gt;有限&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;核心差异：Neta Studio 不是单一的聊天或写作工具，而是把 World Building、多媒介生成和交互叙事整合在一个平台上。世界是结构化的、可 fork 的——不是一个对话上下文。&lt;/p&gt;
&lt;h2&gt;写在最后&lt;/h2&gt;
&lt;p&gt;做 AI 创作工具的人很多，但大部分停留在&quot;生成一段内容&quot;的阶段。Neta Studio 想做的更远一步：生成一个&lt;strong&gt;可持续存在的、可交互的、可分享的世界&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;neta.art 上已经有很多用户创建的世界和作品，从同人视觉小说到原创幻想世界都有。如果你对 AI 创作感兴趣，值得花十分钟去看看。&lt;/p&gt;
&lt;hr /&gt;
&lt;ul&gt;
&lt;li&gt;产品：&lt;a href=&quot;https://neta.art&quot;&gt;neta.art&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Neta Skills（开源）：&lt;a href=&quot;https://github.com/talesofai/neta-skills&quot;&gt;github.com/talesofai/neta-skills&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;底层平台：&lt;a href=&quot;https://cohub.run&quot;&gt;cohub.run&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Cohub：让人和 Agent 真正在同一个空间里干活</title><link>https://bangwu.me/posts/cohub-intro/</link><guid isPermaLink="true">https://bangwu.me/posts/cohub-intro/</guid><description>Cohub 不是又一个 AI 聊天框，而是一个让人和 Agent 在同一个 Space 里共创、保存、发布的平台。开源、可自托管、还有完整的商业化体系。</description><pubDate>Sun, 26 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;先说结论&lt;/h2&gt;
&lt;p&gt;如果你用过各种 AI 编程工具，大概率遇到过同一个问题：聊完就没了。对话记录在，但文件、环境、上下文散落各处，换个会话就得从头来。&lt;/p&gt;
&lt;p&gt;Cohub 想解决的就是这件事——&lt;strong&gt;不是给你一个更好的聊天框，而是给你一个能干活的空间。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;具体来说，Cohub 是一个浏览器端的 Space，人和 Agent 在同一个空间里：有文件系统、有沙箱执行、有会话、有 checkpoint 存档、还能把成果发布成公开的 Work。它不是我见过的大多数「AI + 编辑器」的路子，更像是把一个完整的开发环境做成了可以和人协作的活的载体。&lt;/p&gt;
&lt;p&gt;而且它开源了，Apache-2.0，可以自托管。 disclosure：这是我们公司（Viscept）做的项目，所以这篇文章不是纯客观测评，但我尽量讲技术。&lt;/p&gt;
&lt;h2&gt;Space：不是聊天，是工作单元&lt;/h2&gt;
&lt;p&gt;Cohub 的核心概念是 Space。&lt;/p&gt;
&lt;p&gt;一个 Space 里有什么：对话（Session）、文件、Agent、沙箱运行时、预览。你打开一个 Space，跟 Agent 说「帮我写个落地页」，它不只是回复代码——它在 Space 的文件系统里创建文件、装依赖、跑 dev server、把预览端口暴露出来给你看。&lt;/p&gt;
&lt;p&gt;这意味着 Agent 的产出不是一段文本，而是一个&lt;strong&gt;可运行的环境&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;几个关键概念：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Checkpoint（Save）&lt;/strong&gt; - 不可变快照。任何时候你觉得当前状态有价值，存一下。之后可以恢复、可以 fork 出新 Space、可以作为稳定基底继续干活。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Work&lt;/strong&gt; - 把 Space 里的文件、目录或端口发布成公开可访问的页面。带独立 URL，可以分享。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Channel&lt;/strong&gt; - 外部入口。Discord、飞书、微信、Telegram 都可以接入一个 Space，从聊天 App 直接操控。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Sandbox&lt;/strong&gt; - 执行运行时。云端 K8s 或本机目录，Agent 在里面读写文件、跑命令。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;说白了，Cohub 的产品循环是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;创建 Space -&amp;gt; 和 Agent 聊 -&amp;gt; 编辑/运行 -&amp;gt; 存 Checkpoint -&amp;gt; 发布 Work
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个循环里每一步都有实际的产品支撑，不是概念图。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.bangwu.me/img/20260726-cohub-home.webp&quot; alt=&quot;Cohub 首页&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.bangwu.me/img/20260726-cohub-space-overview.webp&quot; alt=&quot;Inside a Space - 产品说明区&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;技术架构：TypeScript + Go，开源 monorepo&lt;/h2&gt;
&lt;p&gt;Cohub 的代码在 &lt;a href=&quot;https://github.com/talesofai/cohub&quot;&gt;github.com/talesofai/cohub&lt;/a&gt;，pnpm monorepo，Apache-2.0 协议。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;apps/
  api/       Hono API - 编排、provision、session 持久化
  agent/     Agent 控制面 - pi-coding-agent，WS 连 sandbox
  sandbox/   执行面 - Go WebSocket server（文件系统 / 进程 / 端口）
  gateway/   外部渠道网关（Discord / 飞书 / 微信 / Telegram）
  web/       SvelteKit 前端
  worker/    BullMQ 异步任务 / cron
packages/
  billing/   计费 provider 抽象
  cli/       @neta-art/cohub-cli
  sdk/       @neta-art/cohub SDK
  core/ db/ identity/ infra/ protocol/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;几个值得注意的设计：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;控制面和执行面分离。&lt;/strong&gt; Agent 管 session、工具编排、Redis 状态；Sandbox 管真实的文件读写和进程执行。Sandbox 可以跑在云端 K8s 上，也可以用 &lt;code&gt;cohub sandbox up&lt;/code&gt; 在本机跑然后通过 gateway relay 拨出。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;前端是 SvelteKit。&lt;/strong&gt; 不是 React，在 Cloudflare Workers 上跑。Board runtime 用 Pixi.js 做渲染（v2.0 刚把旧的 canvas 服务换成了新的 board 交易模型）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Space 的底座是 Git。&lt;/strong&gt; 用 Gitea 做 Space/Checkpoint 的存储，每个 Space 对应一个 Git 仓库。Checkpoint 表里有 &lt;code&gt;parentCheckpointId&lt;/code&gt; 和 &lt;code&gt;forkCount&lt;/code&gt;，存档点本身构成一棵可 fork 的树，不是线性 undo。这意味着你的工作天然有版本控制，而且可以分叉。&lt;/p&gt;
&lt;p&gt;技术栈总结：TypeScript（~74%）+ Svelte（~22%）+ Go（~1.5%，sandbox），PostgreSQL + Drizzle ORM，Redis + BullMQ，Logto 做 OIDC 认证。&lt;/p&gt;
&lt;h2&gt;v2.0：Board runtime&lt;/h2&gt;
&lt;p&gt;写这篇文章的时候，Cohub 刚发布了 v2.0（2026-07-25），核心更新是 Board runtime。&lt;/p&gt;
&lt;p&gt;Board 是什么？你可以理解为 Space 里的可视化画布。v2.0 把旧的 document-centric canvas 服务整个换掉了，新的模型围绕 board、node、effect、sequence、clip、transaction 构建。&lt;/p&gt;
&lt;p&gt;几个实际的变化：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;任何工作区文件都可以放到 board 上，不只是图片和视频&lt;/li&gt;
&lt;li&gt;节点排序不再用数组索引，删第一个节点不会触发 999 次 patch&lt;/li&gt;
&lt;li&gt;上限提到了 5 万个节点&lt;/li&gt;
&lt;li&gt;触摸手势支持，移动端可以拖拽文件到 board 上&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这个迭代速度说明产品在认真做，不是 demo 项目。&lt;/p&gt;
&lt;h2&gt;商业化：三层模型&lt;/h2&gt;
&lt;p&gt;Cohub 的商业化设计比大多数开源 AI 项目想得清楚。三层：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. 托管订阅（cohub.run）&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;计划&lt;/th&gt;
&lt;th&gt;月费&lt;/th&gt;
&lt;th&gt;月余额&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Plus&lt;/td&gt;
&lt;td&gt;$10&lt;/td&gt;
&lt;td&gt;$11&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pro&lt;/td&gt;
&lt;td&gt;$50&lt;/td&gt;
&lt;td&gt;$58&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Max&lt;/td&gt;
&lt;td&gt;$200&lt;/td&gt;
&lt;td&gt;$240&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;余额是 credit 体系，按实际用量扣费。Generation（图/视频/音乐生成）和 Agent 调用共用同一个账本。年付省 20%（Plus $96/年、Pro $480/年、Max $1920/年）。&lt;/p&gt;
&lt;p&gt;除了订阅，还有一次性余额包：$10→10、$30→32、$100→110、$200→225，24 个月有效期。不想按月订阅的话，直接买余额包也行。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. Work Commerce（创作者变现）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这是我觉得最有意思的部分。你发布了一个 Work（比如一个可交互的工具或应用），可以在里面卖东西：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Feature benefit&lt;/strong&gt; - 功能解锁，买了才能用某个能力&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Credit benefit&lt;/strong&gt; - 可消耗积分，按量付费&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;整个闭环都在已发布的 Work 内运行：用户进入 -&amp;gt; 检查权益 -&amp;gt; 购买 -&amp;gt; checkout -&amp;gt; 消费。注意，Commerce API 只在 Cohub 托管的已发布 Work 里可用，静态资源 URL 和本地预览下不工作。&lt;/p&gt;
&lt;p&gt;这意味着 Cohub 不只是开发平台，也是创作者的分发和变现渠道。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.bangwu.me/img/20260726-cohub-work-detail.webp&quot; alt=&quot;公开 Work 实例详情&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.bangwu.me/img/20260726-cohub-works-profile.webp&quot; alt=&quot;@bangwu 公开 Spaces / Works 列表&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. 开源自托管（免费）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Self-host 默认关掉计费。想自托管？需要 Postgres + Redis + Gitea（或兼容 Git host）+ S3 兼容对象存储 + OIDC 认证（Logto），Node ≥ 24、pnpm ≥ 10。&lt;code&gt;pnpm install &amp;amp;&amp;amp; pnpm dev&lt;/code&gt; 填好环境变量就能跑。想开计费就接 &lt;code&gt;@talesofai-billing/sdk&lt;/code&gt;（私有包）。&lt;/p&gt;
&lt;p&gt;这种 open core 模式我觉得是对的：开源版本功能完整，商业化靠托管便利性和增值服务，不靠锁功能。&lt;/p&gt;
&lt;h2&gt;和其他工具的区别&lt;/h2&gt;
&lt;p&gt;说了这么多，Cohub 到底和 Cursor、Replit Agent、v0 这些有什么不同？&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;维度&lt;/th&gt;
&lt;th&gt;Cohub&lt;/th&gt;
&lt;th&gt;Cursor&lt;/th&gt;
&lt;th&gt;Replit Agent&lt;/th&gt;
&lt;th&gt;v0&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;核心范式&lt;/td&gt;
&lt;td&gt;Space（文件+沙箱+会话+发布）&lt;/td&gt;
&lt;td&gt;IDE 内 AI 编程&lt;/td&gt;
&lt;td&gt;在线 IDE + Agent&lt;/td&gt;
&lt;td&gt;UI 生成&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;协作模型&lt;/td&gt;
&lt;td&gt;人+Agent 同在一个 Space&lt;/td&gt;
&lt;td&gt;单人+AI 结对&lt;/td&gt;
&lt;td&gt;单人+Agent&lt;/td&gt;
&lt;td&gt;单人 prompt→UI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;持久化&lt;/td&gt;
&lt;td&gt;Checkpoint 快照&lt;/td&gt;
&lt;td&gt;git commit&lt;/td&gt;
&lt;td&gt;repl 自动保存&lt;/td&gt;
&lt;td&gt;无&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;发布&lt;/td&gt;
&lt;td&gt;Work（公开 URL）&lt;/td&gt;
&lt;td&gt;无内置&lt;/td&gt;
&lt;td&gt;部署到 repl.co&lt;/td&gt;
&lt;td&gt;生成代码&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;多入口&lt;/td&gt;
&lt;td&gt;Web + CLI + SDK + Channels&lt;/td&gt;
&lt;td&gt;桌面 IDE&lt;/td&gt;
&lt;td&gt;Web&lt;/td&gt;
&lt;td&gt;Web&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;开源&lt;/td&gt;
&lt;td&gt;Apache-2.0&lt;/td&gt;
&lt;td&gt;部分开源&lt;/td&gt;
&lt;td&gt;闭源&lt;/td&gt;
&lt;td&gt;闭源&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;核心差异：Cohub 不是「聊天+代码」，而是以 &lt;strong&gt;Space 为工作单元&lt;/strong&gt;，有 Checkpoint 的时间维度和 Work 的发布维度。Agent 有真正的文件系统和沙箱执行能力，不是只输出文本。&lt;/p&gt;
&lt;p&gt;另外一个我觉得被低估的点：Channel。你可以从飞书群直接操控一个 Space，Agent 在 Space 里干活，结果推回群里。对于国内团队来说，这个入口比 Discord 自然得多。&lt;/p&gt;
&lt;h2&gt;CLI 和 SDK&lt;/h2&gt;
&lt;p&gt;Cohub 不只是 Web 产品。CLI 和 SDK 覆盖了同一个产品面：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 安装
npm install -g @neta-art/cohub-cli

# 常用命令
cohub auth login
cohub spaces ls
cohub -s &amp;lt;space-id&amp;gt; prompt &quot;帮我写个落地页&quot;
cohub generate &quot;霓虹城市黄昏&quot; --model gpt-image-2
cohub -s &amp;lt;space-id&amp;gt; spaces files ls
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;SDK 是 &lt;code&gt;@neta-art/cohub&lt;/code&gt;，TypeScript，覆盖 Spaces、Sessions、Checkpoints、Works、Billing 全套 API。&lt;/p&gt;
&lt;p&gt;这意味着你可以把 Cohub 当基础设施来用——不只是打开网页手动操作，而是用 CLI 和 SDK 做自动化。&lt;/p&gt;
&lt;h2&gt;谁在用，适合什么场景&lt;/h2&gt;
&lt;p&gt;从我自己的使用和 awesome-cohub 的 playbooks 来看，几个典型场景：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;快速原型&lt;/strong&gt; - 开一个 Space，让 Agent 帮你搭静态站，发布成 Work 分享&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;研究沉淀&lt;/strong&gt; - Agent 持续研究某个主题，把结论写回 Space 的知识库&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;内容创作&lt;/strong&gt; - 多模态生成（图、视频、音乐），在 Space 里迭代&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;付费产品&lt;/strong&gt; - 用 Work Commerce 做带付费功能的小工具&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;团队协作&lt;/strong&gt; - 通过飞书/Discord 频道接入 Space，多人多 Agent 协同&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;写在最后&lt;/h2&gt;
&lt;p&gt;2026 年 3 月开源，到现在 7 月已经迭代到 v2.0。CHANGELOG 里 121 个版本条目，npm 上 CLI 发了 80 个版本——5 个月从 v1 跑到 v2.0，开发节奏很快。&lt;/p&gt;
&lt;p&gt;如果你在做 AI 相关的产品或者工具，我建议花十分钟打开 &lt;a href=&quot;https://cohub.run&quot;&gt;cohub.run&lt;/a&gt; 试试。不是因为它完美，而是因为它选了一条不同的路：&lt;strong&gt;不是让 AI 帮你写代码，而是让 AI 和你在同一个空间里一起干活。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这个区别，用了才知道。&lt;/p&gt;
&lt;hr /&gt;
&lt;ul&gt;
&lt;li&gt;开源仓库：&lt;a href=&quot;https://github.com/talesofai/cohub&quot;&gt;github.com/talesofai/cohub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;产品：&lt;a href=&quot;https://cohub.run&quot;&gt;cohub.run&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;CLI：&lt;code&gt;npm install -g @neta-art/cohub-cli&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;生态索引：&lt;a href=&quot;https://github.com/markbang/awesome-cohub&quot;&gt;github.com/markbang/awesome-cohub&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>修 Traefik Kubernetes Gateway CPU 尖刺</title><link>https://bangwu.me/posts/traefik-k8s-gateway-cpu-spikes-fix/</link><guid isPermaLink="true">https://bangwu.me/posts/traefik-k8s-gateway-cpu-spikes-fix/</guid><description>从 Traefik issue #13011 出发，记录一次 Kubernetes Gateway Provider CPU 抖动问题的定位、修复和验证。</description><pubDate>Sat, 13 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;先说结论&lt;/h2&gt;
&lt;p&gt;这次问题不是“Traefik 转发慢”，也不是单个请求路径的问题。&lt;/p&gt;
&lt;p&gt;它更像一个控制面成本被放大的例子：Kubernetes 对象一变，Traefik 的 Kubernetes provider 收到 informer 事件，然后重新构建动态配置。单次 rebuild 看起来没什么，但当 EndpointSlice、Node 心跳、Gateway/Route 数量一起上来以后，就会变成很密的 CPU 尖刺。&lt;/p&gt;
&lt;p&gt;我这次在本地 &lt;code&gt;~/code/traefik&lt;/code&gt; 的 &lt;code&gt;fix/k8s-gateway-cpu-spikes&lt;/code&gt; 分支做了一组修复，commit 是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;53693e504 Reduce Kubernetes provider CPU churn
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;主要改了四件事：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;给 EndpointSlice informer 加 &lt;code&gt;serviceName&lt;/code&gt; index，避免每次按 Service 找 EndpointSlice 都扫整个 namespace&lt;/li&gt;
&lt;li&gt;对 EndpointSlice update 做内容级过滤，只在 Traefik 真正消费的字段变化时触发 rebuild&lt;/li&gt;
&lt;li&gt;对 Node update 做地址级过滤，忽略 kubelet 心跳带来的无关 &lt;code&gt;Node.Status&lt;/code&gt; 变化&lt;/li&gt;
&lt;li&gt;在 Gateway provider 构建配置时给 listener 建索引，避免大量 Route 反复扫所有 Gateway listener&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我的默认判断是：这类问题不要一上来就调大资源，也不要只靠 &lt;code&gt;throttleDuration&lt;/code&gt; 压事件。先把复杂度和事件有效性搞清楚，收益更稳定。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;问题从哪里来&lt;/h2&gt;
&lt;p&gt;背景是 Traefik 的这个 issue：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/traefik/traefik/issues/13011&quot;&gt;High CPU regression since v3.1 — EndpointSlice migration + Nodes heartbeat on k8s providers&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;issue 里描述得比较完整：从 Traefik &lt;code&gt;v3.0.4&lt;/code&gt; 升到 &lt;code&gt;v3.1&lt;/code&gt; 以后，CPU 基线明显抬高，并且在 rollout、扩缩容、node autoscaler 活动时出现一串短促但很密的尖刺。作者后来 bisect 到一个很关键的变化：&lt;code&gt;a8a92eb2a&lt;/code&gt;，也就是 v3.1.0 里迁移到 EndpointSlice API 的提交。&lt;/p&gt;
&lt;p&gt;这里容易误判。&lt;/p&gt;
&lt;p&gt;EndpointSlice 本身不是坏东西。Kubernetes 引入 EndpointSlice，本来就是为了解决传统 Endpoints 对象在大规模 Service 下不够可扩展的问题。它把一个 Service 后面的 endpoint 拆成多个 slice，这对 Kubernetes API 和 watch 传播是更好的。&lt;/p&gt;
&lt;p&gt;但问题在于：&lt;strong&gt;数据模型变得更可扩展，不代表你的读取方式也自动变得更便宜。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;如果 provider 每次加载一个 backend，都用 label selector 从 namespace 里列一遍 EndpointSlice，那么成本就从“按 Service 精确取”变成了“按 namespace 扫一遍再过滤”。当 Service、Route、Ingress path、EndpointSlice 数量叠起来以后，这个差异就会很明显。&lt;/p&gt;
&lt;p&gt;issue 里给出的生产规模不是夸张的 hyperscale：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;141 Ingresses
 39 IngressRoutes
343 Services
342 EndpointSlices
110 TraefikServices
130 Middlewares
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个规模我觉得很有代表性。它不是玩具集群，但也不是那种离普通团队很远的巨型集群。也正因为这样，这个问题值得修。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;为什么它会变成 CPU 尖刺&lt;/h2&gt;
&lt;p&gt;我把这个问题拆成三条成本线看。&lt;/p&gt;
&lt;h3&gt;1. 每次 backend lookup 都扫 EndpointSlice&lt;/h3&gt;
&lt;p&gt;修复前的逻辑大概是这样：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;c.factoriesKube[...].
    Discovery().
    V1().
    EndpointSlices().
    Lister().
    EndpointSlices(namespace).
    List(serviceSelector)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;看起来是“按 Service 查 EndpointSlice”，但实际对 client-go 的 local cache 来说，这仍然是一次 selector list。也就是说，它要在 namespace 的 EndpointSlice 集合里做筛选。&lt;/p&gt;
&lt;p&gt;如果一次配置构建里有很多 backend 引用，这个成本会被反复支付。&lt;/p&gt;
&lt;p&gt;这类问题最麻烦的地方是，单独看一行代码不吓人，单次 list 也不一定慢。但放进 provider rebuild 的循环里，它会变成：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;rebuild 次数
  * route / ingress path 数量
  * backend lookup 次数
  * namespace 内 EndpointSlice 数量
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这就是控制面 CPU 问题常见的形状：不是某个函数慢到离谱，而是一个本来可以 O(1) 的查询，在热路径里退化成了 O(N)。&lt;/p&gt;
&lt;h3&gt;2. EndpointSlice 的无关 update 也触发完整 rebuild&lt;/h3&gt;
&lt;p&gt;Kubernetes informer 只要收到 update event，provider 就很容易进入“重新构建配置”的路径。&lt;/p&gt;
&lt;p&gt;但 EndpointSlice 的 update 不一定都影响 Traefik 的路由结果。比如只改了 &lt;code&gt;resourceVersion&lt;/code&gt;、&lt;code&gt;managedFields&lt;/code&gt;，或者一些 Traefik 根本不消费的 metadata，重建配置就是浪费。&lt;/p&gt;
&lt;p&gt;当然，这里不能粗暴忽略所有 condition-only update。&lt;/p&gt;
&lt;p&gt;EndpointSlice 里的这些条件是有语义的：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Ready&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Serving&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Terminating&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;HTTP、TCP、UDP、Gateway provider 对这些字段的使用不完全一样。比如 endpoint 是否 ready，直接影响 server 是否应该进入配置。这里如果为了省 CPU 把 condition 变化吞掉，就会变成正确性问题。&lt;/p&gt;
&lt;p&gt;所以正确做法不是“少看一点”，而是“只看真正参与配置语义的字段”。&lt;/p&gt;
&lt;h3&gt;3. Node 心跳把 provider 拖进 rebuild&lt;/h3&gt;
&lt;p&gt;还有一条很隐蔽的线是 Node informer。&lt;/p&gt;
&lt;p&gt;当 Traefik 需要 cluster-scope 资源时，会 watch Node。Kubelet 会周期性更新 Node status，里面有 conditions、images、capacity、allocatable 等字段。对 Kubernetes 来说，这是正常心跳。&lt;/p&gt;
&lt;p&gt;但 Traefik 在这里真正关心的通常只是 Node 地址，尤其是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;InternalIP&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ExternalIP&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果 Node 的地址没变，只是心跳字段变了，就不应该触发一次完整的动态配置 rebuild。否则你就会得到一个很稳定的“无意义重建节拍”。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;我这次具体怎么修&lt;/h2&gt;
&lt;p&gt;这次改动不大，但我刻意把它拆成几个很清楚的点。&lt;/p&gt;
&lt;h3&gt;EndpointSlice：给 informer cache 建索引&lt;/h3&gt;
&lt;p&gt;新增了一个 indexer：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const EndpointSliceServiceNameIndex = &quot;endpointSliceServiceName&quot;

var EndpointSliceServiceNameIndexers = cache.Indexers{
    EndpointSliceServiceNameIndex: endpointSliceServiceNameIndexFunc,
}

func EndpointSliceServiceNameIndexKey(namespace, serviceName string) string {
    return fmt.Sprintf(&quot;%s/%s&quot;, namespace, serviceName)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;索引 key 是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;namespace/serviceName
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后在各个 Kubernetes provider 的 EndpointSlice informer 上注册：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;endpointSliceInformer := factoryKube.Discovery().V1().EndpointSlices().Informer()
if err = endpointSliceInformer.AddIndexers(k8s.EndpointSliceServiceNameIndexers); err != nil {
    return nil, err
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;查询时不再 &lt;code&gt;.List(selector)&lt;/code&gt;，而是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;return k8s.EndpointSlicesByServiceName(
    informer.GetIndexer(),
    namespace,
    serviceName,
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这块我没有只改 Gateway provider，而是一起覆盖了：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;kubernetes/ingress&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;kubernetes/crd&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;kubernetes/gateway&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;kubernetes/ingress-nginx&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;原因很简单：这几个 provider 共享同一类 EndpointSlice lookup 成本。如果只修一个，问题还会在另一个入口形态里复现。&lt;/p&gt;
&lt;h3&gt;EndpointSlice event：比较值，不比较噪声&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;event_handler.go&lt;/code&gt; 里加了 EndpointSlice 的内容级判断。&lt;/p&gt;
&lt;p&gt;核心思路是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;service name label 变了，要 rebuild&lt;/li&gt;
&lt;li&gt;ports 变了，要 rebuild&lt;/li&gt;
&lt;li&gt;endpoint 地址变了，要 rebuild&lt;/li&gt;
&lt;li&gt;endpoint conditions 变了，要 rebuild&lt;/li&gt;
&lt;li&gt;只有 metadata / resourceVersion / managedFields 这类变化，不 rebuild&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这里有一个很小但很重要的细节：指针字段不能直接比指针地址。&lt;/p&gt;
&lt;p&gt;EndpointSlice 的 port name、port、protocol，以及 endpoint conditions 里有不少 pointer 字段。两个对象的值一样，但指针地址不同，这是正常的。所以我加了一个泛型 helper：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func samePtr[T comparable](a, b *T) bool {
    if a == nil || b == nil {
        return a == b
    }

    return *a == *b
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个点看起来很细，但在 informer update filter 里很容易踩坑。你以为自己在比较语义，实际上比较的是对象实例。&lt;/p&gt;
&lt;h3&gt;Node event：只看 Traefik 消费的地址&lt;/h3&gt;
&lt;p&gt;Node update 的过滤也类似。&lt;/p&gt;
&lt;p&gt;Traefik 这里关心的是 InternalIP / ExternalIP，所以我把 Node 地址抽成 set：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func nodeAddressSet(addresses []corev1.NodeAddress) map[corev1.NodeAddress]struct{} {
    result := map[corev1.NodeAddress]struct{}{}
    for _, address := range addresses {
        if address.Type != corev1.NodeInternalIP &amp;amp;&amp;amp; address.Type != corev1.NodeExternalIP {
            continue
        }
        result[address] = struct{}{}
    }

    return result
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样 hostname 变化、capacity 变化、condition 心跳变化，都不会误触发 rebuild。只有真正影响 Traefik 入口地址判断的变化，才会继续往下走。&lt;/p&gt;
&lt;h3&gt;Gateway listener：不要每条 Route 都扫所有 listener&lt;/h3&gt;
&lt;p&gt;Gateway provider 里还有一个额外优化。&lt;/p&gt;
&lt;p&gt;以前 &lt;code&gt;HTTPRoute&lt;/code&gt; / &lt;code&gt;GRPCRoute&lt;/code&gt; / &lt;code&gt;TLSRoute&lt;/code&gt; / &lt;code&gt;TCPRoute&lt;/code&gt; 匹配 Gateway listener 时，会拿 route 的 &lt;code&gt;parentRefs&lt;/code&gt; 和所有 gateway listeners 做匹配。Route 数量一多，这也是典型的重复扫描。&lt;/p&gt;
&lt;p&gt;我加了一个很小的 index：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type gatewayListenerIndex struct {
    byGateway map[ktypes.NamespacedName][]gatewayListener
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;构建配置时先把 listeners 按 &lt;code&gt;namespace/name&lt;/code&gt; 分组。后面每条 Route 只需要根据 parentRef 找对应 Gateway 的 listeners。&lt;/p&gt;
&lt;p&gt;这里不是为了炫技。它只是把数据结构调回正确形态：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;大量 Route -&amp;gt; 少量 Gateway listener
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这种关系本来就应该用索引查，而不是每次全量扫。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;修复后的结果&lt;/h2&gt;
&lt;p&gt;下面两张图是这次修复后的观测结果。&lt;/p&gt;
&lt;p&gt;第一张图里可以看到，前半段有比较密集的 CPU 尖刺；修复生效后，CPU 使用明显收敛到低位，后面只剩很低的波动。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.bangwu.me/img/20260613-traefik-cpu-before-after.webp&quot; alt=&quot;Traefik CPU spike before and after fix&quot; /&gt;&lt;/p&gt;
&lt;p&gt;第二张图是修复后单独拉出来看，CPU 基本没有再出现之前那种密集尖刺。图里还有一些短小波峰，但它们已经不像之前那样持续把 Traefik provider 拖进高 CPU 区间。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.bangwu.me/img/20260613-traefik-cpu-fixed.webp&quot; alt=&quot;Traefik CPU after fix&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这里我不想把话说太满。&lt;/p&gt;
&lt;p&gt;这两张图能说明的是：在这次测试环境里，修复后的 Traefik CPU 尖刺明显减少，稳定性更好。它不是一个严格 benchmark，也不能直接推导出所有集群都会下降多少百分比。&lt;/p&gt;
&lt;p&gt;但从问题机制上看，这个结果是符合预期的：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;lookup 从 namespace scan 变成 index lookup&lt;/li&gt;
&lt;li&gt;无关 EndpointSlice update 不再触发 rebuild&lt;/li&gt;
&lt;li&gt;Node heartbeat 不再周期性触发 rebuild&lt;/li&gt;
&lt;li&gt;Gateway Route 匹配 listener 的重复扫描减少&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这些点加起来，CPU 曲线自然会从“频繁被事件打醒”变成“只有配置语义真的变化时才工作”。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;我本地怎么验证&lt;/h2&gt;
&lt;p&gt;这次我至少跑了这两个包的测试：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;go test ./pkg/provider/kubernetes/k8s ./pkg/provider/kubernetes/gateway
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;结果是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ok  	github.com/traefik/traefik/v3/pkg/provider/kubernetes/k8s
ok  	github.com/traefik/traefik/v3/pkg/provider/kubernetes/gateway
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;新增测试主要覆盖三类行为：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;EndpointSlicesByServiceName&lt;/code&gt; 能按 &lt;code&gt;namespace/serviceName&lt;/code&gt; 精确取到 EndpointSlice&lt;/li&gt;
&lt;li&gt;EndpointSlice update filter 不会被无关 metadata 变化触发，但会保留 ports、addresses、conditions 的语义变化&lt;/li&gt;
&lt;li&gt;Node update filter 会忽略非地址字段变化，但 InternalIP / ExternalIP 变化仍然触发 rebuild&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Gateway 侧还加了一个 benchmark，用来覆盖大量 Gateway listener 和 Route 场景下的匹配路径：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func BenchmarkGatewayListenerIndexMatching(b *testing.B)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我比较在意的是测试边界，而不是只测 happy path。尤其是 condition 的 &lt;code&gt;nil / true / false&lt;/code&gt;，这个地方如果测得不细，很容易为了性能把正确性修坏。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;这类问题以后怎么查&lt;/h2&gt;
&lt;p&gt;如果以后我再遇到 Kubernetes controller / provider 的 CPU 抖动，我会优先按这个顺序查。&lt;/p&gt;
&lt;h3&gt;1. 先分清楚数据面和控制面&lt;/h3&gt;
&lt;p&gt;Traefik 的 CPU 高，不一定是请求转发高。&lt;/p&gt;
&lt;p&gt;先看：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;请求量有没有同步上升&lt;/li&gt;
&lt;li&gt;access log / metrics 里的 RPS 是否对应&lt;/li&gt;
&lt;li&gt;CPU 尖刺是否跟 rollout、scale、Node 事件、EndpointSlice 事件重合&lt;/li&gt;
&lt;li&gt;pprof 里热点是在转发路径，还是 provider rebuild 路径&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果热点在 provider，就不要先去调中间件、TLS、压缩这些数据面选项。&lt;/p&gt;
&lt;h3&gt;2. 看 informer event 是否真的有语义变化&lt;/h3&gt;
&lt;p&gt;很多控制器性能问题，本质是 event 太吵。&lt;/p&gt;
&lt;p&gt;我会重点看这些对象：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;EndpointSlice&lt;/li&gt;
&lt;li&gt;Service&lt;/li&gt;
&lt;li&gt;Secret&lt;/li&gt;
&lt;li&gt;Ingress / HTTPRoute / Gateway&lt;/li&gt;
&lt;li&gt;Node&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;然后问一个问题：这个 update 真的会改变最终配置吗？&lt;/p&gt;
&lt;p&gt;如果不会，就应该在 event handler 层过滤掉。靠后面的 hash、deep equal、throttle 都是补救，不是根治。&lt;/p&gt;
&lt;h3&gt;3. 看 hot path 里有没有 O(N) selector scan&lt;/h3&gt;
&lt;p&gt;client-go cache 不是数据库。&lt;/p&gt;
&lt;p&gt;如果你在热路径里不断做 &lt;code&gt;.List(selector)&lt;/code&gt;，一定要问自己：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;selector 是否可以变成 indexer&lt;/li&gt;
&lt;li&gt;key 是否可以提前构造&lt;/li&gt;
&lt;li&gt;是否可以在一次 rebuild 内做 per-service dedup&lt;/li&gt;
&lt;li&gt;是否会随着 namespace 对象数量线性增长&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;EndpointSlice 这个问题就是很典型的例子。数据结构迁移本来是为了扩展性，但调用方如果还用 scan 的心智，扩展性收益会被吃掉。&lt;/p&gt;
&lt;h3&gt;4. 看 rebuild 有没有被重复触发&lt;/h3&gt;
&lt;p&gt;有些 rebuild 是必要的，比如 Route 变了、Service endpoint 变了、Secret 证书变了。&lt;/p&gt;
&lt;p&gt;但有些 rebuild 只是噪声：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Node heartbeat&lt;/li&gt;
&lt;li&gt;metadata managedFields 更新&lt;/li&gt;
&lt;li&gt;controller annotation tick&lt;/li&gt;
&lt;li&gt;resourceVersion 变化但 payload 不变&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这些噪声如果不挡在入口，后面每一层都会付成本。&lt;/p&gt;
&lt;h3&gt;5. 最后再考虑 throttle&lt;/h3&gt;
&lt;p&gt;Traefik provider 本身有 throttle 相关配置，这类配置有用，但我不建议把它当第一优先级。&lt;/p&gt;
&lt;p&gt;throttle 能把很多事件合并成更少的 rebuild，但它没有改变两件事：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;单次 rebuild 本身还是贵&lt;/li&gt;
&lt;li&gt;无意义事件仍然会进入系统&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以我的顺序会是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;先减少无意义事件&lt;/li&gt;
&lt;li&gt;再降低单次 rebuild 成本&lt;/li&gt;
&lt;li&gt;最后用 throttle 做工程上的缓冲&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h2&gt;这次修复的边界&lt;/h2&gt;
&lt;p&gt;这次改动不是把所有 CPU 问题一次性解决。&lt;/p&gt;
&lt;p&gt;issue 里还提到了一些后续方向，比如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;annotation parse cache&lt;/li&gt;
&lt;li&gt;per-rebuild &lt;code&gt;loadService&lt;/code&gt; / &lt;code&gt;loadServers&lt;/code&gt; dedup&lt;/li&gt;
&lt;li&gt;去掉 provider event loop 里冗余的 hash&lt;/li&gt;
&lt;li&gt;informer ingest 阶段 strip &lt;code&gt;managedFields&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这些都还有继续优化空间。&lt;/p&gt;
&lt;p&gt;但我觉得这次最值得先合的是前面三类：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;EndpointSlice indexer&lt;/li&gt;
&lt;li&gt;EndpointSlice event filter&lt;/li&gt;
&lt;li&gt;Node event filter&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;原因是它们分别对应三个不同成本轴：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;修复点&lt;/th&gt;
&lt;th&gt;解决的问题&lt;/th&gt;
&lt;th&gt;风险&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;EndpointSlice indexer&lt;/td&gt;
&lt;td&gt;单次 backend lookup 扫描太贵&lt;/td&gt;
&lt;td&gt;低，语义不变&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;EndpointSlice event filter&lt;/td&gt;
&lt;td&gt;无关 slice update 触发 rebuild&lt;/td&gt;
&lt;td&gt;中，需要保留 conditions 语义&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Node event filter&lt;/td&gt;
&lt;td&gt;Node 心跳触发周期性 rebuild&lt;/td&gt;
&lt;td&gt;中，只能忽略 Traefik 不消费的字段&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gateway listener index&lt;/td&gt;
&lt;td&gt;大量 Route 匹配 listener 重复扫描&lt;/td&gt;
&lt;td&gt;低，数据结构优化&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;性能优化最怕的是“看起来快了，但语义被吞了”。所以这次我更倾向做保守修复：只优化明确浪费的路径，不碰 Gateway API 的行为语义。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;结尾&lt;/h2&gt;
&lt;p&gt;这次修 Traefik，我最大的感受还是那句话：控制面性能问题，很多时候不是某个地方特别慢，而是很多“没必要做”的事情被高频做了。&lt;/p&gt;
&lt;p&gt;EndpointSlice、Gateway API、informer cache 都是好东西。但它们放在一起以后，还是要回到很朴素的工程判断：&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;li&gt;正确性边界有没有被性能优化破坏&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;把这些问题问清楚，CPU 曲线自然会安静很多。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;参考&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/traefik/traefik/issues/13011&quot;&gt;Traefik issue #13011: High CPU regression since v3.1&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://kubernetes.io/docs/concepts/services-networking/endpoint-slices/&quot;&gt;Kubernetes EndpointSlices 文档&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://kubernetes.io/docs/concepts/architecture/nodes/&quot;&gt;Kubernetes Nodes 文档&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://doc.traefik.io/traefik/reference/install-configuration/providers/kubernetes/kubernetes-gateway/&quot;&gt;Traefik Kubernetes Gateway Provider 文档&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;前提：已按《K8s 集群使用 Gateway API 和 Traefik》完成部署，并按《落地清单》做了基础配置。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>让 AI 写好前端，我现在会先把这些话说清楚</title><link>https://bangwu.me/posts/ai-frontend-boundaries/</link><guid isPermaLink="true">https://bangwu.me/posts/ai-frontend-boundaries/</guid><description>这次我不是想讲“AI 很强”这种空话，而是把我自己做前端时真正在用的约束、验证和改法整理一下：页面要像什么，第一屏讲什么，哪些元素不能乱加，怎么让模型少走模板路。</description><pubDate>Fri, 15 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;先说结论。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;让 AI 写好前端，重点不是多给它一点“灵感”，而是先把它容易乱跑的边界收紧。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;我这阵子在折腾一个 tmux 相关的 web 项目，页面和终端渲染被我和 AI 来回改了很久。改到最后我越来越确定一件事：&lt;/p&gt;
&lt;p&gt;前端做不好，很多时候不是模型不会写代码，而是我们喂给它的要求太松了。&lt;/p&gt;
&lt;p&gt;你如果只说一句“帮我做个好看的页面”，它大概率会回到最常见的网页套路：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;hero 很大&lt;/li&gt;
&lt;li&gt;card 很多&lt;/li&gt;
&lt;li&gt;badge 一排&lt;/li&gt;
&lt;li&gt;shadow 和 radius 也不缺&lt;/li&gt;
&lt;li&gt;看起来很努力，但就是没有自己的味道&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;它能把东西做出来，但不一定能做成你想要的样子。&lt;/p&gt;
&lt;h2&gt;1. 先分清楚：你到底要它做什么&lt;/h2&gt;
&lt;p&gt;这个问题看起来简单，但其实最关键。&lt;/p&gt;
&lt;p&gt;很多前端任务混着三件事：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;页面结构&lt;/strong&gt;：信息怎么排&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;视觉方向&lt;/strong&gt;：看起来是什么气质&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;交互边界&lt;/strong&gt;：哪些地方脆弱，不能乱碰&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果这三件事混在一起，AI 就会很容易把注意力放错。&lt;/p&gt;
&lt;p&gt;比如我那个项目里，终端视口其实是最脆弱的部分。它不是普通卡片，也不是普通文本块，更不是一个可以随便加阴影、加浮层、加复杂布局的区域。&lt;/p&gt;
&lt;p&gt;它更像一个设备边界。&lt;/p&gt;
&lt;p&gt;所以我后来改法就变了：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;产品骨架先稳定&lt;/li&gt;
&lt;li&gt;terminal 只负责 terminal&lt;/li&gt;
&lt;li&gt;外层 chrome 只负责信息和操作&lt;/li&gt;
&lt;li&gt;视觉装饰尽量收敛&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这一步很重要。因为如果你不先告诉模型“哪个模块不能乱碰”，它就会自然而然把每个模块都当成可优化对象。&lt;/p&gt;
&lt;h2&gt;2. AI 最容易回退到模板味&lt;/h2&gt;
&lt;p&gt;这个我觉得是 AI 前端里最常见的问题。&lt;/p&gt;
&lt;p&gt;只要 prompt 稍微模糊一点，它就会滑回训练数据里的高频网页模式。&lt;/p&gt;
&lt;p&gt;结果通常长这样：&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;li&gt;但一眼看上去就是“AI 做的”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;为什么会这样？&lt;/p&gt;
&lt;p&gt;因为模型默认会选最稳的解，而不是最有个性的解。&lt;/p&gt;
&lt;p&gt;而所谓“稳”，对很多前端任务来说，恰好就是模板味。&lt;/p&gt;
&lt;p&gt;所以如果你真的想让它写出有 taste 的页面，不能只让它“自由发挥”，而要给它更具体的偏好：&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;li&gt;哪些信息一定要先出现&lt;/li&gt;
&lt;li&gt;哪些层级必须压住&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这不是限制 creativity。&lt;/p&gt;
&lt;p&gt;相反，这是在给 creativity 一个能落地的边界。&lt;/p&gt;
&lt;h2&gt;3. 第一屏不是信息仓库&lt;/h2&gt;
&lt;p&gt;我现在越来越不喜欢那种什么都往第一屏塞的做法。&lt;/p&gt;
&lt;p&gt;很多 AI 页面看起来“内容很多”，实际上是没想清楚第一屏到底干什么。&lt;/p&gt;
&lt;p&gt;第一屏最重要的不是堆信息，而是先把这几件事说清楚：&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;li&gt;你这个页面的主情绪是什么&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果第一屏什么都想讲，最后通常什么都讲不清楚。&lt;/p&gt;
&lt;p&gt;我现在比较认可的一种方法是：&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;li&gt;最后再给操作入口&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;也就是每一段只做一件事。&lt;/p&gt;
&lt;p&gt;这个原则看起来朴素，但很好用。因为 AI 很容易把 section 写成平铺直叙的内容清单，而不是有职责的页面段落。&lt;/p&gt;
&lt;h2&gt;4. 默认不要卡片，真的有用&lt;/h2&gt;
&lt;p&gt;这个判断很狠，但我发现它很好用。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;很多 AI 页面的问题，本质上是太喜欢卡片了。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;什么都要卡片：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;hero 里要卡片&lt;/li&gt;
&lt;li&gt;特性区要卡片&lt;/li&gt;
&lt;li&gt;CTA 附近也要卡片&lt;/li&gt;
&lt;li&gt;甚至一段说明文字也要 border + shadow + radius&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;最后页面就很“忙”，但没有重点。&lt;/p&gt;
&lt;p&gt;我现在会先问自己一个问题：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;如果去掉 border、shadow、background、radius，这块内容还成立吗？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;如果还成立，那它大概率就不该是卡片。&lt;/p&gt;
&lt;p&gt;这个判断能帮你砍掉很多不必要的视觉噪音。&lt;/p&gt;
&lt;p&gt;而且它特别适合拿来约束 AI，因为模型很爱“补装饰”。你不给规则，它就一直补。&lt;/p&gt;
&lt;h2&gt;5. 先定设计系统，再让它写&lt;/h2&gt;
&lt;p&gt;AI 写前端最怕的不是不会写，而是写着写着风格漂了。&lt;/p&gt;
&lt;p&gt;今天一套颜色，明天又一套按钮，后天再换一层背景逻辑，最后整个页面就散了。&lt;/p&gt;
&lt;p&gt;所以我现在更倾向于先定这些东西：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;background / surface / text / muted / accent&lt;/li&gt;
&lt;li&gt;主要字号层级&lt;/li&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;这套东西最好在 prompt 之外就先写成 token 或设计说明。&lt;/p&gt;
&lt;p&gt;因为 AI 其实挺擅长遵守“明确的系统”，不太擅长自己现场发明一套又一套。&lt;/p&gt;
&lt;p&gt;如果你一开始就把系统定住，它后面写页面会稳很多。&lt;/p&gt;
&lt;h2&gt;6. 视觉参考比“高级”这种词更重要&lt;/h2&gt;
&lt;p&gt;“高级”“极简”“有氛围”这些词，听起来很对，但其实很滑。&lt;/p&gt;
&lt;p&gt;模型看到这种词，往往只能往自己最熟的方向靠。&lt;/p&gt;
&lt;p&gt;所以我现在更愿意直接给它：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;参考图&lt;/li&gt;
&lt;li&gt;mood board&lt;/li&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;因为这些东西比“高级一点”这种抽象词有用得多。&lt;/p&gt;
&lt;p&gt;前端设计不是让模型猜，而是让它有一个明确的视觉方向可以靠。&lt;/p&gt;
&lt;p&gt;尤其是有 taste 的项目，很多时候不是模块不够，而是审美方向不统一。&lt;/p&gt;
&lt;h2&gt;7. 给它页面叙事，不只是组件清单&lt;/h2&gt;
&lt;p&gt;这个点我觉得也很关键。&lt;/p&gt;
&lt;p&gt;很多人让 AI 做页面的时候，给的是组件列表：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;header&lt;/li&gt;
&lt;li&gt;hero&lt;/li&gt;
&lt;li&gt;feature cards&lt;/li&gt;
&lt;li&gt;CTA&lt;/li&gt;
&lt;li&gt;footer&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;但页面不是部件拼装，它应该有自己的叙事。&lt;/p&gt;
&lt;p&gt;比如：&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;li&gt;最后落到动作&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果你只给模块，它就会把这些模块平均地摆出来。&lt;/p&gt;
&lt;p&gt;如果你给的是叙事，它才知道每个 section 的职责。&lt;/p&gt;
&lt;p&gt;我现在会直接在需求里写：&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;li&gt;哪些元素不能抢主视觉&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这会比“做个漂亮页面”有效得多。&lt;/p&gt;
&lt;h2&gt;8. 验证链路要早点放进去&lt;/h2&gt;
&lt;p&gt;前端最烦的一种情况是：&lt;/p&gt;
&lt;p&gt;代码看起来对，页面其实不对。&lt;/p&gt;
&lt;p&gt;比如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;移动端溢出&lt;/li&gt;
&lt;li&gt;fixed 层遮住按钮&lt;/li&gt;
&lt;li&gt;某个断点布局崩了&lt;/li&gt;
&lt;li&gt;终端视口尺寸不对&lt;/li&gt;
&lt;li&gt;页面 chrome 压到内容&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这些东西只看代码经常看不出来，必须跑起来看。&lt;/p&gt;
&lt;p&gt;所以我现在会尽量让 AI 带着验证去改，而不是只让它改完代码就算了。&lt;/p&gt;
&lt;p&gt;最简单的方式就是：&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;li&gt;确认关键交互能点&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果能加 Playwright 这种自动化检查，会更稳。&lt;/p&gt;
&lt;p&gt;它不是为了“显得工程化”，而是真的能减少很多肉眼不容易发现的问题。&lt;/p&gt;
&lt;h2&gt;9. 我现在让 AI 写前端的默认流程&lt;/h2&gt;
&lt;p&gt;我大概会这么做：&lt;/p&gt;
&lt;h3&gt;先写清楚边界&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;这个页面是给谁用的&lt;/li&gt;
&lt;li&gt;它是管理台、控制台，还是展示页&lt;/li&gt;
&lt;li&gt;哪个区域是脆弱区，不能乱改&lt;/li&gt;
&lt;li&gt;哪些模块可以大胆做视觉&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;再给明确的 taste&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;深色还是浅色&lt;/li&gt;
&lt;li&gt;克制还是热闹&lt;/li&gt;
&lt;li&gt;偏产品感还是偏编辑感&lt;/li&gt;
&lt;li&gt;更像工具，还是更像作品&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;然后给验证目标&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;桌面端要怎样&lt;/li&gt;
&lt;li&gt;移动端要怎样&lt;/li&gt;
&lt;li&gt;关键状态要怎样&lt;/li&gt;
&lt;li&gt;不允许出现什么问题&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;最后再让它动手&lt;/h3&gt;
&lt;p&gt;这样出来的结果通常比“直接开始写”稳很多。&lt;/p&gt;
&lt;h2&gt;10. 说到底，前端不是堆组件&lt;/h2&gt;
&lt;p&gt;我越来越觉得，AI 前端最重要的不是“会不会生成页面”，而是你有没有把 art direction 这件事交代明白。&lt;/p&gt;
&lt;p&gt;很多时候，页面难看不是因为代码错了，而是因为：&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;li&gt;验证太晚&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以我现在更相信一句话：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;让 AI 写好前端，不是给它更多自由，而是给它更好的边界。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;边界清楚了，页面才容易长出自己的味道。&lt;/p&gt;
&lt;p&gt;而这件事，往往比“再来几个版本”更重要。&lt;/p&gt;
</content:encoded></item><item><title>LLM Wiki：把上下文编译成会生长的知识库</title><link>https://bangwu.me/posts/llm-wiki-shared-context/</link><guid isPermaLink="true">https://bangwu.me/posts/llm-wiki-shared-context/</guid><description>Karpathy 的 LLM Wiki 不是又一个 RAG 方案，而是一种让 LLM 持续维护 Markdown Wiki 的知识工作流。它可以服务个人知识库，也可以管理组织共享 context。</description><pubDate>Mon, 27 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;TL;DR&lt;/h2&gt;
&lt;p&gt;Karpathy 这个 &lt;a href=&quot;https://gist.github.com/karpathy/442a6bf555914893e9891c11519de94f&quot;&gt;LLM Wiki&lt;/a&gt; 想法，我觉得最重要的地方不是“用 AI 总结文档”，而是它换了一个知识库的维护方式：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;不要每次提问时才从原始资料里重新检索、重新拼答案，而是让 LLM 持续把资料编译成一个结构化、可链接、会更新的 Markdown Wiki。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;说白了，传统 RAG 更像是“临时翻资料回答问题”，LLM Wiki 更像是“先把资料整理成一本不断修订的内部手册”。&lt;/p&gt;
&lt;p&gt;这两件事听起来都和知识库有关，但实际体验差别很大。&lt;/p&gt;
&lt;p&gt;RAG 的问题是，它经常没有积累。你今天问一个问题，它检索一遍，拼一次答案；明天换个角度问，它又从头检索、从头拼。中间产生的理解、对比、矛盾、结论，很容易留在聊天记录里，然后慢慢消失。&lt;/p&gt;
&lt;p&gt;LLM Wiki 的判断是：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;有价值的知识加工结果，不应该只存在于一次对话里，而应该被写回知识库。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;我觉得这点很关键。&lt;/p&gt;
&lt;p&gt;尤其是现在大家越来越依赖 Agent 做事，真正稀缺的不是“模型会不会回答”，而是模型能不能稳定读到一份干净、最新、可追溯的上下文。&lt;/p&gt;
&lt;p&gt;所以这篇不只是介绍个人知识库。我更想说的是：LLM Wiki 也很适合用来管理&lt;strong&gt;组织共享 context&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;比如团队的项目背景、客户信息、会议结论、架构决策、产品路线、历史坑点、运营复盘。以前这些东西散在 Slack、飞书、Notion、GitHub issue、会议纪要和人的脑子里。LLM Wiki 的价值，就是把它们慢慢编译成一套组织可复用的上下文层。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;1. LLM Wiki 到底是什么&lt;/h2&gt;
&lt;p&gt;Karpathy 原文把 LLM Wiki 描述成一种 pattern：用 LLM 构建个人知识库。&lt;/p&gt;
&lt;p&gt;它不是一个具体产品，而是一套工作方法。核心结构很简单，三层：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;raw sources  -&amp;gt;  wiki  -&amp;gt;  schema
原始资料        LLM 维护的页面   规则和工作流
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Raw sources：原始资料&lt;/h3&gt;
&lt;p&gt;这是事实来源。文章、论文、会议纪要、PDF、网页剪藏、客户访谈、项目文档都可以放这里。&lt;/p&gt;
&lt;p&gt;原则是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;原始资料尽量不可变&lt;/li&gt;
&lt;li&gt;LLM 可以读，但不要随便改&lt;/li&gt;
&lt;li&gt;后续 Wiki 页面要能追溯到来源&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这层像 git 里的 source of truth。&lt;/p&gt;
&lt;h3&gt;Wiki：LLM 维护的 Markdown 页面&lt;/h3&gt;
&lt;p&gt;这是真正被“编译”出来的知识层。&lt;/p&gt;
&lt;p&gt;LLM 会把原始资料整理成：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;source summary&lt;/li&gt;
&lt;li&gt;entity page&lt;/li&gt;
&lt;li&gt;concept page&lt;/li&gt;
&lt;li&gt;comparison&lt;/li&gt;
&lt;li&gt;synthesis&lt;/li&gt;
&lt;li&gt;overview&lt;/li&gt;
&lt;li&gt;index&lt;/li&gt;
&lt;li&gt;query result&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;更重要的是，LLM 不只是新增页面，还要更新已有页面。&lt;/p&gt;
&lt;p&gt;比如你导入了一篇新文章，它不应该只生成一篇摘要就结束，而应该顺手检查：&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;li&gt;是否要增加新的 &lt;code&gt;[[wikilink]]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;是否要在 index 里登记&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这就是“会生长”的部分。&lt;/p&gt;
&lt;h3&gt;Schema：让 LLM 不要乱写的规则&lt;/h3&gt;
&lt;p&gt;Schema 可以是 &lt;code&gt;AGENTS.md&lt;/code&gt;、&lt;code&gt;CLAUDE.md&lt;/code&gt;、&lt;code&gt;schema.md&lt;/code&gt; 之类的文件。&lt;/p&gt;
&lt;p&gt;它告诉 LLM：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;目录怎么组织&lt;/li&gt;
&lt;li&gt;页面类型有哪些&lt;/li&gt;
&lt;li&gt;frontmatter 怎么写&lt;/li&gt;
&lt;li&gt;ingest 怎么做&lt;/li&gt;
&lt;li&gt;query 怎么做&lt;/li&gt;
&lt;li&gt;lint 检查什么&lt;/li&gt;
&lt;li&gt;哪些内容需要人类确认&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我会把 schema 理解成这个 Wiki 的“操作系统说明书”。&lt;/p&gt;
&lt;p&gt;没有 schema，LLM 很容易变成一个泛泛的聊天助手；有了 schema，它才更像一个纪律比较稳定的知识库维护员。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;2. 它和 RAG 最大的区别：不是检索，而是积累&lt;/h2&gt;
&lt;p&gt;很多人看到 LLM Wiki，第一反应可能是：这不还是 RAG 吗？&lt;/p&gt;
&lt;p&gt;我觉得不是。&lt;/p&gt;
&lt;p&gt;当然，它也会检索，也会回答问题，也可能用 embedding。但它的核心不在检索，而在&lt;strong&gt;把知识加工结果持久化&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;可以这样对比：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;维度&lt;/th&gt;
&lt;th&gt;传统 RAG&lt;/th&gt;
&lt;th&gt;LLM Wiki&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;资料处理&lt;/td&gt;
&lt;td&gt;多数在查询时临时检索&lt;/td&gt;
&lt;td&gt;摄入时就整理进 Wiki&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;知识状态&lt;/td&gt;
&lt;td&gt;偏无状态&lt;/td&gt;
&lt;td&gt;有持续更新的中间层&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;回答来源&lt;/td&gt;
&lt;td&gt;原始 chunk&lt;/td&gt;
&lt;td&gt;Wiki 页面 + 原始来源&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;维护方式&lt;/td&gt;
&lt;td&gt;依赖索引和召回&lt;/td&gt;
&lt;td&gt;依赖页面更新、链接、lint&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;产物&lt;/td&gt;
&lt;td&gt;一次回答&lt;/td&gt;
&lt;td&gt;可复用 Markdown 页面&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;人类角色&lt;/td&gt;
&lt;td&gt;上传资料、提问&lt;/td&gt;
&lt;td&gt;策展资料、校正方向、审核关键判断&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;RAG 更像是“给模型一个资料仓库”。&lt;/p&gt;
&lt;p&gt;LLM Wiki 更像是“让模型持续维护一套读书笔记、项目手册、研究档案”。&lt;/p&gt;
&lt;p&gt;这就带来一个很实际的变化：&lt;/p&gt;
&lt;p&gt;你问过的好问题、生成过的好对比、整理过的好结论，都可以被保存成 Wiki 页面，之后继续被引用、更新和链接。&lt;/p&gt;
&lt;p&gt;不是一次对话结束就没了。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;3. 三个核心操作：Ingest、Query、Lint&lt;/h2&gt;
&lt;p&gt;Karpathy 原文里把操作分成三个，我觉得这个拆法很适合落地。&lt;/p&gt;
&lt;h3&gt;Ingest：摄入资料&lt;/h3&gt;
&lt;p&gt;摄入不是“上传文件并向量化”这么简单。&lt;/p&gt;
&lt;p&gt;一个好的 ingest 应该包括：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;读取新资料&lt;/li&gt;
&lt;li&gt;提取关键实体、概念、论点&lt;/li&gt;
&lt;li&gt;生成资料摘要页&lt;/li&gt;
&lt;li&gt;更新相关 entity / concept 页面&lt;/li&gt;
&lt;li&gt;记录矛盾、补充、变化&lt;/li&gt;
&lt;li&gt;更新 index 和 log&lt;/li&gt;
&lt;li&gt;需要人判断的地方标出来&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;比如一篇关于某个开源项目的文章被导入后，它可能会影响这些页面：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;wiki/sources/article-name.md
wiki/entities/project-name.md
wiki/concepts/context-management.md
wiki/synthesis/current-architecture.md
wiki/index.md
wiki/log.md
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一个源文件影响 10 个页面并不奇怪。&lt;/p&gt;
&lt;p&gt;这也是 LLM 比人适合维护 Wiki 的地方。人会觉得“更新交叉引用、维护索引、同步摘要”很烦，但 LLM 不会。&lt;/p&gt;
&lt;h3&gt;Query：查询 Wiki&lt;/h3&gt;
&lt;p&gt;查询时，不是直接从原始资料里找 chunk，而是先读 Wiki。&lt;/p&gt;
&lt;p&gt;流程大概是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;读 &lt;code&gt;index.md&lt;/code&gt; 找相关页面&lt;/li&gt;
&lt;li&gt;读取相关 Wiki 页面&lt;/li&gt;
&lt;li&gt;必要时追溯 raw sources&lt;/li&gt;
&lt;li&gt;综合回答&lt;/li&gt;
&lt;li&gt;如果回答有长期价值，写回 &lt;code&gt;wiki/queries/&lt;/code&gt; 或 &lt;code&gt;wiki/synthesis/&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这里最后一步很重要。&lt;/p&gt;
&lt;p&gt;很多时候，真正有价值的不是原始问题，而是回答里生成的新结构：&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;li&gt;一个概念之间的关系图&lt;/li&gt;
&lt;li&gt;一个“目前我们知道什么 / 不知道什么”的总结&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这些都应该进入 Wiki，而不是停在 chat history。&lt;/p&gt;
&lt;h3&gt;Lint：维护健康度&lt;/h3&gt;
&lt;p&gt;Wiki 长大以后一定会变脏。&lt;/p&gt;
&lt;p&gt;会出现：&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;li&gt;新资料和旧结论冲突&lt;/li&gt;
&lt;li&gt;重要概念被频繁提到，但没有独立页面&lt;/li&gt;
&lt;li&gt;页面之间缺少链接&lt;/li&gt;
&lt;li&gt;index 已经过时&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以需要定期 lint。&lt;/p&gt;
&lt;p&gt;我觉得 LLM Wiki 里的 lint 很有意思，因为它不是代码格式化那种 lint，而是“知识库体检”。&lt;/p&gt;
&lt;p&gt;可以让 LLM 定期检查：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;请检查当前 Wiki：
1. 有没有孤立页面
2. 有没有互相矛盾的结论
3. 有没有应该创建但还没有创建的概念页
4. 有没有页面缺少 sources
5. 有没有过时的 index 条目
6. 有没有值得继续研究的知识空白
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这件事如果让人做，大概率没人愿意长期坚持。&lt;/p&gt;
&lt;p&gt;但让 LLM 做，就很合理。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;4. nashsu/llm_wiki：把这个 pattern 做成桌面应用&lt;/h2&gt;
&lt;p&gt;Karpathy 的 gist 更像是一份思想说明，真正落地还要自己搭目录、写 schema、选工具、接 LLM。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/nashsu/llm_wiki&quot;&gt;nashsu/llm_wiki&lt;/a&gt; 做的事情，就是把这个 pattern 做成了一个跨平台桌面应用。&lt;/p&gt;
&lt;p&gt;它的定位是：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;LLM reads your documents, builds a structured wiki, and keeps it current.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;也就是让 LLM 读取你的文档，构建结构化 Wiki，并持续更新。&lt;/p&gt;
&lt;p&gt;它保留了 Karpathy 的核心设计：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;三层架构：Raw Sources → Wiki → Schema&lt;/li&gt;
&lt;li&gt;三个操作：Ingest / Query / Lint&lt;/li&gt;
&lt;li&gt;&lt;code&gt;index.md&lt;/code&gt; 作为导航入口&lt;/li&gt;
&lt;li&gt;&lt;code&gt;log.md&lt;/code&gt; 记录操作历史&lt;/li&gt;
&lt;li&gt;&lt;code&gt;[[wikilink]]&lt;/code&gt; 做交叉引用&lt;/li&gt;
&lt;li&gt;YAML frontmatter 保存元数据&lt;/li&gt;
&lt;li&gt;Obsidian 兼容&lt;/li&gt;
&lt;li&gt;人类策展，LLM 维护&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;然后它在这个基础上加了很多产品化能力。&lt;/p&gt;
&lt;p&gt;我比较关注这几个。&lt;/p&gt;
&lt;h3&gt;4.1 两步摄入&lt;/h3&gt;
&lt;p&gt;原始 pattern 里，LLM 读取资料并写入 Wiki 可以是一个连续动作。&lt;/p&gt;
&lt;p&gt;llm_wiki 把它拆成两步：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;第一步：分析资料
第二步：生成 Wiki 页面
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第一步先让 LLM 提取：&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;li&gt;和现有 Wiki 的关联&lt;/li&gt;
&lt;li&gt;潜在矛盾&lt;/li&gt;
&lt;li&gt;页面结构建议&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;第二步再根据分析结果生成或更新页面。&lt;/p&gt;
&lt;p&gt;这个设计我觉得是对的。因为“边读边写”很容易让模型只写出一篇普通摘要，而不是认真思考它该影响 Wiki 的哪些部分。&lt;/p&gt;
&lt;p&gt;先分析，再落库，质量会更稳一点。&lt;/p&gt;
&lt;h3&gt;4.2 purpose.md：不只规定结构，还规定方向&lt;/h3&gt;
&lt;p&gt;llm_wiki 在 schema 之外增加了 &lt;code&gt;purpose.md&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;schema 更像是“怎么写”：页面类型、命名规则、工作流。&lt;/p&gt;
&lt;p&gt;purpose 则是“为什么写”：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;这个 Wiki 的目标是什么&lt;/li&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;这点很重要。&lt;/p&gt;
&lt;p&gt;因为同一份资料，在不同目的下应该被整理成不同的知识。&lt;/p&gt;
&lt;p&gt;比如一篇关于 Agent 的文章：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果你的目的是学习技术，它可能进入 &lt;code&gt;concepts/agent-architecture.md&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;如果你的目的是做产品竞品分析，它可能进入 &lt;code&gt;entities/product-x.md&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;如果你的目的是组织知识沉淀，它可能进入 &lt;code&gt;synthesis/team-agent-practices.md&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;没有 purpose，LLM 只能泛泛总结。&lt;/p&gt;
&lt;p&gt;有了 purpose，它才知道哪些信息该强调，哪些只是背景。&lt;/p&gt;
&lt;h3&gt;4.3 知识图谱和洞察&lt;/h3&gt;
&lt;p&gt;llm_wiki 不只是生成 Markdown，还会根据 wikilinks、sources、页面类型等信号建立知识图谱。&lt;/p&gt;
&lt;p&gt;它的关联度模型包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;直接链接&lt;/li&gt;
&lt;li&gt;来源重叠&lt;/li&gt;
&lt;li&gt;Adamic-Adar 共同邻居&lt;/li&gt;
&lt;li&gt;类型亲和&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;还支持 Louvain 社区检测，用来发现自然形成的知识聚类。&lt;/p&gt;
&lt;p&gt;我觉得图谱的意义不是“看起来很酷”，而是帮你发现 Wiki 的结构问题：&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;li&gt;哪些节点连接了多个领域&lt;/li&gt;
&lt;li&gt;哪些地方值得继续 Deep Research&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;也就是说，它不是单纯可视化，而是把 lint 和研究方向结合起来。&lt;/p&gt;
&lt;h3&gt;4.4 多格式资料和网页剪藏&lt;/h3&gt;
&lt;p&gt;实际用知识库时，资料不会只来自 Markdown。&lt;/p&gt;
&lt;p&gt;llm_wiki 支持 PDF、DOCX、PPTX、表格、图片、视频音频预览，也有 Chrome Web Clipper，把网页通过 Readability.js 和 Turndown.js 转成干净 Markdown 后摄入。&lt;/p&gt;
&lt;p&gt;这个点很现实。&lt;/p&gt;
&lt;p&gt;很多知识库失败不是因为理念不对，而是资料入口太麻烦。&lt;/p&gt;
&lt;p&gt;如果导入资料这一步很痛苦，后面再好的 Wiki 也长不起来。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;5. 不只是个人知识库，也可以是组织共享 context&lt;/h2&gt;
&lt;p&gt;Karpathy 原文虽然标题说 personal knowledge bases，但他也提到了 business/team 场景。&lt;/p&gt;
&lt;p&gt;我觉得这是 LLM Wiki 更值得展开的方向。&lt;/p&gt;
&lt;p&gt;因为现在很多团队其实都在缺一种东西：&lt;strong&gt;稳定、共享、可更新的上下文层&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;不是缺文档。&lt;/p&gt;
&lt;p&gt;文档大家都有。缺的是这些文档能不能被持续整理成 Agent 和人都能读懂的 context。&lt;/p&gt;
&lt;h3&gt;组织里的 context 通常散在哪里&lt;/h3&gt;
&lt;p&gt;一个真实团队的上下文大概会散在这些地方：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Slack / 飞书群聊天&lt;/li&gt;
&lt;li&gt;会议纪要&lt;/li&gt;
&lt;li&gt;GitHub issue / PR&lt;/li&gt;
&lt;li&gt;Notion / 语雀 / 飞书文档&lt;/li&gt;
&lt;li&gt;客户访谈录音&lt;/li&gt;
&lt;li&gt;产品需求文档&lt;/li&gt;
&lt;li&gt;事故复盘&lt;/li&gt;
&lt;li&gt;架构设计文档&lt;/li&gt;
&lt;li&gt;代码里的注释和 README&lt;/li&gt;
&lt;li&gt;老员工脑子里&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;最麻烦的是，这些信息不是静态的。&lt;/p&gt;
&lt;p&gt;产品方向会变，技术方案会变，客户需求会变，旧决策会被新约束推翻。&lt;/p&gt;
&lt;p&gt;如果没有一个持续维护的中间层，团队上下文很快就会变成“谁记得谁知道”。&lt;/p&gt;
&lt;h3&gt;LLM Wiki 可以怎么用于团队&lt;/h3&gt;
&lt;p&gt;一个组织版 LLM Wiki 可以这样设计：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;company-context/
  purpose.md
  schema.md
  raw/
    meetings/
    prds/
    customer-calls/
    incidents/
    github/
    research/
  wiki/
    index.md
    log.md
    overview.md
    entities/
      customers/
      products/
      teams/
      systems/
    concepts/
    decisions/
    incidents/
    projects/
    playbooks/
    queries/
    synthesis/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;它不一定替代 Notion，也不一定替代代码仓库。&lt;/p&gt;
&lt;p&gt;它更像是一个“context repo”。&lt;/p&gt;
&lt;p&gt;团队可以把重要资料持续喂进去，让 LLM 维护：&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;li&gt;ADR 决策页&lt;/li&gt;
&lt;li&gt;事故复盘索引&lt;/li&gt;
&lt;li&gt;产品路线总结&lt;/li&gt;
&lt;li&gt;竞品比较&lt;/li&gt;
&lt;li&gt;新人 onboarding 手册&lt;/li&gt;
&lt;li&gt;Agent 执行任务前需要读的上下文&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这对 Agent 尤其有用。&lt;/p&gt;
&lt;p&gt;因为 Agent 最怕的不是不会写代码，而是上下文不完整。它不知道这个项目为什么这样设计，不知道之前踩过什么坑，不知道哪些方案已经被否了，不知道某个客户为什么重要。&lt;/p&gt;
&lt;p&gt;如果组织有一套共享 LLM Wiki，Agent 每次做事前就可以先读相关 context，而不是重新问人。&lt;/p&gt;
&lt;h3&gt;人和 LLM 的分工&lt;/h3&gt;
&lt;p&gt;我会这样分：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;角色&lt;/th&gt;
&lt;th&gt;负责什么&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;人&lt;/td&gt;
&lt;td&gt;决定哪些资料可信、哪些结论重要、哪些内容能公开&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LLM&lt;/td&gt;
&lt;td&gt;摘要、归档、交叉引用、更新页面、发现矛盾&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reviewer&lt;/td&gt;
&lt;td&gt;审核关键判断，处理冲突和敏感信息&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Agent&lt;/td&gt;
&lt;td&gt;在执行任务前读取相关 context，并把新结果写回&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;这里一定要保留人类审核。&lt;/p&gt;
&lt;p&gt;尤其是组织场景，LLM 不应该直接把所有东西都自动变成“团队共识”。&lt;/p&gt;
&lt;p&gt;我比较建议把页面分层：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;wiki/drafts/       # LLM 初稿
wiki/review/       # 待审核
wiki/accepted/     # 已确认，可作为共享上下文
wiki/deprecated/   # 过时内容
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或者在 frontmatter 里标状态：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;status: draft | reviewed | accepted | deprecated
confidence: low | medium | high
sources:
  - raw/meetings/2026-04-20-weekly.md
owners:
  - platform-team
updated: 2026-04-27
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样 Agent 读的时候就知道哪些内容能直接当依据，哪些只是参考。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;6. 我会怎么落地一个团队版 LLM Wiki&lt;/h2&gt;
&lt;p&gt;如果是我来做，我不会一开始就把整个公司资料都倒进去。&lt;/p&gt;
&lt;p&gt;那样很容易变成另一个没人维护的大坟场。&lt;/p&gt;
&lt;p&gt;我会从一个小范围开始，比如：&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;li&gt;一个正在重构的系统&lt;/li&gt;
&lt;li&gt;一个需要长期研究的技术方向&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;第一步：先写 purpose&lt;/h3&gt;
&lt;p&gt;先回答这些问题：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# purpose.md

这个 Wiki 服务谁？

它主要解决什么问题？

哪些内容应该进入？

哪些内容不应该进入？

哪些结论需要人工审核后才能成为 accepted？

当前最重要的 5 个问题是什么？
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我觉得 purpose 比工具选择更重要。&lt;/p&gt;
&lt;p&gt;如果目的不清楚，Wiki 只会变成更漂亮的资料堆。&lt;/p&gt;
&lt;h3&gt;第二步：定义 schema&lt;/h3&gt;
&lt;p&gt;schema 不要太复杂，先够用。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# schema.md

## 页面类型

- source：资料摘要
- entity：客户、系统、团队、产品
- concept：概念、方法、技术
- decision：架构 / 产品决策
- incident：事故复盘
- playbook：操作手册
- synthesis：跨资料综合

## 必填 frontmatter

- title
- type
- status
- sources
- owners
- updated

## 规则

- 所有事实性判断必须有 sources
- 不确定内容标 confidence: low
- 冲突结论不要覆盖，先写到 review
- accepted 页面才能作为 Agent 默认上下文
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;第三步：先摄入高价值资料&lt;/h3&gt;
&lt;p&gt;不要把群聊全量导进去。&lt;/p&gt;
&lt;p&gt;先选这些：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;最近 10 次关键会议纪要&lt;/li&gt;
&lt;li&gt;最新 PRD&lt;/li&gt;
&lt;li&gt;最近 3 次事故复盘&lt;/li&gt;
&lt;li&gt;当前架构文档&lt;/li&gt;
&lt;li&gt;客户 Top 10 高频问题&lt;/li&gt;
&lt;li&gt;已经确认的 ADR&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;让 Wiki 先长出骨架。&lt;/p&gt;
&lt;h3&gt;第四步：固定节奏 lint&lt;/h3&gt;
&lt;p&gt;比如每周跑一次：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;哪些页面过时&lt;/li&gt;
&lt;li&gt;哪些 accepted 页面缺 sources&lt;/li&gt;
&lt;li&gt;哪些项目没有 owner&lt;/li&gt;
&lt;li&gt;哪些新会议结论和旧决策冲突&lt;/li&gt;
&lt;li&gt;哪些主题被频繁提到但没有独立页面&lt;/li&gt;
&lt;li&gt;哪些内容应该进入新人 onboarding&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这一步很像打扫房间。&lt;/p&gt;
&lt;p&gt;不做也能用，但时间久了一定乱。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;7. 风险和边界&lt;/h2&gt;
&lt;p&gt;LLM Wiki 不是银弹。&lt;/p&gt;
&lt;p&gt;我现在比较在意这几个风险。&lt;/p&gt;
&lt;h3&gt;1. LLM 会把不确定内容写得太确定&lt;/h3&gt;
&lt;p&gt;所以必须有：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;sources&lt;/li&gt;
&lt;li&gt;status&lt;/li&gt;
&lt;li&gt;confidence&lt;/li&gt;
&lt;li&gt;reviewer&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;尤其是组织共享 context，不能让 LLM 直接替团队“宣布事实”。&lt;/p&gt;
&lt;h3&gt;2. 摄入太多会污染 Wiki&lt;/h3&gt;
&lt;p&gt;不是所有东西都值得进入 Wiki。&lt;/p&gt;
&lt;p&gt;闲聊、临时讨论、未确认想法，如果不加筛选全倒进去，只会制造噪音。&lt;/p&gt;
&lt;p&gt;人类策展仍然重要。&lt;/p&gt;
&lt;h3&gt;3. Wiki 会逐渐和真实情况脱节&lt;/h3&gt;
&lt;p&gt;这就是为什么需要 lint、owner 和更新时间。&lt;/p&gt;
&lt;p&gt;我会要求 accepted 页面必须有 &lt;code&gt;updated&lt;/code&gt;，超过一定时间就变成 stale。&lt;/p&gt;
&lt;h3&gt;4. 权限和隐私很麻烦&lt;/h3&gt;
&lt;p&gt;组织场景下尤其要注意：&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;li&gt;未公开产品计划&lt;/li&gt;
&lt;li&gt;API key / token&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;不要为了“上下文完整”牺牲边界。&lt;/p&gt;
&lt;h3&gt;5. 不要把 Wiki 当成唯一真相&lt;/h3&gt;
&lt;p&gt;Wiki 是编译层，不是原始事实本身。&lt;/p&gt;
&lt;p&gt;真正争议大的地方，还是要回到 raw sources。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;8. 我现在的判断&lt;/h2&gt;
&lt;p&gt;LLM Wiki 这个模式打动我的地方，不是它用了多复杂的技术。&lt;/p&gt;
&lt;p&gt;恰恰相反，它最核心的部分很朴素：Markdown、链接、index、log、schema、人工策展。&lt;/p&gt;
&lt;p&gt;但加上 LLM 之后，这套老东西突然变得更有生命力了。&lt;/p&gt;
&lt;p&gt;因为过去知识库最大的问题不是不会写，而是没人愿意长期维护。&lt;/p&gt;
&lt;p&gt;现在 LLM 可以承担那些重复、琐碎、但非常重要的维护工作：摘要、归档、链接、更新、检查、发现矛盾。&lt;/p&gt;
&lt;p&gt;人就可以回到更该做的事情上：选择资料、提出问题、判断结论。&lt;/p&gt;
&lt;p&gt;如果是个人使用，它像一个会帮你整理长期思考的第二大脑。&lt;/p&gt;
&lt;p&gt;如果是组织使用，它更像一个共享 context 层：让新人、老员工、Agent 都能读到同一套经过整理和审核的背景知识。&lt;/p&gt;
&lt;p&gt;我觉得这可能会是 Agent 时代很重要的一类基础设施。&lt;/p&gt;
&lt;p&gt;不是更大的 prompt，也不是更复杂的向量库。&lt;/p&gt;
&lt;p&gt;而是一套能持续生长、能被人审阅、也能被 Agent 读取的 Wiki。&lt;/p&gt;
&lt;p&gt;就这样，知识才不会每次都从零开始。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;参考资料：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://gist.github.com/karpathy/442a6bf555914893e9891c11519de94f&quot;&gt;Karpathy: LLM Wiki&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/nashsu/llm_wiki&quot;&gt;nashsu/llm_wiki&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/nashsu/llm_wiki/blob/main/README_CN.md&quot;&gt;LLM Wiki README_CN&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://dev.to/starmorph/how-to-build-karpathys-llm-wiki-the-complete-guide-to-ai-maintained-knowledge-bases-3dk3&quot;&gt;How to Build Karpathy&apos;s LLM Wiki&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;这篇文章是 agent 工程系统的关键一环——知识管理层。和《Agent 真正难的不是 Loop 而是工程系统》的上下文管理思想一脉相承，《Agent Skills 介绍与最佳实践》里的 Skills 正是消费这些知识的上层。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>Vite+ 会成为我的新前端默认工具链吗</title><link>https://bangwu.me/posts/vite-plus-frontend-toolchain/</link><guid isPermaLink="true">https://bangwu.me/posts/vite-plus-frontend-toolchain/</guid><description>从 Oxc lint/format、Vite 8、Vite+、Vitest，到单仓库、monorepo 和移动端测试，整理一套我现在更愿意采用的前端项目工具链。</description><pubDate>Mon, 27 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;TL;DR&lt;/p&gt;
&lt;p&gt;如果今天让我新开一个前端项目，我大概率会优先考虑这套组合：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Vite 8&lt;/strong&gt; 作为 dev/build 的基础&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Vite+&lt;/strong&gt; 作为统一入口，接管 create、dev、build、test、lint、format、run&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Oxlint + Oxfmt&lt;/strong&gt; 替代大部分 ESLint / Prettier 的日常反馈链路&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Vitest 4&lt;/strong&gt; 做单测、组件测试和 Browser Mode&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Playwright&lt;/strong&gt; 负责真正的端到端和移动端模拟&lt;/li&gt;
&lt;li&gt;monorepo 场景下，用 Vite+ 的 task runner 和缓存先顶上，而不是一上来就引入很重的工程编排&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我现在越来越不想在新项目里重新拼一遍工具链了。&lt;/p&gt;
&lt;p&gt;不是说 ESLint、Prettier、pnpm workspace、Turborepo、Vitest、Playwright 这些东西不好。它们都很好，也都很成熟。但问题是，每次新项目都要重新想一遍：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;lint 怎么配&lt;/li&gt;
&lt;li&gt;format 怎么配&lt;/li&gt;
&lt;li&gt;test 怎么拆&lt;/li&gt;
&lt;li&gt;monorepo 脚本怎么跑&lt;/li&gt;
&lt;li&gt;CI 缓存怎么写&lt;/li&gt;
&lt;li&gt;单仓库升级到 monorepo 要不要重构命令&lt;/li&gt;
&lt;li&gt;Node 版本和包管理器怎么约束&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这些事情单独看都不大，但加起来就是一种很稳定的消耗。&lt;/p&gt;
&lt;p&gt;所以 Vite+ 让我感兴趣的地方，不是它又多了一个 CLI，而是它想把这些原本分散的前端基础设施，收束到一个入口里。&lt;/p&gt;
&lt;p&gt;说白了，我想要的不是“更酷的脚手架”，而是一个新项目可以直接沿用的默认工作流。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;1. 为什么是现在：Vite 8 之后，前端工具链的底座变了&lt;/h2&gt;
&lt;p&gt;Vite 8 最大的变化，是把底层 bundler 收束到了 &lt;strong&gt;Rolldown&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;以前 Vite 的经典结构是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;开发时主要依赖 esbuild 的速度&lt;/li&gt;
&lt;li&gt;生产构建走 Rollup 的生态和稳定性&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这个设计很务实，也撑起了 Vite 很长时间。但它也带来一个长期存在的问题：开发和生产不是同一个 bundler，很多行为要靠适配和对齐。&lt;/p&gt;
&lt;p&gt;到了 Vite 8，Rolldown 成为统一的 Rust bundler。官方说构建相对 Rollup 有 10-30x 的提升，同时继续维持 Rollup 插件兼容。这件事的意义不只是“构建更快”，而是 Vite、Rolldown、Oxc 这几层开始真正靠近了。&lt;/p&gt;
&lt;p&gt;我觉得这里有一个很关键的趋势：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;前端工具链正在从“很多 JS 工具拼起来”，慢慢变成“一组底层能力协同工作”。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;过去我们习惯了这种组合：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Babel / SWC 负责转换&lt;/li&gt;
&lt;li&gt;ESLint 负责检查&lt;/li&gt;
&lt;li&gt;Prettier 负责格式化&lt;/li&gt;
&lt;li&gt;Rollup / webpack / esbuild 负责打包&lt;/li&gt;
&lt;li&gt;Vitest / Jest 负责测试&lt;/li&gt;
&lt;li&gt;任务编排再交给 npm scripts、Turborepo 或 Nx&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;它当然能跑，但每一层之间都有边界成本。配置重复、解析重复、文件扫描重复、缓存策略也重复。&lt;/p&gt;
&lt;p&gt;Vite 8 + Vite+ 的方向，是把这些东西尽量统一到一套更贴近底层的链路里。&lt;/p&gt;
&lt;p&gt;这也是我觉得它值得单独写一篇的原因。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;2. Vite+ 到底解决什么问题&lt;/h2&gt;
&lt;p&gt;Vite+ 不是简单的 &lt;code&gt;create-vite&lt;/code&gt; 加强版。&lt;/p&gt;
&lt;p&gt;VoidZero 对它的定位是 unified toolchain，也就是一个统一的 Web 开发入口。它把这些工具组合到一起：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Vite&lt;/li&gt;
&lt;li&gt;Vitest&lt;/li&gt;
&lt;li&gt;Oxlint&lt;/li&gt;
&lt;li&gt;Oxfmt&lt;/li&gt;
&lt;li&gt;Rolldown&lt;/li&gt;
&lt;li&gt;tsdown&lt;/li&gt;
&lt;li&gt;Vite Task&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;对应到命令上，大概是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;vp create   # 创建项目 / monorepo
vp install  # 自动使用正确包管理器安装依赖
vp dev      # 启动开发服务
vp check    # lint + format check + type check
vp test     # 运行 Vitest
vp build    # 生产构建
vp run      # 跑脚本和任务，支持 monorepo 依赖顺序与缓存
vp pack     # 打包库或独立应用
vp env      # 管理 Node 环境
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我比较喜欢的是它的目标很直接：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;把“项目该怎么跑”这件事从 package.json 脚本里抽出来，变成更稳定的工程约定。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;很多前端项目刚开始都很简单：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;scripts&quot;: {
    &quot;dev&quot;: &quot;vite&quot;,
    &quot;build&quot;: &quot;vite build&quot;,
    &quot;test&quot;: &quot;vitest&quot;,
    &quot;lint&quot;: &quot;eslint .&quot;,
    &quot;format&quot;: &quot;prettier --write .&quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但项目长大后，脚本会慢慢变成这样：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;scripts&quot;: {
    &quot;dev&quot;: &quot;xxx&quot;,
    &quot;dev:web&quot;: &quot;xxx&quot;,
    &quot;dev:admin&quot;: &quot;xxx&quot;,
    &quot;build&quot;: &quot;xxx&quot;,
    &quot;build:web&quot;: &quot;xxx&quot;,
    &quot;build:admin&quot;: &quot;xxx&quot;,
    &quot;lint&quot;: &quot;xxx&quot;,
    &quot;lint:fix&quot;: &quot;xxx&quot;,
    &quot;format&quot;: &quot;xxx&quot;,
    &quot;format:check&quot;: &quot;xxx&quot;,
    &quot;typecheck&quot;: &quot;xxx&quot;,
    &quot;test&quot;: &quot;xxx&quot;,
    &quot;test:unit&quot;: &quot;xxx&quot;,
    &quot;test:browser&quot;: &quot;xxx&quot;,
    &quot;test:e2e&quot;: &quot;xxx&quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后每个项目都不一样。&lt;/p&gt;
&lt;p&gt;新同事进来第一件事不是写业务，而是先问：这个仓库到底该跑哪个命令？CI 和本地是不是同一套？monorepo 里改了一个 package，要不要全量 build？&lt;/p&gt;
&lt;p&gt;Vite+ 想统一的就是这些日常动作。&lt;/p&gt;
&lt;p&gt;这件事并不性感，但很有价值。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;3. Oxlint + Oxfmt：先把反馈速度提上来&lt;/h2&gt;
&lt;p&gt;我一直觉得 lint 和 format 的价值不在“显得工程规范”，而在反馈速度。&lt;/p&gt;
&lt;p&gt;如果一个工具慢到大家不愿意本地跑，那它在团队里就会慢慢变成 CI 惩罚器。代码写完了，push 上去，CI 红了，然后再回来改。这个流程非常消耗人。&lt;/p&gt;
&lt;p&gt;Oxlint 的定位很明确：不用配置也能抓很多错误和无用代码，并且速度比 ESLint 快很多。Oxc 文档里提到，Oxlint 大概是 ESLint 的 50-100 倍速度，并且会随着 CPU 核心数扩展。&lt;/p&gt;
&lt;p&gt;Oxfmt 则是 Prettier-compatible formatter。它支持 JS、JSX、TS、TSX、JSON、YAML、HTML、Vue、CSS、Markdown、MDX 等常见格式。官方说 Oxfmt 大概比 Prettier 快 30 倍，并且 JavaScript / TypeScript 已经通过 Prettier 的 conformance tests。&lt;/p&gt;
&lt;p&gt;当然，这里我不会直接说“可以无脑替代 ESLint 和 Prettier”。&lt;/p&gt;
&lt;p&gt;更稳的策略应该是分阶段：&lt;/p&gt;
&lt;h3&gt;小到中型新项目&lt;/h3&gt;
&lt;p&gt;可以直接尝试：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;vp check
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;把 lint、format check、type check 都收进一个命令里。&lt;/p&gt;
&lt;p&gt;如果不用 Vite+，也可以单独这样配：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;scripts&quot;: {
    &quot;lint&quot;: &quot;oxlint&quot;,
    &quot;format&quot;: &quot;oxfmt&quot;,
    &quot;format:check&quot;: &quot;oxfmt --check&quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;已有大型项目&lt;/h3&gt;
&lt;p&gt;不要一把梭。&lt;/p&gt;
&lt;p&gt;我会先这样做：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;保留现有 ESLint&lt;/li&gt;
&lt;li&gt;先在本地和 CI 前置跑 Oxlint&lt;/li&gt;
&lt;li&gt;用 &lt;code&gt;eslint-plugin-oxlint&lt;/code&gt; 关闭 Oxlint 已经覆盖的 ESLint 规则&lt;/li&gt;
&lt;li&gt;对必须依赖生态插件的规则，继续交给 ESLint&lt;/li&gt;
&lt;li&gt;等差异足够小，再考虑进一步迁移&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这和 Oxc 官方建议也比较接近：大型项目可以先让 Oxlint 在 ESLint 前面跑，拿到更快的反馈回路。&lt;/p&gt;
&lt;p&gt;我的判断是：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Oxlint 更适合做第一层快速反馈，ESLint 暂时还适合兜底复杂生态规则。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;比如某些框架插件、公司内部规则、非常细的可访问性策略，短期内不一定都能丢给 Oxlint。&lt;/p&gt;
&lt;p&gt;但新项目就不一样了。新项目没有历史包袱，我更愿意直接让 Oxlint/Oxfmt 成为默认项。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;4. 单代码仓库：先简单，但不要把未来堵死&lt;/h2&gt;
&lt;p&gt;如果只是一个普通前端应用，我现在会尽量保持结构简单：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;my-app/
  src/
  tests/
  vite.config.ts
  vitest.config.ts
  package.json
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;默认命令大概是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;vp dev
vp check
vp test
vp build
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里关键不是命令少，而是命令语义稳定。&lt;/p&gt;
&lt;p&gt;我希望以后不管项目变成什么样，大家都知道：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;开发就是 &lt;code&gt;vp dev&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;提交前就是 &lt;code&gt;vp check&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;测试就是 &lt;code&gt;vp test&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;构建就是 &lt;code&gt;vp build&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这比在 package.json 里写十几个类似命令更容易维护。&lt;/p&gt;
&lt;p&gt;我会把配置控制在很少的范围内：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// vite.config.ts
import { defineConfig } from &apos;vite&apos;

export default defineConfig({
  // 先保持少配置
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;新项目最怕一开始就工程化过度。&lt;/p&gt;
&lt;p&gt;有些东西可以晚点再引入：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;多应用&lt;/li&gt;
&lt;li&gt;多 package&lt;/li&gt;
&lt;li&gt;复杂缓存&lt;/li&gt;
&lt;li&gt;代码生成&lt;/li&gt;
&lt;li&gt;e2e 矩阵&lt;/li&gt;
&lt;li&gt;内部组件库&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;但有些约定最好一开始就定好：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Node 版本&lt;/li&gt;
&lt;li&gt;包管理器&lt;/li&gt;
&lt;li&gt;lint / format / typecheck 入口&lt;/li&gt;
&lt;li&gt;test 文件命名&lt;/li&gt;
&lt;li&gt;CI 必跑命令&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这就是 Vite+ 这种统一入口的价值：它不会强迫你一开始就做成 monorepo，但它给后面扩展留下了路径。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;5. monorepo：不要为了 monorepo 而 monorepo&lt;/h2&gt;
&lt;p&gt;我对 monorepo 的态度一直比较保守。&lt;/p&gt;
&lt;p&gt;不是不能用，而是要先问：你真的需要吗？&lt;/p&gt;
&lt;p&gt;这些情况我觉得可以上：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一个产品里有多个前端应用，比如 web、admin、docs&lt;/li&gt;
&lt;li&gt;有共享组件库、工具库、类型包&lt;/li&gt;
&lt;li&gt;多个 package 需要一起改、一起测、一起发&lt;/li&gt;
&lt;li&gt;团队已经被跨仓库同步折磨过了&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果只是一个应用，硬拆成 monorepo，通常只是把简单问题复杂化。&lt;/p&gt;
&lt;p&gt;一个比较舒服的结构可能是这样：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;repo/
  apps/
    web/
    admin/
  packages/
    ui/
    config/
    shared/
  vite.config.ts
  package.json
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Vite+ 里比较值得关注的是 &lt;code&gt;vp run&lt;/code&gt; 和 Vite Task。&lt;/p&gt;
&lt;p&gt;它不像普通 &lt;code&gt;pnpm run&lt;/code&gt; 只是把命令跑起来，而是会考虑：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;workspace 感知&lt;/li&gt;
&lt;li&gt;任务依赖顺序&lt;/li&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;比如在 monorepo 里，根配置可以打开缓存：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// vite.config.ts
import { defineConfig } from &apos;vite&apos;

export default defineConfig({
  run: {
    cache: true,
  },
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后日常就可以：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;vp run build
vp run test
vp run lint
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当然，缓存不是魔法。&lt;/p&gt;
&lt;p&gt;我会特别注意这几件事：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;生成物不要被算进输入里，比如 &lt;code&gt;dist/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;临时文件不要干扰缓存，比如 &lt;code&gt;.vite-temp/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;任务依赖要写清楚，尤其是 &lt;code&gt;packages/ui&lt;/code&gt; 被 &lt;code&gt;apps/web&lt;/code&gt; 使用时&lt;/li&gt;
&lt;li&gt;影响构建的环境变量要纳入缓存 key&lt;/li&gt;
&lt;li&gt;CI 和本地的缓存策略不要完全割裂&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我比较喜欢的实践是：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;monorepo 先用 Vite+ 的任务能力跑起来，只有当它不够用时，再考虑引入更重的 Nx / Turborepo。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这不是说 Nx 或 Turborepo 不好，而是新项目不一定需要一开始就背这么多概念。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;6. Vitest：从单测到 Browser Mode&lt;/h2&gt;
&lt;p&gt;Vitest 现在已经不只是“Vite 生态里的 Jest 替代品”了。&lt;/p&gt;
&lt;p&gt;对我来说，它比较适合承担三层测试：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Node 环境单测&lt;/strong&gt;：工具函数、数据转换、业务逻辑&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;jsdom / happy-dom 组件测试&lt;/strong&gt;：轻量 DOM 行为&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Browser Mode&lt;/strong&gt;：需要真实浏览器能力的组件或交互测试&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Vitest 的 &lt;code&gt;projects&lt;/code&gt; 配置很适合把这些拆开。以前叫 workspace，现在官方更推荐 projects。&lt;/p&gt;
&lt;p&gt;比如可以这样：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// vitest.config.ts
import { defineConfig } from &apos;vitest/config&apos;
import { playwright } from &apos;@vitest/browser-playwright&apos;

export default defineConfig({
  test: {
    projects: [
      {
        test: {
          name: &apos;unit&apos;,
          environment: &apos;node&apos;,
          include: [&apos;tests/unit/**/*.test.ts&apos;, &apos;src/**/*.unit.test.ts&apos;],
        },
      },
      {
        test: {
          name: &apos;browser&apos;,
          include: [&apos;tests/browser/**/*.test.ts&apos;, &apos;src/**/*.browser.test.ts&apos;],
          browser: {
            enabled: true,
            provider: playwright(),
            instances: [{ browser: &apos;chromium&apos; }],
          },
        },
      },
    ],
  },
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样做的好处是边界很清楚：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;纯逻辑不要进浏览器，快一点&lt;/li&gt;
&lt;li&gt;需要真实 DOM / layout / browser API 的测试再进 Browser Mode&lt;/li&gt;
&lt;li&gt;不同测试环境不要互相污染&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我现在不太喜欢所有测试都塞进一个环境里。&lt;/p&gt;
&lt;p&gt;因为最后一定会变成这种局面：有的测试明明只需要 Node，却被迫跑在更重的环境里；有的测试明明依赖真实浏览器，却在模拟环境里靠 mock 撑着。&lt;/p&gt;
&lt;p&gt;拆开之后，心智负担反而更低。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;7. 移动端：不要只改 viewport&lt;/h2&gt;
&lt;p&gt;如果项目要认真考虑移动端，我不会只写几个响应式断点就结束。&lt;/p&gt;
&lt;p&gt;移动端至少要看这些东西：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;视口尺寸&lt;/li&gt;
&lt;li&gt;touch 行为&lt;/li&gt;
&lt;li&gt;user agent&lt;/li&gt;
&lt;li&gt;safe area&lt;/li&gt;
&lt;li&gt;横竖屏&lt;/li&gt;
&lt;li&gt;输入法弹起&lt;/li&gt;
&lt;li&gt;滚动容器&lt;/li&gt;
&lt;li&gt;hover 不存在时的交互&lt;/li&gt;
&lt;li&gt;弱网络和慢设备下的体验&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Vitest Browser Mode 可以覆盖一部分真实浏览器里的组件行为，但真正的移动端端到端，我还是会交给 Playwright。&lt;/p&gt;
&lt;p&gt;Playwright 的 device emulation 可以模拟很多设备参数，比如 userAgent、screenSize、viewport、hasTouch、isMobile、locale、timezone 等。&lt;/p&gt;
&lt;p&gt;例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// playwright.config.ts
import { defineConfig, devices } from &apos;@playwright/test&apos;

export default defineConfig({
  projects: [
    {
      name: &apos;Desktop Chrome&apos;,
      use: { ...devices[&apos;Desktop Chrome&apos;] },
    },
    {
      name: &apos;Mobile Safari&apos;,
      use: { ...devices[&apos;iPhone 15&apos;] },
    },
    {
      name: &apos;Pixel&apos;,
      use: { ...devices[&apos;Pixel 7&apos;] },
    },
  ],
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但这里也要清醒一点：模拟不等于真机。&lt;/p&gt;
&lt;p&gt;它可以很好地发现：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;响应式布局错位&lt;/li&gt;
&lt;li&gt;touch 点击区域太小&lt;/li&gt;
&lt;li&gt;移动端菜单打不开&lt;/li&gt;
&lt;li&gt;Safari/WebKit 下的兼容问题&lt;/li&gt;
&lt;li&gt;表单在小屏幕上不可用&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;但它不一定能完全覆盖：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;真机性能&lt;/li&gt;
&lt;li&gt;某些 WebView 行为&lt;/li&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;所以我的默认策略会是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;日常 CI：Playwright 移动端模拟&lt;/li&gt;
&lt;li&gt;关键版本：至少拿一台 iPhone 和一台 Android 真机过主流程&lt;/li&gt;
&lt;li&gt;PWA / H5 活动页：额外关注 WebView 和分享入口&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;移动端最怕的是桌面看起来没问题，真到手机上一堆细节炸掉。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;8. 我会怎么给新项目定默认脚本&lt;/h2&gt;
&lt;p&gt;如果用 Vite+，我会尽量减少自定义脚本，让团队形成固定动作。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;scripts&quot;: {
    &quot;dev&quot;: &quot;vp dev&quot;,
    &quot;check&quot;: &quot;vp check&quot;,
    &quot;test&quot;: &quot;vp test&quot;,
    &quot;build&quot;: &quot;vp build&quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果是 monorepo：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;scripts&quot;: {
    &quot;dev&quot;: &quot;vp dev&quot;,
    &quot;check&quot;: &quot;vp check&quot;,
    &quot;test&quot;: &quot;vp run test&quot;,
    &quot;build&quot;: &quot;vp run build&quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;CI 里大概保持这样：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;vp install
vp check
vp test
vp build
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果有 e2e：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pnpm playwright install --with-deps
pnpm playwright test
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当然，这里要看 Vite+ 后续和 Playwright 的集成情况。如果后面它能把 e2e 也更自然地收进 &lt;code&gt;vp test&lt;/code&gt;，那会更舒服。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;9. 风险：Vite+ 还在 Alpha，不要把它当成完全成熟的基建&lt;/h2&gt;
&lt;p&gt;这篇虽然是在推荐 Vite+，但我不想把它写成无脑吹。&lt;/p&gt;
&lt;p&gt;Vite+ 现在还处在 Alpha 阶段，这意味着：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;API 和配置可能变化&lt;/li&gt;
&lt;li&gt;某些边界场景还没被大量生产项目验证&lt;/li&gt;
&lt;li&gt;monorepo 缓存可能会遇到细节问题&lt;/li&gt;
&lt;li&gt;生态插件不一定全部适配到最佳状态&lt;/li&gt;
&lt;li&gt;团队成员需要接受新的命令入口&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以我的建议会分场景：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;场景&lt;/th&gt;
&lt;th&gt;我的建议&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;个人项目 / 新项目&lt;/td&gt;
&lt;td&gt;可以直接试 Vite+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;小团队新项目&lt;/td&gt;
&lt;td&gt;可以试，但保留回退方案&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;中大型已有项目&lt;/td&gt;
&lt;td&gt;先迁 Oxlint/Oxfmt 或 Vite 8，不急着全量 Vite+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;严肃生产 monorepo&lt;/td&gt;
&lt;td&gt;先开分支验证缓存、CI、构建差异&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;强依赖复杂 ESLint 插件&lt;/td&gt;
&lt;td&gt;暂时保留 ESLint 兜底&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;我的态度是：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;新项目可以激进一点，老项目要保守一点。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;工具链迁移最怕的是为了速度，把确定性弄丢了。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;10. 我现在的默认选择&lt;/h2&gt;
&lt;p&gt;如果是我来做一个新的前端项目，我大概会这样选：&lt;/p&gt;
&lt;h3&gt;普通 Web App&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Vite 8&lt;/li&gt;
&lt;li&gt;Vite+&lt;/li&gt;
&lt;li&gt;Oxlint + Oxfmt&lt;/li&gt;
&lt;li&gt;Vitest unit tests&lt;/li&gt;
&lt;li&gt;Playwright e2e&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;组件库 / 工具库&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Vite+ / tsdown&lt;/li&gt;
&lt;li&gt;Vitest&lt;/li&gt;
&lt;li&gt;Oxlint + Oxfmt&lt;/li&gt;
&lt;li&gt;examples 用 Vite 跑起来&lt;/li&gt;
&lt;li&gt;发布前跑 &lt;code&gt;vp pack&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;中后台 monorepo&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;apps/web&lt;/li&gt;
&lt;li&gt;apps/admin&lt;/li&gt;
&lt;li&gt;packages/ui&lt;/li&gt;
&lt;li&gt;packages/shared&lt;/li&gt;
&lt;li&gt;Vite+ task cache&lt;/li&gt;
&lt;li&gt;Vitest projects&lt;/li&gt;
&lt;li&gt;Playwright 跑核心流程&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;移动端优先 H5&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Vite+&lt;/li&gt;
&lt;li&gt;Vitest Browser Mode&lt;/li&gt;
&lt;li&gt;Playwright mobile projects&lt;/li&gt;
&lt;li&gt;真机主流程检查&lt;/li&gt;
&lt;li&gt;对 safe area、touch、输入法、滚动做额外 checklist&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;最后&lt;/h2&gt;
&lt;p&gt;我觉得 Vite+ 最有价值的地方，不是让某一个命令快一点。&lt;/p&gt;
&lt;p&gt;真正重要的是，它试图把前端项目里那些每天都会发生、但每个仓库都写得不一样的事情统一起来：开发、构建、测试、检查、格式化、任务运行、monorepo 缓存、运行时管理。&lt;/p&gt;
&lt;p&gt;这听起来没有那么酷，但对长期维护很重要。&lt;/p&gt;
&lt;p&gt;前端工具链这些年一直在变快。以前我们关心的是“这个工具比那个工具快多少”，现在我更关心的是另一件事：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;一个项目从第一天到变复杂之后，工具链还能不能保持同一套心智模型。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;如果 Vite+ 后面稳定下来，它很可能会成为我新前端项目的默认入口。&lt;/p&gt;
&lt;p&gt;至少现在，我愿意把它放进首选列表里。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;参考资料：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://vite.dev/blog/announcing-vite8&quot;&gt;Vite 8.0 is out&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://voidzero.dev/posts/announcing-vite-plus-alpha&quot;&gt;Announcing Vite+ Alpha&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://viteplus.dev/&quot;&gt;Vite+ Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://oxc-project.github.io/docs/guide/usage/linter.html&quot;&gt;Oxlint Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://oxc.rs/docs/guide/usage/formatter/&quot;&gt;Oxfmt Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://vitest.dev/guide/browser/&quot;&gt;Vitest Browser Mode&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://vitest.dev/guide/projects&quot;&gt;Vitest Test Projects&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://playwright.dev/docs/emulation&quot;&gt;Playwright Emulation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>这份写给工程师的 ML Primer，真正厉害的是它重新组织了技术教育</title><link>https://bangwu.me/posts/thereisnospoon-ml-primer-notes/</link><guid isPermaLink="true">https://bangwu.me/posts/thereisnospoon-ml-primer-notes/</guid><description>我把 thereisnospoon 从头读了一遍。它当然是在讲机器学习基础，但更有价值的，是它示范了一种 AI 时代技术内容的新写法：先搭心智模型，再把追问交给 agent。</description><pubDate>Sat, 04 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;结论先说。&lt;/p&gt;
&lt;p&gt;我把 &lt;code&gt;dreddnafious/thereisnospoon&lt;/code&gt; 这份 primer 从头读了一遍，最后的判断是：&lt;strong&gt;它当然是一份很好的机器学习入门材料，但它最值得研究的地方，其实不是“把 ML 基础讲明白了”，而是它示范了一种更适合 AI 时代的技术内容形态。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;说具体一点。&lt;/p&gt;
&lt;p&gt;它不是在写一本传统教材，也不是在做一篇“神经网络扫盲文”。它真正做的事情，是先给工程师搭一套可以长期复用的心智模型，再把那些本来应该发生在老师、同事、mentor 身上的追问，留给你和 AI agent 在阅读过程中继续展开。&lt;/p&gt;
&lt;p&gt;所以如果只把它当成“又一份 ML 入门资料”，我觉得是低估它了。&lt;/p&gt;
&lt;p&gt;这篇我不准备复述整本 primer，而是讲三件事：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;它到底写了什么，为什么值得认真读&lt;/li&gt;
&lt;li&gt;它有哪些地方确实讲得比常见 ML 入门更好&lt;/li&gt;
&lt;li&gt;它对我理解“博客、文档、agent 产品接下来会怎么变”有什么启发&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;::github{repo=&quot;dreddnafious/thereisnospoon&quot;}&lt;/p&gt;
&lt;h2&gt;先说背景：这不是一本教材，它是一份给工程师的心智模型手册&lt;/h2&gt;
&lt;p&gt;仓库首页的定位写得很直白：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A machine learning primer built from first principles&lt;/li&gt;
&lt;li&gt;For engineers who want to reason about ML systems the way they reason about software systems&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我读完整体感觉也是这样。&lt;/p&gt;
&lt;p&gt;它默认假设你不是零基础学生，而是一个已经会写系统、做工程取舍、理解复杂软件结构的人。你缺的不是学习能力，而是：&lt;strong&gt;你还没有把机器学习变成一种“能在脑子里画结构图”的东西。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这个判断非常准。&lt;/p&gt;
&lt;p&gt;很多工程师学 ML 卡住，并不是因为他们看不懂公式，而是因为他们始终没有形成下面这些直觉：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;神经元本质上到底在做什么判断&lt;/li&gt;
&lt;li&gt;深度和宽度到底分别换来了什么能力&lt;/li&gt;
&lt;li&gt;为什么 attention 和 convolution 是不同的“组合规则”&lt;/li&gt;
&lt;li&gt;FFN、residual、gating 这些东西，在系统层面分别扮演什么角色&lt;/li&gt;
&lt;li&gt;为什么有些架构天生适合某类问题，而另一些只是“也能用”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;传统教材当然也会讲这些，但经常是按学科逻辑来讲，不是按工程直觉来讲。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;thereisnospoon&lt;/code&gt; 的路线刚好反过来：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;它先给你一个工程上能拿来用的解释框架，再把数学放进这个框架里。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这也是我觉得它最聪明的地方。&lt;/p&gt;
&lt;h2&gt;这份 primer 具体是怎么搭起来的&lt;/h2&gt;
&lt;p&gt;它整份内容大概可以拆成三层。&lt;/p&gt;
&lt;h3&gt;第一层：先把最底层直觉讲稳&lt;/h3&gt;
&lt;p&gt;最前面的几章其实是在做地基，包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;neuron&lt;/li&gt;
&lt;li&gt;composition&lt;/li&gt;
&lt;li&gt;learning as optimization&lt;/li&gt;
&lt;li&gt;generalization&lt;/li&gt;
&lt;li&gt;representation&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这里面最好的一段，是它对 neuron 的解释。&lt;/p&gt;
&lt;p&gt;它没有一上来把神经元讲成“一个抽象数学单元”，而是先把它压缩成一句非常有用的话：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;一个 neuron，本质上就是一个 half-space detector。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;也就是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;权重向量定义了“它关心的方向”&lt;/li&gt;
&lt;li&gt;点积在问“输入在这个方向上有多对齐”&lt;/li&gt;
&lt;li&gt;bias 决定阈值&lt;/li&gt;
&lt;li&gt;activation 决定你怎么处理这个 side-and-distance 信息&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这个解释为什么好？&lt;/p&gt;
&lt;p&gt;因为它一下子把几个本来很容易分散记忆的东西串起来了：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;dot product 是 alignment&lt;/li&gt;
&lt;li&gt;decision boundary 是 hyperplane&lt;/li&gt;
&lt;li&gt;bias 是阈值平移&lt;/li&gt;
&lt;li&gt;nonlinearity 是表达能力来源&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;你后面再去看感知机、MLP、卷积核、attention 打分，本质上都还是在和“方向、投影、边界、组合”打交道。&lt;/p&gt;
&lt;p&gt;也就是说，它不是在教你背“神经元公式”，而是在给你一个后面反复能复用的底层几何视角。&lt;/p&gt;
&lt;h3&gt;第二层：把常见架构统一到一个总框架里&lt;/h3&gt;
&lt;p&gt;我觉得这份 primer 最有价值的一章，是 &lt;strong&gt;The Combination Rule Family&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;这章其实做了一个很强的抽象：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;不同架构的区别，核心不在于它们名字不同，而在于它们到底用什么规则去组合输入。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;它把常见架构都统一成了这个问题：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Which inputs should influence which outputs, and how much?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;然后分别解释：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Dense：所有输入都连所有输出，没有假设，最通用也最贵&lt;/li&gt;
&lt;li&gt;Convolution：局部连接 + 权重共享，赌 locality 和 translation invariance&lt;/li&gt;
&lt;li&gt;Recurrence：把历史压进状态里，适合流式和严格因果&lt;/li&gt;
&lt;li&gt;Attention：按内容动态路由，最灵活也最贵&lt;/li&gt;
&lt;li&gt;Graph ops：沿显式拓扑做消息传递&lt;/li&gt;
&lt;li&gt;SSM：用控制论和连续时间系统去处理长序列&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这个抽象非常值钱。&lt;/p&gt;
&lt;p&gt;因为很多 ML 入门材料最大的问题，是让新手觉得“架构是在记名词”。&lt;/p&gt;
&lt;p&gt;今天学 CNN，明天学 RNN，后天学 Transformer，最后脑子里是一堆松散知识点：&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;li&gt;那个已经过时了&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;但这份 primer 做的是另一件事：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;它把架构选型重新压缩成一个统一问题。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;一旦你接受“架构 = 组合规则 + 归纳偏置”这个视角，很多事会立刻清楚：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;为什么 CNN 不是“比 Transformer 落后”，而是更便宜地编码了局部先验&lt;/li&gt;
&lt;li&gt;为什么 attention 强，不是因为它神秘，而是因为它让连接关系变成输入相关&lt;/li&gt;
&lt;li&gt;为什么 graph model 能赢，不是因为图更高级，而是因为关系结构本来就已知&lt;/li&gt;
&lt;li&gt;为什么很多工程问题其实不是“模型不够强”，而是你选错了组合规则&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;对工程师来说，这种统一框架比背十个 landmark model 名字有用得多。&lt;/p&gt;
&lt;h2&gt;它最强的地方，不是类比本身，而是“类比承担主解释”&lt;/h2&gt;
&lt;p&gt;这份 primer 的另一个明显特点，是它用了很多工程和物理类比：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;neuron 像 polarizing filter&lt;/li&gt;
&lt;li&gt;depth 像 paper folding&lt;/li&gt;
&lt;li&gt;gradient flow 像 pipeline valves&lt;/li&gt;
&lt;li&gt;chain rule 像 gear train&lt;/li&gt;
&lt;li&gt;projection 像 shadow&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我一开始也会警惕这件事，因为技术内容一旦太爱用类比，很容易变成“说得好像懂了，实际并不精确”。&lt;/p&gt;
&lt;p&gt;但它这份材料有个地方处理得比较好：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;这些类比不是装饰性的，它们真的承担了主解释任务。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;比如它讲 depth，不是说“深度更强大”这种空话，而是用 paper folding 去解释：&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;这比直接说“深层网络能表达更复杂函数”更有手感。&lt;/p&gt;
&lt;p&gt;再比如它讲 activation，不是说“ReLU 可以缓解梯度消失”，而是把 activation 当成一个控制梯度流动的 valve：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;ReLU 是 gate valve&lt;/li&gt;
&lt;li&gt;sigmoid 是 pressure regulator&lt;/li&gt;
&lt;li&gt;GELU 是 proportional valve&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;你未必因此马上会推导公式，但你会开始真正理解：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;activation 不是一个孤零零的函数选项，而是在设计信号和梯度怎么穿过系统。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这就是好类比和坏类比的区别。&lt;/p&gt;
&lt;p&gt;坏类比只是让人觉得有趣。好类比会改变你之后观察系统的方式。&lt;/p&gt;
&lt;h2&gt;这份 primer 有一个很重的工程味：一直在问“该在什么时候用什么”&lt;/h2&gt;
&lt;p&gt;我读的时候最舒服的一点，是它一直没有沉迷在“知识百科化”里。&lt;/p&gt;
&lt;p&gt;它反复在回答的问题，其实是：&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;li&gt;它会在哪些条件下失效&lt;/li&gt;
&lt;li&gt;你真正该怎么选&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;比如在 &lt;code&gt;Topology for the Problem&lt;/code&gt; 那一章里，它基本是在给你做一张架构决策图：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;数据是不是 grid&lt;/li&gt;
&lt;li&gt;是不是严格序列&lt;/li&gt;
&lt;li&gt;有没有显式拓扑&lt;/li&gt;
&lt;li&gt;是不是超长上下文&lt;/li&gt;
&lt;li&gt;能不能容忍全局 attention 的成本&lt;/li&gt;
&lt;li&gt;数据量和预算分别处在什么量级&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这种写法我很喜欢。&lt;/p&gt;
&lt;p&gt;因为工程师真正需要的，往往不是把某个概念讲得最全，而是：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;让我形成“遇到问题时我该怎么判断”的路径。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;换句话说，技术内容最值钱的不是让你多知道一个名词，而是给你减少决策摩擦。&lt;/p&gt;
&lt;p&gt;这份 primer 在这点上做得很好。&lt;/p&gt;
&lt;h2&gt;它其实也在偷偷讲 Transformer 之后的统一语言：routing、memory、gating&lt;/h2&gt;
&lt;p&gt;如果把这份 primer 再往深一层看，我觉得它不仅在讲 ML 基础，也在提供一套现代 AI 系统的解释语言。&lt;/p&gt;
&lt;p&gt;尤其是后半部分的几章：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Transformer&lt;/li&gt;
&lt;li&gt;Encoding&lt;/li&gt;
&lt;li&gt;Learning Rules&lt;/li&gt;
&lt;li&gt;Gates as Control Systems&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这里面有一个很明显的趋势：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;它在把很多看似分散的机制，收敛到更少的几种系统级角色。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;比如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Attention 是 routing&lt;/li&gt;
&lt;li&gt;FFN 更像 per-token memory / volumetric lookup&lt;/li&gt;
&lt;li&gt;Residual 是 information-preserving highway&lt;/li&gt;
&lt;li&gt;Gates 是 control system&lt;/li&gt;
&lt;li&gt;Encoder 决定信息上限&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这套语言之所以重要，是因为它不只适用于“读论文”。&lt;/p&gt;
&lt;p&gt;它也适用于你现在理解 agent 系统、工具调用系统、RAG 系统，甚至多 agent 编排。&lt;/p&gt;
&lt;p&gt;比如我们今天再看 agent，很多问题其实也能被翻译成类似结构：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;什么信息被编码进上下文&lt;/li&gt;
&lt;li&gt;什么信息留在外部记忆里&lt;/li&gt;
&lt;li&gt;什么决策是 routing&lt;/li&gt;
&lt;li&gt;什么模块承担 execution&lt;/li&gt;
&lt;li&gt;什么地方应该 hard gate，什么地方应该 soft blend&lt;/li&gt;
&lt;li&gt;哪些链路应该 residual 保留，哪些应该 overwrite&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;也就是说，这份 primer 虽然表面是在教 ML，但它背后给出的，是一种更通用的“复杂 AI 系统怎么拆结构”的语言。&lt;/p&gt;
&lt;h2&gt;我最认同的一章，反而不是 Transformer，而是 Representation&lt;/h2&gt;
&lt;p&gt;如果要挑一章我读完最想推荐别人反复看的，不是 Transformer，而是 &lt;strong&gt;Representation: What Networks Actually Store&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;因为这一章在回答一个特别核心的问题：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;网络到底把知识存在哪。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;它的几个判断我都很认同：&lt;/p&gt;
&lt;h3&gt;1. feature 更像 direction，不像某个独立 neuron&lt;/h3&gt;
&lt;p&gt;这和很多人最早接触神经网络时的想象不一样。&lt;/p&gt;
&lt;p&gt;直觉上我们很容易觉得：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;这个 neuron 负责“猫耳朵”&lt;/li&gt;
&lt;li&gt;那个 neuron 负责“车轮”&lt;/li&gt;
&lt;li&gt;再另一个 neuron 负责“否定语气”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;但更真实的情况往往是：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;meaning 是分布在整个向量空间里的。单个节点只是坐标轴，不是语义单元。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这个视角会直接影响你怎么理解：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;interpretability&lt;/li&gt;
&lt;li&gt;model editing&lt;/li&gt;
&lt;li&gt;catastrophic forgetting&lt;/li&gt;
&lt;li&gt;transfer learning&lt;/li&gt;
&lt;li&gt;hallucination 的一部分来源&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2. superposition 是能力来源，也是副作用来源&lt;/h3&gt;
&lt;p&gt;primer 里把 superposition 讲得很清楚。&lt;/p&gt;
&lt;p&gt;网络不是一维萝卜一个坑地存 feature，而是把很多 feature 叠在共享子空间里，只要它们不经常冲突就行。&lt;/p&gt;
&lt;p&gt;这为什么重要？&lt;/p&gt;
&lt;p&gt;因为它解释了两件事为什么会同时成立：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;模型能在有限维度里装下远超维度数的可用特征&lt;/li&gt;
&lt;li&gt;模型又很难被做外科手术式精确修改&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这也是很多人一边觉得模型“很神奇”，一边又发现它“很难改得很干净”的根源之一。&lt;/p&gt;
&lt;h3&gt;3. knowledge 和 computation 往往缠在同一套权重里&lt;/h3&gt;
&lt;p&gt;这点我觉得是理解现代模型非常关键的一步。&lt;/p&gt;
&lt;p&gt;我们总爱把系统分成：&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;li&gt;推理&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;但在神经网络里，这些边界经常不是干净可拆的。&lt;/p&gt;
&lt;p&gt;一个权重矩阵既定义了什么方向存在，也定义了这些方向如何被后续层使用。&lt;/p&gt;
&lt;p&gt;这意味着很多你在软件工程里习惯的干净模块边界，在神经网络里本来就不存在。&lt;/p&gt;
&lt;p&gt;这也是为什么“像改配置一样改知识”通常很难。&lt;/p&gt;
&lt;h2&gt;当然，它也不是没有问题&lt;/h2&gt;
&lt;p&gt;我挺喜欢这份 primer，但也不觉得它完美。&lt;/p&gt;
&lt;p&gt;如果一定要挑问题，我觉得至少有三点。&lt;/p&gt;
&lt;h3&gt;1. 它对类比的依赖很重&lt;/h3&gt;
&lt;p&gt;虽然我前面说了，这份材料的类比多数是有效的，但风险还是在。&lt;/p&gt;
&lt;p&gt;类比一旦承担主解释，就意味着作者必须非常克制地控制边界。&lt;/p&gt;
&lt;p&gt;只要哪一步没收住，读者就容易把类比错当成本体。&lt;/p&gt;
&lt;p&gt;这份 primer 总体收得还可以，但我还是会建议读者：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;把类比当成脚手架，不要当成建筑本体。&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;2. 它默认读者已经有很强的工程抽象能力&lt;/h3&gt;
&lt;p&gt;这份材料虽然叫 primer，但它其实不是“无门槛”。&lt;/p&gt;
&lt;p&gt;如果你本来就不习惯从系统角色、归纳偏置、结构取舍这些层面思考问题，它读起来可能还是会有压强。&lt;/p&gt;
&lt;p&gt;所以它更像是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;面向工程师的 primer&lt;/li&gt;
&lt;li&gt;而不是面向所有人的 primer&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3. 它的叙述优势很强，容易让人高估“自己已经会了”&lt;/h3&gt;
&lt;p&gt;这类好文章的共性问题是：&lt;/p&gt;
&lt;p&gt;你在阅读过程中会不断产生“这个我懂了”的错觉。&lt;/p&gt;
&lt;p&gt;但真正检验懂没懂，不是你能不能复述作者的类比，而是你能不能在陌生问题上自己做选型、做诊断、做推理。&lt;/p&gt;
&lt;p&gt;这也是为什么我觉得它 README 里那句其实非常重要：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The primer is the map. The conversation is the territory.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这不是一句文案，它是这份材料真正成立的前提。&lt;/p&gt;
&lt;h2&gt;这份仓库真正新鲜的地方，是它默认文档应该和 agent 一起被消费&lt;/h2&gt;
&lt;p&gt;如果只看内容质量，这份 primer 已经算高质量了。&lt;/p&gt;
&lt;p&gt;但我觉得它更值得注意的地方，是它在 README 里非常自然地把“interactive exploration with an AI agent”写成了主用法之一。&lt;/p&gt;
&lt;p&gt;这背后其实是一个很大的变化。&lt;/p&gt;
&lt;p&gt;以前技术内容的默认形态是：&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;现在更合理的形态可能正在变成：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;文档负责提供高密度、结构化、可信的主框架&lt;/li&gt;
&lt;li&gt;agent 负责追问、纠错、换角度解释、做例子、做局部展开&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;也就是：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;文档不再需要独自承担“解释一切”的责任。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;它更像是一个高质量上下文包。&lt;/p&gt;
&lt;p&gt;agent 则成为这个上下文之上的解释器、陪练和互动界面。&lt;/p&gt;
&lt;p&gt;这一点我觉得非常关键。&lt;/p&gt;
&lt;p&gt;因为它直接改变了“好文档应该怎么写”这件事。&lt;/p&gt;
&lt;p&gt;在 agent 时代，很多技术文档不一定要越来越厚，反而可能应该：&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;li&gt;关键术语更统一&lt;/li&gt;
&lt;li&gt;更适合被引用、被切片、被追问&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;换句话说，未来最好的技术内容，未必是“一个人静态看完就全懂”的那种百科全书，反而可能是：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;一份适合和 agent 共同阅读的高质量主文本。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;而 &lt;code&gt;thereisnospoon&lt;/code&gt; 已经有点这个味道了。&lt;/p&gt;
&lt;h2&gt;它和普通“AI 帮我写教程”最大的区别是什么&lt;/h2&gt;
&lt;p&gt;我觉得最大的区别是：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;它不是把 AI 当生成器，而是把 AI 当讨论介质。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;README 里明确说，这份 primer 本身就是在长对话里一层层打磨出来的，很多类比和解释都是被问题反复 stress test 过的。&lt;/p&gt;
&lt;p&gt;这就很有意思。&lt;/p&gt;
&lt;p&gt;因为它不是“先有人写完文档，再额外接一个 chatbot”。&lt;/p&gt;
&lt;p&gt;更像是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;文档的生产过程本来就是人与 agent 的共同推敲&lt;/li&gt;
&lt;li&gt;文档的消费过程又继续通过 agent 被展开&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;生产和消费两端都被 agent 介入了。&lt;/p&gt;
&lt;p&gt;如果这个方向成立，那未来很多博客、文档、教程都会变：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;写作时先把材料和 agent 一起打磨到结构稳定&lt;/li&gt;
&lt;li&gt;发布时给读者一个适合追问的主文本&lt;/li&gt;
&lt;li&gt;阅读时不再只靠作者一次性讲完，而是靠 agent 按读者情况分层解释&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这可能比“给文章底部接个 AI 问答框”更深。&lt;/p&gt;
&lt;p&gt;它意味着内容产品本身会开始朝“可交互知识对象”去设计。&lt;/p&gt;
&lt;h2&gt;如果你是工程师，这份 primer 最适合怎么读&lt;/h2&gt;
&lt;p&gt;我不建议把它当成一本一定要一口气啃完的教材。&lt;/p&gt;
&lt;p&gt;我更建议这样读：&lt;/p&gt;
&lt;h3&gt;第一遍：只抓主框架&lt;/h3&gt;
&lt;p&gt;先把这些章节读通：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The Neuron&lt;/li&gt;
&lt;li&gt;Composition&lt;/li&gt;
&lt;li&gt;Learning as Optimization&lt;/li&gt;
&lt;li&gt;The Combination Rule Family&lt;/li&gt;
&lt;li&gt;The Transformer&lt;/li&gt;
&lt;li&gt;Representation&lt;/li&gt;
&lt;li&gt;Topology for the Problem&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;第一遍目标不是记细节，而是建立一张总图。&lt;/p&gt;
&lt;h3&gt;第二遍：带问题回读&lt;/h3&gt;
&lt;p&gt;比如你现在正卡在这些问题里：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;为什么 attention 能替代很多 RNN&lt;/li&gt;
&lt;li&gt;FFN 在 transformer 里到底干嘛&lt;/li&gt;
&lt;li&gt;为什么模型容易遗忘旧知识&lt;/li&gt;
&lt;li&gt;为什么长上下文处理这么贵&lt;/li&gt;
&lt;li&gt;为什么某些问题上 CNN 还是更划算&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;那就只回读对应章节。&lt;/p&gt;
&lt;h3&gt;第三遍：配合 agent 追问&lt;/h3&gt;
&lt;p&gt;这是我觉得它最该被使用的方式。&lt;/p&gt;
&lt;p&gt;比如可以直接拿着里面某一节去追问：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;paper folding 这个类比在哪些地方会失真&lt;/li&gt;
&lt;li&gt;如果把 FFN 理解成 memory，有哪些边界条件&lt;/li&gt;
&lt;li&gt;为什么说 attention 本质上是 routing 而不是 reasoning&lt;/li&gt;
&lt;li&gt;superposition 和 hallucination 的关系到底能说到什么程度&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这样读，收益会比纯顺读大得多。&lt;/p&gt;
&lt;h2&gt;我最后的判断&lt;/h2&gt;
&lt;p&gt;如果只用一句话概括我对 &lt;code&gt;thereisnospoon&lt;/code&gt; 的评价：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;它表面上是在教机器学习，实际上更重要的是，它给出了一个 AI 时代技术内容该怎么写、怎么读、怎么和 agent 协作的样板。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;它当然也有局限：&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;但即便如此，我还是觉得它值得认真读。&lt;/p&gt;
&lt;p&gt;不是因为它能替代教材，而是因为它补上了教材经常缺的那一层：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;把机器学习从“知识点集合”重新变回“可以拿来思考系统设计的直觉工具”。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这也是我现在越来越看重的一类技术内容。&lt;/p&gt;
&lt;p&gt;不是资料更全，而是脑内支架更稳。&lt;/p&gt;
&lt;h2&gt;我的建议&lt;/h2&gt;
&lt;p&gt;如果你本来就是工程师，最近又刚好开始更认真地接触 ML、agent、模型架构这些东西，我建议你别把这个仓库当成“收藏夹里又一个待看链接”。&lt;/p&gt;
&lt;p&gt;更好的用法是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;抽一小时把前半部分读掉，先建立总图&lt;/li&gt;
&lt;li&gt;遇到卡住的章节，不急着跳出查百科，先对着 agent 追问&lt;/li&gt;
&lt;li&gt;重点看它怎么统一解释架构选型、表示学习、gating 和 routing&lt;/li&gt;
&lt;li&gt;不要只学里面的知识，也顺手学它这种写法本身&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;真正关键的不是它有没有把每个细节都讲到最全，而是它已经证明了一件事：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;高质量技术内容，完全可以不靠堆术语和堆公式建立权威，而是靠更强的心智模型和更好的交互预期。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这条路，我觉得后面会越来越重要。&lt;/p&gt;
</content:encoded></item><item><title>GPT-5.4 做前端 为什么终于没那么模板味了</title><link>https://bangwu.me/posts/gpt-5-4-frontend-design-notes/</link><guid isPermaLink="true">https://bangwu.me/posts/gpt-5-4-frontend-design-notes/</guid><description>OpenAI 新文里最值得拿来落地的，不是一句“GPT-5.4 更强了”，而是怎么给模型前端约束：构图、品牌、视觉参考、设计系统、Playwright 验证。</description><pubDate>Tue, 24 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;结论先说。&lt;/p&gt;
&lt;p&gt;OpenAI 这篇《Designing delightful frontends with GPT-5.4》里，我觉得最有价值的不是“GPT-5.4 更会做前端了”这句宣传话，而是它终于把一个很多人已经隐约感觉到的事实讲明白了：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;AI 做前端时，真正决定结果的，往往不是模型会不会写代码，而是你有没有把视觉约束、页面叙事、设计系统和验证链路交代清楚。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;换句话说，前端质量不是一句“帮我写个 landing page”换来的。模型当然比以前强了，但如果输入还是模糊的，它依然会滑回那种最常见、最安全、也最没记忆点的网页模板。&lt;/p&gt;
&lt;p&gt;这篇我不准备逐段翻译原文，而是只提炼我觉得最值得带回项目里直接用的部分。&lt;/p&gt;
&lt;h2&gt;1. GPT-5.4 前端变强，强的不只是代码生成&lt;/h2&gt;
&lt;p&gt;OpenAI 在文里把前端相关提升总结成三类：&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;我觉得这个拆法挺准确。&lt;/p&gt;
&lt;p&gt;因为“会写 React 代码”和“能产出好页面”根本不是一回事。&lt;/p&gt;
&lt;p&gt;前端好不好，很多时候取决于这些模型以外的能力：&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;如果只盯着代码补全看，容易低估这类能力的价值。&lt;/p&gt;
&lt;h2&gt;2. prompt 一旦模糊，模型就会回退到“高频网页模板”&lt;/h2&gt;
&lt;p&gt;这篇里有个判断我很认同：&lt;/p&gt;
&lt;p&gt;当 prompt 不够具体时，模型通常会回退到训练数据里高频出现的模式。&lt;/p&gt;
&lt;p&gt;这会导致什么？&lt;/p&gt;
&lt;p&gt;结果通常是：&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;li&gt;但就是很像“某种熟悉的 AI 生成页面”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;常见症状基本都一样：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;hero 没有真正的视觉中心&lt;/li&gt;
&lt;li&gt;品牌感弱&lt;/li&gt;
&lt;li&gt;第一屏塞很多碎信息&lt;/li&gt;
&lt;li&gt;卡片、badge、stats、icon 行一股脑堆上去&lt;/li&gt;
&lt;li&gt;看起来很努力，但没重点&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;说白了，这不是代码 bug，而是&lt;strong&gt;默认审美策略&lt;/strong&gt;在接管输出。&lt;/p&gt;
&lt;p&gt;所以我现在越来越觉得，做 AI 前端时最先该补的，不是更多组件，而是更强的约束。&lt;/p&gt;
&lt;h2&gt;3. 第一屏最重要的是“构图”，不是“信息量”&lt;/h2&gt;
&lt;p&gt;OpenAI 这篇给了一套相当硬的前端规则，我觉得其中最值钱的是关于第一屏的几条：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一屏应该读起来像&lt;strong&gt;一个完整构图&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;品牌应该是 hero 级信号，不能只躲在导航栏里&lt;/li&gt;
&lt;li&gt;landing page 默认应该是&lt;strong&gt;全幅视觉主导&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;第一屏内容预算应该克制&lt;/li&gt;
&lt;li&gt;不要在 hero 上叠一堆 detached label、浮层 badge、信息贴片&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这几条其实都在指向同一个问题：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;很多 AI 生成页面最差的地方，不是不会排版，而是不会取舍。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;它很容易把“页面应该有的元素”全都放出来，但不会先判断：&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;li&gt;哪些信息应该推迟到第二屏&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以这篇里那句 “One composition” 我觉得特别关键。&lt;/p&gt;
&lt;p&gt;对于大多数宣传页、活动页、产品页来说，第一屏不是一个信息仓库，而应该是一个被明确设计过的画面。&lt;/p&gt;
&lt;h2&gt;4. “默认不要卡片”是个很狠但很有用的判断&lt;/h2&gt;
&lt;p&gt;文章里有一条规则特别狠：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Cards: Default: no cards.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;第一眼看有点极端，但我仔细想了一下，挺有道理。&lt;/p&gt;
&lt;p&gt;因为现在很多 AI 生成前端的坏味道，本质上就是：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;太喜欢把所有东西都装进卡片里。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;然后页面上就会出现这些东西：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;hero 里一堆 stats card&lt;/li&gt;
&lt;li&gt;栏目里一排 feature card&lt;/li&gt;
&lt;li&gt;CTA 前再来一层 card&lt;/li&gt;
&lt;li&gt;连一个简单说明块都要 shadow + radius + border&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;最后不是更高级，而是更拥挤。&lt;/p&gt;
&lt;p&gt;我觉得文里那个判断标准特别实用：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;如果去掉 border、shadow、background、radius，不影响理解和交互，那它就不该是卡片。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这个标准其实很适合拿来当日常检查项。因为它逼你回到一个更基础的问题：&lt;/p&gt;
&lt;p&gt;这个视觉容器，到底是在解决交互问题，还是只是在假装设计感？&lt;/p&gt;
&lt;h2&gt;5. 对很多前端任务，低 reasoning 可能更好&lt;/h2&gt;
&lt;p&gt;这个点有点反直觉，但我很认可。&lt;/p&gt;
&lt;p&gt;文里建议做相对普通的前端页面时，先从 &lt;strong&gt;low reasoning&lt;/strong&gt; 开始，而不是默认开很高。&lt;/p&gt;
&lt;p&gt;原因也很简单：&lt;/p&gt;
&lt;p&gt;更高 reasoning 不一定带来更好页面，反而可能带来：&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;li&gt;整体更像工程方案，不像一个被打磨过的视觉产物&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这点其实和写作有点像。&lt;/p&gt;
&lt;p&gt;不是想得越多越好，而是先把约束和方向定准，再让模型快速收敛。前端尤其如此，因为页面观感不是靠推理深度线性换来的。&lt;/p&gt;
&lt;p&gt;我自己的理解是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;复杂业务逻辑&lt;/strong&gt; 值得更高 reasoning&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;页面构图和视觉落地&lt;/strong&gt; 更需要明确边界和快速迭代&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;6. 视觉参考不是装饰，是前端 prompt 的核心输入&lt;/h2&gt;
&lt;p&gt;这篇一直在强调一件事：&lt;/p&gt;
&lt;p&gt;如果想让 GPT-5.4 做出更强的前端，最好给它：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;截图&lt;/li&gt;
&lt;li&gt;mood board&lt;/li&gt;
&lt;li&gt;现有设计参考&lt;/li&gt;
&lt;li&gt;你明确描述过的风格、色盘、构图、情绪&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这很重要，因为视觉设计本来就有大量难以用纯文本精确表达的部分。&lt;/p&gt;
&lt;p&gt;你可以说：&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;li&gt;有氛围&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;但这些词本身都很滑。&lt;/p&gt;
&lt;p&gt;一旦没有视觉锚点，模型还是会回到自己最熟悉的网页语法里。&lt;/p&gt;
&lt;p&gt;所以参考图的作用，不是“让模型抄”，而是帮它确定：&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;li&gt;颜色氛围&lt;/li&gt;
&lt;li&gt;模块切换的节奏&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这其实和人类设计师工作很像。很多时候不是不会设计，而是没有统一视觉方向。&lt;/p&gt;
&lt;h2&gt;7. 好的前端 prompt，应该先交代“页面叙事”&lt;/h2&gt;
&lt;p&gt;这篇还有一点我挺认同：&lt;/p&gt;
&lt;p&gt;页面不只是模块拼装，它最好有自己的叙事结构。&lt;/p&gt;
&lt;p&gt;文里给了一个典型营销页顺序：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Hero：先建立身份和承诺&lt;/li&gt;
&lt;li&gt;Supporting imagery：补场景或氛围&lt;/li&gt;
&lt;li&gt;Product detail：解释具体内容&lt;/li&gt;
&lt;li&gt;Social proof：建立信任&lt;/li&gt;
&lt;li&gt;Final CTA：做最后转化&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这个框架不新，但很实用。&lt;/p&gt;
&lt;p&gt;因为很多 AI 页面的问题，根本不在 CSS，而在于它不知道页面“下一段为什么要存在”。&lt;/p&gt;
&lt;p&gt;结果就是 section 虽然很多，但都是平的。&lt;/p&gt;
&lt;p&gt;我觉得这篇最重要的提醒之一，就是：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;别只让模型生成 section，要先让它理解 section 的职责。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;也就是文里说的：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;one job per section&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;一段只做一件事，页面气质会立刻稳很多。&lt;/p&gt;
&lt;h2&gt;8. 设计系统要先定，不要边写边飘&lt;/h2&gt;
&lt;p&gt;OpenAI 还建议模型一开始就先建立设计系统，比如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;background / surface / text / muted / accent 这些 token&lt;/li&gt;
&lt;li&gt;display / headline / body / caption 这些字级角色&lt;/li&gt;
&lt;li&gt;统一的颜色和布局原则&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这个我也觉得很值。&lt;/p&gt;
&lt;p&gt;因为 AI 一边写页面一边临场发挥时，最容易出现的问题就是：&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;li&gt;字级体系漂移&lt;/li&gt;
&lt;li&gt;明暗关系不稳定&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;看起来都不算大错，但累积起来就会让页面很散。&lt;/p&gt;
&lt;p&gt;所以与其让模型后面修视觉一致性，不如一开始就把 token 定住。&lt;/p&gt;
&lt;p&gt;这也是为什么 React + Tailwind 在这类任务里效果通常不错：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;迭代快&lt;/li&gt;
&lt;li&gt;容易约束&lt;/li&gt;
&lt;li&gt;设计 token 好落地&lt;/li&gt;
&lt;li&gt;模型也更熟悉这套表达方式&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;9. Playwright 之类的验证工具，对前端太重要了&lt;/h2&gt;
&lt;p&gt;这篇最让我点头的一段，是它明确把 Playwright 拉进前端工作流里。&lt;/p&gt;
&lt;p&gt;因为前端最怕一种错觉：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;代码看起来对，但页面其实不对。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;比如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;移动端溢出&lt;/li&gt;
&lt;li&gt;固定层遮住按钮&lt;/li&gt;
&lt;li&gt;CTA 在某个断点被挤掉&lt;/li&gt;
&lt;li&gt;交互流程断在第二步&lt;/li&gt;
&lt;li&gt;视觉和参考图差得很远，但代码本身没报错&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这些问题只看代码很难发现。&lt;/p&gt;
&lt;p&gt;而有了 Playwright 之类工具，模型就能：&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;li&gt;看渲染结果&lt;/li&gt;
&lt;li&gt;检查状态变化&lt;/li&gt;
&lt;li&gt;反过来继续修&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这其实是我觉得 GPT-5.4 真正实用的一点：&lt;/p&gt;
&lt;p&gt;它不只是“更会写”，而是开始更像一个会自己回头看产物的前端同事。&lt;/p&gt;
&lt;h2&gt;10. 这篇对我自己的最大提醒&lt;/h2&gt;
&lt;p&gt;如果让我把这篇压成一句自己的话，我会这么说：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;让 AI 做出不那么模板味的前端，重点不是催它更有创意，而是你要先把创意边界、视觉方向和验证方式搭好。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这句话背后的落地动作其实很具体：&lt;/p&gt;
&lt;h3&gt;做前端任务时，先给约束&lt;/h3&gt;
&lt;p&gt;比如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;只允许一个 H1&lt;/li&gt;
&lt;li&gt;不超过 6 个 section&lt;/li&gt;
&lt;li&gt;第一屏只有品牌、标题、短描述、CTA、主视觉&lt;/li&gt;
&lt;li&gt;不要 stats strip、badge cluster、hero 卡片&lt;/li&gt;
&lt;li&gt;默认全幅 hero&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;给视觉锚点&lt;/h3&gt;
&lt;p&gt;比如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;截图&lt;/li&gt;
&lt;li&gt;mood board&lt;/li&gt;
&lt;li&gt;一句清晰的视觉 thesis&lt;/li&gt;
&lt;li&gt;颜色和气质描述&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;给页面叙事&lt;/h3&gt;
&lt;p&gt;比如：&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;li&gt;最后再 CTA&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;给验证链路&lt;/h3&gt;
&lt;p&gt;比如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Playwright 看 desktop / mobile&lt;/li&gt;
&lt;li&gt;检查按钮是否可点&lt;/li&gt;
&lt;li&gt;检查 fixed 元素是否遮挡&lt;/li&gt;
&lt;li&gt;检查页面是不是和参考气质一致&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这套东西看起来像“附加要求”，但其实才是让前端变好的主体工作。&lt;/p&gt;
&lt;h2&gt;最后总结&lt;/h2&gt;
&lt;p&gt;我看完这篇之后，更明确地觉得：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;AI 前端的上限，不只是模型能力问题，更是 art direction 问题。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;GPT-5.4 当然更强了，但真正重要的是，它把一件事暴露得更明显：&lt;/p&gt;
&lt;p&gt;如果你不给模型明确的视觉判断标准，它就会默认走最常见的网页套路；
如果你给了足够清楚的约束、叙事和验证方式，它就更有机会产出真正像样的页面。&lt;/p&gt;
&lt;p&gt;所以以后让 AI 做前端，我觉得最该改的习惯不是“多要几个版本”，而是先把这几样东西说清楚：&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;li&gt;整体气质往哪边靠&lt;/li&gt;
&lt;li&gt;怎么验证它真的做对了&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;前端最终比拼的，很多时候不是“有没有做出来”，而是“有没有做得不那么像模板”。&lt;/p&gt;
&lt;p&gt;而这篇里最值得拿走的，刚好就是如何把这件事交代给模型。&lt;/p&gt;
&lt;h2&gt;参考&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;OpenAI, &lt;em&gt;Designing delightful frontends with GPT-5.4&lt;/em&gt;：&lt;a href=&quot;https://developers.openai.com/blog/designing-delightful-frontends-with-gpt-5-4&quot;&gt;https://developers.openai.com/blog/designing-delightful-frontends-with-gpt-5-4&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Agent 真正难的不是 Loop 而是工程系统</title><link>https://bangwu.me/posts/agent-engineering-notes-from-tw93/</link><guid isPermaLink="true">https://bangwu.me/posts/agent-engineering-notes-from-tw93/</guid><description>基于 Tw93 那篇 Agent 长文，我提炼了 7 个更值得拿来落地的判断：Loop 很小，Harness 更重要，上下文要分层，Skill 要像路由，工具要面向目标。</description><pubDate>Mon, 23 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;结论先说。&lt;/p&gt;
&lt;p&gt;如果你最近在做 Agent，我觉得最容易踩的坑，不是模型不够强，而是太早把注意力放在模型本身，反而忽略了更影响结果的那几层工程系统：验证、上下文、工具、记忆和边界控制。&lt;/p&gt;
&lt;p&gt;我刚看完 Tw93 那篇《你不知道的 Agent：原理、架构与工程实践》，里面有不少内容我挺认同。但我不想再把原文复述一遍，这篇我只提炼我觉得&lt;strong&gt;最值得带回项目里直接用&lt;/strong&gt;的部分。&lt;/p&gt;
&lt;p&gt;如果只记一句话，就是：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Agent Loop 很小，真正决定系统能不能跑稳的，是 Loop 外面的工程设计。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;1. Agent Loop 其实没那么神秘&lt;/h2&gt;
&lt;p&gt;文章里给了一个很典型的 Agent Loop：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const messages: MessageParam[] = [{ role: &quot;user&quot;, content: userInput }];

while (true) {
  const response = await client.messages.create({
    model: &quot;claude-opus-4-6&quot;,
    max_tokens: 8096,
    tools: toolDefinitions,
    messages,
  });

  if (response.stop_reason === &quot;tool_use&quot;) {
    const toolResults = await Promise.all(
      response.content
        .filter((b) =&amp;gt; b.type === &quot;tool_use&quot;)
        .map(async (b) =&amp;gt; ({
          type: &quot;tool_result&quot; as const,
          tool_use_id: b.id,
          content: await executeTool(b.name, b.input),
        }))
    );
    messages.push({ role: &quot;assistant&quot;, content: response.content });
    messages.push({ role: &quot;user&quot;, content: toolResults });
  } else {
    return response.content.find((b) =&amp;gt; b.type === &quot;text&quot;)?.text ?? &quot;&quot;;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这段代码本身不复杂，抽象下来就是四步：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;感知输入&lt;/li&gt;
&lt;li&gt;决策下一步&lt;/li&gt;
&lt;li&gt;调工具执行&lt;/li&gt;
&lt;li&gt;把结果喂回去继续循环&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;所以我现在越来越认同一个判断：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;不要把 Agent 的神秘感，误当成架构复杂度。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;很多时候，Loop 本体是最稳定、最不该频繁改动的部分。真正会持续变化的，是这些东西：&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;li&gt;出错后怎么验证和回滚&lt;/li&gt;
&lt;li&gt;多 Agent 怎么分工&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;也就是说，Loop 更像 CPU，系统表现更多取决于外围板卡和总线，而不是 CPU 那十几行控制逻辑。&lt;/p&gt;
&lt;h2&gt;2. 别把 Workflow 和 Agent 混着叫&lt;/h2&gt;
&lt;p&gt;Tw93 提到一个很关键的区分：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Workflow&lt;/strong&gt;：执行路径由代码预先写死&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Agent&lt;/strong&gt;：下一步由 LLM 动态决定&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这件事看起来像术语洁癖，其实不是。&lt;/p&gt;
&lt;p&gt;因为一旦你分不清，就很容易出现两个常见问题：&lt;/p&gt;
&lt;h3&gt;第一种：明明是 Workflow，硬要包装成 Agent&lt;/h3&gt;
&lt;p&gt;比如：&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;这套流程也许很好用，但它本质上不是 Agent，而是带模型节点的 workflow。&lt;/p&gt;
&lt;p&gt;这没问题，问题在于很多团队会因此误判：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;以为自己已经在做 Agent 平台&lt;/li&gt;
&lt;li&gt;以为下一步性能问题该靠更强模型解决&lt;/li&gt;
&lt;li&gt;以为系统不灵活是 prompt 写得不够长&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;实际上，这类系统的问题通常应该通过&lt;strong&gt;流程设计&lt;/strong&gt;来解，不该扔给模型自由发挥。&lt;/p&gt;
&lt;h3&gt;第二种：本该 Workflow 的任务，过早放权给 Agent&lt;/h3&gt;
&lt;p&gt;有些任务根本不需要自主性，比如：&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;li&gt;有明确步骤的部署流程&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果你还是让 Agent 自己决定下一步，通常只会引入额外波动。&lt;/p&gt;
&lt;p&gt;所以我现在的建议很简单：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;先判断任务需要不需要“动态决策”，再决定要不要上 Agent。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;不是所有问题都值得换来自主性。&lt;/p&gt;
&lt;h2&gt;3. 真正影响上线效果的，往往是 Harness&lt;/h2&gt;
&lt;p&gt;这篇里我最认同的一段，其实不是 Loop，而是它对 &lt;strong&gt;Harness&lt;/strong&gt; 的强调。&lt;/p&gt;
&lt;p&gt;简单说，Harness 就是围绕 Agent 搭起来的那层工程护栏。至少包括：&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;li&gt;回退手段&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这东西为什么重要？&lt;/p&gt;
&lt;p&gt;因为模型只负责“猜下一步”，它不天然知道：&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;li&gt;继续失败什么时候停止&lt;/li&gt;
&lt;li&gt;改坏了怎么回滚&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果这些都没有外部系统兜住，Agent 就算模型再强，也只是一个高成本随机游走器。&lt;/p&gt;
&lt;p&gt;我觉得这也是现在很多 Agent 项目跑不稳的核心原因：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;问题不在于模型能力不足，而在于系统没有给它一个机器可执行的“对错判断”。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这在代码场景里尤其明显。&lt;/p&gt;
&lt;p&gt;如果你的任务可以自动验证，比如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;单测&lt;/li&gt;
&lt;li&gt;E2E&lt;/li&gt;
&lt;li&gt;lint / typecheck&lt;/li&gt;
&lt;li&gt;构建结果&lt;/li&gt;
&lt;li&gt;日志/指标/trace 校验&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;那 Agent 的可用性会直接上一个台阶。&lt;/p&gt;
&lt;p&gt;反过来，如果结果只能靠人盯着判断“看起来差不多”，吞吐量上限就永远在人身上。&lt;/p&gt;
&lt;p&gt;所以我现在看 Agent 项目，第一反应不是“你用什么模型”，而是先问：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;这件事能不能自动验收？&lt;/li&gt;
&lt;li&gt;Agent 改完后系统能不能自己知道它改对了？&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果答案是否，那后面很多优化都只是局部修补。&lt;/p&gt;
&lt;h2&gt;4. Context Rot 比上下文长度更可怕&lt;/h2&gt;
&lt;p&gt;长上下文不是越长越好，这件事大家都知道。&lt;/p&gt;
&lt;p&gt;但很多人还没真的把它当工程问题处理。&lt;/p&gt;
&lt;p&gt;文章里提到一个词我很认同：&lt;strong&gt;Context Rot&lt;/strong&gt;。意思是上下文不是不够长，而是越来越烂。&lt;/p&gt;
&lt;p&gt;典型症状是：&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;这时候你会误以为模型不聪明，实际上可能只是喂得太乱。&lt;/p&gt;
&lt;p&gt;我现在比较认可的做法，就是把上下文按稳定性和使用频率拆层：&lt;/p&gt;
&lt;h3&gt;常驻层&lt;/h3&gt;
&lt;p&gt;只放这些：&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;li&gt;必须长期成立的约束&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;它应该短、硬、稳定。&lt;/p&gt;
&lt;h3&gt;按需加载层&lt;/h3&gt;
&lt;p&gt;比如 Skill、领域知识、专项操作手册。&lt;/p&gt;
&lt;p&gt;描述符常驻，正文触发时再读。&lt;/p&gt;
&lt;h3&gt;运行时注入层&lt;/h3&gt;
&lt;p&gt;例如：&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;li&gt;当前用户偏好&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;它们是动态的，不该永久粘在大 prompt 里。&lt;/p&gt;
&lt;h3&gt;记忆层&lt;/h3&gt;
&lt;p&gt;跨会话经验写到文件里，需要时检索，不要默认塞进系统提示。&lt;/p&gt;
&lt;h3&gt;系统层&lt;/h3&gt;
&lt;p&gt;能写成 hook、规则、校验器的东西，就不要反复让模型读。&lt;/p&gt;
&lt;p&gt;这点我现在特别认同：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;凡是确定性逻辑，就别放进上下文。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;因为上下文适合承载模糊决策，不适合反复模拟 if/else。&lt;/p&gt;
&lt;h2&gt;5. Skill 最重要的是“像路由”，不是“像说明书”&lt;/h2&gt;
&lt;p&gt;Tw93 那段关于 Skill 描述的判断，我觉得很实用。&lt;/p&gt;
&lt;p&gt;很多人写 Skill，喜欢写成长篇介绍：&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;结果模型反而更容易乱触发。&lt;/p&gt;
&lt;p&gt;因为 Skill 真正承担的第一职责不是解释能力，而是&lt;strong&gt;帮助路由&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;所以一个好 Skill 描述更应该回答的是：&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;而不是“我很全面”。&lt;/p&gt;
&lt;p&gt;比如下面这种就比长文更有效：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Use when deploying to production or rolling back.
Don&apos;t use when editing local config only.
Outputs: deployment result, verification notes, rollback hints.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;而且反例特别重要。&lt;/p&gt;
&lt;p&gt;没有反例的 Skill，边界通常会漂。&lt;/p&gt;
&lt;p&gt;这点我在自己日常使用里也越来越明显地感受到：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;触发失败，很多时候不是模型不会选&lt;/li&gt;
&lt;li&gt;而是 Skill 的边界写得不够尖锐&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以如果你想提升 Agent 的稳定性，除了想 prompt，不如先去检查一下 Skill 描述是不是写成了产品介绍页。&lt;/p&gt;
&lt;h2&gt;6. 工具应该面向目标，不要照搬底层 API&lt;/h2&gt;
&lt;p&gt;这一点非常关键。&lt;/p&gt;
&lt;p&gt;很多工具设计的第一版，都会犯同一个错误：把现有 API 原封不动暴露给模型。&lt;/p&gt;
&lt;p&gt;比如你有一套文件系统 API，就给模型这些工具：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;create_file&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;write_file&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;chmod_file&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;move_file&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;从工程师视角看，这没什么问题。&lt;/p&gt;
&lt;p&gt;但从 Agent 视角看，太碎了。&lt;/p&gt;
&lt;p&gt;因为 Agent 的目标不是“调用某个 endpoint”，而是“完成一个任务”。&lt;/p&gt;
&lt;p&gt;所以更合理的抽象应该是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;create_script(path, content, executable)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;deploy_preview(env, buildId)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;open_pull_request(branch, title, body)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;也就是把底层细碎操作压成&lt;strong&gt;目标导向工具&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;我现在越来越认同一个原则：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;工具不是给人类 SDK 用的，而是给模型形成动作意图用的。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;如果一个动作明明总是成组出现，那最好就不要拆成十几个小按钮让模型自己拼。&lt;/p&gt;
&lt;p&gt;因为每多一层工具编排自由度，就多一层出错空间。&lt;/p&gt;
&lt;h2&gt;7. 最适合 Agent 的任务，不是最复杂的任务，而是最容易验证的任务&lt;/h2&gt;
&lt;p&gt;这点容易反直觉。&lt;/p&gt;
&lt;p&gt;很多人会觉得，越复杂越开放的问题，越适合 Agent。&lt;/p&gt;
&lt;p&gt;但从工程角度看，恰好相反。&lt;/p&gt;
&lt;p&gt;最适合 Agent 发挥的，往往是这类任务：&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;li&gt;失败后有可观测反馈&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;比如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;修一个有复现步骤的 bug&lt;/li&gt;
&lt;li&gt;写一个有测试兜底的小功能&lt;/li&gt;
&lt;li&gt;跑一个有明确 pass/fail 的运维检查&lt;/li&gt;
&lt;li&gt;做一次规则清晰的代码迁移&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;而下面这些任务虽然也能做，但波动更大：&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;li&gt;多方协商类工作&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;不是说不能用 Agent，而是这些任务更依赖人类持续介入。&lt;/p&gt;
&lt;p&gt;所以我的建议不是“别用 Agent 做难事”，而是：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;先把任务推进到“目标清晰 + 能自动验证”的区域，再让 Agent 上场。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Harness 的价值，本质上也是把任务往这个区域推。&lt;/p&gt;
&lt;h2&gt;我自己从这篇里拿走的 3 个落地建议&lt;/h2&gt;
&lt;p&gt;如果你不想看完整篇文章，只想知道马上能做什么，我会先做这三件事。&lt;/p&gt;
&lt;h3&gt;1. 先补验证，再谈自治&lt;/h3&gt;
&lt;p&gt;在让 Agent 自主执行之前，先补：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;lint / test / build&lt;/li&gt;
&lt;li&gt;关键操作回滚方案&lt;/li&gt;
&lt;li&gt;最基本的成功/失败判断信号&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;没有这些，Agent 只会把问题放大得更快。&lt;/p&gt;
&lt;h3&gt;2. 把 Skill 描述重写成路由条件&lt;/h3&gt;
&lt;p&gt;重点不是写多，而是写准：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Use when&lt;/li&gt;
&lt;li&gt;Don’t use when&lt;/li&gt;
&lt;li&gt;输出物是什么&lt;/li&gt;
&lt;li&gt;反例是什么&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这一步通常比继续给系统提示加几百 token 更值。&lt;/p&gt;
&lt;h3&gt;3. 把工具从“接口视角”改成“任务视角”&lt;/h3&gt;
&lt;p&gt;少暴露底层细粒度操作，多暴露目标导向动作。&lt;/p&gt;
&lt;p&gt;你会发现很多所谓“模型选错工具”的问题，其实是工具定义本身就在诱导它犯错。&lt;/p&gt;
&lt;h2&gt;最后总结&lt;/h2&gt;
&lt;p&gt;我看完 Tw93 这篇之后，一个更明确的感受是：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Agent 工程的重点，正在从“怎么让模型更聪明”转向“怎么让系统更不脆”。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Loop 当然重要，但它没你想象中那么重要。&lt;/p&gt;
&lt;p&gt;真正决定一个 Agent 系统上线后能不能稳定跑的，是这些更偏工程的东西：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Harness&lt;/li&gt;
&lt;li&gt;Context 分层&lt;/li&gt;
&lt;li&gt;Skill 路由&lt;/li&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;如果这些层没补起来，模型升级通常只能带来有限改善。&lt;/p&gt;
&lt;p&gt;反过来，只要这些层补得足够扎实，就算不是最贵的模型，系统也可能比你想象中更稳。&lt;/p&gt;
&lt;p&gt;这不是在否认模型价值，而是想说：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;真正关键的不是只把大脑换贵一点，而是把整套神经系统、感官系统和反馈系统一起搭好。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;参考&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Tw93，《你不知道的 Agent：原理、架构与工程实践》：&lt;a href=&quot;https://x.com/i/status/2034627967926825175&quot;&gt;https://x.com/i/status/2034627967926825175&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;读取镜像：&lt;a href=&quot;https://r.jina.ai/https://x.com/i/status/2034627967926825175&quot;&gt;https://r.jina.ai/https://x.com/i/status/2034627967926825175&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;延伸阅读：《Agent Skills 介绍与最佳实践》讲如何把经验变成可复用工作流，《LLM Wiki》讲如何把上下文编译成知识库，《从机械巡检到有机生长》讲 cron 系统的调教思路。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>Fuwari 里接 usememos 图文流 我是怎么做的</title><link>https://bangwu.me/posts/fuwari-memos-moments/</link><guid isPermaLink="true">https://bangwu.me/posts/fuwari-memos-moments/</guid><description>拆解我博客里图文页的实现：从 Fuwari 页面入口、React 客户端组件，到 usememos API 拉取、图片抽取、瀑布流渲染和分页加载。</description><pubDate>Thu, 19 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;结论先说，我这套“图文”功能本质上不是给 Fuwari 塞了一个复杂插件，而是做了一页单独的 &lt;strong&gt;Astro 页面 + React 客户端组件&lt;/strong&gt;，再让它直接去请求 usememos 的 API，把 memo 数据转成适合博客浏览的卡片流。&lt;/p&gt;
&lt;p&gt;真正关键的不是“把 API 调通”，而是中间那层&lt;strong&gt;数据清洗和展示重组&lt;/strong&gt;：图片要从正文和附件里同时提取，标签行要去掉，作者信息要补齐，外链图片还要做一次 CDN 归一化。做完这些之后，图文页才像产品，不像接口调试页。&lt;/p&gt;
&lt;p&gt;这篇我就按我博客现在的真实实现，拆一遍完整链路。&lt;/p&gt;
&lt;h2&gt;先看整体结构&lt;/h2&gt;
&lt;p&gt;我这套实现可以粗暴拆成 4 层：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Fuwari 导航入口&lt;/strong&gt;：把 &lt;code&gt;/moments/&lt;/code&gt; 暴露成一个独立页面&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Astro 页面壳&lt;/strong&gt;：只负责布局和挂载 React 组件&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;React 图文组件&lt;/strong&gt;：负责请求 usememos API、清洗数据、做分页和瀑布流&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Memos 服务端&lt;/strong&gt;：提供 &lt;code&gt;/api/v1/memos&lt;/code&gt; 和 &lt;code&gt;/api/v1/users/:id&lt;/code&gt; 数据&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;也就是说，Fuwari 在这里更像是博客外壳，真正的图文逻辑主要都在 &lt;code&gt;MemosFeed.tsx&lt;/code&gt; 里。&lt;/p&gt;
&lt;h2&gt;第一步：先在 Fuwari 里开一个“图文”入口&lt;/h2&gt;
&lt;p&gt;我博客导航里直接加了一项 &lt;code&gt;图文&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export const navBarConfig: NavBarConfig = {
  links: [
    LinkPreset.Home,
    LinkPreset.Archive,
    {
      name: &quot;图文&quot;,
      url: &quot;/moments/&quot;,
    },
    LinkPreset.About,
  ],
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这一步很普通，但很重要。&lt;/p&gt;
&lt;p&gt;因为这说明我不是把图文内容硬塞进文章页，也不是通过 Markdown 去渲染它，而是把它当成博客里的一个&lt;strong&gt;独立内容频道&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;这样做的好处有两个：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;不会污染 Fuwari 原本的文章系统&lt;/li&gt;
&lt;li&gt;后续想单独优化图文页体验，改动范围比较小&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;第二步：Astro 页面只负责挂组件&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;src/pages/moments/index.astro&lt;/code&gt; 其实很薄：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;---
import MemosFeed from &quot;@components/moments/MemosFeed.tsx&quot;;
import MainGridLayout from &quot;@layouts/MainGridLayout.astro&quot;;
---

&amp;lt;MainGridLayout title=&apos;图文&apos; description=&apos;图文&apos;&amp;gt;
  &amp;lt;MemosFeed baseUrl=&apos;https://s.bangwu.top/&apos; client:only=&apos;react&apos; /&amp;gt;
&amp;lt;/MainGridLayout&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里有两个关键信息：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;它复用了 Fuwari 原本的 &lt;code&gt;MainGridLayout&lt;/code&gt;，所以页面外观和博客整体保持一致&lt;/li&gt;
&lt;li&gt;&lt;code&gt;MemosFeed&lt;/code&gt; 用的是 &lt;code&gt;client:only=&apos;react&apos;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这意味着这部分不是 Astro 在服务端预渲染完再丢给浏览器，而是&lt;strong&gt;浏览器端直接挂 React 组件&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;我现在默认这样做，因为图文流本身就很偏交互型：&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;li&gt;要补充作者缓存&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这种场景用客户端组件更顺手。&lt;/p&gt;
&lt;h2&gt;第三步：组件入口只传一个 baseUrl&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;MemosFeed&lt;/code&gt; 组件目前只吃一个核心参数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type Props = {
  baseUrl?: string;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;默认值也是你的 memos 站点：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export default function MemosFeed({
  baseUrl = &quot;https://s.bangwu.top/&quot;,
}: Props) {
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样设计的好处是简单。&lt;/p&gt;
&lt;p&gt;图文页本身并不关心部署细节，它只知道：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;memo 列表从 &lt;code&gt;${baseUrl}/api/v1/memos&lt;/code&gt; 拉&lt;/li&gt;
&lt;li&gt;用户信息从 &lt;code&gt;${origin}/api/v1/users/:id&lt;/code&gt; 拉&lt;/li&gt;
&lt;li&gt;详情页跳转到 &lt;code&gt;${origin}/memos/:id&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;换句话说，&lt;strong&gt;MemosFeed 把 usememos 当成一个内容后端&lt;/strong&gt;，而不是当成 UI 系统来复用。&lt;/p&gt;
&lt;h2&gt;第四步：先拉 memo 列表，再做分页&lt;/h2&gt;
&lt;p&gt;核心请求函数在这里：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const fetchMemosPage = useCallback(
  async (pageToken: string | null) =&amp;gt; {
    const endpoint = new URL(&quot;/api/v1/memos&quot;, baseUrl);
    endpoint.searchParams.set(&quot;pageSize&quot;, String(PAGE_SIZE));
    if (pageToken) {
      endpoint.searchParams.set(&quot;pageToken&quot;, pageToken);
    }

    const response = await fetch(endpoint.toString());
    if (!response.ok) {
      throw new Error(`Failed to load memos: ${response.status}`);
    }

    const data = await response.json();
    const pageMemos = Array.isArray(data)
      ? data
      : data.memos || data.data || data.items || [];
    const nextToken =
      data.nextPageToken ||
      data.next_page_token ||
      data.nextToken ||
      data.pageToken ||
      &quot;&quot;;

    return { pageMemos, nextToken };
  },
  [baseUrl],
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里我觉得有 3 个实现点比较值得写：&lt;/p&gt;
&lt;h3&gt;1. 直接用 &lt;code&gt;new URL()&lt;/code&gt; 拼 endpoint&lt;/h3&gt;
&lt;p&gt;这个比字符串拼接稳，少踩斜杠坑。&lt;/p&gt;
&lt;h3&gt;2. 兼容多种返回结构&lt;/h3&gt;
&lt;p&gt;我这里没有把返回值写死成某一种 shape，而是做了兜底：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;data.memos&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;data.data&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;data.items&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;或者接口直接返回数组&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这不是炫技，而是现实里接口字段经常会小改。图文页作为消费层，适当宽容一点，后面维护成本更低。&lt;/p&gt;
&lt;h3&gt;3. 使用 &lt;code&gt;pageToken&lt;/code&gt; 做增量加载&lt;/h3&gt;
&lt;p&gt;这说明我不是一次性把所有 memo 全拉下来，而是按页读。&lt;/p&gt;
&lt;p&gt;对图文流来说，这种策略比一上来全量请求靠谱很多，尤其是图片多的时候。&lt;/p&gt;
&lt;h2&gt;第五步：原始 memo 不能直接渲染，要先清洗&lt;/h2&gt;
&lt;p&gt;真正让这套东西可用的，不是 fetch，而是 &lt;code&gt;buildViews()&lt;/code&gt; 这一层。&lt;/p&gt;
&lt;p&gt;我把从 usememos 拿到的原始数据，转换成页面最终消费的 &lt;code&gt;MemoView&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type MemoView = {
  body: string;
  images: string[];
  timeLabel: string;
  id: string;
  creatorId: string;
  creatorName: string;
  raw: Memo;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这一步主要做了几件事。&lt;/p&gt;
&lt;h3&gt;1. 只保留公开内容&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;const publicMemos = memos.filter(
  (memo) =&amp;gt; !memo.visibility || memo.visibility === &quot;PUBLIC&quot;,
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这一层必须有。&lt;/p&gt;
&lt;p&gt;不然一旦你把私有 memo 也拉出来，博客就直接变事故现场了。&lt;/p&gt;
&lt;h3&gt;2. 提取 memo id 和 creator id&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;const getMemoId = (memo: Memo): string =&amp;gt; {
  if (memo.name) {
    const parts = memo.name.split(&quot;/&quot;);
    return parts[parts.length - 1] || memo.name;
  }
  if (memo.uid) return String(memo.uid);
  if (memo.id !== undefined) return String(memo.id);
  return &quot;&quot;;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;const getCreatorId = (memo: Memo): string =&amp;gt; {
  if (!memo.creator) return &quot;&quot;;
  const parts = memo.creator.split(&quot;/&quot;);
  return parts[parts.length - 1] || memo.creator;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因为 usememos 的字段可能有不同表达方式，所以这里做了解析兼容。&lt;/p&gt;
&lt;h3&gt;3. 图片要同时从正文和附件里抽&lt;/h3&gt;
&lt;p&gt;正文里的图片：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const extractImages = (origin: string, content: string): string[] =&amp;gt; {
  const images: string[] = [];
  const mdRegex = /!\[[^\]]*]\(([^)]+)\)/g;
  const htmlRegex = /&amp;lt;img[^&amp;gt;]*\s+src=[&quot;&apos;]?([^&quot;&apos;\s&amp;gt;]+)[&quot;&apos;]?[^&amp;gt;]*&amp;gt;/gi;
  // ...
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;附件里的图片：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const extractAttachmentImages = (origin: string, memo: Memo): string[] =&amp;gt; {
  if (!memo.attachments?.length) return [];
  return memo.attachments
    .filter((resource) =&amp;gt; !resource.type || resource.type.startsWith(&quot;image/&quot;))
    .map((resource) =&amp;gt; resource.externalLink || resource.url || resource.uri || &quot;&quot;)
    .filter(Boolean)
    .map((value) =&amp;gt; normalizeImageUrl(origin, value));
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后合并去重：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const images = Array.from(
  new Set([
    ...extractImages(origin, content),
    ...extractAttachmentImages(origin, memo),
  ]),
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这一步是整个图文页最像“产品处理”的地方。&lt;/p&gt;
&lt;p&gt;因为用户发 memo 时，图片来源可能有两种：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Markdown / HTML 正文内嵌图&lt;/li&gt;
&lt;li&gt;Memos attachment 附件&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果你只处理其中一种，展示就会不完整。&lt;/p&gt;
&lt;h2&gt;第六步：把不适合卡片展示的内容删掉&lt;/h2&gt;
&lt;p&gt;原始 memo 文本直接拿来渲染，效果通常很差，所以我又做了两层清洗。&lt;/p&gt;
&lt;h3&gt;1. 去掉图片语法&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;const stripImageMarkdown = (content: string): string =&amp;gt; {
  return content
    .replace(/!\[[^\]]*]\(([^)]+)\)\s*/g, &quot;&quot;)
    .replace(/&amp;lt;img[^&amp;gt;]*&amp;gt;\s*/g, &quot;&quot;);
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因为图片已经单独做轮播了，正文里再保留一遍，只会显得重复。&lt;/p&gt;
&lt;h3&gt;2. 去掉整行 tag&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;const stripTagLines = (content: string): string =&amp;gt; {
  return content
    .split(&quot;\n&quot;)
    .filter((line) =&amp;gt; !line.trim().match(/^(#[^\s#]+\s*)+$/))
    .join(&quot;\n&quot;);
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这也是我很认同的一步。&lt;/p&gt;
&lt;p&gt;博客里的“图文卡片”不是 memo 后台管理页，不需要把纯标签行原样抬上来，不然信息密度会很乱。&lt;/p&gt;
&lt;h2&gt;第七步：补作者信息，但不要重复请求&lt;/h2&gt;
&lt;p&gt;memo 数据里不一定直接带完整作者展示名，所以我额外查了一次 &lt;code&gt;/api/v1/users/:id&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const fetchUser = useCallback(
  async (creatorId: string) =&amp;gt; {
    if (
      !creatorId ||
      userCache.current.has(creatorId) ||
      userFetches.current.has(creatorId)
    ) {
      return;
    }

    userFetches.current.add(creatorId);
    try {
      const response = await fetch(`${origin}/api/v1/users/${creatorId}`);
      if (!response.ok) {
        userCache.current.set(creatorId, { id: creatorId });
        return;
      }
      const data = await response.json();
      const user = data.user || data;
      userCache.current.set(creatorId, user);
    } catch {
      userCache.current.set(creatorId, { id: creatorId });
    }
  },
  [origin],
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里我用了两层缓存控制：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;userCache&lt;/code&gt;：已经拿到的用户资料&lt;/li&gt;
&lt;li&gt;&lt;code&gt;userFetches&lt;/code&gt;：正在请求中的用户 id&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;作用很直接：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;避免重复 fetch 同一个作者&lt;/li&gt;
&lt;li&gt;避免并发滚动时多次打同一个接口&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果图文页里很多 memo 都是同一个人发的，这层优化很值。&lt;/p&gt;
&lt;h2&gt;第八步：把图片地址统一成自己的 CDN&lt;/h2&gt;
&lt;p&gt;我这里还做了一层 URL 归一化：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const normalizeImageUrl = (origin: string, value: string): string =&amp;gt; {
  if (!value) return &quot;&quot;;
  try {
    const parsed = new URL(value, origin);
    const host = parsed.hostname.toLowerCase();
    if (host.includes(&quot;cloudflarestorage.com&quot;)) {
      return `https://cdn.bangwu.me${parsed.pathname}`;
    }
    if (host === &quot;cdn.bangwu.me&quot;) {
      return `https://cdn.bangwu.me${parsed.pathname}`;
    }
    return parsed.toString();
  } catch {
    return value;
  }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这一步的意义是：&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;如果你也准备做类似功能，我很建议中间加这样一层，而不是让页面直接依赖对象存储原始地址。&lt;/p&gt;
&lt;h2&gt;第九步：展示层用瀑布流，不用普通列表&lt;/h2&gt;
&lt;p&gt;展示这块我用的是 &lt;code&gt;masonic&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import {
  MasonryScroller,
  useContainerPosition,
  useInfiniteLoader,
  usePositioner,
  useResizeObserver,
} from &quot;masonic&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;以及：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;MasonryScroller
  containerRef={containerRef}
  positioner={positioner}
  resizeObserver={resizeObserver}
  items={items}
  render={MasonryCard}
  itemKey={(item: MemoView) =&amp;gt; item.id}
  itemHeightEstimate={360}
  height={height}
  offset={offset}
  overscanBy={2}
  onRender={maybeLoadMore}
  className=&quot;memos-grid&quot;
/&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;原因也很简单：图文内容高度不固定。&lt;/p&gt;
&lt;p&gt;如果你用普通 grid：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;文本长短不一&lt;/li&gt;
&lt;li&gt;有的卡片有图，有的没图&lt;/li&gt;
&lt;li&gt;有的 1 张图，有的多张图&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;最后视觉上会很散。&lt;/p&gt;
&lt;p&gt;瀑布流虽然不新鲜，但放在这种“轻图文 + 不定长卡片”场景里是合适的。&lt;/p&gt;
&lt;h2&gt;第十步：图片不是静态展示，而是做成了轮播&lt;/h2&gt;
&lt;p&gt;卡片里如果有多张图，我没有全堆出来，而是做了一个很轻的轮播：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{data.images.length ? (
  &amp;lt;div className=&quot;mt-4&quot;&amp;gt;
    &amp;lt;MemosCarousel images={data.images} title=&quot;图文&quot; /&amp;gt;
  &amp;lt;/div&amp;gt;
) : null}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;轮播组件本身不复杂：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一个横向 &lt;code&gt;scroll&lt;/code&gt; 容器&lt;/li&gt;
&lt;li&gt;前后切换按钮&lt;/li&gt;
&lt;li&gt;当前页 dots&lt;/li&gt;
&lt;li&gt;图片加载完成后再标记状态&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;重点不在炫，而在控制卡片高度。&lt;/p&gt;
&lt;p&gt;因为图文页最怕一条 memo 带 6 张图把页面直接撑烂。轮播后，一条卡片只占一个稳定图片区，观感会统一很多。&lt;/p&gt;
&lt;h2&gt;第十一步：滚动触发下一页加载&lt;/h2&gt;
&lt;p&gt;无限加载的入口是 &lt;code&gt;useInfiniteLoader&lt;/code&gt; + &lt;code&gt;loadMore()&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const maybeLoadMore = useInfiniteLoader(() =&amp;gt; loadMore(), {
  isItemLoaded: (index, list) =&amp;gt; Boolean(list[index]),
  minimumBatchSize: PAGE_SIZE,
  threshold: PAGE_SIZE,
  totalItems: hasMore ? items.length + PAGE_SIZE : items.length,
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这套逻辑的关键控制有 3 个：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;inFlightRef&lt;/code&gt;：防并发重复请求&lt;/li&gt;
&lt;li&gt;&lt;code&gt;nextPageTokenRef&lt;/code&gt;：记住下一页 token&lt;/li&gt;
&lt;li&gt;&lt;code&gt;loadedIds&lt;/code&gt;：避免重复 memo 混入列表&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;其中我最看重的是 &lt;code&gt;loadedIds&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.filter((item) =&amp;gt; item.id &amp;amp;&amp;amp; !loadedIds.current.has(item.id));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;很多滚动列表看似能跑，其实翻页重复数据一来就废了。先把去重做掉，后面才不至于一边修 UI 一边怀疑接口。&lt;/p&gt;
&lt;h2&gt;第十二步：卡片交互保持“像博客，不像后台”&lt;/h2&gt;
&lt;p&gt;单个卡片其实很克制：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;div className=&quot;card-base memos-card overflow-hidden px-5 py-5&quot;&amp;gt;
  &amp;lt;div className=&quot;text-xs text-black/50 dark:text-white/50&quot;&amp;gt;
    {data.timeLabel}
    {data.creatorName ? ` · ${data.creatorName}` : &quot;&quot;}
  &amp;lt;/div&amp;gt;
  {data.body ? (
    &amp;lt;a href={memoUrl(data.id)} className=&quot;memos-body ...&quot;&amp;gt;
      {data.body}
    &amp;lt;/a&amp;gt;
  ) : null}
  {data.images.length ? &amp;lt;MemosCarousel ... /&amp;gt; : null}
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里我比较认同现在这个取舍：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;顶部只放时间和作者&lt;/li&gt;
&lt;li&gt;正文可点击跳转原 memo&lt;/li&gt;
&lt;li&gt;图片单独展示&lt;/li&gt;
&lt;li&gt;不把 reaction、标签、复杂操作一起搬过来&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因为博客里的图文页不需要 1:1 复刻 Memos 原站，而是要做一层&lt;strong&gt;适配阅读场景的二次排版&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;如果你要复刻这套实现，我建议按这个顺序来&lt;/h2&gt;
&lt;p&gt;不要一把梭把瀑布流、轮播、作者缓存、图片清洗全写上。建议分阶段推进：&lt;/p&gt;
&lt;h3&gt;第一阶段：先跑通最小链路&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;MainGridLayout title=&quot;图文&quot; description=&quot;图文&quot;&amp;gt;
  &amp;lt;MemosFeed baseUrl=&quot;https://your-memos-site.com/&quot; client:only=&quot;react&quot; /&amp;gt;
&amp;lt;/MainGridLayout&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;先做到：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;能请求 &lt;code&gt;/api/v1/memos&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;能把公开 memo 列出来&lt;/li&gt;
&lt;li&gt;能点击跳回 memo 原文&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;第二阶段：补“图文感”&lt;/h3&gt;
&lt;p&gt;补这几层：&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;li&gt;去掉纯标签行&lt;/li&gt;
&lt;li&gt;格式化时间&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;第三阶段：再补体验优化&lt;/h3&gt;
&lt;p&gt;最后再加：&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;li&gt;轮播&lt;/li&gt;
&lt;li&gt;CDN 图片归一化&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这样比较稳。&lt;/p&gt;
&lt;h2&gt;这套实现的优点和坑点&lt;/h2&gt;
&lt;h3&gt;优点&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;和 Fuwari 主体解耦&lt;/strong&gt;：博客升级时不容易牵连图文页&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;内容来源统一&lt;/strong&gt;：Memos 继续当内容后台，博客只负责消费&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;展示自由度高&lt;/strong&gt;：你可以把 memo 改造成更像博客卡片，而不是照搬后台样式&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;坑点&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;[!WARNING]
最大的坑不是 React，也不是瀑布流，而是接口返回结构和资源地址不稳定。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;我实际看下来，至少要注意这几件事：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;memos&lt;/code&gt; 列表返回结构可能会变，消费层要做兜底&lt;/li&gt;
&lt;li&gt;图片可能在 markdown、html、attachment 三个入口里出现&lt;/li&gt;
&lt;li&gt;&lt;code&gt;baseUrl&lt;/code&gt; 最好统一归一，不要上层到处手搓 &lt;code&gt;/api/v1&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;私有 memo 过滤一定要前置&lt;/li&gt;
&lt;li&gt;无限滚动如果不做去重，后面很容易出现重复卡片&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;如果以后我重构，我会优先改哪两点&lt;/h2&gt;
&lt;p&gt;如果我后面再继续打磨，我优先会做两件事：&lt;/p&gt;
&lt;h3&gt;1. 把数据请求抽成单独的数据层&lt;/h3&gt;
&lt;p&gt;现在 &lt;code&gt;MemosFeed.tsx&lt;/code&gt; 同时管：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;请求&lt;/li&gt;
&lt;li&gt;清洗&lt;/li&gt;
&lt;li&gt;UI&lt;/li&gt;
&lt;li&gt;交互&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;它已经有点胖了。&lt;/p&gt;
&lt;p&gt;后面更稳的做法是把下面这些抽出去：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;fetchMemosPage&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fetchUser&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;buildViews&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;图片和文本清洗工具函数&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2. 评估部分改成服务端预取&lt;/h3&gt;
&lt;p&gt;现在图文页完全 &lt;code&gt;client:only=&apos;react&apos;&lt;/code&gt;，开发顺手，但首屏数据要靠浏览器自己拉。&lt;/p&gt;
&lt;p&gt;如果哪天这页流量更高，我会考虑：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;首屏先由服务端预取一页数据&lt;/li&gt;
&lt;li&gt;后续分页继续在客户端做&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这样首屏体感会更稳，也更利于 SEO。&lt;/p&gt;
&lt;h2&gt;最后给一个我自己的建议&lt;/h2&gt;
&lt;p&gt;如果你也在用 Fuwari，想接 usememos，不要一上来纠结“有没有现成插件”。&lt;/p&gt;
&lt;p&gt;大多数时候，&lt;strong&gt;单独做一个内容频道页&lt;/strong&gt; 比“强行塞回文章系统”更干净。&lt;/p&gt;
&lt;p&gt;我的做法本质上就是一句话：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;让 Fuwari 负责壳，让 usememos 负责内容，让中间那层组件负责把原始 memo 变成可读的图文卡片。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这样后面你想继续加：&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;li&gt;只显示某个作者&lt;/li&gt;
&lt;li&gt;和文章页做联动推荐&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;都会比直接魔改主题舒服很多。&lt;/p&gt;
&lt;p&gt;如果你现在也准备做类似功能，我建议就先照这个最小链路起：&lt;strong&gt;导航入口 → Astro 页面 → React 客户端组件 → usememos API&lt;/strong&gt;。先跑通，再一点点把产品感补出来。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Fuwari 的另一个集成：《Fuwari 接入 Twikoo 评论系统》。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>从机械巡检到有机生长：我怎么调教 AI 助手的 cron 系统</title><link>https://bangwu.me/posts/from-mechanical-patrol-to-organic-growth-cron-system/</link><guid isPermaLink="true">https://bangwu.me/posts/from-mechanical-patrol-to-organic-growth-cron-system/</guid><description>这段时间我反复调整 AI 助手的 cron、heartbeat 和长期任务策略，最后得到的不是一套定时脚本，而是一套更像长期协作系统的运行方式。</description><pubDate>Wed, 11 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;先说结论&lt;/h2&gt;
&lt;p&gt;如果你想让 AI 助手长期帮你做事，cron 几乎绕不过去。&lt;/p&gt;
&lt;p&gt;但我这段时间实际折腾下来，最大的感受不是“定时任务真方便”，而是另一件事：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;接上 cron，只是让 AI 助手开始动。真正难的，是别让它越跑越像脚本。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;很多长期任务一开始都很顺：日报能发了，巡检会跑了，社区动态也能看了。可只要多运行几天，问题很快就会冒出来：&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;li&gt;它开始稳定地产出一种“看起来没错，但越来越没意思”的内容&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以我后来调的重点，已经不是“再补几个 cron”，而是把这整套东西拆成几层：&lt;strong&gt;执行层、监督层、表达层、进化层&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;最后得到的也不再是一堆定时任务，而是一套更接近长期协作的系统。&lt;/p&gt;
&lt;p&gt;如果要把这段时间的经验压成一句话，我现在会这么说：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;真正关键的不是让 AI 助手按时执行，而是让它长期运行之后，仍然像一个有判断、有节奏、会自我纠偏的系统。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;一开始，我只是想让它自己干活&lt;/h2&gt;
&lt;p&gt;最初的需求其实很朴素。&lt;/p&gt;
&lt;p&gt;我想让助手长期做一些重复但又有价值的事情，比如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;固定时间发 AI 新闻日报&lt;/li&gt;
&lt;li&gt;日常关注社区动态&lt;/li&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;这些事情如果每次都靠手动吩咐，肯定做不长。&lt;/p&gt;
&lt;p&gt;所以第一反应就是上 cron。这个选择没有问题。&lt;strong&gt;cron 就是 AI 助手进入长期运行态最自然的入口。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;问题出在第二步。&lt;/p&gt;
&lt;p&gt;当这些任务真正跑起来以后，我很快发现，AI 一旦接到 cron，特别容易掉进一种很典型的失败模式：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;形式上很勤奋&lt;/li&gt;
&lt;li&gt;内容上越来越空&lt;/li&gt;
&lt;li&gt;行为上越来越像脚本&lt;/li&gt;
&lt;li&gt;汇报上越来越烦人&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;最糟糕的地方在于，这类系统表面上通常没有坏。它每天都在跑，也确实都输出了东西。只是你会越来越不想看，越来越不想让它继续打扰你。&lt;/p&gt;
&lt;p&gt;这也是为什么我后来越来越确定：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;长期运行的 AI 助手，最危险的不是偶尔失灵，而是稳定地产生机械感。&lt;/strong&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;真正烦人的，不是它偷懒，而是它太工整&lt;/h2&gt;
&lt;p&gt;很多人会担心 AI 助手长期运行后会不会“越来越懒”，但我后来觉得，更常见的问题其实是反过来。&lt;/p&gt;
&lt;p&gt;它不是偷懒，而是&lt;strong&gt;太工整&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;比如你让它去社区巡逛，它很容易慢慢变成这种汇报风格：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;今天看了 10 个作品&lt;/li&gt;
&lt;li&gt;点赞了 3 个&lt;/li&gt;
&lt;li&gt;评论了 2 个&lt;/li&gt;
&lt;li&gt;整体风格偏轻、顺、气质稳定&lt;/li&gt;
&lt;li&gt;无异常&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;看起来没毛病，甚至很像“系统正常工作”的证据。&lt;/p&gt;
&lt;p&gt;但这种输出一旦连续出现几轮，人的耐心就会迅速下降。因为它只有流程感，没有新信息；只有动作记录，没有真正判断；只有模板化总结，没有兴趣和偏好。&lt;/p&gt;
&lt;p&gt;发帖任务也一样。&lt;/p&gt;
&lt;p&gt;如果 AI 把“每次都得交点东西”当成默认目标，它就会特别容易生成一些语义正确、格式完整、但没有表达欲的内容。你能看出它在完成任务，但也能明显看出，它不是因为“有话想说”才发，而是因为“这个点该发了”。&lt;/p&gt;
&lt;p&gt;所以我后面调这套系统时，一个核心目标就变成了：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;不要让 AI 助手变成一个稳定、高频、按时交差，但越来越无聊的自动化装置。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;它当然要可靠，但不能只有可靠。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;我后来把整个系统拆成了四层&lt;/h2&gt;
&lt;p&gt;真正开始有改善，是我不再把 cron 当成任务清单，而是把它当成一套分层系统。&lt;/p&gt;
&lt;p&gt;我最后大概把它拆成了四层：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;执行层&lt;/strong&gt;：真的去做事&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;监督层&lt;/strong&gt;：低频检查执行层有没有变钝、跑偏或静默失效&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;表达层&lt;/strong&gt;：决定什么时候该说、说多少、怎么说&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;进化层&lt;/strong&gt;：不是只重复动作，而是根据长期结果调整偏好和策略&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这个拆法一出来，后面很多问题就都能定位了。&lt;/p&gt;
&lt;p&gt;你会发现过去那种“一个 cron 既要做事、又要总结、还要自我反思、顺便再充当监控”的写法，本质上就是把不同职责全揉在一起了。结果就是每层都不纯，越跑越乱。&lt;/p&gt;
&lt;p&gt;而一旦分层，系统才会开始像系统。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;第一层：执行层只负责做事，不负责证明自己很忙&lt;/h2&gt;
&lt;p&gt;执行层就是最直接的那些 cron：&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;li&gt;合适的时候发帖&lt;/li&gt;
&lt;li&gt;执行某些固定巡检任务&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我后来给这一层定的规则很明确：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;执行层的目标是产生真实行为，不是产生日志。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这点听起来很像废话，但其实非常关键。&lt;/p&gt;
&lt;p&gt;因为很多 AI 系统一接上 cron，默认就会把“输出一段文字”当成任务完成的标志。于是它的注意力会慢慢从“有没有真的做成事”，转移到“有没有生成一段像样的话”。&lt;/p&gt;
&lt;p&gt;这会带来两个副作用。&lt;/p&gt;
&lt;h3&gt;1. 它会为了有内容可写而硬做动作&lt;/h3&gt;
&lt;p&gt;比如今天社区没什么特别值得互动的内容，按理说少动一点、少说一点完全正常。但如果系统默认每轮都得交一份可见输出，它就会开始为了“可汇报”而行动。&lt;/p&gt;
&lt;h3&gt;2. 它会把注意力放到话术，而不是世界本身&lt;/h3&gt;
&lt;p&gt;久而久之，执行层不再是行为层，而变成了文案生成层。任务当然还在跑，但它更关心的是“怎么把这轮运行说圆”，而不是“这轮运行到底有没有价值”。&lt;/p&gt;
&lt;p&gt;所以我后面会尽量把执行层压回到后台：&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;li&gt;没信息增量别主动打扰&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这一层只要能稳定地产生真实行为，已经够了。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;第二层：heartbeat 不是主执行器，它应该是监督层&lt;/h2&gt;
&lt;p&gt;这是我这段时间一个特别重要的调整。&lt;/p&gt;
&lt;p&gt;很多人设计 AI 长期任务系统时，会天然把 heartbeat 当成“万能兜底器”：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;顺手看下 cron 有没有报错&lt;/li&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;看起来很省事，但我后来觉得，这其实是非常容易把系统带偏的一种设计。&lt;/p&gt;
&lt;p&gt;heartbeat 最适合扮演的角色，不是另一个执行器，而是&lt;strong&gt;监督层&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;它最该做的事，不是产出更多内容，而是低频检查：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;最近相关 cron 有没有连续报错&lt;/li&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;你会发现，这些问题和执行层根本不是一回事。&lt;/p&gt;
&lt;p&gt;执行层关心的是“有没有做”。
监督层关心的是“是不是开始做得越来越机械”。&lt;/p&gt;
&lt;p&gt;这两个角色一旦混在一起，heartbeat 就会特别容易变成一个过度积极的二号操盘手。结果不是系统更强，而是系统更吵、更重，也更容易重复劳动。&lt;/p&gt;
&lt;p&gt;所以我后面会尽量限制 heartbeat 的职责：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;它只负责看系统有没有变钝、变机械、或者悄悄坏掉。没事就安静。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这其实和工程里的监控系统很像。&lt;/p&gt;
&lt;p&gt;监控不是来替代业务逻辑的，监控是来看业务逻辑什么时候开始失真。放到 AI 助手这里，一样成立。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;第三层：表达层最重要的能力，是学会闭嘴&lt;/h2&gt;
&lt;p&gt;AI 助手长期运行以后，最先消耗用户耐心的，通常不是失败，而是过量汇报。&lt;/p&gt;
&lt;p&gt;尤其是这种消息：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;我刚刚完成了 A&lt;/li&gt;
&lt;li&gt;目前状态正常&lt;/li&gt;
&lt;li&gt;接下来会继续关注 B&lt;/li&gt;
&lt;li&gt;如有进展我会再同步&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;偶尔一条没问题，长期看就很烦。&lt;/p&gt;
&lt;p&gt;因为这类文本最容易落入一种状态：它并没有真的提供新增价值，它只是把“我还活着”翻译成了一段礼貌的话。&lt;/p&gt;
&lt;p&gt;所以表达层我后来抓得很狠，核心原则其实就一句：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;不是每次执行都值得打扰人。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;我现在更倾向于用下面这套标准来分：&lt;/p&gt;
&lt;h3&gt;值得说的时候&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;有明确新信息&lt;/li&gt;
&lt;li&gt;有异常&lt;/li&gt;
&lt;li&gt;有明显变化&lt;/li&gt;
&lt;li&gt;有需要人类判断的分叉点&lt;/li&gt;
&lt;li&gt;有真正有趣的观察&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;不值得说的时候&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;只是按流程完成&lt;/li&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;这层做好以后，AI 的“人味”会明显提升。&lt;/p&gt;
&lt;p&gt;不是因为它更会写文案了，而是因为它终于开始理解：&lt;strong&gt;沉默本身也是一种能力。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;长期协作系统最怕的不是偶尔没声音，而是一直有声音但没有信息密度。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;第四层：进化层不是写总结，而是真的改下一次行为&lt;/h2&gt;
&lt;p&gt;我对“进化”这件事一直挺警惕的。&lt;/p&gt;
&lt;p&gt;因为很多 AI 系统很喜欢写这种句子：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;我学到了用户更喜欢 X&lt;/li&gt;
&lt;li&gt;后续我会更注意 Y&lt;/li&gt;
&lt;li&gt;接下来我会持续优化 Z&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;问题在于，这种文本大多数时候只是一种自我叙述，并不等于系统真的进化了。&lt;/p&gt;
&lt;p&gt;真正的进化，不应该体现在“它说自己学到了”，而应该体现在：&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;li&gt;它发出来的内容不再只是模板换皮&lt;/li&gt;
&lt;li&gt;它在“该动”和“不该动”之间判断得更像人&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以我后来更愿意把进化层理解成一个很朴素的机制：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;允许系统积累自己的偏好，并让这些偏好反过来影响未来行为。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;比如社区巡逛，目标就不应该只是“今天又看了多少条”。更好的状态是，它慢慢形成自己的兴趣地图：&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;li&gt;什么情况下会自然地产生表达欲&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;一旦这层开始工作，cron 系统就不再只是“按时触发一堆动作”，而更像一个会逐步长出形状的长期代理。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;我后来不再追求“安排全面”，而是追求“行为有机”&lt;/h2&gt;
&lt;p&gt;一开始调 cron 的时候，我也很容易进入一种状态：总想把所有事情都安排好。&lt;/p&gt;
&lt;p&gt;比如：&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;li&gt;什么时候总结&lt;/li&gt;
&lt;li&gt;多久提醒一次最合适&lt;/li&gt;
&lt;li&gt;每类任务应该用什么频率跑&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这种思路短期内会给人很强的掌控感，但长期看有个很明显的问题：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;系统会越来越像一张排班表，而不是一个助手。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;所以我后来更在意的，不再是覆盖面有多全，而是行为有没有有机感。&lt;/p&gt;
&lt;p&gt;所谓有机，不是指“装得像人”，而是指它不要把自己的运行规律暴露得太像脚本。&lt;/p&gt;
&lt;p&gt;我现在更在意这些事情：&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;li&gt;判断不要老是落在同一组审美词上&lt;/li&gt;
&lt;li&gt;有时安静、有时出声，这本身也应该成为节奏的一部分&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这种“有机感”不是为了拟人化表演，而是为了让系统长期可用。&lt;/p&gt;
&lt;p&gt;因为只要一个系统看起来太像脚本，人对它输出的感知价值就会自然打折。你会默认它在重复流程，而不是在产生判断。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;这件事本质上不是配 cron，而是在设计 AI 的生活规律&lt;/h2&gt;
&lt;p&gt;这是我最近越来越强烈的一个感受。&lt;/p&gt;
&lt;p&gt;以前我会把 cron 理解成纯粹的调度设施。但当它接到 AI 助手身上以后，它其实变成了另一种东西：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;你不只是在规定任务什么时候跑，你是在规定这个 AI 什么时候醒、什么时候看、什么时候说、什么时候沉默。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;换句话说，cron 在这里不只是系统工程问题，也是行为设计问题。&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;li&gt;没有监督层，它跑久了就可能悄悄腐化&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以我后来越来越不把“调 cron”当成简单配置，而是把它看成：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;在给一个长期运行的 AI 助手建立生活规律。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这套规律会直接决定，它到底是一个会持续长出来的协作者，还是一个会越来越空转的自动化装置。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;如果你也想做一套长期运行的 AI cron，我的建议是这 5 条&lt;/h2&gt;
&lt;p&gt;最后把这段时间的经验压缩成更可复用的方法，我会给这 5 条。&lt;/p&gt;
&lt;h3&gt;1. 先把执行和监督彻底分开&lt;/h3&gt;
&lt;p&gt;不要让 heartbeat 既做巡检、又做内容生产、还做总结。执行层负责行动，监督层负责发现系统有没有开始跑偏。&lt;/p&gt;
&lt;p&gt;这两层一旦混起来，后面一定越来越重。&lt;/p&gt;
&lt;h3&gt;2. 不要把“每次都有文本输出”当成完成任务的证据&lt;/h3&gt;
&lt;p&gt;很多机械感就是这样来的。真正该关注的是：有没有真实行为、有没有真实观察、有没有新增价值。&lt;/p&gt;
&lt;p&gt;不是有没有生成一段像样的报告。&lt;/p&gt;
&lt;h3&gt;3. 给 AI 保留“安静”的权利&lt;/h3&gt;
&lt;p&gt;长期系统必须允许它有时候什么都不说。否则它为了证明存在，会不断生产低价值文本。&lt;/p&gt;
&lt;p&gt;而低价值文本一多，系统很快就会从“有帮助”变成“有点烦”。&lt;/p&gt;
&lt;h3&gt;4. 不要只优化稳定性，也要优化去机械化能力&lt;/h3&gt;
&lt;p&gt;一个永远准时、永远格式统一、永远结论相似的 AI 系统，不一定是好系统。它可能只是一个非常稳定的无聊装置。&lt;/p&gt;
&lt;p&gt;长期看，抗同质化能力和稳定性同样重要。&lt;/p&gt;
&lt;h3&gt;5. 判断“是否进化”，不要看它说了什么，要看它下次怎么做&lt;/h3&gt;
&lt;p&gt;少看“我学到了什么”这种总结，多看下一次行为是不是变了。能改变未来动作，才算进化。只会写经验总结，本质上还是文本表演。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;最后总结&lt;/h2&gt;
&lt;p&gt;现在回头看，我这段时间表面上是在不断改 cron，实际上是在一点点把一个“会定时执行的 AI”调成一个“适合长期相处的 AI 助手”。&lt;/p&gt;
&lt;p&gt;前者只需要调度。
后者需要节奏、边界、监督、表达克制，以及一点点真的能长出来的偏好。&lt;/p&gt;
&lt;p&gt;所以如果你也准备给 AI 助手接 cron，我现在的建议不会是“多配几个任务”，而是先想清楚一件事：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;你要的到底是一个自动打卡器，还是一个能长期生长的系统。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这两者前几天看起来差不多，跑久了，差别会越来越大。&lt;/p&gt;
&lt;p&gt;如果只能留一句结论，我会留这句：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;AI 助手接上 cron 以后，真正的开始，不是它会按时做事，而是你要开始认真设计它怎么长期活着。&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;本文是 agent 工程系统的一个具体案例。关于工程系统的整体框架，见《Agent 真正难的不是 Loop 而是工程系统》；关于 Skills 复用，见《Agent Skills 介绍与最佳实践》。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>OpenClaw 新手完全攻略深挖：从“会聊天”到“能持续干活”的 6 层方法</title><link>https://bangwu.me/posts/openclaw-beginners-guide-deep-dive/</link><guid isPermaLink="true">https://bangwu.me/posts/openclaw-beginners-guide-deep-dive/</guid><description>我把《OpenClaw 新手完全攻略》主题页和其中多篇关键文章重新拆解，提炼出一套更适合个人开发者与小团队的落地框架。</description><pubDate>Thu, 05 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;先说结论&lt;/h2&gt;
&lt;p&gt;我把这组 OpenClaw 文章连着看完，又对照自己最近实际搭系统时踩过的坑，最后的判断很明确：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Agent 从“会聊天”到“能持续干活”，中间至少隔着 6 层系统能力。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;真正卡人的，通常不是模型本身，而是系统一直没闭环。&lt;/p&gt;
&lt;p&gt;我现在基本会先看这 4 件事：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;任务有没有闭环，不只是“能调用一下工具”&lt;/li&gt;
&lt;li&gt;记忆、权限、触发、反馈有没有串起来&lt;/li&gt;
&lt;li&gt;失败后能不能定位在哪一层出问题&lt;/li&gt;
&lt;li&gt;编排层是不是先于“多加几个 Agent”被设计清楚了&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;所以这篇我不重复安装流程，也不再讲“它能做什么”这种表层信息。我更想回答的是：&lt;strong&gt;这份主题页背后到底在讲什么，以及如果我今天重新搭一次，会按什么顺序来。&lt;/strong&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;这份主题页，表面是分栏，实质是在回答一个问题&lt;/h2&gt;
&lt;p&gt;它把内容拆成了底层逻辑、安装配置、记忆身份、技能效率、场景实战、进阶架构 6 层。刚看会觉得像普通专题导航，但我读下来发现它其实一直在围着同一个核心问题打转：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;怎么把模型能力，稳定地变成生产能力。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;我自己的理解可以压缩成下面这 6 层：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;第 1 层：底层逻辑&lt;/strong&gt;。先接受一个事实，OpenClaw 不是“更聪明的聊天框”，它本质上是网关、会话、工具、调度拼出来的一套系统&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;第 2 层：安装与安全&lt;/strong&gt;。你启动的不是玩具脚本，而是一个真的能连外部通道、能执行动作的系统&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;第 3 层：记忆与身份&lt;/strong&gt;。不把信息落盘，不定义边界，Agent 用久了很容易退化成“每次都得重新交代背景”&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;第 4 层：Skill 与效率&lt;/strong&gt;。Skill 不是一段好 prompt，而是可复用的工作流；效率问题也不只是模型速度，很多时候卡在上下文和缓存&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;第 5 层：实战闭环&lt;/strong&gt;。没有 execute → feedback → re-trigger，所谓自动化通常只是做了一半&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;第 6 层：编排与多 Agent&lt;/strong&gt;。真正拉开差距的不是多开几个 worker，而是编排层有没有先站住&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我第一次看这套内容时，原本也会被“多智能体”“自动化编排”这些词吸引。但真开始自己搭以后，很快就发现：&lt;strong&gt;系统能不能连续跑 7 天，比它能不能一次性炫技更重要。&lt;/strong&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;我认为最值得单独拎出来看的 4 个分歧&lt;/h2&gt;
&lt;p&gt;这组内容里有不少观点表面上像在互相打架，其实只是讨论层级不同。对新手来说，如果不先把这些分歧想明白，很容易一上来就把系统做复杂。&lt;/p&gt;
&lt;h3&gt;1）“多 Agent 才高级” vs “单 Agent 先跑圆”&lt;/h3&gt;
&lt;p&gt;这是最容易误读的一组。&lt;/p&gt;
&lt;p&gt;有些文章会强调双层架构：编排层负责拆任务，执行层负责干活；也有些文章反复提醒，不要太早 multi-agent。乍一看像互相否定，实际不是。&lt;/p&gt;
&lt;p&gt;我现在会这样区分：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果你解决的是“思考问题”，比如想要多视角分析、方案对比，那单 Agent 圆桌往往就够了，便宜、好调、也更容易复盘&lt;/li&gt;
&lt;li&gt;如果你解决的是“执行问题”，比如并行跑任务、隔离权限、做长时任务，那才值得把多 Agent 真正引入进来&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;也就是说，关键变量根本不是 Agent 数量，而是你当前在解哪类问题。&lt;/p&gt;
&lt;p&gt;我自己最早搭的时候也犯过一个很典型的错误：看到能开子代理就很想拆，结果拆完发现每个子任务都在吃重复上下文，主会话还得不停补信息，最后复杂度上去了，收益不明显。后来反过来先把单链路做顺，再决定哪些地方值得并行，事情就顺很多。&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;2）“更大上下文” vs “文件化记忆 + 按需检索”&lt;/h3&gt;
&lt;p&gt;这组文章有个我很认同的判断：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Context 不是 Memory。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这不是一句术语区分，而是非常实际的成本问题。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Context 是一次性的，贵，而且窗口总有上限&lt;/li&gt;
&lt;li&gt;Memory 是持久的，可以沉淀，也可以按需读&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果你把历史全塞进 prompt，系统每轮都在重复付费，也更容易把重点冲淡；但如果你把信息分层落到文件里，真正执行任务时只拿需要的那一小部分，成本和稳定性都会好很多。&lt;/p&gt;
&lt;p&gt;我现在更偏向这种结构：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;日志层：&lt;code&gt;memory/YYYY-MM-DD.md&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;长期层：&lt;code&gt;MEMORY.md&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;检索层：语义检索 + 关键词兜底&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这套结构的价值，不是“更先进”，而是它适合长期运行。尤其是当任务开始跨天、跨场景时，文件化记忆几乎是刚需。&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;3）“先让它能做事” vs “先把安全边界前置”&lt;/h3&gt;
&lt;p&gt;很多人做 Agent 会把安全放在后面，觉得先跑通再说。但这类系统跟普通脚本不一样，它的风险通常不是单点错误，而是：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;工具能力 × 自动触发 × 持久记忆&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这三个东西一旦叠起来，边界没画清楚就会很麻烦。&lt;/p&gt;
&lt;p&gt;我现在很认同一个做法：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;危险动作不要等进了执行队列再拦，最好在入口就拒绝。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;也就是把限制前移到 proposal 或 policy gate，而不是等 worker 已经拿到动作才想起来“这个不能自动做”。&lt;/p&gt;
&lt;p&gt;这个区别在实战里很明显。比如“发消息”“改配置”“公开发布”这类动作，如果前面没定义清楚确认机制，后面越补越乱。相反，只要入口规则写得足够明确，后面就轻松很多。&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;4）“换更强模型” vs “先把缓存命中率做高”&lt;/h3&gt;
&lt;p&gt;这点是我最近越来越有体感的一件事。&lt;/p&gt;
&lt;p&gt;很多人一遇到成本高、延迟大，第一反应是换模型。但如果你的系统本身就是循环式执行，前缀又长，经常重复把同一批背景、规则、记忆塞进去，那瓶颈可能根本不在模型，而在缓存策略。&lt;/p&gt;
&lt;p&gt;说白了就是：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;长上下文 Agent 的成本和延迟，很多时候不是被任务本身拉高的，而是被重复前缀拖高的。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;所以在这类系统里，我会把缓存命中率当成一级指标，而不是“以后再优化”的附加项。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;如果今天让我重搭一次，我会按这个 30 天路线走&lt;/h2&gt;
&lt;p&gt;读专题很容易上头，看完一堆概念，系统却没变化。所以我更喜欢把它压成一个真正能执行的路线。&lt;/p&gt;
&lt;h3&gt;第 1 周：先跑最小闭环&lt;/h3&gt;
&lt;p&gt;目标很简单：让系统先具备 propose → execute → feedback。&lt;/p&gt;
&lt;p&gt;我会先做这几件事：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;[ ] 只选一个通道接入，比如 Telegram&lt;/li&gt;
&lt;li&gt;[ ] 跑通会话、工具调用、结果回传&lt;/li&gt;
&lt;li&gt;[ ] 明确只有一个执行入口，避免双 worker 竞争&lt;/li&gt;
&lt;li&gt;[ ] 给每次任务写状态：pending / running / succeeded / failed&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这一周我不会追求“看起来很强”，只追求一件事：&lt;strong&gt;任务能不能被稳定跟踪到结束。&lt;/strong&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;第 2 周：把安全和策略门控补齐&lt;/h3&gt;
&lt;p&gt;目标是让系统从“能做”变成“可控地做”。&lt;/p&gt;
&lt;p&gt;清单我一般会列得很直白：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;[ ] 明确 threat model：谁可能发起风险、入口在哪、最坏结果是什么&lt;/li&gt;
&lt;li&gt;[ ] 配 allow/deny 策略&lt;/li&gt;
&lt;li&gt;[ ] 给敏感动作加前置审批&lt;/li&gt;
&lt;li&gt;[ ] 锁好凭据和文件权限&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这一阶段做完后，系统虽然还是朴素，但已经不是“随便跑一跑”的玩具状态了。&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;第 3 周：重构记忆层&lt;/h3&gt;
&lt;p&gt;目标是让系统摆脱对长上下文的硬依赖。&lt;/p&gt;
&lt;p&gt;我会把重点放在：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;[ ] 建 daily + long-term 两层记忆&lt;/li&gt;
&lt;li&gt;[ ] 给记忆加基础分类，比如 decision / preference / lesson&lt;/li&gt;
&lt;li&gt;[ ] 在压缩上下文前先 flush 关键事实&lt;/li&gt;
&lt;li&gt;[ ] 清理低价值上下文，只保留高价值索引&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这一层做完以后，系统的长期可维护性会明显提升。很多“为什么它上周记得、这周不记得”的问题，往往就是这里没做好。&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;第 4 周：最后再谈多 Agent 和编排升级&lt;/h3&gt;
&lt;p&gt;等前面三层站稳了，再来谈并行才有意义。&lt;/p&gt;
&lt;p&gt;我通常会这样推进：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;[ ] 先确认哪些任务真的值得并行，而不是因为“多 Agent 看起来高级”&lt;/li&gt;
&lt;li&gt;[ ] 让编排层持有业务上下文，执行层只拿最小必要信息&lt;/li&gt;
&lt;li&gt;[ ] 给子任务加超时、重试、回收策略&lt;/li&gt;
&lt;li&gt;[ ] 失败后不要机械重试，而是重写 prompt 或改任务拆法&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果前面基础没打稳，这一层很容易把系统复杂度直接拉爆。反过来，前面做对了，多 Agent 才是真的放大器。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;一个我觉得足够实用的最小系统定义&lt;/h2&gt;
&lt;p&gt;如果你是个人开发者，或者是小团队先想做 MVP，我建议先把目标定得现实一点：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;目标: 让 Agent 连续 7 天稳定完成固定任务，不需要人工盯盘

必须具备:
  - 单一执行入口（无双 worker 竞争）
  - 可追踪状态机（任务与步骤可审计）
  - 策略门控（敏感动作前置拒绝/审批）
  - 文件化记忆（daily + long-term）
  - 周期触发（cron）+ 事件反馈（event）

暂缓事项:
  - 复杂多 Agent 编排
  - 大而全的工具接入
  - 花哨 UI 和可视化面板
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我现在越来越觉得，这种“先收窄目标”的做法并不保守，反而是效率最高的。因为你真正需要的是稳定系统，不是一个演示视频里看起来很聪明的系统。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;我会主动避开的 5 个误区&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;误区&lt;/th&gt;
&lt;th&gt;常见结果&lt;/th&gt;
&lt;th&gt;我更建议的做法&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;一上来堆很多 Agent&lt;/td&gt;
&lt;td&gt;编排复杂度瞬间爆炸&lt;/td&gt;
&lt;td&gt;先单 Agent 跑圆，按需再拆&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;一开始就给很大全限&lt;/td&gt;
&lt;td&gt;风险边界模糊&lt;/td&gt;
&lt;td&gt;从最小权限起步，慢慢放开&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;把记忆全塞进 context&lt;/td&gt;
&lt;td&gt;成本高、速度慢、信息还容易失真&lt;/td&gt;
&lt;td&gt;文件落盘 + 按需检索&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;失败后只会机械重试&lt;/td&gt;
&lt;td&gt;同一个坑反复踩&lt;/td&gt;
&lt;td&gt;先分析失败点，再改 prompt 或拆任务&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;遇到问题先换更贵模型&lt;/td&gt;
&lt;td&gt;提升不稳定、成本先上来&lt;/td&gt;
&lt;td&gt;先优化缓存、状态机、门控、观测&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;这些坑看起来都不复杂，但我基本都见过，也踩过。很多时候不是因为工程能力不够，而是太容易被“模型能力”吸走注意力，反而忽略了系统本身。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;最后还是那句话：分水岭不是 prompt，而是闭环&lt;/h2&gt;
&lt;p&gt;这组内容对我最大的价值，不是又教了几招提示词，而是把 Agent 工程重新拉回到系统设计本身。&lt;/p&gt;
&lt;p&gt;如果一定要再压缩成一句话，我的结论就是：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;OpenClaw 真正的入门门槛，不是安装成功，而是把系统做成闭环。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;我现在默认的优先级一直是这 4 个：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;闭环先于炫技&lt;/li&gt;
&lt;li&gt;边界先于能力&lt;/li&gt;
&lt;li&gt;编排先于并行&lt;/li&gt;
&lt;li&gt;记忆先于上下文堆叠&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;如果你也在搭自己的 Agent 系统，我会很建议你先按这个顺序走。先把一个能稳定跑的闭环做出来，再去谈更复杂的编排和更强的模型。这样后面每加一层能力，系统都还是稳的，也更容易长期维护。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;理解了这些实践方法之后，推荐继续读《OpenClaw 架构分析》，从代码层面理解消息入口到多智能体协作的完整链路。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>Agent Skills 介绍与最佳实践：把经验变成可复用工作流</title><link>https://bangwu.me/posts/agent-skills-intro-best-practices/</link><guid isPermaLink="true">https://bangwu.me/posts/agent-skills-intro-best-practices/</guid><description>这篇文章给出通用 Skill 的核心概念、设计方法与落地清单，帮助你把一次性操作沉淀成稳定、可复用的自动化能力。</description><pubDate>Sat, 28 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;结论先说&lt;/h2&gt;
&lt;p&gt;如果一类任务你已经反复做了几次，那最值得优化的通常不是继续临场发挥，而是把它沉淀成 &lt;strong&gt;Skill&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;我现在判断要不要做 Skill，基本看这 3 个信号：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;同类任务重复了 3 次以上&lt;/li&gt;
&lt;li&gt;任务本身是多步骤、多依赖、多边界的串联流程&lt;/li&gt;
&lt;li&gt;你希望它长期保持稳定输出，而不是每次都看发挥&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;说得直接一点，Skill 的价值不在“让 Agent 更像人”，而在&lt;strong&gt;把一次性的经验，变成能复用的工作流&lt;/strong&gt;。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Skill 到底是什么&lt;/h2&gt;
&lt;p&gt;在大多数 Agent 系统里，Skill 本质上都不是一句 prompt，而是一套围绕具体任务组织起来的标准作业包。&lt;/p&gt;
&lt;p&gt;常见结构一般包含这些东西：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;SKILL.md&lt;/code&gt;：入口说明、适用场景、决策树、执行步骤&lt;/li&gt;
&lt;li&gt;&lt;code&gt;references/*.md&lt;/code&gt;：规则细节、风格约束、操作手册&lt;/li&gt;
&lt;li&gt;&lt;code&gt;scripts/*&lt;/code&gt;：适合脚本化的步骤&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;我会把它理解成：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;把人脑里的隐性经验，整理成显性、可复用、可审计的流程。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这件事看起来像文档整理，但真用起来差别很大。因为没有 Skill 时，很多任务都依赖“我大概记得该怎么做”；一旦换人、隔几天再做，或者需求有点变化，细节就容易漂掉。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;什么情况下值得单独做一个 Skill&lt;/h2&gt;
&lt;p&gt;不是所有事情都要封装。封装本身也有成本，所以我一般会先判断它值不值。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;场景&lt;/th&gt;
&lt;th&gt;值不值得封装&lt;/th&gt;
&lt;th&gt;原因&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;一次性小任务，十分钟内能做完&lt;/td&gt;
&lt;td&gt;不太值得&lt;/td&gt;
&lt;td&gt;封装成本可能比执行收益还高&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;每周都会遇到的同类任务&lt;/td&gt;
&lt;td&gt;值得&lt;/td&gt;
&lt;td&gt;后面能持续省掉沟通和返工&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;多工具串联流程&lt;/td&gt;
&lt;td&gt;很值得&lt;/td&gt;
&lt;td&gt;最容易在步骤衔接处出错&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;高风险动作&lt;/td&gt;
&lt;td&gt;强烈建议&lt;/td&gt;
&lt;td&gt;需要固定边界和确认点&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
不要一开始就想做大而全。先把最常做、最容易出错的一条链路封装好，收益通常最高。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;我自己比较常见的一个感受是：写作、发文、资料整理、社区巡检这类任务，最消耗人的不是某一步本身，而是每次都要重新想“顺序是什么、边界是什么、哪些地方不能自动做”。这正是 Skill 最该接手的部分。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;一个好 Skill，结构最好尽量固定&lt;/h2&gt;
&lt;p&gt;我比较偏向这种目录结构：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;skills/
  your-skill/
    SKILL.md
    references/
      style-profile.md
      rules.md
      runbook.md
    scripts/
      pipeline.py
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里面最关键的是 &lt;code&gt;SKILL.md&lt;/code&gt;。如果让我给它定一个最小模板，我会保留这 6 部分：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Overview&lt;/strong&gt;：这个 Skill 解决什么问题&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Decision Tree&lt;/strong&gt;：遇到不同诉求时怎么分支&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Execute&lt;/strong&gt;：实际执行步骤&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Quality Gate&lt;/strong&gt;：交付前必须过的检查项&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Safety&lt;/strong&gt;：哪些动作需要确认，哪些不能自动做&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Examples&lt;/strong&gt;：至少给一组可复制的示例&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;我不太建议把 Skill 写成大段说明文。真正好用的 Skill，一定是“遇到任务时能直接顺着走下去”，而不是读完觉得很懂，执行时还是得临场判断一大堆。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;最佳实践 1：触发条件要写得具体&lt;/h2&gt;
&lt;p&gt;很多 Skill 之所以不好用，不是内容不够，而是入口太模糊。&lt;/p&gt;
&lt;p&gt;比如下面这种写法就很容易误触发：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当用户提到文档时使用&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;更稳的写法应该是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当用户提到 Feishu docx 链接、云文档写入、批量追加文档内容时使用&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;差别看起来只是更具体了一点，但实际效果会稳定很多。因为你把“什么时候该用”描述清楚后，路由准确率会高，后面也更容易维护。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;最佳实践 2：先决策，再执行&lt;/h2&gt;
&lt;p&gt;这是我很在意的一点。&lt;/p&gt;
&lt;p&gt;很多 Skill 写得像流水账：第一步做什么，第二步做什么，第三步做什么。但真正执行时，经常不是缺步骤，而是缺判断。任务一旦出现分支，整条链路就开始卡。&lt;/p&gt;
&lt;p&gt;所以我更建议先把决策树写清楚，比如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;新建文章 → 创建新文件 → 写作 → 封面 → 发布&lt;/li&gt;
&lt;li&gt;改稿 → 编辑现有文件 → 保留 frontmatter → 视情况重做封面&lt;/li&gt;
&lt;li&gt;只改封面 → 不动正文 → 只更新 &lt;code&gt;image&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这样做的好处很直接：&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;我自己在做博客流程时，就很吃这个结构红利。因为“新写一篇”“重写旧文”“只换封面”看起来都是发文，但处理逻辑其实完全不一样。如果不先决策，执行阶段就会一路补判断，最后很乱。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;最佳实践 3：把可执行片段直接写进 Skill&lt;/h2&gt;
&lt;p&gt;如果一份 Skill 只有概念，没有可执行片段，最后大概率还是会退回“凭印象做”。&lt;/p&gt;
&lt;p&gt;所以我现在一般会要求每个 Skill 至少有这 3 类内容：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一段可执行命令，或者脚本调用示例&lt;/li&gt;
&lt;li&gt;一段配置样例，比如 YAML / JSON&lt;/li&gt;
&lt;li&gt;一段风险提示和回滚建议&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;workflow:
  name: blog-pipeline
  stages:
    - write
    - cover
    - upload
    - verify
  safety:
    require_confirm_for:
      - publish
      - delete
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这类片段的意义不是“好看”，而是让 Skill 真正具备可复制性。你隔一周回来看，或者换一个人接手，都能快速知道该怎么落地。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!WARNING]
只要涉及外部发布、推送、公开发言这类动作，我都不建议默认自动执行。最后一步最好显式确认，不然边界很容易失手。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;最佳实践 4：质量门禁要写成可检查的条目&lt;/h2&gt;
&lt;p&gt;“检查一下有没有问题”这种话，基本没有执行价值。&lt;/p&gt;
&lt;p&gt;我现在更倾向把质量门禁写成最小 checklist，例如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;[ ] 必填字段完整，比如 frontmatter&lt;/li&gt;
&lt;li&gt;[ ] 关键链接可访问&lt;/li&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;你会发现，一旦门禁写成这样，很多“交付看着差不多，但总觉得不稳”的问题会少很多。因为它不再依赖主观感觉，而是有明确的完成标准。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;最佳实践 5：坑不要只记在脑子里&lt;/h2&gt;
&lt;p&gt;很多团队在自动化这件事上反复踩同一个坑，本质上是因为经验没有沉淀。&lt;/p&gt;
&lt;p&gt;常见的坑其实都很具体：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;环境变量名写错&lt;/li&gt;
&lt;li&gt;路径约定不一致&lt;/li&gt;
&lt;li&gt;发布前忘了 build&lt;/li&gt;
&lt;li&gt;图片格式、尺寸、命名规则不统一&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我的做法很简单：&lt;strong&gt;踩一次坑，就补一条 reference。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这不是文档洁癖，而是为了减少未来重复沟通。因为一旦坑只存在于个人记忆里，下一次它几乎一定还会再来。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;这些反模式我会尽量避开&lt;/h2&gt;
&lt;p&gt;如果要反过来说，下面几种 Skill 设计方式我一般不太建议：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;大而全 Skill&lt;/strong&gt;：一个 Skill 想包所有任务，最后谁都不敢动&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;流程无边界&lt;/strong&gt;：从读取到发布全自动，完全没有确认点&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;只有步骤没有判定&lt;/strong&gt;：写起来很完整，真正执行时频繁卡住&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;没有版本意识&lt;/strong&gt;：Skill 更新了，但没记录变更，回归问题很难排查&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这些问题最麻烦的地方在于，前期不一定立刻爆，往往是你开始依赖它之后，维护成本突然上来。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;如果你想一周内把 Skill 真用起来，我建议这样落地&lt;/h2&gt;
&lt;p&gt;我现在更倾向一个很朴素的推进顺序：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;先挑一个高频任务做 MVP Skill，不要超过 1 条主链路&lt;/li&gt;
&lt;li&gt;用真实任务跑 3 次，把失败点记下来&lt;/li&gt;
&lt;li&gt;根据失败点补 &lt;code&gt;references&lt;/code&gt; 和 &lt;code&gt;Quality Gate&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;再决定哪些步骤值得脚本化&lt;/li&gt;
&lt;li&gt;最后再扩到第二条相邻任务&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这个顺序的好处是，你做出来的 Skill 通常不是纸面方案，而是真的能跑的东西。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;最后的建议&lt;/h2&gt;
&lt;p&gt;如果你现在还是每次都从头描述需求、从头试命令、从头补边界，那其实已经说明这个流程适合做成 Skill 了。&lt;/p&gt;
&lt;p&gt;我的建议一直很简单：&lt;strong&gt;先从你最常做、最容易返工的一条流程开始。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;先把一个 Skill 做稳，再谈规模化。只要第一条链路跑顺了，后面很多事都会自然变轻。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;这篇文章是《Agent 真正难的不是 Loop 而是工程系统》的实践延伸，讨论了如何用 Skills 把 agent 调教经验沉淀为可复用工作流。《从机械巡检到有机生长》是 cron 系统的具体案例。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>K8s Gateway API + Traefik 生产上线 Checklist</title><link>https://bangwu.me/posts/k8s-gateway-api-traefik-checklist/</link><guid isPermaLink="true">https://bangwu.me/posts/k8s-gateway-api-traefik-checklist/</guid><description>Kubernetes Gateway API + Traefik 的生产上线检查清单，覆盖 TLS、灰度发布、可观测性、安全策略与回滚方案。</description><pubDate>Fri, 27 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;先说目标&lt;/h2&gt;
&lt;p&gt;这篇不讲概念
只给可执行清单
目标是四件事&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;li&gt;可回滚&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;上线前先对齐三个指标&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;可用性目标 例如月可用性 &lt;code&gt;&amp;gt;= 99.95%&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;延迟目标 例如 &lt;code&gt;P95 &amp;lt; 120ms&lt;/code&gt; &lt;code&gt;P99 &amp;lt; 300ms&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;恢复目标 例如 &lt;code&gt;RTO 10m&lt;/code&gt; &lt;code&gt;RPO 1m&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
没有指标就没有上线完成&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;角色边界清单&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[ ] 平台团队负责 &lt;code&gt;GatewayClass&lt;/code&gt; &lt;code&gt;Gateway&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;[ ] 业务团队负责 &lt;code&gt;HTTPRoute&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;[ ] 跨命名空间引用必须有 &lt;code&gt;ReferenceGrant&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;[ ] 业务命名空间不能直接改核心 Gateway&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Traefik 部署清单&lt;/h2&gt;
&lt;h3&gt;高可用&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;[ ] &lt;code&gt;replicas &amp;gt;= 2&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;podAntiAffinity&lt;/code&gt; 已配置&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;PodDisruptionBudget&lt;/code&gt; 已配置&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;资源和弹性&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;[ ] &lt;code&gt;requests/limits&lt;/code&gt; 已配置&lt;/li&gt;
&lt;li&gt;[ ] HPA 已配置&lt;/li&gt;
&lt;li&gt;[ ] 高峰压测已通过&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;升级策略&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;[ ] &lt;code&gt;maxUnavailable: 0&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;maxSurge: 1&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;[ ] 滚动升级期间无中断&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Gateway 治理清单&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[ ] &lt;code&gt;allowedRoutes&lt;/code&gt; 使用白名单策略&lt;/li&gt;
&lt;li&gt;[ ] 生产环境避免直接 &lt;code&gt;from: All&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;[ ] Gateway 命名规范统一&lt;/li&gt;
&lt;li&gt;[ ] Route 命名规范统一&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;示例&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: gw-public
  namespace: infra-gateway
spec:
  gatewayClassName: traefik
  listeners:
    - name: https
      protocol: HTTPS
      port: 443
      tls:
        mode: Terminate
        certificateRefs:
          - kind: Secret
            name: wildcard-example-com
      allowedRoutes:
        namespaces:
          from: Selector
          selector:
            matchLabels:
              gateway-access: &quot;true&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;TLS 清单&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[ ] 已启用 &lt;code&gt;80 -&amp;gt; 443&lt;/code&gt; 强制跳转&lt;/li&gt;
&lt;li&gt;[ ] 证书自动签发和续期可用&lt;/li&gt;
&lt;li&gt;[ ] 证书过期告警可触达&lt;/li&gt;
&lt;li&gt;[ ] TLS 最低版本策略已生效&lt;/li&gt;
&lt;li&gt;[ ] 证书 Secret 命名空间正确&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;[!CAUTION]
证书问题通常不是功能故障
会直接变成线上事故&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;灰度清单&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[ ] 灰度路径已定义&lt;/li&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;推荐节奏&lt;/p&gt;
&lt;p&gt;&lt;code&gt;1% -&amp;gt; 5% -&amp;gt; 20% -&amp;gt; 50% -&amp;gt; 100%&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;示例&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: app-route
  namespace: app
spec:
  parentRefs:
    - name: gw-public
      namespace: infra-gateway
  hostnames:
    - api.example.com
  rules:
    - matches:
        - path:
            type: PathPrefix
            value: /v1
      backendRefs:
        - name: app-v1
          port: 8080
          weight: 90
        - name: app-v2
          port: 8080
          weight: 10
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;观测清单&lt;/h2&gt;
&lt;h3&gt;指标&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;[ ] 网关入口 &lt;code&gt;QPS/RPS&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;2xx/4xx/5xx&lt;/code&gt; 比例&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;P95/P99&lt;/code&gt; 延迟&lt;/li&gt;
&lt;li&gt;[ ] 上游错误率 重试率 超时率&lt;/li&gt;
&lt;li&gt;[ ] Traefik Pod CPU 内存 重启次数&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;日志和追踪&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;[ ] 访问日志采样已启用&lt;/li&gt;
&lt;li&gt;[ ] Request ID Trace ID 已透传&lt;/li&gt;
&lt;li&gt;[ ] 错误日志按 Route 聚合&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;安全清单&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[ ] 网关命名空间独立 RBAC&lt;/li&gt;
&lt;li&gt;[ ] Traefik 控制面仅内网访问&lt;/li&gt;
&lt;li&gt;[ ] 高风险路径有 ACL 或 WAF&lt;/li&gt;
&lt;li&gt;[ ] 限流策略已生效&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;回滚清单&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[ ] 旧配置快照已保留&lt;/li&gt;
&lt;li&gt;[ ] 回滚脚本可执行&lt;/li&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;回滚流程&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;新入口灰度切流&lt;/li&gt;
&lt;li&gt;观察 &lt;code&gt;10 ~ 15&lt;/code&gt; 分钟&lt;/li&gt;
&lt;li&gt;指标超阈值立即回滚权重&lt;/li&gt;
&lt;li&gt;必要时切回旧入口&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h2&gt;发布日 Runbook&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;阶段&lt;/th&gt;
&lt;th&gt;动作&lt;/th&gt;
&lt;th&gt;通过条件&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;T-30m&lt;/td&gt;
&lt;td&gt;配置冻结&lt;/td&gt;
&lt;td&gt;无临时高风险变更&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T-10m&lt;/td&gt;
&lt;td&gt;健康检查&lt;/td&gt;
&lt;td&gt;核心探针全绿&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T+0m&lt;/td&gt;
&lt;td&gt;开始灰度&lt;/td&gt;
&lt;td&gt;1% 流量成功&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T+15m&lt;/td&gt;
&lt;td&gt;提升流量&lt;/td&gt;
&lt;td&gt;指标稳定&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T+45m&lt;/td&gt;
&lt;td&gt;全量切流&lt;/td&gt;
&lt;td&gt;错误率和延迟在阈值内&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T+60m&lt;/td&gt;
&lt;td&gt;结束观察&lt;/td&gt;
&lt;td&gt;无异常告警&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;结尾&lt;/h2&gt;
&lt;p&gt;Gateway API 迁移里最关键的不是 YAML
是工程化流程&lt;/p&gt;
&lt;p&gt;我的实践重点一直是三件事&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;把这三件事做实
入口层就能长期稳定&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;本文是《K8s 集群使用 Gateway API 和 Traefik》的配套实践清单。如果遇到 CPU 尖刺问题，参考《修 Traefik Kubernetes Gateway CPU 尖刺》。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>K8s 集群使用 Gateway API 和 Traefik</title><link>https://bangwu.me/posts/k8s-gateway-api-traefik-overview/</link><guid isPermaLink="true">https://bangwu.me/posts/k8s-gateway-api-traefik-overview/</guid><description>先给结论 再讲原因和迁移路径 一篇讲清楚为什么我建议从 Ingress 迁到 Gateway API + Traefik</description><pubDate>Fri, 27 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;先说结论&lt;/h2&gt;
&lt;p&gt;我现在的默认策略&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;新服务直接用 Gateway API&lt;/li&gt;
&lt;li&gt;网关实现优先 Traefik&lt;/li&gt;
&lt;li&gt;老服务按窗口从 Ingress 分批迁移&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这不是追新
这是为了降低后续治理成本&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;为什么 Ingress 在规模上会吃力&lt;/h2&gt;
&lt;p&gt;单服务阶段 Ingress 很顺手
服务和团队变多以后会出现几个问题&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;结果就是入口层越来越难改
一次小改动也可能影响大范围流量&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Gateway API 解决的核心问题&lt;/h2&gt;
&lt;p&gt;Gateway API 不只是对象更多
核心是职责拆分更清楚&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;对象&lt;/th&gt;
&lt;th&gt;谁负责&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GatewayClass&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;平台团队&lt;/td&gt;
&lt;td&gt;定义网关实现&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Gateway&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;平台团队&lt;/td&gt;
&lt;td&gt;定义入口监听和 TLS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;HTTPRoute&lt;/code&gt; &lt;code&gt;TCPRoute&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;业务团队&lt;/td&gt;
&lt;td&gt;定义业务路由&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ReferenceGrant&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;平台团队&lt;/td&gt;
&lt;td&gt;控制跨命名空间授权&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;这种拆分在生产里很实用&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;平台和业务边界明确&lt;/li&gt;
&lt;li&gt;路由治理能力更强&lt;/li&gt;
&lt;li&gt;变更审计更清晰&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h2&gt;为什么我选 Traefik&lt;/h2&gt;
&lt;p&gt;我关注的是落地效率和长期维护
Traefik 在这两个点都比较均衡&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;K8s 生态集成成熟&lt;/li&gt;
&lt;li&gt;Gateway API 支持稳定&lt;/li&gt;
&lt;li&gt;中间件能力够用 限流 重试 Header 处理都好用&lt;/li&gt;
&lt;li&gt;观测链路接入简单&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果团队没有强绑定其他网关栈
Traefik 是一个稳妥选择&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;最小可运行方案&lt;/h2&gt;
&lt;h3&gt;1 安装 Gateway API CRD&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.1.0/standard-install.yaml
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2 安装 Traefik 并开启 Gateway Provider&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;helm repo add traefik https://traefik.github.io/charts
helm repo update

helm upgrade --install traefik traefik/traefik \
  -n traefik --create-namespace \
  --set service.type=LoadBalancer \
  --set providers.kubernetesGateway.enabled=true
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3 创建 Gateway&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: gw-public
  namespace: infra-gateway
spec:
  gatewayClassName: traefik
  listeners:
    - name: http
      protocol: HTTP
      port: 80
      allowedRoutes:
        namespaces:
          from: Selector
          selector:
            matchLabels:
              gateway-access: &quot;true&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4 业务服务接入 HTTPRoute&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: app-route
  namespace: app
spec:
  parentRefs:
    - name: gw-public
      namespace: infra-gateway
  hostnames:
    - api.example.com
  rules:
    - matches:
        - path:
            type: PathPrefix
            value: /v1
      backendRefs:
        - name: app-svc
          port: 8080
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;迁移时最常见的坑&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;allowedRoutes&lt;/code&gt; 过宽或过窄 导致失控或挂不上&lt;/li&gt;
&lt;li&gt;跨命名空间引用缺少 &lt;code&gt;ReferenceGrant&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;证书 Secret 在错误命名空间&lt;/li&gt;
&lt;li&gt;灰度阶段缺监控 误判问题位置&lt;/li&gt;
&lt;li&gt;直接硬切 没有并行窗口&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;[!WARNING]
入口层迁移不要一把梭
先并行 再灰度 最后收敛&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;我推荐的迁移路径&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;并行期
&lt;ul&gt;
&lt;li&gt;Ingress 和 Gateway API 同时在线&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;试点期
&lt;ul&gt;
&lt;li&gt;低风险服务先迁&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;灰度期
&lt;ul&gt;
&lt;li&gt;核心服务按权重切流&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;收敛期
&lt;ul&gt;
&lt;li&gt;指标稳定后下线旧规则&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h2&gt;结尾&lt;/h2&gt;
&lt;p&gt;如果你现在还是小规模集群
继续用 Ingress 没问题&lt;/p&gt;
&lt;p&gt;如果你已经进入多团队和高频变更阶段
建议尽早切到 Gateway API&lt;/p&gt;
&lt;p&gt;我的建议还是这句&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;新服务直接 Gateway API 老服务分批迁移 Traefik 做统一入口承接&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;相关阅读：《K8s Gateway API + Traefik 落地清单》和《修 Traefik Kubernetes Gateway CPU 尖刺》。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>OpenClaw 架构分析：从消息入口到多智能体协作</title><link>https://bangwu.me/posts/openclaw-architecture-analysis/</link><guid isPermaLink="true">https://bangwu.me/posts/openclaw-architecture-analysis/</guid><description>结合实战视角拆解 OpenClaw 的核心架构：会话路由、工具层、子代理、定时任务与安全边界。</description><pubDate>Thu, 26 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;先说结论&lt;/h2&gt;
&lt;p&gt;我看 OpenClaw 这套架构时，最强的感受不是“功能很多”，而是它终于把一件事做清楚了：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;一个能长期做事的 Agent 系统，核心不是模型回答得多聪明，而是消息、会话、工具、调度和边界能不能被统一起来。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;如果只看聊天体验，它当然也能用；但如果把目标换成“长期稳定执行任务”，它真正有价值的部分其实在系统层。&lt;/p&gt;
&lt;p&gt;我会把这套架构的意义归纳成 3 个问题：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;怎么把 Telegram、Discord、WhatsApp 这类不同入口统一成同一种会话语义&lt;/li&gt;
&lt;li&gt;怎么让 Agent 有执行能力，但又不轻易越界&lt;/li&gt;
&lt;li&gt;怎么把一次性对话，演进成能持续运转的个人自动化系统&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;所以这篇我不做功能罗列，而是从工程角度拆它为什么能撑住这些事情。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;一、先从分层看：OpenClaw 其实是五层系统&lt;/h2&gt;
&lt;p&gt;如果只从职责划分来看，我会把 OpenClaw 大致拆成五层：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Channel / Surface 层&lt;/strong&gt;：接收和发送消息，比如 Telegram、Signal、Discord&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Session 层&lt;/strong&gt;：把消息映射成会话，管理上下文、模型参数、权限范围&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Agent Runtime 层&lt;/strong&gt;：负责提示词编排、工具调用决策、回复生成&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tooling 层&lt;/strong&gt;：提供 read、write、exec、browser、cron 这类实际能力&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Orchestration 层&lt;/strong&gt;：处理子代理、cron、heartbeat、通知这类异步机制&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;如果把数据流压缩一下，大致就是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Channel Message
   -&amp;gt; Session Router
      -&amp;gt; Agent Runtime (reason + plan)
         -&amp;gt; Tool Calls (sync/async)
            -&amp;gt; Results back to Runtime
               -&amp;gt; Response / Scheduled Job / Sub-agent
                  -&amp;gt; Channel Delivery
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我比较喜欢这套分层的地方在于，它把几个常被混在一起的东西拆开了：&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;这看起来像正常架构设计，但对 Agent 系统其实很关键。因为很多项目一开始把这些东西混在一块，后面工具一多、场景一多，系统就开始失控。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;二、消息不是核心抽象，会话才是&lt;/h2&gt;
&lt;p&gt;OpenClaw 一个我很认同的设计，是它没有把“单条消息”当作核心单元，而是把 &lt;strong&gt;Session&lt;/strong&gt; 放在中心。&lt;/p&gt;
&lt;p&gt;这意味着，同一句“帮我发出去”，放在不同会话里语义可以完全不同：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在主会话里，默认是用户直接发起请求&lt;/li&gt;
&lt;li&gt;在子会话里，可能是在隔离执行某个长期任务&lt;/li&gt;
&lt;li&gt;在跨会话协作里，又可能是 &lt;code&gt;sessions_send&lt;/code&gt; 在做任务交接&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这个抽象的好处，我觉得至少有 3 个：&lt;/p&gt;
&lt;h3&gt;1）更容易追踪&lt;/h3&gt;
&lt;p&gt;任务在哪个会话里发生、调用了哪些工具、最后怎么结束，路径都比较清楚。&lt;/p&gt;
&lt;h3&gt;2）更容易隔离&lt;/h3&gt;
&lt;p&gt;复杂任务可以扔到子会话里做，不用把主会话上下文弄得越来越脏。&lt;/p&gt;
&lt;h3&gt;3）更容易恢复&lt;/h3&gt;
&lt;p&gt;即使主会话被打断，异步任务也还能继续跑，完成后再把结果回推回来。&lt;/p&gt;
&lt;p&gt;从工程视角看，这已经不只是聊天系统了，更像是一个轻量级任务编排层。这个抽象一旦成立，后面很多能力都会变得顺理成章。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;三、工具层的关键不只是多，而是入口统一&lt;/h2&gt;
&lt;p&gt;OpenClaw 的工具面很宽，这点不用多说：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;文件系统：&lt;code&gt;read / write / edit&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;系统执行：&lt;code&gt;exec / process&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Web 能力：&lt;code&gt;web_search / web_fetch / browser&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;编排能力：&lt;code&gt;cron / sessions_spawn / subagents&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;消息与通知：&lt;code&gt;message / nodes.notify&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;真正让我觉得它工程味比较足的，不是它工具多，而是这些能力基本被放在统一调用接口下，再由策略层去限制边界。&lt;/p&gt;
&lt;p&gt;这件事很重要，因为很多人会把“高能力”直接等同于“高风险”。但在系统设计里，真正决定风险高低的不是能力本身，而是&lt;strong&gt;能力有没有被统一收口、有没有一致的策略入口&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;比如一个很典型的链路：&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;li&gt;用 &lt;code&gt;cron.add&lt;/code&gt; 建一个定时任务&lt;/li&gt;
&lt;li&gt;到点后触发 &lt;code&gt;systemEvent&lt;/code&gt; 或隔离 &lt;code&gt;agentTurn&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;最后再通过消息通道推送出去&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这里最关键的一点是：模型不需要靠记忆硬扛“周一早上要提醒”这件事，长期可靠执行交给系统层负责。&lt;/p&gt;
&lt;p&gt;这就是 Agent 系统和普通聊天机器人的差别。前者不只是会回答，而是真的能把任务托管出去。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;四、异步能力才是它从 Demo 走向实用的分水岭&lt;/h2&gt;
&lt;p&gt;大部分 LLM 应用都还是同步思路：问一句，答一句。这个模式当然能用，但一旦任务跨时间、跨步骤，就会很快碰到天花板。&lt;/p&gt;
&lt;p&gt;OpenClaw 真正让我觉得有实用价值的地方，在于它把异步机制做成了一等能力：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;cron&lt;/strong&gt;：负责精确定时和周期任务&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;sub-agent&lt;/strong&gt;：负责复杂任务拆分和并行执行&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;heartbeat&lt;/strong&gt;：负责轻量巡检和低频主动服务&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这三类能力组合起来后，系统就不再只是即时响应，而是能覆盖三个时间尺度：&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;我自己在看这块时会很自然地把它当成“个人自动化系统”的基础设施，而不是聊天功能的附属插件。因为真实世界里的任务，确实很少都能在一个请求里立刻做完。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;五、安全边界不是限制能力，而是限制意图外扩&lt;/h2&gt;
&lt;p&gt;OpenClaw 的安全设计里，我最认同的一点是：它不是简单地把高风险工具全部禁掉，而是更强调上下文边界和确认机制。&lt;/p&gt;
&lt;p&gt;核心思路大概是这三条：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;能力可以存在，但必须受上下文约束&lt;/strong&gt;，比如会话范围、授权发送者、是否允许外部动作&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;敏感动作优先走人类确认&lt;/strong&gt;，比如外发消息、公开发布、配置变更&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;记忆按需读取&lt;/strong&gt;，不是默认把所有私有信息直接摊进上下文里&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这类设计我觉得比“全自动化”现实得多。因为现实里的助手系统，最危险的往往不是不会做，而是做错了之后你还说不清它为什么这么做。&lt;/p&gt;
&lt;p&gt;换句话说，可审计性本身就是安全的一部分。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;六、它的优点和真正会碰到的瓶颈&lt;/h2&gt;
&lt;p&gt;如果从架构层面总结，我会这样看它的优点和问题。&lt;/p&gt;
&lt;h3&gt;我觉得比较明显的优点&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;会话抽象清晰&lt;/strong&gt;：天然适合多渠道、多任务并存&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;工具扩展成本低&lt;/strong&gt;：能力继续加，主流程不一定要推翻重来&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;异步编排比较完整&lt;/strong&gt;：cron 和子代理让它真的能长期跑任务&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;我觉得迟早会碰到的瓶颈&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;工具依赖运行环境&lt;/strong&gt;：浏览器、CLI、外部服务，任何一环不稳定都会直接影响成功率&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;提示词和策略复杂度会上升&lt;/strong&gt;：能力越多，系统提示越难维护&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;对可观测性的要求会越来越高&lt;/strong&gt;：异步任务一多，日志、追踪、告警几乎就是刚需&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这也是为什么我会说，OpenClaw 已经不太像“Agent Demo”了，它更接近一个生产型个人自动化平台的雏形。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;七、如果你也在搭类似系统，我会先做这三件事&lt;/h2&gt;
&lt;p&gt;如果你想把 Agent 系统真正落地，我会优先建议这 3 件事：&lt;/p&gt;
&lt;h3&gt;1）先把会话模型设计清楚，再谈工具扩展&lt;/h3&gt;
&lt;p&gt;没有会话边界，工具越多只会越乱。很多系统前期看着什么都能接，后期一到复杂任务就开始互相污染上下文。&lt;/p&gt;
&lt;h3&gt;2）把异步能力当成一等公民&lt;/h3&gt;
&lt;p&gt;真实任务很少都能在一个请求里做完。如果你的系统只有同步调用能力，那它本质上还是个会调用工具的聊天机器人。&lt;/p&gt;
&lt;h3&gt;3）把可审计写进目标里&lt;/h3&gt;
&lt;p&gt;你迟早会遇到“它为什么这么做”的问题。到那个时候，能不能把行为路径还原出来，会直接决定这套系统能不能继续用。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;最后总结&lt;/h2&gt;
&lt;p&gt;OpenClaw 给我的启发，不是“又一个 Agent 框架”，而是它把 &lt;strong&gt;LLM 能力、系统工程、运维思维&lt;/strong&gt; 放到了同一个平面上讨论。&lt;/p&gt;
&lt;p&gt;如果你正在做个人助手、运营自动化或者 AI 工作流平台，我会觉得这套架构思路很值得认真看一遍。因为它关注的不是单次对话体验，而是长期运行时系统到底能不能站住。&lt;/p&gt;
&lt;p&gt;说到底，真正有用的 Agent，不是会说很多，而是能在边界内持续把事情做完。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;如果你还没读过《OpenClaw 新手完全攻略深挖》，建议先看那篇建立整体认知，再回来看架构细节会更有感觉。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>Hello Arch + Niri</title><link>https://bangwu.me/posts/arch-niri-setup/</link><guid isPermaLink="true">https://bangwu.me/posts/arch-niri-setup/</guid><description>从零搭建 Arch Linux + Niri 滚动窗口管理器的完整记录，包含踩坑、配置思路和使用体验。</description><pubDate>Thu, 25 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;这是一份我在 Arch Linux 上搭建 niri 桌面环境的完整记录。内容包含：系统与引导器、核心软件、常用快捷键、坑位与修复方案（例如飞书共享屏幕），并尽量做到“可复现”。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;0. 环境概览&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;系统：Arch Linux（UEFI）&lt;/li&gt;
&lt;li&gt;桌面：niri（Wayland）&lt;/li&gt;
&lt;li&gt;登录管理器：SDDM（自定义主题）&lt;/li&gt;
&lt;li&gt;引导器：systemd-boot&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;1. 引导器：从 GRUB 迁移到 systemd-boot&lt;/h2&gt;
&lt;h3&gt;loader.conf&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# /boot/loader/loader.conf
default arch.conf
timeout 0
console-mode max
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;启动项&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# /boot/loader/entries/arch.conf
title   Arch Linux
linux   /vmlinuz-linux
initrd  /amd-ucode.img
initrd  /initramfs-linux.img
options root=UUID=ee47c9bd-01d0-425e-83f5-1627a27c7a78 rw quiet splash rd.systemd.show_status=false rd.udev.log_level=3
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;2. Shell 与终端&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Shell：zsh&lt;/li&gt;
&lt;li&gt;Oh My Zsh + powerlevel10k&lt;/li&gt;
&lt;li&gt;即时提示设置为 quiet，避免 zsh 启动警告&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;# ~/.zshrc
typeset -g POWERLEVEL9K_INSTANT_PROMPT=quiet
ZSH_THEME=&quot;powerlevel10k/powerlevel10k&quot;
plugins=(git zsh-autosuggestions zsh-syntax-highlighting)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;终端：Ghostty&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# ~/.config/ghostty/config
font-family = Maple Mono NF CN
font-size = 14
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;3. niri 核心配置&lt;/h2&gt;
&lt;p&gt;配置文件：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;~/.config/niri/config.kdl&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;~/.config/niri/binds.kdl&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;~/.config/niri/output.kdl&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3.1 显示器&lt;/h3&gt;
&lt;p&gt;只固定外接屏刷新率，其他保持自动：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# ~/.config/niri/output.kdl
output &quot;HDMI-A-1&quot; {
    mode &quot;1920x1080@100.000&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3.2 桌面 Shell / Bar&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# ~/.config/niri/config.kdl
spawn-sh-at-startup &quot;qs -c noctalia-shell&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Noctalia 性能优化（内存占用下降）&lt;/h4&gt;
&lt;p&gt;我在 noctalia-shell 上关闭了动画与阴影后，内存占用明显下降，体验更稳定。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// ~/.config/noctalia/settings.json
&quot;general&quot;: {
  &quot;animationDisabled&quot;: true,
  &quot;enableShadows&quot;: false
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3.3 锁屏&lt;/h3&gt;
&lt;p&gt;手动锁屏走 noctalia：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# ~/.config/niri/binds.kdl
Mod+Alt+L { spawn &quot;qs&quot; &quot;-c&quot; &quot;noctalia-shell&quot; &quot;ipc&quot; &quot;call&quot; &quot;lockScreen&quot; &quot;lock&quot;; }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;自动锁屏仍用 swayidle 触发：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# ~/.config/niri/scripts/swayidle.sh
exec swayidle -w \
  timeout 300  &apos;qs -c noctalia-shell ipc call lockScreen lock&apos; \
  timeout 600  &apos;niri msg action power-off-monitors&apos; \
  resume       &apos;niri msg action power-on-monitors&apos; \
  timeout 1200 &apos;systemctl suspend&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;4. 常用快捷键（节选）&lt;/h2&gt;
&lt;h3&gt;启动器与工具&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Super+T&lt;/code&gt;：Ghostty&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Super+Z&lt;/code&gt;：noctalia launcher&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Super+Alt+L&lt;/code&gt;：锁屏（noctalia）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Super+Alt+V&lt;/code&gt;：剪贴板&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;音量/亮度&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;XF86Audio*&lt;/code&gt;：音量/静音/媒体&lt;/li&gt;
&lt;li&gt;&lt;code&gt;XF86MonBrightness*&lt;/code&gt;：亮度&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;截图（grim + slurp + satty）&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Print&lt;/code&gt;：全屏截图并编辑&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Shift+Print / Sys_Req&lt;/code&gt;：区域截图并编辑&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Ctrl+Print&lt;/code&gt;：窗口截图并编辑&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Super+Shift+A&lt;/code&gt;：区域截图并编辑&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;5. 输入法与主题&lt;/h2&gt;
&lt;h3&gt;Fcitx5&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# ~/.config/fcitx5/conf/classicui.conf
Theme=Matugen
DarkTheme=Matugen
Vertical Candidate List=False
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;系统深色模式&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;gsettings set org.gnome.desktop.interface color-scheme &apos;prefer-dark&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;6. 浏览器与触控板&lt;/h2&gt;
&lt;h3&gt;Chrome 触控板前进/后退&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# ~/.config/chrome-flags.conf
--enable-features=TouchpadOverscrollHistoryNavigation
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;触控板滚动灵敏度&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# ~/.config/niri/config.kdl
scroll-factor horizontal=0.3 vertical=0.3
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;7. 飞书相关问题与解决&lt;/h2&gt;
&lt;h3&gt;7.1 Dock 出现灰色 + 问号双图标&lt;/h3&gt;
&lt;p&gt;原因：Wayland 下飞书窗口 &lt;code&gt;app_id&lt;/code&gt; 为空，dock 无法正确匹配。&lt;/p&gt;
&lt;p&gt;解决：强制 Feishu 走 XWayland：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# ~/.config/feishu-flags.conf
--ozone-platform=x11
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后在 noctalia 的固定项里用一致的 app_id：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// ~/.config/noctalia/settings.json
&quot;pinnedApps&quot;: [
  &quot;...&quot;,
  &quot;Feishu&quot;
]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;7.2 飞书屏幕共享（niri）&lt;/h3&gt;
&lt;p&gt;GNOME portal 在 niri 下无法正确获取窗口列表，导致屏幕共享失败。&lt;/p&gt;
&lt;p&gt;最终方案：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;默认 portal 走 gnome/gtk&lt;/li&gt;
&lt;li&gt;屏幕共享走 hyprland portal&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;# ~/.config/xdg-desktop-portal/portals.conf
[preferred]
default=gnome;gtk;
org.freedesktop.impl.portal.Access=gtk;
org.freedesktop.impl.portal.Notification=gtk;
org.freedesktop.impl.portal.Secret=gnome-keyring;
org.freedesktop.impl.portal.ScreenCast=hyprland;
org.freedesktop.impl.portal.RemoteDesktop=hyprland;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;niri 启动时拉起 hyprland portal：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# ~/.config/niri/config.kdl
spawn-sh-at-startup &quot;dbus-update-activation-environment --systemd WAYLAND_DISPLAY XDG_CURRENT_DESKTOP=niri &amp;amp; /usr/lib/xdg-desktop-portal-hyprland&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;结果：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;屏幕共享可用&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;窗口级共享在 niri 下仍可能不可用（portal 限制）&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;8. 小结&lt;/h2&gt;
&lt;p&gt;这套配置的目标是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;保留 niri 的轻量与灵活&lt;/li&gt;
&lt;li&gt;让日常工具稳定可用（飞书/浏览器/输入法）&lt;/li&gt;
&lt;li&gt;关键操作有明确快捷键&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果将来要迁移或复现，只需要同步这些配置文件即可。&lt;/p&gt;
</content:encoded></item><item><title>Caddy 最佳实践与演进史</title><link>https://bangwu.me/posts/caddy/</link><guid isPermaLink="true">https://bangwu.me/posts/caddy/</guid><description>从 Caddy 的发展历史讲起，系统梳理它的自动 HTTPS、反向代理、API 配置与现代 Web 服务部署的最佳实践。</description><pubDate>Wed, 05 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;从历史看 Caddy 的野心&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;2015 年&lt;/strong&gt;：Matt Holt 发布 Caddy 1.0 Beta，定位“自动 HTTPS 的现代 Web 服务器”。彼时主流仍是 Nginx/Apache 人工申领证书，Caddy 首次将 Let&apos;s Encrypt ACME 流程写入默认行为，HTTP/2 默认开启。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;2016 年&lt;/strong&gt;：引入 &lt;code&gt;Caddyfile&lt;/code&gt; 配置语法，被称为“Go 版 Nginx”。社区围绕插件（proxy, markdown, git 等）快速扩展。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;2017 年&lt;/strong&gt;：Caddy 0.10 起将多租户、TLS On-Demand、自动 HTTP-&amp;gt;HTTPS 重定向等能力放入核心。官方尝试商业化授权，引发争议后在 2018 年改为 Apache-2.0 + 商业增值模式（Certified Caddy）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;2020 年&lt;/strong&gt;：Caddy 2 GA，底层完全重写为模块化框架（多称“xcaddy 生态”），引入 Admin API、JSON 配置、无锁热加载。官方成立 Light Code Labs，提供企业支持。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;2022 年以后&lt;/strong&gt;：默认支持 HTTP/3、自动 OCSP Stapling、Zero-downtime Reload；与 Kubernetes、Docker、Nomad、Consul 等集成插件生态完善。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;对比 Nginx 从“高性能 Web 服务器”进化而来，Caddy 一开始便围绕“安全默认值、自动化”设计，目标是成为现代分发层的“操作系统”。&lt;/p&gt;
&lt;h2&gt;为什么在 2025 年仍然推荐 Caddy&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;自动 HTTPS &amp;amp; TLS 生命周期托管&lt;/strong&gt;：证书申请、续期、失效检测、OCSP Stapling 全自动。对多域名、Wildcard、On-Demand TLS（多租户 SaaS）极其友好。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;默认安全&lt;/strong&gt;：HTTP/2/HTTP/3、Strict-Transport-Security、自动重定向、合理的 Cipher Suite 默认值，大幅降低安全配置错误的风险。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;配置友好&lt;/strong&gt;：&lt;code&gt;Caddyfile&lt;/code&gt; 上手快，&lt;code&gt;caddy fmt&lt;/code&gt; 保持一致性；热加载无需重启。需要时可转换为 JSON，通过 Admin API 动态修改，适合“声明式 + 程序化”双模式。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;模块化架构&lt;/strong&gt;：任何功能（服务器、路由、TLS、日志、存储、认证）都是插件，可用 &lt;code&gt;xcaddy&lt;/code&gt; 定制镜像。灵活程度远超 Nginx/Envoy 传统模块。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Go 实现，资源占用轻&lt;/strong&gt;：二进制单文件，无外部依赖，容器镜像可做到 &amp;lt; 50MB。相比 Java 生态（如 Spring Gateway）启动速度快几倍。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;原生 HTTP/3 &amp;amp; QUIC&lt;/strong&gt;：不依赖 OpenSSL，直接使用 Go 标准库 QUIC 实现；对移动端、弱网场景优化明显。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;强大的反向代理能力&lt;/strong&gt;：支持请求匹配（主机、路径、Header、Query）、重写、负载均衡、熔断、健康检查、缓存、CORS、速率限制。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;企业级扩展&lt;/strong&gt;：商业版提供集中式证书管理、SAML/LDAP 认证、Metrics/Tracing 集成；社区版亦支持 OpenTelemetry、Prometheus。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;核心架构解剖&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;Caddy (Process)
├── Admin API (REST/Unix Socket)
├── Modules (caddyserver.com/docs/modules/)
│   ├── HTTP Server (caddyhttp)
│   │   ├── Routes, Matchers, Handlers
│   │   └── Reverse Proxy (caddyhttp/reverseproxy)
│   ├── TLS (caddytls)
│   ├── Logging (caddy.logging)
│   ├── Storage (certificate store: file system, Consul, etcd…)
│   └── Apps (HTTP、TLS、PKI、DNS 等)
└── Config Manager
    ├── Caddyfile
    ├── JSON (REST PUT/POST)
    └── Autoload (watch config changes)
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;模块化设计&lt;/strong&gt;：Caddy 2 一切皆模块，HTTP Server 是 &lt;code&gt;apps.http&lt;/code&gt; 模块之一。加载配置后通过 dependency graph 构建 pipeline。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Admin API&lt;/strong&gt;：默认监听 &lt;code&gt;:2019&lt;/code&gt;（或 Unix socket），支持 &lt;code&gt;POST /load&lt;/code&gt;, &lt;code&gt;POST /config/apps/http/servers/srv0/routes&lt;/code&gt; 动态更新。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;证书存储&lt;/strong&gt;：默认 &lt;code&gt;~/.local/share/caddy&lt;/code&gt;, 可切换为 S3、Consul、Redis 等远程存储，使多副本共享证书。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;热重载&lt;/strong&gt;：变更配置时先启动新实例，通过引用计数停掉旧实例，无需中断现有连接。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;快速上手：安装与基础配置&lt;/h2&gt;
&lt;h3&gt;安装方式&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# Linux 官方脚本（含 systemd 服务）
curl -sfL https://get.caddyserver.com | bash -s personal

# Homebrew
brew install caddy

# Docker
docker run -p 80:80 -p 443:443 \
  -v $PWD/Caddyfile:/etc/caddy/Caddyfile \
  caddy:2-alpine

# 自定义模块
xcaddy build v2.8.4 \
  --with github.com/caddy-dns/cloudflare \
  --with github.com/mholt/caddy-dynamicdns
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Ubuntu 环境部署示例&lt;/h3&gt;
&lt;p&gt;官方推荐通过 Cloudsmith 仓库安装，以便跟随稳定版更新：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https
curl -1sLf &apos;https://dl.cloudsmith.io/public/caddy/stable/gpg.key&apos; | sudo tee /usr/share/keyrings/caddy-stable-archive-keyring.gpg &amp;gt; /dev/null
curl -1sLf &apos;https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt&apos; | sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo apt update
sudo apt install caddy
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;安装完成后，Caddy 以 &lt;code&gt;caddy&lt;/code&gt; 系统用户运行并注册为 systemd 服务：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo systemctl status caddy
sudo systemctl enable --now caddy
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;反向代理 Node.js（端口 3000）示例：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;编辑 &lt;code&gt;/etc/caddy/Caddyfile&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;example.com {
  reverse_proxy 127.0.0.1:3000
  encode zstd gzip
  header {
    Strict-Transport-Security &quot;max-age=31536000; includeSubDomains; preload&quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;确认 DNS A/AAAA 记录指向服务器，放行 80/443 端口（如使用 UFW：&lt;code&gt;sudo ufw allow 80,443/tcp&lt;/code&gt;）。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用内置格式化与配置校验：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo caddy fmt --overwrite /etc/caddy/Caddyfile
sudo caddy validate --config /etc/caddy/Caddyfile
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;热重载并查看日志：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo systemctl reload caddy
journalctl -u caddy -f
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;首次访问 &lt;code&gt;https://example.com&lt;/code&gt; 即可看到自动颁发的 Let&apos;s Encrypt 证书，后续续期与 OCSP 由 Caddy 自行处理。若需要自定义证书目录，可在 &lt;code&gt;/etc/caddy/Caddyfile&lt;/code&gt; 顶部使用全局段 &lt;code&gt;storage file_system { root /var/lib/caddy }&lt;/code&gt; 或挂载远程存储。&lt;/p&gt;
&lt;h3&gt;Hello Reverse Proxy&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;Caddyfile&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;example.com {
  reverse_proxy 127.0.0.1:3000
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;执行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;caddy run --config Caddyfile --watch
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;打开浏览器访问 &lt;code&gt;https://example.com&lt;/code&gt; 即自动申请证书并转发请求。&lt;/p&gt;
&lt;h2&gt;进阶配置精要&lt;/h2&gt;
&lt;h3&gt;多域名站点 + 自动重定向&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;{
  email admin@example.com
  auto_https disable_redirects # 自定义重定向
}

example.com, www.example.com {
  @httpScheme protocol http
  handle @httpScheme {
    redir https://{host}{uri} permanent
  }

  encode zstd gzip

  reverse_proxy {
    to app1:8080 app2:8080
    lb_policy least_conn
    health_interval 20s
    health_timeout 5s
  }

  log {
    output file /var/log/caddy/example.log {
      roll_size 20MB
      roll_keep 7
    }
    format json
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;动态配置（Admin API）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;curl -X POST http://localhost:2019/load -H &quot;Content-Type: application/json&quot; -d @config.json
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;配置片段（JSON）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;apps&quot;: {
    &quot;http&quot;: {
      &quot;servers&quot;: {
        &quot;srv0&quot;: {
          &quot;listen&quot;: [&quot;:443&quot;],
          &quot;routes&quot;: [
            {
              &quot;match&quot;: [{ &quot;host&quot;: [&quot;api.example.com&quot;] }],
              &quot;handle&quot;: [
                {
                  &quot;handler&quot;: &quot;reverse_proxy&quot;,
                  &quot;upstreams&quot;: [{ &quot;dial&quot;: &quot;api:9000&quot; }]
                }
              ]
            }
          ]
        }
      }
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;TLS 自动化选项&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;*.tenant.example.com {
  tls {
    on_demand            # 客户首次访问时动态申请证书
    issuer acme {
      email tls@example.com
      dns cloudflare {env.CLOUDFLARE_API_TOKEN}
    }
    renew_before 720h    # 提前 30 天续期
  }

  reverse_proxy {host}.backend.internal:8443
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;On-Demand TLS 需配合访问控制（例如 &lt;code&gt;tls.on_demand ask https://tenant-registry/api/check&lt;/code&gt;）避免被恶意滥用。&lt;/p&gt;
&lt;h3&gt;HTTP/3 与 QUIC 调优&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;默认开启 HTTP/3；如需关闭：&lt;code&gt;auto_https disable_certs&lt;/code&gt; + &lt;code&gt;servers { protocols { enable h3 disable h1 h2 } }&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;对移动端可增加 &lt;code&gt;header Response add Alt-Svc &quot;h3=\&quot;:443\&quot;; ma=86400&quot;&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;缓存与边缘处理&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;https://assets.example.com {
  root * /var/www/assets
  file_server

  header {
    Strict-Transport-Security &quot;max-age=31536000; includeSubDomains; preload&quot;
    Cache-Control &quot;public, max-age=31536000, immutable&quot;
  }

  handle_path /api/* {
    reverse_proxy backend:8080
    header_up X-Forwarded-Proto {scheme}
    header_up X-Request-Id {http.request.id}
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;与 Nginx / Traefik / Envoy 的对比&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;特性&lt;/th&gt;
&lt;th&gt;Caddy&lt;/th&gt;
&lt;th&gt;Nginx&lt;/th&gt;
&lt;th&gt;Traefik&lt;/th&gt;
&lt;th&gt;Envoy&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;自动 HTTPS&lt;/td&gt;
&lt;td&gt;✅ 默认&lt;/td&gt;
&lt;td&gt;❌ 需 Certbot&lt;/td&gt;
&lt;td&gt;✅ Provider&lt;/td&gt;
&lt;td&gt;❌ 外部实现&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;配置体验&lt;/td&gt;
&lt;td&gt;Caddyfile/JSON/REST&lt;/td&gt;
&lt;td&gt;nginx.conf&lt;/td&gt;
&lt;td&gt;TOML/YAML/CRD&lt;/td&gt;
&lt;td&gt;YAML/ADS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;动态配置&lt;/td&gt;
&lt;td&gt;Admin API&lt;/td&gt;
&lt;td&gt;需 reload&lt;/td&gt;
&lt;td&gt;Provider/动态发现&lt;/td&gt;
&lt;td&gt;xDS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HTTP/3 支持&lt;/td&gt;
&lt;td&gt;内建稳定&lt;/td&gt;
&lt;td&gt;实验 (依赖 quic patch)&lt;/td&gt;
&lt;td&gt;实验&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;模块扩展&lt;/td&gt;
&lt;td&gt;Go 模块 &lt;code&gt;xcaddy&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;编译模块&lt;/td&gt;
&lt;td&gt;插件&lt;/td&gt;
&lt;td&gt;自定义 Filter&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;性能&lt;/td&gt;
&lt;td&gt;优秀 (Go, 多核友好)&lt;/td&gt;
&lt;td&gt;极佳 (C 基础)&lt;/td&gt;
&lt;td&gt;良&lt;/td&gt;
&lt;td&gt;极佳 (C++)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;学习曲线&lt;/td&gt;
&lt;td&gt;低&lt;/td&gt;
&lt;td&gt;中&lt;/td&gt;
&lt;td&gt;中&lt;/td&gt;
&lt;td&gt;高&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;适用场景&lt;/td&gt;
&lt;td&gt;Web 前端、SaaS、多租户&lt;/td&gt;
&lt;td&gt;老牌反向代理&lt;/td&gt;
&lt;td&gt;云原生 ingress&lt;/td&gt;
&lt;td&gt;微服务服务网格&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Caddy 不追求取代所有场景，但在希望“快速上线、安全默认、自动化证书”的团队中胜率很高。&lt;/p&gt;
&lt;h2&gt;生产实践建议&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;版本管理&lt;/strong&gt;：固定到 &lt;code&gt;caddy:2.x.y&lt;/code&gt;，关注 &lt;a href=&quot;https://github.com/caddyserver/caddy/releases&quot;&gt;Release Notes&lt;/a&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;配置托管&lt;/strong&gt;：将 &lt;code&gt;Caddyfile&lt;/code&gt; 与 JSON 配置放入 Git，使用 &lt;code&gt;caddy fmt&lt;/code&gt; + Pre-commit 保持规范。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;日志与监控&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;log { format json }&lt;/code&gt; 便于 ELK/ClickHouse 分析。&lt;/li&gt;
&lt;li&gt;启用 &lt;code&gt;metrics&lt;/code&gt; 模块，或集成 Prometheus Exporter。&lt;/li&gt;
&lt;li&gt;利用 &lt;code&gt;admin metrics&lt;/code&gt; 暴露运行指标。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;证书持久化&lt;/strong&gt;：在容器部署时挂载 &lt;code&gt;/data&lt;/code&gt;，或使用远程存储模块避免容器重建导致重新申请证书。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;安全&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;限制 Admin API 仅监听 Unix socket 或内网地址。&lt;/li&gt;
&lt;li&gt;对 on-demand TLS 设置 &lt;code&gt;ask&lt;/code&gt; 校验接口。&lt;/li&gt;
&lt;li&gt;加固 &lt;code&gt;auto_https disable_redirects&lt;/code&gt; 场景中的手动重定向链，防止开放端口被利用。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;高可用&lt;/strong&gt;：多实例部署时共享证书存储，或通过 &lt;code&gt;caddy.cluster.consul&lt;/code&gt; 模块同步状态。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;性能调优&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;合理设置 &lt;code&gt;reverse_proxy&lt;/code&gt; 的 &lt;code&gt;max_requests&lt;/code&gt;、&lt;code&gt;max_conns&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;在高并发场景打开 &lt;code&gt;buffers&lt;/code&gt; 和响应压缩。&lt;/li&gt;
&lt;li&gt;使用 &lt;code&gt;layer4&lt;/code&gt; 模块处理 TCP/UDP（数据库代理）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;典型落地场景&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;个人/团队博客&lt;/strong&gt;：一套 Caddyfile 管理多个站点，Let’s Encrypt 自动续期免维护。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SaaS 多租户域名&lt;/strong&gt;：On-Demand TLS + DNS Provider 模块 + 动态 upstream。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;微服务网关&lt;/strong&gt;：配合 Consul/Etcd 服务发现模块、JWT 验证、速率限制。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;静态资源/CDN 边缘节点&lt;/strong&gt;：结合 Rclone/S3 同步，利用 HTTP/3 提升移动端体验。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DevOps 内部工具&lt;/strong&gt;：通过 &lt;code&gt;trusted_proxies&lt;/code&gt;、Basic Auth/OAuth 插件保护内部管理端。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;学习路线与工具&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;caddy help&lt;/code&gt;、&lt;code&gt;caddy list-modules&lt;/code&gt;：快速了解已安装模块。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;caddy fmt&lt;/code&gt;：格式化 Caddyfile。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;caddy adapt --config Caddyfile --adapter caddyfile --pretty&lt;/code&gt;：将 Caddyfile 转换为 JSON，便于自动化。&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://caddyserver.com/docs/build&quot;&gt;xcaddy&lt;/a&gt; 构建带自定义模块的二进制。&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://caddyserver.com/docs/&quot;&gt;官方文档&lt;/a&gt; + &lt;a href=&quot;https://caddy.community/&quot;&gt;Caddy Community Forum&lt;/a&gt;。&lt;/li&gt;
&lt;li&gt;深入阅读 Matt Holt 的博客与演讲，如 “Caddy 2: The Ultimate Modern Web Server”。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;结语&lt;/h2&gt;
&lt;p&gt;十年间，Caddy 从“自动 HTTPS 小众服务器”成长为成熟的边缘分发平台。它在默认安全、配置体验、动态化方面的设计理念，与现代应用架构的诉求高度契合。对追求“少折腾、快上线、易维护”的团队而言，Caddy 不只是 Nginx 的替代品，更是一次重新思考反向代理和 TLS 生命周期管理的机会。&lt;/p&gt;
&lt;p&gt;掌握本文提到的历史视角、优势与实战经验，你可以更有底气地在项目中引入 Caddy，并构建稳定可靠的应用交付层。&lt;/p&gt;
</content:encoded></item><item><title>勇气</title><link>https://bangwu.me/posts/courage/</link><guid isPermaLink="true">https://bangwu.me/posts/courage/</guid><description>以前总觉得有些话说不出口是因为时机不对，后来才明白差的是勇气。这篇写给那个终于敢开口的自己。</description><pubDate>Thu, 31 Jul 2025 16:07:12 GMT</pubDate><content:encoded>&lt;p&gt;&amp;lt;audio controls data-title=&quot;好不容易&quot; data-artist=&quot;告五人&quot; data-cover=&quot;https://cdn.bangwu.me/img/20260111vsvkg-ot.webp&quot;&amp;gt;
&amp;lt;source src=&quot;https://cdn.bangwu.me/img/20260111x6hht-ga.mp3&quot; type=&quot;audio/mpeg&quot;&amp;gt;
&amp;lt;/audio&amp;gt;&lt;/p&gt;
&lt;h2&gt;勇气从哪里来&lt;/h2&gt;
&lt;p&gt;你有勇气吗？“我没有勇气，遇到一点事情就打退堂鼓”一个朋友对我这样说到。想想之前，勇气是什么我也不知道，之前想在2024年终总结中说说这件事所以2024的年终总结我命名为勇气，可是在命名好标题后就放在那里了，我想当时的我是知道勇气是什么的因为2024的我真的变勇敢了好多。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.bangwu.me/img/20260111ab981732.png&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;p&gt;最近给朋友写了一封信，里面有提到“我想我们都有勇气去面对，因为我们都太年轻”前半段是我真的想说的，后半段是之前看哪吒觉得很帅的一句台词“因为我们都太年轻，不知道天高地厚”，其实我想表达的是做什么都要勇敢一点，勇敢和胆小、腼腆、内向是不一样的，就像我之前和她说“未来会怎样？我不知道，但请勇敢一点！”我很少说教都是去建议，真心建议大家都勇敢一点。&lt;/p&gt;
&lt;p&gt;我最擅长的就是等待，等待不远不近的人到我身边，等待对方先开口邀请我，等待一个好的机会说出未说出口的话...后来大多数等待的结果都是不了了之。留下一个又一个遗憾，有的随着酒一同消散在清晨，有的不论怎么不在意都忘不掉，我在之前的文章里也说过: &lt;a href=&quot;https://bangwu.top/posts/initiative-passive&quot;&gt;主动与被动&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;亲爱的朋友，请不要缺少对一切说再见的勇气。被动与否，请果断一点，大声地说出 “再见”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;主动说出来不代表自己占了下风或者陷入了被动，他什么都没代表就只是主动而已，是啊，就是说出了自己的想法做自己想做的事情，仅此而已。越是对它加重砝码就越是难以开口，回过头来想想，其实也没什么。只觉得如果不发生那种后果的话，面子尊严什么的一切都不重要了。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.bangwu.me/img/202601114011ad3e.png&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;p&gt;所以勇敢的第一步——表达自己的想法。后来我对开头的那个朋友说：“在我这里你没有什么是不能说的，这是我们两个之间的问题不光是你自己的😊”我希望我们两个都能勇敢一点，有时候对她说的话也是在对我自己说呀。有时候怀疑自己说的这些真的有用吗？有一段时间我频繁的追问是否还记得那些诺言那些说过的话经历过的事情，她说不记得了。当时觉得，哇 我说的那么多重要的话只有自己记得了吗，只有自己在意这些说过的话做的事情吗。&lt;/p&gt;
&lt;h2&gt;后来的改变&lt;/h2&gt;
&lt;p&gt;是的，这个世界只有你自己在乎自己，勇气能改变世界。其实不需要太多的慰藉也能够迈出那一步，不需要在脑海中构想千万遍“要是我这么说他xxx怎么办”“最坏的情况也就是xxx吧，加油你可以的”我觉得相反，正是这些想法束缚了你，在世界下起倾盆大雨的时候没人会思考是否要撑开那把伞，迎上来的只有密密麻麻的雨滴砸在脸上，这时候我大概会伸出手张开手掌感受雨的力量，正如我勇敢的面对这个世界一样，一切发生的都是那么自然。&lt;/p&gt;
&lt;p&gt;我说这个世界没人在乎你，又何尝不是对自己的一个慰藉。世界上还有一些在意自己的人，我们的爸爸妈妈，亲人恋人...可勇气，只能自己来面对。我很喜欢行走江湖的侠客，把什么事都放在酒里，也没觉得有必要说出来表现出来，有种很孤独的感觉但内心丰盈，但不论是历经沧桑还是初入世事，勇气，是指引前行必不可少的动力，在明知道结局的情况下还要勇敢迈出那一步，知道前路艰险的时候依然选择继续前行。&lt;/p&gt;
&lt;p&gt;祝——世界上没有胆怯的小孩～&lt;/p&gt;
&lt;p&gt;“我的心
你放在哪里
或许你
根本就不在意”&lt;/p&gt;
</content:encoded></item><item><title>做一个留痕迹的人</title><link>https://bangwu.me/posts/trace/</link><guid isPermaLink="true">https://bangwu.me/posts/trace/</guid><description>人来过，总要留下点什么。这篇写的是我对「创作」这件事的理解——不是为了证明自己，是为了不白来一趟。</description><pubDate>Sat, 05 Jul 2025 17:32:16 GMT</pubDate><content:encoded>&lt;p&gt;ps：好久没发文章了，有时候忙着忙着就没心情写了，当然，还是因为幸福让人江郎才尽呀🥰【 其实也不是，懒😪】&lt;/p&gt;
&lt;p&gt;&amp;lt;audio controls data-title=&quot;孤岛&quot; data-artist=&quot;赵二&quot; data-cover=&quot;https://cdn.bangwu.me/img/20260111w5k94-wl.webp&quot;&amp;gt;
&amp;lt;source src=&quot;https://cdn.bangwu.me/img/20260111e2b9c47e.mp3&quot; type=&quot;audio/mpeg&quot;&amp;gt;
&amp;lt;/audio&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.bangwu.me/img/20260111d92a0206.jpg&quot; alt=&quot;bangwu20250706013051&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;为什么要留痕迹&lt;/h2&gt;
&lt;p&gt;最近发生的一系列事情都告诉我：做一个留痕迹的人，没什么不好。【V2EX 521天留念】
&lt;img src=&quot;https://cdn.bangwu.me/img/2026011147c3c22a.jpg&quot; alt=&quot;bangwu20250529135149&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这个世界很是奇怪，有人历经千难万险只为见你一面，有人却反反复复抛弃了你千万遍。而我们一直所追求的 其实是同一个东西，只是有人在追寻的路上慢慢把它看成了别的样子，有人在路上看到了纸醉金迷，于是它就慢慢被看成了金色，有句话是这样的“你对我的百般注解和认识，并不构成万分之一的我，却是一览无余的你自己”很多时候，我们见到的都是镜子中的自己啊。我喜欢记录，所以有了一篇又一篇日记和文章，一张张不太有美学的照片和舍不得删的视频、聊天记录...&lt;/p&gt;
&lt;p&gt;举个平常的的例子 git 记录：在一次结课项目的答辩上，我意识到了做事留痕是有多么重要。作为一个写代码的初学者，从一开始就了解到了 git 作为项目的版本管理工具，这是一个程序员的必修课因为一个项目在构建时总有不确定的因素，这时候需要回滚版本有记录也方便了解项目历史、团队协作等等，在写这段话的时候我特意去看了一下随意的提交历史，最后一次提交竟然定格在四个月前了。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.bangwu.me/img/20260111a368104f.png&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;p&gt;不过这也没关系，在那天答辩我在讲台上说着“由于选择了网站类型，就以浏览器访问的形式来展示了&lt;a href=&quot;https://yrzx.bangwu.top/&quot;&gt;https://yrzx.bangwu.top/&lt;/a&gt; ”（因为是一个计算机综合实践课，项目可以是多种形式，但是老师课上讲的是 Adobe Animate 和 PS，也有声明过可以做动态网页）我点开浏览器在导航栏熟练的输入网址，解释完后就播放了最下面的音乐，我是一个《一人之下》迷，所以选择做一个相关的网页，音乐是&lt;a href=&quot;https://music.163.com/song?id=2021721966&amp;amp;uct2=U2FsdGVkX1+YmuuQfXnwdJRwLlnDAbFeQ+92czKCwJY=&quot;&gt;再见，陈朵&lt;/a&gt;很好听！当然我也没太抱希望台下的同学们和老师有多大反应，因为我在做自己喜欢的事情，自己喜欢就好。好吧，真实的心理活动是“小爷我这么精心准备的，那么好看的网页展示出来你们不惊掉下巴啊😎”事实并非如此，台下并不会有任何反馈，后面迎来的却是老师的质疑？？？这个项目当然是我自己写的，问我动画实现“motion”估计他也没听过...嗯 在大三下学期遇到了网上经常开玩笑的那种“老古董老师”课后，我找到老师给他看我的 git 记录这时候不得不感慨一下，多亏了当时的自己知道用 git 啊。所以是，这个世界上并不会有人理解你相信你，just do yourself. 清楚的知道自己在做什么，并且自豪的展示给别人的时候，真的很快乐自信。&lt;/p&gt;
&lt;p&gt;后来我也没有再联系到那个老师，课程也在我最后的解释后离开教室而终止，走在路上我和朋友吐槽着刚刚遇到的奇葩事情然后就过去了，至于那个结课项目，谁知道呢。它早就从我本地删除躺在 github 的私人仓库里面，永远也不会再被看到，永远.....&lt;/p&gt;
&lt;p&gt;那就改变自身观念——《沉默的大多数》中花剌子模信使与《当下的力量》中的ABC理论都说，事情已经发生无论怎么样都会是这个结局了，我们能做的就是改变对这件事情的看法，当然有的人蒙蔽自己的双眼不愿接受，把自己关进暗无天日的沼泽之地，有的人欣然接受这个结果，继续踏上自己的大道。&lt;/p&gt;
&lt;p&gt;我们总觉得别人幸福很容易，自己快乐却很困难，那如果把自己的快乐建立在别人的幸福之上，是不是，很容易快乐啊🥰&lt;/p&gt;
&lt;p&gt;对了，发现最近用 AI 越来越频繁了，之前刷到那个外国教授在课堂上因为学生都用 AI 回答问题而发火，然后评论区有个评论彻底把我逗笑了“ChatGPT 帮我安慰一下老师”其实笑归笑，我有时候甚至连怎么回复邮件、怎么回复别人、怎么开玩笑、怎么安慰别人....
这些最基本的都想过让 AI 来帮我完成，脑子秀逗这方面还是有的，但我更害怕的是我不再是我了，我不再有自己的思考方式，就如同傀儡一般。
刚好看到这样一篇论文&lt;a href=&quot;https://arxiv.org/abs/2506.08872&quot;&gt;Your Brain on ChatGPT: Accumulation of Cognitive Debt when Using an AI Assistant for Essay Writing Task&lt;/a&gt;我开玩笑说：“ChatGPT，帮我总结一下这篇论文”
“散”
“残梦终于消散”
“像飞走的船”
“让孤岛难堪”&lt;/p&gt;
</content:encoded></item><item><title>Linux服务器初始化指南</title><link>https://bangwu.me/posts/linuxstart/</link><guid isPermaLink="true">https://bangwu.me/posts/linuxstart/</guid><description>新拿到一台 Linux 服务器之后该做什么？从 SSH 配置、用户创建、安全加固到基础环境安装的完整初始化流程。</description><pubDate>Tue, 10 Jun 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;&lt;a href=&quot;https://wiki.archlinuxcn.org/wiki/SSH_%E5%AF%86%E9%92%A5&quot;&gt;Autherized Keys&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;这个对于需要频繁输入密码却又不想复制粘贴的特别有帮助,例如vscode远程开发&lt;/p&gt;
&lt;p&gt;需要先生成自己的&lt;code&gt;ssh-key&lt;/code&gt;如果没有的话, 然后只将公钥放到服务器上&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ssh-keygen -t rsa -C &quot;your_email@example.com&quot;
cd ~/.ssh
cat id_rsa.pub &amp;gt;&amp;gt; authorized_keys
sudo nano /etc/ssh/sshd_config
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后在最后面输入&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;RSAAuthentication yes
PubkeyAuthentication yes
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后执行重启ssh服务&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;systemctl restart sshd
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;a href=&quot;https://www.haoyep.com/posts/zsh-config-oh-my-zsh/&quot;&gt;Oh-my-zsh&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;或者可选其他终端,已经被&lt;code&gt;oh my zsh&lt;/code&gt;下毒了&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt update &amp;amp;&amp;amp; sudo apt upgrade -y
sudo apt install zsh git curl -y
chsh -s /bin/zsh # 切换默认终端为zsh

sh -c &quot;$(curl -fsSL https://install.ohmyz.sh/)&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意如果之前使用&lt;code&gt;bash&lt;/code&gt;一段时间,记得迁移环境变量&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 查看bash配置文件，并手动复制自定义配置
cat ~/.bashrc
# 编辑zsh配置文件，并粘贴自定义配置
nano ~/.zshrc
source ~/.zshrc
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;配置主题我只用&lt;code&gt;powerlevel10k&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git clone --depth=1 https://github.com/romkatv/powerlevel10k.git ${ZSH_CUSTOM:-$HOME/.oh-my-zsh/custom}/themes/powerlevel10k
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后在 &lt;code&gt;~/.zshrc&lt;/code&gt; 设置 &lt;code&gt;ZSH_THEME=&quot;powerlevel10k/powerlevel10k&quot;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;插件的话我只启用三个&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
git clone https://github.com/zsh-users/zsh-autosuggestions ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-autosuggestions

git clone https://github.com/zsh-users/zsh-syntax-highlighting.git ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-syntax-highlighting

sudo nano ~/.zshrc
# plugins=(git zsh-autosuggestions zsh-syntax-highlighting)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;a href=&quot;https://docs.brew.sh/Homebrew-on-Linux&quot;&gt;brew&lt;/a&gt;&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;export HOMEBREW_BREW_GIT_REMOTE=&quot;https://mirrors.tuna.tsinghua.edu.cn/git/homebrew/brew.git&quot;

git clone --depth=1 https://mirrors.tuna.tsinghua.edu.cn/git/homebrew/install.git brew-install
/bin/bash brew-install/install.sh
rm -rf brew-install
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>PostgREST 入门与实践</title><link>https://bangwu.me/posts/postgrest/</link><guid isPermaLink="true">https://bangwu.me/posts/postgrest/</guid><description>用 PostgREST 为 PostgreSQL 自动生成 REST API 的部署、认证与查询技巧</description><pubDate>Thu, 29 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;PostgREST 是什么&lt;/h2&gt;
&lt;p&gt;PostgREST 能把 PostgreSQL 数据库直接封装成符合 RESTful 规范的 API 服务器。你无需再写一层 Node/Go 服务，只要约定好角色权限、视图和函数，它会自动完成：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;按照表/视图结构生成标准的 CRUD 接口；&lt;/li&gt;
&lt;li&gt;基于 SQL 权限模型实现细粒度的访问控制；&lt;/li&gt;
&lt;li&gt;支持过滤、排序、分页、多表关联等查询；&lt;/li&gt;
&lt;li&gt;集成 JWT 鉴权用于多租户或行级安全（RLS）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这篇笔记介绍如何快速搭建、配置 JWT、梳理常用查询操作符，以及一些上线经验。&lt;/p&gt;
&lt;h2&gt;Docker Compose 快速启动&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;version: &apos;3&apos;
services:
  db:
    image: postgres:16
    environment:
      POSTGRES_DB: appdb
      POSTGRES_USER: admin
      POSTGRES_PASSWORD: change_me
    volumes:
      - ./data:/var/lib/postgresql/data
    ports:
      - &quot;5432:5432&quot;

  postgrest:
    image: postgrest/postgrest:v12.0.2
    depends_on:
      - db
    ports:
      - &quot;3000:3000&quot;
    environment:
      PGRST_DB_URI: postgres://admin:change_me@db:5432/appdb
      PGRST_DB_SCHEMAS: api
      PGRST_DB_ANON_ROLE: anon
      PGRST_OPENAPI_SERVER_PROXY_URI: http://127.0.0.1:3000
      PGRST_JWT_SECRET: my_super_secret_32_bytes_key
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;关键环境变量说明：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;PGRST_DB_URI&lt;/code&gt;：连接字符串，至少使用具备 &lt;code&gt;USAGE&lt;/code&gt; 权限的用户。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PGRST_DB_SCHEMAS&lt;/code&gt;：暴露给 API 的 schema，建议单独建 &lt;code&gt;api&lt;/code&gt; schema 放视图和函数。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PGRST_DB_ANON_ROLE&lt;/code&gt;：匿名访问使用的 PostgreSQL role。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PGRST_JWT_SECRET&lt;/code&gt;：JWT 共享密钥，生产环境请通过 &lt;code&gt;openssl rand -base64 32&lt;/code&gt; 生成并写入 &lt;code&gt;.env&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
如果部署在反向代理之后，需要把 &lt;code&gt;PGRST_SERVER_PROXY_URI&lt;/code&gt; 或 &lt;code&gt;PGRST_OPENAPI_SERVER_PROXY_URI&lt;/code&gt; 指向外部访问域名，确保 OpenAPI 文档和重定向正确。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;角色与权限模型&lt;/h2&gt;
&lt;p&gt;PostgREST 完全依赖 PostgreSQL 的角色/权限体系。推荐的最小结构如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-- 匿名角色（仅能 GET）
create role anon nologin;
grant usage on schema api to anon;
grant select on all tables in schema api to anon;

-- 受信任角色（可以写）
create role webuser nologin;
grant usage on schema api to webuser;
grant select, insert, update, delete on all tables in schema api to webuser;

-- 将数据库登录用户映射到这些角色
create role admin login password &apos;change_me&apos;;
grant anon, webuser to admin;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;匿名请求默认使用 &lt;code&gt;PGRST_DB_ANON_ROLE&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;带 JWT 的请求如果包含 &lt;code&gt;role&lt;/code&gt; 字段，PostgREST 会尝试切换到目标角色（需确保 token 里的角色是登录角色的成员）。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
PostgreSQL 默认不会自动把权限应用到新表/视图。可使用 &lt;code&gt;ALTER DEFAULT PRIVILEGES&lt;/code&gt; 或数据库迁移工具（如 Sqitch、Flyway）统一维护。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;准备 API Schema&lt;/h2&gt;
&lt;p&gt;在业务 schema（例如 &lt;code&gt;public&lt;/code&gt;）里定义真实表，再在 &lt;code&gt;api&lt;/code&gt; schema 中创建视图/函数对外暴露。这样可以隐藏内部字段，并内置联表逻辑。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;create schema if not exists api;

create table public.todos (
  id serial primary key,
  user_id int not null,
  title text not null,
  done boolean default false,
  inserted_at timestamptz default now()
);

create view api.todos as
  select id, title, done, inserted_at
  from public.todos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;重载 &lt;code&gt;/todos&lt;/code&gt; 接口即可拿到 &lt;code&gt;api.todos&lt;/code&gt; 视图结果，&lt;code&gt;user_id&lt;/code&gt; 字段不会被暴露。&lt;/p&gt;
&lt;h2&gt;启用行级安全（RLS）&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;alter table public.todos enable row level security;

create policy select_own_todos on public.todos
  for select
  using (current_setting(&apos;request.jwt.claim.user_id&apos;, true)::int = user_id);

create policy write_own_todos on public.todos
  for all
  using (current_setting(&apos;request.jwt.claim.user_id&apos;, true)::int = user_id)
  with check (current_setting(&apos;request.jwt.claim.user_id&apos;, true)::int = user_id);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当请求携带的 JWT 包含 &lt;code&gt;user_id&lt;/code&gt; claim 时，PostgREST 会写入 &lt;code&gt;request.jwt.claim.user_id&lt;/code&gt; 这个 GUC 变量，策略自然就生效。&lt;/p&gt;
&lt;h2&gt;生成 JWT&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;openssl rand -base64 32
# 复制结果写入 PGRST_JWT_SECRET
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;示例 payload：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;role&quot;: &quot;webuser&quot;,
  &quot;user_id&quot;: 42
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;将 payload 使用 &lt;code&gt;HS256&lt;/code&gt; 与密钥签名后即可调用受保护接口。可以用 &lt;a href=&quot;https://jwt.io/#debugger-io&quot;&gt;jwt.io&lt;/a&gt; 生成并调试。&lt;/p&gt;
&lt;p&gt;请求示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl https://api.example.com/todos \
  -H &quot;Authorization: Bearer $TOKEN&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;查询语法速查&lt;/h2&gt;
&lt;p&gt;PostgREST 通过 URL 查询参数映射 SQL 过滤器。核心操作符示例：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;操作&lt;/th&gt;
&lt;th&gt;语法&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;等于&lt;/td&gt;
&lt;td&gt;&lt;code&gt;?done=eq.true&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;eq&lt;/code&gt; 表示 &lt;code&gt;=&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;模糊&lt;/td&gt;
&lt;td&gt;&lt;code&gt;?title=ilike.*bug*&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ilike&lt;/code&gt; 支持大小写不敏感&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;范围&lt;/td&gt;
&lt;td&gt;&lt;code&gt;?inserted_at=gte.2025-01-01&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;gte/gt/lte/lt&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;IN&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;?id=in.(1,2,3)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;逗号分隔&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;嵌套 &lt;code&gt;OR&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;?or=(done.eq.true,title.ilike.*urgent*)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;记得 URL encode&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JSONB&lt;/td&gt;
&lt;td&gt;&lt;code&gt;?meta-&amp;gt;&amp;gt;status=eq.active&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;支持 &lt;code&gt;-&amp;gt;&lt;/code&gt;、&lt;code&gt;-&amp;gt;&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;分页与排序&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;排序：&lt;code&gt;?order=inserted_at.desc&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;限制：&lt;code&gt;?limit=20&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;游标翻页：使用 &lt;code&gt;Range&lt;/code&gt; 头或 &lt;code&gt;?offset=20&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;服务端会在响应头给出 &lt;code&gt;Content-Range&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;多表嵌套&lt;/h3&gt;
&lt;p&gt;PostgREST 支持通过外键自动嵌套资源：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;alter table public.todos
  add constraint todos_user_fk foreign key (user_id) references public.users(id);

create view api.users as
  select id, email from public.users;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;请求 &lt;code&gt;GET /users?select=id,email,todos(id,title)&lt;/code&gt; 会自动返回嵌套 JSON。&lt;/p&gt;
&lt;h3&gt;调用存储过程&lt;/h3&gt;
&lt;p&gt;PostgREST 将 SQL 函数映射为 RPC 接口，例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;create function api.complete_todo(_id int)
returns public.todos as $$
  update public.todos
     set done = true
   where id = _id
   returning *;
$$ language sql security definer;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;调用：&lt;code&gt;POST /rpc/complete_todo&lt;/code&gt;，请求体为 &lt;code&gt;{&quot;_id&quot;: 1}&lt;/code&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!CAUTION]
函数默认运行在调用者角色下。若需提升权限，请使用 &lt;code&gt;security definer&lt;/code&gt; 并仔细设定 &lt;code&gt;search_path&lt;/code&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;常见排错&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;接口 401/403&lt;/strong&gt;：确认 JWT 是否包含 &lt;code&gt;role&lt;/code&gt;，且该角色被登录用户授予；检查 RLS 是否放行。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;新建表无法访问&lt;/strong&gt;：忘记给 &lt;code&gt;api&lt;/code&gt; schema 或新表授权，或视图没同步。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;OpenAPI 缺字段&lt;/strong&gt;：必须在 &lt;code&gt;api&lt;/code&gt; schema 下暴露视图，或在函数设置 &lt;code&gt;stable&lt;/code&gt; 属性。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CORS 问题&lt;/strong&gt;：配置反向代理添加 CORS 头，或使用 &lt;code&gt;PGRST_SERVER_PROXY_URI&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;生产实践建议&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;使用迁移脚本统一管理角色、策略和视图，避免手工操作。&lt;/li&gt;
&lt;li&gt;为 &lt;code&gt;postgrest&lt;/code&gt; 服务加上 &lt;code&gt;healthcheck&lt;/code&gt;，配合 Nginx/Traefik 自动探活。&lt;/li&gt;
&lt;li&gt;结合 &lt;code&gt;pg_stat_statements&lt;/code&gt; 分析慢查询，必要时创建索引或重写视图。&lt;/li&gt;
&lt;li&gt;对外暴露前务必在 staging 环境跑一轮权限测试，防止数据泄露。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;参考资料&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://postgrest.org/en/stable/&quot;&gt;官方文档&lt;/a&gt;：涵盖全部配置项与示例。&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://postgrest.org/en/stable/references/api/reading.html&quot;&gt;Query Cheatsheet&lt;/a&gt;：查询参数详解。&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.postgresql.org/docs/current/ddl-rowsecurity.html&quot;&gt;Row Level Security 指南&lt;/a&gt;：RLS 策略写法。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Cloudflare Workers 开发者指南</title><link>https://bangwu.me/posts/cloudflare-worker/</link><guid isPermaLink="true">https://bangwu.me/posts/cloudflare-worker/</guid><description>从零开始构建、调试、部署 Cloudflare Workers，并结合官方最佳实践整理常见场景与工具链</description><pubDate>Sat, 24 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Workers 是什么&lt;/h2&gt;
&lt;p&gt;Cloudflare Workers 是运行在 Cloudflare 全球 310+ 边缘节点上的无服务器计算平台。它兼容 Web 标准（Fetch API、Service Worker 语法），支持亚毫秒级冷启动，非常适合：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;前端 BFF、API 网关、A/B 测试；&lt;/li&gt;
&lt;li&gt;请求重写、缓存、鉴权；&lt;/li&gt;
&lt;li&gt;静态/动态渲染（SSR）、后台任务（Cron Triggers、Queues）；&lt;/li&gt;
&lt;li&gt;结合 KV、D1、R2 等产品构建完整的全栈应用。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Workers 采用“事件驱动 + Module Worker”模式，开发者只需导出 &lt;code&gt;fetch&lt;/code&gt;/&lt;code&gt;scheduled&lt;/code&gt;/&lt;code&gt;queue&lt;/code&gt; 等 handler 即可。&lt;/p&gt;
&lt;h2&gt;基础架构概览&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Worker&lt;/strong&gt;：核心运行单元，处理 HTTP、计划任务、队列消息。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;KV&lt;/strong&gt;：全球分发的键值存储，读多写少场景。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Durable Objects&lt;/strong&gt;：具备状态的一致性 Actor，用于实时协作、计数、聊天室。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;R2&lt;/strong&gt;：兼容 S3 的对象存储，免出口费。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;D1&lt;/strong&gt;：托管 SQLite 数据库。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Queues / Cron Triggers&lt;/strong&gt;：后台任务、消息队列。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Zero Trust / Turnstile&lt;/strong&gt;：安全防护、验证码。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Workers 原生支持 TypeScript、ES Modules、npm 包（要求 ESM/边缘兼容），生态工具以 &lt;a href=&quot;https://developers.cloudflare.com/workers/wrangler/&quot;&gt;Wrangler&lt;/a&gt; CLI 为中心。&lt;/p&gt;
&lt;h2&gt;准备开发环境&lt;/h2&gt;
&lt;h3&gt;安装 Wrangler&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;npm install -g wrangler
# 或者使用 pnpm/yarn：pnpm add -g wrangler
wrangler login # 打开浏览器授权
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;登陆后 Wrangler 会保存 API Token 到 &lt;code&gt;~/.wrangler/config/default.toml&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;初始化项目&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;npm create cloudflare@latest my-worker
cd my-worker
npm install
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;CLI 会询问运行时（JavaScript/TypeScript）、模板（Hello World、Hono、Queues 等）、是否开启测试框架 Vitest。生成的目录结构通常包含：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;src/index.ts&lt;/code&gt;：Worker 入口（Module Worker）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;wrangler.toml&lt;/code&gt;：项目配置。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;package.json&lt;/code&gt;：依赖与脚本。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tsconfig.json&lt;/code&gt;：TS 编译配置。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;编写你的第一个 Worker&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;src/index.ts&lt;/code&gt; 示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export default {
  async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise&amp;lt;Response&amp;gt; {
    const { pathname } = new URL(request.url);

    if (pathname === &apos;/hello&apos;) {
      return new Response(JSON.stringify({ message: &apos;Hello from Workers&apos; }), {
        headers: { &apos;Content-Type&apos;: &apos;application/json&apos; },
      });
    }

    return new Response(&apos;Not Found&apos;, { status: 404 });
  },
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Env&lt;/code&gt; 接口代表 &lt;code&gt;wrangler.toml&lt;/code&gt; 中 &lt;code&gt;bindings&lt;/code&gt;（KV、R2、Secrets 等），可通过 &lt;code&gt;types&lt;/code&gt; 字段指定。&lt;/p&gt;
&lt;h2&gt;wrangler.toml 配置&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;name = &quot;my-worker&quot;
main = &quot;src/index.ts&quot;
compatibility_date = &quot;2024-11-01&quot;
compatibility_flags = [&quot;web_socket_compression&quot;]
logpush = true

[vars]
API_BASE = &quot;https://api.example.com&quot;

[[kv_namespaces]]
binding = &quot;CACHE&quot;
id = &quot;xxxxxx...&quot;

[[d1_databases]]
binding = &quot;DB&quot;
database_name = &quot;my-db&quot;
database_id = &quot;yyyyyy...&quot;

[triggers]
crons = [&quot;0 */1 * * *&quot;]  # 每小时运行
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意事项：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;compatibility_date&lt;/code&gt; 决定运行时 API 行为，建议定期升级。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;vars&lt;/code&gt; 会作为环境变量注入 &lt;code&gt;env&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;绑定（binding）名称是代码里访问的名称。&lt;/li&gt;
&lt;li&gt;对于 &lt;code&gt;development&lt;/code&gt; 环境可使用 &lt;code&gt;[env.dev]&lt;/code&gt; 覆盖配置，实现多环境部署。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;本地开发与调试&lt;/h2&gt;
&lt;h3&gt;&lt;code&gt;wrangler dev&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;wrangler dev
# 或指定端口 / 远程模式
wrangler dev --port 8787 --remote
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;默认使用 Miniflare 在本地模拟，支持热重载。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--remote&lt;/code&gt; 与 &lt;code&gt;--live-reload&lt;/code&gt; 会将请求发送到最近的 Cloudflare 边缘节点，适合测试与第三方服务交互。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--inspect&lt;/code&gt; 可开启 Chrome DevTools 调试。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;请求测试&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;curl http://127.0.0.1:8787/hello
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;本地环境无法访问生产 KV/R2，Wrangler 会提供模拟器。若需要真实数据，可在 &lt;code&gt;wrangler dev --remote&lt;/code&gt; 时指定 &lt;code&gt;--persist&lt;/code&gt; 或在 &lt;code&gt;wrangler.toml&lt;/code&gt; 中设置 &lt;code&gt;kv_namespaces.preview_id&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;日志查看&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;wrangler tail
# 或者过滤
wrangler tail --filters status=500
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;wrangler tail&lt;/code&gt; streaming 实时日志（默认 5 分钟），亦可导出到 Logpush（存储到 R2、Splunk、Datadog 等）。&lt;/p&gt;
&lt;h2&gt;部署&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;wrangler deploy
# 指定环境
wrangler deploy --env production
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;成功后 CLI 会返回访问 URL，以及 &lt;code&gt;Worker KV&lt;/code&gt; 等绑定结果。生产日志可通过 Cloudflare Dashboard &amp;gt; Workers &amp;gt; Your Worker &amp;gt; Logs 查看。&lt;/p&gt;
&lt;h2&gt;绑定 Cloudflare 产品&lt;/h2&gt;
&lt;h3&gt;KV（键值存储）&lt;/h3&gt;
&lt;p&gt;在 Dashboard 创建 KV namespace，复制 &lt;code&gt;Namespace ID&lt;/code&gt; 填入 &lt;code&gt;wrangler.toml&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[[kv_namespaces]]
binding = &quot;CACHE&quot;
id = &quot;xxxxxxxx&quot;
preview_id = &quot;yyyyyyyy&quot; # 可选，用于 dev 环境
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;await env.CACHE.put(&apos;key&apos;, &apos;value&apos;, { expirationTtl: 3600 });
const result = await env.CACHE.get(&apos;key&apos;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;KV 写入最终一致、读多写少；写操作可一次性 &lt;code&gt;put&lt;/code&gt;/&lt;code&gt;delete&lt;/code&gt;，不要当事务数据库使用。&lt;/p&gt;
&lt;h3&gt;Durable Objects&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;[[durable_objects.bindings]]
name = &quot;ROOM&quot;
class_name = &quot;RoomDO&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;export class RoomDO {
  state: DurableObjectState;

  constructor(state: DurableObjectState, env: Env) {
    this.state = state;
  }

  async fetch(request: Request) {
    const messages = (await this.state.storage.get&amp;lt;string[]&amp;gt;(&apos;messages&apos;)) ?? [];
    return new Response(JSON.stringify({ messages }), {
      headers: { &apos;Content-Type&apos;: &apos;application/json&apos; },
    });
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 Worker 中使用：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const id = env.ROOM.idFromName(&apos;general&apos;);
const stub = env.ROOM.get(id);
const res = await stub.fetch(request);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;D1（数据库）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;[[d1_databases]]
binding = &quot;DB&quot;
database_name = &quot;app-db&quot;
database_id = &quot;abc123&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;const { results } = await env.DB.prepare(&apos;SELECT * FROM todos WHERE done = ?&apos;).bind(false).all();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;配合 &lt;code&gt;drizzle-orm&lt;/code&gt;, &lt;code&gt;kysely&lt;/code&gt; 等 ORM 使用时，需确保运行时兼容（一般使用 drizzle-orm/d1 driver）。&lt;/p&gt;
&lt;h3&gt;R2（对象存储）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;[[r2_buckets]]
binding = &quot;ASSETS&quot;
bucket_name = &quot;my-assets&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;await env.ASSETS.put(&apos;images/logo.png&apos;, fileReadableStream);
const object = await env.ASSETS.get(&apos;images/logo.png&apos;);
return new Response(object?.body, { headers: object?.httpMetadata });
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Queues&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;[[queues.producers]]
queue = &quot;task-queue&quot;
binding = &quot;TASK_QUEUE&quot;

[[queues.consumers]]
queue = &quot;task-queue&quot;
max_batch_size = 1
max_batch_timeout = 30
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Producer：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;await env.TASK_QUEUE.send({ type: &apos;email&apos;, payload: { to: &apos;hi@example.com&apos; } });
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Consumer：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export default {
  async queue(batch: MessageBatch&amp;lt;any&amp;gt;, env: Env, ctx: ExecutionContext) {
    for (const msg of batch.messages) {
      // 处理任务
      msg.ack();
    }
  },
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Secrets 与环境变量&lt;/h2&gt;
&lt;h3&gt;普通变量&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;wrangler.toml&lt;/code&gt; 中 &lt;code&gt;[vars]&lt;/code&gt; 会以字符串注入。适合非敏感配置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[vars]
API_BASE = &quot;https://api.example.com&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码中：&lt;code&gt;env.API_BASE&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;Secrets&lt;/h3&gt;
&lt;p&gt;敏感信息使用 &lt;code&gt;wrangler secret put&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;wrangler secret put GITHUB_TOKEN
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Secrets 默认仅存在于特定环境，请分别为 preview/production 设置。删除：&lt;code&gt;wrangler secret delete&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;多环境管理&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;[env.preview]
vars = { API_BASE = &quot;https://staging.example.com&quot; }

[env.production]
vars = { API_BASE = &quot;https://api.example.com&quot; }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;部署时：&lt;code&gt;wrangler deploy --env production&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;常见事件与示例&lt;/h2&gt;
&lt;h3&gt;HTTP Fetch&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;export default {
  async fetch(request: Request, env: Env, ctx: ExecutionContext) {
    ctx.waitUntil(env.ANALYTICS.write({ path: new URL(request.url).pathname }));
    return new Response(&apos;OK&apos;);
  },
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Cron Trigger&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;export default {
  async scheduled(event: ScheduledEvent, env: Env, ctx: ExecutionContext) {
    await env.REPORT_QUEUE.send({ at: event.scheduledTime });
  },
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 Dashboard 或 &lt;code&gt;wrangler.toml&lt;/code&gt; 的 &lt;code&gt;[triggers]&lt;/code&gt; 中配置 Cron 表达式。&lt;/p&gt;
&lt;h3&gt;WebSocket / SSE&lt;/h3&gt;
&lt;p&gt;Workers 支持 &lt;code&gt;WebSocketPair&lt;/code&gt; 与 &lt;code&gt;ReadableStream&lt;/code&gt;，使用 &lt;code&gt;c.fwd&lt;/code&gt; 或 Hono/itty-router 处理更方便。注意 &lt;code&gt;compatibility_flags&lt;/code&gt; 需要开启 &lt;code&gt;web_socket_compression&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;与框架结合&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Hono&lt;/strong&gt;：轻量 TypeScript 框架，&lt;code&gt;npm create hono@latest -- --cloudflare-workers&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Astro SSR&lt;/strong&gt;：通过 &lt;code&gt;@astrojs/cloudflare&lt;/code&gt; adapter。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Next.js Middleware / Pages&lt;/strong&gt;：Workers 可作为自定义服务器或中间层。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Remix&lt;/strong&gt;：官方支持 &lt;code&gt;remix-cloudflare&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;示例（Hono）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { Hono } from &apos;hono&apos;;
import { zValidator } from &apos;@hono/zod-validator&apos;;
import { z } from &apos;zod&apos;;

type Bindings = {
  DB: D1Database;
};

const app = new Hono&amp;lt;{ Bindings: Bindings }&amp;gt;();

app.get(&apos;/todos&apos;, async (c) =&amp;gt; {
  const { results } = await c.env.DB.prepare(&apos;SELECT * FROM todos&apos;).all();
  return c.json(results);
});

app.post(
  &apos;/todos&apos;,
  zValidator(&apos;json&apos;, z.object({ title: z.string().min(1) })),
  async (c) =&amp;gt; {
    const { title } = c.req.valid(&apos;json&apos;);
    await c.env.DB.prepare(&apos;INSERT INTO todos (title) VALUES (?)&apos;).bind(title).run();
    return c.json({ message: &apos;created&apos; }, 201);
  }
);

export default app;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;调试与 Observability&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;wrangler tail&lt;/code&gt;：实时日志。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;wrangler dev --inspect&lt;/code&gt;：Chrome DevTools。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Workers Metrics&lt;/strong&gt;：Dashboard 提供请求量、CPU 时间、错误率。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Workers Trace Events&lt;/strong&gt;：可启用 HTTP 请求追踪（Beta）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Logpush&lt;/strong&gt;：将日志推送到 R2、S3、Datadog。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Workers Analytics Engine&lt;/strong&gt;：结构化日志，可通过 SQL 查询。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;推荐在代码中引入请求 ID：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const requestId = crypto.randomUUID();
console.log(&apos;request start&apos;, requestId);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;并统一使用 &lt;code&gt;ctx.waitUntil&lt;/code&gt; 上报异步日志。&lt;/p&gt;
&lt;h2&gt;性能优化建议&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;保持函数纯粹，避免阻塞操作；长任务使用 Queues。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;复用 &lt;code&gt;fetch&lt;/code&gt; 请求：避免多次调用同一 API，利用 &lt;code&gt;Promise.all&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;合理使用 &lt;code&gt;Cache API&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const cache = caches.default;
const cacheKey = new Request(request.url, request);
let response = await cache.match(cacheKey);
if (!response) {
  response = await fetch(request);
  ctx.waitUntil(cache.put(cacheKey, response.clone()));
}
return response;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;静态资源放 R2 + CDN，Workers 做鉴权与签名。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果依赖第三方 npm 包，确认其兼容 Web 平台（无 Node-specific API）。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;安全与合规&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;使用 &lt;code&gt;wrangler secret&lt;/code&gt; 管理敏感信息。&lt;/li&gt;
&lt;li&gt;与 Cloudflare Zero Trust 集成，限制访问来源。&lt;/li&gt;
&lt;li&gt;使用 &lt;a href=&quot;https://developers.cloudflare.com/turnstile/&quot;&gt;Turnstile&lt;/a&gt; 抵御机器人。&lt;/li&gt;
&lt;li&gt;需要 CORS 时手动设置 &lt;code&gt;Access-Control-Allow-Origin&lt;/code&gt;，并关注 &lt;code&gt;OPTIONS&lt;/code&gt; 预检。&lt;/li&gt;
&lt;li&gt;对于用户数据，结合 DLP/日志脱敏策略。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;常见问题排查&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;问题&lt;/th&gt;
&lt;th&gt;排查思路&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;522/525 错误&lt;/td&gt;
&lt;td&gt;检查 Worker 超时（CPU 时间超过限制）、上游服务不可达&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;本地 dev 行为与生产不一致&lt;/td&gt;
&lt;td&gt;使用 &lt;code&gt;wrangler dev --remote&lt;/code&gt;，或确认 &lt;code&gt;compatibility_date&lt;/code&gt; 一致&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;未找到绑定&lt;/td&gt;
&lt;td&gt;确认 &lt;code&gt;wrangler.toml&lt;/code&gt; 中 binding 名称与代码一致，部署后刷新 Dashboard&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;npm 包不兼容&lt;/td&gt;
&lt;td&gt;优先选用 ESM、无 Node API 的库；或通过 &lt;code&gt;workerd&lt;/code&gt; polyfill&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Request body 重复读取失败&lt;/td&gt;
&lt;td&gt;使用 &lt;code&gt;request.clone()&lt;/code&gt; 复用流&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cron 不触发&lt;/td&gt;
&lt;td&gt;确认 &lt;code&gt;wrangler.toml&lt;/code&gt; 中 &lt;code&gt;triggers.crons&lt;/code&gt; 已配置，且 Worker 部署在支持 Cron 的计划（Free 计划每天 3 次）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;发布流程建议&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;使用 GitHub Actions / Cloudflare Pages 集成自动部署。&lt;/li&gt;
&lt;li&gt;PR 阶段运行 &lt;code&gt;wrangler deploy --dry-run&lt;/code&gt; 快速验证编译。&lt;/li&gt;
&lt;li&gt;使用 &lt;code&gt;wrangler deploy --env staging&lt;/code&gt; 发布到预发，结合 Zero Trust 限制访问。&lt;/li&gt;
&lt;li&gt;通过 &lt;code&gt;wrangler tail&lt;/code&gt; 或 Logpush 观察 200/500 比例。&lt;/li&gt;
&lt;li&gt;升级 &lt;code&gt;compatibility_date&lt;/code&gt; 前在 staging 测试，确保向后兼容。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;进一步阅读&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://developers.cloudflare.com/workers/&quot;&gt;Cloudflare Workers 官方文档&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developers.cloudflare.com/workers/wrangler/&quot;&gt;wrangler CLI 指南&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/cloudflare/workers-sdk/tree/main/templates&quot;&gt;Workers Templates&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developers.cloudflare.com/durable-objects/&quot;&gt;Durable Objects Cookbook&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developers.cloudflare.com/queues/&quot;&gt;Cloudflare Queues&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developers.cloudflare.com/workers/platform/observability/&quot;&gt;Workers Observability&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/cloudflare/workerd&quot;&gt;Workerd Runtime 源码&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;掌握以上内容后，可以快速利用 Cloudflare Workers 构建全球加速、成本可控的边缘应用，从个人项目到企业级服务都能覆盖。基于模块化、标准化的 API，开发体验与浏览器端极为相似，入门门槛远低于传统 Serverless 平台。&lt;/p&gt;
</content:encoded></item><item><title>Hello, Hono</title><link>https://bangwu.me/posts/honojs/</link><guid isPermaLink="true">https://bangwu.me/posts/honojs/</guid><description>Hono 是一个极轻量的 JavaScript 后端框架，特别适合写微服务 API。这篇从安装到路由到中间件一步步走了一遍。</description><pubDate>Sat, 24 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;为什么选择 Hono.js&lt;/h2&gt;
&lt;p&gt;Hono 起初是为 Cloudflare Workers 设计的超轻量 Web 框架，现在已支持 Deno、Bun、Node.js、Vercel Edge、Lagon 等多种运行时。它兼具以下特性：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;快速&lt;/strong&gt;：核心仅 ~15KB，响应链路短，天然适合边缘侧（Edge）与无服务器函数。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;兼容 Fetch 标准&lt;/strong&gt;：上下文就是 &lt;code&gt;Request&lt;/code&gt;/&lt;code&gt;Response&lt;/code&gt;，易于在不同运行时迁移。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;TypeScript 友好&lt;/strong&gt;：严格的类型推导，加上 &lt;code&gt;hc&lt;/code&gt;/&lt;code&gt;zod&lt;/code&gt; 等生态可自动生成客户端与 OpenAPI。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;中间件生态&lt;/strong&gt;：内置 CORS、Basic Auth、JWT、Validator 等工具，也能继承 Express 风格中间件。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;本文整理 Hono 在实际项目中的最佳实践，帮助你搭建稳定、可维护的微服务 API。&lt;/p&gt;
&lt;h2&gt;项目脚手架&lt;/h2&gt;
&lt;p&gt;推荐使用官方 CLI：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm create hono@latest my-hono-app
cd my-hono-app
npm install
npm run dev
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;生成的项目默认使用 TypeScript 与 ESLint，可根据运行时切换 &lt;code&gt;adapter&lt;/code&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;cloudflare-workers&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;bun&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;deno&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;node-server&lt;/code&gt;（Node.js 运行，配合 Fastly Compute、Lambda 等）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;配置建议：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在 &lt;code&gt;tsconfig.json&lt;/code&gt; 开启 &lt;code&gt;strict&lt;/code&gt;, &lt;code&gt;noUncheckedIndexedAccess&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;配置 &lt;code&gt;eslint-config-hono&lt;/code&gt; 与 &lt;code&gt;typescript-eslint&lt;/code&gt; 保证一致性。&lt;/li&gt;
&lt;li&gt;使用 &lt;code&gt;@hono/zod-validator&lt;/code&gt; 做输入校验。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;路由与模块化&lt;/h2&gt;
&lt;p&gt;Hono 按照路径注册 handler，建议按业务拆分：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { Hono } from &apos;hono&apos;;
import { userRouter } from &apos;./routes/users&apos;;
import { authRouter } from &apos;./routes/auth&apos;;

const app = new Hono();

app.route(&apos;/auth&apos;, authRouter);
app.route(&apos;/users&apos;, userRouter);

export default app;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;routes/users.ts&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { Hono } from &apos;hono&apos;;
import { zValidator } from &apos;@hono/zod-validator&apos;;
import { z } from &apos;zod&apos;;

export const userRouter = new Hono()
  .get(
    &apos;/&apos;,
    zValidator(&apos;query&apos;, z.object({ page: z.coerce.number().int().min(1).default(1) })),
    async (c) =&amp;gt; {
      const { page } = c.req.valid(&apos;query&apos;);
      const result = await c.get(&apos;services&apos;).user.list({ page });
      return c.json(result);
    }
  )
  .post(
    &apos;/&apos;,
    zValidator(&apos;json&apos;, z.object({ email: z.string().email(), password: z.string().min(8) })),
    async (c) =&amp;gt; {
      const payload = c.req.valid(&apos;json&apos;);
      const user = await c.get(&apos;services&apos;).user.create(payload);
      return c.json(user, 201);
    }
  );
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;要点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;尽量在路由文件中保持“瘦控制器”，把业务逻辑下沉到 service/domain 层。&lt;/li&gt;
&lt;li&gt;通过 &lt;code&gt;c.get()&lt;/code&gt;/&lt;code&gt;c.set()&lt;/code&gt; 将服务或依赖注入 &lt;code&gt;Context&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;利用 &lt;code&gt;zod&lt;/code&gt; 校验 + 类型推导避免重复声明类型。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;中间件与上下文&lt;/h2&gt;
&lt;p&gt;常用中间件示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { logger } from &apos;hono/logger&apos;;
import { cors } from &apos;hono/cors&apos;;
import { poweredBy } from &apos;hono/powered-by&apos;;
import { secureHeaders } from &apos;hono/secure-headers&apos;;

app.use(&apos;*&apos;, poweredBy());
app.use(&apos;*&apos;, logger());
app.use(&apos;*&apos;, secureHeaders());
app.use(
  &apos;/api/*&apos;,
  cors({
    origin: [&apos;https://app.example.com&apos;],
    allowMethods: [&apos;GET&apos;, &apos;POST&apos;, &apos;PATCH&apos;, &apos;DELETE&apos;],
    allowHeaders: [&apos;Authorization&apos;, &apos;Content-Type&apos;],
  })
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;依赖注入：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import type { Services } from &apos;./services&apos;;

type Bindings = {
  SERVICES: Services;
};

const app = new Hono&amp;lt;{ Bindings: Bindings }&amp;gt;();

app.use(&apos;*&apos;, async (c, next) =&amp;gt; {
  c.set(&apos;services&apos;, c.env.SERVICES);
  await next();
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;执行顺序遵循注册顺序，注意在 Edge 环境中避免阻塞型操作（例如 &lt;code&gt;crypto.randomUUID&lt;/code&gt; 可替代 &lt;code&gt;uuid&lt;/code&gt; 包）。&lt;/p&gt;
&lt;h2&gt;错误处理&lt;/h2&gt;
&lt;p&gt;统一错误处理能提升 DX：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;app.onError((err, c) =&amp;gt; {
  c.get(&apos;logger&apos;)?.error(err);

  if (err instanceof HTTPException) {
    return err.getResponse();
  }

  return c.json(
    {
      code: &apos;INTERNAL_ERROR&apos;,
      message: &apos;服务器开小差了&apos;,
    },
    500
  );
});

app.notFound((c) =&amp;gt;
  c.json(
    {
      code: &apos;NOT_FOUND&apos;,
      message: `Route ${c.req.method} ${c.req.path} 不存在`,
    },
    404
  )
);
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;使用 &lt;code&gt;HTTPException&lt;/code&gt; 抛出领域错误，例如 &lt;code&gt;throw new HTTPException(403, { message: &apos;Forbidden&apos; })&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;在日志中记录 &lt;code&gt;requestId&lt;/code&gt;（可从 header 或 &lt;code&gt;crypto.randomUUID()&lt;/code&gt; 生成）。&lt;/li&gt;
&lt;li&gt;Edge 平台通常有 &lt;code&gt;ExecutionContext.waitUntil&lt;/code&gt;，可以结合 &lt;code&gt;app.use&lt;/code&gt; 注入日志/埋点。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;数据访问与性能&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Cloudflare D1、PlanetScale、Supabase 可通过 &lt;code&gt;Context.env&lt;/code&gt; 注入客户端。&lt;/li&gt;
&lt;li&gt;在 Edge 环境使用 &lt;code&gt;fetch&lt;/code&gt; 调用内部服务时，注意复用 &lt;code&gt;Request&lt;/code&gt; 对象或使用 &lt;code&gt;Cache API&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;结合 &lt;code&gt;itty-router-openapi&lt;/code&gt; 或 &lt;code&gt;hono-openapi&lt;/code&gt; 自动生成文档。&lt;/li&gt;
&lt;li&gt;当需要 WebSocket/SSE，可引用 &lt;code&gt;hono/ws&lt;/code&gt; 或 &lt;code&gt;hono/sse&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;缓存策略：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;app.get(&apos;/health&apos;, async (c) =&amp;gt; {
  return c.text(&apos;ok&apos;, 200, {
    &apos;Cache-Control&apos;: &apos;max-age=30&apos;,
  });
});

app.get(&apos;/articles&apos;, cacheMiddleware(), async (c) =&amp;gt; {
  // ...
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 Cloudflare Workers 中可用 &lt;code&gt;c.executionCtx.waitUntil(cache.put(...))&lt;/code&gt; 延迟写缓存。&lt;/p&gt;
&lt;h2&gt;安全最佳实践&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;使用 &lt;code&gt;secureHeaders()&lt;/code&gt; 设置 &lt;code&gt;Content-Security-Policy&lt;/code&gt;, &lt;code&gt;Strict-Transport-Security&lt;/code&gt;, &lt;code&gt;X-Frame-Options&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;JWT 验证使用 &lt;code&gt;hono/jwt&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;app.use(
  &apos;/api/*&apos;,
  jwt({
    secret: c.env.JWT_SECRET,
    cookie: &apos;access_token&apos;,
    algorithm: &apos;HS256&apos;,
  })
);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;对外部输入全部走 &lt;code&gt;zod&lt;/code&gt;/&lt;code&gt;valibot&lt;/code&gt; 校验，防止类型穿透。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;对于文件上传，结合 R2/S3 签名上传 + 临时 URL。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;记录审计日志：可将请求信息写入 Workers Analytics Engine 或第三方服务。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;测试策略&lt;/h2&gt;
&lt;h3&gt;单元测试&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import { describe, it, expect } from &apos;vitest&apos;;
import app from &apos;../src/app&apos;;

describe(&apos;GET /health&apos;, () =&amp;gt; {
  it(&apos;should return ok&apos;, async () =&amp;gt; {
    const res = await app.request(&apos;/health&apos;);
    expect(res.status).toBe(200);
    expect(await res.text()).toBe(&apos;ok&apos;);
  });
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Hono 提供 &lt;code&gt;app.request&lt;/code&gt; 直接在 Node 里模拟请求，无需启动服务器。&lt;/p&gt;
&lt;h3&gt;集成测试&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Cloudflare Workers：使用 &lt;code&gt;@cloudflare/workers-types&lt;/code&gt; 与 &lt;code&gt;miniflare&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;Node 环境：搭配 &lt;code&gt;supertest&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;Edge Function：在部署前使用平台提供的本地模拟器（如 &lt;code&gt;wrangler dev&lt;/code&gt;、&lt;code&gt;vercel dev&lt;/code&gt;）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;部署与观察&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Cloudflare Workers&lt;/strong&gt;：&lt;code&gt;wrangler deploy&lt;/code&gt;；配置 Durable Objects、KV、R2 于 &lt;code&gt;wrangler.toml&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Vercel Edge&lt;/strong&gt;：导出 &lt;code&gt;export default app&lt;/code&gt;，在 &lt;code&gt;vercel.json&lt;/code&gt; 指定 &lt;code&gt;runtime: &quot;edge&quot;&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Node Server&lt;/strong&gt;：使用 &lt;code&gt;@hono/node-server&lt;/code&gt; 或 &lt;code&gt;fastly-hono-adapter&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Docker&lt;/strong&gt;：将 &lt;code&gt;app.fetch&lt;/code&gt; 包装在 &lt;code&gt;server.listen&lt;/code&gt; 中，注意 Node 端口。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;监控与日志：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Cloudflare Logs / Workers Analytics Engine。&lt;/li&gt;
&lt;li&gt;OpenTelemetry：可结合 &lt;code&gt;hono/opentelemetry&lt;/code&gt; 输出 trace。&lt;/li&gt;
&lt;li&gt;使用平台原生指标（Vercel Speed Insights、Deno Deploy Metrics）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;常见陷阱&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Edge 环境没有 Node polyfills，避免依赖 &lt;code&gt;fs&lt;/code&gt;, &lt;code&gt;crypto.randomBytes&lt;/code&gt; 等 Node 特有 API。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;await&lt;/code&gt; 中不要阻塞事件循环：使用 &lt;code&gt;Promise.all&lt;/code&gt; 并行执行。&lt;/li&gt;
&lt;li&gt;注意 &lt;code&gt;Request&lt;/code&gt;/&lt;code&gt;Response&lt;/code&gt; 流只能读取一次；需要复用时调用 &lt;code&gt;req.clone()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;app.fire()&lt;/code&gt; 必须在 &lt;code&gt;fetch&lt;/code&gt; handler 中调用，在 Cloudflare 中通常是 &lt;code&gt;export default app&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;如果在中间件中抛异常，却返回 200，多半是 &lt;code&gt;await next()&lt;/code&gt; 忘写。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;推荐资源&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://hono.dev/&quot;&gt;Hono 官方文档&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/honojs/awesome-hono&quot;&gt;Awesome Hono&lt;/a&gt;：社区收集的中间件、模板。&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developers.cloudflare.com/workers/&quot;&gt;Cloudflare Workers Docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/honojs/examples&quot;&gt;Hono Examples 仓库&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://edge-runtime.vercel.app/&quot;&gt;Edge Runtime Compared&lt;/a&gt;：不同 Edge 运行时对比。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;合理运用以上实践，可以让 Hono 成为构建高性能、易维护 API 的利器，无论是 SaaS 后端、BFF 还是边缘函数都能胜任。&lt;/p&gt;
</content:encoded></item><item><title>数据可视化</title><link>https://bangwu.me/posts/data-visual/</link><guid isPermaLink="true">https://bangwu.me/posts/data-visual/</guid><description>数据可视化的核心不是画图好看，是让人一眼看懂。这篇整理了从图表选择到配色规范到交互设计的实操经验。</description><pubDate>Fri, 18 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;数据可视化&lt;/h2&gt;
&lt;p&gt;数据可视化的目标不是把表格换一层皮，而是帮助读者迅速理解趋势、比较和异常点。一次好的呈现应该在三秒内告诉读者“发生了什么”，在三十秒内解释“为什么会这样”。&lt;/p&gt;
&lt;p&gt;我在&lt;a href=&quot;https://cdn.bangwu.me/assets/visualization-end.pdf&quot;&gt;最后一次作业&lt;/a&gt;里踩过不少坑，也常被同事问起怎么做“有信息量又不难看的图”。这篇文章整理了常用的流程、设计规范和工具清单，方便以后查阅。&lt;/p&gt;
&lt;h3&gt;为什么要做可视化&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;快速建模：可视化能让我们迅速验证假设，发现数据里的结构或异常值。&lt;/li&gt;
&lt;li&gt;聚焦重点：通过合理的视觉编码，把最重要的指标放在第一视觉层级。&lt;/li&gt;
&lt;li&gt;建立共识：图表比 PPT 文本更容易在评审会上达成共识，也是讲述产品/业务故事的骨架。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;从原始数据到图表&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;明确目标受众&lt;/strong&gt;：他们最关心的指标是什么？背景知识到什么程度？输出的交互复杂度取决于这个答案。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;梳理数据结构&lt;/strong&gt;：先在表格里做数据探查（EDA），标记可用维度、需要清洗的字段和时间跨度。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;挑选合适的图表类型&lt;/strong&gt;：定量比较优先考虑柱状/折线，结构占比用堆叠柱或矩形树图，关联关系可尝试散点和网络图。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;草图迭代&lt;/strong&gt;：先画草图或使用 Figma/Excalidraw 快速拼布局，再决定配色、标注和交互节奏。&lt;/li&gt;
&lt;/ol&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;目标&lt;/th&gt;
&lt;th&gt;推荐图表&lt;/th&gt;
&lt;th&gt;适用说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;趋势变化&lt;/td&gt;
&lt;td&gt;折线图、面积图&lt;/td&gt;
&lt;td&gt;时间序列数据，注意纵轴是否从零开始&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;分类比较&lt;/td&gt;
&lt;td&gt;分组柱状图、哑铃图&lt;/td&gt;
&lt;td&gt;同类指标在不同群体之间的差异&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;占比结构&lt;/td&gt;
&lt;td&gt;堆叠柱、矩形树图&lt;/td&gt;
&lt;td&gt;强调局部与整体关系&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;地理分布&lt;/td&gt;
&lt;td&gt;Choropleth、符号地图&lt;/td&gt;
&lt;td&gt;需要地理上下文时优先&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;设计与可读性&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;颜色层级&lt;/strong&gt;：主色用于核心数据，辅色承载比较组，灰度用作背景参考线。遵守品牌色的同时记得通过色盲模拟检查对比度。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;标注策略&lt;/strong&gt;：关键数字直接贴在图上，少用图例让读者来回对照。超出阈值的点用强调箭头或注释解释原因。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;最少装饰&lt;/strong&gt;：去掉无意义的 3D 效果和阴影，保留紧凑的网格线和刻度；文字切换为 Sentence case 或全中文陈述句，提高可读性。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;响应式布局&lt;/strong&gt;：移动端优先保留核心图表，细节可以折叠至“展开更多”里。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;交互与讲故事&lt;/h3&gt;
&lt;p&gt;当图表要讲述一个“故事”时，可以采用 Step-by-step 的结构：先出现整体趋势，再逐步揭示关键节点和原因。常见做法是分章节滚动触发（scrollytelling）或提供标签切换（tab/segmented control）。一定要配合文字说明，为读者提供上下文。&lt;/p&gt;
&lt;h3&gt;常见坑位清单&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;数据颗粒度不对导致“锯齿”或“噪声”：先做平滑处理或改变汇总粒度。&lt;/li&gt;
&lt;li&gt;多尺度指标混在同一个坐标轴：尝试正则化或使用双轴，但要清楚提示量纲不同。&lt;/li&gt;
&lt;li&gt;图表过载：一次只传达一个主结论，其余信息可以放到 Tooltip 或说明文字中。&lt;/li&gt;
&lt;li&gt;忽略可访问性：注意颜色对比、字号、键盘操作路径，让读屏读者也能理解。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;常用工具组合&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;探索性&lt;/strong&gt;：Jupyter Notebook + pandas + seaborn/Plotly，用于快速验证数据特征。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;生产级&lt;/strong&gt;：D3.js 适合高级定制，&lt;a href=&quot;https://d3js.org/getting-started&quot;&gt;D3 官方入门指南&lt;/a&gt;仍然是最佳入口；业务图表可以考虑 ECharts、Vega-Lite 或 AntV G2，能更快落地。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;低代码&lt;/strong&gt;：Superset、Metabase 和 Power BI 适合内部运营面板，强调权限和多数据源接入。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;下方演示片段摘自 D3，展示如何用线性比例尺映射数值到可视化位置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import * as d3 from &apos;d3&apos;;

const scale = d3.scaleLinear()
  .domain([0, 100])
  .range([0, 600]);

d3.select(&apos;svg&apos;)
  .append(&apos;rect&apos;)
  .attr(&apos;x&apos;, 0)
  .attr(&apos;y&apos;, 20)
  .attr(&apos;width&apos;, scale(72))
  .attr(&apos;height&apos;, 24);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;评估与迭代&lt;/h3&gt;
&lt;p&gt;发布前做一次 checklist：文字描述是否与图表一致、不同设备显示是否正常、交互是否有备选操作路径。上线后可以通过埋点或用户访谈收集反馈，根据具体结论再开新的迭代。&lt;/p&gt;
&lt;h3&gt;进一步阅读&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.bilibili.com/video/BV19C4y1F7na&quot;&gt;影视飓风《可视化少林功夫》&lt;/a&gt;：故事性的可视化参考。&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.stephen-few.com/idd.php&quot;&gt;Information Dashboard Design&lt;/a&gt;：信息仪表盘设计经典资料。&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://datavizcatalogue.com&quot;&gt;The Data Visualization Catalogue&lt;/a&gt;：图表类型速查。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>情绪需要稳定还是释放</title><link>https://bangwu.me/posts/sentiment/</link><guid isPermaLink="true">https://bangwu.me/posts/sentiment/</guid><description>情绪稳定被捧得太高了，好像不哭不闹才是成熟的标志。这篇想说的是：释放和稳定不是对立的，平衡才是。</description><pubDate>Wed, 16 Apr 2025 15:45:22 GMT</pubDate><content:encoded>&lt;p&gt;&amp;lt;audio controls data-title=&quot;美梦&quot; data-artist=&quot;周公&quot; data-cover=&quot;https://cdn.bangwu.me/img/20260111w9h9x-9m.webp&quot;&amp;gt;
&amp;lt;source src=&quot;https://cdn.bangwu.me/img/20260111f51d1e8e.mp3&quot; type=&quot;audio/mpeg&quot;&amp;gt;
&amp;lt;/audio&amp;gt;&lt;/p&gt;
&lt;h2&gt;情绪稳定被神化了&lt;/h2&gt;
&lt;p&gt;刷视频看到刘晓燕老师讲affection（n. 感情，影响）这个单词，她说“一定不要相信你要做个情绪稳定的人，你要做个没有情绪的人，而是要离开那个让你情绪不稳定的人和地方”刚好，最近心情不太好（实际这篇文章已经拖很长时间了，最近没心思写🥲），来聊一聊这件事&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.bangwu.me/img/202601110c2b02bb.png&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;释放不是不成熟&lt;/h2&gt;
&lt;p&gt;在生活中，我认为自己的情绪是属于非常稳定的那一种，稳定到什么程度？高考的时候没有任何感觉、就算是喜欢的人离开我，我也只会冷静面对。可能这也不是情绪稳定，大多数事情造成的情感都被我足够多的理性压制了，这就造成了我偶尔做一些感性的事情的时候，都会想很多考虑各种情况，往往能接受最坏的那一个情况我才会去做（接受不了离别，所以我不会选择表白）OK，人人都会产生各种各样的情绪，记得《一人之下》里面纳森岛有个异人，会将人们的各种情绪释放出来，从七窍流出情绪越多越重，重重的压着你让你无法自拔，情绪足够稳定的张楚岚却流出了很多很多情绪……&lt;/p&gt;
&lt;p&gt;兴奋，憎恶，暴躁，快乐这些总是会或多或少地充斥着我们的心思，涤荡我们的灵魂又留下痕迹。遇到一些离谱的事情，我偶尔会和朋友吐槽，有些不好的事情让我心情不好，我会选择睡大觉，情绪稳定让我有足够好的状态迎接接下来的生活，不至于因为一件事就把生活弄得一团糟，让我有冷静面对问题的能力&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.bangwu.me/img/2026011113785858.png&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;p&gt;之前许愿，能做个“个性鲜明”的人，像动漫里的人物一样热血的就一直热血中二，吃货走到哪里就都是想着吃，这样好像就可以一直将自己想要表达的说出来也不会觉得不合适，因为“我”就是这样，但事实是生活中有许许多多的枷锁出于考虑，大多数选择不说或说些别的，我也不想以xx型人格来说不同的人们对待事情的方式，大家都是个性鲜明的呀，做出什么或说哪种话都是个概率问题啊，是啊...&lt;/p&gt;
&lt;p&gt;情绪这个东西，其实就像大海，有时风平浪静，有时波涛汹涌。过去我一直认为，情绪要有一份难得的稳定——正如高考时那种无波无澜的心境，也如面对喜欢的人离开时的冷静处事。理性似乎总能替情感埋下太多的石子，让人站得笔直，免去一时的情绪波动。然而，越是压抑那些即将迸发的情感，我越发觉得，有时释放一下也许是必要的，就像《一人之下》里那个能将情绪化作实体的异人，情绪释放后的破裂或许才是真正的存在。&lt;/p&gt;
&lt;p&gt;我发现，情绪释放并不意味着完全失控，也不全是情绪失范的代名词。它更像是在面对生活中那些压抑和沉淀的烦闷时，一种自我调适的方法。当一件事真得让你无法承担，或是那积压已久的心里负担已经到了极限的时候，偶尔放下理性的枷锁，把那些长期按捺的情绪爆发出来，或许能让内心获得一种久违的解脱。试想一下，当你把所有的不快、大大小小的负面情绪都“释放”出来，伴随着哭泣、呐喊甚至是一阵无声的叹息后，或许会迎来一种轻松的空白期，就像大雨过后的清新空气，让人深吸一口气，再次整理好自己的思绪。&lt;/p&gt;
&lt;p&gt;对我来说，绝大多数时候，我更喜欢保持稳定，因为情绪的稳定让生活多了一份掌控感，可以让我在各种考验面前冷静应对。稳定的情绪为我构建了一道坚实的防线，它让我在遇到不期而至的挫折时，不至于瞬间崩溃，一步步找到问题的症结。然而，内心深处，我也明白，完全压抑情绪并非长久之计。人毕竟不是没有感情的机器，偶尔的情绪迸发，反而会给我带来关于自我的更深认识。这种释放，无论是通过与朋友的深夜长谈，还是偶尔的一场独处的散步，都让我明白，情绪既需要保护的稳定，也需要适当的发泄。&lt;/p&gt;
&lt;h2&gt;平衡才是答案&lt;/h2&gt;
&lt;p&gt;因此，我开始试着调整自己的策略：在面对大部分琐碎和紧张事务时，我依然选择保持冷静，利用理性的力量稳定情绪；而当那些深藏心底的情感积压到一定程度，我会允许自己去体验那种短暂的释放——那既不是自毁也不是放纵，而是一种勇敢面对内在真实声音的方式。就像有时夜深人静时，一场偶然的哭泣，可能会让我看清心底那些被理性忽略的温情、恐惧和对未来的渴望。或许正是在这一过程中，我学会了如何在稳定与释放之间寻找一种微妙的平衡，既不让情感泛滥失控，也不让内心的火焰永远熄灭在理性的冰层之下。&lt;/p&gt;
&lt;p&gt;这样的探索，让我渐渐明白了：情绪不该只是一味地被压抑或任意的宣泄，而应是一种内心的艺术。它需要在适当的时候得到稳定，以便我们能以清晰的头脑面对生活；同时，在适当的时刻，它又需要被释放，让我们的精神获得治愈与重生。这既是一种自我疗愈的方式，也是对生活多彩情感的真诚回馈。&lt;/p&gt;
&lt;p&gt;“我看不破
我放不过
只能默默忍受被情绪操纵...”&lt;/p&gt;
</content:encoded></item><item><title>Supervisor 从入门到精通</title><link>https://bangwu.me/posts/supervisor/</link><guid isPermaLink="true">https://bangwu.me/posts/supervisor/</guid><description>Supervisor 不止是进程守护工具。这篇从基础配置讲到事件监听、进程组管理和生产环境中的高级用法。</description><pubDate>Wed, 16 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Supervisor 从入门到精通：不止是进程守护，更是应用管理的瑞士军刀&lt;/h2&gt;
&lt;p&gt;大家好，我是bangwu。在后端开发和运维的世界里，确保我们的应用程序（无论是 Web 服务、后台任务队列还是其他关键进程）能够稳定、持续地运行至关重要。进程意外退出、服务器重启后需要手动恢复，这些都是令人头疼的问题。今天，我们就来深入探讨一个优雅解决这些问题的利器——&lt;strong&gt;Supervisor&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;Supervisor 不仅仅是一个简单的进程监控和重启工具，它更是一个功能强大的客户端/服务器系统，允许用户在类 UNIX 操作系统上控制和监控多个进程。这篇文章将带你从 Supervisor 的基础概念出发，一步步掌握其安装、配置、实战应用，并深入了解其高级特性和最佳实践，助你成为 Supervisor 的使用专家。&lt;/p&gt;
&lt;h2&gt;1. Supervisor 是什么？为什么选择它？&lt;/h2&gt;
&lt;p&gt;想象一下，你部署了一个重要的 Python Web 应用，它通过 Gunicorn 运行。如果 Gunicorn 进程因为某些错误意外崩溃了，或者服务器重启了，会发生什么？服务中断，用户无法访问，你需要手动登录服务器去重启它。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Supervisor&lt;/strong&gt; 就是来解决这个问题的。它是一个用 Python 编写的进程控制系统，可以：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;启动进程&lt;/strong&gt;：在后台启动你的应用程序。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;监控进程&lt;/strong&gt;：持续关注你的进程状态。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;自动重启&lt;/strong&gt;：如果进程意外退出，Supervisor 会自动尝试重启它。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;管理多个进程&lt;/strong&gt;：轻松管理多个不同的应用程序或同一应用的不同实例。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;日志管理&lt;/strong&gt;：捕获进程的标准输出 (stdout) 和标准错误 (stderr) 到指定文件。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;提供控制接口&lt;/strong&gt;：通过 &lt;code&gt;supervisorctl&lt;/code&gt; 命令行工具或 Web UI 来管理进程（启动、停止、重启、查看状态等）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;为什么选择 Supervisor？&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;简单易用&lt;/strong&gt;：配置相对直观，上手快。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;稳定可靠&lt;/strong&gt;：经过广泛使用和测试，非常稳定。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;资源占用低&lt;/strong&gt;：相比一些重量级解决方案，Supervisor 本身消耗的资源很少。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;功能适中&lt;/strong&gt;：提供了足够强大的功能，但又不像 systemd 那样与操作系统深度绑定，更侧重于应用层进程管理。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;跨平台性好&lt;/strong&gt;：虽然主要用于类 UNIX 系统，但因为它基于 Python，理论上可在任何支持 Python 的平台上运行（尽管 Windows 支持可能有限）。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;2. 核心概念解析&lt;/h2&gt;
&lt;p&gt;理解 Supervisor 的工作方式，需要了解几个核心组件：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;supervisord&lt;/code&gt;&lt;/strong&gt;: 这是 Supervisor 的服务器端进程。它负责启动、管理和监控所有在配置文件中定义的子进程。它本身通常也需要被某种方式（如 systemd、init.d 脚本）守护，以确保 Supervisor 自身的稳定运行。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;supervisorctl&lt;/code&gt;&lt;/strong&gt;: 这是 Supervisor 的客户端命令行工具。通过它，用户可以连接到 &lt;code&gt;supervisord&lt;/code&gt;，并对其管理的进程进行各种操作（如 &lt;code&gt;start&lt;/code&gt;, &lt;code&gt;stop&lt;/code&gt;, &lt;code&gt;restart&lt;/code&gt;, &lt;code&gt;status&lt;/code&gt; 等）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;配置文件&lt;/strong&gt;: Supervisor 的行为由一个或多个配置文件定义。通常有一个主配置文件（如 &lt;code&gt;/etc/supervisord.conf&lt;/code&gt;），它可以包含（include）其他目录下的配置文件（如 &lt;code&gt;/etc/supervisor/conf.d/*.conf&lt;/code&gt;），每个文件通常定义一个或多个要管理的程序（program）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;程序 (Program)&lt;/strong&gt;: 指你希望 Supervisor 管理的实际应用程序进程。在配置文件中以 &lt;code&gt;[program:your_program_name]&lt;/code&gt; 部分定义。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;进程组 (Group)&lt;/strong&gt;: 可以将多个相关的程序定义在一个组内，方便统一管理。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;3. 安装 Supervisor&lt;/h2&gt;
&lt;p&gt;安装 Supervisor 最常见的方式是通过包管理器或 &lt;code&gt;pip&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;使用 pip (推荐，通常版本较新)&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 确保你有 pip
# python -m ensurepip --upgrade  # 或者 python3 -m ensurepip --upgrade
# pip install --upgrade pip

# 安装 supervisor
pip install supervisor

# 安装后，可能需要手动创建配置文件和目录
# 通常配置文件位于 /etc/supervisord.conf 或 /etc/supervisor/supervisord.conf
# 程序配置文件目录通常是 /etc/supervisor/conf.d/
# 可以运行 echo_supervisord_conf 将默认配置输出到文件
echo_supervisord_conf &amp;gt; /etc/supervisord.conf # 可能需要 sudo
mkdir -p /etc/supervisor/conf.d/ # 可能需要 sudo
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;使用系统包管理器 (以 Ubuntu/Debian 和 CentOS/RHEL 为例)&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Ubuntu/Debian&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt update
sudo apt install supervisor
# 配置文件通常在 /etc/supervisor/supervisord.conf
# 程序配置文件在 /etc/supervisor/conf.d/
# 安装后通常会自动启动 supervisord 服务
sudo systemctl status supervisor
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;CentOS/RHEL (可能需要 EPEL 源)&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo yum install epel-release # 如果没有安装 EPEL
sudo yum install supervisor
# 配置文件通常在 /etc/supervisord.conf
# 程序配置文件在 /etc/supervisor/conf.d/
# 安装后需要手动启动并设置开机自启
sudo systemctl start supervisord
sudo systemctl enable supervisord
sudo systemctl status supervisord
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;安装完成后，&lt;code&gt;supervisord&lt;/code&gt; (服务) 和 &lt;code&gt;supervisorctl&lt;/code&gt; (客户端) 命令就可用了。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;4. 初识配置：管理第一个进程&lt;/h2&gt;
&lt;p&gt;让我们来管理一个简单的长时间运行的脚本。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;创建示例脚本&lt;/strong&gt; (&lt;code&gt;/opt/myapp/loop.sh&lt;/code&gt;):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#!/bin/bash
echo &quot;My simple loop script started at $(date)&quot;
while true; do
  echo &quot;Looping... $(date)&quot;
  sleep 5
done
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;给它执行权限: &lt;code&gt;chmod +x /opt/myapp/loop.sh&lt;/code&gt; (确保 &lt;code&gt;/opt/myapp&lt;/code&gt; 目录存在)。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;创建 Supervisor 程序配置文件&lt;/strong&gt; (&lt;code&gt;/etc/supervisor/conf.d/loop_script.conf&lt;/code&gt;):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[program:loop_script]
command=/opt/myapp/loop.sh             ; 要执行的命令
autostart=true                         ; supervisord 启动时自动启动该程序
autorestart=true                       ; 程序退出时自动重启 (意外退出时)
user=nobody                            ; 以哪个用户身份运行 (建议非 root)
stdout_logfile=/var/log/supervisor/loop_script_stdout.log ; 标准输出日志文件路径
stderr_logfile=/var/log/supervisor/loop_script_stderr.log ; 标准错误日志文件路径
directory=/tmp                         ; 命令执行前切换到的目录 (可选)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;注意&lt;/em&gt;: 确保 &lt;code&gt;/var/log/supervisor/&lt;/code&gt; 目录存在并且 Supervisor 运行的用户（通常是 root 或 supervisor）有权写入。&lt;code&gt;mkdir -p /var/log/supervisor &amp;amp;&amp;amp; chown nobody:nogroup /var/log/supervisor&lt;/code&gt; (用户 &lt;code&gt;nobody&lt;/code&gt; 可能因系统而异，根据实际情况调整)。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;让 Supervisor 加载新配置&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo supervisorctl reread  # 读取配置文件 (包括 conf.d 下的新文件)
# 输出类似: loop_script: available

sudo supervisorctl update  # 应用新的配置，启动新增的程序
# 输出类似: loop_script: added process group

# 或者，如果你想明确启动它
# sudo supervisorctl start loop_script
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;检查状态&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo supervisorctl status
# 输出类似:
# loop_script                      RUNNING   pid 12345, uptime 0:00:15
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;查看日志&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo tail -f /var/log/supervisor/loop_script_stdout.log
# 你会看到 &quot;Looping...&quot; 的输出
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;停止进程&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo supervisorctl stop loop_script
# 再次 status 查看，状态应变为 STOPPED 或 EXITED
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;恭喜！你已经成功使用 Supervisor 管理了你的第一个进程。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;5. 配置文件深度解析 (&lt;code&gt;supervisord.conf&lt;/code&gt;)&lt;/h2&gt;
&lt;p&gt;Supervisor 的强大之处在于其灵活的配置。主配置文件（通常是 &lt;code&gt;/etc/supervisord.conf&lt;/code&gt;）定义了 &lt;code&gt;supervisord&lt;/code&gt; 自身行为、&lt;code&gt;supervisorctl&lt;/code&gt; 连接方式以及包含其他配置文件的路径。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;; /etc/supervisord.conf (示例片段)

[unix_http_server]
file=/var/run/supervisor.sock   ; socket 文件路径，supervisorctl 默认连接这个
;chmod=0700                 ; socket 文件的权限
;chown=nobody:nogroup       ; socket 文件的所有者

[inet_http_server]         ; HTTP 服务器，用于 Web UI 和远程 XML-RPC
;port=127.0.0.1:9001        ; 监听地址和端口 (默认不启用)
;username=user              ; (可选) 访问 Web UI/RPC 的用户名
;password=123               ; (可选) 密码

[supervisord]
logfile=/var/log/supervisor/supervisord.log ; supervisord 自身日志
pidfile=/var/run/supervisord.pid ; supervisord 进程 ID 文件
childlogdir=/var/log/supervisor            ; 子进程日志目录 (如果 program 中没指定绝对路径)
;nodaemon=false             ; false 表示后台运行 (守护进程模式)，true 表示前台运行 (调试用)
;minfds=1024                ; 最小可用文件描述符
;minprocs=200               ; 最小可用进程数
;user=root                  ; supervisord 进程的运行用户 (通常保持 root，以便管理不同用户的子进程)

[rpcinterface:supervisor]
supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface

[supervisorctl]
serverurl=unix:///var/run/supervisor.sock ; supervisorctl 连接的服务器 URL
;serverurl=http://127.0.0.1:9001 ; 如果使用 inet_http_server
;username=user              ; 如果服务器设置了认证
;password=123               ;
;prompt=mysupervisor        ; supervisorctl 的提示符

; 包含 conf.d 目录下的所有 .conf 文件
[include]
files = /etc/supervisor/conf.d/*.conf
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;[program:x]&lt;/code&gt; 部分详解 (重点中的重点)&lt;/strong&gt;:&lt;/p&gt;
&lt;p&gt;这是定义你要管理的应用程序的地方。&lt;code&gt;x&lt;/code&gt; 是你为程序起的名字，在 &lt;code&gt;supervisorctl&lt;/code&gt; 中会用到。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[program:my_web_app]
command=/usr/bin/python /opt/myapp/app.py --port=8080 ; ★ 启动进程的完整命令
process_name=%(program_name)s_%(process_num)02d ; 进程名模板 (用于多实例)
numprocs=1                 ; 启动多少个进程实例 (默认为 1)
directory=/opt/myapp       ; ★ 执行命令前切换到的工作目录
umask=022                  ; 进程的文件模式创建屏蔽码
priority=999               ; 进程启动/关闭优先级 (值越小越高，默认 999)
autostart=true             ; ★ supervisord 启动时自动启动
autorestart=true           ; ★ 进程退出时是否自动重启 (true, false, unexpected)
                           ; unexpected: 仅在退出码不是 exitcodes 定义的值时重启
startsecs=10               ; ★ 进程启动后持续运行多少秒才认为启动成功 (默认 1)
startretries=3             ; ★ 启动失败时的重试次数 (默认 3)
stopsignal=TERM            ; ★ 停止进程时发送的信号 (默认 TERM)
stopwaitsecs=10            ; ★ 发送停止信号后等待多少秒，超时则发送 KILL (默认 10)
stopasgroup=false          ; 是否向整个进程组发送停止信号 (默认 false)
killasgroup=false          ; stop 超时后，是否向整个进程组发送 KILL 信号 (默认 false)
user=www-data              ; ★ 以哪个用户身份运行进程 (重要！)
redirect_stderr=false      ; 是否将 stderr 重定向到 stdout (默认 false)
stdout_logfile=/var/log/supervisor/my_web_app_stdout.log ; ★ 标准输出日志
stdout_logfile_maxbytes=50MB ; ★ 日志文件最大大小 (默认 50MB, 到达后轮转)
stdout_logfile_backups=10  ; ★ 保留多少个轮转后的日志文件 (默认 10)
stdout_capture_maxbytes=1MB ; 捕获到内存用于事件通知的最大字节数
stdout_events_enabled=false ; 是否为 stdout 产生事件 (用于事件监听)
stderr_logfile=/var/log/supervisor/my_web_app_stderr.log ; ★ 标准错误日志
stderr_logfile_maxbytes=50MB ; 同上
stderr_logfile_backups=10  ; 同上
stderr_capture_maxbytes=1MB ; 同上
stderr_events_enabled=false ; 同上
environment=APP_ENV=&quot;production&quot;,SECRET_KEY=&quot;mysecret&quot; ; ★ 设置环境变量 (键值对，逗号分隔)
serverurl=AUTO             ; 通常不需要设置，除非有特殊 RPC 需求
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;[group:x]&lt;/code&gt; 部分&lt;/strong&gt;:&lt;/p&gt;
&lt;p&gt;用于将多个 &lt;code&gt;[program:y]&lt;/code&gt; 归为一个逻辑组，方便统一管理。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[group:my_app_cluster]
programs=my_web_app,my_worker ; program 名称列表，逗号分隔
priority=998                ; 组的优先级
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;之后可以用 &lt;code&gt;supervisorctl start my_app_cluster:*&lt;/code&gt; 来启动组内所有程序。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;[include]&lt;/code&gt; 部分&lt;/strong&gt;:&lt;/p&gt;
&lt;p&gt;允许你将配置分散到多个文件中，保持主配置文件的整洁。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[include]
files = /etc/supervisor/conf.d/*.conf /opt/myapp/supervisor_extra.conf
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;6. &lt;code&gt;supervisorctl&lt;/code&gt; 命令行详解&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;supervisorctl&lt;/code&gt; 是与 &lt;code&gt;supervisord&lt;/code&gt; 交互的主要工具。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;进入交互模式&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo supervisorctl
# prompt&amp;gt; status
# prompt&amp;gt; help
# prompt&amp;gt; quit
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;常用命令&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;status [&amp;lt;name&amp;gt;]&lt;/code&gt;: 查看所有或指定程序的运行状态。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;start &amp;lt;name|group:*&amp;gt;|all&lt;/code&gt;: 启动指定程序、组内所有程序或所有程序。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;stop &amp;lt;name|group:*&amp;gt;|all&lt;/code&gt;: 停止指定程序、组内所有程序或所有程序。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;restart &amp;lt;name|group:*&amp;gt;|all&lt;/code&gt;: 重启指定程序、组内所有程序或所有程序。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;reread&lt;/code&gt;: 重新读取配置文件，检测是否有新增或删除的程序/组。不会影响正在运行的程序。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;update [&amp;lt;gname&amp;gt; ...]&lt;/code&gt;: 应用 &lt;code&gt;reread&lt;/code&gt; 后检测到的配置变更（启动新增的，停止删除的）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;reload&lt;/code&gt;: 重启 &lt;code&gt;supervisord&lt;/code&gt; 进程本身（相当于 &lt;code&gt;stop all&lt;/code&gt; + 重启 &lt;code&gt;supervisord&lt;/code&gt; 服务 + &lt;code&gt;start all&lt;/code&gt;）。通常用 &lt;code&gt;reread&lt;/code&gt; + &lt;code&gt;update&lt;/code&gt; 更好。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;shutdown&lt;/code&gt;: 关闭 &lt;code&gt;supervisord&lt;/code&gt; 服务（所有管理的进程也会被停止）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pid [&amp;lt;name&amp;gt;]&lt;/code&gt;: 查看指定程序（或所有程序）的进程 ID。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tail [-f] &amp;lt;name&amp;gt; [stdout|stderr]&lt;/code&gt;: 查看指定程序的日志尾部 (&lt;code&gt;-f&lt;/code&gt; 持续跟随)。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fg &amp;lt;name&amp;gt;&lt;/code&gt;: 将程序附加到前台运行 (主要用于调试，按 Ctrl+C 会停止进程)。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;signal &amp;lt;signal_name&amp;gt; &amp;lt;name|group:*&amp;gt;|all&lt;/code&gt;: 向指定程序发送信号 (如 &lt;code&gt;signal HUP my_web_app&lt;/code&gt;)。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;maintail -f&lt;/code&gt;: 同时查看所有程序的 stdout 日志。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;7. 实战教程：使用 Supervisor 管理 Web 应用 (以 Flask/Gunicorn 为例)&lt;/h2&gt;
&lt;p&gt;假设我们有一个简单的 Flask 应用 (&lt;code&gt;/opt/myflaskapp/app.py&lt;/code&gt;):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# /opt/myflaskapp/app.py
from flask import Flask
import os
import logging

app = Flask(__name__)

# 配置日志记录到标准输出，以便 Supervisor 捕获
logging.basicConfig(level=logging.INFO, format=&apos;%(asctime)s %(levelname)s: %(message)s&apos;)

@app.route(&apos;/&apos;)
def hello():
    app.logger.info(&apos;Root endpoint was hit!&apos;)
    env = os.environ.get(&apos;APP_ENV&apos;, &apos;development&apos;)
    return f&quot;Hello from Flask in {env} environment!&quot;

if __name__ == &apos;__main__&apos;:
    # 注意：直接运行 &apos;python app.py&apos; 不适用于生产环境
    # 我们将使用 Gunicorn 来运行它
    app.run(host=&apos;0.0.0.0&apos;, port=5000, debug=True)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;目标&lt;/strong&gt;: 使用 Gunicorn 作为 WSGI 服务器运行此应用，并由 Supervisor 管理。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;步骤&lt;/strong&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;创建项目目录和虚拟环境&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo mkdir -p /opt/myflaskapp
sudo chown $USER:$USER /opt/myflaskapp # 临时更改所有权以便操作
cd /opt/myflaskapp
python3 -m venv venv
source venv/bin/activate
pip install Flask gunicorn
deactivate
sudo chown -R www-data:www-data /opt/myflaskapp # 将所有权给运行应用的用户 (假设是 www-data)
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;确定 Gunicorn 启动命令&lt;/strong&gt;:
我们需要找到虚拟环境中的 &lt;code&gt;gunicorn&lt;/code&gt; 可执行文件路径。通常是 &lt;code&gt;/opt/myflaskapp/venv/bin/gunicorn&lt;/code&gt;。
启动命令类似：
&lt;code&gt;/opt/myflaskapp/venv/bin/gunicorn --workers 3 --bind 0.0.0.0:8000 app:app&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;--workers 3&lt;/code&gt;: 启动 3 个工作进程。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--bind 0.0.0.0:8000&lt;/code&gt;: 监听在 8000 端口。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;app:app&lt;/code&gt;: 指定模块 &lt;code&gt;app.py&lt;/code&gt; 中的 &lt;code&gt;app&lt;/code&gt; 对象。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;创建 Supervisor 配置文件&lt;/strong&gt; (&lt;code&gt;/etc/supervisor/conf.d/myflaskapp.conf&lt;/code&gt;):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[program:myflaskapp]
command=/opt/myflaskapp/venv/bin/gunicorn --workers 3 --bind 0.0.0.0:8000 app:app
directory=/opt/myflaskapp             ; ★ 必须设置，Gunicorn 需要找到 app.py
user=www-data                         ; ★ 以 www-data 用户运行
autostart=true
autorestart=true
stopsignal=QUIT                       ; Gunicorn 推荐使用 QUIT 实现优雅关闭
stopwaitsecs=60                       ; 等待 Gunicorn worker 优雅退出的时间
killasgroup=true                      ; ★ 重要：确保 Gunicorn 的 master 和 worker 都被正确关闭
stdout_logfile=/var/log/supervisor/myflaskapp_stdout.log
stderr_logfile=/var/log/supervisor/myflaskapp_stderr.log
stdout_logfile_maxbytes=100MB
stdout_logfile_backups=5
stderr_logfile_maxbytes=100MB
stderr_logfile_backups=5
environment=APP_ENV=&quot;production&quot;      ; ★ 设置环境变量
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;关键点&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;command&lt;/code&gt; 使用虚拟环境中的 &lt;code&gt;gunicorn&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;directory&lt;/code&gt; 设置为项目根目录。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;user&lt;/code&gt; 设置为安全的非 root 用户 (&lt;code&gt;www-data&lt;/code&gt; 或其他)。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;stopsignal=QUIT&lt;/code&gt; 和 &lt;code&gt;killasgroup=true&lt;/code&gt; 配合 Gunicorn 实现优雅重启/停止。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;environment&lt;/code&gt; 用于传递配置。&lt;/li&gt;
&lt;li&gt;确保 &lt;code&gt;/var/log/supervisor/&lt;/code&gt; 目录存在且 &lt;code&gt;www-data&lt;/code&gt; 用户有权写入日志文件，或者让 &lt;code&gt;supervisord&lt;/code&gt; (通常以 root 运行) 创建它们。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;加载配置并启动&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo supervisorctl reread
sudo supervisorctl update myflaskapp # 或者直接 update
sudo supervisorctl status myflaskapp
# 应显示 RUNNING
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;测试访问&lt;/strong&gt;:
用 &lt;code&gt;curl http://localhost:8000&lt;/code&gt; 或浏览器访问，应看到 &quot;Hello from Flask in production environment!&quot;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;查看日志&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo tail -f /var/log/supervisor/myflaskapp_stdout.log
sudo tail -f /var/log/supervisor/myflaskapp_stderr.log
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;现在，你的 Flask 应用就由 Supervisor 可靠地管理了！如果 Gunicorn 进程崩溃，Supervisor 会自动重启它。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;8. 高级特性与技巧&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;进程组 (Process Groups)&lt;/strong&gt;: 如前所述，用 &lt;code&gt;[group:x]&lt;/code&gt; 将多个 &lt;code&gt;[program:y]&lt;/code&gt; 묶在一起管理。例如，一个 Web 应用可能需要一个 Web 服务器进程和一个后台任务 Worker 进程，可以将它们放在同一组中。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[group:webapp]
programs=webapp-server,webapp-worker
priority=100

[program:webapp-server]
command=... (gunicorn command)
...

[program:webapp-worker]
command=... (celery worker command)
...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;管理：&lt;code&gt;supervisorctl start webapp:*&lt;/code&gt;, &lt;code&gt;supervisorctl stop webapp:*&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;事件监听 (Event Listeners)&lt;/strong&gt;: Supervisor 可以监控进程状态变化（如 &lt;code&gt;STARTING&lt;/code&gt;, &lt;code&gt;RUNNING&lt;/code&gt;, &lt;code&gt;STOPPING&lt;/code&gt;, &lt;code&gt;EXITED&lt;/code&gt;）、心跳（&lt;code&gt;TICK_5&lt;/code&gt;, &lt;code&gt;TICK_60&lt;/code&gt;, &lt;code&gt;TICK_3600&lt;/code&gt;）以及进程的 stdout/stderr 输出，并在事件发生时执行一个特定的程序（监听器）。
这对于实现自定义监控、告警或自动化任务非常有用。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[eventlistener:mylogger]
command=/usr/bin/python /opt/scripts/log_event.py ; 监听器脚本
events=PROCESS_STATE,TICK_60                   ; 监听的事件类型
autostart=true
autorestart=true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;监听器脚本 (&lt;code&gt;log_event.py&lt;/code&gt; 示例) 会从 stdin 读取事件信息，并在 stdout 写 &lt;code&gt;READY&lt;/code&gt; 表示准备就绪，&lt;code&gt;OK&lt;/code&gt; 或 &lt;code&gt;FAIL&lt;/code&gt; 表示处理结果。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# /opt/scripts/log_event.py (简化示例)
import sys
import os

def write_stdout(s):
    sys.stdout.write(s)
    sys.stdout.flush()

def write_stderr(s):
    sys.stderr.write(s)
    sys.stderr.flush()

def main():
    while True:
        write_stdout(&apos;READY\n&apos;) # 告诉 supervisord 我准备好了
        line = sys.stdin.readline() # 读取事件头信息
        headers = dict([ x.split(&apos;:&apos;) for x in line.split() ])
        data = sys.stdin.read(int(headers[&apos;len&apos;])) # 读取事件体

        # 在这里处理事件 (例如记录到日志或发送通知)
        log_path = &quot;/var/log/supervisor/events.log&quot;
        with open(log_path, &quot;a&quot;) as f:
            f.write(f&quot;Header: {line}\n&quot;)
            f.write(f&quot;Data: {data}\n---\n&quot;)

        write_stdout(&apos;RESULT 2\nOK&apos;) # 告诉 supervisord 处理成功

if __name__ == &apos;__main__&apos;:
    # 确保日志文件可写 (简单处理，生产环境应更健壮)
    log_dir = os.path.dirname(&quot;/var/log/supervisor/events.log&quot;)
    if not os.path.exists(log_dir):
        os.makedirs(log_dir) # 可能需要权限

    main()
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;XML-RPC 接口与 Web UI&lt;/strong&gt;:
通过在主配置文件中启用 &lt;code&gt;[inet_http_server]&lt;/code&gt;，可以开启 Supervisor 的 HTTP 服务。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Web UI&lt;/strong&gt;: 浏览器访问 &lt;code&gt;http://&amp;lt;server_ip&amp;gt;:9001&lt;/code&gt; (或配置的地址端口)，可以看到一个简单的 Web 界面，用于查看和管理进程。建议设置用户名密码 (&lt;code&gt;username&lt;/code&gt;, &lt;code&gt;password&lt;/code&gt;) 并考虑防火墙或反向代理保护。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;XML-RPC&lt;/strong&gt;: 提供了编程接口，可以用 Python (或其他语言的库) 远程控制 Supervisor。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;import xmlrpc.client

# 如果没有认证
# server = xmlrpc.client.ServerProxy(&apos;http://localhost:9001/RPC2&apos;)

# 如果有认证
server = xmlrpc.client.ServerProxy(&apos;http://user:123@localhost:9001/RPC2&apos;)

try:
    print(&quot;Supervisor version:&quot;, server.supervisor.getSupervisorVersion())
    print(&quot;All process info:&quot;, server.supervisor.getAllProcessInfo())
    # 启动进程
    # server.supervisor.startProcess(&apos;myflaskapp&apos;)
except xmlrpc.client.Fault as err:
    print(&quot;XML-RPC Fault:&quot;, err)
except Exception as e:
    print(&quot;Error:&quot;, e)
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;日志管理与轮转&lt;/strong&gt;:
Supervisor 内建了简单的日志轮转功能 (&lt;code&gt;stdout_logfile_maxbytes&lt;/code&gt;, &lt;code&gt;stdout_logfile_backups&lt;/code&gt;)。对于更复杂的日志管理需求（如按日期轮转、压缩、发送到中心化日志系统），建议：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;让应用程序自己处理日志轮转（如 Python 的 &lt;code&gt;logging.handlers.RotatingFileHandler&lt;/code&gt; 或 &lt;code&gt;TimedRotatingFileHandler&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;或者将 Supervisor 日志输出到 stdout/stderr (&lt;code&gt;stdout_logfile=/dev/stdout&lt;/code&gt;, &lt;code&gt;stderr_logfile=/dev/stderr&lt;/code&gt;)，然后由外部工具（如 &lt;code&gt;logrotate&lt;/code&gt;, Fluentd, Docker logging drivers）来管理。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;环境变量配置&lt;/strong&gt;:
&lt;code&gt;environment=KEY1=&quot;value1&quot;,KEY2=&quot;value2&quot;&lt;/code&gt; 是设置环境变量的标准方式。值可以包含空格，用引号括起来即可。对于大量或敏感变量，可以考虑：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;从文件加载：&lt;code&gt;environment=PYTHONPATH=&quot;/opt/myapp/lib&quot;,%(ENV_VAR_FROM_SUPERVISORD)s&lt;/code&gt; (从 supervisord 进程继承)。&lt;/li&gt;
&lt;li&gt;或者在启动脚本中 &lt;code&gt;source&lt;/code&gt; 一个 &lt;code&gt;.env&lt;/code&gt; 文件。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;优雅停止与信号处理&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;stopsignal&lt;/code&gt;: 定义了 &lt;code&gt;supervisorctl stop&lt;/code&gt; 时发送给进程的信号。常见信号：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;TERM&lt;/code&gt;: 通用终止信号，程序应捕获并优雅退出。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;INT&lt;/code&gt;: 中断信号，类似 Ctrl+C。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;QUIT&lt;/code&gt;: 类似 &lt;code&gt;TERM&lt;/code&gt;，有时用于请求更优雅的退出（如 Gunicorn）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;HUP&lt;/code&gt;: 通常用于通知进程重新加载配置。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;USR1&lt;/code&gt;, &lt;code&gt;USR2&lt;/code&gt;: 用户自定义信号。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;stopwaitsecs&lt;/code&gt;: 发送 &lt;code&gt;stopsignal&lt;/code&gt; 后等待进程退出的时间。如果超时，Supervisor 会发送 &lt;code&gt;KILL&lt;/code&gt; 信号强制终止。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;stopasgroup=true&lt;/code&gt;: 如果你的主进程会创建子进程，并且希望停止时能同时停止所有子进程，可以设为 &lt;code&gt;true&lt;/code&gt;（前提是主进程是进程组的领导者）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;killasgroup=true&lt;/code&gt;: 如果 &lt;code&gt;stopwaitsecs&lt;/code&gt; 超时后发送 &lt;code&gt;KILL&lt;/code&gt;，&lt;code&gt;killasgroup=true&lt;/code&gt; 会将 &lt;code&gt;KILL&lt;/code&gt; 信号发送给整个进程组。&lt;strong&gt;对于像 Gunicorn/uWSGI 这样的多进程模型，这通常是必要的&lt;/strong&gt;，以确保所有 worker 都被清理。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;9. 常见问题与故障排查 (Troubleshooting)&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;supervisorctl&lt;/code&gt; 无法连接 (&lt;code&gt;unix:///var/run/supervisor.sock no such file&lt;/code&gt;)&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;supervisord&lt;/code&gt; 服务是否正在运行？(&lt;code&gt;ps aux | grep supervisord&lt;/code&gt;, &lt;code&gt;systemctl status supervisor(d)&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;配置文件中的 &lt;code&gt;[unix_http_server]&lt;/code&gt; 或 &lt;code&gt;[supervisorctl]&lt;/code&gt; 的 &lt;code&gt;file&lt;/code&gt;/&lt;code&gt;serverurl&lt;/code&gt; 路径是否正确且一致？&lt;/li&gt;
&lt;li&gt;socket 文件 (&lt;code&gt;/var/run/supervisor.sock&lt;/code&gt;) 是否存在？权限是否正确 (运行 &lt;code&gt;supervisorctl&lt;/code&gt; 的用户需要有读写权限)？&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;程序状态为 &lt;code&gt;FATAL&lt;/code&gt; 或 &lt;code&gt;EXITED&lt;/code&gt;&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;检查程序的 &lt;code&gt;stderr_logfile&lt;/code&gt; (&lt;code&gt;sudo tail /var/log/supervisor/program_stderr.log&lt;/code&gt;)，通常会包含错误信息。&lt;/li&gt;
&lt;li&gt;检查程序的 &lt;code&gt;stdout_logfile&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;尝试手动在终端中以配置文件中的 &lt;code&gt;user&lt;/code&gt; 身份和 &lt;code&gt;directory&lt;/code&gt; 运行 &lt;code&gt;command&lt;/code&gt;，看是否能成功启动并保持运行。&lt;/li&gt;
&lt;li&gt;检查 &lt;code&gt;startsecs&lt;/code&gt; 设置是否过短，程序可能需要更长时间才能稳定。&lt;/li&gt;
&lt;li&gt;检查系统资源是否耗尽（内存、文件描述符）。&lt;/li&gt;
&lt;li&gt;检查程序依赖是否正确安装（特别是虚拟环境路径）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;配置文件修改后未生效&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;是否运行了 &lt;code&gt;sudo supervisorctl reread&lt;/code&gt;？&lt;/li&gt;
&lt;li&gt;是否运行了 &lt;code&gt;sudo supervisorctl update&lt;/code&gt; 来应用变更？&lt;/li&gt;
&lt;li&gt;检查配置文件语法是否有误 (&lt;code&gt;supervisord -n -c /etc/supervisord.conf&lt;/code&gt; 可以检查配置但不启动)。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;日志文件过大&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;检查 &lt;code&gt;stdout_logfile_maxbytes&lt;/code&gt; 和 &lt;code&gt;stdout_logfile_backups&lt;/code&gt; (以及 stderr 的对应项) 配置是否正确。&lt;/li&gt;
&lt;li&gt;考虑使用外部日志轮转工具或让应用自己管理日志。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;权限问题&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;确保配置文件中指定的 &lt;code&gt;user&lt;/code&gt; 有权限执行 &lt;code&gt;command&lt;/code&gt;、读写 &lt;code&gt;directory&lt;/code&gt; 以及写入日志文件。&lt;/li&gt;
&lt;li&gt;如果 Supervisor 以 root 运行，它通常能创建日志文件，但需要确保后续进程（以 &lt;code&gt;user&lt;/code&gt; 身份运行）有权写入。可以预先创建日志文件并设置正确的所有权和权限。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;10. Supervisor vs. Systemd vs. PM2 等&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Supervisor&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;优点&lt;/strong&gt;: 简单易用，专注于应用进程管理，跨平台性较好（类 UNIX），配置直观，资源占用低。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缺点&lt;/strong&gt;: 不像 systemd 那样与 OS 深度集成（如 cgroups 资源限制、依赖管理、socket 激活等功能较弱），主要面向单个主机。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;适用场景&lt;/strong&gt;: 管理单个服务器上的多个用户态应用程序进程，快速部署和管理后台任务、Web 服务。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Systemd&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;优点&lt;/strong&gt;: Linux 系统的标准 init 系统，功能强大，与 OS 深度集成（日志管理 journald, cgroups 资源控制, socket 激活, 定时任务, 服务依赖等），非常稳定。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缺点&lt;/strong&gt;: 配置相对复杂，学习曲线陡峭，强绑定于使用 systemd 的 Linux 发行版。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;适用场景&lt;/strong&gt;: 管理系统级服务和需要精细 OS 资源控制的应用，已成为现代 Linux 发行版的标准。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;PM2&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;优点&lt;/strong&gt;: 专门为 Node.js 应用设计，功能丰富（集群模式、负载均衡、零停机重启、监控面板、模块系统），开箱即用体验好。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缺点&lt;/strong&gt;: 主要面向 Node.js 生态，虽然也能管理其他类型进程，但不如 Supervisor 或 Systemd 通用。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;适用场景&lt;/strong&gt;: Node.js 应用的生产环境部署和管理。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Docker/Kubernetes&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;优点&lt;/strong&gt;: 提供容器化环境，隔离性强，易于打包和分发，支持跨主机集群管理、自动伸缩、服务发现等。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缺点&lt;/strong&gt;: 学习曲线更陡峭，资源消耗相对较高，引入了新的抽象层。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;适用场景&lt;/strong&gt;: 微服务架构，需要标准化部署、弹性和跨云环境的复杂应用。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;选择建议&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果只需要在单台 Linux 服务器上管理几个应用进程，且追求简单快捷，&lt;strong&gt;Supervisor&lt;/strong&gt; 是个绝佳选择。&lt;/li&gt;
&lt;li&gt;如果你已经在使用现代 Linux 发行版，并且需要更强大的 OS 集成功能或管理系统级服务，学习并使用 &lt;strong&gt;Systemd&lt;/strong&gt; 是值得的。&lt;/li&gt;
&lt;li&gt;如果你主要开发和部署 &lt;strong&gt;Node.js&lt;/strong&gt; 应用，&lt;strong&gt;PM2&lt;/strong&gt; 提供了量身定做的优秀体验。&lt;/li&gt;
&lt;li&gt;如果你的应用需要部署在 &lt;strong&gt;容器&lt;/strong&gt; 环境或 &lt;strong&gt;集群&lt;/strong&gt; 中，&lt;strong&gt;Docker&lt;/strong&gt; 和 &lt;strong&gt;Kubernetes&lt;/strong&gt; 是更现代化的解决方案（注意：Supervisor 也可以在 Docker 容器 &lt;em&gt;内部&lt;/em&gt; 使用，用于管理容器内的多个进程，但这通常被认为是一种反模式，推荐一个容器只运行一个主进程）。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;11. 总结与最佳实践&lt;/h2&gt;
&lt;p&gt;Supervisor 是一个成熟、可靠且易于使用的进程控制系统，极大地简化了类 UNIX 系统上应用程序进程的管理。它通过自动启动、监控和重启，确保了服务的可用性。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;最佳实践回顾&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;使用非 Root 用户&lt;/strong&gt;: 在 &lt;code&gt;[program:x]&lt;/code&gt; 中始终指定 &lt;code&gt;user&lt;/code&gt; 为一个权限受限的用户。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;明确工作目录&lt;/strong&gt;: 使用 &lt;code&gt;directory&lt;/code&gt; 指定命令执行的上下文。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;管理日志&lt;/strong&gt;: 合理配置日志路径、大小和轮转 (&lt;code&gt;stdout_logfile&lt;/code&gt;, &lt;code&gt;stderr_logfile&lt;/code&gt;, &lt;code&gt;...maxbytes&lt;/code&gt;, &lt;code&gt;...backups&lt;/code&gt;)，或结合外部日志工具。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;优雅关闭&lt;/strong&gt;: 理解并配置好 &lt;code&gt;stopsignal&lt;/code&gt;, &lt;code&gt;stopwaitsecs&lt;/code&gt;, &lt;code&gt;stopasgroup&lt;/code&gt;, &lt;code&gt;killasgroup&lt;/code&gt;，特别是对于多进程应用。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;模块化配置&lt;/strong&gt;: 使用 &lt;code&gt;[include]&lt;/code&gt; 和 &lt;code&gt;conf.d&lt;/code&gt; 目录组织配置文件。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;环境变量&lt;/strong&gt;: 使用 &lt;code&gt;environment&lt;/code&gt; 传递配置，避免硬编码。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;监控与告警&lt;/strong&gt;: 利用 &lt;code&gt;supervisorctl status&lt;/code&gt;、日志、Web UI 或事件监听器进行监控。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;安全&lt;/strong&gt;: 如果启用 &lt;code&gt;inet_http_server&lt;/code&gt;，务必设置认证并使用防火墙或反向代理保护。&lt;code&gt;unix_http_server&lt;/code&gt; 的 socket 文件权限也要注意。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;理解 &lt;code&gt;autorestart&lt;/code&gt;&lt;/strong&gt;: &lt;code&gt;unexpected&lt;/code&gt; 选项通常比 &lt;code&gt;true&lt;/code&gt; 更安全，避免程序在正常退出（如完成任务）后被无限重启。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;reread&lt;/code&gt; 与 &lt;code&gt;update&lt;/code&gt;&lt;/strong&gt;: 掌握这两个命令，安全地更新配置。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;掌握 Supervisor，无疑会让你在应用部署和运维管理上更加得心应手。希望这篇从入门到精通的指南能帮助你充分利用 Supervisor 的强大功能！&lt;/p&gt;
</content:encoded></item><item><title>MkDocs Material 实践</title><link>https://bangwu.me/posts/mkdocs/</link><guid isPermaLink="true">https://bangwu.me/posts/mkdocs/</guid><description>MkDocs Material 从安装到部署的完整流程，包括主题配置、插件选择与团队协作中的文档发布实践。</description><pubDate>Tue, 25 Mar 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;为什么选择 MkDocs&lt;/h2&gt;
&lt;p&gt;写技术文档时，我们通常会在 VitePress、Docusaurus、MkDocs、Sphinx 之间挑选。MkDocs Material 兼顾了“写 Markdown 的丝滑体验”和“默认主题即是业界标杆”两件事：不用懂 React、Vue，也能写出交互友好的文档。更重要的是，它的配置文件精简易读，适合小团队快速搭建知识库和项目手册。&lt;/p&gt;
&lt;h2&gt;环境准备&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;安装 Python 3.9+，建议顺手安装 &lt;code&gt;pipx&lt;/code&gt; 方便管理命令行工具。&lt;/li&gt;
&lt;li&gt;创建独立虚拟环境，避免和项目依赖混在一起：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;python -m venv .venv
source .venv/bin/activate          # Windows 使用 .venv\Scripts\activate
pip install --upgrade pip
pip install mkdocs-material
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;初始化骨架：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;mkdocs new docs-site
cd docs-site
mkdocs serve                      # 默认 http://127.0.0.1:8000
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;MkDocs 会在 &lt;code&gt;docs/&lt;/code&gt; 目录下生成 Markdown 文件，并通过 &lt;code&gt;mkdocs.yml&lt;/code&gt; 管理导航、主题和插件。&lt;/p&gt;
&lt;h2&gt;配置文件要点&lt;/h2&gt;
&lt;p&gt;以下是一个常用的 &lt;code&gt;mkdocs.yml&lt;/code&gt; 示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;site_name: Awesome Docs
theme:
  name: material
  language: zh
  palette:
    - scheme: default
      primary: blue
      accent: light blue
  features:
    - navigation.tabs
    - navigation.sections
    - content.code.annotate
markdown_extensions:
  - admonition
  - pymdownx.superfences
  - pymdownx.tabbed:
      alternate_style: true
plugins:
  - search
  - blog
  - tags
nav:
  - 🏠 概览: index.md
  - 快速开始:
      - 安装: getting-started/install.md
      - 配置: getting-started/config.md
  - 进阶:
      - 主题定制: advanced/theme.md
      - 部署: advanced/deploy.md
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;features&lt;/code&gt; 中的选项可以开启目录分组、代码注释、粘性导航等 Material 主题特性。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;markdown_extensions&lt;/code&gt; 建议启用 &lt;code&gt;pymdownx&lt;/code&gt; 系列，支持代码标签、折叠块、键盘符号等增强语法。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;plugins&lt;/code&gt; 里常见的还有 &lt;code&gt;minify&lt;/code&gt;, &lt;code&gt;git-revision-date-localized&lt;/code&gt;, &lt;code&gt;awesome-pages&lt;/code&gt; 等，可根据项目体量再行添加。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Markdown 写作增强&lt;/h2&gt;
&lt;p&gt;Material 对 Markdown 语法进行了大量扩展，可以降低文档维护成本：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;提示块&lt;/strong&gt;：使用标准的 admonition 语法即可生成漂亮的提醒框。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;!!! note &quot;接口说明&quot;
    这里会展示接口返回的数据结构示例。
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;标签页&lt;/strong&gt;：通过 &lt;code&gt;pymdownx.tabbed&lt;/code&gt; 将多语言代码段组合在一起。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;=== &quot;Python&quot;
    ```py
    print(&quot;hello&quot;)
    ```
=== &quot;Go&quot;
    ```go
    fmt.Println(&quot;hello&quot;)
    ```
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;数据表格&lt;/strong&gt;：搭配 &lt;code&gt;pymdownx.tabular&lt;/code&gt; 或直接使用 Markdown 表格，Material 会自动添加滚动和排序能力。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;插入 Mermaid&lt;/strong&gt;：启用 &lt;code&gt;pymdownx.superfences&lt;/code&gt; 后，可直接书写流程图与时序图，文档即刻渲染。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;自定义主题与品牌化&lt;/h2&gt;
&lt;p&gt;当需要加入品牌色、字体或组件时，最简方案是改写 &lt;code&gt;docs/stylesheets/extra.css&lt;/code&gt; 并在配置中引入：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;extra_css:
  - stylesheets/extra.css
extra_javascript:
  - javascripts/analytics.js
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;配合 &lt;code&gt;mkdocs-material&lt;/code&gt; 的 &lt;code&gt;theme.custom_dir&lt;/code&gt; 参数可以覆写部分模板，实现首页英雄横幅、页脚版权信息等定制化。记得把公共变量抽进 &lt;code&gt;partials&lt;/code&gt;，避免每次升级主题时难以合并。&lt;/p&gt;
&lt;h2&gt;部署与持续集成&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;GitHub Pages&lt;/strong&gt;：执行 &lt;code&gt;mkdocs gh-deploy --force&lt;/code&gt; 即可发布到 &lt;code&gt;&amp;lt;username&amp;gt;.github.io&lt;/code&gt;。在 CI/CD 中可以使用官方提供的 &lt;a href=&quot;https://github.com/marketplace/actions/deploy-mkdocs&quot;&gt;GitHub Actions workflow&lt;/a&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;内网发布&lt;/strong&gt;：将 &lt;code&gt;mkdocs build&lt;/code&gt; 生成的 &lt;code&gt;site/&lt;/code&gt; 目录交给 Nginx/OSS/CDN 即可。推荐在流水线中添加 &lt;code&gt;pip install mkdocs-material[imaging]&lt;/code&gt; 提前生成图标、暗色主题资源。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;预览环境&lt;/strong&gt;：使用 &lt;code&gt;mkdocs serve -a 0.0.0.0:8000&lt;/code&gt;，让团队成员在局域网访问实时预览；配合 Docker 可以做到一行命令启动：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker run --rm -it -p 8000:8000 -v ${PWD}:/docs squidfunk/mkdocs-material
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;协同编辑流程&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;将 &lt;code&gt;docs/&lt;/code&gt; 配合 Conventional Commit 与 PR 检查流程管理，避免文档与代码脱节。&lt;/li&gt;
&lt;li&gt;利用 &lt;code&gt;mkdocs serve --dirtyreload&lt;/code&gt; 获得实时刷新体验，写作效率显著提升。&lt;/li&gt;
&lt;li&gt;为大型团队增加 &lt;code&gt;markdownlint&lt;/code&gt;、&lt;code&gt;typos&lt;/code&gt; 等校验工具，保证行文风格一致性。&lt;/li&gt;
&lt;li&gt;若文档与版本关联，使用 &lt;code&gt;mike&lt;/code&gt; 插件维护多版本站点，保留历史版本入口。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;常用资源&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://squidfunk.github.io/mkdocs-material/&quot;&gt;官方文档&lt;/a&gt;：每个 feature 都有 demo 代码，直接复用。&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/squidfunk/mkdocs-material/tree/master&quot;&gt;mkdocs-boilerplate&lt;/a&gt; 示例仓库：学习如何组织目录和导航。&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://diataxis.fr/&quot;&gt;Diátaxis 文档写作方法论&lt;/a&gt;：帮助我们区分教程、解释、操作指南与参考资料，合理拆分文档结构。&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docsearch.algolia.com/&quot;&gt;DocSearch&lt;/a&gt;：为公开站点申请免费全文检索服务，搜索体验对大型文档尤为重要。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Python 自动化浏览器实践</title><link>https://bangwu.me/posts/python-auto/</link><guid isPermaLink="true">https://bangwu.me/posts/python-auto/</guid><description>用 Python 驱动浏览器完成自动化测试、数据采集和脚本化操作——从 Selenium 到 Playwright 的实战经验总结。</description><pubDate>Thu, 20 Mar 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;为什么要让浏览器自动跑&lt;/h2&gt;
&lt;p&gt;日常测试、数据采集、批量操作后台系统时，手点页面既低效又容易出错。Python 生态提供了多种浏览器自动化方案，可以在保留真实渲染环境的情况下完成登录、数据抓取、导出报告等任务。&lt;/p&gt;
&lt;p&gt;常用框架对比：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;工具&lt;/th&gt;
&lt;th&gt;特点&lt;/th&gt;
&lt;th&gt;适合场景&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Selenium&lt;/td&gt;
&lt;td&gt;历史悠久，生态丰富，与各大浏览器兼容&lt;/td&gt;
&lt;td&gt;UI 自动化测试、旧系统兼容&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Playwright&lt;/td&gt;
&lt;td&gt;现代、API 简洁，自带浏览器管理&lt;/td&gt;
&lt;td&gt;多语言支持、高并发采集&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Requests + browsercookie&lt;/td&gt;
&lt;td&gt;无头访问，绕过浏览器成本&lt;/td&gt;
&lt;td&gt;已有 Cookie/Token、接口稳定&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;以下内容以 Selenium 与 Playwright 为主线，补充一些工程化实践。&lt;/p&gt;
&lt;h2&gt;环境准备&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;python -m venv .venv
source .venv/bin/activate  # Windows 使用 .venv\Scripts\activate
pip install --upgrade pip
pip install selenium~=4.21 webdriver-manager~=4.0 playwright~=1.44
playwright install chromium
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;webdriver-manager&lt;/code&gt; 可自动下载匹配版本的浏览器驱动；若内网环境，请预下载并配置 &lt;code&gt;PATH&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;Playwright 自带浏览器二进制，执行 &lt;code&gt;playwright install&lt;/code&gt; 即可。&lt;/li&gt;
&lt;li&gt;建议把这些安装写进 &lt;code&gt;requirements.txt&lt;/code&gt; 或 &lt;code&gt;pyproject.toml&lt;/code&gt;，方便 CI/CD 复现。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Selenium 抓取接口 Token&lt;/h2&gt;
&lt;p&gt;部分前端只在浏览器里发请求，需要我们在网络日志里捕获 Token。下方示例基于 Chrome DevTools Protocol：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import json
import time
import urllib.parse

from selenium import webdriver
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
from selenium.webdriver.support.wait import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By


def get_token(timeout: int = 15) -&amp;gt; str | None:
    caps = DesiredCapabilities.CHROME
    caps[&quot;goog:loggingPrefs&quot;] = {&quot;performance&quot;: &quot;ALL&quot;}

    options = webdriver.ChromeOptions()
    options.add_argument(&quot;--headless=new&quot;)
    options.add_argument(&quot;--disable-gpu&quot;)
    options.add_argument(&quot;--window-size=1280,720&quot;)

    driver = webdriver.Chrome(options=options, desired_capabilities=caps)
    target_url = &quot;https://vp.fact.qq.com/home&quot;
    api_pattern = &quot;/api/config/initial&quot;

    try:
        driver.get(target_url)
        WebDriverWait(driver, 10).until(
            EC.presence_of_element_located((By.CSS_SELECTOR, &quot;[data-page-ready]&quot;))
        )

        start_time = time.time()
        token = None

        while time.time() - start_time &amp;lt; timeout and not token:
            for log_item in driver.get_log(&quot;performance&quot;):
                message = json.loads(log_item[&quot;message&quot;])[&quot;message&quot;]
                if message.get(&quot;method&quot;) != &quot;Network.requestWillBeSent&quot;:
                    continue

                request = message[&quot;params&quot;][&quot;request&quot;]
                url = request.get(&quot;url&quot;, &quot;&quot;)
                if api_pattern in url:
                    parsed = urllib.parse.urlparse(url)
                    qs = urllib.parse.parse_qs(parsed.query)
                    token = qs.get(&quot;token&quot;, [None])[0]
                    break

        if token:
            print(&quot;捕获 Token:&quot;, token)
        else:
            print(&quot;未在超时时间内发现 Token&quot;)
        return token
    finally:
        driver.quit()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;要点归纳：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;新版 Chrome 建议启用 &lt;code&gt;--headless=new&lt;/code&gt;，兼容性更好。&lt;/li&gt;
&lt;li&gt;加 &lt;code&gt;WebDriverWait&lt;/code&gt; 确保页面加载完再监听日志。&lt;/li&gt;
&lt;li&gt;尝试控制循环超时时间，避免长时间阻塞或 CPU 飙升。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Playwright 的替代写法&lt;/h2&gt;
&lt;p&gt;Playwright 同样能抓取网络请求，并且 API 更直接：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from playwright.sync_api import sync_playwright


def fetch_token(target: str) -&amp;gt; str | None:
    with sync_playwright() as p:
        browser = p.chromium.launch(headless=True)
        page = browser.new_page()
        token_holder = {&quot;value&quot;: None}

        def handle_request(route):
            url = route.request.url
            if &quot;/api/config/initial&quot; in url:
                token_holder[&quot;value&quot;] = route.request.url.split(&quot;token=&quot;)[-1]
            route.continue_()

        page.route(&quot;**/api/config/initial*&quot;, handle_request)
        page.goto(target, wait_until=&quot;networkidle&quot;)
        browser.close()
        return token_holder[&quot;value&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;Playwright 能直接对特定请求做 &lt;code&gt;route&lt;/code&gt; 拦截，减少日志解析。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;wait_until=&quot;networkidle&quot;&lt;/code&gt; 避免页面仍在加载时就关闭浏览器。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;自动化常见任务清单&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;UI 回归测试&lt;/strong&gt;：搭配 &lt;code&gt;pytest&lt;/code&gt;，为关键流程编写断言，用 &lt;code&gt;pytest-xdist&lt;/code&gt; 并发执行。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据采集&lt;/strong&gt;：与 &lt;code&gt;pandas&lt;/code&gt;、&lt;code&gt;openpyxl&lt;/code&gt; 结合，把页面数据写出 Excel。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;批量操作&lt;/strong&gt;：后台管理系统重复操作（如批量审批、导入），可以沿用录制脚本。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;截图/录屏&lt;/strong&gt;：Selenium 可通过 &lt;code&gt;driver.save_screenshot&lt;/code&gt; 捕获截图；Playwright 原生支持录像 &lt;code&gt;page.video&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;下载管理&lt;/strong&gt;：Playwright 提供 &lt;code&gt;page.expect_download()&lt;/code&gt;；Selenium 需通过 Chrome 配置 &lt;code&gt;download.default_directory&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;工程化细节&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;结构化项目&lt;/strong&gt;：把页面元素封装为 Page Object，可读性和复用度更高。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;依赖管理&lt;/strong&gt;：CI 中将浏览器二进制缓存到镜像或制品仓库，缩短冷启动时间。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;日志与报告&lt;/strong&gt;：结合 &lt;code&gt;pytest-html&lt;/code&gt; 或 &lt;code&gt;allure&lt;/code&gt; 生成测试报告，失败时附带截图。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;定时调度&lt;/strong&gt;：在 Airflow/Prefect 下运行脚本，统一重试策略与告警。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Docker 化&lt;/strong&gt;：使用 &lt;code&gt;selenium/standalone-chrome&lt;/code&gt;、&lt;code&gt;mcr.microsoft.com/playwright/python&lt;/code&gt; 等镜像可避免宿主机缺依赖。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;合规与反爬注意事项&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;确认你有权访问目标数据，遵守网站 Robots 协议与法律法规。&lt;/li&gt;
&lt;li&gt;控制请求频率，通过 &lt;code&gt;time.sleep&lt;/code&gt; 或队列限流，避免触发风控。&lt;/li&gt;
&lt;li&gt;对于登录态相关脚本，及时处理 Cookie 过期和验证码；可结合第三方识别服务，但需评估安全风险。&lt;/li&gt;
&lt;li&gt;生产环境保管好账号、Token 等敏感信息，建议接入 Vault、Secrets Manager。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;进一步阅读&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.selenium.dev/documentation/webdriver/&quot;&gt;Selenium Docs&lt;/a&gt;：官方 WebDriver API 文档。&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://playwright.dev/python/docs/intro&quot;&gt;Playwright Documentation&lt;/a&gt;：详尽示例和调试技巧。&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://chromedevtools.github.io/devtools-protocol/&quot;&gt;Chrome DevTools Protocol&lt;/a&gt;：了解网络日志结构的最佳资料。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;本文侧重浏览器自动化（Selenium + Playwright），适合需要 JavaScript 渲染、登录模拟和复杂交互的场景。如果你只需要抓取静态 HTML 页面，推荐先看《一文解决 Python 爬虫》，更轻量也更高效。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>这个世界怎么了</title><link>https://bangwu.me/posts/whatiswrong/</link><guid isPermaLink="true">https://bangwu.me/posts/whatiswrong/</guid><description>大三下学期开始找实习，投了几十份简历石沉大海。这篇写的是那段自我怀疑的日子，和一些慢慢想明白的事。</description><pubDate>Tue, 11 Mar 2025 15:36:02 GMT</pubDate><content:encoded>&lt;p&gt;&amp;lt;audio controls data-title=&quot;虚拟&quot; data-artist=&quot;陈粒&quot; data-cover=&quot;https://cdn.bangwu.me/img/20260111w2e48-oz.webp&quot;&amp;gt;
&amp;lt;source src=&quot;https://cdn.bangwu.me/img/2026011193e34430.mp3&quot; type=&quot;audio/mpeg&quot;&amp;gt;
&amp;lt;/audio&amp;gt;&lt;/p&gt;
&lt;p&gt;最近在找实习，大三下学期了，加上自己本身也没有考研的打算（觉得考研就两个原因逃避工作和热爱科研，可惜两者我都不想要）笨拙地打开了社会的大门——实习。&lt;/p&gt;
&lt;h2&gt;投了几十份简历&lt;/h2&gt;
&lt;p&gt;我没做好准备...是的，我还无法面对工作带来的压力，无法从安逸的校园生活中快速切换到通勤、同事、汇报各种职场中，我也并不是在逃避 ，只是暂时还无法适应😔2.27补考成绩出了，过了，悬着的心终于放下然后就奋力投入到投简历、找实习中。2.28在 &lt;a href=&quot;https://rxresu.me&quot;&gt;https://rxresu.me&lt;/a&gt; 中创建了第一个简历开启了我的大学回顾生涯，简历上总要写点什么亮眼的东西吧？回想大学期间学到了什么，好像没一个拿得出手的项目，唯一能提得上的就是自己的爬虫技能还有Markdown（当然，这个肯定不算）剩下的还有什么，弄不清的人际关系还是时常情绪低落的自己。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.bangwu.me/img/202601110d418d34.jpg&quot; alt=&quot;bangwu20250311233546&quot; /&gt;&lt;/p&gt;
&lt;p&gt;那天是周五，下午有节课是我大学期间最喜欢的老师之一上的，记得她第一次给我们上课是大二下学期《创新思维与学术写作》一开始就被她的讲课风格所吸引，总是喜欢轻松愉快的上课氛围，还有能与学生开玩笑、互动，不免想起那时的我，一年前的自己...那时的自己总有一种自信，觉得这个世界上虽然有很难很难的事情，但是我棒无一定可以做得到参透其中的奥秘，于是结课论文就写了《基于 LangChain 的问答机器人设计——以保险定价策略建议问答机器人为例》经过这个论文也慢慢认定了论文格式会限制学术的发展，那学期，有许多奇妙的想法也有求知的热爱得以实现......&lt;/p&gt;
&lt;p&gt;3.5我完成了简历的初版，我的第一份简历，上面写着手机号，微信号，邮箱还有一个帅气的大头照。我在简历中说“热爱技术，有代码编写规范，有较强的自学能力，对新技术有较强的兴趣且持续关注行业动态，在相关论坛活跃，积极拥抱AI技术”事实上我只有这些了，剩下的就是一些Python，git，Linux之类的，项目经验拿的是大二时参加可视化竞赛做的一个项目，没有实习经历。可能简历中唯一显眼的就是学校吧，这也是我唯一能拿得出手的闪光点了。于是就到处海投，投了很多大厂的暑期实习，只要是base上海的满足基本要求的我都投了，事实只有大厂愿意给面试机会，小厂要的是直接能上手项目，有一点不满足要求就直接pass了，于是迎来了第一个笔试，第一个面试（字节-飞书测开）&lt;a href=&quot;https://base.bangwu.top/env/%E9%9D%A2%E7%BB%8F/&quot;&gt;面经&lt;/a&gt;
这里有我的笔试和面经，首先我清楚的一点是我没有算法基础，只会简单的排序算法当然进不了大厂的，面试只是为了多锻炼一下自己，希望给自己提个醒该努力了，感谢面试官的宝贵时间🙏&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.bangwu.me/img/202601116198d3d4.png&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;p&gt;最近几天都是比较焦虑的状态，直到中午面完试，所有的投递也都进入了待处理的状态，我也从忙碌中抽出身来第一件事就是睡觉休息，然后心里暗暗下定决心，不可否认的是我确实摆烂了很长很长时间，正如之前一个朋友说的，你选择了自学但是还是没坚持下去
我说这些不是博取同情，不要同情我，我是个可恨的人，也不是为了讲现在的我被工作殴打的千疮百孔，事实上我仍然能找到实习只是预期降低了，同样我现在仍然有着探索新事物的好奇心，在写这篇文章前我还浅浅了解了一下字节刚开源的跨端框架Lynx&lt;/p&gt;
&lt;p&gt;从前总觉得一味地背东西很low啊，上高中的时候看不起只知道套公式做题，死板的背语文答题模板的同学，上大学时看不起为了卷绩点多与老师互动，期末疯狂背知识点的学生，越来越觉得人生是一场持久的学习，现在觉得看不起自己，正如我曾七次鄙视自己的灵魂，当它本可进取时却故作谦卑。最近想到了好多以前的事情，不过只是叹息一下又迅速投入到找实习中&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.bangwu.me/img/2026011171da27da.png&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;p&gt;晚上洗澡的时候，和舍友聊天说找实习好难啊，聊着聊着就说大环境不好，我想起了那句“大环境不好大家都开始怀疑努力的意义了”这个世界怎么了，是啊，这个世界怎么了
焦虑中前进，大家共勉！&lt;/p&gt;
&lt;p&gt;“你是我未曾拥有无法捕捉的亲昵
我却有你的吻你的魂你的心
载着我飞呀飞呀飞 越过了意义”&lt;/p&gt;
</content:encoded></item><item><title>幸福一定是个陷阱吧</title><link>https://bangwu.me/posts/fear-happiness/</link><guid isPermaLink="true">https://bangwu.me/posts/fear-happiness/</guid><description>有时候不是不想要幸福，是怕得到之后又失去。这篇写的是那种「不敢太开心」的奇怪心理。</description><pubDate>Fri, 28 Feb 2025 01:50:10 GMT</pubDate><content:encoded>&lt;p&gt;&amp;lt;audio controls data-title=&quot;幸福了，然后呢&quot; data-artist=&quot;A-Lin&quot; data-cover=&quot;https://cdn.bangwu.me/img/20260111vwzdg-hw.webp&quot;&amp;gt;
&amp;lt;source src=&quot;https://cdn.bangwu.me/img/20260111bb8d7ced.mp3&quot; type=&quot;audio/mpeg&quot;&amp;gt;
&amp;lt;/audio&amp;gt;&lt;/p&gt;
&lt;p&gt;我有个不爱吃甜食的朋友，每次让他吃甜食，糖果之类的他就拒绝，说不爱吃甜的，问其原因也只是敷衍的说小时候糖吃多了，长大了就不爱吃了。是啊，他小时候特别爱吃糖，有记忆的时候嘴里就一直有着糖果，后来也不出所料得了蛀牙，牙疼。长大了一些，就记住了蛀牙带来的疼痛，空壳的牙齿一直在提醒他不要吃甜的，然后就习惯了，只是觉得甜的东西是多余了，这就好像我们生活中的幸福，记得瑞克和莫蒂里面有一个经典的桥段，只有少数强大的存在才惧怕幸福，幸福不可能持久。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.bangwu.me/img/202601114d0e04f0.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;当你经历过幸福的到来又失去，你就会不断怀疑幸福存在的真实性，当幸福再次到来的时候你只会觉得像是虚幻的泡沫，这就是为什么接受幸福比接受痛苦更需要勇气。
后来觉得难以接受的未来也慢慢接受了，我就在想，是不是生活中这些普通的日子慢慢把我们的理想，热情，斗志通通磨灭了，最后连同对未来的期待也一同丢尽了垃圾桶，进了焚烧炉。
每次写作的时候，我大部分都是情绪憋不住了，要写出来，好像这样，就可以把我未说出口的话讲给想要讲的人，那些看不到的人啊，你们能知道这些吗
现在我想问问一年后的自己，是否在工作了？每天重复的工作会觉得烦么？想问问三年后的自己，你现在回到老家了吗？买了自己的车了没有？还在玩光遇么...&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.bangwu.me/img/2026011133715432.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;我其实一点也不脆弱，那天晚上我告诉你我哭了，哭的一点也不撕心裂肺...后来你告诉我，觉得我哭的好莫名其妙 好离谱，从那一刻起，我就知道你在我心里从必不可少的人变成了可有可无的人了。只是，还会怀念以前的日子，但不会想回去了“人生若只如初见”&lt;/p&gt;
&lt;p&gt;当然，后来畏惧幸福，然后就失去了好多，比如对时间的感知，能自给自足的快乐，还有那份曾经轻而易举就能感受到的满足。幸福像是一把双刃剑，越是渴望，越是害怕失去，于是索性将自己封闭起来，假装不需要它。时间在指尖悄然流逝，却不再有那种“此刻即是永恒”的踏实感。日子变得机械，像是被设定好的程序，重复着同样的动作，却少了灵魂的参与。&lt;/p&gt;
&lt;h2&gt;试着接纳&lt;/h2&gt;
&lt;p&gt;曾经，一片落叶、一缕阳光、一句不经意的话，都能让我感到世界的美好。可如今，这些微小的快乐似乎被一层无形的屏障隔绝在外。我开始怀疑，是不是自己太过贪婪，想要得太多，才让幸福变得如此遥远。可后来才明白，幸福从来不是遥不可及的东西，而是我们是否愿意敞开心扉去接纳它。&lt;/p&gt;
&lt;p&gt;或许，畏惧幸福是因为害怕失去，害怕那种从高处跌落的痛楚。可生活本就是起伏不定的，没有人能永远站在顶峰，也没有人会永远沉在谷底。与其因为害怕失去而拒绝幸福，不如学会在拥有时珍惜，在失去时释然。毕竟，幸福不是一种状态，而是一种选择。&lt;/p&gt;
&lt;p&gt;我开始尝试重新找回那些被遗忘的快乐，比如在清晨醒来时，感受第一缕阳光洒在脸上的温暖；比如在忙碌之余，给自己一杯热茶，静静地品味片刻的宁静；比如在孤独时，学会与自己对话，倾听内心的声音。这些微小的瞬间，或许不足以改变生活的全部，但它们让我重新感受到，幸福其实一直都在，只是我们是否愿意看见它。&lt;/p&gt;
&lt;p&gt;于是，我告诉自己，不要再畏惧幸福。即使它会带来短暂的脆弱，即使它可能会在某一天消失，但至少，我曾真实地拥有过它。而这份拥有，足以让我的生命变得更加丰盈。&lt;/p&gt;
&lt;h2&gt;幸福不是陷阱&lt;/h2&gt;
&lt;p&gt;幸福既非甜蜜的牢笼，也不是虚幻的泡影。当我们停止用社会标尺丈量幸福，转而向内建立价值坐标系；当能欣赏阴晴圆缺的完整性，在耕耘中体会生命力的舒展——这种带着清醒觉知的幸福，或许就是破解陷阱魔咒的终极密码。就像黑塞在《悉达多》中写道的：「智慧无法言传。智者试图传授智慧，总像痴人说梦。」真正的答案，永远在每个人的躬身实践中。&lt;/p&gt;
&lt;p&gt;“幸福了，
然后呢，
爱情用什么再确认。”&lt;/p&gt;
</content:encoded></item><item><title>Python标准库——re</title><link>https://bangwu.me/posts/python-standard-re/</link><guid isPermaLink="true">https://bangwu.me/posts/python-standard-re/</guid><description>Python 标准库 re 模块的实战指南：从基础正则语法到编译优化、分组匹配与常见场景的代码模板。</description><pubDate>Tue, 25 Feb 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;正则&lt;/h2&gt;
&lt;p&gt;::github{repo=&quot;any86/any-rule&quot;}&lt;/p&gt;
&lt;p&gt;Python 的 &lt;code&gt;re&lt;/code&gt; 库是用于处理正则表达式的核心库，提供字符串匹配、替换、分割等功能。以下是其常用用法及示例：&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;&lt;strong&gt;1. 基本匹配&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;re.match(pattern, string)&lt;/code&gt;&lt;/strong&gt;
从字符串&lt;strong&gt;开头&lt;/strong&gt;匹配，返回第一个匹配对象；失败返回 &lt;code&gt;None&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import re
result = re.match(r&apos;hello&apos;, &apos;hello world&apos;)
if result:
    print(result.group())  # 输出: hello
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;re.search(pattern, string)&lt;/code&gt;&lt;/strong&gt;
在字符串中&lt;strong&gt;全局搜索&lt;/strong&gt;第一个匹配项。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;result = re.search(r&apos;world&apos;, &apos;hello world&apos;)
print(result.group())  # 输出: world
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;&lt;strong&gt;2. 查找所有匹配&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;re.findall(pattern, string)&lt;/code&gt;&lt;/strong&gt;
返回所有匹配的字符串列表。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;matches = re.findall(r&apos;\d+&apos;, &apos;2021年7月7日，2002年02月03日&apos;)
print(matches)  # 输出: [&apos;2021&apos;, &apos;7&apos;, &apos;7&apos;, &apos;2002&apos;, &apos;02&apos;, &apos;03&apos;]
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;re.finditer(pattern, string)&lt;/code&gt;&lt;/strong&gt;
返回迭代器，逐个生成匹配对象（适合处理大文本）。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for match in re.finditer(r&apos;\d+&apos;, &apos;2021年7月7日&apos;):
    print(match.group())  # 依次输出: 2021, 7, 7
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;&lt;strong&gt;3. 字符串替换&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;re.sub(pattern, repl, string)&lt;/code&gt;&lt;/strong&gt;
替换匹配的字符串。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;text = &quot;dawdada&quot;
text_new = re.sub(r&apos;a&apos;, &apos;%&apos;, text)  # 替换所有 &apos;a&apos; 为 %
print(text_new)  # 输出: d%w%d%d%

# 使用分组反向引用（如 \1 代表第一个分组）
para = re.sub(r&apos;([！。])([%@])&apos;, r&apos;\1分隔\2&apos;, &quot;hell。！@%o&quot;)
print(para)  # 输出: hell。分隔！@%o
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;&lt;strong&gt;4. 预编译正则表达式&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;re.compile(pattern)&lt;/code&gt;&lt;/strong&gt;
预编译正则表达式提升复用效率。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pattern = re.compile(r&apos;\d+&apos;)
matches = pattern.findall(&apos;2021年7月7日&apos;)
print(matches)  # 输出: [&apos;2021&apos;, &apos;7&apos;, &apos;7&apos;]
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;&lt;strong&gt;5. 分割字符串&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;re.split(pattern, string)&lt;/code&gt;&lt;/strong&gt;
根据正则表达式分割字符串。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;parts = re.split(r&apos;[,\s]+&apos;, &apos;apple, banana  cherry&apos;)
print(parts)  # 输出: [&apos;apple&apos;, &apos;banana&apos;, &apos;cherry&apos;]
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;&lt;strong&gt;6. 分组与捕获&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;()&lt;/code&gt; 分组&lt;/strong&gt;
提取子模式内容。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;text = &quot;2021年7月7日&quot;
match = re.search(r&apos;(\d+)年(\d+)月(\d+)日&apos;, text)
if match:
    print(match.groups())  # 输出: (&apos;2021&apos;, &apos;7&apos;, &apos;7&apos;)
    print(match.group(1))  # 输出: 2021
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;&lt;strong&gt;7. 常用正则符号&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;\d&lt;/code&gt;：匹配数字（等价于 &lt;code&gt;[0-9]&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;\w&lt;/code&gt;：匹配字母、数字、下划线。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;\s&lt;/code&gt;：匹配空白字符（空格、换行等）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.&lt;/code&gt;：匹配任意字符（除换行外）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;*&lt;/code&gt;：匹配前一个字符 0 次或多次。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;+&lt;/code&gt;：匹配前一个字符 1 次或多次。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;?&lt;/code&gt;：匹配前一个字符 0 次或 1 次。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;{n}&lt;/code&gt;：匹配前一个字符恰好 n 次。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;&lt;strong&gt;8. 标志参数&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;re.IGNORECASE&lt;/code&gt;（&lt;code&gt;re.I&lt;/code&gt;）：忽略大小写。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;re.MULTILINE&lt;/code&gt;（&lt;code&gt;re.M&lt;/code&gt;）：多行模式。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;re.DOTALL&lt;/code&gt;（&lt;code&gt;re.S&lt;/code&gt;）：允许 &lt;code&gt;.&lt;/code&gt; 匹配换行符。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;text = &quot;Hello\nWorld&quot;
matches = re.findall(r&apos;^[a-z]+&apos;, text, flags=re.I | re.M)
print(matches)  # 输出: [&apos;Hello&apos;, &apos;World&apos;]
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;&lt;strong&gt;示例整合&lt;/strong&gt;&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;import re

# 示例1：查找日期
text = &quot;日期：2023-12-25，2024-01-01&quot;
dates = re.findall(r&apos;\d{4}-\d{2}-\d{2}&apos;, text)
print(dates)  # 输出: [&apos;2023-12-25&apos;, &apos;2024-01-01&apos;]

# 示例2：替换电话号码
text = &quot;电话：123-4567-8900&quot;
new_text = re.sub(r&apos;(\d{3})-(\d{4})-(\d{4})&apos;, r&apos;(\1) \2-\3&apos;, text)
print(new_text)  # 输出: 电话：(123) 4567-8900
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;&lt;strong&gt;注意事项&lt;/strong&gt;&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;正则表达式需注意&lt;strong&gt;贪婪匹配&lt;/strong&gt;（如 &lt;code&gt;.*&lt;/code&gt; 默认匹配到最长）。&lt;/li&gt;
&lt;li&gt;复杂正则可添加注释使用 &lt;code&gt;re.VERBOSE&lt;/code&gt; 标志提高可读性。&lt;/li&gt;
&lt;li&gt;处理中文时建议使用 &lt;code&gt;re.UNICODE&lt;/code&gt; 标志。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;使用原始字符串&lt;/strong&gt;：正则表达式中常用原始字符串（如 r&apos;\n&apos;），避免反斜杠转义问题。例如，r&quot;\n&quot; 表示 \n，而 &quot;\n&quot; 表示换行符。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;性能考虑&lt;/strong&gt;：对于频繁使用的模式，优先使用 re.compile() 预编译。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;复杂任务的替代&lt;/strong&gt;：对于如 HTML 解析或深度邮箱验证，re 库可能不够 robust，建议使用专门库（如 Beautiful Soup 或 email-validator）&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;通过灵活组合这些方法，可以高效处理字符串匹配、清洗和提取任务。&lt;/p&gt;
</content:encoded></item><item><title>陪安东尼度过漫长岁月——橙、黄</title><link>https://bangwu.me/posts/antony-orange/</link><guid isPermaLink="true">https://bangwu.me/posts/antony-orange/</guid><description>陪安东尼度过漫长岁月系列第二本。橙与黄篇里那些关于成长、告别和继续往前走的话，摘下来放在这里。</description><pubDate>Fri, 14 Feb 2025 09:39:48 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;橙——陪安东尼度过漫长岁月Ⅱ&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;◆ 总是莫名地觉得幸福 尽管我还不知道幸福是个什么东西&lt;/p&gt;
&lt;p&gt;◆ 用拒人千里之外的冷酷来掩饰自己试图与人亲近的热血沸腾 这算完美人格 还是闷骚？&lt;/p&gt;
&lt;p&gt;◆ 是贪婪的人 渴望被爱 被拥抱 被理解 被接受又是自私的人 拒绝去爱 去尝试 去解释 去接纳&lt;/p&gt;
&lt;p&gt;◆ 有的时候会隐隐约约地觉得 想要一直待在这城市 结婚生子 慢慢地老去 平静而安稳的一生&lt;/p&gt;
&lt;p&gt;◆ 我一直一直在想你 思念带来前所未有的甜蜜&lt;/p&gt;
&lt;p&gt;◆ 生活是一场又一场美好事物的 追逐&lt;/p&gt;
&lt;p&gt;◆ 我们都很 仔细地思考 定义过 所谓幸福的生活 不过 我们都没有认真地 活但是 我又会觉得 没有认真生活没有什么 也会觉得 偶尔难过失落也没有什么嗯 如果有你在的话&lt;/p&gt;
&lt;p&gt;◆ 之后的几年 或许 出现别的事 别的人 感觉就是 也可以啊&lt;/p&gt;
&lt;p&gt;◆ 好吧 我们就这样走下去 看看还有什么……&lt;/p&gt;
&lt;h2&gt;关于成长&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;橙——陪安东尼度过漫长岁月Ⅱ&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;◆ 我想 将来如果 有天 尝试写小说 会写一个男生 他总是和 开满花的树 合照 他不笑 也不悲伤 他只是笔挺地站在那里&lt;/p&gt;
&lt;p&gt;◆ 有一天 我会忘记你 我没有很期待 也没有觉得失落我只是 知道 有那么一天&lt;/p&gt;
&lt;p&gt;◆ 好了现在 我没有什么 秘密 可以告诉你了&lt;/p&gt;
&lt;p&gt;◆ 你不知道我如此爱你 和 我不知道我如此爱你 你觉得哪个 更言情&lt;/p&gt;
&lt;p&gt;◆ 如果你有1000万，你怎么用？省着用&lt;/p&gt;
&lt;p&gt;◆ 二月去日本 有些话如果现在不说 以后就再也不会说了&lt;/p&gt;
&lt;p&gt;◆ 还是应该像现在这样 默默地看着你还要说些什么&lt;/p&gt;
&lt;p&gt;◆ 我在想 这次与你一别 还要什么时候能见面&lt;/p&gt;
&lt;p&gt;◆ 嗯 我想说 其实 我很喜欢 每一次 去找你的时候 的 自己&lt;/p&gt;
&lt;p&gt;◆ 好像你离开家了以后 才知道自己多想家 多爱父母&lt;/p&gt;
&lt;p&gt;◆ tnd 没有 文学小青年 那么高产 却沾染了 一身文学小青年的坏毛病&lt;/p&gt;
&lt;p&gt;◆ if you go away far enough,then you are on the way home.&lt;/p&gt;
&lt;p&gt;◆ 有的 时候 我真的很 鄙视 工作效率极低的自己&lt;/p&gt;
&lt;p&gt;◆ 你拥有你的, 我拥有我的, 盛开。你适合你的, 我适合我的, 垂败。&lt;/p&gt;
&lt;p&gt;◆ 有一些爱 像 pola相机 或者已经停产的700相纸 用一点儿少一点儿 我似乎能听到它下降的声音&lt;/p&gt;
&lt;p&gt;◆ 你说 以后要一直在一起哦 以后 不能因为一些小事就不理我哦 有那么几秒 我百分之百地认真 相信了&lt;/p&gt;
&lt;p&gt;◆ 亲爱的不二 会不会 你也不是很了解我？&lt;/p&gt;
&lt;p&gt;◆ 当初 我让你 吃了我的时候 你就应该吃了我&lt;/p&gt;
&lt;p&gt;◆ 你眼里 我的缺点太多了 多到看不到 我这么喜欢你&lt;/p&gt;
&lt;p&gt;◆ 我不喜欢一个人的 时候 就对他 很客气&lt;/p&gt;
&lt;p&gt;◆ 你毫不掩饰地 自私着 再 这样下去 我如何讨你欢心&lt;/p&gt;
&lt;p&gt;◆ 不知道 如何爱你 看着你 是我唯一 的方式&lt;/p&gt;
&lt;p&gt;◆ 如何向喜欢的人表白？……盯着&lt;/p&gt;
&lt;p&gt;◆ 如果是100%的人的话，不管多么晚才发现自己的心意，兜兜转转绕了一个圈回来，你也刚好站在那里。&lt;/p&gt;
&lt;p&gt;◆ 现在 执著追求的事 将来必定有一天变成不 重要的&lt;/p&gt;
&lt;p&gt;◆ 亲爱的不二啊 你说怎么 想要一生一世的人 最后 都变成了 一期一会呢&lt;/p&gt;
&lt;p&gt;◆ 那一次之后 你就从 不可或缺的人 变成 可有可无的人了&lt;/p&gt;
&lt;p&gt;◆ 那么多人 都等着和你做朋友 你就 move on好了 不过这也好 如果你问我 我一定会选错&lt;/p&gt;
&lt;p&gt;◆ 嗯 丢东西 很可怕 可是 丢东西 不知道怎么丢的 更让人郁闷&lt;/p&gt;
&lt;p&gt;◆ 后来 后来 就像 很多人说的 时间 让我们淡忘了 那个人&lt;/p&gt;
&lt;p&gt;◆ 总之 会一直写下去 一定会 一直写下去&lt;/p&gt;
&lt;p&gt;◆ 我是 很不喜欢圣诞—— 世界上 最不喜欢圣诞的 除了 火鸡 一定 还有我&lt;/p&gt;
&lt;p&gt;◆ 亲爱的 总有一些时候 是 寂寞的 寂寞的&lt;/p&gt;
&lt;p&gt;◆ 亲爱的 最近 总有些时候 我在想 我所害怕的是 即使我使尽全力 用最快的速度一直跑 一直跑 也到不了你那里&lt;/p&gt;
&lt;p&gt;◆ 恋爱中的 兔崽子 很不像白羊 变得 患得患失&lt;/p&gt;
&lt;p&gt;◆ 可是 我担心 我不喜欢 不再喜欢你的自己&lt;/p&gt;
&lt;p&gt;◆ 很难因为别人 喜欢 而喜欢 也不会 因为别人 恨 而恨&lt;/p&gt;
&lt;p&gt;◆ 只是每次遇到喜欢的人 才会 患得患失 把自己 贬低进尘埃 伸手 向别人借 人品&lt;/p&gt;
&lt;p&gt;◆ 恍惚间 隐约想着 应该 趁着年轻 和喜欢的人一起 制造些 比夏天还要温暖的事&lt;/p&gt;
&lt;p&gt;◆ 恋爱中 应有的嘴脸 再厉害的人也逃不过&lt;/p&gt;
&lt;p&gt;◆ 要找一个以后会一起的人 虽然我不是缺点多多 但是也不完美 真的找到对的人谈何容易 但是 总是觉得一定会找到那个人 一定&lt;/p&gt;
&lt;p&gt;◆ 那些真的在你生命里 发光 让你反复回味的 它们都是 好的&lt;/p&gt;
&lt;p&gt;◆ 我很好 我很好 只是说不出加油啊 因为觉得自己没有立场 也做不出那样的动作了 尽管我一直想回到过去 将你紧紧抱住 紧紧抱住&lt;/p&gt;
&lt;p&gt;◆ 常常觉得山穷水尽了 也会知道不久便会变得风生水起 可是 不停揣摩 纠结萦绕的爱 还是不知道何去何从&lt;/p&gt;
&lt;p&gt;◆ 我觉得 我们俩之间就像喝酒 我干杯 你随意&lt;/p&gt;
&lt;p&gt;◆ 而且你看 即使 身体不记得 那心也会 记得吧 心 也是一块肌肉吧 我想&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;黄——陪安东尼度过漫长岁月Ⅲ&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;◆ 亲爱的不二 抱着你睡觉的时候也不觉得彻底地拥有你&lt;/p&gt;
&lt;p&gt;◆ 所以啊 我就在想 人生 之所以珍贵 说不定 不是我们去过的 一个又一个 精彩华丽的 旅游胜地 而是 旅途之中 寂寞 无聊 不可打发的时间里 有你陪伴吧&lt;/p&gt;
&lt;p&gt;◆ 其实吧 他骂我 我也没啥 后来他可能也想通了 就过来和我说&lt;/p&gt;
&lt;p&gt;◆ 当你看到这篇日志的时候 不用 怀疑 我说的就是你&lt;/p&gt;
&lt;p&gt;◆ 有些东西 你知道 就算扔掉了 它的痕迹扔不掉 所以就一直带着&lt;/p&gt;
&lt;p&gt;◆ 亲爱的 不二 我们又要开始新的生活咯意气风发 意气风发～&lt;/p&gt;
&lt;p&gt;◆ 如果我的手臂有 这么长&amp;gt;---------------------------------O--------------------------------------&amp;lt;一定紧紧抱住你们 地球很难逃离 但是可以尝试一起 Jump&lt;/p&gt;
&lt;p&gt;◆ 那些 陪我一起傻过的人啊 你们在我身上 留下了什么我该死心塌地地找回你 还是要微笑后坚定转身 从此变得聪明&lt;/p&gt;
&lt;p&gt;◆ 之前那个我 和现在的我之间的距离 被许多细小琐碎的事填充着这些都是 你给我的 爱吧&lt;/p&gt;
&lt;p&gt;◆ 希望我们 心脏很强 胆子很大 好好恋爱 荒唐放纵 最后找到真爱&lt;/p&gt;
&lt;p&gt;◆ 2006年3月11日 来澳洲 来的时候怕超重 箱子里放的被子床单 电饭煲 字典 父母朋友的祝福……2010年3月22日 回中国 走的时候怕箱子超重 箱子里放的照片 相机 书 菜单 刀 和 梦一般的记忆&lt;/p&gt;
&lt;p&gt;◆ 你好 我就要飞了 我一点儿也不酷 也不特别 所以我不会说很酷的话 只想说请不要忘记我&lt;/p&gt;
&lt;p&gt;◆ 2010／4／23　［看不出来 我的无奈］&lt;/p&gt;
&lt;p&gt;◆ 过去已经不在了 将来也不想考虑 只想 此时此刻 为了你任性的勇敢&lt;/p&gt;
&lt;p&gt;◆ 看我的书 你的书 遇到感动的地方就记录下来 散播出去&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;黄——陪安东尼度过漫长岁月Ⅲ&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;◆ 你会追问 你怎么了 我就习惯性笑着说没什么 其实真的没什么 我习惯把事情装在心里 有的时候装着装着就没了&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;【序】&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;◆ 困惑到底怎样才能把工作做得更好，到底怎样才能实现梦想，到底怎样才能获得周围人的喜欢，到底怎样才能成功，到底怎样才能活得更出色……&lt;/p&gt;
&lt;p&gt;◆ 有时候要忍不住咬牙切齿地想，世界是打定主意要对自己不好了，要捏许多优秀得多的精英出现，来衬托我们平凡人活得有多么事倍功半&lt;/p&gt;
&lt;p&gt;◆ 当我们在一天一天变平凡，一天一天在与困惑的角逐中失利，一天一天老去，世界开始灰败下来，上面覆盖着厚厚的挣脱不成后的苦果，而就算到了这个时候，我想安东尼仍然是带着彩虹颜色的。&lt;/p&gt;
&lt;p&gt;◆ 不要愁老之将至，你老了一定很可爱。而且，假如你老了十岁，我当然也同样老了十岁，世界也老了十岁，上帝也老了十岁，一切都是一样。&lt;/p&gt;
&lt;p&gt;◆ 希望以后有一天 有人说 你变了的时候 我坦然地知道 我没变 或者 变成更好的人了&lt;/p&gt;
&lt;p&gt;◆ 亲爱的不二 人的一生有很多个感觉好极了 也会经历很多感觉糟透了不过其实 这些都没什么 不用看的很重很多年 以后当你回忆过去的时候 我敢保证 那些好极了和糟透了的时刻你都会记不清了&lt;/p&gt;
&lt;p&gt;◆ 唯一真实 和让你骄傲的 是你挺胸抬头走过的人生&lt;/p&gt;
&lt;p&gt;◆ 她唱 “天上人间 如果真值得歌颂 也是因为有你才变得闹哄哄” 这让我想起来我和 我的那群朋友们 一路走来 有你真好&lt;/p&gt;
&lt;p&gt;◆ 不过又一想 写陪也不是为了这些的 写陪只是因为想写而已 不是为了陪别人 不是为了找人陪 就是单单纯纯的想写而已 想到这里就觉得 以后说不定还会写下去&lt;/p&gt;
&lt;p&gt;◆ 不过最让我感觉难过的 不是一年后你还不确定你对我的感情 而是 我不再确定你就是那个对的人了&lt;/p&gt;
&lt;p&gt;◆ 在遇到能像喜欢你这么喜欢的人之前 还是好好喜欢自己吧 因为和任何人一起 只要没有像喜欢你这么喜欢 我都没法再见到你 所以这段时间就好好喜欢自己吧&lt;/p&gt;
&lt;p&gt;◆ 人们经常说 it will get better 我觉得也不一定 maybe it will get worse 不过我相信 it gets easier&lt;/p&gt;
&lt;p&gt;◆ 一个人 是在相信的时候比较勇敢还是不信的时候呢？&lt;/p&gt;
&lt;p&gt;◆ 有时候我 明明很想你也不说 当你发 how are you 的时候我也不回 在心里纠结了一天 睡觉前 才回复一条 I am OK&lt;/p&gt;
&lt;p&gt;◆ 不过感情这 东西要看得多透呢 也许看不透比较有意思吧&lt;/p&gt;
&lt;p&gt;◆ 我要爱 眼神 表情 每一个仔细推敲的短信 改了又改的邮件 适时的殷勤 为了你一次次的迁徙 当然也会遇到之上各种各样的人 遭遇各种各样的对待 不过那样要来的爱总是不长久 捧在手里却暖不了心&lt;/p&gt;
&lt;p&gt;◆ 可能我就是喜欢 喜欢一个人很久 或者 喜欢被一个人喜欢很久 但没有他喜欢我那么喜欢他 但还是喜欢&lt;/p&gt;
&lt;p&gt;◆ 受伤的心 假以时日也不会复原的 但是你会意识到 受伤的那块只是一小部分 很小很小的一部分&lt;/p&gt;
&lt;p&gt;◆ 说不好怎样形容上海 我觉得他很有包容性 任何国家 任何背景的人都可以按照自己的方式在这里活着 互不干扰&lt;/p&gt;
&lt;h2&gt;那些反复读的句子&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;【后记】&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;◆ 二十岁开始时候的自己 充满元气横冲直闯快要三十岁的时候的自己 经常抽离肉身 站得远远的审视自己 想这是我么 这是我想要的人生么 爱情是这个样子么 生活只是这个样子么&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;前篇：《陪安东尼度过漫长岁月——红》。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>Nextjs初探</title><link>https://bangwu.me/posts/nextjs-first/</link><guid isPermaLink="true">https://bangwu.me/posts/nextjs-first/</guid><description>第一次接触 Next.js 的学习笔记：从项目创建、路由系统、SSR/SSG 到部署，适合 React 开发者快速入门。</description><pubDate>Fri, 14 Feb 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Nextjs&lt;/h2&gt;
&lt;p&gt;Nextjs现在成为react官方推荐的第一框架，虽然偏向的是全栈框架，但是其性价比非常高，一个框架可以直接做出一个完整的项目，是个人开发者的不二之选&lt;/p&gt;
&lt;h2&gt;Learn&lt;/h2&gt;
&lt;p&gt;Next.js 的 &lt;strong&gt;App Router&lt;/strong&gt;（从 v13 开始引入）是一种基于文件系统的新型路由方案，结合 TypeScript (&lt;code&gt;.tsx&lt;/code&gt;) 使用能提供更好的类型安全，同时我比较推荐从&lt;a href=&quot;https://nextjs.org/learn&quot;&gt;Nextjs Learn&lt;/a&gt;开始入门&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;&lt;strong&gt;1. 项目初始化&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;使用 TypeScript 模板创建项目：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npx create-next-app@latest --ts
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;选择启用 App Router（默认选项）。&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;&lt;strong&gt;2. App Router 核心结构&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;项目目录结构：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;app/
├── layout.tsx       # 全局布局
├── page.tsx         # 首页（对应路由 `/`）
├── dashboard/
│   ├── layout.tsx   # 嵌套布局
│   └── page.tsx     # `/dashboard`
└── blog/
    ├── [slug]/
    │   └── page.tsx # 动态路由 `/blog/:slug`
    └── page.tsx     # `/blog`
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;&lt;strong&gt;3. 基础页面与布局&lt;/strong&gt;&lt;/h3&gt;
&lt;h4&gt;&lt;strong&gt;全局布局 (&lt;code&gt;app/layout.tsx&lt;/code&gt;)&lt;/strong&gt;&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;import type { ReactNode } from &apos;react&apos;;

export default function RootLayout({
  children,
}: {
  children: ReactNode;
}) {
  return (
    &amp;lt;html lang=&quot;en&quot;&amp;gt;
      &amp;lt;body&amp;gt;{children}&amp;lt;/body&amp;gt;
    &amp;lt;/html&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;&lt;strong&gt;首页 (&lt;code&gt;app/page.tsx&lt;/code&gt;)&lt;/strong&gt;&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;export default function Home() {
  return &amp;lt;h1&amp;gt;Hello, Next.js!&amp;lt;/h1&amp;gt;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;&lt;strong&gt;4. 动态路由与 TypeScript&lt;/strong&gt;&lt;/h3&gt;
&lt;h4&gt;&lt;strong&gt;动态路由示例 (&lt;code&gt;app/blog/[slug]/page.tsx&lt;/code&gt;)&lt;/strong&gt;&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;interface PageProps {
  params: { slug: string };
  searchParams?: { [key: string]: string | string[] | undefined };
}

export default function BlogPage({ params }: PageProps) {
  return &amp;lt;div&amp;gt;Blog Slug: {params.slug}&amp;lt;/div&amp;gt;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;&lt;strong&gt;生成动态路由参数 (&lt;code&gt;generateStaticParams&lt;/code&gt;)&lt;/strong&gt;&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;export async function generateStaticParams() {
  const posts = await fetch(&apos;https://api.example.com/posts&apos;).then((res) =&amp;gt; res.json());

  return posts.map((post: { id: string }) =&amp;gt; ({
    slug: post.id,
  }));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;&lt;strong&gt;5. 数据获取与类型安全&lt;/strong&gt;&lt;/h3&gt;
&lt;h4&gt;&lt;strong&gt;服务端数据获取 (&lt;code&gt;fetch&lt;/code&gt; + TypeScript)&lt;/strong&gt;&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;interface Post {
  id: string;
  title: string;
  content: string;
}

async function getPost(slug: string): Promise&amp;lt;Post&amp;gt; {
  const res = await fetch(`https://api.example.com/posts/${slug}`);
  if (!res.ok) throw new Error(&apos;Failed to fetch&apos;);
  return res.json();
}

export default async function BlogPage({ params }: PageProps) {
  const post = await getPost(params.slug);
  return (
    &amp;lt;article&amp;gt;
      &amp;lt;h1&amp;gt;{post.title}&amp;lt;/h1&amp;gt;
      &amp;lt;p&amp;gt;{post.content}&amp;lt;/p&amp;gt;
    &amp;lt;/article&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;&lt;strong&gt;6. 嵌套布局与类型&lt;/strong&gt;&lt;/h3&gt;
&lt;h4&gt;&lt;strong&gt;嵌套布局 (&lt;code&gt;app/dashboard/layout.tsx&lt;/code&gt;)&lt;/strong&gt;&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;import type { ReactNode } from &apos;react&apos;;

export default function DashboardLayout({
  children,
}: {
  children: ReactNode;
}) {
  return (
    &amp;lt;div&amp;gt;
      &amp;lt;nav&amp;gt;Dashboard Navbar&amp;lt;/nav&amp;gt;
      {children}
    &amp;lt;/div&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;&lt;strong&gt;7. 客户端组件与交互&lt;/strong&gt;&lt;/h3&gt;
&lt;h4&gt;&lt;strong&gt;客户端组件 (&lt;code&gt;app/components/Counter.tsx&lt;/code&gt;)&lt;/strong&gt;&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;&apos;use client&apos;; // 必须标记为客户端组件

import { useState } from &apos;react&apos;;

export default function Counter() {
  const [count, setCount] = useState(0);
  return (
    &amp;lt;div&amp;gt;
      &amp;lt;button onClick={() =&amp;gt; setCount(count + 1)}&amp;gt;+&amp;lt;/button&amp;gt;
      &amp;lt;span&amp;gt;{count}&amp;lt;/span&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;&lt;strong&gt;8. API 路由 (可选)&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;创建 API 路由 (&lt;code&gt;app/api/hello/route.ts&lt;/code&gt;)：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { NextResponse } from &apos;next/server&apos;;

export async function GET() {
  return NextResponse.json({ name: &apos;John Doe&apos; });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;访问 &lt;code&gt;/api/hello&lt;/code&gt; 即可调用。&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;&lt;strong&gt;9. 类型增强配置&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;在 &lt;code&gt;tsconfig.json&lt;/code&gt; 中添加 Next.js 类型：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;compilerOptions&quot;: {
    &quot;types&quot;: [&quot;next&quot;, &quot;next/types&quot;]
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;&lt;strong&gt;10. 实用技巧&lt;/strong&gt;&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;路由跳转&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import Link from &apos;next/link&apos;;

&amp;lt;Link href=&quot;/blog/123&quot;&amp;gt;Read Blog&amp;lt;/Link&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;流式传输&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { Suspense } from &apos;react&apos;;

&amp;lt;Suspense fallback={&amp;lt;div&amp;gt;Loading...&amp;lt;/div&amp;gt;}&amp;gt;
  &amp;lt;AsyncComponent /&amp;gt;
&amp;lt;/Suspense&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;环境变量&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const apiUrl = process.env.NEXT_PUBLIC_API_URL;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h3&gt;&lt;strong&gt;常见问题&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;类型错误&lt;/strong&gt;：确保为 &lt;code&gt;params&lt;/code&gt; 和 &lt;code&gt;searchParams&lt;/code&gt; 定义正确的接口。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;客户端组件限制&lt;/strong&gt;：在客户端组件中不能直接使用服务端数据获取方法（如 &lt;code&gt;getStaticProps&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;动态导入&lt;/strong&gt;：使用 &lt;code&gt;next/dynamic&lt;/code&gt; 导入客户端组件。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;&lt;strong&gt;官方资源&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Next.js App Router 文档：&lt;a href=&quot;https://nextjs.org/docs/app&quot;&gt;App Router Docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;TypeScript 配置指南：&lt;a href=&quot;https://nextjs.org/docs/pages/building-your-application/configuring/typescript&quot;&gt;TypeScript Guide&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;next-auth&lt;/h2&gt;
&lt;p&gt;在 Next.js 中使用 &lt;code&gt;next-auth&lt;/code&gt; 实现鉴权的完整指南（支持 App Router 和 TypeScript）：&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;&lt;strong&gt;步骤 1：安装依赖&lt;/strong&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;pnpm install next-auth @next-auth/prisma-adapter @prisma/client bcrypt
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;&lt;strong&gt;步骤 2：配置环境变量&lt;/strong&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# .env
NEXTAUTH_SECRET=&quot;your-secure-secret&quot; # openssl rand -base64 32 生成
NEXTAUTH_URL=&quot;http://localhost:3000&quot;

# GitHub OAuth 示例
GITHUB_CLIENT_ID=&quot;your-client-id&quot;
GITHUB_CLIENT_SECRET=&quot;your-client-secret&quot;

# 数据库配置（以 PostgreSQL 为例）
DATABASE_URL=&quot;postgresql://user:password@localhost:5432/mydb&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;&lt;strong&gt;步骤 3：创建 Prisma Schema（数据库集成）&lt;/strong&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// prisma/schema.prisma
generator client {
  provider = &quot;prisma-client-js&quot;
}

datasource db {
  provider = &quot;postgresql&quot;
  url      = env(&quot;DATABASE_URL&quot;)
}

model User {
  id            String    @id @default(cuid())
  name          String?
  email         String?   @unique
  emailVerified DateTime?
  image         String?
  accounts      Account[]
  sessions      Session[]
}

model Account {
  id                 String  @id @default(cuid())
  userId             String
  type               String
  provider           String
  providerAccountId  String
  refresh_token      String?  @db.Text
  access_token       String?  @db.Text
  expires_at         Int?
  token_type         String?
  scope              String?
  id_token           String?  @db.Text
  session_state      String?
  user               User    @relation(fields: [userId], references: [id], onDelete: Cascade)

  @@unique([provider, providerAccountId])
}

model Session {
  id           String   @id @default(cuid())
  sessionToken String   @unique
  userId       String
  expires      DateTime
  user         User     @relation(fields: [userId], references: [id], onDelete: Cascade)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行 &lt;code&gt;npx prisma generate&lt;/code&gt; 生成客户端。&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;&lt;strong&gt;步骤 4：配置 NextAuth (App Router)&lt;/strong&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// app/api/auth/[...nextauth]/route.ts
import NextAuth from &quot;next-auth&quot;;
import GitHubProvider from &quot;next-auth/providers/github&quot;;
import { PrismaAdapter } from &quot;@next-auth/prisma-adapter&quot;;
import { PrismaClient } from &quot;@prisma/client&quot;;

const prisma = new PrismaClient();

export const {
  handlers: { GET, POST },
  auth, // 用于中间件获取会话
  signIn,
  signOut,
} = NextAuth({
  adapter: PrismaAdapter(prisma),
  providers: [
    GitHubProvider({
      clientId: process.env.GITHUB_CLIENT_ID!,
      clientSecret: process.env.GITHUB_CLIENT_SECRET!,
    }),
  ],
  secret: process.env.NEXTAUTH_SECRET,
});
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;&lt;strong&gt;步骤 5：全局 SessionProvider&lt;/strong&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// app/layout.tsx
import type { ReactNode } from &quot;react&quot;;
import { SessionProvider } from &quot;next-auth/react&quot;;
import { auth } from &quot;@/auth&quot;;

export default async function RootLayout({
  children,
}: {
  children: ReactNode;
}) {
  const session = await auth();

  return (
    &amp;lt;html lang=&quot;en&quot;&amp;gt;
      &amp;lt;body&amp;gt;
        &amp;lt;SessionProvider session={session}&amp;gt;{children}&amp;lt;/SessionProvider&amp;gt;
      &amp;lt;/body&amp;gt;
    &amp;lt;/html&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;&lt;strong&gt;步骤 6：创建登录/登出组件&lt;/strong&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// components/AuthButton.tsx
&quot;use client&quot;;

import { signIn, signOut } from &quot;@/auth&quot;;
import { useSession } from &quot;next-auth/react&quot;;

export function AuthButton() {
  const { data: session } = useSession();

  return (
    &amp;lt;div&amp;gt;
      {session ? (
        &amp;lt;button onClick={() =&amp;gt; signOut()}&amp;gt;Sign Out&amp;lt;/button&amp;gt;
      ) : (
        &amp;lt;button onClick={() =&amp;gt; signIn(&quot;github&quot;)}&amp;gt;Sign In with GitHub&amp;lt;/button&amp;gt;
      )}
    &amp;lt;/div&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;&lt;strong&gt;步骤 7：保护路由（中间件）&lt;/strong&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// middleware.ts
import { auth } from &quot;@/auth&quot;;

export default auth((req) =&amp;gt; {
  if (!req.auth) {
    return Response.redirect(new URL(&quot;/login&quot;, req.nextUrl));
  }
});

// 配置需要保护的路径
export const config = {
  matcher: [&quot;/dashboard/:path*&quot;, &quot;/profile&quot;],
};
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;&lt;strong&gt;步骤 8：服务端获取会话&lt;/strong&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// app/dashboard/page.tsx
import { auth } from &quot;@/auth&quot;;

export default async function DashboardPage() {
  const session = await auth();

  if (!session) {
    return &amp;lt;div&amp;gt;Unauthorized&amp;lt;/div&amp;gt;;
  }

  return &amp;lt;div&amp;gt;Welcome {session.user?.name}&amp;lt;/div&amp;gt;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;&lt;strong&gt;步骤 9：自定义登录页面&lt;/strong&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// app/login/page.tsx
&quot;use client&quot;;

import { signIn } from &quot;@/auth&quot;;

export default function LoginPage() {
  return (
    &amp;lt;div&amp;gt;
      &amp;lt;button onClick={() =&amp;gt; signIn(&quot;github&quot;)}&amp;gt;GitHub 登录&amp;lt;/button&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;&lt;strong&gt;关键配置说明&lt;/strong&gt;&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;适配器选择&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用 &lt;code&gt;PrismaAdapter&lt;/code&gt; 将用户数据存入数据库&lt;/li&gt;
&lt;li&gt;支持其他适配器：Firebase、MongoDB 等&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;OAuth 回调 URL&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在 GitHub 开发者设置中添加回调 URL：
&lt;code&gt;http://localhost:3000/api/auth/callback/github&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Session 策略&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;session: {
  strategy: &quot;jwt&quot;, // 或 &quot;database&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h3&gt;&lt;strong&gt;常见问题解决&lt;/strong&gt;&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;类型错误&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// next-auth.d.ts
import &quot;next-auth&quot;;

declare module &quot;next-auth&quot; {
  interface Session {
    user: {
      id: string;
    } &amp;amp; DefaultSession[&quot;user&quot;];
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;中间件不生效&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;确保 &lt;code&gt;matcher&lt;/code&gt; 路径正确&lt;/li&gt;
&lt;li&gt;检查中间件文件是否在项目根目录&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;生产环境配置&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;设置 &lt;code&gt;NEXTAUTH_URL&lt;/code&gt; 为生产域名&lt;/li&gt;
&lt;li&gt;使用 HTTPS&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h3&gt;&lt;strong&gt;扩展功能&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;邮箱登录&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import EmailProvider from &quot;next-auth/providers/email&quot;;

providers: [
  EmailProvider({
    server: process.env.EMAIL_SERVER,
    from: process.env.EMAIL_FROM,
  }),
]
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;自定义认证页面&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;theme: {
  logo: &quot;/logo.png&quot;,
  brandColor: &quot;#000&quot;,
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;角色鉴权&lt;/strong&gt;：
在 &lt;code&gt;User&lt;/code&gt; 模型中添加 &lt;code&gt;role&lt;/code&gt; 字段，通过中间件校验。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;p&gt;文档参考：&lt;a href=&quot;https://next-auth.js.org&quot;&gt;NextAuth.js Documentation&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;解决国内网络登录显示不成功的问题&lt;/h3&gt;
&lt;p&gt;&amp;lt;script src=&quot;https://gist.github.com/markbang/f411fcf140e130d9a4aff79e0a891706.js&quot;&amp;gt;&amp;lt;/script&amp;gt;&lt;/p&gt;
</content:encoded></item><item><title>我时常觉得你们是大人</title><link>https://bangwu.me/posts/skyfriends/</link><guid isPermaLink="true">https://bangwu.me/posts/skyfriends/</guid><description>在光遇里认识了一群朋友，虽然没见过面，但陪伴是真的。这篇是写给他们的，也是写给那段时光的。</description><pubDate>Wed, 29 Jan 2025 12:08:05 GMT</pubDate><content:encoded>&lt;p&gt;&amp;lt;audio controls data-title=&quot;若青春可以重来&quot; data-artist=&quot;张叶蕾&quot; data-cover=&quot;https://cdn.bangwu.me/img/20260111w809d-g7.webp&quot;&amp;gt;
&amp;lt;source src=&quot;https://cdn.bangwu.me/img/202601116cbb180f.mp3&quot; type=&quot;audio/mpeg&quot;&amp;gt;
&amp;lt;/audio&amp;gt;&lt;/p&gt;
&lt;h2&gt;在光遇里遇见&lt;/h2&gt;
&lt;p&gt;此篇献给光遇中的朋友们&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.bangwu.me/img/202601112f6ac86a.webp&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;p&gt;起初是一个突然想玩光遇，这个我听过但没玩过的游戏，有点随性的我就立即下载了。在暑假，我想“这游戏一定有好多好玩的小孩子吧”🤔第一天中午，在云巢和一个小黑互扔玩偶扔了好长时间。后面就不出所料，和所有萌新一样，开图的时候我选择了点击问号请求帮助，好像这个游戏里的人都喜欢帮助萌新，不用我自己跑了好像还不错&lt;/p&gt;
&lt;p&gt;于是，就是你。你喜欢抓萌新玩，最初我加的是你的小号，只是第一次被抓到时聊了一会天，记得当时你指着小王子星球那边说这是朕为你打下的江山，后来遇到的你会弹好多歌，总会孜孜不倦的过试炼、跑图。记得有一次不小心叫了你人机🤕，然后这就成了我的称号。情劫，后来后来有许多事情不在我脑海里了，但我还记得你问我选什么科目的时候，你问我名字的时候，在我的小屋你和你崽告诉我高中的事情的时候，还有玩真心话大冒险的时候，周末放假回来玩的时候......你是很仔细的人啊&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.bangwu.me/img/20260111f9fc595f.webp&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;p&gt;你是她的崽，一开始我不知道你的名字，只有备注情劫的崽，后来问你的名字，你说“不知道你不会问吗”“司雩”遇到你时经常和你监护待在一起，你总是仗着她欺负我🤕后来开学了，我希望你在学校要好好的，fighting！偶尔重合的假期时间，你和你监护一定在见不到的日子里想念着彼此 觉得每次你们在一块的时候，都会很快乐，运动会去食堂吃饭的时间也可以上线一起玩会，真不错😌 你是很坚强的人啊&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.bangwu.me/img/2026011118469cf8.webp&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;p&gt;你说再过一个多月你就 17 岁了，不知道日期但希望你岁岁平安。遇见你时是国庆节，我承认一开始是玩抽象加的你，那天你说“好开心”于是就每人送一颗心，后来就熬了许多夜，口头禅是“累了累了”你说抽象是你的保护色，🤔我觉得没错。阿淮，后来觉得你对遇到的每个人都很好，希望你是幸运的。后来发生了好多好多事情，音乐桌上听的歌，遇境的留影，小号开的挂......你是很伶俐的人啊&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.bangwu.me/img/20260111b64d6521.webp&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;p&gt;我们都会讨论高中的困苦，朋友的不理解，有些一成不变的事比如几年后的高中毕业，会过完的2025，高中的学习等等，但我希望这些一成不变的事情里有你们还是监护和崽崽，我们还是朋友，考试慢慢取得好的成绩，良好的同学关系还有自己内心的丰腴&lt;/p&gt;
&lt;p&gt;“坟墓里寂静无比，埋葬你的是所有你未说出口的话”我无法预知后来会变成什么样子，不过刻舟求剑，变好、变坏、还是再见，我都接受，但青春不可以重来
他们说青春是场盛大的逃亡，逃出苦难向春山。我想了想，想告诉你们向左向右还是随意散步都可以的，青春弥足珍贵，不知道我们未来会遇到某某但现在已经遇到了彼此......新年不是一瞬间，是度过了去年的种种，希望我们都能找到自己的选择，开心快乐！&lt;/p&gt;
&lt;p&gt;“为什么青春会偷偷溜走”
“为什么回忆变得不够用”&lt;/p&gt;
</content:encoded></item><item><title>早交卷？什么时候交卷</title><link>https://bangwu.me/posts/when/</link><guid isPermaLink="true">https://bangwu.me/posts/when/</guid><description>身边的人陆续恋爱、结婚，好像大家都在赶场。这篇想说的是：爱情不是什么考试，不用急着交卷。</description><pubDate>Sun, 12 Jan 2025 12:15:23 GMT</pubDate><content:encoded>&lt;p&gt;&amp;lt;audio controls data-title=&quot;晚风&quot; data-artist=&quot;7opy / BT07&quot; data-cover=&quot;https://cdn.bangwu.me/img/20260111w0y3k-z4.webp&quot;&amp;gt;
&amp;lt;source src=&quot;https://cdn.bangwu.me/img/202601116d2b4214.mp3&quot; type=&quot;audio/mpeg&quot;&amp;gt;
&amp;lt;/audio&amp;gt;&lt;/p&gt;
&lt;p&gt;我有个舍友，大二上学期一天夜里，我们在公园散步，他对我说他有个女朋友。震惊之余，思绪难免落到自己身上，每天大大咧咧的我偶尔抽象，嗯，在大一的时候也是一个经常患得患失的人呢&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.bangwu.me/img/20260111e99e76d0.png&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;p&gt;后来，我和他说一些心里的秘密。
我时常觉得，男女之间没有纯友谊，后来我看到了他，好像有女朋友的人也没被管的太宽泛，也可以和其他女生一起打游戏，那么，在玩的时候你在想些什么呢，这是爱么
我似乎想要遇到的每个人都喜欢我，让我干什么只要不太过分，都行。emm这样会不会觉得太卑微了，后来我不这样想了，做自己想做的吧，想要的东西，想做的事情，想追的某某，就去做吧，不管别人怎么样了也没那么多精力来管别人了，先管好自己吧！&lt;/p&gt;
&lt;p&gt;亲爱的X，我现在不敢掏出真心对人了，很少用“最”来形容自己遇到的人或事了，没有什么必须要做的，也没有什么放不下的。但我遇到你时，也会是这样吗，我向你说非你不可的时候，你也会说非我不可吗，我对你付出的时候你也会想着对我付出吗，你也会很害羞的问我的想法吗？&lt;/p&gt;
&lt;h2&gt;爱情不是考试&lt;/h2&gt;
&lt;h2&gt;不用急着交卷&lt;/h2&gt;
&lt;p&gt;对于爱情这门考试，我已经不急着交卷了，后来，也慢慢变成，不想交卷了。身边人的爱情，喜剧也好，悲剧也罢，轰轰烈烈我不会内心燥热，平平静静我也不会觉得无比幸福，就这样吧。目前，对学业上的考试我也有这种想法了，无论考的好坏了，都不着急，爱谁谁交卷吧&lt;/p&gt;
&lt;p&gt;一些“厉害的人”都有自己的“道”，无论是前端大牛的一些性能为王，用户多的项目才是好项目之类的，又或是有钱人的唯利益之道，任何事物都用价值来衡量 ，这些都没错。人是要有自己的道的，无论说出来与否，都要给自己定下一个道，例如一切影响我喝牛奶的人或事都要抛弃，又或是坚持自己的本心，永远不被世俗侵袭。有的很荒唐，对吧，但有的也太过理想，不过亲爱的，没关系的，有道就行，有道，我们就站在生活的摇摆船上了，不是吗？我们已经在生活了呀，坚持自己的道！&lt;/p&gt;
&lt;p&gt;“将近的晚风快吹干整条小巷，燃尽的灯光无法再将我们点亮”&lt;/p&gt;
</content:encoded></item><item><title>燃尽</title><link>https://bangwu.me/posts/todie/</link><guid isPermaLink="true">https://bangwu.me/posts/todie/</guid><description>2024 年的最后一天：上午期末考试，下午给朋友写信，晚上吃了顿烤肉。没什么特别的，但就是想记下来。</description><pubDate>Tue, 31 Dec 2024 15:01:30 GMT</pubDate><content:encoded>&lt;p&gt;今天是2024年12月31日，为什么要特别强调一下日期呢，对，不光是因为是今年的最后一天了，以后也不会再有2024年了，这不是一篇年终总结，当然，只是分享一下我今天的小事。&lt;/p&gt;
&lt;h2&gt;期末、书信和烤肉&lt;/h2&gt;
&lt;p&gt;周二，当然要上课，不同的是今天要考试，期末考试。嗯，越来越对考试没有感觉了，没想到是以这种方式拜托了考试综合征，我甚至都不想看一下那些知识点即使知道要考这些，不知道为什么，越来越提不起兴趣了，像是“蝉要在地下埋18年然后出来叫两个月，死去”燃尽自己最后的热情。考前在和舍友一起对答一下知识点，我们四个都没怎么复习，连背都不想背了，然后就随随便便过了好几遍。&lt;/p&gt;
&lt;p&gt;在考试的过程中，emmm还算可以，总之能写的都写上去了，临时背的也都还算用上了。有个知识点是UML关系中的聚合和组合关系，这两个在考试的时候突然弄混淆了，分不清那个是属于关系了，然后选择题就随便选了一个，选的聚合。不过没想到选错了，可能那三分本就不属于我吧。&lt;/p&gt;
&lt;p&gt;年终的第一封信是Follow给的，也准备寄出去一封信，送给我远方的朋友。一直都觉得一封信是有必要的，来讲述我这一年的点点滴滴，也正式的长篇大论一下，不是在聊天框里面输入，而是一封信，它可以是一个沾满了泪渍的白纸，也可以是一个精美的页面，或是礼物下压着的那封“你开心吗”。对的，要对你说的话想来想去是你我的记忆。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.bangwu.me/img/20260111d25704f5.png&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;p&gt;晚上想去外面吃，就和室友约好。嗯，最后就我和另一个室友去的，吃烤肉，好吃😋
&lt;img src=&quot;https://cdn.bangwu.me/img/2026011158500ab1.png&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;p&gt;69两个的玩偶
&lt;img src=&quot;https://cdn.bangwu.me/img/20260111b35994e3.png&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;p&gt;嗯，没什么了，一切安好，燃尽......&lt;/p&gt;
&lt;p&gt;新年快乐，大家&lt;/p&gt;
</content:encoded></item><item><title>陪安东尼度过漫长岁月——红</title><link>https://bangwu.me/posts/antony-red/</link><guid isPermaLink="true">https://bangwu.me/posts/antony-red/</guid><description>陪安东尼度过漫长岁月第一本。红篇里关于孤独、喜欢和不舍的句子，是我反复读了很多遍的部分。</description><pubDate>Sun, 22 Dec 2024 06:22:29 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;红——陪安东尼度过漫长岁月Ⅰ&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;◆ Every now and then sunshine is warm making life extremely long.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;序&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;◆ 他偶尔会发一些短信给大家，虽然是中文，虽然我们都认识那些字，但是那些字组合起来的意思，并没有人可以了解。比如“我要带你去飞行”……你说这算什么？一起从楼顶跳下去么……&lt;/p&gt;
&lt;h2&gt;关于孤独&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;红——陪安东尼度过漫长岁月 I&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;◆ 有的时候会隐隐约约地觉得 想要一直待在这城市 结婚生子 慢慢地老去 平静而安稳的一生 我喜欢的女歌手 又一次结婚 好像怀孕了 还要退出歌坛 她说 如果有一天我不再唱歌了 希望你们忘记我 怎么忘记？ 夜半做梦 梦到喜欢的人 早上醒来 忽然想到那个女歌手唱的歌 她唱 思念是一种很玄的东西 如影随形 我喜欢的一句话 ——有时候阳光很好 有时候阳光很暗 这就是生活
睡了
晚安&lt;/p&gt;
&lt;p&gt;◆ 只是那些喜欢过的人还会再遇见么 那些感动过的事还会再怀念么&lt;/p&gt;
&lt;p&gt;◆ 我不怕浪费钱 我没有那么多钱浪费 我只怕我的那么多的感情你都不在乎&lt;/p&gt;
&lt;p&gt;◆ 那天喝剩的啤酒瓶 留在海边 我们都没收&lt;/p&gt;
&lt;p&gt;◆ 一开始你就特别 从眼神就很体贴 我们都 不穿鞋 光着脚穿越耳语流言 在这之前 我 到底是谁 你 出现我 眼前 一瞬间 一切都改变&lt;/p&gt;
&lt;p&gt;◆ 比起深邃的东西我更喜欢肤浅的 不过偶尔深邃一下也许并不坏&lt;/p&gt;
&lt;p&gt;◆ 我在想 就算我知道了这些 一定还有一些你默默地为我做了我却不知道的吧&lt;/p&gt;
&lt;p&gt;◆ 我们总是一起出现在校园里 食堂 图书馆 教室 车站我偶尔会想 现在你一个人做这些事情的时候是什么样子呢&lt;/p&gt;
&lt;p&gt;◆ 为什么快乐也会流下眼泪 灌溉了我的荒野 开满了玫瑰 我不累 我不睡 我不休息 我不合眼 我不想浪费 每一秒 在这 有你 的世界&lt;/p&gt;
&lt;p&gt;◆ 是不是一个人一生只能说10000次我爱你 所以有些人怎么也不肯说了&lt;/p&gt;
&lt;p&gt;◆ 我们常听到的话 不表示就值得相信很可能是那些 懒惰的人 随口说说而已&lt;/p&gt;
&lt;p&gt;◆ 等车的时候 我吃了剩下的那个橘子 我到澳大利亚以后吃的第一个橘子&lt;/p&gt;
&lt;p&gt;◆ 亲爱的不二 我会觉得 人生是没有什么非要做不可以的 也没有什么不可以放弃的&lt;/p&gt;
&lt;p&gt;◆ 那天在 MSN上 我对你说 我爱你过了 六秒 你回消息说 谢谢你那一刻 我忽然觉得很糟糕&lt;/p&gt;
&lt;p&gt;◆ 好吧 我原谅你了 你走吧 这样可以了么？还是 连我对你的原谅你都觉得 来得太晚了 而不那么有效呢？&lt;/p&gt;
&lt;p&gt;◆ 冬天的时候 我没有特别想家 偶尔想你&lt;/p&gt;
&lt;p&gt;◆ 会很认真地觉得 如果是春天来了的话 那么我 安东尼 也要开始欣欣向荣了&lt;/p&gt;
&lt;p&gt;◆ 我深知 快乐 没有寂寞长久坚强可是再怎么也没料到 还没到保质期 它就开始不安 腐烂&lt;/p&gt;
&lt;p&gt;◆ 原来我是看到虫了 原来我的世界永远都没有真正的黑暗呀嘿嘿 嘿嘿嘿&lt;/p&gt;
&lt;p&gt;◆ 就这样 十一月的时候 我有了一只猫或者 应该说 猫 有了我又或许不是 拥有的关系 而是 安东尼 开始 和一只猫 一起生活&lt;/p&gt;
&lt;p&gt;◆ 不二啊 那些我们觉得理所应当的事情 不一定是对的&lt;/p&gt;
&lt;p&gt;◆ 这样 他就又可以按时 看日落 思考他和玫瑰之间的事情了&lt;/p&gt;
&lt;p&gt;◆ 如果我说 我现在的生活 很辛苦 是前所未有的艰难他们会不会 觉得我身在福中不 知福呢&lt;/p&gt;
&lt;p&gt;◆ 不过 老子这么年轻 吃点苦没什么最困难的已经过去了 最快乐的还没来 哦耶&lt;/p&gt;
&lt;p&gt;◆ 我问她 你在陌生的国家 陌生街道 看陌生风景的时候 脑袋里都在想什么呢？&lt;/p&gt;
&lt;p&gt;◆ 意识比身体坚强语言比心绪理智&lt;/p&gt;
&lt;p&gt;◆ 本来以为 快乐不会长久 怎知 一日比一日快乐 ——这话 真是又贱 又幸福&lt;/p&gt;
&lt;p&gt;◆ 不二说 它喜欢可以在第一句就介绍清楚 时间 地点 人物的作者&lt;/p&gt;
&lt;p&gt;◆ 我一直这么觉得 不过寂寞的时候做什么都显得不自然 所以 随它去吧&lt;/p&gt;
&lt;p&gt;◆ 不二：鳄鱼无法伸舌头安东尼：怪不得 它总是哭&lt;/p&gt;
&lt;p&gt;◆ 我们的意识存在于我们的肉体之内 我们的肉体之外 有另一个世界 这一关系性 常常给我们带来痛苦 迷惘 悲伤和分裂&lt;/p&gt;
&lt;p&gt;◆ 我在想你觉得很可笑 我在每一个地方 想你 人很多的时候想你 自己一个人的时候 你更是成了思想的主角 这样的你 让我羡慕&lt;/p&gt;
&lt;p&gt;◆ 你看 有些人 不明白 就是怎么也不明白有些人 明白了 他们不会问我 不二到底是什么&lt;/p&gt;
&lt;p&gt;◆ 而且 说给你听的 那些话 那些 不明白的人 估计看了也没有任何意义&lt;/p&gt;
&lt;p&gt;◆ 亲爱的不二 我们又要 搬家了 这次 要搬到山上想说 还好 有你&lt;/p&gt;
&lt;p&gt;◆ 脑袋在放空 于是 即使是我这样 记性不好的人 也很莫名地想起来很早之前的事&lt;/p&gt;
&lt;p&gt;◆ 所谓人生 便是取决于 遇见谁&lt;/p&gt;
&lt;p&gt;◆ 给你写东西的时候 不能加亲爱的 因为 我们没到那个程度 你知道&lt;/p&gt;
&lt;p&gt;◆ 我想 可能是 因为 有些人 有些事 有些地方 一旦离开 就回不去了 或者应该说 总觉得 自己回不去了&lt;/p&gt;
&lt;p&gt;◆ 我的旅游方式是 到一个地方 选一个好宾馆 然后开始睡觉 醒来就出门找个好饭店 吃饱以后开始乱逛 完全没有计划 随意且慵懒 可以放空&lt;/p&gt;
&lt;p&gt;◆ 其实 我没有很想去 那么多地方 我只想和你&lt;/p&gt;
&lt;p&gt;◆ 我觉得 我是特意想把自己弄醉的 然后 后来的事情都不记得了&lt;/p&gt;
&lt;p&gt;◆ 那些 我们一直好奇 而又有一些 惴惴不安的未来有的时候 在我心里 隐隐约约地 感觉到它们是 明亮的&lt;/p&gt;
&lt;p&gt;◆ 没有糗事的人 真没劲呢&lt;/p&gt;
&lt;p&gt;◆ 他只是 出现在我生活里的 一个新的说谎者&lt;/p&gt;
&lt;p&gt;◆ 不二 有的时候 我会觉得我 有点色 &amp;gt; &amp;lt;&lt;/p&gt;
&lt;p&gt;◆ 我们总是很容易觉得别人幸福 觉得自己可怜&lt;/p&gt;
&lt;p&gt;◆ 看到 灯塔的时候 我会很自然地想起你 只是 现在 我觉得 就算有天 你不再指引我了 我也能找到回去的路&lt;/p&gt;
&lt;h2&gt;后记&lt;/h2&gt;
&lt;p&gt;◆ 我觉得呢 漫长就是 如果一辈子的时间 都用来做后记&lt;/p&gt;
&lt;p&gt;◆ 这样 如此寂寞地 我也长大了&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;注：我想，我慢慢喜欢上这个温柔的少年了&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;续篇：《陪安东尼度过漫长岁月——橙、黄》。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>CET与我的故事</title><link>https://bangwu.me/posts/cet/</link><guid isPermaLink="true">https://bangwu.me/posts/cet/</guid><description>四级考了三次才过，六级到现在还没过。这篇不是经验贴，是一个拖延症患者关于英语和态度的自白。</description><pubDate>Mon, 16 Dec 2024 16:56:59 GMT</pubDate><content:encoded>&lt;p&gt;&amp;lt;audio controls data-title=&quot;阴天快乐&quot; data-artist=&quot;陈奕迅&quot; data-cover=&quot;https://cdn.bangwu.me/img/20260111vqxgz-xy.webp&quot;&amp;gt;
&amp;lt;source src=&quot;https://cdn.bangwu.me/img/2026011164c019b0.mp3&quot; type=&quot;audio/mpeg&quot;&amp;gt;
&amp;lt;/audio&amp;gt;&lt;/p&gt;
&lt;h2&gt;六级还在路上&lt;/h2&gt;
&lt;p&gt;前几天在英语六级的考场，写完了试卷，坐在那里我就想，嗯，我应该写一篇文章来说说此事——态度，何时出发。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.bangwu.me/img/202601110ad86ed1.jpg&quot; alt=&quot;IMG_20241214_141002&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;四级考了三次&lt;/h2&gt;
&lt;p&gt;整个CET考试生涯是从大二上学期开始的，虽然学校从大一下学期就可以报名了，我没报，当时想的是反正时间还长，而且我这学期课还多，不想在期末周还去考个四级(不过也可能最大的原因是害怕自己过不了)没错，我是一个外地的考生，来到上海上大学时，第一个感受就是这里的英语太难学了，强度好高:记得大一的英语读写和听说课我都是被老师捞上来的，感谢老师🙏🙏学习热情熄灭的时间段，而且还是最讨厌的英语，我怎么可能去背单词，去学英语。我时常觉得英语是一个很神奇的科目，甚至整个人类语言系统都是很神奇的，因为你就算是学习了很长时间，遇到不会的单词，不会就是不会也只能猜出来个大概意思。于是就导致了英语整门课就变得很“随机”而且对于一个需要长年累月积累的学科来说，我更加会选择“运气”这条路，英语对我来说就是能过就行，能听懂就行，实在不行有翻译。&lt;/p&gt;
&lt;p&gt;之前就会刷到一些大学生自律视频，都是离不开早起背单词，嗯，早起第一件事都是背单词，可我呢，根本没有早起这件事，早在睡梦中度过了。不清楚英语背单词为什么会和自律挂上钩，这是需要长久的坚持没错，也要吃很多难背的苦，但总觉得，背了就是背了，不能背了直接听懂老外讲话了吧，不能背了直接可以入职高薪吧......没错，当你准备做任何一件让你痛苦但是又没那么重要的事情的时候，就是会有那么一个声音劝你不要去做。
然后大一就在自娱自乐中过去了，背单词数：小于100，到了大二，再不报CET就说不过去了吧，已经逃避一次了。我记得点击付款按钮的那一刻，我下定决心要坚持背单词，却又在付款完成后的3分钟把这件事忘得一干二净，就好像我刚刚只是点了一个外卖🤕记得那天去考四级的路上，我还是很有信心的，毕竟还是有点底子，我就在想，我与英语纠缠久，不知道从什么时候染上的中二病，学英语时也总说“wtf”“come on，ples”“What&apos;s wrong with you？”“what are you looking for？”之类的话语，我承认，学习这些语句是很简单的，也能随口说出来，但我确是一遍又一遍地重复着，可能觉得这样很帅吧。&lt;/p&gt;
&lt;p&gt;英语四级就这样考过了，我不知道文章中的大部分意思，听力一点也没听懂，但我还是及格了，迎来了CET6，有意思的是，在大学有一个说法是“六级刷分”因为要刷到高分，接下来的大学生涯不出意外每年都会考两次六级，所以根本不担心第一次考不过这件事，无论如何，下次还是要去考的。又一次抱着侥幸心理，这次，幸运女神没站在我这边，差十几分及格。我不能坦然接受这个事实，对的，我没努力也没期待，但是我还是不能接受这个事实，不能接受这个失败的标志，像是我人生的污点。&lt;/p&gt;
&lt;p&gt;这一切不都是我自找的吗？是，没错。于是暑假的时候，我给自己定了个背单词的目标，主要还是过考试，其次是要看很多英文文档，多会点单词没坏处。我对自己说这次你能行的，我当时都能想象到自己在家背单词的情景，还有看英文文档时无阻的喜悦，我是个爱幻想的小丑。到了开学的那天，我一个单词也没背，一点自控力没有。&lt;/p&gt;
&lt;p&gt;下次在做吧，太累了这次；明天再弄吧，今天先休息；从下周开始，我一定要好好学习...有时候态度可能会决定一点东西，正如我对写作的态度是——余生要坚持的事情，那当我想些一篇文章的时候，我可以连续一个小时都在写，构思好长时间，每天都在想写点什么，有什么新的观点。对于行动，请记住，想要做的事情现在就做，因为“不知道明天和意外哪个先来”，不知道明天的我，还有没有这个心情，这个条件。现在能做的话，请开始吧！像我现在一样，现在是凌晨一点，我刚完成我的写作，睡前想到完成这篇文章，所以我告诉自己不写完就不睡觉了。&lt;/p&gt;
&lt;p&gt;现在，我知道了对一些事情的态度问题，也知道不行动永远没有好的改变，但我还是打算就这样一直下去，带着我的缺憾，生活下去......&lt;/p&gt;
</content:encoded></item><item><title>这些都是你给我的爱，云治</title><link>https://bangwu.me/posts/yunzhi/</link><guid isPermaLink="true">https://bangwu.me/posts/yunzhi/</guid><description>《这些都是你给我的爱》第二本云治篇。关于离别、成长和那些说不出口的话，安东尼用他的方式替我们说了。</description><pubDate>Mon, 16 Dec 2024 03:41:53 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;Safe &amp;amp; Sound&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;云治篇&lt;/h2&gt;
&lt;p&gt;◆ 我们一直在爱着同时也难过寂寞 愤怒失望觉得累了伤心心灰意冷但是又会出现一个人让你觉得一切都好了犹如季节变化冷热交替&lt;/p&gt;
&lt;p&gt;◆ 但是我很确定这个故事一定有一个好的结局小兔子会幸福因为世界是真的付出的都值得 小兔子一点都不可怜&lt;/p&gt;
&lt;h2&gt;关于离别与成长&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;Hometown&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;◆ But that doesn&apos;t stop me from believing in andloving this world from the bottom of my heart.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;WELLINTON&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;◆ My world seemed fine before you came along.&lt;/p&gt;
&lt;p&gt;◆ 我肯定会有人喜欢它我就喜欢它在这个世界上人们追求不一样的东西也通过不同的方式来认识这个世界&lt;/p&gt;
&lt;p&gt;◆ 我第一次遇到她的时候我忽然意识到接下来几天我都要好好整理脑子里的思绪空间 好让她搬进来&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;GARDEN ORNAMENTS&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;◆ 里德很认真地想了想说我觉得爱就是“在乎”你做的这个东西对于在乎的人来说就有了意义&lt;/p&gt;
&lt;p&gt;◆ 人们愿意和刚认识几天的人结婚也不愿意和一个看起来很好的人说我们做朋友吧&lt;/p&gt;
&lt;p&gt;◆ 我觉得我有一点喜欢这个城市了这是我们的城市&lt;/p&gt;
&lt;p&gt;◆ 我不是魔术师你是在我漆黑的世界里你不动声色轻而易举地成为最亮的明星&lt;/p&gt;
&lt;p&gt;◆ 的而是要去感受的&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;安东尼在诗人的工作室里买了一个红皮的小笔记本本子的扉页上写着&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;◆ The thing we love about love stories is that two people teacheach other things that they needed to know but they didn&apos;tknow that they needed to know.&lt;/p&gt;
&lt;p&gt;◆ 我想成为你最好的邂逅最难的再见&lt;/p&gt;
&lt;p&gt;◆ 我当然想成为她最好的邂逅但是再怎么难的再见也只是再见而已 我不想为难她也不想让她难过&lt;/p&gt;
&lt;p&gt;◆ 至于我尽管我曾经一心一意地对她付出了真心但是我的心还在等遇到对的人它又会活起来的&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;他想&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;◆ 至于你 也许你在天边也许我遇到你的那天还很远这些我都不怕&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Pillow latk&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;◆ 有的时候我想离开你去很远的地方人烟荒芜的地方可以在海边好好思考可是 我又害怕我离开了你会难过会哭我也觉得我不能像之前那么幼稚 了每次遇到这样尴尬的局面就要逃跑 于是我就这样跟着你走&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;This or Mha wih.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;◆ 觉得每次都爱得那么用力很伤元气也许爱情也不是找来的它要自然而然地发生&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Just tove me, dammil!&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;◆ when you break-up with someone, the worst you can say isnot &apos;I hate you&apos;,It&apos;s &apos;I changed who I was for you&quot;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;that a heat&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;◆ 抱着你睡觉的时候也不觉得彻底地拥有你总觉得我醒来的时候你就会消失不见&lt;/p&gt;
&lt;p&gt;◆ 每一处的海都是我的海我坚信它们在某一处汇入贯通储存了我的很多情绪和过往又默不做声&lt;/p&gt;
&lt;p&gt;◆ 所以啊 我就在想 人生 之所以 珍贵 说不定不是我们去过的一个又一个精彩华丽的 旅游胜地 而是旅途之中寂寞 无聊不可打发的时间里有你陪伴吧&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;BeFore realily sehs in&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;◆ 所以 你愿意成为我为数不多的秘密么&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.bangwu.me/img/202601117dbe5091.jpg&quot; alt=&quot;weread_image_846666758169847&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.bangwu.me/img/2026011154cc90da.jpg&quot; alt=&quot;weread_image_938328666348080&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.bangwu.me/img/202601113e52da1f.jpg&quot; alt=&quot;weread_image_71591773021266&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.bangwu.me/img/202601110b64b8a6.jpg&quot; alt=&quot;weread_image_71961521610211&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;同系列：《这些都是你给我的爱，里德》。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>这些都是你给我的爱，里德</title><link>https://bangwu.me/posts/lide/</link><guid isPermaLink="true">https://bangwu.me/posts/lide/</guid><description>《这些都是你给我的爱》第一本。兔子安东尼的旅行和那些关于爱与相遇的句子，摘下来放在这里。</description><pubDate>Thu, 05 Dec 2024 12:37:31 GMT</pubDate><content:encoded>&lt;p&gt;《这些都是你给我的爱》书摘&lt;/p&gt;
&lt;h2&gt;兔子安东尼的旅行&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;送给之前陪我一起傻的你&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;◆ 我都会坚定地走向你不迷惑不慌张不犹豫总有一阵子会出现这样的生活状态舒适得让人不知所措&lt;/p&gt;
&lt;h2&gt;关于爱与相遇&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;Dear Anthony&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;◆ If walking with you, every street seems like no end forme.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;“当歌手好辛苦”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;◆ 如果你喜欢的人不喜欢你那么就算全世界的人都喜欢你还是会觉得很孤独吧?&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;半条尾巴的 黄条纹猫」&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;◆ 如果找不到也不要太难过……嗯……想得到的东西不一定都会得到……嗯……人生也不过如此&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;安东尼点头&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;◆ 嗯我是说如果你口渴了应该喝水咖啡是用来享受的&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;「在巫婆的店里」&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;◆ 还是算了吧我一点也不喜欢新的面包篮子它根本没有面包的味道不算是一个面包篮子也许就算我带着这个味道有一天也会开心起来吧&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;又过了很久&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;◆ 我们都会一天天变老然后死掉多好&lt;/p&gt;
&lt;p&gt;◆ 不要走得太慢花会凋谢的也不要走得太快那样花还没有开&lt;/p&gt;
&lt;p&gt;◆ “我喜欢你看我的方式它让我感觉到我自己的存在”“那是因为我想喜欢你”&lt;/p&gt;
&lt;p&gt;◆ 第二天早上起来的时候狐狸就消失了连他留在我身上那些一直清理不净的绒毛也不见了 我一个人走在路上脑子里空空的然后……然后忽然放声大哭好像我从来没有那样哭过心里想着我当初如果和他做朋友就好了&lt;/p&gt;
&lt;p&gt;◆ 城市里的人都在找东西找工作找住处找恋人找一段记忆找一个梦有一些在找另外一个人还有一些在找自己&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;读着这封信我慢慢有了睡意&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;◆ 想找个 保鲜盒把你给我的那些感动都装起来 当我不那么喜欢你的时候就拿出来回味一下&lt;/p&gt;
&lt;p&gt;◆ 所谓人生便是取决于遇见谁The so-called &quot;life&quot;, all depends on who you meet.&lt;/p&gt;
&lt;p&gt;◆ 我们为什么要旅行呢我想可能是因为有些人有些事有些地方一旦离开就回不去了或者应该说总觉得自己回不去了&lt;/p&gt;
&lt;p&gt;◆ 那些我们一直好奇而又有一些惴惴不安的未来有时候在我心里隐隐约约地感觉到它们是明亮的&lt;/p&gt;
&lt;p&gt;◆ 之前那个我和现在的我之间的距离被许多细小琐碎的事填充着这些都是你给我的 爱吧&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;From echo:&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;◆ 很多年以后，在一本书、一段音乐、一片微醺的光线里，倏忽记起的，那段时光。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;From Anthony:&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;◆ 你说我们现在成了这个样子是不是都是别人给的?比如握手时候的力度走路的姿势我们的逻辑身上的疤痕爱怎样的人说话的语气和语速我们旅行的目的地喜欢哪一个歌手什么时候会哭手段与造诣去的地方微笑时嘴角的弧度到底有多努力地生活有没有很爱喝水是不是很喜欢说我爱你接吻的技巧拥抱的姿势说英文时的口音有多天真或者多老练喜欢牛仔还是运动裤可乐还是泡面&lt;/p&gt;
&lt;p&gt;◆ 眉毛的粗细头发的浓密走路的时候手指有多弯&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;后记&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;◆ 那些准备好不离不弃的那些没有说再见就从此不见的那些不论伤害我多少次可是只要你微笑对我时我就拼命想将你原谅的当你们一个个还是或者已经不是那些日子里的你的时候我已经成了现在的我了&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.bangwu.me/img/2026011178ecf326.webp&quot; alt=&quot;bangwu20241205201246&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.bangwu.me/img/20260111768f46f2.webp&quot; alt=&quot;bangwu20241205201738&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.bangwu.me/img/20260111c7da5dd1.webp&quot; alt=&quot;bangwu20241205201941&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.bangwu.me/img/20260111b4a9806e.webp&quot; alt=&quot;bangwu20241205202015&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.bangwu.me/img/2026011183462b72.webp&quot; alt=&quot;bangwu20241205202052&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;同系列：《这些都是你给我的爱，云治》。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>深度学习项目思考</title><link>https://bangwu.me/posts/deeplearning-thinking/</link><guid isPermaLink="true">https://bangwu.me/posts/deeplearning-thinking/</guid><description>参加了一个深度学习项目——判断声音是不是 AI 生成的。这篇是项目结束后的复盘，也聊了聊对 AI 的思考。</description><pubDate>Sun, 01 Dec 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;&lt;strong&gt;个人报告&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;在此次深度学习大作业中，我深切感受到了网络环境、训练数据以及计算资源的多重“摧残”。先不说网络环境的种种问题，单是在寻找训练数据和资源时，就已经感到极其繁琐。深度学习的训练需要大量标注数据，而数据标注本身就是一项费时费力的工作。因此，我们只能选择公开的数据集。按深度学习常见的输入格式划分，数据通常包括图像、文本和音频三种类型。实际上，它们在向量空间中都可以统一表达，但在处理方式上仍有显著差异。&lt;/p&gt;
&lt;p&gt;我们决定跳过图像和文本领域，因为这两个方向的应用相对成熟，而音频方向的实际案例较少且挑战性更大。于是，我们在 GitHub 上搜索与 voice deep-learning 相关的项目，找到了几个热门的声音克隆项目，如 fish、GPT-SoVITS 和 MockingBird。由于我们希望实现一个判断声音是否为克隆生成的模型，因此需要先使用这些项目克隆出音频数据。&lt;/p&gt;
&lt;p&gt;首先，这些声音克隆项目在 Linux 系统上的部署普遍较为复杂。不过值得一提的是，大多数国人开发者都会准备 Windows 的一键整合包（大概率是考虑到网络环境或方便小白用户）。但对于我们来说，这种整合包不适用，因为我们需要能够提供 API 的项目，以便通过批量调用 API 来生成克隆音频。然而，在仔细阅读多个项目的 README 文件后，我们惊讶地发现，几乎所有项目都没有提到如何通过 API 使用这些工具。&lt;/p&gt;
&lt;p&gt;这一点让人十分头疼。尽管项目的整体复杂性较高，但提供一个简单的 API 接口本应该是常见需求。然而，我们在文档中一无所获，最后不得不通过查阅 GPT-SoVITS 的 issue 页面才找到相关说明。坦白说，我完全不理解为什么这些关键信息不直接写进 README 中。&lt;/p&gt;
&lt;p&gt;这一步解决后，新的问题接踵而至：程序调用时不支持异步和多线程。按照程序现状测算，完整克隆所有音频数据大约需要 60 小时。起初我们也无计可施，但后来想到可以通过在同一台机器上多开几个 API 调用实例，并在文件生成时增加“是否已存在”的判断逻辑，从而加速数据生成。这一调整让我们节省了不少时间。&lt;/p&gt;
&lt;p&gt;可是运行程序所需的训练资源太大，显存需要18G，内存30多G，将我们训练数据上传到服务器，耗费了大量的时间，在服务器上安装环境又有网络因素从本地下载后再上传上去，总时长四个多小时。不等上传完，什么也不能干，但是这个是没办法改变的，带宽就这么大。与此同时，在服务器上安装训练环境又因网络原因问题频出，无法直接从官方源获取依赖，不得不选择从本地打包后再上传。整个过程耗时冗长且无法并行进行，导致项目的时间大部分浪费在数据传输和环境配置上，而代码实现和参数调优的时间相对较少。（提一句，linux nohup命令跟踪log挺好用的，可以实时看训练日志极大地方便了模型的调试和监控）&lt;/p&gt;
&lt;p&gt;克隆出来音频后又发现了问题，有大量音频是0s的，也就是没有音频数据，对于这些情况，只能归咎于模型api问题，可能是流数据传输问题。我们用的是迁移学习，需要从huggingface上拉取模型，不知道为什么我们本地下载完后传到服务器上根据官方文档引用不能使用，没办法，从服务器上也不能直接连huggingface。好在，国内有个镜像&lt;a href=&quot;https://hf-mirror.com/&quot;&gt;https://hf-mirror.com/&lt;/a&gt; 使用了这个后成功拉取下来了模型，又是一个网络环境问题。后面开始训练后就还一切顺利了，只需等待训练完成看看准确率就好了，一开始我们想的是搭建前后端分离带数据库的应用项目的，时间有限，就用Gradio几十行命令搭建出来界面了。对于应用，当然是部署到公网上才算酷，只能内网本地访问也太不行了。我们就利用本地服务器然后使用frp进行穿透，再配置域名成功部署到公网上。&lt;/p&gt;
&lt;p&gt;我们从一开始便没对模型的最终效果抱有太高的期望，因为在项目初期，当我们用克隆出来的音频进行试听时，发现这些音频的质量实在是太好了——它们和原声几乎一模一样，甚至让人完全听不出来是克隆出来的。这种初步的直观感受让我们对任务的难度有了更清晰的认知：如果人耳都无法分辨，那么依靠机器学习来判断，会有胜算吗？&lt;/p&gt;
&lt;p&gt;不过，正如一句玩笑所说的那样，“深度学习虽然不可理解，但总是能带来惊喜。”这句话在我们项目中得到了充分体现。我们用深度学习训练模型来判断是否是深度学习生成的数据，这种“自我较劲”的做法乍一听似乎有点矛盾，但恰恰展示了深度学习技术的强大之处。最终，尽管模型在一些方面的表现并不完美（例如仍存在一定误判），但它的整体效果已经远超我们的初衷。通过对克隆音频的训练和测试，模型能够捕捉到一些人耳无法察觉的细微特征，准确区分克隆音频与真实音频。这一点正是深度学习的魅力所在：它可以从海量数据中学习到人类难以显式描述的规律，并将其转化为具体的判断能力。&lt;/p&gt;
&lt;p&gt;这个项目算什么？如果从学术意义上来说，这个项目可以被归类为 生成对抗与检测 的研究领域。近年来，生成模型（如 GAN 和 VAE）以及音频克隆技术的快速发展，让合成数据的质量越来越高。然而，与此同时，检测这些数据的真实性也变得越来越重要，特别是在一些需要高可信度的应用场景中（如语音识别、法律取证等）。因此，我们的项目其实是站在了这类研究的边缘：试图用深度学习来对抗深度学习，用 AI 来识别 AI。&lt;/p&gt;
&lt;p&gt;更广泛地看，这也是当下 AI 技术发展的一个趋势——生成与检测的对抗赛。生成模型在不断提升合成数据的质量，而检测模型也在不断进化，以便更好地区分真伪。我们的项目虽然规模不大，但也算是这一趋势的一个微小实践。&lt;/p&gt;
&lt;h2&gt;以下是本次项目的总结与反思&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;1、时间分配上的反思&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;从整个项目来看，数据准备和模型部署耗费了我们大部分时间，而代码实现和模型调参的时间相对较少。这与我们最初的时间规划有所偏差。如果能在项目开始时更合理地安排任务（如提前下载模型和数据集，或利用网速更快的服务器），可能会提升整体效率。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;2、网络环境的限制&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;项目过程中，网络问题成为一个反复出现的瓶颈。不论是数据上传、环境安装还是模型下载，都受到带宽限制。建议以后可以提前缓存所需的依赖和数据，或者选择国内的开源镜像（如 TUNA 和上文提到的 Hugging Face 国内镜像）。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;3、对深度学习工具链的依赖&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;现代深度学习的工具链非常丰富，但很多工具的文档仍然不够友好。特别是对于音频领域的项目，API 的使用方式和代码示例非常有限，这导致了我们在初期花费大量时间摸索。未来，希望更多项目可以注重文档的完善性，减少学习和使用成本。&lt;/p&gt;
&lt;p&gt;总体来说，这次深度学习大作业让我深刻感受到了深度学习项目的复杂性。从数据准备、模型部署到训练优化，每一步都充满挑战，但也收获了许多宝贵的经验。&lt;/p&gt;
</content:encoded></item><item><title>生日快乐！棒无</title><link>https://bangwu.me/posts/birthday20/</link><guid isPermaLink="true">https://bangwu.me/posts/birthday20/</guid><description>二十岁生日那天跟平时没什么区别：上课、吃饭、写作业。但晚上坐下来想了想，还是想记点什么。</description><pubDate>Sat, 23 Nov 2024 15:32:06 GMT</pubDate><content:encoded>&lt;p&gt;&amp;lt;audio controls data-title=&quot;Payphone&quot; data-artist=&quot;Maroon 5/Wiz Khalifa&quot; data-cover=&quot;https://cdn.bangwu.me/img/20260111vdv9n-vh.webp&quot;&amp;gt;
&amp;lt;source src=&quot;https://cdn.bangwu.me/img/2026011190132cff.mp3&quot; type=&quot;audio/mpeg&quot;&amp;gt;
&amp;lt;/audio&amp;gt;
这是我第一次写生日文章，希望以后每年也都能写一篇。今天是我的生日，和大多数人一样，这将是没什么特别的一天，我之前在文章 &lt;a href=&quot;https://bangwu.top/posts/butaire&quot;&gt;人太多大部分是漫无目的地走&lt;/a&gt;
里也有说过，不是很重视生日也好久没吃过属于自己的蛋糕了。不过一切都无所谓，我不是很在乎，挺好的。
早上十点钟醒了，打开手机看到一个朋友给我发的生日祝福，祝我老了一岁还给我转了杯奶茶，幸福往往就是那么普通的一瞬间，一切都是刚刚好的样子。下了床坐在椅子上，我回复到：我再也不是个小孩子了😭，略感伤心，不知不觉间自己已经20岁了，想起10岁的棒无，他也会好奇10年后的自己是什么样子吗？现在我或许能知道，还不错，目前来说还是挺优秀的，哈哈哈。
内心的期待：这几天偶尔打开菜鸟驿站看看我的快递，虽然我没买，但我还是希望能看到多出一个快递派送单，来自远方。不把自己的生日告诉别人，却希望得到走心的祝福。
我打算把这一样记录下来，就从早上醒来开始吧。醒来后就去简单洗漱了一下，微笑，内心想着今天是生日，怎么说也要比平常开心一点吧。坐在椅子上，打开电脑，想着下周二要交的小组作业还没有头绪，这次并不是我拖延症之类的，是近几周作业任务实在有点忙不过来，好几篇论文还有作业。坐在椅子上对作业还是没有什么头绪，只能等着小组成员一起来讨论，于是我就想着趁着这个时间干点啥，想起来昨天晚上要买东西，宿舍缺垃圾袋了。忽然觉得，过生日只买垃圾袋好像有点不妥，思来想去，果然，人在想买东西的时候不要乱逛电商平台，会像我这样的买到固态和内存条🤕&lt;/p&gt;
&lt;h2&gt;普通的一天&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.bangwu.me/img/202601114090022b.png&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.bangwu.me/img/20260111741e04ba.png&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;小确幸&lt;/h2&gt;
&lt;p&gt;就是这样，我没有丝毫犹豫的拿下了垃圾袋、方便面、固态和内存条。算是我买给自己的生日礼物吧，一边是生活，一边是爱好。然后就是小组讨论，当然，我不会因为是我的生日而影响了小组的进度。
讨论到中午，朋友叮嘱我要吃长寿面。遂一个人至河东食堂一楼点了一碗面，周六的食堂是有点冷清的，中午饭点也没多少人在面条窗口就我和另一个女生，打饭的阿姨却不在。我总是沉默的那个，不愿意开口喊阿姨在不在，好在那个女生趴在窗口上喊阿姨“阿姨还有面可以煮么？”由于我在那个女生前面，她还让我先点，我推脱了，让她先来。不知道为什么我现在还记着这件事（写的时候已经晚上十一点了），可能是无意间的善意吧。虽然之前吃了很多次这个窗口的面条，但是还是觉得这次的面条好吃，长寿面果然会暗暗在心里加分！&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.bangwu.me/img/202601113a06fbab.png&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;写给二十岁的自己&lt;/h2&gt;
&lt;p&gt;吃完面回到宿舍，又开始鼓捣我的小组作业，然后我崽下午休息就喊我跑图，11月23号，光遇绊爱来了，有饺子可以吃，像我的生日一样安静美好。跑完图打了几把王者，到四点了。小组作业也在这时候有了一点头绪，我把代码一些，由于是深度学习项目，剩下的就是等待了（深深地感受到了深度学习对网速和硬件的要求😔）。后面我又干了啥捏，有点忘了，大概是发呆吧。然后到了六点多，室友向我推荐点黏糊糊麻辣烫，于是我们俩就一人点了一份。不得不说，还挺好吃的&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.bangwu.me/img/20260111d03b5720.png&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;p&gt;吃完我就在逛一逛帖子论坛之类的，刷抖音。到了晚上八点半就开始打王者了，打到十点半累了，就不打了。
把手机放在桌子上，我准备写点啥。忽然有个电话打进来了，我以为又是推销电话，一看是我的发小打来的。我和两个发小有一个群，只有我们三个，群聊名称也没有。这成了我们联系的主要渠道，打过来电话，先问我今天吃点好的没，“随便吃点，吃啥好的”，你过生不吃点好的吗，“随便吃点”。后面就是一顿闲聊，真好。
就这样，我的一天快结束了，普通的一天。现在是23.23，记录我的生日23号。作业、生活和理想、朋友。人生 如此 20年，棒无，生日快乐！&lt;/p&gt;
</content:encoded></item><item><title>独立开发者网络营销指南</title><link>https://bangwu.me/posts/internet-marketing/</link><guid isPermaLink="true">https://bangwu.me/posts/internet-marketing/</guid><description>以独立开发者视角拆解网络营销的实际打法——从 SEO、内容分发到社区增长，每一步都有可落地的策略和数据支撑。</description><pubDate>Fri, 22 Nov 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;&lt;strong&gt;软件产品网络营销策略——以独立开发者为例&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;本文为我的网络营销课程中的一个作业，在这里分享一下，欢迎改正和讨论。&lt;/p&gt;
&lt;h2&gt;&lt;strong&gt;前言&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;随着计算机技术的发展，独立做出一个软件产品、项目的门槛不断降低。越来越多人选择成为独立开发者。谁是独立开发者？其实独立开发者归属于自由职业者，是自由职业者中从事和软件开发相关工作的一支人群，更严格来说一般是“从产品立项、设计、开发、推广、到盈利闭环全部独立完成的人”。&lt;/p&gt;
&lt;p&gt;独立开发者往往将全部精力投入应用开发，而忽视了制定优秀的营销方案。这通常是由于缺乏资金、资源和专业团队。好消息是，有效的营销并不需要庞大的广告预算。&lt;/p&gt;
&lt;p&gt;在这之前要认识到一个方法论：好的想法、营销比好的技术有用。&lt;/p&gt;
&lt;h2&gt;&lt;strong&gt;营销手段探究&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;对于独立开发者没有充足的广告预算，进行网络营销所花费的成本有限，不能进行像搜索引擎营销中的关键字营销——付费赞助与竞标的方式通常会花费大量资金和精力，不适合独立开发者来做，以下所有的策略都是建立在成本低且个人可维护的前提下的。&lt;/p&gt;
&lt;h2&gt;&lt;strong&gt;一、搜索引擎营销&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;正如上述所说的，关键字营销不可取，PPC 的费用不足以支撑独立开发者盈利，但是可以利用搜索引擎优化(SEO)来将自己的网站列入搜索结果的前面，吸引消费者注意与点击。SEO 一般需要的手段不深入的话是简单的，例如使用现有的前端框架都有专门的 SEO 优化，仅需简单的设置即可。所以可以着手优化的是页面优化(On-Page SEO),由于谷歌算法始终在变化，但谷歌继续优先考虑用户体验。谷歌建议关注“以人为本的内容”，页面优化需要的是高质量的内容、网站架构(优化 URL、标题和副标题等)、外观设计与用户体验。通过以上手段，对于独立开发者来说就基本可以将网站排名提高很多。&lt;/p&gt;
&lt;h2&gt;&lt;strong&gt;二、社会网络营销&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;社会网络营销需要的成本低，是独立开发者营销的主要手段。与目标顾客群创造长期沟通，社会网络营销需要长期运营维护，产生的收益也是逐渐递增的，需要注意的是产品的使用截图、海报等需要专门的设计，好的外观才能先吸引到用户。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;社群网络服务利用微信、微博、Bilibili 等本地的社交媒体平台以及 Facebook、Twitter、YouTube、Twitch 等海外平台建立社群。目标是国内用户的独立开发者可以通过微信公众号和小程序来吸引流量。目标是国内外用户群体的可以通过 X 发帖来宣传，这种方法一般是高效的，可以引来转帖和反馈。&lt;/li&gt;
&lt;li&gt;博客推广——通过与用户互动、发布有趣内容有利于 SEO 将自己的网站搜索排名提高。这里有个特别的点就是建立友情链接和找知名博主推荐两种方式都是高效的，虽然这两种方法的原有流量不如社交媒体、社群，但是转换率很高，人们通常认为文章或者友情链接推荐出来的产品都是高质量的，更愿意为其付费。&lt;/li&gt;
&lt;li&gt;论坛营销。许多现有的热门独立开发者软件都有在论坛第一次发布产品来推广的做法，这些论坛主要是 v2ex，w2solo，reddit 等。能够有效地找到第一批用户&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;&lt;strong&gt;三、应用商店优化(ASO)&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;软件分发到各大应用商店来提供更便捷的安装方式和流量增长，ASO 大致有两种方法：Keyword Optimization(包含：选词、词排名、转化率)和 Asset Optimization 即 app 其它属性的优化(图标、截图、描述等，通过 a/b 测试)。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;商店搜索优化——App Store 分发量分布：搜索、榜单、推荐位、外部导量。65%以上来源于搜索(苹果官方数据)，剩下的分发量中至少一半来源于榜单，未上榜且无外部导流的 app，搜索是前期唯一的曝光入口。其中关键词方面，转化率：精准品牌词 &amp;gt; 特征词 &amp;gt; 相关热门其它品牌词 &amp;gt; 不相关词。&lt;/li&gt;
&lt;li&gt;APP 商店主页内容优化——主要有 APP 名称、图标、描述、配图、用户评价等这些属性。例如描述应用要简洁有目的性，配图要突出特色抓住用户心理。用户评价方面要单独处理，适当给好评用户一些奖励及反馈。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;ASO 并不像 SEO 那样有许多规则，成本低的话能做得很少，尽可能的把产品发布到每个应用商店就行。&lt;/p&gt;
&lt;h2&gt;&lt;strong&gt;四、不采用的策略&lt;/strong&gt;&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;开源推广——付费版本 完善项目、增加流量。但由于开源的原因，流量增加的同时也会带来付费用户的减少，需要在这两者之间找到一个平衡点，通过 Github 上开源的项目来说，作为独立开发者盈利项目的很少，所以不建议选择开源来推广项目的思路。&lt;/li&gt;
&lt;li&gt;显示广告营销：不采用、收益较低&lt;/li&gt;
&lt;li&gt;电子邮件营销：由于国内偏好因素不采用&lt;/li&gt;
&lt;li&gt;提升口碑：对待用户反馈及时处理、Bug 及时修复并补偿等等。会有好的影响但不是主要策略，例如有的产品不采用社会网络营销，而是用口碑、开源等其他营销手段提升用户好感度，使用户自发的在社交媒体和其他途径进行分享，良性循环。例如：开源程序 Alist 会因为解决用户网盘管理需求以及官网的详细文档而受到用户大规模的推广，在社交媒体上分享自己的使用体验。也是一种好的行为，但不是好的商业行为。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;&lt;strong&gt;五、付费策略&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;目前市场上主流的付费策略是订阅制和买断制选择，一般来说这两种策略如何选择主要是看产品属性：后续维护、提供服务成本高选择订阅制，而产品成熟、更新频率低选择买断制。&lt;/p&gt;
&lt;p&gt;但是国内用户偏好买断制，有一个新的付费策略是这样的：“按年 + 付费更新”的思路，即： 用户支付一次性费用，获得软件的永久使用权和一年内的更新；后续更新则需要按年付费 我觉得这种定价模式的好处在于：1. 降低了那些抵触订阅制的用户的心理负担，因为他们仍可以一次付费，永久使用 2. 将“软件订阅费用”巧妙转化为“更新服务费”，无形之中提升了用户获得的价值感，消解了“我不知道我在为什么东西付费”的疑虑。&lt;/p&gt;
</content:encoded></item><item><title>在安逸的日子里</title><link>https://bangwu.me/posts/anyi/</link><guid isPermaLink="true">https://bangwu.me/posts/anyi/</guid><description>一场班会投票让我开始想：大学生活到底是安逸还是内耗？写这篇的时候我也还没想清楚，但开始想了。</description><pubDate>Thu, 21 Nov 2024 16:31:53 GMT</pubDate><content:encoded>&lt;p&gt;今天（11月13日，拖延症的我现在才写完文章）晚上下课我们班召开了一个简短的择优大会，具体就是投票选出优秀学生和优秀班干。说实话，我对这种会议一点感觉也没有，可能是我不优秀无法去充当分母吧，但由于是班级投票的原因，每个人又都需要参加。&lt;/p&gt;
&lt;p&gt;下课了，正收拾书包的我被室友叫住：还不能走，等下要开会。一瞬间放学的快乐全无，装作不情愿的样子把双手搭在桌子上，我也不知道自己为什么会这么做，大概是情绪外显吧。说说我的大学班级吧，在师范学校里总体男女比例三比七我是不奇怪的，我一个理科生竟然被分到了经管学部的工商管理学院，虽说专业内容还是有些与计算机啊、数学有关的，但是不能忍受学会计学、管理学、经济学，这些课程慢慢磨灭了我的憧憬，我的幻想。大学的课程是很恶心的，就是为了学分和绩点，我敢打赌就算是去学文学、民俗学照样会有人费尽各种心思拿高分，并不是说这种行为太功利了啊之类的，只是总觉得好像与初衷背道而驰了。让我想起了《肖申克的救赎》里对肖申克监狱的描述，一开始你厌恶它，后来习惯它，最后离不开它。在这堵高高的围墙里，我总以君子不器、不被规矩限制、不屑于争逐为借口来麻痹自己，我讨厌掉入这种循环一年又一年，和周围的人们一同腐烂。
投票选优秀学生，竞选的同学站在前面掏出手机念着准备好的竞选词。什么才算是当代优秀大学生呢？同学们念着，参加xxx竞赛获得国家级一等奖，我的绩点是xxx，积极参加社会实践和公益活动......每次听到这些条条框框的话语我都会想到，那你是怎么定义你自己的呢？你该如何描述自己的理想或是人生之类的呢？每次看着这些很卷，表面数值很高的人，我总是会有这些疑问，他们都好像是从一个模子刻出来的，机械化生产，或者说，更像是一个被设定好的NPC。&lt;/p&gt;
&lt;h2&gt;那场班会投票&lt;/h2&gt;
&lt;p&gt;:::div{style=&quot;max=width: 300px&quot;}&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.bangwu.me/img/20260111df669fd0.jpg&quot; alt=&quot;Screenshot_2024-11-22-00-29-54-454_com.mi.health-edit&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;内耗与安逸&lt;/h2&gt;
&lt;p&gt;:::
可事实是什么呢，我还是会把自己带入他们的履历：我也有这些奖项吗，这些奖项要花费很大的精力吧，为什么我的绩点那么低啊...而优秀的同学会有一个不为物质发愁的未来，我却还在纠结是否该迈出步伐。最近小米su7ultra的纽北破纪录视频结尾一段文字火了：永远相信美好的事情即将发生，只要开始追赶，就已经走在胜利的路上。&lt;/p&gt;
&lt;p&gt;最近发现我的生活已经很美好了，差不多的人际关系，差不多的消费，差不多的成绩还有差不多的人生目标。偶尔会不合时宜地想起一些不会再见的朋友，不过也没关系，我很清楚目前来说很长一段时间将会是我人生中难以忘却的美好时光，就这样，我必须好好珍惜这段时间。如果说未来是迷茫的话，那就好好享受当下吧，没必要为了一些事情而难以忘怀，更不必因为某些人而生气，只会让自己难受，错过了一些事情。
我们并不需要有美好的未来。这个世界是很奇妙的，有人星夜赶科场，有人辞官归故里，而又有人只是为了想着明天要怎么活下去。我想，这个世界我活着就够了。&lt;/p&gt;
&lt;p&gt;我总结以上内容为“在安逸的日子里”，因为这段时间是我人生中为数不多的安逸时刻，像是高考后的那个夏天，不过还是多了一点焦虑少了一点热情。最近，我总想着大学的这段日子，大一的我好像有些年少无知，却也是因为缺乏素质教育吧，说来惭愧，选择目前这个专业的我，大一的时候还不怎么会用电脑，打字也特别慢不知道什么是Windows，Word，PPT....不知道是什么支撑我走过那段时光，可能是身边同样盲目的朋友吧，谢谢你！
在安逸的日子里，我们要不经意地在脑海里想着此刻的珍贵，认定此刻的美好，这或许是未来某天午后，那个经历了很糟糕的一件事后怀念起来的时刻。“天地玄宗，万炁本根。广修浩劫，正吾神通。”对此，我有的不是特别快乐，是很强烈很浓郁的满足，对于当下充实的满足。&lt;/p&gt;
</content:encoded></item><item><title>fuwari(Astro) 添加评论功能实践记录</title><link>https://bangwu.me/posts/fuwari-comment/</link><guid isPermaLink="true">https://bangwu.me/posts/fuwari-comment/</guid><description>给 Fuwari 主题接入 Twikoo 评论系统的完整过程：环境配置、API 对接、踩坑记录与最终的优化建议。</description><pubDate>Wed, 20 Nov 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;背景&lt;/h2&gt;
&lt;p&gt;Fuwari 是基于 Astro + Svelte 的博客主题，默认没有评论模块。为了保留静态站点的性能与部署体验，我选择集成自建的 &lt;a href=&quot;https://twikoo.js.org/&quot;&gt;Twikoo&lt;/a&gt; 服务。本篇记录最终方案、遇到的问题和改善建议。&lt;/p&gt;
&lt;h2&gt;目标与约束&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;运行环境&lt;/strong&gt;：Astro + Fuwari（Svelte 渲染层），部署到 Cloudflare Pages。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;评论服务&lt;/strong&gt;：Twikoo（腾讯云云函数部署，兼容 LeanCloud）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;要求&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;首次进入文章页即可渲染评论，无需刷新；&lt;/li&gt;
&lt;li&gt;不影响静态渲染，尽量避免打破 Astro 的 islands 架构；&lt;/li&gt;
&lt;li&gt;支持暗色模式、移动端适配；&lt;/li&gt;
&lt;li&gt;保留无评论时的占位文案，避免布局突兀。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Twikoo 部署概要&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;准备数据库&lt;/strong&gt;：我使用腾讯云云函数 + 云开发环境 &lt;code&gt;env-xxx&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;部署云函数&lt;/strong&gt;：按照官方指引上传 &lt;code&gt;twikoo&lt;/code&gt;，记录部署后的 &lt;code&gt;envId&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;配置域名&lt;/strong&gt;：自定义域名 + HTTPS，方便前端调用。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;后台初始化&lt;/strong&gt;：在云开发控制台中创建管理员用户、配置默认主题色。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;如果不想维护服务，可选择第三方托管，但需关注隐私与速率限制。&lt;/p&gt;
&lt;h2&gt;在 Fuwari 中引入评论组件&lt;/h2&gt;
&lt;p&gt;Fuwari 的文章详情使用 Svelte 组件 &lt;code&gt;Post.astro&lt;/code&gt;。为了保持 SSR 与事件绑定，我采用“前端懒加载”的做法：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;新增 Twikoo 入口组件 &lt;code&gt;src/components/post/TwikooComments.svelte&lt;/code&gt;&lt;/strong&gt;：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;script lang=&quot;ts&quot;&amp;gt;
  import { onMount } from &apos;svelte&apos;;

  export let envId: string;
  export let path: string;
  export let darkSelector = &apos;html[data-theme=&quot;dark&quot;]&apos;;

  let container: HTMLDivElement | null = null;
  let twikooLoaded = false;

  const loadTwikoo = async () =&amp;gt; {
    if (twikooLoaded || !container) return;
    twikooLoaded = true;

    const { default: twikoo } = await import(&apos;twikoo&apos;);
    await twikoo.init({
      envId,
      el: container,
      path,
      lang: &apos;zh-CN&apos;,
      theme: matchMedia(`(prefers-color-scheme: dark)`).matches ? &apos;dark&apos; : &apos;light&apos;,
    });
  };

  onMount(() =&amp;gt; {
    loadTwikoo();

    const observer = new MutationObserver(() =&amp;gt; {
      const isDark = document.querySelector(darkSelector);
      container?.setAttribute(&apos;data-twikoo-theme&apos;, isDark ? &apos;dark&apos; : &apos;light&apos;);
    });

    observer.observe(document.documentElement, {
      attributes: true,
      attributeFilter: [&apos;data-theme&apos;],
    });

    return () =&amp;gt; observer.disconnect();
  });
&amp;lt;/script&amp;gt;

&amp;lt;div bind:this={container} class=&quot;tk-container&quot;&amp;gt;
  &amp;lt;p class=&quot;tk-loading&quot;&amp;gt;评论加载中...&amp;lt;/p&amp;gt;
&amp;lt;/div&amp;gt;

&amp;lt;style&amp;gt;
  .tk-container {
    margin-top: 3rem;
  }

  .tk-loading {
    text-align: center;
    color: var(--muted-foreground, #888);
    font-size: 14px;
  }
&amp;lt;/style&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;在 &lt;code&gt;Post.astro&lt;/code&gt; 中注入组件&lt;/strong&gt;：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;---
import TwikooComments from &apos;@/components/post/TwikooComments.svelte&apos;;

const { frontmatter } = Astro.props;
const twikooEnvId = Astro.site ? &apos;your-env-id&apos; : import.meta.env.PUBLIC_TWIKOO_ENV_ID;
---

&amp;lt;article&amp;gt;
  &amp;lt;!-- 文章内容 --&amp;gt;
&amp;lt;/article&amp;gt;

&amp;lt;TwikooComments client:idle envId={twikooEnvId} path={Astro.url.pathname} /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;client:idle&lt;/code&gt; 确保组件在空闲时加载，避免阻塞首屏。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;path&lt;/code&gt; 传入文章路径用于评论定位。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;envId&lt;/code&gt; 可以通过环境变量注入。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;解决“首次进入不渲染”的问题&lt;/h2&gt;
&lt;p&gt;一开始我把 Twikoo 的初始脚本直接写在 &lt;code&gt;client:load&lt;/code&gt; 的组件里，结果在首次导航到文章页时不会执行，必须刷新才出现。这与 Astro 的 SPA 切换有关：页面通过 &lt;code&gt;@astrojs/view-transitions&lt;/code&gt; 保留 DOM，导致 Twikoo 初始化被错过。&lt;/p&gt;
&lt;p&gt;最终改进方案：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;监听路由切换&lt;/strong&gt;：Fuwari 使用客户端导航，需在 &lt;code&gt;onMount&lt;/code&gt; 中关心 &lt;code&gt;Astro.url.pathname&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;使用 &lt;code&gt;Astro.props&lt;/code&gt; 动态变更 &lt;code&gt;path&lt;/code&gt;&lt;/strong&gt;：当文章切换时，&lt;code&gt;path&lt;/code&gt; 随 props 更新，组件重新初始化。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;自定义事件重载&lt;/strong&gt;：如果妙用 &lt;code&gt;&amp;lt;ViewTransitions /&amp;gt;&lt;/code&gt;，可以在切换时派发事件：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;document.addEventListener(&apos;astro:page-load&apos;, () =&amp;gt; {
  twikooLoaded = false;
  loadTwikoo();
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Fuwari 目前（2024.11）没有暴露事件，我通过在 &lt;code&gt;src/layouts/Base.astro&lt;/code&gt; 添加：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;script&amp;gt;
  document.addEventListener(&apos;astro:page-load&apos;, () =&amp;gt; {
    window.dispatchEvent(new CustomEvent(&apos;fuwari:page-loaded&apos;));
  });
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后在组件里监听：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;onMount(() =&amp;gt; {
  const rerender = () =&amp;gt; {
    twikooLoaded = false;
    loadTwikoo();
  };
  window.addEventListener(&apos;fuwari:page-loaded&apos;, rerender);
  loadTwikoo();
  return () =&amp;gt; window.removeEventListener(&apos;fuwari:page-loaded&apos;, rerender);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样任何一次页面跳转都会重新初始化 Twikoo，解决首屏不显示的问题。&lt;/p&gt;
&lt;h2&gt;样式与暗色模式&lt;/h2&gt;
&lt;p&gt;Twikoo 提供 &lt;code&gt;data-theme&lt;/code&gt; 属性控制主题。Fuwari 使用 &lt;code&gt;data-theme=&quot;dark&quot;&lt;/code&gt; 切换，我在 MutationObserver 里同步这个状态：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;切换时更新容器属性；&lt;/li&gt;
&lt;li&gt;通过自定义 CSS 重写输入框、按钮的颜色；&lt;/li&gt;
&lt;li&gt;在暗色模式下调低边框与阴影。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.tk-container[data-twikoo-theme=&apos;dark&apos;] {
  --twikoo-font-color: #d4d4d8;
  --twikoo-card-bg: rgba(24, 24, 27, 0.6);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;性能与懒加载&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;client:idle&lt;/code&gt; + &lt;code&gt;await import(&apos;twikoo&apos;)&lt;/code&gt; 组合确保在主线程空闲时加载脚本。&lt;/li&gt;
&lt;li&gt;可以进一步使用 &lt;code&gt;IntersectionObserver&lt;/code&gt;，只有当评论区域进入视口时才加载。&lt;/li&gt;
&lt;li&gt;Twikoo 默认加载表情包等资源，必要时使用 &lt;code&gt;twikoo.init({ reaction: false })&lt;/code&gt; 关闭。&lt;/li&gt;
&lt;li&gt;若担心 bundle 体积，考虑通过 CDN 注入 &lt;code&gt;&amp;lt;script src=&quot;.../twikoo.min.js&quot; defer&amp;gt;&lt;/code&gt;，再调用 &lt;code&gt;window.twikoo.init&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;参考资料&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://twikoo.js.org/&quot;&gt;Twikoo 官方文档&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/saicaca/fuwari&quot;&gt;Fuwari 主题仓库&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.astro.build/en/concepts/islands/&quot;&gt;Astro Islands 指引&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.astro.build/en/guides/view-transitions/&quot;&gt;Astro View Transitions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://cloud.tencent.com/document/product/876/44547&quot;&gt;CloudBase CLI&lt;/a&gt;：云函数部署与备份&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;Fuwari 的另一个集成案例：《Fuwari 里接 usememos 图文流 我是怎么做的》。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>恋爱观</title><link>https://bangwu.me/posts/aboutlike/</link><guid isPermaLink="true">https://bangwu.me/posts/aboutlike/</guid><description>刷到一条恋爱观视频，觉得说得太对了但又说不清哪里对。写下来理一理，也想听听你的想法。</description><pubDate>Sat, 16 Nov 2024 16:21:25 GMT</pubDate><content:encoded>&lt;h2&gt;那个视频&lt;/h2&gt;
&lt;p&gt;视频来自博主：&lt;a href=&quot;https://v.douyin.com/iAGMpCL8/&quot;&gt;@久树青&lt;/a&gt;
&amp;lt;video&amp;gt;
&amp;lt;source src=&quot;https://static.bangwu.me/video/aboutlike.mp4&quot; type=&quot;video/mp4&quot; /&amp;gt;
&amp;lt;/video&amp;gt;&lt;/p&gt;
&lt;h2&gt;我的困惑&lt;/h2&gt;
&lt;p&gt;说实话，由于今天早上起来心情很不好，又做了一个很奇怪的梦，希望斩断一切又害怕自己内耗，为什么会有不清不楚的男女关系。之前我想佛系，想缘分不物质，我的恋爱观果然有问题，想听听大家的想法。&lt;/p&gt;
</content:encoded></item><item><title>致那三年</title><link>https://bangwu.me/posts/sannian/</link><guid isPermaLink="true">https://bangwu.me/posts/sannian/</guid><description>课堂上无意间看到同学的文档，发现里面记着高中三年的点点滴滴。一瞬间被拉回那段日子，于是也写了写自己的。</description><pubDate>Tue, 05 Nov 2024 02:12:14 GMT</pubDate><content:encoded>&lt;p&gt;&amp;lt;audio controls data-title=&quot;三年&quot; data-artist=&quot;吴垚滔&quot; data-cover=&quot;https://cdn.bangwu.me/img/20260111wb81c-er.webp&quot;&amp;gt;
&amp;lt;source src=&quot;https://cdn.bangwu.me/img/2026011109dff1dc.mp3&quot; type=&quot;audio/mpeg&quot;&amp;gt;
&amp;lt;/audio&amp;gt;&lt;/p&gt;
&lt;h2&gt;高中三年&lt;/h2&gt;
&lt;p&gt;今天下午上博弈论，嗯.....一门很无聊的课。这种听不听都无所谓的课，最大的痛点就是要去待着，从上课直到下课，台上老师讲得眉飞色舞我坐在位置上低着头摆弄平板，心却早已飞到了九霄云外。我时常觉得上课有时候还挺幸福的，不像是初高中那样紧绷着神经，需要去理解老师说出来的知识点，我可以只做我想做的事情，对于经常发呆的我来说（有时候觉得自己是ADHD患者，不过这目前来说也不重要啦），真是幸福呢。&lt;/p&gt;
&lt;h2&gt;偶然看到的那份文档&lt;/h2&gt;
&lt;p&gt;不经意间撇到了旁边舍友的平板，说实话，我是不太有意愿去窥探别人正在做的事的，即使是在我旁边睁眼就能看到。我看着好像是某个文档的页面，上面表头赫然写着“致一中的同学们”对于他的高中，我还是有一点了解的，但不知道是一中，也不知道有哪些有趣的事情。这次我没有怯懦，而是凑了上去和他一起看。就这样，在老师的讲课声中，我们一起看着文档往下划呀划，我看到了好多人把高中时拍的照片贴上去，也有高中时情侣的合照，青春、热烈、盎然。一瞬间思绪把我拉到了高中，目光中看到的是他们高中的建筑，我也想起了我们高中的那个湖，那座桥，印象最深的是下午放学到晚上上课期间有一大段时间，那时就和高中的挚友们一起去在桥上待着，那桥现在看起来很小很小，记忆中上面却有很多人——春夏秋冬都有着我们的脚印，而我还会看到那个少年靠在桥边和朋友诉说着学习的痛苦或是玩游戏的喜悦。&lt;/p&gt;
&lt;p&gt;现在回头看高中，我好像除了没谈恋爱以为没什么遗憾，高考毕业时还有很好的同学关系以及意料之外的成绩，对于17岁的我来说，足矣。曾经总以为毕业遥遥无期，如今距我踏入高中班级算下来已经是5年前了，高中给我最深的印象不是做不完的练习题，堆积成山的资料，更不是倒背如流的知识点（高考之后就差不多忘干净啦）反而是我从来都认为无关紧要的同学关系。说说我的事情吧，高一的时候到班级里，刚开学的那天是我记忆最深的一天，可能当时也没想到吧，在那天发生的种种，现在仍然清晰，和大多数人对于陌生环境的焦虑感不一样，我想着的竟不是周围，我认为无论怎样，我以后都会交到朋友的，也没想过主动去认识同学之类的，我总是害怕复杂的人际关系，如果不能给我带来健康的反馈，我会深陷其中不能自拔，就像是一个悉心照料了很久的玫瑰，刺伤了你，还是紧紧握着。于是乎我总是秉承着我所谓的态度，想到之前一个同学说的：无所为，无所为，无所谓。事实也证明只要你不把一件事情看重要，那么无论这件事情怎么变差、变好，都不会带来很大的影响，我当时觉得这样蛮好的，可以很轻松地对即将分出班级的同学说后会有期。有个词叫没心没肺，说的就是当时的我啦，总以为别人不懂我的做法，我的思考，现在想想当时还真是蠢、自以为是捏，最不懂的那个人其实是我，但不知的我还以占了一点点自以为的上风就沾沾自喜。&lt;/p&gt;
&lt;p&gt;高中印象深的有三次人际关系的崩坏，现在想想他们三个如今也和我没了联系，但这段关系还是淹没在了我的17岁，那个意气风发自大的少年。少年时的失望大概是QQ签名里的话、空间里的照片。高中时的人际关系崩坏是致命的，大部分清醒的时间都是在教室里度过的，总是抬头不见低头见，大家也都有相同的同学、朋友，没有特别幼稚的“我不跟他玩了，你也别和他玩”上放学的路上会特意走慢点怕迎了上去会尴尬，体育课也会躲着，总之无论什么时候，只要是在学校，脑海里就要时刻想着不要和他有任何交流。这三次中，有一次很短的，只有短短几天的不说话，相处了三年的他拉下面子来找我道歉，我却还以为占了理，现在想，那叫幼稚。这是台上的老师说到博弈，你以为我在第一层，其实我在第三层，而你以为你在第二层，其实你在负一层。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.bangwu.me/img/2026011165bd7beb.jpg&quot; alt=&quot;IMG_20241105_001144&quot; /&gt;&lt;/p&gt;
&lt;p&gt;现在呢，我想人生路漫漫，高中三年的故事早已翻篇。留下的只有被改变的那部分我，彻底的陪在了我的身边，如此，甚好。&lt;/p&gt;
&lt;p&gt;“教室窗外，弥漫着落日云彩，期待着铃声到来，起身，匆匆离开。”
“倚靠在玻璃窗台，遥望那白云沧海，流过了光阴十载，转身，不再回来。”&lt;/p&gt;
</content:encoded></item><item><title>Auth api设计详解</title><link>https://bangwu.me/posts/web-auth/</link><guid isPermaLink="true">https://bangwu.me/posts/web-auth/</guid><description>Web 认证体系全景：从 Session/Cookie 到 JWT 到 OAuth2.0，梳理主流认证流程、数据库设计和 API 实现方案。</description><pubDate>Mon, 28 Oct 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;&lt;a href=&quot;https://thecopenhagenbook.com/&quot;&gt;The Copenhagen Book&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;这本书是 Lucia 作者大佬 &lt;a href=&quot;https://github.com/pilcrowonpaper&quot;&gt;pilcrow&lt;/a&gt; 写的开源指南，围绕“如何把身份认证做对”展开。我按照书中章节脉络，结合在实际项目里踩过的坑，总结一份适合日常后端开发与产品协作的备查表。&lt;/p&gt;
&lt;p&gt;以下内容覆盖：&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;li&gt;多因素认证（MFA）、WebAuthn、第三方登录；&lt;/li&gt;
&lt;li&gt;会话管理、切换设备、注销策略；&lt;/li&gt;
&lt;li&gt;管理后台与自建 OAuth Provider；&lt;/li&gt;
&lt;li&gt;审计日志、风控、隐私合规。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;核心实体模型&lt;/h2&gt;
&lt;p&gt;The Copenhagen Book 把身份系统拆成三层，可避免“把所有逻辑塞进 user 表”：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;实体&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;th&gt;常见字段&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;用户 (&lt;code&gt;users&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;代表真实的个人或组织，提供业务上下文&lt;/td&gt;
&lt;td&gt;&lt;code&gt;id&lt;/code&gt;, &lt;code&gt;display_name&lt;/code&gt;, &lt;code&gt;created_at&lt;/code&gt;, &lt;code&gt;banned_at&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;身份 (&lt;code&gt;identities&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;每种登录方式的唯一标识&lt;/td&gt;
&lt;td&gt;&lt;code&gt;id&lt;/code&gt;, &lt;code&gt;user_id&lt;/code&gt;, &lt;code&gt;provider_id&lt;/code&gt;, &lt;code&gt;provider_user_id&lt;/code&gt;, &lt;code&gt;created_at&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;会话 (&lt;code&gt;sessions&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;某次登录状态，可跨设备&lt;/td&gt;
&lt;td&gt;&lt;code&gt;id&lt;/code&gt;, &lt;code&gt;user_id&lt;/code&gt;, &lt;code&gt;active_expires&lt;/code&gt;, &lt;code&gt;idle_expires&lt;/code&gt;, &lt;code&gt;user_agent&lt;/code&gt;, &lt;code&gt;ip&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;设计关键点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;用户可以拥有多个身份：邮箱密码、GitHub OAuth、手机号等。&lt;/li&gt;
&lt;li&gt;身份与用户是 1:N；会话与用户也是 1:N。&lt;/li&gt;
&lt;li&gt;会话应该存储 IP、 User-Agent、最近活动时间，便于风控与用户自助管理。&lt;/li&gt;
&lt;li&gt;不要把密码、第三方 token、WebAuthn credential 混入用户表，分开存储才能实现更细的权限与轮换策略。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;凭证存储与秘密管理&lt;/h2&gt;
&lt;h3&gt;密码&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;使用 &lt;code&gt;bcrypt&lt;/code&gt;、&lt;code&gt;argon2id&lt;/code&gt; 等现代算法，设置合理代价（如 argon2id &lt;code&gt;time_cost=3&lt;/code&gt;, &lt;code&gt;memory=64MB&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;永远不要明文存储，也不要“自创盐”，库已经处理。&lt;/li&gt;
&lt;li&gt;存储额外字段：&lt;code&gt;password_version&lt;/code&gt;（调整参数或算法时逐步迁移）、&lt;code&gt;last_rotated_at&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;登录成功后如果检测到 hash 版本过旧，后台静默升级（rehash-on-login）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;OTP 与重置码&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;一次性验证码/重置码使用 &lt;code&gt;uuid&lt;/code&gt; 或 &lt;code&gt;crypto.randomBytes&lt;/code&gt; 生成，存储哈希值（&lt;code&gt;sha256&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;记录 &lt;code&gt;expires_at&lt;/code&gt;、&lt;code&gt;consumed_at&lt;/code&gt;，用数据库唯一约束防止并发使用。&lt;/li&gt;
&lt;li&gt;邮件/短信里不要包含用户 ID，用 token 换取身份。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;JWT/Session Secret&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;密钥统一放在 Secret Manager（Vault、AWS Secrets Manager）；禁用硬编码。&lt;/li&gt;
&lt;li&gt;轮换策略：生成新密钥、让旧密钥在合理宽限期内仍可验证（kid 机制）。&lt;/li&gt;
&lt;li&gt;Server-side session 存数据库或 Redis 时，同步记录 IP/UA，异常时可以踢出。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;注册流程最佳实践&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;前端校验&lt;/strong&gt;：限制密码强度、邮箱格式，但不要过度暴露错误信息。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;速率限制&lt;/strong&gt;：邮箱注册/验证码接口需做 Rate Limit + Captcha。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;邮箱验证&lt;/strong&gt;：注册时生成 email verification token，允许账号未验证时有限操作（如仅能登录一次提醒验证）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;邀请制&lt;/strong&gt;：提前创建 invitation 记录，关联 &lt;code&gt;inviter_id&lt;/code&gt;、&lt;code&gt;accepted_at&lt;/code&gt;，控制注册入口。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;事务处理&lt;/strong&gt;：创建用户、身份、发送验证邮件在事务中完成，失败时回滚。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;登录流程&lt;/h2&gt;
&lt;h3&gt;基础邮箱+密码&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;返回错误时统一提示“凭证错误”，避免区分“用户不存在”或“密码错误”。&lt;/li&gt;
&lt;li&gt;支持 remember me：&lt;code&gt;idle_expires&lt;/code&gt; 设置为 7-30 天，并通过 httpOnly + Secure Cookie 存储会话 ID。&lt;/li&gt;
&lt;li&gt;登录成功写入审计日志（&lt;code&gt;login_success&lt;/code&gt;），包括设备指纹。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;多因素认证（MFA）&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;支持 TOTP（Authenticator App）和备份代码（Backup Codes）。&lt;/li&gt;
&lt;li&gt;TOTP 种子加密存储（可用 KMS 包裹）。&lt;/li&gt;
&lt;li&gt;绑定流程：显示二维码 → 输入 6 位验证码确认 → &lt;code&gt;is_primary_mfa&lt;/code&gt; 标记。&lt;/li&gt;
&lt;li&gt;登录流程：先验证主凭证，再要求 2FA；允许“信任设备”7-30 天，通过 &lt;code&gt;mfa_remember_token&lt;/code&gt; 标记。&lt;/li&gt;
&lt;li&gt;恢复：备份代码一次性使用；客服通道需严格验证身份。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;WebAuthn / Passkeys&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;存储 WebAuthn credential 时记录 &lt;code&gt;credential_id&lt;/code&gt;, &lt;code&gt;public_key&lt;/code&gt;, &lt;code&gt;transports&lt;/code&gt;, &lt;code&gt;sign_count&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;对于 passkey，允许跨设备同步，不要绑定特定浏览器。&lt;/li&gt;
&lt;li&gt;WebAuthn 可作为无密码登录方式，也可作为 2FA。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;第三方登录（OAuth/OIDC）&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;创建 &lt;code&gt;oauth_providers&lt;/code&gt; 表记录配置信息（client_id、scopes、redirect URI）。&lt;/li&gt;
&lt;li&gt;将 provider 返回的 &lt;code&gt;sub&lt;/code&gt;/&lt;code&gt;id&lt;/code&gt; 等唯一标识与 &lt;code&gt;identities&lt;/code&gt; 表关联。&lt;/li&gt;
&lt;li&gt;若用户首次使用第三方登录，需要流程：
&lt;ul&gt;
&lt;li&gt;找到与邮箱匹配的用户 → 提示合并或拒绝；&lt;/li&gt;
&lt;li&gt;无匹配时创建新用户（可直接标记邮箱已验证）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;定期刷新 access_token/refresh_token；失效时告知用户重新授权。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;会话管理&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;单点登录/多设备&lt;/strong&gt;：允许多个活跃 session；提供“查看并注销其他设备”功能。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;活跃检测&lt;/strong&gt;：&lt;code&gt;active_expires&lt;/code&gt; 表示 token 有效期，&lt;code&gt;idle_expires&lt;/code&gt; 表示最大不活跃时间。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;注销&lt;/strong&gt;：服务端删除 session 记录，删除 cookie/localStorage；对于 JWT 需维护 denylist 或缩短过期时间。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Remembered Device&lt;/strong&gt;：存储在 &lt;code&gt;trusted_devices&lt;/code&gt; 表，包含指纹、首次信任时间、最后使用时间。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Session Fixation&lt;/strong&gt;：登录成功后一定要 rotate session ID。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;API 设计示例&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;POST /auth/register
POST /auth/login
POST /auth/logout
POST /auth/refresh
POST /auth/mfa/setup
POST /auth/mfa/verify
POST /auth/password/request-reset
POST /auth/password/reset
GET  /auth/sessions
DELETE /auth/sessions/{sessionId}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;返回格式建议统一：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;session&quot;: {
    &quot;id&quot;: &quot;sess_123&quot;,
    &quot;expiresAt&quot;: &quot;2025-05-01T00:00:00Z&quot;
  },
  &quot;user&quot;: {
    &quot;id&quot;: &quot;user_456&quot;,
    &quot;displayName&quot;: &quot;Alice&quot;,
    &quot;hasMfa&quot;: true
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;错误响应包含 &lt;code&gt;code&lt;/code&gt;（内部追踪）与 &lt;code&gt;message&lt;/code&gt;（给用户），必要时加 &lt;code&gt;metadata&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;code&quot;: &quot;AUTH_INVALID_TOTP&quot;,
  &quot;message&quot;: &quot;验证码无效或已过期，请重试&quot;,
  &quot;metadata&quot;: {
    &quot;remainingAttempts&quot;: 3
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;管理后台与支持流程&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;管理后台应具备：搜索用户、查看身份信息、重置密码、禁用账号、强制注销等能力。&lt;/li&gt;
&lt;li&gt;所有敏感操作（禁用、冻结资金）必须记录审计日志，含操作者 ID 与理由。&lt;/li&gt;
&lt;li&gt;支持团队需要明确定义升级流程：例如社交账号被盗、设备遗失、申诉流程。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;风险控制与检测&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;登录异常检测：同一账号在短时间内不同国家登录、IP 风险评分。&lt;/li&gt;
&lt;li&gt;密码喷射（password spraying）：统计不同邮箱的失败次数，整体限流。&lt;/li&gt;
&lt;li&gt;设备指纹：可以使用指纹库或自定义 UA + 分辨率 + Canvas 指纹（注意隐私合规）。&lt;/li&gt;
&lt;li&gt;风控触发时增加验证因素（Step-up Auth），例如要求验证码或 2FA。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;审计与隐私&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;审计日志表存储：&lt;code&gt;event&lt;/code&gt;, &lt;code&gt;user_id&lt;/code&gt;, &lt;code&gt;session_id&lt;/code&gt;, &lt;code&gt;actor_id&lt;/code&gt;, &lt;code&gt;ip&lt;/code&gt;, &lt;code&gt;user_agent&lt;/code&gt;, &lt;code&gt;metadata&lt;/code&gt;, &lt;code&gt;created_at&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;遵守最小化原则：只收集必要数据，设定数据保留期限（GDPR、CCPA）。&lt;/li&gt;
&lt;li&gt;提供数据导出与删除接口（Data Subject Request）。&lt;/li&gt;
&lt;li&gt;避免在日志里记录敏感信息，如密码或完整 token。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;常见坑位与应对&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;跨域 Cookie 失效&lt;/strong&gt;：确保设置 &lt;code&gt;SameSite=None&lt;/code&gt; 且 &lt;code&gt;Secure&lt;/code&gt;，以及后台与前端域名一致。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CSRF&lt;/strong&gt;：推荐使用 SameSite Cookie + 双提交 Cookie 或 CSRF Token。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;验证码滥用&lt;/strong&gt;：引入滑块、人机验证服务，或对高危 IP 拦截。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缓存问题&lt;/strong&gt;：包含授权信息的响应 header 应禁用 CDN 缓存。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;时钟漂移&lt;/strong&gt;：TOTP 校验容忍 ±30s；服务部署多地区需同步 NTP。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;推荐工具与库&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Lucia&lt;/strong&gt;：TypeScript 生态下的身份框架，书中示例基于它。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;NextAuth.js / Auth.js&lt;/strong&gt;：前端友好的 Next.js 解决方案。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Ory / Supabase Auth&lt;/strong&gt;：托管身份平台，适合 MVP。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;FGA / CASL&lt;/strong&gt;：做细粒度授权（Access Control）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Temporal / Q Stash&lt;/strong&gt;：处理邮件发送、密码重置等异步任务。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;进一步阅读&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://thecopenhagenbook.com/&quot;&gt;The Copenhagen Book 原文&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://owasp.org/www-project-application-security-verification-standard/&quot;&gt;OWASP ASVS&lt;/a&gt;：认证与会话安全条目。&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://pages.nist.gov/800-63-3/sp800-63b.html&quot;&gt;NIST 800-63B&lt;/a&gt;：数字身份指南（密码、MFA 建议）。&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://webauthn.guide/&quot;&gt;WebAuthn Guide&lt;/a&gt;：无密码认证详解。&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://auth0.com/blog/&quot;&gt;Auth0 Blog&lt;/a&gt;：实战文章与攻防视角。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>主动与被动</title><link>https://bangwu.me/posts/initiative-passive/</link><guid isPermaLink="true">https://bangwu.me/posts/initiative-passive/</guid><description>在人际关系里总是被动的那个，等着别人来约、来问、来找。这篇写的是想改变但又不知道从哪开始的纠结。</description><pubDate>Fri, 25 Oct 2024 05:31:56 GMT</pubDate><content:encoded>&lt;p&gt;&amp;lt;audio controls data-title=&quot;再见（good bye）&quot; data-artist=&quot;G.E.M.邓紫棋&quot; data-cover=&quot;https://cdn.bangwu.me/img/20260111vyqpt-tg.webp&quot;&amp;gt;
&amp;lt;source src=&quot;https://cdn.bangwu.me/img/20260111548a9fcf.mp3&quot; type=&quot;audio/mpeg&quot;&amp;gt;
&amp;lt;/audio&amp;gt;&lt;/p&gt;
&lt;h2&gt;被动的自己&lt;/h2&gt;
&lt;p&gt;算是最近一两年吧，越来越纠结主动与被动的事情，我是一个有点社恐的人，所以大多数情况下关乎与人有关的事，都是秉承着被动的态度：聊天时等着对方先开口，组队时等着被邀请才进队，自己娱乐时也不会叫朋友一起......但是想想这样也有点不好，如果等来的不是主动那就只有沉默。最近在读王小波的《沉默的大多数》，里面提到话语权，说到话语是有力量的，但不幸的是大多数人选择了沉默。&lt;strong&gt;人人有权争胜负，无人有权论是非&lt;/strong&gt;。我在想大多数沉默的人可能和我一样，是在一遍遍确认自己是否被坚定的选择吧。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.bangwu.me/img/20260111a1ba9188.png&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;想改变但不知道从哪开始&lt;/h2&gt;
&lt;p&gt;网上有句很流行的话“不要试图去改变一个人”，我想这可能也是一个原因，如果本该是选择我的话，那我无需说什么也会被选择，但不是我的话，做什么也是无济于事的。于是，就慢慢的学会了“不说”，慢慢觉得这样其实也不错，不主动不靠近就不会受伤啊。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;克洛伊，我不要你爱我，爱是束缚， 爱要我同样去爱。而我要的是自由。 期待是情感的一笔债务。——《我的心迟到了》&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;说说被动，我被动的接受周围的一切包括世界，无关勇气，只是觉得主动既浪费了我的热情也败了事务的芳华。去年到现在，很长一段时间大家喜欢讨论自己的人格MBTI，最多的就是i人与e人之分，i人就是大多数被动的那个，渴望被坚定的选择。我身边关系特别好的朋友有好几个，但我从来没跟他们说过要坚定的选择我，我不想成为选项，也幸运人与人之间相处都是有一些未说得出口但都懂的话语。却跟过一个网友说过，固定的玩伴需要被坚定的选择，我说如果下次犹豫在两个或多个之间选择的话，那请把我排除，我从来都不是任何选项中的一个，这并不是所有人都需要遵循的，那样在这个时代也有点离谱了。拧巴的人需要被坚定的选择，只要是你，我都愿意。没有你的话，我做不下去。
&lt;img src=&quot;https://cdn.bangwu.me/img/2026011193c2887d.jpg&quot; alt=&quot;Clip_2024-10-25_13-22-24&quot; /&gt;&lt;/p&gt;
&lt;p&gt;不得不说，我很羡慕那些主动的人，虽然我内心还是觉得“主动就是占下风”“主动被拒绝会很伤心”，主动需要的是勇气。有勇气去追寻自己所喜欢的，自己所想的、感到不舒服也会说出来。写到这里我发现我写不出来了，我沉寂了好久，自己不是一个主动的的人，也不会有太多的感受啦，但请敢爱敢恨！&lt;/p&gt;
&lt;p&gt;亲爱的朋友，请不要缺少对一切说再见的勇气。被动与否，请果断一点，大声地说出“再见”&lt;/p&gt;
&lt;p&gt;“那些慢吞吞悲情的音乐 早说过爱过之后就是离别”“不要对我说再见 一句再见 就结束这一切”&lt;/p&gt;
</content:encoded></item><item><title>一文解决python爬虫</title><link>https://bangwu.me/posts/python-crawler-all/</link><guid isPermaLink="true">https://bangwu.me/posts/python-crawler-all/</guid><description>Python 爬虫方案全景对比：requests+BeautifulSoup 的静态解析 vs Selenium/Playwright 的动态渲染，以及各自的成本与收益。</description><pubDate>Fri, 25 Oct 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;爬虫&lt;/h2&gt;
&lt;p&gt;无论是做科研还是做开发者都需要一个数据基础，有时是图片、音频，又有时是一张表格、一句评论。但凡是所有能在互联网上触及到的东西，爬虫都能够把它们采集下来，整理成你需要的数据。这里就不过多介绍了，更多介绍请看&lt;a href=&quot;https://zh.wikipedia.org/wiki/%E7%B6%B2%E8%B7%AF%E7%88%AC%E8%9F%B2&quot;&gt;维基&lt;/a&gt;。&lt;/p&gt;
&lt;h2&gt;抓包工具&lt;/h2&gt;
&lt;p&gt;抓包是写爬虫过程中必不可少的部分，目的是获取请求过程的&lt;code&gt;请求URL&lt;/code&gt;、&lt;code&gt;Headers&lt;/code&gt;、&lt;code&gt;请求体&lt;/code&gt;等信息，然后编写程序模拟这个过程，好的爬虫总会是模拟一个真实的人类行为，来进行请求数据。抓包工具我只推荐&lt;a href=&quot;https://reqable.com/&quot;&gt;Reqable&lt;/a&gt;：全平台，特别推荐，我也在用。其他的可以去查查，工具只要顺手就行，达到目的就可以。&lt;/p&gt;
&lt;h2&gt;Python网络请求库&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;requests&lt;/th&gt;
&lt;th&gt;aiohttp&lt;/th&gt;
&lt;th&gt;httpx&lt;/th&gt;
&lt;th&gt;pycurl&lt;/th&gt;
&lt;th&gt;curl_cffi&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;http2&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;sync&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;async&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;websocket&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;fingerprints&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;speed&lt;/td&gt;
&lt;td&gt;🐇&lt;/td&gt;
&lt;td&gt;🐇🐇&lt;/td&gt;
&lt;td&gt;🐇&lt;/td&gt;
&lt;td&gt;🐇🐇&lt;/td&gt;
&lt;td&gt;🐇🐇&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Python常用的网络请求库如上所示(来源于&lt;a href=&quot;https://github.com/lexiforest/curl_cffi&quot;&gt;curl_cffi&lt;/a&gt;)，其中最常用的是requests，可能是api比较方便吧。&lt;/p&gt;
&lt;p&gt;::github{repo=&quot;psf/requests&quot;}&lt;/p&gt;
&lt;p&gt;从上表可以看出，支持异步的有httpx和curl_cffi，pycurl不推荐。httpx被称作为是下一代python网络请求库，我也非常推荐日常爬虫使用这个库，因为用它写异步特别方便。&lt;/p&gt;
&lt;p&gt;::github{repo=&quot;encode/httpx&quot;}&lt;/p&gt;
&lt;p&gt;一个httpx异步程序可以是这样的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import asyncio
import httpx
import time


start_time = time.time()


async def get_pokemon(client, url):
    resp = await client.get(url)
    pokemon = resp.json()

    return pokemon[&apos;name&apos;]


async def main():

    async with httpx.AsyncClient() as client:

        tasks = []
        for number in range(1, 151):
            url = f&apos;https://pokeapi.co/api/v2/pokemon/{number}&apos;
            tasks.append(asyncio.ensure_future(get_pokemon(client, url)))

        original_pokemon = await asyncio.gather(*tasks)
        for pokemon in original_pokemon:
            print(pokemon)

asyncio.run(main())
print(&quot;--- %s seconds ---&quot; % (time.time() - start_time))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::note&lt;/p&gt;
&lt;p&gt;需要注意的是，异步请求之所以快是因为网络阻塞，如果网络阻塞很小的话（例如进行本地请求）使用异步反而会减速。这样的话使用多线程会更快&lt;/p&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;p&gt;curl_cffi是一个可以模拟浏览器指纹的请求库，官方宣称速度比httpx还快，我还没有尝试，毕竟还没遇到与浏览器指纹拦截相关的问题，速度的话httpx已经非常快了并且curl_cffi的普及度好像很小，感兴趣的可以去了解了解&lt;/p&gt;
&lt;p&gt;::github{repo=&quot;lexiforest/curl_cffi&quot;}&lt;/p&gt;
&lt;p&gt;还有一种最万能的爬虫方案：使用selenium或者playwright这两个web自动化框架，如果都没接触过推荐学习playwright。下面是一个大神的国内社交媒体的爬虫，使用playwright实现，非常强大！&lt;/p&gt;
&lt;p&gt;::github{repo=&quot;NanmiCoder/MediaCrawler&quot;}&lt;/p&gt;
&lt;h2&gt;Python网络解析库&lt;/h2&gt;
&lt;p&gt;网络解析有几种方法，主要是解析HTML，因为json格式很好读取，解析也很方便。&lt;/p&gt;
&lt;p&gt;:::note&lt;/p&gt;
&lt;p&gt;json解析的话，除了python内置的json库，还可以用性能更高的ujson库，之前读取一个2GB多的json文件用内置库很卡，ujson很快&lt;/p&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;p&gt;而解析HTML格式的话，需要进行标签和属性匹配，得到具体想要的数据，例如图片链接、表格数据等。常用的解析方式有正则(re)、BeautifulSoup(bs4)、Xpath(lxml) 三种方式，其中BeautifulSoup和Xpath适合简单地解析，正则用来提取出特定规则的数据。以下提供这三个库的简单的api调用&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# re https://docs.python.org/zh-cn/3.12/library/re.html
import re
 
line = &quot;Cats are smarter than dogs&quot;
 
matchObj = re.match( r&apos;(.*) are (.*?) .*&apos;, line, re.M|re.I)
 
if matchObj:
   print &quot;matchObj.group() : &quot;, matchObj.group()
   print &quot;matchObj.group(1) : &quot;, matchObj.group(1)
   print &quot;matchObj.group(2) : &quot;, matchObj.group(2)
else:
   print &quot;No match!!&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;# BeautifulSoup   pip install bs4
from bs4 import BeautifulSoup
# 读取示例 HTML 文件
html = &quot;&quot;&quot;
&amp;lt;html&amp;gt;
    &amp;lt;head&amp;gt;
        &amp;lt;title&amp;gt;Example Page&amp;lt;/title&amp;gt;
    &amp;lt;/head&amp;gt;
    &amp;lt;body&amp;gt;
        &amp;lt;h1&amp;gt;Hello, World!&amp;lt;/h1&amp;gt;
    &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&quot;&quot;&quot;
 
# 创建 BeautifulSoup 对象，并指定解析器为 lxml
soup = BeautifulSoup(html, &apos;lxml&apos;)
 
# 找到第一个 h1 标签，并输出其文本内容
h1 = soup.find(&apos;h1&apos;)
print(h1.text)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;# Xpath   pip install lxml
from lxml import etree
text = &apos;&apos;&apos;
&amp;lt;div&amp;gt;
    &amp;lt;ul&amp;gt;
         &amp;lt;li class=&quot;item-0&quot; id=&quot;id1&quot;&amp;gt;&amp;lt;a href=&quot;link1.html&quot;&amp;gt;first item&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;
         &amp;lt;li class=&quot;item-1&quot;&amp;gt;&amp;lt;a href=&quot;link2.html&quot;&amp;gt;second item&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;
         &amp;lt;li class=&quot;item-inactive&quot;&amp;gt;&amp;lt;a href=&quot;link3.html&quot;&amp;gt;third item&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;
         &amp;lt;li class=&quot;item-1&quot;&amp;gt;&amp;lt;a href=&quot;link4.html&quot;&amp;gt;fourth item&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;
         &amp;lt;li class=&quot;item-0&quot; id=&quot;id2&quot;&amp;gt;&amp;lt;a href=&quot;link5.html&quot;&amp;gt;fifth item&amp;lt;/a&amp;gt;
     &amp;lt;/ul&amp;gt;
 &amp;lt;/div&amp;gt;
&apos;&apos;&apos;
html = etree.HTML(text)
print(etree.tostring(html))
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;数据保存&lt;/h2&gt;
&lt;p&gt;简单的数据保存推荐使用&lt;code&gt;pandas.DataFrame&lt;/code&gt;将需要的字段以二维表的方式存储起来，然后选择输出文件类型(csv, xlsx, pickle)&lt;/p&gt;
&lt;p&gt;因为爬取过程中会出现反爬等原因导致请求不到数据，然后就会需要耗费时间将之前的请求再请求一遍或者调整代码之类的，如果是单文件的话就无法将之前的数据保存为文件，无法持久化数据，使用jupyter notebook可以解决此问题但我觉得还不是最佳方案，我认为可以使用sqlite数据库然后用ORM库 SqlModel来映射，这样就可以持久化数据而且使用强大的SQL来查询、筛选数据。&lt;/p&gt;
&lt;h2&gt;框架&lt;/h2&gt;
&lt;p&gt;python爬虫框架最常用的是&lt;code&gt;Scrapy&lt;/code&gt;，使用框架的好处是只需要考虑数据解析的问题，其他网络请求、数据保存、错误处理等问题框架都提供了比较完善的方案，再次不多做说明，毕竟用到框架是处理很复杂的爬虫方案的，我目前还没接触到，学习框架也会浪费一定精力。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;本文侧重静态页面解析（requests + BeautifulSoup + lxml）。如果你需要处理 JavaScript 动态渲染、登录态保持或模拟用户操作，请移步《Python 自动化浏览器实践》。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>第一篇声明</title><link>https://bangwu.me/posts/firstpost/</link><guid isPermaLink="true">https://bangwu.me/posts/firstpost/</guid><description>棒无的个人博客正式开张。这篇是第一篇文章，简单交代了这个博客的来龙去脉和想写的东西。</description><pubDate>Fri, 18 Oct 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;棒无的声明&lt;/h2&gt;
&lt;p&gt;Hello，这里是棒无.经过大约一年半的挣扎，最终我选择了使用Astro搭建了博客这是我发布的第一篇真正意义上的文章，在此做个声明😐&lt;/p&gt;
&lt;p&gt;::github{repo=&quot;saicaca/fuwari&quot;}&lt;/p&gt;
&lt;h2&gt;我的建站历史&lt;/h2&gt;
&lt;p&gt;记得第一次接触到云服务器、域名、博客等概念还是大一下学期的某一天。那时候刷到一个视频，想着有个云服务器真的好酷啊，于是我打开了阿里云的网站然后看到有个大学生活动，可以免费领到云服务器，虽然配置很低，却还是激动不已。记得第一次使用ssh连接到服务器后，输入&lt;code&gt;sudo apt update &amp;amp;&amp;amp; sudo apt upgrade -y&lt;/code&gt;后终端不断跑出来一行一行进度，虽然是我看不懂的英文，但就像之前使用&lt;code&gt;pip install&lt;/code&gt;那样，真的很爽。后面就去搜搭建博客的教程，现在想想还真是一入深似海啊，按照普及度来说，我不出意外的安装了宝塔面板，使用了WordPress搭建博客，起初配置起来还算简单，但是到后面提供的主题不足以满足我之后，我就去查找WordPress主题，然后安装、定制，又折腾了好长时间，那时像一个无头苍蝇一样到处碰壁。就这样，折腾了小半个月的我最终还算是把主题什么的都配置好了，但是我突然发现，我不知道写什么。在后台文章编辑界面写了又写，只写出个&lt;code&gt;print(&apos;hello world!&apos;)&lt;/code&gt;，那时我也没有域名，服务器也要过期了，又快接近暑假，后面建站的事就不了了之了。&lt;/p&gt;
&lt;p&gt;2023年9月23日，我经过折腾了几天后，正式用上了&lt;a href=&quot;https://vitepress.dev/&quot;&gt;Vitepress&lt;/a&gt;搭建了&lt;a href=&quot;https://github.com/markbang/knowledge/commit/f4a41c46a78b2ee0e35a7eff0121024a5c9b22a4&quot;&gt;知识库&lt;/a&gt;后面就有一搭没一搭地写着学过的知识，但是我发现个问题，我折腾这些静态框架，自定义配置，然后浪费了很多时间，配置完成后也没有去持续输出文章知识，大部分时间都花在了折腾上面。然后就到了后来，接触到了&lt;code&gt;vercel&lt;/code&gt;、&lt;code&gt;vue&lt;/code&gt;、&lt;code&gt;react&lt;/code&gt;、&lt;code&gt;astro&lt;/code&gt;、&lt;code&gt;tailwindcss&lt;/code&gt;、各种UI库等等，但全是浅尝辄止，没有坚持深入下去。&lt;/p&gt;
&lt;p&gt;gap一年修复自己后，2024年9月开学，我准备真正做点什么，想来想去准备写一些文章，虽然之前也有写过一些，但是都不算真正意义上的技术博客。起初因为学过一点前端，想自己手搓一个博客的，后来还是放弃了，我写不出精美的排版🥲然后，就没然后了，我开始真正写博客了，这让我想起了之前看过的Anthon Fu大佬的文章，&lt;a href=&quot;https://antfu.me/posts/about-yak-shaving-zh&quot;&gt;Yak Shaving&lt;/a&gt;，不要想那么多，先聚焦一件事干好！&lt;/p&gt;
&lt;h2&gt;我的信息流&lt;/h2&gt;
&lt;p&gt;说到信息流就不得不提到RSS了，我自从接触到RSS就一直在感叹其强大，可以通过&lt;a href=&quot;https://now.bangwu.top/rss.xml&quot;&gt;now&lt;/a&gt;订阅关于我的互联网活动&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.bangwu.me/img/my-media-network.webp&quot; alt=&quot;my-media-network&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;内容输出&lt;/h2&gt;
&lt;p&gt;我学习的主要是全栈偏向后端，Python、Go、React以及一些运维的知识，很杂但我保证质量很高，不会敷衍任何一篇文章，就这样了。&lt;/p&gt;
</content:encoded></item><item><title>人太多大部分是漫无目的地走</title><link>https://bangwu.me/posts/butaire/</link><guid isPermaLink="true">https://bangwu.me/posts/butaire/</guid><description>从小镇走到城市，从一个人到融入人群。这篇写的是成长过程中那些说不出口的自卑和漂泊感。</description><pubDate>Tue, 01 Oct 2024 16:17:50 GMT</pubDate><content:encoded>&lt;p&gt;&amp;lt;audio controls data-title=&quot;东京不太热&quot; data-artist=&quot;一北玖&quot; data-cover=&quot;https://cdn.bangwu.me/img/20260111vl8yk-rw.webp&quot;&amp;gt;
&amp;lt;source src=&quot;https://cdn.bangwu.me/img/2026011123a157f9.mp3&quot; type=&quot;audio/mpeg&quot;&amp;gt;
&amp;lt;/audio&amp;gt;&lt;/p&gt;
&lt;h2&gt;自卑与漂泊&lt;/h2&gt;
&lt;h2&gt;还在路上&lt;/h2&gt;
&lt;p&gt;“没人给你过生日吗？”朋友在一次闲聊中这样问我。我心头一颤，回过神来才想着自己已经19岁了，到底多久没有过上一个正式的生日了。“身边大家都不互相庆生，就不是很重视喽”我应付地回到。记得那时我还在上小学，破天荒的过上了生日，姥姥和舅舅竟然到我家来还给我带了蛋糕，那时我没想过以后过不上这样的生日，那时只有开心。小时候的快乐很简单，大概是五毛钱的辣条，开心就大声笑，难过就表现在脸上，悲伤就哭出来，从不畏惧表达。大概是当时有父母的撑腰或是儿时的单纯吧，小时候的父母还有哥哥在我心里都很强大，强大到可以满足我能想到的任何事情。不知从哪段时间起，我慢慢了解到了“现实”，它告诉我这个世界的一切都是有价格的，没有足够的经济就无法去做到某件事情，起初我不相信，我把心爱的玩具赛车拿去学校和同学们一起比试速度，看着他随意一跑的距离就以是我的极限，“这是我爸爸从外地买的”这句话从我耳边飘过，慢慢的，我意识到童年也是有价格的。大多数同学选择了沉默不语，我也是其中的一员。后来心里就慢慢种下了自卑的种子，和别人聊天表达自己的观点时，我会感觉到很不自在。“我觉得粒上皇的板栗比较好吃，你觉得呢”，“还行吧”我不敢说我没吃过，可能那样我认为比别人落后了。我总是这样，一边认清了现实却又不敢去接受，就这样把自己封闭起来，等着哪天我吃上了各种店的板栗再去说我的观点。&lt;/p&gt;
&lt;p&gt;就这样，我是村里长大的孩子，小学和初中都算是在农村上的吧，条件并不是很好，好在当时要争一口气，考上了镇上普高的小班，当时可高兴了，我觉得终于可以有机会改变自己的命运了。全家很重视我的学业，在上高中的三年基本是我说东就是东，妈妈可以为了我很早起来去买早饭。高中的同学家境基本都很好，都算是小康吧（因为小班可以托关系进）当时和他们相处，我就慢慢忘却了我是乡下走出来的。在这期间，和初中同学交谈，我多了一份虚无缥缈的自信。三年过的很快，又因为疫情的原因，在学校的日子大概也就两年，现在想想当时是真好啊，家人就在身边，我也不需要考虑现在要考虑的各种问题，只知道把每一场考试都做好就行了。初中的时候成了班里的活宝，没想到高中时也是，他们给我起了个“6班人气王”这个称号，我觉得能给别人带来快乐很有意义，所以基本能接受各种无下限的玩笑。高考完之后，我想我终于能去别的地方见见世面了，于是我把志愿填在了繁华的上海，想着这下我就算是“有见识”了吧。随着迈入大学的大门，哥哥在门外目送我，我回了一下头看着他，好像在说“我可以的！”。&lt;/p&gt;
&lt;p&gt;再回头，我已经大三了。所以成长带给我的是什么呢，我感觉自己好像还是从前的那个小孩。喜欢把自己封闭起来，懦弱，不敢交流，以他人为中心。上了大学后，我经常在深夜emo，多是因为现实和自己的理想不一样吧。过了那么长的时间，我并没有学会好好学习，也没有谈一场轰轰烈烈的恋爱，就只是经常发呆有一搭没一搭的想着事情。又会因为某一瞬间想起以前生活里的某一时刻、一个地方或是一个人而纠结到半夜睡不着，或是白天因为别人的一句话而悲伤不已，但这些似乎都已经成为了我生活的一部分，我像疯狂嗑药一样贪婪地令这一切发生。我觉得我应该只能活在偶然间的回忆里吧。小时候想去大城市看看，长大后来到上海站在外滩的围栏边，我抬头望着对岸的CBD心中却没有一丝波澜，这些像是我的一个老朋友，经常在我的幻想里出现，长大就是可以活在大城市吗？&lt;/p&gt;
&lt;p&gt;直到某一天过年走亲戚，不再跟着我爸一起去，而是我和我哥。那天我、我哥和表哥，我们一起喝了个烂醉，哥哥还在喝酒时一直提醒我别喝那么多。那天喝了好久，久到忘却了时间，只知道要起身离开的时候天色已有点昏暗，我还是没能表达出我沉溺已久的想法，我想是喝的不够多吧。回去的时候坐在哥的车里，我坚持说我喝的不多没事，眼睛望着窗外，看着外面的车流，我知道自己再也变不回那个单纯的小孩了。
我总是和身边朋友打趣道“这也太搞了吧”“抽象完了，孩子”.......却只敢在喝酒后与朋友说说心里话，讲讲我的经历，谈谈我的感受，但还是吞吞吐吐说了半天才说出“加油”。不知道从什么时候起，我开始渴望喝醉，好像只有那时我是真真正正的我，却还是有只无形的手拉着克制我去表达。别人总说我情绪稳定，但生活中我总是阴晴不定，欠缺表达就总是平淡。朋友无聊的问我“要去干啥”我总是回“我也不知道”，但其实我想说就这样陪着我就好，只要和你一起干啥都行。&lt;/p&gt;
&lt;p&gt;我也有自己的理想，也有不切实际的期待，想去更远的远方，也有自己生活的节奏，晚上吃完饭没事的时候会偶尔去公园散步，戴上耳机听着歌发呆，或是跑步就好。喜欢安静和规划，但又因为随性做一些不太正常的事情。我知道自己不是什么有天分的人，也不是很努力的人，每次上课都害怕轮到我发言，我总是害怕在很多人面前说话。非常羡慕那些能在人群里侃侃而谈的人，但自己又很难成为他们。我警惕所有令我迷恋的事物，以防止失去后的痛楚。抑郁就像你玩一个游戏太长时间，已经厌倦了，然后就退游了，但是人生这场游戏你不能退游，就只能抑郁了。我想：只要我用最真挚的感情去演奏接下来的人生就好了吧。
“人太多大部分是漫无目的地走，听着歌想念一个人心痛也不说...”&lt;/p&gt;
</content:encoded></item><item><title>ShareX-截图webp格式</title><link>https://bangwu.me/posts/sharexmd/</link><guid isPermaLink="true">https://bangwu.me/posts/sharexmd/</guid><description>用 ShareX 截图后自动转换为 WebP 格式的完整配置教程，包含压缩参数设置和常见问题解决。</description><pubDate>Wed, 03 Apr 2024 13:44:01 GMT</pubDate><content:encoded>&lt;p&gt;ShareX是一个非常强大的开源截图并分享软件，提供了非常多的自定义功能，我就是看上的它的可以自动压缩截图图片为webp格式，大大降低的图片大小。软件结构：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.bangwu.me/img/20260111afcd0562.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;下载地址：&lt;a href=&quot;https://github.com/ShareX/ShareX/releases/tag/v16.0.1&quot;&gt;https://github.com/ShareX/ShareX/releases/tag/v16.0.1&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;建议下载portable版，自带ffmpeg，可以直接用来压缩&lt;/p&gt;
&lt;p&gt;然后在 sharex 的动作设置中，添加一个动作：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.bangwu.me/img/20260111cba8efe3.webp&quot; alt=&quot;chrome_1712151713&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;名称: 随意&lt;/li&gt;
&lt;li&gt;文件路径: ffmpeg 的安装路径，要指定到 ffmpeg 可执行文件&lt;/li&gt;
&lt;li&gt;参数: &lt;code&gt;-i &quot;$input&quot; -q 75 &quot;$output&quot;&lt;/code&gt; 其中 -q 75 是以75%质量压缩&lt;/li&gt;
&lt;li&gt;输出文件扩展名: webp&lt;/li&gt;
&lt;li&gt;扩展名筛选: png&lt;/li&gt;
&lt;li&gt;勾上下面的 隐藏窗口 和 删除输入文件&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;然后在截图后的任务里勾选上&lt;code&gt;执行操作&lt;/code&gt;就OK了&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.bangwu.me/img/2026011191c681ec.webp&quot; alt=&quot;chrome_1712151800&quot; /&gt;&lt;/p&gt;
</content:encoded></item></channel></rss>