Skills Plugins MCP Prompt Model 导航 博客 资讯 我的中心

dsh-workbench

DSH 企业工作台:在运行会话中管理 AI 员工、知识库、技能、MCP 服务器和插件。

staff-os @staff-os ⬇ 2 ★ 1 main

安装

dsh plugin --profile web add github:staff-os/dsh-workbench
下载安装清单

需要可复现安装时,可在仓库后追加 #commit 固定提交。

DSH 企业工作台:在运行会话中管理 AI 员工、知识库、技能、MCP 服务器和插件。

该插件未提供要点说明,请参考仓库 README。

  1. 安装并启动 DeepSeek Harness:npx @deepseek-ai/dsh web
  2. 在终端执行上面的安装命令(CLI 会解析插件并核验来源)
  3. 用 dsh plugins list 确认已安装,必要时重启 Harness 生效

插件以当前 dsh 进程的权限运行,安装时可能执行代码。请先通读仓库源码与许可证,确认无破坏性命令与越权访问;本站只做索引,不对第三方插件安全性作担保。

代码仓库github.com/staff-os/dsh-workbench
许可证MIT
主要语言main
下载量2
GitHub 星标1
最近推送2026-08-23
收录日期2026-09-19
分类模型与提供方

事实信息来自公开插件目录快照(2026-10-01),介绍文案由本站再加工。

以下为插件仓库 README 全文(原始内容,由公开目录抓取整理)。

# @staff-os/dsh-workbench

An enterprise workbench for the DeepSeek Harness: **AI employees, knowledge bases, skills,
MCP servers and DSH plugins**, all manageable from a running session.

**Standalone** — no external backend, no database. Every piece of state is a local file
under `$DSH_HOME`.

[中文文档](./README.zh.md)

## Install

```bash
dsh plugin --profile web add @staff-os/dsh-workbench
```

For local development, pass an absolute path:

```bash
dsh plugin --profile web add link:E:/path/to/dsh-workbench
```

Restart DSH afterwards.

## Configure

The bundled `cordis.patch.yml` is an opt-in patch layer that reads its configuration from the
environment:

