🎁 福利专区全网大模型免费应用 + 新用户福利 + 注册活动入口,低成本玩转 AI

技能面板 huanlin-labs/dsh-skill-cockpit

DeepSeek Harness
huanlin-labs1 天前有提交已提供中文文档扫描:含敏感能力

诚实边界。 它不是沙箱:只管理 DSH 自己暴露的技能根,只写 /.dsh/skills 与…

33.6/100
综合分
5
GitHub 星标
—
npm 周下载
—/5
用户评分

数据截至 2026/10/31 · 周下载取自 npm 全网(含 CI / 镜像),非本站安装数;综合分由星标、活跃度与兼容性检查合成。

安装命令 · 需先装 dsh CLI 引擎
dsh plugin --profile web add huanlin-labs/dsh-skill-cockpit
该包不可直接 npm 安装,改从 GitHub 源安装(明细见下方安装检查)

状态速览

判定依据:实装验证 · 静态安装检查 · 自动扫描 · 仓库维护状态
信任档位:已验证
是什么
dsh 原生插件 · market
装得上吗
本站已在 dsh 0.2.1-rc.2 下真实安装成功
非静态推断
L5 · 运行时冒烟等级说明 →
安全吗
本站尚未对该插件做风险分级
暂未覆盖,不等同于无风险
还在维护吗
活跃:最近一次提交在 1 天前
档位由哪些信号合成

本站实装验证(真实安装 L4 覆盖全站;L5 为运行时冒烟或实机截图,覆盖范围小于 L4) · 验证所用 dsh 版本 · 静态安装检查 · 风险分级 · 仓库维护状态。 下方各区块是它的证据明细。

验证判据与等级说明 →
社区实装还没有人打卡 · 装过的第一个就是你
安全扫描自动 · 静态分析源码
v2.7.0
🔵 扫描:含敏感能力尚无人工评估
展开查看扫描说明与证据 ↓

静态扫描发现插件具备触达本地系统的能力:执行外部命令、读写/删除本地文件、读取剪贴板或密钥类环境变量(至少其一)。这是能力清单,不是危险判定——具体能力项与证据见下方,安装前请自行判断是否需要。

