/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:read200 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:
image— Data 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-…"] } // สูงสุด 10id เหล่านี้คือ 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 ไม่ใช่ที่ข้อความ#
| HTTP | code | คุณควรทำอะไร |
|---|---|---|
| 400 | invalid_data | data ผิดสัญญา คำตอบมี fields: [{field, reason}] มาให้ แก้แล้วส่งใหม่ — ห้ามส่งซ้ำแบบเดิม |
| 400 | bad_request | body ผิดรูป, mode ผิด, protection ไม่มีรหัสผ่าน, watermark.text ยาวเกิน, เรียก async บนฉบับร่าง |
| 401 | unauthenticated | ไม่มีหรือ X-Api-Key ผิด |
| 403 | insufficient_scope | คีย์ไม่มี scope ที่การเรียกนี้ต้องใช้ |
| 404 | not_found | ไม่พบ หรือไม่ใช่ของทีมคุณ — แยกไม่ออกโดยเจตนา |
| 410 | gone | เอกสารหมดอายุและถูกลบแล้ว |
| 413 | payload_too_large | body เกิน 1 MB |
| 422 | render_failed | เทมเพลต เรนเดอร์ไม่ผ่าน (รวมถึงกรณีเรนเดอร์นานเกินกำหนดแล้วถูกตัดจบ) ไม่ใช่ความผิดของข้อมูลคุณ อย่าลองใหม่ ให้แจ้งเจ้าของเทมเพลต |
| 429 | rate_limited | เคารพ Retry-After หน่วงเวลาก่อนเรียกซ้ำ อย่าเรียกถี่ต่อเนื่อง (เพดานค่าเริ่มต้น 120 คำขอ/นาที ต่อคีย์) |
| 501 | not_implemented | คุณขอ option ที่ยังไม่มี |
| 503 | protection_unavailable | เซิร์ฟเวอร์ไม่มี qpdf — ภาวะถาวร ห้าม fallback ไปใช้ไฟล์ที่ไม่ได้ล็อก และห้ามลองใหม่ |
| 503 | overloaded | เซิร์ฟเวอร์กำลังเรนเดอร์เต็มกำลัง — ภาวะชั่วคราว เคารพ 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. กฎที่ห้ามละเมิด#
- 404 แปลว่า "ไม่ใช่ของคุณ หรือ ไม่มีอยู่" และคุณแยกไม่ออก นี่คือโมเดลการแยกข้อมูล ไม่ใช่ช่องโหว่ของ API ห้ามสุ่มส่ง id เพื่อหาความแตกต่าง ทุกครั้งที่ถูกปฏิเสธจะถูกบันทึกใน audit log ด้วย
outcome: denied - ห้าม log
dataมันมี PII — ชื่อ เลขบัตรประชาชน จำนวนเงิน ระบบไม่ log มัน และคุณก็ห้าม - ห้ามส่งรหัสผ่านไปที่อื่นนอกจาก
options.protectionไม่ใส่ในmetadataไม่ใส่ใน log ไม่ใส่ในคิว retry ที่คุณเก็บลงดิสก์ - ห้ามลองใหม่กับ
422 render_failedและ400ทั้งคู่เป็น deterministic ลองใหม่ก็ได้ผลเดิม และทำให้ audit log สกปรก - API key คืออัตลักษณ์ของเครื่อง ที่ถือสิทธิ์ของ "ทีม" ไม่ใช่ของคน มันไม่หยุดทำงานเมื่อพนักงานลาออก การหยุด integration คือ revoke คีย์ ไม่ใช่จัดการที่ตัวคน
- คีย์เรนเดอร์ฉบับร่างไม่ได้ ฉบับร่างยังไม่ผ่าน 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.pdfBASE คือโฮสต์ 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.