| Variable | Purpose |
| --- | --- |
| `DSH_PROFILE` | Which profile MCP and plugin management act on. Defaults to `web`. |
| `CLAWHUB_REGISTRY` | Root URL of a ClawHub-compatible marketplace. Without it, the shipped source is used (ClawHub's official site, whose read-only API is public and needs no token). |
| `CLAWHUB_API_KEY` | Marketplace credential. **Only the reference name is stored; the value resolves through the credentials service or the launch environment — never inline it in YAML.** |

Environment variables come from `$DSH_HOME/.env` and from the `.env` of the directory `dsh` was
invoked in — both are read at startup, the nearer layer wins, and a name already in the process
environment is never replaced.

**To point at a self-hosted marketplace**, edit the profile's own patch layer
(`$DSH_HOME/profiles//cordis.patch.yml`) rather than the environment variable: that variable
only changes the URL, while `id` and the display name stay hardcoded, so the UI ends up labelling an
internal host "ClawHub". The patch layer is hot-reloaded — no restart:

```yaml
# A patch replaces the whole `config`; it does not deep-merge. Restate every key.
- id: workbench
  config:
    profile: !!js >-
      process.env.DSH_PROFILE?.trim() || 'web'
    registries:
      - id: skillhub
        name: Internal Skill Hub
        url: http://10.0.14.55        # self-hosted, compatible with ClawHub's /api/v1
        flavor: clawhub
        apiKeyEnv: SKILLHUB_REGISTRY_API_KEY   # omit when anonymous reads suffice
    registryTimeoutMs: 15000
```

`registries` aggregates: list two and the marketplace page searches both, each card labelled with
the source it came from. The `id` is recorded in the install ledger and update checks return to that
same source through it, so **do not change an `id` after installing from it** — those entries lose
their update path.

`flavor` is the protocol dialect, and it has exactly two values: `clawhub` (the default, and what
any ClawHub-compatible self-hosted deployment uses) and `skillhub` (deployments like
`api.skillhub.cn`, which split browsing into `/api/v1/showcase/*` chart endpoints and answer 405 on
`/api/v1/skills`).

Edit the patch directly for finer control. `config` accepts:

```yaml
- insert:
    - id: workbench
      name: '@staff-os/dsh-workbench'
      config:
        profile: web                 # profile to act on
        dshHome: ''                  # override $DSH_HOME; empty resolves from the environment
        registries:                  # aggregated across sources; shared by both marketplaces
          - id: clawhub
            name: ClawHub
            url: https://clawhub.ai
            flavor: clawhub          # clawhub | skillhub
            apiKeyEnv: CLAWHUB_API_KEY
        registryTimeoutMs: 15000     # one marketplace request
        toolTimeoutMs: 20000         # local write operations
        networkToolTimeoutMs: 120000 # operations that download (skill import, marketplace install)
        dshExecutable: dsh           # what plugin install/remove forwards to
        pluginToolTimeoutMs: 300000  # a pnpm install can take tens of seconds
```

## The five tools

One tool per domain, with an `action` parameter. **Destructive actions require an explicit
`confirm: true`** — without it the call errors and nothing is written.

### `workbench_employee` — AI employees

`list` `get` `create` `update` `bind` `delete`(confirm)

An AI employee is a DSH agent preset. Discovery, trust level, copying and deletion all go
through the native `ctx.agentPresets`. `create` copies an existing employee's directory
whole — the preset authoring API deliberately refuses caller-supplied composition text, so a
copy can never carry more capability than its source. Presets with `trust: system` ship with
the deployment and can be neither modified nor deleted.

`bind` writes `employee.yml` inside the preset directory, naming the knowledge bases, skills
and MCP servers this employee should use. **This is a declaration of responsibility, not
auto-wiring**: DSH does not start retrieving just because a knowledge base is bound. Bindings
pointing at things that do not exist are reported in `unknownBindings`.

### `workbench_knowledge` — knowledge bases

`list` `create` `get` `update` `delete`(confirm)
`add_document` `list_documents` `delete_document`(confirm) `search`

Retrieval is **BM25 keyword matching**, not semantic vector search: DSH's `ctx.llm` is a chat
adapter registry with no embedding capability, so a standalone build has nowhere to get
vectors. The returned `mode` is always `keyword`, matching the shape the backend returns when
it falls back to keyword recall.

Chinese text is tokenized into CJK bigrams and **deliberately not unigrams** — unigrams look
like better recall for single-character queries, but they turn characters that appear in nearly
every chunk into matchable terms, so any query containing one recalls the entire corpus.

Text only. PDF and Office files are rejected with a clear message; convert them to Markdown or
plain text first.

### `workbench_skill` — skills (local + marketplace)

Local: `list` `get` `create` `set_visibility` `import` `delete`(confirm)
Marketplace: `market_search` `market_get` `market_install` `market_update` `check_updates`

Reads go through the native `ctx.skills`, which already carries rank and same-name shadowing.
The `shadowed` flag means "this skill exists on disk but a higher-priority source of the same
name covers it" — without it, "I created it and still cannot call it" becomes unanswerable.

**Every write re-reads `ctx.skills` and reports whether that skill actually took effect**,
rather than predicting it. No restart is needed (see "Three things the skills domain must not
paper over" below), but "written" and "in effect" remain two different things: same-name
shadowing, or frontmatter DSH rejects, both turn a successful write into nothing.

`list` also reports the **rejected files**: on disk, but discarded whole by DSH because their
frontmatter breaks its rules. DSH only logs a warning for those, so if this does not say it,
nothing will.

`import` accepts a local archive path, a download URL, a GitHub repository URL, or a marketplace
slug. GitHub page URLs are rewritten to the `/tarball` endpoint. Unpacking validates entry count,
per-file and total size, and rejects `..`, absolute paths and NUL bytes; files land in a staging
directory **outside the skill root**, and only a validated directory is renamed into place.

The UI adds an **upload** entry: pick an archive in the browser and its bytes ride along with one
call. It shares the same unpacking and install path as `import`, differing in exactly two ways — an
8 MiB cap per upload (this path has no chunking and no streaming, and base64 adds another third, so
copying the unpacker's 50 MiB limit would blow up a single call), and **no ledger entry**: an
uploaded package has no marketplace coordinates, and a fabricated one would make later update checks
take the skill's name to the marketplace, find some unrelated package of the same name, and overwrite
the user's own work with it.

**The installed directory name comes from the package's own frontmatter `name`**: that is the
identity DSH registers.

### `workbench_mcp` — MCP servers

`list` `get` `add` `update` `delete`(confirm) `enable` `disable` `import_json`

MCP configuration lives as `@deepseek-ai/dsh-mcp-client` rows in the profile's
`cordis.patch.yml`. Edits are row-level YAML AST operations: comments, other plugins' rows and
`!!js` dynamic values all survive verbatim, and a `.bak-` backup is written first.
**Changes take effect on the next DSH start.**

`import_json` accepts Claude Code / Cursor style `{"mcpServers": {...}}` and converts `${VAR}`
into `!!js process.env.VAR`. DSH performs no client-side variable expansion, so an unconverted
value reaches the MCP server as the literal string `${GITHUB_TOKEN}` — and the failure surfaces
during remote authentication, far from its cause.

Write secrets as `!!js process.env.NAME`, never as plaintext.

### `workbench_plugin` — DSH plugins (local + marketplace)

`list` `install` `remove`(confirm) `update` `market_search` `market_install`

Install and remove forward to the `dsh plugin` CLI (itself a pnpm forwarder). This tool never
touches pnpm directly and never edits `dsh.profile.bundles` itself.

`list` separates two facts: `isBundle` says whether the package declares `dsh.bundle` — read
from **the copy actually installed on disk**, since a package can gain the declaration in a
newer version — and `active` says whether it joined the bundle layer. Both flags matter for
knowing whether an install really produced a plugin.

Local-path installs are checked first: a target without `dsh.bundle` is refused. Pointing at a
monorepo root instead of the plugin package is the usual cause, and pnpm would happily succeed
while reconciliation skips it, leaving "installed but nothing changed".

**Restart DSH for changes to take effect.**

## The sidebar

The plugin ships a web sidebar: a permanent 56px icon rail plus a content column that **belongs
to the sessions section only**. The active section is a filled block with an inverted icon.

There are six sections (sessions, employees, knowledge, skills, MCP, plugins), but the rail draws
**only those marked `visible` in `sections.ts`** — today that is sessions and skills. The other four
do not have finished surfaces yet, and an entry that leads to nothing but an explanation reads more
like breakage than an absence; their tools stay registered and the model can still use them, only
the entry is hidden. Finishing one means turning on its `visible` flag and nothing else.

Select anything but sessions and the content column does not render: the sidebar shrinks to that
rail and the domain's surface spans the full width from the rail's right edge. No narrow column
is kept because in those sections it could only hold a summary saying what the right-hand surface
already says — it costs usable width and makes you pick twice, once in the strip and once on the
right.

AppFrame's sidebar track still occupies whatever width the user dragged it to; the management
panel covers the leftover strip of empty sidebar (the overlay layer outranks the sidebar column).
The layout store is left alone on purpose: `ctx.layout` offers only `toggleSidebar()`, and
simulating "closed" with it would fight the user's own collapse state and might not restore.

It occupies ui-layout's `sidebar` slot, and **occupying is replacing**: the stock ui-sidebar
disappears along with every seat it declared. This plugin therefore re-declares and re-hosts
`sidebar.workspaces`, `sidebar.settings` and the rest with identical names, kinds and scopes,
so ui-workspace and ui-settings keep working unchanged. The sessions section hands its whole
body to ui-workspace.

`sidebar` is a `single` slot: two occupants do not error, they shadow each other by
registration order, which is not deterministic. `cordis.patch.yml` therefore disables the stock
`ui-sidebar`. Remove that row to keep the stock sidebar instead — but then which one renders
depends on plugin load order, which is not recommended.

## The management panel

Selecting any section other than sessions brings up that domain's management surface in the
main area; switching back to sessions removes it entirely and the conversation reappears
untouched.

**Skills** has a complete surface today: the list and the detail are not dialogs but an inline
surface covering the list, with a "back to list" control on top. Two pages — local inventory and
marketplace.

The detail is **two columns**: what this thing is on the left (title, description, tags, and an
overview/files pair of tabs), what state it is in on the right (source, path, visibility and the
rest, plus update and delete). Local skills and marketplace entries share the layout, because they
describe the same kind of thing and a second arrangement would mean learning where to look twice.

"Overview" **renders** the SKILL.md through the host's `MarkdownText` — the same renderer that draws
model replies in the conversation. Not pulling in a markdown stack of our own has two reasons: it is
already in the client loader's module table, so the plugin bundle does not grow for it; and, more to
the point, it is restrained about **untrusted content** (raw HTML and unsafe protocols are off), and
a marketplace SKILL.md is exactly that. A "source" toggle sits next to it: the model reads the raw
text, not the rendered result, so working out why a skill misbehaves means looking at the raw text.
A long body clips to 520px with a "show more" underneath.

"Files" is a **directory tree** — directories fold, files carry their size, SKILL.md sorts first.
Paths inside a skill package look like `references/api.md`, and a flat column of those does not show
what belongs together. Everything starts expanded: skill packages are small, and starting folded
would mean a page showing two or three directory names.

Clicking a file opens it right there: markdown goes through the same renderer as the body (with the
preview/source pair), anything else through the host's `CodeBlock`, syntax highlighting and copy
button included. Two things are stated rather than left blank — a **binary** file's bytes are never
sent to the browser (they would show nothing anyway), and anything over **256 KiB** arrives as its
opening slice with a note saying so. Marketplace files use the same dialog, served out of the package
just fetched: the gateway keeps **one** package (up to 8 MiB), so walking a tree does not re-download
the whole archive per file.

Which file gets read is decided by the browser, so there is a boundary here: a relative path is first
rejected literally for `..` and absolute forms, then, once resolved, **checked again** to confirm it
still sits under that skill directory. The first check alone is not enough — separators and drive
prefixes have more than one spelling per platform, and literal checks always leak one. That boundary,
along with "is it still text after truncation" (a cut through a multi-byte character makes the fatal
decoder call a Chinese document binary), is held by `tests/file-preview.test.mjs`.

The local tree lists the directory of the copy that is **actually in effect**, so a skill shipped
with the deployment shows its files too, not just the ones this plugin can change. One trap here:
`SkillView.path` does not mean the same thing in both projections — `projectLocal` gives
`/SKILL.md` while `projectWinner` gives the **directory**. So listing goes off `resourceBase`,
not `path`; and since a flat skill's (`.md`) `resourceBase` is the skill *root*, listing that
would attribute every skill's files to this one — hence a directory only counts as a skill directory
when it actually holds a SKILL.md.

The local page has two entries: **upload** a ready-made skill archive (the common case) or
**create** a SKILL.md from scratch. Skills installed from a marketplace carry an "Installed vX" pill,
plus a "New vY" one when a newer version exists; the header has "Check for updates" and "Update all
(N)". **Both only apply to skills the ledger has an origin for** — a hand-written skill has no
upstream version, so stamping one on it invents a fact, and taking its name to a marketplace to find
some same-named entry would overwrite the user's own work with an unrelated package.

"Check for updates" is its own button rather than folded into "reload": it goes over the network to
every source, while reload is the most-pressed thing on this page and should not wait on a
marketplace round trip. Update-all lets one failure not stop the rest, and reports successes and
failures **separately** — saying "updated N" while swallowing the failures reads as "all of them
updated". Skills whose update could not be checked at all (source unreachable) get their own line;
otherwise the UI silently presents them as up to date, which is a quiet lie.

The marketplace side follows the same rule: "Installed vX" only appears on the entry the ledger
recognizes (same source, same slug); a mere name collision says "Name taken". Merging the two would
make "overwrite install" read like an update when it actually replaces a same-named skill from a
different origin. Switching to the marketplace loads the first page of entries
immediately rather than waiting for a search click — what people do on that page is look at what is
there, not arrive with a name already in mind.

A marketplace entry gets the same pair of tabs as a local skill: "overview" is the body of its
SKILL.md, "files" is what the package contains and how big each file is. Both come from **fetching
the package**, not from asking the marketplace for a listing: SkillHub has a set of
`/api/web/skills/.../files` endpoints, but they are not part of the ClawHub-compatible contract (the
same paths 404 on clawhub.ai), whereas the download endpoint is the one installing goes through
anyway — so what is listed is what you would actually get. When it cannot be fetched (mirror
entries, entries ClawHub forwards to GitHub, an unreachable source) both tabs say why instead of
raising a failure — someone is looking, not installing.

Below the search box sits a **label grouping** bar: pick one and only entries under it stay. It has
two possible sources, and only one of them is ever shown:

- **The marketplace's own label catalog** (SkillHub's `/api/web/labels`). Where it exists, filtering
  by label happens **server-side** and covers the whole marketplace rather than the current page —
  the internal deployment carries a custom set (`skillhub`, `5g-dpi`, `aiintelligence` and friends),
  and picking one returns every match in the catalog. Labels belong to a specific source, so the
  filter only queries the source that owns the label: handing one source's label to another either
  404s or gets ignored, and the second case returns a full page of unfiltered results that looks
  filtered — worse than an error.
- **Categories counted from the batch already loaded** (each entry's own `topics`). Used only when a
  source offers no label catalog, with a line underneath stating the scope so "only three under this
  category" is not read as "only three in the whole marketplace".

With neither available the whole bar is absent rather than an empty shell holding only "All". Like
the file listing, `/api/web/labels` is not part of the ClawHub-compatible contract (it 404s on
clawhub.ai), so this is best-effort detection: no catalog means the source simply has no labels.

Label filtering goes through `/api/web/skills`, whose entries are shaped unlike either `/api/v1`
route — display name and summary each have a Chinese variant, the version lives under
`headlineVersion`, the statistics use different field names — so it gets its own normalizer. A side
benefit is that filtered cards show the Chinese display name. **`namespace` is not a publisher**: it
is a namespace like `global`, and treating it as the owner would print "published by global" on the
card and attach a coordinate upstream does not recognize to the download.

The **AI employees** surface (a list plus a six-page editor) still exists in the code; only its
entry is hidden for now, see "The sidebar" above. Knowledge, MCP and plugins still have explanatory
surfaces only. All four domains' tools stay registered and usable by the model.

Everything on screen is real local state: employees come from `ctx.agentPresets`, bindings from
the `employee.yml` inside each preset directory, persona and tools are parsed out of the agent
composition; the skill list merges `ctx.skills` with `$DSH_HOME/skills`. An employee shipped with
the deployment (`trust` other than `user`), and any skill outside the user directory, are
read-only in the UI — the same rule the tool enforces.

### Four things the skills domain must not paper over

**Shadowing.** `ctx.skills` answers "what can the model use right now" (rank and same-name
shadowing included); the user directory answers "what can this plugin change". When both hold a
name, the higher-priority source wins and the copy on disk stays there without taking effect. The
UI marks it shadowed and puts a banner at the top of the detail view saying that editing it
changes nothing — not an easily missed badge.

**Rejection by DSH.** Skill frontmatter has a few unforgiving rules: `name` is required and must
be kebab-case, and the two visibility switches must use the hyphenated spelling
(`disable-model-invocation`, `user-invocable`). Camel-case spellings make DSH **throw and discard
the whole skill**. Packages from the Claude Code ecosystem use the camel-case form often enough
that this matters: installed, everything looks fine, and the model never sees the skill. So this
plugin mirrors DSH's parsing rule for rule, lists those files separately, and names the exact key
to change.

**Whether a write actually took effect.** This used to say "takes effect after restarting DSH".
Checked against upstream source, that is wrong, and it costs the reader a pointless restart. The
real chain: `dsh-skill-filesystem` watches the skill roots with chokidar and calls
`control.invalidate()` about 200ms after a write; that callback clears the entire registry's
discovery cache and broadcasts `skills/change`. `dsh-tool-skill` re-snapshots on **every**
`agent/pre-step` and appends a replacement catalog when the digest changes. The user's `/name`
path is even more direct — it looks up `ctx.skills.get(name)` each time, and full definitions are
never cached. **Write to disk, and the next model turn has it.**

This plugin adds two things on top: it registers a provider on `ctx.skills` that produces no
skills at all, purely to obtain the invalidation handle, and rings it after each write (which
holds even when the watcher is disabled or cannot start); then it re-reads `ctx.skills` and
hands back whether that name resolves and whether the winner is the copy it just wrote.

One thing about that read-back has to be stated, or the verdict lies: `ctx.skills` is a
host-plus-per-scope layered registry, and this plugin sits in the **host** layer, on an unscoped
context. **The web profile's shipped composition disables the host `skill-filesystem` row**, moving
local discovery into each agent preset's own layer (`packages/bundle/web-app/cordis.patch.yml`
states the reason: presets own local discovery). Seen from here, nothing under `$DSH_HOME/skills`
resolves at all — while the agent in a session uses those skills perfectly well. So "not found" is
split into three distinct answers: this file was rejected (naming the exact key), the host layer
does not scan this root at all (a scope fact, not a failure), and neither of those (say so plainly
and point at DSH's log). In a headless profile the host mounts `skill-filesystem` itself, and the
read-back is directly meaningful.

**What is actually inside the package.** A skill is a set of instructions the model will follow,
so it is worth a look before installing. Both detail views — local and marketplace — carry a
"security scan" tab: thirteen regex rules plus one charset-smuggling check (text that is valid
UTF-8 on disk but reveals different content when re-decoded through UTF-16 was almost certainly
hidden on purpose). The rule table and the decoder check are ported from the skill-scan pre-scan
in Tencent Zhuque Lab's [AI-Infra-Guard](https://github.com/Tencent/AI-Infra-Guard)
(Apache License 2.0); the internal SkillHub runs the same rules in Java as a pre-publish check,
so both ends say the same thing about the same package.

The tab deliberately shows **no red/green verdict**. A match only means the text looks like a
high-risk pattern, not that the skill does it — `crontab` in a doc about scheduled tasks is
perfectly normal, and a red-team skill's jailbreak corpus is naturally full of prompt injection.
So it lists which rule matched in which file at which line, and clicking a row opens that file.
The reverse holds too: no match does not mean safe, there are only thirteen rules.

### What an employee is

An AI employee is not a name card but a directly-conversable agent template assembled from a
**persona, tools, skills, MCP servers and knowledge bases**. The name card (`preset.yml`) holds
only a display name and a description; what the employee can actually do is decided by
`agent.cordis.yml` — an array of cordis loader entries, one plugin per row, nestable through
`cordis:group`.

The UI therefore **parses** that file ([`employee/composition.ts`](src/employee/composition.ts))
rather than keeping a second record of its own: a second record would start diverging
immediately, and the composition is the only thing that decides. That is what makes the four
built-in modes legible at a glance — `minimal` is two tool plugins and one fixed system prompt,
`standard` is seventeen tool plugins with skills and AGENTS.md, `cordis` is that plus one.

Three things the UI states plainly, recorded here too:

- **Classification is a package-name-prefix heuristic**, not a DSH-sanctioned taxonomy. DSH does
  not mark a plugin as "a tool"; naming convention (`dsh-tool-*` / `dsh-skill*` / `dsh-mcp*`) is
  all there is. Rows that cannot be classified are listed verbatim under "other rows" instead of
  being guessed at.
- **A tool row is not a tool name.** One `dsh-tool-fs` row registers several file operations; the
  actual tool names are decided at runtime. What the UI shows is which tool plugins are composed.
- **A `!!js` `disabled` expression is shown verbatim** (a per-platform condition, say). Whether it
  actually disables the row depends on the runtime, so the UI does not decide that for it.

The two "personas" are two separate blocks in the UI because they are not the same thing: the
**system prompt** in the composition's `dsh-persona` row decides who this agent is when it
speaks (read-only — changing it means editing the composition), while the **role** the workbench
stores in `employee.yml` is an added layer describing what this employee is responsible for.
Calling the latter "the persona" would suggest editing it changes the system prompt. It does not.

For the same reason the skills and MCP pages first state whether the template **supports** that
kind of resource at all, before offering the binding list: `minimal` composes no skill support,
so binding skills to it writes the binding but leaves the model with no tool to invoke them.

The model sees this too: `workbench_employee`'s list output carries a "composition" line, read
from the same projection ([`employee/view.ts`](src/employee/view.ts)) the UI uses.

### Which slot it occupies

The panel occupies `shell.overlay` (a `list` slot, the frame-wide floating layer) and **not**
`conversation`. The latter is `single` and occupied by ui-conversation; taking it would mean
rewriting the entire conversation surface. A `list` slot displaces nobody. The cost is that the
overlay layer covers the whole frame, so the panel has to step aside for the sidebar column —
its width is published by the sidebar into a store both share (the sidebar and the panel are
two non-adjacent React trees, so context cannot reach across).

### The data channel

A browser reads local files through exactly one supported path: a Typert Remote. This package's
Node half registers `WorkbenchEmployeeGateway` as `ctx.workbenchEmployee`, and its `@Remote`
methods surface as `ctx.remote.workbenchEmployee.*` through api-gateway — the same mechanism
DSH's own `ui-settings-plugin-inventory` uses to read the plugin roster.

Two things are worth their own note; read them before changing this area:

- **`src/typert-schemas.ts` is handwritten.** DSH generates its equivalents with
  `@deepseek-ai/dsh-typert-generator`, which discovers packages through the harness workspace
  layout and cannot run for an out-of-tree plugin. The loader only cares about the `./typert`
  export in `package.json` and the shape of `TYPERT`, so this package writes that artifact
  contract by hand. **The cost**: these schemas have no compile-time link to the types in
  `employee/remote.ts`. Change a method signature and nothing fails to compile — it fails at
  runtime against the gateway's strict codec check. Change one, change the other.
- **The Node half builds from JS emitted by `tsc`, not from `.ts` sources.** The `@Remote`
  decorators must be transpiled into runtime code; the oxc transform underneath tsdown emits
  decorator syntax verbatim, which Node rejects on load. Hence `build` is `tsc -b && tsdown`,
  with the intermediate output in `.tsbuild/`.

Writing that table by hand carries two more rules you only learn by tripping over them:

- **No method may be named `remove`.** In the browser each namespace is a Service and `remove`
  already lives on its prototype; the collision is rejected at `$mount`. Both domains call their
  deletion method `delete`.
- **Optional parameters need `acceptsUndefined: true`.** The api-gateway matches argument names
  against the descriptor literally, and `undefined` does not exist in JSON — passing an undefined
  optional argument reads as "a field is missing" and the whole call is rejected. Writing
  `.optional()` on the schema does not cover this; the switch lives on the parameter
  descriptor.

Also: a Remote method cannot be named `remove`. Each browser-side namespace is a Service whose
prototype already carries `remove`, and the clash is refused at `$mount`. The employee domain's
method is called `delete`.

## Where data lives

```
$DSH_HOME/
├── skills//SKILL.md              skills (DSH's own user-level skill root)
├── workbench/skills.json               install ledger for marketplace skills (update checks read it)
├── workbench/staging/                  install staging, deliberately outside the skill root
├── profiles//cordis.patch.yml MCP servers (DSH's own profile patch layer)
├── .agent-presets//                AI employees (DSH's own preset root)
│   ├── agent.cordis.yml                  composition, owned by DSH
│   ├── preset.yml                        display metadata, owned by DSH
│   └── employee.yml                      resource bindings, added by this plugin
└── workbench/
    ├── knowledge//
    │   ├── kb.json                       metadata and chunking parameters
    │   ├── documents/.      source documents
    │   └── index.json                    chunk bodies and term frequencies
    └── cache/                            offline cache of marketplace results
```

Everything is owner-only (`0600` / `0700`): patch files and knowledge bases can hold tokens or
internal company content.

## Deliberately out of scope

- **Vector retrieval.** DSH exposes no embedding seam; pretending otherwise would be worse than
  saying so.
- **PDF / Office parsing.** No heavyweight dependencies up front — convert to text first.
- **Bypassing the preset copy boundary.** `ctx.agentPresets` allows whole-directory copying
  only, and that is a deliberate security design.

## Development

```bash
pnpm install
pnpm build        # both halves: lib/index.js for the host, lib/client.js for the browser
pnpm typecheck    # tsc -b
pnpm test         # node --test
```

The client bundle cannot use DSH's own client preset — it globs the harness repository for
packages by name, and an out-of-tree plugin is not there. `tsdown.client.ts` reimplements the
same artifact contract: the `__ModuleLoader__.load` closure, CJS on `platform: browser`, a fixed
`client.js` name, CSS modules compiled through lightningcss, and externals restricted to the
loader module table.

After changing that config, verify the artifact:

```bash
grep -o 'require("[^"]*")' lib/client.js | sort -u
```

Only module-table specifiers should appear. Anything else is a `require` that throws at runtime.

数据来源:公开的 DeepSeek Harness 插件目录与各插件 GitHub 仓库。本站为独立第三方目录,与 DeepSeek、幻方(High-Flyer)及插件作者均无隶属或背书关系。

每日精选 Skill 推荐,免费送到你邮箱

输入邮箱,每天接收一个精选 AI Agent 技能推荐。完全免费,持续更新。

提交后我们会发送一封确认邮件,点击邮件里的链接才会开始收信。

完全免费,取消任意时间。我们不会发送垃圾邮件。