This commit is contained in:
fengchuanhn@gmail.com
2026-05-16 12:35:04 +08:00
parent e119327ac2
commit c0ddaceb43
10 changed files with 477 additions and 138 deletions

View File

@@ -1,5 +1,6 @@
//! QuickJS 流水线命令:脚本由后端 `/api/v1/quickjs-scripts` 下发,
//! 运行时注入 `__native` 后 eval并调用 `globalThis.__quickjsMain(params)`。
//! 调试命令 `run_quickjs_script_source` 则将源码当作异步函数体直接执行,并注入变量 `params`。
//!
//! 前端用法示例:
//!
@@ -80,6 +81,45 @@ pub async fn run_quickjs_script(
Ok(result)
}
/// 调试:直接执行传入的 QuickJS 源码(异步函数体),可使用 `__native`、`params`、`return`,无需 `__quickjsMain`。
#[tauri::command]
pub async fn run_quickjs_script_source(
app: AppHandle,
window: Window,
script_source: String,
params: Value,
) -> Result<Value, String> {
let (tx, mut rx) = mpsc::unbounded_channel::<PipelineEvent>();
let app_for_emit = app.clone();
let win_label = window.label().to_string();
const EMIT_LABEL: &str = "<debug-source>";
let pump = tokio::spawn(async move {
while let Some(ev) = rx.recv().await {
let payload: FrontendEvent = match ev {
PipelineEvent::Progress(s) => FrontendEvent::Progress {
script_name: EMIT_LABEL.to_string(),
status: s,
},
PipelineEvent::Log { level, msg, fields } => FrontendEvent::Log {
script_name: EMIT_LABEL.to_string(),
level,
msg,
fields,
},
};
let _ = app_for_emit.emit_to(&win_label, EVENT_NAME, payload);
}
});
let result = js_runtime::run_script_source(script_source, params, tx)
.await
.map_err(|e| e.to_string())?;
pump.await.ok();
Ok(result)
}
/// 列出后端 `scripts/quickjs` 目录中的 `.js` 文件名。
#[tauri::command]
pub async fn list_quickjs_scripts() -> Result<Vec<String>, String> {

View File

@@ -1,5 +1,6 @@
//! Embedded QuickJS runtime脚本源码由 **Python 后端** 按文件名下发,
//! 桌面端通过 HTTP 拉取后在运行时 `eval`,入口为单一的 `globalThis.__quickjsMain`。
//! 或由调试接口直接传入字符串:**调试路径**下整段源码作为异步函数体执行,并注入变量 `params`
//! **服务端脚本路径**仍在 eval 后调用 `globalThis.__quickjsMain(params)`。
//!
//! 后端:`GET /api/v1/quickjs-scripts?name=<文件名.js>``ApiResponse``data.source`)。
//! API 基址:`AICLIENT_API_BASE` 环境变量,默认 `http://127.0.0.1:8001`。
@@ -20,6 +21,9 @@ const ENTRY_GLOBAL: &str = "__quickjsMain";
const RESET_ENTRY_JS: &str = "globalThis.__quickjsMain = undefined;";
/// 调试执行路径:暂存 invoke 传入的 `params`eval 前写入 globalThis立即读入局部变量 `params` 后删除。
const INLINE_PARAMS_GLOBAL: &str = "__quickjsInlineParams";
#[derive(Debug, Error)]
pub enum JsError {
#[error("rquickjs: {0}")]
@@ -94,29 +98,34 @@ pub enum PipelineEvent {
},
}
/// 从服务端拉取 `script_name` 对应源码,执行 `__quickjsMain(params)` 并返回 JSON 结果。
pub async fn run_script(
script_name: String,
async fn eval_quickjs_main_entry(
source: String,
eval_label: String,
params: Value,
events: UnboundedSender<PipelineEvent>,
) -> Result<Value, JsError> {
let source = fetch_script_source(&script_name).await?;
let bridge = Arc::new(NativeBridge::new(events));
let runtime = AsyncRuntime::new()?;
let context = AsyncContext::full(&runtime).await?;
let label_for_eval = eval_label.clone();
let label_for_await = eval_label;
let result_json: Value = async_with!(context => |ctx| {
let native_obj = native::build_native_object(ctx.clone(), bridge.clone())
.map_err(|e| JsError::QuickJs(format!("install native: {e}")))?;
ctx.globals().set("__native", native_obj)
.map_err(|e| JsError::QuickJs(format!("set __native: {e}")))?;
// async 类原生函数依赖全局 __native须在挂到 globalThis 之后再包一层解析 JSON / 抛错
let _: () = ctx.eval(native::NATIVE_ASYNC_SHIM).catch(&ctx)
.map_err(|e| JsError::QuickJs(format!("native async shim: {e}")))?;
let _: () = ctx.eval(RESET_ENTRY_JS).catch(&ctx)
.map_err(|e| JsError::QuickJs(format!("reset entry: {e}")))?;
let _: () = ctx.eval(source.as_str()).catch(&ctx)
.map_err(|e| JsError::QuickJs(format!("eval {}: {e}", script_name)))?;
.map_err(|e| JsError::QuickJs(format!("eval {label_for_eval}: {e}")))?;
let main_fn: rquickjs::Function = ctx.globals().get(ENTRY_GLOBAL)
.map_err(|_| JsError::MissingMainEntry)?;
@@ -130,7 +139,7 @@ pub async fn run_script(
.map_err(|e| JsError::QuickJs(format!("call __quickjsMain: {e}")))?;
let value: rquickjs::Value = match Promise::from_value(raw.clone()) {
Ok(p) => p.into_future().await
.map_err(|e| JsError::QuickJs(format!("await {}: {e}", script_name)))?,
.map_err(|e| JsError::QuickJs(format!("await {label_for_await}: {e}")))?,
Err(_) => raw,
};
@@ -148,6 +157,89 @@ pub async fn run_script(
Ok(result_json)
}
/// 调试:`source` 作为异步函数体执行,可使用 `params`、`await`、`return`;不要求 `__quickjsMain`。
async fn eval_quickjs_inline_source(
source: String,
params: Value,
events: UnboundedSender<PipelineEvent>,
) -> Result<Value, JsError> {
let bridge = Arc::new(NativeBridge::new(events));
let runtime = AsyncRuntime::new()?;
let context = AsyncContext::full(&runtime).await?;
let wrapped = format!(
r#"(async () => {{
const params = globalThis.{global};
try {{ delete globalThis.{global}; }} catch (_) {{}}
{user}
}})()"#,
global = INLINE_PARAMS_GLOBAL,
user = source,
);
let result_json: Value = async_with!(context => |ctx| {
let native_obj = native::build_native_object(ctx.clone(), bridge.clone())
.map_err(|e| JsError::QuickJs(format!("install native: {e}")))?;
ctx.globals().set("__native", native_obj)
.map_err(|e| JsError::QuickJs(format!("set __native: {e}")))?;
let _: () = ctx.eval(native::NATIVE_ASYNC_SHIM).catch(&ctx)
.map_err(|e| JsError::QuickJs(format!("native async shim: {e}")))?;
let _: () = ctx.eval(RESET_ENTRY_JS).catch(&ctx)
.map_err(|e| JsError::QuickJs(format!("reset entry: {e}")))?;
let params_str = serde_json::to_string(&params)
.map_err(|e| JsError::QuickJs(format!("serialize params: {e}")))?;
let params_value: rquickjs::Value = ctx.json_parse(params_str)
.map_err(|e| JsError::QuickJs(format!("parse params: {e}")))?;
ctx.globals().set(INLINE_PARAMS_GLOBAL, params_value).map_err(|e| {
JsError::QuickJs(format!("set {}: {e}", INLINE_PARAMS_GLOBAL))
})?;
let raw: rquickjs::Value = ctx.eval(wrapped.as_str()).catch(&ctx)
.map_err(|e| JsError::QuickJs(format!("eval <inline>: {e}")))?;
let value: rquickjs::Value = match Promise::from_value(raw.clone()) {
Ok(p) => p.into_future().await
.map_err(|e| JsError::QuickJs(format!("await <inline>: {e}")))?,
Err(_) => raw,
};
let json_global: rquickjs::Object = ctx.globals().get("JSON")
.map_err(|e| JsError::QuickJs(format!("get JSON: {e}")))?;
let stringify: rquickjs::Function = json_global.get("stringify")
.map_err(|e| JsError::QuickJs(format!("get JSON.stringify: {e}")))?;
let s: String = stringify.call((value,))
.map_err(|e| JsError::QuickJs(format!("stringify result: {e}")))?;
let parsed: Value = serde_json::from_str(&s)?;
Ok::<Value, JsError>(parsed)
})
.await?;
Ok(result_json)
}
/// 从服务端拉取 `script_name` 对应源码,执行 `__quickjsMain(params)` 并返回 JSON 结果。
pub async fn run_script(
script_name: String,
params: Value,
events: UnboundedSender<PipelineEvent>,
) -> Result<Value, JsError> {
let source = fetch_script_source(&script_name).await?;
eval_quickjs_main_entry(source, script_name, params, events).await
}
/// 调试执行:整段 `source` 作为异步函数体运行,注入 `params`,无需 `__quickjsMain`。
pub async fn run_script_source(
source: String,
params: Value,
events: UnboundedSender<PipelineEvent>,
) -> Result<Value, JsError> {
eval_quickjs_inline_source(source, params, events).await
}
/// 当前服务端 `scripts/quickjs` 目录下的 `.js` 文件名列表。
pub async fn list_scripts() -> Result<Vec<String>, JsError> {
let base = api_base().trim_end_matches('/').to_string();

View File

@@ -6,8 +6,8 @@
//! and may panic-free `Func::from` directly.
//! - Async helpers always return a `String` (JSON-encoded result).
//! Failures are encoded as `{ "__error": "..." }`; a JS shim
//! installed at the end of `build_native_object` detects that shape
//! and `throw`s, so JS callers can use natural `try/catch`.
//! installed **after** `__native` is assigned on `globalThis` (see `mod.rs`)
//! detects that shape and `throw`s, so JS callers can use natural `try/catch`.
//! - On success, the JSON string is parsed by the same JS shim and
//! returned to the caller as a plain object.
@@ -26,6 +26,27 @@ use uuid::Uuid;
use super::PipelineEvent;
use crate::{asr, chat, ffmpeg, http, oss};
/// 必须在 `globalThis.__native` 已赋值之后执行,否则会访问未定义的 `__native`。
pub const NATIVE_ASYNC_SHIM: &str = r#"
(function () {
const wrap = (name) => {
const orig = __native[name];
__native[name] = async function (...args) {
const s = await orig.apply(__native, args);
if (typeof s !== "string") return s;
let v;
try { v = JSON.parse(s); } catch (_) { return s; }
if (v && typeof v === "object" && typeof v.__error === "string") {
throw new Error(v.__error);
}
return v;
};
};
["httpRequest", "ffmpegExtractAudio", "aliyunOssUpload",
"aliyunAsrFiletrans", "localAsr", "openaiChat"].forEach(wrap);
})();
"#;
/// Shared host state passed by reference into every native function.
pub struct NativeBridge {
pub events: UnboundedSender<PipelineEvent>,
@@ -267,29 +288,5 @@ pub fn build_native_object<'js>(
})),
)?;
// The async functions above all return JSON strings. Wrap each one in
// a JS shim that JSON.parses the result and throws when the payload
// carries the `__error` discriminator.
let shim = r#"
(function () {
const wrap = (name) => {
const orig = __native[name];
__native[name] = async function (...args) {
const s = await orig.apply(__native, args);
if (typeof s !== "string") return s;
let v;
try { v = JSON.parse(s); } catch (_) { return s; }
if (v && typeof v === "object" && typeof v.__error === "string") {
throw new Error(v.__error);
}
return v;
};
};
["httpRequest", "ffmpegExtractAudio", "aliyunOssUpload",
"aliyunAsrFiletrans", "localAsr", "openaiChat"].forEach(wrap);
})();
"#;
let _: () = ctx.eval(shim)?;
Ok(obj)
}

View File

@@ -26,6 +26,7 @@ pub fn run() {
.invoke_handler(tauri::generate_handler![
commands::auth::sync_auth_session,
commands::quickjs::run_quickjs_script,
commands::quickjs::run_quickjs_script_source,
commands::quickjs::list_quickjs_scripts,
])
.run(tauri::generate_context!())