SSD Nodes Learn Hosting plans →
คู่มือ Matt Connorโดย Matt Connor

วิธีเขียน 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 ของเครื่องมือ, ลูปของ agent, ที่เก็บ session และเว็บเซิร์ฟเวอร์ ทั้งหมดล้วนเป็นส่วนหนึ่งในโครงสร้างปลั๊กอินเดียวกันกับที่คุณกำลังจะเพิ่มแพ็กเกจของคุณเข้าไป

Cordis เป็นเฟรมเวิร์กสำหรับการประกอบส่วนประกอบทั่วไป ซึ่งถูกสร้างขึ้นอย่างอิสระและใช้เป็นฐานของเฟรมเวิร์กแชทบอท Koishi มานานหลายปี Cordis จัดการเรื่องการโหลดและการยกเลิกการโหลด รวมถึงแก้ไขปัญหาการพึ่งพากันระหว่างปลั๊กอิน โดยตัวมันเองไม่มีความรู้เรื่อง agent แต่อย่างใด ทุกสิ่งที่เกี่ยวข้องกับ agent มาจากแพ็กเกจ harness ที่วางซ้อนอยู่ด้านบน ซึ่งเป็นเหตุผลว่าทำไมโครงสร้างปลั๊กอินด้านล่างจึงดูเรียบง่าย สิ่งที่คุณได้รับส่วนใหญ่เป็นการสืบทอดมาจากระบบหลัก

ปลั๊กอินประกอบด้วยสองส่วน ส่วนโฮสต์ทำงานใน Node ทำหน้าที่ลงทะเบียนเครื่องมือและ event listener รวมถึงสามารถให้บริการของตนเองได้ ส่วนเบราว์เซอร์ทำงานภายใน Web UI และลงทะเบียนช่องสำหรับอินเทอร์เฟซ ปลั๊กอินตัวแรกมักจะเป็นแบบโฮสต์เท่านั้น ดังนั้นให้ถือว่าส่วนเบราว์เซอร์เป็นส่วนเสริมจนกว่าคุณจะจำเป็นต้องใช้งาน

คู่มือนี้เขียนขึ้นโดยอ้างอิง @deepseek-ai/dsh เวอร์ชัน 0.1.0-rc.7 ซึ่งเป็นแท็ก latest บน npm เมื่อวันที่ 19 สิงหาคม 2026 dsh ยังอยู่ในสถานะ developer preview และ README ของโครงการระบุว่าจะมีการเปลี่ยนแปลงที่ส่งผลต่อความเข้ากันได้ (breaking changes) ชื่อคีย์ทั้งหมดด้านล่างนี้ถูกอ่านจากเอกสารต้นทางและ repository ณ วันที่ดังกล่าว เมื่อคู่มือนี้ถูกตรวจสอบซ้ำในวันที่ 12 ตุลาคม 2026 แท็ก latest บน npm ได้เปลี่ยนเป็น 0.2.0-rc.2 แล้ว และส่วนต่างๆ ด้านล่างได้ระบุจุดที่รุ่นดังกล่าวมีการทำงานที่แตกต่างออกไป โปรดตรวจสอบชื่อคีย์อีกครั้งก่อนที่จะนำไปใช้งานจริง เนื่องจาก 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) คือกระแสข้อมูลที่คงทนและถูกบันทึกไว้ ในเวอร์ชัน 0.1.0-rc.7 ชื่อของเหตุการณ์คือ session/event, turn/start, turn/end, step/start, step/end, user/message, assistant/message, assistant/chunk, tool/call และ tool/result สำหรับรุ่น 0.2.0-rc.2 จะไม่มี assistant/chunk อีกต่อไป การสตรีมสดถูกย้ายไปที่ agent/assistant-stream ซึ่งเฟรมข้อมูลจะคงอยู่ในกระบวนการทำงานและไม่ได้ถูกบันทึกไว้ และมีเหตุการณ์ใหม่ที่คงทนคือ assistant/attempt ซึ่งจะเก็บข้อมูลความพยายามที่ล้มเหลว การลองใหม่ และการยกเลิก คุณสามารถแนบ 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 จะมีรูปแบบการเรียกใช้งานที่ต่างออกไป โดยจะรับ callback ชื่อ next และห่วงโซ่การทำงานจะดำเนินต่อไปก็ต่อเมื่อมีการเรียก 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 ที่ต้องการปฏิเสธคำขอโดยเจตนา ความแตกต่างเพียงจุดเดียวนี้เป็นสาเหตุของความสับสนส่วนใหญ่สำหรับผู้พัฒนาปลั๊กอินมือใหม่ ให้เขียนคำสั่งเรียก next() ก่อนที่จะเขียนโค้ดส่วนอื่นล้อมรอบ

