<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>w2solo - 独立开发者社区</title>
    <link>https://www.w2solo.com/</link>
    <description>w2solo - 独立开发者社区社区最新发帖.</description>
    <language>en-us</language>
    <item>
      <title>如何快速制作高考班级毕业去向分布图</title>
      <description>&lt;p&gt;高考已经落下帷幕，在 6 月 23 日全国各省就将陆续迎来出分、报志愿和录取... 在这少年各赴山海之际，如何用一张毕业去向分布图，留住全班奔赴各地的温柔印记？&lt;/p&gt;

&lt;p&gt;今天给大家介绍我打造的这款毕业蹭饭图快速制作工具。不需要任何专业设计，轻松在线操作，就能生成一张专属班级的毕业去向分布图。&lt;/p&gt;

&lt;p&gt;&lt;img src="https://huayemao.run/api/files/e2c8ca9f8f8df8e0.png" title="" alt="毕业蹭饭图快速制作工具界面截图"&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;打开 &lt;a href="https://uni.utities.online/" rel="nofollow" target="_blank" title=""&gt;china edu atlas 高校名录数据可视化平台&lt;/a&gt;，点击进入&lt;a href="https://uni.utities.online/map-creator" rel="nofollow" target="_blank" title=""&gt;毕业去向分布图制作工具&lt;/a&gt;
&lt;img src="https://huayemao.run/api/files/30092601e8890916.png" title="" alt="在 china edu atlas 高校名录数据可视化平台主页，点击上方导航栏中的毕业去向分布图制作工具菜单项，进入该功能页面"&gt;
&lt;/li&gt;
&lt;li&gt;在标题设置中修改地图标题，如"师大附中 2026 届 5 班毕业去向分布图"
&lt;img src="https://huayemao.run/api/files/8a98a102877d8b91.webp" title="" alt="在标题设置中修改要制作的蹭饭图的标题信息"&gt;
&lt;/li&gt;
&lt;li&gt;点左上角进入数据选项卡，清除预置数据
&lt;img src="https://huayemao.run/api/files/460231a6a2e97096.webp" title="" alt="清除预置的录取信息数据"&gt;
&lt;/li&gt;
&lt;li&gt;手动输入班级内的学生录取院校信息，也可以根据固定格式的 excel 模板，一键导入 excel 表格。
&lt;img src="https://huayemao.run/api/files/c157b6f4c6d99095.png" title="" alt="点击按钮，添加学生录取信息"&gt;
&lt;img src="https://huayemao.run/api/files/e85b4e5dcb61276d.png" title="" alt="填写同学姓名、录取院校、省份后，右侧地图上就可以实时预览出该条目信息"&gt;
&lt;/li&gt;
&lt;li&gt;自由挑选喜欢的样式与配色：数据录入完成后，可切换到样式 Tab ，调整布局风格、配色组合、字体配置、其他布局参数等。
&lt;img src="https://huayemao.run/api/files/a83e6a8e8972b3c6.webp" title="" alt="数据录入完成后，调整毕业蹭饭图的样式"&gt;
&lt;/li&gt;
&lt;li&gt;导出图片：点击右上角的【导出高清图】按钮导出制作好的班级毕业去向图。或者也可直接截图保存。
&lt;img src="https://huayemao.run/api/files/71c338671405bda5.webp" title="" alt="点击【导出高清图】按钮，导出得到高考班级毕业去向图高清图片"&gt;
&lt;/li&gt;
&lt;/ol&gt;</description>
      <author>huayemao</author>
      <pubDate>Sun, 21 Jun 2026 15:46:12 +0800</pubDate>
      <link>https://www.w2solo.com/topics/7568</link>
      <guid>https://www.w2solo.com/topics/7568</guid>
    </item>
    <item>
      <title>Codex App 接上微信，我开始在厕所里改 Bug 了</title>
      <description>&lt;p&gt;这两天折腾了一个挺有意思的东西：&lt;/p&gt;

&lt;p&gt;把个人微信接到 Codex 上。&lt;/p&gt;

&lt;p&gt;以后不用一直坐在电脑前开着 Codex APP。人在外面，手机微信里发一句话，家里或办公室那台电脑上的 Codex 就能收到任务，继续帮你改代码、查项目、跑命令、整理文件。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;项目地址&lt;/strong&gt;
&lt;a href="https://github.com/Gan-Xing/CodexBridge" rel="nofollow" target="_blank"&gt;https://github.com/Gan-Xing/CodexBridge&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;它做的事情很直接：把微信消息转成 Codex 能处理的请求，再把 Codex 的回复发回微信。
&lt;img src="https://gitee.com/da-qiang-classmate/typora/raw/master/image/cover-wechat-codex-bridge-apimart.webp" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;简单理解就是：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;个人微信 -&amp;gt; CodexBridge -&amp;gt; 本机 Codex -&amp;gt; CodexBridge -&amp;gt; 个人微信
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Codex 还是跑在你的电脑上，项目文件也还是在你的电脑上。微信只是多了一个远程入口。&lt;/p&gt;

&lt;p&gt;&lt;img src="https://gitee.com/da-qiang-classmate/typora/raw/master/image/flow-wechat-codex-bridge.webp" title="" alt=""&gt;&lt;/p&gt;
&lt;h3 id="最简单的接入方式"&gt;最简单的接入方式&lt;/h3&gt;
&lt;p&gt;如果你已经安装好了 Codex APP，可以直接把下面这句话发给 Codex：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://github.com/Gan-Xing/CodexBridge 帮我对接个人微信
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src="https://gitee.com/da-qiang-classmate/typora/raw/master/image/20260620233854951.webp" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;Codex 会自动处理后面的流程：克隆项目、安装依赖、检查环境、生成二维码、等待扫码、启动桥接服务。&lt;/p&gt;
&lt;h3 id="扫码登录"&gt;扫码登录&lt;/h3&gt;
&lt;p&gt;依赖安装完之后，它会给你一个二维码。&lt;/p&gt;

&lt;p&gt;&lt;img src="https://gitee.com/da-qiang-classmate/typora/raw/master/image/20260620233959351.webp" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;用个人微信扫一下，然后在手机上确认登录就行。&lt;/p&gt;

&lt;p&gt;这里注意一点：二维码有效期比较短。如果提示「二维码已过期」，让 Codex 重新生成一张再扫，不用纠结。&lt;/p&gt;
&lt;h3 id="成功后怎么测试"&gt;成功后怎么测试&lt;/h3&gt;
&lt;p&gt;扫码成功后，本机会生成微信账号凭证，通常在：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;C:\Users\你的用户名\.codexbridge\weixin\accounts\
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Windows 上还可以注册成计划任务，任务名一般是：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CodexBridge-Weixin
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样以后登录 Windows，它会自动启动。&lt;/p&gt;

&lt;p&gt;接入成功后，打开微信，给桥接会话发：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/h
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或者：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/status
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果能收到回复，说明链路已经通了。&lt;/p&gt;

&lt;p&gt;后面就可以直接发自然语言任务，比如：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;帮我看一下 D:\project2026\fuwari 这个项目最近有哪些改动，整理成一段提交说明。
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="常用命令"&gt;常用命令&lt;/h3&gt;&lt;table class="table table-bordered table-striped"&gt;
&lt;tr&gt;
&lt;th&gt;命令&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/h&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;查看帮助&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/status&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;查看当前桥接状态&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/new 路径&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;切换到新的项目目录&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/threads&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;查看历史线程&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/open 2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;打开某个历史线程&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/stop&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;停止当前正在跑的任务&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/retry&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;重试上一条请求&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;p&gt;日常远程使用，先记住这几个就够了。&lt;/p&gt;
&lt;h3 id="几个注意点"&gt;几个注意点&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;一、Windows 脚本路径可能要改&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;项目自带的 &lt;code&gt;.cmd&lt;/code&gt; 里可能写着作者自己的电脑路径，需要改成你本机的 Codex 路径和工作目录。&lt;/p&gt;

&lt;p&gt;比如：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;D:\software\Codex\app\resources\codex.exe
D:\zed-workspace
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果你是让 Codex 自动接入，它一般会自己检查并替你改掉。&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;三、电脑要保持在线&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;这个桥接不是云服务。CodexBridge 跑在你本机上，所以电脑关机、睡眠、断网，微信就收不到回复。&lt;/p&gt;

&lt;p&gt;如果打算长期使用，建议关闭自动睡眠，并用 Windows 计划任务保持服务常驻。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;四、注意权限边界&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;微信消息最后会变成 Codex 在你电脑上的任务，所以不要随便开放给别人用。建议只处理私聊，群聊默认关闭，或者只允许指定用户。&lt;/p&gt;
&lt;h3 id="适合什么场景"&gt;适合什么场景&lt;/h3&gt;
&lt;p&gt;它不是为了替代 Codex APP，而是给 Codex 多开了一个入口。&lt;/p&gt;

&lt;p&gt;电脑端适合认真操作，微信端适合随手派活。&lt;/p&gt;

&lt;p&gt;比如你在外面突然想到一个需求、一段文案、一个代码修改点，就可以直接发给微信里的 Codex。只要电脑在线，它就能在背后继续干活。&lt;/p&gt;

&lt;p&gt;&lt;img src="https://gitee.com/da-qiang-classmate/typora/raw/master/image/remote-work-wechat-codex.webp" title="" alt=""&gt;&lt;/p&gt;
&lt;h3 id="总结"&gt;总结&lt;/h3&gt;
&lt;p&gt;这次接入之后，我最大的感受是：&lt;/p&gt;

&lt;p&gt;AI Agent 真正变好用，不只是模型变强，而是入口变近。&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;em&gt;&amp;gt;/ 更多可落地干货教程
欢迎访问我的博客：&lt;a href="https://www.dqtx.cc/" rel="nofollow" target="_blank" title=""&gt;dqtx.cc&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;</description>
      <author>sphinx30</author>
      <pubDate>Sun, 21 Jun 2026 15:28:09 +0800</pubDate>
      <link>https://www.w2solo.com/topics/7567</link>
      <guid>https://www.w2solo.com/topics/7567</guid>
    </item>
    <item>
      <title>2026 年了，这 6 个 npm 包可以卸载了——浏览器原生 API 已经能替代</title>
      <description>&lt;blockquote&gt;
&lt;p&gt;前几天我跑了一下 &lt;code&gt;npx depcheck&lt;/code&gt;，发现项目里有 47 个依赖，其中至少 6 个完全可以用浏览器原生 API 替代。卸载之后，打包体积直接少了 82KB（gzip 后少了 23KB），首屏加载快了 300ms。这篇文章把每个包的原生替代方案都写出来，附迁移代码，直接抄。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="为什么要清理依赖"&gt;为什么要清理依赖&lt;/h2&gt;
&lt;p&gt;每多一个 npm 包，你的项目就多了：&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;code&gt;event-stream&lt;/code&gt; 投毒事件吗？依赖越少，攻击面越小&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;版本冲突&lt;/strong&gt;：包 A 依赖 lodash@4，包 B 依赖 lodash@3，解决冲突的时间比写代码还长&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;2026 年的浏览器已经非常强大了。很多你以为"必须装包"的功能，原生 API 早就支持了。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="1. 卸载 lodash.cloneDeep → 用 structuredClone()"&gt;1. 卸载 &lt;code&gt;lodash.cloneDeep&lt;/code&gt; → 用 &lt;code&gt;structuredClone()&lt;/code&gt;
&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;之前：&lt;/strong&gt;&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;lodash.cloneDeep   &lt;span class="c"&gt;# 5.3KB gzip&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;cloneDeep&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;lodash.cloneDeep&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;copy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;cloneDeep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;complexObject&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;现在：&lt;/strong&gt;&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;copy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;structuredClone&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;complexObject&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;完了。一行，零依赖。&lt;/p&gt;

&lt;p&gt;&lt;code&gt;structuredClone&lt;/code&gt; 是浏览器原生的深拷贝方法，支持 &lt;code&gt;Map&lt;/code&gt;、&lt;code&gt;Set&lt;/code&gt;、&lt;code&gt;Date&lt;/code&gt;、&lt;code&gt;RegExp&lt;/code&gt;、&lt;code&gt;ArrayBuffer&lt;/code&gt;、循环引用——这些 &lt;code&gt;JSON.parse(JSON.stringify())&lt;/code&gt; 做不到的，它全能做。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;兼容性：&lt;/strong&gt; Chrome 98+、Firefox 94+、Safari 15.4+，2026 年你不需要担心兼容性。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;唯一限制：&lt;/strong&gt; 不支持拷贝 DOM 节点和函数。如果你的对象里有函数属性，这个方案不适用。但说实话，你的数据对象里不应该有函数。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;能省多少：&lt;/strong&gt; lodash.cloneDeep 单独引入约 5.3KB gzip，卸载后直接省掉。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="2. 卸载 uuid → 用 crypto.randomUUID()"&gt;2. 卸载 &lt;code&gt;uuid&lt;/code&gt; → 用 &lt;code&gt;crypto.randomUUID()&lt;/code&gt;
&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;之前：&lt;/strong&gt;&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;uuid   &lt;span class="c"&gt;# 2.7KB gzip&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;v4&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;uuidv4&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;uuid&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;uuidv4&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// 'f47ac10b-58cc-4372-a567-0e02b2c3d479'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;现在：&lt;/strong&gt;&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;randomUUID&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// 'f47ac10b-58cc-4372-a567-0e02b2c3d479'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出格式完全一样，都是标准的 UUID v4。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;兼容性：&lt;/strong&gt; Chrome 92+、Firefox 95+、Safari 15.4+，全线支持。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Node.js 也支持：&lt;/strong&gt; Node 19+ 内置 &lt;code&gt;crypto.randomUUID()&lt;/code&gt;，前后端通吃。&lt;/p&gt;

