feat(n11): add base64 document staging from e-Court
เพิ่มทางเข้าเอกสารสำหรับ e-Court ที่ไม่มี URL ให้ดึง — POST /api/note/files/stage (x-api-key DOCVER_API_KEY) ส่งเนื้อไฟล์ PDF/รูปเป็น base64 ระบบเก็บใน RAM staging (TTL sliding 8 ชม. + LRU + cap 100MB/ไฟล์ 256MB รวม — ห้ามเขียน disk/DB/MinIO แม้แต่ไบต์เดียวตามโจทย์) ส่วน annotation/bookmark เก็บถาวรตามระบบเดิม: - lib/staged-docs.ts: Map บน globalThis + sentinel "staged:<uuid>.<ext>" ใน pdf_url (ext จาก magic bytes — ไม่ต้อง migration) + hasStagedDoc แยกจาก getStagedDoc - lib/ecourt-fid.ts: external_id เก็บ HMAC-SHA256(pepper, fid) แทน fid ดิบ (env FID_HASH_PEPPER fail-closed) + แปลง row เก่าด้วย npm run migrate:ecourt-fid - route stage: อ่าน body แบบ stream นับไบต์ + idle 60 วิ/total 10 นาที = 408 + concurrency default 1 = 429 + base64 strict + magic bytes กำหนด ext เอง - registerStagedFile: ลำดับกัน race 23505 — stage ใต้ id ก่อน insert โดนชนแล้ว re-stage ใต้ id ผู้ชนะ + try/finally คืน RAM ผู้แพ้ทุกทางออก - ทางอ่าน 4 จุด: /api/note/pdf + export route (410 gate ไทย), resolvePublicPdfPath guard PdfPathError, PDFViewer โหลดผ่าน route + objectURL (gate ครบทุกชนิดไฟล์) - session route (D5): ลิงก์ fid-only ตัดสิน staging ตั้งแต่แลก token — มี bytes ได้ fileId / หมดอายุ fileError "พ้นช่วงจัดเตรียม" / token ออกทุกกรณี - Swagger UI: endpoint ใหม่ + คู่มือ retry (408/429/5xx ยิงซ้ำได้ stage ซ้ำปลอดภัย) - MockStageForm บน /note/dev ทดสอบ 3 ขั้นในหน้าเดียว (ต้องถือ API key จริง) Verify ครบตาม docs/PLAN-STAGED.md: tsc สะอาด + vitest 59 เคสใหม่ผ่าน + E2E browser จริง (textLayer ไทยเรียงถูก / export ฝัง annotation / restart ได้ fileError ตั้งแต่ bootstrap / RAM 100MB: peak WS +580MB commit จริง +92MB) — ผ่าน code review รอบ 7 แก้ครบทุก finding Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
parent
2e951f99f4
commit
a02e983ba8
34 changed files with 2396 additions and 62 deletions
152
lib/openapi.ts
152
lib/openapi.ts
|
|
@ -687,6 +687,146 @@ export const openApiSpec = {
|
|||
},
|
||||
},
|
||||
},
|
||||
"/api/note/files/stage": {
|
||||
post: {
|
||||
tags: ["Judge's Note"],
|
||||
summary: "ส่งเนื้อไฟล์เอกสาร (PDF/รูป) เข้า RAM staging ด้วย base64 (N11)",
|
||||
description:
|
||||
"ทางเข้าเอกสารที่ e-Court **ไม่มี URL ให้ดึง** (เอกสารภายใน/สร้างสด — ต่างจาก " +
|
||||
"N8 ที่ส่ง `fileUrl`) — ส่งเนื้อไฟล์มาเป็น base64 ระบบเก็บใน **RAM ชั่วคราว** " +
|
||||
"(TTL 8 ชม. นับจากการเข้าใช้ล่าสุด — ห้ามเก็บไฟล์ถาวรทุกรูปแบบตามโจทย์) " +
|
||||
"annotation/bookmark เก็บถาวรใน DB ตามระบบเดิม\n\n" +
|
||||
"ได้ `{fileId}` แล้วเอา `fid` **เดิม** ไป mint login token ที่ " +
|
||||
"`POST /api/note/auth/token` → ลิงก์ `/note?t=<access_token>` (ไม่ต้องมี " +
|
||||
"`fileUrl`) ให้ผู้ใช้คลิกตามเส้นทางเดิม\n\n" +
|
||||
"- auth: header `x-api-key` (env `DOCVER_API_KEY` — ตัวเดียวกับทางเข้า mint)\n" +
|
||||
"- `data`: **base64 มาตรฐานต่อเนื่อง** — ห้าม whitespace/line break และไม่รับ " +
|
||||
"base64url (`-`/`_`)\n" +
|
||||
"- ขนาด: เนื้อไฟล์ ≤ 100MB (base64 ยาว ~134MB) — ระบบตรวจ magic bytes เอง " +
|
||||
"(PDF/JPEG/PNG/TIFF) และใช้ผลตรวจเป็นชนิดไฟล์ ไม่เชื่อค่าที่ประกาศ\n" +
|
||||
"- การ stage ด้วย `fid` เดิมซ้ำ = **replace เนื้อล่าสุด** (row เดิม " +
|
||||
"annotation ครบ — พิกัดอาจคลาดถ้าเลย์เอาต์ขยับ เช่นเดียวกับ N8)\n\n" +
|
||||
"**คู่มือ retry:** `408`/`429`/`5xx` = **ยิงซ้ำได้** (429 ตาม header " +
|
||||
"`Retry-After`, ที่เหลือ backoff) และการ stage ซ้ำ **ปลอดภัย** — `fid` เดิม " +
|
||||
"แทนที่ของเดิม ไม่เกิดรายการซ้ำ ไม่เสียอะไร · `400`/`413` = แก้ request " +
|
||||
"ก่อนยิงใหม่ · อัปโหลดช้าถูกตัดที่ 60 วิว่างเปล่า/10 นาทีรวม (408) — " +
|
||||
"ลองส่งใหม่บนเครือข่ายที่ดีกว่า",
|
||||
security: [{ ApiKeyAuth: [] }],
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: {
|
||||
type: "object",
|
||||
required: ["fid", "data"],
|
||||
properties: {
|
||||
fid: {
|
||||
type: "string",
|
||||
description:
|
||||
"รหัสเอกสารของ e-Court (1–200 ตัวอักษร, ห้ามอักขระ & = + %) — ใช้ mint login token " +
|
||||
"ต่อด้วย fid เดิม · ฝั่งระบบนี้เก็บเป็น hash (ไม่มี fid ดิบใน DB) แต่ผู้เรียกยังส่ง " +
|
||||
"fid ดิบมาก่อน hash ทุกเส้น — contract ไม่เปลี่ยน",
|
||||
},
|
||||
data: {
|
||||
type: "string",
|
||||
description:
|
||||
"เนื้อไฟล์ (.pdf/.jpg/.png/.tif) เข้ารหัส base64 มาตรฐานต่อเนื่อง — เนื้อไฟล์ ≤ 100MB",
|
||||
},
|
||||
typeId: {
|
||||
type: "string",
|
||||
description:
|
||||
"รหัสกลุ่มเอกสาร N9 (ทางเลือก) — ส่งมา = ย้ายกลุ่มตามค่าล่าสุด ไม่ส่ง = คงเดิม · " +
|
||||
"⚠️ ของเอกสาร staging รับทางเส้นนี้**ทางเดียว** — typeId ที่แนบมากับ body ของ " +
|
||||
"`POST /api/note/auth/session` (ตอนแลกลิงก์ fid-only) ไม่ถูกใช้กับกลุ่มของไฟล์",
|
||||
},
|
||||
title: {
|
||||
type: "string",
|
||||
description:
|
||||
"ชื่อแสดง (ทางเลือก, ≤ 200 ตัวอักษร) — อัปเดตได้เฉพาะเมื่อชื่อปัจจุบันยังเป็นค่า default " +
|
||||
'"เอกสารจาก e-Court" (ครั้งแรกที่ส่งชื่อจริงชนะแล้วล็อก)',
|
||||
},
|
||||
},
|
||||
},
|
||||
examples: {
|
||||
"stage คำสั่งศาล PDF": {
|
||||
value: {
|
||||
fid: "F123",
|
||||
data: "JVBERi0xLjQK…",
|
||||
title: "คำสั่งศาลที่ 2501/2569",
|
||||
typeId: "GROUP-01",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description:
|
||||
"ลงทะเบียนสำเร็จ — `fileId` คือ id ของ row เอกสาร (ผู้ใช้เข้าผ่านลิงก์ fid ตามปกติ)",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
fileId: {
|
||||
type: "string",
|
||||
description: "uuid ของ row เอกสาร (sentinel staged:<uuid>.<ext> ใน pdf_url)",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
400: {
|
||||
description:
|
||||
"body ผิด (field ที่ไม่รู้จัก / base64 เสีย / magic bytes ไม่ใช่ PDF หรือรูปที่รองรับ / fid มีอักขระ & = + %)",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: { $ref: "#/components/schemas/Error" },
|
||||
},
|
||||
},
|
||||
},
|
||||
401: {
|
||||
description:
|
||||
"ไม่ได้ส่ง header x-api-key หรือค่าไม่ถูกต้อง (env DOCVER_API_KEY — ข้อความเดียวกันทุกสาเหตุ)",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: { $ref: "#/components/schemas/Error" },
|
||||
},
|
||||
},
|
||||
},
|
||||
408: {
|
||||
description:
|
||||
"อ่าน body ช้าเกินกำหนด (ไม่มี chunk 60 วิ / เกิน 10 นาทีรวม) หรือการเชื่อมต่อขาด — ยิงซ้ำได้",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: { $ref: "#/components/schemas/Error" },
|
||||
},
|
||||
},
|
||||
},
|
||||
413: {
|
||||
description:
|
||||
"ไฟล์ใหญ่เกินขนาดที่รองรับ (เนื้อไฟล์เกิน 100MB / base64 เกิน ~140MB) หรือใหญ่เกินโควตารวม — แก้ไฟล์ก่อนส่งใหม่",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: { $ref: "#/components/schemas/Error" },
|
||||
},
|
||||
},
|
||||
},
|
||||
429: {
|
||||
description:
|
||||
"มีการ stage เอกสารอื่นกำลังดำเนินการ (จำกัด concurrent) — ยิงซ้ำตาม header Retry-After",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: { $ref: "#/components/schemas/Error" },
|
||||
},
|
||||
},
|
||||
},
|
||||
500: { $ref: "#/components/responses/ServerError" },
|
||||
},
|
||||
},
|
||||
},
|
||||
"/api/documents/token": {
|
||||
post: {
|
||||
tags: ["documents"],
|
||||
|
|
@ -1033,9 +1173,10 @@ export const openApiSpec = {
|
|||
name: "x-api-key",
|
||||
description:
|
||||
"x-api-key ของ N10 = env EXTERNAL_API_KEY · ของการขอ token กลุ่ม documents = env " +
|
||||
"DOCVER_API_KEY (คนละค่ากับของ N10 — กดปุ่ม Authorize ใส่ครั้งเดียวแล้วใช้ได้ทุก " +
|
||||
"request ของกลุ่มที่ key ตรง) · GET /api/documents/latest พิเศษ: รับได้ทั้งสองค่า " +
|
||||
"(DOCVER_API_KEY **หรือ** EXTERNAL_API_KEY ตัวใดตัวหนึ่งตรง = ผ่าน)",
|
||||
"DOCVER_API_KEY · ของกลุ่ม Judge's Note (mint login-token + stage เอกสาร base64) " +
|
||||
"= env DOCVER_API_KEY เช่นกัน (คนละค่ากับของ N10 — กดปุ่ม Authorize ใส่ครั้งเดียว " +
|
||||
"แล้วใช้ได้ทุก request ของกลุ่มที่ key ตรง) · GET /api/documents/latest พิเศษ: " +
|
||||
"รับได้ทั้งสองค่า (DOCVER_API_KEY **หรือ** EXTERNAL_API_KEY ตัวใดตัวหนึ่งตรง = ผ่าน)",
|
||||
},
|
||||
DocverBearer: {
|
||||
type: "http",
|
||||
|
|
@ -1241,7 +1382,10 @@ export const openApiSpec = {
|
|||
externalFileId: {
|
||||
type: "string",
|
||||
nullable: true,
|
||||
description: "id ของ e-Court (null = ไฟล์ url:-fallback หรืออัปโหลดในกลุ่ม)",
|
||||
description:
|
||||
"ค่า hash ของ fileId (HMAC-SHA256 ทางเดียว — แปลงกลับเป็น fid เดิมไม่ได้ " +
|
||||
"2026-11-04: ระบบเก็บ hash แทน fid ดิบ; ค่านี้คงเดิมทุกครั้งที่ถามด้วย fid เดียวกัน " +
|
||||
"จึงใช้เทียบความเหมือนได้อย่างเดียว · null = ไฟล์ url:-fallback หรืออัปโหลดในกลุ่ม)",
|
||||
},
|
||||
title: { type: "string" },
|
||||
typeId: { type: "string", nullable: true },
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue