@weibaohui/skills-management 0.1.4 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -5,13 +5,17 @@
5
5
 
6
6
  **技能市场插件**:一个页面管理本机所有 coding agent 的技能,还能浏览安装 6600+ 的 ntd 技能市场。
7
7
 
8
+ ![技能市场:从本机技能浏览到 ntd 市场安装](docs/demo.gif)
9
+
8
10
  ## 核心功能
9
11
 
10
12
  - **全来源扫描**:自动扫描本机各 coding agent 的技能目录(Claude、ZCode、Codex 等十余个执行器),统一在一个页面查看、查看详情、单文件预览
11
13
  - **一键收编**:任意执行器的技能可一键复制进 DSH 用户库,供模型的 `skill` 工具直接调用
12
14
  - **技能市场**:内置 ntd 技能合集(6600+ 条),按来源分组浏览、搜索筛选、详情预览、一键安装
13
15
  - **模型可见性治理**:每个已装技能都有「模型可调用」开关,不想暴露给模型的技能一键隐藏/恢复
16
+ - **输入框 +技能**:composer 工具行新增「+技能」按钮,弹出技能候选菜单,选中即把 `/技能名` 写入草稿,发送时技能内容注入该条消息
14
17
  - **市场自动同步**:市场仓库自动克隆与每日更新(可关),支持 GitCode 私有仓库 access token
18
+ - **稀疏检出**:ntd-resource 仓库同时携带专家/模板等子树,市场只检出 `skills` 子目录(git partial clone + sparse-checkout),省一半以上流量与磁盘;已有全量检出会在下次同步时原地转换
15
19
  - **软链布局兼容**:各执行器目录间软链共享的技能不会重复展示
16
20
 
17
21
  ## 安装
