@weibaohui/experts-management 0.1.3 → 0.2.0

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/src/index.js CHANGED
@@ -5,8 +5,8 @@
5
5
  *
6
6
  * Manages ntd-format experts (WorkBuddy plugin.json + Agent MD + skills)
7
7
  * WITHOUT touching the ntd application's own directories:
8
- * - Market: ntd-resource's experts/ subtree via git sparse checkout into the
9
- * plugin's own dir ($DSH_HOME/experts-management/market). Read-only shelf;
8
+ * - Builtin: ntd-resource's experts/ subtree via git sparse checkout into the
9
+ * plugin's own dir ($DSH_HOME/experts-management/builtin). Read-only shelf;
10
10
  * install copies into the user library.
11
11
  * - User library: $DSH_HOME/experts (writable, the only built-in source).
12
12
  * Additional directories are opt-in via config.extraSources.
@@ -46,9 +46,9 @@ function dshHome() {
46
46
  return process.env.DSH_HOME ? resolve(process.env.DSH_HOME) : join(homedir(), '.dsh')
47
47
  }
48
48
 
49
- const MARKET_SCAN_SKIP = new Set(['.git', 'node_modules'])
49
+ const BUILTIN_SCAN_SKIP = new Set(['.git', 'node_modules'])
50
50
  const RANK_INSTALLED = 100
51
- const RANK_MARKET = 500
51
+ const RANK_BUILTIN = 500
52
52
  const MAX_BODY_BYTES = 64 * 1024
53
53
  const DESCRIPTION_LIMIT = 140
54
54
  // 同款正则见 skill/skill/src/index.ts SKILL_NAME —— 不合规的候选会让 registry 抛错
