packages/dsh-skill-explorer
zhu1090093659/dsh-web技能中心:按来源分级浏览已加载的全部 skill,启用/禁用模型调用、创建新技能、删除进可恢复回收站。
当前会话的技能中心面板:列出可见技能及其提供方与来源根目录,阅读 SKILL.md,开关模型/用户调用,并可创建、软删除技能;随包附带 skill-authoring 技能与 skill_create 工具。
English · 中文
A Skill Center panel for DeepSeek Harness: browse every skill a session can see, switch which
session scope the panel follows, read its SKILL.md, flip the model/user invocation switches, create
and delete skills — without leaving the app and without hand-editing frontmatter. The package ships
icon.svg for the plugin manager and uses the same lightning mark for the sidebar entry, so the row
and the plugin card match.
[!NOTE] The npm name
dsh-skill-centeris taken by an unrelated project, so this plugin is published as@restregular/dsh-skill-center. The plugin id staysdsh-skill-center/skill-center.
Sidebar Main area
├ New session ┌───────────────────────────────────────────────┐
├ Plugins ← order 0 │ Skill Center 8 skills · session scope Back│
├ Skill Center ← order 1 (this) ├──────────────┬────────────────────────────────┤
├ Workspaces / Sessions │ auth0-quick… │ auth0-quickstart │
└ Settings │ discover-… │ provider / source / root │
│ office-docx │ path / size │
│ … │ [model invocable ●] [user ●] │
│ │ [Read body] [Delete] │
└──────────────┴────────────────────────────────┘
| Capability | Detail |
|---|---|
| Browse | Lists every skill visible in the current session scope, including skills embedded in plugins, each with provider, source root and invocation policy |
| Switch scope | The Scope dropdown in the toolbar is a custom popup menu that browses another session's skill scope, with sessions grouped by workspace while archived and blank sessions are left out. Auto (current session) keeps the original follow-the-main-view pick, while any listed session makes list and every write resolve from that session's cwd and preset. It is disabled while invocation edits are staged, so a switch can never carry them into a different scope |
| Search / filter | Search by name or description; filter by provider source |
| Read body | Renders the raw SKILL.md Markdown inside the panel (no right-hand resource pane needed) |
| Invocation switches | Flips disable-model-invocation / user-invocable locally — the switch moves at once, with no round trip — and writes them into the frontmatter when you press Save beside Back to session (the button reads 未修改 → 保存 → 已保存); the host pushes the refreshed catalog to the model immediately |
| Create | Creates <name>/SKILL.md under the user Agents or user DSH skills root; the form pairs Create with Cancel, and a hint under Write to explains what the choice means |
| Export / import | Export downloads the selected bundle as a .zip — SKILL.md plus every resource file under it, flat skills become <name>.md. Import skill accepts that .zip (a single wrapping folder is stripped) or a bare SKILL.md; it refuses entries whose paths escape the bundle, names that already exist, and files without frontmatter name + description |
| Delete | Moves the skill to .trash/<timestamp>-<name> in the same root — recoverable, never a hard delete; the confirmation puts 取消 / Cancel to the left of 确认删除? / Confirm delete? |
| Trash | The Trash button in the toolbar lists every .trash entry across the managed roots, with its root and deletion time. Restore renames one back into its original root (refusing to overwrite a skill that already exists there); Delete in the detail pane removes the selected entry permanently (after 确认删除?, Cancel on its left). Empty trash sits in the top toolbar and clears every entry, also behind a 确认清空? step that pairs a 取消 / Cancel on its left |
| No success toasts | The panel shows no "saved / created / deleted / restored" notice: the state change itself is the feedback (the save button's three states, a row appearing in or leaving the list, a trash entry leaving the trash). Only failures render, as a red bar with a retry button |
| Read-only guard | Skills embedded in plugins (e.g. office-docx) are labelled embedded and their switches/deletion are disabled |
| Bundled skill | Ships an embedded skill-authoring skill (guidance + references/ + a starter template) that teaches the agent which root a skill belongs in, what the frontmatter contract is, and where a skill's resource files go |
| Model tool | Registers skill_create, which writes <root>/<name>/SKILL.md plus any resource files in one call — the path that works for skill roots outside the session workspace, where the file-write tools are sandboxed |
| Security | Every write is confined to managed skill roots that really exist on disk; requests from non-loopback origins get 403 |
Write to (写入位置) is the directory the new skill's files are written into; it decides who can see the skill and how it competes with same-named skills. The form only offers the two user roots.
| Choice | Path | Applies to | Notes |
|---|---|---|---|
| User Agents skills | ~/.agents/skills ($DSH_AGENTS_HOME/skills) | every project | The cross-agent convention: other tools that scan ~/.agents/skills see it too. This is the panel's default. |
| User DSH skills | ~/.dsh/skills ($DSH_HOME/skills) | every project | DeepSeek Harness only. Rank 400, so a same-named skill here shadows the ~/.agents/skills one (rank 500). |
Project roots — <projectRoot>/.dsh/skills (rank 100) and <projectRoot>/.agents/skills (rank 200) —
are not offered in the form (the API and skill_create accept them via rootId). They only apply to
that repository and outrank both user roots, which is why a project copy of a skill wins over a user
copy with the same name.
Beyond the panel, the host half publishes its own guidance for agents and one tool that acts on it.
skill-authoring (embedded skill)The package ships skills/skill-authoring/ and registers it with ctx.skills.registerProvider(),
so it appears in the session catalog exactly like the built-in skills — the model can load it with
the skill tool, and the panel lists it as embedded (read-only, because the file belongs to the
package, not to a managed root).
skills/skill-authoring/
SKILL.md routing + the five-step short path
references/roots.md the root table, ranks, precedence, project-root detection
references/frontmatter.md every field, the boolean spellings, why a skill silently does not load
references/resources.md where resource files go and how the model is told to find them
references/plugin-bundled.md shipping skill content inside a plugin package
templates/minimal/SKILL.md starter bundle
Because the candidate carries resourceBase: { kind: 'directory', path }, the loaded skill arrives
with Base directory for this skill: <abs path> in its <skill_resources> block, so the agent can
read references/*.md with ordinary file tools. Rank 600 means any project or user skill named
skill-authoring shadows this one — override it by writing your own.
skill_create (model tool)skill_create({ name, description, whenToUse?, body?, root?, resources?, modelInvocable?, userInvocable?, overwrite? })
| Argument | Meaning |
|---|---|
name | kebab-case skill name; also the bundle directory name |
description | frontmatter description — the only routing signal the catalog shows |
whenToUse | optional whenToUse frontmatter field |
body | the Markdown after the frontmatter; defaults to a # <name> heading |
root | project-dsh | project-agents | user-dsh | user-agents; defaults to user-agents |
resources | [{ path, content }] written inside the same bundle directory (e.g. references/roots.md) |
modelInvocable / userInvocable | false writes disable-model-invocation: true / user-invocable: false |
overwrite | required to replace an existing skill; the default fails instead of clobbering |
It exists because of a real obstacle: the file-write tools are sandboxed to the session workspace,
while $DSH_HOME/skills and $DSH_AGENTS_HOME/skills are not in it, so an agent asked to "make me
a skill" could only fail there. The tool writes with the same confinement as the panel — only the
four managed roots, only inside <root>/<name>/ — and rejects absolute resource paths, ..
segments, a resource that would overwrite SKILL.md, and a name that collides with an embedded
skill. It registers through ctx.inject(['tools'], …), so a composition without ctx.tools simply
loses the tool instead of failing to mount.
[!NOTE] The tool is registered globally, so every agent on the host sees
skill_create(one extra tool schema in the prompt). Removing the plugin removes it; there is no switch.
ctx.skills registry and the loopback web server.dsh plugin --profile web add @restregular/dsh-skill-center
@restregular/dsh-skill-center and install it.# inside your DSH profile directory, e.g. ~/.dsh/profiles/desktop
pnpm add @restregular/dsh-skill-center
Then add the package to the profile's bundle list and restart:
// <profile>/package.json
{
"dsh": { "profile": { "bundles": [/* … */ "@restregular/dsh-skill-center"] } },
"dependencies": { "@restregular/dsh-skill-center": "^0.3.11" }
}
The package ships its own cordis.patch.yml, so no manual loader edit is required — it inserts the
host plugin (id: skill-center) and the client half. The sidebar row appears below Plugins,
because the client registers sidebar.panellist with order: 1.
[!IMPORTANT] The loader row's
namemust equal the package name verbatim (@restregular/dsh-skill-center).@deepseek-ai/dsh-client-moduleslocates a row'spackage.jsonby that name and compares it strictly (nearestPackage). With a baredsh-skill-centerthe host half still mounts, but the client half is dropped silently: no sidebar row,op=diagnosereportsclient.inGraph: falsewith noclient.url, and nothing is logged. 0.2.1 fixed exactly this; 0.2.0 shipped the bare name.
[!IMPORTANT] The client bundle must also call
__ModuleLoader__.load({ id: '@restregular/dsh-skill-center' })— the package name, not the plugin id. The browser module graph numbers rows by package name, andClientModuleSystem.invalidate(id, rev)(what the HMRrebuiltframe andentries.reloaduse) deletes factory entries by row id only. A registration id that differs from the row id leaves the stale factory in place, so the next bundle revision throwsclient-modules: duplicate factory registration … (bundle executed twice without invalidate?)and the whole web boot reports1 entry did not activate. 0.2.2 fixed this; 0.2.0/0.2.1 used the bare plugin id. The bundle additionally skips registration when that id is already present, so a double execution can no longer take the boot down.
To uninstall: remove the bundle entry and the dependency, then restart.
[!WARNING] Do not leave a bare-name junction/link for this package in the profile's
node_modules(e.g.node_modules\dsh-skill-center→ the repo). Both the link and the scoped package register the same plugin, which is exactly the two-sources situation behind the duplicate-registration crash. Install the scoped package and nothing else.
Nothing appears in the sidebar after install + restart? Check in this order:
http://127.0.0.1:19387/dsh-skill-center/api?op=ping should answer
{"ok":true,…}. A 404 means the loader row never mounted — the package is not in the profile's
dsh.profile.bundles, or is not installed under the profile's node_modules.http://127.0.0.1:19387/dsh-skill-center/api?op=diagnose — client.inGraph must be true.
false while host.mounted is true is the loader-row-name / package-name mismatch above./plugins/@restregular/dsh-skill-center/client.js?rev=<12 hex>), and the route deliberately rejects a
mismatched revision, so requesting the bare path answers 404 even on a healthy install. Read
client.url from op=diagnose (it now reports the row it matched in client.entryId) and fetch that
URL instead.node verify-compose.mjs <profileDir> — it now fails loudly on a loader row name that cannot be
resolved back to the package.pnpm add inside the profile is what makes the package resolvable; installing it elsewhere
(a global npm i -g, an unrelated directory) installs nothing the profile can compose.The host half mounts a loopback-only route at /dsh-skill-center/api:
| Method | op | Parameters | Purpose |
|---|---|---|---|
| GET | ping | — | liveness probe |
| GET | diagnose | sessionId? cwd? | is the host mounted, did the client half reach the browser module graph, resolved skill roots |
| GET | list | sessionId? cwd? | skill catalog + read metadata |
| POST | read | name | raw SKILL.md text |
| POST | setInvocation | name modelInvocable? userInvocable? | rewrite invocation policy |
| POST | create | name description body? rootId? whenToUse? resources? overwrite? modelInvocable? userInvocable? | create (or, with overwrite, replace) a skill bundle and its resource files; shares one implementation with skill_create |
| POST | delete | name | move to .trash |
| GET | trash | sessionId? cwd? | list .trash entries across the managed roots (newest first) |
| POST | restore | rootId entry | rename one .trash entry back into its original root; fails instead of overwriting |
| POST | deleteTrash | rootId entry | permanently delete one .trash entry |
| POST | emptyTrash | sessionId? cwd? rootId? | permanently delete trash entries (all managed roots unless rootId is given) |
| POST | export | name | bundle the skill as a .zip and return it as base64 (filename, bytes) |
| POST | importSkill | base64 filename? rootId? | import a .zip bundle or a bare .md; writes into rootId (default user-agents) and rejects traversal paths and existing names |
Requests from anything other than 127.0.0.1 / ::1 are rejected with 403.
# list the catalog for a session
curl "http://127.0.0.1:19387/dsh-skill-center/api?op=list&sessionId=<session-id>"
# turn off model invocation for one skill
curl -X POST "http://127.0.0.1:19387/dsh-skill-center/api" \
-H 'Content-Type: application/json' \
-d '{"op":"setInvocation","sessionId":"<id>","name":"web-react","modelInvocable":false}'
# PowerShell equivalent (UTF-8 safe)
$body = '{"op":"setInvocation","sessionId":"<id>","name":"web-react","modelInvocable":false}'
Invoke-RestMethod -Method POST -Uri "http://127.0.0.1:19387/dsh-skill-center/api" `
-ContentType 'application/json; charset=utf-8' -Body ([Text.Encoding]::UTF8.GetBytes($body))
Two halves, no shared imports:
Host half — lib/index.js
ctx.skills, resolving scope with the same three-tier fallback as the
official ctx.remote.skills: live agent → session-recorded preset scope (agentPresets.acquireScope)
→ global registry. The first two tiers decide whether the preset's skill-filesystem mount is visible
(with the global tier only, embedded skills are all you see).<projectRoot>/.dsh/skills, <projectRoot>/.agents/skills, $DSH_HOME/skills, $DSH_AGENTS_HOME/skills.list takes the invocation policy from the file on disk instead of
the registry snapshot. The skill catalog is cached and the filesystem watcher invalidates it a moment
after a write, so a panel that trusted the snapshot would show its own stale value right after a save —
which is exactly what made a flipped switch snap back until a manual refresh.rename into .trash; trash lists those entries, restore renames one back (never
overwriting an existing skill), deleteTrash removes one for good, and emptyTrash clears them all.
Trash operations resolve only the working directory — they do not need the skill registry, so they
keep working when the catalog read fails.ctx.skills.registerProvider) for skills/skill-authoring/
and the skill_create tool (ctx.inject(['tools'], …) + ctx.tools.register). The tool schema is a
hand-written JSON Schema using only the subset @deepseek-ai/dsh-tools accepts, so the package keeps
zero runtime dependencies and needs no host-side module resolution.tools or webServer service, or a bundled-skill
asset that fails to parse, only logs a warning and leaves the rest mounted.Browser half — client/client.js
Registers two slots and requires no Harness client package:
ctx.slots.inject('sidebar.panellist', () => ctx.slots.register({
name: 'sidebar.panellist', id: 'skill-center', order: 1, // 1 → right below plugins (order 0)
label: () => t('panel'), locale: NS,
}, SkillCenterIcon));
ctx.slots.inject('main', () => ctx.slots.register({
name: 'main', key: 'skill-center', locale: NS, // `main` is keyed: use `key`
}, Page));
Styles use host theme tokens only (--dsw-alias-*, --dsw-radius-*, --dsw-focus-ring-*), and the
<style> tag carries data-plugin="dsh-skill-center" so the host's ownerless-style sweep does not
reclaim it.
Switch edits are staged in component state: the switch reads a local override first and the catalog
second, and the Save button's three states are derived from the staged set (pendingEdits) plus a
"saved once" flag. After a save the overrides are kept until a fresh catalog actually agrees with them
(load() drops the ones that match), so a lagging host snapshot can never bounce the UI back.
The Scope dropdown is a custom popup (not a native <select>), so it can group candidates by
workspace with sticky headings and mark the running session. It builds those candidates from
sessions.list and workspaces.list: non-subagent sessions, archived and blank (never-messaged)
sessions removed, one group
per workspace in host order, ungrouped sessions bucketed by cwd, at most 12 sessions per workspace
(most recently updated first). The picked sessionId / cwd rides on every request, reads and
writes alike. Auto keeps the original three-step follow (session shown in the main area → last hit →
most recently updated); picking a session explicitly does not update that cache, so switching back
to Auto returns to the followed session instead of sticking to the manual choice. The source filter
and the create form's write-location picker use the same dropdown component.
The whole client bundle is wrapped in an IIFE. Plugin bundles are classic scripts in one page and share
a single global lexical environment that survives a plugin's removal, so re-installing or upgrading the
plugin while the page is open re-executes the script in the same realm: a top-level const MODULE_ID
would throw Identifier 'MODULE_ID' has already been declared, skip the whole script, register no
factory, and the sidebar row would only come back after a page reload. client-smoke.mjs executes the
bundle twice in one vm global-script context to keep this from regressing.
git clone https://github.com/RestRegular/dsh-skill-center.git
cd dsh-skill-center
| Change | Takes effect |
|---|---|
client/client.js | after a page reload — the host's /plugins/<id>/client.js route re-reads from disk and bumps the rev URL when ctime/size change |
lib/index.js (host) | after a host restart. Source-level HMR must be configured before launch: add config: { root: ['.', '<abs path to this repo>'] } to the profile's hmr entry — changing it while running has no effect |
package.json dsh.* | after a host restart |
For local development the repo can be mounted as a junction instead of being installed:
# profile is <DSH_HOME>\profiles\desktop
New-Item -ItemType Junction -Path "$env:USERPROFILE\.dsh\profiles\desktop\node_modules\dsh-skill-center" `
-Target "D:\path\to\dsh-skill-center"
Self-checks (the first two run against the local host route with a mini React shim; the third is fully offline):
$env:ELECTRON_RUN_AS_NODE = 1
$exe = "C:\path\to\DeepSeek Harness.exe"
$here = "D:\path\to\dsh-skill-center"
$env:DSH_SMOKE_SESSION = "<session-id>"; & $exe "$here\client-smoke.mjs" # client half + real loopback route
& $exe "$here\verify-compose.mjs" # client half accepted by the module graph
node "$here\verify-skill.mjs" # bundled skill + skill_create, offline
verify-compose.mjs must run through the Electron binary: it reads the shipped @deepseek-ai
packages out of app.asar, which plain node cannot open. verify-skill.mjs is the opposite — it
imports lib/index.js with a stub context and needs no host at all. It checks the bundled skill's
contract (directory name vs frontmatter, non-empty desc