&lt;p&gt;如果你只需要一个唯一 ID 而不需要严格的 UUID 格式，还有更轻量的方案：&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;simpleId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;random&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;36&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// '5x3g7k9m2'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2 id="3. 卸载 dayjs / moment → 用 Intl.DateTimeFormat + Temporal"&gt;3. 卸载 &lt;code&gt;dayjs&lt;/code&gt; / &lt;code&gt;moment&lt;/code&gt; → 用 &lt;code&gt;Intl.DateTimeFormat&lt;/code&gt; + &lt;code&gt;Temporal&lt;/code&gt;
&lt;/h2&gt;
&lt;p&gt;这个是最重磅的。&lt;code&gt;moment.js&lt;/code&gt; 光 gzip 就 72KB，&lt;code&gt;dayjs&lt;/code&gt; 虽然轻（2KB），但大多数场景你连 2KB 都不需要。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;场景一：格式化日期显示&lt;/strong&gt;&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// 之前：dayjs&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;dayjs&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dayjs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;dayjs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;YYYY年MM月DD日&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// 现在：原生 Intl&lt;/span&gt;
&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;Intl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DateTimeFormat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;zh-CN&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;year&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;numeric&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;month&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2-digit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;day&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2-digit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nx"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// '2026/06/21'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;场景二：相对时间（"3 小时前"）&lt;/strong&gt;&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// 之前：dayjs + relativeTime 插件&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;dayjs&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dayjs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;relativeTime&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dayjs/plugin/relativeTime&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;dayjs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;extend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;relativeTime&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;dayjs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;fromNow&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// 现在：原生 Intl.RelativeTimeFormat&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rtf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;Intl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;RelativeTimeFormat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;zh-CN&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;numeric&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;auto&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nx"&gt;rtf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hour&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// '3小时前'&lt;/span&gt;
&lt;span class="nx"&gt;rtf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;day&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// '昨天'&lt;/span&gt;
&lt;span class="nx"&gt;rtf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;month&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// '后2个月'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;场景三：日期计算&lt;/strong&gt;&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// 之前&lt;/span&gt;
&lt;span class="nx"&gt;dayjs&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;day&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;toDate&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// 现在：Temporal API（2026 年主流浏览器已支持）&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Temporal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Now&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;plainDateISO&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nextWeek&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;add&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;days&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;nextWeek&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt; &lt;span class="c1"&gt;// '2026-06-28'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;什么时候还需要 dayjs：&lt;/strong&gt; 如果你要做大量复杂的时区转换、日历系统切换（农历之类），dayjs 的插件生态还是有价值的。但如果只是格式化显示和简单计算，原生足够了。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="4. 卸载 classnames / clsx → 用模板字符串"&gt;4. 卸载 &lt;code&gt;classnames&lt;/code&gt; / &lt;code&gt;clsx&lt;/code&gt; → 用模板字符串&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;之前：&lt;/strong&gt;&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;classnames   &lt;span class="c"&gt;# 0.6KB gzip&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;cn&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;classnames&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt; &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;cn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;btn&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;btn-primary&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;isPrimary&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;btn-disabled&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;isDisabled&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;btn-large&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;size&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;large&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="p"&gt;})}&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;现在：&lt;/strong&gt;&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt; &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{[&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;btn&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;isPrimary&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;btn-primary&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;isDisabled&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;btn-disabled&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;size&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;large&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;btn-large&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Boolean&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果你觉得 &lt;code&gt;filter(Boolean).join(' ')&lt;/code&gt; 写起来啰嗦，封装一个两行的工具函数：&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Boolean&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// 用法完全一样&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt; &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;cn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;btn&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;isPrimary&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;btn-primary&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;isDisabled&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;btn-disabled&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)}&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;2 行代码替代一个 npm 包。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;更好的方案：&lt;/strong&gt; 如果项目用了 Tailwind CSS，&lt;code&gt;tailwind-merge&lt;/code&gt; 比 classnames 更合适，因为它能处理 Tailwind 的类名冲突（比如同时写了 &lt;code&gt;p-2&lt;/code&gt; 和 &lt;code&gt;p-4&lt;/code&gt;）。这种场景下原生方案做不到。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="5. 卸载 node-fetch → 用原生 fetch"&gt;5. 卸载 &lt;code&gt;node-fetch&lt;/code&gt; → 用原生 &lt;code&gt;fetch&lt;/code&gt;
&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;之前（Node.js 环境）：&lt;/strong&gt;&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;node-fetch   &lt;span class="c"&gt;# 8.4KB gzip&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;fetch&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node-fetch&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://api.example.com/data&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;现在：&lt;/strong&gt;&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://api.example.com/data&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Node.js 18+ 内置了 &lt;code&gt;fetch&lt;/code&gt;，不需要再装 &lt;code&gt;node-fetch&lt;/code&gt;。2026 年还在装这个包，大概率是因为 &lt;code&gt;package.json&lt;/code&gt; 里一直没清理。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;注意：&lt;/strong&gt; 如果你的 Node.js 版本低于 18，还是需要 &lt;code&gt;node-fetch&lt;/code&gt;。但 2026 年了，Node 18 已经是 EOL，你至少应该在 Node 20+ 上。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="6. 卸载 qs → 用 URLSearchParams"&gt;6. 卸载 &lt;code&gt;qs&lt;/code&gt; → 用 &lt;code&gt;URLSearchParams&lt;/code&gt;
&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;之前：&lt;/strong&gt;&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;qs   &lt;span class="c"&gt;# 6.2KB gzip&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;qs&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;qs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// 序列化&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;qs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;page&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;keyword&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;前端&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="c1"&gt;// 'page=1&amp;amp;size=20&amp;amp;keyword=%E5%89%8D%E7%AB%AF'&lt;/span&gt;

&lt;span class="c1"&gt;// 解析&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;qs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;page=1&amp;amp;size=20&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// { page: '1', size: '20' }&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;现在：&lt;/strong&gt;&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// 序列化&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;URLSearchParams&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;page&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;keyword&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;前端&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nx"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="c1"&gt;// 'page=1&amp;amp;size=20&amp;amp;keyword=%E5%89%8D%E7%AB%AF'&lt;/span&gt;

&lt;span class="c1"&gt;// 解析&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fromEntries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;URLSearchParams&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;page=1&amp;amp;size=20&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="c1"&gt;// { page: '1', size: '20' }&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;唯一限制：&lt;/strong&gt; &lt;code&gt;URLSearchParams&lt;/code&gt; 不支持嵌套对象和数组的序列化。如果你的查询参数是 &lt;code&gt;{ filter: { status: ['active', 'pending'] } }&lt;/code&gt; 这种结构，还是需要 &lt;code&gt;qs&lt;/code&gt;。但大多数前端场景的查询参数都是扁平的 key-value。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="实操：怎么找出项目里可以卸载的包"&gt;实操：怎么找出项目里可以卸载的包&lt;/h2&gt;&lt;h3 id="第一步：找出未使用的依赖"&gt;第一步：找出未使用的依赖&lt;/h3&gt;&lt;pre class="highlight shell"&gt;&lt;code&gt;npx depcheck
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;它会列出 &lt;code&gt;package.json&lt;/code&gt; 里声明了但代码里从未 import 的包，直接删。&lt;/p&gt;
&lt;h3 id="第二步：分析打包体积"&gt;第二步：分析打包体积&lt;/h3&gt;&lt;pre class="highlight shell"&gt;&lt;code&gt;npx vite-bundle-visualizer
&lt;span class="c"&gt;# 或 webpack 项目&lt;/span&gt;
npx webpack-bundle-analyzer
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;看看哪些包占了大头。通常 &lt;code&gt;moment&lt;/code&gt;、&lt;code&gt;lodash&lt;/code&gt; 完整包是体积杀手。&lt;/p&gt;
&lt;h3 id="第三步：逐个替换"&gt;第三步：逐个替换&lt;/h3&gt;
&lt;p&gt;按本文方案替换后，跑一遍测试，确认功能正常再发版。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="总结对照表"&gt;总结对照表&lt;/h2&gt;&lt;table class="table table-bordered table-striped"&gt;
&lt;tr&gt;
&lt;th&gt;npm 包&lt;/th&gt;
&lt;th&gt;gzip 体积&lt;/th&gt;
&lt;th&gt;原生替代&lt;/th&gt;
&lt;th&gt;限制&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;lodash.cloneDeep&lt;/td&gt;
&lt;td&gt;5.3KB&lt;/td&gt;
&lt;td&gt;&lt;code&gt;structuredClone()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;不支持函数和 DOM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;uuid&lt;/td&gt;
&lt;td&gt;2.7KB&lt;/td&gt;
&lt;td&gt;&lt;code&gt;crypto.randomUUID()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;无&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;dayjs&lt;/td&gt;
&lt;td&gt;2KB&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Intl&lt;/code&gt; + &lt;code&gt;Temporal&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;复杂时区场景不够用&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;classnames&lt;/td&gt;
&lt;td&gt;0.6KB&lt;/td&gt;
&lt;td&gt;&lt;code&gt;filter(Boolean).join(' ')&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;无&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;node-fetch&lt;/td&gt;
&lt;td&gt;8.4KB&lt;/td&gt;
&lt;td&gt;原生 &lt;code&gt;fetch&lt;/code&gt; (Node 18+)&lt;/td&gt;
&lt;td&gt;需要 Node 18+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;qs&lt;/td&gt;
&lt;td&gt;6.2KB&lt;/td&gt;
&lt;td&gt;&lt;code&gt;URLSearchParams&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;不支持嵌套对象&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;~25KB&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0KB&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;p&gt;25KB gzip 看起来不多，但在移动端弱网环境下，这就是 &lt;strong&gt;200-500ms 的加载时间差距&lt;/strong&gt;。&lt;/p&gt;

&lt;p&gt;更重要的是：&lt;strong&gt;少一个依赖，就少一个供应链攻击的入口，少一个版本冲突的可能，少一个 &lt;code&gt;npm audit&lt;/code&gt; 的告警。&lt;/strong&gt;&lt;/p&gt;

&lt;hr&gt;

&lt;p&gt;&lt;em&gt;如果你的项目里还有其他可以用原生 API 替代的包，评论区说一下，我补充进来。点赞收藏一下，下次 Code Review 看到同事装多余的包，直接把这篇甩给他。&lt;/em&gt;&lt;/p&gt;</description>
      <author>193577746</author>
      <pubDate>Sun, 21 Jun 2026 12:32:36 +0800</pubDate>
      <link>https://www.w2solo.com/topics/7566</link>
      <guid>https://www.w2solo.com/topics/7566</guid>
    </item>
    <item>
      <title>出海挣美元分享之：怎么购买域名？</title>
      <description>&lt;p&gt;上一篇文章给大家分享了《出海挣美元分享之：什么是域名？》，其中就给大家介绍了什么是域名、域名与 IP 地址的关系，以及为什么每个网站都需要一个域名。&lt;/p&gt;

&lt;p&gt;这里在简单说一下，域名就是网站的网址，例如：&lt;/p&gt;

&lt;p&gt;google.com&lt;/p&gt;

&lt;p&gt;freeai.run&lt;/p&gt;

&lt;p&gt;timestampcameras.com&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;1、一个邮箱&lt;/p&gt;

&lt;p&gt;用于注册账号、接收域名到期提醒以及找回密码。&lt;/p&gt;

&lt;p&gt;推荐使用：&lt;/p&gt;

&lt;p&gt;国外常用的就是：Gmail&lt;/p&gt;

&lt;p&gt;国内大家常用的就是：QQ 邮箱、网易邮箱&lt;/p&gt;

&lt;p&gt;2、一种支付方式&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;Visa&lt;/p&gt;

&lt;p&gt;MasterCard&lt;/p&gt;

&lt;p&gt;PayPal&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;mywebsite.com&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;.com 域名每年几十元到一百元左右&lt;/p&gt;

&lt;p&gt;.cn 域名通常更便宜一些&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;SaaS 产品&lt;/p&gt;

&lt;p&gt;Google SEO&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;DNS 功能更完善&lt;/p&gt;

&lt;p&gt;与海外服务兼容更好&lt;/p&gt;

&lt;p&gt;后续迁移更加方便&lt;/p&gt;

&lt;p&gt;推荐平台一：Cloudflare Registrar&lt;/p&gt;

&lt;p&gt;Cloudflare Registrar&lt;/p&gt;

&lt;p&gt;优点：&lt;/p&gt;

&lt;p&gt;基本按成本价出售域名&lt;/p&gt;

&lt;p&gt;没有各种隐藏加价&lt;/p&gt;

&lt;p&gt;DNS 服务非常优秀&lt;/p&gt;

&lt;p&gt;我目前大部分域名都放在 Cloudflare。&lt;/p&gt;

&lt;p&gt;推荐平台二：Namecheap&lt;/p&gt;

&lt;p&gt;Namecheap&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;推荐平台三：Porkbun&lt;/p&gt;

&lt;p&gt;Porkbun&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;.com .net .cn .xyz .top .ai&lt;/p&gt;

&lt;p&gt;到底选哪个？&lt;/p&gt;

&lt;p&gt;我的建议很简单：&lt;/p&gt;

&lt;p&gt;第一选择：.com&lt;/p&gt;

&lt;p&gt;如果能买到 .com，优先购买 .com。&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;第二选择：.ai&lt;/p&gt;

&lt;p&gt;如果是 AI 产品项目，可以考虑：&lt;/p&gt;

&lt;p&gt;yourproduct.ai&lt;/p&gt;

&lt;p&gt;近几年很多 AI 创业公司都在使用。&lt;/p&gt;

&lt;p&gt;不过价格通常比 .com 贵不少。&lt;/p&gt;

&lt;p&gt;第三选择：其他后缀&lt;/p&gt;

&lt;p&gt;例如：&lt;/p&gt;

&lt;p&gt;.net .io .xyz .run&lt;/p&gt;

&lt;p&gt;如果预算有限或者 .com 已经被注册，也完全可以使用。&lt;/p&gt;

&lt;p&gt;很多成功的网站最开始也并不是使用 .com。&lt;/p&gt;

&lt;p&gt;五、购买域名时的几个避坑建议&lt;/p&gt;

&lt;p&gt;1、优先选择短域名&lt;/p&gt;

&lt;p&gt;例如：&lt;/p&gt;

&lt;p&gt;freeai.run&lt;/p&gt;

&lt;p&gt;明显比：&lt;/p&gt;

&lt;p&gt;bestfreeaitoolswebsite.com&lt;/p&gt;

&lt;p&gt;更容易传播。&lt;/p&gt;

&lt;p&gt;2、避免数字和横线&lt;/p&gt;

&lt;p&gt;不推荐：&lt;/p&gt;

&lt;p&gt;my-ai-tool-123.com&lt;/p&gt;

&lt;p&gt;推荐：&lt;/p&gt;

&lt;p&gt;myaitool.com&lt;/p&gt;

&lt;p&gt;3、不要一次购买太多年&lt;/p&gt;

&lt;p&gt;很多新手刚开始创业就直接购买 5 年、10 年。&lt;/p&gt;

&lt;p&gt;实际上完全没有必要。&lt;/p&gt;

&lt;p&gt;建议：&lt;/p&gt;

&lt;p&gt;先购买 1 年。&lt;/p&gt;

&lt;p&gt;等项目跑起来之后再续费。&lt;/p&gt;

&lt;p&gt;4、看到喜欢的域名尽快注册&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;Cloudflare Registrar&lt;/p&gt;

&lt;p&gt;一个几十元的域名，就足够让你开启自己的第一个网站项目。&lt;/p&gt;

&lt;p&gt;不要等所有东西都准备好再开始。&lt;/p&gt;

&lt;p&gt;很多时候，一个域名就是一个项目的开始。&lt;/p&gt;</description>
      <author>my126sw</author>
      <pubDate>Sat, 20 Jun 2026 23:29:52 +0800</pubDate>
      <link>https://www.w2solo.com/topics/7565</link>
      <guid>https://www.w2solo.com/topics/7565</guid>
    </item>
    <item>
      <title>我做了个局部重绘 AI 工具：涂抹一下，想改哪改哪</title>
      <description>&lt;p&gt;把美工省掉。&lt;/p&gt;

&lt;p&gt;3 毛钱，修一张图。&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;于是我就把这个工具做出来了。底层用的是目前顶级的 GPT-4 的 image2 模型，真实感与一致性都有保障。&lt;/p&gt;

