visionA/docs/autoflow/04-architecture/b4-converter-handoff.md
jim800121chen cabbdde495 feat(visionA-backend): DB 接入後續 — OIDC/pairing FK 收尾 + B4 metadata + nginx healthz + 補測試
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>
2026-06-21 06:36:35 +08:00

14 KiB
Raw Blame History

B4 交接檔:轉檔服務端串接 analysis_infoinput_shape metadata

狀態Draft — 給 kneron_model_converter repo 的人實作 作者Architect AgentvisionA 最後更新2026-06-21 跨 repo:本檔在 visionA repodocs/autoflow/04-architecture/),描述的改動全部在 kneron_model_converter repo


1. 目標(一句話)

kneron_model_converterGET /api/v1/jobs/{id} response 帶上 top-level analysis_info 物件(含 input_shape / 維度 metadata使 visionA 能在轉檔完成後顯示 model 的 input_shape。

為什麼這份交接這麼短的工作量

值已經 parse 出來了,只差「往上串」。 bie workerservices/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.gofrontend 也已實作顯示。轉檔端串好的那一刻visionA 自動就能顯示,不需要 visionA 再改任何 code。轉檔端沒串好時 visionA 維持空白、不報錯(全 optional


2. 完整鏈路缺口表

資料從 bie worker 一路傳到 GET /jobs/{id} response 要經過 4 個檔。目前第 1 個檔已有值,後面 3 個檔把值「丟掉」了。

# 檔案 現況 要改什麼
1 services/workers/bie/core.py 已 parseprocess_bie_core() return dict 含 analysis_infoL87-93 不用改(可選:直接補一個 input_shape 陣列欄位,見 §4
2 services/workers/consumer.py _process_message 拿到 resultL206含 analysis_info_push_done(job_id, "ok")L212沒帶它_push_doneL175-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 recordRedis
4b apps/task-scheduler/src/routes/v1/jobs.js serializeJobForResponse(job)L151-227組 GET response body 沒有 analysis_info 欄位 :在 response body 加 top-level analysis_infocompleted 時帶,否則 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 目標 JSONGET /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

  1. input_shape 陣列非空 → 直接用
  2. 否則 batch_size/channels/height/width 四維全齊 → 組 [batch, channel, height, width]NCHW
  3. 四維缺任一nil(不亂組半套維度,防呆)
  4. 都沒有 → nil

全 optional:轉檔端整個 analysis_info 不給、或給空物件 {}、或給部分欄位visionA 都不報錯,維持空白顯示。


4. 兩種表示法都接受(轉檔端可二選一)

bie worker 現在原生輸出的是拆開的四維batch_size/channels/height/widthcore.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-212result 裡的 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_infoonnx / nef stage 的 result.get("analysis_info")None_push_doneif 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-levelinput/parameters/error 同層),不要塞進 metadataparametersvisionA 讀的是 top-level analysis_info
  • completed 與否都可以吐visionA 只在 completed 後才查到 metadata但即使 in_progress 帶上也無害visionA 全 optional。建議有就吐、沒有回 null,最簡單。
  • 這個 helper 同時被 GET /:id 與 GET 列表(listJobsHandler)共用,改一處兩邊都生效。

6. 測試建議(轉檔端怎麼驗)

6.1 端到端驗(最直接)

  1. 跑一個完整轉檔 jobonnx → bie → nef等到 status: completed
  2. GET /api/v1/jobs/{id},確認 response top-level 有 analysis_info,且 batch_size/channels/height/width(或 input_shape)有實際數值(不是 null / 0
  3. 確認數值與 bie worker log 印的維度一致core.py L44-47 讀到的值)

6.2 單元 / 整合驗(對齊既有 test 風格)

  • jobService.advanceJob testanalysisInfogetJob 後確認 job.analysis_info 有寫進去;不傳 → job.analysis_info 維持 undefined
  • serializeJobForResponse testjob record 有 analysis_info → output 有 top-level analysis_info;沒有 → output 為 null
  • 既有 getJobs.integration.test.js 可加一個 casejob record 帶 analysis_info → GET response 帶 analysis_info

6.3 回歸(不要弄壞既有)

  • onnx / nef stage 的 done event 不帶 analysis_info → advanceJob 不應因 undefined 參數報錯(if (analysisInfo && ...) guard
  • 既有 job recordanalysis_info 欄位)→ GET response 的 analysis_infonull,不報錯
  • failJob 路徑不受影響fail 不帶 analysis_info

7. 跨 repo 對接點(給轉檔端參考)

visionA 端讀取位置(轉檔端不用改,僅供對照確認 schema 對齊

項目 位置
visionA unmarshal type visionA-backend/internal/conversion/converter_client.goconverterJobJSON.AnalysisInfoL961/ converterAnalysisInfoJSONL968-976
visionA 對映邏輯 同檔 toInputShape()L985-998+ parseConverterJobL1037-1042
visionA 既有對齊測試 visionA-backend/internal/conversion/converter_client_test.goL1343-14824 個 case明確 input_shape / 四維後備 / 部分維度防呆 / 缺 analysis_info
visionA struct tag權威 schema input_shape / batch_size / channels / height / width / classes / frameworkconverter_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. 額外發現 / 注意事項

  1. bie worker 多一個 input_name 欄位core.py L88visionA 不讀往上傳無害drop 也無害。
  2. bie worker 假設輸入是 4 維 NCHWcore.py L44-47 直接 index dim[0..3]):對非 4 維模型(如某些 NLP / 1D 模型)會 IndexError。這是既有行為、不在本次交接範圍但若未來要支援非影像模型core.py 這段要加防呆。提醒一下、不必現在處理。
  3. classes / framework 目前 bie worker 沒產core.py 的 analysis_info 只有 input_name + 四維,沒有 classes / framework。visionA 這兩個欄位全 optional轉檔端要不要補由你決定。若不補visionA 對應欄位留空,不影響 input_shape 顯示。
  4. done event 的 at-least-once 語意doneListener 的 ACK 在 try 內advanceJob throw 時不 ACK → 重投遞)。advanceJob 寫 analysis_info 是冪等的(同一份 record 重寫同樣的 analysis_info重投遞不會出問題符合既有設計。
  5. stage 順序pipeline 是 onnx → bie → nefjobService.js L46 STAGES。analysis_info 在 bie stage 產出bie 完成時走的是 advanceJob(推進到 nef而非 COMPLETED branch所以一定要在 advanceJob 寫,不能只在 COMPLETED 那段寫,否則會漏。