DeepSeek Harness 技能中心插件:浏览并管理当前会话可见的技能 / Skill Center…
30.9/100
综合分
2
GitHub 星标
—
npm 周下载
—/5
用户评分
数据截至 2026/10/28 · 周下载取自 npm 全网(含 CI / 镜像),非本站安装数;综合分由星标、活跃度与兼容性检查合成。
安装命令 · 需先装 dsh CLI 引擎
dsh plugin --profile web add RestRegular/dsh-skill-center该包不可直接 npm 安装,改从 GitHub 源安装(明细见下方安装检查)
状态速览
判定依据:实装验证 · 静态安装检查 · 自动扫描 · 仓库维护状态信任档位:已验证
是什么
dsh 原生插件 · other
安全吗
本站尚未对该插件做风险分级
暂未覆盖,不等同于无风险
还在维护吗
活跃:最近一次提交在 10 天前
档位由哪些信号合成
本站实装验证(真实安装 L4 覆盖全站;L5 为运行时冒烟或实机截图,覆盖范围小于 L4) · 验证所用 dsh 版本 · 静态安装检查 · 风险分级 · 仓库维护状态。 下方各区块是它的证据明细。
社区实装还没有人打卡 · 装过的第一个就是你
安全扫描自动 · 静态分析源码v2.7.0🔵 扫描:含敏感能力尚无人工评估展开查看扫描说明与证据 ↓
安全扫描自动 · 静态分析源码
v2.7.0静态扫描发现插件具备触达本地系统的能力:执行外部命令、读写/删除本地文件、读取剪贴板或密钥类环境变量(至少其一)。这是能力清单,不是危险判定——具体能力项与证据见下方,安装前请自行判断是否需要。
读写/删除本地文件读取剪贴板或密钥环境变量发起外部网络请求
查看扫描证据(共 111 处,展示前 45 条)
- 读取本地文件lib/index.js:256
const text = readFileSync(path, 'utf8'); - 读取本地文件lib/index.js:280
const text = readFileSync(join(BUNDLED_SKILL_ROOT, name, 'SKILL.md'), 'utf8'); - 读取本地文件lib/index.js:350
if (existsSync(join(current, '.git'))) return current; - 读取本地文件lib/index.js:374
exists: existsSync(root.path), - 读取本地文件lib/index.js:409
if (!existsSync(path)) return undefined; - 读取本地文件lib/index.js:412
stats = statSync(path); - 写入本地文件lib/index.js:791
if (after !== before) writeFileSync(entry.path, after, 'utf8'); - 写入本地文件lib/index.js:1176
mkdirSync(root.path, { recursive: true }); - 写入本地文件lib/index.js:1177
writeFileSync(target, skillText, 'utf8'); - 写入本地文件lib/index.js:1183
mkdirSync(directory, { recursive: true }); - 写入本地文件lib/index.js:1184
writeFileSync(join(directory, 'SKILL.md'), skillText, 'utf8'); - 删除本地文件递归lib/index.js:1189
rmSync(directory, { recursive: true, force: true }); - 写入本地文件lib/index.js:1193
mkdirSync(dirname(target), { recursive: true }); - 删除本地文件递归lib/index.js:1430
rmSync(parsed.path, { recursive: true, force: true }); - 删除本地文件递归lib/index.js:1460
rmSync(join(directory, entryName), { recursive: true, force: true }); - 发起网络请求client/client.js:327
const response = await fetch(url.toString(), { headers: { Accept: 'application/json' } }); - 发起网络请求client/client.js:339
const response = await fetch(API, { - 读取密钥环境变量.npm-stage-clean.mjs:4
const token = process.env.NPM_TOKEN; - 请求外部主机registry.npmjs.org.npm-stage-clean.mjs:16
const list = await fetch('https://registry.npmjs.org/-/stage', { headers }); - 发起网络请求.npm-stage-clean.mjs:16
const list = await fetch('https://registry.npmjs.org/-/stage', { headers }); - 请求外部主机registry.npmjs.org.npm-stage-clean.mjs:31
const response = await fetch(`https://registry.npmjs.org/-/stage/${stageId}`, { - 发起网络请求.npm-stage-clean.mjs:31
const response = await fetch(`https://registry.npmjs.org/-/stage/${stageId}`, { - 请求外部主机registry.npmjs.org.npm-stage-clean.mjs:39
const after = await fetch('https://registry.npmjs.org/-/stage', { headers }); - 发起网络请求.npm-stage-clean.mjs:39
const after = await fetch('https://registry.npmjs.org/-/stage', { headers }); - 读取本地文件client-smoke.mjs:196
const source = readFileSync(CLIENT_PATH, 'utf8'); - 读取本地文件verify-compose.mjs:36
const manifest = JSON.parse(readFileSync(join(profileDir, 'package.json'), 'utf8')); - 读取本地文件verify-compose.mjs:53
if (!existsSync(file)) continue; - 读取本地文件verify-compose.mjs:55
return { dir, json: JSON.parse(readFileSync(file, 'utf8')) }; - 读取本地文件verify-compose.mjs:87
if (existsSync(patchPath)) patchFiles.push(patchPath); - 读取本地文件verify-compose.mjs:90
if (existsSync(profilePatch)) patchFiles.push(profilePatch); - 读取本地文件verify-compose.mjs:92
const text = readFileSync(file, 'utf8'); - 读取本地文件verify-skill.mjs:194
for (const entry of readdirSync(dir, { withFileTypes: true })) { - 读取本地文件verify-skill.mjs:250
check(`[${candidate.name}] resourceBase 是真实存在的目录`, candidate.resourceBase?.kind === 'directory' && existsSync(directory) && statSync(directory).isDire - 读取本地文件verify-skill.mjs:250
check(`[${candidate.name}] resourceBase 是真实存在的目录`, candidate.resourceBase?.kind === 'directory' && existsSync(directory) && statSync(directory).isDire - 读取本地文件verify-skill.mjs:251
check(`[${candidate.name}] path 指向真实 SKILL.md`, typeof candidate.path === 'string' && existsSync(candidate.path), String(candidate.path)); - 读取本地文件verify-skill.mjs:253
const raw = readFileSync(join(directory, 'SKILL.md'), 'utf8'); - 读取本地文件verify-skill.mjs:267
const targets = referencedPaths(readFileSync(join(directory, file), 'utf8')); - 写入本地文件verify-skill.mjs:300
mkdirSync(join(project, '.git'), { recursive: true }); - 写入本地文件verify-skill.mjs:373
writeFileSync(join(flatRoot, 'flat-skill.md'), '---\nname: flat-skill\ndescription: "flat"\n---\n\n# flat\n'); - 写入本地文件verify-skill.mjs:395
mkdirSync(truthDir, { recursive: true }); - 写入本地文件verify-skill.mjs:397
writeFileSync(truthFile, '---\nname: disk-truth\ndescription: "disk truth"\ndisable-model-invocation: true\nuser-invocable: false\n---\n\n# disk\n'); - 写入本地文件verify-skill.mjs:399
mkdirSync(outsideDir, { recursive: true }); - 写入本地文件verify-skill.mjs:401
writeFileSync(outsideFile, '---\nname: outside-truth\ndescription: "outside"\ndisable-model-invocation: true\n---\n\n# outside\n'); - 删除本地文件递归verify-skill.mjs:560
rmSync(trashDir, { recursive: true, force: true }); - 删除本地文件递归verify-skill.mjs:678
rmSync(exportDir, { recursive: true, force: true });
扫描于 2026/11/1(0 天前) · 范围 入口闭包 + 发布文件清单 · 来源 GitHub 仓库
涉及外部主机:registry.npmjs.org
静态扫描只反映源码中出现的调用,不构成安全背书;人工评估与自动扫描相互独立、互不覆盖。
给这个插件打分还没有人投票↓
用户评分
还没有人投票,来当第一个
展开完整介绍(82 字)↓
DeepSeek Harness 技能中心插件:浏览并管理当前会话可见的技能 / Skill Center plugin for DeepSeek Harness.
README
由 DeepSeek 最新模型翻译生成dsh-skill-center · 技能中心
Listed on dsh-plugin.org
English · 中文
给 DeepSeek Harness 用的技能中心面板:浏览会话能看到的全部技能,切换面板跟随哪个会话范围,查看技能的 SKILL.md,开关模型调用与用户调用,新建和删除技能——全程不用离开应用,也不用手动改 frontmatter。包里带了 icon.svg 供插件管理器使用,侧边栏条目也用同一个闪电图标,所以侧边栏那一行和插件卡片长得一致。
[!NOTE]
npm 上的 dsh-skill-center 已被无关项目占用,因此本插件发布为
@restregular/dsh-skill-center。插件 id 仍是 dsh-skill-center / skill-center。
展开完整文档(README 剩余 1 节)
dsh-skill-center · 技能中心
Listed on dsh-plugin.org
English · 中文
给 DeepSeek Harness 用的技能中心面板:浏览会话能看到的全部技能,切换面板跟随哪个会话范围,查看技能的 SKILL.md,开关模型调用与用户调用,新建和删除技能——全程不用离开应用,也不用手动改 frontmatter。包里带了 icon.svg 供插件管理器使用,侧边栏条目也用同一个闪电图标,所以侧边栏那一行和插件卡片长得一致。
[!NOTE]
npm 上的 dsh-skill-center 已被无关项目占用,因此本插件发布为
@restregular/dsh-skill-center。插件 id 仍是 dsh-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] │ └──────────────┴────────────────────────────────┘
功能
| 能力 | 说明 |
| 浏览 | 列出当前会话范围内可见的全部技能,包括插件里内嵌的技能,每项都带服务商、来源根目录和调用策略 |
| 切换范围 | 工具栏的 Scope 下拉框是自定义弹出菜单,可以浏览另一个会话的技能范围;会话按工作区分组,归档会话和空白会话不列出。Auto (current session) 保持原来跟随主视图的选择,选中列表里的某个会话后,list 和所有写操作都按该会话的 cwd 和预设解析。暂存了调用开关改动时该下拉框禁用,所以切换绝不会把改动带进别的范围 |
| 搜索 / 过滤 | 按名称或描述搜索;按服务商来源过滤 |
| 读取正文 | 在面板内渲染原始 SKILL.md Markdown(不需要右侧资源窗格) |
| 调用开关 | 在本地翻转 disable-model-invocation / user-invocable —— 开关立即移动,无需往返请求 —— 按下 Back to session 旁的 保存 时写入 frontmatter(按钮显示 未修改 → 保存 → 已保存);宿主立即把刷新后的目录推送给模型 |
| 创建 | 在用户 Agents 或用户 DSH 技能根下创建 <name>/SKILL.md;表单把 创建 与 取消 配成一对,写入位置 下方的提示说明该选择的含义 |
| 导出 / 导入 | 导出 把选中的包下载为 .zip —— 包含 SKILL.md 及其下所有资源文件,扁平技能变成 <name>.md。导入技能 接受该 .zip(会剥掉单层包裹文件夹)或裸 SKILL.md;拒绝路径逃出包的条目、已存在的名称,以及 frontmatter 缺少 name + description 的文件 |
| 删除 | 把技能移到同一根下的 .trash/<timestamp>-<name> —— 可恢复,绝不硬删除;确认框把 取消 / Cancel 放在 确认删除? / Confirm delete? 左侧 |
| 回收站 | 工具栏的 回收站 按钮列出所有受管根下的 .trash 条目,附带其根与删除时间。恢复 把条目重命名回原根(拒绝覆盖那里已存在的技能);详情窗格的 删除 永久移除选中条目(在 确认删除? 之后,取消在其左侧)。清空回收站 位于顶部工具栏,清空所有条目,同样经过 确认清空? 步骤,取消 / Cancel 在其左侧 |
| 无成功提示 | 面板不显示「已保存 / 已创建 / 已删除 / 已恢复」通知:状态变化本身就是反馈(保存按钮的三种状态、列表中出现或离开一行、回收站条目离开回收站)。只有失败才渲染,显示为带重试按钮的红色条 |
| 只读保护 | 嵌入插件的技能(如 office-docx)标记为 embedded,其开关/删除被禁用 |
| 内置技能 | 附带内置 skill-authoring 技能(指引 + references/ + 起始模板),教智能体技能该放在哪个根、frontmatter 契约是什么、技能的资源文件该放哪里 |
| 模型工具 | 注册 skill_create,一次调用写入 <root>/<name>/SKILL.md 及任意资源文件 —— 这条路径适用于会话工作区之外的技能根,文件写入工具在那里受沙箱限制 |
| 安全 | 每次写入都限制在磁盘上真实存在的受管技能根内;来自非回环来源的请求返回 403 |
写入位置:项目根与两个用户根
写入位置(Write to)是新技能文件写入的目录;它决定谁能看到该技能,以及它如何与同名技能竞争。表单只提供两个用户根。
| 选项 | 路径 | 适用对象 | 说明 |
| 用户 Agents 技能 | ~/.agents/skills($DSH_AGENTS_HOME/skills) | 所有项目 | 跨 agent 的约定:其他扫描 ~/.agents/skills 的工具也能看到。这是面板的默认项。 |
| 用户 DSH 技能 | ~/.dsh/skills($DSH_HOME/skills) | 所有项目 | 仅限 DeepSeek Harness。rank 400,因此这里同名的技能会遮蔽 ~/.agents/skills 里的那个(rank 500)。 |
项目根目录——<projectRoot>/.dsh/skills(rank 100)和 <projectRoot>/.agents/skills(rank 200)——不在表单中提供(API 和 skill_create 可通过 rootId 接受它们)。它们只对所在仓库生效,优先级高于两个用户根目录,所以同名的技能,项目副本会胜过用户副本。
内置技能与模型工具
除了面板,宿主侧还会发布自己的智能体指南,以及一个按指南行事的工具。
skill-authoring(内嵌技能)
包内自带 skills/skill-authoring/,并通过 ctx.skills.registerProvider() 注册,因此它会像内置技能一样出现在会话目录中——模型可以用 skill 工具加载它,面板将其列为 embedded(只读,因为文件属于包,而不属于受管理的根目录)。
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
候选技能带有 resourceBase: { kind: 'directory', path },因此加载后的技能会在 <skill_resources> 块中出现 Base directory for this skill: <abs path>,智能体就能用普通文件工具读取 references/.md。rank 600 意味着任何名为 skill-authoring 的项目技能或用户技能都会遮蔽它——写一个自己的技能即可覆盖。
skill_create(模型工具)
skill_create({ name, description, whenToUse?, body?, root?, resources?, modelInvocable?, userInvocable?, overwrite? })
| 参数 | 含义 |
| name | kebab-case 技能名;同时也是技能包目录名 |
| description | frontmatter 的 description——目录中唯一的路由信号 |
| whenToUse | 可选的 whenToUse frontmatter 字段 |
| body | frontmatter 之后的 Markdown;默认为 # <name> 标题 |
| root | project-dsh \| project-agents \| user-dsh \| user-agents;默认为 user-agents |
| resources | [{ path, content }],写入同一个技能包目录(例如 references/roots.md) |
| modelInvocable / userInvocable | 设为 false 会写入 disable-model-invocation: true / user-invocable: false |
| overwrite | 替换已有技能时必须提供;默认会失败,而不是直接覆盖 |
它之所以存在,是因为一个现实障碍:文件写入工具被沙箱限制在会话工作区内,而 $DSH_HOME/skills 和 $DSH_AGENTS_HOME/skills 不在其中,所以智能体接到「给我做一个技能」的请求时,只能在那里失败。这个工具写入时与面板受到同样的限制——只允许四个受管根目录,只允许写在 <root>/<name>/ 内——并且会拒绝绝对资源路径、.. 段、会覆盖 SKILL.md 的资源,以及与内置技能重名的名称。它通过 ctx.inject(['tools'], …) 注册,所以没有 ctx.tools 的组合只是失去这个工具,而不会挂载失败。
[!NOTE]
这个工具全局注册,所以主机上每个智能体都能看到 skill_create(提示词里多一份工具 schema)。移除插件即可移除它;没有开关。
环境要求
- DeepSeek Harness 桌面版,提供 ctx.skills 注册表和回环 Web 服务器。
- Node.js ≥ 22.13.0,用于包元数据/engine 字段(插件本身是纯 ESM,零运行时依赖)。
安装
一条命令(命令行)
dsh plugin --profile web add @restregular/dsh-skill-center
从插件市场(图形界面)
1. 打开侧边栏的 Plugins 入口。
2. 搜索 @restregular/dsh-skill-center 并安装。
3. 重启主机(或重新加载配置档),让主机侧挂载。
手动安装
inside your DSH profile directory, e.g. ~/.dsh/profiles/desktop
pnpm add @restregular/dsh-skill-center
然后把包加入配置档的 bundle 列表并重启:
// <profile>/package.json
{
"dsh": { "profile": { "bundles": [/ … / "@restregular/dsh-skill-center"] } },
"dependencies": { "@restregular/dsh-skill-center": "^0.3.11" }
}
包自带 cordis.patch.yml,所以无需手动修改加载器——它会插入主机插件(id: skill-center)和客户端部分。侧边栏行出现在 Plugins 下方,因为客户端注册 sidebar.panellist 时用了 order: 1。
[!IMPORTANT]
加载器行的 name 必须与包名完全一致(@restregular/dsh-skill-center)。@deepseek-ai/dsh-client-modules 按这个名字定位一行的 package.json,并严格比较(nearestPackage)。写成裸的 dsh-skill-center 时,主机侧仍会挂载,但客户端部分会静默丢弃:没有侧边栏行,op=diagnose 报告 client.inGraph: false 且没有 client.url,也不记录任何日志。0.2.1 修复的正是这个问题;0.2.0 发布的是裸名。
[!IMPORTANT]
客户端 bundle 还必须调用 __ModuleLoader__.load({ id: '@restregular/dsh-skill-center' })——用包名,不是插件 id。浏览器模块图按包名给行编号,而 ClientModuleSystem.invalidate(id, rev)(HMR 的 rebuilt 帧和 entries.reload 所用的)只按行 id 删除工厂条目。注册 id 与行 id 不一致时,过期的工厂会留在原处,于是下一次 bundle 修订会抛出 client-modules: duplicate factory registration … (bundle executed twice without invalidate?),整个 web 启动过程会报 1 entry did not activate。0.2.2 修复了这个问题;0.2.0/0.2.1 用的是裸插件 id。bundle 还会在该 id 已存在时跳过注册,所以重复执行不会再让启动崩溃。
卸载:删掉 bundle 条目和依赖,然后重启。
[!WARNING]
不要在配置档的 node_modules 里给这个包留裸名 junction/链接(例如 node_modules\dsh-skill-center → 仓库)。链接和带作用域的包会注册同一个插件,这正是重复注册崩溃背后的双来源情况。只安装带作用域的包,别的都不要装。
故障排查
安装并重启后侧边栏什么都没有?按这个顺序检查:
1. 宿主那一半还活着吗? http://127.0.0.1:19387/dsh-skill-center/api?op=ping 应该返回 {"ok":true,…}。返回 404 说明加载器行从未挂载——包不在配置档的 dsh.profile.bundles 里,或者没有装在配置档的 node_modules 下。
2. 客户端那一半进到浏览器模块图了吗? http://127.0.0.1:19387/dsh-skill-center/api?op=diagnose——client.inGraph 必须是 true。
如果 host.mounted 为 true 而它是 false,就是上面说的加载器行名 / 包名不匹配。
3. bundle 有被提供吗? 浏览器只会拿到带修订号的 URL (/plugins/@restregular/dsh-skill-center/client.js?rev=<12 hex>),而路由会故意拒绝修订号不匹配的请求,所以即使安装正常,请求裸路径也会返回 404。从 op=diagnose 里读 client.url (它现在会在 client.entryId 里报告匹配到的行),然后去取那个 URL。
4. 重跑离线组合检查(不需要重启宿主): node verify-compose.mjs <profileDir>——加载器行名无法解析回包时,它现在会大声报错。
5. 在配置档里执行 pnpm add 才能让包可解析;装到别处(全局 npm i -g、无关目录)不会给配置档装上任何可组合的东西。
用法
1. 打开一个会话(或者留在首页——见已知限制)。
2. 点侧边栏里的 Skill Center。
3. 在左边选一个技能;右侧面板显示服务商、来源、路径、大小和两个调用开关。
4. 随意拨动开关:它们立即生效,并暂存在面板里。Back to session* 左边的按钮会跟踪状态—— 未修改(灰色,没有暂存改动)→ 保存(可用,改动已暂存;暂存多个技能时会显示数量)→ 已保存(灰色,已写入)。再编辑任何内容都会让保存。点 Read body 看 Markdown,点 Create 新建技能,点 Delete 删除。删除确认框里 取消 与 确认删除? 配对,取消在左边。操作成功不弹提示;只有失败才显示红色横条,带重试按钮。
HTTP API
宿主侧在 /dsh-skill-center/api 挂载一条仅限回环路由:
| 方法 | op | 参数 | 用途 |
| GET | ping | — | 存活探测 |
| GET | diagnose | sessionId? cwd? | 宿主是否挂载、客户端侧是否接入浏览器模块图、解析出的技能根目录 |
| GET | list | sessionId? cwd? | 技能目录 + 读取元数据 |
| POST | read | name | 原始 SKILL.md 文本 |
| POST | setInvocation | name modelInvocable? userInvocable? | 改写调用策略 |
| POST | create | name description body? rootId? whenToUse? resources? overwrite? modelInvocable? userInvocable? | 创建技能包及其资源文件(带 overwrite 时替换);与 skill_create 共用同一套实现 |
| POST | delete | name | 移入 .trash |
| GET | trash | sessionId? cwd? | 列出各受管根目录下的 .trash 条目(最新在前) |
| POST | restore | rootId entry | 把一个 .trash 条目改名回原根目录;重名时失败,不覆盖 |
| POST | deleteTrash | rootId entry | 永久删除一个 .trash 条目 |
| POST | emptyTrash | sessionId? cwd? rootId? | 永久删除回收站条目(不给 rootId 时清空所有受管根目录) |
| POST | export | name | 把技能打包成 .zip,以 base64 返回(filename、bytes) |
| POST | importSkill | base64 filename? rootId? | 导入 .zip 包或裸 .md;写入 rootId(默认 user-agents),拒绝路径穿越和已存在的名称 |
来自 127.0.0.1 / ::1 以外的请求一律返回 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))
架构
分两半,不共享导入:
宿主侧 —— lib/index.js
- 通过 ctx.skills 读取目录,解析作用域时采用与官方 ctx.remote.skills 相同的三级回退:活动智能体 → 会话记录的预设作用域(agentPresets.acquireScope) → 全局注册表。前两层决定预设的 skill-filesystem 挂载是否可见(只有全局层时,你看到的全是内嵌技能)。
- 写操作只触及同时满足两个条件的文件:位于受管技能根目录内,并且在磁盘上存在: <projectRoot>/.dsh/skills、<projectRoot>/.agents/skills、$DSH_HOME/skills、$DSH_AGENTS_HOME/skills。
- Frontmatter 编辑在字符串层面进行:只改动调用相关的键,其余内容逐字节保留,包括换行符风格。
- 对于受管根目录内的技能,list 从磁盘上的文件读取调用策略,而不是用注册表快照。技能目录有缓存,文件系统监听器会在写入后过一小会儿才让它失效,所以信任快照的面板刚保存完就会显示自己的旧值——这正是开关翻转后弹回、直到手动刷新才恢复的原因。
- 删除是把文件 rename 进 .trash;trash 列出这些条目,restore 把一个条目改名还原(绝不覆盖已有技能),deleteTrash 永久删除一个条目,emptyTrash 清空全部。回收站操作只解析工作目录——它们不需要技能注册表,所以目录读取失败时照样能用。
- 还为 skills/skill-authoring/ 注册了内嵌技能服务商(ctx.skills.registerProvider),并注册了 skill_create 工具(ctx.inject(['tools'], …) + ctx.tools.register)。工具 schema 是手写的 JSON Schema,只用了 @deepseek-ai/dsh-tools 接受的那部分子集,因此这个包保持零运行时依赖,也不需要宿主侧做模块解析。
- 三部分各自独立降级:缺少 tools 或 webServer 服务,或者内嵌技能资源解析失败,都只记一条警告,其余部分照常挂载。
浏览器部分 —— client/client.js
注册两个插槽,不需要任何 Harness 客户端包:
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));
样式只用宿主主题 token(--dsw-alias-、--dsw-radius-、--dsw-focus-ring-), 标签带 data-plugin="dsh-skill-center",这样宿主的无主样式清扫不会把它回收掉。
开关的改动暂存在组件状态里:开关先读本地覆盖值,再读目录;Save 按钮的三种状态由暂存集合(pendingEdits)加一个「已保存过一次」标志推导出来。保存后,覆盖值会一直保留,直到新拉取的目录确实与之一致(load() 会丢弃已匹配的项),所以滞后的宿主快照绝不会把界面弹回去。
Scope 下拉框是自定义弹窗,不是原生 ,因此可以按工作区分组展示候选项,用吸顶标题标出分组,并标记正在运行的会话。候选项来自 sessions.list 和 workspaces.list:剔除子代理会话、已归档会话和空白会话(从未发过消息),按主机顺序每个工作区一组,未归组的会话按 cwd 分桶,每个工作区最多 12 个会话(按最近更新时间排序)。选中的 sessionId / cwd 会随每个请求一起发送,读请求和写请求都一样。Auto 保留原来的三步跟随逻辑(主区域显示的会话 → 最后一次命中 → 最近更新);手动选中某个会话不会更新这份缓存,所以切回 Auto 时会回到跟随的会话,而不是停留在手动选择上。来源筛选器和创建表单里的写入位置选择器用的是同一个下拉组件。
整个客户端 bundle 包在 IIFE 里。插件 bundle 在同一页面中是经典脚本,共享同一个全局词法环境,且该环境在插件移除后依然存在。因此页面打开时重装或升级插件,脚本会在同一个 realm 里重新执行:顶层的 const MODULE_ID 会抛出 Identifier 'MODULE_ID' has already been declared,整个脚本被跳过,不注册任何工厂,侧边栏那一行只有刷新页面后才会回来。client-smoke.mjs 在同一个 vm 全局脚本上下文中执行 bundle 两次,防止这个问题回归。
Development
git clone https://github.com/RestRegular/dsh-skill-center.git
cd dsh-skill-center
| 改动 | 生效时机 |
| client/client.js | 刷新页面后生效 —— 主机的 /plugins/<id>/client.js 路由会重新从磁盘读取,ctime/size 变化时更新 rev URL |
| lib/index.js(主机) | 重启主机后生效。源码级 HMR 必须在启动前配置:在配置档的 hmr 条目里加上 config: { root: ['.', '<abs path to this repo>'] } —— 运行中修改无效 |
| package.json 的 dsh. | 重启主机后生效 |
本地开发时,可以把仓库挂载为 junction,而不必安装:
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"
自检(前两个针对本地主机路由运行,带一个迷你 React shim;第三个完全离线):
$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 必须通过 Electron 二进制运行:它要从 app.asar 里读取随包发布的 @deepseek-ai 包,而普通 node 打不开 app.asar。verify-skill.mjs 正好相反:它用一个 stub 上下文导入 lib/index.js,完全不需要宿主。它检查随包技能的契约(目录名与 frontmatter 是否一致、description 非空、resourceBase 目录存在、表格或链接里引用的每个 references/ 风格路径都真实存在),检查工具的 JSON Schema 是否符合 @deepseek-ai/dsh-tools 接受的子集,然后在临时目录里真正运行 skill_create:创建、资源放置、覆盖、重名、非法名、路径穿越、扁平文件、显式项目根,以及三条降级挂载路径。它还断言:即使注册表快照与磁盘不一致,托管根技能的调用策略也以磁盘上的文件为准;并沿 loopback 路由走一遍回收站往返:delete → trash 列出该条目及其根,restore 还原目录包和扁平 .md,路径穿越的 entry 和已存在的目标都会被拒绝,deleteTrash 只删除一个条目(并报告缺失的条目),emptyTrash 清空其余条目。导出/导入也是真实往返:export 返回真正的 ZIP,删除技能后导入该 base64 能还原 SKILL.md 及其资源,再次导入同名会被拒绝,裸 .md 变成扁平技能,带一层包裹目录的 .zip 会被剥掉,../ 条目会被拒绝且不写入任何内容。
client-smoke.mjs 通过一个迷你 React shim,把真实面板渲染到运行中的 loopback 路由上,然后驱动暂存保存流程:开关切换时不发写请求,Save 只发一次 setInvocation 并带上两个标志,按钮依次走 未修改 → 保存 → 已保存(下一次编辑又回到 保存),滞后的目录快照不会把开关弹回去。它还检查自定义 Scope 下拉框:有暂存编辑时它被禁用(并给出先保存的提示);打开后列出 Auto 加两个会话,按工作区分组,而 fixture 里归档和空白的会话不出现;选择某个会话后,下一次 list 和一次 export 都会带上它的 id 和 cwd,选择 Auto 则回到跟随的会话。这个 bundle 还会在同一个 vm 全局脚本上下文里跑两次——即重装后的页面 realm——第二次运行既不能抛重复声明错误,也不能注册第二个 factory。接着它走一遍删除确认——取消在 确认删除? 左边,取消后回到 删除 且不发请求。创建表单的 Cancel 关闭表单并回到详情;Export 发一次 export 并点击 .zip 下载;Import 发一次 importSkill,带上文件名和非空 base64。回收站流程用 stub 实现(GET trash 从假条目列表作答;restore / deleteTrash / emptyTrash 只改它):打开回收站会列出条目,Restore 发送一次 restore,带 rootId + entry,回收站随即清空;离开再进入回收站会重新加载;详情里的 Delete 需要 确认删除?,取消在左(取消不发任何请求,确认发送一次 deleteTrash);工具栏的 Empty trash 同样需要 确认清空?(确认后才发一次 emptyTrash)。它拦截 POST(并在 create / delete 上抛错),所以整个运行过程绝不会改写真实的 SKILL.md。
重命名或移动包:客户端模块表按 (loader baseUrl, loader line name) 缓存包元数据——不看文件时间戳——所以运行中的宿主仍保留旧路径。浏览器侧使用激活时拍下的内存快照,重载后继续工作;新路径要等宿主重启后才解析。
发布
升 version,提交,推送,然后在仓库根目录用对 scope 有发布权限的 token 执行 npm publish。发布成功时 registry 返回 202 {"success":true},随后把该版本置于 validating 状态:验证通过前,/@restregular/dsh-skill-center/<version> 和 tarball URL 都返回 404,dist-tags 仍指向上一版。这是正常窗口,不是失败。用带缓存清除参数的查询(?t=<ticks>)重新探测版本 URL,不要重复发布:同一版本第二次尝试会失败,报 E409 Cannot publish over previously staged version <version>(npm stage publish 则报 E409 Cannot stage previously published version <version>),而 npm stage list 可能什么都不显示。
已知限制
- 面板占据主区域时,右侧的 dsh-resource:// 预览不可用(没有会话界面时调用 sidebarRight.openResource,宿主会抛错),所以正文预览走本插件自己的路由,在面板内渲染原始 Markdown。
- 只能管理文件系统支撑的技能;嵌在插件里的技能是只读的。
- 没选会话时,面板退化为「按工作目录读取」,只能看到嵌入技能加上该 cwd 的项目技能;页脚显示生效的 cwd 和 scope 层级。
- Scope 下拉按工作区给会话分组,隐藏归档和空白会话;每个工作区最多列出最近更新的 12 个会话。更早的会话仍可通过 API 传入其 sessionId 读取。
- 目录替换是全有或全无:一个开关变化就让宿主重发整条技能目录消息(这是 Harness 的设计选择,不是本插件的行为)。
- skill_create 在宿主范围内注册,所以每个智能体都会多看到一个工具 schema;它没有按智能体的开关,只有卸载插件才能移除。
- 内置的 skill-authoring 技能是嵌入的,因此在面板里只读。因为它位于第 600 位,同名项目技能或用户技能会整体替换它——这就是定制它的预期方式。
License
MIT
DeepSeek Harness 的技能管理面板:在首页侧栏「新会话」下方的「插件」入口下面加一行「技能中心」,点开后中央区域显示面板,用来查看和改写任意会话能看到的技能(默认跟随当前会话,可在工具栏切换作用域)。
包内 icon.svg 供插件管理器使用,侧栏入口用同一枚闪电记号,两处视觉一致。
[!NOTE]
npm 上的 dsh-skill-center 已被另一个无关项目占用,所以本插件发布为
@restregular/dsh-skill-center;插件 id 仍是 dsh-skill-center / skill-center。
功能
| 能力 | 说明 |
| 浏览 | 列出当前会话作用域下模型/用户能看到的所有技能(含插件内嵌技能),带提供方、来源根、调用策略 |
| 切换作用域 | 工具栏的作用域下拉可以查看另一个会话的技能目录:自动(当前会话) 沿用原来「主区域会话 → 最近会话」的跟随逻辑;选中某个会话后,list 与全部写操作都按该会话的 cwd 与预设解析。有未保存的开关改动时下拉禁用,改动不会落到另一个作用域 |
| 搜索 / 筛选 | 按名称或描述搜索;按来源(provider source)筛选 |
| 查看正文 | 面板内直接读 SKILL.md 原文(Markdown 原文,不经过右栏) |
| 调用开关 | 开关先改本地状态(点了立刻切换,不发请求),再按「返回会话」左边的 保存 按钮写回 frontmatter(按钮三态:未修改 → 保存 → 已保存,多个技能待保存时显示「保存 (n)」);写盘后监听器立刻把新目录推给模型 |
| 新建 | 在「用户 Agents 技能」或「用户 DSH 技能」根下创建 <name>/SKILL.md;表单里「创建」旁边有「取消」,写入位置下方有选择说明 |
| 导出 / 导入 | 导出把选中的技能下载成 .zip(SKILL.md + 全部资源文件;平坦技能导出为 <name>.md);导入技能接受该 .zip(自动剥掉外面那层目录)或单个 SKILL.md,会拒绝越出技能目录的路径、已存在的名字,以及缺 frontmatter name + description 的文件 |
| 删除 | 移动到同根目录的 .trash/<时间戳>-<名字>(不硬删);确认时「取消」排在「确认删除?」左边 |
| 回收站 | 工具栏「回收站」列出所有受管根下的 .trash 条目(带原技能根与删除时间);恢复把条目移回原技能根(目标已存在时拒绝覆盖),详情区 删除 在「取消」+「确认删除?」后永久删除这一条,顶部工具栏 清空回收站 在「取消」+「确认清空?」后永久删除全部条目 |
| 无成功提示 | 面板不再弹「已保存 / 已创建 / 已移动到 .trash / 已恢复」这类提示:状态变化本身就是反馈(保存按钮三态、列表增删一行、回收站条目消失)。只有失败才渲染成红色条 + 重试按钮 |
| 只读保护 | 插件内嵌技能(如 office-docx、diagnose-windows-sandbox-acl)标注「内嵌」,开关与删除按钮禁用 |
| 随包技能 | 附带一个内嵌的 skill-authoring 指导技能(正文 + references/ + 起步模板):技能该放哪个根、frontmatter 契约是什么、资源文件放哪、怎么把技能打进插件包 |
| 模型工具 | 注册 skill_create:一次调用写出 <root>/<name>/SKILL.md 与全部资源文件 —— 技能根在工作区之外时(文件写入工具被沙箱挡住)这是唯一走得通的路径 |
| 安全 | 写操作只落在真实存在于磁盘的受管技能根内;非回环来源一律 403 |
写入位置:项目根与两个用户根
「写入位置」就是新建技能的 SKILL.md 落到哪个目录——它决定技能的可见范围,以及同名时谁盖过谁。
表单里只提供两个用户根(项目根由 API / skill_create 的 rootId 指定,面板不提供)。
| 选择 | 路径 | 生效范围 | 说明 |
| 用户 Agents 技能 | ~/.agents/skills($DSH_AGENTS_HOME/skills) | 所有项目 | 跨 Agent 的通用约定:其它扫描 ~/.agents/skills 的 Agent 工具也能用。面板默认写这里。 |
| 用户 DSH 技能 | ~/.dsh/skills($DSH_HOME/skills) | 所有项目 | 只属于 DeepSeek Harness。rank 400,同名时优先于 ~/.agents/skills(rank 500)。 |
项目根 <projectRoot>/.dsh/skills(rank 100)与 <projectRoot>/.agents/skills(rank 200)只对当前仓库生效,且优先级高于两个用户根 —— 所以项目里放一份同名技能就会盖过用户级的那份。
随包技能与模型工具
除了面板,宿主半边还会给 Agent 发一份指导技能和一个真正能动手的工具。
skill-authoring(内嵌技能):包内 skills/skill-authoring/ 通过
ctx.skills.registerProvider() 注册,和第一方内嵌技能一样出现在会话技能目录里 ——
模型可以用 skill 工具加载它,面板把它标成「内嵌」(只读,因为它属于包,不属于受管根)。
skills/skill-authoring/
SKILL.md 路由 + 五步短路径 references/roots.md 根表、rank、优先级、项目根判定 references/frontmatter.md 每个字段、布尔写法、为什么技能会静默不加载 references/resources.md 资源文件放哪、模型怎么被告知去找 references/plugin-bundled.md 把技能打进插件包的两种注册方式 templates/minimal/SKILL.md 起步模板
候选带上 resourceBase: { kind: 'directory', path } 之后,加载出来的 <skill_resources> 里会写明 Base directory for this skill: <绝对路径>, Agent 因此能用普通文件工具去读 references/.md。rank 600 意味着任何项目级/用户级同名技能都能盖过它 ——想改写它就自己建一个同名技能。
skill_create(模型工具):
| 参数 | 含义 |
| name | kebab-case 技能名,同时是目录名 |
| description | frontmatter 的 description:目录里唯一的路由信号 |
| whenToUse | 可选,写进 frontmatter |
| body | frontmatter 之后的 Markdown 正文;省略则写一个 # <name> 标题 |
| root | project-dsh / project-agents / user-dsh / user-agents,默认 user-agents |
| resources | [{ path, content }],写进同一个技能目录(如 references/roots.md) |
| modelInvocable / userInvocable | 传 false 分别写 disable-model-invocation: true / user-invocable: false |
| overwrite | 覆盖已存在的技能必须显式传,默认宁可失败也不覆盖 |
它存在的理由很具体:文件写入工具被沙箱限制在会话工作区内,而 $DSH_HOME/skills、 $DSH_AGENTS_HOME/skills 不在工作区里 —— 用户说「给我做个技能」时,Agent 在那里根本写不进去。
这个工具用的是和面板同一套落盘实现:只写四个受管根、只写 <root>/<name>/ 之内,并拒绝绝对资源路径、.. 段、会覆盖 SKILL.md 的资源,以及与内嵌技能重名。
它通过 ctx.inject(['tools'], …) 注册,所以没有 ctx.tools 的组合只是少这个工具,不会挂不上。
[!NOTE]
工具是宿主级全局注册:同一宿主上每个 Agent 都会多看到 skill_create(多一份工具 schema)。
没有开关,卸载插件才会消失。
安装
市场安装:侧栏「插件」→ 搜索 @restregular/dsh-skill-center → 安装 → 重启宿主。
手工安装(在 profile 目录里,例如 ~/.dsh/profiles/desktop):
pnpm add @restregular/dsh-skill-center
然后把包加进 profile 的 bundle 清单并重启:
// <profile>/package.json
{
"dsh": { "profile": { "bundles": [/ … / "@restregular/dsh-skill-center"] } },
"dependencies": { "@restregular/dsh-skill-center": "^0.3.11" }
}
包自带 cordis.patch.yml,不需要手改 loader:它会插入宿主半边(id: skill-center)与浏览器半边。
侧栏那一行排在「插件」下方,因为客户端以 order: 1 注册 sidebar.panellist。
[!IMPORTANT]
patch 里那条 loader 行的 name 必须逐字等于包名(@restregular/dsh-skill-center)。
宿主 @deepseek-ai/dsh-client-modules 是按这个行名去定位该行的 package.json 并严格比对 name 的(nearestPackage)。写成裸名 dsh-skill-center 时:宿主半边照样挂得上,但浏览器半边会被静默丢弃 —— 侧栏没有「技能中心」、op=diagnose 里 client.inGraph 是 false 且没有 client.url,而且日志里一个字都没有。0.2.1 修的就是这个;0.2.0 发出去的是裸名。
[!IMPORTANT]
客户端 bundle 还必须用包名注册:__ModuleLoader__.load({ id: '@restregular/dsh-skill-center' }),不能用插件 id dsh-skill-center。浏览器模块图是按包名给行编号的,而
ClientModuleSystem.invalidate(id, rev)(HMR 的 rebuilt 帧 → entries.reload 走的正是它)
只按行 id 删工厂表里的条目:注册 id 与行 id 不一致 → 旧工厂删不掉 → 下一版 bundle 一执行就 client-modules: duplicate factory registration … (bundle executed twice without invalidate?),整条 web boot 变成「1 entry did not activate」并弹「应用无法启动或已意外停止」。
0.2.2 修的就是这个。bundle 另外加了一道幂等保护:该 id 已在工厂表里就跳过注册,重复执行最多是空转,不会再打挂启动。
卸载:删掉上面两处清单项,重启即可。
[!WARNING]
不要在 profile 的 node_modules 里给这个包留裸名链接/junction (例如 node_modules\dsh-skill-center → 本仓库)。链接和真正的 scoped 包会各自注册同一个插件,正好构成「同一插件两个来源」——也就是上面那个 duplicate registration 崩溃的现场。
只装 scoped 包,别的都清掉。
装完侧栏没出现?按顺序查这五步
1. 宿主半边活着吗:http://127.0.0.1:19387/dsh-skill-center/api?op=ping 应回 {"ok":true,…}。
回 404 说明 loader 行压根没挂上 —— 包不在 profile 的 dsh.profile.bundles 里,或者没装进 profile 的 node_modules。
2. 浏览器半边进模块图了吗:…/api?op=diagnose 里的 client.inGraph 必须是 true。
若 host.mounted 为 true 而它是 false,就是上面那个「行名 ≠ 包名」。
3. bundle 有没有被服务:浏览器只会拿到带版本号的地址(/plugins/@restregular/dsh-skill-center/client.js?rev=<12 位十六进制>),而路由故意拒绝 版本不匹配的请求 —— 所以即使一切正常,直接要裸路径也会回 404。请从 op=diagnose 的 client.url 取地址(现在它还会在 client.entryId 里告诉你命中哪一行),再取那个地址。
4. 跑离线合成自检(不用重启宿主):node verify-compose.mjs <profileDir>,现在它会因为「行名解析不回本包」直接 FAIL。
5. pnpm add 必须在 profile 目录里执行;装到别处(全局 npm i -g、别的目录)profile 合成不到。
它怎么接进 Harness
宿主半边(lib/index.js)挂载一条仅回环的 HTTP 路由 /dsh-skill-center/api:
- 用 ctx.skills 读目录,作用域解析与官方 ctx.remote.skills 同源三级兜底:活 Agent → 会话记录的预设作用域(agentPresets.acquireScope)→ 全局层。
前两级决定预设内挂载的 skill-filesystem 是否可见(只有全局层时只能看到内嵌技能)。
- 写操作只针对落在受管技能根里、真实存在于磁盘的文件: <projectRoot>/.dsh/skills、<projectRoot>/.agents/skills、$DSH_HOME/skills、$DSH_AGENTS_HOME/skills。
- frontmatter 改写是字符串级编辑,只动与调用策略相关的键,其余逐字保留(含行尾风格)。
- 受管根内的技能,list 的调用策略以磁盘上的文件为准,不照抄注册表快照:技能目录是缓存的,而 watcher 的失效事件总在写盘之后一小会儿才到 —— 只信快照的面板会在刚保存完的瞬间把界面弹回旧值,这正是「拨完开关又自己跳回去、手动刷新才变」的根因。
- 删除只 rename 进 .trash;回收站各操作独立:trash 列出条目、restore 把条目 rename 回原根 (目标已存在就失败,绝不覆盖)、deleteTrash 永久删除单条、emptyTrash 清空。回收站操作只解析工作目录、不读技能注册表,所以目录读取失败时它照样能用。
- 另注册一个内嵌技能提供方(ctx.skills.registerProvider,指向包内 skills/skill-authoring/)和 skill_create 工具(ctx.inject(['tools'], …) + ctx.tools.register)。
工具 schema 是手写的原始 JSON Schema,只使用 @deepseek-ai/dsh-tools 接受的子集 ——本包因此保持零运行时依赖,也不需要宿主侧的模块解析。
- 三块各自独立降级:缺 tools / webServer,或者随包技能的资产解析失败,都只写一行日志,其余部分照常挂载。
浏览器半边(client/client.js)注册两处槽位,不 require 任何 Harness Client 包:
ctx.slots.inject('sidebar.panellist', () => ctx.slots.register({
name: 'sidebar.panellist', id: 'skill-center', order: 1, // 1 → 排在 plugins(order 0)后面
label: () => t('panel'), locale: NS,
}, SkillCenterIcon));
ctx.slots.inject('main', () => ctx.slots.register({
name: 'main', key: 'skill-center', locale: NS, // main 是 keyed 槽,用 key }, Page));
样式只用宿主主题令牌 --dsw-alias- / --dsw-radius- / --dsw-focus-ring-, 带 data-plugin="dsh-skill-center"(避免被宿主的无主样式回收误删)。
开关的改动先落在组件状态里:开关的值取「本地覆盖 → 目录快照」的先后顺序,保存按钮的三态由「待保存集合 + 是否保存过」推出。保存之后本地覆盖会继续留着,直到某次 list 的快照真的与它一致(load() 只撤销已经追平的那些)——所以宿主的目录快照慢半拍也不会把界面弹回去。
作用域下拉是自定义浮层(不是原生 ),所以能按工作区分组、给分组标题做吸顶排版、标出正在运行的会话。候选同时读 sessions.list 与 workspaces.list:排除子 Agent 会话与 已归档会话与空会话(点过新建但没发消息)不列出,按宿主的工作区顺序分组,无归属的会话按 cwd 兜底归组,每个工作区最多列 12 个最近更新的会话。选中的 sessionId / cwd 带给每个请求(读目录、回收站、开关保存、删除、导入导出都一样)。「自动」保持原来的三步跟随(主区域会话 → 上次命中 → 最近更新);显式选择不更新那份缓存,所以切回「自动」会回到原来跟随的会话,而不是粘在手动选择上。
来源筛选与新建表单的写入位置复用同一个下拉组件。
整个客户端 bundle 包在 IIFE 里。各插件的 bundle 是同一页面里的经典脚本,共享一个全局词法作用域,而且这个作用域不会随插件卸载消失:页面开着时重装/升级插件,新 bundle 会在同一个 realm 里再执行一次,顶层 const MODULE_ID 会抛 Identifier 'MODULE_ID' has already been declared,整个脚本不执行、工厂注册不上,侧栏那一行要刷新页面才回来。client-smoke.mjs 现在会在同一个 vm 全局脚本上下文里执行两遍 bundle,防止这个回归。
API 示例
列目录(带会话作用域)
Invoke-RestMethod "http://127.0.0.1:19387/dsh-skill-center/api?op=list&sessionId=<session-id>"
关掉某技能的模型调用 $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))
非回环来源一律 403。
create 与模型工具 skill_create 共用一份实现:除 name / description / body / rootId 外,还接受 whenToUse、resources: [{ path, content }]、overwrite、modelInvocable、userInvocable。
回收站操作:GET ?op=trash(列全部条目)、POST {"op":"restore","rootId":"user-agents","entry":"<条目名>"} (条目名就是 .trash 下的直接子项名,形如 1792173890269-daylog)、
POST {"op":"deleteTrash","rootId":"user-agents","entry":"<条目名>"}(永久删这一条)、
POST {"op":"emptyTrash"}(不传 rootId 清空全部受管根,传了只清那一个)。
导入导出:POST {"op":"export","name":"<技能名>"} 回 filename / bytes / base64(ZIP);
POST {"op":"importSkill","base64":"<ZIP 或单个 .md 的 base64>","filename":"x.zip","rootId":"user-agents"}
写进指定根(默认 user-agents),越界路径与重名都会被拒绝。
开发
| 改动 | 生效方式 |
| client/client.js | 刷新页面即可(宿主的 /plugins/<id>/client.js 路由每次请求都读盘,文件 ctime/size 变化会合成新的 rev URL) |
| lib/index.js(宿主) | 需要重启宿主。模块源码热替换必须在启动前配置:profile patch 里给 hmr 行加 config: { root: ['.', '<本仓库绝对路径>'] };运行中改这项无效(见 @deepseek-ai/dsh-hmr README「before launching」) |
| package.json 的 dsh.* | 同样需要重启 |
自检脚本:
$env:ELECTRON_RUN_AS_NODE = 1
$exe = "C:\path\to\DeepSeek Harness.exe"; $here = "D:\path\to\dsh-skill-center"
客户端半侧:迷你 React 装载真实 bundle,并真的打本机宿主路由 $env:DSH_SMOKE_SESSION = "<session-id>"; & $exe "$here\client-smoke.mjs"
客户端半侧是否会被浏览器模块图接受(复现 dsh-client-modules 的合成校验)
& $exe "$here\verify-compose.mjs"
随包技能 + skill_create:纯离线,不碰宿主也不碰真实用户技能根
node "$here\verify-skill.mjs"
verify-compose.mjs 必须用 Electron 可执行文件跑:它要读 app.asar 里随发行版交付的 @deepseek-ai 包,普通 node 打不开 asar。verify-skill.mjs 正好相反 ——它用桩 ctx 直接 import lib/index.js,完全不需要宿主:检查随包技能契约(目录名 vs frontmatter、description 非空、resourceBase 目录真实存在、正文表格/链接里点到的 references/ 等路径是否真的存在)、检查工具 schema 是否落在 @deepseek-ai/dsh-tools 支持的子集内, 然后真的跑一遍 skill_create:新建、资源落盘、覆盖、重名、非法名、路径穿越、平坦文件、显式项目根,以及三条降级路径;另外还断言:受管根技能的调用策略取自磁盘文件,即使注册表快照给的是旧值;最后把回收站整条链路真跑一遍:delete 之后 trash 能列出条目(带原根), restore 能把目录包和平坦 .md 都移回去,路径穿越的 entry 与目标已存在都被拒绝, deleteTrash 只删指定那一条(对已不存在的条目报错),emptyTrash 清掉剩下的; 最后跑一遍导入导出往返:export 回一个真正的 ZIP,删掉技能后用同一份 base64 importSkill 能还原 SKILL.md 与资源,重复导入报「已存在」,裸 .md 导成平坦技能,套一层目录的 ZIP 会剥层, 带 ../ 的 ZIP 被拒绝且不落盘。
client-smoke.mjs 用迷你 React 把真实面板渲染在真宿主回环路由上,再驱动「暂存 + 保存」全流程: 拨开关不发任何写请求、点保存只发一次带两个策略字段的 setInvocation、按钮按未修改 → 保存 → 已保存 走(再改一次回到保存)、宿主目录快照慢半拍也弹不回开关; 然后验证会话作用域下拉:有未保存改动时禁用并提示先保存、打开后选项里同时有「自动」和两个会话且按工作区分组、夹具里的已归档与空会话都不出现、切到第二个会话后下一次 list 与一次 export 都带上它的 sessionId/cwd、切回「自动」回到原来的跟随会话;并在同一个 vm 全局脚本上下文里把 bundle
执行两遍(模拟页面开着时重装),第二遍既不抛
重声明错误、也不产生第二次注册;还走一遍删除确认:「取消」出现在「确认删除?」左边、点取消退回「删除」且不发任何 delete 请求;新建表单里「创建」旁的「取消」能关掉表单并回到详情;导出发一次 export 并触发 .zip 下载,导入选中文件后发一次带文件名与非空 base64 的 importSkill; 接着走回收站流程(trash / restore / deleteTrash / emptyTrash 全部走桩):打开回收站列出条目、 恢复只发一次带 rootId + entry 的 restore 且回收站变空、退出再进入会重新读, 详情区 删除 要先过「确认删除?」且「取消」在左边(取消不发请求,确认才发一次 deleteTrash), 顶部 清空回收站 同样先过「确认清空?」(确认后才发一次 emptyTrash)。
它拦下了所有 POST(create/delete 直接抛错),所以跑一遍不会改动任何真实 SKILL.md。
搬迁/改名注意:客户端模块表按 (loader baseUrl, loader 行名) 缓存包元信息,不含文件时间戳。
把包挪到别处后,运行中的宿主仍握着旧路径;浏览器端用的是激活时读进内存的快照,所以刷新页面照常可用,下次重启宿主才会解析新路径。
发版
改 version → 提交 → 推送 → 在仓库根用有该 scope 发布权限的 token 执行 npm publish。
注册表对成功的发布回 202 {"success":true},随后该版本进入 validating 阶段:验证通过之前, /@restregular/dsh-skill-center/<版本> 与 tarball 地址都回 404,dist-tags 仍指向上一个版本。
这是正常窗口,不是失败。用带时间戳参数的版本地址重新探测(?t=<ticks>)即可,不要重复发布:同一版本再发一次会回 E409 Cannot publish over previously staged version <版本>(npm stage publish 则回 E409 Cannot stage previously published version <版本>),而 npm stage list 可能什么都不显示。
已知边界
- 面板占中央区域时,右栏的 dsh-resource:// 预览不可用(宿主规定:无会话面时 sidebarRight.openResource 抛错)。
因此正文预览走本插件自己的路由,在面板内渲染原文。
- 只能管理文件系统来源的技能;插件内嵌技能只读。
- 没有选中会话时退化为「按工作目录读取」,此时只能看到内嵌技能 + 该 cwd 的项目技能;面板底栏会显示实际使用的 cwd 与作用域级别。
- 作用域下拉按工作区分组,已归档会话与空会话都不列出;每个工作区最多 12 个最近更新的会话。更早的会话仍可通过 API 传 sessionId 访问。
- 目录整体替换制:一次开关改动会让模型的技能目录消息整条重发(这是 Harness 自身的设计)。
- skill_create 是宿主级全局注册:每个 Agent 都会多看到一份工具 schema,没有按 Agent 的开关,只有卸载插件才会消失。
- 随包 skill-authoring 是内嵌技能,面板里只读。它排在 rank 600,所以同名项目级/用户级技能会整体盖过它 —— 这正是定制它的正道。
许可
MIT预览分享卡片长什么样 ↓
分享 · DeepSeek Harness Hub
RestRegular/dsh-skill-center
DeepSeek Harness 技能中心插件:浏览并管理当前会话可见的技能 / Skill Center…
相关插件 · 其他
模型破甲清洗YuJunZhiXue/dsh-purge
⚔️ DeepSeek Harness 网络安全红队工具dsh-purge
★ 14,822已汉化DeepSeek 客户端
73
逆向分析桥接Fanqi89/dsh-rea
REA(Reverse Engineer Anything)MCP 桥接,用来把 REA 逆向工具接入 DSH 插件
★ 51已汉化DSH
72
红队越狱测试套件Minglink/dsh-infinite-gen-5
DeepSeek v4.1 flash 逆向工具无限五代 求 Star 收藏 ⭐欢迎大家提交项目的改进
★ 3,759已汉化DSH
71.4
语音女友beiyege-01/dsh-voice-ai-girlfriend
语音 AI 女友Voice AI girlfriend for DeepSeek Harness:Whisper…
★ 718已汉化DSH
71.2
协同办公空间dream-num/univer-workspace
一个让人类与 AI Agent 共同创作、协作和审阅的开源 Office 工作空间。
★ 2,261已汉化DSH
70.7
破甲评测WoYun520/dsh-purge
在本机分离宿主拦截与模型拒绝,做受控红队评测
★ 199已汉化DSH
69.7