Hướng Dẫn Phát Triển Plugin Cho DeepSeek Harness (dsh)

Hướng dẫn này dành cho các nhà phát triển muốn viết plugin cho dsh, đưa bạn từ khái niệm đến một plugin có thể chạy được. Bạn sẽ học được:认识 kiến trúc “everything is a plugin” (mọi thứ đều là plugin), viết một plugin hàm, gắn nó vào dsh, tạo một plugin công cụ có thể nhìn thấy bởi mô hình, một plugin hook chặn sự kiện, và cuối cùng了解了解了解如何如何如何如何如何制作成 một gói chính thức vượt qua cổng chất lượng.

Mục Lục

  1. Kiến Trúc Cơ Bản: Mọi Thứ Đều Là Plugin
  2. Chuẩn Bị Môi Trường
  3. Viết Plugin Đầu Tiên Của Bạn
  4. Bốn Xuất Khẩu Của Plugin: name / inject / Config / apply
  5. Gắn Plugin Vào dsh
  6. Thực Hành 1: Plugin Công Cụ Mô Hình Nhìn Thấy Được
  7. Thực Hành 2: Plugin Hook Chặn Sự Kiện
  8. Cấu Hình Khai Niệm (Config)
  9. Lỗ Hổ Năng Lượng: Service Definition / Provider / Consumer
  10. Phát Triển Gói Chính Thức Trong Kho Trữ
  11. Kiểm Tra Và Cửa Kiểm Chất Lượng
  12. Phát Hiện Vấn Đề Thường Gặp

1. Kiến Trúc Cơ Bản: Mọi Thứ Đều Là Plugin

dsh được điều khiển bởi Cordis được đóng gói. Sản phẩm không có lõi đặc quyền nào: trình điều适配器 mô hình, đăng ký công cụ, nhật ký phiên, thậm chí cả vòng lặp agent chính đều là plugin, do đó bất kỳ phần nào cũng có thể được thay thế hoặc mở rộng ở cấp độ cấu hình. Toàn bộ sản phẩm là một cây plugin được kết hợp từ nhiều lớp khi khởi động.

Năm ý tưởng cần nhớ (xem chi tiết):

Idea Ý Nghĩa
Plugin Một đối tượng thực hiện Service: phổ biến nhất là hàm có apply(ctx), hoặc đối tượng có inject, hoặc lớp con của Service.
Context (Ngữ Cảnh) Kho chứa dịch vụ. Dịch vụ treo trên ctx.<key> ổn định (như ctx.tools, ctx.llm, ctx.sessions), các plugin tìm dịch vụ qua key, không import triển khai cụ thể.
inject Khai báo các dịch vụ bắt buộc mà plugin cần. Loader sẽ đợi chúng tồn tại trước khi thực thi plugin, thứ tự tải phụ thuộc vào phụ thuộc chứ không phải thứ tự file.
Sự Kiện Có Kiểu Các dịch vụ định nghĩa sự kiện qua khai báo hợp nhất, sử dụng emit / waterfall / parallel / serial để phân phối cho người nghe.
Đăng Ký Có Thể Đảo Ngược Schema công cụ, đoạn prompt, trình điều适配器, người nghe đều được đăng ký qua ctx.effect() / ctx.on(); khi plugin gỡ bỏ (HMR, tải lại nóng, tắt), mọi thứ tự động hoàn nguyên.

Các điểm mở rộng (sự kiện / dịch vụ) là “API” của dsh. Khi thay đổi hành vi, hãy ưu tiên treo lên điểm mở rộng, đừng sửa vòng lặp. Bảng tương ứng đầy đủ xem trong Where new behavior goes.

2. Chuẩn Bị Môi Trường

Khuyến nghị phát triển trong một kho được clone下来,这样 có thể sử dụng trực tiếp các lệnh và脚本 của kho.

git clone https://github.com/deepseek-ai/deepseek-harness.git
cd deepseek-harness
pnpm install

Các lệnh thường dùng (xem chi tiết):

pnpm run typecheck     # kiểm tra kiểu (strict, không có any thoát)
pnpm run lint          # oxlint
pnpm run test          # vitest unit test
pnpm run build         # tsc tạo lib/types + tsdown tạo lib/
pnpm dsh --profile headless "xin chào"   # chạy một nhiệm vụ từ source (cần DEEPSEEK_API_KEY)

