# Rust SDK (`origin-asset`) — 开发指令

## ⚠️ SDK 双端对齐规则（强制）

**Rust SDK 和 TypeScript SDK (`@doufunao123/assetforge-sdk`) 必须保持完全对齐。** 任何一方的变更必须同步到另一方。

### 对齐维度

| 维度 | 规则 |
|------|------|
| **版本号** | 两端 semver 必须一致（当前 0.11.2） |
| **API 端点** | 覆盖相同的 HTTP 路由集合 |
| **类型定义** | 字段名、字段类型、可选性完全一致，两端统一使用 snake_case |
| **方法覆盖** | Rust `client.generate_image()` ↔ TS `forge.image()`，每个快捷方法两端同时存在 |
| **错误模型** | 两端解析相同的 JSON 信封 `{"ok", "command", "data"\|"error"}`，错误码对齐 |

### 命名映射惯例

两端类型字段统一使用 snake_case。方法命名差异仅限于语言惯例：

| TS SDK | Rust SDK | 说明 |
|--------|----------|------|
| `forge.image(prompt, opts)` | `client.generate_image(prompt, opts)` | Rust 用 `generate_` 前缀 |
| `forge.image(prompt, { async: true })` | `client.generate_image_async(prompt, opts, submit_opts)` | Rust 没有 keyword args，用独立 `_async` 方法 |
| `forge.job.list()` | `client.jobs()` | Rust 扁平，无子命名空间 |
| `forge.job.status(id)` | `client.job_status(id)` | |
| `forge.job.cancel(id)` | `client.job_cancel(id)` | |
| `forge.job.wait(id)` | `client.job_wait_sse(id, on_event)` / `client.job_wait(id, interval_ms)` | SSE 优先，失败回退轮询 |
| `forge.library.search()` | `client.library_search()` | |
| `forge.providers.list()` | `client.providers()` | |
| `forge.providers.health()` | `client.provider_health()` | |
| `forge.providers.health(id)` | `client.provider_health_single(id)` | |
| `forge.assets.upload()` | `client.upload_file()` | |
| `forge.voice.clone()` | `client.voice_clone()` | |
| `forge.voice.clone(..., { async: true })` | `client.voice_clone_async()` | |
| `forge.library.ingest()` | `client.library_ingest()` | |
| `forge.library.ingestSync()` | `client.library_ingest_sync()` | |
| `forge.library.ingestStatus()` | `client.library_ingest_status()` | |
| `forge.library.ingestWait()` | `client.library_ingest_wait()` | |
| `forge.library.packs()` | `client.library_packs()` | |
| `forge.library.packsInstall()` | `client.library_packs_install()` | |
| `forge.library.packsDelete()` | `client.library_packs_delete()` | |
| `forge.library.packsRefresh()` | `client.library_packs_refresh()` | |
| `forge.stream.job()` / `subscribeJob()` | `client.job_wait_sse()` | |
| `subscribeAllJobs()` | _(Rust 暂不暴露全局订阅 helper)_ | |

### 变更检查清单

每次修改 Rust SDK 时，必须检查并同步：

- [ ] `sdk/src/types.ts` — 对应类型是否存在 / 字段是否对齐
- [ ] `sdk/src/client.ts` 或对应子模块 — 对应方法是否存在
- [ ] `sdk/package.json` — 版本号是否一致
- [ ] `sdk/README.md` — 文档是否更新

### 当前 API 方法对照

