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,其核心思路有三条:
- 单一 baseQuery:所有请求共用同一个自定义基础查询函数
dynamicBaseQuery,统一处理鉴权头、令牌刷新、401 会话过期等横切逻辑; - OpenAPI 驱动的类型系统:后端 FastAPI 自动生成 OpenAPI Schema,前端用
openapi-typescript把它翻译成 TS 类型,存入schema.ts,全工程共享; - 端点按业务域拆分:
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; },令牌从localStorage的auth_token读取。
2.2 401 与会话过期
当携带令牌的请求返回 401 时,说明令牌失效或过期。代码通过shouldEndSessionForUnauthorized(token)做双重校验:
- 仅当确实发送过令牌时才触发登出(未认证请求如页面加载期的
client_state查询不应导致登出); - 仅当该令牌仍是当前活跃令牌时才登出(慢请求不能把已经替换掉它的新会话登出)。
满足条件后调用api.dispatch(sessionExpiredLogout())。
2.3 滑动窗口令牌刷新
后端通过响应头X-Refreshed-Token返回刷新后的令牌,前端检测到后调用acceptRefreshedToken()提交新令牌:
- 先检查节流(
isTokenRefreshThrottled)与世代校验(shouldAcceptRefreshedToken,防止旧请求覆盖新令牌); - 在跨标签页互斥锁(
runWithMediaAuthLock)内同步媒体 Cookie:POST /api/v1/auth/media-cookie,并设置MEDIA_COOKIE_SYNC_TIMEOUT_MS超时,避免黑洞请求卡死所有标签页的登录/登出; - Cookie 同步是 best-effort:服务器拒绝(401/403)则不提交令牌;网络失败也不阻塞令牌提交,因为令牌会过期而 Cookie 能自愈(配合
useMediaCookieRefresh与下次刷新头重试); - 通过后
markTokenRefreshAccepted()并dispatch(tokenRefreshed(refreshedToken))。
acceptRefreshedToken的注释特别说明:该函数同时被dynamicBaseQuery与客户端状态持久化驱动共用——后者绕过 RTK Query 直接用原生fetch,若不共享此逻辑,持久化会话(例如整天在画布上调整)会硬性过期。
2.4 媒体相关请求串行化
changesMediaCookie(登录、登出、media-cookie请求)会进入runWithMediaAuthLock(execute),保证涉及 Cookie 变更的请求在不同标签页间串行执行,避免竞态。
三、tagTypes 与缓存失效设计
RTK Query 的缓存失效通过tagTypes实现。index.ts 中定义了约 70 个标签类型,覆盖:
- 资源类:
Board、Image、ImageList、ImageMetadata、ImageWorkflow、Workflow、StylePreset、ModelConfig、Video、GalleryItemList等; - 状态类:
SessionQueueItem、SessionQueueStatus、InvocationCacheStatus、BatchStatus、ClientState; - 特殊标签:
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 序列化查询参数,保证对象参数顺序变化不产生缓存键抖动;customCreateApi由buildCreateApi组合coreModule与reactHooksModule构造,并统一注入lruMemoize选择器(createLruSelector),优化大规模列表数据的选择器性能。
四、端点组织:injectEndpoints 与 URL 构建器
所有业务端点按域拆分为独立文件,见 endpoints/ 目录:appInfo.ts、auth.ts、boards.ts、images.ts、queue.ts、models.ts、workflows.ts、videos.ts、stylePresets.ts、systemPrompts.ts、clientState.ts、customNodes.ts、virtual_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入队后会使CurrentSessionQueueItem、NextSessionQueueItem、QueueCountsByDestination、SessionQueueItemIdList及LIST/LIST_ALL标签全部失效(见 endpoints/queue.ts),并在onQueryStarted中乐观更新队列状态草稿(updateQueryData('getQueueStatus', ...),把in_progress与total加上本次入队数量)。
端点文件底部统一导出useGetXxxQuery/useXxxMutation钩子,供组件直接调用,例如useGetAppVersionQuery、useGetRuntimeConfigQuery、useGetOpenAPISchemaQuery等。
五、OpenAPI 驱动的 TypeScript 类型生成管线
这是本 README 的重头戏。类型体系分两层:
- 自动生成层:schema.ts(约 5 万行)由后端 OpenAPI Schema 直接生成,导出
paths与components; - 手写辅助层:types.ts 基于
schema.ts派生业务类型别名,并用 zod 做运行时校验(assert<Equals<...>>()保证手写类型与生成类型结构一致,例如ImageDTO与S['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):
- 二进制文件上传字段 →
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; MetadataField→Record<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出具体型号配置,如FLUXModelConfig、FLUX2ModelConfig、VAEModelConfig、ControlNetModelConfig、LoRAModelConfig、T5EncoderModelConfig等,并配套一系列类型守卫(isVAEModelConfig、isControlLayerModelConfig、isSelfContainedSDNQPipeline等),供 UI 按模型能力做条件渲染。
七、Graph 执行工具:runGraph
除了声明式端点,run-graph.ts 还提供命令式工具runGraph:给定一个Graph与outputNodeId,它会自动完成"入队(runs=1)→ 订阅queue_item_status_changedSocket 事件 → 轮询队列项 → 提取指定节点输出"的完整链路,返回{ session, output }。
工程要点(均有源码注释佐证):
- Origin 先行:监听器在入队之前就注册,通过
getPrefixedId(graph.id)生成的origin过滤无关事件,规避"快速任务在 enqueue 返回前已完成、事件被错过导致永久等待"的竞态; - 结算互斥:
settle()用async-mutex保证 Promise 只 settle 一次,并统一做监听器/定时器清理; - 选项:
destination、prepend、timeout(毫秒,超时会尽力cancelQueueItem并抛SessionTimeoutError)、signal(AbortSignal,中止时抛SessionAbortedError); - 限制:包含
iterate节点的图会抛IterateNodeFoundInGraphError,因为迭代节点会让单个节点产生多个输出; - 错误体系:
SessionFailedError(含error_type/error_message/error_traceback)、SessionCanceledError、OutputNodeNotFoundInCompletedSessionError、ResultNotFoundInCompletedSessionError等,错误类均携带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 提供
getListImagesUrl、getListVideosUrl、getListGalleryItemsUrl,用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,如
useSelectedBoard、useBoardAccess、useDebouncedMetadata、useIsRefinerAvailable等。
九、小结:这套架构带来的工程收益
InvokeAI 前端 API 层以"RTK Query + OpenAPI 类型生成"双引擎驱动,核心收益可以归结为三点:
- 单一事实来源:后端 FastAPI 的 OpenAPI Schema 是唯一类型真相,
make frontend-typegen一键同步到前端,assert<Equals<...>>()在编译期兜底,杜绝手写接口类型的漂移; - 横切逻辑集中:Bearer 令牌附加、滑动窗口刷新、跨标签页 Cookie 同步、401 会话过期等复杂逻辑全部收敛在
dynamicBaseQuery一处,业务端点只声明 URL 与标签; - 声明式缓存:约 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),仅供参考