Files
PlantAI-dev/apps/web/vue3/lightWeightExtensionTest.js
T
potaserandClaude Opus 5 dfff8b6c45 fix(lightweight-extension): 输出前缀绑定、拖拽导入与前端入口修复
- runtime_binding: 验证时排除输入 sidecar 输出,目录参数跟随输出前缀重命名
- import-folder: 改 multipart 二进制上传,规避大文件 base64 报 Invalid string length
- main: 前端入口切回 index.html,conda 路径兜底

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-24 20:51:22 +08:00

315 lines
13 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
(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;
}
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);
for (const file of files) {
form.append('files', file, normalizedRelativePath(file));
form.append('relative_paths', normalizedRelativePath(file));
}
const response = await hooks.uploadExtensionWithProgress(
'/api/job/import-folder',
form,
'正在镜像导入 Job',
);
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;
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;
try {
if (progress) {
progress.active = true;
progress.percent = 0;
progress.loaded = 0;
progress.total = 0;
progress.speed = 0;
progress.label = '本地文件读取中';
}
hooks.lightweightExtensionTestStatus.value = '本地文件读取中...';
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;
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();
})();