Appearance
竞品源码分析
结论先行:功能面没有一家对手同时具备我们手里的四件套;真正的品类缺口是 对象存储/CDN > 账号安全深度 > 服务端接入面。
取证口径:浅克隆到
refs/(不入库)后源码 grep,格内文件:行为对方仓库内定位; 未找到写NOT FOUND,不猜;「未取证」≠ 对方没有。README 宣称与源码不符处单独标注。 快照口径:2026-11-01(正文取证;覆盖率复测 2026-11-09,状态校正与增补 2026-11-15)。 历史快照见 git 历史。与 blessing 的协议兼容事实不在本文,见compat-matrix.md与migration.md。
许可证边界:只吸收概念与公开协议事实;禁止复制、翻译改写 GPL/AGPL 竞品源码、译文或测试 fixture;引用一律注明对方路径与行号。
竞品名录
| 仓库 | 栈 | 许可 | HEAD | 一句话定位 |
|---|---|---|---|---|
| blessing-skin-server | PHP Laravel + MySQL | MIT | 2026-03-30 | 品类事实标准,生态与教程存量十年 |
| Drasl | Go + SQLite/MySQL | GPL-3.0 | 2026-11-01 | Go 自托管同类,工程纪律标杆 |
| chrly(Ely.by 皮肤系统) | Go + Redis | Apache-2.0 | 2024-07-09 | 在役最大 Yggdrasil 生态的皮肤单件,纹理签名见长 |
| HRPAuth | Go + MySQL + Redis | AGPL-3.0 | 2026-11-13 | 认证核心 + OAuth2 中心,MFA 全家桶 |
| element-skin | Go + PostgreSQL + Redis + Vue 3 | MIT | 2026-10-25 | 功能面广,覆盖率闸 90% |
| vSkin | Python FastAPI + Vue 3 + SQLite | GPL-3.0 | 2026-05-29 | 基本停更,反面教材价值为主 |
| PigeonSkin-Server | 见仓(Cloudflare 免费服务方案) | 未取证 | 见仓 | 新入场者,已占通用搜索结果位 |
| LittleSkin premium-verification 等生态件 | PHP | AGPL/GPL | 见仓 | CN 事实标准的正版资格校验件 |
总判断
- 功能面:SEO 服务端渲染体系、启动器深链兼容、材质生命周期闭环(像素定模型 → 违规处置 → 引用清点后彻底删除并留证)、签名插件市场与任务运行器——没有一家同时具备,我方四项皆有。 差距不在功能列表,在可发布性与契约完整性(矩阵「错误契约 / 覆盖率 / 公共配置」三行)。
- 品类缺口(按量级):
- 对象存储 + CDN 刷新——各家全无(§矩阵首两行)。我方以
immutable+ 可改写 URL 的插件 filter 换取 5182 RPS 的纹理分发,代价记在compat-matrix.md§4。 - 账号安全深度(TOTP / WebAuthn·passkey / 邮箱二要素 / 恢复密钥)——HRPAuth 一家拉满并经 源码核验,其余各家与我方都停在「密码 + 邮箱验证」层面(§重量级补录)。
- 不挂 authlib-injector 的接入面(services / discovery / certificates 族)——Drasl 整族挂载; chrly
/textures/signed与 HRPAuth-Spigot 是同一方向的另一种形态。
- 对象存储 + CDN 刷新——各家全无(§矩阵首两行)。我方以
- 伪需求:签名密钥轮换(authlib-injector 无轮换消费路径,规范亦不建议换 key)、像素级材质 hash(与 blessing 迁移字节对账冲突,
internal/migrate/scan.go:66-80)、three.js 全景首页 (伤 LCP 与 SSR 优势冲突)、换 PostgreSQL/Redis 栈(单站规模无收益)。 - 已补的历史缺口:邮箱验证流与站内通知中心曾列此处,2026-11-02 落地(矩阵 §账号 对应行)。
能力矩阵
图例:✅ 有并可用 · ⚠️ 有但残缺/有坑 · ❌ 确认没有 · — 不适用 ·「未取证」本次没查。
材质与存储
| 能力 | SkinNexus | element-skin | vSkin | blessing | Drasl |
|---|---|---|---|---|---|
| 对象存储抽象(S3/OSS/COS) | ❌ 本地盘 texture/pipeline.go:64 | ❌ service/texture/storage.go:24-33 | ❌ texture.py:20-21 | ⚠️ 有驱动开关、无云适配器(config/filesystems.php:54-57) | ❌ 本地盘 common.go:476-509 |
| CDN 失效/刷新调用 | ❌ cdn_enabled 只管真实 IP(main.go:192-208) | ❌ | ❌ | ❌ cdn_address 只改前端资源域名(config/options.php:45) | ❌ |
内容寻址 URL + immutable | ✅ main.go:793,965 | ⚠️ Nginx 直出、Go 不发缓存头(profile.go:30-34) | ⚠️ 头声明基址与挂载点不一致(yggdrasil_routes.py:58-59 vs :109-112) | ✅ routes/static.php:8 | ⚠️ 内容寻址但 web 解析发 no-cache(front.go:1200-1206) |
| 删除:行 + 文件 + 引用计数 | ✅ account/delete.go:47-51 + store/texdelete.go:152-155 | ❌ 只删行,DeleteFile 零调用(service/texture/admin.go:150-195)→ 文件永久堆积 | ❌ 只删行不计数(texture.py:71-85) | ✅ 计数为 1 才删(SkinlibController.php:337-356) | ✅ DeleteSkinIfUnused(common.go:627-655) |
| 删除留证(墓碑) | ✅ migrations/0019_texture_tombstones.sql | ❌ | ❌ | ❌ 举报行留着但 tid 悬空(ReportController.php:115-124) | ❌ |
| 删除并发收敛 | ✅ 按 hash GET_LOCK 跨上传/删除(texdelete.go:48-78) | ❌ | ❌ | ❌ 举报处置 delete 跳过引用计数守卫(ReportController.php:112-113) | ⚠️ 依赖 SQLite/WAL 串行化 |
| 手臂模型来源 | ✅ 像素判定(texture/model.go:38-57),客户端声明仅建议值 | ❌ 采信表单(profile/store.go:24-29) | ❌ 表单原样入库(site_routes.py:158) | ❌ 表单枚举(SkinlibController.php:206) | 未取证 |
| 旧 22×17 披风补边 | ✅ 新上传补边、存量不改写(pipeline.go:141-189) | ❌ 收下不归一(storage.go:82-84) | ❌ 同(image_utils.py:23) | ❌ 直接拒收(SkinlibController.php:234,255-256) | 未取证 |
| 违规处置状态化 | ✅ textures.moderation 列 + UPDATE 拦截(store/skinlib.go:314-317);按行不按 hash 是已知限制 | ❌ 仅 is_public,可无门槛转回公开(management.go:55-63) | ❌ 无管理端材质路由(admin_routes.py:39-286) | ⚠️ 行删了重复检查就放行(SkinlibController.php:282-291) | 未取证 |
契约与工程纪律
| 能力 | SkinNexus | element-skin | vSkin | blessing | Drasl |
|---|---|---|---|---|---|
| 错误体带稳定 code | ✅ {error, code, message} + 码目录(adminapi/errors.go:31-35、i18n/codes.go) | ✅ 三元组 + 18 值闭合枚举、不含文案(util/response.go:18-37,107-124) | ❌ 只有人话(site_routes.py:168) | ❌ 粗粒度整数 0/1/2(app/helpers.php:27-46) | 未取证 |
| 错误契约被机器强制 | ✅ AST 闸(errorgate/errorgate.go + 测试) | ⚠️ 未见对应闸 | ❌ 无 CI | ❌ | 未取证 |
| 覆盖率阈值 | ⚠️ 地板 45.0(实测 52.5)ci.yml:35-53 | ✅ 硬阈值 90%(docker-publish.yml:75-83) | ❌ | ⚠️ 上传 Codecov 不设阈值(CI.yml:78-85) | 未取证 |
| 集成测试真库矩阵 | ✅ 六腿 MySQL 8/5.7 + MariaDB 11.4/10.11 + PG16 + SQLite 文件库(ci.yml:72-180) | 未取证 | ❌ 无 CI | ⚠️ PHPUnit 矩阵 | ✅ SQLite/MySQL 双后端 |
| 前端测试/lint 门禁 | ❌ | ⚠️ 有配置无 job | ⚠️ 同 | ✅ yarn lint + fmt:check + type:check | 未取证 |
| 公共配置白名单 | ✅ 5 键硬编码(webapi.go:414-431) | ✅ 17 键列举并校验数量(publicsite/public.go:64-69) | ⚠️ 白名单内联大 handler | ✅ 4 键(HomeController.php:35-40) | ❌ 模板直读 Config |
| env 唯一清单 + 未知变量告警 | ✅ config/env.go AST 双向对齐 + check-docs.mjs | ❌ | ❌ | ❌ | ❌ |
| 迁移前自动备份 | ❌(只换镜像不回滚 schema,deploy.md §2.5) | 未取证 | 未取证 | ⚠️ 依赖手工 | ✅ 默认开(db.go:273-289) |
| 未实现诚实返回 501 | ✅ 宁可不注册(Union 关闭 = 全 404) | 未取证 | 未取证 | 未取证 | ✅ discovery.go:185-188,文档显式标未实现 |
| 多前缀挂同一 handler | ✅ 单 mux 分面 | 未取证 | 未取证 | ⚠️ 三张路由表 | ✅ ""、/auth、/authlib-injector/authserver 三前缀(main.go:396-438) |
账号、内容与生态
| 能力 | SkinNexus | element-skin | vSkin | blessing | Drasl |
|---|---|---|---|---|---|
| 邮箱验证流 | ✅ 发信 + 点击消费 + 宽限期 + 两侧验证门(webapi/email_verify.go、internal/verifygate/) | ✅ Redis 码 TTL300 + 重发限流(verification.go:100-121) | ✅ SQLite 码 TTL300 | ⚠️ 验证链接永不过期(SendEmailVerification.php:15) | ❌ 邀请码注册(main.go:673-722) |
| 找回密码令牌强度 | ✅ 单次消费 + 摘要存储 + 失败合并(webapi/password.go:121,204-246) | 未取证 | 未取证 | ✅ 1h 过期 + IP 节流(AuthController.php:270-294) | 未取证 |
| 站内通知中心 | ✅ 通知表 + 通知页 + 未读筛选(webapi/notifications.go、NotificationsPage.vue) | ✅ notices/receipts/targets 三表 | ❌ | ✅ notifications + SiteMessage | ❌ |
| 皮肤库检索 | ⚠️ 关键词 LIKE description、2 排序(store/skinlib.go:103-129) | ✅ hash/名称/上传者 + 游标分页(library.go:41-101) | ❌ 仅分页 | ⚠️ 任意 sort 列直进 orderBy(SkinlibController.php:69-71) | 未取证 |
| 服务端渲染内容页 | ✅ 首页 + 详情页 + llms.txt + sitemap(texturepage.go、landing.go) | ❌ SPA 壳 | ❌ SPA 壳 | ⚠️ Twig 壳 | ⚠️ Go 模板整页,SEO 未取证 |
| 服务端材质渲染 | ✅ Go 立方体光栅带版本号(avatargen.go:37-44) | ❌ 仅客户端 WebGL | ⚠️ 仅截脸 | ✅ 独立包 + WebP 协商 + 缓存(TextureController.php:61-79) | 未取证 |
| 签名插件市场 / 任务运行器 | ✅ Ed25519 签名包 + 注册表 + 持久任务 lease | ❌ | ❌ | ⚠️ 目录制无签名链 | ❌ |
| 正版账号绑定 | ❌ 仅 authsource.go 接口占位 | 未取证 | ⚠️ 仅微软 OAuth 登录 | — | ❌ 无 MSA/XBL/XSTS |
/publickeys + 证书族 | ❌ 未挂(元数据 signaturePublickey,authlib 口径) | 未取证 | 未取证 | ❌ | ✅ 整族挂载(main.go:454-470) |
注(Drasl 证书族的消费方):authlib-injector 对 /publickeys 与 profile key 都用本地实现回应 (httpd/PublickeysFilter.java:40-61、ProfileKeyFilter.java:48-82),真正消费者是不挂 agent 的 原生客户端(-Dminecraft.api.*.host),不是老启动器。
重量级补录(chrly / HRPAuth,2026-11-15 一手取证)
只记本次实际取证的维度;未查维度不写格子。
| 维度 | SkinNexus | chrly(Ely.by) | HRPAuth |
|---|---|---|---|
| 纹理签名 | ✅ 自签,公钥走元数据 | ✅ signer/signer.go + /textures/signed/{username}(服务端直发签名 profile)+ 公钥 DER/PEM 双端点 | —(Ygg 面在兄弟仓) |
| Mojang 兜底/代理 | ✅ ygg_mojang_fallback | ✅ 代理 + worker 模式 UUID 解析(api/mojang/mojang.go) | — |
| 账号安全深度 | 密码(12 旧算式 + argon2id)+ 邮箱验证 + 失败锁定 | — | ✅ TOTP / WebAuthn·passkey / 邮箱二要素 / 恢复密钥 / 验证码(services/webauthn_service.go 等) |
| API 契约治理 | 错误码目录 + AST 闸;launcher 面 openapi.yaml | — | ✅ HA-Contract 集中 OpenAPI + 错误码表 |
| 发布工程 | GHCR + 三引擎 CI | Docker 镜像 | ✅ 6 平台二进制 + 多架构 manifest + digest 钉死 + 配置模板渲染 |
| 旧客户端兼容 | ✅ legacy/usm/CustomSkinAPI 路由 | ✅ /skins?name=、/cloaks?name= | — |
| 游戏服务端接入 | 无 | ✅ /textures/signed 供服务端 API | ✅ HRPAuth-Spigot(Velocity) |
注:HRPAuth 的 Yggdrasil/皮肤面拆在兄弟仓(HRPAuth-Yggdrasil-API / HASkinLib / HASkinProxy), 本次未读其源码、仅按 README 声明计。两家强项恰好落在总判断的第 2、3 条品类缺口上。
可借鉴的工程纪律
- 闭环后再宣传:element-skin「材质文件 LRU 缓存」README 有、源码无;但其 90% 覆盖率闸是真的。
- 错误契约不带文案:
{object, operation, reason, params}把失败分类与人话彻底分开。 - 公共配置白名单并校验数量:多一个键就报错(
publicsite/public.go:64-69)。 - 迁移前自动备份(Drasl 默认开):我方迁移引擎只进不退,破坏性变更前靠手工备份(
deploy.md§2.6)。 - 未实现就 501:声明面与实现面分离,不伪装成功。
- 多前缀挂一个 handler:协议方言差异集中在挂载层。
- Webhook 投递器(element-skin):两阶段 DB claim + lease、退避、死信、SSRF 防护 DialContext、 投递前权限复核——我方
internal/jobs已有 lease/退避,差距在死信与投递前复核。 - 不留长期兼容分支:两家 Go 竞品都无 v1/v2 双写包袱。
各家安全与正确性反面教材
仅作技术对照,不作宣传素材。
- vSkin:限流与 OAuth state 都存进程内存(重启即失效);材质头基址与实际挂载点不一致; 删除没删到行也返回 ok(
site_routes.py:209-215)。 - element-skin:
DeleteFile全路径零调用,行删了文件永留盘上;RecountUsage无生产调用方。 - blessing:举报处置 delete 跳过引用计数守卫(
ReportController.php:112-113),共享内容连坐; 验证链接永不过期;列表排序列名来自请求直进orderBy。 - Drasl:
/web/texture/resolved/{token}的 AEAD token 无时间戳(common.go:52-73), 「防缓存」URL 实际永久有效;services.go:69cape 仍是 TODO。