Civitai 图片检索输入类型体系剖析:GetAllImagesInput 与 ImageSearchInput 的逐字段对比与用户上下文扁平化
Civitai 图片检索输入类型体系剖析GetAllImagesInput 与 ImageSearchInput 的逐字段对比与用户上下文扁平化【免费下载链接】civitaiA repository of models, textual inversions, and more项目地址: https://gitcode.com/GitHub_Trending/ci/civitai本文基于 docs/type-comparison.md 展开系统梳理 Civitai 主应用图片无限流image feed两条核心输入类型——GetAllImagesInput与ImageSearchInput——的继承关系、字段差异与调用链路上的转换逻辑。读者在读完本文后将能准确理解完整会话用户对象与扁平化搜索字段两种设计的分界掌握getAllImagesIndex内部如何解析游标、抽取user.id与user.isModerator并转发给 Meilisearch 搜索路径以及 event-engine-common 中ImageQueryInput与主应用类型之间尚存的兼容性缺口。为什么需要一份类型对比文档在 Civitai 的图片检索架构中一条无限滚动图片流请求会跨越多个层tRPC / REST 控制器层接收原始查询参数getAllImagesIndex作为聚合入口携带完整会话对象usergetImagesFromSearch等搜索函数只接收扁平化的标量字段currentUserId、isModerator不再持有整个会话对象。这两套输入类型的字段面几乎重合但用户这一维度的承载方式截然不同。如果不把类型继承关系显式固化下来开发者很容易在某一层误用userId创建者过滤与currentUserId当前浏览者——正如源码注释中强调的passing the wrong one turns this into a check that always passes传错字段会让权限检查永远通过。本文要做的就是沿着 image.service.ts 的真实定义把这条类型链逐层拆开。类型层级全景四层继承链文档给出的类型关系可以用一张继承链概括对应源码中 image.service.ts 与 image.service.ts 的实际定义baseQuerySchema基座 └─ GetInfiniteImagesOutput图片无限流查询输出/输入最大字段集 └─ GetAllImagesInput增加会话用户与请求上下文 └─ ImageSearchInput增加扁平化用户字段与游标分页字段基座baseQuerySchemabaseQuerySchema { browsingLevel: number (default: allBrowsingLevelsFlag) }browsingLevel是 NSFW 浏览级别的位标志bit flag默认值allBrowsingLevelsFlag由 event-engine-common 中的常量组合而来image-feed-types.tsexport const sfwBrowsingLevelsFlag NsfwLevel.PG | NsfwLevel.PG13; export const nsfwBrowsingLevelsFlag NsfwLevel.R | NsfwLevel.X | NsfwLevel.XXX; export const allBrowsingLevelsFlag sfwBrowsingLevelsFlag | nsfwBrowsingLevelsFlag;其中NsfwLevel是一个数值枚举PG1, PG132, R4, X8, XXX16, Blocked32位运算设计使多个级别可以合并进单个number这也是整个类型体系中几乎所有过滤条件得以高效传递的基础。第一层扩展GetInfiniteImagesOutputGetInfiniteImagesOutput在基座上叠加了两组字段。第一组来自imagesQueryParamSchema常规查询参数字段类型说明baseModels?BaseModel[]基础模型过滤collectionId?/collectionTagId?number集合/集合标签过滤hideAutoResources?/hideManualResources?boolean隐藏自动/手动关联资源followed?boolean仅看关注对象fromPlatform?boolean仅看平台内容hidden?boolean仅看隐藏内容limitnumber每页数量min: 0, max: 200默认galleryFilterDefaults.limitmodelId?/modelVersionId?number模型/模型版本过滤notPublished?boolean仅看未发布内容periodMetricTimeframe时间窗口默认galleryFilterDefaults.periodperiodMode?PeriodMode时间窗口模式postId?number帖子过滤prioritizedUserIds?number[]优先展示的用户reactions?ReviewReactions[]按反应类型过滤scheduled?boolean仅看定时发布内容sortImageSort排序方式默认galleryFilterDefaults.sorttags?/techniques?/tools?number[]标签/技法/工具过滤types?MediaType[]媒体类型image/video/audiouseIndex?boolean是否走索引userId?/username?number/string按创建者过滤withMetaboolean是否带元数据默认falserequiringMeta?boolean仅看必须有元数据的第二组是额外字段包括游标分页、内容审核与排除类条件cursor?: bigint | number | string | Date—— 游标用于续页excludedTagIds?/excludedUserIds?—— 排除指定标签/用户generation?: ImageGenerationProcess[]—— 生成过程类型ids?/imageId?/postIds?/reviewId?—— 按 ID 直接查询include: ImageInclude[]默认[cosmetics]—— 控制服务端返回时附带哪些富化数据如cosmetics、tags、tagIds、profilePictures、metaSelect等includeBaseModel?、pending?、skip?、withTags?—— 各类开关混排/去重控制remixOfId?、remixesOnly?、nonRemixesOnly?POI/未成年内容控制disablePoi?、disableMinor?以及仅审核员可用的poiOnly?、minorOnly?。第二层扩展GetAllImagesInputtype GetAllImagesInput GetInfiniteImagesOutput { useCombinedNsfwLevel?: boolean; user?: SessionUser; // ← FULL USER OBJECT domain?: DomainColor; // 请求来源颜色用于选择 New Upcoming 面板 headers?: Recordstring, string; // 请求头TODO: 是否必需待定 dbTarget?: read | write | datapacket; // 走读库/写库/数据包副本 signal?: AbortSignal; actor?: string; // 调用者身份经 buildSearchActor() 构造后传给 Meili X-Search-Actor };与GetInfiniteImagesOutput相比GetAllImagesInput的关键新增是user?: SessionUser—— 完整会话用户对象内含id、isModerator、username、email、permissions等useLogicalReplica—— 文档描述为必需字段当前源码中该字段名已演化为dbTarget?: read | write | datapacket默认read读者应以当前实现为准domain、headers、signal、actor等请求上下文。这一层是全量上下文边界凡是需要完整用户信息如enforceBlockedBrowsingTags需要user.username的逻辑都在这一层或更上层完成。第三层扩展ImageSearchInputtype ImageSearchInput GetInfiniteImagesOutput { useCombinedNsfwLevel?: boolean; domain?: DomainColor; currentUserId?: number; // ← EXTRACTED FROM user?.id isModerator?: boolean; // ← EXTRACTED FROM user?.isModerator offset?: number; // ← FOR CURSOR PAGINATION entry?: number; // ← FOR CURSOR PAGINATION blockedFor?: string[]; // ← ADDITIONAL FILTER signal?: AbortSignal; actor?: string; };注意ImageSearchInput在源码中也是从GetInfiniteImagesOutput直接扩展而非从GetAllImagesInput扩展但语义上它是经过getAllImagesIndex组装后的搜索输入即文档所述ImageSearchInput GetAllImagesInput 扁平化字段的意图。它不再携带user对象而是把用户信息降维成两个标量。关键差异谁多谁少GetAllImagesInput 独有、ImageSearchInput 没有的字段没有。ImageSearchInput继承了GetAllImagesInput的全部字段并在语义上额外拥有扁平化字段因此不存在前者有、后者无的字段。ImageSearchInput 独有、GetAllImagesInput 没有的字段currentUserId?: number—— 从user?.id抽取的当前浏览者 IDisModerator?: boolean—— 从user?.isModerator抽取的审核员标志offset?: number—— 游标解析出的偏移量entry?: number—— 游标解析出的条目时间戳blockedFor?: string[]—— 额外的屏蔽原因过滤如tos、moderated、CSAM等BlockedReason见 image-feed-types.ts。用户处理的本质差异完整对象 vs 扁平化字段这是两个类型之间最关键的设计分界GetAllImagesInput持有user?: SessionUser即完整的会话对象id、isModerator、username、email、permissions、emailVerified、createdAt等ImageSearchInput只接收两个派生标量currentUserId来自user?.id与isModerator来自user?.isModerator。这种降维有明确的工程动机源码注释给出了两个直接理由防止 PII 泄漏进日志。getInfiniteImagesHandler会把整个ctx.user展开进搜索输入用于业务逻辑如果不剥离每次搜索报错都会把email、emailVerified、username、createdAt写进日志——生产环境一度每天产生 33.1 万条携带真实账号信息的记录。为此源码专门实现了redactSearchInputForLogimage.service.ts把user对象整体丢弃而非逐个删除已知 PII 键因为黑名单会在下次新增字段时失效同时保留currentUserId与isModerator这两个搜索路径真正用到的字段。避免userId语义冲突。userId在搜索输入中已经是按创建者过滤的查询条件若把user.id再映射到同名键会覆盖掉真实的查询条件、污染日志与过滤逻辑。转换流程getAllImagesIndex 中的拆解与组装文档描述的转换发生在getAllImagesIndex当前源码位于 image.service.ts。其核心步骤const { include, user } input; // 1. 取出会话用户 const cursorParsed input.cursor?.toString().split(|); // 2. 解析游标 const offset isNumber(cursorParsed?.[0]) ? Number(cursorParsed?.[0]) : 0; const entry isNumber(cursorParsed?.[1]) ? Number(cursorParsed?.[1]) : undefined; const currentUserId user?.id; // 3. 抽取用户 ID const userId input.userId ?? (input.username ? await getUserIdByUsername(input.username) : undefined); const searchInput { ...input, // 4. 展开全部查询字段 userId, currentUserId, // ← 扁平化user?.id isModerator: user?.isModerator, // ← 扁平化user?.isModerator offset, // ← 游标解析出的偏移量 entry, // ← 游标解析出的条目时间戳 };值得补充的两个源码细节游标格式cursor采用offset|entryTimestamp形式例如500|1724677401898——前半是偏移量后半是条目时间戳毫秒。解析失败时offset回退为0、entry为undefinedimage.service.ts。组装后的分发searchInput会先尝试feedPrimary路径由 Flipt 特性开关FEED_SERVICE_PRIMARY控制按currentUserId或anonymous分桶不可用时才真正调用getImagesFromSearch(searchInput)image.service.ts。若 Meilisearch 瞬时过载会把可重试的瞬时错误重新归类为TRPCError SERVICE_UNAVAILABLEHTTP 503而非 408/500避免请求在事件循环上堆积。对测试的影响moderator 场景的测试难点由于真实实现从会话/认证中取user再抽取user.id与user.isModerator因此真实实现路径测试必须能构造携带user.isModerator true的真实认证会话测试端点路径若测试端点只接受isModerator、currentUserId作为查询参数则无法真正验证 moderator 功能除非满足二者之一接受带有user.isModerator true的真实认证会话或在内部为开发/测试目的 mock 用户对象。这意味着围绕审核权限的测试如notPublished、poiOnly、minorOnly、pendingReviewOnly等审核专属过滤条件不能只靠传个参数来覆盖而必须穿透到会话层或 mock 层。仓库中已有不少针对该链路的测试可以佐证这一结论例如image-infinite-wire.test.ts —— 验证无限流数据结构image.controller.feed-source.test.ts —— 验证 feed 数据源选择image-search.client-ip.test.ts 与 image-search-username-type.test.ts —— 覆盖搜索路径的边界条件。另外授权逻辑本身也在服务层做了防呆设计canRequestUnpublishedimage.service.ts区分创建者被浏览对象targetUserId与当前浏览者currentUserId非审核员只有在请求已明确限定到本人时才允许查看未发布内容缺失targetUserId时宁可拒绝也不默认放行——这正是文档强调currentUserId与userId不可混淆在权限层的落点。event-engine-common 中尚未覆盖的字段event-engine 的ImageQueryInputimage-feed-types.ts是为事件引擎/feed 服务移植的类型目前已经覆盖了大部分常规过滤排序、NSFW 位标志、用户/内容/资源/元数据/混排/时间/发布状态/审核过滤、include富化选项与enableExistenceCheck特性开关。但与主应用的GetInfiniteImagesOutput相比以下字段尚未在ImageQueryInput中处理分类未处理字段集合类collectionId、collectionTagId资源显示类hideAutoResources、hideManualResources可见性/关注类hidden、followed推荐/互动类prioritizedUserIds、reactions单体查询类imageId、includeBaseModel、pending、reviewId分页类skip附加数据类withTags混排控制类remixesOnly、nonRemixesOnlyPOI/未成年控制disablePoi、disableMinor、poiOnly审核、minorOnly审核这些字段若要在 event-engine 中实现与主应用完全兼容的查询面需要按上面清单逐项补齐到ImageQueryInput。对照可见GetInfiniteImagesOutput是最大公约数ImageQueryInput目前是它的一个真子集——这也是 docs/type-comparison.md 落笔时指出的主要兼容性缺口同理主应用侧ImageSearchInput中被注释掉的prioritizedUserIds、modelId、reviewId等Unhandled字段image.service.ts也从侧面印证类型定义宽于实际查询路径支持是这套体系需要持续对齐的现实。小结把整条链路串起来看baseQuerySchema提供 NSFW 浏览级别基座 →GetInfiniteImagesOutput展开全部查询与富化字段 →GetAllImagesInput挂上完整会话对象与请求上下文 →getAllImagesIndex在入口处解析游标、把user扁平化为currentUserId/isModerator→ImageSearchInput作为纯标量搜索输入进入 Meilisearch / feed 路径。这一设计在业务层需要完整用户上下文与搜索层只需要最小标量、且不能泄漏 PII之间划出了清晰边界。对测试与二次开发而言牢记两点即可少踩坑userId是创建者过滤、currentUserId是浏览者身份二者方向相反审核功能验证必须穿透会话层或显式 mock仅靠查询参数无法覆盖。【免费下载链接】civitaiA repository of models, textual inversions, and more项目地址: https://gitcode.com/GitHub_Trending/ci/civitai创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考