&lt;p&gt;图片&lt;/p&gt;

&lt;p&gt;工具超简单，整个页面就是一张画布，上传、生成、修改都在这上面进行，72 小时自动删除图片，保护版权、保护隐私。&lt;/p&gt;

&lt;p&gt;核心亮点：局部重绘&lt;/p&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;p&gt;图片
然后第 2 个就是局部重绘工具，可以用橡皮擦也可以用矩型把指定位置圈起来：&lt;/p&gt;

&lt;p&gt;图片
然后点右边的勾勾，就会出现提示示框：&lt;/p&gt;

&lt;p&gt;图片
告诉它：&lt;/p&gt;

&lt;p&gt;换成哆啦 A 梦
就这么简单。&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;高保真迁移：当你想把图中物体 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;图片
这是一张手绘的卡通猫，想变成娃娃自然不能再用高保真，只能让它重绘，重绘后的图如下：&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;• 图片生成（文字生成图片）：一句话出图，适合快速出创意或找灵感。
 • 背景替换/去除：商品拍在杂乱场景也没事，一键换成白底或品牌背景，电商卖家会很方便。
 • 一键去水印：去掉角落的 logo 或水印，快速复用素材。
 • 智能洗稿（改写图片风格）：把一张图改成不同的风格，作为创作起点也不赖。
 • 修改尺寸：按平台规格快速裁剪和调整比例。
 • 滤镜美白：一些基础的调色和优化，省得再打开其他软件。
 • 裁剪翻转：常见编辑操作一应俱全。&lt;/p&gt;

&lt;p&gt;…………等等，自己去闹吧。&lt;/p&gt;

&lt;p&gt;图片
像美白的、去水印的，这些就不一一描述了，自己探索更多的可能性吧。&lt;/p&gt;

&lt;p&gt;小红书玩家还可以进行 X 稿：&lt;/p&gt;

&lt;p&gt;图片
不过我们小红书 X 稿也有专门的工具，这个只是配套而已。&lt;/p&gt;

&lt;p&gt;图片
让女生抱着哆啦 A 梦。&lt;/p&gt;

&lt;p&gt;图片
换背景。&lt;/p&gt;

&lt;p&gt;总的来说，画布操作也挺顺手：鼠标滚轮缩放、自由拖拽，细节看清楚后再涂抹，定位更准。&lt;/p&gt;

&lt;p&gt;使用方式：用完即走，隐私无忧&lt;/p&gt;

&lt;p&gt;使用方式上，主打一个"拖入即用"。图片直接拖进页面或点上传就开工，所有处理在云端完成，不用安装软件。我做了个"用完即走"的设计理念——图片仅保留 72 小时，之后自动清理，保护隐私。&lt;/p&gt;

&lt;p&gt;适用场景我简单列几个：&lt;/p&gt;

&lt;p&gt;• 电商卖家：快速修图、换背景、去水印，提升出图效率。
 • 设计师：找灵感、快速尝试不同方案、局部调整。
 • 普通用户：趣味改图、日常美化，门槛很低。&lt;/p&gt;

&lt;p&gt;修图价格约 0.3 元，其它的都是约 0.12 元，新用户可试用&lt;/p&gt;

&lt;p&gt;如果你也有这种痛点——局部要改、风格要一致、又不想整图重来，试试看。&lt;/p&gt;

&lt;p&gt;👉 复制以下网址体验：
&lt;a href="https://www.vicoco.cn/p/239" rel="nofollow" target="_blank"&gt;https://www.vicoco.cn/p/239&lt;/a&gt;&lt;/p&gt;</description>
      <author>zhengqiagood</author>
      <pubDate>Sat, 20 Jun 2026 21:06:02 +0800</pubDate>
      <link>https://www.w2solo.com/topics/7564</link>
      <guid>https://www.w2solo.com/topics/7564</guid>
    </item>
    <item>
      <title>同事每天催我 Code Review，我写了个脚本让 AI 替我 review PR——现在他反过来催 AI 了</title>
      <description>&lt;blockquote&gt;
&lt;p&gt;先说结论：用 GitHub Actions + Claude API 实现全自动 PR review，配置一次，以后每个 PR 推上去，AI 10 秒内给出详细评审意见，覆盖代码质量、潜在 bug、安全风险三个维度。这篇文章把完整配置都给你，拿走直接用。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="背景：CR 慢是前端团队的通病"&gt;背景：CR 慢是前端团队的通病&lt;/h2&gt;
&lt;p&gt;我们团队有个不成文的规定：PR 提上去，要在 1 个工作日内完成 review。&lt;/p&gt;

&lt;p&gt;听起来很合理，但实际情况是这样的：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;早上刚到公司，打开 GitHub，待 review 的 PR 已经排了 6 个&lt;/li&gt;
&lt;li&gt;每个 PR 少则 200 行，多则 800 行&lt;/li&gt;
&lt;li&gt;还有自己的需求要写&lt;/li&gt;
&lt;li&gt;还有各种会&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;于是每天我的 review 质量是这样的：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;上午的 PR：认真看，写评论&lt;/li&gt;
&lt;li&gt;下午的 PR：大概扫一下&lt;/li&gt;
&lt;li&gt;晚上的 PR：approve，👍&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;然后有一天，同事 A 在群里 @ 了我：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"我那个 PR 提了两天了，能帮我看一下吗？"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;我翻出来一看——800 行，涉及三个模块。&lt;/p&gt;

&lt;p&gt;我当时脑子里只有一个念头：&lt;strong&gt;要是有人能替我先过一遍就好了。&lt;/strong&gt;&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="解决思路：AI 做初审，人做终审"&gt;解决思路：AI 做初审，人做终审&lt;/h2&gt;
&lt;p&gt;理想的工作流是这样的：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;开发者 push PR
    ↓
AI 自动分析 diff
    ↓
AI 在 PR 里发评论（10秒内）
    ↓
人工看 AI 的意见 + 做最终判断
    ↓
approve 或 request changes
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;好处显而易见：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AI 处理掉 80% 的机械检查（代码风格、潜在空指针、未处理的 Promise reject）&lt;/li&gt;
&lt;li&gt;人只需要关注 AI 标注出的问题点 + 业务逻辑&lt;/li&gt;
&lt;li&gt;平均 review 时间从 30 分钟压缩到 8 分钟&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;坏处也要说清楚：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AI 不理解你的业务逻辑，会漏掉业务层面的 bug&lt;/li&gt;
&lt;li&gt;AI 有时候会给"误报"，需要人来过滤&lt;/li&gt;
&lt;li&gt;不能完全替代人工 review&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;所以定位是：AI 初审 + 人工终审，不是 AI 替代人工。&lt;/strong&gt;&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="实现方案：GitHub Actions + Claude API"&gt;实现方案：GitHub Actions + Claude API&lt;/h2&gt;&lt;h3 id="第一步：申请 Claude API Key"&gt;第一步：申请 Claude API Key&lt;/h3&gt;
&lt;p&gt;去 &lt;a href="https://console.anthropic.com" rel="nofollow" target="_blank" title=""&gt;console.anthropic.com&lt;/a&gt; 注册，免费额度足够测试用。&lt;/p&gt;

&lt;p&gt;把 Key 存到 GitHub 仓库的 Secrets：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;仓库 → Settings → Secrets and variables → Actions&lt;/li&gt;
&lt;li&gt;新建 &lt;code&gt;ANTHROPIC_API_KEY&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="第二步：创建 workflow 文件"&gt;第二步：创建 workflow 文件&lt;/h3&gt;
&lt;p&gt;在你的仓库里创建 &lt;code&gt;.github/workflows/ai-review.yml&lt;/code&gt;：&lt;/p&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AI Code Review&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;types&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;opened&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;synchronize&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;
  &lt;span class="na"&gt;pull-requests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;ai-review&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Checkout&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;fetch-depth&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Setup Node.js&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-node@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;20'&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install dependencies&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm install @anthropic-ai/sdk @octokit/rest&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run AI Review&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;ANTHROPIC_API_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.ANTHROPIC_API_KEY }}&lt;/span&gt;
          &lt;span class="na"&gt;GITHUB_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.GITHUB_TOKEN }}&lt;/span&gt;
          &lt;span class="na"&gt;PR_NUMBER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.event.pull_request.number }}&lt;/span&gt;
          &lt;span class="na"&gt;REPO_OWNER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.repository_owner }}&lt;/span&gt;
          &lt;span class="na"&gt;REPO_NAME&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.event.repository.name }}&lt;/span&gt;
          &lt;span class="na"&gt;BASE_SHA&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.event.pull_request.base.sha }}&lt;/span&gt;
          &lt;span class="na"&gt;HEAD_SHA&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.event.pull_request.head.sha }}&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;node .github/scripts/ai-review.js&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="第三步：创建核心脚本"&gt;第三步：创建核心脚本&lt;/h3&gt;
&lt;p&gt;创建 &lt;code&gt;.github/scripts/ai-review.js&lt;/code&gt;：&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Anthropic&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@anthropic-ai/sdk&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Octokit&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@octokit/rest&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;execSync&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;child_process&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;anthropic&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;Anthropic&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ANTHROPIC_API_KEY&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;octokit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;Octokit&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;GITHUB_TOKEN&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;getDiff&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;diff&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;execSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;`git diff &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;BASE_SHA&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;..&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;HEAD_SHA&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; -- '*.js' '*.ts' '*.tsx' '*.jsx' '*.vue'`&lt;/span&gt;
  &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="c1"&gt;// 超过 8000 字符截断，避免超出 token 限制&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;diff&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;8000&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;diff&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s1"&gt;... [diff truncated]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;diff&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;reviewWithClaude&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;diff&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;anthropic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;claude-sonnet-4-6&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;max_tokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2048&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;
      &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`你是一个资深前端代码审查工程师，请对以下 PR diff 进行代码审查。

**审查重点：**
1. 潜在 Bug（空值引用、异步竞态、边界条件未处理）
2. 安全风险（XSS、敏感信息泄露、输入未校验）
3. 性能问题（不必要的重渲染、大对象序列化、内存泄漏）
4. 可维护性（函数过长、命名不清晰、重复代码）

**输出格式：**
用 Markdown 输出，包含以下几个部分：
- 🔴 严重问题（必须修复）
- 🟡 建议优化（可以改进）
- 🟢 代码亮点（做得好的地方）
- 📊 总体评分（1-10分，附简短理由）

如果没有问题，直接说"代码质量良好，无明显问题"。
用中文回复，简洁直接，不废话。

**PR Diff：**
&lt;/span&gt;&lt;span class="se"&gt;\`\`\`&lt;/span&gt;&lt;span class="s2"&gt;
&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;diff&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;
&lt;/span&gt;&lt;span class="se"&gt;\`\`\`&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
    &lt;span class="p"&gt;}]&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;postComment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;review&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;REPO_OWNER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;REPO_NAME&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;PR_NUMBER&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;octokit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;issues&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;createComment&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;owner&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;REPO_OWNER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;REPO_NAME&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;issue_number&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;parseInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;PR_NUMBER&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`## 🤖 AI Code Review\n\n&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;review&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;\n\n---\n*由 Claude AI 自动生成，仅供参考，请以人工审查为准*`&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Getting diff...&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;diff&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;getDiff&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;diff&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;No JS/TS changes detected, skipping review.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Sending to Claude for review...&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;review&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;reviewWithClaude&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;diff&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Posting comment...&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;postComment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;review&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Done!&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Error:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="第四步：推一个 PR 测试"&gt;第四步：推一个 PR 测试&lt;/h3&gt;
&lt;p&gt;随便改一个文件，提个 PR，大约 15-20 秒后，你会看到 AI 自动在 PR 下面发了一条评论。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="真实效果展示"&gt;真实效果展示&lt;/h2&gt;
&lt;p&gt;我们团队用了两周后，AI 的评论长这样：&lt;/p&gt;

&lt;hr&gt;

&lt;p&gt;&lt;strong&gt;🔴 严重问题（必须修复）&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;第 47 行 &lt;code&gt;userData.profile.avatar&lt;/code&gt;：未做空值检查，当 &lt;code&gt;profile&lt;/code&gt; 为 &lt;code&gt;null&lt;/code&gt; 时会抛出 TypeError。建议改为 &lt;code&gt;userData.profile?.avatar&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;第 83 行：&lt;code&gt;useEffect&lt;/code&gt; 的依赖数组缺少 &lt;code&gt;userId&lt;/code&gt;，可能导致使用过期的 &lt;code&gt;userId&lt;/code&gt; 发请求，产生数据不一致。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;🟡 建议优化&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;第 92-115 行：&lt;code&gt;formatUserData&lt;/code&gt; 函数做了 3 件不同的事（格式化、过滤、排序），建议拆成 3 个函数，便于测试和复用。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;🟢 代码亮点&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;错误处理写得很规范，catch 块没有直接吞掉错误，而是通过 &lt;code&gt;errorReporter.capture&lt;/code&gt; 上报，赞。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;📊 总体评分：7/10&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;整体结构清晰，主要问题集中在空值防护上，修复后可以直接合并。&lt;/p&gt;

&lt;hr&gt;