Để chạy mô hình thực tế cần API Key. Đặt DEEPSEEK_API_KEY vào file .env ở thư mục gốc, pnpm dsh sẽ tự động tải.

Lưu ý: Chương 3 của hướng dẫn này về “demonstration pure link” không cần Key; từ chương 6 trở đi, plugin công cụ/hook chạy mô hình thực tế cần Key. Không có Key vẫn có thể viết code, chạy unit test và dump-config để xác minh.

3. Viết Plugin Đầu Tiên Của Bạn

3.1 Plugin Hàm Tối Thiểu

Trong thư mục phát triển bất kỳ (giả sử ở tmp/hello-plugin/ trong thư mục gốc kho), tạo hello.ts:

import type { Context } from '@deepseek-ai/cordis'

export const name = 'hello'

export function apply(ctx: Context) {
  ctx.logger.info('hello from my first plugin')
}

name</strong> là metadata hiển thị tùy chọn, dùng để nhận diện plugin trong thông tin chẩn đoán. <code>apply(ctx) là nơi plugin đóng góp mọi thứ.

Tạo thêm cordis.yml:

- name: './hello.ts'

cordis.yml là danh sách các mục cấu hình Cordis, name có thể là đường dẫn tương đối hoặc tên npm package. Loader sẽ gắn mỗi mục.

3.2 Chạy Với Loader Được Đóng Gói (Không Cần Key)

Kho đi kèm Cordis loader được đóng gói, có thể chạy trực tiếp đường dẫn gắn tối thiểu:

node --import tsx ../../vendor/cordis/bin.js

Dự kiến output:

[info] hello from my first plugin

Điều gì đã xảy ra:

  1. Trình khởi tạo tạo gốc Context và gắn plugin Loader.
  2. Loader đọc cordis.yml, phân tích ./hello.ts và gắn nó làm plugin con.
  3. Cordis gọi apply(ctx) của bạn.

Sau khi trình xuất nhật ký được cài đặt, tiến trình sẽ chờ sự kiện, khi không có sự kiện nào khác thì Ctrl-C để thoát. Ví dụ này được trích xuất từ Cordis tutorial chapter 1, nơi có giải thích đầy đủ hơn.

3.3 Plugin Không Chỉ Là Hàm

Cordis chấp nhận ba dạng; cho đến khi bạn cần công khai dịch vụ, hãy dùng dạng hàm:

import { Service, type Context } from '@deepseek-ai/cordis'

// 1. Plugin hàm (phổ biến nhất)
export function apply(ctx: Context) {}

// 2. Plugin đối tượng: đối tượng có phương thức apply
export const objectPlugin = {
  name: 'object-plugin',
  apply(ctx: Context) {},
}

// 3. Plugin lớp: lớp con Service (phù hợp khi cung cấp một dịch vụ ctx.<key>)
export class MyService extends Service {
  constructor(ctx: Context) {
    super(ctx, 'myService')
  }
}

4. Bốn Xuất Khẩu Của Plugin: name / inject / Config / apply

Một plugin hàm chính thức thường xuất bốn thứ:

import type { Context } from '@deepseek-ai/cordis'
import z from '@deepseek-ai/schemastery'

/** Tên hiển thị plugin, chỉ dùng cho chẩn đoán. */
export const name = 'my-plugin'

/** Khai báo các dịch vụ bắt buộc; loader sẽ đợi chúng tồn tại trước khi thực thi apply. */
export const inject = ['tools']

/** Schema xác thực cấu hình thời gian triển khai (có thể bỏ qua, xem chương 8). */
export interface Config {
  greeting: string
}
export const Config: z<Config> = z.object({
  greeting: z.string(),
})

/** Core plugin: đăng ký mọi đóng góp và chỉ đăng ký là effect có thể đảo ngược. */
export function apply(ctx: Context, config: Config) {
  ctx.logger.info(config.greeting)
}

Các điểm chính:

  • inject chỉ khai báo dịch vụ bắt buộc. Tùy chọn sử dụng ctx.get(name) để đọc kho dịch vụ toàn cục, không dùng proxy thuộc tính (cảm ứng topo).
  • Plugin hàm phải xuất có tên, không xuất mặc định. Trộn hai dạng sẽ khiến Loader mất metadata inject (xem postmortem).
  • Chữ ký apply: có Config xuất thì là (ctx, config), không có thì là (ctx).
  • Sai cấu hình phải fail loud: tải thất bại sẽ báo lỗi rõ ràng, không im lặng bỏ qua.

