สะพานเชื่อม Cordis
สะพานเชื่อมฝัง DeepSeek Harness (DSH) ใน backend ของ Libre WebUI โดยทำงานเป็นต้นไม้ปลั๊กอินใน runtime Cordis ที่ LWUI เป็นโฮสต์ ความสามารถจึงมาในรูปบริการ Cordis ไม่ใช่โมดูลที่ import โดยตรง
สะพานเชื่อม ปิดตามค่าเริ่มต้น สิ่งที่อธิบายจะเกิดหลังผู้ดูแลระบบติดตั้งเปิดใช้เท่านั้น ดู การตั้งค่า Cordis
เหตุผลที่ใช้สะพานเชื่อมแทนการรวมโดยตรง
การ import แพ็กเกจ DSH จากบริการ LWUI สั้นกว่าแต่เหมาะสมน้อยกว่า เพราะเอนจินจะเป็น dependency ตอนคอมไพล์ การเปลี่ยนอะแดปเตอร์ ลูปเอเจนต์ หรือลบเอนจินต้องแก้และ deploy LWUI ใหม่
สะพานเชื่อมกลับทิศ dependency: LWUI พึ่งสัญญานามธรรมหนึ่งชุด และเอกสารองค์ประกอบ Cordis เลือกตัวทำงานให้:
- เปลี่ยนเป้าหมายโดยไม่ build ใหม่ องค์ประกอบเป็น YAML จึงเปลี่ยนผู้ให้บริการด้วยการตั้งค่า
- กำหนดความสามารถ แต่ละความสามารถคือแถว Loader การเปลี่ยนองค์ประกอบของ operator มีผลเมื่อเริ่มโฮสต์ครั้งถัดไป
- ลบได้สะอาด ไฟเบอร์รากเป็นเจ้าของบริการ ตัวฟัง และผลข้างเคียงทั้งหมด การคืนไฟเบอร์ถอนทุกอย่างและหยุดเอนจินได้โดยไม่ restart LWUI
ชั้นการทำงาน
dependency DSH ที่เป็นรูปธรรมอยู่ภายใน backend/src/cordis/dsh/ เส้นทางและบริการแอปใช้สัญญาสะพานเชื่อม ไดรเวอร์ Work มีองค์ประกอบในหน่วยความจำแยก และไม่โหลดปลั๊กอินไฟล์ของโฮสต์
สัญญา
สัญญาใน backend/src/cordis/contracts.ts จำกัดเฉพาะรูปแบบข้อมูลที่ API ต้องใช้ โดยไม่อิงศัพท์ภายในเอนจิน
| สัญญา | หน้าที่ |
|---|---|
DshEngine.status() | สถานะวงจรชีวิตบริการ (pending / ready / failed) |
DshEngine.modelConfiguration() | โมเดลและผู้ให้บริการเริ่มต้นขององค์ประกอบที่ทำงาน |
DshEngine.listSessions() | สรุปเซสชัน ใหม่ก่อน |
DshEngine.getSession(id) | เซสชันพร้อมข้อความที่ฉายสถานะแล้ว |
DshEngine.createSession(opts) | จอง ID และโฟลเดอร์งาน |
DshEngine.updateSessionSettings(id, settings) | บันทึกโมเดลจริงและสิทธิ์ไฟล์เนทีฟเมื่อว่าง |
DshEngine.decideApproval(id, approvalId, decision) | ตัดสินการอนุมัติเนทีฟของเซสชันเจ้าของ |
DshEngine.deleteSession(id) | จบเซสชันและคืนเอเจนต์ |
DshEngine.listAgents() | เอเจนต์ที่ทำงาน แยกตัวหลักและตัวลูก |
DshEngine.listTools() | เครื่องมือที่ลงทะเบียนและโมเดลมองเห็น |
DshEngine.sendMessage(id, txt) | เริ่มรอบและคืน handle สตรีม |
DshEngine.cancel(id) | ยกเลิกรอบที่กำลังทำงาน |
สัญญาถูกเผยแพร่เป็นบริการ Cordis libreDshEngine ผู้ใช้บริการอ่านด้วย ctx.get('libreDshEngine') โดยไม่ import โมดูลสะพานเชื่อม
EngineStreamChunk ส่ง text, reasoning, tool-call, tool-result, approval-request, approval-decision, error และ done เฟรมสดอ้างเอเจนต์และเซสชันเจ้าของ ข้อความถาวรที่ตรงกันไม่ถูกส่งซ้ำ sendMessage คืน handle ซึ่ง subscribe เล่นสิ่งที่ส่งไปแล้วให้ใหม่ โทเคนแรกที่มาเร็วจึงไม่หายก่อน HTTP handler แนบตัวฟัง
ลำดับหนึ่งรอบแชท
ใช้ NDJSON แทน WebSocket เพราะหนึ่งรอบคือข้อมูลจากเซิร์ฟเวอร์สู่ไคลเอนต์หลังคำขอ การคงไว้บน POST ไม่ต้อง handshake ตั๋ว หรือโปรโตคอลเชื่อมต่อซ้ำอีกชุด และทั้งรอบอยู่ภายในคำขอที่ยืนยันตัวตนเดียว
DONE และ PENDING
Cordis เริ่มปลั๊กอินเมื่อบริการที่ประกาศพร้อม แถวจึงมีช่วงที่ยังไม่เริ่ม การสับสนแนวคิดสองอย่างนี้เป็นสาเหตุหลักที่เอนจินเงียบ
สถานะแถว Loader แต่ละแถวผ่าน PENDING → LOADING → ACTIVE หรือ FAILED หากบริการขาดจะรอไม่สิ้นสุดแทนเกิดข้อผิดพลาด องค์ประกอบไม่ครบจึงอาจเริ่มแล้วไม่ให้บริการอะไร
ความพร้อมบริการ โฮสต์รายงานบริการที่คาดไว้ดังนี้:
| สถานะ | ความหมาย | สาเหตุ |
|---|---|---|
pending | ยังไม่ลงทะเบียนใน context | แถวที่ให้บริการยังไม่เริ่มหรือถูกปิด |
ready | ลงทะเบียนและใช้งานได้ | แถวที่ให้บริการเริ่มแล้ว |
failed | ประกาศแล้วแต่ใช้ไม่ได้ | รายงานพร้อมข้อความ detail |
host.status() แสดงความพร้อมและบริการจำเป็นที่ขาด ส่วน GET /api/cordis/health แสดงข้อมูลเดียวกัน หากขาดบริการจำเป็น การเริ่มจะเกิดข้อผิดพลาดแทนเผยแพร่เอนจินที่คืนรายการว่าง
dependency สองสายที่ตั้งผิดง่าย:
dsh-toolsต้องมีsystemPromptdsh-agent-loopรอagents,sessions,llm,tools,systemPromptและsessionProjections
หากขาดตัวใด ที่เก็บเซสชันอาจทำงานแต่เอนจินไม่ตอบข้อความ
การตั้งค่าผู้ให้บริการ
แถว libre-webui-llm-adapter ที่ให้มาใช้ผู้ให้บริการที่ตั้งไว้ใน LWUI ตัวเลือกโมเดลบนหน้าเอนจินเลือกโมเดลรายเซสชันโดยไม่แทนแถวนั้น
การแก้แถวมีผลเมื่อเริ่มโฮสต์ครั้งถัดไป restart backend หรือปิดแล้วเปิด Cordis หากสวิตช์ผู้ดูแลไม่ถูกล็อก เซสชันถาวรยังอยู่ในที่เก็บและกลับมาทำงานด้วยองค์ประกอบปัจจุบัน
โค้ดรวมระบบที่เชื่อถือได้ใช้ lifecycle API ของ Loader โดยตรงได้ สะพานเชื่อมไม่มี endpoint สลับอะแดปเตอร์และไม่คืนตัวเก่าให้อัตโนมัติหากตัวใหม่ล้มเหลว
การย้อนกลับ
การคืนไฟเบอร์รากของโฮสต์ถอนทุกอย่างที่เอนจินติดตั้ง ความสัมพันธ์เจ้าของนี้คือหลักประกัน:
- ปลั๊กอินลงทะเบียนบริการ และถอนพร้อมไฟเบอร์
- การฟัง
session/eventลงทะเบียนใน constructor ของสะพานเชื่อม และเป็นของไฟเบอร์แถวนั้น - สะพานเชื่อมติดตาม handle เอเจนต์และคืนใน effect ตอนปิด
- โฮสต์คืน root context ที่เป็นเจ้าของทุกแถว
stopCordisHost() เรียกซ้ำได้อย่างปลอดภัย และเป็นส่วนของการปิด backend จึงคืน timer และ file handle อย่างชัดเจน ไม่ปล่อยไว้จนโปรเซสออก
ตัวตนและความถาวรของเซสชัน
หน้าเอนจินจอง ID ที่ไม่สื่อความหมายเมื่อสร้าง หากเปิดการเก็บถาวร header จะถูกบันทึกทันที แม้เซสชันว่างก็อยู่ข้าม restart สะพานเชื่อมแสดงทั้งเซสชันสดและที่เก็บ อ่านบันทึกผ่าน API DSH ที่ตรวจความถูกต้อง และกลับมาที่ ID เดิมเมื่อมีข้อความต่อ ข้อความผู้ใช้ใหม่ใช้ constructor DSH ที่มี ID
การลบยกเลิกและคืนเอเจนต์ก่อนลบไฟล์ อะแดปเตอร์ JSONL ในเครื่องตรวจที่เก็บและเส้นทางเซสชันและปฏิเสธ symlink หากที่เก็บแบบกำหนดเองไม่รองรับการลบ จะคืนข้อผิดพลาด ไม่กล่าวอ้างว่าลบแล้ว
การยกเลิกถึงเอเจนต์ คำขอโมเดล และเครื่องมือ ไคลเอนต์หลุดจะยกเลิกรอบ แต่ข้อความที่เสร็จยังอ่านได้ บัฟเฟอร์เล่นสตรีมซ้ำมีขีดจำกัดและเก็บคำตอบที่มาเร็วกว่าการแนบผู้อ่าน
เอนจินโฮสต์เป็น ความสามารถ solo หนึ่ง replica การติดตั้ง team โหลด runtime JSONL ในเครื่องนี้ไม่ได้ Work ใน sandbox ใช้ที่เก็บ SQL เดิมของงาน การรัน ข้อความ การอนุมัติ และเหตุการณ์
อินเทอร์เฟซ HTTP
| เมธอด | เส้นทาง | หน้าที่ |
|---|---|---|
GET | /api/cordis/health | สถานะสะพานเชื่อม ไม่ต้องยืนยันตัวตน |
GET | /api/cordis/sessions | แสดงเซสชัน |
POST | /api/cordis/sessions | สร้างเซสชัน |
GET | /api/cordis/sessions/:id | อ่านเซสชันและข้อความ |
DELETE | /api/cordis/sessions/:id | จบเซสชัน |
POST | /api/cordis/sessions/:id/messages | ส่งข้อความและสตรีม NDJSON |
POST | /api/cordis/sessions/:id/cancel | ยกเลิกรอบปัจจุบัน |
GET | /api/cordis/agents | แสดงเอเจนต์ที่ทำงาน |
GET | /api/cordis/tools | แสดงเครื่องมือที่ลงทะเบียน |
ทุกเส้นทางนอกจาก /health ต้องใช้เซสชันผู้ดูแลที่ยืนยันตัวตน ระหว่างให้บริการไม่ได้จะคืน 503 พร้อม code เป็น CORDIS_DISABLED, CORDIS_STARTING หรือ CORDIS_UNAVAILABLE

