dsh-pick 0.0.0-stage → 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 +142 -2
- package/bin/dsh-pick.js +155 -0
- package/data/catalog.json +1 -0
- package/lib/classify.js +56 -0
- package/lib/engine.js +409 -0
- package/lib/metrics.js +105 -0
- package/lib/words.js +71 -0
- package/package.json +23 -4
- package/plugin/client.js +119 -0
- package/plugin/cordis.patch.yml +4 -0
- package/plugin/index.js +109 -0
- package/web/panel.html +272 -0
- package/web/panel.js +428 -0
package/README.md
CHANGED
|
@@ -1,3 +1,143 @@
|
|
|
1
|
-
#
|
|
1
|
+
# dsh-pick
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**帮你在 DSH 生态里选对插件。**
|
|
4
|
+
|
|
5
|
+
说需求 → 找候选 → 多维比较 → 出结论。下载量只是证据,判断才算数。
|
|
6
|
+
|
|
7
|
+
> 定位对比:`dsh-find-plugin` 负责「**找到**」(搜 GitHub 仓库,按 star 排);
|
|
8
|
+
> `dsh-pick` 负责「**挑对**」(算去噪指标,给首选 / 备选 / 不推荐)。
|
|
9
|
+
> 一个 `find_dsh_plugin`,一个 `pick_plugin`。
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## 为什么需要它
|
|
14
|
+
|
|
15
|
+
npm 官网和各类 stat 站能看数字,但看数字回答不了「我该装哪个」。
|
|
16
|
+
|
|
17
|
+
更麻烦的是,**npm 下载量是噪声极大的指标**:版本一发布,CI、镜像、npm 客户端缓存刷新会把当天数字推高一个数量级。按原始下载量排出来的「第一」,很可能只是「发版本最勤的那个」。
|
|
18
|
+
|
|
19
|
+
所以 `dsh-pick` 的核心不是下载量,而是**去噪后的指标**:
|
|
20
|
+
|
|
21
|
+
| 指标 | 含义 |
|
|
22
|
+
|---|---|
|
|
23
|
+
| 日常基线 | 扣掉发布日与次日后的每日中位数,最接近真实使用量 |
|
|
24
|
+
| 近 7 日环比 | 在涨还是在凉 |
|
|
25
|
+
| 版本节奏 | 平均几天发一个版本,维护者的投入度 |
|
|
26
|
+
| 最近更新 | 是不是已经弃坑 |
|
|
27
|
+
| 健康度 | 以上四项加权,基线 40% + 增速 25% + 新鲜度 20% + 节奏 15% |
|
|
28
|
+
|
|
29
|
+
效果示例:`dshmarket` 周下载 160,042,日常基线只有 19,721 —— 相差 8 倍。
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## 用法
|
|
34
|
+
|
|
35
|
+
### 作为 DSH 插件(推荐)
|
|
36
|
+
|
|
37
|
+
```sh
|
|
38
|
+
dsh plugin --profile web add dsh-pick
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
装完重启 `dsh web`,打开 **设置 → dsh-pick**,就是完整的选插件面板。
|
|
42
|
+
|
|
43
|
+
首次使用要先生成目录(在 dsh-pick 的安装目录下,或任意目录):
|
|
44
|
+
|
|
45
|
+
```sh
|
|
46
|
+
npx dsh-pick build --deep 332
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
不跑也行——面板会提示「目录未生成」,排行榜和搜索会空着,但界面照常打开。
|
|
50
|
+
|
|
51
|
+
### 作为命令行 / 独立网页
|
|
52
|
+
|
|
53
|
+
```sh
|
|
54
|
+
npm i -g dsh-pick # 或 npx dsh-pick
|
|
55
|
+
|
|
56
|
+
dsh-pick build --deep 332 # 抓目录 + 算去噪指标
|
|
57
|
+
dsh-pick serve --port 8891 # 起本地服务,打开面板
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
装好后浏览器开 `http://127.0.0.1:8891/`。
|
|
61
|
+
|
|
62
|
+
### 命令
|
|
63
|
+
|
|
64
|
+
| 命令 | 作用 |
|
|
65
|
+
|---|---|
|
|
66
|
+
| `build [--deep N] [--pages N]` | 抓目录并算去噪指标 |
|
|
67
|
+
| `build --deep-only` | 只补算缺失的去噪指标(不用重抓目录,省 5 分钟) |
|
|
68
|
+
| `search "<需求>"` | 按需求描述找候选 |
|
|
69
|
+
| `rank [--metric ...] [--cat ...]` | 排行榜,指标支持 `weekly` / `baseline` / `growth` / `score` |
|
|
70
|
+
| `stats <pkg> [pkg...]` | 精确数据,目录没有就实时查 npm |
|
|
71
|
+
| `serve [--port N]` | 本地预览面板 |
|
|
72
|
+
|
|
73
|
+
示例:
|
|
74
|
+
|
|
75
|
+
```sh
|
|
76
|
+
dsh-pick search "我想装个数据库"
|
|
77
|
+
dsh-pick search "任务完成时给我发微信通知"
|
|
78
|
+
dsh-pick rank --metric baseline --cat 记忆与上下文 --limit 20
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## 结构
|
|
84
|
+
|
|
85
|
+
```
|
|
86
|
+
dsh-pick/
|
|
87
|
+
├── package.json
|
|
88
|
+
├── bin/dsh-pick.js CLI 入口
|
|
89
|
+
├── lib/
|
|
90
|
+
│ ├── engine.js 数据引擎:抓目录、算指标、查询(一个引擎,多个入口)
|
|
91
|
+
│ ├── classify.js 13 类 + 其他,关键词加权分类
|
|
92
|
+
│ ├── metrics.js 去噪指标:基线 / 增速 / 节奏 / 新鲜度 / 健康度
|
|
93
|
+
│ ├── words.js 需求 → 搜索词(同义词 + 中文二元切分),CLI 与面板共用
|
|
94
|
+
│ └── normalize.js (在 engine.js 内)排名与链接字段
|
|
95
|
+
├── data/catalog.json 目录产物
|
|
96
|
+
├── plugin/
|
|
97
|
+
│ ├── index.js DSH 宿主端:挂 /api-dsh-pick/* 路由
|
|
98
|
+
│ ├── client.js 设置页面板(iframe 复用 web/panel.html)
|
|
99
|
+
│ └── cordis.patch.yml 插进「设置 → dsh-pick」
|
|
100
|
+
└── web/
|
|
101
|
+
├── panel.html 选插件面板(独立调试用,也是插件内嵌的同一份)
|
|
102
|
+
└── panel.js 面板逻辑
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
面板只有一份代码,两种宿主都认:独立 `dsh-pick serve` 走根路径,插件内嵌走 `/api-dsh-pick` 前缀(`panel.html` 顶部的小加载器自动判断)。
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## 数据口径
|
|
110
|
+
|
|
111
|
+
- **来源**:`registry.npmjs.org`(元数据、版本发布历史)+ `api.npmjs.org/downloads`(下载量)
|
|
112
|
+
- **滞后**:npm 统计约滞后 5 天,页面显示的「数据截止」是最后一个已汇总的完整日
|
|
113
|
+
- **覆盖**:npm 上带 `dsh-plugin` 关键字的 2,250 个包(search 接口深分页上限约 2,250)
|
|
114
|
+
- **去噪范围**:只有周下载 Top 332 会抓 30 天日序列和发布历史,其余只有周/月下载量
|
|
115
|
+
- **下载量口径**:含 CI、镜像源、爬虫、缓存回源等非人类流量,**不等于用户数**
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## 已实现 / 待做
|
|
120
|
+
|
|
121
|
+
**已实现**
|
|
122
|
+
- 目录构建(`build` / `build --deep-only`,带 3 轮补抓对付限流)
|
|
123
|
+
- 13 类自动分类
|
|
124
|
+
- 去噪指标 + 健康度
|
|
125
|
+
- 需求搜索(同义词 + 中文二元切分)
|
|
126
|
+
- 排行榜(4 种指标 × 分类筛选)
|
|
127
|
+
- 选插件四步流程 + 规则版结论
|
|
128
|
+
- 结论导出为独立网页
|
|
129
|
+
- 我的监控(localStorage)
|
|
130
|
+
|
|
131
|
+
**待做(见方案文档)**
|
|
132
|
+
- DSH 插件形态:托管端 `plugin/index.js` + `plugin/client.js` + `cordis.patch.yml`
|
|
133
|
+
- 5 + 2 个 AI 工具:`pick_ranking` / `pick_search` / `pick_stats` / `pick_compare` / `pick_trend` / `pick_recommend` / `pick_export`
|
|
134
|
+
- skill `plugin-analyst`(意图路由 + 口径说明 + 禁止 AI 自己推算数字)
|
|
135
|
+
- AI 语义分类,把「其他」从 29% 压到 10% 以内
|
|
136
|
+
- 周报(可指定方向)
|
|
137
|
+
- PDF 导出(DSH Desktop 可用 Electron `printToPDF`)
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## 许可
|
|
142
|
+
|
|
143
|
+
MIT
|
package/bin/dsh-pick.js
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// bin/dsh-pick.js — dsh-pick 命令行
|
|
3
|
+
//
|
|
4
|
+
// dsh-pick build [--deep 200] [--pages 9] 抓取目录并算去噪指标
|
|
5
|
+
// dsh-pick search "我想装个数据库" 按需求找候选
|
|
6
|
+
// dsh-pick rank [--metric weekly|baseline|growth|score] [--cat 分类] [--limit 30]
|
|
7
|
+
// dsh-pick stats <pkg> [pkg...] 精确数据(目录没有就实时查)
|
|
8
|
+
// dsh-pick serve [--port 8891] 起本地服务,预览面板
|
|
9
|
+
|
|
10
|
+
import { createServer } from 'node:http'
|
|
11
|
+
import { readFile } from 'node:fs/promises'
|
|
12
|
+
import { join, extname } from 'node:path'
|
|
13
|
+
import { fileURLToPath } from 'node:url'
|
|
14
|
+
import { buildCatalog, loadCatalog, searchByNeed, rank, stats, normalize } from '../lib/engine.js'
|
|
15
|
+
|
|
16
|
+
const PKG_ROOT = fileURLToPath(new URL('..', import.meta.url))
|
|
17
|
+
const [, , cmd = 'help', ...rest] = process.argv
|
|
18
|
+
|
|
19
|
+
function arg(name, dflt) {
|
|
20
|
+
const i = rest.indexOf('--' + name)
|
|
21
|
+
return i >= 0 && rest[i + 1] && !rest[i + 1].startsWith('--') ? rest[i + 1] : dflt
|
|
22
|
+
}
|
|
23
|
+
const has = name => rest.includes('--' + name)
|
|
24
|
+
// 位置参数:把 `--flag 值` 整对踢掉,剩下的才是需求文本
|
|
25
|
+
const positionals = (() => {
|
|
26
|
+
const out = []
|
|
27
|
+
for (let i = 0; i < rest.length; i++) {
|
|
28
|
+
if (rest[i].startsWith('--')) { if (rest[i + 1] && !rest[i + 1].startsWith('--')) i++; continue }
|
|
29
|
+
out.push(rest[i])
|
|
30
|
+
}
|
|
31
|
+
return out
|
|
32
|
+
})()
|
|
33
|
+
|
|
34
|
+
const pad = (s, n) => String(s).padEnd(n)
|
|
35
|
+
const padL = (s, n) => String(s).padStart(n)
|
|
36
|
+
const num = n => (n === null || n === undefined) ? '—' : Number(n).toLocaleString('en-US')
|
|
37
|
+
const pct = n => n === null || n === undefined ? '—' : (n >= 0 ? '+' : '') + (n * 100).toFixed(0) + '%'
|
|
38
|
+
|
|
39
|
+
async function main() {
|
|
40
|
+
switch (cmd) {
|
|
41
|
+
case 'build': {
|
|
42
|
+
const deep = Number(arg('deep', 200))
|
|
43
|
+
const pages = Number(arg('pages', 9))
|
|
44
|
+
await buildCatalog({ deep, pages, deepOnly: has('deep-only'), onProgress: m => console.log('·', m) })
|
|
45
|
+
break
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
case 'search': {
|
|
49
|
+
const need = positionals.join(' ') || arg('need', '')
|
|
50
|
+
const cat = await loadCatalog()
|
|
51
|
+
const limit = Number(arg('limit', 12))
|
|
52
|
+
const r = searchByNeed(cat, need, { limit })
|
|
53
|
+
if (!r.length) { console.log('没找到候选。目录需先跑 `dsh-pick build`。'); break }
|
|
54
|
+
console.log(`\n需求:「${need}」 候选 ${r.length} 个(相关性 / 评分 / 周下载 / 日常基线 / 环比 / 最近更新)\n`)
|
|
55
|
+
for (const x of r) {
|
|
56
|
+
const d = x.deep
|
|
57
|
+
console.log(
|
|
58
|
+
pad(x.name, 34) + padL(x.relevance + '%', 6) +
|
|
59
|
+
' ' + padL(num(x.weekly), 9) + ' ' +
|
|
60
|
+
(d ? padL(num(d.baseline), 9) + ' ' + padL(pct(d.growth), 8) + ' ' + pad(d.ageDays + 'd', 6) : pad('—', 25)) +
|
|
61
|
+
' ' + x.category
|
|
62
|
+
)
|
|
63
|
+
if (x.description) console.log(' ' + x.description.slice(0, 100))
|
|
64
|
+
}
|
|
65
|
+
console.log()
|
|
66
|
+
break
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
case 'rank': {
|
|
70
|
+
const cat = await loadCatalog()
|
|
71
|
+
const metric = arg('metric', 'weekly')
|
|
72
|
+
const category = arg('cat', null)
|
|
73
|
+
const limit = Number(arg('limit', 30))
|
|
74
|
+
const rows = rank(cat, { metric, category, limit })
|
|
75
|
+
console.log(`\n榜单:${category || '全部'} · 指标 ${metric} · 截止 ${cat.anchor} 共 ${cat.total} 个包\n`)
|
|
76
|
+
console.log(pad('排名', 5) + pad('插件', 34) + padL('周下载', 10) + padL('月下载', 10) + ' ' + padL('基线', 9) + ' ' + padL('环比', 8) + ' ' + pad('更新', 6) + '分类')
|
|
77
|
+
rows.forEach((r, i) => {
|
|
78
|
+
const d = r.deep
|
|
79
|
+
console.log(
|
|
80
|
+
padL(i + 1, 4) + ' ' + pad(r.n, 34) + padL(num(r.w), 10) + padL(num(r.m), 10) + ' ' +
|
|
81
|
+
(d ? padL(num(d.base), 9) + ' ' + padL(pct(d.growth), 8) + ' ' + pad(d.age + 'd', 6) : pad('—', 24) + ' ') +
|
|
82
|
+
r.cat
|
|
83
|
+
)
|
|
84
|
+
})
|
|
85
|
+
console.log()
|
|
86
|
+
break
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
case 'stats': {
|
|
90
|
+
const names = positionals
|
|
91
|
+
if (!names.length) { console.log('用法:dsh-pick stats <pkg> [pkg...]'); break }
|
|
92
|
+
const cat = await loadCatalog()
|
|
93
|
+
const rows = await stats(cat, names)
|
|
94
|
+
for (const r of rows) {
|
|
95
|
+
if (r.source === 'not-found') { console.log(`\n${r.name}:npm 上不存在或无名下数据`); continue }
|
|
96
|
+
const d = r.deep
|
|
97
|
+
console.log(`\n${r.name} ${r.version || ''} [${r.category || '未分类'}] (${r.source})`)
|
|
98
|
+
if (r.description) console.log(' ' + r.description.slice(0, 120))
|
|
99
|
+
console.log(` 周 ${num(r.weekly)} · 月 ${num(r.monthly)}${r.total != null ? ' · 累计 ' + num(r.total) : ''}`)
|
|
100
|
+
if (d) console.log(` 日常基线 ${num(d.baseline)} · 环比 ${pct(d.growth)} · ${d.versions} 个版本 · ${d.cadence} 天/版 · ${d.ageDays} 天前更新 · 评分 ${d.score}`)
|
|
101
|
+
}
|
|
102
|
+
console.log()
|
|
103
|
+
break
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
case 'serve': {
|
|
107
|
+
const port = Number(arg('port', 8891))
|
|
108
|
+
const types = { '.html': 'text/html', '.json': 'application/json', '.js': 'text/javascript' }
|
|
109
|
+
const extName = f => f.slice(f.lastIndexOf('.'))
|
|
110
|
+
// 只放行这三个目录:面板、目录数据、共享词典
|
|
111
|
+
const ROOTS = {
|
|
112
|
+
'/web/': join(PKG_ROOT, 'web'),
|
|
113
|
+
'/data/': join(PKG_ROOT, 'data'),
|
|
114
|
+
'/lib/': join(PKG_ROOT, 'lib'),
|
|
115
|
+
}
|
|
116
|
+
const server = createServer(async (req, res) => {
|
|
117
|
+
try {
|
|
118
|
+
let p = decodeURIComponent(new URL(req.url, 'http://x').pathname)
|
|
119
|
+
if (p === '/') p = '/web/panel.html'
|
|
120
|
+
const prefix = Object.keys(ROOTS).find(k => p.startsWith(k))
|
|
121
|
+
if (!prefix) { res.writeHead(403).end('forbidden'); return }
|
|
122
|
+
let buf
|
|
123
|
+
if (prefix === '/data/') {
|
|
124
|
+
// 目录要走一遍归一化,补上排名与链接字段
|
|
125
|
+
buf = Buffer.from(JSON.stringify(await loadCatalog()), 'utf8')
|
|
126
|
+
} else {
|
|
127
|
+
buf = await readFile(join(ROOTS[prefix], p.slice(prefix.length)))
|
|
128
|
+
}
|
|
129
|
+
res.writeHead(200, {
|
|
130
|
+
'content-type': (types[extName(p)] || 'text/plain') + '; charset=utf-8',
|
|
131
|
+
'cache-control': 'no-store',
|
|
132
|
+
})
|
|
133
|
+
res.end(buf)
|
|
134
|
+
} catch {
|
|
135
|
+
res.writeHead(404).end('not found')
|
|
136
|
+
}
|
|
137
|
+
})
|
|
138
|
+
server.listen(port, '127.0.0.1', () => console.log(`dsh-pick 面板 → http://127.0.0.1:${port}/`))
|
|
139
|
+
break
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
default:
|
|
143
|
+
console.log(`
|
|
144
|
+
dsh-pick — DSH 插件选型助手
|
|
145
|
+
|
|
146
|
+
build 抓取目录并算去噪指标 --deep 200 --pages 9
|
|
147
|
+
search 按需求描述找候选 --limit 12
|
|
148
|
+
rank 排行榜 --metric weekly|baseline|growth|score --cat 分类 --limit 30
|
|
149
|
+
stats 精确数据(实时兜底) dsh-pick stats <pkg> [pkg...]
|
|
150
|
+
serve 本地预览面板 --port 8891
|
|
151
|
+
`)
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
main().catch(e => { console.error('失败:', e.message); process.exit(1) })
|