MoreRSS

site iconpaul | 保罗修改

一个巨蟹座,会做点小前端、小设计的萌新码农
请复制 RSS 到你的阅读器,或快速订阅到 :

Inoreader Feedly Follow Feedbin Local Reader

paul | 保罗的 RSS 预览

奇趣影棚项目 Vibe Coding 第一天问题记录

2026-06-18 16:46:00

继续失业在家无所事事中,打算找点 Vibe Coding 能落地的实际业务,巩固一下后端和运维相关知识。看到之前认识的一摄影师哥们被公司集体裁员后,目前选择自己创业开影棚,于是在想能不能做一个这样的系统结合实际,于是便有了此项目。

前几天冲凉想到之后初步规划过,写了一份约 2500 多字的文档,随后让 AI 输出了一份完整的「产品设计文档」和对应项目的「技术说明文档」,我再核实确认修改了细节。本文仅作过程和踩坑记录,不包含具体 Prompt 和对应的业务细节。

项目背景

制作一个影棚管理系统,主要面向于摄影工作室场景开发,包含官网(作品展示、预约影棚等)、管理后台(管理员维护)、后端三个项目。

简要记录

今天的 Vibe Coding 已经完成制作了官网、服务端、后台三端的核心代码,期间也确实遇到不少问题,这里简单记录一下。模型主要是采用 GPT-5.5,部分流程自动降为 Cursor 的 Composer 2.5。

官网部分

官网方面,我首先是另外找 AI 对话探讨得到一套靠谱的设计,最终得到结果如下,其实从生成结果来看,整体还不错,就是顶上那个菜单风格真的是有点太熟悉了,哈哈。

整体风格:

- 高级感
- 沉浸式
- 年轻化
- 二次元审美
- 作品驱动
- 内容社区风格

避免:

- 企业官网风格
- 传统婚纱摄影网站
- 影楼宣传页
- 粉色二次元模板风
- 动漫论坛风格

参考风格:

- Pinterest
- Instagram
- Behance
- ArtStation
- 小红书
- Linear
- Raycast

视觉关键词:

- Dark Mode First
- Editorial Design
- Cinematic
- Immersive
- Premium
- Modern
- Elegant
- Gallery Focused
- Large Typography
- Minimal UI
- Rich Photography Showcase

次元影棚官网

后端部分

后端方面,问题主要是 Prisma 的接入相关的坑比较大,还引出了一个 ESM 和 TypeScript 打包配置的问题。

最开始遇到的问题是 PrismaClient 实例失效,直接报错找不到,排查后发现是 schema.prisma 的配置问题,需要指定 output 路径到当前项目下,它默认存的位置有问题,导致 TS 识别不到。这里基本对着 官方文档 接就能解决,AI 自己写的有问题。我自己写的是如下,只要放在当前项目内就行。

generator client {
  provider = "prisma-client"
  output = "./generated"
}

Prisma 接入方式从 7x 版本后发生了变化,包括 seed.ts 文件,需要手动传递对应的适配器包过去才行,AI 这里就搞晕了,用了旧版本的写法,我的解决方式是直接把最新文档的链接甩过去让它修改,问题真的解决了吗... 并没有!

import "dotenv/config";
import { Pool } from "pg";
import { PrismaPg } from "@prisma/adapter-pg";
// 这里必须写 .js 不然找不到文件
import { PrismaClient } from "./generated/client.js";
import * as bcrypt from "bcrypt";

const connectionString = process.env.DATABASE_URL;
if (!connectionString) {
  throw new Error("DATABASE_URL is not set");
}

const pool = new Pool({ connectionString });
const adapter = new PrismaPg(pool);
const prisma = new PrismaClient({ adapter });

此时报错提示找不到模块(错误信息被删了我也找不到了)

