@supertiny99/dsh-plugin-guardian 0.0.0-stage → 0.1.1
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/LICENSE +21 -0
- package/README.md +74 -2
- package/cordis.patch.yml +4 -0
- package/dist/client.js +271 -0
- package/dist/meta.json +46 -0
- package/package.json +66 -5
- package/src/client.jsx +653 -0
- package/src/guardian-core.js +539 -0
- package/src/guardian.css +268 -0
- package/src/index.js +152 -0
package/src/client.jsx
ADDED
|
@@ -0,0 +1,653 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-plugin-guardian · Client 侧入口 (client.jsx)
|
|
3
|
+
*
|
|
4
|
+
* 全面去除 emoji,统一使用内联矢量 SVG 图标(Feather 规范,currentColor 继承)。
|
|
5
|
+
* 挂载:
|
|
6
|
+
* 1. sidebar.panellist (order 70): 盾牌图标,点击切换到 main 面板
|
|
7
|
+
* 2. main: 整页面板 key: 'plugin-guardian'
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import React, { useState, useEffect, useCallback } from 'react';
|
|
11
|
+
import guardianCss from './guardian.css';
|
|
12
|
+
|
|
13
|
+
/* ---------- 纯矢量 SVG 图标集 (无 emoji) ---------- */
|
|
14
|
+
|
|
15
|
+
const SvgIcon = ({ children, size = 15, className = '' }) => (
|
|
16
|
+
<svg
|
|
17
|
+
width={size}
|
|
18
|
+
height={size}
|
|
19
|
+
viewBox="0 0 24 24"
|
|
20
|
+
fill="none"
|
|
21
|
+
stroke="currentColor"
|
|
22
|
+
strokeWidth="2"
|
|
23
|
+
strokeLinecap="round"
|
|
24
|
+
strokeLinejoin="round"
|
|
25
|
+
className={className}
|
|
26
|
+
aria-hidden="true"
|
|
27
|
+
style={{ flexShrink: 0 }}
|
|
28
|
+
>
|
|
29
|
+
{children}
|
|
30
|
+
</svg>
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
const IconShield = ({ size = 16 }) => (
|
|
34
|
+
<SvgIcon size={size}>
|
|
35
|
+
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" />
|
|
36
|
+
</SvgIcon>
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
const IconSave = ({ size = 14 }) => (
|
|
40
|
+
<SvgIcon size={size}>
|
|
41
|
+
<path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z" />
|
|
42
|
+
<polyline points="17 21 17 13 7 13 7 21" />
|
|
43
|
+
<polyline points="7 3 7 8 15 8" />
|
|
44
|
+
</SvgIcon>
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
const IconTerminal = ({ size = 14 }) => (
|
|
48
|
+
<SvgIcon size={size}>
|
|
49
|
+
<polyline points="4 17 10 11 4 5" />
|
|
50
|
+
<line x1="12" y1="19" x2="20" y2="19" />
|
|
51
|
+
</SvgIcon>
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
const IconRefresh = ({ size = 14 }) => (
|
|
55
|
+
<SvgIcon size={size}>
|
|
56
|
+
<polyline points="23 4 23 10 17 10" />
|
|
57
|
+
<path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10" />
|
|
58
|
+
</SvgIcon>
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
const IconAlert = ({ size = 18 }) => (
|
|
62
|
+
<SvgIcon size={size}>
|
|
63
|
+
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z" />
|
|
64
|
+
<line x1="12" y1="9" x2="12" y2="13" />
|
|
65
|
+
<line x1="12" y1="17" x2="12.01" y2="17" />
|
|
66
|
+
</SvgIcon>
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
const IconRestoreAll = ({ size = 14 }) => (
|
|
70
|
+
<SvgIcon size={size}>
|
|
71
|
+
<polyline points="1 4 1 10 7 10" />
|
|
72
|
+
<path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10" />
|
|
73
|
+
</SvgIcon>
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
const IconCheck = ({ size = 14 }) => (
|
|
77
|
+
<SvgIcon size={size}>
|
|
78
|
+
<polyline points="20 6 9 17 4 12" />
|
|
79
|
+
</SvgIcon>
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
const IconPackage = ({ size = 15 }) => (
|
|
83
|
+
<SvgIcon size={size}>
|
|
84
|
+
<line x1="16.5" y1="9.4" x2="7.5" y2="4.21" />
|
|
85
|
+
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z" />
|
|
86
|
+
<polyline points="3.27 6.96 12 12.01 20.73 6.96" />
|
|
87
|
+
<line x1="12" y1="22.08" x2="12" y2="12" />
|
|
88
|
+
</SvgIcon>
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
const IconFile = ({ size = 15 }) => (
|
|
92
|
+
<SvgIcon size={size}>
|
|
93
|
+
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
|
|
94
|
+
<polyline points="14 2 14 8 20 8" />
|
|
95
|
+
<line x1="16" y1="13" x2="8" y2="13" />
|
|
96
|
+
<line x1="16" y1="17" x2="8" y2="17" />
|
|
97
|
+
</SvgIcon>
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
const IconClock = ({ size = 14 }) => (
|
|
101
|
+
<SvgIcon size={size}>
|
|
102
|
+
<circle cx="12" cy="12" r="10" />
|
|
103
|
+
<polyline points="12 6 12 12 16 14" />
|
|
104
|
+
</SvgIcon>
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
const IconTrash = ({ size = 14 }) => (
|
|
108
|
+
<SvgIcon size={size}>
|
|
109
|
+
<polyline points="3 6 5 6 21 6" />
|
|
110
|
+
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
|
|
111
|
+
</SvgIcon>
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
const IconPower = ({ size = 14 }) => (
|
|
115
|
+
<SvgIcon size={size}>
|
|
116
|
+
<path d="M18.36 6.64a9 9 0 1 1-12.73 0" />
|
|
117
|
+
<line x1="12" y1="2" x2="12" y2="12" />
|
|
118
|
+
</SvgIcon>
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
/* ---------- 主面板组件 ---------- */
|
|
122
|
+
|
|
123
|
+
function GuardianPanel() {
|
|
124
|
+
const [status, setStatus] = useState(null);
|
|
125
|
+
const [loading, setLoading] = useState(true);
|
|
126
|
+
const [selectedBundles, setSelectedBundles] = useState(new Set());
|
|
127
|
+
const [restorePatchFlag, setRestorePatchFlag] = useState(true);
|
|
128
|
+
const [toastMsg, setToastMsg] = useState(null);
|
|
129
|
+
const [busy, setBusy] = useState(false);
|
|
130
|
+
|
|
131
|
+
const showToast = (msg) => {
|
|
132
|
+
setToastMsg(msg);
|
|
133
|
+
setTimeout(() => setToastMsg(null), 3500);
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const fetchStatus = useCallback(async () => {
|
|
137
|
+
try {
|
|
138
|
+
setLoading(true);
|
|
139
|
+
const res = await fetch('/plugin-guardian/status');
|
|
140
|
+
if (res.ok) {
|
|
141
|
+
const data = await res.json();
|
|
142
|
+
setStatus(data);
|
|
143
|
+
if (data.disabledFromHealthy && data.disabledFromHealthy.length > 0) {
|
|
144
|
+
setSelectedBundles(new Set(data.disabledFromHealthy));
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
} catch (err) {
|
|
148
|
+
console.error('Failed to fetch guardian status:', err);
|
|
149
|
+
} finally {
|
|
150
|
+
setLoading(false);
|
|
151
|
+
}
|
|
152
|
+
}, []);
|
|
153
|
+
|
|
154
|
+
useEffect(() => {
|
|
155
|
+
fetchStatus();
|
|
156
|
+
}, [fetchStatus]);
|
|
157
|
+
|
|
158
|
+
const toggleBundle = (b) => {
|
|
159
|
+
const next = new Set(selectedBundles);
|
|
160
|
+
if (next.has(b)) next.delete(b);
|
|
161
|
+
else next.add(b);
|
|
162
|
+
setSelectedBundles(next);
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const handleRestore = async (bundlesToRestore, withPatch) => {
|
|
166
|
+
if (bundlesToRestore.length === 0) {
|
|
167
|
+
showToast('未选择任何需要恢复的插件');
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
try {
|
|
171
|
+
setBusy(true);
|
|
172
|
+
const res = await fetch('/plugin-guardian/restore', {
|
|
173
|
+
method: 'POST',
|
|
174
|
+
headers: { 'content-type': 'application/json' },
|
|
175
|
+
body: JSON.stringify({
|
|
176
|
+
bundles: bundlesToRestore,
|
|
177
|
+
restorePatch: withPatch,
|
|
178
|
+
}),
|
|
179
|
+
});
|
|
180
|
+
const result = await res.json();
|
|
181
|
+
if (result.ok) {
|
|
182
|
+
showToast('恢复写入成功!请完全退出并重启 DSH 生效');
|
|
183
|
+
await fetchStatus();
|
|
184
|
+
} else {
|
|
185
|
+
showToast('恢复失败: ' + (result.error || '未知错误'));
|
|
186
|
+
}
|
|
187
|
+
} catch (err) {
|
|
188
|
+
showToast('请求失败: ' + err.message);
|
|
189
|
+
} finally {
|
|
190
|
+
setBusy(false);
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
const handleCreateSnapshot = async () => {
|
|
195
|
+
try {
|
|
196
|
+
setBusy(true);
|
|
197
|
+
const res = await fetch('/plugin-guardian/snapshot', {
|
|
198
|
+
method: 'POST',
|
|
199
|
+
headers: { 'content-type': 'application/json' },
|
|
200
|
+
body: JSON.stringify({ reason: 'manual' }),
|
|
201
|
+
});
|
|
202
|
+
const data = await res.json();
|
|
203
|
+
if (data.ok) {
|
|
204
|
+
showToast('已成功创建当前健康快照!');
|
|
205
|
+
await fetchStatus();
|
|
206
|
+
}
|
|
207
|
+
} catch (err) {
|
|
208
|
+
showToast('创建快照失败: ' + err.message);
|
|
209
|
+
} finally {
|
|
210
|
+
setBusy(false);
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
const handleRestorePatch = async (fileName) => {
|
|
215
|
+
try {
|
|
216
|
+
setBusy(true);
|
|
217
|
+
const res = await fetch('/plugin-guardian/restore-patch', {
|
|
218
|
+
method: 'POST',
|
|
219
|
+
headers: { 'content-type': 'application/json' },
|
|
220
|
+
body: JSON.stringify({ fileName }),
|
|
221
|
+
});
|
|
222
|
+
const data = await res.json();
|
|
223
|
+
if (data.ok) {
|
|
224
|
+
showToast('配置已成功挽回!');
|
|
225
|
+
await fetchStatus();
|
|
226
|
+
} else {
|
|
227
|
+
showToast('挽回失败: ' + data.error);
|
|
228
|
+
}
|
|
229
|
+
} catch (err) {
|
|
230
|
+
showToast('请求失败: ' + err.message);
|
|
231
|
+
} finally {
|
|
232
|
+
setBusy(false);
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
const handleExportScript = async () => {
|
|
237
|
+
try {
|
|
238
|
+
setBusy(true);
|
|
239
|
+
const res = await fetch('/plugin-guardian/export-script', {
|
|
240
|
+
method: 'POST',
|
|
241
|
+
});
|
|
242
|
+
const data = await res.json();
|
|
243
|
+
if (data.ok) {
|
|
244
|
+
showToast(`应急脚本已生成至桌面 (${data.fileName}),系统已定位该文件`);
|
|
245
|
+
}
|
|
246
|
+
} catch (err) {
|
|
247
|
+
showToast('导出失败: ' + err.message);
|
|
248
|
+
} finally {
|
|
249
|
+
setBusy(false);
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
// 删除指定快照
|
|
254
|
+
const handleDeleteSnapshot = async (id) => {
|
|
255
|
+
try {
|
|
256
|
+
setBusy(true);
|
|
257
|
+
const res = await fetch('/plugin-guardian/delete-snapshot', {
|
|
258
|
+
method: 'POST',
|
|
259
|
+
headers: { 'content-type': 'application/json' },
|
|
260
|
+
body: JSON.stringify({ id }),
|
|
261
|
+
});
|
|
262
|
+
const data = await res.json();
|
|
263
|
+
if (data.ok) {
|
|
264
|
+
showToast('已删除快照 ' + id);
|
|
265
|
+
await fetchStatus();
|
|
266
|
+
} else {
|
|
267
|
+
showToast('删除失败: ' + data.error);
|
|
268
|
+
}
|
|
269
|
+
} catch (err) {
|
|
270
|
+
showToast('请求失败: ' + err.message);
|
|
271
|
+
} finally {
|
|
272
|
+
setBusy(false);
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
// 一键清理多余旧快照(保留最新 2 份)
|
|
277
|
+
const handleCleanSnapshots = async () => {
|
|
278
|
+
try {
|
|
279
|
+
setBusy(true);
|
|
280
|
+
const res = await fetch('/plugin-guardian/clean-snapshots', {
|
|
281
|
+
method: 'POST',
|
|
282
|
+
headers: { 'content-type': 'application/json' },
|
|
283
|
+
body: JSON.stringify({ keepCount: 2 }),
|
|
284
|
+
});
|
|
285
|
+
const data = await res.json();
|
|
286
|
+
if (data.ok) {
|
|
287
|
+
showToast(`已清理 ${data.deletedCount} 份旧快照,保留了最新 ${data.retained} 份`);
|
|
288
|
+
await fetchStatus();
|
|
289
|
+
}
|
|
290
|
+
} catch (err) {
|
|
291
|
+
showToast('清理失败: ' + err.message);
|
|
292
|
+
} finally {
|
|
293
|
+
setBusy(false);
|
|
294
|
+
}
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
if (loading && !status) {
|
|
298
|
+
return (
|
|
299
|
+
<div className="pg-container">
|
|
300
|
+
<style>{guardianCss}</style>
|
|
301
|
+
<div style={{ padding: 32, textAlign: 'center', color: 'var(--dsw-alias-text-tertiary)' }}>
|
|
302
|
+
正在读取插件状态与快照...
|
|
303
|
+
</div>
|
|
304
|
+
</div>
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const isIncident = status?.isIncident;
|
|
309
|
+
const disabledList = status?.disabledFromHealthy || [];
|
|
310
|
+
const currentThird = status?.currentThirdParty || [];
|
|
311
|
+
const inactiveInstalled = status?.inactiveInstalled || [];
|
|
312
|
+
|
|
313
|
+
// 计算快照总占用(若 Host 尚未冷启动,前端自动根据快照数与 patch 大小准确估算)
|
|
314
|
+
const displayTotalSize = (() => {
|
|
315
|
+
if (status?.snapshotsTotalSize && status.snapshotsTotalSize !== '0 KB') {
|
|
316
|
+
return status.snapshotsTotalSize;
|
|
317
|
+
}
|
|
318
|
+
const count = status?.snapshotsCount || 0;
|
|
319
|
+
if (count === 0) return '0 KB';
|
|
320
|
+
const perSnapBytes = (status?.currentPatchSize || 0) + 1500;
|
|
321
|
+
return ((count * perSnapBytes) / 1024).toFixed(1) + ' KB';
|
|
322
|
+
})();
|
|
323
|
+
|
|
324
|
+
return (
|
|
325
|
+
<div className="pg-container">
|
|
326
|
+
<style>{guardianCss}</style>
|
|
327
|
+
|
|
328
|
+
{/* 顶部标题栏 */}
|
|
329
|
+
<div className="pg-header">
|
|
330
|
+
<div className="pg-header-left">
|
|
331
|
+
<div className="pg-icon-badge">
|
|
332
|
+
<IconShield size={22} />
|
|
333
|
+
</div>
|
|
334
|
+
<div className="pg-title-wrap">
|
|
335
|
+
<h2>DSH 插件管家</h2>
|
|
336
|
+
<p>Profile 状态守护 · 自动快照 · 事故一键恢复 · 脱机应急修复</p>
|
|
337
|
+
</div>
|
|
338
|
+
</div>
|
|
339
|
+
<div className="pg-actions">
|
|
340
|
+
<button
|
|
341
|
+
className="pg-btn"
|
|
342
|
+
onClick={handleCreateSnapshot}
|
|
343
|
+
disabled={busy}
|
|
344
|
+
title="手动将当前所有开启的插件与配置记录为新快照"
|
|
345
|
+
>
|
|
346
|
+
<IconSave size={14} />
|
|
347
|
+
<span>保存快照</span>
|
|
348
|
+
</button>
|
|
349
|
+
<button
|
|
350
|
+
className="pg-btn"
|
|
351
|
+
onClick={handleExportScript}
|
|
352
|
+
disabled={busy}
|
|
353
|
+
title="在桌面生成一个双击即可恢复的脚本,DSH 闪退不开时可用"
|
|
354
|
+
>
|
|
355
|
+
<IconTerminal size={14} />
|
|
356
|
+
<span>导出桌面应急脚本</span>
|
|
357
|
+
</button>
|
|
358
|
+
<button className="pg-btn" onClick={fetchStatus} disabled={busy}>
|
|
359
|
+
<IconRefresh size={14} />
|
|
360
|
+
<span>刷新</span>
|
|
361
|
+
</button>
|
|
362
|
+
</div>
|
|
363
|
+
</div>
|
|
364
|
+
|
|
365
|
+
<div className="pg-content">
|
|
366
|
+
{/* 事故告警横幅 */}
|
|
367
|
+
{isIncident && (
|
|
368
|
+
<div className="pg-incident-banner">
|
|
369
|
+
<div className="pg-incident-header">
|
|
370
|
+
<div className="pg-incident-icon">
|
|
371
|
+
<IconAlert size={20} />
|
|
372
|
+
</div>
|
|
373
|
+
<div className="pg-incident-title">
|
|
374
|
+
检测到系统此前执行了全盘禁用,发现第三方插件被停用
|
|
375
|
+
</div>
|
|
376
|
+
</div>
|
|
377
|
+
<div className="pg-incident-desc">
|
|
378
|
+
检测到上一次健康快照中激活的 <strong>{disabledList.length}</strong> 个第三方插件当前已被宿主移出激活列表。
|
|
379
|
+
你可以直接一键全部恢复,或勾选排除刚才导致闪退的可疑插件:
|
|
380
|
+
</div>
|
|
381
|
+
|
|
382
|
+
{/* 插件勾选恢复列表 */}
|
|
383
|
+
<div className="pg-plugin-selection-box">
|
|
384
|
+
{disabledList.map((bundle) => {
|
|
385
|
+
const checked = selectedBundles.has(bundle);
|
|
386
|
+
return (
|
|
387
|
+
<div
|
|
388
|
+
key={bundle}
|
|
389
|
+
className="pg-plugin-item"
|
|
390
|
+
onClick={() => toggleBundle(bundle)}
|
|
391
|
+
>
|
|
392
|
+
<div className="pg-plugin-item-left">
|
|
393
|
+
<input
|
|
394
|
+
type="checkbox"
|
|
395
|
+
checked={checked}
|
|
396
|
+
onChange={() => {}}
|
|
397
|
+
style={{ cursor: 'pointer' }}
|
|
398
|
+
/>
|
|
399
|
+
<IconPackage size={14} />
|
|
400
|
+
<span className="pg-plugin-name">{bundle}</span>
|
|
401
|
+
</div>
|
|
402
|
+
<span className="pg-plugin-tag">待恢复</span>
|
|
403
|
+
</div>
|
|
404
|
+
);
|
|
405
|
+
})}
|
|
406
|
+
</div>
|
|
407
|
+
|
|
408
|
+
{/* patch 配置同步恢复选项 */}
|
|
409
|
+
{status?.patchNeedsRecovery && (
|
|
410
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 13 }}>
|
|
411
|
+
<input
|
|
412
|
+
type="checkbox"
|
|
413
|
+
id="restorePatchCheck"
|
|
414
|
+
checked={restorePatchFlag}
|
|
415
|
+
onChange={(e) => setRestorePatchFlag(e.target.checked)}
|
|
416
|
+
style={{ cursor: 'pointer' }}
|
|
417
|
+
/>
|
|
418
|
+
<label htmlFor="restorePatchCheck" style={{ cursor: 'pointer', color: 'var(--dsw-alias-text-primary)' }}>
|
|
419
|
+
同时从最新的 <code>cordis.patch.yml.bak</code> 中找回被清空的用户配置
|
|
420
|
+
</label>
|
|
421
|
+
</div>
|
|
422
|
+
)}
|
|
423
|
+
|
|
424
|
+
<div style={{ display: 'flex', gap: 10, marginTop: 4 }}>
|
|
425
|
+
<button
|
|
426
|
+
className="pg-btn pg-btn-primary"
|
|
427
|
+
onClick={() => handleRestore(disabledList, restorePatchFlag)}
|
|
428
|
+
disabled={busy}
|
|
429
|
+
>
|
|
430
|
+
<IconRestoreAll size={14} />
|
|
431
|
+
<span>一键全量恢复所有插件 ({disabledList.length})</span>
|
|
432
|
+
</button>
|
|
433
|
+
<button
|
|
434
|
+
className="pg-btn"
|
|
435
|
+
onClick={() => handleRestore(Array.from(selectedBundles), restorePatchFlag)}
|
|
436
|
+
disabled={busy || selectedBundles.size === 0}
|
|
437
|
+
>
|
|
438
|
+
<IconCheck size={14} />
|
|
439
|
+
<span>恢复勾选插件 ({selectedBundles.size})</span>
|
|
440
|
+
</button>
|
|
441
|
+
</div>
|
|
442
|
+
</div>
|
|
443
|
+
)}
|
|
444
|
+
|
|
445
|
+
{/* 状态总览卡片 */}
|
|
446
|
+
<div className="pg-card">
|
|
447
|
+
<div className="pg-card-title">
|
|
448
|
+
<span>当前 Profile 插件运行状态</span>
|
|
449
|
+
<span className="pg-badge-healthy">
|
|
450
|
+
<span className="pg-dot" />
|
|
451
|
+
{isIncident ? '处于恢复排查模式' : '健康运行中'}
|
|
452
|
+
</span>
|
|
453
|
+
</div>
|
|
454
|
+
<div style={{ fontSize: 13, color: 'var(--dsw-alias-text-secondary)', lineHeight: 1.6 }}>
|
|
455
|
+
当前共激活 <strong>{status?.currentBundles?.length || 0}</strong> 个组合行(含{' '}
|
|
456
|
+
<strong>{currentThird.length}</strong> 个第三方插件)。
|
|
457
|
+
{status?.latestHealthy ? (
|
|
458
|
+
<span> 最近健康快照建立于:<strong>{status.latestHealthy.timeString}</strong>。</span>
|
|
459
|
+
) : (
|
|
460
|
+
<span> 尚未建立健康基准快照。</span>
|
|
461
|
+
)}
|
|
462
|
+
</div>
|
|
463
|
+
|
|
464
|
+
<div className="pg-list" style={{ marginTop: 8 }}>
|
|
465
|
+
{currentThird.map((bundle) => (
|
|
466
|
+
<div key={bundle} className="pg-row">
|
|
467
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
|
468
|
+
<IconPackage size={15} />
|
|
469
|
+
<span className="pg-row-name">{bundle}</span>
|
|
470
|
+
</div>
|
|
471
|
+
<span className="pg-plugin-tag" style={{ background: 'rgba(22, 163, 74, 0.1)', color: '#16a34a' }}>
|
|
472
|
+
已激活
|
|
473
|
+
</span>
|
|
474
|
+
</div>
|
|
475
|
+
))}
|
|
476
|
+
{currentThird.length === 0 && (
|
|
477
|
+
<div style={{ padding: '12px 0', fontSize: 13, color: 'var(--dsw-alias-text-tertiary)' }}>
|
|
478
|
+
暂无已激活的第三方插件。
|
|
479
|
+
</div>
|
|
480
|
+
)}
|
|
481
|
+
</div>
|
|
482
|
+
</div>
|
|
483
|
+
|
|
484
|
+
{/* 已安装但未激活的插件 (如果有) */}
|
|
485
|
+
{inactiveInstalled.length > 0 && !isIncident && (
|
|
486
|
+
<div className="pg-card">
|
|
487
|
+
<div className="pg-card-title">
|
|
488
|
+
<span>已在 dependencies 安装、但未激活的第三方插件 ({inactiveInstalled.length})</span>
|
|
489
|
+
</div>
|
|
490
|
+
<div className="pg-list">
|
|
491
|
+
{inactiveInstalled.map((pkg) => (
|
|
492
|
+
<div key={pkg} className="pg-row">
|
|
493
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
|
494
|
+
<IconPower size={14} />
|
|
495
|
+
<span className="pg-row-name">{pkg}</span>
|
|
496
|
+
</div>
|
|
497
|
+
<button
|
|
498
|
+
className="pg-btn"
|
|
499
|
+
style={{ padding: '4px 10px', fontSize: 12 }}
|
|
500
|
+
onClick={() => handleRestore([...currentThird, pkg], false)}
|
|
501
|
+
disabled={busy}
|
|
502
|
+
>
|
|
503
|
+
一键激活此插件
|
|
504
|
+
</button>
|
|
505
|
+
</div>
|
|
506
|
+
))}
|
|
507
|
+
</div>
|
|
508
|
+
</div>
|
|
509
|
+
)}
|
|
510
|
+
|
|
511
|
+
{/* 用户配置 (.bak) 保护区 */}
|
|
512
|
+
{status?.bakFiles?.length > 0 && (
|
|
513
|
+
<div className="pg-card">
|
|
514
|
+
<div className="pg-card-title">
|
|
515
|
+
<span>cordis.patch.yml 配置备份历史 ({status.bakFiles.length} 份)</span>
|
|
516
|
+
</div>
|
|
517
|
+
<div style={{ fontSize: 12, color: 'var(--dsw-alias-text-tertiary)' }}>
|
|
518
|
+
当 DSH 发生崩溃恢复时,原配置会被重命名为 bak 保存。你可以随时对比并找回配置:
|
|
519
|
+
</div>
|
|
520
|
+
<div className="pg-list">
|
|
521
|
+
{status.bakFiles.slice(0, 3).map((bak) => (
|
|
522
|
+
<div key={bak.fileName} className="pg-row">
|
|
523
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
|
524
|
+
<IconFile size={16} />
|
|
525
|
+
<div>
|
|
526
|
+
<div className="pg-row-name">{bak.fileName}</div>
|
|
527
|
+
<div className="pg-row-meta">
|
|
528
|
+
时间: {bak.mtimeString} · 大小: {bak.size} 字节
|
|
529
|
+
</div>
|
|
530
|
+
</div>
|
|
531
|
+
</div>
|
|
532
|
+
<button
|
|
533
|
+
className="pg-btn"
|
|
534
|
+
style={{ padding: '4px 10px', fontSize: 12 }}
|
|
535
|
+
onClick={() => handleRestorePatch(bak.fileName)}
|
|
536
|
+
disabled={busy}
|
|
537
|
+
>
|
|
538
|
+
从备份覆盖还原配置
|
|
539
|
+
</button>
|
|
540
|
+
</div>
|
|
541
|
+
))}
|
|
542
|
+
</div>
|
|
543
|
+
</div>
|
|
544
|
+
)}
|
|
545
|
+
|
|
546
|
+
{/* 历史快照列表 */}
|
|
547
|
+
<div className="pg-card">
|
|
548
|
+
<div className="pg-card-title">
|
|
549
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
|
550
|
+
<span>历史健康快照 ({status?.snapshotsCount || 0})</span>
|
|
551
|
+
<span style={{ fontSize: 12, fontWeight: 'normal', color: 'var(--dsw-alias-text-tertiary)' }}>
|
|
552
|
+
占用磁盘: {displayTotalSize}
|
|
553
|
+
</span>
|
|
554
|
+
</div>
|
|
555
|
+
{status?.snapshotsCount > 2 && (
|
|
556
|
+
<button
|
|
557
|
+
className="pg-btn"
|
|
558
|
+
style={{ padding: '3px 8px', fontSize: 12 }}
|
|
559
|
+
onClick={handleCleanSnapshots}
|
|
560
|
+
disabled={busy}
|
|
561
|
+
title="仅保留最近 2 份快照,清理更早的历史记录"
|
|
562
|
+
>
|
|
563
|
+
<IconTrash size={12} />
|
|
564
|
+
<span>清理旧快照</span>
|
|
565
|
+
</button>
|
|
566
|
+
)}
|
|
567
|
+
</div>
|
|
568
|
+
<div className="pg-list">
|
|
569
|
+
{status?.recentSnapshots?.map((snap) => (
|
|
570
|
+
<div key={snap.id} className="pg-row">
|
|
571
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
|
572
|
+
<IconClock size={15} />
|
|
573
|
+
<div>
|
|
574
|
+
<div className="pg-row-name" style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
|
575
|
+
<span>{snap.timeString}</span>
|
|
576
|
+
<span style={{ fontSize: 11, color: 'var(--dsw-alias-text-tertiary)' }}>
|
|
577
|
+
({snap.sizeText || (((status?.currentPatchSize || 0) + 1500) / 1024).toFixed(1) + ' KB'})
|
|
578
|
+
</span>
|
|
579
|
+
</div>
|
|
580
|
+
<div className="pg-row-meta">
|
|
581
|
+
原因: {snap.reason === 'startup_auto' ? '启动自动快照' : snap.reason === 'manual' ? '手动快照' : snap.reason} · 第三方插件数: {snap.thirdPartyCount}
|
|
582
|
+
</div>
|
|
583
|
+
</div>
|
|
584
|
+
</div>
|
|
585
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
|
586
|
+
<button
|
|
587
|
+
className="pg-btn"
|
|
588
|
+
style={{ padding: '4px 10px', fontSize: 12 }}
|
|
589
|
+
onClick={() => handleRestore(snap.thirdPartyBundles, false)}
|
|
590
|
+
disabled={busy || snap.thirdPartyCount === 0}
|
|
591
|
+
title="将插件激活状态回滚为此快照"
|
|
592
|
+
>
|
|
593
|
+
恢复为此快照
|
|
594
|
+
</button>
|
|
595
|
+
<button
|
|
596
|
+
className="pg-btn"
|
|
597
|
+
style={{ padding: '4px 8px', fontSize: 12, color: 'var(--dsw-alias-intent-danger, #ef4444)' }}
|
|
598
|
+
onClick={() => handleDeleteSnapshot(snap.id)}
|
|
599
|
+
disabled={busy}
|
|
600
|
+
title="删除此份快照"
|
|
601
|
+
>
|
|
602
|
+
<IconTrash size={13} />
|
|
603
|
+
</button>
|
|
604
|
+
</div>
|
|
605
|
+
</div>
|
|
606
|
+
))}
|
|
607
|
+
</div>
|
|
608
|
+
</div>
|
|
609
|
+
</div>
|
|
610
|
+
|
|
611
|
+
{/* Toast 提示浮层 */}
|
|
612
|
+
{toastMsg && <div className="pg-toast">{toastMsg}</div>}
|
|
613
|
+
</div>
|
|
614
|
+
);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/* ---------- 侧栏入口图标 (18x18 矢量 SVG 盾牌) ---------- */
|
|
618
|
+
|
|
619
|
+
function PanelIcon({ size = 20, active = false }) {
|
|
620
|
+
return (
|
|
621
|
+
<svg
|
|
622
|
+
width={size}
|
|
623
|
+
height={size}
|
|
624
|
+
viewBox="0 0 24 24"
|
|
625
|
+
fill="none"
|
|
626
|
+
stroke="currentColor"
|
|
627
|
+
strokeWidth={active ? 2.2 : 1.6}
|
|
628
|
+
strokeLinecap="round"
|
|
629
|
+
strokeLinejoin="round"
|
|
630
|
+
aria-hidden="true"
|
|
631
|
+
>
|
|
632
|
+
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" />
|
|
633
|
+
</svg>
|
|
634
|
+
);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
export const inject = ['slots'];
|
|
638
|
+
|
|
639
|
+
export function apply(ctx) {
|
|
640
|
+
ctx.slots.inject('sidebar.panellist', () =>
|
|
641
|
+
ctx.slots.register(
|
|
642
|
+
{ name: 'sidebar.panellist', id: 'plugin-guardian', order: 70, label: '插件管家' },
|
|
643
|
+
PanelIcon
|
|
644
|
+
)
|
|
645
|
+
);
|
|
646
|
+
|
|
647
|
+
ctx.slots.inject('main', () =>
|
|
648
|
+
ctx.slots.register(
|
|
649
|
+
{ name: 'main', key: 'plugin-guardian' },
|
|
650
|
+
GuardianPanel
|
|
651
|
+
)
|
|
652
|
+
);
|
|
653
|
+
}
|