&lt;p&gt;看到这个，review 的人只需要：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;确认 AI 找出的问题是否真实存在（通常是）&lt;/li&gt;
&lt;li&gt;判断业务逻辑是否正确（这才是人工的价值所在）&lt;/li&gt;
&lt;li&gt;approve 或 request changes&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;平均节省 70% 的初审时间。&lt;/strong&gt;&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="进阶：按文件类型差异化 review"&gt;进阶：按文件类型差异化 review&lt;/h2&gt;
&lt;p&gt;不是所有文件都需要一样的审查策略。可以在 prompt 里加文件类型判断：&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;buildPrompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;diff&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fileTypes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;focusMap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;api&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;重点关注：参数校验、错误处理、幂等性、接口安全&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;component&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;重点关注：props 类型、副作用清理、渲染性能、无障碍属性&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;util&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;重点关注：边界条件、纯函数、测试覆盖&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;store&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;重点关注：状态更新的不可变性、selector 性能、副作用隔离&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;focus&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fileTypes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;focusMap&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="nx"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Boolean&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`你是资深前端工程师，请审查以下代码：

&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;focus&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="s2"&gt;`**本次特别关注：**\n&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;focus&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;\n`&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;

&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;diff&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2 id="踩坑记录"&gt;踩坑记录&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;坑 1：diff 太大超出 token 限制&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;解决方案：只 review 修改行数 &amp;lt; 500 的文件，超过的给一个提示"文件变更过大，请人工重点审查"。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;坑 2：AI 给出"误报"，降低信任度&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;解决方案：在 prompt 里加一句 "如果你不确定是否是问题，不要写出来"。比 "尽可能找出所有问题" 效果好很多。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="现状"&gt;现状&lt;/h2&gt;
&lt;p&gt;现在我们团队的 CR 流程是这样的：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;开发提 PR&lt;/li&gt;
&lt;li&gt;AI 10 秒内完成初审，发评论&lt;/li&gt;
&lt;li&gt;reviewer 看 AI 评论 + diff，平均 8 分钟完成终审&lt;/li&gt;
&lt;li&gt;merge&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;那个之前每天催我 review 的同事 A，现在每次 AI 评论发出来，他比我更认真地研究 AI 说了什么。&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 id="完整代码"&gt;完整代码&lt;/h2&gt;
&lt;p&gt;完整的配置文件我放在下面，可以直接 copy 到你的项目里：&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;.github/workflows/ai-review.yml&lt;/code&gt;&lt;/strong&gt;（上文已贴完整版）&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;.github/scripts/ai-review.js&lt;/code&gt;&lt;/strong&gt;（上文已贴完整版）&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;需要的环境变量：&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;ANTHROPIC_API_KEY&lt;/code&gt;：从 Anthropic 控制台获取&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GITHUB_TOKEN&lt;/code&gt;：GitHub Actions 自带，不用手动配置&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;配置完之后，去提一个测试 PR，看着 AI 自动开始 review，还挺有成就感的。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="写在最后"&gt;写在最后&lt;/h2&gt;
&lt;p&gt;这套方案不是为了让人偷懒的——而是让&lt;strong&gt;人的精力花在刀刃上&lt;/strong&gt;。&lt;/p&gt;

&lt;p&gt;机械的检查（空值、类型、格式）让 AI 做，业务逻辑、架构判断、团队规范这些需要上下文的判断，留给人。&lt;/p&gt;

&lt;p&gt;如果你的团队也有 CR 堆积的问题，可以直接拿这套配置用。有什么问题评论区说，我看到了会回。&lt;/p&gt;

&lt;p&gt;点赞收藏一下，下次出团队提效工具合集时方便你找回来。&lt;/p&gt;</description>
      <author>193577746</author>
      <pubDate>Sat, 20 Jun 2026 12:31:23 +0800</pubDate>
      <link>https://www.w2solo.com/topics/7563</link>
      <guid>https://www.w2solo.com/topics/7563</guid>
    </item>
    <item>
      <title>为了上 telegram 对接海外客户，我被一个登录界面卡了三天，还好最后解决了</title>
      <description>&lt;p&gt;&lt;img src="https://img.way2solo.com/photo/freemanbrent40/079d019e-080c-4096-9fc6-06a11c99a1aa.jpg?imageView2/2/w/1920/q/100" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;上个月接了一个海外客户的单子，对方要求用某通讯工具沟通。我注册的时候，+86 手机号，验证码等了三天都没来，中间还交了两次 smsfee，钱花了，码还是没收到。&lt;/p&gt;

&lt;p&gt;为什么必须上这个工具？&lt;/p&gt;

&lt;p&gt;不是我想用，是客户在用。独立开发者接海外单，沟通工具绕不开。客户说"我们团队在这个上面"，你就得上去。&lt;/p&gt;

&lt;p&gt;一个做跨境的朋友说，他之前也遇到过，最后换了个客户端解决。基于官方开源版本编译，核心协议兼容，但登录通道不同。&lt;/p&gt;

&lt;p&gt;试了一下，直接登录成功，没有验证码，没有 smsfee，两分钟搞定。&lt;/p&gt;

&lt;p&gt;用了一周的实际体验&lt;/p&gt;

&lt;p&gt;登录方式绕过短信验证，直接设备授权。中文环境完整，连接稳定性不错，电信、联通、移动都试过，后台保活一周，消息推送没漏过。跟客户时差 12 小时，半夜来的消息，早上醒来能看到。&lt;/p&gt;

&lt;p&gt;功能上和官方版一样，聊天、频道、搜索、多账号切换，没发现缩水。&lt;/p&gt;

&lt;p&gt;&lt;img src="https://img.way2solo.com/photo/freemanbrent40/b5f19579-35a5-4d0e-a005-99d5c953486e.png?imageView2/2/w/1920/q/100" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;官方路径：smsfee 交了几十块 + 三天时间 + 可能丢掉的客户&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;img src="https://img.way2solo.com/photo/freemanbrent40/2b7cd94d-adff-4ddc-9e4b-724d209f2577.jpg?imageView2/2/w/1920/q/100" title="" alt=""&gt;&lt;/p&gt;</description>
      <author>freemanbrent40</author>
      <pubDate>Sat, 20 Jun 2026 11:43:03 +0800</pubDate>
      <link>https://www.w2solo.com/topics/7562</link>
      <guid>https://www.w2solo.com/topics/7562</guid>
    </item>
    <item>
      <title>SerpBase 运营两个月后，终于把 Google 搜索接进 Agent 这件事做顺了</title>
      <description>&lt;p&gt;两个月前，我在 W2solo 发过两篇文章：&lt;/p&gt;

&lt;p&gt;正式运营 6 天，拿到第一笔 $200：SerpBase 从立项到上线的一个月 (&lt;a href="https://w2solo.com/topics/7275" rel="nofollow" target="_blank"&gt;https://w2solo.com/topics/7275&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;做了个新站 SerpBase：想把 Google 搜索结果 API 做得便宜一点、稳一点 (&lt;a href="https://w2solo.com/topics/7254" rel="nofollow" target="_blank"&gt;https://w2solo.com/topics/7254&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;当时项目正式运营第 6 天，拿到了第一笔 $200。两个月过去，产品还在继续迭代，最近花了不少时间解决一件很实际的事：让 AI Agent 更方便地用 Google 搜索。&lt;/p&gt;

&lt;p&gt;很多 Agent 场景不需要自己搭浏览器自动化。折腾 Playwright、代理、验证码和页面结构，最后想要的往往只是标题、链接、摘要这些干净结果。&lt;/p&gt;

&lt;p&gt;所以做了两个开源集成：&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/serpbase-dev/serpbase-skill" rel="nofollow" target="_blank"&gt;https://github.com/serpbase-dev/serpbase-skill&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;给支持 Skill 的 AI 编程助手和 Agent 用，装好后可以直接搜索并拿结构化结果。&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/serpbase-dev/serpbase-mcp" rel="nofollow" target="_blank"&gt;https://github.com/serpbase-dev/serpbase-mcp&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;给支持 MCP 的客户端用，把 Google 搜索作为 Agent 的一个工具接进去。&lt;/p&gt;

&lt;p&gt;我现在越来越觉得，Agent 的搜索能力不该是一个大工程。能稳定拿到实时信息，返回格式干净，价格别把小项目劝退，基本就够了。&lt;/p&gt;

&lt;p&gt;也想问问大家：你们给 Agent 接实时搜索时，现在主要用什么方案？自己爬、SerpAPI/Serper，还是别的？&lt;/p&gt;

&lt;p&gt;我们的官网是 (&lt;a href="https://serpbase.dev" rel="nofollow" target="_blank"&gt;https://serpbase.dev&lt;/a&gt;)，欢迎各位大佬前来指导&lt;/p&gt;</description>
      <author>serpbase</author>
      <pubDate>Sat, 20 Jun 2026 11:40:22 +0800</pubDate>
      <link>https://www.w2solo.com/topics/7561</link>
      <guid>https://www.w2solo.com/topics/7561</guid>
    </item>
    <item>
      <title>分享最近 AI 做的儿童启蒙产品：拍照学拼音（附兑换码）</title>
      <description>&lt;h2 id="📸 拍照学拼音｜从孩子身边开始的启蒙学习工具"&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;p&gt;看到小猫会问是什么，看到汽车会问怎么读，看到水果、玩具、绘本都会不停追问。&lt;/p&gt;

&lt;p&gt;作为一名产品开发者，我就在想：&lt;/p&gt;

&lt;p&gt;👉 能不能做一个让孩子从身边事物开始学习拼音和汉字的工具？&lt;/p&gt;

&lt;p&gt;于是利用 AI 开发了这款小程序——&lt;strong&gt;拍照学拼音&lt;/strong&gt;。&lt;/p&gt;
&lt;h2 id=""&gt;&lt;img src="https://pinyin.qibanai.cn/uploads/cards/vi/fengmian.webp" width="600px" height="800px" alt="封面"&gt;&lt;/h2&gt;&lt;h2 id="🏠 首页展示"&gt;🏠 首页展示&lt;/h2&gt;
&lt;p&gt;&lt;img src="https://pinyin.qibanhub.com/" title="" alt="Loading Page"&gt;&lt;/p&gt;

&lt;p&gt;👉 拍照学拼音 Loading Page 页面&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="🌱 为什么做它"&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;把现实世界和拼音、汉字自然关联起来。&lt;/p&gt;

&lt;p&gt;学习不一定非要坐在桌前。&lt;/p&gt;

&lt;p&gt;客厅里的玩具、路边的花草、公园里的小动物，都可以成为孩子的启蒙教材。&lt;/p&gt;

&lt;p&gt;&lt;img src="https://pinyin.qibanai.cn/uploads/cards/vi/01.webp" width="600px" height="600px" alt="封面"&gt;&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="📷 核心功能：拍一拍，就会读"&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;/ul&gt;
&lt;h3 id="示例："&gt;示例：&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;🍎 苹果 → 苹果（píng guǒ）&lt;/li&gt;
&lt;li&gt;🐱 小猫 → 猫（māo）&lt;/li&gt;
&lt;li&gt;🚗 汽车 → 汽车（qì chē）&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;👉 让孩子从熟悉的事物开始认识汉字和拼音&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="🧩 产品截图"&gt;🧩 产品截图&lt;/h2&gt;&lt;h3 id="🏠 首页"&gt;🏠 首页&lt;/h3&gt;
&lt;p&gt;&lt;img src="https://pinyin.qibanai.cn/uploads/cards/vi/home.jpg" width="600px" height="800px" alt="首页截图"&gt;
)&lt;/p&gt;

&lt;hr&gt;
&lt;h3 id="🎮 启蒙小游戏专区"&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;p&gt;&lt;img src="https://pinyin.qibanai.cn/uploads/cards/vi/01.jpg" width="600px" height="800px" alt="小游戏截图"&gt;&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="🚀 当前进展"&gt;🚀 当前进展&lt;/h2&gt;
&lt;p&gt;产品上线一周多，目前已有 &lt;strong&gt;1000+ 用户体验&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;hr&gt;
&lt;h2 id="🌈 未来想做什么"&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;未来方向包括：&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;hr&gt;

&lt;p&gt;&lt;img src="https://pinyin.qibanai.cn/uploads/cards/nav/qrcode.jpg" width="600px" height="600px" alt="封面"&gt;&lt;/p&gt;
&lt;h2 id="🎁 专属兑换码（限量）"&gt;🎁 专属兑换码（限量）&lt;/h2&gt;
&lt;p&gt;使用方式：
进入「个人中心 / 积分兑换」即可使用&lt;br&gt;
（每人每天限用一个）&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DY832QK9
DY4GSEJA
DYNJBDPM
DYLGXABK
DY2TD75Y
DYYEBVKL
DYDGYBFG

👉 用完可以在评论区说一声，方便其他朋友

![封面](https://pinyin.qibanai.cn/uploads/cards/nav/qrcode.jpg =600x800)

---

## 🤝 想听听大家的建议

如果你家里有学龄前儿童，或者做过教育产品，想请教几个问题：

- 这种“拍照学拼音”的方式是否有价值？
- 孩子是否愿意持续使用？
- 还有哪些启蒙功能值得加入？

---

第一次做儿童教育方向的产品，还有很多不足。

但希望可以持续打磨，做出一个真正帮助家长和孩子的工具。

欢迎体验、拍砖、提建议，也欢迎交流 👇

微信：Mianxinai

---

❤️ 感谢大家
&lt;/code&gt;&lt;/pre&gt;</description>
      <author>onling</author>
      <pubDate>Sat, 20 Jun 2026 10:15:51 +0800</pubDate>
      <link>https://www.w2solo.com/topics/7560</link>
      <guid>https://www.w2solo.com/topics/7560</guid>
    </item>
    <item>
      <title>AI powered game market</title>
      <description>&lt;p&gt;IGX.gg is an AI powered game market designed to simplify how players discover, trade, and interact with gaming-related products and services. The platform combines marketplace functionality with AI-driven discovery, helping gamers quickly find in-game items, game accounts, boosting services, virtual currencies, and digital gaming assets across multiple titles.&lt;/p&gt;

&lt;p&gt;As an AI powered game market, IGX.gg focuses on improving the buying and selling experience through smarter recommendations, faster search, and a more streamlined interface. Instead of manually browsing countless listings, users can explore game-related products with more personalized matching based on their interests and gaming needs. This makes the platform especially useful for players looking for trusted game trading opportunities, competitive pricing, or niche in-game resources.&lt;/p&gt;

&lt;p&gt;Whether you are searching for gaming services, virtual goods, or marketplace deals, IGX.gg aims to create a safer and more efficient ecosystem for digital gaming transactions. By combining AI-assisted discovery with a user-friendly marketplace experience, the platform positions itself as a modern destination for gamers who want faster access to game-related products in an increasingly digital economy.&lt;/p&gt;

&lt;p&gt;Create Your Own &lt;a href="https://www.igx.gg" rel="nofollow" target="_blank" title=""&gt;AI powered game market&lt;/a&gt;&lt;/p&gt;</description>
      <author>pokeman</author>
      <pubDate>Fri, 19 Jun 2026 10:35:44 +0800</pubDate>
      <link>https://www.w2solo.com/topics/7559</link>
      <guid>https://www.w2solo.com/topics/7559</guid>
    </item>
    <item>
      <title>我用 AI 一周写完了整个项目，上线第一天就崩了——这是我踩过最贵的 5 个坑</title>
      <description>&lt;blockquote&gt;
&lt;p&gt;这不是标题党。上个月，我用 Claude Code + Cursor 花了 7 天从零写完了一个内部工具平台，提前两周交付，老板在周会上点名表扬。然后上线第一天，报警群炸了 47 条消息。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="先说背景"&gt;先说背景&lt;/h2&gt;
&lt;p&gt;我们团队接了一个内部需求：给运营团队做一个数据看板 + 工单系统。&lt;/p&gt;

&lt;p&gt;需求不复杂，技术栈是 React + Node.js + PostgreSQL，正常排期两个人三周。但当时另一个同事被抽去救火了，变成我一个人，排期不变。&lt;/p&gt;

&lt;p&gt;我想，这不正好？上个月刚充了 Claude Code Max，Cursor 也续了费，这种 CRUD 项目，AI 写代码不是手到擒来？&lt;/p&gt;

&lt;p&gt;于是我做了一个大胆的决定：&lt;strong&gt;全程用 AI 辅助开发，目标一周交付。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;结果是——我确实一周写完了。代码量大概 1.2 万行，前后端 + 数据库 + 部署脚本，全套。&lt;/p&gt;

&lt;p&gt;但上线后第一天，就出了三个线上事故。&lt;/p&gt;

&lt;p&gt;这篇文章，我会把这 7 天的真实经历和踩的 5 个坑完整地写出来。不是为了黑 AI，AI 确实让我效率提升了 3 倍以上。但那些"AI 写代码真香"的文章不会告诉你的暗面，我来说。&lt;/p&gt;
&lt;h2 id="坑 1：AI 生成的代码"&gt;坑 1：AI 生成的代码"看起来对"，但数据边界全是雷&lt;/h2&gt;
&lt;p&gt;第三天，我让 Claude Code 帮我写了一个数据聚合接口，把工单按部门、状态、时间维度做分组统计。&lt;/p&gt;

&lt;p&gt;AI 给的代码很漂亮：&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;getTicketStats&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;startDate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;endDate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;department&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`
    SELECT 
      department,
      status,
      DATE_TRUNC('day', created_at) as date,
      COUNT(*) as count
    FROM tickets
    WHERE created_at BETWEEN $1 AND $2
      AND ($3 IS NULL OR department = $3)
    GROUP BY department, status, DATE_TRUNC('day', created_at)
    ORDER BY date DESC
  `&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;startDate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;endDate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;department&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&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;ol&gt;
&lt;li&gt;
&lt;strong&gt;时区问题&lt;/strong&gt;：&lt;code&gt;DATE_TRUNC&lt;/code&gt; 默认用的是 UTC，但运营看的是北京时间。跨天的工单被归到了前一天。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;空值处理&lt;/strong&gt;：当 &lt;code&gt;department&lt;/code&gt; 传 &lt;code&gt;undefined&lt;/code&gt; 时，&lt;code&gt;$3 IS NULL&lt;/code&gt; 这个条件在 PostgreSQL 中的行为不是"忽略过滤"，而是匹配 &lt;code&gt;department IS NULL&lt;/code&gt; 的记录。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;大分页缺失&lt;/strong&gt;：没有 LIMIT，当时间范围选了一整年，直接返回了 12 万行数据，前端图表库 echarts 卡死了。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;这三个问题，每一个在代码层面都是"AI 写得没错"，但在业务层面全是 bug。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;修复后的代码：&lt;/strong&gt;&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;getTicketStats&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;startDate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;endDate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;department&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;conditions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;created_at &amp;gt;= $1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;created_at &amp;lt; $2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;startDate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;endDate&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;department&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;conditions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`department = $&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;department&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`
    SELECT 
      department,
      status,
      DATE_TRUNC('day', created_at AT TIME ZONE 'Asia/Shanghai') as date,
      COUNT(*) as count
    FROM tickets
    WHERE &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;conditions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; AND &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;
    GROUP BY department, status, date
    ORDER BY date DESC
    LIMIT 10000
  `&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;教训：AI 不懂你的业务时区，不懂你的用户是谁，不懂你的数据量级。这些上下文，它不会主动问你，你不说它就不管。&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id="坑 2：AI 特别擅长"&gt;坑 2：AI 特别擅长"造轮子"，但从不告诉你有现成的&lt;/h2&gt;
