11
This commit is contained in:
@@ -1,14 +1,8 @@
|
||||
//! Embedded QuickJS runtime that runs *named* JS scripts.
|
||||
//! Embedded QuickJS runtime:脚本源码由 **Python 后端** 按文件名下发,
|
||||
//! 桌面端通过 HTTP 拉取后在运行时 `eval`,入口为单一的 `globalThis.__quickjsMain`。
|
||||
//!
|
||||
//! Each script file under `src-tauri/js/*.js` is compiled into the binary
|
||||
//! via `include_str!`. At startup the runtime evaluates all of them in
|
||||
//! order; each file is expected to register one or more handlers into the
|
||||
//! global `__scripts` table:
|
||||
//!
|
||||
//! globalThis.__scripts = globalThis.__scripts || {};
|
||||
//! globalThis.__scripts["my_script"] = function (params) { ... };
|
||||
//!
|
||||
//! The Rust side then resolves `__scripts[name]` at invocation time.
|
||||
//! 后端:`GET /api/v1/quickjs-scripts?name=<文件名.js>`(`ApiResponse`,`data.source`)。
|
||||
//! API 基址:`AICLIENT_API_BASE` 环境变量,默认 `http://127.0.0.1:8001`。
|
||||
|
||||
pub mod native;
|
||||
|
||||
@@ -21,18 +15,10 @@ use tokio::sync::mpsc::UnboundedSender;
|
||||
|
||||
use native::NativeBridge;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Bundled script registry
|
||||
//
|
||||
// Add a new line here whenever you drop a new file into `src-tauri/js/`.
|
||||
// The order matters only if scripts depend on each other; ours don't.
|
||||
// ---------------------------------------------------------------------------
|
||||
const BOOTSTRAP_JS: &str = "globalThis.__scripts = globalThis.__scripts || {};";
|
||||
/// QuickJS 侧约定的唯一入口(服务端下发的脚本末尾应赋值此全局)。
|
||||
const ENTRY_GLOBAL: &str = "__quickjsMain";
|
||||
|
||||
const SCRIPT_FILES: &[(&str, &str)] = &[
|
||||
("douyin_pipeline.js", include_str!("../../js/douyin_pipeline.js")),
|
||||
// ("my_next_pipeline.js", include_str!("../../js/my_next_pipeline.js")),
|
||||
];
|
||||
const RESET_ENTRY_JS: &str = "globalThis.__quickjsMain = undefined;";
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum JsError {
|
||||
@@ -40,12 +26,61 @@ pub enum JsError {
|
||||
QuickJs(String),
|
||||
#[error("serde: {0}")]
|
||||
Serde(#[from] serde_json::Error),
|
||||
#[error("unknown script: {0}")]
|
||||
UnknownScript(String),
|
||||
#[error("拉取脚本失败: {0}")]
|
||||
FetchScript(String),
|
||||
#[error("脚本未注册全局入口 __quickjsMain")]
|
||||
MissingMainEntry,
|
||||
}
|
||||
|
||||
impl From<rquickjs::Error> for JsError {
|
||||
fn from(e: rquickjs::Error) -> Self { JsError::QuickJs(e.to_string()) }
|
||||
fn from(e: rquickjs::Error) -> Self {
|
||||
JsError::QuickJs(e.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
fn api_base() -> String {
|
||||
std::env::var("AICLIENT_API_BASE").unwrap_or_else(|_| "http://127.0.0.1:8001".to_string())
|
||||
}
|
||||
|
||||
async fn fetch_script_source(script_name: &str) -> Result<String, JsError> {
|
||||
let base = api_base().trim_end_matches('/').to_string();
|
||||
let q = urlencoding::encode(script_name);
|
||||
let url = format!("{base}/api/v1/quickjs-scripts?name={q}");
|
||||
let client = reqwest::Client::builder()
|
||||
.timeout(std::time::Duration::from_secs(120))
|
||||
.build()
|
||||
.map_err(|e| JsError::FetchScript(e.to_string()))?;
|
||||
|
||||
let res = client
|
||||
.get(&url)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| JsError::FetchScript(e.to_string()))?;
|
||||
|
||||
let body = res
|
||||
.text()
|
||||
.await
|
||||
.map_err(|e| JsError::FetchScript(e.to_string()))?;
|
||||
|
||||
let v: Value =
|
||||
serde_json::from_str(&body).map_err(|e| JsError::FetchScript(format!("响应非 JSON: {e}")))?;
|
||||
|
||||
let ok = v.get("ok").and_then(|x| x.as_bool()).unwrap_or(false);
|
||||
if !ok {
|
||||
let msg = v
|
||||
.get("message")
|
||||
.and_then(|x| x.as_str())
|
||||
.unwrap_or("未知错误");
|
||||
return Err(JsError::FetchScript(msg.to_string()));
|
||||
}
|
||||
|
||||
let source = v
|
||||
.get("data")
|
||||
.and_then(|d| d.get("source"))
|
||||
.and_then(|s| s.as_str())
|
||||
.ok_or_else(|| JsError::FetchScript("响应缺少 data.source".into()))?;
|
||||
|
||||
Ok(source.to_string())
|
||||
}
|
||||
|
||||
/// Progress / log message produced by a running script.
|
||||
@@ -59,57 +94,46 @@ pub enum PipelineEvent {
|
||||
},
|
||||
}
|
||||
|
||||
/// Execute the script registered under `script_name` and return its result.
|
||||
/// 从服务端拉取 `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?;
|
||||
|
||||
let bridge = Arc::new(NativeBridge::new(events));
|
||||
|
||||
let runtime = AsyncRuntime::new()?;
|
||||
let context = AsyncContext::full(&runtime).await?;
|
||||
|
||||
let result_json: Value = async_with!(context => |ctx| {
|
||||
// 1) install the __native bridge
|
||||
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}")))?;
|
||||
|
||||
// 2) bootstrap + evaluate every registered script file
|
||||
let _: () = ctx.eval(BOOTSTRAP_JS).catch(&ctx)
|
||||
.map_err(|e| JsError::QuickJs(format!("eval bootstrap: {e}")))?;
|
||||
for (file_name, source) in SCRIPT_FILES {
|
||||
let _: () = ctx.eval(*source).catch(&ctx)
|
||||
.map_err(|e| JsError::QuickJs(format!("eval {file_name}: {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)))?;
|
||||
|
||||
// 3) look up __scripts[script_name]
|
||||
let scripts: rquickjs::Object = ctx.globals().get("__scripts")
|
||||
.map_err(|e| JsError::QuickJs(format!("get __scripts: {e}")))?;
|
||||
let entry: rquickjs::Function = scripts
|
||||
.get(script_name.as_str())
|
||||
.map_err(|_| JsError::UnknownScript(script_name.clone()))?;
|
||||
let main_fn: rquickjs::Function = ctx.globals().get(ENTRY_GLOBAL)
|
||||
.map_err(|_| JsError::MissingMainEntry)?;
|
||||
|
||||
// 4) call entry(params)
|
||||
let params_str = serde_json::to_string(¶ms)
|
||||
.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}")))?;
|
||||
|
||||
// The entry may be sync (returns Value) or async (returns Promise).
|
||||
// Cast the return value to Value first, then promote to Promise if it
|
||||
// *is* a Promise; otherwise use it as-is.
|
||||
let raw: rquickjs::Value = entry.call((params_value,))
|
||||
.map_err(|e| JsError::QuickJs(format!("call entry: {e}")))?;
|
||||
let raw: rquickjs::Value = main_fn.call((params_value,))
|
||||
.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 {script_name}: {e}")))?,
|
||||
.map_err(|e| JsError::QuickJs(format!("await {}: {e}", script_name)))?,
|
||||
Err(_) => raw,
|
||||
};
|
||||
|
||||
// 5) JS value -> serde_json::Value via JSON.stringify
|
||||
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")
|
||||
@@ -124,24 +148,47 @@ pub async fn run_script(
|
||||
Ok(result_json)
|
||||
}
|
||||
|
||||
/// Best-effort list of names currently registered in `__scripts`.
|
||||
/// Useful for diagnostics / a `list_quickjs_scripts` command.
|
||||
/// 当前服务端 `scripts/quickjs` 目录下的 `.js` 文件名列表。
|
||||
pub async fn list_scripts() -> Result<Vec<String>, JsError> {
|
||||
let runtime = AsyncRuntime::new()?;
|
||||
let context = AsyncContext::full(&runtime).await?;
|
||||
let names: Vec<String> = async_with!(context => |ctx| {
|
||||
let _: () = ctx.eval(BOOTSTRAP_JS).catch(&ctx)
|
||||
.map_err(|e| JsError::QuickJs(format!("bootstrap: {e}")))?;
|
||||
for (file_name, src) in SCRIPT_FILES {
|
||||
let _: () = ctx.eval(*src).catch(&ctx)
|
||||
.map_err(|e| JsError::QuickJs(format!("eval {file_name}: {e}")))?;
|
||||
}
|
||||
let keys: String = ctx.eval(r#"Object.keys(globalThis.__scripts || {}).join(",")"#)
|
||||
.map_err(|e| JsError::QuickJs(format!("collect keys: {e}")))?;
|
||||
Ok::<Vec<String>, JsError>(
|
||||
keys.split(',').filter(|s| !s.is_empty()).map(|s| s.to_string()).collect()
|
||||
)
|
||||
})
|
||||
.await?;
|
||||
let base = api_base().trim_end_matches('/').to_string();
|
||||
let url = format!("{base}/api/v1/quickjs-scripts/list");
|
||||
let client = reqwest::Client::builder()
|
||||
.timeout(std::time::Duration::from_secs(30))
|
||||
.build()
|
||||
.map_err(|e| JsError::FetchScript(e.to_string()))?;
|
||||
|
||||
let res = client
|
||||
.get(&url)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| JsError::FetchScript(e.to_string()))?;
|
||||
|
||||
let body = res
|
||||
.text()
|
||||
.await
|
||||
.map_err(|e| JsError::FetchScript(e.to_string()))?;
|
||||
|
||||
let v: Value =
|
||||
serde_json::from_str(&body).map_err(|e| JsError::FetchScript(format!("响应非 JSON: {e}")))?;
|
||||
|
||||
if !v.get("ok").and_then(|x| x.as_bool()).unwrap_or(false) {
|
||||
let msg = v
|
||||
.get("message")
|
||||
.and_then(|x| x.as_str())
|
||||
.unwrap_or("未知错误");
|
||||
return Err(JsError::FetchScript(msg.to_string()));
|
||||
}
|
||||
|
||||
let names = v
|
||||
.get("data")
|
||||
.and_then(|d| d.get("names"))
|
||||
.and_then(|n| n.as_array())
|
||||
.map(|arr| {
|
||||
arr.iter()
|
||||
.filter_map(|x| x.as_str().map(|s| s.to_string()))
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
Ok(names)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user