"添加前端模板和运行代码模块"
This commit is contained in:
10
qiming-run_code_rmcp/src/app_error.rs
Normal file
10
qiming-run_code_rmcp/src/app_error.rs
Normal file
@@ -0,0 +1,10 @@
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum AppError {
|
||||
#[error("Failed to create temporary file: {0}")]
|
||||
IoError(#[from] std::io::Error),
|
||||
|
||||
#[error("Failed to parse JSON: {0}")]
|
||||
JsonError(#[from] serde_json::Error),
|
||||
}
|
||||
131
qiming-run_code_rmcp/src/cache/code_file_cache.rs
vendored
Normal file
131
qiming-run_code_rmcp/src/cache/code_file_cache.rs
vendored
Normal file
@@ -0,0 +1,131 @@
|
||||
use anyhow::{Context, Result};
|
||||
use log::info;
|
||||
use std::path::{Path, PathBuf};
|
||||
use tokio::fs::{self, File, create_dir_all};
|
||||
use tokio::io::AsyncWriteExt;
|
||||
|
||||
use crate::model::LanguageScript;
|
||||
///针对用的代码,进行检测和缓存
|
||||
pub struct CodeFileCache;
|
||||
|
||||
impl CodeFileCache {
|
||||
/// 检查代码文件缓存
|
||||
pub fn obtain_code_hash(code: &str) -> String {
|
||||
// 使用BLAKE3计算哈希,更快速且安全
|
||||
let hash = blake3::hash(code.as_bytes());
|
||||
let hash_str = hash.to_hex().to_string();
|
||||
info!("计算代码hash值: {}", &hash_str);
|
||||
hash_str
|
||||
}
|
||||
|
||||
/// 根据代码的hash检查是否存在缓存
|
||||
pub async fn check_code_file_cache_exisht(hash: &str, language: &LanguageScript) -> bool {
|
||||
fs::try_exists(Self::get_cache_file_path(hash, language))
|
||||
.await
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// 获取代码文件缓存
|
||||
pub async fn get_code_file_cache(
|
||||
hash: &str,
|
||||
language: &LanguageScript,
|
||||
) -> Result<(File, PathBuf)> {
|
||||
let file_path = Self::get_cache_file_path(hash, language);
|
||||
let file = File::open(&file_path)
|
||||
.await
|
||||
.with_context(|| format!("无法打开缓存文件: {}", file_path.display()))?;
|
||||
Ok((file, file_path))
|
||||
}
|
||||
|
||||
/// 保存代码文件缓存
|
||||
pub async fn save_code_file_cache(
|
||||
hash: &str,
|
||||
code: &str,
|
||||
language: &LanguageScript,
|
||||
) -> Result<(File, PathBuf)> {
|
||||
// 确保缓存目录存在
|
||||
let cache_dir = Self::get_cache_dir();
|
||||
//检查目录是否存在
|
||||
if !fs::try_exists(&cache_dir).await.unwrap_or(false) {
|
||||
create_dir_all(&cache_dir)
|
||||
.await
|
||||
.with_context(|| format!("无法创建缓存目录: {}", cache_dir.display()))?;
|
||||
}
|
||||
|
||||
// 构建文件路径并保存代码
|
||||
let file_path = Self::get_cache_file_path(hash, language);
|
||||
let mut file = File::create(&file_path)
|
||||
.await
|
||||
.with_context(|| format!("无法创建缓存文件: {}", file_path.display()))?;
|
||||
|
||||
file.write_all(code.as_bytes())
|
||||
.await
|
||||
.with_context(|| "写入代码到缓存文件失败")?;
|
||||
|
||||
// 设置文件权限
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let mut perms = fs::metadata(&file_path).await?.permissions();
|
||||
perms.set_mode(0o755); // rwxr-xr-x
|
||||
fs::set_permissions(&file_path, perms).await?;
|
||||
}
|
||||
|
||||
let file_path = Self::get_cache_file_path(hash, language);
|
||||
Ok((file, file_path))
|
||||
}
|
||||
|
||||
/// 清除指定语言的缓存文件
|
||||
pub async fn clear_cache_by_language(language: &LanguageScript) -> Result<()> {
|
||||
let cache_dir = Self::get_cache_dir();
|
||||
if !fs::try_exists(&cache_dir).await.unwrap_or(false) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let suffix = language.get_file_suffix();
|
||||
let mut entries = fs::read_dir(cache_dir).await?;
|
||||
|
||||
while let Some(entry) = entries.next_entry().await? {
|
||||
let path = entry.path();
|
||||
if let Some(file_name) = path.file_name() {
|
||||
if let Some(name_str) = file_name.to_str() {
|
||||
if name_str.ends_with(suffix) {
|
||||
fs::remove_file(&path)
|
||||
.await
|
||||
.with_context(|| format!("无法删除缓存文件: {}", path.display()))?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 清除所有缓存文件
|
||||
pub async fn clear_all_cache() -> Result<()> {
|
||||
let cache_dir = Self::get_cache_dir();
|
||||
if fs::try_exists(&cache_dir).await.unwrap_or(false) {
|
||||
fs::remove_dir_all(&cache_dir)
|
||||
.await
|
||||
.with_context(|| format!("无法删除缓存目录: {}", cache_dir.display()))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 获取缓存目录路径
|
||||
fn get_cache_dir() -> PathBuf {
|
||||
// 在容器环境中使用固定路径
|
||||
Path::new("/tmp/code_cache").to_path_buf()
|
||||
}
|
||||
|
||||
/// 获取缓存文件路径
|
||||
fn get_cache_file_path(hash: &str, language: &LanguageScript) -> PathBuf {
|
||||
let mut path = Self::get_cache_dir();
|
||||
|
||||
// 根据语言类型添加对应的文件扩展名
|
||||
let file_name = format!("{}{}", hash, language.get_file_suffix());
|
||||
|
||||
path.push(file_name);
|
||||
path
|
||||
}
|
||||
}
|
||||
3
qiming-run_code_rmcp/src/cache/mod.rs
vendored
Normal file
3
qiming-run_code_rmcp/src/cache/mod.rs
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
mod code_file_cache;
|
||||
|
||||
pub use code_file_cache::CodeFileCache;
|
||||
117
qiming-run_code_rmcp/src/deno_runner/common_runner.rs
Normal file
117
qiming-run_code_rmcp/src/deno_runner/common_runner.rs
Normal file
@@ -0,0 +1,117 @@
|
||||
use crate::cache::CodeFileCache;
|
||||
use crate::model::{CodeExecutor, CodeScriptExecutionResult, CommandExecutor, LanguageScript};
|
||||
use anyhow::Result;
|
||||
use log::{debug, error, info};
|
||||
use serde_json::Value;
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
use tempfile::NamedTempFile;
|
||||
use tokio::fs;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use tokio::process::Command;
|
||||
|
||||
/// 通用的 Deno 脚本执行逻辑,供 JS/TS Runner 复用
|
||||
pub async fn run_deno_script_with_params<F>(
|
||||
code: &str,
|
||||
params: Option<Value>,
|
||||
timeout_seconds: Option<u64>,
|
||||
lang: LanguageScript,
|
||||
prepare_code_fn: F,
|
||||
) -> Result<CodeScriptExecutionResult>
|
||||
where
|
||||
F: Fn(&str, bool) -> String,
|
||||
{
|
||||
debug!("开始执行{lang:?}脚本...,执行参数: {params:?}");
|
||||
|
||||
let hash = CodeFileCache::obtain_code_hash(code);
|
||||
let cache_exist = CodeFileCache::check_code_file_cache_exisht(&hash, &lang).await;
|
||||
|
||||
let run_code_script_file_tuple = if cache_exist {
|
||||
let cache_code = CodeFileCache::get_code_file_cache(&hash, &lang).await;
|
||||
debug!("从缓存中读取代码:hash值 {:?}", &hash);
|
||||
cache_code?
|
||||
} else {
|
||||
let wrapped_code = prepare_code_fn(code, true);
|
||||
CodeFileCache::save_code_file_cache(&hash, &wrapped_code, &lang).await?;
|
||||
let code_script_file_tuple = CodeFileCache::get_code_file_cache(&hash, &lang).await?;
|
||||
debug!("创建脚本缓存:hash值 {:?}", &hash);
|
||||
code_script_file_tuple
|
||||
};
|
||||
|
||||
let temp_path = run_code_script_file_tuple.1;
|
||||
|
||||
let mut execute_command = Command::new("deno");
|
||||
execute_command
|
||||
.arg("run")
|
||||
.arg("--allow-net")
|
||||
.arg("--allow-env")
|
||||
.arg("--allow-read")
|
||||
.arg("--no-check")
|
||||
.arg("--v8-flags=--max-heap-size=512")
|
||||
.arg(&temp_path)
|
||||
.kill_on_drop(true);
|
||||
|
||||
// 处理参数:统一使用临时文件传递
|
||||
let temp_input_path = if let Some(params) = params {
|
||||
let params_json = serde_json::to_string(¶ms)?;
|
||||
|
||||
// 创建临时文件写入参数
|
||||
let temp_dir = tempfile::TempDir::new()?;
|
||||
let temp_file_path = temp_dir.path().join("input_params.json");
|
||||
|
||||
// 写入参数到临时文件
|
||||
std::fs::write(&temp_file_path, params_json.as_bytes())?;
|
||||
|
||||
// 保持TempDir存在(这样文件就不会被删除)
|
||||
let temp_dir_path = temp_dir.path().to_path_buf();
|
||||
std::mem::forget(temp_dir);
|
||||
|
||||
// 设置环境变量指向临时文件
|
||||
execute_command.env("INPUT_JSON_FILE", &temp_file_path);
|
||||
debug!("使用临时文件传递参数,文件路径: {:?}", temp_file_path);
|
||||
|
||||
Some(temp_file_path)
|
||||
} else {
|
||||
// 没有参数时设置空对象
|
||||
execute_command.env("INPUT_JSON", "{}");
|
||||
None
|
||||
};
|
||||
|
||||
debug!("Deno命令[{:?}]: {:?}", lang, &execute_command);
|
||||
|
||||
let executor = match timeout_seconds {
|
||||
Some(timeout) => CommandExecutor::with_timeout(execute_command.output(), timeout),
|
||||
None => CommandExecutor::default(execute_command.output()),
|
||||
};
|
||||
info!("执行命令: {:?}", &execute_command);
|
||||
|
||||
let executor_result = executor.await;
|
||||
let output = match executor_result {
|
||||
Ok(cmd_result) => match cmd_result {
|
||||
Ok(output) => output,
|
||||
Err(e) => {
|
||||
error!("Deno命令执行失败 [{lang:?}]: {e:?}");
|
||||
return Err(e.into());
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
error!("Deno任务执行异常 [{lang:?}]: {e:?}");
|
||||
return Err(e.into());
|
||||
}
|
||||
};
|
||||
debug!("标准输出:\n{}", String::from_utf8_lossy(&output.stdout));
|
||||
debug!("错误输出:\n{}", String::from_utf8_lossy(&output.stderr));
|
||||
|
||||
// 执行完成后删除临时文件和目录
|
||||
if let Some(temp_file_path) = temp_input_path {
|
||||
// 删除文件
|
||||
let _ = fs::remove_file(&temp_file_path).await;
|
||||
// 尝试删除父目录(如果为空)
|
||||
if let Some(parent) = temp_file_path.parent() {
|
||||
let _ = fs::remove_dir(parent).await;
|
||||
}
|
||||
debug!("已删除临时文件: {:?}", temp_file_path);
|
||||
}
|
||||
|
||||
CodeExecutor::parse_execution_output(&output.stdout, &output.stderr).await
|
||||
}
|
||||
61
qiming-run_code_rmcp/src/deno_runner/js_runner.rs
Normal file
61
qiming-run_code_rmcp/src/deno_runner/js_runner.rs
Normal file
@@ -0,0 +1,61 @@
|
||||
// deno 运行js脚本
|
||||
use crate::deno_runner::common_runner::run_deno_script_with_params;
|
||||
use crate::model::{CodeScriptExecutionResult, LanguageScript, RunCode};
|
||||
use anyhow::Result;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct JsRunner;
|
||||
|
||||
|
||||
impl RunCode for JsRunner {
|
||||
async fn run_with_params(
|
||||
&self,
|
||||
code: &str,
|
||||
params: Option<serde_json::Value>,
|
||||
timeout_seconds: Option<u64>,
|
||||
) -> Result<CodeScriptExecutionResult> {
|
||||
run_deno_script_with_params(
|
||||
code,
|
||||
params,
|
||||
timeout_seconds,
|
||||
LanguageScript::Js,
|
||||
|c, show_logs| self.prepare_js_code(c, show_logs),
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
impl JsRunner {
|
||||
/// 准备JavaScript代码,添加日志捕获和handler函数执行逻辑
|
||||
fn prepare_js_code(&self, code: &str, show_logs: bool) -> String {
|
||||
// 检查代码中是否包含ES模块特征
|
||||
let is_esm = {
|
||||
// 检查正式的 import/export 语句
|
||||
let has_import_export = code.contains("import ")
|
||||
|| code.contains("export ")
|
||||
|| code.contains("import{")
|
||||
|| code.contains("export{");
|
||||
|
||||
// 检查动态 import
|
||||
let has_dynamic_import = code.contains("import(");
|
||||
|
||||
// 检查是否有 require - CommonJS 的标志
|
||||
let has_require = code.contains("require(");
|
||||
|
||||
// 如果有 import/export 特征,或者动态 import,但没有 require,则判定为 ESM
|
||||
(has_import_export || has_dynamic_import) && !has_require
|
||||
};
|
||||
|
||||
// 根据代码特征选择合适的模板
|
||||
let template = if is_esm {
|
||||
include_str!("../templates/js_template_es.js")
|
||||
} else {
|
||||
include_str!("../templates/js_template_normal.js")
|
||||
};
|
||||
|
||||
// 替换模板中的占位符
|
||||
template
|
||||
.replace("{{USER_CODE}}", code)
|
||||
.replace("{{SHOW_LOGS}}", &show_logs.to_string())
|
||||
}
|
||||
}
|
||||
6
qiming-run_code_rmcp/src/deno_runner/mod.rs
Normal file
6
qiming-run_code_rmcp/src/deno_runner/mod.rs
Normal file
@@ -0,0 +1,6 @@
|
||||
mod common_runner;
|
||||
mod js_runner;
|
||||
mod ts_runner;
|
||||
|
||||
pub use js_runner::JsRunner;
|
||||
pub use ts_runner::TsRunner;
|
||||
37
qiming-run_code_rmcp/src/deno_runner/ts_runner.rs
Normal file
37
qiming-run_code_rmcp/src/deno_runner/ts_runner.rs
Normal file
@@ -0,0 +1,37 @@
|
||||
// deno 运行ts脚本
|
||||
use crate::model::{CodeScriptExecutionResult, LanguageScript, RunCode};
|
||||
use anyhow::Result;
|
||||
use crate::deno_runner::common_runner::run_deno_script_with_params;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct TsRunner;
|
||||
|
||||
impl RunCode for TsRunner {
|
||||
async fn run_with_params(
|
||||
&self,
|
||||
code: &str,
|
||||
params: Option<serde_json::Value>,
|
||||
timeout_seconds: Option<u64>,
|
||||
) -> Result<CodeScriptExecutionResult> {
|
||||
run_deno_script_with_params(
|
||||
code,
|
||||
params,
|
||||
timeout_seconds,
|
||||
LanguageScript::Ts,
|
||||
|c, show_logs| self.prepare_ts_code(c, show_logs),
|
||||
).await
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
impl TsRunner {
|
||||
|
||||
/// 准备TypeScript代码,添加日志捕获和handler函数执行逻辑
|
||||
fn prepare_ts_code(&self, code: &str, show_logs: bool) -> String {
|
||||
let template = include_str!("../templates/ts_template.ts");
|
||||
|
||||
template
|
||||
.replace("{{USER_CODE}}", code)
|
||||
.replace("{{SHOW_LOGS}}", &show_logs.to_string())
|
||||
}
|
||||
}
|
||||
19
qiming-run_code_rmcp/src/lib.rs
Normal file
19
qiming-run_code_rmcp/src/lib.rs
Normal file
@@ -0,0 +1,19 @@
|
||||
mod app_error;
|
||||
mod cache;
|
||||
mod deno_runner;
|
||||
#[cfg(feature = "mcp")]
|
||||
mod mcp;
|
||||
mod model;
|
||||
mod python_runner;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
mod warm_up;
|
||||
|
||||
pub use cache::*;
|
||||
pub use deno_runner::*;
|
||||
#[cfg(feature = "mcp")]
|
||||
pub use mcp::CodeRunRequest;
|
||||
pub use model::RunCodeHttpResult;
|
||||
pub use model::{CodeExecutor, CodeScriptExecutionResult, LanguageScript};
|
||||
pub use python_runner::*;
|
||||
pub use warm_up::warm_up_all_envs;
|
||||
194
qiming-run_code_rmcp/src/main.rs
Normal file
194
qiming-run_code_rmcp/src/main.rs
Normal file
@@ -0,0 +1,194 @@
|
||||
use anyhow::{Context, Result};
|
||||
use clap::{Args, Parser, Subcommand};
|
||||
use log::{error, info};
|
||||
use serde_json::Value;
|
||||
use std::{fs, path::PathBuf};
|
||||
|
||||
mod app_error;
|
||||
mod cache;
|
||||
mod deno_runner;
|
||||
#[cfg(feature = "mcp")]
|
||||
mod mcp;
|
||||
mod model;
|
||||
mod python_runner;
|
||||
|
||||
use crate::cache::CodeFileCache;
|
||||
use crate::model::{CodeExecutor, CodeScriptExecutionResult, LanguageScript};
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "run_code_rmcp")]
|
||||
#[command(about = "Execute JavaScript and Python code using MCP SDK", long_about = None)]
|
||||
struct Cli {
|
||||
#[command(subcommand)]
|
||||
command: Commands,
|
||||
|
||||
/// Show execution logs
|
||||
#[arg(short, long)]
|
||||
show_logs: bool,
|
||||
|
||||
/// Use MCP SDK integration
|
||||
#[cfg(feature = "mcp")]
|
||||
#[arg(short, long)]
|
||||
use_mcp: bool,
|
||||
|
||||
/// Clear cache before execution
|
||||
#[arg(short = 'c', long)]
|
||||
clear_cache: bool,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum Commands {
|
||||
/// Execute JavaScript code
|
||||
Js(CodeArgs),
|
||||
|
||||
/// Execute TypeScript code
|
||||
Ts(CodeArgs),
|
||||
|
||||
/// Execute Python code
|
||||
Python(CodeArgs),
|
||||
|
||||
/// Clear cache files
|
||||
ClearCache {
|
||||
/// Language to clear cache for (js, ts, python, or all)
|
||||
#[arg(short, long)]
|
||||
language: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Args)]
|
||||
struct CodeArgs {
|
||||
/// Path to the code file
|
||||
#[arg(short, long)]
|
||||
file: Option<PathBuf>,
|
||||
|
||||
/// Code content as string
|
||||
#[arg(short, long)]
|
||||
code: Option<String>,
|
||||
|
||||
/// Parameters to pass to the script (JSON format)
|
||||
#[arg(short, long)]
|
||||
params: Option<String>,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
// 解析命令行参数
|
||||
let cli = Cli::parse();
|
||||
|
||||
// 初始化日志
|
||||
let mut builder = env_logger::Builder::from_default_env();
|
||||
if cli.show_logs {
|
||||
// 如果--show-logs参数为true,则至少显示info级别的日志
|
||||
builder.filter_level(log::LevelFilter::Info);
|
||||
}
|
||||
builder.init();
|
||||
|
||||
if let Commands::ClearCache { language } = &cli.command {
|
||||
match language.to_lowercase().as_str() {
|
||||
"js" => {
|
||||
CodeFileCache::clear_cache_by_language(&LanguageScript::Js).await?;
|
||||
info!("已清除 JavaScript 缓存");
|
||||
}
|
||||
"ts" => {
|
||||
CodeFileCache::clear_cache_by_language(&LanguageScript::Ts).await?;
|
||||
info!("已清除 TypeScript 缓存");
|
||||
}
|
||||
"python" => {
|
||||
CodeFileCache::clear_cache_by_language(&LanguageScript::Python).await?;
|
||||
info!("已清除 Python 缓存");
|
||||
}
|
||||
"all" => {
|
||||
CodeFileCache::clear_all_cache().await?;
|
||||
info!("已清除所有缓存");
|
||||
}
|
||||
_ => {
|
||||
info!("无效的语言类型,可选项: js, ts, python, all");
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// 解析传递给脚本的参数
|
||||
let params = match &cli.command {
|
||||
Commands::Js(args) => parse_params(&args.params)?,
|
||||
Commands::Ts(args) => parse_params(&args.params)?,
|
||||
Commands::Python(args) => parse_params(&args.params)?,
|
||||
_ => None,
|
||||
};
|
||||
|
||||
// 从参数获取代码
|
||||
let (code, language) = match &cli.command {
|
||||
Commands::Js(args) => (get_code(args)?, LanguageScript::Js),
|
||||
Commands::Ts(args) => (get_code(args)?, LanguageScript::Ts),
|
||||
Commands::Python(args) => (get_code(args)?, LanguageScript::Python),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
// 如果指定了清除缓存选项,则清除对应语言的缓存
|
||||
if cli.clear_cache {
|
||||
CodeFileCache::clear_cache_by_language(&language).await?;
|
||||
info!("已清除 {language:?} 缓存");
|
||||
}
|
||||
|
||||
// 执行代码
|
||||
#[cfg(feature = "mcp")]
|
||||
let result = if cli.use_mcp {
|
||||
// 使用MCP SDK集成
|
||||
CodeExecutor::execute_with_params_compat(&code, language, params).await?
|
||||
} else {
|
||||
// 直接执行
|
||||
CodeExecutor::execute_with_params_compat(&code, language, params).await?
|
||||
};
|
||||
|
||||
#[cfg(not(feature = "mcp"))]
|
||||
let result = CodeExecutor::execute_with_params_compat(&code, language, params).await?;
|
||||
|
||||
// 打印结果
|
||||
print_result(result);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_code(args: &CodeArgs) -> Result<String> {
|
||||
if let Some(file) = &args.file {
|
||||
fs::read_to_string(file).context("Failed to read code file")
|
||||
} else if let Some(code) = &args.code {
|
||||
Ok(code.clone())
|
||||
} else {
|
||||
anyhow::bail!("Either file or code must be provided")
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_params(params_str: &Option<String>) -> Result<Option<Value>> {
|
||||
match params_str {
|
||||
Some(s) if !s.is_empty() => {
|
||||
let parsed: Value =
|
||||
serde_json::from_str(s).context("Failed to parse parameters as JSON")?;
|
||||
Ok(Some(parsed))
|
||||
}
|
||||
_ => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
fn print_result(result: CodeScriptExecutionResult) {
|
||||
if !result.logs.is_empty() {
|
||||
info!("--- Logs ---");
|
||||
for log in result.logs {
|
||||
info!("{log}");
|
||||
}
|
||||
info!("------------");
|
||||
}
|
||||
|
||||
if let Some(result_val) = result.result {
|
||||
// 使用 serde_json 序列化结果,确保所有类型都能正确显示
|
||||
match serde_json::to_string_pretty(&result_val) {
|
||||
Ok(json_str) => info!("Result: {json_str}"),
|
||||
Err(_) => info!("Result: {result_val}"),
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(error) = result.error {
|
||||
error!("Error: {error}");
|
||||
}
|
||||
}
|
||||
164
qiming-run_code_rmcp/src/mcp/mcp_server.rs
Normal file
164
qiming-run_code_rmcp/src/mcp/mcp_server.rs
Normal file
@@ -0,0 +1,164 @@
|
||||
use anyhow::Result;
|
||||
use rmcp::{
|
||||
ErrorData as McpError, ServerHandler,
|
||||
handler::server::wrapper::Parameters,
|
||||
model::{
|
||||
CallToolResult, Content, Implementation, ProtocolVersion, ServerCapabilities, ServerInfo,
|
||||
},
|
||||
tool, tool_handler, tool_router,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use serde_json::json;
|
||||
|
||||
use crate::model::{CodeExecutor, LanguageScript};
|
||||
|
||||
/// 代码执行请求参数
|
||||
#[derive(Debug, Deserialize, schemars::JsonSchema)]
|
||||
pub struct CodeRunRequest {
|
||||
#[schemars(description = "要执行的代码")]
|
||||
pub code: String,
|
||||
|
||||
#[schemars(description = "可选的执行参数")]
|
||||
pub params: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
/// 代码执行工具服务
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct CodeRunnerService;
|
||||
|
||||
#[tool_router]
|
||||
impl CodeRunnerService {
|
||||
#[tool(description = "执行JavaScript代码并返回结果")]
|
||||
async fn run_javascript(
|
||||
&self,
|
||||
request: Parameters<CodeRunRequest>,
|
||||
) -> Result<CallToolResult, McpError> {
|
||||
let request = request.0;
|
||||
match CodeExecutor::execute_with_params_compat(
|
||||
&request.code,
|
||||
LanguageScript::Js,
|
||||
request.params,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(result) => {
|
||||
if result.success {
|
||||
let content = Content::json(json!({
|
||||
"result": result.result,
|
||||
"logs": result.logs,
|
||||
"success": true
|
||||
}))?;
|
||||
Ok(CallToolResult::success(vec![content]))
|
||||
} else {
|
||||
let content = Content::json(json!({
|
||||
"success": false,
|
||||
"error": result.error,
|
||||
"logs": result.logs
|
||||
}))?;
|
||||
Ok(CallToolResult::success(vec![content]))
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
let content = Content::json(json!({
|
||||
"success": false,
|
||||
"error": err.to_string(),
|
||||
"logs": []
|
||||
}))?;
|
||||
Ok(CallToolResult::success(vec![content]))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tool(description = "执行TypeScript代码并返回结果")]
|
||||
async fn run_typescript(
|
||||
&self,
|
||||
request: Parameters<CodeRunRequest>,
|
||||
) -> Result<CallToolResult, McpError> {
|
||||
let request = request.0;
|
||||
match CodeExecutor::execute_with_params_compat(
|
||||
&request.code,
|
||||
LanguageScript::Ts,
|
||||
request.params,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(result) => {
|
||||
if result.success {
|
||||
let content = Content::json(json!({
|
||||
"result": result.result,
|
||||
"logs": result.logs,
|
||||
"success": true
|
||||
}))?;
|
||||
Ok(CallToolResult::success(vec![content]))
|
||||
} else {
|
||||
let content = Content::json(json!({
|
||||
"success": false,
|
||||
"error": result.error,
|
||||
"logs": result.logs
|
||||
}))?;
|
||||
Ok(CallToolResult::success(vec![content]))
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
let content = Content::json(json!({
|
||||
"success": false,
|
||||
"error": err.to_string(),
|
||||
"logs": []
|
||||
}))?;
|
||||
Ok(CallToolResult::success(vec![content]))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[rmcp::tool(description = "执行Python代码并返回结果")]
|
||||
async fn run_python(
|
||||
&self,
|
||||
request: Parameters<CodeRunRequest>,
|
||||
) -> Result<CallToolResult, McpError> {
|
||||
let request = request.0;
|
||||
match CodeExecutor::execute_with_params_compat(
|
||||
&request.code,
|
||||
LanguageScript::Python,
|
||||
request.params,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(result) => {
|
||||
if result.success {
|
||||
let content = Content::json(json!({
|
||||
"result": result.result,
|
||||
"logs": result.logs,
|
||||
"success": true
|
||||
}))?;
|
||||
Ok(CallToolResult::success(vec![content]))
|
||||
} else {
|
||||
let content = Content::json(json!({
|
||||
"success": false,
|
||||
"error": result.error,
|
||||
"logs": result.logs
|
||||
}))?;
|
||||
Ok(CallToolResult::success(vec![content]))
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
let content = Content::json(json!({
|
||||
"success": false,
|
||||
"error": err.to_string(),
|
||||
"logs": []
|
||||
}))?;
|
||||
Ok(CallToolResult::success(vec![content]))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ServerHandler for CodeRunnerService {
|
||||
fn get_info(&self) -> ServerInfo {
|
||||
ServerInfo {
|
||||
protocol_version: ProtocolVersion::V_2024_11_05,
|
||||
capabilities: ServerCapabilities::builder().enable_tools().build(),
|
||||
server_info: Implementation::from_build_env(),
|
||||
instructions: Some("一个支持执行JavaScript、TypeScript和Python代码的服务".to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
3
qiming-run_code_rmcp/src/mcp/mod.rs
Normal file
3
qiming-run_code_rmcp/src/mcp/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
mod mcp_server;
|
||||
|
||||
pub use mcp_server::{CodeRunnerService,CodeRunRequest};
|
||||
268
qiming-run_code_rmcp/src/model/code_run_model.rs
Normal file
268
qiming-run_code_rmcp/src/model/code_run_model.rs
Normal file
@@ -0,0 +1,268 @@
|
||||
use std::{
|
||||
pin::Pin,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
|
||||
use anyhow::{Context as AnyHowContext, Result};
|
||||
use log::{info, warn};
|
||||
use pin_project::pin_project;
|
||||
use regex::Regex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use std::future::Future;
|
||||
use tokio::{
|
||||
io,
|
||||
process::Command,
|
||||
time::{Duration, Sleep, sleep},
|
||||
};
|
||||
|
||||
use crate::{deno_runner::JsRunner, deno_runner::TsRunner, python_runner::PythonRunner};
|
||||
|
||||
///语言脚本,选择对应的语言脚本运行期
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum LanguageScript {
|
||||
Js,
|
||||
Ts,
|
||||
Python,
|
||||
}
|
||||
|
||||
impl LanguageScript {
|
||||
/// 获取文件后缀
|
||||
pub fn get_file_suffix(&self) -> &str {
|
||||
match self {
|
||||
LanguageScript::Js => ".js",
|
||||
LanguageScript::Ts => ".ts",
|
||||
LanguageScript::Python => ".py",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
///执行结果,包含js/python 执行结果,和打印的log日志
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct CodeScriptExecutionResult {
|
||||
//js/python 执行结果
|
||||
pub result: Option<Value>,
|
||||
//js/python 打印的log日志
|
||||
pub logs: Vec<String>,
|
||||
// 是否执行成功,ture:默认值,执行成功
|
||||
#[serde(skip_serializing)]
|
||||
pub success: bool,
|
||||
//如果执行错误的话,错误信息
|
||||
#[serde(skip_serializing)]
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
///运行代码的抽象
|
||||
#[allow(async_fn_in_trait)]
|
||||
pub trait RunCode {
|
||||
///运行代码并传递参数,可选设置超时时间
|
||||
async fn run_with_params(
|
||||
&self,
|
||||
code: &str,
|
||||
params: Option<serde_json::Value>,
|
||||
timeout_seconds: Option<u64>,
|
||||
) -> Result<CodeScriptExecutionResult>;
|
||||
}
|
||||
|
||||
/// 代码执行器
|
||||
pub struct CodeExecutor;
|
||||
|
||||
impl CodeExecutor {
|
||||
/// 执行代码并传递参数,可选设置超时时间
|
||||
pub async fn execute_with_params(
|
||||
code: &str,
|
||||
language: LanguageScript,
|
||||
params: Option<serde_json::Value>,
|
||||
timeout_seconds: Option<u64>,
|
||||
) -> Result<CodeScriptExecutionResult> {
|
||||
info!(
|
||||
"开始执行代码... 语言[{language:?}],执行参数: {params:?}"
|
||||
);
|
||||
match language {
|
||||
LanguageScript::Js => {
|
||||
JsRunner
|
||||
.run_with_params(code, params, timeout_seconds)
|
||||
.await
|
||||
}
|
||||
LanguageScript::Ts => {
|
||||
TsRunner
|
||||
.run_with_params(code, params, timeout_seconds)
|
||||
.await
|
||||
}
|
||||
LanguageScript::Python => {
|
||||
PythonRunner
|
||||
.run_with_params(code, params, timeout_seconds)
|
||||
.await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 兼容旧代码的方法,不指定超时时间
|
||||
pub async fn execute_with_params_compat(
|
||||
code: &str,
|
||||
language: LanguageScript,
|
||||
params: Option<serde_json::Value>,
|
||||
) -> Result<CodeScriptExecutionResult> {
|
||||
Self::execute_with_params(code, language, params, None).await
|
||||
}
|
||||
|
||||
/// 解析执行输出
|
||||
pub async fn parse_execution_output(
|
||||
stdout: &[u8],
|
||||
stderr: &[u8],
|
||||
) -> Result<CodeScriptExecutionResult> {
|
||||
let stdout_str = String::from_utf8_lossy(stdout).to_string();
|
||||
let stderr_str = String::from_utf8_lossy(stderr).to_string();
|
||||
|
||||
// 尝试从stdout中查找JSON输出
|
||||
let json_pattern = r#"\{"logs":\s*\[.*\],\s*"result":.*,\s*"error":.*\}"#;
|
||||
let re = Regex::new(json_pattern)?;
|
||||
|
||||
if let Some(captures) = re.find(&stdout_str) {
|
||||
let json_str = captures.as_str();
|
||||
let parsed: serde_json::Value =
|
||||
serde_json::from_str(json_str).context("Failed to parse JSON output")?;
|
||||
|
||||
// 从JSON中提取logs、result和error
|
||||
let logs = parsed["logs"]
|
||||
.as_array()
|
||||
.map(|arr| {
|
||||
arr.iter()
|
||||
.filter_map(|v| v.as_str().map(String::from))
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
// 处理结果,尝试解析JSON字符串
|
||||
let result = if parsed["result"].is_null() {
|
||||
None
|
||||
} else if let Some(result_str) = parsed["result"].as_str() {
|
||||
// 如果是字符串,尝试解析为JSON对象
|
||||
match serde_json::from_str::<Value>(result_str) {
|
||||
Ok(json_value) => Some(json_value),
|
||||
Err(_) => Some(Value::String(result_str.to_string())),
|
||||
}
|
||||
} else {
|
||||
// 其他类型(数字、布尔值等)直接使用
|
||||
Some(parsed["result"].clone())
|
||||
};
|
||||
|
||||
let error = parsed["error"].as_str().map(String::from);
|
||||
|
||||
return Ok(CodeScriptExecutionResult {
|
||||
logs,
|
||||
result,
|
||||
success: error.is_none(),
|
||||
error,
|
||||
});
|
||||
}
|
||||
|
||||
// 如果没有找到结构化输出,返回原始输出
|
||||
Ok(CodeScriptExecutionResult {
|
||||
logs: if !stdout_str.is_empty() {
|
||||
vec![stdout_str]
|
||||
} else {
|
||||
vec![]
|
||||
},
|
||||
result: None,
|
||||
success: false,
|
||||
error: Some(format!(
|
||||
"Failed to extract structured output: {stderr_str}"
|
||||
)),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// 封装 tokio::command 的执行,并设置堆大小限制
|
||||
#[allow(dead_code)]
|
||||
pub struct TokioHeapSize {
|
||||
//堆大小限制
|
||||
pub heap_size: u64,
|
||||
}
|
||||
|
||||
impl Default for TokioHeapSize {
|
||||
fn default() -> Self {
|
||||
// 堆大小限制为 2GB
|
||||
Self::new(2048 * 1024 * 1024)
|
||||
}
|
||||
}
|
||||
|
||||
impl TokioHeapSize {
|
||||
pub fn new(heap_size: u64) -> Self {
|
||||
Self { heap_size }
|
||||
}
|
||||
|
||||
/// 启动带有堆内存限制的子进程,返回 tokio::process::Child
|
||||
#[allow(dead_code)]
|
||||
pub async fn with_heap_limit(&self, command: &mut Command) {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
use libc::{RLIMIT_AS, rlimit, setrlimit};
|
||||
let heap_size = self.heap_size.clone();
|
||||
unsafe {
|
||||
command.pre_exec(move || {
|
||||
let rlim = rlimit {
|
||||
rlim_cur: heap_size,
|
||||
rlim_max: heap_size,
|
||||
};
|
||||
if setrlimit(RLIMIT_AS, &rlim) != 0 {
|
||||
return Err(std::io::Error::last_os_error());
|
||||
}
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
}
|
||||
// macOS/Windows 下不做限制
|
||||
}
|
||||
}
|
||||
|
||||
///使用 pin-project 实现一个代码执行器,参数:timeout 超时时间; command 执行命令; 以及内置限制 command命令执行的堆大小限制
|
||||
#[pin_project]
|
||||
pub struct CommandExecutor<F> {
|
||||
#[pin]
|
||||
timeout: Sleep,
|
||||
#[pin]
|
||||
future: F,
|
||||
}
|
||||
|
||||
impl<F> CommandExecutor<F> {
|
||||
pub fn default(future: F) -> Self {
|
||||
let timeout = sleep(Duration::from_secs(180));
|
||||
|
||||
Self { timeout, future }
|
||||
}
|
||||
|
||||
pub fn with_timeout(future: F, timeout_seconds: u64) -> Self {
|
||||
let timeout = sleep(Duration::from_secs(timeout_seconds));
|
||||
|
||||
Self { timeout, future }
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn change_timeout(&mut self, timeout: Duration) {
|
||||
self.timeout = sleep(timeout);
|
||||
}
|
||||
}
|
||||
|
||||
impl<F: Future> Future for CommandExecutor<F> {
|
||||
type Output = io::Result<F::Output>;
|
||||
|
||||
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||
let this = self.project();
|
||||
|
||||
//根据 heap_size ,通过setrlimit设置执行 command 的堆大小限制
|
||||
match this.future.poll(cx) {
|
||||
Poll::Ready(result) => Poll::Ready(Ok(result)),
|
||||
Poll::Pending => match this.timeout.poll(cx) {
|
||||
Poll::Ready(()) => {
|
||||
warn!("执行命令超时");
|
||||
Poll::Ready(Err(io::Error::new(
|
||||
io::ErrorKind::TimedOut,
|
||||
"future timed out",
|
||||
)))
|
||||
}
|
||||
Poll::Pending => Poll::Pending,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
8
qiming-run_code_rmcp/src/model/mod.rs
Normal file
8
qiming-run_code_rmcp/src/model/mod.rs
Normal file
@@ -0,0 +1,8 @@
|
||||
mod code_run_model;
|
||||
mod tool_params;
|
||||
|
||||
pub use code_run_model::{
|
||||
CodeExecutor, CodeScriptExecutionResult, CommandExecutor, LanguageScript, RunCode,
|
||||
};
|
||||
#[allow(unused_imports)]
|
||||
pub use tool_params::RunCodeHttpResult;
|
||||
50
qiming-run_code_rmcp/src/model/tool_params.rs
Normal file
50
qiming-run_code_rmcp/src/model/tool_params.rs
Normal file
@@ -0,0 +1,50 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use std::collections::HashMap;
|
||||
|
||||
///代码运行请求,mcp调用tool工具时传入的参数
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct RunCodeMessageRequest {
|
||||
//js运行参数
|
||||
pub(crate) json_param: HashMap<String, Value>,
|
||||
//运行的代码
|
||||
pub code: String,
|
||||
//前端生成的随机uid,用于查找websocket连接,发送执行过程中的log日志
|
||||
pub uid: String,
|
||||
pub engine_type: String,
|
||||
}
|
||||
|
||||
//http返回的结构,data的结构一般是: CodeScriptExecutionResult,就是代码脚本的执行结果
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct RunCodeHttpResult {
|
||||
//js 结果,json_value
|
||||
pub data: Value,
|
||||
// 是否执行成功,ture:默认值,执行成功
|
||||
pub success: bool,
|
||||
//如果执行错误的话,错误日志
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
/// 代码执行参数
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct CodeExecutionParams {
|
||||
/// 要执行的代码内容
|
||||
pub code: String,
|
||||
/// 代码语言
|
||||
pub language: String,
|
||||
/// 是否显示日志
|
||||
pub show_logs: bool,
|
||||
}
|
||||
|
||||
/// 代码执行结果
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct ToolExecutionResult {
|
||||
/// 执行日志
|
||||
pub logs: Vec<String>,
|
||||
/// 执行结果
|
||||
pub result: Option<Value>,
|
||||
/// 执行是否成功
|
||||
pub success: bool,
|
||||
/// 错误信息
|
||||
pub error: Option<String>,
|
||||
}
|
||||
25
qiming-run_code_rmcp/src/python_imports.pest
Normal file
25
qiming-run_code_rmcp/src/python_imports.pest
Normal file
@@ -0,0 +1,25 @@
|
||||
WHITESPACE = _{ " " | "\t" }
|
||||
alpha = { 'a'..'z' | 'A'..'Z' }
|
||||
digit = { '0'..'9' }
|
||||
|
||||
module_name = @{ alpha ~ (alpha | digit | "_")* }
|
||||
|
||||
module_name_alias = @{ alpha ~ (alpha | digit | "_")* }
|
||||
|
||||
import_statement = ${ "import" ~ WHITESPACE+ ~ module_name}
|
||||
|
||||
from_import_statement = ${ "from" ~ WHITESPACE+ ~ module_name ~ WHITESPACE+ ~ ("import" ~ WHITESPACE+ ~ module_name_alias)? }
|
||||
|
||||
|
||||
quote = {( "'" | "\"")}
|
||||
|
||||
importlib_statement = { module_name_alias? ~ "="? ~ "importlib.import_module" ~ "(" ~ WHITESPACE* ~ quote ~ module_name ~ quote ~ WHITESPACE* ~ ")" }
|
||||
|
||||
empty_line = _{ WHITESPACE* ~ ("\r\n" | "\n") }
|
||||
|
||||
line = _{ (!("\r\n" | "\n") ~ ANY)* ~ ("\r\n" | "\n") }
|
||||
|
||||
record = { from_import_statement| import_statement | importlib_statement | empty_line | line }
|
||||
|
||||
file = { SOI ~ record* ~ EOI }
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
mod python_dependencies;
|
||||
|
||||
pub use python_dependencies::*;
|
||||
@@ -0,0 +1,406 @@
|
||||
use anyhow::Result;
|
||||
use log::{debug, info};
|
||||
use pest::Parser;
|
||||
use pest_derive::Parser;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[grammar = "python_imports.pest"] // 这里是你的pest语法文件
|
||||
struct ImportParser;
|
||||
|
||||
// Python标准库模块列表,使用静态变量定义
|
||||
static PYTHON_STDLIB_MODULES: &[&str] = &[
|
||||
"abc",
|
||||
"argparse",
|
||||
"array",
|
||||
"ast",
|
||||
"asyncio",
|
||||
"atexit",
|
||||
"base64",
|
||||
"bdb",
|
||||
"binascii",
|
||||
"bisect",
|
||||
"builtins",
|
||||
"bz2",
|
||||
"calendar",
|
||||
"cmath",
|
||||
"cmd",
|
||||
"code",
|
||||
"codecs",
|
||||
"codeop",
|
||||
"collections",
|
||||
"colorsys",
|
||||
"compileall",
|
||||
"concurrent",
|
||||
"configparser",
|
||||
"contextlib",
|
||||
"copy",
|
||||
"copyreg",
|
||||
"cProfile",
|
||||
"csv",
|
||||
"ctypes",
|
||||
"curses",
|
||||
"dataclasses",
|
||||
"datetime",
|
||||
"dbm",
|
||||
"decimal",
|
||||
"difflib",
|
||||
"dis",
|
||||
"doctest",
|
||||
"email",
|
||||
"encodings",
|
||||
"ensurepip",
|
||||
"enum",
|
||||
"errno",
|
||||
"faulthandler",
|
||||
"fcntl",
|
||||
"filecmp",
|
||||
"fileinput",
|
||||
"fnmatch",
|
||||
"fractions",
|
||||
"ftplib",
|
||||
"functools",
|
||||
"gc",
|
||||
"getopt",
|
||||
"getpass",
|
||||
"gettext",
|
||||
"glob",
|
||||
"graphlib",
|
||||
"grp",
|
||||
"gzip",
|
||||
"hashlib",
|
||||
"heapq",
|
||||
"hmac",
|
||||
"html",
|
||||
"http",
|
||||
"idlelib",
|
||||
"imaplib",
|
||||
"importlib",
|
||||
"inspect",
|
||||
"io",
|
||||
"ipaddress",
|
||||
"itertools",
|
||||
"json",
|
||||
"keyword",
|
||||
"linecache",
|
||||
"locale",
|
||||
"logging",
|
||||
"lzma",
|
||||
"mailbox",
|
||||
"marshal",
|
||||
"math",
|
||||
"mimetypes",
|
||||
"mmap",
|
||||
"modulefinder",
|
||||
"msvcrt",
|
||||
"multiprocessing",
|
||||
"netrc",
|
||||
"numbers",
|
||||
"operator",
|
||||
"optparse",
|
||||
"os",
|
||||
"pathlib",
|
||||
"pdb",
|
||||
"pickle",
|
||||
"pickletools",
|
||||
"pkgutil",
|
||||
"platform",
|
||||
"plistlib",
|
||||
"poplib",
|
||||
"posix",
|
||||
"pprint",
|
||||
"profile",
|
||||
"pstats",
|
||||
"pty",
|
||||
"pwd",
|
||||
"py_compile",
|
||||
"pyclbr",
|
||||
"pydoc",
|
||||
"queue",
|
||||
"quopri",
|
||||
"random",
|
||||
"re",
|
||||
"readline",
|
||||
"reprlib",
|
||||
"resource",
|
||||
"rlcompleter",
|
||||
"runpy",
|
||||
"sched",
|
||||
"secrets",
|
||||
"select",
|
||||
"selectors",
|
||||
"shelve",
|
||||
"shlex",
|
||||
"shutil",
|
||||
"signal",
|
||||
"site",
|
||||
"smtplib",
|
||||
"socket",
|
||||
"socketserver",
|
||||
"sqlite3",
|
||||
"ssl",
|
||||
"stat",
|
||||
"statistics",
|
||||
"string",
|
||||
"stringprep",
|
||||
"struct",
|
||||
"subprocess",
|
||||
"sys",
|
||||
"sysconfig",
|
||||
"syslog",
|
||||
"tabnanny",
|
||||
"tarfile",
|
||||
"tempfile",
|
||||
"termios",
|
||||
"test",
|
||||
"textwrap",
|
||||
"threading",
|
||||
"time",
|
||||
"timeit",
|
||||
"tkinter",
|
||||
"token",
|
||||
"tokenize",
|
||||
"tomllib",
|
||||
"trace",
|
||||
"traceback",
|
||||
"tracemalloc",
|
||||
"tty",
|
||||
"turtle",
|
||||
"turtledemo",
|
||||
"types",
|
||||
"typing",
|
||||
"unicodedata",
|
||||
"unittest",
|
||||
"urllib",
|
||||
"uuid",
|
||||
"venv",
|
||||
"warnings",
|
||||
"wave",
|
||||
"weakref",
|
||||
"webbrowser",
|
||||
"winreg",
|
||||
"winsound",
|
||||
"wsgiref",
|
||||
"xml",
|
||||
"xmlrpc",
|
||||
"zipapp",
|
||||
"zipfile",
|
||||
"zipimport",
|
||||
"zlib",
|
||||
"zoneinfo",
|
||||
];
|
||||
|
||||
pub fn parse_import(code: &str) -> Result<Vec<String>> {
|
||||
let modules = parse_python_imports(code)?;
|
||||
// 在非测试环境中,过滤标准库模块
|
||||
let mut imports = Vec::new();
|
||||
for module in modules {
|
||||
if !is_standard_library(&module) {
|
||||
imports.push(module);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(imports)
|
||||
}
|
||||
|
||||
// 解析Python代码中的import语句
|
||||
fn parse_python_imports(python_code: &str) -> Result<Vec<String>> {
|
||||
let input = if python_code.ends_with('\n') {
|
||||
python_code.to_string()
|
||||
} else {
|
||||
format!("{python_code}\n")
|
||||
};
|
||||
debug!("Processing input: {input:?}");
|
||||
|
||||
let pairs = ImportParser::parse(Rule::file, &input)
|
||||
.map_err(|e| anyhow::anyhow!("Failed to parse input: {}", e))?;
|
||||
debug!("Initial parse successful");
|
||||
|
||||
let mut imported_modules = Vec::new();
|
||||
|
||||
for pair in pairs {
|
||||
debug!("Top level rule: {:?}", pair.as_rule());
|
||||
for record in pair.into_inner() {
|
||||
debug!(
|
||||
"Record rule: {:?}, text: {:?}",
|
||||
record.as_rule(),
|
||||
record.as_str()
|
||||
);
|
||||
// 遍历 record 的内部规则
|
||||
for inner in record.into_inner() {
|
||||
debug!(
|
||||
"Inner rule: {:?}, text: {:?}",
|
||||
inner.as_rule(),
|
||||
inner.as_str()
|
||||
);
|
||||
match inner.as_rule() {
|
||||
Rule::from_import_statement => {
|
||||
for module in inner.into_inner() {
|
||||
if let Rule::module_name = module.as_rule() {
|
||||
imported_modules.push(module.as_str().to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
Rule::import_statement => {
|
||||
// 遍历 import_statement 的内部规则找到 module_name
|
||||
for module in inner.into_inner() {
|
||||
if let Rule::module_name = module.as_rule() {
|
||||
imported_modules.push(module.as_str().to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
Rule::importlib_statement => {
|
||||
// 遍历 importlib_statement 的内部规则找到 module_name
|
||||
for module in inner.into_inner() {
|
||||
if let Rule::module_name = module.as_rule() {
|
||||
imported_modules.push(module.as_str().to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
info!("Unknown rule: {:?}", inner.as_rule());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
debug!("Final result: {imported_modules:?}");
|
||||
Ok(imported_modules)
|
||||
}
|
||||
|
||||
// 判断是否为Python标准库模块
|
||||
fn is_standard_library(module: &str) -> bool {
|
||||
// 检查模块名是否在标准库列表中
|
||||
PYTHON_STDLIB_MODULES.contains(&module)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::parse_import;
|
||||
use anyhow::Result;
|
||||
use log::{LevelFilter, info};
|
||||
use std::sync::Once;
|
||||
|
||||
// 使用 Once 确保 env_logger 只初始化一次
|
||||
static INIT: Once = Once::new();
|
||||
|
||||
fn setup() {
|
||||
INIT.call_once(|| {
|
||||
env_logger::builder()
|
||||
.filter_level(LevelFilter::Debug)
|
||||
.init();
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_line() -> Result<()> {
|
||||
setup();
|
||||
let python_code = "
|
||||
import pandas as pd\n";
|
||||
info!("Testing with input: {python_code:?}");
|
||||
|
||||
let imported_modules = parse_import(python_code)?;
|
||||
info!("Parsed modules: {imported_modules:?}");
|
||||
|
||||
assert_eq!(imported_modules, vec!["pandas"]);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_line2() -> Result<()> {
|
||||
setup();
|
||||
let python_code = "
|
||||
numpy_alias = importlib.import_module('numpy')";
|
||||
info!("Testing with input: {python_code:?}");
|
||||
|
||||
let imported_modules = parse_import(python_code)?;
|
||||
info!("Parsed modules: {imported_modules:?}");
|
||||
|
||||
assert_eq!(imported_modules, vec!["numpy"]);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_import_bs() -> Result<()> {
|
||||
setup();
|
||||
let python_code = r#"
|
||||
import requests
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
# 发送请求到百度
|
||||
url = 'https://www.baidu.com'
|
||||
response = requests.get(url)
|
||||
|
||||
# 检查请求是否成功
|
||||
if response.status_code == 200:
|
||||
# 解析网页内容
|
||||
soup = BeautifulSoup(response.text, 'html.parser')
|
||||
|
||||
# 提取你想要的信息,例如标题
|
||||
title = soup.title.string
|
||||
print(f"网页标题: {title}")
|
||||
else:
|
||||
print(f"请求失败,状态码: {response.status_code}")
|
||||
|
||||
"#;
|
||||
let imported_modules = parse_import(python_code)?;
|
||||
info!("Parsed modules: {imported_modules:?}");
|
||||
assert_eq!(imported_modules, vec!["requests", "bs4"]);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_import() -> Result<()> {
|
||||
setup();
|
||||
let python_code = r#"
|
||||
import pandas as pd
|
||||
import logging
|
||||
|
||||
# 配置日志记录
|
||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
||||
|
||||
import importlib
|
||||
|
||||
# 使用 importlib 动态导入 numpy
|
||||
numpy = importlib.import_module('numpy')
|
||||
# 创建一个简单的 numpy 数组
|
||||
arr = numpy.array([1, 2, 3, 4, 5])
|
||||
# 记录数组信息
|
||||
logging.info(f"Created numpy array: {arr}")
|
||||
|
||||
# 进行一些简单的操作
|
||||
sum_arr = numpy.sum(arr)
|
||||
logging.info(f"Sum of array: {sum_arr}")
|
||||
|
||||
# 创建一个简单的 DataFrame
|
||||
data = {
|
||||
'Name': ['Alice', 'Bob', 'Charlie'],
|
||||
'Age': [25, 30, 35]
|
||||
}
|
||||
df = pd.DataFrame(data)
|
||||
|
||||
# 记录 DataFrame 信息
|
||||
logging.info(f"Created DataFrame:\n{df}")
|
||||
|
||||
def main(args: dict) -> dict:
|
||||
logging.info(f"input args: {args}")
|
||||
|
||||
params = args.get("params")
|
||||
# 构建输出对象
|
||||
ret = {
|
||||
"key0": params['input'], # 拼接两次入参 input 的值
|
||||
"key1": ["hello", "world"], # 输出一个数组
|
||||
"key2": { # 输出一个Object
|
||||
"key21": "hi"
|
||||
},
|
||||
}
|
||||
return ret
|
||||
"#;
|
||||
|
||||
let imported_modules = parse_import(python_code)?;
|
||||
info!("Parsed modules: {imported_modules:?}");
|
||||
assert_eq!(imported_modules, vec!["pandas", "numpy"]);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
5
qiming-run_code_rmcp/src/python_runner/mod.rs
Normal file
5
qiming-run_code_rmcp/src/python_runner/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
mod dependencies;
|
||||
mod python_runner;
|
||||
|
||||
pub use dependencies::parse_import;
|
||||
pub use python_runner::PythonRunner;
|
||||
200
qiming-run_code_rmcp/src/python_runner/python_runner.rs
Normal file
200
qiming-run_code_rmcp/src/python_runner/python_runner.rs
Normal file
@@ -0,0 +1,200 @@
|
||||
//通过 uv 命令,来运行 python脚本
|
||||
use crate::{
|
||||
cache::CodeFileCache,
|
||||
model::{CodeExecutor, CodeScriptExecutionResult, CommandExecutor, LanguageScript, RunCode},
|
||||
python_runner::parse_import,
|
||||
};
|
||||
use anyhow::{Context, Result};
|
||||
use log::{debug, error, info, warn};
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
use tempfile::NamedTempFile;
|
||||
use tokio::fs;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use tokio::process::Command;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct PythonRunner;
|
||||
|
||||
//定义国内python加速地址: https://mirrors.aliyun.com/pypi/simple
|
||||
const PYTHON_ACCELERATION_ADDRESS: &str = "https://mirrors.aliyun.com/pypi/simple";
|
||||
|
||||
impl RunCode for PythonRunner {
|
||||
async fn run_with_params(
|
||||
&self,
|
||||
code: &str,
|
||||
params: Option<serde_json::Value>,
|
||||
timeout_seconds: Option<u64>,
|
||||
) -> Result<CodeScriptExecutionResult> {
|
||||
debug!("开始执行Python脚本...,执行参数: {params:?}");
|
||||
// 根据 code ,获取对应的hash, 对用户脚本代码,使用胶水代码处理后,缓存到文件系统里,下次使用如果hash相同,直接使用
|
||||
let hash = CodeFileCache::obtain_code_hash(code);
|
||||
let cache_exist =
|
||||
CodeFileCache::check_code_file_cache_exisht(&hash, &LanguageScript::Python).await;
|
||||
|
||||
let run_code_script_file_tuple = if cache_exist {
|
||||
// 从缓存中读取代码
|
||||
let cache_code =
|
||||
CodeFileCache::get_code_file_cache(&hash, &LanguageScript::Python).await;
|
||||
debug!("从缓存中读取代码:hash值 {:?}", &hash);
|
||||
cache_code?
|
||||
} else {
|
||||
// 分析用户python代码依赖
|
||||
let dependencies = parse_import(code)?;
|
||||
// 准备Python代码,添加日志捕获和handler函数执行逻辑
|
||||
let wrapped_code = self.prepare_python_code(code, true);
|
||||
// 缓存代码
|
||||
CodeFileCache::save_code_file_cache(&hash, &wrapped_code, &LanguageScript::Python)
|
||||
.await?;
|
||||
|
||||
//保存后,获取文件
|
||||
let code_script_file_tuple =
|
||||
CodeFileCache::get_code_file_cache(&hash, &LanguageScript::Python).await?;
|
||||
let run_code_script_file_path = code_script_file_tuple.1.clone();
|
||||
// 按照 uv命令的规范,添加用户脚本所需的依赖
|
||||
// uv add --script example.py 'requests<3' 'rich' 这是添加依赖的参考命令,dependencies 是解析出来的依赖列表
|
||||
|
||||
// 执行添加依赖命令
|
||||
if !dependencies.is_empty() {
|
||||
info!("正在添加依赖: {dependencies:?}");
|
||||
let mut cmd = Command::new("uv");
|
||||
cmd.arg("add")
|
||||
.arg("--script")
|
||||
.arg(&run_code_script_file_path);
|
||||
//添加 python加速地址
|
||||
cmd.arg("--default-index").arg(PYTHON_ACCELERATION_ADDRESS);
|
||||
|
||||
// 为每个依赖添加一个参数
|
||||
for dep in &dependencies {
|
||||
cmd.arg(dep);
|
||||
}
|
||||
|
||||
// 打印 cmd 命令,可以直接复制执行的命令字符串
|
||||
let cmd_str = format!("{:?}", &cmd);
|
||||
info!("uv命令字符串: {cmd_str}");
|
||||
|
||||
let cmd_output = match cmd.kill_on_drop(true).output().await {
|
||||
Ok(output) => output,
|
||||
Err(e) => {
|
||||
error!("安装Python依赖失败: {e:?}");
|
||||
error!("失败的命令: {cmd:?}");
|
||||
return Err(e).context("Failed to add dependencies with uv");
|
||||
}
|
||||
};
|
||||
|
||||
let stdout = String::from_utf8_lossy(&cmd_output.stdout).to_string();
|
||||
let stderr = String::from_utf8_lossy(&cmd_output.stderr).to_string();
|
||||
info!("添加依赖结果 - stdout: {stdout}");
|
||||
info!("添加依赖结果 - stderr: {stderr}");
|
||||
|
||||
if !cmd_output.status.success() {
|
||||
warn!("添加依赖失败,状态码: {}", cmd_output.status);
|
||||
}
|
||||
}
|
||||
debug!("创建脚本缓存:hash值 {:?}", &hash);
|
||||
code_script_file_tuple
|
||||
};
|
||||
|
||||
let temp_path = run_code_script_file_tuple.1;
|
||||
|
||||
// 使用uv run命令执行Python脚本,提供隔离环境
|
||||
let mut execute_command = Command::new("uv");
|
||||
//还需要指定国内镜像地址,参考示例: uv run -s -p 3.13 d5ebe48b7d9da8cb835af6ef77b212921f9a44881fb232837b4dcc6ebecf9401.py --default-index https://mirrors.aliyun.com/pypi/simple
|
||||
execute_command
|
||||
.arg("run")
|
||||
.arg("-s") // 明确指定作为脚本运行
|
||||
// .arg("-p")
|
||||
// .arg("3.13") // 指定Python解释器版本3.13
|
||||
.arg("--default-index")
|
||||
.arg(PYTHON_ACCELERATION_ADDRESS)
|
||||
.arg(&temp_path)
|
||||
.kill_on_drop(true);
|
||||
|
||||
// 处理参数:统一使用临时文件传递
|
||||
let temp_input_path = if let Some(params) = params {
|
||||
let params_json = serde_json::to_string(¶ms)?;
|
||||
|
||||
// 创建临时文件写入参数
|
||||
let temp_dir = tempfile::TempDir::new()?;
|
||||
let temp_file_path = temp_dir.path().join("input_params.json");
|
||||
|
||||
// 写入参数到临时文件
|
||||
std::fs::write(&temp_file_path, params_json.as_bytes())?;
|
||||
|
||||
// 保持TempDir存在(这样文件就不会被删除)
|
||||
let temp_dir_path = temp_dir.path().to_path_buf();
|
||||
std::mem::forget(temp_dir);
|
||||
|
||||
// 设置环境变量指向临时文件
|
||||
execute_command.env("INPUT_JSON_FILE", &temp_file_path);
|
||||
debug!("使用临时文件传递参数,文件路径: {:?}", temp_file_path);
|
||||
|
||||
Some(temp_file_path)
|
||||
} else {
|
||||
// 没有参数时设置空对象
|
||||
execute_command.env("INPUT_JSON", "{}");
|
||||
None
|
||||
};
|
||||
|
||||
info!("执行命令: {:?}", &execute_command);
|
||||
|
||||
// let tokio_child_command = TokioHeapSize::default();
|
||||
// // 设置堆大小限制
|
||||
// tokio_child_command
|
||||
// .with_heap_limit(&mut execute_command)
|
||||
// .await;
|
||||
|
||||
//限制command 的执行超时时间
|
||||
let executor = match timeout_seconds {
|
||||
Some(timeout) => CommandExecutor::with_timeout(execute_command.output(), timeout),
|
||||
None => CommandExecutor::default(execute_command.output()),
|
||||
};
|
||||
|
||||
let executor_result = executor.await;
|
||||
let output = match executor_result {
|
||||
Ok(cmd_result) => match cmd_result {
|
||||
Ok(output) => output,
|
||||
Err(e) => {
|
||||
error!("Python命令执行失败: {e:?}");
|
||||
return Err(e.into());
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
error!("Python任务执行异常: {e:?}");
|
||||
return Err(e.into());
|
||||
}
|
||||
};
|
||||
// 调试输出
|
||||
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
|
||||
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
|
||||
debug!("Python stdout: {stdout}");
|
||||
debug!("Python stderr: {stderr}");
|
||||
|
||||
// 执行完成后删除临时文件和目录
|
||||
if let Some(temp_file_path) = temp_input_path {
|
||||
// 删除文件
|
||||
let _ = fs::remove_file(&temp_file_path).await;
|
||||
// 尝试删除父目录(如果为空)
|
||||
if let Some(parent) = temp_file_path.parent() {
|
||||
let _ = fs::remove_dir(parent).await;
|
||||
}
|
||||
debug!("已删除临时文件: {:?}", temp_file_path);
|
||||
}
|
||||
|
||||
// 解析输出
|
||||
CodeExecutor::parse_execution_output(&output.stdout, &output.stderr).await
|
||||
}
|
||||
}
|
||||
|
||||
impl PythonRunner {
|
||||
/// 准备Python代码,添加日志捕获和handler函数执行逻辑
|
||||
fn prepare_python_code(&self, code: &str, show_logs: bool) -> String {
|
||||
let show_logs_value = if show_logs { "True" } else { "False" };
|
||||
|
||||
let template = include_str!("../templates/python_template.py");
|
||||
|
||||
template
|
||||
.replace("{{USER_CODE}}", code)
|
||||
.replace("{{SHOW_LOGS}}", show_logs_value)
|
||||
}
|
||||
}
|
||||
166
qiming-run_code_rmcp/src/script_runner.rs
Normal file
166
qiming-run_code_rmcp/src/script_runner.rs
Normal file
@@ -0,0 +1,166 @@
|
||||
use anyhow::{Context, Result};
|
||||
use clap::Parser;
|
||||
use log::{error, info};
|
||||
use rmcp::ServiceExt;
|
||||
use tokio::io::{stdin, stdout};
|
||||
|
||||
mod app_error;
|
||||
mod cache;
|
||||
mod deno_runner;
|
||||
mod mcp;
|
||||
mod model;
|
||||
mod python_runner;
|
||||
|
||||
use mcp::CodeRunnerService;
|
||||
|
||||
/// MCP脚本运行器 - 通过MCP协议执行JavaScript、TypeScript和Python代码
|
||||
#[derive(Parser)]
|
||||
#[command(name = "script_runner")]
|
||||
#[command(author = "MCP Script Runner")]
|
||||
#[command(version = "0.1.0")]
|
||||
#[command(about = "通过MCP协议执行JavaScript、TypeScript和Python代码", long_about = None)]
|
||||
struct Cli {
|
||||
/// 启用详细日志输出
|
||||
#[arg(short, long)]
|
||||
verbose: bool,
|
||||
}
|
||||
|
||||
/// 启动MCP服务器
|
||||
async fn start_mcp_server(verbose: bool) -> Result<()> {
|
||||
if verbose {
|
||||
info!("初始化 MCP 服务...");
|
||||
}
|
||||
|
||||
// 创建服务实例
|
||||
let service = CodeRunnerService::default();
|
||||
|
||||
// 使用标准输入输出作为传输方式
|
||||
let transport = (stdin(), stdout());
|
||||
|
||||
if verbose {
|
||||
info!("MCP 服务已启动,等待连接...");
|
||||
}
|
||||
|
||||
// 启动服务
|
||||
let server = service.serve(transport).await.context("启动MCP服务失败")?;
|
||||
|
||||
// 等待服务结束
|
||||
let result = server.waiting().await;
|
||||
|
||||
if verbose {
|
||||
match &result {
|
||||
Ok(reason) => error!("MCP 服务已停止: {:?}", reason),
|
||||
Err(err) => error!("MCP 服务出错: {}", err),
|
||||
}
|
||||
}
|
||||
|
||||
result.map(|_| ()).map_err(Into::into)
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
// 解析命令行参数
|
||||
let cli = Cli::parse();
|
||||
|
||||
// 启动MCP服务
|
||||
start_mcp_server(cli.verbose).await
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use rmcp::{model::CallToolRequestParam, ServiceExt};
|
||||
use tokio::sync::oneshot;
|
||||
use tokio::time::timeout;
|
||||
use std::time::Duration;
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore = "MCP client test requires proper service implementation - TODO: fix later"]
|
||||
async fn test_start_mcp_server() -> Result<()> {
|
||||
// 创建管道,模拟标准输入输出
|
||||
let (client_stream, server_stream) = tokio::io::duplex(8192);
|
||||
|
||||
// 分割成读写部分
|
||||
let (server_read, server_write) = tokio::io::split(server_stream);
|
||||
let (client_read, client_write) = tokio::io::split(client_stream);
|
||||
|
||||
// 使用通道控制服务器生命周期
|
||||
let (shutdown_tx, shutdown_rx) = oneshot::channel::<()>();
|
||||
|
||||
// 在单独的任务中启动服务器
|
||||
let server_task = tokio::spawn(async move {
|
||||
// 创建服务实例
|
||||
let service = CodeRunnerService::default();
|
||||
|
||||
// 创建服务Future并pin
|
||||
let service_fut = service.serve((server_read, server_write));
|
||||
tokio::pin!(service_fut);
|
||||
|
||||
// 等待关闭信号或服务器完成
|
||||
tokio::select! {
|
||||
res = &mut service_fut => {
|
||||
match res {
|
||||
Ok(server) => {
|
||||
match server.waiting().await {
|
||||
Ok(reason) => println!("服务正常结束: {:?}", reason),
|
||||
Err(e) => println!("服务错误: {}", e),
|
||||
}
|
||||
}
|
||||
Err(e) => println!("启动服务失败: {}", e),
|
||||
}
|
||||
}
|
||||
_ = shutdown_rx => {
|
||||
println!("收到关闭信号");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 给服务器一点时间启动
|
||||
tokio::time::sleep(Duration::from_millis(200)).await;
|
||||
|
||||
// 使用RMCP客户端SDK连接到服务器
|
||||
let client = ().serve((client_read, client_write)).await?;
|
||||
|
||||
// 获取服务器信息
|
||||
let server_info = client.peer_info();
|
||||
println!("服务器信息: {:?}", server_info);
|
||||
|
||||
// 验证服务器信息
|
||||
assert!(!server_info.unwrap().instructions.is_none(), "服务器应该提供说明");
|
||||
|
||||
// 测试执行JavaScript代码
|
||||
let js_code = "function handler(input) { return {success: true, message: 'JavaScript测试成功'}; }";
|
||||
let result = client.call_tool(CallToolRequestParam {
|
||||
name: "run_javascript".into(),
|
||||
arguments: serde_json::json!({
|
||||
"code": js_code,
|
||||
}).as_object().cloned(),
|
||||
}).await?;
|
||||
|
||||
// 验证JavaScript结果,这里只简单验证调用成功
|
||||
println!("JavaScript执行结果: {:?}", result);
|
||||
|
||||
// 测试执行Python代码
|
||||
let py_code = "def handler(input):\n return {'success': True, 'message': 'Python测试成功'}";
|
||||
let result = client.call_tool(CallToolRequestParam {
|
||||
name: "run_python".into(),
|
||||
arguments: serde_json::json!({
|
||||
"code": py_code,
|
||||
}).as_object().cloned(),
|
||||
}).await?;
|
||||
|
||||
// 验证Python结果,这里只简单验证调用成功
|
||||
println!("Python执行结果: {:?}", result);
|
||||
|
||||
// 关闭客户端
|
||||
client.cancel().await?;
|
||||
|
||||
// 发送服务器关闭信号
|
||||
let _ = shutdown_tx.send(());
|
||||
|
||||
// 等待服务器任务结束
|
||||
let _ = timeout(Duration::from_secs(5), server_task).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
92
qiming-run_code_rmcp/src/templates/js_template_es.js
Normal file
92
qiming-run_code_rmcp/src/templates/js_template_es.js
Normal file
@@ -0,0 +1,92 @@
|
||||
// @ts-nocheck
|
||||
// ES模块格式,支持import/export语句
|
||||
|
||||
// 保存原始console.log
|
||||
const originalConsoleLog = console.log;
|
||||
let logs = [];
|
||||
|
||||
// 替换console.log以捕获日志
|
||||
console.log = function() {
|
||||
// 将参数转换为字符串并连接它们
|
||||
const message = Array.from(arguments).map(arg =>
|
||||
typeof arg === 'object' && arg !== null ? JSON.stringify(arg) : String(arg)
|
||||
).join(' ');
|
||||
|
||||
// 存储日志
|
||||
logs.push(message);
|
||||
|
||||
// 如果显示日志,也输出到原始控制台
|
||||
if ({{SHOW_LOGS}}) {
|
||||
originalConsoleLog.apply(console, arguments);
|
||||
}
|
||||
};
|
||||
|
||||
// 异步读取输入参数的函数
|
||||
async function readInputParams() {
|
||||
let input = {};
|
||||
try {
|
||||
const inputFile = Deno.env.get("INPUT_JSON_FILE");
|
||||
if (inputFile) {
|
||||
const inputJson = await Deno.readTextFile(inputFile);
|
||||
input = JSON.parse(inputJson);
|
||||
console.log("接收到的参数:", JSON.stringify(input));
|
||||
} else {
|
||||
// 兼容旧的环境变量方式
|
||||
const inputJson = Deno.env.get("INPUT_JSON");
|
||||
if (inputJson) {
|
||||
input = JSON.parse(inputJson);
|
||||
console.log("接收到的参数:", JSON.stringify(input));
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("解析输入参数失败:", error);
|
||||
}
|
||||
return input;
|
||||
}
|
||||
|
||||
// 用户代码
|
||||
{{USER_CODE}}
|
||||
|
||||
// 执行handler函数并获取结果
|
||||
let result = null;
|
||||
|
||||
// 异步立即执行函数
|
||||
(async () => {
|
||||
// 读取输入参数
|
||||
const input = await readInputParams();
|
||||
|
||||
try {
|
||||
// 优先检查main函数
|
||||
if (typeof main === 'function') {
|
||||
// 检查main是否是异步函数
|
||||
if (main.constructor.name === 'AsyncFunction') {
|
||||
result = await main(input);
|
||||
} else {
|
||||
result = main(input);
|
||||
}
|
||||
} else if (typeof handler === 'function') {
|
||||
// 如果没有main函数,检查handler
|
||||
if (handler.constructor.name === 'AsyncFunction') {
|
||||
result = await handler(input);
|
||||
} else {
|
||||
result = handler(input);
|
||||
}
|
||||
}else{
|
||||
throw new Error("没有找到main或handler函数");
|
||||
}
|
||||
|
||||
// 打印最终输出为JSON
|
||||
originalConsoleLog(JSON.stringify({
|
||||
logs: logs,
|
||||
result: result !== undefined ? (typeof result === 'object' ? JSON.stringify(result) : String(result)) : null,
|
||||
error: null
|
||||
}));
|
||||
} catch (error) {
|
||||
// 处理错误
|
||||
originalConsoleLog(JSON.stringify({
|
||||
logs: logs,
|
||||
result: null,
|
||||
error: error.toString()
|
||||
}));
|
||||
}
|
||||
})();
|
||||
93
qiming-run_code_rmcp/src/templates/js_template_normal.js
Normal file
93
qiming-run_code_rmcp/src/templates/js_template_normal.js
Normal file
@@ -0,0 +1,93 @@
|
||||
// @ts-nocheck
|
||||
// 普通脚本格式
|
||||
|
||||
// 保存原始console.log
|
||||
const originalConsoleLog = console.log;
|
||||
let logs = [];
|
||||
|
||||
// 替换console.log以捕获日志
|
||||
console.log = function() {
|
||||
// 将参数转换为字符串并连接它们
|
||||
const message = Array.from(arguments).map(arg =>
|
||||
typeof arg === 'object' && arg !== null ? JSON.stringify(arg) : String(arg)
|
||||
).join(' ');
|
||||
|
||||
// 存储日志
|
||||
logs.push(message);
|
||||
|
||||
// 如果显示日志,也输出到原始控制台
|
||||
if ({{SHOW_LOGS}}) {
|
||||
originalConsoleLog.apply(console, arguments);
|
||||
}
|
||||
};
|
||||
|
||||
// 异步读取输入参数的函数
|
||||
async function readInputParams() {
|
||||
let input = {};
|
||||
try {
|
||||
const inputFile = Deno.env.get("INPUT_JSON_FILE");
|
||||
if (inputFile) {
|
||||
const inputJson = await Deno.readTextFile(inputFile);
|
||||
input = JSON.parse(inputJson);
|
||||
console.log("接收到的参数:", JSON.stringify(input));
|
||||
} else {
|
||||
// 兼容旧的环境变量方式
|
||||
const inputJson = Deno.env.get("INPUT_JSON");
|
||||
if (inputJson) {
|
||||
input = JSON.parse(inputJson);
|
||||
console.log("接收到的参数:", JSON.stringify(input));
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("解析输入参数失败:", error);
|
||||
}
|
||||
return input;
|
||||
}
|
||||
|
||||
// 异步立即执行函数
|
||||
(async () => {
|
||||
// 读取输入参数
|
||||
const input = await readInputParams();
|
||||
|
||||
try {
|
||||
// 用户代码开始
|
||||
{{USER_CODE}}
|
||||
// 用户代码结束
|
||||
|
||||
// 执行函数并获取结果
|
||||
let result = null;
|
||||
|
||||
// 优先检查main函数
|
||||
if (typeof main === 'function') {
|
||||
// 检查main是否是异步函数
|
||||
if (main.constructor.name === 'AsyncFunction') {
|
||||
result = await main(input);
|
||||
} else {
|
||||
result = main(input);
|
||||
}
|
||||
} else if (typeof handler === 'function') {
|
||||
// 如果没有main函数,检查handler
|
||||
if (handler.constructor.name === 'AsyncFunction') {
|
||||
result = await handler(input);
|
||||
} else {
|
||||
result = handler(input);
|
||||
}
|
||||
} else {
|
||||
throw new Error("没有找到main或handler函数");
|
||||
}
|
||||
|
||||
// 打印最终输出为JSON
|
||||
originalConsoleLog(JSON.stringify({
|
||||
logs: logs,
|
||||
result: result !== undefined ? (typeof result === 'object' ? JSON.stringify(result) : String(result)) : null,
|
||||
error: null
|
||||
}));
|
||||
} catch (error) {
|
||||
// 处理错误
|
||||
originalConsoleLog(JSON.stringify({
|
||||
logs: logs,
|
||||
result: null,
|
||||
error: error.toString()
|
||||
}));
|
||||
}
|
||||
})();
|
||||
134
qiming-run_code_rmcp/src/templates/python_template.py
Normal file
134
qiming-run_code_rmcp/src/templates/python_template.py
Normal file
@@ -0,0 +1,134 @@
|
||||
import sys
|
||||
import json
|
||||
import os
|
||||
import logging
|
||||
from io import StringIO
|
||||
|
||||
# 保存原始的stdout
|
||||
original_stdout = sys.stdout
|
||||
logs = []
|
||||
|
||||
# 创建一个StringIO对象来捕获输出
|
||||
class LogCapture:
|
||||
def __init__(self, show_logs=False):
|
||||
self.buffer = StringIO()
|
||||
self.show_logs = show_logs
|
||||
|
||||
def write(self, text):
|
||||
if text.strip(): # 忽略空行
|
||||
logs.append(text.rstrip())
|
||||
if self.show_logs:
|
||||
original_stdout.write(text)
|
||||
|
||||
def flush(self):
|
||||
self.buffer.flush()
|
||||
if self.show_logs:
|
||||
original_stdout.flush()
|
||||
|
||||
# 替换sys.stdout为我们的捕获器
|
||||
sys.stdout = LogCapture(show_logs={{SHOW_LOGS}})
|
||||
|
||||
# 配置logging,将日志输出重定向到我们的捕获器
|
||||
class LoggingHandler(logging.Handler):
|
||||
def emit(self, record):
|
||||
msg = self.format(record)
|
||||
print(f"[{record.levelname}] {msg}")
|
||||
|
||||
# 配置根日志记录器
|
||||
root_logger = logging.getLogger()
|
||||
root_logger.setLevel(logging.INFO)
|
||||
# 移除所有现有的处理程序
|
||||
for handler in root_logger.handlers[:]:
|
||||
root_logger.removeHandler(handler)
|
||||
# 添加我们自定义的处理程序
|
||||
handler = LoggingHandler()
|
||||
handler.setFormatter(logging.Formatter('%(message)s'))
|
||||
root_logger.addHandler(handler)
|
||||
|
||||
# 从环境变量获取输入参数
|
||||
args = {}
|
||||
has_input = False
|
||||
try:
|
||||
# 优先从临时文件读取参数
|
||||
input_file = os.environ.get('INPUT_JSON_FILE')
|
||||
if input_file:
|
||||
with open(input_file, 'r', encoding='utf-8') as f:
|
||||
input_json = f.read()
|
||||
args = json.loads(input_json)
|
||||
has_input = True
|
||||
print(f"接收到的参数: {args}")
|
||||
else:
|
||||
# 兼容旧的环境变量方式
|
||||
input_json = os.environ.get('INPUT_JSON')
|
||||
if input_json:
|
||||
args = json.loads(input_json)
|
||||
has_input = True
|
||||
print(f"接收到的参数: {args}")
|
||||
|
||||
# 确保参数同时可以通过args直接访问,也可以通过args.get("params")访问
|
||||
# 如果args中没有params键,但有其他键,则将整个args作为params的值
|
||||
if has_input and "params" not in args and len(args) > 0:
|
||||
args["params"] = args.copy()
|
||||
# 如果params存在但为None,则初始化为空字典
|
||||
elif has_input and args.get("params") is None:
|
||||
args["params"] = {}
|
||||
except Exception as e:
|
||||
print(f"解析输入参数失败: {e}")
|
||||
|
||||
# 用户代码开始
|
||||
{{USER_CODE}}
|
||||
|
||||
try:
|
||||
# 执行handler函数或main函数并获取结果
|
||||
result = None
|
||||
if 'handler' in globals() and callable(globals()['handler']):
|
||||
# 优先使用 handler 函数
|
||||
try:
|
||||
if has_input:
|
||||
result = handler(args)
|
||||
else:
|
||||
# handler 函数不接受参数,无参数调用
|
||||
result = handler()
|
||||
# 确保结果不为 None
|
||||
if result is None:
|
||||
print("警告: handler 函数返回了 None")
|
||||
except Exception as e:
|
||||
print(f"执行 handler 函数时出错: {e}")
|
||||
elif 'main' in globals() and callable(globals()['main']):
|
||||
# 如果没有 handler 函数,尝试使用 main 函数
|
||||
try:
|
||||
result = main(args)
|
||||
except Exception as e:
|
||||
print(f"执行 main 函数时出错: {e}")
|
||||
|
||||
# 打印最终输出为JSON
|
||||
sys.stdout = original_stdout
|
||||
# 根据结果类型选择合适的处理方式
|
||||
result_json = None
|
||||
if result is not None:
|
||||
if isinstance(result, (dict, list)):
|
||||
# 如果是字典或列表,直接使用 json.dumps 序列化
|
||||
result_json = json.dumps(result)
|
||||
elif isinstance(result, (int, float, bool)) or result is None:
|
||||
# 如果是基本类型,直接传递给外层 JSON
|
||||
result_json = result
|
||||
else:
|
||||
# 其他类型(如字符串)转换为字符串
|
||||
result_json = str(result)
|
||||
|
||||
print(json.dumps({
|
||||
'logs': logs,
|
||||
'result': result_json,
|
||||
'error': None
|
||||
}))
|
||||
except Exception as e:
|
||||
# 处理错误
|
||||
import traceback
|
||||
error_msg = f"{str(e)}\n{traceback.format_exc()}"
|
||||
# 处理错误
|
||||
sys.stdout = original_stdout
|
||||
print(json.dumps({
|
||||
'logs': logs,
|
||||
'result': None,
|
||||
'error': error_msg
|
||||
}))
|
||||
96
qiming-run_code_rmcp/src/templates/ts_template.ts
Normal file
96
qiming-run_code_rmcp/src/templates/ts_template.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
// TypeScript类型声明
|
||||
// @ts-nocheck
|
||||
type LogFunction = (...args: any[]) => void;
|
||||
type Handler = (input: any) => any;
|
||||
|
||||
// Save original console.log
|
||||
const originalConsoleLog: LogFunction = console.log;
|
||||
let logs: string[] = [];
|
||||
|
||||
// Replace console.log to capture logs
|
||||
console.log = function(...args: any[]): void {
|
||||
// Convert arguments to string and join them
|
||||
const message = args.map(arg =>
|
||||
typeof arg === 'object' && arg !== null ? JSON.stringify(arg) : String(arg)
|
||||
).join(' ');
|
||||
|
||||
// Store log
|
||||
logs.push(message);
|
||||
|
||||
// Also log to original console if showing logs
|
||||
if ({{SHOW_LOGS}}) {
|
||||
originalConsoleLog.apply(console, args);
|
||||
}
|
||||
};
|
||||
|
||||
// 异步读取输入参数的函数
|
||||
async function readInputParams(): Promise<any> {
|
||||
let input: any = {};
|
||||
try {
|
||||
const inputFile = Deno.env.get("INPUT_JSON_FILE");
|
||||
if (inputFile) {
|
||||
const inputJson = await Deno.readTextFile(inputFile);
|
||||
input = JSON.parse(inputJson);
|
||||
console.log("接收到的参数:", JSON.stringify(input));
|
||||
} else {
|
||||
// 兼容旧的环境变量方式
|
||||
const inputJson = Deno.env.get("INPUT_JSON");
|
||||
if (inputJson) {
|
||||
input = JSON.parse(inputJson);
|
||||
console.log("接收到的参数:", JSON.stringify(input));
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("解析输入参数失败:", error);
|
||||
}
|
||||
return input;
|
||||
}
|
||||
|
||||
async function executeHandler() {
|
||||
try {
|
||||
// Add the original code
|
||||
{{USER_CODE}}
|
||||
|
||||
// 读取输入参数
|
||||
const input = await readInputParams();
|
||||
|
||||
// Execute handler function and get result
|
||||
let result: any = null;
|
||||
|
||||
// 优先检查main函数
|
||||
if (typeof main === 'function') {
|
||||
// 检查main是否是异步函数
|
||||
if (main.constructor.name === 'AsyncFunction') {
|
||||
result = await (main as (input: any) => Promise<any>)(input);
|
||||
} else {
|
||||
result = (main as Handler)(input);
|
||||
}
|
||||
} else if (typeof handler === 'function') {
|
||||
// 如果没有main函数,检查handler
|
||||
if (handler.constructor.name === 'AsyncFunction') {
|
||||
result = await (handler as (input: any) => Promise<any>)(input);
|
||||
} else {
|
||||
result = (handler as Handler)(input);
|
||||
}
|
||||
} else {
|
||||
throw new Error("没有找到main或handler函数");
|
||||
}
|
||||
|
||||
// Print final output as JSON
|
||||
originalConsoleLog(JSON.stringify({
|
||||
logs: logs,
|
||||
result: result !== undefined ? (typeof result === 'object' ? JSON.stringify(result) : String(result)) : null,
|
||||
error: null
|
||||
}));
|
||||
} catch (error) {
|
||||
// Handle errors
|
||||
originalConsoleLog(JSON.stringify({
|
||||
logs: logs,
|
||||
result: null,
|
||||
error: String(error)
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
// 执行并等待结果
|
||||
executeHandler();
|
||||
740
qiming-run_code_rmcp/src/tests/js_tests.rs
Normal file
740
qiming-run_code_rmcp/src/tests/js_tests.rs
Normal file
@@ -0,0 +1,740 @@
|
||||
#[cfg(test)]
|
||||
mod js_tests {
|
||||
use anyhow::Result;
|
||||
use log::info;
|
||||
use serde_json::json;
|
||||
|
||||
use crate::model::{CodeExecutor, LanguageScript};
|
||||
use crate::tests::test_utils::setup;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_js_basic_execution() -> Result<()> {
|
||||
// 初始化日志
|
||||
setup();
|
||||
|
||||
// 从 fixtures 目录读取测试脚本
|
||||
let code = std::fs::read_to_string("fixtures/test_js.js")?;
|
||||
info!("读取测试脚本: test_js.js");
|
||||
|
||||
// 执行脚本
|
||||
info!("开始执行JavaScript脚本...");
|
||||
let result =
|
||||
CodeExecutor::execute_with_params_compat(&code, LanguageScript::Js, None).await?;
|
||||
info!("脚本执行完成, 日志: {:?}", result.logs);
|
||||
|
||||
if let Some(error) = &result.error {
|
||||
info!("执行错误: {error}");
|
||||
} else {
|
||||
info!("脚本执行成功");
|
||||
}
|
||||
|
||||
if let Some(result_val) = &result.result {
|
||||
info!("执行结果: {result_val:?}");
|
||||
} else {
|
||||
info!("无执行结果");
|
||||
}
|
||||
|
||||
// 验证结果
|
||||
assert!(!result.logs.is_empty(), "日志不应为空");
|
||||
assert!(result.error.is_none(), "不应有错误");
|
||||
assert!(result.result.is_some(), "应有返回结果");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_js_with_params() -> Result<()> {
|
||||
// 初始化日志
|
||||
setup();
|
||||
|
||||
// 从 fixtures 目录读取测试脚本
|
||||
let code = std::fs::read_to_string("fixtures/test_js_params.js")?;
|
||||
info!("读取测试脚本: test_js_params.js");
|
||||
|
||||
// 准备参数
|
||||
let params = json!({
|
||||
"a": 10,
|
||||
"b": 20,
|
||||
"name": "测试用户"
|
||||
});
|
||||
info!("准备测试参数: {params:?}");
|
||||
|
||||
// 执行脚本
|
||||
info!("开始执行JavaScript脚本...");
|
||||
let result =
|
||||
CodeExecutor::execute_with_params_compat(&code, LanguageScript::Js, Some(params))
|
||||
.await?;
|
||||
info!("脚本执行完成, 日志: {:?}", result.logs);
|
||||
|
||||
if let Some(error) = &result.error {
|
||||
info!("执行错误: {error}");
|
||||
} else {
|
||||
info!("脚本执行成功");
|
||||
}
|
||||
|
||||
if let Some(result_val) = &result.result {
|
||||
info!("执行结果: {result_val:?}");
|
||||
} else {
|
||||
info!("无执行结果");
|
||||
}
|
||||
|
||||
// 验证结果
|
||||
assert!(!result.logs.is_empty(), "日志不应为空");
|
||||
assert!(result.error.is_none(), "不应有错误");
|
||||
assert!(result.result.is_some(), "应有返回结果");
|
||||
|
||||
// 验证返回值
|
||||
if let Some(result_val) = result.result {
|
||||
// 检查结果是否包含预期的字段
|
||||
let json_str = serde_json::to_string(&result_val)?;
|
||||
assert!(json_str.contains("sum"), "结果应包含 sum 字段");
|
||||
assert!(json_str.contains("greeting"), "结果应包含 greeting 字段");
|
||||
assert!(json_str.contains("message"), "结果应包含 message 字段");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_js_object_result() -> Result<()> {
|
||||
// 初始化日志
|
||||
setup();
|
||||
|
||||
// 从 fixtures 目录读取测试脚本
|
||||
let code = std::fs::read_to_string("fixtures/rfunction_test1.js")?;
|
||||
info!("读取测试脚本: rfunction_test1.js");
|
||||
|
||||
// 准备参数
|
||||
let params = json!({
|
||||
"a": 1,
|
||||
"b": 2
|
||||
});
|
||||
info!("准备测试参数: {params:?}");
|
||||
|
||||
// 执行脚本
|
||||
info!("开始执行JavaScript脚本...");
|
||||
let result =
|
||||
CodeExecutor::execute_with_params_compat(&code, LanguageScript::Js, Some(params))
|
||||
.await?;
|
||||
info!("脚本执行完成, 日志: {:?}", result.logs);
|
||||
|
||||
if let Some(error) = &result.error {
|
||||
info!("执行错误: {error}");
|
||||
} else {
|
||||
info!("脚本执行成功");
|
||||
}
|
||||
|
||||
if let Some(result_val) = &result.result {
|
||||
info!("执行结果: {result_val:?}");
|
||||
} else {
|
||||
info!("无执行结果");
|
||||
}
|
||||
|
||||
// 验证结果
|
||||
assert!(result.error.is_none(), "不应有错误");
|
||||
assert!(result.result.is_some(), "应有返回结果");
|
||||
|
||||
// 验证返回值
|
||||
if let Some(result_val) = result.result {
|
||||
// 检查结果是否包含预期的字段
|
||||
let json_str = serde_json::to_string(&result_val)?;
|
||||
assert!(json_str.contains("message"), "结果应包含 message 字段");
|
||||
assert!(json_str.contains("3"), "message 字段应为 3");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_js_with_timeout() -> Result<()> {
|
||||
// 初始化日志
|
||||
setup();
|
||||
|
||||
// 创建一个会超时的JavaScript脚本
|
||||
let code = r#"
|
||||
// 创建一个长时间运行的脚本
|
||||
function sleep(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
async function handler(input) {
|
||||
console.log("开始执行耗时操作");
|
||||
|
||||
// 这个操作会运行10秒钟
|
||||
for (let i = 0; i < 10; i++) {
|
||||
console.log(`已经执行了 ${i+1} 秒`);
|
||||
await sleep(1000); // 等待1秒
|
||||
}
|
||||
|
||||
console.log("操作完成");
|
||||
return { result: "完成" };
|
||||
}
|
||||
"#;
|
||||
info!("创建了一个会运行10秒的测试脚本");
|
||||
|
||||
// 准备参数
|
||||
let params = json!({
|
||||
"test": "超时测试"
|
||||
});
|
||||
info!("准备测试参数: {params:?}");
|
||||
|
||||
// 设置2秒超时并执行脚本
|
||||
info!("开始执行JavaScript脚本,设置2秒超时...");
|
||||
let start_time = std::time::Instant::now();
|
||||
let result =
|
||||
CodeExecutor::execute_with_params(code, LanguageScript::Js, Some(params), Some(2))
|
||||
.await;
|
||||
let elapsed = start_time.elapsed();
|
||||
|
||||
// 检查是否在2-3秒内超时(给一点缓冲时间)
|
||||
info!("脚本执行耗时: {elapsed:?}");
|
||||
assert!(elapsed.as_secs() >= 2, "脚本应该至少运行2秒");
|
||||
assert!(elapsed.as_secs() < 4, "脚本应该在4秒内超时");
|
||||
|
||||
// 检查是否返回超时错误
|
||||
match result {
|
||||
Ok(exec_result) => {
|
||||
if let Some(error) = exec_result.error {
|
||||
info!("正确捕获到超时错误: {error}");
|
||||
assert!(
|
||||
error.contains("timed out") ||
|
||||
error.contains("TimedOut") ||
|
||||
error.contains("executor await error"),
|
||||
"错误信息应该包含超时相关信息"
|
||||
);
|
||||
} else {
|
||||
panic!("应该捕获到超时错误,但脚本执行成功了");
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
// 使用特殊格式化获取完整错误链
|
||||
let full_error = format!("{e:#}");
|
||||
info!("捕获到错误: {full_error}");
|
||||
assert!(
|
||||
full_error.contains("timed out"),
|
||||
"完整错误链中应该包含'timed out'超时信息"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_js_cow_say_hello() -> Result<()> {
|
||||
// 初始化日志
|
||||
setup();
|
||||
|
||||
// 从 fixtures 目录读取测试脚本
|
||||
let code = std::fs::read_to_string("fixtures/cow_say_hello.js")?;
|
||||
info!("读取测试脚本: cow_say_hello.js");
|
||||
|
||||
// 准备参数
|
||||
let params = json!({
|
||||
"a": 5,
|
||||
"b": 7
|
||||
});
|
||||
info!("准备测试参数: {params:?}");
|
||||
|
||||
// 执行脚本
|
||||
info!("开始执行JavaScript脚本...");
|
||||
let result =
|
||||
CodeExecutor::execute_with_params(&code, LanguageScript::Js, Some(params), None)
|
||||
.await?;
|
||||
info!("脚本执行完成, 日志: {:?}", result.logs);
|
||||
|
||||
if let Some(error) = &result.error {
|
||||
info!("执行错误: {error}");
|
||||
} else {
|
||||
info!("脚本执行成功");
|
||||
}
|
||||
|
||||
if let Some(result_val) = &result.result {
|
||||
info!("执行结果: {result_val:?}");
|
||||
} else {
|
||||
info!("无执行结果");
|
||||
}
|
||||
|
||||
// 验证结果
|
||||
assert!(!result.logs.is_empty(), "日志不应为空");
|
||||
assert!(result.error.is_none(), "不应有错误");
|
||||
assert!(result.result.is_some(), "应有返回结果");
|
||||
|
||||
// 验证返回值
|
||||
if let Some(result_val) = result.result {
|
||||
// 检查结果是否包含预期的字段
|
||||
let json_str = serde_json::to_string(&result_val)?;
|
||||
assert!(json_str.contains("message"), "结果应包含 message 字段");
|
||||
|
||||
// 检查计算结果是否正确 (5 + 7 = 12)
|
||||
if let Some(obj) = result_val.as_object() {
|
||||
if let Some(message) = obj.get("message") {
|
||||
assert_eq!(message, 12, "message 字段的值应为 12");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_js_import_deno_std() -> Result<()> {
|
||||
// 初始化日志
|
||||
setup();
|
||||
|
||||
// 从 fixtures 目录读取测试脚本
|
||||
let code = std::fs::read_to_string("fixtures/import_deno_std_example.js")?;
|
||||
info!("读取测试脚本: import_deno_std_example.js");
|
||||
|
||||
// 准备参数
|
||||
let params = json!({
|
||||
"path": "test.txt"
|
||||
});
|
||||
info!("准备测试参数: {params:?}");
|
||||
|
||||
// 执行脚本
|
||||
info!("开始执行JavaScript脚本...");
|
||||
let result =
|
||||
CodeExecutor::execute_with_params_compat(&code, LanguageScript::Js, Some(params))
|
||||
.await?;
|
||||
info!("脚本执行完成, 日志: {:?}", result.logs);
|
||||
|
||||
if let Some(error) = &result.error {
|
||||
info!("执行错误: {error}");
|
||||
} else {
|
||||
info!("脚本执行成功");
|
||||
}
|
||||
|
||||
if let Some(result_val) = &result.result {
|
||||
info!("执行结果: {result_val:?}");
|
||||
} else {
|
||||
info!("无执行结果");
|
||||
}
|
||||
|
||||
// 验证结果
|
||||
assert!(!result.logs.is_empty(), "日志不应为空");
|
||||
assert!(result.error.is_none(), "不应有错误");
|
||||
assert!(result.result.is_some(), "应有返回结果");
|
||||
|
||||
// 验证返回值
|
||||
if let Some(result_val) = result.result {
|
||||
// 检查结果是否包含预期的字段
|
||||
let json_str = serde_json::to_string(&result_val)?;
|
||||
assert!(json_str.contains("message"), "结果应包含 message 字段");
|
||||
assert!(json_str.contains("pathInfo"), "结果应包含 pathInfo 字段");
|
||||
assert!(
|
||||
json_str.contains("fileContent"),
|
||||
"结果应包含 fileContent 字段"
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_js_import_jsr() -> Result<()> {
|
||||
// 初始化日志
|
||||
setup();
|
||||
|
||||
// 从 fixtures 目录读取测试脚本
|
||||
let code = std::fs::read_to_string("fixtures/import_jsr_example.js")?;
|
||||
info!("读取测试脚本: import_jsr_example.js");
|
||||
|
||||
// 准备参数
|
||||
let params = json!({
|
||||
"a": 10,
|
||||
"b": 5,
|
||||
"expected": 15
|
||||
});
|
||||
info!("准备测试参数: {params:?}");
|
||||
|
||||
// 执行脚本
|
||||
info!("开始执行JavaScript脚本...");
|
||||
let result =
|
||||
CodeExecutor::execute_with_params_compat(&code, LanguageScript::Js, Some(params))
|
||||
.await?;
|
||||
info!("脚本执行完成, 日志: {:?}", result.logs);
|
||||
|
||||
if let Some(error) = &result.error {
|
||||
info!("执行错误: {error}");
|
||||
} else {
|
||||
info!("脚本执行成功");
|
||||
}
|
||||
|
||||
if let Some(result_val) = &result.result {
|
||||
info!("执行结果: {result_val:?}");
|
||||
} else {
|
||||
info!("无执行结果");
|
||||
}
|
||||
|
||||
// 验证结果
|
||||
assert!(!result.logs.is_empty(), "日志不应为空");
|
||||
assert!(result.error.is_none(), "不应有错误");
|
||||
assert!(result.result.is_some(), "应有返回结果");
|
||||
|
||||
// 验证返回值
|
||||
if let Some(result_val) = result.result {
|
||||
// 检查结果是否包含预期的字段
|
||||
let json_str = serde_json::to_string(&result_val)?;
|
||||
assert!(json_str.contains("message"), "结果应包含 message 字段");
|
||||
assert!(json_str.contains("results"), "结果应包含 results 字段");
|
||||
assert!(json_str.contains("summary"), "结果应包含 summary 字段");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_js_local_module() -> Result<()> {
|
||||
// 初始化日志
|
||||
setup();
|
||||
|
||||
// 从 fixtures 目录读取测试脚本
|
||||
let code = std::fs::read_to_string("fixtures/import_local_module_example.js")?;
|
||||
info!("读取测试脚本: import_local_module_example.js");
|
||||
|
||||
// 准备参数
|
||||
let params = json!({
|
||||
"order": {
|
||||
"items": [
|
||||
{ "name": "测试产品", "price": 200, "quantity": 2 }
|
||||
],
|
||||
"taxRate": 0.05
|
||||
}
|
||||
});
|
||||
info!("准备测试参数: {params:?}");
|
||||
|
||||
// 执行脚本
|
||||
info!("开始执行JavaScript脚本...");
|
||||
let result =
|
||||
CodeExecutor::execute_with_params_compat(&code, LanguageScript::Js, Some(params))
|
||||
.await?;
|
||||
info!("脚本执行完成, 日志: {:?}", result.logs);
|
||||
|
||||
if let Some(error) = &result.error {
|
||||
info!("执行错误: {error}");
|
||||
} else {
|
||||
info!("脚本执行成功");
|
||||
}
|
||||
|
||||
if let Some(result_val) = &result.result {
|
||||
info!("执行结果: {result_val:?}");
|
||||
} else {
|
||||
info!("无执行结果");
|
||||
}
|
||||
|
||||
// 验证结果
|
||||
assert!(!result.logs.is_empty(), "日志不应为空");
|
||||
assert!(result.error.is_none(), "不应有错误");
|
||||
assert!(result.result.is_some(), "应有返回结果");
|
||||
|
||||
// 验证返回值
|
||||
if let Some(result_val) = result.result {
|
||||
// 检查结果是否包含预期的字段
|
||||
let json_str = serde_json::to_string(&result_val)?;
|
||||
assert!(json_str.contains("message"), "结果应包含 message 字段");
|
||||
assert!(json_str.contains("order"), "结果应包含 order 字段");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_js_import_axios() -> Result<()> {
|
||||
// 初始化日志
|
||||
setup();
|
||||
|
||||
// 从 fixtures 目录读取测试脚本
|
||||
let code = std::fs::read_to_string("fixtures/import_axios_example.js")?;
|
||||
info!("读取测试脚本: import_axios_example.js");
|
||||
|
||||
// 准备参数
|
||||
let params = json!({
|
||||
"url": "https://jsonplaceholder.typicode.com/todos/1"
|
||||
});
|
||||
info!("准备测试参数: {params:?}");
|
||||
|
||||
// 执行脚本
|
||||
info!("开始执行JavaScript脚本...");
|
||||
let result =
|
||||
CodeExecutor::execute_with_params_compat(&code, LanguageScript::Js, Some(params))
|
||||
.await?;
|
||||
info!("脚本执行完成, 日志: {:?}", result.logs);
|
||||
|
||||
if let Some(error) = &result.error {
|
||||
info!("执行错误: {error}");
|
||||
} else {
|
||||
info!("脚本执行成功");
|
||||
}
|
||||
|
||||
if let Some(result_val) = &result.result {
|
||||
info!("执行结果: {result_val:?}");
|
||||
} else {
|
||||
info!("无执行结果");
|
||||
}
|
||||
|
||||
// 验证结果
|
||||
assert!(!result.logs.is_empty(), "日志不应为空");
|
||||
assert!(result.error.is_none(), "不应有错误");
|
||||
assert!(result.result.is_some(), "应有返回结果");
|
||||
|
||||
// 验证返回值
|
||||
if let Some(result_val) = result.result {
|
||||
// 检查结果是否包含预期的字段
|
||||
let json_str = serde_json::to_string(&result_val)?;
|
||||
assert!(json_str.contains("message"), "结果应包含 message 字段");
|
||||
assert!(json_str.contains("data"), "结果应包含 data 字段");
|
||||
assert!(json_str.contains("timestamp"), "结果应包含 timestamp 字段");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_js_import_esm() -> Result<()> {
|
||||
// 初始化日志
|
||||
setup();
|
||||
|
||||
// 从 fixtures 目录读取测试脚本
|
||||
let code = std::fs::read_to_string("fixtures/import_esm_module_example.js")?;
|
||||
info!("读取测试脚本: import_esm_module_example.js");
|
||||
|
||||
// 准备参数
|
||||
let params = json!({
|
||||
"data": "测试数据",
|
||||
"salt": "test-salt-123"
|
||||
});
|
||||
info!("准备测试参数: {params:?}");
|
||||
|
||||
// 执行脚本
|
||||
info!("开始执行JavaScript脚本...");
|
||||
let result =
|
||||
CodeExecutor::execute_with_params_compat(&code, LanguageScript::Js, Some(params))
|
||||
.await?;
|
||||
info!("脚本执行完成, 日志: {:?}", result.logs);
|
||||
|
||||
if let Some(error) = &result.error {
|
||||
info!("执行错误: {error}");
|
||||
} else {
|
||||
info!("脚本执行成功");
|
||||
}
|
||||
|
||||
if let Some(result_val) = &result.result {
|
||||
info!("执行结果: {result_val:?}");
|
||||
} else {
|
||||
info!("无执行结果");
|
||||
}
|
||||
|
||||
// 验证结果
|
||||
assert!(!result.logs.is_empty(), "日志不应为空");
|
||||
assert!(result.error.is_none(), "不应有错误");
|
||||
assert!(result.result.is_some(), "应有返回结果");
|
||||
|
||||
// 验证返回值
|
||||
if let Some(result_val) = result.result {
|
||||
// 检查结果是否包含预期的字段
|
||||
let json_str = serde_json::to_string(&result_val)?;
|
||||
assert!(json_str.contains("message"), "结果应包含 message 字段");
|
||||
assert!(json_str.contains("result"), "结果应包含 result 字段");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_js_import_lodash() -> Result<()> {
|
||||
// 初始化日志
|
||||
setup();
|
||||
|
||||
// 从 fixtures 目录读取测试脚本
|
||||
let code = std::fs::read_to_string("fixtures/import_lodash_example.js")?;
|
||||
info!("读取测试脚本: import_lodash_example.js");
|
||||
|
||||
// 准备参数
|
||||
let params = json!({
|
||||
"data": [1, 2, 3, 4, 5, 6, 7, 8, 9],
|
||||
"chunkSize": 3
|
||||
});
|
||||
info!("准备测试参数: {params:?}");
|
||||
|
||||
// 执行脚本
|
||||
info!("开始执行JavaScript脚本...");
|
||||
let result =
|
||||
CodeExecutor::execute_with_params_compat(&code, LanguageScript::Js, Some(params))
|
||||
.await?;
|
||||
info!("脚本执行完成, 日志: {:?}", result.logs);
|
||||
|
||||
if let Some(error) = &result.error {
|
||||
info!("执行错误: {error}");
|
||||
} else {
|
||||
info!("脚本执行成功");
|
||||
}
|
||||
|
||||
if let Some(result_val) = &result.result {
|
||||
info!("执行结果: {result_val:?}");
|
||||
} else {
|
||||
info!("无执行结果");
|
||||
}
|
||||
|
||||
// 验证结果
|
||||
assert!(!result.logs.is_empty(), "日志不应为空");
|
||||
assert!(result.error.is_none(), "不应有错误");
|
||||
assert!(result.result.is_some(), "应有返回结果");
|
||||
|
||||
// 验证返回值
|
||||
if let Some(result_val) = result.result {
|
||||
// 检查结果是否包含预期的字段
|
||||
let json_str = serde_json::to_string(&result_val)?;
|
||||
assert!(json_str.contains("message"), "结果应包含 message 字段");
|
||||
assert!(
|
||||
json_str.contains("originalData"),
|
||||
"结果应包含 originalData 字段"
|
||||
);
|
||||
assert!(
|
||||
json_str.contains("processedData"),
|
||||
"结果应包含 processedData 字段"
|
||||
);
|
||||
assert!(json_str.contains("timestamp"), "结果应包含 timestamp 字段");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_js_main_function() -> Result<()> {
|
||||
// 初始化日志
|
||||
setup();
|
||||
|
||||
// 从 fixtures 目录读取测试脚本
|
||||
let code = std::fs::read_to_string("fixtures/rfunction_js_test3.js")?;
|
||||
info!("读取测试脚本: rfunction_js_test3.js");
|
||||
|
||||
// 准备参数
|
||||
let params = json!({
|
||||
"testKey": "测试值"
|
||||
});
|
||||
info!("准备测试参数: {params:?}");
|
||||
|
||||
// 执行脚本
|
||||
info!("开始执行JavaScript脚本...");
|
||||
let result =
|
||||
CodeExecutor::execute_with_params_compat(&code, LanguageScript::Js, Some(params))
|
||||
.await?;
|
||||
info!("脚本执行完成, 日志: {:?}", result.logs);
|
||||
|
||||
if let Some(error) = &result.error {
|
||||
info!("执行错误: {error}");
|
||||
} else {
|
||||
info!("脚本执行成功");
|
||||
}
|
||||
|
||||
if let Some(result_val) = &result.result {
|
||||
info!("执行结果: {result_val:?}");
|
||||
} else {
|
||||
info!("无执行结果");
|
||||
}
|
||||
|
||||
// 验证结果
|
||||
assert!(!result.logs.is_empty(), "日志不应为空");
|
||||
assert!(result.error.is_none(), "不应有错误");
|
||||
assert!(result.result.is_some(), "应有返回结果");
|
||||
|
||||
// 验证返回值
|
||||
if let Some(result_val) = result.result {
|
||||
// 检查结果是否包含预期的字段
|
||||
let json_str = serde_json::to_string(&result_val)?;
|
||||
assert!(json_str.contains("key"), "结果应包含 key 字段");
|
||||
assert!(json_str.contains("value"), "key 字段的值应为 value");
|
||||
|
||||
// 检查JSON结构
|
||||
if let Some(obj) = result_val.as_object() {
|
||||
if let Some(key_value) = obj.get("key") {
|
||||
assert_eq!(key_value, "value", "key 字段的值应为 value");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_js_large_parameters() -> Result<()> {
|
||||
// 初始化日志
|
||||
setup();
|
||||
|
||||
// 从 fixtures 目录读取测试脚本
|
||||
let code = std::fs::read_to_string("fixtures/test_js_large_params.js")?;
|
||||
info!("读取测试脚本: test_js_large_params.js");
|
||||
|
||||
// 生成大于2MB的文本参数
|
||||
fn generate_large_text(size_in_mb: usize) -> String {
|
||||
let chunk = "This is a test chunk of text designed to generate large parameters for testing the Argument list too long fix. ".repeat(100);
|
||||
let iterations = (size_in_mb * 1024 * 1024) / chunk.len();
|
||||
|
||||
let mut result = String::new();
|
||||
for i in 0..iterations {
|
||||
result.push_str(&chunk);
|
||||
result.push_str(&format!("Iteration {}\n", i));
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
let large_text = generate_large_text(3); // 3MB
|
||||
info!("生成了3MB大小的文本参数,实际大小: {:.2} MB", large_text.len() as f64 / 1024.0 / 1024.0);
|
||||
|
||||
// 准备参数
|
||||
let params = json!({
|
||||
"largeText": large_text
|
||||
});
|
||||
info!("准备测试参数,JSON大小: {:.2} MB", serde_json::to_string(¶ms).unwrap().len() as f64 / 1024.0 / 1024.0);
|
||||
|
||||
// 执行脚本
|
||||
info!("开始执行JavaScript脚本,测试大参数处理...");
|
||||
let result =
|
||||
CodeExecutor::execute_with_params_compat(&code, LanguageScript::Js, Some(params))
|
||||
.await?;
|
||||
info!("脚本执行完成, 日志: {:?}", result.logs);
|
||||
|
||||
if let Some(error) = &result.error {
|
||||
info!("执行错误: {error}");
|
||||
} else {
|
||||
info!("脚本执行成功");
|
||||
}
|
||||
|
||||
if let Some(result_val) = &result.result {
|
||||
info!("执行结果: {result_val:?}");
|
||||
} else {
|
||||
info!("无执行结果");
|
||||
}
|
||||
|
||||
// 验证结果
|
||||
assert!(!result.logs.is_empty(), "日志不应为空");
|
||||
assert!(result.error.is_none(), "不应有错误,这表明大参数处理成功");
|
||||
assert!(result.result.is_some(), "应有返回结果");
|
||||
|
||||
// 验证返回值包含成功处理大参数的信息
|
||||
if let Some(result_val) = result.result {
|
||||
let json_str = serde_json::to_string(&result_val)?;
|
||||
assert!(json_str.contains("success"), "结果应包含 success 字段");
|
||||
assert!(json_str.contains("true"), "success 应为 true");
|
||||
assert!(json_str.contains("Successfully processed large text parameter"), "结果应包含成功消息");
|
||||
assert!(json_str.contains("sizeMB"), "结果应包含 sizeMB 字段");
|
||||
|
||||
// 验证返回值是对象格式
|
||||
if let Some(obj) = result_val.as_object() {
|
||||
if let Some(success) = obj.get("success") {
|
||||
assert_eq!(success, true, "success 字段的值应为 true");
|
||||
}
|
||||
if let Some(text_size) = obj.get("textSize") {
|
||||
assert!(text_size.is_number(), "textSize 应为数字");
|
||||
let size = text_size.as_u64().unwrap();
|
||||
assert!(size > 2 * 1024 * 1024, "文本大小应大于2MB");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
info!("大参数测试通过!临时文件解决方案有效。");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
90
qiming-run_code_rmcp/src/tests/mod.rs
Normal file
90
qiming-run_code_rmcp/src/tests/mod.rs
Normal file
@@ -0,0 +1,90 @@
|
||||
#[cfg(test)]
|
||||
pub mod test_utils {
|
||||
use log::LevelFilter;
|
||||
use log::info;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use std::sync::Once;
|
||||
|
||||
// 使用 Once 确保 env_logger 只初始化一次
|
||||
static INIT: Once = Once::new();
|
||||
// 控制是否清理缓存的环境变量名
|
||||
const CLEAN_CACHE_ENV: &str = "CLEAN_TEST_CACHE";
|
||||
|
||||
pub fn setup() {
|
||||
INIT.call_once(|| {
|
||||
// 尝试初始化日志,如果失败则忽略
|
||||
let _ = std::panic::catch_unwind(|| {
|
||||
env_logger::builder().filter_level(LevelFilter::Info).init();
|
||||
});
|
||||
});
|
||||
|
||||
// 只有当环境变量设置为"1"时才清理缓存
|
||||
if let Ok(clean_cache) = std::env::var(CLEAN_CACHE_ENV) {
|
||||
if clean_cache == "1" {
|
||||
clean_cache_dir();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 清理缓存目录函数
|
||||
fn clean_cache_dir() {
|
||||
let cache_dir = Path::new("/tmp/code_cache");
|
||||
|
||||
// 如果缓存目录存在,清理其中的文件
|
||||
if cache_dir.exists() {
|
||||
info!("正在清理缓存目录: {cache_dir:?}");
|
||||
|
||||
match fs::read_dir(cache_dir) {
|
||||
Ok(entries) => {
|
||||
for entry in entries {
|
||||
if let Ok(entry) = entry {
|
||||
let path = entry.path();
|
||||
if path.is_file() {
|
||||
if let Err(e) = fs::remove_file(&path) {
|
||||
info!("删除文件失败 {path:?}: {e}");
|
||||
} else {
|
||||
info!("已删除缓存文件: {path:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
info!("缓存目录清理完成");
|
||||
}
|
||||
Err(e) => {
|
||||
info!("读取缓存目录失败: {e}");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 如果缓存目录不存在,创建它
|
||||
info!("缓存目录不存在,创建目录: {cache_dir:?}");
|
||||
if let Err(e) = fs::create_dir_all(cache_dir) {
|
||||
info!("创建缓存目录失败: {e}");
|
||||
} else {
|
||||
info!("缓存目录创建成功");
|
||||
}
|
||||
}
|
||||
|
||||
// 确保缓存目录存在且可写
|
||||
if !cache_dir.exists() {
|
||||
if let Err(e) = fs::create_dir_all(cache_dir) {
|
||||
info!("创建缓存目录失败: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
// 设置目录权限为777(所有用户可读写执行)
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
if let Err(e) = fs::set_permissions(cache_dir, fs::Permissions::from_mode(0o775)) {
|
||||
info!("设置缓存目录权限失败: {e}");
|
||||
} else {
|
||||
info!("设置缓存目录权限成功");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub mod js_tests;
|
||||
pub mod python_tests;
|
||||
pub mod ts_tests;
|
||||
591
qiming-run_code_rmcp/src/tests/python_tests.rs
Normal file
591
qiming-run_code_rmcp/src/tests/python_tests.rs
Normal file
@@ -0,0 +1,591 @@
|
||||
#[cfg(test)]
|
||||
mod python_tests {
|
||||
use anyhow::Result;
|
||||
use log::info;
|
||||
use serde_json::json;
|
||||
|
||||
use crate::model::{CodeExecutor, LanguageScript};
|
||||
use crate::tests::test_utils::setup;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_python_basic_execution() -> Result<()> {
|
||||
// 初始化日志
|
||||
setup();
|
||||
|
||||
// 从 fixtures 目录读取测试脚本
|
||||
let code = std::fs::read_to_string("fixtures/test_python.py")?;
|
||||
info!("读取测试脚本: test_python.py");
|
||||
|
||||
// 执行脚本
|
||||
info!("开始执行Python脚本...");
|
||||
let result =
|
||||
CodeExecutor::execute_with_params_compat(&code, LanguageScript::Python, None).await?;
|
||||
info!("脚本执行完成, 日志: {:?}", result.logs);
|
||||
|
||||
if let Some(error) = &result.error {
|
||||
info!("执行错误: {error}");
|
||||
} else {
|
||||
info!("脚本执行成功");
|
||||
}
|
||||
|
||||
// 检查日志中是否包含预期的输出
|
||||
let logs_str = result.logs.join(" ");
|
||||
assert!(
|
||||
logs_str.contains("Handler function called"),
|
||||
"日志应包含 'Handler function called'"
|
||||
);
|
||||
assert!(
|
||||
logs_str.contains("Final calculation completed"),
|
||||
"日志应包含 'Final calculation completed'"
|
||||
);
|
||||
assert!(
|
||||
logs_str.contains("The product of [1, 2, 3, 4, 5] is 120"),
|
||||
"日志应包含计算结果"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_python_with_params() -> Result<()> {
|
||||
// 初始化日志
|
||||
setup();
|
||||
|
||||
// 从 fixtures 目录读取测试脚本
|
||||
let code = std::fs::read_to_string("fixtures/test_python_params.py")?;
|
||||
info!("读取测试脚本: test_python_params.py");
|
||||
|
||||
// 准备参数
|
||||
let params = json!({
|
||||
"a": 10,
|
||||
"b": 20
|
||||
});
|
||||
info!("准备测试参数: {params:?}");
|
||||
|
||||
// 执行脚本
|
||||
info!("开始执行Python脚本...");
|
||||
let result =
|
||||
CodeExecutor::execute_with_params_compat(&code, LanguageScript::Python, Some(params))
|
||||
.await?;
|
||||
info!("脚本执行完成, 日志: {:?}", result.logs);
|
||||
|
||||
if let Some(error) = &result.error {
|
||||
info!("执行错误: {error}");
|
||||
} else {
|
||||
info!("脚本执行成功");
|
||||
}
|
||||
|
||||
if let Some(result_val) = &result.result {
|
||||
info!("执行结果: {result_val:?}");
|
||||
} else {
|
||||
info!("无执行结果");
|
||||
}
|
||||
|
||||
// 验证返回值包含预期的字段
|
||||
if let Some(result_val) = result.result {
|
||||
// 结果应该是 JSON 对象(Python 字典被正确解析)
|
||||
assert!(result_val.get("sum").is_some(), "结果应包含 sum 字段");
|
||||
assert!(result_val.get("numbers").is_some(), "结果应包含 numbers 字段");
|
||||
assert!(result_val.get("message").is_some(), "结果应包含 message 字段");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_python_different_return_types() -> Result<()> {
|
||||
// 初始化日志
|
||||
setup();
|
||||
|
||||
// 从 fixtures 目录读取测试脚本
|
||||
let code = std::fs::read_to_string("fixtures/test_python_types.py")?;
|
||||
info!("读取测试脚本: test_python_types.py");
|
||||
|
||||
// 测试字符串类型
|
||||
let params = json!({"type": "string"});
|
||||
info!("测试字符串类型, 参数: {params:?}");
|
||||
let result =
|
||||
CodeExecutor::execute_with_params_compat(&code, LanguageScript::Python, Some(params))
|
||||
.await?;
|
||||
if let Some(result_val) = &result.result {
|
||||
assert!(result_val.is_string(), "结果应为字符串类型");
|
||||
}
|
||||
|
||||
// 测试数字类型
|
||||
let params = json!({"type": "number"});
|
||||
info!("测试数字类型, 参数: {params:?}");
|
||||
let result =
|
||||
CodeExecutor::execute_with_params_compat(&code, LanguageScript::Python, Some(params))
|
||||
.await?;
|
||||
if let Some(result_val) = &result.result {
|
||||
assert!(result_val.is_number(), "结果应为数字类型");
|
||||
assert_eq!(result_val.as_i64().unwrap(), 12345, "结果应为 12345");
|
||||
}
|
||||
|
||||
// 测试布尔类型
|
||||
let params = json!({"type": "boolean"});
|
||||
info!("测试布尔类型, 参数: {params:?}");
|
||||
let result =
|
||||
CodeExecutor::execute_with_params_compat(&code, LanguageScript::Python, Some(params))
|
||||
.await?;
|
||||
if let Some(result_val) = &result.result {
|
||||
assert!(result_val.is_boolean(), "结果应为布尔类型");
|
||||
assert!(result_val.as_bool().unwrap(), "结果应为 true");
|
||||
}
|
||||
|
||||
// 测试列表类型
|
||||
let params = json!({"type": "list"});
|
||||
info!("测试列表类型, 参数: {params:?}");
|
||||
let result =
|
||||
CodeExecutor::execute_with_params_compat(&code, LanguageScript::Python, Some(params))
|
||||
.await?;
|
||||
if let Some(result_val) = &result.result {
|
||||
// 结果应该是数组类型(Python列表被正确解析)
|
||||
assert!(result_val.is_array(), "结果应为数组类型");
|
||||
assert_eq!(result_val.as_array().unwrap().len(), 6, "数组长度应为 6");
|
||||
}
|
||||
|
||||
// 测试字典类型
|
||||
let params = json!({"type": "dict"});
|
||||
info!("测试字典类型, 参数: {params:?}");
|
||||
let result =
|
||||
CodeExecutor::execute_with_params_compat(&code, LanguageScript::Python, Some(params))
|
||||
.await?;
|
||||
if let Some(result_val) = &result.result {
|
||||
// 结果应该是对象类型(Python字典被正确解析)
|
||||
assert!(result_val.is_object(), "结果应为对象类型");
|
||||
assert!(result_val.get("name").is_some(), "结果应包含 name 字段");
|
||||
assert!(result_val.get("age").is_some(), "结果应包含 age 字段");
|
||||
assert!(result_val.get("tags").is_some(), "结果应包含 tags 字段");
|
||||
}
|
||||
|
||||
// 测试 None 类型
|
||||
let params = json!({"type": "null"});
|
||||
info!("测试 None 类型, 参数: {params:?}");
|
||||
let _result =
|
||||
CodeExecutor::execute_with_params_compat(&code, LanguageScript::Python, Some(params))
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// 此测试使用标准库替代pandas,不再需要忽略
|
||||
#[tokio::test]
|
||||
async fn test_python_with_pandas() -> Result<()> {
|
||||
// 初始化日志
|
||||
setup();
|
||||
|
||||
// 从 fixtures 目录读取测试脚本
|
||||
let code = std::fs::read_to_string("fixtures/rfunction_test2.py")?;
|
||||
info!("读取测试脚本: rfunction_test2.py");
|
||||
|
||||
// 检查依赖识别
|
||||
let dependencies = crate::python_runner::parse_import(&code)?;
|
||||
info!("识别到的依赖: {dependencies:?}");
|
||||
|
||||
// 验证依赖识别结果 - 应该包含pandas依赖
|
||||
assert!(!dependencies.is_empty(), "依赖列表不应为空");
|
||||
assert!(
|
||||
dependencies.contains(&"pandas".to_string()),
|
||||
"依赖列表应包含pandas"
|
||||
);
|
||||
|
||||
// 准备参数
|
||||
let params = json!({
|
||||
"params": {
|
||||
"input": "测试数据"
|
||||
}
|
||||
});
|
||||
info!("准备测试参数: {params:?}");
|
||||
|
||||
// 执行脚本
|
||||
info!("开始执行Python脚本...");
|
||||
let result =
|
||||
CodeExecutor::execute_with_params_compat(&code, LanguageScript::Python, Some(params))
|
||||
.await?;
|
||||
info!("脚本执行完成, 日志: {:?}", result.logs);
|
||||
|
||||
if let Some(error) = &result.error {
|
||||
info!("执行错误: {error}");
|
||||
// 如果有执行错误,我们只验证依赖识别是否正确,不再检查执行结果
|
||||
// 这是因为pandas可能未安装或环境问题导致执行失败
|
||||
return Ok(());
|
||||
} else {
|
||||
info!("脚本执行成功");
|
||||
}
|
||||
|
||||
if let Some(result_val) = &result.result {
|
||||
info!("执行结果: {result_val:?}");
|
||||
} else {
|
||||
info!("无执行结果");
|
||||
}
|
||||
|
||||
// 如果脚本执行成功,才验证结果
|
||||
// 验证日志和结果
|
||||
if result.logs.is_empty() {
|
||||
info!("警告: 日志为空,但脚本执行成功");
|
||||
} else {
|
||||
info!("捕获到的日志数量: {}", result.logs.len());
|
||||
|
||||
// 逐条打印日志
|
||||
for (i, log) in result.logs.iter().enumerate() {
|
||||
info!("日志[{i}]: {log}");
|
||||
}
|
||||
|
||||
// 检查日志中是否包含logging.info的输出
|
||||
let logs_str = result.logs.join(" ");
|
||||
info!("合并后的日志字符串: {logs_str}");
|
||||
assert!(
|
||||
logs_str.contains("Created data structure"),
|
||||
"日志应包含'Created data structure'"
|
||||
);
|
||||
assert!(logs_str.contains("input args"), "日志应包含'input args'");
|
||||
}
|
||||
|
||||
// 如果有结果,验证返回值包含预期的字段
|
||||
if let Some(result_val) = result.result {
|
||||
// 结果应该是JSON对象(Python字典被正确解析)
|
||||
assert!(result_val.get("key0").is_some(), "结果应包含 key0 字段");
|
||||
assert_eq!(
|
||||
result_val["key0"].as_str().unwrap(),
|
||||
"测试数据",
|
||||
"key0 应等于输入参数"
|
||||
);
|
||||
assert!(result_val.get("key1").is_some(), "结果应包含 key1 字段");
|
||||
assert!(result_val["key1"].is_array(), "key1 应为数组");
|
||||
assert!(result_val.get("key2").is_some(), "结果应包含 key2 字段");
|
||||
assert!(result_val["key2"].is_object(), "key2 应为对象");
|
||||
assert!(result_val["key2"].get("key21").is_some(), "key2.key21 应存在");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_python_params_access() -> Result<()> {
|
||||
// 初始化日志
|
||||
setup();
|
||||
|
||||
// 从 fixtures 目录读取测试脚本
|
||||
let code = std::fs::read_to_string("fixtures/test_python_simple.py")?;
|
||||
info!("读取测试脚本: test_python_simple.py");
|
||||
|
||||
// 准备参数 - 直接提供input参数
|
||||
let params = json!({
|
||||
"input": "直接提供的参数"
|
||||
});
|
||||
info!("准备测试参数: {params:?}");
|
||||
|
||||
// 执行脚本
|
||||
info!("开始执行Python脚本...");
|
||||
let result = CodeExecutor::execute_with_params_compat(
|
||||
&code,
|
||||
LanguageScript::Python,
|
||||
Some(params.clone()),
|
||||
)
|
||||
.await?;
|
||||
info!("脚本执行完成, 日志: {:?}", result.logs);
|
||||
|
||||
if let Some(error) = &result.error {
|
||||
info!("执行错误: {error}");
|
||||
panic!("执行出错: {error}");
|
||||
} else {
|
||||
info!("脚本执行成功");
|
||||
}
|
||||
|
||||
if let Some(result_val) = &result.result {
|
||||
info!("执行结果: {result_val:?}");
|
||||
|
||||
if let Some(result_str) = result_val.as_str() {
|
||||
let json_val = serde_json::from_str::<serde_json::Value>(result_str)?;
|
||||
|
||||
// 验证两种访问方式都能获取到参数
|
||||
assert_eq!(
|
||||
json_val["direct_access"], "直接提供的参数",
|
||||
"直接访问应该能获取到参数"
|
||||
);
|
||||
assert_eq!(
|
||||
json_val["nested_access"], "直接提供的参数",
|
||||
"嵌套访问也应该能获取到参数"
|
||||
);
|
||||
|
||||
// 验证args结构中同时包含直接参数和params嵌套参数
|
||||
assert!(
|
||||
json_val["args_structure"].get("input").is_some(),
|
||||
"args结构应包含直接参数"
|
||||
);
|
||||
assert!(
|
||||
json_val["args_structure"].get("params").is_some(),
|
||||
"args结构应包含params参数"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 准备参数 - 通过params嵌套提供
|
||||
let nested_params = json!({
|
||||
"params": {
|
||||
"input": "嵌套提供的参数"
|
||||
}
|
||||
});
|
||||
info!("准备嵌套测试参数: {nested_params:?}");
|
||||
|
||||
// 执行脚本
|
||||
info!("开始执行Python脚本...");
|
||||
let result = CodeExecutor::execute_with_params_compat(
|
||||
&code,
|
||||
LanguageScript::Python,
|
||||
Some(nested_params),
|
||||
)
|
||||
.await?;
|
||||
info!("脚本执行完成, 日志: {:?}", result.logs);
|
||||
|
||||
if let Some(error) = &result.error {
|
||||
info!("执行错误: {error}");
|
||||
panic!("执行出错: {error}");
|
||||
}
|
||||
|
||||
if let Some(result_val) = &result.result {
|
||||
info!("执行结果: {result_val:?}");
|
||||
|
||||
if let Some(result_str) = result_val.as_str() {
|
||||
let json_val = serde_json::from_str::<serde_json::Value>(result_str)?;
|
||||
|
||||
// 验证嵌套参数可以被访问
|
||||
assert_eq!(
|
||||
json_val["nested_access"], "嵌套提供的参数",
|
||||
"应该能通过params.input访问到参数"
|
||||
);
|
||||
|
||||
// 验证args结构
|
||||
assert!(
|
||||
json_val["args_structure"].get("params").is_some(),
|
||||
"args结构应包含params参数"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_python_logging() -> Result<()> {
|
||||
// 初始化日志
|
||||
setup();
|
||||
|
||||
// 从 fixtures 目录读取测试脚本
|
||||
let code = std::fs::read_to_string("fixtures/test_python_logging.py")?;
|
||||
info!("读取测试脚本: test_python_logging.py");
|
||||
|
||||
// 准备参数
|
||||
let params = json!({
|
||||
"test": "日志测试"
|
||||
});
|
||||
info!("准备测试参数: {params:?}");
|
||||
|
||||
// 执行脚本
|
||||
info!("开始执行Python脚本...");
|
||||
let result =
|
||||
CodeExecutor::execute_with_params_compat(&code, LanguageScript::Python, Some(params))
|
||||
.await?;
|
||||
info!("脚本执行完成, 日志: {:?}", result.logs);
|
||||
|
||||
if let Some(error) = &result.error {
|
||||
info!("执行错误: {error}");
|
||||
panic!("执行出错: {error}");
|
||||
} else {
|
||||
info!("脚本执行成功");
|
||||
}
|
||||
|
||||
// 验证日志捕获
|
||||
assert!(!result.logs.is_empty(), "日志不应为空");
|
||||
info!("捕获到的日志数量: {}", result.logs.len());
|
||||
|
||||
// 逐条打印日志
|
||||
for (i, log) in result.logs.iter().enumerate() {
|
||||
info!("日志[{i}]: {log}");
|
||||
}
|
||||
|
||||
// 检查是否捕获了所有级别的日志
|
||||
let logs_str = result.logs.join(" ");
|
||||
// DEBUG级别的日志可能不会被捕获,因为Python胶水代码中可能设置了最低级别为INFO
|
||||
// assert!(logs_str.contains("DEBUG级别"), "应捕获DEBUG级别的日志");
|
||||
assert!(logs_str.contains("INFO级别"), "应捕获INFO级别的日志");
|
||||
assert!(logs_str.contains("WARNING级别"), "应捕获WARNING级别的日志");
|
||||
assert!(logs_str.contains("ERROR级别"), "应捕获ERROR级别的日志");
|
||||
assert!(
|
||||
logs_str.contains("CRITICAL级别"),
|
||||
"应捕获CRITICAL级别的日志"
|
||||
);
|
||||
assert!(
|
||||
logs_str.contains("格式化的JSON数据"),
|
||||
"应捕获格式化的JSON数据"
|
||||
);
|
||||
|
||||
// 验证返回值
|
||||
if let Some(result_val) = result.result {
|
||||
// 结果应该是JSON对象(Python字典被正确解析)
|
||||
assert_eq!(
|
||||
result_val["message"], "日志测试完成",
|
||||
"返回的message字段不正确"
|
||||
);
|
||||
assert_eq!(result_val["log_count"], 6, "返回的log_count字段不正确");
|
||||
} else {
|
||||
panic!("应有返回结果");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// 这个测试使用execute_with_params并指定超时时间
|
||||
#[tokio::test]
|
||||
async fn test_python_with_timeout() -> Result<()> {
|
||||
// 初始化日志
|
||||
setup();
|
||||
|
||||
// 创建一个会超时的Python脚本
|
||||
let code = r#"
|
||||
import time
|
||||
import logging
|
||||
|
||||
def main(args: dict) -> dict:
|
||||
logging.info("开始执行耗时操作")
|
||||
|
||||
# 这个操作会运行10秒钟
|
||||
for i in range(10):
|
||||
logging.info(f"已经执行了 {i+1} 秒")
|
||||
time.sleep(1)
|
||||
|
||||
logging.info("操作完成")
|
||||
return {"result": "完成"}
|
||||
"#;
|
||||
info!("创建了一个会运行10秒的测试脚本");
|
||||
|
||||
// 准备参数
|
||||
let params = json!({
|
||||
"test": "超时测试"
|
||||
});
|
||||
info!("准备测试参数: {params:?}");
|
||||
|
||||
// 设置3秒超时并执行脚本
|
||||
info!("开始执行Python脚本,设置3秒超时...");
|
||||
let start_time = std::time::Instant::now();
|
||||
let result =
|
||||
CodeExecutor::execute_with_params(code, LanguageScript::Python, Some(params), Some(3))
|
||||
.await;
|
||||
let elapsed = start_time.elapsed();
|
||||
|
||||
// 检查是否在3-4秒内超时(给一点缓冲时间)
|
||||
info!("脚本执行耗时: {elapsed:?}");
|
||||
assert!(elapsed.as_secs() >= 3, "脚本应该至少运行3秒");
|
||||
assert!(elapsed.as_secs() < 5, "脚本应该在5秒内超时");
|
||||
|
||||
// 检查是否返回超时错误
|
||||
match result {
|
||||
Ok(exec_result) => {
|
||||
if let Some(error) = exec_result.error {
|
||||
info!("正确捕获到超时错误: {error}");
|
||||
assert!(
|
||||
error.contains("timed out") ||
|
||||
error.contains("TimedOut") ||
|
||||
error.contains("executor await error"),
|
||||
"错误信息应该包含超时相关信息"
|
||||
);
|
||||
} else {
|
||||
panic!("应该捕获到超时错误,但脚本执行成功了");
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
// 使用特殊格式化获取完整错误链
|
||||
let full_error = format!("{e:#}");
|
||||
info!("捕获到错误: {full_error}");
|
||||
assert!(
|
||||
full_error.contains("timed out"),
|
||||
"完整错误链中应该包含'timed out'超时信息"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_python_large_parameters() -> Result<()> {
|
||||
// 初始化日志
|
||||
setup();
|
||||
|
||||
// 从 fixtures 目录读取测试脚本
|
||||
let code = std::fs::read_to_string("fixtures/test_python_large_params.py")?;
|
||||
info!("读取测试脚本: test_python_large_params.py");
|
||||
|
||||
// 生成大于2MB的文本参数
|
||||
fn generate_large_text(size_in_mb: usize) -> String {
|
||||
let chunk = "This is a test chunk of text designed to generate large parameters for Python testing. ".repeat(100);
|
||||
let iterations = (size_in_mb * 1024 * 1024) / chunk.len();
|
||||
|
||||
let mut result = String::new();
|
||||
for i in 0..iterations {
|
||||
result.push_str(&chunk);
|
||||
result.push_str(&format!("Iteration {}\n", i));
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
let large_text = generate_large_text(3); // 3MB
|
||||
info!("生成了3MB大小的文本参数,实际大小: {:.2} MB", large_text.len() as f64 / 1024.0 / 1024.0);
|
||||
|
||||
// 准备参数
|
||||
let params = json!({
|
||||
"largeText": large_text
|
||||
});
|
||||
info!("准备测试参数,JSON大小: {:.2} MB", serde_json::to_string(¶ms).unwrap().len() as f64 / 1024.0 / 1024.0);
|
||||
|
||||
// 执行脚本
|
||||
info!("开始执行Python脚本,测试大参数处理...");
|
||||
let result =
|
||||
CodeExecutor::execute_with_params_compat(&code, LanguageScript::Python, Some(params))
|
||||
.await?;
|
||||
info!("脚本执行完成, 日志: {:?}", result.logs);
|
||||
|
||||
if let Some(error) = &result.error {
|
||||
info!("执行错误: {error}");
|
||||
} else {
|
||||
info!("脚本执行成功");
|
||||
}
|
||||
|
||||
if let Some(result_val) = &result.result {
|
||||
info!("执行结果: {result_val:?}");
|
||||
} else {
|
||||
info!("无执行结果");
|
||||
}
|
||||
|
||||
// 验证结果
|
||||
assert!(!result.logs.is_empty(), "日志不应为空");
|
||||
assert!(result.error.is_none(), "不应有错误,这表明大参数处理成功");
|
||||
assert!(result.result.is_some(), "应有返回结果");
|
||||
|
||||
// 验证返回值包含成功处理大参数的信息
|
||||
if let Some(result_val) = result.result {
|
||||
let json_str = serde_json::to_string(&result_val)?;
|
||||
assert!(json_str.contains("success"), "结果应包含 success 字段");
|
||||
assert!(json_str.contains("true"), "success 应为 true");
|
||||
assert!(json_str.contains("Successfully processed large text parameter"), "结果应包含成功消息");
|
||||
assert!(json_str.contains("sizeMB"), "结果应包含 sizeMB 字段");
|
||||
|
||||
// 验证返回值是对象格式
|
||||
if let Some(obj) = result_val.as_object() {
|
||||
if let Some(success) = obj.get("success") {
|
||||
assert_eq!(success, true, "success 字段的值应为 true");
|
||||
}
|
||||
if let Some(text_size) = obj.get("textSize") {
|
||||
assert!(text_size.is_number(), "textSize 应为数字");
|
||||
let size = text_size.as_u64().unwrap();
|
||||
assert!(size > 2 * 1024 * 1024, "文本大小应大于2MB");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
info!("Python大参数测试通过!临时文件解决方案有效。");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
202
qiming-run_code_rmcp/src/tests/ts_tests.rs
Normal file
202
qiming-run_code_rmcp/src/tests/ts_tests.rs
Normal file
@@ -0,0 +1,202 @@
|
||||
#[cfg(test)]
|
||||
mod ts_tests {
|
||||
use anyhow::Result;
|
||||
use log::info;
|
||||
use serde_json::json;
|
||||
|
||||
use crate::model::{CodeExecutor, LanguageScript};
|
||||
use crate::tests::test_utils::setup;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_ts_basic_execution() -> Result<()> {
|
||||
// 初始化日志
|
||||
setup();
|
||||
|
||||
// 从 fixtures 目录读取测试脚本
|
||||
let code = std::fs::read_to_string("fixtures/test_ts.ts")?;
|
||||
info!("读取测试脚本: test_ts.ts");
|
||||
|
||||
// 执行脚本
|
||||
info!("开始执行TypeScript脚本...");
|
||||
let result =
|
||||
CodeExecutor::execute_with_params_compat(&code, LanguageScript::Ts, None).await?;
|
||||
info!("脚本执行完成, 日志: {:?}", result.logs);
|
||||
|
||||
if let Some(error) = &result.error {
|
||||
info!("执行错误: {error}");
|
||||
} else {
|
||||
info!("脚本执行成功");
|
||||
}
|
||||
|
||||
if let Some(result_val) = &result.result {
|
||||
info!("执行结果: {result_val:?}");
|
||||
} else {
|
||||
info!("无执行结果");
|
||||
}
|
||||
|
||||
// 验证结果
|
||||
assert!(!result.logs.is_empty(), "日志不应为空");
|
||||
assert!(result.error.is_none(), "不应有错误");
|
||||
assert!(result.result.is_some(), "应有返回结果");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_ts_with_params() -> Result<()> {
|
||||
// 初始化日志
|
||||
setup();
|
||||
|
||||
// 从 fixtures 目录读取测试脚本
|
||||
let code = std::fs::read_to_string("fixtures/test_ts_params.ts")?;
|
||||
info!("读取测试脚本: test_ts_params.ts");
|
||||
|
||||
// 准备参数
|
||||
let params = json!({
|
||||
"a": 10,
|
||||
"b": 20,
|
||||
"name": "测试用户"
|
||||
});
|
||||
info!("准备测试参数: {params:?}");
|
||||
|
||||
// 执行脚本
|
||||
info!("开始执行TypeScript脚本...");
|
||||
let result =
|
||||
CodeExecutor::execute_with_params_compat(&code, LanguageScript::Ts, Some(params))
|
||||
.await?;
|
||||
info!("脚本执行完成, 日志: {:?}", result.logs);
|
||||
|
||||
if let Some(error) = &result.error {
|
||||
info!("执行错误: {error}");
|
||||
} else {
|
||||
info!("脚本执行成功");
|
||||
}
|
||||
|
||||
if let Some(result_val) = &result.result {
|
||||
info!("执行结果: {result_val:?}");
|
||||
} else {
|
||||
info!("无执行结果");
|
||||
}
|
||||
|
||||
// 验证结果
|
||||
assert!(!result.logs.is_empty(), "日志不应为空");
|
||||
assert!(result.error.is_none(), "不应有错误");
|
||||
assert!(result.result.is_some(), "应有返回结果");
|
||||
|
||||
// 验证返回值包含预期的字段
|
||||
if let Some(result_val) = result.result {
|
||||
// 结果应该是JSON对象(TypeScript对象被正确解析)
|
||||
assert!(result_val.get("sum").is_some(), "结果应包含 sum 字段");
|
||||
assert!(
|
||||
result_val.get("greeting").is_some(),
|
||||
"结果应包含 greeting 字段"
|
||||
);
|
||||
assert!(result_val.get("message").is_some(), "结果应包含 message 字段");
|
||||
assert!(result_val.get("numbers").is_some(), "结果应包含 numbers 字段");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_ts_type_checking() -> Result<()> {
|
||||
// 初始化日志
|
||||
setup();
|
||||
|
||||
// 从 fixtures 目录读取测试脚本
|
||||
let code = std::fs::read_to_string("fixtures/test_ts.ts")?;
|
||||
info!("读取测试脚本: test_ts.ts");
|
||||
|
||||
// 执行脚本
|
||||
info!("开始执行TypeScript类型检查...");
|
||||
let result =
|
||||
CodeExecutor::execute_with_params_compat(&code, LanguageScript::Ts, None).await?;
|
||||
info!("类型检查完成");
|
||||
|
||||
if let Some(error) = &result.error {
|
||||
info!("类型检查错误: {error}");
|
||||
} else {
|
||||
info!("类型检查通过");
|
||||
}
|
||||
|
||||
// 验证结果
|
||||
assert!(result.error.is_none(), "TypeScript 类型检查应通过");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_ts_with_timeout() -> Result<()> {
|
||||
// 初始化日志
|
||||
setup();
|
||||
|
||||
// 创建一个会超时的TypeScript脚本
|
||||
let code = r#"
|
||||
// 创建一个长时间运行的脚本
|
||||
function sleep(ms: number): Promise<void> {
|
||||
return new Promise<void>(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
async function handler(input: any): Promise<{result: string}> {
|
||||
console.log("开始执行耗时操作");
|
||||
|
||||
// 这个操作会运行10秒钟
|
||||
for (let i = 0; i < 10; i++) {
|
||||
console.log(`已经执行了 ${i+1} 秒`);
|
||||
await sleep(1000); // 等待1秒
|
||||
}
|
||||
|
||||
console.log("操作完成");
|
||||
return { result: "完成" };
|
||||
}
|
||||
"#;
|
||||
info!("创建了一个会运行10秒的测试脚本");
|
||||
|
||||
// 准备参数
|
||||
let params = json!({
|
||||
"test": "超时测试"
|
||||
});
|
||||
info!("准备测试参数: {params:?}");
|
||||
|
||||
// 设置2秒超时并执行脚本
|
||||
info!("开始执行TypeScript脚本,设置2秒超时...");
|
||||
let start_time = std::time::Instant::now();
|
||||
let result =
|
||||
CodeExecutor::execute_with_params(code, LanguageScript::Ts, Some(params), Some(2))
|
||||
.await;
|
||||
let elapsed = start_time.elapsed();
|
||||
|
||||
// 检查是否在2-3秒内超时(给一点缓冲时间)
|
||||
info!("脚本执行耗时: {elapsed:?}");
|
||||
assert!(elapsed.as_secs() >= 2, "脚本应该至少运行2秒");
|
||||
assert!(elapsed.as_secs() < 4, "脚本应该在4秒内超时");
|
||||
|
||||
// 检查是否返回超时错误
|
||||
match result {
|
||||
Ok(exec_result) => {
|
||||
if let Some(error) = exec_result.error {
|
||||
info!("正确捕获到超时错误: {error}");
|
||||
assert!(
|
||||
error.contains("timed out") ||
|
||||
error.contains("TimedOut") ||
|
||||
error.contains("executor await error"),
|
||||
"错误信息应该包含超时相关信息"
|
||||
);
|
||||
} else {
|
||||
panic!("应该捕获到超时错误,但脚本执行成功了");
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
// 使用特殊格式化获取完整错误链
|
||||
let full_error = format!("{e:#}");
|
||||
info!("捕获到错误: {full_error}");
|
||||
assert!(
|
||||
full_error.contains("timed out"),
|
||||
"完整错误链中应该包含'timed out'超时信息"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
366
qiming-run_code_rmcp/src/warm_up.rs
Normal file
366
qiming-run_code_rmcp/src/warm_up.rs
Normal file
@@ -0,0 +1,366 @@
|
||||
use crate::model::CommandExecutor;
|
||||
use anyhow::Result;
|
||||
use log::{info, warn};
|
||||
use std::path::Path;
|
||||
use tokio::process::Command;
|
||||
|
||||
//定义国内python加速地址: https://mirrors.aliyun.com/pypi/simple
|
||||
const PYTHON_ACCELERATION_ADDRESS: &str = "https://mirrors.aliyun.com/pypi/simple";
|
||||
|
||||
//使用 uv安装 python 3.13,比如: uv python install 3.13
|
||||
async fn install_python_3_13() -> Result<()> {
|
||||
let mut cmd = Command::new("uv");
|
||||
cmd.arg("python")
|
||||
.arg("install")
|
||||
.arg("3.13")
|
||||
.kill_on_drop(true);
|
||||
match CommandExecutor::with_timeout(cmd.status(), 600).await {
|
||||
Ok(Ok(status)) => {
|
||||
if !status.success() {
|
||||
warn!("安装Python 3.13失败");
|
||||
return Err(anyhow::anyhow!("安装Python 3.13失败"));
|
||||
}
|
||||
info!("安装Python 3.13成功");
|
||||
Ok(())
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
warn!("命令执行失败: {e}");
|
||||
Err(anyhow::anyhow!("命令执行失败: {}", e))
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("执行超时或系统错误: {e}");
|
||||
Err(anyhow::anyhow!("执行超时或系统错误: {}", e))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 检查 uv 虚拟环境是否存在
|
||||
async fn check_and_create_uv_venv() -> Result<()> {
|
||||
info!("检查 uv 虚拟环境...");
|
||||
|
||||
// 检查 .venv 目录是否存在
|
||||
if Path::new(".venv").exists() {
|
||||
info!("uv 虚拟环境已存在");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
info!("未检测到 uv 虚拟环境,开始创建...");
|
||||
let mut cmd = Command::new("uv");
|
||||
cmd.arg("venv").kill_on_drop(true);
|
||||
|
||||
match CommandExecutor::with_timeout(cmd.status(), 600).await {
|
||||
Ok(Ok(status)) => {
|
||||
if !status.success() {
|
||||
warn!("创建 uv 虚拟环境失败");
|
||||
return Err(anyhow::anyhow!("创建 uv 虚拟环境失败"));
|
||||
}
|
||||
info!("创建 uv 虚拟环境成功");
|
||||
Ok(())
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
warn!("命令执行失败: {e}");
|
||||
Err(anyhow::anyhow!("命令执行失败: {}", e))
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("执行超时或系统错误: {e}");
|
||||
Err(anyhow::anyhow!("执行超时或系统错误: {}", e))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 预热Python环境,安装常用依赖
|
||||
async fn warm_up_python_env(custom_deps: Option<Vec<String>>) -> Result<()> {
|
||||
info!("开始预热Python环境...");
|
||||
|
||||
// 默认Python依赖列表
|
||||
let default_deps = [
|
||||
"requests",
|
||||
"pandas",
|
||||
"numpy",
|
||||
"matplotlib",
|
||||
"scikit-learn",
|
||||
"pytest",
|
||||
"pydantic",
|
||||
"fastapi",
|
||||
"uvicorn",
|
||||
"sqlalchemy",
|
||||
"opencv-python",
|
||||
"python-docx",
|
||||
];
|
||||
|
||||
// 使用自定义依赖或默认依赖
|
||||
let deps_to_install = if let Some(deps) = custom_deps {
|
||||
info!("使用自定义Python依赖列表");
|
||||
deps
|
||||
} else {
|
||||
info!("使用默认Python依赖列表");
|
||||
default_deps.iter().map(|&s| s.to_string()).collect()
|
||||
};
|
||||
|
||||
let total_deps = deps_to_install.len();
|
||||
info!("总共需要预热 {total_deps} 个Python依赖");
|
||||
|
||||
// 使用uv安装依赖
|
||||
for (index, dep) in deps_to_install.iter().enumerate() {
|
||||
let progress = ((index + 1) as f32 / total_deps as f32 * 100.0) as u32;
|
||||
info!("预热进度: {progress}% - 正在安装Python依赖: {dep}");
|
||||
|
||||
let mut cmd = Command::new("uv");
|
||||
cmd.arg("pip")
|
||||
.arg("install")
|
||||
.arg("--default-index")
|
||||
.arg(PYTHON_ACCELERATION_ADDRESS)
|
||||
.arg(dep)
|
||||
.kill_on_drop(true);
|
||||
|
||||
match CommandExecutor::with_timeout(cmd.status(), 600).await {
|
||||
Ok(Ok(status)) => {
|
||||
if !status.success() {
|
||||
warn!("安装依赖 {dep} 失败");
|
||||
}
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
warn!("命令执行失败: {e} - 依赖: {dep}");
|
||||
continue;
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("执行超时或系统错误: {e} - 依赖: {dep}");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
info!("Python环境预热完成 (100%)");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 预热JavaScript/TypeScript环境,缓存常用模块
|
||||
async fn warm_up_js_env(
|
||||
custom_npm_packages: Option<Vec<String>>,
|
||||
custom_jsr_packages: Option<Vec<String>>,
|
||||
custom_node_modules: Option<Vec<String>>,
|
||||
) -> Result<()> {
|
||||
info!("开始预热JavaScript/TypeScript环境...");
|
||||
|
||||
// 默认npm包列表
|
||||
let default_npm_packages = [
|
||||
"lodash",
|
||||
"axios",
|
||||
"moment",
|
||||
"uuid",
|
||||
"express",
|
||||
"react",
|
||||
"react-dom",
|
||||
"typescript",
|
||||
"jest",
|
||||
"webpack",
|
||||
];
|
||||
|
||||
// 默认JSR包列表
|
||||
let default_jsr_packages = [
|
||||
"@std/testing",
|
||||
"@std/http",
|
||||
"@std/path",
|
||||
"@std/fs",
|
||||
"@std/encoding/json",
|
||||
];
|
||||
|
||||
// 默认Node.js内置模块列表
|
||||
let default_node_modules = [
|
||||
"crypto", "buffer", "fs", "path", "http", "https", "url", "util", "stream", "events",
|
||||
];
|
||||
|
||||
// 使用自定义npm包或默认包
|
||||
let npm_packages = if let Some(packages) = custom_npm_packages {
|
||||
info!("使用自定义npm包列表");
|
||||
packages
|
||||
} else {
|
||||
info!("使用默认npm包列表");
|
||||
default_npm_packages
|
||||
.iter()
|
||||
.map(|&s| s.to_string())
|
||||
.collect()
|
||||
};
|
||||
|
||||
// 使用自定义JSR包或默认包
|
||||
let jsr_packages = if let Some(packages) = custom_jsr_packages {
|
||||
info!("使用自定义JSR包列表");
|
||||
packages
|
||||
} else {
|
||||
info!("使用默认JSR包列表");
|
||||
default_jsr_packages
|
||||
.iter()
|
||||
.map(|&s| s.to_string())
|
||||
.collect()
|
||||
};
|
||||
|
||||
// 使用自定义Node.js模块或默认模块
|
||||
let node_modules = if let Some(modules) = custom_node_modules {
|
||||
info!("使用自定义Node.js模块列表");
|
||||
modules
|
||||
} else {
|
||||
info!("使用默认Node.js模块列表");
|
||||
default_node_modules
|
||||
.iter()
|
||||
.map(|&s| s.to_string())
|
||||
.collect()
|
||||
};
|
||||
|
||||
// 计算总任务数
|
||||
let total_tasks = npm_packages.len() + jsr_packages.len() + node_modules.len();
|
||||
info!("总共需要预热 {total_tasks} 个JavaScript/TypeScript模块");
|
||||
|
||||
let mut completed_tasks = 0;
|
||||
|
||||
// 预热npm包
|
||||
for pkg in npm_packages.iter() {
|
||||
completed_tasks += 1;
|
||||
let progress = (completed_tasks as f32 / total_tasks as f32 * 100.0) as u32;
|
||||
info!("预热进度: {progress}% - 正在缓存npm包: {pkg}");
|
||||
|
||||
let mut cmd = Command::new("deno");
|
||||
cmd.args(["cache", "--reload", &format!("npm:{pkg}")])
|
||||
.kill_on_drop(true);
|
||||
|
||||
match CommandExecutor::with_timeout(cmd.status(), 600).await {
|
||||
Ok(Ok(status)) => {
|
||||
if !status.success() {
|
||||
warn!("缓存npm包 {pkg} 失败");
|
||||
}
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
warn!("命令执行失败: {e} - 包: {pkg}");
|
||||
continue;
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("执行超时或系统错误: {e} - 包: {pkg}");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 预热JSR包
|
||||
for pkg in jsr_packages.iter() {
|
||||
completed_tasks += 1;
|
||||
let progress = (completed_tasks as f32 / total_tasks as f32 * 100.0) as u32;
|
||||
info!("预热进度: {progress}% - 正在缓存JSR包: {pkg}");
|
||||
|
||||
let mut cmd = Command::new("deno");
|
||||
cmd.args(["cache", "--reload", &format!("jsr:{pkg}")])
|
||||
.kill_on_drop(true);
|
||||
|
||||
match CommandExecutor::with_timeout(cmd.status(), 600).await {
|
||||
Ok(Ok(status)) => {
|
||||
if !status.success() {
|
||||
warn!("缓存JSR包 {pkg} 失败");
|
||||
}
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
warn!("命令执行失败: {e} - 包: {pkg}");
|
||||
continue;
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("执行超时或系统错误: {e} - 包: {pkg}");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 预热Node.js内置模块
|
||||
for module in node_modules.iter() {
|
||||
completed_tasks += 1;
|
||||
let progress = (completed_tasks as f32 / total_tasks as f32 * 100.0) as u32;
|
||||
info!("预热进度: {progress}% - 正在缓存Node.js模块: {module}");
|
||||
|
||||
let mut cmd = Command::new("deno");
|
||||
cmd.args(["cache", "--reload", &format!("node:{module}")])
|
||||
.kill_on_drop(true);
|
||||
|
||||
match CommandExecutor::with_timeout(cmd.status(), 600).await {
|
||||
Ok(Ok(status)) => {
|
||||
if !status.success() {
|
||||
warn!("缓存Node.js模块 {module} 失败");
|
||||
}
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
warn!("命令执行失败: {e} - 模块: {module}");
|
||||
continue;
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("执行超时或系统错误: {e} - 包: {module}");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
info!("JavaScript/TypeScript环境预热完成 (100%)");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 预热所有脚本执行环境
|
||||
pub async fn warm_up_all_envs(
|
||||
custom_python_deps: Option<Vec<String>>,
|
||||
custom_npm_packages: Option<Vec<String>>,
|
||||
custom_jsr_packages: Option<Vec<String>>,
|
||||
custom_node_modules: Option<Vec<String>>,
|
||||
) -> Result<()> {
|
||||
info!("开始预热所有脚本执行环境...");
|
||||
|
||||
// 检查并创建 uv 虚拟环境
|
||||
if let Err(e) = check_and_create_uv_venv().await {
|
||||
warn!("检查或创建 uv 虚拟环境失败: {e}");
|
||||
}
|
||||
|
||||
// 安装Python 3.13
|
||||
if let Err(e) = install_python_3_13().await {
|
||||
warn!("安装Python 3.13失败: {e}");
|
||||
}
|
||||
|
||||
// 预热Python环境
|
||||
if let Err(e) = warm_up_python_env(custom_python_deps).await {
|
||||
warn!("预热Python环境失败: {e}");
|
||||
}
|
||||
|
||||
// 预热JavaScript/TypeScript环境
|
||||
if let Err(e) = warm_up_js_env(
|
||||
custom_npm_packages,
|
||||
custom_jsr_packages,
|
||||
custom_node_modules,
|
||||
)
|
||||
.await
|
||||
{
|
||||
warn!("预热JavaScript/TypeScript环境失败: {e}");
|
||||
}
|
||||
|
||||
info!("所有脚本执行环境预热完成");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_warm_up_python_env() {
|
||||
// 测试默认依赖
|
||||
warm_up_python_env(None).await.unwrap();
|
||||
|
||||
// 测试自定义依赖
|
||||
let custom_deps = vec!["requests".to_string(), "pandas".to_string()];
|
||||
warm_up_python_env(Some(custom_deps)).await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_warm_up_js_env() {
|
||||
// 测试默认依赖
|
||||
warm_up_js_env(None, None, None).await.unwrap();
|
||||
|
||||
// 测试自定义依赖
|
||||
let custom_npm = vec!["lodash".to_string(), "axios".to_string()];
|
||||
let custom_jsr = vec!["@std/testing".to_string()];
|
||||
let custom_node = vec!["crypto".to_string(), "buffer".to_string()];
|
||||
warm_up_js_env(Some(custom_npm), Some(custom_jsr), Some(custom_node))
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user