(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 encodeBase64(file) { const buffer = await file.arrayBuffer(); const bytes = new Uint8Array(buffer); let binary = ''; const chunkSize = 0x8000; for (let index = 0; index < bytes.length; index += chunkSize) { binary += String.fromCharCode(...bytes.subarray(index, index + chunkSize)); } return btoa(binary); } async function importFolderToJob(hooks, rootName, files) { const payload = { job_id: String(hooks.jobInfo.job_id || '').trim(), root_name: rootName, files: [], relative_paths: [], }; for (const file of files) { payload.files.push(await encodeBase64(file)); payload.relative_paths.push(normalizedRelativePath(file)); } const response = await fetch('/api/job/import-folder', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), }); 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); 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) { try { const files = await readDroppedFiles(event?.dataTransfer); await processFolderFiles(hooks, files); } catch (error) { const message = `失败:${error.message || '目录处理失败'}`; hooks.lightweightExtensionTestStatus.value = message; 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(); })();