Immich API 体系解析:基于 OpenAPI 的规范自动生成与 TypeScript / Dart 双端 SDK 构建
Immich 采用 OpenAPI 标准作为其 API 的单一事实来源:服务器在开发模式下自动导出 OpenAPI 规范文件,再由 mise open-api 任务链一键生成 TypeScript 与 Dart 两套客户端 SDK。本文围绕 Immich 仓库的 API 文档与配套源码,讲清规范文件从 NestJS 控制器装饰器到 immich-openapi-specs.json 的生成机制、mise open-api 的完整执行链路,以及两套 SDK 的产物位置、定制模板与补丁策略,帮助你在新增或修改端点后正确地重建客户端代码。
核心概念:OpenAPI 是文档与 SDK 的共同源头
Immich 的 API 文档与客户端 SDK 均派生自同一份 OpenAPI 规范,而非各自独立维护。这一设计意味着:
- 文档不手写:公开 API 文档(发布地址为 api.immich.app)由 OpenAPI 规范渲染而来,端点行为变更只需修改服务端控制器,文档与 SDK 随之更新;
- SDK 不手改:TypeScript 与 Dart 客户端均由
open-api/immich-openapi-specs.json生成,任何手工修改都会在下次生成时被覆盖。
从仓库中已提交的规范文件 immich-openapi-specs.json 可以看到,它是一份标准 OpenAPI 3.x 文档,顶层包含 openapi、paths、info、tags、servers、components 六个部分,当前共定义了 191 个 API 路径,涵盖 /activities、/admin/config、/admin/database-backups 等管理端与业务端端点。服务器根前缀为 /api(servers 字段中声明为 /api),因此所有业务端点实际都以 /api/... 暴露。
规范中声明了三种认证机制,这也是调用 Immich API 时最核心的三要素:
| 认证方式 | 位置 | 说明 |
|---|---|---|
| Bearer Token | HTTP Header | 用于 OAuth / JWT 类认证 |
| Cookie(Access Token) | Cookie | Web 端登录态会话 |
API Key(x-api-key) |
HTTP Header | 用于外部集成、CLI 等场景 |
这三项声明来自服务端 Swagger 文档构建器的配置,下一节将展开。
服务端自动生成规范文件:useSwagger 的实现
规范文件并非静态维护,而是由服务器代码在运行时“反射”自身路由后写出的。核心逻辑位于 useSwagger:
export const useSwagger = (app: INestApplication, { write }: { write: boolean }) => {
const builder = new DocumentBuilder()
.setTitle('Immich')
.setDescription('Immich API')
.setVersion(serverVersion.toString())
.addBearerAuth({ type: 'http', scheme: 'Bearer', in: 'header' })
.addCookieAuth(ImmichCookie.AccessToken)
.addApiKey(
{ type: 'apiKey', in: 'header', name: ImmichHeader.ApiKey },
MetadataKey.ApiKeySecurity,
)
.addServer('/api');
for (const [tag, description] of Object.entries(endpointTags)) {
builder.addTag(tag, description);
}
const config = builder.build();
const options: SwaggerDocumentOptions = {
operationIdFactory: (controllerKey: string, methodKey: string) => methodKey,
extraModels,
ignoreGlobalPrefix: true,
};
const specification = SwaggerModule.createDocument(app, config, options);
const openApiDoc = cleanupOpenApiDoc(specification);
// ... SwaggerModule.setup('doc', app, openApiDoc, customOptions);
if (write) {
// Generate API Documentation only in development mode
const outputPath = path.resolve(process.cwd(), '../open-api/immich-openapi-specs.json');
writeFileSync(outputPath, JSON.stringify(patchOpenAPI(openApiDoc), null, 2), { encoding: 'utf8' });
}
};
这里有几个值得注意的实现细节:
- 文档内容来自 NestJS 装饰器:
SwaggerModule.createDocument扫描所有控制器与 DTO 上的@nestjs/swagger装饰器(@ApiTags、@ApiProperty、@ApiResponse等)构建规范。因此“修改规范”的正确姿势就是“修改控制器端点及其装饰器”,官方文档(api.md)也明确这一点:immich-openapi-specs.json由控制器端点使用或引用的@nestjs/swagger装饰器决定其内容。 - operationId 取方法名:
operationIdFactory返回方法键(methodKey),保证生成的 SDK 方法名与控制器方法名一一对应,便于跨端对照源码。 - Swagger UI 挂载与导出:文档在
/api/doc提供交互式界面,同时通过jsonDocumentUrl: '/api/spec.json'与yamlDocumentUrl: '/api/spec.yaml'暴露 JSON/YAML 原始规范(ignoreGlobalPrefix: true保证这些路径不受全局前缀影响)。 write开关:只有write: true时才会把(经过patchOpenAPI二次修补后的)规范序列化写入open-api/immich-openapi-specs.json。
开发模式下的自动写入
在生产/开发共用的应用启动流程 app.common.ts 中,写入行为被严格门控:
useSwagger(app, { write: configRepository.isDev() && permitSwaggerWrite });
即:只有开发模式(isDev())且允许写入时,服务器运行期间才会在内存/磁盘上导出规范。这解释了官方文档中“规范文件由服务器在开发模式下运行时自动生成”的表述——生产部署默认不重写该文件。
独立的 sync-open-api 脚本
除运行中服务器外,仓库还提供了一个一次性同步脚本 sync-open-api.ts:
#!/usr/bin/env node
process.env.DB_URL = 'postgres://postgres:postgres@localhost:5432/immich';
import { NestFactory } from '@nestjs/core';
import { NestExpressApplication } from '@nestjs/platform-express';
import { ApiModule } from 'src/app.module';
import { useSwagger } from 'src/utils/misc';
const sync = async () => {
const app = await NestFactory.create<NestExpressApplication>(ApiModule, { preview: true });
useSwagger(app, { write: true });
await app.close();
};
该脚本用 preview: true(NestJS 预加载模式,不监听端口、不真正启动依赖连接)构建完整 ApiModule,强制 write: true 导出规范后立即关闭应用。顶部的 DB_URL 兜底赋值让脚本无需真实数据库即可构建路由树。编译后的入口为 dist/bin/sync-open-api.js,由 server/mise.toml 中的任务包装:
[tasks."sync-open-api"]
run = "node ./dist/bin/sync-open-api.js"
mise open-api:一键重建双端 SDK 的任务链
官方文档给出的开发流程命令是 mise open-api。它在根目录 mise.toml 中定义为一个任务编排,按依赖顺序串联起整个再生成流水线:
[tasks.open-api]
run = [
{ task = "//:plugins" },
{ task = "//server:install" },
{ task = "//server:build" },
{ task = "//server:sync-open-api" },
{ task = ":open-api-typescript" },
{ task = ":open-api-dart" },
]
执行链路可概括为:构建插件 → 安装并编译服务器 → 用 sync-open-api 刷新 immich-openapi-specs.json → 用 oazapfts 生成 TypeScript 客户端 → 用 OpenAPI Generator 生成 Dart 客户端。前四步保证“规范先行”,后两步保证“双端同步”。
TypeScript 端:oazapfts 生成
[tasks.open-api-typescript]
run = [
"oazapfts --optimistic --argumentStyle=object --useEnumType --allSchemas open-api/immich-openapi-specs.json packages/sdk/src/fetch-client.ts",
{ task = "//:sdk:install" },
{ task = "//:sdk:build" },
]
关键参数含义:
--optimistic:对可能缺失的字段采取乐观类型推断,减少运行时的防御性代码;--argumentStyle=object:多参数端点以单个对象字面量传参(client.assets.get({ id })风格),提升 SDK 可用性;--useEnumType:OpenAPI 枚举映射为 TS 联合字面量类型而非enum;--allSchemas:导出components/schemas中的全部数据模型类型。
生成产物是 packages/sdk/src/fetch-client.ts,文件头明确标注:
/**
* Immich
* 3.2.0-rc.0
* DO NOT MODIFY - This file has been generated using oazapfts.
*/
注意:早期文档表述中 TypeScript SDK 位于 packages/sdk/client,而当前仓库的实际产物路径是 packages/sdk/src/(以本仓库为准)。生成后还会执行 sdk:install(pnpm --filter @immich/sdk install --frozen-lockfile)与 sdk:build 完成安装和构建。
Dart 端:OpenAPI Generator + 模板补丁
[tasks.open-api-dart]
dir = "open-api"
sources = [
"immich-openapi-specs.json",
"openapitools.json",
"templates/**/*",
"patch/*",
"bin/generate-dart-sdk.sh",
]
outputs = ["../mobile/generated/openapi/"]
run = "bash ./bin/generate-dart-sdk.sh"
Dart 端采用 openapi-generator-cli(openapitools.json 中固定 generator 版本为 7.25.0),输出目录为 mobile/generated/openapi/(与 api.md 描述一致)。为了贴合 Flutter 侧的手写代码,Immich 在 open-api 目录下维护了两类定制资产:
- Mustache 模板补丁:templates/mobile 下包含
api.mustache.patch与serialization/native/native_class.mustache.patch,用于在生成前修改 Dart 代码生成的模板行为; - 产物级 patch 文件:open-api/patch 目录存放生成后对产物的统一修补,例如 api_client.dart.patch 把
ApiClient的basePath从final String改为可变String,使移动端运行时可以替换 API 地址;asset_edit_action_item_dto.dart.patch、api.dart.patch等则修正个别 DTO 的类型细节,pubspec_immich_mobile.yaml.patch调整依赖声明。
相关工具版本同样被 mise 锁定以保证可重复构建(mise.toml):npm:oazapfts = 7.5.0、npm:@openapitools/openapi-generator-cli = 2.40.1、java = 21.0.2(OpenAPI Generator 为 Java 实现,需要 JDK 环境)。
TypeScript SDK 的手写封装层
生成的 fetch-client.ts 之外,packages/sdk/src/index.ts 是一段手写的薄封装层,负责对外暴露稳定的初始化与鉴权 API:
export const init = ({ baseUrl, apiKey, headers }: InitOptions) => {
setBaseUrl(baseUrl);
setApiKey(apiKey);
if (headers) {
setHeaders(headers);
}
};
export const setApiKey = (apiKey: string) => {
defaults.headers = defaults.headers || {};
defaults.headers['x-api-key'] = apiKey;
};
要点解析:
- API Key 走
x-api-key头:setApiKey把密钥写入x-api-key请求头,与规范中addApiKey声明的服务端校验头完全对应;assertNoApiKey还禁止通过通用setHeader覆盖该头,确保鉴权入口唯一。 jsonOnly响应守卫:默认fetch实现被包装为jsonOnly()(见 fetch-errors.ts),当请求Accept含 JSON 而响应content-type不是 JSON 时抛出MalformedResponseError,把“返回了 HTML 错误页”这类故障显性化。- 资源路径助手:封装层还导出
getAssetOriginalPath、getAssetThumbnailPath、getAssetPlaybackPath等路径构造函数,这些“大对象直链”不走 SDK 方法调用,而是由调用方直接拼接baseUrl + path访问。
这套分层(生成层 + 手写薄封装)是 Immich 能在每次重新生成后保留定制行为的关键:fetch-client.ts 可随意重建,而 index.ts / fetch-errors.ts 作为手写文件不受生成流程影响。
开发者流程:新增或修改端点后该做什么
综合 api.md 与源码,标准开发流程如下:
- 在
server/src/controllers/下新增端点或修改现有端点,用@nestjs/swagger装饰器完善参数与响应描述(这直接决定规范文件内容); - 确保服务器能以开发模式构建(规范导出依赖编译产物);
- 执行
mise open-api,完整跑通“同步规范 → 生成 TS 客户端 → 构建 SDK → 生成 Dart 客户端”的链路; - 检查两份生成产物:
packages/sdk/src/fetch-client.ts与mobile/generated/openapi/,确认新端点已出现且类型正确; - 若 Dart 生成产物存在类型或行为偏差,优先评估通过 open-api/patch 增加统一补丁,而非手工改动生成文件。
此外,从根 mise.toml 的 release 任务可以看到,版本发布流水线也会强制执行 //:open-api,这保证了每次发版的规范文件与双端 SDK 始终与最新服务端代码一致,避免文档、TS SDK、Dart SDK 三者漂移。
小结
Immich 的 API 体系可以归纳为一条清晰的单向数据流:
NestJS 控制器 + @nestjs/swagger 装饰器
│ SwaggerModule.createDocument / patchOpenAPI
▼
open-api/immich-openapi-specs.json (191 个路径,/api 前缀)
│ oazapfts / openapi-generator(mise open-api 编排)
▼
packages/sdk/src/fetch-client.ts(TypeScript) + mobile/generated/openapi(Dart)
│ + 手写封装层 index.ts / 模板补丁与 patch 文件
▼
Web 前端、CLI、移动端共用的强类型客户端
理解这条链路的实际收益是:任何关于“API 文档和 SDK 不同步”的问题,都可以沿 server → specs.json → 生成器 逐层定位;而所有定制行为都被约束在模板补丁与 patch 文件中,使生成流程保持幂等可重放。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00