ข้ามไปยังเนื้อหาหลัก
Thinker Docs
ดาวน์โหลด .md/docs/agent.th.md

คู่มือสำหรับ Agent — Thinker Docs

คุณคือ agent ที่กำลังเชื่อมต่อกับ Thinker Docs: บริการ on-prem ที่แปลง เทมเพลตที่เผยแพร่แล้ว + ข้อมูล JSONไฟล์ PDF

เอกสารนี้เขียนให้ agent อ่าน ไม่ใช่ให้คนอ่าน มันบอก การตัดสินใจที่คุณต้องทำ, โครงสร้างข้อมูลที่แน่นอน และที่สำคัญที่สุดคือ ความผิดพลาดที่ดูเหมือนถูก ทุกอย่างในนี้เขียนจากโค้ดที่รันอยู่จริง — ถ้าเอกสารนี้ขัดกับโค้ด โค้ดถูก และเอกสารนี้คือบั๊ก


ลำดับการทำงาน (Sequence Diagram)#

sync — ได้รับไฟล์ PDF กลับมาทันที#

async — ได้ลิงก์ (ไฟล์ถูกเก็บไว้ให้)#

attach — รวมเอกสาร แล้วเลขหน้าถูกคำนวณใหม่#


0. โมเดลของระบบใน 1 ย่อหน้า#

คนอัปโหลด PDF เดิมของธนาคาร ลากฟิลด์ไปวางบนนั้น แล้ว เผยแพร่ การเผยแพร่จะแช่แข็ง เวอร์ชัน หนึ่งไว้ พร้อม สัญญาฟิลด์ (field contract) ที่คงที่ตลอดไป คุณส่ง data ที่ตรงกับสัญญานั้น ระบบพิมพ์ลงบน PDF แล้วคืนเอกสารให้ คุณไม่เคยออกแบบเทมเพลต และไม่เคยส่งฟิลด์ที่สัญญาไม่ได้ระบุไว้


1. ตัดสินใจ 3 อย่างก่อนเรียกครั้งแรก#

1.1 mode — ตัวนี้ตัวเดียวที่ตัดสินว่าคุณจะได้อะไรกลับ#

modeได้อะไรกลับเก็บไฟล์ไหมใช้เมื่อ
"sync"ไฟล์ PDF (Content-Type: application/pdf)ไม่เก็บ (แต่ยังลง audit log)ส่งไฟล์ต่อให้ผู้เรียกทันที ไม่ต้องเก็บ
"async"JSON { documentId, url, sha256, expiresAt }เก็บต้องแนบเอกสารกับเคส หรือต้องใช้อีกภายหลัง

ความผิดพลาดที่ agent ทำบ่อยที่สุด: คาดว่าจะได้ทั้งสองอย่าง คุณได้ ไฟล์ หรือ ลิงก์ อย่างใดอย่างหนึ่ง ไม่มีวันได้ทั้งคู่ และไม่มี options ตัวไหนเปลี่ยนเรื่องนี้ได้ — ไม่ใช่ attach ไม่ใช่ watermark ไม่ใช่ protection มีแต่ mode เท่านั้น

url มีอายุจำกัด อย่าเก็บมันไว้ ให้เก็บ documentId แล้วขอลิงก์ใหม่จาก GET /v1/documents/:id เมื่อไหร่ก็ได้

1.2 ระบุ version เสมอ#

{ "templateId": "…", "version": 2 }   // ← ส่งเสมอ

ถ้าไม่ส่ง version ระบบจะใช้เวอร์ชันที่เผยแพร่อยู่ ณ ขณะนั้น — พรุ่งนี้มีคนเผยแพร่ v3 ระบบของคุณจะเริ่มผลิตเอกสารคนละแบบโดยไม่มีใครรู้ ให้ระบุเวอร์ชันที่คุณทดสอบไว้ templateId ไม่เคยเปลี่ยน มีแต่ version ที่ขยับ

1.3 ถามสัญญาฟิลด์ อย่าเดา#

GET /v1/templates/{templateId}/fields?version={n}
X-Api-Key: $THINKER_DOCS_API_KEY          # scope: templates:read
200 OK
{
  "templateId": "50e3a8aa-…",
  "version": 2,
  "status": "published",
  "fields": [
    { "name": "case_id", "type": "text", "required": true },
    { "name": "loanAmount", "type": "number", "format": { "decimals": 2, "prefix": "฿" } },
    { "name": "signedOn", "type": "date", "format": { "calendar": "buddhist", "pattern": "d MMMM yyyy" } }
  ]
}

