dsh-plugin-panel 0.1.1 → 0.1.3
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 +820 -143
- package/docs/screenshot.png +0 -0
- package/index.js +99 -1
- package/package.json +5 -2
- package/trend.js +294 -0
- package/docs/promo-3x4.png +0 -0
- package/docs/promo-wechat.png +0 -0
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 { collectHot, collectSearch, 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,9 @@ 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() }
|
|
68
|
+
else if (method === 'hot') result = { ok: true, value: await readHot() }
|
|
69
|
+
else if (method === 'search') result = { ok: true, value: await readSearch(record.payload) }
|
|
66
70
|
else throw new Error('Unknown detail method.')
|
|
67
71
|
} catch (error) {
|
|
68
72
|
const message = error instanceof Error ? error.message : String(error)
|
|
@@ -129,6 +133,100 @@ function directoryFromSource(source) {
|
|
|
129
133
|
}
|
|
130
134
|
}
|
|
131
135
|
|
|
136
|
+
function trendCacheFile() {
|
|
137
|
+
const profile = process.env.DSH_PROFILE_DIR || path.join(process.env.HOME || '', '.dsh/profiles/desktop')
|
|
138
|
+
return path.join(profile, '.plugin-panel', 'trend.json')
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function readTrendCache() {
|
|
142
|
+
try {
|
|
143
|
+
return JSON.parse(readFileSync(trendCacheFile(), 'utf8'))
|
|
144
|
+
} catch {
|
|
145
|
+
return null
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function writeTrendCache(value) {
|
|
150
|
+
try {
|
|
151
|
+
const file = trendCacheFile()
|
|
152
|
+
mkdirSync(path.dirname(file), { recursive: true })
|
|
153
|
+
writeFileSync(file, JSON.stringify(value))
|
|
154
|
+
} catch {
|
|
155
|
+
/* the chart still renders without a cache */
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const memo = new Map()
|
|
160
|
+
|
|
161
|
+
/** Keep one in-process result briefly; the on-disk archive is the real cache. */
|
|
162
|
+
async function withMemo(key, ttlMs, run) {
|
|
163
|
+
const hit = memo.get(key)
|
|
164
|
+
if (hit && Date.now() - hit.at < ttlMs) return hit.value
|
|
165
|
+
const value = await run()
|
|
166
|
+
memo.set(key, { at: Date.now(), value })
|
|
167
|
+
return value
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function readTrend() {
|
|
171
|
+
return withMemo('trend', 5 * 60 * 1000, () => collectTrend(readTrendCache, writeTrendCache))
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function hotCacheFile() {
|
|
175
|
+
const profile = process.env.DSH_PROFILE_DIR || path.join(process.env.HOME || '', '.dsh/profiles/desktop')
|
|
176
|
+
return path.join(profile, '.plugin-panel', 'hot.json')
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function readHotCache() {
|
|
180
|
+
try {
|
|
181
|
+
return JSON.parse(readFileSync(hotCacheFile(), 'utf8'))
|
|
182
|
+
} catch {
|
|
183
|
+
return null
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function writeHotCache(value) {
|
|
188
|
+
try {
|
|
189
|
+
const file = hotCacheFile()
|
|
190
|
+
mkdirSync(path.dirname(file), { recursive: true })
|
|
191
|
+
writeFileSync(file, JSON.stringify(value))
|
|
192
|
+
} catch {
|
|
193
|
+
/* ranking still renders without a cache */
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function readHot() {
|
|
198
|
+
return withMemo('hot', 5 * 60 * 1000, () => collectHot(readHotCache, writeHotCache, 7))
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function searchCacheFile() {
|
|
202
|
+
const profile = process.env.DSH_PROFILE_DIR || path.join(process.env.HOME || '', '.dsh/profiles/desktop')
|
|
203
|
+
return path.join(profile, '.plugin-panel', 'search-index.json')
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function readSearchCache() {
|
|
207
|
+
try {
|
|
208
|
+
return JSON.parse(readFileSync(searchCacheFile(), 'utf8'))
|
|
209
|
+
} catch {
|
|
210
|
+
return null
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function writeSearchCache(value) {
|
|
215
|
+
try {
|
|
216
|
+
const file = searchCacheFile()
|
|
217
|
+
mkdirSync(path.dirname(file), { recursive: true })
|
|
218
|
+
writeFileSync(file, JSON.stringify(value))
|
|
219
|
+
} catch {
|
|
220
|
+
/* search still works without the cache */
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function readSearch(payload) {
|
|
225
|
+
const input = payload && typeof payload === 'object' ? payload : {}
|
|
226
|
+
const query = typeof input.query === 'string' ? input.query : ''
|
|
227
|
+
return collectSearch(readSearchCache, writeSearchCache, query, 100)
|
|
228
|
+
}
|
|
229
|
+
|
|
132
230
|
function readFacts(payload) {
|
|
133
231
|
const input = payload && typeof payload === 'object' ? payload : {}
|
|
134
232
|
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.3",
|
|
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,12 +11,15 @@
|
|
|
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",
|
|
17
18
|
"cordis.patch.yml",
|
|
18
19
|
"locale/*.json",
|
|
19
|
-
"docs",
|
|
20
|
+
"docs/DETAIL.md",
|
|
21
|
+
"docs/DETAIL.zh.md",
|
|
22
|
+
"docs/screenshot.png",
|
|
20
23
|
"icon.svg",
|
|
21
24
|
"README.md"
|
|
22
25
|
],
|
package/trend.js
ADDED
|
@@ -0,0 +1,294 @@
|
|
|
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
|
+
/** The catalog's current published version, without downloading the manifest. */
|
|
27
|
+
async function latestTag() {
|
|
28
|
+
try {
|
|
29
|
+
const tags = await fetchJson(`${REGISTRY}/-/package/${CATALOG}/dist-tags`, 10000)
|
|
30
|
+
return tags && typeof tags.latest === 'string' ? tags.latest : ''
|
|
31
|
+
} catch {
|
|
32
|
+
return ''
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function fetchIndex(version) {
|
|
37
|
+
for (const base of CDN_BASES) {
|
|
38
|
+
try {
|
|
39
|
+
return await fetchJson(`${base}/${CATALOG}@${version}/v1/index.json`)
|
|
40
|
+
} catch {
|
|
41
|
+
/* try the next mirror */
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return null
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function mapLimit(items, limit, worker) {
|
|
48
|
+
const queue = items.slice()
|
|
49
|
+
const runners = Array.from({ length: Math.min(limit, queue.length) }, async () => {
|
|
50
|
+
while (queue.length) {
|
|
51
|
+
const item = queue.shift()
|
|
52
|
+
try {
|
|
53
|
+
await worker(item)
|
|
54
|
+
} catch {
|
|
55
|
+
/* a single version failing must not abort the sweep */
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
})
|
|
59
|
+
await Promise.all(runners)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Fold per-version snapshots into one row per day, keeping the day's last build. */
|
|
63
|
+
function foldByDay(entries) {
|
|
64
|
+
const days = new Map()
|
|
65
|
+
for (const entry of entries) {
|
|
66
|
+
const day = (entry.builtAt || entry.publishedAt || '').slice(0, 10)
|
|
67
|
+
if (!day) continue
|
|
68
|
+
const current = days.get(day)
|
|
69
|
+
if (!current || (entry.builtAt || '') >= (current.builtAt || '')) days.set(day, { ...entry, day })
|
|
70
|
+
}
|
|
71
|
+
return [...days.values()].sort((a, b) => a.day.localeCompare(b.day))
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function collectTrend(readCache, writeCache) {
|
|
75
|
+
const latest = await latestTag()
|
|
76
|
+
const cached = readCache()
|
|
77
|
+
if (cached && Array.isArray(cached.entries) && cached.entries.length) {
|
|
78
|
+
// One probe per day is enough: the archive only grows when the catalog publishes.
|
|
79
|
+
if (!latest || cached.latest === latest) return shape(cached.entries, cached.title, cached.at)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const meta = await fetchJson(`${REGISTRY}/${CATALOG}`)
|
|
83
|
+
const versions = Object.keys(meta.versions || {})
|
|
84
|
+
const published = meta.time || {}
|
|
85
|
+
const known = new Map((cached && Array.isArray(cached.entries) ? cached.entries : []).map((e) => [e.version, e]))
|
|
86
|
+
const missing = versions.filter((version) => !known.has(version))
|
|
87
|
+
|
|
88
|
+
await mapLimit(missing, MAX_PARALLEL, async (version) => {
|
|
89
|
+
const index = await fetchIndex(version)
|
|
90
|
+
if (!index) return
|
|
91
|
+
known.set(version, {
|
|
92
|
+
version,
|
|
93
|
+
count: Number(index.count) || 0,
|
|
94
|
+
rejected: Number(index.rejected) || 0,
|
|
95
|
+
builtAt: index.builtAt || published[version] || '',
|
|
96
|
+
publishedAt: published[version] || '',
|
|
97
|
+
})
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
const entries = [...known.values()].sort((a, b) => String(a.builtAt).localeCompare(String(b.builtAt)))
|
|
101
|
+
const at = new Date().toISOString()
|
|
102
|
+
writeCache({ entries, fetchedAt: Date.now(), at, title: CATALOG, latest })
|
|
103
|
+
return shape(entries, CATALOG, at)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function shape(entries, title, at) {
|
|
107
|
+
const days = foldByDay(entries)
|
|
108
|
+
return {
|
|
109
|
+
title,
|
|
110
|
+
at,
|
|
111
|
+
days: days.map((d) => ({ day: d.day, count: d.count, rejected: d.rejected })),
|
|
112
|
+
versions: entries.length,
|
|
113
|
+
latest: days.length ? days[days.length - 1] : null,
|
|
114
|
+
first: days.length ? days[0] : null,
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Repository icons from the catalog, keyed by the `github:owner/repo` prefix. */
|
|
119
|
+
async function readIcons(version) {
|
|
120
|
+
const file = await readCatalogFile(version, 'icons')
|
|
121
|
+
const body = file && file.body
|
|
122
|
+
if (!body || !body.icons || !body.sources) return new Map()
|
|
123
|
+
const index = new Map()
|
|
124
|
+
for (const [source, hash] of Object.entries(body.sources)) {
|
|
125
|
+
const data = body.icons[hash]
|
|
126
|
+
if (!data) continue
|
|
127
|
+
if (source.startsWith('github:')) {
|
|
128
|
+
const repo = source.slice(7).split('@')[0].toLowerCase()
|
|
129
|
+
if (!index.has(`repo:${repo}`)) index.set(`repo:${repo}`, data)
|
|
130
|
+
} else if (source.startsWith('npm:')) {
|
|
131
|
+
const name = source.slice(4).split('@')[0].toLowerCase()
|
|
132
|
+
if (name && !index.has(`npm:${name}`)) index.set(`npm:${name}`, data)
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return index
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** `2026.1113.0` -> `2026-11-13`; the middle segment packs month and day. */
|
|
139
|
+
function versionDate(version) {
|
|
140
|
+
const [year, md] = String(version).split('.')
|
|
141
|
+
if (!year || !md) return ''
|
|
142
|
+
const month = md.length === 3 ? md.slice(0, 1) : md.slice(0, 2)
|
|
143
|
+
const day = md.length === 3 ? md.slice(1) : md.slice(2)
|
|
144
|
+
return `${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')}`
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function shiftDays(day, offset) {
|
|
148
|
+
const date = new Date(`${day}T00:00:00Z`)
|
|
149
|
+
date.setUTCDate(date.getUTCDate() + offset)
|
|
150
|
+
return date.toISOString().slice(0, 10)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Read one file out of a catalog version. */
|
|
154
|
+
async function readCatalogFile(version, name) {
|
|
155
|
+
const index = await fetchIndex(version)
|
|
156
|
+
if (!index || !index[name] || !index[name].url) return null
|
|
157
|
+
const url = `${CDN_BASES[0]}/${CATALOG}@${version}/v1/${index[name].url}`
|
|
158
|
+
try {
|
|
159
|
+
return { index, body: await fetchJson(url, 60000) }
|
|
160
|
+
} catch {
|
|
161
|
+
return { index, body: null }
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Plugins whose repository gained the most GitHub stars over a window.
|
|
167
|
+
* Star counts are repository-level, so entries sharing a repository collapse
|
|
168
|
+
* into one row; the caller should label them as repository stars.
|
|
169
|
+
*/
|
|
170
|
+
async function collectHot(readCache, writeCache, days = 7) {
|
|
171
|
+
const tag = await latestTag()
|
|
172
|
+
const cached = readCache()
|
|
173
|
+
if (cached && cached.value && Array.isArray(cached.value.top) && cached.value.top.length) {
|
|
174
|
+
if (!tag || cached.latest === tag) return cached.value
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const meta = await fetchJson(`${REGISTRY}/${CATALOG}`)
|
|
178
|
+
const versions = Object.keys(meta.versions || {})
|
|
179
|
+
if (!versions.length) throw new Error('catalog has no versions')
|
|
180
|
+
const latest = versions[versions.length - 1]
|
|
181
|
+
const latestDay = versionDate(latest)
|
|
182
|
+
const cutoff = shiftDays(latestDay, -days)
|
|
183
|
+
const older = versions.filter((v) => versionDate(v) <= cutoff).pop()
|
|
184
|
+
if (!older) throw new Error('catalog has no snapshot old enough')
|
|
185
|
+
|
|
186
|
+
const [current, previous] = await Promise.all([
|
|
187
|
+
readCatalogFile(latest, 'stars'),
|
|
188
|
+
readCatalogFile(older, 'stars'),
|
|
189
|
+
])
|
|
190
|
+
if (!current || !current.body || !previous || !previous.body) throw new Error('star snapshots unavailable')
|
|
191
|
+
const pluginsFile = await readCatalogFile(latest, 'plugins')
|
|
192
|
+
|
|
193
|
+
const byName = new Map()
|
|
194
|
+
for (const entry of (pluginsFile && pluginsFile.body && pluginsFile.body.plugins) || []) {
|
|
195
|
+
if (entry && entry.name) byName.set(entry.name, entry)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const repoOf = (key) => {
|
|
199
|
+
const entry = byName.get(key)
|
|
200
|
+
const repo = entry && entry.repository
|
|
201
|
+
return repo ? repo.replace(/^https?:\/\/github\.com\//i, '').replace(/\/$/, '').toLowerCase() : String(key).toLowerCase()
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const newStars = current.body.stars || {}
|
|
205
|
+
const oldStars = previous.body.stars || {}
|
|
206
|
+
const rows = new Map()
|
|
207
|
+
for (const [key, value] of Object.entries(newStars)) {
|
|
208
|
+
const before = oldStars[key]
|
|
209
|
+
if (typeof before !== 'number') continue
|
|
210
|
+
const delta = value - before
|
|
211
|
+
if (delta <= 0) continue
|
|
212
|
+
const repo = repoOf(key)
|
|
213
|
+
const seen = rows.get(repo)
|
|
214
|
+
if (seen && seen.delta >= delta) continue
|
|
215
|
+
const entry = byName.get(key)
|
|
216
|
+
const catalog = (entry && entry.catalog) || {}
|
|
217
|
+
const summary = catalog.summary || {}
|
|
218
|
+
rows.set(repo, {
|
|
219
|
+
name: (entry && entry.name) || key,
|
|
220
|
+
repo,
|
|
221
|
+
delta,
|
|
222
|
+
stars: value,
|
|
223
|
+
category: catalog.category || '',
|
|
224
|
+
summary: summary.zh || summary.en || '',
|
|
225
|
+
})
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const ranked = [...rows.values()].sort((a, b) => b.delta - a.delta).slice(0, 50)
|
|
229
|
+
const icons = await readIcons(latest).catch(() => new Map())
|
|
230
|
+
const top = ranked.map((row) => ({
|
|
231
|
+
...row,
|
|
232
|
+
icon: icons.get(`repo:${row.repo}`) || icons.get(`npm:${String(row.name).toLowerCase()}`) || '',
|
|
233
|
+
}))
|
|
234
|
+
const value = { days, from: older, to: latest, fromDay: versionDate(older), toDay: latestDay, total: rows.size, top }
|
|
235
|
+
writeCache({ fetchedAt: Date.now(), value, latest })
|
|
236
|
+
return value
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Compact plugin index for search: one row per package with only the fields
|
|
241
|
+
* the UI shows. Built once from the catalog and cached on disk.
|
|
242
|
+
*/
|
|
243
|
+
async function buildCatalogIndex() {
|
|
244
|
+
const tag = await latestTag()
|
|
245
|
+
const versions = await fetchJson(`${REGISTRY}/${CATALOG}`).then((m) => Object.keys(m.versions || {}))
|
|
246
|
+
const latest = versions[versions.length - 1]
|
|
247
|
+
if (!latest) throw new Error('catalog has no versions')
|
|
248
|
+
const file = await readCatalogFile(latest, 'plugins')
|
|
249
|
+
const entries = (file && file.body && file.body.plugins) || []
|
|
250
|
+
const rows = []
|
|
251
|
+
for (const entry of entries) {
|
|
252
|
+
if (!entry || !entry.name) continue
|
|
253
|
+
const catalog = entry.catalog || {}
|
|
254
|
+
const summary = catalog.summary || {}
|
|
255
|
+
rows.push({
|
|
256
|
+
name: entry.name,
|
|
257
|
+
version: entry.version || '',
|
|
258
|
+
category: catalog.category || '',
|
|
259
|
+
summary: summary.zh || summary.en || '',
|
|
260
|
+
repo: String(entry.repository || '').replace(/^https?:\/\/github\.com\//i, '').replace(/\/$/, ''),
|
|
261
|
+
})
|
|
262
|
+
}
|
|
263
|
+
return { latest: tag || latest, rows }
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
async function collectSearch(readCache, writeCache, query, limit = 100) {
|
|
267
|
+
const needle = String(query || '').trim().toLowerCase()
|
|
268
|
+
if (!needle) return { query: '', items: [], total: 0 }
|
|
269
|
+
|
|
270
|
+
let index = readCache()
|
|
271
|
+
if (!index || !Array.isArray(index.rows) || !index.rows.length) {
|
|
272
|
+
index = await buildCatalogIndex()
|
|
273
|
+
writeCache(index)
|
|
274
|
+
} else {
|
|
275
|
+
const tag = await latestTag()
|
|
276
|
+
if (tag && index.latest !== tag) {
|
|
277
|
+
index = await buildCatalogIndex()
|
|
278
|
+
writeCache(index)
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const items = []
|
|
283
|
+
for (const row of index.rows) {
|
|
284
|
+
if (row.name.toLowerCase().includes(needle)
|
|
285
|
+
|| row.summary.toLowerCase().includes(needle)
|
|
286
|
+
|| row.category.toLowerCase().includes(needle)) {
|
|
287
|
+
items.push(row)
|
|
288
|
+
if (items.length >= limit) break
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
return { query, items, total: index.rows.length }
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export { collectTrend, collectHot, collectSearch }
|
package/docs/promo-3x4.png
DELETED
|
Binary file
|
package/docs/promo-wechat.png
DELETED
|
Binary file
|