ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

civitai 单体应用拆分实践:Moderator 独立应用的渐进式 Monorepo 方案全解析

civitai 单体应用拆分实践:Moderator 独立应用的渐进式 Monorepo 方案全解析 civitai 单体应用拆分实践Moderator 独立应用的渐进式 Monorepo 方案全解析【免费下载链接】civitaiA repository of models, textual inversions, and more项目地址: https://gitcode.com/GitHub_Trending/ci/civitai本文以 civitai 仓库中的拆分规划文档 monorepo-split-overview.md 为核心完整解析「把 85 个 Moderator 页面与审核 API 路由从主应用剥离为独立应用」这一工程决策从构建耗时与部署膨胀的问题量化到「渐进式 MonorepoApproach A」与「构建时页面排除Approach B」两条路线的对比、admin 路由的逐类归置分析再到共享基础设施下的双容器部署架构。读完本文你可以掌握在拥有 2,500 源文件、367 个页面入口点的 Next.js 单体中如何以最小迁移风险完成按角色拆分部署并能对照仓库现状验证该方案的实际落地形态。一、问题量化367 个入口点中约 23% 与终端用户无关拆分动因来自单应用构建的两个具体痛点构建耗时主应用每次构建都要编译 367 个页面入口点其中约 85 个是 Moderator 页面与 mod API 路由——绝大多数开发者和全部终端用户从不触碰。剥离后 webpack 入口点减少约 23%编译更快。部署膨胀主应用生产包携带了全部 42 个 Moderator 页面、36 个 mod API 路由和 57 个 admin API 路由。用户下载了永远不会执行的代码且内部工具作为主应用的一部分暴露在应用表面上。原文档给出的页面构成如下类别文件数占比LOCModerator 页面4211%9,731Mod API 路由3610%2,805Admin API 路由审核相关~72%~1,500Admin API 路由系统/定时/迁移~5014%~7,265其他23263%—合计367关于/api/admin/路由的重要澄清大部分 admin 路由是系统运维用的 webhook/cron 端点缓存管理、数据迁移、支付处理并非审核工具。真正属于审核动作的只有约 7 个delete-images、unpublish-all-models、rescan-images、cancel-subscription、grant-subscription、deliver-prepaid-buzz、manage-sanity-checks其余应留在主应用。另一个构建层面的痛点tRPC 端点src/pages/api/trpc/[trpc].ts在每次构建时都导入包含全部 78 个 router 的完整appRouter。专门的modRouter可以被条件性排除从而减少服务端 bundle 的编译量。目标主应用部署时不含 Moderator 页面审核工具作为独立应用部署两者的构建时间都要下降。二、为什么不做完整的 Monorepo 重构原文档明确否决了传统意义上的完整 monorepo 拆分Turborepo pnpm workspaces apps//packages/全面落地理由是其代价与收益严重不匹配需要把全部 2,500 源文件搬入 workspace 包需要重写整个代码库中每一条~/导入路径需要把 Prisma、auth、tRPC、UI 抽取为共享包每个活跃分支都会遭遇灾难性的合并冲突。原文档的结论是这是一项以月为单位、风险高、且在完成前没有任何增量价值的工作。而一个渐进式 monorepo——只迁移 Moderator 代码——是可行的。三、Approach A渐进式 Monorepo文档推荐方案只把 Moderator 页面移入 pnpm workspace 内的一个独立 Next.js 应用。主应用留在项目根目录——主应用零文件移动新应用通过~/路径别名从src/导入共享代码。3.1 关键洞察Moderator 页面是依赖图的叶子节点Moderator 页面从src/导入但src/中没有任何模块反向导入它们。单向依赖意味着移动这些页面不会破坏任何既有代码——这是整个方案可行性的理论根基。3.2 目标目录结构civitai/ # 主应用留在原地不变 ├── apps/ │ └── moderator/ # 新增 —— 独立 Next.js 应用 │ ├── package.json │ ├── next.config.mjs │ ├── tsconfig.json # ~/ → ../../src/ │ └── pages/ │ ├── _app.tsx # 从 ../../src 导入 providers 的薄封装 │ ├── _document.tsx │ ├── moderator/ # 从 src/pages/moderator/ 移入 │ └── api/ │ ├── mod/ # 从 src/pages/api/mod/ 移入 │ ├── admin/ # 从 src/pages/api/admin/ 移入 │ ├── auth/ # 复用主应用的 NextAuth 配置 │ ├── trpc/ # 复用主应用的 tRPC handler │ └── user/ # 复用用户设置 API ├── src/ # 不变除移走的页面外 │ ├── pages/ # 主应用页面moderator/ 与 api/mod/ 已移除 │ ├── components/ # 全部共享组件留在这里 │ ├── server/ # 全部共享服务端代码留在这里 │ └── ... ├── package.json # 更新加入 workspace 配置 ├── pnpm-workspace.yaml # 新增 ├── turbo.json # 新增可选用于构建缓存 ├── next.config.mjs # 主应用配置小幅更新 └── Dockerfile.web / .moderator # 独立的 Docker 构建3.3 工作机制pnpm workspaces 管理两个应用。apps/moderator/是一个独立 workspace拥有自己的package.json和next.config.mjs。共享代码留在src/。新应用的tsconfig.json把~/映射到../../src/因此被移动页面里的既有导入一行都不用改next.config.mjs再用 webpack alias 在构建期解析~/。两个应用独立构建pnpm build构建主应用不含 moderator 页面pnpm --filter moderator build构建 moderator 应用。引入 Turborepo 后两者可缓存、可并行。本地开发同样独立pnpm dev跑主应用pnpm --filter moderator dev跑 moderator 应用。3.4 迁移清单与保留清单从到文件数src/pages/moderator/apps/moderator/pages/moderator/42src/pages/api/mod/apps/moderator/pages/api/mod/36src/pages/api/admin/中约 7 个审核相关路由apps/moderator/pages/api/admin/~7合计迁移~85约 50 个系统类 admin 路由cron/迁移/运维端点留在主应用归置依据见第六节。留在src/的审核相关代码src/components/Moderation/ModerationNav.tsx—— 被主应用的AppHeader.tsx导入src/components/Moderation/ImpersonateButton.tsx—— 同样被AppHeader.tsx导入src/server/routers/moderator/—— 注册在根 router 上web 构建中条件性排除其他所有共享代码components、hooks、utils、server、store不变。3.5 合并冲突影响评估约 85 个文件从src/pages/移入apps/moderator/pages/。若其他分支同时修改了这些文件git 会报「ours 删除、theirs 修改」。修复方式直白把对方修改后的版本放到新位置。原文档判断实际风险较低依据有三Moderator 页面是边缘功能触碰它的特性分支很少被移动的目录是叶子层级moderator/、api/mod/、api/admin/不是共享基础设施这是一次性迁移可与团队协调窗口执行。主应用src/侧零导入变更——依赖单向流动mod 页面 → 共享 src删掉页面不会破坏任何东西。3.6 三个已知挑战Next.js 跨目录导入moderator 应用需导入../../src/要求next.config.mjs中配置 webpack alias 以及transpilePackages来处理外部源码。_app.tsx重复moderator 应用需要自己的_app.tsx携带同样的 provider 栈——这是一个从../../src/providers/导入 providers 的薄封装。一个反向依赖的边界情况src/utils/memberships.util.ts导入了src/pages/api/admin/refresh-sessions中的 handler。需要重构把 handler 抽到src/server/让 API 路由和 util 都从新位置导入。3.7 构建收益主应用从 367 降到约282 个页面入口点约少 23%Moderator 应用只编译约90 个页面mod 页面 mod API 路由 约 7 个 admin 路由 共享基础件配合 Turborepo未变更的应用整体跳过重建本地pnpm dev只处理主应用页面。四、Approach B构建时页面排除更轻量的替代如果一次变更量太大Approach B 让全部代码原地不动靠构建脚本在编译期临时排除页面同一个 Git 仓库无结构变化 ├── build:web → 不含 moderator 页面的 Docker 镜像 └── build:moderator → 含 moderator 页面的 Docker 镜像 共享基础件工作流程build:web脚本把src/pages/moderator/和src/pages/api/mod/移到临时备份用 404 兜底页替换执行next build然后恢复原文件。同时设置BUILD_TARGETweb让 tRPC router 条件性排除modRouter。build:moderator反向操作——把非 moderator 页面移出去保留 moderator 页面 共享基础件_app.tsx、_document.tsx、api/auth/、api/trpc/、api/user/构建后恢复。合并冲突风险几乎为零。文件不会永久移动只新增脚本和 Dockerfile。代价本地开发没有任何收益。pnpm dev仍会加载全部 367 个页面——文件搬移技巧只在构建期有效跨开发会话恢复文件太脆弱。构建期收益与 Approach A 相同。以build:web为例移除 135 个页面入口点占总数 37%意味着更少的 webpack 编译——每个页面都是 webpack 独立处理的入口点更小的服务端 bundle——更少代码需要编译、优化、压缩收窄的 tRPC 类型面——条件性排除modRouter使其从AppRouter类型中消失。一个补充事实另外 22 个 router 中散落着各自的moderatorProcedure端点它们会同时保留在两个构建中——开销极小且对非审核员本来就直接返回 FORBIDDEN。五、两种方案的横向对比Approach A渐进式 MonorepoApproach B构建时排除构建期收益有生产 开发都受益有仅生产本地开发收益有pnpm dev跳过 mod 页面无dev 加载全部页面合并冲突风险低约 85 个文件移动但属边缘代码几乎为零结构清晰度高独立应用边界干净低同一代码库脚本驱动复杂度中workspace 配置、webpack 配置低仅构建脚本升级路径本身已是 monorepo可自然扩展日后再做迁移Turborepo 缓存支持不支持六、Admin 路由归置分析/api/admin/路由大部分不是审核工具而是系统运维的 webhook/cron 端点。原文档给出的逐类归置移入 Moderator 应用约 7 个路由——这些是从审核 UI 调用、或实际作为审核工具使用的动作路由用途delete-images批量删图用于内容审核unpublish-all-models批量下架模型封禁后果rescan-images触发图像重扫cancel-subscription作为审核动作取消用户订阅grant-subscription作为审核动作授予会员资格deliver-prepaid-buzz完成预付费 Buzz 发放manage-sanity-checks管理 sanity-check 条目已使用ModEndpoint可能迁移约 3 个路由路由用途备注permission授予/撤销功能权限可算审核工具refresh-sessions强制刷新会话便于权限即时生效users按 ID 查询用户对审核上下文有用留在主应用约 50 个路由类别示例系统/定时任务clean-up-old-notifications、creator-comp-payout、pay-daily-challenge-users缓存管理clear-cache-by-pattern、fetch-cache-by-pattern、purge-cache-tag数据迁移migrate-likes、migrate-metrics、migrate-model-metrics调试工具cache-check、header-check、test外部集成update-freshdesk-customer、add-manual-assignments生成系统orchestrator/index、orchestrator/timings临时/一次性脚本src/pages/api/admin/temp/约 23 个文件七、部署架构双容器共享基础设施两种方案共用同一部署拓扑┌─────────────────┐ │ Load Balancer / │ │ Reverse Proxy │ └────────┬────────┘ ┌──────────────┼──────────────┐ ▼ │ ▼ ┌─────────────────┐ │ ┌─────────────────┐ │ Web Container │ │ │ Mod Container │ │ All pages │ │ │ /moderator/* │ │ EXCEPT │ │ │ /api/mod/* │ │ /moderator/* │ │ │ /api/admin (7) │ │ /api/mod/* │ │ │ /api/auth/* │ └────────┬────────┘ │ │ /api/trpc/* │ │ │ └────────┬────────┘ └───────────────┼─────────────┘ ┌────────┴────────┐ │ Shared Infra │ │ PostgreSQL / Redis / │ Meilisearch / │ ClickHouse / │ S3 / CloudFlare │ └───────────────────┘两个容器共享数据库——同一 PostgreSQL 实例、同一 Prisma schema认证——同一NEXTAUTH_SECRET、同一套会话 cookieRedis——同一缓存、同一会话存储搜索——同一 Meilisearch 实例。因此在主应用登录的用户会自动通过 moderator 应用的认证共享 cookie 域。路由方式二选一路径式更简单civitai.com/moderator/*→ mod 容器其余 → web 容器子域名式mod.civitai.com→ mod 容器civitai.com→ web 容器。两个构建中都保留的内容认证共享 NextAuth 会话两个应用都需要tRPCmoderator 页面会调用共享 tRPC 端点如trpc.image.moderate所以多数 router 在两个构建中保留数据库共享 Prisma schema 与表22 个散落moderatorProcedure端点的 router——开销极小且对非审核员一律返回 FORBIDDEN。八、对照仓库现状方案如何实际落地规划文档提出方案后仓库已沿着其方向演进当前形态可以作为方案 A 的「落地后形态」来验证workspace 骨架已就位pnpm-workspace.yaml 声明了packages: [., packages/*, apps/*]三个 workspace 根turbo.json 定义了build任务dependsOn: ^build输出缓存.next/**与dist/**以及dev的persistent: true配置——正是原文档「用 Turborepo 做构建缓存与并行」的对应物。apps/moderator/已存在但其实际形态是一个SvelteKit Vite 独立应用包名civitai/moderator-app见 apps/moderator/package.json而非文档中设想的「复用src/的 Next.js 应用」。它的依赖全部指向workspace:*共享包civitai/auth、civitai/db、civitai/mod-utils、civitai/moderation、civitai/redis等配套独立的 Dockerfile、独立 Prisma schemaapps/moderator/prisma/以及独立的审核库脚本moderator-db/、clickhouse/、abuse-detection/。从仓库结构看团队最终选择了比文档方案 A 更彻底的形态moderator 不再通过~/别名借用主应用src/而是整体迁移为独立技术栈、依赖共享 workspace 包的自治应用。主应用保留了对 moderator 的开发与发布入口根 package.json 中定义了dev:moderatorpnpm --filter civitai/moderator-app dev与release:moderatornode scripts/release-app.mjs apps/moderator moderator-v patch脚本——对应原文档「两个应用独立 dev、独立构建、独立发布」的设想。仓库中还并行存在apps/auth、apps/creator-studio、apps/storage、apps/event-engine等更多独立应用说明「按角色/职能拆分应用」已被泛化为该仓库的标准工程模式。主应用侧的src/pages/moderator/目录仍然存在含models/、scanner-policies/、csam/、challenges/等子目录并新增了[...slug].tsx动态路由。从源码结构看主应用仍保留相当一部分 moderator 页面与文档中「42 个页面整体移出」的目标之间存在差距结合仓库内 monorepo-conversion-plan.md、monorepo-package-adaptation-plan.md 等后续规划文档可以推断拆分是一个多轮推进的过程本 overview 文档记录的是其中的决策框架与首轮边界划分。这一对照也印证了原文档的一个核心判断渐进式拆分之所以可行正是因为 moderator 代码处于依赖图叶子层——无论最终目标是「Next.js 子应用借用src/」还是「SvelteKit 应用依赖共享 workspace 包」被拆分侧都可以单向消费共享层而不引发反向耦合。九、文档遗留的开放问题原文档末尾列出待评审的五个问题可作为同类拆分项目的决策清单选型渐进式 MonorepoA还是构建时排除B部署路由路径式/moderator/*→ mod 容器还是子域名mod.civitai.commoderator 应用范围只含 mod/admin 页面还是也包含完整应用让审核员一个 URL 用全部功能CI/CD两个容器每次 push 都构建还是仅在相关文件变更时构建功能开关部分 moderator 页面受 feature flag 控制mod 应用是否沿用同一 feature flag 服务十、可复用的工程经验从这份拆分规划中可以提炼出对大型 Next.js 单体通用的三条经验先用数字界定边界367 个入口点、42 页面、36 路由、约 7 个 admin 路由——把「哪些代码与谁无关」量化成表格拆分范围才有据可依admin 路由逐类归置7 个移 / 3 个待定 / 50 个留是这种量化分析的典型样例。优先选择叶子层代码做首轮拆分被移动代码只向外依赖、不被反向导入迁移零破坏、合并冲突面极小而像memberships.util.ts反向导入 API 路由 handler 这类边界情况必须提前识别并先解耦。部署拓扑与代码拓扑分离设计两个容器共享 PostgreSQL、Redis、Meilisearch 与会话 cookie认证天然贯通代码上保留共享 auth/tRPC/Prisma 层。拆分降低的是构建与发布面而非重复基础设施。【免费下载链接】civitaiA repository of models, textual inversions, and more项目地址: https://gitcode.com/GitHub_Trending/ci/civitai创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进