From 0c1e8b7640946a630b6f59b52c54ffb2701e2183 Mon Sep 17 00:00:00 2001 From: xfy Date: Mon, 6 Jul 2026 18:11:28 +0800 Subject: [PATCH] =?UTF-8?q?feat(bridge):=20=E6=96=B0=E5=A2=9E=20make=5Frun?= =?UTF-8?q?=5Fcode=5Fclosure=20=E6=A1=A5=E6=8E=A5=E7=BC=96=E8=BE=91?= =?UTF-8?q?=E5=99=A8=E5=86=85=E8=BF=90=E8=A1=8C=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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)。 --- src/pages/admin/write.rs | 13 +++- src/tiptap_bridge.rs | 132 +++++++++++++++++++++++++++++++++++++-- 2 files changed, 139 insertions(+), 6 deletions(-) diff --git a/src/pages/admin/write.rs b/src/pages/admin/write.rs index e9a9deb..b7d836f 100644 --- a/src/pages/admin/write.rs +++ b/src/pages/admin/write.rs @@ -186,6 +186,8 @@ fn write_editor(post_id: Option) -> Element { consume_upload_event(&ev, uploads_in_flight, upload_errors); } }); + // 运行代码 closure:start_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) -> 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) -> 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) => { diff --git a/src/tiptap_bridge.rs b/src/tiptap_bridge.rs index 2ec10eb..59ed01d 100644 --- a/src/tiptap_bridge.rs +++ b/src/tiptap_bridge.rs @@ -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); + + /// JS 侧 onRunCode: (opts: RunCodeOptsJs) => Promise。 + /// 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 js_sys::Promise>, + ); } // —— 上传事件(JS UploadEvent 的 Rust 映射)—— @@ -172,6 +180,27 @@ pub mod wasm { pub fn error(this: &UploadCountsJs) -> u32; } + // —— RunCodeOptsJs:onRunCode 回调参数(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 js_sys::Promise>, _on_ready: Closure, _on_upload_event: Closure, + _on_run_code: Closure 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 js_sys::Promise>, on_ready: Closure, on_upload_event: Closure, + on_run_code: Closure 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 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::(&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, };