Civitai 3D Models Phase 2:个人资料页 3D Models Tab 与审核集成的完整实施计划

发布时间:2026/9/17 22:27:33
Civitai 3D Models Phase 2:个人资料页 3D Models Tab 与审核集成的完整实施计划 Civitai 3D Models Phase 2个人资料页 3D Models Tab 与审核集成的完整实施计划【免费下载链接】civitaiA repository of models, textual inversions, and more项目地址: https://gitcode.com/GitHub_Trending/ci/civitai本文基于仓库内的跟进计划文档 docs/3d-models-followups.md完整拆解 Civitai 3D Models 功能二期Phase 2的两块工作M1 为个人主页/user/[username]/*新增 3D Models 内容 Tab 的缓存、页面与导航改造M2 为 Model3D 实体打通举报队列、举报副作用、站长处置端点、Strike 与申诉的完整审核链路。读完本文你可以掌握该计划的调研结论、分阶段落地步骤、新增 tRPC 端点签名设计、关键风险清单以及对照当前仓库源码确认这些设计已被实现的程度。一、背景Phase 2 的两块工作3D Models 功能一期已提供全局 feed/3d-models路由与详情页但个人主页还没有 3D 模型内容 Tab审核体系举报、处置、Strike、申诉也没有完全覆盖Model3D实体。docs/3d-models-followups.md 即针对这两个缺口的 Phase 2 计划将工作拆为M1 — Profile-page 3D Models tab让每个用户的/user/[username]页出现 3D Models 子页带计数徽章、草稿切换、feature-flag 门控工作量估算 S–M约 5–7 小时。M2 — Moderation integration让 Model3D 举报能进入站长队列、触发 NSFW/TOS 副作用提供 NSFW 等级覆盖、布尔标志切换、恢复端点并接入 Strike/申诉与mod-actions脚本技能工作量估算 M–L约 10–13 小时。两项工作的共同前提是model3dFeedfeature flag。该 flag 在 feature-flags.service.ts 中定义为availability: [mod]flipt keymodel3d-feed即上线初期仅站长可见——文档中所有门控设计都围绕这一约束展开。二、M1个人主页 3D Models Tab2.1 现状调研Tab 与计数是如何运作的文档的 Current state (research findings) 部分给出了四条关键调研结论全部可在源码中印证个人页 shell 与导航是静态映射没有注册表。个人页外壳是 ProfileLayout2.tsx包裹每个/user/[username]/*页面并渲染ProfileNavigation /作为子导航。ProfileNavigation.tsx 中是一张以字面量路由末段为 key 的静态opts映射models、posts、images、videos、articles、comics、collections等——新增 Tab 就是往这张对象里加一个 key。活跃 Tab 的解析逻辑在 ProfileNavigation.tsx#L45router.pathname.split(/).pop()因此/user/[username]/3d-models解析出的字面量是3d-modelsopts的 key 必须是带连字符的3d-models而非 camelCase。各 Tab 页遵循统一模式。src/pages/user/[username]/*.tsx如 articles.tsx每个页面都用createServerSideProps({ useSSG: true, ... })flag 关闭时 302 重定向回/user/[username]SSG 阶段预取userProfile.getuserProfile.overview用postgresSlugify(currentUser.username) postgresSlugify(username)推导selfView仅自视时渲染FeedContentTogglepublished/draft内容区是MasonryContainer 对应的*Infinite组件默认导出Page(..., { getLayout: UserProfileLayout })继承外壳。计数走 Redis 复合缓存。trpc.userProfile.overview→getUserContentOverviewHandler→ user-profile.service.ts 的getUserContentOverview→ Redis 复合缓存userContentOverviewCache实现在 caches.ts。UserContentOverview类型原本硬编码modelCount、imageCount、videoCount、postCount、articleCount、comicCount、collectionCount等 key没有model3dCount。all / sfw / public 三种变体各自由逐表计数缓存工厂支撑。trpc.model3d.getInfinite已具备所需入参。model3d.schema.ts 中该 procedure 接受username、userId、includeDraftsmodel3d.service.ts 对非站长、非 owner 的读取有正确门控——allowDrafts以userId user.id为条件个人页应传userId: profileUser.id加includeDrafts自视时。文档特别指出的坑是未登录匿名访问者会拿到空列表因此新 Tab 对登出访客会显示为空这与 launch plan 的 mod-only 门控一致另外传username而非userId可以绕开服务层被 ban 用户分支的限制R3见 2.4 风险。2.2 Phase 1后端计数管道~2–3 h计划要求在 caches.ts 中新增三个createUserContentCountCache实例分别为model3dCount、model3dCount:sfw、model3dCount:public。文档给出的 SQL 模板与仓库中最终落地的实现一致caches.ts#L760-L804SELECT userId as id, COUNT(*)::INT as model3dCount FROM Model3D WHERE userId IN (...) AND status Published AND deletedAt IS NULL AND availability ! Private -- sfw/public 变体追加AND (nsfwLevel ${sfwBrowsingLevelsFlag}) ! 0 GROUP BY userId三个变体的差异只在末尾的位运算过滤sfw 用sfwBrowsingLevelsFlag、public 用publicBrowsingLevelsFlag与nsfwLevel列做位与这是仓库中所有实体计数缓存共用的模式。TS 侧落地为userModel3DCountCache、userModel3DCountSfwCache、userModel3DCountPublicCache三个工厂实例。随后需要扩展UserContentOverview类型增加model3dCount: number当前实现在 caches.ts#L1117-L1129更新mergeOverviewResults与全部三个getUserContentOverview*聚合函数把新缓存结果并入合并逻辑见 caches.ts#L1143-L1160model3dCounts[id]?.model3dCount ?? 0兜底为 0把userContentOverviewCache.refresh(userId)接入 model3d.service.ts 的publishModel3D/unpublishModel3D/deleteModel3D镜像model.service.ts中Model.publish触发userModelCountCache.refresh的既有模式验证 SSR 预取ssg?.userProfile.overview.prefetch无需改动即可返回新字段——因为 procedure 原样返回缓存形状。user-profile.service.ts本身无需修改它只转发缓存结果。2.3 Phase 2–4页面、导航与 SEO页面~2 h新建src/pages/user/[username]/3d-models.tsx该文件现已存在以 articles.tsx 为模板getServerSidePropsif (!features?.model3dFeed) return { redirect: { destination: /user/${username}, permanent: false } }再加 ban 用户重定向然后 SSG 预取userProfile.get和userProfile.overview用useCurrentUser()postgresSlugify推导selfView计算isMod currentUser?.isModerator状态[section, setSection] useStatepublished | draft仅当selfView || isMod时显示FeedContentToggle数据源trpc.model3d.getInfinite.useInfiniteQuery({ limit: 50, username, includeDrafts: section draft (selfView || isMod) })复用 3d-models/index.tsx 中的 Masonry Model3DCardCards/Model3DCard.tsxInViewLoader模式空态复用NoContent /自视时附链接到创建页的 CTA默认导出Page(UserModel3DsPage, { getLayout: UserProfileLayout })。导航~30 min在 ProfileNavigation.tsx#L48-L84 的opts中加一项。当前仓库中已可见最终形态ProfileNavigation.tsx#L78-L843d-models: { url: ${baseUrl}/3d-models, icon: (props) IconCube {...props} /, label: getDisplayName(3d-models), count: userOverview?.model3dCount ?? 0, disabled: !features.model3dFeed || !!user?.bannedAt, },IconCube与Model3DCard同源复用。门控策略是disabled: !features.model3dFeed || !!user?.bannedAt镜像articles项的!features.articles门控。SEO no-index~1 h 内的打磨项ProfileLayout2.tsx#L68-L78 维护一张subpageCounts映射若当前子页计数为 0 则返回noindexif (subpageCounts[subpage] 0) return true。必须给这张表补上3d-models: overview?.model3dCount否则空 3D Tab 的去索引逻辑会失灵。站点地图/user/[username]/3d-models需要加入 sitemap/canonical 辅助逻辑仓库中 sitemap 相关页面在 sitemap.xml、sitemap-pages.xml 等。2.4 M1 风险与开放问题R1–R5文档列了五条风险值得作为落地检查单R1 组件重复3d-models/index.tsx 中的 masonry 块若在个人页重新实现会造成漂移建议在 Phase 2 抽出一个接收过滤 props 的Model3DsInfinite组件镜像ModelsInfinite约 30 分钟重构。R2 匿名访客看到空 Tab服务层对未登录的非站长请求短路返回空列表model3d.service.ts但导航徽章计数可能显示model3dCount 0而点进去是空的。文档的推荐匿名时只隐藏计数徽章、保留 Tab 可点击让空态解释门控原因。R3 ban 用户绕过服务层 ban 检查是userId userId ! user.id !username——传username时任何登录用户都能通过。页面层重定向必须镜像 models.tsx 的user?.bannedAt检查。R4 缓存失效覆盖面model3dCount的失效必须覆盖 publish/unpublish/delete/upsert状态变化时grepuserModelCountCache.refresh找既有模式。R5availability列Model3D.availability默认Public见 packages/civitai-db-schema/prisma/schema.full.prisma计数 SQL 已排除Private将来若开放私有草稿分段再议实体有字段但当前无产品流设置它。三、M2审核集成3.1 现状调研哪些已通、哪些断链文档的调研结论是M2 约一半基础设施已就绪但四条链路各自缺一环Report 枚举与 Schema 已接好。ReportEntity.Model3DModel3DReview存在于 report-helpers.tsreport.service.ts的reportTypeNameMap/reportTypeConnectionMap覆盖两者Report表已有model3d/model3dReview关系schema.full.prisma。但report.create对 Model3D 会失败直到(a)report.controller.ts的getReportsselect 块补上model3dmodel3dReview(b)ReportModal的各表单availableFor数组加入ReportEntity.Model3D。详情页src/pages/3d-models/[id]/[[...slug]].tsx当时有一处 TODO 阻断举报按钮。两条并行的举报路径集中式trpc.report.create→createReportreport.service.ts会进站长队列并触发 tag-vote / NSFW-action 副作用直接式trpc.model3d.reports.createForModelmodel3d-report.service.ts写同一张Report表但绕过副作用。文档的结论UI 一律走trpc.report.create直接路径保留给程序化/SDK 调用或后续废弃。站长队列缺投影getReportsHandlerreport.controller.ts不 select model3d 连接数据队列页的Model3D/Model3DReview分段选项卡能渲染但结果为空getReportLink没有对应 caseOpen reported item 按钮 href 为 undefined。处置端点缺口trpc.model3d.unpublish与trpc.model3d.delete软删已存在但没有setNsfwLevel、setTosViolation、setPoi、setMinor端点。Model3D.nsfwLevel是从缩略图 Image 的nsfwLevel派生的批处理结果详见 docs/model3d-thumbnail-nsfw-propagation.md没有手动 setter对比之下 Model 实体有toggleCannotPublish/toggleCannotPromote等站长操作。lockedProperties: string[]字段存在upsert 服务对非站长会剥离被锁字段——站长理论上可经trpc.model3d.upsert设置这些字段但没有 UI 且对lockedProperties不幂等。Strike 系统零改动可用EntityType.Model3D在枚举中UserStrike.entityType是同一EntityTypecreateStrike只是透传entityType、没有逐类型 switch——UI 调用trpc.strike.create带entityType: Model3D即生效。申诉只有 ImageAppeal.entityType虽用同一枚举但createEntityAppealHandlerreport.controller.ts是硬编码 switchdefault分支抛Entity type not supported for appeals。mod-actions技能无 Model3D 模块.claude/skills/mod-actions/ 下当时只有content.mjsmodels 查询/flag/restore等脚本没有 3D 模型命令也没有/moderator/model3ds页面且Model3DModAction只暴露 unpublish。3.2 Phase 1举报队列表面~2 h在 report.controller.ts 的getReportsHandler中select块补两个连接文档给出的目标形状model3d: { select: { model3d: { select: { id: true, name: true, nsfw: true, tosViolation: true, thumbnailImage: { select: { id: true, url: true, name: true } }, user: { select: simpleUserSelect }, }}, }, }, model3dReview: { select: { model3dReview: { select: { id: true, model3dId: true, rating: true, nsfw: true, tosViolation: true, user: { select: simpleUserSelect }, }}, }, },items.map投影补model3d: item.model3d?.model3d, model3dReview: item.model3dReview?.model3dReview扩展getReportLinkmodel3d→/3d-models/${report.model3d.id}model3dReview→/3d-models/${report.model3dReview.model3dId}/reviews冒烟测试对现有 Model3D 提一条举报打开站长报告队列切到Model3d分段选项卡upperFirst(model3d)为Model3d大小写无坑。3.3 Phase 2集中式举报流~1.5 h围绕 ReportModal.tsx 与 report.service.tsavailableFor数组NSFW 表单复用ArticleNsfwForm组Model3D 的 NSFW 是内容级的不像 Image 那样有 per-tag 投票TOSViolation / AdminAttention / Spam 各数组加入ReportEntity.Model3D守卫useVoteForTags调用其入参类型是entityType: image | model对ReportEntity.Model3D直接 no-op 或传undefined——这是最省的路径R9onSuccessswitch 增加 Model3D 分支失效trpc.model3d.getByIdtrpc.model3d.getInfinite仓库中现已可见该分支ReportModal.tsx#L296-L308queryUtils.model3d.getById.setData(...)后getInfinite.invalidate()另有Model3DReview分支失效reviews.getInfinite用openReportModal({ entityType: ReportEntity.Model3D, entityId: model3d.id })替换详情页 TODO 块在createReport的副作用 switch 中补 Model3D 分支NSFW 举报 →nsfw: true镜像 Post 的tx.post.update写法TOS → 无 engagement 表跳过CSAM → 仅记录3D 没有ingestion列直接路径的去留trpc.model3d.reports.createForModel与集中式重复R6建议保留但把 UI 调用方全部切到trpc.report.create或审计后删除。3.4 Phase 3内容处置端点~2 h新增 mod-only 端点目标签名文档的 Gaps that need new endpoints 表Procedure签名目的trpc.model3d.moderation.setNsfwLevel({ id, nsfwLevel, lock? }) → Model3D覆盖缩略图派生等级lock时把nsfwLevel追加进lockedPropertiestrpc.model3d.moderation.toggleFlag({ id, field: tosViolation \| poi \ minor \| nsfw \| unlisted }) → Model3D单端点翻转布尔字段并锁定该字段trpc.model3d.moderation.restore({ id }) → Model3D站长恢复status: Deleted → Unpublished清除deletedAttrpc.report.createAppealswitch case{ entityType: Model3D, entityId }扩展createEntityAppealHandler实施分三步在 model3d.schema.ts 加setModel3DNsfwLevelSchema/toggleModel3DFlagSchema在 model3d.service.ts 实现setModel3DNsfwLevel({ id, nsfwLevel, user })mod-only锁nsfwLevel与toggleModel3DFlag({ id, field, user })翻转布尔、锁字段、刷新userContentOverviewCache在 model3d.router.ts 挂moderation子路由——仓库中现已存在model3d.router.ts#L89-L98setNsfwLevel/toggleFlag/restore三个moderatorProceduremutation经 model3d.router.ts#L223 的moderation: moderationRouter暴露不走 feature flag站长恒有权限。UI 侧把Model3DModAction当时只有 unpublish扩展为下拉菜单Unpublish / Delete / Toggle NSFW / Toggle TOSViolation / Set NSFW Level / 打开站长视图或为详情页建专用的Model3DModBar /仅currentUser?.isModerator可见。3.5 Phase 4Strike 申诉~1 hStrike 无需改码schema 与服务均实体类型无关Phase 3 落地后直接用mod-actions技能调trpc.strike.createentityType: Model3D, entityId验证即可。申诉加 case在createEntityAppealHandler的 switch 中补文档给出的实现case EntityType.Model3D: const m3d await dbRead.model3D.findUnique({ where: { id: input.entityId }, select: { userId: true }, }); if (!m3d) throw throwNotFoundError(3D model not found); if (m3d.userId ! userId) throw throwAuthorizationError(); break;仓库中该分支已落地report.controller.ts#L85。之后在 Model3D 详情页对被站长下架/删除状态展示 Appeal CTA流程镜像 Image 的申诉组件。3.6 Phase 5–6mod-actions 技能与可选站长页Phase 5~1.5 h新增 model3ds.mjs该文件现已存在命令集listmod 可见全部状态、get id、unpublish id/delete id/restore id、set-nsfw-level id --level n/toggle-tos id/toggle-poi id、files id同步更新 SKILL.md 的文件表。同目录的 reports.mjs、strikes.mjs、content.mjs 提供了既有脚本布局参照。Phase 6可选~3 h/moderator/model3ds一站式控制台——文档明确建议defer除非站长提出需求因为报告队列 缩略图入口 详情页 mod bar 已覆盖多数场景。3.7 M2 风险与开放问题R6–R12R6 双路径分叉集中式进队列且有副作用直接式两者都没有必须废弃其一或强同步。R7 队列 where 过滤where: { [type]: { isNot: null } }依赖 Prisma 关系名与枚举值同名。已确认Report.model3d/Report.model3dReview关系与model3d/model3dReview枚举值匹配getAll查询无需特判。R8 批处理覆盖风险最隐蔽的一条Model3D.nsfwLevel由缩略图批处理派生站长用setNsfwLevel覆盖后下一次批处理会把覆盖值冲掉——必须在updateModel3DNsfwLevels批处理中检查lockedProperties.includes(nsfwLevel)并跳过已锁字段派生机制详见 docs/model3d-thumbnail-nsfw-propagation.md。R9useVoteForTags类型是image | modelModel3D 需 guard 或扩类型最省路径是 guard。R10 API-key 鉴权mod-actions脚本以站长 API key 鉴权新端点是 mod-gated需确认 Bearer token 的 tRPC 会话形状填充了ctx.user.isModeratorcontent.mjs已以同样方式调moderator.models.*预期无碍。R11 CSAM 流程csam.mjs.claude/skills/mod-actions/csam.mjs预期 Image 形状载荷Model3D 若涉 CSAM其缩略图 Image 大概率已走既有 image-CSAM 路径v1 建议举报缩略图 Image 删除 Model3D。R12 tag 级 NSFW 投票addTagVotes的 switch 只认 image/modelModel3D 的 tag 级举报 v1 跳过。开放问题是否需要介于 Unpublished 与 Deleted 之间的 Hidden 状态区分站长强制下架与作者主动下架文档倾向不新增状态改用StrikeReason.ManualModAction的 Strike 记录作为审计痕迹。四、工时估算与推荐实施顺序文档给出的估算M1 约 5–7 h缓存管道 2–3 h、页面导航 2–3 h、打磨 1 hM2 约 10–13 h队列 2 h、集中式流 1.5 h、处置端点UI 2–3 h、申诉Strike 1 h、技能 1.5 h可选站长页 3 h总计 ~15–20 h不含可选页。推荐的交错顺序先解锁价值最大、风险最小的项M2 Phase 1——getReportsselect getReportLink解锁详情页 TODO极低风险站长立即可用M2 Phase 2——ReportModal走trpc.report.create举报闭环端到端完成M1 Phase 1——model3dCount缓存徽章有意义的前置解锁个人页 TabM1 Phases 2–4——个人页 Tab 导航 打磨M2 Phase 3——处置端点 UI超越 unpublish 的真实处置面M2 Phase 4——申诉 switch Strike 验证M2 Phase 5——mod-actions技能命令依赖 Phase 3 端点故排后M2 Phase 6——独立/moderator/model3ds页仅在站长要求后做。五、计划与当前仓库的落地对照从当前仓库源码可以确认该计划的绝大部分已实现ProfileNavigation.tsx#L78-L84 的3d-models导航项IconCubemodel3dCount flag/ban 门控、caches.ts#L760-L804 的三个 Model3D 计数缓存、user/[username]/3d-models.tsx 页面、model3d.router.ts#L89-L98 的moderation子路由、ReportModal.tsx 中availableFor各数组的 Model3D 项与onSuccess失效分支、report.controller.ts 申诉 switch 的EntityType.Model3Dcase、以及 model3ds.mjs 技能脚本。剩余需要持续关注的点集中在风险清单的 R8批处理对lockedProperties的尊重与 R6双举报路径去留可作为后续维护的检查锚点。六、延伸阅读3D Models 的完整上下文可配合以下文档阅读总体规划 docs/3d-models-plan.md、实施跟踪 docs/3d-models-implementation-tracker.md、实体图 docs/3d-models-diagram.md、阶段总结 docs/3d-models-summary.md以及缩略图 NSFW 派生机制 docs/model3d-thumbnail-nsfw-propagation.md。【免费下载链接】civitaiA repository of models, textual inversions, and more项目地址: https://gitcode.com/GitHub_Trending/ci/civitai创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考