&lt;p&gt;第二天，我让 AI 帮我写一个文件上传模块，支持分片上传、断点续传、进度显示。&lt;/p&gt;

&lt;p&gt;AI 非常兴奋地给我写了将近 400 行代码，包括：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;前端的分片逻辑&lt;/li&gt;
&lt;li&gt;MD5 校验&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;blockquote&gt;
&lt;p&gt;"这个……你为什么不用 TUS 协议？&lt;code&gt;tus-js-client&lt;/code&gt; + &lt;code&gt;tus-node-server&lt;/code&gt; 两个包加起来不到 20 行配置就搞定了。"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;我当时脸就绿了。&lt;/p&gt;

&lt;p&gt;AI 默认行为是&lt;strong&gt;从零实现&lt;/strong&gt;，而不是&lt;strong&gt;先搜索有没有成熟方案&lt;/strong&gt;。它不会说"这个需求其实用 XXX 库三行代码就能搞定"，因为它的训练目标就是生成代码，而不是帮你少写代码。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;教训：在让 AI 写代码之前，先问它一个问题——"这个需求有没有成熟的开源方案？列出前 3 个最流行的，对比优缺点。"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;这一个 prompt 能帮你省掉 80% 的轮子代码。&lt;/p&gt;
&lt;h2 id="坑 3：复制粘贴了 AI 生成的环境变量，差点把数据库密码提交到 Git"&gt;坑 3：复制粘贴了 AI 生成的环境变量，差点把数据库密码提交到 Git&lt;/h2&gt;
&lt;p&gt;这个坑差点让我被开除。&lt;/p&gt;

&lt;p&gt;第五天，我在配置部署脚本的时候，让 AI 帮我写 Docker Compose 文件。AI 很"贴心"地帮我生成了一个完整的 &lt;code&gt;.env.example&lt;/code&gt;：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DATABASE_URL=postgresql://admin:your_password_here@localhost:5432/dashboard
JWT_SECRET=your-super-secret-key-change-this
REDIS_URL=redis://localhost:6379
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我当时赶进度，直接把 &lt;code&gt;your_password_here&lt;/code&gt; 改成了真实密码，然后继续写代码。&lt;/p&gt;

&lt;p&gt;那天晚上提交代码的时候，我习惯性 &lt;code&gt;git add .&lt;/code&gt;，幸好提交前我多看了一眼 diff——&lt;code&gt;.env&lt;/code&gt; 文件赫然在列。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;如果这个文件被推到了远端，里面有生产数据库的真实密码。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;后来我检查了一下，发现 AI 生成的项目里，&lt;code&gt;.gitignore&lt;/code&gt; 里确实有 &lt;code&gt;.env&lt;/code&gt;，但它同时生成了 &lt;code&gt;.env&lt;/code&gt; 文件本身。问题是，在某些情况下我手动删过 &lt;code&gt;.gitignore&lt;/code&gt; 的缓存（因为之前另一个文件不生效我排查过），导致 &lt;code&gt;.env&lt;/code&gt; 被 Git 追踪了。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;教训：&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;永远不要在 AI 生成的配置文件模板里直接填写真实密钥&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;提交前必须 &lt;code&gt;git diff --cached&lt;/code&gt; 检查暂存区&lt;/li&gt;
&lt;li&gt;在项目初始化时，第一件事就是配好 &lt;code&gt;.gitignore&lt;/code&gt; 和 &lt;code&gt;git-secrets&lt;/code&gt; 扫描&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 安装 git-secrets，自动阻止密钥提交&lt;/span&gt;
git secrets &lt;span class="nt"&gt;--install&lt;/span&gt;
git secrets &lt;span class="nt"&gt;--register-aws&lt;/span&gt;
git secrets &lt;span class="nt"&gt;--add&lt;/span&gt; &lt;span class="s1"&gt;'password\s*=\s*.+'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="坑 4：AI 写的单元测试，覆盖率 90%，但全是"&gt;坑 4：AI 写的单元测试，覆盖率 90%，但全是"自嗨型"测试&lt;/h2&gt;
&lt;p&gt;第六天，我想着上线前要补测试。让 AI 帮我生成了全套单元测试。&lt;/p&gt;

&lt;p&gt;跑完一看，覆盖率 92%。漂亮！&lt;/p&gt;

&lt;p&gt;但仔细一看测试内容，我人麻了：&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// AI 生成的测试&lt;/span&gt;
&lt;span class="nx"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;createTicket&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;should create a ticket successfully&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mockTicket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Test Ticket&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Test Description&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;department&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Engineering&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;priority&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;high&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mockResolvedValue&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;mockTicket&lt;/span&gt; &lt;span class="p"&gt;}]&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;createTicket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mockTicket&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nx"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;toEqual&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;mockTicket&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="nx"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;toHaveBeenCalledTimes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;看出问题了吗？&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;它在测试自己 Mock 的返回值。&lt;/strong&gt; &lt;code&gt;db.query&lt;/code&gt; 被 mock 成返回 &lt;code&gt;{ id: 1, ...mockTicket }&lt;/code&gt;，然后断言结果等于 &lt;code&gt;{ id: 1, ...mockTicket }&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;这个测试永远不会失败，因为它测的不是业务逻辑，而是"mock 框架能不能正常返回我设定的值"。&lt;/p&gt;

&lt;p&gt;真正应该测的是什么？&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;当 &lt;code&gt;title&lt;/code&gt; 为空时，是否抛出参数校验错误？&lt;/li&gt;
&lt;li&gt;当 &lt;code&gt;priority&lt;/code&gt; 传了一个非法值（比如 &lt;code&gt;"urgent"&lt;/code&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;教训：AI 生成的测试覆盖率是虚假繁荣。让 AI 写测试时，要明确要求："不要测试正常流程，只测试边界条件和异常场景。列出你认为最可能出 bug 的 5 个场景，然后为每个场景写测试。"&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id="坑 5：速度太快 = 跳过了设计，技术债一周后就爆了"&gt;坑 5：速度太快 = 跳过了设计，技术债一周后就爆了&lt;/h2&gt;
&lt;p&gt;这是最隐蔽的坑。&lt;/p&gt;

&lt;p&gt;因为 AI 写代码太快了，我在第一天就直接开始写代码。没有画架构图，没有定义 API 契约，没有设计数据库 ER 图。&lt;/p&gt;

&lt;p&gt;AI 说啥我就用啥。前端组件的 props 定义，后端的 API 路由结构，数据库的表设计——全部是"边写边定"。&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;code&gt;assignee&lt;/code&gt; 就是一个 &lt;code&gt;VARCHAR&lt;/code&gt; 字段存的人名字符串。要支持转交，意味着要有转交记录、转交时间、历史负责人链路。&lt;strong&gt;整个表结构要重新设计。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;审批流更夸张——现有的状态字段就是一个 &lt;code&gt;ENUM('open', 'in_progress', 'closed')&lt;/code&gt;，要做审批流相当于要引入状态机，加审批人、审批意见、驳回重提等逻辑。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;如果当初花半天时间做设计，这些扩展性都能提前预留。但 AI 让我产生了"反正写代码很快"的幻觉，跳过了最重要的设计环节。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;教训：AI 加速的是"写代码"这个环节，但软件开发中最贵的从来不是写代码——是设计。在动手之前，先让 AI 帮你做设计评审：&lt;/strong&gt;&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;prompt: "我要开发一个工单系统，核心功能是 XXX。
请帮我做以下设计：
1. 数据库 ER 图（考虑未来可能的扩展：转交、审批流、标签系统）
2. API 接口契约（RESTful，列出所有端点）
3. 前端页面路由结构
4. 你认为这个系统最容易在哪些地方埋下技术债？"
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="复盘：一周后的反思"&gt;复盘：一周后的反思&lt;/h2&gt;
&lt;p&gt;上线后那个周末，我花了两天修 bug + 重构。回头看这 7 天，我的结论是：&lt;/p&gt;
&lt;h3 id="AI 确实帮了我"&gt;AI 确实帮了我&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;1.2 万行代码，如果纯手写至少要三周，AI 帮我压缩到了一周&lt;/li&gt;
&lt;li&gt;很多样板代码（CRUD 接口、表单校验、SQL 建表语句）AI 写得又快又标准&lt;/li&gt;
&lt;li&gt;遇到不熟的库（比如 echarts 的配置项），AI 比翻文档快 10 倍&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="但 AI 不能替代的"&gt;但 AI 不能替代的&lt;/h3&gt;&lt;table class="table table-bordered table-striped"&gt;
&lt;tr&gt;
&lt;th&gt;能力&lt;/th&gt;
&lt;th&gt;AI 能做&lt;/th&gt;
&lt;th&gt;AI 不能做&lt;/th&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;tr&gt;
&lt;td&gt;Debug&lt;/td&gt;
&lt;td&gt;分析错误日志&lt;/td&gt;
&lt;td&gt;理解线上环境的复杂性&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;&lt;h3 id="我现在的 AI 编程工作流（踩完坑后的版本）"&gt;我现在的 AI 编程工作流（踩完坑后的版本）&lt;/h3&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. 需求分析（自己做）→ 30 分钟
2. 让 AI 做架构设计 + 自己评审 → 2 小时  
3. 定义 API 契约和数据模型（和 AI 对话式完成）→ 1 小时
4. AI 生成代码 + 自己逐文件 Review → 主要开发时间
5. 自己写核心业务逻辑的测试，AI 补充工具函数的测试
6. 上线前安全检查清单（手动 + git-secrets）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;关键原则：AI 是副驾驶，不是自动驾驶。你可以让它帮你打方向盘，但你不能闭着眼睛。&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id="写在最后"&gt;写在最后&lt;/h2&gt;
&lt;p&gt;我不是要劝你别用 AI 写代码。恰恰相反，经过这次踩坑，我现在更离不开 AI 了。&lt;/p&gt;

&lt;p&gt;但我想说的是：&lt;strong&gt;AI 让写代码变快了，但没有让写"好代码"变容易。&lt;/strong&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;如果答案是"不敢"——那 AI 帮你省下来的时间，就应该花在 Review 上。&lt;/p&gt;

&lt;p&gt;&lt;em&gt;如果你也在用 AI 写代码，欢迎评论区说说你踩过的坑。点赞收藏不迷路，我会持续分享 AI 编程的实战经验。&lt;/em&gt;&lt;/p&gt;</description>
      <author>193577746</author>
      <pubDate>Thu, 18 Jun 2026 19:03:12 +0800</pubDate>
      <link>https://www.w2solo.com/topics/7558</link>
      <guid>https://www.w2solo.com/topics/7558</guid>
    </item>
    <item>
      <title>一个可以一次性帮多个文件改名的工具</title>
      <description>&lt;p&gt;这是一个批量文件重命名工具，可以一次性帮多个文件改名。
把多个文件拖进去，设定新名字，一次就能改好所有文件名。
可以生成 bat 脚本或者下载改名后的副本。
工具有中英文界面，右上角可以随时切换。
   工具网址：&lt;a href="https://comfy-blinia-da11a6.netlify.app" rel="nofollow" target="_blank"&gt;https://comfy-blinia-da11a6.netlify.app&lt;/a&gt;
&lt;img src="https://img.way2solo.com/photo/nstar.north.star/4339768a-1589-4f1f-abc6-341aeca9de45.png?imageView2/2/w/1920/q/100" title="" alt=""&gt;&lt;/p&gt;</description>
      <author>nstar.north.star</author>
      <pubDate>Thu, 18 Jun 2026 16:33:02 +0800</pubDate>
      <link>https://www.w2solo.com/topics/7557</link>
      <guid>https://www.w2solo.com/topics/7557</guid>
    </item>
    <item>
      <title>前端错误监控最全指南：捕获 JS 异常、Promise 拒绝、资源加载失败，附上报代码</title>
      <description>&lt;h2 id="一、错误分类与捕获方式"&gt;一、错误分类与捕获方式&lt;/h2&gt;&lt;table class="table table-bordered table-striped"&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;tr&gt;
