"添加前端模板和运行代码模块"
This commit is contained in:
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>,
|
||||
}
|
||||
Reference in New Issue
Block a user