新增CodeRunner组件(JS worker沙箱+Python pyodide)与/utility/code-runner独立页;CourseLearn新增练习Tab(首代码块自动提取装载,无示例可直接练习)。
64 lines
2.1 KiB
TypeScript
64 lines
2.1 KiB
TypeScript
/**
|
|
* C-03 Pyodide 懒加载运行器(浏览器内 WASM,无服务端执行)。
|
|
* 安全约束:
|
|
* - 默认断网:不 loadPackage、不拉取额外包;只运行解释器自带能力。
|
|
* - 超时由调用方(CodeRunner.tsx)经 Promise.race + runId 丢弃过期结果保证;
|
|
* Pyodide 本体不支持强制 terminate,超时后输出"执行超时"并忽略迟到结果。
|
|
*/
|
|
|
|
export const PYODIDE_VERSION = "0.26.4";
|
|
export const PYODIDE_CDN = `https://cdn.jsdelivr.net/pyodide/v${PYODIDE_VERSION}/full/`;
|
|
|
|
type PyodideRuntime = {
|
|
runPython: (code: string) => unknown;
|
|
setStdout: (opts: { batched: (s: string) => void }) => void;
|
|
setStderr: (opts: { batched: (s: string) => void }) => void;
|
|
};
|
|
|
|
declare global {
|
|
interface Window {
|
|
__pyodidePromise?: Promise<PyodideRuntime> | null;
|
|
}
|
|
}
|
|
|
|
function loadPyodideScript(): Promise<void> {
|
|
return new Promise((resolve, reject) => {
|
|
if (document.querySelector('script[data-pyodide="1"]')) {
|
|
resolve();
|
|
return;
|
|
}
|
|
const s = document.createElement("script");
|
|
s.dataset.pyodide = "1";
|
|
s.src = `${PYODIDE_CDN}pyodide.js`;
|
|
s.onload = () => resolve();
|
|
s.onerror = () => reject(new Error("Pyodide 运行时加载失败,请检查网络后重试"));
|
|
document.head.appendChild(s);
|
|
});
|
|
}
|
|
|
|
async function bootPyodide(
|
|
onStdout: (s: string) => void,
|
|
onStderr: (s: string) => void,
|
|
): Promise<PyodideRuntime> {
|
|
await loadPyodideScript();
|
|
const factory = (window as unknown as { loadPyodide?: (opts: { indexURL: string }) => Promise<PyodideRuntime> }).loadPyodide;
|
|
if (!factory) throw new Error("Pyodide 运行时初始化失败");
|
|
const py = await factory({ indexURL: PYODIDE_CDN });
|
|
py.setStdout({ batched: onStdout });
|
|
py.setStderr({ batched: onStderr });
|
|
return py;
|
|
}
|
|
|
|
export function getPyodide(
|
|
onStdout: (s: string) => void,
|
|
onStderr: (s: string) => void,
|
|
): Promise<PyodideRuntime> {
|
|
if (!window.__pyodidePromise) {
|
|
window.__pyodidePromise = bootPyodide(onStdout, onStderr).catch((e) => {
|
|
window.__pyodidePromise = null;
|
|
throw e;
|
|
});
|
|
}
|
|
return window.__pyodidePromise;
|
|
}
|