依 autoflow-agent workspace v2 設計把 PRD / 設計 / 架構 / 交付類 共享文件從個人層 .autoflow/(ignored)搬到 docs/autoflow/(進 git), 讓團隊可共享產品與架構文件,個人層只留 progress / review / testing 等 per-branch 筆記。 - 02-prd/ 21 個檔(PRD、features、market-analysis 等) - 03-design/ 18 個檔(design-spec、wireframes、flows 等) - 04-architecture/ 31 個檔(TDD、design-doc、ADR×14、API 規格等) - 07-delivery/ 3 個檔(project-summary、phase-0.6-handover、stage-deployment-setup) 合計 73 檔。原檔已從 .autoflow/ 移除(migration 工具執行 git mv, 但因 .autoflow/ 在 .gitignore 中、git 將此操作視為新增、無 rename history)。
263 lines
7.4 KiB
Markdown
263 lines
7.4 KiB
Markdown
# Converter Integration Contract
|
||
|
||
> 本文件定義 **visionA-backend 呼叫 kneron_model_converter 的 API 契約**。
|
||
> 目的:提前把介面定義清楚,讓 converter 團隊知道要實作什麼;同時讓 visionA-backend 雛形可先用 stub 開發。
|
||
|
||
---
|
||
|
||
## 1. 通訊方向
|
||
|
||
```
|
||
visionA-backend/api-server kneron_model_converter
|
||
│ │
|
||
│ 1. POST /v1/jobs(提交轉檔) │
|
||
│ ────────────────────────────────────►│
|
||
│ │
|
||
│ ◄────── 202 + {job_id} ──────────────│
|
||
│ │
|
||
│ 2. GET /v1/jobs/{id}(輪詢 / 或等 webhook)
|
||
│ ────────────────────────────────────►│
|
||
│ ◄────── 200 + {status, result_url}──│
|
||
│ │
|
||
│ 3. 下載產物(GET result_url) │
|
||
│ ────────────────────────────────────►│
|
||
```
|
||
|
||
Converter 可選:
|
||
- **Pull 模式**:visionA 輪詢 `/v1/jobs/{id}`
|
||
- **Push 模式**:visionA 提供 webhook URL,converter 完成後回呼
|
||
|
||
雛形先用 **Pull 模式**;Phase 1 評估 webhook。
|
||
|
||
---
|
||
|
||
## 2. 認證
|
||
|
||
**visionA → converter**:
|
||
- 服務對服務,使用 API Key 或 mTLS
|
||
- Header:`Authorization: Bearer <VISIONA_CONVERTER_API_KEY>`
|
||
|
||
API Key 由 converter 團隊簽發,放 `VISIONA_CONVERTER_API_KEY` env。
|
||
|
||
---
|
||
|
||
## 3. 端點
|
||
|
||
### 3.1 POST `/v1/jobs` — 提交轉檔
|
||
|
||
**Request**:
|
||
```json
|
||
POST /v1/jobs
|
||
Authorization: Bearer ...
|
||
Content-Type: application/json
|
||
|
||
{
|
||
"source": {
|
||
"type": "url",
|
||
"url": "https://storage.visiona.cloud/converter/source/demo-user/job-xxx.onnx?signature=...",
|
||
"checksum_sha256": "abc123...",
|
||
"format": "onnx"
|
||
},
|
||
"target": {
|
||
"chip": "kl520",
|
||
"quantization": "int8",
|
||
"input_shape": [1, 3, 224, 224]
|
||
},
|
||
"callback": {
|
||
"webhook_url": null, // 雛形先 null
|
||
"idempotency_key": "<uuid>"
|
||
},
|
||
"client_job_id": "<visionA 這邊的 job id,對應 converter_jobs.id>"
|
||
}
|
||
```
|
||
|
||
**欄位說明**:
|
||
|
||
| 欄位 | 必要 | 說明 |
|
||
|------|-----|------|
|
||
| `source.type` | ✓ | `"url"` \| `"upload"`(雛形只支援 url) |
|
||
| `source.url` | ✓ | Presigned GET URL,converter 自己下載 |
|
||
| `source.checksum_sha256` | ✓ | visionA 計算好的 sha256,converter 下載後驗證 |
|
||
| `source.format` | ✓ | `"onnx"` \| `"keras"` \| `"tflite"` \| ... |
|
||
| `target.chip` | ✓ | `"kl520"` \| `"kl720"` |
|
||
| `target.quantization` | — | `"int8"` \| `"fp16"`,預設依 chip |
|
||
| `target.input_shape` | — | 若 source 不含 shape,由此補 |
|
||
| `callback.webhook_url` | — | 未來 push 模式 |
|
||
| `callback.idempotency_key` | ✓ | 重試時避免重複執行 |
|
||
| `client_job_id` | ✓ | visionA 內部 job id,converter 回傳時要帶上 |
|
||
|
||
**Response 202**:
|
||
```json
|
||
{
|
||
"success": true,
|
||
"data": {
|
||
"job_id": "cvt-abc-123", // converter 側的 id
|
||
"status": "queued",
|
||
"accepted_at": "2026-04-21T12:00:00Z",
|
||
"estimated_duration_seconds": 120
|
||
}
|
||
}
|
||
```
|
||
|
||
**Response 錯誤**:
|
||
```json
|
||
{
|
||
"success": false,
|
||
"error": {
|
||
"code": "UNSUPPORTED_FORMAT" | "INVALID_CHECKSUM" | "SOURCE_UNREACHABLE" | "QUOTA_EXCEEDED",
|
||
"message": "..."
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 3.2 GET `/v1/jobs/{job_id}` — 查詢狀態
|
||
|
||
**Response 200**:
|
||
```json
|
||
{
|
||
"success": true,
|
||
"data": {
|
||
"job_id": "cvt-abc-123",
|
||
"client_job_id": "<visionA 的>",
|
||
"status": "queued" | "running" | "succeeded" | "failed",
|
||
"progress": 0.65, // 0.0 - 1.0
|
||
"stage": "quantizing", // 可選
|
||
"accepted_at": "...",
|
||
"started_at": "...",
|
||
"completed_at": "...",
|
||
|
||
"result": { // status == succeeded 時才有
|
||
"url": "https://converter.cloud/result/....nef?signature=...",
|
||
"url_expires_at": "...",
|
||
"checksum_sha256": "...",
|
||
"size_bytes": 12345678,
|
||
"target_chip": "kl520"
|
||
},
|
||
|
||
"error": { // status == failed 時才有
|
||
"code": "QUANTIZATION_FAILED",
|
||
"message": "Layer ... not supported",
|
||
"details": {}
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 3.3 POST `/v1/jobs/{job_id}/cancel` — 取消
|
||
|
||
**Response**:
|
||
```json
|
||
{ "success": true, "data": { "job_id": "...", "status": "cancelled" } }
|
||
```
|
||
|
||
若已 completed → `400 ALREADY_COMPLETED`;若 already cancelled → `200` idempotent。
|
||
|
||
---
|
||
|
||
### 3.4 Webhook(未來)
|
||
|
||
Converter push 到 visionA 的 webhook URL:
|
||
|
||
**Request(converter → visionA)**:
|
||
```json
|
||
POST https://api.visiona.cloud/webhooks/converter
|
||
X-Converter-Signature: sha256=<hmac of body using shared secret>
|
||
Content-Type: application/json
|
||
|
||
{
|
||
"event": "job.completed" | "job.failed",
|
||
"job_id": "cvt-abc-123",
|
||
"client_job_id": "<visionA 的>",
|
||
"status": "succeeded" | "failed",
|
||
"result": { ... },
|
||
"error": { ... },
|
||
"timestamp": "..."
|
||
}
|
||
```
|
||
|
||
**visionA 驗證 signature 後**:
|
||
- 更新 `converter_jobs` 表
|
||
- 若 succeeded,下載產物存到 `storage/converter/result/`
|
||
- 建立對應的 `models` record(source=converted, source_job_id=<client_job_id>)
|
||
- 回 200
|
||
|
||
**retry 約定**:
|
||
- Webhook 失敗 converter 最多重試 5 次,指數退避
|
||
- visionA 必須 idempotent 處理(用 `event + job_id` 當 key)
|
||
|
||
---
|
||
|
||
## 4. 雛形階段(visionA 端 stub)
|
||
|
||
```go
|
||
// internal/converter/stub.go
|
||
type StubClient struct {
|
||
jobs map[string]*Job
|
||
mu sync.Mutex
|
||
}
|
||
|
||
func (s *StubClient) SubmitConvert(ctx, req) (string, error) {
|
||
s.mu.Lock()
|
||
defer s.mu.Unlock()
|
||
jobID := "stub-job-" + uuid.NewString()
|
||
s.jobs[jobID] = &Job{
|
||
ID: jobID, Status: "queued", CreatedAt: time.Now(),
|
||
TargetChip: req.TargetChip,
|
||
}
|
||
// 雛形:15 秒後「完成」,指向假 result URL
|
||
time.AfterFunc(15*time.Second, func() {
|
||
s.mu.Lock()
|
||
defer s.mu.Unlock()
|
||
if j, ok := s.jobs[jobID]; ok {
|
||
j.Status = "succeeded"
|
||
j.ResultKey = "stub-result-key"
|
||
j.CompletedAt = ptrTime(time.Now())
|
||
}
|
||
})
|
||
return jobID, nil
|
||
}
|
||
```
|
||
|
||
雛形期前端可以用這個 stub 走完整 UX,但不會真的產生 `.nef`。
|
||
|
||
---
|
||
|
||
## 5. Error 對應表
|
||
|
||
| Converter 回傳 | visionA 前端顯示 |
|
||
|---------------|----------------|
|
||
| `UNSUPPORTED_FORMAT` | 「目前不支援此格式」|
|
||
| `INVALID_CHECKSUM` | 「檔案下載驗證失敗,請重新上傳」|
|
||
| `QUOTA_EXCEEDED` | 「本月轉檔配額已滿」|
|
||
| `QUANTIZATION_FAILED` | 「模型轉檔失敗:[detail]」|
|
||
| 其他 | 「轉檔失敗,請聯絡支援」|
|
||
|
||
---
|
||
|
||
## 6. 相容性 / 版本
|
||
|
||
- URL 含 `/v1/` 前綴,日後升級 `/v2/` 可並存
|
||
- 欄位採「新增只是 optional」原則,不破壞舊版
|
||
- visionA 用 env `VISIONA_CONVERTER_API_VERSION` 切換(預設 `v1`)
|
||
|
||
---
|
||
|
||
## 7. 給 Converter 團隊的確認清單
|
||
|
||
- [ ] 同意採用此 API spec?
|
||
- [ ] 確認 source.type `"url"` 的 presigned URL 長度 / TTL 要求
|
||
- [ ] 確認支援的 source format 清單
|
||
- [ ] 確認 webhook push 模式的實作意願 / 時程
|
||
- [ ] 確認 rate limit / quota 政策
|
||
- [ ] 確認產物儲存位置(converter 自己的 bucket,還是回 visionA bucket?)
|
||
- [ ] 提供測試 API key
|
||
|
||
---
|
||
|
||
**雛形實作**:`internal/converter/stub.go` + 前端走 stub 驗流程。
|
||
**Phase 1**:`internal/converter/http.go` 實作上述 API + webhook endpoint。
|