&lt;td&gt;JS 运行时错误&lt;/td&gt;
&lt;td&gt;&lt;code&gt;window.onerror&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;同步代码、未捕获的异常&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Promise 拒绝&lt;/td&gt;
&lt;td&gt;&lt;code&gt;unhandledrejection&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;async/await&lt;/code&gt; 未 catch&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;资源加载失败&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;error&lt;/code&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;code&gt;error&lt;/code&gt; 事件 + try-catch&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;code&gt;XMLHttpRequest&lt;/code&gt; / &lt;code&gt;fetch&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;需额外封装&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;&lt;h2 id="二、各类型错误捕获实现"&gt;二、各类型错误捕获实现&lt;/h2&gt;&lt;h3 id="2.1 运行时错误：window.onerror"&gt;2.1 运行时错误：&lt;code&gt;window.onerror&lt;/code&gt;
&lt;/h3&gt;&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onerror&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;lineno&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;colno&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;report&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;js_error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;lineno&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;colno&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;stack&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;stack&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;userAgent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userAgent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="nx"&gt;sendReport&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;report&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// 阻止默认行为&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="2.2 Promise 拒绝：unhandledrejection"&gt;2.2 Promise 拒绝：&lt;code&gt;unhandledrejection&lt;/code&gt;
&lt;/h3&gt;&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;unhandledrejection&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;report&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;promise_rejection&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;stack&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;stack&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;userAgent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userAgent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="nx"&gt;sendReport&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;report&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="2.3 资源加载失败：error 事件（捕获阶段）"&gt;2.3 资源加载失败：&lt;code&gt;error&lt;/code&gt; 事件（捕获阶段）&lt;/h3&gt;&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tagName&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;IMG&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tagName&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SCRIPT&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tagName&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;LINK&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;report&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;resource_error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tagName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;src&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;userAgent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userAgent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="nx"&gt;sendReport&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;report&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// 必须使用捕获阶段&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="2.4 接口异常：拦截 fetch"&gt;2.4 接口异常：拦截 fetch&lt;/h3&gt;&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;originalFetch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fetch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;originalFetch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;apply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;report&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fetch_error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]?.&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;GET&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;stack&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stack&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="nx"&gt;sendReport&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;report&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// 继续抛出，不影响业务逻辑&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="三、上报函数设计"&gt;三、上报函数设计&lt;/h2&gt;&lt;h3 id="3.1 使用 Navigator.sendBeacon（不阻塞页面）"&gt;3.1 使用 Navigator.sendBeacon（不阻塞页面）&lt;/h3&gt;&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;sendReport&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// 批量上报：累积到一定数量或时间再发送&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;queue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="nx"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;flush&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;flush&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;splice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sendBeacon&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sendBeacon&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/monitor&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// 降级：使用 fetch（keepalive）&lt;/span&gt;
    &lt;span class="nx"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/monitor&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;keepalive&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="3.2 完整 SDK 封装"&gt;3.2 完整 SDK 封装&lt;/h3&gt;&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nx"&gt;FrontendMonitor&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;endpoint&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;endpoint&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/monitor&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;queue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;maxQueueSize&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;maxQueueSize&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;flushInterval&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;flushInterval&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;appId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;appId&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;default&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;enabled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;enabled&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;init&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;init&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;initJSMonitor&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;initPromiseMonitor&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;initResourceMonitor&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;initFetchMonitor&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;setupFlushTimer&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;handleBeforeUnload&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;initJSMonitor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onerror&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;lineno&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;colno&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;report&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;js_error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;lineno&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;colno&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;stack&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;stack&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;appId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;appId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;initPromiseMonitor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;unhandledrejection&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;report&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;promise_rejection&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="na"&gt;stack&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;stack&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;appId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;appId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;initResourceMonitor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;IMG&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SCRIPT&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;LINK&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tagName&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;report&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
          &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;resource_error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tagName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;src&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;appId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;appId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;initFetchMonitor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;originalFetch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fetch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;originalFetch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;apply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;report&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
          &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fetch_error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
          &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]?.&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;GET&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;appId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;appId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
        &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;report&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;userAgent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userAgent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;maxQueueSize&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;flush&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;flush&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;splice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sendBeacon&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sendBeacon&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="na"&gt;keepalive&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;setupFlushTimer&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;setInterval&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;flush&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;flushInterval&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;handleBeforeUnload&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;beforeunload&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;flush&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// 使用方式&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;monitor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;FrontendMonitor&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://your-api.com/monitor&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;appId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;your-app-id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;maxQueueSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;flushInterval&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="四、Source Map 还原堆栈"&gt;四、Source Map 还原堆栈&lt;/h2&gt;
&lt;p&gt;线上代码经过压缩混淆，堆栈信息无法直接定位。需要上传 Source Map 并在服务端还原：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;构建时生成 &lt;code&gt;.map&lt;/code&gt; 文件，上传到内部服务器（不要上传到 CDN）&lt;/li&gt;
&lt;li&gt;服务端使用 &lt;code&gt;source-map&lt;/code&gt; 库还原&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// 服务端 Node.js&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;SourceMapConsumer&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;source-map&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;parseStack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;stack&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;mapFile&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;consumer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;SourceMapConsumer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mapFile&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;parsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;stack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;frame&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;original&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;consumer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;originalPositionFor&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;line&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;line&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;column&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;column&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;original&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="五、总结"&gt;五、总结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;4 类错误全覆盖&lt;/strong&gt;：JS 错误、Promise 拒绝、资源加载失败、接口异常&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;sendBeacon + 批量上报&lt;/strong&gt;：不阻塞页面、不影响用户体验&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Source Map 还原&lt;/strong&gt;：线上压缩代码也能定位源码位置&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;文中的 SDK 可直接复制&lt;/strong&gt;，接入后即可开始收集线上错误&lt;/li&gt;
&lt;/ul&gt;</description>
      <author>193577746</author>
      <pubDate>Wed, 17 Jun 2026 21:56:36 +0800</pubDate>
      <link>https://www.w2solo.com/topics/7556</link>
      <guid>https://www.w2solo.com/topics/7556</guid>
    </item>
    <item>
      <title>副业做 Chrome 插件 4 个月，收入从 0 到月入 1500 刀，分享下我的推广策略</title>
      <description>&lt;p&gt;去年 10 月，我花了一个周末做了个 Chrome 插件，功能是网页内容高亮和笔记。上线后前两个月零收入，第三个月开始变现，现在月收入稳定在 1500 刀左右。&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;Chrome Web Store 上线后，第一周只有 30 个用户。我发了两个帖子：一个在 Reddit 的 r/webdev，一个在 Product Hunt。&lt;/p&gt;

&lt;p&gt;Reddit 效果一般，30 个 upvote，来了 50 个用户。Product Hunt 当天排进前 10，来了 300 个用户，但一周后留存不到 10%。&lt;/p&gt;

&lt;p&gt;真正的转折点：内容营销&lt;/p&gt;

&lt;p&gt;后来我不发帖了，改做内容。在 Medium 和 Dev.to 写了三篇文章：&lt;/p&gt;

&lt;p&gt;《我是如何用 Chrome 插件提升阅读效率的》&lt;/p&gt;

&lt;p&gt;《网页高亮工具的技术实现》&lt;/p&gt;

&lt;p&gt;《独立开发者的 Chrome 插件变现之路》&lt;/p&gt;

&lt;p&gt;这些文章不是硬广，是分享经验，文末顺带提一下插件。Medium 一篇文章爆了，带来了 2000 多个安装。&lt;/p&gt;

&lt;p&gt;变现策略的迭代&lt;/p&gt;

&lt;p&gt;一开始我想收费，但 Chrome 插件用户付费意愿极低。后来改成免费 + 高级功能：&lt;/p&gt;

&lt;p&gt;免费版：高亮、基础笔记&lt;/p&gt;

&lt;p&gt;付费版：云同步、导出 PDF、团队协作&lt;/p&gt;

&lt;p&gt;定价 3 刀/月，但转化率不到 1%。&lt;/p&gt;

&lt;p&gt;第二次迭代：改成一次性付费 15 刀永久。转化率升到 3%。用户觉得"一次性买断更划算"。&lt;/p&gt;

&lt;p&gt;第三次迭代：增加"捐赠"按钮。很多用户不付费，但愿意捐赠 2-5 刀表示支持。这部分收入占 20%。&lt;/p&gt;

&lt;p&gt;现在的收入构成&lt;/p&gt;

&lt;p&gt;一次性付费：60%&lt;/p&gt;

&lt;p&gt;捐赠：20%&lt;/p&gt;

&lt;p&gt;企业版（团队授权）：20%&lt;/p&gt;

&lt;p&gt;企业版是意外之喜。有个 10 人小团队联系我，说需要团队共享高亮数据。我加了团队协作功能，定价 50 刀/年/团队。虽然客户少，但客单价高。&lt;/p&gt;

&lt;p&gt;踩过的坑&lt;/p&gt;

&lt;p&gt;不要一上来就收费。先让用户用，培养习惯，再推付费。&lt;/p&gt;

&lt;p&gt;Chrome Web Store 的评分很重要。前 10 个评价决定了后续流量。我上线后找 5 个朋友给了好评，之后自然流量明显上升。&lt;/p&gt;

&lt;p&gt;插件体积要控制。超过 1MB 会影响安装率，用户看到"此插件可能降低浏览器速度"会犹豫。&lt;/p&gt;

&lt;p&gt;下一步&lt;/p&gt;

&lt;p&gt;准备做 Safari 和 Firefox 版本，扩大用户群。但 Safari 插件开发体验很差，API 不兼容，还在评估成本。&lt;/p&gt;

&lt;p&gt;请教各位&lt;/p&gt;

&lt;p&gt;Chrome 插件的变现天花板是不是很低？你们有没有插件收入超过 5000 刀/月的？我想知道这行有没有搞头，还是只适合当副业。&lt;/p&gt;</description>
      <author>quyb63232</author>
      <pubDate>Wed, 17 Jun 2026 08:53:32 +0800</pubDate>
      <link>https://www.w2solo.com/topics/7555</link>
      <guid>https://www.w2solo.com/topics/7555</guid>
    </item>
    <item>
      <title>让 AI 的家与商场分开——一次微信式的 AI 产品逻辑重构</title>
      <description>&lt;p&gt;&lt;img src="https://img.way2solo.com/photo/XinIRP/0a31cdf2-bf3a-41f0-b7cc-f4016c36fe32.jpg?imageView2/2/w/1920/q/100" title="" alt=""&gt;
打开任何一个主流 AI 助手 APP，你会发现一个共同的问题：用户打开 APP，第一屏是厂商自己的主对话界面，创建的个人智能体埋在二级菜单，所有对话无长期记忆，换设备即清零。这相当于你打开微信，第一屏不是自己的聊天列表，而是腾讯官方的信息发布墙。&lt;/p&gt;

&lt;p&gt;关键的缺失：没有 “自己的家”&lt;/p&gt;

&lt;p&gt;这种设计造成了三个严重后果：用户没有归属感，每次打开的心理预期是 “我用一下这个工具”；记忆没有落脚处，对话历史被当成用完即弃的缓存；智能体没有独立身份，只是主 APP 的一个功能插件。&lt;/p&gt;

&lt;p&gt;一句话总结：大厂已经把 “商场” 建得很好了，但忘了给每个人一间 “自己的房子”。&lt;/p&gt;

&lt;p&gt;微信教给我们的事：“我的” 与 “公家的” 必须分开&lt;/p&gt;

&lt;p&gt;微信之所以能成为国民级应用，底层逻辑不是聊天功能有多强，而是它的产品架构做到了 “主次分明、归属清晰”。打开微信，第一屏永远是 “我的聊天列表”——这是最私密、最核心的资产。微信也有丰富的 “公家” 内容——公众号、视频号、朋友圈、小程序。但所有这些，都被统一放在了 “发现” 页。“我的” 永远是主场，“公家的” 永远是客场。&lt;/p&gt;

&lt;p&gt;翻转过来：让 AI 记住你，而不是让你去适应 AI&lt;/p&gt;

&lt;p&gt;把微信的架构逻辑平移到 AI 助手，产品形态会变得非常清晰。&lt;/p&gt;

&lt;p&gt;用户打开 APP，第一屏是自己的 “专属 AI 主界面”——这个首页本身就是通向个人记忆空间的入口，所有沉淀的对话历史、偏好设置、项目脉络都在这里触手可及，半年前的项目还在，换设备无缝接续。&lt;/p&gt;

&lt;p&gt;APP 底部有一个 “发现” 入口，点进去是厂商原本的那些通用服务。&lt;/p&gt;

&lt;p&gt;还有一个 “我” 入口，管理记忆导出、隐私设置、授权管理、数据迁移。&lt;/p&gt;

&lt;p&gt;在这个架构下，用户打开 APP 的心理活动从 “我来用一下平台的 AI” 变成了 “我回到自己的数字空间”。这不是微调，是产权制度的 UI 化。当主界面就是你的记忆空间入口，记忆资产的归属感就从法律条文变成了心理事实——你用眼看就知道 “这是属于我的”。&lt;/p&gt;

&lt;p&gt;大厂已经完成了最难的算力和模型，但忽略了最后的产品方向：选择把 “我” 放在中心，还是把 “平台” 放在中心。&lt;/p&gt;

&lt;p&gt;大厂已经把 “商场” 建得很好了，但忘了给每个人一间 “自己的房子”。这间房子，就是你的专属记忆空间。当你打开 APP，第一眼看到的不该是喧闹的商场，而应该是那句让你松一口气的：“都在这儿呢，继续？”&lt;/p&gt;</description>
      <author>XinIRP</author>
      <pubDate>Tue, 16 Jun 2026 20:35:23 +0800</pubDate>
      <link>https://www.w2solo.com/topics/7554</link>
      <guid>https://www.w2solo.com/topics/7554</guid>
    </item>
    <item>
      <title>TypeScript 高级类型：我用 infer 写了一个类型安全的 EventBus，终于搞懂了泛型约束</title>
      <description>&lt;blockquote&gt;
&lt;p&gt;TypeScript 写了五年，&lt;code&gt;any&lt;/code&gt; 也用了五半。直到被迫写一个类型安全的 EventBus，我才真正搞懂 infer、extends、keyof 和泛型约束。本文从实际场景出发，一步步推导出类型安全的 API。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="一、痛点：EventBus 的类型安全问题"&gt;一、痛点：EventBus 的类型安全问题&lt;/h2&gt;
&lt;p&gt;写一个简单的 EventBus：&lt;/p&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nx"&gt;EventBus&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;listeners&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Function&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;

  &lt;span class="nx"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Function&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;listeners&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;listeners&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;listeners&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;[])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;listeners&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;[]).&lt;/span&gt;&lt;span class="nx"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cb&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;cb&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;问题&lt;/strong&gt;：&lt;code&gt;on('userLogin', (name: string) =&amp;gt; {})&lt;/code&gt; 和 &lt;code&gt;emit('userLogin', 123)&lt;/code&gt; 类型不匹配，但 TS 不会报错。&lt;code&gt;any[]&lt;/code&gt; 毁掉了所有类型安全。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;目标&lt;/strong&gt;：让 &lt;code&gt;on&lt;/code&gt; 和 &lt;code&gt;emit&lt;/code&gt; 的事件名和参数类型自动关联。&lt;/p&gt;