agent/request จะครอบการเรียก model โดยตรง payload ของมันจะบรรจุข้อมูล agent ที่ทำการเรียก, หมายเลข turn ที่เปิดอยู่, step ที่คำขอสังกัดอยู่ และสัญญาณ abort ของ turn นั้น ซึ่งทำให้มันเป็นจุดเชื่อมต่อที่เหมาะสมที่สุดสำหรับตัวบันทึกคำขอ (request logger) หรือตัวจำกัดอัตรา (rate limiter) ส่วน waterfall ของ tools/* จะมีรูปแบบเดียวกันในระดับที่ลึกลงไปอีกชั้นหนึ่ง tools/pre-execute ทำหน้าที่อนุญาต ปฏิเสธ หรือขออนุมัติก่อนการส่งคำขอ tools/execute จะครอบขั้นตอนการส่งคำขอ tools/post-execute สามารถแทนที่หรือบล็อกผลลัพธ์ที่ถูกปรับรูปแบบแล้วได้ ส่วน 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 คือชื่อของแพ็กเกจ ดังนั้นสตริงทั้งสองค่านี้ต้องตรงกัน บรรทัด id คือสิ่งที่เลเยอร์ถัดไปจะอ้างอิงถึงเมื่อผู้ใช้ทำการ override การตั้งค่าของคุณ ดังนั้นควรเลือกชื่อที่คงที่และห้ามนำกลับมาใช้ซ้ำสำหรับปลั๊กอินอื่นโดยเด็ดขาด

files ต้องระบุ cordis.patch.yml ไว้ หากละเว้นส่วนนี้ tarball ที่เผยแพร่ออกไปจะมี dsh.bundle.patch ที่ชี้ไปยังไฟล์ที่ไม่ได้ถูกรวมเข้าไปในแพ็กเกจ ส่งผลให้การติดตั้งแพ็กเกจเสร็จสมบูรณ์แต่ไม่มีผลใดๆ ต่อโครงสร้างต้นไม้ (tree)

ติดตั้งแพ็กเกจลงในโปรไฟล์จากไดเรกทอรีที่มีโฟลเดอร์ปลั๊กอินของคุณ

dsh plugin --profile demo add ./dsh-plugin-hello
dsh --profile demo --dump-config
dsh --profile demo

dsh plugin --profile <name> จะส่งต่ออาร์กิวเมนต์ที่เหลือไปยัง pnpm ภายในไดเรกทอรีโปรไฟล์นั้น ดังนั้น add และ remove จึงทำงานในลักษณะเดียวกับ pnpm การถอนการติดตั้งให้ใช้ dsh plugin --profile demo remove dsh-plugin-hello โปรไฟล์ที่มาพร้อมกับซอฟต์แวร์อย่าง web, headless, sdk, sdk-minimal และ acp จะสร้างตัวเองขึ้นจากเทมเพลตในการใช้งานครั้งแรก ในเวอร์ชัน 0.2.0-rc.2 ชื่ออื่นใดก็ตามจะถูกสร้างขึ้นด้วยวิธีใดวิธีหนึ่งในสองวิธีนี้ dsh plugin จะเริ่มการทำงานจากชุด bundle พื้นฐานเพียงอย่างเดียว dsh --profile <name> --from-default-profile <template> จะคัดลอกรายการ bundle จากเทมเพลตที่มาพร้อมกับซอฟต์แวร์ การเริ่มโปรไฟล์แบบกำหนดเองที่ยังไม่มีอยู่จริงจะล้มเหลวพร้อมคำแนะนำให้รัน 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 ในภายหลัง

การทำซ้ำโดยไม่ต้องรีสตาร์ททั้งระบบ

ในเวอร์ชัน 0.1.0-rc.7 คำตอบสำหรับฝั่งโฮสต์นั้นเรียบง่าย คือคุณต้องรีสตาร์ท บันเดิลของเว็บแอปพลิเคชันในเวอร์ชันนั้นถูกจัดส่งมาโดยปิดการทำงานของ hot module reload ที่ใช้ร่วมกันไว้ และในไฟล์มีหมายเหตุระบุว่าจะเปิดใช้งานอีกครั้งเมื่อทดสอบวงจรการโหลดซ้ำเสร็จสิ้น ในเวอร์ชัน 0.2.0-rc.2 การตั้งค่า override ดังกล่าวถูกนำออกไปแล้ว บันเดิลพื้นฐานจะ mount @deepseek-ai/dsh-hmr ในทุกโปรไฟล์ด้วย root: [] ภายใต้หมายเหตุที่ว่า "การตั้งค่าโปรไฟล์จะโหลดซ้ำโดยค่าเริ่มต้น ส่วน module root ต้องเลือกเปิดใช้งานเอง" การแก้ไขไฟล์โปรไฟล์และไฟล์ patch ของโฮมไดเรกทอรีที่ถูกต้องจะถูกนำไปใช้ใหม่โดยไม่ต้องรีสตาร์ท อย่างไรก็ตาม ซอร์สโค้ดของปลั๊กอินของคุณจะยังไม่ถูกเฝ้าติดตามจนกว่าคุณจะเพิ่มไดเรกทอรีของมันเข้าไปใน root ของแถวนั้น การเพิ่ม ลบ หรืออัปเดตบันเดิลยังคงจำเป็นต้องรีสตาร์ทอยู่ ส่วน chain การโหลดซ้ำฝั่งไคลเอ็นต์นั้นถูก mount ไว้เสมอแต่จะอยู่ในสถานะว่างจนกว่าตัวเฝ้าติดตามการ build ใหม่จะเขียนบันเดิลฝั่งไคลเอ็นต์ขึ้นมาใหม่ ดังนั้นมันจึงไม่มีผลกับฝั่ง Node ของคุณเช่นกัน

ทำให้การรีสตาร์ทมีต้นทุนต่ำแทนที่จะพึ่งพาการโหลดซ้ำที่คุณต้องเลือกเปิดใช้งานเอง เก็บปลั๊กอินไว้ในไฟล์เดียว โหลดด้วย --patch แทนการติดตั้งลงในโปรไฟล์ เพื่อไม่ให้มีขั้นตอนการ build หรือขั้นตอน pnpm คั่นกลางระหว่างการแก้ไขและการรัน ลงทะเบียนทุกอย่างผ่าน ctx เพื่อไม่ให้การรีสตาร์ททิ้งเครื่องมือที่ซ้ำซ้อนหรือ listener ที่ค้างไว้ ห่อหุ้มทุกอย่างที่คุณจัดสรรขึ้นเองด้วย ctx.effect() พร้อมตัวกำจัด (disposer) ที่ใช้งานได้จริง เพราะอาการปกติของการลืมใส่ตัวกำจัดคือการรันครั้งที่สองจะล้มเหลวเนื่องจากพอร์ตยังถูกการรันครั้งแรกถือครองอยู่

หากคุณพัฒนาโดยใช้ harness ที่รันบนเซิร์ฟเวอร์แทนที่จะเป็นแล็ปท็อปของคุณ สิ่งที่กล่าวมาข้างต้นจะไม่มีการเปลี่ยนแปลง แต่การเชื่อมต่อ Web UI นั้นมีความสำคัญ การเปิดใช้งานในเครื่องจะให้บริการ Web UI ที่ http://127.0.0.1:3080 และเปิดในเบราว์เซอร์เริ่มต้นของคุณ หรือข้ามการเปิดเบราว์เซอร์หากคุณส่งแฟล็ก --no-open หากเป็นการเปิดใช้งานผ่าน SSH ระบบจะแสดงเพียง URL ของโฮสต์เท่านั้น จึงไม่มีอะไรเปิดขึ้นบนเครื่องของคุณ การเชื่อมต่อ 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 จึงเป็นส่วนหนึ่งของ manifest ไม่ใช่เพียงความสะดวกในการใช้งาน ตัว client entry จะได้รับ Cordis Context ที่ขยายขอบเขตด้วยประเภท runtime ของไคลเอนต์ และการลงทะเบียนทุกอย่างจะเกิดขึ้นภายใน apply ผ่านทาง ctx.slots.register โดยไม่อนุญาตให้มี side effect ในระดับโมดูลในส่วนนี้

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 เป็นเพียงเอกสารประกอบไม่ใช่การจัดตารางเวลา โดยจะบันทึกขอบเขตการพึ่งพาระดับแพ็กเกจและไม่ได้ควบคุมลำดับการทำงาน ส่วน external ใช้ประกาศคำขอโมดูลที่อยู่นอกเหนือจาก baseline แต่บันทึกของไคลเอนต์ปัจจุบันระบุว่าไม่ใช่กลไกการพึ่งพาสำหรับปลั๊กอินฟีเจอร์ และสงวนไว้สำหรับโครงสร้างพื้นฐาน การขนส่ง และโค้ดที่สร้างขึ้นอัตโนมัติเท่านั้น ให้เว้นว่างไว้สำหรับปลั๊กอินฟีเจอร์ นี่เป็นส่วนที่มีการเปลี่ยนแปลงรวดเร็วที่สุดของเวอร์ชันพรีวิว ดังนั้นโปรดอ่าน packages/client/AGENTS.md ใน repository ของ 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 หรือไม่?

ไม่จำเป็น เส้นทางไฟล์ใน 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 จะไม่รอ tool 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 ในลำดับที่ระบุไว้ของโปรไฟล์ ตามด้วยไฟล์ patch ของโปรไฟล์ จากนั้นเป็น $DSH_HOME/cordis.patch.yml และปิดท้ายด้วย --patch overlay ใดๆ เลเยอร์ที่อยู่ลำดับหลังสุดจะมีผลเหนือกว่า

ฉันสามารถ hot reload ปลั๊กอิน dsh ขณะที่ agent กำลังทำงานอยู่ได้หรือไม่?

ไม่ได้ในเวอร์ชัน 0.1.0-rc.7 เนื่องจาก web bundle ในเวอร์ชันนั้นปิดการใช้งาน row สำหรับ hot module reload ไว้ ในเวอร์ชัน 0.2.0-rc.2 row ดังกล่าวจะถูก mount ไว้ในทุกโปรไฟล์ แต่โดยค่าเริ่มต้นจะโหลดเฉพาะคอนฟิกูเรชันของโปรไฟล์เท่านั้น ส่วน module root ต้องเลือกเปิดใช้งานผ่านรายการ root ดังนั้นโค้ดปลั๊กอินของคุณยังคงต้องรีสตาร์ทเว้นแต่คุณจะเพิ่มการตั้งค่าดังกล่าวเข้าไป ไม่ว่าจะกรณีใดก็ตาม ควรออกแบบให้รองรับการรีสตาร์ทที่รวดเร็ว: ใช้ไฟล์เดียว โหลดผ่าน --patch โดยไม่มีขั้นตอน build และทำการลงทะเบียนทุกอย่างผ่าน ctx เพื่อไม่ให้มีข้อมูลตกค้างจากการรันครั้งก่อนหน้า ให้ใช้ ctx.effect() ร่วมกับ disposer สำหรับทรัพยากรที่ Cordis ไม่สามารถจัดการล้างข้อมูลได้ด้วยตัวเอง