@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,539 @@
1
+ /**
2
+ * dsh-plugin-guardian · 核心引擎模块 (guardian-core.js)
3
+ *
4
+ * 职责:
5
+ * 1. 探测与读取 profile 状态(bundles、dependencies、cordis.patch.yml)
6
+ * 2. 快照自动维护与持久化(.plugin-guardian/snapshots/)
7
+ * 3. 事故检测(对比当前 bundles 与 latest-healthy 快照)
8
+ * 4. 原子安全写回 package.json(支持排除坏插件的一键恢复)
9
+ * 5. 扫描并挽回官方 sanitizeProfile 产生的 cordis.patch.yml.bak-*
10
+ * 6. 生成免依赖的脱机应急脚本(restore-plugins.command)
11
+ */
12
+
13
+ import fs from 'node:fs';
14
+ import path from 'node:path';
15
+ import os from 'node:os';
16
+
17
+ export const GUARDIAN_BUNDLE_NAME = '@supertiny99/dsh-plugin-guardian';
18
+ export const LEGACY_GUARDIAN_NAME = 'dsh-plugin-guardian';
19
+ export const OFFICIAL_PREFIX = '@deepseek-ai/';
20
+
21
+ /**
22
+ * 确定当前 profile 目录
23
+ */
24
+ export function resolveProfileDir(customDir) {
25
+ if (customDir && fs.existsSync(customDir)) return path.resolve(customDir);
26
+ if (process.env.DSH_PROFILE_DIR && fs.existsSync(process.env.DSH_PROFILE_DIR)) {
27
+ return path.resolve(process.env.DSH_PROFILE_DIR);
28
+ }
29
+ // 默认回退至 desktop profile
30
+ return path.join(os.homedir(), '.dsh', 'profiles', 'desktop');
31
+ }
32
+
33
+ /**
34
+ * 判断是否为第三方 bundle
35
+ */
36
+ export function isThirdPartyBundle(bundleName) {
37
+ if (!bundleName) return false;
38
+ return !bundleName.startsWith(OFFICIAL_PREFIX);
39
+ }
40
+
41
+ /**
42
+ * 获取快照存储目录,若不存在则递归创建
43
+ */
44
+ export function ensureSnapshotDir(profileDir) {
45
+ const dir = path.join(profileDir, '.plugin-guardian', 'snapshots');
46
+ if (!fs.existsSync(dir)) {
47
+ fs.mkdirSync(dir, { recursive: true, mode: 0o755 });
48
+ }
49
+ return dir;
50
+ }
51
+
52
+ /**
53
+ * 读取 profile 的 package.json
54
+ */
55
+ export function readProfileManifest(profileDir) {
56
+ const pkgPath = path.join(profileDir, 'package.json');
57
+ if (!fs.existsSync(pkgPath)) {
58
+ throw new Error(`Profile package.json not found: ${pkgPath}`);
59
+ }
60
+ const content = fs.readFileSync(pkgPath, 'utf8');
61
+ return JSON.parse(content);
62
+ }
63
+
64
+ /**
65
+ * 安全写入 package.json(临时文件 + rename 原子写)
66
+ */
67
+ export function writeProfileManifestAtomic(profileDir, manifest) {
68
+ const pkgPath = path.join(profileDir, 'package.json');
69
+ const tmpPath = path.join(profileDir, `package.json.tmp-${Date.now()}-${process.pid}`);
70
+ const content = JSON.stringify(manifest, null, 2) + '\n';
71
+ fs.writeFileSync(tmpPath, content, { encoding: 'utf8', mode: 0o644 });
72
+ fs.renameSync(tmpPath, pkgPath);
73
+ }
74
+
75
+ /**
76
+ * 读取当前的 cordis.patch.yml
77
+ */
78
+ export function readPatchContent(profileDir) {
79
+ const patchPath = path.join(profileDir, 'cordis.patch.yml');
80
+ if (!fs.existsSync(patchPath)) return null;
81
+ try {
82
+ return fs.readFileSync(patchPath, 'utf8');
83
+ } catch {
84
+ return null;
85
+ }
86
+ }
87
+
88
+ /**
89
+ * 扫描目录下的 cordis.patch.yml.bak-* 备份文件
90
+ */
91
+ export function scanPatchBackups(profileDir) {
92
+ try {
93
+ const files = fs.readdirSync(profileDir);
94
+ const bakFiles = files
95
+ .filter((f) => f.startsWith('cordis.patch.yml.bak'))
96
+ .map((f) => {
97
+ const fullPath = path.join(profileDir, f);
98
+ const stat = fs.statSync(fullPath);
99
+ return {
100
+ fileName: f,
101
+ path: fullPath,
102
+ size: stat.size,
103
+ mtimeMs: stat.mtimeMs,
104
+ mtimeString: stat.mtime.toLocaleString(),
105
+ };
106
+ })
107
+ .sort((a, b) => b.mtimeMs - a.mtimeMs);
108
+ return bakFiles;
109
+ } catch {
110
+ return [];
111
+ }
112
+ }
113
+
114
+ /**
115
+ * 创建新快照
116
+ */
117
+ export function createSnapshot(profileDir, reason = 'manual') {
118
+ const snapshotDir = ensureSnapshotDir(profileDir);
119
+ const manifest = readProfileManifest(profileDir);
120
+ const bundles = manifest.dsh?.profile?.bundles || [];
121
+ const thirdPartyBundles = bundles.filter(isThirdPartyBundle);
122
+ const patchContent = readPatchContent(profileDir);
123
+ const now = Date.now();
124
+
125
+ const snapshot = {
126
+ id: `snap-${now}`,
127
+ timestamp: now,
128
+ timeString: new Date(now).toLocaleString(),
129
+ reason,
130
+ bundles: [...bundles],
131
+ thirdPartyBundles: [...thirdPartyBundles],
132
+ dependencies: { ...(manifest.dependencies || {}) },
133
+ patchContent: patchContent || '',
134
+ hasPatch: !!patchContent,
135
+ totalBundles: bundles.length,
136
+ };
137
+
138
+ const snapPath = path.join(snapshotDir, `${snapshot.id}.json`);
139
+ fs.writeFileSync(snapPath, JSON.stringify(snapshot, null, 2), 'utf8');
140
+
141
+ // 仅在以下情况更新 latest-healthy.json:
142
+ // 1. 当前含有第三方插件,且
143
+ // 2. 是用户主动打快照(manual),或者尚未存在健康快照,或者当前第三方插件数不少于历史健康快照
144
+ // 避免在故障排查/排除部分插件期间意外冲掉原有的完整全量健康快照
145
+ const existingHealthy = getLatestHealthySnapshot(profileDir);
146
+ const shouldUpdateHealthy =
147
+ thirdPartyBundles.length > 0 &&
148
+ (reason === 'manual' ||
149
+ !existingHealthy ||
150
+ thirdPartyBundles.length >= (existingHealthy.thirdPartyBundles?.length || 0));
151
+
152
+ if (shouldUpdateHealthy) {
153
+ const healthyPath = path.join(snapshotDir, 'latest-healthy.json');
154
+ fs.writeFileSync(healthyPath, JSON.stringify(snapshot, null, 2), 'utf8');
155
+ }
156
+
157
+ // 轮转保留最近 20 份快照
158
+ rotateSnapshots(snapshotDir, 20);
159
+
160
+ return snapshot;
161
+ }
162
+
163
+ /**
164
+ * 轮转删除过旧快照
165
+ */
166
+ function rotateSnapshots(snapshotDir, maxRetain = 20) {
167
+ try {
168
+ const files = fs.readdirSync(snapshotDir);
169
+ const snaps = files
170
+ .filter((f) => f.startsWith('snap-') && f.endsWith('.json'))
171
+ .sort()
172
+ .reverse();
173
+ if (snaps.length > maxRetain) {
174
+ for (const oldSnap of snaps.slice(maxRetain)) {
175
+ try {
176
+ fs.unlinkSync(path.join(snapshotDir, oldSnap));
177
+ } catch {}
178
+ }
179
+ }
180
+ } catch {}
181
+ }
182
+
183
+ /**
184
+ * 获取快照列表与存储占用统计
185
+ */
186
+ export function listSnapshots(profileDir) {
187
+ const snapshotDir = ensureSnapshotDir(profileDir);
188
+ try {
189
+ const files = fs.readdirSync(snapshotDir);
190
+ const list = [];
191
+ let totalBytes = 0;
192
+
193
+ for (const f of files) {
194
+ if (f.startsWith('snap-') && f.endsWith('.json')) {
195
+ try {
196
+ const fullPath = path.join(snapshotDir, f);
197
+ const stat = fs.statSync(fullPath);
198
+ totalBytes += stat.size;
199
+ const data = JSON.parse(fs.readFileSync(fullPath, 'utf8'));
200
+ list.push({
201
+ id: data.id,
202
+ timestamp: data.timestamp,
203
+ timeString: data.timeString,
204
+ reason: data.reason,
205
+ size: stat.size,
206
+ sizeText: (stat.size / 1024).toFixed(1) + ' KB',
207
+ thirdPartyCount: data.thirdPartyBundles?.length || 0,
208
+ totalBundles: data.totalBundles || data.bundles?.length || 0,
209
+ thirdPartyBundles: data.thirdPartyBundles || [],
210
+ });
211
+ } catch {}
212
+ }
213
+ }
214
+
215
+ const sorted = list.sort((a, b) => b.timestamp - a.timestamp);
216
+ return {
217
+ snapshots: sorted,
218
+ totalBytes,
219
+ totalSizeText: (totalBytes / 1024).toFixed(1) + ' KB',
220
+ };
221
+ } catch {
222
+ return { snapshots: [], totalBytes: 0, totalSizeText: '0 KB' };
223
+ }
224
+ }
225
+
226
+ /**
227
+ * 手动删除指定快照
228
+ */
229
+ export function deleteSnapshot(profileDir, snapId) {
230
+ if (!snapId || !snapId.startsWith('snap-')) {
231
+ throw new Error('Invalid snapshot id');
232
+ }
233
+ const snapshotDir = ensureSnapshotDir(profileDir);
234
+ const filePath = path.join(snapshotDir, `${snapId}.json`);
235
+ if (fs.existsSync(filePath)) {
236
+ fs.unlinkSync(filePath);
237
+ return { ok: true, deleted: snapId };
238
+ }
239
+ return { ok: false, error: 'Snapshot not found' };
240
+ }
241
+
242
+ /**
243
+ * 一键清理多余旧快照(保留最新的 keepCount 份,默认保留 2 份)
244
+ */
245
+ export function cleanOldSnapshots(profileDir, keepCount = 2) {
246
+ const snapshotDir = ensureSnapshotDir(profileDir);
247
+ const { snapshots } = listSnapshots(profileDir);
248
+ let deletedCount = 0;
249
+
250
+ if (snapshots.length > keepCount) {
251
+ const toDelete = snapshots.slice(keepCount);
252
+ for (const item of toDelete) {
253
+ try {
254
+ const p = path.join(snapshotDir, `${item.id}.json`);
255
+ if (fs.existsSync(p)) {
256
+ fs.unlinkSync(p);
257
+ deletedCount++;
258
+ }
259
+ } catch {}
260
+ }
261
+ }
262
+ return { ok: true, deletedCount, retained: Math.min(snapshots.length, keepCount) };
263
+ }
264
+
265
+ /**
266
+ * 读取最近一份健康快照
267
+ */
268
+ export function getLatestHealthySnapshot(profileDir) {
269
+ const snapshotDir = ensureSnapshotDir(profileDir);
270
+ const healthyPath = path.join(snapshotDir, 'latest-healthy.json');
271
+ if (fs.existsSync(healthyPath)) {
272
+ try {
273
+ return JSON.parse(fs.readFileSync(healthyPath, 'utf8'));
274
+ } catch {}
275
+ }
276
+ // 若无 latest-healthy,取最近一份有第三方的 snap
277
+ const { snapshots: all } = listSnapshots(profileDir);
278
+ for (const item of all) {
279
+ if (item.thirdPartyCount > 0) {
280
+ try {
281
+ const full = JSON.parse(fs.readFileSync(path.join(snapshotDir, `${item.id}.json`), 'utf8'));
282
+ return full;
283
+ } catch {}
284
+ }
285
+ }
286
+ return null;
287
+ }
288
+
289
+ /**
290
+ * 获取插件管家完整状态(前端主接口)
291
+ */
292
+ export function getGuardianStatus(profileDir) {
293
+ const manifest = readProfileManifest(profileDir);
294
+ const currentBundles = manifest.dsh?.profile?.bundles || [];
295
+ const currentThirdParty = currentBundles.filter(isThirdPartyBundle);
296
+ const dependencies = manifest.dependencies || {};
297
+ const allInstalledThirdParty = Object.keys(dependencies).filter(isThirdPartyBundle);
298
+
299
+ const latestHealthy = getLatestHealthySnapshot(profileDir);
300
+ const { snapshots, totalBytes, totalSizeText } = listSnapshots(profileDir);
301
+ const bakFiles = scanPatchBackups(profileDir);
302
+ const currentPatch = readPatchContent(profileDir);
303
+
304
+ // 事故检测:
305
+ // 1. 如果有 latest-healthy 记录过第三方插件,但当前第三方插件数量为 0 或显著减少
306
+ // 2. 并且存在快照中的第三方插件在当前 bundles 缺失
307
+ let isIncident = false;
308
+ let disabledFromHealthy = [];
309
+
310
+ if (latestHealthy && latestHealthy.thirdPartyBundles?.length > 0) {
311
+ const healthyList = latestHealthy.thirdPartyBundles;
312
+ disabledFromHealthy = healthyList.filter((b) => !currentBundles.includes(b));
313
+ if (disabledFromHealthy.length > 0 && currentThirdParty.length <= 1) {
314
+ isIncident = true;
315
+ }
316
+ }
317
+
318
+ // 另外计算:dependencies 里已安装,但当前 bundles 未启用的插件
319
+ const inactiveInstalled = allInstalledThirdParty.filter((pkg) => !currentBundles.includes(pkg));
320
+
321
+ // 配置缺失检测:当前 patch 几乎为空,但有 bak 文件存在
322
+ const currentPatchTrimmed = (currentPatch || '').trim();
323
+ const patchNeedsRecovery = (currentPatchTrimmed === '' || currentPatchTrimmed === '[]') && bakFiles.length > 0;
324
+
325
+ return {
326
+ profileDir,
327
+ currentBundles,
328
+ currentThirdParty,
329
+ allInstalledThirdParty,
330
+ inactiveInstalled,
331
+ isIncident,
332
+ disabledFromHealthy,
333
+ latestHealthy: latestHealthy
334
+ ? {
335
+ id: latestHealthy.id,
336
+ timestamp: latestHealthy.timestamp,
337
+ timeString: latestHealthy.timeString,
338
+ reason: latestHealthy.reason,
339
+ thirdPartyBundles: latestHealthy.thirdPartyBundles,
340
+ }
341
+ : null,
342
+ snapshotsCount: snapshots.length,
343
+ snapshotsTotalSize: totalSizeText,
344
+ snapshotsTotalBytes: totalBytes,
345
+ recentSnapshots: snapshots.slice(0, 10),
346
+ bakFiles,
347
+ patchNeedsRecovery,
348
+ currentPatchSize: currentPatch ? currentPatch.length : 0,
349
+ };
350
+ }
351
+
352
+ /**
353
+ * 执行恢复:将选中的 bundles 写回 package.json
354
+ * @param {string} profileDir
355
+ * @param {string[]} selectedBundles 用户选中的第三方插件
356
+ * @param {boolean} restorePatch 是否同步恢复最新的 patch.bak
357
+ */
358
+ export function restoreBundles(profileDir, selectedBundles, restorePatchFlag = false) {
359
+ if (!Array.isArray(selectedBundles)) {
360
+ throw new Error('selectedBundles must be an array');
361
+ }
362
+
363
+ const manifest = readProfileManifest(profileDir);
364
+ const existingBundles = manifest.dsh?.profile?.bundles || [];
365
+
366
+ // 保留所有的官方 bundles
367
+ const officialBundles = existingBundles.filter((b) => !isThirdPartyBundle(b));
368
+
369
+ // 确保 GUARDIAN_BUNDLE_NAME 也保留在恢复列表中
370
+ const thirdPartySet = new Set(selectedBundles);
371
+ thirdPartySet.add(GUARDIAN_BUNDLE_NAME);
372
+
373
+ const newBundles = [...officialBundles, ...Array.from(thirdPartySet)];
374
+
375
+ manifest.dsh = manifest.dsh || {};
376
+ manifest.dsh.profile = manifest.dsh.profile || {};
377
+ manifest.dsh.profile.bundles = newBundles;
378
+
379
+ writeProfileManifestAtomic(profileDir, manifest);
380
+
381
+ let patchRestored = null;
382
+ if (restorePatchFlag) {
383
+ const bak = scanPatchBackups(profileDir)[0];
384
+ if (bak) {
385
+ restorePatch(profileDir, bak.fileName);
386
+ patchRestored = bak.fileName;
387
+ }
388
+ }
389
+
390
+ // 恢复后立即创建一份新快照
391
+ createSnapshot(profileDir, 'after_recovery');
392
+
393
+ return {
394
+ ok: true,
395
+ restoredBundles: newBundles,
396
+ thirdPartyRestored: Array.from(thirdPartySet),
397
+ patchRestored,
398
+ needRestart: true,
399
+ };
400
+ }
401
+
402
+ /**
403
+ * 挽回指定的 cordis.patch.yml.bak 文件
404
+ */
405
+ export function restorePatch(profileDir, bakFileName) {
406
+ const bakPath = path.join(profileDir, bakFileName);
407
+ if (!fs.existsSync(bakPath)) {
408
+ throw new Error(`Backup patch file not found: ${bakPath}`);
409
+ }
410
+ const content = fs.readFileSync(bakPath, 'utf8');
411
+ const targetPath = path.join(profileDir, 'cordis.patch.yml');
412
+ const tmpPath = path.join(profileDir, `cordis.patch.yml.tmp-${Date.now()}`);
413
+
414
+ fs.writeFileSync(tmpPath, content, { encoding: 'utf8', mode: 0o644 });
415
+ fs.renameSync(tmpPath, targetPath);
416
+
417
+ return { ok: true, restoredFile: bakFileName, size: content.length };
418
+ }
419
+
420
+ /**
421
+ * 生成脱机一键应急恢复脚本
422
+ * 跨平台支持:Windows 生成 .bat,macOS 生成 .command,Linux 生成 .sh
423
+ */
424
+ export function generateOfflineScript(profileDir, outputDir = null) {
425
+ const targetDir = outputDir || profileDir;
426
+ const isWin = process.platform === 'win32';
427
+ const scriptFileName = isWin ? 'restore-plugins.bat' : 'restore-plugins.command';
428
+ const scriptPath = path.join(targetDir, scriptFileName);
429
+
430
+ let scriptContent = '';
431
+
432
+ if (isWin) {
433
+ // Windows 批处理脚本 (.bat)
434
+ scriptContent = `@echo off
435
+ chcp 65001 >nul
436
+ echo ==================================================
437
+ echo DSH 插件管家 · 脱机应急恢复 (Windows)
438
+ echo ==================================================
439
+
440
+ set "PROFILE_DIR=${profileDir.replace(/\//g, '\\')}"
441
+ set "SNAPSHOT_FILE=%PROFILE_DIR%\\.plugin-guardian\\snapshots\\latest-healthy.json"
442
+ set "PKG_FILE=%PROFILE_DIR%\\package.json"
443
+ set "PATCH_FILE=%PROFILE_DIR%\\cordis.patch.yml"
444
+
445
+ if not exist "%SNAPSHOT_FILE%" (
446
+ echo [X] 错误: 未找到最近的健康快照: %SNAPSHOT_FILE%
447
+ echo 请检查快照文件是否存在。
448
+ pause
449
+ exit /b 1
450
+ )
451
+
452
+ node -e "const fs = require('fs'); const path = require('path'); const pDir = process.env.PROFILE_DIR; const sFile = process.env.SNAPSHOT_FILE; const pkgFile = process.env.PKG_FILE; const patchFile = process.env.PATCH_FILE; const snap = JSON.parse(fs.readFileSync(sFile, 'utf8')); const pkg = JSON.parse(fs.readFileSync(pkgFile, 'utf8')); pkg.dsh = pkg.dsh || {}; pkg.dsh.profile = pkg.dsh.profile || {}; pkg.dsh.profile.bundles = snap.bundles; fs.writeFileSync(pkgFile, JSON.stringify(pkg, null, 2) + '\\n'); console.log('[OK] 已成功恢复 ' + snap.bundles.length + ' 个插件组合行至 package.json'); try { const baks = fs.readdirSync(pDir).filter(f => f.startsWith('cordis.patch.yml.bak')).sort().reverse(); if (baks.length > 0) { fs.copyFileSync(path.join(pDir, baks[0]), patchFile); console.log('[OK] 已恢复配置: ' + baks[0] + ' -> cordis.patch.yml'); } } catch (e) {}"
453
+
454
+ if %errorlevel% equ 0 (
455
+ echo --------------------------------------------------
456
+ echo [OK] 恢复大功告成!
457
+ echo 现在请启动/重新打开 DeepSeek Harness 桌面应用即可生效。
458
+ ) else (
459
+ echo [X] 恢复遇到错误,请检查上述提示。
460
+ )
461
+
462
+ echo ==================================================
463
+ echo 按任意键退出窗口...
464
+ pause >nul
465
+ `;
466
+ } else {
467
+ // macOS / Linux 脚本 (.command / .sh)
468
+ scriptContent = `#!/bin/bash
469
+ # ==============================================================================
470
+ # DSH 插件管家 · 脱机应急恢复脚本
471
+ # 双击此脚本,即可脱机一键恢复上次健康的插件激活列表与配置
472
+ # ==============================================================================
473
+
474
+ PROFILE_DIR="${profileDir}"
475
+ SNAPSHOT_FILE="$PROFILE_DIR/.plugin-guardian/snapshots/latest-healthy.json"
476
+ PKG_FILE="$PROFILE_DIR/package.json"
477
+ PATCH_FILE="$PROFILE_DIR/cordis.patch.yml"
478
+
479
+ echo "=================================================="
480
+ echo " DSH 插件管家 · 脱机应急恢复"
481
+ echo "=================================================="
482
+
483
+ if [ ! -f "$SNAPSHOT_FILE" ]; then
484
+ echo "❌ 错误: 未找到最近的健康快照: $SNAPSHOT_FILE"
485
+ echo "按任意键退出..."
486
+ read -n 1
487
+ exit 1
488
+ fi
489
+
490
+ node -e "
491
+ const fs = require('fs');
492
+ const snap = JSON.parse(fs.readFileSync('$SNAPSHOT_FILE', 'utf8'));
493
+ const pkg = JSON.parse(fs.readFileSync('$PKG_FILE', 'utf8'));
494
+
495
+ const healthyBundles = snap.bundles;
496
+ if (!healthyBundles || !healthyBundles.length) {
497
+ console.error('❌ 快照中没有 bundle 数据');
498
+ process.exit(1);
499
+ }
500
+
501
+ pkg.dsh = pkg.dsh || {};
502
+ pkg.dsh.profile = pkg.dsh.profile || {};
503
+ pkg.dsh.profile.bundles = healthyBundles;
504
+
505
+ fs.writeFileSync('$PKG_FILE', JSON.stringify(pkg, null, 2) + '\\n');
506
+ console.log('✅ 已成功恢复 ' + healthyBundles.length + ' 个插件组合行至 package.json');
507
+
508
+ // 恢复 patch.bak(若存在)
509
+ try {
510
+ const files = fs.readdirSync('$PROFILE_DIR');
511
+ const baks = files.filter(f => f.startsWith('cordis.patch.yml.bak')).sort().reverse();
512
+ if (baks.length > 0) {
513
+ const latestBak = baks[0];
514
+ fs.copyFileSync('$PROFILE_DIR/' + latestBak, '$PATCH_FILE');
515
+ console.log('✅ 已恢复配置: ' + latestBak + ' -> cordis.patch.yml');
516
+ }
517
+ } catch (e) {
518
+ console.warn('⚠️ 检查 patch.bak 略过:', e.message);
519
+ }
520
+ "
521
+
522
+ if [ $? -eq 0 ]; then
523
+ echo "--------------------------------------------------"
524
+ echo "🎉 恢复大功告成!"
525
+ echo "现在请启动/重新打开 DeepSeek Harness 桌面应用即可生效。"
526
+ osascript -e 'display notification "已成功恢复上次健康的插件激活列表与配置!" with title "DSH 插件管家"' 2>/dev/null || true
527
+ else
528
+ echo "❌ 恢复过程中遇到错误,请检查上述日志。"
529
+ fi
530
+
531
+ echo "=================================================="
532
+ echo "按任意键退出窗口..."
533
+ read -n 1
534
+ `;
535
+ }
536
+
537
+ fs.writeFileSync(scriptPath, scriptContent, { encoding: 'utf8', mode: 0o755 });
538
+ return { scriptPath, fileName: scriptFileName, ok: true, platform: process.platform };
539
+ }