5. Gắn Plugin Vào dsh

5.1 Cây Plugin Đến Từ Đâu

Một dsh đang chạy được kết hợp từ nhiều lớp, lớp sau ghi đè lớp trước:

  1. Các bundle được liệt kê trong manifesto profile (tải theo thứ tự)
  2. cordis.patch.yml của profile
  3. cordis.patch.yml cấp thư mục home ($DSH_HOME/cordis.patch.yml)
  4. --patch <path> ghi đè (theo thứ tự argv)

Xem cây thực tế được kết hợp trên máy bạn:

pnpm dsh --profile headless --dump-config

Bất kỳ dòng nào được in ra, bạn đều có thể thay thế bằng patch của mình. Patch tìm theo id dòng: hoặc thay thế toàn bộ config của dòng đó (không phải deep merge), hoặc chèn dòng mới.

# overlay.yml
- id: my-tool
  name: '@deepseek-ai/dsh-tool-mine'
  config:
    option: value

5.2 Ba Đường Dẫn Gắn

Đường Dẫn Phù Hợp Cách Làm
Plugin ngoài (khuyến nghị cho hầu hết trường hợp) Tự phát triển, không mã nguồn mở, phát hành riêng Gói npm độc lập, trong profile chạy dsh plugin add cài đặt; package.json khai báo "dsh": {"bundle": ...} có thể tự động vào lớp bundle
Overlay tạm thời Gỡ lỗi, trình diễn pnpm dsh --profile <name> --patch ./overlay.yml "nhiệm vụ"
Gói trong kho Đóng góp code cho chính dsh Đặt vào packages/<group>/<pkg>, xem chương 10

5.3 Cài Đặt Plugin Ngoài

Lệnh con dsh plugin gọi pnpm trong thư mục profile, các động từ được truyền thẳng:

pnpm dsh plugin --profile headless add /path/to/my-plugin
pnpm dsh plugin --profile headless add github:some-org/my-plugin
pnpm dsh plugin --profile headless remove my-plugin
  • Đường dẫn tương đối (., ../plugin) neo vào thư mục nơi lệnh được chạy.
  • Các gói phụ thuộc khai báo "dsh": { "bundle": ... } tự động vào lớp bundle của profile đó.
  • Các plugin được托管 Git sử dụng script prepare để build; pnpm ≥10 cần cho phép build trong pnpm-workspace.yaml của profile (sao chép key allowBuilds theo thông báo lỗi).

6. Thực Hành 1: Plugin Công Cụ Mô Hình Nhìn Thấy Được

Ứng dụng phổ biến nhất của plugin là thêm công cụ cho mô hình. Công cụ được đăng ký trên ctx.tools, schema sẽ tự động vào组装 prompt, mô hình có thể “nhìn thấy” nó.

Dưới đây là một công cụ tối thiểu hoàn chỉnh có thể chạy được (bỏ qua import):

import { readFile } from 'node:fs/promises'
import type { Context } from '@deepseek-ai/cordis'
import { defineTool } from '@deepseek-ai/dsh-tools'

export const name = 'demo-tool'
export const inject = ['tools']

export function apply(ctx: Context) {
  ctx.tools.register(defineTool({
    name: 'read_file',
    description: 'Đọc một file từ đĩa.',       // Mô tả khả năng mô hình thấy
    parameters: {
      path: { type: 'string', required: true, description: 'Đường dẫn tuyệt đối' },
      limit: { type: 'number' },                  // Tùy chọn, mặc định không yêu cầu
    },
    output: {
      schema: { type: 'string' },
      render: (_args, value) => [{ type: 'text', text: value }],
    },
    async execute(args, exec) {
      // args đã được defineTool xác thực theo schema và suy ra kiểu
      return readFile(args.path, { encoding: 'utf8', signal: exec.signal })
    },
  }))
}

