วิธีเขียน dsh plugin สำหรับ DeepSeek Harness ด้วยตนเอง
เรียนรู้วิธีสร้าง dsh plugin ตั้งแต่เริ่มต้น ครอบคลุมการตั้งค่า package.json การใช้ไฟล์ patch เพื่อ mount ปลั๊กอิน รวมถึงการเขียนฟังก์ชันและ hooks ที่จำเป็นต่อการทำงาน
dsh plugin คืออะไร
dsh plugin คือ npm package ที่ส่งออกฟังก์ชัน apply และมาพร้อมกับไฟล์ YAML ขนาดเล็กหนึ่งไฟล์เพื่อแจ้งให้ DeepSeek Harness โหลดปลั๊กอินดังกล่าว โดยไม่มี SDK เฉพาะสำหรับปลั๊กอินที่ต้องเรียนรู้ก่อน dsh เป็นแอปพลิเคชันที่สร้างบน Cordis และแนวคิดที่ว่า "ทุกอย่างคือปลั๊กอิน" นั้นเป็นเรื่องจริง ไม่ว่าจะเป็น registry ของเครื่องมือ, ลูปของเอเจนต์, พื้นที่จัดเก็บเซสชัน หรือเว็บเซิร์ฟเวอร์ ทั้งหมดล้วนเป็นส่วนหนึ่งในโครงสร้างปลั๊กอินเดียวกันกับที่คุณกำลังจะเพิ่มแพ็กเกจของคุณเข้าไป
Cordis เป็นเฟรมเวิร์กสำหรับการประกอบส่วนประกอบต่างๆ (composition framework) ที่ถูกสร้างขึ้นอย่างอิสระและใช้งานมานานหลายปีในฐานะฐานรากของ Koishi chatbot framework โดย Cordis จะจัดการเรื่องการโหลดและการยกเลิกการโหลด รวมถึงแก้ไขความสัมพันธ์ระหว่างปลั๊กอินต่างๆ โดยที่ตัวมันเองไม่มีความรู้เรื่องเอเจนต์เลย ส่วนประกอบทั้งหมดที่เกี่ยวข้องกับเอเจนต์จะมาจากแพ็กเกจ harness ที่วางซ้อนอยู่ด้านบนอีกที นี่คือเหตุผลว่าทำไมโครงสร้างของปลั๊กอินด้านล่างนี้จึงดูเรียบง่าย เพราะฟังก์ชันการทำงานส่วนใหญ่ที่คุณได้รับนั้นเป็นการสืบทอดมาจากระบบหลัก
ปลั๊กอินประกอบด้วยสองส่วน ส่วนที่เป็น host จะทำงานใน Node เพื่อลงทะเบียนเครื่องมือและ event listener รวมถึงสามารถให้บริการของตนเองได้ ส่วนที่เป็น browser จะทำงานภายใน Web UI เพื่อลงทะเบียนช่องสำหรับอินเทอร์เฟซ โดยปกติแล้วปลั๊กอินตัวแรกมักจะเป็นแบบ host เท่านั้น ดังนั้นให้ถือว่าส่วน browser เป็นส่วนเสริมที่คุณสามารถเลือกใช้ได้เมื่อต้องการ
คู่มือนี้เขียนขึ้นโดยอ้างอิงจาก @deepseek-ai/dsh เวอร์ชัน 0.1.0-rc.7 ซึ่งเป็นแท็ก latest บน npm เมื่อวันที่ 19 สิงหาคม 2026 dsh ยังอยู่ในสถานะ developer preview และ README ของโครงการระบุว่าจะมีการเปลี่ยนแปลงที่ส่งผลต่อความเข้ากันได้ (breaking changes) ชื่อคีย์ทั้งหมดที่ระบุไว้ด้านล่างนี้ถูกอ่านมาจากเอกสารประกอบต้นทางและ repository ณ วันที่ดังกล่าว โปรดตรวจสอบข้อมูลอีกครั้งก่อนที่จะนำไปใช้งานจริง เนื่องจาก API ในช่วง preview อาจมีการเปลี่ยนชื่อฟิลด์ระหว่างการออก release candidate หากคุณยังไม่ได้ติดตั้ง harness ให้ดำเนินการติดตั้งผ่าน DeepSeek Harness บน VPS และ การตั้งค่า dsh API key และโมเดล ให้เรียบร้อยก่อน แล้วจึงกลับมาที่นี่
โหลดไฟล์ทดสอบหนึ่งไฟล์ก่อนเริ่มทำแพ็กเกจ
การทำแพ็กเกจตั้งแต่เริ่มต้นเป็นวิธีที่ช้าในการเรียนรู้เรื่องนี้ ให้โหลดไฟล์เพียงไฟล์เดียวเพื่อพิสูจน์ว่า runtime เรียกใช้โค้ดของคุณได้ แล้วจึงค่อยทำแพ็กเกจ
สร้างโฟลเดอร์ไว้นอกไดเรกทอรี harness และใส่ไฟล์ลงไปหนึ่งไฟล์
import type { Context } from '@deepseek-ai/cordis'
export const name = 'hello-plugin'
export function apply(ctx: Context) {
console.log('[hello-plugin] plugin loaded')
}export const name คือข้อมูลเมตาที่ใช้ระบุชื่อปลั๊กอินในการวินิจฉัย apply คือสัญญาการทำงานทั้งหมด: Cordis จะเรียกใช้ฟังก์ชันนี้หนึ่งครั้งและส่ง context ที่กำหนดขอบเขตไว้สำหรับปลั๊กอินของคุณมาให้ สิ่งใดก็ตามที่คุณลงทะเบียนไว้บน context นั้นจะถูกยกเลิกการทำงานให้โดยอัตโนมัติเมื่อปลั๊กอินถูกทำลาย
เขียนไฟล์ cordis.yml ไว้ข้างๆ กัน
- insert:
- id: hello
name: '/absolute/path/to/scratch-plugin/hello.ts'จากนั้นให้บูตโปรไฟล์โดยวางไฟล์ดังกล่าวทับลงไป
dsh web --patch ./scratch-plugin/cordis.ymlหาก dsh ไม่อยู่ใน PATH ของคุณ npx @deepseek-ai/dsh web --patch ./scratch-plugin/cordis.yml สามารถทำงานแทนได้ เส้นทางผ่าน npx อาจทำให้คุณได้รับ release candidate เวอร์ชันเก่าที่ถูกแคชไว้แทนที่จะเป็นเวอร์ชันที่คู่มือนี้ระบุ ดังนั้นหาก harness ปฏิเสธ flag ที่ระบุไว้ในเอกสาร ให้ตรวจสอบ วิธีแก้ไขข้อผิดพลาดในการติดตั้งและเวอร์ชันของ dsh ก่อนที่จะเริ่มสงสัยในไฟล์ของคุณเอง คุณควรเห็น [hello-plugin] plugin loaded ในเทอร์มินัลที่เริ่ม dsh หากไม่มีอะไรปรากฏขึ้น แสดงว่าแถวนั้นไม่ได้รับการแก้ไข
ฟิลด์ name รับค่าเป็นชื่อแพ็กเกจ npm หรือเส้นทางในระบบไฟล์ และเอกสารประกอบของ upstream ระบุว่าเส้นทางต้องเป็นแบบ absolute การใช้ ./hello.ts แบบ relative เป็นสิ่งแรกที่ควรตรวจสอบเมื่อปลั๊กอินทดสอบไม่แสดงผลลัพธ์ สิ่งที่สองคือส่วนขยายของไฟล์ ลูปที่ระบุในเอกสารจะทำงานเป็น pnpm dsh web --patch ... จากการ clone คลังเก็บข้อมูลของ harness ซึ่งรายการที่เป็น TypeScript จะถูกโหลดผ่าน tsx หาก dsh ของคุณมาจาก npm ให้ชี้แถวนั้นไปยังไฟล์ JavaScript ปกติ หรือ build ไฟล์นั้นก่อน
--patch เป็น flag สำหรับตัวเรียกใช้งาน (launcher) และ overlay ของมันจะถูกนำไปใช้เป็นลำดับสุดท้าย หลังจาก bundle ทุกตัวและหลังจาก patch โปรไฟล์ของคุณเอง ดังนั้น scratch overlay จึงมีลำดับความสำคัญสูงสุดเสมอ ซึ่งเป็นสิ่งที่คุณต้องการในขณะที่กำลังวนซ้ำเพื่อแก้ไขโค้ด
เขียนเครื่องมือขนาดเล็กที่สุดที่ทำงานได้จริง
บรรทัดใน log เป็นหลักฐานว่า plugin โหลดสำเร็จ ส่วนเครื่องมือเป็นหลักฐานว่า plugin นั้นเป็นส่วนหนึ่งของ agent
import type { Context } from '@deepseek-ai/cordis'
import { defineTool } from '@deepseek-ai/dsh-tools'
export const name = 'greet-tool'
export const inject = ['tools']
export function apply(ctx: Context) {
ctx.tools.register(defineTool({
name: 'greet',
description: 'Greet someone by name.',
parameters: {
name: { type: 'string', required: true, description: 'The name to greet' },
},
output: {
schema: { type: 'string' },
render: (_args, value) => [{ type: 'text', text: value }],
},
async execute(args) {
return `Hello, ${args.name}!`
},
}))
}export const inject = ['tools'] คือบรรทัดที่ผู้คนมักลืมใส่ รายการในไฟล์ configuration ของ Cordis จะเริ่มทำงานพร้อมกัน ดังนั้นตำแหน่งของแถวในไฟล์จึงไม่รับประกันลำดับการโหลด การกำหนดลำดับจะทำผ่านการประกาศ dependency เท่านั้น inject จะบอกให้ Cordis รอจนกว่า ctx.tools จะมีอยู่จริงก่อนที่จะเรียก apply ของคุณ หากไม่มีบรรทัดนี้ โค้ดของคุณอาจทำงานในจังหวะที่ registry ยังไม่พร้อมให้ลงทะเบียน
ส่วนที่เหลือของ object คือสัญญาที่ model มองเห็น parameters คือ schema ของอาร์กิวเมนต์ และ execute จะรับอาร์กิวเมนต์ที่ผ่านการ parse ตาม schema นั้นมาแล้ว output.schema อธิบายค่าที่ execute ส่งกลับมา ในขณะที่ render จะแปลงค่านั้นให้เป็น content block ที่ model อ่านได้ การแยกสองส่วนนี้ออกจากกันคือสิ่งที่ทำให้ interface แสดงผลอย่างหนึ่งในขณะที่ model อ่านอีกอย่างหนึ่ง
เริ่มการทำงานของ profile และสั่งให้ assistant ทักทายใครบางคนด้วยชื่อ คำตอบจะถูกส่งกลับมาผ่าน execute ของคุณ การลงทะเบียนผ่าน ctx สามารถย้อนกลับได้ ดังนั้นการกำจัด (dispose) plugin จะยกเลิกการลงทะเบียนเครื่องมือให้คุณโดยอัตโนมัติ สำหรับสิ่งใดก็ตามที่ Cordis ไม่สามารถรับรู้ได้ เช่น socket หรือ file handle ให้เรียกใช้ ctx.effect() แล้วส่ง disposer ให้กับมัน
จุดขยายสองจุดที่ปลั๊กอินแรกมักต้องใช้งาน
รายการจุดเชื่อมต่อ (seams) ทั้งหมดนั้นมีจำนวนมาก แต่มีสองจุดที่ครอบคลุมการทำงานของปลั๊กอินส่วนใหญ่
Conversation events คือกระแสข้อมูลที่ถูกบันทึกไว้อย่างถาวร ชื่อเหตุการณ์คือ session/event, turn/start, turn/end, step/start, step/end, user/message, assistant/message, assistant/chunk, tool/call และ tool/result คุณสามารถแนบ listener ปกติเข้ากับเหตุการณ์เหล่านี้ได้
ctx.on('tool/call', (payload) => {
console.log('[my-plugin] tool/call', JSON.stringify(payload))
})ให้พิมพ์ payload ออกมาดูหนึ่งครั้งแล้วอ่านค่า อย่าคัดลอกชื่อฟิลด์ของ payload จากคู่มือใดๆ รวมถึงคู่มือฉบับนี้ด้วย เนื่องจากโครงสร้างของ payload เป็นส่วนหนึ่งของ preview API ที่มีการเปลี่ยนแปลงบ่อยที่สุด
จุดขยายจุดที่สองคือ waterfall โดยเหตุการณ์ agent/pre-step, agent/request, agent/request-error, llm/stream และ tools/* จัดเป็น waterfall ซึ่ง listener ของ waterfall จะมี signature ที่แตกต่างออกไป โดยจะรับ callback เป็น next และ chain จะทำงานต่อเมื่อมีการเรียก callback นี้เท่านั้น
ctx.on('agent/request', async (payload, next) => {
const startedAt = Date.now()
const downstream = await next()
console.log('[my-plugin] model request took', Date.now() - startedAt, 'ms')
return downstream
})หากคุณลืมเรียก await next() เท่ากับว่าคุณไม่ได้เพิ่ม hook เข้าไป แต่คุณกำลังแทนที่การเรียก model ด้วยความว่างเปล่า ซึ่งจะทำให้ agent หยุดทำงานทันที เนื่องจากพฤติกรรม short circuit ถูกออกแบบมาสำหรับ gateway plugin ที่ต้องการปฏิเสธคำขอโดยเจตนา ความแตกต่างเพียงจุดเดียวนี้เป็นสาเหตุของความสับสนส่วนใหญ่ในปลั๊กอินแรก ให้เขียนคำสั่งเรียก next() ก่อนที่จะเขียนโค้ดส่วนอื่นล้อมรอบ
agent/request จะครอบการเรียก model โดยตรง payload ของมันจะบรรจุข้อมูล agent ที่ทำการเรียก, หมายเลข turn ที่เปิดอยู่, step ที่คำขอนั้นสังกัดอยู่ และสัญญาณ abort ของ turn นั้น ซึ่งทำให้มันเป็นจุดเชื่อมต่อที่เหมาะสมสำหรับ request logger หรือ rate limiter ส่วน waterfall ของ tools/* จะมีรูปแบบเดียวกันในชั้นที่ลึกลงไป โดย tools/pre-execute ใช้สำหรับอนุญาต, ปฏิเสธ หรือขออนุมัติก่อนการส่งคำขอ (dispatch), tools/execute ใช้ครอบการส่งคำขอ, tools/post-execute สามารถแทนที่หรือบล็อกผลลัพธ์ที่ถูกทำให้เป็นมาตรฐาน (normalised result) ได้ และ tools/result ทำหน้าที่เพียงสังเกตการณ์ผลลัพธ์ที่เสร็จสมบูรณ์แล้วเท่านั้น
จัดทำเป็นชุดเพื่อให้ผู้อื่นติดตั้งได้
ชุด (bundle) คือ npm package ที่มีไฟล์ package.json ระบุฟิลด์ dsh.bundle ซึ่งชี้ไปยังไฟล์ patch ของคุณ การประกาศดังกล่าวคือความแตกต่างเพียงอย่างเดียวระหว่างไฟล์ที่เขียนขึ้นเองกับสิ่งที่สามารถติดตั้งได้
{
"name": "dsh-plugin-hello",
"version": "0.1.0",
"type": "module",
"main": "lib/index.js",
"files": ["lib", "cordis.patch.yml", "README.md", "LICENSE"],
"engines": { "node": "^22.19 || >=24", "dsh": ">=0.1.0-rc.6" },
"dsh": { "bundle": { "patch": "./cordis.patch.yml" } },
"keywords": ["dsh-plugin", "deepseek-harness"],
"scripts": { "build": "tsdown", "prepare": "pnpm run build" },
"exports": {
".": { "types": "./lib/index.d.ts", "default": "./lib/index.js" },
"./cordis.patch.yml": "./cordis.patch.yml",
"./package.json": "./package.json"
}
}ไฟล์ cordis.patch.yml ที่วางอยู่ข้างกันนั้นมีเนื้อหาสั้นๆ
- insert:
- id: dsh-plugin-hello
name: dsh-plugin-helloบรรทัด name คือชื่อของ package ดังนั้นสตริงทั้งสองต้องตรงกัน บรรทัด id คือสิ่งที่เลเยอร์ถัดไปจะอ้างถึงเมื่อผู้ใช้ทำการ override การตั้งค่าของคุณ ดังนั้นควรเลือกชื่อที่คงที่และห้ามนำกลับมาใช้ซ้ำสำหรับปลั๊กอินอื่นโดยเด็ดขาด
files ต้องระบุ cordis.patch.yml ไว้ หากละเว้นส่วนนี้ tarball ที่เผยแพร่จะมี dsh.bundle.patch ที่ชี้ไปยังไฟล์ที่ไม่ได้ถูกรวมเข้าไปในแพ็กเกจ ส่งผลให้การติดตั้งเสร็จสมบูรณ์แต่ไม่มีการเปลี่ยนแปลงใดๆ เกิดขึ้นใน tree
ติดตั้งลงใน profile จากไดเรกทอรีที่มีโฟลเดอร์ปลั๊กอินของคุณ
dsh plugin --profile demo add ./dsh-plugin-hello
dsh --profile demo --dump-config
dsh --profile demodsh plugin --profile <name> จะส่งต่ออาร์กิวเมนต์ที่เหลือไปยัง pnpm ภายในไดเรกทอรี profile นั้น ดังนั้น add และ remove จึงทำงานในลักษณะเดียวกับ pnpm การถอนการติดตั้งให้ใช้ dsh plugin --profile demo remove dsh-plugin-hello สำหรับ profile web และ headless จะถูกสร้างขึ้นเองจากเทมเพลตที่มาพร้อมกับโปรแกรมในการใช้งานครั้งแรก ส่วนชื่อ profile อื่นๆ จะต้องสร้างผ่าน dsh plugin
เหตุใดแถวของคุณจึงหายไปจากโครงสร้างที่ประกอบขึ้น
กระบวนการประกอบจะเริ่มต้นจากรายการว่างเปล่าและวางเลเยอร์ซ้อนกันตามลำดับที่กำหนด โดยเริ่มจาก bundle แต่ละรายการที่ระบุไว้ใน dsh.profile.bundles ของโปรไฟล์ตามลำดับที่ปรากฏ จากนั้นตามด้วย cordis.patch.yml ของโปรไฟล์เอง ต่อด้วย $DSH_HOME/cordis.patch.yml และปิดท้ายด้วย --patch overlay ใดๆ จากบรรทัดคำสั่ง เลเยอร์ที่อยู่ลำดับหลังจะเข้ามาแทนที่แถวที่มี id เดียวกันจากเลเยอร์ก่อนหน้า
โปรไฟล์จะถูกเก็บไว้ภายใต้ $DSH_HOME/profiles/<name> โดยไดเรกทอรีของโปรไฟล์จะมีไฟล์ package.json ซึ่งบรรจุ manifest dsh.profile พร้อมรายการ bundles ที่เรียงลำดับไว้ รวมถึงไฟล์ patch ของผู้ใช้เอง ชื่อของ bundle จะถูกค้นหาจากที่ติดตั้ง dsh ก่อน และจาก node_modules ของโปรไฟล์เป็นลำดับถัดไป ซึ่งเป็นตำแหน่งที่ pnpm จัดเก็บปลั๊กอินที่อยู่นอกโครงสร้างหลัก
dsh --profile demo --dump-config จะแสดงโครงสร้างที่ประกอบเสร็จสมบูรณ์โดยไม่ต้องเริ่มการทำงานของระบบใดๆ และผลลัพธ์นั้นคือจุดแบ่งสำหรับการตรวจสอบปัญหา หาก id แถวของคุณไม่อยู่ในรายการ แสดงว่าปัญหาอยู่ที่การประกอบ เช่น ชื่อที่ไม่สามารถค้นหาได้ หรือไฟล์ patch ที่ไม่ได้ถูกแพ็ก หากแถวปรากฏอยู่แต่ไม่มีการทำงานใดๆ เกิดขึ้น แสดงว่าปัญหาอยู่ที่โค้ดของคุณ การตอบคำถามนี้ให้ได้ก่อนจะช่วยให้คุณข้ามขั้นตอนการคาดเดาไปได้เกือบทั้งหมด
จุดที่ข้อผิดพลาดในการโหลดปรากฏขึ้นจริง
ข้อผิดพลาดที่เกิดขึ้นภายใน apply จะแสดงผลอย่างชัดเจน โดยกระบวนการจะหยุดทำงานพร้อมกับข้อยกเว้นดังกล่าว และคุณจะได้รับ stack trace ที่ชี้ไปยังบรรทัดที่คุณเขียน
ความล้มเหลวในการแก้ไขชื่อโมดูล (Resolution failures) จะเงียบกว่า ตัวโหลดจะรายงานโมดูลที่ไม่สามารถแก้ไขได้ผ่านทาง Cordis logger แทนที่จะทำให้โปรแกรมหยุดทำงาน และบทเรียนจากต้นทางได้เตือนไว้ว่าข้อความเหล่านี้อาจดูเหมือนหายไปในช่วงเริ่มต้น เนื่องจากถูกส่งออกมาก่อนที่ console exporters จะถูกเชื่อมต่อ ดังนั้นการพิมพ์ path ผิดจึงดูเหมือนกับปลั๊กอินที่โหลดสำเร็จแต่ไม่ทำงาน ซึ่งเป็นเหตุผลว่าทำไมการตรวจสอบด้วย --dump-config ด้านบนจึงคุ้มค่าที่จะทำก่อนที่คุณจะเริ่มอ่านโค้ดใดๆ
ให้คง console.log ไว้เป็นคำสั่งแรกใน apply ในระหว่างที่คุณพัฒนา การไม่มีคำสั่งนี้จะทำให้คุณไม่ทราบว่าปัญหาของคุณอยู่ในส่วนใด และการลบทิ้งในภายหลังก็ไม่มีผลเสียใดๆ บนเซิร์ฟเวอร์ ให้รัน harness ใน foreground ในระหว่างที่คุณทดสอบ แทนที่จะรันภายใต้ service manager เพื่อให้ผลลัพธ์จากตัวโหลดแสดงผลบนเทอร์มินัลของคุณโดยตรง แทนที่จะต้องไปอ่านจาก journal ในภายหลัง
การทำซ้ำโดยไม่ต้องรีสตาร์ททั้งระบบ
คำตอบตามตรงสำหรับฝั่งโฮสต์ในปัจจุบันคือคุณต้องรีสตาร์ท บันเดิลของเว็บแอปพลิเคชันถูกจัดส่งมาโดยปิดการทำงานของฟีเจอร์ hot module reload แบบแชร์ไว้ และในไฟล์มีหมายเหตุระบุว่าจะเปิดใช้งานอีกครั้งเมื่อวงจรการรีโหลดผ่านการทดสอบแล้ว ส่วนเชนการรีโหลดฝั่งไคลเอนต์นั้นถูกเมาท์ไว้ตลอดเวลาแต่จะอยู่ในสถานะว่างจนกว่าตัวเฝ้าติดตามการสร้างใหม่ (rebuild watcher) จะเขียนบันเดิลฝั่งไคลเอนต์ขึ้นมาใหม่ ดังนั้นมันจึงไม่มีผลกับฝั่ง Node ของคุณเช่นกัน
ทำให้การรีสตาร์ทมีต้นทุนต่ำแทนที่จะพยายามไล่ตามการรีโหลดที่ยังไม่มีอยู่จริง เก็บปลั๊กอินไว้ในไฟล์เดียว โหลดด้วย --patch แทนการติดตั้งลงในโปรไฟล์ เพื่อไม่ให้มีขั้นตอนการ build หรือขั้นตอน pnpm คั่นกลางระหว่างการแก้ไขและการรัน ลงทะเบียนทุกอย่างผ่าน ctx เพื่อไม่ให้การรีสตาร์ททิ้งเครื่องมือที่ซ้ำซ้อนหรือ listener ที่ค้างอยู่ไว้เบื้องหลัง ห่อหุ้มทุกอย่างที่คุณจัดสรรขึ้นเองใน ctx.effect() พร้อมตัวกำจัด (disposer) ที่ใช้งานได้จริง เพราะอาการปกติของการขาดตัวกำจัดคือการรันครั้งที่สองจะล้มเหลวเนื่องจากพอร์ตยังถูกใช้งานโดยการรันครั้งแรกอยู่
หากคุณพัฒนาโดยใช้ harness ที่รันบนเซิร์ฟเวอร์แทนที่จะเป็นแล็ปท็อปของคุณ หลักการข้างต้นทั้งหมดจะยังคงเหมือนเดิม แต่การผูก Web UI นั้นมีความสำคัญ การผูก loopback บนพอร์ต 3080 อธิบายว่าเหตุใดหน้าเว็บจึงไม่เปิดขึ้นมาเองและควรจัดการอย่างไร
ส่วนของเบราว์เซอร์และระดับความน่าเชื่อถือ
ให้เพิ่มส่วนนี้เฉพาะเมื่อปลั๊กอินของคุณจำเป็นต้องมีอินเทอร์เฟซของตนเอง โดยจะประกาศไว้ในฟิลด์ dsh เดียวกันกับ bundle
{
"dsh": {
"client": {
"platform": "web",
"inject": [],
"external": [],
"immediately": false
}
},
"exports": {
".": "./src/index.ts",
"./client": "./src/client/apply.ts",
"./package.json": "./package.json"
}
}จำเป็นต้องมี "platform": "web" และตัวสแกนจะแจ้งข้อผิดพลาดหากแพ็กเกจไม่มีการ export ./client ดังนั้น export map จึงเป็นส่วนหนึ่งของ manifest ไม่ใช่เพียงความสะดวกในการใช้งาน client entry จะได้รับ Cordis Context ที่ขยายด้วยประเภท client runtime และการลงทะเบียนทั้งหมดจะเกิดขึ้นภายใน apply ผ่านทาง ctx.slots.register โดยไม่อนุญาตให้มี side effects ในระดับโมดูลในส่วนนี้
import type { Context } from 'cordis'
import type { DshClientContext } from '@deepseek-ai/dsh-client-runtime'
export async function apply(ctx: Context & DshClientContext) {
ctx.slots.register({ name: 'domain.entry.slot' }, MyComponent)
}มีรายละเอียดสองประการที่ควรทราบก่อนเริ่มต้น inject ใน client manifest เป็นเพียงเอกสารประกอบไม่ใช่การจัดตารางเวลา โดยจะบันทึก dependency edges ในระดับแพ็กเกจและไม่ได้ควบคุมลำดับการทำงาน ส่วน external คือจุดที่คุณประกาศ module requests ที่อยู่นอกเหนือจาก baseline เพื่อให้ถูกเตรียมพร้อมก่อนที่ปลั๊กอินของคุณจะเรียกใช้งาน นี่เป็นส่วนที่มีการเปลี่ยนแปลงรวดเร็วที่สุดของรุ่น preview ดังนั้นโปรดอ่าน packages/client/AGENTS.md ในคลังเก็บโค้ดของ harness ในวันที่คุณเขียนโค้ดจริง ไม่ใช่ในวันที่คุณอ่านคู่มือนี้
การเผยแพร่ปลั๊กอินและระบุสิ่งที่ปลั๊กอินของคุณเข้าถึง
การเพิ่มหัวข้อ dsh-plugin ลงในที่เก็บ GitHub จะทำให้ปลั๊กอินของคุณปรากฏในรายการที่ผู้คนใช้ค้นหาปลั๊กอิน นี่คือการสร้างความเชื่อมั่นต่อผู้ใช้งานแปลกหน้า ซึ่งมาพร้อมกับภาระหน้าที่ ภาระหน้าที่เหล่านี้เป็นสิ่งที่สะท้อนกลับมาจากสิ่งที่เราแนะนำให้ผู้อ่านตรวจสอบใน คู่มือการตรวจสอบปลั๊กอิน dsh ก่อนติดตั้ง ดังนั้นการเขียนรายละเอียดให้สอดคล้องกับรายการตรวจสอบดังกล่าวจึงเป็นวิธีที่ง่ายที่สุดในการผ่านการตรวจสอบ
- ตรึงเวอร์ชันของ dependency ของคุณ การใช้ช่วงเวอร์ชันแบบ caret กับ transitive dependency อาจทำให้แพ็กเกจที่เคยปลอดภัยในสัปดาห์ก่อนรันโค้ดที่ต่างออกไปในสัปดาห์นี้ ซึ่งเป็นกลไกเดียวกับที่อยู่เบื้องหลัง การโจมตีห่วงโซ่อุปทาน npm บนเซิร์ฟเวอร์
- ระบุสิ่งที่ปลั๊กอินเข้าถึงในไฟล์ manifest รายการ
injectของคุณคือสรุปที่ซื่อสัตย์และเครื่องอ่านได้ ซึ่งระบุว่าคุณใช้บริการใดของ harness บ้าง ผู้ตรวจสอบจะอ่านรายการนี้ในเวลาไม่กี่วินาทีและใช้เป็นข้อมูลในการตัดสินใจ - ห้ามมีการเรียกใช้เครือข่ายโดยไม่แจ้งให้ทราบ หากเครื่องมือมีการเรียกใช้ API ให้ระบุชื่อโฮสต์ไว้ใน README และทำให้ endpoint สามารถกำหนดค่าได้ ปลั๊กอินที่ติดต่อกับเซิร์ฟเวอร์โดยไม่เคยแจ้งไว้จะถูกถอดออกจากรายการโดยผู้ตรวจสอบความปลอดภัย
- จำกัดไฟล์ใน
filesให้รัดกุม การเผยแพร่ทั้งโฟลเดอร์ทำงานอาจทำให้ไฟล์ข้อมูลรับรองที่หลงเหลืออยู่หลุดเข้าไปใน registry ได้ - จัดเตรียมสคริปต์
prepareสำหรับผู้ติดตั้งผ่าน git ซึ่งสามารถ build ได้โดยไม่ต้องพึ่งพาเครื่องมือสำหรับนักพัฒนา (dev-only) และแจ้งให้ผู้ใช้ทราบใน README ว่าพวกเขาต้องอนุญาต (allowlist) การ build นั้นในpnpm-workspace.yamlของโปรไฟล์ตนเอง - ระบุวันที่ใน README ให้ตรงกับ release candidate ที่คุณใช้ build และทดสอบ ผู้อ่านที่ใช้ preview API จำเป็นต้องทราบว่าคุณใช้เวอร์ชันใดในการพัฒนา
หากต้องการดูว่าปลั๊กอินที่เสร็จสมบูรณ์มีลักษณะอย่างไรจากมุมมองภายนอก ให้อ่าน ปลั๊กอิน dsh ที่น่าติดตั้ง และสังเกตว่า README แต่ละฉบับแจ้งข้อมูลอะไรบ้างก่อนที่คุณจะติดตั้ง หากคุณเคยเขียนส่วนขยายสำหรับเอเจนต์อื่น วิธีการสร้างปลั๊กอินสำหรับ Claude Code จะเป็นข้อมูลเปรียบเทียบที่มีประโยชน์ Harness มอบ object graph แบบสดและระบบการลงทะเบียนที่ย้อนกลับได้ให้แก่คุณ ซึ่งถือเป็นอำนาจที่มากกว่าแค่รายการไฟล์ และมาพร้อมกับความรับผิดชอบที่มากขึ้นด้วยเช่นกัน
FAQ
ฉันจำเป็นต้องเผยแพร่ไปยัง npm เพื่อเขียนปลั๊กอินสำหรับ dsh หรือไม่?
ไม่จำเป็น เส้นทางไฟล์ (filesystem path) ใน cordis.yml overlay ที่โหลดด้วย dsh web --patch ./scratch-plugin/cordis.yml ก็เพียงพอสำหรับการรันโค้ดของคุณภายใน harness แล้ว เส้นทางดังกล่าวต้องเป็น absolute path การทำแพ็กเกจมีความสำคัญก็ต่อเมื่อผู้อื่นต้องการติดตั้งปลั๊กอินของคุณเท่านั้น และแม้ในกรณีนั้น คุณยังสามารถติดตั้งโฟลเดอร์ในเครื่องด้วย dsh plugin --profile demo add ./my-plugin เพื่อทดสอบรูปแบบที่แพ็กเกจแล้วโดยไม่ต้องยุ่งกับ registry
ทำไมปลั๊กอินของฉันโหลดได้แต่เครื่องมือไม่ปรากฏขึ้น?
ให้รัน dsh --profile demo --dump-config ก่อน หาก row id ของคุณไม่ปรากฏในผลลัพธ์ดังกล่าว แสดงว่าปลั๊กอินไม่ได้ถูก mount และสาเหตุเกิดจากการจัดองค์ประกอบ (composition) ไม่ใช่ที่ตัวโค้ด หากพบ row ดังกล่าว ให้ตรวจสอบ export const inject = ['tools'] รายการในไฟล์คอนฟิกูเรชันของ Cordis จะเริ่มทำงานพร้อมกัน ดังนั้นลำดับของไฟล์จึงไม่ได้เป็นตัวกำหนดลำดับการโหลด หากไม่มีการประกาศดังกล่าว Cordis จะไม่รอ registry ของเครื่องมือ และ apply ของคุณอาจรันในจังหวะที่ ctx.tools ยังไม่พร้อมให้ลงทะเบียน
cordis.yml และ cordis.patch.yml แตกต่างกันอย่างไร?
cordis.yml คือรายการทั้งหมดของ entry ส่วน cordis.patch.yml คือเลเยอร์ที่นำไปใช้ทับรายการเดิม โดยกำหนดเป้าหมายที่ row ตาม id เพื่อแทรกรายการใหม่หรือแทนที่คอนฟิกูเรชันเดิม bundle จะชี้ไปยังไฟล์ patch ของตนเองผ่าน dsh.bundle.patch ใน package.json เลเยอร์จะถูกนำไปใช้ตามลำดับที่กำหนด: ทุก bundle ในลำดับที่ระบุไว้ของ profile ตามด้วยไฟล์ patch ของ profile, จากนั้นเป็น $DSH_HOME/cordis.patch.yml และปิดท้ายด้วย --patch overlay ใดๆ เลเยอร์ที่อยู่ลำดับหลังสุดจะมีผลเหนือกว่า
ฉันสามารถ hot reload ปลั๊กอิน dsh ในขณะที่ agent กำลังทำงานอยู่ได้หรือไม่?
ยังไม่ได้สำหรับส่วน host ใน web profile ณ เวอร์ชัน 0.1.0-rc.7 bundle ดังกล่าวได้ปิดการใช้งาน row สำหรับ hot module reload ไว้ โดยมีหมายเหตุในไฟล์ระบุว่าจะนำกลับมาใช้ใหม่เมื่อวงจรชีวิต (lifecycle) ของการ reload ผ่านการทดสอบแล้ว ให้คุณออกแบบโดยเน้นการ restart ที่รวดเร็วแทน: ใช้ไฟล์เดียวที่โหลดผ่าน --patch โดยไม่มีขั้นตอน build และทำการลงทะเบียนทุกอย่างผ่าน ctx เพื่อไม่ให้มีข้อมูลตกค้างจากการรันครั้งก่อนหน้า ใช้ ctx.effect() พร้อมกับ disposer สำหรับทรัพยากรที่ Cordis ไม่สามารถล้างข้อมูลได้ด้วยตัวเอง