first commit

This commit is contained in:
root
2026-03-31 21:04:10 +08:00
commit 8aac33bf65
15 changed files with 3056 additions and 0 deletions
+252
View File
@@ -0,0 +1,252 @@
#!/usr/bin/env node
'use strict';
/**
* Preflight check for uploading a file to IMA Knowledge Base.
*
* Validates file type, size, and extracts all metadata needed for
* create_media and add_knowledge API calls.
*
* Resolution priority:
* 1. If --content-type is provided and recognized → use it (content-type rules over extension)
* 2. If --content-type is unrecognized, fall back to extension
* 3. If no --content-type, use extension
* 4. If neither can resolve → fail
*
* Usage:
* node preflight-check.cjs --file /path/to/report.pdf
* node preflight-check.cjs --file /path/to/downloaded_file --content-type application/pdf
*
* Output (JSON, always to stdout):
*
* Pass:
* {
* "pass": true,
* "file_path": "/absolute/path/to/report.pdf",
* "file_name": "report.pdf",
* "file_ext": "pdf",
* "file_size": 123456,
* "media_type": 1,
* "content_type": "application/pdf"
* }
*
* Pass (no extension, content_type provided):
* {
* "pass": true,
* "file_path": "/absolute/path/to/downloaded_file",
* "file_name": "downloaded_file",
* "file_ext": "",
* "file_size": 123456,
* "media_type": 1,
* "content_type": "application/pdf"
* }
*
* Fail:
* {
* "pass": false,
* "file_path": "/absolute/path/to/video.mp4",
* "file_name": "video.mp4",
* "file_ext": "mp4",
* "reason": "Video files (.mp4) are not supported. ..."
* }
*
* Exit codes:
* 0 = pass — file is ready for upload
* 1 = fail — file rejected (unsupported type, over size limit, etc.)
* 2 = error — file not found, usage error, etc.
*/
const fs = require('node:fs');
const path = require('node:path');
// ─── Extension → media_type + content_type ──────────────────────────────────
const EXT_MAP = {
pdf: { media_type: 1, content_type: 'application/pdf' },
doc: { media_type: 3, content_type: 'application/msword' },
docx: { media_type: 3, content_type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' },
ppt: { media_type: 4, content_type: 'application/vnd.ms-powerpoint' },
pptx: { media_type: 4, content_type: 'application/vnd.openxmlformats-officedocument.presentationml.presentation' },
xls: { media_type: 5, content_type: 'application/vnd.ms-excel' },
xlsx: { media_type: 5, content_type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' },
csv: { media_type: 5, content_type: 'text/csv' },
md: { media_type: 7, content_type: 'text/markdown' },
markdown: { media_type: 7, content_type: 'text/markdown' },
png: { media_type: 9, content_type: 'image/png' },
jpg: { media_type: 9, content_type: 'image/jpeg' },
jpeg: { media_type: 9, content_type: 'image/jpeg' },
webp: { media_type: 9, content_type: 'image/webp' },
txt: { media_type: 13, content_type: 'text/plain' },
xmind: { media_type: 14, content_type: 'application/x-xmind' },
mp3: { media_type: 15, content_type: 'audio/mpeg' },
m4a: { media_type: 15, content_type: 'audio/x-m4a' },
wav: { media_type: 15, content_type: 'audio/wav' },
aac: { media_type: 15, content_type: 'audio/aac' },
};
// ─── Content-Type → media_type (reverse lookup) ─────────────────────────────
const CONTENT_TYPE_MAP = {};
for (const [, value] of Object.entries(EXT_MAP)) {
// First entry wins — keeps the canonical content_type per media_type
if (!CONTENT_TYPE_MAP[value.content_type]) {
CONTENT_TYPE_MAP[value.content_type] = value.media_type;
}
}
// Extra aliases not covered by EXT_MAP
Object.assign(CONTENT_TYPE_MAP, {
'text/x-markdown': 7,
'application/md': 7,
'application/markdown': 7,
'application/vnd.xmind.workbook': 14,
'application/zip': 14, // xmind can be zip
});
// ─── Size limits by media_type (bytes) ──────────────────────────────────────
const MB = 1024 * 1024;
const SIZE_LIMITS = {
5: 10 * MB, // Excel / CSV
7: 10 * MB, // Markdown
13: 10 * MB, // TXT
14: 10 * MB, // Xmind
9: 30 * MB, // Image
};
const DEFAULT_SIZE_LIMIT = 200 * MB; // PDF, Word, PPT, Audio, etc.
// ─── Explicitly unsupported extensions ──────────────────────────────────────
const UNSUPPORTED_VIDEO_EXT = new Set(['mp4', 'avi', 'mov', 'mkv', 'wmv', 'flv', 'webm', 'm4v', 'rmvb', 'rm', '3gp']);
const UNSUPPORTED_VIDEO_CT = new Set([
'video/mp4',
'video/x-msvideo',
'video/quicktime',
'video/x-matroska',
'video/x-ms-wmv',
'video/x-flv',
'video/webm',
]);
// ─── Helpers ────────────────────────────────────────────────────────────────
function fail(result) {
console.log(JSON.stringify({ pass: false, ...result }));
process.exit(1);
}
function formatSize(bytes) {
if (bytes < MB) return `${(bytes / 1024).toFixed(1)} KB`;
return `${(bytes / MB).toFixed(1)} MB`;
}
// ─── Argument parsing ───────────────────────────────────────────────────────
function parseArgs(argv) {
const args = {};
for (let i = 2; i < argv.length; i++) {
if (argv[i].startsWith('--') && i + 1 < argv.length) {
args[argv[i].replace(/^--/, '')] = argv[i + 1];
i += 1;
}
}
return args;
}
// ─── Main ───────────────────────────────────────────────────────────────────
const args = parseArgs(process.argv);
if (!args.file) {
console.error('Usage: node preflight-check.cjs --file <path> [--content-type <mime>]');
process.exit(2);
}
const filePath = path.resolve(args.file);
const fileName = path.basename(filePath);
const extMatch = fileName.match(/\.([^.]+)$/);
const ext = extMatch ? extMatch[1].toLowerCase() : '';
const inputContentType = args['content-type'] || '';
const base = { file_path: filePath, file_name: fileName, file_ext: ext };
// 1. Check file exists
let stat;
try {
stat = fs.statSync(filePath);
} catch (err) {
if (err.code === 'ENOENT') {
console.error(`File not found: ${filePath}`);
process.exit(2);
}
throw err;
}
// 2. Check not an unsupported video type (by ext or content-type)
if (UNSUPPORTED_VIDEO_EXT.has(ext)) {
fail({ ...base, reason: `Video files (.${ext}) are not supported. Only supported in IMA desktop app.` });
}
if (UNSUPPORTED_VIDEO_CT.has(inputContentType)) {
fail({ ...base, reason: `Video files (${inputContentType}) are not supported. Only supported in IMA desktop app.` });
}
// 3. Resolve media_type and content_type
// Priority: content-type first, then fall back to extension
let mediaType = null;
let contentType = null;
const ctMediaType = inputContentType ? CONTENT_TYPE_MAP[inputContentType] : undefined;
const extMapping = ext ? EXT_MAP[ext] : undefined;
if (ctMediaType != null) {
// Content-type recognized — always wins
mediaType = ctMediaType;
contentType = inputContentType;
} else if (inputContentType) {
// Content-type provided but unrecognized — try extension fallback
if (extMapping) {
mediaType = extMapping.media_type;
contentType = extMapping.content_type;
} else {
fail({
...base,
reason: `Unrecognized content type ${inputContentType}${ext ? ` and file extension .${ext}` : ''}. This file type is not supported.`,
});
}
} else {
// No content-type provided — fall back to extension
if (extMapping) {
mediaType = extMapping.media_type;
contentType = extMapping.content_type;
} else if (ext) {
fail({ ...base, reason: `Unrecognized file extension .${ext}. This file type is not supported.` });
} else {
fail({ ...base, reason: 'File has no extension and no --content-type provided. Cannot determine file type.' });
}
}
// 4. Check file size
const fileSize = stat.size;
const sizeLimit = SIZE_LIMITS[mediaType] || DEFAULT_SIZE_LIMIT;
if (fileSize > sizeLimit) {
fail({
...base,
file_size: fileSize,
media_type: mediaType,
content_type: contentType,
reason: `File size ${formatSize(fileSize)} exceeds the ${formatSize(sizeLimit)} limit for this file type.`,
});
}
// 5. All checks passed
console.log(
JSON.stringify({
pass: true,
...base,
file_size: fileSize,
media_type: mediaType,
content_type: contentType,
}),
);
process.exit(0);