ส่ง name ให้ตรงตามนี้ทุกประการ ฟิลด์ที่คุณคิดขึ้นเองจะถูกปฏิเสธ ฟิลด์บังคับที่คุณลืมส่งก็ถูกปฏิเสธ สัญญานี้ถูกสร้างจากตัวเทมเพลตเอง จึงไม่มีทางไม่ตรงกับสิ่งที่ generate บังคับใช้

ส่วนใหญ่ type คือ text/number/date แต่ Field Discovery อาจคืน type อื่นที่ค่าใน data ต้องส่งให้ถูกรูป มิฉะนั้นได้ invalid_data:

  • imageData URL เท่านั้น ("data:image/png;base64,…") ไม่รับ URL แบบ http(s) และภาพต้องไม่เกิน 30 ล้านพิกเซล
  • qrcode — ข้อความที่จะเข้ารหัสเป็น QR (เช่นลิงก์ยืนยันเอกสาร)
  • tableอาเรย์ 2 มิติของสตริง เช่น [["Alice","NY"],["Bob","LA"]] หรือสตริง JSON ของอาเรย์นั้น (แต่ละแถวคือหนึ่งบรรทัด) สูงสุด 2,000 แถว / 20,000 เซลล์
  • svgมาร์กอัป SVG อินไลน์ ("<svg …>…</svg>"ไม่ใช่ data URL) ระบบปฏิเสธ <script>, ตัวจัดการ on*=, <!DOCTYPE>/<!ENTITY> และการอ้างอิงภายนอก (href/xlink:href/src) — ถ้ามีจะได้ invalid_data

2. สร้างเอกสาร#

POST /v1/generate
X-Api-Key: $THINKER_DOCS_API_KEY          # scope: generate
Content-Type: application/json
{
  "templateId": "50e3a8aa-…",
  "version": 2,
  "mode": "async",
  "data": {
    "case_id": "LN-2569-001",
    "loanAmount": 1250000,
    "signedOn": "2026-05-19"
  },
  "options": { /* ไม่บังคับ — ดู §3 */ }
}

ส่งค่าเป็นชนิดข้อมูล JSON ตามธรรมชาติ อย่าจัดรูปแบบมาให้ ส่งตัวเลข 1250000 ไม่ใช่ "฿ 1,250,000.00" — เทมเพลตเป็นเจ้าของการจัดรูปแบบ การแปลง พ.ศ. และเลขไทย ถ้าคุณจัดรูปแบบมาเอง เอกสารจะถูกจัดรูปแบบสองรอบ

วันที่ใช้ ISO: "2026-05-19" เทมเพลตเป็นคนตัดสินว่าจะพิมพ์เป็น 19/05/2026 หรือ 19 พฤษภาคม 2569 อย่าบวก 543 เอง


3. options — และตัวที่จะทำให้คุณพลาด#

metadata — ตั้งชื่อเอกสาร#

"options": { "metadata": { "title": "สัญญาเงินกู้ LN-2569-001", "subject": "case=LN-2569-001" } }

⚠️ metadata ของ PDF ไม่เคยถูกเข้ารหัส และติดไปกับไฟล์ตลอดกาล ใครเปิดดู properties ก็อ่านได้ ใส่ได้แค่ ตัวอ้างอิงห้ามใส่ชื่อลูกค้า เลขบัตรประชาชน หรือจำนวนเงิน ไม่มีระบบไหนบังคับเรื่องนี้ได้ มีแต่คุณที่รู้ว่าค่าที่ใส่หมายถึงอะไร

protection — ใส่รหัสผ่านให้ไฟล์ของผู้รับ#

"options": {
  "protection": {
    "userPassword": "…",                                   // ใช้เปิดไฟล์
    "ownerPassword": "…",                                  // ถ้าไม่ระบุ ใช้ userPassword
    "permissions": { "print": true, "copy": false, "modify": false }
  }
}

AES-256 ผ่าน qpdf ต้องมีรหัสผ่านอย่างน้อยหนึ่งตัว (มิฉะนั้นจะได้รับ 400) หากเซิร์ฟเวอร์ไม่มี qpdf จะตอบ 503 protection_unavailable — ระบบ จะไม่ส่งไฟล์ที่ไม่ได้เข้ารหัสกลับมาแทน

รหัสผ่านไม่ถูก log ไม่ถูกเก็บ ไม่ถูกใส่ใน metadata และไม่ถูกส่งกลับ audit log บันทึกแค่ ว่า เอกสารถูกป้องกัน — ไม่เคยบันทึกว่าด้วยอะไร อย่าคาดหวังว่าจะอ่านมันกลับมาได้ และอย่านำไปใส่ใน metadata