package/client/bundle.js CHANGED
@@ -74,6 +74,37 @@ window.__ModuleLoader__.load({
74
74
  let sessionsApi = null
75
75
  const sessionsSvc = () => sessionsApi
76
76
 
77
+ // Composer services (inputTriggers + sessions) for the +技能 button: opens
78
+ // the `skill` source (registered by dsh-client-ui-skill) as a menu. Absence
79
+ // hides the button; nothing else depends on it.
80
+ let composerScope = null
81
+
82
+ /**
83
+ * Open one registered '/' source over a synthetic collapsed span appended at
84
+ * the draft end (host toggleCommandMenu 同款调用形状;标准 kit 不暴露光标,
85
+ * pick 依赖 span-CAS:点击后草稿若再变动则本次 pick 静默作废)。
86
+ */
87
+ function openTriggerSource(scope, sessionId, input, sourceName) {
88
+ const inputTriggers = scope && scope.inputTriggers
89
+ const sessions = scope && scope.sessions
90
+ if (!inputTriggers || !sessions) return false
91
+ let actx
92
+ try { actx = sessions.scope(sessionId) } catch { return false }
93
+ if (actx === undefined || actx === null) return false
94
+ let controller
95
+ try { controller = inputTriggers.sessionOf(actx) } catch { return false }
96
+ const draft = (input && input.draft) || ''
97
+ const at = draft.length
98
+ controller.toggleSource(sourceName, {
99
+ trigger: '/',
100
+ query: '',
101
+ quoted: false,
102
+ position: draft.trim() === '' ? 'leading' : 'inline',
103
+ span: { start: at, end: at, draftRev: (input && input.draftRev) || 0 },
104
+ })
105
+ return true
106
+ }
107
+
77
108
  // ── Locale ───────────────────────────────────────────────────────────────
78
109
 
79
110
  const NS = 'skillsManagement'
@@ -183,6 +214,8 @@ window.__ModuleLoader__.load({
183
214
  invocationHint: '关闭后技能保留在库里,但不再注入对话目录(skill 工具也调不到)',
184
215
  pathLabel: '路径',
185
216
  meTag: '本机',
217
+ pickSkill: '+技能',
218
+ pickSkillTitle: '选择一个技能,其内容将注入本条消息',
186
219
  }
187
220
 
188
221
  const EN = {
@@ -290,6 +323,8 @@ window.__ModuleLoader__.load({
290
323
  invocationHint: 'When off, the skill stays in the library but is not injected into conversation catalogs',
291
324
  pathLabel: 'Path',
292
325
  meTag: 'me',
326
+ pickSkill: '+ Skill',
327
+ pickSkillTitle: 'Pick a skill; its content is injected into this message',
293
328
  }
294
329
 
295
330
  // ── Pure helpers ────────────────────────────────────────────────────────
@@ -432,6 +467,8 @@ window.__ModuleLoader__.load({
432
467
  .sk-menu-item:hover{background:var(--dsw-alias-interactive-bg-hover)}
433
468
  .sk-menu-item.on{color:var(--dsw-alias-state-business-primary);font-weight:500}
434
469
  .sk-tabpill{background:transparent;border:none;color:var(--dsw-alias-label-secondary);font-family:var(--dsw-font-family)}
470
+ .sk-chip{display:inline-flex;align-items:center;gap:4px;height:26px;padding:0 9px;border-radius:8px;border:1px solid var(--dsw-alias-border-l2);background:transparent;color:var(--dsw-alias-label-secondary);font-family:var(--dsw-font-family);font-size:12px;cursor:pointer;white-space:nowrap}
471
+ .sk-chip:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}
435
472
  </style>`
436
473
 
437
474
  // ── Fetch layer ─────────────────────────────────────────────────────────
@@ -1290,7 +1327,7 @@ window.__ModuleLoader__.load({
1290
1327
  module.exports = {
1291
1328
  name: CLIENT_NAME,
1292
1329
  inject: ['slots', 'locale'],
1293
- __internals: { NS, ZH, EN, matchSkill, formatSize, formatTime },
1330
+ __internals: { NS, ZH, EN, matchSkill, formatSize, formatTime, openTriggerSource },
1294
1331
  /** Test/host helper: mount a standalone page into any container. */
1295
1332
  __boot(container, opts = {}) {
1296
1333
  ensureStyles()
@@ -1318,6 +1355,13 @@ window.__ModuleLoader__.load({
1318
1355
  })
1319
1356
  }
1320
1357
  } catch {}
1358
+ // Composer services (inputTriggers + sessions) for the +技能 button;
1359
+ // absence hides the button only.
1360
+ try {
1361
+ if (typeof ctx.inject === 'function') {
1362
+ ctx.inject(['inputTriggers', 'sessions'], (scope) => { composerScope = scope })
1363
+ }
1364
+ } catch {}
1321
1365
  // Locale service is optional at boot order — degrade to EN until present
1322
1366
  let t = (key, vars) => {
1323
1367
  let out = EN[key] ?? key
@@ -1369,9 +1413,40 @@ window.__ModuleLoader__.load({
1369
1413
  }))
1370
1414
  } catch (e) { (globalThis.__skErrors = globalThis.__skErrors || []).push('settings:' + (e && e.message)); throw e }
1371
1415
  }, 'skills-management: settings section')
1416
+ ctx.effect(() => {
1417
+ try {
1418
+ ctx.slots.inject('conversation.input.left', () => ctx.slots.register({
1419
+ name: 'conversation.input.left',
1420
+ id: CLIENT_NAME,
1421
+ order: 60,
1422
+ locale: NS,
1423
+ label: () => t('pickSkill'),
1424
+ inject: () => ({ t }),
1425
+ }, function SkillButtonSlot(apiProps) {
1426
+ return h(ComposerButtonSlot, {
1427
+ __t: t, icon: '⚡', label: t('pickSkill'), title: t('pickSkillTitle'),
1428
+ source: 'skill', sessionId: apiProps && apiProps.sessionId, input: apiProps && apiProps.input,
1429
+ })
1430
+ }))
1431
+ } catch (e) { (globalThis.__skErrors = globalThis.__skErrors || []).push('input.left:' + (e && e.message)); throw e }
1432
+ }, 'skills-management: input left button')
1372
1433
  },
1373
1434
  }
1374
1435
 
1436
+ /** Composer tool-row button: opens a registered '/' source (ui-skill's
1437
+ * `skill` source) over the session's trigger controller. Hidden while the
1438
+ * inputTriggers/sessions services are absent. */
1439
+ function ComposerButtonSlot(props) {
1440
+ useEffect(ensureStyles, [])
1441
+ const ready = !!(composerScope && composerScope.inputTriggers && composerScope.sessions && props.sessionId)
1442
+ if (!ready) return null
1443
+ return h('button', {
1444
+ className: 'sk-chip',
1445
+ title: props.title || props.label,
1446
+ onClick: () => { openTriggerSource(composerScope, props.sessionId, props.input, props.source) },
1447
+ }, `${props.icon || ''}${props.icon ? ' ' : ''}${props.label}`)
1448
+ }
1449
+
1375
1450
  return module.exports
1376
1451
  }
1377
1452
  })
package/client/index.js CHANGED
@@ -64,6 +64,37 @@ const prim = (name) => P && P[name]
64
64
  let sessionsApi = null
65
65
  const sessionsSvc = () => sessionsApi
66
66
 
67
+ // Composer services (inputTriggers + sessions) for the +技能 button: opens
68
+ // the `skill` source (registered by dsh-client-ui-skill) as a menu. Absence
69
+ // hides the button; nothing else depends on it.
70
+ let composerScope = null
71
+
72
+ /**
73
+ * Open one registered '/' source over a synthetic collapsed span appended at
74
+ * the draft end (host toggleCommandMenu 同款调用形状;标准 kit 不暴露光标,
75
+ * pick 依赖 span-CAS:点击后草稿若再变动则本次 pick 静默作废)。
76
+ */
77
+ function openTriggerSource(scope, sessionId, input, sourceName) {
78
+ const inputTriggers = scope && scope.inputTriggers
79
+ const sessions = scope && scope.sessions
80
+ if (!inputTriggers || !sessions) return false
81
+ let actx
82
+ try { actx = sessions.scope(sessionId) } catch { return false }
83
+ if (actx === undefined || actx === null) return false
84
+ let controller
85
+ try { controller = inputTriggers.sessionOf(actx) } catch { return false }
86
+ const draft = (input && input.draft) || ''
87
+ const at = draft.length
88
+ controller.toggleSource(sourceName, {
89
+ trigger: '/',
90
+ query: '',
91
+ quoted: false,
92
+ position: draft.trim() === '' ? 'leading' : 'inline',
93
+ span: { start: at, end: at, draftRev: (input && input.draftRev) || 0 },
94
+ })
95
+ return true
96
+ }
97
+
67
98
  // ── Locale ───────────────────────────────────────────────────────────────
68
99
 
69
100
  const NS = 'skillsManagement'
@@ -173,6 +204,8 @@ const ZH = {
173
204
  invocationHint: '关闭后技能保留在库里,但不再注入对话目录(skill 工具也调不到)',
174
205
  pathLabel: '路径',
175
206
  meTag: '本机',
207
+ pickSkill: '+技能',
208
+ pickSkillTitle: '选择一个技能,其内容将注入本条消息',
176
209
  }
177
210
 
178
211
  const EN = {
@@ -280,6 +313,8 @@ const EN = {
280
313
  invocationHint: 'When off, the skill stays in the library but is not injected into conversation catalogs',
281
314
  pathLabel: 'Path',
282
315
  meTag: 'me',
316
+ pickSkill: '+ Skill',
317
+ pickSkillTitle: 'Pick a skill; its content is injected into this message',
283
318
  }
284
319
 
285
320
  // ── Pure helpers ────────────────────────────────────────────────────────
@@ -422,6 +457,8 @@ const STYLE = `<style>
422
457
  .sk-menu-item:hover{background:var(--dsw-alias-interactive-bg-hover)}
423
458
  .sk-menu-item.on{color:var(--dsw-alias-state-business-primary);font-weight:500}
424
459
  .sk-tabpill{background:transparent;border:none;color:var(--dsw-alias-label-secondary);font-family:var(--dsw-font-family)}
460
+ .sk-chip{display:inline-flex;align-items:center;gap:4px;height:26px;padding:0 9px;border-radius:8px;border:1px solid var(--dsw-alias-border-l2);background:transparent;color:var(--dsw-alias-label-secondary);font-family:var(--dsw-font-family);font-size:12px;cursor:pointer;white-space:nowrap}
461
+ .sk-chip:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}
425
462
  </style>`
426
463
 
427
464
  // ── Fetch layer ─────────────────────────────────────────────────────────
@@ -1280,7 +1317,7 @@ const CLIENT_NAME = '@weibaohui/skills-management'
1280
1317
  module.exports = {
1281
1318
  name: CLIENT_NAME,
1282
1319
  inject: ['slots', 'locale'],
1283
- __internals: { NS, ZH, EN, matchSkill, formatSize, formatTime },
1320
+ __internals: { NS, ZH, EN, matchSkill, formatSize, formatTime, openTriggerSource },
1284
1321
  /** Test/host helper: mount a standalone page into any container. */
1285
1322
  __boot(container, opts = {}) {
1286
1323
  ensureStyles()
@@ -1308,6 +1345,13 @@ module.exports = {
1308
1345
  })
1309
1346
  }
1310
1347
  } catch {}
1348
+ // Composer services (inputTriggers + sessions) for the +技能 button;
1349
+ // absence hides the button only.
1350
+ try {
1351
+ if (typeof ctx.inject === 'function') {
1352
+ ctx.inject(['inputTriggers', 'sessions'], (scope) => { composerScope = scope })
1353
+ }
1354
+ } catch {}
1311
1355
  // Locale service is optional at boot order — degrade to EN until present
1312
1356
  let t = (key, vars) => {
1313
1357
  let out = EN[key] ?? key
@@ -1359,5 +1403,36 @@ module.exports = {
1359
1403
  }))
1360
1404
  } catch (e) { (globalThis.__skErrors = globalThis.__skErrors || []).push('settings:' + (e && e.message)); throw e }
1361
1405
  }, 'skills-management: settings section')
1406
+ ctx.effect(() => {
1407
+ try {
1408
+ ctx.slots.inject('conversation.input.left', () => ctx.slots.register({
1409
+ name: 'conversation.input.left',
1410
+ id: CLIENT_NAME,
1411
+ order: 60,
1412
+ locale: NS,
1413
+ label: () => t('pickSkill'),
1414
+ inject: () => ({ t }),
1415
+ }, function SkillButtonSlot(apiProps) {
1416
+ return h(ComposerButtonSlot, {
1417
+ __t: t, icon: '⚡', label: t('pickSkill'), title: t('pickSkillTitle'),
1418
+ source: 'skill', sessionId: apiProps && apiProps.sessionId, input: apiProps && apiProps.input,
1419
+ })
1420
+ }))
1421
+ } catch (e) { (globalThis.__skErrors = globalThis.__skErrors || []).push('input.left:' + (e && e.message)); throw e }
1422
+ }, 'skills-management: input left button')
1362
1423
  },
1363
1424
  }
1425
+
1426
+ /** Composer tool-row button: opens a registered '/' source (ui-skill's
1427
+ * `skill` source) over the session's trigger controller. Hidden while the
1428
+ * inputTriggers/sessions services are absent. */
1429
+ function ComposerButtonSlot(props) {
1430
+ useEffect(ensureStyles, [])
1431
+ const ready = !!(composerScope && composerScope.inputTriggers && composerScope.sessions && props.sessionId)
1432
+ if (!ready) return null
1433
+ return h('button', {
1434
+ className: 'sk-chip',
1435
+ title: props.title || props.label,
1436
+ onClick: () => { openTriggerSource(composerScope, props.sessionId, props.input, props.source) },
1437
+ }, `${props.icon || ''}${props.icon ? ' ' : ''}${props.label}`)
1438
+ }
package/docs/demo.gif ADDED
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@weibaohui/skills-management",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "dsh 插件 · 技能市场:一个页面管理本机所有 coding agent 的技能(Claude、ZCode、Codex 等十余个执行器),一键收编进 DSH 用户库供 skill 工具调用;内置 6600+ ntd 技能市场可浏览搜索安装,支持模型可见性治理与市场每日自动同步。",
5
5
  "license": "MIT",
6
6
  "keywords": [
@@ -30,6 +30,7 @@
30
30
  "files": [
31
31
  "src",
32
32
  "client",
33
+ "docs",
33
34
  "cordis.patch.yml"
34
35
  ],
35
36
  "scripts": {
package/src/index.js CHANGED
@@ -36,7 +36,6 @@ function loadSchemastery() {
36
36
  }
37
37
  const Schema = loadSchemastery()
38
38
 
39
- const DEFAULT_MARKET_DIRS = [join(homedir(), '.ntd', 'bundled', 'skills')]
40
39
  const MARKET_SCAN_SKIP = new Set(['.git', 'node_modules'])
41
40
  const RANK_INSTALLED = 100
42
41
  const RANK_MARKET = 500
@@ -312,6 +311,11 @@ function expandTilde(p) {
312
311
  return p === '~' || p.startsWith('~/') || p.startsWith('~\\') ? join(homedir(), p.slice(2)) : p
313
312
  }
314
313
 
314
+ /** dsh 数据根(与宿主一致:$DSH_HOME,缺省 ~/.dsh)。 */
315
+ function dshHome() {
316
+ return process.env.DSH_HOME ? resolve(process.env.DSH_HOME) : join(homedir(), '.dsh')
317
+ }
318
+
315
319
  /**
316
320
  * 切换 dsh 原生治理键 `disable-model-invocation`(docs/subsystems/skills.md)。
317
321
  * modelInvocable=true 时移除该键;false 时写入 true。其余 frontmatter 键与正文原样保留。
@@ -377,17 +381,35 @@ function authedUrl(url, token) {
377
381
  return String(url).replace(/^(https?:\/\/)([^@/]+@)?/, `$1oauth2:${encodeURIComponent(token)}@`)
378
382
  }
379
383
 
380
- /** Clone (first time) or fetch+reset (update); remote branch is truth. */
381
- async function gitSyncRepo(binary, url, branch, repoDir, token) {
384
+ /** Clone (first time) or fetch+reset (update); remote branch is truth.
385
+ * `sparsePaths` (e.g. ['skills']) switches the checkout to sparse mode: fresh
386
+ * clones pass --filter=blob:none --sparse so only those subtrees download
387
+ * (the ntd-resource monorepo also carries experts/ + templates/, ~2x the
388
+ * skills payload); servers without filter support just warn and fall back to
389
+ * a full clone, which sparse-checkout still prunes. An existing full checkout
390
+ * is converted in place — the worktree prunes immediately, already-packed
391
+ * blobs stay (reachable from HEAD), so the big win is on fresh clones. */
392
+ async function gitSyncRepo(binary, url, branch, repoDir, token, sparsePaths) {
382
393
  const remote = authedUrl(url, token)
394
+ const sparse = Array.isArray(sparsePaths) && sparsePaths.length > 0 ? sparsePaths : undefined
383
395
  let repoExists = false
384
396
  try { await fsP.access(join(repoDir, '.git')); repoExists = true } catch { repoExists = false }
385
397
  if (!repoExists) {
386
398
  await fsP.rm(repoDir, { recursive: true, force: true })
387
399
  await fsP.mkdir(join(repoDir, '..'), { recursive: true })
388
- await gitExec(binary, ['clone', '-b', branch, '--depth', '1', remote, repoDir])
400
+ if (sparse) {
401
+ await gitExec(binary, ['clone', '-b', branch, '--depth', '1', '--filter=blob:none', '--sparse', remote, repoDir])
402
+ await gitExec(binary, ['sparse-checkout', 'set', '--cone', ...sparse], repoDir)
403
+ } else {
404
+ await gitExec(binary, ['clone', '-b', branch, '--depth', '1', remote, repoDir])
405
+ }
389
406
  return { isFirstClone: true, hasUpdates: true, before: undefined, after: await gitCurrentCommit(binary, repoDir) }
390
407
  }
408
+ // Migrate a pre-sparse full checkout in place (idempotent no-op once sparse).
409
+ if (sparse) {
410
+ try { await gitExec(binary, ['sparse-checkout', 'set', '--cone', ...sparse], repoDir) }
411
+ catch (e) { console.warn(`skills-management: sparse-checkout conversion failed, continuing full: ${e && e.message}`) }
412
+ }
391
413
  const before = await gitCurrentCommit(binary, repoDir)
392
414
  await gitExec(binary, ['fetch', remote, branch], repoDir)
393
415
  await gitExec(binary, ['reset', '--hard', 'FETCH_HEAD'], repoDir)
@@ -547,10 +569,15 @@ module.exports = {
547
569
  return resolve(expandTilde(
548
570
  typeof eff.repoDir === 'string' && eff.repoDir !== '' ? eff.repoDir
549
571
  : config.marketRepoDir !== undefined ? config.marketRepoDir
550
- : join(homedir(), '.ntd', 'bundled')))
572
+ : join(dshHome(), 'skills-management', 'market')))
551
573
  }
552
574
  const marketRoots = () => configMarketDirs !== undefined ? configMarketDirs : [join(effectiveRepoDir(), 'skills')]
553
575
  const marketDirs = marketRoots // scan/install/locate call sites read through this
576
+ // 稀疏检出子树:ntd-resource 仓库同时携带 experts/templates,只检 skills 一份省一半以上
577
+ // 流量与磁盘。config.marketSparsePaths: null 关闭;自定义数组换目标子树。
578
+ const marketSparsePaths = () => config.marketSparsePaths === null
579
+ ? undefined
580
+ : (Array.isArray(config.marketSparsePaths) && config.marketSparsePaths.length > 0 ? config.marketSparsePaths.map(String) : ['skills'])
554
581
  const installedDir = resolve(expandTilde(config.installedDir !== undefined ? config.installedDir : process.env.DSH_HOME ? join(process.env.DSH_HOME, 'skills') : join(homedir(), '.dsh', 'skills')))
555
582
  const providerName = config.providerName !== undefined ? config.providerName : 'ntd-skills'
556
583
  // 市场库存(数几千条)默认不进模型目录 available_skills —— 只作为可浏览/可安装的货架。
@@ -771,7 +798,7 @@ module.exports = {
771
798
  if (!ok) throw new Error('git is not available on PATH')
772
799
  const started = Date.now()
773
800
  const repoDir = effectiveRepoDir()
774
- const result = await gitSyncRepo(eff.gitBinary, eff.url, eff.branch, repoDir, eff.token)
801
+ const result = await gitSyncRepo(eff.gitBinary, eff.url, eff.branch, repoDir, eff.token, marketSparsePaths())
775
802
  marketState.lastSyncAt = new Date().toISOString()
776
803
  marketState.lastResult = { ...result, at: marketState.lastSyncAt, durationMs: Date.now() - started }
777
804
  await saveMarketState()
@@ -898,6 +925,7 @@ module.exports = {
898
925
  autoSync: eff.autoSync, syncOnStartup: eff.syncOnStartup,
899
926
  hasToken: typeof eff.token === 'string' && eff.token !== '',
900
927
  syncing: marketSyncRun !== null,
928
+ sparsePaths: marketSparsePaths() ?? null,
901
929
  })
902
930
  return
903
931
  }