@noob-stupid/dsh-plugin-console 0.5.92 → 0.5.93

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.
@@ -0,0 +1,97 @@
1
+ // L2 · routes —— /check-updates(已安装插件「一键检测全部更新」;issue #22 ①)
2
+ //
3
+ // 用户原话(issue #22 ①):`familyCheckUpdate` 只覆盖全家桶内部,单独装的第三方插件要逐个点卡片上的
4
+ // 「检测更新」;市场 tab 的批量比对只在市场页生效,已安装列表看不到汇总。本路由补的就是这个汇总。
5
+ //
6
+ // 三条边界(都写在这里,别处不要再实现一遍):
7
+ // ① **判据不分叉**:每个包走 `routes/framework.js#probePackageUpdate` —— 单卡「检测更新」的同一份代码,
8
+ // 末尾是 `dropStaleUpdateCandidates` + `pickUpdateTarget`(即 `infra/semver.js` 的
9
+ // `<<<update-pick>>>` 唯一判据)。本文件里**没有任何版本比较**。
10
+ // ② **并发有限**:执行器是 `domain/update-batch.js#createBatchUpdater`(进程一份闸门,默认 4 条),
11
+ // 不是每来一个请求就无上限地并发出去(0.5.84 市场富化把网络打满那次事故的教训)。
12
+ // ③ **不谎报**:一个包的探针失败只影响它自己,且如实标成 `status: 'unknown'`("未检测到"),
13
+ // 绝不混进"已最新"——没查到 ≠ 没有新版。
14
+ //
15
+ // 入参:`{ packageNames: string[], force?: boolean }`(force=true 越过 60s 结论缓存)
16
+ // 出参:`{ ok, checked, updatable, unknown, items: [...], stats }`(items 顺序 = 请求顺序)
17
+ //
18
+ // 路由注册:`lib/server/routes/index.js` 的 ROUTES(POST,住在 405 守卫之后 —— 它吃 body);
19
+ // 路由清单:`tests/test-route-inventory.mjs`。
20
+ import { installedVersionOf, pickUpdateTarget } from '../domain/update-candidates.js'
21
+ import { createBatchUpdater } from '../domain/update-batch.js'
22
+ import { sendError, sendJson } from '../infra/httpd.js'
23
+ import { profileDirOf } from '../infra/paths.js'
24
+ import { probePackageUpdate } from './framework.js'
25
+
26
+ /** 合法的包名形态:`name` / `@scope/name`,允许子路径行(如 `@linxin666/dsh-web-all/plugin-manager`)。 */
27
+ const PACKAGE_NAME_RE = /^(@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*(\/[A-Za-z0-9._-]+)*$/u
28
+ const PACKAGE_NAME_MAX = 214
29
+ const BATCH_LIMIT = 200
30
+
31
+ // ★ 进程一份执行器:并发上限与结论缓存都在这个实例里共享 ——
32
+ // "用户连点两次不该变成两倍并发"这件事只有模块级实例能保证(见 domain/update-batch.js 顶部)。
33
+ const batchUpdater = createBatchUpdater({ max: BATCH_LIMIT })
34
+
35
+ /** 单测/诊断用:清空结论缓存(生产不会调)。 */
36
+ function __clearBatchUpdateCache() {
37
+ batchUpdater.clearCache()
38
+ }
39
+
40
+ async function routeCheckUpdates(req, res, rc) {
41
+ const ctx = rc.ctx
42
+ const body = rc.body ?? {}
43
+ const raw = body.packageNames
44
+ if (!Array.isArray(raw)) {
45
+ sendError(res, 400, 'packageNames 必须是包名数组(一次传要检测的全部已装插件)')
46
+ return
47
+ }
48
+ if (raw.length === 0) {
49
+ // 空列表不是错误:如实回"一件都没查",不凭空造一个"全部已最新"
50
+ sendJson(res, 200, { ok: true, checked: 0, updatable: 0, unknown: 0, items: [], stats: batchUpdater.stats(), note: '没有需要检测的插件(packageNames 为空)' })
51
+ return
52
+ }
53
+ if (raw.length > BATCH_LIMIT) {
54
+ sendError(res, 400, `一次最多检测 ${BATCH_LIMIT} 个包(收到 ${raw.length} 个)—— 请分批`)
55
+ return
56
+ }
57
+ const names = []
58
+ const bad = []
59
+ for (const item of raw) {
60
+ const name = typeof item === 'string' ? item.trim() : ''
61
+ if (name === '' || name.length > PACKAGE_NAME_MAX || !PACKAGE_NAME_RE.test(name)) {
62
+ bad.push(typeof item === 'string' ? item : JSON.stringify(item))
63
+ continue
64
+ }
65
+ names.push(name)
66
+ }
67
+ if (bad.length > 0) {
68
+ sendError(res, 400, `这些不是合法的包名:${bad.slice(0, 5).join('、')}${bad.length > 5 ? ` 等 ${bad.length} 个` : ''}`)
69
+ return
70
+ }
71
+ const baseUrl = ctx.baseUrl ?? 'file:///'
72
+ const profileDir = profileDirOf(ctx)
73
+ const result = await batchUpdater.run(names, {
74
+ // 每个包都走单卡那条**唯一实现**(含 GitHub release 兜底 + 陈旧候选净化);
75
+ // target 由**同一个** pickUpdateTarget 取(与 /check-update 的 sendJson 一字不差:同一批已净化候选)
76
+ probe: async (packageName) => {
77
+ const r = await probePackageUpdate(packageName, rc, { fetchJson: rc.deps.fetchJson })
78
+ return { ...r, target: pickUpdateTarget({ latest: r.latest, next: r.next, beta: r.beta }) }
79
+ },
80
+ installedOf: (packageName) => installedVersionOf(packageName, baseUrl, profileDir),
81
+ force: body.force === true,
82
+ })
83
+ if (result.error !== undefined) {
84
+ sendError(res, 500, result.error)
85
+ return
86
+ }
87
+ sendJson(res, 200, {
88
+ ok: true,
89
+ checked: result.checked,
90
+ updatable: result.updatable,
91
+ unknown: result.unknown,
92
+ items: result.items,
93
+ stats: result.stats,
94
+ })
95
+ }
96
+
97
+ export { routeCheckUpdates, __clearBatchUpdateCache, PACKAGE_NAME_RE, BATCH_LIMIT }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noob-stupid/dsh-plugin-console",
3
- "version": "0.5.92",
3
+ "version": "0.5.93",
4
4
  "description": "DSH 框架升级安全与插件升级门控:一键升级、失败自动回滚、升级后回滚上版、旧插件不适配自动禁用;内置多源插件市场为发现层,插件源全部可自定义,可指向公司内网私有源 / 私有索引 / 本地 Git 仓库,纯内网离线可用 | Framework upgrade safety & plugin version gating for DSH, with a customizable multi-source plugin market: point every source at internal mirrors or a local file:// repo for intranet-only, offline installs.",
5
5
  "repository": {
6
6
  "type": "git",