Một số quy tắc cứng trong hợp đồng execute() (xem đầy đủ):

  • args tự động xác thực: defineTool sẽ xác thực tham số mô hình tạo ra trước execute, trong execute nhận được args có kiểu và schema giống nhau.
  • Chỉ trả về một giá trị JSON chuẩn: output.schema định nghĩa giá trị trả về; execute chỉ trả về giá trị này. Ném ngoại lệ = isError; kết quả领域 thành công (như mã thoát khác 0) cũng phải đưa vào giá trị chuẩn trả về.
  • Tuân thủ exec.signal</strong>: khi tín hiệu được kích hoạt phải hủy công việc đang diễn ra.</li>
    <li><strong>Chỉ đăng ký một lần</strong>: việc đăng ký mượn definition chỉ có quyền đọc, không thay đổi schema sau đó; muốn đổi công cụ thì giải phóng effect mà nó thuộc về rồi đăng ký lại.</li>
    <li><strong>Thẻ UI là thiết kế độc lập</strong>: nội dung mô hình thấy do <code>output.render
    quyết định; thẻ UI do presentCall / presentResult</strong> trả về ý định hiển thị <code>generic / terminal / diff.

6.1 Nhiệm Vụ Dài Chạy Nền

Nhiều công cụ cần run_in_background và được bật bởi cấu hình triển khai (không viết cứng trong code), hợp lý hơn là đăng ký qua ctx.jobs.start() làm nhiệm vụ nền, phía mô hình trả về句柄 chuẩn带 jobId, job_output / job_kill chịu trách nhiệm thu thập và dừng. Xem Long-running work.

6.2 Công Cụ Render Thành UI Như Thế Nào

Chọn ý định hiển thị card, tạo thẻ theo loại:

  • { card: 'generic', title, locations? } — Thẻ tổng quát; locations: [{ path, line? }] cho phép editor nhảy đến file công cụ đã chạm tới.
  • { card: 'terminal', title, cwd? } — Cuộc gọi của bạn là lệnh shell (tham khảo dsh-tool-bash).
  • { card: 'diff', title, diffs } — Tạo/sửa file, thẻ diff nội tuyến (tham khảo dsh-tool-fs với write/edit).

7. Thực Hành 2: Plugin Hook Chặn Sự Kiện

Không cần công cụ mới, chỉ muốn xen vào một kh环节 nào đó, sử dụng người nghe sự kiện. Vòng lặp chính được điều khiển bởi sự kiện, plugin hook chính là treo người nghe trên các sự kiện này.

7.1 Ví Dụ Cổng Quyền Truy Cập

Plugin sau đây chặn mỗi lần gọi công cụ trên tools/pre-execute, cho phép hoặc từ chối theo quy tắc:

import type { Context } from '@deepseek-ai/cordis'
import type { PreToolDecision, ToolExecution } from '@deepseek-ai/dsh-tools'

declare function isAllowed(exec: ToolExecution): Promise<boolean>

export const name = 'permission-gate'

export function apply(ctx: Context) {
  ctx.on('tools/pre-execute', async (exec, next): Promise<PreToolDecision> => {
    if (!(await isAllowed(exec))) {
      return { kind: 'deny', reason: 'Bị từ chối bởi chính sách.' }
    }
    return next()
  })
}

tools/pre-execute là sự kiện waterfall: người nghe nhận (...args, next), gọi next() để truyền kết quả cho người nghe tiếp theo; không gọi next() mà return trực tiếp là short-circuit (cắt đứt toàn bộ chuỗi). Đây là ngữ nghĩa cốt lõi của Cordis waterfall, lỗi dễ gặp nhất khi viết người nghe là quên next().

7.2 Chọn Điểm Mở Rộng Đúng

Bạn Muốn Làm Gì Dùng Cái Nào
Cho phép / từ chối / hỏi về cuộc gọi công cụ tools/pre-execute, trả về {kind:'deny'} / {kind:'ask'}
Cuộc gọi công cụ phải bị phủ quyết cuối cùng, không thể hủy ctx.tools.guard()
Bọc vòng đời thực thi công cụ (thời gian chờ/thử lại/chỉ số) tools/execute (chỉ exec.signal có thể thay thế)
Hiển nhiên sửa đổi kết quả công cụ hoặc nội dung hiển thị tools/post-execute
Chỉ quan sát kết quả cuối cùng bất biến (kiểm toán/bắt) tools/result
Sửa đổi cấu hình yêu cầu mô hình agent/request (waterfall, trả về LlmCallConfig đã thay thế)
Sửa đổi/từ chối tin nhắn vào step agent/pre-step (waterfall, trả về PreStepDecision)
Kết thúc turn / buộc đi thêm bước agent/turn-stopping (serial, không có next())
Lắng nghe đầu ra bất biến cuối cùng (kết quả công cụ, trạng thái, lỗi) sự kiện emit tương ứng + sự kiện phiên durable trên session/event