| Rust SDK (`AssetClient`) | TS SDK (`AssetForge`) | 端点 |
|-------------------------|----------------------|------|
| `client.generate()` | `forge.generate()` | POST /api/generate |
| `client.generate_image()` | `forge.image()` | POST /api/generate |
| `client.generate_video()` | `forge.video()` | POST /api/generate |
| `client.generate_audio()` | `forge.audio()` | POST /api/generate |
| `client.generate_music()` | `forge.music()` | POST /api/generate |
| `client.generate_tts()` | `forge.tts()` | POST /api/generate |
| `client.generate_model3d()` | `forge.model3d()` | POST /api/generate |
| `client.generate_character()` | `forge.character()` | POST /api/generate |
| `client.generate_prop()` | `forge.prop()` | POST /api/generate |
| `client.generate_sprite()` | `forge.sprite()` | POST /api/generate |
| `client.generate_world()` | `forge.world()` | POST /api/generate |
| `client.generate_text()` | `forge.text()` | POST /api/generate |
| `client.generate_image_async()` 等 `_async` 方法 | `forge.image(..., { async: true })` 等提交方法 | POST /api/generate + `Prefer: respond-async` |
| `client.batch()` | `forge.batch()` | POST /api/generate/batch |
| `client.batch_async()` | `forge.batch(..., { async: true })` | POST /api/generate/batch + `Prefer: respond-async` |
| `client.process()` | `forge.process()` | POST /api/process |
| `client.process_async()` | `forge.process(..., { async: true })` | POST /api/process + `Prefer: respond-async` |
| `client.process3d()` | `forge.process3d()` | POST /api/process3d |
| `client.process3d_async()` | `forge.process3d(..., { async: true })` | POST /api/process3d + `Prefer: respond-async` |
| `client.login()` | `forge.login()` | POST /auth/login |
| `client.jobs()` | `forge.job.list()` | GET /api/jobs |
| `client.job_status()` | `forge.job.status()` | GET /api/jobs/:id |
| `client.job_cancel()` | `forge.job.cancel()` | POST /api/jobs/:id/cancel |
| `client.job_wait_sse()` / `client.job_wait()` | `forge.job.wait()` | SSE GET /api/jobs/:id/events，失败回退轮询 GET /api/jobs/:id |
| `client.providers()` | `forge.providers.list()` | GET /api/providers |
| `client.provider_health()` | `forge.providers.health()` | GET /api/providers/health |
| `client.provider_health_single()` | `forge.providers.health(id)` | GET /api/providers/:id/health |
| `client.upload_file()` | `forge.assets.upload()` | POST /api/assets/upload |
| `client.list_assets()` | `forge.assets.list()` | GET /api/assets |
| `client.delete_asset()` | `forge.assets.delete()` | DELETE /api/assets/:name |
| `client.library_search()` | `forge.library.search()` | GET /api/library/search |
| `client.library_ingest()` | `forge.library.ingest()` | POST /api/library/ingest |
| `client.library_ingest_sync()` | `forge.library.ingestSync()` | POST /api/library/ingest/sync |
| `client.library_ingest_status()` | `forge.library.ingestStatus()` | GET /api/library/ingest/:job_id |
| `client.library_ingest_wait()` | `forge.library.ingestWait()` | GET /api/library/ingest/:job_id |
| `client.library_packs()` | `forge.library.packs()` | GET /api/library/packs |
| `client.library_packs_install()` | `forge.library.packsInstall()` | POST /api/library/packs/install |
| `client.library_packs_delete()` | `forge.library.packsDelete()` | DELETE /api/library/packs/:id |
| `client.library_packs_refresh()` | `forge.library.packsRefresh()` | POST /api/library/packs/:id/refresh |
| `client.library_add()` | `forge.library.add()` | POST /api/library |
| `client.library_get()` | `forge.library.get()` | GET /api/library/:id |
| `client.library_delete()` | `forge.library.delete()` | DELETE /api/library/:id |
| `client.library_catalog_jobs()` | `forge.library.catalogJobs()` | POST /api/library/catalog-jobs |
| `client.voice_clone()` | `forge.voice.clone()` | POST /api/voice/clone |
| `client.voice_clone_async()` | `forge.voice.clone(..., { async: true })` | POST /api/voice/clone + `Prefer: respond-async` |
| `client.voice_design()` | `forge.voice.design()` | POST /api/voice/design |
| `client.voice_list()` | `forge.voice.list()` | GET /api/voice/list |
| `client.voice_delete()` | `forge.voice.delete()` | DELETE /api/voice/:id |
| `client.job_wait_sse()` | `forge.stream.job()` / `subscribeJob()` | GET /api/jobs/:id/events |
| _(Rust 暂不暴露全局订阅 helper)_ | `subscribeAllJobs()` | GET /api/jobs/events |
| `client.health()` | _(未暴露)_ | GET /health |

## 架构

```
AssetClient (client.rs)     ← 主入口，所有方法扁平挂载
HttpTransport (transport.rs) ← HTTP 传输层，信封解析
OriginError (error.rs)       ← 统一错误类型
types.rs                     ← 所有请求/响应类型
```

## 构建

```bash
cargo check
cargo fmt
cargo clippy
```
