# API: `/api/v1/jobs`(POST / GET / GET :id) > **狀態**:Phase 1 完工(OAuth)→ Phase 0.8b 換 auth(API key);其他流程不變。 > > **配套**:`auth.md`、`api/api-result.md`、`api/api-promote.md`、`database.md`。 --- ## 1. 通用約定 - **Base URL**:`https:///api/v1` - **Content-Type**: - `POST /api/v1/jobs`:`multipart/form-data` - 其他 GET:response 為 `application/json; charset=utf-8` - **時間格式**:ISO 8601 UTC - **ID 格式**:`job_id` UUIDv4 - **認證**:`Authorization: Bearer `(除 `/health` 外全部必要) - **Request ID**:若 client 傳 `X-Request-Id`,回應帶同一值;未傳則 server 產 UUIDv4 - **速率限制**:per `client_id`(API key 模式下固定 `visionA-service`)300 req / 5min --- ## 2. 統一錯誤格式 ```json { "error": { "code": "string_code", "message": "human readable message (zh-TW)", "details": { /* 可選 */ }, "request_id": "uuid-v4" } } ``` --- ## 3. `GET /health`(不需 auth) **Response 200**: ```json { "service": "kneron-converter-api", "status": "healthy", "version": "1.0.0", "timestamp": "2026-05-16T12:00:00Z", "dependencies": { "redis": "connected", "file_access_agent": "reachable" } } ``` **Phase 0.8b 變動**:移除 `member_center` dependency(不再驗 MC token);保留 `file_access_agent`(promote 用)+ `redis`。 **Response 503**:任一 critical dependency 失敗。 --- ## 4. `POST /api/v1/jobs` ### 4.1 Request ```http POST /api/v1/jobs Authorization: Bearer Content-Type: multipart/form-data; boundary=----... X-Request-Id: (optional) ------... Content-Disposition: form-data; name="model"; filename="model.onnx" Content-Type: application/octet-stream ------... Content-Disposition: form-data; name="ref_images[]"; filename="img_0.jpg" Content-Type: image/jpeg ------... Content-Disposition: form-data; name="user_id" visionA-user-12345 ------... Content-Disposition: form-data; name="model_id" 1001 ------... Content-Disposition: form-data; name="version" 0001 ------... Content-Disposition: form-data; name="platform" 520 ------...-- ``` ### 4.2 Multer 設定 - `multer.memoryStorage()` - `limits.fileSize`: 500MB(`MULTIPART_MODEL_MAX_BYTES` env 可覆寫) - `fields`: `model`(1 個 file)、`ref_images[]`(`maxCount: 100`) ### 4.3 欄位定義 | 欄位 | 類型 | 位置 | 必填 | 驗證 | |------|------|------|------|------| | `model` | file | multipart file | ✅ | 副檔名 ∈ {`.onnx`, `.pt`, `.pth`, `.tflite`, `.h5`, `.pb`};大小 ≤ 500MB | | `ref_images[]` | file[] | multipart file | ❌ | `image/*`;最多 100 張;單張 ≤ 10MB | | `user_id` | string | multipart field | ✅ | 1-128 字元,嚴格白名單 `^[A-Za-z0-9._-]+$`,不含 `..` | | `model_id` | string → int | multipart field | ✅ | 轉 int 後 1 ≤ x ≤ 65535 | | `version` | string | multipart field | ✅ | 1-32 字元,嚴格白名單 `^[A-Za-z0-9._-]+$` | | `platform` | string | multipart field | ✅ | enum: `520`, `720`, `530`, `630`, `730` | | `enable_evaluate` | string `'true'`/`'false'` | multipart field | ❌ | 預設 `'false'` | | `enable_sim_fp` | string `'true'`/`'false'` | multipart field | ❌ | 預設 `'false'` | | `enable_sim_fixed` | string `'true'`/`'false'` | multipart field | ❌ | 預設 `'false'` | | `enable_sim_hw` | string `'true'`/`'false'` | multipart field | ❌ | 預設 `'false'` | | `metadata` | string(JSON)| multipart field | ❌ | 合法 JSON 物件字串 | ### 4.4 Middleware 順序(**勿改**) ``` requireApiKey() ↓ perClientLimiter(per client_id rate limit) ↓ uploadConcurrencySemaphore(per-process MAX_CONCURRENT_UPLOADS) ↓ uploader.fields([{ name: 'model', maxCount: 1 }, { name: 'ref_images[]', maxCount: 100 }]) ↓ multerErrorAdapter(捕 multer LIMIT_FILE_SIZE → 413) ↓ createJobHandler ``` **理由**:API key middleware 必須在 multer 之前(避免未驗證就 parse 500MB 大檔);rate limiter 第二(超 quota 不該吃 multipart);upload concurrency semaphore 第三(防 OOM);multer 最後(auth + quota + concurrency 三重通過後才 parse)。 ### 4.5 Response 201 Created ```json { "job_id": "550e8400-e29b-41d4-a716-446655440000", "status": "created", "stage": "onnx", "progress": 0, "created_at": "2026-05-16T12:00:00Z", "expires_at": "2026-05-23T12:00:00Z", "user_id": "visionA-user-12345" } ``` ### 4.6 Error responses | HTTP | error.code | 情境 | |------|-----------|------| | 400 | `validation_error` | 欄位缺漏或格式錯誤 | | 400 | `invalid_multipart` | multipart parse 失敗、缺必要 file、副檔名不符 | | 401 | `invalid_token` | API key 缺 / 不符 | | 409 | `user_has_active_job` | user_id 已有進行中 job | | 413 | `file_too_large` | model 檔超過 500MB | | 500 | `misconfiguration` | `STORAGE_BACKEND !== 'minio'` | | 502 | `storage_unavailable` | MinIO 寫入失敗 | | 503 | `service_unavailable` | upload concurrency semaphore 滿、API key 未配置 | | 503 | `service_unavailable` | upload semaphore 滿(含 `Retry-After` header) | --- ## 5. `GET /api/v1/jobs/:id` ### 5.1 Request ```http GET /api/v1/jobs/550e8400-... Authorization: Bearer If-None-Match: "etag-value" (optional) ``` ### 5.2 Response 200 ```json { "job_id": "550e8400-...", "user_id": "visionA-user-12345", "status": "running", "stage": "bie", "progress": 45, "stage_progress": 60, "created_at": "...", "updated_at": "...", "expires_at": "...", "stage_timings": { "onnx": { "started_at": "...", "completed_at": "..." }, "bie": { "started_at": "...", "completed_at": null }, "nef": null }, "input": { "filename": "model.onnx", "object_key": "jobs/.../input/model.onnx", "size_bytes": 204800000, "ref_images_count": 0 }, "result_object_keys": null, "error": null, "parameters": { /* model_id, version, platform, enable_* */ }, "metadata": {}, "estimated_completion_at": null } ``` ### 5.3 狀態機(對外 `status` 欄位) - `created` — 剛建立,等第一階段開工 - `running` — 正在某個 stage(`stage` 欄位有值) - `completed` — 全部完成(`result_object_keys` 有值,`stage=null`) - `failed` — 失敗(`error` 有值) **內部 → 對外映射**(statusMapper): | 內部 status | 對外 `status` + `stage` | |------------|----------------------| | `ONNX` + `stage_timings.onnx.started_at == null` | `created` + stage=onnx | | `ONNX` + `started_at != null` | `running` + stage=onnx | | `BIE` | `running` + stage=bie | | `NEF` | `running` + stage=nef | | `COMPLETED` | `completed` + stage=null | | `FAILED` | `failed` + stage=<最後階段> | ### 5.4 ETag 支援 ETag = hash(`job.updated_at`)。If-None-Match 吻合 → 304 Not Modified(省 body)。 ### 5.5 Error responses | HTTP | error.code | 情境 | |------|-----------|------| | 401 | `invalid_token` | API key 缺 / 不符 | | 404 | `job_not_found` | job 不存在 / 不屬於 client(避免資訊洩露)| --- ## 6. `GET /api/v1/jobs`(列表 / Recovery) ### 6.1 Query 參數 | 參數 | 類型 | 必填 | 說明 | |------|------|------|------| | `user_id` | string | **✅** | 過濾 user_id(強制必填,避免全掃)| | `status` | string | ❌ | `in_progress` (= `created` ∪ `running`) / `completed` / `failed` / `all`(預設 `all`)| | `limit` | int | ❌ | 預設 20,上限 100 | | `offset` | int | ❌ | 預設 0 | | `created_after` | ISO 8601 | ❌ | 過濾 `created_at >= created_after` | ### 6.2 Response 200 ```json { "total": 2, "limit": 20, "offset": 0, "items": [ { /* 同 GET /jobs/:id 格式,精簡版 */ } ] } ``` ### 6.3 實作 以 `user:{user_id}:jobs` Set 為索引,避免全掃 `KEYS job:*`。 --- ## 7. Phase 2 預留端點(Phase 1 + 0.8b 回 501) | 方法 | 路徑 | 說明 | |------|------|------| | POST | `/api/v1/jobs/:id/download-tokens` | Phase 2 預留(未來 browser 直連 download 用)| | DELETE | `/api/v1/jobs/:id` | 取消 job(Phase 2/3)| 兩個都回 501 `not_implemented`。 --- ## 8. 端點清單總表 | 方法 | 路徑 | 說明 | Auth | |------|------|------|------| | GET | `/health` | 健康檢查 | — | | POST | `/api/v1/jobs` | 建立轉檔 job | API key | | GET | `/api/v1/jobs` | 列出 job(user_id 必填)| API key | | GET | `/api/v1/jobs/:id` | 單一 job 狀態 | API key | | POST | `/api/v1/jobs/:id/promote` | 搬檔到 FAA | API key | | GET | `/api/v1/jobs/:id/result` | **NEW** stream NEF | API key | | POST | `/api/v1/jobs/:id/download-tokens` | Phase 2,回 501 | API key | | DELETE | `/api/v1/jobs/:id` | Phase 2,回 501 | API key |