这种 CommonJS 和 ESM 的打包问题,其实我自己也理解的不是很透彻,首先第一步 Prisma 要你在 package.json 里面增加 "type": "module" 就能使 NodeJS 认为当前包默认走 ESM 模式运行,因此你得确保构建后的产物也是 ESM 的代码(之前我有一个「小窝后端 Ext」项目是 KoaJS,用 ESBuild 构建成了 ESM 的 JS,就能直接运行,本质上就是 node index.js 这种执行方式),而 NestJS 自己有一套 CLI 工具,本质上是包装了一次 TSC,具体生成啥根据 type。说白了问题就这样解决:

  • ❌ Prisma 默认 ESM、NestJS 默认 CommonJS(项目没写 type=module
  • ✅ Prisma 强制成 CommonJS(配置 moduleFormat=cjs)、NestJS 默认 CommonJS
  • ✅ Prisma 默认 ESM、NestJS 也改成 ESM(项目写 type=module

Prisma 官方文档 的说明,就要求将包设置成 ESM 模式。

In package.json, add the type field set to "module"
{
  "type": "module"
}

我选择了这种方式,于是所有的 import 全部报错,都需要手动加入 .js 后缀才能正常访问。我看别人的项目没这个问题,为什么呢,后面单独列个说明。

NestJS 自己文档 中的描述则截然相反,要求将 Prisma 自己的输出构建为 CommonJS 模式...

Set moduleFormat in the generator to cjs

generator client {
  provider        = "prisma-client"
  output          = "../src/generated/prisma"
  moduleFormat    = "cjs"
}

The moduleFormat configuration is required because Prisma v7 ships as an ES module by default, which does not work with NestJS's CommonJS setup. Setting moduleFormat to cjs forces Prisma to generate a CommonJS module instead of ESM.

这就是集成第三方最坑的问题,双方各有其词,似乎就没有最正确的实践。

以上问题解决后,项目基本就正常跑起来了。

为什么要加 .js 后缀

对比配置后发现是 tsconfig.json 有两个关键配置:

{
  "module": "esnext",
  "moduleResolution": "bundler"
}

在我这个 NestJS 后端项目写的均是 nodenext。这两个到底啥区别,这是 AI 的解释:


第一个:前端打包器专用

module: esnext:编译输出原生 ESM 语法不降级,保留 import/export,不转成 CommonJS;仅给打包器(Vite/Webpack/Rollup)使用,不能直接丢给 Node 运行。
moduleResolution: bundler:TS 模拟打包器解析规则,适配前端工程习惯。

第二个:Node 原生运行专用

module: nodenext:跟随新版 Node 模块规范,自动根据 package.json type.mts/.cts 区分输出 ESM/CJS 产物,产物可直接 node dist/main.js 运行。
moduleResolution: nodenext:1:1 复刻 Node.js 原生模块解析逻辑,严格遵循 Node ESM/CJS 互操作规则。

Nest CLI 编译底层逻辑

nest start = nest build + 运行 dist 主文件;
nest build 底层调用原生 tsc 编译,完全遵循你 tsconfig.json 里的 module/moduleResolution 配置,Nest 不做特殊转译,仅做装饰器元数据收集。


问:也就是说第一种模式是走了一次打包器,需要打包器自己解决引入问题。而我这个是直接用 NestJS 的 CLI 直接构建输出了,它本身并没有打包的功能。是这样吗

1. 先分清楚两个概念:tsc 编译 vs 打包(bundle)

module: esnext, moduleResolution: bundler

不代表自动走打包器,只是让 TS 的类型解析规则模拟打包器(webpack/vite/rollup)

  • TS 本身只是转译 TS → JS,没有打包、没有文件合并
  • bundler 只是一套解析逻辑:允许省略 .js、自动识别 index、兼容前端库的导出字段;
  • 如果你只用 nest build(底层纯 tsc),不会触发任何打包,只会输出一堆零散 dist 文件。

所以你的理解一半对一半错:

正确部分:这套配置是给打包器环境设计的,依赖打包器运行时补齐导入路径、处理模块;
错误部分:TS / Nest CLI 本身不会自动调用打包器,只是类型校验规则对齐打包器。

② Nest CLI 原生能力

默认 nest build = 调用 tsc转译,不打包

  • 输入:零散 .ts 文件
  • 输出:dist 下一一对应的零散 .js 文件,没有代码合并、没有依赖内联;
  • nest start 只是 node dist/main.js 直接跑编译后的零散文件,全程无打包步骤。

Nest 只有一种情况会打包:你手动启用 webpack 构建(nest build --webpack),否则完全无打包逻辑。

2. 为什么 esnext + bundler 配 Nest 直接跑会崩

  1. tsc 输出纯 ESM 代码;
  2. 源码导入全是 import './utils'(不带 .js);
  3. 没有打包器介入补全后缀、解析路径;
  4. Node 原生 ESM 规则强制要求完整后缀,直接运行就报模块找不到;
  5. 且 dist 无 "type":"module" 时,node 会把文件当成 CommonJS,遇到 import 直接报错。

这套配置的设计初衷:

源码交给 vite/webpack 打包,打包器内部会处理路径补全、模块格式兼容,不会直接用 node 执行 tsc 输出的裸文件

3. nodenext 为什么适配 Nest(纯 tsc 编译后直接 node 跑)

moduleResolution: nodenext模拟 Node.js 原生模块解析规则

  • TS 在编译+类型检查阶段就强制你写 import './utils.js'
  • tsc 输出的产物完全符合 Node 运行标准;
  • 不需要打包器中转,nest start 直接 node dist 就能正常加载所有模块。

一句话总结你的疑问

  1. esnext + bundler 这套规则依赖打包器处理导入路径/模块兼容,但 Nest 默认 CLI 只是 tsc 转译、不带打包;
  2. 如果你不用 webpack 打包,只靠原生 nest build,就不能用 bundler 模式,否则编译产物无法直接 Node 运行;
  3. 后端 Nest 裸编译场景,正确配套是 nodenext(或传统 commonjs),而非 bundler。

所以解决办法无非就两个,要么继续走 nodenext 模式直接用(NestJS 官方默认 Starter 模板就是这个),每个文件携带 .js,要么用 bundler 自己配置走个打包器,就免除这个困扰了。

失业后的一次前端面试经历

2026-06-09 23:44:00

这次面试,主要还是对面主动联系为主,应该是看我写了 NextJS 和电商相关经历吧,他们认为和岗位比较匹配,于是得到了这次宝贵的面试经历。面试过程还是挺紧张刺激的,毕竟人家问到的,不一定你平时经常用。

HR 面

HR 面没有具体考察技术,但我看也问了不少涉及技术、协作相关的话题,和一般公司的人事还是有些不同,整体回答我感觉正常水平吧。以下回答都是在对话里面完成的,所以可以先思考再回答,并完整摘录。

HR:你说说你工作的心得吗,之前工作的部门规模,参与其中的角色作用

答:上家公司挺好的 是创业公司自营业务 大家相处起来氛围也很好 我们团队 20 人左右,我参与的项目里面 10 人这样。因为做了四年 我这边参与维护过的东西也多,就以近期的 AI 项目来说我这边主要是参与制作一个无限画布的功能,因为工期有一点紧,代码质量有一些不够好,我就去对这块做各种优化改进,以及最影响使用的一些性能问题吧 主要是涉及到 React 的组件渲染这块

HR:因为公司的全技术都是老板做起来的,他比较喜欢直接开会对接工作,我们没有产品经理,需求大多是一起讨论、简单文档,原型和说明反而要技术这边编写。你之前有没有经历过没有产品、直接对接技术负责人/老板的工作模式?遇到需求比较模糊、不完整的时候,你一般怎么处理?

答:有的 我第一份工作就是这样 当时在旅行社那边技术负责人就是和其他部门做对接 相当于是直接对接具体使用系统的人 不过当时这块的具体流程应该没有这么细 都是他们大佬带头开干 我这边做对接为主

不过结合之前的工作经历 其实我们也并非每个开发任务都有很详细的产品文档,也有不少是口头需求 临时改动 不过都是针对用户反馈和数据去做的调整 这种需求模糊挺多的,一般分两种情况,一个是业务层面的,这就只能拉上级详细确认,另外一种是交互层面的,我会自己简单设计方案让产品看一下,或者他觉得有更好的方案,再决定用哪个更好的方案

HR:今晚 8 点 45 分左右,进行技术面试,你时间上可以么

答:可以

HR:你期待工资多少?

答:(想先了解上班时长)公司正常上班时间是?

HR:10~7点,期间有1个小时休息,

答:双休吗

HR:是的

答:也就是 12-1 点休息这样是吗

HR:不是固定的,因为多数 11 点半左右开始开会到 1 点左右,你最近的工作时间和待遇如何

答:之前工作时间都比较固定 早上也是 10 点这样,中午是休息 2 小时,晚上我一般 6 点半到七点走,看任务完成情况吧

HR:我们每天会有 IT 技术会议,老板亲自带队跟进进度、同步需求、解决问题。需要你按时参会、主动汇报进度、主动反馈卡点。前后端错开开会,例如今天中午前端开,明天后端开

HR:我们团队没有测试,所有功能都是开发自己自测、自己验收、自己保证上线质量。你平时开发完,有没有自测的习惯?一般会检查哪些内容?如果上线后出现自己没测出的问题,你会怎么处理?

答:功能自测肯定是必须的 包括我每次提交代码之前 都会人工确认一次 确保不会把临时改动也带上去
不过也得分情况比如说有些功能需要有多个前置步骤,可能就会跳过一部分主要测改动后的部分,如果时间允许的话肯定需要完整跑几次

上线后出现问题的情况客观来说肯定会存在 不过因为代码合并产生的冲突导致的比较多 比如多个人在改同一个模块的情况下 所以每次合并的时候还是得确认清楚比较好 关于线上问题当时我们的方案是做 sentry 监听线上的报错信息,以及 clarity 跟踪用户的操作

所以还有一种方案就是 尽可能拆出不同的项目 比如说商城是前台,管理业务时另外一个后台项目,这样也能确保不会相互影响

前端这边有几位同事一起是吗

HR:你是指什么?

就是你说的前端开会

HR:目前 IT 团队配置前 2 后 2,大家都会参与

HR:你上一份薪资待遇如何?

答:税前 xx-xx

技术面

技术面考察的知识点非常丰富,基本上简历上能问的都问了一遍,面试官还是很有水平的。简单复盘下来,有些问题我完全答对,有些回答不够直接明了但也答中关键点了,有些比如 Jenkins、Electron 相关的就确实是不太清楚,也着实是没有答上。以下回答部分为印象记忆,可能和实际有出入。

面试官:http 1.1 2 3 的主要区别是什么

答:印象中区别在于连接重用,之前用过 Lighthouse 提示我有些资源走的是 1.1,优化过

面试官:1.1 和 2 呢

答:没回答上

复盘:这里主要区别的确是并发,1.1 和 2 的最大区别,而 3 增加了 QUIC,使用 UDP 协议

AI 的回答挺详细的,这里分享出来记录 传送门

面试官:这里不行不要紧,那这个浏览器内核有哪一些

答:主要三个,Safari、Chrome、IE

面试官:它们的内核是什么,这是浏览器

答:Blink(Chrome),还有一个叫什么来着,Webkit(Safari)

复盘:其实还有一个 Firefox 浏览器,用的是 Gecko,不过其实很少人用,IE 是 Trident

面试官:可以说明下 Restful API 的 Methods 吗

答:主要用的是 GET、POST、PUT、DELETE 这些

面试官:POST、PUT、PATCH 的区别是啥

答:我理解 POST、PUT 都是提交数据,PATCH 是更新

复盘:这里就答错了,POST 是创建,PUT 是全量更新、PATCH 是部分更新,参考 传送门(之前自己小窝的后端也有这个问题,一个更新接口既要支持全量更新也要支持部分更新咋弄了,我倒是用了一套很诡异的实现,默认按照 PATCH 的逻辑形式来,传什么改什么,全量传默认等同于 PUT 了其实

面试官:Tailwind 里面 Container 是干什么用的

答:Container 主要是容器查询,比如根据容器大小去设置子元素的宽高啥的

面试官:Tailwind 里面哪个呢,怎么用

答:(这里我理解成 md:xx sm:xx 这个东西了,他问的是 className 里面写的那个)这里没有很好回答出来

复盘:直接看 文档 吧,就是这样的一个东西

<div class="@container">
  <div class="flex flex-col @md:flex-row">
    <!-- ... -->
  </div>
</div>

我理解其实就是基于 CSS 容器查询的一个实现。最近确实用到了这个东西,不过是在我 Vue 3 哪个主页项目上,用的还是纯 CSS,哈哈。

面试官:Grid 和 Flex 的区别

答:现在我 Grid 用的多一些,Flex 兼容性更好,两者主要区别一个是宫格,宽高固定,一个是宽高自由可以单独指定。

面试官:block、inline-block、inline 区别是啥

答:block 就是最常见的占满一行的,inline-block 我记得是基线对齐那种模式,主要是和 inline 一起,inline 就是常见的文本排版,这个 inline-block 稍微有点不一样是个整体,也参与到那个基线对齐

复盘:这里其实也算答到了一点,如果更具体一点比如说 inline 的 margin 上下无效、padding 不影响行高计算估计更加分,依旧是 AI 传送门

面试官:align-items 和 justify-content 区别是啥

答:一个垂直居中一个横向居中(实际上这个根据 Flex 的对齐方式,会有出入)

面试官:justify-content 的 space-around、space-between、space-evenly 区别是啥

答:三个 div 举例的话,一个是左右有间距、一个是中间有间距,再一个是比较居中(这里其实没讲清楚,但大概意思一样)

复盘:其实这几个都不咋常用的,大多都是宫格形式布局... 直接上 MDN 链接

  • space-between:首个元素放置于起点,末尾元素放置于终点
  • space-around:每个元素周围分配相同的空间

    • 类似于 Float/Flex + Padding + 负数 Margin 实现宫格间距的那种默认实现
    • 现在有 Gap 属性可以解决间距,但是不能解决一行摆几个元素占比多大的问题,这种场景上 Grid 更舒服
  • space-evenly:每个元素之间的间隔相等
面试官:React 的生命周期

答:现在新版 React 已经没有生命周期函数了,都是改成 useEffect 这种 Hooks 了。(他其实想问我组件挂载到结束,React 都干了些啥,但是一开始没表达清楚我当时也没转过来,死磕在 Vue 那种生命周期函数上了,之后回答完全偏了)

复盘:我的理解是这样,首先组件挂载,先执行渲染函数,得到虚拟 DOM 然后加入渲染队列,渲染成实际 DOM,再执行 useEffect,更新组件状态同理,最后卸载组件。还是看 AI 完整解答 吧,它讲的更详细。

面试官:useCallback 和 useMemo 的区别

答:一个是类似 Vue computed 合并计算结果,一个是缓存函数,都需要传 deps(完美回答)

面试官:为什么这段 TypeScript 后面要加一个 as const
const obj = {
  city: {
    name: "ShangHai",
    zip: 200000,
  }
} as const;

答:as const 主要是确定变量类型,假如不写的话就有可能识别成 string 或者联合类型 string | number 这种,就不方便去判断具体类型

复盘:也算是讲到了一点但回答其实不太准确,其实它会把所有值设置成 readonly 表示这个值不会发生任何变化,并且每个对象的类型固定,这里涉及到了“类型拓宽”概念,传送门

面试官:SPA、MPA 区别是啥

答:SPA 就是 React 这种(我理解成 CSR 了),MPA 就是 NextJS 那种(其实这里是 SSR)

复盘:这里基本答错。CSR、SSR、SSG 是一个概念,SPA、MPA 又是另外一个概念,现在的框架基本上都是融合操作,并不能算具体的 SPA 了,MPA 其实就是之前 PHP 那种老的,依旧 AI 传送门

面试官:interface 和 type 什么区别

答:我这边常用可能还是 interface,type 主要是做联合类型,虽说 interface 也可以做(其实说的是 extends,继承和联合还是有区别的)

复盘:其实区别也挺多,不过建议定义对象类型优先使用 interface,其次再去使用 type。传送门

面试官:我看到你提到了 Electron,我要在视窗里面建立独立的悬浮部分怎么做

答:这个确实没做过(上一次搞 Electron 都是 2 年前了,很多 API 也没印象了,彻底挂)

复盘:传送门,其实我也不确定 AI 的回答是不是他想问的。

面试官:Electron 的 contextBridge 是干什么用的

答:我记得是主进程和子进程去通信使用的,一个前台一个后台,需要用这个传数据

复盘:直接看 文档 吧,确实差不多是这样子,但我认为这个回答并不是很好,毕竟真的好久没摸过 Electron 了(看了眼我 Electron Fiddle 是 24 年安装的,之后一直吃灰)近 2 年都在搞 Felo,他们也没有说需要做客户端。

面试官:来一道 IQ 题(印象记得是猜谁才是凶手,只有一个人回答是正确的)

复盘:这题似乎没有标准答案,主要看你怎么分析每个分支下会产生什么结果,第一次遇到这样的面试题,确实给困惑住了,这是 AI 对这道题内容的猜测,传送门 应该差不多是这样子。

面试官:你不是做了个 AI 项目么,你觉得当时最核心的业务目标是什么,是提高用户留存还是付费还是验证产品方向呢(其实一开始也没听清楚)

答:主要是文员这块吧,我们做的是 PPT、网页 什么的,竞争也挺大的

面试官:你觉得哪些功能是正需求哪些是伪需求

答:我觉得网页多一些

复盘:其实这里我也不知道如何回答,毕竟是运营和产品的策略嘛,我们感觉没啥用也得做不是么,仔细想想似乎这种需求挺多的,有一些还直接被其他优先级更高的需求给埋没掉了,印象最深的是要给 PPT 里面的 SVG 做一个编辑器,虽然我引入了一个库尝试了一下但是发现有个不小的 Bug 挺难解决的,就没继续看了

面试官:你推动切换组件库和优化代码这块本质上是解决什么问题

答:组件数据重构是开发的时候不够顺

面试官:你这个是个人项目还是公司项目,公司项目怎么协调这个平衡

答:因为当时是我们大量推 Vibe Coding 嘛,每个人自己做一套,缺乏统一约束嘛,所以就想着给它做稳定。

面试官:你有没有考虑过需求排期问题,怎么解决的

答:有需求肯定是需求为主嘛,这些都是有空去优化的,当然我也希望一开始流程就能写清晰,这样最好

面试官:那你项目约定怎么做好

答:首先第一个就是组件目录结构那些嘛,哪些公用哪些单独的,Vibe Coding 也是一样你得指清楚哪里哪里嘛,就这块比较重要一些

面试官:我看你用过 Jenkins,那你解释一下 WebHook 是干嘛用的

答:主要是触发构建,你肯定要去 Git 平台绑定这个 Hook 才能发这个创建请求嘛

面试官:Jenkins SCM 是什么

答:这个词挺熟悉的,但是没想起来是什么,是以 Jenkins 本地作为驱动的啥(彻底挂)

复盘:SCM = Source Code Management(源码管理),其实是 Jenkins 里面拉项目仓库代码的方式,传送门 虽说这个 AI 的回答我看的也挺懵的,实际使用差不多就是设一下仓库地址,拉到临时的位置,就可以用 Shell 去做构建等操作了。我自己写的一个简单的 CI 工具,直接只有 Shell 功能,直接在服务器构建,其他全没有...

面试官:Nginx 你主要拿去干什么,有弄过 SSL 吗

答:网站托管,反向代理,我现在用 Caddy 多一些,因为它自动处理 SSL,不用我自己搞,之前用面板操作所以用 Nginx,它里面有个签发 SSL 的工具这样。

结果

HR:“中午好,上周的面试结果出来,很抱歉,本次未能入围,谢谢您参与面试。祝您早日找到适合的工作。”

威联通 NAS 使用 CloudFlared 远程访问

2026-05-19 16:29:00

众所周知,威联通 NAS 内置的“内网穿透”服务在国内是有些问题的,myQNAPcloud 实际上是需要一个公网 IP 才能链接到他们家。此前博主使用的是「自建 FRP 服务器」方案来实现反向代理实现穿透,后续 Docker 一直没配置好且无法安装新的镜像,搁置了许久。

其实问题本源在于国内访问 DockerHub 的网络环境并不友好,只需要简单配置下即可。这里博主使用的是几年前购买的 TS212P3 型号(也是当时入门买的最低配的)作为演示。

本文章于 26.04.30 立坑,结果于 26.05.19 花了半天时间才写完...

配置 Docker 源

首先打开 ContainerStation,点击左侧「属性」菜单项,在右侧页面点击切换到「Registry 服务器」Tab。

这里我们使用 https://docker.1panel.live 这个镜像源,确保国内的网络环境也能正常拉取镜像。

添加 Registry 服务器.webp

获取镜像

从 DockerHub 上获取 最新的镜像地址,这里我选择的是 latest-arm64 版本,因为 TS212P3 是 ARM 处理器的,并不支持传统 X86 的。

docker pull cloudflare/cloudflared:latest-arm64

这是正常的获取镜像的命令,但是我们需要替换成对应镜像源获取。点击 ContainerStation 左侧「镜像」菜单项,在右侧页面点击「提取」按钮,Registry 服务器选择我们刚创建的,「镜像文件名称」和「映像版本」分别输入 cloudflare/cloudflaredlatest-arm64,点击「提取」。

从 Registry 服务器提取镜像.webp

此时镜像已经拉取完成,我们前往 CloudFlare 后台开通 Tunnel 服务。

连接 Tunnel

登录到 CloudFlare 后台,点击「保护与连接 -> 联网 -> Tunnels (新)」左侧菜单项,在右侧页面点击「创建隧道」按钮,输入「隧道名称」,点击「创建隧道」。

创建隧道.webp

点击 Docker 选项,下面会出现你的启动指令以及对应 Token,这里直接复制出来。

docker run cloudflare/cloudflared:latest tunnel --no-autoupdate run --token {你的 Token}

回到刚刚获取镜像的 ContainerStation 界面,点击镜像右侧「+」按钮,在出现的弹窗里面修改「进入点」的命令,粘贴前面在 CloudFlare 后台拿到的启动指令,修改前面部分改成 cloudflared

点击「高级设置」,点击「网络」Tab 页,「网络模式」选择「Host」主机模式,最后点击「创建」。

创建 Container.webp

此时应该能创建成功并正常运行,在 CloudFlare 后台也会提示“您的隧道现已可以使用”。

回到 CloudFlare 后台,找到刚创建的 Tunnel 实例,点击「添加路由」按钮,选择「已发布的应用程序」选项。之后指定自己的主域名和二级域名,在「服务 URL」处输入 http://localhost:5000

添加已发布应用程序.webp

此时访问你设置的对应 URL,应该就能正常访问 NAS 后台界面了。

参考

记一次被街头诈骗的经历

2026-03-16 11:03:00

街头借钱诈骗套路复盘.webp

列表的漫展圈年轻人比较多,特意做成了“瓜条”形式希望能被更多人看到。这里也提供了由 AI 改写的大众阅读版本,方便各位博友阅读,大致表达是相似的。

大众版(AI 改写)

受害者:博主
骗子:自称"王艳"的一位阿姨

简要说明:博主热心帮助一位陌生人借钱坐车,却遭遇诈骗。骗子自称是某物流公司的高层员工(生意人),说自己出差后将手机和钱包落在了司机车上,而自己是外地来的孤身一人,丈夫已离异且是限制高消费人员。实际上这些内容全部是编造的,她就是个骗子。

事件经过

3 月 13 日下班后,博主正常休息吃完晚饭,在回公司的路上遇到这位阿姨。她主动过来向博主问路,说自己要去一个很远的地方(深圳宝安机场)。博主正常打开地图给她简单指路,她表示自己没有手机没办法乘车和购票,需要抵达那个位置联系对应的送货司机。博主当时就问她为什么要去这么远的地方,她强调自己是做生意的,在某个物流公司工作(没听清具体名字),在附近谈客户。她还询问博主觉得在附近开一家店的潜力如何,博主表示附近的制造业工厂确实挺多,或许有这个需求(但博主并不是内行人员,她问博主这样的问题博主又该如何回答呢)。

她还会用各种常见话题和博主套近乎,比如问博主年纪多大,是否有谈女朋友。等博主放松警惕后,她开始询问博主的行业、工作地点和薪资,以及是否是本地人、家乡在哪等(博主出于个人保护,都模糊回答了)。她说自己平常在南京发展,如果有机会博主去那边做客,她会请博主吃饭,还强调出门在外多一个朋友能多一份机遇(实际上这是博取信任的话术)。

为了表示自己能还钱,她要加博主微信。那个微信号用的是一个男人的头像,博主当时就表示怀疑,但她说是和丈夫离婚了,这个号现在实际上是她在使用。她还说自己有个儿子做主播,月收入可达到 2W,表示自己是生意人不差这点钱还给博主。

博主用支付宝给她换了现金后,打算给她拍照留存。她当即破防表示博主对她不信任,要求删除照片。博主表示现在骗子太多,这属于正常防范。删除照片后博主本以为事情结束了,但她仍不断用话术影响博主。博主出于好心最终还是借给了她。她当即表示感谢,并附上各种祝愿(祝你工作顺利,生意兴隆?)。

她承诺第二天还钱,但实际并未归还。截止到这篇文章编写时间,微信好友申请依旧处于未通过状态。

事后复盘:骗局中的疑点

现在简单复盘一下,其实她全程的对话充满了各种套路和漏洞。博主此前也遇到过几次街头骗局并成功逃脱,无奈这次对方特别精明,博主着实是大意了。

疑点一:打车方式不合理

从最开始的问路就挺多问题了。她一直强调自己出差公司报销打车的费用,必须要开发票,而打出租车不能提前支付,只能打表后结算费用。这样她就引导博主以现金支付

博主和她去了附近的一家便利店,店长当即表示无法提供这么大面额的现金(实际上店长可能已经在警示博主这个人有问题,让博主好好想想,但当时博主因工作疲惫没想到这个可能性)。

疑点二:询问博主的工作安排

因为博主已经说自己在工作了,她还询问博主明天还要不要上班。这实际上很有可能是担心自己骗了博主之后再次在附近作案会被抓获。因为是小额诈骗,警方一般是不予处理的。

疑点三:不向儿子求助

既然她都有收入这么高的儿子了,怎么不能直接求助他呢?这也是一个巨大的疑问。其实完全可以让博主用手机联系她儿子,然后即时转账,也就不存在借钱这么一回事了(当然她很有可能会用其他理由推辞,比如已经和儿子分家了什么的)。

疑点四:情绪激动后的反向施压

她情绪激动后还在强调自己的着装以及已经加了微信,表示自己是清白的,当即开始反向 PUA 博主,明显表示博主在冤枉普通人。“你把阿姨当作什么人了”“你这是在侮辱我”(实际上这也是一种洗脑话术,恶人先告状,继续利用了博主的善良)。微信号可以是假的,好友是可以不通过的,手机号也可以是空号,总之博主根本没有办法找到她。

总结

人在外地的身份是自己给的,今天她是企业员工,明天就可以是霸道总裁。所有的身份信息都无法核实。

博主的这次经历再次向各位亲朋好友发出警钟:面对陌生人一定要提高警惕,对方可能会疯狂输出话术影响你的判断。如果来不及思考,则最好提出找警察或直接拒绝

瓜条版(原创)

苦主热心帮助陌生人借钱坐车惨遭背刺

苦主:匿了
瓜主:自称“王艳”的一位阿姨

省流版:苦主热心帮助陌生人借钱坐车惨遭背刺,瓜主称自己是某物流公司的高层员工(生意人),出差后将手机和钱包落在了司机车上,而自己是外地过来孤身一人,丈夫离异且是限制高消费人员。实则内容全部编造,骗子一个。

事件经过

苦主在 3 月 13 日下班后正常休息吃晚饭,回公司路上遇到瓜主表示要问路,说自己要去一个很远的地方(深圳宝安机场)。苦主也是正常打开地图简单指路,瓜主表示自己没有手机没办法乘车和购票,需要抵达该位置联系对应的送货司机。苦主当即问为什么要去这么远的地方,瓜主强调自己是做生意的,在某个物流公司(没听清),在附近谈客户,还询问我附近开一家店的潜力如何,苦主表示附近的制造业工厂确实蛮多,或许有这个需求(但苦主并不是内行人员,你问我这样的问题我又要如何回答呢)

瓜主还会以各种常用话题和你套近乎,比如年纪多大,是否有谈女朋友,等你放松警惕后,开始询问你的行业、工作地点和薪资,以及是否本地人,家乡在哪等(苦主出于个人保护,均模糊回答)。瓜主说自己平常在南京发展,如果有机会前往做客,瓜主会请苦主吃饭,强调出门在外多一个朋友能多一份机遇,实际为博取信任的洗脑话术

为了表示自己能还钱加了个微信,该微信号是一个男人的头像,苦主当即表示怀疑,但瓜主说是和自己丈夫离婚了,这个号现在实际上是瓜主在使用。瓜主还说自己有个儿子做主播,月收入可达到 2W,表示自己是生意人不差这点钱还给苦主。苦主为其借钱(ZFB 换现金)后,打算给瓜主拍照留存,瓜主当即破防表示苦主对她不信任要求删除照片,苦主表示骗子太多属于正常防范,删除照片后本以为事情结束,但瓜主后续依旧持续输出洗脑苦主,苦主出于好心最终还是借给了瓜主。对方当即表示感谢,并附上各种祝愿(祝你工作顺利,生意兴隆?)

对方承诺第二天还,但实际未还,截止到瓜条编写时间,微信好友依旧处于未通过状态。

事后复盘

现在简单复盘一下,其实瓜主全程的对话也是充满各种套路和 Bug 疑点的。苦主此前也遇到过几次街头骗局并成功逃脱,无奈此次对方特别精明,苦主着实是大意了。

从最开始的问路就挺多问题了,瓜主一直强调自己出差公司报销打车的费用(必须要开发票,而打出租车不能提前支付,只能打表后结算费用,后续引导苦主以现金支付

苦主和瓜主去了附近的一家便利店,店长当即表示无法提供这么大面额的现金(实际可能已经在警示苦主这个人有问题,好好想想,但当时苦主因工作疲惫没想到这个可能性)

因为苦主已经说自己在工作了,瓜主还询问过苦主明天还要不要上班(实际很有可能是担心自己骗了苦主后再次在附近作案被抓获)因为是小额诈骗,警方一般是不予处理的。

既然你都有收入这么高的儿子了,怎么不能直接求助他呢,这也是一个巨大的疑问,其实完全可以让苦主手机联系对方然后即时转账,也就不存在借钱这么一回事了(当然她很有可能会以其他的理由推辞,比如已经和儿子分家了什么的)

瓜主破防后还在强调自己的着装以及已经加了微信,表示自己是清白的,当即开始反向 PUA 苦主,明显表示苦主在冤枉普通人。“你把阿姨当作什么人了”“你这是在侮辱我”(实际上这也是一种洗脑话术不对么,恶人先告状,继续利用了你的善良)微信号可以是假的,好友是可以不通过的,手机号也可以是空号,总之你根本没有办法找到对方。

总结

人在外的身份是自己给的,今天我是企业员工,明天我就可以是霸道总裁。苦主的此次经历再次向各位亲朋好友发出警钟,面对陌生人一定要提高警惕,对方可能会疯狂输出洗脑影响你的判断,如果来不及思考,则最好提出找警察或直接拒绝

最后让我们来看看这位丑陋的女人,必须得曝光出来:

骗子“王艳”

虚假的微信号

记一次升级 Nuxt 4 的诡异问题

2026-03-07 17:57:00

前几天 我将 Nuxt 从 3 升级到了 4,升级之后才发现有一个页面出现了异常,CSS 样式出现丢失。

出现问题的 CSS 文件是 styles.module.less,它是一个 CSS Modules 形式的 Less 文件。对应组件的引入方式有些特别,是根据文章内容来渲染的。

// 组件映射表
const componentMap: Record<string, Component> = {
  "photo-collection": PhotoCollection,
};

// 渲染组件的函数
const renderComponent = (componentName: string, props: any) => {
  const Component = componentMap[componentName];

  if (Component) {
    return h(Component, props);
  }

  // 如果组件不存在,返回错误提示
  return h("blockquote", {}, `组件 ${componentName} 不存在`);
};

我依次尝试了一些改动,发现都没解决...

  1. 重命名文件(styles.module.less -> photo-collection.module.less
  2. 修改文件内容(.collection 下样式只写一条 background 规则)
  3. 将引入方式调整成 ifelse 的形式

generateScopedName 解决方案

只好求助牛逼的 Claude Opus 4.6,初步排查原因是开发模式和生产模式下生产的 CSS Hash 不一致。


找到根本原因了!

  • 客户端 JS bundle 用的哈希:_collection_f6c82_1
  • 服务端 SSR bundle 用的哈希:_collection_x8r5c_1
  • CSS 文件 里的规则:._collection_f6c82_1(和客户端一致,和服务端不一致)

这就是问题所在:CSS Module 的类名哈希在 SSR 构建和客户端构建之间不一致。

具体表现:

  1. 服务端渲染 HTML 时,生成的 class_collection_x8r5c_1
  2. 浏览器加载的 CSS 文件里只有 ._collection_f6c82_1 的规则
  3. 两者对不上,样式完全不生效

这与 componentMap 的写法无关,所有前面的方案当然也都没用——核心问题是 Vite 在 SSR 和 client 两次构建中,传给 CSS Modules 的 id(文件路径)不一致,导致哈希不同。

修复方案是在 nuxt.config.ts 里显式配置 generateScopedName,让两次构建使用同样的规则生成类名,消除哈希不一致问题:

export default defineNuxtConfig({
  vite: {
    css: {
      modules: {
        generateScopedName(name: string, filename: string) {
          const normalizedPath = filename
            .replace(/\?.*$/, "")
            .replace(/\\/g, "/");
          const relativePath = path.relative(process.cwd(), normalizedPath).replace(/\\/g, "/");
          const hash = crypto
            .createHash("md5")
            .update(relativePath)
            .digest("hex")
            .slice(0, 5);
          return `_${name}_${hash}`;
        },
      },
    },
  },
});

总感觉这是一个很奇怪的修复方式,理论上 Vite 自己的配置都是经过验证的,自己魔改的配置越多,后续的升级只会更容易出现问题,想着能不能从其他方面再继续分析下其他的解决方法。

修改后缀名方案

我把这件事告诉给了 @Innei,先被他 PUA 了一遍

怎么还在用这么古老的技术栈

骂归骂,能有些解决问题的思路也好。他说他现在使用的“不古老”的方式是 vanilla-extract 这个库,看上去是类似 CSS in JS 的那种编写方式,但是实际背后处理返回的是一个 CSS 文件,看上去亮点主要是 TypeScript 友好。

用 Less 有啥优势

我说没有啥优势,只是好过全局 CSS 而已(因为是 CSS Modules 形式),当时没转过来指的是 Less 本身。

选择 Less 主要是主题色盘的色值可以直接用函数生成,而不是自己算好再写上去。并且它有 CSS 嵌套功能,从小窝后台一直用了好几年,也没啥问题就一直在用。

当然现在这些特性其实原生都有支持了,颜色计算有 color-mix,嵌套兼容性还不够好但也能用(Chrome 要求 120 版,iOS Safari 要求 17.2 的系统),继续沿用也只是为了保持兼容性而已。

我尝试直接将文件的后缀从 .less 改成 .css,居然也是解决的。Innei 看到我的惊喜若狂(卧槽,去掉还真好了,什么原理啊),什么话都没说。

为什么,什么原理

其实到这里就已经找到了最合适的方案了,因为简单直接还不需要改配置。我其实也好奇这两个修改方式为什么有效,也让 AI 替我分析了一下:

修改后缀名的原理


Vite 处理 CSS Modules 时,哈希是根据传给 postcss-modules 的 from(即文件路径)计算的。

.module.css(纯 CSS)的处理流程:

style.module.css → postcss-modules(from = 原始文件路径)→ 哈希

路径简单直接,SSR 和 client 两次构建完全一致 → 哈希相同

.module.less(LESS 预处理器)的处理流程:

style.module.less → LESS 编译器 → 中间产物(id 可能附带 ?used / ?inline 等 query)
                                         ↓
                              postcss-modules(from = 中间产物的路径)→ 哈希

Client build 和 SSR build 内部对中间产物的 id 处理方式不同,传给 postcss-modulesfrom 路径不一致 → 哈希不同

这是 Vite 在处理 CSS 预处理器(LESS/SCSS/Stylus)与 CSS Modules 组合时的一个长期存在的 SSR 一致性问题,在 LESS 这里表现得尤为明显。

你用 .css 替换 .less 已经是最干净彻底的修复方式了,不需要任何额外配置。我之前加的 generateScopedName 现在可以去掉,因为它只是用来绕过这个问题的。


这里我单独重命名更新了那个出问题的 .module.less 文件后,其实并没有彻底解决问题。

generateScopedName 的原理

奇怪的是,第一种方法,那怕采用最直接的 文件名 + 路径 生成 Hash,也是正常的,根本不像 AI 说的那样,是文件路径的问题。

export default defineNuxtConfig({
  vite: {
    css: {
      modules: {
        generateScopedName(name: string, filename: string, css: string) {
          const hash = crypto
            .createHash("md5")
            .update(filename)
            .digest("hex")
            .slice(0, 5);
          return `_${name}_${hash}`;
        },
      },
    },
  },
});

我继续追问它,为什么简化了这个处理函数,也能解决 CSS 的异常:


真相:问题不在 filename,在 Vite 内部的 hash 计算

你说得对——generateScopedNamefilename 参数在两次构建里看上去是一样的。这是因为 Vite 在把 filename 传给用户定义的 generateScopedName 之前,已经做了 clean url 处理(去掉 query 参数)。

但 Vite 自己内部不走这个用户函数。它有一套独立的 hash 计算逻辑,用的是原始的 Vite module ID(可能带 query 参数)。

用简图表示:

.module.less(SSR build)  → id = "/path/style.module.less?used"  → hash = x8r5c
.module.less(client build)→ id = "/path/style.module.less"       → hash = f6c82
.module.css(SSR build)   → id = "/path/style.module.css"        → hash = abc12
.module.css(client build)→ id = "/path/style.module.css"        → hash = abc12 ✅

LESS/SCSS 等预处理器文件在 SSR 构建过程中会被打上 ?used 这类内部标记(Vite 用它区分 SSR 上下文里"已使用"的 CSS),导致原始 id 和 client build 不一样。纯 CSS 文件不走预处理器流程,没有这个标记,id 始终一致。

用户定义 generateScopedName 之所以能修复问题,不是因为 replace 做了什么归一化,而是因为只要你定义了这个函数,Vite 就把 hash 计算权交给你,并传入 cleaned filename。你写 filename + name 直接 hash 就够了,两次构建拿到的是同一个 cleaned filename,自然一致。

所以结论是

方案 原因
换成 .module.css 绕开了预处理器流程,Vite 不会附加 ?used 标记
加 generateScopedName 接管了 hash 计算,Vite 给你的是 cleaned filename

你换 .css 是最干净的解法,不引入任何额外配置。如果项目里还有其他用 .module.less/.module.scss 的组件,遇到同样问题时,加一个简单的 generateScopedName 也能解决,不需要任何 replace,直接用 filename + name 哈希就够了。


这个答案我也不确定是否正确,我就将自己的“所见所得”告诉 AI,因为我发现其他的 .module.less 并没有遇到这个问题。其实这里误导了 AI,后面的内容就当看看笑话吧。

因为那些没有问题的 .module.less 其实都是走的 CSR 渲染,用了 <ClientOnly> 组件包裹,一个页面通过路由方式进入的都是 CSR,必须刷新才是 SSR,而我验证其他组件发现没问题,都是因为走了 CSR 模式

这时候 AI 给我的“猜测”就开始有偏差了,说是什么代码分割的问题,还说是因为我这个组件只有一个页面在用,实际我也验证过了和这个根本没关系,一个组件被多个页面使用也是一样的问题(地址栏直接输入对应 URL,走的是 SSR 渲染)


关键差异:代码分割(Code Splitting)
看引用次数:

组件 被引用的页面数 Vite 打包结果
PhotoCollection 1 个页面([[slug]]) → 打进该页面专属的 独立 chunk
Pager 5 个页面 → 被提取到 shared chunk
ThemePanel layout(全局) → 被提取到 shared chunk
Modal、Notice 等 多个页面 → shared chunk

Vite 发现 PhotoCollection 只有一个页面用,就把它和它的 CSS 打进了 [[slug]] 页面专属的 code-split chunk 里。

问题就出在这里:在 Vite 的 SSR 构建中,对于这种"只属于某一个页面"的 code-split chunk,模块 ID 的生成方式和 client build 不一致(SSR build 会附加额外标记来防止 CSS 被注入到所有页面),导致 postcss-modules 拿到不同的 hash 输入,输出不同的类名。

Shared chunk 里的组件因为 ID 在两次构建中是稳定一致的,所以没有这个问题。

PhotoCollection(独立 chunk):
  SSR build:    id = "/path/photo-collection.module.less?used"  → x8r5c ❌
  client build: id = "/path/photo-collection.module.less"       → f6c82
Pager(shared chunk):
  SSR build:    id = "/path/pager.module.less"   → a1b2c ✅
  client build: id = "/path/pager.module.less"   → a1b2c

如果你把 PhotoCollection 改成也在两个以上的页面引用,Vite 就会把它提取进 shared chunk,.module.less 的问题自然也就消失了。 所以这个问题的触发条件非常精确:.module.less + 只被单个页面引用。


这个推理也是错误的,我继续追问它另外一个组件的 .module.less 没问题,其实也是有问题的(因为是 CSR),最后它也没办法了。

总结

如果想要彻底修复这个 Bug,需要将所有的 .module.less 都改成 .module.css 才行。它并不是其中一个文件才会引发的。

要么就是按照上面的方案修改 generateScopedName 配置,看来这个的确是 Vite 内部和 Less 衔接导致的异常,只是我一直没找到具体的证据。

当然也如 @Innei 所言,Less 现在确实被淘汰了,Antd 也早就迁移变成了自己的 CSS in JS 实现,我还是让 AI 整体优化重写,拥抱最新技术吧!

记一次移动端 Safari 调试踩坑

2026-01-22 02:54:00

最近在做公司项目一个 H5 版本的页面优化,合并到测试环境之后,在手机 Safari 下有部分样式问题,需要联机即时调试更快的解决,但在联机过程遇到了不少坑,这里简单记录一下问题和解决方法。

如果想要在移动端真机上调试,就不能使用 localhost 这种地址了,使用局域网 IP 地址又有可能遇到请求后端 API 出现跨域的问题,我的解决方法是选择建立一个与测试、生产环境共享根域的二级域名(当然前提后端也允许了该二级域名的跨域行为),再路由器上设置 Hosts 连接到开发机的方案。

比如 paul.me 是生产环境,local.paul.me 就是本地开发环境,强制指定 IP 到开发机,这样就能保留此前传递和获取登录态的逻辑代码

访问失败问题

起初在另外一台 Mac 电脑上直接访问开发机对应的页面,访问失败,使用 ping IP 则正常,未分析到具体原因,重新连接网络后得到了解决。

重定向问题

我在手机上打开本地调试地址后,发现会被强制使用 HTTPS,可我完全是本地服务,怎么可能会有重定向?通过搜索得知,这里使用到了一个叫 HSTS 的技术,因为访问生产环境的时候就是根域名,可能在根域名下存在 HSTS 记录,导致所有子域都会强制重定向一次。

在 iPhone 的 Safari 上,需要在 Safari 设置里面找到网站数据,将整个域名下的清空(包括登录态等均被删除),之后重新打开浏览器访问本地调试环境,就没遇到 307 跳转了。

联机调试崩溃

我原本想用电脑上的 iOS 模拟器来试试的,但是发现 Safari 的调试工具一直有问题,无论是“元素”还是“控制台”都是空白无内容的,重开多次均无效。后尝试使用 iPhone 13 真机调试,但是首次设置就遇到了登录态设置上去但没用的情况。

最后我的解决方法是直接在控制台里面设置 Cookie,然后去调整 Cookie 对应的域名和 Path 值为 /,后续请求其他页面就没有掉登录态的情况。

但是在我需要调试的关键页面,手机一打开就出现崩溃甚至闪退的情况。经过检查发现本地调试环境该页面的打包代码居然有 100MB+,可能就是这个原因导致的,手机直接不堪重负。

因为该页面的路由完全和 PC 共享,仅仅只是一个 if 条件判断来返回手机版的页面,因此这里有完整打包的 PC 端的代码(屎山),目前我直接注释掉了 PC 端的代码,NextJS 自动给我去除掉了,JS 文件瞬间缩小到了 2MB,调试终于得以正常进行...

样式问题记录

简单来说就是有一个 button 元素,内部有一个 div 设置了 aspect-ratio: 1/1 强制设置比例,结果发现元素塌陷,并没有支撑起来。对于这个问题我发现有个最简单的办法,就是将元素替换成 div,就解决了。挺诡异的,我只知道 button 相较于 div 会有一个额外的 appearence 属性,但我修改它也并没有用。

具体 Demo 可见 Safari 按钮高度问题

这件事说明目前还是不能完全信任 AI 写出来的代码,依旧得自己充分验证才知道有没有问题。