fix(visionA-frontend): conversion init multipart 對齊 converter 規格 — ref_images + model_id

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) <noreply@anthropic.com>
This commit is contained in:
jim800121chen 2026-05-18 11:21:23 +08:00
parent 9ebf46112b
commit 78c1343e9a
2 changed files with 55 additions and 12 deletions

View File

@ -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-655352026-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.nameconverter 拒非整數)
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");
});

View File

@ -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 0Math.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<ConversionJob>
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`165535 字串使用者編號)+ `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 範圍的隨機整數送給 convertertaskName 留給 promote 流程用。
form.append("model_id", String(generateConverterModelId()));
form.append("version", args.version ?? "v1.0.0");
form.append("platform", targetChipToWire(args.targetChip));