InvokeAI 前端 API 客户端架构解析:RTK Query 与 OpenAPI 类型生成管线
2026/9/10 17:40:05 网站建设 项目流程

InvokeAI 前端 API 客户端架构解析:RTK Query 与 OpenAPI 类型生成管线

【免费下载链接】InvokeAIInvoke is a leading creative engine for Stable Diffusion models, empowering professionals, artists, and enthusiasts to generate and create visual media using the latest AI-driven technologies. The solution offers an industry leading WebUI, and serves as the foundation for multiple commercial products.项目地址: https://gitcode.com/GitHub_Trending/in/InvokeAI

本文以 InvokeAI 前端src/services/api目录为核心,剖析其基于 Redux Toolkit Query(RTK Query)构建的 API 客户端架构,以及从后端 OpenAPI Schema 自动生成 TypeScript 类型的完整管线。读者读完后将掌握dynamicBaseQuery的自定义认证与令牌刷新机制、tagTypes缓存失效设计、make frontend-typegen/pnpm typegen的类型生成流程,并能直接复用到自己的 FastAPI + React 项目中。

一、整体架构:标准 RTK Query 的工程化落地

InvokeAI 的 WebUI 前端(基于 React + Redux)通过一套"相当标准的 Redux Toolkit Query 设置"与后端通信。官方说明见 API README,其核心思路有三条:

  1. 单一 baseQuery:所有请求共用同一个自定义基础查询函数dynamicBaseQuery,统一处理鉴权头、令牌刷新、401 会话过期等横切逻辑;
  2. OpenAPI 驱动的类型系统:后端 FastAPI 自动生成 OpenAPI Schema,前端用openapi-typescript把它翻译成 TS 类型,存入schema.ts,全工程共享;
  3. 端点按业务域拆分endpoints/目录下每个文件对应一个后端路由模块(queue、images、boards、models 等),用api.injectEndpoints()注册。

这套架构的入口文件是 index.ts,它同时扮演了"baseQuery 定义者"与"API 实例创建者"两个角色。

二、dynamicBaseQuery:认证、令牌刷新与特殊请求处理

dynamicBaseQuery是全部 HTTP 请求的中枢。它以BaseQueryFn<string | FetchArgs, unknown, FetchBaseQueryError>类型定义(见 index.ts),在fetchBaseQuery之上包装了多层增强逻辑。

2.1 请求分类:OpenAPI 与认证端点

const isOpenAPIRequest = (args instanceof Object && args.url.includes('openapi.json')) || (typeof args === 'string' && args.includes('openapi.json')); const isAuthEndpoint = (args instanceof Object && typeof args.url === 'string' && (args.url.includes('/auth/login') || args.url.includes('/auth/setup'))) || (typeof args === 'string' && (args.includes('/auth/login') || args.includes('/auth/setup')));
  • OpenAPI 请求:拉取/openapi.json时,Schema 中可能存在循环引用(Circular Reference),因此 baseQuery 为这类请求单独注入jsonReplacer: getCircularReplacer(),把循环引用替换为字符串'[Circular]'(实现见 index.ts);
  • 认证端点/auth/login/auth/setup请求不携带Authorization头,其余所有请求都会在prepareHeaders中自动附加Bearer令牌:
prepareHeaders: (headers) => { if (token && !isAuthEndpoint) { headers.set('Authorization', `Bearer ${token}`); } return headers; },

令牌从localStorageauth_token读取。

2.2 401 与会话过期

当携带令牌的请求返回 401 时,说明令牌失效或过期。代码通过shouldEndSessionForUnauthorized(token)做双重校验:

  • 仅当确实发送过令牌时才触发登出(未认证请求如页面加载期的client_state查询不应导致登出);
  • 仅当该令牌仍是当前活跃令牌时才登出(慢请求不能把已经替换掉它的新会话登出)。

满足条件后调用api.dispatch(sessionExpiredLogout())

2.3 滑动窗口令牌刷新

后端通过响应头X-Refreshed-Token返回刷新后的令牌,前端检测到后调用acceptRefreshedToken()提交新令牌:

  1. 先检查节流(isTokenRefreshThrottled)与世代校验(shouldAcceptRefreshedToken,防止旧请求覆盖新令牌);
  2. 跨标签页互斥锁runWithMediaAuthLock)内同步媒体 Cookie:POST /api/v1/auth/media-cookie,并设置MEDIA_COOKIE_SYNC_TIMEOUT_MS超时,避免黑洞请求卡死所有标签页的登录/登出;
  3. Cookie 同步是 best-effort:服务器拒绝(401/403)则不提交令牌;网络失败也不阻塞令牌提交,因为令牌会过期而 Cookie 能自愈(配合useMediaCookieRefresh与下次刷新头重试);
  4. 通过后markTokenRefreshAccepted()dispatch(tokenRefreshed(refreshedToken))

