- 人工智能
- AI 应用
- 媒体生成
- 桌面应用
【免费下载链接】artcraft
ArtCraft is an intentional crafting engine for artists, designers, and filmmakers
artcraft_api_defs是 ArtCraft 仓库中专门承载 HTTP API 共享类型定义的 crate,其职责是让服务器端(文档中记为storyteller_web)与各类客户端(尤其是 artcraft_client)使用同一套 wire 类型完成序列化与反序列化,避免两端各自维护一套易漂移的 DTO。本文以该 crate 的规范文档 AGENTS.md 为骨架,结合仓库源码逐一拆解其目录组织、Request/Response/Path/Query 四条类型约定、PathInfo后缀规则、孤儿规则(Orphan Rule)边界与最小依赖治理,读者读完后能够直接依据本文在 ArtCraft 中新增一个 API 端点类型,并理解共享类型被artcraft_client消费的完整链路。
一、定位:为什么需要独立的共享类型 crate
在 ArtCraft 的工程布局中,artcraft_api_defs 处于 API 层的中枢位置:它同时被服务器端storyteller_web与客户端 crate 依赖,而不是把请求/响应类型塞进任何一侧。这样做的收益体现在三个方面:
- 单一事实来源:同一份
struct既用于服务端反序列化请求、序列化响应,也用于客户端序列化请求、反序列化响应,字段命名、可选性、枚举取值天然一致; - 版本演进可控:类型变更必须发生在共享层,两端同步升级,不会出现"服务端已改、客户端仍发旧字段"的隐性漂移;
- 与框架解耦:该 crate 刻意不依赖 actix-web(详见下文依赖治理一节),因此类型定义可以被任何 HTTP 客户端栈复用。
从根 Cargo.toml 的 workspace members 可以看到crates/api_clients/artcraft/artcraft_api_defs是仓库的一等成员,并被声明为可被各 crate 通过artcraft_api_defs.workspace = true引用的内部依赖(artcraft_client/Cargo.toml 即如此引用)。
二、目录结构:按业务功能(feature)组织
lib.rs 的模块声明直接反映了文档所述的"Organized by feature"原则。整个 crate 目前按功能域划分了 24 个顶层模块:
users:登录、改密码、改邮箱、改用户名、会话信息与登录质询(users/mod.rs);media_file:媒体文件的删除、按 job 列举、按会话/类型列举、项目文件上传(2D 项目、mood board、3D 场景、视频时间线等);jobs:会话任务列表;moderation:风控与运营侧类型,覆盖告警、调试日志、Top 消费用户、钱包与账本、用户 Stripe 数据等;generate:按生成类型拆分的请求/响应,例如cost_estimate/、image/、video/、object/、splat/等子目录;omni_api与omni_gen:面向第三方/统一入口的 Omni API 与 Omni Gen 生成、成本与任务状态类型;- 其余功能域:
analytics、api_keys、characters、credits、folders、internal、mcp_sessions、prompts、stripe_artcraft、subscriptions、tags、user_referral_codes、video_info、web_referrals、utils、common。
common是一个特殊功能域,只存放跨功能复用的响应类型(详见第四节)。这种"按功能、再按子功能"的层级组织,让一个端点的全部类型(请求、响应、路径参数、查询参数、枚举)都收拢在同一文件里,查找与评审都极为高效。
三、四条核心类型约定:让 wire 形状可预测
AGENTS.md 用四条规则规定了每种类型的 trait 组合与命名,源码中处处可见其落地:
3.1 请求类型:#[derive(Deserialize, ToSchema)]
请求体只负责"接收",因此只派生反序列化与 OpenAPI 模式生成。典型示例是 users/login.rs 中的登录请求:
#[derive(Deserialize, ToSchema)] pub struct LoginRequest { pub username_or_email: String, pub password: String, }ToSchema来自utoipa,用于生成 OpenAPI 文档;Deserialize使服务端可以从 JSON 请求体直接构造该类型。
3.2 响应类型:#[derive(Serialize, ToSchema)]
响应只负责"发送",因此派生序列化。同一个登录端点的成功响应体现了这一点,同时展示了共享层如何承载业务语义注释——signed_session的字段注释解释了它的设计动机(绕过 Cookie、适配禁用跨域 Cookie 的浏览器):
#[derive(Serialize, ToSchema)] pub struct LoginSuccessResponse { pub success: bool, /// A signed session that can be sent as a header, bypassing cookies. /// This is useful for API clients that don't support cookies or Google /// browsers killing cross-domain cookies. pub signed_session: String, }此外,同一个文件里的LoginErrorType枚举(AccountNeedsPassword、InvalidCredentials、ServerError)展示了另一条隐含约定:错误分类的枚举值也可以放在共享层,只要它们不需要actix-web的ResponseError实现(见第五节)。
3.3 路径参数:Deserialize + ToSchema且以PathInfo为后缀
URL 路径中的参数类型以PathInfo结尾,这是共享层最具辨识度的命名约定。media_file/delete_media_file.rs 给出了教科书式示例:
/// For the URL PathInfo #[derive(Serialize, Deserialize, ToSchema)] pub struct DeleteMediaFilePathInfo { pub token: MediaFileToken, }注意这里同时派生Serialize与Deserialize——路径参数既要由服务端从路径中反序列化出来,也要能序列化(例如客户端在拼接 URL 或做调试输出时需要)。token字段使用了 tokens crate 中的MediaFileToken,这也是共享层大量复用内部 schema crate 的证据。顺着PathInfo后缀可以快速在代码库中定位所有带路径参数的端点。
3.4 查询参数:#[derive(Deserialize, IntoParams)]
查询字符串参数类型单独使用IntoParams(同样是 utoipa 的 trait,用于把结构体展开为 OpenAPI 的 query 参数),且通常不需要ToSchema。jobs/list_session_jobs.rs 是文档约定的直接落地,并注释了参数的逗号分隔取值语义:
#[derive(Deserialize, ToSchema, IntoParams)] pub struct ListSessionJobsQueryParams { /// NB: This can be one (or more comma-separated values) from `JobStatusPlus`. /// ?include_states=pending or ?include_states=pending,started,complete_success (etc.) pub include_states: Option<String>, /// The opposite of include_states, this filters out states from view. pub exclude_states: Option<String>, }另一个更纯粹的查询参数示例是 media_file/list/by_type/list_session_common.rs,它只派生Deserialize, IntoParams,承载游标分页所需的四个参数(sort_ascending、page_size、cursor、cursor_is_reversed),注释明确"Pagination only — the media class is fixed by the endpoint"。
3.5 常量约定:URL 路径以const形式与类型同文件
源码中还呈现出一条 AGENTS.md 未显式写出、但普遍遵守的约定:端点 URL 路径作为pub const ..._URL_PATH与类型放在同一文件。例如 jobs/list_session_jobs.rs 的LIST_SESSION_JOBS_URL_PATH: &str = "/v1/jobs/session"、media_file/list/by_type/list_session_mesh_media_files.rs 的LIST_SESSION_MESH_MEDIA_FILES_URL_PATH,以及 analytics/log_active_user.rs 中按客户端版本区分的LOG_ACTIVE_USER_V1_PATH与LOG_ACTIVE_USER_V2_PATH。路径常量与类型定义同处一个文件,客户端只需use该文件即可同时获得路径与类型(见第七节),不会出现路径字符串散落各处的维护问题。
四、跨功能共享响应类型:common/responses
文档将common单列为功能域之一。它的实际内容是 common/responses/mod.rs 导出的七类被多个功能域复用的响应形状:
| 类型 | 文件 | 用途 |
|---|---|---|
MediaLinks/VideoPreviews | media_links.rs | 媒体文件的 CDN 链接、缩略图模板与视频预览 |
PaginationCursors | pagination_cursors.rs | 无限滚动式游标分页 |
JobDetailsLivePortraitRequest/JobDetailsLipsyncRequest | job_details.rs | 直播人像与口型同步任务的请求细节 |
MediaFileCoverImageDetails | media_file_cover_image_details.rs | 封面图细节 |
CoverImageLinks | cover_image_links.rs | 封面图链接 |
SimpleEntityStats | simple_entity_stats.rs | 实体统计 |
SimpleGenericJsonSuccess | simple_generic_json_success.rs | 通用 JSON 成功响应 |
UserDetailsLight | user_details_light.rs | 轻量用户信息 |
以游标分页为例,pagination_cursors.rs 的注释揭示了设计动机——不暴露总页数,从而避免投资者与竞品通过接口推断数据库记录规模,同时说明游标是"不透明的(通常加密的)句柄":
#[derive(Serialize, Deserialize, ToSchema, Debug)] pub struct PaginationCursors { /// The "next" cursor. /// This is an opaque (typically even encrypted) handle. pub maybe_next: Option<String>, /// The "previous" cursor. /// This is an opaque (typically even encrypted) handle. pub maybe_previous: Option<String>, /// Details whether we're walking forward or backward. pub cursor_is_reversed: bool, }MediaLinks则展示了 URL 类型直接进入 wire 结构:cdn_url: Url(来自urlcrate),maybe_thumbnail_template用{WIDTH}占位符让客户端自行拼出不同尺寸的缩略图;视频文件另有maybe_video_previews: Option<VideoPreviews>,内含静态帧、动图及各自的缩略图模板。
这些共享类型被具体端点复用,例如 jobs/list_session_jobs.rs 的ListSessionJobsItem就嵌入了MediaLinks、JobDetailsLivePortraitRequest、JobDetailsLipsyncRequest,并组合了DateTime<Utc>(chrono)与多个tokens类型;list_session_mesh_media_files.rs 则把SessionMediaFileInfo与PaginationCursors组合成网格媒体文件列表响应。
五、孤儿规则(Orphan Rule):共享层的边界在哪里
AGENTS.md 明确划定了共享层的职责边界:需要ResponseError实现(actix-web)的类型不能放在本 crate。原因是 Rust 的孤儿规则——impl ResponseError for MyType要求 trait 或类型至少有一个定义在本地 crate,而ResponseError属于 actix-web,共享层又禁止依赖 actix-web,因此这类实现只能放在服务器端storyteller_web。
这条规则的实际效果是错误响应类型留在服务端、纯数据类型下沉到共享层。仓库源码中的多处注释印证了同一原则在更细粒度上的应用:
- omni_api/omni_upload_video.rs 指出:上传接口的请求体是
multipart/form-data,因为 multipart 表单类型绑定于 Web 框架,所以请求类型在storyteller_web中与 handler 一起声明,共享层只放成功响应类型; - folders/common.rs 指出:folder 的列表项形状因嵌入
MediaFileCoverImageDetails等依赖请求上下文(MediaDomain+ServerEnvironment)的类型,也保留在storyteller_web,共享层只提供不依赖上下文的FolderInfo与FolderThumbnail。
因此,判断一个类型该不该进共享层,可以套用两条检验标准:
- 它是否依赖 actix-web 或请求上下文(
MediaDomain、ServerEnvironment、multipart 绑定等)?——是则留在服务端; - 它是否同时被服务端与客户端读写?——是则应下沉到共享层。
从源码结构看,folders/common.rs还展示了共享层在"被服务端使用"时的另一重职责:为FolderInfo这类跨端点复用的形状提供单一定义(单文件夹 GET、创建响应、列表行、子文件夹列表行共用一个结构),并保持与MediaLinks类似的视频预览镜像结构独立演进(FolderThumbnailVideoPreviews的注释明确"Mirrors the shape ofMediaLinks' video previews, but is its own type")。
六、依赖治理:刻意保持的最小依赖面
Cargo.toml 完整呈现了 AGENTS.md 所述的依赖清单,且是"刻意精简"的:
# Internal enums.workspace = true tokens.workspace = true # External chrono.workspace = true url.workspace = true # Serialization serde.workspace = true serde_derive.workspace = true serde_json.workspace = true utoipa.workspace = true [dev-dependencies] anyhow.workspace = true要点解读:
- 外部依赖仅五个:
serde/serde_derive(序列化)、utoipa(OpenAPI 模式)、chrono(DateTime<Utc>时间戳)、url(Url类型的 CDN 链接);serde_json主要用于测试与序列化工具; - 内部 schema 依赖两个:enums 与 tokens。
enums提供跨端共享的枚举,例如 common_video_model.rs 中#[serde(rename_all = "snake_case")]的CommonVideoModel,被 omni_api/generate_requests/omni_api_video_generate_request.rs 直接引用为请求字段model: Option<CommonVideoModel>;tokens提供MediaFileToken、UserToken、FolderToken、ApiKeyToken、InferenceJobToken、BatchGenerationToken、PromptToken等强类型标识符; - 没有 actix-web 依赖:这是第五节的孤儿规则在依赖层面的对应——共享层保持"纯类型 + 纯序列化"的定位,所有 Web 框架绑定都留在服务端;
- 开发依赖仅
anyhow:共享层自身测试极少引入重型依赖,进一步印证其"薄"的特质。
enumscrate 的测试也保证了这些共享枚举的序列化契约稳定:common_video_model.rs内置了test_serialization、test_deserialization、test_round_trip等测试(如assert_serialization(CommonVideoModel::Kling21Pro, "kling_2p1_pro")),确保 wire 上的字符串取值与枚举变体一一对应、双向往返无损。
七、客户端消费链路:artcraft_client 如何复用共享类型
共享类型最终要被真实客户端使用才产生价值。artcraft_client 是消费方之一,其 jobs/list_session_jobs.rs 展示了标准用法:
use artcraft_api_defs::jobs::list_session_jobs::{ListSessionJobsSuccessResponse, LIST_SESSION_JOBS_URL_PATH};客户端从共享层一次性引入响应类型 + URL 路径常量,再结合本地定义的请求状态枚举(States::All / Include / Exclude)拼出查询串,最终通过basic_json_get_request发起请求并把响应反序列化为共享的ListSessionJobsSuccessResponse:
pub async fn list_session_jobs( api_host: &ApiHost, maybe_creds: Option<&StorytellerCredentialSet>, job_states: States, ) -> Result<ListSessionJobsSuccessResponse, StorytellerError> { let url = match job_states { States::All => LIST_SESSION_JOBS_URL_PATH.to_string(), States::Include(states) => format!("{}?include_states={}", LIST_SESSION_JOBS_URL_PATH, states_string(&states)), States::Exclude(states) => format!("{}?exclude_states={}", LIST_SESSION_JOBS_URL_PATH, states_string(&states)), }; Ok(basic_json_get_request(api_host, &url, maybe_creds).await?) }客户端只负责"如何请求"(认证、Host、HTTP 动词、查询串拼接),而"请求/响应长什么样"完全由共享层裁决。这与服务端反序列化同一组请求类型形成闭环——两端各自use artcraft_api_defs::...,即可保证契约一致,这正是该 crate 存在的全部意义。
八、实践小结:新增一个端点类型时的检查清单
综合 AGENTS.md 的约定与仓库源码的实际落地,在artcraft_api_defs中新增端点类型时可依次自检:
- 目录:放入对应 feature 子目录,若为新功能则创建新
mod.rs并在 lib.rs 注册; - 请求体:
#[derive(Deserialize, ToSchema)],字段按需加Option与 serde 属性(如skip_serializing_if,见 analytics/log_active_user.rs); - 响应体:
#[derive(Serialize, ToSchema)],可复用的形状优先引用common::responses; - 路径参数:
#[derive(Serialize, Deserialize, ToSchema)]并以PathInfo后缀命名; - 查询参数:
#[derive(Deserialize, IntoParams)]; - 路径常量:
pub const XXX_URL_PATH与类型同文件; - 边界检查:若类型需要
ResponseError、multipart 表单或请求上下文(MediaDomain/ServerEnvironment),则留在storyteller_web而非共享层; - 依赖检查:只用 serde / utoipa / chrono / url 与
enums、tokens,不得引入 actix-web。
以上约定配合 AGENTS.md 与同目录的 CLAUDE.md(内容与 AGENTS.md 完全一致),构成了 ArtCraft 共享 API 契约层的完整开发规范,也是后续参与该仓库 API 层开发时的首要阅读材料。
- 人工智能
- AI 应用
- 媒体生成
- 桌面应用
【免费下载链接】artcraft
ArtCraft is an intentional crafting engine for artists, designers, and filmmakers
相关推荐
Gitpod 协议层深度解析:gitpod-protocol 共享类型定义与跨服务通信契约
Gitpod 协议层深度解析:gitpod protocol 共享类型定义与跨服务通信契约 Gitpod Protocol( components/gitpod
开发工具后端云原生Civitai 的 `@civitai/buzz` 包:Buzz 服务类型化 HTTP 客户端与共享货币化规则解析
Civitai 的 @civitai/buzz 包:Buzz 服务类型化 HTTP 客户端与共享货币化规则解析 导读 @civitai/buzz 是 Civit
后端前端AI 应用Cherry Studio 共享数据类型层(src/shared/data)完全指南:Data API、Cache、Preference 与 BootConfig 的类型契约
Cherry Studio 共享数据类型层(src/shared/data)完全指南:Data API、Cache、Preference 与 BootConfi
人工智能大模型AI 应用交互助手本地部署
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考