ปลั๊กอิน Scheduling
นี่คือปลั๊กอินแบบ remote service มันตั้งเวลาให้ version ที่ถูก pin หนึ่งตัวถูกเผยแพร่ หรือให้ version ที่เผยแพร่แล้วและถูก pin หนึ่งตัวถูกยกเลิกการเผยแพร่ และดำเนินการเปลี่ยนแปลงนั้นในภายหลังผ่าน plugin protocol v1 ของ Dee Wan มันมี Cloudflare Worker ของตัวเอง, ฐานข้อมูล D1 ของตัวเอง, cron trigger ของตัวเอง และ UI สำหรับจัดการของตัวเอง ไม่มีโค้ดส่วนใดของมันรันอยู่ภายใน Dee Wan
ชื่อ repository, remote และ id ของ manifest ยังเป็นเรื่องที่เจ้าของโปรเจกต์ต้องตัดสินใจ com.example.scheduling เป็นเพียงค่า placeholder ห้ามติดตั้งปลั๊กอินใน production ด้วยค่านี้
เอกสาร: Scheduling ตั้งแต่ต้นจนจบ รันปลั๊กอินกับ Dee Wan ในเครื่อง ฝั่ง core ดู การเขียนปลั๊กอิน, Protocol v1, ความปลอดภัยของปลั๊กอิน และ การทดสอบปลั๊กอิน
ความเข้ากันได้
หัวข้อที่มีชื่อว่า “ความเข้ากันได้”| ปลั๊กอิน | Manifest | Dee Wan protocol |
|---|---|---|
| 0.1.0 | v1 | v1 |
| Route | ใคร | ทำอะไร |
|---|---|---|
GET /dee-wan/manifest.json |
admin ของ Dee Wan | Manifest v1 โดยใช้ origin จาก PUBLIC_ORIGIN |
POST /admin/activation-codes |
ผู้ดูแลระบบ (operator) พร้อม Bearer ADMIN_SECRET |
ออกรหัสใช้ครั้งเดียว มีอายุ 15 นาที body {} ออกรหัส activate; {"installation_id":"..."} ออกรหัส rotate สำหรับ installation (การติดตั้งหนึ่งชุด) นั้นเท่านั้น |
POST /dee-wan/activate |
core ของ Dee Wan | activate สร้าง installation และปฏิเสธ installation ที่มีอยู่แล้ว rotate แทนที่ token แต่ละรหัสใช้ได้ครั้งเดียว |
GET /installations/:id?dee_wan_launch=dwl_... |
เบราว์เซอร์ จาก Dee Wan | แลกรหัส ตั้ง session และ redirect ด้วย 303 ไปยังหน้าเดิมโดยไม่มีรหัส |
GET /installations/:id |
Session | แสดงเนื้อหา ฟอร์มตั้งเวลา และรายการที่ตั้งเวลาไว้ |
POST /installations/:id/schedules |
Session และ CSRF token | สร้างรายการตั้งเวลา |
POST /installations/:id/schedules/:scheduleId/cancel |
Session และ CSRF token | ยกเลิกรายการตั้งเวลาที่อยู่ในสถานะ pending หรือ retrying |
POST /admin/installations/:id/delete-data |
ผู้ดูแลระบบ | ลบข้อมูลปลั๊กอินทั้งหมดของ installation นั้น |
scheduled() รัน runner หนึ่ง tick
การติดตั้ง
หัวข้อที่มีชื่อว่า “การติดตั้ง”- Deploy Worker (ดูด้านล่าง) และตั้งค่า
PUBLIC_ORIGIN - สร้างรหัส activation:
Terminal window curl -X POST -H "Authorization: Bearer $ADMIN_SECRET" https://scheduling.example/admin/activation-codes - ใน Dee Wan ไปที่ Settings → Plugins แล้วติดตั้งจาก
https://scheduling.example/dee-wan/manifest.jsonด้วยรหัสนั้น มอบ capability และโมเดลให้ - หากต้องการ rotate token ให้สร้างรหัสด้วย
{"installation_id":"<id>"}แล้วส่งให้ action rotate ใน Dee Wan
Wrangler
หัวข้อที่มีชื่อว่า “Wrangler”คัดลอก wrangler.example.jsonc ไปเป็น wrangler.jsonc แล้วใส่ id ของฐานข้อมูล D1
- Binding:
PLUGIN_DB(D1) - Cron:
* * * * * - ตัวแปร:
PUBLIC_ORIGIN,ENVIRONMENT,DEE_WAN_DEV_ORIGINS,MAX_ATTEMPTS(5),MAX_LATENESS_MS(900000),LEASE_MS(60000),MAX_RETRY_AFTER_MS(300000) - Secret:
ADMIN_SECRET(อย่างน้อย 16 ตัวอักษร),TOKEN_KEY(base64 ของ 32 ไบต์แบบสุ่ม;openssl rand -base64 32)
npx wrangler d1 create dee-wan-schedulingnpx wrangler d1 migrations apply dee-wan-scheduling --remote # --local for devnpx wrangler secret put ADMIN_SECRETnpx wrangler secret put TOKEN_KEYnpx wrangler deployการทำ TOKEN_KEY หายจะทำให้ token ของ installation ที่เก็บไว้อ่านไม่ได้ รายการตั้งเวลาจะถูกปฏิเสธด้วย token_unreadable และต้อง rotate ทุก installation
การพัฒนาในเครื่องกับ Dee Wan ในเครื่อง
หัวข้อที่มีชื่อว่า “การพัฒนาในเครื่องกับ Dee Wan ในเครื่อง”cookie ของ UI สำหรับจัดการใช้ prefix __Host- และ attribute Secure ดังนั้นเบราว์เซอร์ต้องใช้ HTTPS หรือ localhost
.dev.varsของปลั๊กอิน:ENVIRONMENT=development,PUBLIC_ORIGIN=http://localhost:8788,DEE_WAN_DEV_ORIGINS=http://localhost:8787,ADMIN_SECRET=...,TOKEN_KEY=...- core ของ Dee Wan:
ENVIRONMENT=development,PLUGIN_DEV_ORIGINS=http://localhost:8788,BACKEND_URL=http://localhost:8787 - รัน
npx wrangler d1 migrations apply dee-wan-scheduling --localแล้วรันnpx wrangler dev --port 8788 --test-scheduled - หากต้องการกระตุ้น tick ให้รัน
curl "http://localhost:8788/__scheduled?cron=*+*+*+*+*"
base URL ของ Dee Wan ที่เป็น HTTP จะถูกยอมรับก็ต่อเมื่อ ENVIRONMENT=development และ origin ของมันอยู่ใน DEE_WAN_DEV_ORIGINS แบบตรงตัวเท่านั้น มิฉะนั้น base URL ต้องใช้ HTTPS และต้องไม่มี credential, ไม่มี query และไม่มี fragment
ความหมายของการตั้งเวลา
หัวข้อที่มีชื่อว่า “ความหมายของการตั้งเวลา”- รายการตั้งเวลาสร้างได้จากการ launch เนื้อหาเท่านั้น (action
Schedule) ปลั๊กอินอ่านเนื้อหาด้วย delegation และ(kind, to)ที่เลือกต้องอยู่ในworkflow.transitionsที่ได้รับ delegate - การ pin: การเผยแพร่ pin
current_version_idการยกเลิกการเผยแพร่ pinpublished_version_idและทั้งสองแบบยัง pinworkflow.revisionด้วย รายการตั้งเวลาถูกเก็บโดยผูกกับ instance หลัก; core เผยแพร่ทั้ง translation group - มีรายการตั้งเวลาที่ยัง live (
pending|retrying|running) ได้ไม่เกินหนึ่งรายการต่อ installation, รายการเนื้อหา และ kind รายการตั้งเวลาใหม่จะแทนที่ (supersede) รายการ live ที่อยู่ในสถานะ pending หรือ retrying ภายใน batch เดียว หากรายการ live กำลัง running การสร้างจะล้มเหลวด้วยexecution_in_progress - การตั้งเวลาใหม่ (reschedule) จะสร้างแถวใหม่พร้อม revision ใหม่และ idempotency key ใหม่
- idempotency key คือ
sched:<installation_id>:<schedule_id>:r<revision>ค่านี้คงเดิมในทุกการ retry - ยกเลิกได้เฉพาะรายการตั้งเวลาที่อยู่ในสถานะ pending หรือ retrying การยกเลิกรายการที่กำลัง running จะคืน
execution_in_progressรายการที่เสร็จแล้วยกเลิกไม่ได้ การยกเลิกไม่เคยเรียก Dee Wan และไม่เคยย้อนกลับ transition - ในเวลาที่ดำเนินการ core จะตรวจอำนาจของบุคคลนั้น, การ pin และ revision อีกครั้ง
- การ launch แบบ manage แสดงรายการและยกเลิกรายการตั้งเวลาทั้งหมดของ installation ได้ แต่สร้างรายการตั้งเวลาไม่ได้
สถานะ: pending, retrying, running, completed, refused, cancelled, superseded
แต่ละรายการตั้งเวลาเก็บ due_at แบบ UTC, timezone ของ IANA, เวลาท้องถิ่นตามนาฬิกา และ fold การเปลี่ยนกฎของ timezone จะไม่เขียน due_at ใหม่ การคำนวณใช้เพียง Intl.DateTimeFormat
| อินพุต | ผลลัพธ์ |
|---|---|
2026-07-01T09:00 America/New_York |
2026-07-01 13:00 UTC |
2026-03-29T02:30 Europe/Berlin |
ปฏิเสธด้วย nonexistent_local_time (นาฬิกากระโดดจาก 02:00→03:00) |
2026-10-25T02:30 Europe/Berlin ไม่มี fold |
ปฏิเสธด้วย ambiguous_local_time |
เหมือนกัน, fold earlier |
2026-10-25 00:30 UTC (CEST) |
เหมือนกัน, fold later |
2026-10-25 01:30 UTC (CET) |
2026-07-01T09:00 Mars/Base |
ปฏิเสธด้วย unknown_timezone |
2025-12-31T23:59 UTC ที่ผ่านไปแล้ว |
ปฏิเสธด้วย past_time |
หน้ายืนยันและรายการแสดงเวลาท้องถิ่นพร้อม zone และเวลา UTC
cron แต่ละ tick จะหยิบแถวที่ถึงกำหนดได้สูงสุด 20 แถว แบบ round-robin ข้าม installation การ claim คือ compare-and-set บนสถานะและ lease token ที่สังเกตได้ มันตั้งค่า running, lease token ใหม่, lease_until = now + LEASE_MS และ attempt_count + 1 แต่ละ claim ส่งคำสั่งหนึ่งคำสั่ง แถวจะถูก settle ก็ต่อเมื่อ lease token ของ runner เองยังใช้ได้อยู่เท่านั้น แถว running ที่ crash จะถูก reclaim หลังจาก lease หมดอายุ และถูกส่งใหม่ด้วย key เดิม
| คำตอบของ Dee Wan | ผลลัพธ์ของปลั๊กอิน |
|---|---|
200 transition_applied / idempotent_replay |
completed |
400 invalid_request |
refused ไม่ retry |
401 plugin_unauthorized |
refused ไม่ retry |
403 capability_refused / requester_unauthorized |
refused ไม่ retry |
404 content_not_found |
refused ไม่ retry |
409 scheduled_target_stale / workflow_conflict / dependent_content / idempotency_key_reused |
refused ไม่ retry |
422 transition_unavailable / transition_refused |
refused ไม่ retry |
| status/code ไม่ตรงกัน, JSON เสีย, 3xx, status อื่น | refused untrusted_response |
| 429 | retrying ที่ Retry-After (วินาทีหรือ HTTP date) |
| 500/502/503/504, network ล้มเหลว, timeout (10 วินาที) | retrying, backoff 15 วินาที·2^(n-1) สูงสุด 5 นาที, jitter 50–100 % |
เกิน MAX_ATTEMPTS, เกิน due_at + MAX_LATENESS_MS หรือ Retry-After > MAX_RETRY_AFTER_MS |
refused retry_exhausted ไม่ส่งอะไรอีก |
retry_exhausted หลัง network ล้มเหลวหมายความว่าไม่ทราบผลลัพธ์ ดังนั้นให้ตรวจเนื้อหาใน Dee Wan รายการตั้งเวลาจะไม่ถูกส่งช้ากว่าขีดจำกัดความล่าช้าเลย รวมถึงตอนที่ lease ที่ crash ถูก reclaim ด้วย
ความพยายามแต่ละครั้งถูกบันทึกไว้ใน schedule_attempt (attempt, เวลาเริ่ม, เวลาเสร็จ, code, HTTP status) นี่คือบันทึกเชิงปฏิบัติการสำหรับผู้ใช้ ไม่ใช่ telemetry
ข้อมูล, การเก็บรักษา, การถอนการติดตั้ง
หัวข้อที่มีชื่อว่า “ข้อมูล, การเก็บรักษา, การถอนการติดตั้ง”- ข้อมูลที่เก็บ: installation (id, site id, base URL ของ Dee Wan และ token ที่เข้ารหัสด้วย AES-GCM), รหัส activation ที่ hash แล้ว, session ที่ hash แล้ว (30 นาที) พร้อม CSRF token, รายการตั้งเวลา และความพยายามแต่ละครั้ง ไม่มีการเก็บ snapshot ของเนื้อหาและไม่มีการเก็บ session ผู้ใช้ของ Dee Wan
- รหัส launch และ token ดิบไม่เคยถูก log หรือเก็บในรูปดิบ
- การถอนการติดตั้งปลั๊กอินใน Dee Wan จะเพิกถอน token ข้อมูลปลั๊กอินยังคงอยู่ รายการตั้งเวลาที่เหลือจะถูกปฏิเสธด้วย
plugin_unauthorizedเมื่อถึงกำหนด - หากต้องการลบข้อมูลปลั๊กอิน ผู้ดูแลระบบเรียก
POST /admin/installations/:id/delete-dataพร้อมBearer ADMIN_SECRETการเรียกจะถูกปฏิเสธด้วย 409execution_in_progressขณะที่มีรายการตั้งเวลากำลัง running ไม่ได้สร้างกลไกล้างข้อมูลตามระยะเวลาเก็บรักษาแบบอัตโนมัติไว้
การทดสอบ
หัวข้อที่มีชื่อว่า “การทดสอบ”npm test # vitest, from this directorynpm run typechecktest/helpers/d1.ts คือ D1 shim ที่ใช้ better-sqlite3 test/helpers/fake-core.ts จำลอง Dee Wan protocol v1: idempotent replay ต่อ key, การตรวจ pin และ revision, อำนาจที่ได้รับ delegate และการฉีดความล้มเหลว
test/docs.test.ts รันตาราง Runner และ Time ข้างต้นผ่าน classify และ resolveLocalTime ดังนั้นแถวที่ไม่มีใครอัปเดตจะ fail backend/test/plugin-contract.test.ts ของ core ตรวจสำเนาตาราง outcome, capability และรูปแบบของ secret ของปลั๊กอินนี้เทียบกับของ core เอง