← บล็อก

เปิด demo ให้คนแปลกหน้าลองเล่น โดยไม่ทำให้ใครเดือดร้อน

Backendอ่าน 21 นาที

Petcare Assistant เป็นผู้ช่วย AI และ CRM บน LINE สำหรับร้านสัตว์เลี้ยง ผมทำคนเดียว และอยากให้คนที่สนใจลองใช้ได้จริง ไม่ใช่ดูแค่ภาพหน้าจอ ตอนนี้ demo มีสองส่วน คือ LINE Official Account ทดสอบที่ใครก็แอดเป็นเพื่อนแล้วคุยกับบอทได้ และ dashboard ที่ https://petcare.rattanun.dev ซึ่งเข้าได้ด้วยปุ่มเดียวผ่านบัญชีเดโมแบบดูอย่างเดียว คนที่ลองคุยใน LINE แล้วเปิด dashboard จะเห็นแชทของตัวเองเข้ามาใน inbox พร้อมการจองและตัวเลขสรุป

ระบบนี้เก็บแชทลูกค้า ชื่อ LINE เบอร์โทร และข้อมูลสัตว์เลี้ยง การเปิดให้คนแปลกหน้าเข้ามาเล่นจึงไม่เหมือนเปิดเว็บ landing page บทความนี้เล่าว่ามีอะไรผิดพลาดได้บ้าง ผมทำอะไรไปแล้ว และอะไรที่ยังค้างอยู่

เริ่มจาก audit ก่อนขึ้น production

ก่อน deploy ครั้งแรก ผมทำ audit แบบอ่านอย่างเดียวสามรอบ คือพื้นผิวภายนอกกับ config ของ production, secret ข้อมูลส่วนบุคคลและการแยก tenant, และ dependency กับการใช้ต้นทุนหรือโควตาในทางที่ผิด สมมติฐานตอนนั้นคือ LINE OA ที่ใครก็แอดได้ เครื่องเดียว งบ AI ราว 20 ดอลลาร์ต่อเดือน และ LINE OA แพ็กเกจฟรี ผลรวมอยู่ใน docs/audit-2026-10-04.md โดยแบ่งตามว่าต้องแก้เมื่อไร ส่วน A คือ 13 ข้อที่ต้องแก้ก่อนเปิด demo

ส่วนที่ audit ยืนยันว่าแข็งแรงอยู่แล้วก็มี เช่น ไม่มี secret หลุดใน git history, webhook ของ LINE ตรวจ HMAC แบบ constant-time, ทุกตาราง tenant มี row-level security และ model ไม่เคยเป็นคนส่ง id มาเอง แต่ส่วน A มีเรื่องที่ทำให้ผมไม่สบายใจหลายข้อ

ภัยของ demo สาธารณะและสิ่งที่ทำ: ทำแล้วหกข้อ ข้อมูลปลอมทำแล้วบางส่วน

รหัสผ่าน seed ที่ทุกคนรู้ ข้อ A1 คือข้อที่หนักที่สุด รหัสผ่านของ seed สำหรับ development ถูกเขียนไว้ในเอกสารของ repo และใช้ร่วมกันทุกบัญชีที่ seed สร้าง รวมถึง platform admin ด้วย และ seed ไม่มีอะไรกันไม่ให้รันบน production ถ้ามีคนล็อกอินเป็นเจ้าของร้านบน demo ได้ เขาจะอ่านและ export แชทของผู้เยี่ยมชมจริงทุกคน ส่งข้อความหาผู้ติดตามทุกคนจาก OA เปลี่ยน LINE credentials หรือเผางบ AI ใน simulator ได้ ข้อ A2 ซ้ำเติมอีกชั้น คือ endpoint self-service ของ Better Auth ยังเปิดอยู่ คนที่รู้รหัสผ่านจึงเปลี่ยนรหัสจนล็อก demo หรือดู session ของคนอื่นได้