acceptRefreshedToken的注释特别说明:该函数同时被dynamicBaseQuery与客户端状态持久化驱动共用——后者绕过 RTK Query 直接用原生fetch,若不共享此逻辑,持久化会话(例如整天在画布上调整)会硬性过期。

2.4 媒体相关请求串行化

changesMediaCookie(登录、登出、media-cookie请求)会进入runWithMediaAuthLock(execute),保证涉及 Cookie 变更的请求在不同标签页间串行执行,避免竞态。

三、tagTypes 与缓存失效设计

RTK Query 的缓存失效通过tagTypes实现。index.ts 中定义了约 70 个标签类型,覆盖:

  • 资源类BoardImageImageListImageMetadataImageWorkflowWorkflowStylePresetModelConfigVideoGalleryItemList等;
  • 状态类SessionQueueItemSessionQueueStatusInvocationCacheStatusBatchStatusClientState
  • 特殊标签
    • LIST_TAG = 'LIST'LIST_ALL_TAG = 'LIST_ALL':用于分页列表的整体失效(配合LIST_ALL_TAG可无差别刷新全部分页);
    • FetchOnReconnect:注释明确说明"重连时失效,用于数据会变化、尤其是队列与生成相关的查询",与api实例的refetchOnReconnect: true配合;
    • Schema:OpenAPI Schema 查询专用。
export const api = customCreateApi({ baseQuery: dynamicBaseQuery, reducerPath: 'api', tagTypes, endpoints: () => ({}), invalidationBehavior: 'immediately', serializeQueryArgs: stableHash, refetchOnReconnect: true, });

值得注意的工程细节:

  • invalidationBehavior: 'immediately'让标签失效立即触发重取,而不是等当前请求完成后才执行;
  • serializeQueryArgs: stableHash用 stable-hash 序列化查询参数,保证对象参数顺序变化不产生缓存键抖动;
  • customCreateApibuildCreateApi组合coreModulereactHooksModule构造,并统一注入lruMemoize选择器(createLruSelector),优化大规模列表数据的选择器性能。

四、端点组织:injectEndpoints 与 URL 构建器

所有业务端点按域拆分为独立文件,见 endpoints/ 目录:appInfo.tsauth.tsboards.tsimages.tsqueue.tsmodels.tsworkflows.tsvideos.tsstylePresets.tssystemPrompts.tsclientState.tscustomNodes.tsvirtual_boards.ts等。

每个文件用api.injectEndpoints({...})扩展 API 实例,并通过buildV1Url/buildV2Url构造 URL:

export const buildV1Url = (path: string, query?: Parameters<typeof queryString.stringify>[0]): string => { if (!query) return `api/v1/${path}`; return `api/v1/${path}?${queryString.stringify(query)}`; }; export const buildV2Url = (path: string): string => `api/v2/${path}`;

典型示例是 appInfo.ts 中的getAppVersion查询:

