dsh-plugin 2.3.1 → 2.3.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.en.md +15 -21
- package/README.md +15 -21
- package/client/client.js +412 -382
- package/client/client.js.map +1 -1
- package/lib/http/catalog.js +4 -4
- package/lib/http/diagnostics.js +4 -4
- package/lib/http/install.js +21 -10
- package/lib/http/npm-release-age.js +9 -3
- package/lib/http/shared.js +1 -1
- package/lib/services/host-version.js +2 -2
- package/lib/services/install/dsh-compat.js +1 -1
- package/lib/services/install/npm-resolve.js +1 -1
- package/lib/services/install/operation-log.js +3 -3
- package/lib/services/install/preflight.js +1 -1
- package/lib/services/install/registry-fallback.js +3 -3
- package/lib/services/install/release-target.js +2 -2
- package/lib/services/install/task-queue.js +35 -20
- package/lib/services/loader.js +8 -8
- package/lib/services/npm-release-age.js +46 -9
- package/lib/services/probe.js +12 -12
- package/lib/services/profile/installed-versions.js +2 -2
- package/lib/services/profile/profile.js +2 -2
- package/lib/types/services/host-version.d.ts +1 -1
- package/lib/types/services/install/install-types.d.ts +5 -5
- package/lib/types/services/install/operation-log.d.ts +1 -1
- package/lib/types/services/install/preflight.d.ts +1 -1
- package/lib/types/services/install/registry-fallback.d.ts +1 -1
- package/lib/types/services/install/release-target.d.ts +1 -1
- package/lib/types/services/install/task-queue.d.ts +10 -7
- package/lib/types/services/loader.d.ts +5 -5
- package/lib/types/services/npm-release-age.d.ts +14 -2
- package/lib/types/services/probe.d.ts +4 -4
- package/package.json +1 -1
- package/src/client/components/PluginHubSection.tsx +59 -28
- package/src/client/components/catalog/CatalogList.tsx +1 -1
- package/src/client/components/catalog/PluginCard.tsx +3 -3
- package/src/client/components/modals/AboutModal.tsx +2 -2
- package/src/client/components/modals/ConfirmDialog.tsx +2 -2
- package/src/client/components/modals/HubUpdateModal.tsx +3 -3
- package/src/client/components/modals/InstalledDetailModal.tsx +1 -1
- package/src/client/components/modals/LogsPathModal.tsx +1 -1
- package/src/client/components/modals/NotificationsModal.tsx +2 -2
- package/src/client/components/modals/modals.tsx +13 -11
- package/src/client/components/views/CustomInstallView.tsx +2 -2
- package/src/client/components/views/InstalledView.tsx +2 -2
- package/src/client/components/views/SettingsView.tsx +3 -3
- package/src/client/data/catalog.ts +1 -1
- package/src/client/data/host.ts +3 -1
- package/src/client/data/hub.ts +3 -3
- package/src/client/hooks/useCatalog.ts +14 -14
- package/src/client/hooks/useSettings.ts +2 -2
- package/src/client/hooks/useTaskQueue.ts +31 -24
- package/src/client/locales.ts +21 -21
- package/src/client/logic/constants.ts +1 -1
- package/src/client/logic/ensure-plugin-css.ts +2 -2
- package/src/client/logic/failures.ts +9 -9
- package/src/client/logic/install-command.ts +5 -3
- package/src/client/logic/installed.ts +1 -1
- package/src/client/logic/release-age.ts +10 -10
- package/src/client/logic/urls.ts +3 -3
- package/src/client/styles/Header.module.css +1 -1
- package/src/client/styles/Modal.module.css +2 -2
- package/src/client/styles/SectionTabs.module.css +1 -1
- package/src/client/styles/tokens.module.css +1 -1
- package/src/client/types.ts +4 -4
- package/src/server/http/catalog.ts +4 -4
- package/src/server/http/diagnostics.ts +4 -4
- package/src/server/http/install.ts +20 -10
- package/src/server/http/npm-release-age.ts +9 -3
- package/src/server/http/shared.ts +1 -1
- package/src/server/services/host-version.ts +2 -2
- package/src/server/services/install/dsh-compat.ts +1 -1
- package/src/server/services/install/install-types.ts +5 -5
- package/src/server/services/install/npm-resolve.ts +1 -1
- package/src/server/services/install/operation-log.ts +3 -3
- package/src/server/services/install/preflight.ts +1 -1
- package/src/server/services/install/registry-fallback.ts +3 -3
- package/src/server/services/install/release-target.ts +2 -2
- package/src/server/services/install/task-queue.ts +40 -26
- package/src/server/services/loader.ts +10 -10
- package/src/server/services/npm-release-age.ts +50 -10
- package/src/server/services/probe.ts +12 -12
- package/src/server/services/profile/installed-versions.ts +2 -2
- package/src/server/services/profile/profile.ts +2 -2
|
@@ -11,10 +11,10 @@ import type { MouseEvent } from 'react'
|
|
|
11
11
|
import styles from '../styles/Header.module.css'
|
|
12
12
|
import modalStyles from '../styles/Modal.module.css'
|
|
13
13
|
import type { EnvInfo, HubPlugin, InstallChannel, SectionProps, ToastState } from '../types.ts'
|
|
14
|
-
import { installCommandOf, repoFromInstallTarget } from '../logic/install-command.ts'
|
|
14
|
+
import { installCommandOf, installTargetOf, repoFromInstallTarget } from '../logic/install-command.ts'
|
|
15
15
|
import { PLUGIN_VERSION } from '../logic/constants.ts'
|
|
16
16
|
import { ensurePluginCss } from '../logic/ensure-plugin-css.ts'
|
|
17
|
-
import { getEnv, revealInstallFolder } from '../data/host.ts'
|
|
17
|
+
import { fetchNpmReleaseAge, getEnv, revealInstallFolder } from '../data/host.ts'
|
|
18
18
|
import { useCatalog } from '../hooks/useCatalog.ts'
|
|
19
19
|
import { useLanguage } from '../hooks/useLanguage.ts'
|
|
20
20
|
import { useSettings } from '../hooks/useSettings.ts'
|
|
@@ -76,9 +76,11 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
|
|
|
76
76
|
const [confirmPlugin, setConfirmPlugin] = useState<HubPlugin | null>(null)
|
|
77
77
|
/** 弹窗动作是否为「更新」:已安装插件点「更新」→ 走同一条 add 命令原位覆盖重装,文案区分安装/更新 */
|
|
78
78
|
const [confirmIsUpdate, setConfirmIsUpdate] = useState(false)
|
|
79
|
-
/**
|
|
80
|
-
*
|
|
79
|
+
/** 安装要锁定的精确版本:自我更新提示的版本、新安装的已过冷却期版本都要锁定,
|
|
80
|
+
* 否则 @latest 会被 pnpm 带回冷却期内的旧版。 */
|
|
81
81
|
const [confirmPinVersion, setConfirmPinVersion] = useState<string | null>(null)
|
|
82
|
+
/** 冷却期查询的进行中序号:只采用最后一次弹窗的查询结果,避免切换插件后旧结果覆盖 */
|
|
83
|
+
const installPinSeq = useRef(0)
|
|
82
84
|
/** 命令行安装确认弹窗:记录待安装的裸目标(npm 包名)——
|
|
83
85
|
* 与应用商店同一套确认/进度/结果弹窗,提供时 plugin 传 null 走 customTarget 模式 */
|
|
84
86
|
const [confirmCustomTarget, setConfirmCustomTarget] = useState<string | null>(null)
|
|
@@ -104,12 +106,12 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
|
|
|
104
106
|
/** 安装/卸载完成后的结果视图:停留弹窗内,点「完成」关闭 */
|
|
105
107
|
const [installDone, setInstallDone] = useState(false)
|
|
106
108
|
const [uninstallDone, setUninstallDone] = useState(false)
|
|
107
|
-
/**
|
|
108
|
-
*
|
|
109
|
+
/** 结果视图是否给「立即重启」:服务端任务终态带出。仅刷新解决不了的例外场景
|
|
110
|
+
* (热加载失败 / 非 dsh 插件 / 更新)为 true;其余(热加载成功、卸载已即时停用)为 false,
|
|
109
111
|
* 结果视图只给「完成」,关闭时自动刷新页面生效 */
|
|
110
112
|
const [installNeedsRestart, setInstallNeedsRestart] = useState(false)
|
|
111
113
|
const [uninstallNeedsRestart, setUninstallNeedsRestart] = useState(false)
|
|
112
|
-
/**
|
|
114
|
+
/** 结果视图是否显示「关闭后自动刷新」说明:插件已热加载进运行中 loader(安装)、或卸载后
|
|
113
115
|
* 插件面板仍残留在当前页面(卸载)。关闭弹窗即整页刷新,不给手动刷新按钮。 */
|
|
114
116
|
const [installNeedsReload, setInstallNeedsReload] = useState(false)
|
|
115
117
|
/** 市场自更新成功、但 profile 的 bundle 列表里已不含 dsh-plugin:结果页提示重新勾选 */
|
|
@@ -133,7 +135,7 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
|
|
|
133
135
|
return () => { alive = false }
|
|
134
136
|
}, [])
|
|
135
137
|
|
|
136
|
-
//
|
|
138
|
+
// 样式自动恢复:宿主重启/热更可能移除 <style> 标签而模块缓存未失效(factory 不重跑、CSS 不回来)。
|
|
137
139
|
// 挂载与每次切换视图时比对全局 CSS 清单,把缺失的样式补注入回去 —— 用户无需再手动点击恢复。
|
|
138
140
|
useEffect(() => { ensurePluginCss() }, [view])
|
|
139
141
|
|
|
@@ -154,7 +156,7 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
|
|
|
154
156
|
setInstallNeedsReload(reload)
|
|
155
157
|
setInstallBundleDeselected(bundleDeselected === true)
|
|
156
158
|
if (viaModal) setInstallDone(true)
|
|
157
|
-
// 弹窗外的后台安装:给结果 toast
|
|
159
|
+
// 弹窗外的后台安装:给结果 toast;需要页面重载时(热加载的客户端 UI)读完后自动刷新
|
|
158
160
|
else setToast({
|
|
159
161
|
id: Date.now(),
|
|
160
162
|
kind: reload ? 'installedReload' : needsRestart ? 'done' : 'installedLive',
|
|
@@ -191,8 +193,8 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
|
|
|
191
193
|
// 失败自动写入通知记录:任务在后台结束时没人盯着也能留痕。
|
|
192
194
|
// 存原始 repo(可能只是裸包名)而非反查结果 —— 反查只服务于提 Issue,
|
|
193
195
|
// 通知中心的展示反查由 NotificationsModal 做、查不到会回退显示原始值,
|
|
194
|
-
// 若这里就丢掉原始值,通知里会连名字都看不到;action
|
|
195
|
-
setNotifications(addFailure({ kind, action: update ? 'update' : 'install', repo: repo ?? '', message, command, attempts }))
|
|
196
|
+
// 若这里就丢掉原始值,通知里会连名字都看不到;action 按任务类型区分「安装/更新/卸载失败」
|
|
197
|
+
setNotifications(addFailure({ kind, action: update ? 'update' : kind === 'uninstall' ? 'uninstall' : 'install', repo: repo ?? '', message, command, attempts }))
|
|
196
198
|
},
|
|
197
199
|
installPlugin: confirmPlugin,
|
|
198
200
|
installCustomTarget: confirmCustomTarget,
|
|
@@ -237,7 +239,7 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
|
|
|
237
239
|
useEffect(() => {
|
|
238
240
|
if (startupCheckedRef.current) return
|
|
239
241
|
if (!settingsReady || catalog.plugins === null || catalog.failed) return
|
|
240
|
-
//
|
|
242
|
+
// 发布冷却期结果未就绪时先等:否则刚发布的插件会先写进通知中心,冒出装不上的更新提醒
|
|
241
243
|
if (!catalog.releaseAgeReady) return
|
|
242
244
|
startupCheckedRef.current = true
|
|
243
245
|
if (!hubSettings.checkUpdatesOnStart) return
|
|
@@ -274,7 +276,7 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
|
|
|
274
276
|
return () => window.clearTimeout(timer)
|
|
275
277
|
}, [toast])
|
|
276
278
|
|
|
277
|
-
/** 复制文本到剪贴板,返回是否成功(Clipboard API + 隐藏 textarea
|
|
279
|
+
/** 复制文本到剪贴板,返回是否成功(Clipboard API + 隐藏 textarea 作为后备)。 */
|
|
278
280
|
const doCopy = async (text: string): Promise<boolean> => {
|
|
279
281
|
try {
|
|
280
282
|
await navigator.clipboard.writeText(text)
|
|
@@ -298,7 +300,7 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
|
|
|
298
300
|
}
|
|
299
301
|
}
|
|
300
302
|
|
|
301
|
-
/** 命令行安装目标是否已安装(与服务端 already
|
|
303
|
+
/** 命令行安装目标是否已安装(与服务端 already 判定同一套规则):GitHub 地址按仓库身份匹配
|
|
302
304
|
* (归一化后比对已装项的 repo/spec),npm 包名按依赖 key 直查。命中 → 弹窗转「更新」语义
|
|
303
305
|
* (mode=update 放行覆盖重装),避免重复点击撞 409 报错。 */
|
|
304
306
|
const customTargetInstalled = (raw: string): boolean => {
|
|
@@ -321,9 +323,9 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
|
|
|
321
323
|
}
|
|
322
324
|
|
|
323
325
|
/** 弹窗动作一:复制安装命令到剪贴板,引导去终端粘贴执行(npm 安装显示包名命令)。 */
|
|
324
|
-
const copyCommand = async (p: HubPlugin) => {
|
|
326
|
+
const copyCommand = async (p: HubPlugin, version?: string) => {
|
|
325
327
|
const repo = p.source?.repo ?? ''
|
|
326
|
-
const ok = await doCopy(installCommandOf(p))
|
|
328
|
+
const ok = await doCopy(installCommandOf(p, false, version))
|
|
327
329
|
if (ok) {
|
|
328
330
|
setCopied(repo)
|
|
329
331
|
setToast({ id: Date.now(), kind: 'copied' })
|
|
@@ -332,6 +334,21 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
|
|
|
332
334
|
setConfirmPlugin(null)
|
|
333
335
|
}
|
|
334
336
|
|
|
337
|
+
/** 新安装先查「已过 pnpm 发布冷却期」的版本:命令条与执行都锁定它,
|
|
338
|
+
* 否则 pnpm 会把发布未满 24h 的最新版不提示地回退成很旧的版本。查不到则不锁定,由服务端再解析一次。 */
|
|
339
|
+
const resolveInstallPin = (p: HubPlugin) => {
|
|
340
|
+
// 目录下发了官方安装命令的条目(CLI-only / 人工干预)只展示该命令,追加版本会改坏它
|
|
341
|
+
if (p.install?.command !== undefined && p.install.command !== '') return
|
|
342
|
+
const pkg = installTargetOf(p)
|
|
343
|
+
if (!pkg) return
|
|
344
|
+
const seq = ++installPinSeq.current
|
|
345
|
+
void fetchNpmReleaseAge([pkg]).then((res) => {
|
|
346
|
+
if (installPinSeq.current !== seq) return
|
|
347
|
+
const version = res?.[pkg]?.installable?.version
|
|
348
|
+
if (version !== undefined && version !== '') setConfirmPinVersion(version)
|
|
349
|
+
})
|
|
350
|
+
}
|
|
351
|
+
|
|
335
352
|
/** 卸载弹窗动作:复制卸载命令,万一直接卸载失败可去终端手动执行。
|
|
336
353
|
* 自定义安装(无目录数据)按已安装项包名直卸,同样给复制通道。 */
|
|
337
354
|
const copyUninstallCommand = async () => {
|
|
@@ -348,7 +365,7 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
|
|
|
348
365
|
}
|
|
349
366
|
|
|
350
367
|
/** 「立即重启」:POST 同源 /restart,宿主进程自杀重启;随后轮询服务恢复后整页刷新。
|
|
351
|
-
*
|
|
368
|
+
* 只在刷新解决不了的例外场景(热加载失败 / 非 dsh 插件 / 更新)出现。
|
|
352
369
|
* 桌面端宿主归应用壳管(服务端回 `desktop: true` 且不重启):不轮询,改提示退出重开。 */
|
|
353
370
|
const requestRestart = async () => {
|
|
354
371
|
if (restarting) return
|
|
@@ -363,7 +380,7 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
|
|
|
363
380
|
/* 服务已终止,无需处理 */
|
|
364
381
|
}
|
|
365
382
|
if (desktop) {
|
|
366
|
-
//
|
|
383
|
+
// 宿主不会重启,轮询等不到新服务:直接给出正确规则
|
|
367
384
|
setRestarting(false)
|
|
368
385
|
setToast({ id: Date.now(), kind: 'restartDesktop' })
|
|
369
386
|
return
|
|
@@ -392,7 +409,7 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
|
|
|
392
409
|
}
|
|
393
410
|
|
|
394
411
|
const { total, installedName, installedVersion, hasUpdate } = catalog
|
|
395
|
-
// 统计数字:官网 /api/stats.json
|
|
412
|
+
// 统计数字:官网 /api/stats.json 优先;拉取失败时改用已加载列表,避免展示 undefined。
|
|
396
413
|
const statsTotal = catalog.stats?.total ?? total
|
|
397
414
|
const statsVerified = catalog.stats?.verified ?? 0
|
|
398
415
|
|
|
@@ -431,7 +448,7 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
|
|
|
431
448
|
noticeCount: notifications.length + queue.queue.length + queue.pendingRestarts.length,
|
|
432
449
|
onOpenNotifications: () => {
|
|
433
450
|
// 打开弹窗时补齐更新提醒(受「启动时检查更新」开关约束):同一版本只一条,
|
|
434
|
-
//
|
|
451
|
+
// 清空/删除后重开仍恢复,直到插件更新完成才消失;冷却期查询结果未就绪时先不写,避免提示装不上的更新
|
|
435
452
|
if (hubSettings.checkUpdatesOnStart && catalog.releaseAgeReady) syncUpdateNotices()
|
|
436
453
|
setShowNotifications(true)
|
|
437
454
|
},
|
|
@@ -451,12 +468,17 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
|
|
|
451
468
|
? t('pluginsTotal', { n: count })
|
|
452
469
|
: t('filterResults', { n: count }),
|
|
453
470
|
onInstall: (p, opts) => {
|
|
471
|
+
const update = opts?.update ?? false
|
|
454
472
|
setInstallDone(false)
|
|
455
|
-
setConfirmIsUpdate(
|
|
473
|
+
setConfirmIsUpdate(update)
|
|
456
474
|
setConfirmPinVersion(null)
|
|
475
|
+
// 本次弹窗重新开始:作废上一次还没返回的冷却期查询
|
|
476
|
+
++installPinSeq.current
|
|
457
477
|
// 与卸载一致:重置弹窗关联任务,避免新弹窗误匹配到进行中的任务而禁用安装按钮
|
|
458
478
|
queue.clearModalTask()
|
|
459
479
|
setConfirmPlugin(p)
|
|
480
|
+
// 更新走 @latest 语义(客户端已按冷却期筛选),只有新安装需要锁定冷却期内可用的版本
|
|
481
|
+
if (!update) resolveInstallPin(p)
|
|
460
482
|
},
|
|
461
483
|
onUninstall: (p) => {
|
|
462
484
|
setUninstallDone(false)
|
|
@@ -489,6 +511,8 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
|
|
|
489
511
|
setInstallDone(false)
|
|
490
512
|
setConfirmIsUpdate(true)
|
|
491
513
|
setConfirmPinVersion(null)
|
|
514
|
+
// 更新不锁定版本:作废可能还没返回的新安装冷却期查询
|
|
515
|
+
++installPinSeq.current
|
|
492
516
|
queue.clearModalTask()
|
|
493
517
|
setConfirmPlugin(item.plugin)
|
|
494
518
|
},
|
|
@@ -552,7 +576,7 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
|
|
|
552
576
|
// 暂缓提示显示同一标题但只给「稍后再说」,无更新显示当前版本 + 记录 + 「已是最新」。
|
|
553
577
|
// 版本号与本机版本一律由 hubUpdateState / hubCurrentVersion 决定,不用远端版号冒充本机。
|
|
554
578
|
showHubUpdate && h(HubUpdateModal, {
|
|
555
|
-
//
|
|
579
|
+
// 提示目标是「已过发布冷却期」的版本,可能不是 latest —— 展示它与它的变更记录
|
|
556
580
|
info: catalog.hubUpdateTarget ?? catalog.hubUpdateInfo ?? { version: PLUGIN_VERSION },
|
|
557
581
|
state: catalog.hubUpdateState,
|
|
558
582
|
currentVersion: catalog.hubCurrentVersion,
|
|
@@ -565,8 +589,10 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
|
|
|
565
589
|
if (hubEntry) {
|
|
566
590
|
setInstallDone(false)
|
|
567
591
|
setConfirmIsUpdate(true)
|
|
568
|
-
//
|
|
592
|
+
// 锁定本次提示的版本:最新版可能仍在冷却期内,@latest 会被 pnpm 拒绝
|
|
569
593
|
setConfirmPinVersion(catalog.hubUpdateTarget?.version ?? null)
|
|
594
|
+
// 作废可能还没返回的新安装冷却期查询,避免它覆盖本次锁定的版本
|
|
595
|
+
++installPinSeq.current
|
|
570
596
|
queue.clearModalTask()
|
|
571
597
|
setConfirmPlugin(hubEntry)
|
|
572
598
|
}
|
|
@@ -574,7 +600,7 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
|
|
|
574
600
|
}),
|
|
575
601
|
// 「关注我们」弹窗:GitHub 图标后按钮点击打开 —— 平台介绍 + 用户反馈群二维码,
|
|
576
602
|
// 内容由 dsh-update Worker 的 /about 接口以 Markdown 形式推送,作者随时可改,非写死。
|
|
577
|
-
// Worker 未推送(info 为 null
|
|
603
|
+
// Worker 未推送(info 为 null)时弹窗仍能打开并展示备用文案,不影响其他功能。
|
|
578
604
|
showAbout && h(AboutModal, {
|
|
579
605
|
info: catalog.hubAboutInfo ?? null,
|
|
580
606
|
lang,
|
|
@@ -668,21 +694,26 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
|
|
|
668
694
|
langPath,
|
|
669
695
|
restarting,
|
|
670
696
|
update: confirmIsUpdate,
|
|
697
|
+
pinVersion: confirmPinVersion ?? undefined,
|
|
671
698
|
// 仅命令行插件(webInstallable=false / 人工干预 manualInstall):弹窗只展示命令供复制,不提供一键安装
|
|
672
699
|
cliOnly: confirmPlugin.install?.webInstallable === false || confirmPlugin.install?.manualInstall === true,
|
|
673
700
|
submitting: queue.submitting,
|
|
674
701
|
needsRestart: installNeedsRestart,
|
|
675
702
|
needsReload: installNeedsReload,
|
|
676
703
|
bundleDeselected: installBundleDeselected,
|
|
677
|
-
//
|
|
704
|
+
// 结果弹窗关闭即生效:安装热加载的插件要页面重载才渲染客户端 UI → 自动刷新,无需用户点按钮
|
|
678
705
|
onClose: () => {
|
|
679
706
|
const reload = installDone && installNeedsReload
|
|
680
707
|
setConfirmPlugin(null)
|
|
681
708
|
setInstallBundleDeselected(false)
|
|
682
709
|
if (reload) window.location.reload()
|
|
683
710
|
},
|
|
684
|
-
onCopy: () => copyCommand(confirmPlugin),
|
|
685
|
-
onInstall: () => queue.installNow(confirmPlugin,
|
|
711
|
+
onCopy: () => copyCommand(confirmPlugin, confirmPinVersion ?? undefined),
|
|
712
|
+
onInstall: () => queue.installNow(confirmPlugin, {
|
|
713
|
+
update: confirmIsUpdate,
|
|
714
|
+
// 新安装锁定已过冷却期的版本:查询尚未回来时为空,服务端会自行解析
|
|
715
|
+
pinVersion: confirmPinVersion ?? undefined,
|
|
716
|
+
}),
|
|
686
717
|
// 重启:结果视图「立即重启」也会中断进行中的任务 → 先弹重启确认弹窗
|
|
687
718
|
onRestart: () => setShowRestartConfirm(true),
|
|
688
719
|
}),
|
|
@@ -825,7 +856,7 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
|
|
|
825
856
|
onRunDiagnostics: openDiagnostics,
|
|
826
857
|
onClose: () => setErrorMsg(null),
|
|
827
858
|
}),
|
|
828
|
-
//
|
|
859
|
+
// 待重启确认弹窗:已安装列表行内「重启」按钮点击后弹出(例外场景),单个「立即重启」,关闭即视为稍后
|
|
829
860
|
showRestartConfirm && h(RestartConfirmModal, {
|
|
830
861
|
t,
|
|
831
862
|
restarting,
|
|
@@ -28,7 +28,7 @@ export function CatalogList({ plugins, failed, visible, total, t, langPath, relo
|
|
|
28
28
|
installedName: (p: HubPlugin) => string | null
|
|
29
29
|
installedVersion: (p: HubPlugin) => string | null
|
|
30
30
|
hasUpdate: (p: HubPlugin) => boolean
|
|
31
|
-
/**
|
|
31
|
+
/** 更新暂缓(新版本还没过冷却期)的可重试时刻(毫秒);无则 null */
|
|
32
32
|
updatePendingUntil: (p: HubPlugin) => number | null
|
|
33
33
|
langKey: LocaleId
|
|
34
34
|
onInstall: (p: HubPlugin, opts?: { update?: boolean }) => void
|
|
@@ -19,9 +19,9 @@ export function PluginCard({ plugin: p, copied, installedName, installedVersion,
|
|
|
19
19
|
installedName: (p: HubPlugin) => string | null
|
|
20
20
|
/** 已安装版本号(安装时从目录信号记录;无 release 的仓库为空,无版本号可展示) */
|
|
21
21
|
installedVersion: (p: HubPlugin) => string | null
|
|
22
|
-
/**
|
|
22
|
+
/** 已安装且目录有更新(有版本比版本、无版本比仓库更新时间);冷却期复核未就绪时为 false */
|
|
23
23
|
hasUpdate: (p: HubPlugin) => boolean
|
|
24
|
-
/**
|
|
24
|
+
/** 更新暂缓(新版本还没过冷却期)的可重试时刻(毫秒);无则 null */
|
|
25
25
|
updatePendingUntil: (p: HubPlugin) => number | null
|
|
26
26
|
t: Translate
|
|
27
27
|
langKey: LocaleId
|
|
@@ -35,7 +35,7 @@ export function PluginCard({ plugin: p, copied, installedName, installedVersion,
|
|
|
35
35
|
const pkg = installedName(p)
|
|
36
36
|
const isInstalled = pkg !== null
|
|
37
37
|
const update = hasUpdate(p)
|
|
38
|
-
//
|
|
38
|
+
// 冷却期未到:更新确实存在但现在装不上,给确切时刻而不是让徽标悄悄消失
|
|
39
39
|
const pendingUntil = isInstalled && !update ? updatePendingUntil(p) : null
|
|
40
40
|
// 版本号展示:已安装 → 当前已装版本(最能反映用户状态);未安装 → 目录最新版本
|
|
41
41
|
const versionShown = isInstalled ? installedVersion(p) : p.version
|
|
@@ -35,7 +35,7 @@ export function AboutModal({ info, lang, t, onClose }: {
|
|
|
35
35
|
t: Translate
|
|
36
36
|
onClose: () => void
|
|
37
37
|
}) {
|
|
38
|
-
// 内容:字符串直接使用;{zh,en}
|
|
38
|
+
// 内容:字符串直接使用;{zh,en} 对象按界面语言取,缺哪种补哪种,全缺则展示备用文案。
|
|
39
39
|
// Worker 未推送(info 为 null / content 为空)时弹窗仍能打开,显示「暂未发布」而非报错。
|
|
40
40
|
const contentRaw = info
|
|
41
41
|
? typeof info.content === 'string'
|
|
@@ -50,7 +50,7 @@ export function AboutModal({ info, lang, t, onClose }: {
|
|
|
50
50
|
// 同一次打开内保持稳定,避免父组件重渲染导致图片被反复重载。
|
|
51
51
|
const qrTs = useMemo(() => Date.now(), [])
|
|
52
52
|
const contentHtml = contentRaw.trim() ? renderMarkdown(bustQrCache(contentRaw, qrTs)) : null
|
|
53
|
-
// 更新时间:ISO
|
|
53
|
+
// 更新时间:ISO 字符串转本地可读格式;非法值直接隐藏
|
|
54
54
|
let updated: string | null = null
|
|
55
55
|
if (info?.updatedAt) {
|
|
56
56
|
const d = new Date(info.updatedAt)
|
|
@@ -23,9 +23,9 @@ export function ConfirmDialog({ type, title, desc, confirmLabel, cancelLabel, bu
|
|
|
23
23
|
desc?: string
|
|
24
24
|
confirmLabel: string
|
|
25
25
|
cancelLabel?: string
|
|
26
|
-
/**
|
|
26
|
+
/** 确认动作进行中:禁用按钮防止二次点击 */
|
|
27
27
|
busy?: boolean
|
|
28
|
-
/**
|
|
28
|
+
/** 进行中时确认按钮文案(如「清空中…」);缺省沿用 confirmLabel */
|
|
29
29
|
busyLabel?: string
|
|
30
30
|
t: Translate
|
|
31
31
|
onConfirm: () => void
|
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
* - 有更新(state='available'):标题「有新版本」,展示新版本号、发布时间与 Worker 下发的
|
|
8
8
|
* Markdown 变更记录(renderMarkdown 渲染,双语言按界面语言取 {zh,en} 对象),
|
|
9
9
|
* 按钮「稍后再说 / 直接更新」,确认后进入安装弹窗执行覆盖重装。
|
|
10
|
-
* - 暂缓提示(state='pending'
|
|
11
|
-
*
|
|
10
|
+
* - 暂缓提示(state='pending'):远端确有新版本,但发布还不满冷却期,pnpm 此时会
|
|
11
|
+
* 不提示地回退到旧版本 —— 仍按「有新版本」展示,只给「稍后再说」,并说明稍后会自动提示。
|
|
12
12
|
* - 无更新 / 信息缺失(state='latest' / 'none'):标题「当前版本」,版本号与本机版本取自
|
|
13
13
|
* currentVersion(构建注入的 PLUGIN_VERSION),**不得**用远端最新版号冒充本机版本。
|
|
14
14
|
* 点击头部版本号(或「可更新」徽标)打开 —— 无论有无更新都能看到更新内容。
|
|
@@ -59,7 +59,7 @@ export function HubUpdateModal({ info, state, currentVersion, lang, t, onProceed
|
|
|
59
59
|
const notesHtml = notesBody.trim() ? renderMarkdown(notesBody) : null
|
|
60
60
|
// 说明只在需要动作或需要解释时出现:已是最新时胶囊已说明状态,不再重复一句话
|
|
61
61
|
const desc = hasUpdate ? t('hubUpdateDesc') : pending ? t('hubPendingDesc') : null
|
|
62
|
-
// 发布时间:ISO
|
|
62
|
+
// 发布时间:ISO 字符串转本地可读格式;非法值直接隐藏
|
|
63
63
|
let published: string | null = null
|
|
64
64
|
if (info.publishedAt) {
|
|
65
65
|
const d = new Date(info.publishedAt)
|
|
@@ -70,7 +70,7 @@ export function InstalledDetailModal({ item, t, lang, langPath, onClose, onUpdat
|
|
|
70
70
|
? t('versionUpHint', { from: item.installedVersion, to: item.catalogVersion })
|
|
71
71
|
: t('updateAvailableHint'))
|
|
72
72
|
: item.pendingUntil != null
|
|
73
|
-
//
|
|
73
|
+
// 冷却期未到:给确切可重试时刻,而不是让「有更新」悄悄消失
|
|
74
74
|
? h('div', { className: styles.detailUpdateHint }, t('updatePending', { time: clockTime(item.pendingUntil) }))
|
|
75
75
|
: null,
|
|
76
76
|
// 详情信息网格:分类 / 状态 / 版本 / 收录入口 / 包名 / 统计 / 安装路径 / 安装时间 / 最后更新
|
|
@@ -70,7 +70,7 @@ export function LogsPathModal({ t, defaultPath, currentPath, onSaved, onClose }:
|
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
/**
|
|
73
|
+
/** 选择目录:系统对话框从默认位置打开,选中路径填入输入框(仍可手改)。 */
|
|
74
74
|
const browse = async () => {
|
|
75
75
|
if (browsing) return
|
|
76
76
|
setBrowsing(true)
|
|
@@ -118,7 +118,7 @@ export function NotificationsModal({ records, tasks, pendingRestarts, t, env, on
|
|
|
118
118
|
h('span', { className: styles.pendingRowStatus },
|
|
119
119
|
p.kind === 'uninstall' ? t('restartPendingHintUninstall') : t('restartPendingHint')),
|
|
120
120
|
h('span', { className: styles.pendingRowActions },
|
|
121
|
-
//
|
|
121
|
+
// 例外项:刷新解决不了(热加载失败 / 非 dsh 插件 / 更新)才登记待重启;
|
|
122
122
|
// 关闭本弹窗即视为「稍后」,只在真正需要时给单个「立即重启」
|
|
123
123
|
h('button', {
|
|
124
124
|
className: styles.restartNow,
|
|
@@ -376,7 +376,7 @@ export function NotificationsModal({ records, tasks, pendingRestarts, t, env, on
|
|
|
376
376
|
])
|
|
377
377
|
}
|
|
378
378
|
if (kind === 'hostIncompatible') {
|
|
379
|
-
//
|
|
379
|
+
// 宿主版本要求:插件声明的兼容范围不含当前 dsh 版本,安装前就被拒绝。
|
|
380
380
|
// 作者能修(把该版本加进兼容清单),故引导到作者仓库;同时说明本地可临时授权
|
|
381
381
|
const version = /incompatible with dsh\s+([^\s:;,]+)/i.exec(r.message ?? '')?.[1] ?? ''
|
|
382
382
|
return h('div', null, [
|
|
@@ -20,9 +20,9 @@ import type { FailureAction } from '../../logic/failures.ts'
|
|
|
20
20
|
|
|
21
21
|
/** 完成结果视图:绿色对勾 + 标题/描述;
|
|
22
22
|
* 终态只有两种:
|
|
23
|
-
* - needsRestart=true
|
|
23
|
+
* - needsRestart=true(刷新解决不了:热加载失败 / 非 dsh 插件 / 更新)→ 仅一个「立即重启」
|
|
24
24
|
* 按钮(重启会中断进行中的任务,故仍需用户确认),关闭即放弃;
|
|
25
|
-
* - 其余(含 needsReload=true
|
|
25
|
+
* - 其余(含 needsReload=true 的「已热加载、仅剩客户端 UI」)→ 仅「完成」按钮:关闭结果弹窗时
|
|
26
26
|
* 上层自动刷新页面让插件生效,不再给「稍后/立即刷新」手动选择。 */
|
|
27
27
|
function ResultView({
|
|
28
28
|
title, desc, t, restarting, needsRestart, needsReload, bundleDeselected, onRestart, onClose,
|
|
@@ -32,7 +32,7 @@ function ResultView({
|
|
|
32
32
|
t: Translate
|
|
33
33
|
restarting: boolean
|
|
34
34
|
needsRestart: boolean
|
|
35
|
-
/**
|
|
35
|
+
/** 仅刷新页面即生效(插件已热加载):给一句自动刷新说明,按钮仍是「完成」 */
|
|
36
36
|
needsReload?: boolean
|
|
37
37
|
/** 市场自更新成功、但 bundle 被取消勾选:重启后市场不会出现,提示重新勾选 */
|
|
38
38
|
bundleDeselected?: boolean
|
|
@@ -90,13 +90,15 @@ export interface InstallModalProps {
|
|
|
90
90
|
restarting: boolean
|
|
91
91
|
/** 已安装插件的覆盖更新(走同一条 add 命令原位重装,文案区分安装/更新) */
|
|
92
92
|
update: boolean
|
|
93
|
+
/** 已过发布冷却期的安装版本:命令条带上它,手抄执行与一键安装结果一致(未查到时不传) */
|
|
94
|
+
pinVersion?: string
|
|
93
95
|
/** 仅命令行插件(webInstallable=false):不提供一键安装,只展示命令供复制到 dsh 终端 */
|
|
94
96
|
cliOnly: boolean
|
|
95
|
-
/**
|
|
97
|
+
/** 安装请求进行中(fetch 等待响应):此时任务尚未入队,需禁用确认按钮防止二次点击 */
|
|
96
98
|
submitting: boolean
|
|
97
99
|
/** 完成结果是否需重启才生效:true → 仅给「立即重启」;false → 仅「完成」(关闭即自动刷新) */
|
|
98
100
|
needsRestart: boolean
|
|
99
|
-
/**
|
|
101
|
+
/** 完成结果是否只需刷新页面即生效(插件已热加载):true → 结果视图显示自动刷新说明 */
|
|
100
102
|
needsReload: boolean
|
|
101
103
|
/** 市场自更新成功、但 profile 的 bundle 列表已不含 dsh-plugin:结果页提示重新勾选 */
|
|
102
104
|
bundleDeselected?: boolean
|
|
@@ -112,7 +114,7 @@ export interface InstallModalProps {
|
|
|
112
114
|
* 目录插件与命令行安装(customTarget 模式)共用同一套确认/进度/结果流程。
|
|
113
115
|
*/
|
|
114
116
|
export function InstallModal(props: InstallModalProps) {
|
|
115
|
-
const { plugin, customTarget, globalNpm, done, task, t, langPath, restarting, submitting, update, cliOnly, needsRestart, needsReload, bundleDeselected, onClose, onCopy, onInstall, onRestart } = props
|
|
117
|
+
const { plugin, customTarget, globalNpm, done, task, t, langPath, restarting, submitting, update, pinVersion, cliOnly, needsRestart, needsReload, bundleDeselected, onClose, onCopy, onInstall, onRestart } = props
|
|
116
118
|
const busy = submitting || (task !== null && (task.status === 'pending' || task.status === 'running'))
|
|
117
119
|
// 进行中标题带上插件名(中文「XX 插件安装中」;英文状态词在前更自然),并用状态色区分;
|
|
118
120
|
// 命令行安装无插件元数据 → 直接用输入的目标展示;全局 npm 安装 → 包列表空格拼接
|
|
@@ -191,7 +193,7 @@ export function InstallModal(props: InstallModalProps) {
|
|
|
191
193
|
h('span', { className: styles.modalCmdText },
|
|
192
194
|
globalNpm !== undefined && globalNpm.length > 0
|
|
193
195
|
? `npm install -g ${globalNpm.join(' ')}`
|
|
194
|
-
: customTarget ? `pnpm add ${customTarget}` : installCommandOf(plugin
|
|
196
|
+
: customTarget ? `pnpm add ${customTarget}` : installCommandOf(plugin!, false, pinVersion)),
|
|
195
197
|
h('span', { className: styles.modalCmdCopy }, h(CopyIcon), t('copyCmdLabel')),
|
|
196
198
|
),
|
|
197
199
|
// 全局 npm 安装说明:装的是系统级 CLI 工具,不进插件列表、无需重启宿主
|
|
@@ -236,7 +238,7 @@ export interface UninstallModalProps {
|
|
|
236
238
|
t: Translate
|
|
237
239
|
langPath: string
|
|
238
240
|
restarting: boolean
|
|
239
|
-
/**
|
|
241
|
+
/** 卸载请求进行中(fetch 等待响应):此时任务尚未入队,需禁用确认按钮防止二次点击 */
|
|
240
242
|
submitting: boolean
|
|
241
243
|
/** 完成结果是否需重启才生效:true(停用失败等刷新解决不了的场景)→ 仅给「立即重启」;
|
|
242
244
|
* false(loader 已即时停用)→ 仅「完成」,关闭即自动刷新页面摘除插件面板 */
|
|
@@ -270,7 +272,7 @@ export function UninstallModal(props: UninstallModalProps) {
|
|
|
270
272
|
h('div', { className: styles.modalHead },
|
|
271
273
|
h('div', {
|
|
272
274
|
className: busy
|
|
273
|
-
// 注意 task 判空:submitting
|
|
275
|
+
// 注意 task 判空:submitting(请求进行中)时任务尚未入队,task 为 null,不能直接解引用
|
|
274
276
|
? `${styles.modalTitle} ${task !== null && task.status === 'pending' ? styles.modalTitleQueued : styles.modalTitleBusy}`
|
|
275
277
|
: styles.modalTitle,
|
|
276
278
|
}, title),
|
|
@@ -365,7 +367,7 @@ export function ErrorModal({ message, repo, action, command, attempts, t, env, o
|
|
|
365
367
|
// 复制完整错误:实际执行/尝试过的安装命令(如有)+ 完整错误正文。
|
|
366
368
|
// 只复制 message 会丢上下文 —— 命令行安装输入的目标、执行过的命令都在 command/attempts 里,
|
|
367
369
|
// 光拷一句「unsupported install target」出去没人看得懂
|
|
368
|
-
//
|
|
370
|
+
// 展示与复制都用展开后的文本:宿主兼容性提示里的 peerDependencies 是压缩 JSON,
|
|
369
371
|
// 原样展示会挤成一整行;格式化只影响可读性,不改变归类与上报逻辑
|
|
370
372
|
const displayMessage = formatFailureMessage(message)
|
|
371
373
|
const copyText = [
|
|
@@ -509,7 +511,7 @@ export function ErrorModal({ message, repo, action, command, attempts, t, env, o
|
|
|
509
511
|
}, t('failIssueBig')) : null,
|
|
510
512
|
])
|
|
511
513
|
: failureKind === 'hostIncompatible'
|
|
512
|
-
//
|
|
514
|
+
// 宿主版本要求:插件声明的兼容范围不含当前 dsh 版本,安装前就被宿主拒绝。
|
|
513
515
|
// 这是作者能修的(把该版本加进兼容清单),故直接引导到作者仓库;同时说明本地可临时授权
|
|
514
516
|
? h('div', null, [
|
|
515
517
|
(() => {
|
|
@@ -26,7 +26,7 @@ import { CloseIcon, HelpIcon } from '../ui/icons.tsx'
|
|
|
26
26
|
const NPM_PACKAGE_RE = /^(?:@[a-z0-9._-]+\/)?[a-z0-9._-]+$/
|
|
27
27
|
|
|
28
28
|
/** npm 全局安装命令(官方 README 格式,如 `npm install -g @deepseek-ai/dsh @deepseek-harness-tui/dsh-tui`)——
|
|
29
|
-
* 提取 -g 之后的包列表;任一包名非法返回 null(交给格式错误提示)。与 server 端 globalNpmPackagesOf
|
|
29
|
+
* 提取 -g 之后的包列表;任一包名非法返回 null(交给格式错误提示)。与 server 端 globalNpmPackagesOf 规则一致。 */
|
|
30
30
|
const NPM_GLOBAL_CMD_RE = /^npm\s+(?:install|i)\s+(?:-g|--global)\s+(.+)$/i
|
|
31
31
|
|
|
32
32
|
/** 常规 npm/pnpm 安装命令(装进当前 profile,如 `npm install lodash`、`pnpm add @scope/pkg`)——
|
|
@@ -34,7 +34,7 @@ const NPM_GLOBAL_CMD_RE = /^npm\s+(?:install|i)\s+(?:-g|--global)\s+(.+)$/i
|
|
|
34
34
|
const NPM_INSTALL_CMD_RE = /^(?:npm\s+(?:install|i)|pnpm\s+(?:add|i|install))\s+(.+)$/i
|
|
35
35
|
|
|
36
36
|
/** 空格分隔的包列表:逐个按 npm 包名语法校验 + 拒绝以 - 开头的 token(npm 会把 --xxx 当参数而非包名),
|
|
37
|
-
* 任一非法返回 null(交给格式错误提示)。与 server 端 globalNpmPackagesOf
|
|
37
|
+
* 任一非法返回 null(交给格式错误提示)。与 server 端 globalNpmPackagesOf 同一套规则。 */
|
|
38
38
|
function splitPackages(raw: string): string[] | null {
|
|
39
39
|
const packages = raw.trim().split(/\s+/).filter((p) => p !== '')
|
|
40
40
|
if (packages.length === 0 || packages.some((p) => !NPM_PACKAGE_RE.test(p) || p.startsWith('-'))) return null
|
|
@@ -122,11 +122,11 @@ function InstalledRow({ item, t, langKey, canReveal, revealLabel, pendingRestart
|
|
|
122
122
|
item.hasUpdate
|
|
123
123
|
? h('span', { className: styles.updateBadge, title: t('updateAvailableHint') }, t('updateAvailable'))
|
|
124
124
|
: item.pendingUntil != null
|
|
125
|
-
//
|
|
125
|
+
// 冷却期未到:更新存在但还装不上,给确切时刻,避免看起来「已是最新」
|
|
126
126
|
? h('span', { className: styles.updateBadge, title: t('updatePendingHint') }, t('updatePending', { time: clockTime(item.pendingUntil) }))
|
|
127
127
|
: null,
|
|
128
128
|
),
|
|
129
|
-
// 右上角 Star / Fork
|
|
129
|
+
// 右上角 Star / Fork:与插件市场列表同一规则,装之前看过的热度在已安装里也能对照。
|
|
130
130
|
// 自定义安装没有目录元数据(plugin === null),没有可展示的统计
|
|
131
131
|
item.plugin
|
|
132
132
|
? h('div', { className: styles.rowStats },
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*
|
|
9
9
|
* 各分组对应本地服务端实际强制的行为:
|
|
10
10
|
* 更新与源 —— 启动检查更新 / npm 镜像 / HTTP 代理(都接进安装命令链路)
|
|
11
|
-
* 安全与信任 —— 受限的自定义安装(服务端 403
|
|
11
|
+
* 安全与信任 —— 受限的自定义安装(服务端 403 拦截)
|
|
12
12
|
* 诊断 —— 实时连通性探测 + 环境快照
|
|
13
13
|
* 日志 —— 系统日志查看器 + 存放位置
|
|
14
14
|
*/
|
|
@@ -176,7 +176,7 @@ export function SettingsView({ t, settings, update, reset, env, onCopy, openSect
|
|
|
176
176
|
className: `${styles.controlDropdown} ${dropdownStyles.dropdownFill}`,
|
|
177
177
|
}),
|
|
178
178
|
}),
|
|
179
|
-
//
|
|
179
|
+
// 自动换源:只在公共源之间切换;自定义/内网源不换源,避免私有包名泄露
|
|
180
180
|
h(SettingRow, {
|
|
181
181
|
title: t('settingsRegistryFallback'),
|
|
182
182
|
desc: t('settingsRegistryFallbackDesc'),
|
|
@@ -214,7 +214,7 @@ export function SettingsView({ t, settings, update, reset, env, onCopy, openSect
|
|
|
214
214
|
)
|
|
215
215
|
|
|
216
216
|
const securityCard = h('div', { className: styles.card },
|
|
217
|
-
// 安全与信任:命令行安装的开关(仅 NPM 包)——
|
|
217
|
+
// 安全与信任:命令行安装的开关(仅 NPM 包)—— 服务端按通道校验 403 拦截
|
|
218
218
|
//(npm 目标吃 enableNpmInstall);目录内收录的插件安装不受影响(默认开启)
|
|
219
219
|
h(SettingRow, {
|
|
220
220
|
title: t('settingsEnableNpm'),
|
|
@@ -10,7 +10,7 @@ import type { HubPlugin, LocaleId } from '../types.ts'
|
|
|
10
10
|
import { normalize } from '../logic/normalize.ts'
|
|
11
11
|
|
|
12
12
|
// 目录/统计数据经服务端 /catalog 代理路由获取(curl 子进程注入设置里的代理),
|
|
13
|
-
// 与 npm / git
|
|
13
|
+
// 与 npm / git 安装通道同一代理规则 —— 浏览器不直连 api.dsh-plugin.org。
|
|
14
14
|
const PROXY_BASE = '/dsh-plugin-hub/catalog'
|
|
15
15
|
// refresh=1:跳过服务端 1 小时缓存直接回源(失败仍回退旧缓存),供「强制刷新」使用
|
|
16
16
|
const PLUGINS_URL = (lang: LocaleId, refresh: boolean) => `${PROXY_BASE}?lang=${lang}${refresh ? '&refresh=1' : ''}`
|
package/src/client/data/host.ts
CHANGED
|
@@ -48,9 +48,11 @@ export async function fetchInstalled(): Promise<HostInstalled | null> {
|
|
|
48
48
|
export interface NpmReleaseInfo {
|
|
49
49
|
version: string
|
|
50
50
|
publishedAt: string
|
|
51
|
+
/** 已过 pnpm 发布冷却期的最高稳定版:安装/展示命令锁定它;无可锁定版本时为 null。 */
|
|
52
|
+
installable?: NpmReleaseInfo | null
|
|
51
53
|
}
|
|
52
54
|
|
|
53
|
-
/** 批量查询已安装插件 npm
|
|
55
|
+
/** 批量查询已安装插件 npm 包的最新版本发布时间(发布冷却期用);失败返回 null(调用方按原逻辑照常提示)。 */
|
|
54
56
|
export async function fetchNpmReleaseAge(packages: string[]): Promise<Record<string, NpmReleaseInfo | null> | null> {
|
|
55
57
|
if (packages.length === 0) return {}
|
|
56
58
|
try {
|
package/src/client/data/hub.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* GitHub: https://github.com/dshplugin/dsh-plugin-hub
|
|
5
5
|
*
|
|
6
6
|
* Hub 元数据仓库:自我更新检查与「关注我们」内容背后的 Cloudflare Worker 接口。
|
|
7
|
-
* 失败降级为 null ——
|
|
7
|
+
* 失败降级为 null —— 由调用方决定失败时如何显示(不显示徽标、占位文案),绝不抛错。
|
|
8
8
|
*/
|
|
9
9
|
import type { HubAboutInfo, HubUpdateInfo, HubUpdateVersion } from '../types.ts'
|
|
10
10
|
import { HUB_ABOUT_URL, HUB_UPDATE_URL } from '../logic/constants.ts'
|
|
@@ -19,7 +19,7 @@ function busted(url: string): string {
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
/** 归一化接口下发的 recentVersions:只保留带版本号的条目,其余字段按类型补。
|
|
22
|
-
* 这条数据是「最新版还在 24.5h
|
|
22
|
+
* 这条数据是「最新版还在 24.5h 冷却期内时回退到更早的已过冷却期版本」的唯一依据;
|
|
23
23
|
* 入口处漏传会让候选只剩最新版,整体变成 pending —— 用户就永远看不到更新入口。 */
|
|
24
24
|
function recentVersionsOf(value: unknown): HubUpdateVersion[] {
|
|
25
25
|
if (!Array.isArray(value)) return []
|
|
@@ -37,7 +37,7 @@ function recentVersionsOf(value: unknown): HubUpdateVersion[] {
|
|
|
37
37
|
return list
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
/** 版本控制中心:最新版本 + Markdown 变更记录 +
|
|
40
|
+
/** 版本控制中心:最新版本 + Markdown 变更记录 + 最近发版(冷却期回退用);不可用返回 null。 */
|
|
41
41
|
export async function fetchHubUpdate(): Promise<HubUpdateInfo | null> {
|
|
42
42
|
try {
|
|
43
43
|
const res = await fetch(busted(HUB_UPDATE_URL), { cache: 'no-store' })
|