คนเดียวเผางบหรือโควตาทั้งเดือน หลายข้อเป็นเรื่องเดียวกันในมุมต่างกัน พิมพ์คำฉุกเฉินซ้ำ ๆ แล้วทุกข้อความจะ push แจ้งกลุ่มแอดมิน (A5) การสกัดความจำส่งข้อความยาวเข้า model โดยไม่ตัด (A6) ต้นทุนต่อเทิร์นไม่มีเพดาน (A7) การจองผ่าน LIFF วนซ้ำได้และแต่ละครั้ง push (A8) push quota มองไม่เห็นเพราะนับแค่ follow-up (A9) และถ้างบหมด demo จะเงียบทั้งเดือนโดยมีแจ้งเตือนแค่ตอน 80% (A10)

ข้อมูลลูกค้าใน log (A11) error ของ drizzle พก parameter ทั้งหมดของ query มาด้วย ซึ่งก็คือข้อความแชท ชื่อ เบอร์ และ LINE id แล้ว worker ก็ log มันทั้งก้อน error ตอน parse ผลของ model ก็มีข้อเท็จจริงของลูกค้าติดมาด้วย

backup ติดไปกับ image (A3) backup.sh เขียน dump ลง ./backups ใน repo แต่ .dockerignore ไม่ได้กันไว้ และ Dockerfile ใช้ COPY . . ฐานข้อมูลจึงอาจถูกอบลงไปใน image

ทั้ง 13 ข้อแก้ใน milestone M13c (docs/specs/M13c.md) ตัวอย่างเช่น ภายใต้ NODE_ENV=production seed จะบังคับให้มี SEED_PASSWORD จาก environment (อย่างน้อย 16 ตัวอักษรและห้ามเป็นรหัสของ development) ไม่สร้าง platform admin และไม่พิมพ์รหัสผ่านออกมา Better Auth ปิด route self-service ด้วย disabledPaths และมีเทสต์ว่าแต่ละ route ตอบ 404 .dockerignore กัน backups, **/*.dump และ **/.env* ส่วน config ของ production ปฏิเสธ secret ที่ยังเป็นค่าตัวอย่าง โดยบอกชื่อตัวแปรแต่ไม่บอกค่า ส่วนเรื่อง log มีกฎว่าห้าม log หรือเก็บ message ของ error เด็ดขาด ให้ใช้ safeError แทน

บัญชีเดโมที่อ่านได้อย่างเดียว

audit ทิ้งคำถามไว้ข้อหนึ่ง คือจะให้ผู้เยี่ยมชมเห็น dashboard ได้อย่างไร ในเมื่อล็อกอินเจ้าของร้านจะเปิดเป็นสาธารณะไม่ได้ ตัวเลือกมีตั้งแต่ไม่มีล็อกอินสาธารณะเลย ไปจนถึง role อ่านอย่างเดียว ตอนแรกผมกับเจ้าของโปรเจกต์คิดจะทำ sandbox แยกต่อผู้เยี่ยมชม แต่สุดท้าย M13d (docs/specs/M13d.md) เลือกทางที่ง่ายกว่าและเล่าเรื่องได้ดีกว่า คือบัญชีเดโมสาธารณะบัญชีเดียวที่มี role demo_viewer ดูร้านที่ผูกกับ LINE OA ทดสอบ

role นี้ได้แค่ inbox.view, customer.view, booking.view, catalog.view, knowledge.view และ analytics.view ทั้งองค์กร และไม่มีอย่างอื่นเลย ไม่มี customer.view_phone เบอร์จึงถูก mask ด้วยกฎที่มีอยู่แล้ว ไม่มี audit.view เพราะค่า before/after ใน audit เก็บข้อมูลดิบ และไม่มีสิทธิ์เขียนอะไรทั้งนั้น demo_viewer ไม่อยู่ใน ASSIGNABLE_ROLES ทำให้เจ้าของร้านเชิญหรือตั้งใครเป็น role นี้ไม่ได้ seed เท่านั้นที่สร้างได้

