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 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.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
- status === 'loading' && plugins.length === 0
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
  }
Binary file
Binary file
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.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 }