dsh-plugin 1.3.2 → 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 +635 -327
- package/client/client.js.map +1 -1
- package/lib/http/routes.js +22 -1
- package/lib/services/log.js +12 -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 +20 -1
- package/src/server/services/log.ts +12 -0
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',
|
|
@@ -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
|