@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/README.md +14 -8
- package/client/bundle.js +308 -107
- package/client/index.js +308 -107
- package/docs/demo.gif +0 -0
- package/package.json +2 -2
- package/src/index.js +101 -85
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
|
-
* -
|
|
9
|
-
* plugin's own dir ($DSH_HOME/experts-management/
|
|
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
|
|
49
|
+
const BUILTIN_SCAN_SKIP = new Set(['.git', 'node_modules'])
|
|
50
50
|
const RANK_INSTALLED = 100
|
|
51
|
-
const
|
|
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 (
|
|
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
|
-
// ──
|
|
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
|
|
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
|
|
492
|
+
const DEFAULT_BUILTIN_SPARSE_PATHS = ['experts']
|
|
493
493
|
|
|
494
|
-
const
|
|
494
|
+
const BUILTIN_SETTINGS_NS = 'experts-management-builtin'
|
|
495
495
|
|
|
496
|
-
function
|
|
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.
|
|
511
|
-
const base = { ...
|
|
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.
|
|
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: '
|
|
564
|
+
{ key: 'builtin', label: '内置', root: join(builtinRootDir(), 'experts'), readOnly: true },
|
|
565
565
|
]
|
|
566
566
|
|
|
567
567
|
// ── Market sync state / settings(skills-management 同款管线)────────
|
|
568
|
-
const
|
|
569
|
-
const eff =
|
|
568
|
+
const builtinRootDir = () => {
|
|
569
|
+
const eff = builtinSettings()
|
|
570
570
|
return resolve(typeof eff.repoDir === 'string' && eff.repoDir !== '' ? eff.repoDir
|
|
571
|
-
: config.
|
|
572
|
-
: join(dshHome(), 'experts-management', '
|
|
571
|
+
: config.builtinRepoDir !== undefined ? resolve(String(config.builtinRepoDir))
|
|
572
|
+
: join(dshHome(), 'experts-management', 'builtin'))
|
|
573
573
|
}
|
|
574
|
-
const
|
|
574
|
+
const builtinSparsePaths = () => config.builtinSparsePaths === null
|
|
575
575
|
? undefined
|
|
576
|
-
: (Array.isArray(config.
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
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
|
|
583
|
-
.then(
|
|
584
|
-
|
|
585
|
-
|
|
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(
|
|
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
|
|
609
|
+
const saveBuiltinState = async () => {
|
|
594
610
|
try {
|
|
595
|
-
await fsP.mkdir(join(
|
|
596
|
-
await atomicWriteJs(
|
|
597
|
-
await fsP.chmod(
|
|
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
|
|
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
|
|
609
|
-
const
|
|
610
|
-
if (
|
|
611
|
-
|
|
612
|
-
await
|
|
613
|
-
const eff =
|
|
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 =
|
|
618
|
-
const result = await gitSyncRepo(eff.gitBinary, eff.url, eff.branch, repoDir, eff.token,
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
await
|
|
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 { ...
|
|
624
|
-
})().finally(() => {
|
|
625
|
-
return
|
|
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 =
|
|
646
|
+
const eff = builtinSettings()
|
|
631
647
|
if (eff.syncOnStartup) {
|
|
632
|
-
|
|
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 =
|
|
651
|
+
const eff2 = builtinSettings()
|
|
636
652
|
if (!eff2.autoSync) return
|
|
637
|
-
const last =
|
|
653
|
+
const last = builtinState.lastSyncAt ? Date.parse(builtinState.lastSyncAt) : 0
|
|
638
654
|
if (Date.now() - last > 24 * 3600 * 1000) {
|
|
639
|
-
|
|
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:
|
|
660
|
+
}, 'experts-management: builtin auto-sync')
|
|
645
661
|
|
|
646
662
|
// ── Discovery ────────────────────────────────────────────────────────
|
|
647
663
|
async function discoverAll() {
|
|
648
|
-
const
|
|
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')
|
|
654
|
-
else
|
|
669
|
+
if (row.key === 'dsh') mine.push(...experts)
|
|
670
|
+
else builtin.push(...experts)
|
|
655
671
|
}
|
|
656
|
-
return {
|
|
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 {
|
|
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
|
|
717
|
-
const
|
|
718
|
-
for (const e of
|
|
719
|
-
if (
|
|
720
|
-
push(e, '
|
|
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,
|
|
786
|
+
// GET /experts-management/api → { sources, mine, builtin }
|
|
771
787
|
if (req.method === 'GET' && apiPath === '/experts-management/api') {
|
|
772
|
-
const {
|
|
773
|
-
const
|
|
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:
|
|
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
|
-
|
|
789
|
-
|
|
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 !== '
|
|
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/
|
|
887
|
-
if (req.method === 'GET' && apiPath.endsWith('/experts-management/api/
|
|
888
|
-
await
|
|
889
|
-
const eff =
|
|
890
|
-
const repoDir =
|
|
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:
|
|
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:
|
|
905
|
-
sparsePaths:
|
|
920
|
+
syncing: builtinSyncRun !== null,
|
|
921
|
+
sparsePaths: builtinSparsePaths() ?? null,
|
|
906
922
|
})
|
|
907
923
|
return
|
|
908
924
|
}
|
|
909
925
|
|
|
910
|
-
// POST /experts-management/api/
|
|
911
|
-
if (req.method === 'POST' && apiPath.endsWith('/experts-management/api/
|
|
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
|
|
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/
|
|
920
|
-
if (req.method === 'PUT' && apiPath.endsWith('/experts-management/api/
|
|
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
|
|
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 =
|
|
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
|