Skip to content

refactor(desktop): generate Rust models from OpenAPI - #2113

Merged
AlexStocks merged 8 commits into
oceanbase:masterfrom
CX33071:refactor/desktop-openapi-generator
Oct 21, 2026
Merged

AlexStocks merged 8 commits into
oceanbase:masterfrom
CX33071:refactor/desktop-openapi-generator

Conversation

@CX33071

@CX33071 CX33071 commented Oct 20, 2026

Copy link
Copy Markdown
Contributor

Which issue or RFC does this PR close?

Closes #1896

Rationale for this change

Desktop previously used a custom hand-written OpenAPI-to-Rust schema mapper. This could drift from the OpenAPI contract and required manual updates when request or response schemas changed.

This PR replaces that mapping with a pinned OpenAPI Generator while preserving the existing Desktop transport and security boundary.

What changes are included in this PR?

  • Add and pin OpenAPI Generator 7.26.0.
  • Generate Rust wire models from openapi/powercontext.yaml.
  • Limit generated models to schemas reachable from Desktop's reviewed ten-operation allowlist.
  • Add a custom Rust generation template for serde, ts-rs, naming, nullable fields, and enums.
  • Replace hand-written JSON request bodies with generated RememberMemoryRequest and SearchMemoryRequest.
  • Preserve the existing authentication, TLS, URL validation, timeout, response-size, error-projection, and operation allowlist behavior.
  • Add deterministic generation and generate:check drift validation.
  • Document the generator and regeneration workflow.
  • Update the server probe example for generated nullable types.

Are there any user-facing changes?

No direct user-facing behavior changes are intended.

This is an internal Desktop contract and build refactor. The existing HTTP API, transport security policy, IPC boundary, persisted profile format, and memory workflow remain unchanged.

How was this change tested?

The following checks passed on Linux:

  • pnpm --dir desktop generate:check
  • pnpm --dir desktop test
  • pnpm --dir desktop build
  • pnpm --dir desktop lint
  • cargo fmt --all --manifest-path desktop/src-tauri/Cargo.toml -- --check
  • cargo check --locked --manifest-path desktop/src-tauri/Cargo.toml
  • cargo clippy --locked --manifest-path desktop/src-tauri/Cargo.toml --all-targets -- -D warnings
  • cargo test --locked --manifest-path desktop/src-tauri/Cargo.toml

The frontend test suite reports 32 passing tests. Cross-platform package validation remains covered by CI.

AI usage statement

AI tools were used during development. OpenAI Codex, powered by GPT-5, was used to inspect the existing Desktop/OpenAPI implementation, propose and implement the generator integration, resolve generated-type compatibility issues, review the changes, and run validation commands. The final implementation and test results were reviewed locally by the contributor.

Comment thread desktop/ui/src/generated/ipc.ts Outdated
export type AccessControlMode = "disabled" | "enforced";
export type AccessMeResponse = { principal: AccessPrincipal, mode: AccessControlMode, resource_kinds: Array<AccessResourceType>, provider_capabilities: AccessProviderCapabilities, artifact_families: Array<ArtifactFamilyAccessCapability>, };
export type AccessPrincipal = { type: string, id: string, description?: string | null, };
export type AccessPrincipal = { type: Type, id: string, description?: string | null | undefined, };

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] 提交的生成物引用了从未声明的枚举类型,pnpm typecheck 在该 head 上直接失败

AccessPrincipal.type 现在是 Type,但整份 ipc.ts 没有 export type Type。CI 在同一 head(d46395e1)给出 13 条同类错误,job 118487486779(desktop 工作流 windows,2026-10-20T06:27:37Z 失败):

ui/src/generated/ipc.ts(52,39): error TS2304: Cannot find name 'Type'.
(56,93) OutputKind   (56,113) Effect   (57,94) ShareUnit   (82,34) MatchedBy
(123,8) Role         (127,8) State     (140,8) Stage       (149,9) ExtractionObservationStatus
(162,16) Configuration  (167,42) PromptCapabilityStatus  (167,74) Reason  (167,159) BuiltinProfile

原因是生成链只导出了顶层 component schema 的声明:

  • desktop/scripts/generate.mjs 用 --global-property models=<schemaNames> 生成整份 wire 模型;内联 enum(AccessPrincipal.type、ArtifactRevision.state、ArtifactDreamingOperationCapability.effect 等)被 OpenAPI Generator 提升为独立 Rust 类型(wire.rs 的 pub enum Type / Role / State …)。
  • wire.rs::declarations() 只遍历 schemaNames,examples/export_ipc.rs:77 把该列表 .chain(...) 进 ipc.ts,于是这些名字被字段引用却从未输出声明。
  • 改动前的手写映射把这些字段降级为 string,所以旧类型自洽;换成生成模型后这条依赖被打破。

