civitai 审核查询移植全景清单:主应用到@civitai/db-queries的收敛(Convergence)与 Net-new 实战指南
【免费下载链接】civitaiA repository of models, textual inversions, and more项目地址: https://gitcode.com/GitHub_Trending/ci/civitai
导读
本文以仓库中的 docs/db-queries-moderation-port-checklist.md 为骨架,系统梳理 civitai 主应用(main app)尚未迁入共享包@civitai/db-queries的全部审核(moderation)数据库查询:一部分是已移植域在主应用中的"孪生实现"(Convergence,收敛项),另一部分是审核应用完全没有覆盖、需要全新写入共享包的能力(Net-new,全新项)。读完本文,你将掌握:主应用与共享查询包之间的边界划分规则、19 个已移植域与剩余待办域的一一对应关系、14 个 Net-new 域的取舍理由与优先级,以及 Prisma → Kysely 移植过程中被EXPLAIN测试网捕获的真实坑位(含strike.service.ts的FOR UPDATE聚合陷阱等源码级证据)。
一、背景:为什么需要一份"审核查询移植清单"
1.1 共享数据访问层@civitai/db-queries
@civitai/db-queries是 monorepo 的共享数据访问层:以类型化的 Kysely 查询函数形式承载跨应用的数据库读写,主应用与各 SvelteKit 分支(spoke)共用同一套查询。Prisma 并不消失,而是降级为schema 来源、迁移工具与类型生成器(通过 prisma-kysely 生成@civitai/db-schema/kysely的DB类型),查询 API 全部由 Kysely 承担。该包的完整设计与迁移计划见 docs/db-queries-kysely-plan.md,作者约定与测试规范见 packages/civitai-db-queries/README.md。
1.2 现状:moderator 应用已先行,主应用只消费了一个函数
审核应用(moderator app)自身的查询已经完成移植,共 19 个域(reports、users、model3d、cosmetics、mod-activity、comics、ingestion、blocklist、image-rating-review、image-review、image-moderation、image-moderation-effects、image-tags、articles、article-rating-review、scanner、tags-on-image、sidebar-counts、rewards),约 60 个查询函数,每个都配有编译 SQL + DB 支撑的EXPLAIN测试,以子路径@civitai/db-queries/<domain>导出。
而主应用侧目前只有report.service.ts一个消费点——bulkSetReportStatus委托共享包的setReportStatusMany。清单中bulkSetReportStatus一项已打勾([x]),其余全部是主应用本地实现,等待收敛或新写入。
1.3 黄金规则:纯查询进包,副作用留应用
这是整个移植工作的第一原则,清单开头原文即强调:
许多"convergence"项是签名分叉(signature-divergent)的:主应用函数在包暴露的窄原语之上叠加了多步编排(索引/缓存/申诉/通知等副作用)——收敛意味着在主应用侧基于包原语重组编排,而不是 1:1 互换。纯查询留在包内,副作用留在应用(与 moderator 移植采用的规则一致)。
从源码看这条规则的落地:bulkSetReportStatus(src/server/services/report.service.ts)先以findMany读取目标报告、再用$transaction批量更新状态,随后仍在应用侧完成trackModReports(审核行为审计)与reportAcceptedReward(举报奖励发放)等副作用——包内的setReportStatusMany只负责那一条原子UPDATE … WHERE id IN (…) RETURNING。
二、Part A — 收敛项:18 个已移植域的孪生实现
收敛项的含义:该域在包中已有共享查询,主应用里存在同一能力的本地版本,目标是用共享实现替换本地实现(必要时重组编排)。以下按主文档顺序逐域展开,标注来源服务文件与包内对应原语。
2.1 reports(services/report.service.ts)
| 主应用函数 | 包内对应 | 备注 |
|---|---|---|
getReports | 包getReports | 审核队列读取 |
getReportById/getReportByIds | — | 批量 id 读取 |
updateReportById(notes/status) | 包updateReportNotes/setReportStatus | 状态转移窄原语 |
updateImageReportStatusByReason(在image.service.ts) | — | 按原因批量更新图片报告 |
createReport | 尚未入包 | 含 Report + 各类型 join + ImageRatingRequest / tag-vote / article 分支,是净新增工作 |
bulkSetReportStatus | setReportStatusMany | ✅已委托(唯一已收敛项) |
2.2 image-review(services/image.service.ts)
getImageModerationReviewQueue:主应用的"巨石"队列查询,内含 needsReview / tagReview / reportReview 三个 CTE。注意翻译注记中的警告:其tagReview分支(TagsOnImageNew.attributes位掩码 CTE)在包内所有 image-review 函数中均无对应实现,收敛时必须补移植。getImageModerationCounts:按桶 + reported 的计数,与包内getImageReviewCounts语义不同(见下文调和注记)。getImagesModRules/bustImageModRulesCache→ 包getModerationRuleDefinitions。getModeratorPOITags:POI 标签汇总,包内无精确孪生,接近 Net-new。
2.3 image-moderation(services/image.service.ts,基于包原语重组)
| 主应用函数 | 包原语 |
|---|---|
handleBlockImages | setImageBlocked(phash/通知/mod-activity 副作用留应用) |
handleUnblockImages | setImageAccepted(申诉/索引/标签副作用留应用) |
moderateImages | 调度器(dispatcher),自身不迁移,重组调用 |
updateNsfwLevel | recomputeImageNsfwLevel |
updateImageNsfwLevel(6779) | setImageNsfwLevel |
updateImageAcceptableMinor、toggleImageFlag/updateImagesFlag | — |
reportCsamImages(image 分支) | 报告分支归 reports 域 |
resolveEntityAppealimage 分支 | setImageAppealStatus/-Restored/-Rejected、getPendingImageAppealAppellants |
2.4 image-moderation-effects(services/image.service.ts)
- phash 的 ClickHouse 写入:
addBlockedImage/bulkAddBlockedImages/bulkRemoveBlockedImages——大概率留在应用侧(ClickHouse 不在包章程范围内)。 - 搜索索引/缓存扇出辅助函数(
queueImageSearchIndexUpdate):只是副作用辅助,不迁移。
2.5 image-tags(services/tag.service.ts、services/image-review.service.ts)
getTagsForReview→ 包getImageTagReviewQueue/getImageTagReviewTags。moderateTags、disableTags:标签禁用核心。注意disableTags的 model 分支引用了一个TagsOnModels上不存在的disabled列(源实现自身就带TODO.fix,是死分支),移植时如实照搬并在注释中标注。
2.6 tags-on-image(services/tagsOnImageNew.service.ts)
insertTagsOnImageNew/upsertTagsOnImageNew/deleteTagsOnImageNew:存储过程写入 →upsert_tag_on_image。applyTagRules/getTagRules。
2.7 blocklist(services/blocklist.service.ts)
upsertBlocklist、getBlocklistDTO/getBlocklistData、removeBlocklistItems。- 执法读取:
throwOnBlockedLinkDomain、throwOnBlockedMessagePattern、getBlockedEmailDomains、stripBenignPhrases。 - 边界澄清:
moderation-utils.ts中的 blocklist 是Redis 支撑而非 DB 表,不属于移植目标——清单特意标注,防止把 Redis 逻辑误当 PG 查询迁入。
2.8 ingestion(services/image.service.ts、jobs/image-ingestion.ts)
getImagesPendingIngestion、getIngestionErrorImages、resolveIngestionError、getIngestionResults。ingestImages作业的读写(JobQueue 驱动)。
2.9 scanner(services/scanner-audit.service.ts)
recordImageScan→scanner_label_results表 → 包upsertScannerLabelVerdict/insertScannerContentSnapshot。
2.10 image-rating-review(services/image.service.ts)
getImageRatingRequests:包getImageRatingRequests的主应用孪生。
2.11 articles(services/article.service.ts、nsfwLevels.service.ts)
unpublishArticleById、restoreArticleById、getModeratorArticles、rescanArticle。recomputeArticleIngestion/recomputeArticleIngestionInTx:这两处源实现特意用原生$executeRaw绕过@updatedAt自增——Kysely 移植版天然规避(类型化 builder 的set中不含updatedAt)。updateArticleNsfwLevels。articleModerationAdapter:EntityModeration 管线钩子,与 Net-new #5 联动。
2.12 article-rating-review(services/article.service.ts、services/article-rating-review.helpers.ts)
getArticleRatingReviews、getArticleRatingReviewCounts、resolveArticleRatingReview、createArticleRatingReview、getArticleRatingReviewForOwner。- 辅助函数:
computeArticleDerivedNsfwLevel、evaluateAutoApproveGate、autoResolveArticleRatingReview、maybeAutoResolveDisputeAfterScan。
2.13 model3d(services/model3d.service.ts、nsfwLevels.service.ts)
unpublishModel3D、deleteModel3D、restoreModel3D、setModel3DNsfwLevel、toggleModel3DFlag。updateModel3DNsfwLevels/updateModel3DNsfwLevelForThumbnailImage(后者在翻译注记中有一个 CTE 优化:直接以thumbnailImageId过滤,替代"先读 id 再IN(...)"的两趟往返)。- 范围确认待办:
model3d-review.service.ts(用户评价)与model3d-report.service.ts(用户举报)是否属于已移植的 "model3d" 域,需要与包维护者确认。
2.14 comics(routers/comics.router.ts内联实现)
setTosViolation、setProjectNsfwLevel、setChapterNsfwLevel、getModReviewQueue、moderatorUnpublishChapter。updateComicChapterNsfwLevels/updateComicProjectNsfwLevels/updateComicNsfwLevels(ForImage)。- 关键前置:comic 审核逻辑内联在
comics.router.ts里(没有comic.service.ts),必须先抽取为 service 函数再移植(见"提取后再移植"原则)。
2.15 cosmetics(services/cosmetic.service.ts)
grantCosmeticsToUsers+grantCosmetics→ 包grantCosmeticsToUsers/insertUserCosmeticGrant。getPaginatedCosmetics已移植,只需替换调用方。
2.16 mod-activity(services/moderator.service.ts)
trackModActivity→ 包recordModActivity。调用方分布在 report / image 服务与 entity-moderation 作业中。
2.17 rewards(部分,services/rewards-bonus-event.service.ts)
getGlobalRewardsBonus✅ 已移植。- 扩展项:
getActiveRewardsBonusEvent+RewardsBonusEventCRUD(见 Net-new #12)。
2.18 sidebar-counts(部分)
- 包内已有 report / appeal / tag-review 计数,但主应用仪表盘计数不同。需调和:
getImageModerationCounts、CSAM 的getCsamReportStats、申诉的getAppealCount与包内计数对齐。
三、Part B — 14 个 Net-new 域:moderator 应用没有对应物
这些是共享包完全未覆盖、需全新写入的能力。按主文档编号逐一说明。
#1 model / modelVersion 审核(services/model.service.ts、model-version.service.ts)
unpublishModelById、unpublishModelVersionById、deleteModelById、restoreModelById、permaDeleteModelById(事务组合器)、toggleLockModel、toggleLockComments、setModelsCategory;poi/nsfw/minor/sfwOnly +lockedProperties经upsertModel/updateModelById;getTrainingModelsForModerators;updateModelNsfwLevels/updateModelVersionNsfwLevels(nsfwLevels.service.ts)。
#2 model-flag 自动扫描队列(services/model-flag.service.ts、model-file-scan.service.ts)
upsertModelFlag、getFlaggedModels、resolveFlaggedModel、unpublishBlockedModel。注意:源实现upsertModelFlag的原生 upsert SQL缺少逗号(无法执行),移植时已修正,保证每个扫描列都能回写;且ModelFlag.sfwOnly列在schema.prisma和生成的 Kysely 类型中存在,但线上表尚无该列(未应用的迁移),EXPLAIN因此失败,移植版已移除该列写入(isFlagged决策仍使用该字段),待迁移落地后恢复。
#3 collection 审核(services/collection.service.ts、nsfwLevels.service.ts)
updateCollectionItemsStatus、setCollectionItemNsfwLevel、updateCollectionsNsfwLevels。
#4 post 审核(services/post.service.ts、nsfwLevels.service.ts)
deletePost(mod 路径,事务组合器)、updatePostNsfwLevel/updatePostNsfwLevels。
#5 entity / 文本审核(XGuard / Clavata)(services/entity-moderation.service.ts、jobs/entity-moderation.ts)
upsertEntityModerationPending、recordEntityModerationSuccess、recordEntityModerationFailure、getEntityModerationWithImageNsfwLevel。- 作业编排:
runModQueue、runModChat、clearAutomatedReports、autoMuteIfScamAccount(+ReportAutomated、JobQueue)。 - 决策点:主应用已有
ModerationAdapter注册表(src/server/services/moderation-adapters.ts,目前注册 Article / Challenge / Model / WildcardSetCategory 四个适配器,getModerationAdapter(entityType)按实体类型取用),需决定包是否将其标准化。适配器接口定义在 src/server/services/entity-moderation.service.ts,核心是resolveContent(ids)(批量取文本内容)与submit({entityId, content})(提交审核,失败时写 EM 行为 Failed 供重试作业拾取)。
#6 申诉生命周期(超出图片状态)(services/report.service.ts)
createEntityAppeal、getRecentAppealsByUserId、getAppealCount、getAppealDetails、resolveEntityAppeal(非 image 分支)。
#7 用户审核 / 执法(services/user.service.ts、jobs/confirm-mutes.ts)
toggleBan、toggleContestBan、setUserMuted、setUserModerator、updateUserById。softDeleteUser(CSAM)、deleteUser、removeAllContent(约 15 张表的清空)。bulkUnpublishModelsForBannedUser、setLeaderboardEligibility。getUsers(mod 查找,含 banned/muted/deleted 状态、原生前缀搜索)——与包内searchUsers需要先核实 Meili 还是 SQL再决定取舍。confirmMutes作业。
#8 strike / 惩罚系统(services/strike.service.ts)——纯 Net-new,raw-SQL 重
- 读:
shouldRateLimitStrike、getActiveStrikePoints、getStrikeSummary、getStrikesForUser、getStrikeHistoryForMod、getStrikesForMod、getUserStandings(动态 SQL)。 - 写:
evaluateStrikeEscalation(FOR UPDATE事务)、createStrike、voidStrike、expireStrikes、processTimedUnmutes。
源码证据表明该域为何是 raw-SQL 重灾区:evaluateStrikeEscalation(src/server/services/strike.service.ts)在源实现中直接用SELECT SUM(points) … FOR UPDATE,而 Postgres 明确拒绝聚合查询带FOR UPDATE(错误码 0A000),移植版把行锁下推进WITH locked AS MATERIALIZED (SELECT points … FOR UPDATE) SELECT SUM(points) FROM locked子查询——MATERIALIZED是关键,内联 CTE 会把两个层级塌缩回一个。该函数还演示了正确的边界划分:点数和禁言状态写入是原子单元(锁必须与写入在同一事务内),而通知、会话失效等 I/O 全部留在事务外。
#9 用户限制(生成执法)(routers/user-restriction.router.ts、orchestrator/promptAuditing.ts)
getAll(mod 队列)、resolve(uphold/overturn)、submitContext(PromptAllowlist upsert)、backfillTriggers。auditPromptServer(拦截提示词 → 自动禁言 + UserRestriction)、getCachedPromptAllowlist/bustPromptAllowlistCache。
#10 CSAM / NCMEC(services/csam.service.ts+csam.service-new.ts)
createCsamReport、createExternalCsamReport、uploadExternalCsamEvidence。getCsamReportsPaged、getCsamReportStats、getCsamsToReport/getCsamsToArchive/getCsamsToRemoveContent。processCsamReport、archiveCsamDataForReport、getUserIpInfo(ClickHouse + PG 混合)、getImageResources。- 先调和再移植:两个 CSAM 实现需先合并(
csam.service-new.ts是超集,旧csam.service.ts是其严格子集),翻译注记明确 CSAM 以-new.ts为移植源。
#11 装扮撤销 / 定向发放(services/cosmetic.service.ts)
assignCosmeticByTarget(collection/userIds,支持 dry-run)、unassignCosmetic、equipCosmeticToEntity/unequipCosmetic。
#12 RewardsBonusEvent CRUD(services/rewards-bonus-event.service.ts)
getActiveRewardsBonusEvent、upsertRewardsBonusEvent、deleteRewardsBonusEvent、getRewardsBonusEventById、getRewardsBonusEventsPaged。
#13 knights "降级" 复核(services/image.service.ts)
getDownleveledImages(ClickHouseknights_new_order_downleveled+ PG join)+addToNewOrderQueue。
#14 扫描结果摄取管线内部(services/image-scan-result.service.ts)
resolveScanOutcome、auditScanResults、markImageScanError、blockImageFromRating、getAssociatedEntities、evaluateImageModRules、isExemptFromAiVerification、processTags。
四、排序原则与执行策略(Sequencing notes)
主文档给出的四条推进纪律,是执行这份清单时避免返工的关键:
- ClickHouse 一律排除在包外(按当前包章程):phash
blocked_images、降级复核(down-leveled review)、getUserIpInfo的 CH 读取、prohibited-request 计数等均只移植 PG 部分,CH 部分按 moderator 移植先例标注留在应用。这也与 docs/db-queries-kysely-plan.md 的"Out of scope — ClickHouse (deliberately not ported)"一致:@civitai/db-queries是 Postgres/Kysely-only。 - 先调和再移植(Reconcile-before-port):两个 CSAM 实现(
csam.service.tsvs-new.ts)与 Meili-vs-SQL 用户搜索,必须在移植前拍板以哪个为准。 - 先提取再移植(Extract-before-port):comic 审核内联在
comics.router.ts,先抽出 service 函数再动查询。 - 最高 Net-new 价值排序:strike 系统、用户 ban/mute/delete 执法、CSAM/NCMEC、entity/文本审核——这四类体积大、raw-SQL 密集,且今天没有共享等价物,值得优先投入。
五、移植工程底座:Prisma → Kysely 的翻译纪律与验证体系
清单落地所依托的翻译纪律与测试体系(完整记录见 docs/db-queries-moderation-translation-notes.md 与 packages/civitai-db-queries/README.md),是理解清单各条目"为什么这么列"的底层背景。
5.1EXPLAIN测试网捕获的源实现缺陷
每个移植查询都配有 DB 支撑的EXPLAIN测试(对实时 schema 解析+规划、不执行),借此发现了多处"源实现本身就是坏的"问题——这些问题与迁移无关,主应用也值得修复:
| 位置 | 源问题 | 移植处理 |
|---|---|---|
strike.service.tsevaluateStrikeEscalation | SUM(points) … FOR UPDATE被 Postgres 拒绝 | 行锁下推子查询,外层求和,锁意图一致 |
user.service.tsgetUsers | ORDER BY LENGTH(username)夹在两个WHERE AND之间(query 设置时 SQL 非法);头像读取用了未加引号的i.nsfwLevel(折叠成不存在的nsfwlevel)且以文本'None'默认值对 int 列 | ORDER BY移到WHERE之后;加引号"nsfwLevel"且默认值改0 |
user.service.tsremoveAllContent | Prisma{ OR: [...] }实际编译为单个 AND 谓词(只清自己) | 忠实照搬并注释标注 |
model-flag.service.tsupsertModelFlag | 原生 upsert SQL 缺逗号,无法执行 | 修正,保证列完整回写 |
ModelFlag.sfwOnly | schema 与类型有、线上表无此列 | 移除写入,isFlagged仍用,注释待迁移恢复 |
tag.service.tsdisableTags(model 分支) | 引用不存在的disabled列,死分支 | 忠实移植并注释标注,排除出 EXPLAIN |
5.2 语义翻译选择(行为等价,非字节一致)
典型如cannotPublish过滤器从 Prisma 的{ not: { path: ['cannotPublish'], equals: true } }译为meta -> 'cannotPublish' IS DISTINCT FROM 'true'::jsonb(null/false 均视为"可发布",与 Prisma 对 null 键的NOT(x = true)略有差异);toggleImageFlag把源实现的"读后取反写"两条语句折叠为一条无竞态的SET flag = NOT flag;updateModel3DNsfwLevelForThumbnailImage以thumbnailImageId直接过滤 CTE,省一次往返。@updatedAt是系统性问题:Prisma 客户端自动 bump,Kysely 不 bump 且无 DB 触发器——Prisma-builder 类 UPDATE 显式写updatedAt,raw-SQL 类 UPDATE 刻意不写(源 raw SQL 也不写)。
5.3 收敛调和注记(Convergence reconcile)
主应用函数与已移植的 moderator 版本语义分叉的 5 处,采用"不重复、先调和"策略:
getImageReviewCounts(包)vsgetImageModerationCounts(主应用):后者计appeal(无摄取门禁)且追加 reported-image 的 UNION,语义不同。getImageReviewQueue(包)vsgetImageModerationReviewQueue(巨石):monolith 的tagReview分支无对应物,需补移植。getArticlesForModeration(包)vsgetModeratorArticles(主应用):后者更丰富(id游标分页 + 更宽嵌套 select),未移植。unpublishModel3d(包)vsunpublishModel3D(主应用):写入核心相同,保留已有实现。getComicReviewQueue(包)vsgetModReviewQueue(主应用):谓词相同,仅结果形状不同(router 返回 Prisma 嵌套对象,移植版返回扁平行)。
5.4 三层测试体系
- 编译 SQL 测试(必需、免 DB):断言函数编译出的精确 SQL 与参数,用离线 harness(
src/test/harness.ts的compileHarness(),基于 KyselyDummyDriver)——能捕获IN ()空数组守卫缺失、where丢失、set乱序等回归。执行:pnpm --filter @civitai/db-queries test。 - 行为 + 执行计划测试(热路径必需、需活库):
explainHarness()对编译 SQL 做EXPLAIN(不 ANALYZE),列/join/类型/存储过程签名解析失败会在此暴露;环境门控(TEST_DATABASE_URL或本地根.env的DATABASE_URL),无库时describe.skipIf(!h.hasDb)跳过。 - Prisma-vs-Kysely 行为等价(移植自 Prisma 的热路径):双实现跑同一批行、深比较结果,防谓词丢失;位于消费应用内
<civitai>/src/server/db/__tests__/kysely-prisma-parity.test.ts,通过KYSELY_PARITY_DATABASE_URL开启且刻意不回退DATABASE_URL(它要写 fixture,需要一次性数据库)。
正确性硬规则还包括:in ([])前的空数组守卫(Kysely 编译出IN ()是语法错误,Prisma 静默 no-op);jsonb 写入必须toJson()(node-postgres 会把 JS 数组误序列化成 Postgres 数组字面量);Postgres 枚举数组列需注册registerEnumArrayTypeParsers(动态 oid 无默认解析器,否则读出{a,b}字面量)。事务组合器(如unpublishModelById、permaDeleteModelById、deletePost、unpublishBlockedModel、setModelsCategory、autoResolveArticleRatingReview)无法在离线 harness 上EXPLAIN,只对其组成语句函数做编译与 EXPLAIN,组合器本身保持薄封装。
六、相关文档导航
- docs/db-queries-moderation-port-checklist.md —— 本文主体,主应用剩余审核查询的全量清单(Convergence + Net-new)。
- docs/db-queries-moderation-translation-notes.md —— Prisma→Kysely 翻译偏差逐条记录(按风险分组)。
- docs/db-queries-kysely-plan.md —— 包的整体迁移计划:执行器注入、事务组合、lag 感知、分阶段推进。
- packages/civitai-db-queries/README.md —— 包结构与作者约定(命名、签名、jsonb/
@updatedAt/枚举数组处理、三层测试)。 - docs/moderator-app-package-boundary.md —— moderator 应用包边界分析,"纯查询入包、副作用留应用"规则的由来(读队列自持、跨图写入按动作选 Redis 缓存失效或代理回主应用)。
- 源码参照:src/server/services/report.service.ts(已收敛的
bulkSetReportStatus)、src/server/services/strike.service.ts(FOR UPDATE聚合陷阱)、src/server/services/moderation-adapters.ts(实体审核适配器注册表)、packages/civitai-db-queries/src/model.db.ts(包内查询函数的作者风格范例,含空数组守卫与枚举类型说明)。
这份清单的执行边界清晰:包负责类型化、可共享、可EXPLAIN验证的纯查询;主应用保留编排、缓存、通知、ClickHouse、计费等一切副作用。按"ClickHouse 排除 → 先调和 → 先提取 → 高价值优先"四条纪律推进,即可逐步把主应用审核查询全部收敛到@civitai/db-queries,与已移植的 19 个 moderator 域最终合流。
【免费下载链接】civitaiA repository of models, textual inversions, and more项目地址: https://gitcode.com/GitHub_Trending/ci/civitai
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考