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.
- package/README.md +208 -0
- package/README.zh-CN.md +406 -0
- package/dist/bundle/analysis.js +91 -0
- package/dist/bundle/context7.js +301 -0
- package/dist/bundle/deepwiki.js +206 -0
- package/dist/bundle/facts.js +296 -0
- package/dist/bundle/github.js +55 -0
- package/dist/bundle/guides.js +65 -0
- package/dist/bundle/ingest.js +152 -0
- package/dist/bundle/manifest.js +14 -0
- package/dist/bundle/overview.js +222 -0
- package/dist/bundle/paths.js +29 -0
- package/dist/bundle/service.js +803 -0
- package/dist/bundle/tagging.js +206 -0
- package/dist/config.js +65 -0
- package/dist/context7/client.js +30 -0
- package/dist/context7/tools.js +58 -0
- package/dist/core/scheduler.js +166 -0
- package/dist/errors.js +150 -0
- package/dist/index.js +7 -0
- package/dist/jobs/bundle-auto-update-job.js +71 -0
- package/dist/jobs/health-check-job.js +172 -0
- package/dist/jobs/storage-cleanup-job.js +148 -0
- package/dist/logging/logger.js +311 -0
- package/dist/mcp/uris.js +45 -0
- package/dist/search/sqliteFts.js +481 -0
- package/dist/server/optimized-server.js +255 -0
- package/dist/server.js +778 -0
- package/dist/storage/compression.js +249 -0
- package/dist/storage/storage-adapter.js +316 -0
- package/dist/utils/index.js +100 -0
- package/package.json +44 -0
|
@@ -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
|
+
}
|