进度台账(截至 2026-10-27)
本页是时点记录,最后更新 2026-10-27。最新进展以各仓库 CHANGELOG 为准。
本次更新同时是一次审计后的修复。措辞按“能跑通的最小可用链路 + 已知差距”如实描述, 不再用笼统的“可用”。距 Blessing Skin / Modrinth 的成熟度差距见文末“已知差距”。
代码量约 11.7k 行(mirror 6.3k Go / hoard 2.6k Go / shared 0.2k / web 2.1k TS / compass 0.5k)。 定性:核心链路已跑通的原型(M1~M3 为主),不是半成品,也远未到成熟产物。 两个后端能 build/vet/test 全绿,web 能 typecheck/lint/build,关键链路有真进程冒烟验证。
| 仓库 | 实际状态 |
|---|---|
mythic-mirror |
Go 后端。Yggdrasil 全套端点 + RSA 签名、皮肤/披风上传校验与分发、crafatar 风格头像/渲染、账号体系(注册/邮箱验证/找回密码/GitHub OAuth/Microsoft 正版绑定/邀请码/验证码)、皮肤库(发布/点赞/评论读写 + 管理删除)、管理后台、OIDC Provider(Fosite,会话落 Postgres oidc_sessions,无 DB 时内存)。存储本地/S3、缓存内存/Redis、邮件 SMTP/Resend/日志可切。集成测试对真 Postgres 全绿。 |
mythic-hoard |
Go 后端。Project→Version→File 三级模型(Postgres/sqlc 或内存)、本地/S3 存储、pg_trgm/Meili 搜索、inline/asynq 队列、下载统计、审核状态、Modrinth 兼容读子集、.mrpack 生成(含 dependencies + mods/ 路径)。用户体系已接通(OIDC SSO、团队、PAT、用户级上传);管理 token 降级为 bootstrap/救援。 |
web |
Next.js 16 App Router。皮肤库列表/详情已改 RSC 服务端渲染(搜索/排序/分页/评论区/应用到我的皮肤),其余页面仍为客户端渲染。landing/注册/登录(OAuth+忘记密码)/邮箱验证与重置落地页/上传皮肤披风(profile)/3D 预览/管理后台/mod 列表+上传。类型由 OpenAPI 契约生成。无前端单测;会话已加固为 httpOnly cookie;管理后台会话登录 + 救援 token;Playwright E2E 进 CI。 |
compass |
Tauri 2。原生启动模块(版本解析、资源下载、Java 探测、参数构造、进程管理、实例)已在 Rust 实现并有测试,Tauri 命令与 UI 尚未接线;Prism 委托与 .mrpack 下载已删除。无 CI、无自动更新。 |
shared |
Go 能力接口 + 零依赖进程内实现:storage/search/cache/ratelimit(Memory 固定窗口)/clientip(可信代理感知的客户端 IP 解析)/queue/mailer/i18n/captcha/cdn/oauth/mc。已删除无人引用的 auth 接口(认证在各服务内直接实现,见 能力分层 §5)。 |
api |
OpenAPI 契约(mirror + hoard + common),已对齐实现(见下)。 |
docs |
计划与架构决策(ADR-0001..0012)。 |
本轮已修(审计发现 → 修复)
Section titled “本轮已修(审计发现 → 修复)”- 邮箱验证 / 找回密码断链:后端邮件链接原指向 API 的
POST /account/verify(浏览器 GET 必失败),且前端无对应路由。现新增前端/{locale}/account/verify与/account/reset-password落地页 + 登录页“忘记密码”入口;后端邮件链接改指MIRROR_WEB_URL(新配置,回退MIRROR_BASE_URL),邮件文案走 i18n。冒烟验证 register→邮件链接→verify(204)→authenticate 全通。 - OpenAPI 契约漂移:spec 与实现严重不符(
/avatarvs/avatars;hoard 9 个幽灵端点;已实现端点未入 spec)。现契约只描述已实现路由:mirror 补 account/skinlib/admin/纹理端点、修/avatars;hoard 删幽灵端点、补/api/mods*与 mrpack。结构性防再犯:两个 Go 服务加TestSpecMatchesRoutes漂移门(负向测试已验证能抓幽灵路由),web 用 openapi-typescript 生成类型(src/lib/api-schema/)、api.ts改为 re-export、CIgen:api && git diff --exit-code兜底。 - 限流形同虚设:原按
RemoteAddr(含端口)建 key,每条连接独立桶 → 等于没限流;且进程内 map 无界(内存泄漏)。现改用shared/ratelimit.Memory(按 IP 去端口的固定窗口 + 惰性 TTL 清扫)。回归测试 + 真进程验证:同源 30 请求 → 前 10 通过、后 20 个 429。 - 客户端 IP 可伪造:原 chi
middleware.RealIP无条件信任X-Forwarded-For/X-Real-IP,公网客户端发个伪造头即可绕过按 IP 限流、污染日志。现由shared/clientip.Resolver统一解析:仅当直连对端落在可信代理网段内才采信转发头;列表头从右向左跳过可信网段(Express trust-proxy 同款算法,抗左侧塞伪造条目);头名称/顺序与可信网段可配(*_CLIENT_IP_HEADERS/*_TRUSTED_PROXIES/*_CLIENT_IP_TRUST_NONE)。默认头序X-Forwarded-For, X-Real-IP, CF-Connecting-IP, True-Client-IP,默认可信网段 = 环回+私网+链路本地(compose 开箱即用;公网直连客户端无法伪造)。CDN/云 LB 场景必须把边缘网段(如 Cloudflare IP 段)配进 TrustedProxies,否则解析到的是边缘 IP。对照实验验证:可信代理下按 XFF 分桶;对端不可信时伪造 XFF 被忽略。 .mrpack不合规:缺dependencies、文件路径非mods/。现按 loader 生成 dependencies、mod 文件加mods/前缀。冒烟验证 index 合法。- 安全基线:两个服务加安全响应头(
X-Content-Type-Options/X-Frame-Options/Referrer-Policy/CSP,TLS 或X-Forwarded-Proto=https时加 HSTS)+http.ServerReadHeaderTimeout/IdleTimeout(防 Slowloris)。 - DB 层 CI 覆盖:CI 加 Postgres service + 测试自动跑迁移,pgstore 集成测试不再永久 skip(本地对真 PG17 全绿)。
- 死代码:删
shared/auth;shared/ratelimit从死代码变为被两服务引用。 - 仓库卫生:web/compass 补 AGPL-3.0 LICENSE;把此前大量未提交的工作(mirror 18 / hoard 21 / web 16 / compass 8 文件)落入基线提交;README(mirror/hoard/web/compass)、能力分层 §5、ADR-0009 修订、docs/deploy 同步为与现实一致。
- 皮肤站:
web(modules=["skin"]) +mythic-mirror - mod 站:
web(modules=["mods"]) +mythic-mirror(身份中心)+mythic-hoard。Hoard 不自建账号体系,经 Mirror OIDC 登录(见下“身份架构决策”);Mirror 是单二进制 + 同一 Postgres,代价可忽略 - 混合站:
web(modules=["skin","mods"]) + 两后端,共享同一 Postgres(mirror 用默认 goose 表,hoard 用hoard_goose_version,不冲突) - 单 API 域名经 Caddy 分流:
/v2/*、/api/mods*→ hoard,其余 → mirror - 邮件链接依赖
MIRROR_WEB_URL/WEB_URL指向前端(compose 已配)
已知差距(距成熟产物,未在本轮处理)
Section titled “已知差距(距成熟产物,未在本轮处理)”后端深度
OIDC token 仍存内存✅ 已落 Postgres(含客户端跨副本感知);✅ 已落 cacheaccount.mojangStates进程内- 皮肤库仍缺:标签体系、2FA、举报、配额
- 邮箱校验
validEmail仍偏宽松;多数请求体未跑 validator - Hoard 无用户/组织/团队/API Token(只有管理员 token)→ 不是 Modrinth 替代品;无版本依赖关系、上传不解析 mod 元数据、无上游镜像代理、搜索无 facets 透传到 UI
- Meili 分支
total_hits仍是近似
前端 / 启动器
- 皮肤库列表/详情为 RSC SSR;mods 列表/详情于 2026-10-24 整改中改为 RSC(见下);profile/登录注册/管理后台为客户端岛(有意为之:登录态控制台);token 已换 httpOnly cookie(0.2.0);管理后台会话登录(token 为救援通道,改存 sessionStorage)
- 前端测试:Playwright E2E 已进 CI(
web/e2e/,skin-flow + mods-sso);仍无组件单测与视觉回归 - compass 无 CI、
launch直接执行 UI 可编辑的程序名、下载全量入内存无进度/校验
2026-10-24 前端整改(web,台账见根目录 FRONTEND-REMEDIATION.md)
Section titled “2026-10-24 前端整改(web,台账见根目录 FRONTEND-REMEDIATION.md)”一次针对“dev 预览廉价感/粗糙”的专项审计与整改,要点:
- 运行时断裂修复:主题系统因
custom-theme.json空值整体失效(换肤/亮色全空操作)→ token 过滤收口;/skins因后端形状漂移白屏 → API 边界形状归一化 + 契约错误码;错误边界保留站点 chrome 并本地化;mirror404 误报 500修复;裸跑 CORS 文档化 - 设计系统落地:CJK 字体策略(Noto Sans SC + 系统回退栈)、shadcn 语义变量别名层、
color-scheme、阴影/动效 token 与prefers-reduced-motion、品牌标识(icon.svg + logo 组件,删脚手架残留)、3D 预览器重写(响应式/DPR/减弱动效/回退)、图标体系与空状态/骨架 - 交互:全站表单语义化(form/autocomplete/label 关联)、提交 pending 防重复、base-ui 确认对话框接入全部破坏性操作、死链接与注册入口、移动端导航、自定义文件选择 + 上传进度、搜索改 URL 提交消除竞态
- 架构:每页
generateMetadata(locale 感知 + OG)、移除全站force-dynamic、/admin/profile路由守卫、site-config 服务端单一来源、主题 FOUC 内联脚本、mods 列表/详情 RSC、cn换 clsx+tailwind-merge - 治理:stylelint 去全文件 disable(原语层现场注解豁免)、eslint-disable 清零(仅余 3 处 no-img-element 现场注解)、Prettier + tailwindcss 插件接入、死代码/死键/依赖清理
同日晚二轮(用户复看反馈,台账见根目录 FRONTEND-REMEDIATION.md §G):locale 前缀改部署级开关(默认 never 同路径 + 非索引部署 noindex + bot UA 403;always 供 SEO 站);滚动条扰动(scrollbar-gutter 经主题通道落地);皮肤库占位几何锁比例;品牌标识换回仓库内真实 logo(light/dark 双版本 + favicon + OG)。
同日晚三轮:列表页加载骨架统一(list-page-skeleton);设置入口仅登录后账户菜单可见;OIDC 登录/同意 UI 迁至前端(mirror 仅留 JSON API + HMAC 无状态 challenge,删除内嵌 HTML 页);许可证 AGPL-3.0 → Elastic License 2.0(ELv2)(产品五仓;api 保持 MIT、docs 保持 CC BY-SA 4.0;ADR-0004 修订记录决策与后果)。
同日晚四轮:n 合一共享登录——mirror IdP 识别 web 会话 cookie、第一方客户端(first_party)免同意、hoard 支持 next 回跳(CORS 白名单校验);mixed 部署下 mods 静默同步登录态、不再显示“登录 Mod 站”,仅单模块 mods 部署保留显式跳转(台账见根目录 FRONTEND-REMEDIATION.md §G7)。
2026-10-25 三轮 backlog(台账 §H):注册前置要求(邀请码/验证码)+ 公告横幅 + 按名头像 + mods facets 过滤 + 页码分页 + Microsoft 绑定 + 多玩家管理(mirror/hoard 共 5 个新端点);web 安全头(CSP/HSTS 等)+ robots/sitemap;/legal/* 法务页。剩余 backlog 与不可会话内完成项见台账 §H4。
2026-10-25 四轮(台账 §H5):admin 校验收紧(role/status/枚举/长度 + 邮箱正则单测);admin 四面板搜索+分页、审计 q 过滤、PAT scopes 复选;e2e 新增 ui-misc / consent-thirdparty / moderation 三 spec(共 7 tests)并修正旧按钮名;皮肤详情 OG 图改渲染头像。markdown changelog、Meili total、团队邀请流、标签、2FA、Compass、镜像代理、i18n 基建、webhook、Isles/Atlas 评估留待后续轮次。
工程化
- 无 E2E/契约字段级测试;无性能/负载数据;无 Helm/K8s(总规划 §6 的 C/D 档只是纸面);无 CHANGELOG/release/tag(除 shared 1 个);Caddyfile 无 TLS、不路由 web
- CI 无 gofmt 门(仓库存在既有未格式化文件:admin.go/migrate.go/s3storage.go/render.go/memstore.go)
- 无 ToS/Privacy/DMCA/内容合规
下一阶段规划(收敛,勿并行)
Section titled “下一阶段规划(收敛,勿并行)”原则:先把 Mirror 单产品做到“能给陌生人用”,再补生产运维基线,然后才动 Hoard 的用户体系(重写级)。每阶段有独立验收标准,做完打 tag 发版。
身份架构决策(Phase 3 的前提,2026-10-23 定)
Section titled “身份架构决策(Phase 3 的前提,2026-10-23 定)”问题:Hoard 缺用户系统。共享 Mirror 的 users 表?还是把账号体系放进 shared?
决策:都不是。Mirror = 生态身份中心(OIDC Provider,已建成),Hoard = OIDC 客户端(RP),只存用户投影 + teams + API Token。
| 方案 | 结论 | 理由 |
|---|---|---|
| 共享 users 表(同库同表) | 否决 | 两服务 schema/迁移互相锁死;独立 mod 站模式直接坏掉;双写方破坏服务边界 |
| 账号体系放 shared | 否决 | shared 章程是零依赖接口/纯实现;账号要 PG/bcrypt/mailer/captcha,塞进去必然膨胀;只放接口就是刚删掉的 shared/auth 死代码重演 |
| Hoard 自建完整账号 | 否决 | 重复 Mirror 约 1.5k 行账号/邮件/验证码逻辑,双倍安全面与维护成本 |
| Hoard 作 OIDC RP + 投影表 | 采纳 | 复用已建成的 /oauth2/*(ADR-0010 就是为此);Hoard 不存密码不发邮件,账号安全面为零;投影表从 ID token claims(sub/username/email)登录时 upsert,无手工同步;C/D 档换 Zitadel 只改 issuer 配置 |
要点:
- 两服务可同库实例(compose 便利),但表绝不共享;关联键 = OIDC
sub(即 mirror users.id 字符串),不建跨服务外键 - Hoard 侧:
users(投影:sub/username/email/avatar/role)+teams/team_members+personal_access_tokens(哈希存储,上传/管理用);HOARD_ADMIN_TOKEN保留作 bootstrap/救援通道 - 管理员映射:Hoard 自管角色(admin UI 提升 +
HOARD_ADMIN_SUBJECTS环境变量白名单自动映射),不隐式信任 Mirror 的 role - 唯一值得重新考虑的情形:若坚持“mod 站必须是单二进制(无 Mirror)”,则退路是把 Mirror 账号核心抽成独立 library 模块(不进 shared)——当前不接受该约束,故不抽
- ADR-0011(身份架构)已补:
src/content/docs/architecture/adr-0011-identity-architecture.md(2026-10-26)
Phase 1 — Mirror 可交付(预计 4~6 周)
Section titled “Phase 1 — Mirror 可交付(预计 4~6 周)”目标:一个陌生人按文档部署后,能完成 注册→验证→登录→传皮肤→皮肤库浏览互动 全流程。范围=产品功能补齐;生产化正确性(多实例/存储加固)归 Phase 2。
进度(2026-10-23):W1 后端✅(详情/评论读删/搜索排序分页/apply 全部落地,单测+真 PG 集成+11 项运行时冒烟全过,spec 同步且漂移门绿);W2✅(皮肤库两页 RSC SSR + 搜索/排序/分页/评论区/apply + toast(sonner) + error/loading 边界 + admin 页 Tabs/Table 组件化,SSR 真渲染验证过,
API_URL_INTERNAL已进 compose);W3✅(admin 会话登录上线:后端 AdminAuth 本就支持 role=admin 会话,web 管理面板改用登录会话、手贴 token 降级为救援通道、hoard token 独立输入;CORS 收窄为可配白名单*_CORS_ORIGINS,mirror 回退 WEB_URL,运行时验证允许/拒绝/PATCH 预检/会话 200/非管理员 403 五项全过)。W4✅(Phase 1 完成):Playwright E2E 全链路本地真跑通过并进 CI(注册→mailpit 收验证邮件→验证→登录→上传皮肤→发布到皮肤库→SSR 搜索→详情点赞/评论/应用);gofmt 门 + 存量格式化;mirror/hoard/web CHANGELOG;profile 页补“发布到皮肤库”UI。E2E 抓出并修复两个真 bug:SMTP 邮件从未发出(go-mail host/port 误用 + 默认强制 STARTTLS + 错误被_ =吞掉;现按端口/凭证自动选 TLS 策略、发送失败进日志)与 profile 上传 playerName 恒为空(auth 异步恢复后 state 不回填,UI 上传从未通过)。v0.1.0 tag 已打(本地,待 push)。
后端(W1,先行,前端依赖它):✅
GET /skinlib/{id}详情端点(现状:前端拉listSkinLib(0,1000)客户端 find,必须废掉)GET /skinlib/{id}/comments评论读取 + 管理端删除;皮肤库q搜索 + 排序(最新/点赞)+ 分页 totalPOST /skinlib/{id}/apply“应用到我的皮肤”(认证,将库条目纹理设为自己玩家的皮肤)- 每项同步改 spec →
pnpm gen:api→ 漂移门自动兜底
前端(W2):
- 皮肤库列表/详情改 RSC/SSR(SEO+首屏),废弃客户端全量拉取
- 分页/排序/搜索 UI、评论展示+发表、apply 按钮、toast/
error.tsx/loading.tsx/skeleton - 补 shadcn table/tabs/dialog/pagination/skeleton(admin 页去手搓 div)
认证与安全(W3):
- admin 会话登录:role=admin 账号经 authenticate 换会话,替代手贴
adminToken(token 通道保留为救援);web 管理后台改造 - CORS 从
*收窄为MIRROR_CORS_ORIGINS/HOARD_CORS_ORIGINS白名单(默认前端 origin)
质量与发布(W4):
- Playwright E2E 进 CI:compose 起栈 + mailpit 收验证邮件 → 注册→验证→登录→上传→皮肤库可见→评论
- gofmt 门(顺带格式化 5 个既有未格式化文件);CHANGELOG 起步
- 打
v0.1.0(mirror/web/shared/api 同步 tag),compose 提供 pin GHCR 镜像版本 - 验收:陌生人按文档部署并走完全流程;CI 全绿含 E2E
Phase 2 — 生产化与运维基线(预计 3~4 周)
Section titled “Phase 2 — 生产化与运维基线(预计 3~4 周)”目标:敢开一个 A/B 档公共小站并持续运营。范围=正确性与可运维性(Phase 3 依赖其中的 IdP 加固)。
状态(2026-10-23):全部工程项完成 ✅,v0.2.0 已发布;剩余“两周无手工干预运维”验收待真实部署窗口。
- 多实例正确性:
fosite storage 落 Postgres✅(pgstore.OIDCStore,JSONB 全量会话 + 惰性 GC;双副本真机验证:A 发码 B 换 token、跨副本 refresh 轮换、重放拒绝、userinfo claims 完整;顺带修复两个存量 bug——consent 后从未 GrantScope 导致 id_token 永远发不出、userinfo sub 恒为空;客户端缓存未命中节流重载解决跨副本注册感知);✅;mojangStates落 cache✅(ratelimitRedis 实现internal/redislimit,Lua 原子 INCR+PEXPIRE,双副本共享计数真机验证 10 过/14 拒);goose 迁移加 advisory lock(双副本并发启动竞态,真机复现后修复) web 会话加固✅:access token 移出 localStorage → httpOnly cookie(mythic_session,SameSite=Lax+Secure,7d);Next route handlers/api/auth/login|logout|session(BFF:token 只在服务端经手,响应仅回非敏感 meta);mirrorBearerTokencookie 回退(Auth/AdminAuth 共用;启动器 Bearer 头不受影响);CORS 白名单时 AllowCredentials;登出服务端 invalidate;CSRF=SameSite Lax+JSON-only 双层阻断(cookie 路径无表单可达端点);E2E 增断言:localStorage 无 token、document.cookie 读不到会话Caddyfile 生产模板✅:docs/deploy/Caddyfile.prod(自动 TLS、双域名路由、安全头兜底、mirror 会话亲和作 OIDC 过渡措施,caddy validate通过)审计日志✅:audit_log表(migration 00008),全部管理操作记录 actor(token/user:id)+ 来源 IP(clientip),GET /api/admin/audit+ 后台“审计日志”页签;集成测试 + 运行时验证配额(上传数量/大小)✅:mirror 存储配额(migration 00010files登记表按上传者归属 + sha256 去重、users.storage_tier分级、MIRROR_QUOTA_STORAGE_BYTES/TIERS/PUBLISHED;登记→检查→超限回滚,重传同图不误拒)+ 发布数量配额(413);hoard 单文件上限HOARD_MAX_FILE_BYTES(默认 512MiB,413);memstore/PG 双实现测试举报 + 皮肤审核队列✅:migration 00011(reports表 +skinlib_items.status);POST /reports(登录用户,同目标唯一 409);MIRROR_SKIN_REVIEW=true发布进 under_review(公开列表零泄漏);adminreports/skins端点 + 后台“举报/皮肤审核”页签(驳回/删除并结案/批准/下架/退回);全部落审计;memstore+PG 集成测试 + 真机 HTTP 全流程验证(204/409/404/队列过滤/批准上架/审计记录)HTML 邮件模板✅:multipart/alternative(SMTP)+ html(Resend),i18n intro/button/footer 三段式;E2E 抓出并修复 SMTP 从未送达(go-mail host/port 误用 + 强制 STARTTLS + 吞错)备份/恢复✅:docs/deploy/backup.sh+backup.md(pg_dump custom + 卷打包 + 保留策略 + 恢复演练要求)Prometheus/Grafana✅:docs/deploy/observability/(告警规则 promtool 校验过、dashboard JSON、抓取配置;hoard 补齐 /metrics);k6 压测基线✅:docs/deploy/k6/(读 20→50VU p95=4.5ms 0 错误;写 5VU 登录+上传+发布 checks 100% p95=63ms;阈值即 SLO 草案,生产验收在目标硬件重跑记录)- 验收:按文档从零运维小站两周无手工干预(待真实部署窗口);
双副本部署登录态/限流/OIDC 正确✅(限流/OIDC/正版绑定/迁移均已双副本真机验证);发布✅(mirror/hoard/web/api 同步 tag;web 会话为破坏性变更:AuthState 不再含 accessToken)v0.2.0
Phase 3 — Hoard 用户体系与开放上传(预计 6~8 周)
Section titled “Phase 3 — Hoard 用户体系与开放上传(预计 6~8 周)”目标:普通用户不需要管理员 token 就能发布 mod;在此之前 Hoard 不宣传为 Modrinth 替代。按上方身份架构决策执行:
状态(2026-10-23):W1~W8 全部完成 ✅,v0.3.0 已发布(hoard/web/api;mirror 无变更不重打);剩余 Prism 人工安装验收待部署窗口。
后端:
Hoard OIDC RP✅(W1,2026-10-23):internal/oidcrp(go-oidc v3:discovery/JWKS 轮换/签名/iss/aud/exp/nonce 全校验,机密客户端);migration 00002(users投影 +sessions);/auth/oidc/login|callback、/auth/logout、/auth/me;state/nonce 短期 httpOnly cookie;会话 cookiehoard_session(httpOnly+Lax,7d,服务端可撤销);投影 upsert 重登不覆盖已提升的 role;HOARD_ADMIN_SUBJECTS白名单幂等映射 admin;CORS 白名单允许凭证;spec 漂移门绿;mem+PG 测试;真机 SSO 全流程验证通过(mirror IdP:login→consent→code→callback→me(role=admin)→logout→401,投影/会话落 PG 验证)迁移:✅(W2,migration 00002/00003):投影表 + 服务端会话(W1);teams 两级角色(owner 管成员/删项目,member 传版本);PATusers投影表、teams/team_members、personal_access_tokensmhp_前缀明文仅创建时返回一次、sha256 哈希存储、scopes、过期、last_used、可撤销;projects.team_id可空兼容存量;mem+PG 集成 + HTTP 全链路测试(401/403/撤销即失效/明文不泄漏)用户级上传✅(W3):POST /v2/project(Modrinth 形 multipart data+file,自动建团队归属)、POST /v2/version(团队成员校验)、DELETE /v2/project/{id}(owner/admin);PAT scopes 强制(声明 scopes 须含 UPLOAD);HOARD_REQUIRE_REVIEW审核开关接线;管理员/api/mods队列保留;spec 漂移门绿mod 元数据解析✅(W4):internal/modmeta解析 jar 内fabric.mod.json/quilt.mod.json/META-INF/(neo)forge.mods.toml(BurntSushi/toml)→ 自动填充 title/description/loaders/game_versions(minecraft 依赖下界)/license/version/icon;表单优先、冲突写metadata_flags(title/description/loader/game_versions_mismatch);icon 提取入对象存储 +GET /api/mods/{id}/icon直出;migration 00004(license/icon_key/meta_flags 列);非 jar/损坏元数据静默兼容;7 项解析单测 + 上传集成测试版本依赖✅(W5):migration 00005version_dependencies(required/optional/incompatible/embedded,Modrinth 形);POST /v2/project|versiondata 带dependencies[](非法类型 400);GET /v2/project/{id}/dependencies;mrpack 完全合规增强:dependencies.minecraft.version(版本级 game_versions 优先)、env.client/server(映射 client_side/server_side)、overrides/目录;deps+mrpack 集成测试搜索 facets 透传 + Meili✅(W6):facets 解析已在位(内层 OR/外层 AND,补单测);shared/searchtotal_hitsQuery返回 total(v0.1.2,破坏性签名——仅 hoard 消费);Meili 分支 total_hits 用 estimatedTotalHits 且修复同键多值 filter 被 AND 的语义 bug;hoard go.mod 升 shared v0.1.2
前端:
mod 详情页(版本列表/changelog/文件/依赖)、创建项目+发版向导、teams 管理、PAT 管理✅(W7):/mods/[id](icon/license/冲突旗标/版本/依赖/成员内联发版/admin 删除)、/mods/upload(jar 元数据自动填充)、/mods/settings(团队+PAT,明文仅一次)mods 模块登录改走 Hoard 会话(OIDC 跳转),与 skins 模块的 Yggdrasil Bearer 并存✅(W7):useHoardUserhook + mods 页登录态区;hoardClient 全量 credentials(httpOnly cookie)
验收:SSO 注册→建项目→传 jar(元数据自动填充)✅(W8,mods-sso.spec.ts 真机全链路 2.4s:注册→验证→SSO 授权码流+consent→上传 jar→详情页断言 license MIT/loader fabric/版本 1.0.0/游戏版本 1.20.1→建团队→建 PAT);发依赖→生成 mrpack✅(后端集成测试覆盖:依赖存取 + mrpack minecraft/env/overrides);Prism 实际安装为人工验收项(待部署窗口);全程零管理员 token ✅
Phase 4 — 生态扩展(持续,按需启动)
Section titled “Phase 4 — 生态扩展(持续,按需启动)”- i18n 扩语言(目标 15+,接 Crowdin/Weblate)
Compass 原生启动(piston-meta + assets + authlib-injector 注入)或先做 Prism 账号深度集成;补 CI/自动更新;→ 已展开为独立计划:Compass 启动器开发计划(P0 骨架与仓库卫生 → P1 原生启动最小可用链路 → P2 实例与内容管理 → P3 mod loader 与整合包 → P4 账号与令牌安全 → P5 诊断/Java 自动下载/自动更新 → P6 三平台打包签名发布;每阶段含周任务、验收与版本号,Forge processor pipeline 为最大单点风险并已写下降级验收标准)。决策:直接全量对标 HMCL/PCL,三平台,不再走“先做 Prism 账号深度集成”的过渡路线。launch白名单化- 上游镜像代理(Modrinth/CurseForge 冷启动);插件/Webhook;Isles/Atlas 建仓评估
补记(2026-10-26):工作区换树事故 + canonical 树上的修复轮
Section titled “补记(2026-10-26):工作区换树事故 + canonical 树上的修复轮”事故:会话中途工作区被换成另一棵树(Q: 是文件支持的 VHDX;系统时钟同时被回拨约 8 小时,Get-Date=05:08 而当日写入文件 mtime=12:31~12:51)。旧树的部分目录(根 hoard/mods/isles/atlas、docs/adr|plans|specs、web/src/app/[locale]/teams、mirror/internal/api、hoard/internal/httpapi)在当前树不存在;回收站无近期删除、Q:\Backups 无本项目备份、opencode.db 无本项目会话记录 ⇒ 会话内不可还原,唯一完整来源在宿主侧快照。经用户决定:不还原,在当前(canonical,7 个独立 git 仓库)树上重做缺失项。详见工作区根 FRONTEND-REMEDIATION.md §J/§K。
复核结论:多数被判“丢失”的能力其实只是换了位置/命名(注册要求端点=/api/register-requirements、公告=/api/announcement+admin、Microsoft 绑定=/mojang/oauth/*、按名头像=/avatars/by-name/{name}、举报+皮肤审核队列在 mirror 而非 hoard、法务文在 web/messages 的 legal.*、admin i18n 54 键齐全)。真缺口只剩:团队邀请接受/拒绝、玩家改名/删除与“当前玩家”、Microsoft 解绑、ADR-0011。
本轮修复(全部有门禁与测试):
- 后台不可用(P0):
admin/client.tsx在登录态把 token 置空串(=用会话 cookie),但 7 个面板一律if (!token) return needToken,且此时令牌输入框被隐藏 ⇒ 会话管理员完全无法使用后台。凭据语义显式化为token: string | null(null=无凭据/“”=会话/非空=手贴),面板与全部变更处理函数改判=== null。 - 鉴权语义统一(P0):
/api/admin/*认“静态 token 或 role=admin 会话”,而/admin/batch-import、/api/admin/oauth-clients只认静态 token。新增yggdrasil.AdminRole(store, token)+AdminActor,三处统一;删除只认静态 token 的yggdrasil.Admin;审计 actor 不再硬编码"token"。新增TestAdminRoleAcceptsTokenAndAdminSession。 - CI 必红(P0):
e2e/moderation.spec.ts夹具指向../docs/deploy/k6/skin.png(CI 不 checkout docs)→ 改用 web 自带public/skins/steve.png;e2e.yml缺MIRROR_CORS_ORIGINS⇒ 浏览器带凭据跨源被拦(三个 spec 第一步就挂)→ 已补;web CI/e2e 的 node 22 与.nvmrc/Dockerfile 的 24 不一致 → 统一 24。 - 生产反代漏路由(P0):根
Caddyfile.prod只转/v2/*、/api/mods*给 hoard,缺/auth/*、/api/team*、/api/user*⇒ 生产 SSO/团队/PAT 全落 mirror 404(dev Caddyfile 是对的)→ 补齐,并修正“OIDC token 存进程内存”的过期注释(fosite 已落 PG,migration 00009,多副本无需粘性)。 - 部署模板分叉(P1):
docs/deploy/{docker-compose.yml,Caddyfile,.env.example}落后于根副本,而docs/deploy/index.md声称一致;照它部署会让 hoard 落进 mirror 的publicschema(同名表碰撞)、缺HOARD_OIDC_INTERNAL_BASE_URL(容器内 hairpin 失败)、web 缺LOCALE_PREFIX/INDEXABLE/BLOCK_BOTS构建参数 → 已同步,Caddyfile.prod两侧取并集后完全一致,caddy validate通过。注意根副本不在任何 git 仓库内(根无 .git),docs/deploy才是可追溯版本。 - 末位 owner 保护(P1):唯一 owner 退出或被降级会让团队永久无人可管 → 两条路径均 409(须先移交);新增
TestTeamLastOwnerGuard。 - 契约与配置(P1):
/api/register-requirements增minPasswordLength/emailDomains,密码长度收敛为单一常量account.MinPasswordLength(去掉两处validate:"min=8"标签防漂移);同步api/specs/mirror.openapi.yaml+pnpm gen:api;注册页minLength读契约值并渲染规则提示(新增auth.passwordMin{n}/auth.emailDomains{domains})。go.work的go 1.27.0与模块/CI 的 1.26 不一致 → 改回;api/specs/hoard.openapi.yaml的info.version0.2.0 → 0.3.0;mythic-hoard/.env.example补HOARD_OIDC_INTERNAL_BASE_URL。 - i18n/UI(P1):
modSettings.patScopes键被错放在admin下 ⇒ 界面直出原始键名 → 归位;主题名硬编码中文(英文界面显示中文)→ 改 id 驱动theme.name.<id>(中英各 6 条),删Theme.label;mod 更新日志max-h-16 overflow-hidden静默截断 → 原生<details>折叠;删markdown.tsx未定义的prose-mythic死类;中文 ADR 索引链接写成大写ADR-000N-*.md(文件名小写,GitHub Pages 区分大小写 ⇒ 死链)且漏 0010 → 修正。 - 其它:
/metrics由r.Handle(对所有方法响应)改r.Get;hoard Meili 索引懒开通(filterableAttributes +maxTotalHits=1e6使总数精确),并修掉Mythicose→Mythicove坏 import(此前 meili 驱动编译不过);死代码清理(hoardCountTeamMembers、mirror 生成的ListAudit/CountAudit、webapi.ts6 个零调用者导出)+sqlc generate无漂移;mythic-hoard/README.md重写(此前仍称“无用户体系/团队/PAT”,端点表缺 12 条);补 ADR-0011 身份架构(Mirror 唯一 IdP / RP+投影表 /sub关联不建跨服务外键 / 同库 schema 隔离 / 权限不隐式继承 / PAT 与 issuer 分离视界),中英 ADR 索引同步。
门禁:web typecheck/lint/stylelint/build 全绿;mirror、hoard go build ./... + go test ./... -count=1 全绿(含 2 个新增测试);gofmt -l 干净;caddy validate 通过;sqlc generate 无漂移。
遗留(下一轮):团队邀请接受/拒绝流;玩家改名/删除 + 当前玩家选择(会话目前硬绑第一个玩家);Microsoft 解绑;mods 管理面板仍用独立 hoard 静态 token(与 hoard 已有 /auth/me role=admin 并存两套管理员模型);OAuth 回调把 token 放 URL fragment(会留在浏览历史);POST /mojang/verify 在生产恒失败(main.go 未 SetResolver);providerLabels 预列 microsoft/google/discord/qq/wechat/weibo 但只实现了 github;/api/admin/oauth-clients 未进 OpenAPI 契约;未提交改动量很大(web 67 改 + 47 未跟踪、mirror 17、hoard 10、docs 12),建议尽快落分支以免再次丢失。
补记(2026-10-27):凭证分离——网站主密码 vs 启动器独立密码(ADR-0012)
Section titled “补记(2026-10-27):凭证分离——网站主密码 vs 启动器独立密码(ADR-0012)”用户决策:启动器登录密码从主密码里拆出来(每玩家可设若干条),管理员可强制“必须有独立密码才能从启动器登录”,从而在第三方登录可用后彻底关掉账号密码登录;随后开始做启动器(全量对标 HMCL/PCL,全平台)。
- 拆分的必要性:此前全站只有一个凭据(
users.password_hash)和一个校验原语(Store.Authenticate),被网站登录、启动器登录、signout、IdP 登录表单四处共用 ⇒ 关掉密码登录就等于切断启动器。且login_methods的password开关纯装饰(只有internal/admin读它),“禁用账号密码登录”这个能力此前根本不存在。 - 数据(migration 00013):
player_credentials(每玩家 ≤3 条,Argon2id;player_uuid用ON UPDATE CASCADE让凭证跟随改名后的新 uuid)、user_identities(第三方绑定;此前 GitHub 登录只按 email 匹配且不留痕,“绑过第三方吗”无从判定)、sessions.credential_id(ON DELETE CASCADE⇒ 撤销凭证立即销毁其会话)。删除users.active_player_uuid整套机制(列、Player.active、/account/players/{name}/activate、Store.PlayerByUser/SetActivePlayer、web“设为当前玩家”)。 - 语义:
/authserver/authenticate与signout的username改为玩家名、password为该玩家独立密码(主密码在协议端点上完全不被查询,邮箱也不能作用户名);availableProfiles只含认证到的那个玩家;网站/OIDC 会话player_id为空串(不代表任何玩家,也就不能 join 服务器),/account/login响应改为{accessToken, clientToken};玩哪个玩家改为浏览器本地选择(AuthProvider 登录后从玩家列表补齐,存 localStorage)。signout从“校验完密码什么都不删”的 no-op 改为真正销毁该玩家的游戏会话。 - 策略(
admin.AuthPolicy,三处共用同一份实现):password开关取自login_methods(不新增第二个开关)——关闭后账号密码注册立即停止,主密码登录只对未绑定第三方的历史账号放行(宽限),已绑定者 403 + 稳定错误码;require_third_party_binding拦皮肤上传/皮肤库发布与应用/启动器登录/SSO,但不拦账号管理、绑定流程本身、举报与评论(否则用户被锁在“必须先绑定才能进绑定页”);max_players默认 1,存量超额不回收。 - “纯密码账号还有多少”如何优雅实现(用户担心查询昂贵):不进登录热路径。热路径上的判定是“这个账号自己有没有绑定”(登录本来就要读用户行,一次索引查询);全站计数
CountPasswordOnlyUsers(走users_password_only部分索引)只在管理面板打开时算一次,纯作决策提示。于是宽限集合自然收缩,归零那一刻主密码登录已无人可用——不需要第二个开关、不需要自动切换状态、不需要定时任务。 - 顺带修掉三个既有缺口:
GetUserAuth加status='active'过滤(banned/suspended 此前照样能拿会话);/oauth2/login/accept(校验的正是主密码)挂上auth限流桶(此前是全站唯一能无限速试密码的端点);login_methods真正强制生效。另修两个探针抓出的 500:POST /account/players超上限(错误未映射,改为复用writePlayerErr)、策略关闭时的账号密码注册与密码重置。 - 成本与上限:Argon2id 无快速查找键,登录要对候选哈希逐条校验 ⇒ 每玩家上限 3 条(常量,非配置项)+
last_used_at DESC让常用凭证先命中。要放宽条数应加服务端 pepper 的 HMAC 快查列,而不是调大常量。 - 未处理(记入台账):
sessionsTTL 24h vs web cookie 7d 不一致;memstore 会话不过期;email_verified仍不参与任何门禁(无 SMTP 的部署会因此把所有人锁在门外,属独立策略决定)。 - 验证:内存 + PG 双实现测试全绿(新增 7 个测试);部署级探针逐条验证(真容器 + 真 PG):主密码/邮箱进不了启动器、玩家名+独立密码可进、撤销后旧游戏会话立即失效而网站会话不受影响、玩家数上限 409、关主密码后未绑定者可登录(宽限)而注册 403、落库绑定后登录 403、开强制绑定后启动器登录 403
binding_required而读玩家列表 200。e2e 新增launcher-auth.spec.ts(10 tests)。 - 协作事故:收尾时另一会话在 mirror 合并
feat/site-info,解冲突时吃掉一个},把不能编译的合并提交推上了 main(mirror CI 与 web e2e 双双变红,e2e 是因为它从 mirror main 构建)。对方随后用前向修复提交a46035f补齐(未改写历史)。教训:合并后必须 build+test 再推。另:本机gofmt -l因core.autocrlf把所有文件都报成未格式化,真凶(memstore.go的注释对齐)要把文件转成 LF 副本、在 Linux 容器里跑 gofmt 才看得见。