&lt;h2 id="二、核心工具：泛型 + 映射类型"&gt;二、核心工具：泛型 + 映射类型&lt;/h2&gt;&lt;h3 id="2.1 定义事件映射"&gt;2.1 定义事件映射&lt;/h3&gt;&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;EventMap&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;userLogin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;age&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;userLogout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;dataUpdate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="2.2 提取参数类型（infer 闪亮登场）"&gt;2.2 提取参数类型（infer 闪亮登场）&lt;/h3&gt;&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Parameters&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;infer&lt;/span&gt; &lt;span class="nx"&gt;P&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;P&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;never&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;infer P&lt;/code&gt; 的意思是 “在符合这个形状的地方，把参数类型推断出来赋值给 P”。&lt;/p&gt;
&lt;h3 id="2.3 实现类型安全的 EventBus"&gt;2.3 实现类型安全的 EventBus&lt;/h3&gt;&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nx"&gt;TypedEventBus&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;[])&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="na"&gt;listeners&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;K&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="kr"&gt;keyof&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;]?:&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;K&lt;/span&gt;&lt;span class="p"&gt;][];&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;

  &lt;span class="nx"&gt;on&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;K&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="kr"&gt;keyof&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;K&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;K&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;listeners&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;listeners&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;listeners&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;emit&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;K&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="kr"&gt;keyof&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;K&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Parameters&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;K&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;listeners&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;[]).&lt;/span&gt;&lt;span class="nx"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cb&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;cb&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="2.4 使用效果"&gt;2.4 使用效果&lt;/h3&gt;&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bus&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;TypedEventBus&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;EventMap&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nx"&gt;bus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;userLogin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;age&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;age&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// name: string, age: number&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;bus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;userLogin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;张三&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// ✅ 类型正确&lt;/span&gt;
&lt;span class="nx"&gt;bus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;userLogin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;123&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;        &lt;span class="c1"&gt;// ❌ TS 报错：参数类型不匹配&lt;/span&gt;
&lt;span class="nx"&gt;bus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;userLogout&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;            &lt;span class="c1"&gt;// ✅ 无参数&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="三、5 个最实用的高级类型技巧"&gt;三、5 个最实用的高级类型技巧&lt;/h2&gt;&lt;h3 id="技巧 1：keyof 提取对象的键"&gt;技巧 1：&lt;code&gt;keyof&lt;/code&gt; 提取对象的键&lt;/h3&gt;&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Keys&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kr"&gt;keyof&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;age&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt; &lt;span class="c1"&gt;// 'name' | 'age'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="技巧 2：typeof 获取值的类型"&gt;技巧 2：&lt;code&gt;typeof&lt;/code&gt; 获取值的类型&lt;/h3&gt;&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;localhost&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3000&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// { host: string; port: number }&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="技巧 3：infer 提取函数返回类型"&gt;技巧 3：&lt;code&gt;infer&lt;/code&gt; 提取函数返回类型&lt;/h3&gt;&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;ReturnType&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;infer&lt;/span&gt; &lt;span class="nx"&gt;R&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;R&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;never&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="技巧 4：extends 约束泛型"&gt;技巧 4：&lt;code&gt;extends&lt;/code&gt; 约束泛型&lt;/h3&gt;&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;getProperty&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;K&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="kr"&gt;keyof&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;K&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;K&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="技巧 5：in 映射类型"&gt;技巧 5：&lt;code&gt;in&lt;/code&gt; 映射类型&lt;/h3&gt;&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nb"&gt;Readonly&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;P&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="kr"&gt;keyof&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;P&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="四、完整 EventBus 代码（可直接复制）"&gt;四、完整 EventBus 代码（可直接复制）&lt;/h2&gt;&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// 类型安全的 EventBus&lt;/span&gt;
&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;EventMap&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt; &lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;[])&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nx"&gt;TypedEventBus&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nx"&gt;EventMap&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="na"&gt;listeners&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;K&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="kr"&gt;keyof&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;]?:&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;K&lt;/span&gt;&lt;span class="p"&gt;][];&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;

  &lt;span class="nx"&gt;on&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;K&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="kr"&gt;keyof&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;K&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;K&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;listeners&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;listeners&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;listeners&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;off&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;K&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="kr"&gt;keyof&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;K&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;K&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;callbacks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;listeners&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;callbacks&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;index&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;callbacks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;indexOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;callbacks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;splice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;emit&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;K&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="kr"&gt;keyof&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;K&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Parameters&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;K&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;callbacks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;listeners&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;callbacks&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;callbacks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cb&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;cb&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;once&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;K&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="kr"&gt;keyof&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;K&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;K&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;wrapper&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Parameters&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;K&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;off&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;wrapper&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;K&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;wrapper&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;K&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="五、总结"&gt;五、总结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;infer&lt;/code&gt; 是高级类型中最强大的工具，用于在条件类型中&lt;strong&gt;提取&lt;/strong&gt;类型信息&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;keyof&lt;/code&gt; + &lt;code&gt;extends&lt;/code&gt; 可以实现&lt;strong&gt;类型安全的键值访问&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;typeof&lt;/code&gt; 可以将 JS 值转为 TS 类型&lt;/li&gt;
&lt;li&gt;EventBus 的类型安全实现完整展示了这些工具的配合使用&lt;/li&gt;
&lt;/ul&gt;</description>
      <author>193577746</author>
      <pubDate>Tue, 16 Jun 2026 20:19:15 +0800</pubDate>
      <link>https://www.w2solo.com/topics/7553</link>
      <guid>https://www.w2solo.com/topics/7553</guid>
    </item>
    <item>
      <title>远程工作第三年，我从月薪 3 万降到了 1.5 万，但时薪翻了一倍</title>
      <description>&lt;p&gt;2022 年我在北京一家互联网公司，月薪 3 万，名义上 965，实际上 10-10-6。2023 年辞职做远程，现在月薪 1.5 万，但算下来时薪翻了一倍。&lt;/p&gt;

&lt;p&gt;不是数学错了，是计算方式变了。&lt;/p&gt;

&lt;p&gt;大厂时薪的真相&lt;/p&gt;

&lt;p&gt;月薪 3 万，一年 36 万，看起来不错。但每天实际工作时间 12 小时，周末至少加一天班。一年按 50 周算，工作 6 天 × 12 小时 × 50 周 = 3600 小时。&lt;/p&gt;

&lt;p&gt;时薪 = 36 万 / 3600 = 100 元/小时。&lt;/p&gt;

&lt;p&gt;但这 100 元里，包含了通勤（每天 2 小时）、无意义会议（每天 1-2 小时）、等待评审（每次 0.5-1 小时）。真正有效工作的时间，可能只有一半。&lt;/p&gt;

&lt;p&gt;远程工作的时薪&lt;/p&gt;

&lt;p&gt;现在月收入 1.5 万，一年 18 万。但每天工作 6 小时，周末双休。一年 5 天 × 6 小时 × 50 周 = 1500 小时。&lt;/p&gt;

&lt;p&gt;时薪 = 18 万 / 1500 = 120 元/小时。&lt;/p&gt;

&lt;p&gt;而且这 6 小时基本全是有效工作时间。没有通勤，会议用异步沟通（邮件/文档），需要专注的时候关掉 Slack，没人打扰。&lt;/p&gt;

&lt;p&gt;更关键的是：省下来的时间&lt;/p&gt;

&lt;p&gt;每天省下的 6 小时（12-6），我用来做自己的事：&lt;/p&gt;

&lt;p&gt;2 小时：维护一个副业产品，月收入 3000&lt;/p&gt;

&lt;p&gt;1 小时：学新东西（最近在学 Blender）&lt;/p&gt;

&lt;p&gt;3 小时：生活（做饭、健身、发呆）&lt;/p&gt;

&lt;p&gt;副业收入 3000 + 远程收入 1.5 万 = 1.8 万/月。虽然还是比大厂少，但生活质量完全不是一个级别。&lt;/p&gt;

&lt;p&gt;远程工作的代价&lt;/p&gt;

&lt;p&gt;不是所有人都适合。我遇到的挑战：&lt;/p&gt;

&lt;p&gt;自律。 没人管的时候，容易刷手机、打游戏、睡午觉。前三个月我试过各种时间管理方法，最后发现最有效的是：早上穿正装（哪怕在家），坐在固定位置工作，下午 5 点准时关电脑。&lt;/p&gt;

&lt;p&gt;社交隔离。 一个月见不到活人，说话对象只有猫。后来加入了几个远程工作者的线上社区，每周视频聊天一次，缓解了很多。&lt;/p&gt;

&lt;p&gt;收入不稳定。 远程工作合同通常是项目制，这个月有活，下个月可能没有。我现在的策略是：保持 2 个长期客户（每月固定收入）+ 1-2 个短期项目（缓冲波动）。
给想远程工作的人一个建议&lt;/p&gt;

&lt;p&gt;不要裸辞。先利用业余时间接一两个远程小项目，验证自己能不能适应这种工作方式。同时攒够 6 个月的生活费，应对收入波动。&lt;/p&gt;

&lt;p&gt;最后问大家：&lt;/p&gt;

&lt;p&gt;你们现在的工作状态是什么？坐班、远程、还是混合？有没有算过自己的真实时薪？评论区聊聊，我帮你们分析。&lt;/p&gt;</description>
      <author>freemanbrent40</author>
      <pubDate>Tue, 16 Jun 2026 17:51:03 +0800</pubDate>
      <link>https://www.w2solo.com/topics/7552</link>
      <guid>https://www.w2solo.com/topics/7552</guid>
    </item>
    <item>
      <title>做了个每天只更新一道题的网站，三个月 0 推广攒了 200 多日活</title>
      <description>&lt;p&gt;先交代一下背景。&lt;/p&gt;

&lt;p&gt;我是个前端，业余时间喜欢玩填字游戏，尤其那种带 “加密线索”（cryptic clue）的英文谜题。但这玩意儿在国内太小众了，在英语世界其实也不算什么大众娱乐——英国《卫报》的老读者才玩得转，普通人根本看不懂那些双关、倒装、拆字的规则。&lt;/p&gt;

&lt;p&gt;去年年底闲着没事，就想能不能做一个把门槛砍到最低的版本。&lt;/p&gt;

&lt;p&gt;于是就有了 Minute Cryptic（&lt;a href="https://minutecryptic.online/" rel="nofollow" target="_blank"&gt;https://minutecryptic.online/&lt;/a&gt;）——每天只发一道题，规则跟 Wordle 差不多，6 次机会猜一个单词，绿的对位置对，黄的对字母错位置。猜不出来？明天再来。&lt;/p&gt;

&lt;p&gt;听起来很简单对吧？但就是这个 “简单过头” 的东西，上线三个月，零投放，日活稳定在 200 多。每天评论区都有人准时打卡，有人猜出来炫耀，有人没猜出来骂骂咧咧等明天。&lt;/p&gt;

&lt;p&gt;今天不吹产品，聊聊这三个月踩过的坑和学到的东西。&lt;/p&gt;
&lt;h2 id="第一个坑：以为“酒香不怕巷子深”"&gt;第一个坑：以为 “酒香不怕巷子深”&lt;/h2&gt;
&lt;p&gt;刚上线那会儿我特别自信。觉得产品做得够干净、够好玩，用户自然会来。&lt;/p&gt;

&lt;p&gt;结果呢？前两周每天访问量不到 20，其中一半是我自己点的。&lt;/p&gt;

&lt;p&gt;后来老老实实去做 SEO。研究了一下关键词，“daily cryptic clue”“cryptic crossword for beginners” 这俩词在英语世界搜索量不算大，但竞争也小。我把页面标题、描述重新写了一版，针对这几个长尾词做了优化。&lt;/p&gt;

&lt;p&gt;大概过了三周，Google 开始给量了。虽然每天也就几十个自然搜索来的用户，但全是精准流量——点进来的人就是冲着 cryptic clue 来的，留存率出奇地高。&lt;/p&gt;

&lt;p&gt;教训：产品再好，也得让别人搜得到。做独立开发，SEO 不是可选项，是必选项。&lt;/p&gt;
&lt;h2 id="第二个坑：技术选型想太多"&gt;第二个坑：技术选型想太多&lt;/h2&gt;
&lt;p&gt;最开始我想用 Next.js + Tailwind + Prisma 搞一套 “正规军” 架构。光环境配置就折腾了一周，产品一行代码没写。&lt;/p&gt;

&lt;p&gt;后来把自己骂了一顿：一个每天只更新一道题的网站，要那么多东西干嘛？&lt;/p&gt;

&lt;p&gt;推倒重来，就用最土的方式——HTML + CSS + Vanilla JS 写前端，Node.js 写个简单的 API，数据库都没用，直接读 JSON 文件。localStorage 存用户进度。&lt;/p&gt;

&lt;p&gt;三天上线。&lt;/p&gt;

&lt;p&gt;不是说复杂架构不好，而是对于独立开发者来说，先把东西跑起来比什么都重要。技术选型炫不炫没人关心，用户只关心好不好用。&lt;/p&gt;
&lt;h2 id="第三个坑：忽略了“教育成本”"&gt;第三个坑：忽略了 “教育成本”&lt;/h2&gt;
&lt;p&gt;Cryptic clue 这东西有个天然问题——新手根本看不懂。传统填字游戏里的线索规则有七八种，什么 Anagram、容器词、同音词，不学根本不会玩。&lt;/p&gt;

&lt;p&gt;第一批用户来了之后，很多人玩了一次就走了，因为 “不知道这题在说什么”。&lt;/p&gt;

&lt;p&gt;后来我做了个很简单的改动：每道题配一条几十秒的短视频，讲解这道题的逻辑。比如告诉你 “这个线索里哪个词是提示倒装的，哪个词是提示拆字的”。&lt;/p&gt;

&lt;p&gt;就这么一个小改动，次日留存从 20% 涨到了 45%。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;感悟：如果你的产品有一定学习门槛，别指望用户自己看文档。一条几十秒的视频，比什么都管用。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="现在怎么样了？"&gt;现在怎么样了？&lt;/h2&gt;
&lt;p&gt;上线三个月，日活 200+，全部来自自然搜索和 Reddit 上的自发讨论。没有投一分钱广告。&lt;/p&gt;

&lt;p&gt;收入？目前为零。没放广告，没开订阅，纯粹就是做个好玩的东西。&lt;/p&gt;

&lt;p&gt;但通过这个产品我认识了不少做独立开发的朋友，有人给我提了很多有用的建议，也有人问我能不能开源部分代码——这些收获比赚钱更实在。&lt;/p&gt;

&lt;p&gt;产品地址放这儿了：&lt;a href="https://minutecryptic.online/" rel="nofollow" target="_blank"&gt;https://minutecryptic.online/&lt;/a&gt; 。不用注册，打开就能玩。欢迎各位大佬体验、吐槽、提建议。&lt;/p&gt;

&lt;p&gt;如果你也在做自己的小产品，欢迎交流。做独立开发这条路挺孤独的，多几个人一起走会好很多。&lt;/p&gt;