หน้า login มีกล่อง “ลองใช้ระบบ” กับปุ่ม “เข้าสู่ระบบด้วยบัญชีเดโม” ที่ล็อกอินฝั่ง server ด้วย credentials ที่ตั้งไว้ใน DEMO_LOGIN_EMAIL และ DEMO_LOGIN_PASSWORD ทั้งสองค่านี้ตั้งใจให้เป็นสาธารณะ จึงมีข้อห้ามว่าต้องไม่ซ้ำกับรหัสผ่านที่ใช้ที่อื่น และบน production ห้ามเท่ากับ SEED_PASSWORD เมื่อมีคนใช้ในทางที่ผิด สคริปต์ pnpm demo:password จะเปลี่ยนรหัสและตัด session ทุกอันของบัญชีนี้ ปุ่มนี้เรียก auth.api.signInEmail จาก server action ซึ่งข้าม rate limit ของ router ใน Better Auth ไป ผมจึงใส่ limit เดิมกลับเข้าไปเอง คือ 3 ครั้งต่อ 10 วินาทีต่อ client

สิ่งที่ผมไม่อยากเชื่อแค่จากการอ่านโค้ดคือ “ไม่มีหน้าไหนที่ role นี้เข้าถึงแล้วเขียนข้อมูล” การเขียนผ่าน authorize อยู่แล้วก็จริง แต่เส้นทางที่ควรจะแค่อ่านก็อาจแอบเขียนได้ เช่น mark as read, การสร้างข้อมูลแบบ lazy หรือตัวนับ M13d จึงมี sweep test ที่ล็อกอินเป็น demo_viewer เรียก service ที่ทุกหน้าใช้ 60 ครั้งสำหรับการอ่านและ 22 ครั้งสำหรับการเขียน แล้วเทียบจำนวนแถวและ md5 ของแถวในทุกตาราง ผลคือไม่มีเส้นทางอ่านไหนเขียนข้อมูล แต่ระหว่างทางก็เจอประตูที่ role นี้เดินผ่านได้ เช่น createFact ที่ตรวจแค่ customer.view และ previewImport ที่ validate ไฟล์ก่อน authorize ทั้งสองถูกปิดไปแล้ว

หลักที่ผมยึดตลอดคือ “ตรวจ permission ไม่ใช่ชื่อ role” ในโค้ดไม่มีที่ไหนเขียนว่า if role === "demo_viewer" เพื่อซ่อนปุ่ม อีกกฎของ repo คือ can() ในหน้าเว็บมีหน้าที่แค่ตัดสินว่าจะวาดปุ่มหรือไม่ การตัดสินว่าทำได้หรือไม่เกิดที่ service เสมอ ใน M13d แต่ละส่วนของ dashboard จึงมี helper access.ts ที่จับคู่ทุกปุ่มกับ permission ที่ service ของมันตรวจ และมีเทสต์รายละ role ก่อนหน้านี้บางหน้ายังวาดปุ่มที่กดแล้ว error ตอน submit สำหรับ demo_viewer เราไม่ยอมให้มีแม้แต่ปุ่มเดียว

ชื่อลูกค้าต้องถูกซ่อนด้วย

ตอนออกแบบ M13d เราตัดสินใจโชว์ชื่อ LINE ให้ผู้เยี่ยมชมเห็น เพื่อให้เขาหาแชทของตัวเองเจอ ส่วนข้อความต้อนรับของ OA บอกไว้ว่าสิ่งที่พิมพ์จะแสดงให้ผู้ชมเดโมเห็น วันที่ 2 พฤศจิกายน 2026 เจ้าของโปรเจกต์เปลี่ยนใจ เหตุผลคือคนจริงคุยกับ OA นี้ และชื่อ LINE ก็มักเป็นชื่อจริง ชื่อนั้นจึงถูกโชว์ให้ผู้เยี่ยมชมทุกคนเห็นก่อนที่เจ้าของชื่อจะได้เลือกว่าจะไม่แชร์