หน้า frontend/src/pages/CordisPage.tsx เข้าถึงที่ /cordis จากแถบด้านข้าง ใช้แสดงและสร้างเซสชัน แสดงเครื่องมือ และสตรีมรอบลงในบทสนทนา หากปิดหรือเริ่มไม่สำเร็จจะแสดงเหตุผลแทนรายการว่าง เพราะไม่เช่นนั้น “ไม่มีเซสชัน” และ “ไม่มีเอนจิน” จะเหมือนกัน
ไคลเอนต์ frontend/src/utils/api/cordisApi.ts ใช้เฉพาะ API นี้ ไม่ import ชนิด backend หรือแพ็กเกจ @deepseek-ai/* จึงเปลี่ยนเอนจินได้โดยไม่แก้ frontend ใช้ sendMessage(sessionId, text, { onChunk }) อ่านรอบ ไคลเอนต์แยก JSON ตามบรรทัดเองและรองรับเฟรมที่แยกข้ามการอ่านเครือข่าย
ตัวควบคุมแชทเอนจิน
หน้าแสดง Markdown ตาราง และโค้ดเน้นไวยากรณ์ พร้อมคัดลอกคำตอบและโค้ด พรอมต์ระบบและบริบท runtime อยู่ใน บริบทเซสชัน ที่พับไว้ ไม่แสดงเป็นข้อความผู้ใช้ ส่วนความคิดที่เปิดเผยและกิจกรรมเครื่องมือแยกพับได้ ผลเครื่องมือยังจับคู่กับการทำงานเดิมหลังโหลดใหม่
เลือกโมเดลผู้ให้บริการจริงจากโมเดลในเครื่องและปลั๊กอินที่ผู้ดูแลใช้ได้ โดยมีตัวตนผู้ให้บริการ persona และเอเจนต์ Chat ไม่ใช่ ID โมเดลและไม่แทรกคำสั่งในบทสนทนาเอนจิน header persona-model เก่าที่ผิดจะไม่ใช้เป็นค่าเริ่มต้น โดยไม่แก้บันทึกเดิม
แต่ละเซสชันเลือก อ่านอย่างเดียว หรือ เขียนในพื้นที่ทำงาน ซึ่งบังคับด้วยนโยบายไฟล์ DSH และขอบเขต canonical ช่องเขียนแสดงพื้นที่สิทธิ์ การตั้งค่าเก็บเป็นเหตุการณ์เนทีฟ อยู่ข้าม restart และแก้ระหว่างรอบไม่ได้
คำขอเพิ่มสิทธิ์เนทีฟแสดงเป็นการ์ด อนุญาตครั้งเดียว / ปฏิเสธ ที่การทำงาน การอนุมัติใช้เฉพาะคำขอนั้น ไม่เปลี่ยนโหมดถาวร คำขอเก่าหรือยกเลิกแล้วอนุมัติไม่ได้ และ Chat แบบไม่มีหน้าจอปฏิเสธคำถามที่แสดงไม่ได้ สะพานเชื่อมไม่ให้สิทธิ์โฮสต์ไม่จำกัด
endpoint ผู้ดูแลเพิ่มเติม:
| เมธอด | เส้นทาง | หน้าที่ |
|---|---|---|
GET | /api/cordis/models | โมเดลผู้ให้บริการที่พร้อมใช้และค่าเริ่มต้นจริง |
PATCH | /api/cordis/sessions/:id/settings | กำหนดโมเดลหรือโหมดสิทธิ์ของเซสชัน |
POST | /api/cordis/sessions/:id/approvals/:approvalId | ตัดสินคำขอด้วย allowed-once หรือ rejected |
ใช้เอนจินใน Chat
เปิด การเข้าถึงและนโยบาย → โมเดลเอเจนต์ CLI และ เอนจิน Cordis ผู้ดูแลจึงเลือก DeepSeek Harness ใน Chat ได้ ทุกคำขอใช้เซสชันชั่วคราวใหม่พร้อมประวัติที่คำขอส่งมา ฐาน Chat ปกติยังเป็นหลัก บทสนทนาอื่น fork และการลองซ้ำไม่แชร์ประวัติลับของเอนจิน บันทึกชั่วคราวลบหลังจบหรือยกเลิกและไม่แสดงในหน้าเอนจิน
องค์ประกอบมาตรฐานมี DeepSeek Harness · โมเดล (ผู้ให้บริการ) ในกลุ่มเอเจนต์ ID ที่เก็บห่อเส้นทางผู้ให้บริการเดียวกับหน้าเอนจิน: dsh:lwui:ollama:<model> หรือ dsh:lwui:plugin:<plugin>:<model> โดยแต่ละส่วนเข้ารหัสเปอร์เซ็นต์ การเชื่อมต่อ DSH เนทีฟในเครื่อง เพิ่ม dsh:native:<provider>:<model> จากแคตตาล็อกสด โดยใช้การตั้งค่าและข้อมูลรับรองของ DSH
ติดตั้งแพ็กเกจ Apache-2.0 แยกจาก libre-webui/dsh-native-provider หรือเตรียม bundle จาก LWUI ทั้งสองใช้ชื่อ @libre-webui/dsh-native-provider และเก็บคีย์ใน DSH ต้องเป็นเครื่อง Unix และบัญชี OS เดียวกันผ่าน socket ส่วนตัว จึงไม่แยกแอปที่ใช้บัญชีเดียวกัน เปิดเฉพาะการอนุมาน ไม่มีเซสชันเอเจนต์หรือเครื่องมือเนทีฟ ดูการติดตั้ง restart โปรไฟล์ อัปเกรด และลบในคู่มือการตั้งค่า หากขาดการเชื่อมต่อหรือโมเดลจะล้มเหลวโดยไม่เปลี่ยนผู้ให้บริการ การเรียกเนทีฟยังปรากฏใน การใช้งานผู้ให้บริการ พร้อมโมเดล โทเคน เวลา และผล
โปรไฟล์พื้นฐาน dsh ใช้โมเดลเริ่มต้นขององค์ประกอบที่ทำงาน อะแดปเตอร์กำหนดเองแสดงเพียงโปรไฟล์พื้นฐาน ไม่เสนอการแทนผู้ให้บริการ LWUI ที่ไม่รองรับ
ชื่อและสรุปความคิดแกะตัวเลือก DSH เป็นผู้ให้บริการแล้วขอข้อความโดยตรง โดยไม่ใช้เครื่องมือหรือเซสชันเอเจนต์ โปรไฟล์พื้นฐานอ่านค่าเริ่มต้นที่ทำงานอยู่รวมการแทนค่าแถวสะพานเชื่อม ไม่เดาจากแคตตาล็อก อะแดปเตอร์อื่นต้องมีโมเดลงาน Ollama หรือปลั๊กอินอย่างชัดเจน หากผู้ให้บริการไม่พร้อมจะเกิดข้อผิดพลาดปกติหรือชื่อ preview ในเครื่อง ไม่ส่งไปผู้ให้บริการอื่น
คำขอใช้ข้อมูลและการตั้งค่าของผู้ดูแลที่ยืนยันตัวตน ไม่เลือกของผู้ดูแลอื่นโดยปริยาย พื้นที่ Cordis ที่ตั้งไว้ยังเป็นค่าเริ่มต้น Chat ไม่แทนด้วย home ของผู้ใช้เซิร์ฟเวอร์
Work แบบ sandbox
Cordis ที่เปิดเพิ่มช่อง เอนจิน ใน Work ให้เลือก Libre WebUI หรือ DeepSeek Harness โดยคงชื่อโมเดลและผู้ให้บริการเดิม สำหรับ LWUI เก็บเป็น dsh:<model> ส่วนเนทีฟเก็บ providerType: dsh พร้อม ID ผู้ให้บริการตรงตัวและ ID โมเดลดิบ การตรวจสิทธิ์และเครื่องมือยังเหมือนเดิม และข้อมูลรับรองเนทีฟต้องใช้ผู้ดูแลที่ active
ทุกการรันสร้างลูป DSH แยกในหน่วยความจำ อะแดปเตอร์รับประวัติ Work ปัจจุบัน metadata รูปภาพ และ schema เครื่องมือ ตัวเครื่องมือเพียงรอผลจาก Work ไม่อ่านไฟล์หรือเปิดโปรเซสโฮสต์
Work ตรวจอาร์กิวเมนต์ ขออนุมัติ รันในพื้นที่งาน บันทึกผลและ replay state ของผู้ให้บริการลง SQL บังคับ budget และส่งเหตุการณ์ การปฏิเสธเครื่องมือให้ผลปฏิเสธปกติ การยกเลิกคืน DSH และเก็บกวาดคอนเทนเนอร์ตามเดิม หลัง worker กลับมา ไดรเวอร์ใหม่ใช้บริบทที่กู้คืนโดยไม่ทำผลที่เสร็จซ้ำ
ไม่ต้องมีองค์ประกอบเอนจินโฮสต์หรือที่เก็บ JSONL ใช้กฎ Work Docker/Kubernetes และการติดตั้งเดิม รวมการเก็บถาวรร่วมกันใน team
ขอบเขตความปลอดภัย
หน้าเอนจินและเอเจนต์โฮสต์ของ Chat ใช้เฉพาะผู้ดูแล เซสชันเป็นคอนโซลผู้ดูแลร่วมกันรวมพรอมต์ระบบ ไม่ใช่พื้นที่ส่วนตัว บัญชีทั่วไปอ่าน สร้าง แก้ หรือยกเลิกผ่าน API ไม่ได้
เครื่องมือไฟล์ที่ให้มาจำกัดอ่านและเขียนในพื้นที่ที่กำหนดด้วยเส้นทาง canonical รวมการแก้ symlink โฟลเดอร์เซสชันที่เปลี่ยนต้องอยู่ภายใน นโยบาย DSH และอนุมัติครั้งเดียวยังมีผล ปลั๊กอินที่ operator ติดตั้งคือโค้ดเซิร์ฟเวอร์ที่เชื่อถือได้และเพิ่มความสามารถได้ การอนุมัติเอนจินแยกจากการอนุมัติและคอนเทนเนอร์ Work
ไดรเวอร์ Work แยกออกมา ไม่โหลดปลั๊กอินไฟล์ shell หรือ persistence ของโฮสต์ และทำงานผ่านสิทธิ์และ sandbox ของ Work เท่านั้น ผู้ให้บริการระยะไกลยังต้องเลือกเปิด โดยใช้เส้นทางที่บัญชีที่เลือกตั้งไว้