SSD Nodes Learn Hosting plans →
راهنماها Matt Connorتوسط Matt Connor

آموزش ساخت پلاگین اختصاصی برای DeepSeek Harness

با دنبال کردن این راهنما، از یک پوشه خالی شروع کنید و پلاگین dsh خود را بسازید. فیلدهای حیاتی package.json، نحوه تنظیم فایل patch و دو hook اصلی را یاد بگیرید.

پلاگین dsh دقیقاً چیست

یک پلاگین dsh یک بسته npm است که یک تابع apply را export می‌کند و یک فایل YAML کوچک به همراه دارد که به DeepSeek Harness می‌گوید آن را بارگذاری کند. هیچ SDK پلاگین مجزایی برای یادگیری اولیه وجود ندارد. dsh یک برنامه Cordis است و عبارت «همه چیز یک پلاگین است» کاملاً تحت‌اللفظی است: رجیستری ابزار، حلقه عامل (agent loop)، ذخیره‌ساز نشست (session store) و وب‌سرور، همگی ردیف‌هایی در همان درخت پلاگینی هستند که بسته شما به آن می‌پیوندد.

Cordis یک چارچوب ترکیب‌بندی عمومی است که به‌طور مستقل ساخته شده و سال‌ها به عنوان پایه چارچوب چت‌بات Koishi استفاده شده است. این چارچوب بارگذاری و تخلیه (unloading) را مدیریت کرده و وابستگی‌های بین پلاگین‌ها را حل می‌کند. Cordis هیچ دانشی درباره عامل‌ها (agents) ندارد. هر چیزی که ماهیت عامل‌گونه دارد، از بسته‌های harness که روی آن لایه‌بندی شده‌اند می‌آید؛ به همین دلیل است که ساختار پلاگین در ادامه بسیار کوچک به نظر می‌رسد. بیشتر آنچه دریافت می‌کنید، به ارث رسیده است.

یک پلاگین دو نیمه دارد. نیمه میزبان (host) در Node اجرا می‌شود، ابزارها و شنونده‌های رویداد را ثبت می‌کند و می‌تواند سرویس‌های خاص خود را ارائه دهد. نیمه مرورگر در داخل Web UI اجرا می‌شود و شکاف‌های رابط کاربری (interface slots) را ثبت می‌کند. اولین پلاگین تقریباً همیشه فقط شامل بخش میزبان است، بنابراین تا زمانی که به نیمه مرورگر نیاز پیدا نکرده‌اید، آن را اختیاری در نظر بگیرید.

این راهنما بر اساس نسخه @deepseek-ai/dsh 0.1.0-rc.7 نوشته شده است که تگ npm latest در تاریخ 19 آگوست 2026 است. dsh یک نسخه پیش‌نمایش توسعه‌دهنده است و فایل README آن اعلام می‌کند که تغییرات ناسازگار (breaking changes) در آینده وجود خواهد داشت. تمام نام کلیدهای زیر در آن تاریخ از مستندات بالادستی و مخزن کد خوانده شده‌اند. هنگامی که این راهنما در تاریخ 12 اکتبر 2026 بازبینی شد، تگ npm latest نسخه 0.2.0-rc.2 بود و بخش‌های زیر اشاره می‌کنند که آن نسخه در کجا رفتار متفاوتی دارد. پیش از آنکه به یک کلید وابسته شوید، نام آن را دوباره بررسی کنید، زیرا APIهای پیش‌نمایش، فیلدها را بین نسخه‌های release candidate تغییر نام می‌دهند. اگر harness هنوز در حال اجرا نیست، ابتدا آن را با DeepSeek Harness روی یک VPS و کلید API dsh و پیکربندی مدل راه‌اندازی کنید و سپس به اینجا بازگردید.

پیش از بسته‌بندی، یک فایل آزمایشی را بارگذاری کنید

بسته‌بندی در همان ابتدا، روشی کند برای یادگیری این فرآیند است. یک فایل تکی بارگذاری کنید، اطمینان حاصل کنید که runtime کد شما را فراخوانی می‌کند، و سپس آن را بسته‌بندی کنید.

یک پوشه خارج از مسیر checkout ابزار 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 ثبت کنید، هنگام dispose شدن پلاگین، به‌طور خودکار برای شما پاکسازی می‌شود.

در کنار آن، cordis.yml را بنویسید.

- insert:
    - id: hello
      name: '/absolute/path/to/scratch-plugin/hello.ts'