ทางแก้คือ permission ใหม่ customer.view_name ทุก role ที่มี customer.view ได้สิทธิ์นี้ ยกเว้น demo_viewer ส่วนบอทได้เฉพาะลูกค้าที่ยืนยันตัวแล้วของแชทตัวเอง ถ้าไม่มีสิทธิ์นี้ service จะคืน maskName() คือตัวแรกตามด้วย ••• เช่น ลูกค้าตัวอย่าง แพรวา ใจดี กลายเป็น แ•••

พนักงานกับผู้ชม demo เห็นต่างกัน: ชื่อถูกย่อเหลืออักษรตัวแรกและ •••

มีรายละเอียดที่ดูเล็กแต่สำคัญอยู่สามข้อ

  • “ตัวแรก” หมายถึง grapheme ไม่ใช่ code unit จึงใช้ Intl.Segmenter พยัญชนะไทยยังได้สระหรือวรรณยุกต์ติดไปด้วย และ emoji ไม่ถูกตัดครึ่ง mask ยาวสามจุดเสมอ ความยาวของชื่อจึงไม่หลุดไปด้วย
  • ชื่อไม่ได้อยู่แค่ช่องชื่อ บอททักลูกค้าด้วยชื่อ ชื่อจึงอยู่ในข้อความแชท ร่างคำตอบ สรุป AI และโน้ตด้วย maskNamesInText หาทุกตำแหน่งของทุกชื่อก่อน แล้วรวมช่วงที่ซ้อนกันเป็นช่วงเดียว จากนั้นค่อย mask ทีละช่วง ที่ต้องทำแบบนี้เพราะชื่อของลูกค้าคนเดียวซ้อนกันได้ เช่น ชื่อเล่นกับชื่อจริงที่ขึ้นต้นเหมือนกัน regex แบบ alternation จะเลือก match ซ้ายสุด ส่วนการแทนทีละชื่อจากยาวไปสั้นก็เหลือเศษของชื่ออีกชื่อทิ้งไว้ ทั้งสองวิธีจึงทิ้งชิ้นส่วนของชื่อจริงไว้ เรื่องนี้บันทึกไว้ใน docs/decisions.md แล้ว คนที่มาทีหลังจะได้ไม่ “ทำให้ง่ายลง” จนรูกลับมา
  • ทำใน services ไม่ใช่ใน web เหมือนเบอร์โทร ถ้า mask ที่หน้าเว็บ ข้อมูลเต็มก็ยังเดินทางมาถึง browser อยู่ดี และ client ตัวถัดไปอย่าง LIFF หรือ mobile ก็ต้องจำไปทำเองอีกรอบ

ผลข้างเคียงคือการค้นหาด้วยชื่อในรายการลูกค้าจะ match เฉพาะชื่อสัตว์และเบอร์ ผู้เยี่ยมชมจึงต้องหาแชทของตัวเองจากตำแหน่ง ซึ่งแชทล่าสุดอยู่บนสุดของ inbox

เพดาน spam ต้นทุน และโควตา

บอทใช้ model จริงและ push ผ่าน LINE จริง ทุกข้อความจึงมีต้นทุน เพดานชุดแรกตั้งไว้ใน M13a (docs/decisions.md, “Spam and abuse limits”) และใช้ตัวเลขเดียวกันทั้งร้าน demo และร้านจริง

  • ข้อความต่อลูกค้า 10 ข้อความต่อนาที
  • คำตอบของบอทต่อลูกค้า 30 ครั้งต่อวัน นับตามวันของร้าน
  • ข้อความเดียวยาวได้ไม่เกิน 1,000 ตัวอักษร เกินแล้วไม่ตัด แต่ส่งต่อให้พนักงาน
  • LIFF API 60 request ต่อนาทีต่อ IP

เมื่อเกินเพดาน บอทจะหยุดแล้วส่งแชทให้พนักงานผ่านเส้นทางเดียวกับ request_human และบอกลูกค้าครั้งเดียวว่าผู้ช่วย AI ขอส่งต่อ ข้อยกเว้นคือเหตุฉุกเฉินจะไม่ถูกเพดานกั้น การตรวจคำฉุกเฉินจะรันก่อนเสมอ

