Minecraft 皮肤站与外置登录
Blessing Skin Server 的 TypeScript 全栈重写:TypeScript · Next.js · Hono · Kysely。和 BS 同一套协议,玩家的模组和启动器不用升级,旧站可以一键迁过来。
一条命令,零原生依赖
node nextskin.mjs start 启动整个站点:网页、协议端点、后台任务和插件进程都在里面,不用装 PHP,也不需要 Redis。
图像处理换成纯 TypeScript 的 PNG 管线,数据库驱动用 Node 内置的 node:sqlite。发行包里没有一个原生模块,同一个 ZIP 在 Linux、Windows、macOS 的 x64 和 arm64 上都能直接跑:需要 Node 24 以上,推荐 26。
一键升级,准备好了才停站
发行包能在后台一键升级。点“准备升级”时站点照常运行:下载 ZIP、核对 ed25519 签名和 SHA-256、解压到 releases/<版本>/,再用新版本对着在用的数据库预检。点“升级”以后才停掉旧版本、执行迁移、把 current.json 指向新版本、等它的 /api/health 返回 200;没有迁移时站点只停几秒。
新版本没通过健康检查时自动换回旧版本,SQLite 连同迁移前的备份一起换回;MySQL、PostgreSQL 有迁移时要先确认已经备份。上一个版本留在 releases/ 里,可以回到它。Docker 镜像只提示新版本,换标签升级。
和 Blessing Skin 同一套协议
CustomSkinLoader、UniSkinMod、旧版材质地址和 Yggdrasil(authlib-injector)四种协议,路径、状态码、响应头和响应结构都和 Blessing Skin v6 一致。玩家的模组和启动器不用改配置,也不用升级。
玩家 UUID 创建以后永不改变,从旧站导入时原样保留:游戏服务器按 UUID 存背包和权限,换了就全对不上。
从 BS 迁移:一个事务,全有或全无
node nextskin.mjs import blessing-skin 只读地连上 Blessing Skin v6 的数据库,把用户、角色、材质、衣柜和站点设置在一个事务里整体导入;安装向导里也能直接点。
提交之前,在同一个事务里把每个角色读回来和旧库逐项比对:UUID、皮肤、披风、模型。有一项对不上就整体回滚,不会留下导了一半的站点。
四种数据库,一份代码
默认用 Node 内置的 SQLite,什么都不用装;也支持 MySQL、MariaDB 和 PostgreSQL。每一个数据库测试都在四种上跑。
同一个 upsert 在四种数据库上发出不同的语句:SQLite 和 PostgreSQL 用 ON CONFLICT;MySQL 和 MariaDB 先 UPDATE 拿排他锁、没有行再 INSERT,避开 InnoDB 共享锁升级的死锁。不需要 Redis。
出了问题,先问 doctor
node nextskin.mjs doctor 只读地检查运行环境:Node 版本、配置文件、密钥、数据目录、数据库连接和迁移状态、插件进程能不能带权限模型启动、服务在不在运行。有失败时退出码是 1,适合放进部署脚本。
忘了超级管理员的密码、两步验证丢了、验证码把自己挡在外面,也都有对应的命令。
玩家要的,都有
皮肤库、3D 预览、衣柜、本周收藏最多的领奖台、外置登录(authlib-injector,以及 Yggdrasil Connect 的设备码登录)、两步验证和通行密钥、GitHub / Microsoft / Google / LittleSkin 第三方登录、签到积分。
界面有亮色和暗色两种模式,默认跟随系统。默认配色是钻石青;官方插件“更多配色”另带钴蓝、洋红和高对比三套(高对比还换上直角、粗边框和带阴影的卡片),站长启用以后用户自己挑,每套都有亮暗两种。
接下来,是看不见的部分
上面是服主和玩家看得见的部分。下面几页讲 NextSkin 在底下下的功夫:材质怎么存、上传怎么查、协议怎么守、皮肤图怎么画、插件怎么隔离、后台任务怎么不丢。
材质按像素存,不按文件存
同一张皮肤,换个编辑器保存、换个压缩级别,文件的 sha256 就全变了。NextSkin 的材质 hash 算的是像素:sha256(宽 ‖ 高 ‖ RGBA 像素)。上面九种文件解出来是同一组像素,hash 只有一个。
这个 hash 既是存储键,也是公开材质地址的一部分。换编码器、换 zlib 版本、换 CPU 架构都不会变;已经存下的文件也永远不重新编码。
上传的 PNG,先过安检
上传的 PNG 逐块核对 CRC,只把 IHDR、PLTE、tRNS、IDAT、IEND 交给解码,色彩配置、文字、时间这些附加块一律丢掉。
解码之前先按 IHDR 的宽高算出解压后的精确长度。一个宽高只写 64×64 的文件也能解压出几百 MB:这样的解压炸弹在预算处就被截住,不会把服务器的内存吃光。
连 BS 的怪癖都照样复现,漏洞不复现
协议端点有 228 条契约用例:请求从真实的 Blessing Skin v6 录下响应,NextSkin 回放同样的请求逐项比较。玩家的模组和启动器依赖的怪癖也照样复现,比如角色名末尾多一个空格照样查得到、字符串 "false" 按 PHP 的规则算真。
安全漏洞是例外:用自己的令牌改别人的皮肤、按角色名退出登录却不吊销令牌,这些在 NextSkin 里有意修掉了,用例里写明了和 BS 的差别。
皮肤图是服务端用纯 TS 一个个像素画出来的
皮肤库和衣柜里的人物图不用 WebGL,也不用原生的图形库:packages/skin 把六个部位当成盒子,相机转 30°、俯视 18° 正交投影,再逐个面光栅化,深度缓冲决定谁挡住谁。
明暗是相机空间的固定光源,顶面、正面、侧面三档。三角函数写成字面量,因为各个 JS 引擎的 Math.cos 不保证逐位相同,出图要和黄金图逐像素比对。
鼠标悬浮时人物抬手,用的是 5 帧的拼图。颜色不到 256 种的图存成调色板 PNG,比 RGBA 编码小得多。
插件热更新:整个进程换一代
安装的插件不跑在核心进程里,而是跑在核心拉起的 plugin host 进程里:带 Node 的权限模型启动,环境变量是空的,拿不到数据库凭据,只能通过 RPC 调用核心。
升级插件时,核心拉起新一代 host、加载插件并核对实际的定义和 manifest 一致,再原子地把注册表切过去;旧一代等在途的调用结束后整个进程退出。ESM 模块卸载不掉,那就让整个进程退出。
插件的查询,由核心重建
插件也能用 Kysely 写自己的表,但 plugin host 发给核心的不是 SQL 字符串,而是 Kysely 的节点树。核心按白名单一个节点一个节点地重建,不是事后检查。
重建时表名改写成这个插件独占的物理表名,再用核心自己的编译器编译、在核心的事务里执行。原始 SQL 节点、没有声明过的表(比如 users)一律拒绝。
事件和业务写入,同一个事务提交
注册一个用户时,写用户、加初始积分、记积分流水,再把 user.registered 事件按订阅它的插件展开成任务,全部在同一个事务里。任务表本身就是 outbox:提交了才看得见,回滚就一起消失。
几个实例同时认领同一个任务时,带条件的 UPDATE 只让一个成功。失败的任务按 10 秒起翻倍的间隔重试,最长 1 小时;5 次用完进死信表,后台可以重试。四种数据库同一套做法,不靠 SKIP LOCKED、LISTEN/NOTIFY,也不要 Redis。






