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
|
@@ -52,7 +52,7 @@ export function useCatalog(lang: LocaleId) {
|
|
|
52
52
|
* 界面停在「正在加载插件数据…」假死 —— 超时自动重拉,最多自动重试 2 次后转失败态;
|
|
53
53
|
* 用户手动刷新(reload)时清零重新计数。 */
|
|
54
54
|
const loadRetriesRef = useRef(0)
|
|
55
|
-
/** 目录加载超时阈值:超过即判定挂起,强制刷新一次(服务端代理 curl 上限 60s,客户端 30s
|
|
55
|
+
/** 目录加载超时阈值:超过即判定挂起,强制刷新一次(服务端代理 curl 上限 60s,客户端 30s 先超时) */
|
|
56
56
|
const LOAD_TIMEOUT_MS = 30000
|
|
57
57
|
/** 挂起后最多自动重试次数:避免代理持续不通时无限循环刷新(每次重试仍 30s,2 次后转失败界面可手动重试) */
|
|
58
58
|
const MAX_LOAD_RETRIES = 2
|
|
@@ -68,7 +68,7 @@ export function useCatalog(lang: LocaleId) {
|
|
|
68
68
|
* 强制刷新 Hub 版本与「关注我们」信息:点击头部版本号/徽标时触发。
|
|
69
69
|
* 挂载期的 useEffect 只在进入/reload 时拉一次,发版后已打开的界面点版本号
|
|
70
70
|
* 仍显示旧数据;此处重新拉接口中心,最新版本与变更记录点击即见。
|
|
71
|
-
*
|
|
71
|
+
* 失败时返回 null(弹窗退回展示当前版本号),与挂载期拉取规则一致。
|
|
72
72
|
*/
|
|
73
73
|
const refreshHub = async () => {
|
|
74
74
|
// 版本接口已恒带时间戳绕过 CDN,手动/自动拉取都能拿到最新公告
|
|
@@ -115,7 +115,7 @@ export function useCatalog(lang: LocaleId) {
|
|
|
115
115
|
settled = true
|
|
116
116
|
// 插件市场不显示自己:DSH Plugin Hub 从目录里排除自身条目,
|
|
117
117
|
// 防止「自己出现在自己的插件列表里、还能自己安装自己」;统计计数同步减 1 保持与列表一致。
|
|
118
|
-
//
|
|
118
|
+
// 目录数据已排除自身,此处再过滤一次,防旧快照仍含自身条目。
|
|
119
119
|
const hadSelf = list.some((p) => p.source?.repo === SELF_REPO)
|
|
120
120
|
// 过滤前单独保留 hub 自身条目:它不进目录列表,但「可更新」徽标的直接更新需要它作为重装目标
|
|
121
121
|
setHubPlugin(list.find((p) => p.source?.repo === SELF_REPO) ?? null)
|
|
@@ -131,7 +131,7 @@ export function useCatalog(lang: LocaleId) {
|
|
|
131
131
|
settled = true
|
|
132
132
|
setFailed(true)
|
|
133
133
|
})
|
|
134
|
-
//
|
|
134
|
+
// 加载超时保护:宿主刚重启/代理未就绪时目录请求可能长时间挂起,界面停在「正在加载插件数据…」
|
|
135
135
|
// 像假死。超过阈值未收尾 → 强制刷新列表(重拉一次,最多 MAX_LOAD_RETRIES 次自动重试),
|
|
136
136
|
// 重试仍失败 → 转失败界面(显示「重试」按钮,手动刷新时计数清零重新开始)。
|
|
137
137
|
const timer = window.setTimeout(() => {
|
|
@@ -152,7 +152,7 @@ export function useCatalog(lang: LocaleId) {
|
|
|
152
152
|
// 否则英文模式仍停留在 plugins.zh.json,描述/名称全是中文
|
|
153
153
|
}, [reloadKey, lang])
|
|
154
154
|
|
|
155
|
-
/** 刷新当前 profile
|
|
155
|
+
/** 刷新当前 profile 已安装插件表;宿主未挂本地路由时降级为空表。 */
|
|
156
156
|
const refreshInstalled = async () => {
|
|
157
157
|
const data = await fetchInstalled()
|
|
158
158
|
if (data === null) return
|
|
@@ -168,7 +168,7 @@ export function useCatalog(lang: LocaleId) {
|
|
|
168
168
|
|
|
169
169
|
// Hub 自身版本检查走独立 Worker(版本控制中心):发新版后写一次 Worker KV,
|
|
170
170
|
// 所有已装用户立即看到「可更新」+ Markdown 变更记录,不经过主站目录数据管道。
|
|
171
|
-
//
|
|
171
|
+
// 失败时降级为 null(徽标不出现,不影响目录本身加载)。
|
|
172
172
|
useEffect(() => {
|
|
173
173
|
let cancelled = false
|
|
174
174
|
void fetchHubUpdate().then((info) => { if (!cancelled) setHubUpdateInfo(info) })
|
|
@@ -176,7 +176,7 @@ export function useCatalog(lang: LocaleId) {
|
|
|
176
176
|
}, [reloadKey])
|
|
177
177
|
|
|
178
178
|
// 「关注我们」内容走同一套 Worker:作者写好 Markdown(平台介绍 + 反馈群二维码)推送 /admin/about,
|
|
179
|
-
// 客户端下次进入拉 /about
|
|
179
|
+
// 客户端下次进入拉 /about 即可看到最新,非写死。失败时返回 null(弹窗展示备用文案,不阻塞)。
|
|
180
180
|
useEffect(() => {
|
|
181
181
|
let cancelled = false
|
|
182
182
|
void fetchHubAbout().then((info) => { if (!cancelled) setHubAboutInfo(info) })
|
|
@@ -231,7 +231,7 @@ export function useCatalog(lang: LocaleId) {
|
|
|
231
231
|
return
|
|
232
232
|
}
|
|
233
233
|
let cancelled = false
|
|
234
|
-
// 批量一次查询;失败/返回 null
|
|
234
|
+
// 批量一次查询;失败/返回 null 时按原逻辑照常提示,网络问题不会漏掉真实更新
|
|
235
235
|
void fetchNpmReleaseAge(releaseAgeCandidates).then((result) => {
|
|
236
236
|
if (cancelled) return
|
|
237
237
|
setReleaseAge({ key: releaseAgeKey, releases: result ?? EMPTY_RELEASES })
|
|
@@ -244,7 +244,7 @@ export function useCatalog(lang: LocaleId) {
|
|
|
244
244
|
|
|
245
245
|
/**
|
|
246
246
|
* 不能提示更新的包:
|
|
247
|
-
* - npm
|
|
247
|
+
* - npm 上最新版还没过发布冷却期(发布不满 24h,pnpm 会不提示地回退,点了也装不上);
|
|
248
248
|
* - npm 上最新版并不高于已装版本 —— 更新走的是 `pkg@latest`,此时无论目录标到哪个 tag,
|
|
249
249
|
* 都不可能装上(作者可能只打了 GitHub release 没发 npm),提示只会变成永远点不掉的循环。
|
|
250
250
|
*/
|
|
@@ -280,7 +280,7 @@ export function useCatalog(lang: LocaleId) {
|
|
|
280
280
|
return releaseAgeReady && !updateBlocked.has(pkg)
|
|
281
281
|
}
|
|
282
282
|
|
|
283
|
-
/**
|
|
283
|
+
/** 更新被冷却期未到时的可重试时刻(毫秒);未安装/无更新/非冷却期原因 → null。 */
|
|
284
284
|
const updatePendingUntil = (p: HubPlugin): number | null => {
|
|
285
285
|
if (!hasUpdateOf(p, installed, versions)) return null
|
|
286
286
|
const pkg = npmPackageOf(p, versions)
|
|
@@ -289,7 +289,7 @@ export function useCatalog(lang: LocaleId) {
|
|
|
289
289
|
}
|
|
290
290
|
|
|
291
291
|
/** 已安装项统一列表(目录元数据 + 运行时信息合并):校验未就绪/被拦下的去掉更新标记、
|
|
292
|
-
*
|
|
292
|
+
* 冷却期未到的带上可重试时刻,驱动「已安装」tab。 */
|
|
293
293
|
const installedItems = useMemo<InstalledItem[]>(() => {
|
|
294
294
|
return rawInstalledItems.map((item) => {
|
|
295
295
|
if (!item.hasUpdate || !item.plugin) return item
|
|
@@ -309,13 +309,13 @@ export function useCatalog(lang: LocaleId) {
|
|
|
309
309
|
* 构建时注入的 PLUGIN_VERSION 是运行 bundle 的真实版本;测试/异常场景缺失时
|
|
310
310
|
* 回退到安装时记录的版本。版本号不等即新版(版本号只在发版时变更,绝无降级场景)。
|
|
311
311
|
*
|
|
312
|
-
* 另加 pnpm
|
|
312
|
+
* 另加 pnpm 发布冷却期:新版本发布还不满 24 小时(+30 分钟缓冲)时,pnpm 会不提示地
|
|
313
313
|
* 回退到旧版本,此时若提示「可更新」用户点了也更新不到。publishedAt 由接口中心按 npm
|
|
314
|
-
* registry
|
|
314
|
+
* registry 真实发布时间下发(同源),据此判断最准。冷却期命中要单列成 `pending` 而不是
|
|
315
315
|
* 落回「无更新」——回落会让弹窗在本机版本落后时谎称「已是最新」。
|
|
316
316
|
*/
|
|
317
317
|
const hubCurrentVersion = PLUGIN_VERSION || versions[HUB_REPO]?.version || ''
|
|
318
|
-
// 候选含 recentVersions
|
|
318
|
+
// 候选含 recentVersions:最新版还在冷却期内时回退到「更早、但已过冷却期」的版本,
|
|
319
319
|
// 避免连续快速发版把所有提示窗口都吞掉(见 pickHubUpdate)。
|
|
320
320
|
const hubUpdateResult = hubUpdateInfo === null
|
|
321
321
|
? { state: 'none' as HubUpdateState, target: null }
|
|
@@ -60,7 +60,7 @@ export function useSettings() {
|
|
|
60
60
|
return () => { alive = false }
|
|
61
61
|
}, [])
|
|
62
62
|
|
|
63
|
-
/** 部分更新:本地立即生效(乐观),同时 POST
|
|
63
|
+
/** 部分更新:本地立即生效(乐观),同时 POST 持久化;持久化失败不影响本次会话。 */
|
|
64
64
|
const update = useCallback((patch: Partial<HubSettings>) => {
|
|
65
65
|
setSettings((prev) => ({ ...prev, ...patch }))
|
|
66
66
|
void fetch('/dsh-plugin-hub/settings', {
|
|
@@ -71,7 +71,7 @@ export function useSettings() {
|
|
|
71
71
|
}).catch(() => { /* 宿主未挂路由:仅本次会话生效 */ })
|
|
72
72
|
}, [])
|
|
73
73
|
|
|
74
|
-
/**
|
|
74
|
+
/** 重置为默认设置:本地立即回默认,同时通知服务端删除设置文件;失败不影响本地已重置的设置。 */
|
|
75
75
|
const reset = useCallback(() => {
|
|
76
76
|
setSettings({ ...DEFAULT_SETTINGS })
|
|
77
77
|
void fetch('/dsh-plugin-hub/settings/reset', {
|
|
@@ -49,11 +49,11 @@ export interface QueueTask {
|
|
|
49
49
|
command?: string
|
|
50
50
|
/** 实际执行/重试过的安装命令(按先后顺序):失败时 issue 预填一并贴给作者 */
|
|
51
51
|
attempts?: string[]
|
|
52
|
-
/** 完成结果是否需要重启才生效(服务端任务终态带出)。默认 true
|
|
53
|
-
*
|
|
54
|
-
*
|
|
52
|
+
/** 完成结果是否需要重启才生效(服务端任务终态带出)。默认 true:数据缺失时不视为已生效。
|
|
53
|
+
* 仅刷新解决不了的例外场景(热加载失败 / 非 dsh 插件 / 更新)为 true,结果视图给「立即重启」;
|
|
54
|
+
* 其余(热加载成功、卸载已即时停用、全局安装)为 false,结果视图仅「完成」。 */
|
|
55
55
|
needsRestart: boolean
|
|
56
|
-
/**
|
|
56
|
+
/** 完成结果是否只需刷新页面即生效(插件已热加载进运行中 loader,或卸载后插件面板仍残留在当前页面):
|
|
57
57
|
* true 时关闭结果弹窗 / Toast 消失后由上层自动刷新页面,不给手动刷新按钮。 */
|
|
58
58
|
needsReload?: boolean
|
|
59
59
|
/** 市场自更新成功、但 profile 的 bundle 列表里已不含 dsh-plugin:重启后市场不会出现 */
|
|
@@ -78,11 +78,11 @@ export interface TaskQueueOptions {
|
|
|
78
78
|
refreshInstalled: () => void
|
|
79
79
|
/** 任务成功完成:viaModal 表示任务对应当前打开弹窗(弹窗切结果视图),否则走 Toast;repo 供成功通知记录;
|
|
80
80
|
* needsRestart 表示该任务完成后是否仍需宿主重启(结果视图据此给「重启 / 仅完成」);
|
|
81
|
-
* needsReload
|
|
81
|
+
* needsReload 表示是否只需刷新页面即生效(热加载成功、或卸载后插件面板仍残留)。
|
|
82
82
|
* update 表示实际执行的是更新(覆盖重装)而非首次安装:通知中心据此显示「更新成功」而非「安装成功」。 */
|
|
83
83
|
onInstallDone: (viaModal: boolean, repo: string | null, needsRestart: boolean, update?: boolean, needsReload?: boolean, bundleDeselected?: boolean) => void
|
|
84
84
|
onUninstallDone: (viaModal: boolean, repo: string | null, needsRestart: boolean, needsReload?: boolean) => void
|
|
85
|
-
/**
|
|
85
|
+
/** 任务失败:完整输出或备用文案 + 所属插件仓库 + 操作类型(安装/卸载)+ 实际执行的安装命令(issue 预填用,可缺省)+ 实际执行/重试过的安装命令(可缺省)+ 是否更新。 */
|
|
86
86
|
onError: (message: string, repo: string | null, kind: 'install' | 'uninstall', command?: string, attempts?: string[], update?: boolean) => void
|
|
87
87
|
/** 当前打开的安装/卸载弹窗插件:用于在弹窗内匹配进行中的任务。 */
|
|
88
88
|
installPlugin: HubPlugin | null
|
|
@@ -114,7 +114,7 @@ export function useTaskQueue(opts: TaskQueueOptions) {
|
|
|
114
114
|
/** 待重启列表(服务端内存态 + 本地展示信息补齐);与 queue 一样以 ref 为唯一事实来源。 */
|
|
115
115
|
const [pendingRestarts, setPendingRestarts] = useState<PendingRestart[]>([])
|
|
116
116
|
const pendingRef = useRef<PendingRestart[]>([])
|
|
117
|
-
/** 本次会话内安装成功插件的展示信息(目标 → 简介/版本):轮询合并待重启列表时补齐,刷新后由 resolvePending
|
|
117
|
+
/** 本次会话内安装成功插件的展示信息(目标 → 简介/版本):轮询合并待重启列表时补齐,刷新后由 resolvePending 补上。 */
|
|
118
118
|
const pendingInfoRef = useRef<Map<string, { desc?: string; version?: string }>>(new Map())
|
|
119
119
|
/** 保持最新的 resolvePending:轮询闭包捕获的是调用那一刻的目录状态,目录晚加载时也能补齐。 */
|
|
120
120
|
const resolvePendingRef = useRef(resolvePending)
|
|
@@ -125,12 +125,12 @@ export function useTaskQueue(opts: TaskQueueOptions) {
|
|
|
125
125
|
const settlingRef = useRef<Set<number>>(new Set())
|
|
126
126
|
/** 轮询定时器句柄(队列清空/组件卸载时清理) */
|
|
127
127
|
const pollRef = useRef<number | null>(null)
|
|
128
|
-
/**
|
|
128
|
+
/** 请求还没返回的目标集合(同步防重):双击安装/卸载时第二击直接忽略。
|
|
129
129
|
* 本地队列任务要等 fetch 返回后才入队,仅靠 queueRef 检查拦不住请求窗口内的重复点击。 */
|
|
130
130
|
const submittingRef = useRef<Set<string>>(new Set())
|
|
131
131
|
/** 乐观入队临时 id 计数器:递减产生唯一负数,与服务端自增正数 id 永不冲突。 */
|
|
132
132
|
const tempIdRef = useRef(0)
|
|
133
|
-
/**
|
|
133
|
+
/** 请求进行中标记:弹窗据此禁用确认按钮,避免等待响应期间被再次点击。 */
|
|
134
134
|
const [submitting, setSubmitting] = useState(false)
|
|
135
135
|
|
|
136
136
|
/** 停止后台任务轮询(任务结束或组件卸载时清理)。 */
|
|
@@ -146,7 +146,7 @@ export function useTaskQueue(opts: TaskQueueOptions) {
|
|
|
146
146
|
if (queueRef.current.length === 0 && pendingRef.current.length === 0) stopPoll()
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
-
//
|
|
149
|
+
// 组件卸载时清理轮询定时器
|
|
150
150
|
useEffect(() => stopPoll, [])
|
|
151
151
|
|
|
152
152
|
/** 更新队列(同步镜像到 ref:queueRef 是唯一事实来源,state 只负责触发渲染,
|
|
@@ -200,7 +200,7 @@ export function useTaskQueue(opts: TaskQueueOptions) {
|
|
|
200
200
|
body: JSON.stringify({ repo, version, updatedAt }),
|
|
201
201
|
})
|
|
202
202
|
} catch {
|
|
203
|
-
//
|
|
203
|
+
// 宿主未挂本地路由:直接忽略,仅本机展示受影响
|
|
204
204
|
}
|
|
205
205
|
}
|
|
206
206
|
|
|
@@ -274,7 +274,7 @@ export function useTaskQueue(opts: TaskQueueOptions) {
|
|
|
274
274
|
}, stepMs)
|
|
275
275
|
}
|
|
276
276
|
|
|
277
|
-
/** 任务在 /active 中消失 = 已结束:查 /status 拿终态并收尾(cancelled
|
|
277
|
+
/** 任务在 /active 中消失 = 已结束:查 /status 拿终态并收尾(cancelled 直接移除)。 */
|
|
278
278
|
const settleTask = async (q: QueueTask) => {
|
|
279
279
|
let status = 'failed'
|
|
280
280
|
let lines: string[] = []
|
|
@@ -292,7 +292,7 @@ export function useTaskQueue(opts: TaskQueueOptions) {
|
|
|
292
292
|
attempts = Array.isArray(task.attempts) ? task.attempts : undefined
|
|
293
293
|
bundleDeselected = task.bundleDeselected === true ? true : undefined
|
|
294
294
|
installedVersion = typeof task.installedVersion === 'string' && task.installedVersion !== '' ? task.installedVersion : undefined
|
|
295
|
-
// 终态带出的「是否需要重启 / 是否只需刷新」覆盖本地(服务端 done 时按 loader
|
|
295
|
+
// 终态带出的「是否需要重启 / 是否只需刷新」覆盖本地(服务端 done 时按 loader 热加载/即时移除情况设置)
|
|
296
296
|
const needsRestart = typeof task.needsRestart === 'boolean' ? task.needsRestart : q.needsRestart
|
|
297
297
|
const needsReload = typeof task.needsReload === 'boolean' ? task.needsReload : (q.needsReload ?? false)
|
|
298
298
|
if (status === 'done') {
|
|
@@ -383,7 +383,7 @@ export function useTaskQueue(opts: TaskQueueOptions) {
|
|
|
383
383
|
try { await settleTask(q) } finally { settlingRef.current.delete(q.id) }
|
|
384
384
|
}
|
|
385
385
|
} catch {
|
|
386
|
-
//
|
|
386
|
+
// 服务端暂不可达:等下一轮再试
|
|
387
387
|
}
|
|
388
388
|
}, 600)
|
|
389
389
|
}
|
|
@@ -455,8 +455,8 @@ export function useTaskQueue(opts: TaskQueueOptions) {
|
|
|
455
455
|
source: 'catalog' | 'custom'
|
|
456
456
|
/** 更新已安装目标(放行 add 覆盖重装) */
|
|
457
457
|
update?: boolean
|
|
458
|
-
/**
|
|
459
|
-
*
|
|
458
|
+
/** 锁定精确版本(Hub 自我更新用):最新版可能仍在 pnpm 发布冷却期内,
|
|
459
|
+
* 必须安装「已过冷却期」的那个版本,不能再用 @latest。 */
|
|
460
460
|
pinVersion?: string
|
|
461
461
|
/** 全局 npm 安装包列表(`npm install -g <pkgs>`):非空时服务端执行全局安装,不进任何 profile */
|
|
462
462
|
globalNpm?: string[]
|
|
@@ -466,7 +466,7 @@ export function useTaskQueue(opts: TaskQueueOptions) {
|
|
|
466
466
|
}) => {
|
|
467
467
|
// 防重复入队:同一目标已在排队/执行中则忽略
|
|
468
468
|
if (queueRef.current.some((q) => q.kind === 'install' && q.target === input.repo)) return
|
|
469
|
-
//
|
|
469
|
+
// 请求进行中防重:任务要等响应回来才进本地队列,这之前再次点击(双击)直接忽略
|
|
470
470
|
if (submittingRef.current.has(input.repo)) return
|
|
471
471
|
submittingRef.current.add(input.repo)
|
|
472
472
|
// 乐观入队:点击安装立即显示「正在安装中」,不等 POST 响应。
|
|
@@ -498,7 +498,7 @@ export function useTaskQueue(opts: TaskQueueOptions) {
|
|
|
498
498
|
display: input.repo,
|
|
499
499
|
source: input.source,
|
|
500
500
|
mode: input.update ? 'update' : undefined,
|
|
501
|
-
//
|
|
501
|
+
// 锁定版本(Hub 自我更新):服务端据此执行 `add <pkg>@<version>` 而不是 @latest
|
|
502
502
|
...(input.pinVersion ? { version: input.pinVersion } : {}),
|
|
503
503
|
// 界面语言带给服务端:安装前网络探测等错误消息按用户当前语言返回(中文界面给中文,英文界面给英文)
|
|
504
504
|
...(langKey ? { lang: langKey } : {}),
|
|
@@ -531,7 +531,7 @@ export function useTaskQueue(opts: TaskQueueOptions) {
|
|
|
531
531
|
// 摘要条常驻顶部(含实时进度),明细面板保持折叠,想看时再点开
|
|
532
532
|
pollQueue()
|
|
533
533
|
} catch (err) {
|
|
534
|
-
//
|
|
534
|
+
// 网络异常:移除乐观条目,并以错误弹窗提示(附上底层原因,避免只剩一句笼统的「安装失败」)
|
|
535
535
|
applyQueue((prev) => prev.filter((x) => x.id !== tempId))
|
|
536
536
|
const aborted = (err as { name?: string } | null)?.name === 'AbortError'
|
|
537
537
|
// [request-timeout]:给分类器一个与界面语言无关的超时标记
|
|
@@ -562,7 +562,7 @@ export function useTaskQueue(opts: TaskQueueOptions) {
|
|
|
562
562
|
desc: p.description,
|
|
563
563
|
version: p.version,
|
|
564
564
|
updatedAt: p.dates?.repoUpdatedAt,
|
|
565
|
-
command: installCommandOf(p, true),
|
|
565
|
+
command: installCommandOf(p, true, opts?.pinVersion),
|
|
566
566
|
source: 'catalog',
|
|
567
567
|
update: opts?.update,
|
|
568
568
|
pinVersion: opts?.pinVersion,
|
|
@@ -572,8 +572,8 @@ export function useTaskQueue(opts: TaskQueueOptions) {
|
|
|
572
572
|
/** 命令行安装:用户手输 npm 包名,走 custom 源(受安全开关控制)。
|
|
573
573
|
* update=true 时对已安装目标放行覆盖重装(与目录插件「更新」同一语义)。
|
|
574
574
|
* channel=入口渠道(NPM 包卡片):上报服务端落日志,失败弹窗/通知据此精准溯源。
|
|
575
|
-
* 队列展示/防重一律用归一化身份:与服务端 /active
|
|
576
|
-
* npm
|
|
575
|
+
* 队列展示/防重一律用归一化身份:与服务端 /active 合并、弹窗目标匹配同一套规则,
|
|
576
|
+
* npm 包名原样传递不受影响。 */
|
|
577
577
|
const installCustom = async (raw: string, opts?: { update?: boolean; channel?: InstallChannel }) => {
|
|
578
578
|
const target = raw.trim()
|
|
579
579
|
if (!target) return
|
|
@@ -589,7 +589,7 @@ export function useTaskQueue(opts: TaskQueueOptions) {
|
|
|
589
589
|
}
|
|
590
590
|
|
|
591
591
|
/** 全局 npm 安装(官方 README 的 `npm install -g <pkgs>`):不进任何 profile、无需宿主重启。
|
|
592
|
-
* 队列展示/防重用包列表空格拼接的身份(与服务端 target
|
|
592
|
+
* 队列展示/防重用包列表空格拼接的身份(与服务端 target 同一套规则);入口渠道固定为 npm(NPM 包卡片)。 */
|
|
593
593
|
const installGlobalNpm = async (pkgs: string[], opts?: { update?: boolean; channel?: InstallChannel }) => {
|
|
594
594
|
const list = (pkgs ?? []).map((p) => p.trim()).filter((p) => p !== '')
|
|
595
595
|
if (list.length === 0) return
|
|
@@ -609,7 +609,7 @@ export function useTaskQueue(opts: TaskQueueOptions) {
|
|
|
609
609
|
const enqueueUninstall = async (name: string, repo: string | null, desc?: string) => {
|
|
610
610
|
if (!name) return
|
|
611
611
|
if (queueRef.current.some((q) => q.kind === 'uninstall' && q.target === name)) return
|
|
612
|
-
//
|
|
612
|
+
// 请求进行中防重:与安装一致,响应回来前再次点击直接忽略
|
|
613
613
|
if (submittingRef.current.has(name)) return
|
|
614
614
|
submittingRef.current.add(name)
|
|
615
615
|
setSubmitting(true)
|
|
@@ -624,6 +624,13 @@ export function useTaskQueue(opts: TaskQueueOptions) {
|
|
|
624
624
|
})
|
|
625
625
|
const data = await res.json() as { ok?: boolean; task?: number; error?: string }
|
|
626
626
|
if (!data.ok || typeof data.task !== 'number') {
|
|
627
|
+
// 依赖已经不在了(列表还没刷新时重复点击):按已卸载收尾,刷新列表,不弹失败
|
|
628
|
+
if (res.status === 400 && data.error === 'plugin is not installed') {
|
|
629
|
+
refreshInstalled()
|
|
630
|
+
void clearInstalledVersion(repo)
|
|
631
|
+
onUninstallDone(false, repo, false, false)
|
|
632
|
+
return
|
|
633
|
+
}
|
|
627
634
|
onError(data.error || `HTTP ${res.status}`, repo, 'uninstall')
|
|
628
635
|
return
|
|
629
636
|
}
|
package/src/client/locales.ts
CHANGED
|
@@ -59,7 +59,7 @@ export const zh = {
|
|
|
59
59
|
npmInstallPlaceholder: 'npm install <包名> 或直接输包名',
|
|
60
60
|
npmInstallExample: '支持原生命令:npm install lodash、npm i lodash、pnpm add @scope/pkg',
|
|
61
61
|
npmInstallExample2: '全局安装 CLI:npm install -g @scope/cli;或直接输入包名 lodash',
|
|
62
|
-
npmInstallInvalid: '请输入 npm 包名(如 lodash、@scope/pkg),或原生安装命令(如 npm install lodash、npm i lodash、pnpm add @scope/pkg、npm install -g @scope/cli
|
|
62
|
+
npmInstallInvalid: '请输入 npm 包名(如 lodash、@scope/pkg),或原生安装命令(如 npm install lodash、npm i lodash、pnpm add @scope/pkg、npm install -g @scope/cli);不支持 GitHub 源码地址,请改用该插件的 npm 包名',
|
|
63
63
|
installHelp: '帮助',
|
|
64
64
|
helpModalTitle: '支持的命令格式',
|
|
65
65
|
npmInstallHelp: 'lodash、@scope/pkg\t直接输入包名\nnpm install lodash\nnpm i lodash\npnpm add @scope/pkg\npnpm i @scope/pkg\nnpm install -g @scope/cli\t全局安装 CLI 工具,不进「已安装」列表',
|
|
@@ -257,14 +257,14 @@ export const zh = {
|
|
|
257
257
|
requestTimeout: '请求超时:无法访问 GitHub 或网络不稳定,请稍后重试',
|
|
258
258
|
confirmCancel: '取消',
|
|
259
259
|
doneBtn: '完成',
|
|
260
|
-
//
|
|
260
|
+
// 结果视图补充:仅当「刷新解决不了」时(热加载失败 / 非 dsh 插件 / 更新)才出现单个「立即重启」;
|
|
261
261
|
// 关闭弹窗即视为「稍后」,不再单独给「稍后重启」按钮
|
|
262
262
|
restartNow: '立即重启',
|
|
263
263
|
restarting: '正在重启…',
|
|
264
264
|
restartHint: '重启会中断正在进行的安装/卸载任务,其进度将丢失。部分插件(如插件市场、顶层 bundle)需重启后才会生效。',
|
|
265
265
|
// 桌面端宿主归应用壳管、插件重启不了它:点「立即重启」时改提示用户退出重开
|
|
266
266
|
toastRestartDesktop: '桌面端的宿主由应用管理,请在应用菜单(或托盘)选择退出后重新打开,重启后生效',
|
|
267
|
-
//
|
|
267
|
+
// 装完即生效:插件已热加载进运行中 loader,结果弹窗关闭时自动刷新页面(无需用户点按钮)
|
|
268
268
|
reloadHint: '插件已加载,关闭本窗口后会自动刷新页面生效。',
|
|
269
269
|
toastInstalledLive: '安装成功,已立即生效',
|
|
270
270
|
toastInstalledReload: '安装成功,正在自动刷新页面…',
|
|
@@ -311,7 +311,7 @@ export const zh = {
|
|
|
311
311
|
// 非插件侧失败(未知原因)改为引导到插件中心仓库排查
|
|
312
312
|
failHubIssueHint: '此失败已被判定为环境或插件中心自身问题(非插件作者问题),带着错误日志到插件中心仓库一键提交 Issue',
|
|
313
313
|
failHubIssueBig: '反馈给插件中心排查',
|
|
314
|
-
//
|
|
314
|
+
// 未知失败补充:不归到插件作者,引导到插件中心仓库排查
|
|
315
315
|
failUnclassifiedHint: '{action}失败,原因未能自动识别。这通常不是插件自身的问题(可能与本机环境或插件中心有关),请将错误信息反馈给插件中心,由维护者排查。',
|
|
316
316
|
// 插件中心自身状态类消息(插件未安装 / 不在此处卸载 / 已在队列中):已安装列表与实读快照不同步,非插件问题,不引导提 Issue
|
|
317
317
|
failHubStateHint: '插件列表状态已过期或与本地不一致:这次{action}的目标可能已经在列表里了(要升级请点「更新」),也可能已经不在已安装列表中,或已在队列中。这不是插件本身的问题 —— 请刷新页面后重试。',
|
|
@@ -404,7 +404,7 @@ export const zh = {
|
|
|
404
404
|
failKind_pluginMissingDep: '插件依赖未发布',
|
|
405
405
|
failReason_pluginMissingDep: '插件声明依赖的包在官方 npm 源上不存在(已删除或从未发布)。',
|
|
406
406
|
failKind_registryVersionStale: '该版本在所用源上不存在',
|
|
407
|
-
failReason_registryVersionStale: '
|
|
407
|
+
failReason_registryVersionStale: '实际安装用的源还没同步到这个版本。可稍后重试,或到「设置 → npm 镜像源」换成官方源;也可先安装上一个可用版本。',
|
|
408
408
|
failKind_registryAuth: 'npm 源拒绝了访问',
|
|
409
409
|
failReason_registryAuth: 'registry 返回 401/403:这个源需要登录凭据,或当前网络不被允许。',
|
|
410
410
|
failKind_profileExclusive: 'profile 由桌面应用独占管理',
|
|
@@ -491,10 +491,10 @@ export const zh = {
|
|
|
491
491
|
update: '可更新',
|
|
492
492
|
updateAvailable: '有更新',
|
|
493
493
|
updateAvailableHint: '检测到更新:有版本号的仓库比对版本号,无版本号的仓库比对仓库最近更新时间',
|
|
494
|
-
//
|
|
495
|
-
//
|
|
494
|
+
// 冷却期未到:新版本发布未满 24 小时,此时安装会被 pnpm 不提示地回退(或直接拒绝),
|
|
495
|
+
// 给出确切可重试时刻,而不是让徽标悄悄消失、让用户以为已经是最新
|
|
496
496
|
updatePending: '暂缓到 {time} 后可更新',
|
|
497
|
-
updatePendingHint: '
|
|
497
|
+
updatePendingHint: '新版本发布还不满 24 小时,pnpm 暂时不让安装;到时间后再更新即可',
|
|
498
498
|
hubUpdateHint: '检测到 DSH Plugin Hub 新版本,点击查看更新内容并更新',
|
|
499
499
|
hubUpdateTitle: 'DSH Plugin Hub 有新版本',
|
|
500
500
|
hubUpdateDesc: '点击下方按钮即可更新,完成后需重启宿主生效。',
|
|
@@ -508,12 +508,12 @@ export const zh = {
|
|
|
508
508
|
hubVersionStateAvailable: '可更新',
|
|
509
509
|
hubVersionStatePending: '暂缓提示',
|
|
510
510
|
close: '关闭',
|
|
511
|
-
// 暂缓提示:远端确有新版本,但发布未满 24 小时(pnpm
|
|
512
|
-
hubPendingDesc: '最新版已发布,但发布尚未满 24 小时:此时更新会被 pnpm
|
|
513
|
-
//
|
|
511
|
+
// 暂缓提示:远端确有新版本,但发布未满 24 小时(pnpm 发布冷却期),与「已是最新」严格区分
|
|
512
|
+
hubPendingDesc: '最新版已发布,但发布尚未满 24 小时:此时更新会被 pnpm 不提示地回退到旧版本,因此暂不提示。稍后这里会自动出现更新入口。',
|
|
513
|
+
// 头部「关注我们」弹窗:平台介绍(内容由 Worker /about Markdown 推送,非写死)
|
|
514
514
|
followUs: '关注我们',
|
|
515
515
|
aboutTitle: '关注我们',
|
|
516
|
-
aboutDesc: '了解 DSH Plugin Hub
|
|
516
|
+
aboutDesc: '了解 DSH Plugin Hub 是做什么的,访问主站浏览插件,或到 GitHub 提交 Issue。',
|
|
517
517
|
aboutEmpty: '内容暂未发布,敬请期待。',
|
|
518
518
|
aboutUpdated: '更新于',
|
|
519
519
|
empty: '该分类暂无插件',
|
|
@@ -573,7 +573,7 @@ export const en = {
|
|
|
573
573
|
npmInstallPlaceholder: 'npm install <pkg> or a bare package name',
|
|
574
574
|
npmInstallExample: 'Native commands: npm install lodash, npm i lodash, pnpm add @scope/pkg',
|
|
575
575
|
npmInstallExample2: 'Global CLI: npm install -g @scope/cli; or just type a package name like lodash',
|
|
576
|
-
npmInstallInvalid: 'Enter an npm package name (e.g. lodash, @scope/pkg), or a native install command (e.g. npm install lodash, npm i lodash, pnpm add @scope/pkg, npm install -g @scope/cli)',
|
|
576
|
+
npmInstallInvalid: 'Enter an npm package name (e.g. lodash, @scope/pkg), or a native install command (e.g. npm install lodash, npm i lodash, pnpm add @scope/pkg, npm install -g @scope/cli); GitHub source URLs are not supported — use the plugin\'s npm package name instead',
|
|
577
577
|
installHelp: 'Help',
|
|
578
578
|
helpModalTitle: 'Supported command formats',
|
|
579
579
|
npmInstallHelp: 'lodash or @scope/pkg\tjust type the package name\nnpm install lodash\nnpm i lodash\npnpm add @scope/pkg\npnpm i @scope/pkg\nnpm install -g @scope/cli\tinstalls the CLI globally, not shown in Installed',
|
|
@@ -700,7 +700,7 @@ export const en = {
|
|
|
700
700
|
settingsDiagFail: 'Unreachable',
|
|
701
701
|
// 系统诊断通道列表:一键全检 + 每通道一行,点击重测
|
|
702
702
|
diagNpm: 'npm registry',
|
|
703
|
-
// 诊断 npm
|
|
703
|
+
// 诊断 npm 行:未配置镜像时显示的备用文案(探测仍打官方源)
|
|
704
704
|
diagNpmUnset: 'Unset (follow local npm config)',
|
|
705
705
|
diagGithub: 'GitHub',
|
|
706
706
|
diagCatalog: 'Catalog site',
|
|
@@ -771,14 +771,14 @@ export const en = {
|
|
|
771
771
|
requestTimeout: 'Request timed out: GitHub may be unreachable or the network is unstable — try again later',
|
|
772
772
|
confirmCancel: 'Cancel',
|
|
773
773
|
doneBtn: 'Done',
|
|
774
|
-
//
|
|
774
|
+
// 结果视图补充:仅刷新解决不了时(热加载失败 / 非 dsh 插件 / 更新)才给单个「立即重启」;
|
|
775
775
|
// 关闭弹窗即视为「稍后」
|
|
776
776
|
restartNow: 'Restart now',
|
|
777
777
|
restarting: 'Restarting…',
|
|
778
778
|
restartHint: 'Restarting interrupts any in-progress install / uninstall task — its progress will be lost. Some plugins (such as plugin markets / top-level bundles) only take effect after a restart.',
|
|
779
779
|
// 桌面端:宿主由应用壳托管,插件重启不了它 —— 提示用户退出后重开
|
|
780
780
|
toastRestartDesktop: 'In the desktop app the host is managed by the app: quit from the app menu (or tray) and open it again — the change takes effect after that restart',
|
|
781
|
-
//
|
|
781
|
+
// 免重启即生效:安装已热加载进运行中 loader;关闭弹窗时页面自动刷新(不给手动刷新按钮)
|
|
782
782
|
reloadHint: 'The plugin is loaded — closing this window will refresh the page automatically.',
|
|
783
783
|
toastInstalledLive: 'Installed — active now',
|
|
784
784
|
toastInstalledReload: 'Installed — refreshing the page automatically…',
|
|
@@ -823,7 +823,7 @@ export const en = {
|
|
|
823
823
|
// 非插件侧失败(未分类)改为投递到插件中心仓库分诊
|
|
824
824
|
failHubIssueHint: 'This failure was classified as an environment or Plugin Hub issue (not a plugin-author issue). Open a pre-filled issue on the Plugin Hub repo with this error log.',
|
|
825
825
|
failHubIssueBig: 'Report to the Plugin Hub for triage',
|
|
826
|
-
//
|
|
826
|
+
// 未知失败补充:不投插件作者,改投插件中心仓库分诊
|
|
827
827
|
failUnclassifiedHint: 'The {action} failed and the cause could not be identified automatically. This is usually not the plugin itself (it may relate to your local environment or the Plugin Hub). Please report the error to the Plugin Hub so the maintainers can triage it.',
|
|
828
828
|
// 插件中心自身的状态守卫消息(未安装 / 不在此处卸载 / 已在队列中 / 已安装):已装列表与实读快照不同步 —— 不是插件问题,不引导提 Issue
|
|
829
829
|
failHubStateHint: 'The plugin list is out of date or out of sync: the target of this {action} may already be in the list (use "Update" to upgrade), may no longer be in the installed list, or may already be queued. This is not a plugin problem — refresh the page and retry.',
|
|
@@ -915,7 +915,7 @@ export const en = {
|
|
|
915
915
|
failKind_pluginMissingDep: 'plugin dependency is not published',
|
|
916
916
|
failReason_pluginMissingDep: 'A package the plugin depends on does not exist on the official npm registry.',
|
|
917
917
|
failKind_registryVersionStale: 'this version is not available on the registry in use',
|
|
918
|
-
failReason_registryVersionStale: 'The registry that actually performs the install has not synced this version yet,
|
|
918
|
+
failReason_registryVersionStale: 'The registry that actually performs the install has not synced this version yet. Retry later, switch to the official registry under Settings → npm mirror, or install a previous version for now.',
|
|
919
919
|
failKind_registryAuth: 'the npm registry rejected access',
|
|
920
920
|
failReason_registryAuth: 'The registry returned 401/403: it needs credentials, or the current network is not allowed.',
|
|
921
921
|
failKind_profileExclusive: 'the profile is managed only by the desktop app',
|
|
@@ -1017,12 +1017,12 @@ export const en = {
|
|
|
1017
1017
|
hubVersionStateAvailable: 'Update available',
|
|
1018
1018
|
hubVersionStatePending: 'Held for 24h',
|
|
1019
1019
|
close: 'Close',
|
|
1020
|
-
// 暂缓提示:确有新版本,但仍在 pnpm 24h
|
|
1020
|
+
// 暂缓提示:确有新版本,但仍在 pnpm 24h 发布冷却期内
|
|
1021
1021
|
hubPendingDesc: 'The latest version has been released, but it is still within the 24-hour window where pnpm silently falls back to the older version, so the update is not offered yet. The update entry will appear here shortly.',
|
|
1022
|
-
//
|
|
1022
|
+
// 头部「关注我们」弹窗:平台介绍(内容由 Worker /about 以 Markdown 推送)
|
|
1023
1023
|
followUs: 'Follow us',
|
|
1024
1024
|
aboutTitle: 'Follow us',
|
|
1025
|
-
aboutDesc: 'Learn what DSH Plugin Hub does
|
|
1025
|
+
aboutDesc: 'Learn what DSH Plugin Hub does — browse plugins on the website, or open an Issue on GitHub.',
|
|
1026
1026
|
aboutEmpty: 'Content not published yet — stay tuned.',
|
|
1027
1027
|
aboutUpdated: 'Updated',
|
|
1028
1028
|
empty: 'No plugins in this category yet',
|
|
@@ -33,7 +33,7 @@ declare const __PLUGIN_VERSION__: string
|
|
|
33
33
|
/**
|
|
34
34
|
* 插件当前版本号,供头部标题展示「DSH Plugin Hub v0.1.1」。
|
|
35
35
|
* tsdown 构建时用 define 把 __PLUGIN_VERSION__ 替换成 package.json 的版本号;
|
|
36
|
-
* node --test 直接 import 本模块时该标识符不存在,typeof
|
|
36
|
+
* node --test 直接 import 本模块时该标识符不存在,typeof 守卫缺省为空串。
|
|
37
37
|
*/
|
|
38
38
|
export const PLUGIN_VERSION: string =
|
|
39
39
|
typeof __PLUGIN_VERSION__ === 'string' ? __PLUGIN_VERSION__ : ''
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* 自动补回缺失的插件 CSS 标签。
|
|
3
3
|
*
|
|
4
4
|
* CSS 注入是 bundle 工厂的一次性副作用(以「标签已存在」为守卫)。宿主重启后页面仍在,
|
|
5
5
|
* 但宿主 HMR 链可能移除 <style data-plugin> 标签,而内存模块缓存仍持有旧工厂,工厂不再
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*
|
|
8
8
|
* tsdown 的 `dsh-css-modules-inline` 构建插件会在工厂执行时把每个模块的 { tagId, css }
|
|
9
9
|
* 登记进全局列表 `__DSH_PLUGIN_CSS__`。此工具随后比对列表与实时 DOM,补注入缺失的标签,
|
|
10
|
-
*
|
|
10
|
+
* 把「点一圈才恢复」变成挂载 / 切换视图时自动完成。
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
interface PluginCssEntry {
|
|
@@ -16,7 +16,7 @@ export interface NotificationRecord {
|
|
|
16
16
|
/** owner/repo(可能为空:请求层失败但拿不到仓库时) */
|
|
17
17
|
repo: string
|
|
18
18
|
/** 实际动作(install/update/uninstall):成功/失败通知据此区分「安装成功」与「更新成功」。
|
|
19
|
-
* 缺省按 kind
|
|
19
|
+
* 缺省按 kind 归类(历史记录可能没有该字段)。更新提醒(kind='update')不用此字段。 */
|
|
20
20
|
action?: 'install' | 'update' | 'uninstall'
|
|
21
21
|
/** 失败时的完整错误日志;成功记录为空串 */
|
|
22
22
|
message: string
|
|
@@ -150,7 +150,7 @@ export function removeNotification(id: number): NotificationRecord[] {
|
|
|
150
150
|
|
|
151
151
|
export type FailureKind = 'npmTooOld' | 'nodeEngine' | 'dshMissing' | 'gitMissing' | 'pnpmMissing' | 'npmMissing' | 'pnpmStore' | 'pnpmModulesMismatch' | 'profileYamlCorrupted' | 'profileLocalBuild' | 'profileState' | 'pnpmWorkspace' | 'originRejected' | 'pnpmPolicy' | 'pnpmUnusedPatch' | 'pnpmMissingDep' | 'pluginMissingDep' | 'registryVersionStale' | 'registryAuth' | 'profileExclusive' | 'pnpmLinkedDirMissing' | 'fileLocked' | 'accessDenied' | 'fsUnavailable' | 'buildToolchainMissing' | 'pnpmIgnoredBuild' | 'pluginPrepare' | 'hostIncompatible' | 'tlsTrust' | 'network' | 'hostCliSpawn' | 'hubState' | 'profileBundleMissing' | 'timeout' | 'profileLock' | 'dshTooOld' | 'localEnv' | 'hub'
|
|
152
152
|
|
|
153
|
-
/**
|
|
153
|
+
/** 失败动作规则:标题、上报正文与提示文案按安装/更新/卸载取词,不写死「安装」。 */
|
|
154
154
|
export type FailureAction = 'install' | 'update' | 'uninstall'
|
|
155
155
|
|
|
156
156
|
/** 动作词(英文):名词短语,标题与句子里都成立。 */
|
|
@@ -163,7 +163,7 @@ export function actionWordZh(action: FailureAction): string {
|
|
|
163
163
|
return action === 'uninstall' ? '卸载' : action === 'update' ? '更新' : '安装'
|
|
164
164
|
}
|
|
165
165
|
|
|
166
|
-
/** 记录的动作:老记录只有 kind,按 kind
|
|
166
|
+
/** 记录的动作:老记录只有 kind,按 kind 归类。 */
|
|
167
167
|
export function actionOfRecord(rec: { kind: NotificationRecord['kind']; action?: FailureAction }): FailureAction {
|
|
168
168
|
if (rec.action !== undefined) return rec.action
|
|
169
169
|
return rec.kind === 'uninstall' ? 'uninstall' : rec.kind === 'update' ? 'update' : 'install'
|
|
@@ -231,7 +231,7 @@ export function pnpmPolicyHintParams(message: string): Record<string, string> {
|
|
|
231
231
|
* (只影响这个插件 → 才引导提 Issue)。各分支判定顺序有依赖,见函数内注释。
|
|
232
232
|
*/
|
|
233
233
|
export function classifyFailure(message: string): FailureKind {
|
|
234
|
-
//
|
|
234
|
+
// 宿主版本要求:插件声明的兼容范围不含当前 dsh 版本,宿主在安装前直接拒绝。
|
|
235
235
|
// 必须最先判 —— 这是宿主自己给的结论,不是 pnpm 的安装失败
|
|
236
236
|
if (/installation rejected:.*incompatible with dsh/i.test(message)) return 'hostIncompatible'
|
|
237
237
|
// desktop profile 由桌面应用独占:外部 dsh CLI 无权管理,宿主在安装前直接拒绝。
|
|
@@ -266,7 +266,7 @@ export function classifyFailure(message: string): FailureKind {
|
|
|
266
266
|
// pnpm 供应链策略拦截(缺 tarball integrity / 校验和不匹配 / minimumReleaseAge / untrusted origin)。
|
|
267
267
|
// 必须在 pnpmIgnoredBuild 之前
|
|
268
268
|
if (missingTarballIntegrity(message) || mismatchedTarballIntegrity(message) || /ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION|ERR_PNPM_NO_MATURE_MATCHING_VERSION|Minimum release age|minimumReleaseAge|untrusted origin/i.test(message)) return 'pnpmPolicy'
|
|
269
|
-
// profile 残留旧版本 patch 声明,pnpm
|
|
269
|
+
// profile 残留旧版本 patch 声明,pnpm 中止安装。必须排在默认归类之前
|
|
270
270
|
if (/ERR_PNPM_UNUSED_PATCH|patches were not used/i.test(message)) return 'pnpmUnusedPatch'
|
|
271
271
|
// profile 里的 YAML 配置解析失败(pnpm-workspace.yaml / pnpm-lock.yaml 有重复键、缩进坏了):
|
|
272
272
|
// pnpm 在依赖解析之前就退出,装任何插件都会同样失败,属本机 profile 配置问题。
|
|
@@ -334,7 +334,7 @@ export function classifyFailure(message: string): FailureKind {
|
|
|
334
334
|
if (/git-hosted plugins build on install via their prepare script|add the exact key pnpm printed above under allowBuilds/i.test(message)) return 'pnpmIgnoredBuild'
|
|
335
335
|
// 宿主启动插件管理时找不到某个 profile bundle(`cannot resolve profile bundle "<pkg>"`):
|
|
336
336
|
// 该 profile 声明的依赖没装全(node_modules 被删过 / 上次安装中断 / 桌面端更新不完整),
|
|
337
|
-
// 与本次要装的插件无关 ——
|
|
337
|
+
// 与本次要装的插件无关 —— 必须排在 hub 的默认归类之前,否则又会被当成未知错误丢给我们
|
|
338
338
|
if (/cannot resolve profile bundle/i.test(message)) return 'profileBundleMissing'
|
|
339
339
|
// 宿主自身入口起不来(服务端 [host-spawn] 标记):环境问题,给提示、不引导提 Issue。
|
|
340
340
|
// 不匹配 `spawn … ENOENT` 文本 —— 那个形态任何 spawn(curl/git/taskkill…)都会产出
|
|
@@ -354,7 +354,7 @@ export function classifyFailure(message: string): FailureKind {
|
|
|
354
354
|
return 'hub'
|
|
355
355
|
}
|
|
356
356
|
|
|
357
|
-
/**
|
|
357
|
+
/** 「特征指向本机环境但说不清具体哪一类」的最后的判断规则:系统调用错误码(权限/路径过长/设备异常)、
|
|
358
358
|
* 证书与代理校验失败、本地配置文件(~/.npmrc、profile 的 package.json)损坏。
|
|
359
359
|
* 只做排除用:命中它表示「不是插件侧问题」,不代表我们已经定位到原因。 */
|
|
360
360
|
const LOCAL_ENV_RE = /os error \d{1,3}\b|ERROR_[A-Z_]{4,}|\bE(?:ACCES|PERM|NOENT|NAMETOOLONG|INVAL|PROTO|ISDIR|NOTDIR|EXIST|BADF|NOTEMPTY|JSONPARSE|NOMEM)\b|JavaScript heap out of memory|Unexpected token .{0,60}in JSON|SELF_SIGNED_CERT_IN_CHAIN|UNABLE_TO_VERIFY_LEAF_SIGNATURE|ERR_TLS_CERT_ALTNAME_INVALID|unable to verify the first certificate|self[- ]signed certificate|ERR_INVALID_URL|invalid registry|proxy authentication|407 Proxy/i
|
|
@@ -386,7 +386,7 @@ export const MAX_CORE_CHARS = 5000
|
|
|
386
386
|
const MAX_LINE_CHARS = 400
|
|
387
387
|
|
|
388
388
|
/**
|
|
389
|
-
*
|
|
389
|
+
* 展示用格式化:宿主兼容性提示里的 peerDependencies 是压缩成一行的 JSON,
|
|
390
390
|
* 直接展示像一团乱码;展开成缩进格式,解析失败则原样返回。
|
|
391
391
|
*/
|
|
392
392
|
export function formatFailureMessage(message: string): string {
|
|
@@ -420,7 +420,7 @@ export function summarizeError(message: string, maxChars: number = MAX_CORE_CHAR
|
|
|
420
420
|
}
|
|
421
421
|
let out: string
|
|
422
422
|
if (core.length === 0) {
|
|
423
|
-
// 整段本来就是短消息时直接给全量:head+tail
|
|
423
|
+
// 整段本来就是短消息时直接给全量:head+tail 的截断方式会把同一段重复两遍,正文反而更难读
|
|
424
424
|
const whole = message.trim()
|
|
425
425
|
if (whole.length <= maxChars) return whole
|
|
426
426
|
// 无关键行时退化为「头部 + 尾部」快照:优先保尾部完整,头部按预算压缩,总长不超上限
|
|
@@ -29,12 +29,14 @@ export function installTargetOf(p: HubPlugin): string {
|
|
|
29
29
|
|
|
30
30
|
/** 展示用安装命令(复制/弹窗):目录下发的权威命令优先(CLI-only 插件如 dsh-tui
|
|
31
31
|
* 需专属 profile(--profile dsh-tui),无法从 npm 包名推断,必须用官方命令);
|
|
32
|
-
* 常规插件无目录命令时用 npm 包名回退生成。
|
|
33
|
-
|
|
32
|
+
* 常规插件无目录命令时用 npm 包名回退生成。
|
|
33
|
+
* version 为已过发布冷却期的版本:带上它,手抄命令与一键安装的执行结果一致。 */
|
|
34
|
+
export function installCommandOf(p: HubPlugin, withProfile = false, version?: string): string {
|
|
34
35
|
const cmd = p.install?.command
|
|
35
36
|
if (cmd) return cmd
|
|
36
37
|
const prefix = `dsh plugin${withProfile ? ' --profile web' : ''} add `
|
|
37
|
-
|
|
38
|
+
const target = installTargetOf(p)
|
|
39
|
+
return `${prefix}${version === undefined || version === '' ? target : `${target}@${version}`}`
|
|
38
40
|
}
|
|
39
41
|
|
|
40
42
|
/** 把任务/安装目标归一化为 owner/repo 展示身份。 */
|
|
@@ -42,7 +42,7 @@ export interface InstalledItem {
|
|
|
42
42
|
catalogVersion: string | null
|
|
43
43
|
/** 是否有可用更新(有版本比版本,无版本比仓库更新时间) */
|
|
44
44
|
hasUpdate: boolean
|
|
45
|
-
/**
|
|
45
|
+
/** 更新暂缓(新版本还没过冷却期)的可重试时刻(毫秒);无更新/非冷却期原因 → null */
|
|
46
46
|
pendingUntil: number | null
|
|
47
47
|
/** 是否已加载进运行中 loader(官方 ctx.loader 对账);false = 已安装但未挂载(装完未重启) */
|
|
48
48
|
loaded: boolean
|