M13c เพิ่มเพดานด้านต้นทุนตามที่ audit ชี้ (docs/decisions.md, “Cost and quota caps before the demo is public”) ได้แก่ ค่า AI ต่อลูกค้าต่อวันไม่เกิน 0.25 ดอลลาร์ ถ้าเกินจะส่งต่อด้วยเหตุผล limit_spend ประวัติที่ส่งเข้า model ไม่เกิน 1,000 ตัวอักษรต่อข้อความและ 8,000 ตัวอักษรรวม output token มีเพดาน การสกัดความจำทำได้ 3 รอบต่อลูกค้าต่อวัน การจองที่ยังไม่ถึงวันได้ 3 รายการต่อลูกค้า แจ้งเตือนฉุกเฉินเข้ากลุ่มแอดมินได้ 1 ครั้งต่อลูกค้าต่อ 15 นาที โดยลูกค้ายังได้คำตอบทุกครั้ง และ follow-up หยุดเมื่อใช้ push quota ของเดือนไปแล้ว 80%

push quota นับในจุดเดียวคือ deliver ในทรานแซกชันเดียวกับที่ mark ว่าส่งแล้ว เพราะถ้านับหลายที่ก็จะนับซ้ำ ซึ่งเคยเกิดจริงกับ follow-up ที่ retry ค่าที่ provider ไม่รายงานต้นทุนมาจะถูกคิดด้วยราคาสำรองที่ตั้งใจให้แพงไว้ก่อน แทนที่จะนับเป็นศูนย์อย่างเดิม

ชั้นสุดท้ายอยู่นอกโค้ด คือ hard credit limit บน API key ของ OpenRouter ที่เจ้าของตั้งเอง ถ้าโค้ดผิด ชั้นนี้คือที่กั้นสุดท้าย และงบของร้านตั้งไว้ราว 70% ของ limit นั้น key ที่ใช้บน production ผมตั้ง limit ไว้ 5 ดอลลาร์ ส่วน push quota ของแพ็กเกจ LINE ที่ระบบตั้งไว้ 300 ต่อเดือนยังต้องยืนยันกับแพ็กเกจจริงของ OA

ล้างแชทของ demo อย่างปลอดภัย

ระหว่างทดสอบ ฐานข้อมูล production สะสมแชทจากการทดลองไว้ ก่อนเปิดให้คนทั่วไปเข้ามา ผมอยากให้ inbox สะอาด และให้ทุกรอบทดสอบเริ่มจากจุดเดียวกัน วันที่ 2 พฤศจิกายน 2026 จึงล้างแชททั้งหมดตามลำดับนี้

  1. backup ไป Cloudflare R2 ก่อน ระบบมี backup ทุกคืนอยู่แล้ว และ bucket lock 30 วันก็ผ่านการทดสอบด้วยการลองลบจริงแล้ว แต่ก่อนลบข้อมูลด้วยมือ ผมทำ dump ใหม่อีกครั้งเสมอ
  2. ลบเฉพาะข้อมูลแชท ได้แก่ conversation, ข้อความ, inbound event และ contact ที่ไม่ได้มาจาก seed โดยไม่แตะข้อมูล seed ของร้าน ทั้งสาขา บริการ knowledge base และบัญชีพนักงาน
  3. นับหลังลบว่าตรงกับที่คาด รอบนั้นลบไป 8 conversation, 56 ข้อความ, 20 inbound event และ 8 contact ที่ไม่ใช่ seed ส่วน seed ยังอยู่ครบ

สคริปต์ที่ใช้ไม่ได้อยู่ใน repo เพราะเป็นงานเฉพาะเครื่อง production แต่ขั้นตอนทั้งหมดบันทึกไว้ใน STATUS หลังล้างแล้ว การทดสอบรอบถัดไปจะใช้ docs/demo-chat-script.md ซึ่งเป็นชุดข้อความตายตัวที่ส่งหลังล้างฐานทุกครั้ง ผลแต่ละรอบจึงเทียบกันได้

