Files

315 lines
13 KiB
JavaScript
Raw Permalink Normal View History

2026-08-09 04:07:38 +08:00
(function () {
const PY_EXTENSIONS = new Set(['.py']);
const R_EXTENSIONS = new Set(['.r']);
function normalizedRelativePath(file) {
const relative = String(file.webkitRelativePath || file.relativePath || file.name || '').replace(/^\/+/, '');
return relative.split('\\').join('/');
}
function parsePythonImports(content) {
const imports = new Set();
const lines = String(content || '').split(/\r?\n/);
for (const line of lines) {
let match = line.match(/^\s*import\s+([A-Za-z0-9_.,\s]+)/);
if (match) {
match[1].split(',').forEach((item) => {
const token = item.trim().split(/\s+as\s+/i)[0].split('.')[0].trim();
if (token) imports.add(token);
});
}
match = line.match(/^\s*from\s+([A-Za-z0-9_.]+)\s+import\s+/);
if (match) {
const token = match[1].split('.')[0].trim();
if (token) imports.add(token);
}
}
return Array.from(imports);
}
function parseRPackages(content) {
const packages = new Set();
const pattern = /(?:library|require)\(\s*["']?([A-Za-z0-9_.-]+)["']?\s*\)/g;
let match;
const text = String(content || '');
while ((match = pattern.exec(text)) !== null) {
if (match[1]) packages.add(match[1]);
}
return Array.from(packages);
}
function commonPathPrefix(files) {
const paths = files.map((f) => normalizedRelativePath(f).split('/')).filter((parts) => parts.length > 0);
if (!paths.length) return '';
const first = paths[0];
let prefixLen = 0;
for (let i = 0; i < first.length; i++) {
if (!paths.every((parts) => parts[i] === first[i])) break;
prefixLen = i + 1;
}
return first.slice(0, prefixLen).join('/');
}
function sanitizeDisplayName(raw) {
return String(raw || '').replace(/[^A-Za-z0-9_-]+/g, '_').replace(/^_+|_+$/g, '') || 'lightweight_extension';
}
function extractRootName(files) {
const prefix = commonPathPrefix(files);
const base = (prefix && prefix !== 'raw_code' && prefix !== 'raw_test_data')
? sanitizeDisplayName(prefix.split('/')[0])
: 'lightweight_extension_drop';
const ts = Date.now();
return base + '_' + ts;
}
function splitFixedStructure(files) {
const codeFiles = [];
const dataFiles = [];
let hasRunScript = false;
let prefix = commonPathPrefix(files);
if (prefix && !prefix.endsWith('/')) prefix += '/';
const sampled = [];
for (const file of files) {
let relativePath = normalizedRelativePath(file);
if (prefix && relativePath.startsWith(prefix)) {
relativePath = relativePath.slice(prefix.length);
}
if (sampled.length < 10) sampled.push(relativePath);
if (relativePath.startsWith('raw_code/')) {
file.lightweightRelativePath = relativePath.slice('raw_code/'.length) || file.name;
codeFiles.push(file);
if (relativePath === 'raw_code/run.sh') {
hasRunScript = true;
}
} else if (relativePath.startsWith('raw_test_data/')) {
file.lightweightRelativePath = relativePath.slice('raw_test_data/'.length) || file.name;
dataFiles.push(file);
}
}
if (!codeFiles.length) {
const preview = sampled.length ? sampled.slice(0, 10).join(', ') : '(无)';
throw new Error('缺少 raw_code/ 目录(检测到的路径样例: ' + preview + ')');
}
if (!dataFiles.length) {
throw new Error('缺少 raw_test_data/ 目录');
}
if (!hasRunScript) {
throw new Error('缺少 raw_code/run.sh');
}
return { codeFiles, dataFiles };
}
async function extractDependencies(files) {
const pythonImports = new Set();
const rPackages = new Set();
for (const file of files) {
const path = normalizedRelativePath(file);
const lower = path.toLowerCase();
const extension = lower.includes('.') ? lower.slice(lower.lastIndexOf('.')) : '';
if (!PY_EXTENSIONS.has(extension) && !R_EXTENSIONS.has(extension)) {
continue;
}
const content = await file.text();
if (PY_EXTENSIONS.has(extension)) {
parsePythonImports(content).forEach((name) => pythonImports.add(name));
} else {
parseRPackages(content).forEach((name) => rPackages.add(name));
}
}
return {
python_imports: Array.from(pythonImports),
r_packages: Array.from(rPackages),
};
}
async function extractEnvDeclaration(files) {
for (const file of files) {
const path = normalizedRelativePath(file).toLowerCase();
if (!path.endsWith('.sh')) continue;
const content = await file.text();
const m = content.match(/^\s*#\s*lightweight-env\s*:\s*([A-Za-z0-9_.-]+)/m);
if (m) return m[1];
}
return null;
}
2026-08-09 04:07:38 +08:00
async function importFolderToJob(hooks, rootName, files) {
const form = new FormData();
form.append('job_id', String(hooks.jobInfo.job_id || '').trim());
form.append('root_name', rootName);
2026-08-09 04:07:38 +08:00
for (const file of files) {
form.append('files', file, normalizedRelativePath(file));
form.append('relative_paths', normalizedRelativePath(file));
2026-08-09 04:07:38 +08:00
}
const response = await hooks.uploadExtensionWithProgress(
'/api/job/import-folder',
form,
'正在镜像导入 Job',
);
2026-08-09 04:07:38 +08:00
const data = await response.json();
if (!response.ok || !data.success) {
throw new Error(data.detail || data.error || data.message || 'Job 导入失败');
}
return data;
}
async function resolveRuntimeEnv(files) {
const dependencies = await extractDependencies(files);
const envDeclaration = await extractEnvDeclaration(files);
if (envDeclaration) dependencies.env_declaration = envDeclaration;
2026-08-09 04:07:38 +08:00
console.log('[lightWeightExtensionTest] 提取的依赖:', JSON.stringify(dependencies), '| 扫描文件数:', files.length);
try {
const response = await fetch('/api/extension-stages/envs/resolve', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(dependencies),
});
const data = await response.json();
console.log('[lightWeightExtensionTest] 后端 env/resolve 原始响应:', response.status, JSON.stringify(data).slice(0, 500));
if (!response.ok || !data.success) {
throw new Error(data.detail || data.error || data.message || '运行环境解析失败');
}
return data;
} catch (e) {
console.error('[lightWeightExtensionTest] env/resolve 调用失败:', e);
throw e;
}
}
function notifyError(message) {
if (window.ElementPlus && window.ElementPlus.ElMessage) {
window.ElementPlus.ElMessage.error(message);
}
}
async function ensureJob(hooks) {
if (String(hooks.jobInfo.job_id || '').trim() && String(hooks.jobInfo.job_id) !== 'default_job') {
return;
}
await hooks.createNewJob();
if (!String(hooks.jobInfo.job_id || '').trim() || String(hooks.jobInfo.job_id) === 'default_job') {
throw new Error('自动创建 Job 失败');
}
}
async function processFolderFiles(hooks, inputFiles) {
const files = Array.from(inputFiles || []).filter((file) => normalizedRelativePath(file));
if (!files.length) {
throw new Error('未读取到任何文件');
}
hooks.lightweightExtensionTestStatus.value = '正在校验目录结构...';
const rootName = extractRootName(files);
const fixed = splitFixedStructure(files);
hooks.lightweightExtensionTestStatus.value = '正在确保 Job 可用...';
await ensureJob(hooks);
hooks.lightweightExtensionTestStatus.value = '正在解析运行环境...';
const envResult = await resolveRuntimeEnv(files);
hooks.extensionBuild.runtimeEnv = envResult.runtime_env || 'test4py311';
console.log(
'[lightWeightExtensionTest] ✓ 选定环境: ' + hooks.extensionBuild.runtimeEnv,
'| 原因: ' + (envResult.reason || 'unknown'),
'| 兜底: ' + (envResult.fallback ? '是' : '否'),
envResult.matches ? '| 候选: ' + JSON.stringify(envResult.matches.map(m => m.env + '(py:' + (m.python_hits || []).join(',') + ' r:' + (m.r_hits || []).join(',') + ')')) : '',
);
hooks.lightweightExtensionTestStatus.value = '正在镜像导入当前 Job...';
await importFolderToJob(hooks, rootName, files);
hooks.extensionBuild.displayName = rootName;
hooks.extensionBuild.command = 'bash run.sh';
hooks.extensionBuild.codeFiles = hooks.appendExtensionFiles([], fixed.codeFiles);
hooks.extensionBuild.dataFiles = hooks.appendExtensionFiles([], fixed.dataFiles);
hooks.extensionBuild.dataPaths = '';
hooks.lightweightExtensionTestStatus.value = `运行环境 ${hooks.extensionBuild.runtimeEnv} 已确定,正在触发 upload...`;
await hooks.buildLightweightExtensionStage();
hooks.lightweightExtensionTestStatus.value = '已开始 upload,系统正在自动轮询并等待候选卡。';
}
async function readEntry(entry, basePath) {
if (entry.isFile) {
return new Promise((resolve) => {
entry.file((file) => {
Object.defineProperty(file, 'webkitRelativePath', {
value: basePath ? basePath + '/' + entry.name : entry.name,
writable: false,
});
resolve([file]);
});
});
}
if (entry.isDirectory) {
const reader = entry.createReader();
const entries = await new Promise((resolve) => {
reader.readEntries(resolve);
});
const subPath = basePath ? basePath + '/' + entry.name : entry.name;
const nested = await Promise.all(entries.map((e) => readEntry(e, subPath)));
return nested.flat();
}
return [];
}
async function readDroppedFiles(dataTransfer) {
if (!dataTransfer || !dataTransfer.items) {
return Array.from(dataTransfer?.files || []);
}
const items = Array.from(dataTransfer.items);
const entries = items
.map((item) => item.webkitGetAsEntry && item.webkitGetAsEntry())
.filter(Boolean);
if (!entries.length) {
return Array.from(dataTransfer.files || []);
}
const results = await Promise.all(entries.map((entry) => readEntry(entry, '')));
return results.flat();
}
function mountApi() {
window.LightWeightExtensionTest = {
async handleDrop(hooks, event) {
const progress = hooks.lightweightUploadProgress;
2026-08-09 04:07:38 +08:00
try {
if (progress) {
progress.active = true;
progress.percent = 0;
progress.loaded = 0;
progress.total = 0;
progress.speed = 0;
progress.label = '本地文件读取中';
}
hooks.lightweightExtensionTestStatus.value = '本地文件读取中...';
2026-08-09 04:07:38 +08:00
const files = await readDroppedFiles(event?.dataTransfer);
await processFolderFiles(hooks, files);
} catch (error) {
const message = `失败:${error.message || '目录处理失败'}`;
hooks.lightweightExtensionTestStatus.value = message;
if (progress) progress.active = false;
2026-08-09 04:07:38 +08:00
notifyError(message);
}
},
async handlePick(hooks, event) {
const files = event?.target?.files || [];
try {
await processFolderFiles(hooks, files);
} catch (error) {
const message = `失败:${error.message || '目录处理失败'}`;
hooks.lightweightExtensionTestStatus.value = message;
notifyError(message);
} finally {
if (event?.target) {
event.target.value = '';
}
}
},
openPicker() {
const input = document.getElementById('lightweight-extension-test-folder-input');
if (input) {
input.click();
}
},
};
}
mountApi();
})();