dsh-plugin 1.3.1 → 1.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/client/client.js +629 -321
- package/client/client.js.map +1 -1
- package/lib/http/routes.js +24 -1
- package/lib/services/install/task-queue.js +9 -3
- package/lib/services/log.js +12 -0
- package/lib/types/services/install/task-queue.d.ts +2 -0
- package/lib/types/services/log.d.ts +2 -0
- package/package.json +1 -1
- package/src/client/components/PluginHubSection.tsx +16 -6
- package/src/client/components/modals/ConfirmDialog.tsx +73 -0
- package/src/client/components/modals/LogsModal.tsx +49 -4
- package/src/client/components/modals/NotificationsModal.tsx +29 -4
- package/src/client/components/modals/modals.tsx +13 -1
- package/src/client/components/ui/icons.tsx +48 -0
- package/src/client/components/views/LogsView.tsx +1 -1
- package/src/client/components/views/SettingsView.tsx +5 -1
- package/src/client/data/catalog.ts +8 -7
- package/src/client/hooks/useCatalog.ts +37 -5
- package/src/client/locales.ts +22 -2
- package/src/client/logic/ensure-plugin-css.ts +42 -0
- package/src/client/styles/LogsView.module.css +29 -8
- package/src/client/styles/Modal.module.css +77 -0
- package/src/server/http/routes.ts +22 -1
- package/src/server/services/install/task-queue.ts +13 -3
- package/src/server/services/log.ts +12 -0
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* installed-plugin table, and the filter/search/sort/install-status view
|
|
10
10
|
* state, exposing the derived visible list and per-category counts.
|
|
11
11
|
*/
|
|
12
|
-
import { useEffect, useMemo, useState } from 'react'
|
|
12
|
+
import { useEffect, useMemo, useRef, useState } from 'react'
|
|
13
13
|
import type { HubPlugin, LocaleId } from '../types.ts'
|
|
14
14
|
import { HUB_REPO, PLUGIN_VERSION } from '../logic/constants.ts'
|
|
15
15
|
import type { HubAboutInfo, HubUpdateInfo } from '../types.ts'
|
|
@@ -45,6 +45,14 @@ export function useCatalog(lang: LocaleId) {
|
|
|
45
45
|
const [sort, setSort] = useState<SortKey>('sortStars')
|
|
46
46
|
/** 当前排序方向:点同一个排序按钮切换 正序/倒序;点新排序用该排序的默认方向(与已安装视图一致) */
|
|
47
47
|
const [sortDir, setSortDir] = useState<'asc' | 'desc'>('desc')
|
|
48
|
+
/** 目录加载「超时自动强制刷新」重试计数:宿主重启/代理未就绪时请求可能长时间挂起,
|
|
49
|
+
* 界面停在「正在加载插件数据…」假死 —— 超时自动重拉,最多自动重试 2 次后转失败态;
|
|
50
|
+
* 用户手动刷新(reload)时清零重新计数。 */
|
|
51
|
+
const loadRetriesRef = useRef(0)
|
|
52
|
+
/** 目录加载超时阈值:超过即判定挂起,强制刷新一次(服务端代理 curl 上限 20s,客户端 12s 先兜底) */
|
|
53
|
+
const LOAD_TIMEOUT_MS = 12000
|
|
54
|
+
/** 挂起后最多自动重试次数:避免代理持续不通时无限循环刷新(每次重试仍 12s,2 次后转失败界面可手动重试) */
|
|
55
|
+
const MAX_LOAD_RETRIES = 2
|
|
48
56
|
|
|
49
57
|
/** 点击排序按钮:同一按钮切换正/倒序,新按钮用默认方向 */
|
|
50
58
|
function toggleSort(key: SortKey) {
|
|
@@ -83,15 +91,21 @@ export function useCatalog(lang: LocaleId) {
|
|
|
83
91
|
// 拉取目录 + 统计。在线 API 已只返回 verified;再过滤一次,保证只展示人工验证通过的插件。
|
|
84
92
|
useEffect(() => {
|
|
85
93
|
let cancelled = false
|
|
94
|
+
// 本次加载是否已收尾(成功或失败):超时定时器只对「还在加载中」生效
|
|
95
|
+
let settled = false
|
|
96
|
+
// 中止控制器:请求挂起时(宿主重启/代理抖动)超时自动重试会换新请求,
|
|
97
|
+
// 旧请求在此中止释放连接,避免挂起堆积
|
|
98
|
+
const controller = new AbortController()
|
|
86
99
|
setPlugins(null)
|
|
87
100
|
setHubPlugin(null)
|
|
88
101
|
setStats(null)
|
|
89
102
|
setFailed(false)
|
|
90
103
|
// 目录数据直拉:hub 自身版本由独立 Worker 版本控制中心管理,不再依赖主站版本号接口。
|
|
91
|
-
const load = () => Promise.all([fetchCatalog(lang), fetchStats()])
|
|
104
|
+
const load = () => Promise.all([fetchCatalog(lang, controller.signal), fetchStats(controller.signal)])
|
|
92
105
|
load()
|
|
93
106
|
.then(([list, stats]) => {
|
|
94
107
|
if (cancelled) return
|
|
108
|
+
settled = true
|
|
95
109
|
// 插件市场不显示自己:DSH Plugin Hub 从目录里排除自身条目,
|
|
96
110
|
// 防止「自己出现在自己的插件列表里、还能自己安装自己」;统计计数同步减 1 保持与列表一致。
|
|
97
111
|
// 目录数据已排除自身,此处再兜底过滤一次,防旧快照仍含自身条目。
|
|
@@ -106,9 +120,27 @@ export function useCatalog(lang: LocaleId) {
|
|
|
106
120
|
}
|
|
107
121
|
})
|
|
108
122
|
.catch(() => {
|
|
109
|
-
if (
|
|
123
|
+
if (cancelled) return
|
|
124
|
+
settled = true
|
|
125
|
+
setFailed(true)
|
|
110
126
|
})
|
|
111
|
-
|
|
127
|
+
// 加载超时兜底:宿主刚重启/代理未就绪时目录请求可能长时间挂起,界面停在「正在加载插件数据…」
|
|
128
|
+
// 像假死。超过阈值未收尾 → 强制刷新列表(重拉一次,最多 MAX_LOAD_RETRIES 次自动重试),
|
|
129
|
+
// 重试仍失败 → 转失败界面(显示「重试」按钮,手动刷新时计数清零重新开始)。
|
|
130
|
+
const timer = window.setTimeout(() => {
|
|
131
|
+
if (cancelled || settled) return
|
|
132
|
+
if (loadRetriesRef.current < MAX_LOAD_RETRIES) {
|
|
133
|
+
loadRetriesRef.current += 1
|
|
134
|
+
setReloadKey((k) => k + 1)
|
|
135
|
+
} else {
|
|
136
|
+
setFailed(true)
|
|
137
|
+
}
|
|
138
|
+
}, LOAD_TIMEOUT_MS)
|
|
139
|
+
return () => {
|
|
140
|
+
cancelled = true
|
|
141
|
+
window.clearTimeout(timer)
|
|
142
|
+
controller.abort()
|
|
143
|
+
}
|
|
112
144
|
// lang 必须进依赖:切换界面语言要重新拉对应语言的数据文件,
|
|
113
145
|
// 否则英文模式仍停留在 plugins.zh.json,描述/名称全是中文
|
|
114
146
|
}, [reloadKey, lang])
|
|
@@ -218,7 +250,7 @@ export function useCatalog(lang: LocaleId) {
|
|
|
218
250
|
hubPlugin,
|
|
219
251
|
stats,
|
|
220
252
|
failed,
|
|
221
|
-
reload: () => setReloadKey((k) => k + 1),
|
|
253
|
+
reload: () => { loadRetriesRef.current = 0; setReloadKey((k) => k + 1) },
|
|
222
254
|
installed,
|
|
223
255
|
installedItems,
|
|
224
256
|
installedName,
|
package/src/client/locales.ts
CHANGED
|
@@ -141,6 +141,12 @@ export const zh = {
|
|
|
141
141
|
logOpenFile: '打开文件',
|
|
142
142
|
logOpenFileTip: '在系统文件管理器中定位日志文件',
|
|
143
143
|
logOpenFileFail: '打开失败,请在文件管理器中手动定位',
|
|
144
|
+
logClearAll: '清空所有日志',
|
|
145
|
+
logClearConfirmTitle: '清空所有日志?',
|
|
146
|
+
logClearConfirmDesc: '将删除全部日志记录,且不可恢复。',
|
|
147
|
+
logClearing: '正在清空…',
|
|
148
|
+
logClearAllTip: '清空全部日志记录',
|
|
149
|
+
logClearFailed: '清空失败,请重试',
|
|
144
150
|
logPathSettingTitle: '日志存放位置',
|
|
145
151
|
logPathSettingDesc: '默认存于宿主配置目录,可填目录或 .log 文件路径改存别处',
|
|
146
152
|
logPathPlaceholder: '留空恢复默认',
|
|
@@ -241,7 +247,7 @@ export const zh = {
|
|
|
241
247
|
restartNow: '立即重启',
|
|
242
248
|
restartLater: '稍后重启',
|
|
243
249
|
restarting: '正在重启…',
|
|
244
|
-
restartHint: '
|
|
250
|
+
restartHint: '重启会中断正在进行的安装/卸载任务,其进度将丢失。部分插件(如插件市场、顶层 bundle)需重启后才会生效。',
|
|
245
251
|
done: '完成',
|
|
246
252
|
// 待重启:装完没重启的插件常驻提醒,直到宿主真正重启
|
|
247
253
|
restartPendingHint: '安装成功,重启后生效',
|
|
@@ -254,7 +260,11 @@ export const zh = {
|
|
|
254
260
|
notificationsDesc: '安装、卸载任务与更新提醒都会记录在这里,即使错过提示也能随时回来查看。',
|
|
255
261
|
notificationsEmpty: '暂无通知记录',
|
|
256
262
|
notificationsClear: '清空通知',
|
|
263
|
+
notificationsClearConfirmTitle: '清空所有通知?',
|
|
264
|
+
notificationsClearConfirmDesc: '将删除全部通知记录,且不可恢复。',
|
|
257
265
|
removeNotification: '删除这条通知',
|
|
266
|
+
removeNotificationConfirmTitle: '删除这条通知?',
|
|
267
|
+
removeNotificationConfirmDesc: '该通知将被移除,且不可恢复。',
|
|
258
268
|
// 串行队列:安装/卸载弹窗内可后台关闭,排队任务自动串行执行
|
|
259
269
|
queuedTitle: '已加入安装排队',
|
|
260
270
|
queuedUninstallTitle: '已加入卸载排队',
|
|
@@ -458,6 +468,12 @@ export const en = {
|
|
|
458
468
|
logOpenFile: 'Reveal',
|
|
459
469
|
logOpenFileTip: 'Reveal the log file in your system file manager',
|
|
460
470
|
logOpenFileFail: 'Could not open; locate the file manually',
|
|
471
|
+
logClearAll: 'Clear all logs',
|
|
472
|
+
logClearConfirmTitle: 'Clear all logs?',
|
|
473
|
+
logClearConfirmDesc: 'All log entries will be deleted. This cannot be undone.',
|
|
474
|
+
logClearing: 'Clearing…',
|
|
475
|
+
logClearAllTip: 'Delete all log entries',
|
|
476
|
+
logClearFailed: 'Failed to clear; try again',
|
|
461
477
|
logPathSettingTitle: 'Log Location',
|
|
462
478
|
logPathSettingDesc: 'Stored in the host config directory by default — set a directory or a .log file path to store elsewhere',
|
|
463
479
|
logPathPlaceholder: 'Leave empty for default',
|
|
@@ -558,7 +574,7 @@ export const en = {
|
|
|
558
574
|
restartNow: 'Restart now',
|
|
559
575
|
restartLater: 'Later',
|
|
560
576
|
restarting: 'Restarting…',
|
|
561
|
-
restartHint: 'Some plugins (such as plugin markets / top-level bundles) only take effect after a restart.',
|
|
577
|
+
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.',
|
|
562
578
|
done: 'Done',
|
|
563
579
|
restartPendingHint: 'Installed — takes effect after restart',
|
|
564
580
|
restartPendingHintUninstall: 'Removed — cleaned up after restart',
|
|
@@ -570,7 +586,11 @@ export const en = {
|
|
|
570
586
|
notificationsDesc: 'Every install, remove and update notice is logged here — even if you missed the prompt, you can always check back.',
|
|
571
587
|
notificationsEmpty: 'No notifications yet',
|
|
572
588
|
notificationsClear: 'Clear notifications',
|
|
589
|
+
notificationsClearConfirmTitle: 'Clear all notifications?',
|
|
590
|
+
notificationsClearConfirmDesc: 'All notification records will be deleted. This cannot be undone.',
|
|
573
591
|
removeNotification: 'Remove this notification',
|
|
592
|
+
removeNotificationConfirmTitle: 'Remove this notification?',
|
|
593
|
+
removeNotificationConfirmDesc: 'This notification will be removed. This cannot be undone.',
|
|
574
594
|
queuedTitle: 'Queued…',
|
|
575
595
|
queuedUninstallTitle: 'Queued for removal…',
|
|
576
596
|
queuedHint: 'Queued — starts automatically after earlier tasks finish. You may close this window.',
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Self-heal missing plugin CSS tags.
|
|
3
|
+
*
|
|
4
|
+
* CSS injection is a one-shot side effect of the bundle factory (guarded by
|
|
5
|
+
* "tag already present"). When the host restarts the page survives, but the
|
|
6
|
+
* host HMR chain may remove the <style data-plugin> tags while the in-memory
|
|
7
|
+
* module cache still holds the old factory — so the factory never re-runs and
|
|
8
|
+
* the styles never come back until some interaction triggers a reload.
|
|
9
|
+
*
|
|
10
|
+
* The tsdown `dsh-css-modules-inline` build plugin registers every module's
|
|
11
|
+
* { tagId, css } into the global list `__DSH_PLUGIN_CSS__` at factory time.
|
|
12
|
+
* This helper compares that list against the live DOM and re-injects any
|
|
13
|
+
* missing tags, turning the "click around to fix it" self-heal into an
|
|
14
|
+
* automatic one on mount / view switch.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
interface PluginCssEntry {
|
|
18
|
+
tagId: string
|
|
19
|
+
css: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
declare global {
|
|
23
|
+
interface Window {
|
|
24
|
+
__DSH_PLUGIN_CSS__?: PluginCssEntry[]
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Re-inject any registered stylesheet that is missing from the DOM. Idempotent. */
|
|
29
|
+
export function ensurePluginCss(): void {
|
|
30
|
+
if (typeof document === 'undefined') return
|
|
31
|
+
const registry = window.__DSH_PLUGIN_CSS__
|
|
32
|
+
if (!registry || registry.length === 0) return
|
|
33
|
+
for (const { tagId, css } of registry) {
|
|
34
|
+
const selector = 'style[data-plugin-css=' + JSON.stringify(tagId) + ']'
|
|
35
|
+
if (document.querySelector(selector) !== null) continue
|
|
36
|
+
const tag = document.createElement('style')
|
|
37
|
+
tag.dataset.plugin = 'dsh-plugin'
|
|
38
|
+
tag.dataset.pluginCss = tagId
|
|
39
|
+
tag.textContent = css
|
|
40
|
+
document.head.appendChild(tag)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -62,16 +62,37 @@
|
|
|
62
62
|
cursor: default;
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
/*
|
|
66
|
-
.
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
65
|
+
/* 弹窗头部动作:清空所有日志(危险操作,二次确认时转红) */
|
|
66
|
+
.headActions {
|
|
67
|
+
flex: 1;
|
|
68
|
+
display: flex;
|
|
69
|
+
justify-content: flex-end;
|
|
70
|
+
margin-right: 4px;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.clearBtn {
|
|
74
|
+
flex: none;
|
|
75
|
+
height: 22px;
|
|
76
|
+
padding: 0 10px;
|
|
77
|
+
font-size: 11px;
|
|
78
|
+
font-weight: 500;
|
|
79
|
+
color: var(--hub-text-secondary);
|
|
80
|
+
background: transparent;
|
|
81
|
+
border: 1px solid var(--hub-border-2);
|
|
82
|
+
border-radius: 6px;
|
|
83
|
+
cursor: pointer;
|
|
84
|
+
user-select: none;
|
|
85
|
+
transition: color 0.12s ease, background-color 0.12s ease, border-color 0.12s ease;
|
|
70
86
|
}
|
|
71
87
|
|
|
72
|
-
.
|
|
73
|
-
|
|
74
|
-
|
|
88
|
+
.clearBtn:hover:not(:disabled) {
|
|
89
|
+
color: var(--hub-text-primary);
|
|
90
|
+
background: var(--hub-bg-2);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.clearBtn:disabled {
|
|
94
|
+
opacity: 0.55;
|
|
95
|
+
cursor: default;
|
|
75
96
|
}
|
|
76
97
|
|
|
77
98
|
/* 日志列表:一行一条,悬浮高亮;超出限高内部滚动 */
|
|
@@ -122,6 +122,83 @@
|
|
|
122
122
|
flex-shrink: 0;
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
+
/* 确认弹窗图标区:图标居中占位,位于标题下方、描述上方 —— 所有询问框都要有对应图标 */
|
|
126
|
+
.confirmIconWrap {
|
|
127
|
+
display: flex;
|
|
128
|
+
justify-content: center;
|
|
129
|
+
padding: 2px 0 6px;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/* 确认弹窗图标:圆形徽标(48px),普通确认 = 品牌蓝底白图形 */
|
|
133
|
+
.confirmIcon {
|
|
134
|
+
width: 48px;
|
|
135
|
+
height: 48px;
|
|
136
|
+
flex: none;
|
|
137
|
+
display: inline-flex;
|
|
138
|
+
align-items: center;
|
|
139
|
+
justify-content: center;
|
|
140
|
+
border-radius: 50%;
|
|
141
|
+
background: var(--hub-brand);
|
|
142
|
+
box-shadow: 0 4px 12px rgba(79, 110, 247, 0.28);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/* 危险操作确认图标:红色底白图形 —— 清空/删除/卸载/重置,醒目区分于普通确认 */
|
|
146
|
+
.confirmIconDanger {
|
|
147
|
+
background: var(--hub-danger);
|
|
148
|
+
box-shadow: 0 4px 12px rgba(209, 36, 47, 0.3);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/* 危险操作确认按钮:红色实底白字 —— 清空/删除/卸载/重置的最终确认,显著醒目 */
|
|
152
|
+
.dangerConfirm {
|
|
153
|
+
flex: none;
|
|
154
|
+
height: 28px;
|
|
155
|
+
padding: 0 14px;
|
|
156
|
+
font-size: 12px;
|
|
157
|
+
font-weight: 600;
|
|
158
|
+
color: #ffffff;
|
|
159
|
+
background: var(--hub-danger);
|
|
160
|
+
border: 1px solid var(--hub-danger);
|
|
161
|
+
border-radius: 6px;
|
|
162
|
+
cursor: pointer;
|
|
163
|
+
user-select: none;
|
|
164
|
+
transition: background-color 0.12s ease, border-color 0.12s ease;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.dangerConfirm:hover:not(:disabled) {
|
|
168
|
+
background: var(--hub-danger-hover);
|
|
169
|
+
border-color: var(--hub-danger-hover);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
.dangerConfirm:disabled {
|
|
173
|
+
opacity: 0.55;
|
|
174
|
+
cursor: default;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/* 普通确认按钮:品牌蓝实底白字 —— 安装/更新/重启/忽略等常规确认 */
|
|
178
|
+
.confirmPrimary {
|
|
179
|
+
flex: none;
|
|
180
|
+
height: 28px;
|
|
181
|
+
padding: 0 14px;
|
|
182
|
+
font-size: 12px;
|
|
183
|
+
font-weight: 600;
|
|
184
|
+
color: #ffffff;
|
|
185
|
+
background: var(--hub-brand);
|
|
186
|
+
border: 1px solid var(--hub-brand);
|
|
187
|
+
border-radius: 6px;
|
|
188
|
+
cursor: pointer;
|
|
189
|
+
user-select: none;
|
|
190
|
+
transition: filter 0.12s ease;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
.confirmPrimary:hover:not(:disabled) {
|
|
194
|
+
filter: brightness(1.08);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
.confirmPrimary:disabled {
|
|
198
|
+
opacity: 0.55;
|
|
199
|
+
cursor: default;
|
|
200
|
+
}
|
|
201
|
+
|
|
125
202
|
.modalDesc {
|
|
126
203
|
font-size: 12px;
|
|
127
204
|
line-height: 18px;
|
|
@@ -20,7 +20,7 @@ import { resolveNpmPackage } from '../services/install/npm-resolve.ts'
|
|
|
20
20
|
import { preflightTarget } from '../services/install/preflight.ts'
|
|
21
21
|
import { isDshPlugin, isEntryLoaded } from '../services/loader.ts'
|
|
22
22
|
import { loadSettings, saveSettings, resetSettings, type HubSettings } from '../services/settings.ts'
|
|
23
|
-
import { appendLog, readLog, logFilePath, defaultLogFilePath, customLogFile } from '../services/log.ts'
|
|
23
|
+
import { appendLog, clearLog, readLog, logFilePath, defaultLogFilePath, customLogFile } from '../services/log.ts'
|
|
24
24
|
|
|
25
25
|
export interface WebRoute {
|
|
26
26
|
kind: 'exact'
|
|
@@ -325,6 +325,25 @@ export function mountPluginHubRoutes(webServer: WebServerService, profile: strin
|
|
|
325
325
|
}
|
|
326
326
|
},
|
|
327
327
|
}),
|
|
328
|
+
webServer.register({
|
|
329
|
+
kind: 'exact',
|
|
330
|
+
path: '/dsh-plugin-hub/clear-log',
|
|
331
|
+
handler: (request, response) => {
|
|
332
|
+
if (!requireTrustedPost(request, response)) return
|
|
333
|
+
// 清空所有日志:截断当前日志文件为空
|
|
334
|
+
try {
|
|
335
|
+
const ok = clearLog(profile)
|
|
336
|
+
if (!ok) {
|
|
337
|
+
sendJson(response, 500, { error: 'failed to clear log' })
|
|
338
|
+
return
|
|
339
|
+
}
|
|
340
|
+
appendLog(profile, { at: Date.now(), level: 'info', category: 'system', event: 'system.clear', message: '日志已由用户清空' })
|
|
341
|
+
sendJson(response, 200, { ok: true })
|
|
342
|
+
} catch {
|
|
343
|
+
sendJson(response, 500, { error: 'failed to clear log' })
|
|
344
|
+
}
|
|
345
|
+
},
|
|
346
|
+
}),
|
|
328
347
|
webServer.register({
|
|
329
348
|
kind: 'exact',
|
|
330
349
|
path: '/dsh-plugin-hub/open-log',
|
|
@@ -659,6 +678,8 @@ export function mountPluginHubRoutes(webServer: WebServerService, profile: strin
|
|
|
659
678
|
attempts,
|
|
660
679
|
timeoutMs: COMMAND_TIMEOUT_MS,
|
|
661
680
|
env: mutationEnv(settings),
|
|
681
|
+
// 覆盖更新:npm 通道命令显式 @latest(pnpm add 无版本对已存在依赖是幂等的,不加版本不会真更新)
|
|
682
|
+
updateNpm: mode === 'update',
|
|
662
683
|
})
|
|
663
684
|
sendJson(response, 200, { ok: true, task: task.id })
|
|
664
685
|
} catch (error) {
|
|
@@ -177,6 +177,8 @@ export function startPluginMutation(options: {
|
|
|
177
177
|
uninstallLoader?: LoaderHandle
|
|
178
178
|
/** 入队前已尝试的安装方式(npm registry 反查等):失败提 Issue 时如实展示;实际执行的命令由 spawn 时追加 */
|
|
179
179
|
attempts?: string[]
|
|
180
|
+
/** 更新已安装的 npm 包(mode=update):命令显式带 @latest,让 pnpm 真正解析最新版本 */
|
|
181
|
+
updateNpm?: boolean
|
|
180
182
|
}): InstallTask {
|
|
181
183
|
const task: InstallTask = { id: nextTaskId, target: options.target, displayTarget: options.displayTarget, globalNpm: options.globalNpm, action: options.action, status: 'pending', timedOut: false, exitCode: null, progress: 0, lines: [], attempts: options.attempts ?? [], needsRestart: false }
|
|
182
184
|
nextTaskId = nextTaskId >= Number.MAX_SAFE_INTEGER ? 1 : nextTaskId + 1
|
|
@@ -266,21 +268,29 @@ function spawnMutation(options: {
|
|
|
266
268
|
uninstallLoader?: LoaderHandle
|
|
267
269
|
/** 全局 npm 安装(官方 `npm install -g <pkgs>`):非空时执行全局安装,不进任何 profile、无需宿主重启 */
|
|
268
270
|
globalNpm?: string[]
|
|
271
|
+
/** 更新已安装的 npm 包(mode=update):命令显式带 @latest,让 pnpm 真正解析最新版本 */
|
|
272
|
+
updateNpm?: boolean
|
|
269
273
|
}): Promise<InstallResult> {
|
|
270
|
-
const { action, profile, target, timeoutMs = 5 * 60 * 1000, env, task, displayTarget, uninstallLoader, globalNpm } = options
|
|
274
|
+
const { action, profile, target, timeoutMs = 5 * 60 * 1000, env, task, displayTarget, uninstallLoader, globalNpm, updateNpm } = options
|
|
271
275
|
// 全局 npm 安装:直接 spawn 本机 npm(`npm install -g <pkgs>`),不是 dsh plugin 命令
|
|
272
276
|
const isGlobalNpm = (globalNpm ?? []).length > 0
|
|
273
277
|
const invocation = isGlobalNpm
|
|
274
278
|
? { file: 'npm', prefixArgs: [], cwd: process.cwd(), useShell: process.platform === 'win32' }
|
|
275
279
|
: cliInvocation()
|
|
280
|
+
// 更新已安装的 npm 包:显式指定 @latest。pnpm add <pkg>(无版本)对已存在依赖
|
|
281
|
+
// (尤其精确 spec 如 "1.3.0")是幂等的 —— 输出 "Already up to date" 且不改 package.json,
|
|
282
|
+
// 导致「更新成功」但实际仍停留在旧版本;git 地址与全局安装不受此影响。
|
|
283
|
+
const npmUpdateTarget = updateNpm && !isGlobalNpm && githubRepoOf(target) === null
|
|
284
|
+
? `${target}@latest`
|
|
285
|
+
: target
|
|
276
286
|
const args = isGlobalNpm
|
|
277
287
|
? ['install', '-g', ...(globalNpm ?? [])]
|
|
278
|
-
: [...invocation.prefixArgs, 'plugin', '--profile', profile, action,
|
|
288
|
+
: [...invocation.prefixArgs, 'plugin', '--profile', profile, action, npmUpdateTarget]
|
|
279
289
|
// 记录实际执行的命令(npm 通道显示包名,git 通道显示 HTTPS 源),
|
|
280
290
|
// 失败提 Issue 时作为「已尝试的安装方式」随附
|
|
281
291
|
const executed = isGlobalNpm
|
|
282
292
|
? `npm install -g ${(globalNpm ?? []).join(' ')}`
|
|
283
|
-
: `dsh plugin --profile ${profile} ${action} ${
|
|
293
|
+
: `dsh plugin --profile ${profile} ${action} ${npmUpdateTarget}`
|
|
284
294
|
if (task && !task.attempts.includes(executed)) {
|
|
285
295
|
task.attempts.push(executed)
|
|
286
296
|
}
|
|
@@ -105,6 +105,18 @@ export function appendLog(profile: string, entry: LogEntry): void {
|
|
|
105
105
|
}
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
+
/** 清空系统日志:把当前日志文件截断为空。失败返回 false(不阻断调用方)。 */
|
|
109
|
+
export function clearLog(profile: string): boolean {
|
|
110
|
+
try {
|
|
111
|
+
const file = logFilePath(profile)
|
|
112
|
+
mkdirSync(dirname(file), { recursive: true })
|
|
113
|
+
writeFileSync(file, '')
|
|
114
|
+
return true
|
|
115
|
+
} catch {
|
|
116
|
+
return false
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
108
120
|
/** 兼容旧格式:早期无 category(level 用 info/ok/fail/warn),归一化到新版。 */
|
|
109
121
|
function normalize(raw: unknown): LogEntry | null {
|
|
110
122
|
if (typeof raw !== 'object' || raw === null) return null
|