⚠️ รหัสผ่าน PDF ไม่ใช่การเข้ารหัสข้อมูลที่จัดเก็บ (encryption at rest) มันปกป้องแค่ไฟล์ของผู้รับเท่านั้น

attach — เอาหน้าของเอกสารอื่นมาต่อท้าย#

"options": { "attach": ["8108147a-…", "c3f9b1e2-…"] }     // สูงสุด 10

id เหล่านี้คือ documentId จากการเรียก async ก่อนหน้า — ไม่มีที่มาอื่นเลย ดังนั้น attach แปลว่าคุณต้องเคยเรียก async มาแล้ว และ id จะถูกตรวจกับ ทีมของคุณเอง — id ของทีมอื่นจะได้ 404 เหมือนกับ id ที่ไม่เคยมีอยู่จริง

เลขหน้าจะถูกคำนวณใหม่ตามเอกสารที่รวมแล้ว ท้ายกระดาษที่เขียนว่า หน้า {currentPage} จาก {totalPages} จะนับหน้าที่มีอยู่จริงหลัง merge — สัญญา 2 หน้า + สำเนาบัตร 2 หน้า จะขึ้นว่า "หน้า 3 จาก 4" ที่หน้า 3 คุณไม่ต้องทำอะไรเพิ่ม และอย่าพยายามชดเชยเอง

watermark — ประทับสถานะ#

"options": { "watermark": { "text": "ฉบับร่าง", "opacity": 0.15 } }   // ข้อความไม่เกิน 60 ตัวอักษร

ประทับ ทุกหน้า รวมถึงหน้าที่แนบมาด้วย

หมายเหตุ: เทมเพลตอาจมี ฟิลด์ลายน้ำ ของตัวเองอยู่แล้ว ซึ่งคนออกแบบเป็นคนวาง ส่วน option นี้คือลายน้ำที่ คุณ ตัดสินใจเป็นราย request — เช่นเพราะเคสอยู่ในสถานะไหนใน workflow ซึ่งคนออกแบบเทมเพลตไม่มีทางรู้ แต่คุณรู้

bulkOutput — ตอบ 501#

ยังไม่ได้สร้าง มันจะ ถูกปฏิเสธ ไม่ใช่ถูกเมิน ผู้เรียกที่ขอ bulk output แล้วได้ PDF ใบเดียวกลับไปคือการถูกโกหก ระบบจึงเลือกที่จะล้มเหลวแทน


4. ดึงเอกสารแบบ async#

GET /v1/documents/{documentId}
X-Api-Key: $THINKER_DOCS_API_KEY          # scope: documents:read
  • ปกติ → JSON พร้อม url ที่ ออกใหม่ (ลิงก์จาก generate หมดอายุไปแล้ว)
  • ?download=1 → ส่งไฟล์ PDF กลับมาโดยตรง

เอกสารของทีมอื่น → 404 · เลย expiresAt แล้ว → 410 Gone (ไฟล์มี PII จึงถูกลบตามกำหนด แต่ audit row ยังอยู่เพื่อพิสูจน์ว่าเคยมี)


5. ข้อผิดพลาด — ให้แตกเงื่อนไขที่ code ไม่ใช่ที่ข้อความ#

HTTPcodeคุณควรทำอะไร
400invalid_datadata ผิดสัญญา คำตอบมี fields: [{field, reason}] มาให้ แก้แล้วส่งใหม่ — ห้ามส่งซ้ำแบบเดิม
400bad_requestbody ผิดรูป, mode ผิด, protection ไม่มีรหัสผ่าน, watermark.text ยาวเกิน, เรียก async บนฉบับร่าง
401unauthenticatedไม่มีหรือ X-Api-Key ผิด
403insufficient_scopeคีย์ไม่มี scope ที่การเรียกนี้ต้องใช้
404not_foundไม่พบ หรือไม่ใช่ของทีมคุณ — แยกไม่ออกโดยเจตนา
410goneเอกสารหมดอายุและถูกลบแล้ว
413payload_too_largebody เกิน 1 MB
422render_failedเทมเพลต เรนเดอร์ไม่ผ่าน (รวมถึงกรณีเรนเดอร์นานเกินกำหนดแล้วถูกตัดจบ) ไม่ใช่ความผิดของข้อมูลคุณ อย่าลองใหม่ ให้แจ้งเจ้าของเทมเพลต
429rate_limitedเคารพ Retry-After หน่วงเวลาก่อนเรียกซ้ำ อย่าเรียกถี่ต่อเนื่อง (เพดานค่าเริ่มต้น 120 คำขอ/นาที ต่อคีย์)
501not_implementedคุณขอ option ที่ยังไม่มี
503protection_unavailableเซิร์ฟเวอร์ไม่มี qpdf — ภาวะถาวร ห้าม fallback ไปใช้ไฟล์ที่ไม่ได้ล็อก และห้ามลองใหม่
503overloadedเซิร์ฟเวอร์กำลังเรนเดอร์เต็มกำลัง — ภาวะชั่วคราว เคารพ Retry-After แล้วลองใหม่แบบ exponential backoff (แยกจาก protection_unavailable ให้ดูที่ code ไม่ใช่แค่สถานะ 503)

