feat(bridge): 新增 make_run_code_closure 桥接编辑器内运行代码

tiptap_bridge.rs:
- EditorOptions extern 追加 set_on_run_code setter (js_name=onRunCode)
- 新增 RunCodeOptsJs 映射(language/source/overridesJson getter)
- 新增 format_run_result + make_run_code_closure
  · start_exec + 500ms 轮询 get_exec_result,30s 上限
  · overridesJson 空串视为 None,畸形 JSON 静默降级
  · 不依赖 server-only 的 languages 模块(前端 extractLang 已提取纯语言名)
- EditorHandle 加 _on_run_code 字段 + new 参数
- pub use 导出 make_run_code_closure + RunCodeOptsJs

write.rs: 跟进 EditorHandle::new 新签名(构造 on_run_code + set_on_run_code)。
This commit is contained in:
xfy 2026-07-06 18:11:28 +08:00
parent 6bdc48e84c
commit 0c1e8b7640
2 changed files with 139 additions and 6 deletions

View File

@ -186,6 +186,8 @@ fn write_editor(post_id: Option<i32>) -> Element {
consume_upload_event(&ev, uploads_in_flight, upload_errors);
}
});
// 运行代码 closurestart_exec + 轮询,结果字符串回填 JS 结果区 DOM。
let on_run_code = crate::tiptap_bridge::make_run_code_closure();
// —— 构造 options ——
let opts = crate::tiptap_bridge::EditorOptions::new();
@ -194,6 +196,7 @@ fn write_editor(post_id: Option<i32>) -> Element {
opts.set_on_ready(&on_ready);
opts.set_on_image_upload(&on_image_upload);
opts.set_on_upload_event(&on_upload_event);
opts.set_on_run_code(&on_run_code);
// —— create同步返回找不到容器返回 None构造失败抛异常——
match crate::tiptap_bridge::get_module().create("tiptap-editor", &opts) {
@ -211,8 +214,14 @@ fn write_editor(post_id: Option<i32>) -> Element {
}
editor_content_set.set(true);
}
let handle =
EditorHandle::new(inst, on_update, on_image_upload, on_ready, on_upload_event);
let handle = EditorHandle::new(
inst,
on_update,
on_image_upload,
on_ready,
on_upload_event,
on_run_code,
);
editor.set(Some(handle));
}
Ok(None) => {

View File

@ -126,6 +126,14 @@ pub mod wasm {
/// 上传事件回调coordinator.emit 时触发,携带 counts
#[wasm_bindgen(method, setter, js_name = onUploadEvent)]
pub fn set_on_upload_event(this: &EditorOptions, cb: &Closure<dyn FnMut(UploadEventJs)>);
/// JS 侧 onRunCode: (opts: RunCodeOptsJs) => Promise<string>。
/// Rust closure 返回 js_sys::Promise内部调 start_exec + 轮询 get_exec_result。
#[wasm_bindgen(method, setter, js_name = onRunCode)]
pub fn set_on_run_code(
this: &EditorOptions,
cb: &Closure<dyn Fn(RunCodeOptsJs) -> js_sys::Promise>,
);
}
// —— 上传事件JS UploadEvent 的 Rust 映射)——
@ -172,6 +180,27 @@ pub mod wasm {
pub fn error(this: &UploadCountsJs) -> u32;
}
// —— RunCodeOptsJsonRunCode 回调参数JS 侧传给 Rust 的纯数据对象)——
/// JS 侧传给 onRunCode 的参数对象Rust 侧读取 getter。
/// language 是纯语言名(如 "python",前端已 extractLang 提取overridesJson 是 overrides 的 JSON 字符串(可能为空)。
#[wasm_bindgen]
extern "C" {
pub type RunCodeOptsJs;
/// 纯语言名(前端 extractLang 从完整 info string 提取,如 "python")。
#[wasm_bindgen(method, getter)]
pub fn language(this: &RunCodeOptsJs) -> String;
/// 代码块文本内容。
#[wasm_bindgen(method, getter)]
pub fn source(this: &RunCodeOptsJs) -> String;
/// overrides 的 JSON 字符串(如 `{"timeout_secs":10}`),空串表示无 overrides。
/// 前端从 info string 提取大括号部分Rust 用 serde_json 反序列化。
#[wasm_bindgen(method, getter, js_name = overridesJson)]
pub fn overrides_json(this: &RunCodeOptsJs) -> String;
}
// —— EditorHandle实例 + closure 统一生命周期 ——
/// 持有编辑器实例 + 其全部 closure统一生命周期。
@ -185,13 +214,14 @@ pub mod wasm {
_on_image_upload: Closure<dyn Fn(web_sys::File) -> js_sys::Promise>,
_on_ready: Closure<dyn FnMut()>,
_on_upload_event: Closure<dyn FnMut(UploadEventJs)>,
_on_run_code: Closure<dyn Fn(RunCodeOptsJs) -> js_sys::Promise>,
}
impl EditorHandle {
/// 聚合编辑器实例与四个回调 closure使其共用同一生命周期。
/// 聚合编辑器实例与回调 closure使其共用同一生命周期。
///
/// 调用方负责先用各 setter 把 closure 装进 [`EditorOptions`]、`create` 出实例后,
/// 再把实例与这四个 closure 一并交由本函数持有。返回的 [`EditorHandle`] 一旦
/// 再把实例与这 closure 一并交由本函数持有。返回的 [`EditorHandle`] 一旦
/// drop会先 `destroy` 实例、再按字段逆序 drop closure。
pub fn new(
instance: EditorInstance,
@ -199,6 +229,7 @@ pub mod wasm {
on_image_upload: Closure<dyn Fn(web_sys::File) -> js_sys::Promise>,
on_ready: Closure<dyn FnMut()>,
on_upload_event: Closure<dyn FnMut(UploadEventJs)>,
on_run_code: Closure<dyn Fn(RunCodeOptsJs) -> js_sys::Promise>,
) -> Self {
Self {
instance,
@ -206,6 +237,7 @@ pub mod wasm {
_on_image_upload: on_image_upload,
_on_ready: on_ready,
_on_upload_event: on_upload_event,
_on_run_code: on_run_code,
}
}
@ -348,12 +380,104 @@ pub mod wasm {
})
})
}
// —— make_run_code_closure编辑器内运行代码 ——
/// 把 ExecTask 格式化为结果字符串(供编辑器结果区展示)。
fn format_run_result(task: &crate::api::code_runner::ExecTask) -> String {
use crate::api::code_runner::ExecStatus;
let status_label = match task.status {
ExecStatus::Success => "Success",
ExecStatus::Error => "Error",
ExecStatus::Timeout => "Timeout",
ExecStatus::OomKilled => "OOM",
ExecStatus::Failed => "Failed",
_ => "Unknown",
};
match &task.result {
Some(res) => {
let mut out = format!("状态: {} · 耗时: {}ms", status_label, res.duration_ms);
if !res.stdout.is_empty() {
out.push_str("\nStdout:\n");
out.push_str(&res.stdout);
}
if !res.stderr.is_empty() {
out.push_str("\nStderr:\n");
out.push_str(&res.stderr);
}
out
}
None => format!("状态: {} · {}", status_label, task.stage),
}
}
/// 创建「编辑器内运行代码」closure内部调 start_exec + 轮询 get_exec_result
/// 把格式化结果字符串回传 JS。
///
/// 返回的 closure 签名 `(RunCodeOptsJs) -> Promise` 对应 JS `onRunCode`。
/// JS 侧 NodeView await Promise拿到字符串直接填进结果区 DOM。
///
/// 注意info string 的解析(提取语言名 + overrides JSON在前端 extractLang 完成,
/// Rust 收到的 language 已是纯语言名(如 "python"),不依赖 server-only 的 languages 模块。
pub fn make_run_code_closure() -> Closure<dyn Fn(RunCodeOptsJs) -> js_sys::Promise> {
Closure::new(move |opts: RunCodeOptsJs| -> js_sys::Promise {
wasm_bindgen_futures::future_to_promise(async move {
use crate::api::code_runner::{execute, ExecRequest, ExecStatus};
use crate::infra::runner_config::ResourceLimits;
let language = opts.language();
let source = opts.source();
let overrides_json = opts.overrides_json();
// 反序列化 overrides JSON前端已提取大括号部分空串视为 None
let overrides = if overrides_json.trim().is_empty() {
None
} else {
match serde_json::from_str::<ResourceLimits>(&overrides_json) {
Ok(o) => Some(o),
Err(_) => None, // 畸形 JSON 静默降级为无 overrides
}
};
let req = ExecRequest {
language,
source,
overrides,
};
match execute::start_exec(req).await {
Ok(task_id) => {
let poll_interval = 500;
// 500ms * 60 = 30s 上限(编辑器内运行是写作辅助,比 reader 的 120s 短)
for _ in 0..60 {
crate::utils::time::sleep_ms(poll_interval).await;
match execute::get_exec_result(task_id.clone()).await {
Ok(task) => {
let terminal = task.status != ExecStatus::Queued
&& task.status != ExecStatus::Running;
if terminal {
let s = format_run_result(&task);
return Ok(js_sys::JsString::from(s).into());
}
}
Err(_) => {
return Err(js_sys::Error::new("结果获取异常").into());
}
}
}
Err(js_sys::Error::new("轮询超时,请重试").into())
}
Err(e) => Err(js_sys::Error::new(&e.to_string()).into()),
}
})
})
}
}
/// 将 WASM 子模块中的桥接类型与函数重导出到 crate 根,供 `write.rs` 直接引用。
/// server 构建剥离该子模块,故此重导出仅对 WASM 前端生效。
#[cfg(target_arch = "wasm32")]
pub use wasm::{
consume_upload_event, get_module, make_upload_closure, upload_image_file, EditorHandle,
EditorOptions, UploadEventJs,
consume_upload_event, get_module, make_run_code_closure, make_upload_closure,
upload_image_file, EditorHandle, EditorOptions, RunCodeOptsJs, UploadEventJs,
};