Files
PlantAI-dev/apps/web/vue3/lightWeightExtensionTest.js
potaserandClaude Opus 5 23f265f2b1 feat(lightweight-extension): 拖拽/选取上传进度上报
挂载 lightweightUploadProgress 到 handleDrop/handlePick,
读取本地文件时初始化进度并显示状态,失败时关闭进度。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 10:14:33 +08:00

330 lines
14 KiB
JavaScript
Raw Permalink 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 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 hooks.uploadExtensionWithProgress(
'/api/job/import-folder',
JSON.stringify(payload),
'正在镜像导入 Job',
{ 'Content-Type': 'application/json' },
);
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();
})();