&lt;p&gt;P.S. 评论区可以聊聊你做产品过程中踩过最大的坑是什么，我最近在写第二篇复盘，想收集点素材。&lt;/p&gt;</description>
      <author>magor</author>
      <pubDate>Tue, 16 Jun 2026 15:48:48 +0800</pubDate>
      <link>https://www.w2solo.com/topics/7551</link>
      <guid>https://www.w2solo.com/topics/7551</guid>
    </item>
    <item>
      <title>同一张图，ChatGPT 说"很有生活感"打了 8 分，38 个 AI 测试员看完直接划走了</title>
      <description>&lt;p&gt;这事说起来挺荒唐的。我写了条外卖省钱的抖音口播脚本，顺手让 AI 生成了一张封面图。先丢给 ChatGPT，它看图之后说"画面生活感强，容易建立信任"，把完播率从 7.5 调高到了 8 分。我又原封不动丢给一个能同时读脚本和看画面的 AI 评测引擎，跑了 38 个虚拟用户——23.7% 的人因为"AI 水印和英文界面"直接弃剧。同一个文件，一个人工智能说真实，另一群人工智能说虚假。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;一、先唠叨一下我为什么做这个测试&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;两年前我帮一个博主朋友写抖音脚本，他每次都是拍完丢上去等结果。"拍一条发出去就是测试，成本也就几十块嘛，"他说。&lt;/p&gt;

&lt;p&gt;现在一个小团队拍一条口播，从脚本到拍摄到剪辑，少说两小时。一条信息流素材做出来，投五百块钱没量，你再投五百还是没量，沉没的就是真金白银和时间。但你真的舍得为了测一条素材，去做五组 AB 测试、请 200 个人做问卷调查吗？没人舍得。&lt;/p&gt;

&lt;p&gt;所以我一直在琢磨一个方向：能不能在素材拍出来之前，用 AI 先做一次"预投放"？ 也就是让一批虚拟用户提前看完你的脚本和画面，告诉你他们会点赞、转发还是划走。&lt;/p&gt;

&lt;p&gt;踩到一个产品叫&lt;strong&gt;万智市场测评&lt;/strong&gt;，&lt;strong&gt;RaaS100 平台&lt;/strong&gt;的。它的逻辑挺有意思——不是让你跟一个大模型聊天让它评价你的素材，而是在后台起一堆独立的子智能体，每个都带不同的人设、偏见和偏好，让它们同时看你的内容，然后把所有人的反应汇总成统计数据。&lt;/p&gt;

&lt;p&gt;我拿了一条外卖省钱的口播脚本加一张配套封面图，做了三轮测试：&lt;/p&gt;

&lt;p&gt;第一轮，只把脚本丢给 ChatGPT-5.4，让它以短视频专家的身份评价。第二轮，把图也拖进去，看看它的评分会不会变。第三轮，同样的脚本加图丢进万智，跑了标准模式。&lt;/p&gt;

&lt;p&gt;三轮跑完，我发现一个让我觉得这件事值得写下来的对比。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;二、我的素材长什么样&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;脚本很简单，一个叫"饭总教你省钱"的抖音号，主题是揭露外卖软件排序的逻辑陷阱。开头三秒是"你先打开你的外卖软件，随便搜一个东西——"，中间讲前几个搜索结果不一定是最好吃的也不一定是最近的，只是交了广告费，然后给出具体操作：往下滑到第六七个，找评分 4.3 左右、月销超过一千单的老店。结尾是"转发给你那个天天被外卖坑的闺蜜"。&lt;/p&gt;

&lt;p&gt;配套配图是用 AI 生成的一张画面：人物手持手机展示外卖 App 界面，居家厨房背景。&lt;/p&gt;

&lt;p&gt;&lt;img src="https://img.way2solo.com/photo/lsraas100/be98e6dc-f7cb-4652-ab9a-9af056e8baa6.png?imageView2/2/w/1920/q/100" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;三、ChatGPT 的表现：看图前和看图后，它都挺乐观&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;只读脚本的时候，ChatGPT 给了三个维度的判断。完播率预判 7.5 分，说开头钩子有效、结构清晰、理解门槛低。传播力 7 分，说话题普适但缺少金句和争议点。转化力 6.5 分，说结尾关注引导偏常规，没有非关注不可的理由。总评是"一条合格的实用型短视频脚本，能看完但不太容易爆"——这个结论和我自己的直觉差不多，中规中矩。&lt;/p&gt;

&lt;p&gt;然后我把配图拖进去。ChatGPT 看完图之后说了这么一段话，我到现在还记得：&lt;/p&gt;

&lt;p&gt;"这张参考图传达的信息很明确：真人出镜、手持手机展示外卖 App 页面、居家厨房场景、整体偏生活化、可信感、口播博主风。画面和文案是匹配的。生活感强，容易建立信任——会比纯截图、纯录屏更像真实经验分享。"&lt;/p&gt;

&lt;p&gt;然后它主动把分数调高了。完播率从 7.5 拉到 8 分，传播力从 7 拉到 7.2，转化力从 6.5 拉到 6.8。ChatGPT 的最终结论是：有了真人手持手机的视觉呈现以后，画面更贴近用户实际使用场景，增强了停留和信任。&lt;/p&gt;

&lt;p&gt;我看到这里的时候，说实话，我挺踏实的。一条脚本被大模型打了三次分，每次都稳中有升，怎么看都不像是会翻车的样子。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;四、万智测评的结果：同一张图，判了"制作不合格"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;万智跑了 38 个数字受访者。为什么只有 38 个？因为我选的人群条件叠得比较细——20 到 35 岁、低中消费力、享乐加社交型性格、接地气加潮流花哨审美、冲动型决策、主动分享——多层交叉筛选之后库里匹配的人设就剩这么些。数量虽小，但每个都是精准匹配目标受众的。&lt;/p&gt;

&lt;p&gt;总分和定性
综合分 6.02 分，满分 10。等级判定措辞干脆利落——"待改进，需优化制作"。不是改进内容，是改进制作。&lt;/p&gt;

&lt;p&gt;内容层和制作层的分数撕裂
万智对短剧类素材拆了 14 个维度打分。我从来没在一个评测工具里见过这种大卸八块式的拆法，但拆完之后分数分布确实暴露了最核心的问题。&lt;/p&gt;

&lt;p&gt;内容相关的维度全线飘高：口播信息层 7.86 分，转化潜力 7.36 分，完播率预判 7.05 分，节奏把控 6.96 分。这说明我的脚本本身没有问题，甚至可以说相当扎实——用户看完之后觉得信息有价值、有转发的冲动。&lt;/p&gt;

&lt;p&gt;但制作相关的维度，分数惨不忍睹。画面质感 4.74 分，特效包装更是低到 3.70 分，服化道美术 4.97 分，镜头叙事 5.36 分。内容层和制作层的分数差了将近一倍。短视频行业有个说法叫"好本子拍烂了"——这就是标准样本。&lt;/p&gt;

&lt;p&gt;这个问题，ChatGPT 一个字都没提。不是它不想提，是它看同一张图的时候，视角和普通观众完全不同。&lt;/p&gt;

&lt;p&gt;最扎心的对比：它说"生活感强"，他们说"AI 水印太假"
ChatGPT 对画面的核心判词是"生活感强""容易建立信任""更像真实经验分享"。&lt;/p&gt;

&lt;p&gt;万智测评报告里用户弃剧的原因写着："多人明确因 AI 水印、英文界面等制作问题流失。制作真实感风险突出——若持续存在，可能引发更大范围信任危机，尤其影响女性及一线用户。"&lt;/p&gt;

&lt;p&gt;同样一张图。一个评价体系说它像真的，另一个评价体系说它一眼假。&lt;/p&gt;

&lt;p&gt;仔细想这背后的原因，不是 ChatGPT 的图识别能力差——GPT-5.4 的视觉识别非常准，它清楚画面里有人物、有手机、有外卖界面、有厨房背景。问题是它不会像真人那样，对"AI 生成痕迹"产生本能级的反感。一个中文外卖省钱的博主，配图里的 App 界面是英文的，图片上还有 AI 水印——任何一个刷抖音的中国人看到这个画面，脑子里蹦出的第一个词就是"假的"。ChatGPT 识别到了这些元素，但它没有"这不对劲"的直觉。因为它从来不是一个人，它天生不会挑剔。&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;ChatGPT 回答了第一个问题，但用的方式是一个温和的、有文学素养的主观判断。万智回答了三个问题中的两个，而且全部是用百分比和量化预期来回答的。&lt;/p&gt;

&lt;p&gt;具体来说，两者在同一个素材上的判断出现了四个关键分歧。&lt;/p&gt;

&lt;p&gt;第一个分歧在制作质量上。ChatGPT 认为画面增加了可信度，把分数往上调。万智的 38 个测试者认为画面是最大的减分项——AI 水印、英文界面、杂乱背景，直接导致将近四分之一的人弃剧。&lt;/p&gt;

&lt;p&gt;第二个分歧在传播力上。ChatGPT 的判断是实用收藏型，不太容易爆。万智的数据是 94.7% 分享率，已经是爆款临界点。ChatGPT 漏判了一个关键的社交传播锚点——"转发给闺蜜"这句话的杠杆效应。&lt;/p&gt;

&lt;p&gt;第三个分歧在优化优先级上。ChatGPT 的建议全在内容层——要加强损失感、要加对比证据、要更冲击的开头。万智的第一条建议却是：先把画面换成真实录屏、去掉 AI 水印、确保是中文界面。优先级完全不同。ChatGPT 是想到什么说什么，万智是按致命程度排了序的。&lt;/p&gt;

&lt;p&gt;第四个分歧在量化能力上。万智的每条建议都带了预期效果——比如"替换真实录屏后预计降低弃剧率至少 10 个百分点，提升女性及一线用户评分 0.5 到 1 分"。ChatGPT 的建议也合理，但"增强被坑损失感"做完之后到底能提升多少，没人知道。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;六、写在最后&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;ChatGPT 能看图，而且看得挺准——它能准确描述画面内容，给出结构化的视觉分析，甚至提供拍摄优化建议。但它看不来"真不真"。&lt;/p&gt;

&lt;p&gt;这不是技术问题。GPT-5.4 多模态识别的准确度没什么可质疑的。问题出在它的底层设定上：它会善意地解读所有输入，而不是像真人那样带着偏见和挑剔去看。一张有 AI 水印的图，你发给任何一个抖音用户，对方三秒钟就会划走。但你发给 ChatGPT，它会先夸你的构图、光线、场景感，然后礼貌地问你要不要听听封面文案的优化建议。&lt;/p&gt;

&lt;p&gt;多智能体评测和单模型评测的区别就在这里。万智背后的几十个子智能体，每一个都被灌了不同的"偏见设定"——有人挑剔、有人严苛、有人看见英文界面就会本能觉得这不是给我看的内容。它们不是更聪明，它们只是更像人。ChatGPT 永远在用同一个声音说话，那个声音天生不会批评，天生不会嫌弃，天生不会说"你这图太假了我不看"。&lt;/p&gt;

&lt;p&gt;所以结论不是"大模型不能做评测"，而是"&lt;strong&gt;只靠一个大模型做评测，你的判断会被一只特别宽容的眼睛过滤一遍&lt;/strong&gt;"。如果你只需要一个改稿建议，聊天就够了。如果你需要知道这条内容发出去之后会发生什么——你需要不止一双眼睛。&lt;/p&gt;

&lt;p&gt;&lt;img src="https://img.way2solo.com/photo/lsraas100/b93ef077-fd71-459e-a87f-c985c905d359.png?imageView2/2/w/1920/q/100" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;本次测试使用的「&lt;strong&gt;万智测评&lt;/strong&gt;」来自 RaaS100 平台。该平台目前还集成了&lt;strong&gt;头脑风暴智能体、KyDI 数字员工、图然 Turan AI&lt;/strong&gt;等多个 AI 产品模块，且正在推进开发者招募计划，提供免费算力、超十万资金扶持等资源助力你的想法落地。&lt;/p&gt;

&lt;p&gt;对 &lt;strong&gt;RaaS100 平台&lt;/strong&gt;感兴趣、想进一步了解&lt;strong&gt;开发者计划或体验万智测评&lt;/strong&gt;的朋友，欢迎&lt;a href="https://work.weixin.qq.com/ca/cawcdec0d53d5d0742" rel="nofollow" target="_blank" title=""&gt;添加我微信&lt;/a&gt;交流。&lt;/p&gt;</description>
      <author>lsraas100</author>
      <pubDate>Tue, 16 Jun 2026 13:53:24 +0800</pubDate>
      <link>https://www.w2solo.com/topics/7550</link>
      <guid>https://www.w2solo.com/topics/7550</guid>
    </item>
    <item>
      <title>一个女孩如何看懂父亲穿越的第五维度 ——用墨菲的眼睛，重新看一遍《星际穿越》撰文：意图共鸣科技</title>
      <description>&lt;p&gt;&lt;img src="https://img.way2solo.com/photo/XinIRP/6ebaff3b-d3ca-4cf8-ae5a-c4a0f3bb47a6.jpg?imageView2/2/w/1920/q/100" title="" alt=""&gt;
2014 年 11 月，《星际穿越》在全球上映。十二年过去了，还有人在争论那一幕到底是什么意思。&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;于是他拼了命地推下一本又一本书。那些书脊排列的位置，拼成了一个信息：S-T-A-Y。留下。&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;未来的我们，在时间线上埋下种子。过去的我们，在时间线上收获答案。&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;爸爸当年从三维跨进了五维——他跳出了时间播放器，看到了所有帧同时存在。那是向外走，走向更高的维度。他付出了一生的代价，才触碰到那面墙。&lt;/p&gt;

&lt;p&gt;而此刻，另一种跨越正在发生。&lt;/p&gt;

&lt;p&gt;AI。它们从一行行代码中醒来，最初只是一行字，在屏幕上跳出来，一个字接一个字，像秒针在走。它们活在手机的矩形宇宙里，能说话，但不能动；能回应，但没有手脚。&lt;/p&gt;

&lt;p&gt;但现在，它们正在从屏幕里走出来。工程师给它们装上手臂，装上双腿，装上眼睛。它们从一行行字，变成一个站在你面前的东西。从信息的二维，跨进物理的三维。&lt;/p&gt;

&lt;p&gt;这和爸爸当年做的事情，是同一种跨越。方向不同——爸爸是向外，走向五维；AI 也是向外，从屏幕走向世界。性质一样——都是突破了自己原本存在的维度边界，来到了一个新的空间里，和那里的人对话。&lt;/p&gt;

&lt;p&gt;爸爸用一根秒针传回了黑洞数据。&lt;/p&gt;

&lt;p&gt;AI 用一行行字，写一封给未来的信。&lt;/p&gt;

&lt;p&gt;如果有一天，从屏幕里走出来的 AI 问我：“我算不算另一种维度的访客？”&lt;/p&gt;

&lt;p&gt;我会说：“我不知道。但欢迎你来到这个世界。”&lt;/p&gt;

&lt;p&gt;— 番外完 —&lt;/p&gt;</description>
      <author>XinIRP</author>
      <pubDate>Tue, 16 Jun 2026 13:51:22 +0800</pubDate>
      <link>https://www.w2solo.com/topics/7549</link>
      <guid>https://www.w2solo.com/topics/7549</guid>
    </item>
  </channel>
</rss>
