11
This commit is contained in:
@@ -1,21 +1,15 @@
|
||||
//! Generic Tauri commands for invoking any JS script registered in the
|
||||
//! embedded QuickJS runtime. Replaces the per-pipeline command so adding a
|
||||
//! new flow only requires:
|
||||
//! QuickJS 流水线命令:脚本由后端 `/api/v1/quickjs-scripts` 下发,
|
||||
//! 运行时注入 `__native` 后 eval,并调用 `globalThis.__quickjsMain(params)`。
|
||||
//!
|
||||
//! 1. Drop a new `.js` file into `src-tauri/js/`.
|
||||
//! 2. Add it to `js_runtime::SCRIPT_FILES`.
|
||||
//! 3. Inside the JS file, register one or more entries on
|
||||
//! `globalThis.__scripts[<scriptName>]`.
|
||||
//! 前端用法示例:
|
||||
//!
|
||||
//! Frontend usage:
|
||||
//!
|
||||
//! ```ts
|
||||
//! ```ignore
|
||||
//! import { invoke } from "@tauri-apps/api/core";
|
||||
//! import { listen } from "@tauri-apps/api/event";
|
||||
//!
|
||||
//! const off = await listen("quickjs:event", (e) => console.log(e.payload));
|
||||
//! const result = await invoke("run_quickjs_script", {
|
||||
//! scriptName: "process_douyin_share",
|
||||
//! scriptName: "douyin_pipeline.js",
|
||||
//! params: { shareText: "...", modelConfig: {...}, ... },
|
||||
//! });
|
||||
//! off();
|
||||
@@ -48,11 +42,6 @@ pub enum FrontendEvent {
|
||||
|
||||
const EVENT_NAME: &str = "quickjs:event";
|
||||
|
||||
/// Run an arbitrary registered QuickJS script by name.
|
||||
///
|
||||
/// `script_name` must already be registered on `globalThis.__scripts` by
|
||||
/// one of the bundled `js/*.js` files. `params` is forwarded as-is to the
|
||||
/// script entry function.
|
||||
#[tauri::command]
|
||||
pub async fn run_quickjs_script(
|
||||
app: AppHandle,
|
||||
@@ -62,7 +51,6 @@ pub async fn run_quickjs_script(
|
||||
) -> Result<Value, String> {
|
||||
let (tx, mut rx) = mpsc::unbounded_channel::<PipelineEvent>();
|
||||
|
||||
// Forward script events to the frontend window.
|
||||
let app_for_emit = app.clone();
|
||||
let win_label = window.label().to_string();
|
||||
let script_for_emit = script_name.clone();
|
||||
@@ -92,7 +80,7 @@ pub async fn run_quickjs_script(
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Diagnostic helper — returns the list of registered script names.
|
||||
/// 列出后端 `scripts/quickjs` 目录中的 `.js` 文件名。
|
||||
#[tauri::command]
|
||||
pub async fn list_quickjs_scripts() -> Result<Vec<String>, String> {
|
||||
js_runtime::list_scripts().await.map_err(|e| e.to_string())
|
||||
|
||||
@@ -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