@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.
@@ -0,0 +1,268 @@
1
+ /* plugin-guardian · 样式表 (token 化,明暗自适应) */
2
+
3
+ .pg-container {
4
+ display: flex;
5
+ flex-direction: column;
6
+ height: 100%;
7
+ width: 100%;
8
+ background: var(--dsw-alias-surface-overlay, #ffffff);
9
+ color: var(--dsw-alias-text-primary, #1e293b);
10
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
11
+ box-sizing: border-box;
12
+ overflow-y: auto;
13
+ }
14
+
15
+ .pg-header {
16
+ display: flex;
17
+ align-items: center;
18
+ justify-content: space-between;
19
+ padding: 20px 28px;
20
+ border-bottom: 1px solid var(--dsw-alias-border-subtle, rgba(0, 0, 0, 0.08));
21
+ background: var(--dsw-alias-surface-raised, rgba(0, 0, 0, 0.02));
22
+ }
23
+
24
+ .pg-header-left {
25
+ display: flex;
26
+ align-items: center;
27
+ gap: 14px;
28
+ }
29
+
30
+ .pg-icon-badge {
31
+ width: 42px;
32
+ height: 42px;
33
+ border-radius: 10px;
34
+ background: var(--dsw-alias-accent-subtle, rgba(59, 130, 246, 0.12));
35
+ display: flex;
36
+ align-items: center;
37
+ justify-content: center;
38
+ font-size: 22px;
39
+ }
40
+
41
+ .pg-title-wrap h2 {
42
+ margin: 0;
43
+ font-size: 18px;
44
+ font-weight: 600;
45
+ color: var(--dsw-alias-text-primary, #0f172a);
46
+ }
47
+
48
+ .pg-title-wrap p {
49
+ margin: 3px 0 0 0;
50
+ font-size: 12px;
51
+ color: var(--dsw-alias-text-tertiary, #64748b);
52
+ }
53
+
54
+ .pg-actions {
55
+ display: flex;
56
+ align-items: center;
57
+ gap: 10px;
58
+ }
59
+
60
+ .pg-btn {
61
+ display: inline-flex;
62
+ align-items: center;
63
+ gap: 6px;
64
+ padding: 7px 14px;
65
+ font-size: 13px;
66
+ font-weight: 500;
67
+ border-radius: 6px;
68
+ border: 1px solid var(--dsw-alias-border-subtle, rgba(0, 0, 0, 0.15));
69
+ background: var(--dsw-alias-control-bg-neutral, #ffffff);
70
+ color: var(--dsw-alias-text-primary, #1e293b);
71
+ cursor: pointer;
72
+ transition: background-color 0.15s ease, border-color 0.15s ease;
73
+ }
74
+
75
+ .pg-btn:hover {
76
+ background: var(--dsw-alias-control-bg-neutral-hover, rgba(0, 0, 0, 0.05));
77
+ border-color: var(--dsw-alias-border-strong, rgba(0, 0, 0, 0.3));
78
+ }
79
+
80
+ .pg-btn:disabled {
81
+ opacity: 0.5;
82
+ cursor: not-allowed;
83
+ }
84
+
85
+ .pg-btn-primary {
86
+ background: var(--dsw-alias-accent-primary, #2563eb);
87
+ color: #ffffff;
88
+ border-color: transparent;
89
+ }
90
+
91
+ .pg-btn-primary:hover {
92
+ background: var(--dsw-alias-accent-hover, #1d4ed8);
93
+ }
94
+
95
+ .pg-btn-danger {
96
+ background: var(--dsw-alias-intent-danger, #ef4444);
97
+ color: #ffffff;
98
+ border-color: transparent;
99
+ }
100
+
101
+ .pg-content {
102
+ padding: 24px 28px;
103
+ display: flex;
104
+ flex-direction: column;
105
+ gap: 20px;
106
+ }
107
+
108
+ /* 事故告警横幅 (Incident Banner) */
109
+ .pg-incident-banner {
110
+ background: var(--dsw-alias-surface-sunken, rgba(239, 68, 68, 0.08));
111
+ border: 1px solid var(--dsw-alias-intent-danger, rgba(239, 68, 68, 0.4));
112
+ border-radius: 10px;
113
+ padding: 18px 22px;
114
+ display: flex;
115
+ flex-direction: column;
116
+ gap: 14px;
117
+ }
118
+
119
+ .pg-incident-header {
120
+ display: flex;
121
+ align-items: center;
122
+ gap: 12px;
123
+ }
124
+
125
+ .pg-incident-title {
126
+ font-size: 15px;
127
+ font-weight: 600;
128
+ color: var(--dsw-alias-intent-danger, #b91c1c);
129
+ }
130
+
131
+ .pg-incident-desc {
132
+ font-size: 13px;
133
+ color: var(--dsw-alias-text-secondary, #475569);
134
+ line-height: 1.5;
135
+ }
136
+
137
+ .pg-plugin-selection-box {
138
+ background: var(--dsw-alias-surface-overlay, #ffffff);
139
+ border: 1px solid var(--dsw-alias-border-subtle, rgba(0, 0, 0, 0.1));
140
+ border-radius: 8px;
141
+ padding: 12px 16px;
142
+ max-height: 240px;
143
+ overflow-y: auto;
144
+ display: flex;
145
+ flex-direction: column;
146
+ gap: 8px;
147
+ }
148
+
149
+ .pg-plugin-item {
150
+ display: flex;
151
+ align-items: center;
152
+ justify-content: space-between;
153
+ padding: 6px 8px;
154
+ border-radius: 6px;
155
+ cursor: pointer;
156
+ }
157
+
158
+ .pg-plugin-item:hover {
159
+ background: var(--dsw-alias-control-bg-neutral-hover, rgba(0, 0, 0, 0.04));
160
+ }
161
+
162
+ .pg-plugin-item-left {
163
+ display: flex;
164
+ align-items: center;
165
+ gap: 10px;
166
+ }
167
+
168
+ .pg-plugin-name {
169
+ font-size: 13px;
170
+ font-weight: 500;
171
+ color: var(--dsw-alias-text-primary, #0f172a);
172
+ }
173
+
174
+ .pg-plugin-tag {
175
+ font-size: 11px;
176
+ padding: 2px 6px;
177
+ border-radius: 4px;
178
+ background: var(--dsw-alias-accent-subtle, rgba(59, 130, 246, 0.1));
179
+ color: var(--dsw-alias-accent-primary, #2563eb);
180
+ }
181
+
182
+ .pg-card {
183
+ background: var(--dsw-alias-surface-raised, rgba(0, 0, 0, 0.02));
184
+ border: 1px solid var(--dsw-alias-border-subtle, rgba(0, 0, 0, 0.08));
185
+ border-radius: 10px;
186
+ padding: 18px 22px;
187
+ display: flex;
188
+ flex-direction: column;
189
+ gap: 12px;
190
+ }
191
+
192
+ .pg-card-title {
193
+ font-size: 14px;
194
+ font-weight: 600;
195
+ color: var(--dsw-alias-text-primary, #1e293b);
196
+ display: flex;
197
+ align-items: center;
198
+ justify-content: space-between;
199
+ }
200
+
201
+ .pg-dot {
202
+ display: inline-block;
203
+ width: 6px;
204
+ height: 6px;
205
+ border-radius: 50%;
206
+ background: var(--dsw-alias-intent-success, #16a34a);
207
+ }
208
+
209
+ .pg-incident-icon {
210
+ color: var(--dsw-alias-intent-danger, #ef4444);
211
+ display: flex;
212
+ align-items: center;
213
+ justify-content: center;
214
+ }
215
+
216
+ .pg-badge-healthy {
217
+ display: inline-flex;
218
+ align-items: center;
219
+ gap: 6px;
220
+ font-size: 12px;
221
+ font-weight: 500;
222
+ color: var(--dsw-alias-intent-success, #16a34a);
223
+ background: rgba(22, 163, 74, 0.1);
224
+ padding: 3px 10px;
225
+ border-radius: 12px;
226
+ }
227
+
228
+ .pg-list {
229
+ display: flex;
230
+ flex-direction: column;
231
+ gap: 6px;
232
+ }
233
+
234
+ .pg-row {
235
+ display: flex;
236
+ align-items: center;
237
+ justify-content: space-between;
238
+ padding: 8px 12px;
239
+ background: var(--dsw-alias-surface-overlay, #ffffff);
240
+ border: 1px solid var(--dsw-alias-border-subtle, rgba(0, 0, 0, 0.06));
241
+ border-radius: 6px;
242
+ font-size: 13px;
243
+ }
244
+
245
+ .pg-row-name {
246
+ font-weight: 500;
247
+ color: var(--dsw-alias-text-primary, #1e293b);
248
+ }
249
+
250
+ .pg-row-meta {
251
+ font-size: 12px;
252
+ color: var(--dsw-alias-text-tertiary, #94a3b8);
253
+ }
254
+
255
+ .pg-toast {
256
+ position: fixed;
257
+ bottom: 24px;
258
+ right: 28px;
259
+ background: var(--dsw-alias-text-primary, #0f172a);
260
+ color: var(--dsw-alias-surface-overlay, #ffffff);
261
+ padding: 10px 18px;
262
+ border-radius: 8px;
263
+ font-size: 13px;
264
+ font-weight: 500;
265
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
266
+ z-index: 9999;
267
+ transition: opacity 0.2s ease;
268
+ }
package/src/index.js ADDED
@@ -0,0 +1,152 @@
1
+ /**
2
+ * dsh-plugin-guardian · Host 侧入口 (index.js)
3
+ *
4
+ * 职责:
5
+ * 1. 注入 webServer 注册 /plugin-guardian/* 路由
6
+ * 2. 启动时执行健康自动快照与应急脚本就绪
7
+ * 3. 响应前端关于状态、快照、恢复、配置挽回与脚本导出的请求
8
+ */
9
+
10
+ import {
11
+ resolveProfileDir,
12
+ getGuardianStatus,
13
+ createSnapshot,
14
+ listSnapshots,
15
+ deleteSnapshot,
16
+ cleanOldSnapshots,
17
+ restoreBundles,
18
+ restorePatch,
19
+ generateOfflineScript,
20
+ } from './guardian-core.js';
21
+ import path from 'node:path';
22
+ import os from 'node:os';
23
+ import { exec } from 'node:child_process';
24
+
25
+ function parseBody(req) {
26
+ return new Promise((resolve) => {
27
+ const chunks = [];
28
+ req.on('data', (c) => chunks.push(c));
29
+ req.on('end', () => {
30
+ if (chunks.length === 0) return resolve({});
31
+ try {
32
+ const str = Buffer.concat(chunks).toString('utf8');
33
+ resolve(JSON.parse(str));
34
+ } catch {
35
+ resolve({});
36
+ }
37
+ });
38
+ req.on('error', () => resolve({}));
39
+ });
40
+ }
41
+
42
+ export function apply(ctx, config) {
43
+ const profileDir = resolveProfileDir(config?.profileDir);
44
+
45
+ // 启动时在后台静默建立/更新一份启动快照,并保证脱机脚本就绪
46
+ try {
47
+ createSnapshot(profileDir, 'startup_auto');
48
+ generateOfflineScript(profileDir);
49
+ } catch (e) {
50
+ console.warn('[plugin-guardian] startup auto snapshot error:', e.message);
51
+ }
52
+
53
+ ctx.inject(['webServer'], (scopeCtx) => {
54
+ scopeCtx.effect?.(() =>
55
+ scopeCtx.webServer.register({
56
+ kind: 'prefix',
57
+ path: '/plugin-guardian',
58
+ handler: async (req, res) => {
59
+ const json = (code, value) => {
60
+ res.writeHead(code, {
61
+ 'content-type': 'application/json; charset=utf-8',
62
+ 'cache-control': 'no-store',
63
+ });
64
+ res.end(JSON.stringify(value));
65
+ };
66
+
67
+ try {
68
+ const url = new URL(req.url, 'http://127.0.0.1');
69
+ const pathname = url.pathname.replace(/^\/plugin-guardian/, '') || '/';
70
+
71
+ // 1. 获取管家整体状态与事故检测
72
+ if (pathname === '/status' && req.method === 'GET') {
73
+ const status = getGuardianStatus(profileDir);
74
+ return json(200, { ok: true, ...status });
75
+ }
76
+
77
+ // 2. 手动创建快照
78
+ if (pathname === '/snapshot' && req.method === 'POST') {
79
+ const body = await parseBody(req);
80
+ const snap = createSnapshot(profileDir, body.reason || 'manual');
81
+ return json(200, { ok: true, snapshot: snap });
82
+ }
83
+
84
+ // 3. 列出所有快照
85
+ if (pathname === '/snapshots' && req.method === 'GET') {
86
+ const result = listSnapshots(profileDir);
87
+ return json(200, { ok: true, ...result });
88
+ }
89
+
90
+ // 3.1 删除单个快照
91
+ if (pathname === '/delete-snapshot' && req.method === 'POST') {
92
+ const body = await parseBody(req);
93
+ if (!body.id) {
94
+ return json(400, { ok: false, error: 'Snapshot id is required' });
95
+ }
96
+ const res = deleteSnapshot(profileDir, body.id);
97
+ return json(res.ok ? 200 : 404, res);
98
+ }
99
+
100
+ // 3.2 一键清理多余旧快照(保留最新 2 份)
101
+ if (pathname === '/clean-snapshots' && req.method === 'POST') {
102
+ const body = await parseBody(req);
103
+ const res = cleanOldSnapshots(profileDir, body.keepCount || 2);
104
+ return json(200, res);
105
+ }
106
+
107
+ // 4. 执行插件恢复
108
+ if (pathname === '/restore' && req.method === 'POST') {
109
+ const body = await parseBody(req);
110
+ const selected = body.bundles;
111
+ if (!Array.isArray(selected)) {
112
+ return json(400, { ok: false, error: 'bundles array is required' });
113
+ }
114
+ const result = restoreBundles(profileDir, selected, !!body.restorePatch);
115
+ return json(200, result);
116
+ }
117
+
118
+ // 5. 挽回 patch.bak 配置
119
+ if (pathname === '/restore-patch' && req.method === 'POST') {
120
+ const body = await parseBody(req);
121
+ const status = getGuardianStatus(profileDir);
122
+ const targetBak = body.fileName || status.bakFiles?.[0]?.fileName;
123
+ if (!targetBak) {
124
+ return json(400, { ok: false, error: 'No bak file found to restore' });
125
+ }
126
+ const result = restorePatch(profileDir, targetBak);
127
+ return json(200, result);
128
+ }
129
+
130
+ // 6. 导出脱机应急脚本到桌面
131
+ if (pathname === '/export-script' && req.method === 'POST') {
132
+ const desktopDir = path.join(os.homedir(), 'Desktop');
133
+ const result = generateOfflineScript(profileDir, desktopDir);
134
+ try {
135
+ if (process.platform === 'darwin') {
136
+ exec(`open -R "${result.scriptPath}"`);
137
+ } else if (process.platform === 'win32') {
138
+ exec(`explorer.exe /select,"${result.scriptPath.replace(/\//g, '\\')}"`);
139
+ }
140
+ } catch {}
141
+ return json(200, { ok: true, target: result.scriptPath, fileName: result.fileName });
142
+ }
143
+
144
+ return json(404, { ok: false, error: 'Not Found' });
145
+ } catch (err) {
146
+ return json(500, { ok: false, error: err.message });
147
+ }
148
+ },
149
+ })
150
+ );
151
+ });
152
+ }