ตัวอย่าง invalid_data — ความล้มเหลวที่พบบ่อยที่สุด ถูกจับที่ชายแดนก่อนที่จะสร้าง PDF แม้แต่ไบต์เดียว:

400 Bad Request
{
  "error": "data does not match the template field contract",
  "code": "invalid_data",
  "fields": [
    { "field": "loanAmount", "reason": "expected a number" },
    { "field": "case_id",    "reason": "required" }
  ]
}

6. กฎที่ห้ามละเมิด#

  1. 404 แปลว่า "ไม่ใช่ของคุณ หรือ ไม่มีอยู่" และคุณแยกไม่ออก นี่คือโมเดลการแยกข้อมูล ไม่ใช่ช่องโหว่ของ API ห้ามสุ่มส่ง id เพื่อหาความแตกต่าง ทุกครั้งที่ถูกปฏิเสธจะถูกบันทึกใน audit log ด้วย outcome: denied
  2. ห้าม log data มันมี PII — ชื่อ เลขบัตรประชาชน จำนวนเงิน ระบบไม่ log มัน และคุณก็ห้าม
  3. ห้ามส่งรหัสผ่านไปที่อื่นนอกจาก options.protection ไม่ใส่ใน metadata ไม่ใส่ใน log ไม่ใส่ในคิว retry ที่คุณเก็บลงดิสก์
  4. ห้ามลองใหม่กับ 422 render_failed และ 400 ทั้งคู่เป็น deterministic ลองใหม่ก็ได้ผลเดิม และทำให้ audit log สกปรก
  5. API key คืออัตลักษณ์ของเครื่อง ที่ถือสิทธิ์ของ "ทีม" ไม่ใช่ของคน มันไม่หยุดทำงานเมื่อพนักงานลาออก การหยุด integration คือ revoke คีย์ ไม่ใช่จัดการที่ตัวคน
  6. คีย์เรนเดอร์ฉบับร่างไม่ได้ ฉบับร่างยังไม่ผ่าน lint จึงเป็นเทมเพลตที่ระบบเชื่ออยู่แล้วว่าจะผลิตเอกสารผิด เรียก async บนฉบับร่าง → 400

7. sha256 — วิธีพิสูจน์ว่าเอกสารไหนคือของคุณ#

ทุกคำตอบมี sha256 ให้ hash ไฟล์ที่คุณถืออยู่แล้วเทียบ มันคือหลักฐานการกำเนิด: ถ้าต้องพิสูจน์ว่า PDF ใบไหนถูกออกให้เคสที่เป็นข้อพิพาท ให้เทียบ hash กับแถวใน documents / audit_logโดยไม่ต้องเก็บ PII ไว้เลยแม้แต่น้อย


8. คำสั่งทดสอบแบบคัดลอกไปวางได้#

# 1. ถามสัญญาฟิลด์
curl -s "$BASE/v1/templates/$TID/fields?version=2" -H "X-Api-Key: $THINKER_DOCS_API_KEY"

# 2. สร้างเอกสาร (async) → ได้ { documentId, url, sha256, expiresAt }
curl -s -X POST "$BASE/v1/generate" \
  -H "X-Api-Key: $THINKER_DOCS_API_KEY" -H 'Content-Type: application/json' \
  -d "{\"templateId\":\"$TID\",\"version\":2,\"mode\":\"async\",\"data\":{\"case_id\":\"demo-1\"}}"

# 3. ดึงไฟล์ที่เก็บไว้ (ลิงก์จากข้อ 2 หมดอายุ อันนี้สดเสมอ)
curl -s "$BASE/v1/documents/$DOCID?download=1" -H "X-Api-Key: $THINKER_DOCS_API_KEY" -o out.pdf

BASE คือโฮสต์ on-prem ไม่ใช่ localhost เว้นแต่คุณอยู่บนเครื่องเซิร์ฟเวอร์เอง

/docs/agent.th.md — this page renders that file. It does not keep a second copy, so the two can never disagree.