影响:windows job 停在 pnpm typecheck,其后的 pnpm test、pnpm build、cargo fmt/clippy/test 全部被跳过,因此本 head 的 Rust 侧编译与测试没有 CI 证据;PR 正文的验证清单里也没有 pnpm typecheck(pnpm build 虽然内含 tsc --noEmit,但它排在失败的步骤之后,未被执行)。

建议:让 declarations() 覆盖生成模型里所有独立声明的类型(按模块导出这些 enum 的 <T as TS>::decl,或在模板里对这类字段用 #[ts(inline)] 而非裸类型名),并在生成检查中补一步 tsc --noEmit,避免"生成物与 OpenAPI 一致但不可编译"这种情况静默通过。

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for catching this. Addressed in 32ced08 and included in the current head 08b78d4.
wire.rs::declarations() now exports all generated Rust model declarations, including the promoted inline enums, instead of only the top-level OpenAPI schemas. As a result, Type,OutputKind, Effect, ShareUnit, Role, State, Stage, and the other referenced types are now emitted into desktop/ui/src/generated/ipc.ts.
The generated IPC file no longer contains unresolved type references, and pnpm typecheck passes. The Windows validation for the current head has also passed in my fork; the official PR check is now pending/running.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

确认已在 head 08b78d48 修复。

  • desktop/ui/src/generated/ipc.ts blob 由 f844532e(d46395e1)变为 7bda98b3(08b78d48),80 个 export type 声明中已包含此前全部 13 个悬空类型,包括末段的 export type Type = "user" | "service"。
  • 结构差分(wire.rs 声明集):base 39 个 → head 53 个,新增 14 个 inline enum 提升类型(Type/Role/State/Stage/Effect/ShareUnit/MatchedBy/OutputKind/Reason/Configuration/BuiltinProfile/PromptCapabilityStatus/ExtractionObservationStatus/Location),base 有而 head 缺失的类型为 0。declarations() 改为从生成的 Rust 源码回扫 pub (struct|enum),与 generate.mjs 第 194 行的顶层 await generateRustModels() 一致。
  • CI Desktop validation / windows(job 118638863265)在该 head 上 pnpm typecheck(tsc --noEmit)无输出、pnpm build 内的 tsc --noEmit 通过、pnpm test 32 passed、cargo clippy --all-targets -- -D warnings 通过、pnpm ipc:check 通过,此前失败的步骤之后不再被 skip。

残留一处保护范围限定,不影响当前结论:desktop/src-tauri/examples/export_ipc.rs 新增的空类型断言是 !output.contains("?: |"),只覆盖 optional + nullable 形态。required + nullable 的字段生成失败时是 ": |",不含 ?,该断言不会命中。当前契约与生成物中不存在该形态(wire.rs 内 13 处 #[ts(type = "... | null")] 均带具体类型),故这是回归保护的覆盖边界,不是本 head 的缺陷。

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for verifying. Confirmed that all 13 promoted enum types are now exported and the Windows validation passed. The remaining assertion is only a guard coverage limitation, not a defect in this head.

{{#vendorExtensions.isMandatory}}#[serde_as(as = "serde_with::base64::Base64")]{{/vendorExtensions.isMandatory}}{{^vendorExtensions.isMandatory}}#[serde_as(as = "{{^serdeAsDoubleOption}}Option{{/serdeAsDoubleOption}}{{#serdeAsDoubleOption}}super::DoubleOption{{/serdeAsDoubleOption}}<serde_with::base64::Base64>")]{{/vendorExtensions.isMandatory}}
{{/isByteArray}}
{{!-- a mapped child's discriminator is the union's consumed serde tag: default it, skip it while unset --}}
{{#isMap}}#[ts(type = "Record<string, unknown>")]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] #[ts(type = ...)] 覆盖掉 ts-rs 的可选性与引用类型,生成物里出现空类型并丢失 nullability

同一 head(d46395e1)有两处可直接对照的输出:

  1. ui/src/generated/ipc.ts:68 生成出 created_by?: | null | undefined,。created_by 在契约里是 allOf: [$ref AccessPrincipal] + nullable,模板这一行把 isEnum/isString/isInteger/isNumber/isBoolean/isDateTime 全部落空,拼出 " | null | undefined",引用类型整段丢失。该写法在 TS 里合法(联合类型允许前导 |),所以 tsc 不会报错,属性实际不可用。
  2. Capabilities.prompts 由基线的 prompts?: { [key in string]: PromptCapability } | null 变成 prompts: Record<string, unknown>:{{#isMap}} 的注解把可选性与 | null 一并覆盖,而 Rust 侧仍是 Option<HashMap<String, PromptCapability>> + skip_serializing_if = "Option::is_none"。TS 声明为必填非空,服务端省略该字段时前端没有类型保护,PromptCapability 的值类型也一并丢失。

当前 desktop/ui/src 没有消费这两处字段,因此是类型契约上的潜在缺口,不是现行故障;grep -n "?: |" desktop/ui/src/generated/ipc.ts 只有 1 处,isMap 注解在 wire.rs 中 3 处。

建议:对 isMap/nullable 字段改为保留真实类型(例如 #[ts(type = "Record<string, PromptCapability> | null")])或只加 #[ts(optional)] 而不用 type = 覆盖;并在生成后用一条断言检查不存在 ?: | 形态的空类型,避免模板回归再次静默通过。

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for catching this. Addressed in the current head 08b78d4.
The isMap template no longer overrides the generated type with #[ts(type = "Record<string, unknown>")]. It now preserves the field’s real value type and optionality through #[ts(optional)].
After regenerating the IPC declarations:

  • created_by is generated as created_by?: AccessPrincipal | null
  • prompts is generated as prompts?: { [key in string]:PromptCapability }
  • no empty declarations such as ?: | null | undefined remain
  • the PromptCapability value type is preserved instead of becoming unknown
    The OpenAPI schema declares prompts as optional but not nullable,so the generated type correctly does not include | null. pnpm typecheck and the Windows validation both pass for the current head.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

确认已在 head 08b78d48 修复,且修复方式与契约一致。

  • desktop/scripts/openapi-generator/rust/model.mustache 第 154 行现在只在 {{#isMap}}{{#isFreeFormObject}} 分支发 #[ts(type = "Record<string, unknown>")],有 value 类型的 map 交给 ts-rs 自行推导;{{^required}} 走 #[ts(optional)],{{#isNullable}} 单独发 #[ts(type = "... | null")]。
  • 两处此前指出的输出已符合预期:ipc.ts:68 为 created_by?: AccessPrincipal | null;ipc.ts:86 为 prompts?: { [key in string]: PromptCapability }。
  • 关于 prompts 不再带 | null:契约 Capabilities.prompts(openapi/powercontext.yaml:6966)只有 type: object + default: {} + additionalProperties,没有 nullable,原 | null 是旧生成器的过度声明。这一点用未经本 PR 修改的 desktop/ui/src/generated/api.d.ts(openapi-typescript 从同一契约生成)交叉核对:26 个同名 schema 逐字段比对 optional/null 语义,0 处差异。
  • 空类型已在两处归零:grep -E '\?:\s*\|' ipc.ts 与 grep -E '#\[ts\(type = "\s*\|' wire.rs 均 0 命中。

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for rechecking. Confirmed that optionality, nullability, and map value types are now preserved, and the generated output contains no empty types.

@hidb4ai hidb4ai left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed 08b78d48. The terminal-page assertion now correctly uses second.next_cursor.flatten().is_none(). I verified against the generated models that both an omitted cursor and explicit null are treated as terminal, while a real cursor is not. This addresses the pagination acceptance failure observed at 32ced081. The earlier enum-export and created_by/prompts type issues are also addressed; I found no remaining blocking correctness issue in the reviewed changes.

Windows Desktop validation for this head is still pending, so successful Windows and installer acceptance has not yet been established for this commit.

@CX33071
CX33071 requested review from AlexStocks and hidb4ai October 20, 2026 14:38
Comment thread desktop/README.md
## Contracts and resources

`pnpm --dir desktop generate` derives TypeScript schemas and the reviewed ten-operation manifest from `openapi/powercontext.yaml`; `generate:check` detects drift, including a normalized contract SHA-256. Typed Rust adapters consume that generated manifest and generated wire schemas. No public route is independently handwritten in the application. The manifest is not an IPC permission grant.
`pnpm --dir desktop generate` derives TypeScript schemas, the reviewed ten-operation manifest, and the Rust wire models from `openapi/powercontext.yaml`; `generate:check` detects drift, including a normalized contract SHA-256. The OpenAPI Generator CLI version is pinned in `desktop/openapitools.json`, and the limited Rust customization lives in `desktop/scripts/openapi-generator/rust/`. Generated `src-tauri/src/transport/wire.rs` must not be edited by hand: after changing the contract or generator template, run `pnpm --dir desktop generate`, then run `pnpm --dir desktop generate:check` in CI. Typed Rust adapters consume that generated manifest and generated wire schemas. No public route is independently handwritten in the application. The manifest is not an IPC permission grant.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] 新增的生成链前置(PATH 上的 java、首次运行下载 JAR)未写入两份 README 的构建前置

本 PR 把 generate:check 的代价从纯 Node 计算变成「启动 JVM + 可能下载 JAR + 调用 rustfmt」,但 desktop/README.md 第 20 行的前置清单与 README.zh.md 对应位置仍只有 VS C++ Build Tools、Node 24.14.1、pnpm 11.13.1、Rust 1.95.0、WebView2。

证据链:

  1. desktop/scripts/generate.mjs 第 194 行是顶层 const rustModels = await generateRustModels();,--check 分支在第 226 行才开始比对字节 ⇒ generate:check 同样执行生成器,不是只读校验。generateRustModels() 内先 pnpm exec openapi-generator-cli generate ...,再 run("rustfmt", ["--edition", "2024", wirePath])。
  2. desktop/package.json 的 lint 与 build 都串了 generate:check;CI 日志(job 118638863265)可对照:第 460 行 $ oxlint ui scripts && pnpm generate:check && ...,第 527 行 $ pnpm generate:check && tsc --noEmit && vite build。即 pnpm lint / pnpm build 两个常规命令都依赖该链路。
  3. 该 CLI 的官方要求:「The OpenAPI Generator is a Java project. openapi-generator-cli will download the appropriate JAR file and invoke the java executable to run the OpenAPI Generator. You must have the java binary executable available on your PATH for this to work. (JDK 11 is the minimal version supported.)」(openapitools/openapi-generator-cli)
  4. CI 通过不能证明本地可复现:windows-2025 runner 的 PATH 已预装 C:/hostedtoolcache/windows/Java_Temurin-Hotspot_jdk/17.0.20-101/x64/bin(同一 job 日志的环境 PATH 行),仓库未声明这一前置。JAR 在首次运行时从 Maven Central/Sonatype 拉取,无哈希校验,openapitools.json 只固定版本串。

影响:缺 JDK 或无法访问 Maven 的贡献者执行 pnpm --dir desktop lint / build 会在 generate:check 阶段失败,而 README 的构建前置不会指向原因。

建议:在 desktop/README.md 的前置段落与其 README.zh.md 对应段落同时补上 JDK 11+ 与「首次 generate 需访问 Maven Central」;若希望离线可复现,在 openapitools.json 里加 storageDir 或说明预置 JAR 的位置。无需改动产品运行时行为。

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in the latest update.
Both desktop/README.md and desktop/README.zh.md now document:

  • JDK 11 or newer
  • java must be available on PATH
  • generate and generate:check invoke the Java-based OpenAPI Generator
  • the first run may download the pinned Generator JAR from Maven Central and therefore requires network access
    I also verified the updated setup with pnpm install --frozen-lockfile, pnpm generate:check, and pnpm typecheck.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

确认已在 head e7ce9f38 修复,两份文档均已落实且语义一致。

改动核实(提交 6f5d5074,desktop/README.md +3/-1、desktop/README.zh.md +3/-1):

  • README.md 第 20 行前置清单已含 JDK 11 or newer with `java` available on `PATH` ;README.zh.md 第 20 行为「JDK 11 或更高版本(确保 java 位于 PATH 中)」,与英文版逐项对应。
  • 两份均新增一段说明:generate / generate:check 启动基于 Java 的 OpenAPI Generator,首次运行可能从 Maven Central 下载固定版本 JAR、因此首次生成或漂移检查需网络,后续可复用。英文用 may download、中文用「可能……下载」,措辞与「已有缓存时不下载」的实际行为一致,未过度声明。
  • 「JDK 11」这一下限对 openapitools.json 固定的 Generator 7.26.0 准确:上游 README 写明 Java 11 runtime at a minimum,安装前置列 * [Java 11];openapitools.json 的 version 为 7.26.0,未随本次合并漂移。

声称的验证已由 CI 独立证实(Desktop validation / windows,run 39590150278,job 119188793670,head_sha=e7ce9f38,conclusion success):

  • pnpm install --frozen-lockfile 通过(说明 merge 解决的 pnpm-lock.yaml 冲突与 package.json 自洽)
  • pnpm lint 输出 $ node scripts/generate.mjs --check → Desktop contract matches OpenAPI.
  • pnpm typecheck(tsc --noEmit)、pnpm test(vitest)、pnpm build(含 generate:check + tsc + vite build)、cargo fmt --check、cargo clippy --all-targets -- -D warnings、cargo test、pnpm ipc:check 全部通过,且 pnpm tauri build --bundles nsis 完成

一处可选改进,不阻塞合并:desktop/.mise.toml 目前只 pin 了 node 与 pnpm,未包含 java。用 mise 的贡献者仍需自行装 JDK,mise 无法一键就绪。若希望与「Node/pnpm 版本由 .mise.toml 固定」的原则对齐,可考虑加一行 Java(例如 java = "temurin-21"),同时保持 README 的 11+ 下限不变。

该线程可以关闭。

@hidb4ai hidb4ai left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@CX33071
CX33071 requested a review from AlexStocks October 21, 2026 11:51

@AlexStocks AlexStocks left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

复审结论(head e7ce9f38,含 master 合并)

上一轮 3 条 Finding 全部闭合,本轮未发现新增阻断项。

已闭合

Finding 验证方式 结果
P1 ipc.ts 引用未声明的提升枚举 80 个 export type 声明覆盖全部引用(仅剩泛型参数 T) 闭合
P2 模板 #[ts(type)] 覆盖 optional/nullable created_by?: AccessPrincipal | null、prompts?: { [key in string]: PromptCapability };空类型 0 处 闭合
P2 README 未记录 Java 生成前置 两份 README 均补 JDK 11+/PATH/首次下载 JAR 闭合

合并 master 的正确性

本次 e7ce9f38 合并了 master(88899876)。上游把 HostDiagnostic.presence 改名为 status(diagnostics.rs blob 95740d75→2a998ec8),这份 PR 随之重新生成了 ipc.ts(blob 7bda98b3→7474c293):

  • ipc.ts:25 为 export type HostDiagnostic = { host: string, status: string, checks: Array<DiagnosticItem>, };,与 diagnostics.rs:60-64 一致
  • 消费方 Diagnostics.tsx:150 已用 host.status
  • wire.rs blob 未变(73828fce)是正确现象:HostDiagnostic 由 export_ipc.rs 从 diagnostics 模块导出,不走 wire::declarations()

这正好实证了上一轮 P1 修复的价值——合并引入上游字段重命名时,generate:check / ipc:check 的漂移保护捕获到了差异,生成物没有静默过期。

CI

e7ce9f38 上 43 个 check run 全部 success(installed-package SKIPPED 属设计,仅 workflow_dispatch 触发)。Desktop validation / windows(run 39590150278)中 generate:check 输出 Desktop contract matches OpenAPI.,typecheck/test/build/cargo fmt/clippy -D warnings/cargo test/ipc:check/tauri build --bundles nsis 均通过。

遗留观察(非阻断)

  1. export_ipc.rs 的空类型断言 !output.contains("?: |") 只覆盖 optional + nullable 形态;required + nullable 失败时产生 ": |"(无 ?),不会被命中。当前契约无该形态,属保护范围边界。
  2. ipc:check 输出 30 条 ts-rs failed to parse serde attribute(deserialize_with = "Option::deserialize"),两版 head 数量相同,属既有现象;对应 3 处字段均带显式 #[ts(type = "... | null")] 覆盖,不影响生成结果。

合并门禁

mergeable: MERGEABLE、mergeStateStatus: BLOCKED、reviewDecision: REVIEW_REQUIRED。按 ruleset 9930477 需 2 个 approval,当前 1 个(hidb4ai)⇒ 还缺 1 个,与代码质量无关。

本结论基于 exact head 的 CI 日志与提交树静态核对;本地未执行 pnpm / cargo(本机缺少 pnpm 与可用的 registry/工具链),原因在此标注。

@AlexStocks
AlexStocks merged commit 92572d2 into oceanbase:master Oct 21, 2026
45 of 46 checks passed
@CX33071
CX33071 deleted the refactor/desktop-openapi-generator branch October 21, 2026 11:57
@CX33071
CX33071 restored the refactor/desktop-openapi-generator branch October 21, 2026 11:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

1.4.1 v1.4.1 release tracking

Projects

None yet

Development

Successfully merging this pull request may close these issues.

refactor(desktop): replace the custom OpenAPI-to-Rust schema mapping

3 participants