1
This commit is contained in:
180
src-tauri/src/app_config.rs
Normal file
180
src-tauri/src/app_config.rs
Normal file
@@ -0,0 +1,180 @@
|
||||
//! 全局应用配置:登录后从 `GET /api/v1/appConfig` 拉取 `desktop_configs` 键值表,
|
||||
//! 原样缓存;启动 Node 时写入环境变量 `AICLIENT_CFG_<规范化键名>`。
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use serde_json::Value;
|
||||
use thiserror::Error;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
/// Node 子进程环境变量前缀,与 `scripts/nodejs/desktop_config.js` 一致。
|
||||
pub const NODE_ENV_PREFIX: &str = "AICLIENT_CFG_";
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum AppConfigError {
|
||||
#[error("拉取配置失败: {0}")]
|
||||
Fetch(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct Inner {
|
||||
entries: HashMap<String, String>,
|
||||
last_message: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct AppConfig {
|
||||
inner: Arc<RwLock<Inner>>,
|
||||
}
|
||||
|
||||
pub fn api_base() -> String {
|
||||
std::env::var("AICLIENT_API_BASE").unwrap_or_else(|_| "http://127.0.0.1:8001".to_string())
|
||||
}
|
||||
|
||||
/// 将库表 `name` 转为 Node 环境变量名(不含前缀),如 `llm.api_key` → `LLM_API_KEY`。
|
||||
pub fn normalize_config_key(name: &str) -> String {
|
||||
let mut out = String::new();
|
||||
let mut prev_underscore = false;
|
||||
for c in name.trim().chars() {
|
||||
if c.is_ascii_alphanumeric() {
|
||||
out.push(c.to_ascii_uppercase());
|
||||
prev_underscore = false;
|
||||
} else if !prev_underscore {
|
||||
out.push('_');
|
||||
prev_underscore = true;
|
||||
}
|
||||
}
|
||||
while out.starts_with('_') {
|
||||
out.remove(0);
|
||||
}
|
||||
while out.ends_with('_') {
|
||||
out.pop();
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// 完整环境变量名:`AICLIENT_CFG_LLM_API_KEY`
|
||||
pub fn node_env_var_name(db_name: &str) -> String {
|
||||
format!("{}{}", NODE_ENV_PREFIX, normalize_config_key(db_name))
|
||||
}
|
||||
|
||||
impl AppConfig {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
inner: Arc::new(RwLock::new(Inner::default())),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn entries(&self) -> HashMap<String, String> {
|
||||
self.inner.read().await.entries.clone()
|
||||
}
|
||||
|
||||
pub async fn get(&self, name: &str) -> Option<String> {
|
||||
self.inner.read().await.entries.get(name).cloned()
|
||||
}
|
||||
|
||||
pub async fn last_message(&self) -> String {
|
||||
self.inner.read().await.last_message.clone()
|
||||
}
|
||||
|
||||
pub async fn clear(&self) {
|
||||
let mut g = self.inner.write().await;
|
||||
g.entries.clear();
|
||||
g.last_message.clear();
|
||||
}
|
||||
|
||||
/// 供 Node 子进程 `Command::env` 使用。
|
||||
pub async fn env_for_node(&self) -> HashMap<String, String> {
|
||||
let g = self.inner.read().await;
|
||||
g.entries
|
||||
.iter()
|
||||
.map(|(name, value)| (node_env_var_name(name), value.clone()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub async fn refresh_after_auth(&self, token: &str) -> Result<(), AppConfigError> {
|
||||
if token.trim().is_empty() {
|
||||
self.clear().await;
|
||||
return Ok(());
|
||||
}
|
||||
match fetch_from_server(token).await {
|
||||
Ok(map) => {
|
||||
let mut g = self.inner.write().await;
|
||||
g.entries = map;
|
||||
g.last_message.clear();
|
||||
log::info!(
|
||||
target: "app_config",
|
||||
"应用配置已更新,共 {} 项",
|
||||
g.entries.len()
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
let mut g = self.inner.write().await;
|
||||
g.last_message = e.to_string();
|
||||
log::warn!(target: "app_config", "拉取应用配置失败: {}", e);
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn fetch_from_server(token: &str) -> Result<HashMap<String, String>, AppConfigError> {
|
||||
let base = api_base().trim_end_matches('/').to_string();
|
||||
let url = format!("{base}/api/v1/appConfig");
|
||||
|
||||
let client = reqwest::Client::builder()
|
||||
.timeout(std::time::Duration::from_secs(30))
|
||||
.build()
|
||||
.map_err(|e| AppConfigError::Fetch(e.to_string()))?;
|
||||
|
||||
let res = client
|
||||
.get(&url)
|
||||
.header("Authorization", format!("Bearer {}", token.trim()))
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| AppConfigError::Fetch(e.to_string()))?;
|
||||
|
||||
let status = res.status();
|
||||
let body = res
|
||||
.text()
|
||||
.await
|
||||
.map_err(|e| AppConfigError::Fetch(e.to_string()))?;
|
||||
|
||||
if status == reqwest::StatusCode::UNAUTHORIZED {
|
||||
return Err(AppConfigError::Fetch("未认证或令牌已失效".into()));
|
||||
}
|
||||
|
||||
let v: Value =
|
||||
serde_json::from_str(&body).map_err(|e| AppConfigError::Fetch(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(AppConfigError::Fetch(msg.to_string()));
|
||||
}
|
||||
|
||||
let data = v
|
||||
.get("data")
|
||||
.ok_or_else(|| AppConfigError::Fetch("响应缺少 data".into()))?;
|
||||
|
||||
match data {
|
||||
Value::Object(obj) => {
|
||||
let mut map = HashMap::new();
|
||||
for (k, val) in obj {
|
||||
let s = match val {
|
||||
Value::String(s) => s.clone(),
|
||||
Value::Null => String::new(),
|
||||
other => other.to_string(),
|
||||
};
|
||||
map.insert(k.clone(), s);
|
||||
}
|
||||
Ok(map)
|
||||
}
|
||||
_ => Err(AppConfigError::Fetch("data 应为 JSON 对象".into())),
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ use std::sync::Mutex;
|
||||
pub struct AuthUser {
|
||||
pub id: i64,
|
||||
pub username: String,
|
||||
pub role_id: i64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
|
||||
19
src-tauri/src/commands/app_config.rs
Normal file
19
src-tauri/src/commands/app_config.rs
Normal file
@@ -0,0 +1,19 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use tauri::State;
|
||||
|
||||
use crate::app_config::AppConfig;
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_app_config(
|
||||
app_config: State<'_, AppConfig>,
|
||||
) -> Result<HashMap<String, String>, String> {
|
||||
Ok(app_config.entries().await)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_app_config_last_message(
|
||||
app_config: State<'_, AppConfig>,
|
||||
) -> Result<String, String> {
|
||||
Ok(app_config.last_message().await)
|
||||
}
|
||||
@@ -1,12 +1,18 @@
|
||||
use tauri::State;
|
||||
|
||||
use crate::app_config::AppConfig;
|
||||
use crate::auth_session::{AuthSession, AuthUser};
|
||||
|
||||
#[tauri::command]
|
||||
pub fn sync_auth_session(
|
||||
pub async fn sync_auth_session(
|
||||
session: State<'_, AuthSession>,
|
||||
app_config: State<'_, AppConfig>,
|
||||
token: String,
|
||||
user: Option<AuthUser>,
|
||||
) {
|
||||
session.set(token, user);
|
||||
) -> Result<(), String> {
|
||||
session.set(token.clone(), user);
|
||||
if let Err(e) = app_config.refresh_after_auth(&token).await {
|
||||
log::debug!(target: "auth", "refresh app config: {}", e);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod app_config;
|
||||
pub mod auth;
|
||||
pub mod nodejs;
|
||||
pub mod quickjs;
|
||||
|
||||
@@ -26,6 +26,7 @@ use serde_json::Value;
|
||||
use tauri::{AppHandle, Emitter, Window};
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
use crate::app_config::AppConfig;
|
||||
use crate::js_runtime::PipelineEvent;
|
||||
use crate::nodejs;
|
||||
|
||||
@@ -80,13 +81,15 @@ fn spawn_event_pump(
|
||||
pub async fn run_nodejs_script(
|
||||
app: AppHandle,
|
||||
window: Window,
|
||||
app_config: tauri::State<'_, AppConfig>,
|
||||
script_name: String,
|
||||
params: Value,
|
||||
) -> Result<Value, String> {
|
||||
let (tx, rx) = mpsc::unbounded_channel::<PipelineEvent>();
|
||||
let pump = spawn_event_pump(app, window, script_name.clone(), rx);
|
||||
let config_env = app_config.env_for_node().await;
|
||||
|
||||
let result = nodejs::run_node_script(script_name, params, tx)
|
||||
let result = nodejs::run_node_script(script_name, params, tx, config_env)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
@@ -99,13 +102,15 @@ pub async fn run_nodejs_script(
|
||||
pub async fn run_nodejs_script_source(
|
||||
app: AppHandle,
|
||||
window: Window,
|
||||
app_config: tauri::State<'_, AppConfig>,
|
||||
script_source: String,
|
||||
params: Value,
|
||||
) -> Result<Value, String> {
|
||||
let (tx, rx) = mpsc::unbounded_channel::<PipelineEvent>();
|
||||
let pump = spawn_event_pump(app, window, "<debug-source>".to_string(), rx);
|
||||
let config_env = app_config.env_for_node().await;
|
||||
|
||||
let result = nodejs::run_node_script_source(script_source, params, tx)
|
||||
let result = nodejs::run_node_script_source(script_source, params, tx, config_env)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod app_config;
|
||||
pub mod asr;
|
||||
pub mod auth_session;
|
||||
pub mod chat;
|
||||
@@ -16,6 +17,7 @@ pub fn run() {
|
||||
|
||||
tauri::Builder::default()
|
||||
.manage(auth_session::AuthSession::default())
|
||||
.manage(app_config::AppConfig::new())
|
||||
.plugin(tauri_plugin_single_instance::init(|app, _args, _cwd| {
|
||||
if let Some(window) = app.get_webview_window("main") {
|
||||
let _ = window.unminimize();
|
||||
@@ -26,6 +28,8 @@ pub fn run() {
|
||||
.plugin(tauri_plugin_opener::init())
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
commands::auth::sync_auth_session,
|
||||
commands::app_config::get_app_config,
|
||||
commands::app_config::get_app_config_last_message,
|
||||
commands::quickjs::run_quickjs_script,
|
||||
commands::quickjs::run_quickjs_script_source,
|
||||
commands::quickjs::list_quickjs_scripts,
|
||||
|
||||
@@ -8,16 +8,21 @@
|
||||
//! - 进度 / 日志由用户脚本通过 `globalThis.__native.emitProgress(s)`
|
||||
//! / `__native.log(level, msg, fields)` 写到 stderr,Rust 这边解析后
|
||||
//! 转成 `PipelineEvent` 推给前端;
|
||||
//! - 用户脚本的最终结果通过赋值 / `return` 的方式(约定函数名为
|
||||
//! `globalThis.__nodejsMain(params)`)返回,runner 把 JSON.stringify 后
|
||||
//! 的结果以 `__RESULT__<json>` 这一行写到 stdout,Rust 抓回来反序列化。
|
||||
//! - 用户脚本通过 `require('./xxx.js')` 引用同目录模块;Rust 在启动前
|
||||
//! 按 require 图从服务端递归拉取并写入临时 bundle,Node 原生 CommonJS
|
||||
//! `require` 加载(比 ESM `import` 更简单,与 `.cjs` runner 一致);
|
||||
//! - 用户脚本的最终结果通过 `globalThis.__nodejsMain(params)` 返回,runner
|
||||
//! 把 JSON.stringify 后的结果以 `__RESULT__<json>` 写到 stdout。
|
||||
//!
|
||||
//! 这套协议有意保持极小:跟 QuickJS 的 `globalThis.__quickjsMain` / `__native`
|
||||
//! 看起来完全对称,方便用户在两种运行时之间迁移脚本。
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::collections::HashMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Stdio;
|
||||
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
use serde_json::Value;
|
||||
use thiserror::Error;
|
||||
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
|
||||
@@ -27,6 +32,15 @@ use uuid::Uuid;
|
||||
|
||||
use crate::js_runtime::PipelineEvent;
|
||||
|
||||
/// 与后端 `nodejs_scripts._SAFE_NAME` 一致:仅允许扁平 `.js` 文件名。
|
||||
static RE_SAFE_SCRIPT: Lazy<Regex> =
|
||||
Lazy::new(|| Regex::new(r"^[a-zA-Z0-9][a-zA-Z0-9._-]*\.js$").unwrap());
|
||||
|
||||
static RE_REQUIRE: Lazy<Regex> =
|
||||
Lazy::new(|| Regex::new(r#"require\s*\(\s*['"]([^'"]+\.js)['"]\s*\)"#).unwrap());
|
||||
|
||||
const INLINE_ENTRY: &str = "__inline_entry__.js";
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum NodeError {
|
||||
#[error("node.exe 未找到(请检查 resources/nodejs/ 是否存在)")]
|
||||
@@ -76,91 +90,168 @@ pub fn locate_node() -> Option<PathBuf> {
|
||||
Some(PathBuf::from(exe_name()))
|
||||
}
|
||||
|
||||
/// 用于设置 `cwd`:让用户脚本里的 `require('xxx')` 能解析到
|
||||
/// `resources/nodejs/node_modules/` 内置依赖。
|
||||
/// 用于 `NODE_PATH`:让用户脚本里的 `require('puppeteer')` 等解析到
|
||||
/// `resources/nodejs/node_modules/`(需在该目录执行 `npm install`)。
|
||||
pub fn nodejs_dir() -> Option<PathBuf> {
|
||||
locate_node().and_then(|p| p.parent().map(|x| x.to_path_buf()))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Runner 模板
|
||||
//
|
||||
// HEADER 安装 __native 桥,开始 IIFE 异步函数;
|
||||
// 用户脚本原样夹在中间(通常用户脚本里会赋值 globalThis.__nodejsMain);
|
||||
// FOOTER 调用 __nodejsMain(params)、捕获异常、写 __RESULT__。
|
||||
// Runner 模板(文件模式:入口脚本与 require 依赖均落盘,Node 原生 require)
|
||||
// ---------------------------------------------------------------------------
|
||||
const RUNNER_HEADER: &str = r#"
|
||||
fn build_file_runner(entry_script: &str) -> String {
|
||||
format!(
|
||||
r#"
|
||||
// ---- aiclient auto-generated nodejs runner (do not edit) ----
|
||||
"use strict";
|
||||
(function () {
|
||||
function emit(obj) {
|
||||
try {
|
||||
(function () {{
|
||||
function emit(obj) {{
|
||||
try {{
|
||||
process.stderr.write("__EVT__" + JSON.stringify(obj) + "\n");
|
||||
} catch (_) {}
|
||||
}
|
||||
globalThis.__native = {
|
||||
emitProgress(status) {
|
||||
emit({ type: "progress", status: String(status) });
|
||||
},
|
||||
log(level, msg, fields) {
|
||||
emit({
|
||||
}} catch (_) {{}}
|
||||
}}
|
||||
globalThis.__native = {{
|
||||
emitProgress(status) {{
|
||||
emit({{ type: "progress", status: String(status) }});
|
||||
}},
|
||||
log(level, msg, fields) {{
|
||||
emit({{
|
||||
type: "log",
|
||||
level: String(level || "info"),
|
||||
msg: String(msg),
|
||||
fields: fields == null ? null : fields,
|
||||
});
|
||||
},
|
||||
};
|
||||
})();
|
||||
}});
|
||||
}},
|
||||
}};
|
||||
}})();
|
||||
|
||||
(async () => {
|
||||
// 1) read params JSON from stdin (one-shot)
|
||||
(async () => {{
|
||||
let raw = "";
|
||||
process.stdin.setEncoding("utf8");
|
||||
for await (const chunk of process.stdin) raw += chunk;
|
||||
const params = raw ? JSON.parse(raw) : null;
|
||||
|
||||
// 2) inline user script begins ↓↓↓
|
||||
"#;
|
||||
require("./{entry_script}");
|
||||
|
||||
const RUNNER_FOOTER: &str = r#"
|
||||
// ↑↑↑ inline user script ends
|
||||
|
||||
// 3) dispatch to __nodejsMain(params)
|
||||
if (typeof globalThis.__nodejsMain !== "function") {
|
||||
if (typeof globalThis.__nodejsMain !== "function") {{
|
||||
process.stderr.write(
|
||||
"__EVT__" + JSON.stringify({
|
||||
"__EVT__" + JSON.stringify({{
|
||||
type: "log",
|
||||
level: "error",
|
||||
msg: "脚本未注册 globalThis.__nodejsMain",
|
||||
}) + "\n"
|
||||
}}) + "\n"
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
try {
|
||||
}}
|
||||
try {{
|
||||
const result = await globalThis.__nodejsMain(params);
|
||||
process.stdout.write(
|
||||
"__RESULT__" + JSON.stringify(result === undefined ? null : result) + "\n"
|
||||
);
|
||||
process.exit(0);
|
||||
} catch (e) {
|
||||
}} catch (e) {{
|
||||
const msg = (e && (e.stack || e.message)) || String(e);
|
||||
process.stderr.write(
|
||||
"__EVT__" + JSON.stringify({ type: "log", level: "error", msg }) + "\n"
|
||||
"__EVT__" + JSON.stringify({{ type: "log", level: "error", msg }}) + "\n"
|
||||
);
|
||||
process.exit(2);
|
||||
}
|
||||
})();
|
||||
"#;
|
||||
}}
|
||||
}})();
|
||||
"#
|
||||
)
|
||||
}
|
||||
|
||||
fn build_runner(user_source: &str) -> String {
|
||||
let mut s = String::with_capacity(
|
||||
RUNNER_HEADER.len() + user_source.len() + RUNNER_FOOTER.len(),
|
||||
);
|
||||
s.push_str(RUNNER_HEADER);
|
||||
s.push_str(user_source);
|
||||
s.push_str(RUNNER_FOOTER);
|
||||
s
|
||||
fn normalize_script_name(name: &str) -> Option<String> {
|
||||
let base = Path::new(name.trim())
|
||||
.file_name()
|
||||
.and_then(|s| s.to_str())?;
|
||||
if RE_SAFE_SCRIPT.is_match(base) {
|
||||
Some(base.to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn extract_require_deps(source: &str) -> Vec<String> {
|
||||
RE_REQUIRE
|
||||
.captures_iter(source)
|
||||
.filter_map(|c| c.get(1).map(|m| m.as_str().to_string()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// 从入口脚本名出发,按 `require('*.js')` 递归拉取服务端模块。
|
||||
async fn resolve_script_bundle(entry: &str) -> Result<HashMap<String, String>, NodeError> {
|
||||
let entry = normalize_script_name(entry)
|
||||
.ok_or_else(|| NodeError::FetchScript(format!("无效的入口脚本名: {entry}")))?;
|
||||
|
||||
let mut modules: HashMap<String, String> = HashMap::new();
|
||||
let mut stack = vec![entry];
|
||||
|
||||
while let Some(name) = stack.pop() {
|
||||
if modules.contains_key(&name) {
|
||||
continue;
|
||||
}
|
||||
let source = fetch_node_script_source(&name).await?;
|
||||
for dep in extract_require_deps(&source) {
|
||||
if let Some(dep_name) = normalize_script_name(&dep) {
|
||||
if !modules.contains_key(&dep_name) {
|
||||
stack.push(dep_name);
|
||||
}
|
||||
}
|
||||
}
|
||||
modules.insert(name, source);
|
||||
}
|
||||
|
||||
Ok(modules)
|
||||
}
|
||||
|
||||
/// 调试源码:仅拉取其中 require 到的服务端模块,入口为内存写入的 `__inline_entry__.js`。
|
||||
async fn resolve_script_bundle_for_inline(
|
||||
inline_source: &str,
|
||||
) -> Result<HashMap<String, String>, NodeError> {
|
||||
let mut modules: HashMap<String, String> = HashMap::new();
|
||||
let mut stack: Vec<String> = extract_require_deps(inline_source)
|
||||
.into_iter()
|
||||
.filter_map(|d| normalize_script_name(&d))
|
||||
.collect();
|
||||
|
||||
while let Some(name) = stack.pop() {
|
||||
if modules.contains_key(&name) {
|
||||
continue;
|
||||
}
|
||||
let source = fetch_node_script_source(&name).await?;
|
||||
for dep in extract_require_deps(&source) {
|
||||
if let Some(dep_name) = normalize_script_name(&dep) {
|
||||
if !modules.contains_key(&dep_name) {
|
||||
stack.push(dep_name);
|
||||
}
|
||||
}
|
||||
}
|
||||
modules.insert(name, source);
|
||||
}
|
||||
|
||||
modules.insert(INLINE_ENTRY.to_string(), inline_source.to_string());
|
||||
Ok(modules)
|
||||
}
|
||||
|
||||
async fn write_script_bundle(
|
||||
modules: &HashMap<String, String>,
|
||||
entry: &str,
|
||||
) -> Result<(PathBuf, PathBuf), NodeError> {
|
||||
let tmp_dir = std::env::temp_dir().join("aiclient-node");
|
||||
tokio::fs::create_dir_all(&tmp_dir).await?;
|
||||
let bundle_dir = tmp_dir.join(format!("bundle-{}", Uuid::new_v4()));
|
||||
tokio::fs::create_dir_all(&bundle_dir).await?;
|
||||
|
||||
for (name, source) in modules {
|
||||
tokio::fs::write(bundle_dir.join(name), source).await?;
|
||||
}
|
||||
|
||||
let runner_path = bundle_dir.join("_runner.cjs");
|
||||
let runner_src = build_file_runner(entry);
|
||||
tokio::fs::write(&runner_path, runner_src).await?;
|
||||
|
||||
Ok((bundle_dir, runner_path))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -247,46 +338,59 @@ pub async fn list_scripts() -> Result<Vec<String>, NodeError> {
|
||||
// 公开入口
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// 按脚本名运行:去后端拉源码 → 包装 runner → 启动 node → 收集结果。
|
||||
/// 按脚本名运行:去后端拉入口及 require 依赖 → 落盘 → 启动 node → 收集结果。
|
||||
pub async fn run_node_script(
|
||||
script_name: String,
|
||||
params: Value,
|
||||
events: UnboundedSender<PipelineEvent>,
|
||||
config_env: HashMap<String, String>,
|
||||
) -> Result<Value, NodeError> {
|
||||
let user_source = fetch_node_script_source(&script_name).await?;
|
||||
run_inline_source(user_source, params, events).await
|
||||
let entry = normalize_script_name(&script_name)
|
||||
.ok_or_else(|| NodeError::FetchScript(format!("无效的脚本名称: {script_name}")))?;
|
||||
let modules = resolve_script_bundle(&entry).await?;
|
||||
run_script_bundle(&entry, modules, params, events, config_env).await
|
||||
}
|
||||
|
||||
/// 调试入口:直接传源码字符串,跳过后端拉取。
|
||||
/// 调试入口:直接传源码;其中 `require('*.js')` 仍从服务端拉取依赖。
|
||||
pub async fn run_node_script_source(
|
||||
source: String,
|
||||
params: Value,
|
||||
events: UnboundedSender<PipelineEvent>,
|
||||
config_env: HashMap<String, String>,
|
||||
) -> Result<Value, NodeError> {
|
||||
run_inline_source(source, params, events).await
|
||||
let modules = resolve_script_bundle_for_inline(&source).await?;
|
||||
run_script_bundle(INLINE_ENTRY, modules, params, events, config_env).await
|
||||
}
|
||||
|
||||
async fn run_inline_source(
|
||||
user_source: String,
|
||||
async fn run_script_bundle(
|
||||
entry: &str,
|
||||
modules: HashMap<String, String>,
|
||||
params: Value,
|
||||
events: UnboundedSender<PipelineEvent>,
|
||||
config_env: HashMap<String, String>,
|
||||
) -> Result<Value, NodeError> {
|
||||
let node = locate_node().ok_or(NodeError::NotFound)?;
|
||||
let cwd = nodejs_dir().unwrap_or_else(std::env::temp_dir);
|
||||
let node_modules_cwd = nodejs_dir().unwrap_or_else(std::env::temp_dir);
|
||||
|
||||
// 写到 OS 临时目录而非 resources/,避免污染只读资源
|
||||
let tmp_dir = std::env::temp_dir().join("aiclient-node");
|
||||
tokio::fs::create_dir_all(&tmp_dir).await?;
|
||||
let script_path = tmp_dir.join(format!("script-{}.cjs", Uuid::new_v4()));
|
||||
tokio::fs::write(&script_path, build_runner(&user_source)).await?;
|
||||
let (bundle_dir, script_path) = write_script_bundle(&modules, entry).await?;
|
||||
|
||||
let mut child = Command::new(&node)
|
||||
.arg(&script_path)
|
||||
.current_dir(&cwd)
|
||||
let mut cmd = Command::new(&node);
|
||||
cmd.arg(&script_path)
|
||||
.current_dir(&bundle_dir)
|
||||
.env(
|
||||
"NODE_PATH",
|
||||
node_modules_cwd
|
||||
.join("node_modules")
|
||||
.to_string_lossy()
|
||||
.to_string(),
|
||||
)
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()?;
|
||||
.stderr(Stdio::piped());
|
||||
for (key, value) in config_env {
|
||||
cmd.env(key, value);
|
||||
}
|
||||
let mut child = cmd.spawn()?;
|
||||
|
||||
// 把 params JSON 灌进 stdin
|
||||
if let Some(mut stdin) = child.stdin.take() {
|
||||
@@ -377,7 +481,7 @@ async fn run_inline_source(
|
||||
let stderr_tail = stderr_task.await.unwrap_or_default();
|
||||
let result_line = stdout_task.await.unwrap_or(None);
|
||||
|
||||
let _ = tokio::fs::remove_file(&script_path).await;
|
||||
let _ = tokio::fs::remove_dir_all(&bundle_dir).await;
|
||||
|
||||
if !status.success() {
|
||||
return Err(NodeError::NonZero {
|
||||
|
||||
Reference in New Issue
Block a user