اکنون یک profile را با لایه‌گذاری آن فایل در بالاترین سطح اجرا کنید.

dsh web --patch ./scratch-plugin/cordis.yml

اگر dsh در PATH شما نیست، npx @deepseek-ai/dsh web --patch ./scratch-plugin/cordis.yml همان کار را انجام می‌دهد. مسیر npx ممکن است یک نسخه قدیمی‌تر (release candidate) کش‌شده را به جای نسخه‌ای که در این راهنما توصیف شده است به شما بدهد؛ بنابراین اگر harness یک فلگ مستند را نپذیرفت، پیش از آنکه به فایل خود شک کنید، اصلاحات مربوط به نصب و خطاهای نسخه dsh را بررسی کنید. شما باید [hello-plugin] plugin loaded را در ترمینالی که dsh را اجرا کرده است ببینید. اگر چیزی ظاهر نشد، آن ردیف resolve نشده است.

فیلد name یک نام بسته npm یا یک مسیر فایل‌سیستم می‌پذیرد و مستندات upstream بیان می‌کنند که مسیر باید مطلق (absolute) باشد. یک ./hello.ts نسبی، اولین چیزی است که باید هنگام عدم خروجی پلاگین آزمایشی بررسی کنید. مورد دوم، پسوند فایل است. حلقه مستندشده به صورت pnpm dsh web --patch ... از یک clone مخزن harness اجرا می‌شود، جایی که ورودی‌های TypeScript از طریق tsx بارگذاری می‌شوند. اگر dsh خود را از npm دریافت کرده‌اید، ردیف را به یک فایل JavaScript ساده اشاره دهید یا ابتدا فایل را build کنید.

--patch یک فلگ راه‌انداز (launcher flag) است و overlay آن در آخرین مرحله، پس از تمام bundleها و پس از patch پروفایل خودتان اعمال می‌شود. بنابراین یک overlay آزمایشی همیشه اولویت دارد، که دقیقاً همان چیزی است که هنگام توسعه و تکرار به آن نیاز دارید.

نوشتن کوچک‌ترین ابزاری که کار مفیدی انجام می‌دهد

یک خط لاگ ثابت می‌کند که پلاگین بارگذاری شده است. یک ابزار ثابت می‌کند که پلاگین بخشی از 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'] خطی است که افراد از قلم می‌اندازند. ورودی‌ها در پیکربندی Cordis به‌طور هم‌زمان شروع می‌شوند، بنابراین موقعیت یک ردیف در فایل، هیچ تضمینی برای ترتیب بارگذاری ایجاد نمی‌کند. ترتیب‌بندی از وابستگی‌های اعلام‌شده ناشی می‌شود. inject به Cordis می‌گوید تا زمانی که ctx.tools وجود نداشته باشد، منتظر بماند و سپس apply شما را فراخوانی کند؛ بدون این دستور، کد شما ممکن است در لحظه‌ای اجرا شود که registry برای ثبت در دسترس نیست.

باقی شیء، قراردادی است که مدل مشاهده می‌کند. parameters طرحوارهٔ آرگومان‌ها است و execute آرگومان‌هایی را دریافت می‌کند که قبلاً بر اساس آن طرحواره تجزیه شده‌اند. output.schema مقداری را که execute بازمی‌گرداند توصیف می‌کند، در حالی که render آن مقدار را به بلوک‌های محتوایی که مدل می‌خواند تبدیل می‌کند. جدا نگه‌داشتن این دو مورد باعث می‌شود رابط کاربری یک چیز را نمایش دهد در حالی که مدل چیز دیگری را می‌خواند.

پروفایل را شروع کنید و از دستیار بخواهید به کسی با نام سلام کند. پاسخ از طریق execute شما بازمی‌گردد. ثبت از طریق ctx قابل‌برگشت است، بنابراین دور انداختن (dispose) پلاگین، ابزار را برای شما لغو ثبت می‌کند. برای هر چیزی که 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 است. پخش زنده (Live streaming) به agent/assistant-stream منتقل شده است که فریم‌های آن در پردازش باقی می‌مانند و هرگز لاگ نمی‌شوند، و یک رویداد پایدار جدید به نام assistant/attempt تلاش‌های ناموفق، دوباره انجام‌شده و لغوشده را نگه می‌دارد. شما یک listener معمولی به آن متصل می‌کنید.

ctx.on('tool/call', (payload) => {
  console.log('[my-plugin] tool/call', JSON.stringify(payload))
})

