From 78c1343e9a9699d1e86015486b9389a03a04ca96 Mon Sep 17 00:00:00 2001 From: jim800121chen Date: Mon, 18 May 2026 11:21:23 +0800 Subject: [PATCH] =?UTF-8?q?fix(visionA-frontend):=20conversion=20init=20mu?= =?UTF-8?q?ltipart=20=E5=B0=8D=E9=BD=8A=20converter=20=E8=A6=8F=E6=A0=BC?= =?UTF-8?q?=20=E2=80=94=20ref=5Fimages=20+=20model=5Fid?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 0.8b stage e2e 連環 2 個跨層 schema mismatch(5/2 寫 frontend 時對 converter spec 沒驗、5/16 stage e2e 第一次跑通才暴露)。 Bug #5 ref_images field name PHP-style 不對齊 - 現象:converter multer LIMIT_UNEXPECTED_FILE、visionA 透傳 → 400 validation_failed - 原因:frontend `form.append('ref_images[]', img)`、converter `uploader.fields([{name:'ref_images',...}])`(Express/multer 標準、無 []) - 修法:frontend `form.append('ref_images', img)`(FormData 多筆同 key、multer 自動收成陣列) Bug #6 model_id 用 taskName 當值、converter 要 integer - 現象(修完 Bug #5 後暴露):converter validator → 400 validation_error 「model_id 必須為非負整數」 - 原因:frontend `args.taskName ?? args.file.name` → 字串 "input" / "yolov5s_test"; converter validator (`createJob.js:153-164`) 規定 integer 1-65535(KTC tool 內部 model 編號) - 修法:新增 `generateConverterModelId()` helper(Math.random 1-65535)、每次 init 自動生成; taskName 留給 visionA UX(promote 後 model store 的 name),與 converter model_id 解耦 驗證: - pnpm test src/lib/api/conversion.test 22/22 pass - pnpm build 通過 - stage redeploy 兩次(commit ce6a657 後跑出 Bug #5、fix Bug #5 deploy 後跑出 Bug #6、fix Bug #6 已 deploy) 剩餘 e2e 待 user browser 真實上傳 ONNX file 驗證。 Co-Authored-By: Claude Opus 4.7 (1M context) --- .../src/lib/api/conversion.test.ts | 29 ++++++++++---- visionA-frontend/src/lib/api/conversion.ts | 38 ++++++++++++++++--- 2 files changed, 55 insertions(+), 12 deletions(-) diff --git a/visionA-frontend/src/lib/api/conversion.test.ts b/visionA-frontend/src/lib/api/conversion.test.ts index e489692..7892415 100644 --- a/visionA-frontend/src/lib/api/conversion.test.ts +++ b/visionA-frontend/src/lib/api/conversion.test.ts @@ -151,12 +151,17 @@ describe("initConversion", () => { expect(form).toBeInstanceOf(FormData); // model 檔 expect((form!.get("model") as File).name).toBe("yolov5s.onnx"); - // ref_images[](多筆同 key) - const refs = form!.getAll("ref_images[]"); + // ref_images(多筆同 key — converter multer 'ref_images' field、2026-05-18 從 'ref_images[]' 改正) + const refs = form!.getAll("ref_images"); expect(refs.length).toBe(1); expect((refs[0] as File).name).toBe("ref.jpg"); - // text fields — backend 用 `model_id` 欄裝 taskName、`platform` 用 wire 數字格式 - expect(form!.get("model_id")).toBe("yolov5s_test"); + // text fields — converter 規格:`model_id` integer 1-65535(2026-05-18 從 taskName 改自動生成) + const modelIdStr = form!.get("model_id"); + expect(typeof modelIdStr).toBe("string"); + const modelIdNum = Number(modelIdStr); + expect(Number.isInteger(modelIdNum)).toBe(true); + expect(modelIdNum).toBeGreaterThanOrEqual(1); + expect(modelIdNum).toBeLessThanOrEqual(65535); expect(form!.get("platform")).toBe("720"); expect(form!.get("version")).toBe("v1.0.0"); @@ -182,10 +187,12 @@ describe("initConversion", () => { expect(job.stage).toBe("onnx"); }); - it("沒給 taskName 時用檔名當 model_id", async () => { + it("model_id 一律自動生成 1-65535 整數(與 taskName 解耦)", async () => { + // 2026-05-18 從「沒 taskName 時用檔名」改成「自動生成」測試。 + // 理由:converter validator 規定 model_id integer 1-65535、frontend 不該用 taskName / 檔名。 const { instances } = installMockXHR(); const file = new File(["x"], "model.onnx"); - const promise = initConversion({ file, targetChip: "KL520" }); + const promise = initConversion({ file, targetChip: "KL520", taskName: "myTask" }); const xhr = instances[0]; xhr._fireLoad(200, { success: true, @@ -200,7 +207,15 @@ describe("initConversion", () => { }, }); await promise; - expect(xhr._formDataSent!.get("model_id")).toBe("model.onnx"); + const modelIdStr = xhr._formDataSent!.get("model_id"); + expect(typeof modelIdStr).toBe("string"); + const modelIdNum = Number(modelIdStr); + // 不應該等於 taskName 或 file.name(converter 拒非整數) + expect(modelIdStr).not.toBe("myTask"); + expect(modelIdStr).not.toBe("model.onnx"); + expect(Number.isInteger(modelIdNum)).toBe(true); + expect(modelIdNum).toBeGreaterThanOrEqual(1); + expect(modelIdNum).toBeLessThanOrEqual(65535); expect(xhr._formDataSent!.get("platform")).toBe("520"); }); diff --git a/visionA-frontend/src/lib/api/conversion.ts b/visionA-frontend/src/lib/api/conversion.ts index 1238ada..38daca2 100644 --- a/visionA-frontend/src/lib/api/conversion.ts +++ b/visionA-frontend/src/lib/api/conversion.ts @@ -112,6 +112,29 @@ function targetChipToWire(chip: TargetChip): string { return chip.replace(/^KL/, ""); } +/** + * 生成 1-65535 範圍的 converter `model_id`(KTC tool 內部 model 編號)。 + * + * 為什麼這樣設計: + * - converter scheduler validator (`createJob.js:153-164`) 規定 `model_id` 必須是 + * 1 ≤ x ≤ 65535 的整數 + * - visionA frontend conversion form 沒有對應 UI 欄位(user 從不知這個 ID) + * - taskName 是 visionA UX 概念(任務名稱 / promote 後 model store 的 name), + * 不能當 converter model_id 用 + * - 簡單做法:每次 init 都隨機生一個 1-65535、給 converter 用一次性 ID + * + * 採用 Math.random: + * - 不需要密碼學強度的隨機(converter 不靠 model_id 做 auth / dedup) + * - 範圍 1-65535(避開 0):Math.floor(Math.random() * 65535) + 1 + * - 碰撞機率約 1/65535 — 對 user 一次轉檔的場景可接受;converter 端 model_id + * 並非唯一鍵(job_id 才是),即使碰撞也只是同號被覆用、不阻擋 e2e + * + * 2026-05-18 e2e debug 新增。 + */ +function generateConverterModelId(): number { + return Math.floor(Math.random() * 65535) + 1; +} + /** backend status `created` / `completed` → UI `queued` / `succeeded` */ function normalizeStatus(raw: unknown): ConversionStatus { const v = String(raw ?? "").toLowerCase(); @@ -204,13 +227,18 @@ export function initConversion(args: InitConversionArgs): Promise form.append("model", args.file); if (args.refImages && args.refImages.length > 0) { for (const img of args.refImages) { - // backend 接 `ref_images[]`(converter 規格)— FormData 多筆同 key 即可 - form.append("ref_images[]", img); + // converter scheduler multer `uploader.fields([{name:'ref_images',...}])` 預期 name='ref_images' + // (不是 PHP/Rails 風格的 'ref_images[]')。FormData 多筆同 key 即可、multer 收成陣列。 + // 2026-05-18 e2e debug:原本 'ref_images[]' → multer LIMIT_UNEXPECTED_FILE → visionA 回 400 validation_failed + form.append("ref_images", img); } } - // backend 必填欄位:`model_id`(1–65535 字串使用者編號)+ `version` + `platform` - // task spec 用 taskName 作 model_id(轉檔任務的顯示名稱),非 model 庫的 model_id - form.append("model_id", args.taskName ?? args.file.name); + // backend 必填欄位:`model_id`(converter 規格:integer,範圍 1-65535)+ `version` + `platform` + // 2026-05-18 e2e debug:原本送 args.taskName("input" 之類字串)→ converter validator + // `model_id 必須為非負整數` 拒絕。taskName 是 visionA UX 概念(轉檔任務顯示名 / promote + // 到 model store 的 name),與 converter 內部 KTC tool model_id 是不同 namespace。 + // 修法:自動生成 1-65535 範圍的隨機整數送給 converter;taskName 留給 promote 流程用。 + form.append("model_id", String(generateConverterModelId())); form.append("version", args.version ?? "v1.0.0"); form.append("platform", targetChipToWire(args.targetChip));