export const appInfoApi = api.injectEndpoints({ endpoints: (build) => ({ getAppVersion: build.query<AppVersion, void>({ query: () => ({ url: buildAppInfoUrl('version'), method: 'GET' }), providesTags: ['FetchOnReconnect'], }), // ... }), });

每个端点都显式声明providesTags/invalidatesTags。例如queueApi.enqueueBatch入队后会使CurrentSessionQueueItemNextSessionQueueItemQueueCountsByDestinationSessionQueueItemIdListLIST/LIST_ALL标签全部失效(见 endpoints/queue.ts),并在onQueryStarted中乐观更新队列状态草稿(updateQueryData('getQueueStatus', ...),把in_progresstotal加上本次入队数量)。

端点文件底部统一导出useGetXxxQuery/useXxxMutation钩子,供组件直接调用,例如useGetAppVersionQueryuseGetRuntimeConfigQueryuseGetOpenAPISchemaQuery等。

五、OpenAPI 驱动的 TypeScript 类型生成管线

这是本 README 的重头戏。类型体系分两层:

  • 自动生成层:schema.ts(约 5 万行)由后端 OpenAPI Schema 直接生成,导出pathscomponents
  • 手写辅助层:types.ts 基于schema.ts派生业务类型别名,并用 zod 做运行时校验(assert<Equals<...>>()保证手写类型与生成类型结构一致,例如ImageDTOS['ImageDTO']tsafe相等断言)。

5.1 生成工具链

类型生成由两个脚本协作完成:

环节脚本职责
后端导出scripts/generate_openapi_schema.py导入 FastAPI 应用,调用自定义 OpenAPI 函数输出 Schema JSON 到 stdout
前端翻译invokeai/frontend/web/scripts/typegen.js调用openapi-typescript把 Schema 翻译为 TS 类型,写入schema.ts

Python 侧的关键逻辑(generate_openapi_schema.py):

def main(): repo_root = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) os.chdir(repo_root) # 确保 sys.path 指向仓库根,从本地源码导入 invokeai, # 避免多工作树 editable install 下拾取缺少 invocation 模块的命名空间包 if sys.path[0] != repo_root: sys.path.insert(0, repo_root) from invokeai.app.api_app import app from invokeai.app.util.custom_openapi import get_openapi_func schema = get_openapi_func(app)() json.dump(schema, sys.stdout, indent=2)

即:从invokeai.app.api_app拿到 FastAPIapp实例,用invokeai.app.util.custom_openapi.get_openapi_func生成带全部 invocation 注册的 OpenAPI Schema(自定义函数而非 FastAPI 默认app.openapi(),这样才能覆盖所有动态注册的节点/路由),以缩进 JSON 打印到标准输出。

5.2 两种运行方式

方式一:make 一键生成(推荐)

make frontend-typegen

该 target 的完整定义在 Makefile:

frontend-typegen: cd invokeai/frontend/web && python ../../../scripts/generate_openapi_schema.py | pnpm typegen

即把 Python 脚本的 stdout管道pnpm typegen(对应package.json中的"typegen": "node scripts/typegen.js")。前提是已激活 venv(Python 侧能 importinvokeai)且前端依赖已安装。

方式二:先起后端服务,再单独运行

pnpm typegen

此时typegen.js检测到process.stdin不是 TTY,会直接请求http://127.0.0.1:9090/openapi.json(见 typegen.js)。因此需要先以 9090 端口启动 InvokeAI 后端服务,再执行该命令。typegen.js 也支持传入参数:node scripts/typegen.js <openapi.json>,或从 stdin 读取 JSON。

5.3 typegen.js 的类型转换细节

typegen.js在调用openapiTS时配置了两个重要的transform规则(见 typegen.js):

  1. 二进制文件上传字段 →Blob:FastAPI 在 0.129 之前输出format: binary,0.130 起改为 OpenAPI 3.1 的contentMediaType: application/octet-stream,两者都必须映射为Blob,否则上传调用点会静默地把File参数类型推断成string
    const isBinary = ('format' in schemaObject && schemaObject.format === 'binary') || ('contentMediaType' in schemaObject && schemaObject.contentMediaType === 'application/octet-stream'); if (isBinary) return schemaObject.nullable ? ts.factory.createUnionTypeNode([BLOB, NULL]) : BLOB;
  2. MetadataFieldRecord<string, unknown>:该字段默认被推断为Record<string, never>,但实际接受任意合法 JSON 值的字典,因此用 TS AST 工厂节点覆盖为RECORD_STRING_UNKNOWN

此外,脚本还对生成结果做后处理:openapi-typescript有时会从判别联合中的const用法反推枚举类型,丢掉只在部分联合成员中出现的值。脚本遍历 Schema 的components.schemas,对每个type === 'string'且有enum的定义,用正则把生成输出中的TypeName: ...;替换为 Schema 中声明的完整联合类型(typegen.js)。

5.4 schema.ts 的仓库同步与 CI 校验

schema.ts提交进仓库的产物,并且有 CI 检查保证它与当前后端 Schema 一致——这意味着:

  • 后端新增/修改路由或 invocation 字段后,必须重新运行make frontend-typegen并提交更新的schema.ts
  • 类型定义以提交的schema.ts为准,前端开发者无需先起后端即可获得全部接口类型提示。

六、types.ts:生成类型之上的业务封装

schema.ts 是机械产物,业务代码更常引用 types.ts 中精心命名的别名,例如:

export type S = components['schemas']; export type ListImagesArgs = NonNullable<paths['/api/v1/images/']['get']['parameters']['query']>; export type ListImagesResponse = paths['/api/v1/images/']['get']['responses']['200']['content']['application/json']; export type ImageDTO = z.infer<typeof _zImageDTO>;

ImageDTO先定义 zod schema(_zImageDTO),再z.infer出类型,并用assert<Equals<ImageDTO, S['ImageDTO']>>()在编译期断言它与 OpenAPI 生成类型完全相等(types.ts)——这是防止前后端类型漂移的双保险。

types.ts 还提供大量判别式模型类型,从AnyModelConfig(OpenAPI 判别联合)中Extract出具体型号配置,如FLUXModelConfigFLUX2ModelConfigVAEModelConfigControlNetModelConfigLoRAModelConfigT5EncoderModelConfig等,并配套一系列类型守卫(isVAEModelConfigisControlLayerModelConfigisSelfContainedSDNQPipeline等),供 UI 按模型能力做条件渲染。

七、Graph 执行工具:runGraph

除了声明式端点,run-graph.ts 还提供命令式工具runGraph:给定一个GraphoutputNodeId,它会自动完成"入队(runs=1)→ 订阅queue_item_status_changedSocket 事件 → 轮询队列项 → 提取指定节点输出"的完整链路,返回{ session, output }

工程要点(均有源码注释佐证):

  • Origin 先行:监听器在入队之前就注册,通过getPrefixedId(graph.id)生成的origin过滤无关事件,规避"快速任务在 enqueue 返回前已完成、事件被错过导致永久等待"的竞态;
  • 结算互斥settle()async-mutex保证 Promise 只 settle 一次,并统一做监听器/定时器清理;
  • 选项destinationprependtimeout(毫秒,超时会尽力cancelQueueItem并抛SessionTimeoutError)、signal(AbortSignal,中止时抛SessionAbortedError);
  • 限制:包含iterate节点的图会抛IterateNodeFoundInGraphError,因为迭代节点会让单个节点产生多个输出;
  • 错误体系SessionFailedError(含error_type/error_message/error_traceback)、SessionCanceledErrorOutputNodeNotFoundInCompletedSessionErrorResultNotFoundInCompletedSessionError等,错误类均携带queueItemId便于排查;
  • 依赖注入buildRunGraphDependencies(dispatch, socket)用 Redux dispatch 调用queueApi端点、用 Socket.IO 订阅队列事件,便于测试替换。

典型用法(摘自源码 JSDoc 示例):

const dependencies = buildRunGraphDependencies(store, socket); const graph = new Graph(); const outputNode = graph.addNode({ id: 'my-resize-node', type: 'img_resize', image: { image_name: 'my-image.png' }, }); const controller = new AbortController(); const result = await runGraph({ graph, outputNodeId: outputNode.id, dependencies, prepend: true, signal: controller.signal, }); controller.abort(); // 取消操作

八、配套工具与测试

  • URL 辅助:util.ts 提供getListImagesUrlgetListVideosUrlgetListGalleryItemsUrl,用queryString.stringify(..., { arrayFormat: 'none' })序列化查询参数,并同时充当 RTK Query 的缓存键;
  • 乐观更新:util/optimisticUpdates.ts 实现 mutation 的乐观 UI 更新;
  • 标签失效辅助:util/tagInvalidation.ts 提供组合式失效标签;
  • 测试:run-graph.test.ts、types.test.ts 覆盖图执行与类型守卫;endpoints/auth.test.ts、endpoints/images.test.ts 覆盖认证与图片端点;
  • Hooks:hooks/ 目录存放基于 API 数据派生的业务 Hook,如useSelectedBoarduseBoardAccessuseDebouncedMetadatauseIsRefinerAvailable等。

九、小结:这套架构带来的工程收益

InvokeAI 前端 API 层以"RTK Query + OpenAPI 类型生成"双引擎驱动,核心收益可以归结为三点:

  1. 单一事实来源:后端 FastAPI 的 OpenAPI Schema 是唯一类型真相,make frontend-typegen一键同步到前端,assert<Equals<...>>()在编译期兜底,杜绝手写接口类型的漂移;
  2. 横切逻辑集中:Bearer 令牌附加、滑动窗口刷新、跨标签页 Cookie 同步、401 会话过期等复杂逻辑全部收敛在dynamicBaseQuery一处,业务端点只声明 URL 与标签;
  3. 声明式缓存:约 70 个tagTypes让资源变更(图片、画板、队列、模型)自动触发精确的缓存失效,配合FetchOnReconnect与乐观更新,WebUI 的图库、队列面板和节点编辑器得以保持实时一致。

对于任何"FastAPI 后端 + React 前端"的项目,这套"OpenAPI 管道生成 + 统一 baseQuery + 标签化缓存"的组合都是可直接借鉴的高质量范本。

【免费下载链接】InvokeAIInvoke is a leading creative engine for Stable Diffusion models, empowering professionals, artists, and enthusiasts to generate and create visual media using the latest AI-driven technologies. The solution offers an industry leading WebUI, and serves as the foundation for multiple commercial products.项目地址: https://gitcode.com/GitHub_Trending/in/InvokeAI

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询