Auth pillar 從 OAuth 2.0 resource server 改成 pre-shared API key (visionA ↔ converter 1:1 internal trust)。新增 GET /api/v1/jobs/:id/result streaming endpoint 給 visionA backend 中轉 NEF 下載。 Phase A(auth 切換): - 新增 apiKeyMiddleware(constant-time compare、tokenFingerprint、4 audit events) - 砍 OAuth middleware + JWKS(保留 oauthClient 供 promote → FAA 使用) - 4 個 endpoint 換掛 requireApiKey - 加 TRUST_PROXY env + Express trust proxy 設定(forensic source_ip) Phase B(/result endpoint): - streaming NEF download with 5min timeout + concurrent cap 10 - Two-tier rate limit(burst 5/10s + sustained 20/min) - Bandwidth quota(1 GB/hr + 6 GB/24hr)by token_fingerprint - Range header silently ignored + Accept-Ranges: none - filename quote-escape + RFC 5987 fallback + sanitize - 8 個 /result audit events(forensic 完整) 設計演進記錄:docs/TODO-visionA-integration-v2.md(5/2 OAuth → 5/16 API key → 5/16 download via converter;對應 visionA repo ADR-015/016) Tests: 597 → 666 (+69)、29 suites all pass Security: APPROVE WITH CONDITIONS(單 instance 部署、6 新 env、24hr 監控) npm audit: 3 vuln → 0(transitive AWS SDK xml chain) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
8.6 KiB
8.6 KiB
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://<converter-host>/api/v1 - Content-Type:
POST /api/v1/jobs:multipart/form-data- 其他 GET:response 為
application/json; charset=utf-8
- 時間格式:ISO 8601 UTC
- ID 格式:
job_idUUIDv4 - 認證:
Authorization: Bearer <CONVERTER_API_KEY>(除/health外全部必要) - Request ID:若 client 傳
X-Request-Id,回應帶同一值;未傳則 server 產 UUIDv4 - 速率限制:per
client_id(API key 模式下固定visionA-service)300 req / 5min
2. 統一錯誤格式
{
"error": {
"code": "string_code",
"message": "human readable message (zh-TW)",
"details": { /* 可選 */ },
"request_id": "uuid-v4"
}
}
3. GET /health(不需 auth)
Response 200:
{
"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
POST /api/v1/jobs
Authorization: Bearer <CONVERTER_API_KEY>
Content-Type: multipart/form-data; boundary=----...
X-Request-Id: <uuid> (optional)
------...
Content-Disposition: form-data; name="model"; filename="model.onnx"
Content-Type: application/octet-stream
<binary model file>
------...
Content-Disposition: form-data; name="ref_images[]"; filename="img_0.jpg"
Content-Type: image/jpeg
<binary image>
------...
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_BYTESenv 可覆寫)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
{
"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
GET /api/v1/jobs/550e8400-...
Authorization: Bearer <CONVERTER_API_KEY>
If-None-Match: "etag-value" (optional)
5.2 Response 200
{
"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
{
"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 |