可执行外部命令读写/删除本地文件写入剪贴板(不计风险)发起外部网络请求
查看扫描证据(共 201 处,展示前 138 条)
  • 读取本地文件
    lib/index.js:122
    if (existsSync(agentsSkills)) roots.push({ source: 'user-agents', path: agentsSkills });
  • 读取本地文件
    lib/index.js:185
    if (existsSync(localPath)) {
  • 读取本地文件
    lib/index.js:186
    local = JSON.parse(readFileSync(localPath, 'utf8'));
  • 发起网络请求
    lib/client.js:1191
    const res = await fetch(path, init)
  • 写入剪贴板
    lib/client.js:2020
    navigator.clipboard.writeText(row.filePath)
  • 写入剪贴板
    lib/client.js:4459
    if (navigator.clipboard) navigator.clipboard.writeText(text)
  • 读取本地文件
    bin/cockpit-doctor.mjs:20
    const PKG = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), '..', 'package.json'), 'utf8'))
  • 读取本地文件
    bin/cockpit-doctor.mjs:50
    if (!existsSync(baseHome)) {
  • 写入本地文件
    bin/cockpit-doctor.mjs:51
    if (doFix) mkdirSync(baseHome, { recursive: true })
  • 读取本地文件
    bin/cockpit-doctor.mjs:54
    statSync(baseHome)
  • 读取本地文件
    bin/cockpit-doctor.mjs:71
    localConfig = JSON.parse(readFileSync(localConfigPath, 'utf8'))
  • 读取本地文件
    bin/cockpit-doctor.mjs:73
    if (existsSync(localConfigPath)) localConfigError = error.message
  • 读取本地文件
    bin/cockpit-doctor.mjs:78
    } else if (existsSync(localConfigPath)) {
  • 写入本地文件
    bin/cockpit-doctor.mjs:117
    mkdirSync(stateDir, { recursive: true })
  • 写入本地文件
    bin/cockpit-doctor.mjs:119
    writeFileSync(probe, 'ok')
  • 删除本地文件
    bin/cockpit-doctor.mjs:120
    rmSync(probe, { force: true })
  • 写入本地文件
    bin/cockpit-doctor.mjs:135
    mkdirSync(userSkills, { recursive: true })
  • 写入本地文件
    bin/cockpit-doctor.mjs:260
    mkdirSync(dirname(resolve(reportPath)), { recursive: true })
  • 写入本地文件
    bin/cockpit-doctor.mjs:261
    writeFileSync(resolve(reportPath), JSON.stringify(payload, null, 2) + '\n', 'utf8')
  • 读取本地文件
    lib/scan.js:63
    const info = statSync(fullPath);
  • 读取本地文件
    lib/scan.js:74
    return { text: readFileSync(path, 'utf8'), bytes: statSync(path).size };
  • 读取本地文件
    lib/scan.js:74
    return { text: readFileSync(path, 'utf8'), bytes: statSync(path).size };
  • 读取本地文件
    lib/scan.js:162
    entries = readdirSync(root.path, { withFileTypes: true });
  • 读取本地文件
    lib/scan.js:180
    const live = existsSync(livePath);
  • 读取本地文件
    lib/scan.js:300
    return existsSync(parked) ? parked : undefined;
  • 读取本地文件
    lib/toggle.js:67
    assert(existsSync(abs), `文件不存在:${abs}`);
  • 读取本地文件
    lib/toggle.js:68
    const info = lstatSync(abs);
  • 读取本地文件
    lib/toggle.js:123
    assert(!existsSync(dest), `拒绝:目标已存在,避免覆盖:${dest}`);
  • 读取本地文件
    lib/toggle.js:142
    const before = statSync(target);
  • 写入本地文件
    lib/toggle.js:143
    renameSync(target, dest);
  • 读取本地文件
    lib/toggle.js:144
    const after = statSync(dest);
  • 读取本地文件
    lib/toggle.js:213
    assert(existsSync(abs), `文件不存在:${abs}`);
  • 写入本地文件
    lib/toggle.js:293
    if (workingPath !== target) renameSync(target, workingPath);
  • 写入本地文件
    lib/toggle.js:295
    copyFileSync(workingPath, backup);
  • 写入本地文件
    lib/toggle.js:299
    writeFileSync(tmp, next.text, 'utf8');
  • 写入本地文件
    lib/toggle.js:300
    renameSync(tmp, workingPath);
  • 读取本地文件
    lib/agent-presets.js:28
    if (!existsSync(filePath)) return out
  • 读取本地文件
    lib/agent-presets.js:31
    text = readFileSync(filePath, 'utf8')
  • 读取本地文件
    lib/agent-presets.js:57
    if (!existsSync(file)) continue
  • 读取本地文件
    lib/agent-presets.js:59
    const head = readFileSync(file, 'utf8').slice(0, 8192)
  • 读取本地文件
    lib/agent-presets.js:80
    if (!existsSync(root)) return { ok: true, root, presets: [], pool }
  • 读取本地文件
    lib/agent-presets.js:82
    for (const dirent of readdirSync(root, { withFileTypes: true })) {
  • 写入本地文件
    lib/agent-presets.js:151
    if (!existsSync(skillsDir)) mkdirSync(skillsDir, { recursive: true })
  • 写入本地文件
    lib/agent-presets.js:187
    mkdirSync(pool, { recursive: true })
  • 写入本地文件
    lib/agent-presets.js:190
    renameSync(src, dest)
  • 删除本地文件
    lib/agent-presets.js:209
    process.platform === 'win32' ? rmdirSync(linkPath) : unlinkSync(linkPath)
  • 删除本地文件
    lib/agent-presets.js:209
    process.platform === 'win32' ? rmdirSync(linkPath) : unlinkSync(linkPath)
  • 读取本地文件
    lib/skill-zh.js:28
    const parsed = JSON.parse(readFileSync(file, 'utf8'));
  • 读取本地文件
    lib/undeclared.js:62
    if (!existsSync(indexPath)) {
  • 读取本地文件
    lib/undeclared.js:65
    const info = statSync(indexPath);
  • 读取本地文件
    lib/undeclared.js:82
    text = readFileSync(status.path, 'utf8');
  • 读取本地文件
    lib/usage.js:85
    if (!existsSync(root)) return [];
  • 读取本地文件
    lib/usage.js:91
    entries = readdirSync(dir, { withFileTypes: true });
  • 读取本地文件
    lib/usage.js:103
    const info = statSync(full);
  • 读取本地文件
    lib/usage.js:119
    buf = readFileSync(file);
  • 读取本地文件
    lib/usage.js:289
    if (!storePath || !existsSync(storePath)) return { files: [], skills: [] };
  • 读取本地文件
    lib/usage.js:291
    const parsed = JSON.parse(readFileSync(storePath, 'utf8'));
  • 写入本地文件
    lib/usage.js:305
    mkdirSync(dirname(storePath), { recursive: true });
  • 写入本地文件
    lib/usage.js:306
    writeFileSync(storePath, `${JSON.stringify(store, null, 2)}\n`, 'utf8');
  • 读取本地文件
    lib/manage-write.js:56
    if (!existsSync(abs)) throw new Error(`文件不存在:${abs}`)
  • 读取本地文件
    lib/manage-write.js:57
    const info = lstatSync(abs)
  • 读取本地文件
    lib/manage-write.js:69
    const raw = readFileSync(file)
  • 写入本地文件
    lib/manage-write.js:138
    writeFileSync(tmp, text, 'utf8')
  • 写入本地文件
    lib/manage-write.js:140
    renameSync(tmp, target)
  • 删除本地文件
    lib/manage-write.js:142
    rmSync(tmp, { force: true })
  • 读取本地文件
    lib/manage-write.js:159
    if (!existsSync(rootAbs)) mkdirSync(rootAbs, { recursive: true })
  • 写入本地文件
    lib/manage-write.js:159
    if (!existsSync(rootAbs)) mkdirSync(rootAbs, { recursive: true })
  • 读取本地文件
    lib/manage-write.js:160
    if (!lstatSync(rootAbs).isDirectory()) throw new Error(`技能根不是目录:${rootAbs}`)
  • 读取本地文件
    lib/manage-write.js:163
    if (existsSync(dir)) throw new Error(`同名技能已存在:${dir}(换一个名字,或去列表里编辑它)`)
  • 写入本地文件
    lib/manage-write.js:165
    mkdirSync(dir)
  • 删除本地文件递归
    lib/manage-write.js:170
    rmSync(dir, { recursive: true, force: true })
  • 写入本地文件
    lib/manage-write.js:195
    writeFileSync(backup, before, 'utf8')
  • 写入本地文件
    lib/manage-write.js:216
    mkdirSync(trashRoot, { recursive: true })
  • 删除本地文件递归
    lib/manage-write.js:222
    rmSync(target, { recursive: true, force: false })
  • 删除本地文件递归
    lib/manage-write.js:252
    rmSync(target, { recursive: true, force: true })
  • 读取本地文件
    lib/manage-trash.js:34
    if (!existsSync(dir)) throw new Error(`回收站里没有这一条:${text}`)
  • 读取本地文件
    lib/manage-trash.js:41
    if (!existsSync(file)) return fallback
  • 读取本地文件
    lib/manage-trash.js:42
    const text = readFileSync(file, 'utf8')
  • 读取本地文件
    lib/manage-trash.js:55
    for (const item of readdirSync(current, { withFileTypes: true })) {
  • 读取本地文件
    lib/manage-trash.js:60
    try { bytes += statSync(full).size } catch { }
  • 读取本地文件
    lib/manage-trash.js:70
    if (!existsSync(trashRoot)) {
  • 写入本地文件
    lib/manage-trash.js:109
    mkdirSync(targetRoot, { recursive: true })
  • 写入本地文件
    lib/manage-trash.js:130
    chmodSync(p, 0o777)
  • 写入本地文件
    lib/manage-trash.js:132
    chmodSync(p, 0o666)
  • 删除本地文件递归
    lib/manage-trash.js:165
    rmSync(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 })
  • 读取本地文件
    lib/vault.js:54
    if (!existsSync(listPath)) {
  • 读取本地文件
    lib/vault.js:59
    text = readFileSync(listPath, 'utf8').replace(/^ FEFF/, '')
  • 读取本地文件
    lib/vault.js:105
    if (!existsSync(full)) continue
  • 读取本地文件
    lib/vault.js:107
    const text = readFileSync(full, 'utf8').replace(/^ FEFF/, '')
  • 读取本地文件
    lib/vault.js:133
    items = readdirSync(current, { withFileTypes: true })
  • 读取本地文件
    lib/vault.js:142
    bytes += statSync(full).size
  • 写入本地文件
    lib/vault.js:154
    mkdirSync(dirname(listPath), { recursive: true })
  • 写入本地文件
    lib/vault.js:167
    writeFileSync(listPath, text, 'utf8')
  • 执行外部命令固定命令
    lib/vault.js:200
    const result = spawnSync('git', ['clone', '--depth', '1', '--quiet', url, tmp], {
  • 删除本地文件递归
    lib/vault.js:206
    rmSync(tmp, { recursive: true, force: true })
  • 删除本地文件递归
    lib/vault.js:213
    rmSync(tmp, { recursive: true, force: true })
  • 写入本地文件
    lib/vault.js:231
    mkdirSync(targetRoot, { recursive: true })
  • 删除本地文件递归
    lib/vault.js:242
    rmSync(tmp, { recursive: true, force: true })
  • 读取本地文件
    lib/parked.js:17
    if (!existsSync(root)) return [];
  • 读取本地文件
    lib/parked.js:19
    for (const entry of readdirSync(root, { withFileTypes: true })) {
  • 读取本地文件
    lib/parked.js:26
    for (const e of readdirSync(dir, { withFileTypes: true })) {
  • 读取本地文件
    lib/parked.js:31
    bytes += statSync(q).size;
  • 读取本地文件
    lib/parked.js:41
    deletedAt = statSync(p).mtime.toISOString().replace('T', ' ').slice(0, 16);
  • 读取本地文件
    lib/parked.js:54
    entries = readdirSync(root, { withFileTypes: true });
  • 写入本地文件
    lib/parked.js:63
    chmodSync(p, 0o777);
  • 写入本地文件
    lib/parked.js:65
    chmodSync(p, 0o666);
  • 删除本地文件递归
    lib/parked.js:77
    rmSync(dir, { recursive: true, force: true, maxRetries: 10, retryDelay: 300 });
  • 读取本地文件
    lib/manage-repair.js:48
    if (!existsSync(filePath)) throw new Error(`文件不存在:${filePath}`)
  • 读取本地文件
    lib/manage-repair.js:49
    const raw = readFileSync(filePath, 'utf8')
  • 读取本地文件
    lib/manage-repair.js:131
    writeFileSync(backup, readFileSync(filePath))
  • 写入本地文件
    lib/manage-repair.js:131
    writeFileSync(backup, readFileSync(filePath))
  • 写入本地文件
    lib/manage-repair.js:133
    writeFileSync(tmp, plan.text, 'utf8')
  • 写入本地文件
    lib/manage-repair.js:134
    renameSync(tmp, filePath)
  • 读取本地文件
    lib/manage-backup.js:24
    for (const name of readdirSync(dir)) {
  • 读取本地文件
    lib/manage-backup.js:26
    const st = statSync(full)
  • 读取本地文件
    lib/manage-backup.js:41
    if (!existsSync(filePath)) throw new Error(`文件不存在:${filePath}`)
  • 读取本地文件
    lib/manage-backup.js:44
    if (existsSync(dest)) throw new Error(`拒绝:备份目标已存在:${dest}`)
  • 写入本地文件
    lib/manage-backup.js:45
    mkdirSync(dirname(dest), { recursive: true })
  • 读取本地文件
    lib/manage-backup.js:56
    if (!existsSync(root)) continue
  • 读取本地文件
    lib/manage-backup.js:57
    for (const entry of readdirSync(root, { withFileTypes: true })) {
  • 写入本地文件
    lib/manage-backup.js:69
    mkdirSync(dest, { recursive: true })
  • 写入本地文件
    lib/manage-template.js:37
    mkdirSync(join(full, '..'), { recursive: true })
  • 写入本地文件
    lib/manage-template.js:38
    writeFileSync(full, files[rel], 'utf8')
  • 读取本地文件
    lib/roots.js:138
    if (root.path !== null) root.exists = existsSync(root.path);
  • 读取本地文件
    lib/tools.js:211
    const refresh = args.refresh === true || !existsSync(storePath);
  • 读取本地文件
    lib/autoload-runtime.js:34
    if (!existsSync(file)) return normalizeSettings(fallback);
  • 读取本地文件
    lib/autoload-runtime.js:35
    return normalizeSettings({ ...(fallback && typeof fallback === 'object' ? fallback : {}), ...JSON.parse(readFileSync(file, 'utf8')) });
  • 写入本地文件
    lib/autoload-runtime.js:46
    mkdirSync(dirname(file), { recursive: true });
  • 写入本地文件
    lib/autoload-runtime.js:47
    writeFileSync(file, `${JSON.stringify(next, null, 2)}\n`, 'utf8');
  • 读取本地文件
    lib/autoload-runtime.js:61
    const st = statSync(entryPath);
  • 读取本地文件
    lib/autoload-runtime.js:65
    const body = stripFrontmatter(readFileSync(entryPath, 'utf8'));
  • 写入本地文件
    lib/autoload-runtime.js:143
    mkdirSync(dirname(file), { recursive: true });
  • 写入本地文件
    lib/autoload-runtime.js:144
    writeFileSync(file, `${JSON.stringify(payload, null, 2)}\n`, 'utf8');
  • 读取本地文件
    lib/gloss.js:9
    const parsed = JSON.parse(readFileSync(GLOSS_FILE, 'utf8'))
  • 写入本地文件
    lib/gloss.js:22
    mkdirSync(DIR, { recursive: true })
  • 写入本地文件
    lib/gloss.js:23
    writeFileSync(GLOSS_FILE, JSON.stringify({ version: 1, items }, null, 2) + '\n', 'utf8')
  • 执行外部命令固定命令
    lib/translate.js:28
    const raw = execFileSync('reg', ['query', key], { encoding: 'utf8', timeout: 3000, windowsHide: true })
  • 发起网络请求
    lib/translate.js:51
    const req = http.request({
扫描于 2026/10/31(1 天前) · 范围 入口闭包 + 发布文件清单 · 来源 GitHub 仓库

静态扫描只反映源码中出现的调用,不构成安全背书;人工评估与自动扫描相互独立、互不覆盖。

给这个插件打分还没有人投票↓
用户评分
还没有人投票,来当第一个

DSH 的技能驾驶舱:应用级导航里一页列出本机暴露的所有技能、某个技能为何不可见,以及单文件开关。(早期 / 开发中)

README

由 DeepSeek 最新模型翻译生成
dsh-skill-cockpit v1.5.1 · 已发布 npm dsh-skill-cockpit@1.5.1 | npm test 19/19 套件全绿 | npm run verify:install 13/13(pack → 空目录安装 → 用装机副本启动 → /ping 读回版本)| MIT 源码 vs npm:1.5.1 已发布,源码与 npm 包一致;改动见 CHANGELOG.md 顶部。 诚实边界。 它不是沙箱:只管理 DSH 自己暴露的技能根,只写 ~/.dsh/skills 与 ~/.agents/skills;管不了别的进程、手工改动,也管不了绕过 DSH 的调用。CLI / doctor / verify:install 在本机 Windows 有自动化实测;源码里新加的宿主半守卫要重启 dsh 才生效,重启后已确认左侧入口与主区页面槽位 active,但未做逐按钮的视觉复验,视觉层仍按「未验证」看待。CI 已在 ubuntu / macOS / windows × Node 20/22/24 上跑全量套件与 verify:install。 DSH(DeepSeek Harness)技能面板:在左上应用级导航排加一个「技能」入口,点开是主区一整页,列出本机全部技能、按技能开关,并接「未安装技能索引」做第二跳。
展开完整文档(README 剩余 1 节)
dsh-skill-cockpit

v1.5.1 · 已发布 npm dsh-skill-cockpit@1.5.1 | npm test 19/19 套件全绿 | npm run verify:install 13/13(pack → 空目录安装 → 用装机副本启动 → /ping 读回版本)| MIT

源码 vs npm:1.5.1 已发布,源码与 npm 包一致;改动见 CHANGELOG.md 顶部。

诚实边界。 它不是沙箱:只管理 DSH 自己暴露的技能根,只写 ~/.dsh/skills 与 ~/.agents/skills;管不了别的进程、手工改动,也管不了绕过 DSH 的调用。CLI / doctor / verify:install 在本机 Windows 有自动化实测;源码里新加的宿主半守卫要重启 dsh 才生效,重启后已确认左侧入口与主区页面槽位 active,但未做逐按钮的视觉复验,视觉层仍按「未验证」看待。CI 已在 ubuntu / macOS / windows × Node 20/22/24 上跑全量套件与 verify:install。
DSH(DeepSeek Harness)技能面板:在左上应用级导航排加一个「技能」入口,点开是主区一整页,列出本机全部技能、按技能开关,并接「未安装技能索引」做第二跳。

- 挂载点 = 官方 slots 扩展点的两个座位(不再走第三方 dsh-better-sidebar):
1. sidebar.panellist(root 作用域的 list,即左上应用级导航排)→ 图标行,order: 5;
2. main(root 作用域的 keyed)→ 主区整页,key 与行 id 同名(skill-panel)。
- 顺序严格夹在「插件」(order 0) 与「自动化任务」(order 10) 之间 —— 外壳按 order 升序排(sort((a,b)=>a.order-b.order)),5 落在唯一的空档里,与加载先后无关。
- 因此本包不声明任何 @deepseek-ai/dsh peer ⇒ 不受 DSH 兼容性豁免闸门影响(见工作区 AGENTS.md §四 铁律 1/3)
- 纯 ESM JavaScript,无构建链:lib/.js 直接可跑,自测用 node 即可复跑

自查:dsh-skill-cockpit-doctor

npx dsh-skill-cockpit-doctor # 面板没出现?查:状态目录 / 技能根 / 装配 / 译文
npx dsh-skill-cockpit-doctor --json # 给机器读
npx dsh-skill-cockpit-doctor --home <dir> --fix # 路径全重定向到临时 home;只补能安全补的
npx dsh-skill-cockpit-doctor --dry-run # 只说 --fix 会做什么,不落盘
npx dsh-skill-cockpit-doctor --report out.json # 同一份 JSON 写到文件(只读,不改状态)

退出码:0 就绪 · 1 有要修的 · 2 环境问题。

平台矩阵(谁验过、谁没验):docs/platforms.md。
交付前自检(哪条打了勾、证据是什么、哪条还没做):docs/self-check.md。
出问题了(检查项对照表 + 三个常见现象):docs/troubleshooting.md。
哪些 backlog 已实现、哪些是真缺(逐条给证据):docs/status-ledger.md。

诚实边界(先看这段,再看下面)

- 只在 Windows 桌面版 DSH 真机验证过。 本文所有结论都来自 Windows 11 上的真实 DSH 桌面安装。macOS / Linux 的语法检查、全量套件与 verify:install 已在 CI 里跑过;也没有人在那些平台跑过真实 DSH 会话。
- 全量自测在 CI 里覆盖 ubuntu + macOS + windows。 CI 通过只是自动化证据,不等于那些平台上的真实 DSH 会话已验证。
- 翻译用的是免费公开接口(优先 MyMemory,失败退 Google)。它们会限流、会不打招呼地改;一次批量翻译可能少于请求条数。除这两家之外不发任何数据,而且只在你按下「翻译」时才发。
- 你让它改,它才改技能文件。 一键修会重写你点的那份技能的 frontmatter(必先写 .bak-<时间戳> 备份),且不编内容:只补 entry 名、用该技能自己的正文补缺失/过短的描述、给会破坏 YAML 的冒号加引号。修不了时会明确说原因,不再静默。
- 它不是沙箱。 它读 DSH 自己解析的六个技能根(project-dsh / project-agents / custom / user-dsh / user-agents / bundled),只写 ~/.dsh/skills 与 ~/.agents/skills;别的 harness 目录一概不碰。
- 调用次数只是下界。 数字来自解析会话日志里的 tool-call(name: "skill");更早的日志形状或从别的入口调用的技能会读成 0。

安装

三步,每步都有可对照的输出。

① 装包

npm i dsh-skill-cockpit # 或 pnpm add / yarn add

期望:node_modules/dsh-skill-cockpit/package.json 里的 version 与你装的版本一致。

② 挂进 profile(这步归 DSH,插件不替你改配置)

在 ~/.dsh/profiles/<你的 profile>/package.json 的 dependencies 里加 "dsh-skill-cockpit": "^1.5.0"(或你装的版本),或在 cordis.patch.yml 用 insert: 挂一行。本机开发可直接写 "dsh-skill-cockpit": "link:D:/插件/dsh-skill-panel"。

期望:node -e "console.log(require('./package.json').dependencies)" 的输出里能看到这一条。

③ 重启 DSH,然后自查

npx dsh-skill-cockpit-doctor

期望:最后一行是「一切就绪」,左侧导航「插件」与「自动化任务」之间出现「技能」。
没出现就看 --json 里第一个不是 ok 的项:装配那项回第 ② 步;状态目录 / 技能根那项跑 --fix。
同时给 dsh 一个工具:skill_panel(20 个动作,覆盖面板全部能力)——基础 11 个:list / read / create / update / delete / toggle / repair / backup / trash_list / trash_restore / trash_purge;二期 9 个:后备库 vault_list / vault_put / vault_install、DSH 预设 preset_list / preset_mount / preset_unmount、批量四态 mode_set、usage、checkup。
以后「把某技能停掉」「新建一个技能」「这套技能全关掉」「最近谁在被调用」这类话,模型直接调工具改磁盘,不必点面板;破坏性动作必须显式 confirm: true。

它解决什么

~/.dsh/skills 只是六类技能根中的一类。只扫它 → 面板少报技能(本机实测 31 vs 会话可见 39)。
更要紧的是:frontmatter 写坏 = 运行期静默丢弃(dsh-skill-filesystem 解析失败即 continue),用户看到的是"这个技能不存在",而不是"这个技能写坏了"。

本面板:

| 能力 | 说明 |
| 六类根全枚举 | project-dsh(100) / project-agents(200) / custom(300) / user-dsh(400) / user-agents(500) / bundled(600),各带 source + rank |
| 坏 frontmatter 显式报错 | dropped:no-frontmatter / dropped:unclosed / dropped:bad-yaml / dropped:missing-fields / dropped:bad-name,每条带原因;"丢了"就显示"丢了" |
| 根不存在也算一种状态 | 显示"不存在(正常)";bundled 未挂载时显示 $DSH_BUNDLED_SKILL_DIR 未设置,而不是假装有 |
| 每技能开关 | 唯一写盘动作 = 改入口文件名:SKILL.md ↔ SKILL.md.disabled(扁平型 x.md ↔ x.md.disabled) |
| 会话作用域 | 面板顶部显示"当前会话 cwd / 项目根";项目级根只读(写工作区 = 绕过"工作区=权限域"边界) |
| 第二跳 | 命中「未安装技能索引」时给信息卡 + 复制按钮,不下载、不安装 |

和同类比(2026-10-08 源码级实查)

生态里做技能面板的不止一家。我们不求「功能最多」,只把三条讲清楚——都是可验证的机制差异,不是形容词:

| 维度 | 本插件 | cheshireez/dsh-skill-hub | re-ITRT/dsh-skill-mcp-panel | VDERR/dsh-echocat-skill-panel |
| 挂载面 | 左上导航「技能」整页(sidebar.panellist order 5) | 设置页「技能中枢」 | 侧栏「技能与 MCP」双 tab | 横栏 + 左栏 + 中栏报告 |
| 启停机制 | 改入口文件名 SKILL.md ↔ .disabled(唯一写盘动作) | 改名 + sidecar 对账 | 不碰文件:写 overlay JSON + overlay provider | 把技能目录移出监视目录 |
| 卸载本插件后 | 状态保持(文件名就是状态) | 保持禁用(启动时对账) | 全部失效(状态在插件自己的 storage) | 保持禁用 |
| 给模型的接口 | 有:skill_panel 20 个动作(模型可直接改,不必点面板) | 未见模型工具 | 未见模型工具 | 未见模型工具 |
| 依赖与体积 | 零依赖 / 零 peer,无构建链 | TS 构建 | 无构建链 | 无构建链 |
| 技能根覆盖 | 六类根全扫,并解释「某技能为何不可见」;项目级只读 | 诊断 + 一键修复(这条它比我们多走一步) | — | — |

口径:同类三列来自 2026-10-08 对各自仓库源码的实查;同类插件迭代快,以它们自己的仓库为准。
我们不主张「独占导航席位」——@linxin666/dsh-client-ui-skill-explorer 也挂 sidebar.panellist(2026-10-20 实查更正)。

开 = 自动加载

干什么:每轮对话开始前(agent/pre-step),拿你这句话去和各技能的 description 做词面匹配;命中的技能把正文直接放进本轮上下文(系统提醒)——模型不用先想起「去读一下技能」,你也不用写规则。

总开关 + 三档(工具栏右侧;只有「已开」时三档才有意义):

| 档 | 相关分阈值 | 描述至少命中几个词 | 单技能正文上限 |
| 严格 | ≥ 4.0 | 2 | 4 KB |
| 标准(默认) | ≥ 2.5 | 1 | 8 KB |
| 宽松 | ≥ 1.5 | 1 | 16 KB |

三档共同上限:单轮最多 3 个技能、总预算 24 KB。
同一技能同一会话只注入一次;</system-reminder> 一律转义;出任何异常只跳过该轮,不影响正常对话。

每行的「必用」(pin):不看相关度,直接排进每轮注入最前(仍受单技能上限与总预算约束);
技能拨到「关」时 pin 不生效。必用 / 不用名单存在 ~/.dsh/skill-cockpit/autoload.json(pinned / perSkillOff)。

开了的代价(实测,不粉饰):

- 占上下文:命中即注入正文。pdf 这类 7.9 KB ≈ 2–3k token;宽松档最坏 24 KB ≈ 6–8k token/轮,且每轮都占注意力。
- 会误注入:真实技能集实测 1/10(10 条本不该命中的查询里 1 条被塞)。
- 命中不等于执行:真实集 recall 7/10(70%),注入了模型仍可能不照做。
- 会互相挤:单轮只装 3 个;pin 多了会把真正相关的技能挤出预算。
- 严格档近乎不触发;宽松档噪音大。

建议:默认「标准 + 已开」;只做文档/论文那类活时开;pin 控制在 1–2 个。
关掉后技能仍在,模型需要时仍可自己调用——只是不再自动塞正文。

为什么不是 100%:宿主没有公开的「插件往系统提示写入」通道,能做到的上限是"每轮把正文塞进上下文",模型仍可能不照做。所以对外口径是高置信触发,不是"强制/保证"。工具 skill_autoload: action=on|off / tier / except / pin / unpin / status / probe / eval。

四个页面

面板是导航里的一页,顶部四个视图:

| 视图 | 干什么 |
| 列表 | 全部技能一处看:名字、来源根标签、功能标签、描述、体积、单技能开关、「看正文」读入口文件 |
| 分组 | 自己摆的看板 —— 从左侧把卡片拖进右边某个组;六个内置功能分组(文档办公 · 代码工程 · 设计视觉 · 写作沟通 · 研究资料 · 元技能)+ 自建组,可改名、可删 |
| 用量 | 从会话日志聚合每个技能的调用次数,看清哪些装了从没用过 |
| 问题 | 「能加载但白占坑」与「某技能为何不在」的诊断,一条一行 |

两个必须写明的行为,因为这两处正是同类面板容易撒谎的地方:

- 组头是一个三态开关。 全亮 = 整组启用,全灭 = 整组停用,半亮 = 混合。点它逐条改入口文件名;整组停用时整块变灰,不用去读「启用 0」那行字。
- 分组与归属存在本浏览器(localStorage),不落盘。 摆看板不写任何文件;整个插件唯一的写盘动作仍是入口文件改名。

中文摘要(只改显示)

很多第三方技能的 description 是英文原文。面板不会改别人的 SKILL.md,所以中文摘要放在面板自己这一层:

- 内建 12 条常用英文技能的中文摘要(架构图、论文配图、Git 规范等)。
- 其它英文技能行尾有「补中文」,填一行就存进本机浏览器 localStorage(键 dsh-skill-cockpit-gloss);留空即清除。
- 摘要只影响显示:技能本体、description、注入给模型的内容一律不变。

状态即文件名(无第二份状态)

开关状态每次扫描都从文件名重新读出,插件里没有 prefs、没有数据库、没有内存副本。
所以面板不可能出现"显示已关、模型照样能加载"的假开关。
(对照:better-sidebar 自带的 pluginSettings 管的是"这个 tab 显不显示",不是技能开关。)

截图

深色主题、真实 CSS 渲染(900px 宽):

体检页:六类问题 + 逐条解法

列表页:顶部体检摘要条 + 每行四态

装配

npm install dsh-skill-cockpit

(现在 npm 上是 0.2.3,0.6.7 待发。想直接跑当前源码,用下面的本地 link。)

// ~/.dsh/profiles/<profile>/package.json
"dependencies": { "dsh-skill-cockpit": "link:D:/插件/dsh-skill-panel" }

~/.dsh/profiles/<profile>/cordis.patch.yml
- insert:
- id: skill-panel
name: 'dsh-skill-cockpit'
可选配置(全部有默认值): config: dshHome: 'C:\\Users\\<你>\\.dsh' # 默认 $DSH_HOME 或 ~/.dsh trashRoot / backupRoot 都不得放在技能根内(否则删除 / 备份会把自己搬进自己) backupRoot: 'C:\\Users\\you\\Backups\\skill-panel' # 默认 <dshHome>/skill-panel/backups agentsHome: 'C:\\Users\\<你>\\.agents' # 默认 $DSH_AGENTS_HOME 或 ~/.agents
customSkillDirs: [] # 额外的技能根
bundledSkillDir: '' # 默认 $DSH_BUNDLED_SKILL_DIR includeDefaultRoots: true
writableSources: ['user-dsh'] # 默认 ['user-dsh','user-agents']
undeclaredIndexPath: 'C:\\Users\\you\\Documents\\skills-index.md'

改装配 = 要重启 dsh(进程红线:重启只由户主执行)。技能开关不需要重启。

HTTP 路由(宿主半,本机同源)

| 方法 | 路径 | 作用 |
| GET | /__dsh-skill-panel/ping | 插件身份 + 路由清单 + 可写根 |
| GET | /__dsh-skill-panel/skills?cwd=… | 全量扫描报告(cwd 决定项目级根) |
| GET | /__dsh-skill-panel/body?path=…&cwd=… | 读一个入口文件正文(白名单制:只认 /skills 报告里出现过的入口,带体积上限) |
| GET | /__dsh-skill-panel/undeclared?q=… | 搜未安装技能索引(只读) |
| GET | /__dsh-skill-panel/usage | 技能调用次数:{ ok, skills:[{name,count,lastAt}], scanned, skipped, sessions, updatedAt } |
| GET | /__dsh-skill-panel/checks?cwd=… | 四类「能加载但白占坑」诊断:{ ok, checks:[{kind,skill,detail}], summary, thresholds, note } |
| POST | /__dsh-skill-panel/toggle | { filePath, disable, cwd, plan? } → 改名(唯一写动作) |

/usage 只写一个文件:<dshHome>/dsh-skill-cockpit/usage.json,内容只有技能名 / 调用次数 / 时间戳 (不存任何对话内容);扫描是增量的 —— 会话日志 (size, mtime) 没变就跳过,本机 104 个会话的第二次调用 21ms。
/checks 是纯计算(不读磁盘),阈值默认「描述 <20 字」「文件 <300 字节(按字节估计,含 frontmatter)」。

为什么有些技能禁不了

技能到会话有两条路,只有一条落在文件上:

- 文件型(user-dsh / user-agents / custom / 项目根 / 内置目录):入口是真 SKILL.md 或 x.md,面板改入口文件名即可停用。
- 非文件型(provider 提供):宿主的契约是 ctx.skills.registerProvider(() => provider),只做加法 ——
provider 贡献自己的 list(),没有任何钩子能包装 / 过滤 / 排除别人输出的技能。面板对它无能为力。

这不是漏做,是宿主 API 的形状;所以面板如实标注:这类行在开关位置显示「本面板无法禁用(属 … provider)」,不画一个点了也不会有反应的假开关。

plan: true 只算不动 → 面板用它做"确认改名"预告,自测也用它验 dry-run。

两条拒绝是所有权的边界(各有负向测试)

面板是本机 web 面,所以路由不信调用方:

- /body 是白名单制:只回答 /skills 报告里列过的入口文件。其它路径 —— 任意文件、插件自己的数据文件、仅仅"存在"的路径 —— 一律 403 且不回传任何内容。所以这条路由不可能被当成任意文件读取器用。
(0.3.1 之前是"给什么读什么",这条是复核时发现的,不是事故。)
- /toggle 只在两个写根内落笔:目标必须位于 ~/.dsh/skills 或 ~/.agents/skills 内;项目级根只读,哪怕报告里列出了那个文件 (因为由会话 cwd 推出来的路径永远不该变成写操作)。

cwd 在两条路由上都是可选的(0.3.2 起):这面板同时也是没有会话的全局页。没有 cwd 时报告里就没有项目级根,于是项目级入口被拒、用户级入口照常工作;而解析不出来的 cwd 按"不存在"处理、不被信任,因此不可能绕过写根门。

其余写路由护栏(任一条不满足即拒绝并给中文原因):目标必须存在、必须是普通文件(永不改目录名)、必须直接位于可写根内(真实路径比对,防符号链接逃逸)、必须正好是入口文件(<目录>/SKILL.md 或 <根>/x.md)、目标名不得已存在(防覆盖);另有同源栅栏(带外部 Origin 的请求一律 403)。

后备库(reserve list)

- 默认名单:<DSH home>/skill-panel/后备库.md(首次「存进后备库」时自动创建)。
- 想用自己已有的清单:配置 reserveListPath 指向那个 markdown 文件(表格列:名字 / 一句话能力 / 来源 / 体积 / 备注)。
- 删除技能时勾「存进后备库」= 照常删进回收站 + 名单追加一行;「恢复」按「来源」下载(git 地址,需本机装 git)或复制(本机目录)。

后备技能库(可选)

技能不必「先装才能找」。在 ~/.dsh/skills 与 ~/.agents/skills 之外随便找个文件夹放那些没装的技能目录,再在旁边写一份 markdown 表格 —— 那个文件夹 dsh 不扫描、不加载,既不占上下文也不占内存,等于冷备:只有面板的搜索能看见它。

表格就是契约,一个技能一行,五列:

| 名字 | 一句话能力 | 来源(仓库·许可) | 体积 | 适用会话/预设 |
| code-review | 审查改动:正确性、安全、测试覆盖 | my-org/skills · MIT | 42.1 KB | 任意 |

把插件配置 undeclaredIndexPath 指向这份文件。之后搜索在已装列表里没命中时,会顺手查这张表,渲染成只读卡片(名字 / 一句话能力 / 来源与许可 / 体积 / 建议会话)并给一个「复制」按钮 ——面板自己不下载、不安装。

真要用了再「临时装」:点「导入技能」,填备用库里那个目录的路径,面板把它拷进可写根;任务做完删掉,删除先进回收站,随时能还原,备用库原件不动。

不配、或配了但文件不存在,第二跳直接不出现,不报错。(默认值指的是作者本机那份清单,请改成你自己的。)

自测

node test/run-all.mjs [报告目录]

六个套件(全部不需要 GUI / 不需要 dsh):

| 套件 | 覆盖 |
| run-data-layer.mjs | 六类根 / dropped: 全可达(合成 fixture)/ root-missing / 独立目录遍历比对(不多报、不漏报) / 字节数与磁盘一致 |
| run-toggle.mjs | 在真实 ~/.dsh/skills 上对 1 个新建探针做 改名→停用→改回 闭环:sha256 一致、根项数不变、面板仍显示"已停用"、清理后零残留;另含目录/非入口/根外/项目级四类拒绝路径 |
| run-routes.mjs | 假 ctx 跑真 apply():33 条路由注册、200/400/403/405、ping、/usage 落盘到配置的路径(自测绝不写真实 ~/.dsh)且第二次增量跳过、/checks 真实命中 |
| run-client.mjs | 假 __ModuleLoader__ + React 桩:bundle 求值、挂载对(sidebar.panellist 行 id/order/label + main keyed 页 key 一致)、order=5 夹位断言、座位未声明时不注册也不抛错、健康/坏 frontmatter/第二跳/错误态/无 useSessions 降级、三个视图(列表/用量/问题)、用量空态那句、非文件型的「禁不了」标注、主题令牌无死令牌 |
| run-usage.mjs | 真 zstd 多帧文件(每帧独立压缩再拼接):多帧解码 / 单帧 / 纯文本回退 / 垃圾字节 / seq 去重 / 深层嵌套事件筛选 / 增量跳过 / 文件变大不双计 / 落盘结构 / 隐私(落盘不含对话原文) |
| run-checks.mjs | 四类诊断各自正反例:shadowed / short-description(空、19 字、正好 20 字)/ dir-name-mismatch(仅 dir-bundle)/ empty-body(阈值边界)/ 汇总 / 阈值覆盖 / 畸形输入不抛 / 纯函数不改入参 |

边界(有意不做)

- 项目级根不写(安全边界:工作区 = 权限域)
- 不安装、不下载技能;删除 = 先整份复制进插件自己的回收站、二次确认(purge 才永久删);停用只改入口文件名,正文一字节不动
- 不做"最近被加载"(要动宿主埋点,成本大于收益)
- 技能开关状态只存在文件名里(SKILL.md ↔ .md.disabled);面板自己只存偏好(自动加载档位、必用/不用、译文缓存),都在 ~/.dsh/skill-cockpit/

未实查项(如实标注)

- bundledSkillDir 的实际取值:本机 $DSH_BUNDLED_SKILL_DIR 未设置、profile patch 里也没有该配置项 ⇒ 本插件按"未挂载"如实显示。会话可见 39 条 vs ~/.dsh/skills 31 条的差额来源仍未定位 (app.asar 内未见 reviewer-response 之类的技能文件;E:\DSH\resources 下也无)。面板不会替它猜。
- @deepseek-ai/dsh-client- 原生 Slot 树(设计方案的 H4)已实查:本次改挂载面时从运行中的 E:\DSH\resources\app.asar(0.2.0-rc.2)字节扫描确认 sidebar.panellist / main 两个座位、order 升序语义、以及各占用方(client-ui-plugin-manager 0 / client-ui-schedule 10 / task-board 20 / ssh 40)。
- 生态惯例与户主指定顺序不一致(已如实上报):dsh-ssh 源码注释把 0/10/20/30/40 记为「Plugins / Schedule / board / skill center 30」,即生态预留 30 给"技能中心";本包按户主明确要求取 5(夹在 插件 0 与 自动化任务 10 之间)。要改回 30 只需改 lib/client.js 的 PANEL_ORDER 一个数字。
- 重启后真实 GUI 渲染(左排出现「技能」、点击开整页、右侧栏不再出现)尚未经人眼确认 —— lib/client.js 是启动时载入的,必须由户主重启 dsh 才装载。

许可

MIT。

给 dsh 的工具接口(1.0.0)

面板是给人看的;平时更常用的是让 dsh 直接改技能。插件因此注册了一个工具 skill_panel(与任务板同一套路,不需要 SKILL.md),模型可以自己调:

| action | 作用 |
| list | 列出全部技能(名字 / 来源 / 启用状态 / 路径) |
| read | 读一个技能(简介 + 正文) |
| create | 新建技能(写 SKILL.md) |
| update | 改简介 / 正文 / 适用场景(旧文件自动备份) |
| delete | 删除,整份进回收站(需 confirm: true) |
| toggle | 启用 / 停用(只改入口文件名,plan: true 可预览) |
| repair | 修结构问题(缺 frontmatter、名字不合法等) |
| backup | 备份单个或全部技能 |
| trash_list / trash_restore / trash_purge | 回收站:列出 / 还原 / 彻底删除(后者需 confirm: true) |
| vault_list / vault_put / vault_install | 后备库:看名单 / 存进去(= 本机删除 + 名单记一行,需 confirm: true)/ 按名单装回来 |
| preset_list / preset_mount / preset_unmount | DSH 预设:列预设 / 把技能以 junction 挂进去 / 摘掉(挂载改动重启 dsh 生效) |
| mode_set | 批量切四态(on / user-only / model-only / off);scope: exclusive 当白名单,清单外全关;不带 confirm: true 只返回预演 |
| usage | 从会话日志统计每个技能被调多少次(默认复用索引,refresh: true 重扫) |
| checkup | 体检:描述过短 / 目录名不符 / 正文疑似为空 / 同名被遮蔽 |

边界与面板一致:只写用户级技能根(~/.dsh/skills、~/.agents/skills),项目级技能只读;删除一律先整份复制进回收站,绝不半删。
预览分享卡片长什么样 ↓huanlin-labs/dsh-skill-cockpit
分享 · DeepSeek Harness Hub
huanlin-labs/dsh-skill-cockpit

诚实边界。 它不是沙箱:只管理 DSH 自己暴露的技能根,只写 /.dsh/skills 与…

回到顶部