preflight-mcp 0.1.0

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,71 @@
1
+ import { Job } from '../core/scheduler.js';
2
+ import { getConfig } from '../config.js';
3
+ import { listBundles, updateBundle } from '../bundle/service.js';
4
+ import { readManifest } from '../bundle/manifest.js';
5
+ import { logger } from '../logging/logger.js';
6
+ export class BundleAutoUpdateJob extends Job {
7
+ getName() {
8
+ return 'BundleAutoUpdateJob';
9
+ }
10
+ getMaxRetries() {
11
+ return 2; // 更新任务重试次数较少
12
+ }
13
+ getRetryDelay() {
14
+ return 5000; // 5秒重试延迟
15
+ }
16
+ async run() {
17
+ const cfg = getConfig();
18
+ const effectiveDir = cfg.storageDirs[0]; // 使用主存储路径
19
+ const maxAgeHours = 24; // 24小时未更新的 bundle 需要检查
20
+ try {
21
+ const bundleIds = await listBundles(effectiveDir);
22
+ const results = [];
23
+ let updated = 0;
24
+ let failed = 0;
25
+ logger.info(`Checking ${bundleIds.length} bundles for updates`);
26
+ for (const bundleId of bundleIds) {
27
+ try {
28
+ const manifestPath = `${effectiveDir}/${bundleId}/manifest.json`;
29
+ const manifest = await readManifest(manifestPath);
30
+ const updatedAt = new Date(manifest.updatedAt).getTime();
31
+ const ageMs = Date.now() - updatedAt;
32
+ const ageHours = ageMs / (1000 * 60 * 60);
33
+ if (ageHours > maxAgeHours) {
34
+ logger.debug(`Bundle ${bundleId} is ${ageHours.toFixed(1)}h old, checking for updates`);
35
+ const { changed, summary } = await updateBundle(cfg, bundleId, { force: false });
36
+ if (changed) {
37
+ updated++;
38
+ logger.info(`Bundle ${bundleId} updated successfully`);
39
+ }
40
+ else {
41
+ logger.debug(`Bundle ${bundleId} is up to date`);
42
+ }
43
+ results.push({ bundleId, success: true });
44
+ }
45
+ else {
46
+ logger.debug(`Bundle ${bundleId} is recent (${ageHours.toFixed(1)}h old), skipping`);
47
+ results.push({ bundleId, success: true });
48
+ }
49
+ }
50
+ catch (error) {
51
+ failed++;
52
+ const errorMsg = error instanceof Error ? error.message : String(error);
53
+ logger.error(`Failed to update bundle ${bundleId}`, error instanceof Error ? error : undefined);
54
+ results.push({ bundleId, success: false, error: errorMsg });
55
+ }
56
+ }
57
+ const result = {
58
+ updated,
59
+ failed,
60
+ total: bundleIds.length,
61
+ details: results
62
+ };
63
+ logger.info(`BundleAutoUpdate completed`, { updated, failed, total: bundleIds.length });
64
+ return result;
65
+ }
66
+ catch (error) {
67
+ logger.error('Failed to list bundles', error instanceof Error ? error : undefined);
68
+ throw error;
69
+ }
70
+ }
71
+ }
@@ -0,0 +1,172 @@
1
+ import { Job } from '../core/scheduler.js';
2
+ import { getConfig } from '../config.js';
3
+ import { listBundles, bundleExists } from '../bundle/service.js';
4
+ import { readManifest } from '../bundle/manifest.js';
5
+ import { PreflightScheduler } from '../core/scheduler.js';
6
+ import { logger } from '../logging/logger.js';
7
+ import fs from 'node:fs/promises';
8
+ import path from 'node:path';
9
+ export class HealthCheckJob extends Job {
10
+ getName() {
11
+ return 'HealthCheckJob';
12
+ }
13
+ getMaxRetries() {
14
+ return 3;
15
+ }
16
+ getRetryDelay() {
17
+ return 2000; // 2秒重试延迟
18
+ }
19
+ async run() {
20
+ logger.debug('Starting system health check');
21
+ const cfg = getConfig();
22
+ const result = {
23
+ status: 'healthy',
24
+ bundles: {
25
+ total: 0,
26
+ healthy: 0,
27
+ corrupted: 0,
28
+ outdated: 0
29
+ },
30
+ storage: {
31
+ totalPaths: cfg.storageDirs.length,
32
+ accessiblePaths: 0,
33
+ totalSpace: 0,
34
+ freeSpace: 0
35
+ },
36
+ scheduler: {
37
+ totalJobs: 0,
38
+ runningJobs: 0,
39
+ failedJobs: 0
40
+ },
41
+ timestamp: new Date().toISOString()
42
+ };
43
+ try {
44
+ // 检查存储路径健康状态
45
+ await this.checkStorageHealth(cfg, result);
46
+ // 检查 bundles 健康状态
47
+ if (result.storage.accessiblePaths > 0) {
48
+ await this.checkBundlesHealth(cfg, result);
49
+ }
50
+ // 检查调度器健康状态
51
+ this.checkSchedulerHealth(result);
52
+ this.determineOverallHealth(result);
53
+ logger.info(`Health check completed: ${result.status}`, {
54
+ bundles: `${result.bundles.healthy}/${result.bundles.total}`,
55
+ storage: `${result.storage.accessiblePaths}/${result.storage.totalPaths}`,
56
+ jobs: result.scheduler.totalJobs
57
+ });
58
+ return result;
59
+ }
60
+ catch (error) {
61
+ logger.error('Health check failed', error instanceof Error ? error : undefined);
62
+ result.status = 'error';
63
+ return result;
64
+ }
65
+ }
66
+ async checkStorageHealth(cfg, result) {
67
+ for (const storageDir of cfg.storageDirs) {
68
+ try {
69
+ // 检查路径是否可访问
70
+ await fs.access(storageDir);
71
+ result.storage.accessiblePaths++;
72
+ // 获取磁盘空间信息(简化版本,仅适用于某些系统)
73
+ try {
74
+ const stats = await fs.stat(storageDir);
75
+ result.storage.totalSpace += 0;
76
+ result.storage.freeSpace += 0;
77
+ }
78
+ catch (spaceError) {
79
+ logger.warn(`Cannot get disk space info for ${storageDir}`);
80
+ }
81
+ logger.debug(`Storage path accessible: ${storageDir}`);
82
+ }
83
+ catch (error) {
84
+ logger.warn(`Storage path not accessible: ${storageDir}`);
85
+ }
86
+ }
87
+ }
88
+ async checkBundlesHealth(cfg, result) {
89
+ const effectiveDir = cfg.storageDirs[0]; // 使用第一个可访问的路径
90
+ const maxAgeHours = 72; // 3天未更新认为是过期的
91
+ const maxAgeMs = maxAgeHours * 60 * 60 * 1000;
92
+ try {
93
+ const bundleIds = await listBundles(effectiveDir);
94
+ result.bundles.total = bundleIds.length;
95
+ for (const bundleId of bundleIds) {
96
+ try {
97
+ // 检查 bundle 是否存在
98
+ const exists = await bundleExists(effectiveDir, bundleId);
99
+ if (!exists) {
100
+ result.bundles.corrupted++;
101
+ logger.warn(`Bundle ${bundleId} missing from filesystem`);
102
+ continue;
103
+ }
104
+ // 检查 manifest 是否可读
105
+ const manifestPath = path.join(effectiveDir, bundleId, 'manifest.json');
106
+ try {
107
+ const manifest = await readManifest(manifestPath);
108
+ // 检查是否过期
109
+ const updatedAt = new Date(manifest.updatedAt).getTime();
110
+ const ageMs = Date.now() - updatedAt;
111
+ if (ageMs > maxAgeMs) {
112
+ result.bundles.outdated++;
113
+ }
114
+ result.bundles.healthy++;
115
+ }
116
+ catch (manifestError) {
117
+ result.bundles.corrupted++;
118
+ logger.warn(`Bundle ${bundleId} has corrupted manifest`);
119
+ }
120
+ }
121
+ catch (error) {
122
+ result.bundles.corrupted++;
123
+ logger.warn(`Bundle ${bundleId} health check failed`);
124
+ }
125
+ }
126
+ }
127
+ catch (error) {
128
+ logger.error('Failed to list bundles for health check', error instanceof Error ? error : undefined);
129
+ }
130
+ }
131
+ checkSchedulerHealth(result) {
132
+ try {
133
+ const jobsStatus = PreflightScheduler.getAllJobsStatus();
134
+ result.scheduler.totalJobs = Object.keys(jobsStatus).length;
135
+ for (const [jobName, status] of Object.entries(jobsStatus)) {
136
+ if (status.scheduled) {
137
+ result.scheduler.runningJobs++;
138
+ }
139
+ if (status.lastError && status.retries > 0) {
140
+ result.scheduler.failedJobs++;
141
+ }
142
+ }
143
+ }
144
+ catch (error) {
145
+ logger.error('Failed to check scheduler health', error instanceof Error ? error : undefined);
146
+ }
147
+ }
148
+ determineOverallHealth(result) {
149
+ // 如果有任何存储路径不可访问,状态为 error
150
+ if (result.storage.accessiblePaths < result.storage.totalPaths) {
151
+ result.status = 'error';
152
+ return;
153
+ }
154
+ // 如果有任何 bundles 损坏,状态为 error
155
+ if (result.bundles.corrupted > 0) {
156
+ result.status = 'error';
157
+ return;
158
+ }
159
+ // 如果有过时的 bundles,状态为 warning
160
+ if (result.bundles.outdated > 0) {
161
+ result.status = 'warning';
162
+ return;
163
+ }
164
+ // 如果调度器有失败的任务,状态为 warning
165
+ if (result.scheduler.failedJobs > 0) {
166
+ result.status = 'warning';
167
+ return;
168
+ }
169
+ // 否则状态为 healthy
170
+ result.status = 'healthy';
171
+ }
172
+ }
@@ -0,0 +1,148 @@
1
+ import { Job } from '../core/scheduler.js';
2
+ import { getConfig } from '../config.js';
3
+ import { listBundles, bundleExists, clearBundleMulti } from '../bundle/service.js';
4
+ import { readManifest } from '../bundle/manifest.js';
5
+ import { logger } from '../logging/logger.js';
6
+ import fs from 'node:fs/promises';
7
+ import path from 'node:path';
8
+ export class StorageCleanupJob extends Job {
9
+ getName() {
10
+ return 'StorageCleanupJob';
11
+ }
12
+ getMaxRetries() {
13
+ return 1; // 清理任务通常不需要重试
14
+ }
15
+ getRetryDelay() {
16
+ return 10000; // 10秒重试延迟
17
+ }
18
+ async run() {
19
+ const cfg = getConfig();
20
+ const maxAgeDays = 30; // 30天未访问的 bundle 被认为是过期的
21
+ const maxAgeMs = maxAgeDays * 24 * 60 * 60 * 1000;
22
+ try {
23
+ const effectiveDir = cfg.storageDirs[0];
24
+ const bundleIds = await listBundles(effectiveDir);
25
+ let cleanedBundles = 0;
26
+ let freedSpace = 0;
27
+ const errors = [];
28
+ logger.info(`Scanning ${bundleIds.length} bundles for cleanup`, { maxAgeDays });
29
+ for (const bundleId of bundleIds) {
30
+ try {
31
+ const bundlePath = path.join(effectiveDir, bundleId);
32
+ const exists = await bundleExists(effectiveDir, bundleId);
33
+ if (!exists) {
34
+ logger.warn(`Bundle ${bundleId} not found, skipping`);
35
+ continue;
36
+ }
37
+ // 检查最后访问时间
38
+ const manifestPath = path.join(bundlePath, 'manifest.json');
39
+ const manifest = await readManifest(manifestPath);
40
+ const lastAccess = new Date(manifest.updatedAt).getTime();
41
+ const ageMs = Date.now() - lastAccess;
42
+ if (ageMs > maxAgeMs) {
43
+ const size = await this.calculateDirectorySize(bundlePath);
44
+ const ageDays = Math.floor(ageMs / (24 * 60 * 60 * 1000));
45
+ logger.info(`Removing old bundle ${bundleId}`, { sizeMB: (size / 1024 / 1024).toFixed(2), ageDays });
46
+ const deleted = await clearBundleMulti(cfg.storageDirs, bundleId);
47
+ if (deleted) {
48
+ cleanedBundles++;
49
+ freedSpace += size;
50
+ }
51
+ else {
52
+ errors.push(`Failed to delete bundle ${bundleId}`);
53
+ }
54
+ }
55
+ else {
56
+ const ageDays = Math.floor(ageMs / (24 * 60 * 60 * 1000));
57
+ logger.debug(`Bundle ${bundleId} is recent, keeping`, { ageDays });
58
+ }
59
+ }
60
+ catch (error) {
61
+ const errorMsg = error instanceof Error ? error.message : String(error);
62
+ errors.push(`Error processing bundle ${bundleId}: ${errorMsg}`);
63
+ logger.error(`Error processing bundle ${bundleId}`, error instanceof Error ? error : undefined);
64
+ }
65
+ }
66
+ try {
67
+ const tmpFreed = await this.cleanupTempFiles();
68
+ freedSpace += tmpFreed;
69
+ logger.debug(`Cleaned up temporary files`, { freedMB: (tmpFreed / 1024 / 1024).toFixed(2) });
70
+ }
71
+ catch (error) {
72
+ const errorMsg = error instanceof Error ? error.message : String(error);
73
+ errors.push(`Temp file cleanup failed: ${errorMsg}`);
74
+ }
75
+ const result = {
76
+ cleanedBundles,
77
+ freedSpace,
78
+ errors,
79
+ maxAgeDays,
80
+ totalBundles: bundleIds.length
81
+ };
82
+ logger.info('Storage cleanup completed', { cleanedBundles, freedMB: (freedSpace / 1024 / 1024).toFixed(2), errors: errors.length });
83
+ return result;
84
+ }
85
+ catch (error) {
86
+ logger.error('Failed to cleanup storage', error instanceof Error ? error : undefined);
87
+ throw error;
88
+ }
89
+ }
90
+ async calculateDirectorySize(dirPath) {
91
+ let totalSize = 0;
92
+ try {
93
+ const entries = await fs.readdir(dirPath, { withFileTypes: true });
94
+ for (const entry of entries) {
95
+ const fullPath = path.join(dirPath, entry.name);
96
+ if (entry.isDirectory()) {
97
+ totalSize += await this.calculateDirectorySize(fullPath);
98
+ }
99
+ else if (entry.isFile()) {
100
+ const stats = await fs.stat(fullPath);
101
+ totalSize += stats.size;
102
+ }
103
+ }
104
+ }
105
+ catch (error) {
106
+ logger.debug(`Cannot calculate size for ${dirPath}`);
107
+ }
108
+ return totalSize;
109
+ }
110
+ async cleanupTempFiles() {
111
+ const cfg = getConfig();
112
+ const tmpDir = cfg.tmpDir;
113
+ let freedSpace = 0;
114
+ try {
115
+ // 检查临时目录是否存在
116
+ try {
117
+ await fs.access(tmpDir);
118
+ }
119
+ catch {
120
+ // 临时目录不存在,无需清理
121
+ return 0;
122
+ }
123
+ const entries = await fs.readdir(tmpDir, { withFileTypes: true });
124
+ const now = Date.now();
125
+ const maxAgeMs = 24 * 60 * 60 * 1000; // 24小时
126
+ for (const entry of entries) {
127
+ const fullPath = path.join(tmpDir, entry.name);
128
+ try {
129
+ const stats = await fs.stat(fullPath);
130
+ const ageMs = now - stats.mtime.getTime();
131
+ if (ageMs > maxAgeMs) {
132
+ const size = stats.size;
133
+ await fs.rm(fullPath, { recursive: true, force: true });
134
+ freedSpace += size;
135
+ logger.debug(`Removed old temp file/dir: ${entry.name}`);
136
+ }
137
+ }
138
+ catch (error) {
139
+ logger.debug(`Failed to remove temp file ${entry.name}`);
140
+ }
141
+ }
142
+ }
143
+ catch (error) {
144
+ logger.debug('Failed to cleanup temp files');
145
+ }
146
+ return freedSpace;
147
+ }
148
+ }