Danh sách đầy đủ nhà sản xuất/người tiêu dùng sự kiện xem trong docs/event-producer-consumer.md.

7.3 Phân Công Giữa Sự Kiện Durable Và Live

  • Sự kiện phiên durable (turn/*, step/*, user/message, assistant/*, tool/*) được thêm vào nhật ký phiên, khởi động lại vẫn có thể tái tạo. Bất kỳ thứ gì mô hình có thể thấy đều phải có thể tái tạo từ nhật ký này (mô hình thấy ⟺ đã ghi lại).
  • Sự kiện live (agent/*, tools/*) chỉ dùng phối hợp thời gian chạy: queue/trạng thái, chặn, xây dựng yêu cầu, điều hướng.

Để thêm đầu vào mới có thể thấy bởi mô hình, tuân thủ quy tắc này: thêm một loại sự kiện mới vào phần mở rộng SessionEventMap, render từ nhật ký, không vòng qua nhật ký.

8. Cấu Hình Khai Niệm (Config)

Các tùy chọn có thể điều chỉnh thời gian triển khai không được viết cứng trong plugin, mà làm thành trường Config, sửa đổi từ cordis.yml.

8.1 Định Nghĩa Schema

Sử dụng @deepseek-ai/schemastery (đây cũng là trình xác thực của Cordis), kiểu và xác thực runtime hợp nhất:

import z from '@deepseek-ai/schemastery'

export interface Config {
  allowParallelInProgress: boolean
}

export const Config: z<Config> = z.object({
  allowParallelInProgress: z.boolean().required(),
})

8.2 Lắp Ráp Trong cordis.yml

- id: todo
  name: '@deepseek-ai/dsh-tool-todo'
  config:
    allowParallelInProgress: true

8.3 Cấu Hình Chỉ Là Metadata, Điều Kiện Tổ Hợp Dùng Overlay

  • !!js trong cordis.yml chỉ được phép xuất hiện dưới config plugin và disabled mục; các metadata khác giữ nguyên văn (ngữ nghĩa loader).
  • Chọn plugin theo môi trường dùng overlay, không dùng !js.
# Trong patch lấy giá trị theo môi trường là cách dùng hợp lệ
- id: sandbox-policy
  name: '@deepseek-ai/dsh-sandbox-policy'
  config:
    mode: !!js "process.env.DSH_PERMISSION_MODE ?? 'workspace-write'"

9. Lỗ Hổ Năng Lượng: Service Definition / Provider / Consumer

Khi một năng lực cần “có thể thay thế”, dsh sử dụng không phải một lớp, mà là lỗ hổ (seam): ba vai trò.

Vai Trò Nhiệm Vụ Ví Dụ
Service Definition Khai báo giao diện (dịch vụ + từ vựng sự kiện), treo trên ctx.<key> dsh-subagent cung cấp ctx.subagents và từ vựng một lần gọi
Service Provider Triển khai giao diện này dsh-subagent-spawn-in-process, -fork, -acp, -codex…
Consumer Tiêu dùng nó, thường là công cụ có thể dùng bởi mô hình dsh-tool-subagent exposes provider đã cấu hình cho mô hình

Đổi một Provider là thay đổi hành vi sản phẩm toàn diện: hệ thống file và Provider con tiến trình chia sẻ cùng thế giới thực thi, đặt ctx.shell hướng đến sandbox từ xa, bash / PTY / LSP cùng thay đổi theo, không cần派生 Provider (xem Capability seams).

Một vai trò không phải lỗ hổ. Để thêm năng lực, hãy thiết kế cả ba vai trò — ngay cả khi bắt đầu chỉ có một Provider. Mẫu kinh điển là bộ ba shell, code ở packages/shell/.

10. Phát Triển Gói Chính Thức Trong Kho Trữ

Nếu plugin của bạn sẽ trở thành một phần của @deepseek-ai/dsh-<name>, hãy tạo gói theo checklist. Danh sách đầy đủ cấp file xem trong docs/cookbook/adding-a-package.md, đây là nội dung cốt lõi:

packages/<group>/<pkg>/
  package.json     # private: true;version giống root package.json;type: module
                   # main/types/exports trỏ vào lib/;@deepseek-ai/cordis ở peer+dev deps
  tsconfig.json    # extends ../../../tsconfig.base.json;references trỏ vào workspace package phụ thuộc
  src/index.ts     # Plugin hàm xuất name/inject/Config/apply;lớp dịch vụ xuất mặc định lớp
  README.md        # API dịch vụ, sự kiện, điểm mở rộng, đoạn Model Experience

Quy trình:

  1. Chọn nhóm: core, llm, shell, fs, web… nhóm hiện có phù hợp vai trò năng lực thì bỏ vào packages/<group>/<pkg> của nhóm đó.
  2. Đăng ký: thêm một dòng vào references của tsconfig.host.json / tsconfig.client.json; nhóm mới mới động tsconfig.base.json.
  3. Đặt tên: gói giao diện đặt tên năng lực (dsh-subagent), gói triển khai thêm cơ chế (-in-process, -fork). Key ctx số ít biểu thị một engine, số nhiều biểu thị bảng đăng ký.
  4. Xác minh: chạy cổng chất lượng bên dưới.

11. Kiểm Tra Và Cửa Kiểm Chất Lượng

11.1 Chạy Bắt Buộc Địa Phương

Sau khi thêm/sửa đổi gói, chạy từ thấp lên cao (chạy ảnh hưởng, CI mới chạy toàn bộ; xem docs/testing.md):

pnpm run constraints   # ràng buộc workspace
pnpm run typecheck
pnpm run lint
pnpm run build
pnpm run hygiene       # knip + publint + kiểm tra tiêu dùng NodeNext
pnpm run doc-sync      # cổng tài liệu
pnpm run test          # unit test

11.2 Phương Châm Kiểm Tra

  • Test hành vi mô tả hành vi, không tính đúng sai; thay đổi hành vi phải thay đổi test và giải thích lý do.
  • Mỗi file 100% coverage là ngưỡng CI (pnpm run test:coverage).
  • Plugin hiển thị trong sản phẩm phải có test tổ hợp thực: chạy qua Loader và khởi động cordis.yml qua app/process, không chỉ dùng unit test ctx.plugin(...code> do tay搭建).</li>
    <li>Mỗi thay đổi hành vi không tầm thường, có thể thấy bởi mô hình/người dùng sản phẩm, phải bổ sung trong <strong>cùng PR</strong> một snapshot keyless có thể phát lại (<code>pnpm run test:snapshot
    ).
  • Đăng ký có thể đảo ngược xác nhận bằng test an toàn HMR: giải phóng fiber sau đó đăng ký biến mất.

11.3 Nghĩa Vụ Tài Liệu

  • Mỗi thay đổi không tầm thường mang theo một Agent Note (.agents/notes/).
  • Thay đổi hành vi (key config, giá trị mặc định, mã lỗi, wire field) phải cùng commit cập nhật README / JSDoc.
  • README gói phải viết đúng đoạn Model Experience và Known Limitations.

12. Phát Hiện Vấn Đề Thường Gặp

Hiện Tượng Nguyên Nhân / Xử Lý
Plugin không hoạt động Trước tiên kiểm tra chính tả name: giải析模块 thất bại chỉ ghi nhật ký không crash, và khởi động sớm có thể mất. Sau đó --dump-config xác nhận trong cây tổ hợp thực có nó không
apply ném ngoại lệ Plugin tải thất bại sẽ chấm dứt tiến trình rõ ràng, là thiết kế, không im lặng bỏ qua
Trường cấu hình mới không hiệu quả Patch thay thế toàn bộ config theo id, không phải deep merge; kiểm tra có bị ghi đè mất trường khác không
!!js báo lỗi !!js chỉ được phép dưới config plugin và disabled mục, nơi khác phải dùng overlay
Mô hình không thấy công cụ của tôi Schema công cụ tự động vào prompt, nhưng scope (cô lập preset) hoặc restrict() có thể đã lọc nó đi
Quên next() Người nghe waterfall không gọi next() sẽ short-circuit toàn bộ chuỗi; muốn cho qua phải gọi
HMR tải nóng sau còn sót Đăng ký phải là hệ ctx.effect() / ctx.on(), việc裸 làm trong apply sẽ không được hoàn nguyên
dsh plugin add build thất bại pnpm ≥10 cần cho phép allowBuilds trong pnpm-workspace.yaml của profile

Đọc Thêm