dsh-plugin-panel 0.1.0 → 0.1.2
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 +9 -1
- package/client.js +146 -6
- package/docs/promo-3x4.png +0 -0
- package/docs/promo-wechat.png +0 -0
- package/docs/screenshot.png +0 -0
- package/index.js +30 -1
- package/package.json +2 -1
- package/trend.js +107 -0
package/README.md
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# 插件管理 · dsh-plugin-panel
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
DSH 官方插件管理翻不动(还没修),所以自己做了一个。欢迎使用。
|
|
4
|
+
|
|
5
|
+
这是个给 DeepSeek Harness 右侧栏用的插件管理器。装上以后右侧栏会多一个「插件管理」标签,把已安装的插件列出来,并且告诉你每个插件到底从哪里进。
|
|
4
6
|
|
|
5
7
|

|
|
6
8
|
|
|
@@ -24,6 +26,12 @@
|
|
|
24
26
|
- **打开所在位置**:在访达里定位到这个插件的实际目录。
|
|
25
27
|
- **启停**:不必回设置页,详情里直接停用或启用。
|
|
26
28
|
|
|
29
|
+
## 市场趋势
|
|
30
|
+
|
|
31
|
+
面板里有一个「市场」视图,显示 `dsh-plugin-shop` 目录每天收录了多少插件,以及被过滤掉多少。
|
|
32
|
+
|
|
33
|
+
数据来自该目录每天发布的 npm 快照(每个版本就是当天的一份记录),首次打开会回溯历史,之后每天增量补齐,缓存 6 小时。它统计的是这个目录收录的数,不是 npm 全站的包数。
|
|
34
|
+
|
|
27
35
|
## 安装
|
|
28
36
|
|
|
29
37
|
```bash
|
package/client.js
CHANGED
|
@@ -3,10 +3,10 @@ window.__ModuleLoader__.load({
|
|
|
3
3
|
factory(require) {
|
|
4
4
|
const React = require('react')
|
|
5
5
|
const h = React.createElement
|
|
6
|
-
const PANEL_ID = 'dsh-
|
|
6
|
+
const PANEL_ID = 'dsh-plugin-panel'
|
|
7
7
|
const TAB_KIND = 'plugin-canvas'
|
|
8
|
-
const NS = 'dsh-
|
|
9
|
-
const VERSION = 'v0.1.
|
|
8
|
+
const NS = 'dsh-plugin-panel'
|
|
9
|
+
const VERSION = 'v0.1.2'
|
|
10
10
|
const PREF_KEY = 'dsh.plugin-list.pref.v2'
|
|
11
11
|
|
|
12
12
|
const COPY = {
|
|
@@ -18,6 +18,16 @@ window.__ModuleLoader__.load({
|
|
|
18
18
|
'list.builtin': '系统内置',
|
|
19
19
|
'list.all': '全部',
|
|
20
20
|
'list.refresh': '刷新',
|
|
21
|
+
'list.market': '市场',
|
|
22
|
+
'list.back': '返回',
|
|
23
|
+
'list.marketTitle': '插件市场趋势',
|
|
24
|
+
'list.marketHint': '来自 dsh-plugin-shop 目录的每日快照,不是 npm 全站统计。',
|
|
25
|
+
'list.marketLoading': '正在取历史数据,第一次大约十几秒…',
|
|
26
|
+
'list.marketError': '取不到目录数据。如果刚更新过插件,重启一次 DeepSeek Harness 再试。',
|
|
27
|
+
'list.countNow': '当前收录',
|
|
28
|
+
'list.countFiltered': '已过滤',
|
|
29
|
+
'list.perDay': '日均新增',
|
|
30
|
+
'list.spanDays': '覆盖天数',
|
|
21
31
|
'list.viewList': '列表',
|
|
22
32
|
'list.viewCard': '卡片',
|
|
23
33
|
'list.open': '打开',
|
|
@@ -81,6 +91,16 @@ window.__ModuleLoader__.load({
|
|
|
81
91
|
'list.builtin': 'Built-in',
|
|
82
92
|
'list.all': 'All',
|
|
83
93
|
'list.refresh': 'Refresh',
|
|
94
|
+
'list.market': 'Market',
|
|
95
|
+
'list.back': 'Back',
|
|
96
|
+
'list.marketTitle': 'Plugin market trend',
|
|
97
|
+
'list.marketHint': 'Daily snapshots of the dsh-plugin-shop catalog, not a site-wide npm count.',
|
|
98
|
+
'list.marketLoading': 'Fetching history, about ten seconds the first time…',
|
|
99
|
+
'list.marketError': 'Could not read the catalog. Restart DeepSeek Harness once if you just updated.',
|
|
100
|
+
'list.countNow': 'Listed',
|
|
101
|
+
'list.countFiltered': 'Filtered',
|
|
102
|
+
'list.perDay': 'Per day',
|
|
103
|
+
'list.spanDays': 'Days',
|
|
84
104
|
'list.viewList': 'List',
|
|
85
105
|
'list.viewCard': 'Cards',
|
|
86
106
|
'list.open': 'Open',
|
|
@@ -253,6 +273,44 @@ window.__ModuleLoader__.load({
|
|
|
253
273
|
return ''
|
|
254
274
|
}
|
|
255
275
|
|
|
276
|
+
function trendChart(days) {
|
|
277
|
+
const W = 1000
|
|
278
|
+
const H = 340
|
|
279
|
+
const L = 76
|
|
280
|
+
const R = 28
|
|
281
|
+
const T = 24
|
|
282
|
+
const B = 52
|
|
283
|
+
const values = days.map((d) => d.count)
|
|
284
|
+
const max = Math.max(...values, 1) * 1.04
|
|
285
|
+
const n = days.length
|
|
286
|
+
const x = (i) => L + (W - L - R) * (n <= 1 ? 0.5 : i / (n - 1))
|
|
287
|
+
const y = (v) => T + (H - T - B) * (1 - v / max)
|
|
288
|
+
const line = days.map((d, i) => `${i === 0 ? 'M' : 'L'} ${x(i).toFixed(1)} ${y(d.count).toFixed(1)}`).join(' ')
|
|
289
|
+
const area = `${line} L ${x(n - 1).toFixed(1)} ${H - B} L ${x(0).toFixed(1)} ${H - B} Z`
|
|
290
|
+
const ticks = [0, 0.25, 0.5, 0.75, 1].map((r) => Math.round((max * r) / 1000) * 1000)
|
|
291
|
+
const labelIndexes = n <= 1 ? [0] : [0, Math.floor((n - 1) / 2), n - 1]
|
|
292
|
+
return h('svg', { className: 'dsh-pl-chart', viewBox: `0 0 ${W} ${H}`, role: 'img' },
|
|
293
|
+
ticks.map((tick, i) => h('line', {
|
|
294
|
+
key: `g${i}`, x1: L, x2: W - R, y1: y(tick), y2: y(tick),
|
|
295
|
+
stroke: 'var(--dsw-alias-border-l1)', strokeWidth: 1,
|
|
296
|
+
})),
|
|
297
|
+
ticks.map((tick, i) => h('text', {
|
|
298
|
+
key: `t${i}`, x: L - 12, y: y(tick) + 6, textAnchor: 'end',
|
|
299
|
+
fill: 'var(--dsw-alias-label-secondary)', fontSize: 20,
|
|
300
|
+
}, `${Math.round(tick / 1000)}k`)),
|
|
301
|
+
h('path', { d: area, fill: 'var(--dsw-alias-brand-primary)', opacity: 0.12 }),
|
|
302
|
+
h('path', { d: line, fill: 'none', stroke: 'var(--dsw-alias-brand-primary)', strokeWidth: 4, strokeLinejoin: 'round' }),
|
|
303
|
+
labelIndexes.map((i) => h('text', {
|
|
304
|
+
key: `d${i}`, x: x(i), y: H - B + 34, textAnchor: i === 0 ? 'start' : i === n - 1 ? 'end' : 'middle',
|
|
305
|
+
fill: 'var(--dsw-alias-label-secondary)', fontSize: 20,
|
|
306
|
+
}, days[i].day.slice(5))),
|
|
307
|
+
h('text', {
|
|
308
|
+
x: x(n - 1), y: y(values[n - 1]) - 16, textAnchor: 'end',
|
|
309
|
+
fill: 'var(--dsw-alias-label-primary)', fontSize: 26, fontWeight: 600,
|
|
310
|
+
}, String(values[n - 1])),
|
|
311
|
+
)
|
|
312
|
+
}
|
|
313
|
+
|
|
256
314
|
function detailNames(plugin, bundle) {
|
|
257
315
|
const names = []
|
|
258
316
|
const moduleName = String((plugin && plugin.moduleName) || '')
|
|
@@ -349,6 +407,14 @@ window.__ModuleLoader__.load({
|
|
|
349
407
|
throw new Error(result && result.error && result.error.message ? result.error.message : 'list failed')
|
|
350
408
|
}
|
|
351
409
|
|
|
410
|
+
const SELF_NAMES = new Set(['dsh-plugin-panel', 'dsh-plugins-ui', 'dsh-plugin-canvas'])
|
|
411
|
+
|
|
412
|
+
function isSelf(plugin) {
|
|
413
|
+
const entry = String((plugin && plugin.entryId) || '').replace(/^include:/, '')
|
|
414
|
+
const moduleName = String((plugin && plugin.moduleName) || '')
|
|
415
|
+
return SELF_NAMES.has(entry) || SELF_NAMES.has(moduleName) || SELF_NAMES.has(shortModule(moduleName))
|
|
416
|
+
}
|
|
417
|
+
|
|
352
418
|
function tabKindFor(plugin, entries, localeId) {
|
|
353
419
|
const names = new Set([
|
|
354
420
|
plugin.entryId,
|
|
@@ -380,7 +446,7 @@ window.__ModuleLoader__.load({
|
|
|
380
446
|
function placesFor(plugin, guide, seats, localeId) {
|
|
381
447
|
const names = namesOf(plugin)
|
|
382
448
|
const places = []
|
|
383
|
-
if (tabKindFor(plugin, guide, localeId)) places.push('right')
|
|
449
|
+
if (tabKindFor(plugin, guide, localeId) || isSelf(plugin)) places.push('right')
|
|
384
450
|
for (const seat of seats || []) {
|
|
385
451
|
const id = seat.id || seat.key
|
|
386
452
|
const owner = seat.registrant
|
|
@@ -441,6 +507,9 @@ window.__ModuleLoader__.load({
|
|
|
441
507
|
const [filter, setFilter] = React.useState(pref.current.filter)
|
|
442
508
|
const [pins, setPins] = React.useState(pref.current.pins || [])
|
|
443
509
|
const [view, setView] = React.useState(pref.current.layout === 'card' ? 'card' : 'list')
|
|
510
|
+
const [surface, setSurface] = React.useState('plugins')
|
|
511
|
+
const [trend, setTrend] = React.useState(null)
|
|
512
|
+
const [trendStatus, setTrendStatus] = React.useState('')
|
|
444
513
|
const [plugins, setPlugins] = React.useState([])
|
|
445
514
|
const [bundles, setBundles] = React.useState([])
|
|
446
515
|
const [status, setStatus] = React.useState(listPlugins ? 'loading' : 'unavailable')
|
|
@@ -566,6 +635,27 @@ window.__ModuleLoader__.load({
|
|
|
566
635
|
return () => { cancelled = true }
|
|
567
636
|
}, [selectedId, plugins, bundles, loadFacts])
|
|
568
637
|
|
|
638
|
+
const loadTrend = React.useCallback(async () => {
|
|
639
|
+
if (typeof loadMarketTrend !== 'function') return
|
|
640
|
+
setTrendStatus('loading')
|
|
641
|
+
try {
|
|
642
|
+
const result = await loadMarketTrend()
|
|
643
|
+
const value = result && result.ok === true ? result.value : result
|
|
644
|
+
if (value && Array.isArray(value.days) && value.days.length) {
|
|
645
|
+
setTrend(value)
|
|
646
|
+
setTrendStatus('ready')
|
|
647
|
+
} else {
|
|
648
|
+
setTrendStatus('error')
|
|
649
|
+
}
|
|
650
|
+
} catch {
|
|
651
|
+
setTrendStatus('error')
|
|
652
|
+
}
|
|
653
|
+
}, [loadMarketTrend])
|
|
654
|
+
|
|
655
|
+
React.useEffect(() => {
|
|
656
|
+
if (surface === 'market' && trendStatus === '') void loadTrend()
|
|
657
|
+
}, [surface, trendStatus, loadTrend])
|
|
658
|
+
|
|
569
659
|
const visible = React.useMemo(() => {
|
|
570
660
|
const rows = plugins.filter((plugin) => {
|
|
571
661
|
const origin = originOf(plugin)
|
|
@@ -711,6 +801,40 @@ window.__ModuleLoader__.load({
|
|
|
711
801
|
}, label)
|
|
712
802
|
}
|
|
713
803
|
|
|
804
|
+
function marketView() {
|
|
805
|
+
const days = trend && Array.isArray(trend.days) ? trend.days : []
|
|
806
|
+
const first = days[0]
|
|
807
|
+
const last = days[days.length - 1]
|
|
808
|
+
const span = first && last
|
|
809
|
+
? Math.max(1, Math.round((Date.parse(last.day) - Date.parse(first.day)) / 86400000))
|
|
810
|
+
: 0
|
|
811
|
+
const perDay = first && last ? Math.round((last.count - first.count) / Math.max(span, 1)) : 0
|
|
812
|
+
return h('div', { className: 'dsh-pl-market' },
|
|
813
|
+
h('div', { className: 'dsh-pl-market-head' },
|
|
814
|
+
h('strong', null, textOf(t, 'list.marketTitle')),
|
|
815
|
+
h('p', { className: 'dsh-pl-note' }, textOf(t, 'list.marketHint'))),
|
|
816
|
+
trendStatus === 'loading'
|
|
817
|
+
? h('p', { className: 'dsh-pl-state' }, textOf(t, 'list.marketLoading'))
|
|
818
|
+
: trendStatus === 'error'
|
|
819
|
+
? h('p', { className: 'dsh-pl-state' }, textOf(t, 'list.marketError'))
|
|
820
|
+
: days.length
|
|
821
|
+
? [
|
|
822
|
+
h('div', { className: 'dsh-pl-market-stats' },
|
|
823
|
+
stat(textOf(t, 'list.countNow'), last.count.toLocaleString()),
|
|
824
|
+
stat(textOf(t, 'list.countFiltered'), (last.rejected || 0).toLocaleString()),
|
|
825
|
+
stat(textOf(t, 'list.perDay'), `+${perDay.toLocaleString()}`),
|
|
826
|
+
stat(textOf(t, 'list.spanDays'), String(days.length))),
|
|
827
|
+
trendChart(days),
|
|
828
|
+
]
|
|
829
|
+
: null)
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
function stat(label, value) {
|
|
833
|
+
return h('div', { className: 'dsh-pl-stat' },
|
|
834
|
+
h('span', null, label),
|
|
835
|
+
h('strong', null, value))
|
|
836
|
+
}
|
|
837
|
+
|
|
714
838
|
return h('div', { className: 'dsh-pl' },
|
|
715
839
|
h('style', null, CSS),
|
|
716
840
|
h('header', { className: 'dsh-pl-bar' },
|
|
@@ -720,11 +844,18 @@ window.__ModuleLoader__.load({
|
|
|
720
844
|
chip('builtin', textOf(t, 'list.builtin'), filter, (id) => { setFilter(id); remember({ filter: id }) }),
|
|
721
845
|
chip('all', textOf(t, 'list.all'), filter, (id) => { setFilter(id); remember({ filter: id }) })),
|
|
722
846
|
h('button', { type: 'button', onClick: () => { void load() } }, textOf(t, 'list.refresh')),
|
|
847
|
+
h('button', {
|
|
848
|
+
type: 'button',
|
|
849
|
+
className: `dsh-pl-market-btn${surface === 'market' ? ' is-on' : ''}`,
|
|
850
|
+
onClick: () => setSurface(surface === 'market' ? 'plugins' : 'market'),
|
|
851
|
+
}, surface === 'market' ? textOf(t, 'list.back') : textOf(t, 'list.market')),
|
|
723
852
|
h('div', { className: 'dsh-pl-view', role: 'group', 'aria-label': 'view' },
|
|
724
853
|
chip('list', textOf(t, 'list.viewList'), view, (id) => { setView(id); remember({ layout: id }) }),
|
|
725
854
|
chip('card', textOf(t, 'list.viewCard'), view, (id) => { setView(id); remember({ layout: id }) })),
|
|
726
855
|
h('span', { className: 'dsh-pl-version' }, VERSION))),
|
|
727
|
-
|
|
856
|
+
surface === 'market'
|
|
857
|
+
? marketView()
|
|
858
|
+
: status === 'loading' && plugins.length === 0
|
|
728
859
|
? h('p', { className: 'dsh-pl-state' }, textOf(t, 'list.loading'))
|
|
729
860
|
: status === 'unavailable'
|
|
730
861
|
? h('p', { className: 'dsh-pl-state' }, textOf(t, 'list.unavailable'))
|
|
@@ -846,6 +977,14 @@ window.__ModuleLoader__.load({
|
|
|
846
977
|
.dsh-pl-md th { color: var(--dsw-alias-label-secondary); font-weight: 650; background: var(--dsw-alias-bg-layer-1); white-space: nowrap; }
|
|
847
978
|
.dsh-pl-md td:first-child { white-space: nowrap; }
|
|
848
979
|
.dsh-pl-md tr:last-child td { border-bottom: 0; }
|
|
980
|
+
.dsh-pl-market-btn.is-on { background: var(--dsw-alias-bg-layer-2); }
|
|
981
|
+
.dsh-pl-market { flex: 1 1 auto; min-height: 0; overflow: auto; padding: 16px 18px 28px; }
|
|
982
|
+
.dsh-pl-market-head strong { display: block; font-size: 15px; margin-bottom: 6px; }
|
|
983
|
+
.dsh-pl-market-stats { display: flex; flex-wrap: wrap; gap: 10px; margin: 16px 0 8px; }
|
|
984
|
+
.dsh-pl-stat { display: flex; flex-direction: column; gap: 4px; min-width: 118px; padding: 10px 14px; border: 1px solid var(--dsw-alias-border-l2); border-radius: 12px; background: var(--dsw-alias-bg-layer-1); }
|
|
985
|
+
.dsh-pl-stat span { font-size: 11px; color: var(--dsw-alias-label-secondary); }
|
|
986
|
+
.dsh-pl-stat strong { font-size: 20px; font-weight: 600; }
|
|
987
|
+
.dsh-pl-chart { display: block; width: 100%; height: auto; margin-top: 12px; }
|
|
849
988
|
.dsh-pl-actions { display: flex; gap: 8px; align-items: center; margin-top: 16px; }
|
|
850
989
|
`
|
|
851
990
|
|
|
@@ -862,6 +1001,7 @@ window.__ModuleLoader__.load({
|
|
|
862
1001
|
const setPluginEnabled = pluginManager.setPluginEnabled.bind(pluginManager)
|
|
863
1002
|
const rpcCall = ctx.connection.rpc.call.bind(ctx.connection.rpc)
|
|
864
1003
|
const loadFacts = (payload) => rpcCall('/dsh-plugins-ui-rpc', 'facts', payload || {})
|
|
1004
|
+
const loadMarketTrend = (payload) => rpcCall('/dsh-plugins-ui-rpc', 'trend', payload || {})
|
|
865
1005
|
const revealPackage = async (payload) => {
|
|
866
1006
|
const input = payload && typeof payload === 'object' ? payload : {}
|
|
867
1007
|
const dir = directoryFromSource(input.source)
|
|
@@ -945,7 +1085,7 @@ window.__ModuleLoader__.load({
|
|
|
945
1085
|
name: 'sidebar.right.pane.tab',
|
|
946
1086
|
key: PANEL_ID,
|
|
947
1087
|
locale: NS,
|
|
948
|
-
inject: () => ({ listPlugins, listBundles, readBundleDetail, setPluginEnabled, loadFacts, revealPackage, listGuide, subscribeGuide, listSeats, subscribeSeats, openPanel, t, readLocale, subscribeLocale }),
|
|
1088
|
+
inject: () => ({ listPlugins, listBundles, readBundleDetail, setPluginEnabled, loadFacts, revealPackage, loadMarketTrend, listGuide, subscribeGuide, listSeats, subscribeSeats, openPanel, t, readLocale, subscribeLocale }),
|
|
949
1089
|
}, ListView)), 'plugin-list: tab body')
|
|
950
1090
|
},
|
|
951
1091
|
}
|
|
Binary file
|
package/docs/promo-wechat.png
CHANGED
|
Binary file
|
package/docs/screenshot.png
CHANGED
|
Binary file
|
package/index.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { createRequire } from 'node:module'
|
|
2
2
|
import { spawn } from 'node:child_process'
|
|
3
|
-
import { readFileSync, readdirSync, statSync } from 'node:fs'
|
|
3
|
+
import { mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs'
|
|
4
4
|
import path from 'node:path'
|
|
5
5
|
import { fileURLToPath } from 'node:url'
|
|
6
|
+
import { collectTrend } from './trend.js'
|
|
6
7
|
|
|
7
8
|
const CHANNEL = '/dsh-plugins-ui-rpc'
|
|
8
9
|
const MAX_SOURCE_BYTES = 1_200_000
|
|
@@ -63,6 +64,7 @@ function registerDetailRpc(ctx, services) {
|
|
|
63
64
|
else if (method === 'counts') result = { ok: true, value: countTools(services, record.payload) }
|
|
64
65
|
else if (method === 'facts') result = { ok: true, value: readFacts(record.payload) }
|
|
65
66
|
else if (method === 'reveal') result = { ok: true, value: revealPackage(record.payload) }
|
|
67
|
+
else if (method === 'trend') result = { ok: true, value: await readTrend() }
|
|
66
68
|
else throw new Error('Unknown detail method.')
|
|
67
69
|
} catch (error) {
|
|
68
70
|
const message = error instanceof Error ? error.message : String(error)
|
|
@@ -129,6 +131,33 @@ function directoryFromSource(source) {
|
|
|
129
131
|
}
|
|
130
132
|
}
|
|
131
133
|
|
|
134
|
+
function trendCacheFile() {
|
|
135
|
+
const profile = process.env.DSH_PROFILE_DIR || path.join(process.env.HOME || '', '.dsh/profiles/desktop')
|
|
136
|
+
return path.join(profile, '.plugin-panel', 'trend.json')
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function readTrendCache() {
|
|
140
|
+
try {
|
|
141
|
+
return JSON.parse(readFileSync(trendCacheFile(), 'utf8'))
|
|
142
|
+
} catch {
|
|
143
|
+
return null
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function writeTrendCache(value) {
|
|
148
|
+
try {
|
|
149
|
+
const file = trendCacheFile()
|
|
150
|
+
mkdirSync(path.dirname(file), { recursive: true })
|
|
151
|
+
writeFileSync(file, JSON.stringify(value))
|
|
152
|
+
} catch {
|
|
153
|
+
/* the chart still renders without a cache */
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function readTrend() {
|
|
158
|
+
return collectTrend(readTrendCache, writeTrendCache)
|
|
159
|
+
}
|
|
160
|
+
|
|
132
161
|
function readFacts(payload) {
|
|
133
162
|
const input = payload && typeof payload === 'object' ? payload : {}
|
|
134
163
|
const moduleName = typeof input.moduleName === 'string' ? input.moduleName : ''
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin-panel",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Plugin manager for the DeepSeek Harness right sidebar: list installed plugins, see where each one lives, pin favourites, and open a plugin folder in Finder.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
},
|
|
12
12
|
"files": [
|
|
13
13
|
"index.js",
|
|
14
|
+
"trend.js",
|
|
14
15
|
"client.js",
|
|
15
16
|
"catalog.js",
|
|
16
17
|
"catalog.test.mjs",
|
package/trend.js
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Catalog trend: how many plugins the dsh-plugin-shop catalog listed on each
|
|
3
|
+
* day. The catalog publishes one immutable npm version per build, so the
|
|
4
|
+
* version list doubles as a dated history; we read only the small index.json
|
|
5
|
+
* of each version instead of downloading its multi-megabyte tarball.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const CATALOG = 'dsh-plugin-shop-catalog'
|
|
9
|
+
const CDN_BASES = ['https://cdn.jsdelivr.net/npm', 'https://unpkg.com']
|
|
10
|
+
const REGISTRY = 'https://registry.npmjs.org'
|
|
11
|
+
const MAX_PARALLEL = 6
|
|
12
|
+
const FETCH_TIMEOUT_MS = 20000
|
|
13
|
+
|
|
14
|
+
async function fetchJson(url, timeoutMs = FETCH_TIMEOUT_MS) {
|
|
15
|
+
const controller = new AbortController()
|
|
16
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs)
|
|
17
|
+
try {
|
|
18
|
+
const response = await fetch(url, { signal: controller.signal })
|
|
19
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`)
|
|
20
|
+
return await response.json()
|
|
21
|
+
} finally {
|
|
22
|
+
clearTimeout(timer)
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function fetchIndex(version) {
|
|
27
|
+
for (const base of CDN_BASES) {
|
|
28
|
+
try {
|
|
29
|
+
return await fetchJson(`${base}/${CATALOG}@${version}/v1/index.json`)
|
|
30
|
+
} catch {
|
|
31
|
+
/* try the next mirror */
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return null
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function mapLimit(items, limit, worker) {
|
|
38
|
+
const queue = items.slice()
|
|
39
|
+
const runners = Array.from({ length: Math.min(limit, queue.length) }, async () => {
|
|
40
|
+
while (queue.length) {
|
|
41
|
+
const item = queue.shift()
|
|
42
|
+
try {
|
|
43
|
+
await worker(item)
|
|
44
|
+
} catch {
|
|
45
|
+
/* a single version failing must not abort the sweep */
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
})
|
|
49
|
+
await Promise.all(runners)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Fold per-version snapshots into one row per day, keeping the day's last build. */
|
|
53
|
+
function foldByDay(entries) {
|
|
54
|
+
const days = new Map()
|
|
55
|
+
for (const entry of entries) {
|
|
56
|
+
const day = (entry.builtAt || entry.publishedAt || '').slice(0, 10)
|
|
57
|
+
if (!day) continue
|
|
58
|
+
const current = days.get(day)
|
|
59
|
+
if (!current || (entry.builtAt || '') >= (current.builtAt || '')) days.set(day, { ...entry, day })
|
|
60
|
+
}
|
|
61
|
+
return [...days.values()].sort((a, b) => a.day.localeCompare(b.day))
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function collectTrend(readCache, writeCache) {
|
|
65
|
+
const cached = readCache()
|
|
66
|
+
if (cached && Array.isArray(cached.entries) && cached.entries.length) {
|
|
67
|
+
const age = Date.now() - (cached.fetchedAt || 0)
|
|
68
|
+
if (age < 6 * 3600 * 1000) return shape(cached.entries, cached.title, cached.at)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const meta = await fetchJson(`${REGISTRY}/${CATALOG}`)
|
|
72
|
+
const versions = Object.keys(meta.versions || {})
|
|
73
|
+
const published = meta.time || {}
|
|
74
|
+
const known = new Map((cached && Array.isArray(cached.entries) ? cached.entries : []).map((e) => [e.version, e]))
|
|
75
|
+
const missing = versions.filter((version) => !known.has(version))
|
|
76
|
+
|
|
77
|
+
await mapLimit(missing, MAX_PARALLEL, async (version) => {
|
|
78
|
+
const index = await fetchIndex(version)
|
|
79
|
+
if (!index) return
|
|
80
|
+
known.set(version, {
|
|
81
|
+
version,
|
|
82
|
+
count: Number(index.count) || 0,
|
|
83
|
+
rejected: Number(index.rejected) || 0,
|
|
84
|
+
builtAt: index.builtAt || published[version] || '',
|
|
85
|
+
publishedAt: published[version] || '',
|
|
86
|
+
})
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
const entries = [...known.values()].sort((a, b) => String(a.builtAt).localeCompare(String(b.builtAt)))
|
|
90
|
+
const at = new Date().toISOString()
|
|
91
|
+
writeCache({ entries, fetchedAt: Date.now(), at, title: CATALOG })
|
|
92
|
+
return shape(entries, CATALOG, at)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function shape(entries, title, at) {
|
|
96
|
+
const days = foldByDay(entries)
|
|
97
|
+
return {
|
|
98
|
+
title,
|
|
99
|
+
at,
|
|
100
|
+
days: days.map((d) => ({ day: d.day, count: d.count, rejected: d.rejected })),
|
|
101
|
+
versions: entries.length,
|
|
102
|
+
latest: days.length ? days[days.length - 1] : null,
|
|
103
|
+
first: days.length ? days[0] : null,
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export { collectTrend }
|