preflight-mcp 0.1.0 → 0.1.2
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 +282 -27
- package/README.zh-CN.md +277 -308
- package/dist/bundle/cleanup.js +155 -0
- package/dist/bundle/deepwiki.js +1 -1
- package/dist/bundle/github.js +100 -15
- package/dist/bundle/githubArchive.js +82 -0
- package/dist/bundle/ingest.js +2 -2
- package/dist/bundle/paths.js +23 -0
- package/dist/bundle/service.js +800 -57
- package/dist/config.js +1 -0
- package/dist/context7/client.js +1 -1
- package/dist/core/concurrency-limiter.js +100 -0
- package/dist/core/scheduler.js +4 -1
- package/dist/jobs/tmp-cleanup-job.js +71 -0
- package/dist/mcp/errorKinds.js +54 -0
- package/dist/mcp/uris.js +28 -8
- package/dist/search/sqliteFts.js +68 -36
- package/dist/server/optimized-server.js +4 -0
- package/dist/server.js +498 -279
- package/dist/tools/searchByTags.js +80 -0
- package/package.json +26 -1
package/dist/config.js
CHANGED
|
@@ -50,6 +50,7 @@ export function getConfig() {
|
|
|
50
50
|
githubToken: process.env.GITHUB_TOKEN,
|
|
51
51
|
context7ApiKey: process.env.CONTEXT7_API_KEY,
|
|
52
52
|
context7McpUrl: process.env.CONTEXT7_MCP_URL ?? 'https://mcp.context7.com/mcp',
|
|
53
|
+
gitCloneTimeoutMs: envNumber('PREFLIGHT_GIT_CLONE_TIMEOUT_MS', 5 * 60_000),
|
|
53
54
|
maxFileBytes: envNumber('PREFLIGHT_MAX_FILE_BYTES', 512 * 1024),
|
|
54
55
|
maxTotalBytes: envNumber('PREFLIGHT_MAX_TOTAL_BYTES', 50 * 1024 * 1024),
|
|
55
56
|
analysisMode,
|
package/dist/context7/client.js
CHANGED
|
@@ -19,7 +19,7 @@ export async function connectContext7(cfg) {
|
|
|
19
19
|
maxRetries: 1,
|
|
20
20
|
},
|
|
21
21
|
});
|
|
22
|
-
const client = new Client({ name: 'preflight-context7', version: '0.1.
|
|
22
|
+
const client = new Client({ name: 'preflight-context7', version: '0.1.1' });
|
|
23
23
|
await client.connect(transport);
|
|
24
24
|
return {
|
|
25
25
|
client,
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Concurrency limiter to prevent DoS attacks via resource exhaustion.
|
|
3
|
+
* Limits the number of concurrent operations (e.g., bundle creations).
|
|
4
|
+
*/
|
|
5
|
+
import { logger } from '../logging/logger.js';
|
|
6
|
+
export class ConcurrencyLimiter {
|
|
7
|
+
activeCount = 0;
|
|
8
|
+
maxConcurrent;
|
|
9
|
+
queue = [];
|
|
10
|
+
queueTimeoutMs;
|
|
11
|
+
constructor(maxConcurrent, queueTimeoutMs = 5 * 60 * 1000) {
|
|
12
|
+
if (maxConcurrent <= 0) {
|
|
13
|
+
throw new Error('maxConcurrent must be positive');
|
|
14
|
+
}
|
|
15
|
+
this.maxConcurrent = maxConcurrent;
|
|
16
|
+
this.queueTimeoutMs = queueTimeoutMs;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Acquire a slot for executing an operation.
|
|
20
|
+
* Waits in queue if all slots are occupied.
|
|
21
|
+
* Throws if waiting exceeds queueTimeoutMs.
|
|
22
|
+
*/
|
|
23
|
+
async acquire() {
|
|
24
|
+
if (this.activeCount < this.maxConcurrent) {
|
|
25
|
+
this.activeCount++;
|
|
26
|
+
logger.debug(`Concurrency slot acquired (${this.activeCount}/${this.maxConcurrent})`);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
// Queue is full, wait for a slot
|
|
30
|
+
logger.info(`Concurrency limit reached (${this.maxConcurrent}), queuing request. Queue size: ${this.queue.length}`);
|
|
31
|
+
return new Promise((resolve, reject) => {
|
|
32
|
+
const timeoutId = setTimeout(() => {
|
|
33
|
+
// Remove from queue
|
|
34
|
+
const index = this.queue.findIndex(item => item.resolve === resolve);
|
|
35
|
+
if (index >= 0) {
|
|
36
|
+
this.queue.splice(index, 1);
|
|
37
|
+
}
|
|
38
|
+
reject(new Error(`Operation timed out waiting in queue (${this.queueTimeoutMs}ms)`));
|
|
39
|
+
}, this.queueTimeoutMs);
|
|
40
|
+
this.queue.push({
|
|
41
|
+
resolve: () => {
|
|
42
|
+
clearTimeout(timeoutId);
|
|
43
|
+
this.activeCount++;
|
|
44
|
+
logger.debug(`Concurrency slot acquired from queue (${this.activeCount}/${this.maxConcurrent})`);
|
|
45
|
+
resolve();
|
|
46
|
+
},
|
|
47
|
+
reject: (err) => {
|
|
48
|
+
clearTimeout(timeoutId);
|
|
49
|
+
reject(err);
|
|
50
|
+
},
|
|
51
|
+
addedAt: Date.now(),
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Release a slot after operation completes.
|
|
57
|
+
*/
|
|
58
|
+
release() {
|
|
59
|
+
if (this.activeCount <= 0) {
|
|
60
|
+
logger.warn('ConcurrencyLimiter.release() called with no active operations');
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
this.activeCount--;
|
|
64
|
+
logger.debug(`Concurrency slot released (${this.activeCount}/${this.maxConcurrent})`);
|
|
65
|
+
// Process next item in queue
|
|
66
|
+
if (this.queue.length > 0) {
|
|
67
|
+
const next = this.queue.shift();
|
|
68
|
+
if (next) {
|
|
69
|
+
const waitTime = Date.now() - next.addedAt;
|
|
70
|
+
logger.info(`Processing queued request (waited ${waitTime}ms)`);
|
|
71
|
+
next.resolve();
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Execute a function with concurrency control.
|
|
77
|
+
*/
|
|
78
|
+
async run(fn) {
|
|
79
|
+
await this.acquire();
|
|
80
|
+
try {
|
|
81
|
+
return await fn();
|
|
82
|
+
}
|
|
83
|
+
finally {
|
|
84
|
+
this.release();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Get current status for monitoring.
|
|
89
|
+
*/
|
|
90
|
+
getStatus() {
|
|
91
|
+
return {
|
|
92
|
+
active: this.activeCount,
|
|
93
|
+
max: this.maxConcurrent,
|
|
94
|
+
queued: this.queue.length,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
// Global limiter for bundle creation operations
|
|
99
|
+
// Default: allow 10 concurrent bundle creations
|
|
100
|
+
export const bundleCreationLimiter = new ConcurrencyLimiter(parseInt(process.env.PREFLIGHT_MAX_CONCURRENT_BUNDLES ?? '10', 10));
|
package/dist/core/scheduler.js
CHANGED
|
@@ -96,7 +96,10 @@ export class Scheduler {
|
|
|
96
96
|
}
|
|
97
97
|
jobTask.retryTimeout = setTimeout(() => {
|
|
98
98
|
jobTask.retryTimeout = undefined;
|
|
99
|
-
|
|
99
|
+
// Properly handle Promise rejection
|
|
100
|
+
this.executeJob(jobName, job, true).catch((err) => {
|
|
101
|
+
logger.error(`Unhandled error in retry for job ${jobName}`, err instanceof Error ? err : undefined);
|
|
102
|
+
});
|
|
100
103
|
}, delay);
|
|
101
104
|
jobTask.retryTimeout.unref?.();
|
|
102
105
|
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { Job } from '../core/scheduler.js';
|
|
4
|
+
import { logger } from '../logging/logger.js';
|
|
5
|
+
import { getConfig } from '../config.js';
|
|
6
|
+
/**
|
|
7
|
+
* Cleanup job for temporary directories.
|
|
8
|
+
* Removes temporary checkout directories older than 24 hours.
|
|
9
|
+
*/
|
|
10
|
+
export class TmpCleanupJob extends Job {
|
|
11
|
+
maxAgeHours = 24;
|
|
12
|
+
getName() {
|
|
13
|
+
return 'tmp-cleanup';
|
|
14
|
+
}
|
|
15
|
+
getMaxRetries() {
|
|
16
|
+
return 2; // Fewer retries for cleanup jobs
|
|
17
|
+
}
|
|
18
|
+
async run() {
|
|
19
|
+
const cfg = getConfig();
|
|
20
|
+
const checkoutsDir = path.join(cfg.tmpDir, 'checkouts');
|
|
21
|
+
try {
|
|
22
|
+
const exists = await this.pathExists(checkoutsDir);
|
|
23
|
+
if (!exists) {
|
|
24
|
+
logger.debug('Checkouts directory does not exist, skipping cleanup');
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
const entries = await fs.readdir(checkoutsDir, { withFileTypes: true });
|
|
28
|
+
const now = Date.now();
|
|
29
|
+
const maxAgeMs = this.maxAgeHours * 60 * 60 * 1000;
|
|
30
|
+
let cleanedCount = 0;
|
|
31
|
+
let errorCount = 0;
|
|
32
|
+
for (const entry of entries) {
|
|
33
|
+
if (!entry.isDirectory())
|
|
34
|
+
continue;
|
|
35
|
+
const entryPath = path.join(checkoutsDir, entry.name);
|
|
36
|
+
try {
|
|
37
|
+
const stats = await fs.stat(entryPath);
|
|
38
|
+
const ageMs = now - stats.mtimeMs;
|
|
39
|
+
if (ageMs > maxAgeMs) {
|
|
40
|
+
logger.info(`Cleaning up old temporary directory: ${entry.name} (age: ${Math.round(ageMs / 3600000)}h)`);
|
|
41
|
+
await fs.rm(entryPath, { recursive: true, force: true });
|
|
42
|
+
cleanedCount++;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
logger.warn(`Failed to cleanup temporary directory ${entry.name}`, err instanceof Error ? err : undefined);
|
|
47
|
+
errorCount++;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (cleanedCount > 0) {
|
|
51
|
+
logger.info(`Temporary directory cleanup completed: ${cleanedCount} removed, ${errorCount} errors`);
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
logger.debug('No old temporary directories to clean up');
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
logger.error('Temporary directory cleanup failed', err instanceof Error ? err : undefined);
|
|
59
|
+
throw err;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
async pathExists(p) {
|
|
63
|
+
try {
|
|
64
|
+
await fs.access(p);
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
function msgOf(err) {
|
|
2
|
+
return err instanceof Error ? err.message : String(err);
|
|
3
|
+
}
|
|
4
|
+
function codeOf(err) {
|
|
5
|
+
const anyErr = err;
|
|
6
|
+
const code = anyErr?.code;
|
|
7
|
+
return typeof code === 'string' ? code : undefined;
|
|
8
|
+
}
|
|
9
|
+
function isLikelyIndexProblemMessage(message) {
|
|
10
|
+
const m = message.toLowerCase();
|
|
11
|
+
return ((m.includes('sqlite') || m.includes('fts') || m.includes('database')) &&
|
|
12
|
+
(m.includes('unable to open database file') ||
|
|
13
|
+
m.includes('cannot open') ||
|
|
14
|
+
m.includes('cantopen') ||
|
|
15
|
+
m.includes('no such table') ||
|
|
16
|
+
m.includes('malformed') ||
|
|
17
|
+
m.includes('file is not a database') ||
|
|
18
|
+
m.includes('disk i/o')));
|
|
19
|
+
}
|
|
20
|
+
export function classifyPreflightErrorKind(err) {
|
|
21
|
+
const message = msgOf(err);
|
|
22
|
+
const m = message.toLowerCase();
|
|
23
|
+
const code = codeOf(err);
|
|
24
|
+
if (m.includes('bundle not found'))
|
|
25
|
+
return 'bundle_not_found';
|
|
26
|
+
// Path traversal attempts from safeJoin.
|
|
27
|
+
if (m.includes('unsafe path traversal attempt'))
|
|
28
|
+
return 'invalid_path';
|
|
29
|
+
// Filesystem errors.
|
|
30
|
+
if (code === 'ENOENT')
|
|
31
|
+
return 'file_not_found';
|
|
32
|
+
if (code === 'EACCES' || code === 'EPERM')
|
|
33
|
+
return 'permission_denied';
|
|
34
|
+
// Common string forms when error.code is not preserved.
|
|
35
|
+
if (m.includes('enoent') && m.includes('no such file or directory'))
|
|
36
|
+
return 'file_not_found';
|
|
37
|
+
if ((m.includes('eacces') || m.includes('eperm')) && m.includes('permission'))
|
|
38
|
+
return 'permission_denied';
|
|
39
|
+
if (m.includes('deprecated') && (m.includes('ensurefresh') || m.includes('autorepairindex'))) {
|
|
40
|
+
return 'deprecated_parameter';
|
|
41
|
+
}
|
|
42
|
+
if (isLikelyIndexProblemMessage(message))
|
|
43
|
+
return 'index_missing_or_corrupt';
|
|
44
|
+
return 'unknown';
|
|
45
|
+
}
|
|
46
|
+
export function formatPreflightError(kind, message) {
|
|
47
|
+
// Stable, machine-parseable prefix for UIs.
|
|
48
|
+
return `[preflight_error kind=${kind}] ${message}`;
|
|
49
|
+
}
|
|
50
|
+
export function wrapPreflightError(err) {
|
|
51
|
+
const message = msgOf(err);
|
|
52
|
+
const kind = classifyPreflightErrorKind(err);
|
|
53
|
+
return new Error(formatPreflightError(kind, message));
|
|
54
|
+
}
|
package/dist/mcp/uris.js
CHANGED
|
@@ -27,19 +27,39 @@ export function normalizeRelativePath(p) {
|
|
|
27
27
|
return p.replaceAll('\\', '/').replace(/^\/+/, '');
|
|
28
28
|
}
|
|
29
29
|
export function safeJoin(rootDir, relativePath) {
|
|
30
|
-
// Block absolute paths BEFORE normalization.
|
|
31
|
-
// This catches Unix-style /etc/passwd and Windows-style C:\path.
|
|
32
30
|
const trimmed = relativePath.trim();
|
|
33
|
-
|
|
34
|
-
|
|
31
|
+
// Early rejection of obviously malicious patterns
|
|
32
|
+
if (!trimmed || trimmed.length > 4096) {
|
|
33
|
+
throw new Error('Invalid path: empty or too long');
|
|
35
34
|
}
|
|
36
|
-
//
|
|
35
|
+
// Block UNC paths (\\server\share or \\?\C:\path)
|
|
36
|
+
if (trimmed.startsWith('\\\\')) {
|
|
37
|
+
throw new Error('Unsafe path traversal attempt: UNC path not allowed');
|
|
38
|
+
}
|
|
39
|
+
// Block Windows device paths (CON, PRN, AUX, NUL, COM1-9, LPT1-9)
|
|
40
|
+
const devicePattern = /^(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])(\..*)?$/i;
|
|
41
|
+
const baseName = path.basename(trimmed);
|
|
42
|
+
if (devicePattern.test(baseName)) {
|
|
43
|
+
throw new Error('Unsafe path: Windows device name not allowed');
|
|
44
|
+
}
|
|
45
|
+
// Normalize FIRST to canonicalize the path
|
|
37
46
|
const norm = normalizeRelativePath(relativePath);
|
|
47
|
+
// After normalization, check for absolute path indicators
|
|
48
|
+
if (norm.startsWith('/') || /^[a-zA-Z]:/.test(norm)) {
|
|
49
|
+
throw new Error('Unsafe path traversal attempt: absolute path after normalization');
|
|
50
|
+
}
|
|
51
|
+
// Check for null bytes (path injection)
|
|
52
|
+
if (norm.includes('\0')) {
|
|
53
|
+
throw new Error('Unsafe path: null byte not allowed');
|
|
54
|
+
}
|
|
55
|
+
// Resolve and validate containment
|
|
38
56
|
const joined = path.resolve(rootDir, norm.split('/').join(path.sep));
|
|
39
57
|
const rootResolved = path.resolve(rootDir);
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
58
|
+
// Ensure normalized form of joined path starts with root
|
|
59
|
+
const joinedNorm = path.normalize(joined);
|
|
60
|
+
const rootNorm = path.normalize(rootResolved);
|
|
61
|
+
if (!joinedNorm.startsWith(rootNorm + path.sep) && joinedNorm !== rootNorm) {
|
|
62
|
+
throw new Error('Unsafe path traversal attempt: path escapes root directory');
|
|
43
63
|
}
|
|
44
64
|
return joined;
|
|
45
65
|
}
|
package/dist/search/sqliteFts.js
CHANGED
|
@@ -123,43 +123,64 @@ export async function incrementalIndexUpdate(dbPath, files, opts) {
|
|
|
123
123
|
const updateTransaction = db.transaction((updateFiles) => {
|
|
124
124
|
const now = new Date().toISOString();
|
|
125
125
|
for (const f of updateFiles) {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
126
|
+
try {
|
|
127
|
+
// Delete old lines
|
|
128
|
+
deleteLines.run(f.bundleNormRelativePath);
|
|
129
|
+
// Insert new lines
|
|
130
|
+
const text = fsSync.readFileSync(f.bundleNormAbsPath, 'utf8');
|
|
131
|
+
const lines = text.split('\n');
|
|
132
|
+
for (let i = 0; i < lines.length; i++) {
|
|
133
|
+
const line = lines[i] ?? '';
|
|
134
|
+
if (!line.trim())
|
|
135
|
+
continue;
|
|
136
|
+
insertLine.run(line, f.bundleNormRelativePath, f.repoId, f.kind, i + 1);
|
|
137
|
+
}
|
|
138
|
+
// Update metadata
|
|
139
|
+
upsertMeta.run(f.bundleNormRelativePath, f.sha256, now);
|
|
140
|
+
}
|
|
141
|
+
catch (err) {
|
|
142
|
+
// Log error but continue with other files to avoid transaction rollback
|
|
143
|
+
console.error(`Failed to index file ${f.bundleNormRelativePath}:`, err);
|
|
144
|
+
throw err; // Re-throw to trigger transaction rollback and ensure finally block runs
|
|
136
145
|
}
|
|
137
|
-
// Update metadata
|
|
138
|
-
upsertMeta.run(f.bundleNormRelativePath, f.sha256, now);
|
|
139
146
|
}
|
|
140
147
|
});
|
|
141
148
|
// Process new files
|
|
142
149
|
const insertTransaction = db.transaction((newFiles) => {
|
|
143
150
|
const now = new Date().toISOString();
|
|
144
151
|
for (const f of newFiles) {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
+
try {
|
|
153
|
+
const text = fsSync.readFileSync(f.bundleNormAbsPath, 'utf8');
|
|
154
|
+
const lines = text.split('\n');
|
|
155
|
+
for (let i = 0; i < lines.length; i++) {
|
|
156
|
+
const line = lines[i] ?? '';
|
|
157
|
+
if (!line.trim())
|
|
158
|
+
continue;
|
|
159
|
+
insertLine.run(line, f.bundleNormRelativePath, f.repoId, f.kind, i + 1);
|
|
160
|
+
}
|
|
161
|
+
// Insert metadata
|
|
162
|
+
upsertMeta.run(f.bundleNormRelativePath, f.sha256, now);
|
|
163
|
+
}
|
|
164
|
+
catch (err) {
|
|
165
|
+
console.error(`Failed to index file ${f.bundleNormRelativePath}:`, err);
|
|
166
|
+
throw err; // Re-throw to trigger transaction rollback and ensure finally block runs
|
|
152
167
|
}
|
|
153
|
-
// Insert metadata
|
|
154
|
-
upsertMeta.run(f.bundleNormRelativePath, f.sha256, now);
|
|
155
168
|
}
|
|
156
169
|
});
|
|
157
|
-
// Execute transactions
|
|
170
|
+
// Execute transactions in batches to avoid memory overflow
|
|
171
|
+
// Process 100 files at a time to keep memory usage bounded
|
|
172
|
+
const BATCH_SIZE = 100;
|
|
158
173
|
if (filesToUpdate.length > 0) {
|
|
159
|
-
|
|
174
|
+
for (let i = 0; i < filesToUpdate.length; i += BATCH_SIZE) {
|
|
175
|
+
const batch = filesToUpdate.slice(i, i + BATCH_SIZE);
|
|
176
|
+
updateTransaction(batch);
|
|
177
|
+
}
|
|
160
178
|
}
|
|
161
179
|
if (filesToIndex.length > 0) {
|
|
162
|
-
|
|
180
|
+
for (let i = 0; i < filesToIndex.length; i += BATCH_SIZE) {
|
|
181
|
+
const batch = filesToIndex.slice(i, i + BATCH_SIZE);
|
|
182
|
+
insertTransaction(batch);
|
|
183
|
+
}
|
|
163
184
|
}
|
|
164
185
|
return {
|
|
165
186
|
added,
|
|
@@ -226,21 +247,32 @@ export async function rebuildIndex(dbPathOrFiles, filesOrDbPath, opts) {
|
|
|
226
247
|
continue;
|
|
227
248
|
if (f.kind === 'code' && !opts.includeCode)
|
|
228
249
|
continue;
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
250
|
+
try {
|
|
251
|
+
// Read file synchronously inside transaction for better performance.
|
|
252
|
+
const text = fsSync.readFileSync(f.bundleNormAbsPath, 'utf8');
|
|
253
|
+
const lines = text.split('\n');
|
|
254
|
+
for (let i = 0; i < lines.length; i++) {
|
|
255
|
+
const line = lines[i] ?? '';
|
|
256
|
+
// Skip empty lines to keep the index smaller.
|
|
257
|
+
if (!line.trim())
|
|
258
|
+
continue;
|
|
259
|
+
insertLine.run(line, f.bundleNormRelativePath, f.repoId, f.kind, i + 1);
|
|
260
|
+
}
|
|
261
|
+
// Store file metadata for incremental updates
|
|
262
|
+
insertMeta.run(f.bundleNormRelativePath, f.sha256, now);
|
|
263
|
+
}
|
|
264
|
+
catch (err) {
|
|
265
|
+
console.error(`Failed to index file ${f.bundleNormRelativePath}:`, err);
|
|
266
|
+
throw err;
|
|
238
267
|
}
|
|
239
|
-
// Store file metadata for incremental updates
|
|
240
|
-
insertMeta.run(f.bundleNormRelativePath, f.sha256, now);
|
|
241
268
|
}
|
|
242
269
|
});
|
|
243
|
-
|
|
270
|
+
// Process files in batches to avoid memory overflow
|
|
271
|
+
const BATCH_SIZE = 100;
|
|
272
|
+
for (let i = 0; i < files.length; i += BATCH_SIZE) {
|
|
273
|
+
const batch = files.slice(i, i + BATCH_SIZE);
|
|
274
|
+
insertMany(batch);
|
|
275
|
+
}
|
|
244
276
|
}
|
|
245
277
|
finally {
|
|
246
278
|
db.close();
|
|
@@ -2,6 +2,7 @@ import { PreflightScheduler } from '../core/scheduler.js';
|
|
|
2
2
|
import { BundleAutoUpdateJob } from '../jobs/bundle-auto-update-job.js';
|
|
3
3
|
import { StorageCleanupJob } from '../jobs/storage-cleanup-job.js';
|
|
4
4
|
import { HealthCheckJob } from '../jobs/health-check-job.js';
|
|
5
|
+
import { TmpCleanupJob } from '../jobs/tmp-cleanup-job.js';
|
|
5
6
|
import { getStorageManager } from '../storage/storage-adapter.js';
|
|
6
7
|
import { compressData, decompressData, detectCompressionType } from '../storage/compression.js';
|
|
7
8
|
import { logger, createModuleLogger } from '../logging/logger.js';
|
|
@@ -85,6 +86,9 @@ export class OptimizedPreflightServer {
|
|
|
85
86
|
// 健康检查任务 - 每30分钟执行一次
|
|
86
87
|
PreflightScheduler.build(HealthCheckJob).schedule('*/30 * * * *');
|
|
87
88
|
moduleLogger.info('Health check job scheduled (every 30 minutes)');
|
|
89
|
+
// 临时目录清理任务 - 每6小时执行一次
|
|
90
|
+
PreflightScheduler.build(TmpCleanupJob).schedule('0 */6 * * *');
|
|
91
|
+
moduleLogger.info('Temporary directory cleanup job scheduled (every 6 hours)');
|
|
88
92
|
moduleLogger.info('All scheduled jobs configured', {
|
|
89
93
|
totalJobs: PreflightScheduler.getAllJobsStatus()
|
|
90
94
|
});
|