DB 接入塊 0-5 上主幹後的收尾工作,讓 DB-on 模式可真人使用 + 補齊功能與測試。 OIDC / pairing FK 修復(接 DB 上線必要): - 新建 internal/user package(User + Store + InMemory + Postgres);OIDC callback 驗證 id_token 成功後 fail-closed upsert users(sub 直接當 users.id,MC sub 為 UUID) - pairing exchange 雲端自建 device(不動 local-tool)+ 同 tx 綁 session token; 自建 device 空 serial 寫 NULL(避免撞 partial unique) - device.SaveTx / session.CreateTx 新增 tx-aware 版本 B4 model metadata: - 轉檔 result 的 analysis_info(input_shape/classes/framework)串進 model: converter_client → flow → adapter → model.Model → PG → ModelResponse DTO - input_shape 優先用陣列、後備四維組 NCHW、缺一不亂組;全 optional 防禦性 - 前端詳細頁顯示(另 repo);轉檔端串接交接檔 b4-converter-handoff.md nginx healthz(部署層): - 新增 /healthz/deep 轉發 backend(ping PG+Redis、down 回 503)給 LB - 修掉 default_server return 444 短路 bug(docker healthcheck 長期 unhealthy 真因) storage error 統一映射(不洩漏 storage 後端細節)。 測試:補 internal/api(storage/errors handler)、cmd/api-server(seed/adapter)、 internal/db(redis)、relay/session 弱處,含 testcontainers integration。 DB 接入相關 package 真環境覆蓋達 88-94%。全程 Reviewer 審查 + 130 真 PG/Redis dbtest 綠。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
14 KiB
B4 交接檔:轉檔服務端串接 analysis_info(input_shape metadata)
狀態:Draft — 給
kneron_model_converterrepo 的人實作 作者:Architect Agent(visionA) 最後更新:2026-06-21 跨 repo:本檔在 visionA repo(docs/autoflow/04-architecture/),描述的改動全部在kneron_model_converterrepo
1. 目標(一句話)
讓 kneron_model_converter 的 GET /api/v1/jobs/{id} response 帶上 top-level analysis_info 物件(含 input_shape / 維度 metadata),使 visionA 能在轉檔完成後顯示 model 的 input_shape。
為什麼這份交接這麼短的工作量
值已經 parse 出來了,只差「往上串」。 bie worker(services/workers/bie/core.py)在量化階段已從 ONNX graph 讀出 batch_size / channels / height / width 並放進 process_bie_core() 的 return dict 的 analysis_info 欄位(L87-93)。但這個 dict 從 worker 回到 consumer 後就被丟掉了 —— consumer 推 done event 時沒帶它,所以一路到 GET /jobs/{id} response 都沒有這個資訊。
visionA 端已全部就緒、等接。 visionA backend 已實作好 analysis_info 的解析(converter_client.go),frontend 也已實作顯示。轉檔端串好的那一刻,visionA 自動就能顯示,不需要 visionA 再改任何 code。轉檔端沒串好時 visionA 維持空白、不報錯(全 optional)。
2. 完整鏈路缺口表
資料從 bie worker 一路傳到 GET /jobs/{id} response 要經過 4 個檔。目前第 1 個檔已有值,後面 3 個檔把值「丟掉」了。
| # | 檔案 | 現況 | 要改什麼 |
|---|---|---|---|
| 1 | services/workers/bie/core.py |
✅ 已 parse,process_bie_core() return dict 含 analysis_info(L87-93) |
不用改(可選:直接補一個 input_shape 陣列欄位,見 §4) |
| 2 | services/workers/consumer.py |
❌ _process_message 拿到 result(L206)含 analysis_info,但 _push_done(job_id, "ok")(L212)沒帶它;_push_done(L175-186)組的 done message 也沒這欄位 |
改:把 result["analysis_info"] 串進 done message |
| 3 | apps/task-scheduler/src/services/doneListener.js |
❌ 解析 done message 只取 { job_id, step, result, reason }(L102),丟掉 analysis_info |
改:解析出 analysis_info,傳給 jobService.advanceJob |
| 4a | apps/task-scheduler/src/services/jobService.js |
❌ advanceJob(jobId, completedStage)(L246)不接收也不持久化 analysis_info |
改:advanceJob 多收一個 analysis_info 參數,寫進 job record(Redis) |
| 4b | apps/task-scheduler/src/routes/v1/jobs.js |
❌ serializeJobForResponse(job)(L151-227)組 GET response body 沒有 analysis_info 欄位 |
改:在 response body 加 top-level analysis_info(completed 時帶,否則 null) |
關鍵理解:done event 是「stage 完成事件」,bie 是 pipeline 第 2 階段(onnx → bie → nef)。analysis_info 是 bie 階段產出的。bie stage 完成時就要把 analysis_info 持久化到 job record,這樣即使後面還有 nef 階段、job record 也已經帶著 analysis_info,等到整個 job COMPLETED 後
GET /jobs/{id}就讀得到。
3. Result schema(一字不差對齊 visionA backend 已定案的讀取格式)
visionA backend 在 GET /api/v1/jobs/{id} response 的 top-level analysis_info 物件(與 input/parameters/error/result_object_keys 同層)讀 metadata。
3.1 目標 JSON(GET /jobs/{id} response,節錄)
{
"job_id": "abc-123",
"status": "completed",
"stage": null,
"progress": 100,
"input": { "...": "..." },
"result_object_keys": { "...": "..." },
"parameters": { "...": "..." },
"error": null,
"analysis_info": {
"input_shape": [1, 3, 224, 224],
"batch_size": 1,
"channels": 3,
"height": 224,
"width": 224,
"classes": ["face", "person"],
"framework": "onnx"
}
}
3.2 欄位定義(對齊 visionA converterAnalysisInfoJSON)
| 欄位 | 型別 | 必填 | 說明 |
|---|---|---|---|
input_shape |
[]int |
optional | 優先表示法。NCHW 順序,如 [1, 3, 224, 224]。給了這個,visionA 直接用 |
batch_size |
int |
optional | 後備:input_shape 缺時用這 4 個維度組 |
channels |
int |
optional | 後備維度 |
height |
int |
optional | 後備維度 |
width |
int |
optional | 後備維度 |
classes |
[]string |
optional | 分類標籤,如 ["face", "person"] |
framework |
string |
optional | 來源框架,如 "onnx" |
3.3 visionA 端對映優先序(已實作、不會變)
visionA 收到 analysis_info 後,這樣決定 input_shape:
input_shape陣列非空 → 直接用- 否則
batch_size/channels/height/width四維全齊 → 組[batch, channel, height, width](NCHW) - 四維缺任一 →
nil(不亂組半套維度,防呆) - 都沒有 →
nil
全 optional:轉檔端整個 analysis_info 不給、或給空物件 {}、或給部分欄位,visionA 都不報錯,維持空白顯示。
4. 兩種表示法都接受(轉檔端可二選一)
bie worker 現在原生輸出的是拆開的四維(batch_size/channels/height/width,core.py L89-92),沒有 input_shape 陣列。
visionA 兩種都接:
| 轉檔端給法 | visionA 處理 | 建議 |
|---|---|---|
(a) 直接給 input_shape: [1,3,224,224] 陣列 |
走優先序 #1,直接用 | ✅ 更推薦 — 語意最明確、未來若有非 4 維 shape(如 NLP 模型)也能表達 |
(b) 給拆開的 batch_size/channels/height/width 四維 |
走優先序 #2,組成 NCHW | ✅ 也可 — bie worker 現成欄位,改動最小 |
最省事路徑(推薦):consumer / doneListener / jobService 在傳遞時,原封不動把 bie worker 的 analysis_info dict 往上傳(裡面已含 batch_size/channels/height/width),最後 serializeJobForResponse 直接吐出去。visionA 走後備路徑 #2 即可。如果順手在 core.py 補一個 input_shape 陣列欄位更好,但非必要。
⚠️ 注意:bie worker 的
analysis_info還有一個input_name欄位(core.py L88)。visionA 不讀它,無害,可一起往上傳、也可在某一層 drop 掉,都行。
5. 各檔要改的細節
以下是「建議改法」,實際命名 / 風格依轉檔端 repo 慣例。核心是讓 analysis_info 一路傳到 job record,再從 GET response 吐出。
5.1 services/workers/consumer.py(檔 #2)
_push_done 目前不帶 metadata。把 bie 的 analysis_info 串進去:
def _push_done(self, job_id, result, reason=None, analysis_info=None):
message = {
"job_id": job_id,
"step": self.stage,
"result": result,
"completed_at": time.strftime("%Y-%m-%dT%H:%M:%S%z"),
}
if reason:
message["reason"] = reason
if analysis_info: # ← 新增
message["analysis_info"] = analysis_info
self.client.xadd("queue:done", {"data": json.dumps(message)})
_process_message 成功路徑(L211-212)把 result 裡的 analysis_info 帶上:
result = self.process_fn(input_paths, output_path, parameters)
self._upload_output(job_id, job_dir)
logger.info(...)
self._push_done(job_id, "ok", analysis_info=result.get("analysis_info")) # ← 改
只有 bie stage 的
result有 analysis_info;onnx / nef stage 的result.get("analysis_info")是None,_push_done的if analysis_info自然 skip,無害。
5.2 apps/task-scheduler/src/services/doneListener.js(檔 #3)
L101-102 解析 done message 時多解一個欄位,並傳給 advanceJob:
const data = JSON.parse(fields[1]);
const { job_id, step, result, reason, analysis_info } = data; // ← 多解 analysis_info
if (result === 'ok') {
await jobService.advanceJob(job_id, step, analysis_info); // ← 多傳一個參數
} else {
await jobService.failJob(job_id, step, reason || 'Unknown error');
}
5.3 apps/task-scheduler/src/services/jobService.js(檔 #4a)
advanceJob 多收 analysis_info,寫進 job record(持久化到 Redis):
async function advanceJob(jobId, completedStage, analysisInfo) { // ← 多收參數
const job = await getJob(jobId);
if (!job) { /* ...既有... */ return; }
const currentIndex = STAGES.indexOf(completedStage);
if (currentIndex < 0) { /* ...既有... */ return; }
recordStageComplete(job, completedStage);
// ← 新增:bie stage 帶 analysis_info 時,持久化到 job record
// (bie 是中間 stage;先寫進 record,等 job COMPLETED 後 GET 就讀得到)
if (analysisInfo && typeof analysisInfo === 'object') {
job.analysis_info = analysisInfo;
}
// ...既有的推進 / COMPLETED 邏輯不變(setJob 會把 job.analysis_info 一起寫進 Redis)...
}
為什麼寫在
advanceJob而非等 COMPLETED 才寫:bie 完成時呼叫的是advanceJob(jobId, 'bie', analysisInfo),此時 stage 推進到 nef、job 還沒 COMPLETED。把 analysis_info 在這一刻寫進 record,後續 nef 完成走 COMPLETED branch 時 record 已帶著它(setJob是整個 record 序列化寫 Redis,不會掉欄位)。
5.4 apps/task-scheduler/src/routes/v1/jobs.js(檔 #4b)
serializeJobForResponse(job)(L151-227)的 return 物件加一個 top-level analysis_info:
return {
job_id: job.job_id,
user_id: job.user_id || null,
status: externalStatus,
// ...既有欄位...
parameters,
metadata,
analysis_info: // ← 新增 top-level 欄位
job.analysis_info && typeof job.analysis_info === 'object'
? job.analysis_info
: null,
created_by_client_id: job.created_by_client_id || null,
};
- 放 top-level(與
input/parameters/error同層),不要塞進metadata或parameters,visionA 讀的是 top-levelanalysis_info。- completed 與否都可以吐(visionA 只在 completed 後才查到 metadata;但即使 in_progress 帶上也無害,visionA 全 optional)。建議有就吐、沒有回 null,最簡單。
- 這個 helper 同時被 GET /:id 與 GET 列表(
listJobsHandler)共用,改一處兩邊都生效。
6. 測試建議(轉檔端怎麼驗)
6.1 端到端驗(最直接)
- 跑一個完整轉檔 job(onnx → bie → nef),等到
status: completed GET /api/v1/jobs/{id},確認 response top-level 有analysis_info,且batch_size/channels/height/width(或input_shape)有實際數值(不是 null / 0)- 確認數值與 bie worker log 印的維度一致(core.py L44-47 讀到的值)
6.2 單元 / 整合驗(對齊既有 test 風格)
jobService.advanceJobtest:傳analysisInfo→getJob後確認job.analysis_info有寫進去;不傳 →job.analysis_info維持 undefinedserializeJobForResponsetest:job record 有analysis_info→ output 有 top-levelanalysis_info;沒有 → output 為null- 既有
getJobs.integration.test.js可加一個 case:job record 帶 analysis_info → GET response 帶 analysis_info
6.3 回歸(不要弄壞既有)
- onnx / nef stage 的 done event 不帶 analysis_info →
advanceJob不應因 undefined 參數報錯(if (analysisInfo && ...)guard) - 既有 job record(無
analysis_info欄位)→ GET response 的analysis_info回null,不報錯 failJob路徑不受影響(fail 不帶 analysis_info)
7. 跨 repo 對接點(給轉檔端參考)
visionA 端讀取位置(轉檔端不用改,僅供對照確認 schema 對齊):
| 項目 | 位置 |
|---|---|
| visionA unmarshal type | visionA-backend/internal/conversion/converter_client.go → converterJobJSON.AnalysisInfo(L961)/ converterAnalysisInfoJSON(L968-976) |
| visionA 對映邏輯 | 同檔 toInputShape()(L985-998)+ parseConverterJob(L1037-1042) |
| visionA 既有對齊測試 | visionA-backend/internal/conversion/converter_client_test.go(L1343-1482,4 個 case:明確 input_shape / 四維後備 / 部分維度防呆 / 缺 analysis_info) |
| visionA struct tag(權威 schema) | input_shape / batch_size / channels / height / width / classes / framework(converter_client.go L969-975) |
驗證對齊的最快方法:把 §3.1 的 JSON 丟進 visionA 的 parseConverterJob(或對照 converter_client_test.go L1361-1372 的 test fixture),確認 cj.InputShape == [1,3,224,224]。轉檔端 GET response 只要長得跟那個 test fixture 一樣,就一定對得上。
8. 額外發現 / 注意事項
- bie worker 多一個
input_name欄位(core.py L88):visionA 不讀,往上傳無害,drop 也無害。 - bie worker 假設輸入是 4 維 NCHW(core.py L44-47 直接 index
dim[0..3]):對非 4 維模型(如某些 NLP / 1D 模型)會 IndexError。這是既有行為、不在本次交接範圍,但若未來要支援非影像模型,core.py 這段要加防呆。提醒一下、不必現在處理。 - classes / framework 目前 bie worker 沒產:core.py 的 analysis_info 只有 input_name + 四維,沒有
classes/framework。visionA 這兩個欄位全 optional,轉檔端要不要補由你決定。若不補,visionA 對應欄位留空,不影響 input_shape 顯示。 - done event 的 at-least-once 語意:doneListener 的 ACK 在 try 內(advanceJob throw 時不 ACK → 重投遞)。
advanceJob寫 analysis_info 是冪等的(同一份 record 重寫同樣的 analysis_info),重投遞不會出問題,符合既有設計。 - stage 順序:pipeline 是
onnx → bie → nef(jobService.js L46STAGES)。analysis_info 在 bie stage 產出,bie 完成時走的是advanceJob(推進到 nef)而非 COMPLETED branch,所以一定要在 advanceJob 寫,不能只在 COMPLETED 那段寫,否則會漏。