@@ -226,7 +226,7 @@ async function scanExpertsRoot(root, sourceKey) {
226
226
  try { entries = await fsP.readdir(root, { withFileTypes: true }) } catch { return { experts, errors } }
227
227
  for (const entry of entries) {
228
228
  if (!entry.isDirectory()) continue
229
- if (MARKET_SCAN_SKIP.has(entry.name)) continue
229
+ if (BUILTIN_SCAN_SKIP.has(entry.name)) continue
230
230
  const dir = join(root, entry.name)
231
231
  try {
232
232
  experts.push(await readExpertDir(root, dir, sourceKey))
@@ -418,7 +418,7 @@ async function atomicWriteJs(file, content) {
418
418
  await fsP.rename(temp, file)
419
419
  }
420
420
 
421
- // ── Market git sync(与 skills-management 同款;稀疏检出 experts/ 子树)──
421
+ // ── Builtin git sync(与 skills-management 同款管线;稀疏检出 experts/ 子树)──
422
422
 
423
423
  function gitExec(binary, args, cwd) {
424
424
  return new Promise((fulfil, reject) => {
@@ -481,7 +481,7 @@ async function gitSyncRepo(binary, url, branch, repoDir, token, sparsePaths) {
481
481
  return { isFirstClone: false, hasUpdates: before !== after, before, after }
482
482
  }
483
483
 
484
- const DEFAULT_MARKET_SYNC = {
484
+ const DEFAULT_BUILTIN_SYNC = {
485
485
  url: 'https://gitcode.com/weibaohui/ntd-resource.git',
486
486
  branch: 'main',
487
487
  gitBinary: 'git',
@@ -489,11 +489,11 @@ const DEFAULT_MARKET_SYNC = {
489
489
  syncOnStartup: true,
490
490
  }
491
491
  // ntd-resource 同时携带 skills(~400MB),专家市场只稀疏检出 experts/ 子树
492
- const DEFAULT_MARKET_SPARSE_PATHS = ['experts']
492
+ const DEFAULT_BUILTIN_SPARSE_PATHS = ['experts']
493
493
 
494
- const MARKET_SETTINGS_NS = 'experts-management-market'
494
+ const BUILTIN_SETTINGS_NS = 'experts-management-builtin'
495
495
 
496
- function marketSettingsSchema() {
496
+ function builtinSettingsSchema() {
497
497
  if (!Schema) return null
498
498
  return Schema.object({
499
499
  url: Schema.string(),
@@ -507,12 +507,12 @@ function marketSettingsSchema() {
507
507
  }
508
508
 
509
509
  function baseSettings(config) {
510
- const cfg = (config.marketSync && typeof config.marketSync === 'object') ? config.marketSync : {}
511
- const base = { ...DEFAULT_MARKET_SYNC }
510
+ const cfg = (config.builtinSync && typeof config.builtinSync === 'object') ? config.builtinSync : {}
511
+ const base = { ...DEFAULT_BUILTIN_SYNC }
512
512
  for (const key of ['url', 'branch', 'gitBinary', 'autoSync', 'syncOnStartup']) {
513
513
  if (cfg[key] !== undefined) base[key] = cfg[key]
514
514
  }
515
- if (config.marketRepoDir !== undefined) base.repoDir = resolve(String(config.marketRepoDir))
515
+ if (config.builtinRepoDir !== undefined) base.repoDir = resolve(String(config.builtinRepoDir))
516
516
  return base
517
517
  }
518
518
 
@@ -561,43 +561,59 @@ module.exports = {
561
561
  // git 市场检出(稀疏 experts/ 子树)也是一路来源:root 运行期可变,按调用时解析
562
562
  const allSourceRows = () => [
563
563
  ...sourceRows,
564
- { key: 'market', label: '专家市场', root: join(marketRootDir(), 'experts'), readOnly: true },
564
+ { key: 'builtin', label: '内置', root: join(builtinRootDir(), 'experts'), readOnly: true },
565
565
  ]
566
566
 
567
567
  // ── Market sync state / settings(skills-management 同款管线)────────
568
- const marketRootDir = () => {
569
- const eff = marketSettings()
568
+ const builtinRootDir = () => {
569
+ const eff = builtinSettings()
570
570
  return resolve(typeof eff.repoDir === 'string' && eff.repoDir !== '' ? eff.repoDir
571
- : config.marketRepoDir !== undefined ? resolve(String(config.marketRepoDir))
572
- : join(dshHome(), 'experts-management', 'market'))
571
+ : config.builtinRepoDir !== undefined ? resolve(String(config.builtinRepoDir))
572
+ : join(dshHome(), 'experts-management', 'builtin'))
573
573
  }
574
- const marketSparsePaths = () => config.marketSparsePaths === null
574
+ const builtinSparsePaths = () => config.builtinSparsePaths === null
575
575
  ? undefined
576
- : (Array.isArray(config.marketSparsePaths) && config.marketSparsePaths.length > 0 ? config.marketSparsePaths.map(String) : DEFAULT_MARKET_SPARSE_PATHS)
577
-
578
- const marketStateFile = join(dshHome(), 'experts-management', 'market-sync.json')
579
- let marketState = { lastSyncAt: undefined, lastResult: undefined }
576
+ : (Array.isArray(config.builtinSparsePaths) && config.builtinSparsePaths.length > 0 ? config.builtinSparsePaths.map(String) : DEFAULT_BUILTIN_SPARSE_PATHS)
577
+
578
+ // v0.1 v0.2 落盘迁移:market 命名 → builtin(检出目录 + 同步状态文件)
579
+ const migrateV1Layout = async () => {
580
+ const base = join(dshHome(), 'experts-management')
581
+ for (const [oldName, newName] of [['market', 'builtin'], ['market-sync.json', 'builtin-sync.json']]) {
582
+ const from = join(base, oldName)
583
+ const to = join(base, newName)
584
+ try {
585
+ await fsP.access(from)
586
+ try { await fsP.access(to); continue } catch {}
587
+ await fsP.rename(from, to)
588
+ ctx.logger.info && ctx.logger.info(`experts-management: migrated ${oldName} → ${newName}`)
589
+ } catch { /* 旧布局不存在:跳过 */ }
590
+ }
591
+ }
592
+ const builtinStateFile = join(dshHome(), 'experts-management', 'builtin-sync.json')
593
+ let builtinState = { lastSyncAt: undefined, lastResult: undefined }
580
594
  let settingsScope = null
581
595
  const settingsOverrides = {} // fallback sheet when the settings service is absent
582
- const marketStateLoaded = fsP.readFile(marketStateFile, 'utf8')
583
- .then(raw => {
584
- const parsed = JSON.parse(raw)
585
- marketState = { lastSyncAt: parsed.lastSyncAt, lastResult: parsed.lastResult }
596
+ const builtinStateLoaded = migrateV1Layout()
597
+ .then(() => fsP.readFile(builtinStateFile, 'utf8'))
598
+ .then((raw) => {
599
+ let parsed
600
+ try { parsed = JSON.parse(raw) } catch { parsed = null }
601
+ if (parsed) builtinState = { lastSyncAt: parsed.lastSyncAt, lastResult: parsed.lastResult }
586
602
  })
587
603
  .catch(() => {})
588
604
  if (Schema && ctx.settings && typeof ctx.settings.register === 'function') {
589
605
  try {
590
- settingsScope = ctx.settings.register(MARKET_SETTINGS_NS, marketSettingsSchema(), { base: baseSettings(config) })
606
+ settingsScope = ctx.settings.register(BUILTIN_SETTINGS_NS, builtinSettingsSchema(), { base: baseSettings(config) })
591
607
  } catch (e) { ctx.logger.warn(`experts-management: settings register: ${e && e.message}`) }
592
608
  }
593
- const saveMarketState = async () => {
609
+ const saveBuiltinState = async () => {
594
610
  try {
595
- await fsP.mkdir(join(marketStateFile, '..'), { recursive: true })
596
- await atomicWriteJs(marketStateFile, JSON.stringify(marketState, null, 2))
597
- await fsP.chmod(marketStateFile, 0o600)
611
+ await fsP.mkdir(join(builtinStateFile, '..'), { recursive: true })
612
+ await atomicWriteJs(builtinStateFile, JSON.stringify(builtinState, null, 2))
613
+ await fsP.chmod(builtinStateFile, 0o600)
598
614
  } catch {}
599
615
  }
600
- const marketSettings = () => {
616
+ const builtinSettings = () => {
601
617
  if (settingsScope && typeof settingsScope.get === 'function') {
602
618
  const v = settingsScope.get()
603
619
  if (v && typeof v === 'object') return { ...baseSettings(config), ...v }
@@ -605,55 +621,55 @@ module.exports = {
605
621
  return { ...baseSettings(config), ...settingsOverrides }
606
622
  }
607
623
 
608
- let marketSyncRun = null
609
- const runMarketSync = async () => {
610
- if (marketSyncRun !== null) return marketSyncRun
611
- marketSyncRun = (async () => {
612
- await marketStateLoaded
613
- const eff = marketSettings()
624
+ let builtinSyncRun = null
625
+ const runBuiltinSync = async () => {
626
+ if (builtinSyncRun !== null) return builtinSyncRun
627
+ builtinSyncRun = (async () => {
628
+ await builtinStateLoaded
629
+ const eff = builtinSettings()
614
630
  const ok = await gitAvailable(eff.gitBinary)
615
631
  if (!ok) throw new Error('git is not available on PATH')
616
632
  const started = Date.now()
617
- const repoDir = marketRootDir()
618
- const result = await gitSyncRepo(eff.gitBinary, eff.url, eff.branch, repoDir, eff.token, marketSparsePaths())
619
- marketState.lastSyncAt = new Date().toISOString()
620
- marketState.lastResult = { ...result, at: marketState.lastSyncAt, durationMs: Date.now() - started }
621
- await saveMarketState()
633
+ const repoDir = builtinRootDir()
634
+ const result = await gitSyncRepo(eff.gitBinary, eff.url, eff.branch, repoDir, eff.token, builtinSparsePaths())
635
+ builtinState.lastSyncAt = new Date().toISOString()
636
+ builtinState.lastResult = { ...result, at: builtinState.lastSyncAt, durationMs: Date.now() - started }
637
+ await saveBuiltinState()
622
638
  invalidate()
623
- return { ...marketState.lastResult, url: eff.url, branch: eff.branch, dir: repoDir }
624
- })().finally(() => { marketSyncRun = null })
625
- return marketSyncRun
639
+ return { ...builtinState.lastResult, url: eff.url, branch: eff.branch, dir: repoDir }
640
+ })().finally(() => { builtinSyncRun = null })
641
+ return builtinSyncRun
626
642
  }
627
643
 
628
644
  // Startup + periodic auto-sync (fire-and-forget; failures only warn)
629
645
  ctx.effect(() => {
630
- const eff = marketSettings()
646
+ const eff = builtinSettings()
631
647
  if (eff.syncOnStartup) {
632
- marketStateLoaded.then(() => runMarketSync()).catch(e => ctx.logger.warn(`experts-management: startup market sync: ${e && e.message}`))
648
+ builtinStateLoaded.then(() => runBuiltinSync()).catch(e => ctx.logger.warn(`experts-management: startup builtin sync: ${e && e.message}`))
633
649
  }
634
650
  const timer = setInterval(() => {
635
- const eff2 = marketSettings()
651
+ const eff2 = builtinSettings()
636
652
  if (!eff2.autoSync) return
637
- const last = marketState.lastSyncAt ? Date.parse(marketState.lastSyncAt) : 0
653
+ const last = builtinState.lastSyncAt ? Date.parse(builtinState.lastSyncAt) : 0
638
654
  if (Date.now() - last > 24 * 3600 * 1000) {
639
- runMarketSync().catch(e => ctx.logger.warn(`experts-management: auto market sync: ${e && e.message}`))
655
+ runBuiltinSync().catch(e => ctx.logger.warn(`experts-management: auto builtin sync: ${e && e.message}`))
640
656
  }
641
657
  }, 6 * 3600 * 1000)
642
658
  if (typeof timer.unref === 'function') timer.unref()
643
659
  return () => clearInterval(timer)
644
- }, 'experts-management: market auto-sync')
660
+ }, 'experts-management: builtin auto-sync')
645
661
 
646
662
  // ── Discovery ────────────────────────────────────────────────────────
647
663
  async function discoverAll() {
648
- const installed = [], market = []
664
+ const mine = [], builtin = []
649
665
  const errors = []
650
666
  for (const row of allSourceRows()) {
651
667
  const { experts, errors: errs } = await scanExpertsRoot(row.root, row.key)
652
668
  errors.push(...errs)
653
- if (row.key === 'dsh') installed.push(...experts)
654
- else market.push(...experts)
669
+ if (row.key === 'dsh') mine.push(...experts)
670
+ else builtin.push(...experts)
655
671
  }
656
- return { installed, market, errors }
672
+ return { mine, builtin, errors }
657
673
  }
658
674
 
659
675
  async function locateExpert(name, sourceKey) {
@@ -681,7 +697,7 @@ module.exports = {
681
697
  return {
682
698
  name: providerName,
683
699
  async list() {
684
- const { installed, market } = await discoverAll()
700
+ const { mine, builtin } = await discoverAll()
685
701
  const candidates = []
686
702
  const seen = new Set()
687
703
  // 专家一律 modelInvocable:false:不进模型目录(零 token 污染),
@@ -713,11 +729,11 @@ module.exports = {
713
729
  metadata: { expertType: e.expertType, profession: e.professionZh ?? e.professionEn, version: e.version },
714
730
  })
715
731
  }
716
- for (const e of installed) push(e, 'user-installed', RANK_INSTALLED)
717
- const installedNames = new Set(installed.map((e) => e.name))
718
- for (const e of market) {
719
- if (installedNames.has(e.name)) continue // 用户库覆盖市场同名专家
720
- push(e, 'market', RANK_MARKET)
732
+ for (const e of mine) push(e, 'user-installed', RANK_INSTALLED)
733
+ const mineNames = new Set(mine.map((e) => e.name))
734
+ for (const e of builtin) {
735
+ if (mineNames.has(e.name)) continue // 用户库覆盖内置同名专家
736
+ push(e, 'builtin', RANK_BUILTIN)
721
737
  }
722
738
  return candidates
723
739
  },
@@ -767,10 +783,10 @@ module.exports = {
767
783
  const apiPath = url.pathname.replace(/\/+$/, '')
768
784
  const query = url.searchParams
769
785
 
770
- // GET /experts-management/api → { sources, installed, market }
786
+ // GET /experts-management/api → { sources, mine, builtin }
771
787
  if (req.method === 'GET' && apiPath === '/experts-management/api') {
772
- const { installed, market, errors } = await discoverAll()
773
- const installedNames = new Set(installed.map((e) => e.name))
788
+ const { mine, builtin, errors } = await discoverAll()
789
+ const mineNames = new Set(mine.map((e) => e.name))
774
790
  const summarize = (e) => ({
775
791
  name: e.name,
776
792
  displayName: e.displayNameZh ?? e.displayNameEn ?? e.name,
@@ -780,13 +796,13 @@ module.exports = {
780
796
  tags: e.tags.filter((t) => t.zh || t.en).map((t) => t.zh || t.en),
781
797
  hasAvatar: e.avatar !== undefined,
782
798
  source: e.source,
783
- installed: installedNames.has(e.name),
799
+ installed: mineNames.has(e.name),
784
800
  mtime: e.mtime,
785
801
  })
786
802
  sendJson(res, 200, {
787
803
  sources: allSourceRows().map((row) => ({ key: row.key, label: row.label, dir: displayPath(row.root), readOnly: row.readOnly })),
788
- installed: installed.map(summarize),
789
- market: market.map(summarize),
804
+ mine: mine.map(summarize),
805
+ builtin: builtin.map(summarize),
790
806
  errors,
791
807
  })
792
808
  return
@@ -854,7 +870,7 @@ module.exports = {
854
870
  if (req.method === 'POST' && apiPath.endsWith('/experts-management/api/install')) {
855
871
  const body = await readJsonBody(req)
856
872
  if (typeof body.name !== 'string' || body.name === '') { sendJson(res, 400, { error: 'body must provide name' }); return }
857
- const { expert } = await locateExpert(body.name, typeof body.from === 'string' && body.from !== '' && body.from !== 'market' ? body.from : undefined)
873
+ const { expert } = await locateExpert(body.name, typeof body.from === 'string' && body.from !== '' && body.from !== 'builtin' ? body.from : undefined)
858
874
  if (!isSafeExpertName(expert.name)) throw new Error(`invalid expert name: ${expert.name}`)
859
875
  const target = join(installedDir, expert.name)
860
876
  if (body.overwrite !== true) {
@@ -883,11 +899,11 @@ module.exports = {
883
899
  return
884
900
  }
885
901
 
886
- // GET /experts-management/api/market/status
887
- if (req.method === 'GET' && apiPath.endsWith('/experts-management/api/market/status')) {
888
- await marketStateLoaded
889
- const eff = marketSettings()
890
- const repoDir = marketRootDir()
902
+ // GET /experts-management/api/builtin/status
903
+ if (req.method === 'GET' && apiPath.endsWith('/experts-management/api/builtin/status')) {
904
+ await builtinStateLoaded
905
+ const eff = builtinSettings()
906
+ const repoDir = builtinRootDir()
891
907
  const repoExists = await fsP.access(join(repoDir, '.git')).then(() => true).catch(() => false)
892
908
  const ok = await gitAvailable(eff.gitBinary)
893
909
  const [localCommit, remoteCommit] = repoExists && ok
@@ -898,28 +914,28 @@ module.exports = {
898
914
  gitAvailable: ok, repoExists,
899
915
  localCommit, remoteCommit,
900
916
  needsUpdate: localCommit !== undefined && remoteCommit !== undefined ? localCommit !== remoteCommit : undefined,
901
- lastSyncAt: marketState.lastSyncAt, lastResult: marketState.lastResult,
917
+ lastSyncAt: builtinState.lastSyncAt, lastResult: builtinState.lastResult,
902
918
  autoSync: eff.autoSync, syncOnStartup: eff.syncOnStartup,
903
919
  hasToken: typeof eff.token === 'string' && eff.token !== '',
904
- syncing: marketSyncRun !== null,
905
- sparsePaths: marketSparsePaths() ?? null,
920
+ syncing: builtinSyncRun !== null,
921
+ sparsePaths: builtinSparsePaths() ?? null,
906
922
  })
907
923
  return
908
924
  }
909
925
 
910
- // POST /experts-management/api/market/sync
911
- if (req.method === 'POST' && apiPath.endsWith('/experts-management/api/market/sync')) {
926
+ // POST /experts-management/api/builtin/sync
927
+ if (req.method === 'POST' && apiPath.endsWith('/experts-management/api/builtin/sync')) {
912
928
  try {
913
- const result = await runMarketSync()
929
+ const result = await runBuiltinSync()
914
930
  sendJson(res, 200, result)
915
931
  } catch (e) { sendJson(res, 400, { error: String(e && e.message || e) }) }
916
932
  return
917
933
  }
918
934
 
919
- // PUT /experts-management/api/market/settings {url?, branch?, repoDir?, token?, autoSync?, syncOnStartup?}
920
- if (req.method === 'PUT' && apiPath.endsWith('/experts-management/api/market/settings')) {
935
+ // PUT /experts-management/api/builtin/settings {url?, branch?, repoDir?, token?, autoSync?, syncOnStartup?}
936
+ if (req.method === 'PUT' && apiPath.endsWith('/experts-management/api/builtin/settings')) {
921
937
  const body = await readJsonBody(req)
922
- await marketStateLoaded
938
+ await builtinStateLoaded
923
939
  const patch = {}
924
940
  for (const key of ['url', 'branch', 'gitBinary']) {
925
941
  if (typeof body[key] === 'string' && body[key] !== '') patch[key] = body[key]
@@ -935,7 +951,7 @@ module.exports = {
935
951
  } else {
936
952
  Object.assign(settingsOverrides, patch)
937
953
  }
938
- const eff = marketSettings()
954
+ const eff = builtinSettings()
939
955
  const { token, ...safe } = eff // token 只写不回读
940
956
  sendJson(res, 200, { settings: safe, hasToken: typeof token === 'string' && token !== '' })
941
957
  return