payload را یک‌بار چاپ کنید و آن را بخوانید. نام فیلدهای payload را از هیچ راهنمایی، از جمله همین راهنما، کپی نکنید؛ زیرا ساختار payload بخشی از یک API پیش‌نمایش است که بیشترین تغییرات را دارد.

دومین نقطهٔ توسعه، آبشار (waterfall) است. رویدادهای agent/pre-step، agent/request، agent/request-error، llm/stream و tools/* آبشاری هستند و یک listener آبشاری امضای متفاوتی دارد. این listener یک callback به نام next دریافت می‌کند و زنجیره تنها در صورتی ادامه می‌یابد که آن را فراخوانی کنید.

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ای اضافه نکرده‌اید. شما فراخوانی مدل را با هیچ جایگزین کرده‌اید و عامل (agent) در همان‌جا متوقف می‌شود، زیرا اتصال کوتاه (short circuiting) رفتار طراحی‌شده برای یک افزونهٔ دروازه (gateway plugin) است که عمداً یک درخواست را رد می‌کند. همین یک تفاوت، عامل بیشترین سردرگمی در افزونه‌های اولیه است. فراخوانی next() را پیش از نوشتن هر چیز دیگری در اطراف آن بنویسید.

agent/request خودِ فراخوانی مدل را در بر می‌گیرد. payload آن شامل عاملی است که فراخوانی را انجام می‌دهد، شماره نوبت باز، مرحله‌ای که درخواست به آن تعلق دارد و سیگنال لغو (abort signal) آن نوبت؛ همین ویژگی‌ها آن را به نقطهٔ اتصال مناسبی برای یک لاگر درخواست یا محدودکننده نرخ (rate limiter) تبدیل می‌کند. آبشارهای tools/* در یک لایه پایین‌تر، ساختار مشابهی دارند. tools/pre-execute پیش از ارسال، اجازه می‌دهد، رد می‌کند یا درخواست تأیید می‌کند. tools/execute عملیات ارسال را در بر می‌گیرد. tools/post-execute می‌تواند نتیجهٔ نرمال‌شده را جایگزین یا مسدود کند. tools/result تنها نتیجهٔ نهایی و تثبیت‌شده را مشاهده می‌کند.

بسته‌بندی آن به عنوان یک bundle قابل نصب برای دیگران

یک bundle در واقع یک پکیج npm است که در package.json آن، یک فیلد dsh.bundle تعریف شده که به فایل patch شما اشاره می‌کند. همین تعریف، تمام تفاوت بین یک فایل اولیه (scratch file) و چیزی است که قابلیت نصب دارد.

{
  "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 ایجاد نمی‌کند.

آن را از دایرکتوری که حاوی پوشه پلاگین شماست، در یک profile نصب کنید.

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

دستور dsh plugin --profile <name> باقی آرگومان‌های خود را به pnpm در آن دایرکتوری profile ارسال می‌کند، بنابراین add و remove دقیقاً مشابه رفتار pnpm عمل می‌کنند. برای حذف نصب از dsh plugin --profile demo remove dsh-plugin-hello استفاده کنید. پروفایل‌های پیش‌فرض web، headless، sdk، sdk-minimal و acp در اولین استفاده از روی قالب‌ها (templates) ساخته می‌شوند. در نسخه 0.2.0-rc.2 هر نام دیگری به یکی از دو روش زیر ساخته می‌شود. dsh plugin آن را فقط از روی bundle پایه شروع می‌کند. dsh --profile <name> --from-default-profile <template> لیست bundleهای یک قالب پیش‌فرض را کپی می‌کند. بوت کردن یک پروفایل سفارشی که هنوز وجود ندارد، با خطا مواجه شده و راهنمایی برای اجرای dsh plugin نمایش می‌دهد.

چرا ردیف شما در درخت ترکیب‌شده وجود ندارد

فرآیند ترکیب (Composition) با یک لیست ورودی خالی شروع می‌شود و لایه‌ها را به ترتیب مشخصی روی هم قرار می‌دهد. هر بسته (bundle) که در dsh.profile.bundles پروفایل نام برده شده، به همان ترتیبی که ذکر شده است، اضافه می‌شود. سپس cordis.patch.yml خودِ پروفایل، بعد از آن $DSH_HOME/cordis.patch.yml و در نهایت هرگونه overlay از نوع --patch که از طریق خط فرمان اعمال شده باشد. لایه‌های بعدی، ردیف‌های قبلی را بر اساس id جایگزین می‌کنند.

پروفایل‌ها در مسیر $DSH_HOME/profiles/<name> قرار دارند. هر دایرکتوری پروفایل شامل یک package.json است که مانیفست dsh.profile را به همراه لیست مرتب‌شده bundles و همچنین فایل patch کاربر در خود جای داده است. نام بسته‌ها ابتدا از محل نصب dsh و سپس از node_modules پروفایل حل (resolve) می‌شوند؛ این همان محلی است که pnpm یک پلاگین خارج از درخت (out of tree) را در آن قرار می‌دهد.

دستور dsh --profile demo --dump-config درخت کاملاً ترکیب‌شده را بدون اجرای هیچ چیزی چاپ می‌کند و خروجی آن، مرز تفکیک برای عیب‌یابی است. اگر id ردیف شما در خروجی وجود ندارد، مشکل از ترکیب است: نامی که حل نمی‌شود یا فایل patchای که هرگز بسته‌بندی نشده است. اگر ردیف موجود است اما اتفاقی نمی‌افتد، مشکل از کد شماست. ابتدا به این پرسش پاسخ دهید تا از بخش بزرگی از حدس و گمان‌ها عبور کنید.

محل واقعی بروز خطاهای بارگذاری

خطایی که در داخل apply رخ می‌دهد، آشکار است. پردازش با همان استثنا متوقف می‌شود و شما یک stack trace دریافت می‌کنید که دقیقاً به خط کد شما اشاره دارد.

خطاهای مربوط به resolution (حل وابستگی‌ها) بی‌سروصدا هستند. لودر به جای کرش کردن، ماژولی را که قادر به حل آن نیست از طریق Cordis logger گزارش می‌کند. آموزش‌های بالادستی هشدار می‌دهند که این پیام‌ها ممکن است در زمان راه‌اندازی گم شوند، زیرا پیش از متصل شدن console exporterها صادر می‌شوند. بنابراین، یک غلط تایپی در مسیر (path) دقیقاً مشابه پلاگینی به نظر می‌رسد که بارگذاری شده اما کاری انجام نمی‌دهد؛ به همین دلیل است که اجرای بررسی --dump-config در بالا، پیش از خواندن هرگونه کد، ارزشمند است.

در حین توسعه، یک console.log به عنوان اولین دستور در apply نگه دارید. نبود آن به شما می‌گوید که با کدام نیمه از مشکل مواجه هستید و حذف آن در آینده هیچ هزینه‌ای ندارد. روی سرور، به جای اجرای harness تحت یک service manager، آن را در foreground اجرا کنید تا خروجی لودر به جای ژورنالی که باید برای خواندنش به سراغ آن بروید، مستقیماً به ترمینال شما برسد.

تکرار بدون راه‌اندازی مجدد کل سیستم

در نسخه 0.1.0-rc.7، پاسخ برای بخش host ساده بود: باید سیستم را restart می‌کردید. باندل اپلیکیشن وب در آن نسخه، قابلیت hot module reload را غیرفعال کرده بود و فایلی حاوی یادداشتی بود که پس از تست چرخه حیات reload، دوباره فعال خواهد شد. در نسخه 0.2.0-rc.2، آن override حذف شده است. باندل پایه، @deepseek-ai/dsh-hmr را در تمام پروفایل‌ها با root: [] mount می‌کند؛ با این توضیح که «پیکربندی پروفایل به‌صورت پیش‌فرض reload می‌شود؛ ریشه‌های ماژول انتخابی هستند.» ویرایش‌های معتبر در فایل‌های profile و home patch اکنون بدون نیاز به restart اعمال می‌شوند. سورس‌کد پلاگین شما همچنان مانیتور نمی‌شود، مگر اینکه دایرکتوری آن را به root در آن ردیف اضافه کنید. افزودن، حذف یا به‌روزرسانی یک باندل همچنان نیازمند restart است. زنجیره reload سمت کلاینت همیشه mount است اما تا زمانی که watcher بازسازی، باندل‌های کلاینت را بازنویسی نکند، غیرفعال می‌ماند؛ بنابراین برای بخش Node شما نیز کاری انجام نمی‌دهد.

به‌جای تکیه بر reload که باید آن را فعال کنید، restart را کم‌هزینه کنید. پلاگین را در یک فایل نگه دارید. آن را با --patch بارگذاری کنید، نه اینکه در یک پروفایل نصب کنید؛ به این ترتیب هیچ مرحله build یا pnpm بین ویرایش و اجرا قرار نمی‌گیرد. همه چیز را از طریق ctx ثبت کنید تا restart باعث باقی ماندن ابزار تکراری یا listener قدیمی نشود. هر چیزی را که خودتان تخصیص می‌دهید در ctx.effect() با یک disposer واقعی قرار دهید، زیرا نشانه معمول نبود disposer این است که اجرای دوم به دلیل اشغال بودن پورت توسط اجرای اول، با شکست مواجه می‌شود.

اگر توسعه را روی یک harness در سرور انجام می‌دهید و نه روی لپ‌تاپ خود، هیچ‌کدام از موارد بالا تغییر نمی‌کند، اما binding رابط کاربری وب اهمیت پیدا می‌کند. اجرای محلی، رابط کاربری وب را روی 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 است و نه یک قابلیت جانبی. ورودی کلاینت، Cordis Context را که با نوع 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 در manifest کلاینت، جنبه مستندسازی دارد و نه زمان‌بندی: این فیلد لبه‌های وابستگی در سطح پکیج را ثبت می‌کند و ترتیب فعال‌سازی را کنترل نمی‌کند. external درخواست‌های ماژول خارج از baseline را اعلام می‌کند، اما یادداشت‌های فعلی کلاینت بیان می‌کنند که این یک مکانیزم وابستگی برای پلاگین‌های ویژگی (feature plugin) نیست و برای زیرساخت، انتقال و کدهای اسمبلی تولیدشده رزرو شده است. در یک پلاگین ویژگی، آن را خالی بگذارید. این بخش از نسخه پیش‌نمایش سریع‌ترین تغییرات را دارد، بنابراین packages/client/AGENTS.md را در مخزن harness در همان روزی که کد را می‌نویسید مطالعه کنید، نه در روزی که راهنمای آن را می‌خوانید.

انتشار و مشخص کردن دسترسی‌های پلاگین

افزودن موضوع dsh-plugin به یک مخزن GitHub، آن را در فهرستی قرار می‌دهد که کاربران هنگام جستجو برای پلاگین‌ها مشاهده می‌کنند. این کار به معنای جلب اعتماد کاربران ناشناس است و تعهداتی را به همراه دارد. این تعهدات دقیقاً بازتابی از مواردی است که در راهنمای بررسی پلاگین‌های dsh پیش از نصب به کاربران توصیه شده است؛ بنابراین، نوشتن مستندات بر اساس این چک‌لیست، ساده‌ترین راه برای تأیید شدن است.

  • وابستگی‌های خود را Pin کنید. استفاده از caret range برای وابستگی‌های غیرمستقیم باعث می‌شود بسته‌ای که هفته گذشته ایمن بود، این هفته کد متفاوتی را اجرا کند؛ این دقیقاً همان مکانیزمی است که پشت حملات زنجیره تأمین npm روی سرور قرار دارد.
  • در فایل manifest مشخص کنید که به چه بخش‌هایی دسترسی دارید. لیست inject شما یک خلاصه صادقانه و قابل‌خواندن توسط ماشین از سرویس‌های harness است که استفاده می‌کنید. یک بازبین در چند ثانیه آن را می‌خواند و بر اساس آن قضاوت می‌کند.
  • هیچ تماس شبکه مخفیانه‌ای برقرار نکنید. اگر ابزاری با یک API تماس می‌گیرد، نام میزبان (host) را در README ذکر کنید و endpoint را قابل‌تنظیم قرار دهید. پلاگینی که با سروری تماس می‌گیرد که هرگز نامی از آن نبرده است، توسط افرادی که این موارد را ممیزی می‌کنند، از لیست حذف خواهد شد.
  • فایل files را محدود نگه دارید. انتشار کل پوشه کاری باعث می‌شود فایل‌های حاوی اعتبارنامه‌های حساس به اشتباه وارد رجیستری شوند.
  • برای نصب‌کننده‌های git، یک اسکریپت prepare ارائه دهید که بدون پیش‌فرض‌های مخصوص محیط توسعه (dev-only) ساخته شود و در README به آن‌ها اطلاع دهید که باید این build را در pnpm-workspace.yaml پروفایل خود در لیست سفید (allowlist) قرار دهند.
  • در README تاریخ انتشار را بر اساس release candidate که با آن تست کرده‌اید، درج کنید. خوانندگان یک API پیش‌نمایش باید بدانند شما با کدام نسخه کار کرده‌اید.

برای مشاهده ظاهر یک پلاگین تکمیل‌شده از دید کاربر، پلاگین‌های dsh که ارزش نصب دارند را بخوانید و توجه کنید که هر README پیش از نصب چه اطلاعاتی به شما می‌دهد. اگر برای عامل دیگری افزونه نوشته‌اید، نحوه ساختار پلاگین‌های Claude Code مقایسه مفیدی است. این harness یک گراف شیء زنده و قابلیت ثبت‌نام برگشت‌پذیر (reversible registration) در اختیار شما می‌گذارد که قدرت بیشتری نسبت به یک لیست ساده از فایل‌ها دارد و به همان نسبت مسئولیت بیشتری نیز به همراه می‌آورد.

FAQ

آیا برای نوشتن یک پلاگین dsh حتماً باید آن را در npm منتشر کنم؟

خیر. یک مسیر فایل‌سیستم در یک overlay با cordis.yml که با dsh web --patch ./scratch-plugin/cordis.yml بارگذاری شده باشد، برای اجرای کد شما در محیط harness کافی است. این مسیر باید مطلق (absolute) باشد. بسته‌بندی (Packaging) تنها زمانی اهمیت دارد که شخص دیگری بخواهد پلاگین را نصب کند؛ حتی در آن حالت هم می‌توانید برای تست فرم بسته‌بندی‌شده، یک پوشه محلی را با dsh plugin --profile demo add ./my-plugin نصب کنید بدون اینکه نیازی به تعامل با رجیستری باشد.

چرا پلاگین من بارگذاری می‌شود اما ابزار ظاهر نمی‌شود؟

ابتدا dsh --profile demo --dump-config را اجرا کنید. اگر شناسه ردیف (row id) شما در خروجی وجود ندارد، پلاگین هرگز mount نشده و علت آن مربوط به composition است، نه کد. اگر ردیف موجود است، export const inject = ['tools'] را بررسی کنید. ورودی‌ها در پیکربندی Cordis به‌صورت همزمان (concurrently) شروع می‌شوند، بنابراین ترتیب فایل‌ها تعیین‌کننده ترتیب بارگذاری نیست. بدون آن اعلان، Cordis منتظر رجیستری ابزار نمی‌ماند و ممکن است apply شما در لحظه‌ای اجرا شود که ctx.tools هنوز برای ثبت‌نام در دسترس نیست.

تفاوت بین cordis.yml و cordis.patch.yml چیست؟

cordis.yml یک لیست کامل از ورودی‌ها است. cordis.patch.yml لایه‌ای است که روی یک لیست اعمال می‌شود و ردیف‌ها را بر اساس شناسه هدف قرار می‌دهد تا ورودی‌های جدید اضافه یا پیکربندی موجود را جایگزین کند. یک bundle از طریق dsh.bundle.patch در package.json به فایل patch خود اشاره می‌کند. لایه‌ها به ترتیب ثابتی اعمال می‌شوند: ابتدا هر bundle به ترتیبی که در پروفایل لیست شده، سپس فایل patch پروفایل، بعد $DSH_HOME/cordis.patch.yml و در نهایت هر overlay با --patch. لایه‌های بعدی اولویت دارند.

آیا می‌توانم پلاگین dsh را در حین اجرای agent به‌صورت hot reload بارگذاری کنم؟

در نسخه 0.1.0-rc.7 خیر، زیرا bundle وب آن، ردیف shared hot module reload را غیرفعال کرده بود. در نسخه 0.2.0-rc.2 آن ردیف در تمام پروفایل‌ها mount می‌شود اما به‌صورت پیش‌فرض فقط پیکربندی پروفایل را reload می‌کند. ریشه‌های ماژول از طریق لیست root آن قابل انتخاب (opt-in) هستند، بنابراین کد پلاگین شما همچنان به restart نیاز دارد مگر اینکه یکی اضافه کنید. در هر صورت، طراحی را برای restart سریع انجام دهید: یک فایل که از طریق --patch بدون مرحله build بارگذاری شود و تمام ثبت‌نام‌ها از طریق ctx انجام گیرد تا چیزی از یک اجرا به اجرای بعدی نشت نکند. برای منابعی که Cordis نمی‌تواند به‌تنهایی پاکسازی کند، از ctx.effect() به همراه یک disposer استفاده کنید.

آیا این مقاله مفید بود؟