"添加前端模板和运行代码模块"
This commit is contained in:
109
qiming-run_code_rmcp/.cursor/rules/code-execution.mdc
Normal file
109
qiming-run_code_rmcp/.cursor/rules/code-execution.mdc
Normal file
@@ -0,0 +1,109 @@
|
||||
---
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: false
|
||||
---
|
||||
# 代码执行流程
|
||||
|
||||
本项目的核心功能是执行JavaScript、TypeScript和Python代码,并捕获执行结果。以下是代码执行的主要流程:
|
||||
|
||||
## 代码执行模型
|
||||
|
||||
项目使用模块化的代码执行模型,定义在[src/model/code_run_model.rs](mdc:src/model/code_run_model.rs)中:
|
||||
|
||||
```rust
|
||||
/// 执行结果,包含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>,
|
||||
}
|
||||
|
||||
/// 代码执行器
|
||||
pub struct CodeExecutor;
|
||||
|
||||
impl CodeExecutor {
|
||||
/// 执行代码
|
||||
pub async fn execute(
|
||||
code: &str,
|
||||
language: LanguageScript,
|
||||
) -> Result<CodeScriptExecutionResult> {
|
||||
Self::execute_with_params(code, language, None).await
|
||||
}
|
||||
|
||||
/// 执行代码并传递参数
|
||||
pub async fn execute_with_params(
|
||||
code: &str,
|
||||
language: LanguageScript,
|
||||
params: Option<serde_json::Value>,
|
||||
) -> Result<CodeScriptExecutionResult> {
|
||||
match language {
|
||||
LanguageScript::Js => JsRunner::new().run_with_params(code, params).await,
|
||||
LanguageScript::Ts => TsRunner::new().run_with_params(code, params).await,
|
||||
LanguageScript::Python => PythonRunner::new().run_with_params(code, params).await,
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 语言特定执行器
|
||||
|
||||
### JavaScript执行器
|
||||
|
||||
JavaScript代码通过[JsRunner](mdc:src/deno_runner/js_runner.rs)执行:
|
||||
|
||||
1. 准备JavaScript代码,添加日志捕获和handler函数执行逻辑
|
||||
2. 使用代码哈希值缓存处理过的代码
|
||||
3. 使用Deno执行代码
|
||||
4. 解析执行结果
|
||||
|
||||
### TypeScript执行器
|
||||
|
||||
TypeScript代码通过[TsRunner](mdc:src/deno_runner/ts_runner.rs)执行:
|
||||
|
||||
1. 准备TypeScript代码,添加日志捕获和handler函数执行逻辑
|
||||
2. 使用代码哈希值缓存处理过的代码
|
||||
3. 使用Deno执行代码
|
||||
4. 解析执行结果
|
||||
|
||||
### Python执行器
|
||||
|
||||
Python代码通过[PythonRunner](mdc:src/python_runner/python_runner.rs)执行:
|
||||
|
||||
1. 分析Python代码的依赖
|
||||
2. 准备Python代码,添加日志捕获和handler/main函数执行逻辑
|
||||
3. 使用代码哈希值缓存处理过的代码
|
||||
4. 使用uv在隔离环境中执行Python代码
|
||||
5. 解析执行结果
|
||||
|
||||
## 参数传递
|
||||
|
||||
参数通过JSON格式传递给脚本:
|
||||
|
||||
1. JavaScript/TypeScript:参数直接传递给handler函数
|
||||
2. Python:参数通过环境变量`INPUT_JSON`传递,然后在脚本中解析并传递给handler或main函数
|
||||
|
||||
## 结果处理
|
||||
|
||||
执行结果通过[CodeScriptExecutionResult](mdc:src/model/code_run_model.rs)结构体表示,包含:
|
||||
|
||||
- result: 执行结果(通过handler/main函数返回)
|
||||
- logs: 执行过程中的日志
|
||||
- success: 执行是否成功
|
||||
- error: 执行错误(如果有)
|
||||
|
||||
## 缓存机制
|
||||
|
||||
为了提高性能,项目使用[CodeFileCache](mdc:src/cache)缓存处理过的代码:
|
||||
|
||||
1. 计算代码的哈希值
|
||||
2. 检查是否存在对应的缓存
|
||||
3. 如果存在,直接使用缓存;否则处理代码并缓存结果
|
||||
72
qiming-run_code_rmcp/.cursor/rules/dependencies.mdc
Normal file
72
qiming-run_code_rmcp/.cursor/rules/dependencies.mdc
Normal file
@@ -0,0 +1,72 @@
|
||||
---
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: false
|
||||
---
|
||||
# 项目依赖
|
||||
|
||||
本项目使用多个Rust库来实现其功能。以下是主要依赖项及其用途:
|
||||
|
||||
## Cargo.toml
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
tokio = { version = "1.32", features = ["full"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
log = "0.4"
|
||||
env_logger = "0.10"
|
||||
anyhow = "1.0"
|
||||
tempfile = "3.8"
|
||||
regex = "1.9"
|
||||
clap = { version = "4.4", features = ["derive"] }
|
||||
thiserror = "1.0"
|
||||
```
|
||||
|
||||
## 主要依赖项
|
||||
|
||||
### 异步运行时
|
||||
|
||||
- [tokio](https://crates.io/crates/tokio): 异步运行时,用于支持异步/await语法和并发执行
|
||||
|
||||
### 序列化/反序列化
|
||||
|
||||
- [serde](https://crates.io/crates/serde): 通用序列化/反序列化框架
|
||||
- [serde_json](https://crates.io/crates/serde_json): JSON序列化/反序列化支持
|
||||
|
||||
### 错误处理
|
||||
|
||||
- [anyhow](https://crates.io/crates/anyhow): 简化错误处理的库
|
||||
- [thiserror](https://crates.io/crates/thiserror): 用于定义自定义错误类型
|
||||
|
||||
### 日志记录
|
||||
|
||||
- [log](https://crates.io/crates/log): 日志记录接口
|
||||
- [env_logger](https://crates.io/crates/env_logger): 基于环境变量的日志记录器
|
||||
|
||||
### 命令行参数解析
|
||||
|
||||
- [clap](https://crates.io/crates/clap): 命令行参数解析库,支持派生宏
|
||||
|
||||
### 其他工具
|
||||
|
||||
- [rmcp](https://crates.io/crates/rmcp): Model Context Protocol (MCP) SDK
|
||||
- [tempfile](https://crates.io/crates/tempfile): 临时文件创建和管理
|
||||
- [regex](https://crates.io/crates/regex): 正则表达式支持
|
||||
|
||||
## 外部依赖
|
||||
|
||||
除了Rust库外,本项目还依赖以下外部工具:
|
||||
|
||||
1. [Deno](https://deno.land/): 现代JavaScript和TypeScript运行时,用于执行JavaScript代码
|
||||
2. [uv](https://github.com/astral-sh/uv): Python包管理器和隔离环境工具,用于安全执行Python代码
|
||||
|
||||
## 安装外部依赖
|
||||
|
||||
```bash
|
||||
# 安装Deno
|
||||
curl -fsSL https://deno.land/x/install/install.sh | sh
|
||||
|
||||
# 安装uv
|
||||
curl -fsSL https://astral.sh/uv/install.sh | sh
|
||||
```
|
||||
101
qiming-run_code_rmcp/.cursor/rules/error-handling.mdc
Normal file
101
qiming-run_code_rmcp/.cursor/rules/error-handling.mdc
Normal file
@@ -0,0 +1,101 @@
|
||||
---
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: false
|
||||
---
|
||||
# 错误处理
|
||||
|
||||
本项目使用[anyhow](mdc:https:/crates.io/crates/anyhow)和[thiserror](mdc:https:/crates.io/crates/thiserror)库进行错误处理,错误类型定义在[error.rs](mdc:src/error.rs)中。
|
||||
|
||||
## 错误类型
|
||||
|
||||
```rust
|
||||
#[derive(Error, Debug)]
|
||||
pub enum AppError {
|
||||
#[error("Failed to execute code: {0}")]
|
||||
ExecutionError(String),
|
||||
|
||||
#[error("Failed to parse output: {0}")]
|
||||
ParseError(String),
|
||||
|
||||
#[error("Failed to create temporary file: {0}")]
|
||||
IoError(#[from] std::io::Error),
|
||||
|
||||
#[error("Failed to parse JSON: {0}")]
|
||||
JsonError(#[from] serde_json::Error),
|
||||
|
||||
#[error("Missing required field: {0}")]
|
||||
MissingField(String),
|
||||
|
||||
#[error("Unsupported language: {0}")]
|
||||
UnsupportedLanguage(String),
|
||||
|
||||
#[error("Handler function not found")]
|
||||
HandlerNotFound,
|
||||
|
||||
#[error("MCP error: {0}")]
|
||||
McpError(String),
|
||||
|
||||
#[error("{0}")]
|
||||
Other(String),
|
||||
}
|
||||
|
||||
pub type AppResult<T> = Result<T, AppError>;
|
||||
```
|
||||
|
||||
## 错误处理流程
|
||||
|
||||
1. 在代码执行过程中使用`anyhow::Result`类型传递错误
|
||||
2. 使用`?`操作符传播错误
|
||||
3. 使用`context`方法添加错误上下文
|
||||
4. 在`main`函数中处理所有未捕获的错误
|
||||
|
||||
## 示例
|
||||
|
||||
```rust
|
||||
// 读取代码文件
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
// 执行JavaScript代码
|
||||
fn execute_js(code: &str, show_logs: bool) -> Result<ExecutionResult> {
|
||||
// Prepare the JavaScript code with wrapped handler
|
||||
let wrapped_code = prepare_js_code(code, show_logs);
|
||||
|
||||
// Create a temporary file
|
||||
let mut temp_file = NamedTempFile::new()?;
|
||||
write!(temp_file.as_file_mut(), "{}", wrapped_code)?;
|
||||
|
||||
// Execute with Deno
|
||||
let output = Command::new("deno")
|
||||
.arg("run")
|
||||
.arg("--allow-all")
|
||||
.arg(temp_file.path())
|
||||
.output()
|
||||
.context("Failed to execute Deno")?;
|
||||
|
||||
// Parse the output
|
||||
parse_execution_output(&output.stdout, &output.stderr)
|
||||
}
|
||||
```
|
||||
|
||||
## 错误输出
|
||||
|
||||
执行结果中的错误通过`ExecutionResult`结构体的`error`字段返回,并在`print_result`函数中输出:
|
||||
|
||||
```rust
|
||||
fn print_result(result: ExecutionResult) {
|
||||
// ...
|
||||
|
||||
if let Some(error) = result.error {
|
||||
eprintln!("Error: {}", error);
|
||||
}
|
||||
}
|
||||
```
|
||||
138
qiming-run_code_rmcp/.cursor/rules/examples.mdc
Normal file
138
qiming-run_code_rmcp/.cursor/rules/examples.mdc
Normal file
@@ -0,0 +1,138 @@
|
||||
---
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: false
|
||||
---
|
||||
# 示例代码
|
||||
|
||||
本项目包含JavaScript和Python的示例代码,用于测试代码执行功能。
|
||||
|
||||
## JavaScript示例
|
||||
|
||||
[examples/test_js.js](mdc:examples/test_js.js)是一个简单的JavaScript示例,包含:
|
||||
|
||||
```javascript
|
||||
// Sample JavaScript file with handler function
|
||||
|
||||
// Log some debug information
|
||||
console.log("Initializing JavaScript test...");
|
||||
|
||||
// Define a simple add function
|
||||
function add(a, b) {
|
||||
console.log(`Adding ${a} + ${b}`);
|
||||
return a + b;
|
||||
}
|
||||
|
||||
// Some processing
|
||||
const numbers = [1, 2, 3, 4, 5];
|
||||
console.log("Processing numbers:", numbers);
|
||||
|
||||
// Handler function that will be called to get the result
|
||||
function handler() {
|
||||
console.log("Handler function called");
|
||||
|
||||
// Calculate sum
|
||||
const sum = numbers.reduce((acc, num) => add(acc, num), 0);
|
||||
console.log("Final calculation completed");
|
||||
|
||||
return `The sum of [${numbers.join(", ")}] is ${sum}`;
|
||||
}
|
||||
```
|
||||
|
||||
执行这个示例将输出:
|
||||
|
||||
```
|
||||
--- Logs ---
|
||||
Initializing JavaScript test...
|
||||
Processing numbers: [1,2,3,4,5]
|
||||
Handler function called
|
||||
Adding 0 + 1
|
||||
Adding 1 + 2
|
||||
Adding 3 + 3
|
||||
Adding 6 + 4
|
||||
Adding 10 + 5
|
||||
Final calculation completed
|
||||
------------
|
||||
Result: The sum of [1, 2, 3, 4, 5] is 15
|
||||
```
|
||||
|
||||
## Python示例
|
||||
|
||||
[examples/test_python.py](mdc:examples/test_python.py)是一个简单的Python示例,包含:
|
||||
|
||||
```python
|
||||
#!/usr/bin/env python3
|
||||
# Sample Python file with handler function
|
||||
|
||||
import time
|
||||
|
||||
# Log some debug information
|
||||
print("Initializing Python test...")
|
||||
|
||||
# Define a simple function
|
||||
def multiply(a, b):
|
||||
print(f"Multiplying {a} * {b}")
|
||||
return a * b
|
||||
|
||||
# Some processing
|
||||
numbers = [1, 2, 3, 4, 5]
|
||||
print(f"Processing numbers: {numbers}")
|
||||
|
||||
# Handler function that will be called to get the result
|
||||
def handler():
|
||||
print("Handler function called")
|
||||
|
||||
# Calculate product
|
||||
product = 1
|
||||
for num in numbers:
|
||||
product = multiply(product, num)
|
||||
|
||||
print("Final calculation completed")
|
||||
|
||||
return f"The product of {numbers} is {product}"
|
||||
|
||||
# This part won't be executed when the code is run through the MCP runner
|
||||
if __name__ == "__main__":
|
||||
print("Running directly, calling handler...")
|
||||
result = handler()
|
||||
print(f"Result: {result}")
|
||||
```
|
||||
|
||||
执行这个示例将输出:
|
||||
|
||||
```
|
||||
--- Logs ---
|
||||
Handler function called
|
||||
Multiplying 1 * 1
|
||||
Multiplying 1 * 2
|
||||
Multiplying 2 * 3
|
||||
Multiplying 6 * 4
|
||||
Multiplying 24 * 5
|
||||
Final calculation completed
|
||||
------------
|
||||
Result: The product of [1, 2, 3, 4, 5] is 120
|
||||
```
|
||||
|
||||
## 如何创建自己的示例
|
||||
|
||||
创建自己的示例代码时,需要遵循以下规则:
|
||||
|
||||
1. 必须包含一个名为`handler`的函数,作为执行结果的返回点
|
||||
2. `handler`函数的返回值将作为执行结果
|
||||
3. 使用`console.log`(JavaScript)或`print`(Python)输出日志
|
||||
4. 可以定义其他函数和变量,它们将在执行过程中被使用
|
||||
|
||||
示例:
|
||||
|
||||
```javascript
|
||||
// JavaScript示例
|
||||
function handler() {
|
||||
return "Hello from JavaScript!";
|
||||
}
|
||||
```
|
||||
|
||||
```python
|
||||
# Python示例
|
||||
def handler():
|
||||
return "Hello from Python!"
|
||||
```
|
||||
98
qiming-run_code_rmcp/.cursor/rules/fixtures-guide.mdc
Normal file
98
qiming-run_code_rmcp/.cursor/rules/fixtures-guide.mdc
Normal file
@@ -0,0 +1,98 @@
|
||||
---
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: false
|
||||
---
|
||||
# 测试脚本示例指南
|
||||
|
||||
本项目包含多种语言的测试脚本示例,位于`fixtures`目录下。这些示例展示了如何编写可被本工具执行的JavaScript、TypeScript和Python脚本。
|
||||
|
||||
## JavaScript示例
|
||||
|
||||
### 基本执行
|
||||
|
||||
[test_js.js](mdc:fixtures/test_js.js) - 基本的JavaScript执行示例:
|
||||
- 展示了如何使用`console.log`输出日志
|
||||
- 定义了`handler`函数作为执行结果的返回点
|
||||
|
||||
### 参数传递
|
||||
|
||||
[test_js_params.js](mdc:fixtures/test_js_params.js) - 带参数的JavaScript执行示例:
|
||||
- 展示了如何接收和处理传入的参数
|
||||
- 演示了如何根据参数执行不同的逻辑
|
||||
|
||||
### 函数示例
|
||||
|
||||
[rfunction_test1.js](mdc:fixtures/rfunction_test1.js) - 函数式JavaScript示例:
|
||||
- 展示了如何定义和使用函数
|
||||
- 演示了如何处理和返回复杂数据结构
|
||||
|
||||
## TypeScript示例
|
||||
|
||||
### 基本执行
|
||||
|
||||
[test_ts.ts](mdc:fixtures/test_ts.ts) - 基本的TypeScript执行示例:
|
||||
- 展示了TypeScript的类型定义
|
||||
- 定义了带类型的`handler`函数
|
||||
|
||||
### 参数传递
|
||||
|
||||
[test_ts_params.ts](mdc:fixtures/test_ts_params.ts) - 带参数的TypeScript执行示例:
|
||||
- 展示了如何定义参数类型
|
||||
- 演示了TypeScript中的类型安全参数处理
|
||||
|
||||
## Python示例
|
||||
|
||||
### 基本执行
|
||||
|
||||
[test_python.py](mdc:fixtures/test_python.py) - 基本的Python执行示例:
|
||||
- 使用`handler`函数作为执行结果的返回点
|
||||
- 展示了如何使用Python进行基本计算和日志输出
|
||||
|
||||
### 参数传递
|
||||
|
||||
[test_python_params.py](mdc:fixtures/test_python_params.py) - 带参数的Python执行示例:
|
||||
- 展示了如何接收和处理传入的参数
|
||||
- 演示了如何返回包含多个字段的结果
|
||||
|
||||
### 类型示例
|
||||
|
||||
[test_python_types.py](mdc:fixtures/test_python_types.py) - 不同返回类型的Python示例:
|
||||
- 展示了如何返回不同类型的值(字符串、数字、布尔值、列表、字典等)
|
||||
- 演示了不同类型在JSON序列化中的处理方式
|
||||
|
||||
### 函数示例
|
||||
|
||||
[rfunction_python.py](mdc:fixtures/rfunction_python.py) - 函数式Python示例:
|
||||
- 展示了如何定义和使用Python函数
|
||||
- 演示了如何处理和返回复杂数据结构
|
||||
|
||||
## 使用示例
|
||||
|
||||
以下是如何使用这些示例脚本的命令:
|
||||
|
||||
```bash
|
||||
# 执行基本JavaScript示例
|
||||
cargo run -- --show-logs js -f fixtures/test_js.js
|
||||
|
||||
# 执行带参数的JavaScript示例
|
||||
cargo run -- --show-logs js -f fixtures/test_js_params.js -p '{"name":"User"}'
|
||||
|
||||
# 执行基本TypeScript示例
|
||||
cargo run -- --show-logs ts -f fixtures/test_ts.ts
|
||||
|
||||
# 执行带参数的TypeScript示例
|
||||
cargo run -- --show-logs ts -f fixtures/test_ts_params.ts -p '{"a":10, "b":20, "name":"User"}'
|
||||
|
||||
# 执行基本Python示例
|
||||
cargo run -- --show-logs python -f fixtures/test_python.py
|
||||
|
||||
# 执行带参数的Python示例
|
||||
cargo run -- --show-logs python -f fixtures/test_python_params.py -p '{"a":10, "b":20}'
|
||||
|
||||
# 执行不同类型的Python示例
|
||||
cargo run -- --show-logs python -f fixtures/test_python_types.py -p '{"type":"string"}'
|
||||
cargo run -- --show-logs python -f fixtures/test_python_types.py -p '{"type":"number"}'
|
||||
cargo run -- --show-logs python -f fixtures/test_python_types.py -p '{"type":"list"}'
|
||||
cargo run -- --show-logs python -f fixtures/test_python_types.py -p '{"type":"dict"}'
|
||||
```
|
||||
146
qiming-run_code_rmcp/.cursor/rules/mcp-integration.mdc
Normal file
146
qiming-run_code_rmcp/.cursor/rules/mcp-integration.mdc
Normal file
@@ -0,0 +1,146 @@
|
||||
---
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: false
|
||||
---
|
||||
# MCP SDK集成
|
||||
|
||||
本项目支持Model Context Protocol (MCP) SDK集成,允许通过标准化协议执行代码。MCP相关功能主要在[mcp.rs](mdc:src/mcp.rs)中实现。
|
||||
|
||||
## 核心数据结构
|
||||
|
||||
```rust
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct CodeExecutionParams {
|
||||
pub code: String,
|
||||
pub language: String,
|
||||
pub show_logs: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct CodeExecutionResult {
|
||||
pub logs: Vec<String>,
|
||||
pub result: Option<String>,
|
||||
pub error: Option<String>,
|
||||
}
|
||||
```
|
||||
|
||||
## JavaScript执行
|
||||
|
||||
MCP中的JavaScript执行使用以下包装代码:
|
||||
|
||||
```javascript
|
||||
// MCP SDK integration for JavaScript
|
||||
// Note: MCP SDK is not required for basic functionality
|
||||
|
||||
// Log capture setup
|
||||
const logs = [];
|
||||
const originalConsoleLog = 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 mcpExecute() {
|
||||
try {
|
||||
// User code here
|
||||
|
||||
// Execute handler
|
||||
let result = null;
|
||||
if (typeof handler === 'function') {
|
||||
result = await handler();
|
||||
}
|
||||
|
||||
return {
|
||||
logs: logs,
|
||||
result: result !== undefined ? String(result) : null,
|
||||
error: null
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
logs: logs,
|
||||
result: null,
|
||||
error: error.toString()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Execute and return results
|
||||
mcpExecute().then(result => {
|
||||
originalConsoleLog(JSON.stringify(result));
|
||||
});
|
||||
```
|
||||
|
||||
## Python执行
|
||||
|
||||
MCP中的Python执行使用以下包装代码:
|
||||
|
||||
```python
|
||||
# MCP SDK integration for Python
|
||||
# Note: MCP SDK is not required for basic functionality
|
||||
import sys
|
||||
import json
|
||||
import io
|
||||
from contextlib import redirect_stdout
|
||||
|
||||
# Store logs
|
||||
logs = []
|
||||
|
||||
# Create custom stdout to capture logs
|
||||
class LogCapture(io.StringIO):
|
||||
def write(self, text):
|
||||
if text.strip():
|
||||
logs.append(text.rstrip())
|
||||
if show_logs:
|
||||
sys.__stdout__.write(text)
|
||||
|
||||
def mcp_execute():
|
||||
try:
|
||||
// User code here
|
||||
|
||||
// Execute handler function and capture result
|
||||
result = None
|
||||
with redirect_stdout(LogCapture()):
|
||||
if 'handler' in globals() and callable(handler):
|
||||
result = handler()
|
||||
|
||||
return {
|
||||
"logs": logs,
|
||||
"result": str(result) if result is not None else None,
|
||||
"error": None
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
"logs": logs,
|
||||
"result": None,
|
||||
"error": str(e)
|
||||
}
|
||||
|
||||
# Execute and print results
|
||||
result = mcp_execute()
|
||||
print(json.dumps(result))
|
||||
```
|
||||
|
||||
## 执行流程
|
||||
|
||||
MCP执行流程由[execute_with_mcp](mdc:src/mcp.rs#L138)函数处理:
|
||||
|
||||
1. 创建临时文件并写入包装后的代码
|
||||
2. 根据语言类型选择执行命令(Deno或uv)
|
||||
3. 执行代码并捕获输出
|
||||
4. 解析JSON格式的执行结果
|
||||
5. 如果无法解析JSON,则返回原始输出
|
||||
|
||||
## 使用方法
|
||||
|
||||
在命令行中使用`--use-mcp`参数启用MCP SDK集成:
|
||||
|
||||
```bash
|
||||
cargo run -- --use-mcp js -c "function handler() { return 'Hello from MCP JS'; }"
|
||||
cargo run -- --use-mcp python -f examples/test_python.py
|
||||
```
|
||||
50
qiming-run_code_rmcp/.cursor/rules/project-overview.mdc
Normal file
50
qiming-run_code_rmcp/.cursor/rules/project-overview.mdc
Normal file
@@ -0,0 +1,50 @@
|
||||
---
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: false
|
||||
---
|
||||
# 项目概述
|
||||
|
||||
这是一个使用Rust开发的命令行工具,用于执行JavaScript、TypeScript和Python代码。该工具支持以下功能:
|
||||
|
||||
1. 执行JavaScript代码(使用Deno运行时)
|
||||
2. 执行TypeScript代码(使用Deno运行时)
|
||||
3. 执行Python代码(使用uv提供隔离环境)
|
||||
4. 捕获并区分脚本中的日志输出和执行结果
|
||||
5. 支持JavaScript/TypeScript的handler函数和Python的handler/main函数作为执行结果的返回点
|
||||
6. 通过环境变量传递参数给脚本
|
||||
7. 可以通过参数控制是否显示日志输出
|
||||
8. 支持MCP SDK集成
|
||||
|
||||
## 主要文件
|
||||
|
||||
- [src/main.rs](mdc:src/main.rs) - 应用程序入口点,处理命令行参数和执行流程
|
||||
- [src/lib.rs](mdc:src/lib.rs) - 库入口点,导出模块和类型
|
||||
- [src/model/code_run_model.rs](mdc:src/model/code_run_model.rs) - 代码执行模型和结果处理
|
||||
- [src/deno_runner/js_runner.rs](mdc:src/deno_runner/js_runner.rs) - JavaScript代码执行器
|
||||
- [src/deno_runner/ts_runner.rs](mdc:src/deno_runner/ts_runner.rs) - TypeScript代码执行器
|
||||
- [src/python_runner/python_runner.rs](mdc:src/python_runner/python_runner.rs) - Python代码执行器
|
||||
- [src/cache/](mdc:src/cache) - 代码缓存管理
|
||||
- [fixtures/](mdc:fixtures) - 测试脚本示例
|
||||
|
||||
## 使用示例
|
||||
|
||||
```bash
|
||||
# 执行JavaScript文件
|
||||
cargo run -- --show-logs js -f fixtures/test_js.js
|
||||
|
||||
# 执行TypeScript文件
|
||||
cargo run -- --show-logs ts -f fixtures/test_ts.ts
|
||||
|
||||
# 执行Python文件
|
||||
cargo run -- --show-logs python -f fixtures/test_python.py
|
||||
|
||||
# 带参数执行Python文件
|
||||
cargo run -- --show-logs python -f fixtures/test_python_params.py -p '{"a":10, "b":20}'
|
||||
|
||||
# 直接执行JavaScript代码
|
||||
cargo run -- js -c "function handler(input) { return 'Hello from JS: ' + input.name; }" -p '{"name":"User"}'
|
||||
|
||||
# 直接执行Python代码
|
||||
cargo run -- python -c "def handler(args): return 'Hello from Python: ' + args.get('name', 'Guest')" -p '{"name":"User"}'
|
||||
```
|
||||
143
qiming-run_code_rmcp/.cursor/rules/python-execution.mdc
Normal file
143
qiming-run_code_rmcp/.cursor/rules/python-execution.mdc
Normal file
@@ -0,0 +1,143 @@
|
||||
---
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: false
|
||||
---
|
||||
# Python代码执行
|
||||
|
||||
本项目使用`uv`在隔离环境中执行Python代码,提供安全可靠的执行环境。以下是Python代码执行的详细流程:
|
||||
|
||||
## Python运行器
|
||||
|
||||
Python代码通过[PythonRunner](mdc:src/python_runner/python_runner.rs)执行,实现了`RunCode`特性:
|
||||
|
||||
```rust
|
||||
pub struct PythonRunner;
|
||||
|
||||
impl RunCode for PythonRunner {
|
||||
async fn run(&self, code: &str) -> Result<CodeScriptExecutionResult> {
|
||||
// 实现代码...
|
||||
}
|
||||
|
||||
async fn run_with_params(&self, code: &str, params: Option<serde_json::Value>) -> Result<CodeScriptExecutionResult> {
|
||||
// 实现代码...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 依赖解析
|
||||
|
||||
Python代码的依赖通过[python_dependencies.rs](mdc:src/python_runner/dependencies/python_dependencies.rs)解析:
|
||||
|
||||
1. 使用Pest解析器分析Python代码中的import语句
|
||||
2. 支持常规import语句和importlib动态导入
|
||||
3. 提取所有导入的模块名称
|
||||
|
||||
```rust
|
||||
pub fn parse_import(python_code: &str) -> Result<Vec<String>> {
|
||||
// 使用Pest解析器分析Python代码
|
||||
let pairs = ImportParser::parse(Rule::file, &input)?;
|
||||
|
||||
// 提取模块名称
|
||||
let mut imported_modules = Vec::new();
|
||||
// 处理各种import语句...
|
||||
|
||||
Ok(imported_modules)
|
||||
}
|
||||
```
|
||||
|
||||
## 代码准备
|
||||
|
||||
Python代码在执行前需要进行包装,添加日志捕获和函数执行逻辑:
|
||||
|
||||
```rust
|
||||
fn prepare_python_code(&self, code: &str, show_logs: bool) -> String {
|
||||
// 创建包装代码,包括:
|
||||
// 1. 日志捕获
|
||||
// 2. 参数解析
|
||||
// 3. 执行handler或main函数
|
||||
// 4. 结果格式化
|
||||
}
|
||||
```
|
||||
|
||||
包装代码主要功能:
|
||||
1. 捕获标准输出作为日志
|
||||
2. 从环境变量`INPUT_JSON`解析参数
|
||||
3. 优先执行`handler`函数,如果不存在则执行`main`函数
|
||||
4. 将执行结果和日志格式化为JSON
|
||||
|
||||
## 参数传递
|
||||
|
||||
参数通过环境变量`INPUT_JSON`传递给Python脚本:
|
||||
|
||||
```rust
|
||||
// 将参数序列化为JSON字符串
|
||||
let params_json = match params {
|
||||
Some(p) => serde_json::to_string(&p)?,
|
||||
None => "{}".to_string(),
|
||||
};
|
||||
|
||||
// 通过环境变量传递参数
|
||||
execute_command.env("INPUT_JSON", ¶ms_json);
|
||||
```
|
||||
|
||||
在Python脚本中,参数通过以下方式获取:
|
||||
|
||||
```python
|
||||
# 从环境变量获取输入参数
|
||||
args = {}
|
||||
try:
|
||||
input_json = os.environ.get('INPUT_JSON')
|
||||
if input_json:
|
||||
args = json.loads(input_json)
|
||||
except Exception as e:
|
||||
print(f"解析输入参数失败: {e}")
|
||||
```
|
||||
|
||||
## 执行流程
|
||||
|
||||
1. 计算代码哈希值,检查缓存
|
||||
2. 如果缓存存在,直接使用;否则:
|
||||
- 分析代码依赖
|
||||
- 准备包装后的代码
|
||||
- 保存到缓存
|
||||
3. 使用`uv`命令在隔离环境中执行代码:
|
||||
```rust
|
||||
let mut execute_command = Command::new("uv");
|
||||
execute_command
|
||||
.arg("run")
|
||||
.arg("-s") // 明确指定作为脚本运行
|
||||
.arg("--isolated") // 在隔离环境中运行
|
||||
.arg("-p")
|
||||
.arg("python3") // 指定Python解释器
|
||||
.env("INPUT_JSON", ¶ms_json) // 通过环境变量传递参数
|
||||
.arg(&temp_path)
|
||||
```
|
||||
4. 解析执行结果,提取日志和返回值
|
||||
|
||||
## 结果处理
|
||||
|
||||
执行结果通过JSON格式返回,包含三个字段:
|
||||
- `logs`: 执行过程中的日志数组
|
||||
- `result`: 执行结果(可能是字符串、数字、布尔值、对象等)
|
||||
- `error`: 执行错误(如果有)
|
||||
|
||||
```json
|
||||
{
|
||||
"logs": ["日志1", "日志2", ...],
|
||||
"result": "执行结果",
|
||||
"error": null
|
||||
}
|
||||
```
|
||||
|
||||
## 支持的返回类型
|
||||
|
||||
Python脚本可以返回多种类型的值:
|
||||
1. 字符串
|
||||
2. 数字
|
||||
3. 布尔值
|
||||
4. 列表/数组
|
||||
5. 字典/对象
|
||||
6. None/null
|
||||
|
||||
所有复杂类型(列表、字典)都会被序列化为JSON字符串。
|
||||
43
qiming-run_code_rmcp/.devcontainer/devcontainer.json
Normal file
43
qiming-run_code_rmcp/.devcontainer/devcontainer.json
Normal file
@@ -0,0 +1,43 @@
|
||||
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
|
||||
// README at: https://github.com/devcontainers/templates/tree/main/src/rust
|
||||
{
|
||||
"name": "Rust",
|
||||
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
|
||||
// "image": "mcr.microsoft.com/devcontainers/rust:1-1-bullseye",
|
||||
// 使用你自定义的Dockerfile
|
||||
"build": {
|
||||
"dockerfile": "../Dockerfile",
|
||||
"context": ".."
|
||||
},
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/go:1": {},
|
||||
"ghcr.io/devcontainers-community/features/deno:1": {},
|
||||
"ghcr.io/va-h/devcontainers-features/uv:1": {},
|
||||
"ghcr.io/devcontainers-extra/features/curl-apt-get:1": {},
|
||||
"ghcr.io/devcontainers-extra/features/wget-apt-get:1": {}
|
||||
}
|
||||
|
||||
// Use 'mounts' to make the cargo cache persistent in a Docker Volume.
|
||||
// "mounts": [
|
||||
// {
|
||||
// "source": "devcontainer-cargo-cache-${devcontainerId}",
|
||||
// "target": "/usr/local/cargo",
|
||||
// "type": "volume"
|
||||
// }
|
||||
// ]
|
||||
|
||||
// Features to add to the dev container. More info: https://containers.dev/features.
|
||||
// "features": {},
|
||||
|
||||
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
||||
// "forwardPorts": [],
|
||||
|
||||
// Use 'postCreateCommand' to run commands after the container is created.
|
||||
// "postCreateCommand": "rustc --version",
|
||||
|
||||
// Configure tool-specific properties.
|
||||
// "customizations": {},
|
||||
|
||||
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
|
||||
// "remoteUser": "root"
|
||||
}
|
||||
6
qiming-run_code_rmcp/.gitignore
vendored
Normal file
6
qiming-run_code_rmcp/.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
/target
|
||||
.idea
|
||||
.vscode
|
||||
.DS_Store
|
||||
|
||||
temp/
|
||||
108
qiming-run_code_rmcp/CLAUDE.md
Normal file
108
qiming-run_code_rmcp/CLAUDE.md
Normal file
@@ -0,0 +1,108 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
This is a Rust-based MCP (Model Context Protocol) server that executes JavaScript, TypeScript, and Python code in isolated environments. The project provides both a command-line tool and a library for code execution with proper sandboxing and result capture.
|
||||
|
||||
## Key Commands
|
||||
|
||||
### Building and Running
|
||||
```bash
|
||||
# Build the project
|
||||
cargo build
|
||||
|
||||
# Run with cargo
|
||||
cargo run -- js -f fixtures/test_js.js
|
||||
cargo run -- ts -f fixtures/test_ts.ts
|
||||
cargo run -- python -f fixtures/test_python.py
|
||||
|
||||
# Install as binary
|
||||
cargo install --path . --bin script_runner
|
||||
|
||||
# Run tests
|
||||
cargo test
|
||||
|
||||
# Clear cache
|
||||
cargo run -- clear-cache all
|
||||
```
|
||||
|
||||
### Common Development Commands
|
||||
```bash
|
||||
# Execute JavaScript with logs
|
||||
cargo run -- --show-logs js -f fixtures/test_js.js
|
||||
|
||||
# Execute with parameters
|
||||
cargo run -- --show-logs python -f fixtures/test_python_params.py -p '{"a":10, "b":20}'
|
||||
|
||||
# Execute inline code
|
||||
cargo run -- js -c "function handler(input) { return 'Hello: ' + input.name; }" -p '{"name":"User"}'
|
||||
|
||||
# Use MCP integration
|
||||
cargo run -- --use-mcp python -f fixtures/test_python.py
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
### Core Components
|
||||
- **src/main.rs** - CLI entry point with command parsing using clap
|
||||
- **src/lib.rs** - Library interface exporting public APIs
|
||||
- **src/model/code_run_model.rs** - Core execution models and `CodeExecutor` struct
|
||||
- **src/cache/** - Code caching system using blake3 hashing
|
||||
- **src/mcp/** - MCP protocol server implementation
|
||||
|
||||
### Language Runners
|
||||
- **src/deno_runner/js_runner.rs** - JavaScript execution via Deno
|
||||
- **src/deno_runner/ts_runner.rs** - TypeScript execution via Deno
|
||||
- **src/python_runner/python_runner.rs** - Python execution via uv with dependency analysis
|
||||
- **src/python_runner/dependencies/** - Python dependency parsing and management
|
||||
|
||||
### Execution Flow
|
||||
1. Code is processed and cached using blake3 hashing
|
||||
2. Language-specific runner prepares the execution environment
|
||||
3. Code is wrapped with logging capture and handler function execution
|
||||
4. Results are captured and returned as `CodeScriptExecutionResult`
|
||||
|
||||
## Important Patterns
|
||||
|
||||
### Handler Function Convention
|
||||
Scripts must expose a `handler` function (JS/TS) or `handler`/`main` function (Python) that returns the final result:
|
||||
```javascript
|
||||
function handler(input) {
|
||||
return "Result: " + input.param;
|
||||
}
|
||||
```
|
||||
|
||||
### Parameter Passing
|
||||
- JS/TS: Parameters passed directly to handler function
|
||||
- Python: Parameters passed via `INPUT_JSON` environment variable
|
||||
|
||||
### Error Handling
|
||||
- Uses `anyhow::Result` for error propagation
|
||||
- Custom `app_error` module for application-specific errors
|
||||
- Execution results include error information in `CodeScriptExecutionResult`
|
||||
|
||||
### Dependencies
|
||||
- **Deno** for JavaScript/TypeScript execution
|
||||
- **uv** for Python environment isolation
|
||||
- **rmcp** crate for MCP protocol implementation
|
||||
- **tokio** for async runtime
|
||||
- **serde** for JSON serialization
|
||||
|
||||
## Development Notes
|
||||
|
||||
### Testing
|
||||
- Test fixtures in `fixtures/` directory for all supported languages
|
||||
- Integration tests in `src/tests/`
|
||||
- Use `--show-logs` flag during development to see execution output
|
||||
|
||||
### Caching
|
||||
- Code processing results are cached based on content hash
|
||||
- Use `--clear-cache` to invalidate cache when debugging
|
||||
- Cache files stored in system temp directory
|
||||
|
||||
### MCP Integration
|
||||
- Can run as standalone MCP server via `script_runner` binary
|
||||
- Supports SSE and HTTP transports
|
||||
- Tools: `run_javascript`, `run_typescript`, `run_python`
|
||||
1429
qiming-run_code_rmcp/Cargo.lock
generated
Normal file
1429
qiming-run_code_rmcp/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
56
qiming-run_code_rmcp/Cargo.toml
Normal file
56
qiming-run_code_rmcp/Cargo.toml
Normal file
@@ -0,0 +1,56 @@
|
||||
[package]
|
||||
name = "run_code_rmcp"
|
||||
description = "云函数服务,执行JS/TS/Python语言代码,脚本必须有约定的函数名称(handler/main),会调用约定的函数名称结果和日志返回."
|
||||
version = "0.0.35"
|
||||
edition = "2024"
|
||||
license = "Apache-2.0"
|
||||
repository = "https://github.com/nuwax-ai/run_code_rmcp"
|
||||
homepage = "https://nuwax.com/"
|
||||
readme = "README.md"
|
||||
default-run = "run_code_rmcp"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
mcp = ["dep:rmcp"]
|
||||
|
||||
[dependencies]
|
||||
tokio = { version = "1.47", features = [
|
||||
"fs",
|
||||
"macros",
|
||||
"net",
|
||||
"rt",
|
||||
"rt-multi-thread",
|
||||
"signal",
|
||||
"io-util",
|
||||
"process",
|
||||
"time",
|
||||
] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
log = "0.4"
|
||||
env_logger = "0.11"
|
||||
anyhow = "1.0"
|
||||
rmcp = { version = "0.12", features = [
|
||||
"server",
|
||||
"client",
|
||||
"transport-io",
|
||||
"transport-async-rw",
|
||||
"macros",
|
||||
], optional = true }
|
||||
tempfile = "3.21"
|
||||
regex = "1.11"
|
||||
clap = { version = "4.5", features = ["derive"] }
|
||||
thiserror = "2.0"
|
||||
pest = { version = "2.8", features = ["pretty-print"] }
|
||||
pest_derive = "2.8"
|
||||
blake3 = "1.8"
|
||||
pin-project = "1.1"
|
||||
libc = "0.2"
|
||||
once_cell = "1.21"
|
||||
schemars = "1.0"
|
||||
async-trait = "0.1"
|
||||
|
||||
[[bin]]
|
||||
name = "script_runner"
|
||||
path = "src/script_runner.rs"
|
||||
required-features = ["mcp"]
|
||||
80
qiming-run_code_rmcp/Dockerfile
Normal file
80
qiming-run_code_rmcp/Dockerfile
Normal file
@@ -0,0 +1,80 @@
|
||||
FROM rust:1.85
|
||||
|
||||
# 设置环境变量
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
ENV TZ=Asia/Shanghai
|
||||
ENV PATH="/root/.deno/bin:/root/.cargo/bin:${PATH}"
|
||||
|
||||
# 安装基础依赖
|
||||
RUN apt-get update && apt-get install -y \
|
||||
python3.11 \
|
||||
python3.11-venv \
|
||||
python3.11-distutils \
|
||||
python3.11-dev \
|
||||
python3-pip \
|
||||
curl \
|
||||
vim \
|
||||
net-tools \
|
||||
gettext-base \
|
||||
telnet \
|
||||
wget \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& ln -snf /usr/share/zoneinfo/$TZ /etc/localtime \
|
||||
&& echo $TZ > /etc/timezone
|
||||
|
||||
# 安装 Node.js
|
||||
RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \
|
||||
&& apt-get install -y nodejs \
|
||||
&& npm install -g npm@latest
|
||||
|
||||
# 安装 go
|
||||
# 设置 Go 环境变量
|
||||
ENV GOPATH=/root/go
|
||||
ENV PATH=$PATH:/usr/local/go/bin:/root/go/bin
|
||||
|
||||
# 创建 Go 工作目录
|
||||
RUN mkdir -p $GOPATH/bin $GOPATH/src $GOPATH/pkg
|
||||
# 安装 Go 1.24(根据架构自动选择下载链接)
|
||||
ARG TARGETARCH
|
||||
RUN echo "Target Architecture: $TARGETARCH" \
|
||||
&& if [ "$TARGETARCH" = "amd64" ]; then \
|
||||
GO_URL="https://go.dev/dl/go1.24.3.linux-amd64.tar.gz"; \
|
||||
elif [ "$TARGETARCH" = "arm64" ]; then \
|
||||
GO_URL="https://go.dev/dl/go1.24.3.linux-arm64.tar.gz"; \
|
||||
else \
|
||||
echo "Unsupported architecture: $TARGETARCH"; \
|
||||
exit 1; \
|
||||
fi \
|
||||
&& echo "Downloading Go from: $GO_URL" \
|
||||
&& curl -fsSL "$GO_URL" -o go.tar.gz \
|
||||
&& tar -C /usr/local -xzf go.tar.gz \
|
||||
&& rm go.tar.gz \
|
||||
&& ln -s /usr/local/go/bin/go /usr/local/bin/go \
|
||||
&& ln -s /usr/local/go/bin/gofmt /usr/local/bin/gofmt
|
||||
|
||||
|
||||
RUN go version
|
||||
|
||||
# 测试mcp用 go-mcp-mysql@latest
|
||||
RUN go install -v github.com/Zhwt/go-mcp-mysql@latest
|
||||
|
||||
|
||||
# 安装 Deno
|
||||
RUN curl -fsSL https://deno.land/install.sh | sh
|
||||
|
||||
|
||||
# 安装 uv
|
||||
RUN curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||
|
||||
# 添加uv到PATH
|
||||
ENV PATH="/root/.local/bin:${PATH}"
|
||||
|
||||
# 创建虚拟环境
|
||||
RUN uv venv
|
||||
|
||||
# 设置工作目录
|
||||
WORKDIR /app
|
||||
|
||||
# 暴露端口,实际端口一般是8080
|
||||
EXPOSE 8080
|
||||
|
||||
202
qiming-run_code_rmcp/LICENSE
Normal file
202
qiming-run_code_rmcp/LICENSE
Normal file
@@ -0,0 +1,202 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but not
|
||||
limited to compiled object code, generated documentation, and
|
||||
conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2024-2025 Nuwax AI and contributors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
202
qiming-run_code_rmcp/LICENSE-APACHE
Normal file
202
qiming-run_code_rmcp/LICENSE-APACHE
Normal file
@@ -0,0 +1,202 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but not
|
||||
limited to compiled object code, generated documentation, and
|
||||
conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2024-2025 Nuwax AI and contributors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
16
qiming-run_code_rmcp/NOTICE
Normal file
16
qiming-run_code_rmcp/NOTICE
Normal file
@@ -0,0 +1,16 @@
|
||||
Copyright 2024-2025 Nuwax AI
|
||||
|
||||
This product includes software developed by Nuwax AI and contributors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
335
qiming-run_code_rmcp/README.md
Normal file
335
qiming-run_code_rmcp/README.md
Normal file
@@ -0,0 +1,335 @@
|
||||
# MCP Code Execution Tool
|
||||
|
||||
A Rust-based tool that provides both a command-line interface and an MCP (Model Context Protocol) server for executing JavaScript, TypeScript, and Python code in isolated environments.
|
||||
|
||||
**Repository:** <https://github.com/nuwax-ai/run_code_rmcp>
|
||||
|
||||
English | [中文](./README_zh.md)
|
||||
|
||||
## Features
|
||||
|
||||
- Execute JavaScript, TypeScript, and Python code in isolated environments
|
||||
- **JavaScript/TypeScript**: Powered by Deno runtime
|
||||
- **Python**: Powered by `uv` with automatic dependency management
|
||||
- Automatic code caching using blake3 hashing for faster repeated executions
|
||||
- Support for ESM and CommonJS modules in JavaScript
|
||||
- Automatic Python dependency parsing and installation
|
||||
- Separate log capture and execution result handling
|
||||
- Available as both CLI tool (`run_code_rmcp`) and MCP server (`script_runner`)
|
||||
|
||||
## Installation
|
||||
|
||||
### Install CLI Tool
|
||||
|
||||
```bash
|
||||
# Clone the repository
|
||||
git clone https://github.com/nuwax-ai/run_code_rmcp.git
|
||||
cd run_code_rmcp
|
||||
|
||||
# Install the CLI tool (run_code_rmcp)
|
||||
cargo install --path .
|
||||
|
||||
# Or install both CLI tool and MCP server
|
||||
cargo install --path . --features mcp
|
||||
cargo install --path . --bin script_runner --features mcp
|
||||
```
|
||||
|
||||
### System Requirements
|
||||
|
||||
- **Rust**: 1.85 or higher (for building)
|
||||
- **Deno**: Required for JavaScript/TypeScript execution
|
||||
- **uv**: Required for Python execution and dependency management
|
||||
|
||||
Install Deno:
|
||||
```bash
|
||||
curl -fsSL https://deno.land/install.sh | sh
|
||||
```
|
||||
|
||||
Install uv:
|
||||
```bash
|
||||
curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### CLI Tool: run_code_rmcp
|
||||
|
||||
The `run_code_rmcp` command allows you to execute code directly from the command line.
|
||||
|
||||
#### Basic Syntax
|
||||
|
||||
```bash
|
||||
run_code_rmcp [OPTIONS] <COMMAND>
|
||||
```
|
||||
|
||||
**Options:**
|
||||
- `--show-logs`: Display execution logs
|
||||
- `--clear-cache`: Clear cache before execution
|
||||
- `--use-mcp`: Use MCP SDK integration (requires mcp feature)
|
||||
|
||||
**Commands:**
|
||||
- `js`: Execute JavaScript code
|
||||
- `ts`: Execute TypeScript code
|
||||
- `python`: Execute Python code
|
||||
- `clear-cache`: Clear cached files
|
||||
|
||||
#### Execute JavaScript/TypeScript
|
||||
|
||||
```bash
|
||||
# Execute a JavaScript file
|
||||
run_code_rmcp --show-logs js -f fixtures/test_js.js
|
||||
|
||||
# Execute with parameters
|
||||
run_code_rmcp --show-logs js -f fixtures/test_js_params.js -p '{"name":"User"}'
|
||||
|
||||
# Execute TypeScript
|
||||
run_code_rmcp --show-logs ts -f fixtures/test_ts.ts
|
||||
|
||||
# Execute inline code
|
||||
run_code_rmcp js -c "function handler() { return 'Hello!'; }"
|
||||
```
|
||||
|
||||
#### Execute Python
|
||||
|
||||
```bash
|
||||
# Execute a Python file
|
||||
run_code_rmcp --show-logs python -f fixtures/test_python.py
|
||||
|
||||
# Execute with parameters
|
||||
run_code_rmcp --show-logs python -f fixtures/test_python_params.py -p '{"a":10, "b":20}'
|
||||
|
||||
# Execute inline code
|
||||
run_code_rmcp python -c "def handler(): return 'Hello from Python!'"
|
||||
```
|
||||
|
||||
#### Cache Management
|
||||
|
||||
```bash
|
||||
# Clear all cache
|
||||
run_code_rmcp clear-cache --language all
|
||||
|
||||
# Clear specific language cache
|
||||
run_code_rmcp clear-cache --language python
|
||||
run_code_rmcp clear-cache --language js
|
||||
run_code_rmcp clear-cache --language ts
|
||||
|
||||
# Clear cache before execution
|
||||
run_code_rmcp --clear-cache js -f fixtures/test_js.js
|
||||
```
|
||||
|
||||
### Using as a Rust Library
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
run_code_rmcp = { git = "https://github.com/nuwax-ai/run_code_rmcp.git" }
|
||||
```
|
||||
|
||||
```rust
|
||||
use run_code_rmcp::{CodeExecutor, LanguageScript};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Execute JavaScript
|
||||
let js_result = CodeExecutor::execute_with_params_compat(
|
||||
"function handler() { return {success: true}; }",
|
||||
LanguageScript::Js,
|
||||
None
|
||||
).await?;
|
||||
|
||||
// Execute Python
|
||||
let py_result = CodeExecutor::execute_with_params_compat(
|
||||
"def handler(): return {'success': True}",
|
||||
LanguageScript::Python,
|
||||
None
|
||||
).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### MCP Server: script_runner
|
||||
|
||||
The `script_runner` is an MCP server that provides tools for AI assistants.
|
||||
|
||||
#### Starting the Server
|
||||
|
||||
```bash
|
||||
# Start MCP server
|
||||
script_runner
|
||||
|
||||
# Start with verbose logging
|
||||
script_runner --verbose
|
||||
```
|
||||
|
||||
The server communicates via stdio and waits for MCP protocol JSON requests.
|
||||
|
||||
#### Available Tools
|
||||
|
||||
The MCP server provides the following tools:
|
||||
|
||||
1. **run_javascript** - Execute JavaScript code
|
||||
2. **run_typescript** - Execute TypeScript code
|
||||
3. **run_python** - Execute Python code
|
||||
|
||||
Each tool accepts:
|
||||
- `code` (string): The code to execute
|
||||
- `params` (object, optional): Parameters to pass to the handler function
|
||||
|
||||
#### Integration with AI Assistants
|
||||
|
||||
Add to your MCP client configuration (e.g., Claude Desktop):
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "script-runner",
|
||||
"command": "script_runner",
|
||||
"transport": "stdio"
|
||||
}
|
||||
```
|
||||
|
||||
## Code Structure
|
||||
|
||||
### Handler Functions
|
||||
|
||||
Your scripts should define a handler function that returns the result.
|
||||
|
||||
#### JavaScript/TypeScript
|
||||
|
||||
Supports both `main` and `handler` functions (main takes priority):
|
||||
|
||||
```javascript
|
||||
// This function takes priority
|
||||
function main(input) {
|
||||
console.log("Processing with main:", input);
|
||||
return { result: "Hello from main!" };
|
||||
}
|
||||
|
||||
// This function is used if main is not defined
|
||||
function handler(input) {
|
||||
console.log("Processing with handler:", input);
|
||||
return { result: "Hello from handler!" };
|
||||
}
|
||||
```
|
||||
|
||||
ESM modules are also supported:
|
||||
|
||||
```javascript
|
||||
import { serve } from "https://deno.land/std/http/server.ts";
|
||||
|
||||
function handler(input) {
|
||||
return { message: "ESM works!" };
|
||||
}
|
||||
```
|
||||
|
||||
#### Python
|
||||
|
||||
Supports both `handler` and `main` functions (handler takes priority):
|
||||
|
||||
```python
|
||||
import pandas as pd
|
||||
|
||||
# This function takes priority
|
||||
def handler(args):
|
||||
print(f"Processing: {args}")
|
||||
data = pd.DataFrame({"a": [1, 2, 3]})
|
||||
return {"result": data.to_dict()}
|
||||
|
||||
# This function is used if handler is not defined
|
||||
def main(args):
|
||||
return {"result": "Using main instead"}
|
||||
```
|
||||
|
||||
### Parameter Passing
|
||||
|
||||
Parameters are passed to your handler function:
|
||||
|
||||
```bash
|
||||
run_code_rmcp js -f script.js -p '{"name": "User", "count": 42}'
|
||||
```
|
||||
|
||||
In your code:
|
||||
```javascript
|
||||
function handler(input) {
|
||||
// input = { name: "User", count: 42 }
|
||||
return `Hello ${input.name}, count: ${input.count}`;
|
||||
}
|
||||
```
|
||||
|
||||
### Python Dependencies
|
||||
|
||||
Python dependencies are automatically detected and installed:
|
||||
|
||||
```python
|
||||
import requests
|
||||
import pandas as pd
|
||||
|
||||
def handler(args):
|
||||
# Dependencies are automatically installed via uv
|
||||
response = requests.get("https://api.example.com")
|
||||
data = pd.DataFrame(response.json())
|
||||
return data.to_dict()
|
||||
```
|
||||
|
||||
### Log Capture
|
||||
|
||||
All console output is captured separately from the return value:
|
||||
|
||||
```javascript
|
||||
console.log("This goes to logs");
|
||||
console.log("So does this");
|
||||
|
||||
function handler() {
|
||||
return "This is the result";
|
||||
}
|
||||
```
|
||||
|
||||
Result:
|
||||
```json
|
||||
{
|
||||
"logs": ["This goes to logs", "So does this"],
|
||||
"result": "This is the result",
|
||||
"error": null
|
||||
}
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
# Build
|
||||
cargo build
|
||||
|
||||
# Run with cargo
|
||||
cargo run -- --show-logs js -f fixtures/test_js.js
|
||||
|
||||
# Run tests
|
||||
cargo test
|
||||
|
||||
# Build with MCP feature
|
||||
cargo build --features mcp
|
||||
|
||||
# Run MCP server with cargo
|
||||
cargo run --bin script_runner --features mcp
|
||||
```
|
||||
|
||||
## Environment Pre-warming
|
||||
|
||||
The project includes a warm-up function to cache common dependencies:
|
||||
|
||||
```rust
|
||||
use run_code_rmcp::warm_up_all_envs;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
// Pre-install common Python and JS/TS dependencies
|
||||
warm_up_all_envs(None, None, None, None).await?;
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
## TODO
|
||||
|
||||
- [ ] TTS (Text-to-Speech) functionality - Currently has known issues
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the Apache License 2.0. See `LICENSE` and `NOTICE` files in the root directory for details.
|
||||
335
qiming-run_code_rmcp/README_zh.md
Normal file
335
qiming-run_code_rmcp/README_zh.md
Normal file
@@ -0,0 +1,335 @@
|
||||
# MCP 代码执行工具
|
||||
|
||||
一个基于 Rust 的工具,提供命令行界面和 MCP(Model Context Protocol)服务器,用于在隔离环境中执行 JavaScript、TypeScript 和 Python 代码。
|
||||
|
||||
**项目仓库:** <https://github.com/nuwax-ai/run_code_rmcp>
|
||||
|
||||
[English](./README.md) | 中文
|
||||
|
||||
## 功能特点
|
||||
|
||||
- 在隔离环境中执行 JavaScript、TypeScript 和 Python 代码
|
||||
- **JavaScript/TypeScript**:由 Deno 运行时驱动
|
||||
- **Python**:由 `uv` 驱动,支持自动依赖管理
|
||||
- 使用 blake3 哈希自动缓存代码,加快重复执行速度
|
||||
- 支持 JavaScript 的 ESM 和 CommonJS 模块
|
||||
- 自动解析和安装 Python 依赖
|
||||
- 独立的日志捕获和执行结果处理
|
||||
- 提供命令行工具(`run_code_rmcp`)和 MCP 服务器(`script_runner`)两种使用方式
|
||||
|
||||
## 安装方法
|
||||
|
||||
### 安装命令行工具
|
||||
|
||||
```bash
|
||||
# 克隆仓库
|
||||
git clone https://github.com/nuwax-ai/run_code_rmcp.git
|
||||
cd run_code_rmcp
|
||||
|
||||
# 安装命令行工具 (run_code_rmcp)
|
||||
cargo install --path .
|
||||
|
||||
# 或同时安装命令行工具和 MCP 服务器
|
||||
cargo install --path . --features mcp
|
||||
cargo install --path . --bin script_runner --features mcp
|
||||
```
|
||||
|
||||
### 系统要求
|
||||
|
||||
- **Rust**:1.85 或更高版本(用于构建)
|
||||
- **Deno**:JavaScript/TypeScript 执行必需
|
||||
- **uv**:Python 执行和依赖管理必需
|
||||
|
||||
安装 Deno:
|
||||
```bash
|
||||
curl -fsSL https://deno.land/install.sh | sh
|
||||
```
|
||||
|
||||
安装 uv:
|
||||
```bash
|
||||
curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||
```
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 命令行工具:run_code_rmcp
|
||||
|
||||
`run_code_rmcp` 命令允许直接从命令行执行代码。
|
||||
|
||||
#### 基本语法
|
||||
|
||||
```bash
|
||||
run_code_rmcp [选项] <命令>
|
||||
```
|
||||
|
||||
**选项:**
|
||||
- `--show-logs`:显示执行日志
|
||||
- `--clear-cache`:执行前清除缓存
|
||||
- `--use-mcp`:使用 MCP SDK 集成(需要 mcp feature)
|
||||
|
||||
**命令:**
|
||||
- `js`:执行 JavaScript 代码
|
||||
- `ts`:执行 TypeScript 代码
|
||||
- `python`:执行 Python 代码
|
||||
- `clear-cache`:清除缓存文件
|
||||
|
||||
#### 执行 JavaScript/TypeScript
|
||||
|
||||
```bash
|
||||
# 执行 JavaScript 文件
|
||||
run_code_rmcp --show-logs js -f fixtures/test_js.js
|
||||
|
||||
# 带参数执行
|
||||
run_code_rmcp --show-logs js -f fixtures/test_js_params.js -p '{"name":"User"}'
|
||||
|
||||
# 执行 TypeScript
|
||||
run_code_rmcp --show-logs ts -f fixtures/test_ts.ts
|
||||
|
||||
# 执行内联代码
|
||||
run_code_rmcp js -c "function handler() { return 'Hello!'; }"
|
||||
```
|
||||
|
||||
#### 执行 Python
|
||||
|
||||
```bash
|
||||
# 执行 Python 文件
|
||||
run_code_rmcp --show-logs python -f fixtures/test_python.py
|
||||
|
||||
# 带参数执行
|
||||
run_code_rmcp --show-logs python -f fixtures/test_python_params.py -p '{"a":10, "b":20}'
|
||||
|
||||
# 执行内联代码
|
||||
run_code_rmcp python -c "def handler(): return 'Hello from Python!'"
|
||||
```
|
||||
|
||||
#### 缓存管理
|
||||
|
||||
```bash
|
||||
# 清除所有缓存
|
||||
run_code_rmcp clear-cache --language all
|
||||
|
||||
# 清除特定语言的缓存
|
||||
run_code_rmcp clear-cache --language python
|
||||
run_code_rmcp clear-cache --language js
|
||||
run_code_rmcp clear-cache --language ts
|
||||
|
||||
# 执行前清除缓存
|
||||
run_code_rmcp --clear-cache js -f fixtures/test_js.js
|
||||
```
|
||||
|
||||
### 作为 Rust 库使用
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
run_code_rmcp = { git = "https://github.com/nuwax-ai/run_code_rmcp.git" }
|
||||
```
|
||||
|
||||
```rust
|
||||
use run_code_rmcp::{CodeExecutor, LanguageScript};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// 执行 JavaScript
|
||||
let js_result = CodeExecutor::execute_with_params_compat(
|
||||
"function handler() { return {success: true}; }",
|
||||
LanguageScript::Js,
|
||||
None
|
||||
).await?;
|
||||
|
||||
// 执行 Python
|
||||
let py_result = CodeExecutor::execute_with_params_compat(
|
||||
"def handler(): return {'success': True}",
|
||||
LanguageScript::Python,
|
||||
None
|
||||
).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### MCP 服务器:script_runner
|
||||
|
||||
`script_runner` 是一个 MCP 服务器,为 AI 助手提供代码执行工具。
|
||||
|
||||
#### 启动服务器
|
||||
|
||||
```bash
|
||||
# 启动 MCP 服务器
|
||||
script_runner
|
||||
|
||||
# 启用详细日志
|
||||
script_runner --verbose
|
||||
```
|
||||
|
||||
服务器通过 stdio 通信,等待 MCP 协议的 JSON 请求。
|
||||
|
||||
#### 可用工具
|
||||
|
||||
MCP 服务器提供以下工具:
|
||||
|
||||
1. **run_javascript** - 执行 JavaScript 代码
|
||||
2. **run_typescript** - 执行 TypeScript 代码
|
||||
3. **run_python** - 执行 Python 代码
|
||||
|
||||
每个工具接受:
|
||||
- `code`(字符串):要执行的代码
|
||||
- `params`(对象,可选):传递给 handler 函数的参数
|
||||
|
||||
#### 与 AI 助手集成
|
||||
|
||||
添加到你的 MCP 客户端配置(如 Claude Desktop):
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "script-runner",
|
||||
"command": "script_runner",
|
||||
"transport": "stdio"
|
||||
}
|
||||
```
|
||||
|
||||
## 代码结构
|
||||
|
||||
### Handler 函数
|
||||
|
||||
脚本应定义一个返回结果的 handler 函数。
|
||||
|
||||
#### JavaScript/TypeScript
|
||||
|
||||
支持 `main` 和 `handler` 函数(main 优先):
|
||||
|
||||
```javascript
|
||||
// 这个函数优先级更高
|
||||
function main(input) {
|
||||
console.log("使用 main 处理:", input);
|
||||
return { result: "Hello from main!" };
|
||||
}
|
||||
|
||||
// 如果没有定义 main,则使用这个函数
|
||||
function handler(input) {
|
||||
console.log("使用 handler 处理:", input);
|
||||
return { result: "Hello from handler!" };
|
||||
}
|
||||
```
|
||||
|
||||
也支持 ESM 模块:
|
||||
|
||||
```javascript
|
||||
import { serve } from "https://deno.land/std/http/server.ts";
|
||||
|
||||
function handler(input) {
|
||||
return { message: "ESM works!" };
|
||||
}
|
||||
```
|
||||
|
||||
#### Python
|
||||
|
||||
支持 `handler` 和 `main` 函数(handler 优先):
|
||||
|
||||
```python
|
||||
import pandas as pd
|
||||
|
||||
# 这个函数优先级更高
|
||||
def handler(args):
|
||||
print(f"处理中: {args}")
|
||||
data = pd.DataFrame({"a": [1, 2, 3]})
|
||||
return {"result": data.to_dict()}
|
||||
|
||||
# 如果没有定义 handler,则使用这个函数
|
||||
def main(args):
|
||||
return {"result": "使用 main 代替"}
|
||||
```
|
||||
|
||||
### 参数传递
|
||||
|
||||
参数会传递给 handler 函数:
|
||||
|
||||
```bash
|
||||
run_code_rmcp js -f script.js -p '{"name": "User", "count": 42}'
|
||||
```
|
||||
|
||||
在代码中:
|
||||
```javascript
|
||||
function handler(input) {
|
||||
// input = { name: "User", count: 42 }
|
||||
return `Hello ${input.name}, count: ${input.count}`;
|
||||
}
|
||||
```
|
||||
|
||||
### Python 依赖
|
||||
|
||||
Python 依赖会被自动检测并安装:
|
||||
|
||||
```python
|
||||
import requests
|
||||
import pandas as pd
|
||||
|
||||
def handler(args):
|
||||
# 依赖会通过 uv 自动安装
|
||||
response = requests.get("https://api.example.com")
|
||||
data = pd.DataFrame(response.json())
|
||||
return data.to_dict()
|
||||
```
|
||||
|
||||
### 日志捕获
|
||||
|
||||
所有控制台输出都会与返回值分开捕获:
|
||||
|
||||
```javascript
|
||||
console.log("这是日志");
|
||||
console.log("这也是日志");
|
||||
|
||||
function handler() {
|
||||
return "这是结果";
|
||||
}
|
||||
```
|
||||
|
||||
结果:
|
||||
```json
|
||||
{
|
||||
"logs": ["这是日志", "这也是日志"],
|
||||
"result": "这是结果",
|
||||
"error": null
|
||||
}
|
||||
```
|
||||
|
||||
## 开发
|
||||
|
||||
```bash
|
||||
# 构建
|
||||
cargo build
|
||||
|
||||
# 使用 cargo 运行
|
||||
cargo run -- --show-logs js -f fixtures/test_js.js
|
||||
|
||||
# 运行测试
|
||||
cargo test
|
||||
|
||||
# 使用 mcp feature 构建
|
||||
cargo build --features mcp
|
||||
|
||||
# 使用 cargo 运行 MCP 服务器
|
||||
cargo run --bin script_runner --features mcp
|
||||
```
|
||||
|
||||
## 环境预热
|
||||
|
||||
项目包含预热函数来缓存常用依赖:
|
||||
|
||||
```rust
|
||||
use run_code_rmcp::warm_up_all_envs;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
// 预安装常用的 Python 和 JS/TS 依赖
|
||||
warm_up_all_envs(None, None, None, None).await?;
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
## 待办事项
|
||||
|
||||
- [ ] TTS(文本转语音)功能 - 目前存在已知问题
|
||||
|
||||
## 许可证
|
||||
|
||||
本项目采用 Apache License 2.0 发布。详见根目录的 `LICENSE` 与 `NOTICE` 文件。
|
||||
6
qiming-run_code_rmcp/examples/cursor_mcp_config.json
Normal file
6
qiming-run_code_rmcp/examples/cursor_mcp_config.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "Script Runner",
|
||||
"description": "执行JavaScript、TypeScript和Python代码",
|
||||
"path": "/path/to/script_runner",
|
||||
"transport": "stdio"
|
||||
}
|
||||
151
qiming-run_code_rmcp/examples/mcp_client.js
Normal file
151
qiming-run_code_rmcp/examples/mcp_client.js
Normal file
@@ -0,0 +1,151 @@
|
||||
// MCP客户端示例,用于与script_runner交互
|
||||
// 使用方法:node mcp_client.js
|
||||
|
||||
const { spawn } = require('node:child_process');
|
||||
const readline = require('node:readline');
|
||||
|
||||
// 启动script_runner进程
|
||||
const scriptRunner = spawn('script_runner', ['--verbose']);
|
||||
|
||||
// 创建读取标准输入的接口
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout
|
||||
});
|
||||
|
||||
// 处理script_runner的输出
|
||||
scriptRunner.stdout.on('data', (data) => {
|
||||
console.log(`接收到MCP服务器响应: ${data}`);
|
||||
try {
|
||||
const response = JSON.parse(data);
|
||||
console.log('解析后的响应:', JSON.stringify(response, null, 2));
|
||||
} catch (e) {
|
||||
console.log('无法解析为JSON,显示原始输出');
|
||||
}
|
||||
});
|
||||
|
||||
// 处理script_runner的错误
|
||||
scriptRunner.stderr.on('data', (data) => {
|
||||
console.error(`MCP服务器错误: ${data}`);
|
||||
});
|
||||
|
||||
// 处理script_runner进程结束
|
||||
scriptRunner.on('close', (code) => {
|
||||
console.log(`MCP服务器进程退出,退出码: ${code}`);
|
||||
rl.close();
|
||||
});
|
||||
|
||||
// 初始化MCP连接
|
||||
const initializeRequest = {
|
||||
jsonrpc: '2.0',
|
||||
id: 1,
|
||||
method: 'initialize',
|
||||
params: {
|
||||
client_info: {
|
||||
name: 'mcp_client_example'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 发送初始化请求
|
||||
console.log('发送初始化请求...');
|
||||
scriptRunner.stdin.write(JSON.stringify(initializeRequest) + '\n');
|
||||
|
||||
// 等待初始化响应后,发送工具列表请求
|
||||
setTimeout(() => {
|
||||
const listToolsRequest = {
|
||||
jsonrpc: '2.0',
|
||||
id: 2,
|
||||
method: 'listTools'
|
||||
};
|
||||
console.log('发送工具列表请求...');
|
||||
scriptRunner.stdin.write(JSON.stringify(listToolsRequest) + '\n');
|
||||
}, 1000);
|
||||
|
||||
// 等待工具列表响应后,执行JavaScript代码
|
||||
setTimeout(() => {
|
||||
const callToolRequest = {
|
||||
jsonrpc: '2.0',
|
||||
id: 3,
|
||||
method: 'callTool',
|
||||
params: {
|
||||
name: 'run_javascript',
|
||||
arguments: {
|
||||
code: 'console.log("Hello from JavaScript!"); return { message: "Hello, World!", value: 42 };'
|
||||
}
|
||||
}
|
||||
};
|
||||
console.log('发送JavaScript代码执行请求...');
|
||||
scriptRunner.stdin.write(JSON.stringify(callToolRequest) + '\n');
|
||||
}, 2000);
|
||||
|
||||
// 等待JavaScript执行响应后,执行TypeScript代码
|
||||
setTimeout(() => {
|
||||
const callToolRequest = {
|
||||
jsonrpc: '2.0',
|
||||
id: 4,
|
||||
method: 'callTool',
|
||||
params: {
|
||||
name: 'run_typescript',
|
||||
arguments: {
|
||||
code: 'const greeting: string = "Hello from TypeScript!"; console.log(greeting); return { message: greeting, timestamp: new Date().toISOString() };'
|
||||
}
|
||||
}
|
||||
};
|
||||
console.log('发送TypeScript代码执行请求...');
|
||||
scriptRunner.stdin.write(JSON.stringify(callToolRequest) + '\n');
|
||||
}, 3000);
|
||||
|
||||
// 等待TypeScript执行响应后,执行Python代码
|
||||
setTimeout(() => {
|
||||
const callToolRequest = {
|
||||
jsonrpc: '2.0',
|
||||
id: 5,
|
||||
method: 'callTool',
|
||||
params: {
|
||||
name: 'run_python',
|
||||
arguments: {
|
||||
code: 'import math\nprint("Hello from Python!")\nresult = math.sqrt(16)\nprint(f"Square root of 16 is {result}")\n{"message": "Hello from Python", "result": result}'
|
||||
}
|
||||
}
|
||||
};
|
||||
console.log('发送Python代码执行请求...');
|
||||
scriptRunner.stdin.write(JSON.stringify(callToolRequest) + '\n');
|
||||
}, 4000);
|
||||
|
||||
// 处理用户输入,允许用户发送自定义请求
|
||||
rl.on('line', (input) => {
|
||||
if (input.toLowerCase() === 'exit') {
|
||||
console.log('退出程序...');
|
||||
scriptRunner.kill();
|
||||
rl.close();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 尝试解析用户输入为JSON
|
||||
const request = JSON.parse(input);
|
||||
console.log('发送自定义请求:', JSON.stringify(request, null, 2));
|
||||
scriptRunner.stdin.write(JSON.stringify(request) + '\n');
|
||||
} catch (e) {
|
||||
console.error('输入不是有效的JSON:', e.message);
|
||||
}
|
||||
});
|
||||
|
||||
// 显示帮助信息
|
||||
console.log('\n=== MCP客户端示例 ===');
|
||||
console.log('已自动发送初始化、工具列表和代码执行请求');
|
||||
console.log('您可以输入自定义JSON请求,或输入"exit"退出程序');
|
||||
console.log('示例请求:');
|
||||
console.log(JSON.stringify({
|
||||
jsonrpc: '2.0',
|
||||
id: 100,
|
||||
method: 'callTool',
|
||||
params: {
|
||||
name: 'run_javascript',
|
||||
arguments: {
|
||||
code: 'return "Hello, " + new Date().toISOString();'
|
||||
}
|
||||
}
|
||||
}, null, 2));
|
||||
console.log('====================\n');
|
||||
78
qiming-run_code_rmcp/examples/mcp_client_example.js
Normal file
78
qiming-run_code_rmcp/examples/mcp_client_example.js
Normal file
@@ -0,0 +1,78 @@
|
||||
// 这是一个使用JavaScript调用MCP服务的示例
|
||||
// 你可以使用任何支持MCP协议的客户端来调用我们的服务
|
||||
// 以下是一个简单的示例,展示如何调用我们的服务
|
||||
|
||||
// 假设我们已经连接到了MCP服务
|
||||
|
||||
// 执行JavaScript代码示例
|
||||
async function testRunJavaScript() {
|
||||
const code = `
|
||||
// 一些处理代码
|
||||
console.log("Processing JavaScript...");
|
||||
|
||||
function handler(input) {
|
||||
// 输入包含通过参数传递的数据
|
||||
console.log("Received input:", input);
|
||||
|
||||
// 返回最终结果
|
||||
return "Hello from JavaScript! Input: " + JSON.stringify(input);
|
||||
}
|
||||
`;
|
||||
|
||||
const params = { name: "JavaScript User", data: [1, 2, 3] };
|
||||
|
||||
// 在实际的MCP客户端中,你会使用类似以下的代码调用服务
|
||||
// const result = await client.run_javascript({ code, params });
|
||||
// console.log(result);
|
||||
}
|
||||
|
||||
// 执行TypeScript代码示例
|
||||
async function testRunTypeScript() {
|
||||
const code = `
|
||||
// 一些处理代码
|
||||
console.log("Processing TypeScript...");
|
||||
|
||||
function handler(input: any): string {
|
||||
// 输入包含通过参数传递的数据
|
||||
console.log("Received input:", input);
|
||||
|
||||
// 返回最终结果
|
||||
return \`Hello from TypeScript! Input: \${JSON.stringify(input)}\`;
|
||||
}
|
||||
`;
|
||||
|
||||
const params = { name: "TypeScript User", count: 42 };
|
||||
|
||||
// 在实际的MCP客户端中,你会使用类似以下的代码调用服务
|
||||
// const result = await client.run_typescript({ code, params });
|
||||
// console.log(result);
|
||||
}
|
||||
|
||||
// 执行Python代码示例
|
||||
async function testRunPython() {
|
||||
const code = `
|
||||
# 一些处理代码
|
||||
print("Processing Python...")
|
||||
|
||||
def handler(args):
|
||||
# args 包含通过参数传递的数据
|
||||
print(f"Received args: {args}")
|
||||
|
||||
# 返回最终结果
|
||||
return f"Hello from Python! Args: {args}"
|
||||
`;
|
||||
|
||||
const params = { name: "Python User", values: [10, 20, 30] };
|
||||
|
||||
// 在实际的MCP客户端中,你会使用类似以下的代码调用服务
|
||||
// const result = await client.run_python({ code, params });
|
||||
// console.log(result);
|
||||
}
|
||||
|
||||
// 在实际的MCP客户端中,你可以按顺序调用这些函数
|
||||
// await testRunJavaScript();
|
||||
// await testRunTypeScript();
|
||||
// await testRunPython();
|
||||
|
||||
// 注意:这个文件只是一个示例,不能直接运行
|
||||
// 你需要使用支持MCP协议的客户端(如Cursor)来调用我们的服务
|
||||
21
qiming-run_code_rmcp/examples/test_mcp.sh
Normal file
21
qiming-run_code_rmcp/examples/test_mcp.sh
Normal file
@@ -0,0 +1,21 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 测试script_runner的简单bash脚本
|
||||
|
||||
# 初始化请求
|
||||
echo "发送初始化请求..."
|
||||
echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"client_info":{"name":"test_client"}}}' | script_runner
|
||||
|
||||
# 等待一下
|
||||
sleep 1
|
||||
|
||||
# 列出工具请求
|
||||
echo -e "\n发送列出工具请求..."
|
||||
echo '{"jsonrpc":"2.0","id":2,"method":"listTools"}' | script_runner
|
||||
|
||||
# 等待一下
|
||||
sleep 1
|
||||
|
||||
# 执行JavaScript代码
|
||||
echo -e "\n发送JavaScript代码执行请求..."
|
||||
echo '{"jsonrpc":"2.0","id":3,"method":"callTool","params":{"name":"run_javascript","arguments":{"code":"console.log(\"Hello from JavaScript!\"); return { message: \"Hello, World!\", value: 42 };"}}}' | script_runner
|
||||
24
qiming-run_code_rmcp/fixtures/cow_say_hello.js
Normal file
24
qiming-run_code_rmcp/fixtures/cow_say_hello.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import * as o from 'https://deno.land/x/cowsay/mod.ts'
|
||||
|
||||
// 使用类型注解时应确保使用的是支持该特性的环境或编译器(如 TypeScript)
|
||||
function add(input) {
|
||||
console.log("test Add", input.a, "+", input.b);
|
||||
|
||||
let m = o.say({
|
||||
text: 'hello every one'
|
||||
})
|
||||
console.log(m)
|
||||
|
||||
return input.a + input.b;
|
||||
}
|
||||
|
||||
|
||||
export default async function handler(input) {
|
||||
|
||||
let a = add(input)
|
||||
console.log("计算结果=" + a)
|
||||
|
||||
return {
|
||||
message: a,
|
||||
};
|
||||
}
|
||||
30
qiming-run_code_rmcp/fixtures/import_axios_example.js
Normal file
30
qiming-run_code_rmcp/fixtures/import_axios_example.js
Normal file
@@ -0,0 +1,30 @@
|
||||
import axios from 'npm:axios';
|
||||
|
||||
// 使用axios发起HTTP请求的函数
|
||||
async function fetchData(url) {
|
||||
console.log(`正在请求: ${url}`);
|
||||
try {
|
||||
const response = await axios.get(url);
|
||||
console.log(`请求成功,状态码: ${response.status}`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error(`请求失败: ${error.message}`);
|
||||
return { error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
export default async function handler(input) {
|
||||
// 默认URL或从输入参数获取
|
||||
const url = input.url || 'https://jsonplaceholder.typicode.com/todos/1';
|
||||
|
||||
console.log(`开始处理请求,URL: ${url}`);
|
||||
|
||||
// 发起请求并获取数据
|
||||
const data = await fetchData(url);
|
||||
|
||||
return {
|
||||
message: '请求完成',
|
||||
data: data,
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
}
|
||||
79
qiming-run_code_rmcp/fixtures/import_deno_std_example.js
Normal file
79
qiming-run_code_rmcp/fixtures/import_deno_std_example.js
Normal file
@@ -0,0 +1,79 @@
|
||||
import { join, basename } from "https://deno.land/std/path/mod.ts";
|
||||
import { readLines } from "https://deno.land/std/io/mod.ts";
|
||||
import { format } from "https://deno.land/std/datetime/mod.ts";
|
||||
|
||||
// 使用Deno标准库处理路径和文件
|
||||
async function processPath(path) {
|
||||
console.log(`处理路径: ${path}`);
|
||||
|
||||
// 使用path模块处理路径
|
||||
const fileName = basename(path);
|
||||
console.log(`文件名: ${fileName}`);
|
||||
|
||||
const fullPath = join("/tmp", fileName);
|
||||
console.log(`完整路径: ${fullPath}`);
|
||||
|
||||
// 获取当前时间并格式化
|
||||
const now = new Date();
|
||||
const formattedDate = format(now, "yyyy-MM-dd HH:mm:ss");
|
||||
console.log(`当前时间: ${formattedDate}`);
|
||||
|
||||
return {
|
||||
originalPath: path,
|
||||
fileName: fileName,
|
||||
fullPath: fullPath,
|
||||
timestamp: formattedDate
|
||||
};
|
||||
}
|
||||
|
||||
// 模拟读取文件内容
|
||||
async function simulateFileReading() {
|
||||
const text = `这是第一行
|
||||
这是第二行
|
||||
这是第三行
|
||||
这是最后一行`;
|
||||
|
||||
// 使用TextEncoder和StringReader代替Deno.Buffer
|
||||
const encoder = new TextEncoder();
|
||||
const decoder = new TextDecoder();
|
||||
const data = encoder.encode(text);
|
||||
|
||||
// 创建一个可读流
|
||||
const stream = new ReadableStream({
|
||||
start(controller) {
|
||||
controller.enqueue(data);
|
||||
controller.close();
|
||||
}
|
||||
});
|
||||
|
||||
// 将流转换为Reader
|
||||
const reader = stream.getReader();
|
||||
|
||||
console.log("开始读取文件内容:");
|
||||
|
||||
// 手动处理文本行
|
||||
const lines = text.split('\n');
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
console.log(`第${i+1}行: ${lines[i]}`);
|
||||
}
|
||||
|
||||
return lines;
|
||||
}
|
||||
|
||||
export default async function handler(input) {
|
||||
console.log("开始处理");
|
||||
|
||||
// 处理路径
|
||||
const path = input.path || "example.txt";
|
||||
const pathInfo = await processPath(path);
|
||||
|
||||
// 模拟读取文件
|
||||
const fileLines = await simulateFileReading();
|
||||
|
||||
return {
|
||||
message: '处理完成',
|
||||
pathInfo: pathInfo,
|
||||
fileContent: fileLines,
|
||||
linesCount: fileLines.length
|
||||
};
|
||||
}
|
||||
59
qiming-run_code_rmcp/fixtures/import_esm_module_example.js
Normal file
59
qiming-run_code_rmcp/fixtures/import_esm_module_example.js
Normal file
@@ -0,0 +1,59 @@
|
||||
// 使用ES模块导入方式
|
||||
import { createHash } from "node:crypto";
|
||||
import { Buffer } from "node:buffer";
|
||||
|
||||
// 使用Node.js内置模块处理数据
|
||||
function hashData(data, algorithm = 'sha256') {
|
||||
console.log(`使用 ${algorithm} 算法处理数据`);
|
||||
|
||||
// 将数据转换为Buffer
|
||||
const buffer = typeof data === 'string'
|
||||
? Buffer.from(data)
|
||||
: Buffer.from(JSON.stringify(data));
|
||||
|
||||
// 创建哈希
|
||||
const hash = createHash(algorithm);
|
||||
hash.update(buffer);
|
||||
|
||||
// 获取哈希结果
|
||||
const digest = hash.digest('hex');
|
||||
console.log(`哈希结果: ${digest}`);
|
||||
|
||||
return digest;
|
||||
}
|
||||
|
||||
// 加密数据
|
||||
function encryptData(data, salt = 'default-salt') {
|
||||
console.log(`加密数据,使用盐值: ${salt}`);
|
||||
|
||||
// 组合数据和盐值
|
||||
const combined = `${data}:${salt}:${Date.now()}`;
|
||||
|
||||
// 生成多种哈希
|
||||
const sha256Hash = hashData(combined, 'sha256');
|
||||
const md5Hash = hashData(combined, 'md5');
|
||||
|
||||
return {
|
||||
original: data,
|
||||
salt,
|
||||
sha256: sha256Hash,
|
||||
md5: md5Hash,
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
}
|
||||
|
||||
export default async function handler(input) {
|
||||
console.log("开始处理加密任务");
|
||||
|
||||
// 获取输入数据
|
||||
const data = input.data || "这是需要加密的默认数据";
|
||||
const salt = input.salt || "custom-salt-" + Math.floor(Math.random() * 1000);
|
||||
|
||||
// 加密数据
|
||||
const encryptedResult = encryptData(data, salt);
|
||||
|
||||
return {
|
||||
message: '加密处理完成',
|
||||
result: encryptedResult
|
||||
};
|
||||
}
|
||||
79
qiming-run_code_rmcp/fixtures/import_jsr_example.js
Normal file
79
qiming-run_code_rmcp/fixtures/import_jsr_example.js
Normal file
@@ -0,0 +1,79 @@
|
||||
import { assertEquals } from "jsr:@std/assert";
|
||||
import { delay } from "jsr:@std/async";
|
||||
import { bold, red, green } from "jsr:@std/fmt/colors";
|
||||
|
||||
// 使用JSR标准库中的工具函数
|
||||
async function runTests(input) {
|
||||
console.log(bold("开始运行测试..."));
|
||||
|
||||
// 使用delay函数模拟异步操作
|
||||
console.log("等待1秒...");
|
||||
await delay(1000);
|
||||
|
||||
// 测试结果数组
|
||||
const results = [];
|
||||
|
||||
// 测试1: 加法运算
|
||||
try {
|
||||
const a = input.a || 5;
|
||||
const b = input.b || 3;
|
||||
const expected = input.expected || 8;
|
||||
|
||||
console.log(`测试加法: ${a} + ${b} = ${expected}`);
|
||||
assertEquals(a + b, expected);
|
||||
|
||||
console.log(green("✓ 加法测试通过"));
|
||||
results.push({ name: "加法测试", success: true });
|
||||
} catch (error) {
|
||||
console.log(red(`✗ 加法测试失败: ${error.message}`));
|
||||
results.push({ name: "加法测试", success: false, error: error.message });
|
||||
}
|
||||
|
||||
// 测试2: 字符串操作
|
||||
try {
|
||||
const str1 = input.str1 || "hello";
|
||||
const str2 = input.str2 || "world";
|
||||
const expectedStr = input.expectedStr || "hello world";
|
||||
|
||||
console.log(`测试字符串连接: "${str1}" + " " + "${str2}" = "${expectedStr}"`);
|
||||
assertEquals(`${str1} ${str2}`, expectedStr);
|
||||
|
||||
console.log(green("✓ 字符串测试通过"));
|
||||
results.push({ name: "字符串测试", success: true });
|
||||
} catch (error) {
|
||||
console.log(red(`✗ 字符串测试失败: ${error.message}`));
|
||||
results.push({ name: "字符串测试", success: false, error: error.message });
|
||||
}
|
||||
|
||||
// 等待再次展示结果
|
||||
console.log("处理结果中...");
|
||||
await delay(500);
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
export default async function handler(input) {
|
||||
console.log(bold("JSR依赖示例"));
|
||||
|
||||
// 运行测试
|
||||
const testResults = await runTests(input);
|
||||
|
||||
// 统计结果
|
||||
const passed = testResults.filter(r => r.success).length;
|
||||
const failed = testResults.filter(r => !r.success).length;
|
||||
|
||||
// 使用彩色输出显示结果
|
||||
console.log(bold("\n测试结果摘要:"));
|
||||
console.log(green(`通过: ${passed}`));
|
||||
console.log(failed > 0 ? red(`失败: ${failed}`) : green(`失败: ${failed}`));
|
||||
|
||||
return {
|
||||
message: '测试完成',
|
||||
results: testResults,
|
||||
summary: {
|
||||
total: testResults.length,
|
||||
passed,
|
||||
failed
|
||||
}
|
||||
};
|
||||
}
|
||||
74
qiming-run_code_rmcp/fixtures/import_local_module_example.js
Normal file
74
qiming-run_code_rmcp/fixtures/import_local_module_example.js
Normal file
@@ -0,0 +1,74 @@
|
||||
// 首先创建一个本地模块,通过相对路径引入
|
||||
// 注意:这个示例假设有一个名为utils.js的本地模块
|
||||
|
||||
// 模拟本地utils.js模块的内容
|
||||
const utils = {
|
||||
formatNumber(num) {
|
||||
return num.toLocaleString('zh-CN');
|
||||
},
|
||||
|
||||
calculateTax(amount, rate = 0.1) {
|
||||
return amount * rate;
|
||||
},
|
||||
|
||||
generateId() {
|
||||
return `id-${Date.now()}-${Math.floor(Math.random() * 1000)}`;
|
||||
},
|
||||
|
||||
formatDate(date = new Date()) {
|
||||
return date.toLocaleDateString('zh-CN');
|
||||
}
|
||||
};
|
||||
|
||||
// 使用本地模块的函数
|
||||
function processOrder(order) {
|
||||
console.log(`处理订单: ${order.id || utils.generateId()}`);
|
||||
|
||||
// 计算订单总额
|
||||
const total = order.items.reduce((sum, item) => sum + (item.price * item.quantity), 0);
|
||||
console.log(`订单总额: ${utils.formatNumber(total)}`);
|
||||
|
||||
// 计算税费
|
||||
const taxRate = order.taxRate || 0.13;
|
||||
const tax = utils.calculateTax(total, taxRate);
|
||||
console.log(`税费(${taxRate * 100}%): ${utils.formatNumber(tax)}`);
|
||||
|
||||
// 计算最终金额
|
||||
const finalAmount = total + tax;
|
||||
console.log(`最终金额: ${utils.formatNumber(finalAmount)}`);
|
||||
|
||||
// 生成订单日期
|
||||
const orderDate = order.date ? new Date(order.date) : new Date();
|
||||
console.log(`订单日期: ${utils.formatDate(orderDate)}`);
|
||||
|
||||
return {
|
||||
id: order.id || utils.generateId(),
|
||||
items: order.items,
|
||||
subtotal: total,
|
||||
tax: tax,
|
||||
total: finalAmount,
|
||||
date: utils.formatDate(orderDate)
|
||||
};
|
||||
}
|
||||
|
||||
export default async function handler(input) {
|
||||
console.log("开始处理订单");
|
||||
|
||||
// 默认订单或从输入获取
|
||||
const order = input.order || {
|
||||
items: [
|
||||
{ name: "产品A", price: 100, quantity: 2 },
|
||||
{ name: "产品B", price: 50, quantity: 1 },
|
||||
{ name: "产品C", price: 200, quantity: 3 }
|
||||
],
|
||||
taxRate: 0.13
|
||||
};
|
||||
|
||||
// 处理订单
|
||||
const processedOrder = processOrder(order);
|
||||
|
||||
return {
|
||||
message: '订单处理完成',
|
||||
order: processedOrder
|
||||
};
|
||||
}
|
||||
43
qiming-run_code_rmcp/fixtures/import_lodash_example.js
Normal file
43
qiming-run_code_rmcp/fixtures/import_lodash_example.js
Normal file
@@ -0,0 +1,43 @@
|
||||
import _ from 'npm:lodash';
|
||||
|
||||
// 使用lodash处理数据的函数
|
||||
function processData(input) {
|
||||
console.log("输入数据:", input);
|
||||
|
||||
// 使用lodash的chunk方法将数组分块
|
||||
if (Array.isArray(input.data)) {
|
||||
const chunks = _.chunk(input.data, input.chunkSize || 2);
|
||||
console.log("数据分块结果:", chunks);
|
||||
|
||||
// 使用lodash的map方法处理每个块
|
||||
const processedChunks = _.map(chunks, (chunk) => {
|
||||
return {
|
||||
items: chunk,
|
||||
sum: _.sum(chunk),
|
||||
average: _.mean(chunk)
|
||||
};
|
||||
});
|
||||
|
||||
return processedChunks;
|
||||
}
|
||||
|
||||
return { error: "输入数据不是数组" };
|
||||
}
|
||||
|
||||
export default async function handler(input) {
|
||||
console.log("开始处理数据");
|
||||
|
||||
// 从输入获取数据,或使用默认数据
|
||||
const data = input.data || [1, 2, 3, 4, 5, 6, 7, 8, 9];
|
||||
const chunkSize = input.chunkSize || 3;
|
||||
|
||||
// 处理数据
|
||||
const result = processData({ data, chunkSize });
|
||||
|
||||
return {
|
||||
message: '数据处理完成',
|
||||
originalData: data,
|
||||
processedData: result,
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
}
|
||||
13
qiming-run_code_rmcp/fixtures/rfunction_js_test3.js
Normal file
13
qiming-run_code_rmcp/fixtures/rfunction_js_test3.js
Normal file
@@ -0,0 +1,13 @@
|
||||
//引入js组件,必须是http(s)形式引入,可以去网站: https://deno.land/x 搜索自己所需组件
|
||||
//下面是一个引入示例
|
||||
//import * as o from https://deno.land/x/cowsay/mod.ts
|
||||
//网络请求,可以直接使用fetch ,具体自行查阅 fetch 文档
|
||||
//打印日志使用console,比如:console.log
|
||||
|
||||
// 入口函数不可修改,否则无法执行,args 为配置的入参
|
||||
export default async function main(args) {
|
||||
// 构建输出对象,出参中的key需与配置的出参保持一致
|
||||
return {
|
||||
key: 'value',
|
||||
};
|
||||
}
|
||||
26
qiming-run_code_rmcp/fixtures/rfunction_python.py
Normal file
26
qiming-run_code_rmcp/fixtures/rfunction_python.py
Normal file
@@ -0,0 +1,26 @@
|
||||
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
|
||||
else:
|
||||
print(f"请求失败,状态码:")
|
||||
|
||||
|
||||
# 入口函数不可修改,否则无法执行,args 为配置的入参
|
||||
def main(args: dict) -> dict:
|
||||
params = args.get("params")
|
||||
# 构建输出对象,出参中的key需与配置的出参保持一致
|
||||
ret = {
|
||||
"key": "value"
|
||||
}
|
||||
return ret
|
||||
20
qiming-run_code_rmcp/fixtures/rfunction_test1.js
Normal file
20
qiming-run_code_rmcp/fixtures/rfunction_test1.js
Normal file
@@ -0,0 +1,20 @@
|
||||
// 使用类型注解时应确保使用的是支持该特性的环境或编译器(如 TypeScript)
|
||||
function add(input) {
|
||||
console.log("test Add", input.a, "+", input.b);
|
||||
|
||||
// 简单的字符串输出,不使用外部模块
|
||||
console.log("Hello from JavaScript!");
|
||||
|
||||
return input.a + input.b;
|
||||
}
|
||||
|
||||
function handler(input) {
|
||||
console.log("输入参数:", JSON.stringify(input));
|
||||
|
||||
let a = add(input);
|
||||
console.log("计算结果=" + a);
|
||||
|
||||
return {
|
||||
message: a,
|
||||
};
|
||||
}
|
||||
40
qiming-run_code_rmcp/fixtures/rfunction_test2.py
Normal file
40
qiming-run_code_rmcp/fixtures/rfunction_test2.py
Normal file
@@ -0,0 +1,40 @@
|
||||
#!/usr/bin/env python3
|
||||
import pandas as pd
|
||||
|
||||
import logging
|
||||
import json
|
||||
|
||||
# 创建一个简单的数据结构,不使用pandas
|
||||
data = {
|
||||
'Name': ['Alice', 'Bob', 'Charlie'],
|
||||
'Age': [25, 30, 35]
|
||||
}
|
||||
df = pd.DataFrame(data)
|
||||
# 记录数据信息
|
||||
logging.info(f"Created data structure:\n{json.dumps(data, indent=2)}")
|
||||
|
||||
def main(args: dict) -> dict:
|
||||
logging.info(f"input args: {args}")
|
||||
|
||||
params = args.get("params")
|
||||
|
||||
# 处理params为None的情况
|
||||
if params is None:
|
||||
logging.warning("params is None, using default values")
|
||||
params = {"input": "default_value"}
|
||||
elif not isinstance(params, dict):
|
||||
logging.warning(f"params is not a dictionary: {type(params)}, using default values")
|
||||
params = {"input": str(params)}
|
||||
elif "input" not in params:
|
||||
logging.warning("input key not found in params, using default value")
|
||||
params["input"] = "default_value"
|
||||
|
||||
# 构建输出对象
|
||||
ret = {
|
||||
"key0": params['input'], # 使用input参数值
|
||||
"key1": ["hello", "world"], # 输出一个数组
|
||||
"key2": { # 输出一个Object
|
||||
"key21": "hi"
|
||||
},
|
||||
}
|
||||
return ret
|
||||
25
qiming-run_code_rmcp/fixtures/test_js.js
Normal file
25
qiming-run_code_rmcp/fixtures/test_js.js
Normal file
@@ -0,0 +1,25 @@
|
||||
// Sample JavaScript file with handler function
|
||||
|
||||
// Log some debug information
|
||||
console.log("Initializing JavaScript test...");
|
||||
|
||||
// Define a simple add function
|
||||
function add(a, b) {
|
||||
console.log(`Adding ${a} + ${b}`);
|
||||
return a + b;
|
||||
}
|
||||
|
||||
// Some processing
|
||||
const numbers = [1, 2, 3, 4, 5];
|
||||
console.log("Processing numbers:", numbers);
|
||||
|
||||
// Handler function that will be called to get the result
|
||||
function handler() {
|
||||
console.log("Handler function called");
|
||||
|
||||
// Calculate sum
|
||||
const sum = numbers.reduce((acc, num) => add(acc, num), 0);
|
||||
console.log("Final calculation completed");
|
||||
|
||||
return `The sum of [${numbers.join(", ")}] is ${sum}`;
|
||||
}
|
||||
75
qiming-run_code_rmcp/fixtures/test_js_large_params.js
Normal file
75
qiming-run_code_rmcp/fixtures/test_js_large_params.js
Normal file
@@ -0,0 +1,75 @@
|
||||
// Test JavaScript script with large text parameters to verify "Argument list too long" fix
|
||||
|
||||
// Generate a large text parameter (>2MB) to test the temporary file parameter passing solution
|
||||
function generateLargeText(sizeInMB) {
|
||||
const chunk = "This is a test chunk of text designed to generate large parameters. ".repeat(100);
|
||||
const iterations = Math.ceil((sizeInMB * 1024 * 1024) / chunk.length);
|
||||
|
||||
let result = "";
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
result += chunk + `Iteration ${i}\n`;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Handler function that processes large text parameters
|
||||
function handler(args) {
|
||||
console.log("Handler function called with large parameters");
|
||||
|
||||
// Test that we received the large text parameter
|
||||
if (!args || !args.largeText) {
|
||||
console.log("ERROR: No largeText parameter found");
|
||||
return { success: false, error: "Missing largeText parameter" };
|
||||
}
|
||||
|
||||
const largeText = args.largeText;
|
||||
console.log("Received large text parameter, length:", largeText.length);
|
||||
console.log("Large text parameter size (MB):", (largeText.length / 1024 / 1024).toFixed(2));
|
||||
|
||||
// Verify the text contains expected content
|
||||
if (largeText.includes("This is a test chunk of text designed to generate large parameters")) {
|
||||
console.log("SUCCESS: Large text parameter contains expected content");
|
||||
|
||||
// Count occurrences of "Iteration" to verify data integrity
|
||||
const iterationCount = (largeText.match(/Iteration \d+/g) || []).length;
|
||||
console.log("Found", iterationCount, "iterations in the text");
|
||||
|
||||
// Return some statistics about the processed data
|
||||
return {
|
||||
success: true,
|
||||
message: "Successfully processed large text parameter",
|
||||
textSize: largeText.length,
|
||||
sizeMB: (largeText.length / 1024 / 1024).toFixed(2),
|
||||
iterationCount: iterationCount,
|
||||
sampleText: largeText.substring(0, 100) + "..."
|
||||
};
|
||||
} else {
|
||||
console.log("ERROR: Large text parameter does not contain expected content");
|
||||
return { success: false, error: "Invalid content in largeText parameter" };
|
||||
}
|
||||
}
|
||||
|
||||
// This part won't be executed when the code is run through the MCP runner
|
||||
if (typeof require !== 'undefined' && require.main === module) {
|
||||
console.log("Running large parameter test directly...");
|
||||
|
||||
// Generate test data (3MB to ensure we exceed the 2MB threshold)
|
||||
const largeText = generateLargeText(3);
|
||||
console.log("Generated test data, size:", (largeText.length / 1024 / 1024).toFixed(2), "MB");
|
||||
|
||||
// Create test arguments
|
||||
const testArgs = {
|
||||
largeText: largeText,
|
||||
additionalParam: "test value",
|
||||
numberParam: 42
|
||||
};
|
||||
|
||||
console.log("Calling handler with large parameters...");
|
||||
const result = handler(testArgs);
|
||||
console.log("Result:", JSON.stringify(result, null, 2));
|
||||
}
|
||||
|
||||
if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
|
||||
module.exports = { handler, generateLargeText };
|
||||
}
|
||||
39
qiming-run_code_rmcp/fixtures/test_js_params.js
Normal file
39
qiming-run_code_rmcp/fixtures/test_js_params.js
Normal file
@@ -0,0 +1,39 @@
|
||||
// JavaScript测试文件,演示参数传递
|
||||
|
||||
// 打印一些调试信息
|
||||
console.log("JavaScript脚本开始执行...");
|
||||
|
||||
// 定义一个加法函数
|
||||
function add(a, b) {
|
||||
console.log(`正在计算: ${a} + ${b}`);
|
||||
return a + b;
|
||||
}
|
||||
|
||||
// 处理一些数据
|
||||
const numbers = [1, 2, 3, 4, 5];
|
||||
console.log(`处理数字列表: ${numbers}`);
|
||||
|
||||
/**
|
||||
* 处理函数,接收参数并返回结果
|
||||
* 注意:这个函数必须存在,并且会被框架调用来获取结果
|
||||
*/
|
||||
function handler(input) {
|
||||
console.log(`接收到的参数: ${JSON.stringify(input)}`);
|
||||
|
||||
// 从参数中获取值,提供默认值
|
||||
const a = input.a || 0;
|
||||
const b = input.b || 0;
|
||||
const name = input.name || "用户";
|
||||
|
||||
// 计算结果
|
||||
const result = add(a, b);
|
||||
console.log(`计算完成: ${a} + ${b} = ${result}`);
|
||||
|
||||
// 返回结果
|
||||
return {
|
||||
sum: result,
|
||||
numbers: numbers,
|
||||
greeting: `你好,${name}!`,
|
||||
message: `成功计算 ${a} + ${b} 的结果`
|
||||
};
|
||||
}
|
||||
41
qiming-run_code_rmcp/fixtures/test_python.py
Normal file
41
qiming-run_code_rmcp/fixtures/test_python.py
Normal file
@@ -0,0 +1,41 @@
|
||||
#!/usr/bin/env python3
|
||||
# Sample Python file with handler function
|
||||
|
||||
import time
|
||||
time.time()
|
||||
# Debug print to confirm script execution
|
||||
print("DEBUGGING: Script is being executed!")
|
||||
|
||||
# Log some debug information
|
||||
print("Initializing Python test...")
|
||||
|
||||
# Define a simple function
|
||||
def multiply(a, b):
|
||||
print(f"Multiplying {a} * {b}")
|
||||
return a * b
|
||||
|
||||
# Some processing
|
||||
numbers = [1, 2, 3, 4, 5]
|
||||
print(f"Processing numbers: {numbers}")
|
||||
|
||||
# Handler function that will be called to get the result
|
||||
def handler(args):
|
||||
print("Handler function called")
|
||||
print(f"Received args: {args}")
|
||||
|
||||
# Calculate product (using the predefined numbers)
|
||||
product = 1
|
||||
for num in numbers:
|
||||
product = multiply(product, num)
|
||||
|
||||
print("Final calculation completed")
|
||||
|
||||
return f"The product of {numbers} is {product}"
|
||||
|
||||
# This part won't be executed when the code is run through the MCP runner
|
||||
if __name__ == "__main__":
|
||||
print("Running directly, calling handler...")
|
||||
# Create sample args for direct execution
|
||||
sample_args = {"test": "direct_execution", "data": [1, 2, 3]}
|
||||
result = handler(sample_args)
|
||||
print(f"Result: {result}")
|
||||
70
qiming-run_code_rmcp/fixtures/test_python_large_params.py
Normal file
70
qiming-run_code_rmcp/fixtures/test_python_large_params.py
Normal file
@@ -0,0 +1,70 @@
|
||||
#!/usr/bin/env python3
|
||||
# Test Python script with large text parameters to verify "Argument list too long" fix
|
||||
|
||||
import sys
|
||||
import json
|
||||
import time
|
||||
|
||||
# Generate a large text parameter (>2MB) to test the temporary file parameter passing solution
|
||||
def generate_large_text(size_in_mb):
|
||||
chunk = "This is a test chunk of text designed to generate large parameters for Python testing. " * 100
|
||||
iterations = (size_in_mb * 1024 * 1024) // len(chunk)
|
||||
|
||||
result = ""
|
||||
for i in range(iterations):
|
||||
result += chunk + f"Iteration {i}\n"
|
||||
|
||||
return result
|
||||
|
||||
# Handler function that processes large text parameters
|
||||
def handler(args):
|
||||
print("Handler function called with large parameters")
|
||||
|
||||
# Test that we received the large text parameter
|
||||
if not args or 'largeText' not in args:
|
||||
print("ERROR: No largeText parameter found")
|
||||
return {"success": False, "error": "Missing largeText parameter"}
|
||||
|
||||
large_text = args['largeText']
|
||||
print(f"Received large text parameter, length: {len(large_text)}")
|
||||
print(f"Large text parameter size (MB): {len(large_text) / 1024 / 1024:.2f}")
|
||||
|
||||
# Verify the text contains expected content
|
||||
expected_content = "This is a test chunk of text designed to generate large parameters for Python testing"
|
||||
if expected_content in large_text:
|
||||
print("SUCCESS: Large text parameter contains expected content")
|
||||
|
||||
# Count occurrences of "Iteration" to verify data integrity
|
||||
iteration_count = large_text.count("Iteration")
|
||||
print(f"Found {iteration_count} iterations in the text")
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": "Successfully processed large text parameter",
|
||||
"textSize": len(large_text),
|
||||
"sizeMB": round(len(large_text) / 1024 / 1024, 2),
|
||||
"iterationCount": iteration_count,
|
||||
"sampleText": large_text[:100] + "..."
|
||||
}
|
||||
else:
|
||||
print("ERROR: Large text parameter does not contain expected content")
|
||||
return {"success": False, "error": "Invalid content in largeText parameter"}
|
||||
|
||||
# This part won't be executed when the code is run through the MCP runner
|
||||
if __name__ == "__main__":
|
||||
print("Running large parameter test directly...")
|
||||
|
||||
# Generate test data (3MB to ensure we exceed the 2MB threshold)
|
||||
large_text = generate_large_text(3)
|
||||
print(f"Generated test data, size: {len(large_text) / 1024 / 1024:.2f} MB")
|
||||
|
||||
# Create test arguments
|
||||
test_args = {
|
||||
"largeText": large_text,
|
||||
"additionalParam": "test value",
|
||||
"numberParam": 42
|
||||
}
|
||||
|
||||
print("Calling handler with large parameters...")
|
||||
result = handler(test_args)
|
||||
print("Result:", json.dumps(result, indent=2))
|
||||
36
qiming-run_code_rmcp/fixtures/test_python_logging.py
Normal file
36
qiming-run_code_rmcp/fixtures/test_python_logging.py
Normal file
@@ -0,0 +1,36 @@
|
||||
#!/usr/bin/env python3
|
||||
# 测试logging模块的日志捕获功能
|
||||
|
||||
import logging
|
||||
import json
|
||||
|
||||
# 配置日志
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
|
||||
# 使用不同级别的日志
|
||||
def log_messages():
|
||||
logging.debug("这是一条DEBUG级别的日志")
|
||||
logging.info("这是一条INFO级别的日志")
|
||||
logging.warning("这是一条WARNING级别的日志")
|
||||
logging.error("这是一条ERROR级别的日志")
|
||||
logging.critical("这是一条CRITICAL级别的日志")
|
||||
|
||||
# 使用格式化
|
||||
data = {"name": "测试", "value": 42}
|
||||
logging.info(f"格式化的JSON数据: {json.dumps(data, ensure_ascii=False)}")
|
||||
|
||||
return "日志测试完成"
|
||||
|
||||
def handler(args):
|
||||
# 记录输入参数
|
||||
logging.info(f"收到参数: {args}")
|
||||
|
||||
# 生成一些日志
|
||||
result = log_messages()
|
||||
|
||||
# 返回结果
|
||||
return {
|
||||
"message": result,
|
||||
"log_count": 6, # 总共记录了6条日志
|
||||
"args": args
|
||||
}
|
||||
33
qiming-run_code_rmcp/fixtures/test_python_params.py
Normal file
33
qiming-run_code_rmcp/fixtures/test_python_params.py
Normal file
@@ -0,0 +1,33 @@
|
||||
import json
|
||||
import os
|
||||
|
||||
# 打印一些调试信息
|
||||
print("Python脚本开始执行...")
|
||||
|
||||
# 定义一个简单的加法函数
|
||||
def add(a, b):
|
||||
print("正在计算: " + str(a) + " + " + str(b))
|
||||
return a + b
|
||||
|
||||
# 处理一些数据
|
||||
numbers = [1, 2, 3, 4, 5]
|
||||
print("处理数字列表: " + str(numbers))
|
||||
|
||||
# 入口函数,接收参数
|
||||
def main(args):
|
||||
print("接收到的参数: " + str(args))
|
||||
|
||||
# 从参数中获取值
|
||||
a = args.get("a", 0)
|
||||
b = args.get("b", 0)
|
||||
|
||||
# 计算结果
|
||||
result = add(a, b)
|
||||
print("计算完成: " + str(a) + " + " + str(b) + " = " + str(result))
|
||||
|
||||
# 返回结果
|
||||
return {
|
||||
"sum": result,
|
||||
"numbers": numbers,
|
||||
"message": "成功计算 " + str(a) + " + " + str(b) + " 的结果"
|
||||
}
|
||||
23
qiming-run_code_rmcp/fixtures/test_python_simple.py
Normal file
23
qiming-run_code_rmcp/fixtures/test_python_simple.py
Normal file
@@ -0,0 +1,23 @@
|
||||
#!/usr/bin/env python3
|
||||
# 简单的Python测试脚本,用于测试参数传递机制
|
||||
|
||||
def main(args: dict) -> dict:
|
||||
print(f"接收到的参数: {args}")
|
||||
|
||||
# 尝试通过两种方式获取参数
|
||||
direct_params = args.get("input", "direct_default")
|
||||
nested_params = None
|
||||
|
||||
params = args.get("params")
|
||||
if params:
|
||||
if isinstance(params, dict):
|
||||
nested_params = params.get("input", "nested_default")
|
||||
else:
|
||||
nested_params = str(params)
|
||||
|
||||
# 返回结果,展示两种方式获取的参数
|
||||
return {
|
||||
"direct_access": direct_params,
|
||||
"nested_access": nested_params,
|
||||
"args_structure": args
|
||||
}
|
||||
46
qiming-run_code_rmcp/fixtures/test_python_types.py
Normal file
46
qiming-run_code_rmcp/fixtures/test_python_types.py
Normal file
@@ -0,0 +1,46 @@
|
||||
import json
|
||||
import os
|
||||
|
||||
# 打印一些调试信息
|
||||
print("Python类型测试脚本开始执行...")
|
||||
|
||||
# 入口函数,接收参数并返回不同类型的值
|
||||
def main(args):
|
||||
print("接收到的参数:", args)
|
||||
|
||||
# 获取要测试的类型
|
||||
test_type = args.get("type", "string")
|
||||
|
||||
# 根据参数返回不同类型的值
|
||||
if test_type == "string":
|
||||
print("返回字符串类型")
|
||||
return "这是一个字符串"
|
||||
|
||||
elif test_type == "number":
|
||||
print("返回数字类型")
|
||||
return 12345
|
||||
|
||||
elif test_type == "boolean":
|
||||
print("返回布尔类型")
|
||||
return True
|
||||
|
||||
elif test_type == "null":
|
||||
print("返回None类型")
|
||||
return None
|
||||
|
||||
elif test_type == "list":
|
||||
print("返回列表类型")
|
||||
return [1, 2, 3, "四", "五", True]
|
||||
|
||||
elif test_type == "dict":
|
||||
print("返回字典类型")
|
||||
return {
|
||||
"name": "测试用户",
|
||||
"age": 30,
|
||||
"is_active": True,
|
||||
"tags": ["python", "rust", "json"]
|
||||
}
|
||||
|
||||
else:
|
||||
print("未知类型,返回默认字符串")
|
||||
return "未知类型"
|
||||
41
qiming-run_code_rmcp/fixtures/test_ts.ts
Normal file
41
qiming-run_code_rmcp/fixtures/test_ts.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
// Sample TypeScript file with handler function
|
||||
|
||||
// Log some debug information
|
||||
console.log("Initializing TypeScript test...");
|
||||
|
||||
// Define a simple add function with type annotations
|
||||
function add(a: number, b: number): number {
|
||||
console.log(`Adding ${a} + ${b}`);
|
||||
return a + b;
|
||||
}
|
||||
|
||||
// Some processing with type annotations
|
||||
const numbers: number[] = [1, 2, 3, 4, 5];
|
||||
console.log("Processing numbers:", numbers);
|
||||
|
||||
// Interface for a person object
|
||||
interface Person {
|
||||
name: string;
|
||||
age: number;
|
||||
}
|
||||
|
||||
// Create a person object
|
||||
const person: Person = {
|
||||
name: "TypeScript User",
|
||||
age: 30
|
||||
};
|
||||
console.log("Person object:", person);
|
||||
|
||||
/**
|
||||
* Handler function that will be called to get the result
|
||||
* 注意:这个函数必须存在,并且会被框架调用来获取结果
|
||||
*/
|
||||
function handler(): string {
|
||||
console.log("Handler function called");
|
||||
|
||||
// Calculate sum
|
||||
const sum = numbers.reduce((acc, num) => add(acc, num), 0);
|
||||
console.log("Final calculation completed");
|
||||
|
||||
return `Hello ${person.name}! The sum of [${numbers.join(", ")}] is ${sum}`;
|
||||
}
|
||||
46
qiming-run_code_rmcp/fixtures/test_ts_params.ts
Normal file
46
qiming-run_code_rmcp/fixtures/test_ts_params.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
// TypeScript测试文件,演示参数传递
|
||||
|
||||
// 定义参数接口
|
||||
interface InputParams {
|
||||
a: number;
|
||||
b: number;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
// 打印一些调试信息
|
||||
console.log("TypeScript脚本开始执行...");
|
||||
|
||||
// 定义一个带类型的加法函数
|
||||
function add(a: number, b: number): number {
|
||||
console.log(`正在计算: ${a} + ${b}`);
|
||||
return a + b;
|
||||
}
|
||||
|
||||
// 处理一些数据
|
||||
const numbers: number[] = [1, 2, 3, 4, 5];
|
||||
console.log(`处理数字列表: ${numbers}`);
|
||||
|
||||
/**
|
||||
* 处理函数,接收参数并返回结果
|
||||
* 注意:这个函数必须存在,并且会被框架调用来获取结果
|
||||
*/
|
||||
function handler(input: InputParams): object {
|
||||
console.log(`接收到的参数: ${JSON.stringify(input)}`);
|
||||
|
||||
// 从参数中获取值,提供默认值
|
||||
const a = input.a || 0;
|
||||
const b = input.b || 0;
|
||||
const name = input.name || "用户";
|
||||
|
||||
// 计算结果
|
||||
const result = add(a, b);
|
||||
console.log(`计算完成: ${a} + ${b} = ${result}`);
|
||||
|
||||
// 返回结果
|
||||
return {
|
||||
sum: result,
|
||||
numbers: numbers,
|
||||
greeting: `你好,${name}!`,
|
||||
message: `成功计算 ${a} + ${b} 的结果`
|
||||
};
|
||||
}
|
||||
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