ข้อมูลปลอมต้องดูออกว่าปลอม

ข้อมูล seed ของร้าน Happy Paws เป็นของสมมติ แต่บางอย่างดูเหมือนจริงเกินไปจนคนอาจหลงทำตาม

  • PromptPay ข้อความมัดจำการจองส่งเลข PromptPay ของร้านไปให้ลูกค้า ถ้าเลขนั้นบังเอิญเป็นของคนจริง ผู้เยี่ยมชมอาจโอนเงินไปให้คนแปลกหน้าได้ บน production ผมจึงแก้ด้วย SQL ให้เป็น xxx-xxx-xxxx (ตัวอย่าง อย่าโอนเงินจริง)
  • เบอร์ฉุกเฉิน เบอร์ร้าน (02-000-0199) และโรงพยาบาลสัตว์สองแห่งเป็นเบอร์ที่แต่งขึ้น ปุ่มโทรใต้คำตอบฉุกเฉินจึงโทรไม่ติด ถ้าสัตว์ของใครป่วยจริงแล้วกดปุ่มนี้ เขาจะเสียเวลาไปเปล่า ๆ
  • ลิงก์แผนที่ ลิงก์ของแต่ละสาขาค้น Google Maps ด้วยคำว่า “Happy Paws” ซึ่งอาจพาไปเจอร้านจริงที่ชื่อเดียวกัน

ข้อแรกทำเสร็จแล้ว ส่วนสองข้อหลังเจ้าของโปรเจกต์พักไว้ SQL พร้อมแล้วแต่ยังไม่ได้รัน แผนคือติดป้าย “ตัวอย่าง” ที่เบอร์ ให้คำตอบฉุกเฉินแนะนำให้โทรหาโรงพยาบาลสัตว์ที่ใกล้ที่สุด และล้าง branches.map_url ทิ้ง มีข้อควรระวังด้วยว่าการแก้พวกนี้ทำบน production อย่างเดียว ถ้า seed production ใหม่ก็ต้องแก้ซ้ำ

basic auth ระหว่างทาง

