dsh-plugin-panel 0.1.1 → 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 +6 -0
- package/client.js +135 -3
- 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
|
@@ -26,6 +26,12 @@ DSH 官方插件管理翻不动(还没修),所以自己做了一个。欢
|
|
|
26
26
|
- **打开所在位置**:在访达里定位到这个插件的实际目录。
|
|
27
27
|
- **启停**:不必回设置页,详情里直接停用或启用。
|
|
28
28
|
|
|
29
|
+
## 市场趋势
|
|
30
|
+
|
|
31
|
+
面板里有一个「市场」视图,显示 `dsh-plugin-shop` 目录每天收录了多少插件,以及被过滤掉多少。
|
|
32
|
+
|
|
33
|
+
数据来自该目录每天发布的 npm 快照(每个版本就是当天的一份记录),首次打开会回溯历史,之后每天增量补齐,缓存 6 小时。它统计的是这个目录收录的数,不是 npm 全站的包数。
|
|
34
|
+
|
|
29
35
|
## 安装
|
|
30
36
|
|
|
31
37
|
```bash
|
package/client.js
CHANGED
|
@@ -6,7 +6,7 @@ window.__ModuleLoader__.load({
|
|
|
6
6
|
const PANEL_ID = 'dsh-plugin-panel'
|
|
7
7
|
const TAB_KIND = 'plugin-canvas'
|
|
8
8
|
const NS = 'dsh-plugin-panel'
|
|
9
|
-
const VERSION = 'v0.1.
|
|
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) || '')
|
|
@@ -449,6 +507,9 @@ window.__ModuleLoader__.load({
|
|
|
449
507
|
const [filter, setFilter] = React.useState(pref.current.filter)
|
|
450
508
|
const [pins, setPins] = React.useState(pref.current.pins || [])
|
|
451
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('')
|
|
452
513
|
const [plugins, setPlugins] = React.useState([])
|
|
453
514
|
const [bundles, setBundles] = React.useState([])
|
|
454
515
|
const [status, setStatus] = React.useState(listPlugins ? 'loading' : 'unavailable')
|
|
@@ -574,6 +635,27 @@ window.__ModuleLoader__.load({
|
|
|
574
635
|
return () => { cancelled = true }
|
|
575
636
|
}, [selectedId, plugins, bundles, loadFacts])
|
|
576
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
|
+
|
|
577
659
|
const visible = React.useMemo(() => {
|
|
578
660
|
const rows = plugins.filter((plugin) => {
|
|
579
661
|
const origin = originOf(plugin)
|
|
@@ -719,6 +801,40 @@ window.__ModuleLoader__.load({
|
|
|
719
801
|
}, label)
|
|
720
802
|
}
|
|
721
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
|
+
|
|
722
838
|
return h('div', { className: 'dsh-pl' },
|
|
723
839
|
h('style', null, CSS),
|
|
724
840
|
h('header', { className: 'dsh-pl-bar' },
|
|
@@ -728,11 +844,18 @@ window.__ModuleLoader__.load({
|
|
|
728
844
|
chip('builtin', textOf(t, 'list.builtin'), filter, (id) => { setFilter(id); remember({ filter: id }) }),
|
|
729
845
|
chip('all', textOf(t, 'list.all'), filter, (id) => { setFilter(id); remember({ filter: id }) })),
|
|
730
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')),
|
|
731
852
|
h('div', { className: 'dsh-pl-view', role: 'group', 'aria-label': 'view' },
|
|
732
853
|
chip('list', textOf(t, 'list.viewList'), view, (id) => { setView(id); remember({ layout: id }) }),
|
|
733
854
|
chip('card', textOf(t, 'list.viewCard'), view, (id) => { setView(id); remember({ layout: id }) })),
|
|
734
855
|
h('span', { className: 'dsh-pl-version' }, VERSION))),
|
|
735
|
-
|
|
856
|
+
surface === 'market'
|
|
857
|
+
? marketView()
|
|
858
|
+
: status === 'loading' && plugins.length === 0
|
|
736
859
|
? h('p', { className: 'dsh-pl-state' }, textOf(t, 'list.loading'))
|
|
737
860
|
: status === 'unavailable'
|
|
738
861
|
? h('p', { className: 'dsh-pl-state' }, textOf(t, 'list.unavailable'))
|
|
@@ -854,6 +977,14 @@ window.__ModuleLoader__.load({
|
|
|
854
977
|
.dsh-pl-md th { color: var(--dsw-alias-label-secondary); font-weight: 650; background: var(--dsw-alias-bg-layer-1); white-space: nowrap; }
|
|
855
978
|
.dsh-pl-md td:first-child { white-space: nowrap; }
|
|
856
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; }
|
|
857
988
|
.dsh-pl-actions { display: flex; gap: 8px; align-items: center; margin-top: 16px; }
|
|
858
989
|
`
|
|
859
990
|
|
|
@@ -870,6 +1001,7 @@ window.__ModuleLoader__.load({
|
|
|
870
1001
|
const setPluginEnabled = pluginManager.setPluginEnabled.bind(pluginManager)
|
|
871
1002
|
const rpcCall = ctx.connection.rpc.call.bind(ctx.connection.rpc)
|
|
872
1003
|
const loadFacts = (payload) => rpcCall('/dsh-plugins-ui-rpc', 'facts', payload || {})
|
|
1004
|
+
const loadMarketTrend = (payload) => rpcCall('/dsh-plugins-ui-rpc', 'trend', payload || {})
|
|
873
1005
|
const revealPackage = async (payload) => {
|
|
874
1006
|
const input = payload && typeof payload === 'object' ? payload : {}
|
|
875
1007
|
const dir = directoryFromSource(input.source)
|
|
@@ -953,7 +1085,7 @@ window.__ModuleLoader__.load({
|
|
|
953
1085
|
name: 'sidebar.right.pane.tab',
|
|
954
1086
|
key: PANEL_ID,
|
|
955
1087
|
locale: NS,
|
|
956
|
-
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 }),
|
|
957
1089
|
}, ListView)), 'plugin-list: tab body')
|
|
958
1090
|
},
|
|
959
1091
|
}
|
package/docs/promo-3x4.png
CHANGED
|
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 }
|