ตลอดช่วงที่ยังปิดช่องโหว่ไม่ครบ เว็บอยู่หลัง basic auth ของ Caddy คนที่ไม่รู้รหัสจะเข้าไม่ได้เลย ตอนเชื่อม LINE จริงเจอปัญหาว่าหน้า LIFF ขาวโพลนในแอป LINE เพราะ browser ของ LINE ไม่แสดงกล่องใส่รหัส จึงต้องเอา /liff/* และ /_next/* ออกจาก basic auth ก่อน ส่วน /healthz สำหรับ uptime monitor ก็ถูกแยกไว้นอกรหัสเช่นกัน

basic auth ถูกถอดออกเป็นขั้นสุดท้ายของเช็กลิสต์ “Before the demo is opened to visitors” หลังทดสอบบอทด้วย model จริงบน server, เชื่อม LINE, ทดสอบบนมือถือ, ซ้อม restore จาก R2 และตั้ง uptime monitor แล้ว ผมตรวจจากภายนอกว่า /login ตอบ 200 โดยไม่มี WWW-Authenticate และ webhook ที่ไม่มีลายเซ็นถูกปฏิเสธด้วย 401 ตั้งแต่วันที่ 2 พฤศจิกายน 2026 demo ก็เปิดเป็นสาธารณะ

สิ่งที่ยังไม่เรียบร้อย

เส้นทางสู่ demo สาธารณะ ตั้งแต่ audit 4 ต.ค. ถึงเปิดสาธารณะ 2 พ.ย.

  • ความยินยอมก่อนเริ่มแชท ตอนนี้มีแค่ข้อความต้อนรับของ OA ที่บอกว่าสิ่งที่พิมพ์จะถูกแสดงให้ผู้ชมเดโมเห็น ไอเดียใน docs/ROADMAP.md คือให้ผู้ใช้ LINE กด “ยอมรับและเริ่มใช้งาน” ก่อน ระหว่างที่ยังไม่กด ระบบจะไม่เก็บข้อความ ไม่เรียก model และไม่สร้างรายการใน inbox ทำเป็นการตั้งค่าระดับร้านที่เปิดเฉพาะร้าน demo ข้อจำกัดคือ LINE ยังส่งทุกข้อความมาที่ webhook อยู่ดี เราทำได้แค่ทิ้งมัน และต้องเก็บ LINE user id ไว้เพื่อจำว่ายินยอมแล้ว นอกจากนี้หน้า LIFF ก็ต้องมีด่านเดียวกัน ไม่อย่างนั้น rich menu จะอ้อมผ่านไปได้ ประเมินไว้ราวครึ่งวัน แต่ยังรอเจ้าของตัดสินใจสองข้อ
  • เบอร์ฉุกเฉินและลิงก์แผนที่ ยังเป็นของปลอมที่ดูเหมือนจริง ตามที่เล่าไว้ข้างบน
  • การ monitor uptime monitor ทุก 5 นาทียิงไปที่ /healthz ซึ่งถาม Postgres กับ Redis แล้ว แต่ watchdog ใน worker ยังตั้งเป็น OPS_ALERTS=log คือเขียนลง log อย่างเดียว ปลายตุลาคม embedding key หมดอายุ การค้นหา knowledge บน production พังไปราวสี่วันโดยไม่มีอะไรเตือน ยังต้องเพิ่ม check สำหรับ embedding ที่ล้มซ้ำ ๆ
  • ร้าน demo ไม่มีพนักงาน เมื่อแชทชนเพดานแล้วถูกส่งต่อให้พนักงาน ก็จะไม่มีใครตอบ ตอนนี้ยอมรับไว้ก่อน แต่ต้องกลับมาดูก่อนโปรโมต demo จริงจัง
  • push quota ของ LINE ยังต้องยืนยันกับแพ็กเกจจริงของ OA และเมื่อใช้ Haiku ผ่าน key ของ Anthropic แบบ BYOK ระบบนับค่าใช้จ่ายจะเห็นเป็น 0 จึงต้องพึ่งวงเงินที่ตั้งใน Anthropic Console แทน

บทเรียนที่ผมได้คือ demo ไม่ใช่ของเล่นที่ปลอดภัยโดยธรรมชาติ ถ้าคนจริงคุยกับมัน มันก็เก็บข้อมูลของคนจริง และถ้ามันเรียก model จริง มันก็ใช้เงินจริง การ audit ก่อนเปิด เขียนเพดานเป็นตัวเลขที่ตรวจได้ และยอมรับตรง ๆ ว่ายังเหลืออะไร ทำให้ผมกล้ากดเปิด demo ได้โดยรู้ว่ากำลังรับความเสี่ยงอะไรอยู่

ที่มา

  • docs/audit-2026-10-04.md — ทั้งไฟล์ ส่วน A, “Decision needed”
  • docs/specs/M13c.md — “Decisions (2026-10-04)”
  • docs/specs/M13d.md — “How the demo is shown”, “Contracts”, “Decisions” ข้อ 1–7, “Landed — accepted deviations”
  • docs/decisions.md — “Spam and abuse limits (2026-10-03, owner)”, “Cost and quota caps before the demo is public (2026-10-04)”, “Names in chat text are masked as merged spans (2026-11-02)”, “Caddy mounts the whole deploy/ folder, and /healthz skips the password (2026-10-31)”
  • docs/STATUS.md — “Before the demo is opened to visitors”, “Demo data a visitor could act on (2026-11-02)”, รายการวันที่ 2026-10-27 และ 2026-10-30
  • docs/ROADMAP.md — “Consent before the demo chat”
  • docs/deploy.md — “The public demo login”
  • docs/demo-chat-script.md
  • docs/portfolio-notes.md — “The pre-deploy audit”, “LINE on production (2026-10-28)”
  • packages/services/src/customers/names.ts — คอมเมนต์หัวไฟล์, maskName, maskNamesInText
  • packages/permissions/src/grants.ts — grants ของ demo_viewer และ customer.view_name